perf(线上代发价格): 成本核查改为全量预计算缓存,页面打开不再现算 #862

Merged
limeng merged 1 commits from feature/limeng-online-daifa-perf into main 2026-11-04 15:16:06 +08:00
2 changed files with 114 additions and 27 deletions

View File

@ -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;
}
}

View File

@ -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<string,list<string>>,counts:array<string,int>,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<string,array{0:string,1:?float}>
*/
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)]));
}
/**