diff --git a/app/Console/Commands/SyncDingOnlineDaifaPrice.php b/app/Console/Commands/SyncDingOnlineDaifaPrice.php index b33cc6a0..bfb2f3ba 100644 --- a/app/Console/Commands/SyncDingOnlineDaifaPrice.php +++ b/app/Console/Commands/SyncDingOnlineDaifaPrice.php @@ -4,6 +4,7 @@ namespace App\Console\Commands; use Illuminate\Console\Command; use Illuminate\Support\Facades\Log; +use Modules\EcommerceFinance\Repositories\DingOnlineDaifaPriceMirrorRepository; use Modules\EcommerceFinance\Services\DingPayableWorkflowMirrorSynchronizer; use Throwable; @@ -44,6 +45,16 @@ class SyncDingOnlineDaifaPrice extends Command $result['rows'], )); + // 同步完成后预先算好成本核查结果,页面打开时直接读缓存。 + if ($result['status'] === 'succeeded') { + try { + $this->info(sprintf('成本核查已预计算:%d 行。', app(DingOnlineDaifaPriceMirrorRepository::class)->warmAuditCache())); + } catch (Throwable $exception) { + report($exception); + $this->warn('成本核查预计算失败,页面打开时会重新计算。'); + } + } + return self::SUCCESS; } } diff --git a/modules/EcommerceFinance/Repositories/DingOnlineDaifaPriceMirrorRepository.php b/modules/EcommerceFinance/Repositories/DingOnlineDaifaPriceMirrorRepository.php index 3e92c34e..332b3c36 100644 --- a/modules/EcommerceFinance/Repositories/DingOnlineDaifaPriceMirrorRepository.php +++ b/modules/EcommerceFinance/Repositories/DingOnlineDaifaPriceMirrorRepository.php @@ -55,7 +55,10 @@ class DingOnlineDaifaPriceMirrorRepository /** 报销金额比系统成本高出超过此值即判为多报销(元)。 */ private const AUDIT_TOLERANCE = 0.01; - private const AUDIT_CACHE_SECONDS = 600; + /** 核查结果全量缓存:最长 6 小时重算一次;数据有变化时先用旧结果、响应后后台重算。 */ + private const AUDIT_CACHE_SECONDS = 21600; + + private const AUDIT_CACHE_KEY = 'online-daifa:audit-map:v2'; private readonly ConnectionInterface $connection; @@ -405,6 +408,18 @@ class DingOnlineDaifaPriceMirrorRepository } } + /** + * 预先算好全部钉钉行的成本核查结果并缓存(同步命令和发布后调用),页面打开时直接读缓存。 + */ + public function warmAuditCache(): int + { + if (! (bool) ($this->sourceStatuses()['ding']['ready'] ?? false)) { + return 0; + } + + return count($this->auditMap(true)); + } + /** * 按线上订单号(钉钉行)或内部单号(采购单行)批量关联聚水潭订单,补充: * - ship_skus:实际发货商品编码(组合装拆开后的子商品编码 × 数量); @@ -944,49 +959,102 @@ class DingOnlineDaifaPriceMirrorRepository } /** - * 当前筛选条件下全部钉钉行的核查结果索引(按页面顺序的行键),供“核查结果”筛选、汇总和导出使用。 - * 按筛选条件 + 镜像同步时间缓存 10 分钟。 + * 当前筛选条件下钉钉行的核查结果索引(按页面顺序的行键),供“成本核查”筛选、汇总和导出使用。 + * 核查结果来自全量缓存(auditMap),这里只按筛选条件取行键,不再逐行查聚水潭订单。 * * @return array{keys:array>,counts:array,over_amount:float} */ private function auditIndex(array $filters): array { + $index = ['keys' => array_fill_keys(self::AUDIT_RESULTS, []), 'counts' => array_fill_keys(self::AUDIT_RESULTS, 0), 'over_amount' => 0.0]; $base = $filters; unset($base['audit']); - $synced = (string) ($this->sourceStatuses()['ding']['last_synced_at'] ?? ''); - $key = 'online-daifa:audit-index:v1:' . sha1(json_encode($base) . '|' . $synced); - $cached = Cache::get($key); - if (is_array($cached)) { - return $cached; + $ding = (bool) ($this->sourceStatuses()['ding']['ready'] ?? false) ? $this->dingFiltered($base) : null; + if ($ding === null) { + return $index; } - $index = ['keys' => array_fill_keys(self::AUDIT_RESULTS, []), 'counts' => array_fill_keys(self::AUDIT_RESULTS, 0), 'over_amount' => 0.0]; + $map = $this->auditMap(); + $rows = $ding->orderByDesc('d.workflow_created_at') + ->orderByDesc('d.process_instance_id') + ->orderByDesc('d.detail_index') + ->get(['d.process_instance_id', 'd.detail_index']); + foreach ($rows as $row) { + $key = $row->process_instance_id . '|' . (int) $row->detail_index; + if (! isset($map[$key])) { + continue; + } + [$result, $diff] = $map[$key]; + $index['keys'][$result][] = $key; + $index['counts'][$result]++; + if ($result === self::AUDIT_OVER) { + $index['over_amount'] += (float) $diff; + } + } + $index['over_amount'] = round($index['over_amount'], 2); + + return $index; + } + + /** + * 全部钉钉行的核查结果:“流程ID|明细序号” => [结果, 报销−成本]。 + * 按数据指纹(行数、最新创建时间、金额合计)缓存最多 6 小时;指纹变了而旧缓存还在时, + * 页面先用旧结果,响应结束后在后台重算,避免让打开页面的人等待。 + * + * @return array + */ + private function auditMap(bool $force = false): array + { + $fingerprint = $this->auditFingerprint(); + $cached = null; + try { + $cached = Cache::get(self::AUDIT_CACHE_KEY); + } catch (\Throwable $exception) { + report($exception); + } + $fresh = is_array($cached) + && ($cached['fingerprint'] ?? null) === $fingerprint + && (int) ($cached['built_at'] ?? 0) > time() - self::AUDIT_CACHE_SECONDS; + if ($fresh && ! $force) { + return (array) $cached['map']; + } + + if (is_array($cached) && ! $force && ! app()->runningInConsole()) { + try { + if (Cache::add(self::AUDIT_CACHE_KEY . ':rebuilding', 1, 600)) { + app()->terminating(function (): void { + try { + app(self::class)->auditMap(true); + } catch (\Throwable $exception) { + report($exception); + } finally { + Cache::forget(self::AUDIT_CACHE_KEY . ':rebuilding'); + } + }); + } + } catch (\Throwable $exception) { + report($exception); + } + + return (array) $cached['map']; + } + + $map = []; $buffer = []; - $flush = function () use (&$buffer, &$index): void { + $flush = function () use (&$buffer, &$map): void { if ($buffer === []) { return; } $this->attachJstOrderDetails($buffer); $this->attachCostAudit($buffer); foreach ($buffer as $row) { - $result = (string) $row['audit_result']; - if ($result === '') { - continue; - } - $index['keys'][$result][] = $row['process_instance_id'] . '|' . $row['detail_index']; - $index['counts'][$result]++; - if ($result === self::AUDIT_OVER) { - $index['over_amount'] += (float) $row['cost_diff']; + if ((string) $row['audit_result'] !== '') { + $map[$row['process_instance_id'] . '|' . $row['detail_index']] = [(string) $row['audit_result'], $row['cost_diff']]; } } $buffer = []; }; - // 只核查钉钉行:直接查钉钉镜像,不再经过与采购单整表合并的派生表。 - $ding = (bool) ($this->sourceStatuses()['ding']['ready'] ?? false) ? $this->dingFiltered($base) : null; - $rows = $ding === null ? [] : $ding - ->orderByDesc('d.workflow_created_at') - ->orderByDesc('d.process_instance_id') - ->orderByDesc('d.detail_index') + $rows = $this->connection->table(self::TARGET_TABLE . ' as d') ->get(['d.process_instance_id', 'd.detail_index', 'd.shop_trade_no', 'd.material_no', 'd.daifa_amount']); foreach ($rows as $row) { $buffer[] = [ @@ -1002,15 +1070,23 @@ class DingOnlineDaifaPriceMirrorRepository } } $flush(); - $index['over_amount'] = round($index['over_amount'], 2); try { - Cache::put($key, $index, self::AUDIT_CACHE_SECONDS); + Cache::put(self::AUDIT_CACHE_KEY, ['fingerprint' => $fingerprint, 'built_at' => time(), 'map' => $map], now()->addDay()); } catch (\Throwable $exception) { report($exception); } - return $index; + return $map; + } + + private function auditFingerprint(): string + { + $row = $this->connection->table(self::TARGET_TABLE) + ->selectRaw('COUNT(*) AS c, MAX(workflow_created_at) AS m, COALESCE(SUM(daifa_amount), 0) AS s') + ->first(); + + return sha1(json_encode([(int) ($row->c ?? 0), (string) ($row->m ?? ''), round((float) ($row->s ?? 0), 2)])); } /**