Merge remote-tracking branch 'origin/test' into dev

# Conflicts:
#	lastleg-service/src/main/resources/db/lastleg.sql
This commit is contained in:
2026-09-10 10:01:35 +08:00
10 changed files with 2888 additions and 254 deletions
+3 -3
View File
@@ -3,8 +3,8 @@
## 唯一数据库初始化与发布入口
- 国内区域固定入口:`头程数据库统一增量-MySQL8.sql`。文件名保留历史兼容,内容维护为当前完整数据库基线和存量环境兼容升级,不再按日期拆分发布脚本。
- 海外区域固定增量入口:`海外区域统一增量-MySQL8.sql`;空库先分别执行 `lastleg-service/src/main/resources/db/lastleg.sql`与 `warehouse-overseas-service/src/main/resources/db/warehouse.sql` 基线,再执行该入口。固定入口会创建并校验独立的 `baiyue_event_bridge_overseas`,再升级 `baiyue_lastleg`与 `baiyue_lastlag_warehouse`;IAM 安全事件 Inbox 和可恢复投影已并入 `baiyue_lastleg.ll_security_*`,Redis 只作可重建镜像。
- Lastleg 基线和海外固定增量仅确定性初始化国内固定 `platform_admin`(`authUserId=1000001`、`tenantId=0`)的 USER/AUTHORIZATION version=0 安全投影;其他既有开发账号不建立兼容表或长期补投入口。
- 海外区域固定入口:`海外区域统一增量-MySQL8.sql`。该文件已经内联 Lastleg 与 Warehouse Overseas 的完整空库基线,会自行创建并初始化 `baiyue_lastleg`、`baiyue_lastlag_warehouse` 和独立的 `baiyue_event_bridge_overseas`,清库后不得再额外执行模块基线。IAM 安全事件 Inbox 和可恢复投影位于 `baiyue_lastleg.ll_security_*`,Redis 只作可重建镜像。
- Lastleg 基线和海外固定增量仅确定性初始化国内固定 `platform_admin`(`authUserId=1000001`、`tenantId=0`)的 USER/AUTHORIZATION version=0 安全投影;AUTHORIZATION 同步冻结当前 `PLATFORM_ADMIN` 角色及 IAM 跨区前缀下的完整权限集合,保证空库重建后海外 Gateway 可立即鉴权。统一 SQL 重放只修正仍处于固定 baseline version=0 的该行,其他既有开发账号不建立兼容表或长期补投入口。
- 国内、海外 Compose 部署默认设置 `SPRING_SQL_INIT_MODE=never`,执行任一 SQL 均需用户单独授权。
- 该脚本汇总订单与客户、入库测量与计费及已实现国内货柜履约出仓能力的数据库基线、存量升级、IAM 权限资源和最终结构校验,适用于 MySQL 8.0。
- 正式上线前的数据库变更直接维护该文件中的当前基线、存量兼容处理和最终校验,不再创建按日期拆分的发布 SQL;正式上线后的迁移策略在生产发布规范中另行冻结。
@@ -239,7 +239,7 @@ SELECT
- 派送三类资料当前基线包含 `ll_delivery_order.bol_no/documents_json` 与 `ll_delivery_document_revision`;海外固定入口和 Lastleg 模块基线同步维护这些定义。仅保存装车照片、BOL文件(含签名)、签收文件(POD签收/快递签收)三个类别,摘要按类别计数。资料历史不可变,文件对象由 File 按真实客户与派送单绑定;不按 BOL 文本复用文件,不生成旧类别迁移或清理脚本。
- 海外仓出库与派送数据库部署仍只执行 `海外区域统一增量-MySQL8.sql`;空库先执行 Lastleg 与 Warehouse Overseas 当前基线,再完整执行固定入口并确认最末 `migration_status = PASS`。
- 海外仓出库与派送数据库部署只执行 `海外区域统一增量-MySQL8.sql`;无论空库还是存量测试库都不得在此前单独执行 Lastleg 或 Warehouse Overseas 模块基线,完整执行固定入口并确认最末 `migration_status = PASS`。
- 该海外固定入口会在任何海外仓出库与派送 `ALTER` 与数据门禁前幂等创建排车 PDA、出库、派送、一件代发、异常订单和客户回访的完整当前结构,因此既有海外仓业务库不依赖模块基线或历史 `scripts/db/incremental.sql`。国内固定入口只保留 IAM 海外仓出库与派送权限目录,不创建或修改 `baiyue_lastleg` 业务表。
- 固定入口只补目标字段并做 fail-fast 校验,不回填、改号、清理或从其他表推断开发旧行。`ll_dispatch_pda_task.warehouse_id` 存在 `NULL`、空值或 `0`,或任一待收敛全局业务号/任务号、完成幂等键发生冲突时,脚本立即 `SIGNAL`;应重新造数,重置或清理仍需用户另行授权。
- 数据门禁通过后,脚本原位把 `dispatch_no`、`outbound_no`、排车 PDA `task_no`、出库 PDA `task_no` 收敛为全局唯一,并建立 `(customer_id, completion_idempotency_key)` 完成命令唯一约束;旧客户复合索引不保留。
@@ -12855,6 +12855,40 @@ CALL queue_table_comment('baiyue_order', 'ord_order_status_history', '订单状
CALL queue_table_comment('baiyue_order', 'ord_outbox_event', '订单事务Outbox表:与订单业务变更在同一本地事务写入,供后台发布器可靠投递RocketMQ,保证数据库状态与领域事件最终一致;不是普通操作日志');
CALL queue_table_comment('baiyue_order', 'sys_audit_outbox', '可靠审计事件本地发送箱');
CALL queue_table_comment('baiyue_warehouse', 'sys_audit_outbox', '可靠审计事件本地发送箱');
USE baiyue_firstleg;
CREATE TABLE IF NOT EXISTS fl_order_declaration_snapshot (
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和隔离键',
order_id BIGINT UNSIGNED NOT NULL COMMENT 'Order订单ID',
event_id VARCHAR(128) NOT NULL COMMENT 'Order Outbox事件ID',
order_version INT NOT NULL COMMENT 'Order聚合版本,严格单调消费',
request_hash CHAR(64) NOT NULL COMMENT '完整事件SHA-256',
customer_remark VARCHAR(1000) NULL COMMENT '客户备注快照',
expected_arrival_at DATETIME(3) NULL COMMENT '预计到仓时间快照',
channel_code VARCHAR(64) NOT NULL COMMENT '渠道编码快照',
channel_name VARCHAR(128) NOT NULL COMMENT '渠道名称快照',
cargoes_json JSON NOT NULL COMMENT '客户申报货物行完整快照,不覆盖实测事实',
changes_json JSON NULL COMMENT '本版本申报字段变化摘要',
occurred_at DATETIME(3) NOT NULL COMMENT 'Order事件发生时间',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '快照创建时间',
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '快照更新时间',
PRIMARY KEY (tenant_id, order_id),
UNIQUE KEY uk_order_declaration_event (tenant_id, event_id),
KEY idx_order_declaration_version (tenant_id, order_id, order_version)
) ENGINE=InnoDB COMMENT='Order客户申报版本快照;与箱码、实测、托盘、PDA和计费事实隔离';
CREATE TABLE IF NOT EXISTS fl_order_declaration_event (
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和隔离键',
event_id VARCHAR(128) NOT NULL COMMENT '永久保留的Order Outbox事件ID',
order_id BIGINT UNSIGNED NOT NULL COMMENT 'Order订单ID',
order_version INT NOT NULL COMMENT '事件携带的Order聚合版本',
request_hash CHAR(64) NOT NULL COMMENT '完整事件SHA-256,用于异参重放拒绝',
consumed_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '事件消费完成时间',
PRIMARY KEY (tenant_id, event_id),
KEY idx_order_declaration_event_order (tenant_id, order_id, order_version)
) ENGINE=InnoDB COMMENT='Order客户申报事件永久幂等账本';
CALL queue_table_comment('baiyue_firstleg', 'fl_order_declaration_event', 'Order客户申报事件永久幂等账本');
CALL queue_table_comment('baiyue_firstleg', 'fl_order_declaration_snapshot', 'Order客户申报版本快照;与箱码、实测、托盘、PDA和计费事实隔离');
CALL queue_table_comment('baiyue_warehouse', 'wh_express_company', '平台公共快递公司主数据,固定归属 tenant_id=0');
@@ -16908,6 +16942,12 @@ VALUES
(5107061,0,'PLATFORM','BUTTON',5107060,'platform:overseas-warehouse:location:create','创建海外库位',NULL,'POST',761,'ACTIVE',1,0),
(5107062,0,'PLATFORM','BUTTON',5107060,'platform:overseas-warehouse:location:edit','编辑海外库位',NULL,'PUT',762,'ACTIVE',1,0),
(5107063,0,'PLATFORM','BUTTON',5107060,'platform:overseas-warehouse:location:delete','删除海外库位',NULL,'DELETE',763,'ACTIVE',1,0),
(5107230,0,'PLATFORM','MENU',0,'platform:overseas-warehouse:carton:list','海外箱号','/overseas-warehouse/cartons','GET',930,'ACTIVE',1,0),
(5107231,0,'PLATFORM','BUTTON',5107230,'platform:overseas-warehouse:carton:create','创建海外箱号',NULL,'POST',931,'ACTIVE',1,0),
(5107232,0,'PLATFORM','BUTTON',5107230,'platform:overseas-warehouse:carton:edit','编辑海外箱号',NULL,'PUT',932,'ACTIVE',1,0),
(5107240,0,'PLATFORM','MENU',0,'platform:overseas-warehouse:product:list','海外产品库','/overseas-warehouse/products','GET',940,'ACTIVE',1,0),
(5107241,0,'PLATFORM','BUTTON',5107240,'platform:overseas-warehouse:product:create','创建海外产品',NULL,'POST',941,'ACTIVE',1,0),
(5107242,0,'PLATFORM','BUTTON',5107240,'platform:overseas-warehouse:product:edit','编辑海外产品',NULL,'PUT',942,'ACTIVE',1,0),
(5107090,0,'PLATFORM','MENU',0,'platform:overseas-warehouse:outbound:list','海外出库单','/overseas-warehouse/outbounds','GET',765,'ACTIVE',1,0),
(5107091,0,'PLATFORM','BUTTON',5107090,'platform:overseas-warehouse:outbound:create','创建海外出库单',NULL,'POST',766,'ACTIVE',1,0),
(5107092,0,'PLATFORM','MENU',0,'platform:overseas-warehouse:delivery:list','海外派送单','/overseas-warehouse/delivery-orders','GET',767,'ACTIVE',1,0),
@@ -18314,7 +18354,12 @@ SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE tab
PREPARE lx_order_ddl FROM @ddl; EXECUTE lx_order_ddl; DEALLOCATE PREPARE lx_order_ddl;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='baiyue_firstleg' AND table_name='fl_inbound_order' AND column_name='lx_code'), 'SELECT 1', 'ALTER TABLE baiyue_firstleg.fl_inbound_order ADD COLUMN lx_code VARCHAR(128) NULL COMMENT ''一件代发领星入库单代码'' AFTER fulfillment_type'));
PREPARE lx_inbound_ddl FROM @ddl; EXECUTE lx_inbound_ddl; DEALLOCATE PREPARE lx_inbound_ddl;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='baiyue_lastleg' AND table_name='ll_outbound_order' AND column_name='lx_codes'), 'SELECT 1', 'ALTER TABLE baiyue_lastleg.ll_outbound_order ADD COLUMN lx_codes VARCHAR(1000) NULL COMMENT ''关联一件代发领星入库单代码汇总'' AFTER outbound_type'));
SET @ddl = (SELECT IF(
NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema='baiyue_lastleg' AND table_name='ll_outbound_order')
OR EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='baiyue_lastleg' AND table_name='ll_outbound_order' AND column_name='lx_codes'),
'SELECT 1',
'ALTER TABLE baiyue_lastleg.ll_outbound_order ADD COLUMN lx_codes VARCHAR(1000) NULL COMMENT ''关联一件代发领星入库单代码汇总'' AFTER outbound_type'
));
PREPARE lx_outbound_ddl FROM @ddl; EXECUTE lx_outbound_ddl; DEALLOCATE PREPARE lx_outbound_ddl;
ALTER TABLE baiyue_firstleg.fl_inbound_order
File diff suppressed because it is too large Load Diff
@@ -39,6 +39,12 @@ VALUES
(5107061,0,'PLATFORM','BUTTON',5107060,'platform:overseas-warehouse:location:create','创建海外库位',NULL,'POST',761,'ACTIVE',1,0),
(5107062,0,'PLATFORM','BUTTON',5107060,'platform:overseas-warehouse:location:edit','编辑海外库位',NULL,'PUT',762,'ACTIVE',1,0),
(5107063,0,'PLATFORM','BUTTON',5107060,'platform:overseas-warehouse:location:delete','删除海外库位',NULL,'DELETE',763,'ACTIVE',1,0),
(5107230,0,'PLATFORM','MENU',0,'platform:overseas-warehouse:carton:list','海外箱号','/overseas-warehouse/cartons','GET',930,'ACTIVE',1,0),
(5107231,0,'PLATFORM','BUTTON',5107230,'platform:overseas-warehouse:carton:create','创建海外箱号',NULL,'POST',931,'ACTIVE',1,0),
(5107232,0,'PLATFORM','BUTTON',5107230,'platform:overseas-warehouse:carton:edit','编辑海外箱号',NULL,'PUT',932,'ACTIVE',1,0),
(5107240,0,'PLATFORM','MENU',0,'platform:overseas-warehouse:product:list','海外产品库','/overseas-warehouse/products','GET',940,'ACTIVE',1,0),
(5107241,0,'PLATFORM','BUTTON',5107240,'platform:overseas-warehouse:product:create','创建海外产品',NULL,'POST',941,'ACTIVE',1,0),
(5107242,0,'PLATFORM','BUTTON',5107240,'platform:overseas-warehouse:product:edit','编辑海外产品',NULL,'PUT',942,'ACTIVE',1,0),
(5107090,0,'PLATFORM','MENU',0,'platform:overseas-warehouse:outbound:list','海外出库单','/overseas-warehouse/outbounds','GET',765,'ACTIVE',1,0),
(5107091,0,'PLATFORM','BUTTON',5107090,'platform:overseas-warehouse:outbound:create','创建海外出库单',NULL,'POST',766,'ACTIVE',1,0),
(5107092,0,'PLATFORM','MENU',0,'platform:overseas-warehouse:delivery:list','海外派送单','/overseas-warehouse/delivery-orders','GET',767,'ACTIVE',1,0),
+1 -1
View File
@@ -96,7 +96,7 @@
- 内部接口先执行常量时间令牌校验;缺失或错误令牌不得进入消费事务。
- IAM 安全事件只能进入 `/internal/lastleg/security/v1/events`;通用 `/internal/lastleg/events` 只白名单接收装柜完成与实际船节点事件,禁止把 IAM 事件交给 `LoadingCompletedConsumer`。
- Lastleg 安全投影只接受 `ACTIVE`/`DISABLED` 两态安全语义;客户建档和审核的 `PROFILE_REQUIRED`/`PENDING`/`REJECTED`/`ARCHIVED` 由 IAM 在发布前统一折叠为 `DISABLED`,Lastleg 不兼容或解释国内工作流状态。
- 开发基线只为国内固定 `authUserId=1000001`、`tenantId=0` 的 `platform_admin` 写 USER/AUTHORIZATION version=0 投影;使用 `INSERT IGNORE`,后续 IAM version>=1 事件单调覆盖。不得扩大为历史账号运行时补齐或缺失投影默认放行。
- 开发/测试空库基线只为国内固定 `authUserId=1000001`、`tenantId=0` 的 `platform_admin` 写 USER/AUTHORIZATION version=0 投影;AUTHORIZATION 必须携带当前 `PLATFORM_ADMIN` 角色及按 IAM 跨区前缀筛选的完整权限快照,统一 SQL 重放只修正仍处于固定 baseline version=0 的该行,后续 IAM version>=1 事件单调覆盖。不得扩大为其他历史账号运行时补齐或缺失投影默认放行。
- PDA 入仓任务完成前必须先通过 `/unload-photos` 保存非空且不重复的卸柜照片文件 ID;照片上传时间由服务端记录,预计报空时间固定为照片上传时间加两天。完成入仓时,库存入账、计划/任务状态推进与 `OVERSEAS_CONTAINER_UNLOADED` V1 Outbox 必须处于同一事务;事件绑定来源头程货柜身份,不得从前端请求拼装或回查头程数据库。
- 海外 PDA 柜号签收只允许平台 `OVERSEAS_WAREHOUSE_PDA_OPERATOR` 使用 `lastleg:pda:operate`;国内头程 PDA 角色不得进入。PDA 不接收 `customerId`,候选按来源柜跨客户聚合,同柜允许多个客户;选柜会话、三类照片及整柜完成事实只绑定 `sourceContainerBookingId + containerNo`,文件与柜级跨区事件使用 `tenantId=0` 平台技术范围。签收后按每个入仓计划自身客户生成任务,扫码、SKU 实收、托盘和库存始终使用入仓码所属票的真实客户。未完成会话按平台操作账号和柜号分别全局唯一;平台管理员和客户账号不得进入该作业入口。
+125 -153
View File
@@ -1436,8 +1436,6 @@ CREATE TABLE IF NOT EXISTS ll_outbound_pda_pick (
inventory_position_id BIGINT NOT NULL COMMENT '库存仓位ID', packages BIGINT NOT NULL COMMENT '拣货箱数', idempotency_key VARCHAR(255) NOT NULL COMMENT '拣货幂等键',
picked_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '拣货时间', PRIMARY KEY(id), UNIQUE KEY uk_ll_outbound_pda_pick_key(customer_id,idempotency_key)
) ENGINE=InnoDB COMMENT='一件代发出库PDA托盘拣货记录';
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_outbound_pda_pick' AND column_name='inventory_position_id' AND is_nullable='YES'), 'SELECT 1', 'ALTER TABLE ll_outbound_pda_pick MODIFY COLUMN inventory_position_id BIGINT NULL COMMENT ''库存仓位ID,退货箱唛出库为空'''));
PREPARE outbound_pda_pick_position_stmt FROM @ddl; EXECUTE outbound_pda_pick_position_stmt; DEALLOCATE PREPARE outbound_pda_pick_position_stmt;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_outbound_order' AND column_name='outbound_type'), 'SELECT 1', 'ALTER TABLE ll_outbound_order ADD COLUMN outbound_type VARCHAR(32) NOT NULL DEFAULT ''DISPATCH_DELIVERY'' COMMENT ''出库类型'' AFTER status'));
PREPARE outbound_upgrade_stmt FROM @ddl; EXECUTE outbound_upgrade_stmt; DEALLOCATE PREPARE outbound_upgrade_stmt;
@@ -1566,9 +1564,6 @@ CREATE TABLE IF NOT EXISTS ll_relabel_application (
contact_name VARCHAR(128) NOT NULL COMMENT '联系人姓名',
contact_phone VARCHAR(64) NOT NULL COMMENT '联系人电话',
remark VARCHAR(1000) NULL COMMENT '备注',
total_file_id VARCHAR(128) NULL COMMENT '总换标PDF文件ID',
total_file_name VARCHAR(255) NULL COMMENT '总换标PDF文件名',
total_file_url VARCHAR(1000) NULL COMMENT '总换标PDF预览地址',
status VARCHAR(32) NOT NULL DEFAULT 'PENDING_PROCESSING' COMMENT '业务状态',
item_count INT NOT NULL DEFAULT 0 COMMENT '明细数量',
total_package_count INT NOT NULL DEFAULT 0 COMMENT '总箱数',
@@ -1593,10 +1588,6 @@ CREATE TABLE IF NOT EXISTS ll_relabel_application (
KEY idx_ll_relabel_application_warehouse (customer_id,overseas_warehouse_id)
) ENGINE=InnoDB COMMENT='客户换标申请单';
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_application' AND column_name='total_file_id'), 'SELECT 1', 'ALTER TABLE ll_relabel_application ADD COLUMN total_file_id VARCHAR(128) NULL COMMENT ''总换标PDF文件ID'' AFTER remark')); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_application' AND column_name='total_file_name'), 'SELECT 1', 'ALTER TABLE ll_relabel_application ADD COLUMN total_file_name VARCHAR(255) NULL COMMENT ''总换标PDF文件名'' AFTER total_file_id')); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_application' AND column_name='total_file_url'), 'SELECT 1', 'ALTER TABLE ll_relabel_application ADD COLUMN total_file_url VARCHAR(1000) NULL COMMENT ''总换标PDF预览地址'' AFTER total_file_name')); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
CREATE TABLE IF NOT EXISTS ll_relabel_item (
id BIGINT NOT NULL COMMENT '主键ID',
application_id BIGINT NOT NULL COMMENT '申请单ID',
@@ -1618,23 +1609,12 @@ CREATE TABLE IF NOT EXISTS ll_relabel_item (
relabel_file_url VARCHAR(1000) NULL COMMENT '换标文件地址',
carton_mark VARCHAR(255) NULL COMMENT '客户自有箱唛',
fba_carton_mark VARCHAR(255) NULL COMMENT '关联FBA箱唛',
destination_warehouse_id VARCHAR(128) NOT NULL COMMENT '该箱目的海外仓ID',
destination_fba_warehouse_code VARCHAR(128) NOT NULL COMMENT 'FBA目的仓编码(与海外操作仓分离)',
operation_type VARCHAR(32) NOT NULL DEFAULT 'CARTON_LABEL' COMMENT '换标模式:箱唛、SKU或组合换标',
sku_relabel_items_json JSON NULL COMMENT '同箱SKU换标子明细快照(数量按件)',
source_inventory_balance_id BIGINT NOT NULL COMMENT '原库存余额ID',
source_inventory_position_id BIGINT NOT NULL COMMENT '原库存库位ID',
source_inbound_order_id BIGINT NOT NULL COMMENT '原入仓单ID',
source_inbound_order_no VARCHAR(128) NULL COMMENT '原入仓单号快照',
source_inbound_code VARCHAR(128) NULL COMMENT '原进仓编码快照',
source_warehouse_id VARCHAR(128) NOT NULL COMMENT '原海外中转仓ID',
source_warehouse_code VARCHAR(128) NULL COMMENT '原海外中转仓编码快照',
source_warehouse_name VARCHAR(255) NULL COMMENT '原海外中转仓名称快照',
destination_warehouse_id VARCHAR(128) NULL COMMENT '该箱目的海外仓ID',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
PRIMARY KEY (id),
KEY idx_ll_relabel_item_application (customer_id,application_id),
KEY idx_ll_relabel_item_sku (customer_id,sku_code),
KEY idx_ll_relabel_item_source_position (customer_id,source_inventory_position_id)
KEY idx_ll_relabel_item_destination (customer_id,destination_warehouse_id,application_id)
) ENGINE=InnoDB COMMENT='换标货物冻结明细';
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_item' AND column_name='remark'), 'SELECT 1', 'ALTER TABLE ll_relabel_item ADD COLUMN remark VARCHAR(1000) NULL COMMENT ''货物明细备注'' AFTER label_requirement')); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
@@ -2018,17 +1998,6 @@ CREATE TABLE IF NOT EXISTS ll_exception_order_command (
KEY idx_ll_exception_command_order(customer_id,exception_order_id,created_at)
) ENGINE=InnoDB COMMENT='异常处理命令幂等审计';
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_exception_order' AND column_name='handling_result'), 'SELECT 1', 'ALTER TABLE ll_exception_order ADD COLUMN handling_result VARCHAR(2000) NULL COMMENT ''异常处理结果'' AFTER handling_action'));
PREPARE exception_lifecycle_stmt FROM @ddl; EXECUTE exception_lifecycle_stmt; DEALLOCATE PREPARE exception_lifecycle_stmt;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_exception_order' AND column_name='evidence_file_ids_json'), 'SELECT 1', 'ALTER TABLE ll_exception_order ADD COLUMN evidence_file_ids_json JSON NULL COMMENT ''异常处理证据文件ID列表'' AFTER handling_result'));
PREPARE exception_lifecycle_stmt FROM @ddl; EXECUTE exception_lifecycle_stmt; DEALLOCATE PREPARE exception_lifecycle_stmt;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_exception_order' AND column_name='inventory_adjustment_id'), 'SELECT 1', 'ALTER TABLE ll_exception_order ADD COLUMN inventory_adjustment_id BIGINT NULL COMMENT ''退回库存恢复操作ID'' AFTER evidence_file_ids_json'));
PREPARE exception_lifecycle_stmt FROM @ddl; EXECUTE exception_lifecycle_stmt; DEALLOCATE PREPARE exception_lifecycle_stmt;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_exception_order' AND column_name='completed_at'), 'SELECT 1', 'ALTER TABLE ll_exception_order ADD COLUMN completed_at DATETIME(3) NULL COMMENT ''异常处理完成时间'' AFTER exception_at'));
PREPARE exception_lifecycle_stmt FROM @ddl; EXECUTE exception_lifecycle_stmt; DEALLOCATE PREPARE exception_lifecycle_stmt;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_exception_order' AND column_name='version'), 'SELECT 1', 'ALTER TABLE ll_exception_order ADD COLUMN version BIGINT NOT NULL DEFAULT 0 COMMENT ''异常订单乐观锁版本'' AFTER completed_at'));
PREPARE exception_lifecycle_stmt FROM @ddl; EXECUTE exception_lifecycle_stmt; DEALLOCATE PREPARE exception_lifecycle_stmt;
CREATE TABLE IF NOT EXISTS ll_customer_visit (
id BIGINT NOT NULL COMMENT '客户回访记录ID',
customer_id VARCHAR(128) NOT NULL COMMENT '客户数据隔离键',
@@ -2089,31 +2058,137 @@ CREATE TABLE IF NOT EXISTS ll_security_projection (
KEY idx_security_projection_permission (aggregate_id,permission_version)
) ENGINE=InnoDB COMMENT='海外IAM安全状态与权限版本投影';
-- 兼容已有的旧版投影表:CREATE TABLE IF NOT EXISTS 不会补充后续新增字段。
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE()
AND table_name='ll_security_projection' AND column_name='roles_json'), 'SELECT 1',
'ALTER TABLE ll_security_projection ADD COLUMN roles_json JSON NULL COMMENT ''IAM投影的当前角色快照'' AFTER permission_version'));
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE()
AND table_name='ll_security_projection' AND column_name='authorities_json'), 'SELECT 1',
'ALTER TABLE ll_security_projection ADD COLUMN authorities_json JSON NULL COMMENT ''IAM投影的当前海外权限快照'' AFTER roles_json'));
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
UPDATE ll_security_projection SET roles_json=JSON_ARRAY() WHERE roles_json IS NULL;
UPDATE ll_security_projection SET authorities_json=JSON_ARRAY() WHERE authorities_json IS NULL;
ALTER TABLE ll_security_projection
MODIFY COLUMN roles_json JSON NOT NULL COMMENT 'IAM投影的当前角色快照',
MODIFY COLUMN authorities_json JSON NOT NULL COMMENT 'IAM投影的当前海外权限快照';
-- 开发基线固定管理员;后续真实 IAM 事件从 aggregate_version=1 单调覆盖该 version=0 投影。
-- 空库基线固定管理员必须携带当前海外授权快照;后续真实 IAM 事件从 aggregate_version=1 单调覆盖该 version=0 投影。
SET @platform_admin_overseas_roles = JSON_ARRAY('PLATFORM_ADMIN');
SET @platform_admin_overseas_authorities = JSON_ARRAY(
'platform:finance:bill:adjust',
'platform:finance:bill:update',
'platform:finance:bill:void',
'platform:finance:cost:create',
'platform:finance:cost:view',
'platform:finance:customer-account:adjust',
'platform:finance:customer-account:export',
'platform:finance:customer-account:list',
'platform:finance:customer-arap:export',
'platform:finance:customer-arap:list',
'platform:finance:customer-statement:export',
'platform:finance:customer-statement:list',
'platform:finance:customer-terms:update',
'platform:finance:customer-terms:view',
'platform:finance:fee-source:edit',
'platform:finance:fee-source:review',
'platform:finance:payment:create',
'platform:finance:receipt:create',
'platform:finance:receipt:view',
'platform:finance:receivable-statistics:export',
'platform:finance:receivable-statistics:list',
'platform:finance:recharge:review',
'platform:finance:reconciliation:adjust',
'platform:finance:reconciliation:create',
'platform:finance:reconciliation:issue',
'platform:finance:reconciliation:review',
'platform:finance:reconciliation:void',
'platform:finance:settlement-ap:create',
'platform:finance:settlement-ap:export',
'platform:finance:settlement-ap:list',
'platform:finance:settlement-ar:create',
'platform:finance:settlement-ar:export',
'platform:finance:settlement-ar:list',
'platform:finance:settlement:void',
'platform:overseas-business:looseboard:create',
'platform:overseas-business:looseboard:edit',
'platform:overseas-business:looseboard:list',
'platform:overseas-business:looseboard:status',
'platform:overseas-business:purchase:edit',
'platform:overseas-business:purchase:list',
'platform:overseas-business:purchase:review',
'platform:overseas-business:truck:create',
'platform:overseas-business:truck:edit',
'platform:overseas-business:truck:list',
'platform:overseas-business:truck:status',
'platform:overseas-master-data:exchange-rate:create',
'platform:overseas-master-data:exchange-rate:edit',
'platform:overseas-master-data:exchange-rate:export',
'platform:overseas-master-data:exchange-rate:list',
'platform:overseas-master-data:exchange-rate:status',
'platform:overseas-master-data:quote:create',
'platform:overseas-master-data:quote:edit',
'platform:overseas-master-data:quote:export',
'platform:overseas-master-data:quote:list',
'platform:overseas-master-data:quote:status',
'platform:overseas-master-data:vehicle:create',
'platform:overseas-master-data:vehicle:edit',
'platform:overseas-master-data:vehicle:export',
'platform:overseas-master-data:vehicle:list',
'platform:overseas-master-data:vehicle:status',
'platform:overseas-warehouse:carton:create',
'platform:overseas-warehouse:carton:edit',
'platform:overseas-warehouse:carton:list',
'platform:overseas-warehouse:customer-visit:edit',
'platform:overseas-warehouse:customer-visit:list',
'platform:overseas-warehouse:delivery:documents',
'platform:overseas-warehouse:delivery:list',
'platform:overseas-warehouse:delivery:return',
'platform:overseas-warehouse:delivery:status',
'platform:overseas-warehouse:dispatch:cancel',
'platform:overseas-warehouse:dispatch:edit',
'platform:overseas-warehouse:dispatch:list',
'platform:overseas-warehouse:dispatch:transfer-delivery',
'platform:overseas-warehouse:exception-order:create',
'platform:overseas-warehouse:exception-order:list',
'platform:overseas-warehouse:exception-order:process',
'platform:overseas-warehouse:inbound-order:create',
'platform:overseas-warehouse:inbound-order:edit',
'platform:overseas-warehouse:inbound-order:fba-appointment',
'platform:overseas-warehouse:inbound-order:list',
'platform:overseas-warehouse:inbound-order:smart-dispatch',
'platform:overseas-warehouse:inventory:adjust',
'platform:overseas-warehouse:inventory:edit',
'platform:overseas-warehouse:inventory:export',
'platform:overseas-warehouse:inventory:list',
'platform:overseas-warehouse:last-mile-order:edit',
'platform:overseas-warehouse:last-mile-order:export',
'platform:overseas-warehouse:last-mile-order:list',
'platform:overseas-warehouse:location:create',
'platform:overseas-warehouse:location:delete',
'platform:overseas-warehouse:location:edit',
'platform:overseas-warehouse:location:list',
'platform:overseas-warehouse:outbound:cancel',
'platform:overseas-warehouse:outbound:create',
'platform:overseas-warehouse:outbound:list',
'platform:overseas-warehouse:pallet:create',
'platform:overseas-warehouse:pallet:delete',
'platform:overseas-warehouse:pallet:edit',
'platform:overseas-warehouse:pallet:list',
'platform:overseas-warehouse:product:create',
'platform:overseas-warehouse:product:edit',
'platform:overseas-warehouse:product:list',
'platform:overseas-warehouse:relabel-order:create',
'platform:overseas-warehouse:relabel-order:list',
'platform:overseas-warehouse:relabel-order:status',
'platform:overseas-warehouse:warehouse:create',
'platform:overseas-warehouse:warehouse:edit',
'platform:overseas-warehouse:warehouse:list',
'platform:overseas-warehouse:warehouse:status'
);
INSERT IGNORE INTO ll_security_projection (
aggregate_type,aggregate_id,tenant_id,security_status,permission_version,roles_json,authorities_json,aggregate_version,
payload_hash,last_event_id,changed_at,updated_at
) VALUES
('USER','1000001',0,'ACTIVE',0,JSON_ARRAY(),JSON_ARRAY(),0,SHA2('lastleg-baseline:platform_admin:USER:0',256),
'baseline-platform-admin-user-v0','1970-01-01 00:00:00.000','1970-01-01 00:00:00.000'),
('AUTHORIZATION','1000001',0,NULL,0,JSON_ARRAY(),JSON_ARRAY(),0,SHA2('lastleg-baseline:platform_admin:AUTHORIZATION:0',256),
('AUTHORIZATION','1000001',0,NULL,0,@platform_admin_overseas_roles,@platform_admin_overseas_authorities,0,
SHA2('lastleg-baseline:platform_admin:AUTHORIZATION:0:current-authorities',256),
'baseline-platform-admin-authorization-v0','1970-01-01 00:00:00.000','1970-01-01 00:00:00.000');
UPDATE ll_security_projection
SET roles_json=@platform_admin_overseas_roles,
authorities_json=@platform_admin_overseas_authorities,
payload_hash=SHA2('lastleg-baseline:platform_admin:AUTHORIZATION:0:current-authorities',256),
updated_at=CURRENT_TIMESTAMP(3)
WHERE aggregate_type='AUTHORIZATION' AND aggregate_id='1000001' AND tenant_id=0
AND aggregate_version=0 AND last_event_id='baseline-platform-admin-authorization-v0';
CREATE TABLE IF NOT EXISTS ll_wallet_event_inbox (
event_id VARCHAR(64) NOT NULL COMMENT '国内Finance资金结果事件ID',
@@ -2175,106 +2250,3 @@ CREATE TABLE IF NOT EXISTS ll_destuff_pallet_cost_allocation (
PRIMARY KEY(id), UNIQUE KEY uk_ll_destuff_customer(cost_id,customer_id), UNIQUE KEY uk_ll_destuff_fee_source(fee_source_id),
CONSTRAINT chk_ll_destuff_allocation CHECK(actual_volume_cbm>0 AND allocated_amount>=0)
) ENGINE=InnoDB COMMENT='拆柜打托成本按客户实收体积分摊';
-- 客户退货预报、PDA 签收与仓储费起算批次
CREATE TABLE IF NOT EXISTS ll_return_order (
-- need_relabel is added below for existing deployments
id BIGINT NOT NULL COMMENT '退货订单ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户ID和隔离键', return_no VARCHAR(128) NOT NULL COMMENT '退货订单号', overseas_warehouse_id VARCHAR(128) NOT NULL COMMENT '操作海外仓ID', customer_reference_no VARCHAR(128) NULL COMMENT '客户参考号', status VARCHAR(32) NOT NULL COMMENT 'PENDING_RECEIPT、PARTIALLY_RECEIVED或COMPLETED', expected_sku_types INT NOT NULL COMMENT '预报SKU种类数', expected_quantity BIGINT NOT NULL COMMENT '预报总数量', received_quantity BIGINT NOT NULL DEFAULT 0 COMMENT '累计实收总数量', remark VARCHAR(1000) NULL COMMENT '客户备注', idempotency_key VARCHAR(255) NOT NULL COMMENT '创建幂等键', request_hash CHAR(64) NOT NULL COMMENT '创建请求SHA-256', created_by VARCHAR(128) NULL COMMENT '创建操作人ID', created_at DATETIME(3) NOT NULL COMMENT '创建时间', updated_at DATETIME(3) NOT NULL COMMENT '最后更新时间',
PRIMARY KEY(id), UNIQUE KEY uk_ll_return_order_customer_no(customer_id,return_no), UNIQUE KEY uk_ll_return_order_customer_key(customer_id,idempotency_key), KEY idx_ll_return_order_warehouse_status(overseas_warehouse_id,status,created_at), CONSTRAINT chk_ll_return_order_quantity CHECK(expected_sku_types>0 AND expected_quantity>0 AND received_quantity>=0 AND received_quantity<=expected_quantity)
) ENGINE=InnoDB COMMENT='客户退货订单及SKU实收汇总';
SET @ll_return_order_need_relabel = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_order' AND column_name='need_relabel'), 'SELECT 1', 'ALTER TABLE ll_return_order ADD COLUMN need_relabel TINYINT(1) NOT NULL DEFAULT 0 COMMENT ''是否需要换标'' AFTER customer_reference_no')); PREPARE stmt FROM @ll_return_order_need_relabel; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_order' AND column_name='relabel_application_id'), 'SELECT 1', 'ALTER TABLE ll_return_order ADD COLUMN relabel_application_id BIGINT NULL COMMENT ''绑定换标申请单ID'' AFTER need_relabel')); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_order' AND column_name='relabel_no'), 'SELECT 1', 'ALTER TABLE ll_return_order ADD COLUMN relabel_no VARCHAR(64) NULL COMMENT ''绑定换标单号'' AFTER relabel_application_id')); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_order' AND column_name='relabel_status'), 'SELECT 1', 'ALTER TABLE ll_return_order ADD COLUMN relabel_status VARCHAR(32) NULL COMMENT ''换标状态'' AFTER relabel_no')); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
CREATE TABLE IF NOT EXISTS ll_return_order_item (
id BIGINT NOT NULL COMMENT '退货预报明细ID', return_order_id BIGINT NOT NULL COMMENT '退货订单ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户ID和隔离键', sku_code VARCHAR(128) NOT NULL COMMENT 'SKU编码', product_name_zh VARCHAR(255) NULL COMMENT '中文品名', expected_quantity BIGINT NOT NULL COMMENT 'SKU预报数量', received_quantity BIGINT NOT NULL DEFAULT 0 COMMENT 'SKU累计实收数量', created_at DATETIME(3) NOT NULL COMMENT '创建时间', updated_at DATETIME(3) NOT NULL COMMENT '最后更新时间',
PRIMARY KEY(id), UNIQUE KEY uk_ll_return_item_order_sku(return_order_id,sku_code), KEY idx_ll_return_item_sku(sku_code,received_quantity), CONSTRAINT chk_ll_return_item_quantity CHECK(expected_quantity>0 AND received_quantity>=0 AND received_quantity<=expected_quantity)
) ENGINE=InnoDB COMMENT='客户退货订单的SKU预报与实收明细';
CREATE TABLE IF NOT EXISTS ll_return_receipt (
id BIGINT NOT NULL COMMENT 'PDA退货签收单ID', return_order_id BIGINT NOT NULL COMMENT '退货订单ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户ID和隔离键', receipt_no VARCHAR(128) NOT NULL COMMENT '签收单号', receipt_batch_no VARCHAR(128) NOT NULL COMMENT '签收批次号', tracking_no VARCHAR(128) NULL COMMENT '本次签收包裹的可选快递单号', bound_by VARCHAR(16) NOT NULL COMMENT '开始签收方式,RETURN_NO退货单号或SKU候选', status VARCHAR(32) NOT NULL COMMENT '签收状态,当前SIGNED', received_quantity BIGINT NOT NULL COMMENT '本次实收数量', operator_id VARCHAR(128) NOT NULL COMMENT 'PDA操作人ID', operator_name VARCHAR(128) NULL COMMENT 'PDA操作人名称', idempotency_key VARCHAR(255) NOT NULL COMMENT 'PDA确认幂等键', request_hash CHAR(64) NOT NULL COMMENT 'PDA确认请求SHA-256', signed_at DATETIME(3) NOT NULL COMMENT '签收时间', created_at DATETIME(3) NOT NULL COMMENT '创建时间',
PRIMARY KEY(id), UNIQUE KEY uk_ll_return_receipt_no(receipt_no), UNIQUE KEY uk_ll_return_receipt_batch(receipt_batch_no), UNIQUE KEY uk_ll_return_receipt_operator_key(operator_id,idempotency_key), KEY idx_ll_return_receipt_order(return_order_id,signed_at), CONSTRAINT chk_ll_return_receipt_quantity CHECK(received_quantity>0), CONSTRAINT chk_ll_return_receipt_bound CHECK(bound_by IN('RETURN_NO','SKU'))
) ENGINE=InnoDB COMMENT='PDA退货实收签收事实';
-- 已创建开发库升级为“客户只预报 SKU,快递单号仅属于签收批次”的最终模型。
SET @ll_return_item_drop_tracking_order_sku = (SELECT IF(COUNT(*) > 0, 'ALTER TABLE ll_return_order_item DROP INDEX uk_ll_return_item_order_tracking_sku', 'SELECT 1') FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name='ll_return_order_item' AND index_name='uk_ll_return_item_order_tracking_sku');
PREPARE ll_return_item_drop_tracking_order_sku_stmt FROM @ll_return_item_drop_tracking_order_sku;
EXECUTE ll_return_item_drop_tracking_order_sku_stmt;
DEALLOCATE PREPARE ll_return_item_drop_tracking_order_sku_stmt;
SET @ll_return_item_drop_tracking = (SELECT IF(COUNT(*) > 0, 'ALTER TABLE ll_return_order_item DROP INDEX idx_ll_return_item_tracking', 'SELECT 1') FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name='ll_return_order_item' AND index_name='idx_ll_return_item_tracking');
PREPARE ll_return_item_drop_tracking_stmt FROM @ll_return_item_drop_tracking;
EXECUTE ll_return_item_drop_tracking_stmt;
DEALLOCATE PREPARE ll_return_item_drop_tracking_stmt;
SET @ll_return_item_drop_order = (SELECT IF(COUNT(*) > 0, 'ALTER TABLE ll_return_order_item DROP INDEX idx_ll_return_item_order', 'SELECT 1') FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name='ll_return_order_item' AND index_name='idx_ll_return_item_order');
PREPARE ll_return_item_drop_order_stmt FROM @ll_return_item_drop_order;
EXECUTE ll_return_item_drop_order_stmt;
DEALLOCATE PREPARE ll_return_item_drop_order_stmt;
SET @ll_return_item_drop_tracking_no = (SELECT IF(COUNT(*) > 0, 'ALTER TABLE ll_return_order_item DROP COLUMN tracking_no', 'SELECT 1') FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_order_item' AND column_name='tracking_no');
PREPARE ll_return_item_drop_tracking_no_stmt FROM @ll_return_item_drop_tracking_no;
EXECUTE ll_return_item_drop_tracking_no_stmt;
DEALLOCATE PREPARE ll_return_item_drop_tracking_no_stmt;
SET @ll_return_item_add_order_sku = (SELECT IF(COUNT(*) = 0, 'ALTER TABLE ll_return_order_item ADD UNIQUE KEY uk_ll_return_item_order_sku(return_order_id,sku_code)', 'SELECT 1') FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name='ll_return_order_item' AND index_name='uk_ll_return_item_order_sku');
PREPARE ll_return_item_add_order_sku_stmt FROM @ll_return_item_add_order_sku;
EXECUTE ll_return_item_add_order_sku_stmt;
DEALLOCATE PREPARE ll_return_item_add_order_sku_stmt;
SET @ll_return_item_add_sku_idx = (SELECT IF(COUNT(*) = 0, 'ALTER TABLE ll_return_order_item ADD KEY idx_ll_return_item_sku(sku_code,received_quantity)', 'SELECT 1') FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name='ll_return_order_item' AND index_name='idx_ll_return_item_sku');
PREPARE ll_return_item_add_sku_idx_stmt FROM @ll_return_item_add_sku_idx;
EXECUTE ll_return_item_add_sku_idx_stmt;
DEALLOCATE PREPARE ll_return_item_add_sku_idx_stmt;
UPDATE ll_return_receipt SET bound_by='RETURN_NO' WHERE bound_by='TRACKING';
SET @ll_return_receipt_drop_bound_check = (SELECT IF(COUNT(*) > 0, 'ALTER TABLE ll_return_receipt DROP CHECK chk_ll_return_receipt_bound', 'SELECT 1') FROM information_schema.table_constraints WHERE constraint_schema=DATABASE() AND table_name='ll_return_receipt' AND constraint_name='chk_ll_return_receipt_bound');
PREPARE ll_return_receipt_drop_bound_check_stmt FROM @ll_return_receipt_drop_bound_check;
EXECUTE ll_return_receipt_drop_bound_check_stmt;
DEALLOCATE PREPARE ll_return_receipt_drop_bound_check_stmt;
SET @ll_return_receipt_alter = 'ALTER TABLE ll_return_receipt MODIFY COLUMN tracking_no VARCHAR(128) NULL COMMENT ''本次签收包裹的可选快递单号'', MODIFY COLUMN bound_by VARCHAR(16) NOT NULL COMMENT ''开始签收方式,RETURN_NO退货单号或SKU候选'', ADD CONSTRAINT chk_ll_return_receipt_bound CHECK(bound_by IN(''RETURN_NO'',''SKU''))';
PREPARE ll_return_receipt_alter_stmt FROM @ll_return_receipt_alter;
EXECUTE ll_return_receipt_alter_stmt;
DEALLOCATE PREPARE ll_return_receipt_alter_stmt;
CREATE TABLE IF NOT EXISTS ll_return_receipt_item (
id BIGINT NOT NULL COMMENT '签收SKU明细ID', receipt_id BIGINT NOT NULL COMMENT 'PDA退货签收单ID', return_order_item_id BIGINT NOT NULL COMMENT '退货预报明细ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户ID和隔离键', sku_code VARCHAR(128) NOT NULL COMMENT 'SKU编码', received_quantity BIGINT NOT NULL COMMENT '本次SKU实收数量',
PRIMARY KEY(id), KEY idx_ll_return_receipt_item_receipt(receipt_id), KEY idx_ll_return_receipt_item_order_item(return_order_item_id), CONSTRAINT chk_ll_return_receipt_item_quantity CHECK(received_quantity>0)
) ENGINE=InnoDB COMMENT='PDA退货签收SKU实际数量明细';
CREATE TABLE IF NOT EXISTS ll_return_receipt_batch (
id BIGINT NOT NULL COMMENT '退货仓储计费批次ID', return_order_id BIGINT NOT NULL COMMENT '退货订单ID', receipt_id BIGINT NOT NULL COMMENT 'PDA退货签收单ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户ID和隔离键', batch_no VARCHAR(128) NOT NULL COMMENT '签收批次号', overseas_warehouse_id VARCHAR(128) NOT NULL COMMENT '操作海外仓ID', storage_billing_status VARCHAR(32) NOT NULL COMMENT '仓储计费状态,初始PENDING_PRICING', storage_started_at DATETIME(3) NOT NULL COMMENT '仓储计费起算时间', created_at DATETIME(3) NOT NULL COMMENT '创建时间',
PRIMARY KEY(id), UNIQUE KEY uk_ll_return_batch_no(batch_no), UNIQUE KEY uk_ll_return_batch_receipt(receipt_id), KEY idx_ll_return_batch_billing(customer_id,overseas_warehouse_id,storage_billing_status,storage_started_at)
) ENGINE=InnoDB COMMENT='每次退货签收生成的仓储费起算批次';
-- 退货库存:签收即生成箱号,库存按客户、海外仓和SKU汇总;箱内明细保留给合箱与箱唛。
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_receipt' AND column_name='carton_mark'), 'SELECT 1', 'ALTER TABLE ll_return_receipt ADD COLUMN carton_mark VARCHAR(128) NULL COMMENT ''签收生成或绑定的退货箱号'' AFTER receipt_batch_no'));
PREPARE return_receipt_carton_mark_stmt FROM @ddl; EXECUTE return_receipt_carton_mark_stmt; DEALLOCATE PREPARE return_receipt_carton_mark_stmt;
CREATE TABLE IF NOT EXISTS ll_return_inventory_carton (
id BIGINT NOT NULL COMMENT '退货库存箱ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户ID和隔离键', overseas_warehouse_id VARCHAR(128) NOT NULL COMMENT '海外仓ID', source_receipt_id BIGINT NULL COMMENT '来源PDA签收单ID', source_batch_no VARCHAR(128) NULL COMMENT '来源签收批次号', carton_mark VARCHAR(128) NOT NULL COMMENT '退货箱号和箱唛编码', status VARCHAR(32) NOT NULL COMMENT 'AVAILABLE可用或MERGED已合箱', sku_types INT NOT NULL COMMENT '箱内SKU种类数', total_quantity BIGINT NOT NULL COMMENT '箱内总件数', length DECIMAL(12,3) NULL COMMENT '箱长CM', width DECIMAL(12,3) NULL COMMENT '箱宽CM', height DECIMAL(12,3) NULL COMMENT '箱高CM', weight DECIMAL(12,3) NULL COMMENT '箱重KG', created_at DATETIME(3) NOT NULL COMMENT '创建时间', updated_at DATETIME(3) NOT NULL COMMENT '最后更新时间', merged_at DATETIME(3) NULL COMMENT '合箱完成时间',
PRIMARY KEY(id), UNIQUE KEY uk_ll_return_inventory_carton_mark(carton_mark), UNIQUE KEY uk_ll_return_inventory_carton_receipt(source_receipt_id), KEY idx_ll_return_inventory_carton_customer(customer_id,overseas_warehouse_id,status,created_at), CONSTRAINT chk_ll_return_inventory_carton_quantity CHECK((status IN('AVAILABLE','OUTBOUNDED') AND sku_types>0 AND total_quantity>0) OR (status='MERGED' AND sku_types=0 AND total_quantity=0)), CONSTRAINT chk_ll_return_inventory_carton_status CHECK(status IN('AVAILABLE','MERGED','OUTBOUNDED'))
) ENGINE=InnoDB COMMENT='退货实物库存箱及箱唛';
-- 已合箱的来源箱保留审计记录,但其箱内库存必须归零;动态 SQL 兼容 Spring 的数据库初始化器。
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND constraint_name='chk_ll_return_inventory_carton_quantity'), 'ALTER TABLE ll_return_inventory_carton DROP CHECK chk_ll_return_inventory_carton_quantity', 'SELECT 1'));
PREPARE return_inventory_carton_check_drop_stmt FROM @ddl; EXECUTE return_inventory_carton_check_drop_stmt; DEALLOCATE PREPARE return_inventory_carton_check_drop_stmt;
ALTER TABLE ll_return_inventory_carton ADD CONSTRAINT chk_ll_return_inventory_carton_quantity CHECK((status IN('AVAILABLE','OUTBOUNDED') AND sku_types>0 AND total_quantity>0) OR (status='MERGED' AND sku_types=0 AND total_quantity=0));
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND constraint_name='chk_ll_return_inventory_carton_status'), 'ALTER TABLE ll_return_inventory_carton DROP CHECK chk_ll_return_inventory_carton_status', 'SELECT 1'));
PREPARE return_inventory_carton_status_drop_stmt FROM @ddl; EXECUTE return_inventory_carton_status_drop_stmt; DEALLOCATE PREPARE return_inventory_carton_status_drop_stmt;
ALTER TABLE ll_return_inventory_carton ADD CONSTRAINT chk_ll_return_inventory_carton_status CHECK(status IN('AVAILABLE','MERGED','OUTBOUNDED'));
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND column_name='source_return_no'), 'SELECT 1', 'ALTER TABLE ll_return_inventory_carton ADD COLUMN source_return_no VARCHAR(2000) NULL COMMENT ''来源退货单号'' AFTER source_batch_no'));
PREPARE return_inventory_carton_return_no_stmt FROM @ddl; EXECUTE return_inventory_carton_return_no_stmt; DEALLOCATE PREPARE return_inventory_carton_return_no_stmt;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND column_name='fba_carton_mark'), 'SELECT 1', 'ALTER TABLE ll_return_inventory_carton ADD COLUMN fba_carton_mark VARCHAR(255) NULL COMMENT ''FBA箱唛'' AFTER carton_mark')); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND column_name='destination_warehouse_code'), 'SELECT 1', 'ALTER TABLE ll_return_inventory_carton ADD COLUMN destination_warehouse_code VARCHAR(128) NULL COMMENT ''目的仓编码'' AFTER fba_carton_mark')); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND column_name='destination_warehouse_name'), 'SELECT 1', 'ALTER TABLE ll_return_inventory_carton ADD COLUMN destination_warehouse_name VARCHAR(255) NULL COMMENT ''目的仓名称'' AFTER destination_warehouse_code')); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND column_name='relabel_application_id'), 'SELECT 1', 'ALTER TABLE ll_return_inventory_carton ADD COLUMN relabel_application_id BIGINT NULL COMMENT ''绑定换标申请单ID'' AFTER destination_warehouse_name')); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND column_name='relabel_no'), 'SELECT 1', 'ALTER TABLE ll_return_inventory_carton ADD COLUMN relabel_no VARCHAR(64) NULL COMMENT ''绑定换标单号'' AFTER relabel_application_id')); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND column_name='relabel_status'), 'SELECT 1', 'ALTER TABLE ll_return_inventory_carton ADD COLUMN relabel_status VARCHAR(32) NULL COMMENT ''换标状态'' AFTER relabel_no')); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND column_name='relabel_completed_at'), 'SELECT 1', 'ALTER TABLE ll_return_inventory_carton ADD COLUMN relabel_completed_at DATETIME(3) NULL COMMENT ''换标完成时间'' AFTER relabel_status')); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
CREATE TABLE IF NOT EXISTS ll_return_inventory_carton_item (
id BIGINT NOT NULL COMMENT '退货库存箱SKU明细ID', carton_id BIGINT NOT NULL COMMENT '退货库存箱ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户ID和隔离键', sku_code VARCHAR(128) NOT NULL COMMENT 'SKU编码', product_name_zh VARCHAR(255) NULL COMMENT '中文品名', quantity BIGINT NOT NULL COMMENT '该SKU箱内数量',
PRIMARY KEY(id), UNIQUE KEY uk_ll_return_inventory_carton_item(carton_id,sku_code), KEY idx_ll_return_inventory_carton_item_customer(customer_id,sku_code), CONSTRAINT chk_ll_return_inventory_carton_item_quantity CHECK(quantity>0)
) ENGINE=InnoDB COMMENT='退货库存箱内SKU和数量';
CREATE TABLE IF NOT EXISTS ll_return_inventory_balance (
id BIGINT NOT NULL COMMENT '退货库存余额ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户ID和隔离键', overseas_warehouse_id VARCHAR(128) NOT NULL COMMENT '海外仓ID', sku_code VARCHAR(128) NOT NULL COMMENT 'SKU编码', product_name_zh VARCHAR(255) NULL COMMENT '中文品名', total_quantity BIGINT NOT NULL DEFAULT 0 COMMENT '实物总库存,仅实际出库扣减', available_quantity BIGINT NOT NULL COMMENT '未合箱可用上架数量', carton_quantity BIGINT NOT NULL COMMENT '包含该SKU的可用箱数', created_at DATETIME(3) NOT NULL COMMENT '创建时间', updated_at DATETIME(3) NOT NULL COMMENT '最后更新时间',
PRIMARY KEY(id), UNIQUE KEY uk_ll_return_inventory_balance(customer_id,overseas_warehouse_id,sku_code), CONSTRAINT chk_ll_return_inventory_balance_quantity CHECK(total_quantity>=available_quantity AND available_quantity>=0 AND carton_quantity>=0)
) ENGINE=InnoDB COMMENT='退货库存按客户海外仓SKU汇总余额';
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_balance' AND column_name='total_quantity'), 'SELECT 1', 'ALTER TABLE ll_return_inventory_balance ADD COLUMN total_quantity BIGINT NULL COMMENT ''实物总库存,仅实际出库扣减'' AFTER product_name_zh'));
PREPARE return_inventory_balance_total_stmt FROM @ddl; EXECUTE return_inventory_balance_total_stmt; DEALLOCATE PREPARE return_inventory_balance_total_stmt;
-- 旧数据的可用量原先包含已合箱库存;升级时一次性拆分为总库存和未合箱可用库存。
UPDATE ll_return_inventory_balance b LEFT JOIN (SELECT c.customer_id,c.overseas_warehouse_id,i.sku_code,SUM(i.quantity) merged_quantity FROM ll_return_inventory_carton c JOIN ll_return_inventory_carton_item i ON i.carton_id=c.id WHERE c.source_batch_no='MERGED' GROUP BY c.customer_id,c.overseas_warehouse_id,i.sku_code) m ON m.customer_id=b.customer_id AND m.overseas_warehouse_id=b.overseas_warehouse_id AND m.sku_code=b.sku_code SET b.total_quantity=b.available_quantity,b.available_quantity=GREATEST(0,b.available_quantity-COALESCE(m.merged_quantity,0)) WHERE b.total_quantity IS NULL;
ALTER TABLE ll_return_inventory_balance MODIFY COLUMN total_quantity BIGINT NOT NULL DEFAULT 0 COMMENT '实物总库存,仅实际出库扣减';
CREATE TABLE IF NOT EXISTS ll_return_inventory_operation (
id BIGINT NOT NULL COMMENT '退货库存PDA操作ID', operator_id VARCHAR(128) NOT NULL COMMENT 'PDA操作人ID', idempotency_key VARCHAR(255) NOT NULL COMMENT '操作幂等键', request_hash CHAR(64) NOT NULL COMMENT '请求内容SHA-256', operation_type VARCHAR(32) NOT NULL COMMENT '操作类型,当前MERGE', result_carton_id BIGINT NULL COMMENT '合箱结果退货库存箱ID', created_at DATETIME(3) NOT NULL COMMENT '创建时间', updated_at DATETIME(3) NOT NULL COMMENT '最后更新时间',
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合箱幂等操作审计';
@@ -27,11 +27,21 @@ class LastlegSecurityContractTest {
"UPDATE ll_security_projection SET roles_json=JSON_ARRAY() WHERE roles_json IS NULL",
"UPDATE ll_security_projection SET authorities_json=JSON_ARRAY() WHERE authorities_json IS NULL");
for (String sql : new String[]{baseline, migration}) {
assertThat(sql).contains("('USER','1000001',0,'ACTIVE',0,JSON_ARRAY(),JSON_ARRAY(),0",
"('AUTHORIZATION','1000001',0,NULL,0,JSON_ARRAY(),JSON_ARRAY(),0",
assertThat(sql).contains("SET @platform_admin_overseas_roles = JSON_ARRAY('PLATFORM_ADMIN')",
"SET @platform_admin_overseas_authorities = JSON_ARRAY(",
"('USER','1000001',0,'ACTIVE',0,JSON_ARRAY(),JSON_ARRAY(),0",
"('AUTHORIZATION','1000001',0,NULL,0,@platform_admin_overseas_roles,@platform_admin_overseas_authorities,0",
"baseline-platform-admin-user-v0",
"baseline-platform-admin-authorization-v0")
.doesNotContain("baseline-merchant-admin");
"baseline-platform-admin-authorization-v0",
"platform:overseas-warehouse:last-mile-order:list",
"platform:overseas-warehouse:inbound-order:list",
"platform:overseas-warehouse:dispatch:list",
"platform:overseas-business:purchase:list",
"platform:overseas-master-data:quote:list",
"platform:finance:fee-source:review",
"AND aggregate_version=0 AND last_event_id='baseline-platform-admin-authorization-v0'")
.doesNotContain("('AUTHORIZATION','1000001',0,NULL,0,JSON_ARRAY(),JSON_ARRAY(),0",
"baseline-merchant-admin");
}
assertThat(authSeed).contains("(1000001, 'platform_admin'", "default_tenant_id",
"default_app_code");
+4 -3
View File
@@ -237,18 +237,19 @@ Dry-run 只检查根 `pom.xml` 和关键回归测试文件并打印 Maven 命令
### 本地 MySQL 全量验收
正式入口是 `local-db-full-acceptance.ps1`,只允许连接本机 `127.0.0.1` 或 `localhost`,默认端口 `3306`;测试库 `8.163.30.128` 会立即以退出码 2 拒绝。脚本固定校验统一 SQL SHA-256 `00E8F9C24CE16FE6B4D2B6AF60460AE1AF1D264DFE110D4BDCE611F09EB69EAA` 和 15403 行,并静态确认中文注释升级采用校验入队、每表一次 ALTER、结构签名复核、九库覆盖断言及唯一最终 `PASS`。
正式入口是 `local-db-full-acceptance.ps1`,只允许连接本机 `127.0.0.1` 或 `localhost`,默认端口 `3306`;任何非本机地址都会立即以退出码 2 拒绝。非 dry-run 默认连续执行两轮,每轮精确删除 12 个 `baiyue_*` 国内/海外业务库,再只执行 `deploy/sql/头程数据库统一增量-MySQL8.sql` 和 `deploy/sql/海外区域统一增量-MySQL8.sql`,最后核对两个文件的最终 `PASS` 与 12 库完整性。
```powershell
.\scripts\local-db-full-acceptance.ps1 -DryRun
.\scripts\local-db-full-acceptance.ps1
.\scripts\local-db-full-acceptance.ps1 -Rounds 1
```
Dry-run 只校验 SQL 输入并打印空库、复跑、两类 picking 旧结构、WAIT_SPLIT/IAM、海外仓旧结构、中断恢复和最终结构的执行顺序,不启动 MySQL、不写库。真实运行每阶段写入 `.codex-tmp/p23-04-acceptance/acceptance-run-*.log`,不使用 `--force`,任一 SQL 错误立即停止并透传退出码,最后执行 `verify-final.sql`。脚本不保存或传入数据库密码,使用本机默认认证环境。
Dry-run 只校验两个统一 SQL 的入口标记,打印当前 SHA-256、行数、清理范围和执行顺序,不连接 MySQL。真实运行会先把中文文件名按原始字节暂存为英文文件名,规避 Windows MySQL 客户端的 `source` 路径问题;每轮日志写入 `.codex-tmp/local-db-full-acceptance/round-*/`。脚本不保存数据库密码,认证由本机登录配置或当前进程的 `MYSQL_PWD` 等外部方式提供。成功后保留第二轮完整初始化结果;失败时可能留下半初始化库,下次运行会先清理精确范围再重建。
仓库中 `phase2-test-env-acceptance.ps1`、`admin-test-env-acceptance.mjs`、`client-test-env-acceptance.mjs`、`export-center-test-env-acceptance.ps1` 等属于测试环境/API 验收脚本;它们可能读取或写入测试环境数据,不由本入口调用。`migrate-local-qualification-file.ps1`、`local-env.ps1` 等是环境/迁移辅助脚本,也不由本入口调用。
本入口不读取数据库凭据、不执行 MySQL、HTTP、迁移、DDL、INSERT/UPDATE/DELETE,也不启动 Nacos、Gateway 或业务服务。需要数据库矩阵时必须单独运行受控验收流程。
本入口只操作明确指定的本机 MySQL,不访问开发或测试 HTTP 环境,不启动 Nacos、Gateway 或业务服务,也不会清理系统库或非 `baiyue_*` 数据库。
## 退出码
@@ -3,6 +3,13 @@ import { readFileSync } from 'node:fs'
import test from 'node:test'
const sql = readFileSync(new URL('../../deploy/sql/头程数据库统一增量-MySQL8.sql', import.meta.url), 'utf8')
const overseasSql = readFileSync(new URL('../../deploy/sql/海外区域统一增量-MySQL8.sql', import.meta.url), 'utf8')
const lastlegBaseline = readFileSync(new URL('../../lastleg-service/src/main/resources/db/lastleg.sql', import.meta.url), 'utf8')
const lastlegPermissionSeed = readFileSync(new URL('../../iam/src/main/resources/db/lastleg-permissions.sql', import.meta.url), 'utf8')
const phase5DeliveryPermissionSeed = readFileSync(new URL('../../iam/src/main/resources/db/phase5-delivery-permissions.sql', import.meta.url), 'utf8')
const overseasWarehousePermissions = readFileSync(new URL('../../../apps/admin-web/src/modules/overseas-warehouse/permissions.ts', import.meta.url), 'utf8')
const overseasBusinessPermissions = readFileSync(new URL('../../../apps/admin-web/src/modules/overseas-business/permissions.ts', import.meta.url), 'utf8')
const overseasMasterDataPermissions = readFileSync(new URL('../../../apps/admin-web/src/modules/overseas-master-data/permissions.ts', import.meta.url), 'utf8')
const iamSeed = readFileSync(new URL('../../iam/src/main/resources/db/phase1-admin-rbac.sql', import.meta.url), 'utf8')
const oceanBookingSchema = readFileSync(new URL('../../firstleg/src/main/resources/db/phase3-ocean-booking-schema.sql', import.meta.url), 'utf8')
const truckAppointmentSchema = readFileSync(new URL('../../firstleg/src/main/resources/db/phase3-truck-appointment-schema.sql', import.meta.url), 'utf8')
@@ -50,6 +57,81 @@ test('国内统一初始化支持真实空库且不越界创建海外业务库',
)
})
test('国内统一初始化在注释门禁前创建订单申报快照表', () => {
for (const table of ['fl_order_declaration_snapshot', 'fl_order_declaration_event']) {
const createIndex = sql.indexOf(`CREATE TABLE IF NOT EXISTS ${table} (`)
const commentIndex = sql.indexOf(`CALL queue_table_comment('baiyue_firstleg', '${table}'`)
assert.ok(createIndex >= 0, `missing domestic baseline table: ${table}`)
assert.ok(commentIndex >= 0, `missing domestic table comment gate: ${table}`)
assert.ok(createIndex < commentIndex, `${table} must exist before the table-comment gate runs`)
}
})
test('海外统一初始化内联空库基线且不依赖模块 SQL', () => {
assert.match(overseasSql, /BEGIN INLINE EMPTY-DATABASE BASELINE: LASTLEG/)
assert.match(overseasSql, /BEGIN INLINE EMPTY-DATABASE BASELINE: OVERSEAS WAREHOUSE/)
assert.match(overseasSql, /CREATE DATABASE IF NOT EXISTS baiyue_lastleg/)
assert.match(overseasSql, /CREATE DATABASE IF NOT EXISTS baiyue_lastlag_warehouse/)
assert.match(overseasSql, /CREATE TABLE IF NOT EXISTS ll_order \(/)
assert.match(overseasSql, /CREATE TABLE IF NOT EXISTS wh_location \(/)
assert.ok(
overseasSql.indexOf('CREATE TABLE IF NOT EXISTS ll_order (') <
overseasSql.indexOf('CREATE TABLE IF NOT EXISTS ll_order_source_pallet ('),
'll_order must exist before the source-pallet foreign key is created',
)
assert.ok(
overseasSql.indexOf('CREATE DATABASE IF NOT EXISTS baiyue_lastlag_warehouse') <
overseasSql.indexOf('CREATE TABLE IF NOT EXISTS wh_location ('),
'the overseas warehouse schema must be selected before its baseline tables are created',
)
})
test('海外空库基线为固定平台管理员提供完整version0授权投影', () => {
const authorityBlock = lastlegBaseline.match(
/SET @platform_admin_overseas_authorities = JSON_ARRAY\(([\s\S]*?)\n\);/,
)?.[1]
assert.ok(authorityBlock, 'missing platform_admin overseas authority snapshot')
const authorities = [...authorityBlock.matchAll(/'([^']+)'/g)].map((match) => match[1])
assert.equal(authorities.length, 109)
assert.equal(new Set(authorities).size, authorities.length, 'baseline authorities must not contain duplicates')
assert.equal(authorities.filter((item) => item.startsWith('platform:finance:')).length, 34)
assert.equal(authorities.filter((item) => item.startsWith('platform:overseas-business:')).length, 11)
assert.equal(authorities.filter((item) => item.startsWith('platform:overseas-master-data:')).length, 15)
assert.equal(authorities.filter((item) => item.startsWith('platform:overseas-warehouse:')).length, 49)
for (const authority of authorities) {
assert.ok(sql.includes(`'${authority}'`), `authority is absent from the domestic IAM baseline: ${authority}`)
}
for (const source of [overseasWarehousePermissions, overseasBusinessPermissions, overseasMasterDataPermissions]) {
const declared = [...source.matchAll(/'((?:platform:overseas-warehouse|platform:overseas-business|platform:overseas-master-data):[^']+)'/g)]
.map((match) => match[1])
for (const authority of declared) {
assert.ok(
[lastlegPermissionSeed, phase5DeliveryPermissionSeed].some((seed) => seed.includes(`'${authority}'`)),
`Admin permission is absent from the IAM source seed: ${authority}`,
)
assert.ok(authorities.includes(authority), `Admin permission is absent from the fixed-admin overseas snapshot: ${authority}`)
}
}
for (const required of [
'platform:overseas-warehouse:last-mile-order:list',
'platform:overseas-warehouse:inbound-order:list',
'platform:overseas-warehouse:dispatch:list',
'platform:overseas-business:purchase:list',
'platform:overseas-master-data:quote:list',
'platform:finance:fee-source:review',
]) assert.ok(authorities.includes(required), `missing fixed-admin overseas authority: ${required}`)
assert.match(lastlegBaseline, /SET @platform_admin_overseas_roles = JSON_ARRAY\('PLATFORM_ADMIN'\)/)
assert.doesNotMatch(lastlegBaseline, /\('AUTHORIZATION','1000001',0,NULL,0,JSON_ARRAY\(\),JSON_ARRAY\(\)/)
assert.doesNotMatch(overseasSql, /\('AUTHORIZATION','1000001',0,NULL,0,JSON_ARRAY\(\),JSON_ARRAY\(\)/)
const inline = overseasSql.match(
/-- BEGIN INLINE EMPTY-DATABASE BASELINE: LASTLEG\r?\n([\s\S]*?)\r?\n\r?\n-- END INLINE EMPTY-DATABASE BASELINE: LASTLEG/,
)?.[1]
assert.ok(inline, 'missing inline Lastleg baseline')
assert.equal(inline.replace(/\r\n/g, '\n').trim(), lastlegBaseline.replace(/\r\n/g, '\n').trim())
})
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/)
+98 -62
View File
@@ -2,92 +2,128 @@ param(
[switch]$DryRun,
[Alias('Host')][string]$TargetHost = '127.0.0.1',
[int]$Port = 3306,
[string]$InputPath,
[string]$MysqlExe = 'C:\Program Files\MySQL\MySQL Server 8.4\bin\mysql.exe',
[string]$User = 'root'
[string]$User = 'root',
[ValidateRange(1, 10)][int]$Rounds = 2
)
$ErrorActionPreference = 'Stop'
$Repo = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
$Workspace = (Resolve-Path (Join-Path $Repo '..')).Path
$Acceptance = Join-Path $Workspace '.codex-tmp\p23-04-acceptance'
$ExpectedSha256 = '0EE13427AB60CECCEB42985CF1E78A05645C18CD9BF2886110C0AFEAF3CA7B9B'
$ExpectedLines = 15674
$ReportRoot = Join-Path $Workspace '.codex-tmp\local-db-full-acceptance'
$DomesticSql = Join-Path $Repo 'deploy\sql\头程数据库统一增量-MySQL8.sql'
$OverseasSql = Join-Path $Repo 'deploy\sql\海外区域统一增量-MySQL8.sql'
$Schemas = @(
'baiyue_auth', 'baiyue_iam', 'baiyue_order', 'baiyue_warehouse',
'baiyue_firstleg', 'baiyue_file', 'baiyue_audit', 'baiyue_event_bridge',
'baiyue_finance', 'baiyue_lastleg', 'baiyue_lastlag_warehouse',
'baiyue_event_bridge_overseas'
)
if ($TargetHost -notin @('127.0.0.1', 'localhost')) {
[Console]::Error.WriteLine('Refusing non-local database; only 127.0.0.1 or localhost is allowed.')
exit 2
}
if ([string]::IsNullOrWhiteSpace($InputPath)) {
$InputPath = (Get-ChildItem (Join-Path $Repo 'deploy\sql') -Filter '*MySQL8.sql' -File |
Where-Object { $_.Length -gt 100000 } | Select-Object -First 1).FullName
}
if (-not (Test-Path -LiteralPath $InputPath)) {
[Console]::Error.WriteLine("InputPath does not exist: $InputPath")
if (-not (Test-Path -LiteralPath $MysqlExe)) {
[Console]::Error.WriteLine("mysql executable does not exist: $MysqlExe")
exit 2
}
$actual = ((Get-FileHash -Algorithm SHA256 -LiteralPath $InputPath).Hash).Trim().ToUpperInvariant()
$lines = [System.IO.File]::ReadAllLines($InputPath).Length
if ($actual -ne $ExpectedSha256 -or $lines -ne $ExpectedLines) {
[Console]::Error.WriteLine("Unified SQL snapshot mismatch: $actual/$lines")
exit 2
}
$sqlText = [System.IO.File]::ReadAllText($InputPath)
$commentGateTokens = @(
'CREATE PROCEDURE queue_column_comment',
'CREATE PROCEDURE apply_one_table_comments',
'CALL apply_queued_comments();',
'字段注释批量升级改变了结构签名',
'存在缺失中文注释的业务表',
'存在缺失中文注释的业务字段'
)
foreach ($token in $commentGateTokens) {
if (-not $sqlText.Contains($token)) {
[Console]::Error.WriteLine("Unified SQL comment acceptance marker missing: $token")
foreach ($sqlFile in @($DomesticSql, $OverseasSql)) {
if (-not (Test-Path -LiteralPath $sqlFile)) {
[Console]::Error.WriteLine("Unified SQL does not exist: $sqlFile")
exit 2
}
}
if ([regex]::Matches($sqlText, "SELECT 'PASS' AS migration_status").Count -ne 1) {
[Console]::Error.WriteLine('Unified SQL must contain exactly one final PASS statement')
$DomesticText = [IO.File]::ReadAllText($DomesticSql)
$OverseasText = [IO.File]::ReadAllText($OverseasSql)
if (-not $DomesticText.Contains("SELECT 'PASS' AS migration_status")) {
[Console]::Error.WriteLine('Domestic unified SQL is missing its final PASS statement.')
exit 2
}
foreach ($marker in @(
'-- BEGIN INLINE EMPTY-DATABASE BASELINE: LASTLEG',
'-- BEGIN INLINE EMPTY-DATABASE BASELINE: OVERSEAS WAREHOUSE',
"SELECT 'PASS' AS migration_status"
)) {
if (-not $OverseasText.Contains($marker)) {
[Console]::Error.WriteLine("Overseas unified SQL is missing marker: $marker")
exit 2
}
}
$sequence = @(
'empty: base.sql', 'replay: base.sql + input.sql + input.sql',
'smart-old: fixture-smart-old.sql', 'inbound-old: fixture-inbound-old.sql',
'waitsplit-iam: fixture-test-shape.sql', 'overseas-old: fixture-overseas-old.sql',
'overseas-missing-objects: fixture-overseas-missing-objects.sql',
'interrupted-recovery: fixture-interrupted.sql', 'final-structure: verify-final.sql')
Write-Output "SQL SHA-256: $actual / lines: $lines"
Write-Output 'Comment gate: queued validation, per-table ALTER, signature verification, nine-schema coverage, unique PASS'
$DomesticHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $DomesticSql).Hash
$OverseasHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $OverseasSql).Hash
Write-Output "Target: $TargetHost`:$Port"
Write-Output 'Fixture order:'
$sequence | ForEach-Object { Write-Output " $_" }
if ($DryRun) { Write-Output 'DRY-RUN PASS (mysql not started; no database writes)'; exit 0 }
Write-Output "Rounds: $Rounds"
Write-Output "Domestic SQL: $DomesticHash / $([IO.File]::ReadAllLines($DomesticSql).Length) lines"
Write-Output "Overseas SQL: $OverseasHash / $([IO.File]::ReadAllLines($OverseasSql).Length) lines"
Write-Output "Reset scope: $($Schemas -join ', ')"
Write-Output 'Execution order per round: clear exact schemas -> domestic unified SQL -> overseas unified SQL -> final PASS and schema checks'
if ($DryRun) {
Write-Output 'DRY-RUN PASS (mysql not contacted; no database writes)'
exit 0
}
$acceptanceInput = Join-Path $Acceptance 'input.sql'
Copy-Item -LiteralPath $InputPath -Destination $acceptanceInput -Force
$stagedHash = ((Get-FileHash -Algorithm SHA256 -LiteralPath $acceptanceInput).Hash).Trim().ToUpperInvariant()
if ($stagedHash -ne $actual) {
[Console]::Error.WriteLine("Failed to stage the validated unified SQL snapshot: $stagedHash")
New-Item -ItemType Directory -Path $ReportRoot -Force | Out-Null
$StagedDomestic = Join-Path $ReportRoot 'domestic.sql'
$StagedOverseas = Join-Path $ReportRoot 'overseas.sql'
Copy-Item -LiteralPath $DomesticSql -Destination $StagedDomestic -Force
Copy-Item -LiteralPath $OverseasSql -Destination $StagedOverseas -Force
if ((Get-FileHash -Algorithm SHA256 -LiteralPath $StagedDomestic).Hash -ne $DomesticHash -or
(Get-FileHash -Algorithm SHA256 -LiteralPath $StagedOverseas).Hash -ne $OverseasHash) {
[Console]::Error.WriteLine('Failed to stage byte-identical SQL files under ASCII filenames.')
exit 2
}
$run = 0
function Invoke-Stage([string]$Name, [string]$File) {
$script = Join-Path $Acceptance $File
$log = Join-Path $Acceptance ("acceptance-run-{0:D2}-{1}.log" -f $script:run, $Name)
$script:run++
if (-not (Test-Path -LiteralPath $script)) { [Console]::Error.WriteLine("Script does not exist: $script"); return 2 }
& $MysqlExe '--protocol=TCP' "--host=$TargetHost" "--port=$Port" "--user=$User" '--database=mysql' '--batch' ("--execute=source " + $script.Replace('\','/')) *> $log
return $LASTEXITCODE
$MysqlCommon = @(
'--protocol=TCP', "--host=$TargetHost", "--port=$Port", "--user=$User",
'--default-character-set=utf8mb4', '--batch'
)
& $MysqlExe @MysqlCommon '--skip-column-names' '--execute=SELECT VERSION(), @@hostname, @@port;' | Out-Null
if ($LASTEXITCODE -ne 0) {
[Console]::Error.WriteLine('Cannot connect to the local MySQL target. Configure authentication outside this script, for example with MYSQL_PWD for the current process.')
exit $LASTEXITCODE
}
$calls = @(
@('empty','base.sql'), @('replay-reset','base.sql'), @('replay-1','input.sql'), @('replay-2','input.sql'),
@('smart-old','fixture-smart-old.sql'), @('inbound-old','fixture-inbound-old.sql'), @('waitsplit-iam','fixture-test-shape.sql'),
@('overseas-old','fixture-overseas-old.sql'), @('overseas-missing','fixture-overseas-missing-objects.sql'),
@('interrupted','fixture-interrupted.sql'), @('final-verify','verify-final.sql'))
foreach ($call in $calls) { $code = Invoke-Stage $call[0] $call[1]; if ($code -ne 0) { exit $code } }
Write-Output 'FULL LOCAL ACCEPTANCE PASS'
$DropSql = ($Schemas | ForEach-Object { "DROP DATABASE IF EXISTS ``$_```;" }) -join ' '
for ($round = 1; $round -le $Rounds; $round++) {
$RoundRoot = Join-Path $ReportRoot ("round-{0}" -f $round)
New-Item -ItemType Directory -Path $RoundRoot -Force | Out-Null
$Stopwatch = [Diagnostics.Stopwatch]::StartNew()
& $MysqlExe @MysqlCommon "--execute=$DropSql" *> (Join-Path $RoundRoot '00-clear.log')
if ($LASTEXITCODE -ne 0) {
[Console]::Error.WriteLine("Round $round failed while clearing the exact project schemas.")
exit $LASTEXITCODE
}
$DomesticLog = Join-Path $RoundRoot '10-domestic.log'
$DomesticSource = $StagedDomestic.Replace('\', '/')
& $MysqlExe @MysqlCommon '--database=mysql' "--execute=source $DomesticSource" *> $DomesticLog
if ($LASTEXITCODE -ne 0 -or -not (Select-String -LiteralPath $DomesticLog -Pattern '^PASS\s+统一增量全部执行完成' -Quiet)) {
[Console]::Error.WriteLine("Round $round domestic unified SQL failed. See $DomesticLog")
exit $(if ($LASTEXITCODE -ne 0) { $LASTEXITCODE } else { 2 })
}
$OverseasLog = Join-Path $RoundRoot '20-overseas.log'
$OverseasSource = $StagedOverseas.Replace('\', '/')
& $MysqlExe @MysqlCommon '--database=mysql' "--execute=source $OverseasSource" *> $OverseasLog
if ($LASTEXITCODE -ne 0 -or -not (Select-String -LiteralPath $OverseasLog -Pattern '^PASS\s+海外财务P0费用来源字段已就绪$' -Quiet)) {
[Console]::Error.WriteLine("Round $round overseas unified SQL failed. See $OverseasLog")
exit $(if ($LASTEXITCODE -ne 0) { $LASTEXITCODE } else { 2 })
}
$SchemaList = "'" + ($Schemas -join "','") + "'"
$SchemaCount = (& $MysqlExe @MysqlCommon '--skip-column-names' "--execute=SELECT COUNT(*) FROM information_schema.schemata WHERE schema_name IN ($SchemaList);").Trim()
if ($LASTEXITCODE -ne 0 -or [int]$SchemaCount -ne $Schemas.Count) {
[Console]::Error.WriteLine("Round $round schema verification failed: $SchemaCount/$($Schemas.Count)")
exit 2
}
$Stopwatch.Stop()
Write-Output "ROUND $round PASS / $SchemaCount schemas / $([Math]::Round($Stopwatch.Elapsed.TotalSeconds, 1)) seconds"
}
Write-Output 'FULL LOCAL TWO-REGION DATABASE ACCEPTANCE PASS'
exit 0