CaiWuXiTong/app/Business/StoreReceiptDifferenceMaterialization.php

218 lines
7.1 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Business;
use App\Http\Controllers\Business\Pages;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
/**
* 「业绩明细」差额规则的物化状态。
*
* 备注分类、有效差额、有/无差额原先都在查询时按同一套规则链逐行现算,整月
* 30 多万行需要十几到几十秒。store-receipt:materialize-differences 把结果写进
* store_receipt_order_match_rows 的 difference_reason_index / base_difference /
* has_difference 三列,页面改读列。
*
* 规则表达式始终取自 Pages 的静态方法(唯一口径),这里只负责:
* - 用规则表达式的指纹判断物化结果是否仍与当前代码一致;
* - 记录/读取每个月是否已物化。
*/
class StoreReceiptDifferenceMaterialization
{
public const REASON_INDEX_COLUMN = 'difference_reason_index';
public const BASE_DIFFERENCE_COLUMN = 'base_difference';
public const HAS_DIFFERENCE_COLUMN = 'has_difference';
private const TABLE = 'store_receipt_order_match_rows';
private const STATE_TABLE = 'store_receipt_order_match_materializations';
/** @var array<string,bool> */
private static array $memo = [];
public static function connection(): string
{
return (string) config('upstream_mirrors.connection', 'business_mirror');
}
public static function table(): string
{
return self::TABLE;
}
public static function stateTable(): string
{
return self::STATE_TABLE;
}
/**
* 当前规则表达式的指纹。规则改动后指纹变化,旧的物化结果会自动视为过期。
*/
public static function ruleVersion(): string
{
return md5(implode('|', [
Pages::receiptDefaultDifferenceSql(),
Pages::receiptDifferenceRemarkIndexSql(),
Pages::receiptEffectiveDifferenceSql(),
]));
}
/**
* 物化列与状态表是否已就绪(迁移是否执行过)。
*
* 结果缓存 10 分钟:schema 探测在共享镜像库上每次要 0.3~0.5 秒,
* 不能放在每个页面请求的热路径上。迁移执行后由 markMaterialized() 主动失效。
*/
public static function available(bool $fresh = false): bool
{
static $memo = null;
if (! $fresh) {
if ($memo !== null) {
return $memo;
}
return $memo = (bool) Cache::remember(
self::availabilityCacheKey(),
now()->addMinutes(10),
static fn (): bool => self::detectAvailability(),
);
}
$available = self::detectAvailability();
if ($available) {
Cache::put(self::availabilityCacheKey(), true, now()->addMinutes(10));
}
return $memo = $available;
}
private static function detectAvailability(): bool
{
try {
$schema = Schema::connection(self::connection());
return $schema->hasTable(self::TABLE)
&& $schema->hasTable(self::STATE_TABLE)
&& $schema->hasColumn(self::TABLE, self::REASON_INDEX_COLUMN)
&& $schema->hasColumn(self::TABLE, self::BASE_DIFFERENCE_COLUMN)
&& $schema->hasColumn(self::TABLE, self::HAS_DIFFERENCE_COLUMN);
} catch (\Throwable) {
return false;
}
}
private static function availabilityCacheKey(): string
{
return 'store-receipt-materialization-available:v1';
}
/**
* 该账期是否已经按当前规则物化完成。未物化时页面回退到实时规则表达式。
*/
public static function materialized(string $month): bool
{
$month = substr(trim($month), 0, 10);
if ($month === '' || ! self::available()) {
return false;
}
if (array_key_exists($month, self::$memo)) {
return self::$memo[$month];
}
$cacheKey = 'store-receipt-materialized:' . $month . ':' . self::ruleVersion();
return self::$memo[$month] = (bool) Cache::remember($cacheKey, now()->addMinutes(10), function () use ($month): bool {
$row = DB::connection(self::connection())
->table(self::STATE_TABLE)
->where('report_month', $month)
->first();
if ($row === null || (string) ($row->rule_version ?? '') !== self::ruleVersion()) {
return false;
}
// 过渡期保护:尚未部署新代码的同步任务重刷账期时不会写物化列,
// 一旦出现 NULL 就回退实时口径,避免页面把未物化的行当成 0 差额。
return ! DB::connection(self::connection())
->table(self::TABLE)
->where('report_month', $month)
->whereNull(self::HAS_DIFFERENCE_COLUMN)
->limit(1)
->exists();
});
}
/**
* 该账期的数据版本:物化后自增,页面聚合/联动统计的缓存键带上它即可精确失效。
*/
public static function dataVersion(string $month): string
{
return (string) Cache::remember(
self::dataVersionKey($month),
now()->addDays(30),
static fn (): int => 1,
);
}
public static function bumpDataVersion(string $month): void
{
$key = self::dataVersionKey($month);
$current = (int) Cache::get($key, 1);
Cache::put($key, $current + 1, now()->addDays(30));
}
private static function dataVersionKey(string $month): string
{
return 'store-receipt-data-version:' . substr(trim($month), 0, 10);
}
public static function markMaterialized(string $month, int $rowCount): void
{
$month = substr(trim($month), 0, 10);
$connection = self::connection();
DB::connection($connection)->table(self::STATE_TABLE)->updateOrInsert(
['report_month' => $month],
[
'rule_version' => self::ruleVersion(),
'row_count' => $rowCount,
'materialized_at' => now(),
],
);
Cache::forget('store-receipt-materialized:' . $month . ':' . self::ruleVersion());
Cache::forget(self::availabilityCacheKey());
self::bumpDataVersion($month);
unset(self::$memo[$month]);
}
public static function forget(string $month): void
{
$month = substr(trim($month), 0, 10);
Cache::forget('store-receipt-materialized:' . $month . ':' . self::ruleVersion());
unset(self::$memo[$month]);
}
/**
* 账期相邻月份(物化跨月备注规则时需要一起重算)。
*
* @return list<string>
*/
public static function adjacentMonths(string $month): array
{
$date = \Illuminate\Support\Carbon::parse(substr($month, 0, 10) . ' 00:00:00');
return [
$date->copy()->subMonthNoOverflow()->format('Y-m-01'),
$date->copy()->format('Y-m-01'),
$date->copy()->addMonthNoOverflow()->format('Y-m-01'),
];
}
}