diff --git a/AGENTS.md b/AGENTS.md index ce05de7fc..0e0fb4794 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,7 +10,7 @@ - 当前产品只有“平台—客户”两级。`tenant_id=0` 是平台技术作用域;`tenant_id>0` 是客户隔离键,不得新增租户切换、租户选择或租户级 RBAC。 - 公共仓库、往来单位、快递、托盘、库位统一归属 `tenant_id=0`;订单、文件、费用等客户数据按客户 `tenant_id>0` 隔离。不得跨库 Join 或信任客户端客户 ID。 -- 订单交易状态归 `order`,履约/入库状态归 `firstleg`,仓储资源归 `warehouse`,文件对象归 `file`,OCR 适配归 `integration-service`;通过内部客户端、DTO 和服务令牌协作。 +- 订单交易状态归 `order`,履约/入库状态归 `firstleg`,财务账期、账单、收款、成本与核销归 `finance`,仓储资源归 `warehouse`,文件对象归 `file`,OCR 适配归 `integration-service`;通过内部客户端、DTO 和服务令牌协作。 - 普通业务成功/失败使用 HTTP 200 + body `code`(成功为字符串 `"200"`);认证、基础设施和网关拒绝可使用真实 401/403/503。 ## 服务路由(按关键词定位) @@ -23,6 +23,7 @@ | 履约分配、入库、测量、审核、FBA 计费 | `firstleg/` | `firstleg/AGENTS.md` | | 分柜、柜次、智能排柜、预装/装柜清单、PDA | `firstleg/` | `firstleg/AGENTS.md`,再按 `split/`、`loading/`、`outbound/` 定位 | | 订舱、船期、拖车、报关 | `firstleg/` | `oceanbooking/`、`portschedule/`、`outbound/dispatch/`、`customs/` | +| 客户账期、费用池、账单、收款、成本、核销 | `finance/` | `finance/AGENTS.md`;费用来源和柜次成本快照再联查 `firstleg/AGENTS.md`,凭证再联查 `file/AGENTS.md` | | 仓库、往来单位、快递、托盘、库位 | `warehouse/` | `warehouse/AGENTS.md` | | 资质、导出、测量图片、文件下载 | `file/` | `file/AGENTS.md` | | 路由、CORS、JWT 入口、访问域 | `gateway/` | `gateway/AGENTS.md` | @@ -34,6 +35,7 @@ - 一期:`auth`、`iam`、`warehouse`、`order`、`firstleg`、`file` 完成登录、客户/RBAC、公共仓、订单闭环;先读 `../docs/一期/README.md` 及闭环设计。 - 二期:`firstleg` 主导入库/测量/审核/计费,`warehouse` 管托盘库位,`file` 管测量图片,`integration-service` 管 OCR,`order` 仅提供货物内部查询;终点为 `WAIT_SPLIT + APPROVED + CONFIRMED`,不实现分柜、装柜、PDA 和完整财务。 - 三期:已确认范围可按 `../docs/三期/三期开工门禁.md` 进入契约和模块实现。`firstleg` 负责履约编排、柜次、排柜、订舱、拖车、PDA 与 `LOADED` 门禁;`order`、`warehouse`、`file`、`iam`、`gateway` 和 `integration-service` 仅在各自契约需要时协作。未冻结的字段、权限、迁移或跨服务调用只阻断对应模块。 +- 财务:作为跨阶段独立业务域,范围先读 `../docs/财务管理/README.md` 中已确认的产品结论与契约;`finance` 保存财务状态,`firstleg` 只提供不可变费用来源和柜次成本快照,`file` 保存收付款/成本凭证。不要把财务范围反向并入二期计费或三期履约。 ## 契约、数据库与验证入口 @@ -47,5 +49,6 @@ - 一期:`../docs/一期/README.md`、`../docs/一期/一期头程订单闭环设计.md` - 二期:`../docs/二期/README.md`、`../docs/二期/二期需求优化版-v2-测量计费入库主流程.md`、需求确认清单及联调报告 - 三期:`../docs/三期/README.md`、`../docs/三期/三期开工门禁.md`、`../docs/三期/三期开发规划.md`、`../docs/三期/三期需求确认表.md`、页面与原型清单 +- 财务:`../docs/财务管理/README.md` 中已确认的范围、对象、状态和接口约束 进度、排期、负责人或验收状态问题再读取阶段进度表和验收报告;普通开发定位只使用范围、契约、模块指引和当前代码。 diff --git a/audit-service/src/main/resources/db/audit.sql b/audit-service/src/main/resources/db/audit.sql index efa939fa2..533d1ed5c 100644 --- a/audit-service/src/main/resources/db/audit.sql +++ b/audit-service/src/main/resources/db/audit.sql @@ -11,8 +11,8 @@ CREATE TABLE IF NOT EXISTS sys_audit_log ( description VARCHAR(255) DEFAULT NULL COMMENT '操作说明', request_id VARCHAR(128) DEFAULT NULL COMMENT '请求ID,用于单次请求追踪', trace_id VARCHAR(128) DEFAULT NULL COMMENT '分布式链路追踪ID', - tenant_id BIGINT DEFAULT NULL COMMENT '业务数据所属租户ID', - operator_tenant_id BIGINT DEFAULT NULL COMMENT '操作主体所属租户ID', + tenant_id BIGINT DEFAULT NULL COMMENT '客户ID和数据隔离键;平台级审计可为空或为0', + operator_tenant_id BIGINT DEFAULT NULL COMMENT '操作主体所属客户ID;平台操作可为空或为0', user_id BIGINT DEFAULT NULL COMMENT '操作用户ID', username VARCHAR(128) DEFAULT NULL COMMENT '操作用户名快照', app_code VARCHAR(64) DEFAULT NULL COMMENT '操作来源应用端', @@ -38,17 +38,17 @@ CREATE TABLE IF NOT EXISTS sys_audit_log ( ) ENGINE=InnoDB COMMENT='统一业务审计日志表:追加记录敏感操作的主体、租户、资源、结果及前后快照;不同于消息Outbox和消费幂等表'; CREATE TABLE IF NOT EXISTS sys_audit_dead_letter ( - id BIGINT NOT NULL AUTO_INCREMENT, + id BIGINT NOT NULL AUTO_INCREMENT COMMENT '死信记录自增ID', event_id VARCHAR(64) NOT NULL COMMENT '审计事件全局唯一ID', payload_text LONGTEXT NOT NULL COMMENT 'RocketMQ死信原始载荷', - status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING、PROCESSING、PROCESSED、DEAD', - retry_count INT NOT NULL DEFAULT 0, - next_retry_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - lease_owner VARCHAR(128) NULL, - lease_until DATETIME(3) NULL, - last_error VARCHAR(1000) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - processed_at DATETIME(3) NULL, + status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT '状态:PENDING待补偿、PROCESSING处理中、PROCESSED已处理、DEAD永久失败', + retry_count INT NOT NULL DEFAULT 0 COMMENT '死信重试次数', + next_retry_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '下次允许补偿时间', + lease_owner VARCHAR(128) NULL COMMENT '当前补偿租约持有实例', + lease_until DATETIME(3) NULL COMMENT '当前补偿租约截止时间', + last_error VARCHAR(1000) NULL COMMENT '最近一次补偿错误', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '死信记录创建时间', + processed_at DATETIME(3) NULL COMMENT '补偿处理完成时间', PRIMARY KEY (id), UNIQUE KEY uk_audit_dead_event (event_id), KEY idx_audit_dead_retry (status, next_retry_at, lease_until) diff --git a/audit-service/src/main/resources/db/reliable-audit-migration.sql b/audit-service/src/main/resources/db/reliable-audit-migration.sql index eab91d8e5..0889751a8 100644 --- a/audit-service/src/main/resources/db/reliable-audit-migration.sql +++ b/audit-service/src/main/resources/db/reliable-audit-migration.sql @@ -22,17 +22,17 @@ CALL add_audit_event_id(); DROP PROCEDURE IF EXISTS add_audit_event_id; CREATE TABLE IF NOT EXISTS sys_audit_dead_letter ( - id BIGINT NOT NULL AUTO_INCREMENT, + id BIGINT NOT NULL AUTO_INCREMENT COMMENT '死信记录自增ID', event_id VARCHAR(64) NOT NULL COMMENT '审计事件全局唯一ID', payload_text LONGTEXT NOT NULL COMMENT 'RocketMQ死信原始载荷', - status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING、PROCESSING、PROCESSED、DEAD', - retry_count INT NOT NULL DEFAULT 0, - next_retry_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - lease_owner VARCHAR(128) NULL, - lease_until DATETIME(3) NULL, - last_error VARCHAR(1000) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - processed_at DATETIME(3) NULL, + status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT '状态:PENDING待补偿、PROCESSING处理中、PROCESSED已处理、DEAD永久失败', + retry_count INT NOT NULL DEFAULT 0 COMMENT '死信重试次数', + next_retry_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '下次允许补偿时间', + lease_owner VARCHAR(128) NULL COMMENT '当前补偿租约持有实例', + lease_until DATETIME(3) NULL COMMENT '当前补偿租约截止时间', + last_error VARCHAR(1000) NULL COMMENT '最近一次补偿错误', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '死信记录创建时间', + processed_at DATETIME(3) NULL COMMENT '补偿处理完成时间', PRIMARY KEY (id), UNIQUE KEY uk_audit_dead_event (event_id), KEY idx_audit_dead_retry (status, next_retry_at, lease_until) diff --git a/auth/src/main/resources/db/audit-outbox.sql b/auth/src/main/resources/db/audit-outbox.sql index fc7fe485f..580e2ce7a 100644 --- a/auth/src/main/resources/db/audit-outbox.sql +++ b/auth/src/main/resources/db/audit-outbox.sql @@ -8,17 +8,17 @@ CREATE TABLE IF NOT EXISTS sys_audit_outbox ( resource_id VARCHAR(128) NULL COMMENT '资源ID', service_name VARCHAR(64) NULL COMMENT '来源服务', payload_json JSON NOT NULL COMMENT '脱敏后的审计事件', - status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING、SENDING、SENT、DEAD', + status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT '发送状态:PENDING待发送、SENDING发送中、SENT已发送、DEAD进入死信', attempt_count INT NOT NULL DEFAULT 0 COMMENT '当前周期发送次数', compensation_count INT NOT NULL DEFAULT 0 COMMENT '自动补偿次数', - next_retry_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - lease_owner VARCHAR(128) NULL, - lease_until DATETIME(3) NULL, - last_error VARCHAR(1000) NULL, - dead_at DATETIME(3) NULL, - sent_at DATETIME(3) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + next_retry_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '下一次重试时间', + lease_owner VARCHAR(128) NULL COMMENT '当前租约持有者', + lease_until DATETIME(3) NULL COMMENT '发送租约截止时间', + last_error VARCHAR(1000) NULL COMMENT '最近一次发送错误信息', + dead_at DATETIME(3) NULL COMMENT '进入死信时间', + sent_at DATETIME(3) NULL COMMENT '成功发送时间', + 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 (event_id), KEY idx_audit_outbox_publish (status, next_retry_at, lease_until, created_at), KEY idx_audit_outbox_retention (status, sent_at) diff --git a/auth/src/main/resources/db/auth.sql b/auth/src/main/resources/db/auth.sql index 408de4f3f..a1ee7ad64 100644 --- a/auth/src/main/resources/db/auth.sql +++ b/auth/src/main/resources/db/auth.sql @@ -8,15 +8,15 @@ CREATE TABLE IF NOT EXISTS auth_account ( username VARCHAR(64) NOT NULL COMMENT '全局唯一登录名', password_hash VARCHAR(255) NOT NULL COMMENT 'BCrypt密码哈希,禁止保存明文密码', display_name VARCHAR(64) NOT NULL COMMENT '登录后展示名称', - account_status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '账号状态:ACTIVE、DISABLED、LOCKED', + account_status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '账号状态:ACTIVE正常、DISABLED停用、LOCKED锁定', failed_login_count INT NOT NULL DEFAULT 0 COMMENT '连续登录失败次数', locked_until DATETIME(3) DEFAULT NULL COMMENT '临时锁定截止时间', token_version INT NOT NULL DEFAULT 1 COMMENT '令牌版本,递增后可使历史令牌失效', password_change_required TINYINT NOT NULL DEFAULT 0 COMMENT '管理员重置后是否强制下次登录改密', - default_tenant_id BIGINT NOT NULL COMMENT '默认业务租户ID', - operator_tenant_id BIGINT NOT NULL COMMENT '操作主体租户ID,平台端与商户端身份隔离', - default_membership_id BIGINT DEFAULT NULL COMMENT '默认租户成员关系ID', - default_app_code VARCHAR(20) NOT NULL COMMENT '默认应用端:MERCHANT或PLATFORM', + default_tenant_id BIGINT NOT NULL COMMENT '默认业务作用域ID:0为平台,正数为客户ID', + operator_tenant_id BIGINT NOT NULL COMMENT '操作主体作用域ID:0为平台,正数为客户ID,用于平台端与客户端身份隔离', + default_membership_id BIGINT DEFAULT NULL COMMENT '默认客户成员关系ID;平台账号可为空', + default_app_code VARCHAR(20) NOT NULL COMMENT '默认应用端:MERCHANT企业客户端、PLATFORM平台后台', deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除标记', last_login_at DATETIME(3) DEFAULT NULL COMMENT '最近一次成功登录时间', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', @@ -30,9 +30,9 @@ CREATE TABLE IF NOT EXISTS auth_refresh_token ( id BIGINT NOT NULL COMMENT '刷新令牌记录ID(雪花ID)', token_hash CHAR(64) NOT NULL COMMENT '刷新令牌SHA-256摘要,不保存令牌明文', user_id BIGINT NOT NULL COMMENT '认证账号ID', - tenant_id BIGINT NOT NULL COMMENT '令牌当前业务租户ID', - operator_tenant_id BIGINT NOT NULL COMMENT '令牌操作主体租户ID', - app_code VARCHAR(20) NOT NULL COMMENT '令牌所属应用端', + tenant_id BIGINT NOT NULL COMMENT '令牌当前业务作用域ID:0为平台,正数为客户ID', + operator_tenant_id BIGINT NOT NULL COMMENT '令牌操作主体作用域ID:0为平台,正数为客户ID', + app_code VARCHAR(20) NOT NULL COMMENT '令牌所属应用端:MERCHANT企业客户端、PLATFORM平台后台', expires_at DATETIME(3) NOT NULL COMMENT '到期时间', revoked_at DATETIME(3) DEFAULT NULL COMMENT '吊销时间,非空表示不可继续使用', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '签发时间', @@ -40,6 +40,6 @@ CREATE TABLE IF NOT EXISTS auth_refresh_token ( UNIQUE KEY uk_auth_refresh_hash (token_hash), KEY idx_auth_refresh_user (user_id, revoked_at, expires_at), KEY idx_auth_refresh_expire (expires_at, revoked_at) -) ENGINE=InnoDB COMMENT='刷新令牌表:保存令牌摘要、租户上下文、有效期和轮换/吊销状态'; +) ENGINE=InnoDB COMMENT='刷新令牌表:保存令牌摘要、平台或客户作用域、有效期和轮换/吊销状态'; -- 初始管理员密码必须由部署脚本生成 BCrypt 哈希后显式写入,仓库内禁止提供默认密码。 diff --git a/deploy/sql/头程数据库统一增量-MySQL8.sql b/deploy/sql/头程数据库统一增量-MySQL8.sql index c039540ff..c1785078d 100644 --- a/deploy/sql/头程数据库统一增量-MySQL8.sql +++ b/deploy/sql/头程数据库统一增量-MySQL8.sql @@ -91,8 +91,8 @@ CREATE TABLE IF NOT EXISTS sys_audit_log ( description VARCHAR(255) DEFAULT NULL COMMENT '操作说明', request_id VARCHAR(128) DEFAULT NULL COMMENT '请求ID,用于单次请求追踪', trace_id VARCHAR(128) DEFAULT NULL COMMENT '分布式链路追踪ID', - tenant_id BIGINT DEFAULT NULL COMMENT '业务数据所属租户ID', - operator_tenant_id BIGINT DEFAULT NULL COMMENT '操作主体所属租户ID', + tenant_id BIGINT DEFAULT NULL COMMENT '客户ID和数据隔离键;平台级审计可为空或为0', + operator_tenant_id BIGINT DEFAULT NULL COMMENT '操作主体所属客户ID;平台操作可为空或为0', user_id BIGINT DEFAULT NULL COMMENT '操作用户ID', username VARCHAR(128) DEFAULT NULL COMMENT '操作用户名快照', app_code VARCHAR(64) DEFAULT NULL COMMENT '操作来源应用端', @@ -129,7 +129,7 @@ CREATE TABLE IF NOT EXISTS bridge_outbox ( event_version INT NOT NULL COMMENT '事件契约版本', source_region VARCHAR(32) NOT NULL COMMENT '来源区域', target_region VARCHAR(32) NOT NULL COMMENT '目标区域', - tenant_id VARCHAR(128) NOT NULL COMMENT '租户标识', + tenant_id VARCHAR(128) NOT NULL COMMENT '客户或业务隔离标识;跨区域事件沿用来源系统口径', aggregate_type VARCHAR(64) NOT NULL COMMENT '聚合类型', aggregate_id VARCHAR(128) NOT NULL COMMENT '聚合标识', aggregate_version BIGINT NOT NULL COMMENT '聚合版本', @@ -160,7 +160,7 @@ CREATE TABLE IF NOT EXISTS bridge_inbox ( event_version INT NOT NULL COMMENT '事件契约版本', source_region VARCHAR(32) NOT NULL COMMENT '来源区域', target_region VARCHAR(32) NOT NULL COMMENT '目标区域', - tenant_id VARCHAR(128) NOT NULL COMMENT '租户标识', + tenant_id VARCHAR(128) NOT NULL COMMENT '客户或业务隔离标识;跨区域事件沿用来源系统口径', aggregate_type VARCHAR(64) NOT NULL COMMENT '聚合类型', aggregate_id VARCHAR(128) NOT NULL COMMENT '聚合标识', aggregate_version BIGINT NOT NULL COMMENT '聚合版本', @@ -213,15 +213,15 @@ CREATE TABLE IF NOT EXISTS auth_account ( username VARCHAR(64) NOT NULL COMMENT '全局唯一登录名', password_hash VARCHAR(255) NOT NULL COMMENT 'BCrypt密码哈希,禁止保存明文密码', display_name VARCHAR(64) NOT NULL COMMENT '登录后展示名称', - account_status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '账号状态:ACTIVE、DISABLED、LOCKED', + account_status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '账号状态:ACTIVE正常、DISABLED停用、LOCKED锁定', failed_login_count INT NOT NULL DEFAULT 0 COMMENT '连续登录失败次数', locked_until DATETIME(3) DEFAULT NULL COMMENT '临时锁定截止时间', token_version INT NOT NULL DEFAULT 1 COMMENT '令牌版本,递增后可使历史令牌失效', password_change_required TINYINT NOT NULL DEFAULT 0 COMMENT '管理员重置后是否强制下次登录改密', - default_tenant_id BIGINT NOT NULL COMMENT '默认业务租户ID', - operator_tenant_id BIGINT NOT NULL COMMENT '操作主体租户ID,平台端与商户端身份隔离', - default_membership_id BIGINT DEFAULT NULL COMMENT '默认租户成员关系ID', - default_app_code VARCHAR(20) NOT NULL COMMENT '默认应用端:MERCHANT或PLATFORM', + default_tenant_id BIGINT NOT NULL COMMENT '默认业务作用域ID:0为平台,正数为客户ID', + operator_tenant_id BIGINT NOT NULL COMMENT '操作主体作用域ID:0为平台,正数为客户ID,用于平台端与客户端身份隔离', + default_membership_id BIGINT DEFAULT NULL COMMENT '默认客户成员关系ID;平台账号可为空', + default_app_code VARCHAR(20) NOT NULL COMMENT '默认应用端:MERCHANT企业客户端、PLATFORM平台后台', deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除标记', last_login_at DATETIME(3) DEFAULT NULL COMMENT '最近一次成功登录时间', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', @@ -247,9 +247,9 @@ CREATE TABLE IF NOT EXISTS auth_refresh_token ( id BIGINT NOT NULL COMMENT '刷新令牌记录ID(雪花ID)', token_hash CHAR(64) NOT NULL COMMENT '刷新令牌SHA-256摘要,不保存令牌明文', user_id BIGINT NOT NULL COMMENT '认证账号ID', - tenant_id BIGINT NOT NULL COMMENT '令牌当前业务租户ID', - operator_tenant_id BIGINT NOT NULL COMMENT '令牌操作主体租户ID', - app_code VARCHAR(20) NOT NULL COMMENT '令牌所属应用端', + tenant_id BIGINT NOT NULL COMMENT '令牌当前业务作用域ID:0为平台,正数为客户ID', + operator_tenant_id BIGINT NOT NULL COMMENT '令牌操作主体作用域ID:0为平台,正数为客户ID', + app_code VARCHAR(20) NOT NULL COMMENT '令牌所属应用端:MERCHANT企业客户端、PLATFORM平台后台', expires_at DATETIME(3) NOT NULL COMMENT '到期时间', revoked_at DATETIME(3) DEFAULT NULL COMMENT '吊销时间,非空表示不可继续使用', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '签发时间', @@ -257,7 +257,7 @@ CREATE TABLE IF NOT EXISTS auth_refresh_token ( UNIQUE KEY uk_auth_refresh_hash (token_hash), KEY idx_auth_refresh_user (user_id, revoked_at, expires_at), KEY idx_auth_refresh_expire (expires_at, revoked_at) -) ENGINE=InnoDB COMMENT='刷新令牌表:保存令牌摘要、租户上下文、有效期和轮换/吊销状态'; +) ENGINE=InnoDB COMMENT='刷新令牌表:保存令牌摘要、平台或客户作用域、有效期和轮换/吊销状态'; -- 初始管理员密码必须由部署脚本生成 BCrypt 哈希后显式写入,仓库内禁止提供默认密码。 @@ -272,11 +272,11 @@ CREATE DATABASE IF NOT EXISTS baiyue_iam USE baiyue_iam; CREATE TABLE IF NOT EXISTS iam_tenant ( - id BIGINT NOT NULL COMMENT '租户ID(雪花ID)', - tenant_code VARCHAR(64) NOT NULL COMMENT '全局唯一租户编码', - tenant_name VARCHAR(128) NOT NULL COMMENT '租户名称', - tenant_type VARCHAR(20) NOT NULL DEFAULT 'MERCHANT' COMMENT '租户类型', - status VARCHAR(20) NOT NULL DEFAULT 'PROFILE_REQUIRED' COMMENT '租户状态:PROFILE_REQUIRED、PENDING、ACTIVE、REJECTED、DISABLED', + id BIGINT NOT NULL COMMENT '客户主档ID(雪花ID);0保留为平台技术作用域', + tenant_code VARCHAR(64) NOT NULL COMMENT '全局唯一客户编码', + tenant_name VARCHAR(128) NOT NULL COMMENT '客户企业名称', + tenant_type VARCHAR(20) NOT NULL DEFAULT 'MERCHANT' COMMENT '主体类型:MERCHANT企业客户', + status VARCHAR(20) NOT NULL DEFAULT 'PROFILE_REQUIRED' COMMENT '客户状态:PROFILE_REQUIRED待完善资料、PENDING待审核、ACTIVE正常、REJECTED已驳回、DISABLED已停用', legal_name VARCHAR(128) NULL COMMENT '企业法定名称', legal_representative VARCHAR(64) NULL COMMENT '法定代表人', unified_social_credit_code VARCHAR(32) NULL COMMENT '统一社会信用代码', @@ -293,9 +293,9 @@ CREATE TABLE IF NOT EXISTS iam_tenant ( registered_capital_currency CHAR(3) NULL COMMENT '注册资本币种 ISO 4217', registered_address VARCHAR(500) NULL COMMENT '注册地址文本', internal_remark VARCHAR(1000) NULL COMMENT '平台内部备注', - approved_at DATETIME(3) NULL COMMENT '审核通过时间', - approved_by BIGINT NULL COMMENT '审核人ID', - reject_reason VARCHAR(500) NULL COMMENT '驳回原因', + approved_at DATETIME(3) NULL COMMENT '客户资料审核通过时间', + approved_by BIGINT NULL COMMENT '客户资料审核人ID', + reject_reason VARCHAR(500) NULL COMMENT '客户资料审核驳回原因', created_by BIGINT NULL COMMENT '创建人ID', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', updated_by BIGINT NULL COMMENT '最后修改人ID', @@ -306,13 +306,13 @@ CREATE TABLE IF NOT EXISTS iam_tenant ( UNIQUE KEY uk_iam_tenant_code (tenant_code), UNIQUE KEY uk_iam_tenant_credit (unified_social_credit_code), KEY idx_iam_tenant_status (status, created_at) -) ENGINE=InnoDB COMMENT='企业租户主表:作为SaaS数据隔离边界,保存商户企业资料、审核结果和启停状态'; +) ENGINE=InnoDB COMMENT='客户企业主档表:保存企业资料、审核结果和启停状态;ID同时作为客户数据隔离键'; CREATE TABLE IF NOT EXISTS iam_tenant_review ( id BIGINT NOT NULL COMMENT '审核记录ID', - tenant_id BIGINT NOT NULL COMMENT '被审核租户ID', - review_type VARCHAR(20) NOT NULL DEFAULT 'REGISTER' COMMENT '审核类型', - status VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT '审核状态', + tenant_id BIGINT NOT NULL COMMENT '被审核客户ID和数据隔离键', + review_type VARCHAR(20) NOT NULL DEFAULT 'REGISTER' COMMENT '审核类型:REGISTER注册审核等', + status VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT '审核状态:PENDING待审核、APPROVED已通过、REJECTED已驳回', reviewer_id BIGINT NULL COMMENT '审核人ID', review_comment VARCHAR(500) NULL COMMENT '审核意见', reviewed_at DATETIME(3) NULL COMMENT '审核完成时间', @@ -320,51 +320,51 @@ CREATE TABLE IF NOT EXISTS iam_tenant_review ( PRIMARY KEY (id), KEY idx_review_queue (status, created_at), KEY idx_review_tenant (tenant_id) -) ENGINE=InnoDB COMMENT='租户审核记录表:保存企业注册、资料变更等审核过程和审核意见'; +) ENGINE=InnoDB COMMENT='客户审核记录表:保存企业注册、资料变更等审核过程和审核意见'; CREATE TABLE IF NOT EXISTS iam_tenant_address ( - id BIGINT NOT NULL COMMENT '租户地址ID', - tenant_id BIGINT NOT NULL COMMENT '所属租户ID', - address_type VARCHAR(20) NOT NULL COMMENT '地址类型', + id BIGINT NOT NULL COMMENT '客户地址ID', + tenant_id BIGINT NOT NULL COMMENT '客户ID和数据隔离键', + address_type VARCHAR(20) NOT NULL COMMENT '地址类型:注册地址、办公地址等', country_code CHAR(2) NOT NULL DEFAULT 'CN' COMMENT '国家二字码', province VARCHAR(64) NULL COMMENT '省/州', city VARCHAR(64) NULL COMMENT '城市', district VARCHAR(64) NULL COMMENT '区县', detail VARCHAR(255) NOT NULL COMMENT '详细地址', postal_code VARCHAR(16) NULL COMMENT '邮政编码', - is_default TINYINT NOT NULL DEFAULT 0 COMMENT '是否默认地址', + is_default TINYINT NOT NULL DEFAULT 0 COMMENT '是否默认地址:0否、1是', 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 '更新时间', deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除标记', PRIMARY KEY (id), KEY idx_address_tenant (tenant_id, address_type) -) ENGINE=InnoDB COMMENT='租户地址表:保存企业注册地址、办公地址等租户级资料'; +) ENGINE=InnoDB COMMENT='客户地址表:保存企业注册地址、办公地址等客户资料'; CREATE TABLE IF NOT EXISTS iam_tenant_qualification ( id BIGINT NOT NULL COMMENT '资质记录ID', - tenant_id BIGINT NOT NULL COMMENT '所属租户ID', - qualification_type VARCHAR(32) NOT NULL COMMENT '资质类型', + tenant_id BIGINT NOT NULL COMMENT '客户ID和数据隔离键', + qualification_type VARCHAR(32) NOT NULL COMMENT '客户资质类型', certificate_no VARCHAR(128) NULL COMMENT '证书编号', document_url VARCHAR(500) NULL COMMENT '资质文件存储地址', valid_from DATE NULL COMMENT '有效期开始日期', valid_to DATE NULL COMMENT '有效期结束日期', - status VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT '资质审核状态', - review_comment VARCHAR(500) NULL COMMENT '审核意见', + status VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT '资质审核状态:PENDING待审核、APPROVED已通过、REJECTED已驳回', + review_comment VARCHAR(500) NULL COMMENT '客户资质审核意见', 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 '更新时间', deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除标记', PRIMARY KEY (id), KEY idx_qualification_tenant (tenant_id, status) -) ENGINE=InnoDB COMMENT='租户资质表:保存企业证照、有效期、文件地址和审核状态'; +) ENGINE=InnoDB COMMENT='客户资质表:保存企业证照、有效期、文件地址和审核状态'; CREATE TABLE IF NOT EXISTS iam_user ( id BIGINT NOT NULL COMMENT 'IAM用户资料ID', auth_user_id BIGINT NOT NULL COMMENT '认证服务账号逻辑ID', - user_type VARCHAR(20) NOT NULL DEFAULT 'ENTERPRISE' COMMENT '用户类型', + user_type VARCHAR(20) NOT NULL DEFAULT 'ENTERPRISE' COMMENT '用户类型:ENTERPRISE企业用户、PLATFORM平台用户', display_name VARCHAR(64) NOT NULL COMMENT '用户展示名称', phone VARCHAR(32) NULL COMMENT '手机号', email VARCHAR(128) NULL COMMENT '邮箱', - status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '用户资料状态', + status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '用户资料状态:ACTIVE正常、DISABLED停用', 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 '更新时间', deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除标记', @@ -375,14 +375,14 @@ CREATE TABLE IF NOT EXISTS iam_user ( ) ENGINE=InnoDB COMMENT='IAM用户资料表:连接统一认证账号与业务身份资料,不保存密码'; CREATE TABLE IF NOT EXISTS iam_membership ( - id BIGINT NOT NULL COMMENT '租户成员关系ID', - tenant_id BIGINT NOT NULL COMMENT '所属租户ID', + id BIGINT NOT NULL COMMENT '客户成员关系ID', + tenant_id BIGINT NOT NULL COMMENT '客户ID和数据隔离键', user_id BIGINT NOT NULL COMMENT 'IAM用户资料ID', - member_type VARCHAR(20) NOT NULL DEFAULT 'EMPLOYEE' COMMENT '成员类型', - member_no VARCHAR(64) NULL COMMENT '租户内部成员编号', - status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '成员状态', - joined_at DATETIME(3) NULL COMMENT '加入租户时间', - left_at DATETIME(3) NULL COMMENT '离开租户时间', + member_type VARCHAR(20) NOT NULL DEFAULT 'EMPLOYEE' COMMENT '成员类型:EMPLOYEE企业员工等', + member_no VARCHAR(64) NULL COMMENT '客户企业内部成员编号', + status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '成员状态:ACTIVE在职、DISABLED停用、LEFT已离开', + joined_at DATETIME(3) NULL COMMENT '加入客户企业时间', + left_at DATETIME(3) NULL COMMENT '离开客户企业时间', 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 '更新时间', deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除标记', @@ -392,16 +392,16 @@ CREATE TABLE IF NOT EXISTS iam_membership ( UNIQUE KEY uk_membership_tenant_no (tenant_id, member_no), KEY idx_membership_user (user_id, status), KEY idx_membership_tenant (tenant_id, status) -) ENGINE=InnoDB COMMENT='租户成员关系表:表示一个用户在某个企业租户中的成员身份,是分配商户端角色的主体'; +) ENGINE=InnoDB COMMENT='客户成员关系表:表示用户在客户企业中的成员身份,是分配企业客户端角色的主体'; CREATE TABLE IF NOT EXISTS iam_role ( id BIGINT NOT NULL COMMENT '角色ID', - tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '角色所属租户;0表示平台预置角色', - app_code VARCHAR(20) NOT NULL COMMENT '应用端:MERCHANT或PLATFORM', + tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '角色作用域:0为平台预置模板,正数为客户ID和数据隔离键', + app_code VARCHAR(20) NOT NULL COMMENT '应用端:MERCHANT企业客户端、PLATFORM平台后台', role_code VARCHAR(64) NOT NULL COMMENT '作用域内唯一角色编码', role_name VARCHAR(128) NOT NULL COMMENT '角色名称', role_type VARCHAR(20) NOT NULL DEFAULT 'CUSTOM' COMMENT '角色类型:SYSTEM或CUSTOM', - status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '角色状态', + status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '角色状态:ACTIVE启用、DISABLED停用', description VARCHAR(255) NULL COMMENT '角色用途说明', created_by BIGINT NULL COMMENT '创建人ID', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', @@ -412,21 +412,21 @@ CREATE TABLE IF NOT EXISTS iam_role ( PRIMARY KEY (id), UNIQUE KEY uk_role_scope_code (tenant_id, app_code, role_code), KEY idx_role_tenant (tenant_id, app_code, status) -) ENGINE=InnoDB COMMENT='角色表:按租户和应用端隔离商户端、平台后台两套角色体系'; +) ENGINE=InnoDB COMMENT='角色表:按客户或平台作用域及应用端隔离企业客户端、平台后台两套角色体系'; CREATE TABLE IF NOT EXISTS iam_membership_role ( - membership_id BIGINT NOT NULL COMMENT '租户成员关系ID', + membership_id BIGINT NOT NULL COMMENT '客户成员关系ID', role_id BIGINT NOT NULL COMMENT '角色ID', - tenant_id BIGINT NOT NULL COMMENT '冗余租户ID,用于租户隔离校验和查询', + tenant_id BIGINT NOT NULL COMMENT '冗余客户ID和数据隔离键,用于授权归属校验和查询', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '授权时间', PRIMARY KEY (membership_id, role_id), KEY idx_mr_tenant (tenant_id) -) ENGINE=InnoDB COMMENT='成员角色关联表:将租户成员分配到对应应用端的一个或多个角色'; +) ENGINE=InnoDB COMMENT='成员角色关联表:将客户成员分配到对应应用端的一个或多个角色'; CREATE TABLE IF NOT EXISTS iam_permission ( id BIGINT NOT NULL COMMENT '权限资源ID', - tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '权限所属租户;0表示平台预置权限', - app_code VARCHAR(20) NOT NULL COMMENT '所属应用端:MERCHANT或PLATFORM', + tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '权限作用域:0为平台预置权限,正数为客户ID和数据隔离键', + app_code VARCHAR(20) NOT NULL COMMENT '所属应用端:MERCHANT企业客户端、PLATFORM平台后台', permission_type VARCHAR(20) NOT NULL COMMENT '权限类型:MENU、PAGE、BUTTON或API', parent_id BIGINT NOT NULL DEFAULT 0 COMMENT '父权限ID,用于构造菜单和页面树', permission_code VARCHAR(128) NOT NULL COMMENT '鉴权使用的唯一权限码', @@ -434,8 +434,8 @@ CREATE TABLE IF NOT EXISTS iam_permission ( route_path VARCHAR(255) NULL COMMENT '前端路由或API路径', http_method VARCHAR(16) NULL COMMENT 'API权限对应的HTTP方法', sort_no INT NOT NULL DEFAULT 0 COMMENT '同级显示顺序', - status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '权限状态', - built_in TINYINT NOT NULL DEFAULT 0 COMMENT '是否为系统预置权限;预置权限不可通过管理接口修改', + status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '权限状态:ACTIVE启用、DISABLED停用', + built_in TINYINT NOT NULL DEFAULT 0 COMMENT '是否为系统预置权限;预置资源不可通过管理接口修改', 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 '更新时间', deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除标记', @@ -448,7 +448,7 @@ CREATE TABLE IF NOT EXISTS iam_permission ( CREATE TABLE IF NOT EXISTS iam_role_permission ( role_id BIGINT NOT NULL COMMENT '角色ID', permission_id BIGINT NOT NULL COMMENT '权限资源ID', - tenant_id BIGINT NOT NULL COMMENT '冗余租户ID,用于隔离校验和查询', + tenant_id BIGINT NOT NULL COMMENT '冗余客户ID或平台作用域ID,用于授权归属校验和查询', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '授权时间', PRIMARY KEY (role_id, permission_id), KEY idx_rp_tenant (tenant_id) @@ -457,13 +457,13 @@ CREATE TABLE IF NOT EXISTS iam_role_permission ( CREATE TABLE IF NOT EXISTS iam_data_scope ( id BIGINT NOT NULL COMMENT '数据范围规则ID', role_id BIGINT NOT NULL COMMENT '角色ID', - tenant_id BIGINT NOT NULL COMMENT '规则所属租户ID', - scope_type VARCHAR(20) NOT NULL DEFAULT 'SELF' COMMENT '范围类型:SELF、DEPARTMENT、TENANT或CUSTOM', + tenant_id BIGINT NOT NULL COMMENT '规则作用域:0为平台,正数为客户ID和数据隔离键', + scope_type VARCHAR(20) NOT NULL DEFAULT 'SELF' COMMENT '数据范围类型:SELF本人、DEPARTMENT部门、TENANT当前客户、CUSTOM自定义', scope_expression JSON NULL COMMENT '自定义数据范围表达式', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', PRIMARY KEY (id), UNIQUE KEY uk_scope_role (role_id) -) ENGINE=InnoDB COMMENT='角色数据范围表:在功能权限之外限制角色可查看和操作的租户内数据集合'; +) ENGINE=InnoDB COMMENT='角色数据范围表:在功能权限之外限制角色可查看和操作的平台或客户范围内数据集合'; -- 存量数据库重复执行初始化脚本时,不重复创建审核状态索引。 SET @review_status_index_sql = IF( @@ -489,12 +489,12 @@ USE baiyue_order; CREATE TABLE IF NOT EXISTS ord_order ( id BIGINT UNSIGNED NOT NULL COMMENT '订单ID(雪花ID)', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', - order_no VARCHAR(32) NOT NULL COMMENT '租户内唯一订单号', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + order_no VARCHAR(32) NOT NULL COMMENT '客户范围内唯一订单号', inbound_code VARCHAR(40) NULL COMMENT '客户提交后生成的入仓码,草稿为空', customer_id BIGINT UNSIGNED NULL COMMENT '客户逻辑ID', customer_name VARCHAR(128) NOT NULL COMMENT '下单时客户名称快照', - status VARCHAR(24) NOT NULL DEFAULT 'DRAFT' COMMENT '订单业务状态', + status VARCHAR(24) NOT NULL DEFAULT 'DRAFT' COMMENT '订单业务状态:DRAFT草稿、SUBMITTED已提交、APPROVED审核通过、ACCEPTED已接单、REJECTED审核驳回、PROCESSING履约中、COMPLETED已完成、CANCELLED已取消', transport_mode VARCHAR(16) NOT NULL COMMENT '运输方式', trade_term VARCHAR(16) NULL COMMENT '贸易条款', destination_country_code CHAR(2) NOT NULL COMMENT '目的国家二字码', @@ -503,22 +503,22 @@ CREATE TABLE IF NOT EXISTS ord_order ( expected_ship_date DATE NULL COMMENT '预计发运日期', customer_remark VARCHAR(1000) NULL COMMENT '客户可见备注', pickup_method VARCHAR(24) NULL COMMENT '取货方式', - receiving_warehouse_id BIGINT UNSIGNED NULL COMMENT '国内收货仓ID', - receiving_warehouse_name VARCHAR(128) NULL COMMENT '国内收货仓快照', + receiving_warehouse_id BIGINT UNSIGNED NULL COMMENT '国内收货仓逻辑ID', + receiving_warehouse_name VARCHAR(128) NULL COMMENT '下单时国内收货仓名称快照;主数据变化不回写', receiving_warehouse_address VARCHAR(512) NULL COMMENT '国内收货仓完整地址快照', receiving_time VARCHAR(128) NULL COMMENT '国内收货仓收货时间快照', - channel_code VARCHAR(64) NULL COMMENT '渠道编码', - channel_name VARCHAR(128) NULL COMMENT '渠道名称快照', + channel_code VARCHAR(64) NULL COMMENT '运输渠道编码', + channel_name VARCHAR(128) NULL COMMENT '下单时运输渠道名称快照;渠道主数据变化不回写', carrier_id BIGINT UNSIGNED NULL COMMENT '船司逻辑ID快照', carrier_name VARCHAR(128) NULL COMMENT '船司名称快照', - tax_mode VARCHAR(24) NULL COMMENT '买单或退税', + tax_mode VARCHAR(24) NULL COMMENT '报关税务方式:BUY_DECLARATION买单、TAX_REFUND退税', pricing_tax_mode VARCHAR(16) NULL COMMENT '报价包税维度:TAX_INCLUDED或TAX_EXCLUDED;独立于tax_mode', - container_mode VARCHAR(24) NULL COMMENT '整柜或散货', - customs_inspection TINYINT(1) NULL COMMENT '是否商检', - destination_type VARCHAR(24) NULL COMMENT 'FBA/OVERSEAS/PRIVATE', - amazon_warehouse_code VARCHAR(32) NULL COMMENT '亚马逊仓库编码', - shipment_id VARCHAR(64) NULL COMMENT 'Amazon Shipment ID', - reference_id VARCHAR(64) NULL COMMENT 'Amazon Reference ID', + container_mode VARCHAR(24) NULL COMMENT '装运方式:FULL_CONTAINER整柜、LCL散货拼箱', + customs_inspection TINYINT(1) NULL COMMENT '是否需要商检:0否、1是', + destination_type VARCHAR(24) NULL COMMENT '目的仓类型:FBA亚马逊仓、OVERSEAS海外仓、PRIVATE私人地址', + amazon_warehouse_code VARCHAR(32) NULL COMMENT 'Amazon目的仓编码', + shipment_id VARCHAR(64) NULL COMMENT '订单级Amazon Shipment ID', + reference_id VARCHAR(64) NULL COMMENT '订单级Amazon Reference ID', express_company VARCHAR(64) NULL COMMENT '履约快递公司名称快照', express_tracking_no VARCHAR(128) NULL COMMENT '履约快递单号', submitted_at DATETIME(3) NULL COMMENT '提交时间', @@ -539,7 +539,7 @@ CREATE TABLE IF NOT EXISTS ord_order ( CREATE TABLE IF NOT EXISTS ord_order_party ( id BIGINT UNSIGNED NOT NULL COMMENT '参与方快照ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', order_id BIGINT UNSIGNED NOT NULL COMMENT '所属订单ID', party_type VARCHAR(16) NOT NULL COMMENT '参与方类型:SHIPPER或CONSIGNEE', company_name VARCHAR(128) NOT NULL COMMENT '公司名称快照', @@ -565,7 +565,7 @@ CREATE TABLE IF NOT EXISTS ord_order_party ( CREATE TABLE IF NOT EXISTS ord_order_cargo ( id BIGINT UNSIGNED NOT NULL COMMENT '货物明细ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', order_id BIGINT UNSIGNED NOT NULL COMMENT '所属订单ID', item_no INT NOT NULL COMMENT '订单内明细序号', cargo_name VARCHAR(128) NOT NULL COMMENT '货物名称', @@ -581,24 +581,24 @@ CREATE TABLE IF NOT EXISTS ord_order_cargo ( currency CHAR(3) NULL COMMENT '币种三字码', dangerous_goods TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否危险品', cargo_remark VARCHAR(500) NULL COMMENT '货物备注', - sku_no VARCHAR(64) NULL COMMENT 'SKU', - upc_no VARCHAR(64) NULL COMMENT 'UPC', - cargo_name_en VARCHAR(128) NULL COMMENT '英文品名', - customs_hs_code VARCHAR(20) NULL COMMENT '目的国清关HS CODE', - units_per_package INT UNSIGNED NULL COMMENT '每箱件数', - total_quantity INT UNSIGNED NULL COMMENT '总件数', - net_weight_kg DECIMAL(18,3) NULL COMMENT '净重kg', - unit_declared_value DECIMAL(18,2) NULL COMMENT '单件申报价值', - material VARCHAR(128) NULL COMMENT '材质', - product_usage VARCHAR(128) NULL COMMENT '用途', - model VARCHAR(64) NULL COMMENT '型号', - brand VARCHAR(64) NULL COMMENT '品牌', - product_length_cm DECIMAL(12,2) NULL COMMENT '产品长度(厘米)', - product_width_cm DECIMAL(12,2) NULL COMMENT '产品宽度(厘米)', - product_height_cm DECIMAL(12,2) NULL COMMENT '产品高度(厘米)', - shipment_id VARCHAR(64) NULL COMMENT '货物级 Amazon Shipment ID', - reference_id VARCHAR(64) NULL COMMENT '货物级 Amazon Reference ID', - product_image_file_id BIGINT UNSIGNED NULL COMMENT '文件中心产品图片逻辑ID', + sku_no VARCHAR(64) NULL COMMENT '货物SKU编码', + upc_no VARCHAR(64) NULL COMMENT '货物UPC条码', + cargo_name_en VARCHAR(128) NULL COMMENT '货物英文品名', + customs_hs_code VARCHAR(20) NULL COMMENT '目的国清关HS编码', + units_per_package INT UNSIGNED NULL COMMENT '每箱产品数量', + total_quantity INT UNSIGNED NULL COMMENT '产品总数量', + net_weight_kg DECIMAL(18,3) NULL COMMENT '货物净重,单位千克', + unit_declared_value DECIMAL(18,2) NULL COMMENT '单件申报价值;币种取订单申报币种', + material VARCHAR(128) NULL COMMENT '产品材质', + product_usage VARCHAR(128) NULL COMMENT '产品用途', + model VARCHAR(64) NULL COMMENT '产品型号', + brand VARCHAR(64) NULL COMMENT '产品品牌', + product_length_cm DECIMAL(12,2) NULL COMMENT '单件产品长度,单位厘米', + product_width_cm DECIMAL(12,2) NULL COMMENT '单件产品宽度,单位厘米', + product_height_cm DECIMAL(12,2) NULL COMMENT '单件产品高度,单位厘米', + shipment_id VARCHAR(64) NULL COMMENT '本货物明细对应的Amazon Shipment ID', + reference_id VARCHAR(64) NULL COMMENT '本货物明细对应的Amazon Reference ID', + product_image_file_id BIGINT UNSIGNED NULL COMMENT 'File服务中的产品图片逻辑文件ID', created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改人ID', @@ -613,7 +613,7 @@ CREATE TABLE IF NOT EXISTS ord_order_cargo ( CREATE TABLE IF NOT EXISTS ord_order_internal_note ( id BIGINT UNSIGNED NOT NULL COMMENT '内部备注ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', order_id BIGINT UNSIGNED NOT NULL COMMENT '所属订单ID', content VARCHAR(2000) NOT NULL COMMENT '仅内部人员可见的备注内容', created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID', @@ -629,7 +629,7 @@ CREATE TABLE IF NOT EXISTS ord_order_internal_note ( CREATE TABLE IF NOT EXISTS ord_order_status_history ( id BIGINT UNSIGNED NOT NULL COMMENT '状态历史ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', order_id BIGINT UNSIGNED NOT NULL COMMENT '所属订单ID', from_status VARCHAR(24) NULL COMMENT '变更前状态,首次创建可为空', to_status VARCHAR(24) NOT NULL COMMENT '变更后状态', @@ -645,12 +645,12 @@ CREATE TABLE IF NOT EXISTS ord_order_status_history ( CREATE TABLE IF NOT EXISTS ord_outbox_event ( id BIGINT UNSIGNED NOT NULL COMMENT '领域事件全局ID,同时作为消费幂等键', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '事件所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '事件所属客户ID和数据隔离键', aggregate_type VARCHAR(32) NOT NULL COMMENT '聚合类型,例如 ORDER', aggregate_id BIGINT UNSIGNED NOT NULL COMMENT '发生变更的聚合根ID,例如订单ID', event_type VARCHAR(64) NOT NULL COMMENT '领域事件类型,例如 ORDER_SUBMITTED', payload_json JSON NOT NULL COMMENT '供下游消费的事件负载快照', - publish_status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT '发布状态:PENDING、PUBLISHED或FAILED', + publish_status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT '发布状态:PENDING待发布、PUBLISHED已发布、FAILED发布失败', retry_count INT NOT NULL DEFAULT 0 COMMENT '投递失败后的累计重试次数', next_retry_at DATETIME(3) NULL COMMENT '允许再次投递的最早时间', published_at DATETIME(3) NULL COMMENT '成功发布到消息中间件的时间', @@ -660,22 +660,22 @@ CREATE TABLE IF NOT EXISTS ord_outbox_event ( ) ENGINE=InnoDB COMMENT='订单事务Outbox表:与订单业务变更在同一本地事务写入,供后台发布器可靠投递RocketMQ,保证数据库状态与领域事件最终一致;不是普通操作日志'; CREATE TABLE IF NOT EXISTS ord_idempotency_record ( - id BIGINT UNSIGNED NOT NULL COMMENT '幂等记录ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '租户ID', - operator_id BIGINT UNSIGNED NOT NULL COMMENT '操作者ID', + id BIGINT UNSIGNED NOT NULL COMMENT '幂等记录 ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + operator_id BIGINT UNSIGNED NOT NULL COMMENT '发起写操作的用户ID', operation VARCHAR(32) NOT NULL COMMENT '幂等操作类型', - resource_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '资源ID,创建草稿时为0', + resource_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '操作目标资源ID;创建前未知时为0', idempotency_key VARCHAR(128) NOT NULL COMMENT '客户端幂等键', - request_hash CHAR(64) NOT NULL COMMENT '请求内容SHA-256摘要', - status VARCHAR(16) NOT NULL COMMENT '处理状态:PROCESSING或COMPLETED', - result_order_id BIGINT UNSIGNED NULL COMMENT '成功结果订单ID', + request_hash CHAR(64) NOT NULL COMMENT '规范化请求内容SHA-256摘要,用于识别同键不同请求', + status VARCHAR(16) NOT NULL COMMENT '处理状态:PROCESSING处理中、COMPLETED已完成', + result_order_id BIGINT UNSIGNED NULL COMMENT '成功操作生成或更新的订单ID,用于幂等重放', completed_at DATETIME(3) NULL COMMENT '处理完成时间', 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 (id), UNIQUE KEY uk_order_idempotency_scope (tenant_id, operator_id, operation, idempotency_key), KEY idx_order_idempotency_created (created_at) -) ENGINE=InnoDB COMMENT='订单写接口持久化幂等记录:唯一键保证同租户同操作者并发请求只执行一次'; +) ENGINE=InnoDB COMMENT='订单写接口持久化幂等记录'; -- ============================================================================ -- 来源:warehouse\src\main\resources\db\warehouse.sql @@ -692,7 +692,7 @@ CREATE TABLE IF NOT EXISTS wh_warehouse ( warehouse_name VARCHAR(120) NOT NULL COMMENT '仓库名称', warehouse_type VARCHAR(32) NOT NULL DEFAULT 'DOMESTIC' COMMENT '公共仓库类型:DOMESTIC、FBA、OVERSEAS', country_code CHAR(2) NOT NULL COMMENT '所在国家二字码', - timezone VARCHAR(64) NOT NULL COMMENT '仓库所在地IANA时区', + timezone VARCHAR(64) NOT NULL COMMENT '仓库所在地UTC偏移量', contact_name VARCHAR(64) DEFAULT NULL COMMENT '仓库联系人', contact_phone VARCHAR(32) DEFAULT NULL COMMENT '联系电话', contact_email VARCHAR(128) DEFAULT NULL COMMENT '联系邮箱', @@ -764,25 +764,25 @@ CREATE TABLE IF NOT EXISTS wh_service_capability ( SET NAMES utf8mb4; USE baiyue_warehouse; CREATE TABLE IF NOT EXISTS wh_partner ( - id BIGINT UNSIGNED NOT NULL, tenant_id BIGINT UNSIGNED NOT NULL, + id BIGINT UNSIGNED NOT NULL COMMENT '往来单位ID(雪花ID)', tenant_id BIGINT UNSIGNED NOT NULL COMMENT '平台公共往来单位固定使用 tenant_id=0', data_scope_id BIGINT UNSIGNED NOT NULL COMMENT '所属业务数据范围', - partner_code VARCHAR(50) NOT NULL, partner_name VARCHAR(128) NOT NULL, + partner_code VARCHAR(50) NOT NULL COMMENT '平台内唯一往来单位编码', partner_name VARCHAR(128) NOT NULL COMMENT '往来单位名称', partner_type VARCHAR(20) NOT NULL COMMENT 'CUSTOMER、SUPPLIER或BOTH', - contact_name VARCHAR(64) NOT NULL, contact_phone VARCHAR(32) NOT NULL, - contact_email VARCHAR(128) NULL, country_code CHAR(2) NOT NULL, - province VARCHAR(64) NULL, city VARCHAR(64) NOT NULL, district VARCHAR(64) NULL, - address_line VARCHAR(255) NOT NULL, postal_code VARCHAR(20) NULL, - status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE', remark VARCHAR(500) NULL, - created_by BIGINT UNSIGNED NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), updated_by BIGINT UNSIGNED NULL, - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - deleted TINYINT(1) NOT NULL DEFAULT 0, version INT NOT NULL DEFAULT 0, + contact_name VARCHAR(64) NOT NULL COMMENT '联系人姓名', contact_phone VARCHAR(32) NOT NULL COMMENT '联系电话', + contact_email VARCHAR(128) NULL COMMENT '联系邮箱', country_code CHAR(2) NOT NULL COMMENT '国家二字码', + province VARCHAR(64) NULL COMMENT '省或州', city VARCHAR(64) NOT NULL COMMENT '城市', district VARCHAR(64) NULL COMMENT '区县', + address_line VARCHAR(255) NOT NULL COMMENT '详细地址', postal_code VARCHAR(20) NULL COMMENT '邮政编码', + status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '状态:ACTIVE启用、DISABLED停用', remark VARCHAR(500) NULL COMMENT '备注', + created_by BIGINT UNSIGNED NULL COMMENT '创建人ID', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', updated_by BIGINT UNSIGNED NULL COMMENT '最后修改人ID', + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间', + deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记:0未删除、1已删除', version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', PRIMARY KEY(id), UNIQUE KEY uk_wh_partner_tenant_code(tenant_id,partner_code,deleted), KEY idx_wh_partner_tenant_status(tenant_id,status,deleted) ) ENGINE=InnoDB COMMENT='平台统一维护、供全部客户共享的往来单位主数据'; CREATE TABLE IF NOT EXISTS wh_partner_type ( - partner_id BIGINT UNSIGNED NOT NULL, tenant_id BIGINT UNSIGNED NOT NULL, - data_scope_id BIGINT UNSIGNED NOT NULL, partner_type VARCHAR(32) NOT NULL, + partner_id BIGINT UNSIGNED NOT NULL COMMENT '往来单位ID', tenant_id BIGINT UNSIGNED NOT NULL COMMENT '平台公共往来单位固定使用 tenant_id=0', + data_scope_id BIGINT UNSIGNED NOT NULL COMMENT '所属业务数据范围,平台公共数据为0', partner_type VARCHAR(32) NOT NULL COMMENT '稳定业务类型:CUSTOMER客户、SHIPPER发货方等', PRIMARY KEY(partner_id,partner_type), KEY idx_wh_partner_type_scope(tenant_id,data_scope_id,partner_type,partner_id) ) ENGINE=InnoDB COMMENT='平台共享往来单位稳定业务类型'; @@ -833,11 +833,11 @@ USE baiyue_firstleg; CREATE TABLE IF NOT EXISTS fl_fulfillment_task ( id BIGINT UNSIGNED NOT NULL COMMENT '履约任务ID(雪花ID)', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', order_id BIGINT UNSIGNED NOT NULL COMMENT '订单服务中的订单逻辑ID,不建立跨库外键', order_no VARCHAR(32) NOT NULL COMMENT '订单号快照', order_snapshot_json JSON NOT NULL COMMENT '创建履约任务时所需的订单业务快照', - status VARCHAR(24) NOT NULL DEFAULT 'PENDING_ASSIGNMENT' COMMENT '履约任务当前状态', + status VARCHAR(24) NOT NULL DEFAULT 'PENDING_ASSIGNMENT' COMMENT '履约状态:PENDING_ASSIGNMENT待指派、ASSIGNED已指派、PROCESSING处理中、COMPLETED已完成、CANCELLED已取消', assignee_id BIGINT UNSIGNED NULL COMMENT '当前负责人ID', assignee_name VARCHAR(64) NULL COMMENT '当前负责人名称快照', assigned_at DATETIME(3) NULL COMMENT '最近指派时间', @@ -857,7 +857,7 @@ CREATE TABLE IF NOT EXISTS fl_fulfillment_task ( CREATE TABLE IF NOT EXISTS fl_assignment_history ( id BIGINT UNSIGNED NOT NULL COMMENT '指派历史ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', task_id BIGINT UNSIGNED NOT NULL COMMENT '所属履约任务ID', previous_assignee_id BIGINT UNSIGNED NULL COMMENT '原负责人ID', assignee_id BIGINT UNSIGNED NOT NULL COMMENT '新负责人ID', @@ -887,7 +887,7 @@ USE baiyue_file; CREATE TABLE IF NOT EXISTS file_object ( id BIGINT UNSIGNED NOT NULL COMMENT '文件ID(雪花ID)', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', business_type VARCHAR(32) NULL COMMENT '关联业务类型', business_id VARCHAR(64) NULL COMMENT '关联业务逻辑ID', original_name VARCHAR(255) NOT NULL COMMENT '原始文件名', @@ -912,7 +912,7 @@ CREATE TABLE IF NOT EXISTS file_object ( CREATE TABLE IF NOT EXISTS file_download_token ( id BIGINT UNSIGNED NOT NULL COMMENT '下载令牌ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', file_id BIGINT UNSIGNED NOT NULL COMMENT '文件逻辑ID', token_hash CHAR(64) NOT NULL COMMENT '下载令牌SHA-256摘要', expires_at DATETIME(3) NOT NULL COMMENT '过期时间', @@ -929,7 +929,7 @@ CREATE TABLE IF NOT EXISTS file_download_token ( CREATE TABLE IF NOT EXISTS file_download_log ( id BIGINT UNSIGNED NOT NULL COMMENT '下载日志ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', file_id BIGINT UNSIGNED NOT NULL COMMENT '文件逻辑ID', user_id BIGINT UNSIGNED NULL COMMENT '下载用户ID', request_id VARCHAR(64) NULL COMMENT '请求ID', @@ -944,14 +944,14 @@ CREATE TABLE IF NOT EXISTS file_download_log ( CREATE TABLE IF NOT EXISTS file_export_task ( id BIGINT UNSIGNED NOT NULL COMMENT '导出任务ID(雪花ID)', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '申请人所属租户ID,平台任务为0', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键;平台任务固定为0', app_code VARCHAR(24) NOT NULL COMMENT '申请端:PLATFORM或MERCHANT', task_no VARCHAR(40) NOT NULL COMMENT '导出任务编号', export_type VARCHAR(64) NOT NULL COMMENT '导出类型', format VARCHAR(16) NOT NULL DEFAULT 'XLSX' COMMENT '导出格式', task_name VARCHAR(128) NOT NULL COMMENT '任务名称', parameters_json JSON NOT NULL COMMENT '筛选条件快照', - status VARCHAR(24) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/RUNNING/SUCCEEDED/FAILED/EXPIRED', + status VARCHAR(24) NOT NULL DEFAULT 'PENDING' COMMENT '状态:PENDING待处理、RUNNING执行中、SUCCEEDED成功、FAILED失败、EXPIRED已过期', total_rows BIGINT UNSIGNED NULL COMMENT '预计总行数', processed_rows BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '已处理行数', file_name VARCHAR(255) NULL COMMENT '下载文件名', @@ -990,7 +990,7 @@ CREATE TABLE IF NOT EXISTS file_export_task ( CREATE TABLE IF NOT EXISTS file_export_task_guard ( guard_key VARCHAR(64) NOT NULL COMMENT '任务创建串行化锁键', - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '锁记录最后更新时间', PRIMARY KEY (guard_key) ) ENGINE=InnoDB COMMENT='导出任务创建并发保护表:低频创建入口使用单行锁保证重复提交及活动上限'; @@ -1005,15 +1005,15 @@ CREATE TABLE IF NOT EXISTS file_export_outbox ( tag VARCHAR(64) NOT NULL COMMENT '消息Tag', message_key VARCHAR(64) NOT NULL COMMENT '消息Key', payload_json JSON NOT NULL COMMENT '命令消息体', - status VARCHAR(24) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/CLAIMED/SENT/QUARANTINED', + status VARCHAR(24) NOT NULL DEFAULT 'PENDING' COMMENT '状态:PENDING待发送、CLAIMED已领取、SENT已发送、QUARANTINED隔离', attempt_count INT NOT NULL DEFAULT 0 COMMENT '累计发送尝试次数', next_retry_at DATETIME(3) NOT NULL COMMENT '允许发送的最早时间', lease_owner VARCHAR(128) NULL COMMENT '领取实例标识', lease_until DATETIME(3) NULL COMMENT '领取租约截止时间', sent_at DATETIME(3) NULL COMMENT '发送成功时间', last_error VARCHAR(1000) NULL COMMENT '最后发送错误', - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 'Outbox记录创建时间', + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT 'Outbox记录更新时间', PRIMARY KEY (id), UNIQUE KEY uk_file_export_outbox_event (event_id), UNIQUE KEY uk_file_export_outbox_task_version (task_id, dispatch_version), @@ -1656,15 +1656,15 @@ DROP PROCEDURE phase1_add_column; -- 已有订单库的幂等记录迁移,和订单写入处于同一个本地事务。 CREATE TABLE IF NOT EXISTS ord_idempotency_record ( id BIGINT UNSIGNED NOT NULL COMMENT '幂等记录 ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '租户 ID', - operator_id BIGINT UNSIGNED NOT NULL COMMENT '操作人 ID', - operation VARCHAR(32) NOT NULL COMMENT '操作类型', - resource_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '资源 ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + operator_id BIGINT UNSIGNED NOT NULL COMMENT '发起写操作的用户ID', + operation VARCHAR(32) NOT NULL COMMENT '幂等操作类型', + resource_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '操作目标资源ID;创建前未知时为0', idempotency_key VARCHAR(128) NOT NULL COMMENT '客户端幂等键', - request_hash CHAR(64) NOT NULL COMMENT '请求内容摘要', - status VARCHAR(16) NOT NULL COMMENT '处理状态', - result_order_id BIGINT UNSIGNED NULL COMMENT '重放结果订单 ID', - completed_at DATETIME(3) NULL COMMENT '完成时间', + request_hash CHAR(64) NOT NULL COMMENT '规范化请求内容SHA-256摘要,用于识别同键不同请求', + status VARCHAR(16) NOT NULL COMMENT '处理状态:PROCESSING处理中、COMPLETED已完成', + result_order_id BIGINT UNSIGNED NULL COMMENT '成功操作生成或更新的订单ID,用于幂等重放', + completed_at DATETIME(3) NULL COMMENT '处理完成时间', 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 (id), @@ -2355,17 +2355,17 @@ USE baiyue_firstleg; CREATE TABLE IF NOT EXISTS fl_inbound_order ( id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID(雪花ID)', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', task_id BIGINT UNSIGNED NULL COMMENT '头程履约任务ID', order_id BIGINT UNSIGNED NOT NULL COMMENT '订单服务订单ID,不建立跨库外键', order_no VARCHAR(32) NOT NULL COMMENT '订单号快照', - inbound_code VARCHAR(40) NOT NULL COMMENT '租户内唯一入仓码', - process_status VARCHAR(24) NOT NULL DEFAULT 'WAIT_MEASURE' COMMENT '入库流程状态', + inbound_code VARCHAR(40) NOT NULL COMMENT '客户内唯一入仓码', + process_status VARCHAR(24) NOT NULL DEFAULT 'WAIT_MEASURE' COMMENT '入库流程状态:WAIT_MEASURE待测量、MEASURING测量中、MEASURE_PENDING待审核、MEASURE_REJECTED测量驳回、WAIT_PRICING待计费、FEE_CONFIRMING费用确认中、INBOUNDING入库收口中、INBOUNDED已入库、WAIT_SPLIT历史待分柜', order_status_snapshot VARCHAR(24) NOT NULL COMMENT '订单状态快照', - measure_review_status VARCHAR(24) NOT NULL DEFAULT 'NOT_SUBMITTED' COMMENT '测量数据审核状态', - fee_status VARCHAR(24) NOT NULL DEFAULT 'UNPRICED' COMMENT '费用业务状态', - fee_review_status VARCHAR(24) NOT NULL DEFAULT 'NOT_SUBMITTED' COMMENT '费用审核状态', - notification_status VARCHAR(24) NOT NULL DEFAULT 'NOT_CREATED' COMMENT '客户通知状态', + measure_review_status VARCHAR(24) NOT NULL DEFAULT 'NOT_SUBMITTED' COMMENT '测量审核状态:NOT_SUBMITTED未提交、PENDING待审核、APPROVED已通过、REJECTED已驳回', + fee_status VARCHAR(24) NOT NULL DEFAULT 'UNPRICED' COMMENT '费用状态:UNPRICED未计费、CALCULATED已计算、CONFIRMED已确认、SENT已发送、VOIDED已作废', + fee_review_status VARCHAR(24) NOT NULL DEFAULT 'NOT_SUBMITTED' COMMENT '费用审核状态:NOT_SUBMITTED未提交、PENDING待审核、APPROVED已通过、REJECTED已驳回、CONFIRMED已确认', + notification_status VARCHAR(24) NOT NULL DEFAULT 'NOT_CREATED' COMMENT '客户通知状态:NOT_CREATED未创建、PENDING待通知', expected_arrival_at DATETIME(3) NULL COMMENT '预计到达时间', customer_id BIGINT UNSIGNED NULL COMMENT '客户逻辑ID', customer_name VARCHAR(128) NOT NULL COMMENT '客户名称快照', @@ -2446,7 +2446,7 @@ UPDATE baiyue_order.ord_order CREATE TABLE IF NOT EXISTS fl_inbound_pickup_snapshot ( id BIGINT UNSIGNED NOT NULL COMMENT '提货信息快照ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID', pickup_method VARCHAR(24) NULL COMMENT '提货方式', pickup_at DATETIME(3) NULL COMMENT '提货时间', @@ -2467,7 +2467,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_pickup_snapshot ( CREATE TABLE IF NOT EXISTS fl_inbound_destination_snapshot ( id BIGINT UNSIGNED NOT NULL COMMENT '目的仓地址快照ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID', destination_type VARCHAR(24) NULL COMMENT '目的仓类型', destination_code VARCHAR(32) NULL COMMENT '目的仓编码', @@ -2487,7 +2487,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_destination_snapshot ( CREATE TABLE IF NOT EXISTS fl_inbound_transport_snapshot ( id BIGINT UNSIGNED NOT NULL COMMENT '运输信息快照ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID', loading_status VARCHAR(24) NULL COMMENT '配载状态快照', container_no VARCHAR(64) NULL COMMENT '集装箱号', @@ -2509,7 +2509,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_transport_snapshot ( CREATE TABLE IF NOT EXISTS fl_inbound_measurement ( id BIGINT UNSIGNED NOT NULL COMMENT '测量批次ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID', source_type VARCHAR(24) NOT NULL COMMENT '数据来源:THIRD_PARTY或MANUAL', source_code VARCHAR(64) NULL COMMENT '第三方应用或来源编码', @@ -2518,7 +2518,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_measurement ( device_code VARCHAR(64) NULL COMMENT '测量设备编码', warehouse_code VARCHAR(50) NULL COMMENT '第三方回传仓库编码', measured_at DATETIME(3) NULL COMMENT '设备测量时间', - status VARCHAR(24) NOT NULL DEFAULT 'RECEIVED' COMMENT '测量批次状态', + status VARCHAR(24) NOT NULL DEFAULT 'RECEIVED' COMMENT '测量批次状态:DRAFT草稿、RECEIVED已接收、SUBMITTED已提交、APPROVED已通过、REJECTED已驳回', raw_payload_json JSON NULL COMMENT '第三方原始报文', original_document_no VARCHAR(64) NULL COMMENT '原单据号码快照', original_gross_weight_kg DECIMAL(18,3) NULL COMMENT '原单据毛重kg', @@ -2540,7 +2540,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_measurement ( CREATE TABLE IF NOT EXISTS fl_inbound_measurement_item ( id BIGINT UNSIGNED NOT NULL COMMENT '测量明细ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', measurement_id BIGINT UNSIGNED NOT NULL COMMENT '测量批次ID', inbound_detail_id BIGINT UNSIGNED NULL COMMENT '绑定的订单货物明细ID,平台手工明细可为空', external_detail_no VARCHAR(128) NULL COMMENT '第三方测量明细唯一号,平台手工明细可为空', @@ -2580,7 +2580,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_measurement_item ( CREATE TABLE IF NOT EXISTS fl_mojia_measurement_receipt ( id BIGINT UNSIGNED NOT NULL COMMENT '墨家联调收件记录ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', subject_id BIGINT UNSIGNED NOT NULL COMMENT '设备应用操作主体ID', box_code VARCHAR(128) NOT NULL COMMENT '设备扫描的原始箱码', scanned_shipment_id VARCHAR(64) NULL COMMENT '设备从Amazon FBA箱标实扫的Shipment ID', @@ -2591,7 +2591,7 @@ CREATE TABLE IF NOT EXISTS fl_mojia_measurement_receipt ( machine_no VARCHAR(64) NOT NULL COMMENT '设备编号', measured_at DATETIME(3) NOT NULL COMMENT '设备测量时间或服务接收时间', image_file_id BIGINT UNSIGNED NOT NULL COMMENT 'File服务中的墨家联调图片ID', - status VARCHAR(24) NOT NULL DEFAULT 'UNMATCHED' COMMENT '收件状态', + status VARCHAR(24) NOT NULL DEFAULT 'UNMATCHED' COMMENT '测量回执匹配状态:UNMATCHED未匹配;当前首版仅记录未匹配回执', match_failure_code VARCHAR(16) NOT NULL COMMENT '正式业务匹配失败码', match_failure_reason VARCHAR(500) NOT NULL COMMENT '脱敏后的匹配失败原因', request_id VARCHAR(128) NULL COMMENT '请求链路ID', @@ -2605,7 +2605,7 @@ CREATE TABLE IF NOT EXISTS fl_mojia_measurement_receipt ( CREATE TABLE IF NOT EXISTS fl_inbound_carton_label ( id BIGINT UNSIGNED NOT NULL COMMENT '实际箱唛头映射ID(雪花ID)', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '当前客户隔离ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID', order_id BIGINT UNSIGNED NOT NULL COMMENT '订单服务订单ID快照', inbound_detail_id BIGINT UNSIGNED NOT NULL COMMENT '订单货物明细ID,不建立跨库外键', @@ -2630,7 +2630,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_carton_label ( CREATE TABLE IF NOT EXISTS fl_inbound_weight_result ( id BIGINT UNSIGNED NOT NULL COMMENT 'OCR重量结果ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID', file_id BIGINT UNSIGNED NOT NULL COMMENT '称重图片文件ID', ocr_provider VARCHAR(64) NULL COMMENT 'OCR服务商编码', @@ -2641,7 +2641,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_weight_result ( adjusted_weight_value DECIMAL(18,3) NULL COMMENT '人工修正后的原单位重量', adjusted_weight_unit VARCHAR(8) NULL COMMENT '人工修正后的单位', final_weight_kg DECIMAL(18,3) NULL COMMENT '确认并统一换算后的重量kg', - recognition_status VARCHAR(24) NOT NULL DEFAULT 'PENDING' COMMENT '识别和确认状态', + recognition_status VARCHAR(24) NOT NULL DEFAULT 'PENDING' COMMENT '重量识别状态:PENDING待识别、PENDING_CONFIRM待确认、NEEDS_MANUAL需人工录入、CONFIRMED已确认', adjustment_reason VARCHAR(500) NULL COMMENT '人工修正原因', adjusted_by BIGINT UNSIGNED NULL COMMENT '修正人ID', adjusted_at DATETIME(3) NULL COMMENT '修正时间', @@ -2661,7 +2661,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_weight_result ( CREATE TABLE IF NOT EXISTS fl_inbound_weight_revision ( id BIGINT UNSIGNED NOT NULL COMMENT 'OCR重量修订记录ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID', weight_result_id BIGINT UNSIGNED NOT NULL COMMENT 'OCR重量结果ID', revision_no INT UNSIGNED NOT NULL COMMENT '结果版本号,与乐观锁版本一致', @@ -2684,7 +2684,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_weight_revision ( CREATE TABLE IF NOT EXISTS fl_measure_review_record ( id BIGINT UNSIGNED NOT NULL COMMENT '测量审核记录ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID', measurement_id BIGINT UNSIGNED NULL COMMENT '被审核的测量批次ID', review_result VARCHAR(24) NOT NULL COMMENT '审核结果', @@ -2704,8 +2704,8 @@ CREATE TABLE IF NOT EXISTS fl_measure_review_record ( CREATE TABLE IF NOT EXISTS fl_customer_charge_rule ( id BIGINT UNSIGNED NOT NULL COMMENT '客户计费规则ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', - rule_code VARCHAR(50) NOT NULL COMMENT '租户内规则编码', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + rule_code VARCHAR(50) NOT NULL COMMENT '客户内唯一计费规则编码', rule_version INT UNSIGNED NOT NULL COMMENT '规则版本号', customer_id BIGINT UNSIGNED NOT NULL COMMENT '客户逻辑ID', customer_name VARCHAR(128) NOT NULL COMMENT '客户名称快照', @@ -2718,7 +2718,7 @@ CREATE TABLE IF NOT EXISTS fl_customer_charge_rule ( currency CHAR(3) NOT NULL COMMENT '默认币种', effective_from DATETIME(3) NOT NULL COMMENT '生效开始时间', effective_to DATETIME(3) NULL COMMENT '生效结束时间', - status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '规则状态', + status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '规则状态:ACTIVE启用、DISABLED停用', remark VARCHAR(500) NULL COMMENT '规则备注', created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', @@ -2761,7 +2761,7 @@ CREATE TABLE IF NOT EXISTS fl_carrier_fba_rate ( source_destination_text VARCHAR(1000) NULL COMMENT '原始组合仓库或区域文本', effective_from DATETIME(3) NOT NULL COMMENT '生效开始时间', effective_to DATETIME(3) NULL COMMENT '生效结束时间', - status VARCHAR(24) NOT NULL DEFAULT 'ACTIVE' COMMENT '价格状态:PENDING_CONFIRMATION、ACTIVE或SUPERSEDED', + status VARCHAR(24) NOT NULL DEFAULT 'ACTIVE' COMMENT '价格状态:PENDING_CONFIRMATION、ACTIVE或SUPERSEDED;分别表示待确认、已生效、已被替代;FAILED表示解析失败', remark VARCHAR(500) NULL COMMENT '价格备注', created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', @@ -2783,9 +2783,9 @@ ALTER TABLE fl_carrier_fba_rate CREATE TABLE IF NOT EXISTS fl_inbound_fee ( id BIGINT UNSIGNED NOT NULL COMMENT '入库费用ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID', - fee_no VARCHAR(40) NOT NULL COMMENT '租户内唯一费用单号', + fee_no VARCHAR(40) NOT NULL COMMENT '客户内唯一费用单号', fee_version INT UNSIGNED NOT NULL DEFAULT 1 COMMENT '费用快照版本', charge_rule_id BIGINT UNSIGNED NULL COMMENT '历史客户计费规则ID;渠道价格可为空', charge_rule_version INT UNSIGNED NULL COMMENT '历史客户规则版本快照', @@ -2825,8 +2825,8 @@ CREATE TABLE IF NOT EXISTS fl_inbound_fee ( subtotal_amount DECIMAL(18,2) NOT NULL COMMENT '基础费用金额', additional_amount DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '附加费金额', total_amount DECIMAL(18,2) NOT NULL COMMENT '总金额', - fee_status VARCHAR(24) NOT NULL DEFAULT 'CALCULATED' COMMENT '费用状态', - review_status VARCHAR(24) NOT NULL DEFAULT 'NOT_SUBMITTED' COMMENT '费用审核状态', + fee_status VARCHAR(24) NOT NULL DEFAULT 'CALCULATED' COMMENT '费用状态:CALCULATED已计算、SUPERSEDED已被新版本替代', + review_status VARCHAR(24) NOT NULL DEFAULT 'NOT_SUBMITTED' COMMENT '费用审核状态:NOT_SUBMITTED未提交、PENDING待审核、APPROVED已通过、REJECTED已驳回、CONFIRMED已确认', exception_release TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否基于测量异常放行生成', calculated_by BIGINT UNSIGNED NOT NULL COMMENT '试算人ID', calculated_at DATETIME(3) NOT NULL COMMENT '试算时间', @@ -2847,7 +2847,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_fee ( CREATE TABLE IF NOT EXISTS fl_inbound_fee_item ( id BIGINT UNSIGNED NOT NULL COMMENT '费用明细ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', fee_id BIGINT UNSIGNED NOT NULL COMMENT '费用头ID', item_no INT UNSIGNED NOT NULL COMMENT '费用单内序号', fee_code VARCHAR(50) NOT NULL COMMENT '费用项目编码', @@ -2867,7 +2867,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_fee_item ( CREATE TABLE IF NOT EXISTS fl_inbound_pallet_allocation ( id BIGINT UNSIGNED NOT NULL COMMENT '头程托盘分配引用ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID', allocation_no INT UNSIGNED NOT NULL COMMENT '入库单内分配序号', warehouse_id BIGINT UNSIGNED NOT NULL COMMENT '仓库服务仓库ID', @@ -2880,7 +2880,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_pallet_allocation ( location_id BIGINT UNSIGNED NOT NULL COMMENT '仓库服务库位ID', location_code VARCHAR(50) NOT NULL COMMENT '库位编码快照', reservation_no VARCHAR(64) NOT NULL COMMENT '仓库库位预占单号', - allocation_status VARCHAR(24) NOT NULL DEFAULT 'RESERVED' COMMENT '分配状态', + allocation_status VARCHAR(24) NOT NULL DEFAULT 'RESERVED' COMMENT '托盘库位分配状态:RESERVED已预占、CONFIRMED已确认、TRANSFERRED已转移、RELEASED已释放', 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 (id), @@ -2892,7 +2892,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_pallet_allocation ( CREATE TABLE IF NOT EXISTS fl_inbound_file ( id BIGINT UNSIGNED NOT NULL COMMENT '入库单文件关系ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID', file_id BIGINT UNSIGNED NOT NULL COMMENT '文件服务文件ID', file_type VARCHAR(24) NOT NULL COMMENT '文件类型:RELATED、MARK、PC_ORDER、MEASURE或WEIGHT', @@ -2910,7 +2910,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_file ( CREATE TABLE IF NOT EXISTS fl_inbound_hold_record ( id BIGINT UNSIGNED NOT NULL COMMENT '扣货记录ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID', hold_action VARCHAR(16) NOT NULL COMMENT '动作:HOLD或RELEASE', reason VARCHAR(1000) NOT NULL COMMENT '扣货或解除原因', @@ -2925,7 +2925,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_hold_record ( CREATE TABLE IF NOT EXISTS fl_inbound_internal_note ( id BIGINT UNSIGNED NOT NULL COMMENT '客服内部备注ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID', content VARCHAR(2000) NOT NULL COMMENT '仅内部可见备注', created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID', @@ -2938,7 +2938,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_internal_note ( CREATE TABLE IF NOT EXISTS fl_inbound_status_history ( id BIGINT UNSIGNED NOT NULL COMMENT '入库状态历史ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID', from_status VARCHAR(24) NULL COMMENT '变更前状态', to_status VARCHAR(24) NOT NULL COMMENT '变更后状态', @@ -2954,12 +2954,12 @@ CREATE TABLE IF NOT EXISTS fl_inbound_status_history ( CREATE TABLE IF NOT EXISTS fl_inbound_outbox ( id BIGINT UNSIGNED NOT NULL COMMENT '领域事件全局ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '事件所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '事件所属客户ID和数据隔离键', aggregate_type VARCHAR(32) NOT NULL DEFAULT 'INBOUND' COMMENT '聚合类型', aggregate_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID', event_type VARCHAR(64) NOT NULL COMMENT '事件类型', payload_json JSON NOT NULL COMMENT '事件负载快照', - publish_status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT '发布状态', + publish_status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT '事件发布状态:PENDING待发布、PUBLISHED已发布、FAILED发布失败', retry_count INT NOT NULL DEFAULT 0 COMMENT '重试次数', next_retry_at DATETIME(3) NULL COMMENT '下次重试时间', published_at DATETIME(3) NULL COMMENT '发布时间', @@ -2970,13 +2970,13 @@ CREATE TABLE IF NOT EXISTS fl_inbound_outbox ( CREATE TABLE IF NOT EXISTS fl_inbound_idempotency ( id BIGINT UNSIGNED NOT NULL COMMENT '入库写操作幂等记录ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', operation VARCHAR(40) NOT NULL COMMENT '操作类型', operator_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '操作者或外部应用ID', inbound_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '入库单ID,创建前可为0', idempotency_key VARCHAR(128) NOT NULL COMMENT '幂等键', request_hash CHAR(64) NOT NULL COMMENT '请求内容摘要', - status VARCHAR(16) NOT NULL COMMENT '处理状态', + status VARCHAR(16) NOT NULL COMMENT '幂等处理状态:PROCESSING处理中、SUCCEEDED已成功、FAILED已失败', result_json JSON NULL COMMENT '成功结果摘要', result_resource_id BIGINT UNSIGNED NULL COMMENT '成功结果资源ID', completed_at DATETIME(3) NULL COMMENT '完成时间', @@ -3090,7 +3090,7 @@ DROP PROCEDURE IF EXISTS phase2_measure_add_column; CREATE TABLE IF NOT EXISTS fl_inbound_weight_revision ( id BIGINT UNSIGNED NOT NULL COMMENT 'OCR重量修订记录ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID', weight_result_id BIGINT UNSIGNED NOT NULL COMMENT 'OCR重量结果ID', revision_no INT UNSIGNED NOT NULL COMMENT '结果版本号,与乐观锁版本一致', @@ -3320,9 +3320,9 @@ CREATE TABLE IF NOT EXISTS fl_price_channel_catalog ( price_model VARCHAR(32) NOT NULL COMMENT '默认价格模型', source_sheet_name VARCHAR(128) NOT NULL COMMENT '来源Sheet名称;海派为同一Sheet', aliases_json JSON NOT NULL COMMENT '上传名称别名数组', - status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '目录状态', - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '目录状态:ACTIVE启用、DISABLED停用', + 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 (id), UNIQUE KEY uk_price_channel_code (channel_code), KEY idx_price_channel_status (status, price_model) @@ -3332,9 +3332,9 @@ CREATE TABLE IF NOT EXISTS fl_price_origin_alias ( id BIGINT UNSIGNED NOT NULL COMMENT '起运仓别名ID', warehouse_code VARCHAR(64) NOT NULL COMMENT '平台仓库稳定编码,不存环境专属ID', alias_name VARCHAR(128) NOT NULL COMMENT 'Excel起运仓名称别名', - status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE', - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '状态:ACTIVE启用、DISABLED停用', + 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 (id), UNIQUE KEY uk_price_origin_alias (warehouse_code, alias_name), KEY idx_price_origin_alias_name (alias_name, status) @@ -3347,9 +3347,9 @@ CREATE TABLE IF NOT EXISTS fl_price_warehouse_mapping ( warehouse_id BIGINT UNSIGNED NOT NULL COMMENT '固定平台公共仓库ID', warehouse_code VARCHAR(64) NOT NULL COMMENT '平台仓库稳定编码快照', warehouse_type VARCHAR(16) NOT NULL COMMENT 'DOMESTIC或OVERSEAS', - status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE', - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '状态:ACTIVE启用、DISABLED停用', + 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 (id), UNIQUE KEY uk_price_warehouse_mapping_source (source_type, source_value), KEY idx_price_warehouse_mapping_target (warehouse_id, status) @@ -3362,20 +3362,20 @@ CREATE TABLE IF NOT EXISTS fl_price_book_version ( original_file_name VARCHAR(255) NOT NULL COMMENT '原始Excel文件名', source_file_id BIGINT UNSIGNED NULL COMMENT 'File服务原始文件ID', source_sha256 CHAR(64) NULL COMMENT '原始文件SHA-256', - status VARCHAR(24) NOT NULL DEFAULT 'PENDING_CONFIRMATION' COMMENT '待确认上传记录;ACTIVE/SUPERSEDED/FAILED', + status VARCHAR(24) NOT NULL DEFAULT 'PENDING_CONFIRMATION' COMMENT '价格版本状态:PENDING_CONFIRMATION待确认、ACTIVE已生效、SUPERSEDED已被替代、FAILED解析失败', blocking_error_count INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '阻断错误数', warning_count INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '警告数', parse_issues_json JSON NULL COMMENT '上传解析问题及待确认重新校验结果', channel_count INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '解析渠道数', rate_count INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '解析价格数', - created_by BIGINT UNSIGNED NOT NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - published_by BIGINT UNSIGNED NULL, + created_by BIGINT UNSIGNED NOT NULL COMMENT '上传价格表的操作人ID', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '上传记录创建时间', + published_by BIGINT UNSIGNED NULL COMMENT '确认并发布价格版本的操作人ID', published_at DATETIME(3) NULL COMMENT '确认并生效时间', superseded_at DATETIME(3) NULL COMMENT '被下一版本替换时间', - updated_by BIGINT UNSIGNED NOT NULL, - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - deleted TINYINT(1) NOT NULL DEFAULT 0, + updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改人ID', + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后更新时间', + deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记:0未删除、1已删除', version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', PRIMARY KEY (id), UNIQUE KEY uk_price_book_version (tenant_id, version_no), @@ -3399,9 +3399,9 @@ CREATE TABLE IF NOT EXISTS fl_price_book_channel ( surcharge_description VARCHAR(2000) NULL COMMENT '附加费说明,仅展示', remark VARCHAR(2000) NULL COMMENT '渠道备注', created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID', - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '渠道快照创建时间', updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改人ID', - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '渠道快照最后更新时间', deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记', version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', PRIMARY KEY (id), @@ -4025,53 +4025,53 @@ DROP PROCEDURE IF EXISTS unified_assert_permission_count; USE baiyue_warehouse; CREATE TABLE IF NOT EXISTS wh_overseas_storage_fee_rule ( - id BIGINT UNSIGNED NOT NULL COMMENT '海外仓阶梯费用规则ID(雪花ID)', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户账号ID,沿用tenant_id物理字段', - customer_name VARCHAR(128) NOT NULL COMMENT '客户公司名称快照', - currency CHAR(3) NOT NULL COMMENT 'ISO 4217币种三字码', - initialization_key VARCHAR(191) NULL COMMENT '初始化幂等键', + id BIGINT UNSIGNED NOT NULL COMMENT '海外仓费用规则ID(雪花ID)', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键;客户费用规则不得使用0', + customer_name VARCHAR(128) NOT NULL COMMENT '客户名称快照', + currency CHAR(3) NOT NULL COMMENT '费用币种三字码;首版为USD', + initialization_key VARCHAR(191) NULL COMMENT '初始化或导入幂等键', created_by BIGINT UNSIGNED NULL COMMENT '创建人ID', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', updated_by BIGINT UNSIGNED NULL COMMENT '最后修改人ID', updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间', - deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记', + deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记:0未删除、1已删除', version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', active_customer_id BIGINT UNSIGNED GENERATED ALWAYS AS (CASE WHEN deleted = 0 THEN tenant_id ELSE NULL END) STORED - COMMENT '未删除规则的客户唯一键', + COMMENT '未删除规则的客户唯一索引值', PRIMARY KEY (id), UNIQUE KEY uk_overseas_fee_active_customer (active_customer_id), KEY idx_overseas_fee_page (deleted, updated_at, id), KEY idx_overseas_fee_customer_name (customer_name, deleted) -) ENGINE=InnoDB COMMENT='客户维度海外仓阶梯仓储费规则'; +) ENGINE=InnoDB COMMENT='客户海外仓费用规则'; CREATE TABLE IF NOT EXISTS wh_overseas_storage_fee_tier ( - id BIGINT UNSIGNED NOT NULL COMMENT '海外仓阶梯费用明细ID(雪花ID)', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户账号ID,沿用tenant_id物理字段', - rule_id BIGINT UNSIGNED NOT NULL COMMENT '阶梯费用规则ID', - start_day SMALLINT UNSIGNED NOT NULL COMMENT '起始天,含边界', - end_day SMALLINT UNSIGNED NULL COMMENT '结束天,含边界;NULL表示以上', - fee_type VARCHAR(32) NOT NULL DEFAULT 'STORAGE' COMMENT '费用类型', - billing_unit VARCHAR(16) NOT NULL DEFAULT 'CBM' COMMENT '计费单位', - unit_price DECIMAL(18,6) NOT NULL COMMENT '仓储费标准,可为0', - sort_no SMALLINT UNSIGNED NOT NULL COMMENT '阶梯排序号', + id BIGINT UNSIGNED NOT NULL COMMENT '海外仓费用阶梯ID(雪花ID)', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键;与规则归属一致', + rule_id BIGINT UNSIGNED NOT NULL COMMENT '所属海外仓费用规则ID', + start_day SMALLINT UNSIGNED NOT NULL COMMENT '计费阶梯起始存储天数,含边界', + end_day SMALLINT UNSIGNED NULL COMMENT '计费阶梯结束存储天数,含边界;NULL表示以上', + fee_type VARCHAR(32) NOT NULL DEFAULT 'STORAGE' COMMENT '费用类型;首版为STORAGE仓储费', + billing_unit VARCHAR(16) NOT NULL DEFAULT 'CBM' COMMENT '计费单位;首版为CBM立方米', + unit_price DECIMAL(18,6) NOT NULL COMMENT '每计费单位单价,非负', + sort_no SMALLINT UNSIGNED NOT NULL COMMENT '阶梯展示和计算顺序', created_by BIGINT UNSIGNED NULL COMMENT '创建人ID', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', updated_by BIGINT UNSIGNED NULL COMMENT '最后修改人ID', updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间', - deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记', + deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记:0未删除、1已删除', version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', active_start_day SMALLINT UNSIGNED GENERATED ALWAYS AS (CASE WHEN deleted = 0 THEN start_day ELSE NULL END) STORED - COMMENT '未删除阶梯的起始天唯一键', + COMMENT '未删除阶梯的起始天唯一索引值', PRIMARY KEY (id), UNIQUE KEY uk_overseas_fee_tier_start (rule_id, fee_type, active_start_day), KEY idx_overseas_fee_tier_rule (tenant_id, rule_id, deleted, sort_no, id), CONSTRAINT chk_overseas_fee_tier_days CHECK (start_day >= 1 AND (end_day IS NULL OR end_day >= start_day)), CONSTRAINT chk_overseas_fee_tier_price CHECK (unit_price >= 0) -) ENGINE=InnoDB COMMENT='海外仓阶梯仓储费明细'; +) ENGINE=InnoDB COMMENT='客户海外仓费用阶梯'; -- 兼容已存在的旧版海外仓费用表:补齐新字段及免费档/无上限档约束。 DROP PROCEDURE IF EXISTS migrate_overseas_storage_fee_schema; @@ -4313,7 +4313,7 @@ CREATE TABLE IF NOT EXISTS fl_fee_item ( tenant_id BIGINT UNSIGNED NOT NULL COMMENT '平台公共费用项目作用域,固定为0;物理字段名沿用 tenant_id', category VARCHAR(64) NOT NULL COMMENT '费用分类', trade_scope VARCHAR(20) NOT NULL COMMENT 'DOMESTIC国内、INTERNATIONAL国际', - fee_code VARCHAR(50) NOT NULL COMMENT '稳定费用编码;平台公共主数据固定编码', + fee_code VARCHAR(50) NOT NULL COMMENT '稳定费用编码;系统费用使用固定编码', fee_type_name VARCHAR(128) NOT NULL COMMENT '费用类型名称', pickup_payable TINYINT(1) NOT NULL DEFAULT 0 COMMENT '提柜应付是否参与', pickup_receivable TINYINT(1) NOT NULL DEFAULT 0 COMMENT '提柜应收是否参与', @@ -4321,15 +4321,15 @@ CREATE TABLE IF NOT EXISTS fl_fee_item ( warehouse_receivable TINYINT(1) NOT NULL DEFAULT 0 COMMENT '仓储应收是否参与', allocation_fee TINYINT(1) NOT NULL DEFAULT 0 COMMENT '平摊费用是否参与', description VARCHAR(500) NULL COMMENT '说明', - status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT 'ACTIVE或DISABLED', - created_by BIGINT NULL, - created_by_name VARCHAR(64) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_by BIGINT NULL, - updated_by_name VARCHAR(64) NULL, - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - deleted TINYINT(1) NOT NULL DEFAULT 0, - version INT NOT NULL DEFAULT 0, + status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '状态:ACTIVE启用、DISABLED停用', + created_by BIGINT NULL COMMENT '创建人ID;系统初始化时可为空', + created_by_name VARCHAR(64) NULL COMMENT '创建人名称快照', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', + updated_by BIGINT NULL COMMENT '最后修改人ID;系统初始化时可为空', + updated_by_name VARCHAR(64) NULL COMMENT '最后修改人名称快照', + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后更新时间', + deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记:0未删除、1已删除', + version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', PRIMARY KEY (id), UNIQUE KEY uk_fee_item_code (fee_code), KEY idx_fee_item_query (tenant_id, trade_scope, category, status, deleted), @@ -4352,26 +4352,26 @@ CREATE TABLE IF NOT EXISTS fl_inbound_fee_detail ( destination_code VARCHAR(32) NULL COMMENT '目的仓编码快照', destination_name VARCHAR(128) NULL COMMENT '目的仓名称快照', package_count INT NULL COMMENT '件数快照', - actual_gross_weight_kg DECIMAL(18,4) NULL COMMENT '仓库实测毛重', - actual_volume_cbm DECIMAL(18,6) NULL COMMENT '体积', - volume_weight_kg DECIMAL(18,4) NULL COMMENT '材积重', - chargeable_weight_kg DECIMAL(18,4) NULL COMMENT '计费重量', - chargeable_volume_cbm DECIMAL(18,6) NULL COMMENT '计费体积', - unit_price DECIMAL(18,4) NOT NULL COMMENT '单价', - currency CHAR(3) NOT NULL COMMENT '币种', - amount DECIMAL(18,2) NOT NULL COMMENT '金额', + actual_gross_weight_kg DECIMAL(18,4) NULL COMMENT '仓库实测毛重快照,单位千克', + actual_volume_cbm DECIMAL(18,6) NULL COMMENT '实际体积快照,单位立方米', + volume_weight_kg DECIMAL(18,4) NULL COMMENT '材积重快照,单位千克', + chargeable_weight_kg DECIMAL(18,4) NULL COMMENT '费用计算采用的计费重量,单位千克', + chargeable_volume_cbm DECIMAL(18,6) NULL COMMENT '费用计算采用的计费体积,单位立方米', + unit_price DECIMAL(18,4) NOT NULL COMMENT '费用单价;计价单位由对应费用项目规则确定', + currency CHAR(3) NOT NULL COMMENT '金额币种三字码,如CNY', + amount DECIMAL(18,2) NOT NULL COMMENT '该费用明细金额,币种由currency字段指定', remark VARCHAR(500) NULL COMMENT '备注', - review_status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING、APPROVED、REJECTED', + review_status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT '审核状态:PENDING待审核、APPROVED已通过、REJECTED已驳回', reviewer_id BIGINT NULL COMMENT '审核人ID', reviewer_name VARCHAR(64) NULL COMMENT '审核人名称快照', reviewed_at DATETIME(3) NULL COMMENT '审核时间', review_reason VARCHAR(500) NULL COMMENT '审核驳回原因', - created_by BIGINT NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_by BIGINT NULL, - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - deleted TINYINT(1) NOT NULL DEFAULT 0, - version INT NOT NULL DEFAULT 0, + created_by BIGINT NULL COMMENT '创建人或系统主体ID', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', + updated_by BIGINT NULL COMMENT '最后修改人或系统主体ID', + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后更新时间', + deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记:0未删除、1已删除', + version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', PRIMARY KEY (id), KEY idx_inbound_fee_detail (tenant_id, inbound_id, business_date, deleted), KEY idx_inbound_fee_review (tenant_id, review_status, business_date, deleted), @@ -4489,11 +4489,11 @@ CALL add_audit_event_id(); DROP PROCEDURE IF EXISTS add_audit_event_id; CREATE TABLE IF NOT EXISTS sys_audit_dead_letter ( - id BIGINT NOT NULL AUTO_INCREMENT,event_id VARCHAR(64) NOT NULL,payload_text LONGTEXT NOT NULL, - status VARCHAR(16) NOT NULL DEFAULT 'PENDING',retry_count INT NOT NULL DEFAULT 0, - next_retry_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),lease_owner VARCHAR(128) NULL, - lease_until DATETIME(3) NULL,last_error VARCHAR(1000) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),processed_at DATETIME(3) NULL, + id BIGINT NOT NULL AUTO_INCREMENT COMMENT '死信记录自增ID',event_id VARCHAR(64) NOT NULL COMMENT '审计事件全局唯一ID',payload_text LONGTEXT NOT NULL COMMENT 'RocketMQ死信原始载荷', + status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT '状态:PENDING待补偿、PROCESSING处理中、PROCESSED已处理、DEAD永久失败',retry_count INT NOT NULL DEFAULT 0 COMMENT '死信重试次数', + next_retry_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '下次允许补偿时间',lease_owner VARCHAR(128) NULL COMMENT '当前补偿租约持有实例', + lease_until DATETIME(3) NULL COMMENT '当前补偿租约截止时间',last_error VARCHAR(1000) NULL COMMENT '最近一次补偿错误', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '死信记录创建时间',processed_at DATETIME(3) NULL COMMENT '补偿处理完成时间', PRIMARY KEY(id),UNIQUE KEY uk_audit_dead_event(event_id), KEY idx_audit_dead_retry(status,next_retry_at,lease_until) ) ENGINE=InnoDB COMMENT='审计消息死信补偿表'; @@ -4565,7 +4565,7 @@ CREATE TABLE IF NOT EXISTS fl_container_booking ( updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改人ID', updated_by_name VARCHAR(64) NULL COMMENT '最后修改人名称快照', updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) - COMMENT '更新时间', + COMMENT '货柜资料最后更新时间', version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', PRIMARY KEY (id), UNIQUE KEY uk_container_booking_no (booking_no), @@ -4582,7 +4582,7 @@ CREATE TABLE IF NOT EXISTS fl_loading_plan ( plan_no VARCHAR(40) NOT NULL COMMENT '平台唯一装柜计划号', container_booking_id BIGINT UNSIGNED NOT NULL COMMENT '货柜预订ID', plan_status VARCHAR(24) NOT NULL DEFAULT 'EDITING' - COMMENT '唯一装柜状态:EDITING、LOADING、COMPLETED', + COMMENT '装柜计划状态:EDITING编辑中、LOADING装柜中、COMPLETED装柜完成;建议须经人工确认', push_idempotency_key VARCHAR(128) NULL COMMENT '推送预装幂等键', preload_pushed_by BIGINT UNSIGNED NULL COMMENT '推送预装操作人ID', preload_pushed_by_name VARCHAR(64) NULL COMMENT '推送预装操作人名称快照', @@ -4600,7 +4600,7 @@ CREATE TABLE IF NOT EXISTS fl_loading_plan ( updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改人ID', updated_by_name VARCHAR(64) NULL COMMENT '最后修改人名称快照', updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) - COMMENT '更新时间', + COMMENT '装柜计划最后更新时间', version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', PRIMARY KEY (id), UNIQUE KEY uk_loading_plan_no (plan_no), @@ -4662,7 +4662,7 @@ CREATE TABLE IF NOT EXISTS fl_loading_plan_item ( updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改人ID', updated_by_name VARCHAR(64) NULL COMMENT '最后修改人名称快照', updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) - COMMENT '更新时间', + COMMENT '装柜明细最后更新时间', version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', PRIMARY KEY (id), UNIQUE KEY uk_loading_item_plan_detail (loading_plan_id, active_inbound_detail_id), @@ -4689,7 +4689,7 @@ CREATE TABLE IF NOT EXISTS fl_loading_plan_item ( CREATE TABLE IF NOT EXISTS fl_container_document ( id BIGINT UNSIGNED NOT NULL COMMENT '货柜文件关联ID(雪花ID)', container_booking_id BIGINT UNSIGNED NOT NULL COMMENT '货柜预订ID', - document_type VARCHAR(32) NOT NULL COMMENT 'CUSTOMS_CLEARANCE、DELIVERY_ORDER、PICKUP_ORDER', + document_type VARCHAR(32) NOT NULL COMMENT '文件类型:CUSTOMS_CLEARANCE清关文件、DELIVERY_ORDER提货单、PICKUP_ORDER取货单', file_id BIGINT UNSIGNED NOT NULL COMMENT 'File服务文件ID,不建立跨库外键', file_name VARCHAR(255) NOT NULL COMMENT '文件名称快照', file_url VARCHAR(1000) NULL COMMENT '文件访问地址快照', @@ -4697,7 +4697,7 @@ CREATE TABLE IF NOT EXISTS fl_container_document ( uploaded_by_name VARCHAR(64) NULL COMMENT '上传操作人名称快照', 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 '更新时间', + COMMENT '货柜文件关联最后更新时间', version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', PRIMARY KEY (id), UNIQUE KEY uk_container_document_type (container_booking_id, document_type), @@ -4714,7 +4714,7 @@ CREATE TABLE IF NOT EXISTS fl_loading_plan_history ( loading_plan_item_id BIGINT UNSIGNED NULL COMMENT '装柜计划明细ID', operation_id VARCHAR(128) NOT NULL COMMENT '操作幂等ID', action_type VARCHAR(32) NOT NULL - COMMENT 'CREATE_PLAN、ADD_ITEM、ADJUST_ITEM、REMOVE_ITEM、START_LOADING、COMPLETE_LOADING、PICK_ITEM', + COMMENT '动作类型:CREATE_PLAN创建计划、ADD_ITEM新增明细、ADJUST_ITEM调整明细、REMOVE_ITEM移除明细、START_LOADING开始装柜、COMPLETE_LOADING完成装柜、PICK_ITEM拣货', from_status VARCHAR(24) NULL COMMENT '变更前状态', to_status VARCHAR(24) NULL COMMENT '变更后状态', before_snapshot JSON NULL COMMENT '变更前快照', @@ -4912,7 +4912,7 @@ CREATE TABLE IF NOT EXISTS fl_loading_pick_task ( id BIGINT UNSIGNED NOT NULL COMMENT 'PDA拣货任务ID', loading_plan_id BIGINT UNSIGNED NOT NULL COMMENT '装柜计划ID', container_no VARCHAR(64) NOT NULL COMMENT '集装箱号快照', - task_status VARCHAR(16) NOT NULL DEFAULT 'LOADING' COMMENT 'LOADING、COMPLETED', + task_status VARCHAR(16) NOT NULL DEFAULT 'LOADING' COMMENT 'PDA任务状态:LOADING拣货装柜中、COMPLETED已完成', started_by BIGINT UNSIGNED NOT NULL COMMENT '开始装柜人', started_by_name VARCHAR(64) NULL COMMENT '开始装柜人名称', started_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '开始时间', @@ -4935,10 +4935,10 @@ CREATE TABLE IF NOT EXISTS fl_loading_pick_item ( inbound_detail_id BIGINT UNSIGNED NOT NULL COMMENT '订单货物明细ID', planned_package_count INT UNSIGNED NOT NULL COMMENT '计划拣货箱数;上层拆分按业务规则向上取整', picked_package_count INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '已拣货箱数', - item_status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING、PICKING、PICKED、PARTIAL、SKIPPED', + item_status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT '拣货明细状态:PENDING待拣、PICKING拣货中、PICKED已拣完、PARTIAL部分拣货、SKIPPED跳拣', version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 'PDA拣货明细创建时间', + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT 'PDA拣货明细最后更新时间', PRIMARY KEY (id), UNIQUE KEY uk_loading_pick_item_plan_item (loading_plan_item_id), KEY idx_loading_pick_item_task (pick_task_id, item_status, id), @@ -4954,13 +4954,13 @@ CREATE TABLE IF NOT EXISTS fl_loading_pick_history ( pick_task_id BIGINT UNSIGNED NOT NULL COMMENT 'PDA拣货任务ID', pick_item_id BIGINT UNSIGNED NOT NULL COMMENT 'PDA拣货明细ID', operation_id VARCHAR(128) NOT NULL COMMENT '客户端幂等操作号', - request_hash CHAR(64) NOT NULL COMMENT '请求指纹', - action_type VARCHAR(24) NOT NULL DEFAULT 'PICK' COMMENT 'PICK、PARTIAL、SKIP', + request_hash CHAR(64) NOT NULL COMMENT '完整批量请求内容SHA-256摘要', + action_type VARCHAR(24) NOT NULL DEFAULT 'PICK' COMMENT '拣货动作:PICK正常拣货、PARTIAL部分拣货、SKIP跳拣', picked_package_count INT UNSIGNED NOT NULL COMMENT '本次拣货箱数', operation_reason VARCHAR(500) NULL COMMENT '部分拣货或跳拣原因', - operator_id BIGINT UNSIGNED NOT NULL COMMENT '操作人', - operator_name VARCHAR(64) NULL COMMENT '操作人名称', - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + operator_id BIGINT UNSIGNED NOT NULL COMMENT '执行PDA拣货操作的作业员ID', + operator_name VARCHAR(64) NULL COMMENT '作业员姓名快照', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '不可变拣货操作历史创建时间', PRIMARY KEY (id), UNIQUE KEY uk_loading_pick_history_operation (operation_id), KEY idx_loading_pick_history_item (pick_item_id, created_at), @@ -6819,7 +6819,7 @@ CALL unified_publish_fee_add_column('fl_inbound_fee_detail', 'chargeable_quantit CALL unified_publish_fee_add_column('fl_inbound_fee_detail', 'charge_unit', "VARCHAR(16) NULL COMMENT 'KG或CBM' AFTER chargeable_quantity"); CALL unified_publish_fee_add_column('fl_inbound_fee_detail', 'publication_status', - "VARCHAR(16) NOT NULL DEFAULT 'DRAFT' COMMENT 'DRAFT、PUBLISHED、SUPERSEDED' AFTER review_reason"); + "VARCHAR(16) NOT NULL DEFAULT 'DRAFT' COMMENT '费用发布状态:DRAFT草稿、PUBLISHED已发布、SUPERSEDED已替代' AFTER review_reason"); CALL unified_publish_fee_add_column('fl_inbound_fee_detail', 'published_by', "BIGINT NULL COMMENT '发布人ID' AFTER publication_status"); CALL unified_publish_fee_add_column('fl_inbound_fee_detail', 'published_by_name', @@ -6862,18 +6862,18 @@ USE baiyue_firstleg; CREATE TABLE IF NOT EXISTS fl_preload_plan ( id BIGINT UNSIGNED NOT NULL COMMENT '预装清单ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离ID', - inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单/订单ID', - status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/CONFIRMING/CONFIRMED', - version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本', - items_json JSON NOT NULL COMMENT '可编辑分柜明细快照', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + inbound_id BIGINT UNSIGNED NOT NULL COMMENT '关联入库单逻辑ID', + status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT '预装状态:PENDING待确认、CONFIRMING确认中、CONFIRMED已确认', + version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号,每次更新递增', + items_json JSON NOT NULL COMMENT '可编辑分柜明细JSON快照;人工确认后作为预装依据', remark VARCHAR(1000) NULL COMMENT '预装备注', - created_by BIGINT UNSIGNED NOT NULL, - created_by_name VARCHAR(64) NULL, - updated_by BIGINT UNSIGNED NOT NULL, - updated_by_name VARCHAR(64) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + created_by BIGINT UNSIGNED NOT NULL COMMENT '创建预装清单的操作人ID', + created_by_name VARCHAR(64) NULL COMMENT '创建操作人姓名快照', + updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改操作人ID', + updated_by_name VARCHAR(64) NULL COMMENT '最后修改操作人姓名快照', + 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 (id), UNIQUE KEY uk_preload_tenant_inbound (tenant_id, inbound_id), KEY idx_preload_status (tenant_id, status, updated_at), @@ -6882,60 +6882,123 @@ CREATE TABLE IF NOT EXISTS fl_preload_plan ( CREATE TABLE IF NOT EXISTS fl_split_operation ( id BIGINT UNSIGNED NOT NULL COMMENT '幂等记录ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离ID', - inbound_id BIGINT UNSIGNED NOT NULL, - operator_id BIGINT UNSIGNED NOT NULL, - operation VARCHAR(40) NOT NULL, - idempotency_key VARCHAR(128) NOT NULL, - status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING' COMMENT 'PROCESSING/SUCCESS', - result_json JSON NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + inbound_id BIGINT UNSIGNED NOT NULL COMMENT '操作目标入库单逻辑ID', + operator_id BIGINT UNSIGNED NOT NULL COMMENT '执行分柜或预装写操作的平台用户ID', + operation VARCHAR(40) NOT NULL COMMENT '分柜或预装写操作类型', + idempotency_key VARCHAR(128) NOT NULL COMMENT '客户端幂等键,同一操作重试复用', + status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING' COMMENT '幂等处理状态:PROCESSING处理中、SUCCESS成功', + result_json JSON NULL COMMENT '操作结果JSON快照,用于幂等重放', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '幂等操作声明创建时间', PRIMARY KEY (id), UNIQUE KEY uk_split_operation (tenant_id, inbound_id, operator_id, operation, idempotency_key), KEY idx_split_operation_time (tenant_id, inbound_id, created_at), CONSTRAINT fk_split_operation_inbound FOREIGN KEY (inbound_id) REFERENCES fl_inbound_order (id) ) ENGINE=InnoDB COMMENT='分柜操作幂等与审计索引'; +CREATE TABLE IF NOT EXISTS fl_inbound_split_detail ( + id BIGINT UNSIGNED NOT NULL COMMENT '部分锁定明细ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + parent_inbound_id BIGINT UNSIGNED NOT NULL COMMENT '主入库单ID', + child_inbound_id BIGINT UNSIGNED NULL COMMENT '完成拆单后生成的二级子入库单ID', + inbound_detail_id BIGINT UNSIGNED NOT NULL COMMENT '原订单货物明细ID,不建立跨库外键', + source_package_count INT UNSIGNED NOT NULL COMMENT '拆分前该明细总箱数快照', + locked_package_count INT UNSIGNED NOT NULL COMMENT '本次装柜锁定箱数', + remaining_package_count INT UNSIGNED GENERATED ALWAYS AS (source_package_count - locked_package_count) STORED COMMENT '锁定后剩余箱数,由来源箱数减锁定箱数生成', + status VARCHAR(16) NOT NULL DEFAULT 'LOCKED' COMMENT '分柜明细状态:LOCKED已锁定、COMPLETED已完成拆分', + operation_id VARCHAR(128) NOT NULL COMMENT '部分锁定操作幂等号', + created_by BIGINT UNSIGNED NOT NULL COMMENT '创建部分锁定明细的操作人ID', + created_by_name VARCHAR(64) NULL COMMENT '创建操作人姓名快照', + completed_by BIGINT UNSIGNED NULL COMMENT '完成拆分的操作人ID', + completed_by_name VARCHAR(64) NULL COMMENT '完成操作人姓名快照', + completed_at DATETIME(3) NULL COMMENT '部分拆分完成时间', + 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 '部分锁定明细最后更新时间', + version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号,每次更新递增', + PRIMARY KEY (id), + UNIQUE KEY uk_inbound_split_parent_detail (tenant_id, parent_inbound_id, inbound_detail_id), + UNIQUE KEY uk_inbound_split_child_detail (tenant_id, child_inbound_id, inbound_detail_id), + UNIQUE KEY uk_inbound_split_operation (operation_id), + KEY idx_inbound_split_status (tenant_id, parent_inbound_id, status), + CONSTRAINT fk_inbound_split_detail_parent FOREIGN KEY (parent_inbound_id) REFERENCES fl_inbound_order (id), + CONSTRAINT fk_inbound_split_detail_child FOREIGN KEY (child_inbound_id) REFERENCES fl_inbound_order (id), + CONSTRAINT chk_inbound_split_partial CHECK ( + source_package_count > 1 AND locked_package_count > 0 + AND locked_package_count < source_package_count + ), + CONSTRAINT chk_legacy_inbound_split_status CHECK (status IN ('LOCKED', 'COMPLETED')), + CONSTRAINT chk_inbound_split_completion CHECK ( + (status = 'LOCKED' AND child_inbound_id IS NULL AND completed_at IS NULL) + OR (status = 'COMPLETED' AND child_inbound_id IS NOT NULL AND completed_at IS NOT NULL) + ) +) ENGINE=InnoDB COMMENT='按箱数部分锁定及剩余箱数生成二级子入库单的不可变契约'; + +DROP TRIGGER IF EXISTS trg_inbound_split_detail_before_insert; +DROP TRIGGER IF EXISTS trg_inbound_split_detail_before_update; +DROP TRIGGER IF EXISTS trg_inbound_split_detail_before_delete; +DELIMITER $$ +CREATE TRIGGER trg_inbound_split_detail_before_insert +BEFORE INSERT ON fl_inbound_split_detail +FOR EACH ROW +BEGIN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '旧部分锁定台账已退役,请使用主子分片批次'; +END$$ + +CREATE TRIGGER trg_inbound_split_detail_before_update +BEFORE UPDATE ON fl_inbound_split_detail +FOR EACH ROW +BEGIN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '旧部分锁定台账只读,不允许更新'; +END$$ + +CREATE TRIGGER trg_inbound_split_detail_before_delete +BEFORE DELETE ON fl_inbound_split_detail +FOR EACH ROW +BEGIN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '部分锁定与拆单明细不可删除'; +END$$ +DELIMITER ; + CREATE TABLE IF NOT EXISTS fl_loading_evidence ( - id BIGINT UNSIGNED NOT NULL, - tenant_id BIGINT UNSIGNED NOT NULL, - inbound_id BIGINT UNSIGNED NOT NULL, - evidence_type VARCHAR(32) NOT NULL COMMENT 'PHOTO_STEP_1..4/SEAL', + id BIGINT UNSIGNED NOT NULL COMMENT '装柜证据记录ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + inbound_id BIGINT UNSIGNED NOT NULL COMMENT '关联入库单逻辑ID', + evidence_type VARCHAR(32) NOT NULL COMMENT 'PHOTO_STEP_1空仓/PHOTO_STEP_2半仓/PHOTO_STEP_3满仓/PHOTO_STEP_4锁仓/SEAL带锁码照', file_id BIGINT UNSIGNED NOT NULL COMMENT 'File 服务逻辑文件ID', - file_name VARCHAR(255) NULL, - seal_no VARCHAR(64) NULL, - remark VARCHAR(1000) NULL, - status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT 'ACTIVE/VOID', - void_reason VARCHAR(500) NULL, - voided_by BIGINT UNSIGNED NULL, - voided_by_name VARCHAR(64) NULL, - voided_at DATETIME(3) NULL, - version INT NOT NULL DEFAULT 0, - created_by BIGINT UNSIGNED NOT NULL, - created_by_name VARCHAR(64) NULL, - updated_by BIGINT UNSIGNED NOT NULL, - updated_by_name VARCHAR(64) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + file_name VARCHAR(255) NULL COMMENT '上传时文件名称快照', + seal_no VARCHAR(64) NULL COMMENT '锁码文本;带锁码照必填,客户不可见', + remark VARCHAR(1000) NULL COMMENT '证据补充说明', + status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '证据状态:ACTIVE有效、VOID已作废;错误照片保留记录不硬删除', + void_reason VARCHAR(500) NULL COMMENT '照片作废原因', + voided_by BIGINT UNSIGNED NULL COMMENT '执行作废的平台操作人ID', + voided_by_name VARCHAR(64) NULL COMMENT '执行作废的平台操作人姓名快照', + voided_at DATETIME(3) NULL COMMENT '照片作废时间', + version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号,每次更新递增', + created_by BIGINT UNSIGNED NOT NULL COMMENT '上传证据的操作人ID', + created_by_name VARCHAR(64) NULL COMMENT '上传证据的操作人姓名快照', + updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改操作人ID', + updated_by_name VARCHAR(64) NULL COMMENT '最后修改操作人姓名快照', + 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 (id), KEY idx_loading_evidence_type (tenant_id, inbound_id, evidence_type, status), KEY idx_loading_evidence_seal (tenant_id, seal_no, status), KEY idx_loading_evidence_time (tenant_id, inbound_id, updated_at), CONSTRAINT fk_loading_evidence_inbound FOREIGN KEY (inbound_id) REFERENCES fl_inbound_order (id) -) ENGINE=InnoDB COMMENT='订单装柜四步照片和铅封证据'; +) ENGINE=InnoDB COMMENT='订单装柜空仓/半仓/满仓/锁仓/带锁码照证据'; CREATE TABLE IF NOT EXISTS fl_loading_operation ( - id BIGINT UNSIGNED NOT NULL, - tenant_id BIGINT UNSIGNED NOT NULL, - inbound_id BIGINT UNSIGNED NOT NULL, - operator_id BIGINT UNSIGNED NOT NULL, - operation VARCHAR(48) NOT NULL, - idempotency_key VARCHAR(128) NOT NULL, - request_hash CHAR(64) NOT NULL, - status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING', - result_json JSON NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - completed_at DATETIME(3) NULL, + id BIGINT UNSIGNED NOT NULL COMMENT '装柜写操作记录ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + inbound_id BIGINT UNSIGNED NOT NULL COMMENT '操作目标入库单逻辑ID', + operator_id BIGINT UNSIGNED NOT NULL COMMENT '执行写操作的平台用户ID', + operation VARCHAR(48) NOT NULL COMMENT '装柜或证据写操作类型', + idempotency_key VARCHAR(128) NOT NULL COMMENT '客户端幂等键,同一操作重试复用', + request_hash CHAR(64) NOT NULL COMMENT '规范化请求内容SHA-256摘要', + status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING' COMMENT '幂等处理状态:PROCESSING处理中、SUCCESS成功、FAILED失败', + result_json JSON NULL COMMENT '成功结果或失败信息JSON快照,用于幂等重放', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '操作声明创建时间', + completed_at DATETIME(3) NULL COMMENT '操作完成时间', PRIMARY KEY (id), UNIQUE KEY uk_loading_operation (tenant_id, inbound_id, operator_id, operation, idempotency_key), KEY idx_loading_operation_time (tenant_id, inbound_id, created_at), @@ -6943,18 +7006,18 @@ CREATE TABLE IF NOT EXISTS fl_loading_operation ( ) ENGINE=InnoDB COMMENT='装柜与派送写操作幂等记录'; CREATE TABLE IF NOT EXISTS fl_inbound_loading_pick_task ( - id BIGINT UNSIGNED NOT NULL, - tenant_id BIGINT UNSIGNED NOT NULL, - inbound_id BIGINT UNSIGNED NOT NULL, - status VARCHAR(24) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/IN_PROGRESS/COMPLETED', - version INT NOT NULL DEFAULT 0, - expected_count INT UNSIGNED NULL, - picked_count INT UNSIGNED NULL, - remark VARCHAR(1000) NULL, - updated_by BIGINT UNSIGNED NOT NULL, - updated_by_name VARCHAR(64) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + id BIGINT UNSIGNED NOT NULL COMMENT '入库单级PDA拣货任务ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + inbound_id BIGINT UNSIGNED NOT NULL COMMENT '关联入库单逻辑ID', + status VARCHAR(24) NOT NULL DEFAULT 'PENDING' COMMENT '任务状态:PENDING待拣货、IN_PROGRESS拣货中、COMPLETED已完成', + version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号,每次更新递增', + expected_count INT UNSIGNED NULL COMMENT '计划应拣箱数', + picked_count INT UNSIGNED NULL COMMENT '实际已拣箱数', + remark VARCHAR(1000) NULL COMMENT '拣货结果或异常补充说明', + updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后操作人ID', + updated_by_name VARCHAR(64) NULL COMMENT '最后操作人姓名快照', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 'PDA拣货任务创建时间', + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT 'PDA拣货任务最后更新时间', PRIMARY KEY (id), UNIQUE KEY uk_loading_pick_tenant_inbound (tenant_id, inbound_id), KEY idx_loading_pick_status (tenant_id, status, updated_at), @@ -7223,7 +7286,7 @@ BEGIN END IF; END$$ DELIMITER ; -CALL phase3_order_add_column('ord_order','order_role',"VARCHAR(12) NOT NULL DEFAULT 'MASTER' COMMENT '订单层级:MASTER、CHILD'"); +CALL phase3_order_add_column('ord_order','order_role',"VARCHAR(12) NOT NULL DEFAULT 'MASTER' COMMENT '订单层级:MASTER主订单、CHILD子订单'"); CALL phase3_order_add_column('ord_order','parent_order_id','BIGINT UNSIGNED NULL COMMENT ''直属父订单ID;禁止子订单嵌套'''); CALL phase3_order_add_column('ord_order','root_order_id','BIGINT UNSIGNED NULL COMMENT ''根主订单ID'''); CALL phase3_order_add_column('ord_order','split_sequence','INT UNSIGNED NULL COMMENT ''同一父订单下的同级顺序'''); @@ -7259,26 +7322,26 @@ CALL phase3_order_parent_fk(); DROP PROCEDURE IF EXISTS phase3_order_parent_fk; CREATE TABLE IF NOT EXISTS ord_order_fulfillment_split ( - id BIGINT UNSIGNED NOT NULL, - tenant_id BIGINT UNSIGNED NOT NULL, - parent_order_id BIGINT UNSIGNED NOT NULL, - source_order_id BIGINT UNSIGNED NOT NULL, - child_order_id BIGINT UNSIGNED NOT NULL, - split_role VARCHAR(12) NOT NULL COMMENT 'LOADED或REMAINDER', - loading_plan_id BIGINT UNSIGNED NULL, - container_booking_id BIGINT UNSIGNED NULL, - source_package_count INT UNSIGNED NOT NULL, - loaded_package_count INT UNSIGNED NOT NULL, - remaining_package_count INT UNSIGNED NOT NULL, - source_detail_json JSON NULL, - operation_id VARCHAR(64) NOT NULL, - idempotency_key VARCHAR(128) NOT NULL, - request_hash CHAR(64) NOT NULL, - expected_parent_version INT NOT NULL, - created_by BIGINT UNSIGNED NOT NULL, - created_by_name VARCHAR(64) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - version INT NOT NULL DEFAULT 0, + id BIGINT UNSIGNED NOT NULL COMMENT '订单履约分片台账ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + parent_order_id BIGINT UNSIGNED NOT NULL COMMENT '直属父订单ID;主订单自身作为拆分根', + source_order_id BIGINT UNSIGNED NOT NULL COMMENT '实际被本次分片消费的来源订单ID', + child_order_id BIGINT UNSIGNED NOT NULL COMMENT '本次分片生成或承接的子订单ID', + split_role VARCHAR(12) NOT NULL COMMENT '分片角色:LOADED已装柜部分、REMAINDER剩余部分', + loading_plan_id BIGINT UNSIGNED NULL COMMENT 'Firstleg装柜计划逻辑ID', + container_booking_id BIGINT UNSIGNED NULL COMMENT 'Firstleg柜次逻辑ID', + source_package_count INT UNSIGNED NOT NULL COMMENT '来源订单本次分片前的总包装件数', + loaded_package_count INT UNSIGNED NOT NULL COMMENT '本次已装柜分片的包装件数', + remaining_package_count INT UNSIGNED NOT NULL COMMENT '本次分片后剩余包装件数', + source_detail_json JSON NULL COMMENT '来源货物明细及数量快照;来源订单变化不回写', + operation_id VARCHAR(64) NOT NULL COMMENT '履约分片业务操作ID', + idempotency_key VARCHAR(128) NOT NULL COMMENT '客户端幂等键', + request_hash CHAR(64) NOT NULL COMMENT '规范化请求内容SHA-256摘要', + expected_parent_version INT NOT NULL COMMENT '执行前预期父订单乐观锁版本号', + created_by BIGINT UNSIGNED NOT NULL COMMENT '创建该分片台账的用户ID', + created_by_name VARCHAR(64) NULL COMMENT '创建人名称快照', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '台账创建时间', + version INT NOT NULL DEFAULT 0 COMMENT '台账乐观锁版本号', PRIMARY KEY(id), UNIQUE KEY uk_order_split_operation (tenant_id,operation_id,split_role), UNIQUE KEY uk_order_split_idempotency (tenant_id,parent_order_id,idempotency_key,split_role), @@ -7326,7 +7389,7 @@ BEGIN END$$ DELIMITER ; -CALL phase3_order_add_column('ord_order_fulfillment_split','split_role',"VARCHAR(12) NOT NULL DEFAULT 'LOADED' COMMENT 'LOADED或REMAINDER'"); +CALL phase3_order_add_column('ord_order_fulfillment_split','split_role',"VARCHAR(12) NOT NULL DEFAULT 'LOADED' COMMENT '分片角色:LOADED已装柜部分、REMAINDER剩余部分'"); CALL phase3_order_add_column('ord_order_fulfillment_split','source_order_id','BIGINT UNSIGNED NULL COMMENT ''实际被消费的来源订单ID'''); CALL phase3_order_add_column('ord_order_fulfillment_split','request_hash',"CHAR(64) NULL COMMENT '请求内容SHA-256;存量记录允许为空'"); -- 旧版台账没有来源字段;当时只允许从根主订单拆分,因此可无歧义回填为父订单。 @@ -7611,19 +7674,19 @@ DROP PROCEDURE IF EXISTS phase3_inbound_add_index; CREATE TABLE IF NOT EXISTS fl_inbound_split_batch ( id BIGINT UNSIGNED NOT NULL COMMENT '拆分批次ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', parent_inbound_id BIGINT UNSIGNED NOT NULL COMMENT '主入仓单ID', operation_id VARCHAR(64) NOT NULL COMMENT '业务操作幂等号', idempotency_key VARCHAR(128) NOT NULL COMMENT '请求幂等键', expected_parent_version INT NOT NULL COMMENT '操作时主单乐观锁版本', package_count INT UNSIGNED NOT NULL COMMENT '本批次拆分箱数', - status VARCHAR(16) NOT NULL DEFAULT 'PREPARED' COMMENT 'PREPARED、APPLIED、CANCELLED', + status VARCHAR(16) NOT NULL DEFAULT 'PREPARED' COMMENT '拆分批次状态:PREPARED已准备、APPLIED已生效、CANCELLED已取消', remark VARCHAR(500) NULL COMMENT '拆分原因或备注', created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID', created_by_name VARCHAR(64) NULL COMMENT '创建人名称快照', - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '拆分批次创建时间', applied_at DATETIME(3) NULL COMMENT '批次生效时间', - version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本', + version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号,每次更新递增', PRIMARY KEY (id), UNIQUE KEY uk_inbound_split_operation (tenant_id, operation_id), UNIQUE KEY uk_inbound_split_idempotency (tenant_id, parent_inbound_id, idempotency_key), @@ -7635,7 +7698,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_split_batch ( CREATE TABLE IF NOT EXISTS fl_inbound_split_item ( id BIGINT UNSIGNED NOT NULL COMMENT '拆分明细ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', split_batch_id BIGINT UNSIGNED NOT NULL COMMENT '拆分批次ID', parent_inbound_id BIGINT UNSIGNED NOT NULL COMMENT '主入仓单ID', child_inbound_id BIGINT UNSIGNED NOT NULL COMMENT '子入仓单ID', @@ -7644,7 +7707,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_split_item ( source_measurement_id BIGINT UNSIGNED NOT NULL COMMENT '来源测量批次ID', source_measurement_item_id BIGINT UNSIGNED NOT NULL COMMENT '来源测量箱规明细ID', package_count INT UNSIGNED NOT NULL COMMENT '分配给子单的箱数', - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '拆分数量台账创建时间', PRIMARY KEY (id), UNIQUE KEY uk_inbound_split_batch_line (tenant_id, split_batch_id, line_no), UNIQUE KEY uk_inbound_split_batch_source_child (tenant_id, split_batch_id, source_measurement_item_id, child_inbound_id), @@ -7659,16 +7722,16 @@ CREATE TABLE IF NOT EXISTS fl_inbound_split_item ( CREATE TABLE IF NOT EXISTS fl_inbound_split_history ( id BIGINT UNSIGNED NOT NULL COMMENT '拆分历史ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', split_batch_id BIGINT UNSIGNED NOT NULL COMMENT '拆分批次ID', operation_id VARCHAR(64) NOT NULL COMMENT '历史操作幂等号', - action_type VARCHAR(24) NOT NULL COMMENT 'CREATE、APPLY、CANCEL', + action_type VARCHAR(24) NOT NULL COMMENT '拆分动作:CREATE创建批次、APPLY生效、CANCEL取消', before_snapshot JSON NULL COMMENT '操作前快照', after_snapshot JSON NOT NULL COMMENT '操作后快照', operator_id BIGINT UNSIGNED NOT NULL COMMENT '操作人ID', operator_name VARCHAR(64) NULL COMMENT '操作人名称快照', operation_reason VARCHAR(500) NULL COMMENT '操作原因', - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '不可变拆分历史创建时间', PRIMARY KEY (id), UNIQUE KEY uk_inbound_split_history_operation (tenant_id, operation_id), KEY idx_inbound_split_history (tenant_id, split_batch_id, created_at, id), @@ -8292,42 +8355,42 @@ SET NAMES utf8mb4; USE baiyue_firstleg; CREATE TABLE IF NOT EXISTS fl_ocean_booking ( - id BIGINT UNSIGNED NOT NULL, - booking_no VARCHAR(40) NOT NULL, - status VARCHAR(24) NOT NULL DEFAULT 'DRAFT', - customer_id BIGINT UNSIGNED NOT NULL COMMENT '当前产品客户ID', - customer_name VARCHAR(128) NOT NULL, - bill_of_lading_no VARCHAR(128) NULL, - shipping_company_id BIGINT UNSIGNED NULL, - shipping_company_name VARCHAR(128) NULL, - booking_agent_id BIGINT UNSIGNED NULL, - booking_agent_name VARCHAR(128) NULL, - vessel_name VARCHAR(128) NULL, - voyage_no VARCHAR(64) NULL, - departure_port_code VARCHAR(32) NULL, - departure_port_name VARCHAR(128) NULL, - destination_port_code VARCHAR(32) NULL, - destination_port_name VARCHAR(128) NULL, - place_of_delivery VARCHAR(128) NULL, - service_term VARCHAR(32) NULL, - freight_term VARCHAR(32) NULL, - estimated_sailing_at DATETIME(3) NULL, - estimated_arrival_at DATETIME(3) NULL, - pending_vessel_name VARCHAR(128) NULL, - pending_voyage_no VARCHAR(64) NULL, - pending_estimated_sailing_at DATETIME(3) NULL, - pending_estimated_arrival_at DATETIME(3) NULL, - change_reason VARCHAR(1000) NULL, - failure_reason VARCHAR(1000) NULL, - cancellation_reason VARCHAR(1000) NULL, - remark VARCHAR(1000) NULL, - created_by BIGINT UNSIGNED NOT NULL, - created_by_name VARCHAR(64) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_by BIGINT UNSIGNED NOT NULL, - updated_by_name VARCHAR(64) NULL, - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - version INT UNSIGNED NOT NULL DEFAULT 0, + id BIGINT UNSIGNED NOT NULL COMMENT '海运订舱ID', + booking_no VARCHAR(40) NOT NULL COMMENT '平台海运订舱编号', + status VARCHAR(24) NOT NULL DEFAULT 'DRAFT' COMMENT '订舱状态:DRAFT草稿、SUBMITTED已提交、CONFIRMED已确认、RELEASED已放舱、FAILED失败、CHANGE_PENDING船期待变更、CANCELLED已取消', + customer_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + customer_name VARCHAR(128) NOT NULL COMMENT '客户名称快照', + bill_of_lading_no VARCHAR(128) NULL COMMENT '提单号', + shipping_company_id BIGINT UNSIGNED NULL COMMENT '船公司逻辑ID快照', + shipping_company_name VARCHAR(128) NULL COMMENT '船公司名称快照', + booking_agent_id BIGINT UNSIGNED NULL COMMENT '订舱代理逻辑ID快照', + booking_agent_name VARCHAR(128) NULL COMMENT '订舱代理名称快照', + vessel_name VARCHAR(128) NULL COMMENT '当前确认船名快照', + voyage_no VARCHAR(64) NULL COMMENT '当前确认航次号快照', + departure_port_code VARCHAR(32) NULL COMMENT '起运港编码快照', + departure_port_name VARCHAR(128) NULL COMMENT '起运港名称快照', + destination_port_code VARCHAR(32) NULL COMMENT '目的港编码快照', + destination_port_name VARCHAR(128) NULL COMMENT '目的港名称快照', + place_of_delivery VARCHAR(128) NULL COMMENT '最终交货地快照', + service_term VARCHAR(32) NULL COMMENT '运输服务条款快照', + freight_term VARCHAR(32) NULL COMMENT '海运费支付条款快照', + estimated_sailing_at DATETIME(3) NULL COMMENT '当前确认预计开船时间', + estimated_arrival_at DATETIME(3) NULL COMMENT '当前确认预计到港时间', + pending_vessel_name VARCHAR(128) NULL COMMENT '待确认变更船名', + pending_voyage_no VARCHAR(64) NULL COMMENT '待确认变更航次号', + pending_estimated_sailing_at DATETIME(3) NULL COMMENT '待确认变更预计开船时间', + pending_estimated_arrival_at DATETIME(3) NULL COMMENT '待确认变更预计到港时间', + change_reason VARCHAR(1000) NULL COMMENT '船期变更原因', + failure_reason VARCHAR(1000) NULL COMMENT '订舱失败原因', + cancellation_reason VARCHAR(1000) NULL COMMENT '取消订舱原因', + remark VARCHAR(1000) NULL COMMENT '平台内部备注', + created_by BIGINT UNSIGNED NOT NULL COMMENT '创建操作人ID', + created_by_name VARCHAR(64) NULL COMMENT '创建操作人姓名快照', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '订舱记录创建时间', + updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改操作人ID', + updated_by_name VARCHAR(64) NULL COMMENT '最后修改操作人姓名快照', + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '订舱记录最后更新时间', + version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', PRIMARY KEY (id), UNIQUE KEY uk_ocean_booking_no (booking_no), KEY idx_ocean_booking_customer (customer_id,status,updated_at,id), @@ -8343,13 +8406,13 @@ CREATE TABLE IF NOT EXISTS fl_ocean_booking ( ) ENGINE=InnoDB COMMENT='独立海运订舱聚合;柜次仍由fl_container_booking承载'; CREATE TABLE IF NOT EXISTS fl_ocean_booking_party ( - id BIGINT UNSIGNED NOT NULL, - ocean_booking_id BIGINT UNSIGNED NOT NULL, - party_type VARCHAR(24) NOT NULL COMMENT 'SHIPPER/CONSIGNEE/NOTIFY_PARTY', - party_name VARCHAR(255) NOT NULL, - address VARCHAR(1000) NULL, - contact_name VARCHAR(128) NULL, - contact_phone VARCHAR(64) NULL, + id BIGINT UNSIGNED NOT NULL COMMENT '订舱提单关系方ID', + ocean_booking_id BIGINT UNSIGNED NOT NULL COMMENT '所属海运订舱ID', + party_type VARCHAR(24) NOT NULL COMMENT '关系方类型:SHIPPER发货人、CONSIGNEE收货人、NOTIFY_PARTY通知人', + party_name VARCHAR(255) NOT NULL COMMENT '关系方名称快照', + address VARCHAR(1000) NULL COMMENT '关系方地址快照', + contact_name VARCHAR(128) NULL COMMENT '关系方联系人快照', + contact_phone VARCHAR(64) NULL COMMENT '关系方联系电话快照', PRIMARY KEY (id), UNIQUE KEY uk_ocean_booking_party (ocean_booking_id,party_type), CONSTRAINT fk_ocean_booking_party FOREIGN KEY (ocean_booking_id) REFERENCES fl_ocean_booking(id), @@ -8357,12 +8420,12 @@ CREATE TABLE IF NOT EXISTS fl_ocean_booking_party ( ) ENGINE=InnoDB COMMENT='订舱提单关系方快照'; CREATE TABLE IF NOT EXISTS fl_ocean_booking_container_requirement ( - id BIGINT UNSIGNED NOT NULL, - ocean_booking_id BIGINT UNSIGNED NOT NULL, - container_type VARCHAR(32) NOT NULL, - container_kind VARCHAR(32) NOT NULL, - quantity INT UNSIGNED NOT NULL, - sort_no INT UNSIGNED NOT NULL DEFAULT 0, + id BIGINT UNSIGNED NOT NULL COMMENT '订舱箱型需求ID', + ocean_booking_id BIGINT UNSIGNED NOT NULL COMMENT '所属海运订舱ID', + container_type VARCHAR(32) NOT NULL COMMENT '所需集装箱尺寸类型编码', + container_kind VARCHAR(32) NOT NULL COMMENT '所需集装箱箱种编码', + quantity INT UNSIGNED NOT NULL COMMENT '该箱型箱种所需柜数', + sort_no INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '订舱需求展示顺序', PRIMARY KEY (id), KEY idx_ocean_requirement_booking (ocean_booking_id,sort_no,id), CONSTRAINT fk_ocean_requirement_booking FOREIGN KEY (ocean_booking_id) REFERENCES fl_ocean_booking(id), @@ -8370,13 +8433,13 @@ CREATE TABLE IF NOT EXISTS fl_ocean_booking_container_requirement ( ) ENGINE=InnoDB COMMENT='订舱箱型箱种及数量需求'; CREATE TABLE IF NOT EXISTS fl_ocean_booking_container_allocation ( - id BIGINT UNSIGNED NOT NULL, - ocean_booking_id BIGINT UNSIGNED NOT NULL, - requirement_id BIGINT UNSIGNED NOT NULL, + id BIGINT UNSIGNED NOT NULL COMMENT '订舱柜次分配关系ID', + ocean_booking_id BIGINT UNSIGNED NOT NULL COMMENT '所属海运订舱ID', + requirement_id BIGINT UNSIGNED NOT NULL COMMENT '对应订舱箱型需求ID', container_booking_id BIGINT UNSIGNED NOT NULL COMMENT '明确引用现有柜次ID', - allocated_by BIGINT UNSIGNED NOT NULL, - allocated_by_name VARCHAR(64) NULL, - allocated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + allocated_by BIGINT UNSIGNED NOT NULL COMMENT '分配操作人ID', + allocated_by_name VARCHAR(64) NULL COMMENT '分配操作人姓名快照', + allocated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '柜次分配时间', PRIMARY KEY (id), UNIQUE KEY uk_ocean_allocation_container (container_booking_id), KEY idx_ocean_allocation_booking (ocean_booking_id,requirement_id,allocated_at), @@ -8387,33 +8450,33 @@ CREATE TABLE IF NOT EXISTS fl_ocean_booking_container_allocation ( ) ENGINE=InnoDB COMMENT='订舱需求与现有柜次的唯一分配关系'; CREATE TABLE IF NOT EXISTS fl_ocean_booking_status_history ( - id BIGINT UNSIGNED NOT NULL, - ocean_booking_id BIGINT UNSIGNED NOT NULL, - from_status VARCHAR(24) NULL, - to_status VARCHAR(24) NOT NULL, - action VARCHAR(40) NOT NULL, - reason VARCHAR(1000) NULL, - before_snapshot JSON NULL, - after_snapshot JSON NOT NULL, - operator_id BIGINT UNSIGNED NOT NULL, - operator_name VARCHAR(64) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + id BIGINT UNSIGNED NOT NULL COMMENT '订舱状态历史ID', + ocean_booking_id BIGINT UNSIGNED NOT NULL COMMENT '所属海运订舱ID', + from_status VARCHAR(24) NULL COMMENT '变更前订舱状态;创建时为空', + to_status VARCHAR(24) NOT NULL COMMENT '变更后订舱状态', + action VARCHAR(40) NOT NULL COMMENT '触发状态变更的业务动作', + reason VARCHAR(1000) NULL COMMENT '状态变更原因', + before_snapshot JSON NULL COMMENT '状态变更前订舱数据快照;创建时为空', + after_snapshot JSON NOT NULL COMMENT '状态变更后订舱数据快照', + operator_id BIGINT UNSIGNED NOT NULL COMMENT '操作人ID', + operator_name VARCHAR(64) NULL COMMENT '操作人姓名快照', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '状态历史创建时间', PRIMARY KEY (id), KEY idx_ocean_history_booking (ocean_booking_id,created_at,id), CONSTRAINT fk_ocean_history_booking FOREIGN KEY (ocean_booking_id) REFERENCES fl_ocean_booking(id) ) ENGINE=InnoDB COMMENT='海运订舱不可变状态历史'; CREATE TABLE IF NOT EXISTS fl_ocean_booking_operation ( - id BIGINT UNSIGNED NOT NULL, - ocean_booking_id BIGINT UNSIGNED NULL COMMENT '创建请求完成前允许为空', - operator_id BIGINT UNSIGNED NOT NULL, - operation VARCHAR(40) NOT NULL, - idempotency_key VARCHAR(128) NOT NULL, - request_hash CHAR(64) NOT NULL, - status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING', - result_json JSON NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - completed_at DATETIME(3) NULL, + id BIGINT UNSIGNED NOT NULL COMMENT '订舱写操作幂等记录ID', + ocean_booking_id BIGINT UNSIGNED NULL COMMENT '关联海运订舱ID;创建请求完成前允许为空', + operator_id BIGINT UNSIGNED NOT NULL COMMENT '发起写操作的平台操作人ID', + operation VARCHAR(40) NOT NULL COMMENT '幂等写操作类型', + idempotency_key VARCHAR(128) NOT NULL COMMENT '客户端写操作幂等键', + request_hash CHAR(64) NOT NULL COMMENT '规范化请求内容SHA-256摘要;用于识别同键不同请求', + status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING' COMMENT '处理状态:PROCESSING处理中、SUCCESS成功', + result_json JSON NULL COMMENT '成功结果JSON快照;供幂等重放返回', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '幂等记录创建时间', + completed_at DATETIME(3) NULL COMMENT '写操作成功完成时间', PRIMARY KEY (id), UNIQUE KEY uk_ocean_operation (operator_id,operation,idempotency_key), KEY idx_ocean_operation_booking (ocean_booking_id,created_at), @@ -8545,34 +8608,34 @@ USE baiyue_firstleg; SET NAMES utf8mb4; CREATE TABLE IF NOT EXISTS fl_truck_appointment ( - id BIGINT UNSIGNED NOT NULL, appointment_no VARCHAR(40) NOT NULL, outbound_order_id BIGINT UNSIGNED NULL, - ocean_booking_id BIGINT UNSIGNED NULL, container_booking_id BIGINT UNSIGNED NULL, loading_plan_id BIGINT UNSIGNED NULL, - warehouse_id BIGINT UNSIGNED NULL, customer_id BIGINT UNSIGNED NULL, customer_name VARCHAR(128) NULL, - carrier_id BIGINT UNSIGNED NULL, carrier_name VARCHAR(128) NULL, fleet_contact VARCHAR(64) NULL, fleet_phone VARCHAR(32) NULL, - driver_name VARCHAR(64) NULL, driver_phone VARCHAR(32) NULL, driver_id_card VARCHAR(32) NULL, vehicle_no VARCHAR(32) NULL, - store_name VARCHAR(128) NULL, store_contact VARCHAR(64) NULL, store_phone VARCHAR(32) NULL, store_address VARCHAR(255) NULL, - store_city VARCHAR(64) NULL, store_zipcode VARCHAR(16) NULL, package_count INT UNSIGNED NULL, gross_weight_kg DECIMAL(18,3) NULL, - volume_cbm DECIMAL(18,4) NULL, pallet_count INT UNSIGNED NULL, po_no VARCHAR(255) NULL, planned_loading_at DATETIME(3) NULL, - actual_arrived_warehouse_at DATETIME(3) NULL, actual_loading_at DATETIME(3) NULL, actual_departed_at DATETIME(3) NULL, transit_minutes INT UNSIGNED NULL, payable_amount DECIMAL(18,2) NULL, receivable_amount DECIMAL(18,2) NULL, - currency VARCHAR(8) NOT NULL DEFAULT 'RMB', status VARCHAR(32) NOT NULL DEFAULT 'DRAFT', revision INT UNSIGNED NOT NULL DEFAULT 0, - version INT UNSIGNED NOT NULL DEFAULT 0, remark VARCHAR(1000) NULL, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), PRIMARY KEY(id), UNIQUE KEY uk_truck_appointment_no(appointment_no), + id BIGINT UNSIGNED NOT NULL COMMENT '拖车预约ID', appointment_no VARCHAR(40) NOT NULL COMMENT '平台拖车预约编号', outbound_order_id BIGINT UNSIGNED NULL COMMENT '关联头程订单逻辑ID;不跨库建外键', + ocean_booking_id BIGINT UNSIGNED NULL COMMENT '关联海运订舱逻辑ID', container_booking_id BIGINT UNSIGNED NULL COMMENT '关联柜次逻辑ID;拖车完成供装柜门禁校验', loading_plan_id BIGINT UNSIGNED NULL COMMENT '关联装柜计划逻辑ID', + warehouse_id BIGINT UNSIGNED NULL COMMENT '预约到仓的国内收货仓逻辑ID', customer_id BIGINT UNSIGNED NULL COMMENT '客户ID和数据隔离键', customer_name VARCHAR(128) NULL COMMENT '客户名称快照', + carrier_id BIGINT UNSIGNED NULL COMMENT '拖车服务商逻辑ID', carrier_name VARCHAR(128) NULL COMMENT '拖车服务商名称快照', fleet_contact VARCHAR(64) NULL COMMENT '车队联系人', fleet_phone VARCHAR(32) NULL COMMENT '车队联系电话', + driver_name VARCHAR(64) NULL COMMENT '司机姓名', driver_phone VARCHAR(32) NULL COMMENT '司机联系电话', driver_id_card VARCHAR(32) NULL COMMENT '司机身份证号;平台内部敏感信息', vehicle_no VARCHAR(32) NULL COMMENT '车辆车牌号', + store_name VARCHAR(128) NULL COMMENT '到仓装柜地点名称快照', store_contact VARCHAR(64) NULL COMMENT '到仓地点联系人快照', store_phone VARCHAR(32) NULL COMMENT '到仓地点联系电话快照', store_address VARCHAR(255) NULL COMMENT '到仓装柜地点地址快照', + store_city VARCHAR(64) NULL COMMENT '到仓地点城市快照', store_zipcode VARCHAR(16) NULL COMMENT '到仓地点邮编快照', package_count INT UNSIGNED NULL COMMENT '预约货物包装件数快照', gross_weight_kg DECIMAL(18,3) NULL COMMENT '预约货物毛重,单位千克', + volume_cbm DECIMAL(18,4) NULL COMMENT '预约货物体积,单位立方米', pallet_count INT UNSIGNED NULL COMMENT '预约托盘数量', po_no VARCHAR(255) NULL COMMENT '关联采购单号或客户PO号', planned_loading_at DATETIME(3) NULL COMMENT '预约到仓装柜时间', + actual_arrived_warehouse_at DATETIME(3) NULL COMMENT '车辆实际到达收货仓时间', actual_loading_at DATETIME(3) NULL COMMENT '实际开始装柜时间', actual_departed_at DATETIME(3) NULL COMMENT '车辆实际离仓时间;DEPARTED后供LOADED门禁使用', transit_minutes INT UNSIGNED NULL COMMENT '从到仓到离仓的运输作业耗时,单位分钟', payable_amount DECIMAL(18,2) NULL COMMENT '应付拖车服务商金额;币种由currency指定', receivable_amount DECIMAL(18,2) NULL COMMENT '向客户收取的拖车金额;币种由currency指定', + currency VARCHAR(8) NOT NULL DEFAULT 'RMB' COMMENT '金额币种;首期默认RMB', status VARCHAR(32) NOT NULL DEFAULT 'DRAFT' COMMENT '拖车预约状态:DRAFT草稿、PENDING_DISPATCH待派车、DISPATCHED已派车、PICKUP_IN_PROGRESS提货中、ARRIVED_WAREHOUSE已到仓、LOADING装柜中、DEPARTED已离仓、COMPLETED已完成、RESCHEDULED已改约、EXCEPTION异常、CANCELLED已取消', revision INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '拖车预约业务修订号;每次状态或改约递增', + version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', remark VARCHAR(1000) NULL COMMENT '平台内部备注或异常说明', 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(id), UNIQUE KEY uk_truck_appointment_no(appointment_no), KEY idx_truck_appointment_status(status,updated_at), KEY idx_truck_appointment_container(container_booking_id), KEY idx_truck_appointment_customer(customer_id), CONSTRAINT chk_truck_appointment_status CHECK(status IN ('DRAFT','PENDING_DISPATCH','DISPATCHED','PICKUP_IN_PROGRESS','ARRIVED_WAREHOUSE','LOADING','DEPARTED','COMPLETED','RESCHEDULED','EXCEPTION','CANCELLED')) ) ENGINE=InnoDB COMMENT='三期拖车预约派车聚合'; CREATE TABLE IF NOT EXISTS fl_truck_appointment_history ( - id BIGINT UNSIGNED NOT NULL, appointment_id BIGINT UNSIGNED NOT NULL, revision INT UNSIGNED NOT NULL, action VARCHAR(32) NOT NULL, - from_status VARCHAR(32) NULL, to_status VARCHAR(32) NULL, reason VARCHAR(500) NULL, operator_id BIGINT UNSIGNED NOT NULL, - operator_name VARCHAR(64) NULL, request_id VARCHAR(128) NULL, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), PRIMARY KEY(id), KEY idx_truck_history(appointment_id,revision) + id BIGINT UNSIGNED NOT NULL COMMENT '拖车预约历史ID', appointment_id BIGINT UNSIGNED NOT NULL COMMENT '所属拖车预约ID', revision INT UNSIGNED NOT NULL COMMENT '该预约业务修订号', action VARCHAR(32) NOT NULL COMMENT '触发状态或改约的业务动作', + from_status VARCHAR(32) NULL COMMENT '变更前拖车状态;创建时为空', to_status VARCHAR(32) NULL COMMENT '变更后拖车状态;改约时可保持原状态', reason VARCHAR(500) NULL COMMENT '状态或改约原因', operator_id BIGINT UNSIGNED NOT NULL COMMENT '执行操作的平台用户ID', + operator_name VARCHAR(64) NULL COMMENT '操作人姓名快照', request_id VARCHAR(128) NULL COMMENT '请求链路中的请求ID', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '历史记录创建时间', PRIMARY KEY(id), KEY idx_truck_history(appointment_id,revision) ) ENGINE=InnoDB COMMENT='拖车状态与改约历史'; CREATE TABLE IF NOT EXISTS fl_truck_appointment_attachment ( - id BIGINT UNSIGNED NOT NULL, appointment_id BIGINT UNSIGNED NOT NULL, file_id BIGINT UNSIGNED NOT NULL, attachment_type VARCHAR(32) NOT NULL, - file_name VARCHAR(255) NULL, file_hash CHAR(64) NULL, visibility VARCHAR(24) NOT NULL DEFAULT 'INTERNAL', uploaded_by BIGINT UNSIGNED NOT NULL, - uploaded_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), PRIMARY KEY(id), UNIQUE KEY uk_truck_attachment(appointment_id,file_id), KEY idx_truck_attachment(appointment_id) + id BIGINT UNSIGNED NOT NULL COMMENT '拖车附件关系ID', appointment_id BIGINT UNSIGNED NOT NULL COMMENT '所属拖车预约ID', file_id BIGINT UNSIGNED NOT NULL COMMENT 'File服务文件逻辑ID', attachment_type VARCHAR(32) NOT NULL COMMENT '附件类型编码;由业务动作约定', + file_name VARCHAR(255) NULL COMMENT '上传时文件名快照', file_hash CHAR(64) NULL COMMENT '文件内容SHA-256摘要', visibility VARCHAR(24) NOT NULL DEFAULT 'INTERNAL' COMMENT '可见范围:INTERNAL仅平台内部、CLIENT客户可见', uploaded_by BIGINT UNSIGNED NOT NULL COMMENT '上传操作人ID', + uploaded_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '附件上传时间', PRIMARY KEY(id), UNIQUE KEY uk_truck_attachment(appointment_id,file_id), KEY idx_truck_attachment(appointment_id) ) ENGINE=InnoDB COMMENT='拖车附件文件引用'; CREATE TABLE IF NOT EXISTS fl_truck_appointment_operation ( - id BIGINT UNSIGNED NOT NULL, appointment_id BIGINT UNSIGNED NOT NULL, idempotency_key VARCHAR(128) NOT NULL, request_hash CHAR(64) NOT NULL, - action VARCHAR(32) NOT NULL, response_snapshot JSON NULL, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), PRIMARY KEY(id), UNIQUE KEY uk_truck_operation(idempotency_key), KEY idx_truck_operation_appointment(appointment_id) + id BIGINT UNSIGNED NOT NULL COMMENT '拖车写操作幂等记录ID', appointment_id BIGINT UNSIGNED NOT NULL COMMENT '所属拖车预约ID', idempotency_key VARCHAR(128) NOT NULL COMMENT '客户端写操作幂等键', request_hash CHAR(64) NOT NULL COMMENT '规范化请求内容SHA-256摘要;用于识别同键不同请求', + action VARCHAR(32) NOT NULL COMMENT '执行的拖车业务动作', response_snapshot JSON NULL COMMENT '成功响应JSON快照;供相同幂等请求重放', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '幂等记录创建时间', PRIMARY KEY(id), UNIQUE KEY uk_truck_operation(idempotency_key), KEY idx_truck_operation_appointment(appointment_id) ) ENGINE=InnoDB COMMENT='拖车操作幂等记录'; DROP PROCEDURE IF EXISTS phase3_truck_add_column; @@ -8699,22 +8762,22 @@ DROP PROCEDURE IF EXISTS assert_phase3_service_center_schema; USE baiyue_firstleg; CREATE TABLE IF NOT EXISTS fl_outbound_dispatch ( - id BIGINT UNSIGNED NOT NULL, - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离ID', - order_id BIGINT UNSIGNED NOT NULL COMMENT '订单引用,不复制订单主数据', - inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单主对象', - container_booking_id BIGINT UNSIGNED NOT NULL COMMENT '柜次引用', - status VARCHAR(32) NOT NULL DEFAULT 'READY_FOR_DISPATCH', - version INT UNSIGNED NOT NULL DEFAULT 0, - confirmed_by BIGINT UNSIGNED NULL, - confirmed_by_name VARCHAR(64) NULL, - confirmed_at DATETIME(3) NULL, - reversed_by BIGINT UNSIGNED NULL, - reversed_by_name VARCHAR(64) NULL, - reversed_at DATETIME(3) NULL, - remark VARCHAR(500) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + id BIGINT UNSIGNED NOT NULL COMMENT '国内出仓调度记录ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + order_id BIGINT UNSIGNED NOT NULL COMMENT 'Order服务订单逻辑ID,不复制订单主数据', + inbound_id BIGINT UNSIGNED NOT NULL COMMENT '关联入库单逻辑ID', + container_booking_id BIGINT UNSIGNED NOT NULL COMMENT '关联柜次逻辑ID', + status VARCHAR(32) NOT NULL DEFAULT 'READY_FOR_DISPATCH' COMMENT '出仓状态:READY_FOR_DISPATCH待确认出仓、WAITING_FOR_LOADING等待装柜', + version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号,每次更新递增', + confirmed_by BIGINT UNSIGNED NULL COMMENT '确认国内出仓的操作人ID', + confirmed_by_name VARCHAR(64) NULL COMMENT '确认国内出仓的操作人姓名快照', + confirmed_at DATETIME(3) NULL COMMENT '国内出仓确认时间', + reversed_by BIGINT UNSIGNED NULL COMMENT '撤销国内出仓的操作人ID', + reversed_by_name VARCHAR(64) NULL COMMENT '撤销国内出仓的操作人姓名快照', + reversed_at DATETIME(3) NULL COMMENT '国内出仓撤销时间', + remark VARCHAR(500) NULL COMMENT '国内出仓调度备注', + 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 (id), UNIQUE KEY uk_outbound_dispatch_owner (tenant_id,inbound_id,container_booking_id), KEY idx_outbound_dispatch_order (tenant_id,order_id,updated_at), @@ -8725,33 +8788,33 @@ CREATE TABLE IF NOT EXISTS fl_outbound_dispatch ( ) ENGINE=InnoDB COMMENT='三期国内出仓派车状态,不重复创建出仓单'; CREATE TABLE IF NOT EXISTS fl_outbound_dispatch_history ( - id BIGINT UNSIGNED NOT NULL, - dispatch_id BIGINT UNSIGNED NOT NULL, - tenant_id BIGINT UNSIGNED NOT NULL, - inbound_id BIGINT UNSIGNED NOT NULL, - container_booking_id BIGINT UNSIGNED NOT NULL, - action VARCHAR(32) NOT NULL, - from_status VARCHAR(32) NULL, - to_status VARCHAR(32) NOT NULL, - remark VARCHAR(500) NULL, - operator_id BIGINT UNSIGNED NOT NULL, - operator_name VARCHAR(64) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + id BIGINT UNSIGNED NOT NULL COMMENT '出仓状态历史记录ID', + dispatch_id BIGINT UNSIGNED NOT NULL COMMENT '关联国内出仓调度记录ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + inbound_id BIGINT UNSIGNED NOT NULL COMMENT '关联入库单逻辑ID快照', + container_booking_id BIGINT UNSIGNED NOT NULL COMMENT '关联柜次逻辑ID快照', + action VARCHAR(32) NOT NULL COMMENT '出仓状态变更动作', + from_status VARCHAR(32) NULL COMMENT '变更前出仓状态;首次创建时为空', + to_status VARCHAR(32) NOT NULL COMMENT '变更后出仓状态', + remark VARCHAR(500) NULL COMMENT '状态变更原因或备注', + operator_id BIGINT UNSIGNED NOT NULL COMMENT '执行状态变更的平台用户ID', + operator_name VARCHAR(64) NULL COMMENT '执行状态变更的平台用户姓名快照', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '状态历史创建时间', PRIMARY KEY (id), KEY idx_outbound_dispatch_history (tenant_id,inbound_id,container_booking_id,created_at,id), CONSTRAINT fk_outbound_dispatch_history FOREIGN KEY (dispatch_id) REFERENCES fl_outbound_dispatch(id) ) ENGINE=InnoDB COMMENT='国内出仓追加式审计历史'; CREATE TABLE IF NOT EXISTS fl_outbound_dispatch_operation ( - id BIGINT UNSIGNED NOT NULL, + id BIGINT UNSIGNED NOT NULL COMMENT '出仓幂等操作记录ID', dispatch_id BIGINT UNSIGNED NULL COMMENT '首次声明幂等键时允许为空', - operator_id BIGINT UNSIGNED NOT NULL, - operation VARCHAR(32) NOT NULL, - idempotency_key VARCHAR(128) NOT NULL, - request_hash CHAR(64) NOT NULL, - status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING', - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - completed_at DATETIME(3) NULL, + operator_id BIGINT UNSIGNED NOT NULL COMMENT '执行写操作的平台用户ID', + operation VARCHAR(32) NOT NULL COMMENT '国内出仓写操作类型', + idempotency_key VARCHAR(128) NOT NULL COMMENT '客户端幂等键,同一操作重试复用', + request_hash CHAR(64) NOT NULL COMMENT '规范化请求内容SHA-256摘要', + status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING' COMMENT '幂等处理状态:PROCESSING处理中、SUCCESS成功', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '幂等声明创建时间', + completed_at DATETIME(3) NULL COMMENT '幂等操作完成时间', PRIMARY KEY (id), UNIQUE KEY uk_outbound_dispatch_operation (operator_id,operation,idempotency_key), KEY idx_outbound_dispatch_operation_resource (dispatch_id,created_at), @@ -8810,35 +8873,35 @@ USE baiyue_firstleg; USE baiyue_firstleg; CREATE TABLE IF NOT EXISTS fl_port_schedule ( - id BIGINT UNSIGNED NOT NULL, - schedule_no VARCHAR(40) NOT NULL, - customer_id BIGINT UNSIGNED NOT NULL COMMENT '当前产品客户ID', - ocean_booking_id BIGINT UNSIGNED NOT NULL, - container_booking_id BIGINT UNSIGNED NULL, - source_type VARCHAR(24) NOT NULL COMMENT 'MANUAL/CARRIER/TERMINAL/API', - source_name VARCHAR(128) NOT NULL, - source_ref VARCHAR(255) NOT NULL, - vessel_name VARCHAR(128) NOT NULL, - voyage_no VARCHAR(64) NOT NULL, - departure_port_code VARCHAR(32) NOT NULL, - departure_port_name VARCHAR(128) NULL, - destination_port_code VARCHAR(32) NOT NULL, - destination_port_name VARCHAR(128) NULL, - etd_at DATETIME(3) NULL, - eta_at DATETIME(3) NULL, + id BIGINT UNSIGNED NOT NULL COMMENT '港务船期记录ID', + schedule_no VARCHAR(40) NOT NULL COMMENT '港务船期记录编号', + customer_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + ocean_booking_id BIGINT UNSIGNED NOT NULL COMMENT '关联海运订舱ID', + container_booking_id BIGINT UNSIGNED NULL COMMENT '关联柜次ID;订舱整票船期时为空', + source_type VARCHAR(24) NOT NULL COMMENT '船期来源类型:MANUAL人工录入、CARRIER船公司、TERMINAL码头、API外部接口', + source_name VARCHAR(128) NOT NULL COMMENT '船期来源名称', + source_ref VARCHAR(255) NOT NULL COMMENT '来源侧记录唯一引用或查询凭据摘要', + vessel_name VARCHAR(128) NOT NULL COMMENT '船名快照', + voyage_no VARCHAR(64) NOT NULL COMMENT '航次号快照', + departure_port_code VARCHAR(32) NOT NULL COMMENT '起运港编码快照', + departure_port_name VARCHAR(128) NULL COMMENT '起运港名称快照', + destination_port_code VARCHAR(32) NOT NULL COMMENT '目的港编码快照', + destination_port_name VARCHAR(128) NULL COMMENT '目的港名称快照', + etd_at DATETIME(3) NULL COMMENT '预计离港时间', + eta_at DATETIME(3) NULL COMMENT '预计到港时间', booking_cutoff_at DATETIME(3) NULL COMMENT '截单时间', customs_cutoff_at DATETIME(3) NULL COMMENT '截关时间', port_cutoff_at DATETIME(3) NULL COMMENT '截港时间', observed_at DATETIME(3) NOT NULL COMMENT '来源数据观测时间', expires_at DATETIME(3) NOT NULL COMMENT '超过该时间即由API计算为过期', - remark VARCHAR(1000) NULL, - created_by BIGINT UNSIGNED NOT NULL, - created_by_name VARCHAR(64) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_by BIGINT UNSIGNED NOT NULL, - updated_by_name VARCHAR(64) NULL, - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - version INT UNSIGNED NOT NULL DEFAULT 0, + remark VARCHAR(1000) NULL COMMENT '平台内部备注', + created_by BIGINT UNSIGNED NOT NULL COMMENT '创建操作人ID', + created_by_name VARCHAR(64) NULL COMMENT '创建操作人姓名快照', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '船期记录创建时间', + updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改操作人ID', + updated_by_name VARCHAR(64) NULL COMMENT '最后修改操作人姓名快照', + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '船期记录最后更新时间', + version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', PRIMARY KEY (id), UNIQUE KEY uk_port_schedule_no (schedule_no), UNIQUE KEY uk_port_schedule_source (source_type,source_name,source_ref), @@ -8861,17 +8924,17 @@ CREATE TABLE IF NOT EXISTS fl_port_schedule ( ) ENGINE=InnoDB COMMENT='独立港务船期来源聚合,不替代订舱快照'; CREATE TABLE IF NOT EXISTS fl_port_schedule_history ( - id BIGINT UNSIGNED NOT NULL, - port_schedule_id BIGINT UNSIGNED NOT NULL, - action VARCHAR(24) NOT NULL, - from_version INT UNSIGNED NULL, - to_version INT UNSIGNED NOT NULL, - reason VARCHAR(1000) NULL, - before_snapshot JSON NULL, - after_snapshot JSON NOT NULL, - operator_id BIGINT UNSIGNED NOT NULL, - operator_name VARCHAR(64) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + id BIGINT UNSIGNED NOT NULL COMMENT '港务船期变更历史ID', + port_schedule_id BIGINT UNSIGNED NOT NULL COMMENT '所属港务船期记录ID', + action VARCHAR(24) NOT NULL COMMENT '变更动作:CREATE创建、UPDATE更新', + from_version INT UNSIGNED NULL COMMENT '变更前版本;创建时为空', + to_version INT UNSIGNED NOT NULL COMMENT '变更后版本', + reason VARCHAR(1000) NULL COMMENT '变更原因或说明', + before_snapshot JSON NULL COMMENT '变更前船期数据快照;创建时为空', + after_snapshot JSON NOT NULL COMMENT '变更后船期数据快照', + operator_id BIGINT UNSIGNED NOT NULL COMMENT '操作人ID', + operator_name VARCHAR(64) NULL COMMENT '操作人姓名快照', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '历史记录创建时间', PRIMARY KEY (id), KEY idx_port_schedule_history (port_schedule_id,created_at,id), CONSTRAINT fk_port_schedule_history FOREIGN KEY (port_schedule_id) REFERENCES fl_port_schedule(id), @@ -8879,16 +8942,16 @@ CREATE TABLE IF NOT EXISTS fl_port_schedule_history ( ) ENGINE=InnoDB COMMENT='港务船期不可变变更历史'; CREATE TABLE IF NOT EXISTS fl_port_schedule_operation ( - id BIGINT UNSIGNED NOT NULL, - port_schedule_id BIGINT UNSIGNED NULL COMMENT '创建请求完成前允许为空', - operator_id BIGINT UNSIGNED NOT NULL, - operation VARCHAR(24) NOT NULL, - idempotency_key VARCHAR(128) NOT NULL, - request_hash CHAR(64) NOT NULL, - status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING', - result_json JSON NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - completed_at DATETIME(3) NULL, + id BIGINT UNSIGNED NOT NULL COMMENT '港务船期写操作幂等记录ID', + port_schedule_id BIGINT UNSIGNED NULL COMMENT '关联港务船期ID;创建请求完成前允许为空', + operator_id BIGINT UNSIGNED NOT NULL COMMENT '发起写操作的平台操作人ID', + operation VARCHAR(24) NOT NULL COMMENT '幂等写操作类型', + idempotency_key VARCHAR(128) NOT NULL COMMENT '客户端写操作幂等键', + request_hash CHAR(64) NOT NULL COMMENT '规范化请求内容SHA-256摘要;用于识别同键不同请求', + status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING' COMMENT '处理状态:PROCESSING处理中、SUCCESS成功', + result_json JSON NULL COMMENT '成功结果JSON快照;供幂等重放返回', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '幂等记录创建时间', + completed_at DATETIME(3) NULL COMMENT '写操作成功完成时间', PRIMARY KEY (id), UNIQUE KEY uk_port_schedule_operation (operator_id,idempotency_key), KEY idx_port_schedule_operation_resource (port_schedule_id,created_at), @@ -8979,25 +9042,25 @@ USE baiyue_firstleg; USE baiyue_firstleg; CREATE TABLE IF NOT EXISTS fl_customs_checklist_version ( - id BIGINT UNSIGNED NOT NULL, checklist_code VARCHAR(40) NOT NULL, version_no VARCHAR(32) NOT NULL, - country_code VARCHAR(32) NOT NULL, channel_code VARCHAR(40) NOT NULL, cargo_type VARCHAR(40) NOT NULL, - items_json JSON NOT NULL, status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE', created_by BIGINT UNSIGNED NOT NULL, - created_by_name VARCHAR(64) NULL, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), PRIMARY KEY (id), + id BIGINT UNSIGNED NOT NULL COMMENT '报关资料清单版本逻辑ID', checklist_code VARCHAR(40) NOT NULL COMMENT '报关资料清单编码;同一清单的多个版本共用', version_no VARCHAR(32) NOT NULL COMMENT '清单业务版本号', + country_code VARCHAR(32) NOT NULL COMMENT '清单适用的目的国家或地区编码', channel_code VARCHAR(40) NOT NULL COMMENT '清单适用的运输渠道编码', cargo_type VARCHAR(40) NOT NULL COMMENT '清单适用的货物类型编码', + items_json JSON NOT NULL COMMENT '不可变清单项目数组;元素包含code项目编码、name项目名称和required是否必填', status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '清单版本状态:ACTIVE启用、RETIRED停用', created_by BIGINT UNSIGNED NOT NULL COMMENT '创建清单版本的平台操作人ID', + created_by_name VARCHAR(64) NULL COMMENT '创建清单版本时的平台操作人名称快照', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '清单版本创建时间', PRIMARY KEY (id), UNIQUE KEY uk_customs_checklist_version (checklist_code,version_no), KEY idx_customs_checklist_match (country_code,channel_code,cargo_type,status), CONSTRAINT chk_customs_checklist_status CHECK (status IN ('ACTIVE','RETIRED')) ) ENGINE=InnoDB COMMENT='三期报关资料清单不可变版本'; CREATE TABLE IF NOT EXISTS fl_customs_case ( - id BIGINT UNSIGNED NOT NULL, customs_no VARCHAR(40) NOT NULL, tenant_id BIGINT UNSIGNED NOT NULL, - order_id BIGINT UNSIGNED NOT NULL, inbound_id BIGINT UNSIGNED NOT NULL, - container_booking_id BIGINT UNSIGNED NOT NULL DEFAULT 0, checklist_version_id BIGINT UNSIGNED NOT NULL, - checklist_code VARCHAR(40) NOT NULL, checklist_version_no VARCHAR(32) NOT NULL, - status VARCHAR(32) NOT NULL DEFAULT 'DOCUMENTS_PENDING', remark VARCHAR(500) NULL, - created_by BIGINT UNSIGNED NOT NULL, created_by_name VARCHAR(64) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), updated_by BIGINT UNSIGNED NOT NULL, - updated_by_name VARCHAR(64) NULL, updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - version INT UNSIGNED NOT NULL DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY uk_customs_no (customs_no), + id BIGINT UNSIGNED NOT NULL COMMENT '报关协同单逻辑ID', customs_no VARCHAR(40) NOT NULL COMMENT '全局唯一报关协同单号', tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + order_id BIGINT UNSIGNED NOT NULL COMMENT 'Order服务订单逻辑ID;跨库不建立外键', inbound_id BIGINT UNSIGNED NOT NULL COMMENT 'Firstleg入库单逻辑ID', + container_booking_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '关联柜次逻辑ID;0表示创建时尚未关联柜次', checklist_version_id BIGINT UNSIGNED NOT NULL COMMENT '本单冻结使用的报关资料清单版本ID', + checklist_code VARCHAR(40) NOT NULL COMMENT '创建协同单时的清单编码快照;源清单变化不回写', checklist_version_no VARCHAR(32) NOT NULL COMMENT '创建协同单时的清单版本号快照;源清单变化不回写', + status VARCHAR(32) NOT NULL DEFAULT 'DOCUMENTS_PENDING' COMMENT '协同状态:DOCUMENTS_PENDING待补资料、REVIEWING审核中、SUPPLEMENT_REQUIRED需补件、READY_TO_SUBMIT待提交代理、RECEIPT_PENDING待回执、RECEIPT_AVAILABLE已有回执、CLEARANCE_COMPLETED已放行、CANCELLED已取消', remark VARCHAR(500) NULL COMMENT '报关协同单内部业务备注', + created_by BIGINT UNSIGNED NOT NULL COMMENT '创建协同单的平台操作人ID', created_by_name VARCHAR(64) NULL COMMENT '创建协同单时的平台操作人名称快照', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '协同单创建时间', updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改协同单的平台操作人ID', + updated_by_name VARCHAR(64) NULL COMMENT '最后修改时的平台操作人名称快照', updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '协同单最后修改时间', + version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号;每次状态变更递增', PRIMARY KEY (id), UNIQUE KEY uk_customs_no (customs_no), UNIQUE KEY uk_customs_case_scope (tenant_id,order_id,inbound_id,container_booking_id), KEY idx_customs_case_status (status,updated_at), KEY idx_customs_case_container (container_booking_id), CONSTRAINT fk_customs_case_checklist FOREIGN KEY (checklist_version_id) REFERENCES fl_customs_checklist_version(id), @@ -9005,13 +9068,13 @@ CREATE TABLE IF NOT EXISTS fl_customs_case ( ) ENGINE=InnoDB COMMENT='三期订单报关协同聚合'; CREATE TABLE IF NOT EXISTS fl_customs_document ( - id BIGINT UNSIGNED NOT NULL, customs_case_id BIGINT UNSIGNED NOT NULL, checklist_item_code VARCHAR(40) NOT NULL, - file_id BIGINT UNSIGNED NOT NULL, document_version INT UNSIGNED NOT NULL, replaces_document_id BIGINT UNSIGNED NULL, - file_name VARCHAR(255) NOT NULL, file_hash CHAR(64) NOT NULL, status VARCHAR(16) NOT NULL DEFAULT 'UPLOADED', - visibility VARCHAR(16) NOT NULL DEFAULT 'CUSTOMER', uploaded_by BIGINT UNSIGNED NOT NULL, - uploaded_by_type VARCHAR(16) NOT NULL, uploaded_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - reviewed_by BIGINT UNSIGNED NULL, reviewed_by_name VARCHAR(64) NULL, reviewed_at DATETIME(3) NULL, - review_reason VARCHAR(500) NULL, PRIMARY KEY (id), + id BIGINT UNSIGNED NOT NULL COMMENT '报关资料文件版本逻辑ID', customs_case_id BIGINT UNSIGNED NOT NULL COMMENT '所属报关协同单ID', checklist_item_code VARCHAR(40) NOT NULL COMMENT '对应冻结清单中的资料项目编码', + file_id BIGINT UNSIGNED NOT NULL COMMENT 'File服务文件逻辑ID;跨库不建立外键', document_version INT UNSIGNED NOT NULL COMMENT '同一协同单和清单项目下的文件版本序号', replaces_document_id BIGINT UNSIGNED NULL COMMENT '被本版本替换的上一资料版本ID;首个版本为空', + file_name VARCHAR(255) NOT NULL COMMENT '上传或引用时的文件名称快照', file_hash CHAR(64) NOT NULL COMMENT '文件内容SHA-256摘要;用于版本审计和幂等校验', status VARCHAR(16) NOT NULL DEFAULT 'UPLOADED' COMMENT '资料状态:UPLOADED待审核、APPROVED审核通过、RETURNED已退回、SUPERSEDED已被替换、FROZEN已随代理提交批次冻结', + visibility VARCHAR(16) NOT NULL DEFAULT 'CUSTOMER' COMMENT '资料可见范围:PLATFORM仅平台可见、CUSTOMER客户可见', uploaded_by BIGINT UNSIGNED NOT NULL COMMENT '上传或引用资料的操作人ID', + uploaded_by_type VARCHAR(16) NOT NULL COMMENT '上传主体类型:PLATFORM平台操作人、MERCHANT客户操作人', uploaded_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '资料上传或引用时间', + reviewed_by BIGINT UNSIGNED NULL COMMENT '审核资料的平台操作人ID;未审核时为空', reviewed_by_name VARCHAR(64) NULL COMMENT '审核时的平台操作人名称快照', reviewed_at DATETIME(3) NULL COMMENT '资料审核时间', + review_reason VARCHAR(500) NULL COMMENT '审核退回原因或审核备注', PRIMARY KEY (id), UNIQUE KEY uk_customs_document_version (customs_case_id,checklist_item_code,document_version), UNIQUE KEY uk_customs_document_file (customs_case_id,file_id), KEY idx_customs_document_case (customs_case_id,status), CONSTRAINT fk_customs_document_case FOREIGN KEY (customs_case_id) REFERENCES fl_customs_case(id), @@ -9021,33 +9084,33 @@ CREATE TABLE IF NOT EXISTS fl_customs_document ( ) ENGINE=InnoDB COMMENT='报关客户文件版本引用'; CREATE TABLE IF NOT EXISTS fl_customs_supplement_request ( - id BIGINT UNSIGNED NOT NULL, customs_case_id BIGINT UNSIGNED NOT NULL, source_document_id BIGINT UNSIGNED NOT NULL, - resolved_document_id BIGINT UNSIGNED NULL, checklist_item_code VARCHAR(40) NOT NULL, reason VARCHAR(500) NOT NULL, - due_at DATETIME(3) NULL, status VARCHAR(16) NOT NULL DEFAULT 'OPEN', requested_by BIGINT UNSIGNED NOT NULL, - requested_by_name VARCHAR(64) NULL, requested_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - resolved_by BIGINT UNSIGNED NULL, resolved_at DATETIME(3) NULL, PRIMARY KEY (id), + id BIGINT UNSIGNED NOT NULL COMMENT '报关补件请求逻辑ID', customs_case_id BIGINT UNSIGNED NOT NULL COMMENT '所属报关协同单ID', source_document_id BIGINT UNSIGNED NOT NULL COMMENT '触发本次补件的被退回资料版本ID', + resolved_document_id BIGINT UNSIGNED NULL COMMENT '解决本次补件的新资料版本ID;未解决时为空', checklist_item_code VARCHAR(40) NOT NULL COMMENT '需要补充的报关资料项目编码', reason VARCHAR(500) NOT NULL COMMENT '平台审核人员填写的补件原因', + due_at DATETIME(3) NULL COMMENT '期望完成补件时间;未设置时为空', status VARCHAR(16) NOT NULL DEFAULT 'OPEN' COMMENT '补件状态:OPEN待补件、RESOLVED已由新资料解决、CANCELLED已取消', requested_by BIGINT UNSIGNED NOT NULL COMMENT '发起补件的平台操作人ID', + requested_by_name VARCHAR(64) NULL COMMENT '发起补件时的平台操作人名称快照', requested_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '补件请求发起时间', + resolved_by BIGINT UNSIGNED NULL COMMENT '提交解决资料的操作人ID;未解决时为空', resolved_at DATETIME(3) NULL COMMENT '补件请求解决时间', PRIMARY KEY (id), KEY idx_customs_supplement_case (customs_case_id,status,due_at), CONSTRAINT fk_customs_supplement_case FOREIGN KEY (customs_case_id) REFERENCES fl_customs_case(id), CONSTRAINT chk_customs_supplement_status CHECK (status IN ('OPEN','RESOLVED','CANCELLED')) ) ENGINE=InnoDB COMMENT='报关补件请求'; CREATE TABLE IF NOT EXISTS fl_customs_submission_batch ( - id BIGINT UNSIGNED NOT NULL, customs_case_id BIGINT UNSIGNED NOT NULL, batch_no VARCHAR(40) NOT NULL, - batch_version INT UNSIGNED NOT NULL, customs_agent_name VARCHAR(128) NOT NULL, external_reference VARCHAR(128) NULL, - file_snapshot_json JSON NOT NULL, status VARCHAR(16) NOT NULL DEFAULT 'SUBMITTED', note VARCHAR(500) NULL, - submitted_by BIGINT UNSIGNED NOT NULL, submitted_by_name VARCHAR(64) NULL, - submitted_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), PRIMARY KEY (id), + id BIGINT UNSIGNED NOT NULL COMMENT '报关代理提交批次逻辑ID', customs_case_id BIGINT UNSIGNED NOT NULL COMMENT '所属报关协同单ID', batch_no VARCHAR(40) NOT NULL COMMENT '全局唯一代理提交批次号', + batch_version INT UNSIGNED NOT NULL COMMENT '同一报关协同单内的提交批次版本序号', customs_agent_name VARCHAR(128) NOT NULL COMMENT '本批次接收报关资料的报关代理名称快照', external_reference VARCHAR(128) NULL COMMENT '报关代理或外部系统返回的业务参考号', + file_snapshot_json JSON NOT NULL COMMENT '提交时冻结的已审核资料数组;元素包含documentId、fileId、itemCode、documentVersion和fileHash', status VARCHAR(16) NOT NULL DEFAULT 'SUBMITTED' COMMENT '代理提交状态:SUBMITTED已提交、ACKNOWLEDGED代理已确认、REJECTED代理已拒绝、COMPLETED代理处理完成', note VARCHAR(500) NULL COMMENT '提交报关代理时的平台备注', + submitted_by BIGINT UNSIGNED NOT NULL COMMENT '提交报关代理的平台操作人ID', submitted_by_name VARCHAR(64) NULL COMMENT '提交时的平台操作人名称快照', + submitted_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '提交报关代理时间', PRIMARY KEY (id), UNIQUE KEY uk_customs_batch_no (batch_no), UNIQUE KEY uk_customs_batch_version (customs_case_id,batch_version), CONSTRAINT fk_customs_batch_case FOREIGN KEY (customs_case_id) REFERENCES fl_customs_case(id), CONSTRAINT chk_customs_batch_status CHECK (status IN ('SUBMITTED','ACKNOWLEDGED','REJECTED','COMPLETED')) ) ENGINE=InnoDB COMMENT='报关代理提交批次及冻结文件版本'; CREATE TABLE IF NOT EXISTS fl_customs_receipt ( - id BIGINT UNSIGNED NOT NULL, customs_case_id BIGINT UNSIGNED NOT NULL, submission_batch_id BIGINT UNSIGNED NOT NULL, - receipt_type VARCHAR(16) NOT NULL, file_id BIGINT UNSIGNED NOT NULL, receipt_version INT UNSIGNED NOT NULL, - replaces_receipt_id BIGINT UNSIGNED NULL, file_name VARCHAR(255) NOT NULL, file_hash CHAR(64) NOT NULL, - visibility VARCHAR(16) NOT NULL DEFAULT 'PLATFORM', note VARCHAR(500) NULL, uploaded_by BIGINT UNSIGNED NOT NULL, - uploaded_by_name VARCHAR(64) NULL, uploaded_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), PRIMARY KEY (id), + id BIGINT UNSIGNED NOT NULL COMMENT '报关回执文件版本逻辑ID', customs_case_id BIGINT UNSIGNED NOT NULL COMMENT '所属报关协同单ID', submission_batch_id BIGINT UNSIGNED NOT NULL COMMENT '产生本回执的报关代理提交批次ID', + receipt_type VARCHAR(16) NOT NULL COMMENT '回执类型:RECEIVED已收件、DECLARED已申报、REJECTED已拒绝、INSPECTION查验通知、RELEASED放行回执、OTHER其他回执', file_id BIGINT UNSIGNED NOT NULL COMMENT 'File服务回执文件逻辑ID;跨库不建立外键', receipt_version INT UNSIGNED NOT NULL COMMENT '同一提交批次和回执类型下的版本序号', + replaces_receipt_id BIGINT UNSIGNED NULL COMMENT '被本版本替换的上一回执版本ID;首个版本为空', file_name VARCHAR(255) NOT NULL COMMENT '上传或引用时的回执文件名称快照', file_hash CHAR(64) NOT NULL COMMENT '回执文件内容SHA-256摘要;用于版本审计和幂等校验', + visibility VARCHAR(16) NOT NULL DEFAULT 'PLATFORM' COMMENT '回执可见范围:PLATFORM仅平台可见、CUSTOMER客户可查询下载', note VARCHAR(500) NULL COMMENT '回执业务备注', uploaded_by BIGINT UNSIGNED NOT NULL COMMENT '上传或引用回执的平台操作人ID', + uploaded_by_name VARCHAR(64) NULL COMMENT '上传回执时的平台操作人名称快照', uploaded_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '回执上传或引用时间', PRIMARY KEY (id), UNIQUE KEY uk_customs_receipt_version (submission_batch_id,receipt_type,receipt_version), UNIQUE KEY uk_customs_receipt_file (submission_batch_id,file_id), KEY idx_customs_receipt_case (customs_case_id,visibility,uploaded_at), @@ -9058,21 +9121,21 @@ CREATE TABLE IF NOT EXISTS fl_customs_receipt ( ) ENGINE=InnoDB COMMENT='报关代理回执版本与客户可见范围'; CREATE TABLE IF NOT EXISTS fl_customs_history ( - id BIGINT UNSIGNED NOT NULL, customs_case_id BIGINT UNSIGNED NOT NULL, action VARCHAR(40) NOT NULL, - from_status VARCHAR(32) NULL, to_status VARCHAR(32) NULL, resource_type VARCHAR(32) NOT NULL, - resource_id BIGINT UNSIGNED NOT NULL, note VARCHAR(500) NULL, operator_id BIGINT UNSIGNED NOT NULL, - operator_name VARCHAR(64) NULL, operator_type VARCHAR(16) NOT NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), PRIMARY KEY (id), + id BIGINT UNSIGNED NOT NULL COMMENT '报关协同历史逻辑ID', customs_case_id BIGINT UNSIGNED NOT NULL COMMENT '所属报关协同单ID', action VARCHAR(40) NOT NULL COMMENT '业务动作编码,如CREATE、UPLOAD_DOCUMENT、REPLACE_DOCUMENT、REVIEW_DOCUMENT_APPROVE、REVIEW_DOCUMENT_RETURN、SUBMIT_AGENT_BATCH或UPLOAD_RECEIPT', + from_status VARCHAR(32) NULL COMMENT '动作发生前的协同状态;创建动作时为空', to_status VARCHAR(32) NULL COMMENT '动作完成后的协同状态', resource_type VARCHAR(32) NOT NULL COMMENT '动作关联资源类型:CASE协同单、DOCUMENT资料、SUBMISSION_BATCH提交批次或RECEIPT回执', + resource_id BIGINT UNSIGNED NOT NULL COMMENT '动作关联资源逻辑ID', note VARCHAR(500) NULL COMMENT '动作原因、资料项目编码或业务备注快照', operator_id BIGINT UNSIGNED NOT NULL COMMENT '执行业务动作的操作人ID', + operator_name VARCHAR(64) NULL COMMENT '执行动作时的操作人名称快照', operator_type VARCHAR(16) NOT NULL COMMENT '操作主体类型:PLATFORM平台操作人、MERCHANT客户操作人', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '历史记录创建时间', PRIMARY KEY (id), KEY idx_customs_history_case (customs_case_id,created_at,id), CONSTRAINT fk_customs_history_case FOREIGN KEY (customs_case_id) REFERENCES fl_customs_case(id), CONSTRAINT chk_customs_history_actor CHECK (operator_type IN ('PLATFORM','MERCHANT')) ) ENGINE=InnoDB COMMENT='报关协同稳定历史'; CREATE TABLE IF NOT EXISTS fl_customs_operation ( - id BIGINT UNSIGNED NOT NULL, operator_id BIGINT UNSIGNED NOT NULL, action VARCHAR(64) NOT NULL, - idempotency_key VARCHAR(128) NOT NULL, request_hash CHAR(64) NOT NULL, - status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING', result_type VARCHAR(24) NULL, result_id BIGINT UNSIGNED NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), completed_at DATETIME(3) NULL, PRIMARY KEY (id), + id BIGINT UNSIGNED NOT NULL COMMENT '报关写命令幂等记录逻辑ID', operator_id BIGINT UNSIGNED NOT NULL COMMENT '发起写命令的操作人ID;与动作和幂等键共同限定重放范围', action VARCHAR(64) NOT NULL COMMENT '幂等业务动作编码,包含平台或客户动作及资料、审核、提交、回执类型', + idempotency_key VARCHAR(128) NOT NULL COMMENT '客户端提供的幂等键', request_hash CHAR(64) NOT NULL COMMENT '规范化完整请求内容的SHA-256摘要;同键不同请求时拒绝', + status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING' COMMENT '幂等处理状态:PROCESSING处理中、SUCCESS处理成功', result_type VARCHAR(24) NULL COMMENT '成功结果资源类型:CHECKLIST、CASE、DOCUMENT、BATCH或RECEIPT', result_id BIGINT UNSIGNED NULL COMMENT '成功结果资源逻辑ID;处理中为空', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '幂等记录创建时间', completed_at DATETIME(3) NULL COMMENT '写命令成功完成时间;处理中为空', PRIMARY KEY (id), UNIQUE KEY uk_customs_operation (operator_id,action,idempotency_key), CONSTRAINT chk_customs_operation_status CHECK (status IN ('PROCESSING','SUCCESS')) ) ENGINE=InnoDB COMMENT='报关协同命令幂等记录'; @@ -9182,16 +9245,16 @@ PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; /* Phase 3: link inbound preload plans to one or more container loading plans. */ CREATE TABLE IF NOT EXISTS fl_preload_loading_plan_link ( - id BIGINT UNSIGNED NOT NULL, - tenant_id BIGINT UNSIGNED NOT NULL, - inbound_id BIGINT UNSIGNED NOT NULL, - preload_plan_id BIGINT UNSIGNED NOT NULL, - loading_plan_id BIGINT UNSIGNED NOT NULL, - idempotency_key VARCHAR(128) NOT NULL, - request_hash CHAR(64) NOT NULL, - created_by BIGINT UNSIGNED NOT NULL, - created_by_name VARCHAR(64) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + id BIGINT UNSIGNED NOT NULL COMMENT '关联ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '入库单客户隔离ID', + inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID', + preload_plan_id BIGINT UNSIGNED NOT NULL COMMENT '入库单级预装计划ID', + loading_plan_id BIGINT UNSIGNED NOT NULL COMMENT '柜次级装柜计划ID', + idempotency_key VARCHAR(128) NOT NULL COMMENT '建立关联幂等键', + request_hash CHAR(64) NOT NULL COMMENT '建立关联请求SHA-256', + created_by BIGINT UNSIGNED NOT NULL COMMENT '建立预装与装柜计划关联的操作人ID', + created_by_name VARCHAR(64) NULL COMMENT '建立关联的操作人姓名快照', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '关联记录创建时间', PRIMARY KEY (id), UNIQUE KEY uk_preload_loading_pair (tenant_id, inbound_id, loading_plan_id), UNIQUE KEY uk_preload_loading_idempotency (created_by, idempotency_key), @@ -9220,15 +9283,15 @@ SELECT CAST(CONV(SUBSTRING(SHA2(CONCAT('PRELOAD_LOADING|', item.account_id, '|', /* Phase 3: PDA assignment, immutable task operations and review records. */ CREATE TABLE IF NOT EXISTS fl_loading_pick_assignment ( - pick_task_id BIGINT UNSIGNED NOT NULL, - assignee_id BIGINT UNSIGNED NOT NULL, - assignee_name VARCHAR(64) NOT NULL, - assigned_by BIGINT UNSIGNED NOT NULL, - assigned_by_name VARCHAR(64) NULL, - assignment_type VARCHAR(16) NOT NULL, - version INT UNSIGNED NOT NULL DEFAULT 0, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + pick_task_id BIGINT UNSIGNED NOT NULL COMMENT '柜次PDA拣货任务ID;同一任务仅保留一名作业员', + assignee_id BIGINT UNSIGNED NOT NULL COMMENT '当前唯一作业员ID', + assignee_name VARCHAR(64) NOT NULL COMMENT '当前作业员姓名快照', + assigned_by BIGINT UNSIGNED NOT NULL COMMENT '领取任务的作业员或执行分配的平台用户ID', + assigned_by_name VARCHAR(64) NULL COMMENT '领取或分配操作人姓名快照', + assignment_type VARCHAR(16) NOT NULL COMMENT '分配方式:CLAIMED作业员领取、ASSIGNED平台指派', + version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号,防止多人并发领取或覆盖', + 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 (pick_task_id), KEY idx_loading_pick_assignee (assignee_id, updated_at), CONSTRAINT fk_loading_pick_assignment_task FOREIGN KEY (pick_task_id) REFERENCES fl_loading_pick_task (id), @@ -9236,14 +9299,14 @@ CREATE TABLE IF NOT EXISTS fl_loading_pick_assignment ( ) ENGINE=InnoDB COMMENT='柜次PDA任务领取与作业员分配'; CREATE TABLE IF NOT EXISTS fl_loading_pick_review ( - pick_task_id BIGINT UNSIGNED NOT NULL, - review_status VARCHAR(16) NOT NULL DEFAULT 'APPROVED', - reviewed_by BIGINT UNSIGNED NOT NULL, - reviewed_by_name VARCHAR(64) NULL, - remark VARCHAR(500) NULL, - version INT UNSIGNED NOT NULL DEFAULT 0, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + pick_task_id BIGINT UNSIGNED NOT NULL COMMENT '已完成的柜次PDA拣货任务ID', + review_status VARCHAR(16) NOT NULL DEFAULT 'APPROVED' COMMENT '复核状态:APPROVED已通过;记录创建后不可修改或删除', + reviewed_by BIGINT UNSIGNED NOT NULL COMMENT '执行完成复核的平台用户ID', + reviewed_by_name VARCHAR(64) NULL COMMENT '复核人姓名快照', + remark VARCHAR(500) NULL COMMENT 'PDA任务复核备注', + version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '复核记录版本号', + 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 (pick_task_id), KEY idx_loading_pick_reviewer (reviewed_by, created_at), CONSTRAINT fk_loading_pick_review_task FOREIGN KEY (pick_task_id) REFERENCES fl_loading_pick_task (id), @@ -9251,14 +9314,14 @@ CREATE TABLE IF NOT EXISTS fl_loading_pick_review ( ) ENGINE=InnoDB COMMENT='柜次PDA任务完成复核'; CREATE TABLE IF NOT EXISTS fl_loading_pick_operation ( - id BIGINT UNSIGNED NOT NULL, - pick_task_id BIGINT UNSIGNED NOT NULL, - operation_id VARCHAR(128) NOT NULL, - request_hash CHAR(64) NOT NULL, - action_type VARCHAR(16) NOT NULL, - operator_id BIGINT UNSIGNED NOT NULL, - operator_name VARCHAR(64) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + id BIGINT UNSIGNED NOT NULL COMMENT 'PDA任务操作审计记录ID', + pick_task_id BIGINT UNSIGNED NOT NULL COMMENT '关联柜次PDA拣货任务ID', + operation_id VARCHAR(128) NOT NULL COMMENT '全局唯一操作ID,用于幂等重放', + request_hash CHAR(64) NOT NULL COMMENT '完整批量请求或任务操作内容SHA-256摘要', + action_type VARCHAR(16) NOT NULL COMMENT '动作类型:CLAIM领取、ASSIGN分配、REVIEW复核、BATCH批量提交', + operator_id BIGINT UNSIGNED NOT NULL COMMENT '执行PDA任务操作的用户ID', + operator_name VARCHAR(64) NULL COMMENT '操作人姓名快照', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '不可变操作审计创建时间', PRIMARY KEY (id), UNIQUE KEY uk_loading_pick_operation_id (operation_id), KEY idx_loading_pick_operation_task (pick_task_id, created_at), @@ -9445,15 +9508,15 @@ CALL phase3_pda_add_column( DROP PROCEDURE IF EXISTS phase3_pda_add_column; -- PDA 多人协作:领取粒度由整柜任务下沉为单条拣货明细。 CREATE TABLE IF NOT EXISTS fl_loading_pick_item_assignment ( - pick_item_id BIGINT UNSIGNED NOT NULL, - assignee_id BIGINT UNSIGNED NOT NULL, - assignee_name VARCHAR(64) NOT NULL, - assigned_by BIGINT UNSIGNED NOT NULL, - assigned_by_name VARCHAR(64) NULL, - assignment_type VARCHAR(16) NOT NULL COMMENT 'CLAIMED/ASSIGNED', - version INT UNSIGNED NOT NULL DEFAULT 0, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + pick_item_id BIGINT UNSIGNED NOT NULL COMMENT 'PDA拣货明细ID,同时作为本表主键', + assignee_id BIGINT UNSIGNED NOT NULL COMMENT '当前领取或被分配的作业员ID', + assignee_name VARCHAR(64) NOT NULL COMMENT '当前作业员姓名快照', + assigned_by BIGINT UNSIGNED NOT NULL COMMENT '执行领取或分配操作的用户ID', + assigned_by_name VARCHAR(64) NULL COMMENT '执行领取或分配操作的用户姓名快照', + assignment_type VARCHAR(16) NOT NULL COMMENT '分配类型:CLAIMED作业员领取、ASSIGNED管理员分配', + version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号,每次更新递增', + 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 (pick_item_id), KEY idx_loading_pick_item_assignee (assignee_id, updated_at), CONSTRAINT fk_loading_pick_item_assignment_item FOREIGN KEY (pick_item_id) REFERENCES fl_loading_pick_item (id), @@ -9505,38 +9568,38 @@ SET NAMES utf8mb4; USE baiyue_firstleg; CREATE TABLE IF NOT EXISTS fl_carrier_schedule_query ( - id BIGINT UNSIGNED NOT NULL, - query_no VARCHAR(40) NOT NULL, - customer_id BIGINT UNSIGNED NOT NULL COMMENT '当前产品客户ID', - ocean_booking_id BIGINT UNSIGNED NOT NULL, - container_booking_id BIGINT UNSIGNED NULL, - container_scope_id BIGINT UNSIGNED GENERATED ALWAYS AS (COALESCE(container_booking_id,0)) STORED, - carrier_code VARCHAR(16) NOT NULL COMMENT 'MATSON/SML/ZIM/COSCO/EMC/HEDE', - tracking_type VARCHAR(32) NOT NULL COMMENT 'BOOKING_NO/BILL_OF_LADING/CONTAINER_NO', - tracking_no VARCHAR(128) NOT NULL, - official_url VARCHAR(500) NOT NULL, - access_mode VARCHAR(32) NOT NULL COMMENT 'PUBLIC_FORM/HCAPTCHA/DYNAMIC_IFRAME', - status VARCHAR(24) NOT NULL COMMENT 'READY/MANUAL_REQUIRED/SUCCESS', - window_started_at DATETIME(3) NOT NULL, - attempt_count INT UNSIGNED NOT NULL DEFAULT 0, - next_allowed_at DATETIME(3) NOT NULL, - last_attempt_at DATETIME(3) NULL, - last_success_at DATETIME(3) NULL, - source_updated_at DATETIME(3) NULL, - expires_at DATETIME(3) NULL, - result_port_schedule_id BIGINT UNSIGNED NULL, - last_attempt_status VARCHAR(24) NULL, - manual_reason VARCHAR(1000) NULL, - last_error_code VARCHAR(64) NULL, - last_error_message VARCHAR(1000) NULL, - remark VARCHAR(1000) NULL, - created_by BIGINT UNSIGNED NOT NULL, - created_by_name VARCHAR(64) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_by BIGINT UNSIGNED NOT NULL, - updated_by_name VARCHAR(64) NULL, - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - version INT UNSIGNED NOT NULL DEFAULT 0, + id BIGINT UNSIGNED NOT NULL COMMENT '船司官网查询任务ID', + query_no VARCHAR(40) NOT NULL COMMENT '船司官网查询任务编号', + customer_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + ocean_booking_id BIGINT UNSIGNED NOT NULL COMMENT '关联海运订舱ID', + container_booking_id BIGINT UNSIGNED NULL COMMENT '指定查询的柜次ID;整票订舱查询时为空', + container_scope_id BIGINT UNSIGNED GENERATED ALWAYS AS (COALESCE(container_booking_id,0)) STORED COMMENT '柜次唯一作用域计算值;整票订舱查询固定为0', + carrier_code VARCHAR(16) NOT NULL COMMENT '船公司编码:MATSON美森、SML森罗、ZIM以星、COSCO中远海运、EMC长荣、HEDE合德', + tracking_type VARCHAR(32) NOT NULL COMMENT '官网跟踪类型:BOOKING_NO订舱号、BILL_OF_LADING提单号、CONTAINER_NO箱号', + tracking_no VARCHAR(128) NOT NULL COMMENT '提交至船公司官网的跟踪号码', + official_url VARCHAR(500) NOT NULL COMMENT '船公司官网查询入口地址快照', + access_mode VARCHAR(32) NOT NULL COMMENT '官网访问方式:PUBLIC_FORM公开表单、HCAPTCHA人机校验、DYNAMIC_IFRAME动态页面', + status VARCHAR(24) NOT NULL COMMENT '任务状态:READY可查询、MANUAL_REQUIRED需人工处理、SUCCESS已取得有效结果', + window_started_at DATETIME(3) NOT NULL COMMENT '当前三天查询窗口起始时间', + attempt_count INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '当前查询窗口已执行次数;最多3次', + next_allowed_at DATETIME(3) NOT NULL COMMENT '频控规则允许的下次查询时间', + last_attempt_at DATETIME(3) NULL COMMENT '最近一次查询尝试时间', + last_success_at DATETIME(3) NULL COMMENT '最近一次取得有效结果时间', + source_updated_at DATETIME(3) NULL COMMENT '船公司官网展示的结果更新时间', + expires_at DATETIME(3) NULL COMMENT '当前最近有效结果过期时间;到期后仍保留但标记过期', + result_port_schedule_id BIGINT UNSIGNED NULL COMMENT '最近有效结果对应的船期记录ID', + last_attempt_status VARCHAR(24) NULL COMMENT '最近查询尝试状态快照', + manual_reason VARCHAR(1000) NULL COMMENT '需要人工查询或录入的原因', + last_error_code VARCHAR(64) NULL COMMENT '最近查询失败错误码', + last_error_message VARCHAR(1000) NULL COMMENT '最近查询失败错误说明', + remark VARCHAR(1000) NULL COMMENT '平台内部备注', + created_by BIGINT UNSIGNED NOT NULL COMMENT '创建操作人ID', + created_by_name VARCHAR(64) NULL COMMENT '创建操作人姓名快照', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '任务创建时间', + updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改操作人ID', + updated_by_name VARCHAR(64) NULL COMMENT '最后修改操作人姓名快照', + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '任务最后更新时间', + version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', PRIMARY KEY (id), UNIQUE KEY uk_carrier_schedule_query_no (query_no), UNIQUE KEY uk_carrier_schedule_query_scope @@ -9560,21 +9623,21 @@ CREATE TABLE IF NOT EXISTS fl_carrier_schedule_query ( ) ENGINE=InnoDB COMMENT='六家船公司官网查询任务与三天窗口频控'; CREATE TABLE IF NOT EXISTS fl_carrier_schedule_query_attempt ( - id BIGINT UNSIGNED NOT NULL, - query_id BIGINT UNSIGNED NOT NULL, - window_attempt_no INT UNSIGNED NOT NULL, - carrier_code VARCHAR(16) NOT NULL, - access_mode VARCHAR(32) NOT NULL, - official_url VARCHAR(500) NOT NULL, - attempt_status VARCHAR(24) NOT NULL, - coverage_gap TINYINT(1) NOT NULL DEFAULT 0, - coverage_gap_code VARCHAR(64) NULL, - manual_reason VARCHAR(1000) NULL, - error_code VARCHAR(64) NULL, - error_message VARCHAR(1000) NULL, - operator_id BIGINT UNSIGNED NOT NULL, - operator_name VARCHAR(64) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + id BIGINT UNSIGNED NOT NULL COMMENT '船司官网查询尝试ID', + query_id BIGINT UNSIGNED NOT NULL COMMENT '所属船司官网查询任务ID', + window_attempt_no INT UNSIGNED NOT NULL COMMENT '当前三天窗口内的尝试序号;取值1至3', + carrier_code VARCHAR(16) NOT NULL COMMENT '本次尝试的船公司编码快照', + access_mode VARCHAR(32) NOT NULL COMMENT '本次尝试的官网访问方式快照', + official_url VARCHAR(500) NOT NULL COMMENT '本次尝试使用的官网地址快照', + attempt_status VARCHAR(24) NOT NULL COMMENT '尝试结果:MANUAL_REQUIRED需人工处理、SUCCESS成功、FAILED失败', + coverage_gap TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否存在自动化覆盖缺口:0否、1是', + coverage_gap_code VARCHAR(64) NULL COMMENT '自动化覆盖缺口编码', + manual_reason VARCHAR(1000) NULL COMMENT '转人工处理原因', + error_code VARCHAR(64) NULL COMMENT '查询失败错误码', + error_message VARCHAR(1000) NULL COMMENT '查询失败错误说明', + operator_id BIGINT UNSIGNED NOT NULL COMMENT '执行本次尝试的操作人ID', + operator_name VARCHAR(64) NULL COMMENT '执行操作人姓名快照', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '尝试记录创建时间', PRIMARY KEY (id), KEY idx_carrier_schedule_attempt_query (query_id,created_at,id), CONSTRAINT fk_carrier_schedule_attempt_query FOREIGN KEY (query_id) REFERENCES fl_carrier_schedule_query(id), @@ -9584,17 +9647,17 @@ CREATE TABLE IF NOT EXISTS fl_carrier_schedule_query_attempt ( ) ENGINE=InnoDB COMMENT='船司官网查询尝试不可变审计'; CREATE TABLE IF NOT EXISTS fl_carrier_schedule_query_history ( - id BIGINT UNSIGNED NOT NULL, - query_id BIGINT UNSIGNED NOT NULL, - action VARCHAR(24) NOT NULL, - from_version INT UNSIGNED NULL, - to_version INT UNSIGNED NOT NULL, - reason VARCHAR(1000) NULL, - before_snapshot JSON NULL, - after_snapshot JSON NOT NULL, - operator_id BIGINT UNSIGNED NOT NULL, - operator_name VARCHAR(64) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + id BIGINT UNSIGNED NOT NULL COMMENT '船司查询任务历史ID', + query_id BIGINT UNSIGNED NOT NULL COMMENT '所属船司官网查询任务ID', + action VARCHAR(24) NOT NULL COMMENT '历史动作:CREATE创建、ATTEMPT执行查询、MANUAL_RESULT录入人工结果', + from_version INT UNSIGNED NULL COMMENT '变更前任务版本;创建时为空', + to_version INT UNSIGNED NOT NULL COMMENT '变更后任务版本', + reason VARCHAR(1000) NULL COMMENT '操作原因或说明', + before_snapshot JSON NULL COMMENT '变更前任务数据快照;创建时为空', + after_snapshot JSON NOT NULL COMMENT '变更后任务数据快照', + operator_id BIGINT UNSIGNED NOT NULL COMMENT '操作人ID', + operator_name VARCHAR(64) NULL COMMENT '操作人姓名快照', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '历史记录创建时间', PRIMARY KEY (id), KEY idx_carrier_schedule_history_query (query_id,created_at,id), CONSTRAINT fk_carrier_schedule_history_query FOREIGN KEY (query_id) REFERENCES fl_carrier_schedule_query(id), @@ -9602,16 +9665,16 @@ CREATE TABLE IF NOT EXISTS fl_carrier_schedule_query_history ( ) ENGINE=InnoDB COMMENT='船司查询任务不可变历史'; CREATE TABLE IF NOT EXISTS fl_carrier_schedule_query_operation ( - id BIGINT UNSIGNED NOT NULL, - query_id BIGINT UNSIGNED NULL, - operator_id BIGINT UNSIGNED NOT NULL, - operation VARCHAR(24) NOT NULL, - idempotency_key VARCHAR(128) NOT NULL, - request_hash CHAR(64) NOT NULL, - status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING', - result_json JSON NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - completed_at DATETIME(3) NULL, + id BIGINT UNSIGNED NOT NULL COMMENT '船司查询写操作幂等记录ID', + query_id BIGINT UNSIGNED NULL COMMENT '关联查询任务ID;创建任务落库前允许为空', + operator_id BIGINT UNSIGNED NOT NULL COMMENT '发起写操作的平台操作人ID', + operation VARCHAR(24) NOT NULL COMMENT '幂等写操作类型', + idempotency_key VARCHAR(128) NOT NULL COMMENT '客户端写操作幂等键', + request_hash CHAR(64) NOT NULL COMMENT '规范化请求内容SHA-256摘要;用于识别同键不同请求', + status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING' COMMENT '处理状态:PROCESSING处理中、SUCCESS成功', + result_json JSON NULL COMMENT '成功结果JSON快照;供幂等重放返回', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '幂等记录创建时间', + completed_at DATETIME(3) NULL COMMENT '写操作成功完成时间', PRIMARY KEY (id), UNIQUE KEY uk_carrier_schedule_operation (operator_id,idempotency_key), KEY idx_carrier_schedule_operation_query (query_id,created_at,id), @@ -9946,24 +10009,35 @@ DROP PROCEDURE assert_phase3_warehouse_roles_ready; DROP TEMPORARY TABLE IF EXISTS tmp_phase3_warehouse_role_permission; --- Finance 首期人工财务基线:CNY 账单、收款、成本、来源事件、幂等与审计。 + + +-- BEGIN FINANCE FEE SOURCE MODULE MIGRATIONS +-- 来源:finance/src/main/resources/db/finance.sql CREATE DATABASE IF NOT EXISTS baiyue_finance DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; USE baiyue_finance; CREATE TABLE IF NOT EXISTS fin_fee_source ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - source_system VARCHAR(32) NOT NULL DEFAULT 'FIRSTLEG', - source_fee_id BIGINT UNSIGNED NOT NULL, - source_fee_version INT UNSIGNED NOT NULL, - customer_id BIGINT UNSIGNED NOT NULL, - order_id BIGINT UNSIGNED NULL, - inbound_id BIGINT UNSIGNED NULL, - fee_item_name VARCHAR(200) NOT NULL, - amount DECIMAL(20,6) NOT NULL, - currency_code CHAR(3) NOT NULL DEFAULT 'CNY', - status VARCHAR(16) NOT NULL DEFAULT 'AVAILABLE', - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + source_system VARCHAR(32) NOT NULL DEFAULT 'FIRSTLEG' COMMENT '费用来源系统,当前FIRSTLEG表示头程服务', + source_fee_id BIGINT UNSIGNED NOT NULL COMMENT '来源系统费用记录ID', + source_fee_version INT UNSIGNED NOT NULL COMMENT '来源费用版本号,用于固化不可变来源版本', + customer_id BIGINT UNSIGNED NOT NULL COMMENT '费用来源所属客户ID', + source_type VARCHAR(32) NOT NULL COMMENT '来源费用类型:INBOUND_FEE入库单费用、CONTAINER_FEE柜子费用', + source_no VARCHAR(64) NULL COMMENT '来源入库单号或柜号快照,可为空', + direction VARCHAR(16) NOT NULL COMMENT '费用方向:RECEIVABLE应收、PAYABLE应付', + order_id BIGINT UNSIGNED NULL COMMENT '来源头程订单ID,可为空', + inbound_id BIGINT UNSIGNED NULL COMMENT '来源入库单ID,可为空', + container_id BIGINT UNSIGNED NULL COMMENT '来源Firstleg装柜计划ID,可为空', + fee_item_code VARCHAR(64) NOT NULL COMMENT '来源费用项目编码快照', + fee_item_name VARCHAR(200) NOT NULL COMMENT '来源费用项目名称快照', + amount DECIMAL(20,6) NOT NULL COMMENT '来源费用金额,精度6位小数', + currency_code CHAR(3) NOT NULL DEFAULT 'CNY' COMMENT 'ISO 4217三位币种代码,首期仅CNY', + source_hash CHAR(64) NOT NULL COMMENT 'Firstleg费用来源完整快照SHA-256哈希', + source_snapshot JSON NOT NULL COMMENT 'Firstleg费用来源完整不可变JSON快照', + status VARCHAR(16) NOT NULL DEFAULT 'AVAILABLE' COMMENT '费用来源状态:AVAILABLE可结算、SUPERSEDED已被新版本替代、VOIDED已作废', + 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 (id), UNIQUE KEY uk_fin_fee_source (source_system, source_fee_id, source_fee_version), KEY idx_fin_fee_source_customer (customer_id, status), @@ -9972,43 +10046,46 @@ CREATE TABLE IF NOT EXISTS fin_fee_source ( ) ENGINE=InnoDB COMMENT='Firstleg费用来源快照池;账单人工选择并固化明细'; CREATE TABLE IF NOT EXISTS fin_inbox_event ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - event_id VARCHAR(128) NOT NULL, - source_system VARCHAR(32) NOT NULL, - event_type VARCHAR(64) NOT NULL, - payload JSON NOT NULL, - status VARCHAR(16) NOT NULL DEFAULT 'RECEIVED', - received_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - processed_at DATETIME(3) NULL, + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + event_id VARCHAR(128) NOT NULL COMMENT '来源系统事件唯一ID', + source_system VARCHAR(32) NOT NULL COMMENT '事件来源系统标识', + event_type VARCHAR(64) NOT NULL COMMENT '来源事件类型', + payload JSON NOT NULL COMMENT '来源事件原始JSON载荷快照', + payload_hash CHAR(64) NOT NULL COMMENT '规范化事件内容SHA-256哈希,用于同事件ID冲突检测', + result_resource_id BIGINT UNSIGNED NULL COMMENT '成功处理生成或定位的费用来源快照ID', + error_code VARCHAR(64) NULL COMMENT '处理失败时的稳定FIN错误码', + status VARCHAR(16) NOT NULL DEFAULT 'RECEIVED' COMMENT '事件处理状态:RECEIVED已接收、PROCESSED已完成、FAILED处理失败', + received_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '事件接收时间', + processed_at DATETIME(3) NULL COMMENT '事件处理完成时间,未完成时为空', PRIMARY KEY (id), UNIQUE KEY uk_fin_inbox_event (source_system, event_id) ) ENGINE=InnoDB COMMENT='Firstleg费用事件幂等接收记录'; CREATE TABLE IF NOT EXISTS fin_subject ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - subject_code VARCHAR(64) NOT NULL, - subject_name VARCHAR(200) NOT NULL, - status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE', - version INT UNSIGNED NOT NULL DEFAULT 0, - created_by BIGINT UNSIGNED NOT NULL DEFAULT 0, - updated_by BIGINT UNSIGNED NOT NULL DEFAULT 0, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + subject_code VARCHAR(64) NOT NULL COMMENT '平台财务主体唯一编码', + subject_name VARCHAR(200) NOT NULL COMMENT '平台收付款及核算主体名称', + status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '财务主体状态,ACTIVE表示启用', + version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', + created_by BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建操作人ID,0表示系统初始化', + updated_by BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '最后修改操作人ID,0表示系统初始化', + 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 (id), UNIQUE KEY uk_fin_subject_code (subject_code) ) ENGINE=InnoDB COMMENT='平台财务主体;首期仅一个有效主体'; CREATE TABLE IF NOT EXISTS fin_customer_settlement_profile ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', - customer_name_snapshot VARCHAR(200) NOT NULL, - credit_days INT UNSIGNED NOT NULL DEFAULT 30 COMMENT '默认30个自然日', - status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE', - version INT UNSIGNED NOT NULL DEFAULT 0, - created_by BIGINT UNSIGNED NOT NULL, - updated_by BIGINT UNSIGNED NOT NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + customer_name_snapshot VARCHAR(200) NOT NULL COMMENT '客户名称快照,客户主数据后续变更不追改', + credit_days INT UNSIGNED NOT NULL DEFAULT 30 COMMENT '客户账期自然日数,默认30天', + status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '客户结算配置状态,ACTIVE表示启用', + version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', + created_by BIGINT UNSIGNED NOT NULL COMMENT '创建操作人ID', + updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改操作人ID', + 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 (id), UNIQUE KEY uk_fin_customer_profile (tenant_id), CONSTRAINT chk_fin_customer_profile_tenant CHECK (tenant_id > 0), @@ -10016,29 +10093,29 @@ CREATE TABLE IF NOT EXISTS fin_customer_settlement_profile ( ) ENGINE=InnoDB COMMENT='客户结算配置;一客户一个结算对象'; CREATE TABLE IF NOT EXISTS fin_bill ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - tenant_id BIGINT UNSIGNED NOT NULL, - bill_no VARCHAR(64) NOT NULL, - subject_id BIGINT UNSIGNED NOT NULL, - customer_name_snapshot VARCHAR(200) NOT NULL, - currency_code CHAR(3) NOT NULL DEFAULT 'CNY', - bill_date DATE NOT NULL, - due_date DATE NOT NULL, - status VARCHAR(24) NOT NULL DEFAULT 'DRAFT' COMMENT 'DRAFT/ISSUED/PARTIALLY_PAID/PAID/VOIDED', - total_amount DECIMAL(20,6) NOT NULL DEFAULT 0, - received_amount DECIMAL(20,6) NOT NULL DEFAULT 0, - outstanding_amount DECIMAL(20,6) NOT NULL DEFAULT 0, - remark VARCHAR(500) NULL, - issued_at DATETIME(3) NULL, - issued_by BIGINT UNSIGNED NULL, - voided_at DATETIME(3) NULL, - voided_by BIGINT UNSIGNED NULL, - void_reason VARCHAR(500) NULL, - version INT UNSIGNED NOT NULL DEFAULT 0, - created_by BIGINT UNSIGNED NOT NULL, - updated_by BIGINT UNSIGNED NOT NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + bill_no VARCHAR(64) NOT NULL COMMENT '客户范围内唯一账单编号', + subject_id BIGINT UNSIGNED NOT NULL COMMENT '平台财务主体ID', + customer_name_snapshot VARCHAR(200) NOT NULL COMMENT '出账时客户名称快照', + currency_code CHAR(3) NOT NULL DEFAULT 'CNY' COMMENT 'ISO 4217三位币种代码,首期仅CNY', + bill_date DATE NOT NULL COMMENT '账单日期', + due_date DATE NOT NULL COMMENT '到期日期,由账单日期加客户账期计算', + status VARCHAR(24) NOT NULL DEFAULT 'DRAFT' COMMENT '账单状态:DRAFT草稿、ISSUED已出账、PARTIALLY_PAID部分收款、PAID已收清、VOIDED已作废', + total_amount DECIMAL(20,6) NOT NULL DEFAULT 0 COMMENT '账单全部明细总金额,精度6位小数', + received_amount DECIMAL(20,6) NOT NULL DEFAULT 0 COMMENT '账单累计已收金额,精度6位小数', + outstanding_amount DECIMAL(20,6) NOT NULL DEFAULT 0 COMMENT '账单未收余额,精度6位小数', + remark VARCHAR(500) NULL COMMENT '平台账单内部备注', + issued_at DATETIME(3) NULL COMMENT '出账时间,未出账时为空', + issued_by BIGINT UNSIGNED NULL COMMENT '出账操作人ID,未出账时为空', + voided_at DATETIME(3) NULL COMMENT '作废时间,未作废时为空', + voided_by BIGINT UNSIGNED NULL COMMENT '作废操作人ID,未作废时为空', + void_reason VARCHAR(500) NULL COMMENT '账单作废原因', + version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', + created_by BIGINT UNSIGNED NOT NULL COMMENT '创建操作人ID', + updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改操作人ID', + 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 (id), UNIQUE KEY uk_fin_bill_no (tenant_id, bill_no), KEY idx_fin_bill_customer_status_date (tenant_id, status, bill_date), @@ -10048,20 +10125,20 @@ CREATE TABLE IF NOT EXISTS fin_bill ( ) ENGINE=InnoDB COMMENT='客户CNY账单;首期账单即应收台账'; CREATE TABLE IF NOT EXISTS fin_bill_line ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - tenant_id BIGINT UNSIGNED NOT NULL, - bill_id BIGINT UNSIGNED NOT NULL, - source_type VARCHAR(32) NOT NULL DEFAULT 'FIRSTLEG_FEE', - source_id BIGINT UNSIGNED NOT NULL, - source_version INT UNSIGNED NOT NULL, - source_no VARCHAR(64) NULL, - order_id BIGINT UNSIGNED NULL, - inbound_id BIGINT UNSIGNED NULL, - fee_item_code VARCHAR(64) NOT NULL, - fee_item_name VARCHAR(200) NOT NULL, - amount DECIMAL(20,6) NOT NULL, - currency_code CHAR(3) NOT NULL DEFAULT 'CNY', - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + bill_id BIGINT UNSIGNED NOT NULL COMMENT '所属账单ID', + source_type VARCHAR(32) NOT NULL DEFAULT 'FIRSTLEG_FEE' COMMENT '费用来源类型,FIRSTLEG_FEE表示头程费用', + source_id BIGINT UNSIGNED NOT NULL COMMENT '来源费用ID', + source_version INT UNSIGNED NOT NULL COMMENT '出账固化的来源费用版本号', + source_no VARCHAR(64) NULL COMMENT '来源费用单号快照,可为空', + order_id BIGINT UNSIGNED NULL COMMENT '来源头程订单ID,可为空', + inbound_id BIGINT UNSIGNED NULL COMMENT '来源入库单ID,可为空', + fee_item_code VARCHAR(64) NOT NULL COMMENT '出账时费用项目编码快照', + fee_item_name VARCHAR(200) NOT NULL COMMENT '出账时费用项目名称快照', + amount DECIMAL(20,6) NOT NULL COMMENT '账单明细金额,精度6位小数', + currency_code CHAR(3) NOT NULL DEFAULT 'CNY' COMMENT 'ISO 4217三位币种代码,首期仅CNY', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '账单明细创建时间', PRIMARY KEY (id), UNIQUE KEY uk_fin_bill_line_source (source_type, source_id, source_version), KEY idx_fin_bill_line_bill (tenant_id, bill_id), @@ -10071,26 +10148,26 @@ CREATE TABLE IF NOT EXISTS fin_bill_line ( ) ENGINE=InnoDB COMMENT='账单费用来源快照明细'; CREATE TABLE IF NOT EXISTS fin_receipt ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - tenant_id BIGINT UNSIGNED NOT NULL, - receipt_no VARCHAR(64) NOT NULL, - subject_id BIGINT UNSIGNED NOT NULL, - currency_code CHAR(3) NOT NULL DEFAULT 'CNY', - received_amount DECIMAL(20,6) NOT NULL, - received_at DATETIME(3) NOT NULL, - payment_method VARCHAR(32) NULL, - voucher_no VARCHAR(128) NULL, - voucher_file_ids JSON NOT NULL COMMENT 'File服务文件ID字符串数组', - status VARCHAR(16) NOT NULL DEFAULT 'CONFIRMED' COMMENT 'CONFIRMED/VOIDED', - remark VARCHAR(500) NULL, - voided_at DATETIME(3) NULL, - voided_by BIGINT UNSIGNED NULL, - void_reason VARCHAR(500) NULL, - version INT UNSIGNED NOT NULL DEFAULT 0, - created_by BIGINT UNSIGNED NOT NULL, - updated_by BIGINT UNSIGNED NOT NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + receipt_no VARCHAR(64) NOT NULL COMMENT '客户范围内唯一收款编号', + subject_id BIGINT UNSIGNED NOT NULL COMMENT '实际收款的平台财务主体ID', + currency_code CHAR(3) NOT NULL DEFAULT 'CNY' COMMENT 'ISO 4217三位币种代码,首期仅CNY', + received_amount DECIMAL(20,6) NOT NULL COMMENT '本次实际到账金额,精度6位小数', + received_at DATETIME(3) NOT NULL COMMENT '实际到账时间', + payment_method VARCHAR(32) NULL COMMENT '收款方式编码', + voucher_no VARCHAR(128) NULL COMMENT '银行流水号或线下收款凭证号', + voucher_file_ids JSON NOT NULL COMMENT 'File服务财务凭证文件ID字符串数组JSON', + status VARCHAR(16) NOT NULL DEFAULT 'CONFIRMED' COMMENT '收款状态:CONFIRMED已确认、VOIDED已作废', + remark VARCHAR(500) NULL COMMENT '平台收款内部备注', + voided_at DATETIME(3) NULL COMMENT '收款作废时间', + voided_by BIGINT UNSIGNED NULL COMMENT '收款作废操作人ID', + void_reason VARCHAR(500) NULL COMMENT '收款作废原因', + version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', + created_by BIGINT UNSIGNED NOT NULL COMMENT '创建操作人ID', + updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改操作人ID', + 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 (id), UNIQUE KEY uk_fin_receipt_no (tenant_id, receipt_no), KEY idx_fin_receipt_customer_time (tenant_id, received_at), @@ -10100,13 +10177,13 @@ CREATE TABLE IF NOT EXISTS fin_receipt ( ) ENGINE=InnoDB COMMENT='平台财务人工登记收款及凭证'; CREATE TABLE IF NOT EXISTS fin_receipt_bill ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - tenant_id BIGINT UNSIGNED NOT NULL, - receipt_id BIGINT UNSIGNED NOT NULL, - bill_id BIGINT UNSIGNED NOT NULL, - applied_amount DECIMAL(20,6) NOT NULL, - created_by BIGINT UNSIGNED NOT NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + receipt_id BIGINT UNSIGNED NOT NULL COMMENT '收款记录ID', + bill_id BIGINT UNSIGNED NOT NULL COMMENT '被分配收款的账单ID', + applied_amount DECIMAL(20,6) NOT NULL COMMENT '本笔收款分配到该账单的金额,精度6位小数', + created_by BIGINT UNSIGNED NOT NULL COMMENT '分配操作人ID', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '收款分配创建时间', PRIMARY KEY (id), UNIQUE KEY uk_fin_receipt_bill_pair (receipt_id, bill_id), KEY idx_fin_receipt_bill_bill (tenant_id, bill_id), @@ -10115,32 +10192,32 @@ CREATE TABLE IF NOT EXISTS fin_receipt_bill ( ) ENGINE=InnoDB COMMENT='人工收款对应账单金额;UI不使用复杂核销术语'; CREATE TABLE IF NOT EXISTS fin_cost ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - tenant_id BIGINT UNSIGNED NOT NULL, - cost_no VARCHAR(64) NOT NULL, - cost_type VARCHAR(24) NOT NULL COMMENT 'SINGLE_ORDER/CONTAINER_SHARED', - order_id BIGINT UNSIGNED NULL, - inbound_id BIGINT UNSIGNED NULL, - container_id BIGINT UNSIGNED NULL, - supplier_id BIGINT UNSIGNED NULL, - supplier_name_snapshot VARCHAR(200) NULL, - fee_item_code VARCHAR(64) NOT NULL, - fee_item_name VARCHAR(200) NOT NULL, - currency_code CHAR(3) NOT NULL DEFAULT 'CNY', - cost_amount DECIMAL(20,6) NOT NULL, - voucher_file_ids JSON NOT NULL, - status VARCHAR(16) NOT NULL DEFAULT 'DRAFT' COMMENT 'DRAFT/CONFIRMED/VOIDED', - remark VARCHAR(500) NULL, - confirmed_at DATETIME(3) NULL, - confirmed_by BIGINT UNSIGNED NULL, - voided_at DATETIME(3) NULL, - voided_by BIGINT UNSIGNED NULL, - void_reason VARCHAR(500) NULL, - version INT UNSIGNED NOT NULL DEFAULT 0, - created_by BIGINT UNSIGNED NOT NULL, - updated_by BIGINT UNSIGNED NOT NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + cost_no VARCHAR(64) NOT NULL COMMENT '客户范围内唯一成本编号', + cost_type VARCHAR(24) NOT NULL COMMENT '成本归属类型:SINGLE_ORDER单票成本、CONTAINER_SHARED整柜公共成本', + order_id BIGINT UNSIGNED NULL COMMENT '成本归属的头程订单ID,单票成本必填', + inbound_id BIGINT UNSIGNED NULL COMMENT '成本关联的入库单ID,可为空', + container_id BIGINT UNSIGNED NULL COMMENT 'Firstleg装柜计划ID,整柜公共成本必填', + supplier_id BIGINT UNSIGNED NULL COMMENT 'Warehouse公共往来单位或供应商引用ID', + supplier_name_snapshot VARCHAR(200) NULL COMMENT '登记成本时供应商或成本对象名称快照', + fee_item_code VARCHAR(64) NOT NULL COMMENT '成本费用项目编码快照', + fee_item_name VARCHAR(200) NOT NULL COMMENT '成本费用项目名称快照', + currency_code CHAR(3) NOT NULL DEFAULT 'CNY' COMMENT 'ISO 4217三位币种代码,首期仅CNY', + cost_amount DECIMAL(20,6) NOT NULL COMMENT '成本总金额,精度6位小数', + voucher_file_ids JSON NOT NULL COMMENT 'File服务财务凭证文件ID字符串数组JSON', + status VARCHAR(16) NOT NULL DEFAULT 'DRAFT' COMMENT '成本状态:DRAFT草稿、CONFIRMED已确认、VOIDED已作废', + remark VARCHAR(500) NULL COMMENT '平台成本内部备注', + confirmed_at DATETIME(3) NULL COMMENT '成本确认时间', + confirmed_by BIGINT UNSIGNED NULL COMMENT '成本确认操作人ID', + voided_at DATETIME(3) NULL COMMENT '成本作废时间', + voided_by BIGINT UNSIGNED NULL COMMENT '成本作废操作人ID', + void_reason VARCHAR(500) NULL COMMENT '成本作废原因', + version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', + created_by BIGINT UNSIGNED NOT NULL COMMENT '创建操作人ID', + updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改操作人ID', + 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 (id), UNIQUE KEY uk_fin_cost_no (tenant_id, cost_no), KEY idx_fin_cost_ticket (tenant_id, order_id, inbound_id), @@ -10155,17 +10232,17 @@ CREATE TABLE IF NOT EXISTS fin_cost ( ) ENGINE=InnoDB COMMENT='人工成本;单票直接归属,整柜按体积分摊'; CREATE TABLE IF NOT EXISTS fin_cost_allocation ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - tenant_id BIGINT UNSIGNED NOT NULL, - cost_id BIGINT UNSIGNED NOT NULL, - order_id BIGINT UNSIGNED NOT NULL, - inbound_id BIGINT UNSIGNED NULL, - volume_cbm DECIMAL(20,6) NOT NULL, - total_volume_cbm DECIMAL(20,6) NOT NULL, - allocation_rate DECIMAL(20,10) NOT NULL, - allocated_amount DECIMAL(20,6) NOT NULL, - rounding_adjustment TINYINT NOT NULL DEFAULT 0 COMMENT '1表示尾差归集行', - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + cost_id BIGINT UNSIGNED NOT NULL COMMENT '整柜公共成本ID', + order_id BIGINT UNSIGNED NOT NULL COMMENT '参与分摊的头程订单ID', + inbound_id BIGINT UNSIGNED NULL COMMENT '参与分摊订单关联的入库单ID,可为空', + volume_cbm DECIMAL(20,6) NOT NULL COMMENT '该订单分摊时有效体积快照,单位CBM、精度6位', + total_volume_cbm DECIMAL(20,6) NOT NULL COMMENT '柜内有效订单总体积快照,单位CBM、精度6位', + allocation_rate DECIMAL(20,10) NOT NULL COMMENT '订单体积占总体积的分摊比例,精度10位', + allocated_amount DECIMAL(20,6) NOT NULL COMMENT '分摊到该订单的成本金额,精度6位', + rounding_adjustment TINYINT NOT NULL DEFAULT 0 COMMENT '尾差归集标记:0未归集、1该行为实际尾差归集行', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '不可变分摊快照创建时间', PRIMARY KEY (id), UNIQUE KEY uk_fin_cost_allocation_order (cost_id, order_id), KEY idx_fin_cost_allocation_query (tenant_id, order_id), @@ -10174,55 +10251,3116 @@ CREATE TABLE IF NOT EXISTS fin_cost_allocation ( ) ENGINE=InnoDB COMMENT='整柜成本按有效体积分摊的可审计结果'; CREATE TABLE IF NOT EXISTS fin_bill_revision ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - tenant_id BIGINT UNSIGNED NOT NULL, - bill_id BIGINT UNSIGNED NOT NULL, - version INT UNSIGNED NOT NULL, - reason VARCHAR(500) NULL, - bill_snapshot JSON NOT NULL, - line_snapshot JSON NOT NULL, - created_by BIGINT UNSIGNED NOT NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + bill_id BIGINT UNSIGNED NOT NULL COMMENT '被调整账单ID', + version INT UNSIGNED NOT NULL COMMENT '调整前账单乐观锁版本号', + reason VARCHAR(500) NULL COMMENT '本次账单调整原因', + bill_snapshot JSON NOT NULL COMMENT '调整前账单主记录不可变JSON快照', + line_snapshot JSON NOT NULL COMMENT '调整前全部账单明细不可变JSON数组快照', + created_by BIGINT UNSIGNED NOT NULL COMMENT '执行调整的操作人ID', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '修订快照创建时间', PRIMARY KEY (id), UNIQUE KEY uk_fin_bill_revision (bill_id, version), KEY idx_fin_bill_revision_tenant (tenant_id, bill_id) ) ENGINE=InnoDB COMMENT='出账后调整前的不可变账单及明细版本快照'; CREATE TABLE IF NOT EXISTS fin_idempotency_record ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - tenant_id BIGINT UNSIGNED NOT NULL, - operator_id BIGINT UNSIGNED NOT NULL, - operation VARCHAR(64) NOT NULL, - idempotency_key VARCHAR(128) NOT NULL, - request_hash CHAR(64) NOT NULL, - status VARCHAR(16) NOT NULL COMMENT 'PROCESSING/SUCCEEDED/FAILED', - result_resource_id BIGINT UNSIGNED NULL, - error_code VARCHAR(64) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + operator_id BIGINT UNSIGNED NOT NULL COMMENT '发起幂等写操作的操作人ID', + operation VARCHAR(64) NOT NULL COMMENT '幂等隔离的财务操作类型', + idempotency_key VARCHAR(128) NOT NULL COMMENT '调用方提供的幂等键', + request_hash CHAR(64) NOT NULL COMMENT '规范化完整请求的SHA-256十六进制哈希', + status VARCHAR(16) NOT NULL COMMENT '幂等处理状态:PROCESSING处理中、SUCCEEDED成功、FAILED失败', + result_resource_id BIGINT UNSIGNED NULL COMMENT '成功操作生成或更新的业务资源ID', + error_code VARCHAR(64) NULL COMMENT '失败操作的稳定业务错误码', + 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 (id), UNIQUE KEY uk_fin_idempotency (tenant_id, operator_id, operation, idempotency_key) ) ENGINE=InnoDB COMMENT='Finance人工写操作幂等记录'; CREATE TABLE IF NOT EXISTS fin_audit_log ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - tenant_id BIGINT UNSIGNED NOT NULL, - resource_type VARCHAR(32) NOT NULL, - resource_id BIGINT UNSIGNED NOT NULL, - action VARCHAR(64) NOT NULL, - reason VARCHAR(500) NULL, - before_data JSON NULL, - after_data JSON NULL, - operator_id BIGINT UNSIGNED NOT NULL, - operator_name_snapshot VARCHAR(128) NOT NULL, - request_id VARCHAR(128) NULL, - trace_id VARCHAR(128) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + resource_type VARCHAR(32) NOT NULL COMMENT '被审计财务资源类型', + resource_id BIGINT UNSIGNED NOT NULL COMMENT '被审计财务资源ID', + action VARCHAR(64) NOT NULL COMMENT '审计业务动作类型', + reason VARCHAR(500) NULL COMMENT '操作原因或说明', + before_data JSON NULL COMMENT '操作前业务数据JSON快照', + after_data JSON NULL COMMENT '操作后业务数据JSON快照', + operator_id BIGINT UNSIGNED NOT NULL COMMENT '操作人ID', + operator_name_snapshot VARCHAR(128) NOT NULL COMMENT '操作发生时的操作人名称快照', + request_id VARCHAR(128) NULL COMMENT '入口请求ID,用于问题定位', + trace_id VARCHAR(128) NULL COMMENT '分布式调用链追踪ID', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '不可变审计记录创建时间', PRIMARY KEY (id), KEY idx_fin_audit_resource (tenant_id, resource_type, resource_id, created_at), KEY idx_fin_audit_operator (operator_id, created_at) ) ENGINE=InnoDB COMMENT='Finance不可变业务审计明细'; +USE baiyue_firstleg; +-- 来源:firstleg/src/main/resources/db/finance-fee-source-migration.sql +-- Firstleg -> Finance 费用来源事件和柜次费用模型(MySQL 8,可重复执行) + +DROP PROCEDURE IF EXISTS firstleg_finance_add_column; +DELIMITER $$ +CREATE PROCEDURE firstleg_finance_add_column( + IN target_table VARCHAR(64), IN target_column VARCHAR(64), IN column_definition TEXT +) +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = target_table AND column_name = target_column + ) THEN + SET @ddl = CONCAT('ALTER TABLE `', target_table, '` ADD COLUMN `', target_column, '` ', column_definition); + PREPARE statement FROM @ddl; + EXECUTE statement; + DEALLOCATE PREPARE statement; + END IF; +END$$ +DELIMITER ; + +CALL firstleg_finance_add_column('fl_inbound_outbox', 'event_key', + "VARCHAR(160) NULL COMMENT '费用来源事件幂等键;历史事件允许为空' AFTER event_type"); +CALL firstleg_finance_add_column('fl_inbound_outbox', 'event_version', + "INT UNSIGNED NOT NULL DEFAULT 1 COMMENT '事件信封版本' AFTER event_key"); +CALL firstleg_finance_add_column('fl_inbound_outbox', 'aggregate_sequence', + "INT UNSIGNED NULL COMMENT '同一费用来源的单调版本序号' AFTER event_version"); +CALL firstleg_finance_add_column('fl_inbound_outbox', 'payload_hash', + "CHAR(64) NULL COMMENT '短payload规范化JSON的SHA-256' AFTER aggregate_sequence"); +CALL firstleg_finance_add_column('fl_inbound_outbox', 'next_retry_at', + "DATETIME(3) NULL COMMENT '下一次允许投递时间' AFTER retry_count"); +CALL firstleg_finance_add_column('fl_inbound_outbox', 'published_at', + "DATETIME(3) NULL COMMENT 'Finance确认接收时间' AFTER next_retry_at"); +CALL firstleg_finance_add_column('fl_inbound_outbox', 'locked_at', + "DATETIME(3) NULL COMMENT 'Relay抢占租约时间' AFTER published_at"); +CALL firstleg_finance_add_column('fl_inbound_outbox', 'locked_by', + "VARCHAR(64) NULL COMMENT 'Relay抢占实例ID' AFTER locked_at"); +CALL firstleg_finance_add_column('fl_inbound_outbox', 'last_error', + "VARCHAR(500) NULL COMMENT '最近一次投递失败摘要,不记录令牌' AFTER locked_by"); + +DROP PROCEDURE IF EXISTS firstleg_finance_add_column; + +DROP PROCEDURE IF EXISTS firstleg_finance_add_index; +DELIMITER $$ +CREATE PROCEDURE firstleg_finance_add_index( + IN target_table VARCHAR(64), IN target_index VARCHAR(64), IN index_definition TEXT +) +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.statistics + WHERE table_schema = DATABASE() AND table_name = target_table AND index_name = target_index + ) THEN + SET @ddl = CONCAT('ALTER TABLE `', target_table, '` ADD ', index_definition); + PREPARE statement FROM @ddl; + EXECUTE statement; + DEALLOCATE PREPARE statement; + END IF; +END$$ +DELIMITER ; + +CALL firstleg_finance_add_index('fl_inbound_outbox', 'uk_inbound_outbox_event_key', + 'UNIQUE KEY `uk_inbound_outbox_event_key` (`event_key`)'); +CALL firstleg_finance_add_index('fl_inbound_outbox', 'idx_inbound_outbox_finance_relay', + 'KEY `idx_inbound_outbox_finance_relay` (`publish_status`, `next_retry_at`, `created_at`, `id`)'); +DROP PROCEDURE IF EXISTS firstleg_finance_add_index; + +CREATE TABLE IF NOT EXISTS fl_container_fee_detail ( + id BIGINT UNSIGNED NOT NULL COMMENT '柜次费用来源ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + loading_plan_id BIGINT UNSIGNED NOT NULL COMMENT '柜次ID,对应fl_loading_plan.id', + container_booking_id BIGINT UNSIGNED NULL COMMENT '订舱柜ID快照', + container_no VARCHAR(64) NULL COMMENT '柜号快照', + fee_item_id BIGINT UNSIGNED NOT NULL COMMENT '平台公共费用项目ID', + fee_item_code_snapshot VARCHAR(50) NOT NULL COMMENT '费用项目稳定编码快照', + fee_item_name_snapshot VARCHAR(128) NOT NULL COMMENT '费用项目名称快照', + direction VARCHAR(16) NOT NULL COMMENT '费用方向:RECEIVABLE应收、PAYABLE应付', + business_date DATE NOT NULL COMMENT '费用业务日期', + amount DECIMAL(18,2) NOT NULL COMMENT 'CNY费用金额', + currency CHAR(3) NOT NULL DEFAULT 'CNY' COMMENT '币种,首期固定CNY', + settlement_included TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否参与财务结算来源池', + review_status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT '审核状态:PENDING待审核、APPROVED通过、REJECTED驳回', + source_status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '来源状态:ACTIVE有效、VOIDED作废', + source_version INT UNSIGNED NOT NULL DEFAULT 1 COMMENT '费用来源版本;调整递增,作废携带当前版本', + remark VARCHAR(500) NULL COMMENT '人工备注', + reviewer_id BIGINT UNSIGNED NULL COMMENT '审核人ID', + reviewer_name VARCHAR(64) NULL COMMENT '审核人名称快照', + reviewed_at DATETIME(3) NULL COMMENT '审核时间', + review_reason VARCHAR(500) NULL COMMENT '审核驳回原因', + created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', + updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后更新人ID', + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后更新时间', + PRIMARY KEY (id), + KEY idx_container_fee_customer_plan (tenant_id, loading_plan_id, source_status), + KEY idx_container_fee_settlement (tenant_id, review_status, settlement_included, source_status), + CONSTRAINT chk_container_fee_direction CHECK (direction IN ('RECEIVABLE', 'PAYABLE')), + CONSTRAINT chk_container_fee_currency CHECK (currency = 'CNY'), + CONSTRAINT chk_container_fee_amount CHECK (amount >= 0), + CONSTRAINT chk_container_fee_review CHECK (review_status IN ('PENDING', 'APPROVED', 'REJECTED')), + CONSTRAINT chk_container_fee_source CHECK (source_status IN ('ACTIVE', 'VOIDED')) +) ENGINE=InnoDB COMMENT='柜次级人工应收应付费用来源;审核通过且参与结算后同步Finance'; + +DROP PROCEDURE IF EXISTS firstleg_finance_assert_schema; +DELIMITER $$ +CREATE PROCEDURE firstleg_finance_assert_schema() +BEGIN + IF (SELECT COUNT(*) FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'fl_inbound_outbox' + AND column_name IN ('event_key', 'event_version', 'aggregate_sequence', 'payload_hash', + 'next_retry_at', 'published_at', 'locked_at', 'locked_by', 'last_error')) <> 9 THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Firstleg Finance迁移失败:Outbox事件信封字段不完整'; + END IF; + IF NOT EXISTS (SELECT 1 FROM information_schema.tables + WHERE table_schema = DATABASE() AND table_name = 'fl_container_fee_detail') THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Firstleg Finance迁移失败:柜次费用表不存在'; + END IF; +END$$ +DELIMITER ; +CALL firstleg_finance_assert_schema(); +DROP PROCEDURE IF EXISTS firstleg_finance_assert_schema; + +-- 来源:finance/src/main/resources/db/fee-source-event-migration.sql +-- Firstleg费用来源事件消费模型存量升级(MySQL 8,可重复执行) +USE baiyue_finance; + +DROP PROCEDURE IF EXISTS finance_fee_source_add_column; +DELIMITER $$ +CREATE PROCEDURE finance_fee_source_add_column( + IN target_table VARCHAR(64), IN target_column VARCHAR(64), IN column_definition TEXT +) +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = target_table AND column_name = target_column + ) THEN + SET @ddl = CONCAT('ALTER TABLE `', target_table, '` ADD COLUMN `', target_column, '` ', column_definition); + PREPARE statement FROM @ddl; + EXECUTE statement; + DEALLOCATE PREPARE statement; + END IF; +END$$ +DELIMITER ; + +CALL finance_fee_source_add_column('fin_fee_source', 'source_type', + "VARCHAR(32) NULL COMMENT '来源费用类型:INBOUND_FEE入库单费用、CONTAINER_FEE柜子费用' AFTER customer_id"); +CALL finance_fee_source_add_column('fin_fee_source', 'source_no', + "VARCHAR(64) NULL COMMENT '来源入库单号或柜号快照,可为空' AFTER source_type"); +CALL finance_fee_source_add_column('fin_fee_source', 'direction', + "VARCHAR(16) NULL COMMENT '费用方向:RECEIVABLE应收、PAYABLE应付' AFTER source_no"); +CALL finance_fee_source_add_column('fin_fee_source', 'container_id', + "BIGINT UNSIGNED NULL COMMENT '来源Firstleg装柜计划ID,可为空' AFTER inbound_id"); +CALL finance_fee_source_add_column('fin_fee_source', 'fee_item_code', + "VARCHAR(64) NULL COMMENT '来源费用项目编码快照' AFTER container_id"); +CALL finance_fee_source_add_column('fin_fee_source', 'source_hash', + "CHAR(64) NULL COMMENT 'Firstleg费用来源完整快照SHA-256哈希' AFTER currency_code"); +CALL finance_fee_source_add_column('fin_fee_source', 'source_snapshot', + "JSON NULL COMMENT 'Firstleg费用来源完整不可变JSON快照' AFTER source_hash"); +CALL finance_fee_source_add_column('fin_fee_source', 'updated_at', + "DATETIME(3) NULL COMMENT '来源状态最后更新时间,数据修改时自动刷新' AFTER created_at"); +CALL finance_fee_source_add_column('fin_inbox_event', 'payload_hash', + "CHAR(64) NULL COMMENT '规范化事件内容SHA-256哈希,用于同事件ID冲突检测' AFTER payload"); +CALL finance_fee_source_add_column('fin_inbox_event', 'result_resource_id', + "BIGINT UNSIGNED NULL COMMENT '成功处理生成或定位的费用来源快照ID' AFTER payload_hash"); +CALL finance_fee_source_add_column('fin_inbox_event', 'error_code', + "VARCHAR(64) NULL COMMENT '处理失败时的稳定FIN错误码' AFTER result_resource_id"); + +DROP PROCEDURE IF EXISTS finance_fee_source_add_column; + +-- 产品确认无历史财务数据;以下兼容值仅用于空环境重复迁移时保证结构收敛。 +UPDATE fin_fee_source + SET source_type = COALESCE(source_type, 'INBOUND_FEE'), + direction = COALESCE(direction, 'RECEIVABLE'), + fee_item_code = COALESCE(fee_item_code, 'LEGACY'), + source_hash = COALESCE(source_hash, SHA2(CONCAT_WS('|', source_system, source_fee_id, source_fee_version), 256)), + source_snapshot = COALESCE(source_snapshot, JSON_OBJECT('migrated', TRUE)), + updated_at = COALESCE(updated_at, created_at); +UPDATE fin_inbox_event + SET payload_hash = COALESCE(payload_hash, SHA2(CAST(payload AS CHAR), 256)); + +ALTER TABLE fin_fee_source + MODIFY COLUMN source_type VARCHAR(32) NOT NULL COMMENT '来源费用类型:INBOUND_FEE入库单费用、CONTAINER_FEE柜子费用', + MODIFY COLUMN direction VARCHAR(16) NOT NULL COMMENT '费用方向:RECEIVABLE应收、PAYABLE应付', + MODIFY COLUMN fee_item_code VARCHAR(64) NOT NULL COMMENT '来源费用项目编码快照', + MODIFY COLUMN source_hash CHAR(64) NOT NULL COMMENT 'Firstleg费用来源完整快照SHA-256哈希', + MODIFY COLUMN source_snapshot JSON NOT NULL COMMENT 'Firstleg费用来源完整不可变JSON快照', + MODIFY COLUMN status VARCHAR(16) NOT NULL DEFAULT 'AVAILABLE' COMMENT '费用来源状态:AVAILABLE可结算、SUPERSEDED已被新版本替代、VOIDED已作废', + MODIFY COLUMN updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '来源状态最后更新时间,数据修改时自动刷新'; +ALTER TABLE fin_inbox_event + MODIFY COLUMN payload_hash CHAR(64) NOT NULL COMMENT '规范化事件内容SHA-256哈希,用于同事件ID冲突检测', + MODIFY COLUMN status VARCHAR(16) NOT NULL DEFAULT 'RECEIVED' COMMENT '事件处理状态:RECEIVED已接收、PROCESSED已完成、FAILED处理失败'; + +DROP PROCEDURE IF EXISTS finance_fee_source_assert_schema; +DELIMITER $$ +CREATE PROCEDURE finance_fee_source_assert_schema() +BEGIN + IF (SELECT COUNT(*) FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'fin_fee_source' + AND column_name IN ('source_type','source_no','direction','container_id','fee_item_code', + 'source_hash','source_snapshot','updated_at')) <> 8 THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Finance费用来源迁移失败:来源快照字段不完整'; + END IF; + IF (SELECT COUNT(*) FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'fin_inbox_event' + AND column_name IN ('payload_hash','result_resource_id','error_code')) <> 3 THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Finance费用来源迁移失败:Inbox幂等字段不完整'; + END IF; +END$$ +DELIMITER ; +CALL finance_fee_source_assert_schema(); +DROP PROCEDURE IF EXISTS finance_fee_source_assert_schema; +-- END FINANCE FEE SOURCE MODULE MIGRATIONS + +-- 九库中文注释存量升级。 +-- MySQL DDL 会隐式提交,以下过程不能依赖事务回滚。所有目标先只读校验并排入临时队列, +-- 再按 database.table 聚合为每表至多一次 ALTER TABLE;字段 DDL 前后均校验不含注释的完整结构签名。 +-- 不使用 GROUP_CONCAT,LONGTEXT 由游标逐项拼接,不受 group_concat_max_len 截断影响。 +USE baiyue_firstleg; +DROP TEMPORARY TABLE IF EXISTS tmp_table_comment_target; +CREATE TEMPORARY TABLE tmp_table_comment_target ( + schema_name VARCHAR(64) NOT NULL, + table_name VARCHAR(64) NOT NULL, + desired_comment VARCHAR(2048) NOT NULL, + current_comment VARCHAR(2048) NOT NULL, + needs_change TINYINT(1) NOT NULL, + PRIMARY KEY (schema_name,table_name) +) ENGINE=InnoDB; +DROP TEMPORARY TABLE IF EXISTS tmp_column_comment_target; +CREATE TEMPORARY TABLE tmp_column_comment_target ( + schema_name VARCHAR(64) NOT NULL, + table_name VARCHAR(64) NOT NULL, + column_name VARCHAR(64) NOT NULL, + desired_comment VARCHAR(2048) NOT NULL, + definition_sql LONGTEXT NOT NULL, + signature_before CHAR(64) NOT NULL, + needs_change TINYINT(1) NOT NULL, + PRIMARY KEY (schema_name,table_name,column_name) +) ENGINE=InnoDB; + +DROP PROCEDURE IF EXISTS queue_table_comment; +DELIMITER $$ +CREATE PROCEDURE queue_table_comment( + IN p_schema_name VARCHAR(64), IN p_table_name VARCHAR(64), IN p_table_comment VARCHAR(2048) +) +BEGIN + DECLARE v_found TINYINT DEFAULT 1; + DECLARE v_current_comment LONGTEXT; + DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_found=0; + IF p_schema_name NOT IN ('baiyue_auth','baiyue_iam','baiyue_order','baiyue_warehouse', + 'baiyue_firstleg','baiyue_file','baiyue_audit','baiyue_event_bridge','baiyue_finance') + OR p_table_name NOT REGEXP '^[a-z0-9_]+$' + OR p_table_comment IS NULL OR p_table_comment='' THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='表注释升级参数无效'; + END IF; + SELECT table_comment INTO v_current_comment FROM information_schema.tables + WHERE table_schema=p_schema_name AND table_name=p_table_name AND table_type='BASE TABLE'; + IF v_found=0 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='表注释升级目标不存在'; END IF; + INSERT INTO tmp_table_comment_target + (schema_name,table_name,desired_comment,current_comment,needs_change) + VALUES (p_schema_name,p_table_name,p_table_comment,v_current_comment, + NOT (BINARY v_current_comment=BINARY p_table_comment)) + ON DUPLICATE KEY UPDATE desired_comment=VALUES(desired_comment), + current_comment=VALUES(current_comment),needs_change=VALUES(needs_change); +END$$ +DELIMITER ; + +DROP PROCEDURE IF EXISTS queue_column_comment; +DELIMITER $$ +CREATE PROCEDURE queue_column_comment( + IN p_schema_name VARCHAR(64), IN p_table_name VARCHAR(64), + IN p_column_name VARCHAR(64), IN p_column_comment VARCHAR(2048) +) +BEGIN + DECLARE v_found TINYINT DEFAULT 1; + DECLARE v_column_type LONGTEXT; + DECLARE v_is_nullable VARCHAR(3); + DECLARE v_column_default LONGTEXT; + DECLARE v_extra VARCHAR(255); + DECLARE v_data_type VARCHAR(64); + DECLARE v_character_set VARCHAR(64); + DECLARE v_collation VARCHAR(64); + DECLARE v_generation_expression LONGTEXT; + DECLARE v_current_comment LONGTEXT; + DECLARE v_default_sql LONGTEXT DEFAULT ''; + DECLARE v_extra_sql LONGTEXT DEFAULT ''; + DECLARE v_definition_sql LONGTEXT; + DECLARE v_signature_before CHAR(64); + DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_found=0; + IF p_schema_name NOT IN ('baiyue_auth','baiyue_iam','baiyue_order','baiyue_warehouse', + 'baiyue_firstleg','baiyue_file','baiyue_audit','baiyue_event_bridge','baiyue_finance') + OR p_table_name NOT REGEXP '^[a-z0-9_]+$' OR p_column_name NOT REGEXP '^[a-z0-9_]+$' + OR p_column_comment IS NULL OR p_column_comment='' THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='字段注释升级参数无效'; + END IF; + SELECT column_type,is_nullable,column_default,extra,data_type,character_set_name, + collation_name,generation_expression,column_comment + INTO v_column_type,v_is_nullable,v_column_default,v_extra,v_data_type,v_character_set, + v_collation,v_generation_expression,v_current_comment + FROM information_schema.columns + WHERE table_schema=p_schema_name AND table_name=p_table_name AND column_name=p_column_name; + IF v_found=0 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='字段注释升级目标不存在'; END IF; + IF v_data_type NOT IN ('bigint','binary','bit','blob','boolean','char','date','datetime', + 'decimal','double','enum','float','int','integer','json','longblob','longtext', + 'mediumblob','mediumint','mediumtext','numeric','real','set','smallint','text','time', + 'timestamp','tinyblob','tinyint','tinytext','varbinary','varchar','year') THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='字段注释升级遇到未知数据类型'; + END IF; + SET v_signature_before=SHA2(CONCAT_WS(CHAR(31),v_column_type,v_is_nullable, + COALESCE(v_column_default,''),COALESCE(v_extra,''),COALESCE(v_character_set,''), + COALESCE(v_collation,''),COALESCE(v_generation_expression,'')),256); + SET v_extra_sql=TRIM(REPLACE(COALESCE(v_extra,''),'DEFAULT_GENERATED','')); + IF COALESCE(v_generation_expression,'')<>'' THEN + IF UPPER(v_extra_sql) NOT IN ('STORED GENERATED','VIRTUAL GENERATED') + OR v_column_default IS NOT NULL THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='字段注释升级遇到未知生成列结构'; + END IF; + SET v_definition_sql=CONCAT(v_column_type, + IF(v_character_set IS NULL,'',CONCAT(' CHARACTER SET ',v_character_set)), + IF(v_collation IS NULL,'',CONCAT(' COLLATE ',v_collation)), + ' GENERATED ALWAYS AS (',v_generation_expression,') ', + IF(UPPER(v_extra_sql)='STORED GENERATED','STORED','VIRTUAL')); + ELSE + IF UPPER(v_extra_sql) LIKE '%GENERATED%' THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='字段注释升级遇到缺失表达式的生成列'; + END IF; + IF v_column_default IS NULL THEN + IF v_is_nullable='YES' THEN SET v_default_sql=' DEFAULT NULL'; END IF; + ELSEIF UPPER(v_column_default) REGEXP '^CURRENT_TIMESTAMP(\\([0-9]+\\))?$' + OR v_column_default REGEXP '^b''[01]+''$' + OR (v_data_type IN ('tinyint','smallint','mediumint','int','integer','bigint','decimal', + 'numeric','float','double','real','boolean') + AND v_column_default REGEXP '^-?[0-9]+(\\.[0-9]+)?$') THEN + SET v_default_sql=CONCAT(' DEFAULT ',v_column_default); + ELSEIF COALESCE(v_extra,'') LIKE '%DEFAULT_GENERATED%' + AND v_column_default REGEXP '^\\(.*\\)$' THEN + SET v_default_sql=CONCAT(' DEFAULT ',v_column_default); + ELSE + SET v_default_sql=CONCAT(' DEFAULT ',QUOTE(v_column_default)); + END IF; + IF v_extra_sql<>'' + AND LOWER(v_extra_sql) NOT REGEXP '^(auto_increment|on update current_timestamp(\\([0-9]+\\))?)$' THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='字段注释升级遇到未知EXTRA定义'; + END IF; + SET v_definition_sql=CONCAT(v_column_type, + IF(v_character_set IS NULL,'',CONCAT(' CHARACTER SET ',v_character_set)), + IF(v_collation IS NULL,'',CONCAT(' COLLATE ',v_collation)), + IF(v_is_nullable='YES',' NULL',' NOT NULL'),v_default_sql, + IF(v_extra_sql='','',CONCAT(' ',v_extra_sql))); + END IF; + INSERT INTO tmp_column_comment_target + (schema_name,table_name,column_name,desired_comment,definition_sql,signature_before,needs_change) + VALUES (p_schema_name,p_table_name,p_column_name,p_column_comment,v_definition_sql, + v_signature_before,NOT (BINARY v_current_comment=BINARY p_column_comment)) + ON DUPLICATE KEY UPDATE desired_comment=VALUES(desired_comment),definition_sql=VALUES(definition_sql), + signature_before=VALUES(signature_before),needs_change=VALUES(needs_change); +END$$ +DELIMITER ; + +DROP PROCEDURE IF EXISTS apply_one_table_comments; +DELIMITER $$ +CREATE PROCEDURE apply_one_table_comments(IN p_schema_name VARCHAR(64), IN p_table_name VARCHAR(64)) +apply_one: BEGIN + DECLARE v_done TINYINT DEFAULT 0; + DECLARE v_column_name VARCHAR(64); + DECLARE v_comment VARCHAR(2048); + DECLARE v_definition LONGTEXT; + DECLARE v_sql LONGTEXT; + DECLARE v_separator VARCHAR(2) DEFAULT ''; + DECLARE column_cursor CURSOR FOR + SELECT column_name,desired_comment,definition_sql + FROM tmp_column_comment_target + WHERE schema_name=p_schema_name AND table_name=p_table_name AND needs_change=1 + ORDER BY column_name; + DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done=1; + SET v_sql=CONCAT('ALTER TABLE `',p_schema_name,'`.`',p_table_name,'` '); + IF EXISTS (SELECT 1 FROM tmp_table_comment_target + WHERE schema_name=p_schema_name AND table_name=p_table_name AND needs_change=1) THEN + SELECT desired_comment INTO v_comment FROM tmp_table_comment_target + WHERE schema_name=p_schema_name AND table_name=p_table_name; + SET v_sql=CONCAT(v_sql,'COMMENT = ',QUOTE(v_comment)); + SET v_separator=', '; + END IF; + OPEN column_cursor; + column_loop: LOOP + FETCH column_cursor INTO v_column_name,v_comment,v_definition; + IF v_done=1 THEN LEAVE column_loop; END IF; + SET v_sql=CONCAT(v_sql,v_separator,'MODIFY COLUMN `',v_column_name,'` ',v_definition, + ' COMMENT ',QUOTE(v_comment)); + SET v_separator=', '; + END LOOP; + CLOSE column_cursor; + IF v_separator='' THEN LEAVE apply_one; END IF; + SET @table_comment_batch_sql=v_sql; + PREPARE table_comment_batch_stmt FROM @table_comment_batch_sql; + EXECUTE table_comment_batch_stmt; + DEALLOCATE PREPARE table_comment_batch_stmt; + IF EXISTS ( + SELECT 1 FROM tmp_table_comment_target target + LEFT JOIN information_schema.tables actual + ON actual.table_schema=target.schema_name AND actual.table_name=target.table_name + AND actual.table_type='BASE TABLE' + WHERE target.schema_name=p_schema_name AND target.table_name=p_table_name + AND target.needs_change=1 + AND (actual.table_name IS NULL OR BINARY actual.table_comment<>BINARY target.desired_comment) + ) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='表注释批量升级结果校验失败'; END IF; + IF EXISTS ( + SELECT 1 FROM tmp_column_comment_target target + LEFT JOIN information_schema.columns actual + ON actual.table_schema=target.schema_name AND actual.table_name=target.table_name + AND actual.column_name=target.column_name + WHERE target.schema_name=p_schema_name AND target.table_name=p_table_name + AND target.needs_change=1 + AND (actual.column_name IS NULL OR BINARY actual.column_comment<>BINARY target.desired_comment + OR BINARY SHA2(CONCAT_WS(CHAR(31),actual.column_type,actual.is_nullable, + COALESCE(actual.column_default,''),COALESCE(actual.extra,''), + COALESCE(actual.character_set_name,''),COALESCE(actual.collation_name,''), + COALESCE(actual.generation_expression,'')),256)<>BINARY target.signature_before) + ) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='字段注释批量升级改变了结构签名'; END IF; +END$$ +DELIMITER ; + +DROP PROCEDURE IF EXISTS apply_queued_comments; +DELIMITER $$ +CREATE PROCEDURE apply_queued_comments() +BEGIN + DECLARE v_done TINYINT DEFAULT 0; + DECLARE v_schema_name VARCHAR(64); + DECLARE v_table_name VARCHAR(64); + DECLARE table_cursor CURSOR FOR + SELECT schema_name,table_name FROM ( + SELECT schema_name,table_name FROM tmp_table_comment_target WHERE needs_change=1 + UNION + SELECT schema_name,table_name FROM tmp_column_comment_target WHERE needs_change=1 + ) changed_tables ORDER BY schema_name,table_name; + DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done=1; + OPEN table_cursor; + table_loop: LOOP + FETCH table_cursor INTO v_schema_name,v_table_name; + IF v_done=1 THEN LEAVE table_loop; END IF; + CALL apply_one_table_comments(v_schema_name,v_table_name); + END LOOP; + CLOSE table_cursor; +END$$ +DELIMITER ; + + +-- BEGIN GENERATED NINE-SCHEMA COMMENT UPGRADE CALLS +CALL queue_table_comment('baiyue_audit', 'sys_audit_dead_letter', '审计消息死信补偿表'); +CALL queue_table_comment('baiyue_audit', 'sys_audit_log', '统一业务审计日志表:追加记录敏感操作的主体、租户、资源、结果及前后快照;不同于消息Outbox和消费幂等表'); +CALL queue_table_comment('baiyue_auth', 'auth_account', '统一登录账号表:保存平台端和商户端共用的认证凭据及账号安全状态'); +CALL queue_table_comment('baiyue_auth', 'auth_refresh_token', '刷新令牌表:保存令牌摘要、平台或客户作用域、有效期和轮换/吊销状态'); +CALL queue_table_comment('baiyue_auth', 'sys_audit_outbox', '可靠审计事件本地发送箱'); +CALL queue_table_comment('baiyue_event_bridge', 'bridge_delivery_attempt', '跨区域事件投递尝试记录'); +CALL queue_table_comment('baiyue_event_bridge', 'bridge_inbox', '跨区域事件已接收箱'); +CALL queue_table_comment('baiyue_event_bridge', 'bridge_outbox', '跨区域事件待投递箱'); +CALL queue_table_comment('baiyue_file', 'file_download_log', '文件下载审计日志表'); +CALL queue_table_comment('baiyue_file', 'file_download_token', '临时下载授权令牌表'); +CALL queue_table_comment('baiyue_file', 'file_export_outbox', '导出命令事务Outbox表'); +CALL queue_table_comment('baiyue_file', 'file_export_task', '公共异步导出任务表'); +CALL queue_table_comment('baiyue_file', 'file_export_task_guard', '导出任务创建并发保护表:低频创建入口使用单行锁保证重复提交及活动上限'); +CALL queue_table_comment('baiyue_file', 'file_object', '文件元数据表'); +CALL queue_table_comment('baiyue_finance', 'fin_audit_log', 'Finance不可变业务审计明细'); +CALL queue_table_comment('baiyue_finance', 'fin_bill', '客户CNY账单;首期账单即应收台账'); +CALL queue_table_comment('baiyue_finance', 'fin_bill_line', '账单费用来源快照明细'); +CALL queue_table_comment('baiyue_finance', 'fin_bill_revision', '出账后调整前的不可变账单及明细版本快照'); +CALL queue_table_comment('baiyue_finance', 'fin_cost', '人工成本;单票直接归属,整柜按体积分摊'); +CALL queue_table_comment('baiyue_finance', 'fin_cost_allocation', '整柜成本按有效体积分摊的可审计结果'); +CALL queue_table_comment('baiyue_finance', 'fin_customer_settlement_profile', '客户结算配置;一客户一个结算对象'); +CALL queue_table_comment('baiyue_finance', 'fin_fee_source', 'Firstleg费用来源快照池;账单人工选择并固化明细'); +CALL queue_table_comment('baiyue_finance', 'fin_idempotency_record', 'Finance人工写操作幂等记录'); +CALL queue_table_comment('baiyue_finance', 'fin_inbox_event', 'Firstleg费用事件幂等接收记录'); +CALL queue_table_comment('baiyue_finance', 'fin_receipt', '平台财务人工登记收款及凭证'); +CALL queue_table_comment('baiyue_finance', 'fin_receipt_bill', '人工收款对应账单金额;UI不使用复杂核销术语'); +CALL queue_table_comment('baiyue_finance', 'fin_subject', '平台财务主体;首期仅一个有效主体'); +CALL queue_table_comment('baiyue_firstleg', 'fl_assignment_history', '头程任务指派历史表:不可变记录每次负责人变更及原因,用于追踪和审计'); +CALL queue_table_comment('baiyue_firstleg', 'fl_carrier_fba_rate', '船司FBA目的仓价格版本'); +CALL queue_table_comment('baiyue_firstleg', 'fl_carrier_schedule_query', '六家船公司官网查询任务与三天窗口频控'); +CALL queue_table_comment('baiyue_firstleg', 'fl_carrier_schedule_query_attempt', '船司官网查询尝试不可变审计'); +CALL queue_table_comment('baiyue_firstleg', 'fl_carrier_schedule_query_history', '船司查询任务不可变历史'); +CALL queue_table_comment('baiyue_firstleg', 'fl_carrier_schedule_query_operation', '船司查询写操作幂等与请求摘要'); +CALL queue_table_comment('baiyue_firstleg', 'fl_consumed_event', '头程消息消费幂等表:记录已处理事件并按事件ID去重,防止RocketMQ重复投递造成重复建单;不是消息发布表'); +CALL queue_table_comment('baiyue_firstleg', 'fl_container_booking', '平台货柜预订资料;预装状态唯一来源为装柜计划'); +CALL queue_table_comment('baiyue_firstleg', 'fl_container_document', '平台货柜清关、DO与PU文件关联'); +CALL queue_table_comment('baiyue_firstleg', 'fl_container_fee_detail', '柜次级人工应收应付费用来源;审核通过且参与结算后同步Finance'); +CALL queue_table_comment('baiyue_firstleg', 'fl_customer_charge_rule', '客户头程计费规则版本'); +CALL queue_table_comment('baiyue_firstleg', 'fl_customer_dimension_config', '客户固定四箱规尺寸配置'); +CALL queue_table_comment('baiyue_firstleg', 'fl_customs_case', '三期订单报关协同聚合'); +CALL queue_table_comment('baiyue_firstleg', 'fl_customs_checklist_version', '三期报关资料清单不可变版本'); +CALL queue_table_comment('baiyue_firstleg', 'fl_customs_document', '报关客户文件版本引用'); +CALL queue_table_comment('baiyue_firstleg', 'fl_customs_history', '报关协同稳定历史'); +CALL queue_table_comment('baiyue_firstleg', 'fl_customs_operation', '报关协同命令幂等记录'); +CALL queue_table_comment('baiyue_firstleg', 'fl_customs_receipt', '报关代理回执版本与客户可见范围'); +CALL queue_table_comment('baiyue_firstleg', 'fl_customs_submission_batch', '报关代理提交批次及冻结文件版本'); +CALL queue_table_comment('baiyue_firstleg', 'fl_customs_supplement_request', '报关补件请求'); +CALL queue_table_comment('baiyue_firstleg', 'fl_fee_item', '平台公共费用项目主数据'); +CALL queue_table_comment('baiyue_firstleg', 'fl_fulfillment_task', '头程履约任务表:接收订单事件后形成独立履约任务,管理指派、执行和完成状态'); +CALL queue_table_comment('baiyue_firstleg', 'fl_inbound_carton_label', '首次打印前冻结的实际箱号与订单货物明细映射'); +CALL queue_table_comment('baiyue_firstleg', 'fl_inbound_destination_snapshot', '入库单目的仓地址和联系人快照'); +CALL queue_table_comment('baiyue_firstleg', 'fl_inbound_fee', '入库头程费用计算和确认快照'); +CALL queue_table_comment('baiyue_firstleg', 'fl_inbound_fee_detail', '入库单应收应付费用明细'); +CALL queue_table_comment('baiyue_firstleg', 'fl_inbound_fee_item', '入库头程费用项目明细'); +CALL queue_table_comment('baiyue_firstleg', 'fl_inbound_file', '入库单与文件中心对象的分类关系'); +CALL queue_table_comment('baiyue_firstleg', 'fl_inbound_hold_record', '入库单扣货和解除扣货不可变历史'); +CALL queue_table_comment('baiyue_firstleg', 'fl_inbound_idempotency', '第三方测量、审核、费用发送和入库写操作幂等记录'); +CALL queue_table_comment('baiyue_firstleg', 'fl_inbound_internal_note', '入库单客服内部备注历史'); +CALL queue_table_comment('baiyue_firstleg', 'fl_inbound_loading_pick_task', 'PDA拣货任务及结果'); +CALL queue_table_comment('baiyue_firstleg', 'fl_inbound_measurement', '入库长宽高测量批次和第三方原始数据'); +CALL queue_table_comment('baiyue_firstleg', 'fl_inbound_measurement_item', '入库长宽高和箱数测量明细'); +CALL queue_table_comment('baiyue_firstleg', 'fl_inbound_order', '二期头程入库单聚合根和列表查询快照'); +CALL queue_table_comment('baiyue_firstleg', 'fl_inbound_outbox', '入库事务Outbox事件表'); +CALL queue_table_comment('baiyue_firstleg', 'fl_inbound_pallet_allocation', '头程侧托盘、库位和仓库预占结果引用'); +CALL queue_table_comment('baiyue_firstleg', 'fl_inbound_pickup_snapshot', '入库单提货信息不可变快照'); +CALL queue_table_comment('baiyue_firstleg', 'fl_inbound_split_batch', '主入仓单拆分批次;数量口径仅为箱数'); +CALL queue_table_comment('baiyue_firstleg', 'fl_inbound_split_detail', '按箱数部分锁定及剩余箱数生成二级子入库单的不可变契约'); +CALL queue_table_comment('baiyue_firstleg', 'fl_inbound_split_history', '不可变主子入仓单拆分审计历史'); +CALL queue_table_comment('baiyue_firstleg', 'fl_inbound_split_item', '主子入仓单拆分数量台账'); +CALL queue_table_comment('baiyue_firstleg', 'fl_inbound_status_history', '入库单流程状态不可变历史'); +CALL queue_table_comment('baiyue_firstleg', 'fl_inbound_transport_snapshot', '入库单关联运输信息只读快照'); +CALL queue_table_comment('baiyue_firstleg', 'fl_inbound_weight_result', '称重图片OCR原值、人工修正值和确认重量'); +CALL queue_table_comment('baiyue_firstleg', 'fl_inbound_weight_revision', 'OCR重量人工修正和确认的不可变版本历史'); +CALL queue_table_comment('baiyue_firstleg', 'fl_loading_evidence', '订单装柜空仓/半仓/满仓/锁仓/带锁码照证据'); +CALL queue_table_comment('baiyue_firstleg', 'fl_loading_operation', '装柜与派送写操作幂等记录'); +CALL queue_table_comment('baiyue_firstleg', 'fl_loading_pick_assignment', '柜次PDA任务领取与作业员分配'); +CALL queue_table_comment('baiyue_firstleg', 'fl_loading_pick_history', 'PDA拣货幂等和操作审计历史'); +CALL queue_table_comment('baiyue_firstleg', 'fl_loading_pick_item', 'PDA装柜拣货明细'); +CALL queue_table_comment('baiyue_firstleg', 'fl_loading_pick_item_assignment', 'PDA拣货明细领取与作业员分配'); +CALL queue_table_comment('baiyue_firstleg', 'fl_loading_pick_operation', '柜次PDA任务级幂等审计'); +CALL queue_table_comment('baiyue_firstleg', 'fl_loading_pick_review', '柜次PDA任务完成复核'); +CALL queue_table_comment('baiyue_firstleg', 'fl_loading_pick_task', 'PDA装柜拣货任务'); +CALL queue_table_comment('baiyue_firstleg', 'fl_loading_plan', '平台智能装柜计划与唯一预装状态源'); +CALL queue_table_comment('baiyue_firstleg', 'fl_loading_plan_history', '智能装柜追加式不可变操作历史'); +CALL queue_table_comment('baiyue_firstleg', 'fl_loading_plan_item', '智能装柜客户货物明细;客户归属仅在明细层'); +CALL queue_table_comment('baiyue_firstleg', 'fl_measure_review_record', '测量审核、驳回和异常放行不可变记录'); +CALL queue_table_comment('baiyue_firstleg', 'fl_mojia_measurement_receipt', '墨家模拟或未匹配入库单的原始测量收件箱'); +CALL queue_table_comment('baiyue_firstleg', 'fl_ocean_booking', '独立海运订舱聚合;柜次仍由fl_container_booking承载'); +CALL queue_table_comment('baiyue_firstleg', 'fl_ocean_booking_container_allocation', '订舱需求与现有柜次的唯一分配关系'); +CALL queue_table_comment('baiyue_firstleg', 'fl_ocean_booking_container_requirement', '订舱箱型箱种及数量需求'); +CALL queue_table_comment('baiyue_firstleg', 'fl_ocean_booking_operation', '海运订舱写操作幂等与请求哈希'); +CALL queue_table_comment('baiyue_firstleg', 'fl_ocean_booking_party', '订舱提单关系方快照'); +CALL queue_table_comment('baiyue_firstleg', 'fl_ocean_booking_status_history', '海运订舱不可变状态历史'); +CALL queue_table_comment('baiyue_firstleg', 'fl_outbound_dispatch', '三期国内出仓派车状态,不重复创建出仓单'); +CALL queue_table_comment('baiyue_firstleg', 'fl_outbound_dispatch_history', '国内出仓追加式审计历史'); +CALL queue_table_comment('baiyue_firstleg', 'fl_outbound_dispatch_operation', '国内出仓写操作幂等声明'); +CALL queue_table_comment('baiyue_firstleg', 'fl_port_schedule', '独立港务船期来源聚合,不替代订舱快照'); +CALL queue_table_comment('baiyue_firstleg', 'fl_port_schedule_history', '港务船期不可变变更历史'); +CALL queue_table_comment('baiyue_firstleg', 'fl_port_schedule_operation', '港务船期写操作幂等与请求哈希'); +CALL queue_table_comment('baiyue_firstleg', 'fl_preload_loading_plan_link', '入库单预装与柜次装柜计划多对多关联'); +CALL queue_table_comment('baiyue_firstleg', 'fl_preload_plan', '订单分柜预装清单'); +CALL queue_table_comment('baiyue_firstleg', 'fl_price_book_channel', '价格版本渠道及船期快照'); +CALL queue_table_comment('baiyue_firstleg', 'fl_price_book_version', '平台公共船期价格整表版本'); +CALL queue_table_comment('baiyue_firstleg', 'fl_price_channel_catalog', '平台公共船期价格固定渠道目录'); +CALL queue_table_comment('baiyue_firstleg', 'fl_price_origin_alias', '价格表起运仓名称别名映射'); +CALL queue_table_comment('baiyue_firstleg', 'fl_price_warehouse_mapping', '价格表来源仓与固定平台公共仓映射'); +CALL queue_table_comment('baiyue_firstleg', 'fl_split_operation', '分柜操作幂等与审计索引'); +CALL queue_table_comment('baiyue_firstleg', 'fl_third_party_call_log', '第三方测量开放接口原始请求、签名主体和处理结果留档'); +CALL queue_table_comment('baiyue_firstleg', 'fl_truck_appointment', '三期拖车预约派车聚合'); +CALL queue_table_comment('baiyue_firstleg', 'fl_truck_appointment_attachment', '拖车附件文件引用'); +CALL queue_table_comment('baiyue_firstleg', 'fl_truck_appointment_history', '拖车状态与改约历史'); +CALL queue_table_comment('baiyue_firstleg', 'fl_truck_appointment_operation', '拖车操作幂等记录'); +CALL queue_table_comment('baiyue_firstleg', 'sys_audit_outbox', '可靠审计事件本地发送箱'); +CALL queue_table_comment('baiyue_iam', 'iam_data_scope', '角色数据范围表:在功能权限之外限制角色可查看和操作的平台或客户范围内数据集合'); +CALL queue_table_comment('baiyue_iam', 'iam_membership', '客户成员关系表:表示用户在客户企业中的成员身份,是分配企业客户端角色的主体'); +CALL queue_table_comment('baiyue_iam', 'iam_membership_role', '成员角色关联表:将客户成员分配到对应应用端的一个或多个角色'); +CALL queue_table_comment('baiyue_iam', 'iam_permission', '统一权限资源表:支持商户端和平台后台两套菜单、页面、按钮及API权限,权限最细可控制到按钮'); +CALL queue_table_comment('baiyue_iam', 'iam_role', '角色表:按客户或平台作用域及应用端隔离企业客户端、平台后台两套角色体系'); +CALL queue_table_comment('baiyue_iam', 'iam_role_permission', '角色权限关联表:定义角色可访问的菜单、页面、按钮和API资源'); +CALL queue_table_comment('baiyue_iam', 'iam_tenant', '客户企业主档表:保存企业资料、审核结果和启停状态;ID同时作为客户数据隔离键'); +CALL queue_table_comment('baiyue_iam', 'iam_tenant_address', '客户地址表:保存企业注册地址、办公地址等客户资料'); +CALL queue_table_comment('baiyue_iam', 'iam_tenant_qualification', '客户资质表:保存企业证照、有效期、文件地址和审核状态'); +CALL queue_table_comment('baiyue_iam', 'iam_tenant_review', '客户审核记录表:保存企业注册、资料变更等审核过程和审核意见'); +CALL queue_table_comment('baiyue_iam', 'iam_user', 'IAM用户资料表:连接统一认证账号与业务身份资料,不保存密码'); +CALL queue_table_comment('baiyue_iam', 'sys_audit_outbox', '可靠审计事件本地发送箱'); +CALL queue_table_comment('baiyue_order', 'ord_idempotency_record', '订单写接口持久化幂等记录'); +CALL queue_table_comment('baiyue_order', 'ord_order', '头程订单主表:保存商户委托、目的仓、运输方式和订单当前状态'); +CALL queue_table_comment('baiyue_order', 'ord_order_cargo', '订单货物明细表:记录件数、重量、体积、申报价值和危险品属性'); +CALL queue_table_comment('baiyue_order', 'ord_order_fulfillment_split', '父订单→子订单→柜次数量台账'); +CALL queue_table_comment('baiyue_order', 'ord_order_internal_note', '订单内部备注表:保存平台运营或履约人员的内部协作记录,不向商户客户展示'); +CALL queue_table_comment('baiyue_order', 'ord_order_party', '订单收发货方快照表:固化下单时发货人和收货人信息,避免主数据变化影响历史订单'); +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', '可靠审计事件本地发送箱'); +CALL queue_table_comment('baiyue_warehouse', 'wh_express_company', '平台公共快递公司主数据,固定归属 tenant_id=0'); +CALL queue_table_comment('baiyue_warehouse', 'wh_location', '二期库位基础资料,默认一托盘对应一个库位且不作容量校验'); +CALL queue_table_comment('baiyue_warehouse', 'wh_location_occupancy', '已确认的一托一库位占用事实记录'); +CALL queue_table_comment('baiyue_warehouse', 'wh_location_reservation', '跨服务入库Saga的托盘和库位临时预占'); +CALL queue_table_comment('baiyue_warehouse', 'wh_overseas_storage_fee_rule', '客户海外仓费用规则'); +CALL queue_table_comment('baiyue_warehouse', 'wh_overseas_storage_fee_tier', '客户海外仓费用阶梯'); +CALL queue_table_comment('baiyue_warehouse', 'wh_pallet', '二期托盘基础资料和物理托盘实例'); +CALL queue_table_comment('baiyue_warehouse', 'wh_pallet_location_release_operation', 'PDA已拣满托盘库位精确释放的持久化幂等操作记录'); +CALL queue_table_comment('baiyue_warehouse', 'wh_pallet_location_settlement_operation', 'PDA拣货资源原子结算的持久化幂等操作记录'); +CALL queue_table_comment('baiyue_warehouse', 'wh_pallet_location_transfer_operation', '托盘库位归属转移的持久化幂等操作记录'); +CALL queue_table_comment('baiyue_warehouse', 'wh_partner', '平台统一维护、供全部客户共享的往来单位主数据'); +CALL queue_table_comment('baiyue_warehouse', 'wh_partner_type', '平台共享往来单位稳定业务类型'); +CALL queue_table_comment('baiyue_warehouse', 'wh_service_capability', '仓库服务能力表:声明仓库可提供的收货、仓储、拣包、退货等能力及截单时间'); +CALL queue_table_comment('baiyue_warehouse', 'wh_warehouse', '平台公共仓库主数据表:统一归属平台作用域 tenant_id=0'); +CALL queue_table_comment('baiyue_warehouse', 'wh_warehouse_address', '仓库地址表:保存运营、退货等不同用途的地址及可选经纬度'); +CALL queue_column_comment('baiyue_audit', 'sys_audit_dead_letter', 'created_at', '死信记录创建时间'); +CALL queue_column_comment('baiyue_audit', 'sys_audit_dead_letter', 'event_id', '审计事件全局唯一ID'); +CALL queue_column_comment('baiyue_audit', 'sys_audit_dead_letter', 'id', '死信记录自增ID'); +CALL queue_column_comment('baiyue_audit', 'sys_audit_dead_letter', 'last_error', '最近一次补偿错误'); +CALL queue_column_comment('baiyue_audit', 'sys_audit_dead_letter', 'lease_owner', '当前补偿租约持有实例'); +CALL queue_column_comment('baiyue_audit', 'sys_audit_dead_letter', 'lease_until', '当前补偿租约截止时间'); +CALL queue_column_comment('baiyue_audit', 'sys_audit_dead_letter', 'next_retry_at', '下次允许补偿时间'); +CALL queue_column_comment('baiyue_audit', 'sys_audit_dead_letter', 'payload_text', 'RocketMQ死信原始载荷'); +CALL queue_column_comment('baiyue_audit', 'sys_audit_dead_letter', 'processed_at', '补偿处理完成时间'); +CALL queue_column_comment('baiyue_audit', 'sys_audit_dead_letter', 'retry_count', '死信重试次数'); +CALL queue_column_comment('baiyue_audit', 'sys_audit_dead_letter', 'status', '状态:PENDING待补偿、PROCESSING处理中、PROCESSED已处理、DEAD永久失败'); +CALL queue_column_comment('baiyue_audit', 'sys_audit_log', 'after_data', '变更后数据快照(已脱敏)'); +CALL queue_column_comment('baiyue_audit', 'sys_audit_log', 'app_code', '操作来源应用端'); +CALL queue_column_comment('baiyue_audit', 'sys_audit_log', 'before_data', '变更前数据快照(已脱敏)'); +CALL queue_column_comment('baiyue_audit', 'sys_audit_log', 'client_ip', '客户端IP'); +CALL queue_column_comment('baiyue_audit', 'sys_audit_log', 'client_type', '客户端类型'); +CALL queue_column_comment('baiyue_audit', 'sys_audit_log', 'created_at', '审计服务落库时间'); +CALL queue_column_comment('baiyue_audit', 'sys_audit_log', 'description', '操作说明'); +CALL queue_column_comment('baiyue_audit', 'sys_audit_log', 'duration_ms', '请求耗时(毫秒)'); +CALL queue_column_comment('baiyue_audit', 'sys_audit_log', 'error_code', '失败时的错误码'); +CALL queue_column_comment('baiyue_audit', 'sys_audit_log', 'error_message', '脱敏后的失败信息'); +CALL queue_column_comment('baiyue_audit', 'sys_audit_log', 'event_id', '审计事件全局唯一ID'); +CALL queue_column_comment('baiyue_audit', 'sys_audit_log', 'event_type', '审计事件编码'); +CALL queue_column_comment('baiyue_audit', 'sys_audit_log', 'http_method', 'HTTP请求方法'); +CALL queue_column_comment('baiyue_audit', 'sys_audit_log', 'id', '审计日志自增ID'); +CALL queue_column_comment('baiyue_audit', 'sys_audit_log', 'occurred_at', '业务操作发生时间'); +CALL queue_column_comment('baiyue_audit', 'sys_audit_log', 'operator_tenant_id', '操作主体所属客户ID;平台操作可为空或为0'); +CALL queue_column_comment('baiyue_audit', 'sys_audit_log', 'request_id', '请求ID,用于单次请求追踪'); +CALL queue_column_comment('baiyue_audit', 'sys_audit_log', 'request_uri', '请求URI'); +CALL queue_column_comment('baiyue_audit', 'sys_audit_log', 'resource_id', '被操作资源ID'); +CALL queue_column_comment('baiyue_audit', 'sys_audit_log', 'resource_type', '被操作资源类型'); +CALL queue_column_comment('baiyue_audit', 'sys_audit_log', 'service_name', '产生日志的服务名'); +CALL queue_column_comment('baiyue_audit', 'sys_audit_log', 'success', '操作是否成功'); +CALL queue_column_comment('baiyue_audit', 'sys_audit_log', 'tenant_id', '客户ID和数据隔离键;平台级审计可为空或为0'); +CALL queue_column_comment('baiyue_audit', 'sys_audit_log', 'trace_id', '分布式链路追踪ID'); +CALL queue_column_comment('baiyue_audit', 'sys_audit_log', 'user_id', '操作用户ID'); +CALL queue_column_comment('baiyue_audit', 'sys_audit_log', 'username', '操作用户名快照'); +CALL queue_column_comment('baiyue_auth', 'auth_account', 'account_status', '账号状态:ACTIVE正常、DISABLED停用、LOCKED锁定'); +CALL queue_column_comment('baiyue_auth', 'auth_account', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_auth', 'auth_account', 'default_app_code', '默认应用端:MERCHANT企业客户端、PLATFORM平台后台'); +CALL queue_column_comment('baiyue_auth', 'auth_account', 'default_membership_id', '默认客户成员关系ID;平台账号可为空'); +CALL queue_column_comment('baiyue_auth', 'auth_account', 'default_tenant_id', '默认业务作用域ID:0为平台,正数为客户ID'); +CALL queue_column_comment('baiyue_auth', 'auth_account', 'deleted', '逻辑删除标记'); +CALL queue_column_comment('baiyue_auth', 'auth_account', 'display_name', '登录后展示名称'); +CALL queue_column_comment('baiyue_auth', 'auth_account', 'failed_login_count', '连续登录失败次数'); +CALL queue_column_comment('baiyue_auth', 'auth_account', 'id', '统一登录账号ID(雪花ID)'); +CALL queue_column_comment('baiyue_auth', 'auth_account', 'last_login_at', '最近一次成功登录时间'); +CALL queue_column_comment('baiyue_auth', 'auth_account', 'locked_until', '临时锁定截止时间'); +CALL queue_column_comment('baiyue_auth', 'auth_account', 'operator_tenant_id', '操作主体作用域ID:0为平台,正数为客户ID,用于平台端与客户端身份隔离'); +CALL queue_column_comment('baiyue_auth', 'auth_account', 'password_change_required', '管理员重置后是否强制下次登录改密'); +CALL queue_column_comment('baiyue_auth', 'auth_account', 'password_hash', 'BCrypt密码哈希,禁止保存明文密码'); +CALL queue_column_comment('baiyue_auth', 'auth_account', 'token_version', '令牌版本,递增后可使历史令牌失效'); +CALL queue_column_comment('baiyue_auth', 'auth_account', 'updated_at', '更新时间'); +CALL queue_column_comment('baiyue_auth', 'auth_account', 'username', '全局唯一登录名'); +CALL queue_column_comment('baiyue_auth', 'auth_refresh_token', 'app_code', '令牌所属应用端:MERCHANT企业客户端、PLATFORM平台后台'); +CALL queue_column_comment('baiyue_auth', 'auth_refresh_token', 'created_at', '签发时间'); +CALL queue_column_comment('baiyue_auth', 'auth_refresh_token', 'expires_at', '到期时间'); +CALL queue_column_comment('baiyue_auth', 'auth_refresh_token', 'id', '刷新令牌记录ID(雪花ID)'); +CALL queue_column_comment('baiyue_auth', 'auth_refresh_token', 'operator_tenant_id', '令牌操作主体作用域ID:0为平台,正数为客户ID'); +CALL queue_column_comment('baiyue_auth', 'auth_refresh_token', 'revoked_at', '吊销时间,非空表示不可继续使用'); +CALL queue_column_comment('baiyue_auth', 'auth_refresh_token', 'tenant_id', '令牌当前业务作用域ID:0为平台,正数为客户ID'); +CALL queue_column_comment('baiyue_auth', 'auth_refresh_token', 'token_hash', '刷新令牌SHA-256摘要,不保存令牌明文'); +CALL queue_column_comment('baiyue_auth', 'auth_refresh_token', 'user_id', '认证账号ID'); +CALL queue_column_comment('baiyue_auth', 'sys_audit_outbox', 'attempt_count', '当前周期发送次数'); +CALL queue_column_comment('baiyue_auth', 'sys_audit_outbox', 'compensation_count', '自动补偿次数'); +CALL queue_column_comment('baiyue_auth', 'sys_audit_outbox', 'created_at', '记录创建时间'); +CALL queue_column_comment('baiyue_auth', 'sys_audit_outbox', 'dead_at', '进入死信时间'); +CALL queue_column_comment('baiyue_auth', 'sys_audit_outbox', 'event_id', '审计事件全局唯一ID'); +CALL queue_column_comment('baiyue_auth', 'sys_audit_outbox', 'event_type', '审计事件类型'); +CALL queue_column_comment('baiyue_auth', 'sys_audit_outbox', 'last_error', '最近一次发送错误信息'); +CALL queue_column_comment('baiyue_auth', 'sys_audit_outbox', 'lease_owner', '当前租约持有者'); +CALL queue_column_comment('baiyue_auth', 'sys_audit_outbox', 'lease_until', '发送租约截止时间'); +CALL queue_column_comment('baiyue_auth', 'sys_audit_outbox', 'next_retry_at', '下一次重试时间'); +CALL queue_column_comment('baiyue_auth', 'sys_audit_outbox', 'payload_json', '脱敏后的审计事件'); +CALL queue_column_comment('baiyue_auth', 'sys_audit_outbox', 'resource_id', '资源ID'); +CALL queue_column_comment('baiyue_auth', 'sys_audit_outbox', 'resource_type', '资源类型'); +CALL queue_column_comment('baiyue_auth', 'sys_audit_outbox', 'sent_at', '成功发送时间'); +CALL queue_column_comment('baiyue_auth', 'sys_audit_outbox', 'service_name', '来源服务'); +CALL queue_column_comment('baiyue_auth', 'sys_audit_outbox', 'status', '发送状态:PENDING待发送、SENDING发送中、SENT已发送、DEAD进入死信'); +CALL queue_column_comment('baiyue_auth', 'sys_audit_outbox', 'updated_at', '记录更新时间'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_delivery_attempt', 'attempt_no', '投递序号'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_delivery_attempt', 'error_message', '错误信息'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_delivery_attempt', 'event_id', '事件全局标识'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_delivery_attempt', 'finished_at', '完成时间'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_delivery_attempt', 'id', '主键'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_delivery_attempt', 'response_code', '对端响应码'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_delivery_attempt', 'source_region', '来源区域'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_delivery_attempt', 'started_at', '开始时间'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_delivery_attempt', 'status', '投递结果'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_delivery_attempt', 'target_region', '目标区域'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_inbox', 'aggregate_id', '聚合标识'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_inbox', 'aggregate_type', '聚合类型'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_inbox', 'aggregate_version', '聚合版本'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_inbox', 'causation_id', '上游事件标识'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_inbox', 'correlation_id', '业务关联标识'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_inbox', 'event_id', '事件全局标识'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_inbox', 'event_type', '事件类型'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_inbox', 'event_version', '事件契约版本'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_inbox', 'handover_version', '跨区交接版本'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_inbox', 'idempotency_key', '接收幂等键'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_inbox', 'last_error', '最后错误'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_inbox', 'lease_owner', '消费租约持有者'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_inbox', 'lease_until', '消费租约截止时间'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_inbox', 'next_retry_at', '下次消费时间'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_inbox', 'occurred_at', '业务事实发生时间'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_inbox', 'payload_hash', '规范化负载摘要'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_inbox', 'payload_json', '事件负载'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_inbox', 'processed_at', '消费完成时间'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_inbox', 'received_at', '接收时间'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_inbox', 'retry_count', '消费重试次数'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_inbox', 'source_region', '来源区域'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_inbox', 'status', '消费状态'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_inbox', 'target_region', '目标区域'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_inbox', 'tenant_id', '客户或业务隔离标识;跨区域事件沿用来源系统口径'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_inbox', 'trace_id', '链路标识'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_outbox', 'aggregate_id', '聚合标识'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_outbox', 'aggregate_type', '聚合类型'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_outbox', 'aggregate_version', '聚合版本'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_outbox', 'causation_id', '上游事件标识'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_outbox', 'correlation_id', '业务关联标识'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_outbox', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_outbox', 'delivered_at', '投递成功时间'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_outbox', 'event_id', '事件全局标识'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_outbox', 'event_type', '事件类型'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_outbox', 'event_version', '事件契约版本'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_outbox', 'handover_version', '跨区交接版本'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_outbox', 'idempotency_key', '发送幂等键'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_outbox', 'last_error', '最后错误'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_outbox', 'lease_owner', '租约持有者'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_outbox', 'lease_until', '租约截止时间'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_outbox', 'next_retry_at', '下次重试时间'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_outbox', 'occurred_at', '业务事实发生时间'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_outbox', 'payload_hash', '规范化负载摘要'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_outbox', 'payload_json', '事件负载'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_outbox', 'retry_count', '已重试次数'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_outbox', 'source_region', '来源区域'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_outbox', 'status', '投递状态'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_outbox', 'target_region', '目标区域'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_outbox', 'tenant_id', '客户或业务隔离标识;跨区域事件沿用来源系统口径'); +CALL queue_column_comment('baiyue_event_bridge', 'bridge_outbox', 'trace_id', '链路标识'); +CALL queue_column_comment('baiyue_file', 'file_download_log', 'client_ip', '客户端IP'); +CALL queue_column_comment('baiyue_file', 'file_download_log', 'downloaded_at', '下载时间'); +CALL queue_column_comment('baiyue_file', 'file_download_log', 'failure_reason', '失败原因'); +CALL queue_column_comment('baiyue_file', 'file_download_log', 'file_id', '文件逻辑ID'); +CALL queue_column_comment('baiyue_file', 'file_download_log', 'id', '下载日志ID'); +CALL queue_column_comment('baiyue_file', 'file_download_log', 'request_id', '请求ID'); +CALL queue_column_comment('baiyue_file', 'file_download_log', 'result', '下载结果'); +CALL queue_column_comment('baiyue_file', 'file_download_log', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_file', 'file_download_log', 'user_id', '下载用户ID'); +CALL queue_column_comment('baiyue_file', 'file_download_token', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_file', 'file_download_token', 'created_by', '创建人ID'); +CALL queue_column_comment('baiyue_file', 'file_download_token', 'download_count', '已下载次数'); +CALL queue_column_comment('baiyue_file', 'file_download_token', 'expires_at', '过期时间'); +CALL queue_column_comment('baiyue_file', 'file_download_token', 'file_id', '文件逻辑ID'); +CALL queue_column_comment('baiyue_file', 'file_download_token', 'id', '下载令牌ID'); +CALL queue_column_comment('baiyue_file', 'file_download_token', 'max_downloads', '最大下载次数'); +CALL queue_column_comment('baiyue_file', 'file_download_token', 'revoked_at', '撤销时间'); +CALL queue_column_comment('baiyue_file', 'file_download_token', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_file', 'file_download_token', 'token_hash', '下载令牌SHA-256摘要'); +CALL queue_column_comment('baiyue_file', 'file_export_outbox', 'attempt_count', '累计发送尝试次数'); +CALL queue_column_comment('baiyue_file', 'file_export_outbox', 'created_at', 'Outbox记录创建时间'); +CALL queue_column_comment('baiyue_file', 'file_export_outbox', 'dispatch_version', '任务派发版本'); +CALL queue_column_comment('baiyue_file', 'file_export_outbox', 'event_id', '投递意图唯一标识'); +CALL queue_column_comment('baiyue_file', 'file_export_outbox', 'id', 'Outbox记录ID'); +CALL queue_column_comment('baiyue_file', 'file_export_outbox', 'last_error', '最后发送错误'); +CALL queue_column_comment('baiyue_file', 'file_export_outbox', 'lease_owner', '领取实例标识'); +CALL queue_column_comment('baiyue_file', 'file_export_outbox', 'lease_until', '领取租约截止时间'); +CALL queue_column_comment('baiyue_file', 'file_export_outbox', 'message_key', '消息Key'); +CALL queue_column_comment('baiyue_file', 'file_export_outbox', 'next_retry_at', '允许发送的最早时间'); +CALL queue_column_comment('baiyue_file', 'file_export_outbox', 'payload_json', '命令消息体'); +CALL queue_column_comment('baiyue_file', 'file_export_outbox', 'sent_at', '发送成功时间'); +CALL queue_column_comment('baiyue_file', 'file_export_outbox', 'status', '状态:PENDING待发送、CLAIMED已领取、SENT已发送、QUARANTINED隔离'); +CALL queue_column_comment('baiyue_file', 'file_export_outbox', 'tag', '消息Tag'); +CALL queue_column_comment('baiyue_file', 'file_export_outbox', 'task_id', '关联导出任务ID'); +CALL queue_column_comment('baiyue_file', 'file_export_outbox', 'topic', '目标Topic'); +CALL queue_column_comment('baiyue_file', 'file_export_outbox', 'updated_at', 'Outbox记录更新时间'); +CALL queue_column_comment('baiyue_file', 'file_export_task', 'app_code', '申请端:PLATFORM或MERCHANT'); +CALL queue_column_comment('baiyue_file', 'file_export_task', 'attempt_count', '已执行尝试次数'); +CALL queue_column_comment('baiyue_file', 'file_export_task', 'completed_at', '完成时间'); +CALL queue_column_comment('baiyue_file', 'file_export_task', 'content_type', '文件类型'); +CALL queue_column_comment('baiyue_file', 'file_export_task', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_file', 'file_export_task', 'created_by', '创建人ID'); +CALL queue_column_comment('baiyue_file', 'file_export_task', 'deleted', '逻辑删除标记'); +CALL queue_column_comment('baiyue_file', 'file_export_task', 'dispatch_version', '派发版本,仅执行重试时递增'); +CALL queue_column_comment('baiyue_file', 'file_export_task', 'execution_deadline_at', '本次执行截止时间'); +CALL queue_column_comment('baiyue_file', 'file_export_task', 'expires_at', '文件过期时间'); +CALL queue_column_comment('baiyue_file', 'file_export_task', 'export_type', '导出类型'); +CALL queue_column_comment('baiyue_file', 'file_export_task', 'failure_message', '失败原因'); +CALL queue_column_comment('baiyue_file', 'file_export_task', 'file_name', '下载文件名'); +CALL queue_column_comment('baiyue_file', 'file_export_task', 'format', '导出格式'); +CALL queue_column_comment('baiyue_file', 'file_export_task', 'id', '导出任务ID(雪花ID)'); +CALL queue_column_comment('baiyue_file', 'file_export_task', 'last_heartbeat_at', '最后心跳时间'); +CALL queue_column_comment('baiyue_file', 'file_export_task', 'lease_owner', '当前执行实例标识'); +CALL queue_column_comment('baiyue_file', 'file_export_task', 'lease_until', '执行租约截止时间'); +CALL queue_column_comment('baiyue_file', 'file_export_task', 'max_attempts', '自动重试上限'); +CALL queue_column_comment('baiyue_file', 'file_export_task', 'next_retry_at', '允许执行的最早时间'); +CALL queue_column_comment('baiyue_file', 'file_export_task', 'object_key', '存储对象键'); +CALL queue_column_comment('baiyue_file', 'file_export_task', 'parameters_json', '筛选条件快照'); +CALL queue_column_comment('baiyue_file', 'file_export_task', 'processed_rows', '已处理行数'); +CALL queue_column_comment('baiyue_file', 'file_export_task', 'requested_at', '申请时间'); +CALL queue_column_comment('baiyue_file', 'file_export_task', 'requested_by', '申请用户ID'); +CALL queue_column_comment('baiyue_file', 'file_export_task', 'requested_username', '申请账号快照'); +CALL queue_column_comment('baiyue_file', 'file_export_task', 'size_bytes', '文件字节数'); +CALL queue_column_comment('baiyue_file', 'file_export_task', 'started_at', '开始时间'); +CALL queue_column_comment('baiyue_file', 'file_export_task', 'status', '状态:PENDING待处理、RUNNING执行中、SUCCEEDED成功、FAILED失败、EXPIRED已过期'); +CALL queue_column_comment('baiyue_file', 'file_export_task', 'task_name', '任务名称'); +CALL queue_column_comment('baiyue_file', 'file_export_task', 'task_no', '导出任务编号'); +CALL queue_column_comment('baiyue_file', 'file_export_task', 'tenant_id', '客户ID和数据隔离键;平台任务固定为0'); +CALL queue_column_comment('baiyue_file', 'file_export_task', 'total_rows', '预计总行数'); +CALL queue_column_comment('baiyue_file', 'file_export_task', 'updated_at', '更新时间'); +CALL queue_column_comment('baiyue_file', 'file_export_task', 'updated_by', '最后修改人ID'); +CALL queue_column_comment('baiyue_file', 'file_export_task', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_file', 'file_export_task_guard', 'guard_key', '任务创建串行化锁键'); +CALL queue_column_comment('baiyue_file', 'file_export_task_guard', 'updated_at', '锁记录最后更新时间'); +CALL queue_column_comment('baiyue_file', 'file_object', 'bucket_name', '存储桶名称'); +CALL queue_column_comment('baiyue_file', 'file_object', 'business_id', '关联业务逻辑ID'); +CALL queue_column_comment('baiyue_file', 'file_object', 'business_type', '关联业务类型'); +CALL queue_column_comment('baiyue_file', 'file_object', 'content_type', '文件内容类型'); +CALL queue_column_comment('baiyue_file', 'file_object', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_file', 'file_object', 'created_by', '创建人ID'); +CALL queue_column_comment('baiyue_file', 'file_object', 'deleted', '逻辑删除标记'); +CALL queue_column_comment('baiyue_file', 'file_object', 'id', '文件ID(雪花ID)'); +CALL queue_column_comment('baiyue_file', 'file_object', 'object_key', '对象存储键'); +CALL queue_column_comment('baiyue_file', 'file_object', 'original_name', '原始文件名'); +CALL queue_column_comment('baiyue_file', 'file_object', 'sha256', '文件SHA-256摘要'); +CALL queue_column_comment('baiyue_file', 'file_object', 'size_bytes', '文件字节数'); +CALL queue_column_comment('baiyue_file', 'file_object', 'status', '文件状态'); +CALL queue_column_comment('baiyue_file', 'file_object', 'storage_provider', '存储提供方'); +CALL queue_column_comment('baiyue_file', 'file_object', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_file', 'file_object', 'updated_at', '更新时间'); +CALL queue_column_comment('baiyue_file', 'file_object', 'updated_by', '最后修改人ID'); +CALL queue_column_comment('baiyue_file', 'file_object', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_finance', 'fin_audit_log', 'action', '审计业务动作类型'); +CALL queue_column_comment('baiyue_finance', 'fin_audit_log', 'after_data', '操作后业务数据JSON快照'); +CALL queue_column_comment('baiyue_finance', 'fin_audit_log', 'before_data', '操作前业务数据JSON快照'); +CALL queue_column_comment('baiyue_finance', 'fin_audit_log', 'created_at', '不可变审计记录创建时间'); +CALL queue_column_comment('baiyue_finance', 'fin_audit_log', 'id', '主键ID'); +CALL queue_column_comment('baiyue_finance', 'fin_audit_log', 'operator_id', '操作人ID'); +CALL queue_column_comment('baiyue_finance', 'fin_audit_log', 'operator_name_snapshot', '操作发生时的操作人名称快照'); +CALL queue_column_comment('baiyue_finance', 'fin_audit_log', 'reason', '操作原因或说明'); +CALL queue_column_comment('baiyue_finance', 'fin_audit_log', 'request_id', '入口请求ID,用于问题定位'); +CALL queue_column_comment('baiyue_finance', 'fin_audit_log', 'resource_id', '被审计财务资源ID'); +CALL queue_column_comment('baiyue_finance', 'fin_audit_log', 'resource_type', '被审计财务资源类型'); +CALL queue_column_comment('baiyue_finance', 'fin_audit_log', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_finance', 'fin_audit_log', 'trace_id', '分布式调用链追踪ID'); +CALL queue_column_comment('baiyue_finance', 'fin_bill', 'bill_date', '账单日期'); +CALL queue_column_comment('baiyue_finance', 'fin_bill', 'bill_no', '客户范围内唯一账单编号'); +CALL queue_column_comment('baiyue_finance', 'fin_bill', 'created_at', '记录创建时间'); +CALL queue_column_comment('baiyue_finance', 'fin_bill', 'created_by', '创建操作人ID'); +CALL queue_column_comment('baiyue_finance', 'fin_bill', 'currency_code', 'ISO 4217三位币种代码,首期仅CNY'); +CALL queue_column_comment('baiyue_finance', 'fin_bill', 'customer_name_snapshot', '出账时客户名称快照'); +CALL queue_column_comment('baiyue_finance', 'fin_bill', 'due_date', '到期日期,由账单日期加客户账期计算'); +CALL queue_column_comment('baiyue_finance', 'fin_bill', 'id', '主键ID'); +CALL queue_column_comment('baiyue_finance', 'fin_bill', 'issued_at', '出账时间,未出账时为空'); +CALL queue_column_comment('baiyue_finance', 'fin_bill', 'issued_by', '出账操作人ID,未出账时为空'); +CALL queue_column_comment('baiyue_finance', 'fin_bill', 'outstanding_amount', '账单未收余额,精度6位小数'); +CALL queue_column_comment('baiyue_finance', 'fin_bill', 'received_amount', '账单累计已收金额,精度6位小数'); +CALL queue_column_comment('baiyue_finance', 'fin_bill', 'remark', '平台账单内部备注'); +CALL queue_column_comment('baiyue_finance', 'fin_bill', 'status', '账单状态:DRAFT草稿、ISSUED已出账、PARTIALLY_PAID部分收款、PAID已收清、VOIDED已作废'); +CALL queue_column_comment('baiyue_finance', 'fin_bill', 'subject_id', '平台财务主体ID'); +CALL queue_column_comment('baiyue_finance', 'fin_bill', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_finance', 'fin_bill', 'total_amount', '账单全部明细总金额,精度6位小数'); +CALL queue_column_comment('baiyue_finance', 'fin_bill', 'updated_at', '最后更新时间,数据修改时自动刷新'); +CALL queue_column_comment('baiyue_finance', 'fin_bill', 'updated_by', '最后修改操作人ID'); +CALL queue_column_comment('baiyue_finance', 'fin_bill', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_finance', 'fin_bill', 'void_reason', '账单作废原因'); +CALL queue_column_comment('baiyue_finance', 'fin_bill', 'voided_at', '作废时间,未作废时为空'); +CALL queue_column_comment('baiyue_finance', 'fin_bill', 'voided_by', '作废操作人ID,未作废时为空'); +CALL queue_column_comment('baiyue_finance', 'fin_bill_line', 'amount', '账单明细金额,精度6位小数'); +CALL queue_column_comment('baiyue_finance', 'fin_bill_line', 'bill_id', '所属账单ID'); +CALL queue_column_comment('baiyue_finance', 'fin_bill_line', 'created_at', '账单明细创建时间'); +CALL queue_column_comment('baiyue_finance', 'fin_bill_line', 'currency_code', 'ISO 4217三位币种代码,首期仅CNY'); +CALL queue_column_comment('baiyue_finance', 'fin_bill_line', 'fee_item_code', '出账时费用项目编码快照'); +CALL queue_column_comment('baiyue_finance', 'fin_bill_line', 'fee_item_name', '出账时费用项目名称快照'); +CALL queue_column_comment('baiyue_finance', 'fin_bill_line', 'id', '主键ID'); +CALL queue_column_comment('baiyue_finance', 'fin_bill_line', 'inbound_id', '来源入库单ID,可为空'); +CALL queue_column_comment('baiyue_finance', 'fin_bill_line', 'order_id', '来源头程订单ID,可为空'); +CALL queue_column_comment('baiyue_finance', 'fin_bill_line', 'source_id', '来源费用ID'); +CALL queue_column_comment('baiyue_finance', 'fin_bill_line', 'source_no', '来源费用单号快照,可为空'); +CALL queue_column_comment('baiyue_finance', 'fin_bill_line', 'source_type', '费用来源类型,FIRSTLEG_FEE表示头程费用'); +CALL queue_column_comment('baiyue_finance', 'fin_bill_line', 'source_version', '出账固化的来源费用版本号'); +CALL queue_column_comment('baiyue_finance', 'fin_bill_line', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_finance', 'fin_bill_revision', 'bill_id', '被调整账单ID'); +CALL queue_column_comment('baiyue_finance', 'fin_bill_revision', 'bill_snapshot', '调整前账单主记录不可变JSON快照'); +CALL queue_column_comment('baiyue_finance', 'fin_bill_revision', 'created_at', '修订快照创建时间'); +CALL queue_column_comment('baiyue_finance', 'fin_bill_revision', 'created_by', '执行调整的操作人ID'); +CALL queue_column_comment('baiyue_finance', 'fin_bill_revision', 'id', '主键ID'); +CALL queue_column_comment('baiyue_finance', 'fin_bill_revision', 'line_snapshot', '调整前全部账单明细不可变JSON数组快照'); +CALL queue_column_comment('baiyue_finance', 'fin_bill_revision', 'reason', '本次账单调整原因'); +CALL queue_column_comment('baiyue_finance', 'fin_bill_revision', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_finance', 'fin_bill_revision', 'version', '调整前账单乐观锁版本号'); +CALL queue_column_comment('baiyue_finance', 'fin_cost', 'confirmed_at', '成本确认时间'); +CALL queue_column_comment('baiyue_finance', 'fin_cost', 'confirmed_by', '成本确认操作人ID'); +CALL queue_column_comment('baiyue_finance', 'fin_cost', 'container_id', 'Firstleg装柜计划ID,整柜公共成本必填'); +CALL queue_column_comment('baiyue_finance', 'fin_cost', 'cost_amount', '成本总金额,精度6位小数'); +CALL queue_column_comment('baiyue_finance', 'fin_cost', 'cost_no', '客户范围内唯一成本编号'); +CALL queue_column_comment('baiyue_finance', 'fin_cost', 'cost_type', '成本归属类型:SINGLE_ORDER单票成本、CONTAINER_SHARED整柜公共成本'); +CALL queue_column_comment('baiyue_finance', 'fin_cost', 'created_at', '记录创建时间'); +CALL queue_column_comment('baiyue_finance', 'fin_cost', 'created_by', '创建操作人ID'); +CALL queue_column_comment('baiyue_finance', 'fin_cost', 'currency_code', 'ISO 4217三位币种代码,首期仅CNY'); +CALL queue_column_comment('baiyue_finance', 'fin_cost', 'fee_item_code', '成本费用项目编码快照'); +CALL queue_column_comment('baiyue_finance', 'fin_cost', 'fee_item_name', '成本费用项目名称快照'); +CALL queue_column_comment('baiyue_finance', 'fin_cost', 'id', '主键ID'); +CALL queue_column_comment('baiyue_finance', 'fin_cost', 'inbound_id', '成本关联的入库单ID,可为空'); +CALL queue_column_comment('baiyue_finance', 'fin_cost', 'order_id', '成本归属的头程订单ID,单票成本必填'); +CALL queue_column_comment('baiyue_finance', 'fin_cost', 'remark', '平台成本内部备注'); +CALL queue_column_comment('baiyue_finance', 'fin_cost', 'status', '成本状态:DRAFT草稿、CONFIRMED已确认、VOIDED已作废'); +CALL queue_column_comment('baiyue_finance', 'fin_cost', 'supplier_id', 'Warehouse公共往来单位或供应商引用ID'); +CALL queue_column_comment('baiyue_finance', 'fin_cost', 'supplier_name_snapshot', '登记成本时供应商或成本对象名称快照'); +CALL queue_column_comment('baiyue_finance', 'fin_cost', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_finance', 'fin_cost', 'updated_at', '最后更新时间,数据修改时自动刷新'); +CALL queue_column_comment('baiyue_finance', 'fin_cost', 'updated_by', '最后修改操作人ID'); +CALL queue_column_comment('baiyue_finance', 'fin_cost', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_finance', 'fin_cost', 'void_reason', '成本作废原因'); +CALL queue_column_comment('baiyue_finance', 'fin_cost', 'voided_at', '成本作废时间'); +CALL queue_column_comment('baiyue_finance', 'fin_cost', 'voided_by', '成本作废操作人ID'); +CALL queue_column_comment('baiyue_finance', 'fin_cost', 'voucher_file_ids', 'File服务财务凭证文件ID字符串数组JSON'); +CALL queue_column_comment('baiyue_finance', 'fin_cost_allocation', 'allocated_amount', '分摊到该订单的成本金额,精度6位'); +CALL queue_column_comment('baiyue_finance', 'fin_cost_allocation', 'allocation_rate', '订单体积占总体积的分摊比例,精度10位'); +CALL queue_column_comment('baiyue_finance', 'fin_cost_allocation', 'cost_id', '整柜公共成本ID'); +CALL queue_column_comment('baiyue_finance', 'fin_cost_allocation', 'created_at', '不可变分摊快照创建时间'); +CALL queue_column_comment('baiyue_finance', 'fin_cost_allocation', 'id', '主键ID'); +CALL queue_column_comment('baiyue_finance', 'fin_cost_allocation', 'inbound_id', '参与分摊订单关联的入库单ID,可为空'); +CALL queue_column_comment('baiyue_finance', 'fin_cost_allocation', 'order_id', '参与分摊的头程订单ID'); +CALL queue_column_comment('baiyue_finance', 'fin_cost_allocation', 'rounding_adjustment', '尾差归集标记:0未归集、1该行为实际尾差归集行'); +CALL queue_column_comment('baiyue_finance', 'fin_cost_allocation', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_finance', 'fin_cost_allocation', 'total_volume_cbm', '柜内有效订单总体积快照,单位CBM、精度6位'); +CALL queue_column_comment('baiyue_finance', 'fin_cost_allocation', 'volume_cbm', '该订单分摊时有效体积快照,单位CBM、精度6位'); +CALL queue_column_comment('baiyue_finance', 'fin_customer_settlement_profile', 'created_at', '记录创建时间'); +CALL queue_column_comment('baiyue_finance', 'fin_customer_settlement_profile', 'created_by', '创建操作人ID'); +CALL queue_column_comment('baiyue_finance', 'fin_customer_settlement_profile', 'credit_days', '客户账期自然日数,默认30天'); +CALL queue_column_comment('baiyue_finance', 'fin_customer_settlement_profile', 'customer_name_snapshot', '客户名称快照,客户主数据后续变更不追改'); +CALL queue_column_comment('baiyue_finance', 'fin_customer_settlement_profile', 'id', '主键ID'); +CALL queue_column_comment('baiyue_finance', 'fin_customer_settlement_profile', 'status', '客户结算配置状态,ACTIVE表示启用'); +CALL queue_column_comment('baiyue_finance', 'fin_customer_settlement_profile', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_finance', 'fin_customer_settlement_profile', 'updated_at', '最后更新时间,数据修改时自动刷新'); +CALL queue_column_comment('baiyue_finance', 'fin_customer_settlement_profile', 'updated_by', '最后修改操作人ID'); +CALL queue_column_comment('baiyue_finance', 'fin_customer_settlement_profile', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_finance', 'fin_fee_source', 'amount', '来源费用金额,精度6位小数'); +CALL queue_column_comment('baiyue_finance', 'fin_fee_source', 'container_id', '来源Firstleg装柜计划ID,可为空'); +CALL queue_column_comment('baiyue_finance', 'fin_fee_source', 'created_at', '来源快照入池时间'); +CALL queue_column_comment('baiyue_finance', 'fin_fee_source', 'currency_code', 'ISO 4217三位币种代码,首期仅CNY'); +CALL queue_column_comment('baiyue_finance', 'fin_fee_source', 'customer_id', '费用来源所属客户ID'); +CALL queue_column_comment('baiyue_finance', 'fin_fee_source', 'direction', '费用方向:RECEIVABLE应收、PAYABLE应付'); +CALL queue_column_comment('baiyue_finance', 'fin_fee_source', 'fee_item_code', '来源费用项目编码快照'); +CALL queue_column_comment('baiyue_finance', 'fin_fee_source', 'fee_item_name', '来源费用项目名称快照'); +CALL queue_column_comment('baiyue_finance', 'fin_fee_source', 'id', '主键ID'); +CALL queue_column_comment('baiyue_finance', 'fin_fee_source', 'inbound_id', '来源入库单ID,可为空'); +CALL queue_column_comment('baiyue_finance', 'fin_fee_source', 'order_id', '来源头程订单ID,可为空'); +CALL queue_column_comment('baiyue_finance', 'fin_fee_source', 'source_fee_id', '来源系统费用记录ID'); +CALL queue_column_comment('baiyue_finance', 'fin_fee_source', 'source_fee_version', '来源费用版本号,用于固化不可变来源版本'); +CALL queue_column_comment('baiyue_finance', 'fin_fee_source', 'source_hash', 'Firstleg费用来源完整快照SHA-256哈希'); +CALL queue_column_comment('baiyue_finance', 'fin_fee_source', 'source_no', '来源入库单号或柜号快照,可为空'); +CALL queue_column_comment('baiyue_finance', 'fin_fee_source', 'source_snapshot', 'Firstleg费用来源完整不可变JSON快照'); +CALL queue_column_comment('baiyue_finance', 'fin_fee_source', 'source_system', '费用来源系统,当前FIRSTLEG表示头程服务'); +CALL queue_column_comment('baiyue_finance', 'fin_fee_source', 'source_type', '来源费用类型:INBOUND_FEE入库单费用、CONTAINER_FEE柜子费用'); +CALL queue_column_comment('baiyue_finance', 'fin_fee_source', 'status', '费用来源状态:AVAILABLE可结算、SUPERSEDED已被新版本替代、VOIDED已作废'); +CALL queue_column_comment('baiyue_finance', 'fin_fee_source', 'updated_at', '来源状态最后更新时间,数据修改时自动刷新'); +CALL queue_column_comment('baiyue_finance', 'fin_idempotency_record', 'created_at', '幂等请求首次登记时间'); +CALL queue_column_comment('baiyue_finance', 'fin_idempotency_record', 'error_code', '失败操作的稳定业务错误码'); +CALL queue_column_comment('baiyue_finance', 'fin_idempotency_record', 'id', '主键ID'); +CALL queue_column_comment('baiyue_finance', 'fin_idempotency_record', 'idempotency_key', '调用方提供的幂等键'); +CALL queue_column_comment('baiyue_finance', 'fin_idempotency_record', 'operation', '幂等隔离的财务操作类型'); +CALL queue_column_comment('baiyue_finance', 'fin_idempotency_record', 'operator_id', '发起幂等写操作的操作人ID'); +CALL queue_column_comment('baiyue_finance', 'fin_idempotency_record', 'request_hash', '规范化完整请求的SHA-256十六进制哈希'); +CALL queue_column_comment('baiyue_finance', 'fin_idempotency_record', 'result_resource_id', '成功操作生成或更新的业务资源ID'); +CALL queue_column_comment('baiyue_finance', 'fin_idempotency_record', 'status', '幂等处理状态:PROCESSING处理中、SUCCEEDED成功、FAILED失败'); +CALL queue_column_comment('baiyue_finance', 'fin_idempotency_record', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_finance', 'fin_idempotency_record', 'updated_at', '幂等处理状态最后更新时间'); +CALL queue_column_comment('baiyue_finance', 'fin_inbox_event', 'error_code', '处理失败时的稳定FIN错误码'); +CALL queue_column_comment('baiyue_finance', 'fin_inbox_event', 'event_id', '来源系统事件唯一ID'); +CALL queue_column_comment('baiyue_finance', 'fin_inbox_event', 'event_type', '来源事件类型'); +CALL queue_column_comment('baiyue_finance', 'fin_inbox_event', 'id', '主键ID'); +CALL queue_column_comment('baiyue_finance', 'fin_inbox_event', 'payload', '来源事件原始JSON载荷快照'); +CALL queue_column_comment('baiyue_finance', 'fin_inbox_event', 'payload_hash', '规范化事件内容SHA-256哈希,用于同事件ID冲突检测'); +CALL queue_column_comment('baiyue_finance', 'fin_inbox_event', 'processed_at', '事件处理完成时间,未完成时为空'); +CALL queue_column_comment('baiyue_finance', 'fin_inbox_event', 'received_at', '事件接收时间'); +CALL queue_column_comment('baiyue_finance', 'fin_inbox_event', 'result_resource_id', '成功处理生成或定位的费用来源快照ID'); +CALL queue_column_comment('baiyue_finance', 'fin_inbox_event', 'source_system', '事件来源系统标识'); +CALL queue_column_comment('baiyue_finance', 'fin_inbox_event', 'status', '事件处理状态:RECEIVED已接收、PROCESSED已完成、FAILED处理失败'); +CALL queue_column_comment('baiyue_finance', 'fin_receipt', 'created_at', '记录创建时间'); +CALL queue_column_comment('baiyue_finance', 'fin_receipt', 'created_by', '创建操作人ID'); +CALL queue_column_comment('baiyue_finance', 'fin_receipt', 'currency_code', 'ISO 4217三位币种代码,首期仅CNY'); +CALL queue_column_comment('baiyue_finance', 'fin_receipt', 'id', '主键ID'); +CALL queue_column_comment('baiyue_finance', 'fin_receipt', 'payment_method', '收款方式编码'); +CALL queue_column_comment('baiyue_finance', 'fin_receipt', 'receipt_no', '客户范围内唯一收款编号'); +CALL queue_column_comment('baiyue_finance', 'fin_receipt', 'received_amount', '本次实际到账金额,精度6位小数'); +CALL queue_column_comment('baiyue_finance', 'fin_receipt', 'received_at', '实际到账时间'); +CALL queue_column_comment('baiyue_finance', 'fin_receipt', 'remark', '平台收款内部备注'); +CALL queue_column_comment('baiyue_finance', 'fin_receipt', 'status', '收款状态:CONFIRMED已确认、VOIDED已作废'); +CALL queue_column_comment('baiyue_finance', 'fin_receipt', 'subject_id', '实际收款的平台财务主体ID'); +CALL queue_column_comment('baiyue_finance', 'fin_receipt', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_finance', 'fin_receipt', 'updated_at', '最后更新时间,数据修改时自动刷新'); +CALL queue_column_comment('baiyue_finance', 'fin_receipt', 'updated_by', '最后修改操作人ID'); +CALL queue_column_comment('baiyue_finance', 'fin_receipt', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_finance', 'fin_receipt', 'void_reason', '收款作废原因'); +CALL queue_column_comment('baiyue_finance', 'fin_receipt', 'voided_at', '收款作废时间'); +CALL queue_column_comment('baiyue_finance', 'fin_receipt', 'voided_by', '收款作废操作人ID'); +CALL queue_column_comment('baiyue_finance', 'fin_receipt', 'voucher_file_ids', 'File服务财务凭证文件ID字符串数组JSON'); +CALL queue_column_comment('baiyue_finance', 'fin_receipt', 'voucher_no', '银行流水号或线下收款凭证号'); +CALL queue_column_comment('baiyue_finance', 'fin_receipt_bill', 'applied_amount', '本笔收款分配到该账单的金额,精度6位小数'); +CALL queue_column_comment('baiyue_finance', 'fin_receipt_bill', 'bill_id', '被分配收款的账单ID'); +CALL queue_column_comment('baiyue_finance', 'fin_receipt_bill', 'created_at', '收款分配创建时间'); +CALL queue_column_comment('baiyue_finance', 'fin_receipt_bill', 'created_by', '分配操作人ID'); +CALL queue_column_comment('baiyue_finance', 'fin_receipt_bill', 'id', '主键ID'); +CALL queue_column_comment('baiyue_finance', 'fin_receipt_bill', 'receipt_id', '收款记录ID'); +CALL queue_column_comment('baiyue_finance', 'fin_receipt_bill', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_finance', 'fin_subject', 'created_at', '记录创建时间'); +CALL queue_column_comment('baiyue_finance', 'fin_subject', 'created_by', '创建操作人ID,0表示系统初始化'); +CALL queue_column_comment('baiyue_finance', 'fin_subject', 'id', '主键ID'); +CALL queue_column_comment('baiyue_finance', 'fin_subject', 'status', '财务主体状态,ACTIVE表示启用'); +CALL queue_column_comment('baiyue_finance', 'fin_subject', 'subject_code', '平台财务主体唯一编码'); +CALL queue_column_comment('baiyue_finance', 'fin_subject', 'subject_name', '平台收付款及核算主体名称'); +CALL queue_column_comment('baiyue_finance', 'fin_subject', 'updated_at', '最后更新时间,数据修改时自动刷新'); +CALL queue_column_comment('baiyue_finance', 'fin_subject', 'updated_by', '最后修改操作人ID,0表示系统初始化'); +CALL queue_column_comment('baiyue_finance', 'fin_subject', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_assignment_history', 'assignee_id', '新负责人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_assignment_history', 'assignee_name', '新负责人名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_assignment_history', 'created_at', '指派发生时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_assignment_history', 'id', '指派历史ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_assignment_history', 'operator_id', '执行指派的操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_assignment_history', 'previous_assignee_id', '原负责人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_assignment_history', 'reason', '指派或改派原因'); +CALL queue_column_comment('baiyue_firstleg', 'fl_assignment_history', 'task_id', '所属履约任务ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_assignment_history', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_fba_rate', 'additional_fee', '固定附加费'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_fba_rate', 'business_type', '业务或计费类型'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_fba_rate', 'carrier_id', '船司逻辑ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_fba_rate', 'carrier_name', '历史承运商名称;渠道价格可为空'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_fba_rate', 'channel_code', '渠道编码'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_fba_rate', 'charge_unit', '计费单位:KG或CBM'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_fba_rate', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_fba_rate', 'created_by', '创建人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_fba_rate', 'currency', '币种'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_fba_rate', 'deleted', '逻辑删除标记'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_fba_rate', 'destination_code', 'FBA目的仓编码;区域阶梯模型为空'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_fba_rate', 'destination_name', '目的仓名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_fba_rate', 'destination_type', '目的地类型:FBA或OVERSEAS;REGION_WEIGHT_TIER区域阶梯行为空'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_fba_rate', 'destination_warehouse_id', '平台海外仓稳定ID;不存环境专属ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_fba_rate', 'effective_from', '生效开始时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_fba_rate', 'effective_to', '生效结束时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_fba_rate', 'id', '船司FBA价格ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_fba_rate', 'origin_warehouse_code', '起运仓稳定编码'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_fba_rate', 'origin_warehouse_id', '确认前可为空;确认后绑定平台公共起运仓逻辑ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_fba_rate', 'origin_warehouse_name', '起运仓名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_fba_rate', 'price_book_version_id', '整表价格版本ID;历史兼容价格可为空'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_fba_rate', 'price_model', '价格模型'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_fba_rate', 'pricing_tax_mode', '报价包税维度;独立于订单tax_mode'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_fba_rate', 'rate_code', '平台公共价格编码'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_fba_rate', 'rate_version', '价格版本号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_fba_rate', 'region_code', '区域阶梯模型区域编码'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_fba_rate', 'remark', '价格备注'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_fba_rate', 'source_destination_text', '原始组合仓库或区域文本'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_fba_rate', 'source_row_number', '原始Excel行号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_fba_rate', 'source_sheet_name', '原始Excel Sheet'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_fba_rate', 'status', '价格状态:PENDING_CONFIRMATION、ACTIVE或SUPERSEDED;分别表示待确认、已生效、已被替代;FAILED表示解析失败'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_fba_rate', 'tenant_id', '平台价格作用域ID,船司价格固定为0'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_fba_rate', 'unit_price', '基础单价'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_fba_rate', 'updated_at', '更新时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_fba_rate', 'updated_by', '最后修改人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_fba_rate', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_fba_rate', 'weight_from_kg', '区域阶梯起始计费重量KG'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query', 'access_mode', '官网访问方式:PUBLIC_FORM公开表单、HCAPTCHA人机校验、DYNAMIC_IFRAME动态页面'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query', 'attempt_count', '当前查询窗口已执行次数;最多3次'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query', 'carrier_code', '船公司编码:MATSON美森、SML森罗、ZIM以星、COSCO中远海运、EMC长荣、HEDE合德'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query', 'container_booking_id', '指定查询的柜次ID;整票订舱查询时为空'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query', 'container_scope_id', '柜次唯一作用域计算值;整票订舱查询固定为0'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query', 'created_at', '任务创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query', 'created_by', '创建操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query', 'created_by_name', '创建操作人姓名快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query', 'customer_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query', 'expires_at', '当前最近有效结果过期时间;到期后仍保留但标记过期'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query', 'id', '船司官网查询任务ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query', 'last_attempt_at', '最近一次查询尝试时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query', 'last_attempt_status', '最近查询尝试状态快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query', 'last_error_code', '最近查询失败错误码'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query', 'last_error_message', '最近查询失败错误说明'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query', 'last_success_at', '最近一次取得有效结果时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query', 'manual_reason', '需要人工查询或录入的原因'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query', 'next_allowed_at', '频控规则允许的下次查询时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query', 'ocean_booking_id', '关联海运订舱ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query', 'official_url', '船公司官网查询入口地址快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query', 'query_no', '船司官网查询任务编号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query', 'remark', '平台内部备注'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query', 'result_port_schedule_id', '最近有效结果对应的船期记录ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query', 'source_updated_at', '船公司官网展示的结果更新时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query', 'status', '任务状态:READY可查询、MANUAL_REQUIRED需人工处理、SUCCESS已取得有效结果'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query', 'tracking_no', '提交至船公司官网的跟踪号码'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query', 'tracking_type', '官网跟踪类型:BOOKING_NO订舱号、BILL_OF_LADING提单号、CONTAINER_NO箱号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query', 'updated_at', '任务最后更新时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query', 'updated_by', '最后修改操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query', 'updated_by_name', '最后修改操作人姓名快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query', 'window_started_at', '当前三天查询窗口起始时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query_attempt', 'access_mode', '本次尝试的官网访问方式快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query_attempt', 'attempt_status', '尝试结果:MANUAL_REQUIRED需人工处理、SUCCESS成功、FAILED失败'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query_attempt', 'carrier_code', '本次尝试的船公司编码快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query_attempt', 'coverage_gap', '是否存在自动化覆盖缺口:0否、1是'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query_attempt', 'coverage_gap_code', '自动化覆盖缺口编码'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query_attempt', 'created_at', '尝试记录创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query_attempt', 'error_code', '查询失败错误码'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query_attempt', 'error_message', '查询失败错误说明'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query_attempt', 'id', '船司官网查询尝试ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query_attempt', 'manual_reason', '转人工处理原因'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query_attempt', 'official_url', '本次尝试使用的官网地址快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query_attempt', 'operator_id', '执行本次尝试的操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query_attempt', 'operator_name', '执行操作人姓名快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query_attempt', 'query_id', '所属船司官网查询任务ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query_attempt', 'window_attempt_no', '当前三天窗口内的尝试序号;取值1至3'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query_history', 'action', '历史动作:CREATE创建、ATTEMPT执行查询、MANUAL_RESULT录入人工结果'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query_history', 'after_snapshot', '变更后任务数据快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query_history', 'before_snapshot', '变更前任务数据快照;创建时为空'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query_history', 'created_at', '历史记录创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query_history', 'from_version', '变更前任务版本;创建时为空'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query_history', 'id', '船司查询任务历史ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query_history', 'operator_id', '操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query_history', 'operator_name', '操作人姓名快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query_history', 'query_id', '所属船司官网查询任务ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query_history', 'reason', '操作原因或说明'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query_history', 'to_version', '变更后任务版本'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query_operation', 'completed_at', '写操作成功完成时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query_operation', 'created_at', '幂等记录创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query_operation', 'id', '船司查询写操作幂等记录ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query_operation', 'idempotency_key', '客户端写操作幂等键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query_operation', 'operation', '幂等写操作类型'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query_operation', 'operator_id', '发起写操作的平台操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query_operation', 'query_id', '关联查询任务ID;创建任务落库前允许为空'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query_operation', 'request_hash', '规范化请求内容SHA-256摘要;用于识别同键不同请求'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query_operation', 'result_json', '成功结果JSON快照;供幂等重放返回'); +CALL queue_column_comment('baiyue_firstleg', 'fl_carrier_schedule_query_operation', 'status', '处理状态:PROCESSING处理中、SUCCESS成功'); +CALL queue_column_comment('baiyue_firstleg', 'fl_consumed_event', 'consumed_at', '首次成功消费时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_consumed_event', 'event_id', '已消费领域事件ID,与生产端Outbox事件ID一致'); +CALL queue_column_comment('baiyue_firstleg', 'fl_consumed_event', 'event_type', '已消费事件类型'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'actual_port_arrival_at', '实际到港时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'actual_sailing_at', '实际开船时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'actual_warehouse_arrival_at', '实际到仓时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'bill_of_lading_no', '提单号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'booking_no', '平台唯一货柜预订单号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'cabinet_no', '平台唯一内部柜号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'capacity_cbm', '货柜容量m3'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'container_no', '集装箱号;确认前允许修订'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'container_type', '箱型,例如20GP、40GP、40HQ'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'created_by', '创建人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'created_by_name', '创建人名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'departure_port_code', '起运港编码快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'departure_port_name', '起运港名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'destination_port_code', '目的港编码快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'destination_port_name', '目的港名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'destination_warehouse_code', '目的仓编码快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'destination_warehouse_name', '目的仓名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'estimated_port_arrival_at', '预计到港时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'estimated_sailing_at', '预计开船时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'estimated_warehouse_arrival_at', '预计到仓时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'id', '货柜预订ID(雪花ID)'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'loading_at', '计划或实际装柜时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'max_weight_kg', '货柜最大承重kg'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'shipping_company_id', '船司ID快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'shipping_company_name', '船司名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'terminal_trucking_company', '码头拖车公司'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'updated_at', '货柜资料最后更新时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'updated_by', '最后修改人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'updated_by_name', '最后修改人名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'vessel_name', '船名快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'voyage_no', '航次;确认前允许修订'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'warehouse_id', '装柜仓库ID;平台公共资源'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_booking', 'warehouse_name', '装柜仓库名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_document', 'container_booking_id', '货柜预订ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_document', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_document', 'document_type', '文件类型:CUSTOMS_CLEARANCE清关文件、DELIVERY_ORDER提货单、PICKUP_ORDER取货单'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_document', 'file_id', 'File服务文件ID,不建立跨库外键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_document', 'file_name', '文件名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_document', 'file_url', '文件访问地址快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_document', 'id', '货柜文件关联ID(雪花ID)'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_document', 'updated_at', '货柜文件关联最后更新时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_document', 'uploaded_by', '上传操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_document', 'uploaded_by_name', '上传操作人名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_document', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_fee_detail', 'amount', 'CNY费用金额'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_fee_detail', 'business_date', '费用业务日期'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_fee_detail', 'container_booking_id', '订舱柜ID快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_fee_detail', 'container_no', '柜号快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_fee_detail', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_fee_detail', 'created_by', '创建人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_fee_detail', 'currency', '币种,首期固定CNY'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_fee_detail', 'direction', '费用方向:RECEIVABLE应收、PAYABLE应付'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_fee_detail', 'fee_item_code_snapshot', '费用项目稳定编码快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_fee_detail', 'fee_item_id', '平台公共费用项目ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_fee_detail', 'fee_item_name_snapshot', '费用项目名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_fee_detail', 'id', '柜次费用来源ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_fee_detail', 'loading_plan_id', '柜次ID,对应fl_loading_plan.id'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_fee_detail', 'remark', '人工备注'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_fee_detail', 'review_reason', '审核驳回原因'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_fee_detail', 'review_status', '审核状态:PENDING待审核、APPROVED通过、REJECTED驳回'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_fee_detail', 'reviewed_at', '审核时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_fee_detail', 'reviewer_id', '审核人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_fee_detail', 'reviewer_name', '审核人名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_fee_detail', 'settlement_included', '是否参与财务结算来源池'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_fee_detail', 'source_status', '来源状态:ACTIVE有效、VOIDED作废'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_fee_detail', 'source_version', '费用来源版本;调整递增,作废携带当前版本'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_fee_detail', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_fee_detail', 'updated_at', '最后更新时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_fee_detail', 'updated_by', '最后更新人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customer_charge_rule', 'business_type', '业务类型'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customer_charge_rule', 'charge_mode', '计费口径:KG或CBM'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customer_charge_rule', 'conversion_factor', '重量体积换算系数'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customer_charge_rule', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customer_charge_rule', 'created_by', '创建人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customer_charge_rule', 'currency', '默认币种'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customer_charge_rule', 'customer_id', '客户逻辑ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customer_charge_rule', 'customer_name', '客户名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customer_charge_rule', 'deleted', '逻辑删除标记'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customer_charge_rule', 'effective_from', '生效开始时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customer_charge_rule', 'effective_to', '生效结束时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customer_charge_rule', 'id', '客户计费规则ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customer_charge_rule', 'remark', '规则备注'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customer_charge_rule', 'rounding_mode', '舍入方式'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customer_charge_rule', 'rounding_scale', '结果保留小数位数'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customer_charge_rule', 'rule_code', '客户内唯一计费规则编码'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customer_charge_rule', 'rule_version', '规则版本号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customer_charge_rule', 'status', '规则状态:ACTIVE启用、DISABLED停用'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customer_charge_rule', 'tax_mode', '包税或不包税类型'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customer_charge_rule', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customer_charge_rule', 'updated_at', '更新时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customer_charge_rule', 'updated_by', '最后修改人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customer_charge_rule', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customer_dimension_config', 'box_type', '固定箱规类型:SMALL/MEDIUM/LARGE/EXTRA_LARGE'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customer_dimension_config', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customer_dimension_config', 'created_by', '创建人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customer_dimension_config', 'deleted', '逻辑删除'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customer_dimension_config', 'edge_addition_cm', '每个尺寸方向的单侧加宽厘米数'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customer_dimension_config', 'id', '配置ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customer_dimension_config', 'initialization_key', '创建流程幂等键及补偿归属'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customer_dimension_config', 'inner_height_cm', '箱规内高(厘米)'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customer_dimension_config', 'inner_length_cm', '箱规内长(厘米)'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customer_dimension_config', 'inner_width_cm', '箱规内宽(厘米)'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customer_dimension_config', 'remark', '备注'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customer_dimension_config', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customer_dimension_config', 'updated_at', '更新时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customer_dimension_config', 'updated_by', '最后修改人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customer_dimension_config', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_case', 'checklist_code', '创建协同单时的清单编码快照;源清单变化不回写'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_case', 'checklist_version_id', '本单冻结使用的报关资料清单版本ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_case', 'checklist_version_no', '创建协同单时的清单版本号快照;源清单变化不回写'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_case', 'container_booking_id', '关联柜次逻辑ID;0表示创建时尚未关联柜次'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_case', 'created_at', '协同单创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_case', 'created_by', '创建协同单的平台操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_case', 'created_by_name', '创建协同单时的平台操作人名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_case', 'customs_no', '全局唯一报关协同单号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_case', 'id', '报关协同单逻辑ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_case', 'inbound_id', 'Firstleg入库单逻辑ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_case', 'order_id', 'Order服务订单逻辑ID;跨库不建立外键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_case', 'remark', '报关协同单内部业务备注'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_case', 'status', '协同状态:DOCUMENTS_PENDING待补资料、REVIEWING审核中、SUPPLEMENT_REQUIRED需补件、READY_TO_SUBMIT待提交代理、RECEIPT_PENDING待回执、RECEIPT_AVAILABLE已有回执、CLEARANCE_COMPLETED已放行、CANCELLED已取消'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_case', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_case', 'updated_at', '协同单最后修改时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_case', 'updated_by', '最后修改协同单的平台操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_case', 'updated_by_name', '最后修改时的平台操作人名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_case', 'version', '乐观锁版本号;每次状态变更递增'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_checklist_version', 'cargo_type', '清单适用的货物类型编码'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_checklist_version', 'channel_code', '清单适用的运输渠道编码'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_checklist_version', 'checklist_code', '报关资料清单编码;同一清单的多个版本共用'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_checklist_version', 'country_code', '清单适用的目的国家或地区编码'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_checklist_version', 'created_at', '清单版本创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_checklist_version', 'created_by', '创建清单版本的平台操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_checklist_version', 'created_by_name', '创建清单版本时的平台操作人名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_checklist_version', 'id', '报关资料清单版本逻辑ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_checklist_version', 'items_json', '不可变清单项目数组;元素包含code项目编码、name项目名称和required是否必填'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_checklist_version', 'status', '清单版本状态:ACTIVE启用、RETIRED停用'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_checklist_version', 'version_no', '清单业务版本号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_document', 'checklist_item_code', '对应冻结清单中的资料项目编码'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_document', 'customs_case_id', '所属报关协同单ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_document', 'document_version', '同一协同单和清单项目下的文件版本序号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_document', 'file_hash', '文件内容SHA-256摘要;用于版本审计和幂等校验'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_document', 'file_id', 'File服务文件逻辑ID;跨库不建立外键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_document', 'file_name', '上传或引用时的文件名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_document', 'id', '报关资料文件版本逻辑ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_document', 'replaces_document_id', '被本版本替换的上一资料版本ID;首个版本为空'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_document', 'review_reason', '审核退回原因或审核备注'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_document', 'reviewed_at', '资料审核时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_document', 'reviewed_by', '审核资料的平台操作人ID;未审核时为空'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_document', 'reviewed_by_name', '审核时的平台操作人名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_document', 'status', '资料状态:UPLOADED待审核、APPROVED审核通过、RETURNED已退回、SUPERSEDED已被替换、FROZEN已随代理提交批次冻结'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_document', 'uploaded_at', '资料上传或引用时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_document', 'uploaded_by', '上传或引用资料的操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_document', 'uploaded_by_type', '上传主体类型:PLATFORM平台操作人、MERCHANT客户操作人'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_document', 'visibility', '资料可见范围:PLATFORM仅平台可见、CUSTOMER客户可见'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_history', 'action', '业务动作编码,如CREATE、UPLOAD_DOCUMENT、REPLACE_DOCUMENT、REVIEW_DOCUMENT_APPROVE、REVIEW_DOCUMENT_RETURN、SUBMIT_AGENT_BATCH或UPLOAD_RECEIPT'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_history', 'created_at', '历史记录创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_history', 'customs_case_id', '所属报关协同单ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_history', 'from_status', '动作发生前的协同状态;创建动作时为空'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_history', 'id', '报关协同历史逻辑ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_history', 'note', '动作原因、资料项目编码或业务备注快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_history', 'operator_id', '执行业务动作的操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_history', 'operator_name', '执行动作时的操作人名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_history', 'operator_type', '操作主体类型:PLATFORM平台操作人、MERCHANT客户操作人'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_history', 'resource_id', '动作关联资源逻辑ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_history', 'resource_type', '动作关联资源类型:CASE协同单、DOCUMENT资料、SUBMISSION_BATCH提交批次或RECEIPT回执'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_history', 'to_status', '动作完成后的协同状态'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_operation', 'action', '幂等业务动作编码,包含平台或客户动作及资料、审核、提交、回执类型'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_operation', 'completed_at', '写命令成功完成时间;处理中为空'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_operation', 'created_at', '幂等记录创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_operation', 'id', '报关写命令幂等记录逻辑ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_operation', 'idempotency_key', '客户端提供的幂等键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_operation', 'operator_id', '发起写命令的操作人ID;与动作和幂等键共同限定重放范围'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_operation', 'request_hash', '规范化完整请求内容的SHA-256摘要;同键不同请求时拒绝'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_operation', 'result_id', '成功结果资源逻辑ID;处理中为空'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_operation', 'result_type', '成功结果资源类型:CHECKLIST、CASE、DOCUMENT、BATCH或RECEIPT'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_operation', 'status', '幂等处理状态:PROCESSING处理中、SUCCESS处理成功'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_receipt', 'customs_case_id', '所属报关协同单ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_receipt', 'file_hash', '回执文件内容SHA-256摘要;用于版本审计和幂等校验'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_receipt', 'file_id', 'File服务回执文件逻辑ID;跨库不建立外键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_receipt', 'file_name', '上传或引用时的回执文件名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_receipt', 'id', '报关回执文件版本逻辑ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_receipt', 'note', '回执业务备注'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_receipt', 'receipt_type', '回执类型:RECEIVED已收件、DECLARED已申报、REJECTED已拒绝、INSPECTION查验通知、RELEASED放行回执、OTHER其他回执'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_receipt', 'receipt_version', '同一提交批次和回执类型下的版本序号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_receipt', 'replaces_receipt_id', '被本版本替换的上一回执版本ID;首个版本为空'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_receipt', 'submission_batch_id', '产生本回执的报关代理提交批次ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_receipt', 'uploaded_at', '回执上传或引用时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_receipt', 'uploaded_by', '上传或引用回执的平台操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_receipt', 'uploaded_by_name', '上传回执时的平台操作人名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_receipt', 'visibility', '回执可见范围:PLATFORM仅平台可见、CUSTOMER客户可查询下载'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_submission_batch', 'batch_no', '全局唯一代理提交批次号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_submission_batch', 'batch_version', '同一报关协同单内的提交批次版本序号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_submission_batch', 'customs_agent_name', '本批次接收报关资料的报关代理名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_submission_batch', 'customs_case_id', '所属报关协同单ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_submission_batch', 'external_reference', '报关代理或外部系统返回的业务参考号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_submission_batch', 'file_snapshot_json', '提交时冻结的已审核资料数组;元素包含documentId、fileId、itemCode、documentVersion和fileHash'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_submission_batch', 'id', '报关代理提交批次逻辑ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_submission_batch', 'note', '提交报关代理时的平台备注'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_submission_batch', 'status', '代理提交状态:SUBMITTED已提交、ACKNOWLEDGED代理已确认、REJECTED代理已拒绝、COMPLETED代理处理完成'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_submission_batch', 'submitted_at', '提交报关代理时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_submission_batch', 'submitted_by', '提交报关代理的平台操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_submission_batch', 'submitted_by_name', '提交时的平台操作人名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_supplement_request', 'checklist_item_code', '需要补充的报关资料项目编码'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_supplement_request', 'customs_case_id', '所属报关协同单ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_supplement_request', 'due_at', '期望完成补件时间;未设置时为空'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_supplement_request', 'id', '报关补件请求逻辑ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_supplement_request', 'reason', '平台审核人员填写的补件原因'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_supplement_request', 'requested_at', '补件请求发起时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_supplement_request', 'requested_by', '发起补件的平台操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_supplement_request', 'requested_by_name', '发起补件时的平台操作人名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_supplement_request', 'resolved_at', '补件请求解决时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_supplement_request', 'resolved_by', '提交解决资料的操作人ID;未解决时为空'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_supplement_request', 'resolved_document_id', '解决本次补件的新资料版本ID;未解决时为空'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_supplement_request', 'source_document_id', '触发本次补件的被退回资料版本ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_customs_supplement_request', 'status', '补件状态:OPEN待补件、RESOLVED已由新资料解决、CANCELLED已取消'); +CALL queue_column_comment('baiyue_firstleg', 'fl_fee_item', 'allocation_fee', '平摊费用是否参与'); +CALL queue_column_comment('baiyue_firstleg', 'fl_fee_item', 'category', '费用分类'); +CALL queue_column_comment('baiyue_firstleg', 'fl_fee_item', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_fee_item', 'created_by', '创建人ID;系统初始化时可为空'); +CALL queue_column_comment('baiyue_firstleg', 'fl_fee_item', 'created_by_name', '创建人名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_fee_item', 'deleted', '逻辑删除标记:0未删除、1已删除'); +CALL queue_column_comment('baiyue_firstleg', 'fl_fee_item', 'description', '说明'); +CALL queue_column_comment('baiyue_firstleg', 'fl_fee_item', 'fee_code', '稳定费用编码;系统费用使用固定编码'); +CALL queue_column_comment('baiyue_firstleg', 'fl_fee_item', 'fee_type_name', '费用类型名称'); +CALL queue_column_comment('baiyue_firstleg', 'fl_fee_item', 'id', '费用项目ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_fee_item', 'pickup_payable', '提柜应付是否参与'); +CALL queue_column_comment('baiyue_firstleg', 'fl_fee_item', 'pickup_receivable', '提柜应收是否参与'); +CALL queue_column_comment('baiyue_firstleg', 'fl_fee_item', 'status', '状态:ACTIVE启用、DISABLED停用'); +CALL queue_column_comment('baiyue_firstleg', 'fl_fee_item', 'tenant_id', '平台公共费用项目作用域,固定为0;物理字段名沿用 tenant_id'); +CALL queue_column_comment('baiyue_firstleg', 'fl_fee_item', 'trade_scope', 'DOMESTIC国内、INTERNATIONAL国际'); +CALL queue_column_comment('baiyue_firstleg', 'fl_fee_item', 'updated_at', '最后更新时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_fee_item', 'updated_by', '最后修改人ID;系统初始化时可为空'); +CALL queue_column_comment('baiyue_firstleg', 'fl_fee_item', 'updated_by_name', '最后修改人名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_fee_item', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_fee_item', 'warehouse_payable', '仓储应付是否参与'); +CALL queue_column_comment('baiyue_firstleg', 'fl_fee_item', 'warehouse_receivable', '仓储应收是否参与'); +CALL queue_column_comment('baiyue_firstleg', 'fl_fulfillment_task', 'assigned_at', '最近指派时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_fulfillment_task', 'assignee_id', '当前负责人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_fulfillment_task', 'assignee_name', '当前负责人名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_fulfillment_task', 'completed_at', '履约完成时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_fulfillment_task', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_fulfillment_task', 'created_by', '创建人或系统主体ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_fulfillment_task', 'deleted', '逻辑删除标记'); +CALL queue_column_comment('baiyue_firstleg', 'fl_fulfillment_task', 'id', '履约任务ID(雪花ID)'); +CALL queue_column_comment('baiyue_firstleg', 'fl_fulfillment_task', 'order_id', '订单服务中的订单逻辑ID,不建立跨库外键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_fulfillment_task', 'order_no', '订单号快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_fulfillment_task', 'order_snapshot_json', '创建履约任务时所需的订单业务快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_fulfillment_task', 'started_at', '开始履约时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_fulfillment_task', 'status', '履约状态:PENDING_ASSIGNMENT待指派、ASSIGNED已指派、PROCESSING处理中、COMPLETED已完成、CANCELLED已取消'); +CALL queue_column_comment('baiyue_firstleg', 'fl_fulfillment_task', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_fulfillment_task', 'updated_at', '更新时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_fulfillment_task', 'updated_by', '最后修改人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_fulfillment_task', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_carton_label', 'box_code', '固定二维码内容:入仓码#总箱数-箱下标'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_carton_label', 'box_specification', '订单箱规或货物明细描述快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_carton_label', 'cargo_item_no', '订单货物明细序号快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_carton_label', 'cargo_name', '货物名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_carton_label', 'carton_index', '整单从1开始的稳定箱下标'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_carton_label', 'created_at', '首次分配时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_carton_label', 'detail_package_count', '该订单货物明细箱数快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_carton_label', 'height_cm', '下单箱规高度cm快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_carton_label', 'id', '实际箱唛头映射ID(雪花ID)'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_carton_label', 'inbound_code', '入仓码快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_carton_label', 'inbound_detail_id', '订单货物明细ID,不建立跨库外键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_carton_label', 'inbound_id', '入库单ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_carton_label', 'length_cm', '下单箱规长度cm快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_carton_label', 'order_id', '订单服务订单ID快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_carton_label', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_carton_label', 'total_carton_count', '首次分配时整单总箱数'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_carton_label', 'width_cm', '下单箱规宽度cm快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_destination_snapshot', 'address_line', '详细地址'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_destination_snapshot', 'city', '城市'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_destination_snapshot', 'contact_name', '联系人'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_destination_snapshot', 'contact_phone', '联系人电话'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_destination_snapshot', 'country_code', '国家二字码'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_destination_snapshot', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_destination_snapshot', 'destination_code', '目的仓编码'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_destination_snapshot', 'destination_name', '目的仓名称'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_destination_snapshot', 'destination_type', '目的仓类型'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_destination_snapshot', 'id', '目的仓地址快照ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_destination_snapshot', 'inbound_id', '入库单ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_destination_snapshot', 'postal_code', '邮编'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_destination_snapshot', 'state_province', '省或州'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_destination_snapshot', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'actual_volume_cbm', '试算时实际体积m3'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'actual_weight_kg', '试算时实际重量kg'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'additional_amount', '附加费金额'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'business_type', '业务类型快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'calculated_at', '试算时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'calculated_by', '试算人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'carrier_id', '历史承运商ID;渠道价格可为空'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'carrier_name', '历史承运商名称;渠道价格可为空'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'carrier_rate_id', '船司FBA价格ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'carrier_rate_version', '船司价格版本快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'channel_code', '渠道编码快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'charge_mode', '计费口径:KG或CBM'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'charge_rule_id', '历史客户计费规则ID;渠道价格可为空'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'charge_rule_version', '历史客户规则版本快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'charge_unit', '计费单位'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'chargeable_quantity', '最终计费量'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'confirmed_at', '确认时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'confirmed_by', '确认人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'conversion_factor', '历史客户规则使用的换算系数快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'converted_quantity', '换算后的重量或体积'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'currency', '币种'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'customer_id', '客户ID快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'customer_name', '客户名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'destination_code', 'FBA仓编码快照;海外仓可为空'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'destination_name', '目的仓名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'destination_type', '目的地类型快照:FBA或OVERSEAS'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'destination_warehouse_id', '海外仓或FBA目的仓ID快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'exception_release', '是否基于测量异常放行生成'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'fee_no', '客户内唯一费用单号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'fee_status', '费用状态:CALCULATED已计算、SUPERSEDED已被新版本替代'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'fee_version', '费用快照版本'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'id', '入库费用ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'inbound_id', '入库单ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'origin_warehouse_id', '起运仓ID快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'origin_warehouse_name', '起运仓名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'price_book_version_id', '整表价格版本ID快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'pricing_tax_mode', '报价包税维度快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'rate_code', '船司价格编码快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'rate_effective_from', '价格生效开始快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'rate_effective_to', '价格生效结束快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'review_status', '费用审核状态:NOT_SUBMITTED未提交、PENDING待审核、APPROVED已通过、REJECTED已驳回、CONFIRMED已确认'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'rounding_mode', '历史客户规则舍入方式快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'rounding_scale', '历史客户规则计费量保留位数快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'rule_code', '历史客户规则编码快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'rule_effective_from', '历史客户规则生效开始快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'rule_effective_to', '客户规则生效结束快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'sent_at', '发送客户时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'sent_by', '发送人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'subtotal_amount', '基础费用金额'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'tax_mode', '订单买单或退税模式快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'total_amount', '总金额'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee', 'updated_at', '更新时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'actual_gross_weight_kg', '仓库实测毛重快照,单位千克'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'actual_volume_cbm', '实际体积快照,单位立方米'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'amount', '该费用明细金额,币种由currency字段指定'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'business_date', '业务发生日'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'channel_code', '渠道编码快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'channel_name', '渠道名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'charge_unit', 'KG或CBM'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'chargeable_quantity', '本费用最终计费量'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'chargeable_volume_cbm', '费用计算采用的计费体积,单位立方米'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'chargeable_weight_kg', '费用计算采用的计费重量,单位千克'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'created_by', '创建人或系统主体ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'currency', '金额币种三字码,如CNY'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'customer_id', '客户ID快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'customer_name', '客户公司名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'deleted', '逻辑删除标记:0未删除、1已删除'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'destination_code', '目的仓编码快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'destination_name', '目的仓名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'direction', 'RECEIVABLE应收、PAYABLE应付'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'fee_category_snapshot', '费用分类快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'fee_item_id', '绑定费用项目ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'fee_type_name_snapshot', '费用类型名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'id', '入库费用明细ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'inbound_code', '入仓码快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'inbound_id', '入库单ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'inbounded_at', '入库完成时间快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'invoice_title', '发票抬头快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'package_count', '件数快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'publication_status', '费用发布状态:DRAFT草稿、PUBLISHED已发布、SUPERSEDED已替代'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'published_at', '发布时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'published_by', '发布人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'published_by_name', '发布人名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'remark', '备注'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'review_reason', '审核驳回原因'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'review_status', '审核状态:PENDING待审核、APPROVED已通过、REJECTED已驳回'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'reviewed_at', '审核时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'reviewer_id', '审核人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'reviewer_name', '审核人名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'settlement_included', '是否参与结算'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'source_fee_id', '来源计费快照ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'source_fee_no', '来源费用单号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'source_fee_version', '来源计费快照版本'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'source_type', 'MANUAL或FBA_PRICING'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'tenant_id', '客户ID(客户隔离键);费用明细引用平台公共费用项目'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'trade_scope_snapshot', '国内国际快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'unit_price', '费用单价;计价单位由对应费用项目规则确定'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'updated_at', '最后更新时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'updated_by', '最后修改人或系统主体ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'volume_weight_kg', '材积重快照,单位千克'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_item', 'amount', '金额'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_item', 'automatic_flag', '是否自动计算'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_item', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_item', 'currency', '币种'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_item', 'fee_code', '费用项目编码'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_item', 'fee_id', '费用头ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_item', 'fee_name', '费用项目名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_item', 'id', '费用明细ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_item', 'item_no', '费用单内序号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_item', 'quantity', '计费数量'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_item', 'remark', '费用备注'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_item', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_item', 'unit', '计费单位'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_item', 'unit_price', '单价'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_file', 'deleted', '逻辑删除标记'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_file', 'file_id', '文件服务文件ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_file', 'file_name_snapshot', '文件名快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_file', 'file_type', '文件类型:RELATED、MARK、PC_ORDER、MEASURE或WEIGHT'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_file', 'file_version', '业务文件版本'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_file', 'id', '入库单文件关系ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_file', 'inbound_id', '入库单ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_file', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_file', 'uploaded_at', '上传时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_file', 'uploaded_by', '上传人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_file', 'visibility', '可见范围'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_hold_record', 'evidence_file_id', '证据文件ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_hold_record', 'hold_action', '动作:HOLD或RELEASE'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_hold_record', 'id', '扣货记录ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_hold_record', 'inbound_id', '入库单ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_hold_record', 'operated_at', '操作时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_hold_record', 'operator_id', '操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_hold_record', 'operator_name', '操作人名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_hold_record', 'reason', '扣货或解除原因'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_hold_record', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_idempotency', 'completed_at', '完成时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_idempotency', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_idempotency', 'id', '入库写操作幂等记录ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_idempotency', 'idempotency_key', '幂等键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_idempotency', 'inbound_id', '入库单ID,创建前可为0'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_idempotency', 'operation', '操作类型'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_idempotency', 'operator_id', '操作者或外部应用ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_idempotency', 'request_hash', '请求内容摘要'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_idempotency', 'result_json', '成功结果摘要'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_idempotency', 'result_resource_id', '成功结果资源ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_idempotency', 'status', '幂等处理状态:PROCESSING处理中、SUCCEEDED已成功、FAILED已失败'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_idempotency', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_idempotency', 'updated_at', '更新时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_internal_note', 'content', '仅内部可见备注'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_internal_note', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_internal_note', 'created_by', '创建人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_internal_note', 'created_by_name', '创建人名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_internal_note', 'id', '客服内部备注ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_internal_note', 'inbound_id', '入库单ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_internal_note', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_loading_pick_task', 'created_at', 'PDA拣货任务创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_loading_pick_task', 'expected_count', '计划应拣箱数'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_loading_pick_task', 'id', '入库单级PDA拣货任务ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_loading_pick_task', 'inbound_id', '关联入库单逻辑ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_loading_pick_task', 'picked_count', '实际已拣箱数'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_loading_pick_task', 'remark', '拣货结果或异常补充说明'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_loading_pick_task', 'status', '任务状态:PENDING待拣货、IN_PROGRESS拣货中、COMPLETED已完成'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_loading_pick_task', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_loading_pick_task', 'updated_at', 'PDA拣货任务最后更新时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_loading_pick_task', 'updated_by', '最后操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_loading_pick_task', 'updated_by_name', '最后操作人姓名快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_loading_pick_task', 'version', '乐观锁版本号,每次更新递增'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement', 'actual_goods_name', '实际品名'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement', 'created_by', '创建人或系统主体ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement', 'device_code', '测量设备编码'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement', 'external_event_no', '第三方事件号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement', 'external_request_hash', '第三方data原文和图片内容联合请求哈希'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement', 'id', '测量批次ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement', 'inbound_id', '入库单ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement', 'measured_at', '设备测量时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement', 'original_document_no', '原单据号码快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement', 'original_gross_weight_kg', '原单据毛重kg'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement', 'original_volume_cbm', '原单据体积或材积m3'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement', 'raw_payload_json', '第三方原始报文'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement', 'source_code', '第三方应用或来源编码'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement', 'source_measurement_id', '子单继承或复核时的来源测量批次ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement', 'source_type', '数据来源:THIRD_PARTY或MANUAL'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement', 'split_batch_id', '产生该测量批次的入仓拆分批次ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement', 'status', '测量批次状态:DRAFT草稿、RECEIVED已接收、SUBMITTED已提交、APPROVED已通过、REJECTED已驳回'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement', 'total_package_count', '本批次总箱数'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement', 'total_volume_cbm', '本批次业务总体积m3'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement', 'total_volume_raw_cbm', '本批次原始总体积m3'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement', 'updated_at', '更新时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement', 'volume_weight_kg', '本批次总材积重kg'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement', 'warehouse_code', '第三方回传仓库编码'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'box_code', '柏悦唛头箱号;设备单箱测量时同一客户内唯一'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'box_specification', '箱规标识'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'calculation_rule_version', '体积计算和舍入规则版本'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'device_measured_at', '本次设备测量时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'external_detail_no', '第三方测量明细唯一号,平台手工明细可为空'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'girth_cm', '周长cm,规则为长+2*(宽+高)'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'height_cm', '原始高度cm'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'id', '测量明细ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'inbound_detail_id', '绑定的订单货物明细ID,平台手工明细可为空'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'item_no', '批次内明细号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'length_cm', '原始长度cm'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'machine_no', '本次测量设备编号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'measurement_id', '测量批次ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'outer_height_cm', '外箱规高度cm'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'outer_length_cm', '外箱规长度cm'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'outer_width_cm', '外箱规宽度cm'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'over_limit_warning', '超线提醒快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'package_count', '相同箱规箱数'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'remark', '测量明细备注'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'scanned_shipment_id', '设备从Amazon FBA箱标实扫的Shipment ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'source_image_file_id', '测量设备图片文件ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'source_measurement_item_id', '被拆分继承的来源测量箱规明细ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'split_item_id', '产生该子单测量明细的拆分台账ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'total_volume_cbm', '本箱规总体积m3'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'total_weight_kg', '本箱规总重量kg'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'unit_volume_cbm', '单箱业务体积m3'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'unit_weight_kg', '单箱重量kg,第三方仅回传尺寸时可为空'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'updated_at', '最后更新时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'volume_cbm', '业务体积m3'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'volume_raw_cbm', '原始体积m3'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'volume_weight_kg', '本箱规材积重kg'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_measurement_item', 'width_cm', '原始宽度cm'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'active_remainder_root_id', '有效余量唯一约束键;历史已完成分片为空'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'actual_gross_weight_kg', '确认后的实际总毛重kg'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'actual_net_weight_kg', '总净重kg'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'actual_volume_cbm', '确认后的实际总体积m3'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'business_type', '业务类型'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'carrier_id', '计费船司逻辑ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'carrier_name', '计费船司名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'channel_code', '渠道编码'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'channel_name', '渠道名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'chargeable_volume_cbm', '当前费用快照的计费体积m3'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'chargeable_weight_kg', '当前费用快照的计费重量kg'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'company_id', '运营公司逻辑ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'company_name', '运营公司名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'container_mode', '整柜或散货模式'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'created_by', '创建人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'created_by_name', '创建人名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'current_hold_reason', '当前扣货理由快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'customer_id', '客户逻辑ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'customer_name', '客户名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'customs_inspection', '是否商检'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'deleted', '逻辑删除标记'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'delivery_warehouse_id', '交货仓逻辑ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'delivery_warehouse_name', '交货仓名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'destination_code', '目的仓编码,例如FBA仓编码'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'destination_name', '目的仓名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'destination_type', '目的仓类型'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'destination_warehouse_id', '目的仓逻辑ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'expected_arrival_at', '预计到达时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'express_company_code', '快递公司编码快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'express_company_name', '快递公司名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'express_tracking_no', '快递单号快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'fee_review_status', '费用审核状态:NOT_SUBMITTED未提交、PENDING待审核、APPROVED已通过、REJECTED已驳回、CONFIRMED已确认'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'fee_status', '费用状态:UNPRICED未计费、CALCULATED已计算、CONFIRMED已确认、SENT已发送、VOIDED已作废'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'hold_flag', '当前是否扣货'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'id', '入库单ID(雪花ID)'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'inbound_code', '客户内唯一入仓码'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'inbound_role', '入仓单层级:MASTER主单、CHILD子单'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'inbounded_at', '入库完成时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'master_order_id', '仅用于约束有效主单订单唯一性'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'measure_completed_at', '测量完成时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'measure_review_status', '测量审核状态:NOT_SUBMITTED未提交、PENDING待审核、APPROVED已通过、REJECTED已驳回'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'notification_status', '客户通知状态:NOT_CREATED未创建、PENDING待通知'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'order_id', '订单服务订单ID,不建立跨库外键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'order_no', '订单号快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'order_status_snapshot', '订单状态快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'package_count', '入仓单箱数;三期唯一数量口径'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'pallet_count', '已分配总托盘数快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'parent_inbound_id', '直属主入仓单ID;主单为空'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'pricing_tax_mode', '报价包税维度:TAX_INCLUDED/TAX_EXCLUDED'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'process_status', '入库流程状态:WAIT_MEASURE待测量、MEASURING测量中、MEASURE_PENDING待审核、MEASURE_REJECTED测量驳回、WAIT_PRICING待计费、FEE_CONFIRMING费用确认中、INBOUNDING入库收口中、INBOUNDED已入库、WAIT_SPLIT历史待分柜'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'public_remark', '客户可见备注'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'root_inbound_id', '根主入仓单ID;存量主单可为空,按自身ID解释'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'split_purpose', '履约分片用途:MASTER、LOADED、REMAINDER'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'split_sequence', '同一主单下的子单顺序号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'split_status', '拆分状态:NOT_SPLIT、PARTIALLY_SPLIT、FULLY_SPLIT'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'task_id', '头程履约任务ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'tax_mode', '买单或退税模式'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'total_package_count', '确认后的总箱数'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'updated_at', '更新时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'updated_by', '最后修改人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'volume_weight_kg', '总材积重kg'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'warehouse_id', '实际作业仓库ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'warehouse_name', '实际作业仓库名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_outbox', 'aggregate_id', '入库单ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_outbox', 'aggregate_sequence', '同一费用来源的单调版本序号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_outbox', 'aggregate_type', '聚合类型'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_outbox', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_outbox', 'event_key', '费用来源事件幂等键;历史事件允许为空'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_outbox', 'event_type', '事件类型'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_outbox', 'event_version', '事件信封版本'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_outbox', 'id', '领域事件全局ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_outbox', 'last_error', '最近一次投递失败摘要,不记录令牌'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_outbox', 'locked_at', 'Relay抢占租约时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_outbox', 'locked_by', 'Relay抢占实例ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_outbox', 'next_retry_at', '下次重试时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_outbox', 'payload_hash', '短payload规范化JSON的SHA-256'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_outbox', 'payload_json', '事件负载快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_outbox', 'publish_status', '事件发布状态:PENDING待发布、PUBLISHED已发布、FAILED发布失败'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_outbox', 'published_at', '发布时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_outbox', 'retry_count', '重试次数'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_outbox', 'tenant_id', '事件所属客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_pallet_allocation', 'allocated_package_count', '本托盘分配箱数'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_pallet_allocation', 'allocated_volume_cbm', '本托盘分配体积'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_pallet_allocation', 'allocation_no', '入库单内分配序号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_pallet_allocation', 'allocation_status', '托盘库位分配状态:RESERVED已预占、CONFIRMED已确认、TRANSFERRED已转移、RELEASED已释放'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_pallet_allocation', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_pallet_allocation', 'id', '头程托盘分配引用ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_pallet_allocation', 'inbound_id', '入库单ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_pallet_allocation', 'location_code', '库位编码快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_pallet_allocation', 'location_id', '仓库服务库位ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_pallet_allocation', 'pallet_capacity_cbm', '托盘可用容积快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_pallet_allocation', 'pallet_code', '托盘编码快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_pallet_allocation', 'pallet_id', '仓库服务托盘ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_pallet_allocation', 'picked_package_count', '已拣货箱数'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_pallet_allocation', 'reservation_id', '仓库预占资源唯一ID,A0共享场景用于区分托盘'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_pallet_allocation', 'reservation_no', '仓库库位预占单号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_pallet_allocation', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_pallet_allocation', 'updated_at', '更新时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_pallet_allocation', 'warehouse_id', '仓库服务仓库ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_pallet_allocation', 'warehouse_resource_tenant_id', '平台公共仓储资源固定使用 0;tenant_id 仍是客户隔离键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_pickup_snapshot', 'address_line', '提货详细地址'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_pickup_snapshot', 'city', '城市'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_pickup_snapshot', 'contact_email', '提货联系人邮箱'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_pickup_snapshot', 'contact_name', '提货联系人'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_pickup_snapshot', 'contact_phone', '提货联系人电话'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_pickup_snapshot', 'country_code', '国家二字码'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_pickup_snapshot', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_pickup_snapshot', 'district', '区县'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_pickup_snapshot', 'id', '提货信息快照ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_pickup_snapshot', 'inbound_id', '入库单ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_pickup_snapshot', 'pickup_at', '提货时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_pickup_snapshot', 'pickup_method', '提货方式'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_pickup_snapshot', 'postal_code', '提货邮编'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_pickup_snapshot', 'province', '省或州'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_pickup_snapshot', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_batch', 'applied_at', '批次生效时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_batch', 'container_booking_id', '关联柜次标识(兼容查询)'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_batch', 'created_at', '拆分批次创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_batch', 'created_by', '创建人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_batch', 'created_by_name', '创建人名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_batch', 'expected_parent_version', '操作时主单乐观锁版本'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_batch', 'id', '拆分批次ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_batch', 'idempotency_key', '请求幂等键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_batch', 'loaded_child_inbound_id', '本批次实装子单ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_batch', 'loaded_package_count', '本批次实装箱数'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_batch', 'loading_plan_id', '关联装柜计划/柜次'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_batch', 'operation_id', '业务操作幂等号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_batch', 'package_count', '本批次拆分箱数'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_batch', 'parent_inbound_id', '主入仓单ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_batch', 'remainder_child_inbound_id', '本批次余量子单ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_batch', 'remaining_package_count', '本批次未装箱数'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_batch', 'remark', '拆分原因或备注'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_batch', 'source_inbound_id', '本批次实际消耗的MASTER或REMAINDER分片ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_batch', 'status', '拆分批次状态:PREPARED已准备、APPLIED已生效、CANCELLED已取消'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_batch', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_batch', 'version', '乐观锁版本号,每次更新递增'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_detail', 'child_inbound_id', '完成拆单后生成的二级子入库单ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_detail', 'completed_at', '部分拆分完成时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_detail', 'completed_by', '完成拆分的操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_detail', 'completed_by_name', '完成操作人姓名快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_detail', 'created_at', '部分锁定明细创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_detail', 'created_by', '创建部分锁定明细的操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_detail', 'created_by_name', '创建操作人姓名快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_detail', 'id', '部分锁定明细ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_detail', 'inbound_detail_id', '原订单货物明细ID,不建立跨库外键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_detail', 'locked_package_count', '本次装柜锁定箱数'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_detail', 'operation_id', '部分锁定操作幂等号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_detail', 'parent_inbound_id', '主入库单ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_detail', 'remaining_package_count', '锁定后剩余箱数,由来源箱数减锁定箱数生成'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_detail', 'source_package_count', '拆分前该明细总箱数快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_detail', 'status', '分柜明细状态:LOCKED已锁定、COMPLETED已完成拆分'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_detail', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_detail', 'updated_at', '部分锁定明细最后更新时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_detail', 'version', '乐观锁版本号,每次更新递增'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_history', 'action_type', '拆分动作:CREATE创建批次、APPLY生效、CANCEL取消'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_history', 'after_snapshot', '操作后快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_history', 'before_snapshot', '操作前快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_history', 'created_at', '不可变拆分历史创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_history', 'id', '拆分历史ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_history', 'operation_id', '历史操作幂等号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_history', 'operation_reason', '操作原因'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_history', 'operator_id', '操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_history', 'operator_name', '操作人名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_history', 'split_batch_id', '拆分批次ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_history', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_item', 'child_inbound_id', '子入仓单ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_item', 'created_at', '拆分数量台账创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_item', 'id', '拆分明细ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_item', 'line_no', '同一拆分批次内的行号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_item', 'package_count', '分配给子单的箱数'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_item', 'parent_inbound_id', '主入仓单ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_item', 'source_inbound_detail_id', '订单货物明细逻辑ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_item', 'source_measurement_id', '来源测量批次ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_item', 'source_measurement_item_id', '来源测量箱规明细ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_item', 'split_batch_id', '拆分批次ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_split_item', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_status_history', 'action', '触发动作'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_status_history', 'from_status', '变更前状态'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_status_history', 'id', '入库状态历史ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_status_history', 'inbound_id', '入库单ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_status_history', 'occurred_at', '发生时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_status_history', 'operator_id', '操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_status_history', 'operator_name', '操作人名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_status_history', 'reason', '变更原因'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_status_history', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_status_history', 'to_status', '变更后状态'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_transport_snapshot', 'cabinet_no', '柜号或内部柜单号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_transport_snapshot', 'container_no', '集装箱号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_transport_snapshot', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_transport_snapshot', 'id', '运输信息快照ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_transport_snapshot', 'inbound_id', '入库单ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_transport_snapshot', 'loading_status', '配载状态快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_transport_snapshot', 'port_arrival_at', '到港时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_transport_snapshot', 'sailing_at', '开船时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_transport_snapshot', 'source_updated_at', '上游数据更新时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_transport_snapshot', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_transport_snapshot', 'updated_at', '更新时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_transport_snapshot', 'vessel_name', '船名'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_transport_snapshot', 'voyage_no', '航次'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_weight_result', 'adjusted_at', '修正时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_weight_result', 'adjusted_by', '修正人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_weight_result', 'adjusted_weight_unit', '人工修正后的单位'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_weight_result', 'adjusted_weight_value', '人工修正后的原单位重量'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_weight_result', 'adjustment_reason', '人工修正原因'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_weight_result', 'confidence', '识别置信度'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_weight_result', 'confirmed_at', '确认时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_weight_result', 'confirmed_by', '确认人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_weight_result', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_weight_result', 'created_by', '创建人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_weight_result', 'deleted', '逻辑删除标记'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_weight_result', 'file_id', '称重图片文件ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_weight_result', 'final_weight_kg', '确认并统一换算后的重量kg'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_weight_result', 'id', 'OCR重量结果ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_weight_result', 'inbound_id', '入库单ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_weight_result', 'ocr_provider', 'OCR服务商编码'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_weight_result', 'ocr_raw_text', 'OCR原始文本'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_weight_result', 'ocr_weight_unit', 'OCR识别原始单位'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_weight_result', 'ocr_weight_value', 'OCR识别原始重量值'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_weight_result', 'recognition_status', '重量识别状态:PENDING待识别、PENDING_CONFIRM待确认、NEEDS_MANUAL需人工录入、CONFIRMED已确认'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_weight_result', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_weight_result', 'updated_at', '更新时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_weight_result', 'updated_by', '最后修改人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_weight_result', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_weight_revision', 'action', '动作:CORRECT或CONFIRM'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_weight_revision', 'after_weight_unit', '变更后单位'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_weight_revision', 'after_weight_value', '变更后原单位重量'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_weight_revision', 'before_weight_unit', '变更前单位'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_weight_revision', 'before_weight_value', '变更前原单位重量'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_weight_revision', 'final_weight_kg', '确认时统一换算后的重量kg'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_weight_revision', 'id', 'OCR重量修订记录ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_weight_revision', 'inbound_id', '入库单ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_weight_revision', 'operated_at', '操作时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_weight_revision', 'operator_id', '操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_weight_revision', 'operator_name', '操作人名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_weight_revision', 'reason', '人工修正原因'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_weight_revision', 'revision_no', '结果版本号,与乐观锁版本一致'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_weight_revision', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_weight_revision', 'weight_result_id', 'OCR重量结果ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_evidence', 'created_at', '证据记录创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_evidence', 'created_by', '上传证据的操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_evidence', 'created_by_name', '上传证据的操作人姓名快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_evidence', 'evidence_type', 'PHOTO_STEP_1空仓/PHOTO_STEP_2半仓/PHOTO_STEP_3满仓/PHOTO_STEP_4锁仓/SEAL带锁码照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_evidence', 'file_id', 'File 服务逻辑文件ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_evidence', 'file_name', '上传时文件名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_evidence', 'id', '装柜证据记录ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_evidence', 'inbound_id', '关联入库单逻辑ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_evidence', 'remark', '证据补充说明'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_evidence', 'seal_no', '锁码文本;带锁码照必填,客户不可见'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_evidence', 'status', '证据状态:ACTIVE有效、VOID已作废;错误照片保留记录不硬删除'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_evidence', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_evidence', 'updated_at', '证据记录最后更新时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_evidence', 'updated_by', '最后修改操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_evidence', 'updated_by_name', '最后修改操作人姓名快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_evidence', 'version', '乐观锁版本号,每次更新递增'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_evidence', 'void_reason', '照片作废原因'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_evidence', 'voided_at', '照片作废时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_evidence', 'voided_by', '执行作废的平台操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_evidence', 'voided_by_name', '执行作废的平台操作人姓名快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_operation', 'completed_at', '操作完成时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_operation', 'created_at', '操作声明创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_operation', 'id', '装柜写操作记录ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_operation', 'idempotency_key', '客户端幂等键,同一操作重试复用'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_operation', 'inbound_id', '操作目标入库单逻辑ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_operation', 'operation', '装柜或证据写操作类型'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_operation', 'operator_id', '执行写操作的平台用户ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_operation', 'request_hash', '规范化请求内容SHA-256摘要'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_operation', 'result_json', '成功结果或失败信息JSON快照,用于幂等重放'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_operation', 'status', '幂等处理状态:PROCESSING处理中、SUCCESS成功、FAILED失败'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_operation', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_assignment', 'assigned_by', '领取任务的作业员或执行分配的平台用户ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_assignment', 'assigned_by_name', '领取或分配操作人姓名快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_assignment', 'assignee_id', '当前唯一作业员ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_assignment', 'assignee_name', '当前作业员姓名快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_assignment', 'assignment_type', '分配方式:CLAIMED作业员领取、ASSIGNED平台指派'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_assignment', 'created_at', '任务领取或分配时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_assignment', 'pick_task_id', '柜次PDA拣货任务ID;同一任务仅保留一名作业员'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_assignment', 'updated_at', '作业员分配最后更新时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_assignment', 'version', '乐观锁版本号,防止多人并发领取或覆盖'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_history', 'action_type', '拣货动作:PICK正常拣货、PARTIAL部分拣货、SKIP跳拣'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_history', 'created_at', '不可变拣货操作历史创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_history', 'id', 'PDA拣货操作历史ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_history', 'operation_id', '客户端幂等操作号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_history', 'operation_reason', '部分拣货或跳拣原因'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_history', 'operator_id', '执行PDA拣货操作的作业员ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_history', 'operator_name', '作业员姓名快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_history', 'pick_item_id', 'PDA拣货明细ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_history', 'pick_task_id', 'PDA拣货任务ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_history', 'picked_package_count', '本次拣货箱数'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_history', 'request_hash', '完整批量请求内容SHA-256摘要'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item', 'created_at', 'PDA拣货明细创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item', 'id', 'PDA拣货明细ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item', 'inbound_detail_id', '订单货物明细ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item', 'inbound_id', '父入库单ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item', 'item_status', '拣货明细状态:PENDING待拣、PICKING拣货中、PICKED已拣完、PARTIAL部分拣货、SKIPPED跳拣'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item', 'loading_plan_item_id', '装柜计划明细ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item', 'pick_task_id', 'PDA拣货任务ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item', 'picked_package_count', '已拣货箱数'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item', 'planned_package_count', '计划拣货箱数;上层拆分按业务规则向上取整'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item', 'release_location_ids', '人工选择释放库位ID;空数组表示明确释放0个'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item', 'release_mode', 'AUTO按比例释放,MANUAL按人工选择释放'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item', 'updated_at', 'PDA拣货明细最后更新时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item_assignment', 'assigned_by', '执行领取或分配操作的用户ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item_assignment', 'assigned_by_name', '执行领取或分配操作的用户姓名快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item_assignment', 'assignee_id', '当前领取或被分配的作业员ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item_assignment', 'assignee_name', '当前作业员姓名快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item_assignment', 'assignment_type', '分配类型:CLAIMED作业员领取、ASSIGNED管理员分配'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item_assignment', 'created_at', '明细分配记录创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item_assignment', 'pick_item_id', 'PDA拣货明细ID,同时作为本表主键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item_assignment', 'updated_at', '明细分配记录最后更新时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_item_assignment', 'version', '乐观锁版本号,每次更新递增'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_operation', 'action_type', '动作类型:CLAIM领取、ASSIGN分配、REVIEW复核、BATCH批量提交'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_operation', 'created_at', '不可变操作审计创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_operation', 'id', 'PDA任务操作审计记录ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_operation', 'operation_id', '全局唯一操作ID,用于幂等重放'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_operation', 'operator_id', '执行PDA任务操作的用户ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_operation', 'operator_name', '操作人姓名快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_operation', 'pick_task_id', '关联柜次PDA拣货任务ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_operation', 'request_hash', '完整批量请求或任务操作内容SHA-256摘要'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_review', 'created_at', '复核通过时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_review', 'pick_task_id', '已完成的柜次PDA拣货任务ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_review', 'remark', 'PDA任务复核备注'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_review', 'review_status', '复核状态:APPROVED已通过;记录创建后不可修改或删除'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_review', 'reviewed_by', '执行完成复核的平台用户ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_review', 'reviewed_by_name', '复核人姓名快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_review', 'updated_at', '复核记录最后更新时间;正常业务不允许更新'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_review', 'version', '复核记录版本号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_task', 'completed_at', '完成时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_task', 'completed_by', '完成装柜人'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_task', 'completed_by_name', '完成装柜人名称'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_task', 'container_no', '集装箱号快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_task', 'id', 'PDA拣货任务ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_task', 'loading_plan_id', '装柜计划ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_task', 'started_at', '开始时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_task', 'started_by', '开始装柜人'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_task', 'started_by_name', '开始装柜人名称'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_task', 'task_status', 'PDA任务状态:LOADING拣货装柜中、COMPLETED已完成'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_task', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan', 'confirmed_at', '确认时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan', 'confirmed_by', '确认操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan', 'confirmed_by_name', '确认操作人名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan', 'container_booking_id', '货柜预订ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan', 'created_by', '创建人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan', 'created_by_name', '创建人名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan', 'id', '智能装柜计划ID(雪花ID)'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan', 'plan_no', '平台唯一装柜计划号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan', 'plan_status', '装柜计划状态:EDITING编辑中、LOADING装柜中、COMPLETED装柜完成;建议须经人工确认'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan', 'preload_completed_at', '预装完成时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan', 'preload_completed_by', '预装完成操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan', 'preload_completed_by_name', '预装完成操作人名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan', 'preload_pushed_at', '开始装柜并进入LOADING的时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan', 'preload_pushed_by', '推送预装操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan', 'preload_pushed_by_name', '推送预装操作人名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan', 'push_idempotency_key', '推送预装幂等键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan', 'remark', '平台装柜计划备注'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan', 'updated_at', '装柜计划最后更新时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan', 'updated_by', '最后修改人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan', 'updated_by_name', '最后修改人名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_history', 'action_type', '动作类型:CREATE_PLAN创建计划、ADD_ITEM新增明细、ADJUST_ITEM调整明细、REMOVE_ITEM移除明细、START_LOADING开始装柜、COMPLETE_LOADING完成装柜、PICK_ITEM拣货'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_history', 'after_snapshot', '变更后快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_history', 'before_snapshot', '变更前快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_history', 'created_at', '操作时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_history', 'from_status', '变更前状态'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_history', 'id', '装柜操作历史ID(雪花ID)'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_history', 'loading_plan_id', '智能装柜计划ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_history', 'loading_plan_item_id', '装柜计划明细ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_history', 'operation_id', '操作幂等ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_history', 'operation_reason', '操作原因'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_history', 'operator_id', '操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_history', 'operator_name', '操作人名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_history', 'to_status', '变更后状态'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'account_id', '客户账号ID;装柜业务唯一客户归属字段'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'active_inbound_detail_id', '未删除明细唯一锁;删除后返回NULL释放分配资格'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'created_by', '创建人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'created_by_name', '创建人名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'customer_name', '客户名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'customer_remark', '客户备注快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'deleted', '推送前逻辑删除标记;冻结后不可修改'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'destination_warehouse_code', '目的仓编码快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'destination_warehouse_id', '目的仓ID快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'destination_warehouse_name', '目的仓名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'dimensional_weight_kg', '本次装柜材积重kg'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'fba_tracking_no', 'FBA追踪号快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'frozen_at', '装柜完成后的永久冻结时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'frozen_by', '冻结操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'gross_weight_kg', '本次装柜毛重kg'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'id', '装柜计划明细ID(雪花ID)'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'inbound_code', '入仓码快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'inbound_detail_id', '订单货物或入库货物明细ID,不建立跨库外键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'inbound_id', 'Firstleg入库单ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'internal_remark', '平台内部备注'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'load_sequence', '装柜顺序'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'loading_plan_id', '智能装柜计划ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'net_weight_kg', '本次装柜净重kg'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'order_id', '订单服务订单ID快照,不建立跨库外键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'order_no', '订单号快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'package_count', '本次装柜箱数'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'pallet_count', '托盘数'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'position_snapshot', '智能装柜位置与摆放结果快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'product_image_file_id', '产品图片File对象ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'product_image_url', '产品图片访问地址快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'product_name_en', '英文品名快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'product_name_zh', '中文品名快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'product_quantity', '产品总数量'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'removed_at', '推送前逻辑删除时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'removed_by', '推送前逻辑删除操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'removed_by_name', '推送前逻辑删除操作人名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'shipment_no', '货件编号快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'shipping_mark', '唛头快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'sku_code', 'SKU编码快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'units_per_carton', '每箱产品数'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'updated_at', '装柜明细最后更新时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'updated_by', '最后修改人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'updated_by_name', '最后修改人名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'volume_cbm', '本次装柜体积m3'); +CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan_item', 'warehouse_remark', '仓库备注快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_measure_review_record', 'allow_rejected_config', '操作时Nacos异常放行开关值'); +CALL queue_column_comment('baiyue_firstleg', 'fl_measure_review_record', 'exception_release', '是否异常放行'); +CALL queue_column_comment('baiyue_firstleg', 'fl_measure_review_record', 'id', '测量审核记录ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_measure_review_record', 'inbound_id', '入库单ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_measure_review_record', 'measurement_id', '被审核的测量批次ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_measure_review_record', 'release_reason', '异常放行原因'); +CALL queue_column_comment('baiyue_firstleg', 'fl_measure_review_record', 'review_reason', '审核或驳回原因'); +CALL queue_column_comment('baiyue_firstleg', 'fl_measure_review_record', 'review_result', '审核结果'); +CALL queue_column_comment('baiyue_firstleg', 'fl_measure_review_record', 'reviewed_at', '审核时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_measure_review_record', 'reviewer_id', '审核人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_measure_review_record', 'reviewer_name', '审核人名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_measure_review_record', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_mojia_measurement_receipt', 'box_code', '设备扫描的原始箱码'); +CALL queue_column_comment('baiyue_firstleg', 'fl_mojia_measurement_receipt', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_mojia_measurement_receipt', 'height_cm', '设备实测高度cm'); +CALL queue_column_comment('baiyue_firstleg', 'fl_mojia_measurement_receipt', 'id', '墨家联调收件记录ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_mojia_measurement_receipt', 'image_file_id', 'File服务中的墨家联调图片ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_mojia_measurement_receipt', 'length_cm', '设备实测长度cm'); +CALL queue_column_comment('baiyue_firstleg', 'fl_mojia_measurement_receipt', 'machine_no', '设备编号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_mojia_measurement_receipt', 'match_failure_code', '正式业务匹配失败码'); +CALL queue_column_comment('baiyue_firstleg', 'fl_mojia_measurement_receipt', 'match_failure_reason', '脱敏后的匹配失败原因'); +CALL queue_column_comment('baiyue_firstleg', 'fl_mojia_measurement_receipt', 'measured_at', '设备测量时间或服务接收时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_mojia_measurement_receipt', 'request_id', '请求链路ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_mojia_measurement_receipt', 'scanned_shipment_id', '设备从Amazon FBA箱标实扫的Shipment ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_mojia_measurement_receipt', 'status', '测量回执匹配状态:UNMATCHED未匹配;当前首版仅记录未匹配回执'); +CALL queue_column_comment('baiyue_firstleg', 'fl_mojia_measurement_receipt', 'subject_id', '设备应用操作主体ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_mojia_measurement_receipt', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_mojia_measurement_receipt', 'trace_id', '调用链ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_mojia_measurement_receipt', 'updated_at', '更新时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_mojia_measurement_receipt', 'weight_kg', '设备实测重量kg'); +CALL queue_column_comment('baiyue_firstleg', 'fl_mojia_measurement_receipt', 'width_cm', '设备实测宽度cm'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'bill_of_lading_no', '提单号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'booking_agent_id', '订舱代理逻辑ID快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'booking_agent_name', '订舱代理名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'booking_no', '平台海运订舱编号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'cancellation_reason', '取消订舱原因'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'change_reason', '船期变更原因'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'created_at', '订舱记录创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'created_by', '创建操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'created_by_name', '创建操作人姓名快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'customer_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'customer_name', '客户名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'departure_port_code', '起运港编码快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'departure_port_name', '起运港名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'destination_port_code', '目的港编码快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'destination_port_name', '目的港名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'estimated_arrival_at', '当前确认预计到港时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'estimated_sailing_at', '当前确认预计开船时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'failure_reason', '订舱失败原因'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'freight_term', '海运费支付条款快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'id', '海运订舱ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'pending_estimated_arrival_at', '待确认变更预计到港时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'pending_estimated_sailing_at', '待确认变更预计开船时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'pending_vessel_name', '待确认变更船名'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'pending_voyage_no', '待确认变更航次号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'place_of_delivery', '最终交货地快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'remark', '平台内部备注'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'service_term', '运输服务条款快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'shipping_company_id', '船公司逻辑ID快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'shipping_company_name', '船公司名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'status', '订舱状态:DRAFT草稿、SUBMITTED已提交、CONFIRMED已确认、RELEASED已放舱、FAILED失败、CHANGE_PENDING船期待变更、CANCELLED已取消'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'updated_at', '订舱记录最后更新时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'updated_by', '最后修改操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'updated_by_name', '最后修改操作人姓名快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'vessel_name', '当前确认船名快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking', 'voyage_no', '当前确认航次号快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking_container_allocation', 'allocated_at', '柜次分配时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking_container_allocation', 'allocated_by', '分配操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking_container_allocation', 'allocated_by_name', '分配操作人姓名快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking_container_allocation', 'container_booking_id', '明确引用现有柜次ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking_container_allocation', 'id', '订舱柜次分配关系ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking_container_allocation', 'ocean_booking_id', '所属海运订舱ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking_container_allocation', 'requirement_id', '对应订舱箱型需求ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking_container_requirement', 'container_kind', '所需集装箱箱种编码'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking_container_requirement', 'container_type', '所需集装箱尺寸类型编码'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking_container_requirement', 'id', '订舱箱型需求ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking_container_requirement', 'ocean_booking_id', '所属海运订舱ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking_container_requirement', 'quantity', '该箱型箱种所需柜数'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking_container_requirement', 'sort_no', '订舱需求展示顺序'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking_operation', 'completed_at', '写操作成功完成时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking_operation', 'created_at', '幂等记录创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking_operation', 'id', '订舱写操作幂等记录ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking_operation', 'idempotency_key', '客户端写操作幂等键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking_operation', 'ocean_booking_id', '关联海运订舱ID;创建请求完成前允许为空'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking_operation', 'operation', '幂等写操作类型'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking_operation', 'operator_id', '发起写操作的平台操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking_operation', 'request_hash', '规范化请求内容SHA-256摘要;用于识别同键不同请求'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking_operation', 'result_json', '成功结果JSON快照;供幂等重放返回'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking_operation', 'status', '处理状态:PROCESSING处理中、SUCCESS成功'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking_party', 'address', '关系方地址快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking_party', 'contact_name', '关系方联系人快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking_party', 'contact_phone', '关系方联系电话快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking_party', 'id', '订舱提单关系方ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking_party', 'ocean_booking_id', '所属海运订舱ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking_party', 'party_name', '关系方名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking_party', 'party_type', '关系方类型:SHIPPER发货人、CONSIGNEE收货人、NOTIFY_PARTY通知人'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking_status_history', 'action', '触发状态变更的业务动作'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking_status_history', 'after_snapshot', '状态变更后订舱数据快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking_status_history', 'before_snapshot', '状态变更前订舱数据快照;创建时为空'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking_status_history', 'created_at', '状态历史创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking_status_history', 'from_status', '变更前订舱状态;创建时为空'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking_status_history', 'id', '订舱状态历史ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking_status_history', 'ocean_booking_id', '所属海运订舱ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking_status_history', 'operator_id', '操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking_status_history', 'operator_name', '操作人姓名快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking_status_history', 'reason', '状态变更原因'); +CALL queue_column_comment('baiyue_firstleg', 'fl_ocean_booking_status_history', 'to_status', '变更后订舱状态'); +CALL queue_column_comment('baiyue_firstleg', 'fl_outbound_dispatch', 'confirmed_at', '国内出仓确认时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_outbound_dispatch', 'confirmed_by', '确认国内出仓的操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_outbound_dispatch', 'confirmed_by_name', '确认国内出仓的操作人姓名快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_outbound_dispatch', 'container_booking_id', '关联柜次逻辑ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_outbound_dispatch', 'created_at', '出仓调度记录创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_outbound_dispatch', 'id', '国内出仓调度记录ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_outbound_dispatch', 'inbound_id', '关联入库单逻辑ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_outbound_dispatch', 'order_id', 'Order服务订单逻辑ID,不复制订单主数据'); +CALL queue_column_comment('baiyue_firstleg', 'fl_outbound_dispatch', 'remark', '国内出仓调度备注'); +CALL queue_column_comment('baiyue_firstleg', 'fl_outbound_dispatch', 'reversed_at', '国内出仓撤销时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_outbound_dispatch', 'reversed_by', '撤销国内出仓的操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_outbound_dispatch', 'reversed_by_name', '撤销国内出仓的操作人姓名快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_outbound_dispatch', 'status', '出仓状态:READY_FOR_DISPATCH待确认出仓、WAITING_FOR_LOADING等待装柜'); +CALL queue_column_comment('baiyue_firstleg', 'fl_outbound_dispatch', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_outbound_dispatch', 'updated_at', '出仓调度记录最后更新时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_outbound_dispatch', 'version', '乐观锁版本号,每次更新递增'); +CALL queue_column_comment('baiyue_firstleg', 'fl_outbound_dispatch_history', 'action', '出仓状态变更动作'); +CALL queue_column_comment('baiyue_firstleg', 'fl_outbound_dispatch_history', 'container_booking_id', '关联柜次逻辑ID快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_outbound_dispatch_history', 'created_at', '状态历史创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_outbound_dispatch_history', 'dispatch_id', '关联国内出仓调度记录ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_outbound_dispatch_history', 'from_status', '变更前出仓状态;首次创建时为空'); +CALL queue_column_comment('baiyue_firstleg', 'fl_outbound_dispatch_history', 'id', '出仓状态历史记录ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_outbound_dispatch_history', 'inbound_id', '关联入库单逻辑ID快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_outbound_dispatch_history', 'operator_id', '执行状态变更的平台用户ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_outbound_dispatch_history', 'operator_name', '执行状态变更的平台用户姓名快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_outbound_dispatch_history', 'remark', '状态变更原因或备注'); +CALL queue_column_comment('baiyue_firstleg', 'fl_outbound_dispatch_history', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_outbound_dispatch_history', 'to_status', '变更后出仓状态'); +CALL queue_column_comment('baiyue_firstleg', 'fl_outbound_dispatch_operation', 'completed_at', '幂等操作完成时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_outbound_dispatch_operation', 'created_at', '幂等声明创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_outbound_dispatch_operation', 'dispatch_id', '首次声明幂等键时允许为空'); +CALL queue_column_comment('baiyue_firstleg', 'fl_outbound_dispatch_operation', 'id', '出仓幂等操作记录ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_outbound_dispatch_operation', 'idempotency_key', '客户端幂等键,同一操作重试复用'); +CALL queue_column_comment('baiyue_firstleg', 'fl_outbound_dispatch_operation', 'operation', '国内出仓写操作类型'); +CALL queue_column_comment('baiyue_firstleg', 'fl_outbound_dispatch_operation', 'operator_id', '执行写操作的平台用户ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_outbound_dispatch_operation', 'request_hash', '规范化请求内容SHA-256摘要'); +CALL queue_column_comment('baiyue_firstleg', 'fl_outbound_dispatch_operation', 'status', '幂等处理状态:PROCESSING处理中、SUCCESS成功'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule', 'booking_cutoff_at', '截单时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule', 'container_booking_id', '关联柜次ID;订舱整票船期时为空'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule', 'created_at', '船期记录创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule', 'created_by', '创建操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule', 'created_by_name', '创建操作人姓名快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule', 'customer_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule', 'customs_cutoff_at', '截关时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule', 'departure_port_code', '起运港编码快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule', 'departure_port_name', '起运港名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule', 'destination_port_code', '目的港编码快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule', 'destination_port_name', '目的港名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule', 'eta_at', '预计到港时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule', 'etd_at', '预计离港时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule', 'expires_at', '超过该时间即由API计算为过期'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule', 'id', '港务船期记录ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule', 'observed_at', '来源数据观测时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule', 'ocean_booking_id', '关联海运订舱ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule', 'port_cutoff_at', '截港时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule', 'remark', '平台内部备注'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule', 'schedule_no', '港务船期记录编号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule', 'source_name', '船期来源名称'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule', 'source_ref', '来源侧记录唯一引用或查询凭据摘要'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule', 'source_type', '船期来源类型:MANUAL人工录入、CARRIER船公司、TERMINAL码头、API外部接口'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule', 'updated_at', '船期记录最后更新时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule', 'updated_by', '最后修改操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule', 'updated_by_name', '最后修改操作人姓名快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule', 'vessel_name', '船名快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule', 'voyage_no', '航次号快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule_history', 'action', '变更动作:CREATE创建、UPDATE更新'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule_history', 'after_snapshot', '变更后船期数据快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule_history', 'before_snapshot', '变更前船期数据快照;创建时为空'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule_history', 'created_at', '历史记录创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule_history', 'from_version', '变更前版本;创建时为空'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule_history', 'id', '港务船期变更历史ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule_history', 'operator_id', '操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule_history', 'operator_name', '操作人姓名快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule_history', 'port_schedule_id', '所属港务船期记录ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule_history', 'reason', '变更原因或说明'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule_history', 'to_version', '变更后版本'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule_operation', 'completed_at', '写操作成功完成时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule_operation', 'created_at', '幂等记录创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule_operation', 'id', '港务船期写操作幂等记录ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule_operation', 'idempotency_key', '客户端写操作幂等键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule_operation', 'operation', '幂等写操作类型'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule_operation', 'operator_id', '发起写操作的平台操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule_operation', 'port_schedule_id', '关联港务船期ID;创建请求完成前允许为空'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule_operation', 'request_hash', '规范化请求内容SHA-256摘要;用于识别同键不同请求'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule_operation', 'result_json', '成功结果JSON快照;供幂等重放返回'); +CALL queue_column_comment('baiyue_firstleg', 'fl_port_schedule_operation', 'status', '处理状态:PROCESSING处理中、SUCCESS成功'); +CALL queue_column_comment('baiyue_firstleg', 'fl_preload_loading_plan_link', 'created_at', '关联记录创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_preload_loading_plan_link', 'created_by', '建立预装与装柜计划关联的操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_preload_loading_plan_link', 'created_by_name', '建立关联的操作人姓名快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_preload_loading_plan_link', 'id', '关联ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_preload_loading_plan_link', 'idempotency_key', '建立关联幂等键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_preload_loading_plan_link', 'inbound_id', '入库单ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_preload_loading_plan_link', 'loading_plan_id', '柜次级装柜计划ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_preload_loading_plan_link', 'preload_plan_id', '入库单级预装计划ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_preload_loading_plan_link', 'request_hash', '建立关联请求SHA-256'); +CALL queue_column_comment('baiyue_firstleg', 'fl_preload_loading_plan_link', 'tenant_id', '入库单客户隔离ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_preload_plan', 'created_at', '预装清单创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_preload_plan', 'created_by', '创建预装清单的操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_preload_plan', 'created_by_name', '创建操作人姓名快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_preload_plan', 'id', '预装清单ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_preload_plan', 'inbound_id', '关联入库单逻辑ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_preload_plan', 'items_json', '可编辑分柜明细JSON快照;人工确认后作为预装依据'); +CALL queue_column_comment('baiyue_firstleg', 'fl_preload_plan', 'remark', '预装备注'); +CALL queue_column_comment('baiyue_firstleg', 'fl_preload_plan', 'status', '预装状态:PENDING待确认、CONFIRMING确认中、CONFIRMED已确认'); +CALL queue_column_comment('baiyue_firstleg', 'fl_preload_plan', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_preload_plan', 'updated_at', '预装清单最后更新时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_preload_plan', 'updated_by', '最后修改操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_preload_plan', 'updated_by_name', '最后修改操作人姓名快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_preload_plan', 'version', '乐观锁版本号,每次更新递增'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_channel', 'channel_code', '固定渠道编码'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_channel', 'channel_name', '渠道名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_channel', 'created_at', '渠道快照创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_channel', 'created_by', '创建人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_channel', 'deleted', '逻辑删除标记'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_channel', 'estimated_departure_date', '标准预计开船日期'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_channel', 'estimated_departure_text', '原始预计开船日期文本'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_channel', 'id', '版本渠道快照ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_channel', 'latest_warehouse_entry', '最晚进仓时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_channel', 'price_book_version_id', '整表价格版本ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_channel', 'price_model', '价格模型快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_channel', 'reference_transit', '参考时效'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_channel', 'remark', '渠道备注'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_channel', 'source_sheet_name', '原始Excel Sheet'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_channel', 'surcharge_description', '附加费说明,仅展示'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_channel', 'updated_at', '渠道快照最后更新时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_channel', 'updated_by', '最后修改人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_channel', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_channel', 'vessel_name', '船名'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_channel', 'voyage_no', '航次'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_version', 'blocking_error_count', '阻断错误数'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_version', 'channel_count', '解析渠道数'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_version', 'created_at', '上传记录创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_version', 'created_by', '上传价格表的操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_version', 'deleted', '逻辑删除标记:0未删除、1已删除'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_version', 'id', '整表价格版本ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_version', 'original_file_name', '原始Excel文件名'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_version', 'parse_issues_json', '上传解析问题及待确认重新校验结果'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_version', 'published_at', '确认并生效时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_version', 'published_by', '确认并发布价格版本的操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_version', 'rate_count', '解析价格数'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_version', 'source_file_id', 'File服务原始文件ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_version', 'source_sha256', '原始文件SHA-256'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_version', 'status', '价格版本状态:PENDING_CONFIRMATION待确认、ACTIVE已生效、SUPERSEDED已被替代、FAILED解析失败'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_version', 'superseded_at', '被下一版本替换时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_version', 'tenant_id', '平台公共价格作用域固定为0'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_version', 'updated_at', '最后更新时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_version', 'updated_by', '最后修改人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_version', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_version', 'version_no', '整表版本号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_book_version', 'warning_count', '警告数'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_channel_catalog', 'aliases_json', '上传名称别名数组'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_channel_catalog', 'channel_code', '稳定渠道编码'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_channel_catalog', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_channel_catalog', 'display_name', '平台渠道名称'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_channel_catalog', 'id', '渠道目录ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_channel_catalog', 'price_model', '默认价格模型'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_channel_catalog', 'source_sheet_name', '来源Sheet名称;海派为同一Sheet'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_channel_catalog', 'status', '目录状态:ACTIVE启用、DISABLED停用'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_channel_catalog', 'updated_at', '最后更新时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_origin_alias', 'alias_name', 'Excel起运仓名称别名'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_origin_alias', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_origin_alias', 'id', '起运仓别名ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_origin_alias', 'status', '状态:ACTIVE启用、DISABLED停用'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_origin_alias', 'updated_at', '最后更新时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_origin_alias', 'warehouse_code', '平台仓库稳定编码,不存环境专属ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_warehouse_mapping', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_warehouse_mapping', 'id', '价格仓映射ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_warehouse_mapping', 'source_type', '来源维度:ORIGIN_CODE或OVERSEAS_NAME'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_warehouse_mapping', 'source_value', '解析后的来源仓编码或海外仓名称'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_warehouse_mapping', 'status', '状态:ACTIVE启用、DISABLED停用'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_warehouse_mapping', 'updated_at', '最后更新时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_warehouse_mapping', 'warehouse_code', '平台仓库稳定编码快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_warehouse_mapping', 'warehouse_id', '固定平台公共仓库ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_price_warehouse_mapping', 'warehouse_type', 'DOMESTIC或OVERSEAS'); +CALL queue_column_comment('baiyue_firstleg', 'fl_split_operation', 'created_at', '幂等操作声明创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_split_operation', 'id', '幂等记录ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_split_operation', 'idempotency_key', '客户端幂等键,同一操作重试复用'); +CALL queue_column_comment('baiyue_firstleg', 'fl_split_operation', 'inbound_id', '操作目标入库单逻辑ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_split_operation', 'operation', '分柜或预装写操作类型'); +CALL queue_column_comment('baiyue_firstleg', 'fl_split_operation', 'operator_id', '执行分柜或预装写操作的平台用户ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_split_operation', 'result_json', '操作结果JSON快照,用于幂等重放'); +CALL queue_column_comment('baiyue_firstleg', 'fl_split_operation', 'status', '幂等处理状态:PROCESSING处理中、SUCCESS成功'); +CALL queue_column_comment('baiyue_firstleg', 'fl_split_operation', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_third_party_call_log', 'app_key', '第三方应用标识'); +CALL queue_column_comment('baiyue_firstleg', 'fl_third_party_call_log', 'id', '第三方调用日志ID(雪花ID)'); +CALL queue_column_comment('baiyue_firstleg', 'fl_third_party_call_log', 'idempotency_anchor', '签名通过后首个请求为1,重复调用为空'); +CALL queue_column_comment('baiyue_firstleg', 'fl_third_party_call_log', 'idempotency_key', 'HTTP幂等键,查询接口为空'); +CALL queue_column_comment('baiyue_firstleg', 'fl_third_party_call_log', 'nonce', '请求Nonce'); +CALL queue_column_comment('baiyue_firstleg', 'fl_third_party_call_log', 'processed_at', '处理完成时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_third_party_call_log', 'processing_status', 'PROCESSING、ACCEPTED或REJECTED'); +CALL queue_column_comment('baiyue_firstleg', 'fl_third_party_call_log', 'raw_payload', '未经重新序列化的UTF-8原始请求体'); +CALL queue_column_comment('baiyue_firstleg', 'fl_third_party_call_log', 'request_hash', '原始请求体SHA-256小写十六进制'); +CALL queue_column_comment('baiyue_firstleg', 'fl_third_party_call_log', 'request_method', 'HTTP方法'); +CALL queue_column_comment('baiyue_firstleg', 'fl_third_party_call_log', 'request_path', '签名使用的规范路径'); +CALL queue_column_comment('baiyue_firstleg', 'fl_third_party_call_log', 'request_timestamp', '请求头Unix毫秒时间戳原值'); +CALL queue_column_comment('baiyue_firstleg', 'fl_third_party_call_log', 'requested_at', '服务端接收时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_third_party_call_log', 'result_code', '统一响应业务码'); +CALL queue_column_comment('baiyue_firstleg', 'fl_third_party_call_log', 'result_json', '首次成功结果快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_third_party_call_log', 'result_message', '处理结果或失败原因'); +CALL queue_column_comment('baiyue_firstleg', 'fl_third_party_call_log', 'signature_subject', '签名主体快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_third_party_call_log', 'signature_value', '第三方签名原值'); +CALL queue_column_comment('baiyue_firstleg', 'fl_third_party_call_log', 'tenant_id', '第三方未知/未解析主体哨兵值;不是平台客户记录或平台作用域,不得用于业务数据授权'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'actual_arrived_warehouse_at', '车辆实际到达收货仓时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'actual_departed_at', '车辆实际离仓时间;DEPARTED后供LOADED门禁使用'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'actual_loading_at', '实际开始装柜时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'appointment_no', '平台拖车预约编号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'carrier_id', '拖车服务商逻辑ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'carrier_name', '拖车服务商名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'container_booking_id', '关联柜次逻辑ID;拖车完成供装柜门禁校验'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'created_at', '拖车预约创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'currency', '金额币种;首期默认RMB'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'customer_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'customer_name', '客户名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'driver_id_card', '司机身份证号;平台内部敏感信息'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'driver_name', '司机姓名'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'driver_phone', '司机联系电话'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'fleet_contact', '车队联系人'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'fleet_phone', '车队联系电话'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'gross_weight_kg', '预约货物毛重,单位千克'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'id', '拖车预约ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'loading_plan_id', '关联装柜计划逻辑ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'ocean_booking_id', '关联海运订舱逻辑ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'outbound_order_id', '关联头程订单逻辑ID;不跨库建外键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'package_count', '预约货物包装件数快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'pallet_count', '预约托盘数量'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'payable_amount', '应付拖车服务商金额;币种由currency指定'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'planned_loading_at', '预约到仓装柜时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'po_no', '关联采购单号或客户PO号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'receivable_amount', '向客户收取的拖车金额;币种由currency指定'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'remark', '平台内部备注或异常说明'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'revision', '拖车预约业务修订号;每次状态或改约递增'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'status', '拖车预约状态:DRAFT草稿、PENDING_DISPATCH待派车、DISPATCHED已派车、PICKUP_IN_PROGRESS提货中、ARRIVED_WAREHOUSE已到仓、LOADING装柜中、DEPARTED已离仓、COMPLETED已完成、RESCHEDULED已改约、EXCEPTION异常、CANCELLED已取消'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'store_address', '到仓装柜地点地址快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'store_city', '到仓地点城市快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'store_contact', '到仓地点联系人快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'store_name', '到仓装柜地点名称快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'store_phone', '到仓地点联系电话快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'store_zipcode', '到仓地点邮编快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'transit_minutes', '从到仓到离仓的运输作业耗时,单位分钟'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'updated_at', '拖车预约最后更新时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'vehicle_no', '车辆车牌号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'volume_cbm', '预约货物体积,单位立方米'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment', 'warehouse_id', '预约到仓的国内收货仓逻辑ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment_attachment', 'appointment_id', '所属拖车预约ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment_attachment', 'attachment_type', '附件类型编码;由业务动作约定'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment_attachment', 'file_hash', '文件内容SHA-256摘要'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment_attachment', 'file_id', 'File服务文件逻辑ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment_attachment', 'file_name', '上传时文件名快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment_attachment', 'id', '拖车附件关系ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment_attachment', 'uploaded_at', '附件上传时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment_attachment', 'uploaded_by', '上传操作人ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment_attachment', 'visibility', '可见范围:INTERNAL仅平台内部、CLIENT客户可见'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment_history', 'action', '触发状态或改约的业务动作'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment_history', 'appointment_id', '所属拖车预约ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment_history', 'created_at', '历史记录创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment_history', 'from_status', '变更前拖车状态;创建时为空'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment_history', 'id', '拖车预约历史ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment_history', 'operator_id', '执行操作的平台用户ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment_history', 'operator_name', '操作人姓名快照'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment_history', 'reason', '状态或改约原因'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment_history', 'request_id', '请求链路中的请求ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment_history', 'revision', '该预约业务修订号'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment_history', 'to_status', '变更后拖车状态;改约时可保持原状态'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment_operation', 'action', '执行的拖车业务动作'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment_operation', 'appointment_id', '所属拖车预约ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment_operation', 'created_at', '幂等记录创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment_operation', 'id', '拖车写操作幂等记录ID'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment_operation', 'idempotency_key', '客户端写操作幂等键'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment_operation', 'request_hash', '规范化请求内容SHA-256摘要;用于识别同键不同请求'); +CALL queue_column_comment('baiyue_firstleg', 'fl_truck_appointment_operation', 'response_snapshot', '成功响应JSON快照;供相同幂等请求重放'); +CALL queue_column_comment('baiyue_firstleg', 'sys_audit_outbox', 'attempt_count', '正常发送累计尝试次数'); +CALL queue_column_comment('baiyue_firstleg', 'sys_audit_outbox', 'compensation_count', '人工或自动补偿累计次数'); +CALL queue_column_comment('baiyue_firstleg', 'sys_audit_outbox', 'created_at', '事件创建时间'); +CALL queue_column_comment('baiyue_firstleg', 'sys_audit_outbox', 'dead_at', '转入死信状态的时间'); +CALL queue_column_comment('baiyue_firstleg', 'sys_audit_outbox', 'event_id', '审计事件全局唯一ID'); +CALL queue_column_comment('baiyue_firstleg', 'sys_audit_outbox', 'event_type', '审计事件类型'); +CALL queue_column_comment('baiyue_firstleg', 'sys_audit_outbox', 'last_error', '最近一次发送失败原因'); +CALL queue_column_comment('baiyue_firstleg', 'sys_audit_outbox', 'lease_owner', '当前发送任务租约持有者标识'); +CALL queue_column_comment('baiyue_firstleg', 'sys_audit_outbox', 'lease_until', '当前发送任务租约到期时间'); +CALL queue_column_comment('baiyue_firstleg', 'sys_audit_outbox', 'next_retry_at', '允许下次重试的最早时间'); +CALL queue_column_comment('baiyue_firstleg', 'sys_audit_outbox', 'payload_json', '待发送的审计事件完整负载快照'); +CALL queue_column_comment('baiyue_firstleg', 'sys_audit_outbox', 'resource_id', '被审计业务资源ID;无法确定时为空'); +CALL queue_column_comment('baiyue_firstleg', 'sys_audit_outbox', 'resource_type', '被审计业务资源类型'); +CALL queue_column_comment('baiyue_firstleg', 'sys_audit_outbox', 'sent_at', '首次成功发送时间'); +CALL queue_column_comment('baiyue_firstleg', 'sys_audit_outbox', 'service_name', '产生审计事件的服务名称'); +CALL queue_column_comment('baiyue_firstleg', 'sys_audit_outbox', 'status', '发送状态:PENDING待发送、SENDING发送中、SENT已发送、DEAD已转死信'); +CALL queue_column_comment('baiyue_firstleg', 'sys_audit_outbox', 'updated_at', '记录最后更新时间'); +CALL queue_column_comment('baiyue_iam', 'iam_data_scope', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_iam', 'iam_data_scope', 'id', '数据范围规则ID'); +CALL queue_column_comment('baiyue_iam', 'iam_data_scope', 'role_id', '角色ID'); +CALL queue_column_comment('baiyue_iam', 'iam_data_scope', 'scope_expression', '自定义数据范围表达式'); +CALL queue_column_comment('baiyue_iam', 'iam_data_scope', 'scope_type', '数据范围类型:SELF本人、DEPARTMENT部门、TENANT当前客户、CUSTOM自定义'); +CALL queue_column_comment('baiyue_iam', 'iam_data_scope', 'tenant_id', '规则作用域:0为平台,正数为客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_iam', 'iam_membership', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_iam', 'iam_membership', 'created_by', '客户成员关系创建人ID;系统创建时为0'); +CALL queue_column_comment('baiyue_iam', 'iam_membership', 'deleted', '逻辑删除标记'); +CALL queue_column_comment('baiyue_iam', 'iam_membership', 'id', '客户成员关系ID'); +CALL queue_column_comment('baiyue_iam', 'iam_membership', 'joined_at', '加入客户企业时间'); +CALL queue_column_comment('baiyue_iam', 'iam_membership', 'left_at', '离开客户企业时间'); +CALL queue_column_comment('baiyue_iam', 'iam_membership', 'member_no', '客户企业内部成员编号'); +CALL queue_column_comment('baiyue_iam', 'iam_membership', 'member_type', '成员类型:EMPLOYEE企业员工等'); +CALL queue_column_comment('baiyue_iam', 'iam_membership', 'status', '成员状态:ACTIVE在职、DISABLED停用、LEFT已离开'); +CALL queue_column_comment('baiyue_iam', 'iam_membership', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_iam', 'iam_membership', 'updated_at', '更新时间'); +CALL queue_column_comment('baiyue_iam', 'iam_membership', 'updated_by', '客户成员关系最后修改人ID;系统修改时为0'); +CALL queue_column_comment('baiyue_iam', 'iam_membership', 'user_id', 'IAM用户资料ID'); +CALL queue_column_comment('baiyue_iam', 'iam_membership', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_iam', 'iam_membership_role', 'created_at', '授权时间'); +CALL queue_column_comment('baiyue_iam', 'iam_membership_role', 'membership_id', '客户成员关系ID'); +CALL queue_column_comment('baiyue_iam', 'iam_membership_role', 'role_id', '角色ID'); +CALL queue_column_comment('baiyue_iam', 'iam_membership_role', 'tenant_id', '冗余客户ID和数据隔离键,用于授权归属校验和查询'); +CALL queue_column_comment('baiyue_iam', 'iam_permission', 'app_code', '所属应用端:MERCHANT企业客户端、PLATFORM平台后台'); +CALL queue_column_comment('baiyue_iam', 'iam_permission', 'built_in', '是否为系统预置权限;预置资源不可通过管理接口修改'); +CALL queue_column_comment('baiyue_iam', 'iam_permission', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_iam', 'iam_permission', 'deleted', '逻辑删除标记'); +CALL queue_column_comment('baiyue_iam', 'iam_permission', 'http_method', 'API权限对应的HTTP方法'); +CALL queue_column_comment('baiyue_iam', 'iam_permission', 'id', '权限资源ID'); +CALL queue_column_comment('baiyue_iam', 'iam_permission', 'parent_id', '父权限ID,用于构造菜单和页面树'); +CALL queue_column_comment('baiyue_iam', 'iam_permission', 'permission_code', '鉴权使用的唯一权限码'); +CALL queue_column_comment('baiyue_iam', 'iam_permission', 'permission_name', '权限名称'); +CALL queue_column_comment('baiyue_iam', 'iam_permission', 'permission_type', '权限类型:MENU、PAGE、BUTTON或API'); +CALL queue_column_comment('baiyue_iam', 'iam_permission', 'route_path', '前端路由或API路径'); +CALL queue_column_comment('baiyue_iam', 'iam_permission', 'sort_no', '同级显示顺序'); +CALL queue_column_comment('baiyue_iam', 'iam_permission', 'status', '权限状态:ACTIVE启用、DISABLED停用'); +CALL queue_column_comment('baiyue_iam', 'iam_permission', 'tenant_id', '权限作用域:0为平台预置权限,正数为客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_iam', 'iam_permission', 'updated_at', '更新时间'); +CALL queue_column_comment('baiyue_iam', 'iam_role', 'app_code', '应用端:MERCHANT企业客户端、PLATFORM平台后台'); +CALL queue_column_comment('baiyue_iam', 'iam_role', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_iam', 'iam_role', 'created_by', '创建人ID'); +CALL queue_column_comment('baiyue_iam', 'iam_role', 'deleted', '逻辑删除标记'); +CALL queue_column_comment('baiyue_iam', 'iam_role', 'description', '角色用途说明'); +CALL queue_column_comment('baiyue_iam', 'iam_role', 'id', '角色ID'); +CALL queue_column_comment('baiyue_iam', 'iam_role', 'role_code', '作用域内唯一角色编码'); +CALL queue_column_comment('baiyue_iam', 'iam_role', 'role_name', '角色名称'); +CALL queue_column_comment('baiyue_iam', 'iam_role', 'role_type', '角色类型:SYSTEM或CUSTOM'); +CALL queue_column_comment('baiyue_iam', 'iam_role', 'status', '角色状态:ACTIVE启用、DISABLED停用'); +CALL queue_column_comment('baiyue_iam', 'iam_role', 'tenant_id', '角色作用域:0为平台预置模板,正数为客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_iam', 'iam_role', 'updated_at', '更新时间'); +CALL queue_column_comment('baiyue_iam', 'iam_role', 'updated_by', '更新人ID'); +CALL queue_column_comment('baiyue_iam', 'iam_role', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_iam', 'iam_role_permission', 'created_at', '授权时间'); +CALL queue_column_comment('baiyue_iam', 'iam_role_permission', 'permission_id', '权限资源ID'); +CALL queue_column_comment('baiyue_iam', 'iam_role_permission', 'role_id', '角色ID'); +CALL queue_column_comment('baiyue_iam', 'iam_role_permission', 'tenant_id', '冗余客户ID或平台作用域ID,用于授权归属校验和查询'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'approved_at', '客户资料审核通过时间'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'approved_by', '客户资料审核人ID'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'contact_email', '联系邮箱'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'contact_name', '企业联系人'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'contact_phone', '联系电话'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'contact_wechat', '联系微信'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'created_by', '创建人ID'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'deleted', '逻辑删除标记'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'enterprise_type', '企业类型'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'established_date', '成立日期'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'id', '客户主档ID(雪花ID);0保留为平台技术作用域'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'internal_remark', '平台内部备注'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'legal_name', '企业法定名称'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'legal_representative', '法定代表人'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'payment_term_days', '账期天数'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'registered_address', '注册地址文本'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'registered_capital', '注册资本'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'registered_capital_currency', '注册资本币种 ISO 4217'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'reject_reason', '客户资料审核驳回原因'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'sales_owner_name', '销售负责人姓名'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'service_package', '客户服务套餐'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'status', '客户状态:PROFILE_REQUIRED待完善资料、PENDING待审核、ACTIVE正常、REJECTED已驳回、DISABLED已停用'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'tenant_code', '全局唯一客户编码'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'tenant_name', '客户企业名称'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'tenant_type', '主体类型:MERCHANT企业客户'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'unified_social_credit_code', '统一社会信用代码'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'updated_at', '更新时间'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'updated_by', '最后修改人ID'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant_address', 'address_type', '地址类型:注册地址、办公地址等'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant_address', 'city', '城市'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant_address', 'country_code', '国家二字码'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant_address', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant_address', 'deleted', '逻辑删除标记'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant_address', 'detail', '详细地址'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant_address', 'district', '区县'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant_address', 'id', '客户地址ID'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant_address', 'is_default', '是否默认地址:0否、1是'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant_address', 'postal_code', '邮政编码'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant_address', 'province', '省/州'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant_address', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant_address', 'updated_at', '更新时间'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant_qualification', 'certificate_no', '证书编号'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant_qualification', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant_qualification', 'deleted', '逻辑删除标记'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant_qualification', 'document_url', '资质文件存储地址'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant_qualification', 'id', '资质记录ID'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant_qualification', 'qualification_type', '客户资质类型'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant_qualification', 'review_comment', '客户资质审核意见'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant_qualification', 'status', '资质审核状态:PENDING待审核、APPROVED已通过、REJECTED已驳回'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant_qualification', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant_qualification', 'updated_at', '更新时间'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant_qualification', 'valid_from', '有效期开始日期'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant_qualification', 'valid_to', '有效期结束日期'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant_review', 'created_at', '进入审核队列时间'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant_review', 'id', '审核记录ID'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant_review', 'review_comment', '审核意见'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant_review', 'review_type', '审核类型:REGISTER注册审核等'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant_review', 'reviewed_at', '审核完成时间'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant_review', 'reviewer_id', '审核人ID'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant_review', 'status', '审核状态:PENDING待审核、APPROVED已通过、REJECTED已驳回'); +CALL queue_column_comment('baiyue_iam', 'iam_tenant_review', 'tenant_id', '被审核客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_iam', 'iam_user', 'auth_user_id', '认证服务账号逻辑ID'); +CALL queue_column_comment('baiyue_iam', 'iam_user', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_iam', 'iam_user', 'deleted', '逻辑删除标记'); +CALL queue_column_comment('baiyue_iam', 'iam_user', 'display_name', '用户展示名称'); +CALL queue_column_comment('baiyue_iam', 'iam_user', 'email', '邮箱'); +CALL queue_column_comment('baiyue_iam', 'iam_user', 'id', 'IAM用户资料ID'); +CALL queue_column_comment('baiyue_iam', 'iam_user', 'phone', '手机号'); +CALL queue_column_comment('baiyue_iam', 'iam_user', 'status', '用户资料状态:ACTIVE正常、DISABLED停用'); +CALL queue_column_comment('baiyue_iam', 'iam_user', 'updated_at', '更新时间'); +CALL queue_column_comment('baiyue_iam', 'iam_user', 'user_type', '用户类型:ENTERPRISE企业用户、PLATFORM平台用户'); +CALL queue_column_comment('baiyue_iam', 'iam_user', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_iam', 'sys_audit_outbox', 'attempt_count', '当前重试周期发送次数'); +CALL queue_column_comment('baiyue_iam', 'sys_audit_outbox', 'compensation_count', '自动补偿发送次数'); +CALL queue_column_comment('baiyue_iam', 'sys_audit_outbox', 'created_at', '记录创建时间'); +CALL queue_column_comment('baiyue_iam', 'sys_audit_outbox', 'dead_at', '进入死信时间'); +CALL queue_column_comment('baiyue_iam', 'sys_audit_outbox', 'event_id', '审计事件全局唯一ID'); +CALL queue_column_comment('baiyue_iam', 'sys_audit_outbox', 'event_type', '审计事件类型'); +CALL queue_column_comment('baiyue_iam', 'sys_audit_outbox', 'last_error', '最近一次发送错误信息'); +CALL queue_column_comment('baiyue_iam', 'sys_audit_outbox', 'lease_owner', '当前租约持有者'); +CALL queue_column_comment('baiyue_iam', 'sys_audit_outbox', 'lease_until', '发送租约截止时间'); +CALL queue_column_comment('baiyue_iam', 'sys_audit_outbox', 'next_retry_at', '下一次重试时间'); +CALL queue_column_comment('baiyue_iam', 'sys_audit_outbox', 'payload_json', '脱敏后的审计事件内容'); +CALL queue_column_comment('baiyue_iam', 'sys_audit_outbox', 'resource_id', '被审计资源ID'); +CALL queue_column_comment('baiyue_iam', 'sys_audit_outbox', 'resource_type', '被审计资源类型'); +CALL queue_column_comment('baiyue_iam', 'sys_audit_outbox', 'sent_at', '成功发送时间'); +CALL queue_column_comment('baiyue_iam', 'sys_audit_outbox', 'service_name', '来源服务名称'); +CALL queue_column_comment('baiyue_iam', 'sys_audit_outbox', 'status', '发送状态:PENDING待发送、SENDING发送中、SENT已发送、DEAD进入死信'); +CALL queue_column_comment('baiyue_iam', 'sys_audit_outbox', 'updated_at', '记录更新时间'); +CALL queue_column_comment('baiyue_order', 'ord_idempotency_record', 'completed_at', '处理完成时间'); +CALL queue_column_comment('baiyue_order', 'ord_idempotency_record', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_order', 'ord_idempotency_record', 'id', '幂等记录 ID'); +CALL queue_column_comment('baiyue_order', 'ord_idempotency_record', 'idempotency_key', '客户端幂等键'); +CALL queue_column_comment('baiyue_order', 'ord_idempotency_record', 'operation', '幂等操作类型'); +CALL queue_column_comment('baiyue_order', 'ord_idempotency_record', 'operator_id', '发起写操作的用户ID'); +CALL queue_column_comment('baiyue_order', 'ord_idempotency_record', 'request_hash', '规范化请求内容SHA-256摘要,用于识别同键不同请求'); +CALL queue_column_comment('baiyue_order', 'ord_idempotency_record', 'resource_id', '操作目标资源ID;创建前未知时为0'); +CALL queue_column_comment('baiyue_order', 'ord_idempotency_record', 'result_order_id', '成功操作生成或更新的订单ID,用于幂等重放'); +CALL queue_column_comment('baiyue_order', 'ord_idempotency_record', 'status', '处理状态:PROCESSING处理中、COMPLETED已完成'); +CALL queue_column_comment('baiyue_order', 'ord_idempotency_record', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_order', 'ord_idempotency_record', 'updated_at', '更新时间'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'amazon_warehouse_code', 'Amazon目的仓编码'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'carrier_id', '船司逻辑ID快照'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'carrier_name', '船司名称快照'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'channel_code', '运输渠道编码'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'channel_name', '下单时运输渠道名称快照;渠道主数据变化不回写'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'completed_at', '完成时间'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'container_mode', '装运方式:FULL_CONTAINER整柜、LCL散货拼箱'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'created_by', '创建人ID'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'customer_id', '客户逻辑ID'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'customer_name', '下单时客户名称快照'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'customer_remark', '客户可见备注'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'customs_inspection', '是否需要商检:0否、1是'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'deleted', '逻辑删除标记'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'destination_country_code', '目的国家二字码'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'destination_type', '目的仓类型:FBA亚马逊仓、OVERSEAS海外仓、PRIVATE私人地址'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'destination_warehouse_id', '目的仓逻辑ID'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'destination_warehouse_name', '目的仓名称快照'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'expected_ship_date', '预计发运日期'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'express_company', '履约快递公司名称快照'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'express_tracking_no', '履约快递单号'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'id', '订单ID(雪花ID)'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'inbound_code', '客户提交后生成的入仓码,草稿为空'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'order_no', '客户范围内唯一订单号'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'order_role', '订单层级:MASTER主订单、CHILD子订单'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'parent_order_id', '直属父订单ID;主订单为空;禁止子订单嵌套'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'pickup_method', '取货方式'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'pricing_tax_mode', '报价包税维度:TAX_INCLUDED或TAX_EXCLUDED;独立于tax_mode'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'receiving_time', '国内收货仓收货时间快照'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'receiving_warehouse_address', '国内收货仓完整地址快照'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'receiving_warehouse_id', '国内收货仓逻辑ID'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'receiving_warehouse_name', '下单时国内收货仓名称快照;主数据变化不回写'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'reference_id', '订单级Amazon Reference ID'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'root_order_id', '根主订单ID'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'shipment_id', '订单级Amazon Shipment ID'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'split_sequence', '同一父订单下的同级子订单顺序'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'status', '订单业务状态:DRAFT草稿、SUBMITTED已提交、APPROVED审核通过、ACCEPTED已接单、REJECTED审核驳回、PROCESSING履约中、COMPLETED已完成、CANCELLED已取消'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'submitted_at', '提交时间'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'tax_mode', '报关税务方式:BUY_DECLARATION买单、TAX_REFUND退税'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'trade_term', '贸易条款'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'transport_mode', '运输方式'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'updated_at', '更新时间'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'updated_by', '最后修改人ID'); +CALL queue_column_comment('baiyue_order', 'ord_order', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_order', 'ord_order_cargo', 'brand', '产品品牌'); +CALL queue_column_comment('baiyue_order', 'ord_order_cargo', 'cargo_name', '货物名称'); +CALL queue_column_comment('baiyue_order', 'ord_order_cargo', 'cargo_name_en', '货物英文品名'); +CALL queue_column_comment('baiyue_order', 'ord_order_cargo', 'cargo_remark', '货物备注'); +CALL queue_column_comment('baiyue_order', 'ord_order_cargo', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_order', 'ord_order_cargo', 'created_by', '创建人ID'); +CALL queue_column_comment('baiyue_order', 'ord_order_cargo', 'currency', '币种三字码'); +CALL queue_column_comment('baiyue_order', 'ord_order_cargo', 'customs_hs_code', '目的国清关HS编码'); +CALL queue_column_comment('baiyue_order', 'ord_order_cargo', 'dangerous_goods', '是否危险品'); +CALL queue_column_comment('baiyue_order', 'ord_order_cargo', 'declared_value', '申报价值'); +CALL queue_column_comment('baiyue_order', 'ord_order_cargo', 'deleted', '逻辑删除标记'); +CALL queue_column_comment('baiyue_order', 'ord_order_cargo', 'gross_weight_kg', '毛重(千克)'); +CALL queue_column_comment('baiyue_order', 'ord_order_cargo', 'height_cm', '单件高度(厘米)'); +CALL queue_column_comment('baiyue_order', 'ord_order_cargo', 'hs_code', '海关商品编码'); +CALL queue_column_comment('baiyue_order', 'ord_order_cargo', 'id', '货物明细ID'); +CALL queue_column_comment('baiyue_order', 'ord_order_cargo', 'item_no', '订单内明细序号'); +CALL queue_column_comment('baiyue_order', 'ord_order_cargo', 'length_cm', '单件长度(厘米)'); +CALL queue_column_comment('baiyue_order', 'ord_order_cargo', 'material', '产品材质'); +CALL queue_column_comment('baiyue_order', 'ord_order_cargo', 'model', '产品型号'); +CALL queue_column_comment('baiyue_order', 'ord_order_cargo', 'net_weight_kg', '货物净重,单位千克'); +CALL queue_column_comment('baiyue_order', 'ord_order_cargo', 'order_id', '所属订单ID'); +CALL queue_column_comment('baiyue_order', 'ord_order_cargo', 'package_count', '包装件数'); +CALL queue_column_comment('baiyue_order', 'ord_order_cargo', 'package_type', '包装类型'); +CALL queue_column_comment('baiyue_order', 'ord_order_cargo', 'product_height_cm', '单件产品高度,单位厘米'); +CALL queue_column_comment('baiyue_order', 'ord_order_cargo', 'product_image_file_id', 'File服务中的产品图片逻辑文件ID'); +CALL queue_column_comment('baiyue_order', 'ord_order_cargo', 'product_length_cm', '单件产品长度,单位厘米'); +CALL queue_column_comment('baiyue_order', 'ord_order_cargo', 'product_usage', '产品用途'); +CALL queue_column_comment('baiyue_order', 'ord_order_cargo', 'product_width_cm', '单件产品宽度,单位厘米'); +CALL queue_column_comment('baiyue_order', 'ord_order_cargo', 'reference_id', '本货物明细对应的Amazon Reference ID'); +CALL queue_column_comment('baiyue_order', 'ord_order_cargo', 'shipment_id', '本货物明细对应的Amazon Shipment ID'); +CALL queue_column_comment('baiyue_order', 'ord_order_cargo', 'sku_no', '货物SKU编码'); +CALL queue_column_comment('baiyue_order', 'ord_order_cargo', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_order', 'ord_order_cargo', 'total_quantity', '产品总数量'); +CALL queue_column_comment('baiyue_order', 'ord_order_cargo', 'unit_declared_value', '单件申报价值;币种取订单申报币种'); +CALL queue_column_comment('baiyue_order', 'ord_order_cargo', 'units_per_package', '每箱产品数量'); +CALL queue_column_comment('baiyue_order', 'ord_order_cargo', 'upc_no', '货物UPC条码'); +CALL queue_column_comment('baiyue_order', 'ord_order_cargo', 'updated_at', '更新时间'); +CALL queue_column_comment('baiyue_order', 'ord_order_cargo', 'updated_by', '最后修改人ID'); +CALL queue_column_comment('baiyue_order', 'ord_order_cargo', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_order', 'ord_order_cargo', 'volume_cbm', '体积(立方米)'); +CALL queue_column_comment('baiyue_order', 'ord_order_cargo', 'width_cm', '单件宽度(厘米)'); +CALL queue_column_comment('baiyue_order', 'ord_order_fulfillment_split', 'child_order_id', '本次分片生成或承接的子订单ID'); +CALL queue_column_comment('baiyue_order', 'ord_order_fulfillment_split', 'container_booking_id', 'Firstleg柜次逻辑ID'); +CALL queue_column_comment('baiyue_order', 'ord_order_fulfillment_split', 'created_at', '台账创建时间'); +CALL queue_column_comment('baiyue_order', 'ord_order_fulfillment_split', 'created_by', '创建该分片台账的用户ID'); +CALL queue_column_comment('baiyue_order', 'ord_order_fulfillment_split', 'created_by_name', '创建人名称快照'); +CALL queue_column_comment('baiyue_order', 'ord_order_fulfillment_split', 'expected_parent_version', '执行前预期父订单乐观锁版本号'); +CALL queue_column_comment('baiyue_order', 'ord_order_fulfillment_split', 'id', '订单履约分片台账ID'); +CALL queue_column_comment('baiyue_order', 'ord_order_fulfillment_split', 'idempotency_key', '客户端幂等键'); +CALL queue_column_comment('baiyue_order', 'ord_order_fulfillment_split', 'loaded_package_count', '本次已装柜分片的包装件数'); +CALL queue_column_comment('baiyue_order', 'ord_order_fulfillment_split', 'loading_plan_id', 'Firstleg装柜计划逻辑ID'); +CALL queue_column_comment('baiyue_order', 'ord_order_fulfillment_split', 'operation_id', '履约分片业务操作ID'); +CALL queue_column_comment('baiyue_order', 'ord_order_fulfillment_split', 'parent_order_id', '直属父订单ID;主订单自身作为拆分根'); +CALL queue_column_comment('baiyue_order', 'ord_order_fulfillment_split', 'remaining_package_count', '本次分片后剩余包装件数'); +CALL queue_column_comment('baiyue_order', 'ord_order_fulfillment_split', 'request_hash', '请求内容SHA-256;存量记录允许为空'); +CALL queue_column_comment('baiyue_order', 'ord_order_fulfillment_split', 'source_detail_json', '来源货物明细及数量快照;来源订单变化不回写'); +CALL queue_column_comment('baiyue_order', 'ord_order_fulfillment_split', 'source_order_id', '实际被本次分片消费的来源订单ID'); +CALL queue_column_comment('baiyue_order', 'ord_order_fulfillment_split', 'source_package_count', '来源订单本次分片前的总包装件数'); +CALL queue_column_comment('baiyue_order', 'ord_order_fulfillment_split', 'split_role', '分片角色:LOADED已装柜部分、REMAINDER剩余部分'); +CALL queue_column_comment('baiyue_order', 'ord_order_fulfillment_split', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_order', 'ord_order_fulfillment_split', 'version', '台账乐观锁版本号'); +CALL queue_column_comment('baiyue_order', 'ord_order_internal_note', 'content', '仅内部人员可见的备注内容'); +CALL queue_column_comment('baiyue_order', 'ord_order_internal_note', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_order', 'ord_order_internal_note', 'created_by', '创建人ID'); +CALL queue_column_comment('baiyue_order', 'ord_order_internal_note', 'deleted', '逻辑删除标记'); +CALL queue_column_comment('baiyue_order', 'ord_order_internal_note', 'id', '内部备注ID'); +CALL queue_column_comment('baiyue_order', 'ord_order_internal_note', 'order_id', '所属订单ID'); +CALL queue_column_comment('baiyue_order', 'ord_order_internal_note', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_order', 'ord_order_internal_note', 'updated_at', '更新时间'); +CALL queue_column_comment('baiyue_order', 'ord_order_internal_note', 'updated_by', '最后修改人ID'); +CALL queue_column_comment('baiyue_order', 'ord_order_internal_note', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_order', 'ord_order_party', 'address_line', '详细地址'); +CALL queue_column_comment('baiyue_order', 'ord_order_party', 'city', '城市'); +CALL queue_column_comment('baiyue_order', 'ord_order_party', 'company_name', '公司名称快照'); +CALL queue_column_comment('baiyue_order', 'ord_order_party', 'contact_name', '联系人快照'); +CALL queue_column_comment('baiyue_order', 'ord_order_party', 'country_code', '国家二字码'); +CALL queue_column_comment('baiyue_order', 'ord_order_party', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_order', 'ord_order_party', 'created_by', '创建人ID'); +CALL queue_column_comment('baiyue_order', 'ord_order_party', 'deleted', '逻辑删除标记'); +CALL queue_column_comment('baiyue_order', 'ord_order_party', 'district', '区县'); +CALL queue_column_comment('baiyue_order', 'ord_order_party', 'email', '联系邮箱快照'); +CALL queue_column_comment('baiyue_order', 'ord_order_party', 'id', '参与方快照ID'); +CALL queue_column_comment('baiyue_order', 'ord_order_party', 'order_id', '所属订单ID'); +CALL queue_column_comment('baiyue_order', 'ord_order_party', 'party_type', '参与方类型:SHIPPER或CONSIGNEE'); +CALL queue_column_comment('baiyue_order', 'ord_order_party', 'phone', '联系电话快照'); +CALL queue_column_comment('baiyue_order', 'ord_order_party', 'postal_code', '邮政编码'); +CALL queue_column_comment('baiyue_order', 'ord_order_party', 'province', '省/州'); +CALL queue_column_comment('baiyue_order', 'ord_order_party', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_order', 'ord_order_party', 'updated_at', '更新时间'); +CALL queue_column_comment('baiyue_order', 'ord_order_party', 'updated_by', '最后修改人ID'); +CALL queue_column_comment('baiyue_order', 'ord_order_party', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_order', 'ord_order_status_history', 'action', '触发状态变化的业务动作'); +CALL queue_column_comment('baiyue_order', 'ord_order_status_history', 'from_status', '变更前状态,首次创建可为空'); +CALL queue_column_comment('baiyue_order', 'ord_order_status_history', 'id', '状态历史ID'); +CALL queue_column_comment('baiyue_order', 'ord_order_status_history', 'occurred_at', '状态变化时间'); +CALL queue_column_comment('baiyue_order', 'ord_order_status_history', 'operator_id', '操作人ID'); +CALL queue_column_comment('baiyue_order', 'ord_order_status_history', 'operator_name', '操作人名称快照'); +CALL queue_column_comment('baiyue_order', 'ord_order_status_history', 'order_id', '所属订单ID'); +CALL queue_column_comment('baiyue_order', 'ord_order_status_history', 'reason', '变更原因'); +CALL queue_column_comment('baiyue_order', 'ord_order_status_history', 'tenant_id', '客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_order', 'ord_order_status_history', 'to_status', '变更后状态'); +CALL queue_column_comment('baiyue_order', 'ord_outbox_event', 'aggregate_id', '发生变更的聚合根ID,例如订单ID'); +CALL queue_column_comment('baiyue_order', 'ord_outbox_event', 'aggregate_type', '聚合类型,例如 ORDER'); +CALL queue_column_comment('baiyue_order', 'ord_outbox_event', 'created_at', '事件在本地事务中的创建时间'); +CALL queue_column_comment('baiyue_order', 'ord_outbox_event', 'event_type', '领域事件类型,例如 ORDER_SUBMITTED'); +CALL queue_column_comment('baiyue_order', 'ord_outbox_event', 'id', '领域事件全局ID,同时作为消费幂等键'); +CALL queue_column_comment('baiyue_order', 'ord_outbox_event', 'next_retry_at', '允许再次投递的最早时间'); +CALL queue_column_comment('baiyue_order', 'ord_outbox_event', 'payload_json', '供下游消费的事件负载快照'); +CALL queue_column_comment('baiyue_order', 'ord_outbox_event', 'publish_status', '发布状态:PENDING待发布、PUBLISHED已发布、FAILED发布失败'); +CALL queue_column_comment('baiyue_order', 'ord_outbox_event', 'published_at', '成功发布到消息中间件的时间'); +CALL queue_column_comment('baiyue_order', 'ord_outbox_event', 'retry_count', '投递失败后的累计重试次数'); +CALL queue_column_comment('baiyue_order', 'ord_outbox_event', 'tenant_id', '事件所属客户ID和数据隔离键'); +CALL queue_column_comment('baiyue_order', 'sys_audit_outbox', 'attempt_count', '当前重试周期发送次数'); +CALL queue_column_comment('baiyue_order', 'sys_audit_outbox', 'compensation_count', '自动补偿发送次数'); +CALL queue_column_comment('baiyue_order', 'sys_audit_outbox', 'created_at', '记录创建时间'); +CALL queue_column_comment('baiyue_order', 'sys_audit_outbox', 'dead_at', '进入死信时间'); +CALL queue_column_comment('baiyue_order', 'sys_audit_outbox', 'event_id', '审计事件全局唯一ID'); +CALL queue_column_comment('baiyue_order', 'sys_audit_outbox', 'event_type', '审计事件类型'); +CALL queue_column_comment('baiyue_order', 'sys_audit_outbox', 'last_error', '最近一次发送错误信息'); +CALL queue_column_comment('baiyue_order', 'sys_audit_outbox', 'lease_owner', '当前租约持有者'); +CALL queue_column_comment('baiyue_order', 'sys_audit_outbox', 'lease_until', '发送租约截止时间'); +CALL queue_column_comment('baiyue_order', 'sys_audit_outbox', 'next_retry_at', '下一次重试时间'); +CALL queue_column_comment('baiyue_order', 'sys_audit_outbox', 'payload_json', '脱敏后的审计事件内容'); +CALL queue_column_comment('baiyue_order', 'sys_audit_outbox', 'resource_id', '被审计资源ID'); +CALL queue_column_comment('baiyue_order', 'sys_audit_outbox', 'resource_type', '被审计资源类型'); +CALL queue_column_comment('baiyue_order', 'sys_audit_outbox', 'sent_at', '成功发送时间'); +CALL queue_column_comment('baiyue_order', 'sys_audit_outbox', 'service_name', '来源服务名称'); +CALL queue_column_comment('baiyue_order', 'sys_audit_outbox', 'status', '发送状态:PENDING待发送、SENDING发送中、SENT已发送、DEAD进入死信'); +CALL queue_column_comment('baiyue_order', 'sys_audit_outbox', 'updated_at', '记录更新时间'); +CALL queue_column_comment('baiyue_warehouse', 'sys_audit_outbox', 'attempt_count', '发送尝试次数'); +CALL queue_column_comment('baiyue_warehouse', 'sys_audit_outbox', 'compensation_count', '补偿处理次数'); +CALL queue_column_comment('baiyue_warehouse', 'sys_audit_outbox', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_warehouse', 'sys_audit_outbox', 'dead_at', '进入死信时间'); +CALL queue_column_comment('baiyue_warehouse', 'sys_audit_outbox', 'event_id', '审计事件全局唯一ID'); +CALL queue_column_comment('baiyue_warehouse', 'sys_audit_outbox', 'event_type', '审计事件类型'); +CALL queue_column_comment('baiyue_warehouse', 'sys_audit_outbox', 'last_error', '最近一次发送错误'); +CALL queue_column_comment('baiyue_warehouse', 'sys_audit_outbox', 'lease_owner', '当前租约持有实例'); +CALL queue_column_comment('baiyue_warehouse', 'sys_audit_outbox', 'lease_until', '当前租约截止时间'); +CALL queue_column_comment('baiyue_warehouse', 'sys_audit_outbox', 'next_retry_at', '下次允许重试时间'); +CALL queue_column_comment('baiyue_warehouse', 'sys_audit_outbox', 'payload_json', '待投递审计事件载荷'); +CALL queue_column_comment('baiyue_warehouse', 'sys_audit_outbox', 'resource_id', '被审计资源ID'); +CALL queue_column_comment('baiyue_warehouse', 'sys_audit_outbox', 'resource_type', '被审计资源类型'); +CALL queue_column_comment('baiyue_warehouse', 'sys_audit_outbox', 'sent_at', '发送成功时间'); +CALL queue_column_comment('baiyue_warehouse', 'sys_audit_outbox', 'service_name', '产生事件的服务名'); +CALL queue_column_comment('baiyue_warehouse', 'sys_audit_outbox', 'status', '投递状态:PENDING待发送、PROCESSING处理中、SENT已发送、FAILED失败'); +CALL queue_column_comment('baiyue_warehouse', 'sys_audit_outbox', 'updated_at', '更新时间'); +CALL queue_column_comment('baiyue_warehouse', 'wh_express_company', 'company_code', '平台内唯一快递公司编码'); +CALL queue_column_comment('baiyue_warehouse', 'wh_express_company', 'company_name', '快递公司名称'); +CALL queue_column_comment('baiyue_warehouse', 'wh_express_company', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_warehouse', 'wh_express_company', 'created_by', '创建人ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_express_company', 'deleted', '逻辑删除标记'); +CALL queue_column_comment('baiyue_warehouse', 'wh_express_company', 'id', '快递公司记录ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_express_company', 'remark', '备注'); +CALL queue_column_comment('baiyue_warehouse', 'wh_express_company', 'service_phone', '客服电话'); +CALL queue_column_comment('baiyue_warehouse', 'wh_express_company', 'status', '快递公司状态'); +CALL queue_column_comment('baiyue_warehouse', 'wh_express_company', 'tenant_id', '平台公共快递公司固定使用 tenant_id=0'); +CALL queue_column_comment('baiyue_warehouse', 'wh_express_company', 'tracking_url_template', '轨迹查询地址模板,可包含{trackingNo}'); +CALL queue_column_comment('baiyue_warehouse', 'wh_express_company', 'updated_at', '更新时间'); +CALL queue_column_comment('baiyue_warehouse', 'wh_express_company', 'updated_by', '最后修改人ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_express_company', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_warehouse', 'wh_express_company', 'website', '官方网站'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location', 'created_by', '创建人ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location', 'deleted', '逻辑删除标记'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location', 'id', '库位ID(雪花ID)'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location', 'location_code', '平台仓库内唯一库位编码'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location', 'location_name', '库位名称'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location', 'location_type', '库位类型,二期为PALLET'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location', 'max_weight_kg', '最大承重展示值,二期不作容量拦截'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location', 'remark', '备注'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location', 'shared_overflow', '是否为系统共享溢出位A0'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location', 'status', 'AVAILABLE、RESERVED、OCCUPIED或DISABLED'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location', 'tenant_id', '平台公共库位固定使用 tenant_id=0'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location', 'updated_at', '更新时间'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location', 'updated_by', '最后修改人ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location', 'warehouse_id', '所属仓库ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location', 'zone_code', '库区编码'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location', 'zone_name', '库区名称'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_occupancy', 'active_location_occupancy', '普通库位活动占用唯一标记'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_occupancy', 'active_pallet_occupancy', '托盘活动占用唯一标记(包含A0)'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_occupancy', 'allocated_package_count', '本托盘分配箱数'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_occupancy', 'allocated_volume_cbm', '本托盘分配体积CBM'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_occupancy', 'business_id', '来源服务入库单ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_occupancy', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_occupancy', 'created_by', '创建人或系统主体ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_occupancy', 'id', '库位占用ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_occupancy', 'inbound_code', '入仓码快照'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_occupancy', 'location_code', '库位编码快照'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_occupancy', 'location_id', '库位ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_occupancy', 'occupancy_no', '平台内唯一占用业务号'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_occupancy', 'occupancy_status', 'OCCUPIED或RELEASED'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_occupancy', 'occupied_at', '占用开始时间'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_occupancy', 'pallet_code', '托盘编码快照'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_occupancy', 'pallet_id', '托盘ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_occupancy', 'release_reason', '释放原因'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_occupancy', 'released_at', '释放时间'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_occupancy', 'reservation_id', '来源预占ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_occupancy', 'shared_overflow', '是否使用共享溢出位A0'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_occupancy', 'source_service', '来源服务'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_occupancy', 'tenant_id', '平台公共库位占用固定使用 tenant_id=0'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_occupancy', 'updated_at', '更新时间'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_occupancy', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_occupancy', 'warehouse_id', '仓库ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_reservation', 'active_pallet_reservation', '托盘活动预占唯一标记(包含A0)'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_reservation', 'active_reservation', '普通库位活动预占唯一标记'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_reservation', 'business_id', '来源服务入库单ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_reservation', 'confirmed_at', '确认占用时间'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_reservation', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_reservation', 'created_by', '创建人或系统主体ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_reservation', 'expires_at', '预占过期时间'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_reservation', 'id', '库位预占ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_reservation', 'idempotency_key', '调用方幂等键'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_reservation', 'inbound_code', '入仓码快照'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_reservation', 'location_code', '库位编码快照'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_reservation', 'location_id', '库位ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_reservation', 'pallet_code', '托盘编码快照'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_reservation', 'pallet_id', '托盘ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_reservation', 'release_reason', '释放或过期原因'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_reservation', 'released_at', '释放时间'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_reservation', 'reservation_no', '平台内唯一预占业务号'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_reservation', 'reservation_status', 'RESERVED、CONFIRMED、RELEASED或EXPIRED'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_reservation', 'shared_overflow', '是否使用共享溢出位A0'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_reservation', 'source_service', '来源服务'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_reservation', 'tenant_id', '平台公共库位预占固定使用 tenant_id=0'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_reservation', 'updated_at', '更新时间'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_reservation', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_warehouse', 'wh_location_reservation', 'warehouse_id', '仓库ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_overseas_storage_fee_rule', 'active_customer_id', '未删除规则的客户唯一索引值'); +CALL queue_column_comment('baiyue_warehouse', 'wh_overseas_storage_fee_rule', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_warehouse', 'wh_overseas_storage_fee_rule', 'created_by', '创建人ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_overseas_storage_fee_rule', 'currency', '费用币种三字码;首版为USD'); +CALL queue_column_comment('baiyue_warehouse', 'wh_overseas_storage_fee_rule', 'customer_name', '客户名称快照'); +CALL queue_column_comment('baiyue_warehouse', 'wh_overseas_storage_fee_rule', 'deleted', '逻辑删除标记:0未删除、1已删除'); +CALL queue_column_comment('baiyue_warehouse', 'wh_overseas_storage_fee_rule', 'id', '海外仓费用规则ID(雪花ID)'); +CALL queue_column_comment('baiyue_warehouse', 'wh_overseas_storage_fee_rule', 'initialization_key', '初始化或导入幂等键'); +CALL queue_column_comment('baiyue_warehouse', 'wh_overseas_storage_fee_rule', 'tenant_id', '客户ID和数据隔离键;客户费用规则不得使用0'); +CALL queue_column_comment('baiyue_warehouse', 'wh_overseas_storage_fee_rule', 'updated_at', '更新时间'); +CALL queue_column_comment('baiyue_warehouse', 'wh_overseas_storage_fee_rule', 'updated_by', '最后修改人ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_overseas_storage_fee_rule', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_warehouse', 'wh_overseas_storage_fee_tier', 'active_start_day', '未删除阶梯的起始天唯一索引值'); +CALL queue_column_comment('baiyue_warehouse', 'wh_overseas_storage_fee_tier', 'billing_unit', '计费单位;首版为CBM立方米'); +CALL queue_column_comment('baiyue_warehouse', 'wh_overseas_storage_fee_tier', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_warehouse', 'wh_overseas_storage_fee_tier', 'created_by', '创建人ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_overseas_storage_fee_tier', 'deleted', '逻辑删除标记:0未删除、1已删除'); +CALL queue_column_comment('baiyue_warehouse', 'wh_overseas_storage_fee_tier', 'end_day', '计费阶梯结束存储天数,含边界;NULL表示以上'); +CALL queue_column_comment('baiyue_warehouse', 'wh_overseas_storage_fee_tier', 'fee_type', '费用类型;首版为STORAGE仓储费'); +CALL queue_column_comment('baiyue_warehouse', 'wh_overseas_storage_fee_tier', 'id', '海外仓费用阶梯ID(雪花ID)'); +CALL queue_column_comment('baiyue_warehouse', 'wh_overseas_storage_fee_tier', 'rule_id', '所属海外仓费用规则ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_overseas_storage_fee_tier', 'sort_no', '阶梯展示和计算顺序'); +CALL queue_column_comment('baiyue_warehouse', 'wh_overseas_storage_fee_tier', 'start_day', '计费阶梯起始存储天数,含边界'); +CALL queue_column_comment('baiyue_warehouse', 'wh_overseas_storage_fee_tier', 'tenant_id', '客户ID和数据隔离键;与规则归属一致'); +CALL queue_column_comment('baiyue_warehouse', 'wh_overseas_storage_fee_tier', 'unit_price', '每计费单位单价,非负'); +CALL queue_column_comment('baiyue_warehouse', 'wh_overseas_storage_fee_tier', 'updated_at', '更新时间'); +CALL queue_column_comment('baiyue_warehouse', 'wh_overseas_storage_fee_tier', 'updated_by', '最后修改人ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_overseas_storage_fee_tier', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet', 'capacity_cbm', '托盘可用容积CBM'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet', 'color', '托盘颜色'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet', 'created_by', '创建人ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet', 'deleted', '逻辑删除标记'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet', 'id', '托盘ID(雪花ID)'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet', 'pallet_code', '平台仓库内唯一托盘编码'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet', 'pallet_name', '托盘名称'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet', 'pallet_type', '托盘类型'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet', 'remark', '备注'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet', 'status', '内部状态:AVAILABLE、RESERVED或OCCUPIED'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet', 'tenant_id', '平台公共托盘固定使用 tenant_id=0'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet', 'updated_at', '更新时间'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet', 'updated_by', '最后修改人ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet', 'warehouse_id', '所属仓库ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet', 'weight_kg', '托盘重量kg'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet_location_release_operation', 'business_id', '入库单ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet_location_release_operation', 'completed_at', '完成时间'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet_location_release_operation', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet_location_release_operation', 'id', '精确释放幂等操作ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet_location_release_operation', 'idempotency_key', '调用方幂等键'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet_location_release_operation', 'operation_status', 'PROCESSING处理中,COMPLETED已完成'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet_location_release_operation', 'request_hash', '规范化请求SHA-256摘要'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet_location_release_operation', 'response_snapshot', '完成后的精确释放结果快照,用于重复请求重放'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet_location_release_operation', 'source_service', '来源服务'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet_location_release_operation', 'tenant_id', '仓储公共资源作用域,固定为0'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet_location_settlement_operation', 'completed_at', '完成时间'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet_location_settlement_operation', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet_location_settlement_operation', 'id', 'PDA资源结算幂等操作ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet_location_settlement_operation', 'idempotency_key', 'PDA结算幂等键'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet_location_settlement_operation', 'operation_status', 'PROCESSING处理中,COMPLETED已完成'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet_location_settlement_operation', 'request_hash', '规范化请求SHA-256摘要'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet_location_settlement_operation', 'response_snapshot', '完成后的释放和转移组合结果快照,用于重复请求重放'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet_location_settlement_operation', 'source_business_id', '主入库单ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet_location_settlement_operation', 'source_service', '来源服务'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet_location_settlement_operation', 'target_business_id', '二级子入库单ID;全拣完成时为0,仅作审计快照'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet_location_settlement_operation', 'tenant_id', '仓储公共资源作用域,固定为0'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet_location_transfer_operation', 'completed_at', '完成时间'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet_location_transfer_operation', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet_location_transfer_operation', 'id', '调拨幂等操作ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet_location_transfer_operation', 'idempotency_key', '调用方幂等键'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet_location_transfer_operation', 'operation_status', 'PROCESSING处理中、COMPLETED已完成'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet_location_transfer_operation', 'request_hash', '规范化请求SHA-256摘要'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet_location_transfer_operation', 'response_snapshot', '完成后的调拨结果快照,用于重复请求重放'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet_location_transfer_operation', 'source_business_id', '来源入仓单ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet_location_transfer_operation', 'source_service', '来源服务'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet_location_transfer_operation', 'target_business_id', '目标子入仓单ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_pallet_location_transfer_operation', 'tenant_id', '仓储公共资源域,固定为0'); +CALL queue_column_comment('baiyue_warehouse', 'wh_partner', 'address_line', '详细地址'); +CALL queue_column_comment('baiyue_warehouse', 'wh_partner', 'city', '城市'); +CALL queue_column_comment('baiyue_warehouse', 'wh_partner', 'contact_email', '联系邮箱'); +CALL queue_column_comment('baiyue_warehouse', 'wh_partner', 'contact_name', '联系人姓名'); +CALL queue_column_comment('baiyue_warehouse', 'wh_partner', 'contact_phone', '联系电话'); +CALL queue_column_comment('baiyue_warehouse', 'wh_partner', 'country_code', '国家二字码'); +CALL queue_column_comment('baiyue_warehouse', 'wh_partner', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_warehouse', 'wh_partner', 'created_by', '创建人ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_partner', 'data_scope_id', '所属业务数据范围'); +CALL queue_column_comment('baiyue_warehouse', 'wh_partner', 'deleted', '逻辑删除标记:0未删除、1已删除'); +CALL queue_column_comment('baiyue_warehouse', 'wh_partner', 'district', '区县'); +CALL queue_column_comment('baiyue_warehouse', 'wh_partner', 'id', '往来单位ID(雪花ID)'); +CALL queue_column_comment('baiyue_warehouse', 'wh_partner', 'partner_code', '平台内唯一往来单位编码'); +CALL queue_column_comment('baiyue_warehouse', 'wh_partner', 'partner_name', '往来单位名称'); +CALL queue_column_comment('baiyue_warehouse', 'wh_partner', 'partner_type', 'CUSTOMER、SUPPLIER或BOTH'); +CALL queue_column_comment('baiyue_warehouse', 'wh_partner', 'postal_code', '邮政编码'); +CALL queue_column_comment('baiyue_warehouse', 'wh_partner', 'province', '省或州'); +CALL queue_column_comment('baiyue_warehouse', 'wh_partner', 'remark', '备注'); +CALL queue_column_comment('baiyue_warehouse', 'wh_partner', 'status', '状态:ACTIVE启用、DISABLED停用'); +CALL queue_column_comment('baiyue_warehouse', 'wh_partner', 'tenant_id', '平台公共往来单位固定使用 tenant_id=0'); +CALL queue_column_comment('baiyue_warehouse', 'wh_partner', 'updated_at', '更新时间'); +CALL queue_column_comment('baiyue_warehouse', 'wh_partner', 'updated_by', '最后修改人ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_partner', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_warehouse', 'wh_partner_type', 'data_scope_id', '所属业务数据范围,平台公共数据为0'); +CALL queue_column_comment('baiyue_warehouse', 'wh_partner_type', 'partner_id', '往来单位ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_partner_type', 'partner_type', '稳定业务类型:CUSTOMER客户、SHIPPER发货方等'); +CALL queue_column_comment('baiyue_warehouse', 'wh_partner_type', 'tenant_id', '平台公共往来单位固定使用 tenant_id=0'); +CALL queue_column_comment('baiyue_warehouse', 'wh_service_capability', 'capability_code', '能力编码:RECEIVING、STORAGE、PICK_PACK、RETURN等'); +CALL queue_column_comment('baiyue_warehouse', 'wh_service_capability', 'capability_name', '能力名称'); +CALL queue_column_comment('baiyue_warehouse', 'wh_service_capability', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_warehouse', 'wh_service_capability', 'created_by', '创建人ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_service_capability', 'cutoff_time', '当日作业截单时间'); +CALL queue_column_comment('baiyue_warehouse', 'wh_service_capability', 'deleted', '逻辑删除标记'); +CALL queue_column_comment('baiyue_warehouse', 'wh_service_capability', 'description', '能力说明'); +CALL queue_column_comment('baiyue_warehouse', 'wh_service_capability', 'enabled', '是否启用'); +CALL queue_column_comment('baiyue_warehouse', 'wh_service_capability', 'id', '服务能力ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_service_capability', 'tenant_id', '平台公共仓库服务能力固定使用 tenant_id=0'); +CALL queue_column_comment('baiyue_warehouse', 'wh_service_capability', 'updated_at', '更新时间'); +CALL queue_column_comment('baiyue_warehouse', 'wh_service_capability', 'updated_by', '最后修改人ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_service_capability', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_warehouse', 'wh_service_capability', 'warehouse_id', '所属仓库ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse', 'contact_email', '联系邮箱'); +CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse', 'contact_name', '仓库联系人'); +CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse', 'contact_phone', '联系电话'); +CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse', 'country_code', '所在国家二字码'); +CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse', 'created_by', '创建人ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse', 'deleted', '逻辑删除标记'); +CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse', 'id', '仓库ID(雪花ID)'); +CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse', 'remark', '仓库备注'); +CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse', 'status', '仓库状态'); +CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse', 'tenant_id', '平台公共仓库固定使用 tenant_id=0'); +CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse', 'timezone', '仓库所在地UTC偏移量'); +CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse', 'updated_at', '更新时间'); +CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse', 'updated_by', '最后修改人ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse', 'warehouse_code', '平台内唯一仓库编码'); +CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse', 'warehouse_name', '仓库名称'); +CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse', 'warehouse_type', '公共仓库类型:DOMESTIC、FBA、OVERSEAS'); +CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse_address', 'address_line1', '主地址行'); +CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse_address', 'address_line2', '补充地址行'); +CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse_address', 'address_type', '地址类型,例如OPERATING或RETURN'); +CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse_address', 'city', '城市'); +CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse_address', 'country_code', '国家二字码'); +CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse_address', 'created_at', '创建时间'); +CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse_address', 'created_by', '创建人ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse_address', 'deleted', '逻辑删除标记'); +CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse_address', 'district', '区县'); +CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse_address', 'id', '仓库地址ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse_address', 'latitude', '纬度'); +CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse_address', 'longitude', '经度'); +CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse_address', 'postal_code', '邮政编码'); +CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse_address', 'state_province', '省或州'); +CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse_address', 'tenant_id', '平台公共仓库地址固定使用 tenant_id=0'); +CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse_address', 'updated_at', '更新时间'); +CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse_address', 'updated_by', '最后修改人ID'); +CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse_address', 'version', '乐观锁版本号'); +CALL queue_column_comment('baiyue_warehouse', 'wh_warehouse_address', 'warehouse_id', '所属仓库ID'); +-- END GENERATED NINE-SCHEMA COMMENT UPGRADE CALLS +CALL apply_queued_comments(); +DROP PROCEDURE IF EXISTS apply_queued_comments; +DROP PROCEDURE IF EXISTS apply_one_table_comments; +DROP PROCEDURE IF EXISTS queue_column_comment; +DROP PROCEDURE IF EXISTS queue_table_comment; +DROP TEMPORARY TABLE IF EXISTS tmp_column_comment_target; +DROP TEMPORARY TABLE IF EXISTS tmp_table_comment_target; -- 三期统一最终总断言:稳定箱号、单箱测量、墨家收件箱、客户尺寸和装柜权限。 USE baiyue_firstleg; @@ -10230,6 +13368,32 @@ DROP PROCEDURE IF EXISTS assert_unified_final_schema; DELIMITER $$ CREATE PROCEDURE assert_unified_final_schema() BEGIN + IF (SELECT COUNT(*) FROM information_schema.schemata + WHERE schema_name IN ('baiyue_auth','baiyue_iam','baiyue_order','baiyue_warehouse', + 'baiyue_firstleg','baiyue_file','baiyue_audit','baiyue_event_bridge','baiyue_finance'))<>9 THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '统一迁移失败:九个业务数据库未完整创建'; + END IF; + IF EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_schema IN ('baiyue_auth','baiyue_iam','baiyue_order','baiyue_warehouse', + 'baiyue_firstleg','baiyue_file','baiyue_audit','baiyue_event_bridge','baiyue_finance') + AND table_type='BASE TABLE' + AND (TRIM(COALESCE(table_comment,''))='' OR table_comment NOT REGEXP '[一-龥]') + ) THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '统一迁移失败:存在缺失中文注释的业务表'; + END IF; + IF EXISTS ( + SELECT 1 FROM information_schema.columns column_info + JOIN information_schema.tables table_info + ON table_info.table_schema=column_info.table_schema + AND table_info.table_name=column_info.table_name AND table_info.table_type='BASE TABLE' + WHERE column_info.table_schema IN ('baiyue_auth','baiyue_iam','baiyue_order','baiyue_warehouse', + 'baiyue_firstleg','baiyue_file','baiyue_audit','baiyue_event_bridge','baiyue_finance') + AND (TRIM(COALESCE(column_info.column_comment,''))='' + OR column_info.column_comment NOT REGEXP '[一-龥]') + ) THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '统一迁移失败:存在缺失中文注释的业务字段'; + END IF; IF (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'baiyue_finance' AND table_name IN ('fin_fee_source', 'fin_inbox_event', 'fin_subject', diff --git a/event-bridge-service/src/main/resources/db/event-bridge.sql b/event-bridge-service/src/main/resources/db/event-bridge.sql index 3fd4368b7..a7a52b539 100644 --- a/event-bridge-service/src/main/resources/db/event-bridge.sql +++ b/event-bridge-service/src/main/resources/db/event-bridge.sql @@ -8,7 +8,7 @@ CREATE TABLE IF NOT EXISTS bridge_outbox ( event_version INT NOT NULL COMMENT '事件契约版本', source_region VARCHAR(32) NOT NULL COMMENT '来源区域', target_region VARCHAR(32) NOT NULL COMMENT '目标区域', - tenant_id VARCHAR(128) NOT NULL COMMENT '租户标识', + tenant_id VARCHAR(128) NOT NULL COMMENT '客户或业务隔离标识;跨区域事件沿用来源系统口径', aggregate_type VARCHAR(64) NOT NULL COMMENT '聚合类型', aggregate_id VARCHAR(128) NOT NULL COMMENT '聚合标识', aggregate_version BIGINT NOT NULL COMMENT '聚合版本', @@ -39,7 +39,7 @@ CREATE TABLE IF NOT EXISTS bridge_inbox ( event_version INT NOT NULL COMMENT '事件契约版本', source_region VARCHAR(32) NOT NULL COMMENT '来源区域', target_region VARCHAR(32) NOT NULL COMMENT '目标区域', - tenant_id VARCHAR(128) NOT NULL COMMENT '租户标识', + tenant_id VARCHAR(128) NOT NULL COMMENT '客户或业务隔离标识;跨区域事件沿用来源系统口径', aggregate_type VARCHAR(64) NOT NULL COMMENT '聚合类型', aggregate_id VARCHAR(128) NOT NULL COMMENT '聚合标识', aggregate_version BIGINT NOT NULL COMMENT '聚合版本', diff --git a/file/src/main/resources/db/file.sql b/file/src/main/resources/db/file.sql index 1934219a3..d9fd2da3b 100644 --- a/file/src/main/resources/db/file.sql +++ b/file/src/main/resources/db/file.sql @@ -5,7 +5,7 @@ USE baiyue_file; CREATE TABLE IF NOT EXISTS file_object ( id BIGINT UNSIGNED NOT NULL COMMENT '文件ID(雪花ID)', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', business_type VARCHAR(32) NULL COMMENT '关联业务类型', business_id VARCHAR(64) NULL COMMENT '关联业务逻辑ID', original_name VARCHAR(255) NOT NULL COMMENT '原始文件名', @@ -30,7 +30,7 @@ CREATE TABLE IF NOT EXISTS file_object ( CREATE TABLE IF NOT EXISTS file_download_token ( id BIGINT UNSIGNED NOT NULL COMMENT '下载令牌ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', file_id BIGINT UNSIGNED NOT NULL COMMENT '文件逻辑ID', token_hash CHAR(64) NOT NULL COMMENT '下载令牌SHA-256摘要', expires_at DATETIME(3) NOT NULL COMMENT '过期时间', @@ -47,7 +47,7 @@ CREATE TABLE IF NOT EXISTS file_download_token ( CREATE TABLE IF NOT EXISTS file_download_log ( id BIGINT UNSIGNED NOT NULL COMMENT '下载日志ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', file_id BIGINT UNSIGNED NOT NULL COMMENT '文件逻辑ID', user_id BIGINT UNSIGNED NULL COMMENT '下载用户ID', request_id VARCHAR(64) NULL COMMENT '请求ID', @@ -62,14 +62,14 @@ CREATE TABLE IF NOT EXISTS file_download_log ( CREATE TABLE IF NOT EXISTS file_export_task ( id BIGINT UNSIGNED NOT NULL COMMENT '导出任务ID(雪花ID)', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '申请人所属租户ID,平台任务为0', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键;平台任务固定为0', app_code VARCHAR(24) NOT NULL COMMENT '申请端:PLATFORM或MERCHANT', task_no VARCHAR(40) NOT NULL COMMENT '导出任务编号', export_type VARCHAR(64) NOT NULL COMMENT '导出类型', format VARCHAR(16) NOT NULL DEFAULT 'XLSX' COMMENT '导出格式', task_name VARCHAR(128) NOT NULL COMMENT '任务名称', parameters_json JSON NOT NULL COMMENT '筛选条件快照', - status VARCHAR(24) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/RUNNING/SUCCEEDED/FAILED/EXPIRED', + status VARCHAR(24) NOT NULL DEFAULT 'PENDING' COMMENT '状态:PENDING待处理、RUNNING执行中、SUCCEEDED成功、FAILED失败、EXPIRED已过期', total_rows BIGINT UNSIGNED NULL COMMENT '预计总行数', processed_rows BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '已处理行数', file_name VARCHAR(255) NULL COMMENT '下载文件名', @@ -108,7 +108,7 @@ CREATE TABLE IF NOT EXISTS file_export_task ( CREATE TABLE IF NOT EXISTS file_export_task_guard ( guard_key VARCHAR(64) NOT NULL COMMENT '任务创建串行化锁键', - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '锁记录最后更新时间', PRIMARY KEY (guard_key) ) ENGINE=InnoDB COMMENT='导出任务创建并发保护表:低频创建入口使用单行锁保证重复提交及活动上限'; @@ -123,15 +123,15 @@ CREATE TABLE IF NOT EXISTS file_export_outbox ( tag VARCHAR(64) NOT NULL COMMENT '消息Tag', message_key VARCHAR(64) NOT NULL COMMENT '消息Key', payload_json JSON NOT NULL COMMENT '命令消息体', - status VARCHAR(24) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/CLAIMED/SENT/QUARANTINED', + status VARCHAR(24) NOT NULL DEFAULT 'PENDING' COMMENT '状态:PENDING待发送、CLAIMED已领取、SENT已发送、QUARANTINED隔离', attempt_count INT NOT NULL DEFAULT 0 COMMENT '累计发送尝试次数', next_retry_at DATETIME(3) NOT NULL COMMENT '允许发送的最早时间', lease_owner VARCHAR(128) NULL COMMENT '领取实例标识', lease_until DATETIME(3) NULL COMMENT '领取租约截止时间', sent_at DATETIME(3) NULL COMMENT '发送成功时间', last_error VARCHAR(1000) NULL COMMENT '最后发送错误', - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 'Outbox记录创建时间', + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT 'Outbox记录更新时间', PRIMARY KEY (id), UNIQUE KEY uk_file_export_outbox_event (event_id), UNIQUE KEY uk_file_export_outbox_task_version (task_id, dispatch_version), diff --git a/finance/AGENTS.md b/finance/AGENTS.md new file mode 100644 index 000000000..a8a355241 --- /dev/null +++ b/finance/AGENTS.md @@ -0,0 +1,36 @@ +# Finance Service Instructions + +## 模块定位 + +- 本模块是独立财务服务,拥有客户结算条款、财务费用来源、CNY 账单与明细、收款及分配、成本及分摊、账单修订、幂等记录和财务审计状态。 +- 产品范围先读 `../../docs/财务管理/README.md` 中已确认的范围、对象、状态和接口约束;任务进度、完成比例、临时阻断和验收结论不写入本文件,也不作为业务契约。 +- Firstleg 拥有入库/柜次业务费用和履约事实,File 拥有凭证文件;Finance 通过内部接口读取或消费快照,不跨库查询,也不复制上游业务状态机。 + +## 快速查询 + +| 目标 | 首选入口 | +|---|---| +| 平台账户、费用池、账单、收款与成本 API | `controller/PlatformFinanceController.java`,统一前缀 `/api/platform/finance/v1` | +| 客户只读账单、收款与应收 API | `controller/ClientFinanceController.java`,统一前缀 `/api/client/finance/v1` | +| Firstleg 费用来源事件入口 | `controller/InternalFinanceFeeSourceController.java`、`service/FinanceFeeSourceEventService.java` | +| 查询模型与客户可见性 | `service/FinanceQueryService.java`、`dto/FinanceReadModels.java` | +| 账期、账单、收款、成本及幂等写模型 | `service/FinanceService.java` | +| Firstleg 费用来源与柜次成本快照 | `client/FirstlegFeeSourceClient.java`、`client/FirstlegCostSnapshotClient.java`;生产端见 `../firstleg/AGENTS.md` | +| 收款/成本凭证 | `client/FinanceFileClient.java`;文件归属与下载规则见 `../file/AGENTS.md` | +| 异常与响应包装 | `controller/FinanceExceptionHandler.java`、`controller/FinanceResponses.java` | +| 数据库 | `src/main/resources/db/finance.sql`;存量/测试环境统一入口为 `../deploy/sql/头程数据库统一增量-MySQL8.sql` | +| 配置与网关 | `src/main/resources/application.yml`、`../config/nacos/{local,cn}/finance-service.yml`、`../gateway/src/main/resources/application.yml` | +| 测试 | `src/test/java/com/baiyue/finance/`;执行 `mvn -pl finance -am test` | + +## 契约与边界 + +- 平台写操作必须校验权限、目标客户、幂等键、请求哈希、乐观锁版本和审计;客户接口只能从登录态取得客户范围,不接受客户端指定客户 ID。 +- 出账后的业务来源和账单不得原地覆盖或物理删除;调整、作废、收款分配和修订必须保留可追溯记录。金额使用定点小数,当前业务币种固定为 CNY,长整型 ID 跨前端按字符串传输。 +- 费用入池只接受满足上游发布与结算条件的不可变来源版本;来源 ID、版本、客户归属、方向或金额变化时同时核对事件消费、主动快照查询、唯一约束和账单明细消费者。 +- 整柜成本分摊使用 Firstleg 返回的真实客户归属与订单体积快照;不得信任前端提交的订单归属或体积。凭证必须由 File 元数据校验客户和业务类型,依赖不可用时不得静默跳过。 +- 修改平台或客户 DTO 时同步检查 Admin `../../apps/admin-web/src/modules/finance/`、Client `../../apps/client-web/src/modules/finance/`、Gateway 路由、权限码和契约测试;客户响应不得暴露供应商成本、内部备注、平台凭证或其他客户数据。 + +## 验证 + +- 文档或索引变更只做路径核对和 `git diff --check`;代码变更至少运行 `mvn -pl finance -am test`。 +- 跨服务契约变更同时运行 Finance 与直接生产者/消费者测试。未经用户明确要求,不在本机启动 Java、Nacos 或数据库。 diff --git a/finance/src/main/java/com/baiyue/finance/client/FirstlegFeeSourceClient.java b/finance/src/main/java/com/baiyue/finance/client/FirstlegFeeSourceClient.java new file mode 100644 index 000000000..3178f4b7e --- /dev/null +++ b/finance/src/main/java/com/baiyue/finance/client/FirstlegFeeSourceClient.java @@ -0,0 +1,34 @@ +package com.baiyue.finance.client; + +import com.baiyue.common.core.api.ApiResponse; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestHeader; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +@FeignClient(name = "firstleg-service", contextId = "financeFirstlegFeeSourceClient", + url = "${baiyue.clients.firstleg.url:}", path = "/internal/firstleg/v1") +public interface FirstlegFeeSourceClient { + @GetMapping("/inbounds/{inboundId}/fees/{feeId}/source-snapshot") + ApiResponse inbound(@RequestHeader("X-Internal-Service-Token") String token, + @PathVariable long inboundId, @PathVariable long feeId); + + @GetMapping("/containers/{containerId}/fees/{feeId}/source-snapshot") + ApiResponse container(@RequestHeader("X-Internal-Service-Token") String token, + @PathVariable long containerId, @PathVariable long feeId); + + record Snapshot(long tenantId, String sourceType, long feeId, int feeVersion, + Long inboundId, String inboundCode, Long orderId, Long containerId, + Long containerBookingId, String containerNo, long customerId, + String customerName, Long feeItemId, String feeItemCode, + String feeItemName, String direction, LocalDate businessDate, + String currency, BigDecimal totalAmount, String reviewStatus, + String publicationStatus, boolean settlementIncluded, + LocalDateTime publishedAt, BigDecimal chargeableQuantity, + String chargeUnit, BigDecimal actualGrossWeightKg, + BigDecimal actualVolumeCbm, String sourceHash, int snapshotVersion) {} +} diff --git a/finance/src/main/java/com/baiyue/finance/controller/ClientFinanceController.java b/finance/src/main/java/com/baiyue/finance/controller/ClientFinanceController.java index 555ae39ac..a98216343 100644 --- a/finance/src/main/java/com/baiyue/finance/controller/ClientFinanceController.java +++ b/finance/src/main/java/com/baiyue/finance/controller/ClientFinanceController.java @@ -3,33 +3,52 @@ package com.baiyue.finance.controller; import com.baiyue.common.core.api.ApiResponse; import com.baiyue.common.security.AuthenticatedUser; import com.baiyue.finance.service.FinanceService; +import com.baiyue.finance.service.FinanceQueryService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; -import java.util.Map; - @Tag(name = "客户财务只读") @RestController @RequestMapping("/api/client/finance/v1") public class ClientFinanceController { private final FinanceService service; - public ClientFinanceController(FinanceService service) { this.service = service; } + private final FinanceQueryService queryService; + public ClientFinanceController(FinanceService service, FinanceQueryService queryService) { + this.service = service; + this.queryService = queryService; + } @GetMapping("/bills") @PreAuthorize("hasAuthority('merchant:finance:statement:list')") @Operation(summary = "客户只读查询账单") - public ApiResponse bills(@AuthenticationPrincipal AuthenticatedUser user) { return ApiResponse.success(service.listClientBills(user.tenantId())); } + public ApiResponse bills(@AuthenticationPrincipal AuthenticatedUser user) { return FinanceResponses.success(queryService.statements(user.tenantId(), true)); } @GetMapping("/bills/{billId}") @PreAuthorize("hasAuthority('merchant:finance:statement:view')") @Operation(summary = "客户只读查看账单详情") - public ApiResponse> bill(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable long billId) { return ApiResponse.success(service.clientBill(user.tenantId(), billId)); } + public ApiResponse bill(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable long billId) { return FinanceResponses.success(queryService.statement(user.tenantId(), billId, true)); } @GetMapping("/receipts") @PreAuthorize("hasAuthority('merchant:finance:statement:view')") @Operation(summary = "客户只读查询收款记录") - public ApiResponse receipts(@AuthenticationPrincipal AuthenticatedUser user) { return ApiResponse.success(service.listReceipts(user.tenantId())); } + public ApiResponse receipts(@AuthenticationPrincipal AuthenticatedUser user) { + return FinanceResponses.success(queryService.clientReceipts(user.tenantId())); + } + + @GetMapping("/statements") + @PreAuthorize("hasAuthority('merchant:finance:statement:list')") + @Operation(summary = "客户只读查询对账单") + public ApiResponse statements(@AuthenticationPrincipal AuthenticatedUser user) { + return FinanceResponses.success(queryService.statements(user.tenantId(), true)); + } + + @GetMapping("/receivable-settlements") + @PreAuthorize("hasAuthority('merchant:finance:statement:list')") + @Operation(summary = "客户只读查询结算应收") + public ApiResponse receivableSettlements(@AuthenticationPrincipal AuthenticatedUser user) { + return FinanceResponses.success(queryService.receivableSettlements(user.tenantId(), true)); + } } diff --git a/finance/src/main/java/com/baiyue/finance/controller/FinanceExceptionHandler.java b/finance/src/main/java/com/baiyue/finance/controller/FinanceExceptionHandler.java new file mode 100644 index 000000000..732174ecf --- /dev/null +++ b/finance/src/main/java/com/baiyue/finance/controller/FinanceExceptionHandler.java @@ -0,0 +1,60 @@ +package com.baiyue.finance.controller; + +import com.baiyue.common.core.api.ApiResponse; +import com.baiyue.common.core.api.ResultCode; +import com.baiyue.common.core.exception.BizException; +import com.baiyue.finance.service.FinanceEventException; +import org.slf4j.MDC; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.time.Instant; +import java.util.UUID; + +/** Finance-owned error codes stay stable even when the common result catalog evolves. */ +@Order(Ordered.HIGHEST_PRECEDENCE) +@RestControllerAdvice(basePackages = "com.baiyue.finance.controller") +public class FinanceExceptionHandler { + @ExceptionHandler(FinanceEventException.class) + public ResponseEntity> event(FinanceEventException error) { + return ResponseEntity.ok(response(error.code(), error.getMessage())); + } + @ExceptionHandler(BizException.class) + public ResponseEntity> business(BizException error) { + ResultCode result = error.getResultCode(); + HttpStatus status = result == ResultCode.SERVICE_UNAVAILABLE ? HttpStatus.SERVICE_UNAVAILABLE : HttpStatus.OK; + return ResponseEntity.status(status).body(response(financeCode(result), error.getMessage())); + } + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity> validation(IllegalArgumentException error) { + return ResponseEntity.ok(response("FIN_400", error.getMessage())); + } + + private static String financeCode(ResultCode result) { + return switch (result) { + case NOT_FOUND -> "FIN_404"; + case CONFLICT -> "FIN_409"; + case SERVICE_UNAVAILABLE -> "FIN_503"; + case VALIDATION_FAILED, BAD_REQUEST -> "FIN_400"; + default -> "FIN_001"; + }; + } + + private static ApiResponse response(String code, String message) { + String requestId = first(MDC.get("requestId"), MDC.get("request_id")); + if (!StringUtils.hasText(requestId)) requestId = UUID.randomUUID().toString(); + String traceId = first(MDC.get("traceId"), MDC.get("trace_id")); + if (!StringUtils.hasText(traceId)) traceId = requestId; + return new ApiResponse<>(code, message, null, requestId, traceId, Instant.now()); + } + + private static String first(String first, String second) { + return StringUtils.hasText(first) ? first : second; + } +} diff --git a/finance/src/main/java/com/baiyue/finance/controller/FinanceResponses.java b/finance/src/main/java/com/baiyue/finance/controller/FinanceResponses.java new file mode 100644 index 000000000..9a1ae5941 --- /dev/null +++ b/finance/src/main/java/com/baiyue/finance/controller/FinanceResponses.java @@ -0,0 +1,23 @@ +package com.baiyue.finance.controller; + +import com.baiyue.common.core.api.ApiResponse; +import org.slf4j.MDC; +import org.springframework.util.StringUtils; + +import java.util.UUID; + +final class FinanceResponses { + private FinanceResponses() {} + + static ApiResponse success(T data) { + String requestId = first(MDC.get("requestId"), MDC.get("request_id")); + if (!StringUtils.hasText(requestId)) requestId = UUID.randomUUID().toString(); + String traceId = first(MDC.get("traceId"), MDC.get("trace_id")); + if (!StringUtils.hasText(traceId)) traceId = requestId; + return ApiResponse.success(data, requestId, traceId); + } + + private static String first(String first, String second) { + return StringUtils.hasText(first) ? first : second; + } +} diff --git a/finance/src/main/java/com/baiyue/finance/controller/InternalFinanceFeeSourceController.java b/finance/src/main/java/com/baiyue/finance/controller/InternalFinanceFeeSourceController.java new file mode 100644 index 000000000..8ff92e919 --- /dev/null +++ b/finance/src/main/java/com/baiyue/finance/controller/InternalFinanceFeeSourceController.java @@ -0,0 +1,36 @@ +package com.baiyue.finance.controller; + +import com.baiyue.common.core.api.ApiResponse; +import com.baiyue.finance.dto.FinanceFeeSourceEvent; +import com.baiyue.finance.service.FinanceFeeSourceEventService; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.Map; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/internal/finance/v1/fee-source-events") +public class InternalFinanceFeeSourceController { + private final FinanceFeeSourceEventService service; + @Value("${baiyue.security.internal-service-token}") + private String expectedToken; + + @PostMapping + public ApiResponse> consume( + @RequestHeader(value = "X-Internal-Service-Token", required = false) String token, + @RequestBody FinanceFeeSourceEvent event) { + if (token == null || expectedToken == null || expectedToken.isBlank() + || !MessageDigest.isEqual(token.getBytes(StandardCharsets.UTF_8), expectedToken.getBytes(StandardCharsets.UTF_8))) { + throw new com.baiyue.finance.service.FinanceEventException("FIN-EVT-401", "内部服务令牌无效"); + } + return FinanceResponses.success(Map.of("feeSourceId", service.consume(event))); + } +} diff --git a/finance/src/main/java/com/baiyue/finance/controller/PlatformFinanceController.java b/finance/src/main/java/com/baiyue/finance/controller/PlatformFinanceController.java index b253ff4e2..01376191c 100644 --- a/finance/src/main/java/com/baiyue/finance/controller/PlatformFinanceController.java +++ b/finance/src/main/java/com/baiyue/finance/controller/PlatformFinanceController.java @@ -4,6 +4,7 @@ import com.baiyue.common.audit.BusinessAudit; import com.baiyue.common.core.api.ApiResponse; import com.baiyue.common.security.AuthenticatedUser; import com.baiyue.finance.service.FinanceService; +import com.baiyue.finance.service.FinanceQueryService; import com.baiyue.finance.client.FinanceFileClient; import com.baiyue.finance.client.FirstlegCostSnapshotClient; import io.swagger.v3.oas.annotations.Operation; @@ -16,6 +17,7 @@ import org.springframework.web.bind.annotation.*; import java.util.Map; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.multipart.MultipartFile; +import org.springframework.beans.factory.annotation.Autowired; @Tag(name = "人工财务台账") @RestController @@ -24,8 +26,58 @@ public class PlatformFinanceController { private final FinanceService service; private final FinanceFileClient fileClient; private final FirstlegCostSnapshotClient costSnapshotClient; + private final FinanceQueryService queryService; @Value("${baiyue.security.internal-service-token:}") private String fileToken; - public PlatformFinanceController(FinanceService service, FinanceFileClient fileClient, FirstlegCostSnapshotClient costSnapshotClient) { this.service = service; this.fileClient = fileClient; this.costSnapshotClient = costSnapshotClient; } + @Autowired + public PlatformFinanceController(FinanceService service, FinanceFileClient fileClient, + FirstlegCostSnapshotClient costSnapshotClient, FinanceQueryService queryService) { + this.service = service; + this.fileClient = fileClient; + this.costSnapshotClient = costSnapshotClient; + this.queryService = queryService; + } + + /** Kept for direct unit tests of write behavior. */ + public PlatformFinanceController(FinanceService service, FinanceFileClient fileClient, + FirstlegCostSnapshotClient costSnapshotClient) { + this(service, fileClient, costSnapshotClient, null); + } + + @GetMapping("/customer-accounts/{customerId}") + @PreAuthorize("hasAuthority('platform:finance:bill:view')") + @Operation(summary = "客户人工应收账户及流水") + public ApiResponse customerAccount(@PathVariable long customerId) { + return FinanceResponses.success(queryService.customerAccount(customerId)); + } + + @GetMapping("/fee-pool") + @PreAuthorize("hasAuthority('platform:finance:bill:view')") + @Operation(summary = "客户应收应付费用池") + public ApiResponse feePool(@RequestParam long customerId, + @RequestParam(required = false) String direction) { + return FinanceResponses.success(queryService.feePool(customerId, direction)); + } + + @GetMapping("/statements") + @PreAuthorize("hasAuthority('platform:finance:bill:view')") + @Operation(summary = "客户对账单") + public ApiResponse statements(@RequestParam long customerId) { + return FinanceResponses.success(queryService.statements(customerId, false)); + } + + @GetMapping("/receivable-settlements") + @PreAuthorize("hasAuthority('platform:finance:bill:view')") + @Operation(summary = "结算单应收") + public ApiResponse receivableSettlements(@RequestParam long customerId) { + return FinanceResponses.success(queryService.receivableSettlements(customerId, false)); + } + + @GetMapping("/payable-settlements") + @PreAuthorize("hasAuthority('platform:finance:cost:view')") + @Operation(summary = "结算单应付") + public ApiResponse payableSettlements(@RequestParam long customerId) { + return FinanceResponses.success(queryService.payableSettlements(customerId)); + } @PostMapping(value = "/vouchers", consumes = "multipart/form-data") @PreAuthorize("hasAnyAuthority('platform:finance:receipt:create','platform:finance:cost:create')") @@ -33,60 +85,65 @@ public class PlatformFinanceController { @RequestParam long customerId, @RequestPart("file") MultipartFile file) { if (customerId <= 0) throw new IllegalArgumentException("客户范围无效"); - return fileClient.upload(fileToken, user.userId(), customerId, customerId, file); + var response = fileClient.upload(fileToken, user.userId(), customerId, customerId, file); + if (response == null || response.data() == null) throw new IllegalStateException("File服务未返回有效上传结果"); + return FinanceResponses.success(response.data()); } @GetMapping("/customers/{tenantId}/terms") @PreAuthorize("hasAuthority('platform:finance:customer-terms:view')") @Operation(summary = "查询客户账期") - public ApiResponse> terms(@PathVariable long tenantId) { return ApiResponse.success(service.customerTerms(tenantId)); } + public ApiResponse terms(@PathVariable long tenantId) { return FinanceResponses.success(termsView(tenantId, service.customerTerms(tenantId))); } @GetMapping("/bills") @PreAuthorize("hasAuthority('platform:finance:bill:view')") @Operation(summary = "查询平台账单") - public ApiResponse bills(@RequestParam long customerId) { return ApiResponse.success(service.listBills(customerId)); } + public ApiResponse bills(@RequestParam long customerId) { return FinanceResponses.success(queryService.statements(customerId, false)); } @PutMapping("/customers/{tenantId}/terms") @PreAuthorize("hasAuthority('platform:finance:customer-terms:update')") @BusinessAudit(event = "FINANCE_CUSTOMER_TERMS_UPDATE", resourceType = "CUSTOMER") - public ApiResponse> saveTerms(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable long tenantId, @RequestHeader("Idempotency-Key") String key, @RequestBody TermsRequest request) { - return ApiResponse.success(service.saveCustomerTerms(tenantId, user.userId(), request.customerName(), request.creditDays(), key)); + public ApiResponse saveTerms(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable long tenantId, @RequestHeader("Idempotency-Key") String key, @RequestBody TermsRequest request) { + return FinanceResponses.success(termsView(tenantId, service.saveCustomerTerms(tenantId, user.userId(), request.customerName(), request.creditDays(), key))); } @PostMapping("/bills") @PreAuthorize("hasAuthority('platform:finance:bill:create')") @BusinessAudit(event = "FINANCE_BILL_CREATE", resourceType = "FINANCE_BILL") - public ApiResponse> createBill(@AuthenticationPrincipal AuthenticatedUser user, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody CreateBillRequest request) { + public ApiResponse createBill(@AuthenticationPrincipal AuthenticatedUser user, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody CreateBillRequest request) { var lines = request.lines().stream().map(line -> new FinanceService.BillLine("FIRSTLEG_FEE", line.sourceFeeId(), line.sourceFeeVersion(), null, line.orderId(), line.inboundId(), "MANUAL", line.feeItemName(), line.amount())).toList(); var internal = new FinanceService.BillRequest(null, 1L, request.billDate(), null, lines); - return ApiResponse.success(service.createBill(request.customerId(), user.userId(), "客户" + request.customerId(), internal, key)); + return FinanceResponses.success(resourceView(service.createBill(request.customerId(), user.userId(), "客户" + request.customerId(), internal, key))); } @PostMapping("/bills/{billId}/issue") @PreAuthorize("hasAuthority('platform:finance:bill:issue')") @BusinessAudit(event = "FINANCE_BILL_ISSUE", resourceType = "FINANCE_BILL") - public ApiResponse> issueBill(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable long billId, @RequestHeader("Idempotency-Key") String key, @RequestBody VersionRequest request) { - return ApiResponse.success(service.issueBillById(user.userId(), billId, request.expectedVersion(), key)); + public ApiResponse issueBill(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable long billId, @RequestHeader("Idempotency-Key") String key, @RequestBody VersionRequest request) { + return FinanceResponses.success(resourceView(service.issueBillById(user.userId(), billId, request.expectedVersion(), key))); } @GetMapping("/bills/{billId}") @PreAuthorize("hasAuthority('platform:finance:bill:view')") - public ApiResponse> bill(@PathVariable long billId) { return ApiResponse.success(service.billById(billId)); } + public ApiResponse bill(@PathVariable long billId) { return FinanceResponses.success(queryService.platformStatement(billId)); } @GetMapping("/bills/{billId}/revisions") @PreAuthorize("hasAuthority('platform:finance:bill:view')") public ApiResponse revisions(@PathVariable long billId, @RequestParam long customerId) { - return ApiResponse.success(service.billRevisions(customerId, billId)); + return FinanceResponses.success(queryService.billRevisions(customerId, billId)); } @GetMapping("/receipts") @PreAuthorize("hasAuthority('platform:finance:receipt:view')") - public ApiResponse receipts(@RequestParam(required = false) Long customerId) { return ApiResponse.success(service.listReceipts(customerId)); } + public ApiResponse receipts(@RequestParam(required = false) Long customerId) { + if (customerId == null) throw new IllegalArgumentException("查询收款记录必须指定客户"); + return FinanceResponses.success(queryService.platformReceipts(customerId)); + } @PostMapping("/receipts") @PreAuthorize("hasAuthority('platform:finance:receipt:create')") @BusinessAudit(event = "FINANCE_RECEIPT_CREATE", resourceType = "FINANCE_RECEIPT") - public ApiResponse> receipt(@AuthenticationPrincipal AuthenticatedUser user, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody ReceiptRequest request) { + public ApiResponse receipt(@AuthenticationPrincipal AuthenticatedUser user, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody ReceiptRequest request) { if (request.amount() == null || request.amount().signum() <= 0) throw new IllegalArgumentException("收款金额必须大于0"); if (request.receivedDate() == null) throw new IllegalArgumentException("到账日期不能为空"); if (request.allocations() == null || request.allocations().isEmpty()) throw new IllegalArgumentException("收款至少需要分配一张账单"); @@ -95,17 +152,20 @@ public class PlatformFinanceController { if (request.amount().compareTo(allocated) != 0) throw new IllegalArgumentException("收款金额必须等于分配金额合计"); var allocations = request.allocations().stream().map(a -> new FinanceService.ReceiptAllocation(a.billId(), a.amount())).toList(); var internal = new FinanceService.ReceiptRequest(null, 1L, request.receivedDate().atStartOfDay(), request.paymentMethod(), null, request.voucherFileIds(), request.remark(), allocations); - return ApiResponse.success(service.createReceipt(request.customerId(), user.userId(), internal, key)); + return FinanceResponses.success(resourceView(service.createReceipt(request.customerId(), user.userId(), internal, key))); } @GetMapping("/cost-entries") @PreAuthorize("hasAuthority('platform:finance:cost:view')") - public ApiResponse costs(@RequestParam(required = false) Long customerId) { return ApiResponse.success(service.listCosts(customerId)); } + public ApiResponse costs(@RequestParam(required = false) Long customerId) { + if (customerId == null) throw new IllegalArgumentException("查询成本记录必须指定客户"); + return FinanceResponses.success(queryService.payableSettlements(customerId)); + } @PostMapping("/cost-entries") @PreAuthorize("hasAuthority('platform:finance:cost:create')") @BusinessAudit(event = "FINANCE_COST_CREATE", resourceType = "FINANCE_COST") - public ApiResponse> cost(@AuthenticationPrincipal AuthenticatedUser user, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody CostRequest request) { + public ApiResponse cost(@AuthenticationPrincipal AuthenticatedUser user, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody CostRequest request) { if (request.currency() != null && !"CNY".equals(request.currency())) throw new IllegalArgumentException("首期仅支持CNY"); var internal = new FinanceService.CostRequest(null, request.costType(), request.orderId(), null, request.containerId(), null, null, "MANUAL", request.expenseName(), request.amount(), request.voucherFileIds(), request.remark()); java.util.List volumes = java.util.List.of(); @@ -118,20 +178,42 @@ public class PlatformFinanceController { } volumes = snapshot.orders().stream().map(v -> new FinanceService.ContainerOrderVolume(v.orderId(), v.inboundId(), snapshot.containerId(), snapshot.customerId(), v.volumeCbm())).toList(); } - return ApiResponse.success(service.createCost(request.customerId(), user.userId(), internal, volumes, key)); + return FinanceResponses.success(resourceView(service.createCost(request.customerId(), user.userId(), internal, volumes, key))); } @PutMapping("/bills/{billId}") @PreAuthorize("hasAuthority('platform:finance:bill:update')") - public ApiResponse> update(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable long billId, @RequestHeader("Idempotency-Key") String key, @RequestBody UpdateBillRequest request) { return ApiResponse.success(service.updateBill(user.userId(), billId, request.customerId(), request.billDate(), request.lines(), request.expectedVersion(), key)); } + public ApiResponse update(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable long billId, @RequestHeader("Idempotency-Key") String key, @RequestBody UpdateBillRequest request) { return FinanceResponses.success(resourceView(service.updateBill(user.userId(), billId, request.customerId(), request.billDate(), request.lines(), request.expectedVersion(), key))); } @PostMapping("/bills/{billId}/adjust") @PreAuthorize("hasAuthority('platform:finance:bill:adjust')") - public ApiResponse> adjust(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable long billId, @RequestHeader("Idempotency-Key") String key, @RequestBody AdjustBillRequest request) { return ApiResponse.success(service.adjustBill(user.userId(), billId, request.lines(), request.expectedVersion(), request.reason(), key)); } + public ApiResponse adjust(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable long billId, @RequestHeader("Idempotency-Key") String key, @RequestBody AdjustBillRequest request) { return FinanceResponses.success(resourceView(service.adjustBill(user.userId(), billId, request.lines(), request.expectedVersion(), request.reason(), key))); } @PostMapping("/bills/{billId}/void") @PreAuthorize("hasAuthority('platform:finance:bill:void')") - public ApiResponse> voidBill(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable long billId, @RequestHeader("Idempotency-Key") String key, @RequestBody ReasonRequest request) { return ApiResponse.success(service.voidBill(user.userId(), billId, request.expectedVersion(), request.reason(), key)); } + public ApiResponse voidBill(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable long billId, @RequestHeader("Idempotency-Key") String key, @RequestBody ReasonRequest request) { return FinanceResponses.success(resourceView(service.voidBill(user.userId(), billId, request.expectedVersion(), request.reason(), key))); } + + private static com.baiyue.finance.dto.FinanceReadModels.CustomerTerms termsView(long customerId, Map row) { + Object name = row.getOrDefault("customer_name_snapshot", row.get("customerNameSnapshot")); + Object days = row.getOrDefault("credit_days", row.get("creditDays")); + return new com.baiyue.finance.dto.FinanceReadModels.CustomerTerms(customerId, name == null ? null : name.toString(), + days == null ? 30 : ((Number) days).intValue(), String.valueOf(row.getOrDefault("status", "ACTIVE"))); + } + + private static com.baiyue.finance.dto.FinanceReadModels.ResourceResult resourceView(Map row) { + long id = number(row.get("id"), 0L).longValue(); + Object no = first(row, "bill_no", "receipt_no", "cost_no"); + Object version = row.get("version"); + return new com.baiyue.finance.dto.FinanceReadModels.ResourceResult(id, no == null ? null : no.toString(), + String.valueOf(row.getOrDefault("status", "DRAFT")), version instanceof Number n ? n.intValue() : null); + } + + private static Object first(Map row, String... keys) { + for (String key : keys) if (row.get(key) != null) return row.get(key); + return null; + } + + private static Number number(Object value, Number fallback) { return value instanceof Number n ? n : fallback; } public record TermsRequest(String customerName, Integer creditDays) {} public record CreateBillRequest(long customerId, java.time.LocalDate billDate, java.util.List lines) {} diff --git a/finance/src/main/java/com/baiyue/finance/dto/FinanceFeeSourceEvent.java b/finance/src/main/java/com/baiyue/finance/dto/FinanceFeeSourceEvent.java new file mode 100644 index 000000000..ae88b33eb --- /dev/null +++ b/finance/src/main/java/com/baiyue/finance/dto/FinanceFeeSourceEvent.java @@ -0,0 +1,12 @@ +package com.baiyue.finance.dto; + +import java.time.LocalDateTime; + +public record FinanceFeeSourceEvent( + String eventId, String eventType, int eventVersion, String producer, + LocalDateTime occurredAt, String tenantId, String aggregateType, + String aggregateId, int sequence, String idempotencyKey, + String payloadHash, Payload payload +) { + public record Payload(String inboundId, String containerId, String feeId, int feeVersion) {} +} diff --git a/finance/src/main/java/com/baiyue/finance/dto/FinanceReadModels.java b/finance/src/main/java/com/baiyue/finance/dto/FinanceReadModels.java new file mode 100644 index 000000000..8c69fcec2 --- /dev/null +++ b/finance/src/main/java/com/baiyue/finance/dto/FinanceReadModels.java @@ -0,0 +1,146 @@ +package com.baiyue.finance.dto; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +/** Stable read projections used by the Finance pages. */ +public final class FinanceReadModels { + private FinanceReadModels() {} + + public record CustomerAccount( + long customerId, + String currency, + String accountMode, + BigDecimal totalReceivable, + BigDecimal totalReceived, + BigDecimal outstandingAmount, + List entries) {} + + public record AccountEntry( + String entryType, + long resourceId, + String resourceNo, + LocalDateTime occurredAt, + BigDecimal debitAmount, + BigDecimal creditAmount, + String currency, + String status) {} + + public record FeePoolItem( + String poolItemId, + String direction, + String sourceType, + long sourceId, + int sourceVersion, + Long orderId, + Long inboundId, + Long containerId, + String feeItemName, + BigDecimal amount, + String currency, + String status, + LocalDateTime createdAt) {} + + public record Statement( + long id, + long customerId, + String billNo, + String customerName, + LocalDate billDate, + LocalDate dueDate, + BigDecimal totalAmount, + BigDecimal receivedAmount, + BigDecimal outstandingAmount, + String currency, + String status, + int version) {} + + public record StatementDetail(Statement statement, List lines) {} + + public record StatementLine( + long id, + String sourceType, + long sourceId, + int sourceVersion, + Long orderId, + Long inboundId, + String feeItemCode, + String feeItemName, + BigDecimal amount, + String currency) {} + + public record ReceivableSettlement( + long id, + long customerId, + String settlementNo, + LocalDate billDate, + LocalDate dueDate, + BigDecimal receivableAmount, + BigDecimal receivedAmount, + BigDecimal outstandingAmount, + String currency, + String status, + int version) {} + + public record PayableSettlement( + long id, + long customerId, + String settlementNo, + String costType, + Long orderId, + Long inboundId, + Long containerId, + String supplierName, + String feeItemName, + BigDecimal payableAmount, + BigDecimal paidAmount, + BigDecimal outstandingAmount, + String currency, + String status, + LocalDateTime createdAt, + int version) {} + + /** Client-safe receipt view. Voucher and operator/internal fields are deliberately absent. */ + public record ClientReceipt( + long id, + String receiptNo, + BigDecimal receivedAmount, + LocalDateTime receivedAt, + String paymentMethod, + String currency, + String status, + List allocations) {} + + public record ClientReceiptAllocation(long billId, String billNo, BigDecimal appliedAmount) {} + + public record PlatformReceipt( + long id, + long customerId, + String receiptNo, + BigDecimal receivedAmount, + LocalDateTime receivedAt, + String paymentMethod, + String voucherNo, + List voucherFileIds, + String currency, + String status, + String remark, + int version, + List allocations) {} + + public record BillRevision( + long id, + long billId, + int version, + String reason, + String billSnapshot, + String lineSnapshot, + long createdBy, + LocalDateTime createdAt) {} + + public record CustomerTerms(long customerId, String customerName, int creditDays, String status) {} + + public record ResourceResult(long id, String resourceNo, String status, Integer version) {} +} diff --git a/finance/src/main/java/com/baiyue/finance/service/FinanceEventException.java b/finance/src/main/java/com/baiyue/finance/service/FinanceEventException.java new file mode 100644 index 000000000..1965712f5 --- /dev/null +++ b/finance/src/main/java/com/baiyue/finance/service/FinanceEventException.java @@ -0,0 +1,12 @@ +package com.baiyue.finance.service; + +public final class FinanceEventException extends RuntimeException { + private final String code; + + public FinanceEventException(String code, String message) { + super(message); + this.code = code; + } + + public String code() { return code; } +} diff --git a/finance/src/main/java/com/baiyue/finance/service/FinanceFeeSourceEventService.java b/finance/src/main/java/com/baiyue/finance/service/FinanceFeeSourceEventService.java new file mode 100644 index 000000000..69d61ead8 --- /dev/null +++ b/finance/src/main/java/com/baiyue/finance/service/FinanceFeeSourceEventService.java @@ -0,0 +1,153 @@ +package com.baiyue.finance.service; + +import com.baiyue.finance.client.FirstlegFeeSourceClient; +import com.baiyue.finance.dto.FinanceFeeSourceEvent; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@Service +@RequiredArgsConstructor +public class FinanceFeeSourceEventService { + private static final Set EVENT_TYPES = Set.of( + "FIRSTLEG_FEE_PUBLISHED", "FIRSTLEG_FEE_ADJUSTED", "FIRSTLEG_FEE_VOIDED"); + + private final JdbcTemplate jdbc; + private final FirstlegFeeSourceClient firstleg; + private final ObjectMapper objectMapper; + @Value("${baiyue.security.internal-service-token}") + private String internalToken; + + @Transactional + public long consume(FinanceFeeSourceEvent event) { + validateEnvelope(event); + String eventJson = json(event); + String requestHash = sha256(eventJson); + List> existing = jdbc.queryForList( + "SELECT payload_hash,status,result_resource_id FROM fin_inbox_event WHERE source_system=? AND event_id=? FOR UPDATE", + "FIRSTLEG", event.eventId()); + if (!existing.isEmpty()) { + Map row = existing.get(0); + if (!requestHash.equals(row.get("payload_hash"))) { + throw error("FIN-EVT-002", "同一事件ID对应不同请求内容"); + } + if ("PROCESSED".equals(row.get("status")) && row.get("result_resource_id") instanceof Number id) { + return id.longValue(); + } + throw error("FIN-EVT-003", "相同事件正在处理或上次处理失败"); + } + jdbc.update("INSERT INTO fin_inbox_event(event_id,source_system,event_type,payload,payload_hash,status) VALUES (?,?,?,?,?,'RECEIVED')", + event.eventId(), "FIRSTLEG", event.eventType(), eventJson, requestHash); + + FirstlegFeeSourceClient.Snapshot snapshot = loadSnapshot(event); + validateSnapshot(event, snapshot); + long resourceId = applyEvent(event, snapshot); + jdbc.update("UPDATE fin_inbox_event SET status='PROCESSED',result_resource_id=?,processed_at=CURRENT_TIMESTAMP(3),error_code=NULL WHERE source_system=? AND event_id=?", + resourceId, "FIRSTLEG", event.eventId()); + return resourceId; + } + + private long applyEvent(FinanceFeeSourceEvent event, FirstlegFeeSourceClient.Snapshot snapshot) { + List> sameVersion = jdbc.queryForList( + "SELECT id,source_hash,status FROM fin_fee_source WHERE source_system='FIRSTLEG' AND source_fee_id=? AND source_fee_version=? FOR UPDATE", + snapshot.feeId(), snapshot.feeVersion()); + if (!sameVersion.isEmpty()) { + Map row = sameVersion.get(0); + if (!snapshot.sourceHash().equals(row.get("source_hash"))) { + throw error("FIN-EVT-004", "同一来源费用版本快照不一致"); + } + long id = ((Number) row.get("id")).longValue(); + if ("FIRSTLEG_FEE_VOIDED".equals(event.eventType())) { + jdbc.update("UPDATE fin_fee_source SET status='VOIDED' WHERE id=?", id); + } + return id; + } + if ("FIRSTLEG_FEE_VOIDED".equals(event.eventType())) { + throw error("FIN-EVT-005", "作废事件对应的费用来源版本不存在"); + } + jdbc.update("UPDATE fin_fee_source SET status='SUPERSEDED' WHERE source_system='FIRSTLEG' AND source_fee_id=? AND status='AVAILABLE'", + snapshot.feeId()); + jdbc.update("INSERT INTO fin_fee_source(source_system,source_fee_id,source_fee_version,customer_id,source_type,source_no,direction,order_id,inbound_id,container_id,fee_item_code,fee_item_name,amount,currency_code,source_hash,source_snapshot,status) VALUES ('FIRSTLEG',?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,'AVAILABLE')", + snapshot.feeId(), snapshot.feeVersion(), snapshot.customerId(), snapshot.sourceType(), + snapshot.sourceType().equals("INBOUND_FEE") ? snapshot.inboundCode() : snapshot.containerNo(), + snapshot.direction(), snapshot.orderId(), snapshot.inboundId(), snapshot.containerId(), + snapshot.feeItemCode(), snapshot.feeItemName(), snapshot.totalAmount(), snapshot.currency(), + snapshot.sourceHash(), json(snapshot)); + return jdbc.queryForObject("SELECT id FROM fin_fee_source WHERE source_system='FIRSTLEG' AND source_fee_id=? AND source_fee_version=?", + Long.class, snapshot.feeId(), snapshot.feeVersion()); + } + + private FirstlegFeeSourceClient.Snapshot loadSnapshot(FinanceFeeSourceEvent event) { + FinanceFeeSourceEvent.Payload payload = event.payload(); + var response = payload.inboundId() != null + ? firstleg.inbound(internalToken, positive(payload.inboundId(), "inboundId"), positive(payload.feeId(), "feeId")) + : firstleg.container(internalToken, positive(payload.containerId(), "containerId"), positive(payload.feeId(), "feeId")); + if (response == null || response.data() == null || !"200".equals(response.code())) { + throw error("FIN-EVT-006", "Firstleg费用来源快照不可用"); + } + return response.data(); + } + + private void validateEnvelope(FinanceFeeSourceEvent event) { + if (event == null || blank(event.eventId()) || !EVENT_TYPES.contains(event.eventType()) + || !"firstleg-service".equals(event.producer()) || event.payload() == null + || blank(event.tenantId()) || blank(event.payload().feeId())) { + throw error("FIN-EVT-001", "费用来源事件信封无效"); + } + boolean inbound = event.payload().inboundId() != null; + boolean container = event.payload().containerId() != null; + if (inbound == container || !sha256(json(event.payload())).equals(event.payloadHash())) { + throw error("FIN-EVT-001", "费用来源事件载荷无效"); + } + } + + private void validateSnapshot(FinanceFeeSourceEvent event, FirstlegFeeSourceClient.Snapshot value) { + long tenantId = positive(event.tenantId(), "tenantId"); + long feeId = positive(event.payload().feeId(), "feeId"); + if (value.tenantId() != tenantId || value.customerId() != tenantId) throw error("FIN-EVT-007", "费用来源客户归属不一致"); + if (value.feeId() != feeId || value.feeVersion() != event.payload().feeVersion()) throw error("FIN-EVT-008", "费用来源版本不一致"); + if (!"APPROVED".equals(value.reviewStatus()) || !value.settlementIncluded()) throw error("FIN-EVT-009", "费用未审核通过或未参与结算"); + if (!"FIRSTLEG_FEE_VOIDED".equals(event.eventType()) && !"PUBLISHED".equals(value.publicationStatus())) { + throw error("FIN-EVT-015", "费用来源尚未发布"); + } + if (!"CNY".equals(value.currency()) || value.totalAmount() == null || value.totalAmount().signum() <= 0) throw error("FIN-EVT-010", "费用金额或币种无效"); + if (!Set.of("RECEIVABLE", "PAYABLE").contains(value.direction())) throw error("FIN-EVT-011", "费用方向无效"); + if (!Set.of("INBOUND_FEE", "CONTAINER_FEE").contains(value.sourceType()) || blank(value.feeItemCode()) || blank(value.feeItemName()) || blank(value.sourceHash())) throw error("FIN-EVT-012", "费用来源快照字段不完整"); + if ("INBOUND_FEE".equals(value.sourceType()) && (value.inboundId() == null || !String.valueOf(value.inboundId()).equals(event.payload().inboundId()))) throw error("FIN-EVT-013", "入库单归属不一致"); + if ("CONTAINER_FEE".equals(value.sourceType()) && (value.containerId() == null || !String.valueOf(value.containerId()).equals(event.payload().containerId()))) throw error("FIN-EVT-014", "柜子归属不一致"); + } + + private String json(Object value) { + try { return objectMapper.writeValueAsString(value); } + catch (JsonProcessingException e) { throw error("FIN-EVT-001", "事件序列化失败"); } + } + + private static long positive(String value, String field) { + try { + long parsed = Long.parseLong(value); + if (parsed > 0) return parsed; + } catch (RuntimeException ignored) { } + throw error("FIN-EVT-001", field + "无效"); + } + + private static String sha256(String value) { + try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8))); } + catch (NoSuchAlgorithmException e) { throw new IllegalStateException(e); } + } + + private static boolean blank(String value) { return value == null || value.isBlank(); } + private static FinanceEventException error(String code, String message) { return new FinanceEventException(code, message); } +} diff --git a/finance/src/main/java/com/baiyue/finance/service/FinanceQueryService.java b/finance/src/main/java/com/baiyue/finance/service/FinanceQueryService.java new file mode 100644 index 000000000..5eef68a30 --- /dev/null +++ b/finance/src/main/java/com/baiyue/finance/service/FinanceQueryService.java @@ -0,0 +1,197 @@ +package com.baiyue.finance.service; + +import com.baiyue.common.core.api.ResultCode; +import com.baiyue.common.core.exception.BizException; +import com.baiyue.finance.dto.FinanceReadModels.AccountEntry; +import com.baiyue.finance.dto.FinanceReadModels.ClientReceipt; +import com.baiyue.finance.dto.FinanceReadModels.ClientReceiptAllocation; +import com.baiyue.finance.dto.FinanceReadModels.BillRevision; +import com.baiyue.finance.dto.FinanceReadModels.CustomerAccount; +import com.baiyue.finance.dto.FinanceReadModels.FeePoolItem; +import com.baiyue.finance.dto.FinanceReadModels.PayableSettlement; +import com.baiyue.finance.dto.FinanceReadModels.PlatformReceipt; +import com.baiyue.finance.dto.FinanceReadModels.ReceivableSettlement; +import com.baiyue.finance.dto.FinanceReadModels.Statement; +import com.baiyue.finance.dto.FinanceReadModels.StatementDetail; +import com.baiyue.finance.dto.FinanceReadModels.StatementLine; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Service; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.math.BigDecimal; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** Typed query facade for the five Finance pages and client-safe read APIs. */ +@Service +public class FinanceQueryService { + private static final String CNY = "CNY"; + private final JdbcTemplate jdbc; + private final ObjectMapper objectMapper; + + public FinanceQueryService(JdbcTemplate jdbc, ObjectMapper objectMapper) { + this.jdbc = jdbc; + this.objectMapper = objectMapper; + } + + public CustomerAccount customerAccount(long customerId) { + requireCustomer(customerId); + BigDecimal total = amount("SELECT COALESCE(SUM(total_amount),0) FROM fin_bill WHERE tenant_id=? AND status<>'VOIDED'", customerId); + BigDecimal received = amount("SELECT COALESCE(SUM(received_amount),0) FROM fin_bill WHERE tenant_id=? AND status<>'VOIDED'", customerId); + BigDecimal outstanding = amount("SELECT COALESCE(SUM(outstanding_amount),0) FROM fin_bill WHERE tenant_id=? AND status<>'VOIDED'", customerId); + List entries = new ArrayList<>(); + entries.addAll(jdbc.query("SELECT id,bill_no,created_at,total_amount,currency_code,status FROM fin_bill WHERE tenant_id=? AND status<>'VOIDED'", (rs, rowNum) -> + new AccountEntry("RECEIVABLE", rs.getLong("id"), rs.getString("bill_no"), time(rs, "created_at"), + rs.getBigDecimal("total_amount"), BigDecimal.ZERO, rs.getString("currency_code"), rs.getString("status")), customerId)); + entries.addAll(jdbc.query("SELECT id,receipt_no,received_at,received_amount,currency_code,status FROM fin_receipt WHERE tenant_id=? AND status<>'VOIDED'", (rs, rowNum) -> + new AccountEntry("RECEIPT", rs.getLong("id"), rs.getString("receipt_no"), time(rs, "received_at"), + BigDecimal.ZERO, rs.getBigDecimal("received_amount"), rs.getString("currency_code"), rs.getString("status")), customerId)); + entries.sort(Comparator.comparing(AccountEntry::occurredAt, Comparator.nullsLast(Comparator.reverseOrder()))); + return new CustomerAccount(customerId, CNY, "MANUAL_RECEIVABLE_LEDGER", total, received, outstanding, List.copyOf(entries)); + } + + public List feePool(long customerId, String direction) { + requireCustomer(customerId); + String normalized = direction == null || direction.isBlank() ? "ALL" : direction.trim().toUpperCase(java.util.Locale.ROOT); + if (!List.of("ALL", "RECEIVABLE", "PAYABLE").contains(normalized)) throw bad("费用方向只能是RECEIVABLE或PAYABLE"); + List result = new ArrayList<>(); + String sourceSql = "SELECT id,direction,source_type,source_fee_id,source_fee_version,order_id,inbound_id,container_id,fee_item_name,amount,currency_code,status,created_at FROM fin_fee_source WHERE customer_id=? AND status='AVAILABLE'"; + Object[] sourceArgs; + if ("ALL".equals(normalized)) { + sourceSql += " ORDER BY created_at DESC,id DESC"; + sourceArgs = new Object[]{customerId}; + } else { + sourceSql += " AND direction=? ORDER BY created_at DESC,id DESC"; + sourceArgs = new Object[]{customerId, normalized}; + } + result.addAll(jdbc.query(sourceSql, (rs, rowNum) -> { + String rowDirection = rs.getString("direction"); + return new FeePoolItem(rowDirection + ":" + rs.getLong("id"), rowDirection, + rs.getString("source_type"), rs.getLong("source_fee_id"), rs.getInt("source_fee_version"), + nullableLong(rs, "order_id"), nullableLong(rs, "inbound_id"), nullableLong(rs, "container_id"), + rs.getString("fee_item_name"), rs.getBigDecimal("amount"), rs.getString("currency_code"), + rs.getString("status"), time(rs, "created_at")); + }, sourceArgs)); + if (!"RECEIVABLE".equals(normalized)) { + result.addAll(jdbc.query("SELECT id,order_id,inbound_id,container_id,fee_item_name,cost_amount,currency_code,status,created_at FROM fin_cost WHERE tenant_id=? ORDER BY created_at DESC,id DESC", (rs, rowNum) -> + new FeePoolItem("PAYABLE:" + rs.getLong("id"), "PAYABLE", "MANUAL_COST", rs.getLong("id"), 1, + nullableLong(rs, "order_id"), nullableLong(rs, "inbound_id"), nullableLong(rs, "container_id"), + rs.getString("fee_item_name"), rs.getBigDecimal("cost_amount"), rs.getString("currency_code"), rs.getString("status"), time(rs, "created_at")), customerId)); + } + result.sort(Comparator.comparing(FeePoolItem::createdAt, Comparator.nullsLast(Comparator.reverseOrder()))); + return List.copyOf(result); + } + + public List statements(long customerId, boolean clientVisibleOnly) { + requireCustomer(customerId); + String visible = clientVisibleOnly ? " AND status IN ('ISSUED','PARTIALLY_PAID','PAID')" : ""; + return jdbc.query("SELECT id,tenant_id,bill_no,customer_name_snapshot,bill_date,due_date,total_amount,received_amount,outstanding_amount,currency_code,status,version FROM fin_bill WHERE tenant_id=?" + visible + " ORDER BY bill_date DESC,id DESC", + (rs, rowNum) -> new Statement(rs.getLong("id"), rs.getLong("tenant_id"), rs.getString("bill_no"), rs.getString("customer_name_snapshot"), + rs.getDate("bill_date").toLocalDate(), rs.getDate("due_date").toLocalDate(), rs.getBigDecimal("total_amount"), rs.getBigDecimal("received_amount"), + rs.getBigDecimal("outstanding_amount"), rs.getString("currency_code"), rs.getString("status"), rs.getInt("version")), customerId); + } + + public StatementDetail statement(long customerId, long billId, boolean clientVisibleOnly) { + requireCustomer(customerId); + String visible = clientVisibleOnly ? " AND status IN ('ISSUED','PARTIALLY_PAID','PAID')" : ""; + List rows = jdbc.query("SELECT id,tenant_id,bill_no,customer_name_snapshot,bill_date,due_date,total_amount,received_amount,outstanding_amount,currency_code,status,version FROM fin_bill WHERE tenant_id=? AND id=?" + visible, + (rs, rowNum) -> new Statement(rs.getLong("id"), rs.getLong("tenant_id"), rs.getString("bill_no"), rs.getString("customer_name_snapshot"), + rs.getDate("bill_date").toLocalDate(), rs.getDate("due_date").toLocalDate(), rs.getBigDecimal("total_amount"), rs.getBigDecimal("received_amount"), + rs.getBigDecimal("outstanding_amount"), rs.getString("currency_code"), rs.getString("status"), rs.getInt("version")), customerId, billId); + if (rows.isEmpty()) throw new BizException(ResultCode.NOT_FOUND, "账单不存在或不属于客户"); + List lines = jdbc.query("SELECT id,source_type,source_id,source_version,order_id,inbound_id,fee_item_code,fee_item_name,amount,currency_code FROM fin_bill_line WHERE tenant_id=? AND bill_id=? ORDER BY id", + (rs, rowNum) -> new StatementLine(rs.getLong("id"), rs.getString("source_type"), rs.getLong("source_id"), rs.getInt("source_version"), + nullableLong(rs, "order_id"), nullableLong(rs, "inbound_id"), rs.getString("fee_item_code"), rs.getString("fee_item_name"), + rs.getBigDecimal("amount"), rs.getString("currency_code")), customerId, billId); + return new StatementDetail(rows.get(0), lines); + } + + public StatementDetail platformStatement(long billId) { + Long customerId = jdbc.queryForObject("SELECT tenant_id FROM fin_bill WHERE id=?", Long.class, billId); + if (customerId == null) throw new BizException(ResultCode.NOT_FOUND, "账单不存在"); + return statement(customerId, billId, false); + } + + public List billRevisions(long customerId, long billId) { + statement(customerId, billId, false); + return jdbc.query("SELECT id,bill_id,version,reason,bill_snapshot,line_snapshot,created_by,created_at FROM fin_bill_revision WHERE tenant_id=? AND bill_id=? ORDER BY version DESC,id DESC", + (rs, rowNum) -> new BillRevision(rs.getLong("id"), rs.getLong("bill_id"), rs.getInt("version"), rs.getString("reason"), + rs.getString("bill_snapshot"), rs.getString("line_snapshot"), rs.getLong("created_by"), time(rs, "created_at")), customerId, billId); + } + + public List receivableSettlements(long customerId, boolean clientVisibleOnly) { + return statements(customerId, clientVisibleOnly).stream().map(row -> new ReceivableSettlement( + row.id(), row.customerId(), row.billNo(), row.billDate(), row.dueDate(), row.totalAmount(), row.receivedAmount(), + row.outstandingAmount(), row.currency(), row.status(), row.version())).toList(); + } + + public List payableSettlements(long customerId) { + requireCustomer(customerId); + return jdbc.query("SELECT id,tenant_id,cost_no,cost_type,order_id,inbound_id,container_id,supplier_name_snapshot,fee_item_name,cost_amount,currency_code,status,created_at,version FROM fin_cost WHERE tenant_id=? ORDER BY created_at DESC,id DESC", + (rs, rowNum) -> { + BigDecimal amount = rs.getBigDecimal("cost_amount"); + return new PayableSettlement(rs.getLong("id"), rs.getLong("tenant_id"), rs.getString("cost_no"), rs.getString("cost_type"), + nullableLong(rs, "order_id"), nullableLong(rs, "inbound_id"), nullableLong(rs, "container_id"), rs.getString("supplier_name_snapshot"), + rs.getString("fee_item_name"), amount, BigDecimal.ZERO, amount, rs.getString("currency_code"), rs.getString("status"), + time(rs, "created_at"), rs.getInt("version")); + }, customerId); + } + + public List clientReceipts(long customerId) { + requireCustomer(customerId); + return jdbc.query("SELECT id,receipt_no,received_amount,received_at,payment_method,currency_code,status FROM fin_receipt WHERE tenant_id=? AND status='CONFIRMED' ORDER BY received_at DESC,id DESC", + (rs, rowNum) -> new ClientReceipt(rs.getLong("id"), rs.getString("receipt_no"), rs.getBigDecimal("received_amount"), + time(rs, "received_at"), rs.getString("payment_method"), rs.getString("currency_code"), rs.getString("status"), allocations(customerId, rs.getLong("id"))), customerId); + } + + public List platformReceipts(long customerId) { + requireCustomer(customerId); + return jdbc.query("SELECT id,tenant_id,receipt_no,received_amount,received_at,payment_method,voucher_no,voucher_file_ids,currency_code,status,remark,version FROM fin_receipt WHERE tenant_id=? ORDER BY received_at DESC,id DESC", + (rs, rowNum) -> new PlatformReceipt(rs.getLong("id"), rs.getLong("tenant_id"), rs.getString("receipt_no"), rs.getBigDecimal("received_amount"), + time(rs, "received_at"), rs.getString("payment_method"), rs.getString("voucher_no"), fileIds(rs.getString("voucher_file_ids")), + rs.getString("currency_code"), rs.getString("status"), rs.getString("remark"), rs.getInt("version"), allocations(customerId, rs.getLong("id"))), customerId); + } + + private List fileIds(String json) { + if (json == null || json.isBlank()) return List.of(); + try { + return objectMapper.readValue(json, new TypeReference>() {}); + } catch (Exception error) { + throw new IllegalStateException("财务凭证文件字段损坏", error); + } + } + + private List allocations(long customerId, long receiptId) { + return jdbc.query("SELECT rb.bill_id,b.bill_no,rb.applied_amount FROM fin_receipt_bill rb JOIN fin_bill b ON b.id=rb.bill_id AND b.tenant_id=rb.tenant_id WHERE rb.tenant_id=? AND rb.receipt_id=? ORDER BY rb.id", + (rs, rowNum) -> new ClientReceiptAllocation(rs.getLong("bill_id"), rs.getString("bill_no"), rs.getBigDecimal("applied_amount")), customerId, receiptId); + } + + private BigDecimal amount(String sql, long customerId) { + BigDecimal value = jdbc.queryForObject(sql, BigDecimal.class, customerId); + return value == null ? BigDecimal.ZERO : value; + } + + private static Long nullableLong(ResultSet rs, String column) throws SQLException { + long value = rs.getLong(column); + return rs.wasNull() ? null : value; + } + + private static LocalDateTime time(ResultSet rs, String column) throws SQLException { + Timestamp timestamp = rs.getTimestamp(column); + return timestamp == null ? null : timestamp.toLocalDateTime(); + } + + private static void requireCustomer(long customerId) { + if (customerId <= 0) throw bad("客户范围无效"); + } + + private static BizException bad(String message) { + return new BizException(ResultCode.BAD_REQUEST, message); + } +} diff --git a/finance/src/main/java/com/baiyue/finance/service/FinanceService.java b/finance/src/main/java/com/baiyue/finance/service/FinanceService.java index 3955c88a8..4f514a95d 100644 --- a/finance/src/main/java/com/baiyue/finance/service/FinanceService.java +++ b/finance/src/main/java/com/baiyue/finance/service/FinanceService.java @@ -194,8 +194,8 @@ public class FinanceService { } private void validateVoucherFiles(long tenantId, List fileIds) { - if (fileIds == null || fileIds.isEmpty()) throw bad("收款凭证不能为空"); - if (fileClient == null) return; + if (fileIds == null || fileIds.isEmpty()) throw bad("财务凭证不能为空"); + if (fileClient == null) throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "File服务校验能力不可用"); for (String raw : fileIds) { long id; try { id = Long.parseLong(raw); } catch (Exception ex) { throw bad("凭证文件ID无效"); } var response = fileClient.metadata(fileToken, tenantId, tenantId, id); @@ -298,12 +298,23 @@ public class FinanceService { private Long reserve(long tenantId, long operatorId, String operation, String key, Object request) { if (!StringUtils.hasText(key) || key.length() > 128) throw bad("必须提供有效的 Idempotency-Key"); - String hash = hash(request); - List> rows = jdbc.queryForList("SELECT request_hash,status,result_resource_id FROM fin_idempotency_record WHERE tenant_id=? AND operator_id=? AND operation=? AND idempotency_key=?", tenantId, operatorId, operation, key); + String hash = hash(new IdempotencyRequest(httpMethod(operation), requestPath(operation), request)); + List> rows = jdbc.queryForList("SELECT request_hash,status,result_resource_id,updated_at FROM fin_idempotency_record WHERE tenant_id=? AND operator_id=? AND operation=? AND idempotency_key=?", tenantId, operatorId, operation, key); if (!rows.isEmpty()) { Map row = rows.get(0); if (!hash.equals(row.get("request_hash"))) throw new BizException(ResultCode.CONFLICT, "Idempotency-Key已用于其他请求"); if ("SUCCEEDED".equals(row.get("status")) && row.get("result_resource_id") != null) return ((Number) row.get("result_resource_id")).longValue(); + if ("FAILED".equals(row.get("status"))) { + int recovered = jdbc.update("UPDATE fin_idempotency_record SET status='PROCESSING',error_code=NULL WHERE tenant_id=? AND operator_id=? AND operation=? AND idempotency_key=? AND status='FAILED'", + tenantId, operatorId, operation, key); + if (recovered == 1) return null; + } + LocalDateTime updatedAt = idempotencyUpdatedAt(row.get("updated_at")); + if ("PROCESSING".equals(row.get("status")) && updatedAt != null && updatedAt.isBefore(LocalDateTime.now().minusMinutes(5))) { + int recovered = jdbc.update("UPDATE fin_idempotency_record SET updated_at=CURRENT_TIMESTAMP(3) WHERE tenant_id=? AND operator_id=? AND operation=? AND idempotency_key=? AND status='PROCESSING' AND updated_at=?", + tenantId, operatorId, operation, key, updatedAt); + if (recovered == 1) return null; + } throw new BizException(ResultCode.CONFLICT, "相同请求正在处理中"); } jdbc.update("INSERT INTO fin_idempotency_record(tenant_id,operator_id,operation,idempotency_key,request_hash,status) VALUES(?,?,?,?,?,'PROCESSING')", tenantId, operatorId, operation, key, hash); @@ -315,6 +326,30 @@ public class FinanceService { } private static String temporaryNo(String prefix) { return prefix + "-PENDING-" + java.util.UUID.randomUUID(); } private static String stableNo(String prefix, long id) { return prefix + "-" + String.format(java.util.Locale.ROOT, "%020d", id); } + private static LocalDateTime idempotencyUpdatedAt(Object value) { + if (value instanceof LocalDateTime dateTime) return dateTime; + if (value instanceof java.sql.Timestamp timestamp) return timestamp.toLocalDateTime(); + return null; + } + private static String httpMethod(String operation) { + return switch (operation) { + case "CUSTOMER_TERMS_SAVE", "BILL_UPDATE" -> "PUT"; + default -> "POST"; + }; + } + private static String requestPath(String operation) { + return switch (operation) { + case "CUSTOMER_TERMS_SAVE" -> "/api/platform/finance/v1/customers/{customerId}/terms"; + case "BILL_CREATE" -> "/api/platform/finance/v1/bills"; + case "BILL_ISSUE" -> "/api/platform/finance/v1/bills/{billId}/issue"; + case "RECEIPT_CREATE" -> "/api/platform/finance/v1/receipts"; + case "COST_CREATE" -> "/api/platform/finance/v1/cost-entries"; + case "BILL_UPDATE" -> "/api/platform/finance/v1/bills/{billId}"; + case "BILL_ADJUST" -> "/api/platform/finance/v1/bills/{billId}/adjust"; + case "BILL_VOID" -> "/api/platform/finance/v1/bills/{billId}/void"; + default -> throw new IllegalArgumentException("未知财务幂等操作: " + operation); + }; + } private String hash(Object request) { try { byte[] bytes = java.security.MessageDigest.getInstance("SHA-256").digest(json(request).getBytes(java.nio.charset.StandardCharsets.UTF_8)); return java.util.HexFormat.of().formatHex(bytes); } catch (Exception ex) { throw new IllegalStateException("请求哈希失败", ex); } } private static void requireCustomer(long tenantId) { if (tenantId <= 0) throw bad("客户范围无效"); } @@ -357,4 +392,5 @@ public class FinanceService { public record ContainerOrderVolume(long orderId, Long inboundId, long containerId, long customerId, BigDecimal volumeCbm) {} public record CostAllocation(ContainerOrderVolume order, BigDecimal totalVolumeCbm, BigDecimal rate, BigDecimal amount, boolean roundingAdjustment) {} + private record IdempotencyRequest(String method, String path, Object body) {} } diff --git a/finance/src/main/resources/db/fee-source-event-migration.sql b/finance/src/main/resources/db/fee-source-event-migration.sql new file mode 100644 index 000000000..56ab96e0d --- /dev/null +++ b/finance/src/main/resources/db/fee-source-event-migration.sql @@ -0,0 +1,88 @@ +-- Firstleg费用来源事件消费模型存量升级(MySQL 8,可重复执行) +USE baiyue_finance; + +DROP PROCEDURE IF EXISTS finance_fee_source_add_column; +DELIMITER $$ +CREATE PROCEDURE finance_fee_source_add_column( + IN target_table VARCHAR(64), IN target_column VARCHAR(64), IN column_definition TEXT +) +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = target_table AND column_name = target_column + ) THEN + SET @ddl = CONCAT('ALTER TABLE `', target_table, '` ADD COLUMN `', target_column, '` ', column_definition); + PREPARE statement FROM @ddl; + EXECUTE statement; + DEALLOCATE PREPARE statement; + END IF; +END$$ +DELIMITER ; + +CALL finance_fee_source_add_column('fin_fee_source', 'source_type', + "VARCHAR(32) NULL COMMENT '来源费用类型:INBOUND_FEE入库单费用、CONTAINER_FEE柜子费用' AFTER customer_id"); +CALL finance_fee_source_add_column('fin_fee_source', 'source_no', + "VARCHAR(64) NULL COMMENT '来源入库单号或柜号快照,可为空' AFTER source_type"); +CALL finance_fee_source_add_column('fin_fee_source', 'direction', + "VARCHAR(16) NULL COMMENT '费用方向:RECEIVABLE应收、PAYABLE应付' AFTER source_no"); +CALL finance_fee_source_add_column('fin_fee_source', 'container_id', + "BIGINT UNSIGNED NULL COMMENT '来源Firstleg装柜计划ID,可为空' AFTER inbound_id"); +CALL finance_fee_source_add_column('fin_fee_source', 'fee_item_code', + "VARCHAR(64) NULL COMMENT '来源费用项目编码快照' AFTER container_id"); +CALL finance_fee_source_add_column('fin_fee_source', 'source_hash', + "CHAR(64) NULL COMMENT 'Firstleg费用来源完整快照SHA-256哈希' AFTER currency_code"); +CALL finance_fee_source_add_column('fin_fee_source', 'source_snapshot', + "JSON NULL COMMENT 'Firstleg费用来源完整不可变JSON快照' AFTER source_hash"); +CALL finance_fee_source_add_column('fin_fee_source', 'updated_at', + "DATETIME(3) NULL COMMENT '来源状态最后更新时间,数据修改时自动刷新' AFTER created_at"); +CALL finance_fee_source_add_column('fin_inbox_event', 'payload_hash', + "CHAR(64) NULL COMMENT '规范化事件内容SHA-256哈希,用于同事件ID冲突检测' AFTER payload"); +CALL finance_fee_source_add_column('fin_inbox_event', 'result_resource_id', + "BIGINT UNSIGNED NULL COMMENT '成功处理生成或定位的费用来源快照ID' AFTER payload_hash"); +CALL finance_fee_source_add_column('fin_inbox_event', 'error_code', + "VARCHAR(64) NULL COMMENT '处理失败时的稳定FIN错误码' AFTER result_resource_id"); + +DROP PROCEDURE IF EXISTS finance_fee_source_add_column; + +-- 产品确认无历史财务数据;以下兼容值仅用于空环境重复迁移时保证结构收敛。 +UPDATE fin_fee_source + SET source_type = COALESCE(source_type, 'INBOUND_FEE'), + direction = COALESCE(direction, 'RECEIVABLE'), + fee_item_code = COALESCE(fee_item_code, 'LEGACY'), + source_hash = COALESCE(source_hash, SHA2(CONCAT_WS('|', source_system, source_fee_id, source_fee_version), 256)), + source_snapshot = COALESCE(source_snapshot, JSON_OBJECT('migrated', TRUE)), + updated_at = COALESCE(updated_at, created_at); +UPDATE fin_inbox_event + SET payload_hash = COALESCE(payload_hash, SHA2(CAST(payload AS CHAR), 256)); + +ALTER TABLE fin_fee_source + MODIFY COLUMN source_type VARCHAR(32) NOT NULL COMMENT '来源费用类型:INBOUND_FEE入库单费用、CONTAINER_FEE柜子费用', + MODIFY COLUMN direction VARCHAR(16) NOT NULL COMMENT '费用方向:RECEIVABLE应收、PAYABLE应付', + MODIFY COLUMN fee_item_code VARCHAR(64) NOT NULL COMMENT '来源费用项目编码快照', + MODIFY COLUMN source_hash CHAR(64) NOT NULL COMMENT 'Firstleg费用来源完整快照SHA-256哈希', + MODIFY COLUMN source_snapshot JSON NOT NULL COMMENT 'Firstleg费用来源完整不可变JSON快照', + MODIFY COLUMN status VARCHAR(16) NOT NULL DEFAULT 'AVAILABLE' COMMENT '费用来源状态:AVAILABLE可结算、SUPERSEDED已被新版本替代、VOIDED已作废', + MODIFY COLUMN updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '来源状态最后更新时间,数据修改时自动刷新'; +ALTER TABLE fin_inbox_event + MODIFY COLUMN payload_hash CHAR(64) NOT NULL COMMENT '规范化事件内容SHA-256哈希,用于同事件ID冲突检测', + MODIFY COLUMN status VARCHAR(16) NOT NULL DEFAULT 'RECEIVED' COMMENT '事件处理状态:RECEIVED已接收、PROCESSED已完成、FAILED处理失败'; + +DROP PROCEDURE IF EXISTS finance_fee_source_assert_schema; +DELIMITER $$ +CREATE PROCEDURE finance_fee_source_assert_schema() +BEGIN + IF (SELECT COUNT(*) FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'fin_fee_source' + AND column_name IN ('source_type','source_no','direction','container_id','fee_item_code', + 'source_hash','source_snapshot','updated_at')) <> 8 THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Finance费用来源迁移失败:来源快照字段不完整'; + END IF; + IF (SELECT COUNT(*) FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'fin_inbox_event' + AND column_name IN ('payload_hash','result_resource_id','error_code')) <> 3 THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Finance费用来源迁移失败:Inbox幂等字段不完整'; + END IF; +END$$ +DELIMITER ; +CALL finance_fee_source_assert_schema(); +DROP PROCEDURE IF EXISTS finance_fee_source_assert_schema; diff --git a/finance/src/main/resources/db/finance.sql b/finance/src/main/resources/db/finance.sql index a02b17a73..9e60d2adb 100644 --- a/finance/src/main/resources/db/finance.sql +++ b/finance/src/main/resources/db/finance.sql @@ -3,18 +3,26 @@ CREATE DATABASE IF NOT EXISTS baiyue_finance USE baiyue_finance; CREATE TABLE IF NOT EXISTS fin_fee_source ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - source_system VARCHAR(32) NOT NULL DEFAULT 'FIRSTLEG', - source_fee_id BIGINT UNSIGNED NOT NULL, - source_fee_version INT UNSIGNED NOT NULL, - customer_id BIGINT UNSIGNED NOT NULL, - order_id BIGINT UNSIGNED NULL, - inbound_id BIGINT UNSIGNED NULL, - fee_item_name VARCHAR(200) NOT NULL, - amount DECIMAL(20,6) NOT NULL, - currency_code CHAR(3) NOT NULL DEFAULT 'CNY', - status VARCHAR(16) NOT NULL DEFAULT 'AVAILABLE', - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + source_system VARCHAR(32) NOT NULL DEFAULT 'FIRSTLEG' COMMENT '费用来源系统,当前FIRSTLEG表示头程服务', + source_fee_id BIGINT UNSIGNED NOT NULL COMMENT '来源系统费用记录ID', + source_fee_version INT UNSIGNED NOT NULL COMMENT '来源费用版本号,用于固化不可变来源版本', + customer_id BIGINT UNSIGNED NOT NULL COMMENT '费用来源所属客户ID', + source_type VARCHAR(32) NOT NULL COMMENT '来源费用类型:INBOUND_FEE入库单费用、CONTAINER_FEE柜子费用', + source_no VARCHAR(64) NULL COMMENT '来源入库单号或柜号快照,可为空', + direction VARCHAR(16) NOT NULL COMMENT '费用方向:RECEIVABLE应收、PAYABLE应付', + order_id BIGINT UNSIGNED NULL COMMENT '来源头程订单ID,可为空', + inbound_id BIGINT UNSIGNED NULL COMMENT '来源入库单ID,可为空', + container_id BIGINT UNSIGNED NULL COMMENT '来源Firstleg装柜计划ID,可为空', + fee_item_code VARCHAR(64) NOT NULL COMMENT '来源费用项目编码快照', + fee_item_name VARCHAR(200) NOT NULL COMMENT '来源费用项目名称快照', + amount DECIMAL(20,6) NOT NULL COMMENT '来源费用金额,精度6位小数', + currency_code CHAR(3) NOT NULL DEFAULT 'CNY' COMMENT 'ISO 4217三位币种代码,首期仅CNY', + source_hash CHAR(64) NOT NULL COMMENT 'Firstleg费用来源完整快照SHA-256哈希', + source_snapshot JSON NOT NULL COMMENT 'Firstleg费用来源完整不可变JSON快照', + status VARCHAR(16) NOT NULL DEFAULT 'AVAILABLE' COMMENT '费用来源状态:AVAILABLE可结算、SUPERSEDED已被新版本替代、VOIDED已作废', + 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 (id), UNIQUE KEY uk_fin_fee_source (source_system, source_fee_id, source_fee_version), KEY idx_fin_fee_source_customer (customer_id, status), @@ -23,43 +31,46 @@ CREATE TABLE IF NOT EXISTS fin_fee_source ( ) ENGINE=InnoDB COMMENT='Firstleg费用来源快照池;账单人工选择并固化明细'; CREATE TABLE IF NOT EXISTS fin_inbox_event ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - event_id VARCHAR(128) NOT NULL, - source_system VARCHAR(32) NOT NULL, - event_type VARCHAR(64) NOT NULL, - payload JSON NOT NULL, - status VARCHAR(16) NOT NULL DEFAULT 'RECEIVED', - received_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - processed_at DATETIME(3) NULL, + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + event_id VARCHAR(128) NOT NULL COMMENT '来源系统事件唯一ID', + source_system VARCHAR(32) NOT NULL COMMENT '事件来源系统标识', + event_type VARCHAR(64) NOT NULL COMMENT '来源事件类型', + payload JSON NOT NULL COMMENT '来源事件原始JSON载荷快照', + payload_hash CHAR(64) NOT NULL COMMENT '规范化事件内容SHA-256哈希,用于同事件ID冲突检测', + result_resource_id BIGINT UNSIGNED NULL COMMENT '成功处理生成或定位的费用来源快照ID', + error_code VARCHAR(64) NULL COMMENT '处理失败时的稳定FIN错误码', + status VARCHAR(16) NOT NULL DEFAULT 'RECEIVED' COMMENT '事件处理状态:RECEIVED已接收、PROCESSED已完成、FAILED处理失败', + received_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '事件接收时间', + processed_at DATETIME(3) NULL COMMENT '事件处理完成时间,未完成时为空', PRIMARY KEY (id), UNIQUE KEY uk_fin_inbox_event (source_system, event_id) ) ENGINE=InnoDB COMMENT='Firstleg费用事件幂等接收记录'; CREATE TABLE IF NOT EXISTS fin_subject ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - subject_code VARCHAR(64) NOT NULL, - subject_name VARCHAR(200) NOT NULL, - status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE', - version INT UNSIGNED NOT NULL DEFAULT 0, - created_by BIGINT UNSIGNED NOT NULL DEFAULT 0, - updated_by BIGINT UNSIGNED NOT NULL DEFAULT 0, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + subject_code VARCHAR(64) NOT NULL COMMENT '平台财务主体唯一编码', + subject_name VARCHAR(200) NOT NULL COMMENT '平台收付款及核算主体名称', + status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '财务主体状态,ACTIVE表示启用', + version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', + created_by BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建操作人ID,0表示系统初始化', + updated_by BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '最后修改操作人ID,0表示系统初始化', + 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 (id), UNIQUE KEY uk_fin_subject_code (subject_code) ) ENGINE=InnoDB COMMENT='平台财务主体;首期仅一个有效主体'; CREATE TABLE IF NOT EXISTS fin_customer_settlement_profile ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', - customer_name_snapshot VARCHAR(200) NOT NULL, - credit_days INT UNSIGNED NOT NULL DEFAULT 30 COMMENT '默认30个自然日', - status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE', - version INT UNSIGNED NOT NULL DEFAULT 0, - created_by BIGINT UNSIGNED NOT NULL, - updated_by BIGINT UNSIGNED NOT NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + customer_name_snapshot VARCHAR(200) NOT NULL COMMENT '客户名称快照,客户主数据后续变更不追改', + credit_days INT UNSIGNED NOT NULL DEFAULT 30 COMMENT '客户账期自然日数,默认30天', + status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '客户结算配置状态,ACTIVE表示启用', + version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', + created_by BIGINT UNSIGNED NOT NULL COMMENT '创建操作人ID', + updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改操作人ID', + 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 (id), UNIQUE KEY uk_fin_customer_profile (tenant_id), CONSTRAINT chk_fin_customer_profile_tenant CHECK (tenant_id > 0), @@ -67,29 +78,29 @@ CREATE TABLE IF NOT EXISTS fin_customer_settlement_profile ( ) ENGINE=InnoDB COMMENT='客户结算配置;一客户一个结算对象'; CREATE TABLE IF NOT EXISTS fin_bill ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - tenant_id BIGINT UNSIGNED NOT NULL, - bill_no VARCHAR(64) NOT NULL, - subject_id BIGINT UNSIGNED NOT NULL, - customer_name_snapshot VARCHAR(200) NOT NULL, - currency_code CHAR(3) NOT NULL DEFAULT 'CNY', - bill_date DATE NOT NULL, - due_date DATE NOT NULL, - status VARCHAR(24) NOT NULL DEFAULT 'DRAFT' COMMENT 'DRAFT/ISSUED/PARTIALLY_PAID/PAID/VOIDED', - total_amount DECIMAL(20,6) NOT NULL DEFAULT 0, - received_amount DECIMAL(20,6) NOT NULL DEFAULT 0, - outstanding_amount DECIMAL(20,6) NOT NULL DEFAULT 0, - remark VARCHAR(500) NULL, - issued_at DATETIME(3) NULL, - issued_by BIGINT UNSIGNED NULL, - voided_at DATETIME(3) NULL, - voided_by BIGINT UNSIGNED NULL, - void_reason VARCHAR(500) NULL, - version INT UNSIGNED NOT NULL DEFAULT 0, - created_by BIGINT UNSIGNED NOT NULL, - updated_by BIGINT UNSIGNED NOT NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + bill_no VARCHAR(64) NOT NULL COMMENT '客户范围内唯一账单编号', + subject_id BIGINT UNSIGNED NOT NULL COMMENT '平台财务主体ID', + customer_name_snapshot VARCHAR(200) NOT NULL COMMENT '出账时客户名称快照', + currency_code CHAR(3) NOT NULL DEFAULT 'CNY' COMMENT 'ISO 4217三位币种代码,首期仅CNY', + bill_date DATE NOT NULL COMMENT '账单日期', + due_date DATE NOT NULL COMMENT '到期日期,由账单日期加客户账期计算', + status VARCHAR(24) NOT NULL DEFAULT 'DRAFT' COMMENT '账单状态:DRAFT草稿、ISSUED已出账、PARTIALLY_PAID部分收款、PAID已收清、VOIDED已作废', + total_amount DECIMAL(20,6) NOT NULL DEFAULT 0 COMMENT '账单全部明细总金额,精度6位小数', + received_amount DECIMAL(20,6) NOT NULL DEFAULT 0 COMMENT '账单累计已收金额,精度6位小数', + outstanding_amount DECIMAL(20,6) NOT NULL DEFAULT 0 COMMENT '账单未收余额,精度6位小数', + remark VARCHAR(500) NULL COMMENT '平台账单内部备注', + issued_at DATETIME(3) NULL COMMENT '出账时间,未出账时为空', + issued_by BIGINT UNSIGNED NULL COMMENT '出账操作人ID,未出账时为空', + voided_at DATETIME(3) NULL COMMENT '作废时间,未作废时为空', + voided_by BIGINT UNSIGNED NULL COMMENT '作废操作人ID,未作废时为空', + void_reason VARCHAR(500) NULL COMMENT '账单作废原因', + version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', + created_by BIGINT UNSIGNED NOT NULL COMMENT '创建操作人ID', + updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改操作人ID', + 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 (id), UNIQUE KEY uk_fin_bill_no (tenant_id, bill_no), KEY idx_fin_bill_customer_status_date (tenant_id, status, bill_date), @@ -99,20 +110,20 @@ CREATE TABLE IF NOT EXISTS fin_bill ( ) ENGINE=InnoDB COMMENT='客户CNY账单;首期账单即应收台账'; CREATE TABLE IF NOT EXISTS fin_bill_line ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - tenant_id BIGINT UNSIGNED NOT NULL, - bill_id BIGINT UNSIGNED NOT NULL, - source_type VARCHAR(32) NOT NULL DEFAULT 'FIRSTLEG_FEE', - source_id BIGINT UNSIGNED NOT NULL, - source_version INT UNSIGNED NOT NULL, - source_no VARCHAR(64) NULL, - order_id BIGINT UNSIGNED NULL, - inbound_id BIGINT UNSIGNED NULL, - fee_item_code VARCHAR(64) NOT NULL, - fee_item_name VARCHAR(200) NOT NULL, - amount DECIMAL(20,6) NOT NULL, - currency_code CHAR(3) NOT NULL DEFAULT 'CNY', - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + bill_id BIGINT UNSIGNED NOT NULL COMMENT '所属账单ID', + source_type VARCHAR(32) NOT NULL DEFAULT 'FIRSTLEG_FEE' COMMENT '费用来源类型,FIRSTLEG_FEE表示头程费用', + source_id BIGINT UNSIGNED NOT NULL COMMENT '来源费用ID', + source_version INT UNSIGNED NOT NULL COMMENT '出账固化的来源费用版本号', + source_no VARCHAR(64) NULL COMMENT '来源费用单号快照,可为空', + order_id BIGINT UNSIGNED NULL COMMENT '来源头程订单ID,可为空', + inbound_id BIGINT UNSIGNED NULL COMMENT '来源入库单ID,可为空', + fee_item_code VARCHAR(64) NOT NULL COMMENT '出账时费用项目编码快照', + fee_item_name VARCHAR(200) NOT NULL COMMENT '出账时费用项目名称快照', + amount DECIMAL(20,6) NOT NULL COMMENT '账单明细金额,精度6位小数', + currency_code CHAR(3) NOT NULL DEFAULT 'CNY' COMMENT 'ISO 4217三位币种代码,首期仅CNY', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '账单明细创建时间', PRIMARY KEY (id), UNIQUE KEY uk_fin_bill_line_source (source_type, source_id, source_version), KEY idx_fin_bill_line_bill (tenant_id, bill_id), @@ -122,26 +133,26 @@ CREATE TABLE IF NOT EXISTS fin_bill_line ( ) ENGINE=InnoDB COMMENT='账单费用来源快照明细'; CREATE TABLE IF NOT EXISTS fin_receipt ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - tenant_id BIGINT UNSIGNED NOT NULL, - receipt_no VARCHAR(64) NOT NULL, - subject_id BIGINT UNSIGNED NOT NULL, - currency_code CHAR(3) NOT NULL DEFAULT 'CNY', - received_amount DECIMAL(20,6) NOT NULL, - received_at DATETIME(3) NOT NULL, - payment_method VARCHAR(32) NULL, - voucher_no VARCHAR(128) NULL, - voucher_file_ids JSON NOT NULL COMMENT 'File服务文件ID字符串数组', - status VARCHAR(16) NOT NULL DEFAULT 'CONFIRMED' COMMENT 'CONFIRMED/VOIDED', - remark VARCHAR(500) NULL, - voided_at DATETIME(3) NULL, - voided_by BIGINT UNSIGNED NULL, - void_reason VARCHAR(500) NULL, - version INT UNSIGNED NOT NULL DEFAULT 0, - created_by BIGINT UNSIGNED NOT NULL, - updated_by BIGINT UNSIGNED NOT NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + receipt_no VARCHAR(64) NOT NULL COMMENT '客户范围内唯一收款编号', + subject_id BIGINT UNSIGNED NOT NULL COMMENT '实际收款的平台财务主体ID', + currency_code CHAR(3) NOT NULL DEFAULT 'CNY' COMMENT 'ISO 4217三位币种代码,首期仅CNY', + received_amount DECIMAL(20,6) NOT NULL COMMENT '本次实际到账金额,精度6位小数', + received_at DATETIME(3) NOT NULL COMMENT '实际到账时间', + payment_method VARCHAR(32) NULL COMMENT '收款方式编码', + voucher_no VARCHAR(128) NULL COMMENT '银行流水号或线下收款凭证号', + voucher_file_ids JSON NOT NULL COMMENT 'File服务财务凭证文件ID字符串数组JSON', + status VARCHAR(16) NOT NULL DEFAULT 'CONFIRMED' COMMENT '收款状态:CONFIRMED已确认、VOIDED已作废', + remark VARCHAR(500) NULL COMMENT '平台收款内部备注', + voided_at DATETIME(3) NULL COMMENT '收款作废时间', + voided_by BIGINT UNSIGNED NULL COMMENT '收款作废操作人ID', + void_reason VARCHAR(500) NULL COMMENT '收款作废原因', + version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', + created_by BIGINT UNSIGNED NOT NULL COMMENT '创建操作人ID', + updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改操作人ID', + 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 (id), UNIQUE KEY uk_fin_receipt_no (tenant_id, receipt_no), KEY idx_fin_receipt_customer_time (tenant_id, received_at), @@ -151,13 +162,13 @@ CREATE TABLE IF NOT EXISTS fin_receipt ( ) ENGINE=InnoDB COMMENT='平台财务人工登记收款及凭证'; CREATE TABLE IF NOT EXISTS fin_receipt_bill ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - tenant_id BIGINT UNSIGNED NOT NULL, - receipt_id BIGINT UNSIGNED NOT NULL, - bill_id BIGINT UNSIGNED NOT NULL, - applied_amount DECIMAL(20,6) NOT NULL, - created_by BIGINT UNSIGNED NOT NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + receipt_id BIGINT UNSIGNED NOT NULL COMMENT '收款记录ID', + bill_id BIGINT UNSIGNED NOT NULL COMMENT '被分配收款的账单ID', + applied_amount DECIMAL(20,6) NOT NULL COMMENT '本笔收款分配到该账单的金额,精度6位小数', + created_by BIGINT UNSIGNED NOT NULL COMMENT '分配操作人ID', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '收款分配创建时间', PRIMARY KEY (id), UNIQUE KEY uk_fin_receipt_bill_pair (receipt_id, bill_id), KEY idx_fin_receipt_bill_bill (tenant_id, bill_id), @@ -166,32 +177,32 @@ CREATE TABLE IF NOT EXISTS fin_receipt_bill ( ) ENGINE=InnoDB COMMENT='人工收款对应账单金额;UI不使用复杂核销术语'; CREATE TABLE IF NOT EXISTS fin_cost ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - tenant_id BIGINT UNSIGNED NOT NULL, - cost_no VARCHAR(64) NOT NULL, - cost_type VARCHAR(24) NOT NULL COMMENT 'SINGLE_ORDER/CONTAINER_SHARED', - order_id BIGINT UNSIGNED NULL, - inbound_id BIGINT UNSIGNED NULL, - container_id BIGINT UNSIGNED NULL, - supplier_id BIGINT UNSIGNED NULL, - supplier_name_snapshot VARCHAR(200) NULL, - fee_item_code VARCHAR(64) NOT NULL, - fee_item_name VARCHAR(200) NOT NULL, - currency_code CHAR(3) NOT NULL DEFAULT 'CNY', - cost_amount DECIMAL(20,6) NOT NULL, - voucher_file_ids JSON NOT NULL, - status VARCHAR(16) NOT NULL DEFAULT 'DRAFT' COMMENT 'DRAFT/CONFIRMED/VOIDED', - remark VARCHAR(500) NULL, - confirmed_at DATETIME(3) NULL, - confirmed_by BIGINT UNSIGNED NULL, - voided_at DATETIME(3) NULL, - voided_by BIGINT UNSIGNED NULL, - void_reason VARCHAR(500) NULL, - version INT UNSIGNED NOT NULL DEFAULT 0, - created_by BIGINT UNSIGNED NOT NULL, - updated_by BIGINT UNSIGNED NOT NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + cost_no VARCHAR(64) NOT NULL COMMENT '客户范围内唯一成本编号', + cost_type VARCHAR(24) NOT NULL COMMENT '成本归属类型:SINGLE_ORDER单票成本、CONTAINER_SHARED整柜公共成本', + order_id BIGINT UNSIGNED NULL COMMENT '成本归属的头程订单ID,单票成本必填', + inbound_id BIGINT UNSIGNED NULL COMMENT '成本关联的入库单ID,可为空', + container_id BIGINT UNSIGNED NULL COMMENT 'Firstleg装柜计划ID,整柜公共成本必填', + supplier_id BIGINT UNSIGNED NULL COMMENT 'Warehouse公共往来单位或供应商引用ID', + supplier_name_snapshot VARCHAR(200) NULL COMMENT '登记成本时供应商或成本对象名称快照', + fee_item_code VARCHAR(64) NOT NULL COMMENT '成本费用项目编码快照', + fee_item_name VARCHAR(200) NOT NULL COMMENT '成本费用项目名称快照', + currency_code CHAR(3) NOT NULL DEFAULT 'CNY' COMMENT 'ISO 4217三位币种代码,首期仅CNY', + cost_amount DECIMAL(20,6) NOT NULL COMMENT '成本总金额,精度6位小数', + voucher_file_ids JSON NOT NULL COMMENT 'File服务财务凭证文件ID字符串数组JSON', + status VARCHAR(16) NOT NULL DEFAULT 'DRAFT' COMMENT '成本状态:DRAFT草稿、CONFIRMED已确认、VOIDED已作废', + remark VARCHAR(500) NULL COMMENT '平台成本内部备注', + confirmed_at DATETIME(3) NULL COMMENT '成本确认时间', + confirmed_by BIGINT UNSIGNED NULL COMMENT '成本确认操作人ID', + voided_at DATETIME(3) NULL COMMENT '成本作废时间', + voided_by BIGINT UNSIGNED NULL COMMENT '成本作废操作人ID', + void_reason VARCHAR(500) NULL COMMENT '成本作废原因', + version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', + created_by BIGINT UNSIGNED NOT NULL COMMENT '创建操作人ID', + updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改操作人ID', + 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 (id), UNIQUE KEY uk_fin_cost_no (tenant_id, cost_no), KEY idx_fin_cost_ticket (tenant_id, order_id, inbound_id), @@ -206,17 +217,17 @@ CREATE TABLE IF NOT EXISTS fin_cost ( ) ENGINE=InnoDB COMMENT='人工成本;单票直接归属,整柜按体积分摊'; CREATE TABLE IF NOT EXISTS fin_cost_allocation ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - tenant_id BIGINT UNSIGNED NOT NULL, - cost_id BIGINT UNSIGNED NOT NULL, - order_id BIGINT UNSIGNED NOT NULL, - inbound_id BIGINT UNSIGNED NULL, - volume_cbm DECIMAL(20,6) NOT NULL, - total_volume_cbm DECIMAL(20,6) NOT NULL, - allocation_rate DECIMAL(20,10) NOT NULL, - allocated_amount DECIMAL(20,6) NOT NULL, - rounding_adjustment TINYINT NOT NULL DEFAULT 0 COMMENT '1表示尾差归集行', - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + cost_id BIGINT UNSIGNED NOT NULL COMMENT '整柜公共成本ID', + order_id BIGINT UNSIGNED NOT NULL COMMENT '参与分摊的头程订单ID', + inbound_id BIGINT UNSIGNED NULL COMMENT '参与分摊订单关联的入库单ID,可为空', + volume_cbm DECIMAL(20,6) NOT NULL COMMENT '该订单分摊时有效体积快照,单位CBM、精度6位', + total_volume_cbm DECIMAL(20,6) NOT NULL COMMENT '柜内有效订单总体积快照,单位CBM、精度6位', + allocation_rate DECIMAL(20,10) NOT NULL COMMENT '订单体积占总体积的分摊比例,精度10位', + allocated_amount DECIMAL(20,6) NOT NULL COMMENT '分摊到该订单的成本金额,精度6位', + rounding_adjustment TINYINT NOT NULL DEFAULT 0 COMMENT '尾差归集标记:0未归集、1该行为实际尾差归集行', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '不可变分摊快照创建时间', PRIMARY KEY (id), UNIQUE KEY uk_fin_cost_allocation_order (cost_id, order_id), KEY idx_fin_cost_allocation_query (tenant_id, order_id), @@ -225,50 +236,50 @@ CREATE TABLE IF NOT EXISTS fin_cost_allocation ( ) ENGINE=InnoDB COMMENT='整柜成本按有效体积分摊的可审计结果'; CREATE TABLE IF NOT EXISTS fin_bill_revision ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - tenant_id BIGINT UNSIGNED NOT NULL, - bill_id BIGINT UNSIGNED NOT NULL, - version INT UNSIGNED NOT NULL, - reason VARCHAR(500) NULL, - bill_snapshot JSON NOT NULL, - line_snapshot JSON NOT NULL, - created_by BIGINT UNSIGNED NOT NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + bill_id BIGINT UNSIGNED NOT NULL COMMENT '被调整账单ID', + version INT UNSIGNED NOT NULL COMMENT '调整前账单乐观锁版本号', + reason VARCHAR(500) NULL COMMENT '本次账单调整原因', + bill_snapshot JSON NOT NULL COMMENT '调整前账单主记录不可变JSON快照', + line_snapshot JSON NOT NULL COMMENT '调整前全部账单明细不可变JSON数组快照', + created_by BIGINT UNSIGNED NOT NULL COMMENT '执行调整的操作人ID', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '修订快照创建时间', PRIMARY KEY (id), UNIQUE KEY uk_fin_bill_revision (bill_id, version), KEY idx_fin_bill_revision_tenant (tenant_id, bill_id) ) ENGINE=InnoDB COMMENT='出账后调整前的不可变账单及明细版本快照'; CREATE TABLE IF NOT EXISTS fin_idempotency_record ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - tenant_id BIGINT UNSIGNED NOT NULL, - operator_id BIGINT UNSIGNED NOT NULL, - operation VARCHAR(64) NOT NULL, - idempotency_key VARCHAR(128) NOT NULL, - request_hash CHAR(64) NOT NULL, - status VARCHAR(16) NOT NULL COMMENT 'PROCESSING/SUCCEEDED/FAILED', - result_resource_id BIGINT UNSIGNED NULL, - error_code VARCHAR(64) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + operator_id BIGINT UNSIGNED NOT NULL COMMENT '发起幂等写操作的操作人ID', + operation VARCHAR(64) NOT NULL COMMENT '幂等隔离的财务操作类型', + idempotency_key VARCHAR(128) NOT NULL COMMENT '调用方提供的幂等键', + request_hash CHAR(64) NOT NULL COMMENT '规范化完整请求的SHA-256十六进制哈希', + status VARCHAR(16) NOT NULL COMMENT '幂等处理状态:PROCESSING处理中、SUCCEEDED成功、FAILED失败', + result_resource_id BIGINT UNSIGNED NULL COMMENT '成功操作生成或更新的业务资源ID', + error_code VARCHAR(64) NULL COMMENT '失败操作的稳定业务错误码', + 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 (id), UNIQUE KEY uk_fin_idempotency (tenant_id, operator_id, operation, idempotency_key) ) ENGINE=InnoDB COMMENT='Finance人工写操作幂等记录'; CREATE TABLE IF NOT EXISTS fin_audit_log ( - id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - tenant_id BIGINT UNSIGNED NOT NULL, - resource_type VARCHAR(32) NOT NULL, - resource_id BIGINT UNSIGNED NOT NULL, - action VARCHAR(64) NOT NULL, - reason VARCHAR(500) NULL, - before_data JSON NULL, - after_data JSON NULL, - operator_id BIGINT UNSIGNED NOT NULL, - operator_name_snapshot VARCHAR(128) NOT NULL, - request_id VARCHAR(128) NULL, - trace_id VARCHAR(128) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + resource_type VARCHAR(32) NOT NULL COMMENT '被审计财务资源类型', + resource_id BIGINT UNSIGNED NOT NULL COMMENT '被审计财务资源ID', + action VARCHAR(64) NOT NULL COMMENT '审计业务动作类型', + reason VARCHAR(500) NULL COMMENT '操作原因或说明', + before_data JSON NULL COMMENT '操作前业务数据JSON快照', + after_data JSON NULL COMMENT '操作后业务数据JSON快照', + operator_id BIGINT UNSIGNED NOT NULL COMMENT '操作人ID', + operator_name_snapshot VARCHAR(128) NOT NULL COMMENT '操作发生时的操作人名称快照', + request_id VARCHAR(128) NULL COMMENT '入口请求ID,用于问题定位', + trace_id VARCHAR(128) NULL COMMENT '分布式调用链追踪ID', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '不可变审计记录创建时间', PRIMARY KEY (id), KEY idx_fin_audit_resource (tenant_id, resource_type, resource_id, created_at), KEY idx_fin_audit_operator (operator_id, created_at) diff --git a/finance/src/test/java/com/baiyue/finance/FinanceContractTest.java b/finance/src/test/java/com/baiyue/finance/FinanceContractTest.java index 16393dc53..e5c641558 100644 --- a/finance/src/test/java/com/baiyue/finance/FinanceContractTest.java +++ b/finance/src/test/java/com/baiyue/finance/FinanceContractTest.java @@ -19,7 +19,7 @@ class FinanceContractTest { assertTrue(sql.contains("currency_code CHECK (currency_code = 'CNY')") || sql.contains("currency_code = 'CNY'")); assertTrue(sql.contains("fin_fee_source") && sql.contains("fin_inbox_event")); assertTrue(sql.contains("fin_bill_revision")); - assertTrue(sql.contains("PROCESSING/SUCCEEDED/FAILED")); + assertTrue(sql.contains("PROCESSING") && sql.contains("SUCCEEDED") && sql.contains("FAILED")); String service = Files.readString(Path.of("src/main/java/com/baiyue/finance/service/FinanceService.java")); assertTrue(service.contains("status='PROCESSING'") && service.contains("stableNo")); assertTrue(service.contains("request_hash") && service.contains("result_resource_id") && service.contains("Idempotency-Key已用于其他请求")); diff --git a/finance/src/test/java/com/baiyue/finance/FinanceExceptionHandlerTest.java b/finance/src/test/java/com/baiyue/finance/FinanceExceptionHandlerTest.java new file mode 100644 index 000000000..87581fddc --- /dev/null +++ b/finance/src/test/java/com/baiyue/finance/FinanceExceptionHandlerTest.java @@ -0,0 +1,31 @@ +package com.baiyue.finance; + +import com.baiyue.common.core.api.ResultCode; +import com.baiyue.common.core.exception.BizException; +import com.baiyue.finance.controller.FinanceExceptionHandler; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class FinanceExceptionHandlerTest { + private final FinanceExceptionHandler handler = new FinanceExceptionHandler(); + + @Test + void returnsStableFinanceConflictCodeAndTraceFields() { + var response = handler.business(new BizException(ResultCode.CONFLICT, "幂等键冲突")); + + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().code()).isEqualTo("FIN_409"); + assertThat(response.getBody().requestId()).isNotBlank(); + assertThat(response.getBody().traceId()).isNotBlank(); + } + + @Test + void dependencyFailureKeepsInfrastructureHttpStatus() { + var response = handler.business(new BizException(ResultCode.SERVICE_UNAVAILABLE, "File服务不可用")); + + assertThat(response.getStatusCode().value()).isEqualTo(503); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().code()).isEqualTo("FIN_503"); + } +} diff --git a/finance/src/test/java/com/baiyue/finance/FinanceFeePoolDirectionBehaviorTest.java b/finance/src/test/java/com/baiyue/finance/FinanceFeePoolDirectionBehaviorTest.java new file mode 100644 index 000000000..0b87371e7 --- /dev/null +++ b/finance/src/test/java/com/baiyue/finance/FinanceFeePoolDirectionBehaviorTest.java @@ -0,0 +1,67 @@ +package com.baiyue.finance; + +import com.baiyue.finance.dto.FinanceReadModels.FeePoolItem; +import com.baiyue.finance.service.FinanceQueryService; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class FinanceFeePoolDirectionBehaviorTest { + @Test + void receivablePoolNeverContainsPayableSource() { + DirectionJdbc jdbc = new DirectionJdbc(); + FinanceQueryService service = new FinanceQueryService(jdbc, new ObjectMapper()); + + List result = service.feePool(7L, "RECEIVABLE"); + + assertThat(result).isNotEmpty().allMatch(item -> "RECEIVABLE".equals(item.direction())); + assertThat(jdbc.sourceDirections).containsExactly("RECEIVABLE"); + assertThat(jdbc.costQueryCount).isZero(); + } + + @Test + void payablePoolNeverContainsReceivableSource() { + DirectionJdbc jdbc = new DirectionJdbc(); + FinanceQueryService service = new FinanceQueryService(jdbc, new ObjectMapper()); + + List result = service.feePool(7L, "PAYABLE"); + + assertThat(result).isNotEmpty().allMatch(item -> "PAYABLE".equals(item.direction())); + assertThat(jdbc.sourceDirections).containsExactly("PAYABLE"); + assertThat(jdbc.costQueryCount).isEqualTo(1); + } + + private static final class DirectionJdbc extends JdbcTemplate { + private final List sourceDirections = new ArrayList<>(); + private int costQueryCount; + + @Override + @SuppressWarnings("unchecked") + public List query(String sql, RowMapper mapper, Object... args) { + if (sql.contains("FROM fin_fee_source")) { + String direction = String.valueOf(args[1]); + sourceDirections.add(direction); + return (List) List.of(item(direction, "FIRSTLEG_FEE", 1L)); + } + if (sql.contains("FROM fin_cost")) { + costQueryCount++; + return (List) List.of(item("PAYABLE", "MANUAL_COST", 2L)); + } + return List.of(); + } + + private static FeePoolItem item(String direction, String sourceType, long sourceId) { + return new FeePoolItem(direction + ":" + sourceId, direction, sourceType, sourceId, 1, + 11L, 21L, null, "清关费", new BigDecimal("100.00"), "CNY", "AVAILABLE", + LocalDateTime.of(2026, 8, 11, 1, 0)); + } + } +} diff --git a/finance/src/test/java/com/baiyue/finance/FinanceFeeSourceEventServiceTest.java b/finance/src/test/java/com/baiyue/finance/FinanceFeeSourceEventServiceTest.java new file mode 100644 index 000000000..507a948ef --- /dev/null +++ b/finance/src/test/java/com/baiyue/finance/FinanceFeeSourceEventServiceTest.java @@ -0,0 +1,197 @@ +package com.baiyue.finance; + +import com.baiyue.common.core.api.ApiResponse; +import com.baiyue.finance.client.FirstlegFeeSourceClient; +import com.baiyue.finance.dto.FinanceFeeSourceEvent; +import com.baiyue.finance.service.FinanceEventException; +import com.baiyue.finance.service.FinanceFeeSourceEventService; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.util.ReflectionTestUtils; + +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.HexFormat; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +class FinanceFeeSourceEventServiceTest { + private final JdbcTemplate jdbc = mock(JdbcTemplate.class); + private final FirstlegFeeSourceClient firstleg = mock(FirstlegFeeSourceClient.class); + private final ObjectMapper mapper = new ObjectMapper().findAndRegisterModules(); + private final FinanceFeeSourceEventService service = new FinanceFeeSourceEventService(jdbc, firstleg, mapper); + + @BeforeEach + void setUp() { + ReflectionTestUtils.setField(service, "internalToken", "test-token"); + } + + @Test + void importsApprovedSettlementInboundFee() { + FinanceFeeSourceEvent event = inboundEvent("published-1", "FIRSTLEG_FEE_PUBLISHED", 1, 7); + givenNewEvent(); + when(firstleg.inbound("test-token", 21, 31)).thenReturn(ApiResponse.success(snapshot("INBOUND_FEE", 7, 1, 21L, null))); + + assertThat(service.consume(event)).isEqualTo(101L); + verify(firstleg).inbound("test-token", 21, 31); + verify(jdbc).update(startsWith("INSERT INTO fin_fee_source"), any(Object[].class)); + } + + @Test + void importsApprovedSettlementContainerFeeWithCustomerOwnership() { + FinanceFeeSourceEvent event = containerEvent("container-1", "FIRSTLEG_FEE_PUBLISHED", 1, 7); + givenNewEvent(); + when(firstleg.container("test-token", 41, 31)).thenReturn(ApiResponse.success(snapshot("CONTAINER_FEE", 7, 1, null, 41L))); + + assertThat(service.consume(event)).isEqualTo(101L); + verify(firstleg).container("test-token", 41, 31); + verify(jdbc).update(contains("status='SUPERSEDED'"), eq(31L)); + } + + @Test + void adjustmentCreatesNewImmutableVersionAndSupersedesAvailableVersion() { + FinanceFeeSourceEvent event = inboundEvent("adjusted-2", "FIRSTLEG_FEE_ADJUSTED", 2, 7); + givenNewEvent(); + when(firstleg.inbound(anyString(), eq(21L), eq(31L))).thenReturn(ApiResponse.success(snapshot("INBOUND_FEE", 7, 2, 21L, null))); + + assertThat(service.consume(event)).isEqualTo(101L); + verify(jdbc).update(contains("status='SUPERSEDED'"), eq(31L)); + verify(jdbc).update(startsWith("INSERT INTO fin_fee_source"), any(Object[].class)); + } + + @Test + void voidMarksExistingVersionWithoutDeletingHistory() { + FinanceFeeSourceEvent event = inboundEvent("voided-2", "FIRSTLEG_FEE_VOIDED", 2, 7); + when(jdbc.queryForList(startsWith("SELECT payload_hash,status"), eq("FIRSTLEG"), eq("voided-2"))).thenReturn(List.of()); + when(firstleg.inbound(anyString(), eq(21L), eq(31L))).thenReturn(ApiResponse.success(snapshot("INBOUND_FEE", 7, 2, 21L, null))); + when(jdbc.queryForList(startsWith("SELECT id,source_hash,status"), eq(31L), eq(2))) + .thenReturn(List.of(Map.of("id", 88L, "source_hash", "source-hash", "status", "AVAILABLE"))); + + assertThat(service.consume(event)).isEqualTo(88L); + verify(jdbc).update("UPDATE fin_fee_source SET status='VOIDED' WHERE id=?", 88L); + verify(jdbc, never()).update(startsWith("DELETE"), any(Object[].class)); + } + + @Test + void sameEventAndContentReturnsFirstResourceWithoutCallingFirstlegAgain() throws Exception { + FinanceFeeSourceEvent event = inboundEvent("same-1", "FIRSTLEG_FEE_PUBLISHED", 1, 7); + String requestHash = sha256(mapper.writeValueAsString(event)); + when(jdbc.queryForList(startsWith("SELECT payload_hash,status"), eq("FIRSTLEG"), eq("same-1"))) + .thenReturn(List.of(Map.of("payload_hash", requestHash, "status", "PROCESSED", "result_resource_id", 66L))); + + assertThat(service.consume(event)).isEqualTo(66L); + verifyNoInteractions(firstleg); + } + + @Test + void sameEventIdWithDifferentContentReturnsStableConflictCode() { + FinanceFeeSourceEvent event = inboundEvent("conflict-1", "FIRSTLEG_FEE_PUBLISHED", 1, 7); + when(jdbc.queryForList(startsWith("SELECT payload_hash,status"), eq("FIRSTLEG"), eq("conflict-1"))) + .thenReturn(List.of(Map.of("payload_hash", "different", "status", "PROCESSED", "result_resource_id", 66L))); + + assertThatThrownBy(() -> service.consume(event)) + .isInstanceOf(FinanceEventException.class) + .extracting(error -> ((FinanceEventException) error).code()).isEqualTo("FIN-EVT-002"); + } + + @Test + void rejectsSnapshotBelongingToAnotherCustomer() { + FinanceFeeSourceEvent event = inboundEvent("tenant-1", "FIRSTLEG_FEE_PUBLISHED", 1, 7); + when(jdbc.queryForList(startsWith("SELECT payload_hash,status"), eq("FIRSTLEG"), eq("tenant-1"))).thenReturn(List.of()); + when(firstleg.inbound(anyString(), eq(21L), eq(31L))).thenReturn(ApiResponse.success(snapshot("INBOUND_FEE", 8, 1, 21L, null))); + + assertThatThrownBy(() -> service.consume(event)) + .isInstanceOf(FinanceEventException.class) + .extracting(error -> ((FinanceEventException) error).code()).isEqualTo("FIN-EVT-007"); + verify(jdbc, never()).update(startsWith("INSERT INTO fin_fee_source"), any(Object[].class)); + } + + @Test + void rejectsApprovedButUnpublishedNonVoidedFee() { + FinanceFeeSourceEvent event = inboundEvent("draft-1", "FIRSTLEG_FEE_PUBLISHED", 1, 7); + when(jdbc.queryForList(startsWith("SELECT payload_hash,status"), eq("FIRSTLEG"), eq("draft-1"))).thenReturn(List.of()); + when(firstleg.inbound(anyString(), eq(21L), eq(31L))).thenReturn( + ApiResponse.success(snapshot("INBOUND_FEE", 7, 1, 21L, null, "DRAFT", new BigDecimal("100.00")))); + + assertThatThrownBy(() -> service.consume(event)) + .isInstanceOf(FinanceEventException.class) + .extracting(error -> ((FinanceEventException) error).code()).isEqualTo("FIN-EVT-015"); + verify(jdbc, never()).update(startsWith("INSERT INTO fin_fee_source"), any(Object[].class)); + } + + @Test + void rejectsZeroAmount() { + assertInvalidAmount("zero-1", BigDecimal.ZERO); + } + + @Test + void rejectsNegativeAmount() { + assertInvalidAmount("negative-1", new BigDecimal("-0.01")); + } + + private void givenNewEvent() { + when(jdbc.queryForList(startsWith("SELECT payload_hash,status"), eq("FIRSTLEG"), anyString())).thenReturn(List.of()); + when(jdbc.queryForList(startsWith("SELECT id,source_hash,status"), eq(31L), anyInt())).thenReturn(List.of()); + when(jdbc.queryForObject(startsWith("SELECT id FROM fin_fee_source"), eq(Long.class), eq(31L), anyInt())).thenReturn(101L); + } + + private FinanceFeeSourceEvent inboundEvent(String id, String type, int version, long tenant) { + return event(id, type, version, tenant, new FinanceFeeSourceEvent.Payload("21", null, "31", version)); + } + + private FinanceFeeSourceEvent containerEvent(String id, String type, int version, long tenant) { + return event(id, type, version, tenant, new FinanceFeeSourceEvent.Payload(null, "41", "31", version)); + } + + private FinanceFeeSourceEvent event(String id, String type, int version, long tenant, FinanceFeeSourceEvent.Payload payload) { + try { + return new FinanceFeeSourceEvent(id, type, 1, "firstleg-service", LocalDateTime.of(2026, 8, 11, 1, 2), + String.valueOf(tenant), payload.inboundId() == null ? "CONTAINER" : "INBOUND", + payload.inboundId() == null ? payload.containerId() : payload.inboundId(), version, + id, sha256(mapper.writeValueAsString(payload)), payload); + } catch (Exception error) { + throw new AssertionError(error); + } + } + + private FirstlegFeeSourceClient.Snapshot snapshot(String sourceType, long customerId, int version, Long inboundId, Long containerId) { + return snapshot(sourceType, customerId, version, inboundId, containerId, "PUBLISHED", new BigDecimal("100.00")); + } + + private FirstlegFeeSourceClient.Snapshot snapshot(String sourceType, long customerId, int version, + Long inboundId, Long containerId, + String publicationStatus, BigDecimal amount) { + return new FirstlegFeeSourceClient.Snapshot(customerId, sourceType, 31, version, inboundId, + inboundId == null ? null : "IB-21", 51L, containerId, null, + containerId == null ? null : "C-41", customerId, "客户", 61L, "CUSTOMS", + "清关费", "RECEIVABLE", LocalDate.of(2026, 8, 11), "CNY", amount, + "APPROVED", publicationStatus, true, LocalDateTime.of(2026, 8, 11, 1, 0), + BigDecimal.ONE, "SHIPMENT", BigDecimal.TEN, BigDecimal.ONE, "source-hash", version); + } + + private void assertInvalidAmount(String eventId, BigDecimal amount) { + FinanceFeeSourceEvent event = inboundEvent(eventId, "FIRSTLEG_FEE_PUBLISHED", 1, 7); + when(jdbc.queryForList(startsWith("SELECT payload_hash,status"), eq("FIRSTLEG"), eq(eventId))).thenReturn(List.of()); + when(firstleg.inbound(anyString(), eq(21L), eq(31L))).thenReturn( + ApiResponse.success(snapshot("INBOUND_FEE", 7, 1, 21L, null, "PUBLISHED", amount))); + + assertThatThrownBy(() -> service.consume(event)) + .isInstanceOf(FinanceEventException.class) + .extracting(error -> ((FinanceEventException) error).code()).isEqualTo("FIN-EVT-010"); + verify(jdbc, never()).update(startsWith("INSERT INTO fin_fee_source"), any(Object[].class)); + } + + private static String sha256(String value) throws Exception { + return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8))); + } +} diff --git a/finance/src/test/java/com/baiyue/finance/FinanceFeeSourceMigrationContractTest.java b/finance/src/test/java/com/baiyue/finance/FinanceFeeSourceMigrationContractTest.java new file mode 100644 index 000000000..7c1aa8669 --- /dev/null +++ b/finance/src/test/java/com/baiyue/finance/FinanceFeeSourceMigrationContractTest.java @@ -0,0 +1,20 @@ +package com.baiyue.finance; + +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.assertj.core.api.Assertions.assertThat; + +class FinanceFeeSourceMigrationContractTest { + @Test + void moduleMigrationAddsImmutableSourceAndInboxIdempotencyFields() throws Exception { + String sql = Files.readString(Path.of("src/main/resources/db/fee-source-event-migration.sql"), StandardCharsets.UTF_8); + assertThat(sql).contains("information_schema.columns", "source_type", "direction", "container_id", + "source_hash", "source_snapshot", "payload_hash", "result_resource_id", "error_code"); + assertThat(sql).contains("AVAILABLE可结算", "SUPERSEDED已被新版本替代", "VOIDED已作废"); + assertThat(sql).contains("finance_fee_source_assert_schema"); + } +} diff --git a/finance/src/test/java/com/baiyue/finance/FinanceReadApiBehaviorTest.java b/finance/src/test/java/com/baiyue/finance/FinanceReadApiBehaviorTest.java new file mode 100644 index 000000000..98e87e1bd --- /dev/null +++ b/finance/src/test/java/com/baiyue/finance/FinanceReadApiBehaviorTest.java @@ -0,0 +1,78 @@ +package com.baiyue.finance; + +import com.baiyue.common.security.AuthenticatedUser; +import com.baiyue.finance.client.FinanceFileClient; +import com.baiyue.finance.client.FirstlegCostSnapshotClient; +import com.baiyue.finance.controller.ClientFinanceController; +import com.baiyue.finance.controller.PlatformFinanceController; +import com.baiyue.finance.dto.FinanceReadModels.ClientReceipt; +import com.baiyue.finance.dto.FinanceReadModels.ClientReceiptAllocation; +import com.baiyue.finance.service.FinanceQueryService; +import com.baiyue.finance.service.FinanceService; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class FinanceReadApiBehaviorTest { + @Mock FinanceService service; + @Mock FinanceQueryService queryService; + @Mock FinanceFileClient fileClient; + @Mock FirstlegCostSnapshotClient snapshotClient; + + @Test + void clientReceiptUsesAuthenticatedCustomerAndDoesNotExposeVoucherOrInternalFields() throws Exception { + AuthenticatedUser user = new AuthenticatedUser(7L, 23L, null, null, "client", "MERCHANT", "WEB", Set.of()); + ClientReceipt row = new ClientReceipt(4L, "RCPT-4", new BigDecimal("300.00"), LocalDateTime.of(2026, 8, 11, 10, 0), + "BANK_TRANSFER", "CNY", "CONFIRMED", List.of(new ClientReceiptAllocation(8L, "BILL-8", new BigDecimal("100.00")))); + when(queryService.clientReceipts(23L)).thenReturn(List.of(row)); + + var response = new ClientFinanceController(service, queryService).receipts(user); + + verify(queryService).clientReceipts(23L); + String json = new ObjectMapper().findAndRegisterModules().writeValueAsString(response.data()); + assertThat(json).doesNotContainIgnoringCase("voucher").doesNotContain("createdBy").doesNotContain("updatedBy").doesNotContain("tenantId"); + assertThat(response.requestId()).isNotBlank(); + assertThat(response.traceId()).isNotBlank(); + + new ClientFinanceController(service, queryService).bill(user, 88L); + verify(queryService).statement(23L, 88L, true); + } + + @Test + void platformFeePoolPassesExplicitCustomerScopeAndReturnsTraceFields() { + var controller = new PlatformFinanceController(service, fileClient, snapshotClient, queryService); + + var response = controller.feePool(31L, "RECEIVABLE"); + + verify(queryService).feePool(31L, "RECEIVABLE"); + assertThat(response.requestId()).isNotBlank(); + assertThat(response.traceId()).isNotBlank(); + } + + @Test + void fivePlatformPageMethodsUseTypedQueryFacade() { + var controller = new PlatformFinanceController(service, fileClient, snapshotClient, queryService); + + controller.customerAccount(11L); + controller.statements(11L); + controller.receivableSettlements(11L); + controller.payableSettlements(11L); + + verify(queryService).customerAccount(11L); + verify(queryService).statements(11L, false); + verify(queryService).receivableSettlements(11L, false); + verify(queryService).payableSettlements(11L); + } +} diff --git a/finance/src/test/java/com/baiyue/finance/FinanceServiceValidationTest.java b/finance/src/test/java/com/baiyue/finance/FinanceServiceValidationTest.java index acfc6717b..097386b4e 100644 --- a/finance/src/test/java/com/baiyue/finance/FinanceServiceValidationTest.java +++ b/finance/src/test/java/com/baiyue/finance/FinanceServiceValidationTest.java @@ -3,6 +3,8 @@ package com.baiyue.finance; import com.baiyue.finance.service.FinanceService; import com.baiyue.finance.client.FinanceFileClient; import com.baiyue.common.core.api.ApiResponse; +import com.baiyue.common.core.api.ResultCode; +import com.baiyue.common.core.exception.BizException; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.springframework.jdbc.core.JdbcTemplate; @@ -71,6 +73,19 @@ class FinanceServiceValidationTest { assertThrows(RuntimeException.class, () -> service.createReceipt(1L, 2L, request, "k3")); } + @Test + void receiptFailsClosedWhenFileValidationCapabilityIsUnavailable() { + JdbcTemplate jdbc = mock(JdbcTemplate.class); + FinanceService service = new FinanceService(jdbc, new ObjectMapper()); + var request = new FinanceService.ReceiptRequest("R-FILE", 1L, null, "BANK_TRANSFER", null, List.of("77"), null, + List.of(new FinanceService.ReceiptAllocation(10L, BigDecimal.ONE))); + + BizException error = assertThrows(BizException.class, () -> service.createReceipt(9L, 2L, request, "file-down")); + + assertEquals(ResultCode.SERVICE_UNAVAILABLE, error.getResultCode()); + verify(jdbc, never()).update(startsWith("INSERT INTO fin_receipt"), (Object[]) any()); + } + @Test void rejectsVoucherFromAnotherCustomer() { JdbcTemplate jdbc = mock(JdbcTemplate.class); diff --git a/finance/src/test/java/com/baiyue/finance/FinanceSqlCommentContractTest.java b/finance/src/test/java/com/baiyue/finance/FinanceSqlCommentContractTest.java new file mode 100644 index 000000000..92830596e --- /dev/null +++ b/finance/src/test/java/com/baiyue/finance/FinanceSqlCommentContractTest.java @@ -0,0 +1,60 @@ +package com.baiyue.finance; + +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +import static org.assertj.core.api.Assertions.assertThat; + +class FinanceSqlCommentContractTest { + private static final Pattern TABLE = Pattern.compile("CREATE TABLE IF NOT EXISTS (fin_[a-z0-9_]+) "); + private static final Pattern COLUMN = Pattern.compile("^\\s{2}([a-z][a-z0-9_]*)\\s+(?:BIGINT|INT|VARCHAR|CHAR|DECIMAL|DATETIME|DATE|JSON|TINYINT)\\b", Pattern.CASE_INSENSITIVE); + private static final Pattern CHINESE = Pattern.compile("[\\p{IsHan}]"); + + @Test + void everyFinanceBaselineColumnHasMeaningfulChineseComment() throws Exception { + List lines = Files.readAllLines(Path.of("src/main/resources/db/finance.sql"), StandardCharsets.UTF_8); + int tableCount = 0; + int columnCount = 0; + String currentTable = null; + List missing = new ArrayList<>(); + for (String line : lines) { + var table = TABLE.matcher(line); + if (table.find()) { + currentTable = table.group(1); + tableCount++; + continue; + } + if (currentTable == null) continue; + if (line.startsWith(") ENGINE=")) { + currentTable = null; + continue; + } + var column = COLUMN.matcher(line); + if (!column.find()) continue; + columnCount++; + int comment = line.indexOf("COMMENT '"); + if (comment < 0 || !CHINESE.matcher(line.substring(comment)).find()) { + missing.add(currentTable + "." + column.group(1)); + } + } + assertThat(tableCount).isEqualTo(13); + assertThat(columnCount).isEqualTo(184); + assertThat(missing).as("缺少中文业务注释的Finance字段").isEmpty(); + } + + @Test + void keyIsolationStatusSnapshotAndIdempotencyCommentsUseFrozenMeaning() throws Exception { + String sql = Files.readString(Path.of("src/main/resources/db/finance.sql"), StandardCharsets.UTF_8); + assertThat(sql).contains("tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键'"); + assertThat(sql).contains("AVAILABLE可结算、SUPERSEDED已被新版本替代、VOIDED已作废"); + assertThat(sql).contains("RECEIVED已接收、PROCESSED已完成、FAILED处理失败"); + assertThat(sql).contains("Firstleg费用来源完整不可变JSON快照"); + assertThat(sql).contains("规范化完整请求的SHA-256十六进制哈希"); + } +} diff --git a/finance/src/test/java/com/baiyue/finance/InternalFinanceFeeSourceControllerTest.java b/finance/src/test/java/com/baiyue/finance/InternalFinanceFeeSourceControllerTest.java new file mode 100644 index 000000000..256c38235 --- /dev/null +++ b/finance/src/test/java/com/baiyue/finance/InternalFinanceFeeSourceControllerTest.java @@ -0,0 +1,43 @@ +package com.baiyue.finance; + +import com.baiyue.finance.controller.InternalFinanceFeeSourceController; +import com.baiyue.finance.dto.FinanceFeeSourceEvent; +import com.baiyue.finance.service.FinanceEventException; +import com.baiyue.finance.service.FinanceFeeSourceEventService; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class InternalFinanceFeeSourceControllerTest { + private final FinanceFeeSourceEventService service = mock(FinanceFeeSourceEventService.class); + private final InternalFinanceFeeSourceController controller = controller(); + + @Test + void successfulAcknowledgementAlwaysHasTrackingFields() { + FinanceFeeSourceEvent event = mock(FinanceFeeSourceEvent.class); + when(service.consume(event)).thenReturn(91L); + + var response = controller.consume("internal-token", event); + + assertThat(response.data()).containsEntry("feeSourceId", 91L); + assertThat(response.requestId()).isNotBlank(); + assertThat(response.traceId()).isNotBlank(); + } + + @Test + void missingInternalTokenFailsClosedWithStableCode() { + assertThatThrownBy(() -> controller.consume(null, mock(FinanceFeeSourceEvent.class))) + .isInstanceOf(FinanceEventException.class) + .extracting(error -> ((FinanceEventException) error).code()).isEqualTo("FIN-EVT-401"); + } + + private InternalFinanceFeeSourceController controller() { + InternalFinanceFeeSourceController value = new InternalFinanceFeeSourceController(service); + ReflectionTestUtils.setField(value, "expectedToken", "internal-token"); + return value; + } +} diff --git a/firstleg/AGENTS.md b/firstleg/AGENTS.md index ce12366d8..3c3844a9b 100644 --- a/firstleg/AGENTS.md +++ b/firstleg/AGENTS.md @@ -17,6 +17,9 @@ | FBA 主数据/计费 | `controller/PlatformFbaMasterDataController.java`、`PlatformFbaPricingController.java`、`service/FbaPricingService.java` | | 客户费用查询 | `controller/MerchantInboundBillingController.java`、`service/MerchantInboundBillingService.java` | | 费用项目与价格簿 | `controller/PlatformFeeItemController.java`、`PlatformPriceBookController.java`、`MerchantPriceBookController.java` | +| 财务费用来源快照 | `controller/InternalFinanceFeeSourceController.java`;入库费用与柜次费用分别按来源 ID、版本向 Finance 提供不可变快照 | +| 柜次费用明细 | `controller/PlatformContainerFeeDetailController.java` 及相邻 Service/Entity | +| 财务整柜成本分摊快照 | `loading/controller/InternalFinanceCostSnapshotController.java`、`loading/service/FinanceCostSnapshotService.java` | | 分柜与预装 | `split/controller/PlatformSplitPreloadController.java`、`controller/PlatformLoadingListController.java` | | 货柜与智能排柜 | `controller/PlatformContainerController.java`、`PlatformSmartLoadingController.java`、`loading/` | | PDA 拣货与装柜 | `controller/PlatformLoadingPdaController.java`、`loading/controller/PlatformLoadingDeliveryController.java` | @@ -33,6 +36,7 @@ - 平台接口必须校验目标客户、权限、乐观锁、幂等键、历史和审计;商户查询只能使用登录态客户。 - 第三方回传必须签名、时间窗、防重放、请求哈希,并绑定订单货物和 File 证据;不得信任外部客户字段。 - 测量审核→计费确认→费用发布→入库完成不得跳过门禁;状态或 DTO 变更时逐个检查 Order/Warehouse/File/Integration 消费者。 +- Firstleg 只拥有业务费用及履约事实;Finance 拥有费用入池、账期、账单、收款、成本分摊结果和财务状态。修改费用来源版本、发布条件、柜次归属或体积快照时同步检查 `../finance/src/main/java/com/baiyue/finance/client/FirstlegFeeSourceClient.java`、同目录 `FirstlegCostSnapshotClient.java` 及双方契约测试,不在 Firstleg 复制财务状态机。 ## 三期装柜与 PDA 资源结算 diff --git a/firstleg/src/main/java/com/baiyue/firstleg/BaiyueFirstlegApplication.java b/firstleg/src/main/java/com/baiyue/firstleg/BaiyueFirstlegApplication.java index 8110dda33..a23f10e2a 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/BaiyueFirstlegApplication.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/BaiyueFirstlegApplication.java @@ -4,6 +4,7 @@ import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.scheduling.annotation.EnableScheduling; /** * 头程履约微服务启动入口,负责装配履约任务组件并扫描 MyBatis-Plus Mapper。 @@ -19,6 +20,7 @@ import org.springframework.cloud.openfeign.EnableFeignClients; "com.baiyue.firstleg.customs.mapper" }) @EnableFeignClients(basePackages = "com.baiyue.firstleg") +@EnableScheduling @SpringBootApplication(scanBasePackages = "com.baiyue") public class BaiyueFirstlegApplication { public static void main(String[] args) { SpringApplication.run(BaiyueFirstlegApplication.class, args); } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/client/FinanceFeeSourceEventClient.java b/firstleg/src/main/java/com/baiyue/firstleg/client/FinanceFeeSourceEventClient.java new file mode 100644 index 000000000..f1087295a --- /dev/null +++ b/firstleg/src/main/java/com/baiyue/firstleg/client/FinanceFeeSourceEventClient.java @@ -0,0 +1,22 @@ +package com.baiyue.firstleg.client; + +import com.baiyue.common.core.api.ApiResponse; +import com.baiyue.firstleg.dto.FinanceFeeSourceEvent; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import com.fasterxml.jackson.annotation.JsonProperty; + +@FeignClient(name = "finance-service", contextId = "firstlegFinanceFeeSourceEventClient", + url = "${baiyue.clients.finance.url:}", path = "/internal/finance/v1/fee-source-events") +public interface FinanceFeeSourceEventClient { + @PostMapping + ApiResponse receive( + @RequestHeader("X-Internal-Service-Token") String internalToken, + @RequestHeader("Idempotency-Key") String idempotencyKey, + @RequestBody FinanceFeeSourceEvent event); + + record Acknowledgement(@JsonProperty("feeSourceId") Long feeSourceId) { + } +} diff --git a/firstleg/src/main/java/com/baiyue/firstleg/controller/InternalFinanceFeeSourceController.java b/firstleg/src/main/java/com/baiyue/firstleg/controller/InternalFinanceFeeSourceController.java new file mode 100644 index 000000000..3c0c116ba --- /dev/null +++ b/firstleg/src/main/java/com/baiyue/firstleg/controller/InternalFinanceFeeSourceController.java @@ -0,0 +1,50 @@ +package com.baiyue.firstleg.controller; + +import com.baiyue.common.core.api.ApiResponse; +import com.baiyue.common.core.api.ResultCode; +import com.baiyue.common.core.exception.BizException; +import com.baiyue.firstleg.dto.FinanceFeeSourceSnapshot; +import com.baiyue.firstleg.service.FinanceFeeSourceSnapshotService; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/internal/firstleg/v1") +public class InternalFinanceFeeSourceController { + private final FinanceFeeSourceSnapshotService service; + @Value("${baiyue.security.internal-service-token}") + private String expectedToken; + + @GetMapping("/inbounds/{inboundId}/fees/{feeId}/source-snapshot") + public ApiResponse inbound( + @RequestHeader(value = "X-Internal-Service-Token", required = false) String token, + @PathVariable long inboundId, @PathVariable long feeId) { + authenticate(token); + return ApiResponse.success(service.inbound(inboundId, feeId)); + } + + @GetMapping("/containers/{containerId}/fees/{feeId}/source-snapshot") + public ApiResponse container( + @RequestHeader(value = "X-Internal-Service-Token", required = false) String token, + @PathVariable long containerId, @PathVariable long feeId) { + authenticate(token); + return ApiResponse.success(service.container(containerId, feeId)); + } + + private void authenticate(String token) { + if (token == null || expectedToken == null || expectedToken.isBlank() + || !MessageDigest.isEqual(token.getBytes(StandardCharsets.UTF_8), + expectedToken.getBytes(StandardCharsets.UTF_8))) { + throw new BizException(ResultCode.FORBIDDEN, "内部服务令牌无效"); + } + } +} diff --git a/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformContainerFeeDetailController.java b/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformContainerFeeDetailController.java new file mode 100644 index 000000000..e545498d0 --- /dev/null +++ b/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformContainerFeeDetailController.java @@ -0,0 +1,93 @@ +package com.baiyue.firstleg.controller; + +import com.baiyue.common.audit.BusinessAudit; +import com.baiyue.common.core.api.ApiResponse; +import com.baiyue.common.security.AuthenticatedUser; +import com.baiyue.firstleg.dto.ContainerFeeDetailDtos.AdjustRequest; +import com.baiyue.firstleg.dto.ContainerFeeDetailDtos.RejectRequest; +import com.baiyue.firstleg.dto.ContainerFeeDetailDtos.SaveRequest; +import com.baiyue.firstleg.dto.ContainerFeeDetailDtos.VersionRequest; +import com.baiyue.firstleg.entity.ContainerFeeDetailEntity; +import com.baiyue.firstleg.service.ContainerFeeDetailService; +import com.baiyue.firstleg.service.FirstlegScopeGuard; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/platform/v1/loading-plans/{loadingPlanId}/fee-details") +public class PlatformContainerFeeDetailController { + private final ContainerFeeDetailService service; + + @GetMapping + @PreAuthorize("hasAuthority('firstleg:inbound:fee-detail:view')") + public ApiResponse> list( + @AuthenticationPrincipal AuthenticatedUser user, @RequestParam long accountId, + @PathVariable long loadingPlanId) { + return ApiResponse.success(service.list(FirstlegScopeGuard.platform(user, accountId), + loadingPlanId)); + } + + @PostMapping + @PreAuthorize("hasAuthority('firstleg:inbound:fee-detail:edit')") + @BusinessAudit(event = "CONTAINER_FEE_DETAIL_CREATE", resourceType = "LOADING_PLAN", resourceId = "#loadingPlanId", captureBefore = false) + public ApiResponse create( + @AuthenticationPrincipal AuthenticatedUser user, @RequestParam long accountId, + @PathVariable long loadingPlanId, @Valid @RequestBody SaveRequest request) { + return ApiResponse.success(service.create(FirstlegScopeGuard.platform(user, accountId), + loadingPlanId, user.userId(), request)); + } + + @PostMapping("/{feeId}/approve") + @PreAuthorize("hasAuthority('firstleg:inbound:fee-detail:review')") + @BusinessAudit(event = "CONTAINER_FEE_DETAIL_APPROVE", resourceType = "CONTAINER_FEE_DETAIL", resourceId = "#feeId") + public ApiResponse approve(@AuthenticationPrincipal AuthenticatedUser user, + @RequestParam long accountId, + @PathVariable long loadingPlanId, + @PathVariable long feeId) { + service.approve(FirstlegScopeGuard.platform(user, accountId), loadingPlanId, feeId, + user.userId(), user.username()); + return ApiResponse.success(null); + } + + @PostMapping("/{feeId}/reject") + @PreAuthorize("hasAuthority('firstleg:inbound:fee-detail:review')") + @BusinessAudit(event = "CONTAINER_FEE_DETAIL_REJECT", resourceType = "CONTAINER_FEE_DETAIL", resourceId = "#feeId") + public ApiResponse reject(@AuthenticationPrincipal AuthenticatedUser user, + @RequestParam long accountId, + @PathVariable long loadingPlanId, + @PathVariable long feeId, + @Valid @RequestBody RejectRequest request) { + service.reject(FirstlegScopeGuard.platform(user, accountId), loadingPlanId, feeId, + user.userId(), user.username(), request.reason()); + return ApiResponse.success(null); + } + + @PostMapping("/{feeId}/adjust") + @PreAuthorize("hasAuthority('firstleg:inbound:fee-detail:edit')") + @BusinessAudit(event = "CONTAINER_FEE_DETAIL_ADJUST", resourceType = "CONTAINER_FEE_DETAIL", resourceId = "#feeId") + public ApiResponse adjust( + @AuthenticationPrincipal AuthenticatedUser user, @RequestParam long accountId, + @PathVariable long loadingPlanId, @PathVariable long feeId, + @Valid @RequestBody AdjustRequest request) { + return ApiResponse.success(service.adjust(FirstlegScopeGuard.platform(user, accountId), + loadingPlanId, feeId, request.expectedSourceVersion(), user.userId(), request.values())); + } + + @PostMapping("/{feeId}/void") + @PreAuthorize("hasAuthority('firstleg:inbound:fee-detail:review')") + @BusinessAudit(event = "CONTAINER_FEE_DETAIL_VOID", resourceType = "CONTAINER_FEE_DETAIL", resourceId = "#feeId") + public ApiResponse voidApproved( + @AuthenticationPrincipal AuthenticatedUser user, @RequestParam long accountId, + @PathVariable long loadingPlanId, @PathVariable long feeId, + @Valid @RequestBody VersionRequest request) { + service.voidApproved(FirstlegScopeGuard.platform(user, accountId), loadingPlanId, feeId, + request.expectedSourceVersion(), user.userId()); + return ApiResponse.success(null); + } +} diff --git a/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformInboundFeeDetailController.java b/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformInboundFeeDetailController.java index fc671f2e4..f4ff30e34 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformInboundFeeDetailController.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformInboundFeeDetailController.java @@ -5,6 +5,7 @@ import com.baiyue.common.audit.BusinessAudit; import com.baiyue.common.security.AuthenticatedUser; import com.baiyue.firstleg.dto.InboundFeeDetailDtos.RejectRequest; import com.baiyue.firstleg.dto.InboundFeeDetailDtos.SaveRequest; +import com.baiyue.firstleg.dto.InboundFeeDetailDtos.VersionRequest; import com.baiyue.firstleg.entity.InboundFeeDetailEntity; import com.baiyue.firstleg.service.FirstlegScopeGuard; import com.baiyue.firstleg.service.InboundFeeDetailService; @@ -104,4 +105,32 @@ public class PlatformInboundFeeDetailController { user.userId(), user.username(), request.reason()); return ApiResponse.success(null); } + + @PostMapping("/{detailId}/adjust") + @BusinessAudit(event = "INBOUND_FEE_DETAIL_ADJUST", resourceType = "INBOUND_FEE_DETAIL", resourceId = "#detailId", description = "调整已审核入库费用明细") + @PreAuthorize("hasAuthority('firstleg:inbound:fee-detail:edit')") + public ApiResponse adjust( + @AuthenticationPrincipal AuthenticatedUser user, + @RequestParam long accountId, + @PathVariable long inboundId, + @PathVariable long detailId, + @RequestParam int expectedSourceVersion, + @Valid @RequestBody SaveRequest request) { + return ApiResponse.success(service.adjust(FirstlegScopeGuard.platform(user, accountId), + inboundId, detailId, expectedSourceVersion, user.userId(), request)); + } + + @PostMapping("/{detailId}/void") + @BusinessAudit(event = "INBOUND_FEE_DETAIL_VOID", resourceType = "INBOUND_FEE_DETAIL", resourceId = "#detailId", description = "作废已审核入库费用明细") + @PreAuthorize("hasAuthority('firstleg:inbound:fee-detail:review')") + public ApiResponse voidApproved( + @AuthenticationPrincipal AuthenticatedUser user, + @RequestParam long accountId, + @PathVariable long inboundId, + @PathVariable long detailId, + @Valid @RequestBody VersionRequest request) { + service.voidApproved(FirstlegScopeGuard.platform(user, accountId), inboundId, detailId, + request.expectedSourceVersion(), user.userId()); + return ApiResponse.success(null); + } } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/dto/ContainerFeeDetailDtos.java b/firstleg/src/main/java/com/baiyue/firstleg/dto/ContainerFeeDetailDtos.java new file mode 100644 index 000000000..7acd6fa3a --- /dev/null +++ b/firstleg/src/main/java/com/baiyue/firstleg/dto/ContainerFeeDetailDtos.java @@ -0,0 +1,49 @@ +package com.baiyue.firstleg.dto; + +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; + +import java.math.BigDecimal; +import java.time.LocalDate; + +public final class ContainerFeeDetailDtos { + private ContainerFeeDetailDtos() { + } + + public record SaveRequest( + @NotNull @Min(1) Long feeItemId, + @NotBlank @Pattern(regexp = "RECEIVABLE|PAYABLE") String direction, + @NotNull LocalDate businessDate, + @NotNull @DecimalMin("0.00") BigDecimal amount, + @NotBlank @Pattern(regexp = "CNY") String currency, + @NotNull Boolean settlementIncluded, + @Size(max = 500) String remark + ) { + } + + public record AdjustRequest( + @NotNull @Min(1) Integer expectedSourceVersion, + @NotNull @Min(1) Long feeItemId, + @NotBlank @Pattern(regexp = "RECEIVABLE|PAYABLE") String direction, + @NotNull LocalDate businessDate, + @NotNull @DecimalMin("0.00") BigDecimal amount, + @NotBlank @Pattern(regexp = "CNY") String currency, + @NotNull Boolean settlementIncluded, + @Size(max = 500) String remark + ) { + public SaveRequest values() { + return new SaveRequest(feeItemId, direction, businessDate, amount, currency, + settlementIncluded, remark); + } + } + + public record VersionRequest(@NotNull @Min(1) Integer expectedSourceVersion) { + } + + public record RejectRequest(@NotBlank @Size(max = 500) String reason) { + } +} diff --git a/firstleg/src/main/java/com/baiyue/firstleg/dto/FinanceFeeSourceEvent.java b/firstleg/src/main/java/com/baiyue/firstleg/dto/FinanceFeeSourceEvent.java new file mode 100644 index 000000000..ca654b191 --- /dev/null +++ b/firstleg/src/main/java/com/baiyue/firstleg/dto/FinanceFeeSourceEvent.java @@ -0,0 +1,22 @@ +package com.baiyue.firstleg.dto; + +import java.time.LocalDateTime; + +/** Firstleg 向 Finance 发布的短事件信封;完整金额必须回查来源快照接口。 */ +public record FinanceFeeSourceEvent( + String eventId, + String eventType, + int eventVersion, + String producer, + LocalDateTime occurredAt, + String tenantId, + String aggregateType, + String aggregateId, + int sequence, + String idempotencyKey, + String payloadHash, + Payload payload +) { + public record Payload(String inboundId, String containerId, String feeId, int feeVersion) { + } +} diff --git a/firstleg/src/main/java/com/baiyue/firstleg/dto/FinanceFeeSourceSnapshot.java b/firstleg/src/main/java/com/baiyue/firstleg/dto/FinanceFeeSourceSnapshot.java new file mode 100644 index 000000000..0772af2cb --- /dev/null +++ b/firstleg/src/main/java/com/baiyue/firstleg/dto/FinanceFeeSourceSnapshot.java @@ -0,0 +1,39 @@ +package com.baiyue.firstleg.dto; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** Finance 回查的 Firstleg 费用来源只读快照。 */ +public record FinanceFeeSourceSnapshot( + long tenantId, + String sourceType, + long feeId, + int feeVersion, + Long inboundId, + String inboundCode, + Long orderId, + Long containerId, + Long containerBookingId, + String containerNo, + long customerId, + String customerName, + Long feeItemId, + String feeItemCode, + String feeItemName, + String direction, + LocalDate businessDate, + String currency, + BigDecimal totalAmount, + String reviewStatus, + String publicationStatus, + boolean settlementIncluded, + LocalDateTime publishedAt, + BigDecimal chargeableQuantity, + String chargeUnit, + BigDecimal actualGrossWeightKg, + BigDecimal actualVolumeCbm, + String sourceHash, + int snapshotVersion +) { +} diff --git a/firstleg/src/main/java/com/baiyue/firstleg/dto/InboundFeeDetailDtos.java b/firstleg/src/main/java/com/baiyue/firstleg/dto/InboundFeeDetailDtos.java index c0598cbaa..87e935baa 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/dto/InboundFeeDetailDtos.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/dto/InboundFeeDetailDtos.java @@ -21,8 +21,17 @@ public final class InboundFeeDetailDtos { @NotNull @DecimalMin("0.00") BigDecimal unitPrice, @NotBlank @Pattern(regexp = "[A-Z]{3}") String currency, @NotNull @DecimalMin("0.00") BigDecimal amount, + @NotNull Boolean settlementIncluded, @Size(max = 500) String remark ) { + public SaveRequest(Long feeItemId, String direction, LocalDate businessDate, + BigDecimal unitPrice, String currency, BigDecimal amount, + String remark) { + this(feeItemId, direction, businessDate, unitPrice, currency, amount, false, remark); + } + } + + public record VersionRequest(@NotNull @jakarta.validation.constraints.Min(1) Integer expectedSourceVersion) { } public record RejectRequest(@NotBlank @Size(max = 500) String reason) { diff --git a/firstleg/src/main/java/com/baiyue/firstleg/entity/ContainerFeeDetailEntity.java b/firstleg/src/main/java/com/baiyue/firstleg/entity/ContainerFeeDetailEntity.java new file mode 100644 index 000000000..a3cd100c8 --- /dev/null +++ b/firstleg/src/main/java/com/baiyue/firstleg/entity/ContainerFeeDetailEntity.java @@ -0,0 +1,44 @@ +package com.baiyue.firstleg.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Getter; +import lombok.Setter; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** 柜次级人工应收/应付费用来源。 */ +@Getter +@Setter +@TableName("fl_container_fee_detail") +public class ContainerFeeDetailEntity { + @TableId(type = IdType.ASSIGN_ID) + private Long id; + private Long tenantId; + private Long loadingPlanId; + private Long containerBookingId; + private String containerNo; + private Long feeItemId; + private String feeItemCodeSnapshot; + private String feeItemNameSnapshot; + private String direction; + private LocalDate businessDate; + private BigDecimal amount; + private String currency; + private Boolean settlementIncluded; + private String reviewStatus; + private String sourceStatus; + private Integer sourceVersion; + private String remark; + private Long reviewerId; + private String reviewerName; + private LocalDateTime reviewedAt; + private String reviewReason; + private Long createdBy; + private LocalDateTime createdAt; + private Long updatedBy; + private LocalDateTime updatedAt; +} diff --git a/firstleg/src/main/java/com/baiyue/firstleg/entity/InboundOutboxEntity.java b/firstleg/src/main/java/com/baiyue/firstleg/entity/InboundOutboxEntity.java index f796e244e..dbc87b436 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/entity/InboundOutboxEntity.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/entity/InboundOutboxEntity.java @@ -19,8 +19,17 @@ public class InboundOutboxEntity { private String aggregateType; private Long aggregateId; private String eventType; + private String eventKey; + private Integer eventVersion; + private Integer aggregateSequence; + private String payloadHash; private String payloadJson; private String publishStatus; private Integer retryCount; + private LocalDateTime nextRetryAt; + private LocalDateTime publishedAt; + private LocalDateTime lockedAt; + private String lockedBy; + private String lastError; private LocalDateTime createdAt; } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/mapper/ContainerFeeDetailMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/mapper/ContainerFeeDetailMapper.java new file mode 100644 index 000000000..b4b9b1b4d --- /dev/null +++ b/firstleg/src/main/java/com/baiyue/firstleg/mapper/ContainerFeeDetailMapper.java @@ -0,0 +1,9 @@ +package com.baiyue.firstleg.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baiyue.firstleg.entity.ContainerFeeDetailEntity; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface ContainerFeeDetailMapper extends BaseMapper { +} diff --git a/firstleg/src/main/java/com/baiyue/firstleg/repository/ContainerFeeDetailRepository.java b/firstleg/src/main/java/com/baiyue/firstleg/repository/ContainerFeeDetailRepository.java new file mode 100644 index 000000000..9a7ebb587 --- /dev/null +++ b/firstleg/src/main/java/com/baiyue/firstleg/repository/ContainerFeeDetailRepository.java @@ -0,0 +1,81 @@ +package com.baiyue.firstleg.repository; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baiyue.firstleg.entity.ContainerFeeDetailEntity; +import com.baiyue.firstleg.mapper.ContainerFeeDetailMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class ContainerFeeDetailRepository { + private final ContainerFeeDetailMapper mapper; + + public List list(long tenantId, long loadingPlanId) { + return mapper.selectList(new LambdaQueryWrapper() + .eq(ContainerFeeDetailEntity::getTenantId, tenantId) + .eq(ContainerFeeDetailEntity::getLoadingPlanId, loadingPlanId) + .orderByDesc(ContainerFeeDetailEntity::getBusinessDate) + .orderByDesc(ContainerFeeDetailEntity::getId)); + } + + public ContainerFeeDetailEntity find(long tenantId, long loadingPlanId, long feeId) { + return mapper.selectOne(new LambdaQueryWrapper() + .eq(ContainerFeeDetailEntity::getTenantId, tenantId) + .eq(ContainerFeeDetailEntity::getLoadingPlanId, loadingPlanId) + .eq(ContainerFeeDetailEntity::getId, feeId)); + } + + public ContainerFeeDetailEntity find(long loadingPlanId, long feeId) { + return mapper.selectOne(new LambdaQueryWrapper() + .eq(ContainerFeeDetailEntity::getLoadingPlanId, loadingPlanId) + .eq(ContainerFeeDetailEntity::getId, feeId)); + } + + public void insert(ContainerFeeDetailEntity value) { + mapper.insert(value); + } + + public int review(long tenantId, long loadingPlanId, long feeId, String status, + long reviewerId, String reviewerName, String reason) { + return mapper.update(null, new LambdaUpdateWrapper() + .eq(ContainerFeeDetailEntity::getTenantId, tenantId) + .eq(ContainerFeeDetailEntity::getLoadingPlanId, loadingPlanId) + .eq(ContainerFeeDetailEntity::getId, feeId) + .eq(ContainerFeeDetailEntity::getReviewStatus, "PENDING") + .eq(ContainerFeeDetailEntity::getSourceStatus, "ACTIVE") + .set(ContainerFeeDetailEntity::getReviewStatus, status) + .set(ContainerFeeDetailEntity::getReviewerId, reviewerId) + .set(ContainerFeeDetailEntity::getReviewerName, reviewerName) + .set(ContainerFeeDetailEntity::getReviewedAt, LocalDateTime.now()) + .set(ContainerFeeDetailEntity::getReviewReason, reason) + .set(ContainerFeeDetailEntity::getUpdatedBy, reviewerId)); + } + + public int adjust(ContainerFeeDetailEntity value, int expectedVersion) { + return mapper.update(value, new LambdaUpdateWrapper() + .eq(ContainerFeeDetailEntity::getTenantId, value.getTenantId()) + .eq(ContainerFeeDetailEntity::getLoadingPlanId, value.getLoadingPlanId()) + .eq(ContainerFeeDetailEntity::getId, value.getId()) + .eq(ContainerFeeDetailEntity::getReviewStatus, "APPROVED") + .eq(ContainerFeeDetailEntity::getSourceStatus, "ACTIVE") + .eq(ContainerFeeDetailEntity::getSourceVersion, expectedVersion)); + } + + public int voidApproved(long tenantId, long loadingPlanId, long feeId, + int expectedVersion, long operatorId) { + return mapper.update(null, new LambdaUpdateWrapper() + .eq(ContainerFeeDetailEntity::getTenantId, tenantId) + .eq(ContainerFeeDetailEntity::getLoadingPlanId, loadingPlanId) + .eq(ContainerFeeDetailEntity::getId, feeId) + .eq(ContainerFeeDetailEntity::getReviewStatus, "APPROVED") + .eq(ContainerFeeDetailEntity::getSourceStatus, "ACTIVE") + .eq(ContainerFeeDetailEntity::getSourceVersion, expectedVersion) + .set(ContainerFeeDetailEntity::getSourceStatus, "VOIDED") + .set(ContainerFeeDetailEntity::getUpdatedBy, operatorId)); + } +} diff --git a/firstleg/src/main/java/com/baiyue/firstleg/repository/FeeItemRepository.java b/firstleg/src/main/java/com/baiyue/firstleg/repository/FeeItemRepository.java index 5c82a61b4..8e12bce84 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/repository/FeeItemRepository.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/repository/FeeItemRepository.java @@ -113,6 +113,18 @@ public class FeeItemRepository { .eq(InboundFeeDetailEntity::getId, detailId)); } + public InboundFeeDetailEntity findDetail(long inboundId, long detailId) { + return feeDetails.selectOne(new LambdaQueryWrapper() + .eq(InboundFeeDetailEntity::getInboundId, inboundId) + .eq(InboundFeeDetailEntity::getId, detailId)); + } + + public InboundFeeDetailEntity findBySourceFee(String sourceType, long sourceFeeId) { + return feeDetails.selectOne(new LambdaQueryWrapper() + .eq(InboundFeeDetailEntity::getSourceType, sourceType) + .eq(InboundFeeDetailEntity::getSourceFeeId, sourceFeeId)); + } + public InboundFeeDetailEntity findBySourceFee(long tenantId, String sourceType, long sourceFeeId) { return feeDetails.selectOne(new LambdaQueryWrapper() .eq(InboundFeeDetailEntity::getTenantId, tenantId) @@ -171,6 +183,33 @@ public class FeeItemRepository { .set(InboundFeeDetailEntity::getReviewerName, reviewerName) .set(InboundFeeDetailEntity::getReviewedAt, LocalDateTime.now()) .set(InboundFeeDetailEntity::getReviewReason, reason) + .set("APPROVED".equals(targetStatus), InboundFeeDetailEntity::getPublicationStatus, + "PUBLISHED") + .set("APPROVED".equals(targetStatus), InboundFeeDetailEntity::getPublishedBy, reviewerId) + .set("APPROVED".equals(targetStatus), InboundFeeDetailEntity::getPublishedByName, reviewerName) + .set("APPROVED".equals(targetStatus), InboundFeeDetailEntity::getPublishedAt, LocalDateTime.now()) .set(InboundFeeDetailEntity::getUpdatedBy, reviewerId)); } + + public int adjustApproved(InboundFeeDetailEntity detail, int expectedSourceVersion) { + return feeDetails.update(detail, new LambdaUpdateWrapper() + .eq(InboundFeeDetailEntity::getTenantId, detail.getTenantId()) + .eq(InboundFeeDetailEntity::getInboundId, detail.getInboundId()) + .eq(InboundFeeDetailEntity::getId, detail.getId()) + .eq(InboundFeeDetailEntity::getReviewStatus, "APPROVED") + .eq(InboundFeeDetailEntity::getSourceFeeVersion, expectedSourceVersion)); + } + + public int voidApproved(long tenantId, long inboundId, long detailId, + int expectedSourceVersion, long operatorId) { + return feeDetails.update(null, new LambdaUpdateWrapper() + .eq(InboundFeeDetailEntity::getTenantId, tenantId) + .eq(InboundFeeDetailEntity::getInboundId, inboundId) + .eq(InboundFeeDetailEntity::getId, detailId) + .eq(InboundFeeDetailEntity::getReviewStatus, "APPROVED") + .eq(InboundFeeDetailEntity::getSourceFeeVersion, expectedSourceVersion) + .ne(InboundFeeDetailEntity::getPublicationStatus, "VOIDED") + .set(InboundFeeDetailEntity::getPublicationStatus, "VOIDED") + .set(InboundFeeDetailEntity::getUpdatedBy, operatorId)); + } } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/repository/FinanceFeeSourceOutboxRepository.java b/firstleg/src/main/java/com/baiyue/firstleg/repository/FinanceFeeSourceOutboxRepository.java new file mode 100644 index 000000000..d776c0bc2 --- /dev/null +++ b/firstleg/src/main/java/com/baiyue/firstleg/repository/FinanceFeeSourceOutboxRepository.java @@ -0,0 +1,107 @@ +package com.baiyue.firstleg.repository; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baiyue.firstleg.entity.InboundOutboxEntity; +import com.baiyue.firstleg.mapper.InboundOutboxMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class FinanceFeeSourceOutboxRepository { + private static final List EVENT_TYPES = List.of( + "FIRSTLEG_FEE_PUBLISHED", "FIRSTLEG_FEE_ADJUSTED", "FIRSTLEG_FEE_VOIDED"); + private final InboundOutboxMapper mapper; + + public List pending(int limit, LocalDateTime now) { + return mapper.selectList(new LambdaQueryWrapper() + .in(InboundOutboxEntity::getEventType, EVENT_TYPES) + .eq(InboundOutboxEntity::getPublishStatus, "PENDING") + .lt(InboundOutboxEntity::getRetryCount, 12) + .and(value -> value.isNull(InboundOutboxEntity::getNextRetryAt) + .or().le(InboundOutboxEntity::getNextRetryAt, now)) + .orderByAsc(InboundOutboxEntity::getCreatedAt) + .orderByAsc(InboundOutboxEntity::getId) + .last("LIMIT " + Math.min(Math.max(limit, 1), 100))); + } + + public boolean claim(long id, String workerId, LocalDateTime now) { + return mapper.update(null, new LambdaUpdateWrapper() + .eq(InboundOutboxEntity::getId, id) + .eq(InboundOutboxEntity::getPublishStatus, "PENDING") + .set(InboundOutboxEntity::getPublishStatus, "PROCESSING") + .set(InboundOutboxEntity::getLockedAt, now) + .set(InboundOutboxEntity::getLockedBy, workerId)) == 1; + } + + public InboundOutboxEntity findClaimed(long id, String workerId) { + return mapper.selectOne(new LambdaQueryWrapper() + .eq(InboundOutboxEntity::getId, id) + .eq(InboundOutboxEntity::getPublishStatus, "PROCESSING") + .eq(InboundOutboxEntity::getLockedBy, workerId)); + } + + public boolean published(long id, String workerId, LocalDateTime now) { + return mapper.update(null, claimed(id, workerId) + .set(InboundOutboxEntity::getPublishStatus, "PUBLISHED") + .set(InboundOutboxEntity::getPublishedAt, now) + .set(InboundOutboxEntity::getNextRetryAt, null) + .set(InboundOutboxEntity::getLockedAt, null) + .set(InboundOutboxEntity::getLockedBy, null) + .set(InboundOutboxEntity::getLastError, null)) == 1; + } + + public boolean retry(long id, String workerId, LocalDateTime nextRetryAt, + String error, boolean exhausted) { + return mapper.update(null, claimed(id, workerId) + .setSql("retry_count = retry_count + 1") + .set(InboundOutboxEntity::getPublishStatus, exhausted ? "FAILED" : "PENDING") + .set(InboundOutboxEntity::getNextRetryAt, exhausted ? null : nextRetryAt) + .set(InboundOutboxEntity::getLockedAt, null) + .set(InboundOutboxEntity::getLockedBy, null) + .set(InboundOutboxEntity::getLastError, truncate(error))) == 1; + } + + public int recoverStale(LocalDateTime staleBefore, LocalDateTime retryAt, int maxRetries) { + int failed = mapper.update(null, new LambdaUpdateWrapper() + .in(InboundOutboxEntity::getEventType, EVENT_TYPES) + .eq(InboundOutboxEntity::getPublishStatus, "PROCESSING") + .le(InboundOutboxEntity::getLockedAt, staleBefore) + .ge(InboundOutboxEntity::getRetryCount, maxRetries - 1) + .setSql("retry_count = retry_count + 1") + .set(InboundOutboxEntity::getPublishStatus, "FAILED") + .set(InboundOutboxEntity::getNextRetryAt, null) + .set(InboundOutboxEntity::getLockedAt, null) + .set(InboundOutboxEntity::getLockedBy, null) + .set(InboundOutboxEntity::getLastError, "relay lease expired")); + int pending = mapper.update(null, new LambdaUpdateWrapper() + .in(InboundOutboxEntity::getEventType, EVENT_TYPES) + .eq(InboundOutboxEntity::getPublishStatus, "PROCESSING") + .le(InboundOutboxEntity::getLockedAt, staleBefore) + .lt(InboundOutboxEntity::getRetryCount, maxRetries - 1) + .setSql("retry_count = retry_count + 1") + .set(InboundOutboxEntity::getPublishStatus, "PENDING") + .set(InboundOutboxEntity::getNextRetryAt, retryAt) + .set(InboundOutboxEntity::getLockedAt, null) + .set(InboundOutboxEntity::getLockedBy, null) + .set(InboundOutboxEntity::getLastError, "relay lease expired")); + return failed + pending; + } + + private static LambdaUpdateWrapper claimed(long id, String workerId) { + return new LambdaUpdateWrapper() + .eq(InboundOutboxEntity::getId, id) + .eq(InboundOutboxEntity::getPublishStatus, "PROCESSING") + .eq(InboundOutboxEntity::getLockedBy, workerId); + } + + private static String truncate(String value) { + if (value == null || value.isBlank()) return "unknown relay error"; + String sanitized = value.replaceAll("[\\r\\n\\t]+", " ").trim(); + return sanitized.length() <= 500 ? sanitized : sanitized.substring(0, 500); + } +} diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/ContainerFeeDetailService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/ContainerFeeDetailService.java new file mode 100644 index 000000000..dd3a0b6a5 --- /dev/null +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/ContainerFeeDetailService.java @@ -0,0 +1,162 @@ +package com.baiyue.firstleg.service; + +import com.baiyue.common.core.api.ResultCode; +import com.baiyue.common.core.exception.BizException; +import com.baiyue.firstleg.dto.ContainerFeeDetailDtos.SaveRequest; +import com.baiyue.firstleg.entity.ContainerBookingEntity; +import com.baiyue.firstleg.entity.ContainerFeeDetailEntity; +import com.baiyue.firstleg.entity.FeeItemEntity; +import com.baiyue.firstleg.entity.LoadingPlanEntity; +import com.baiyue.firstleg.repository.ContainerFeeDetailRepository; +import com.baiyue.firstleg.repository.FeeItemRepository; +import com.baiyue.firstleg.repository.SmartLoadingRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.RoundingMode; +import java.time.LocalDateTime; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class ContainerFeeDetailService { + private final ContainerFeeDetailRepository repository; + private final SmartLoadingRepository loading; + private final FeeItemRepository feeItems; + private final FinanceFeeSourceOutboxService financeOutbox; + + public List list(long tenantId, long loadingPlanId) { + requireOwnedPlan(tenantId, loadingPlanId); + return repository.list(tenantId, loadingPlanId); + } + + @Transactional + public ContainerFeeDetailEntity create(long tenantId, long loadingPlanId, long operatorId, + SaveRequest request) { + PlanContext plan = requireOwnedPlan(tenantId, loadingPlanId); + FeeItemEntity item = requireItem(request.feeItemId(), request.direction()); + ContainerFeeDetailEntity value = new ContainerFeeDetailEntity(); + value.setTenantId(tenantId); + value.setLoadingPlanId(loadingPlanId); + value.setContainerBookingId(plan.plan().getContainerBookingId()); + value.setContainerNo(plan.booking() == null ? null : plan.booking().getContainerNo()); + value.setSourceVersion(1); + value.setSourceStatus("ACTIVE"); + value.setReviewStatus("PENDING"); + value.setCreatedBy(operatorId); + value.setCreatedAt(LocalDateTime.now()); + apply(value, item, request, operatorId); + repository.insert(value); + return value; + } + + @Transactional + public void approve(long tenantId, long loadingPlanId, long feeId, + long reviewerId, String reviewerName) { + required(tenantId, loadingPlanId, feeId); + if (repository.review(tenantId, loadingPlanId, feeId, "APPROVED", + reviewerId, reviewerName, null) != 1) { + throw conflict("只有待审核柜次费用可以审核通过"); + } + ContainerFeeDetailEntity approved = required(tenantId, loadingPlanId, feeId); + if (Boolean.TRUE.equals(approved.getSettlementIncluded())) { + String eventType = approved.getSourceVersion() > 1 + ? FinanceFeeSourceOutboxService.ADJUSTED : FinanceFeeSourceOutboxService.PUBLISHED; + financeOutbox.publishContainer(eventType, tenantId, loadingPlanId, feeId, + approved.getSourceVersion(), LocalDateTime.now()); + } + } + + @Transactional + public void reject(long tenantId, long loadingPlanId, long feeId, long reviewerId, + String reviewerName, String reason) { + required(tenantId, loadingPlanId, feeId); + if (repository.review(tenantId, loadingPlanId, feeId, "REJECTED", reviewerId, + reviewerName, reason.trim()) != 1) { + throw conflict("只有待审核柜次费用可以驳回"); + } + } + + @Transactional + public ContainerFeeDetailEntity adjust(long tenantId, long loadingPlanId, long feeId, + int expectedVersion, long operatorId, + SaveRequest request) { + requireOwnedPlan(tenantId, loadingPlanId); + ContainerFeeDetailEntity value = required(tenantId, loadingPlanId, feeId); + FeeItemEntity item = requireItem(request.feeItemId(), request.direction()); + apply(value, item, request, operatorId); + value.setSourceVersion(expectedVersion + 1); + value.setReviewStatus("PENDING"); + value.setReviewerId(null); + value.setReviewerName(null); + value.setReviewedAt(null); + value.setReviewReason(null); + if (repository.adjust(value, expectedVersion) != 1) { + throw conflict("只有当前版本的已审核柜次费用可以调整"); + } + return required(tenantId, loadingPlanId, feeId); + } + + @Transactional + public void voidApproved(long tenantId, long loadingPlanId, long feeId, + int expectedVersion, long operatorId) { + ContainerFeeDetailEntity current = required(tenantId, loadingPlanId, feeId); + if (repository.voidApproved(tenantId, loadingPlanId, feeId, expectedVersion, operatorId) != 1) { + throw conflict("只有当前版本的已审核柜次费用可以作废"); + } + if (Boolean.TRUE.equals(current.getSettlementIncluded())) { + financeOutbox.publishContainer(FinanceFeeSourceOutboxService.VOIDED, tenantId, + loadingPlanId, feeId, expectedVersion, LocalDateTime.now()); + } + } + + private PlanContext requireOwnedPlan(long tenantId, long loadingPlanId) { + LoadingPlanEntity plan = loading.plan(loadingPlanId); + if (plan == null) throw new BizException(ResultCode.NOT_FOUND, "柜次不存在"); + boolean owned = loading.items(loadingPlanId).stream() + .anyMatch(item -> Long.valueOf(tenantId).equals(item.getAccountId())); + if (!owned) throw new BizException(ResultCode.NOT_FOUND, "柜次不属于该客户"); + return new PlanContext(plan, plan.getContainerBookingId() == null + ? null : loading.booking(plan.getContainerBookingId())); + } + + private FeeItemEntity requireItem(long feeItemId, String direction) { + FeeItemEntity item = feeItems.findItem(feeItemId); + if (item == null) throw new BizException(ResultCode.NOT_FOUND, "费用项目不存在"); + if (!"ACTIVE".equals(item.getStatus())) throw conflict("费用项目已停用"); + boolean applicable = "RECEIVABLE".equals(direction) + ? Boolean.TRUE.equals(item.getPickupReceivable()) || Boolean.TRUE.equals(item.getWarehouseReceivable()) + : Boolean.TRUE.equals(item.getPickupPayable()) || Boolean.TRUE.equals(item.getWarehousePayable()); + if (!applicable) throw conflict("费用项目未启用对应应收/应付方向"); + return item; + } + + private ContainerFeeDetailEntity required(long tenantId, long loadingPlanId, long feeId) { + ContainerFeeDetailEntity value = repository.find(tenantId, loadingPlanId, feeId); + if (value == null) throw new BizException(ResultCode.NOT_FOUND, "柜次费用明细不存在"); + return value; + } + + private static void apply(ContainerFeeDetailEntity value, FeeItemEntity item, + SaveRequest request, long operatorId) { + value.setFeeItemId(item.getId()); + value.setFeeItemCodeSnapshot(item.getFeeCode()); + value.setFeeItemNameSnapshot(item.getFeeTypeName()); + value.setDirection(request.direction()); + value.setBusinessDate(request.businessDate()); + value.setAmount(request.amount().setScale(2, RoundingMode.HALF_UP)); + value.setCurrency(request.currency()); + value.setSettlementIncluded(Boolean.TRUE.equals(request.settlementIncluded())); + value.setRemark(request.remark() == null ? null : request.remark().trim()); + value.setUpdatedBy(operatorId); + value.setUpdatedAt(LocalDateTime.now()); + } + + private static BizException conflict(String message) { + return new BizException(ResultCode.CONFLICT, message); + } + + private record PlanContext(LoadingPlanEntity plan, ContainerBookingEntity booking) { + } +} diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/FbaPricingService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/FbaPricingService.java index f439ead5e..d8a9e75cd 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/FbaPricingService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/FbaPricingService.java @@ -11,7 +11,6 @@ import com.baiyue.firstleg.domain.PricingTaxMode; import com.baiyue.firstleg.dto.PriceBookDtos.EstimateRequest; import com.baiyue.firstleg.entity.InboundFeeEntity; import com.baiyue.firstleg.entity.InboundFeeItemEntity; -import com.baiyue.firstleg.entity.InboundOutboxEntity; import com.baiyue.firstleg.entity.InboundPricingEntity; import com.baiyue.firstleg.entity.InboundPricingIdempotencyEntity; import com.baiyue.firstleg.pricing.ActivePriceBookEstimator; @@ -44,6 +43,8 @@ public class FbaPricingService { private Clock clock; @Resource private InboundFeeDetailService feeDetails; + @Resource + private FinanceFeeSourceOutboxService financeOutbox; @Transactional public FeeSnapshotResponse calculate(long tenantId, long operatorId, long inboundId, @@ -126,16 +127,8 @@ public class FbaPricingService { throw conflict("费用未确认或入库单状态已变化,不能发送"); } feeDetails.publishFreight(tenantId, inboundId, current.getId(), operatorId, null, now); - InboundOutboxEntity event = new InboundOutboxEntity(); - event.setTenantId(tenantId); - event.setAggregateType("INBOUND"); - event.setAggregateId(inboundId); - event.setEventType("FIRSTLEG_FEE_SENT"); - event.setPayloadJson("{\"inboundId\":\"" + inboundId + "\",\"feeId\":\"" + current.getId() + "\"}"); - event.setPublishStatus("PENDING"); - event.setRetryCount(0); - event.setCreatedAt(now); - repository.insertOutbox(event); + financeOutbox.publishInbound(FinanceFeeSourceOutboxService.PUBLISHED, + tenantId, inboundId, current.getId(), current.getFeeVersion(), now); complete(token, current.getId(), now); return response(tenantId, current.getId(), nextProcessStatus); } @@ -208,17 +201,8 @@ public class FbaPricingService { repository.insertItem(extra); } feeDetails.publishFreight(tenantId, inboundId, adjusted.getId(), operatorId, null, now); - InboundOutboxEntity event = new InboundOutboxEntity(); - event.setTenantId(tenantId); - event.setAggregateType("INBOUND"); - event.setAggregateId(inboundId); - event.setEventType("FIRSTLEG_FEE_ADJUSTED"); - event.setPayloadJson("{\"inboundId\":\"" + inboundId + "\",\"feeId\":\"" - + adjusted.getId() + "\",\"feeVersion\":" + nextVersion + "}"); - event.setPublishStatus("PENDING"); - event.setRetryCount(0); - event.setCreatedAt(now); - repository.insertOutbox(event); + financeOutbox.publishInbound(FinanceFeeSourceOutboxService.ADJUSTED, + tenantId, inboundId, adjusted.getId(), nextVersion, now); complete(token, adjusted.getId(), now); return response(tenantId, adjusted.getId(), "INBOUNDED"); } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/FinanceFeeSourceOutboxRelay.java b/firstleg/src/main/java/com/baiyue/firstleg/service/FinanceFeeSourceOutboxRelay.java new file mode 100644 index 000000000..3c130f230 --- /dev/null +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/FinanceFeeSourceOutboxRelay.java @@ -0,0 +1,182 @@ +package com.baiyue.firstleg.service; + +import com.baiyue.common.core.api.ApiResponse; +import com.baiyue.firstleg.client.FinanceFeeSourceEventClient; +import com.baiyue.firstleg.dto.FinanceFeeSourceEvent; +import com.baiyue.firstleg.entity.InboundOutboxEntity; +import com.baiyue.firstleg.repository.FinanceFeeSourceOutboxRepository; +import com.fasterxml.jackson.databind.ObjectMapper; +import feign.FeignException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.Clock; +import java.time.LocalDateTime; +import java.util.HexFormat; +import java.util.Set; +import java.util.UUID; + +/** 将 Firstleg 费用来源 Outbox 至少一次投递到 Finance Inbox。 */ +@Component +@Slf4j +public class FinanceFeeSourceOutboxRelay { + private static final int MAX_RETRIES = 12; + private static final Set TYPES = Set.of( + FinanceFeeSourceOutboxService.PUBLISHED, + FinanceFeeSourceOutboxService.ADJUSTED, + FinanceFeeSourceOutboxService.VOIDED); + + private final FinanceFeeSourceOutboxRepository repository; + private final FinanceFeeSourceEventClient client; + private final ObjectMapper objectMapper; + private final Clock clock; + private final String workerId; + @Value("${baiyue.security.internal-service-token}") + private String internalToken; + + public FinanceFeeSourceOutboxRelay(FinanceFeeSourceOutboxRepository repository, + FinanceFeeSourceEventClient client, + ObjectMapper objectMapper) { + this(repository, client, objectMapper, Clock.systemDefaultZone(), UUID.randomUUID().toString()); + } + + FinanceFeeSourceOutboxRelay(FinanceFeeSourceOutboxRepository repository, + FinanceFeeSourceEventClient client, ObjectMapper objectMapper, + Clock clock, String workerId) { + this.repository = repository; + this.client = client; + this.objectMapper = objectMapper; + this.clock = clock; + this.workerId = workerId; + } + + @Scheduled(fixedDelayString = "${baiyue.finance.outbox-relay.fixed-delay-ms:5000}") + public void relay() { + LocalDateTime now = LocalDateTime.now(clock); + repository.recoverStale(now.minusMinutes(5), now.plusSeconds(10), MAX_RETRIES); + for (InboundOutboxEntity candidate : repository.pending(50, now)) { + if (!repository.claim(candidate.getId(), workerId, now)) continue; + InboundOutboxEntity event = repository.findClaimed(candidate.getId(), workerId); + if (event == null) continue; + deliver(event); + } + } + + private void deliver(InboundOutboxEntity row) { + try { + FinanceFeeSourceEvent event = parseAndValidate(row); + ApiResponse response = + client.receive(internalToken, event.idempotencyKey(), event); + validateAcknowledgement(event, response); + if (!repository.published(row.getId(), workerId, LocalDateTime.now(clock))) { + log.warn("Finance fee event acknowledgement lost lease: eventId={}", row.getEventKey()); + } + } catch (Exception exception) { + int retryCount = row.getRetryCount() == null ? 0 : row.getRetryCount(); + boolean permanent = permanent(exception); + boolean exhausted = permanent || retryCount + 1 >= MAX_RETRIES; + String error = safeError(exception); + repository.retry(row.getId(), workerId, + LocalDateTime.now(clock).plusSeconds(backoffSeconds(retryCount + 1)), + error, exhausted); + log.warn("Finance fee event relay failed: eventId={}, retry={}, exhausted={}, reason={}", + row.getEventKey(), retryCount + 1, exhausted, error); + } + } + + private FinanceFeeSourceEvent parseAndValidate(InboundOutboxEntity row) throws Exception { + FinanceFeeSourceEvent event = objectMapper.readValue(row.getPayloadJson(), FinanceFeeSourceEvent.class); + if (event == null || event.eventVersion() != 1 || !"firstleg-service".equals(event.producer()) + || !TYPES.contains(event.eventType()) || !event.eventType().equals(row.getEventType()) + || event.eventId() == null || !event.eventId().equals(row.getEventKey()) + || !event.idempotencyKey().equals(row.getEventKey()) + || !String.valueOf(row.getTenantId()).equals(event.tenantId()) + || !String.valueOf(row.getAggregateId()).equals(event.aggregateId()) + || !event.aggregateType().equals(row.getAggregateType()) + || event.payload() == null || event.payload().feeVersion() != event.sequence() + || !sha256(objectMapper.writeValueAsString(event.payload())).equals(event.payloadHash()) + || !event.payloadHash().equals(row.getPayloadHash())) { + throw new InvalidEventException("费用来源事件信封、归属或哈希不一致"); + } + if ("INBOUND".equals(event.aggregateType()) + && !event.aggregateId().equals(event.payload().inboundId())) { + throw new InvalidEventException("入库费用事件聚合归属不一致"); + } + if ("CONTAINER".equals(event.aggregateType()) + && !event.aggregateId().equals(event.payload().containerId())) { + throw new InvalidEventException("柜次费用事件聚合归属不一致"); + } + return event; + } + + private static void validateAcknowledgement( + FinanceFeeSourceEvent event, + ApiResponse response) { + if (response != null && !response.successful()) { + String code = response.code(); + boolean permanent = code != null && !Set.of("FIN-EVT-003", "FIN-EVT-005", "FIN-EVT-006").contains(code); + throw new FinanceRejectedException(code == null ? "FIN-EVT-UNKNOWN" : code, + response.message(), permanent); + } + if (response == null || response.data() == null + || response.data().feeSourceId() == null || response.data().feeSourceId() <= 0) { + throw new IllegalStateException("Finance未确认费用来源事件"); + } + } + + private static boolean permanent(Exception exception) { + if (exception instanceof InvalidEventException) return true; + if (exception instanceof FinanceRejectedException rejected) return rejected.permanent; + if (exception instanceof FeignException feign) { + int status = feign.status(); + return status >= 400 && status < 500 && status != 408 && status != 429; + } + return false; + } + + private static long backoffSeconds(int retry) { + return switch (Math.min(Math.max(retry, 1), 5)) { + case 1 -> 10; + case 2 -> 30; + case 3 -> 120; + case 4 -> 600; + default -> 1800; + }; + } + + private String safeError(Exception exception) { + String value = exception.getMessage(); + if (value == null || value.isBlank()) value = exception.getClass().getSimpleName(); + if (internalToken != null && !internalToken.isBlank()) value = value.replace(internalToken, "***"); + return value; + } + + private static String sha256(String value) { + try { + return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256") + .digest(value.getBytes(StandardCharsets.UTF_8))); + } catch (NoSuchAlgorithmException exception) { + throw new IllegalStateException("SHA-256 unavailable", exception); + } + } + + private static final class InvalidEventException extends RuntimeException { + private InvalidEventException(String message) { + super(message); + } + } + + private static final class FinanceRejectedException extends RuntimeException { + private final boolean permanent; + + private FinanceRejectedException(String code, String message, boolean permanent) { + super(code + ": " + (message == null ? "Finance拒绝费用来源事件" : message)); + this.permanent = permanent; + } + } +} diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/FinanceFeeSourceOutboxService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/FinanceFeeSourceOutboxService.java new file mode 100644 index 000000000..c3eed5e24 --- /dev/null +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/FinanceFeeSourceOutboxService.java @@ -0,0 +1,96 @@ +package com.baiyue.firstleg.service; + +import com.baiyue.common.core.api.ResultCode; +import com.baiyue.common.core.exception.BizException; +import com.baiyue.firstleg.dto.FinanceFeeSourceEvent; +import com.baiyue.firstleg.entity.InboundOutboxEntity; +import com.baiyue.firstleg.mapper.InboundOutboxMapper; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.stereotype.Service; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.LocalDateTime; +import java.util.HexFormat; + +/** 在业务事务内记录 Finance 费用来源事件,eventKey 保证重复动作不会重复入箱。 */ +@Service +@RequiredArgsConstructor +public class FinanceFeeSourceOutboxService { + public static final String PUBLISHED = "FIRSTLEG_FEE_PUBLISHED"; + public static final String ADJUSTED = "FIRSTLEG_FEE_ADJUSTED"; + public static final String VOIDED = "FIRSTLEG_FEE_VOIDED"; + + private final InboundOutboxMapper outbox; + private final ObjectMapper objectMapper; + + public void publishInbound(String eventType, long tenantId, long inboundId, + long feeId, int feeVersion, LocalDateTime occurredAt) { + FinanceFeeSourceEvent.Payload payload = new FinanceFeeSourceEvent.Payload( + String.valueOf(inboundId), null, String.valueOf(feeId), feeVersion); + publish(event(eventType, tenantId, "INBOUND", inboundId, feeId, feeVersion, + payload, occurredAt)); + } + + public void publishContainer(String eventType, long tenantId, long containerId, + long feeId, int feeVersion, LocalDateTime occurredAt) { + FinanceFeeSourceEvent.Payload payload = new FinanceFeeSourceEvent.Payload( + null, String.valueOf(containerId), String.valueOf(feeId), feeVersion); + publish(event(eventType, tenantId, "CONTAINER", containerId, feeId, feeVersion, + payload, occurredAt)); + } + + private void publish(FinanceFeeSourceEvent event) { + InboundOutboxEntity value = new InboundOutboxEntity(); + value.setTenantId(Long.parseLong(event.tenantId())); + value.setAggregateType(event.aggregateType()); + value.setAggregateId(Long.parseLong(event.aggregateId())); + value.setEventType(event.eventType()); + value.setEventKey(event.eventId()); + value.setEventVersion(event.eventVersion()); + value.setAggregateSequence(event.sequence()); + value.setPayloadHash(event.payloadHash()); + value.setPayloadJson(json(event)); + value.setPublishStatus("PENDING"); + value.setRetryCount(0); + value.setCreatedAt(event.occurredAt()); + try { + outbox.insert(value); + } catch (DuplicateKeyException ignored) { + // 同一来源版本和状态的事件已经进入 Outbox,调用方可安全重试。 + } + } + + private FinanceFeeSourceEvent event(String eventType, long tenantId, String aggregateType, + long aggregateId, long feeId, int feeVersion, + FinanceFeeSourceEvent.Payload payload, LocalDateTime occurredAt) { + String idempotencyKey = "FIRSTLEG_FEE:" + aggregateType + ":" + aggregateId + + ":" + feeId + ":" + feeVersion + ":" + eventType; + String payloadJson = json(payload); + return new FinanceFeeSourceEvent(idempotencyKey, eventType, 1, "firstleg-service", + occurredAt == null ? LocalDateTime.now() : occurredAt, String.valueOf(tenantId), + aggregateType, String.valueOf(aggregateId), feeVersion, idempotencyKey, + sha256(payloadJson), payload); + } + + private String json(Object value) { + try { + return objectMapper.writeValueAsString(value); + } catch (JsonProcessingException exception) { + throw new BizException(ResultCode.INTERNAL_ERROR, "费用来源事件序列化失败"); + } + } + + private static String sha256(String value) { + try { + return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256") + .digest(value.getBytes(StandardCharsets.UTF_8))); + } catch (NoSuchAlgorithmException exception) { + throw new IllegalStateException("SHA-256 unavailable", exception); + } + } +} diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/FinanceFeeSourceSnapshotService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/FinanceFeeSourceSnapshotService.java new file mode 100644 index 000000000..a4d9c3032 --- /dev/null +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/FinanceFeeSourceSnapshotService.java @@ -0,0 +1,87 @@ +package com.baiyue.firstleg.service; + +import com.baiyue.common.core.api.ResultCode; +import com.baiyue.common.core.exception.BizException; +import com.baiyue.firstleg.dto.FinanceFeeSourceSnapshot; +import com.baiyue.firstleg.entity.ContainerFeeDetailEntity; +import com.baiyue.firstleg.entity.FeeItemEntity; +import com.baiyue.firstleg.entity.InboundFeeDetailEntity; +import com.baiyue.firstleg.entity.InboundOrderEntity; +import com.baiyue.firstleg.repository.ContainerFeeDetailRepository; +import com.baiyue.firstleg.repository.FeeItemRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; + +@Service +@RequiredArgsConstructor +public class FinanceFeeSourceSnapshotService { + private final FeeItemRepository inboundFees; + private final ContainerFeeDetailRepository containerFees; + + public FinanceFeeSourceSnapshot inbound(long inboundId, long feeId) { + InboundFeeDetailEntity detail = inboundFees.findDetail(inboundId, feeId); + if (detail == null) detail = inboundFees.findBySourceFee("FBA_PRICING", feeId); + if (detail == null || !Long.valueOf(inboundId).equals(detail.getInboundId())) { + throw new BizException(ResultCode.NOT_FOUND, "入库费用来源不存在"); + } + InboundOrderEntity inbound = inboundFees.findInbound(detail.getTenantId(), inboundId); + FeeItemEntity item = inboundFees.findItem(detail.getFeeItemId()); + if (inbound == null || item == null) { + throw new BizException(ResultCode.NOT_FOUND, "入库费用来源关联快照不完整"); + } + long sourceFeeId = "FBA_PRICING".equals(detail.getSourceType()) + ? detail.getSourceFeeId() : detail.getId(); + int version = detail.getSourceFeeVersion() == null ? 1 : detail.getSourceFeeVersion(); + String canonical = String.join("|", "INBOUND_FEE", String.valueOf(sourceFeeId), + String.valueOf(version), String.valueOf(detail.getTenantId()), String.valueOf(inboundId), + String.valueOf(inbound.getOrderId()), nullSafe(item.getFeeCode()), + nullSafe(detail.getDirection()), detail.getAmount().toPlainString(), + nullSafe(detail.getCurrency()), String.valueOf(detail.getSettlementIncluded())); + return new FinanceFeeSourceSnapshot(detail.getTenantId(), "INBOUND_FEE", sourceFeeId, + version, inboundId, inbound.getInboundCode(), inbound.getOrderId(), null, null, null, + detail.getTenantId(), inbound.getCustomerName(), item.getId(), item.getFeeCode(), + detail.getFeeTypeNameSnapshot(), detail.getDirection(), detail.getBusinessDate(), + detail.getCurrency(), detail.getAmount(), detail.getReviewStatus(), + detail.getPublicationStatus(), Boolean.TRUE.equals(detail.getSettlementIncluded()), + detail.getPublishedAt(), detail.getChargeableQuantity(), detail.getChargeUnit(), + detail.getActualGrossWeightKg(), detail.getActualVolumeCbm(), sha256(canonical), 1); + } + + public FinanceFeeSourceSnapshot container(long containerId, long feeId) { + ContainerFeeDetailEntity detail = containerFees.find(containerId, feeId); + if (detail == null) throw new BizException(ResultCode.NOT_FOUND, "柜次费用来源不存在"); + int version = detail.getSourceVersion(); + String publicationStatus = "VOIDED".equals(detail.getSourceStatus()) ? "VOIDED" + : "APPROVED".equals(detail.getReviewStatus()) ? "PUBLISHED" : "DRAFT"; + String canonical = String.join("|", "CONTAINER_FEE", String.valueOf(feeId), + String.valueOf(version), String.valueOf(detail.getTenantId()), String.valueOf(containerId), + nullSafe(detail.getFeeItemCodeSnapshot()), nullSafe(detail.getDirection()), + detail.getAmount().toPlainString(), nullSafe(detail.getCurrency()), + String.valueOf(detail.getSettlementIncluded())); + return new FinanceFeeSourceSnapshot(detail.getTenantId(), "CONTAINER_FEE", feeId, + version, null, null, null, containerId, detail.getContainerBookingId(), + detail.getContainerNo(), detail.getTenantId(), null, detail.getFeeItemId(), + detail.getFeeItemCodeSnapshot(), detail.getFeeItemNameSnapshot(), detail.getDirection(), + detail.getBusinessDate(), detail.getCurrency(), detail.getAmount(), detail.getReviewStatus(), + publicationStatus, Boolean.TRUE.equals(detail.getSettlementIncluded()), + detail.getReviewedAt(), null, null, null, null, sha256(canonical), 1); + } + + private static String nullSafe(Object value) { + return value == null ? "" : String.valueOf(value); + } + + private static String sha256(String value) { + try { + return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256") + .digest(value.getBytes(StandardCharsets.UTF_8))); + } catch (NoSuchAlgorithmException exception) { + throw new IllegalStateException("SHA-256 unavailable", exception); + } + } +} diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/InboundCompletionOrchestrator.java b/firstleg/src/main/java/com/baiyue/firstleg/service/InboundCompletionOrchestrator.java index 0b66074ab..17fc906e4 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/InboundCompletionOrchestrator.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/InboundCompletionOrchestrator.java @@ -6,7 +6,6 @@ import com.baiyue.common.core.api.ResultCode; import com.baiyue.common.core.exception.BizException; import com.baiyue.firstleg.entity.InboundFeeEntity; import com.baiyue.firstleg.entity.InboundOrderEntity; -import com.baiyue.firstleg.entity.InboundOutboxEntity; import com.baiyue.firstleg.repository.InboundWorkflowRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -29,6 +28,8 @@ public class InboundCompletionOrchestrator { private InboundIdempotencyService idempotency; @Resource private InboundFeeDetailService feeDetails; + @Resource + private FinanceFeeSourceOutboxService financeOutbox; /** * 任务5对接方法一:锁定发送动作。允许前置状态仅为 FEE_CONFIRMING + CONFIRMED。 @@ -89,11 +90,12 @@ public class InboundCompletionOrchestrator { if (repository.markFeeSent(tenantId, inboundId, feeId, operatorId) != 1) { throw new BizException(ResultCode.CONFLICT, "费用快照已被并发发送或状态不正确"); } - feeDetails.publishFreight(tenantId, inboundId, feeId, operatorId, operatorName, - LocalDateTime.now()); + var published = feeDetails.publishFreight(tenantId, inboundId, feeId, operatorId, + operatorName, LocalDateTime.now()); repository.insertHistory(MeasurementReviewService.history(tenantId, inboundId, "INBOUNDING", "INBOUNDED", "SEND_FEE_COMPLETE", null, operatorId, operatorName)); - repository.insertOutbox(completedEvent(tenantId, inboundId, feeId)); + financeOutbox.publishInbound(FinanceFeeSourceOutboxService.PUBLISHED, tenantId, + inboundId, feeId, published.getSourceFeeVersion(), LocalDateTime.now()); idempotency.complete(context, inboundId); } @@ -139,19 +141,6 @@ public class InboundCompletionOrchestrator { return order; } - private static InboundOutboxEntity completedEvent(long tenantId, long inboundId, long feeId) { - InboundOutboxEntity event = new InboundOutboxEntity(); - event.setTenantId(tenantId); - event.setAggregateType("INBOUND"); - event.setAggregateId(inboundId); - event.setEventType("FIRSTLEG_INBOUND_FEE_SENT"); - event.setPayloadJson("{\"inboundId\":\"" + inboundId + "\",\"feeId\":\"" + feeId + "\"}"); - event.setPublishStatus("PENDING"); - event.setRetryCount(0); - event.setCreatedAt(LocalDateTime.now()); - return event; - } - private static BizException concurrentConflict() { return new BizException(ResultCode.CONFLICT, "入库单已被并发更新,请刷新后重试"); } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/InboundFeeDetailService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/InboundFeeDetailService.java index 3f778f211..368751f34 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/InboundFeeDetailService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/InboundFeeDetailService.java @@ -28,6 +28,8 @@ public class InboundFeeDetailService { private FeeItemRepository repository; @Resource private FbaPricingRepository pricingRepository; + @Resource + private FinanceFeeSourceOutboxService financeOutbox; public List list(long tenantId, long inboundId) { requiredInbound(tenantId, inboundId); @@ -132,8 +134,8 @@ public class InboundFeeDetailService { apply(value, inbound, item, request, operatorId); value.setReviewStatus("PENDING"); value.setSourceType("MANUAL"); + value.setSourceFeeVersion(1); value.setPublicationStatus("DRAFT"); - value.setSettlementIncluded(false); repository.insertDetail(value); return value; } @@ -168,6 +170,49 @@ public class InboundFeeDetailService { reviewerId, reviewerName, null) != 1) { throw conflict("只有待审核费用明细可以审核通过"); } + InboundFeeDetailEntity approved = required(tenantId, inboundId, detailId); + publishFinanceSource(approved, "AVAILABLE", LocalDateTime.now()); + } + + @Transactional + public InboundFeeDetailEntity adjust(long tenantId, long inboundId, long detailId, + int expectedSourceVersion, long operatorId, + SaveRequest request) { + InboundFeeDetailEntity value = required(tenantId, inboundId, detailId); + if (!"APPROVED".equals(value.getReviewStatus()) || "VOIDED".equals(value.getPublicationStatus())) { + throw conflict("只有审核通过且未作废的费用明细可以调整"); + } + if (value.getSourceFeeVersion() == null || value.getSourceFeeVersion() != expectedSourceVersion) { + throw conflict("费用来源版本已变化,请刷新后重试"); + } + InboundOrderEntity inbound = requiredInbound(tenantId, inboundId); + FeeItemEntity item = requiredActiveItem(request.feeItemId()); + apply(value, inbound, item, request, operatorId); + value.setSourceType("MANUAL"); + value.setSourceFeeVersion(expectedSourceVersion + 1); + value.setReviewStatus("PENDING"); + value.setReviewerId(null); + value.setReviewerName(null); + value.setReviewedAt(null); + value.setReviewReason(null); + value.setPublicationStatus("DRAFT"); + if (repository.adjustApproved(value, expectedSourceVersion) != 1) { + throw conflict("费用明细已被其他人调整,请刷新后重试"); + } + return required(tenantId, inboundId, detailId); + } + + @Transactional + public void voidApproved(long tenantId, long inboundId, long detailId, + int expectedSourceVersion, long operatorId) { + InboundFeeDetailEntity current = required(tenantId, inboundId, detailId); + if (repository.voidApproved(tenantId, inboundId, detailId, expectedSourceVersion, operatorId) != 1) { + throw conflict("只有当前版本的已审核费用明细可以作废"); + } + if (Boolean.TRUE.equals(current.getSettlementIncluded())) { + current.setPublicationStatus("VOIDED"); + publishFinanceSource(current, "VOIDED", LocalDateTime.now()); + } } @Transactional @@ -282,6 +327,7 @@ public class InboundFeeDetailService { value.setUnitPrice(request.unitPrice().setScale(4, RoundingMode.HALF_UP)); value.setCurrency(request.currency()); value.setAmount(request.amount().setScale(2, RoundingMode.HALF_UP)); + value.setSettlementIncluded(Boolean.TRUE.equals(request.settlementIncluded())); value.setRemark(request.remark() == null ? null : request.remark().trim()); value.setUpdatedBy(operatorId); } @@ -289,4 +335,16 @@ public class InboundFeeDetailService { private static BizException conflict(String message) { return new BizException(ResultCode.CONFLICT, message); } + + private void publishFinanceSource(InboundFeeDetailEntity detail, String status, + LocalDateTime occurredAt) { + if (!Boolean.TRUE.equals(detail.getSettlementIncluded())) return; + if ("FBA_PRICING".equals(detail.getSourceType())) return; + int version = detail.getSourceFeeVersion() == null ? 1 : detail.getSourceFeeVersion(); + String eventType = "VOIDED".equals(status) ? FinanceFeeSourceOutboxService.VOIDED + : version > 1 ? FinanceFeeSourceOutboxService.ADJUSTED + : FinanceFeeSourceOutboxService.PUBLISHED; + financeOutbox.publishInbound(eventType, detail.getTenantId(), detail.getInboundId(), + detail.getId(), version, occurredAt); + } } diff --git a/firstleg/src/main/resources/db/audit-outbox.sql b/firstleg/src/main/resources/db/audit-outbox.sql index 467265542..f0af9146f 100644 --- a/firstleg/src/main/resources/db/audit-outbox.sql +++ b/firstleg/src/main/resources/db/audit-outbox.sql @@ -2,14 +2,23 @@ USE baiyue_firstleg; -- 可靠审计本地 Outbox;与头程业务表位于同一数据库。 CREATE TABLE IF NOT EXISTS sys_audit_outbox ( - event_id VARCHAR(64) NOT NULL, event_type VARCHAR(64) NOT NULL, resource_type VARCHAR(64) NOT NULL, - resource_id VARCHAR(128) NULL, service_name VARCHAR(64) NULL, payload_json JSON NOT NULL, - status VARCHAR(16) NOT NULL DEFAULT 'PENDING', attempt_count INT NOT NULL DEFAULT 0, - compensation_count INT NOT NULL DEFAULT 0, - next_retry_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), lease_owner VARCHAR(128) NULL, - lease_until DATETIME(3) NULL, last_error VARCHAR(1000) NULL, dead_at DATETIME(3) NULL, - sent_at DATETIME(3) NULL, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + event_id VARCHAR(64) NOT NULL COMMENT '审计事件全局唯一ID', + event_type VARCHAR(64) NOT NULL COMMENT '审计事件类型', + resource_type VARCHAR(64) NOT NULL COMMENT '被审计业务资源类型', + resource_id VARCHAR(128) NULL COMMENT '被审计业务资源ID;无法确定时为空', + service_name VARCHAR(64) NULL COMMENT '产生审计事件的服务名称', + payload_json JSON NOT NULL COMMENT '待发送的审计事件完整负载快照', + status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT '发送状态:PENDING待发送、SENDING发送中、SENT已发送、DEAD已转死信', + attempt_count INT NOT NULL DEFAULT 0 COMMENT '正常发送累计尝试次数', + compensation_count INT NOT NULL DEFAULT 0 COMMENT '人工或自动补偿累计次数', + next_retry_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '允许下次重试的最早时间', + lease_owner VARCHAR(128) NULL COMMENT '当前发送任务租约持有者标识', + lease_until DATETIME(3) NULL COMMENT '当前发送任务租约到期时间', + last_error VARCHAR(1000) NULL COMMENT '最近一次发送失败原因', + dead_at DATETIME(3) NULL COMMENT '转入死信状态的时间', + sent_at DATETIME(3) NULL COMMENT '首次成功发送时间', + 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 (event_id), KEY idx_audit_outbox_publish (status, next_retry_at, lease_until, created_at), KEY idx_audit_outbox_retention (status, sent_at) ) ENGINE=InnoDB COMMENT='可靠审计事件本地发送箱'; diff --git a/firstleg/src/main/resources/db/customer-dimension-config.sql b/firstleg/src/main/resources/db/customer-dimension-config.sql index 84052664a..30929988f 100644 --- a/firstleg/src/main/resources/db/customer-dimension-config.sql +++ b/firstleg/src/main/resources/db/customer-dimension-config.sql @@ -4,7 +4,7 @@ USE baiyue_firstleg; -- 每个客户固定四条箱规尺寸配置;用于实测尺寸归档和计费体积加权,不包含价格。 CREATE TABLE IF NOT EXISTS fl_customer_dimension_config ( id BIGINT UNSIGNED NOT NULL COMMENT '配置ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', box_type VARCHAR(24) NOT NULL COMMENT '固定箱规类型:SMALL/MEDIUM/LARGE/EXTRA_LARGE', inner_length_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内长(厘米)', inner_width_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内宽(厘米)', diff --git a/firstleg/src/main/resources/db/finance-fee-source-migration.sql b/firstleg/src/main/resources/db/finance-fee-source-migration.sql new file mode 100644 index 000000000..5f3c18f25 --- /dev/null +++ b/firstleg/src/main/resources/db/finance-fee-source-migration.sql @@ -0,0 +1,119 @@ +-- Firstleg -> Finance 费用来源事件和柜次费用模型(MySQL 8,可重复执行) + +DROP PROCEDURE IF EXISTS firstleg_finance_add_column; +DELIMITER $$ +CREATE PROCEDURE firstleg_finance_add_column( + IN target_table VARCHAR(64), IN target_column VARCHAR(64), IN column_definition TEXT +) +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = target_table AND column_name = target_column + ) THEN + SET @ddl = CONCAT('ALTER TABLE `', target_table, '` ADD COLUMN `', target_column, '` ', column_definition); + PREPARE statement FROM @ddl; + EXECUTE statement; + DEALLOCATE PREPARE statement; + END IF; +END$$ +DELIMITER ; + +CALL firstleg_finance_add_column('fl_inbound_outbox', 'event_key', + "VARCHAR(160) NULL COMMENT '费用来源事件幂等键;历史事件允许为空' AFTER event_type"); +CALL firstleg_finance_add_column('fl_inbound_outbox', 'event_version', + "INT UNSIGNED NOT NULL DEFAULT 1 COMMENT '事件信封版本' AFTER event_key"); +CALL firstleg_finance_add_column('fl_inbound_outbox', 'aggregate_sequence', + "INT UNSIGNED NULL COMMENT '同一费用来源的单调版本序号' AFTER event_version"); +CALL firstleg_finance_add_column('fl_inbound_outbox', 'payload_hash', + "CHAR(64) NULL COMMENT '短payload规范化JSON的SHA-256' AFTER aggregate_sequence"); +CALL firstleg_finance_add_column('fl_inbound_outbox', 'next_retry_at', + "DATETIME(3) NULL COMMENT '下一次允许投递时间' AFTER retry_count"); +CALL firstleg_finance_add_column('fl_inbound_outbox', 'published_at', + "DATETIME(3) NULL COMMENT 'Finance确认接收时间' AFTER next_retry_at"); +CALL firstleg_finance_add_column('fl_inbound_outbox', 'locked_at', + "DATETIME(3) NULL COMMENT 'Relay抢占租约时间' AFTER published_at"); +CALL firstleg_finance_add_column('fl_inbound_outbox', 'locked_by', + "VARCHAR(64) NULL COMMENT 'Relay抢占实例ID' AFTER locked_at"); +CALL firstleg_finance_add_column('fl_inbound_outbox', 'last_error', + "VARCHAR(500) NULL COMMENT '最近一次投递失败摘要,不记录令牌' AFTER locked_by"); + +DROP PROCEDURE IF EXISTS firstleg_finance_add_column; + +DROP PROCEDURE IF EXISTS firstleg_finance_add_index; +DELIMITER $$ +CREATE PROCEDURE firstleg_finance_add_index( + IN target_table VARCHAR(64), IN target_index VARCHAR(64), IN index_definition TEXT +) +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.statistics + WHERE table_schema = DATABASE() AND table_name = target_table AND index_name = target_index + ) THEN + SET @ddl = CONCAT('ALTER TABLE `', target_table, '` ADD ', index_definition); + PREPARE statement FROM @ddl; + EXECUTE statement; + DEALLOCATE PREPARE statement; + END IF; +END$$ +DELIMITER ; + +CALL firstleg_finance_add_index('fl_inbound_outbox', 'uk_inbound_outbox_event_key', + 'UNIQUE KEY `uk_inbound_outbox_event_key` (`event_key`)'); +CALL firstleg_finance_add_index('fl_inbound_outbox', 'idx_inbound_outbox_finance_relay', + 'KEY `idx_inbound_outbox_finance_relay` (`publish_status`, `next_retry_at`, `created_at`, `id`)'); +DROP PROCEDURE IF EXISTS firstleg_finance_add_index; + +CREATE TABLE IF NOT EXISTS fl_container_fee_detail ( + id BIGINT UNSIGNED NOT NULL COMMENT '柜次费用来源ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + loading_plan_id BIGINT UNSIGNED NOT NULL COMMENT '柜次ID,对应fl_loading_plan.id', + container_booking_id BIGINT UNSIGNED NULL COMMENT '订舱柜ID快照', + container_no VARCHAR(64) NULL COMMENT '柜号快照', + fee_item_id BIGINT UNSIGNED NOT NULL COMMENT '平台公共费用项目ID', + fee_item_code_snapshot VARCHAR(50) NOT NULL COMMENT '费用项目稳定编码快照', + fee_item_name_snapshot VARCHAR(128) NOT NULL COMMENT '费用项目名称快照', + direction VARCHAR(16) NOT NULL COMMENT '费用方向:RECEIVABLE应收、PAYABLE应付', + business_date DATE NOT NULL COMMENT '费用业务日期', + amount DECIMAL(18,2) NOT NULL COMMENT 'CNY费用金额', + currency CHAR(3) NOT NULL DEFAULT 'CNY' COMMENT '币种,首期固定CNY', + settlement_included TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否参与财务结算来源池', + review_status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT '审核状态:PENDING待审核、APPROVED通过、REJECTED驳回', + source_status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '来源状态:ACTIVE有效、VOIDED作废', + source_version INT UNSIGNED NOT NULL DEFAULT 1 COMMENT '费用来源版本;调整递增,作废携带当前版本', + remark VARCHAR(500) NULL COMMENT '人工备注', + reviewer_id BIGINT UNSIGNED NULL COMMENT '审核人ID', + reviewer_name VARCHAR(64) NULL COMMENT '审核人名称快照', + reviewed_at DATETIME(3) NULL COMMENT '审核时间', + review_reason VARCHAR(500) NULL COMMENT '审核驳回原因', + created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', + updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后更新人ID', + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后更新时间', + PRIMARY KEY (id), + KEY idx_container_fee_customer_plan (tenant_id, loading_plan_id, source_status), + KEY idx_container_fee_settlement (tenant_id, review_status, settlement_included, source_status), + CONSTRAINT chk_container_fee_direction CHECK (direction IN ('RECEIVABLE', 'PAYABLE')), + CONSTRAINT chk_container_fee_currency CHECK (currency = 'CNY'), + CONSTRAINT chk_container_fee_amount CHECK (amount >= 0), + CONSTRAINT chk_container_fee_review CHECK (review_status IN ('PENDING', 'APPROVED', 'REJECTED')), + CONSTRAINT chk_container_fee_source CHECK (source_status IN ('ACTIVE', 'VOIDED')) +) ENGINE=InnoDB COMMENT='柜次级人工应收应付费用来源;审核通过且参与结算后同步Finance'; + +DROP PROCEDURE IF EXISTS firstleg_finance_assert_schema; +DELIMITER $$ +CREATE PROCEDURE firstleg_finance_assert_schema() +BEGIN + IF (SELECT COUNT(*) FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'fl_inbound_outbox' + AND column_name IN ('event_key', 'event_version', 'aggregate_sequence', 'payload_hash', + 'next_retry_at', 'published_at', 'locked_at', 'locked_by', 'last_error')) <> 9 THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Firstleg Finance迁移失败:Outbox事件信封字段不完整'; + END IF; + IF NOT EXISTS (SELECT 1 FROM information_schema.tables + WHERE table_schema = DATABASE() AND table_name = 'fl_container_fee_detail') THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Firstleg Finance迁移失败:柜次费用表不存在'; + END IF; +END$$ +DELIMITER ; +CALL firstleg_finance_assert_schema(); +DROP PROCEDURE IF EXISTS firstleg_finance_assert_schema; diff --git a/firstleg/src/main/resources/db/firstleg.sql b/firstleg/src/main/resources/db/firstleg.sql index 52b5677b0..762d0a30e 100644 --- a/firstleg/src/main/resources/db/firstleg.sql +++ b/firstleg/src/main/resources/db/firstleg.sql @@ -5,11 +5,11 @@ USE baiyue_firstleg; CREATE TABLE IF NOT EXISTS fl_fulfillment_task ( id BIGINT UNSIGNED NOT NULL COMMENT '履约任务ID(雪花ID)', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', order_id BIGINT UNSIGNED NOT NULL COMMENT '订单服务中的订单逻辑ID,不建立跨库外键', order_no VARCHAR(32) NOT NULL COMMENT '订单号快照', order_snapshot_json JSON NOT NULL COMMENT '创建履约任务时所需的订单业务快照', - status VARCHAR(24) NOT NULL DEFAULT 'PENDING_ASSIGNMENT' COMMENT '履约任务当前状态', + status VARCHAR(24) NOT NULL DEFAULT 'PENDING_ASSIGNMENT' COMMENT '履约状态:PENDING_ASSIGNMENT待指派、ASSIGNED已指派、PROCESSING处理中、COMPLETED已完成、CANCELLED已取消', assignee_id BIGINT UNSIGNED NULL COMMENT '当前负责人ID', assignee_name VARCHAR(64) NULL COMMENT '当前负责人名称快照', assigned_at DATETIME(3) NULL COMMENT '最近指派时间', @@ -29,7 +29,7 @@ CREATE TABLE IF NOT EXISTS fl_fulfillment_task ( CREATE TABLE IF NOT EXISTS fl_assignment_history ( id BIGINT UNSIGNED NOT NULL COMMENT '指派历史ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', task_id BIGINT UNSIGNED NOT NULL COMMENT '所属履约任务ID', previous_assignee_id BIGINT UNSIGNED NULL COMMENT '原负责人ID', assignee_id BIGINT UNSIGNED NOT NULL COMMENT '新负责人ID', @@ -51,7 +51,7 @@ CREATE TABLE IF NOT EXISTS fl_consumed_event ( CREATE TABLE IF NOT EXISTS fl_customer_dimension_config ( id BIGINT UNSIGNED NOT NULL COMMENT '配置ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', box_type VARCHAR(24) NOT NULL COMMENT '固定箱规类型:SMALL/MEDIUM/LARGE/EXTRA_LARGE', inner_length_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内长(厘米)', inner_width_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内宽(厘米)', diff --git a/firstleg/src/main/resources/db/phase2-fee-item-and-inbound-detail.sql b/firstleg/src/main/resources/db/phase2-fee-item-and-inbound-detail.sql index 1fdcfe01e..4091fec0c 100644 --- a/firstleg/src/main/resources/db/phase2-fee-item-and-inbound-detail.sql +++ b/firstleg/src/main/resources/db/phase2-fee-item-and-inbound-detail.sql @@ -14,15 +14,15 @@ CREATE TABLE IF NOT EXISTS fl_fee_item ( warehouse_receivable TINYINT(1) NOT NULL DEFAULT 0 COMMENT '仓储应收是否参与', allocation_fee TINYINT(1) NOT NULL DEFAULT 0 COMMENT '平摊费用是否参与', description VARCHAR(500) NULL COMMENT '说明', - status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT 'ACTIVE或DISABLED', - created_by BIGINT NULL, - created_by_name VARCHAR(64) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_by BIGINT NULL, - updated_by_name VARCHAR(64) NULL, - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - deleted TINYINT(1) NOT NULL DEFAULT 0, - version INT NOT NULL DEFAULT 0, + status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '状态:ACTIVE启用、DISABLED停用', + created_by BIGINT NULL COMMENT '创建人ID;系统初始化时可为空', + created_by_name VARCHAR(64) NULL COMMENT '创建人名称快照', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', + updated_by BIGINT NULL COMMENT '最后修改人ID;系统初始化时可为空', + updated_by_name VARCHAR(64) NULL COMMENT '最后修改人名称快照', + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后更新时间', + deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记:0未删除、1已删除', + version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', PRIMARY KEY (id), UNIQUE KEY uk_fee_item_code (fee_code), KEY idx_fee_item_query (tenant_id, trade_scope, category, status, deleted), @@ -45,26 +45,26 @@ CREATE TABLE IF NOT EXISTS fl_inbound_fee_detail ( destination_code VARCHAR(32) NULL COMMENT '目的仓编码快照', destination_name VARCHAR(128) NULL COMMENT '目的仓名称快照', package_count INT NULL COMMENT '件数快照', - actual_gross_weight_kg DECIMAL(18,4) NULL COMMENT '仓库实测毛重', - actual_volume_cbm DECIMAL(18,6) NULL COMMENT '体积', - volume_weight_kg DECIMAL(18,4) NULL COMMENT '材积重', - chargeable_weight_kg DECIMAL(18,4) NULL COMMENT '计费重量', - chargeable_volume_cbm DECIMAL(18,6) NULL COMMENT '计费体积', - unit_price DECIMAL(18,4) NOT NULL COMMENT '单价', - currency CHAR(3) NOT NULL COMMENT '币种', - amount DECIMAL(18,2) NOT NULL COMMENT '金额', + actual_gross_weight_kg DECIMAL(18,4) NULL COMMENT '仓库实测毛重快照,单位千克', + actual_volume_cbm DECIMAL(18,6) NULL COMMENT '实际体积快照,单位立方米', + volume_weight_kg DECIMAL(18,4) NULL COMMENT '材积重快照,单位千克', + chargeable_weight_kg DECIMAL(18,4) NULL COMMENT '费用计算采用的计费重量,单位千克', + chargeable_volume_cbm DECIMAL(18,6) NULL COMMENT '费用计算采用的计费体积,单位立方米', + unit_price DECIMAL(18,4) NOT NULL COMMENT '费用单价;计价单位由对应费用项目规则确定', + currency CHAR(3) NOT NULL COMMENT '金额币种三字码,如CNY', + amount DECIMAL(18,2) NOT NULL COMMENT '该费用明细金额,币种由currency字段指定', remark VARCHAR(500) NULL COMMENT '备注', - review_status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING、APPROVED、REJECTED', + review_status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT '审核状态:PENDING待审核、APPROVED已通过、REJECTED已驳回', reviewer_id BIGINT NULL COMMENT '审核人ID', reviewer_name VARCHAR(64) NULL COMMENT '审核人名称快照', reviewed_at DATETIME(3) NULL COMMENT '审核时间', review_reason VARCHAR(500) NULL COMMENT '审核驳回原因', - created_by BIGINT NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_by BIGINT NULL, - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - deleted TINYINT(1) NOT NULL DEFAULT 0, - version INT NOT NULL DEFAULT 0, + created_by BIGINT NULL COMMENT '创建人或系统主体ID', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', + updated_by BIGINT NULL COMMENT '最后修改人或系统主体ID', + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后更新时间', + deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记:0未删除、1已删除', + version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', PRIMARY KEY (id), KEY idx_inbound_fee_detail (tenant_id, inbound_id, business_date, deleted), KEY idx_inbound_fee_review (tenant_id, review_status, business_date, deleted), diff --git a/firstleg/src/main/resources/db/phase2-inbound-carton-label.sql b/firstleg/src/main/resources/db/phase2-inbound-carton-label.sql index c3bd7b825..84895235e 100644 --- a/firstleg/src/main/resources/db/phase2-inbound-carton-label.sql +++ b/firstleg/src/main/resources/db/phase2-inbound-carton-label.sql @@ -4,7 +4,7 @@ USE baiyue_firstleg; -- 存量环境可重复执行:首次打印前冻结整单箱号;Print 服务继续保持无状态。 CREATE TABLE IF NOT EXISTS fl_inbound_carton_label ( id BIGINT UNSIGNED NOT NULL COMMENT '实际箱唛头映射ID(雪花ID)', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '当前客户隔离ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID', order_id BIGINT UNSIGNED NOT NULL COMMENT '订单服务订单ID快照', inbound_detail_id BIGINT UNSIGNED NOT NULL COMMENT '订单货物明细ID,不建立跨库外键', diff --git a/firstleg/src/main/resources/db/phase2-inbound-measurement-maintenance.sql b/firstleg/src/main/resources/db/phase2-inbound-measurement-maintenance.sql index db74556b3..11869f9a4 100644 --- a/firstleg/src/main/resources/db/phase2-inbound-measurement-maintenance.sql +++ b/firstleg/src/main/resources/db/phase2-inbound-measurement-maintenance.sql @@ -68,7 +68,7 @@ DROP PROCEDURE IF EXISTS phase2_measure_add_column; CREATE TABLE IF NOT EXISTS fl_inbound_weight_revision ( id BIGINT UNSIGNED NOT NULL COMMENT 'OCR重量修订记录ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID', weight_result_id BIGINT UNSIGNED NOT NULL COMMENT 'OCR重量结果ID', revision_no INT UNSIGNED NOT NULL COMMENT '结果版本号,与乐观锁版本一致', diff --git a/firstleg/src/main/resources/db/phase2-inbound-schema.sql b/firstleg/src/main/resources/db/phase2-inbound-schema.sql index 52474e8c0..95b51d791 100644 --- a/firstleg/src/main/resources/db/phase2-inbound-schema.sql +++ b/firstleg/src/main/resources/db/phase2-inbound-schema.sql @@ -6,17 +6,17 @@ USE baiyue_firstleg; CREATE TABLE IF NOT EXISTS fl_inbound_order ( id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID(雪花ID)', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', task_id BIGINT UNSIGNED NULL COMMENT '头程履约任务ID', order_id BIGINT UNSIGNED NOT NULL COMMENT '订单服务订单ID,不建立跨库外键', order_no VARCHAR(32) NOT NULL COMMENT '订单号快照', - inbound_code VARCHAR(40) NOT NULL COMMENT '租户内唯一入仓码', - process_status VARCHAR(24) NOT NULL DEFAULT 'WAIT_MEASURE' COMMENT '入库流程状态', + inbound_code VARCHAR(40) NOT NULL COMMENT '客户内唯一入仓码', + process_status VARCHAR(24) NOT NULL DEFAULT 'WAIT_MEASURE' COMMENT '入库流程状态:WAIT_MEASURE待测量、MEASURING测量中、MEASURE_PENDING待审核、MEASURE_REJECTED测量驳回、WAIT_PRICING待计费、FEE_CONFIRMING费用确认中、INBOUNDING入库收口中、INBOUNDED已入库、WAIT_SPLIT历史待分柜', order_status_snapshot VARCHAR(24) NOT NULL COMMENT '订单状态快照', - measure_review_status VARCHAR(24) NOT NULL DEFAULT 'NOT_SUBMITTED' COMMENT '测量数据审核状态', - fee_status VARCHAR(24) NOT NULL DEFAULT 'UNPRICED' COMMENT '费用业务状态', - fee_review_status VARCHAR(24) NOT NULL DEFAULT 'NOT_SUBMITTED' COMMENT '费用审核状态', - notification_status VARCHAR(24) NOT NULL DEFAULT 'NOT_CREATED' COMMENT '客户通知状态', + measure_review_status VARCHAR(24) NOT NULL DEFAULT 'NOT_SUBMITTED' COMMENT '测量审核状态:NOT_SUBMITTED未提交、PENDING待审核、APPROVED已通过、REJECTED已驳回', + fee_status VARCHAR(24) NOT NULL DEFAULT 'UNPRICED' COMMENT '费用状态:UNPRICED未计费、CALCULATED已计算、CONFIRMED已确认、SENT已发送、VOIDED已作废', + fee_review_status VARCHAR(24) NOT NULL DEFAULT 'NOT_SUBMITTED' COMMENT '费用审核状态:NOT_SUBMITTED未提交、PENDING待审核、APPROVED已通过、REJECTED已驳回、CONFIRMED已确认', + notification_status VARCHAR(24) NOT NULL DEFAULT 'NOT_CREATED' COMMENT '客户通知状态:NOT_CREATED未创建、PENDING待通知', expected_arrival_at DATETIME(3) NULL COMMENT '预计到达时间', customer_id BIGINT UNSIGNED NULL COMMENT '客户逻辑ID', customer_name VARCHAR(128) NOT NULL COMMENT '客户名称快照', @@ -79,7 +79,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_order ( CREATE TABLE IF NOT EXISTS fl_inbound_pickup_snapshot ( id BIGINT UNSIGNED NOT NULL COMMENT '提货信息快照ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID', pickup_method VARCHAR(24) NULL COMMENT '提货方式', pickup_at DATETIME(3) NULL COMMENT '提货时间', @@ -100,7 +100,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_pickup_snapshot ( CREATE TABLE IF NOT EXISTS fl_inbound_destination_snapshot ( id BIGINT UNSIGNED NOT NULL COMMENT '目的仓地址快照ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID', destination_type VARCHAR(24) NULL COMMENT '目的仓类型', destination_code VARCHAR(32) NULL COMMENT '目的仓编码', @@ -120,7 +120,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_destination_snapshot ( CREATE TABLE IF NOT EXISTS fl_inbound_transport_snapshot ( id BIGINT UNSIGNED NOT NULL COMMENT '运输信息快照ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID', loading_status VARCHAR(24) NULL COMMENT '配载状态快照', container_no VARCHAR(64) NULL COMMENT '集装箱号', @@ -142,7 +142,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_transport_snapshot ( CREATE TABLE IF NOT EXISTS fl_inbound_carton_label ( id BIGINT UNSIGNED NOT NULL COMMENT '实际箱唛头映射ID(雪花ID)', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '当前客户隔离ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID', order_id BIGINT UNSIGNED NOT NULL COMMENT '订单服务订单ID快照', inbound_detail_id BIGINT UNSIGNED NOT NULL COMMENT '订单货物明细ID,不建立跨库外键', @@ -167,7 +167,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_carton_label ( CREATE TABLE IF NOT EXISTS fl_inbound_measurement ( id BIGINT UNSIGNED NOT NULL COMMENT '测量批次ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID', source_type VARCHAR(24) NOT NULL COMMENT '数据来源:THIRD_PARTY或MANUAL', source_code VARCHAR(64) NULL COMMENT '第三方应用或来源编码', @@ -176,7 +176,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_measurement ( device_code VARCHAR(64) NULL COMMENT '测量设备编码', warehouse_code VARCHAR(50) NULL COMMENT '第三方回传仓库编码', measured_at DATETIME(3) NULL COMMENT '设备测量时间', - status VARCHAR(24) NOT NULL DEFAULT 'RECEIVED' COMMENT '测量批次状态', + status VARCHAR(24) NOT NULL DEFAULT 'RECEIVED' COMMENT '测量批次状态:DRAFT草稿、RECEIVED已接收、SUBMITTED已提交、APPROVED已通过、REJECTED已驳回', raw_payload_json JSON NULL COMMENT '第三方原始报文', original_document_no VARCHAR(64) NULL COMMENT '原单据号码快照', original_gross_weight_kg DECIMAL(18,3) NULL COMMENT '原单据毛重kg', @@ -198,7 +198,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_measurement ( CREATE TABLE IF NOT EXISTS fl_inbound_measurement_item ( id BIGINT UNSIGNED NOT NULL COMMENT '测量明细ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', measurement_id BIGINT UNSIGNED NOT NULL COMMENT '测量批次ID', inbound_detail_id BIGINT UNSIGNED NULL COMMENT '绑定的订单货物明细ID,平台手工明细可为空', external_detail_no VARCHAR(128) NULL COMMENT '第三方测量明细唯一号,平台手工明细可为空', @@ -238,7 +238,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_measurement_item ( CREATE TABLE IF NOT EXISTS fl_mojia_measurement_receipt ( id BIGINT UNSIGNED NOT NULL COMMENT '墨家联调收件记录ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', subject_id BIGINT UNSIGNED NOT NULL COMMENT '设备应用操作主体ID', box_code VARCHAR(128) NOT NULL COMMENT '设备扫描的原始箱码', scanned_shipment_id VARCHAR(64) NULL COMMENT '设备从Amazon FBA箱标实扫的Shipment ID', @@ -249,7 +249,7 @@ CREATE TABLE IF NOT EXISTS fl_mojia_measurement_receipt ( machine_no VARCHAR(64) NOT NULL COMMENT '设备编号', measured_at DATETIME(3) NOT NULL COMMENT '设备测量时间或服务接收时间', image_file_id BIGINT UNSIGNED NOT NULL COMMENT 'File服务中的墨家联调图片ID', - status VARCHAR(24) NOT NULL DEFAULT 'UNMATCHED' COMMENT '收件状态', + status VARCHAR(24) NOT NULL DEFAULT 'UNMATCHED' COMMENT '测量回执匹配状态:UNMATCHED未匹配;当前首版仅记录未匹配回执', match_failure_code VARCHAR(16) NOT NULL COMMENT '正式业务匹配失败码', match_failure_reason VARCHAR(500) NOT NULL COMMENT '脱敏后的匹配失败原因', request_id VARCHAR(128) NULL COMMENT '请求链路ID', @@ -263,7 +263,7 @@ CREATE TABLE IF NOT EXISTS fl_mojia_measurement_receipt ( CREATE TABLE IF NOT EXISTS fl_inbound_weight_result ( id BIGINT UNSIGNED NOT NULL COMMENT 'OCR重量结果ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID', file_id BIGINT UNSIGNED NOT NULL COMMENT '称重图片文件ID', ocr_provider VARCHAR(64) NULL COMMENT 'OCR服务商编码', @@ -274,7 +274,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_weight_result ( adjusted_weight_value DECIMAL(18,3) NULL COMMENT '人工修正后的原单位重量', adjusted_weight_unit VARCHAR(8) NULL COMMENT '人工修正后的单位', final_weight_kg DECIMAL(18,3) NULL COMMENT '确认并统一换算后的重量kg', - recognition_status VARCHAR(24) NOT NULL DEFAULT 'PENDING' COMMENT '识别和确认状态', + recognition_status VARCHAR(24) NOT NULL DEFAULT 'PENDING' COMMENT '重量识别状态:PENDING待识别、PENDING_CONFIRM待确认、NEEDS_MANUAL需人工录入、CONFIRMED已确认', adjustment_reason VARCHAR(500) NULL COMMENT '人工修正原因', adjusted_by BIGINT UNSIGNED NULL COMMENT '修正人ID', adjusted_at DATETIME(3) NULL COMMENT '修正时间', @@ -294,7 +294,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_weight_result ( CREATE TABLE IF NOT EXISTS fl_inbound_weight_revision ( id BIGINT UNSIGNED NOT NULL COMMENT 'OCR重量修订记录ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID', weight_result_id BIGINT UNSIGNED NOT NULL COMMENT 'OCR重量结果ID', revision_no INT UNSIGNED NOT NULL COMMENT '结果版本号,与乐观锁版本一致', @@ -317,7 +317,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_weight_revision ( CREATE TABLE IF NOT EXISTS fl_measure_review_record ( id BIGINT UNSIGNED NOT NULL COMMENT '测量审核记录ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID', measurement_id BIGINT UNSIGNED NULL COMMENT '被审核的测量批次ID', review_result VARCHAR(24) NOT NULL COMMENT '审核结果', @@ -337,8 +337,8 @@ CREATE TABLE IF NOT EXISTS fl_measure_review_record ( CREATE TABLE IF NOT EXISTS fl_customer_charge_rule ( id BIGINT UNSIGNED NOT NULL COMMENT '客户计费规则ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', - rule_code VARCHAR(50) NOT NULL COMMENT '租户内规则编码', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + rule_code VARCHAR(50) NOT NULL COMMENT '客户内唯一计费规则编码', rule_version INT UNSIGNED NOT NULL COMMENT '规则版本号', customer_id BIGINT UNSIGNED NOT NULL COMMENT '客户逻辑ID', customer_name VARCHAR(128) NOT NULL COMMENT '客户名称快照', @@ -351,7 +351,7 @@ CREATE TABLE IF NOT EXISTS fl_customer_charge_rule ( currency CHAR(3) NOT NULL COMMENT '默认币种', effective_from DATETIME(3) NOT NULL COMMENT '生效开始时间', effective_to DATETIME(3) NULL COMMENT '生效结束时间', - status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '规则状态', + status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '规则状态:ACTIVE启用、DISABLED停用', remark VARCHAR(500) NULL COMMENT '规则备注', created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', @@ -394,7 +394,7 @@ CREATE TABLE IF NOT EXISTS fl_carrier_fba_rate ( source_destination_text VARCHAR(1000) NULL COMMENT '原始组合仓库或区域文本', effective_from DATETIME(3) NOT NULL COMMENT '生效开始时间', effective_to DATETIME(3) NULL COMMENT '生效结束时间', - status VARCHAR(24) NOT NULL DEFAULT 'ACTIVE' COMMENT '价格状态:PENDING_CONFIRMATION、ACTIVE或SUPERSEDED', + status VARCHAR(24) NOT NULL DEFAULT 'ACTIVE' COMMENT '价格状态:PENDING_CONFIRMATION、ACTIVE或SUPERSEDED;分别表示待确认、已生效、已被替代;FAILED表示解析失败', remark VARCHAR(500) NULL COMMENT '价格备注', created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', @@ -417,9 +417,9 @@ CREATE TABLE IF NOT EXISTS fl_price_channel_catalog ( price_model VARCHAR(32) NOT NULL COMMENT '默认价格模型', source_sheet_name VARCHAR(128) NOT NULL COMMENT '来源Sheet名称;海派为同一Sheet', aliases_json JSON NOT NULL COMMENT '上传名称别名数组', - status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '目录状态', - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '目录状态:ACTIVE启用、DISABLED停用', + 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 (id), UNIQUE KEY uk_price_channel_code (channel_code), KEY idx_price_channel_status (status, price_model) @@ -429,9 +429,9 @@ CREATE TABLE IF NOT EXISTS fl_price_origin_alias ( id BIGINT UNSIGNED NOT NULL COMMENT '起运仓别名ID', warehouse_code VARCHAR(64) NOT NULL COMMENT '平台仓库稳定编码,不存环境专属ID', alias_name VARCHAR(128) NOT NULL COMMENT 'Excel起运仓名称别名', - status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE', - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '状态:ACTIVE启用、DISABLED停用', + 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 (id), UNIQUE KEY uk_price_origin_alias (warehouse_code, alias_name), KEY idx_price_origin_alias_name (alias_name, status) @@ -444,9 +444,9 @@ CREATE TABLE IF NOT EXISTS fl_price_warehouse_mapping ( warehouse_id BIGINT UNSIGNED NOT NULL COMMENT '固定平台公共仓库ID', warehouse_code VARCHAR(64) NOT NULL COMMENT '平台仓库稳定编码快照', warehouse_type VARCHAR(16) NOT NULL COMMENT 'DOMESTIC或OVERSEAS', - status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE', - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '状态:ACTIVE启用、DISABLED停用', + 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 (id), UNIQUE KEY uk_price_warehouse_mapping_source (source_type, source_value), KEY idx_price_warehouse_mapping_target (warehouse_id, status) @@ -459,20 +459,20 @@ CREATE TABLE IF NOT EXISTS fl_price_book_version ( original_file_name VARCHAR(255) NOT NULL COMMENT '原始Excel文件名', source_file_id BIGINT UNSIGNED NULL COMMENT 'File服务原始文件ID', source_sha256 CHAR(64) NULL COMMENT '原始文件SHA-256', - status VARCHAR(24) NOT NULL DEFAULT 'PENDING_CONFIRMATION' COMMENT '待确认上传记录;ACTIVE/SUPERSEDED/FAILED', + status VARCHAR(24) NOT NULL DEFAULT 'PENDING_CONFIRMATION' COMMENT '价格版本状态:PENDING_CONFIRMATION待确认、ACTIVE已生效、SUPERSEDED已被替代、FAILED解析失败', blocking_error_count INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '阻断错误数', warning_count INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '警告数', parse_issues_json JSON NULL COMMENT '上传解析问题及待确认重新校验结果', channel_count INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '解析渠道数', rate_count INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '解析价格数', - created_by BIGINT UNSIGNED NOT NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - published_by BIGINT UNSIGNED NULL, + created_by BIGINT UNSIGNED NOT NULL COMMENT '上传价格表的操作人ID', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '上传记录创建时间', + published_by BIGINT UNSIGNED NULL COMMENT '确认并发布价格版本的操作人ID', published_at DATETIME(3) NULL COMMENT '确认并生效时间', superseded_at DATETIME(3) NULL COMMENT '被下一版本替换时间', - updated_by BIGINT UNSIGNED NOT NULL, - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - deleted TINYINT(1) NOT NULL DEFAULT 0, + updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改人ID', + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后更新时间', + deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记:0未删除、1已删除', version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', PRIMARY KEY (id), UNIQUE KEY uk_price_book_version (tenant_id, version_no), @@ -496,9 +496,9 @@ CREATE TABLE IF NOT EXISTS fl_price_book_channel ( surcharge_description VARCHAR(2000) NULL COMMENT '附加费说明,仅展示', remark VARCHAR(2000) NULL COMMENT '渠道备注', created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID', - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '渠道快照创建时间', updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改人ID', - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '渠道快照最后更新时间', deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记', version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', PRIMARY KEY (id), @@ -552,9 +552,9 @@ ON DUPLICATE KEY UPDATE CREATE TABLE IF NOT EXISTS fl_inbound_fee ( id BIGINT UNSIGNED NOT NULL COMMENT '入库费用ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID', - fee_no VARCHAR(40) NOT NULL COMMENT '租户内唯一费用单号', + fee_no VARCHAR(40) NOT NULL COMMENT '客户内唯一费用单号', fee_version INT UNSIGNED NOT NULL DEFAULT 1 COMMENT '费用快照版本', charge_rule_id BIGINT UNSIGNED NULL COMMENT '历史客户计费规则ID;渠道价格可为空', charge_rule_version INT UNSIGNED NULL COMMENT '历史客户规则版本快照', @@ -594,8 +594,8 @@ CREATE TABLE IF NOT EXISTS fl_inbound_fee ( subtotal_amount DECIMAL(18,2) NOT NULL COMMENT '基础费用金额', additional_amount DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '附加费金额', total_amount DECIMAL(18,2) NOT NULL COMMENT '总金额', - fee_status VARCHAR(24) NOT NULL DEFAULT 'CALCULATED' COMMENT '费用状态', - review_status VARCHAR(24) NOT NULL DEFAULT 'NOT_SUBMITTED' COMMENT '费用审核状态', + fee_status VARCHAR(24) NOT NULL DEFAULT 'CALCULATED' COMMENT '费用状态:CALCULATED已计算、SUPERSEDED已被新版本替代', + review_status VARCHAR(24) NOT NULL DEFAULT 'NOT_SUBMITTED' COMMENT '费用审核状态:NOT_SUBMITTED未提交、PENDING待审核、APPROVED已通过、REJECTED已驳回、CONFIRMED已确认', exception_release TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否基于测量异常放行生成', calculated_by BIGINT UNSIGNED NOT NULL COMMENT '试算人ID', calculated_at DATETIME(3) NOT NULL COMMENT '试算时间', @@ -616,7 +616,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_fee ( CREATE TABLE IF NOT EXISTS fl_inbound_fee_item ( id BIGINT UNSIGNED NOT NULL COMMENT '费用明细ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', fee_id BIGINT UNSIGNED NOT NULL COMMENT '费用头ID', item_no INT UNSIGNED NOT NULL COMMENT '费用单内序号', fee_code VARCHAR(50) NOT NULL COMMENT '费用项目编码', @@ -636,7 +636,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_fee_item ( CREATE TABLE IF NOT EXISTS fl_inbound_pallet_allocation ( id BIGINT UNSIGNED NOT NULL COMMENT '头程托盘分配引用ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID', allocation_no INT UNSIGNED NOT NULL COMMENT '入库单内分配序号', warehouse_id BIGINT UNSIGNED NOT NULL COMMENT '仓库服务仓库ID', @@ -650,7 +650,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_pallet_allocation ( location_code VARCHAR(50) NOT NULL COMMENT '库位编码快照', reservation_id BIGINT UNSIGNED NULL COMMENT '仓库预占资源唯一ID,A0共享场景用于区分托盘', reservation_no VARCHAR(64) NOT NULL COMMENT '仓库库位预占单号', - allocation_status VARCHAR(24) NOT NULL DEFAULT 'RESERVED' COMMENT '分配状态', + allocation_status VARCHAR(24) NOT NULL DEFAULT 'RESERVED' COMMENT '托盘库位分配状态:RESERVED已预占、CONFIRMED已确认、TRANSFERRED已转移、RELEASED已释放', 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 (id), @@ -663,7 +663,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_pallet_allocation ( CREATE TABLE IF NOT EXISTS fl_inbound_file ( id BIGINT UNSIGNED NOT NULL COMMENT '入库单文件关系ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID', file_id BIGINT UNSIGNED NOT NULL COMMENT '文件服务文件ID', file_type VARCHAR(24) NOT NULL COMMENT '文件类型:RELATED、MARK、PC_ORDER、MEASURE或WEIGHT', @@ -681,7 +681,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_file ( CREATE TABLE IF NOT EXISTS fl_inbound_hold_record ( id BIGINT UNSIGNED NOT NULL COMMENT '扣货记录ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID', hold_action VARCHAR(16) NOT NULL COMMENT '动作:HOLD或RELEASE', reason VARCHAR(1000) NOT NULL COMMENT '扣货或解除原因', @@ -696,7 +696,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_hold_record ( CREATE TABLE IF NOT EXISTS fl_inbound_internal_note ( id BIGINT UNSIGNED NOT NULL COMMENT '客服内部备注ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID', content VARCHAR(2000) NOT NULL COMMENT '仅内部可见备注', created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID', @@ -709,7 +709,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_internal_note ( CREATE TABLE IF NOT EXISTS fl_inbound_status_history ( id BIGINT UNSIGNED NOT NULL COMMENT '入库状态历史ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID', from_status VARCHAR(24) NULL COMMENT '变更前状态', to_status VARCHAR(24) NOT NULL COMMENT '变更后状态', @@ -725,12 +725,12 @@ CREATE TABLE IF NOT EXISTS fl_inbound_status_history ( CREATE TABLE IF NOT EXISTS fl_inbound_outbox ( id BIGINT UNSIGNED NOT NULL COMMENT '领域事件全局ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '事件所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '事件所属客户ID和数据隔离键', aggregate_type VARCHAR(32) NOT NULL DEFAULT 'INBOUND' COMMENT '聚合类型', aggregate_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID', event_type VARCHAR(64) NOT NULL COMMENT '事件类型', payload_json JSON NOT NULL COMMENT '事件负载快照', - publish_status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT '发布状态', + publish_status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT '事件发布状态:PENDING待发布、PUBLISHED已发布、FAILED发布失败', retry_count INT NOT NULL DEFAULT 0 COMMENT '重试次数', next_retry_at DATETIME(3) NULL COMMENT '下次重试时间', published_at DATETIME(3) NULL COMMENT '发布时间', @@ -741,13 +741,13 @@ CREATE TABLE IF NOT EXISTS fl_inbound_outbox ( CREATE TABLE IF NOT EXISTS fl_inbound_idempotency ( id BIGINT UNSIGNED NOT NULL COMMENT '入库写操作幂等记录ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', operation VARCHAR(40) NOT NULL COMMENT '操作类型', operator_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '操作者或外部应用ID', inbound_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '入库单ID,创建前可为0', idempotency_key VARCHAR(128) NOT NULL COMMENT '幂等键', request_hash CHAR(64) NOT NULL COMMENT '请求内容摘要', - status VARCHAR(16) NOT NULL COMMENT '处理状态', + status VARCHAR(16) NOT NULL COMMENT '幂等处理状态:PROCESSING处理中、SUCCEEDED已成功、FAILED已失败', result_json JSON NULL COMMENT '成功结果摘要', result_resource_id BIGINT UNSIGNED NULL COMMENT '成功结果资源ID', completed_at DATETIME(3) NULL COMMENT '完成时间', @@ -760,7 +760,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_idempotency ( CREATE TABLE IF NOT EXISTS fl_customer_dimension_config ( id BIGINT UNSIGNED NOT NULL COMMENT '配置ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', box_type VARCHAR(24) NOT NULL COMMENT '固定箱规类型:SMALL/MEDIUM/LARGE/EXTRA_LARGE', inner_length_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内长(厘米)', inner_width_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内宽(厘米)', diff --git a/firstleg/src/main/resources/db/phase2-published-freight-detail-migration.sql b/firstleg/src/main/resources/db/phase2-published-freight-detail-migration.sql index 4bb4cdd04..9390524ef 100644 --- a/firstleg/src/main/resources/db/phase2-published-freight-detail-migration.sql +++ b/firstleg/src/main/resources/db/phase2-published-freight-detail-migration.sql @@ -422,7 +422,7 @@ CALL phase2_publish_add_column('fl_inbound_fee_detail', 'chargeable_quantity', CALL phase2_publish_add_column('fl_inbound_fee_detail', 'charge_unit', "VARCHAR(16) NULL COMMENT 'KG或CBM' AFTER chargeable_quantity"); CALL phase2_publish_add_column('fl_inbound_fee_detail', 'publication_status', - "VARCHAR(16) NOT NULL DEFAULT 'DRAFT' COMMENT 'DRAFT、PUBLISHED、SUPERSEDED' AFTER review_reason"); + "VARCHAR(16) NOT NULL DEFAULT 'DRAFT' COMMENT '费用发布状态:DRAFT草稿、PUBLISHED已发布、SUPERSEDED已替代' AFTER review_reason"); CALL phase2_publish_add_column('fl_inbound_fee_detail', 'published_by', "BIGINT NULL COMMENT '发布人ID' AFTER publication_status"); CALL phase2_publish_add_column('fl_inbound_fee_detail', 'published_by_name', diff --git a/firstleg/src/main/resources/db/phase3-carrier-schedule-query-schema.sql b/firstleg/src/main/resources/db/phase3-carrier-schedule-query-schema.sql index 5bf946d83..93a182dc1 100644 --- a/firstleg/src/main/resources/db/phase3-carrier-schedule-query-schema.sql +++ b/firstleg/src/main/resources/db/phase3-carrier-schedule-query-schema.sql @@ -2,38 +2,38 @@ SET NAMES utf8mb4; USE baiyue_firstleg; CREATE TABLE IF NOT EXISTS fl_carrier_schedule_query ( - id BIGINT UNSIGNED NOT NULL, - query_no VARCHAR(40) NOT NULL, - customer_id BIGINT UNSIGNED NOT NULL COMMENT '当前产品客户ID', - ocean_booking_id BIGINT UNSIGNED NOT NULL, - container_booking_id BIGINT UNSIGNED NULL, - container_scope_id BIGINT UNSIGNED GENERATED ALWAYS AS (COALESCE(container_booking_id,0)) STORED, - carrier_code VARCHAR(16) NOT NULL COMMENT 'MATSON/SML/ZIM/COSCO/EMC/HEDE', - tracking_type VARCHAR(32) NOT NULL COMMENT 'BOOKING_NO/BILL_OF_LADING/CONTAINER_NO', - tracking_no VARCHAR(128) NOT NULL, - official_url VARCHAR(500) NOT NULL, - access_mode VARCHAR(32) NOT NULL COMMENT 'PUBLIC_FORM/HCAPTCHA/DYNAMIC_IFRAME', - status VARCHAR(24) NOT NULL COMMENT 'READY/MANUAL_REQUIRED/SUCCESS', - window_started_at DATETIME(3) NOT NULL, - attempt_count INT UNSIGNED NOT NULL DEFAULT 0, - next_allowed_at DATETIME(3) NOT NULL, - last_attempt_at DATETIME(3) NULL, - last_success_at DATETIME(3) NULL, - source_updated_at DATETIME(3) NULL, - expires_at DATETIME(3) NULL, - result_port_schedule_id BIGINT UNSIGNED NULL, - last_attempt_status VARCHAR(24) NULL, - manual_reason VARCHAR(1000) NULL, - last_error_code VARCHAR(64) NULL, - last_error_message VARCHAR(1000) NULL, - remark VARCHAR(1000) NULL, - created_by BIGINT UNSIGNED NOT NULL, - created_by_name VARCHAR(64) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_by BIGINT UNSIGNED NOT NULL, - updated_by_name VARCHAR(64) NULL, - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - version INT UNSIGNED NOT NULL DEFAULT 0, + id BIGINT UNSIGNED NOT NULL COMMENT '船司官网查询任务ID', + query_no VARCHAR(40) NOT NULL COMMENT '船司官网查询任务编号', + customer_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + ocean_booking_id BIGINT UNSIGNED NOT NULL COMMENT '关联海运订舱ID', + container_booking_id BIGINT UNSIGNED NULL COMMENT '指定查询的柜次ID;整票订舱查询时为空', + container_scope_id BIGINT UNSIGNED GENERATED ALWAYS AS (COALESCE(container_booking_id,0)) STORED COMMENT '柜次唯一作用域计算值;整票订舱查询固定为0', + carrier_code VARCHAR(16) NOT NULL COMMENT '船公司编码:MATSON美森、SML森罗、ZIM以星、COSCO中远海运、EMC长荣、HEDE合德', + tracking_type VARCHAR(32) NOT NULL COMMENT '官网跟踪类型:BOOKING_NO订舱号、BILL_OF_LADING提单号、CONTAINER_NO箱号', + tracking_no VARCHAR(128) NOT NULL COMMENT '提交至船公司官网的跟踪号码', + official_url VARCHAR(500) NOT NULL COMMENT '船公司官网查询入口地址快照', + access_mode VARCHAR(32) NOT NULL COMMENT '官网访问方式:PUBLIC_FORM公开表单、HCAPTCHA人机校验、DYNAMIC_IFRAME动态页面', + status VARCHAR(24) NOT NULL COMMENT '任务状态:READY可查询、MANUAL_REQUIRED需人工处理、SUCCESS已取得有效结果', + window_started_at DATETIME(3) NOT NULL COMMENT '当前三天查询窗口起始时间', + attempt_count INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '当前查询窗口已执行次数;最多3次', + next_allowed_at DATETIME(3) NOT NULL COMMENT '频控规则允许的下次查询时间', + last_attempt_at DATETIME(3) NULL COMMENT '最近一次查询尝试时间', + last_success_at DATETIME(3) NULL COMMENT '最近一次取得有效结果时间', + source_updated_at DATETIME(3) NULL COMMENT '船公司官网展示的结果更新时间', + expires_at DATETIME(3) NULL COMMENT '当前最近有效结果过期时间;到期后仍保留但标记过期', + result_port_schedule_id BIGINT UNSIGNED NULL COMMENT '最近有效结果对应的船期记录ID', + last_attempt_status VARCHAR(24) NULL COMMENT '最近查询尝试状态快照', + manual_reason VARCHAR(1000) NULL COMMENT '需要人工查询或录入的原因', + last_error_code VARCHAR(64) NULL COMMENT '最近查询失败错误码', + last_error_message VARCHAR(1000) NULL COMMENT '最近查询失败错误说明', + remark VARCHAR(1000) NULL COMMENT '平台内部备注', + created_by BIGINT UNSIGNED NOT NULL COMMENT '创建操作人ID', + created_by_name VARCHAR(64) NULL COMMENT '创建操作人姓名快照', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '任务创建时间', + updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改操作人ID', + updated_by_name VARCHAR(64) NULL COMMENT '最后修改操作人姓名快照', + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '任务最后更新时间', + version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', PRIMARY KEY (id), UNIQUE KEY uk_carrier_schedule_query_no (query_no), UNIQUE KEY uk_carrier_schedule_query_scope @@ -57,21 +57,21 @@ CREATE TABLE IF NOT EXISTS fl_carrier_schedule_query ( ) ENGINE=InnoDB COMMENT='六家船公司官网查询任务与三天窗口频控'; CREATE TABLE IF NOT EXISTS fl_carrier_schedule_query_attempt ( - id BIGINT UNSIGNED NOT NULL, - query_id BIGINT UNSIGNED NOT NULL, - window_attempt_no INT UNSIGNED NOT NULL, - carrier_code VARCHAR(16) NOT NULL, - access_mode VARCHAR(32) NOT NULL, - official_url VARCHAR(500) NOT NULL, - attempt_status VARCHAR(24) NOT NULL, - coverage_gap TINYINT(1) NOT NULL DEFAULT 0, - coverage_gap_code VARCHAR(64) NULL, - manual_reason VARCHAR(1000) NULL, - error_code VARCHAR(64) NULL, - error_message VARCHAR(1000) NULL, - operator_id BIGINT UNSIGNED NOT NULL, - operator_name VARCHAR(64) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + id BIGINT UNSIGNED NOT NULL COMMENT '船司官网查询尝试ID', + query_id BIGINT UNSIGNED NOT NULL COMMENT '所属船司官网查询任务ID', + window_attempt_no INT UNSIGNED NOT NULL COMMENT '当前三天窗口内的尝试序号;取值1至3', + carrier_code VARCHAR(16) NOT NULL COMMENT '本次尝试的船公司编码快照', + access_mode VARCHAR(32) NOT NULL COMMENT '本次尝试的官网访问方式快照', + official_url VARCHAR(500) NOT NULL COMMENT '本次尝试使用的官网地址快照', + attempt_status VARCHAR(24) NOT NULL COMMENT '尝试结果:MANUAL_REQUIRED需人工处理、SUCCESS成功、FAILED失败', + coverage_gap TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否存在自动化覆盖缺口:0否、1是', + coverage_gap_code VARCHAR(64) NULL COMMENT '自动化覆盖缺口编码', + manual_reason VARCHAR(1000) NULL COMMENT '转人工处理原因', + error_code VARCHAR(64) NULL COMMENT '查询失败错误码', + error_message VARCHAR(1000) NULL COMMENT '查询失败错误说明', + operator_id BIGINT UNSIGNED NOT NULL COMMENT '执行本次尝试的操作人ID', + operator_name VARCHAR(64) NULL COMMENT '执行操作人姓名快照', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '尝试记录创建时间', PRIMARY KEY (id), KEY idx_carrier_schedule_attempt_query (query_id,created_at,id), CONSTRAINT fk_carrier_schedule_attempt_query FOREIGN KEY (query_id) REFERENCES fl_carrier_schedule_query(id), @@ -81,17 +81,17 @@ CREATE TABLE IF NOT EXISTS fl_carrier_schedule_query_attempt ( ) ENGINE=InnoDB COMMENT='船司官网查询尝试不可变审计'; CREATE TABLE IF NOT EXISTS fl_carrier_schedule_query_history ( - id BIGINT UNSIGNED NOT NULL, - query_id BIGINT UNSIGNED NOT NULL, - action VARCHAR(24) NOT NULL, - from_version INT UNSIGNED NULL, - to_version INT UNSIGNED NOT NULL, - reason VARCHAR(1000) NULL, - before_snapshot JSON NULL, - after_snapshot JSON NOT NULL, - operator_id BIGINT UNSIGNED NOT NULL, - operator_name VARCHAR(64) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + id BIGINT UNSIGNED NOT NULL COMMENT '船司查询任务历史ID', + query_id BIGINT UNSIGNED NOT NULL COMMENT '所属船司官网查询任务ID', + action VARCHAR(24) NOT NULL COMMENT '历史动作:CREATE创建、ATTEMPT执行查询、MANUAL_RESULT录入人工结果', + from_version INT UNSIGNED NULL COMMENT '变更前任务版本;创建时为空', + to_version INT UNSIGNED NOT NULL COMMENT '变更后任务版本', + reason VARCHAR(1000) NULL COMMENT '操作原因或说明', + before_snapshot JSON NULL COMMENT '变更前任务数据快照;创建时为空', + after_snapshot JSON NOT NULL COMMENT '变更后任务数据快照', + operator_id BIGINT UNSIGNED NOT NULL COMMENT '操作人ID', + operator_name VARCHAR(64) NULL COMMENT '操作人姓名快照', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '历史记录创建时间', PRIMARY KEY (id), KEY idx_carrier_schedule_history_query (query_id,created_at,id), CONSTRAINT fk_carrier_schedule_history_query FOREIGN KEY (query_id) REFERENCES fl_carrier_schedule_query(id), @@ -99,16 +99,16 @@ CREATE TABLE IF NOT EXISTS fl_carrier_schedule_query_history ( ) ENGINE=InnoDB COMMENT='船司查询任务不可变历史'; CREATE TABLE IF NOT EXISTS fl_carrier_schedule_query_operation ( - id BIGINT UNSIGNED NOT NULL, - query_id BIGINT UNSIGNED NULL, - operator_id BIGINT UNSIGNED NOT NULL, - operation VARCHAR(24) NOT NULL, - idempotency_key VARCHAR(128) NOT NULL, - request_hash CHAR(64) NOT NULL, - status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING', - result_json JSON NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - completed_at DATETIME(3) NULL, + id BIGINT UNSIGNED NOT NULL COMMENT '船司查询写操作幂等记录ID', + query_id BIGINT UNSIGNED NULL COMMENT '关联查询任务ID;创建任务落库前允许为空', + operator_id BIGINT UNSIGNED NOT NULL COMMENT '发起写操作的平台操作人ID', + operation VARCHAR(24) NOT NULL COMMENT '幂等写操作类型', + idempotency_key VARCHAR(128) NOT NULL COMMENT '客户端写操作幂等键', + request_hash CHAR(64) NOT NULL COMMENT '规范化请求内容SHA-256摘要;用于识别同键不同请求', + status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING' COMMENT '处理状态:PROCESSING处理中、SUCCESS成功', + result_json JSON NULL COMMENT '成功结果JSON快照;供幂等重放返回', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '幂等记录创建时间', + completed_at DATETIME(3) NULL COMMENT '写操作成功完成时间', PRIMARY KEY (id), UNIQUE KEY uk_carrier_schedule_operation (operator_id,idempotency_key), KEY idx_carrier_schedule_operation_query (query_id,created_at,id), diff --git a/firstleg/src/main/resources/db/phase3-customs-schema.sql b/firstleg/src/main/resources/db/phase3-customs-schema.sql index f6c8925e8..84ffef072 100644 --- a/firstleg/src/main/resources/db/phase3-customs-schema.sql +++ b/firstleg/src/main/resources/db/phase3-customs-schema.sql @@ -2,17 +2,17 @@ SET NAMES utf8mb4; USE baiyue_firstleg; CREATE TABLE IF NOT EXISTS fl_customs_checklist_version ( - id BIGINT UNSIGNED NOT NULL, - checklist_code VARCHAR(40) NOT NULL, - version_no VARCHAR(32) NOT NULL, - country_code VARCHAR(32) NOT NULL, - channel_code VARCHAR(40) NOT NULL, - cargo_type VARCHAR(40) NOT NULL, - items_json JSON NOT NULL, - status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE', - created_by BIGINT UNSIGNED NOT NULL, - created_by_name VARCHAR(64) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + id BIGINT UNSIGNED NOT NULL COMMENT '报关资料清单版本逻辑ID', + checklist_code VARCHAR(40) NOT NULL COMMENT '报关资料清单编码;同一清单的多个版本共用', + version_no VARCHAR(32) NOT NULL COMMENT '清单业务版本号', + country_code VARCHAR(32) NOT NULL COMMENT '清单适用的目的国家或地区编码', + channel_code VARCHAR(40) NOT NULL COMMENT '清单适用的运输渠道编码', + cargo_type VARCHAR(40) NOT NULL COMMENT '清单适用的货物类型编码', + items_json JSON NOT NULL COMMENT '不可变清单项目数组;元素包含code项目编码、name项目名称和required是否必填', + status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '清单版本状态:ACTIVE启用、RETIRED停用', + created_by BIGINT UNSIGNED NOT NULL COMMENT '创建清单版本的平台操作人ID', + created_by_name VARCHAR(64) NULL COMMENT '创建清单版本时的平台操作人名称快照', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '清单版本创建时间', PRIMARY KEY (id), UNIQUE KEY uk_customs_checklist_version (checklist_code, version_no), KEY idx_customs_checklist_match (country_code, channel_code, cargo_type, status), @@ -20,24 +20,24 @@ CREATE TABLE IF NOT EXISTS fl_customs_checklist_version ( ) ENGINE=InnoDB COMMENT='三期报关资料清单不可变版本'; CREATE TABLE IF NOT EXISTS fl_customs_case ( - id BIGINT UNSIGNED NOT NULL, - customs_no VARCHAR(40) NOT NULL, - tenant_id BIGINT UNSIGNED NOT NULL, - order_id BIGINT UNSIGNED NOT NULL, - inbound_id BIGINT UNSIGNED NOT NULL, - container_booking_id BIGINT UNSIGNED NOT NULL DEFAULT 0, - checklist_version_id BIGINT UNSIGNED NOT NULL, - checklist_code VARCHAR(40) NOT NULL, - checklist_version_no VARCHAR(32) NOT NULL, - status VARCHAR(32) NOT NULL DEFAULT 'DOCUMENTS_PENDING', - remark VARCHAR(500) NULL, - created_by BIGINT UNSIGNED NOT NULL, - created_by_name VARCHAR(64) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_by BIGINT UNSIGNED NOT NULL, - updated_by_name VARCHAR(64) NULL, - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - version INT UNSIGNED NOT NULL DEFAULT 0, + id BIGINT UNSIGNED NOT NULL COMMENT '报关协同单逻辑ID', + customs_no VARCHAR(40) NOT NULL COMMENT '全局唯一报关协同单号', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + order_id BIGINT UNSIGNED NOT NULL COMMENT 'Order服务订单逻辑ID;跨库不建立外键', + inbound_id BIGINT UNSIGNED NOT NULL COMMENT 'Firstleg入库单逻辑ID', + container_booking_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '关联柜次逻辑ID;0表示创建时尚未关联柜次', + checklist_version_id BIGINT UNSIGNED NOT NULL COMMENT '本单冻结使用的报关资料清单版本ID', + checklist_code VARCHAR(40) NOT NULL COMMENT '创建协同单时的清单编码快照;源清单变化不回写', + checklist_version_no VARCHAR(32) NOT NULL COMMENT '创建协同单时的清单版本号快照;源清单变化不回写', + status VARCHAR(32) NOT NULL DEFAULT 'DOCUMENTS_PENDING' COMMENT '协同状态:DOCUMENTS_PENDING待补资料、REVIEWING审核中、SUPPLEMENT_REQUIRED需补件、READY_TO_SUBMIT待提交代理、RECEIPT_PENDING待回执、RECEIPT_AVAILABLE已有回执、CLEARANCE_COMPLETED已放行、CANCELLED已取消', + remark VARCHAR(500) NULL COMMENT '报关协同单内部业务备注', + created_by BIGINT UNSIGNED NOT NULL COMMENT '创建协同单的平台操作人ID', + created_by_name VARCHAR(64) NULL COMMENT '创建协同单时的平台操作人名称快照', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '协同单创建时间', + updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改协同单的平台操作人ID', + updated_by_name VARCHAR(64) NULL COMMENT '最后修改时的平台操作人名称快照', + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '协同单最后修改时间', + version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号;每次状态变更递增', PRIMARY KEY (id), UNIQUE KEY uk_customs_no (customs_no), UNIQUE KEY uk_customs_case_scope (tenant_id, order_id, inbound_id, container_booking_id), @@ -48,23 +48,23 @@ CREATE TABLE IF NOT EXISTS fl_customs_case ( ) ENGINE=InnoDB COMMENT='三期订单报关协同聚合'; CREATE TABLE IF NOT EXISTS fl_customs_document ( - id BIGINT UNSIGNED NOT NULL, - customs_case_id BIGINT UNSIGNED NOT NULL, - checklist_item_code VARCHAR(40) NOT NULL, - file_id BIGINT UNSIGNED NOT NULL, - document_version INT UNSIGNED NOT NULL, - replaces_document_id BIGINT UNSIGNED NULL, - file_name VARCHAR(255) NOT NULL, - file_hash CHAR(64) NOT NULL, - status VARCHAR(16) NOT NULL DEFAULT 'UPLOADED', - visibility VARCHAR(16) NOT NULL DEFAULT 'CUSTOMER', - uploaded_by BIGINT UNSIGNED NOT NULL, - uploaded_by_type VARCHAR(16) NOT NULL, - uploaded_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - reviewed_by BIGINT UNSIGNED NULL, - reviewed_by_name VARCHAR(64) NULL, - reviewed_at DATETIME(3) NULL, - review_reason VARCHAR(500) NULL, + id BIGINT UNSIGNED NOT NULL COMMENT '报关资料文件版本逻辑ID', + customs_case_id BIGINT UNSIGNED NOT NULL COMMENT '所属报关协同单ID', + checklist_item_code VARCHAR(40) NOT NULL COMMENT '对应冻结清单中的资料项目编码', + file_id BIGINT UNSIGNED NOT NULL COMMENT 'File服务文件逻辑ID;跨库不建立外键', + document_version INT UNSIGNED NOT NULL COMMENT '同一协同单和清单项目下的文件版本序号', + replaces_document_id BIGINT UNSIGNED NULL COMMENT '被本版本替换的上一资料版本ID;首个版本为空', + file_name VARCHAR(255) NOT NULL COMMENT '上传或引用时的文件名称快照', + file_hash CHAR(64) NOT NULL COMMENT '文件内容SHA-256摘要;用于版本审计和幂等校验', + status VARCHAR(16) NOT NULL DEFAULT 'UPLOADED' COMMENT '资料状态:UPLOADED待审核、APPROVED审核通过、RETURNED已退回、SUPERSEDED已被替换、FROZEN已随代理提交批次冻结', + visibility VARCHAR(16) NOT NULL DEFAULT 'CUSTOMER' COMMENT '资料可见范围:PLATFORM仅平台可见、CUSTOMER客户可见', + uploaded_by BIGINT UNSIGNED NOT NULL COMMENT '上传或引用资料的操作人ID', + uploaded_by_type VARCHAR(16) NOT NULL COMMENT '上传主体类型:PLATFORM平台操作人、MERCHANT客户操作人', + uploaded_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '资料上传或引用时间', + reviewed_by BIGINT UNSIGNED NULL COMMENT '审核资料的平台操作人ID;未审核时为空', + reviewed_by_name VARCHAR(64) NULL COMMENT '审核时的平台操作人名称快照', + reviewed_at DATETIME(3) NULL COMMENT '资料审核时间', + review_reason VARCHAR(500) NULL COMMENT '审核退回原因或审核备注', PRIMARY KEY (id), UNIQUE KEY uk_customs_document_version (customs_case_id, checklist_item_code, document_version), UNIQUE KEY uk_customs_document_file (customs_case_id, file_id), @@ -76,19 +76,19 @@ CREATE TABLE IF NOT EXISTS fl_customs_document ( ) ENGINE=InnoDB COMMENT='报关客户文件版本引用'; CREATE TABLE IF NOT EXISTS fl_customs_supplement_request ( - id BIGINT UNSIGNED NOT NULL, - customs_case_id BIGINT UNSIGNED NOT NULL, - source_document_id BIGINT UNSIGNED NOT NULL, - resolved_document_id BIGINT UNSIGNED NULL, - checklist_item_code VARCHAR(40) NOT NULL, - reason VARCHAR(500) NOT NULL, - due_at DATETIME(3) NULL, - status VARCHAR(16) NOT NULL DEFAULT 'OPEN', - requested_by BIGINT UNSIGNED NOT NULL, - requested_by_name VARCHAR(64) NULL, - requested_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - resolved_by BIGINT UNSIGNED NULL, - resolved_at DATETIME(3) NULL, + id BIGINT UNSIGNED NOT NULL COMMENT '报关补件请求逻辑ID', + customs_case_id BIGINT UNSIGNED NOT NULL COMMENT '所属报关协同单ID', + source_document_id BIGINT UNSIGNED NOT NULL COMMENT '触发本次补件的被退回资料版本ID', + resolved_document_id BIGINT UNSIGNED NULL COMMENT '解决本次补件的新资料版本ID;未解决时为空', + checklist_item_code VARCHAR(40) NOT NULL COMMENT '需要补充的报关资料项目编码', + reason VARCHAR(500) NOT NULL COMMENT '平台审核人员填写的补件原因', + due_at DATETIME(3) NULL COMMENT '期望完成补件时间;未设置时为空', + status VARCHAR(16) NOT NULL DEFAULT 'OPEN' COMMENT '补件状态:OPEN待补件、RESOLVED已由新资料解决、CANCELLED已取消', + requested_by BIGINT UNSIGNED NOT NULL COMMENT '发起补件的平台操作人ID', + requested_by_name VARCHAR(64) NULL COMMENT '发起补件时的平台操作人名称快照', + requested_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '补件请求发起时间', + resolved_by BIGINT UNSIGNED NULL COMMENT '提交解决资料的操作人ID;未解决时为空', + resolved_at DATETIME(3) NULL COMMENT '补件请求解决时间', PRIMARY KEY (id), KEY idx_customs_supplement_case (customs_case_id, status, due_at), CONSTRAINT fk_customs_supplement_case FOREIGN KEY (customs_case_id) REFERENCES fl_customs_case(id), @@ -96,18 +96,18 @@ CREATE TABLE IF NOT EXISTS fl_customs_supplement_request ( ) ENGINE=InnoDB COMMENT='报关补件请求'; CREATE TABLE IF NOT EXISTS fl_customs_submission_batch ( - id BIGINT UNSIGNED NOT NULL, - customs_case_id BIGINT UNSIGNED NOT NULL, - batch_no VARCHAR(40) NOT NULL, - batch_version INT UNSIGNED NOT NULL, - customs_agent_name VARCHAR(128) NOT NULL, - external_reference VARCHAR(128) NULL, - file_snapshot_json JSON NOT NULL, - status VARCHAR(16) NOT NULL DEFAULT 'SUBMITTED', - note VARCHAR(500) NULL, - submitted_by BIGINT UNSIGNED NOT NULL, - submitted_by_name VARCHAR(64) NULL, - submitted_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + id BIGINT UNSIGNED NOT NULL COMMENT '报关代理提交批次逻辑ID', + customs_case_id BIGINT UNSIGNED NOT NULL COMMENT '所属报关协同单ID', + batch_no VARCHAR(40) NOT NULL COMMENT '全局唯一代理提交批次号', + batch_version INT UNSIGNED NOT NULL COMMENT '同一报关协同单内的提交批次版本序号', + customs_agent_name VARCHAR(128) NOT NULL COMMENT '本批次接收报关资料的报关代理名称快照', + external_reference VARCHAR(128) NULL COMMENT '报关代理或外部系统返回的业务参考号', + file_snapshot_json JSON NOT NULL COMMENT '提交时冻结的已审核资料数组;元素包含documentId、fileId、itemCode、documentVersion和fileHash', + status VARCHAR(16) NOT NULL DEFAULT 'SUBMITTED' COMMENT '代理提交状态:SUBMITTED已提交、ACKNOWLEDGED代理已确认、REJECTED代理已拒绝、COMPLETED代理处理完成', + note VARCHAR(500) NULL COMMENT '提交报关代理时的平台备注', + submitted_by BIGINT UNSIGNED NOT NULL COMMENT '提交报关代理的平台操作人ID', + submitted_by_name VARCHAR(64) NULL COMMENT '提交时的平台操作人名称快照', + submitted_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '提交报关代理时间', PRIMARY KEY (id), UNIQUE KEY uk_customs_batch_no (batch_no), UNIQUE KEY uk_customs_batch_version (customs_case_id, batch_version), @@ -116,20 +116,20 @@ CREATE TABLE IF NOT EXISTS fl_customs_submission_batch ( ) ENGINE=InnoDB COMMENT='报关代理提交批次及冻结文件版本'; CREATE TABLE IF NOT EXISTS fl_customs_receipt ( - id BIGINT UNSIGNED NOT NULL, - customs_case_id BIGINT UNSIGNED NOT NULL, - submission_batch_id BIGINT UNSIGNED NOT NULL, - receipt_type VARCHAR(16) NOT NULL, - file_id BIGINT UNSIGNED NOT NULL, - receipt_version INT UNSIGNED NOT NULL, - replaces_receipt_id BIGINT UNSIGNED NULL, - file_name VARCHAR(255) NOT NULL, - file_hash CHAR(64) NOT NULL, - visibility VARCHAR(16) NOT NULL DEFAULT 'PLATFORM', - note VARCHAR(500) NULL, - uploaded_by BIGINT UNSIGNED NOT NULL, - uploaded_by_name VARCHAR(64) NULL, - uploaded_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + id BIGINT UNSIGNED NOT NULL COMMENT '报关回执文件版本逻辑ID', + customs_case_id BIGINT UNSIGNED NOT NULL COMMENT '所属报关协同单ID', + submission_batch_id BIGINT UNSIGNED NOT NULL COMMENT '产生本回执的报关代理提交批次ID', + receipt_type VARCHAR(16) NOT NULL COMMENT '回执类型:RECEIVED已收件、DECLARED已申报、REJECTED已拒绝、INSPECTION查验通知、RELEASED放行回执、OTHER其他回执', + file_id BIGINT UNSIGNED NOT NULL COMMENT 'File服务回执文件逻辑ID;跨库不建立外键', + receipt_version INT UNSIGNED NOT NULL COMMENT '同一提交批次和回执类型下的版本序号', + replaces_receipt_id BIGINT UNSIGNED NULL COMMENT '被本版本替换的上一回执版本ID;首个版本为空', + file_name VARCHAR(255) NOT NULL COMMENT '上传或引用时的回执文件名称快照', + file_hash CHAR(64) NOT NULL COMMENT '回执文件内容SHA-256摘要;用于版本审计和幂等校验', + visibility VARCHAR(16) NOT NULL DEFAULT 'PLATFORM' COMMENT '回执可见范围:PLATFORM仅平台可见、CUSTOMER客户可查询下载', + note VARCHAR(500) NULL COMMENT '回执业务备注', + uploaded_by BIGINT UNSIGNED NOT NULL COMMENT '上传或引用回执的平台操作人ID', + uploaded_by_name VARCHAR(64) NULL COMMENT '上传回执时的平台操作人名称快照', + uploaded_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '回执上传或引用时间', PRIMARY KEY (id), UNIQUE KEY uk_customs_receipt_version (submission_batch_id, receipt_type, receipt_version), UNIQUE KEY uk_customs_receipt_file (submission_batch_id, file_id), @@ -141,18 +141,18 @@ CREATE TABLE IF NOT EXISTS fl_customs_receipt ( ) ENGINE=InnoDB COMMENT='报关代理回执版本与客户可见范围'; CREATE TABLE IF NOT EXISTS fl_customs_history ( - id BIGINT UNSIGNED NOT NULL, - customs_case_id BIGINT UNSIGNED NOT NULL, - action VARCHAR(40) NOT NULL, - from_status VARCHAR(32) NULL, - to_status VARCHAR(32) NULL, - resource_type VARCHAR(32) NOT NULL, - resource_id BIGINT UNSIGNED NOT NULL, - note VARCHAR(500) NULL, - operator_id BIGINT UNSIGNED NOT NULL, - operator_name VARCHAR(64) NULL, - operator_type VARCHAR(16) NOT NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + id BIGINT UNSIGNED NOT NULL COMMENT '报关协同历史逻辑ID', + customs_case_id BIGINT UNSIGNED NOT NULL COMMENT '所属报关协同单ID', + action VARCHAR(40) NOT NULL COMMENT '业务动作编码,如CREATE、UPLOAD_DOCUMENT、REPLACE_DOCUMENT、REVIEW_DOCUMENT_APPROVE、REVIEW_DOCUMENT_RETURN、SUBMIT_AGENT_BATCH或UPLOAD_RECEIPT', + from_status VARCHAR(32) NULL COMMENT '动作发生前的协同状态;创建动作时为空', + to_status VARCHAR(32) NULL COMMENT '动作完成后的协同状态', + resource_type VARCHAR(32) NOT NULL COMMENT '动作关联资源类型:CASE协同单、DOCUMENT资料、SUBMISSION_BATCH提交批次或RECEIPT回执', + resource_id BIGINT UNSIGNED NOT NULL COMMENT '动作关联资源逻辑ID', + note VARCHAR(500) NULL COMMENT '动作原因、资料项目编码或业务备注快照', + operator_id BIGINT UNSIGNED NOT NULL COMMENT '执行业务动作的操作人ID', + operator_name VARCHAR(64) NULL COMMENT '执行动作时的操作人名称快照', + operator_type VARCHAR(16) NOT NULL COMMENT '操作主体类型:PLATFORM平台操作人、MERCHANT客户操作人', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '历史记录创建时间', PRIMARY KEY (id), KEY idx_customs_history_case (customs_case_id, created_at, id), CONSTRAINT fk_customs_history_case FOREIGN KEY (customs_case_id) REFERENCES fl_customs_case(id), @@ -160,16 +160,16 @@ CREATE TABLE IF NOT EXISTS fl_customs_history ( ) ENGINE=InnoDB COMMENT='报关协同稳定历史'; CREATE TABLE IF NOT EXISTS fl_customs_operation ( - id BIGINT UNSIGNED NOT NULL, - operator_id BIGINT UNSIGNED NOT NULL, - action VARCHAR(64) NOT NULL, - idempotency_key VARCHAR(128) NOT NULL, - request_hash CHAR(64) NOT NULL, - status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING', - result_type VARCHAR(24) NULL, - result_id BIGINT UNSIGNED NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - completed_at DATETIME(3) NULL, + id BIGINT UNSIGNED NOT NULL COMMENT '报关写命令幂等记录逻辑ID', + operator_id BIGINT UNSIGNED NOT NULL COMMENT '发起写命令的操作人ID;与动作和幂等键共同限定重放范围', + action VARCHAR(64) NOT NULL COMMENT '幂等业务动作编码,包含平台或客户动作及资料、审核、提交、回执类型', + idempotency_key VARCHAR(128) NOT NULL COMMENT '客户端提供的幂等键', + request_hash CHAR(64) NOT NULL COMMENT '规范化完整请求内容的SHA-256摘要;同键不同请求时拒绝', + status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING' COMMENT '幂等处理状态:PROCESSING处理中、SUCCESS处理成功', + result_type VARCHAR(24) NULL COMMENT '成功结果资源类型:CHECKLIST、CASE、DOCUMENT、BATCH或RECEIPT', + result_id BIGINT UNSIGNED NULL COMMENT '成功结果资源逻辑ID;处理中为空', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '幂等记录创建时间', + completed_at DATETIME(3) NULL COMMENT '写命令成功完成时间;处理中为空', PRIMARY KEY (id), UNIQUE KEY uk_customs_operation (operator_id, action, idempotency_key), CONSTRAINT chk_customs_operation_status CHECK (status IN ('PROCESSING','SUCCESS')) diff --git a/firstleg/src/main/resources/db/phase3-inbound-parent-child-schema.sql b/firstleg/src/main/resources/db/phase3-inbound-parent-child-schema.sql index 37bfa1803..8447dc94b 100644 --- a/firstleg/src/main/resources/db/phase3-inbound-parent-child-schema.sql +++ b/firstleg/src/main/resources/db/phase3-inbound-parent-child-schema.sql @@ -169,19 +169,19 @@ DROP PROCEDURE IF EXISTS phase3_inbound_add_index; CREATE TABLE IF NOT EXISTS fl_inbound_split_batch ( id BIGINT UNSIGNED NOT NULL COMMENT '拆分批次ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', parent_inbound_id BIGINT UNSIGNED NOT NULL COMMENT '主入仓单ID', operation_id VARCHAR(64) NOT NULL COMMENT '业务操作幂等号', idempotency_key VARCHAR(128) NOT NULL COMMENT '请求幂等键', expected_parent_version INT NOT NULL COMMENT '操作时主单乐观锁版本', package_count INT UNSIGNED NOT NULL COMMENT '本批次拆分箱数', - status VARCHAR(16) NOT NULL DEFAULT 'PREPARED' COMMENT 'PREPARED、APPLIED、CANCELLED', + status VARCHAR(16) NOT NULL DEFAULT 'PREPARED' COMMENT '拆分批次状态:PREPARED已准备、APPLIED已生效、CANCELLED已取消', remark VARCHAR(500) NULL COMMENT '拆分原因或备注', created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID', created_by_name VARCHAR(64) NULL COMMENT '创建人名称快照', - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '拆分批次创建时间', applied_at DATETIME(3) NULL COMMENT '批次生效时间', - version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本', + version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号,每次更新递增', PRIMARY KEY (id), UNIQUE KEY uk_inbound_split_operation (tenant_id, operation_id), UNIQUE KEY uk_inbound_split_idempotency (tenant_id, parent_inbound_id, idempotency_key), @@ -193,7 +193,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_split_batch ( CREATE TABLE IF NOT EXISTS fl_inbound_split_item ( id BIGINT UNSIGNED NOT NULL COMMENT '拆分明细ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', split_batch_id BIGINT UNSIGNED NOT NULL COMMENT '拆分批次ID', parent_inbound_id BIGINT UNSIGNED NOT NULL COMMENT '主入仓单ID', child_inbound_id BIGINT UNSIGNED NOT NULL COMMENT '子入仓单ID', @@ -202,7 +202,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_split_item ( source_measurement_id BIGINT UNSIGNED NOT NULL COMMENT '来源测量批次ID', source_measurement_item_id BIGINT UNSIGNED NOT NULL COMMENT '来源测量箱规明细ID', package_count INT UNSIGNED NOT NULL COMMENT '分配给子单的箱数', - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '拆分数量台账创建时间', PRIMARY KEY (id), UNIQUE KEY uk_inbound_split_batch_line (tenant_id, split_batch_id, line_no), UNIQUE KEY uk_inbound_split_batch_source_child (tenant_id, split_batch_id, source_measurement_item_id, child_inbound_id), @@ -217,16 +217,16 @@ CREATE TABLE IF NOT EXISTS fl_inbound_split_item ( CREATE TABLE IF NOT EXISTS fl_inbound_split_history ( id BIGINT UNSIGNED NOT NULL COMMENT '拆分历史ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', split_batch_id BIGINT UNSIGNED NOT NULL COMMENT '拆分批次ID', operation_id VARCHAR(64) NOT NULL COMMENT '历史操作幂等号', - action_type VARCHAR(24) NOT NULL COMMENT 'CREATE、APPLY、CANCEL', + action_type VARCHAR(24) NOT NULL COMMENT '拆分动作:CREATE创建批次、APPLY生效、CANCEL取消', before_snapshot JSON NULL COMMENT '操作前快照', after_snapshot JSON NOT NULL COMMENT '操作后快照', operator_id BIGINT UNSIGNED NOT NULL COMMENT '操作人ID', operator_name VARCHAR(64) NULL COMMENT '操作人名称快照', operation_reason VARCHAR(500) NULL COMMENT '操作原因', - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '不可变拆分历史创建时间', PRIMARY KEY (id), UNIQUE KEY uk_inbound_split_history_operation (tenant_id, operation_id), KEY idx_inbound_split_history (tenant_id, split_batch_id, created_at, id), diff --git a/firstleg/src/main/resources/db/phase3-loading-delivery-schema.sql b/firstleg/src/main/resources/db/phase3-loading-delivery-schema.sql index b812b323b..d67dbabb7 100644 --- a/firstleg/src/main/resources/db/phase3-loading-delivery-schema.sql +++ b/firstleg/src/main/resources/db/phase3-loading-delivery-schema.sql @@ -3,26 +3,26 @@ USE baiyue_firstleg; /* 装柜证据、PDA 拣货复核和派送完成。五类证据按有效记录计数,同类型允许多图追加;作废记录保留审计。 */ CREATE TABLE IF NOT EXISTS fl_loading_evidence ( - id BIGINT UNSIGNED NOT NULL, - tenant_id BIGINT UNSIGNED NOT NULL, - inbound_id BIGINT UNSIGNED NOT NULL, + id BIGINT UNSIGNED NOT NULL COMMENT '装柜证据记录ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + inbound_id BIGINT UNSIGNED NOT NULL COMMENT '关联入库单逻辑ID', evidence_type VARCHAR(32) NOT NULL COMMENT 'PHOTO_STEP_1空仓/PHOTO_STEP_2半仓/PHOTO_STEP_3满仓/PHOTO_STEP_4锁仓/SEAL带锁码照', file_id BIGINT UNSIGNED NOT NULL COMMENT 'File 服务逻辑文件ID', - file_name VARCHAR(255) NULL, - seal_no VARCHAR(64) NULL, - remark VARCHAR(1000) NULL, - status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT 'ACTIVE/VOID,作废不硬删除', - void_reason VARCHAR(500) NULL, - voided_by BIGINT UNSIGNED NULL, - voided_by_name VARCHAR(64) NULL, - voided_at DATETIME(3) NULL, - version INT NOT NULL DEFAULT 0, - created_by BIGINT UNSIGNED NOT NULL, - created_by_name VARCHAR(64) NULL, - updated_by BIGINT UNSIGNED NOT NULL, - updated_by_name VARCHAR(64) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + file_name VARCHAR(255) NULL COMMENT '上传时文件名称快照', + seal_no VARCHAR(64) NULL COMMENT '锁码文本;带锁码照必填,客户不可见', + remark VARCHAR(1000) NULL COMMENT '证据补充说明', + status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '证据状态:ACTIVE有效、VOID已作废;错误照片保留记录不硬删除', + void_reason VARCHAR(500) NULL COMMENT '照片作废原因', + voided_by BIGINT UNSIGNED NULL COMMENT '执行作废的平台操作人ID', + voided_by_name VARCHAR(64) NULL COMMENT '执行作废的平台操作人姓名快照', + voided_at DATETIME(3) NULL COMMENT '照片作废时间', + version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号,每次更新递增', + created_by BIGINT UNSIGNED NOT NULL COMMENT '上传证据的操作人ID', + created_by_name VARCHAR(64) NULL COMMENT '上传证据的操作人姓名快照', + updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改操作人ID', + updated_by_name VARCHAR(64) NULL COMMENT '最后修改操作人姓名快照', + 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 (id), KEY idx_loading_evidence_type (tenant_id, inbound_id, evidence_type, status), KEY idx_loading_evidence_seal (tenant_id, seal_no, status), @@ -46,11 +46,11 @@ BEGIN END IF; END$$ DELIMITER ; -CALL phase3_loading_add_column('status', 'VARCHAR(16) NOT NULL DEFAULT ''ACTIVE'' COMMENT ''ACTIVE/VOID,作废不硬删除'''); -CALL phase3_loading_add_column('void_reason', 'VARCHAR(500) NULL'); -CALL phase3_loading_add_column('voided_by', 'BIGINT UNSIGNED NULL'); -CALL phase3_loading_add_column('voided_by_name', 'VARCHAR(64) NULL'); -CALL phase3_loading_add_column('voided_at', 'DATETIME(3) NULL'); +CALL phase3_loading_add_column('status', 'VARCHAR(16) NOT NULL DEFAULT ''ACTIVE'' COMMENT ''证据状态:ACTIVE有效、VOID已作废;错误照片保留记录不硬删除'''); +CALL phase3_loading_add_column('void_reason', 'VARCHAR(500) NULL COMMENT ''照片作废原因'''); +CALL phase3_loading_add_column('voided_by', 'BIGINT UNSIGNED NULL COMMENT ''执行作废的平台操作人ID'''); +CALL phase3_loading_add_column('voided_by_name', 'VARCHAR(64) NULL COMMENT ''执行作废的平台操作人姓名快照'''); +CALL phase3_loading_add_column('voided_at', 'DATETIME(3) NULL COMMENT ''照片作废时间'''); DROP PROCEDURE IF EXISTS phase3_loading_add_column; SET @phase3_loading_has_legacy_type_index = ( @@ -94,17 +94,17 @@ EXECUTE phase3_loading_seal_stmt; DEALLOCATE PREPARE phase3_loading_seal_stmt; CREATE TABLE IF NOT EXISTS fl_loading_operation ( - id BIGINT UNSIGNED NOT NULL, - tenant_id BIGINT UNSIGNED NOT NULL, - inbound_id BIGINT UNSIGNED NOT NULL, - operator_id BIGINT UNSIGNED NOT NULL, - operation VARCHAR(48) NOT NULL, - idempotency_key VARCHAR(128) NOT NULL, - request_hash CHAR(64) NOT NULL, - status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING', - result_json JSON NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - completed_at DATETIME(3) NULL, + id BIGINT UNSIGNED NOT NULL COMMENT '装柜写操作记录ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + inbound_id BIGINT UNSIGNED NOT NULL COMMENT '操作目标入库单逻辑ID', + operator_id BIGINT UNSIGNED NOT NULL COMMENT '执行写操作的平台用户ID', + operation VARCHAR(48) NOT NULL COMMENT '装柜或证据写操作类型', + idempotency_key VARCHAR(128) NOT NULL COMMENT '客户端幂等键,同一操作重试复用', + request_hash CHAR(64) NOT NULL COMMENT '规范化请求内容SHA-256摘要', + status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING' COMMENT '幂等处理状态:PROCESSING处理中、SUCCESS成功、FAILED失败', + result_json JSON NULL COMMENT '成功结果或失败信息JSON快照,用于幂等重放', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '操作声明创建时间', + completed_at DATETIME(3) NULL COMMENT '操作完成时间', PRIMARY KEY (id), UNIQUE KEY uk_loading_operation (tenant_id, inbound_id, operator_id, operation, idempotency_key), KEY idx_loading_operation_time (tenant_id, inbound_id, created_at), @@ -112,18 +112,18 @@ CREATE TABLE IF NOT EXISTS fl_loading_operation ( ) ENGINE=InnoDB COMMENT='装柜与派送写操作幂等记录'; CREATE TABLE IF NOT EXISTS fl_inbound_loading_pick_task ( - id BIGINT UNSIGNED NOT NULL, - tenant_id BIGINT UNSIGNED NOT NULL, - inbound_id BIGINT UNSIGNED NOT NULL, - status VARCHAR(24) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/IN_PROGRESS/COMPLETED', - version INT NOT NULL DEFAULT 0, - expected_count INT UNSIGNED NULL, - picked_count INT UNSIGNED NULL, - remark VARCHAR(1000) NULL, - updated_by BIGINT UNSIGNED NOT NULL, - updated_by_name VARCHAR(64) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + id BIGINT UNSIGNED NOT NULL COMMENT '入库单级PDA拣货任务ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + inbound_id BIGINT UNSIGNED NOT NULL COMMENT '关联入库单逻辑ID', + status VARCHAR(24) NOT NULL DEFAULT 'PENDING' COMMENT '任务状态:PENDING待拣货、IN_PROGRESS拣货中、COMPLETED已完成', + version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号,每次更新递增', + expected_count INT UNSIGNED NULL COMMENT '计划应拣箱数', + picked_count INT UNSIGNED NULL COMMENT '实际已拣箱数', + remark VARCHAR(1000) NULL COMMENT '拣货结果或异常补充说明', + updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后操作人ID', + updated_by_name VARCHAR(64) NULL COMMENT '最后操作人姓名快照', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 'PDA拣货任务创建时间', + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT 'PDA拣货任务最后更新时间', PRIMARY KEY (id), UNIQUE KEY uk_loading_pick_tenant_inbound (tenant_id, inbound_id), KEY idx_loading_pick_status (tenant_id, status, updated_at), diff --git a/firstleg/src/main/resources/db/phase3-loading-model-bridge-schema.sql b/firstleg/src/main/resources/db/phase3-loading-model-bridge-schema.sql index ac6af695f..3f3a9785a 100644 --- a/firstleg/src/main/resources/db/phase3-loading-model-bridge-schema.sql +++ b/firstleg/src/main/resources/db/phase3-loading-model-bridge-schema.sql @@ -10,9 +10,9 @@ CREATE TABLE IF NOT EXISTS fl_preload_loading_plan_link ( loading_plan_id BIGINT UNSIGNED NOT NULL COMMENT '柜次级装柜计划ID', idempotency_key VARCHAR(128) NOT NULL COMMENT '建立关联幂等键', request_hash CHAR(64) NOT NULL COMMENT '建立关联请求SHA-256', - created_by BIGINT UNSIGNED NOT NULL, - created_by_name VARCHAR(64) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + created_by BIGINT UNSIGNED NOT NULL COMMENT '建立预装与装柜计划关联的操作人ID', + created_by_name VARCHAR(64) NULL COMMENT '建立关联的操作人姓名快照', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '关联记录创建时间', PRIMARY KEY (id), UNIQUE KEY uk_preload_loading_pair (tenant_id, inbound_id, loading_plan_id), UNIQUE KEY uk_preload_loading_idempotency (created_by, idempotency_key), diff --git a/firstleg/src/main/resources/db/phase3-ocean-booking-schema.sql b/firstleg/src/main/resources/db/phase3-ocean-booking-schema.sql index 9f9aed850..9e549b6c5 100644 --- a/firstleg/src/main/resources/db/phase3-ocean-booking-schema.sql +++ b/firstleg/src/main/resources/db/phase3-ocean-booking-schema.sql @@ -2,42 +2,42 @@ SET NAMES utf8mb4; USE baiyue_firstleg; CREATE TABLE IF NOT EXISTS fl_ocean_booking ( - id BIGINT UNSIGNED NOT NULL, - booking_no VARCHAR(40) NOT NULL, - status VARCHAR(24) NOT NULL DEFAULT 'DRAFT', - customer_id BIGINT UNSIGNED NOT NULL COMMENT '当前产品客户ID', - customer_name VARCHAR(128) NOT NULL, - bill_of_lading_no VARCHAR(128) NULL, - shipping_company_id BIGINT UNSIGNED NULL, - shipping_company_name VARCHAR(128) NULL, - booking_agent_id BIGINT UNSIGNED NULL, - booking_agent_name VARCHAR(128) NULL, - vessel_name VARCHAR(128) NULL, - voyage_no VARCHAR(64) NULL, - departure_port_code VARCHAR(32) NULL, - departure_port_name VARCHAR(128) NULL, - destination_port_code VARCHAR(32) NULL, - destination_port_name VARCHAR(128) NULL, - place_of_delivery VARCHAR(128) NULL, - service_term VARCHAR(32) NULL, - freight_term VARCHAR(32) NULL, - estimated_sailing_at DATETIME(3) NULL, - estimated_arrival_at DATETIME(3) NULL, - pending_vessel_name VARCHAR(128) NULL, - pending_voyage_no VARCHAR(64) NULL, - pending_estimated_sailing_at DATETIME(3) NULL, - pending_estimated_arrival_at DATETIME(3) NULL, - change_reason VARCHAR(1000) NULL, - failure_reason VARCHAR(1000) NULL, - cancellation_reason VARCHAR(1000) NULL, - remark VARCHAR(1000) NULL, - created_by BIGINT UNSIGNED NOT NULL, - created_by_name VARCHAR(64) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_by BIGINT UNSIGNED NOT NULL, - updated_by_name VARCHAR(64) NULL, - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - version INT UNSIGNED NOT NULL DEFAULT 0, + id BIGINT UNSIGNED NOT NULL COMMENT '海运订舱ID', + booking_no VARCHAR(40) NOT NULL COMMENT '平台海运订舱编号', + status VARCHAR(24) NOT NULL DEFAULT 'DRAFT' COMMENT '订舱状态:DRAFT草稿、SUBMITTED已提交、CONFIRMED已确认、RELEASED已放舱、FAILED失败、CHANGE_PENDING船期待变更、CANCELLED已取消', + customer_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + customer_name VARCHAR(128) NOT NULL COMMENT '客户名称快照', + bill_of_lading_no VARCHAR(128) NULL COMMENT '提单号', + shipping_company_id BIGINT UNSIGNED NULL COMMENT '船公司逻辑ID快照', + shipping_company_name VARCHAR(128) NULL COMMENT '船公司名称快照', + booking_agent_id BIGINT UNSIGNED NULL COMMENT '订舱代理逻辑ID快照', + booking_agent_name VARCHAR(128) NULL COMMENT '订舱代理名称快照', + vessel_name VARCHAR(128) NULL COMMENT '当前确认船名快照', + voyage_no VARCHAR(64) NULL COMMENT '当前确认航次号快照', + departure_port_code VARCHAR(32) NULL COMMENT '起运港编码快照', + departure_port_name VARCHAR(128) NULL COMMENT '起运港名称快照', + destination_port_code VARCHAR(32) NULL COMMENT '目的港编码快照', + destination_port_name VARCHAR(128) NULL COMMENT '目的港名称快照', + place_of_delivery VARCHAR(128) NULL COMMENT '最终交货地快照', + service_term VARCHAR(32) NULL COMMENT '运输服务条款快照', + freight_term VARCHAR(32) NULL COMMENT '海运费支付条款快照', + estimated_sailing_at DATETIME(3) NULL COMMENT '当前确认预计开船时间', + estimated_arrival_at DATETIME(3) NULL COMMENT '当前确认预计到港时间', + pending_vessel_name VARCHAR(128) NULL COMMENT '待确认变更船名', + pending_voyage_no VARCHAR(64) NULL COMMENT '待确认变更航次号', + pending_estimated_sailing_at DATETIME(3) NULL COMMENT '待确认变更预计开船时间', + pending_estimated_arrival_at DATETIME(3) NULL COMMENT '待确认变更预计到港时间', + change_reason VARCHAR(1000) NULL COMMENT '船期变更原因', + failure_reason VARCHAR(1000) NULL COMMENT '订舱失败原因', + cancellation_reason VARCHAR(1000) NULL COMMENT '取消订舱原因', + remark VARCHAR(1000) NULL COMMENT '平台内部备注', + created_by BIGINT UNSIGNED NOT NULL COMMENT '创建操作人ID', + created_by_name VARCHAR(64) NULL COMMENT '创建操作人姓名快照', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '订舱记录创建时间', + updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改操作人ID', + updated_by_name VARCHAR(64) NULL COMMENT '最后修改操作人姓名快照', + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '订舱记录最后更新时间', + version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', PRIMARY KEY (id), UNIQUE KEY uk_ocean_booking_no (booking_no), KEY idx_ocean_booking_customer (customer_id,status,updated_at,id), @@ -53,13 +53,13 @@ CREATE TABLE IF NOT EXISTS fl_ocean_booking ( ) ENGINE=InnoDB COMMENT='独立海运订舱聚合;柜次仍由fl_container_booking承载'; CREATE TABLE IF NOT EXISTS fl_ocean_booking_party ( - id BIGINT UNSIGNED NOT NULL, - ocean_booking_id BIGINT UNSIGNED NOT NULL, - party_type VARCHAR(24) NOT NULL COMMENT 'SHIPPER/CONSIGNEE/NOTIFY_PARTY', - party_name VARCHAR(255) NOT NULL, - address VARCHAR(1000) NULL, - contact_name VARCHAR(128) NULL, - contact_phone VARCHAR(64) NULL, + id BIGINT UNSIGNED NOT NULL COMMENT '订舱提单关系方ID', + ocean_booking_id BIGINT UNSIGNED NOT NULL COMMENT '所属海运订舱ID', + party_type VARCHAR(24) NOT NULL COMMENT '关系方类型:SHIPPER发货人、CONSIGNEE收货人、NOTIFY_PARTY通知人', + party_name VARCHAR(255) NOT NULL COMMENT '关系方名称快照', + address VARCHAR(1000) NULL COMMENT '关系方地址快照', + contact_name VARCHAR(128) NULL COMMENT '关系方联系人快照', + contact_phone VARCHAR(64) NULL COMMENT '关系方联系电话快照', PRIMARY KEY (id), UNIQUE KEY uk_ocean_booking_party (ocean_booking_id,party_type), CONSTRAINT fk_ocean_booking_party FOREIGN KEY (ocean_booking_id) REFERENCES fl_ocean_booking(id), @@ -67,12 +67,12 @@ CREATE TABLE IF NOT EXISTS fl_ocean_booking_party ( ) ENGINE=InnoDB COMMENT='订舱提单关系方快照'; CREATE TABLE IF NOT EXISTS fl_ocean_booking_container_requirement ( - id BIGINT UNSIGNED NOT NULL, - ocean_booking_id BIGINT UNSIGNED NOT NULL, - container_type VARCHAR(32) NOT NULL, - container_kind VARCHAR(32) NOT NULL, - quantity INT UNSIGNED NOT NULL, - sort_no INT UNSIGNED NOT NULL DEFAULT 0, + id BIGINT UNSIGNED NOT NULL COMMENT '订舱箱型需求ID', + ocean_booking_id BIGINT UNSIGNED NOT NULL COMMENT '所属海运订舱ID', + container_type VARCHAR(32) NOT NULL COMMENT '所需集装箱尺寸类型编码', + container_kind VARCHAR(32) NOT NULL COMMENT '所需集装箱箱种编码', + quantity INT UNSIGNED NOT NULL COMMENT '该箱型箱种所需柜数', + sort_no INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '订舱需求展示顺序', PRIMARY KEY (id), KEY idx_ocean_requirement_booking (ocean_booking_id,sort_no,id), CONSTRAINT fk_ocean_requirement_booking FOREIGN KEY (ocean_booking_id) REFERENCES fl_ocean_booking(id), @@ -80,13 +80,13 @@ CREATE TABLE IF NOT EXISTS fl_ocean_booking_container_requirement ( ) ENGINE=InnoDB COMMENT='订舱箱型箱种及数量需求'; CREATE TABLE IF NOT EXISTS fl_ocean_booking_container_allocation ( - id BIGINT UNSIGNED NOT NULL, - ocean_booking_id BIGINT UNSIGNED NOT NULL, - requirement_id BIGINT UNSIGNED NOT NULL, + id BIGINT UNSIGNED NOT NULL COMMENT '订舱柜次分配关系ID', + ocean_booking_id BIGINT UNSIGNED NOT NULL COMMENT '所属海运订舱ID', + requirement_id BIGINT UNSIGNED NOT NULL COMMENT '对应订舱箱型需求ID', container_booking_id BIGINT UNSIGNED NOT NULL COMMENT '明确引用现有柜次ID', - allocated_by BIGINT UNSIGNED NOT NULL, - allocated_by_name VARCHAR(64) NULL, - allocated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + allocated_by BIGINT UNSIGNED NOT NULL COMMENT '分配操作人ID', + allocated_by_name VARCHAR(64) NULL COMMENT '分配操作人姓名快照', + allocated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '柜次分配时间', PRIMARY KEY (id), UNIQUE KEY uk_ocean_allocation_container (container_booking_id), KEY idx_ocean_allocation_booking (ocean_booking_id,requirement_id,allocated_at), @@ -97,33 +97,33 @@ CREATE TABLE IF NOT EXISTS fl_ocean_booking_container_allocation ( ) ENGINE=InnoDB COMMENT='订舱需求与现有柜次的唯一分配关系'; CREATE TABLE IF NOT EXISTS fl_ocean_booking_status_history ( - id BIGINT UNSIGNED NOT NULL, - ocean_booking_id BIGINT UNSIGNED NOT NULL, - from_status VARCHAR(24) NULL, - to_status VARCHAR(24) NOT NULL, - action VARCHAR(40) NOT NULL, - reason VARCHAR(1000) NULL, - before_snapshot JSON NULL, - after_snapshot JSON NOT NULL, - operator_id BIGINT UNSIGNED NOT NULL, - operator_name VARCHAR(64) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + id BIGINT UNSIGNED NOT NULL COMMENT '订舱状态历史ID', + ocean_booking_id BIGINT UNSIGNED NOT NULL COMMENT '所属海运订舱ID', + from_status VARCHAR(24) NULL COMMENT '变更前订舱状态;创建时为空', + to_status VARCHAR(24) NOT NULL COMMENT '变更后订舱状态', + action VARCHAR(40) NOT NULL COMMENT '触发状态变更的业务动作', + reason VARCHAR(1000) NULL COMMENT '状态变更原因', + before_snapshot JSON NULL COMMENT '状态变更前订舱数据快照;创建时为空', + after_snapshot JSON NOT NULL COMMENT '状态变更后订舱数据快照', + operator_id BIGINT UNSIGNED NOT NULL COMMENT '操作人ID', + operator_name VARCHAR(64) NULL COMMENT '操作人姓名快照', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '状态历史创建时间', PRIMARY KEY (id), KEY idx_ocean_history_booking (ocean_booking_id,created_at,id), CONSTRAINT fk_ocean_history_booking FOREIGN KEY (ocean_booking_id) REFERENCES fl_ocean_booking(id) ) ENGINE=InnoDB COMMENT='海运订舱不可变状态历史'; CREATE TABLE IF NOT EXISTS fl_ocean_booking_operation ( - id BIGINT UNSIGNED NOT NULL, - ocean_booking_id BIGINT UNSIGNED NULL COMMENT '创建请求完成前允许为空', - operator_id BIGINT UNSIGNED NOT NULL, - operation VARCHAR(40) NOT NULL, - idempotency_key VARCHAR(128) NOT NULL, - request_hash CHAR(64) NOT NULL, - status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING', - result_json JSON NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - completed_at DATETIME(3) NULL, + id BIGINT UNSIGNED NOT NULL COMMENT '订舱写操作幂等记录ID', + ocean_booking_id BIGINT UNSIGNED NULL COMMENT '关联海运订舱ID;创建请求完成前允许为空', + operator_id BIGINT UNSIGNED NOT NULL COMMENT '发起写操作的平台操作人ID', + operation VARCHAR(40) NOT NULL COMMENT '幂等写操作类型', + idempotency_key VARCHAR(128) NOT NULL COMMENT '客户端写操作幂等键', + request_hash CHAR(64) NOT NULL COMMENT '规范化请求内容SHA-256摘要;用于识别同键不同请求', + status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING' COMMENT '处理状态:PROCESSING处理中、SUCCESS成功', + result_json JSON NULL COMMENT '成功结果JSON快照;供幂等重放返回', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '幂等记录创建时间', + completed_at DATETIME(3) NULL COMMENT '写操作成功完成时间', PRIMARY KEY (id), UNIQUE KEY uk_ocean_operation (operator_id,operation,idempotency_key), KEY idx_ocean_operation_booking (ocean_booking_id,created_at), diff --git a/firstleg/src/main/resources/db/phase3-outbound-dispatch-schema.sql b/firstleg/src/main/resources/db/phase3-outbound-dispatch-schema.sql index a3a46c3f8..5761ba7b3 100644 --- a/firstleg/src/main/resources/db/phase3-outbound-dispatch-schema.sql +++ b/firstleg/src/main/resources/db/phase3-outbound-dispatch-schema.sql @@ -2,22 +2,22 @@ USE baiyue_firstleg; CREATE TABLE IF NOT EXISTS fl_outbound_dispatch ( - id BIGINT UNSIGNED NOT NULL, - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离ID', - order_id BIGINT UNSIGNED NOT NULL COMMENT '订单引用,不复制订单主数据', - inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单主对象', - container_booking_id BIGINT UNSIGNED NOT NULL COMMENT '柜次引用', - status VARCHAR(32) NOT NULL DEFAULT 'READY_FOR_DISPATCH', - version INT UNSIGNED NOT NULL DEFAULT 0, - confirmed_by BIGINT UNSIGNED NULL, - confirmed_by_name VARCHAR(64) NULL, - confirmed_at DATETIME(3) NULL, - reversed_by BIGINT UNSIGNED NULL, - reversed_by_name VARCHAR(64) NULL, - reversed_at DATETIME(3) NULL, - remark VARCHAR(500) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + id BIGINT UNSIGNED NOT NULL COMMENT '国内出仓调度记录ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + order_id BIGINT UNSIGNED NOT NULL COMMENT 'Order服务订单逻辑ID,不复制订单主数据', + inbound_id BIGINT UNSIGNED NOT NULL COMMENT '关联入库单逻辑ID', + container_booking_id BIGINT UNSIGNED NOT NULL COMMENT '关联柜次逻辑ID', + status VARCHAR(32) NOT NULL DEFAULT 'READY_FOR_DISPATCH' COMMENT '出仓状态:READY_FOR_DISPATCH待确认出仓、WAITING_FOR_LOADING等待装柜', + version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号,每次更新递增', + confirmed_by BIGINT UNSIGNED NULL COMMENT '确认国内出仓的操作人ID', + confirmed_by_name VARCHAR(64) NULL COMMENT '确认国内出仓的操作人姓名快照', + confirmed_at DATETIME(3) NULL COMMENT '国内出仓确认时间', + reversed_by BIGINT UNSIGNED NULL COMMENT '撤销国内出仓的操作人ID', + reversed_by_name VARCHAR(64) NULL COMMENT '撤销国内出仓的操作人姓名快照', + reversed_at DATETIME(3) NULL COMMENT '国内出仓撤销时间', + remark VARCHAR(500) NULL COMMENT '国内出仓调度备注', + 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 (id), UNIQUE KEY uk_outbound_dispatch_owner (tenant_id,inbound_id,container_booking_id), KEY idx_outbound_dispatch_order (tenant_id,order_id,updated_at), @@ -28,33 +28,33 @@ CREATE TABLE IF NOT EXISTS fl_outbound_dispatch ( ) ENGINE=InnoDB COMMENT='三期国内出仓派车状态,不重复创建出仓单'; CREATE TABLE IF NOT EXISTS fl_outbound_dispatch_history ( - id BIGINT UNSIGNED NOT NULL, - dispatch_id BIGINT UNSIGNED NOT NULL, - tenant_id BIGINT UNSIGNED NOT NULL, - inbound_id BIGINT UNSIGNED NOT NULL, - container_booking_id BIGINT UNSIGNED NOT NULL, - action VARCHAR(32) NOT NULL, - from_status VARCHAR(32) NULL, - to_status VARCHAR(32) NOT NULL, - remark VARCHAR(500) NULL, - operator_id BIGINT UNSIGNED NOT NULL, - operator_name VARCHAR(64) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + id BIGINT UNSIGNED NOT NULL COMMENT '出仓状态历史记录ID', + dispatch_id BIGINT UNSIGNED NOT NULL COMMENT '关联国内出仓调度记录ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + inbound_id BIGINT UNSIGNED NOT NULL COMMENT '关联入库单逻辑ID快照', + container_booking_id BIGINT UNSIGNED NOT NULL COMMENT '关联柜次逻辑ID快照', + action VARCHAR(32) NOT NULL COMMENT '出仓状态变更动作', + from_status VARCHAR(32) NULL COMMENT '变更前出仓状态;首次创建时为空', + to_status VARCHAR(32) NOT NULL COMMENT '变更后出仓状态', + remark VARCHAR(500) NULL COMMENT '状态变更原因或备注', + operator_id BIGINT UNSIGNED NOT NULL COMMENT '执行状态变更的平台用户ID', + operator_name VARCHAR(64) NULL COMMENT '执行状态变更的平台用户姓名快照', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '状态历史创建时间', PRIMARY KEY (id), KEY idx_outbound_dispatch_history (tenant_id,inbound_id,container_booking_id,created_at,id), CONSTRAINT fk_outbound_dispatch_history FOREIGN KEY (dispatch_id) REFERENCES fl_outbound_dispatch(id) ) ENGINE=InnoDB COMMENT='国内出仓追加式审计历史'; CREATE TABLE IF NOT EXISTS fl_outbound_dispatch_operation ( - id BIGINT UNSIGNED NOT NULL, + id BIGINT UNSIGNED NOT NULL COMMENT '出仓幂等操作记录ID', dispatch_id BIGINT UNSIGNED NULL COMMENT '首次声明幂等键时允许为空', - operator_id BIGINT UNSIGNED NOT NULL, - operation VARCHAR(32) NOT NULL, - idempotency_key VARCHAR(128) NOT NULL, - request_hash CHAR(64) NOT NULL, - status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING', - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - completed_at DATETIME(3) NULL, + operator_id BIGINT UNSIGNED NOT NULL COMMENT '执行写操作的平台用户ID', + operation VARCHAR(32) NOT NULL COMMENT '国内出仓写操作类型', + idempotency_key VARCHAR(128) NOT NULL COMMENT '客户端幂等键,同一操作重试复用', + request_hash CHAR(64) NOT NULL COMMENT '规范化请求内容SHA-256摘要', + status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING' COMMENT '幂等处理状态:PROCESSING处理中、SUCCESS成功', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '幂等声明创建时间', + completed_at DATETIME(3) NULL COMMENT '幂等操作完成时间', PRIMARY KEY (id), UNIQUE KEY uk_outbound_dispatch_operation (operator_id,operation,idempotency_key), KEY idx_outbound_dispatch_operation_resource (dispatch_id,created_at), diff --git a/firstleg/src/main/resources/db/phase3-pda-convergence-schema.sql b/firstleg/src/main/resources/db/phase3-pda-convergence-schema.sql index dce998cad..40374faa2 100644 --- a/firstleg/src/main/resources/db/phase3-pda-convergence-schema.sql +++ b/firstleg/src/main/resources/db/phase3-pda-convergence-schema.sql @@ -2,30 +2,46 @@ SET NAMES utf8mb4; USE baiyue_firstleg; CREATE TABLE IF NOT EXISTS fl_loading_pick_assignment ( - pick_task_id BIGINT UNSIGNED NOT NULL, - assignee_id BIGINT UNSIGNED NOT NULL, - assignee_name VARCHAR(64) NOT NULL, - assigned_by BIGINT UNSIGNED NOT NULL, - assigned_by_name VARCHAR(64) NULL, - assignment_type VARCHAR(16) NOT NULL COMMENT 'CLAIMED/ASSIGNED', - version INT UNSIGNED NOT NULL DEFAULT 0, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + pick_task_id BIGINT UNSIGNED NOT NULL COMMENT '柜次PDA拣货任务ID;同一任务仅保留一名作业员', + assignee_id BIGINT UNSIGNED NOT NULL COMMENT '当前唯一作业员ID', + assignee_name VARCHAR(64) NOT NULL COMMENT '当前作业员姓名快照', + assigned_by BIGINT UNSIGNED NOT NULL COMMENT '领取任务的作业员或执行分配的平台用户ID', + assigned_by_name VARCHAR(64) NULL COMMENT '领取或分配操作人姓名快照', + assignment_type VARCHAR(16) NOT NULL COMMENT '分配方式:CLAIMED作业员领取、ASSIGNED平台指派', + version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号,防止多人并发领取或覆盖', + 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 (pick_task_id), KEY idx_loading_pick_assignee (assignee_id, updated_at), CONSTRAINT fk_loading_pick_assignment_task FOREIGN KEY (pick_task_id) REFERENCES fl_loading_pick_task (id), CONSTRAINT chk_loading_pick_assignment_type CHECK (assignment_type IN ('CLAIMED', 'ASSIGNED')) ) ENGINE=InnoDB COMMENT='柜次PDA任务领取与作业员分配'; +CREATE TABLE IF NOT EXISTS fl_loading_pick_item_assignment ( + pick_item_id BIGINT UNSIGNED NOT NULL COMMENT 'PDA拣货明细ID,同时作为本表主键', + assignee_id BIGINT UNSIGNED NOT NULL COMMENT '当前领取或被分配的作业员ID', + assignee_name VARCHAR(64) NOT NULL COMMENT '当前作业员姓名快照', + assigned_by BIGINT UNSIGNED NOT NULL COMMENT '执行领取或分配操作的用户ID', + assigned_by_name VARCHAR(64) NULL COMMENT '执行领取或分配操作的用户姓名快照', + assignment_type VARCHAR(16) NOT NULL COMMENT '分配类型:CLAIMED作业员领取、ASSIGNED管理员分配', + version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号,每次更新递增', + 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 (pick_item_id), + KEY idx_loading_pick_item_assignee (assignee_id, updated_at), + CONSTRAINT fk_loading_pick_item_assignment_item FOREIGN KEY (pick_item_id) REFERENCES fl_loading_pick_item (id), + CONSTRAINT chk_loading_pick_item_assignment_type CHECK (assignment_type IN ('CLAIMED', 'ASSIGNED')) +) ENGINE=InnoDB COMMENT='PDA拣货明细领取与作业员分配'; + CREATE TABLE IF NOT EXISTS fl_loading_pick_review ( - pick_task_id BIGINT UNSIGNED NOT NULL, - review_status VARCHAR(16) NOT NULL DEFAULT 'APPROVED', - reviewed_by BIGINT UNSIGNED NOT NULL, - reviewed_by_name VARCHAR(64) NULL, - remark VARCHAR(500) NULL, - version INT UNSIGNED NOT NULL DEFAULT 0, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + pick_task_id BIGINT UNSIGNED NOT NULL COMMENT '已完成的柜次PDA拣货任务ID', + review_status VARCHAR(16) NOT NULL DEFAULT 'APPROVED' COMMENT '复核状态:APPROVED已通过;记录创建后不可修改或删除', + reviewed_by BIGINT UNSIGNED NOT NULL COMMENT '执行完成复核的平台用户ID', + reviewed_by_name VARCHAR(64) NULL COMMENT '复核人姓名快照', + remark VARCHAR(500) NULL COMMENT 'PDA任务复核备注', + version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '复核记录版本号', + 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 (pick_task_id), KEY idx_loading_pick_reviewer (reviewed_by, created_at), CONSTRAINT fk_loading_pick_review_task FOREIGN KEY (pick_task_id) REFERENCES fl_loading_pick_task (id), @@ -33,14 +49,14 @@ CREATE TABLE IF NOT EXISTS fl_loading_pick_review ( ) ENGINE=InnoDB COMMENT='柜次PDA任务完成复核'; CREATE TABLE IF NOT EXISTS fl_loading_pick_operation ( - id BIGINT UNSIGNED NOT NULL, - pick_task_id BIGINT UNSIGNED NOT NULL, - operation_id VARCHAR(128) NOT NULL, - request_hash CHAR(64) NOT NULL, - action_type VARCHAR(16) NOT NULL COMMENT 'CLAIM/ASSIGN/REVIEW', - operator_id BIGINT UNSIGNED NOT NULL, - operator_name VARCHAR(64) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + id BIGINT UNSIGNED NOT NULL COMMENT 'PDA任务操作审计记录ID', + pick_task_id BIGINT UNSIGNED NOT NULL COMMENT '关联柜次PDA拣货任务ID', + operation_id VARCHAR(128) NOT NULL COMMENT '全局唯一操作ID,用于幂等重放', + request_hash CHAR(64) NOT NULL COMMENT '完整批量请求或任务操作内容SHA-256摘要', + action_type VARCHAR(16) NOT NULL COMMENT '动作类型:CLAIM领取、ASSIGN分配、REVIEW复核、BATCH批量提交', + operator_id BIGINT UNSIGNED NOT NULL COMMENT '执行PDA任务操作的用户ID', + operator_name VARCHAR(64) NULL COMMENT '操作人姓名快照', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '不可变操作审计创建时间', PRIMARY KEY (id), UNIQUE KEY uk_loading_pick_operation_id (operation_id), KEY idx_loading_pick_operation_task (pick_task_id, created_at), diff --git a/firstleg/src/main/resources/db/phase3-port-schedule-schema.sql b/firstleg/src/main/resources/db/phase3-port-schedule-schema.sql index 8be920775..fb055126f 100644 --- a/firstleg/src/main/resources/db/phase3-port-schedule-schema.sql +++ b/firstleg/src/main/resources/db/phase3-port-schedule-schema.sql @@ -2,35 +2,35 @@ SET NAMES utf8mb4; USE baiyue_firstleg; CREATE TABLE IF NOT EXISTS fl_port_schedule ( - id BIGINT UNSIGNED NOT NULL, - schedule_no VARCHAR(40) NOT NULL, - customer_id BIGINT UNSIGNED NOT NULL COMMENT '当前产品客户ID', - ocean_booking_id BIGINT UNSIGNED NOT NULL, - container_booking_id BIGINT UNSIGNED NULL, - source_type VARCHAR(24) NOT NULL COMMENT 'MANUAL/CARRIER/TERMINAL/API', - source_name VARCHAR(128) NOT NULL, - source_ref VARCHAR(255) NOT NULL, - vessel_name VARCHAR(128) NOT NULL, - voyage_no VARCHAR(64) NOT NULL, - departure_port_code VARCHAR(32) NOT NULL, - departure_port_name VARCHAR(128) NULL, - destination_port_code VARCHAR(32) NOT NULL, - destination_port_name VARCHAR(128) NULL, - etd_at DATETIME(3) NULL, - eta_at DATETIME(3) NULL, + id BIGINT UNSIGNED NOT NULL COMMENT '港务船期记录ID', + schedule_no VARCHAR(40) NOT NULL COMMENT '港务船期记录编号', + customer_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + ocean_booking_id BIGINT UNSIGNED NOT NULL COMMENT '关联海运订舱ID', + container_booking_id BIGINT UNSIGNED NULL COMMENT '关联柜次ID;订舱整票船期时为空', + source_type VARCHAR(24) NOT NULL COMMENT '船期来源类型:MANUAL人工录入、CARRIER船公司、TERMINAL码头、API外部接口', + source_name VARCHAR(128) NOT NULL COMMENT '船期来源名称', + source_ref VARCHAR(255) NOT NULL COMMENT '来源侧记录唯一引用或查询凭据摘要', + vessel_name VARCHAR(128) NOT NULL COMMENT '船名快照', + voyage_no VARCHAR(64) NOT NULL COMMENT '航次号快照', + departure_port_code VARCHAR(32) NOT NULL COMMENT '起运港编码快照', + departure_port_name VARCHAR(128) NULL COMMENT '起运港名称快照', + destination_port_code VARCHAR(32) NOT NULL COMMENT '目的港编码快照', + destination_port_name VARCHAR(128) NULL COMMENT '目的港名称快照', + etd_at DATETIME(3) NULL COMMENT '预计离港时间', + eta_at DATETIME(3) NULL COMMENT '预计到港时间', booking_cutoff_at DATETIME(3) NULL COMMENT '截单时间', customs_cutoff_at DATETIME(3) NULL COMMENT '截关时间', port_cutoff_at DATETIME(3) NULL COMMENT '截港时间', observed_at DATETIME(3) NOT NULL COMMENT '来源数据观测时间', expires_at DATETIME(3) NOT NULL COMMENT '超过该时间即由API计算为过期', - remark VARCHAR(1000) NULL, - created_by BIGINT UNSIGNED NOT NULL, - created_by_name VARCHAR(64) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_by BIGINT UNSIGNED NOT NULL, - updated_by_name VARCHAR(64) NULL, - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - version INT UNSIGNED NOT NULL DEFAULT 0, + remark VARCHAR(1000) NULL COMMENT '平台内部备注', + created_by BIGINT UNSIGNED NOT NULL COMMENT '创建操作人ID', + created_by_name VARCHAR(64) NULL COMMENT '创建操作人姓名快照', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '船期记录创建时间', + updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改操作人ID', + updated_by_name VARCHAR(64) NULL COMMENT '最后修改操作人姓名快照', + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '船期记录最后更新时间', + version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', PRIMARY KEY (id), UNIQUE KEY uk_port_schedule_no (schedule_no), UNIQUE KEY uk_port_schedule_source (source_type,source_name,source_ref), @@ -53,17 +53,17 @@ CREATE TABLE IF NOT EXISTS fl_port_schedule ( ) ENGINE=InnoDB COMMENT='独立港务船期来源聚合,不替代订舱快照'; CREATE TABLE IF NOT EXISTS fl_port_schedule_history ( - id BIGINT UNSIGNED NOT NULL, - port_schedule_id BIGINT UNSIGNED NOT NULL, - action VARCHAR(24) NOT NULL, - from_version INT UNSIGNED NULL, - to_version INT UNSIGNED NOT NULL, - reason VARCHAR(1000) NULL, - before_snapshot JSON NULL, - after_snapshot JSON NOT NULL, - operator_id BIGINT UNSIGNED NOT NULL, - operator_name VARCHAR(64) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + id BIGINT UNSIGNED NOT NULL COMMENT '港务船期变更历史ID', + port_schedule_id BIGINT UNSIGNED NOT NULL COMMENT '所属港务船期记录ID', + action VARCHAR(24) NOT NULL COMMENT '变更动作:CREATE创建、UPDATE更新', + from_version INT UNSIGNED NULL COMMENT '变更前版本;创建时为空', + to_version INT UNSIGNED NOT NULL COMMENT '变更后版本', + reason VARCHAR(1000) NULL COMMENT '变更原因或说明', + before_snapshot JSON NULL COMMENT '变更前船期数据快照;创建时为空', + after_snapshot JSON NOT NULL COMMENT '变更后船期数据快照', + operator_id BIGINT UNSIGNED NOT NULL COMMENT '操作人ID', + operator_name VARCHAR(64) NULL COMMENT '操作人姓名快照', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '历史记录创建时间', PRIMARY KEY (id), KEY idx_port_schedule_history (port_schedule_id,created_at,id), CONSTRAINT fk_port_schedule_history FOREIGN KEY (port_schedule_id) REFERENCES fl_port_schedule(id), @@ -71,16 +71,16 @@ CREATE TABLE IF NOT EXISTS fl_port_schedule_history ( ) ENGINE=InnoDB COMMENT='港务船期不可变变更历史'; CREATE TABLE IF NOT EXISTS fl_port_schedule_operation ( - id BIGINT UNSIGNED NOT NULL, - port_schedule_id BIGINT UNSIGNED NULL COMMENT '创建请求完成前允许为空', - operator_id BIGINT UNSIGNED NOT NULL, - operation VARCHAR(24) NOT NULL, - idempotency_key VARCHAR(128) NOT NULL, - request_hash CHAR(64) NOT NULL, - status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING', - result_json JSON NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - completed_at DATETIME(3) NULL, + id BIGINT UNSIGNED NOT NULL COMMENT '港务船期写操作幂等记录ID', + port_schedule_id BIGINT UNSIGNED NULL COMMENT '关联港务船期ID;创建请求完成前允许为空', + operator_id BIGINT UNSIGNED NOT NULL COMMENT '发起写操作的平台操作人ID', + operation VARCHAR(24) NOT NULL COMMENT '幂等写操作类型', + idempotency_key VARCHAR(128) NOT NULL COMMENT '客户端写操作幂等键', + request_hash CHAR(64) NOT NULL COMMENT '规范化请求内容SHA-256摘要;用于识别同键不同请求', + status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING' COMMENT '处理状态:PROCESSING处理中、SUCCESS成功', + result_json JSON NULL COMMENT '成功结果JSON快照;供幂等重放返回', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '幂等记录创建时间', + completed_at DATETIME(3) NULL COMMENT '写操作成功完成时间', PRIMARY KEY (id), UNIQUE KEY uk_port_schedule_operation (operator_id,idempotency_key), KEY idx_port_schedule_operation_resource (port_schedule_id,created_at), diff --git a/firstleg/src/main/resources/db/phase3-smart-loading-schema.sql b/firstleg/src/main/resources/db/phase3-smart-loading-schema.sql index dc2d3194c..0e9d91cb2 100644 --- a/firstleg/src/main/resources/db/phase3-smart-loading-schema.sql +++ b/firstleg/src/main/resources/db/phase3-smart-loading-schema.sql @@ -60,8 +60,7 @@ CREATE TABLE IF NOT EXISTS fl_container_booking ( created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改人ID', updated_by_name VARCHAR(64) NULL COMMENT '最后修改人名称快照', - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) - COMMENT '更新时间', + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '货柜资料最后更新时间', version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', PRIMARY KEY (id), UNIQUE KEY uk_container_booking_no (booking_no), @@ -77,8 +76,7 @@ CREATE TABLE IF NOT EXISTS fl_loading_plan ( id BIGINT UNSIGNED NOT NULL COMMENT '智能装柜计划ID(雪花ID)', plan_no VARCHAR(40) NOT NULL COMMENT '平台唯一装柜计划号', container_booking_id BIGINT UNSIGNED NOT NULL COMMENT '货柜预订ID', - plan_status VARCHAR(24) NOT NULL DEFAULT 'EDITING' - COMMENT '唯一装柜状态:EDITING、LOADING、COMPLETED', + plan_status VARCHAR(24) NOT NULL DEFAULT 'EDITING' COMMENT '装柜计划状态:EDITING编辑中、LOADING装柜中、COMPLETED装柜完成;建议须经人工确认', push_idempotency_key VARCHAR(128) NULL COMMENT '推送预装幂等键', preload_pushed_by BIGINT UNSIGNED NULL COMMENT '推送预装操作人ID', preload_pushed_by_name VARCHAR(64) NULL COMMENT '推送预装操作人名称快照', @@ -95,8 +93,7 @@ CREATE TABLE IF NOT EXISTS fl_loading_plan ( created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改人ID', updated_by_name VARCHAR(64) NULL COMMENT '最后修改人名称快照', - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) - COMMENT '更新时间', + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '装柜计划最后更新时间', version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', PRIMARY KEY (id), UNIQUE KEY uk_loading_plan_no (plan_no), @@ -149,16 +146,13 @@ CREATE TABLE IF NOT EXISTS fl_loading_plan_item ( removed_by_name VARCHAR(64) NULL COMMENT '推送前逻辑删除操作人名称快照', removed_at DATETIME(3) NULL COMMENT '推送前逻辑删除时间', deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '推送前逻辑删除标记;冻结后不可修改', - active_inbound_detail_id BIGINT UNSIGNED GENERATED ALWAYS AS ( - CASE WHEN deleted = 0 THEN inbound_detail_id ELSE NULL END - ) STORED COMMENT '未删除明细唯一锁;删除后返回NULL释放分配资格', + active_inbound_detail_id BIGINT UNSIGNED GENERATED ALWAYS AS (CASE WHEN deleted = 0 THEN inbound_detail_id ELSE NULL END) STORED COMMENT '未删除明细唯一锁;删除后返回NULL释放分配资格', created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID', created_by_name VARCHAR(64) NULL COMMENT '创建人名称快照', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改人ID', updated_by_name VARCHAR(64) NULL COMMENT '最后修改人名称快照', - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) - COMMENT '更新时间', + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '装柜明细最后更新时间', version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', PRIMARY KEY (id), UNIQUE KEY uk_loading_item_plan_detail (loading_plan_id, active_inbound_detail_id), @@ -185,15 +179,14 @@ CREATE TABLE IF NOT EXISTS fl_loading_plan_item ( CREATE TABLE IF NOT EXISTS fl_container_document ( id BIGINT UNSIGNED NOT NULL COMMENT '货柜文件关联ID(雪花ID)', container_booking_id BIGINT UNSIGNED NOT NULL COMMENT '货柜预订ID', - document_type VARCHAR(32) NOT NULL COMMENT 'CUSTOMS_CLEARANCE、DELIVERY_ORDER、PICKUP_ORDER', + document_type VARCHAR(32) NOT NULL COMMENT '文件类型:CUSTOMS_CLEARANCE清关文件、DELIVERY_ORDER提货单、PICKUP_ORDER取货单', file_id BIGINT UNSIGNED NOT NULL COMMENT 'File服务文件ID,不建立跨库外键', file_name VARCHAR(255) NOT NULL COMMENT '文件名称快照', file_url VARCHAR(1000) NULL COMMENT '文件访问地址快照', uploaded_by BIGINT UNSIGNED NOT NULL COMMENT '上传操作人ID', uploaded_by_name VARCHAR(64) NULL COMMENT '上传操作人名称快照', 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 '更新时间', + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '货柜文件关联最后更新时间', version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', PRIMARY KEY (id), UNIQUE KEY uk_container_document_type (container_booking_id, document_type), @@ -209,8 +202,7 @@ CREATE TABLE IF NOT EXISTS fl_loading_plan_history ( loading_plan_id BIGINT UNSIGNED NOT NULL COMMENT '智能装柜计划ID', loading_plan_item_id BIGINT UNSIGNED NULL COMMENT '装柜计划明细ID', operation_id VARCHAR(128) NOT NULL COMMENT '操作幂等ID', - action_type VARCHAR(32) NOT NULL - COMMENT 'CREATE_PLAN、ADD_ITEM、ADJUST_ITEM、REMOVE_ITEM、START_LOADING、COMPLETE_LOADING、PICK_ITEM', + action_type VARCHAR(32) NOT NULL COMMENT '动作类型:CREATE_PLAN创建计划、ADD_ITEM新增明细、ADJUST_ITEM调整明细、REMOVE_ITEM移除明细、START_LOADING开始装柜、COMPLETE_LOADING完成装柜、PICK_ITEM拣货', from_status VARCHAR(24) NULL COMMENT '变更前状态', to_status VARCHAR(24) NULL COMMENT '变更后状态', before_snapshot JSON NULL COMMENT '变更前快照', @@ -405,7 +397,7 @@ CREATE TABLE IF NOT EXISTS fl_loading_pick_task ( id BIGINT UNSIGNED NOT NULL COMMENT 'PDA拣货任务ID', loading_plan_id BIGINT UNSIGNED NOT NULL COMMENT '装柜计划ID', container_no VARCHAR(64) NOT NULL COMMENT '集装箱号快照', - task_status VARCHAR(16) NOT NULL DEFAULT 'LOADING' COMMENT 'LOADING、COMPLETED', + task_status VARCHAR(16) NOT NULL DEFAULT 'LOADING' COMMENT 'PDA任务状态:LOADING拣货装柜中、COMPLETED已完成', started_by BIGINT UNSIGNED NOT NULL COMMENT '开始装柜人', started_by_name VARCHAR(64) NULL COMMENT '开始装柜人名称', started_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '开始时间', @@ -428,12 +420,12 @@ CREATE TABLE IF NOT EXISTS fl_loading_pick_item ( inbound_detail_id BIGINT UNSIGNED NOT NULL COMMENT '订单货物明细ID', planned_package_count INT UNSIGNED NOT NULL COMMENT '计划拣货箱数;上层拆分按业务规则向上取整', picked_package_count INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '已拣货箱数', - item_status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING、PICKING、PICKED、PARTIAL、SKIPPED', + item_status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT '拣货明细状态:PENDING待拣、PICKING拣货中、PICKED已拣完、PARTIAL部分拣货、SKIPPED跳拣', release_mode VARCHAR(10) NULL COMMENT 'AUTO按比例释放,MANUAL按人工选择释放', release_location_ids JSON NULL COMMENT '人工选择释放库位ID;空数组表示明确释放0个', version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 'PDA拣货明细创建时间', + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT 'PDA拣货明细最后更新时间', PRIMARY KEY (id), UNIQUE KEY uk_loading_pick_item_plan_item (loading_plan_item_id), KEY idx_loading_pick_item_task (pick_task_id, item_status, id), @@ -449,13 +441,13 @@ CREATE TABLE IF NOT EXISTS fl_loading_pick_history ( pick_task_id BIGINT UNSIGNED NOT NULL COMMENT 'PDA拣货任务ID', pick_item_id BIGINT UNSIGNED NOT NULL COMMENT 'PDA拣货明细ID', operation_id VARCHAR(128) NOT NULL COMMENT '客户端幂等操作号', - request_hash CHAR(64) NOT NULL COMMENT '请求指纹', - action_type VARCHAR(24) NOT NULL DEFAULT 'PICK' COMMENT 'PICK、PARTIAL、SKIP', + request_hash CHAR(64) NOT NULL COMMENT '完整批量请求内容SHA-256摘要', + action_type VARCHAR(24) NOT NULL DEFAULT 'PICK' COMMENT '拣货动作:PICK正常拣货、PARTIAL部分拣货、SKIP跳拣', picked_package_count INT UNSIGNED NOT NULL COMMENT '本次拣货箱数', operation_reason VARCHAR(500) NULL COMMENT '部分拣货或跳拣原因', - operator_id BIGINT UNSIGNED NOT NULL COMMENT '操作人', - operator_name VARCHAR(64) NULL COMMENT '操作人名称', - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + operator_id BIGINT UNSIGNED NOT NULL COMMENT '执行PDA拣货操作的作业员ID', + operator_name VARCHAR(64) NULL COMMENT '作业员姓名快照', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '不可变拣货操作历史创建时间', PRIMARY KEY (id), UNIQUE KEY uk_loading_pick_history_operation (operation_id), KEY idx_loading_pick_history_item (pick_item_id, created_at), diff --git a/firstleg/src/main/resources/db/phase3-split-preload-schema.sql b/firstleg/src/main/resources/db/phase3-split-preload-schema.sql index f0a099c77..8004ba31b 100644 --- a/firstleg/src/main/resources/db/phase3-split-preload-schema.sql +++ b/firstleg/src/main/resources/db/phase3-split-preload-schema.sql @@ -4,18 +4,18 @@ USE baiyue_firstleg; /* 三期分柜与预装清单;独立于现有测量、计费表,可重复部署。 */ CREATE TABLE IF NOT EXISTS fl_preload_plan ( id BIGINT UNSIGNED NOT NULL COMMENT '预装清单ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离ID', - inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单/订单ID', - status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/CONFIRMING/CONFIRMED', - version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本', - items_json JSON NOT NULL COMMENT '可编辑分柜明细快照', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + inbound_id BIGINT UNSIGNED NOT NULL COMMENT '关联入库单逻辑ID', + status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT '预装状态:PENDING待确认、CONFIRMING确认中、CONFIRMED已确认', + version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号,每次更新递增', + items_json JSON NOT NULL COMMENT '可编辑分柜明细JSON快照;人工确认后作为预装依据', remark VARCHAR(1000) NULL COMMENT '预装备注', - created_by BIGINT UNSIGNED NOT NULL, - created_by_name VARCHAR(64) NULL, - updated_by BIGINT UNSIGNED NOT NULL, - updated_by_name VARCHAR(64) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + created_by BIGINT UNSIGNED NOT NULL COMMENT '创建预装清单的操作人ID', + created_by_name VARCHAR(64) NULL COMMENT '创建操作人姓名快照', + updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改操作人ID', + updated_by_name VARCHAR(64) NULL COMMENT '最后修改操作人姓名快照', + 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 (id), UNIQUE KEY uk_preload_tenant_inbound (tenant_id, inbound_id), KEY idx_preload_status (tenant_id, status, updated_at), @@ -24,14 +24,14 @@ CREATE TABLE IF NOT EXISTS fl_preload_plan ( CREATE TABLE IF NOT EXISTS fl_split_operation ( id BIGINT UNSIGNED NOT NULL COMMENT '幂等记录ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离ID', - inbound_id BIGINT UNSIGNED NOT NULL, - operator_id BIGINT UNSIGNED NOT NULL, - operation VARCHAR(40) NOT NULL, - idempotency_key VARCHAR(128) NOT NULL, - status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING' COMMENT 'PROCESSING/SUCCESS', - result_json JSON NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + inbound_id BIGINT UNSIGNED NOT NULL COMMENT '操作目标入库单逻辑ID', + operator_id BIGINT UNSIGNED NOT NULL COMMENT '执行分柜或预装写操作的平台用户ID', + operation VARCHAR(40) NOT NULL COMMENT '分柜或预装写操作类型', + idempotency_key VARCHAR(128) NOT NULL COMMENT '客户端幂等键,同一操作重试复用', + status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING' COMMENT '幂等处理状态:PROCESSING处理中、SUCCESS成功', + result_json JSON NULL COMMENT '操作结果JSON快照,用于幂等重放', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '幂等操作声明创建时间', PRIMARY KEY (id), UNIQUE KEY uk_split_operation (tenant_id, inbound_id, operator_id, operation, idempotency_key), KEY idx_split_operation_time (tenant_id, inbound_id, created_at), @@ -96,25 +96,23 @@ DEALLOCATE PREPARE phase3_split_stmt; CREATE TABLE IF NOT EXISTS fl_inbound_split_detail ( id BIGINT UNSIGNED NOT NULL COMMENT '部分锁定明细ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', parent_inbound_id BIGINT UNSIGNED NOT NULL COMMENT '主入库单ID', child_inbound_id BIGINT UNSIGNED NULL COMMENT '完成拆单后生成的二级子入库单ID', inbound_detail_id BIGINT UNSIGNED NOT NULL COMMENT '原订单货物明细ID,不建立跨库外键', source_package_count INT UNSIGNED NOT NULL COMMENT '拆分前该明细总箱数快照', locked_package_count INT UNSIGNED NOT NULL COMMENT '本次装柜锁定箱数', - remaining_package_count INT UNSIGNED GENERATED ALWAYS AS ( - source_package_count - locked_package_count - ) STORED COMMENT '生成二级子单的剩余箱数', - status VARCHAR(16) NOT NULL DEFAULT 'LOCKED' COMMENT 'LOCKED/COMPLETED', + remaining_package_count INT UNSIGNED GENERATED ALWAYS AS (source_package_count - locked_package_count) STORED COMMENT '锁定后剩余箱数,由来源箱数减锁定箱数生成', + status VARCHAR(16) NOT NULL DEFAULT 'LOCKED' COMMENT '分柜明细状态:LOCKED已锁定、COMPLETED已完成拆分', operation_id VARCHAR(128) NOT NULL COMMENT '部分锁定操作幂等号', - created_by BIGINT UNSIGNED NOT NULL, - created_by_name VARCHAR(64) NULL, - completed_by BIGINT UNSIGNED NULL, - completed_by_name VARCHAR(64) NULL, - completed_at DATETIME(3) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - version INT UNSIGNED NOT NULL DEFAULT 0, + created_by BIGINT UNSIGNED NOT NULL COMMENT '创建部分锁定明细的操作人ID', + created_by_name VARCHAR(64) NULL COMMENT '创建操作人姓名快照', + completed_by BIGINT UNSIGNED NULL COMMENT '完成拆分的操作人ID', + completed_by_name VARCHAR(64) NULL COMMENT '完成操作人姓名快照', + completed_at DATETIME(3) NULL COMMENT '部分拆分完成时间', + 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 '部分锁定明细最后更新时间', + version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号,每次更新递增', PRIMARY KEY (id), UNIQUE KEY uk_inbound_split_parent_detail (tenant_id, parent_inbound_id, inbound_detail_id), UNIQUE KEY uk_inbound_split_child_detail (tenant_id, child_inbound_id, inbound_detail_id), diff --git a/firstleg/src/main/resources/db/phase3-truck-appointment-schema.sql b/firstleg/src/main/resources/db/phase3-truck-appointment-schema.sql index 85dd1686c..97299a87b 100644 --- a/firstleg/src/main/resources/db/phase3-truck-appointment-schema.sql +++ b/firstleg/src/main/resources/db/phase3-truck-appointment-schema.sql @@ -1,33 +1,64 @@ SET NAMES utf8mb4; CREATE TABLE IF NOT EXISTS fl_truck_appointment ( - id BIGINT UNSIGNED NOT NULL, appointment_no VARCHAR(40) NOT NULL, outbound_order_id BIGINT UNSIGNED NULL, - ocean_booking_id BIGINT UNSIGNED NULL, container_booking_id BIGINT UNSIGNED NULL, loading_plan_id BIGINT UNSIGNED NULL, - warehouse_id BIGINT UNSIGNED NULL, customer_id BIGINT UNSIGNED NULL, customer_name VARCHAR(128) NULL, - carrier_id BIGINT UNSIGNED NULL, carrier_name VARCHAR(128) NULL, fleet_contact VARCHAR(64) NULL, fleet_phone VARCHAR(32) NULL, - driver_name VARCHAR(64) NULL, driver_phone VARCHAR(32) NULL, driver_id_card VARCHAR(32) NULL, vehicle_no VARCHAR(32) NULL, - store_name VARCHAR(128) NULL, store_contact VARCHAR(64) NULL, store_phone VARCHAR(32) NULL, store_address VARCHAR(255) NULL, - store_city VARCHAR(64) NULL, store_zipcode VARCHAR(16) NULL, package_count INT UNSIGNED NULL, gross_weight_kg DECIMAL(18,3) NULL, - volume_cbm DECIMAL(18,4) NULL, pallet_count INT UNSIGNED NULL, po_no VARCHAR(255) NULL, planned_loading_at DATETIME(3) NULL, - actual_arrived_warehouse_at DATETIME(3) NULL, actual_loading_at DATETIME(3) NULL, actual_departed_at DATETIME(3) NULL, transit_minutes INT UNSIGNED NULL, payable_amount DECIMAL(18,2) NULL, receivable_amount DECIMAL(18,2) NULL, - currency VARCHAR(8) NOT NULL DEFAULT 'RMB', status VARCHAR(32) NOT NULL DEFAULT 'DRAFT', revision INT UNSIGNED NOT NULL DEFAULT 0, - version INT UNSIGNED NOT NULL DEFAULT 0, remark VARCHAR(1000) NULL, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), PRIMARY KEY(id), UNIQUE KEY uk_truck_appointment_no(appointment_no), + id BIGINT UNSIGNED NOT NULL COMMENT '拖车预约ID', + appointment_no VARCHAR(40) NOT NULL COMMENT '平台拖车预约编号', + outbound_order_id BIGINT UNSIGNED NULL COMMENT '关联头程订单逻辑ID;不跨库建外键', + ocean_booking_id BIGINT UNSIGNED NULL COMMENT '关联海运订舱逻辑ID', + container_booking_id BIGINT UNSIGNED NULL COMMENT '关联柜次逻辑ID;拖车完成供装柜门禁校验', + loading_plan_id BIGINT UNSIGNED NULL COMMENT '关联装柜计划逻辑ID', + warehouse_id BIGINT UNSIGNED NULL COMMENT '预约到仓的国内收货仓逻辑ID', + customer_id BIGINT UNSIGNED NULL COMMENT '客户ID和数据隔离键', + customer_name VARCHAR(128) NULL COMMENT '客户名称快照', + carrier_id BIGINT UNSIGNED NULL COMMENT '拖车服务商逻辑ID', + carrier_name VARCHAR(128) NULL COMMENT '拖车服务商名称快照', + fleet_contact VARCHAR(64) NULL COMMENT '车队联系人', + fleet_phone VARCHAR(32) NULL COMMENT '车队联系电话', + driver_name VARCHAR(64) NULL COMMENT '司机姓名', + driver_phone VARCHAR(32) NULL COMMENT '司机联系电话', + driver_id_card VARCHAR(32) NULL COMMENT '司机身份证号;平台内部敏感信息', + vehicle_no VARCHAR(32) NULL COMMENT '车辆车牌号', + store_name VARCHAR(128) NULL COMMENT '到仓装柜地点名称快照', + store_contact VARCHAR(64) NULL COMMENT '到仓地点联系人快照', + store_phone VARCHAR(32) NULL COMMENT '到仓地点联系电话快照', + store_address VARCHAR(255) NULL COMMENT '到仓装柜地点地址快照', + store_city VARCHAR(64) NULL COMMENT '到仓地点城市快照', + store_zipcode VARCHAR(16) NULL COMMENT '到仓地点邮编快照', + package_count INT UNSIGNED NULL COMMENT '预约货物包装件数快照', + gross_weight_kg DECIMAL(18,3) NULL COMMENT '预约货物毛重,单位千克', + volume_cbm DECIMAL(18,4) NULL COMMENT '预约货物体积,单位立方米', + pallet_count INT UNSIGNED NULL COMMENT '预约托盘数量', + po_no VARCHAR(255) NULL COMMENT '关联采购单号或客户PO号', + planned_loading_at DATETIME(3) NULL COMMENT '预约到仓装柜时间', + actual_arrived_warehouse_at DATETIME(3) NULL COMMENT '车辆实际到达收货仓时间', + actual_loading_at DATETIME(3) NULL COMMENT '实际开始装柜时间', + actual_departed_at DATETIME(3) NULL COMMENT '车辆实际离仓时间;DEPARTED后供LOADED门禁使用', + transit_minutes INT UNSIGNED NULL COMMENT '从到仓到离仓的运输作业耗时,单位分钟', + payable_amount DECIMAL(18,2) NULL COMMENT '应付拖车服务商金额;币种由currency指定', + receivable_amount DECIMAL(18,2) NULL COMMENT '向客户收取的拖车金额;币种由currency指定', + currency VARCHAR(8) NOT NULL DEFAULT 'RMB' COMMENT '金额币种;首期默认RMB', + status VARCHAR(32) NOT NULL DEFAULT 'DRAFT' COMMENT '拖车预约状态:DRAFT草稿、PENDING_DISPATCH待派车、DISPATCHED已派车、PICKUP_IN_PROGRESS提货中、ARRIVED_WAREHOUSE已到仓、LOADING装柜中、DEPARTED已离仓、COMPLETED已完成、RESCHEDULED已改约、EXCEPTION异常、CANCELLED已取消', + revision INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '拖车预约业务修订号;每次状态或改约递增', + version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', + remark VARCHAR(1000) NULL COMMENT '平台内部备注或异常说明', + 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(id), UNIQUE KEY uk_truck_appointment_no(appointment_no), KEY idx_truck_appointment_status(status,updated_at), KEY idx_truck_appointment_container(container_booking_id), KEY idx_truck_appointment_customer(customer_id), CONSTRAINT chk_truck_appointment_status CHECK(status IN ('DRAFT','PENDING_DISPATCH','DISPATCHED','PICKUP_IN_PROGRESS','ARRIVED_WAREHOUSE','LOADING','DEPARTED','COMPLETED','RESCHEDULED','EXCEPTION','CANCELLED')) ) ENGINE=InnoDB COMMENT='三期拖车预约派车聚合'; CREATE TABLE IF NOT EXISTS fl_truck_appointment_history ( - id BIGINT UNSIGNED NOT NULL, appointment_id BIGINT UNSIGNED NOT NULL, revision INT UNSIGNED NOT NULL, action VARCHAR(32) NOT NULL, - from_status VARCHAR(32) NULL, to_status VARCHAR(32) NULL, reason VARCHAR(500) NULL, operator_id BIGINT UNSIGNED NOT NULL, - operator_name VARCHAR(64) NULL, request_id VARCHAR(128) NULL, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), PRIMARY KEY(id), KEY idx_truck_history(appointment_id,revision) + id BIGINT UNSIGNED NOT NULL COMMENT '拖车预约历史ID', appointment_id BIGINT UNSIGNED NOT NULL COMMENT '所属拖车预约ID', revision INT UNSIGNED NOT NULL COMMENT '该预约业务修订号', action VARCHAR(32) NOT NULL COMMENT '触发状态或改约的业务动作', + from_status VARCHAR(32) NULL COMMENT '变更前拖车状态;创建时为空', to_status VARCHAR(32) NULL COMMENT '变更后拖车状态;改约时可保持原状态', reason VARCHAR(500) NULL COMMENT '状态或改约原因', operator_id BIGINT UNSIGNED NOT NULL COMMENT '执行操作的平台用户ID', + operator_name VARCHAR(64) NULL COMMENT '操作人姓名快照', request_id VARCHAR(128) NULL COMMENT '请求链路中的请求ID', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '历史记录创建时间', PRIMARY KEY(id), KEY idx_truck_history(appointment_id,revision) ) ENGINE=InnoDB COMMENT='拖车状态与改约历史'; CREATE TABLE IF NOT EXISTS fl_truck_appointment_attachment ( - id BIGINT UNSIGNED NOT NULL, appointment_id BIGINT UNSIGNED NOT NULL, file_id BIGINT UNSIGNED NOT NULL, attachment_type VARCHAR(32) NOT NULL, - file_name VARCHAR(255) NULL, file_hash CHAR(64) NULL, visibility VARCHAR(24) NOT NULL DEFAULT 'INTERNAL', uploaded_by BIGINT UNSIGNED NOT NULL, - uploaded_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), PRIMARY KEY(id), UNIQUE KEY uk_truck_attachment(appointment_id,file_id), KEY idx_truck_attachment(appointment_id) + id BIGINT UNSIGNED NOT NULL COMMENT '拖车附件关系ID', appointment_id BIGINT UNSIGNED NOT NULL COMMENT '所属拖车预约ID', file_id BIGINT UNSIGNED NOT NULL COMMENT 'File服务文件逻辑ID', attachment_type VARCHAR(32) NOT NULL COMMENT '附件类型编码;由业务动作约定', + file_name VARCHAR(255) NULL COMMENT '上传时文件名快照', file_hash CHAR(64) NULL COMMENT '文件内容SHA-256摘要', visibility VARCHAR(24) NOT NULL DEFAULT 'INTERNAL' COMMENT '可见范围:INTERNAL仅平台内部、CLIENT客户可见', uploaded_by BIGINT UNSIGNED NOT NULL COMMENT '上传操作人ID', + uploaded_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '附件上传时间', PRIMARY KEY(id), UNIQUE KEY uk_truck_attachment(appointment_id,file_id), KEY idx_truck_attachment(appointment_id) ) ENGINE=InnoDB COMMENT='拖车附件文件引用'; CREATE TABLE IF NOT EXISTS fl_truck_appointment_operation ( - id BIGINT UNSIGNED NOT NULL, appointment_id BIGINT UNSIGNED NOT NULL, idempotency_key VARCHAR(128) NOT NULL, request_hash CHAR(64) NOT NULL, - action VARCHAR(32) NOT NULL, response_snapshot JSON NULL, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), PRIMARY KEY(id), UNIQUE KEY uk_truck_operation(idempotency_key), KEY idx_truck_operation_appointment(appointment_id) + id BIGINT UNSIGNED NOT NULL COMMENT '拖车写操作幂等记录ID', appointment_id BIGINT UNSIGNED NOT NULL COMMENT '所属拖车预约ID', idempotency_key VARCHAR(128) NOT NULL COMMENT '客户端写操作幂等键', request_hash CHAR(64) NOT NULL COMMENT '规范化请求内容SHA-256摘要;用于识别同键不同请求', + action VARCHAR(32) NOT NULL COMMENT '执行的拖车业务动作', response_snapshot JSON NULL COMMENT '成功响应JSON快照;供相同幂等请求重放', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '幂等记录创建时间', PRIMARY KEY(id), UNIQUE KEY uk_truck_operation(idempotency_key), KEY idx_truck_operation_appointment(appointment_id) ) ENGINE=InnoDB COMMENT='拖车操作幂等记录'; DROP PROCEDURE IF EXISTS phase3_truck_add_column; @@ -54,11 +85,11 @@ DELIMITER ; CALL phase3_truck_add_column( 'actual_arrived_warehouse_at', - 'DATETIME(3) NULL AFTER planned_loading_at' + 'DATETIME(3) NULL COMMENT ''车辆实际到达收货仓时间'' AFTER planned_loading_at' ); CALL phase3_truck_add_column( 'actual_departed_at', - 'DATETIME(3) NULL AFTER actual_loading_at' + 'DATETIME(3) NULL COMMENT ''车辆实际离仓时间;DEPARTED后供LOADED门禁使用'' AFTER actual_loading_at' ); DROP PROCEDURE IF EXISTS phase3_truck_add_column; diff --git a/firstleg/src/test/java/com/baiyue/firstleg/client/FinanceFeeSourceEventClientContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/client/FinanceFeeSourceEventClientContractTest.java new file mode 100644 index 000000000..542b707f3 --- /dev/null +++ b/firstleg/src/test/java/com/baiyue/firstleg/client/FinanceFeeSourceEventClientContractTest.java @@ -0,0 +1,25 @@ +package com.baiyue.firstleg.client; + +import org.junit.jupiter.api.Test; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.RequestHeader; + +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; + +class FinanceFeeSourceEventClientContractTest { + @Test + void usesFrozenFinanceInboxPathAndIdempotencyHeaders() { + FeignClient client = FinanceFeeSourceEventClient.class.getAnnotation(FeignClient.class); + assertThat(client.name()).isEqualTo("finance-service"); + assertThat(client.path()).isEqualTo("/internal/finance/v1/fee-source-events"); + var method = FinanceFeeSourceEventClient.class.getDeclaredMethods()[0]; + assertThat(Arrays.stream(method.getParameterAnnotations()) + .flatMap(Arrays::stream) + .filter(RequestHeader.class::isInstance) + .map(RequestHeader.class::cast) + .map(RequestHeader::value)) + .contains("X-Internal-Service-Token", "Idempotency-Key"); + } +} diff --git a/firstleg/src/test/java/com/baiyue/firstleg/controller/InternalFinanceFeeSourceControllerTest.java b/firstleg/src/test/java/com/baiyue/firstleg/controller/InternalFinanceFeeSourceControllerTest.java new file mode 100644 index 000000000..dbb07bbb5 --- /dev/null +++ b/firstleg/src/test/java/com/baiyue/firstleg/controller/InternalFinanceFeeSourceControllerTest.java @@ -0,0 +1,54 @@ +package com.baiyue.firstleg.controller; + +import com.baiyue.common.core.exception.BizException; +import com.baiyue.firstleg.service.FinanceFeeSourceSnapshotService; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +class InternalFinanceFeeSourceControllerTest { + private final FinanceFeeSourceSnapshotService service = mock(FinanceFeeSourceSnapshotService.class); + private final InternalFinanceFeeSourceController controller = controller(); + + @Test + void rejectsMissingOrWrongInternalTokenBeforeSnapshotLookup() { + assertThatThrownBy(() -> controller.inbound(null, 21L, 31L)) + .isInstanceOf(BizException.class).hasMessageContaining("令牌无效"); + assertThatThrownBy(() -> controller.container("wrong", 41L, 51L)) + .isInstanceOf(BizException.class).hasMessageContaining("令牌无效"); + } + + @Test + void routesAuthorizedSnapshotLookup() { + controller.inbound("finance-token", 21L, 31L); + controller.container("finance-token", 41L, 51L); + + verify(service).inbound(21L, 31L); + verify(service).container(41L, 51L); + } + + @Test + void exposesFrozenInternalPaths() { + var mapping = InternalFinanceFeeSourceController.class + .getAnnotation(org.springframework.web.bind.annotation.RequestMapping.class); + assertThat(mapping.value()).containsExactly("/internal/firstleg/v1"); + assertThat(methodPath("inbound")).isEqualTo("/inbounds/{inboundId}/fees/{feeId}/source-snapshot"); + assertThat(methodPath("container")).isEqualTo("/containers/{containerId}/fees/{feeId}/source-snapshot"); + } + + private InternalFinanceFeeSourceController controller() { + InternalFinanceFeeSourceController value = new InternalFinanceFeeSourceController(service); + ReflectionTestUtils.setField(value, "expectedToken", "finance-token"); + return value; + } + + private static String methodPath(String name) { + return java.util.Arrays.stream(InternalFinanceFeeSourceController.class.getDeclaredMethods()) + .filter(method -> method.getName().equals(name)).findFirst().orElseThrow() + .getAnnotation(org.springframework.web.bind.annotation.GetMapping.class).value()[0]; + } +} diff --git a/firstleg/src/test/java/com/baiyue/firstleg/loading/repository/PdaConvergenceSqlContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/loading/repository/PdaConvergenceSqlContractTest.java index 8bdf3a4ba..257649cee 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/loading/repository/PdaConvergenceSqlContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/loading/repository/PdaConvergenceSqlContractTest.java @@ -8,6 +8,8 @@ import org.apache.ibatis.annotations.Select; import org.springframework.core.io.ClassPathResource; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import static org.assertj.core.api.Assertions.assertThat; @@ -18,6 +20,9 @@ class PdaConvergenceSqlContractTest { .getContentAsString(StandardCharsets.UTF_8); assertThat(sql) .contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_assignment") + .contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_item_assignment") + .contains("assignment_type VARCHAR(16) NOT NULL COMMENT '分配类型:CLAIMED作业员领取、ASSIGNED管理员分配'") + .contains("CONSTRAINT fk_loading_pick_item_assignment_item") .contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_review") .contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_operation") .contains("UNIQUE KEY uk_loading_pick_operation_id") @@ -30,6 +35,11 @@ class PdaConvergenceSqlContractTest { .contains("platform:firstleg:pda:assign") .contains("platform:firstleg:pda:work") .contains("platform:firstleg:pda:review"); + + String unified = Files.readString(Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql"), + StandardCharsets.UTF_8); + assertThat(createBlock(unified, "fl_loading_pick_item_assignment")) + .isEqualTo(createBlock(sql, "fl_loading_pick_item_assignment")); } @Test @@ -51,4 +61,13 @@ class PdaConvergenceSqlContractTest { .contains("ON DUPLICATE KEY UPDATE") .contains("柜次PDA自动汇总"); } + + private static String createBlock(String sql, String table) { + String marker = "CREATE TABLE IF NOT EXISTS " + table + " ("; + int start = sql.indexOf(marker); + int end = sql.indexOf(";", start); + assertThat(start).as("missing table %s", table).isGreaterThanOrEqualTo(0); + assertThat(end).as("missing table terminator %s", table).isGreaterThan(start); + return sql.substring(start, end + 1).replace("\r\n", "\n"); + } } diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/FinanceFeeSourceMigrationContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/FinanceFeeSourceMigrationContractTest.java new file mode 100644 index 000000000..8f8813308 --- /dev/null +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/FinanceFeeSourceMigrationContractTest.java @@ -0,0 +1,22 @@ +package com.baiyue.firstleg.repository; + +import org.junit.jupiter.api.Test; +import org.springframework.core.io.ClassPathResource; + +import java.nio.charset.StandardCharsets; + +import static org.assertj.core.api.Assertions.assertThat; + +class FinanceFeeSourceMigrationContractTest { + @Test + void migrationDefinesIdempotentOutboxEnvelopeAndContainerFeeModel() throws Exception { + String sql = new ClassPathResource("db/finance-fee-source-migration.sql") + .getContentAsString(StandardCharsets.UTF_8); + + assertThat(sql).contains("information_schema.columns", "event_key", "payload_hash", + "uk_inbound_outbox_event_key", "next_retry_at", "locked_at", "locked_by", + "idx_inbound_outbox_finance_relay", "CREATE TABLE IF NOT EXISTS fl_container_fee_detail", + "settlement_included", "source_version", "RECEIVABLE", "PAYABLE", + "chk_container_fee_currency", "currency = 'CNY'", "firstleg_finance_assert_schema"); + } +} diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/Phase3FinalUnifiedSqlContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/Phase3FinalUnifiedSqlContractTest.java index 11b0224c6..95c24f03b 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/repository/Phase3FinalUnifiedSqlContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/Phase3FinalUnifiedSqlContractTest.java @@ -5,11 +5,21 @@ import org.junit.jupiter.api.Test; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import static org.assertj.core.api.Assertions.assertThat; class Phase3FinalUnifiedSqlContractTest { private static final Path UNIFIED = Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql"); + private static final Pattern CREATE_TABLE = Pattern.compile( + "(?i)CREATE\\s+TABLE\\s+IF\\s+NOT\\s+EXISTS\\s+(?:`[^`]+`\\.)?`?([a-zA-Z0-9_]+)`?\\s*\\("); + private static final Pattern COLUMN = Pattern.compile( + "(?i)^\\s*`?([a-zA-Z0-9_]+)`?\\s+(bigint|binary|bit|blob|boolean|char|date|datetime|decimal|double|enum|float|int|integer|json|longblob|longtext|mediumblob|mediumint|mediumtext|numeric|real|set|smallint|text|time|timestamp|tinyblob|tinyint|tinytext|varbinary|varchar|year)\\b"); + private static final Pattern CHINESE_COMMENT = Pattern.compile( + "(?i)\\bCOMMENT\\s*=?\\s*'(?:(?:''|\\\\'|[^'])*[\\u3400-\\u9fff](?:''|\\\\'|[^'])*)'"); @Test void unifiedEntryOwnsPhase3OutboundCustomsPortAndEvidenceHardening() throws Exception { @@ -27,6 +37,10 @@ class Phase3FinalUnifiedSqlContractTest { .contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_assignment") .contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_review") .contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_operation") + .contains("CREATE TABLE IF NOT EXISTS fl_inbound_split_detail") + .contains("trg_inbound_split_detail_before_insert") + .contains("trg_inbound_split_detail_before_update") + .contains("trg_inbound_split_detail_before_delete") .contains("CHECK (action_type IN ('CLAIM', 'ASSIGN', 'ITEM_CLAIM', 'ITEM_ASSIGN', 'REVIEW', 'BATCH'))") .contains("phase3_loading_evidence_add_column") .contains("DROP INDEX uk_loading_evidence_type") @@ -74,6 +88,86 @@ class Phase3FinalUnifiedSqlContractTest { "iam/src/main/resources/db/phase3-warehouse-roles.sql"); } + @Test + void financeFeeSourceMigrationsAreContiguousAndSynchronised() throws Exception { + String unified = normalise(Files.readString(resolveUnified())); + assertBlock(unified, "-- 来源:firstleg/src/main/resources/db/finance-fee-source-migration.sql", + "-- 来源:finance/src/main/resources/db/fee-source-event-migration.sql", + "firstleg/src/main/resources/db/finance-fee-source-migration.sql"); + assertBlock(unified, "-- 来源:finance/src/main/resources/db/fee-source-event-migration.sql", + "-- END FINANCE FEE SOURCE MODULE MIGRATIONS", + "finance/src/main/resources/db/fee-source-event-migration.sql"); + + assertThat(unified) + .contains("column_name IN ('source_type','source_no','direction','container_id','fee_item_code',") + .contains("column_name IN ('payload_hash','result_resource_id','error_code')") + .contains("column_name IN ('event_key', 'event_version', 'aggregate_sequence', 'payload_hash',") + .contains("CREATE TABLE IF NOT EXISTS fl_container_fee_detail"); + } + + @Test + void commentUpgradeIsIdempotentAndGuardsNineSchemaStructure() throws Exception { + String sql = normalise(Files.readString(resolveUnified())); + + assertThat(sql) + .contains("CREATE PROCEDURE queue_table_comment") + .contains("CREATE PROCEDURE queue_column_comment") + .contains("CREATE PROCEDURE apply_one_table_comments") + .contains("CREATE PROCEDURE apply_queued_comments") + .contains("NOT (BINARY v_current_comment=BINARY p_column_comment)") + .contains("SET v_signature_before=SHA2") + .contains("BINARY target.signature_before") + .contains("字段注释升级遇到未知数据类型") + .contains("字段注释升级遇到未知EXTRA定义") + .contains("字段注释升级遇到未知生成列结构") + .contains("MySQL DDL 会隐式提交") + .contains("不使用 GROUP_CONCAT") + .contains("DECLARE table_cursor CURSOR FOR") + .contains("CALL apply_one_table_comments(v_schema_name,v_table_name)") + .contains("CALL apply_queued_comments();") + .contains("PREPARE table_comment_batch_stmt FROM @table_comment_batch_sql") + .contains("九个业务数据库未完整创建") + .contains("存在缺失中文注释的业务表") + .contains("存在缺失中文注释的业务字段") + .doesNotContain("CREATE PROCEDURE upgrade_column_comment"); + assertThat(count(sql, "CALL queue_table_comment(")).isEqualTo(150); + assertThat(count(sql, "CALL queue_column_comment(")).isEqualTo(2456); + assertThat(sql.indexOf("CALL apply_queued_comments();", + sql.indexOf("-- END GENERATED NINE-SCHEMA COMMENT UPGRADE CALLS"))) + .isGreaterThan(sql.indexOf("-- END GENERATED NINE-SCHEMA COMMENT UPGRADE CALLS")); + } + + @Test + void unifiedCreateDefinitionsHaveChineseTableAndColumnComments() throws Exception { + String sql = normalise(Files.readString(resolveUnified())); + List issues = new ArrayList<>(); + Matcher tableMatcher = CREATE_TABLE.matcher(sql); + int tableCount = 0; + int columnCount = 0; + while (tableMatcher.find()) { + tableCount++; + String table = tableMatcher.group(1); + int open = tableMatcher.end() - 1; + int close = matchingParen(sql, open); + int semicolon = sql.indexOf(';', close); + assertThat(semicolon).as("missing semicolon for %s", table).isGreaterThan(close); + if (!CHINESE_COMMENT.matcher(sql.substring(close + 1, semicolon)).find()) { + issues.add(table + ": table comment"); + } + for (String definition : splitDefinitions(sql.substring(open + 1, close))) { + Matcher columnMatcher = COLUMN.matcher(definition); + if (!columnMatcher.find()) continue; + columnCount++; + if (!CHINESE_COMMENT.matcher(definition).find()) { + issues.add(table + "." + columnMatcher.group(1)); + } + } + } + assertThat(tableCount).isGreaterThan(100); + assertThat(columnCount).isGreaterThan(2000); + assertThat(issues).isEmpty(); + } + @Test void passIsUniqueAndOnlyTheFinalExecutableStatement() throws Exception { String sql = normalise(Files.readString(resolveUnified())); @@ -153,4 +247,51 @@ class Phase3FinalUnifiedSqlContractTest { private static int count(String value, String token) { return value.split(java.util.regex.Pattern.quote(token), -1).length - 1; } + + private static int matchingParen(String sql, int open) { + int depth = 0; + char quote = 0; + for (int index = open; index < sql.length(); index++) { + char current = sql.charAt(index); + if (quote != 0) { + if (current == '\\') index++; + else if (current == quote) { + if (index + 1 < sql.length() && sql.charAt(index + 1) == quote) index++; + else quote = 0; + } + continue; + } + if (current == '\'' || current == '"' || current == '`') quote = current; + else if (current == '(') depth++; + else if (current == ')' && --depth == 0) return index; + } + throw new AssertionError("unclosed CREATE TABLE at " + open); + } + + private static List splitDefinitions(String body) { + List definitions = new ArrayList<>(); + int start = 0; + int depth = 0; + char quote = 0; + for (int index = 0; index < body.length(); index++) { + char current = body.charAt(index); + if (quote != 0) { + if (current == '\\') index++; + else if (current == quote) { + if (index + 1 < body.length() && body.charAt(index + 1) == quote) index++; + else quote = 0; + } + continue; + } + if (current == '\'' || current == '"' || current == '`') quote = current; + else if (current == '(') depth++; + else if (current == ')') depth--; + else if (current == ',' && depth == 0) { + definitions.add(body.substring(start, index)); + start = index + 1; + } + } + definitions.add(body.substring(start)); + return definitions; + } } diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/ContainerFeeDetailServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/ContainerFeeDetailServiceTest.java new file mode 100644 index 000000000..9d6c7712c --- /dev/null +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/ContainerFeeDetailServiceTest.java @@ -0,0 +1,114 @@ +package com.baiyue.firstleg.service; + +import com.baiyue.common.core.exception.BizException; +import com.baiyue.firstleg.dto.ContainerFeeDetailDtos; +import com.baiyue.firstleg.entity.ContainerFeeDetailEntity; +import com.baiyue.firstleg.entity.FeeItemEntity; +import com.baiyue.firstleg.entity.LoadingPlanEntity; +import com.baiyue.firstleg.entity.LoadingPlanItemEntity; +import com.baiyue.firstleg.repository.ContainerFeeDetailRepository; +import com.baiyue.firstleg.repository.FeeItemRepository; +import com.baiyue.firstleg.repository.SmartLoadingRepository; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class ContainerFeeDetailServiceTest { + private final ContainerFeeDetailRepository repository = mock(ContainerFeeDetailRepository.class); + private final SmartLoadingRepository loading = mock(SmartLoadingRepository.class); + private final FeeItemRepository feeItems = mock(FeeItemRepository.class); + private final FinanceFeeSourceOutboxService outbox = mock(FinanceFeeSourceOutboxService.class); + private final ContainerFeeDetailService service = + new ContainerFeeDetailService(repository, loading, feeItems, outbox); + + @Test + void rejectsContainerThatDoesNotContainCustomerOrders() { + LoadingPlanEntity plan = new LoadingPlanEntity(); + plan.setId(41L); + when(loading.plan(41L)).thenReturn(plan); + LoadingPlanItemEntity other = new LoadingPlanItemEntity(); + other.setAccountId(20002L); + when(loading.items(41L)).thenReturn(List.of(other)); + + assertThatThrownBy(() -> service.create(10001L, 41L, 9L, request(true))) + .isInstanceOf(BizException.class).hasMessageContaining("不属于该客户"); + verify(repository, never()).insert(any()); + } + + @Test + void approvedSettlementFeePublishesContainerSource() { + ownedPlan(); + ContainerFeeDetailEntity approved = new ContainerFeeDetailEntity(); + approved.setId(51L); + approved.setTenantId(10001L); + approved.setLoadingPlanId(41L); + approved.setSourceVersion(1); + approved.setSettlementIncluded(true); + when(repository.find(10001L, 41L, 51L)).thenReturn(approved); + when(repository.review(10001L, 41L, 51L, "APPROVED", 9L, "admin", null)).thenReturn(1); + + service.approve(10001L, 41L, 51L, 9L, "admin"); + + verify(outbox).publishContainer(eq(FinanceFeeSourceOutboxService.PUBLISHED), + eq(10001L), eq(41L), eq(51L), eq(1), any(LocalDateTime.class)); + } + + @Test + void adjustmentIncrementsSourceVersionAndReturnsToReview() { + ownedPlan(); + FeeItemEntity item = item(); + when(feeItems.findItem(11L)).thenReturn(item); + ContainerFeeDetailEntity current = new ContainerFeeDetailEntity(); + current.setId(51L); + current.setTenantId(10001L); + current.setLoadingPlanId(41L); + current.setSourceVersion(1); + when(repository.find(10001L, 41L, 51L)).thenReturn(current); + when(repository.adjust(any(), eq(1))).thenReturn(1); + + service.adjust(10001L, 41L, 51L, 1, 9L, request(true)); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ContainerFeeDetailEntity.class); + verify(repository).adjust(captor.capture(), eq(1)); + assertThat(captor.getValue().getSourceVersion()).isEqualTo(2); + assertThat(captor.getValue().getReviewStatus()).isEqualTo("PENDING"); + } + + private void ownedPlan() { + LoadingPlanEntity plan = new LoadingPlanEntity(); + plan.setId(41L); + when(loading.plan(41L)).thenReturn(plan); + LoadingPlanItemEntity item = new LoadingPlanItemEntity(); + item.setAccountId(10001L); + when(loading.items(41L)).thenReturn(List.of(item)); + } + + private static FeeItemEntity item() { + FeeItemEntity value = new FeeItemEntity(); + value.setId(11L); + value.setFeeCode("TRUCKING"); + value.setFeeTypeName("拖车费"); + value.setWarehousePayable(true); + value.setStatus("ACTIVE"); + return value; + } + + private static ContainerFeeDetailDtos.SaveRequest request(boolean settlementIncluded) { + return new ContainerFeeDetailDtos.SaveRequest(11L, "PAYABLE", + LocalDate.of(2026, 8, 11), new BigDecimal("300.00"), "CNY", + settlementIncluded, "整柜拖车费"); + } +} diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/FbaPricingServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/FbaPricingServiceTest.java index 6585f5a44..ade9ec959 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/FbaPricingServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/FbaPricingServiceTest.java @@ -42,6 +42,7 @@ import static org.mockito.Mockito.when; @DisplayName("FBA费用状态机") @SpringJUnitConfig(classes = {FbaPricingService.class, FbaPricingServiceTest.TestConfig.class}) class FbaPricingServiceTest { + @MockBean private FinanceFeeSourceOutboxService financeOutbox; private static final long TENANT = 10001L; private static final long OPERATOR = 20001L; private static final long INBOUND = 30001L; @@ -224,7 +225,9 @@ class FbaPricingServiceTest { assertThat(first.nextProcessStatus()).isEqualTo("WAIT_PALLET"); assertThat(repeated.nextProcessStatus()).isEqualTo("WAIT_PALLET"); - verify(repository, times(1)).insertOutbox(any()); + verify(financeOutbox, times(1)).publishInbound( + eq(FinanceFeeSourceOutboxService.PUBLISHED), eq(TENANT), eq(INBOUND), + eq(501L), eq(1), eq(NOW)); verify(repository, times(1)).sendFee(TENANT, 501L, OPERATOR, NOW); verify(feeDetails, times(2)).publishFreight(TENANT, INBOUND, 501L, OPERATOR, null, NOW); } @@ -250,7 +253,8 @@ class FbaPricingServiceTest { assertThat(response.nextProcessStatus()).isEqualTo("INBOUNDED"); verify(repository).markInboundFeeSent(TENANT, INBOUND, "INBOUNDED"); - verify(repository).insertOutbox(any()); + verify(financeOutbox).publishInbound(eq(FinanceFeeSourceOutboxService.PUBLISHED), + eq(TENANT), eq(INBOUND), eq(501L), eq(1), eq(NOW)); } @Test @@ -300,7 +304,8 @@ class FbaPricingServiceTest { assertThat(response.nextProcessStatus()).isEqualTo("INBOUNDED"); assertThat(inbound.getProcessStatus()).isEqualTo("INBOUNDED"); verify(repository).supersedeSent(TENANT, 501L); - verify(repository).insertOutbox(any()); + verify(financeOutbox).publishInbound(eq(FinanceFeeSourceOutboxService.ADJUSTED), + eq(TENANT), eq(INBOUND), eq(502L), eq(2), eq(NOW)); } private void readyRepository() { diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/FeeManagementServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/FeeManagementServiceTest.java index a6ab27163..37ab8e57f 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/FeeManagementServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/FeeManagementServiceTest.java @@ -27,6 +27,7 @@ import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.never; @@ -40,6 +41,8 @@ class FeeManagementServiceTest { private FeeItemRepository repository; @MockBean private FbaPricingRepository pricingRepository; + @MockBean + private FinanceFeeSourceOutboxService financeOutbox; @Resource private FeeItemService feeItems; @Resource @@ -268,6 +271,54 @@ class FeeManagementServiceTest { verify(repository).review(10001L, 21L, 31L, "APPROVED", 9L, "admin", null); } + @Test + @DisplayName("审核通过且参与结算的人工费用写入Finance事件") + void publishesApprovedSettlementFee() { + InboundFeeDetailEntity approved = new InboundFeeDetailEntity(); + approved.setId(31L); + approved.setTenantId(10001L); + approved.setInboundId(21L); + approved.setFeeItemId(11L); + approved.setSourceType("MANUAL"); + approved.setSourceFeeVersion(1); + approved.setSettlementIncluded(true); + when(repository.findDetail(10001L, 21L, 31L)).thenReturn(approved); + when(repository.review(10001L, 21L, 31L, "APPROVED", 9L, "admin", null)).thenReturn(1); + + details.approve(10001L, 21L, 31L, 9L, "admin"); + + verify(financeOutbox).publishInbound(eq(FinanceFeeSourceOutboxService.PUBLISHED), + eq(10001L), eq(21L), eq(31L), eq(1), any(LocalDateTime.class)); + } + + @Test + @DisplayName("作废已入池人工费用引用当前版本并发布VOIDED") + void voidsPublishedSettlementFee() { + InboundFeeDetailEntity current = new InboundFeeDetailEntity(); + current.setId(31L); + current.setTenantId(10001L); + current.setInboundId(21L); + current.setFeeItemId(11L); + current.setSourceType("MANUAL"); + current.setSourceFeeVersion(1); + current.setSettlementIncluded(true); + InboundFeeDetailEntity voided = new InboundFeeDetailEntity(); + voided.setId(31L); + voided.setTenantId(10001L); + voided.setInboundId(21L); + voided.setFeeItemId(11L); + voided.setSourceType("MANUAL"); + voided.setSourceFeeVersion(1); + voided.setSettlementIncluded(true); + when(repository.findDetail(10001L, 21L, 31L)).thenReturn(current, voided); + when(repository.voidApproved(10001L, 21L, 31L, 1, 9L)).thenReturn(1); + + details.voidApproved(10001L, 21L, 31L, 1, 9L); + + verify(financeOutbox).publishInbound(eq(FinanceFeeSourceOutboxService.VOIDED), + eq(10001L), eq(21L), eq(31L), eq(1), any(LocalDateTime.class)); + } + private static FeeItemDtos.SaveRequest itemRequest() { return new FeeItemDtos.SaveRequest("仓储成本", "INTERNATIONAL", "仓库租赁费", false, false, true, true, true, "应付仓库租赁费"); diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/FinanceFeeSourceOutboxRelayTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/FinanceFeeSourceOutboxRelayTest.java new file mode 100644 index 000000000..bd857fb73 --- /dev/null +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/FinanceFeeSourceOutboxRelayTest.java @@ -0,0 +1,135 @@ +package com.baiyue.firstleg.service; + +import com.baiyue.common.core.api.ApiResponse; +import com.baiyue.firstleg.client.FinanceFeeSourceEventClient; +import com.baiyue.firstleg.dto.FinanceFeeSourceEvent; +import com.baiyue.firstleg.entity.InboundOutboxEntity; +import com.baiyue.firstleg.mapper.InboundOutboxMapper; +import com.baiyue.firstleg.repository.FinanceFeeSourceOutboxRepository; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.test.util.ReflectionTestUtils; + +import java.time.Clock; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class FinanceFeeSourceOutboxRelayTest { + private static final LocalDateTime NOW = LocalDateTime.of(2026, 8, 11, 15, 0); + private final ObjectMapper json = new ObjectMapper().registerModule(new JavaTimeModule()); + private final FinanceFeeSourceOutboxRepository repository = mock(FinanceFeeSourceOutboxRepository.class); + private final FinanceFeeSourceEventClient client = mock(FinanceFeeSourceEventClient.class); + private final FinanceFeeSourceOutboxRelay relay = relay(); + + @Test + void claimsThenPublishesOnlyAfterFinanceAcknowledgesSameEvent() throws Exception { + InboundOutboxEntity row = event(); + ready(row); + FinanceFeeSourceEvent envelope = json.readValue(row.getPayloadJson(), FinanceFeeSourceEvent.class); + when(client.receive("finance-token", envelope.idempotencyKey(), envelope)).thenReturn( + ApiResponse.success(new FinanceFeeSourceEventClient.Acknowledgement( + 901L))); + when(repository.published(row.getId(), "worker-1", NOW)).thenReturn(true); + + relay.relay(); + + verify(repository).recoverStale(NOW.minusMinutes(5), NOW.plusSeconds(10), 12); + verify(repository).claim(row.getId(), "worker-1", NOW); + verify(client).receive("finance-token", envelope.idempotencyKey(), envelope); + verify(repository).published(row.getId(), "worker-1", NOW); + verify(repository, never()).retry(any(Long.class), any(), any(), any(), eq(false)); + } + + @Test + void concurrentWorkerThatLosesClaimDoesNotDeliver() { + InboundOutboxEntity row = event(); + when(repository.pending(50, NOW)).thenReturn(List.of(row)); + when(repository.claim(row.getId(), "worker-1", NOW)).thenReturn(false); + + relay.relay(); + + verify(client, never()).receive(any(), any(), any()); + verify(repository, never()).published(any(Long.class), any(), any()); + } + + @Test + void transientFailureReturnsClaimToPendingWithBackoff() throws Exception { + InboundOutboxEntity row = event(); + ready(row); + FinanceFeeSourceEvent envelope = json.readValue(row.getPayloadJson(), FinanceFeeSourceEvent.class); + when(client.receive("finance-token", envelope.idempotencyKey(), envelope)) + .thenThrow(new IllegalStateException("finance unavailable")); + + relay.relay(); + + verify(repository).retry(row.getId(), "worker-1", NOW.plusSeconds(10), + "finance unavailable", false); + verify(repository, never()).published(any(Long.class), any(), any()); + } + + @Test + void invalidEventHashFailsPermanentlyWithoutCallingFinance() { + InboundOutboxEntity row = event(); + row.setPayloadHash("0".repeat(64)); + ready(row); + + relay.relay(); + + verify(repository).retry(eq(row.getId()), eq("worker-1"), eq(NOW.plusSeconds(10)), + any(), eq(true)); + verify(client, never()).receive(any(), any(), any()); + } + + @Test + void financeIdempotencyConflictFailsPermanently() throws Exception { + InboundOutboxEntity row = event(); + ready(row); + FinanceFeeSourceEvent envelope = json.readValue(row.getPayloadJson(), FinanceFeeSourceEvent.class); + when(client.receive("finance-token", envelope.idempotencyKey(), envelope)).thenReturn( + new ApiResponse<>("FIN-EVT-002", "同一事件ID对应不同请求内容", null, "r", "t", Instant.now())); + + relay.relay(); + + verify(repository).retry(eq(row.getId()), eq("worker-1"), eq(NOW.plusSeconds(10)), + any(), eq(true)); + verify(repository, never()).published(any(Long.class), any(), any()); + } + + private void ready(InboundOutboxEntity row) { + when(repository.pending(50, NOW)).thenReturn(List.of(row)); + when(repository.claim(row.getId(), "worker-1", NOW)).thenReturn(true); + when(repository.findClaimed(row.getId(), "worker-1")).thenReturn(row); + } + + private InboundOutboxEntity event() { + InboundOutboxMapper mapper = mock(InboundOutboxMapper.class); + FinanceFeeSourceOutboxService producer = new FinanceFeeSourceOutboxService(mapper, json); + producer.publishInbound(FinanceFeeSourceOutboxService.PUBLISHED, + 10001L, 21L, 31L, 1, NOW); + ArgumentCaptor captor = ArgumentCaptor.forClass(InboundOutboxEntity.class); + verify(mapper).insert(captor.capture()); + InboundOutboxEntity row = captor.getValue(); + row.setId(71L); + return row; + } + + private FinanceFeeSourceOutboxRelay relay() { + Clock clock = Clock.fixed(Instant.parse("2026-08-11T07:00:00Z"), ZoneId.of("Asia/Shanghai")); + FinanceFeeSourceOutboxRelay value = + new FinanceFeeSourceOutboxRelay(repository, client, json, clock, "worker-1"); + ReflectionTestUtils.setField(value, "internalToken", "finance-token"); + return value; + } +} diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/FinanceFeeSourceOutboxServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/FinanceFeeSourceOutboxServiceTest.java new file mode 100644 index 000000000..278c9a2a3 --- /dev/null +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/FinanceFeeSourceOutboxServiceTest.java @@ -0,0 +1,52 @@ +package com.baiyue.firstleg.service; + +import com.baiyue.firstleg.entity.InboundOutboxEntity; +import com.baiyue.firstleg.mapper.InboundOutboxMapper; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.dao.DuplicateKeyException; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +class FinanceFeeSourceOutboxServiceTest { + private final InboundOutboxMapper mapper = mock(InboundOutboxMapper.class); + private final ObjectMapper objectMapper = new ObjectMapper().registerModule(new JavaTimeModule()); + private final FinanceFeeSourceOutboxService service = + new FinanceFeeSourceOutboxService(mapper, objectMapper); + + @Test + void writesContractEnvelopeWithShortPayloadAndStableHash() { + LocalDateTime occurredAt = LocalDateTime.of(2026, 8, 11, 12, 0); + + service.publishInbound(FinanceFeeSourceOutboxService.PUBLISHED, + 10001L, 21L, 31L, 1, occurredAt); + + ArgumentCaptor captor = ArgumentCaptor.forClass(InboundOutboxEntity.class); + verify(mapper).insert(captor.capture()); + InboundOutboxEntity event = captor.getValue(); + assertThat(event.getEventType()).isEqualTo("FIRSTLEG_FEE_PUBLISHED"); + assertThat(event.getEventVersion()).isEqualTo(1); + assertThat(event.getAggregateSequence()).isEqualTo(1); + assertThat(event.getPayloadHash()).matches("[0-9a-f]{64}"); + assertThat(event.getPayloadJson()).contains("\"producer\":\"firstleg-service\"", + "\"inboundId\":\"21\"", "\"feeId\":\"31\"", "\"feeVersion\":1") + .doesNotContain("amount", "currency"); + } + + @Test + void treatsDuplicateEventKeyAsSuccessfulReplay() { + doThrow(new DuplicateKeyException("duplicate event_key")).when(mapper) + .insert((InboundOutboxEntity) org.mockito.ArgumentMatchers.any()); + + assertThatCode(() -> service.publishContainer(FinanceFeeSourceOutboxService.VOIDED, + 10001L, 41L, 51L, 2, LocalDateTime.now())).doesNotThrowAnyException(); + } +} diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/FinanceFeeSourceSnapshotServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/FinanceFeeSourceSnapshotServiceTest.java new file mode 100644 index 000000000..52b431f68 --- /dev/null +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/FinanceFeeSourceSnapshotServiceTest.java @@ -0,0 +1,71 @@ +package com.baiyue.firstleg.service; + +import com.baiyue.common.core.exception.BizException; +import com.baiyue.firstleg.dto.FinanceFeeSourceSnapshot; +import com.baiyue.firstleg.entity.FeeItemEntity; +import com.baiyue.firstleg.entity.InboundFeeDetailEntity; +import com.baiyue.firstleg.entity.InboundOrderEntity; +import com.baiyue.firstleg.repository.ContainerFeeDetailRepository; +import com.baiyue.firstleg.repository.FeeItemRepository; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class FinanceFeeSourceSnapshotServiceTest { + private final FeeItemRepository inboundFees = mock(FeeItemRepository.class); + private final ContainerFeeDetailRepository containerFees = mock(ContainerFeeDetailRepository.class); + private final FinanceFeeSourceSnapshotService service = + new FinanceFeeSourceSnapshotService(inboundFees, containerFees); + + @Test + void returnsReviewedSettlementSnapshotWithStableSourceHash() { + InboundFeeDetailEntity detail = new InboundFeeDetailEntity(); + detail.setId(31L); + detail.setTenantId(10001L); + detail.setInboundId(21L); + detail.setFeeItemId(11L); + detail.setSourceType("MANUAL"); + detail.setSourceFeeVersion(2); + detail.setDirection("RECEIVABLE"); + detail.setAmount(new BigDecimal("100.00")); + detail.setCurrency("CNY"); + detail.setReviewStatus("APPROVED"); + detail.setPublicationStatus("PUBLISHED"); + detail.setSettlementIncluded(true); + when(inboundFees.findDetail(21L, 31L)).thenReturn(detail); + InboundOrderEntity inbound = new InboundOrderEntity(); + inbound.setId(21L); + inbound.setOrderId(81L); + inbound.setInboundCode("RK-21"); + inbound.setCustomerName("客户A"); + when(inboundFees.findInbound(10001L, 21L)).thenReturn(inbound); + FeeItemEntity item = new FeeItemEntity(); + item.setId(11L); + item.setFeeCode("STORAGE"); + when(inboundFees.findItem(11L)).thenReturn(item); + + FinanceFeeSourceSnapshot snapshot = service.inbound(21L, 31L); + + assertThat(snapshot.customerId()).isEqualTo(10001L); + assertThat(snapshot.orderId()).isEqualTo(81L); + assertThat(snapshot.feeVersion()).isEqualTo(2); + assertThat(snapshot.sourceHash()).matches("[0-9a-f]{64}"); + detail.setPublicationStatus("VOIDED"); + assertThat(service.inbound(21L, 31L).sourceHash()).isEqualTo(snapshot.sourceHash()); + } + + @Test + void rejectsFeeThatDoesNotBelongToInboundPath() { + InboundFeeDetailEntity detail = new InboundFeeDetailEntity(); + detail.setInboundId(22L); + when(inboundFees.findDetail(21L, 31L)).thenReturn(detail); + + assertThatThrownBy(() -> service.inbound(21L, 31L)) + .isInstanceOf(BizException.class).hasMessageContaining("不存在"); + } +} diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/InboundCompletionOrchestratorTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/InboundCompletionOrchestratorTest.java index 0f2770e35..04bb68275 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/InboundCompletionOrchestratorTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/InboundCompletionOrchestratorTest.java @@ -25,6 +25,7 @@ import static org.mockito.Mockito.when; @DisplayName("发送费用并入库状态编排") @SpringJUnitConfig(InboundCompletionOrchestrator.class) class InboundCompletionOrchestratorTest { + @MockBean private FinanceFeeSourceOutboxService financeOutbox; @MockBean private InboundWorkflowRepository repository; @MockBean @@ -78,6 +79,11 @@ class InboundCompletionOrchestratorTest { fixture.order.setProcessStatus("INBOUNDING"); when(fixture.repository.completeInbound(10001L, 10L, 9L, 5)).thenReturn(1); when(fixture.repository.markFeeSent(10001L, 10L, 30L, 9L)).thenReturn(1); + com.baiyue.firstleg.entity.InboundFeeDetailEntity published = + new com.baiyue.firstleg.entity.InboundFeeDetailEntity(); + published.setSourceFeeVersion(1); + when(feeDetails.publishFreight(eq(10001L), eq(10L), eq(30L), eq(9L), eq("操作员"), + org.mockito.ArgumentMatchers.any(java.time.LocalDateTime.class))).thenReturn(published); fixture.service.completeSendFeeAndInbound( 10001L, 9L, "操作员", 10L, 30L, 5, "complete-1"); @@ -86,7 +92,9 @@ class InboundCompletionOrchestratorTest { verify(fixture.repository).markFeeSent(10001L, 10L, 30L, 9L); verify(feeDetails).publishFreight(eq(10001L), eq(10L), eq(30L), eq(9L), eq("操作员"), org.mockito.ArgumentMatchers.any(java.time.LocalDateTime.class)); - verify(fixture.repository).insertOutbox(any()); + verify(financeOutbox).publishInbound(eq(FinanceFeeSourceOutboxService.PUBLISHED), + eq(10001L), eq(10L), eq(30L), eq(1), + org.mockito.ArgumentMatchers.any(java.time.LocalDateTime.class)); } @Test @@ -102,7 +110,9 @@ class InboundCompletionOrchestratorTest { .extracting(error -> ((BizException) error).getResultCode()) .isEqualTo(ResultCode.CONFLICT); verify(fixture.repository, never()).markFeeSent(anyLong(), anyLong(), anyLong(), anyLong()); - verify(fixture.repository, never()).insertOutbox(any()); + verify(financeOutbox, never()).publishInbound(any(), anyLong(), anyLong(), anyLong(), + org.mockito.ArgumentMatchers.anyInt(), + org.mockito.ArgumentMatchers.any(java.time.LocalDateTime.class)); } @Test diff --git a/iam/src/main/resources/db/audit-outbox.sql b/iam/src/main/resources/db/audit-outbox.sql index 8442b5db7..effbafffb 100644 --- a/iam/src/main/resources/db/audit-outbox.sql +++ b/iam/src/main/resources/db/audit-outbox.sql @@ -2,14 +2,14 @@ USE baiyue_iam; -- 可靠审计本地 Outbox;与 IAM 业务表位于同一数据库。 CREATE TABLE IF NOT EXISTS sys_audit_outbox ( - event_id VARCHAR(64) NOT NULL, event_type VARCHAR(64) NOT NULL, resource_type VARCHAR(64) NOT NULL, - resource_id VARCHAR(128) NULL, service_name VARCHAR(64) NULL, payload_json JSON NOT NULL, - status VARCHAR(16) NOT NULL DEFAULT 'PENDING', attempt_count INT NOT NULL DEFAULT 0, - compensation_count INT NOT NULL DEFAULT 0, - next_retry_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), lease_owner VARCHAR(128) NULL, - lease_until DATETIME(3) NULL, last_error VARCHAR(1000) NULL, dead_at DATETIME(3) NULL, - sent_at DATETIME(3) NULL, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + event_id VARCHAR(64) NOT NULL COMMENT '审计事件全局唯一ID', event_type VARCHAR(64) NOT NULL COMMENT '审计事件类型', resource_type VARCHAR(64) NOT NULL COMMENT '被审计资源类型', + resource_id VARCHAR(128) NULL COMMENT '被审计资源ID', service_name VARCHAR(64) NULL COMMENT '来源服务名称', payload_json JSON NOT NULL COMMENT '脱敏后的审计事件内容', + status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT '发送状态:PENDING待发送、SENDING发送中、SENT已发送、DEAD进入死信', attempt_count INT NOT NULL DEFAULT 0 COMMENT '当前重试周期发送次数', + compensation_count INT NOT NULL DEFAULT 0 COMMENT '自动补偿发送次数', + next_retry_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '下一次重试时间', lease_owner VARCHAR(128) NULL COMMENT '当前租约持有者', + lease_until DATETIME(3) NULL COMMENT '发送租约截止时间', last_error VARCHAR(1000) NULL COMMENT '最近一次发送错误信息', dead_at DATETIME(3) NULL COMMENT '进入死信时间', + sent_at DATETIME(3) NULL COMMENT '成功发送时间', 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 (event_id), KEY idx_audit_outbox_publish (status, next_retry_at, lease_until, created_at), KEY idx_audit_outbox_retention (status, sent_at) ) ENGINE=InnoDB COMMENT='可靠审计事件本地发送箱'; diff --git a/iam/src/main/resources/db/iam.sql b/iam/src/main/resources/db/iam.sql index 5ef63f26f..2f0d3b823 100644 --- a/iam/src/main/resources/db/iam.sql +++ b/iam/src/main/resources/db/iam.sql @@ -6,11 +6,11 @@ CREATE DATABASE IF NOT EXISTS baiyue_iam USE baiyue_iam; CREATE TABLE IF NOT EXISTS iam_tenant ( - id BIGINT NOT NULL COMMENT '租户ID(雪花ID)', - tenant_code VARCHAR(64) NOT NULL COMMENT '全局唯一租户编码', - tenant_name VARCHAR(128) NOT NULL COMMENT '租户名称', - tenant_type VARCHAR(20) NOT NULL DEFAULT 'MERCHANT' COMMENT '租户类型', - status VARCHAR(20) NOT NULL DEFAULT 'PROFILE_REQUIRED' COMMENT '租户状态:PROFILE_REQUIRED、PENDING、ACTIVE、REJECTED、DISABLED', + id BIGINT NOT NULL COMMENT '客户主档ID(雪花ID);0保留为平台技术作用域', + tenant_code VARCHAR(64) NOT NULL COMMENT '全局唯一客户编码', + tenant_name VARCHAR(128) NOT NULL COMMENT '客户企业名称', + tenant_type VARCHAR(20) NOT NULL DEFAULT 'MERCHANT' COMMENT '主体类型:MERCHANT企业客户', + status VARCHAR(20) NOT NULL DEFAULT 'PROFILE_REQUIRED' COMMENT '客户状态:PROFILE_REQUIRED待完善资料、PENDING待审核、ACTIVE正常、REJECTED已驳回、DISABLED已停用', legal_name VARCHAR(128) NULL COMMENT '企业法定名称', legal_representative VARCHAR(64) NULL COMMENT '法定代表人', unified_social_credit_code VARCHAR(32) NULL COMMENT '统一社会信用代码', @@ -40,13 +40,13 @@ CREATE TABLE IF NOT EXISTS iam_tenant ( UNIQUE KEY uk_iam_tenant_code (tenant_code), UNIQUE KEY uk_iam_tenant_credit (unified_social_credit_code), KEY idx_iam_tenant_status (status, created_at) -) ENGINE=InnoDB COMMENT='企业租户主表:作为SaaS数据隔离边界,保存商户企业资料、审核结果和启停状态'; +) ENGINE=InnoDB COMMENT='客户企业主档表:保存企业资料、审核结果和启停状态;ID同时作为客户数据隔离键'; CREATE TABLE IF NOT EXISTS iam_tenant_review ( id BIGINT NOT NULL COMMENT '审核记录ID', - tenant_id BIGINT NOT NULL COMMENT '被审核租户ID', - review_type VARCHAR(20) NOT NULL DEFAULT 'REGISTER' COMMENT '审核类型', - status VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT '审核状态', + tenant_id BIGINT NOT NULL COMMENT '被审核客户ID和数据隔离键', + review_type VARCHAR(20) NOT NULL DEFAULT 'REGISTER' COMMENT '审核类型:REGISTER注册审核等', + status VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT '审核状态:PENDING待审核、APPROVED已通过、REJECTED已驳回', reviewer_id BIGINT NULL COMMENT '审核人ID', review_comment VARCHAR(500) NULL COMMENT '审核意见', reviewed_at DATETIME(3) NULL COMMENT '审核完成时间', @@ -54,51 +54,51 @@ CREATE TABLE IF NOT EXISTS iam_tenant_review ( PRIMARY KEY (id), KEY idx_review_queue (status, created_at), KEY idx_review_tenant (tenant_id) -) ENGINE=InnoDB COMMENT='租户审核记录表:保存企业注册、资料变更等审核过程和审核意见'; +) ENGINE=InnoDB COMMENT='客户审核记录表:保存企业注册、资料变更等审核过程和审核意见'; CREATE TABLE IF NOT EXISTS iam_tenant_address ( - id BIGINT NOT NULL COMMENT '租户地址ID', - tenant_id BIGINT NOT NULL COMMENT '所属租户ID', - address_type VARCHAR(20) NOT NULL COMMENT '地址类型', + id BIGINT NOT NULL COMMENT '客户地址ID', + tenant_id BIGINT NOT NULL COMMENT '客户ID和数据隔离键', + address_type VARCHAR(20) NOT NULL COMMENT '地址类型:注册地址、办公地址等', country_code CHAR(2) NOT NULL DEFAULT 'CN' COMMENT '国家二字码', province VARCHAR(64) NULL COMMENT '省/州', city VARCHAR(64) NULL COMMENT '城市', district VARCHAR(64) NULL COMMENT '区县', detail VARCHAR(255) NOT NULL COMMENT '详细地址', postal_code VARCHAR(16) NULL COMMENT '邮政编码', - is_default TINYINT NOT NULL DEFAULT 0 COMMENT '是否默认地址', + is_default TINYINT NOT NULL DEFAULT 0 COMMENT '是否默认地址:0否、1是', 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 '更新时间', deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除标记', PRIMARY KEY (id), KEY idx_address_tenant (tenant_id, address_type) -) ENGINE=InnoDB COMMENT='租户地址表:保存企业注册地址、办公地址等租户级资料'; +) ENGINE=InnoDB COMMENT='客户地址表:保存企业注册地址、办公地址等客户资料'; CREATE TABLE IF NOT EXISTS iam_tenant_qualification ( id BIGINT NOT NULL COMMENT '资质记录ID', - tenant_id BIGINT NOT NULL COMMENT '所属租户ID', - qualification_type VARCHAR(32) NOT NULL COMMENT '资质类型', + tenant_id BIGINT NOT NULL COMMENT '客户ID和数据隔离键', + qualification_type VARCHAR(32) NOT NULL COMMENT '客户资质类型', certificate_no VARCHAR(128) NULL COMMENT '证书编号', document_url VARCHAR(500) NULL COMMENT '资质文件存储地址', valid_from DATE NULL COMMENT '有效期开始日期', valid_to DATE NULL COMMENT '有效期结束日期', - status VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT '资质审核状态', + status VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT '资质审核状态:PENDING待审核、APPROVED已通过、REJECTED已驳回', review_comment VARCHAR(500) NULL COMMENT '审核意见', 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 '更新时间', deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除标记', PRIMARY KEY (id), KEY idx_qualification_tenant (tenant_id, status) -) ENGINE=InnoDB COMMENT='租户资质表:保存企业证照、有效期、文件地址和审核状态'; +) ENGINE=InnoDB COMMENT='客户资质表:保存企业证照、有效期、文件地址和审核状态'; CREATE TABLE IF NOT EXISTS iam_user ( id BIGINT NOT NULL COMMENT 'IAM用户资料ID', auth_user_id BIGINT NOT NULL COMMENT '认证服务账号逻辑ID', - user_type VARCHAR(20) NOT NULL DEFAULT 'ENTERPRISE' COMMENT '用户类型', + user_type VARCHAR(20) NOT NULL DEFAULT 'ENTERPRISE' COMMENT '用户类型:ENTERPRISE企业用户、PLATFORM平台用户', display_name VARCHAR(64) NOT NULL COMMENT '用户展示名称', phone VARCHAR(32) NULL COMMENT '手机号', email VARCHAR(128) NULL COMMENT '邮箱', - status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '用户资料状态', + status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '用户资料状态:ACTIVE正常、DISABLED停用', 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 '更新时间', deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除标记', @@ -109,14 +109,14 @@ CREATE TABLE IF NOT EXISTS iam_user ( ) ENGINE=InnoDB COMMENT='IAM用户资料表:连接统一认证账号与业务身份资料,不保存密码'; CREATE TABLE IF NOT EXISTS iam_membership ( - id BIGINT NOT NULL COMMENT '租户成员关系ID', - tenant_id BIGINT NOT NULL COMMENT '所属租户ID', + id BIGINT NOT NULL COMMENT '客户成员关系ID', + tenant_id BIGINT NOT NULL COMMENT '客户ID和数据隔离键', user_id BIGINT NOT NULL COMMENT 'IAM用户资料ID', - member_type VARCHAR(20) NOT NULL DEFAULT 'EMPLOYEE' COMMENT '成员类型', - member_no VARCHAR(64) NULL COMMENT '租户内部成员编号', - status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '成员状态', - joined_at DATETIME(3) NULL COMMENT '加入租户时间', - left_at DATETIME(3) NULL COMMENT '离开租户时间', + member_type VARCHAR(20) NOT NULL DEFAULT 'EMPLOYEE' COMMENT '成员类型:EMPLOYEE企业员工等', + member_no VARCHAR(64) NULL COMMENT '客户企业内部成员编号', + status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '成员状态:ACTIVE在职、DISABLED停用、LEFT已离开', + joined_at DATETIME(3) NULL COMMENT '加入客户企业时间', + left_at DATETIME(3) NULL COMMENT '离开客户企业时间', 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 '更新时间', deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除标记', @@ -126,16 +126,16 @@ CREATE TABLE IF NOT EXISTS iam_membership ( UNIQUE KEY uk_membership_tenant_no (tenant_id, member_no), KEY idx_membership_user (user_id, status), KEY idx_membership_tenant (tenant_id, status) -) ENGINE=InnoDB COMMENT='租户成员关系表:表示一个用户在某个企业租户中的成员身份,是分配商户端角色的主体'; +) ENGINE=InnoDB COMMENT='客户成员关系表:表示用户在客户企业中的成员身份,是分配企业客户端角色的主体'; CREATE TABLE IF NOT EXISTS iam_role ( id BIGINT NOT NULL COMMENT '角色ID', - tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '角色所属租户;0表示平台预置角色', - app_code VARCHAR(20) NOT NULL COMMENT '应用端:MERCHANT或PLATFORM', + tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '角色作用域:0为平台预置模板,正数为客户ID和数据隔离键', + app_code VARCHAR(20) NOT NULL COMMENT '应用端:MERCHANT企业客户端、PLATFORM平台后台', role_code VARCHAR(64) NOT NULL COMMENT '作用域内唯一角色编码', role_name VARCHAR(128) NOT NULL COMMENT '角色名称', role_type VARCHAR(20) NOT NULL DEFAULT 'CUSTOM' COMMENT '角色类型:SYSTEM或CUSTOM', - status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '角色状态', + status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '角色状态:ACTIVE启用、DISABLED停用', description VARCHAR(255) NULL COMMENT '角色用途说明', created_by BIGINT NULL COMMENT '创建人ID', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', @@ -146,21 +146,21 @@ CREATE TABLE IF NOT EXISTS iam_role ( PRIMARY KEY (id), UNIQUE KEY uk_role_scope_code (tenant_id, app_code, role_code), KEY idx_role_tenant (tenant_id, app_code, status) -) ENGINE=InnoDB COMMENT='角色表:按租户和应用端隔离商户端、平台后台两套角色体系'; +) ENGINE=InnoDB COMMENT='角色表:按客户或平台作用域及应用端隔离企业客户端、平台后台两套角色体系'; CREATE TABLE IF NOT EXISTS iam_membership_role ( - membership_id BIGINT NOT NULL COMMENT '租户成员关系ID', + membership_id BIGINT NOT NULL COMMENT '客户成员关系ID', role_id BIGINT NOT NULL COMMENT '角色ID', - tenant_id BIGINT NOT NULL COMMENT '冗余租户ID,用于租户隔离校验和查询', + tenant_id BIGINT NOT NULL COMMENT '冗余客户ID和数据隔离键,用于授权归属校验和查询', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '授权时间', PRIMARY KEY (membership_id, role_id), KEY idx_mr_tenant (tenant_id) -) ENGINE=InnoDB COMMENT='成员角色关联表:将租户成员分配到对应应用端的一个或多个角色'; +) ENGINE=InnoDB COMMENT='成员角色关联表:将客户成员分配到对应应用端的一个或多个角色'; CREATE TABLE IF NOT EXISTS iam_permission ( id BIGINT NOT NULL COMMENT '权限资源ID', - tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '权限所属租户;0表示平台预置权限', - app_code VARCHAR(20) NOT NULL COMMENT '所属应用端:MERCHANT或PLATFORM', + tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '权限作用域:0为平台预置权限,正数为客户ID和数据隔离键', + app_code VARCHAR(20) NOT NULL COMMENT '所属应用端:MERCHANT企业客户端、PLATFORM平台后台', permission_type VARCHAR(20) NOT NULL COMMENT '权限类型:MENU、PAGE、BUTTON或API', parent_id BIGINT NOT NULL DEFAULT 0 COMMENT '父权限ID,用于构造菜单和页面树', permission_code VARCHAR(128) NOT NULL COMMENT '鉴权使用的唯一权限码', @@ -168,8 +168,8 @@ CREATE TABLE IF NOT EXISTS iam_permission ( route_path VARCHAR(255) NULL COMMENT '前端路由或API路径', http_method VARCHAR(16) NULL COMMENT 'API权限对应的HTTP方法', sort_no INT NOT NULL DEFAULT 0 COMMENT '同级显示顺序', - status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '权限状态', - built_in TINYINT NOT NULL DEFAULT 0 COMMENT '是否为系统预置权限;预置权限不可通过管理接口修改', + status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '权限状态:ACTIVE启用、DISABLED停用', + built_in TINYINT NOT NULL DEFAULT 0 COMMENT '是否为系统预置权限:0否、1是;预置权限不可通过管理接口修改', 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 '更新时间', deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除标记', @@ -182,7 +182,7 @@ CREATE TABLE IF NOT EXISTS iam_permission ( CREATE TABLE IF NOT EXISTS iam_role_permission ( role_id BIGINT NOT NULL COMMENT '角色ID', permission_id BIGINT NOT NULL COMMENT '权限资源ID', - tenant_id BIGINT NOT NULL COMMENT '冗余租户ID,用于隔离校验和查询', + tenant_id BIGINT NOT NULL COMMENT '冗余客户ID或平台作用域ID,用于授权归属校验和查询', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '授权时间', PRIMARY KEY (role_id, permission_id), KEY idx_rp_tenant (tenant_id) @@ -191,13 +191,13 @@ CREATE TABLE IF NOT EXISTS iam_role_permission ( CREATE TABLE IF NOT EXISTS iam_data_scope ( id BIGINT NOT NULL COMMENT '数据范围规则ID', role_id BIGINT NOT NULL COMMENT '角色ID', - tenant_id BIGINT NOT NULL COMMENT '规则所属租户ID', - scope_type VARCHAR(20) NOT NULL DEFAULT 'SELF' COMMENT '范围类型:SELF、DEPARTMENT、TENANT或CUSTOM', + tenant_id BIGINT NOT NULL COMMENT '规则作用域:0为平台,正数为客户ID和数据隔离键', + scope_type VARCHAR(20) NOT NULL DEFAULT 'SELF' COMMENT '数据范围类型:SELF本人、DEPARTMENT部门、TENANT当前客户、CUSTOM自定义', scope_expression JSON NULL COMMENT '自定义数据范围表达式', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', PRIMARY KEY (id), UNIQUE KEY uk_scope_role (role_id) -) ENGINE=InnoDB COMMENT='角色数据范围表:在功能权限之外限制角色可查看和操作的租户内数据集合'; +) ENGINE=InnoDB COMMENT='角色数据范围表:在功能权限之外限制角色可查看和操作的平台或客户范围内数据集合'; -- 存量数据库重复执行初始化脚本时,不重复创建审核状态索引。 SET @review_status_index_sql = IF( diff --git a/iam/src/main/resources/db/phase1-onboarding.sql b/iam/src/main/resources/db/phase1-onboarding.sql index 6060c3934..a31f31127 100644 --- a/iam/src/main/resources/db/phase1-onboarding.sql +++ b/iam/src/main/resources/db/phase1-onboarding.sql @@ -3,7 +3,7 @@ USE baiyue_iam; SET NAMES utf8mb4; ALTER TABLE iam_tenant MODIFY COLUMN status VARCHAR(20) NOT NULL DEFAULT 'PROFILE_REQUIRED' - COMMENT 'PROFILE_REQUIRED, PENDING, ACTIVE, REJECTED or DISABLED'; + COMMENT '客户状态:PROFILE_REQUIRED待完善资料、PENDING待审核、ACTIVE正常、REJECTED已驳回、DISABLED已停用'; DROP PROCEDURE IF EXISTS phase1_iam_add_column; DELIMITER $$ CREATE PROCEDURE phase1_iam_add_column( @@ -22,13 +22,13 @@ BEGIN END IF; END$$ DELIMITER ; -CALL phase1_iam_add_column('iam_tenant', 'approved_at', "DATETIME(3) NULL COMMENT 'Approval time'"); -CALL phase1_iam_add_column('iam_tenant', 'approved_by', "BIGINT NULL COMMENT 'Reviewer ID'"); -CALL phase1_iam_add_column('iam_tenant', 'reject_reason', "VARCHAR(500) NULL COMMENT 'Rejection reason'"); +CALL phase1_iam_add_column('iam_tenant', 'approved_at', "DATETIME(3) NULL COMMENT '客户资料审核通过时间'"); +CALL phase1_iam_add_column('iam_tenant', 'approved_by', "BIGINT NULL COMMENT '客户资料审核人ID'"); +CALL phase1_iam_add_column('iam_tenant', 'reject_reason', "VARCHAR(500) NULL COMMENT '客户资料审核驳回原因'"); CALL phase1_iam_add_column('iam_tenant', 'legal_representative', "VARCHAR(64) NULL COMMENT '法定代表人' AFTER legal_name"); -CALL phase1_iam_add_column('iam_tenant_qualification', 'review_comment', "VARCHAR(500) NULL COMMENT 'Review comment'"); -CALL phase1_iam_add_column('iam_membership', 'created_by', "BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Creator ID'"); -CALL phase1_iam_add_column('iam_membership', 'updated_by', "BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Updater ID'"); +CALL phase1_iam_add_column('iam_tenant_qualification', 'review_comment', "VARCHAR(500) NULL COMMENT '客户资质审核意见'"); +CALL phase1_iam_add_column('iam_membership', 'created_by', "BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '客户成员关系创建人ID;系统创建时为0'"); +CALL phase1_iam_add_column('iam_membership', 'updated_by', "BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '客户成员关系最后修改人ID;系统修改时为0'"); DROP PROCEDURE phase1_iam_add_column; -- A global merchant owner template is assigned to newly registered enterprises. diff --git a/order/src/main/resources/db/audit-outbox.sql b/order/src/main/resources/db/audit-outbox.sql index 836c5ada6..f38d4f0b5 100644 --- a/order/src/main/resources/db/audit-outbox.sql +++ b/order/src/main/resources/db/audit-outbox.sql @@ -2,14 +2,14 @@ USE baiyue_order; -- 可靠审计本地 Outbox;与订单业务表位于同一数据库。 CREATE TABLE IF NOT EXISTS sys_audit_outbox ( - event_id VARCHAR(64) NOT NULL, event_type VARCHAR(64) NOT NULL, resource_type VARCHAR(64) NOT NULL, - resource_id VARCHAR(128) NULL, service_name VARCHAR(64) NULL, payload_json JSON NOT NULL, - status VARCHAR(16) NOT NULL DEFAULT 'PENDING', attempt_count INT NOT NULL DEFAULT 0, - compensation_count INT NOT NULL DEFAULT 0, - next_retry_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), lease_owner VARCHAR(128) NULL, - lease_until DATETIME(3) NULL, last_error VARCHAR(1000) NULL, dead_at DATETIME(3) NULL, - sent_at DATETIME(3) NULL, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + event_id VARCHAR(64) NOT NULL COMMENT '审计事件全局唯一ID', event_type VARCHAR(64) NOT NULL COMMENT '审计事件类型', resource_type VARCHAR(64) NOT NULL COMMENT '被审计资源类型', + resource_id VARCHAR(128) NULL COMMENT '被审计资源ID', service_name VARCHAR(64) NULL COMMENT '来源服务名称', payload_json JSON NOT NULL COMMENT '脱敏后的审计事件内容', + status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT '发送状态:PENDING待发送、SENDING发送中、SENT已发送、DEAD进入死信', attempt_count INT NOT NULL DEFAULT 0 COMMENT '当前重试周期发送次数', + compensation_count INT NOT NULL DEFAULT 0 COMMENT '自动补偿发送次数', + next_retry_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '下一次重试时间', lease_owner VARCHAR(128) NULL COMMENT '当前租约持有者', + lease_until DATETIME(3) NULL COMMENT '发送租约截止时间', last_error VARCHAR(1000) NULL COMMENT '最近一次发送错误信息', dead_at DATETIME(3) NULL COMMENT '进入死信时间', + sent_at DATETIME(3) NULL COMMENT '成功发送时间', 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 (event_id), KEY idx_audit_outbox_publish (status, next_retry_at, lease_until, created_at), KEY idx_audit_outbox_retention (status, sent_at) ) ENGINE=InnoDB COMMENT='可靠审计事件本地发送箱'; diff --git a/order/src/main/resources/db/order.sql b/order/src/main/resources/db/order.sql index 53709f630..85bffca92 100644 --- a/order/src/main/resources/db/order.sql +++ b/order/src/main/resources/db/order.sql @@ -7,12 +7,12 @@ USE baiyue_order; CREATE TABLE IF NOT EXISTS ord_order ( id BIGINT UNSIGNED NOT NULL COMMENT '订单ID(雪花ID)', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', - order_no VARCHAR(32) NOT NULL COMMENT '租户内唯一订单号', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + order_no VARCHAR(32) NOT NULL COMMENT '客户范围内唯一订单号', inbound_code VARCHAR(40) NULL COMMENT '客户提交后生成的入仓码,草稿为空', customer_id BIGINT UNSIGNED NULL COMMENT '客户逻辑ID', customer_name VARCHAR(128) NOT NULL COMMENT '下单时客户名称快照', - status VARCHAR(24) NOT NULL DEFAULT 'DRAFT' COMMENT '订单业务状态', + status VARCHAR(24) NOT NULL DEFAULT 'DRAFT' COMMENT '订单业务状态:DRAFT草稿、SUBMITTED已提交、APPROVED审核通过、ACCEPTED已接单、REJECTED审核驳回、PROCESSING履约中、COMPLETED已完成、CANCELLED已取消', transport_mode VARCHAR(16) NOT NULL COMMENT '运输方式', trade_term VARCHAR(16) NULL COMMENT '贸易条款', destination_country_code CHAR(2) NOT NULL COMMENT '目的国家二字码', @@ -33,10 +33,10 @@ CREATE TABLE IF NOT EXISTS ord_order ( pricing_tax_mode VARCHAR(16) NULL COMMENT '报价包税维度:TAX_INCLUDED或TAX_EXCLUDED;独立于tax_mode', container_mode VARCHAR(24) NULL COMMENT '整柜或散货', customs_inspection TINYINT(1) NULL COMMENT '是否商检', - destination_type VARCHAR(24) NULL COMMENT 'FBA/OVERSEAS/PRIVATE', + destination_type VARCHAR(24) NULL COMMENT '目的仓类型:FBA亚马逊仓、OVERSEAS海外仓、PRIVATE私人地址', amazon_warehouse_code VARCHAR(32) NULL COMMENT '亚马逊仓库编码', - shipment_id VARCHAR(64) NULL COMMENT 'Amazon Shipment ID', - reference_id VARCHAR(64) NULL COMMENT 'Amazon Reference ID', + shipment_id VARCHAR(64) NULL COMMENT '订单级Amazon Shipment ID', + reference_id VARCHAR(64) NULL COMMENT '订单级Amazon Reference ID', order_role VARCHAR(12) NOT NULL DEFAULT 'MASTER' COMMENT '订单层级:MASTER主订单、CHILD子订单', parent_order_id BIGINT UNSIGNED NULL COMMENT '直属父订单ID;主订单为空;禁止子订单嵌套', root_order_id BIGINT UNSIGNED NULL COMMENT '根主订单ID', @@ -61,25 +61,25 @@ CREATE TABLE IF NOT EXISTS ord_order ( CREATE TABLE IF NOT EXISTS ord_order_fulfillment_split ( id BIGINT UNSIGNED NOT NULL COMMENT '拆分台账ID', - tenant_id BIGINT UNSIGNED NOT NULL, - parent_order_id BIGINT UNSIGNED NOT NULL, + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + parent_order_id BIGINT UNSIGNED NOT NULL COMMENT '直属父订单ID;主订单作为拆分根', source_order_id BIGINT UNSIGNED NOT NULL COMMENT '本次实际消费的主订单或余量子订单ID', - child_order_id BIGINT UNSIGNED NOT NULL, - split_role VARCHAR(12) NOT NULL COMMENT '本台账子片:LOADED或REMAINDER', + child_order_id BIGINT UNSIGNED NOT NULL COMMENT '本次分片承接的子订单ID', + split_role VARCHAR(12) NOT NULL COMMENT '本台账分片角色:LOADED已装柜部分、REMAINDER剩余部分', loading_plan_id BIGINT UNSIGNED NULL COMMENT 'Firstleg装柜计划逻辑ID', container_booking_id BIGINT UNSIGNED NULL COMMENT 'Firstleg柜次逻辑ID', - source_package_count INT UNSIGNED NOT NULL, - loaded_package_count INT UNSIGNED NOT NULL, - remaining_package_count INT UNSIGNED NOT NULL, + source_package_count INT UNSIGNED NOT NULL COMMENT '来源订单本次分片前的总包装件数', + loaded_package_count INT UNSIGNED NOT NULL COMMENT '本次已装柜分片包装件数', + remaining_package_count INT UNSIGNED NOT NULL COMMENT '本次分片后剩余包装件数', source_detail_json JSON NULL COMMENT '来源货物明细及箱数快照', - operation_id VARCHAR(64) NOT NULL, - idempotency_key VARCHAR(128) NOT NULL, - request_hash CHAR(64) NOT NULL, - expected_parent_version INT NOT NULL, - created_by BIGINT UNSIGNED NOT NULL, - created_by_name VARCHAR(64) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - version INT NOT NULL DEFAULT 0, + operation_id VARCHAR(64) NOT NULL COMMENT '履约分片业务操作ID', + idempotency_key VARCHAR(128) NOT NULL COMMENT '客户端幂等键', + request_hash CHAR(64) NOT NULL COMMENT '规范化请求内容SHA-256摘要', + expected_parent_version INT NOT NULL COMMENT '执行前预期父订单乐观锁版本号', + created_by BIGINT UNSIGNED NOT NULL COMMENT '创建该分片台账的用户ID', + created_by_name VARCHAR(64) NULL COMMENT '创建人名称快照', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '台账创建时间', + version INT NOT NULL DEFAULT 0 COMMENT '台账乐观锁版本号', PRIMARY KEY (id), UNIQUE KEY uk_order_split_operation (tenant_id, operation_id, split_role), UNIQUE KEY uk_order_split_idempotency (tenant_id, parent_order_id, idempotency_key, split_role), @@ -127,7 +127,7 @@ DELIMITER ; CREATE TABLE IF NOT EXISTS ord_order_party ( id BIGINT UNSIGNED NOT NULL COMMENT '参与方快照ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', order_id BIGINT UNSIGNED NOT NULL COMMENT '所属订单ID', party_type VARCHAR(16) NOT NULL COMMENT '参与方类型:SHIPPER或CONSIGNEE', company_name VARCHAR(128) NOT NULL COMMENT '公司名称快照', @@ -153,7 +153,7 @@ CREATE TABLE IF NOT EXISTS ord_order_party ( CREATE TABLE IF NOT EXISTS ord_order_cargo ( id BIGINT UNSIGNED NOT NULL COMMENT '货物明细ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', order_id BIGINT UNSIGNED NOT NULL COMMENT '所属订单ID', item_no INT NOT NULL COMMENT '订单内明细序号', cargo_name VARCHAR(128) NOT NULL COMMENT '货物名称', @@ -169,8 +169,8 @@ CREATE TABLE IF NOT EXISTS ord_order_cargo ( currency CHAR(3) NULL COMMENT '币种三字码', dangerous_goods TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否危险品', cargo_remark VARCHAR(500) NULL COMMENT '货物备注', - sku_no VARCHAR(64) NULL COMMENT 'SKU', - upc_no VARCHAR(64) NULL COMMENT 'UPC', + sku_no VARCHAR(64) NULL COMMENT '货物SKU编码', + upc_no VARCHAR(64) NULL COMMENT '货物UPC条码', cargo_name_en VARCHAR(128) NULL COMMENT '英文品名', customs_hs_code VARCHAR(20) NULL COMMENT '目的国清关HS CODE', units_per_package INT UNSIGNED NULL COMMENT '每箱件数', @@ -201,7 +201,7 @@ CREATE TABLE IF NOT EXISTS ord_order_cargo ( CREATE TABLE IF NOT EXISTS ord_order_internal_note ( id BIGINT UNSIGNED NOT NULL COMMENT '内部备注ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', order_id BIGINT UNSIGNED NOT NULL COMMENT '所属订单ID', content VARCHAR(2000) NOT NULL COMMENT '仅内部人员可见的备注内容', created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID', @@ -217,7 +217,7 @@ CREATE TABLE IF NOT EXISTS ord_order_internal_note ( CREATE TABLE IF NOT EXISTS ord_order_status_history ( id BIGINT UNSIGNED NOT NULL COMMENT '状态历史ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', order_id BIGINT UNSIGNED NOT NULL COMMENT '所属订单ID', from_status VARCHAR(24) NULL COMMENT '变更前状态,首次创建可为空', to_status VARCHAR(24) NOT NULL COMMENT '变更后状态', @@ -233,12 +233,12 @@ CREATE TABLE IF NOT EXISTS ord_order_status_history ( CREATE TABLE IF NOT EXISTS ord_outbox_event ( id BIGINT UNSIGNED NOT NULL COMMENT '领域事件全局ID,同时作为消费幂等键', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '事件所属租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '事件所属客户ID和数据隔离键', aggregate_type VARCHAR(32) NOT NULL COMMENT '聚合类型,例如 ORDER', aggregate_id BIGINT UNSIGNED NOT NULL COMMENT '发生变更的聚合根ID,例如订单ID', event_type VARCHAR(64) NOT NULL COMMENT '领域事件类型,例如 ORDER_SUBMITTED', payload_json JSON NOT NULL COMMENT '供下游消费的事件负载快照', - publish_status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT '发布状态:PENDING、PUBLISHED或FAILED', + publish_status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT '发布状态:PENDING待发布、PUBLISHED已发布、FAILED发布失败', retry_count INT NOT NULL DEFAULT 0 COMMENT '投递失败后的累计重试次数', next_retry_at DATETIME(3) NULL COMMENT '允许再次投递的最早时间', published_at DATETIME(3) NULL COMMENT '成功发布到消息中间件的时间', @@ -249,13 +249,13 @@ CREATE TABLE IF NOT EXISTS ord_outbox_event ( CREATE TABLE IF NOT EXISTS ord_idempotency_record ( id BIGINT UNSIGNED NOT NULL COMMENT '幂等记录ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '租户ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', operator_id BIGINT UNSIGNED NOT NULL COMMENT '操作者ID', operation VARCHAR(32) NOT NULL COMMENT '幂等操作类型', resource_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '资源ID,创建草稿时为0', idempotency_key VARCHAR(128) NOT NULL COMMENT '客户端幂等键', request_hash CHAR(64) NOT NULL COMMENT '请求内容SHA-256摘要', - status VARCHAR(16) NOT NULL COMMENT '处理状态:PROCESSING或COMPLETED', + status VARCHAR(16) NOT NULL COMMENT '处理状态:PROCESSING处理中、COMPLETED已完成', result_order_id BIGINT UNSIGNED NULL COMMENT '成功结果订单ID', completed_at DATETIME(3) NULL COMMENT '处理完成时间', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', diff --git a/order/src/main/resources/db/phase1-order-extension.sql b/order/src/main/resources/db/phase1-order-extension.sql index acbbde30f..00dd9e56b 100644 --- a/order/src/main/resources/db/phase1-order-extension.sql +++ b/order/src/main/resources/db/phase1-order-extension.sql @@ -25,41 +25,41 @@ BEGIN END$$ DELIMITER ; -CALL phase1_add_column('ord_order', 'pickup_method', "VARCHAR(24) NULL COMMENT 'Pickup method'"); -CALL phase1_add_column('ord_order', 'receiving_warehouse_id', "BIGINT UNSIGNED NULL COMMENT 'Domestic receiving warehouse ID'"); -CALL phase1_add_column('ord_order', 'receiving_warehouse_name', "VARCHAR(128) NULL COMMENT 'Domestic receiving warehouse snapshot'"); +CALL phase1_add_column('ord_order', 'pickup_method', "VARCHAR(24) NULL COMMENT '取货方式'"); +CALL phase1_add_column('ord_order', 'receiving_warehouse_id', "BIGINT UNSIGNED NULL COMMENT '国内收货仓逻辑ID'"); +CALL phase1_add_column('ord_order', 'receiving_warehouse_name', "VARCHAR(128) NULL COMMENT '下单时国内收货仓名称快照;主数据变化不回写'"); CALL phase1_add_column('ord_order', 'receiving_warehouse_address', "VARCHAR(512) NULL COMMENT '国内收货仓完整地址快照'"); CALL phase1_add_column('ord_order', 'receiving_time', "VARCHAR(128) NULL COMMENT '国内收货仓收货时间快照'"); -CALL phase1_add_column('ord_order', 'channel_code', "VARCHAR(64) NULL COMMENT 'Shipping channel code'"); -CALL phase1_add_column('ord_order', 'channel_name', "VARCHAR(128) NULL COMMENT 'Shipping channel snapshot'"); -CALL phase1_add_column('ord_order', 'tax_mode', "VARCHAR(24) NULL COMMENT 'BUY_DECLARATION or TAX_REFUND'"); -CALL phase1_add_column('ord_order', 'container_mode', "VARCHAR(24) NULL COMMENT 'FULL_CONTAINER or LCL'"); -CALL phase1_add_column('ord_order', 'customs_inspection', "TINYINT(1) NULL COMMENT 'Whether customs inspection is required'"); -CALL phase1_add_column('ord_order', 'destination_type', "VARCHAR(24) NULL COMMENT 'FBA, OVERSEAS or PRIVATE'"); -CALL phase1_add_column('ord_order', 'amazon_warehouse_code', "VARCHAR(32) NULL COMMENT 'Amazon warehouse code'"); -CALL phase1_add_column('ord_order', 'shipment_id', "VARCHAR(64) NULL COMMENT 'Amazon Shipment ID'"); -CALL phase1_add_column('ord_order', 'reference_id', "VARCHAR(64) NULL COMMENT 'Amazon Reference ID'"); +CALL phase1_add_column('ord_order', 'channel_code', "VARCHAR(64) NULL COMMENT '运输渠道编码'"); +CALL phase1_add_column('ord_order', 'channel_name', "VARCHAR(128) NULL COMMENT '下单时运输渠道名称快照;渠道主数据变化不回写'"); +CALL phase1_add_column('ord_order', 'tax_mode', "VARCHAR(24) NULL COMMENT '报关税务方式:BUY_DECLARATION买单、TAX_REFUND退税'"); +CALL phase1_add_column('ord_order', 'container_mode', "VARCHAR(24) NULL COMMENT '装运方式:FULL_CONTAINER整柜、LCL散货拼箱'"); +CALL phase1_add_column('ord_order', 'customs_inspection', "TINYINT(1) NULL COMMENT '是否需要商检:0否、1是'"); +CALL phase1_add_column('ord_order', 'destination_type', "VARCHAR(24) NULL COMMENT '目的仓类型:FBA亚马逊仓、OVERSEAS海外仓、PRIVATE私人地址'"); +CALL phase1_add_column('ord_order', 'amazon_warehouse_code', "VARCHAR(32) NULL COMMENT 'Amazon目的仓编码'"); +CALL phase1_add_column('ord_order', 'shipment_id', "VARCHAR(64) NULL COMMENT '订单级Amazon Shipment ID'"); +CALL phase1_add_column('ord_order', 'reference_id', "VARCHAR(64) NULL COMMENT '订单级Amazon Reference ID'"); CALL phase1_add_column('ord_order', 'express_company', "VARCHAR(64) NULL COMMENT '履约快递公司名称快照'"); CALL phase1_add_column('ord_order', 'express_tracking_no', "VARCHAR(128) NULL COMMENT '履约快递单号'"); -CALL phase1_add_column('ord_order_cargo', 'sku_no', "VARCHAR(64) NULL COMMENT 'SKU number'"); -CALL phase1_add_column('ord_order_cargo', 'upc_no', "VARCHAR(64) NULL COMMENT 'UPC number'"); -CALL phase1_add_column('ord_order_cargo', 'cargo_name_en', "VARCHAR(128) NULL COMMENT 'English cargo name'"); -CALL phase1_add_column('ord_order_cargo', 'customs_hs_code', "VARCHAR(20) NULL COMMENT 'Destination customs HS code'"); -CALL phase1_add_column('ord_order_cargo', 'units_per_package', "INT UNSIGNED NULL COMMENT 'Units per carton'"); -CALL phase1_add_column('ord_order_cargo', 'total_quantity', "INT UNSIGNED NULL COMMENT 'Total units'"); -CALL phase1_add_column('ord_order_cargo', 'net_weight_kg', "DECIMAL(18,3) NULL COMMENT 'Net weight in kilograms'"); -CALL phase1_add_column('ord_order_cargo', 'unit_declared_value', "DECIMAL(18,2) NULL COMMENT 'Declared value per unit'"); -CALL phase1_add_column('ord_order_cargo', 'material', "VARCHAR(128) NULL COMMENT 'Material'"); -CALL phase1_add_column('ord_order_cargo', 'product_usage', "VARCHAR(128) NULL COMMENT 'Usage'"); -CALL phase1_add_column('ord_order_cargo', 'model', "VARCHAR(64) NULL COMMENT 'Model'"); -CALL phase1_add_column('ord_order_cargo', 'brand', "VARCHAR(64) NULL COMMENT 'Brand'"); -CALL phase1_add_column('ord_order_cargo', 'product_length_cm', "DECIMAL(12,2) NULL COMMENT 'Product length in centimeters'"); -CALL phase1_add_column('ord_order_cargo', 'product_width_cm', "DECIMAL(12,2) NULL COMMENT 'Product width in centimeters'"); -CALL phase1_add_column('ord_order_cargo', 'product_height_cm', "DECIMAL(12,2) NULL COMMENT 'Product height in centimeters'"); -CALL phase1_add_column('ord_order_cargo', 'shipment_id', "VARCHAR(64) NULL COMMENT 'Cargo Amazon Shipment ID'"); -CALL phase1_add_column('ord_order_cargo', 'reference_id', "VARCHAR(64) NULL COMMENT 'Cargo Amazon Reference ID'"); -CALL phase1_add_column('ord_order_cargo', 'product_image_file_id', "BIGINT UNSIGNED NULL COMMENT 'Product image logical file ID'"); +CALL phase1_add_column('ord_order_cargo', 'sku_no', "VARCHAR(64) NULL COMMENT '货物SKU编码'"); +CALL phase1_add_column('ord_order_cargo', 'upc_no', "VARCHAR(64) NULL COMMENT '货物UPC条码'"); +CALL phase1_add_column('ord_order_cargo', 'cargo_name_en', "VARCHAR(128) NULL COMMENT '货物英文品名'"); +CALL phase1_add_column('ord_order_cargo', 'customs_hs_code', "VARCHAR(20) NULL COMMENT '目的国清关HS编码'"); +CALL phase1_add_column('ord_order_cargo', 'units_per_package', "INT UNSIGNED NULL COMMENT '每箱产品数量'"); +CALL phase1_add_column('ord_order_cargo', 'total_quantity', "INT UNSIGNED NULL COMMENT '产品总数量'"); +CALL phase1_add_column('ord_order_cargo', 'net_weight_kg', "DECIMAL(18,3) NULL COMMENT '货物净重,单位千克'"); +CALL phase1_add_column('ord_order_cargo', 'unit_declared_value', "DECIMAL(18,2) NULL COMMENT '单件申报价值;币种取订单申报币种'"); +CALL phase1_add_column('ord_order_cargo', 'material', "VARCHAR(128) NULL COMMENT '产品材质'"); +CALL phase1_add_column('ord_order_cargo', 'product_usage', "VARCHAR(128) NULL COMMENT '产品用途'"); +CALL phase1_add_column('ord_order_cargo', 'model', "VARCHAR(64) NULL COMMENT '产品型号'"); +CALL phase1_add_column('ord_order_cargo', 'brand', "VARCHAR(64) NULL COMMENT '产品品牌'"); +CALL phase1_add_column('ord_order_cargo', 'product_length_cm', "DECIMAL(12,2) NULL COMMENT '单件产品长度,单位厘米'"); +CALL phase1_add_column('ord_order_cargo', 'product_width_cm', "DECIMAL(12,2) NULL COMMENT '单件产品宽度,单位厘米'"); +CALL phase1_add_column('ord_order_cargo', 'product_height_cm', "DECIMAL(12,2) NULL COMMENT '单件产品高度,单位厘米'"); +CALL phase1_add_column('ord_order_cargo', 'shipment_id', "VARCHAR(64) NULL COMMENT '本货物明细对应的Amazon Shipment ID'"); +CALL phase1_add_column('ord_order_cargo', 'reference_id', "VARCHAR(64) NULL COMMENT '本货物明细对应的Amazon Reference ID'"); +CALL phase1_add_column('ord_order_cargo', 'product_image_file_id', "BIGINT UNSIGNED NULL COMMENT 'File服务中的产品图片逻辑文件ID'"); -- 货物级字段上线前,FBA 编号只保存在主单;回填后提交校验可统一按每条货物执行。 UPDATE ord_order_cargo cargo @@ -75,15 +75,15 @@ DROP PROCEDURE phase1_add_column; -- 已有订单库的幂等记录迁移,和订单写入处于同一个本地事务。 CREATE TABLE IF NOT EXISTS ord_idempotency_record ( id BIGINT UNSIGNED NOT NULL COMMENT '幂等记录 ID', - tenant_id BIGINT UNSIGNED NOT NULL COMMENT '租户 ID', - operator_id BIGINT UNSIGNED NOT NULL COMMENT '操作人 ID', - operation VARCHAR(32) NOT NULL COMMENT '操作类型', - resource_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '资源 ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + operator_id BIGINT UNSIGNED NOT NULL COMMENT '发起写操作的用户ID', + operation VARCHAR(32) NOT NULL COMMENT '幂等操作类型', + resource_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '操作目标资源ID;创建前未知时为0', idempotency_key VARCHAR(128) NOT NULL COMMENT '客户端幂等键', - request_hash CHAR(64) NOT NULL COMMENT '请求内容摘要', - status VARCHAR(16) NOT NULL COMMENT '处理状态', - result_order_id BIGINT UNSIGNED NULL COMMENT '重放结果订单 ID', - completed_at DATETIME(3) NULL COMMENT '完成时间', + request_hash CHAR(64) NOT NULL COMMENT '规范化请求内容SHA-256摘要,用于识别同键不同请求', + status VARCHAR(16) NOT NULL COMMENT '处理状态:PROCESSING处理中、COMPLETED已完成', + result_order_id BIGINT UNSIGNED NULL COMMENT '成功操作生成或更新的订单ID,用于幂等重放', + completed_at DATETIME(3) NULL COMMENT '处理完成时间', 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 (id), diff --git a/order/src/main/resources/db/phase3-order-parent-child-schema.sql b/order/src/main/resources/db/phase3-order-parent-child-schema.sql index da9d7f092..c66f26f1c 100644 --- a/order/src/main/resources/db/phase3-order-parent-child-schema.sql +++ b/order/src/main/resources/db/phase3-order-parent-child-schema.sql @@ -12,7 +12,7 @@ BEGIN END IF; END$$ DELIMITER ; -CALL phase3_order_add_column('ord_order','order_role',"VARCHAR(12) NOT NULL DEFAULT 'MASTER' COMMENT '订单层级:MASTER、CHILD'"); +CALL phase3_order_add_column('ord_order','order_role',"VARCHAR(12) NOT NULL DEFAULT 'MASTER' COMMENT '订单层级:MASTER主订单、CHILD子订单'"); CALL phase3_order_add_column('ord_order','parent_order_id','BIGINT UNSIGNED NULL COMMENT ''直属父订单ID;禁止子订单嵌套'''); CALL phase3_order_add_column('ord_order','root_order_id','BIGINT UNSIGNED NULL COMMENT ''根主订单ID'''); CALL phase3_order_add_column('ord_order','split_sequence','INT UNSIGNED NULL COMMENT ''同一父订单下的同级顺序'''); @@ -48,26 +48,26 @@ CALL phase3_order_parent_fk(); DROP PROCEDURE IF EXISTS phase3_order_parent_fk; CREATE TABLE IF NOT EXISTS ord_order_fulfillment_split ( - id BIGINT UNSIGNED NOT NULL, - tenant_id BIGINT UNSIGNED NOT NULL, - parent_order_id BIGINT UNSIGNED NOT NULL, - source_order_id BIGINT UNSIGNED NOT NULL, - child_order_id BIGINT UNSIGNED NOT NULL, - split_role VARCHAR(12) NOT NULL COMMENT 'LOADED或REMAINDER', - loading_plan_id BIGINT UNSIGNED NULL, - container_booking_id BIGINT UNSIGNED NULL, - source_package_count INT UNSIGNED NOT NULL, - loaded_package_count INT UNSIGNED NOT NULL, - remaining_package_count INT UNSIGNED NOT NULL, - source_detail_json JSON NULL, - operation_id VARCHAR(64) NOT NULL, - idempotency_key VARCHAR(128) NOT NULL, - request_hash CHAR(64) NOT NULL, - expected_parent_version INT NOT NULL, - created_by BIGINT UNSIGNED NOT NULL, - created_by_name VARCHAR(64) NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - version INT NOT NULL DEFAULT 0, + id BIGINT UNSIGNED NOT NULL COMMENT '订单履约分片台账ID', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键', + parent_order_id BIGINT UNSIGNED NOT NULL COMMENT '直属父订单ID;主订单自身作为拆分根', + source_order_id BIGINT UNSIGNED NOT NULL COMMENT '实际被本次分片消费的来源订单ID', + child_order_id BIGINT UNSIGNED NOT NULL COMMENT '本次分片生成或承接的子订单ID', + split_role VARCHAR(12) NOT NULL COMMENT '分片角色:LOADED已装柜部分、REMAINDER剩余部分', + loading_plan_id BIGINT UNSIGNED NULL COMMENT 'Firstleg装柜计划逻辑ID', + container_booking_id BIGINT UNSIGNED NULL COMMENT 'Firstleg柜次逻辑ID', + source_package_count INT UNSIGNED NOT NULL COMMENT '来源订单本次分片前的总包装件数', + loaded_package_count INT UNSIGNED NOT NULL COMMENT '本次已装柜分片的包装件数', + remaining_package_count INT UNSIGNED NOT NULL COMMENT '本次分片后剩余包装件数', + source_detail_json JSON NULL COMMENT '来源货物明细及数量快照;来源订单变化不回写', + operation_id VARCHAR(64) NOT NULL COMMENT '履约分片业务操作ID', + idempotency_key VARCHAR(128) NOT NULL COMMENT '客户端幂等键', + request_hash CHAR(64) NOT NULL COMMENT '规范化请求内容SHA-256摘要', + expected_parent_version INT NOT NULL COMMENT '执行前预期父订单乐观锁版本号', + created_by BIGINT UNSIGNED NOT NULL COMMENT '创建该分片台账的用户ID', + created_by_name VARCHAR(64) NULL COMMENT '创建人名称快照', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '台账创建时间', + version INT NOT NULL DEFAULT 0 COMMENT '台账乐观锁版本号', PRIMARY KEY(id), UNIQUE KEY uk_order_split_operation (tenant_id,operation_id,split_role), UNIQUE KEY uk_order_split_idempotency (tenant_id,parent_order_id,idempotency_key,split_role), @@ -115,7 +115,7 @@ BEGIN END$$ DELIMITER ; -CALL phase3_order_add_column('ord_order_fulfillment_split','split_role',"VARCHAR(12) NOT NULL DEFAULT 'LOADED' COMMENT 'LOADED或REMAINDER'"); +CALL phase3_order_add_column('ord_order_fulfillment_split','split_role',"VARCHAR(12) NOT NULL DEFAULT 'LOADED' COMMENT '分片角色:LOADED已装柜部分、REMAINDER剩余部分'"); CALL phase3_order_add_column('ord_order_fulfillment_split','source_order_id','BIGINT UNSIGNED NULL COMMENT ''实际被消费的来源订单ID'''); CALL phase3_order_add_column('ord_order_fulfillment_split','request_hash',"CHAR(64) NULL COMMENT '请求内容SHA-256;存量记录允许为空'"); -- 旧版台账没有来源字段;当时只允许从根主订单拆分,因此可无歧义回填为父订单。 diff --git a/scripts/README.md b/scripts/README.md index d392e2f1c..64b712a82 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -44,9 +44,24 @@ Dry-run 只检查根 `pom.xml` 和关键回归测试文件并打印 Maven 命令 ## 现有相关脚本与边界 +### 数据库中文注释审计 + +`database-comment-audit.ps1` 离线扫描各模块 `src/main/resources/db/*.sql` 中的 `CREATE TABLE` 定义,不连接数据库、不执行 SQL,也不修改任何文件。 + +```powershell +.\scripts\database-comment-audit.ps1 +.\scripts\database-comment-audit.ps1 -Modules finance,order +.\scripts\database-comment-audit.ps1 -Modules finance -SummaryOnly +.\scripts\database-comment-audit.ps1 -Format Json +.\scripts\database-comment-audit.ps1 -FailOnIssues +.\scripts\database-comment-audit.ps1 -SelfTest +``` + +默认报告模式即使存在存量缺口也返回 `0`;CI 使用 `-FailOnIssues`,缺失或纯英文表/字段注释返回 `1`,模块或路径配置错误返回 `2`。规范和注释口径见 `../docs/数据库注释维护规范.md`。 + ### 本地 MySQL 全量验收 -正式入口是 `local-db-full-acceptance.ps1`,只允许连接本机 `127.0.0.1` 或 `localhost`,默认端口 `3306`;测试库 `8.163.30.128` 会立即以退出码 2 拒绝。脚本固定校验统一 SQL SHA-256 `E86BB8DE105E9B59736FFCD180CBA902F2E3F5032C7EFF2E3DC3B24B289360A0` 和 7803 行。 +正式入口是 `local-db-full-acceptance.ps1`,只允许连接本机 `127.0.0.1` 或 `localhost`,默认端口 `3306`;测试库 `8.163.30.128` 会立即以退出码 2 拒绝。脚本固定校验统一 SQL SHA-256 `7F61C3BAB0D4D49525A30C1E5030DEA4B798F38F6A0287901E6D1D676967DFFA` 和 13712 行,并静态确认中文注释升级采用校验入队、每表一次 ALTER、结构签名复核、九库覆盖断言及唯一最终 `PASS`。 ```powershell .\local-db-full-acceptance.ps1 -DryRun diff --git a/scripts/database-comment-audit.ps1 b/scripts/database-comment-audit.ps1 new file mode 100644 index 000000000..3e8b5e2f7 --- /dev/null +++ b/scripts/database-comment-audit.ps1 @@ -0,0 +1,230 @@ +[CmdletBinding()] +param( + [string]$RepositoryRoot, + [string[]]$Modules, + [ValidateSet("Text", "Json")] + [string]$Format = "Text", + [switch]$SummaryOnly, + [switch]$FailOnIssues, + [switch]$SelfTest +) + +$ErrorActionPreference = "Stop" + +function Get-SqlComment { + param([string]$Text) + + $match = [regex]::Match($Text, "(?is)\bCOMMENT\s*(?:=\s*)?'(?(?:''|[^'])*)'") + if (-not $match.Success) { + return $null + } + return $match.Groups["value"].Value.Replace("''", "'").Trim() +} + +function Get-CreateTableDefinitions { + param( + [string]$Sql, + [string]$Source + ) + + $pattern = '(?is)\bCREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(?`?[A-Za-z_][A-Za-z0-9_$]*`?)\s*\((?.*?)\)\s*(?ENGINE\s*=.*?);' + $mysqlColumnTypes = @( + "BIGINT", "INT", "INTEGER", "MEDIUMINT", "SMALLINT", "TINYINT", + "DECIMAL", "NUMERIC", "FIXED", "FLOAT", "DOUBLE", "REAL", "BIT", "BOOL", "BOOLEAN", + "CHAR", "VARCHAR", "NCHAR", "NVARCHAR", "BINARY", "VARBINARY", + "TINYTEXT", "TEXT", "MEDIUMTEXT", "LONGTEXT", + "TINYBLOB", "BLOB", "MEDIUMBLOB", "LONGBLOB", "ENUM", "SET", + "DATE", "TIME", "DATETIME", "TIMESTAMP", "YEAR", "JSON", + "GEOMETRY", "POINT", "LINESTRING", "POLYGON", "MULTIPOINT", + "MULTILINESTRING", "MULTIPOLYGON", "GEOMETRYCOLLECTION" + ) + $matches = [regex]::Matches($Sql, $pattern) + $definitions = @() + + foreach ($match in $matches) { + $columns = @() + foreach ($line in ($match.Groups["body"].Value -split "`r?`n")) { + $columnMatch = [regex]::Match($line, '^\s*(?`?[A-Za-z_][A-Za-z0-9_$]*`?)\s+(?[A-Za-z].*?)\s*,?\s*$') + if (-not $columnMatch.Success) { + continue + } + + $definition = $columnMatch.Groups["definition"].Value + $typeMatch = [regex]::Match($definition, '^(?[A-Za-z]+)\b') + if (-not $typeMatch.Success -or $typeMatch.Groups["type"].Value.ToUpperInvariant() -notin $mysqlColumnTypes) { + continue + } + + $columnName = $columnMatch.Groups["name"].Value.Trim('`') + $columns += [pscustomobject]@{ + Name = $columnName + Comment = Get-SqlComment -Text $line + } + } + + $definitions += [pscustomobject]@{ + Source = $Source + Table = $match.Groups["name"].Value.Trim('`') + Comment = Get-SqlComment -Text $match.Groups["tail"].Value + Columns = $columns + } + } + + return $definitions +} + +function Test-ChineseComment { + param([AllowNull()][string]$Comment) + return -not [string]::IsNullOrWhiteSpace($Comment) -and $Comment -match '[\p{IsCJKUnifiedIdeographs}]' +} + +function Get-AuditResult { + param([object[]]$Definitions) + + $issues = @() + $tableCount = 0 + $commentedTableCount = 0 + $columnCount = 0 + $commentedColumnCount = 0 + + foreach ($definition in $Definitions) { + $tableCount++ + if (-not [string]::IsNullOrWhiteSpace($definition.Comment)) { + $commentedTableCount++ + } + + if ([string]::IsNullOrWhiteSpace($definition.Comment)) { + $issues += [pscustomobject]@{ Code = "MISSING_TABLE_COMMENT"; Source = $definition.Source; Table = $definition.Table; Column = $null } + } elseif (-not (Test-ChineseComment -Comment $definition.Comment)) { + $issues += [pscustomobject]@{ Code = "NON_CHINESE_TABLE_COMMENT"; Source = $definition.Source; Table = $definition.Table; Column = $null } + } + + foreach ($column in $definition.Columns) { + $columnCount++ + if (-not [string]::IsNullOrWhiteSpace($column.Comment)) { + $commentedColumnCount++ + } + + if ([string]::IsNullOrWhiteSpace($column.Comment)) { + $issues += [pscustomobject]@{ Code = "MISSING_COLUMN_COMMENT"; Source = $definition.Source; Table = $definition.Table; Column = $column.Name } + } elseif (-not (Test-ChineseComment -Comment $column.Comment)) { + $issues += [pscustomobject]@{ Code = "NON_CHINESE_COLUMN_COMMENT"; Source = $definition.Source; Table = $definition.Table; Column = $column.Name } + } + } + } + + $tableCoverage = if ($tableCount -eq 0) { 100 } else { [math]::Round(100 * $commentedTableCount / $tableCount, 2) } + $columnCoverage = if ($columnCount -eq 0) { 100 } else { [math]::Round(100 * $commentedColumnCount / $columnCount, 2) } + + return [pscustomobject]@{ + Summary = [pscustomobject]@{ + TableDefinitions = $tableCount + CommentedTables = $commentedTableCount + TableCoveragePercent = $tableCoverage + Columns = $columnCount + CommentedColumns = $commentedColumnCount + ColumnCoveragePercent = $columnCoverage + Issues = $issues.Count + } + Issues = $issues + } +} + +function Invoke-SelfTest { + $validSql = @" +CREATE TABLE IF NOT EXISTS sample_valid ( + id BIGINT NOT NULL COMMENT '主键ID', + status VARCHAR(16) NOT NULL COMMENT '状态:ACTIVE启用、DISABLED停用', + amount DECIMAL(20,6) NOT NULL COMMENT '含税金额;币种由currency_code指定', + and_flag TINYINT(1) NOT NULL COMMENT '附加条件标记:0否、1是', + CONSTRAINT chk_sample_status CHECK ( + status IN ('ACTIVE', 'DISABLED') + AND amount >= 0 + OR (status = 'DISABLED' AND amount = 0) + ) +) ENGINE=InnoDB COMMENT='自测业务表'; +"@ + $invalidSql = @" +CREATE TABLE sample_invalid ( + id BIGINT NOT NULL, + payload JSON NULL COMMENT 'JSON payload' +) ENGINE=InnoDB COMMENT='Sample table'; +"@ + + $valid = Get-AuditResult -Definitions @(Get-CreateTableDefinitions -Sql $validSql -Source "valid.sql") + if ($valid.Summary.TableDefinitions -ne 1 -or $valid.Summary.Columns -ne 4 -or $valid.Summary.Issues -ne 0) { + throw "Self-test failed: valid SQL was not parsed as expected." + } + + $invalid = Get-AuditResult -Definitions @(Get-CreateTableDefinitions -Sql $invalidSql -Source "invalid.sql") + $codes = @($invalid.Issues | ForEach-Object { $_.Code }) + foreach ($expected in @("NON_CHINESE_TABLE_COMMENT", "MISSING_COLUMN_COMMENT", "NON_CHINESE_COLUMN_COMMENT")) { + if ($expected -notin $codes) { + throw "Self-test failed: missing expected issue $expected." + } + } + + Write-Output "SELF-TEST PASS: parser, coverage and Chinese-comment checks are working." +} + +if ($SelfTest) { + Invoke-SelfTest + exit 0 +} + +if ([string]::IsNullOrWhiteSpace($RepositoryRoot)) { + $RepositoryRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path +} else { + $RepositoryRoot = (Resolve-Path $RepositoryRoot).Path +} + +$dbDirectories = @(Get-ChildItem -LiteralPath $RepositoryRoot -Directory | ForEach-Object { + $candidate = Join-Path $_.FullName "src/main/resources/db" + if (Test-Path -LiteralPath $candidate -PathType Container) { + [pscustomobject]@{ Module = $_.Name; Path = $candidate } + } +}) + +if ($Modules -and $Modules.Count -gt 0) { + $requested = @($Modules | ForEach-Object { $_.ToLowerInvariant() }) + $dbDirectories = @($dbDirectories | Where-Object { $_.Module.ToLowerInvariant() -in $requested }) + $found = @($dbDirectories | ForEach-Object { $_.Module.ToLowerInvariant() }) + $missingModules = @($requested | Where-Object { $_ -notin $found }) + if ($missingModules.Count -gt 0) { + Write-Error ("Unknown modules or missing db directories: " + ($missingModules -join ", ")) + exit 2 + } +} + +$definitions = @() +foreach ($dbDirectory in ($dbDirectories | Sort-Object Module)) { + foreach ($file in (Get-ChildItem -LiteralPath $dbDirectory.Path -Filter "*.sql" -File | Sort-Object FullName)) { + $relativePath = $file.FullName.Substring($RepositoryRoot.Length + 1).Replace('\', '/') + $sql = Get-Content -Raw -LiteralPath $file.FullName + $definitions += @(Get-CreateTableDefinitions -Sql $sql -Source $relativePath) + } +} + +$result = Get-AuditResult -Definitions $definitions +if ($Format -eq "Json") { + $jsonValue = if ($SummaryOnly) { $result.Summary } else { $result } + Write-Output ($jsonValue | ConvertTo-Json -Depth 6) +} else { + Write-Output "Database comment audit" + Write-Output "Repository: $RepositoryRoot" + Write-Output "Database/API access: none" + Write-Output ("Table definitions: {0}, commented: {1}, coverage: {2}%" -f $result.Summary.TableDefinitions, $result.Summary.CommentedTables, $result.Summary.TableCoveragePercent) + Write-Output ("Columns: {0}, commented: {1}, coverage: {2}%" -f $result.Summary.Columns, $result.Summary.CommentedColumns, $result.Summary.ColumnCoveragePercent) + Write-Output ("Issues: {0}" -f $result.Summary.Issues) + if (-not $SummaryOnly) { + foreach ($issue in $result.Issues) { + $target = if ([string]::IsNullOrWhiteSpace($issue.Column)) { $issue.Table } else { "$($issue.Table).$($issue.Column)" } + Write-Output ("[{0}] {1} ({2})" -f $issue.Code, $target, $issue.Source) + } + } +} + +if ($FailOnIssues -and $result.Summary.Issues -gt 0) { + exit 1 +} +exit 0 diff --git a/scripts/local-db-full-acceptance.ps1 b/scripts/local-db-full-acceptance.ps1 index d5419d8d4..a9faa068b 100644 --- a/scripts/local-db-full-acceptance.ps1 +++ b/scripts/local-db-full-acceptance.ps1 @@ -11,8 +11,8 @@ $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 = 'E86BB8DE105E9B59736FFCD180CBA902F2E3F5032C7EFF2E3DC3B24B289360A0' -$ExpectedLines = 7803 +$ExpectedSha256 = '03234DF522426213FA64768FDF35182DC315F2E358E5FD993AA24A2E99936D63' +$ExpectedLines = 13775 if ($TargetHost -notin @('127.0.0.1', 'localhost')) { [Console]::Error.WriteLine('Refusing non-local database; only 127.0.0.1 or localhost is allowed.') @@ -32,6 +32,25 @@ 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") + 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') + exit 2 +} $sequence = @( 'empty: base.sql', 'replay: base.sql + input.sql + input.sql', @@ -40,11 +59,20 @@ $sequence = @( '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' 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 } +$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") + exit 2 +} + $run = 0 function Invoke-Stage([string]$Name, [string]$File) { $script = Join-Path $Acceptance $File diff --git a/warehouse/src/main/resources/db/audit-outbox.sql b/warehouse/src/main/resources/db/audit-outbox.sql index e678802a7..88bc05479 100644 --- a/warehouse/src/main/resources/db/audit-outbox.sql +++ b/warehouse/src/main/resources/db/audit-outbox.sql @@ -2,14 +2,14 @@ USE baiyue_warehouse; -- 可靠审计本地 Outbox;与仓储业务表位于同一数据库。 CREATE TABLE IF NOT EXISTS sys_audit_outbox ( - event_id VARCHAR(64) NOT NULL, event_type VARCHAR(64) NOT NULL, resource_type VARCHAR(64) NOT NULL, - resource_id VARCHAR(128) NULL, service_name VARCHAR(64) NULL, payload_json JSON NOT NULL, - status VARCHAR(16) NOT NULL DEFAULT 'PENDING', attempt_count INT NOT NULL DEFAULT 0, - compensation_count INT NOT NULL DEFAULT 0, - next_retry_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), lease_owner VARCHAR(128) NULL, - lease_until DATETIME(3) NULL, last_error VARCHAR(1000) NULL, dead_at DATETIME(3) NULL, - sent_at DATETIME(3) NULL, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + event_id VARCHAR(64) NOT NULL COMMENT '审计事件全局唯一ID', event_type VARCHAR(64) NOT NULL COMMENT '审计事件类型', resource_type VARCHAR(64) NOT NULL COMMENT '被审计资源类型', + resource_id VARCHAR(128) NULL COMMENT '被审计资源ID', service_name VARCHAR(64) NULL COMMENT '产生事件的服务名', payload_json JSON NOT NULL COMMENT '待投递审计事件载荷', + status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT '投递状态:PENDING待发送、PROCESSING处理中、SENT已发送、FAILED失败', attempt_count INT NOT NULL DEFAULT 0 COMMENT '发送尝试次数', + compensation_count INT NOT NULL DEFAULT 0 COMMENT '补偿处理次数', + next_retry_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '下次允许重试时间', lease_owner VARCHAR(128) NULL COMMENT '当前租约持有实例', + lease_until DATETIME(3) NULL COMMENT '当前租约截止时间', last_error VARCHAR(1000) NULL COMMENT '最近一次发送错误', dead_at DATETIME(3) NULL COMMENT '进入死信时间', + sent_at DATETIME(3) NULL COMMENT '发送成功时间', 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 (event_id), KEY idx_audit_outbox_publish (status, next_retry_at, lease_until, created_at), KEY idx_audit_outbox_retention (status, sent_at) ) ENGINE=InnoDB COMMENT='可靠审计事件本地发送箱'; diff --git a/warehouse/src/main/resources/db/warehouse-partner.sql b/warehouse/src/main/resources/db/warehouse-partner.sql index 9608f009e..b70707d67 100644 --- a/warehouse/src/main/resources/db/warehouse-partner.sql +++ b/warehouse/src/main/resources/db/warehouse-partner.sql @@ -1,25 +1,25 @@ SET NAMES utf8mb4; USE baiyue_warehouse; CREATE TABLE IF NOT EXISTS wh_partner ( - id BIGINT UNSIGNED NOT NULL, tenant_id BIGINT UNSIGNED NOT NULL, + id BIGINT UNSIGNED NOT NULL COMMENT '往来单位ID(雪花ID)', tenant_id BIGINT UNSIGNED NOT NULL COMMENT '平台公共往来单位固定使用 tenant_id=0', data_scope_id BIGINT UNSIGNED NOT NULL COMMENT '所属业务数据范围', - partner_code VARCHAR(50) NOT NULL, partner_name VARCHAR(128) NOT NULL, + partner_code VARCHAR(50) NOT NULL COMMENT '平台内唯一往来单位编码', partner_name VARCHAR(128) NOT NULL COMMENT '往来单位名称', partner_type VARCHAR(20) NOT NULL COMMENT 'CUSTOMER、SUPPLIER或BOTH', - contact_name VARCHAR(64) NOT NULL, contact_phone VARCHAR(32) NOT NULL, - contact_email VARCHAR(128) NULL, country_code CHAR(2) NOT NULL, - province VARCHAR(64) NULL, city VARCHAR(64) NOT NULL, district VARCHAR(64) NULL, - address_line VARCHAR(255) NOT NULL, postal_code VARCHAR(20) NULL, - status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE', remark VARCHAR(500) NULL, - created_by BIGINT UNSIGNED NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), updated_by BIGINT UNSIGNED NULL, - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - deleted TINYINT(1) NOT NULL DEFAULT 0, version INT NOT NULL DEFAULT 0, + contact_name VARCHAR(64) NOT NULL COMMENT '联系人姓名', contact_phone VARCHAR(32) NOT NULL COMMENT '联系电话', + contact_email VARCHAR(128) NULL COMMENT '联系邮箱', country_code CHAR(2) NOT NULL COMMENT '国家二字码', + province VARCHAR(64) NULL COMMENT '省或州', city VARCHAR(64) NOT NULL COMMENT '城市', district VARCHAR(64) NULL COMMENT '区县', + address_line VARCHAR(255) NOT NULL COMMENT '详细地址', postal_code VARCHAR(20) NULL COMMENT '邮政编码', + status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '状态:ACTIVE启用、DISABLED停用', remark VARCHAR(500) NULL COMMENT '备注', + created_by BIGINT UNSIGNED NULL COMMENT '创建人ID', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', updated_by BIGINT UNSIGNED NULL COMMENT '最后修改人ID', + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间', + deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记:0未删除、1已删除', version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', PRIMARY KEY(id), UNIQUE KEY uk_wh_partner_tenant_code(tenant_id,partner_code,deleted), KEY idx_wh_partner_tenant_status(tenant_id,status,deleted) ) ENGINE=InnoDB COMMENT='平台统一维护、供全部客户共享的往来单位主数据'; CREATE TABLE IF NOT EXISTS wh_partner_type ( - partner_id BIGINT UNSIGNED NOT NULL, tenant_id BIGINT UNSIGNED NOT NULL, - data_scope_id BIGINT UNSIGNED NOT NULL, partner_type VARCHAR(32) NOT NULL, + partner_id BIGINT UNSIGNED NOT NULL COMMENT '往来单位ID', tenant_id BIGINT UNSIGNED NOT NULL COMMENT '平台公共往来单位固定使用 tenant_id=0', + data_scope_id BIGINT UNSIGNED NOT NULL COMMENT '所属业务数据范围,平台公共数据为0', partner_type VARCHAR(32) NOT NULL COMMENT '稳定业务类型:CUSTOMER客户、SHIPPER发货方等', PRIMARY KEY(partner_id,partner_type), KEY idx_wh_partner_type_scope(tenant_id,data_scope_id,partner_type,partner_id) ) ENGINE=InnoDB COMMENT='平台共享往来单位稳定业务类型'; diff --git a/warehouse/src/main/resources/db/warehouse.sql b/warehouse/src/main/resources/db/warehouse.sql index 4ba2a6fd6..044e55cb3 100644 --- a/warehouse/src/main/resources/db/warehouse.sql +++ b/warehouse/src/main/resources/db/warehouse.sql @@ -77,24 +77,38 @@ CREATE TABLE IF NOT EXISTS wh_service_capability ( -- 不使用跨服务数据库外键;仓库聚合内的归属由服务代码通过 tenant_id 与 warehouse_id 共同校验。 CREATE TABLE IF NOT EXISTS wh_overseas_storage_fee_rule ( - id BIGINT UNSIGNED NOT NULL, tenant_id BIGINT UNSIGNED NOT NULL, customer_name VARCHAR(128) NOT NULL, - currency CHAR(3) NOT NULL DEFAULT 'USD', initialization_key VARCHAR(191) NULL, - created_by BIGINT UNSIGNED NULL, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_by BIGINT UNSIGNED NULL, updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - deleted TINYINT(1) NOT NULL DEFAULT 0, version INT NOT NULL DEFAULT 0, - active_customer_id BIGINT UNSIGNED GENERATED ALWAYS AS (CASE WHEN deleted=0 THEN tenant_id ELSE NULL END) STORED, + id BIGINT UNSIGNED NOT NULL COMMENT '海外仓费用规则ID(雪花ID)', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键;客户费用规则不得使用0', + customer_name VARCHAR(128) NOT NULL COMMENT '客户名称快照', + currency CHAR(3) NOT NULL DEFAULT 'USD' COMMENT '费用币种三字码;首版为USD', + initialization_key VARCHAR(191) NULL COMMENT '初始化或导入幂等键', + created_by BIGINT UNSIGNED NULL COMMENT '创建人ID', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', + updated_by BIGINT UNSIGNED NULL COMMENT '最后修改人ID', + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间', + deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记:0未删除、1已删除', + version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', + active_customer_id BIGINT UNSIGNED GENERATED ALWAYS AS (CASE WHEN deleted=0 THEN tenant_id ELSE NULL END) STORED COMMENT '未删除规则的客户唯一索引值', PRIMARY KEY(id), UNIQUE KEY uk_overseas_fee_active_customer(active_customer_id), KEY idx_overseas_fee_page(deleted,updated_at,id) ) ENGINE=InnoDB COMMENT='客户海外仓费用规则'; CREATE TABLE IF NOT EXISTS wh_overseas_storage_fee_tier ( - id BIGINT UNSIGNED NOT NULL, tenant_id BIGINT UNSIGNED NOT NULL, rule_id BIGINT UNSIGNED NOT NULL, - start_day SMALLINT UNSIGNED NOT NULL, end_day SMALLINT UNSIGNED NULL, - fee_type VARCHAR(32) NOT NULL DEFAULT 'STORAGE', billing_unit VARCHAR(16) NOT NULL DEFAULT 'CBM', - unit_price DECIMAL(18,6) NOT NULL, sort_no SMALLINT UNSIGNED NOT NULL, - created_by BIGINT UNSIGNED NULL, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - updated_by BIGINT UNSIGNED NULL, updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - deleted TINYINT(1) NOT NULL DEFAULT 0, version INT NOT NULL DEFAULT 0, - active_start_day SMALLINT UNSIGNED GENERATED ALWAYS AS (CASE WHEN deleted=0 THEN start_day ELSE NULL END) STORED, + id BIGINT UNSIGNED NOT NULL COMMENT '海外仓费用阶梯ID(雪花ID)', + tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键;与规则归属一致', + rule_id BIGINT UNSIGNED NOT NULL COMMENT '所属海外仓费用规则ID', + start_day SMALLINT UNSIGNED NOT NULL COMMENT '计费阶梯起始存储天数,含边界', + end_day SMALLINT UNSIGNED NULL COMMENT '计费阶梯结束存储天数,含边界;NULL表示以上', + fee_type VARCHAR(32) NOT NULL DEFAULT 'STORAGE' COMMENT '费用类型;首版为STORAGE仓储费', + billing_unit VARCHAR(16) NOT NULL DEFAULT 'CBM' COMMENT '计费单位;首版为CBM立方米', + unit_price DECIMAL(18,6) NOT NULL COMMENT '每计费单位单价,非负', + sort_no SMALLINT UNSIGNED NOT NULL COMMENT '阶梯展示和计算顺序', + created_by BIGINT UNSIGNED NULL COMMENT '创建人ID', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', + updated_by BIGINT UNSIGNED NULL COMMENT '最后修改人ID', + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间', + deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记:0未删除、1已删除', + version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', + active_start_day SMALLINT UNSIGNED GENERATED ALWAYS AS (CASE WHEN deleted=0 THEN start_day ELSE NULL END) STORED COMMENT '未删除阶梯的起始天唯一索引值', PRIMARY KEY(id), UNIQUE KEY uk_overseas_fee_tier_start(rule_id,fee_type,active_start_day), KEY idx_overseas_fee_tier_rule(tenant_id,rule_id,deleted,sort_no,id), CONSTRAINT chk_overseas_fee_tier_days CHECK (start_day>=1 AND (end_day IS NULL OR end_day>=start_day)), CONSTRAINT chk_overseas_fee_tier_price CHECK (unit_price>=0)