fix(发货数据): 重跑时清除被取代的兜底包裹,保留包裹也刷新省市区/快递/仓库
- 凌晨同步时发货明细常未到齐,包裹先按自动发货仓/出库单兜底入库(无省份、无商品);之后发货明细到齐换成发货明细来源键, 旧兜底包裹因"上游未再出现"被保留,造成重复和空白省份(10 月重跑 2,226 个)。现在:旧包裹的快递单号已由本次其它包裹吐出时 视为被取代并删除;无对应的仍保留待核对。 - 商品金额无法安全匹配(或新商品明细缺失)而保留的旧包裹(10 月 28,201 个),此前整包不更新;现在商品与金额不动, 省市区、快递名称、仓库、店铺、平台、交易单号、付款时间按本次上游刷新。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014nvGZkg27YMEdhRy6EiUwf
This commit is contained in:
parent
b4f8416794
commit
f6497f5afc
@ -43,6 +43,10 @@ class ShippingReplicaSynchronizer
|
||||
'settle_volume_cm3', 'recon_weight_kg', 'recon_volume_cm3', 'front_no',
|
||||
];
|
||||
private const PRESERVED_CONTACT_FIELDS = ['receiver_name', 'receiver_mobile', 'receiver_phone'];
|
||||
private const REFRESHED_PACKAGE_FIELDS = [
|
||||
'trade_no', 'express_name1', 'warehouse_name', 'shop_name', 'platform', 'pay_time',
|
||||
'receiver_state', 'receiver_city', 'receiver_district',
|
||||
];
|
||||
private const PRESERVED_ITEM_FIELDS = ['finished_barcode', 'finished_name', 'finished_supplier'];
|
||||
|
||||
private const VIEW = 'rpt.Jst_SendData_View_extension';
|
||||
@ -68,6 +72,7 @@ class ShippingReplicaSynchronizer
|
||||
private int $packagesRetainedForMissingItems = 0;
|
||||
private int $packagesRetainedForUnmatchedAmount = 0;
|
||||
private int $staleMirrorPackages = 0;
|
||||
private int $supersededRemoved = 0;
|
||||
|
||||
/** @var array<int,true> */
|
||||
private array $seenSourceIds = [];
|
||||
@ -174,6 +179,7 @@ class ShippingReplicaSynchronizer
|
||||
$this->packagesRetainedForMissingItems = 0;
|
||||
$this->packagesRetainedForUnmatchedAmount = 0;
|
||||
$this->staleMirrorPackages = 0;
|
||||
$this->supersededRemoved = 0;
|
||||
$this->seenSourceIds = [];
|
||||
$this->expressOrderIds = [];
|
||||
$this->dwsFailedDays = [];
|
||||
@ -261,6 +267,7 @@ class ShippingReplicaSynchronizer
|
||||
$result['packages_retained_for_unmatched_amount'] = $this->packagesRetainedForUnmatchedAmount;
|
||||
$result['dws_failed_days'] = $this->dwsFailedDays;
|
||||
$result['stale_mirror_packages'] = $this->staleMirrorPackages;
|
||||
$result['superseded_packages_removed'] = $this->supersededRemoved;
|
||||
$result['tracking_pairing_skipped_pages'] = $this->pairingSkipped;
|
||||
}
|
||||
if ($skipFreight) {
|
||||
@ -2177,9 +2184,11 @@ class ShippingReplicaSynchronizer
|
||||
|
||||
$rows = [];
|
||||
$replaceIds = [];
|
||||
$refresh = [];
|
||||
foreach ($packages as $id => $packageRows) {
|
||||
if (! ($hasItems[$id] ?? false) && ($oldHasItems[$id] ?? false)) {
|
||||
$this->packagesRetainedForMissingItems++;
|
||||
$refresh[$id] = $packageRows[0] ?? [];
|
||||
continue;
|
||||
}
|
||||
$unmatchedOldAmounts = $oldAmountSkuIds[$id] ?? [];
|
||||
@ -2216,6 +2225,7 @@ class ShippingReplicaSynchronizer
|
||||
}
|
||||
if ($unsafeAmount) {
|
||||
$this->packagesRetainedForUnmatchedAmount++;
|
||||
$refresh[$id] = $packageRows[0] ?? [];
|
||||
continue;
|
||||
}
|
||||
$replaceIds[] = $id;
|
||||
@ -2245,6 +2255,7 @@ class ShippingReplicaSynchronizer
|
||||
}
|
||||
array_push($rows, ...$packageRows);
|
||||
}
|
||||
$this->refreshRetainedPackages($refresh);
|
||||
if ($replaceIds === []) {
|
||||
return;
|
||||
}
|
||||
@ -2347,21 +2358,55 @@ class ShippingReplicaSynchronizer
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 窗口内本次未扫到的旧包裹:若其快递单号已由本次其它包裹(新来源键)吐出,说明是被取代的旧记录
|
||||
* (如凌晨同步时发货明细未到、先按自动发货仓/出库单兜底入库,之后发货明细到齐换成发货明细来源),删除以免重复和残留空白;
|
||||
* 其余仍保留并计数,需按源键核对。
|
||||
*/
|
||||
private function countUnseenMirrorPackages(string $fromS, string $toS): int
|
||||
{
|
||||
$unseen = 0;
|
||||
$ids = $this->db->table('jst_oms_shipping')
|
||||
$superseded = [];
|
||||
$rows = $this->db->table('jst_oms_shipping')
|
||||
->where('ship_time', '>=', $fromS)->where('ship_time', '<', $toS)
|
||||
->distinct()->pluck('source_id');
|
||||
foreach ($ids as $id) {
|
||||
if (! isset($this->seenSourceIds[(int) $id])) {
|
||||
$unseen++;
|
||||
->selectRaw('source_id, MAX(express_no) AS express_no')->groupBy('source_id')->get();
|
||||
foreach ($rows as $row) {
|
||||
$id = (int) $row->source_id;
|
||||
if (isset($this->seenSourceIds[$id])) {
|
||||
continue;
|
||||
}
|
||||
$tk = ltrim(trim((string) ($row->express_no ?? '')), '@');
|
||||
if ($tk !== '' && isset($this->emittedTrack[$tk])) {
|
||||
$superseded[] = $id;
|
||||
continue;
|
||||
}
|
||||
$unseen++;
|
||||
}
|
||||
foreach (array_chunk($superseded, 500) as $chunk) {
|
||||
$this->db->table('jst_oms_shipping')->whereIn('source_id', $chunk)->delete();
|
||||
}
|
||||
$this->supersededRemoved = count($superseded);
|
||||
|
||||
return $unseen;
|
||||
}
|
||||
|
||||
/** 因商品/金额无法安全替换而保留的旧包裹:包裹级字段(省市区、快递、仓库、店铺等)仍按本次上游刷新,商品与金额不动。 */
|
||||
private function refreshRetainedPackages(array $refresh): void
|
||||
{
|
||||
foreach ($refresh as $id => $new) {
|
||||
$set = [];
|
||||
foreach (self::REFRESHED_PACKAGE_FIELDS as $field) {
|
||||
$v = $new[$field] ?? null;
|
||||
if ($v !== null && trim((string) $v) !== '') {
|
||||
$set[$field] = $v;
|
||||
}
|
||||
}
|
||||
if ($set !== []) {
|
||||
$this->db->table('jst_oms_shipping')->where('source_id', (int) $id)->update($set);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function withPreservedFreight(array $payload, array $previous): array
|
||||
{
|
||||
$payload = $this->withPreservedCharges($payload, $previous);
|
||||
|
||||
@ -1315,6 +1315,65 @@ class ShippingReplicaSynchronizerTest extends TestCase
|
||||
$this->assertSame('15170678', $pending->getValue($sync)[2]);
|
||||
}
|
||||
|
||||
public function test_retained_package_refreshes_address_and_superseded_fallback_package_is_removed(): void
|
||||
{
|
||||
$db = DB::connection('business_mirror');
|
||||
$db->table('jst_oms_shipping')->insert([
|
||||
['source_id' => 5000, 'sku_source_id' => 500000, 'ship_time' => '2026-09-02 12:00:00', 'express_no' => 'YT5',
|
||||
'barcode' => null, 'product_name' => '旧商品甲', 'amount' => 10.50, 'receiver_state' => null],
|
||||
// 凌晨先按自动发货仓兜底入库的旧包裹,之后发货明细到齐由 6000 吐出同一单号
|
||||
['source_id' => 1000000777, 'sku_source_id' => 1000000777, 'ship_time' => '2026-09-02 13:00:00', 'express_no' => 'YT6',
|
||||
'barcode' => null, 'product_name' => null, 'amount' => null, 'receiver_state' => null],
|
||||
['source_id' => 1000000888, 'sku_source_id' => 1000000888, 'ship_time' => '2026-09-02 14:00:00', 'express_no' => 'ZZ9',
|
||||
'barcode' => null, 'product_name' => null, 'amount' => null, 'receiver_state' => null],
|
||||
]);
|
||||
$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' => 5000, 'inner_order_id' => 'order-1', 'logistic_id' => 'YT5', 'send_time' => '2026-09-02 12:00:00',
|
||||
'receiver_address' => '河南省-郑州市-金水区'],
|
||||
['id' => 6000, 'inner_order_id' => 'order-2', 'logistic_id' => 'YT6', 'send_time' => '2026-09-02 13:00:00',
|
||||
'receiver_address' => '江苏省-常州市-武进区'],
|
||||
];
|
||||
}
|
||||
if (str_contains($sql, 'FROM rpt.jst_salesOutboundInfo WHERE io_date')) {
|
||||
return [
|
||||
['o_id' => 'order-1', 'io_id' => 'outbound-1', 'l_id' => 'YT5'],
|
||||
['o_id' => 'order-2', 'io_id' => 'outbound-2', 'l_id' => 'YT6'],
|
||||
];
|
||||
}
|
||||
if (str_contains($sql, 'FROM rpt.Jst_SendData_View_extension')) {
|
||||
return [
|
||||
['io_id' => 'outbound-1', 'sku_ids' => 'new-sku', 'qty' => 1, 'type' => 0, 'sku_name' => '新商品甲', 'volume' => null],
|
||||
['io_id' => 'outbound-2', 'sku_ids' => 'new-sku', 'qty' => 1, 'type' => 0, 'sku_name' => '新商品乙', '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(1, $result['packages_retained_for_unmatched_amount']);
|
||||
$this->assertSame(1, $result['superseded_packages_removed']);
|
||||
$this->assertSame(1, $result['stale_mirror_packages']);
|
||||
// 保留的旧包裹:商品金额不动,省市区按上游刷新
|
||||
$kept = $db->table('jst_oms_shipping')->where('source_id', 5000)->sole();
|
||||
$this->assertSame('旧商品甲', $kept->product_name);
|
||||
$this->assertEquals(10.50, $kept->amount);
|
||||
$this->assertSame('河南省', $kept->receiver_state);
|
||||
$this->assertSame('郑州市', $kept->receiver_city);
|
||||
// 被发货明细取代的兜底包裹删除;无对应的仍保留待核对
|
||||
$this->assertSame(0, $db->table('jst_oms_shipping')->where('source_id', 1000000777)->count());
|
||||
$this->assertSame(1, $db->table('jst_oms_shipping')->where('source_id', 1000000888)->count());
|
||||
$this->assertSame('江苏省', $db->table('jst_oms_shipping')->where('source_id', 6000)->value('receiver_state'));
|
||||
}
|
||||
|
||||
private function setScanBatchLimit(ShippingReplicaSynchronizer $synchronizer, string $property, int $limit): void
|
||||
{
|
||||
$reflection = new \ReflectionProperty($synchronizer, $property);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user