perf(线上代发价格): 成本核查改为全量预计算缓存,页面打开不再现算 #862
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)]));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user