From f484fab7eb314712b9c8f06519362343e2a28bf9 Mon Sep 17 00:00:00 2001 From: WuYongJie <549727955@qq.com> Date: Wed, 9 Sep 2026 19:33:16 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96=E5=BF=AB=E9=80=92?= =?UTF-8?q?=E5=85=A8=E9=87=8F=E6=B1=87=E6=80=BB=E7=9A=84=E5=93=88=E5=B8=8C?= =?UTF-8?q?=E5=85=B3=E8=81=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...rFreightReconciliationMirrorRepository.php | 65 ++++++++++++++++--- ...ightReconciliationMirrorRepositoryTest.php | 18 +++++ 2 files changed, 75 insertions(+), 8 deletions(-) diff --git a/modules/EcommerceFinance/Repositories/CourierFreightReconciliationMirrorRepository.php b/modules/EcommerceFinance/Repositories/CourierFreightReconciliationMirrorRepository.php index 6853215e..ee27d9b0 100644 --- a/modules/EcommerceFinance/Repositories/CourierFreightReconciliationMirrorRepository.php +++ b/modules/EcommerceFinance/Repositories/CourierFreightReconciliationMirrorRepository.php @@ -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->connection->statement('SET SESSION ' . $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,7 +242,9 @@ 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->connection->statement('SET SESSION ' . $name . '=?', [$value]); + } } finally { $lock->release(); } @@ -242,12 +255,48 @@ class CourierFreightReconciliationMirrorRepository { $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'] ?? ''); diff --git a/tests/Unit/EcommerceFinance/CourierFreightReconciliationMirrorRepositoryTest.php b/tests/Unit/EcommerceFinance/CourierFreightReconciliationMirrorRepositoryTest.php index c36159d0..1878dc2e 100644 --- a/tests/Unit/EcommerceFinance/CourierFreightReconciliationMirrorRepositoryTest.php +++ b/tests/Unit/EcommerceFinance/CourierFreightReconciliationMirrorRepositoryTest.php @@ -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(); -- 2.47.2 From 535902c8f3baaa012c5fe9be25c4baba044d084f Mon Sep 17 00:00:00 2001 From: WuYongJie <549727955@qq.com> Date: Wed, 9 Sep 2026 19:34:38 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=E6=8C=89=E6=95=B0=E5=80=BC=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E8=BF=98=E5=8E=9F=E6=B1=87=E6=80=BB=E8=BF=9E=E6=8E=A5?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...urierFreightReconciliationMirrorRepository.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/modules/EcommerceFinance/Repositories/CourierFreightReconciliationMirrorRepository.php b/modules/EcommerceFinance/Repositories/CourierFreightReconciliationMirrorRepository.php index ee27d9b0..a366da48 100644 --- a/modules/EcommerceFinance/Repositories/CourierFreightReconciliationMirrorRepository.php +++ b/modules/EcommerceFinance/Repositories/CourierFreightReconciliationMirrorRepository.php @@ -230,7 +230,7 @@ class CourierFreightReconciliationMirrorRepository } foreach ($settings as $name => $value) { $previousSettings[$name] = $this->connection->selectOne('SELECT @@SESSION.' . $name . ' AS value')->value; - $this->connection->statement('SET SESSION ' . $name . '=?', [$value]); + $this->setSummarySessionSetting($name, $value); } } $asOf = now()->toDateTimeString(); @@ -243,7 +243,7 @@ class CourierFreightReconciliationMirrorRepository } finally { try { foreach (array_reverse($previousSettings, true) as $name => $value) { - $this->connection->statement('SET SESSION ' . $name . '=?', [$value]); + $this->setSummarySessionSetting($name, $value); } } finally { $lock->release(); @@ -251,6 +251,17 @@ class CourierFreightReconciliationMirrorRepository } } + 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 !== ''); -- 2.47.2