fix: 优化快递全量汇总的哈希关联 #150
@ -215,15 +215,26 @@ class CourierFreightReconciliationMirrorRepository
|
||||
// One expensive aggregate at a time, including requests for different filters.
|
||||
$lock = Cache::lock('courier-freight-summary:calculation', 360);
|
||||
if (! $lock->get()) return null;
|
||||
$previousTimeout = null;
|
||||
$previousSettings = [];
|
||||
try {
|
||||
if (is_array($cached = Cache::get($key))) return $cached;
|
||||
if ($this->connection->getDriverName() === 'mysql') {
|
||||
$previousTimeout = $this->connection->selectOne('SELECT @@SESSION.max_statement_time AS value')->value;
|
||||
$this->connection->statement('SET SESSION max_statement_time=300');
|
||||
$settings = ['max_statement_time' => 300];
|
||||
if (array_filter($filters, static fn ($value): bool => trim((string) $value) !== '') === []) {
|
||||
$settings += [
|
||||
'optimizer_switch' => 'derived_merge=off,derived_with_keys=off',
|
||||
'join_cache_level' => 8,
|
||||
'join_buffer_size' => 16777216,
|
||||
'join_buffer_space_limit' => 33554432,
|
||||
];
|
||||
}
|
||||
foreach ($settings as $name => $value) {
|
||||
$previousSettings[$name] = $this->connection->selectOne('SELECT @@SESSION.' . $name . ' AS value')->value;
|
||||
$this->setSummarySessionSetting($name, $value);
|
||||
}
|
||||
}
|
||||
$asOf = now()->toDateTimeString();
|
||||
$row = $this->filteredQuery($filters)->selectRaw(
|
||||
$row = $this->summaryQuery($filters)->selectRaw(
|
||||
'COUNT(d.id) AS aggregate_count, COALESCE(SUM(o.freight), 0) AS freight',
|
||||
)->first();
|
||||
$summary = ['count' => (int) $row->aggregate_count, 'freight' => (float) $row->freight, 'computed_at' => $asOf];
|
||||
@ -231,23 +242,72 @@ class CourierFreightReconciliationMirrorRepository
|
||||
return $summary;
|
||||
} finally {
|
||||
try {
|
||||
if ($previousTimeout !== null) $this->connection->statement('SET SESSION max_statement_time=' . (float) $previousTimeout);
|
||||
foreach (array_reverse($previousSettings, true) as $name => $value) {
|
||||
$this->setSummarySessionSetting($name, $value);
|
||||
}
|
||||
} finally {
|
||||
$lock->release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function setSummarySessionSetting(string $name, mixed $value): void
|
||||
{
|
||||
if ($name === 'optimizer_switch') {
|
||||
$this->connection->statement('SET SESSION optimizer_switch=?', [$value]);
|
||||
return;
|
||||
}
|
||||
// MariaDB rejects quoted numeric values for these session variables.
|
||||
// Names come only from the fixed settings map above.
|
||||
$this->connection->statement('SET SESSION ' . $name . '=' . (float) $value);
|
||||
}
|
||||
|
||||
private function summaryCacheKey(array $filters): string
|
||||
{
|
||||
$filters = array_filter(array_map(static fn ($value): string => trim((string) $value), $filters), static fn (string $value): bool => $value !== '');
|
||||
ksort($filters);
|
||||
return 'courier-freight-summary:v1:' . hash('sha256', json_encode($filters));
|
||||
return 'courier-freight-summary:v3:' . hash('sha256', json_encode($filters));
|
||||
}
|
||||
|
||||
private function filteredQuery(array $filters, bool $includeRelations = true): Builder
|
||||
/** Materialize latest associations once; use compact collation-aware hash joins for the full scan. */
|
||||
private function summaryQuery(array $filters = []): Builder
|
||||
{
|
||||
$query = ($includeRelations ? $this->detailQuery() : $this->connection->table($this->datasets->get(self::DETAILS_DATASET)->targetTable() . ' as d'))
|
||||
$activeFilters = array_filter($filters, static fn ($value): bool => trim((string) $value) !== '');
|
||||
if ($activeFilters !== []) {
|
||||
$ids = $this->filteredQuery($filters, ($filters['express_name'] ?? '') !== '')->select('d.id')->distinct();
|
||||
return $this->detailQuery($ids);
|
||||
}
|
||||
$columns = ['so_id', 'io_id'];
|
||||
$lookup = fn (string $column): string => $this->connection->getDriverName() === 'mysql'
|
||||
? 'CRC32(WEIGHT_STRING(RTRIM(' . $column . ')))'
|
||||
: 'SUBSTR(' . $column . ', 1, 64)';
|
||||
$extension = $this->latestSummaryProjection(self::EXTENSION_DATASET, 'so_id', $columns)
|
||||
->selectRaw($lookup('candidate.so_id') . ' AS lookup_key');
|
||||
$outbound = $this->latestSummaryProjection(self::OUTBOUND_DATASET, 'io_id', ['io_id', 'freight']);
|
||||
$query = $this->connection->table($this->datasets->get(self::DETAILS_DATASET)->targetTable() . ' as d')
|
||||
->leftJoinSub($extension, 'x', static function ($join) use ($lookup): void {
|
||||
$join->whereRaw('x.lookup_key = ' . $lookup('d.so_id'))
|
||||
// Keep the full comparison: a hash collision must never
|
||||
// combine different orders or multiply freight amounts.
|
||||
->whereRaw("COALESCE(x.so_id, '') = COALESCE(d.so_id, '')");
|
||||
})
|
||||
->leftJoinSub($outbound, 'o', 'o.io_id', '=', 'x.io_id');
|
||||
return $this->filteredQuery([], true, $query);
|
||||
}
|
||||
|
||||
private function latestSummaryProjection(string $dataset, string $relation, array $columns): Builder
|
||||
{
|
||||
$table = $this->datasets->get($dataset)->targetTable();
|
||||
$ids = $this->connection->table($table)->select($relation)->selectRaw('MAX(Id) AS latest_id')->groupBy($relation)->orderByRaw('NULL');
|
||||
return $this->connection->query()->fromSub($ids, 'latest_ids')
|
||||
->join($table . ' as candidate', static function ($join) use ($relation): void {
|
||||
$join->on('latest_ids.' . $relation, '=', 'candidate.' . $relation)->on('latest_ids.latest_id', '=', 'candidate.Id');
|
||||
})->select(array_map(static fn (string $column): string => 'candidate.' . $column, $columns));
|
||||
}
|
||||
|
||||
private function filteredQuery(array $filters, bool $includeRelations = true, ?Builder $base = null): Builder
|
||||
{
|
||||
$query = ($base ?? ($includeRelations ? $this->detailQuery() : $this->connection->table($this->datasets->get(self::DETAILS_DATASET)->targetTable() . ' as d')))
|
||||
->whereNotNull('d.logistic_id')->where('d.logistic_id', '!=', '');
|
||||
$this->containsFilter($query, 'd.logistic_id', $filters['express_no'] ?? '');
|
||||
|
||||
|
||||
@ -209,6 +209,24 @@ class CourierFreightReconciliationMirrorRepositoryTest extends TestCase
|
||||
$db->disableQueryLog();
|
||||
}
|
||||
|
||||
public function test_summary_lookup_keeps_full_long_order_identifiers_distinct(): void
|
||||
{
|
||||
$this->markReady();
|
||||
$db = DB::connection('business_mirror');
|
||||
$prefix = str_repeat('X', 64);
|
||||
$db->table('js_jst_order_express_details')->insert(['id' => 1, 'logistic_id' => 'A', 'so_id' => $prefix . 'A']);
|
||||
$db->table('js_jst_send_data_view_extension')->insert([
|
||||
['Id' => 1, 'so_id' => $prefix . 'A', 'io_id' => 1],
|
||||
['Id' => 2, 'so_id' => $prefix . 'B', 'io_id' => 2],
|
||||
]);
|
||||
$db->table('js_jst_sales_outbound_info')->insert([
|
||||
['Id' => 1, 'io_id' => 1, 'freight' => 1], ['Id' => 2, 'io_id' => 2, 'freight' => 2],
|
||||
]);
|
||||
$summary = $this->repository->summarize([]);
|
||||
$this->assertSame(1, $summary['count']);
|
||||
$this->assertSame(1.0, $summary['freight']);
|
||||
}
|
||||
|
||||
private function markReady(): void
|
||||
{
|
||||
$now = now()->toDateTimeString();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user