From 1035da14876c41d5f8662b7d2c36ec08825cb64d Mon Sep 17 00:00:00 2001 From: limeng <1131177064@qq.com> Date: Wed, 4 Nov 2026 15:15:38 +0800 Subject: [PATCH] =?UTF-8?q?perf(=E7=BA=BF=E4=B8=8A=E4=BB=A3=E5=8F=91?= =?UTF-8?q?=E4=BB=B7=E6=A0=BC):=20=E6=88=90=E6=9C=AC=E6=A0=B8=E6=9F=A5?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E5=85=A8=E9=87=8F=E9=A2=84=E8=AE=A1=E7=AE=97?= =?UTF-8?q?=E7=BC=93=E5=AD=98=EF=BC=8C=E9=A1=B5=E9=9D=A2=E6=89=93=E5=BC=80?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E7=8E=B0=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #844 已去掉整表合并;但成本核查仍按“筛选条件+同步时间”缓存 10 分钟,生产 cron 每轮同步都会刷新同步时间, 等于每次打开页面都要把全部钉钉行逐批关联聚水潭订单重算一遍,页面依旧打不开。 - 核查结果按数据指纹(行数/最新创建时间/金额合计)缓存全量结果,最长 6 小时;筛选只取行键再查缓存 - 数据变化时先用旧结果,响应结束后后台重算(加锁防并发) - 每日同步命令完成后预计算成本核查 涉及:只读查询与缓存;不涉及迁移、权限和上游写入。 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VANWYsNUbu2piMtCKymoyd --- .../Commands/SyncDingOnlineDaifaPrice.php | 11 ++ .../DingOnlineDaifaPriceMirrorRepository.php | 130 ++++++++++++++---- 2 files changed, 114 insertions(+), 27 deletions(-) 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)])); } /** -- 2.47.2