diff --git a/deploy/sql/海外区域统一增量-MySQL8.sql b/deploy/sql/海外区域统一增量-MySQL8.sql index 8a15f3e66..9744fe5ca 100644 --- a/deploy/sql/海外区域统一增量-MySQL8.sql +++ b/deploy/sql/海外区域统一增量-MySQL8.sql @@ -1634,6 +1634,8 @@ SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE tab PREPARE transfer_estimated_ship_stmt FROM @ddl; EXECUTE transfer_estimated_ship_stmt; DEALLOCATE PREPARE transfer_estimated_ship_stmt; SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_order' AND column_name='label_deadline'), 'SELECT 1', 'ALTER TABLE ll_warehouse_transfer_order ADD COLUMN label_deadline DATETIME(3) NULL COMMENT ''换标截止时间'', ADD COLUMN relabel_contact_name VARCHAR(128) NULL COMMENT ''换标联系人'', ADD COLUMN relabel_contact_phone VARCHAR(64) NULL COMMENT ''换标联系电话'', ADD COLUMN relabel_charge_amount DECIMAL(20,2) NULL COMMENT ''换标费用CNY'', ADD COLUMN relabel_delivery_method VARCHAR(32) NULL COMMENT ''换标后派送方式''')); PREPARE transfer_relabel_config_stmt FROM @ddl; EXECUTE transfer_relabel_config_stmt; DEALLOCATE PREPARE transfer_relabel_config_stmt; +SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_order' AND column_name='relabel_completed_at'), 'SELECT 1', 'ALTER TABLE ll_warehouse_transfer_order ADD COLUMN relabel_completed_at DATETIME(3) NULL COMMENT ''关联贴标完成时间'' AFTER relabel_no')); +PREPARE transfer_relabel_completed_stmt FROM @ddl; EXECUTE transfer_relabel_completed_stmt; DEALLOCATE PREPARE transfer_relabel_completed_stmt; SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_order' AND column_name='palletize_idempotency_key'), 'SELECT 1', 'ALTER TABLE ll_warehouse_transfer_order ADD COLUMN palletize_idempotency_key VARCHAR(255) NULL COMMENT ''PDA打托幂等键'', ADD COLUMN palletize_request_hash CHAR(64) NULL COMMENT ''PDA打托请求哈希'', ADD COLUMN confirmation_idempotency_key VARCHAR(255) NULL COMMENT ''客户测量确认幂等键'', ADD COLUMN confirmation_request_hash CHAR(64) NULL COMMENT ''客户测量确认请求哈希'', ADD COLUMN customer_confirmed_at DATETIME(3) NULL COMMENT ''客户确认测量时间''')); PREPARE transfer_fulfillment_stmt FROM @ddl; EXECUTE transfer_fulfillment_stmt; DEALLOCATE PREPARE transfer_fulfillment_stmt; @@ -2025,7 +2027,9 @@ CREATE TABLE IF NOT EXISTS ll_cargo_discard_item ( id BIGINT NOT NULL COMMENT '主键ID', application_id BIGINT NOT NULL COMMENT '申请单ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID', order_id BIGINT NOT NULL COMMENT '订单ID', inbound_code VARCHAR(128) NOT NULL COMMENT '入仓单号快照', profile_id BIGINT NOT NULL COMMENT '托盘档案ID', sku_inventory_id BIGINT NOT NULL COMMENT 'SKU库存记录ID', sku_code VARCHAR(128) NOT NULL COMMENT 'SKU编码', product_name VARCHAR(255) NULL COMMENT '产品名称', - available_packages_snapshot INT NOT NULL COMMENT '可用箱数快照', discard_packages INT NOT NULL COMMENT '废弃箱数', + available_packages_snapshot INT NOT NULL COMMENT '可用箱数快照', discard_packages INT NOT NULL COMMENT '申请废弃箱数', + actual_discard_packages INT NULL COMMENT 'PDA确认实际废弃箱数', pda_status VARCHAR(32) NOT NULL DEFAULT 'PENDING' COMMENT 'PDA处理状态:待处理PENDING、已完成COMPLETED', + pda_operator_id VARCHAR(128) NULL COMMENT 'PDA操作人', pda_completed_at DATETIME(3) NULL COMMENT 'PDA完成时间', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', PRIMARY KEY(id), UNIQUE KEY uk_ll_cargo_discard_item(application_id,profile_id,sku_code), KEY idx_ll_cargo_discard_item_list(customer_id,application_id), @@ -2091,7 +2095,7 @@ CREATE TABLE IF NOT EXISTS ll_us_truck_order ( origin_address VARCHAR(500) NULL COMMENT '起运地址', destination VARCHAR(255) NOT NULL COMMENT '目的地', destination_address VARCHAR(500) NULL COMMENT '目的地址', - container_type VARCHAR(32) NULL COMMENT '历史兼容设备类型,零担托盘业务不采集', + container_type VARCHAR(32) NOT NULL DEFAULT '40HQ' COMMENT '平台统一柜型:40GP、40HQ、45HQ', pallet_count INT NOT NULL DEFAULT 1 COMMENT '客户申报托盘数', package_count INT NULL COMMENT '可选箱数,零或空值均有效', received_pallet_count INT NOT NULL DEFAULT 0 COMMENT 'PDA实收托盘数', @@ -2169,7 +2173,7 @@ CREATE TABLE IF NOT EXISTS ll_local_looseboard_order ( pallet_required VARCHAR(8) NOT NULL COMMENT '是否需要打盘:YES或NO', transport_type VARCHAR(32) NOT NULL COMMENT '运输类型', origin VARCHAR(255) NOT NULL COMMENT '起运地', origin_address VARCHAR(500) NULL COMMENT '起运详细地址', destination VARCHAR(255) NOT NULL COMMENT '目的地', destination_address VARCHAR(500) NULL COMMENT '目的详细地址、码头或仓号', - container_type VARCHAR(32) NULL COMMENT '柜型', package_count INT NOT NULL DEFAULT 0 COMMENT '货物件数', + container_type VARCHAR(32) NOT NULL DEFAULT '40HQ' COMMENT '平台统一柜型:40GP、40HQ、45HQ', package_count INT NOT NULL DEFAULT 0 COMMENT '货物件数', total_weight_kg DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT '总重量(千克)', total_volume_cbm DECIMAL(18,6) NOT NULL DEFAULT 0 COMMENT '总体积(立方米)', budget_amount DECIMAL(18,2) NULL COMMENT '预算总价', currency CHAR(3) NOT NULL DEFAULT 'USD' COMMENT '预算币种', @@ -2618,6 +2622,18 @@ CREATE TABLE IF NOT EXISTS ll_return_inventory_operation ( PRIMARY KEY(id), UNIQUE KEY uk_ll_return_inventory_operation_operator_key(operator_id,idempotency_key), KEY idx_ll_return_inventory_operation_result(result_carton_id) ) ENGINE=InnoDB COMMENT='退货库存PDA合箱幂等操作审计'; +CREATE TABLE IF NOT EXISTS ll_container_loading_evidence ( + id BIGINT NOT NULL COMMENT '海外整柜装柜照片投影ID', + source_container_booking_id VARCHAR(128) NOT NULL COMMENT '国内稳定来源柜预订ID', + evidence_type VARCHAR(32) NOT NULL COMMENT 'PHOTO_STEP_1至PHOTO_STEP_4或SEAL', + source_file_id BIGINT NOT NULL COMMENT '国内File服务文件ID', + file_name VARCHAR(255) NOT NULL COMMENT '国内文件名快照', + created_at DATETIME(3) NOT NULL COMMENT '投影创建时间', + PRIMARY KEY(id), + UNIQUE KEY uk_ll_container_loading_evidence_type(source_container_booking_id,evidence_type), + UNIQUE KEY uk_ll_container_loading_evidence_file(source_container_booking_id,source_file_id) +) ENGINE=InnoDB COMMENT='海外拆柜只读国内装柜照片引用'; + -- END INLINE EMPTY-DATABASE BASELINE: LASTLEG -- BEGIN INLINE EMPTY-DATABASE BASELINE: OVERSEAS WAREHOUSE diff --git a/lastleg-service/src/main/resources/db/lastleg.sql b/lastleg-service/src/main/resources/db/lastleg.sql index 0bc0c658f..ac1d3bc66 100644 --- a/lastleg-service/src/main/resources/db/lastleg.sql +++ b/lastleg-service/src/main/resources/db/lastleg.sql @@ -1959,20 +1959,6 @@ CREATE TABLE IF NOT EXISTS ll_cargo_discard_status_history ( KEY idx_ll_cargo_discard_history(customer_id,application_id,created_at) ) ENGINE=InnoDB COMMENT='货物废弃审批历史'; --- Backfill applications created before cargo discard became a first-class tail-leg order. -INSERT IGNORE INTO ll_order ( - id,order_no,customer_id,order_type,inbound_code,inbound_order_no,source_region,source_inbound_id, - source_order_id,source_order_no,overseas_warehouse_id,overseas_warehouse_name,package_count, - current_status,version,source_event_id,source_loading_plan_id,source_payload_hash,created_at,updated_at -) -SELECT a.id,a.discard_no,a.customer_id,'CARGO_DISCARD',a.inbound_codes,a.discard_no,'LASTLEG_DISCARD', - CONCAT('CARGO_DISCARD:',a.id),CAST(a.id AS CHAR),a.discard_no,a.overseas_warehouse_id,a.overseas_warehouse_name, - a.total_packages,a.status,0,CONCAT('CARGO_DISCARD:',a.id),'CARGO_DISCARD',a.request_hash,a.created_at,a.updated_at -FROM ll_cargo_discard_application a -LEFT JOIN ll_order o ON o.customer_id=a.customer_id AND o.source_region='LASTLEG_DISCARD' - AND o.source_inbound_id=CONCAT('CARGO_DISCARD:',a.id) -WHERE o.id IS NULL; - CREATE TABLE IF NOT EXISTS ll_overseas_procurement_order ( id BIGINT NOT NULL COMMENT '主键ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID', customer_name VARCHAR(255) NULL COMMENT '客户名称快照', order_no VARCHAR(64) NOT NULL COMMENT '订单号', diff --git a/scripts/admin-test-env-modules/initialization-baseline-contract.test.mjs b/scripts/admin-test-env-modules/initialization-baseline-contract.test.mjs index 3e714198c..aa0af1f0c 100644 --- a/scripts/admin-test-env-modules/initialization-baseline-contract.test.mjs +++ b/scripts/admin-test-env-modules/initialization-baseline-contract.test.mjs @@ -159,6 +159,12 @@ test('海外空库基线为固定平台管理员提供完整version0授权投影 assert.equal(inline.replace(/\r\n/g, '\n').trim(), lastlegBaseline.replace(/\r\n/g, '\n').trim()) }) +test('当前海外基线不从废弃申请回填历史尾程订单', () => { + for (const source of [lastlegBaseline, overseasSql]) { + assert.doesNotMatch(source, /INSERT IGNORE INTO ll_order\s*\([\s\S]*?SELECT a\.id,a\.discard_no/) + } +}) + test('国内统一初始化补齐入库费用人工调整基线字段', () => { assert.match(sql, /baseline_chargeable_quantity DECIMAL\(18,6\) NOT NULL/) assert.match(sql, /SET baseline_chargeable_quantity = chargeable_quantity[\s\S]*?WHERE baseline_chargeable_quantity IS NULL/) diff --git a/scripts/admin-test-env-modules/loading-history-noop-contract.test.mjs b/scripts/admin-test-env-modules/loading-history-noop-contract.test.mjs index 4e7e4fbeb..55727ddb2 100644 --- a/scripts/admin-test-env-modules/loading-history-noop-contract.test.mjs +++ b/scripts/admin-test-env-modules/loading-history-noop-contract.test.mjs @@ -44,7 +44,7 @@ test('backend never appends a generic STATUS_UPDATE and current operation replay assert.match(smartLoadingTest, /void replaysBeforeVersionCheck\(\)/) assert.match(smartLoadingTest, /void exactWarehouseRemarkReplayIsReadOnly\(\)/) assert.match(smartLoadingTest, /replaysBeforeVersionCheck[\s\S]{0,1200}verify\(repository, never\(\)\)\.candidate/) - assert.match(smartLoadingTest, /exactWarehouseRemarkReplayIsReadOnly[\s\S]{0,1200}verify\(repository, never\(\)\)\.updateWarehouseRemark/) + assert.match(smartLoadingTest, /exactWarehouseRemarkReplayIsReadOnly[\s\S]{0,1200}verify\(repository, never\(\)\)\.updateRemarks/) }) test('operationId is projected end-to-end for identity-based frontend deduplication', () => { diff --git a/scripts/admin-test-env-modules/manual-state-ocean-trucking-contract.test.mjs b/scripts/admin-test-env-modules/manual-state-ocean-trucking-contract.test.mjs index d6e88e869..b817a9550 100644 --- a/scripts/admin-test-env-modules/manual-state-ocean-trucking-contract.test.mjs +++ b/scripts/admin-test-env-modules/manual-state-ocean-trucking-contract.test.mjs @@ -14,10 +14,17 @@ test('manual matrix retains only DRAFT and RELEASED ocean booking states', () => test('truck matrix remains independent while retaining its own lifecycle', () => { for (const status of ['DRAFT', 'PENDING_DISPATCH', 'DISPATCHED', 'PICKUP_IN_PROGRESS', - 'ARRIVED_WAREHOUSE', 'LOADING', 'DEPARTED', 'COMPLETED', 'RESCHEDULED', 'EXCEPTION', 'CANCELLED']) { + 'ARRIVED_WAREHOUSE', 'LOADING', 'RESCHEDULED', 'EXCEPTION', 'CANCELLED']) { assert.match(source, new RegExp(`['\"]${status}['\"]`)) } assert.match(source, /page=1&size=100&status=/) assert.match(source, /category=TRUCKING&status=ACTIVE/) assert.match(source, /carrierId: id\(carrier\.id\), carrierName: carrier\.name/) + const completion = readFileSync(new URL('../../firstleg/src/main/java/com/baiyue/firstleg/service/TruckAppointmentCompletionService.java', import.meta.url), 'utf8') + const statuses = readFileSync(new URL('../../firstleg/src/main/java/com/baiyue/firstleg/domain/TruckAppointmentStatus.java', import.meta.url), 'utf8') + assert.doesNotMatch(statuses, /\bDEPARTED\b/) + assert.match(statuses, /\bCOMPLETED\b/) + assert.match(completion, /if \(!allDeparted\(rows\)\) return/) + assert.match(completion, /history\.setAction\("AUTO_COMPLETE"\)/) + assert.match(completion, /history\.setToStatus\("COMPLETED"\)/) }) diff --git a/scripts/java-quality-baseline.json b/scripts/java-quality-baseline.json index 9811044e9..2e8d955f3 100644 --- a/scripts/java-quality-baseline.json +++ b/scripts/java-quality-baseline.json @@ -11,7 +11,7 @@ "services": 15 }, "openapi": { - "documents": 48, + "documents": 49, "coveredServices": 10, "codeFirstServices": 5 }, diff --git a/scripts/java-quality-summary.test.mjs b/scripts/java-quality-summary.test.mjs index a8b56435d..841efff6e 100644 --- a/scripts/java-quality-summary.test.mjs +++ b/scripts/java-quality-summary.test.mjs @@ -65,7 +65,8 @@ test('repository freezes the current topology and non-blocking aggregate summary const summary = await buildQualitySummary(repositoryRoot) const baseline = JSON.parse(await readFile(path.join(repositoryRoot, 'scripts', 'java-quality-baseline.json'), 'utf8')) assert.deepEqual(summary.topology, { modules: 22, codeModules: 21, services: 15 }) - assert.equal(summary.openapi.documents, 48) + assert.equal(summary.openapi.documents, 49) + assert.ok(summary.openapi.files.includes('file/src/main/resources/openapi/loading-evidence-v1.yaml')) assert.ok(summary.openapi.files.includes('file/src/main/resources/openapi/overseas-relabel-document-v1.yaml')) assert.ok(summary.openapi.files.includes('finance/src/main/resources/openapi/finance-container-settlement-v1.yaml')) assert.ok(summary.openapi.files.includes('warehouse-overseas-service/src/main/resources/openapi/warehouse-pallet-batch-internal-v1.yaml')) diff --git a/scripts/synchronous-dependency-baseline.mjs b/scripts/synchronous-dependency-baseline.mjs index 309907610..1c7845f0f 100644 --- a/scripts/synchronous-dependency-baseline.mjs +++ b/scripts/synchronous-dependency-baseline.mjs @@ -76,6 +76,8 @@ export const SYNCHRONOUS_CLIENTS = Object.freeze([ '在线测量/箱号/排柜读取;Order 审核 Outbox 创建 Firstleg 入库及箱号时也会后台回读订单货物'), feign('firstleg/src/main/java/com/baiyue/firstleg/client/OrderFulfillmentSplitClient.java', 'firstleg-service', 'order-service', ['REQUEST'], 'WRITE', 'IDEMPOTENCY_KEY', 'PDA 结算拆单携带 idempotencyKey/operationId'), + feign('firstleg/src/main/java/com/baiyue/firstleg/client/OrderFulfillmentCompletionClient.java', + 'firstleg-service', 'order-service', ['BACKGROUND'], 'WRITE', 'EVENT_ID', '海外派送完成事件按 eventId/payloadHash 幂等确认来源订单完成'), feign('firstleg/src/main/java/com/baiyue/firstleg/client/PendingReviewOrderClient.java', 'firstleg-service', 'order-service', ['REQUEST'], 'READ', 'SAFE_READ', '智能排柜分页读取待审核订单投影'), feign('firstleg/src/main/java/com/baiyue/firstleg/client/PriceBookFileClient.java', @@ -117,6 +119,8 @@ export const SYNCHRONOUS_CLIENTS = Object.freeze([ 'lastleg-service', 'warehouse-service', ['REQUEST'], 'READ', 'SAFE_READ', '读取国内共享仓库权威目录'), feign('lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/quote/client/DomesticPartnerWarehouseClient.java', 'lastleg-service', 'warehouse-service', ['REQUEST'], 'READ', 'SAFE_READ', '报价创建时读取国内合作仓快照'), + feign('warehouse-overseas-service/src/main/java/com/baiyue/warehouse/client/PrintPdfClient.java', + 'warehouse-overseas-service', 'print-service', ['REQUEST'], 'READ', 'SAFE_READ', '按已授权托盘生成标签PDF,内部令牌保护,无业务状态写入'), feign('warehouse-overseas-service/src/main/java/com/baiyue/warehouse/client/DomesticWarehouseDirectoryClient.java', 'warehouse-overseas-service', 'warehouse-service', ['REQUEST'], 'READ', 'SAFE_READ', '海外库位、托盘和资源锁写入前读取国内共享收货仓权威目录'), @@ -167,6 +171,8 @@ export const SYNCHRONOUS_CLIENTS = Object.freeze([ 'lastleg-service', 'file-service', ['REQUEST'], 'READ_WRITE', 'MIXED', '异常处理证据上传无幂等键,归属校验可重复'), http('lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpUnloadPhotoFileGateway.java', 'lastleg-service', 'file-service', ['REQUEST'], 'READ_WRITE', 'MIXED', '上传无幂等键,元数据校验可重复'), + http('lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpDomesticLoadingEvidenceGateway.java', + 'lastleg-service', 'file-service', ['REQUEST'], 'READ', 'SAFE_READ', '当前PDA会话校验来源柜后,通过内部服务令牌只读下载国内装柜照片'), http('lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpWarehouseResourceGateway.java', 'lastleg-service', 'warehouse-overseas-service', ['REQUEST'], 'READ_WRITE', 'MIXED', '查询可重复;写操作按 business/reservation key 或状态收敛'), http('lastleg-service/src/main/java/com/baiyue/lastleg/palletops/gateway/HttpPalletPhysicalGateway.java', diff --git a/scripts/synchronous-dependency-contract.test.mjs b/scripts/synchronous-dependency-contract.test.mjs index c2613a429..bb70439d8 100644 --- a/scripts/synchronous-dependency-contract.test.mjs +++ b/scripts/synchronous-dependency-contract.test.mjs @@ -42,6 +42,7 @@ const expectedOnlineEdges = [ 'order-service->firstleg-service', 'order-service->iam-service', 'order-service->warehouse-service', + 'warehouse-overseas-service->print-service', 'warehouse-overseas-service->warehouse-service' ] @@ -100,15 +101,15 @@ async function discoverFeignEdges() { return clients.sort((left, right) => left.path.localeCompare(right.path)) } -test('all 58 Feign clients have reviewed context, access and idempotency metadata', async () => { +test('all 60 Feign clients have reviewed context, access and idempotency metadata', async () => { const reviewed = SYNCHRONOUS_CLIENTS.filter(client => client.transport === 'FEIGN') .map(({ path, source, target }) => ({ path, source, target })) .sort((left, right) => left.path.localeCompare(right.path)) - assert.equal(reviewed.length, 58) + assert.equal(reviewed.length, 60) assert.deepEqual(await discoverFeignEdges(), reviewed) }) -test('all 22 raw HTTP transports have reviewed context, access and idempotency metadata', async () => { +test('all 23 raw HTTP transports have reviewed context, access and idempotency metadata', async () => { const discovered = [] for (const module of ['common-audit', 'event-bridge-service', 'gateway-overseas', 'lastleg-service', 'integration-service']) { for (const file of await javaFiles(path.join(root, module, 'src', 'main', 'java'))) { @@ -120,7 +121,7 @@ test('all 22 raw HTTP transports have reviewed context, access and idempotency m } const reviewed = SYNCHRONOUS_CLIENTS.filter(client => client.transport === 'HTTP') .map(client => client.path).sort() - assert.equal(reviewed.length, 22) + assert.equal(reviewed.length, 23) assert.deepEqual(discovered.sort(), reviewed) }) @@ -271,6 +272,7 @@ test('known user entries expose every longest simple REQUEST path over the two-h ] }, 'warehouse-service': { hops: 0, paths: ['warehouse-service'] }, 'warehouse-overseas-service': { hops: 1, paths: [ + 'warehouse-overseas-service->print-service', 'warehouse-overseas-service->warehouse-service' ] } })