From 47a70406f6425b4ffb71c5f90ca8e68c6bfe75e4 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Thu, 10 Sep 2026 20:47:15 +0800 Subject: [PATCH] fix(acceptance): reuse initialized pallet fixtures --- deploy/sql/头程数据库统一增量-MySQL8.sql | 11 +++++- scripts/admin-test-env-acceptance.mjs | 37 +------------------ scripts/admin-test-env-modules/README.md | 2 +- .../inbound-measurement.mjs | 33 ++--------------- .../it-test-wait-split-bulk-contract.test.mjs | 8 ++-- .../phase3-container-loading.mjs | 16 +++++--- .../admin-test-env-modules/phase3-loading.mjs | 17 +-------- .../phase3-order-splitting.mjs | 4 +- .../phase3-producer-chain-contract.test.mjs | 4 +- .../smart-loading-channel-group.mjs | 14 +++++++ .../main/resources/db/phase2-pallet-seed.sql | 11 +++++- ...lWarehouseResourceSeedSqlContractTest.java | 3 ++ 12 files changed, 65 insertions(+), 95 deletions(-) create mode 100644 scripts/admin-test-env-modules/smart-loading-channel-group.mjs diff --git a/deploy/sql/头程数据库统一增量-MySQL8.sql b/deploy/sql/头程数据库统一增量-MySQL8.sql index 776b0e0b1..d939bf45c 100644 --- a/deploy/sql/头程数据库统一增量-MySQL8.sql +++ b/deploy/sql/头程数据库统一增量-MySQL8.sql @@ -6988,7 +6988,8 @@ ON DUPLICATE KEY UPDATE capacity_cbm = VALUES(capacity_cbm), weight_kg = VALUES(weight_kg), color = VALUES(color), - remark = VALUES(remark); + remark = VALUES(remark), + deleted = 0; DROP PROCEDURE IF EXISTS assert_domestic_pallet_seed; DELIMITER // @@ -7010,6 +7011,14 @@ BEGIN ) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '国内作业仓木托盘、塑料托盘初始化不完整'; END IF; + IF (SELECT COUNT(*) + FROM wh_pallet + WHERE tenant_id = 0 + AND deleted = 0 + AND ((warehouse_id = 193000000000200001 AND pallet_code IN ('TP-001','YW-TP-002')) + OR (warehouse_id = 193000000000200002 AND pallet_code IN ('TP-002','DG-TP-002')))) <> 4 THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '国内作业仓固定可复用托盘编码初始化不完整'; + END IF; END// DELIMITER ; CALL assert_domestic_pallet_seed(); diff --git a/scripts/admin-test-env-acceptance.mjs b/scripts/admin-test-env-acceptance.mjs index a92d0ef0b..6e83c061d 100644 --- a/scripts/admin-test-env-acceptance.mjs +++ b/scripts/admin-test-env-acceptance.mjs @@ -1317,7 +1317,7 @@ export class AcceptanceRun { const createdKeys = ['orders', 'ocrInbound', 'testInbound', 'inboundMeasurement'] const contextKeys = ['inboundWeightOcr', 'ocrInbound', 'testInbound', 'createdInbound', 'phase3LoadingRequireSharedOverflow', 'phase3LoadingIncrementalPackageCount', - 'phase3LoadingIncrementalDetailCount', 'orderProfile', 'measurementPalletOverride'] + 'phase3LoadingIncrementalDetailCount', 'orderProfile'] const createdSnapshot = new Map(createdKeys.map((key) => [key, { present: Object.hasOwn(this.createdResources, key), value: this.createdResources[key], }])) @@ -1362,9 +1362,6 @@ export class AcceptanceRun { : { forced: false, disabled: [] } this.context.phase3LoadingRequireSharedOverflow = requireSharedOverflow try { - if (requireSharedOverflow) { - this.context.measurementPalletOverride = await this.createPhase3FreshMeasurementPallet(warehouseId) - } const measurement = await import(new URL('./admin-test-env-modules/inbound-measurement.mjs', import.meta.url)) await this.executeModule('inbound-measurement-phase3-loading-fixture', () => measurement.run(this)) } finally { @@ -1397,7 +1394,6 @@ export class AcceptanceRun { const incrementalMarker = `${originalMarker}-LOAD${suffix}-INC` this.marker = incrementalMarker this.context.phase3LoadingRequireSharedOverflow = false - delete this.context.measurementPalletOverride this.context.phase3LoadingIncrementalPackageCount = 2 this.context.phase3LoadingIncrementalDetailCount = 4 // orders.mjs derives the actual SKU/package shape from waitSplitVariant. Override that @@ -1609,37 +1605,6 @@ export class AcceptanceRun { { scenario: 'phase3-phase4-producer-fixture', deploymentBlocker: true }) } - async createPhase3FreshMeasurementPallet(warehouseId) { - const module = 'phase3-loading-fixture' - const options = { scenario: 'phase3-loading-a0-fixture', deploymentBlocker: true } - const payload = { - warehouseId, palletType: 'STANDARD', capacityCbm: 1.5, weightKg: 25, - color: 'ORANGE', remark: `${this.marker}-inventory-snapshot-a0`, - } - const createResponse = await this.request('POST', '/api/platform/v1/pallets?tenantId=0', payload) - const palletId = String(this.apiSuccess(module, '创建本轮库存快照专用称重托盘', createResponse, - `warehouseId=${warehouseId}`, options) || '') - if (!palletId) return null - - const detailResponse = await this.request('GET', - `/api/platform/v1/pallets/${encodeURIComponent(palletId)}?tenantId=0&warehouseId=${encodeURIComponent(warehouseId)}`) - const pallet = this.apiSuccess(module, '回读本轮库存快照专用称重托盘', detailResponse, - `palletId=${palletId}`, options) - const valid = String(pallet?.id || '') === palletId - && String(pallet?.warehouseId || '') === String(warehouseId) - && Boolean(String(pallet?.palletCode || '').trim()) - this.assert(module, '本轮专用称重托盘尚未复用历史占用身份', detailResponse, valid, - `palletId=${palletId}, palletCode=${pallet?.palletCode || 'missing'}`, options) - if (!valid) return null - - const fixture = { - id: palletId, code: String(pallet.palletCode), warehouseId: String(warehouseId), - marker: this.marker, retainedReason: '测量审核后成为正式库存占用,保留为开发验收证据', - } - this.createdResources.phase3LoadingFreshMeasurementPallet = fixture - return fixture - } - async listAvailableLocationsForA0(warehouseId, module, options) { const locations = [] const seen = new Set() diff --git a/scripts/admin-test-env-modules/README.md b/scripts/admin-test-env-modules/README.md index 11d777acc..201863e74 100644 --- a/scripts/admin-test-env-modules/README.md +++ b/scripts/admin-test-env-modules/README.md @@ -140,7 +140,7 @@ node --env-file="$env:USERPROFILE\.codex\chongchong-global-dev-acceptance.env" s 其中,无号 push 只冻结装柜计划、建立 bridge 并使预装可见,不得创建 PDA task;订舱放舱也不创建真实货柜或 allocation。港口取得真实柜号后,四字段 link 直接补全原模拟柜并创建 allocation,原 `containerBookingId/cabinetNo/loadingPlan/items` 不变;未关联、订舱未 `RELEASED` 或整份计划未确认时,PDA 查询必须返回业务码 `A0404`。唯一确认入口为 `POST /api/platform/v1/first-leg/loading-plans/{loadingPlanId}/confirm`,请求体只含当前计划 `expectedVersion` 并携带 `Idempotency-Key`;成功必须返回 `confirmationStatus=CONFIRMED`、`readiness.readyForPda=true` 和唯一 `readiness.pdaTaskId`。验收脚本禁止调用旧入库单级 `/preload/confirming`、`/preload/confirm`。 -计划在 `EDITING/LOADING` 可通过正式 assignments/PATCH/DELETE 端点增量;计划、任务和明细版本必须递增,PDA 四元身份不变,旧 `inventorySnapshotToken` 必须返回 `A0409 库存已变化,请刷新后重试`。完整国内货柜履约装柜 fixture 通过平台托盘 API 创建本轮专用称重托盘,再交给通用测量模块;当普通位不足时由 Warehouse 自然分配到 A0,随后仅使用公开 A0 换绑接口制造库存版本变化,不直连数据库或调用内部 Warehouse 接口。主国内货柜履约链强制完成该专项,海外仓业务生产矩阵的附加柜只负责跨区生产,不重复要求 A0;附加柜若自然落入 A0 仍执行同一验证。批量待分柜入口仍只复用 `TP-001`/`YW-TP-002`,不创建托盘。已拣项不可删或减量,未拣项可删;增量会把全拣的 `AWAITING_REVIEW` 撤回为 `IN_PROGRESS`,刷新 task detail 与 inventory 后续拣才可再次等待最终确认。最终唯一完成入口仍为 `POST /api/platform/v1/first-leg/pda-loading/tasks/{taskId}/complete`:保留同柜 `RELEASED` 订舱、bridge 与五类 ACTIVE 证据门禁;拖车状态和数量不是前置门禁。主事务提交后,零条有效拖车直接跳过,恰有一条时在独立事务尝试推进 `COMPLETED`,多条或并发冲突只记录拖车同步失败,不回滚已完成的 task/plan/inbound。PDA 结算可能先递增来源入库单版本或创建实装分片,完成动作必须在结算后重新锁定实际完成行并使用当前版本;HTTP finalizer 会回查 full/partial 履约分片,失败时保存 `requestId/traceId` 并断言 task/plan/inbound/operation 零漂移。后续模块必须复用 `phase3Loading` 发布的本轮 `taskId + inboundId + loadingPlanId + containerBookingId`,不得回退历史资源。 +计划在 `EDITING/LOADING` 可通过正式 assignments/PATCH/DELETE 端点增量;计划、任务和明细版本必须递增,PDA 四元身份不变,旧 `inventorySnapshotToken` 必须返回 `A0409 库存已变化,请刷新后重试`。完整国内货柜履约装柜 fixture 与批量待分柜入口都只随机复用初始化的 `TP-001`/`YW-TP-002`,任一缺失即报告部署阻断,不调用托盘新增接口;当普通位不足时由 Warehouse 自然分配到 A0,随后仅使用公开 A0 换绑接口制造库存版本变化,不直连数据库、创建库位或调用内部 Warehouse 接口。模拟柜的 `channelGroupCode` 统一由 `smart-loading-channel-group.mjs` 根据订单渠道映射。主国内货柜履约链强制完成该专项,海外仓业务生产矩阵的附加柜只负责跨区生产,不重复要求 A0;附加柜若自然落入 A0 仍执行同一验证。已拣项不可删或减量,未拣项可删;增量会把全拣的 `AWAITING_REVIEW` 撤回为 `IN_PROGRESS`,刷新 task detail 与 inventory 后续拣才可再次等待最终确认。最终唯一完成入口仍为 `POST /api/platform/v1/first-leg/pda-loading/tasks/{taskId}/complete`:保留同柜 `RELEASED` 订舱、bridge 与五类 ACTIVE 证据门禁;拖车状态和数量不是前置门禁。主事务提交后,零条有效拖车直接跳过,恰有一条时在独立事务尝试推进 `COMPLETED`,多条或并发冲突只记录拖车同步失败,不回滚已完成的 task/plan/inbound。PDA 结算可能先递增来源入库单版本或创建实装分片,完成动作必须在结算后重新锁定实际完成行并使用当前版本;HTTP finalizer 会回查 full/partial 履约分片,失败时保存 `requestId/traceId` 并断言 task/plan/inbound/operation 零漂移。后续模块必须复用 `phase3Loading` 发布的本轮 `taskId + inboundId + loadingPlanId + containerBookingId`,不得回退历史资源。 动态增量还会读取计划明细中的服务端 `totalPackageCount/remainingPackageCount`,断言 `remaining = total - assigned`;随后尝试把箱数调整为权威总数加一,必须得到 `A0400`,并回查 diff --git a/scripts/admin-test-env-modules/inbound-measurement.mjs b/scripts/admin-test-env-modules/inbound-measurement.mjs index 1326096c1..7ccb05bc8 100644 --- a/scripts/admin-test-env-modules/inbound-measurement.mjs +++ b/scripts/admin-test-env-modules/inbound-measurement.mjs @@ -30,13 +30,6 @@ function validPdf(response) { async function reusablePallets(context, warehouseId) { const cacheKey = asId(warehouseId) - const override = context.context?.measurementPalletOverride - if (asId(override?.warehouseId) === cacheKey && asId(override?.id) - && String(override?.code || '').trim()) { - context.assert(MODULE, '使用编排层提供的本轮专用称重托盘', null, true, - `warehouseId=${warehouseId}, palletId=${asId(override.id)}`, OPTIONS) - return [{ id: asId(override.id), code: String(override.code) }] - } const allowAnyExistingPallet = context.context?.allowAnyExistingPallet === true const scopedCacheKey = `${cacheKey}:${allowAnyExistingPallet ? 'any' : 'fixed'}` if (reusablePalletsByWarehouse.has(scopedCacheKey)) return reusablePalletsByWarehouse.get(scopedCacheKey) @@ -64,29 +57,11 @@ async function reusablePallets(context, warehouseId) { const exact = (page?.records || []).find((item) => asId(item?.warehouseId) === cacheKey && String(item?.palletCode || '') === palletCode && asId(item?.id)) context.assert(MODULE, `查询原有称重托盘 ${palletCode}`, response, true, - `warehouseId=${warehouseId}, matched=${exact?.palletCode || 'missing'}, fallback=${exact ? 'not-needed' : 'create-marker-owned'}`, OPTIONS) + `warehouseId=${warehouseId}, matched=${exact?.palletCode || 'missing'}, fallback=disabled`, OPTIONS) if (!exact) { - // A newly selected development warehouse may not have the historical fixture pallets. - // Create one marker-owned pallet through the public platform API rather than coupling - // measurement acceptance to another warehouse's seed data. - const createResponse = await context.request('POST', '/api/platform/v1/pallets?tenantId=0', { - warehouseId, palletType: 'STANDARD', capacityCbm: 1.5, weightKg: 25, - color: 'ORANGE', remark: `${context.marker}-measurement-fixture`, - }) - const createdId = asId(context.apiSuccess(MODULE, '创建本轮称重托盘', createResponse, - `warehouseId=${warehouseId}`, OPTIONS)) - if (!createdId) return null - const detailResponse = await context.request('GET', - `/api/platform/v1/pallets/${encodeURIComponent(createdId)}?tenantId=0&warehouseId=${encodeURIComponent(warehouseId)}`) - const created = context.apiSuccess(MODULE, '回读本轮称重托盘', detailResponse, - `warehouseId=${warehouseId}, palletId=${createdId}`, OPTIONS) - const valid = asId(created?.id) === createdId && asId(created?.warehouseId) === cacheKey - && String(created?.palletCode || '').trim() - context.assert(MODULE, '新建称重托盘归属当前仓库且编码完整', detailResponse, valid, - `warehouseId=${warehouseId}, palletId=${createdId}, palletCode=${created?.palletCode || 'missing'}`, OPTIONS) - if (!valid) return null - pallets.push({ id: createdId, code: String(created.palletCode) }) - break + context.assert(MODULE, '固定称重托盘主数据完整', response, false, + `warehouseId=${warehouseId}, missing=${palletCode}`, OPTIONS) + return null } pallets.push({ id: asId(exact.id), code: palletCode }) } diff --git a/scripts/admin-test-env-modules/it-test-wait-split-bulk-contract.test.mjs b/scripts/admin-test-env-modules/it-test-wait-split-bulk-contract.test.mjs index efd755ca0..6048ff9c7 100644 --- a/scripts/admin-test-env-modules/it-test-wait-split-bulk-contract.test.mjs +++ b/scripts/admin-test-env-modules/it-test-wait-split-bulk-contract.test.mjs @@ -28,14 +28,14 @@ test('随机线路只固定义乌仓并从客户真实可计价候选选择', () assert.doesNotMatch(runner, /channel-name|channel-code/) }) -test('称重优先复用固定托盘并允许为无基线仓创建本轮托盘', () => { +test('称重只随机复用固定托盘且不创建新托盘', () => { assert.match(measurement, /REUSABLE_PALLET_CODES = Object\.freeze\(\['TP-001', 'YW-TP-002'\]\)/) assert.match(measurement, /context\.request\('GET',[\s\S]*\/api\/platform\/v1\/pallets\?page=1&size=20/) assert.match(measurement, /randomlyReusePalletIds\(candidates, palletCount/) assert.match(measurement, /随机复用原有称重托盘/) - assert.match(measurement, /context\.request\('POST', '\/api\/platform\/v1\/pallets\?tenantId=0'/) - assert.match(measurement, /创建本轮称重托盘/) - assert.match(measurement, /measurement-fixture/) + assert.match(measurement, /固定称重托盘主数据完整/) + assert.doesNotMatch(measurement, /context\.request\('POST', '\/api\/platform\/v1\/pallets/) + assert.doesNotMatch(acceptance, /createPhase3FreshMeasurementPallet/) }) test('称重脚本不新增库位且容量不足由仓库服务复用 A0 公共位', () => { diff --git a/scripts/admin-test-env-modules/phase3-container-loading.mjs b/scripts/admin-test-env-modules/phase3-container-loading.mjs index 5b1fe75b8..174eb2f69 100644 --- a/scripts/admin-test-env-modules/phase3-container-loading.mjs +++ b/scripts/admin-test-env-modules/phase3-container-loading.mjs @@ -3,6 +3,7 @@ import { diffPushPreloadStability, projectPushPreloadStability } from './push-pr import { containerFieldGaps, loadingListFieldGaps, preloadFieldGaps } from './fulfillment-list-field-contract.mjs' import { ensureMatsonCarrierPartner } from './carrier-partner-fixture.mjs' import { iso6346ContainerNumber } from './iso6346-container-number.mjs' +import { channelGroup } from './smart-loading-channel-group.mjs' const MODULE = 'phase3-container-loading' const OPTIONS = { scenario: 'phase3-container-smart-loading-pda', deploymentBlocker: true } @@ -208,7 +209,7 @@ export async function run(context) { context.assert(MODULE, '订舱渠道与仓库快照完整', inboundResponse, Boolean(channelCode && channelName && warehouseName), `channelCode=${channelCode || 'missing'}, warehouseId=${warehouseId}, warehouseName=${warehouseName || 'missing'}`, OPTIONS) const simulationResponse = await request(context, 'POST', CONTAINERS, { - channelCode, warehouseId, containerType: primaryRequirement.containerType, + channelGroupCode: channelGroup(channelCode), warehouseId, containerType: primaryRequirement.containerType, capacityCbm: primaryRequirement.capacityCbm, maxWeightKg: primaryRequirement.maxWeightKg, }) const simulation = context.apiSuccess(MODULE, '新增内部模拟柜', simulationResponse, @@ -216,7 +217,8 @@ export async function run(context) { const containerId = id(simulation?.id) context.assert(MODULE, '模拟柜由服务端生成稳定身份且不伪造真实柜号或订舱', simulationResponse, Boolean(containerId) && Boolean(simulation?.cabinetNo) && simulation?.containerNo == null - && simulation?.oceanBookingId == null && simulation?.channelCode === channelCode + && simulation?.oceanBookingId == null && simulation?.channelGroupCode === channelGroup(channelCode) + && simulation?.channelCode == null && id(simulation?.warehouseId) === warehouseId, `containerBookingId=${containerId || 'missing'}, cabinetNo=${simulation?.cabinetNo || 'missing'}, oceanBookingId=${id(simulation?.oceanBookingId) || 'null'}`, OPTIONS) if (!containerId) return @@ -240,7 +242,8 @@ export async function run(context) { } else { const foreignChannel = channelIsolation.pair.foreign const foreignContainerResponse = await request(context, 'POST', CONTAINERS, { - channelCode: foreignChannel.channelCode, warehouseId, containerType: primaryRequirement.containerType, + channelGroupCode: channelGroup(foreignChannel.channelCode), warehouseId, + containerType: primaryRequirement.containerType, capacityCbm: primaryRequirement.capacityCbm, maxWeightKg: primaryRequirement.maxWeightKg, }) const foreignContainer = context.apiSuccess(MODULE, '新增真实异渠道模拟柜', foreignContainerResponse, '', OPTIONS) @@ -288,9 +291,10 @@ export async function run(context) { context.assert(MODULE, '货柜详情保留候选货物路线约束', containerDetailResponse, (!containerDetail?.destinationWarehouseCode || !candidate.destinationWarehouseCode || containerDetail.destinationWarehouseCode === candidate.destinationWarehouseCode) - && containerDetail?.channelCode === channelCode + && containerDetail?.channelGroupCode === channelGroup(channelCode) + && containerDetail?.channelCode == null && ['EDITING', 'WAIT_LOADING'].includes(containerDetail?.planStatus), - `planStatus=${containerDetail?.planStatus || 'missing'}, channel=${containerDetail?.channelCode || 'missing'}`, OPTIONS) + `planStatus=${containerDetail?.planStatus || 'missing'}, channelGroup=${containerDetail?.channelGroupCode || 'missing'}`, OPTIONS) const containerNo = id(containerDetail?.containerNo || containerDetail?.cabinetNo) const planResponse = await request(context, 'GET', `${SMART}/plans/by-booking/${encodeURIComponent(containerId)}`) @@ -517,7 +521,7 @@ export async function run(context) { `containerBookingId=${id(mergedContainer?.containerBookingId) || 'missing'}, requirementId=${id(mergedContainer?.requirementId) || 'missing'}, allocationId=${id(mergedContainer?.allocationId) || 'missing'}`, OPTIONS) const duplicateSimulationResponse = await request(context, 'POST', CONTAINERS, { - channelCode, warehouseId, containerType: primaryRequirement.containerType, + channelGroupCode: channelGroup(channelCode), warehouseId, containerType: primaryRequirement.containerType, capacityCbm: primaryRequirement.capacityCbm, maxWeightKg: primaryRequirement.maxWeightKg, }) const duplicateSimulation = context.apiSuccess(MODULE, '新增重复容量占用负向模拟柜', duplicateSimulationResponse, '', OPTIONS) diff --git a/scripts/admin-test-env-modules/phase3-loading.mjs b/scripts/admin-test-env-modules/phase3-loading.mjs index 47d0eca4a..5c804fa85 100644 --- a/scripts/admin-test-env-modules/phase3-loading.mjs +++ b/scripts/admin-test-env-modules/phase3-loading.mjs @@ -1,6 +1,7 @@ import { diffPushPreloadStability, projectPushPreloadStability } from './push-preload-stability.mjs' import { isPureStatusNoop, visibleLoadingHistories } from './loading-history-noop.mjs' import { ensureMatsonCarrierPartner } from './carrier-partner-fixture.mjs' +import { channelGroup } from './smart-loading-channel-group.mjs' import { createHash } from 'node:crypto' const MODULE = 'phase3-loading' @@ -135,20 +136,6 @@ function inventorySignature(snapshot) { }))) } -function channelGroup(channelCode) { - const value = String(channelCode || '').toUpperCase() - if (value.startsWith('EXX_')) return 'EXX' - if (value.startsWith('MATSON_CLX_')) return 'MATSON_CLX' - if (value.startsWith('MATSON_MAX_')) return 'MATSON_MAX' - if (value.startsWith('HEDE_')) return 'HEDE' - if (value.startsWith('OA_LA_') || value === 'OA_PARCEL') return 'OA_LA' - if (value.startsWith('OA_CHICAGO_')) return 'OA_CHICAGO' - if (value.startsWith('OA_HOUSTON_')) return 'OA_HOUSTON' - if (value.startsWith('OA_SAVANNAH_')) return 'OA_SAVANNAH' - if (value === 'OA_NEW_YORK_TRUCK') return value - throw new Error(`unsupported smart-loading channel group: ${channelCode}`) -} - function operationsSignature(operations) { return JSON.stringify((operations || []).map((operation) => ({ id: id(operation?.id), operationId: operation?.operationId || null, @@ -226,7 +213,7 @@ export async function run(context) { maxWeightKg: 28000 }], } const simulationResponse = await request(context, 'POST', '/api/platform/v1/containers', { - channelCode: initial?.summary?.channelCode, warehouseId, containerType: '40HC', + channelGroupCode: channelGroup(initial?.summary?.channelCode), warehouseId, containerType: '40HC', capacityCbm: 76, maxWeightKg: 28000, }) const simulation = context.apiSuccess(MODULE, '新增三期内部模拟柜', simulationResponse, '', OPTIONS) diff --git a/scripts/admin-test-env-modules/phase3-order-splitting.mjs b/scripts/admin-test-env-modules/phase3-order-splitting.mjs index b57902ad2..b498c44c8 100644 --- a/scripts/admin-test-env-modules/phase3-order-splitting.mjs +++ b/scripts/admin-test-env-modules/phase3-order-splitting.mjs @@ -10,6 +10,7 @@ const PDA = '/api/platform/v1/first-leg/pda-loading' import { oceanBookingDraft } from './ocean-booking-fixture.mjs' import { iso6346ContainerNumber } from './iso6346-container-number.mjs' import { ensureInitialCalculated } from './inbound-fees.mjs' +import { channelGroup } from './smart-loading-channel-group.mjs' function id(value) { return value == null ? '' : String(value) } function codeOf(response) { return response?.body?.code == null ? 'missing' : String(response.body.code) } @@ -186,7 +187,8 @@ async function loadOneFragment(context, state) { const carrier = await ensureMatsonCarrierPartner(context, MODULE, OPTIONS) if (!carrier?.id) return null const simulationResponse = await request(context, 'POST', CONTAINERS, { - channelCode: state.channelCode, warehouseId, containerType: '40HC', capacityCbm: 76, maxWeightKg: 28000, + channelGroupCode: channelGroup(state.channelCode), warehouseId, + containerType: '40HC', capacityCbm: 76, maxWeightKg: 28000, }) const simulation = context.apiSuccess(MODULE, `为 ${sourceOrderNo} 新增内部模拟柜`, simulationResponse, '', OPTIONS) const containerId = id(simulation?.id) diff --git a/scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs b/scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs index a08fa5af1..0c2b126a5 100644 --- a/scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs @@ -434,7 +434,9 @@ test('container loading permits manual cross-channel allocation and keeps stable '换柜号后按稳定计划身份回查 PDA 任务', '换柜号不改变任务计划柜次柜次号和库存快照身份', ]) - assert.match(value, /containerDetail\?\.channelCode === channelCode/) + assert.match(value, /simulation\?\.channelGroupCode === channelGroup\(channelCode\)/) + assert.match(value, /simulation\?\.channelCode == null/) + assert.match(value, /containerDetail\?\.channelGroupCode === channelGroup\(channelCode\)/) assert.match(value, /id\(replacedTask\?\.cabinetNo\) === stableIdentity\.cabinetNo/) assert.match(value, /afterReplacementSnapshot\?\.snapshotToken === beforeReplacementSnapshot\?\.snapshotToken/) assert.match(value, /pdaIdentityStable: true/) diff --git a/scripts/admin-test-env-modules/smart-loading-channel-group.mjs b/scripts/admin-test-env-modules/smart-loading-channel-group.mjs new file mode 100644 index 000000000..dacbfbdef --- /dev/null +++ b/scripts/admin-test-env-modules/smart-loading-channel-group.mjs @@ -0,0 +1,14 @@ +export function channelGroup(channelCode) { + const value = String(channelCode || '').trim().toUpperCase() + if (value.startsWith('EXX_') || ['EXXHK', 'EXXHWK', 'NYEXX'].includes(value)) return 'EXX' + if (value.startsWith('MATSON_CLX_') || ['ZBYHK', 'ZBYHWK', 'ZBYHP'].includes(value)) return 'MATSON_CLX' + if (value.startsWith('MATSON_MAX_') || ['BYHK', 'BYHWK', 'BYHP'].includes(value)) return 'MATSON_MAX' + if (value.startsWith('HEDE_') || ['HDHK', 'HDHWK', 'HDHP'].includes(value)) return 'HEDE' + if (value.startsWith('OA_LA_') || value === 'OA_PARCEL' + || ['PCHK', 'PCHWK', 'PCKK', 'PCHP'].includes(value)) return 'OA_LA' + if (value.startsWith('OA_CHICAGO_') || value === 'CPHT') return 'OA_CHICAGO' + if (value.startsWith('OA_HOUSTON_') || value === 'HPHK') return 'OA_HOUSTON' + if (value.startsWith('OA_SAVANNAH_') || value === 'SPHK') return 'OA_SAVANNAH' + if (['OA_NEW_YORK_TRUCK', 'NPHK', 'NPHWK'].includes(value)) return 'OA_NEW_YORK_TRUCK' + throw new Error(`unsupported smart-loading channel group: ${channelCode}`) +} diff --git a/warehouse/src/main/resources/db/phase2-pallet-seed.sql b/warehouse/src/main/resources/db/phase2-pallet-seed.sql index 69c6aa626..d40c77f8b 100644 --- a/warehouse/src/main/resources/db/phase2-pallet-seed.sql +++ b/warehouse/src/main/resources/db/phase2-pallet-seed.sql @@ -16,7 +16,8 @@ ON DUPLICATE KEY UPDATE capacity_cbm=VALUES(capacity_cbm), weight_kg=VALUES(weight_kg), color=VALUES(color), - remark=VALUES(remark); + remark=VALUES(remark), + deleted=0; DROP PROCEDURE IF EXISTS assert_domestic_pallet_seed; DELIMITER // @@ -38,6 +39,14 @@ BEGIN ) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='国内作业仓木托盘、塑料托盘初始化不完整'; END IF; + IF (SELECT COUNT(*) + FROM wh_pallet + WHERE tenant_id=0 + AND deleted=0 + AND ((warehouse_id=190000000000000001 AND pallet_code IN ('TP-001','YW-TP-002')) + OR (warehouse_id=190000000000000002 AND pallet_code IN ('TP-002','DG-TP-002')))) <> 4 THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='国内作业仓固定可复用托盘编码初始化不完整'; + END IF; END// DELIMITER ; CALL assert_domestic_pallet_seed(); diff --git a/warehouse/src/test/java/com/baiyue/warehouse/repository/InitialWarehouseResourceSeedSqlContractTest.java b/warehouse/src/test/java/com/baiyue/warehouse/repository/InitialWarehouseResourceSeedSqlContractTest.java index 9e09034f1..e2dd761f1 100644 --- a/warehouse/src/test/java/com/baiyue/warehouse/repository/InitialWarehouseResourceSeedSqlContractTest.java +++ b/warehouse/src/test/java/com/baiyue/warehouse/repository/InitialWarehouseResourceSeedSqlContractTest.java @@ -27,6 +27,7 @@ class InitialWarehouseResourceSeedSqlContractTest { } private static void assertSeed(String sql, String yiwuId, String dongguanId, String idPrefix) { + String compactSql = sql.replaceAll("\\s+", ""); int locationSeedStart = sql.indexOf("WHEN zones.zone_index = 0 AND slots.slot_number = 1 THEN 201"); int locationSeedEnd = sql.indexOf("DROP PROCEDURE IF EXISTS assert_yiwu_location_seed", locationSeedStart); String locationSeed = sql.substring(locationSeedStart, locationSeedEnd); @@ -37,6 +38,7 @@ class InitialWarehouseResourceSeedSqlContractTest { .contains("0," + dongguanId + ",'DG-TP-002','木托盘'") .contains("COUNT(DISTINCT pallet.pallet_type)") .contains("国内作业仓木托盘、塑料托盘初始化不完整") + .contains("固定可复用托盘编码初始化不完整") .doesNotContain("'胶托盘'") .contains("WHEN zones.zone_index = 0 AND slots.slot_number = 1 THEN 201") .contains("ELSE 203 + zones.zone_index * 15 + slots.slot_number") @@ -55,6 +57,7 @@ class InitialWarehouseResourceSeedSqlContractTest { .contains("'AVAILABLE'") .doesNotContain("INSERT INTO wh_location_reservation") .doesNotContain("INSERT INTO wh_location_occupancy"); + assertThat(compactSql).contains("remark=VALUES(remark),deleted=0;"); assertThat(locationSeed) .doesNotContain("status='AVAILABLE'") .doesNotContain("status = 'AVAILABLE'")