From 0a601fbc5595cf221094603e14365332a5e5e613 Mon Sep 17 00:00:00 2001 From: limeng <1131177064@qq.com> Date: Tue, 3 Nov 2026 10:36:01 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E5=8F=91=E8=B4=A7=E6=95=B0=E6=8D=AE):=20?= =?UTF-8?q?=E5=87=BA=E5=BA=93=E5=8D=95=E5=AE=9E=E5=8F=91=E6=98=8E=E7=BB=86?= =?UTF-8?q?=E4=B8=8E=E6=97=A7=E8=AE=B0=E5=BD=95=E6=9D=A1=E7=A0=81=E4=B8=8D?= =?UTF-8?q?=E5=90=8C=E6=97=B6=E4=BB=A5=E5=87=BA=E5=BA=93=E5=8D=95=E4=B8=BA?= =?UTF-8?q?=E5=87=86=E8=A6=86=E7=9B=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 早期同步时找不到出库单实发明细的包裹,商品用订单商品兜底(配件母件、被替换的原商品等,数量多为0但带金额); 之后出库单明细到齐,因新明细没有金额、"金额对不上"一直保留旧记录,导致条码/件数/体积与聚水潭不符 (10.1-10.25 约 3000 单,如配件仓记成母件 DNZ0134108N24V01,实发为 -23/-30/-31/-41 配件)。 - 新商品取自出库单实发明细(发货数据视图)且条码集合与旧记录不同:按出库单覆盖,条码相同的行沿用旧金额, 计入 packages_items_corrected;条码完全相同或旧记录无条码时仍按原规则保留,避免丢金额。 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014nvGZkg27YMEdhRy6EiUwf --- .../Jushuitan/ShippingReplicaSynchronizer.php | 26 ++++++++++- .../ShippingReplicaSynchronizerTest.php | 43 +++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/modules/EcommerceFinance/Services/Jushuitan/ShippingReplicaSynchronizer.php b/modules/EcommerceFinance/Services/Jushuitan/ShippingReplicaSynchronizer.php index f3fee6c7..eadd753c 100644 --- a/modules/EcommerceFinance/Services/Jushuitan/ShippingReplicaSynchronizer.php +++ b/modules/EcommerceFinance/Services/Jushuitan/ShippingReplicaSynchronizer.php @@ -78,6 +78,12 @@ class ShippingReplicaSynchronizer /** 本轮因商品已更正(新旧条码完全不同)而覆盖旧记录的包裹数;旧金额属于错配商品,不再阻止更新。 */ private int $packagesItemsCorrected = 0; + /** 当前正在写入的包裹,商品是否取自出库单实发明细(发货数据视图)。 */ + private bool $itemsFromView = false; + + /** @var array 待替换包裹 => 商品取自出库单实发明细 */ + private array $replacementFromView = []; + /** 本轮按条码沿用旧金额的明细行数(商品行 ID 变化但条码相同)。 */ private int $amountsMatchedByBarcode = 0; private int $staleMirrorPackages = 0; @@ -340,6 +346,8 @@ class ShippingReplicaSynchronizer $this->replacementHasFreight = []; $this->replacementHasItems = []; $this->replacementSyntheticSkuIds = []; + $this->replacementFromView = []; + $this->itemsFromView = false; $this->replacementRowCount = 0; $this->skipFreight = false; $this->replaceWindow = false; @@ -970,6 +978,7 @@ class ShippingReplicaSynchronizer } $virtualEmitted++; } + $this->itemsFromView = true; $total += $this->emitPackage(self::OUTBOUND_OFFSET + (int) $io, $pkg, $viewItems[$io], $skus, $freight, $so, $tracking !== '' ? $tracking : null, $now, true); $this->markDone($io, $tracking); $this->markTrackSo($so, $tracking); @@ -1077,6 +1086,7 @@ class ShippingReplicaSynchronizer 'weight' => null, ]; $pkg = $this->withViewAddr($io, $pkg); + $this->itemsFromView = true; $total += $this->emitPackage(self::PURCHASE_OUT_OFFSET + (int) $io, $pkg, $viewItems[$io], $skus, [], $so, $tracking, $now, true); $this->markDone($io, (string) $tracking); $emitted++; @@ -1614,7 +1624,10 @@ class ShippingReplicaSynchronizer */ private function packageItems(string $io, array $viewItems, array $byOid, array $bySo, array $dws, string $oid, string $so, string $tracking): array { + $this->itemsFromView = false; if ($io !== '' && ! empty($viewItems[$io])) { + $this->itemsFromView = true; + return $viewItems[$io]; } $tk = ltrim(trim($tracking), '@'); @@ -2304,6 +2317,8 @@ class ShippingReplicaSynchronizer /** @param list> $rows */ private function writePackage(int $sourceId, array $rows, bool $hasFreight, bool $hasItems, array $syntheticSkuIds): void { + $fromView = $this->itemsFromView; + $this->itemsFromView = false; if (! $this->replaceWindow) { foreach ($rows as $row) { $this->push($sourceId, (int) $row['sku_source_id'], $row); @@ -2316,6 +2331,7 @@ class ShippingReplicaSynchronizer $this->replacementHasFreight[$sourceId] = $hasFreight; $this->replacementHasItems[$sourceId] = $hasItems; $this->replacementSyntheticSkuIds[$sourceId] = $syntheticSkuIds; + $this->replacementFromView[$sourceId] = $fromView; $this->replacementRowCount += count($rows); if ($this->replacementRowCount >= self::FLUSH) { $this->flushReplacements(); @@ -2340,7 +2356,8 @@ class ShippingReplicaSynchronizer $hasFreight = $this->replacementHasFreight; $hasItems = $this->replacementHasItems; $syntheticSkuIds = $this->replacementSyntheticSkuIds; - $this->db->transaction(function () use ($packages, $hasFreight, $hasItems, $syntheticSkuIds): void { + $fromView = $this->replacementFromView; + $this->db->transaction(function () use ($packages, $hasFreight, $hasItems, $syntheticSkuIds, $fromView): void { $ids = array_keys($packages); $previous = []; $previousItems = []; @@ -2457,6 +2474,12 @@ class ShippingReplicaSynchronizer // 新旧商品完全不同:旧记录是错配商品(如把同订单其它包裹的商品挂到本单号),旧金额不属于本包裹,按新商品覆盖 $unsafeAmount = false; $this->packagesItemsCorrected++; + } elseif ($unsafeAmount && ($fromView[$id] ?? false) && $oldBarcodes !== [] && $newBarcodes !== [] + && $oldBarcodes != $newBarcodes) { + // 新商品取自出库单实发明细且条码集合与旧记录不同(旧记录多为订单商品兜底:整件/原商品/配件母件等): + // 以出库单为准覆盖;条码相同的行已按条码沿用旧金额,其余旧金额属于错配商品,不再保留。 + $unsafeAmount = false; + $this->packagesItemsCorrected++; } if ($unsafeAmount) { $this->packagesRetainedForUnmatchedAmount++; @@ -2504,6 +2527,7 @@ class ShippingReplicaSynchronizer $this->replacementHasFreight = []; $this->replacementHasItems = []; $this->replacementSyntheticSkuIds = []; + $this->replacementFromView = []; $this->replacementRowCount = 0; } diff --git a/tests/Unit/EcommerceFinance/ShippingReplicaSynchronizerTest.php b/tests/Unit/EcommerceFinance/ShippingReplicaSynchronizerTest.php index 31ae0a56..2d6b517c 100644 --- a/tests/Unit/EcommerceFinance/ShippingReplicaSynchronizerTest.php +++ b/tests/Unit/EcommerceFinance/ShippingReplicaSynchronizerTest.php @@ -1587,6 +1587,49 @@ class ShippingReplicaSynchronizerTest extends TestCase $this->assertSame(2, $db->table('jst_oms_shipping')->where('source_id', 9004)->count()); } + public function test_outbound_view_items_replace_stale_order_fallback_items_when_barcodes_differ(): void + { + $db = DB::connection('business_mirror'); + // 旧记录是订单商品兜底(配件母件/原商品,数量0但带金额),与出库单实发明细部分重叠 + foreach ([ + ['source_id' => 7000, 'sku_source_id' => 700001, 'ship_time' => '2026-09-02 12:00:00', 'express_no' => 'DPK7', + 'barcode' => 'XG017603N113V06-11', 'product_name' => '订单商品-11', 'qty' => 0, 'amount' => 12.50], + ['source_id' => 7000, 'sku_source_id' => 700002, 'ship_time' => '2026-09-02 12:00:00', 'express_no' => 'DPK7', + 'barcode' => 'XG017603N113V06-32', 'product_name' => '订单商品-32', 'qty' => 0, 'amount' => 3.00], + ] as $row) { + $db->table('jst_oms_shipping')->insert($row); + } + $upstream = $this->createMock(UpstreamReadOnlyQuery::class); + $upstream->method('select')->willReturnCallback(function (string $sql): array { + if (str_contains($sql, 'FROM rpt.jst_order_express_details e')) { + return [['id' => 7000, 'inner_order_id' => 'order-7', 'logistic_id' => 'DPK7', 'send_time' => '2026-09-02 12:00:00']]; + } + if (str_contains($sql, 'FROM rpt.jst_salesOutboundInfo WHERE io_date')) { + return [['o_id' => 'order-7', 'io_id' => 'outbound-7', 'l_id' => 'DPK7']]; + } + if (str_contains($sql, 'FROM rpt.Jst_SendData_View_extension')) { + return [ + ['io_id' => 'outbound-7', 'sku_ids' => 'XG017603N113V06-11', 'qty' => 1, 'type' => 0, 'sku_name' => '实发配件-11', 'volume' => null], + ['io_id' => 'outbound-7', 'sku_ids' => 'XG017603N113V06-40', 'qty' => 1, 'type' => 0, 'sku_name' => '实发配件-40', 'volume' => null], + ]; + } + + return []; + }); + $factory = $this->createMock(UpstreamReadOnlyQueryFactory::class); + $factory->method('forSource')->with('operations_replica')->willReturn($upstream); + + $result = (new ShippingReplicaSynchronizer($factory))->sync(Carbon::parse('2026-09-01'), Carbon::parse('2026-09-05')); + + $this->assertSame(0, $result['packages_retained_for_unmatched_amount']); + $this->assertSame(1, $result['packages_items_corrected']); + $rows = $db->table('jst_oms_shipping')->where('source_id', 7000)->orderBy('barcode')->get(); + $this->assertSame(['XG017603N113V06-11', 'XG017603N113V06-40'], $rows->pluck('barcode')->all()); + $this->assertEquals(12.50, $rows[0]->amount); + $this->assertNull($rows[1]->amount); + $this->assertSame(['1/2'], $rows->pluck('pkg_count')->unique()->values()->all()); + } + private function setScanBatchLimit(ShippingReplicaSynchronizer $synchronizer, string $property, int $limit): void { $reflection = new \ReflectionProperty($synchronizer, $property); -- 2.47.2