From 7c3113a3b7766cda6a6af59346415d935fa501ba Mon Sep 17 00:00:00 2001 From: limeng <1131177064@qq.com> Date: Thu, 5 Nov 2026 14:03:27 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E5=8F=91=E8=B4=A7=E6=95=B0=E6=8D=AE):=20?= =?UTF-8?q?=E9=94=80=E5=94=AE=E5=87=BA=E5=BA=93=E5=8D=95=E9=87=8D=E9=87=8F?= =?UTF-8?q?=E6=A0=A1=E6=AD=A3=E4=B8=8D=E5=86=8D=E5=9B=A0=E8=B6=85=E6=97=B6?= =?UTF-8?q?=E6=82=84=E6=82=84=E8=B7=B3=E8=BF=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit alignWithOutbound 带重量的出库单查询一旦报错(包括副本库超时),就退回只校正发货时间, 发货明细里的预估重量一直留在实称里。回刷 10-11~10-31、11-01~11-04 时约 18 万个包裹因此 实称=预估重量。现改为: - 出库单按天分段查询,降低超时概率; - 只有副本库确实缺重量列时才退回只校正发货时间,其它错误直接抛出; - sync() 里校正失败重试 3 次,仍失败在进度里明确报出"重量校正跳过"。 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014nvGZkg27YMEdhry6eiuwf --- .../Jushuitan/ShippingReplicaSynchronizer.php | 77 +++++++++++++++---- .../ShippingReplicaSynchronizerTest.php | 50 +++++++++++- 2 files changed, 112 insertions(+), 15 deletions(-) diff --git a/modules/EcommerceFinance/Services/Jushuitan/ShippingReplicaSynchronizer.php b/modules/EcommerceFinance/Services/Jushuitan/ShippingReplicaSynchronizer.php index 319a4abd..a580890c 100644 --- a/modules/EcommerceFinance/Services/Jushuitan/ShippingReplicaSynchronizer.php +++ b/modules/EcommerceFinance/Services/Jushuitan/ShippingReplicaSynchronizer.php @@ -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) { diff --git a/tests/Unit/EcommerceFinance/ShippingReplicaSynchronizerTest.php b/tests/Unit/EcommerceFinance/ShippingReplicaSynchronizerTest.php index 3718e893..7972f093 100644 --- a/tests/Unit/EcommerceFinance/ShippingReplicaSynchronizerTest.php +++ b/tests/Unit/EcommerceFinance/ShippingReplicaSynchronizerTest.php @@ -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'); -- 2.47.2