fix(发货数据): 销售出库单重量校正不再因超时悄悄跳过 #998

Merged
limeng merged 1 commits from fix/shipping-weight-align-no-silent-skip-1105134334 into main 2026-11-09 09:13:55 +08:00
2 changed files with 112 additions and 15 deletions

View File

@ -276,7 +276,23 @@ class ShippingReplicaSynchronizer
}
try {
$this->placeholderPurged = $this->purgeZeroQtyPlaceholders($fromS, $toS);
$al = $this->alignWithOutbound($fromS, $toS);
// 出库单校正查副本库,偶发超时:重试几次,仍失败才跳过(并在进度里报出)
for ($try = 1; ; $try++) {
try {
$al = $this->alignWithOutbound($fromS, $toS);
break;
} catch (Throwable $e) {
if ($try >= 3) {
throw $e;
}
if ($progress) {
$progress('按销售出库单校正失败,第 ' . $try . ' 次重试:' . mb_substr($e->getMessage(), 0, 120));
}
if (! app()->runningUnitTests()) {
sleep(30);
}
}
}
$this->shipTimeFixed = $al['ship_time'];
$this->weightFixed = $al['weight'];
if ($progress && ($this->shipTimeFixed > 0 || $this->weightFixed > 0)) {
@ -287,7 +303,7 @@ class ShippingReplicaSynchronizer
}
} catch (Throwable $e) {
if ($progress) {
$progress('发货时间校正跳过:' . $e->getMessage());
$progress('按销售出库单校正(发货时间/线上实称重量)跳过:' . $e->getMessage());
}
}
} else {
@ -2866,6 +2882,15 @@ class ShippingReplicaSynchronizer
return $this->alignWithOutbound($fromS, $toS)['ship_time'];
}
/** 副本库缺列(如没有 weight/f_weight)的报错:只有这种情况才允许退回不带重量的查询。 */
private function isMissingColumnError(Throwable $e): bool
{
$m = mb_strtolower($e->getMessage());
return str_contains($m, 'invalid column name') || str_contains($m, 'unknown column') || str_contains($m, '列名')
|| (str_contains($m, 'column') && (str_contains($m, 'weight') || str_contains($m, 'not found')));
}
/**
* 按销售出库单对齐包裹级字段(聚水潭"销售出库单"界面口径):
* - 发货时间 = 出库时间 io_date(只往后校正,见 fixShipTimes)
@ -2879,18 +2904,42 @@ class ShippingReplicaSynchronizer
{
$ioTo = Carbon::parse($toS)->addDays(3)->format('Y-m-d H:i:s');
$withWeight = true;
try {
$rows = $this->selectWithReconnect(
'SELECT so.l_id, so.io_date, so.status, so.weight, so.f_weight FROM ' . self::OUTBOUND . ' so WHERE so.io_date >= ? AND so.io_date < ?',
[$fromS, $ioTo],
);
} catch (Throwable $e) {
// 副本库若没有重量列,只做发货时间校正
$withWeight = false;
$rows = $this->selectWithReconnect(
'SELECT so.l_id, so.io_date, so.status FROM ' . self::OUTBOUND . ' so WHERE so.io_date >= ? AND so.io_date < ?',
[$fromS, $ioTo],
);
// 按天分段查出库单:整段一次查副本库容易超时。
// 只有副本库确实没有重量列时才退回"只校正发货时间";超时等其它错误直接抛出,
// 不能悄悄跳过重量校正(否则发货明细里的预估重量会一直留在实称里)。
$rows = [];
$cur = Carbon::parse($fromS);
$end = Carbon::parse($ioTo);
while ($cur->lt($end)) {
$next = $cur->copy()->addDay();
if ($next->gt($end)) {
$next = $end->copy();
}
$bind = [$cur->format('Y-m-d H:i:s'), $next->format('Y-m-d H:i:s')];
$part = null;
if ($withWeight) {
try {
$part = $this->selectWithReconnect(
'SELECT so.l_id, so.io_date, so.status, so.weight, so.f_weight FROM ' . self::OUTBOUND . ' so WHERE so.io_date >= ? AND so.io_date < ?',
$bind,
);
} catch (Throwable $e) {
if (! $this->isMissingColumnError($e)) {
throw $e;
}
$withWeight = false;
}
}
if ($part === null) {
$part = $this->selectWithReconnect(
'SELECT so.l_id, so.io_date, so.status FROM ' . self::OUTBOUND . ' so WHERE so.io_date >= ? AND so.io_date < ?',
$bind,
);
}
foreach ($part as $r) {
$rows[] = $r;
}
$cur = $next;
}
$best = [];
foreach ($rows as $r) {

View File

@ -1475,7 +1475,12 @@ class ShippingReplicaSynchronizerTest extends TestCase
$n = (new ShippingReplicaSynchronizer($factory))->fixShipTimes('2026-10-20 00:00:00', '2026-10-26 00:00:00');
$this->assertSame(2, $n);
$this->assertSame([['2026-10-20 00:00:00', '2026-10-29 00:00:00']], $queries);
// 出库单按天分段查询(整段查副本库容易超时):io_date 范围 10-20 ~ 10-29 共 9 段
$days = [];
foreach (range(20, 28) as $d) {
$days[] = ["2026-10-{$d} 00:00:00", '2026-10-' . ($d + 1) . ' 00:00:00'];
}
$this->assertSame($days, $queries);
$this->assertSame(['2026-10-21 08:46:09'], $db->table('jst_oms_shipping')->where('source_id', 7001)->distinct()->pluck('ship_time')->all());
$this->assertSame(2, $db->table('jst_oms_shipping')->where('source_id', 7001)->where('ship_time', '2026-10-21 08:46:09')->count());
$this->assertSame('2026-10-21 09:00:00', $db->table('jst_oms_shipping')->where('source_id', 7002)->value('ship_time'));
@ -1540,6 +1545,49 @@ class ShippingReplicaSynchronizerTest extends TestCase
$this->assertEquals(3.0, $w(8005, 1)->actual_weight);
}
public function test_outbound_weight_alignment_does_not_silently_skip_weights_on_timeout(): void
{
$db = DB::connection('business_mirror');
$db->table('jst_oms_shipping')->insert([
['source_id' => 8101, 'sku_source_id' => 1, 'ship_time' => '2026-10-21 10:00:00', 'express_no' => 'TO1',
'online_weight' => 5.0, 'actual_weight' => 5.0],
]);
// 带重量的出库单查询超时:不能退回"只校正发货时间",要报错让上层知道校正没做
$upstream = $this->createMock(UpstreamReadOnlyQuery::class);
$upstream->method('select')->willReturnCallback(function (string $sql): array {
if (str_contains($sql, 'so.f_weight')) {
throw new RuntimeException('SQLSTATE[HYT00]: Query timeout expired');
}
return [['l_id' => 'TO1', 'io_date' => '2026-10-21 10:00:00', 'status' => '已出库']];
});
$factory = $this->createMock(UpstreamReadOnlyQueryFactory::class);
$factory->method('forSource')->willReturn($upstream);
try {
(new ShippingReplicaSynchronizer($factory))->alignWithOutbound('2026-10-21 00:00:00', '2026-10-22 00:00:00');
$this->fail('timeout should not be swallowed');
} catch (RuntimeException $e) {
$this->assertStringContainsString('timeout', $e->getMessage());
}
// 副本库确实没有重量列:只校正发货时间,重量不动
$upstream2 = $this->createMock(UpstreamReadOnlyQuery::class);
$upstream2->method('select')->willReturnCallback(function (string $sql): array {
if (str_contains($sql, 'so.f_weight')) {
throw new RuntimeException("SQLSTATE[42S22]: Invalid column name 'f_weight'.");
}
return [['l_id' => 'TO1', 'io_date' => '2026-10-21 12:00:00', 'status' => '已出库']];
});
$factory2 = $this->createMock(UpstreamReadOnlyQueryFactory::class);
$factory2->method('forSource')->willReturn($upstream2);
$res = (new ShippingReplicaSynchronizer($factory2))->alignWithOutbound('2026-10-21 00:00:00', '2026-10-22 00:00:00');
$this->assertSame(['ship_time' => 1, 'weight' => 0], $res);
$row = $db->table('jst_oms_shipping')->where('source_id', 8101)->first(['ship_time', 'actual_weight']);
$this->assertSame('2026-10-21 12:00:00', (string) $row->ship_time);
$this->assertEquals(5.0, $row->actual_weight);
}
public function test_stale_zero_quantity_order_rows_are_purged_and_package_fields_move_to_first_kept_row(): void
{
$db = DB::connection('business_mirror');