fix: 保留主线同步保护并验证发货补漏与导出口径

This commit is contained in:
WuYongJie 2026-10-07 13:49:43 +08:00
parent 1bb1c80816
commit 7def7261f7
2 changed files with 84 additions and 5 deletions

View File

@ -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'] ?? '')));

View File

@ -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);