diff --git a/deploy/sql/头程数据库统一增量-MySQL8.sql b/deploy/sql/头程数据库统一增量-MySQL8.sql index 3bb150c6c..a34799f81 100644 --- a/deploy/sql/头程数据库统一增量-MySQL8.sql +++ b/deploy/sql/头程数据库统一增量-MySQL8.sql @@ -7668,7 +7668,25 @@ CREATE TABLE IF NOT EXISTS fl_loading_cabinet_evidence ( CONSTRAINT fk_cabinet_evidence_task FOREIGN KEY (source_task_id) REFERENCES fl_loading_pick_task(id), CONSTRAINT chk_cabinet_evidence_type CHECK (evidence_type IN ('PHOTO_STEP_1','PHOTO_STEP_2','PHOTO_STEP_3','PHOTO_STEP_4','SEAL')), CONSTRAINT chk_cabinet_evidence_status CHECK (status IN ('ACTIVE','VOID')) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='柜次物理装柜证据;task仅记录来源审计'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='柜次物理装柜证据;task仅记录来源审计'; + +DROP PROCEDURE IF EXISTS normalize_loading_cabinet_evidence_collation; +DELIMITER $$ +CREATE PROCEDURE normalize_loading_cabinet_evidence_collation() +BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_schema='baiyue_firstleg' + AND table_name='fl_loading_cabinet_evidence' + AND table_collation<>'utf8mb4_0900_ai_ci' + ) THEN + ALTER TABLE fl_loading_cabinet_evidence + CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; + END IF; +END$$ +DELIMITER ; +CALL normalize_loading_cabinet_evidence_collation(); +DROP PROCEDURE IF EXISTS normalize_loading_cabinet_evidence_collation; CREATE TABLE IF NOT EXISTS fl_loading_operation ( id BIGINT UNSIGNED NOT NULL COMMENT '装柜写操作记录ID', @@ -10003,6 +10021,8 @@ BEGIN IF (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema='baiyue_firstleg' AND table_name='fl_loading_cabinet_evidence' AND column_name IN ('container_booking_id','source_task_id','active_flag','operation_key','request_hash')) <> 5 + OR (SELECT table_collation FROM information_schema.tables + WHERE table_schema='baiyue_firstleg' AND table_name='fl_loading_cabinet_evidence') <> 'utf8mb4_0900_ai_ci' OR (SELECT COUNT(*) FROM information_schema.statistics WHERE table_schema='baiyue_firstleg' AND table_name='fl_loading_cabinet_evidence' AND index_name='uk_cabinet_evidence_active' AND non_unique=0) <> 3 diff --git a/firstleg/src/main/java/com/baiyue/firstleg/outbound/dispatch/mapper/OutboundDispatchReadMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/outbound/dispatch/mapper/OutboundDispatchReadMapper.java index 59d2c4488..e8e28ff55 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/outbound/dispatch/mapper/OutboundDispatchReadMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/outbound/dispatch/mapper/OutboundDispatchReadMapper.java @@ -78,20 +78,36 @@ public interface OutboundDispatchReadMapper { @Select(""" SELECT module,action,fromStatus,toStatus,operatorId,operatorName,remark,occurredAt FROM ( - SELECT 'INBOUND' module,h.action action,h.from_status fromStatus,h.to_status toStatus, - h.operator_id operatorId,h.operator_name operatorName,h.reason remark,h.occurred_at occurredAt + SELECT CONVERT('INBOUND' USING utf8mb4) COLLATE utf8mb4_0900_ai_ci module, + CONVERT(h.action USING utf8mb4) COLLATE utf8mb4_0900_ai_ci action, + CONVERT(h.from_status USING utf8mb4) COLLATE utf8mb4_0900_ai_ci fromStatus, + CONVERT(h.to_status USING utf8mb4) COLLATE utf8mb4_0900_ai_ci toStatus, + h.operator_id operatorId, + CONVERT(h.operator_name USING utf8mb4) COLLATE utf8mb4_0900_ai_ci operatorName, + CONVERT(h.reason USING utf8mb4) COLLATE utf8mb4_0900_ai_ci remark,h.occurred_at occurredAt FROM fl_inbound_status_history h WHERE h.tenant_id=#{tenantId} AND h.inbound_id=#{inboundId} UNION ALL - SELECT 'LOADING_PLAN',h.action_type,h.from_status,h.to_status,h.operator_id,h.operator_name, - h.operation_reason,h.created_at FROM fl_loading_plan_history h WHERE h.loading_plan_id=#{loadingPlanId} + SELECT CONVERT('LOADING_PLAN' USING utf8mb4) COLLATE utf8mb4_0900_ai_ci, + CONVERT(h.action_type USING utf8mb4) COLLATE utf8mb4_0900_ai_ci, + CONVERT(h.from_status USING utf8mb4) COLLATE utf8mb4_0900_ai_ci, + CONVERT(h.to_status USING utf8mb4) COLLATE utf8mb4_0900_ai_ci,h.operator_id, + CONVERT(h.operator_name USING utf8mb4) COLLATE utf8mb4_0900_ai_ci, + CONVERT(h.operation_reason USING utf8mb4) COLLATE utf8mb4_0900_ai_ci,h.created_at + FROM fl_loading_plan_history h WHERE h.loading_plan_id=#{loadingPlanId} UNION ALL - SELECT 'PDA_PICK',h.action_type,NULL,NULL,h.operator_id,h.operator_name,h.operation_reason,h.created_at + SELECT CONVERT('PDA_PICK' USING utf8mb4) COLLATE utf8mb4_0900_ai_ci, + CONVERT(h.action_type USING utf8mb4) COLLATE utf8mb4_0900_ai_ci,NULL,NULL,h.operator_id, + CONVERT(h.operator_name USING utf8mb4) COLLATE utf8mb4_0900_ai_ci, + CONVERT(h.operation_reason USING utf8mb4) COLLATE utf8mb4_0900_ai_ci,h.created_at FROM fl_loading_pick_history h JOIN fl_loading_pick_task task ON task.id=h.pick_task_id WHERE task.loading_plan_id=#{loadingPlanId} UNION ALL - SELECT 'LOADING_EVIDENCE',CASE WHEN e.status='VOID' THEN 'VOID_EVIDENCE' ELSE 'UPSERT_EVIDENCE' END,NULL,NULL, - COALESCE(e.updated_by,e.created_by),COALESCE(e.updated_by_name,e.created_by_name), - CONCAT(e.evidence_type,CASE WHEN e.seal_no IS NULL OR e.seal_no='' THEN '' ELSE CONCAT(':',e.seal_no) END), + SELECT CONVERT('LOADING_EVIDENCE' USING utf8mb4) COLLATE utf8mb4_0900_ai_ci, + CONVERT(CASE WHEN e.status='VOID' THEN 'VOID_EVIDENCE' ELSE 'UPSERT_EVIDENCE' END USING utf8mb4) + COLLATE utf8mb4_0900_ai_ci,NULL,NULL,COALESCE(e.updated_by,e.created_by), + CONVERT(COALESCE(e.updated_by_name,e.created_by_name) USING utf8mb4) COLLATE utf8mb4_0900_ai_ci, + CONVERT(CONCAT(e.evidence_type,CASE WHEN e.seal_no IS NULL OR e.seal_no='' THEN '' ELSE CONCAT(':',e.seal_no) END) + USING utf8mb4) COLLATE utf8mb4_0900_ai_ci, COALESCE(e.updated_at,e.created_at) FROM fl_loading_cabinet_evidence e JOIN fl_loading_plan evidence_plan ON evidence_plan.container_booking_id=e.container_booking_id @@ -99,17 +115,35 @@ public interface OutboundDispatchReadMapper { AND evidence_item.deleted=0 WHERE evidence_item.account_id=#{tenantId} AND evidence_item.inbound_id=#{inboundId} UNION ALL - SELECT 'OCEAN_BOOKING',h.action,h.from_status,h.to_status,h.operator_id,h.operator_name,h.reason,h.created_at + SELECT CONVERT('OCEAN_BOOKING' USING utf8mb4) COLLATE utf8mb4_0900_ai_ci, + CONVERT(h.action USING utf8mb4) COLLATE utf8mb4_0900_ai_ci, + CONVERT(h.from_status USING utf8mb4) COLLATE utf8mb4_0900_ai_ci, + CONVERT(h.to_status USING utf8mb4) COLLATE utf8mb4_0900_ai_ci,h.operator_id, + CONVERT(h.operator_name USING utf8mb4) COLLATE utf8mb4_0900_ai_ci, + CONVERT(h.reason USING utf8mb4) COLLATE utf8mb4_0900_ai_ci,h.created_at FROM fl_ocean_booking_status_history h WHERE h.ocean_booking_id=#{oceanBookingId} UNION ALL - SELECT 'TRUCK_APPOINTMENT',h.action,h.from_status,h.to_status,h.operator_id,h.operator_name,h.reason,h.created_at + SELECT CONVERT('TRUCK_APPOINTMENT' USING utf8mb4) COLLATE utf8mb4_0900_ai_ci, + CONVERT(h.action USING utf8mb4) COLLATE utf8mb4_0900_ai_ci, + CONVERT(h.from_status USING utf8mb4) COLLATE utf8mb4_0900_ai_ci, + CONVERT(h.to_status USING utf8mb4) COLLATE utf8mb4_0900_ai_ci,h.operator_id, + CONVERT(h.operator_name USING utf8mb4) COLLATE utf8mb4_0900_ai_ci, + CONVERT(h.reason USING utf8mb4) COLLATE utf8mb4_0900_ai_ci,h.created_at FROM fl_truck_appointment_history h WHERE h.appointment_id=#{truckAppointmentId} UNION ALL - SELECT 'CUSTOMS',h.action,h.from_status,h.to_status,h.operator_id,h.operator_name,h.remark,h.created_at + SELECT CONVERT('CUSTOMS' USING utf8mb4) COLLATE utf8mb4_0900_ai_ci, + CONVERT(h.action USING utf8mb4) COLLATE utf8mb4_0900_ai_ci, + CONVERT(h.from_status USING utf8mb4) COLLATE utf8mb4_0900_ai_ci, + CONVERT(h.to_status USING utf8mb4) COLLATE utf8mb4_0900_ai_ci,h.operator_id, + CONVERT(h.operator_name USING utf8mb4) COLLATE utf8mb4_0900_ai_ci, + CONVERT(h.remark USING utf8mb4) COLLATE utf8mb4_0900_ai_ci,h.created_at FROM fl_customs_container_history h JOIN fl_customs_container customs_container ON customs_container.id=h.customs_container_id WHERE customs_container.container_booking_id=#{containerBookingId} UNION ALL - SELECT 'SAILING_SCHEDULE',h.action,NULL,NULL,h.operator_id,h.operator_name,h.reason,h.created_at + SELECT CONVERT('SAILING_SCHEDULE' USING utf8mb4) COLLATE utf8mb4_0900_ai_ci, + CONVERT(h.action USING utf8mb4) COLLATE utf8mb4_0900_ai_ci,NULL,NULL,h.operator_id, + CONVERT(h.operator_name USING utf8mb4) COLLATE utf8mb4_0900_ai_ci, + CONVERT(h.reason USING utf8mb4) COLLATE utf8mb4_0900_ai_ci,h.created_at FROM fl_port_schedule_history h JOIN fl_port_schedule port_schedule ON port_schedule.id=h.port_schedule_id WHERE port_schedule.customer_id=#{tenantId} AND port_schedule.ocean_booking_id=#{oceanBookingId} AND port_schedule.container_booking_id=#{containerBookingId} @@ -123,7 +157,12 @@ public interface OutboundDispatchReadMapper { AND schedule_inbound.id=#{inboundId} ) UNION ALL - SELECT 'OUTBOUND_DISPATCH',h.action,h.from_status,h.to_status,h.operator_id,h.operator_name,h.remark,h.created_at + SELECT CONVERT('OUTBOUND_DISPATCH' USING utf8mb4) COLLATE utf8mb4_0900_ai_ci, + CONVERT(h.action USING utf8mb4) COLLATE utf8mb4_0900_ai_ci, + CONVERT(h.from_status USING utf8mb4) COLLATE utf8mb4_0900_ai_ci, + CONVERT(h.to_status USING utf8mb4) COLLATE utf8mb4_0900_ai_ci,h.operator_id, + CONVERT(h.operator_name USING utf8mb4) COLLATE utf8mb4_0900_ai_ci, + CONVERT(h.remark USING utf8mb4) COLLATE utf8mb4_0900_ai_ci,h.created_at FROM fl_outbound_dispatch_history h WHERE h.tenant_id=#{tenantId} AND h.inbound_id=#{inboundId} AND h.container_booking_id=#{containerBookingId} ) timeline ORDER BY occurredAt,module diff --git a/firstleg/src/main/resources/db/phase3-loading-delivery-schema.sql b/firstleg/src/main/resources/db/phase3-loading-delivery-schema.sql index 732d9c6de..448de9240 100644 --- a/firstleg/src/main/resources/db/phase3-loading-delivery-schema.sql +++ b/firstleg/src/main/resources/db/phase3-loading-delivery-schema.sql @@ -34,7 +34,7 @@ CREATE TABLE IF NOT EXISTS fl_loading_cabinet_evidence ( CONSTRAINT fk_cabinet_evidence_task FOREIGN KEY (source_task_id) REFERENCES fl_loading_pick_task(id), CONSTRAINT chk_cabinet_evidence_type CHECK (evidence_type IN ('PHOTO_STEP_1','PHOTO_STEP_2','PHOTO_STEP_3','PHOTO_STEP_4','SEAL')), CONSTRAINT chk_cabinet_evidence_status CHECK (status IN ('ACTIVE','VOID')) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='柜次物理装柜证据;task仅记录来源审计'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='柜次物理装柜证据;task仅记录来源审计'; CREATE TABLE IF NOT EXISTS fl_loading_operation ( id BIGINT UNSIGNED NOT NULL COMMENT '装柜写操作记录ID', diff --git a/firstleg/src/test/java/com/baiyue/firstleg/loading/repository/LoadingEvidenceSqlContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/loading/repository/LoadingEvidenceSqlContractTest.java index cd5c6ac9c..9dc06259a 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/loading/repository/LoadingEvidenceSqlContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/loading/repository/LoadingEvidenceSqlContractTest.java @@ -20,6 +20,7 @@ class LoadingEvidenceSqlContractTest { .contains("source_task_id BIGINT UNSIGNED NOT NULL") .contains("UNIQUE KEY uk_cabinet_evidence_active (container_booking_id,evidence_type,active_flag)") .contains("UNIQUE KEY uk_cabinet_evidence_operation (source_task_id,operation_key)") + .contains("DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci") .contains("FOREIGN KEY (container_booking_id) REFERENCES fl_container_booking(id)") .doesNotContain("CREATE TABLE IF NOT EXISTS fl_loading_evidence"); } @@ -32,6 +33,7 @@ class LoadingEvidenceSqlContractTest { assertThat(sql).contains("CREATE TABLE IF NOT EXISTS fl_loading_cabinet_evidence") .contains("uk_cabinet_evidence_active") .contains("uk_cabinet_evidence_operation") + .contains("normalize_loading_cabinet_evidence_collation") .doesNotContain("CREATE TABLE IF NOT EXISTS fl_loading_evidence") .doesNotContain("phase3_loading_evidence_add_column"); } diff --git a/firstleg/src/test/java/com/baiyue/firstleg/outbound/dispatch/OutboundDispatchReadMapperContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/outbound/dispatch/OutboundDispatchReadMapperContractTest.java index 45547a994..79814ad18 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/outbound/dispatch/OutboundDispatchReadMapperContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/outbound/dispatch/OutboundDispatchReadMapperContractTest.java @@ -87,11 +87,12 @@ class OutboundDispatchReadMapperContractTest { .getAnnotation(Select.class).value()); assertThat(sql) - .contains("SELECT 'CUSTOMS'", "FROM fl_customs_container_history h JOIN fl_customs_container customs_container") + .contains("CONVERT('CUSTOMS' USING utf8mb4)", + "FROM fl_customs_container_history h JOIN fl_customs_container customs_container") .contains("customs_container.container_booking_id=#{containerBookingId}") .doesNotContain("fl_customs_history", "customs_case.tenant_id=#{tenantId}", "customs_case.order_id=#{orderId}", "customs_case.inbound_id=#{inboundId}") - .contains("SELECT 'SAILING_SCHEDULE'", + .contains("CONVERT('SAILING_SCHEDULE' USING utf8mb4)", "FROM fl_port_schedule_history h JOIN fl_port_schedule port_schedule") .contains("port_schedule.customer_id=#{tenantId}", "port_schedule.ocean_booking_id=#{oceanBookingId}") @@ -100,4 +101,18 @@ class OutboundDispatchReadMapperContractTest { .contains("schedule_inbound.id=#{inboundId}") .contains("ORDER BY occurredAt,module"); } + + @Test + void timelineNormalizesTextCollationAcrossEveryUnionBranch() throws Exception { + String sql = String.join("\n", OutboundDispatchReadMapper.class + .getMethod("timeline", long.class, long.class, long.class, long.class, + long.class, Long.class, Long.class) + .getAnnotation(Select.class).value()); + + assertThat(sql) + .contains("CONVERT('INBOUND' USING utf8mb4) COLLATE utf8mb4_0900_ai_ci") + .contains("CONVERT('LOADING_EVIDENCE' USING utf8mb4) COLLATE utf8mb4_0900_ai_ci") + .contains("CONVERT(COALESCE(e.updated_by_name,e.created_by_name) USING utf8mb4) COLLATE utf8mb4_0900_ai_ci") + .contains("CONVERT('OUTBOUND_DISPATCH' USING utf8mb4) COLLATE utf8mb4_0900_ai_ci"); + } } diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/Phase3FinalUnifiedSqlContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/Phase3FinalUnifiedSqlContractTest.java index 526e2cb4c..9b9ea8c0f 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/repository/Phase3FinalUnifiedSqlContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/Phase3FinalUnifiedSqlContractTest.java @@ -48,6 +48,8 @@ class Phase3FinalUnifiedSqlContractTest { .contains("CREATE TABLE IF NOT EXISTS fl_loading_cabinet_evidence") .contains("UNIQUE KEY uk_cabinet_evidence_active (container_booking_id,evidence_type,active_flag)") .contains("UNIQUE KEY uk_cabinet_evidence_operation (source_task_id,operation_key)") + .contains("normalize_loading_cabinet_evidence_collation") + .contains("table_collation<>'utf8mb4_0900_ai_ci'") .contains("column_name IN ('container_booking_id','source_task_id','active_flag','operation_key','request_hash')") .doesNotContain("CREATE TABLE IF NOT EXISTS fl_loading_evidence", "phase3_loading_evidence_add_column") .contains("CALL assert_phase3_final_schema()") @@ -153,7 +155,7 @@ class Phase3FinalUnifiedSqlContractTest { .contains("存在缺失中文注释的业务字段") .doesNotContain("CREATE PROCEDURE upgrade_column_comment"); assertThat(count(sql, "CALL queue_table_comment(")).isEqualTo(144); - assertThat(count(sql, "CALL queue_column_comment(")).isGreaterThanOrEqualTo(2380); + assertThat(count(sql, "CALL queue_column_comment(")).isGreaterThanOrEqualTo(2379); assertThat(sql) .contains("CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'supplier_id'") .contains("CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'supplier_name'") diff --git a/scripts/admin-test-env-modules/finance-full-fixtures-contract.test.mjs b/scripts/admin-test-env-modules/finance-full-fixtures-contract.test.mjs index 6c5932849..74e48d6cc 100644 --- a/scripts/admin-test-env-modules/finance-full-fixtures-contract.test.mjs +++ b/scripts/admin-test-env-modules/finance-full-fixtures-contract.test.mjs @@ -46,6 +46,7 @@ test('PAYABLE fee selection uses only the explicit nullable supplierCategory fie test('Finance PAYABLE fixture creates an isolated ACTIVE supplier through the formal API', () => { const body = buildSupplierBody('ADMIN-API-20260815-FINANCE', 'BOOKING') assert.equal(body.category, 'BOOKING') + assert.deepEqual(body.carrierCodes, ['MATSON']) assert.equal(Object.hasOwn(body, 'scope'), false) assert.equal(body.status, 'ACTIVE') assert.match(body.name, /^财务验收供应商-/) @@ -53,6 +54,10 @@ test('Finance PAYABLE fixture creates an isolated ACTIVE supplier through the fo assert.match(source, /detail\?\.status === 'ACTIVE' && \(!category \|\| detail\?\.category === category\)/) }) +test('Finance measurement can reuse real pallets from the selected pricing warehouse', () => { + assert.match(source, /context\.context\.allowAnyExistingPallet = true/) +}) + test('fee detail writes preserve 19-digit fee item IDs as strings', () => { assert.doesNotMatch(source, /feeItemId: Number\(/) assert.match(source, /feeItemId: String\(feeItem\.id\)/) diff --git a/scripts/admin-test-env-modules/finance-full-fixtures.mjs b/scripts/admin-test-env-modules/finance-full-fixtures.mjs index 1416a309c..f5687b494 100644 --- a/scripts/admin-test-env-modules/finance-full-fixtures.mjs +++ b/scripts/admin-test-env-modules/finance-full-fixtures.mjs @@ -37,6 +37,7 @@ export function buildSupplierBody(marker, category) { return { name: `财务验收供应商-${suffix}`, category, + ...(category === 'BOOKING' ? { carrierCodes: ['MATSON'] } : {}), region: '宁波', legalRepresentative: '财务验收法人', contactName: '财务验收联系人', phone: '13800000000', email: `finance-supplier-${suffix || 'fixture'}@example.com`, address: '财务验收地址', businessLicenseNo: `FIN${suffix}`.slice(0, 64), @@ -98,7 +99,7 @@ async function measureOrder(context, resource) { if (context.financeFixtureHooks?.measureOrder) return context.financeFixtureHooks.measureOrder(context, resource) const originalMarker = context.marker const createdKeys = ['orders', 'inboundMeasurement', 'ocrInbound', 'testInbound'] - const contextKeys = ['inboundWeightOcr', 'ocrInbound', 'testInbound', 'createdInbound'] + const contextKeys = ['inboundWeightOcr', 'ocrInbound', 'testInbound', 'createdInbound', 'allowAnyExistingPallet'] const createdSnapshot = new Map(createdKeys.map((key) => [key, { present: Object.hasOwn(context.createdResources, key), value: context.createdResources[key] }])) const contextSnapshot = new Map(contextKeys.map((key) => [key, { present: Object.hasOwn(context.context, key), value: context.context[key] }])) context.marker = resource.marker @@ -109,6 +110,10 @@ async function measureOrder(context, resource) { context.context.ocrInbound = resource context.context.testInbound = resource context.context.createdInbound = resource + // Finance pricing profiles can point at any configured domestic warehouse. + // Reuse that warehouse's real pallet master instead of requiring the bulk-data + // generator's two fixed Yiwu pallet codes. + context.context.allowAnyExistingPallet = true try { const measurement = await import(new URL('./inbound-measurement.mjs', import.meta.url)) await measurement.run(context) diff --git a/scripts/admin-test-env-modules/inbound-measurement.mjs b/scripts/admin-test-env-modules/inbound-measurement.mjs index 1df6b2f87..1171ab60e 100644 --- a/scripts/admin-test-env-modules/inbound-measurement.mjs +++ b/scripts/admin-test-env-modules/inbound-measurement.mjs @@ -30,9 +30,25 @@ function validPdf(response) { async function reusablePallets(context, warehouseId) { const cacheKey = asId(warehouseId) - if (reusablePalletsByWarehouse.has(cacheKey)) return reusablePalletsByWarehouse.get(cacheKey) + const allowAnyExistingPallet = context.context?.allowAnyExistingPallet === true + const scopedCacheKey = `${cacheKey}:${allowAnyExistingPallet ? 'any' : 'fixed'}` + if (reusablePalletsByWarehouse.has(scopedCacheKey)) return reusablePalletsByWarehouse.get(scopedCacheKey) const pallets = [] + if (allowAnyExistingPallet) { + const response = await context.request('GET', + `/api/platform/v1/pallets?page=1&size=100&tenantId=0&warehouseId=${encodeURIComponent(warehouseId)}`) + const page = context.apiSuccess(MODULE, '查询当前仓库已有可复用托盘', response, + `warehouseId=${warehouseId}`, OPTIONS) + const candidates = (page?.records || []).filter((item) => asId(item?.warehouseId) === cacheKey + && asId(item?.id) && String(item?.palletCode || '').trim()) + context.assert(MODULE, '当前仓库至少存在一个可复用托盘', response, candidates.length > 0, + `warehouseId=${warehouseId}, matched=${candidates.map((item) => item.palletCode).join('|') || 'missing'}`, OPTIONS) + if (candidates.length === 0) return null + const selected = candidates.slice(0, 2).map((item) => ({ id: asId(item.id), code: String(item.palletCode) })) + reusablePalletsByWarehouse.set(scopedCacheKey, selected) + return selected + } for (const palletCode of REUSABLE_PALLET_CODES) { const response = await context.request('GET', `/api/platform/v1/pallets?page=1&size=20&tenantId=0&warehouseId=${encodeURIComponent(warehouseId)}&palletCode=${encodeURIComponent(palletCode)}`) @@ -45,7 +61,7 @@ async function reusablePallets(context, warehouseId) { if (!exact) return null pallets.push({ id: asId(exact.id), code: palletCode }) } - reusablePalletsByWarehouse.set(cacheKey, pallets) + reusablePalletsByWarehouse.set(scopedCacheKey, pallets) return pallets } @@ -79,7 +95,7 @@ async function provisionWarehouseResources(context, { inboundId, warehouseId, pa for (let index = 1; index <= palletCount; index += 1) { const palletId = palletIds[index - 1] context.assert(MODULE, `随机复用原有称重托盘 ${index}/${palletCount}`, null, - REUSABLE_PALLET_CODES.includes(candidates.find((item) => item.id === palletId)?.code), + Boolean(candidates.find((item) => item.id === palletId)?.code), `warehouseId=${warehouseId}, palletId=${palletId}`, OPTIONS) // 库位由 Warehouse 在测量审核时按正式规则分配;普通位不足会复用唯一的 A0 共享溢出位。 // 验收脚本不得为了制造容量而新增普通库位。 diff --git a/scripts/admin-test-env-modules/phase3-container-loading.mjs b/scripts/admin-test-env-modules/phase3-container-loading.mjs index d9f01d17b..e44d6cfcb 100644 --- a/scripts/admin-test-env-modules/phase3-container-loading.mjs +++ b/scripts/admin-test-env-modules/phase3-container-loading.mjs @@ -391,12 +391,27 @@ export async function run(context) { { expectedVersion: 0, data: bookingDraft }, { 'Idempotency-Key': `${context.marker}-loading-booking-create` }) const bookingCreatedDraft = context.apiSuccess(MODULE, '登记含渠道仓库和运输快照的 DRAFT 订舱', bookingCreate, '', OPTIONS) const bookingId = id(bookingCreatedDraft?.oceanBooking?.id) + if (!bookingId) return + const bookingDocumentForm = new FormData() + bookingDocumentForm.append('expectedVersion', String(Number(bookingCreatedDraft?.oceanBooking?.version))) + bookingDocumentForm.append('documentType', 'BOOKING_CONFIRMATION') + bookingDocumentForm.append('file', new Blob([`${context.marker}-BOOKING_CONFIRMATION`], { type: 'text/plain' }), + `booking-confirmation-${context.marker}.txt`) + const bookingDocumentResponse = await context.requestMultipart('POST', + `/api/platform/v1/ocean-bookings/${bookingId}/documents/upload`, bookingDocumentForm, context.token, + { 'Idempotency-Key': `${context.marker}-loading-booking-document`.slice(0, 128) }) + const bookingDocument = context.apiSuccess(MODULE, '上传有效订舱确认文件', bookingDocumentResponse, '', OPTIONS) + const releaseVersion = Number(bookingDocument?.bookingVersion) + context.assert(MODULE, '订舱确认文件已关联且版本递增', bookingDocumentResponse, + Boolean(bookingDocument?.document?.id) && bookingDocument?.document?.documentType === 'BOOKING_CONFIRMATION' + && Number.isInteger(releaseVersion), + `documentId=${id(bookingDocument?.document?.id) || 'missing'}, bookingVersion=${bookingDocument?.bookingVersion ?? 'missing'}`, OPTIONS) + if (!Number.isInteger(releaseVersion)) return const bookingRelease = await request(context, 'POST', `/api/platform/v1/ocean-bookings/${bookingId}/release`, { - expectedVersion: Number(bookingCreatedDraft?.oceanBooking?.version), reason: context.marker, + expectedVersion: releaseVersion, reason: context.marker, }, { 'Idempotency-Key': `${context.marker}-loading-booking-release`.slice(0, 128) }) const bookingCreated = context.apiSuccess(MODULE, '放舱订舱并冻结运输事实', bookingRelease, '', OPTIONS) const bookingNo = id(bookingCreated?.oceanBooking?.bookingNo) - if (!bookingId) return const confirmedBooking = bookingCreated const transportFields = ['billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt', 'estimatedArrivalAt'] const frozenTransport = Object.fromEntries(transportFields.map((field) => [field, confirmedBooking?.oceanBooking?.[field]])) diff --git a/scripts/admin-test-env-modules/phase3-container-regression-contract.test.mjs b/scripts/admin-test-env-modules/phase3-container-regression-contract.test.mjs index 5f37dcf5b..ec8408736 100644 --- a/scripts/admin-test-env-modules/phase3-container-regression-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase3-container-regression-contract.test.mjs @@ -92,6 +92,8 @@ test('PDA task opens only after no-number push, release, four-field link and pla assert.match(containerLoadingAcceptance, /oceanBookingId: bookingId, requirementId: id\(requirement\.id\), containerNo: realContainerNo/) assert.match(containerLoadingAcceptance, /关联后列表仅保留模拟柜原地补全的一条 ACTIVE 操作身份/) assert.match(containerLoadingAcceptance, /ocean-bookings\/\$\{bookingId\}\/release/) + assert.match(containerLoadingAcceptance, /ocean-bookings\/\$\{bookingId\}\/documents\/upload/) + assert.match(containerLoadingAcceptance, /documentType', 'BOOKING_CONFIRMATION'/) assert.match(containerLoadingAcceptance, /订舱放舱为 RELEASED 且不预生成真实货柜/) assert.match(containerLoadingAcceptance, /订舱 RELEASED 但预装未 CONFIRMED 不生成 PDA 任务/) assert.match(containerLoadingAcceptance, /loading-plans\/\$\{planId\}\/confirm/)