Compare commits
3
Commits
be9f5d2a30
...
c2735cd906
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2735cd906 | ||
|
|
2927fbee06 | ||
|
|
5b53be23e8 |
@@ -11,7 +11,14 @@
|
||||
|
||||
两份文件虽然名称包含“增量”,仍只用于对应区域的空库初始化。新增列、索引和约束直接维护在最终 `CREATE TABLE` 中,菜单及授权使用初始化种子;不追加存量改造、权限版本回填或动态迁移段。目标 schema 必须为空,按文件原有初始化流程执行;不能向已有业务库重放。
|
||||
|
||||
本次不再提供这项功能的独立升级脚本。已有库升级必须按实际结构和明确授权另行准备、审阅受控增量,不能把删除重复文件理解为已有库已完成升级。
|
||||
已有库只按实际缺项执行定向结构补齐,不能把删除重复文件理解为已有库已完成升级。指定 TEST 的最小执行集合如下;文件准备不代表获得执行授权或已部署。
|
||||
|
||||
| 顺序 | 文件 | 允许的变更与核对结果 |
|
||||
|---|---|---|
|
||||
| 1 | [表与审核人字段补齐](overseas-fees-storage-rent-structure-add.sql) | 只补 `ll_storage_rent_inbound`、`ll_finance_fee_source.reviewer_name`;已存在且匹配时跳过,同名异构则停止;成功回查表结构、字段并输出 `TABLE_COLUMN_READY` |
|
||||
| 2 | [免费仓租定向修复](overseas-storage-rent-zero-fee-repair.sql) | 仅自动应收仓租允许原币/结算双零与空汇率;保留其他费用正额及有效汇率约束,保留已有汇率精度与数值默认值;最终核对 `SHOW CREATE TABLE` 中金额、汇率两项 CHECK 和汇率可空性 |
|
||||
|
||||
两步均固定 `baiyue_lastleg`,使用独立连接、支持 `DELIMITER` 的客户端,禁止遇错继续;不执行业务 DML、不补历史计租记录、不变更余额、汇率配置或权限。DDL 隐式提交,不能宣称整体可回滚;失败保留错误并只读复查,已完成项不删除重建。两步是已核缺项的补齐方案,不是任意版本升级工具;重新执行前仍需核对目标实例与结构。
|
||||
|
||||
## 海外表和字段
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# 单测试环境数据库空库初始化
|
||||
|
||||
`overseas-fees-storage-rent-structure-add.sql` 为指定 TEST 海外费用/仓租的定向结构补齐,只增加缺失的 `ll_storage_rent_inbound` 和 `ll_finance_fee_source.reviewer_name`。全部前置检查在首个业务 DDL 前完成;已有结构匹配时跳过,不匹配则停止,不覆盖或回填。随后按单独授权执行已有 `overseas-storage-rent-zero-fee-repair.sql`,补齐自动免费仓租的金额与汇率约束。使用独立连接、遇错停止,不重放空库初始化;DDL 隐式提交,失败先回查,不删除已完成项。两步范围与发布清单见[海外费用数据库定义汇总](20261022-海外费用仓租数据库变更汇总.md)及[发布配套](../../lastleg-service/docs/overseas-fees-storage-rent-release.md#发布配套)。文件准备不代表已执行或部署。
|
||||
|
||||
车辆与司机档案照片复用:先执行 `lastleg-fleet-resource-photos.sql` 新增照片版本表,再配套发布 File、Lastleg 和 Admin。车辆详情/司机详情维护照片,排车保存时复制缺失照片为当趟证据;已有照片不被后续档案替换覆盖。驾驶证统一接受 PDF/JPG/PNG。源基线与海外空库基线已同步;本次未执行环境 SQL 或部署。
|
||||
|
||||
放舱后预计到港窄维护需要配套更新 `trg_ocean_booking_before_update`:仅从冻结比较中移除 `estimated_arrival_at`,其余运输、预报、关系方及柜型冻结不变。定义已同步 `firstleg/src/main/resources/db/phase3-ocean-booking-schema.sql` 与国内统一基线;已有 TEST 需另行审阅并授权替换该触发器,不重放空库文件,不回填数据。本轮只准备定义,未执行 SQL 或部署。
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
-- 指定 TEST 的海外费用/仓租结构补齐;实例必须由执行者另行核对并获准。
|
||||
-- 仅新增 ll_storage_rent_inbound 和 ll_finance_fee_source.reviewer_name。
|
||||
-- 使用独立连接,支持 DELIMITER,遇错停止;禁止 mysql --force。
|
||||
-- 不更新费用、库存、钱包或历史计租数据,不重放空库初始化。
|
||||
-- DDL 隐式提交;失败先回查结构,禁止删除已建表/字段来重跑。
|
||||
USE baiyue_lastleg;
|
||||
|
||||
-- 仅清理本脚本可能遗留的专用校验过程,不删除业务对象。
|
||||
DROP PROCEDURE IF EXISTS overseas_fee_rent_structure_add_20261026;
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE overseas_fee_rent_structure_add_20261026()
|
||||
BEGIN
|
||||
DECLARE inbound_exists INT DEFAULT 0;
|
||||
DECLARE reviewer_exists INT DEFAULT 0;
|
||||
|
||||
IF DATABASE()<>'baiyue_lastleg' THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='目标必须为 baiyue_lastleg';
|
||||
END IF;
|
||||
-- 在首个业务 DDL 前核对两张依赖表及新增字段的定位列。
|
||||
IF (SELECT COUNT(*) FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME IN ('ll_finance_fee_source','ll_storage_rent_lot')
|
||||
AND TABLE_TYPE='BASE TABLE' AND ENGINE='InnoDB')<>2
|
||||
OR (SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ll_finance_fee_source'
|
||||
AND COLUMN_NAME='reviewer_id' AND DATA_TYPE='varchar' AND CHARACTER_MAXIMUM_LENGTH=128)<>1 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='费用来源或计租批次基础结构不匹配,停止补齐';
|
||||
END IF;
|
||||
|
||||
SELECT COUNT(*) INTO reviewer_exists FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ll_finance_fee_source' AND COLUMN_NAME='reviewer_name';
|
||||
IF reviewer_exists>0 AND (SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ll_finance_fee_source' AND COLUMN_NAME='reviewer_name'
|
||||
AND DATA_TYPE='varchar' AND CHARACTER_MAXIMUM_LENGTH=255 AND IS_NULLABLE='YES'
|
||||
AND COLUMN_DEFAULT IS NULL AND EXTRA='')<>1 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='已存在的审核人名称字段与正式定义不同,停止且不覆盖';
|
||||
END IF;
|
||||
|
||||
SELECT COUNT(*) INTO inbound_exists FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ll_storage_rent_inbound';
|
||||
-- 已存在的同名表必须完整匹配;不以 IF NOT EXISTS 掩盖半成品或异构表。
|
||||
IF inbound_exists>0 THEN
|
||||
IF (SELECT COUNT(*) FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ll_storage_rent_inbound'
|
||||
AND TABLE_TYPE='BASE TABLE' AND ENGINE='InnoDB' AND TABLE_COLLATION='utf8mb4_unicode_ci')<>1
|
||||
OR (SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ll_storage_rent_inbound')<>4
|
||||
OR (SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ll_storage_rent_inbound'
|
||||
AND IS_NULLABLE='NO' AND COLUMN_DEFAULT IS NULL AND EXTRA=''
|
||||
AND ((COLUMN_NAME IN ('receipt_id','lot_id') AND COLUMN_TYPE='bigint')
|
||||
OR (COLUMN_NAME='quantity' AND COLUMN_TYPE='decimal(24,12)')
|
||||
OR (COLUMN_NAME='created_at' AND COLUMN_TYPE='datetime(3)')))<>4 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='已存在的计租实收表字段与正式定义不同,停止且不覆盖';
|
||||
END IF;
|
||||
IF (SELECT COUNT(*) FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ll_storage_rent_inbound' AND INDEX_NAME='PRIMARY')<>1
|
||||
OR (SELECT COUNT(*) FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ll_storage_rent_inbound' AND INDEX_NAME='PRIMARY'
|
||||
AND COLUMN_NAME='receipt_id' AND SEQ_IN_INDEX=1 AND NON_UNIQUE=0 AND SUB_PART IS NULL)<>1
|
||||
OR (SELECT COUNT(*) FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ll_storage_rent_inbound'
|
||||
AND INDEX_NAME='idx_storage_rent_inbound_lot')<>1
|
||||
OR (SELECT COUNT(*) FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ll_storage_rent_inbound'
|
||||
AND INDEX_NAME='idx_storage_rent_inbound_lot' AND COLUMN_NAME='lot_id'
|
||||
AND SEQ_IN_INDEX=1 AND NON_UNIQUE=1 AND SUB_PART IS NULL)<>1 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='计租实收表主键或批次索引不匹配,停止且不覆盖';
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
-- 全部前置检查通过后才补缺项;重跑时保持已存在的业务结构和数据。
|
||||
IF reviewer_exists=0 THEN
|
||||
ALTER TABLE ll_finance_fee_source
|
||||
ADD COLUMN reviewer_name VARCHAR(255) NULL COMMENT '审核人名称快照' AFTER reviewer_id;
|
||||
END IF;
|
||||
IF inbound_exists=0 THEN
|
||||
CREATE TABLE 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='头程实收计租幂等登记';
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
CALL overseas_fee_rent_structure_add_20261026();
|
||||
DROP PROCEDURE overseas_fee_rent_structure_add_20261026;
|
||||
|
||||
SHOW CREATE TABLE ll_storage_rent_inbound;
|
||||
SELECT COLUMN_NAME,COLUMN_TYPE,IS_NULLABLE,COLUMN_DEFAULT
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ll_finance_fee_source' AND COLUMN_NAME='reviewer_name';
|
||||
SELECT 'TABLE_COLUMN_READY' AS structure_status;
|
||||
-- TABLE_COLUMN_READY 只表示上述表/字段补齐;免费仓租另执行已审阅的
|
||||
-- overseas-storage-rent-zero-fee-repair.sql,不能据此宣称完整功能上线。
|
||||
@@ -1,4 +1,4 @@
|
||||
-- 固定目标本机 baiyue_lastleg,只修复自动免费仓租结构,不更新业务数据。
|
||||
-- 固定目标经单独授权的 TEST 或本机开发 baiyue_lastleg,只修复自动免费仓租结构,不更新业务数据。
|
||||
-- 使用独立连接、遇错停止;先核对既有表结构,不能重放空库初始化。
|
||||
USE baiyue_lastleg;
|
||||
SELECT COLUMN_NAME,COLUMN_TYPE,IS_NULLABLE FROM information_schema.COLUMNS
|
||||
|
||||
@@ -219,6 +219,7 @@ public class FinanceQueryService {
|
||||
row.outstandingAmount(), row.currency(), row.status(), row.version())).toList();
|
||||
}
|
||||
|
||||
/** 成本查询按来源付款事实回显金额,直接已付不能再次展示为待付款。 */
|
||||
public List<PayableSettlement> payableSettlements(Long customerId) {
|
||||
validateOptionalCustomer(customerId);
|
||||
String sql = "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 1=1";
|
||||
@@ -227,9 +228,13 @@ public class FinanceQueryService {
|
||||
return jdbc.query(sql + " ORDER BY created_at DESC,id DESC",
|
||||
(rs, rowNum) -> {
|
||||
BigDecimal amount = rs.getBigDecimal("cost_amount");
|
||||
boolean directPaid = "SOURCE_DIRECT_PAID".equals(rs.getString("cost_type"));
|
||||
// 已确认直付成本已全额付款;作废历史不再计入有效已付,也不形成新应付。
|
||||
BigDecimal paid = directPaid && "CONFIRMED".equals(rs.getString("status")) ? amount : BigDecimal.ZERO;
|
||||
BigDecimal outstanding = directPaid ? BigDecimal.ZERO : 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"),
|
||||
rs.getString("fee_item_name"), amount, paid, outstanding, rs.getString("currency_code"), rs.getString("status"),
|
||||
time(rs, "created_at"), rs.getInt("version"));
|
||||
}, args);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.baiyue.finance;
|
||||
|
||||
import com.baiyue.finance.service.FinanceQueryService;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.datasource.DriverManagerDataSource;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/** 成本列表和详情须与直接已付事实一致,不能提示供应商费用仍待支付。 */
|
||||
class FinanceDirectPaidCostReadTest {
|
||||
private JdbcTemplate jdbc;
|
||||
private FinanceQueryService service;
|
||||
|
||||
/** 使用隔离内存表验证正式读取路径,不访问共享测试环境。 */
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
jdbc = new JdbcTemplate(new DriverManagerDataSource(
|
||||
"jdbc:h2:mem:cost-read-" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1", "sa", ""));
|
||||
jdbc.execute("CREATE TABLE fin_cost(id BIGINT PRIMARY KEY,tenant_id BIGINT,cost_no VARCHAR(128),"
|
||||
+ "cost_type VARCHAR(24),cost_origin VARCHAR(24),order_id BIGINT,inbound_id BIGINT,container_id BIGINT,"
|
||||
+ "supplier_name_snapshot VARCHAR(128),fee_item_name VARCHAR(128),cost_amount DECIMAL(18,2),"
|
||||
+ "currency_code VARCHAR(3),status VARCHAR(24),created_at TIMESTAMP,version INT)");
|
||||
jdbc.execute("CREATE TABLE fin_cost_allocation(id BIGINT,tenant_id BIGINT,cost_id BIGINT,order_id BIGINT,"
|
||||
+ "inbound_id BIGINT,volume_cbm DECIMAL(18,6),total_volume_cbm DECIMAL(18,6),allocation_rate DECIMAL(18,6),"
|
||||
+ "allocated_amount DECIMAL(18,2),rounding_adjustment BOOLEAN)");
|
||||
service = new FinanceQueryService(jdbc, new ObjectMapper());
|
||||
}
|
||||
|
||||
/** 头程与海外直付均已付款,列表和详情不能再次显示未付余额。 */
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"BUSINESS_DIRECT_PAID", "FIRSTLEG_DIRECT_PAID"})
|
||||
void confirmedDirectPaidCostIsFullyPaidInListAndDetail(String origin) {
|
||||
insertCost(1, 11, "SOURCE_DIRECT_PAID", origin, "CONFIRMED");
|
||||
|
||||
var row = service.payableSettlements(11L).get(0);
|
||||
assertThat(row.payableAmount()).isEqualByComparingTo("0.03");
|
||||
assertThat(row.paidAmount()).isEqualByComparingTo("0.03");
|
||||
assertThat(row.outstandingAmount()).isZero();
|
||||
assertThat(service.cost(11, 1).cost()).isEqualTo(row);
|
||||
}
|
||||
|
||||
/** 已作废的直付历史保留原金额,但不再作为有效已付或待付款展示。 */
|
||||
@Test
|
||||
void voidedDirectPaidCostHasNoActivePaymentOrOutstandingAmount() {
|
||||
insertCost(1, 11, "SOURCE_DIRECT_PAID", "BUSINESS_DIRECT_PAID", "VOIDED");
|
||||
|
||||
var row = service.payableSettlements(11L).get(0);
|
||||
assertThat(row.payableAmount()).isEqualByComparingTo("0.03");
|
||||
assertThat(row.paidAmount()).isZero();
|
||||
assertThat(row.outstandingAmount()).isZero();
|
||||
assertThat(row.status()).isEqualTo("VOIDED");
|
||||
}
|
||||
|
||||
/** 人工补充成本没有直接已付事实,不能被自动视为付款完成。 */
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"SINGLE_ORDER", "CONTAINER_SHARED"})
|
||||
void manualCostKeepsItsExistingUnpaidAmounts(String type) {
|
||||
insertCost(1, 11, type, "MANUAL", "CONFIRMED");
|
||||
|
||||
var row = service.payableSettlements(11L).get(0);
|
||||
assertThat(row.paidAmount()).isZero();
|
||||
assertThat(row.outstandingAmount()).isEqualByComparingTo("0.03");
|
||||
}
|
||||
|
||||
/** 金额回显修复不能让其他客户读到成本列表或详情。 */
|
||||
@Test
|
||||
void costReadsKeepCustomerIsolation() {
|
||||
insertCost(1, 11, "SOURCE_DIRECT_PAID", "BUSINESS_DIRECT_PAID", "CONFIRMED");
|
||||
|
||||
assertThat(service.payableSettlements(12L)).isEmpty();
|
||||
assertThatThrownBy(() -> service.cost(12, 1)).hasMessageContaining("成本不存在或不属于客户");
|
||||
}
|
||||
|
||||
/** 仅构造测试所需的成本事实,金额对应本轮发现问题的小额样本。 */
|
||||
private void insertCost(long id, long customer, String type, String origin, String status) {
|
||||
jdbc.update("INSERT INTO fin_cost(id,tenant_id,cost_no,cost_type,cost_origin,order_id,container_id,"
|
||||
+ "supplier_name_snapshot,fee_item_name,cost_amount,currency_code,status,created_at,version) "
|
||||
+ "VALUES(?,?,?,?,?,101,201,'验收供应商','人工费',0.03,'CNY',?,CURRENT_TIMESTAMP,0)",
|
||||
id, customer, "COST-READ-TEST", type, origin, status);
|
||||
}
|
||||
}
|
||||
@@ -38,10 +38,27 @@
|
||||
- 后端:Firstleg 公共目录内部接口、Warehouse 客户仓租内部报价接口、Lastleg 费用/仓租及真实收发货挂钩。Finance 继续作为余额、授信、冻结和实扣的唯一权威;不得绕过 Outbox/Inbox 链路。
|
||||
- 前端:Admin 海外费用管理与海外 PDA 出库预校验同时发布,不能只更新一端。
|
||||
- 数据库文件:本次五份版本脚本及两份专项汇总 SQL 已收敛到 [国内统一空库基线](../../deploy/sql/头程数据库统一增量-MySQL8.sql) 和 [海外统一空库基线](../../deploy/sql/海外区域统一增量-MySQL8.sql)。海外基线完整包含资金与仓租四表、拆柜成本可空费用引用、退货 SKU 换标仓租份额快照、审核人名称及采购退款请求幂等字段和唯一索引,国内基线包含菜单和默认管理员授权。内容清单见 [数据库变更汇总](../../deploy/sql/20261022-海外费用仓租数据库变更汇总.md)。
|
||||
- 已有库:不能重放统一空库初始化;须按实际结构和明确授权审阅增量。[免费仓租定向结构修复](../../deploy/sql/overseas-storage-rent-zero-fee-repair.sql) 仅解决自动仓租双零金额和空汇率入费用源表,保留人工正额与正常费用有效汇率约束,不更新业务数据。独立的 [既有费用写权限修复](../../deploy/sql/overseas-fee-canonical-write-permissions-repair.sql) 仍只处理已有管理员两项 canonical 写权限,不能代替结构升级或菜单初始化。
|
||||
- 已有库:不能重放统一空库初始化;须按实际结构和明确授权审阅增量。指定 TEST 先使用[表与审核人字段补齐](../../deploy/sql/overseas-fees-storage-rent-structure-add.sql),再使用[免费仓租定向结构修复](../../deploy/sql/overseas-storage-rent-zero-fee-repair.sql);精确范围与失败语义只维护在[数据库变更汇总](../../deploy/sql/20261022-海外费用仓租数据库变更汇总.md)。独立的 [既有费用写权限修复](../../deploy/sql/overseas-fee-canonical-write-permissions-repair.sql) 仍只处理已有管理员两项 canonical 写权限,不能代替结构升级或菜单初始化。
|
||||
- 新空库:Lastleg 源基线与海外统一初始化脚本同步新表和字段;国内统一基线同步菜单。菜单查询权限 `platform:overseas-fee:list`,编辑与审核分别沿用 `platform:finance:fee-source:edit/review`,仅有查询权限不能写费用。
|
||||
- 配置:核对 Firstleg、Warehouse 内部服务地址及内部令牌、客户仓租阶梯、有效 USD/CNY 月度汇率、Finance 资金与费用事件直投及回执配置。当前结算支持 USD、CNY,CNY 按1:1结算,其他币种必须先调整为受支持币种。不得在材料中记录密钥。
|
||||
- 发布顺序:先发布 Warehouse 客户计租接口与 Finance,再发布 Lastleg,配套更新 Admin 与海外 PDA。成功扣款引用新增可选的 `walletBusinessType/walletBusinessNo`,分别指向人工费用冻结编号或仓租出库预占编号;外层业务编号仍是实际服务单。Finance 校验客户、金额、成功命令、冻结与流水一致且不可重复关联,接收后标记已结清。旧版 Finance 不识别这两个字段,不得先发生产者。
|
||||
- 发布顺序:先发布 Firstleg 公共费用目录、Warehouse 客户计租接口与 Finance,再发布 Lastleg,配套更新 Admin 与海外 PDA。成功扣款引用新增可选的 `walletBusinessType/walletBusinessNo`,分别指向人工费用冻结编号或仓租出库预占编号;外层业务编号仍是实际服务单。Finance 校验客户、金额、成功命令、冻结与流水一致且不可重复关联,接收后标记已结清。旧版 Finance 不识别这两个字段,不得先发生产者。
|
||||
|
||||
### TEST 配套发布清单
|
||||
|
||||
本清单用于准备和审阅,不表示环境已完成执行。实际版本、部署结果及未关闭验收项仍只维护于[测试环境主契约](../../../docs/测试环境单机部署与配置契约.md)。发布前冻结后端、Admin、海外 PDA 的精确提交和产物摘要;执行期间任一版本变化都需重新核对受影响项,不使用浮动分支头替代审阅版本。
|
||||
|
||||
| 顺序 | 对象 | 执行前提与通过标准 |
|
||||
|---|---|---|
|
||||
| 1 | 规则与执行窗口 | 确认人工应收是否开放“推送结算/余额扣费”选择:当前前后端已支持显式选择,未选择才按业务默认规则;本文件首版固定分流描述不能作为该选择功能已获确认的依据。明确本批 TEST SQL、服务发布及测试客户写入授权,切换期间暂停相关收费与出库操作 |
|
||||
| 2 | Lastleg 结构 | 按数据库变更汇总审阅并执行两个定向文件,核对新增表/字段及免费仓租约束;不重新初始化已有资金、计租表,不重放已有权限种子,不改现行汇率 |
|
||||
| 3 | Firstleg、Warehouse、Finance | 用匹配后端提交分别发布,核对容器镜像与健康;Firstleg 具备 `/internal/firstleg/v1/fee-items`,Warehouse 具备客户计租 `/pricing`,Finance 接受应收结算方式及独立扣款引用;内部服务地址、统一服务令牌及事件回传配置匹配 |
|
||||
| 4 | Lastleg | 依赖就绪后发布费用/仓租编排及收发货挂钩;统一 Gateway 下费用分页、项目目录、仓租列表与仓库选项均返回业务成功,不能只检查 HTTP 200 |
|
||||
| 5 | Admin、海外 PDA | Admin 包含海外费用管理和单据费用操作;海外 PDA 包含 `/pda/v1/last-leg/storage-rent/preparations` 出库预校验。H5 按宿主机静态站点规则发布并核验资源摘要;APK/WGT 按实际设备另行核对,H5 通过不等于设备已更新 |
|
||||
| 6 | 最小业务验收 | 使用明确授权的新测试样本完成下表;逐笔核对业务单、费用、冻结/消费/退款流水及事件回执。查询成功、表为空或历史事件已投递不能替代新功能闭环 |
|
||||
|
||||
第一轮最小样本按以下顺序执行:人工余额收费的登记冻结、审核实扣、原请求重放与驳回释放;账单收费审核入池且钱包不变;免费仓租记录无冻结/扣款;非零仓租出库预占、审核前调价及审核实扣;头程同单同仓补入库不重置库龄、重复实收不增加计租数量。供应商账期/直付与采购专用资金链继续按下表分别验收,不由人工收费样本推定通过。
|
||||
|
||||
若结构、服务、页面或设备任一项不匹配,保持相关收费/出库操作暂停并记录失败,不通过补零、伪造回执或重建费用继续;DDL 不做自动回滚,服务回滚仅限明确核对过的同协议同 Schema 版本。页面布局由人工按 1920×1080、100% 缩放提供截图核对。
|
||||
|
||||
## 实际环境验收
|
||||
|
||||
@@ -65,7 +82,7 @@
|
||||
|
||||
## 验证记录
|
||||
|
||||
本地验证结果在本次交付说明中列出;真实环境 SQL、部署、HTTP 联调与 PDA 设备验收尚未执行。上线前逐项填写环境、版本、执行时间与证据,未通过项不得标成通过。
|
||||
本地验证结果在本次交付说明中列出;真实环境 SQL、部署、HTTP 联调及 PDA 验收状态统一维护在[测试环境单机部署与配置契约](../../../docs/测试环境单机部署与配置契约.md)。该主契约逐项记录环境、版本、执行时间与证据,未通过项不得标成通过。
|
||||
|
||||
2026-10-22 本地验证记录(使用最终受影响文件;复用已通过且输入未变的结果):
|
||||
|
||||
|
||||
@@ -6,6 +6,59 @@ import { fileURLToPath } from 'node:url'
|
||||
import { buildPlan, loadScopes } from '../deploy/sql/scripts/db-migrations.mjs'
|
||||
|
||||
const repositoryRoot = path.resolve(fileURLToPath(new URL('..', import.meta.url)))
|
||||
|
||||
const overseasFeeStructurePath = 'deploy/sql/overseas-fees-storage-rent-structure-add.sql'
|
||||
const readSql = relative => readFile(path.join(repositoryRoot, relative), 'utf8')
|
||||
const withoutSqlComments = source => source.replace(/^\s*--.*$/gm, '')
|
||||
|
||||
test('海外费用结构补齐只修改已核准的表字段,不包含业务数据写入', async () => {
|
||||
const source = withoutSqlComments(await readSql(overseasFeeStructurePath))
|
||||
assert.match(source, /USE baiyue_lastleg;/)
|
||||
assert.doesNotMatch(source, /\b(?:INSERT|UPDATE|DELETE|REPLACE|TRUNCATE)\b/i)
|
||||
assert.doesNotMatch(source, /\bDROP\s+(?:TABLE|DATABASE|COLUMN|INDEX)\b/i)
|
||||
assert.deepEqual([...source.matchAll(/^\s*ALTER TABLE\s+(\w+)/gim)].map(match => match[1]), ['ll_finance_fee_source'])
|
||||
assert.deepEqual([...source.matchAll(/^\s*CREATE TABLE\s+(\w+)/gim)].map(match => match[1]), ['ll_storage_rent_inbound'])
|
||||
assert.match(source, /ADD COLUMN reviewer_name VARCHAR\(255\) NULL COMMENT '审核人名称快照' AFTER reviewer_id;/)
|
||||
})
|
||||
|
||||
test('海外费用计租实收表与两份正式基线完全一致', async () => {
|
||||
const files = [overseasFeeStructurePath, 'lastleg-service/src/main/resources/db/lastleg.sql', 'deploy/sql/海外区域统一增量-MySQL8.sql']
|
||||
const definitions = await Promise.all(files.map(async file => {
|
||||
const source = await readSql(file)
|
||||
const definition = source.match(/CREATE TABLE(?: IF NOT EXISTS)? ll_storage_rent_inbound\s*\([\s\S]*?\) ENGINE=[^;]+;/)?.[0]
|
||||
assert.ok(definition, file)
|
||||
assert.ok(/reviewer_name\s+VARCHAR\(255\)\s+(?:COLLATE\s+\w+\s+)?(?:DEFAULT\s+)?NULL\s+COMMENT '审核人名称快照'/i.test(source), `${file} 审核人名称须为可空 VARCHAR(255)`)
|
||||
return definition.replace(' IF NOT EXISTS', '').replace(/\s+/g, ' ').toLowerCase()
|
||||
}))
|
||||
assert.equal(definitions[0], definitions[1])
|
||||
assert.equal(definitions[0], definitions[2])
|
||||
})
|
||||
|
||||
test('海外费用结构补齐先检查同名结构再修改,支持匹配结构重跑', async () => {
|
||||
const source = withoutSqlComments(await readSql(overseasFeeStructurePath))
|
||||
const firstDdl = source.indexOf('ALTER TABLE ll_finance_fee_source')
|
||||
assert.ok(firstDdl > 0)
|
||||
const guards = source.slice(0, firstDdl)
|
||||
for (const marker of ["DATABASE()<>'baiyue_lastleg'", 'reviewer_exists>0', 'inbound_exists>0', "TABLE_COLLATION='utf8mb4_unicode_ci'", "COLUMN_TYPE='decimal(24,12)'", "COLUMN_TYPE='datetime(3)'", "INDEX_NAME='PRIMARY'", "INDEX_NAME='idx_storage_rent_inbound_lot'"]) {
|
||||
assert.ok(guards.includes(marker), marker)
|
||||
}
|
||||
assert.match(guards, /SIGNAL SQLSTATE '45000'/)
|
||||
assert.match(source, /IF reviewer_exists=0 THEN\s+ALTER TABLE/)
|
||||
assert.match(source, /IF inbound_exists=0 THEN\s+CREATE TABLE/)
|
||||
assert.match(source, /CALL overseas_fee_rent_structure_add_20261026\(\);[\s\S]+SHOW CREATE TABLE ll_storage_rent_inbound;[\s\S]+'TABLE_COLUMN_READY'/)
|
||||
})
|
||||
|
||||
test('海外费用免费仓租修复只放行自动应收双零,保留其他费用及汇率边界', async () => {
|
||||
const repair = await readSql('deploy/sql/overseas-storage-rent-zero-fee-repair.sql')
|
||||
const normalized = repair.replaceAll("''", "'")
|
||||
const autoZero = "amount=0 AND original_amount=0 AND created_by='STORAGE_RENT' AND fee_item_code='FEE_TYPE_077' AND direction='RECEIVABLE'"
|
||||
assert.ok(normalized.includes(`CHECK(amount>0 OR (${autoZero}))`))
|
||||
assert.ok(normalized.includes(`CHECK((exchange_rate IS NOT NULL AND exchange_rate>0) OR (exchange_rate IS NULL AND ${autoZero}))`))
|
||||
assert.match(repair, /SELECT COLUMN_TYPE,COLUMN_COMMENT,COLUMN_DEFAULT INTO rate_type,rate_comment,rate_default/)
|
||||
assert.match(repair, /rate_default IS NOT NULL AND rate_default NOT REGEXP/)
|
||||
assert.doesNotMatch(withoutSqlComments(repair), /^\s*(?:INSERT|UPDATE|DELETE|REPLACE|TRUNCATE)\b/im)
|
||||
})
|
||||
|
||||
const migrations = {
|
||||
iam: 'deploy/sql/migrations/cn/baiyue_iam/V1__align_schema_baseline.sql',
|
||||
iamForwardFix: 'deploy/sql/migrations/cn/baiyue_iam/V3__align_domestic_pda_mojia_sailing_permissions.sql',
|
||||
|
||||
Reference in New Issue
Block a user