diff --git a/common-event-contract/src/main/java/com/baiyue/common/event/WalletSettlementReference.java b/common-event-contract/src/main/java/com/baiyue/common/event/WalletSettlementReference.java
index 71fe1fd86..5824c0f62 100644
--- a/common-event-contract/src/main/java/com/baiyue/common/event/WalletSettlementReference.java
+++ b/common-event-contract/src/main/java/com/baiyue/common/event/WalletSettlementReference.java
@@ -1,4 +1,13 @@
package com.baiyue.common.event;
-/** Finance-issued identity of a successful consumption, carried by its immutable fee source. */
-public record WalletSettlementReference(String commandEventId, String holdId, String ledgerId) { }
+/** 成功扣款凭据;资金业务身份独立于费用对应的实际服务订单。 */
+public record WalletSettlementReference(String commandEventId, String holdId, String ledgerId,
+ @com.fasterxml.jackson.annotation.JsonInclude(com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL)
+ String walletBusinessType,
+ @com.fasterxml.jackson.annotation.JsonInclude(com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL)
+ String walletBusinessNo) {
+ /** 原有资金身份与服务订单一致的来源继续使用原三项凭据。 */
+ public WalletSettlementReference(String commandEventId, String holdId, String ledgerId) {
+ this(commandEventId, holdId, ledgerId, null, null);
+ }
+}
diff --git a/common-event-contract/src/main/java/com/baiyue/common/event/package-info.java b/common-event-contract/src/main/java/com/baiyue/common/event/package-info.java
index 785251b9d..41089c3f4 100644
--- a/common-event-contract/src/main/java/com/baiyue/common/event/package-info.java
+++ b/common-event-contract/src/main/java/com/baiyue/common/event/package-info.java
@@ -1,4 +1,11 @@
/**
* 国内与海外业务单元共享的稳定事件契约。
+ *
+ *
海外费用的 walletSettlement 引用成功扣款的 commandEventId、holdId、ledgerId。
+ * 独立资金流程另外携带 walletBusinessType、walletBusinessNo:人工费用为
+ * OVERSEAS_MANUAL_FEE / 来源费用ID-冻结次数,仓租为 STORAGE_RENT / 出库预占ID。
+ * 外层 businessType、businessNo 始终表示实际服务订单,不能替换为资金流程编号。
+ * Finance 同时核对冻结、实扣流水、成功指令结果及客户金额,一个 hold 只能关联一个费用来源。
+ * 资金身份与服务订单一致的既有引用省略这两个字段,保持既有快照哈希。
*/
package com.baiyue.common.event;
diff --git a/common-event-contract/src/test/java/com/baiyue/common/event/WalletSettlementReferenceTest.java b/common-event-contract/src/test/java/com/baiyue/common/event/WalletSettlementReferenceTest.java
new file mode 100644
index 000000000..1183d7d88
--- /dev/null
+++ b/common-event-contract/src/test/java/com/baiyue/common/event/WalletSettlementReferenceTest.java
@@ -0,0 +1,22 @@
+package com.baiyue.common.event;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.Test;
+import static org.assertj.core.api.Assertions.assertThat;
+
+class WalletSettlementReferenceTest {
+ private final ObjectMapper json = new ObjectMapper();
+
+ @Test
+ void oldReferencesRetainExactHashShape() throws Exception {
+ String original = "{\"commandEventId\":\"c1\",\"holdId\":\"9\",\"ledgerId\":\"72\"}";
+ var reference = json.readValue(original, WalletSettlementReference.class);
+ assertThat(json.readTree(json.writeValueAsString(reference))).isEqualTo(json.readTree(original));
+ }
+
+ @Test
+ void dedicatedFundingIdentitySurvivesRoundTrip() throws Exception {
+ var reference = new WalletSettlementReference("c1", "9", "72", "STORAGE_RENT", "445");
+ assertThat(json.readValue(json.writeValueAsString(reference), WalletSettlementReference.class)).isEqualTo(reference);
+ }
+}
diff --git a/deploy/sql/20261022-海外费用仓租数据库变更汇总.md b/deploy/sql/20261022-海外费用仓租数据库变更汇总.md
new file mode 100644
index 000000000..74645a113
--- /dev/null
+++ b/deploy/sql/20261022-海外费用仓租数据库变更汇总.md
@@ -0,0 +1,39 @@
+# 海外费用与仓租数据库定义汇总
+
+本次功能的数据库定义已统一到下面两份 SQL,五份重复版本脚本和两份专项汇总 SQL 已移除。仅整理文件,未连接数据库、执行 SQL 或部署。
+
+## 两个统一入口
+
+| 文件 | 相关数据库 | 本次内容 |
+|---|---|---|
+| [头程数据库统一增量-MySQL8.sql](头程数据库统一增量-MySQL8.sql) | `baiyue_iam` | 海外费用管理菜单及默认平台管理员授权;保留既有财务编辑、审核按钮和授权 |
+| [海外区域统一增量-MySQL8.sql](海外区域统一增量-MySQL8.sql) | `baiyue_lastleg` | 资金与仓租四表、费用关联及换标快照、审核人名称、采购退款请求幂等字段与唯一索引 |
+
+两份文件虽然名称包含“增量”,仍只用于对应区域的空库初始化。新增列、索引和约束直接维护在最终 `CREATE TABLE` 中,菜单及授权使用初始化种子;不追加存量改造、权限版本回填或动态迁移段。目标 schema 必须为空,按文件原有初始化流程执行;不能向已有业务库重放。
+
+本次不再提供这项功能的独立升级脚本。已有库升级必须按实际结构和明确授权另行准备、审阅受控增量,不能把删除重复文件理解为已有库已完成升级。
+
+## 海外表和字段
+
+| 类型 | 表 / 字段 | 用途 |
+|---|---|---|
+| 新表 | `ll_finance_fee_funding` | 人工费用冻结、审核扣款、释放及财务流水引用 |
+| 新表 | `ll_storage_rent_lot` | 实际入库计租原批次、数量及入库时间 |
+| 新表 | `ll_storage_rent_inbound` | 头程每次实收凭证去重,关联同单同仓计租批次 |
+| 新表 | `ll_storage_rent_share` | 库存与原计租批次份额关联,保留原库龄 |
+| 新表 | `ll_storage_rent_preparation` | 出库计租快照、汇率、资金预占及实际出库记录 |
+| 可空字段 | `ll_destuff_pallet_cost_allocation.fee_source_id` | `BIGINT NULL`,允许人工审核后关联海外费用来源 |
+| 字段 | `ll_return_sku_conversion.storage_rent_shares_json` | `LONGTEXT NULL`,退货 SKU 换标的原仓租份额快照 |
+| 字段 | `ll_finance_fee_source.reviewer_name` | `VARCHAR(255) NULL`,审核人名称快照 |
+| 字段 | `ll_overseas_procurement_fee.idempotency_key` | `VARCHAR(128) NULL`,采购退款请求幂等键 |
+| 唯一索引 | `uk_ll_procurement_refund_request(order_id,purpose,idempotency_key)` | 同一采购订单、用途和退款请求去重 |
+
+## 国内菜单和权限
+
+海外业务 → 海外费用管理,菜单 ID `5117200`、权限 `platform:overseas-fee:list`、路由 `/overseas-business/fees`;默认仅授予 `PLATFORM_ADMIN`。编辑和审核分别复用 `platform:finance:fee-source:edit` 与 `platform:finance:fee-source:review`,列表权限不包含写操作。国内基线已包含两项既有按钮及管理员授权。
+
+已有库的 [费用写权限修复](overseas-fee-canonical-write-permissions-repair.sql) 是单独的定向补丁,继续保留原适用条件和授权边界,不是结构升级或菜单初始化入口。其他独立修复脚本与历史迁移不在本次清理范围。
+
+费用项目继续使用 Firstleg 现有目录及 `FEE_TYPE_077 / 仓储费`,不重复插入项目、不批量覆盖客户专价、不回填历史仓租或费用、不修改钱包余额。
+
+配套服务、页面、配置及验收边界见 [首版发布验收](../../lastleg-service/docs/overseas-fees-storage-rent-release.md)。
diff --git a/deploy/sql/README.md b/deploy/sql/README.md
index fda18d5bf..5674b6160 100644
--- a/deploy/sql/README.md
+++ b/deploy/sql/README.md
@@ -4,6 +4,14 @@
货柜关联已放舱订舱时不再限制 ISO 柜号格式,Firstleg 模块基线与国内统一空库基线已移除 `chk_container_booking_iso6346`,保留同订舱柜号唯一键。既有 TEST 发布前需核对并单独授权移除该约束:`ALTER TABLE baiyue_firstleg.fl_container_booking DROP CHECK chk_container_booking_iso6346;`。配套发布 Firstleg 与 Admin;不重放空库初始化、不修改业务数据。本次仅准备源码与 SQL 定义,未执行环境 SQL 或部署。
+`overseas-storage-rent-zero-fee-repair.sql` 为免费仓租的定向结构修复,固定 `baiyue_lastleg.ll_finance_fee_source`:先检查列类型、既有金额约束及存量行,再以单次原子 ALTER 仅放行自动应收仓租的原币及结算双零金额和空汇率,其余费用仍须正额及有效汇率。不更新费用、余额或库存;禁止重放空库初始化。源码基线与海外统一空库基线同步。脚本待审阅执行,文件存在不代表已执行。
+
+`overseas-fee-remark-add.sql` 为海外人工费用说明的独立结构增量,固定目标 `baiyue_lastleg.ll_finance_fee_source`,仅在缺失时增加允许为空的 `fee_remark VARCHAR(500)`,保留旧费用与审核记录,不回填、不扣款、不改菜单。Lastleg 源基线与海外统一空库基线已同步;已有开发库须逐项授权执行本脚本后才能加载配套 Lastleg 版本。文件准备不表示已执行。
+
+`finance-overseas-fee-hash-width-repair.sql` 用于明确授权的本机开发环境海外费用推送修复,固定目标 `baiyue_finance`。先核对 `fin_inbox_event.payload_hash`、`fin_fee_source.source_hash` 两列;仅将不足71位的长度扩至71位,兼容 `sha256:` 前缀,保留空值约束和字符集排序规则,不更新业务数据。缺列、异常类型或小于64位时停止;已有足够长度则跳过。使用独立连接、遇错停止,执行后回查结构,再通过原费用事件重试验证,禁止重新生成费用或扣款。脚本包含执行后移除的专用校验过程;创建或执行失败须停止,不扩大修复范围。文件准备不表示已执行。
+
+`warehouse-supplier-query-structure-repair.sql` 用于明确授权的本机开发环境供应商查询结构修复,固定目标 `baiyue_warehouse`:仅在缺失时新增 `wh_supplier.trucking_scope`,并按当前模块基线创建缺失的 `wh_supplier_carrier` 空表。结构存在则跳过,不更新供应商类别、费用或资金,不推断历史拖车的国内/海外归属。使用独立连接、遇错停止,执行后回查结构并通过正式供应商列表 API 验证;不需要重启服务。文件准备不表示已获授权或已执行。
+
仓储转运到仓取货、本土卡车接入现有排车的**可重复执行汇总SQL**为 `lastleg-dispatch-transport-tasks.sql`,文件内固定 `USE baiyue_lastleg`,对已存在字段、表安全跳过。新增 `ll_dispatch_order.task_type` 字段、`ll_dispatch_truck_task`、`ll_dispatch_transport_create`、`ll_dispatch_transport_command` 三张表,并确保既有正式排车单号日流水表存在;不回填或删除业务数据。前置是目标库已具备现有整车排车主表、FleetTrip、取货任务及本土卡车原单结构。按序先执行此 SQL,再配套发布 Lastleg 与 Admin;取消释放有效原单关联、完成保留关联。`migrations/overseas/baiyue_lastleg/V32__dispatch_transport_tasks.sql` 是迁移系统中的新增结构记录。本次仅在已授权本机执行并验收,不代表远端已部署。
排车单号使用`PCyyyyMMdd0001`,日期取`Asia/Shanghai`,全平台跨客户/仓库共用独立日流水。`ll_dispatch_daily_sequence`同步Lastleg源基线与海外空库基线;已有结构的定向新增入口为`lastleg-dispatch-daily-sequence.sql`,只建序列表,不改已有业务号、不回填历史、不重放空库初始化。源码和DDL须配套发布;文件存在不表示已执行或部署。流水在独立短事务分配,回滚或取消不复用,四位范围每日0001至9999,准确单量以主单统计为准。
@@ -69,7 +77,7 @@ V13 收货覆盖由 `lastleg-service/src/main/resources/db/appointment-receiving
| 区域 | 完整入口 | 服务 schema | 表数 |
|---|---|---|---:|
| 国内 | `头程数据库统一增量-MySQL8.sql` | `baiyue_auth`、`baiyue_iam`、`baiyue_order`、`baiyue_warehouse`、`baiyue_firstleg`、`baiyue_file`、`baiyue_audit`、`baiyue_finance` | 229 |
-| 海外 | `海外区域统一增量-MySQL8.sql` | `baiyue_lastleg`、`baiyue_lastlag_warehouse` | 234 |
+| 海外 | `海外区域统一增量-MySQL8.sql` | `baiyue_lastleg`、`baiyue_lastlag_warehouse` | 238 |
- 所有服务位于同一测试环境;两份文件只分组组织服务 schema,不代表两个部署区域。`baiyue_lastlag_warehouse` 保留当前正式的 `lastlag` 拼写。
@@ -92,7 +100,7 @@ V13 收货覆盖由 `lastleg-service/src/main/resources/db/appointment-receiving
5. DDL 隐式提交,不可整体回滚。失败后停止并保留错误;经另行授权准备空库后从头执行,不针对半初始化库追加迁移或从失败行续跑。
6. Firstleg 测量项与拆分项互为外键:建空表期间暂设会话 `foreign_key_checks=0`,随后立即恢复 `1`,再插入种子。失败时关闭连接。两边外键完整保留在 CREATE TABLE 内。
-成功时只在最末输出一行 `initialization_status=PASS`,并带 `region=CN/OVERSEAS`、`initialized_tables=229/234`。必须同时确认客户端退出码 0;忽略错误后出现的 PASS 不算成功。
+成功时只在最末输出一行 `initialization_status=PASS`,并带 `region=CN/OVERSEAS`、`initialized_tables=229/238`。必须同时确认客户端退出码 0;忽略错误后出现的 PASS 不算成功。
## 平台管理员密码
@@ -192,3 +200,9 @@ node --test scripts/admin-test-env-modules/initialization-baseline-contract.test
## 统一业务单号
编号规则见 `../../../docs/业务单据编号契约.md`。Order、Warehouse、Lastleg 空库基线新增各自的 `ord_document_sequence`、`wh_document_sequence`、`ll_document_sequence`;Firstleg 与 Finance 复用本库编号表。Lastleg 空库基线移除无生产调用的客户日序列、逻辑秒时钟及旧尾程日序列表,保留排车日序列。采购费用新增可空 `idempotency_key` 及订单/用途/请求键唯一约束,用于退款重放,业务号统一使用 PF。此处只准备基线,不执行 SQL、回填或删除测试存量。
+
+海外费用与仓租首版统一维护在 `头程数据库统一增量-MySQL8.sql` 的 IAM 菜单/管理员授权种子及 `海外区域统一增量-MySQL8.sql` 的最终建表定义中。海外基线包含资金与仓租四表、拆柜成本可空费用引用、退货 SKU 换标仓租快照、审核人名称,以及采购退款请求幂等字段和唯一索引;同步模块源基线、国内权限种子及海外空库 238 表,复用现有 `FEE_TYPE_077 / 仓储费`,不新增费用项目种子。发布须同时更新 Firstleg、Lastleg、Admin 与海外 PDA;规则、配置及资金验收见 [首版发布验收](../../lastleg-service/docs/overseas-fees-storage-rent-release.md)。仅整理文件,未执行 SQL 或部署。
+
+本次重复的五份版本脚本和两份专项汇总 SQL 已移除,内容清单见 [20261022-海外费用仓租数据库变更汇总](20261022-海外费用仓租数据库变更汇总.md)。两份统一文件仍只用于空库初始化,不能向已有业务库重放;本次不再提供这项功能的独立升级脚本,已有库升级须另按实际结构和明确授权准备、审阅增量。其他独立修复补丁及历史迁移保持各自适用边界。
+
+采购方向扩展:`migrations/overseas/baiyue_lastleg/V34__procurement_direction.sql` 先于匹配 Lastleg 版本执行,为采购单增加 `procurement_direction`;历史订单默认海外采购回国内,不修改既有实物和资金记录。国内采购通过既有国内仓目录选起运收货仓;源基线与统一空库基线同步。本地验证不表示测试库已执行或服务已发布。
diff --git a/deploy/sql/finance-overseas-fee-hash-width-repair.sql b/deploy/sql/finance-overseas-fee-hash-width-repair.sql
new file mode 100644
index 000000000..a04ca476f
--- /dev/null
+++ b/deploy/sql/finance-overseas-fee-hash-width-repair.sql
@@ -0,0 +1,75 @@
+-- 本机开发环境海外费用推送结构修复。固定目标:baiyue_finance。
+-- 仅将两个既有摘要字段不足 71 位的长度扩至 71;不改数据、费用、账单或余额。
+-- 保留原字段的空值约束、字符集及排序规则;缺表、缺列或异常字段类型时拒绝执行。
+-- 执行须单独授权。使用独立连接、遇错停止;不得以本文件代替完整数据库初始化。
+USE baiyue_finance;
+
+-- 执行前只读核对;sha256: 前缀 7 位加摘要 64 位,共 71 位。
+SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE,
+ CHARACTER_SET_NAME, COLLATION_NAME
+FROM information_schema.COLUMNS
+WHERE TABLE_SCHEMA = 'baiyue_finance'
+ AND ((TABLE_NAME = 'fin_inbox_event' AND COLUMN_NAME = 'payload_hash')
+ OR (TABLE_NAME = 'fin_fee_source' AND COLUMN_NAME = 'source_hash'));
+
+DELIMITER $$
+CREATE PROCEDURE finance_fee_hash_width_repair_20261023()
+BEGIN
+ DECLARE checked_columns INT DEFAULT 0;
+ DECLARE column_index INT DEFAULT 0;
+ DECLARE target_table VARCHAR(64);
+ DECLARE target_column VARCHAR(64);
+ DECLARE current_length BIGINT;
+ DECLARE nullable_value VARCHAR(3);
+ DECLARE charset_value VARCHAR(64);
+ DECLARE collation_value VARCHAR(64);
+ DECLARE comment_value TEXT;
+
+ IF DATABASE() <> 'baiyue_finance' THEN
+ SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '目标必须是 baiyue_finance';
+ END IF;
+ SELECT COUNT(*) INTO checked_columns
+ FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = 'baiyue_finance' AND DATA_TYPE IN ('char', 'varchar')
+ AND CHARACTER_MAXIMUM_LENGTH >= 64
+ AND COLUMN_DEFAULT IS NULL AND EXTRA = ''
+ AND ((TABLE_NAME = 'fin_inbox_event' AND COLUMN_NAME = 'payload_hash')
+ OR (TABLE_NAME = 'fin_fee_source' AND COLUMN_NAME = 'source_hash'));
+ IF checked_columns <> 2 THEN
+ SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '摘要字段缺失或类型异常,停止修复';
+ END IF;
+
+ WHILE column_index < 2 DO
+ SET target_table = IF(column_index = 0, 'fin_inbox_event', 'fin_fee_source');
+ SET target_column = IF(column_index = 0, 'payload_hash', 'source_hash');
+ SELECT CHARACTER_MAXIMUM_LENGTH, IS_NULLABLE, CHARACTER_SET_NAME, COLLATION_NAME, COLUMN_COMMENT
+ INTO current_length, nullable_value, charset_value, collation_value, comment_value
+ FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = 'baiyue_finance' AND TABLE_NAME = target_table
+ AND COLUMN_NAME = target_column;
+ IF current_length < 71 THEN
+ SET @finance_fee_hash_width_ddl = CONCAT(
+ 'ALTER TABLE baiyue_finance.`', target_table, '` MODIFY COLUMN `', target_column,
+ '` VARCHAR(71) CHARACTER SET ', charset_value, ' COLLATE ', collation_value,
+ IF(nullable_value = 'YES', ' NULL', ' NOT NULL'),
+ ' COMMENT ', QUOTE(comment_value));
+ PREPARE finance_fee_hash_width_stmt FROM @finance_fee_hash_width_ddl;
+ EXECUTE finance_fee_hash_width_stmt;
+ DEALLOCATE PREPARE finance_fee_hash_width_stmt;
+ END IF;
+ SET column_index = column_index + 1;
+ END WHILE;
+END$$
+DELIMITER ;
+
+CALL finance_fee_hash_width_repair_20261023();
+DROP PROCEDURE finance_fee_hash_width_repair_20261023;
+SET @finance_fee_hash_width_ddl = NULL;
+
+-- 执行后两列长度均应至少为 71;结构已正确时不执行 ALTER。
+SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE,
+ CHARACTER_MAXIMUM_LENGTH >= 71 AS width_ok
+FROM information_schema.COLUMNS
+WHERE TABLE_SCHEMA = 'baiyue_finance'
+ AND ((TABLE_NAME = 'fin_inbox_event' AND COLUMN_NAME = 'payload_hash')
+ OR (TABLE_NAME = 'fin_fee_source' AND COLUMN_NAME = 'source_hash'));
diff --git a/deploy/sql/migrations/overseas/baiyue_lastleg/V34__procurement_direction.sql b/deploy/sql/migrations/overseas/baiyue_lastleg/V34__procurement_direction.sql
new file mode 100644
index 000000000..3f38b7bad
--- /dev/null
+++ b/deploy/sql/migrations/overseas/baiyue_lastleg/V34__procurement_direction.sql
@@ -0,0 +1,9 @@
+-- 先于新版本 Lastleg 上线执行。历史订单保持海外采购回国内,不改数量、资金或交寄事实。
+SET @procurement_direction_ddl = IF(
+ (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = 'll_overseas_procurement_order' AND COLUMN_NAME = 'procurement_direction') = 0,
+ 'ALTER TABLE ll_overseas_procurement_order ADD COLUMN procurement_direction VARCHAR(32) NOT NULL DEFAULT ''OVERSEAS_TO_DOMESTIC'' COMMENT ''采购方向:海外采购回国内/国内采购发海外''',
+ 'SELECT 1');
+PREPARE procurement_direction_stmt FROM @procurement_direction_ddl;
+EXECUTE procurement_direction_stmt;
+DEALLOCATE PREPARE procurement_direction_stmt;
diff --git a/deploy/sql/overseas-fee-canonical-write-permissions-repair.sql b/deploy/sql/overseas-fee-canonical-write-permissions-repair.sql
new file mode 100644
index 000000000..f4b432857
--- /dev/null
+++ b/deploy/sql/overseas-fee-canonical-write-permissions-repair.sql
@@ -0,0 +1,121 @@
+-- 海外费用既有写按钮最小修复;MySQL 8,目标只能是 baiyue_iam。
+-- 仅恢复基线 5105024/5105025 及既有 4000001 PLATFORM_ADMIN 缺失授权。
+-- 不创建角色/成员,不授予其他角色,不新增 overseas-fee:edit/review 别名。
+-- 在支持 DELIMITER、遇错停止的客户端同连接整份执行;执行期间暂停相关角色权限维护。
+-- 不在其他未提交事务中调用。所有业务DML及subject版本变更同事务,任何异常回滚。
+USE baiyue_iam;
+SET NAMES utf8mb4;
+SELECT DATABASE() AS target_database;
+
+DROP PROCEDURE IF EXISTS repair_overseas_fee_canonical_write;
+DELIMITER $$
+CREATE PROCEDURE repair_overseas_fee_canonical_write()
+BEGIN
+ DECLARE locked_role BIGINT;
+ DECLARE changed_rows BIGINT DEFAULT 0;
+ DECLARE EXIT HANDLER FOR SQLEXCEPTION
+ BEGIN
+ ROLLBACK;
+ RESIGNAL;
+ END;
+
+ START TRANSACTION;
+ IF DATABASE()<>'baiyue_iam' THEN
+ SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='Wrong schema: expected baiyue_iam';
+ END IF;
+ IF (SELECT COUNT(*) FROM iam_role WHERE id=4000001 AND tenant_id=0 AND app_code='PLATFORM'
+ AND role_code='PLATFORM_ADMIN' AND role_type='SYSTEM' AND status='ACTIVE' AND deleted=0)<>1
+ OR EXISTS(SELECT 1 FROM iam_role WHERE role_code='PLATFORM_ADMIN' AND id<>4000001) THEN
+ SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='Existing PLATFORM_ADMIN identity/status mismatch; no role will be created';
+ END IF;
+ SELECT id INTO locked_role FROM iam_role WHERE id=4000001 FOR UPDATE;
+
+ -- 新建缺失按钮使用当前客户应收父菜单;只接受已核对的既有财务菜单,不修复其他菜单。
+ IF (SELECT COUNT(*) FROM iam_permission WHERE id=5105041 AND tenant_id=0 AND app_code='PLATFORM'
+ AND permission_type='MENU' AND permission_code='platform:finance:customer-arap:list'
+ AND status='ACTIVE' AND deleted=0)<>1
+ OR EXISTS(SELECT 1 FROM iam_permission WHERE permission_code='platform:finance:customer-arap:list' AND id<>5105041) THEN
+ SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='Existing customer AR/AP parent menu is missing or mismatched';
+ END IF;
+ IF EXISTS(SELECT 1 FROM iam_permission WHERE
+ (id=5105024 AND NOT(permission_code<=>'platform:finance:fee-source:review'))
+ OR (id=5105025 AND NOT(permission_code<=>'platform:finance:fee-source:edit'))
+ OR (permission_code='platform:finance:fee-source:review' AND id<>5105024)
+ OR (permission_code='platform:finance:fee-source:edit' AND id<>5105025)
+ OR (id IN(5105024,5105025) AND (tenant_id<>0 OR app_code<>'PLATFORM'
+ OR permission_type<>'BUTTON' OR built_in<>1 OR parent_id NOT IN(5105000,5105041)
+ OR route_path IS NOT NULL
+ OR NOT(http_method<=>IF(id=5105024,'POST','PUT'))))) THEN
+ SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='Canonical fee button identity/type/metadata conflict';
+ END IF;
+ IF EXISTS(SELECT 1 FROM iam_permission child LEFT JOIN iam_permission parent ON parent.id=child.parent_id
+ WHERE child.id IN(5105024,5105025) AND (parent.id IS NULL OR parent.tenant_id<>0
+ OR parent.app_code<>'PLATFORM' OR parent.permission_type<>'MENU' OR parent.status<>'ACTIVE' OR parent.deleted<>0
+ OR NOT(parent.permission_code<=>IF(child.parent_id=5105000,'platform:finance','platform:finance:customer-arap:list')))) THEN
+ SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='Existing fee button parent is invalid';
+ END IF;
+ IF EXISTS(SELECT 1 FROM iam_role_permission WHERE permission_id IN(5105024,5105025) AND tenant_id<>0)
+ OR EXISTS(SELECT 1 FROM iam_membership_role mr LEFT JOIN iam_membership m ON m.id=mr.membership_id
+ WHERE mr.role_id=4000001 AND (mr.tenant_id<>0 OR m.id IS NULL OR m.tenant_id<>0)) THEN
+ SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='Existing canonical permission or administrator membership scope mismatch';
+ END IF;
+ -- 恢复停用/缺失资源也会恢复其他角色的旧授权;这种情况必须单独核对,不能随本补丁扩权。
+ IF EXISTS(SELECT 1 FROM iam_role_permission rp LEFT JOIN iam_permission p ON p.id=rp.permission_id
+ WHERE rp.permission_id IN(5105024,5105025) AND rp.role_id<>4000001
+ AND (p.id IS NULL OR p.status<>'ACTIVE' OR p.deleted<>0)) THEN
+ SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='Restoring fee button would affect another role; review separately';
+ END IF;
+
+ INSERT INTO iam_permission
+ (id,tenant_id,app_code,permission_type,parent_id,permission_code,permission_name,route_path,http_method,sort_no,status,built_in,deleted)
+ SELECT 5105024,0,'PLATFORM','BUTTON',5105041,'platform:finance:fee-source:review','审核财务费用来源',NULL,'POST',150,'ACTIVE',1,0
+ WHERE NOT EXISTS(SELECT 1 FROM iam_permission WHERE id=5105024);
+ SET changed_rows=changed_rows+ROW_COUNT();
+ INSERT INTO iam_permission
+ (id,tenant_id,app_code,permission_type,parent_id,permission_code,permission_name,route_path,http_method,sort_no,status,built_in,deleted)
+ SELECT 5105025,0,'PLATFORM','BUTTON',5105041,'platform:finance:fee-source:edit','编辑财务费用来源',NULL,'PUT',160,'ACTIVE',1,0
+ WHERE NOT EXISTS(SELECT 1 FROM iam_permission WHERE id=5105025);
+ SET changed_rows=changed_rows+ROW_COUNT();
+ UPDATE iam_permission SET status='ACTIVE',deleted=0
+ WHERE id IN(5105024,5105025) AND (status<>'ACTIVE' OR deleted<>0);
+ SET changed_rows=changed_rows+ROW_COUNT();
+ INSERT INTO iam_role_permission(tenant_id,role_id,permission_id,created_at)
+ SELECT 0,4000001,p.id,CURRENT_TIMESTAMP(3) FROM iam_permission p WHERE p.id IN(5105024,5105025)
+ AND NOT EXISTS(SELECT 1 FROM iam_role_permission rp WHERE rp.role_id=4000001 AND rp.permission_id=p.id);
+ SET changed_rows=changed_rows+ROW_COUNT();
+
+ -- 只有真实变更才推进版本;枚举所有既有管理员成员,不依赖当前登录用户,也不遗漏停用成员。
+ IF changed_rows>0 THEN
+ INSERT INTO iam_security_subject_version(auth_user_id,permission_version)
+ SELECT DISTINCT u.auth_user_id,1 FROM iam_user u
+ JOIN iam_membership m ON m.user_id=u.id AND m.tenant_id=0
+ JOIN iam_membership_role mr ON mr.membership_id=m.id AND mr.tenant_id=m.tenant_id AND mr.role_id=4000001
+ WHERE u.auth_user_id IS NOT NULL
+ ON DUPLICATE KEY UPDATE permission_version=permission_version+1;
+ END IF;
+ IF (SELECT COUNT(*) FROM iam_role_permission rp JOIN iam_permission p ON p.id=rp.permission_id
+ WHERE rp.role_id=4000001 AND rp.tenant_id=0 AND p.id IN(5105024,5105025)
+ AND p.tenant_id=0 AND p.app_code='PLATFORM' AND p.permission_type='BUTTON'
+ AND p.status='ACTIVE' AND p.deleted=0)<>2 THEN
+ SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='Canonical fee write grants incomplete; repair rolled back';
+ END IF;
+ COMMIT;
+ SELECT changed_rows AS repaired_catalog_or_grant_rows;
+END$$
+DELIMITER ;
+CALL repair_overseas_fee_canonical_write();
+DROP PROCEDURE repair_overseas_fee_canonical_write;
+
+-- 核验1:必须恰好两行,角色4000001、tenant=0、BUTTON、ACTIVE、deleted=0。
+SELECT r.id AS role_id,r.role_code,rp.tenant_id,p.id AS permission_id,p.permission_code,
+ p.permission_type,p.status,p.deleted
+FROM iam_role r JOIN iam_role_permission rp ON rp.role_id=r.id
+JOIN iam_permission p ON p.id=rp.permission_id
+WHERE r.id=4000001 AND p.id IN(5105024,5105025) ORDER BY p.id;
+-- 核验2:仅查看既有管理员主体与当前版本,不创建或切换任何成员角色。
+SELECT DISTINCT u.auth_user_id,m.id AS membership_id,u.status AS user_status,m.status AS membership_status,
+ COALESCE(sv.permission_version,0) AS permission_version
+FROM iam_user u JOIN iam_membership m ON m.user_id=u.id AND m.tenant_id=0
+JOIN iam_membership_role mr ON mr.membership_id=m.id AND mr.tenant_id=m.tenant_id AND mr.role_id=4000001
+LEFT JOIN iam_security_subject_version sv ON sv.auth_user_id=u.auth_user_id
+ORDER BY u.auth_user_id,m.id;
diff --git a/deploy/sql/overseas-fee-remark-add.sql b/deploy/sql/overseas-fee-remark-add.sql
new file mode 100644
index 000000000..10efb6dd9
--- /dev/null
+++ b/deploy/sql/overseas-fee-remark-add.sql
@@ -0,0 +1,19 @@
+-- 海外人工费用说明字段。仅修改 baiyue_lastleg.ll_finance_fee_source 的结构。
+-- 执行需单独授权;使用独立连接、遇错停止。先执行本脚本,再加载配套 Lastleg 代码。
+-- 无业务数据回填、费用重建、资金操作或菜单变更。已有字段时跳过新增。
+USE baiyue_lastleg;
+
+SET @overseas_fee_remark_ddl = (
+ SELECT IF(EXISTS(
+ SELECT 1 FROM information_schema.columns
+ WHERE table_schema = DATABASE() AND table_name = 'll_finance_fee_source' AND column_name = 'fee_remark'
+ ), 'SELECT ''fee_remark already exists'' AS result',
+ 'ALTER TABLE ll_finance_fee_source ADD COLUMN fee_remark VARCHAR(500) NULL COMMENT ''费用说明,补收或重复作业原因'' AFTER fee_item_name')
+);
+PREPARE overseas_fee_remark_stmt FROM @overseas_fee_remark_ddl;
+EXECUTE overseas_fee_remark_stmt;
+DEALLOCATE PREPARE overseas_fee_remark_stmt;
+
+SELECT table_schema, table_name, column_name, column_type, is_nullable, column_comment
+FROM information_schema.columns
+WHERE table_schema = 'baiyue_lastleg' AND table_name = 'll_finance_fee_source' AND column_name = 'fee_remark';
diff --git a/deploy/sql/overseas-storage-rent-zero-fee-repair.sql b/deploy/sql/overseas-storage-rent-zero-fee-repair.sql
new file mode 100644
index 000000000..6eadb4cd1
--- /dev/null
+++ b/deploy/sql/overseas-storage-rent-zero-fee-repair.sql
@@ -0,0 +1,73 @@
+-- 固定目标本机 baiyue_lastleg,只修复自动免费仓租结构,不更新业务数据。
+-- 使用独立连接、遇错停止;先核对既有表结构,不能重放空库初始化。
+USE baiyue_lastleg;
+SELECT COLUMN_NAME,COLUMN_TYPE,IS_NULLABLE FROM information_schema.COLUMNS
+WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ll_finance_fee_source'
+ AND COLUMN_NAME IN ('amount','original_amount','exchange_rate','created_by','fee_item_code','direction');
+
+-- 上次 CALL 前置检查失败可能遗留本专用过程,仅清理本脚本自己的过程后重建。
+DROP PROCEDURE IF EXISTS storage_rent_zero_fee_repair_20261023;
+DELIMITER $$
+CREATE PROCEDURE storage_rent_zero_fee_repair_20261023()
+BEGIN
+ DECLARE checked_columns INT DEFAULT 0;
+ DECLARE checked_constraints INT DEFAULT 0;
+ DECLARE rate_type VARCHAR(64);
+ DECLARE rate_comment TEXT;
+ DECLARE rate_default TEXT;
+ DECLARE rate_default_sql TEXT;
+ IF DATABASE()<>'baiyue_lastleg' THEN
+ SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='目标必须为 baiyue_lastleg';
+ END IF;
+ SELECT COUNT(*) INTO checked_columns FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ll_finance_fee_source'
+ AND ((COLUMN_NAME IN ('amount','original_amount') AND DATA_TYPE='decimal' AND IS_NULLABLE='NO')
+ OR (COLUMN_NAME='exchange_rate' AND DATA_TYPE='decimal' AND EXTRA='')
+ OR (COLUMN_NAME IN ('created_by','fee_item_code','direction') AND DATA_TYPE='varchar' AND IS_NULLABLE='NO'));
+ SELECT COUNT(*) INTO checked_constraints FROM information_schema.TABLE_CONSTRAINTS
+ WHERE CONSTRAINT_SCHEMA=DATABASE() AND TABLE_NAME='ll_finance_fee_source'
+ AND CONSTRAINT_NAME='chk_ll_finance_fee_amount' AND CONSTRAINT_TYPE='CHECK';
+ IF checked_columns<>6 OR checked_constraints<>1 THEN
+ SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='费用源表字段或金额约束异常,停止修复';
+ END IF;
+ -- 增加新约束前只读检查;人工费用仍须正额且有效汇率,防止意外放宽。
+ IF EXISTS (SELECT 1 FROM ll_finance_fee_source
+ WHERE NOT ((amount>0 OR (amount=0 AND original_amount=0 AND created_by='STORAGE_RENT' AND fee_item_code='FEE_TYPE_077' AND direction='RECEIVABLE'))
+ AND ((exchange_rate IS NOT NULL AND exchange_rate>0) OR (exchange_rate IS NULL AND amount=0 AND original_amount=0 AND created_by='STORAGE_RENT' AND fee_item_code='FEE_TYPE_077' AND direction='RECEIVABLE')))) THEN
+ SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='存在不满足新约束的费用,停止且不改数据';
+ END IF;
+ SELECT COUNT(*) INTO checked_constraints FROM information_schema.TABLE_CONSTRAINTS
+ WHERE CONSTRAINT_SCHEMA=DATABASE() AND TABLE_NAME='ll_finance_fee_source'
+ AND CONSTRAINT_NAME='chk_ll_finance_fee_exchange_rate' AND CONSTRAINT_TYPE='CHECK';
+ IF checked_constraints=0 THEN
+ -- 保留真实已有列的精度和备注,不把存量 DECIMAL(18,8) 收窄到基线精度。
+ SELECT COLUMN_TYPE,COLUMN_COMMENT,COLUMN_DEFAULT INTO rate_type,rate_comment,rate_default
+ FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ll_finance_fee_source' AND COLUMN_NAME='exchange_rate';
+ IF rate_type NOT REGEXP '^decimal[(][0-9]+,[0-9]+[)]$' THEN
+ SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='汇率列不是预期定点数类型,停止修复';
+ END IF;
+ IF LOCATE('自动零元仓租可空',rate_comment)=0 THEN
+ SET rate_comment=CONCAT(rate_comment,',自动零元仓租可空');
+ END IF;
+ -- 保留既有数值默认值(例如1.00000000);拒绝表达式或非数值默认值。
+ IF rate_default IS NOT NULL AND rate_default NOT REGEXP '^-?[0-9]+([.][0-9]+)?$' THEN
+ SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='汇率默认值不是预期数值,停止修复';
+ END IF;
+ SET rate_default_sql=IF(rate_default IS NULL,' DEFAULT NULL',CONCAT(' DEFAULT ',rate_default));
+ -- 单次原子 ALTER 同步调整可空性与两项约束,原币及结算列完全不改。
+ SET @storage_rent_zero_fee_ddl=CONCAT(
+ 'ALTER TABLE baiyue_lastleg.ll_finance_fee_source DROP CHECK chk_ll_finance_fee_amount, ',
+ 'MODIFY COLUMN exchange_rate ',rate_type,' NULL',rate_default_sql,' COMMENT ',QUOTE(rate_comment),', ',
+ 'ADD CONSTRAINT chk_ll_finance_fee_amount CHECK(amount>0 OR (amount=0 AND original_amount=0 AND created_by=''STORAGE_RENT'' AND fee_item_code=''FEE_TYPE_077'' AND direction=''RECEIVABLE'')), ',
+ 'ADD CONSTRAINT chk_ll_finance_fee_exchange_rate CHECK((exchange_rate IS NOT NULL AND exchange_rate>0) OR (exchange_rate IS NULL AND amount=0 AND original_amount=0 AND created_by=''STORAGE_RENT'' AND fee_item_code=''FEE_TYPE_077'' AND direction=''RECEIVABLE''))');
+ PREPARE storage_rent_zero_fee_stmt FROM @storage_rent_zero_fee_ddl;
+ EXECUTE storage_rent_zero_fee_stmt;
+ DEALLOCATE PREPARE storage_rent_zero_fee_stmt;
+ SET @storage_rent_zero_fee_ddl=NULL;
+ END IF;
+END$$
+DELIMITER ;
+CALL storage_rent_zero_fee_repair_20261023();
+DROP PROCEDURE storage_rent_zero_fee_repair_20261023;
+SHOW CREATE TABLE ll_finance_fee_source;
diff --git a/deploy/sql/warehouse-supplier-query-structure-repair.sql b/deploy/sql/warehouse-supplier-query-structure-repair.sql
new file mode 100644
index 000000000..41cff3c89
--- /dev/null
+++ b/deploy/sql/warehouse-supplier-query-structure-repair.sql
@@ -0,0 +1,40 @@
+-- 供应商查询缺失结构修复。MySQL 8;仅用于本次明确授权的本机开发库。
+-- 固定目标 baiyue_warehouse;不修改供应商、费用、余额或授信数据。
+-- 已有结构安全跳过;不推断历史拖车供应商的国内/海外类别。
+-- DDL 会隐式提交,请在独立连接以遇错停止模式整份执行。
+USE baiyue_warehouse;
+SET NAMES utf8mb4;
+SELECT DATABASE() AS target_database;
+
+-- 先确认正式供应商主表存在;不存在时 ALTER 会报错并停止,不创建第二份主档。
+SET @supplier_query_column_ddl = IF(
+ EXISTS (SELECT 1 FROM information_schema.columns
+ WHERE table_schema = 'baiyue_warehouse'
+ AND table_name = 'wh_supplier' AND column_name = 'trucking_scope'),
+ 'SELECT ''trucking_scope 已存在,跳过'' AS structure_result',
+ 'ALTER TABLE baiyue_warehouse.wh_supplier ADD COLUMN trucking_scope VARCHAR(16) NULL COMMENT ''拖车类型:DOMESTIC国内拖车、OVERSEAS海外车队;非拖车为空'' AFTER category'
+);
+PREPARE supplier_query_statement FROM @supplier_query_column_ddl;
+EXECUTE supplier_query_statement;
+DEALLOCATE PREPARE supplier_query_statement;
+
+-- 正式列表还会读取船司关联;若未初始化,仅按当前模块基线建立空表。
+CREATE TABLE IF NOT EXISTS wh_supplier_carrier (
+ supplier_id BIGINT UNSIGNED NOT NULL COMMENT 'BOOKING供应商ID',
+ tenant_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '平台公共作用域,固定为0',
+ carrier_code VARCHAR(16) NOT NULL COMMENT '船司标准编码',
+ created_by BIGINT UNSIGNED NULL COMMENT '创建人ID',
+ created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
+ PRIMARY KEY (supplier_id,carrier_code),
+ KEY idx_wh_supplier_carrier_code (tenant_id,carrier_code,supplier_id),
+ CONSTRAINT chk_wh_supplier_carrier_tenant CHECK (tenant_id = 0),
+ CONSTRAINT chk_wh_supplier_carrier_code CHECK (carrier_code IN ('MATSON','SML','ZIM','COSCO','EMC','HEDE','OTHER'))
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='BOOKING供应商与标准船司编码多对多关联';
+
+-- 执行后只核对结构,不读取供应商联系信息。
+SELECT table_name, column_name, column_type, is_nullable
+FROM information_schema.columns
+WHERE table_schema = 'baiyue_warehouse'
+ AND ((table_name = 'wh_supplier' AND column_name = 'trucking_scope')
+ OR table_name = 'wh_supplier_carrier')
+ORDER BY table_name, ordinal_position;
diff --git a/deploy/sql/头程数据库统一增量-MySQL8.sql b/deploy/sql/头程数据库统一增量-MySQL8.sql
index 8d6c1cedb..cadcdd050 100644
--- a/deploy/sql/头程数据库统一增量-MySQL8.sql
+++ b/deploy/sql/头程数据库统一增量-MySQL8.sql
@@ -1,4 +1,5 @@
-- CN 当前纯空库初始化入口(文件名沿用原入口,不提供增量升级语义)。
+-- 海外费用菜单及平台管理员授权统一在本文件 IAM 种子中维护,不再单列本次版本或专项菜单脚本。
-- MySQL 8.0+;只允许目标测试环境的服务 schema 为空,已有表立即报错;禁止 --force/遇错继续。
-- 字段、索引、约束及注释直接定义在 CREATE TABLE 内;不回填、不清理、不改造存量。
-- 国内 IAM 管理两区域权限;海外不访问国内库。执行方法及系统种子清单见同目录 README.md。
@@ -8458,4 +8459,8 @@ CREATE TABLE wh_document_sequence (
CONSTRAINT chk_wh_document_sequence_value CHECK (current_value > 0)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='业务单据每日独立原子流水';
+-- 海外费用查询菜单;编辑、审核继续使用独立财务按钮授权。
+USE baiyue_iam;
+INSERT INTO iam_permission (id,tenant_id,app_code,permission_type,parent_id,permission_code,permission_name,route_path,http_method,sort_no,status,built_in,created_at,updated_at,deleted) VALUES (5117200,0,'PLATFORM','MENU',0,'platform:overseas-fee:list','海外费用管理','/overseas-business/fees','GET',890,'ACTIVE',1,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3),0);
+INSERT INTO iam_role_permission (role_id,permission_id,tenant_id,created_at) VALUES (4000001,5117200,0,CURRENT_TIMESTAMP(3));
SELECT 'PASS' AS initialization_status, 'CN' AS region, 229 AS initialized_tables;
diff --git a/deploy/sql/海外区域统一增量-MySQL8.sql b/deploy/sql/海外区域统一增量-MySQL8.sql
index 14527036f..dd8a4fd9b 100644
--- a/deploy/sql/海外区域统一增量-MySQL8.sql
+++ b/deploy/sql/海外区域统一增量-MySQL8.sql
@@ -1,4 +1,5 @@
-- OVERSEAS 当前纯空库初始化入口(文件名沿用原入口,不提供增量升级语义)。
+-- 海外费用资金、仓租四表、审核人名称快照、采购退款请求幂等字段及唯一索引均在本文件最终建表定义中维护。
-- MySQL 8.0+;只允许目标测试环境的服务 schema 为空,已有表立即报错;禁止 --force/遇错继续。
-- 字段、索引、约束及注释直接定义在 CREATE TABLE 内;不回填、不清理、不改造存量。
-- 国内 IAM 管理两区域权限;海外不访问国内库。执行方法及系统种子清单见同目录 README.md。
@@ -314,7 +315,7 @@ CREATE TABLE ll_destuff_pallet_cost_allocation (
customer_id varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '客户ID',
actual_volume_cbm decimal(20,6) NOT NULL COMMENT '客户实收体积',
allocated_amount decimal(20,2) NOT NULL COMMENT '客户分摊金额',
- fee_source_id bigint NOT NULL COMMENT '海外费用来源ID',
+ fee_source_id bigint DEFAULT NULL COMMENT '人工审核后关联的海外费用来源ID',
created_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
PRIMARY KEY (id),
UNIQUE KEY uk_ll_destuff_customer (cost_id,customer_id),
@@ -947,6 +948,7 @@ CREATE TABLE ll_finance_fee_source (
warehouse_id bigint DEFAULT NULL COMMENT '关联海外仓ID',
fee_item_code varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '费用项目编码快照',
fee_item_name varchar(200) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '费用项目名称快照',
+ fee_remark varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '费用说明,补收或重复作业原因',
direction varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'RECEIVABLE应收或PAYABLE应付',
supplier_id bigint DEFAULT NULL COMMENT '应付供应商ID',
supplier_name varchar(200) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '应付供应商名称快照',
@@ -962,7 +964,7 @@ CREATE TABLE ll_finance_fee_source (
amount decimal(20,6) NOT NULL COMMENT '人民币结算金额',
original_currency char(3) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '业务原币种,支持CNY或USD',
original_amount decimal(20,6) NOT NULL COMMENT '业务原币金额',
- exchange_rate decimal(20,6) NOT NULL COMMENT '原币折算人民币汇率,人民币固定为1',
+ exchange_rate decimal(20,6) DEFAULT NULL COMMENT '原币折算人民币汇率,人民币固定为1,自动零元仓租可空',
exchange_rate_month char(7) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '汇率月份yyyy-MM',
chargeable_quantity decimal(20,6) DEFAULT NULL COMMENT '计费数量,如箱数、托数、体积或天数',
quantity_unit varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '计费数量单位',
@@ -974,6 +976,7 @@ CREATE TABLE ll_finance_fee_source (
source_status varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'ACTIVE、SUPERSEDED或VOIDED',
review_reason varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '审核或作废原因',
reviewer_id varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '审核人ID',
+ reviewer_name varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '审核人名称快照',
reviewed_at datetime(3) DEFAULT NULL COMMENT '审核时间',
created_by varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '创建人ID',
updated_by varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '最后更新人ID',
@@ -984,7 +987,8 @@ CREATE TABLE ll_finance_fee_source (
KEY idx_ll_finance_fee_customer (customer_id,source_status,created_at),
KEY idx_ll_finance_fee_business (business_type,business_no,created_at),
KEY idx_ll_finance_fee_container_customer (container_booking_id,customer_id,source_status,created_at),
- CONSTRAINT chk_ll_finance_fee_amount CHECK ((amount > 0)),
+ CONSTRAINT chk_ll_finance_fee_exchange_rate CHECK((exchange_rate IS NOT NULL AND exchange_rate>0) OR (exchange_rate IS NULL AND amount=0 AND original_amount=0 AND created_by='STORAGE_RENT' AND fee_item_code='FEE_TYPE_077' AND direction='RECEIVABLE')),
+ CONSTRAINT chk_ll_finance_fee_amount CHECK(amount>0 OR (amount=0 AND original_amount=0 AND created_by='STORAGE_RENT' AND fee_item_code='FEE_TYPE_077' AND direction='RECEIVABLE')),
CONSTRAINT chk_ll_finance_fee_currency CHECK ((currency = _utf8mb4'CNY')),
CONSTRAINT chk_ll_finance_fee_direction CHECK ((direction in (_utf8mb4'RECEIVABLE',_utf8mb4'PAYABLE'))),
CONSTRAINT chk_ll_finance_fee_original_currency CHECK ((original_currency in (_utf8mb4'CNY',_utf8mb4'USD'))),
@@ -2122,6 +2126,7 @@ CREATE TABLE ll_overseas_procurement_file (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='海外采购签收文件及费用凭证';
CREATE TABLE ll_overseas_procurement_order (
+ procurement_direction VARCHAR(32) NOT NULL DEFAULT 'OVERSEAS_TO_DOMESTIC' COMMENT '采购方向:海外采购回国内/国内采购发海外',
id bigint NOT NULL COMMENT '主键ID',
customer_id varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '客户租户ID',
customer_name varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '客户名称快照',
@@ -4593,6 +4598,7 @@ CREATE TABLE ll_return_sku_conversion (
quantity BIGINT NOT NULL COMMENT '实际完成转换件数',
status VARCHAR(32) NOT NULL COMMENT 'APPLIED待客户确认/CONFIRMED已确认/REVERSED退回冲回',
ownership_json LONGTEXT NOT NULL COMMENT '本次转换件数的来源退货单快照,供精确冲回',
+ storage_rent_shares_json LONGTEXT NULL COMMENT '原仓租批次迁移快照',
completed_at DATETIME(3) NOT NULL COMMENT '实物SKU转换时间',
confirmed_at DATETIME(3) NULL COMMENT '客户确认时间',
reversed_at DATETIME(3) NULL COMMENT '退回冲回时间',
@@ -4987,4 +4993,117 @@ CREATE TABLE ll_document_sequence (
CONSTRAINT chk_ll_document_sequence_value CHECK (current_value > 0)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='业务单据每日独立原子流水';
-SELECT 'PASS' AS initialization_status, 'OVERSEAS' AS region, 234 AS initialized_tables;
+-- 海外费用资金及实收仓租。
+-- 海外人工费用资金编排。金额与余额以 Finance 钱包为唯一事实,本表仅记录指令及回执状态。
+CREATE TABLE ll_finance_fee_funding (
+ source_fee_id BIGINT NOT NULL COMMENT '关联海外费用稳定来源ID',
+ customer_id VARCHAR(128) NOT NULL COMMENT '费用所属客户隔离标识',
+ collection_mode VARCHAR(16) NOT NULL COMMENT '客户收费方式:BILL账单结算,WALLET余额及授信',
+ wallet_status VARCHAR(32) NOT NULL COMMENT '资金进度:待冻结、已冻结、待扣款、已扣款、待释放及失败状态',
+ wallet_business_no VARCHAR(128) NULL COMMENT '本次冻结使用的独立业务编号,重提使用新编号',
+ command_event_id VARCHAR(64) NULL COMMENT '当前待处理资金指令事件ID',
+ wallet_hold_id VARCHAR(64) NULL COMMENT 'Finance确认的资金冻结记录ID',
+ wallet_ledger_id VARCHAR(64) NULL COMMENT 'Finance确认的已扣资金流水ID',
+ command_version BIGINT NOT NULL DEFAULT 0 COMMENT '资金指令单调递增版本,防止重试重复入账',
+ error_message VARCHAR(500) NULL COMMENT '最新资金失败原因,成功后清空',
+ command_history JSON NOT NULL COMMENT '已发资金指令及核验结果哈希,识别重复和错序回执',
+ created_at DATETIME(3) NOT NULL COMMENT '资金编排创建UTC时间',
+ updated_at DATETIME(3) NOT NULL COMMENT '资金编排最近变更UTC时间',
+ PRIMARY KEY (source_fee_id),
+ KEY idx_ll_fee_funding_customer_state (customer_id, wallet_status),
+ KEY idx_ll_fee_funding_command (command_event_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='海外人工费用资金冻结、审核扣款及释放编排';
+
+CREATE TABLE IF NOT EXISTS ll_storage_rent_inbound (
+ receipt_id bigint NOT NULL COMMENT '实际入库凭证主键,防止重复累计计租数量',
+ lot_id bigint NOT NULL COMMENT '头程同单同仓计租批次主键',
+ quantity decimal(24,12) NOT NULL COMMENT '本次实收追加计租箱数',
+ created_at datetime(3) NOT NULL COMMENT '计租登记时间,采用协调世界时',
+ PRIMARY KEY (receipt_id),
+ KEY idx_storage_rent_inbound_lot (lot_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='头程实收计租幂等登记';
+
+CREATE TABLE ll_storage_rent_lot (
+ id bigint NOT NULL COMMENT '原批次主键',
+ source_key varchar(80) NOT NULL COMMENT '实收来源幂等键',
+ customer_id varchar(32) NOT NULL COMMENT '客户标识',
+ customer_name varchar(255) DEFAULT NULL COMMENT '客户名称',
+ warehouse_id varchar(32) NOT NULL COMMENT '海外仓标识',
+ warehouse_name varchar(255) DEFAULT NULL COMMENT '海外仓名称',
+ business_type varchar(32) NOT NULL COMMENT '原入库业务类型',
+ business_id bigint NOT NULL COMMENT '原入库业务主键',
+ business_no varchar(128) NOT NULL COMMENT '原入库业务单号',
+ order_id bigint DEFAULT NULL COMMENT '尾程订单主键',
+ inbound_code varchar(128) DEFAULT NULL COMMENT '入仓码',
+ balance_id bigint DEFAULT NULL COMMENT '普通库存余额主键',
+ batch_no varchar(128) DEFAULT NULL COMMENT '原实收批次',
+ sku_code varchar(128) DEFAULT NULL COMMENT '商品编码',
+ rule_code varchar(32) NOT NULL COMMENT '仓租规则',
+ original_quantity decimal(24,12) NOT NULL COMMENT '实收原始数量',
+ remaining_quantity decimal(24,12) NOT NULL COMMENT '未出库原始数量',
+ original_units decimal(24,12) DEFAULT NULL COMMENT '原批次计费体积或托数',
+ inbound_at datetime(3) NOT NULL COMMENT '实际入库时间',
+ created_at datetime(3) NOT NULL COMMENT '创建时间',
+ updated_at datetime(3) NOT NULL COMMENT '更新时间',
+ PRIMARY KEY (id),
+ UNIQUE KEY uk_storage_rent_lot_source (source_key),
+ KEY idx_storage_rent_stock (balance_id,batch_no),
+ KEY idx_storage_rent_customer (customer_id,warehouse_id,inbound_at)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='实收计租原批次';
+
+CREATE TABLE ll_storage_rent_share (
+ balance_id bigint DEFAULT NULL COMMENT '普通库存当前余额,退货箱份额为空',
+ batch_no varchar(128) DEFAULT NULL COMMENT '普通库存当前批次,换标搬移保持原计租批次引用',
+ id bigint NOT NULL COMMENT '份额主键',
+ lot_id bigint NOT NULL COMMENT '原计租批次主键',
+ customer_id varchar(32) NOT NULL COMMENT '客户标识',
+ warehouse_id varchar(32) NOT NULL COMMENT '海外仓标识',
+ carton_mark varchar(255) DEFAULT NULL COMMENT '当前箱唛',
+ sku_code varchar(128) DEFAULT NULL COMMENT '商品编码',
+ quantity decimal(24,12) NOT NULL COMMENT '剩余商品数量',
+ PRIMARY KEY (id),
+ UNIQUE KEY uk_storage_rent_return_share (lot_id,carton_mark,sku_code),
+ UNIQUE KEY uk_storage_rent_stock_share (lot_id,balance_id,batch_no),
+ KEY idx_storage_rent_carton (customer_id,warehouse_id,carton_mark),
+ KEY idx_storage_rent_share_stock (customer_id,warehouse_id,balance_id,batch_no)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='当前库存与原计租批次份额';
+
+CREATE TABLE ll_storage_rent_preparation (
+ id bigint NOT NULL COMMENT '预校验主键',
+ warehouse_id varchar(32) NOT NULL COMMENT '作业海外仓',
+ customer_id varchar(32) NOT NULL COMMENT '费用客户',
+ mode varchar(32) NOT NULL COMMENT '扫码类型',
+ execution_no varchar(128) NOT NULL COMMENT '实际作业单号',
+ request_key varchar(255) NOT NULL COMMENT '原扫码幂等键',
+ request_hash varchar(64) NOT NULL COMMENT '原扫码内容哈希',
+ attempt int NOT NULL COMMENT '冻结尝试次数',
+ business_type varchar(32) NOT NULL COMMENT '本次应收业务类型',
+ business_id bigint NOT NULL COMMENT '本次应收业务主键',
+ business_no varchar(128) NOT NULL COMMENT '本次应收业务单号',
+ status varchar(32) NOT NULL COMMENT '资金与扫码状态',
+ plan_json json NOT NULL COMMENT '原批次计费不可变快照',
+ original_amount decimal(24,12) NOT NULL COMMENT '原币仓租金额,币种见计费快照',
+ amount decimal(24,12) NOT NULL COMMENT '人民币结算金额',
+ exchange_rate decimal(24,12) NULL COMMENT '冻结确认汇率,免费出库为空',
+ exchange_rate_month varchar(7) NOT NULL COMMENT '汇率月份',
+ freeze_event_id varchar(128) DEFAULT NULL COMMENT '冻结指令事件',
+ settle_event_id varchar(128) DEFAULT NULL COMMENT '扣款指令事件',
+ release_event_id varchar(128) DEFAULT NULL COMMENT '解冻指令事件',
+ release_status varchar(32) DEFAULT NULL COMMENT '解冻回执状态,失败后由恢复任务重发',
+ wallet_version bigint NOT NULL COMMENT '同一预占资金命令版本,重试使用新版本保留审计',
+ hold_id varchar(128) DEFAULT NULL COMMENT '财务冻结主键',
+ ledger_id varchar(128) DEFAULT NULL COMMENT '财务扣款流水',
+ error_message varchar(1000) DEFAULT NULL COMMENT '资金处理结果',
+ source_fee_id bigint DEFAULT NULL COMMENT '财务费用来源主键',
+ created_at datetime(3) NOT NULL COMMENT '预校验时间',
+ consumed_at datetime(3) DEFAULT NULL COMMENT '实际扫描出库时间',
+ updated_at datetime(3) NOT NULL COMMENT '更新时间',
+ PRIMARY KEY (id),
+ UNIQUE KEY uk_storage_rent_request (warehouse_id,mode,request_key,attempt),
+ KEY idx_storage_rent_execution (mode,execution_no,status),
+ KEY idx_storage_rent_business (customer_id,business_no),
+ KEY idx_storage_rent_recovery (status,updated_at),
+ KEY idx_storage_rent_release (release_status,updated_at)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='仓租扫码资金预占与结算快照';
+
+SELECT 'PASS' AS initialization_status, 'OVERSEAS' AS region, 238 AS initialized_tables;
diff --git a/finance/AGENTS.md b/finance/AGENTS.md
index 5013adedc..4cde3e21b 100644
--- a/finance/AGENTS.md
+++ b/finance/AGENTS.md
@@ -23,7 +23,7 @@
| 客户只读账单、收款与应收 API | `controller/ClientFinanceController.java`,统一前缀 `/api/client/finance/v1` |
| 订单与客户账户、费用来源审核与结算纳入 | `controller/FinancePhase1OperationController.java`、`service/FinancePhase1OperationService.java` |
| 客户财务账户汇总 | `service/FinanceCustomerAccountService.java`、`client/IamCustomerClient.java`;客户主档事实仍归 IAM |
-| 客户资金账户、授信与充值 | `controller/ClientFinanceWalletController.java`、`controller/PlatformFinanceWalletController.java`、`controller/InternalFinanceWalletController.java`、`service/FinanceWalletService.java`、`dto/FinanceWalletDtos.java`;支持静态收款码、截图申请、平台审核、手工充扣、授信配置及供业务模块使用的冻结/结算/释放,第三方支付回调尚未接入 |
+| 客户资金账户、授信与充值 | `controller/ClientFinanceWalletController.java`、`controller/PlatformFinanceWalletController.java`、`controller/InternalFinanceWalletController.java`、`service/FinanceWalletService.java`、`dto/FinanceWalletDtos.java`;支持静态收款码、截图申请、平台审核、手工充扣、授信配置及供业务模块使用的冻结/结算/释放;原消费关联退款契约见 `src/main/resources/openapi/finance-wallet-refund-v1.yaml`,第三方支付回调尚未接入 |
| 客户对账单 | `controller/ReconciliationStatementController.java`、`service/ReconciliationStatementService.java` |
| 应收款月度统计 | `controller/ReceivableStatisticsController.java`、`service/ReceivableStatisticsService.java`、`dto/ReceivableStatisticsDtos.java`;按结算单生成月份和客户汇总 CNY 正式应收结算单 |
| 应收/应付结算单 | `controller/FinanceSettlementController.java`、`service/FinanceSettlementService.java` |
diff --git a/finance/src/main/java/com/baiyue/finance/controller/PlatformFinanceWalletController.java b/finance/src/main/java/com/baiyue/finance/controller/PlatformFinanceWalletController.java
index c026b5321..2264a4b74 100644
--- a/finance/src/main/java/com/baiyue/finance/controller/PlatformFinanceWalletController.java
+++ b/finance/src/main/java/com/baiyue/finance/controller/PlatformFinanceWalletController.java
@@ -7,11 +7,16 @@ import com.baiyue.finance.dto.FinanceWalletDtos.CreditLimitRequest;
import com.baiyue.finance.dto.FinanceWalletDtos.RechargeReviewRequest;
import com.baiyue.finance.dto.FinancePhase1Dtos.PageEnvelope;
import com.baiyue.finance.dto.FinanceWalletDtos.RechargeRequest;
+import com.baiyue.finance.dto.FinanceWalletDtos.WalletRefundRequest;
import com.baiyue.finance.service.FinanceWalletService;
+import java.nio.charset.StandardCharsets;
+import java.util.UUID;
+import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
+import org.springframework.web.server.ResponseStatusException;
@RestController
@RequestMapping("/api/platform/finance/v1")
@@ -62,4 +67,25 @@ public class PlatformFinanceWalletController {
@GetMapping("/recharge-requests/{requestId}/voucher")
@PreAuthorize("hasAuthority('platform:finance:customer-account:list')")
public ResponseEntity voucher(@PathVariable long requestId) { return service.voucher(requestId); }
+
+ /** 退款必须由平台财务针对原消费发起,复用资金服务的账户锁及累计可退上限。 */
+ @PostMapping("/accounts/{customerId}/ledger/{ledgerId}/refunds")
+ @PreAuthorize("hasAuthority('platform:finance:customer-account:adjust')")
+ public ApiResponse> refund(@AuthenticationPrincipal AuthenticatedUser user,
+ @PathVariable long customerId, @PathVariable long ledgerId,
+ @RequestHeader("Idempotency-Key") String idempotencyKey,
+ @RequestBody WalletRefundRequest request) {
+ if (user == null || user.userId() == null || !"PLATFORM".equals(user.appCode()) || !Long.valueOf(0L).equals(user.tenantId())) {
+ throw new ResponseStatusException(HttpStatus.FORBIDDEN, "仅平台财务可操作退款");
+ }
+ if (request == null || idempotencyKey == null || idempotencyKey.isBlank()
+ || idempotencyKey.trim().length() > 128) throw new IllegalArgumentException("退款请求及幂等键必填且幂等键不超过128字符");
+ if (request.reason() == null || request.reason().isBlank() || request.reason().length() > 500)
+ throw new IllegalArgumentException("退款原因必填且不超过500字符");
+ // 技术退款引用由客户和幂等键确定;资金流水号仍由正式 AC 编号生成器分配。
+ String refundNo = "MANUAL_REFUND:" + UUID.nameUUIDFromBytes(
+ (customerId + ":" + idempotencyKey.trim()).getBytes(StandardCharsets.UTF_8));
+ return FinanceResponses.success(service.refundConsumption(customerId, ledgerId, refundNo,
+ idempotencyKey.trim(), request.amount(), user.userId(), user.displayName(), request.reason().trim()));
+ }
}
diff --git a/finance/src/main/java/com/baiyue/finance/dto/FinanceWalletDtos.java b/finance/src/main/java/com/baiyue/finance/dto/FinanceWalletDtos.java
index db7893993..08ec295fe 100644
--- a/finance/src/main/java/com/baiyue/finance/dto/FinanceWalletDtos.java
+++ b/finance/src/main/java/com/baiyue/finance/dto/FinanceWalletDtos.java
@@ -54,6 +54,9 @@ public final class FinanceWalletDtos {
public record WalletHoldActionRequest(String remark) { }
+ /** 关联原消费退款,原因必填,原流水保持不可变。 */
+ public record WalletRefundRequest(BigDecimal amount, String reason) { }
+
public record WalletHold(long holdId, long customerId, String businessType,
String businessNo, BigDecimal amount,
BigDecimal actualFrozenAmount, BigDecimal creditFrozenAmount,
diff --git a/finance/src/main/java/com/baiyue/finance/service/BusinessFeeSourceEventService.java b/finance/src/main/java/com/baiyue/finance/service/BusinessFeeSourceEventService.java
index 1312e6dc0..9796e4820 100644
--- a/finance/src/main/java/com/baiyue/finance/service/BusinessFeeSourceEventService.java
+++ b/finance/src/main/java/com/baiyue/finance/service/BusinessFeeSourceEventService.java
@@ -135,6 +135,7 @@ public class BusinessFeeSourceEventService {
positive(value.walletSettlement().holdId(), "walletSettlement.holdId");
positive(value.walletSettlement().ledgerId(), "walletSettlement.ledgerId");
if (blank(value.walletSettlement().commandEventId())) throw error("FIN-EVT-012", "缺少原资金命令引用");
+ walletIdentity(value);
} else if ("OVERSEAS_PROCUREMENT_FEE".equals(value.sourceType()) && "RECEIVABLE".equals(value.direction())) {
throw error("FIN-EVT-012", "采购应收必须引用成功实扣资金,不能进入待收费池");
}
@@ -154,7 +155,7 @@ public class BusinessFeeSourceEventService {
throw error("FIN-EVT-012", "应付来源供应商快照不一致");
validateSettlement(value);
} else if (!blank(value.supplierId()) || !blank(value.supplierName()) || !blank(value.supplierCategory())
- || !blank(value.settlementMode()) || value.paidAt() != null
+ || value.settlementMode() != null && !Set.of("BILL", "WALLET").contains(value.settlementMode()) || value.paidAt() != null
|| !blank(value.paymentMethod()) || !blank(value.paymentReferenceNo())
|| (value.paymentVoucherFileIds() != null && !value.paymentVoucherFileIds().isEmpty())) {
throw error("FIN-EVT-012", "应收来源不得携带供应商或付款事实");
@@ -202,9 +203,10 @@ public class BusinessFeeSourceEventService {
long customerId = positive(source.customerId(), "customerId");
long holdId = positive(reference.holdId(), "holdId");
long ledgerId = positive(reference.ledgerId(), "ledgerId");
+ String[] identity = walletIdentity(source);
List