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