From 3f8ac23ababe34b16e38ddb9d4731c48838ebba2 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 5 Oct 2026 03:13:31 +0800 Subject: [PATCH] feat(transfer): persist receipt resource intents in schema baseline --- deploy/sql/README.md | 6 ++-- ..._add_transfer_receipt_resource_command.sql | 30 +++++++++++++++++++ deploy/sql/海外区域统一增量-MySQL8.sql | 27 ++++++++++++++++- .../src/main/resources/db/lastleg.sql | 25 ++++++++++++++++ .../initialization-baseline-contract.test.mjs | 17 +++++++++-- 5 files changed, 98 insertions(+), 7 deletions(-) create mode 100644 deploy/sql/migrations/overseas/baiyue_lastleg/V9__add_transfer_receipt_resource_command.sql diff --git a/deploy/sql/README.md b/deploy/sql/README.md index 4b3ce062f..de832a3cd 100644 --- a/deploy/sql/README.md +++ b/deploy/sql/README.md @@ -7,7 +7,7 @@ | 区域 | 完整入口 | 服务 schema | 表数 | |---|---|---|---:| | 国内 | `头程数据库统一增量-MySQL8.sql` | `baiyue_auth`、`baiyue_iam`、`baiyue_order`、`baiyue_warehouse`、`baiyue_firstleg`、`baiyue_file`、`baiyue_audit`、`baiyue_finance` | 226 | -| 海外 | `海外区域统一增量-MySQL8.sql` | `baiyue_lastleg`、`baiyue_lastlag_warehouse` | 153 | +| 海外 | `海外区域统一增量-MySQL8.sql` | `baiyue_lastleg`、`baiyue_lastlag_warehouse` | 154 | - 所有服务位于同一测试环境;两份文件只分组组织服务 schema,不代表两个部署区域。`baiyue_lastlag_warehouse` 保留当前正式的 `lastlag` 拼写。 - 两份文件必须在同一测试数据库实例中完成初始化;业务服务仍只访问本服务 schema,经内部接口访问 IAM/Warehouse。已移除事件桥两个 schema 的创建,不对已有数据库执行 DROP。 @@ -29,7 +29,7 @@ 5. DDL 隐式提交,不可整体回滚。失败后停止并保留错误;经另行授权准备空库后从头执行,不针对半初始化库追加迁移或从失败行续跑。 6. Firstleg 测量项与拆分项互为外键:建空表期间暂设会话 `foreign_key_checks=0`,随后立即恢复 `1`,再插入种子。失败时关闭连接。两边外键完整保留在 CREATE TABLE 内。 -成功时只在最末输出一行 `initialization_status=PASS`,并带 `region=CN/OVERSEAS`、`initialized_tables=226/153`。必须同时确认客户端退出码 0;忽略错误后出现的 PASS 不算成功。 +成功时只在最末输出一行 `initialization_status=PASS`,并带 `region=CN/OVERSEAS`、`initialized_tables=226/154`。必须同时确认客户端退出码 0;忽略错误后出现的 PASS 不算成功。 ## 平台管理员密码 @@ -45,7 +45,7 @@ | 所属库 | 保留内容 | 不包含 | |---|---|---| -| Auth / IAM | 平台主体、唯一平台管理员、8 个系统角色及企业管理员模板、392 个权限、573 条角色权限、7 个数据范围、客户编号起点;无人工完成订单权限 | 测试客户、固定客户账号、额外扩权 | +| Auth / IAM | 平台主体、唯一平台管理员、8 个系统角色及企业管理员模板、412 个权限、593 条角色权限、7 个数据范围、客户编号起点;无人工完成订单权限 | 测试客户、固定客户账号、额外扩权 | | Warehouse | 既有公共仓目录 318 条及地址;义乌、东莞、洛杉矶、纽约 4 个作业仓收货能力 | 示例供应商、快递、往来单位、托盘、库位、预占、库存 | | Firstleg | 112 个费用项目、15 个渠道目录、7 个始发地别名、4 个公共仓映射 | 客户报价、订单、入库、实测或履约记录 | | File / Finance | 导出互斥键 `GLOBAL_CREATE`、默认平台财务主体 | 文件任务、资金、账单或交易流水 | diff --git a/deploy/sql/migrations/overseas/baiyue_lastleg/V9__add_transfer_receipt_resource_command.sql b/deploy/sql/migrations/overseas/baiyue_lastleg/V9__add_transfer_receipt_resource_command.sql new file mode 100644 index 000000000..2e0e6f4cc --- /dev/null +++ b/deploy/sql/migrations/overseas/baiyue_lastleg/V9__add_transfer_receipt_resource_command.sql @@ -0,0 +1,30 @@ +-- Region: OVERSEAS +-- Schema: baiyue_lastleg +-- Purpose: persist original receipt commands before reserving shared target pallet resources. +-- Preconditions: target schema exists; this new table is absent. Do not replay after an unknown result. +-- Postconditions: per-source original payload and shared target reservation identity are durable. +-- No business-row backfill, old receipt repair, inventory change, or resource occupation. +CREATE TABLE ll_warehouse_transfer_receipt_command ( + id BIGINT NOT NULL COMMENT '收货命令主键', + customer_id VARCHAR(128) NOT NULL COMMENT '客户标识', + transfer_order_id BIGINT NOT NULL COMMENT '转运单标识', + source_inventory_position_id BIGINT NOT NULL COMMENT '本次收货来源库存位置', + idempotency_key VARCHAR(255) NOT NULL COMMENT '原始收货幂等键', + request_hash CHAR(64) NOT NULL COMMENT '完整原始收货请求摘要', + request_json JSON NOT NULL COMMENT '完整原始收货请求快照', + target_warehouse_id BIGINT NOT NULL COMMENT '实际目标海外收货仓', + target_pallet_id BIGINT NOT NULL COMMENT '目标实物托盘', + target_location_id BIGINT NOT NULL COMMENT '目标实际库位', + target_hash CHAR(64) NOT NULL COMMENT '共享目标资源绑定摘要', + target_json JSON NOT NULL COMMENT '权威目标仓托盘库位及目的仓快照', + reservation_key VARCHAR(128) NOT NULL COMMENT '同转运同目标托盘共享资源占用键', + resource_lock_id BIGINT NULL COMMENT '已确认有效的海外资源锁标识', + status VARCHAR(32) NOT NULL COMMENT '命令状态PREPARED准备完成RESERVED资源占用COMPLETED收货完成', + created_at DATETIME(3) NOT NULL COMMENT '命令创建时间', + updated_at DATETIME(3) NOT NULL COMMENT '命令最后更新时间', + PRIMARY KEY (id), + UNIQUE KEY uk_ll_wh_receipt_cmd_source (transfer_order_id, source_inventory_position_id), + UNIQUE KEY uk_ll_wh_receipt_cmd_key (customer_id, idempotency_key), + KEY idx_ll_wh_receipt_cmd_target (customer_id, transfer_order_id, target_pallet_id), + CONSTRAINT ck_ll_wh_receipt_cmd_status CHECK (status IN ('PREPARED','RESERVED','COMPLETED')) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='转运逐来源收货原请求与共享目标资源占用命令'; diff --git a/deploy/sql/海外区域统一增量-MySQL8.sql b/deploy/sql/海外区域统一增量-MySQL8.sql index 925faed59..f43d529fc 100644 --- a/deploy/sql/海外区域统一增量-MySQL8.sql +++ b/deploy/sql/海外区域统一增量-MySQL8.sql @@ -3203,6 +3203,31 @@ CREATE TABLE ll_warehouse_transfer_receipt ( KEY idx_ll_wh_transfer_receipt_list (customer_id,transfer_order_id,received_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='转仓来源仓位的目标库存收货记录'; +CREATE TABLE ll_warehouse_transfer_receipt_command ( + id BIGINT NOT NULL COMMENT '收货命令主键', + customer_id VARCHAR(128) NOT NULL COMMENT '客户标识', + transfer_order_id BIGINT NOT NULL COMMENT '转运单标识', + source_inventory_position_id BIGINT NOT NULL COMMENT '本次收货来源库存位置', + idempotency_key VARCHAR(255) NOT NULL COMMENT '原始收货幂等键', + request_hash CHAR(64) NOT NULL COMMENT '完整原始收货请求摘要', + request_json JSON NOT NULL COMMENT '完整原始收货请求快照', + target_warehouse_id BIGINT NOT NULL COMMENT '实际目标海外收货仓', + target_pallet_id BIGINT NOT NULL COMMENT '目标实物托盘', + target_location_id BIGINT NOT NULL COMMENT '目标实际库位', + target_hash CHAR(64) NOT NULL COMMENT '共享目标资源绑定摘要', + target_json JSON NOT NULL COMMENT '权威目标仓托盘库位及目的仓快照', + reservation_key VARCHAR(128) NOT NULL COMMENT '同转运同目标托盘共享资源占用键', + resource_lock_id BIGINT NULL COMMENT '已确认有效的海外资源锁标识', + status VARCHAR(32) NOT NULL COMMENT '命令状态PREPARED准备完成RESERVED资源占用COMPLETED收货完成', + created_at DATETIME(3) NOT NULL COMMENT '命令创建时间', + updated_at DATETIME(3) NOT NULL COMMENT '命令最后更新时间', + PRIMARY KEY (id), + UNIQUE KEY uk_ll_wh_receipt_cmd_source (transfer_order_id, source_inventory_position_id), + UNIQUE KEY uk_ll_wh_receipt_cmd_key (customer_id, idempotency_key), + KEY idx_ll_wh_receipt_cmd_target (customer_id, transfer_order_id, target_pallet_id), + CONSTRAINT ck_ll_wh_receipt_cmd_status CHECK (status IN ('PREPARED','RESERVED','COMPLETED')) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='转运逐来源收货原请求与共享目标资源占用命令'; + CREATE TABLE ll_warehouse_transfer_relabel_binding ( id bigint NOT NULL COMMENT '主键ID', transfer_order_id bigint NOT NULL COMMENT '转运单ID', @@ -3428,4 +3453,4 @@ CREATE TABLE event_delivery_attempt ( KEY idx_delivery_attempt_outcome (outcome,started_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='本服务业务事件逐次投递审计'; -SELECT 'PASS' AS initialization_status, 'OVERSEAS' AS region, 153 AS initialized_tables; +SELECT 'PASS' AS initialization_status, 'OVERSEAS' AS region, 154 AS initialized_tables; diff --git a/lastleg-service/src/main/resources/db/lastleg.sql b/lastleg-service/src/main/resources/db/lastleg.sql index 798829569..a014168b8 100644 --- a/lastleg-service/src/main/resources/db/lastleg.sql +++ b/lastleg-service/src/main/resources/db/lastleg.sql @@ -2191,6 +2191,31 @@ CREATE TABLE IF NOT EXISTS ll_warehouse_transfer_receipt ( KEY idx_ll_wh_transfer_receipt_list(customer_id,transfer_order_id,received_at) ) ENGINE=InnoDB COMMENT='转仓来源仓位的目标库存收货记录'; +CREATE TABLE IF NOT EXISTS ll_warehouse_transfer_receipt_command ( + id BIGINT NOT NULL COMMENT '收货命令主键', + customer_id VARCHAR(128) NOT NULL COMMENT '客户标识', + transfer_order_id BIGINT NOT NULL COMMENT '转运单标识', + source_inventory_position_id BIGINT NOT NULL COMMENT '本次收货来源库存位置', + idempotency_key VARCHAR(255) NOT NULL COMMENT '原始收货幂等键', + request_hash CHAR(64) NOT NULL COMMENT '完整原始收货请求摘要', + request_json JSON NOT NULL COMMENT '完整原始收货请求快照', + target_warehouse_id BIGINT NOT NULL COMMENT '实际目标海外收货仓', + target_pallet_id BIGINT NOT NULL COMMENT '目标实物托盘', + target_location_id BIGINT NOT NULL COMMENT '目标实际库位', + target_hash CHAR(64) NOT NULL COMMENT '共享目标资源绑定摘要', + target_json JSON NOT NULL COMMENT '权威目标仓托盘库位及目的仓快照', + reservation_key VARCHAR(128) NOT NULL COMMENT '同转运同目标托盘共享资源占用键', + resource_lock_id BIGINT NULL COMMENT '已确认有效的海外资源锁标识', + status VARCHAR(32) NOT NULL COMMENT '命令状态PREPARED准备完成RESERVED资源占用COMPLETED收货完成', + created_at DATETIME(3) NOT NULL COMMENT '命令创建时间', + updated_at DATETIME(3) NOT NULL COMMENT '命令最后更新时间', + PRIMARY KEY (id), + UNIQUE KEY uk_ll_wh_receipt_cmd_source (transfer_order_id, source_inventory_position_id), + UNIQUE KEY uk_ll_wh_receipt_cmd_key (customer_id, idempotency_key), + KEY idx_ll_wh_receipt_cmd_target (customer_id, transfer_order_id, target_pallet_id), + CONSTRAINT ck_ll_wh_receipt_cmd_status CHECK (status IN ('PREPARED','RESERVED','COMPLETED')) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='转运逐来源收货原请求与共享目标资源占用命令'; + -- Customer relabel application: header, item snapshots, and immutable process history. CREATE TABLE IF NOT EXISTS ll_relabel_application ( id BIGINT NOT NULL COMMENT '主键ID', 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 63ac026b0..d49d44e4c 100644 --- a/scripts/admin-test-env-modules/initialization-baseline-contract.test.mjs +++ b/scripts/admin-test-env-modules/initialization-baseline-contract.test.mjs @@ -9,7 +9,7 @@ const domesticSchemas = ['baiyue_auth', 'baiyue_iam', 'baiyue_order', 'baiyue_wa const overseasSchemas = ['baiyue_lastleg', 'baiyue_lastlag_warehouse'] test('两个入口分别只操作八个主业务库与两个仓储业务库,包括限定名引用', () => { - for (const [sql, allowed, other, count] of [[cn, domesticSchemas, overseasSchemas, 226], [overseas, overseasSchemas, domesticSchemas, 153]]) { + for (const [sql, allowed, other, count] of [[cn, domesticSchemas, overseasSchemas, 226], [overseas, overseasSchemas, domesticSchemas, 154]]) { assert.deepEqual([...sql.matchAll(/^CREATE DATABASE IF NOT EXISTS (\w+)/gm)].map(m => m[1]), allowed) assert.deepEqual([...new Set([...sql.matchAll(/^USE (\w+);/gm)].map(m => m[1]))], allowed) const body = sql.replace(/^--.*$/gm, '') @@ -19,6 +19,17 @@ test('两个入口分别只操作八个主业务库与两个仓储业务库, } }) +test('转运收货命令锁定原来源和原请求,多来源可共享目标资源锁', () => { + const command = table(overseas, 'baiyue_lastleg', 'll_warehouse_transfer_receipt_command') + assert.match(command, /UNIQUE KEY uk_ll_wh_receipt_cmd_source\s*\(transfer_order_id,\s*source_inventory_position_id\)/i) + assert.match(command, /UNIQUE KEY uk_ll_wh_receipt_cmd_key\s*\(customer_id,\s*idempotency_key\)/i) + assert.match(command, /request_json JSON NOT NULL/i) + assert.match(command, /target_json JSON NOT NULL/i) + assert.match(command, /resource_lock_id BIGINT NULL/i) + assert.match(command, /CHECK\s*\(status IN \('PREPARED','RESERVED','COMPLETED'\)\)/i) + assert.doesNotMatch(command, /UNIQUE[^\n]*resource_lock_id/i) +}) + test('纯初始化禁止迁移、数据回填、清理、重复建表和提前成功', () => { for (const sql of [cn, overseas]) { const body = sql.replace(/^--.*$/gm, '') @@ -28,7 +39,7 @@ test('纯初始化禁止迁移、数据回填、清理、重复建表和提前 assert.doesNotMatch(body.replace(/^CREATE TRIGGER [\s\S]*?\$\$/gm, ''), /^\s*(?:UPDATE|DELETE FROM|REPLACE INTO)\b/gmi) assert.doesNotMatch(body, /CREATE TABLE IF NOT EXISTS/i) assert.equal((body.match(/AS initialization_status/g) ?? []).length, 1) - assert.match(body.trim(), /SELECT 'PASS' AS initialization_status, '(CN|OVERSEAS)' AS region, (226|153) AS initialized_tables;$/) + assert.match(body.trim(), /SELECT 'PASS' AS initialization_status, '(CN|OVERSEAS)' AS region, (226|154) AS initialized_tables;$/) } }) @@ -110,7 +121,7 @@ test('平台管理员由外部bcrypt引导,强制首登改密且不创建客 }) test('必要系统种子完整保留,但不写入示例资源与业务事实', () => { - for (const [name, count] of Object.entries({ iam_role: 8, iam_permission: 411, iam_role_permission: 592, + for (const [name, count] of Object.entries({ iam_role: 8, iam_permission: 412, iam_role_permission: 593, iam_data_scope: 7, wh_warehouse: 318, wh_warehouse_address: 318, wh_service_capability: 4, fl_fee_item: 112, fl_price_channel_catalog: 15, fl_price_origin_alias: 7, fl_price_warehouse_mapping: 4, file_export_task_guard: 1, fin_subject: 1 })) assert.equal(seedRows(cn, name).length, count, name)