fix(发货数据): 出库单实发明细与旧记录条码不同时以出库单为准覆盖 #835

Merged
limeng merged 1 commits from fix/shipping-view-items-override-1103103125 into main 2026-11-03 10:41:35 +08:00
2 changed files with 68 additions and 1 deletions

View File

@ -78,6 +78,12 @@ class ShippingReplicaSynchronizer
/** 本轮因商品已更正(新旧条码完全不同)而覆盖旧记录的包裹数;旧金额属于错配商品,不再阻止更新。 */
private int $packagesItemsCorrected = 0;
/** 当前正在写入的包裹,商品是否取自出库单实发明细(发货数据视图)。 */
private bool $itemsFromView = false;
/** @var array<int,bool> 待替换包裹 => 商品取自出库单实发明细 */
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<array<string,mixed>> $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;
}

View File

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