*/ 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 */ 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'), ]; } }