fix(发货数据): 快递公司/仓库按实际出库单取,同步命令内存上限调至 4G
- 发货明细(jst_order_express_details)可能停在作废前的旧分配(如先分工厂代发仓、后改外采由总仓出库;或换单后仍是旧快递公司), 工作平台按实际出库单显示。快递公司与出库单不一致或为空时改用出库单 logistics_company, 仓库按出库单 wms_co_id 映射(从快递公司一致的明细行学习仓库名,扫描结束统一回填)。 - 9 月核对:快递名称不一致 434/331,612,仓库不一致 85,其中外采快递、线下快递、换单类为此原因。 - jushuitan:sync-shipping 内存上限 1536M -> 4096M:整月窗口重跑在扫描后段内存耗尽(退出码 255)。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014nvGZkg27YMEdhry6eiuwf
This commit is contained in:
parent
c515da02d0
commit
63799de416
@ -24,7 +24,7 @@ class SyncJushuitanShipping extends Command
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
@ini_set('memory_limit', '1536M');
|
||||
@ini_set('memory_limit', '4096M'); // 整月窗口重跑约需 2GB+,原 1536M 会在扫描后段内存耗尽
|
||||
@set_time_limit(0);
|
||||
$from = $this->option('from') ? Carbon::parse($this->option('from')) : null;
|
||||
$to = $this->option('to') ? Carbon::parse($this->option('to')) : null;
|
||||
|
||||
@ -109,6 +109,18 @@ class ShippingReplicaSynchronizer
|
||||
/** @var array<string,string> io_id => 出库时间 io_date(工作平台发货时间口径) */
|
||||
private array $ioDate = [];
|
||||
|
||||
/** @var array<string,string> io_id => 出库单快递公司(工作平台按实际出库单取,发货明细可能停在作废前的旧分配) */
|
||||
private array $ioLc = [];
|
||||
|
||||
/** @var array<string,string> io_id => 出库单发货仓 wms_co_id */
|
||||
private array $ioWms = [];
|
||||
|
||||
/** @var array<string,array<string,int>> wms_co_id => [仓库名 => 次数]:从快递公司一致的发货明细行学习仓库名 */
|
||||
private array $wmsNames = [];
|
||||
|
||||
/** @var array<int,string> 包裹 source_id => 待补仓库的 wms_co_id(扫描时尚未学到仓库名,扫描结束后统一回填) */
|
||||
private array $pendingWarehouse = [];
|
||||
|
||||
/** @var array<string,string> 最近一次 loadViewItems 的 io_id => 视图店铺名(发货明细缺店铺时兜底) */
|
||||
private array $ioShop = [];
|
||||
/** @var array<string,array{l_id:mixed,send_date:mixed,status:?string,online:bool}> 已查询订单元数据缓存 o_id=>字段 */
|
||||
@ -169,6 +181,10 @@ class ShippingReplicaSynchronizer
|
||||
$this->doneTrackSo = [];
|
||||
$this->excludedSourceIds = [];
|
||||
$this->ioDate = [];
|
||||
$this->ioLc = [];
|
||||
$this->ioWms = [];
|
||||
$this->wmsNames = [];
|
||||
$this->pendingWarehouse = [];
|
||||
if ($windowed) {
|
||||
$this->preloadDws($from, $to);
|
||||
$this->preloadOutboundMap($fromS, $toS);
|
||||
@ -181,6 +197,7 @@ class ShippingReplicaSynchronizer
|
||||
));
|
||||
}
|
||||
$total = $this->syncExpress(true, $fromS, $toS, $limit, $now, $progress);
|
||||
$this->applyPendingWarehouses($progress);
|
||||
$this->orderMetadata = []; // 全表扫描后释放(数百万订单),后续各步不再使用
|
||||
gc_collect_cycles();
|
||||
$auto = $this->syncAutoShip($fromS, $toS, $now, $progress);
|
||||
@ -199,6 +216,7 @@ class ShippingReplicaSynchronizer
|
||||
}
|
||||
} else {
|
||||
$total = $this->syncExpress(false, $fromS, $toS, $limit, $now, $progress);
|
||||
$this->applyPendingWarehouses($progress);
|
||||
}
|
||||
|
||||
if ($windowed && ! $skipFreight && $this->staleMirrorPackages === 0) {
|
||||
@ -455,6 +473,7 @@ class ShippingReplicaSynchronizer
|
||||
$shipTime = ($io !== '' ? $this->ts($this->ioDate[$io] ?? null) : null)
|
||||
?? $this->ts($r['send_time'] ?? null) ?? $this->ts($r['jo_send'] ?? null);
|
||||
$pkg = $this->pkgFromExpress($r, $tracking, $shipTime);
|
||||
$pkg = $this->withOutboundCarrier($io, $r, $pkg, $pkgId);
|
||||
$pkg['platform'] = $this->platformOf($io, $pkg['platform']);
|
||||
$pkg = $this->withViewAddr($io, $pkg);
|
||||
$total += $this->emitPackage($pkgId, $pkg, $items, $skus, $freight, $so, $tracking, $now);
|
||||
@ -1186,6 +1205,80 @@ class ShippingReplicaSynchronizer
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 快递公司/仓库以实际出库单为准(工作平台口径):发货明细可能停在作废前的旧分配
|
||||
* (如先分给工厂代发仓、后改外采由总仓出库;或换单后仍是旧快递公司)。
|
||||
* 出库单快递公司与发货明细一致时不改,并借此学习 wms_co_id => 仓库名。
|
||||
*/
|
||||
private function withOutboundCarrier(string $io, array $r, array $pkg, int $pkgId): array
|
||||
{
|
||||
if ($io === '') {
|
||||
return $pkg;
|
||||
}
|
||||
$lc = $this->ioLc[$io] ?? '';
|
||||
if ($lc === '') {
|
||||
return $pkg;
|
||||
}
|
||||
$wms = $this->ioWms[$io] ?? '';
|
||||
$eLc = trim((string) ($r['logistic_company_name'] ?? ''));
|
||||
$eWh = trim((string) ($r['warehouse_name'] ?? ''));
|
||||
if ($lc === $eLc) {
|
||||
if ($wms !== '' && $eWh !== '' && $eWh !== '{未指定发货仓}') {
|
||||
$this->wmsNames[$wms][$eWh] = ($this->wmsNames[$wms][$eWh] ?? 0) + 1;
|
||||
}
|
||||
|
||||
return $pkg;
|
||||
}
|
||||
$pkg['express_name1'] = $lc;
|
||||
if ($wms !== '') {
|
||||
$name = $this->wmsName($wms);
|
||||
if ($name !== null) {
|
||||
$pkg['warehouse_name'] = $name;
|
||||
} else {
|
||||
$this->pendingWarehouse[$pkgId] = $wms;
|
||||
}
|
||||
}
|
||||
|
||||
return $pkg;
|
||||
}
|
||||
|
||||
private function wmsName(string $wms): ?string
|
||||
{
|
||||
if (empty($this->wmsNames[$wms])) {
|
||||
return null;
|
||||
}
|
||||
$names = $this->wmsNames[$wms];
|
||||
arsort($names);
|
||||
|
||||
return (string) array_key_first($names);
|
||||
}
|
||||
|
||||
/** 扫描时还没学到仓库名的包裹:扫描结束后按学到的 wms_co_id => 仓库名统一回填。 */
|
||||
private function applyPendingWarehouses(?callable $progress = null): void
|
||||
{
|
||||
$byName = [];
|
||||
foreach ($this->pendingWarehouse as $pkgId => $wms) {
|
||||
$name = $this->wmsName($wms);
|
||||
if ($name !== null) {
|
||||
$byName[$name][] = $pkgId;
|
||||
}
|
||||
}
|
||||
$n = 0;
|
||||
foreach ($byName as $name => $ids) {
|
||||
foreach (array_chunk($ids, 500) as $chunk) {
|
||||
$n += $this->db->table('jst_oms_shipping')->whereIn('source_id', $chunk)
|
||||
->where(function ($q) use ($name): void {
|
||||
$q->whereNull('warehouse_name')->orWhere('warehouse_name', '!=', $name);
|
||||
})
|
||||
->update(['warehouse_name' => $name]);
|
||||
}
|
||||
}
|
||||
if ($progress && $this->pendingWarehouse !== []) {
|
||||
$progress(sprintf('出库单仓库回填:待补 %d 个包裹,更新 %d 行', count($this->pendingWarehouse), $n));
|
||||
}
|
||||
$this->pendingWarehouse = [];
|
||||
}
|
||||
|
||||
/** 发货明细表行 → 包裹级字段(省市区从地址拆) */
|
||||
private function pkgFromExpress(array $r, ?string $tracking, ?string $shipTime): array
|
||||
{
|
||||
@ -1390,7 +1483,7 @@ class ShippingReplicaSynchronizer
|
||||
/** 窗口同步按 io_date 一次预载,避免对无可用索引的 l_id/o_id 反复执行 IN 查询。 */
|
||||
private function preloadOutboundMap(string $fromS, string $toS): void
|
||||
{
|
||||
$sql = 'SELECT l_id, o_id, io_id, io_date, status FROM ' . self::OUTBOUND . ' WHERE io_date >= ? AND io_date < ?';
|
||||
$sql = 'SELECT l_id, o_id, io_id, io_date, status, logistics_company, wms_co_id FROM ' . self::OUTBOUND . ' WHERE io_date >= ? AND io_date < ?';
|
||||
$this->outboundMap = $this->mapOutboundRows($this->selectWithReconnect($sql, [$fromS, $toS]));
|
||||
}
|
||||
|
||||
@ -1429,6 +1522,14 @@ class ShippingReplicaSynchronizer
|
||||
if (! empty($r['io_date'])) {
|
||||
$this->ioDate[$rio] = (string) $r['io_date'];
|
||||
}
|
||||
$lc = trim((string) ($r['logistics_company'] ?? ''));
|
||||
if ($lc !== '') {
|
||||
$this->ioLc[$rio] = $lc;
|
||||
}
|
||||
$wms = trim((string) ($r['wms_co_id'] ?? ''));
|
||||
if ($wms !== '' && $wms !== '0') {
|
||||
$this->ioWms[$rio] = $wms;
|
||||
}
|
||||
$l = ltrim(trim((string) ($r['l_id'] ?? '')), '@');
|
||||
$o = (string) ($r['o_id'] ?? '');
|
||||
$m['ioL'][$rio] = $l;
|
||||
@ -1474,7 +1575,7 @@ class ShippingReplicaSynchronizer
|
||||
}
|
||||
$q = array_keys($q);
|
||||
$ph = implode(',', array_fill(0, count($q), '?'));
|
||||
foreach ($this->selectWithReconnect('SELECT l_id, io_id, io_date, status, o_id FROM ' . self::OUTBOUND . " WHERE l_id IN ({$ph})", $q) as $r) {
|
||||
foreach ($this->selectWithReconnect('SELECT l_id, io_id, io_date, status, o_id, logistics_company, wms_co_id FROM ' . self::OUTBOUND . " WHERE l_id IN ({$ph})", $q) as $r) {
|
||||
$this->addIoRow($map, $r, true, false);
|
||||
}
|
||||
}
|
||||
@ -1483,7 +1584,7 @@ class ShippingReplicaSynchronizer
|
||||
continue;
|
||||
}
|
||||
$ph = implode(',', array_fill(0, count($chunk), '?'));
|
||||
foreach ($this->selectWithReconnect('SELECT o_id, io_id, io_date, l_id, status FROM ' . self::OUTBOUND . " WHERE o_id IN ({$ph})", $chunk) as $r) {
|
||||
foreach ($this->selectWithReconnect('SELECT o_id, io_id, io_date, l_id, status, logistics_company, wms_co_id FROM ' . self::OUTBOUND . " WHERE o_id IN ({$ph})", $chunk) as $r) {
|
||||
$this->addIoRow($map, $r, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1178,6 +1178,55 @@ class ShippingReplicaSynchronizerTest extends TestCase
|
||||
$this->assertSame(0.0, $volume->invoke($sync, 10, 3, 2, 2.0, '普通仓', '中通礼品单'));
|
||||
}
|
||||
|
||||
public function test_stale_express_detail_carrier_and_warehouse_follow_the_actual_outbound(): void
|
||||
{
|
||||
$upstream = $this->createMock(UpstreamReadOnlyQuery::class);
|
||||
$upstream->method('select')->willReturn([
|
||||
// 先分给工厂代发仓后作废,再由总仓按外采出库(实际发货单)
|
||||
['l_id' => 'JT1', 'o_id' => 'o1', 'io_id' => '11', 'io_date' => '2026-08-29 17:52:09', 'status' => 'Delete', 'logistics_company' => '浩浦极兔', 'wms_co_id' => 15170678],
|
||||
['l_id' => 'JT1', 'o_id' => 'o1', 'io_id' => '12', 'io_date' => '2026-09-01 10:17:16', 'status' => 'Confirmed', 'logistics_company' => '外采快递', 'wms_co_id' => 14138566],
|
||||
['l_id' => 'DP2', 'o_id' => 'o2', 'io_id' => '21', 'io_date' => '2026-09-02 09:00:00', 'status' => 'Confirmed', 'logistics_company' => '外采快递', 'wms_co_id' => 14138566],
|
||||
['l_id' => 'YT3', 'o_id' => 'o3', 'io_id' => '31', 'io_date' => '2026-09-02 09:00:00', 'status' => 'Confirmed', 'logistics_company' => '线下快递', 'wms_co_id' => 15137729],
|
||||
]);
|
||||
$factory = $this->createMock(UpstreamReadOnlyQueryFactory::class);
|
||||
$factory->method('forSource')->willReturn($upstream);
|
||||
$sync = new ShippingReplicaSynchronizer($factory);
|
||||
$preload = new \ReflectionMethod($sync, 'preloadOutboundMap');
|
||||
$preload->setAccessible(true);
|
||||
$preload->invoke($sync, '2026-08-25 00:00:00', '2026-09-05 00:00:00');
|
||||
$map = (new \ReflectionProperty($sync, 'outboundMap'));
|
||||
$map->setAccessible(true);
|
||||
$ioFor = new \ReflectionMethod($sync, 'ioFor');
|
||||
$ioFor->setAccessible(true);
|
||||
$this->assertSame('12', $ioFor->invoke($sync, $map->getValue($sync), 'JT1', 'o1'));
|
||||
$carrier = new \ReflectionMethod($sync, 'withOutboundCarrier');
|
||||
$carrier->setAccessible(true);
|
||||
|
||||
// 仓库名尚未学到:快递公司按出库单改,仓库待扫描结束后回填
|
||||
$early = $carrier->invoke($sync, '31', ['logistic_company_name' => '霸州百世快运-雅柏', 'warehouse_name' => '工厂-河北雅柏代发仓'],
|
||||
['express_name1' => '霸州百世快运-雅柏', 'warehouse_name' => '工厂-河北雅柏代发仓'], 3);
|
||||
$this->assertSame('线下快递', $early['express_name1']);
|
||||
$this->assertSame('工厂-河北雅柏代发仓', $early['warehouse_name']);
|
||||
$pending = new \ReflectionProperty($sync, 'pendingWarehouse');
|
||||
$pending->setAccessible(true);
|
||||
$this->assertSame([3 => '15137729'], $pending->getValue($sync));
|
||||
|
||||
// 快递公司一致的行不改,并学习 wms_co_id => 仓库名
|
||||
$same = $carrier->invoke($sync, '21', ['logistic_company_name' => '外采快递', 'warehouse_name' => '常州简尚家居有限公司'],
|
||||
['express_name1' => '外采快递', 'warehouse_name' => '常州简尚家居有限公司'], 2);
|
||||
$this->assertSame('外采快递', $same['express_name1']);
|
||||
|
||||
// 发货明细停在作废前的工厂代发分配:快递公司、仓库都按实际出库单
|
||||
$stale = $carrier->invoke($sync, '12', ['logistic_company_name' => '浩浦极兔', 'warehouse_name' => '工厂-河北浩浦代发仓'],
|
||||
['express_name1' => '浩浦极兔', 'warehouse_name' => '工厂-河北浩浦代发仓'], 1);
|
||||
$this->assertSame('外采快递', $stale['express_name1']);
|
||||
$this->assertSame('常州简尚家居有限公司', $stale['warehouse_name']);
|
||||
|
||||
// 出库单没有快递公司时保留发货明细
|
||||
$none = $carrier->invoke($sync, '', ['logistic_company_name' => '中通速递'], ['express_name1' => '中通速递', 'warehouse_name' => 'X'], 4);
|
||||
$this->assertSame('中通速递', $none['express_name1']);
|
||||
}
|
||||
|
||||
private function setScanBatchLimit(ShippingReplicaSynchronizer $synchronizer, string $property, int $limit): void
|
||||
{
|
||||
$reflection = new \ReflectionProperty($synchronizer, $property);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user