218 lines
7.1 KiB
PHP
218 lines
7.1 KiB
PHP
<?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'),
|
||
];
|
||
}
|
||
}
|