From 6cca03f22eb2885d878015a9595e93876e303a53 Mon Sep 17 00:00:00 2001 From: limeng <1131177064@qq.com> Date: Mon, 26 Oct 2026 17:38:43 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E5=8F=91=E8=B4=A7=E6=95=B0=E6=8D=AE):=20?= =?UTF-8?q?=E8=A1=A5=E6=8B=89=E6=94=B9=E5=88=B0=E6=AF=8F=E6=99=9A21:28?= =?UTF-8?q?=E5=B9=B6=E8=AE=B0=E5=BD=95=E6=97=A5=E5=BF=97=EF=BC=8C=E8=BF=87?= =?UTF-8?q?=E6=BB=A4=E8=AE=A2=E5=8D=95=E5=95=86=E5=93=81=E6=95=B0=E9=87=8F?= =?UTF-8?q?0=E5=8D=A0=E4=BD=8D=E8=A1=8C=EF=BC=8C=E8=B6=85=E9=95=BF?= =?UTF-8?q?=E6=96=87=E6=9C=AC=E6=8C=89=E5=88=97=E5=AE=BD=E6=88=AA=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 早上 7:28 的补拉在白天副本库高负载时频繁超时断开、未写入任何数据,改为每晚 21:28 拉最近 3 天; 两次同步输出追加到 storage/logs/jst-sync-shipping.log,便于排查。 - 订单商品表中平台原始商品行(数量 0、金额 0)没有实际出库,聚水潭工作平台也不显示; 包裹已有实发商品时不再写入,避免多出一个"条码"(实为平台商品编号)。 - 上游偶有超长值(如组合装商品ID 78 位)导致整批写入失败,写入前按 jst_oms_shipping 列宽截断。 --- app/Console/Kernel.php | 9 ++-- .../Jushuitan/ShippingReplicaSynchronizer.php | 35 +++++++++++++- .../ShippingReplicaSynchronizerTest.php | 46 +++++++++++++++++++ 3 files changed, 86 insertions(+), 4 deletions(-) diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index e689ba38..be7cca20 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -110,12 +110,15 @@ class Kernel extends ConsoleKernel $schedule->command('jushuitan:sync-shipping --days=40') ->dailyAt('01:28') ->withoutOverlapping(300) + ->appendOutputTo(storage_path('logs/jst-sync-shipping.log')) ->runInBackground(); - // 凌晨副本库最近一两天的订单/商品明细常未到齐(视图、订单商品表滞后), - // 早上再拉一次最近 3 天,补齐凌晨只写入单号、缺商品明细的包裹。 + // 凌晨副本库最近一两天的商品明细常未到齐(出库单常在发货次日下午才确认,视图随后才有商品); + // 白天副本库负载高、查询常超时断开,补拉放到晚上:每天 21:28 再拉最近 3 天, + // 补齐凌晨只写入单号、缺商品明细的包裹。 $schedule->command('jushuitan:sync-shipping --days=3') - ->dailyAt('07:28') + ->dailyAt('21:28') ->withoutOverlapping(120) + ->appendOutputTo(storage_path('logs/jst-sync-shipping.log')) ->runInBackground(); } diff --git a/modules/EcommerceFinance/Services/Jushuitan/ShippingReplicaSynchronizer.php b/modules/EcommerceFinance/Services/Jushuitan/ShippingReplicaSynchronizer.php index f55fdfe8..218aca7d 100644 --- a/modules/EcommerceFinance/Services/Jushuitan/ShippingReplicaSynchronizer.php +++ b/modules/EcommerceFinance/Services/Jushuitan/ShippingReplicaSynchronizer.php @@ -1115,6 +1115,7 @@ class ShippingReplicaSynchronizer private function emitPackage(int $pkgId, array $pkg, array $items, array $skus, array $freight, string $so, ?string $tracking, $now, bool $auto = false): int { $items = array_values(array_filter($items, fn ($it) => ! $this->isPricePatch($it['name'] ?? null))); + $items = $this->dropZeroQtyPlaceholders($items); $n = count($items); $tk = ltrim(trim((string) ($tracking ?? '')), '@'); if ($tk !== '') { @@ -2458,17 +2459,49 @@ class ShippingReplicaSynchronizer return $payload; } + /** jst_oms_shipping 文本列长度上限:上游偶有超长值(如组合装商品ID),超长截断而不是整批写入失败。 */ + private const COLUMN_LIMITS = [ + 'express_no' => 64, 'trade_no' => 64, 'item_id' => 64, 'barcode' => 128, 'product_name' => 512, + 'receiver_state' => 64, 'receiver_city' => 64, 'receiver_district' => 64, 'receiver_name' => 128, + 'receiver_mobile' => 64, 'receiver_phone' => 64, 'shop_name' => 255, 'express_name1' => 128, + 'express_name2' => 128, 'warehouse_name' => 128, 'supplier_name' => 128, 'platform' => 64, + 'product_code' => 128, 'pkg_count' => 32, 'm_name' => 500, + ]; + private function cleanPayload(array $data): array { foreach ($data as $k => $v) { if (is_string($v)) { - $data[$k] = $this->cleanStr($v); + $v = $this->cleanStr($v); + $limit = self::COLUMN_LIMITS[$k] ?? null; + if ($limit !== null && mb_strlen($v) > $limit) { + $v = mb_substr($v, 0, $limit); + } + $data[$k] = $v; } } return $data; } + /** + * 订单商品表里平台原始商品行(数量 0、金额 0)并没有实际出库,聚水潭工作平台也不显示; + * 包裹已有实发商品时不写入这类行,避免多出一个"条码"(其实是平台商品编号)。 + * + * @param list> $items + * @return list> + */ + private function dropZeroQtyPlaceholders(array $items): array + { + $zero = static fn ($v): bool => $v !== null && $v !== '' && is_numeric($v) && (float) $v == 0.0; + $placeholder = static fn (array $it): bool => $zero($it['qty'] ?? null) + && (($it['amount'] ?? null) === null || ($it['amount'] ?? null) === '' || $zero($it['amount'])); + $keep = array_values(array_filter($items, static fn (array $it): bool => ! $placeholder($it))); + $hasShipped = array_filter($keep, static fn (array $it): bool => is_numeric($it['qty'] ?? null) && (float) $it['qty'] > 0); + + return ($hasShipped !== [] && count($keep) < count($items)) ? $keep : $items; + } + private function cleanStr(string $v): string { $v = str_replace("\0", '', $v); diff --git a/tests/Unit/EcommerceFinance/ShippingReplicaSynchronizerTest.php b/tests/Unit/EcommerceFinance/ShippingReplicaSynchronizerTest.php index 41fafcc6..2a1fa97d 100644 --- a/tests/Unit/EcommerceFinance/ShippingReplicaSynchronizerTest.php +++ b/tests/Unit/EcommerceFinance/ShippingReplicaSynchronizerTest.php @@ -1388,6 +1388,52 @@ class ShippingReplicaSynchronizerTest extends TestCase $this->assertSame('江苏省', $db->table('jst_oms_shipping')->where('source_id', 6000)->value('receiver_state')); } + public function test_zero_quantity_order_item_placeholder_is_dropped_when_package_has_shipped_items(): void + { + $factory = $this->createMock(UpstreamReadOnlyQueryFactory::class); + $factory->method('forSource')->willReturn($this->createMock(UpstreamReadOnlyQuery::class)); + $synchronizer = new ShippingReplicaSynchronizer($factory); + $method = new \ReflectionMethod($synchronizer, 'dropZeroQtyPlaceholders'); + $method->setAccessible(true); + + $items = [ + ['id' => 4343977, 'sku' => '923141324442', 'shopid' => '3780268746917151177', 'qty' => 0, 'amount' => '0.00'], + ['id' => 4344020, 'sku' => 'CBZ000216N1071V01', 'shopid' => '3780268746917151177', 'qty' => 1, 'amount' => '81.66'], + ]; + $kept = $method->invoke($synchronizer, $items); + $this->assertCount(1, $kept); + $this->assertSame('CBZ000216N1071V01', $kept[0]['sku']); + + // 只有占位行时保持原样(不把包裹变成无商品) + $onlyZero = [['id' => 1, 'sku' => 'X', 'qty' => 0, 'amount' => 0]]; + $this->assertSame($onlyZero, $method->invoke($synchronizer, $onlyZero)); + + // 数量未知(null)的行不当作占位行 + $unknownQty = [['id' => 2, 'sku' => 'A', 'qty' => null, 'amount' => null], ['id' => 3, 'sku' => 'B', 'qty' => 1, 'amount' => 5]]; + $this->assertCount(2, $method->invoke($synchronizer, $unknownQty)); + + // 数量 0 但有金额的行保留 + $paidZero = [['id' => 4, 'sku' => 'A', 'qty' => 0, 'amount' => 3.5], ['id' => 5, 'sku' => 'B', 'qty' => 1, 'amount' => 5]]; + $this->assertCount(2, $method->invoke($synchronizer, $paidZero)); + } + + public function test_overlong_text_values_are_truncated_to_column_limits(): void + { + $factory = $this->createMock(UpstreamReadOnlyQueryFactory::class); + $factory->method('forSource')->willReturn($this->createMock(UpstreamReadOnlyQuery::class)); + $synchronizer = new ShippingReplicaSynchronizer($factory); + $method = new \ReflectionMethod($synchronizer, 'cleanPayload'); + $method->setAccessible(true); + + $itemId = 'JST15438196_14138566_ZH-GBSFY-YMJ011901AN1403V01-1-GBSFY-YMJ011901BN1403V01-1'; + $this->assertGreaterThan(64, mb_strlen($itemId)); + $out = $method->invoke($synchronizer, ['item_id' => $itemId, 'barcode' => 'ABC', 'qty' => 2]); + $this->assertSame(64, mb_strlen($out['item_id'])); + $this->assertSame(mb_substr($itemId, 0, 64), $out['item_id']); + $this->assertSame('ABC', $out['barcode']); + $this->assertSame(2, $out['qty']); + } + private function setScanBatchLimit(ShippingReplicaSynchronizer $synchronizer, string $property, int $limit): void { $reflection = new \ReflectionProperty($synchronizer, $property); -- 2.47.2