From 7def7261f7841bbdb473723b5ee8fa0a7ff4a158 Mon Sep 17 00:00:00 2001 From: WuYongJie <549727955@qq.com> Date: Wed, 7 Oct 2026 13:49:43 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=9D=E7=95=99=E4=B8=BB=E7=BA=BF?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=E4=BF=9D=E6=8A=A4=E5=B9=B6=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E5=8F=91=E8=B4=A7=E8=A1=A5=E6=BC=8F=E4=B8=8E=E5=AF=BC=E5=87=BA?= =?UTF-8?q?=E5=8F=A3=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Jushuitan/ShippingReplicaSynchronizer.php | 8 +- .../ShippingReplicaSynchronizerTest.php | 81 +++++++++++++++++++ 2 files changed, 84 insertions(+), 5 deletions(-) diff --git a/modules/EcommerceFinance/Services/Jushuitan/ShippingReplicaSynchronizer.php b/modules/EcommerceFinance/Services/Jushuitan/ShippingReplicaSynchronizer.php index 6b06f01c..c04f9e57 100644 --- a/modules/EcommerceFinance/Services/Jushuitan/ShippingReplicaSynchronizer.php +++ b/modules/EcommerceFinance/Services/Jushuitan/ShippingReplicaSynchronizer.php @@ -626,11 +626,9 @@ class ShippingReplicaSynchronizer if ($oid === '') { continue; } - // 单号已属于其它包裹(发货明细已出):本单是虚拟/补差单,工作平台此类行快递单号为空 $tracking = $this->tracking((string) ($r['l_id'] ?? ''), '', $freight, (string) ($r['so_id'] ?? '')); - if ($tracking !== null && isset($this->emittedTrack[ltrim(trim($tracking), '@')])) { - $tracking = null; - } + // Shared tracking can represent a real combined shipment across orders. + // Keep the upstream identifier; sharing alone does not prove a virtual order. $this->markDone($this->ioFor($ioMap, (string) ($r['l_id'] ?? ''), $oid), (string) ($r['l_id'] ?? '')); $this->markTrackSo((string) ($r['so_id'] ?? ''), (string) ($r['l_id'] ?? '')); if ($this->excludedByLabel($r['labels'] ?? null)) { @@ -1655,7 +1653,7 @@ class ShippingReplicaSynchronizer } } $fysW = []; - foreach (array_chunk($ioKeys, 800) as $ck) { + foreach (array_chunk($this->skipFreight ? [] : $ioKeys, 800) as $ck) { $ph = implode(',', array_fill(0, count($ck), '?')); foreach ($this->selectWithReconnect("SELECT io_id, sku_id, weight FROM rpt.fys_jstFreightCostDetail WHERE io_id IN ({$ph})", $ck) as $r) { $k = mb_strtolower(trim((string) ($r['sku_id'] ?? ''))); diff --git a/tests/Unit/EcommerceFinance/ShippingReplicaSynchronizerTest.php b/tests/Unit/EcommerceFinance/ShippingReplicaSynchronizerTest.php index 33d34337..ab2b853f 100644 --- a/tests/Unit/EcommerceFinance/ShippingReplicaSynchronizerTest.php +++ b/tests/Unit/EcommerceFinance/ShippingReplicaSynchronizerTest.php @@ -1093,6 +1093,87 @@ class ShippingReplicaSynchronizerTest extends TestCase ); } + public function test_outbound_fallback_flushes_replacements_and_preserves_freight_when_skipped(): void + { + $db = DB::connection('business_mirror'); + $db->table('jst_oms_shipping')->insert([ + 'source_id' => 2000000420, 'sku_source_id' => 2000000420, + 'express_no' => 'outbound-420', 'trade_no' => 'trade-420', + 'ship_time' => '2026-09-02 12:00:00', 'freight' => 18.50, 'settle_weight_kg' => 7, + ]); + $upstream = $this->createMock(UpstreamReadOnlyQuery::class); + $upstream->method('select')->willReturnCallback(function (string $sql): array { + if (str_contains($sql, 'MIN(Id) AS lo')) { + return [['lo' => 1, 'hi' => 1]]; + } + if (str_contains($sql, 'WHERE Id >= ?')) { + return [['io_id' => '420', 'so_id' => 'trade-420', 'l_id' => 'outbound-420', 'io_date' => '2026-09-02 12:00:00']]; + } + if (str_contains($sql, 'SELECT io_id, sku_ids')) { + return [['io_id' => '420', 'sku_ids' => 'sku-420', 'qty' => 2, 'type' => '0', 'volume' => 100, 'l' => 10, 'w' => 5, 'h' => 2]]; + } + $this->assertStringNotContainsString('fys_jstFreightCost', $sql); + return []; + }); + $factory = $this->createMock(UpstreamReadOnlyQueryFactory::class); + $factory->method('forSource')->willReturn($upstream); + $result = (new ShippingReplicaSynchronizer($factory))->sync( + Carbon::parse('2026-09-01'), Carbon::parse('2026-09-05'), 2000, null, true, + ); + $this->assertSame(1, $result['outbound']); + $row = $db->table('jst_oms_shipping')->where('source_id', 2000000420)->sole(); + $this->assertSame('sku-420', $row->barcode); + $this->assertEquals(2, $row->qty); + $this->assertEquals(18.50, $row->freight); + $this->assertEquals(7, $row->settle_weight_kg); + } + + public function test_outbound_mapping_keeps_shared_tracking_orders_distinct_and_ignores_cancelled(): void + { + $upstream = $this->createMock(UpstreamReadOnlyQuery::class); + $factory = $this->createMock(UpstreamReadOnlyQueryFactory::class); + $factory->method('forSource')->willReturn($upstream); + $sync = new ShippingReplicaSynchronizer($factory); + $map = (new \ReflectionMethod($sync, 'mapOutboundRows'))->invoke($sync, [ + ['io_id' => '10', 'o_id' => 'A', 'l_id' => '@shared', 'io_date' => '2026-09-02'], + ['io_id' => '20', 'o_id' => 'B', 'l_id' => 'shared', 'io_date' => '2026-09-03'], + ['io_id' => '30', 'o_id' => 'A', 'l_id' => 'shared', 'io_date' => '2026-09-04', 'status' => 'Cancelled'], + ]); + $method = new \ReflectionMethod($sync, 'ioFor'); + $this->assertSame('10', $method->invoke($sync, $map, 'shared', 'A')); + $this->assertSame('20', $method->invoke($sync, $map, '@shared', 'B')); + $this->assertArrayNotHasKey(30, $map['ioL']); + } + + public function test_tracking_replacement_requires_one_unique_pair(): void + { + $upstream = $this->createMock(UpstreamReadOnlyQuery::class); + $upstream->method('select')->willReturn([]); + $factory = $this->createMock(UpstreamReadOnlyQueryFactory::class); + $factory->method('forSource')->willReturn($upstream); + $sync = new ShippingReplicaSynchronizer($factory); + $method = new \ReflectionMethod($sync, 'pairChangedTracking'); + $rows = [['id' => 1, 'inner_order_id' => 'A', 'logistic_id' => 'old']]; + $this->assertSame([], $method->invoke($sync, $rows, ['byL' => [], 'byOAll' => ['A' => ['10' => 'new-a', '20' => 'new-b']]])); + $this->assertSame(['1' => ['io' => '10', 'l_id' => 'new-a', 'old' => 'old']], + $method->invoke($sync, $rows, ['byL' => [], 'byOAll' => ['A' => ['10' => 'new-a']]])); + } + + public function test_cloud_order_label_is_included_and_volume_rules_are_bounded(): void + { + $factory = $this->createMock(UpstreamReadOnlyQueryFactory::class); + $factory->method('forSource')->willReturn($this->createMock(UpstreamReadOnlyQuery::class)); + $sync = new ShippingReplicaSynchronizer($factory); + $label = new \ReflectionMethod($sync, 'excludedByLabel'); + $this->assertTrue($label->invoke($sync, '线上发货')); + $this->assertFalse($label->invoke($sync, '线上发货,云仓订单')); + $volume = new \ReflectionMethod($sync, 'detailVolume'); + $this->assertSame(120.0, $volume->invoke($sync, 10, 3, 2, 2.0, '普通仓', '中通速递')); + $this->assertSame(60.0, $volume->invoke($sync, 10, 3, 2, 2.0, '常州简尚家居有限公司', '自送')); + $this->assertNull($volume->invoke($sync, 0, 3, 2, 2.0, '普通仓', '中通速递')); + $this->assertSame(0.0, $volume->invoke($sync, 10, 3, 2, 2.0, '普通仓', '中通礼品单')); + } + private function setScanBatchLimit(ShippingReplicaSynchronizer $synchronizer, string $property, int $limit): void { $reflection = new \ReflectionProperty($synchronizer, $property);