优化
This commit is contained in:
@@ -49,13 +49,13 @@
|
||||
- 通用配置在 `config/nacos/{local,cn}/`;服务日志、上传文件、`target/`、`reports/` 不属于源码。
|
||||
- 全量验证:`mvn -DskipTests validate`、`mvn test`;单服务优先 `mvn -pl <module> -am test`。不要在本地启动 Java/Nacos/数据库,除非用户明确要求。
|
||||
|
||||
## 全量测试脚本路由
|
||||
## 全量 HTTP 验收脚本路由
|
||||
|
||||
- 本地后端静态/单元回归:在 `baiyue-firstleg/` 执行 `pwsh scripts/backend-full-regression.ps1 -DryRun` 先检查覆盖文件,再按需去掉 `-DryRun` 执行 `order + firstleg` Maven 测试;脚本不访问数据库、Gateway 或测试环境。
|
||||
- 本地前端回归:在 `baiyue-firstleg/` 执行 `pwsh scripts/frontend-full-regression.ps1 -DryRun` 先检查入口,再按需执行 Admin/Client 的 `pnpm test` 和 `pnpm typecheck`;脚本不启动开发服务器、不访问 Gateway。
|
||||
- 测试环境 API 全量验收:`scripts/admin-test-env-acceptance.mjs`、`scripts/client-test-env-acceptance.mjs`、`scripts/phase2-test-env-acceptance.ps1` 及 `scripts/admin-test-env-modules/` 属于有数据写入的场景脚本,会创建测试客户、订单或业务记录;只有用户在当前对话明确授权测试环境验收时才执行,并使用报告目录留存结果。
|
||||
- `scripts/local-db-full-acceptance.ps1` 仅允许 `127.0.0.1`/`localhost` 数据库,先用 `-DryRun` 校验统一 SQL 快照和注释门禁;不得改参数连接远程或测试数据库。
|
||||
- 验收脚本按“先本地静态回归,再测试环境 API 验收,最后人工浏览器样式验收”顺序使用;脚本通过不等于页面视觉、权限和真实数据链路全部通过。
|
||||
- 全量验收唯一指向测试环境 HTTP:平台端执行 `scripts/admin-test-env-acceptance.mjs`,客户端执行 `scripts/client-test-env-acceptance.mjs`;二期兼容场景才单独执行 `scripts/client-phase2-test-env-acceptance.mjs`。
|
||||
- Admin 总入口默认按模块列表覆盖平台全链路;需要定位单个业务时使用 `--modules <name>`,模块名称和静态合同先查 `scripts/admin-test-env-modules/README.md`。
|
||||
- 这些脚本会创建、更新或最终停用测试数据,必须在当前对话明确授权测试环境验收后执行,并保存 JSON 报告;未授权不得请求 Gateway。
|
||||
- 后端 Maven、前端 `pnpm test/typecheck`、PDA `node --test` 和数据库 Dry-run 都是本地质量检查,不属于全量 HTTP 验收。
|
||||
- HTTP 全量通过后,才进入 Admin/Client 真实页面调用、浏览器样式和 PDA 页面/设备验收;自动化 HTTP 通过不代表视觉验收完成。
|
||||
|
||||
## 阶段文档入口
|
||||
|
||||
|
||||
@@ -2401,7 +2401,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_order (
|
||||
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 '入库流程状态:WAIT_MEASURE待测量、MEASURING测量中、MEASURE_PENDING待审核、MEASURE_REJECTED测量驳回、WAIT_PRICING待计费、FEE_CONFIRMING费用确认中、INBOUNDING入库收口中、INBOUNDED已入库、WAIT_SPLIT历史待分柜',
|
||||
process_status VARCHAR(24) NOT NULL DEFAULT 'WAIT_MEASURE' COMMENT '入库流程状态:WAIT_MEASURE待测量、MEASURING测量中、MEASURE_PENDING待审核、MEASURE_REJECTED测量驳回、WAIT_PRICING待计费、FEE_CONFIRMING费用确认中、INBOUNDING入库收口中、WAIT_SPLIT待分柜',
|
||||
order_status_snapshot VARCHAR(24) NOT NULL 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已作废',
|
||||
@@ -12162,7 +12162,7 @@ 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', 'process_status', '入库流程状态:WAIT_MEASURE待测量、MEASURING测量中、MEASURE_PENDING待审核、MEASURE_REJECTED测量驳回、WAIT_PRICING待计费、FEE_CONFIRMING费用确认中、INBOUNDING入库收口中、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');
|
||||
@@ -13889,7 +13889,6 @@ INSERT INTO baiyue_iam.iam_permission
|
||||
(id, tenant_id, app_code, permission_type, parent_id, permission_code, permission_name, route_path, http_method, sort_no, status, deleted)
|
||||
VALUES
|
||||
(5105000, 0, 'PLATFORM', 'MENU', 0, 'platform:finance', '财务管理', '/finance', NULL, 60, 'ACTIVE', 0),
|
||||
(5105010, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:bill:view', '查看账单', NULL, 'GET', 10, 'ACTIVE', 0),
|
||||
(5105011, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:bill:create', '创建账单', NULL, 'POST', 20, 'ACTIVE', 0),
|
||||
(5105012, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:bill:update', '修改草稿账单', NULL, 'PUT', 30, 'ACTIVE', 0),
|
||||
(5105013, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:bill:issue', '出账', NULL, 'POST', 40, 'ACTIVE', 0),
|
||||
@@ -13925,8 +13924,6 @@ BEGIN
|
||||
OR (id = 5105030 AND permission_code <> 'platform:finance:reconciliation:void')
|
||||
OR (id = 5105031 AND permission_code <> 'platform:finance:settlement-ar:create')
|
||||
OR (id = 5105032 AND permission_code <> 'platform:finance:settlement-ap:create')
|
||||
OR (id = 5105033 AND permission_code <> 'platform:finance:settlement-ar:view')
|
||||
OR (id = 5105034 AND permission_code <> 'platform:finance:settlement-ap:view')
|
||||
OR (id = 5105035 AND permission_code <> 'platform:finance:payment:create')
|
||||
OR (id = 5105036 AND permission_code <> 'platform:finance:settlement:void')
|
||||
OR (permission_code = 'platform:finance:account:view' AND id <> 5105022)
|
||||
@@ -13940,8 +13937,6 @@ BEGIN
|
||||
OR (permission_code = 'platform:finance:reconciliation:void' AND id <> 5105030)
|
||||
OR (permission_code = 'platform:finance:settlement-ar:create' AND id <> 5105031)
|
||||
OR (permission_code = 'platform:finance:settlement-ap:create' AND id <> 5105032)
|
||||
OR (permission_code = 'platform:finance:settlement-ar:view' AND id <> 5105033)
|
||||
OR (permission_code = 'platform:finance:settlement-ap:view' AND id <> 5105034)
|
||||
OR (permission_code = 'platform:finance:payment:create' AND id <> 5105035)
|
||||
OR (permission_code = 'platform:finance:settlement:void' AND id <> 5105036)
|
||||
OR (id BETWEEN 5105022 AND 5105036
|
||||
@@ -13970,8 +13965,6 @@ BEGIN
|
||||
(5105030, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:reconciliation:void', '作废或重开客户对账单', NULL, 'POST', 210, 'ACTIVE', 1, 0),
|
||||
(5105031, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:settlement-ar:create', '生成应收结算单', NULL, 'POST', 220, 'ACTIVE', 1, 0),
|
||||
(5105032, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:settlement-ap:create', '生成应付结算单', NULL, 'POST', 230, 'ACTIVE', 1, 0),
|
||||
(5105033, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:settlement-ar:view', '查看应收结算单', NULL, 'GET', 240, 'ACTIVE', 1, 0),
|
||||
(5105034, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:settlement-ap:view', '查看应付结算单', NULL, 'GET', 250, 'ACTIVE', 1, 0),
|
||||
(5105035, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:payment:create', '登记应付付款', NULL, 'POST', 260, 'ACTIVE', 1, 0),
|
||||
(5105036, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:settlement:void', '作废或重开结算单', NULL, 'POST', 270, 'ACTIVE', 1, 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
@@ -14001,10 +13994,8 @@ BEGIN
|
||||
'platform:finance:reconciliation:void',
|
||||
'platform:finance:settlement-ar:create',
|
||||
'platform:finance:settlement-ap:create',
|
||||
'platform:finance:settlement-ar:view',
|
||||
'platform:finance:settlement-ap:view',
|
||||
'platform:finance:payment:create',
|
||||
'platform:finance:settlement:void')) <> 15 THEN
|
||||
'platform:finance:settlement:void')) <> 13 THEN
|
||||
SIGNAL SQLSTATE '45000'
|
||||
SET MESSAGE_TEXT = 'finance G1-G4 permission catalog is incomplete';
|
||||
END IF;
|
||||
@@ -14017,7 +14008,7 @@ INSERT INTO baiyue_iam.iam_role_permission (role_id, permission_id, tenant_id)
|
||||
SELECT 4000001, id, 0 FROM baiyue_iam.iam_permission
|
||||
WHERE tenant_id=0 AND app_code='PLATFORM' AND permission_code IN (
|
||||
'platform:finance',
|
||||
'platform:finance:bill:view', 'platform:finance:bill:create',
|
||||
'platform:finance:bill:create',
|
||||
'platform:finance:bill:update', 'platform:finance:bill:issue',
|
||||
'platform:finance:bill:adjust', 'platform:finance:bill:void',
|
||||
'platform:finance:receipt:view', 'platform:finance:receipt:create',
|
||||
@@ -14032,8 +14023,7 @@ WHERE tenant_id=0 AND app_code='PLATFORM' AND permission_code IN (
|
||||
'platform:finance:reconciliation:create', 'platform:finance:reconciliation:review',
|
||||
'platform:finance:reconciliation:issue', 'platform:finance:reconciliation:adjust',
|
||||
'platform:finance:reconciliation:void', 'platform:finance:settlement-ar:create',
|
||||
'platform:finance:settlement-ap:create', 'platform:finance:settlement-ar:view',
|
||||
'platform:finance:settlement-ap:view', 'platform:finance:payment:create',
|
||||
'platform:finance:settlement-ap:create', 'platform:finance:payment:create',
|
||||
'platform:finance:settlement:void')
|
||||
ON DUPLICATE KEY UPDATE tenant_id=VALUES(tenant_id);
|
||||
INSERT INTO baiyue_iam.iam_role_permission (role_id, permission_id, tenant_id)
|
||||
@@ -14045,13 +14035,13 @@ ON DUPLICATE KEY UPDATE tenant_id=VALUES(tenant_id);
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE assert_finance_permissions()
|
||||
BEGIN
|
||||
IF (SELECT COUNT(*) FROM baiyue_iam.iam_permission WHERE tenant_id=0 AND app_code='PLATFORM' AND permission_code LIKE 'platform:finance:%' AND status='ACTIVE' AND deleted=0) <> 27 THEN
|
||||
IF (SELECT COUNT(*) FROM baiyue_iam.iam_permission WHERE tenant_id=0 AND app_code='PLATFORM' AND permission_code LIKE 'platform:finance:%' AND status='ACTIVE' AND deleted=0) <> 24 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='统一迁移失败:平台 Finance 权限不完整';
|
||||
END IF;
|
||||
IF (SELECT COUNT(*) FROM baiyue_iam.iam_permission WHERE tenant_id=10001 AND app_code='MERCHANT' AND permission_code IN ('merchant:finance:statement:list','merchant:finance:statement:view') AND status='ACTIVE' AND deleted=0) <> 2 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='统一迁移失败:客户 Finance 只读权限不完整';
|
||||
END IF;
|
||||
IF (SELECT COUNT(*) FROM baiyue_iam.iam_role_permission rp JOIN baiyue_iam.iam_permission p ON p.id=rp.permission_id AND p.tenant_id=rp.tenant_id WHERE rp.role_id=4000001 AND rp.tenant_id=0 AND (p.permission_code='platform:finance' OR p.permission_code LIKE 'platform:finance:%')) <> 27 THEN
|
||||
IF (SELECT COUNT(*) FROM baiyue_iam.iam_role_permission rp JOIN baiyue_iam.iam_permission p ON p.id=rp.permission_id AND p.tenant_id=rp.tenant_id WHERE rp.role_id=4000001 AND rp.tenant_id=0 AND (p.permission_code='platform:finance' OR p.permission_code LIKE 'platform:finance:%')) <> 25 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='统一迁移失败:平台管理员 Finance 授权不完整';
|
||||
END IF;
|
||||
IF (SELECT COUNT(*) FROM baiyue_iam.iam_role_permission rp JOIN baiyue_iam.iam_permission p ON p.id=rp.permission_id AND p.tenant_id=rp.tenant_id WHERE rp.role_id=4000101 AND rp.tenant_id=10001 AND (p.permission_code='merchant:finance:statement' OR p.permission_code LIKE 'merchant:finance:%')) <> 3 THEN
|
||||
@@ -14431,7 +14421,7 @@ BEGIN
|
||||
END$$
|
||||
DELIMITER ;
|
||||
|
||||
-- 二、三期对外统一使用 INBOUNDED;历史 WAIT_SPLIT 仅由查询适配为 INBOUNDED,统一迁移不改写主表或历史状态。
|
||||
-- 二、三期入库完成后的唯一状态为 WAIT_SPLIT。
|
||||
USE baiyue_firstleg;
|
||||
CREATE TABLE IF NOT EXISTS fl_container_exception_history (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '异常历史ID',
|
||||
@@ -14491,7 +14481,14 @@ UPDATE iam_permission SET status='INACTIVE', deleted=1 WHERE tenant_id=0 AND app
|
||||
INSERT IGNORE INTO iam_role_permission (tenant_id, role_id, permission_id, created_at)
|
||||
SELECT 0, r.id, p.id, CURRENT_TIMESTAMP(3) FROM iam_role r JOIN iam_permission p ON p.tenant_id=0 AND p.app_code='PLATFORM'
|
||||
WHERE r.tenant_id=0 AND r.app_code='PLATFORM' AND r.role_code='PLATFORM_ADMIN' AND p.permission_code IN ('platform:finance:customer-account:list','platform:finance:customer-arap:list','platform:finance:customer-statement:list','platform:finance:settlement-ar:list','platform:finance:settlement-ap:list');
|
||||
UPDATE iam_permission p JOIN (SELECT 'platform:finance:account:view' code,5105040 parent_id UNION ALL SELECT 'platform:finance:fee-source:view',5105041 UNION ALL SELECT 'platform:finance:fee-source:review',5105041 UNION ALL SELECT 'platform:finance:fee-source:settlement',5105041 UNION ALL SELECT 'platform:finance:reconciliation:create',5105042 UNION ALL SELECT 'platform:finance:reconciliation:review',5105042 UNION ALL SELECT 'platform:finance:reconciliation:issue',5105042 UNION ALL SELECT 'platform:finance:reconciliation:adjust',5105042 UNION ALL SELECT 'platform:finance:reconciliation:void',5105042 UNION ALL SELECT 'platform:finance:settlement-ar:create',5105043 UNION ALL SELECT 'platform:finance:settlement-ar:view',5105043 UNION ALL SELECT 'platform:finance:settlement-ap:create',5105044 UNION ALL SELECT 'platform:finance:settlement-ap:view',5105044 UNION ALL SELECT 'platform:finance:payment:create',5105044) mapping ON mapping.code=p.permission_code SET p.parent_id=mapping.parent_id WHERE p.tenant_id=0 AND p.app_code='PLATFORM' AND p.permission_type='BUTTON';
|
||||
UPDATE iam_permission p JOIN (SELECT 'platform:finance:account:view' code,5105040 parent_id UNION ALL SELECT 'platform:finance:fee-source:view',5105041 UNION ALL SELECT 'platform:finance:fee-source:review',5105041 UNION ALL SELECT 'platform:finance:fee-source:settlement',5105041 UNION ALL SELECT 'platform:finance:reconciliation:create',5105042 UNION ALL SELECT 'platform:finance:reconciliation:review',5105042 UNION ALL SELECT 'platform:finance:reconciliation:issue',5105042 UNION ALL SELECT 'platform:finance:reconciliation:adjust',5105042 UNION ALL SELECT 'platform:finance:reconciliation:void',5105042 UNION ALL SELECT 'platform:finance:settlement-ar:create',5105043 UNION ALL SELECT 'platform:finance:settlement-ap:create',5105044 UNION ALL SELECT 'platform:finance:payment:create',5105044) mapping ON mapping.code=p.permission_code SET p.parent_id=mapping.parent_id WHERE p.tenant_id=0 AND p.app_code='PLATFORM' AND p.permission_type='BUTTON';
|
||||
|
||||
DELETE rp FROM iam_role_permission rp JOIN iam_permission p ON p.id=rp.permission_id AND p.tenant_id=rp.tenant_id
|
||||
WHERE p.tenant_id=0 AND p.app_code='PLATFORM'
|
||||
AND p.permission_code IN('platform:finance:bill:view','platform:finance:settlement-ar:view','platform:finance:settlement-ap:view');
|
||||
UPDATE iam_permission SET status='INACTIVE',deleted=1
|
||||
WHERE tenant_id=0 AND app_code='PLATFORM'
|
||||
AND permission_code IN('platform:finance:bill:view','platform:finance:settlement-ar:view','platform:finance:settlement-ap:view');
|
||||
|
||||
INSERT INTO baiyue_iam.iam_permission
|
||||
(id, tenant_id, app_code, permission_type, parent_id, permission_code, permission_name, route_path, http_method, sort_no, status, deleted)
|
||||
@@ -14701,6 +14698,6 @@ WHERE p.permission_code IN('platform:firstleg:container:view','platform:firstleg
|
||||
UPDATE iam_permission SET status='INACTIVE',deleted=1
|
||||
WHERE permission_code IN('platform:firstleg:container:view','platform:firstleg:preload:view','platform:firstleg:loading-list:view','platform:supplier:view');
|
||||
|
||||
-- 新完成入库记录使用 WAIT_SPLIT;存量 INBOUNDED 继续作为可分柜兼容状态,统一迁移不改写主表或历史状态。
|
||||
-- 入库完成后的唯一状态为 WAIT_SPLIT。
|
||||
SELECT 'PASS' AS migration_status,
|
||||
'统一增量全部执行完成;父子订单、履约分片、装柜门禁、订舱拖车、报关船期、船司查询、仓库角色、费用、Finance人工财务、公共资源及可靠审计结构校验通过' AS message;
|
||||
|
||||
+3
-3
@@ -54,7 +54,7 @@
|
||||
| `PROCESSING` | 履约处理中。 |
|
||||
| `COMPLETED` | 订单完成。 |
|
||||
|
||||
统一订单列表的主状态是展示分组,不等于订单表的内部状态:`PENDING_REVIEW`、`WAIT_MEASURE`、`INBOUNDED`、`LOADING`、`LOADED`、`DELIVERED`。
|
||||
统一订单列表的主状态是展示分组,不等于订单表的内部状态:`PENDING_REVIEW`、`WAIT_MEASURE`、`WAIT_SPLIT`、`LOADING`、`LOADED`、`DELIVERED`。
|
||||
|
||||
## 仓库与库位
|
||||
|
||||
@@ -89,13 +89,13 @@
|
||||
| `WAIT_PRICING` | 测量通过或异常放行,等待计费。 |
|
||||
| `FEE_CONFIRMING` | 已生成费用,等待费用确认和发送。 |
|
||||
| `INBOUNDING` | 费用已确认,正在占用托盘、库位并完成入库。 |
|
||||
| `INBOUNDED` | 入库完成,可进入智能装柜或预装确认;费用状态独立维护。 |
|
||||
| `WAIT_SPLIT` | 入库完成,可进入智能装柜或预装确认;费用状态独立维护。 |
|
||||
| `LOADING` | 已进入装柜作业,PDA 可执行拣货。 |
|
||||
| `LOADED` | 装柜完成,等待派送完成。 |
|
||||
| `DELIVERED` | 已完成派送。 |
|
||||
| `VOIDED` | 已作废,不允许继续处理。 |
|
||||
|
||||
主链路为:`WAIT_MEASURE` -> `MEASURING` -> `MEASURE_PENDING` -> `WAIT_PRICING` -> `FEE_CONFIRMING` -> `INBOUNDING` -> `INBOUNDED` -> `LOADING` -> `LOADED` -> `DELIVERED`。测量驳回走 `MEASURE_PENDING` -> `MEASURE_REJECTED`;异常放行后回到 `WAIT_PRICING`。
|
||||
主链路为:`WAIT_MEASURE` -> `MEASURING` -> `MEASURE_PENDING` -> `WAIT_PRICING` -> `FEE_CONFIRMING` -> `INBOUNDING` -> `WAIT_SPLIT` -> `LOADING` -> `LOADED` -> `DELIVERED`。测量驳回走 `MEASURE_PENDING` -> `MEASURE_REJECTED`;异常放行后回到 `WAIT_PRICING`。
|
||||
|
||||
### 测量、重量和费用状态
|
||||
|
||||
|
||||
+3
-3
@@ -27,7 +27,7 @@ public class FinancePhase1OperationController {
|
||||
public FinancePhase1OperationController(FinancePhase1OperationService service) { this.service = service; }
|
||||
|
||||
@GetMapping("/accounts")
|
||||
@PreAuthorize("hasAnyAuthority('platform:finance:customer-account:list','platform:finance:account:view')")
|
||||
@PreAuthorize("hasAuthority('platform:finance:customer-account:list')")
|
||||
public Object listDerivedCustomerAccounts(@RequestParam(defaultValue = "1") int page,
|
||||
@RequestParam(defaultValue = "20") int pageSize,
|
||||
@RequestParam(required = false) String keyword,
|
||||
@@ -36,7 +36,7 @@ public class FinancePhase1OperationController {
|
||||
}
|
||||
|
||||
@GetMapping("/accounts/{customerId}/ledger")
|
||||
@PreAuthorize("hasAnyAuthority('platform:finance:customer-account:list','platform:finance:account:view')")
|
||||
@PreAuthorize("hasAuthority('platform:finance:customer-account:list')")
|
||||
public Object listDerivedCustomerAccountLedger(@PathVariable long customerId,
|
||||
@RequestParam(defaultValue = "1") int page,
|
||||
@RequestParam(defaultValue = "20") int pageSize,
|
||||
@@ -47,7 +47,7 @@ public class FinancePhase1OperationController {
|
||||
}
|
||||
|
||||
@GetMapping("/fee-sources/{feeSourceId}")
|
||||
@PreAuthorize("hasAnyAuthority('platform:finance:customer-arap:list','platform:finance:fee-source:view')")
|
||||
@PreAuthorize("hasAuthority('platform:finance:customer-arap:list')")
|
||||
public ApiResponse<?> getFinanceFeeSource(@PathVariable long feeSourceId) {
|
||||
return FinanceResponses.success(service.getFinanceFeeSource(feeSourceId));
|
||||
}
|
||||
|
||||
+4
-7
@@ -41,7 +41,7 @@ public class FinanceSettlementController {
|
||||
}
|
||||
|
||||
@GetMapping("/receivable-settlements")
|
||||
@PreAuthorize("hasAnyAuthority('platform:finance:settlement-ar:list','platform:finance:settlement-ar:view')")
|
||||
@PreAuthorize("hasAuthority('platform:finance:settlement-ar:list')")
|
||||
@Operation(summary = "查询正式应收结算单")
|
||||
public SettlementListEnvelope receivableSettlements(
|
||||
@Parameter(description = "可选客户筛选;省略时查询全部客户")
|
||||
@@ -50,7 +50,7 @@ public class FinanceSettlementController {
|
||||
}
|
||||
|
||||
@GetMapping("/payable-settlements")
|
||||
@PreAuthorize("hasAnyAuthority('platform:finance:settlement-ap:list','platform:finance:settlement-ap:view')")
|
||||
@PreAuthorize("hasAuthority('platform:finance:settlement-ap:list')")
|
||||
@Operation(summary = "查询正式应付结算单")
|
||||
public SettlementListEnvelope payableSettlements(
|
||||
@Parameter(description = "可选客户筛选;省略时查询全部客户")
|
||||
@@ -77,17 +77,14 @@ public class FinanceSettlementController {
|
||||
}
|
||||
|
||||
@GetMapping("/settlements/{settlementId}")
|
||||
@PreAuthorize("hasAnyAuthority('platform:finance:settlement-ar:list','platform:finance:settlement-ar:view','platform:finance:settlement-ap:list','platform:finance:settlement-ap:view')")
|
||||
@PreAuthorize("hasAnyAuthority('platform:finance:settlement-ar:list','platform:finance:settlement-ap:list')")
|
||||
public SettlementDetailEnvelope getSettlement(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable long settlementId) {
|
||||
SettlementDetail detail = service.getSettlement(settlementId);
|
||||
String direction = detail.summary().direction();
|
||||
String listPermission = "RECEIVABLE".equals(direction)
|
||||
? "platform:finance:settlement-ar:list" : "platform:finance:settlement-ap:list";
|
||||
String legacyViewPermission = "RECEIVABLE".equals(direction)
|
||||
? "platform:finance:settlement-ar:view" : "platform:finance:settlement-ap:view";
|
||||
if (user == null || (!user.permissions().contains(listPermission)
|
||||
&& !user.permissions().contains(legacyViewPermission))) {
|
||||
if (user == null || !user.permissions().contains(listPermission)) {
|
||||
throw new AccessDeniedException("无权查看该方向结算单");
|
||||
}
|
||||
return success(detail);
|
||||
|
||||
@@ -45,7 +45,7 @@ public class PlatformFinanceController {
|
||||
}
|
||||
|
||||
@GetMapping("/customer-accounts/{customerId}")
|
||||
@PreAuthorize("hasAnyAuthority('platform:finance:customer-account:list','platform:finance:account:view','platform:finance:bill:view')")
|
||||
@PreAuthorize("hasAuthority('platform:finance:customer-account:list')")
|
||||
@Operation(summary = "客户人工应收账户及流水")
|
||||
public ApiResponse<?> customerAccount(@PathVariable long customerId) {
|
||||
return FinanceResponses.success(queryService.customerAccount(customerId));
|
||||
@@ -59,7 +59,7 @@ public class PlatformFinanceController {
|
||||
}
|
||||
|
||||
@GetMapping("/fee-pool")
|
||||
@PreAuthorize("hasAnyAuthority('platform:finance:customer-arap:list','platform:finance:bill:view')")
|
||||
@PreAuthorize("hasAuthority('platform:finance:customer-arap:list')")
|
||||
@Operation(summary = "客户应收应付费用池")
|
||||
public ApiResponse<?> feePool(
|
||||
@Parameter(description = "可选客户筛选;省略时查询全部客户")
|
||||
@@ -69,7 +69,7 @@ public class PlatformFinanceController {
|
||||
}
|
||||
|
||||
@GetMapping("/statements")
|
||||
@PreAuthorize("hasAnyAuthority('platform:finance:customer-statement:list','platform:finance:bill:view')")
|
||||
@PreAuthorize("hasAuthority('platform:finance:customer-statement:list')")
|
||||
@Operation(summary = "客户对账单")
|
||||
public ApiResponse<?> statements(
|
||||
@Parameter(description = "可选客户筛选;省略时查询全部客户")
|
||||
@@ -94,7 +94,7 @@ public class PlatformFinanceController {
|
||||
public ApiResponse<?> terms(@PathVariable long tenantId) { return FinanceResponses.success(termsView(tenantId, service.customerTerms(tenantId))); }
|
||||
|
||||
@GetMapping("/bills")
|
||||
@PreAuthorize("hasAnyAuthority('platform:finance:customer-statement:list','platform:finance:bill:view')")
|
||||
@PreAuthorize("hasAuthority('platform:finance:customer-statement:list')")
|
||||
@Operation(summary = "查询平台账单")
|
||||
public ApiResponse<?> bills(@RequestParam long customerId) { return FinanceResponses.success(queryService.statements(customerId, false)); }
|
||||
|
||||
@@ -122,11 +122,11 @@ public class PlatformFinanceController {
|
||||
}
|
||||
|
||||
@GetMapping("/bills/{billId}")
|
||||
@PreAuthorize("hasAnyAuthority('platform:finance:customer-statement:list','platform:finance:bill:view')")
|
||||
@PreAuthorize("hasAuthority('platform:finance:customer-statement:list')")
|
||||
public ApiResponse<?> bill(@PathVariable long billId) { return FinanceResponses.success(queryService.platformStatement(billId)); }
|
||||
|
||||
@GetMapping("/bills/{billId}/revisions")
|
||||
@PreAuthorize("hasAnyAuthority('platform:finance:customer-statement:list','platform:finance:bill:view')")
|
||||
@PreAuthorize("hasAuthority('platform:finance:customer-statement:list')")
|
||||
public ApiResponse<?> revisions(@PathVariable long billId, @RequestParam long customerId) {
|
||||
return FinanceResponses.success(queryService.billRevisions(customerId, billId));
|
||||
}
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ public class ReconciliationStatementController {
|
||||
}
|
||||
|
||||
@GetMapping("/reconciliation-statements/{statementId}")
|
||||
@PreAuthorize("hasAnyAuthority('platform:finance:customer-statement:list','platform:finance:bill:view')")
|
||||
@PreAuthorize("hasAuthority('platform:finance:customer-statement:list')")
|
||||
public ApiResponse<ReconciliationDetail> getReconciliationStatement(@PathVariable long statementId) {
|
||||
return FinanceResponses.success(service.detailForPlatform(statementId));
|
||||
}
|
||||
|
||||
@@ -15,20 +15,20 @@ class FinancePhase1PermissionContractTest {
|
||||
@Test
|
||||
void freezesG1AndG2ControllerPermissionsAndHttpActions() throws Exception {
|
||||
assertEndpoint("listDerivedCustomerAccounts", GetMapping.class,
|
||||
"platform:finance:customer-account:list", "platform:finance:account:view",
|
||||
"platform:finance:customer-account:list",
|
||||
int.class, int.class, String.class, String.class);
|
||||
assertEndpoint("listDerivedCustomerAccountLedger", GetMapping.class,
|
||||
"platform:finance:customer-account:list", "platform:finance:account:view",
|
||||
"platform:finance:customer-account:list",
|
||||
long.class, int.class, int.class,
|
||||
String.class, java.time.LocalDate.class, java.time.LocalDate.class);
|
||||
assertEndpoint("getFinanceFeeSource", GetMapping.class,
|
||||
"platform:finance:customer-arap:list", "platform:finance:fee-source:view", long.class);
|
||||
"platform:finance:customer-arap:list", long.class);
|
||||
assertEndpoint("reviewFinanceFeeSource", PostMapping.class,
|
||||
"platform:finance:fee-source:review", null,
|
||||
"platform:finance:fee-source:review",
|
||||
com.baiyue.common.security.AuthenticatedUser.class, long.class, String.class,
|
||||
com.baiyue.finance.dto.FinancePhase1Dtos.ReviewRequest.class);
|
||||
assertEndpoint("updateFeeSourceSettlementInclusion", PutMapping.class,
|
||||
"platform:finance:fee-source:settlement", null,
|
||||
"platform:finance:fee-source:settlement",
|
||||
com.baiyue.common.security.AuthenticatedUser.class, long.class, String.class,
|
||||
com.baiyue.finance.dto.FinancePhase1Dtos.SettlementInclusionRequest.class);
|
||||
}
|
||||
@@ -36,13 +36,10 @@ class FinancePhase1PermissionContractTest {
|
||||
private static void assertEndpoint(String methodName,
|
||||
Class<? extends java.lang.annotation.Annotation> mapping,
|
||||
String authority,
|
||||
String compatibleAuthority,
|
||||
Class<?>... parameterTypes) throws Exception {
|
||||
Method method = FinancePhase1OperationController.class.getMethod(methodName, parameterTypes);
|
||||
assertThat(method.isAnnotationPresent(mapping)).isTrue();
|
||||
assertThat(method.getAnnotation(PreAuthorize.class).value()).contains(authority);
|
||||
if (compatibleAuthority != null) {
|
||||
assertThat(method.getAnnotation(PreAuthorize.class).value()).contains(compatibleAuthority);
|
||||
}
|
||||
assertThat(method.getAnnotation(PreAuthorize.class).value()).doesNotContain(":view");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package com.baiyue.finance;
|
||||
|
||||
import com.baiyue.finance.controller.PlatformFinanceController;
|
||||
import com.baiyue.finance.controller.FinanceSettlementController;
|
||||
import com.baiyue.finance.controller.FinancePhase1OperationController;
|
||||
import com.baiyue.finance.controller.ReconciliationStatementController;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
@@ -30,10 +32,35 @@ class FinancePlatformListContractTest {
|
||||
Map<String, String> expected = Map.of(
|
||||
"feePool", "platform:finance:customer-arap:list",
|
||||
"statements", "platform:finance:customer-statement:list",
|
||||
"receivableSettlements", "platform:finance:settlement-ar:view",
|
||||
"payableSettlements", "platform:finance:settlement-ap:view");
|
||||
"receivableSettlements", "platform:finance:settlement-ar:list",
|
||||
"payableSettlements", "platform:finance:settlement-ap:list");
|
||||
for (var entry : expected.entrySet()) {
|
||||
assertThat(find(entry.getKey()).getAnnotation(PreAuthorize.class).value()).contains(entry.getValue());
|
||||
assertThat(find(entry.getKey()).getAnnotation(PreAuthorize.class).value())
|
||||
.contains(entry.getValue())
|
||||
.doesNotContain(":view");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformReadEndpointsDoNotAcceptLegacyViewAuthorities() {
|
||||
String[] legacyAuthorities = {
|
||||
"platform:finance:account:view",
|
||||
"platform:finance:fee-source:view",
|
||||
"platform:finance:bill:view",
|
||||
"platform:finance:settlement-ar:view",
|
||||
"platform:finance:settlement-ap:view"
|
||||
};
|
||||
for (Class<?> controller : new Class<?>[]{
|
||||
PlatformFinanceController.class,
|
||||
FinancePhase1OperationController.class,
|
||||
ReconciliationStatementController.class,
|
||||
FinanceSettlementController.class
|
||||
}) {
|
||||
for (Method method : controller.getDeclaredMethods()) {
|
||||
PreAuthorize permission = method.getAnnotation(PreAuthorize.class);
|
||||
if (permission == null) continue;
|
||||
assertThat(permission.value()).doesNotContain(legacyAuthorities);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ class FinanceSettlementBehaviorTest {
|
||||
FinanceSettlementController controller = new FinanceSettlementController(controllerService);
|
||||
SettlementDetail payable = detail("PAYABLE", "ISSUED", BigDecimal.ZERO, new BigDecimal("100.00"), 0);
|
||||
when(controllerService.getSettlement(88L)).thenReturn(payable);
|
||||
AuthenticatedUser apViewer = user(Set.of("platform:finance:settlement-ap:view"));
|
||||
AuthenticatedUser apViewer = user(Set.of("platform:finance:settlement-ap:list"));
|
||||
|
||||
var response = controller.getSettlement(apViewer, 88L);
|
||||
|
||||
@@ -62,7 +62,7 @@ class FinanceSettlementBehaviorTest {
|
||||
assertThat(response.data()).isSameAs(payable);
|
||||
assertThat(response.requestId()).isNotBlank();
|
||||
assertThatThrownBy(() -> controller.getSettlement(
|
||||
user(Set.of("platform:finance:settlement-ar:view")), 88L)).isInstanceOf(AccessDeniedException.class);
|
||||
user(Set.of("platform:finance:settlement-ar:list")), 88L)).isInstanceOf(AccessDeniedException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ public class MerchantInboundBillingController {
|
||||
@GetMapping("/{orderId}/inbound-billing")
|
||||
@PreAuthorize("hasAuthority('merchant:order:view')")
|
||||
@Operation(summary = "查询订单入库结果和最新费用",
|
||||
description = "客户只读查看已达到 INBOUNDED + APPROVED + CONFIRMED 的二期费用,不需要确认费用。")
|
||||
description = "客户只读查看已达到 WAIT_SPLIT + APPROVED + CONFIRMED 的二期费用,不需要确认费用。")
|
||||
public ApiResponse<MerchantInboundBillingResponse> find(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable long orderId) {
|
||||
return ApiResponse.success(service.find(FirstlegScopeGuard.merchant(user), orderId));
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@ public class PlatformInboundMeasurementReviewController {
|
||||
@Valid @RequestBody MeasurementReviewRequest request) {
|
||||
validateScope(user, tenantId);
|
||||
service.approve(tenantId, user.userId(), user.username(), inboundId, idempotencyKey, request);
|
||||
// 统一订单主线要求测量审核后直接进入 INBOUNDED,不把计费、库位或其他资料
|
||||
// 统一订单主线要求测量审核后直接进入 WAIT_SPLIT,不把计费、库位或其他资料
|
||||
// 作为本版本的强制门禁。保留原 FeeSnapshotResponse 泛型以兼容既有客户端。
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ public enum InboundListTab {
|
||||
WAIT_REVIEW(List.of("MEASURE_PENDING")),
|
||||
/** 待发送费用:等待计费、费用确认或正在完成入库。 */
|
||||
WAIT_FEE_SEND(List.of("WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING")),
|
||||
/** 已入库:费用已发送且入库完成。 */
|
||||
INBOUNDED(List.of("INBOUNDED"));
|
||||
/** 待分柜:入库和费用发布完成,等待形成有效分柜方案。 */
|
||||
WAIT_SPLIT(List.of("WAIT_SPLIT"));
|
||||
|
||||
private final List<String> processStatuses;
|
||||
|
||||
|
||||
@@ -18,8 +18,6 @@ public final class InboundProcessStatus {
|
||||
public static final String FEE_CONFIRMING = "FEE_CONFIRMING";
|
||||
/** 费用已确认,正在执行托盘、库位占用和入库收口。 */
|
||||
public static final String INBOUNDING = "INBOUNDING";
|
||||
/** 历史已入库终态;存量数据仍允许进入分柜。 */
|
||||
public static final String INBOUNDED = "INBOUNDED";
|
||||
/** 入库和费用发布完成,等待分柜。 */
|
||||
public static final String WAIT_SPLIT = "WAIT_SPLIT";
|
||||
/** 已进入装柜作业,PDA 可执行拣货。 */
|
||||
@@ -33,7 +31,7 @@ public final class InboundProcessStatus {
|
||||
|
||||
public static final Set<String> VALUES = Set.of(
|
||||
WAIT_MEASURE, MEASURING, MEASURE_PENDING, MEASURE_REJECTED,
|
||||
WAIT_PRICING, FEE_CONFIRMING, INBOUNDING, INBOUNDED, WAIT_SPLIT,
|
||||
WAIT_PRICING, FEE_CONFIRMING, INBOUNDING, WAIT_SPLIT,
|
||||
LOADING, LOADED, DELIVERED, VOIDED
|
||||
);
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ public record UnifiedInboundQuery(
|
||||
Long tenantId,
|
||||
@Size(max = 64) String keyword,
|
||||
@Size(max = 64) String orderNo,
|
||||
@Schema(allowableValues = {"PENDING_REVIEW", "WAIT_MEASURE", "INBOUNDED", "WAIT_SPLIT",
|
||||
@Schema(allowableValues = {"PENDING_REVIEW", "WAIT_MEASURE", "WAIT_SPLIT",
|
||||
"LOADING", "LOADED", "DELIVERED"})
|
||||
@Size(max = 32) String mainStatus,
|
||||
Long customerId,
|
||||
|
||||
@@ -62,7 +62,7 @@ public interface LoadingInboundMapper {
|
||||
AND loaded.process_status<>'LOADED' AND loaded.deleted=0
|
||||
SET root.process_status='LOADED', root.updated_by=#{operatorId}, root.version=root.version+1
|
||||
WHERE root.tenant_id=#{tenantId} AND root.id=#{rootInboundId} AND root.deleted=0
|
||||
AND root.inbound_role='MASTER' AND root.process_status IN ('INBOUNDED','LOADING')
|
||||
AND root.inbound_role='MASTER' AND root.process_status IN ('WAIT_SPLIT','LOADING')
|
||||
AND remainder.id IS NULL AND loaded.id IS NULL
|
||||
""")
|
||||
int aggregateRootLoaded(@Param("tenantId") long tenantId,
|
||||
|
||||
@@ -46,7 +46,7 @@ public interface InboundSplitSettlementMapper {
|
||||
express_tracking_no,public_remark,created_by,created_by_name,created_at,updated_by,updated_at,deleted,version,
|
||||
inbound_role,parent_inbound_id,root_inbound_id,split_sequence,split_status,split_purpose)
|
||||
SELECT #{childId},tenant_id,task_id,#{childOrderId},#{childOrderNo},#{childInboundCode},
|
||||
CASE WHEN #{splitPurpose}='LOADED' THEN 'LOADING' ELSE 'INBOUNDED' END,order_status_snapshot,
|
||||
CASE WHEN #{splitPurpose}='LOADED' THEN 'LOADING' ELSE 'WAIT_SPLIT' END,order_status_snapshot,
|
||||
'APPROVED',fee_status,fee_review_status,notification_status,expected_arrival_at,customer_id,customer_name,
|
||||
company_id,company_name,warehouse_id,warehouse_name,delivery_warehouse_id,delivery_warehouse_name,business_type,
|
||||
channel_code,channel_name,carrier_id,carrier_name,tax_mode,pricing_tax_mode,container_mode,customs_inspection,
|
||||
@@ -151,7 +151,7 @@ public interface InboundSplitSettlementMapper {
|
||||
SET package_count = GREATEST(COALESCE(package_count, total_package_count) - #{packageCount}, 0),
|
||||
split_status = 'PARTIALLY_SPLIT', updated_by = #{operatorId}, version = version + 1
|
||||
WHERE id = #{parentInboundId} AND tenant_id = #{accountId} AND deleted = 0
|
||||
AND inbound_role = 'MASTER' AND process_status = 'INBOUNDED' AND version = #{expectedVersion}
|
||||
AND inbound_role = 'MASTER' AND process_status = 'WAIT_SPLIT' AND version = #{expectedVersion}
|
||||
""")
|
||||
int markParentPartiallySplit(@Param("accountId") long accountId, @Param("parentInboundId") long parentInboundId,
|
||||
@Param("packageCount") int packageCount, @Param("operatorId") long operatorId,
|
||||
@@ -160,7 +160,7 @@ public interface InboundSplitSettlementMapper {
|
||||
@Update("UPDATE fl_inbound_order SET package_count=#{loadedPackageCount},process_status='LOADING',"
|
||||
+ "split_status='FULLY_SPLIT',updated_by=#{operatorId},version=version+1 "
|
||||
+ "WHERE id=#{sourceInboundId} AND tenant_id=#{accountId} AND deleted=0 "
|
||||
+ "AND process_status IN ('INBOUNDED','LOADING') AND split_status='NOT_SPLIT' "
|
||||
+ "AND process_status IN ('WAIT_SPLIT','LOADING') AND split_status='NOT_SPLIT' "
|
||||
+ "AND COALESCE(package_count,total_package_count)>=#{loadedPackageCount} AND version=#{expectedVersion}")
|
||||
int markSourceLoaded(@Param("accountId") long accountId, @Param("sourceInboundId") long sourceInboundId,
|
||||
@Param("loadedPackageCount") int loadedPackageCount,
|
||||
|
||||
@@ -23,7 +23,7 @@ import com.baiyue.firstleg.dto.SmartLoadingDtos.Candidate; import org.apache.iba
|
||||
AND m.id=(SELECT MAX(m2.id) FROM fl_inbound_measurement m2
|
||||
WHERE m2.tenant_id=o.tenant_id AND m2.inbound_id=o.id AND m2.status='APPROVED')
|
||||
JOIN fl_inbound_measurement_item i ON i.tenant_id=o.tenant_id AND i.measurement_id=m.id
|
||||
WHERE o.warehouse_id=#{warehouseId} AND o.process_status IN ('WAIT_SPLIT','INBOUNDED')
|
||||
WHERE o.warehouse_id=#{warehouseId} AND o.process_status='WAIT_SPLIT'
|
||||
AND (o.inbound_role='MASTER' OR (o.split_purpose='REMAINDER' AND o.split_status='NOT_SPLIT'))
|
||||
AND i.inbound_detail_id IS NOT NULL
|
||||
AND i.package_count>COALESCE((SELECT SUM(li.package_count) FROM fl_loading_plan_item li
|
||||
@@ -61,7 +61,7 @@ import com.baiyue.firstleg.dto.SmartLoadingDtos.Candidate; import org.apache.iba
|
||||
AND m.id=(SELECT MAX(m2.id) FROM fl_inbound_measurement m2
|
||||
WHERE m2.tenant_id=o.tenant_id AND m2.inbound_id=o.id AND m2.status='APPROVED')
|
||||
JOIN fl_inbound_measurement_item i ON i.tenant_id=o.tenant_id AND i.measurement_id=m.id
|
||||
WHERE o.warehouse_id=#{warehouseId} AND o.id=#{inboundId} AND o.process_status IN ('WAIT_SPLIT','INBOUNDED')
|
||||
WHERE o.warehouse_id=#{warehouseId} AND o.id=#{inboundId} AND o.process_status='WAIT_SPLIT'
|
||||
AND (o.inbound_role='MASTER' OR (o.split_purpose='REMAINDER' AND o.split_status='NOT_SPLIT'))
|
||||
AND i.inbound_detail_id=#{detailId}
|
||||
ORDER BY m.id DESC LIMIT 1 FOR UPDATE
|
||||
|
||||
@@ -209,7 +209,7 @@ public class FbaPricingRepository {
|
||||
.eq(InboundPricingEntity::getProcessStatus, processStatus)
|
||||
.in(InboundPricingEntity::getFeeStatus, List.of("UNPRICED", "CALCULATED"))
|
||||
.set(InboundPricingEntity::getFeeStatus, "CALCULATED");
|
||||
if (!List.of("WAIT_SPLIT", "INBOUNDED").contains(processStatus)) {
|
||||
if (!"WAIT_SPLIT".equals(processStatus)) {
|
||||
update.set(InboundPricingEntity::getProcessStatus, "FEE_CONFIRMING");
|
||||
}
|
||||
if ("KG".equals(chargeMode)) update.set(InboundPricingEntity::getChargeableWeightKg, chargeable);
|
||||
@@ -229,7 +229,7 @@ public class FbaPricingRepository {
|
||||
public int markInboundConfirmed(long tenantId, long inboundId) {
|
||||
return inbounds.update(null, new LambdaUpdateWrapper<InboundPricingEntity>()
|
||||
.eq(InboundPricingEntity::getTenantId, tenantId).eq(InboundPricingEntity::getId, inboundId)
|
||||
.in(InboundPricingEntity::getProcessStatus, List.of("FEE_CONFIRMING", "WAIT_SPLIT", "INBOUNDED"))
|
||||
.in(InboundPricingEntity::getProcessStatus, List.of("FEE_CONFIRMING", "WAIT_SPLIT"))
|
||||
.eq(InboundPricingEntity::getFeeStatus, "CALCULATED")
|
||||
.set(InboundPricingEntity::getFeeStatus, "CONFIRMED"));
|
||||
}
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@ public class AutomaticInboundCompletionService {
|
||||
}
|
||||
|
||||
private static boolean completed(InboundOrderEntity order) {
|
||||
return ("WAIT_SPLIT".equals(order.getProcessStatus()) || "INBOUNDED".equals(order.getProcessStatus()))
|
||||
return "WAIT_SPLIT".equals(order.getProcessStatus())
|
||||
&& "SENT".equals(order.getFeeStatus());
|
||||
}
|
||||
|
||||
|
||||
@@ -209,7 +209,7 @@ public class FbaPricingService {
|
||||
|
||||
private InboundPricingEntity requireEligibleInbound(long tenantId, long inboundId) {
|
||||
InboundPricingEntity inbound = requireApprovedInbound(tenantId, inboundId);
|
||||
if (!List.of("WAIT_PRICING", "FEE_CONFIRMING", "WAIT_SPLIT", "INBOUNDED").contains(inbound.getProcessStatus())
|
||||
if (!List.of("WAIT_PRICING", "FEE_CONFIRMING", "WAIT_SPLIT").contains(inbound.getProcessStatus())
|
||||
|| !List.of("UNPRICED", "CALCULATED").contains(inbound.getFeeStatus())) {
|
||||
throw conflict("当前入库单状态不允许费用试算");
|
||||
}
|
||||
@@ -235,7 +235,7 @@ public class FbaPricingService {
|
||||
}
|
||||
|
||||
private static boolean isCompletedInbound(String processStatus) {
|
||||
return "WAIT_SPLIT".equals(processStatus) || "INBOUNDED".equals(processStatus);
|
||||
return "WAIT_SPLIT".equals(processStatus);
|
||||
}
|
||||
|
||||
private static PricingDestinationType destinationType(InboundPricingEntity inbound) {
|
||||
|
||||
+1
-1
@@ -114,7 +114,7 @@ public class MerchantInboundBillingService {
|
||||
}
|
||||
|
||||
private static boolean isPublished(InboundOrderEntity inbound) {
|
||||
return (InboundProcessStatus.INBOUNDED.equals(inbound.getProcessStatus())
|
||||
return (InboundProcessStatus.WAIT_SPLIT.equals(inbound.getProcessStatus())
|
||||
|| "WAIT_SPLIT".equals(inbound.getProcessStatus()))
|
||||
&& "APPROVED".equals(inbound.getMeasureReviewStatus())
|
||||
&& "CONFIRMED".equals(inbound.getFeeStatus());
|
||||
|
||||
@@ -98,7 +98,7 @@ public class PdaPickSettlementService {
|
||||
ParentRow parent = rootInboundId == parentInboundId ? root : splits.lockParent(parentInboundId);
|
||||
if (parent == null || parent.tenantId() == null || parent.tenantId() <= 0
|
||||
|| !("MASTER".equals(parent.inboundRole()) || "CHILD".equals(parent.inboundRole()))
|
||||
|| !("INBOUNDED".equals(parent.processStatus()) || "LOADING".equals(parent.processStatus()))
|
||||
|| !("WAIT_SPLIT".equals(parent.processStatus()) || "LOADING".equals(parent.processStatus()))
|
||||
|| ("MASTER".equals(parent.inboundRole()) && "FULLY_SPLIT".equals(parent.splitStatus()))) {
|
||||
throw conflict("待拆分主入库单不在可拆分状态");
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ import java.util.stream.Collectors;
|
||||
@Service
|
||||
public class UnifiedInboundQueryService {
|
||||
private static final Set<String> MAIN_STATUSES = Set.of(
|
||||
"PENDING_REVIEW", "WAIT_MEASURE", "INBOUNDED", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED");
|
||||
"PENDING_REVIEW", "WAIT_MEASURE", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED");
|
||||
private static final Set<String> REVIEW_STATUSES = Set.of("UNRECORDED", "PENDING", "APPROVED", "REJECTED");
|
||||
|
||||
@Resource private InboundOrderRepository inbounds;
|
||||
@@ -140,7 +140,8 @@ public class UnifiedInboundQueryService {
|
||||
return switch (status) {
|
||||
case "WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED",
|
||||
"WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING" -> "WAIT_MEASURE";
|
||||
case "INBOUNDED", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED" -> status;
|
||||
case "WAIT_SPLIT" -> "WAIT_SPLIT";
|
||||
case "LOADING", "LOADED", "DELIVERED" -> status;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
@@ -150,7 +151,7 @@ public class UnifiedInboundQueryService {
|
||||
return switch (mainStatus) {
|
||||
case "PENDING_REVIEW" -> "APPROVE_ORDER";
|
||||
case "WAIT_MEASURE" -> "MEASURE_PENDING".equals(processStatus) ? "CONFIRM_MEASUREMENT" : null;
|
||||
case "INBOUNDED", "WAIT_SPLIT" -> !("INBOUNDED".equals(processStatus) || "WAIT_SPLIT".equals(processStatus)) ? null
|
||||
case "WAIT_SPLIT" -> !"WAIT_SPLIT".equals(processStatus) ? null
|
||||
: "CONFIRMED".equals(preloadStatus) ? null
|
||||
: preloadStatus == null ? "SMART_SPLIT" : "CONFIRM_PRELOAD";
|
||||
case "LOADING" -> "COMPLETE_LOADING";
|
||||
@@ -169,7 +170,6 @@ public class UnifiedInboundQueryService {
|
||||
return switch (mainStatus) {
|
||||
case "WAIT_MEASURE" -> List.of("WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED",
|
||||
"WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING");
|
||||
case "INBOUNDED" -> List.of("INBOUNDED");
|
||||
case "WAIT_SPLIT" -> List.of("WAIT_SPLIT");
|
||||
case "LOADING", "LOADED", "DELIVERED" -> List.of(mainStatus);
|
||||
default -> List.of("__NO_MATCH__");
|
||||
|
||||
@@ -13,7 +13,7 @@ public interface SplitInboundMapper {
|
||||
|
||||
@Update("UPDATE fl_inbound_order SET process_status='LOADING', updated_by=#{operatorId}, "
|
||||
+ "version=version+1 WHERE tenant_id=#{tenantId} AND id=#{inboundId} AND deleted=0 "
|
||||
+ "AND process_status IN ('WAIT_SPLIT','INBOUNDED') AND version=#{expectedVersion}")
|
||||
+ "AND process_status='WAIT_SPLIT' AND version=#{expectedVersion}")
|
||||
int beginLoading(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId,
|
||||
@Param("operatorId") long operatorId, @Param("expectedVersion") int expectedVersion);
|
||||
|
||||
|
||||
@@ -177,7 +177,7 @@ public class SplitPreloadService {
|
||||
}
|
||||
|
||||
private static boolean isWaitingSplit(String status) {
|
||||
return "WAIT_SPLIT".equals(status) || "INBOUNDED".equals(status);
|
||||
return "WAIT_SPLIT".equals(status);
|
||||
}
|
||||
|
||||
private void addHistory(long tenantId, long inboundId, String from, String to, String action,
|
||||
|
||||
@@ -11,7 +11,7 @@ CREATE TABLE IF NOT EXISTS fl_inbound_order (
|
||||
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 '入库流程状态:WAIT_MEASURE待测量、MEASURING测量中、MEASURE_PENDING待审核、MEASURE_REJECTED测量驳回、WAIT_PRICING待计费、FEE_CONFIRMING费用确认中、INBOUNDING入库收口中、INBOUNDED已入库、WAIT_SPLIT历史待分柜',
|
||||
process_status VARCHAR(24) NOT NULL DEFAULT 'WAIT_MEASURE' COMMENT '入库流程状态:WAIT_MEASURE待测量、MEASURING测量中、MEASURE_PENDING待审核、MEASURE_REJECTED测量驳回、WAIT_PRICING待计费、FEE_CONFIRMING费用确认中、INBOUNDING入库收口中、WAIT_SPLIT待分柜',
|
||||
order_status_snapshot VARCHAR(24) NOT NULL 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已作废',
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
-- 新完成入库记录使用 WAIT_SPLIT,存量 INBOUNDED 继续作为可分柜兼容状态。
|
||||
-- 本脚本不改写业务主表或历史状态,避免影响既有审计记录。
|
||||
-- 入库完成后的唯一状态为 WAIT_SPLIT。
|
||||
USE baiyue_firstleg;
|
||||
|
||||
DROP PROCEDURE IF EXISTS assert_inbounded_compatibility;
|
||||
DROP PROCEDURE IF EXISTS assert_wait_split_status;
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE assert_inbounded_compatibility()
|
||||
CREATE PROCEDURE assert_wait_split_status()
|
||||
BEGIN
|
||||
IF (SELECT COUNT(*) FROM information_schema.columns
|
||||
WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_inbound_order'
|
||||
@@ -13,9 +12,9 @@ BEGIN
|
||||
WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_inbound_status_history'
|
||||
AND column_name IN ('from_status', 'to_status')) <> 2 THEN
|
||||
SIGNAL SQLSTATE '45000'
|
||||
SET MESSAGE_TEXT = 'WAIT_SPLIT/INBOUNDED 兼容校验失败:入库状态字段不完整';
|
||||
SET MESSAGE_TEXT = 'WAIT_SPLIT 状态校验失败:入库状态字段不完整';
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
CALL assert_inbounded_compatibility();
|
||||
DROP PROCEDURE assert_inbounded_compatibility;
|
||||
CALL assert_wait_split_status();
|
||||
DROP PROCEDURE assert_wait_split_status;
|
||||
|
||||
@@ -130,14 +130,14 @@ paths:
|
||||
/api/platform/v1/inbounds/{inboundId}/fees/calculate:
|
||||
post:
|
||||
summary: 按审核通过测量快照试算 FBA 费用
|
||||
description: 仅匹配当前唯一 ACTIVE 船期价格表中的 ACTIVE 价格行,按起运仓、渠道、目的仓、pricingTaxMode 和计费单位估价;不再以 carrierId 或客户计费规则作为前置。海派还需按FBA区域和重量阶梯匹配;测量审核后即使主流程已到 INBOUNDED,也可独立试算且不回退主流程状态;需要 firstleg:fba-pricing:calculate 权限。
|
||||
description: 仅匹配当前唯一 ACTIVE 船期价格表中的 ACTIVE 价格行,按起运仓、渠道、目的仓、pricingTaxMode 和计费单位估价;不再以 carrierId 或客户计费规则作为前置。海派还需按FBA区域和重量阶梯匹配;测量审核后即使主流程已到 WAIT_SPLIT,也可独立试算且不回退主流程状态;需要 firstleg:fba-pricing:calculate 权限。
|
||||
parameters: [{$ref: '#/components/parameters/TenantId'}, {$ref: '#/components/parameters/InboundId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
|
||||
requestBody: {content: {application/json: {schema: {$ref: '#/components/schemas/CalculateRequest'}}}}
|
||||
responses: {'200': {description: 成功或业务失败}, '403': {description: 无权限}}
|
||||
/api/platform/v1/inbounds/{inboundId}/fees/confirm:
|
||||
post:
|
||||
summary: 确认最新费用快照
|
||||
description: 确认费用只更新费用状态;主流程已到 INBOUNDED 时保持不变。
|
||||
description: 确认费用只更新费用状态;主流程已到 WAIT_SPLIT 时保持不变。
|
||||
parameters: [{$ref: '#/components/parameters/TenantId'}, {$ref: '#/components/parameters/InboundId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
|
||||
responses: {'200': {description: 成功或业务失败}, '403': {description: 无权限}}
|
||||
/api/platform/v1/inbounds/{inboundId}/fees/send:
|
||||
@@ -154,7 +154,7 @@ paths:
|
||||
/api/platform/v1/inbounds/{inboundId}/fees/published:
|
||||
put:
|
||||
summary: 调整已发布费用
|
||||
description: 需要 firstleg:fba-pricing:adjust 权限;生成新 SENT 版本并将旧版本标记 SUPERSEDED,不回退 INBOUNDED 状态。
|
||||
description: 需要 firstleg:fba-pricing:adjust 权限;生成新 SENT 版本并将旧版本标记 SUPERSEDED,不回退 WAIT_SPLIT 状态。
|
||||
parameters: [{$ref: '#/components/parameters/TenantId'}, {$ref: '#/components/parameters/InboundId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
|
||||
requestBody:
|
||||
required: true
|
||||
|
||||
@@ -68,7 +68,7 @@ paths:
|
||||
post:
|
||||
summary: 按统一订单条件分页查询履约摘要(Order 内部)
|
||||
description: >-
|
||||
mainStatus 接受七态产品状态;WAIT_SPLIT 为正式待分柜状态,INBOUNDED 仅兼容存量已入库记录。
|
||||
mainStatus 接受六态产品状态;WAIT_SPLIT 为入库完成后的唯一待分柜状态。
|
||||
计费与入库处理中状态在完成前仍归并为 WAIT_MEASURE,不提前开放分柜。
|
||||
security:
|
||||
- internalServiceToken: []
|
||||
@@ -102,7 +102,7 @@ paths:
|
||||
/internal/firstleg/v1/inbounds/unified-orders/detail:
|
||||
post:
|
||||
summary: 按订单 ID 查询统一履约详情(Order 内部)
|
||||
description: 时间线保留 Firstleg 内部 fromStatus/toStatus;summary.mainStatus 使用七态产品状态。
|
||||
description: 时间线保留 Firstleg 内部 fromStatus/toStatus;summary.mainStatus 使用六态产品状态。
|
||||
security:
|
||||
- internalServiceToken: []
|
||||
requestBody:
|
||||
@@ -390,7 +390,7 @@ paths:
|
||||
description: 需要 platform:firstleg:inbound:list 权限;默认查询全部企业,可选 tenantId 筛选;仅使用 V2 四个页签
|
||||
parameters:
|
||||
- {name: tenantId, in: query, required: false, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
- {name: tab, in: query, schema: {type: string, enum: [WAIT_MEASURE, WAIT_REVIEW, WAIT_FEE_SEND, INBOUNDED]}}
|
||||
- {name: tab, in: query, schema: {type: string, enum: [WAIT_MEASURE, WAIT_REVIEW, WAIT_FEE_SEND, WAIT_SPLIT]}}
|
||||
responses:
|
||||
'200':
|
||||
description: 成功或业务失败,按响应体 code 判断;每条记录携带后续客户隔离操作所需的 tenantId,该字段当前表示客户 ID
|
||||
@@ -565,7 +565,7 @@ paths:
|
||||
/api/merchant/v1/first-leg/orders/{orderId}/inbound-billing:
|
||||
get:
|
||||
summary: 客户查询已完成入库结果和最新已发布费用
|
||||
description: 需要 merchant:order:view 权限;客户只读查看,不需要确认费用,仅 INBOUNDED + APPROVED + CONFIRMED 的二期终态可见。
|
||||
description: 需要 merchant:order:view 权限;客户只读查看,不需要确认费用,仅 WAIT_SPLIT + APPROVED + CONFIRMED 的二期终态可见。
|
||||
parameters:
|
||||
- {name: orderId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
responses:
|
||||
@@ -656,8 +656,8 @@ components:
|
||||
schemas:
|
||||
UnifiedMainStatus:
|
||||
type: string
|
||||
enum: [PENDING_REVIEW, WAIT_MEASURE, INBOUNDED, WAIT_SPLIT, LOADING, LOADED, DELIVERED]
|
||||
description: 七态产品主状态;WAIT_SPLIT 为正式待分柜状态,INBOUNDED 仅兼容存量已入库记录
|
||||
enum: [PENDING_REVIEW, WAIT_MEASURE, WAIT_SPLIT, LOADING, LOADED, DELIVERED]
|
||||
description: 六态产品主状态;WAIT_SPLIT 为入库完成后的唯一待分柜状态
|
||||
UnifiedInboundQuery:
|
||||
type: object
|
||||
properties:
|
||||
@@ -666,9 +666,9 @@ components:
|
||||
orderNo: {type: string, maxLength: 64, nullable: true}
|
||||
mainStatus:
|
||||
type: string
|
||||
enum: [PENDING_REVIEW, WAIT_MEASURE, INBOUNDED, LOADING, LOADED, DELIVERED, WAIT_SPLIT]
|
||||
enum: [PENDING_REVIEW, WAIT_MEASURE, WAIT_SPLIT, LOADING, LOADED, DELIVERED]
|
||||
nullable: true
|
||||
description: WAIT_SPLIT 与 INBOUNDED 为独立筛选值
|
||||
description: WAIT_SPLIT 为唯一待分柜筛选值
|
||||
customerId: {type: integer, format: int64, nullable: true}
|
||||
receivingWarehouseId: {type: integer, format: int64, nullable: true}
|
||||
destinationWarehouseId: {type: integer, format: int64, nullable: true}
|
||||
@@ -767,7 +767,7 @@ components:
|
||||
orderId: {type: integer, format: int64, nullable: true}
|
||||
orderNo: {type: string, nullable: true}
|
||||
inboundCode: {type: string, nullable: true}
|
||||
processStatus: {type: string, nullable: true, description: Firstleg 内部状态;新入库完成记录进入 WAIT_SPLIT,历史记录可能为 INBOUNDED}
|
||||
processStatus: {type: string, nullable: true, description: Firstleg 内部状态;入库完成记录进入 WAIT_SPLIT}
|
||||
orderStatusSnapshot: {type: string, nullable: true}
|
||||
measureReviewStatus: {type: string, nullable: true}
|
||||
feeStatus: {type: string, nullable: true}
|
||||
|
||||
@@ -48,9 +48,9 @@ class FbaPricingContractTest {
|
||||
.contains("/api/platform/v1/inbounds/{inboundId}/fees/send:")
|
||||
.contains("/api/platform/v1/inbounds/{inboundId}/fees/published:")
|
||||
.contains("Idempotency-Key")
|
||||
.contains("主流程已到 INBOUNDED,也可独立试算且不回退主流程状态")
|
||||
.contains("主流程已到 WAIT_SPLIT,也可独立试算且不回退主流程状态")
|
||||
.contains("该动作涉及后续仓储流程,本轮二期验收不调用")
|
||||
.contains("不回退 INBOUNDED 状态")
|
||||
.contains("不回退 WAIT_SPLIT 状态")
|
||||
.doesNotContain("应收")
|
||||
.doesNotContain("应付")
|
||||
.doesNotContain("待客户确认");
|
||||
|
||||
+2
-2
@@ -42,7 +42,7 @@ class InboundOrderControllerContractTest {
|
||||
openapi.indexOf("/api/platform/v1/first-leg/tasks:"),
|
||||
openapi.indexOf("/api/platform/v1/first-leg/tasks/{taskId}:"));
|
||||
assertThat(inboundList)
|
||||
.doesNotContain("WAIT_SPLIT");
|
||||
.contains("WAIT_SPLIT");
|
||||
assertThat(openapi)
|
||||
.contains("/api/platform/inbounds:")
|
||||
.contains("/api/platform/inbounds/{inboundId}:")
|
||||
@@ -53,7 +53,7 @@ class InboundOrderControllerContractTest {
|
||||
.contains("required: [id, tenantId]")
|
||||
.contains("pricingTaxMode: {type: string, enum: [TAX_INCLUDED, TAX_EXCLUDED]")
|
||||
.contains("tenantId: {type: string, pattern: '^[1-9][0-9]*$', description:")
|
||||
.contains("[WAIT_MEASURE, WAIT_REVIEW, WAIT_FEE_SEND, INBOUNDED]")
|
||||
.contains("[WAIT_MEASURE, WAIT_REVIEW, WAIT_FEE_SEND, WAIT_SPLIT]")
|
||||
.contains("platform:firstleg:inbound:list")
|
||||
.doesNotContain("WAIT_CUSTOMER_CONFIRM");
|
||||
assertThat(inboundList).contains("#/components/schemas/InboundOrderPageEnvelope");
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ class InternalInboundOrderReferenceControllerTest {
|
||||
@Test
|
||||
void unifiedQueryRequiresInternalTokenAndForwardsBatchRequest() {
|
||||
UnifiedInboundQuery request = new UnifiedInboundQuery(
|
||||
10001L, null, null, "INBOUNDED", null, null, null,
|
||||
10001L, null, null, "WAIT_SPLIT", null, null, null,
|
||||
null, null, null, null, null, null, null, null, null, null, null, null,
|
||||
null, null, 1, 20);
|
||||
when(unifiedQueryService.page(request)).thenReturn(new PageResult<>(List.of(), 1, 20, 0, 0));
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ class MerchantInboundBillingControllerContractTest {
|
||||
String openapi = new ClassPathResource("openapi/firstleg-v1.yaml")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
assertThat(openapi).contains("/api/merchant/v1/first-leg/orders/{orderId}/inbound-billing:")
|
||||
.contains("INBOUNDED + APPROVED + CONFIRMED");
|
||||
.contains("WAIT_SPLIT + APPROVED + CONFIRMED");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+4
-4
@@ -12,7 +12,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class UnifiedInboundOpenApiContractTest {
|
||||
@Test
|
||||
void documentsInternalUnifiedOrderEndpointsAndSevenStatuses() throws Exception {
|
||||
void documentsInternalUnifiedOrderEndpointsAndSixStatuses() throws Exception {
|
||||
String yaml = new ClassPathResource("openapi/firstleg-v1.yaml")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
Map<String, Object> document = stringMap(new Yaml().load(yaml));
|
||||
@@ -25,13 +25,13 @@ class UnifiedInboundOpenApiContractTest {
|
||||
Map<String, Object> schemas = stringMap(stringMap(document.get("components")).get("schemas"));
|
||||
Map<String, Object> responseStatus = stringMap(schemas.get("UnifiedMainStatus"));
|
||||
assertThat(stringList(responseStatus.get("enum"))).containsExactly(
|
||||
"PENDING_REVIEW", "WAIT_MEASURE", "INBOUNDED", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED");
|
||||
assertThat(responseStatus.get("description").toString()).contains("WAIT_SPLIT", "INBOUNDED");
|
||||
"PENDING_REVIEW", "WAIT_MEASURE", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED");
|
||||
assertThat(responseStatus.get("description").toString()).contains("WAIT_SPLIT", "唯一待分柜状态");
|
||||
|
||||
Map<String, Object> query = stringMap(schemas.get("UnifiedInboundQuery"));
|
||||
Map<String, Object> queryStatus = stringMap(stringMap(query.get("properties")).get("mainStatus"));
|
||||
assertThat(stringList(queryStatus.get("enum"))).contains("WAIT_SPLIT");
|
||||
assertThat(queryStatus.get("description").toString()).contains("WAIT_SPLIT", "INBOUNDED", "独立");
|
||||
assertThat(queryStatus.get("description").toString()).contains("WAIT_SPLIT", "唯一待分柜筛选值");
|
||||
|
||||
Map<String, Object> inbound = stringMap(schemas.get("UnifiedInboundOrderSummary"));
|
||||
assertThat(stringMap(inbound.get("properties"))).containsKeys(
|
||||
|
||||
+2
-2
@@ -86,7 +86,7 @@ class LoadingDeliveryServiceTest {
|
||||
@Test
|
||||
void completesLoadedFulfillmentSliceBeforeAggregatingRoot() {
|
||||
when(repository.claimOperation(anyLong(), anyLong(), anyLong(), anyString(), anyString(), anyString())).thenReturn(true);
|
||||
when(repository.findInbound(10, 20)).thenReturn(new Snapshot(20L, 10L, "INBOUNDED", 3));
|
||||
when(repository.findInbound(10, 20)).thenReturn(new Snapshot(20L, 10L, "WAIT_SPLIT", 3));
|
||||
when(repository.completionTarget(10, 20)).thenReturn(new CompletionTarget(
|
||||
20L, null, 20L, "LOADING", 3, "LOADING", 3));
|
||||
LoadingPickItemMapper.InboundProgress progress = mock(LoadingPickItemMapper.InboundProgress.class);
|
||||
@@ -180,7 +180,7 @@ class LoadingDeliveryServiceTest {
|
||||
@Test
|
||||
void keepsHistoricalLoadedChildAsCompletionTarget() {
|
||||
CompletionTarget historical = new CompletionTarget(
|
||||
20L, 21L, 20L, "INBOUNDED", 3, "LOADING", 1);
|
||||
20L, 21L, 20L, "WAIT_SPLIT", 3, "LOADING", 1);
|
||||
|
||||
assertThat(historical.completionInboundId()).isEqualTo(21L);
|
||||
}
|
||||
|
||||
+2
-2
@@ -156,7 +156,7 @@ class FbaPricingRepositoryRuleListTest {
|
||||
when(pricing.update(org.mockito.ArgumentMatchers.isNull(), org.mockito.ArgumentMatchers.any()))
|
||||
.thenReturn(1);
|
||||
|
||||
assertThat(repository.markInboundFeeSent(10001L, 88L, "INBOUNDED")).isEqualTo(1);
|
||||
assertThat(repository.markInboundFeeSent(10001L, 88L, "WAIT_SPLIT")).isEqualTo(1);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
ArgumentCaptor<LambdaUpdateWrapper<InboundPricingEntity>> wrapper =
|
||||
@@ -164,7 +164,7 @@ class FbaPricingRepositoryRuleListTest {
|
||||
verify(pricing).update(org.mockito.ArgumentMatchers.isNull(), wrapper.capture());
|
||||
assertThat(wrapper.getValue().getSqlSet()).contains("feeStatus").doesNotContain("processStatus");
|
||||
assertThat(wrapper.getValue().getCustomSqlSegment()).contains("tenantId", "id", "processStatus", "feeStatus");
|
||||
assertThat(params(wrapper.getValue()).values()).contains(10001L, 88L, "INBOUNDED", "CONFIRMED", "SENT");
|
||||
assertThat(params(wrapper.getValue()).values()).contains(10001L, 88L, "WAIT_SPLIT", "CONFIRMED", "SENT");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+9
-9
@@ -11,23 +11,23 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class InboundStatusUnificationSqlContractTest {
|
||||
@Test
|
||||
void preservesHistoricalWaitSplitWithoutDataRewrite() throws Exception {
|
||||
void enforcesCanonicalWaitSplitStatusWithoutCompatibilityRewrite() throws Exception {
|
||||
String module = normalize(new ClassPathResource("db/phase3-inbounded-status-unification.sql")
|
||||
.getContentAsString(StandardCharsets.UTF_8)).trim();
|
||||
String unified = normalize(Files.readString(unifiedPath(), StandardCharsets.UTF_8));
|
||||
|
||||
assertThat(module)
|
||||
.contains("CREATE PROCEDURE assert_inbounded_compatibility()")
|
||||
.contains("新完成入库记录使用 WAIT_SPLIT")
|
||||
.contains("存量 INBOUNDED")
|
||||
.contains("CREATE PROCEDURE assert_wait_split_status()")
|
||||
.contains("入库完成后的唯一状态为 WAIT_SPLIT")
|
||||
.doesNotContain("兼容")
|
||||
.doesNotContain("UPDATE fl_inbound_order")
|
||||
.doesNotContain("UPDATE fl_inbound_status_history");
|
||||
assertThat(unified)
|
||||
.contains("新完成入库记录使用 WAIT_SPLIT")
|
||||
.contains("存量 INBOUNDED")
|
||||
.doesNotContain("SET process_status = 'INBOUNDED'")
|
||||
.doesNotContain("SET from_status = 'INBOUNDED'")
|
||||
.doesNotContain("SET to_status = 'INBOUNDED'");
|
||||
.contains("入库完成后的唯一状态为 WAIT_SPLIT")
|
||||
.doesNotContain("历史 WAIT_SPLIT")
|
||||
.doesNotContain("SET process_status = 'WAIT_SPLIT'")
|
||||
.doesNotContain("SET from_status = 'WAIT_SPLIT'")
|
||||
.doesNotContain("SET to_status = 'WAIT_SPLIT'");
|
||||
}
|
||||
|
||||
private static Path unifiedPath() {
|
||||
|
||||
+2
-2
@@ -76,7 +76,7 @@ class PdaSplitSettlementSqlContractTest {
|
||||
.getAnnotation(Insert.class).value()[0];
|
||||
assertThat(clone)
|
||||
.contains("#{childOrderId},#{childOrderNo}")
|
||||
.contains("CASE WHEN #{splitPurpose}='LOADED' THEN 'LOADING' ELSE 'INBOUNDED' END");
|
||||
.contains("CASE WHEN #{splitPurpose}='LOADED' THEN 'LOADING' ELSE 'WAIT_SPLIT' END");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -89,7 +89,7 @@ class PdaSplitSettlementSqlContractTest {
|
||||
long.class, long.class, int.class, long.class, int.class)
|
||||
.getAnnotation(Update.class).value()[0];
|
||||
assertThat(markSource)
|
||||
.contains("process_status IN ('INBOUNDED','LOADING')")
|
||||
.contains("process_status IN ('WAIT_SPLIT','LOADING')")
|
||||
.contains("package_count=#{loadedPackageCount}")
|
||||
.contains("split_status='FULLY_SPLIT'");
|
||||
}
|
||||
|
||||
+1
-1
@@ -84,7 +84,7 @@ class Phase3FinalUnifiedSqlContractTest {
|
||||
"-- 来源:iam/src/main/resources/db/phase3-warehouse-roles.sql",
|
||||
"firstleg/src/main/resources/db/phase3-carrier-schedule-query-schema.sql");
|
||||
assertBlock(unified, "-- 来源:iam/src/main/resources/db/phase3-warehouse-roles.sql",
|
||||
"-- 新完成入库记录使用 WAIT_SPLIT;存量 INBOUNDED 继续作为可分柜兼容状态,统一迁移不改写主表或历史状态。",
|
||||
"-- 入库完成后的唯一状态为 WAIT_SPLIT。",
|
||||
"iam/src/main/resources/db/phase3-warehouse-roles.sql");
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -21,7 +21,7 @@ class SmartLoadingCandidateMapperContractTest {
|
||||
.contains("m2.status='APPROVED'")
|
||||
.contains("i.inbound_detail_id IS NOT NULL")
|
||||
.contains("o.warehouse_id=#{warehouseId}")
|
||||
.contains("o.process_status IN ('WAIT_SPLIT','INBOUNDED')")
|
||||
.contains("o.process_status='WAIT_SPLIT'")
|
||||
.contains("SELECT SUM(li.package_count)")
|
||||
.contains("li.deleted=0")
|
||||
.contains("ORDER BY o.inbounded_at ASC, o.id ASC, i.id ASC");
|
||||
@@ -38,7 +38,7 @@ class SmartLoadingCandidateMapperContractTest {
|
||||
.contains("m.status='APPROVED'")
|
||||
.contains("SELECT MAX(m2.id)")
|
||||
.contains("o.warehouse_id=#{warehouseId}")
|
||||
.contains("o.process_status IN ('WAIT_SPLIT','INBOUNDED')")
|
||||
.contains("o.process_status='WAIT_SPLIT'")
|
||||
.contains("SELECT SUM(li.package_count)")
|
||||
.contains("FOR UPDATE");
|
||||
}
|
||||
|
||||
+3
-3
@@ -32,7 +32,7 @@ class AutomaticInboundCompletionServiceTest {
|
||||
order("FEE_CONFIRMING", "CALCULATED", 5),
|
||||
order("FEE_CONFIRMING", "CONFIRMED", 6),
|
||||
order("INBOUNDING", "CONFIRMED", 7),
|
||||
order("INBOUNDED", "SENT", 8));
|
||||
order("WAIT_SPLIT", "SENT", 8));
|
||||
|
||||
FeeSnapshotResponse result = service.completeApproved(10001L, 88L, 9L, "客服", 30L, 4);
|
||||
|
||||
@@ -48,7 +48,7 @@ class AutomaticInboundCompletionServiceTest {
|
||||
@Test
|
||||
void shouldHandleCase02() {
|
||||
FeeSnapshotResponse fee = fee(501L);
|
||||
when(workflow.findOwned(10001L, 88L)).thenReturn(order("INBOUNDED", "SENT", 8));
|
||||
when(workflow.findOwned(10001L, 88L)).thenReturn(order("WAIT_SPLIT", "SENT", 8));
|
||||
when(pricing.latest(10001L, 88L)).thenReturn(fee);
|
||||
|
||||
assertThat(service.completeApproved(10001L, 88L, 9L, "客服", 30L, 4)).isSameAs(fee);
|
||||
@@ -65,7 +65,7 @@ class AutomaticInboundCompletionServiceTest {
|
||||
FeeSnapshotResponse fee = fee(501L);
|
||||
when(pricing.latest(10001L, 88L)).thenReturn(fee);
|
||||
when(workflow.findOwned(10001L, 88L)).thenReturn(
|
||||
order("INBOUNDING", "CONFIRMED", 7), order("INBOUNDED", "SENT", 8));
|
||||
order("INBOUNDING", "CONFIRMED", 7), order("WAIT_SPLIT", "SENT", 8));
|
||||
|
||||
service.completeApproved(10001L, 88L, 9L, "客服", 30L, 4);
|
||||
|
||||
|
||||
@@ -185,7 +185,7 @@ class FbaPricingServiceTest {
|
||||
void returnsOriginalSnapshotForRepeatedCalculation() {
|
||||
InboundPricingIdempotencyEntity completed = completed("FEE_CALCULATE", 501L);
|
||||
InboundPricingEntity inbound = inbound("10", "1");
|
||||
inbound.setProcessStatus("INBOUNDED");
|
||||
inbound.setProcessStatus("WAIT_SPLIT");
|
||||
when(repository.findIdempotency(TENANT, OPERATOR, "FEE_CALCULATE", "same")).thenReturn(completed);
|
||||
when(repository.findFee(TENANT, 501L)).thenReturn(savedFee("CALCULATED"));
|
||||
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
|
||||
@@ -195,7 +195,7 @@ class FbaPricingServiceTest {
|
||||
FeeSnapshotResponse response = service.calculate(TENANT, OPERATOR, INBOUND, NOW, "same");
|
||||
|
||||
assertThat(response.feeId()).isEqualTo(501L);
|
||||
assertThat(response.nextProcessStatus()).isEqualTo("INBOUNDED");
|
||||
assertThat(response.nextProcessStatus()).isEqualTo("WAIT_SPLIT");
|
||||
verify(repository, never()).matchRules(any(), any());
|
||||
verify(repository, never()).insertFee(any());
|
||||
}
|
||||
@@ -236,7 +236,7 @@ class FbaPricingServiceTest {
|
||||
@DisplayName("已入库费用发送只发布费用且不回退主流程状态")
|
||||
void sendsFeeAfterInboundWithoutRollingBackProcessStatus() {
|
||||
InboundPricingEntity inbound = inbound("10", "1");
|
||||
inbound.setProcessStatus("INBOUNDED");
|
||||
inbound.setProcessStatus("WAIT_SPLIT");
|
||||
inbound.setFeeStatus("CONFIRMED");
|
||||
InboundFeeEntity confirmed = savedFee("CONFIRMED");
|
||||
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
|
||||
@@ -245,14 +245,14 @@ class FbaPricingServiceTest {
|
||||
doAnswer(invocation -> { ((InboundPricingIdempotencyEntity) invocation.getArgument(0)).setId(900L); return null; })
|
||||
.when(repository).insertIdempotency(any());
|
||||
when(repository.sendFee(TENANT, 501L, OPERATOR, NOW)).thenReturn(1);
|
||||
when(repository.markInboundFeeSent(TENANT, INBOUND, "INBOUNDED")).thenReturn(1);
|
||||
when(repository.markInboundFeeSent(TENANT, INBOUND, "WAIT_SPLIT")).thenReturn(1);
|
||||
when(repository.completeIdempotency(any(), eq(501L), eq(NOW))).thenReturn(1);
|
||||
when(repository.findFee(TENANT, 501L)).thenReturn(confirmed);
|
||||
|
||||
FeeSnapshotResponse response = service.send(TENANT, OPERATOR, INBOUND, "send-inbounded");
|
||||
|
||||
assertThat(response.nextProcessStatus()).isEqualTo("INBOUNDED");
|
||||
verify(repository).markInboundFeeSent(TENANT, INBOUND, "INBOUNDED");
|
||||
assertThat(response.nextProcessStatus()).isEqualTo("WAIT_SPLIT");
|
||||
verify(repository).markInboundFeeSent(TENANT, INBOUND, "WAIT_SPLIT");
|
||||
verify(financeOutbox).publishInbound(eq(FinanceFeeSourceOutboxService.PUBLISHED),
|
||||
eq(TENANT), eq(INBOUND), eq(501L), eq(1), eq(NOW));
|
||||
}
|
||||
@@ -274,7 +274,7 @@ class FbaPricingServiceTest {
|
||||
@DisplayName("已发布费用可生成新版本且不回退已入库状态")
|
||||
void adjustsPublishedFeeWithoutRollingBackInbound() {
|
||||
InboundPricingEntity inbound = inbound("10", "1");
|
||||
inbound.setProcessStatus("INBOUNDED");
|
||||
inbound.setProcessStatus("WAIT_SPLIT");
|
||||
inbound.setFeeStatus("SENT");
|
||||
InboundFeeEntity current = savedFee("SENT");
|
||||
AtomicReference<InboundFeeEntity> inserted = new AtomicReference<>();
|
||||
@@ -301,8 +301,8 @@ class FbaPricingServiceTest {
|
||||
assertThat(response.feeVersion()).isEqualTo(2);
|
||||
assertThat(response.feeStatus()).isEqualTo("SENT");
|
||||
assertThat(response.totalAmount()).isEqualByComparingTo("120.00");
|
||||
assertThat(response.nextProcessStatus()).isEqualTo("INBOUNDED");
|
||||
assertThat(inbound.getProcessStatus()).isEqualTo("INBOUNDED");
|
||||
assertThat(response.nextProcessStatus()).isEqualTo("WAIT_SPLIT");
|
||||
assertThat(inbound.getProcessStatus()).isEqualTo("WAIT_SPLIT");
|
||||
verify(repository).supersedeSent(TENANT, 501L);
|
||||
verify(financeOutbox).publishInbound(eq(FinanceFeeSourceOutboxService.ADJUSTED),
|
||||
eq(TENANT), eq(INBOUND), eq(502L), eq(2), eq(NOW));
|
||||
|
||||
+2
-2
@@ -506,7 +506,7 @@ class InboundMeasurementServiceTest {
|
||||
|
||||
@Test
|
||||
void editingAndResubmittingInboundMeasurementDoesNotRollBackMainStatus() {
|
||||
InboundOrderEntity inbound = inbound("INBOUNDED");
|
||||
InboundOrderEntity inbound = inbound("WAIT_SPLIT");
|
||||
inbound.setMeasureReviewStatus("APPROVED");
|
||||
InboundMeasurementEntity measurement = new InboundMeasurementEntity();
|
||||
measurement.setId(30L);
|
||||
@@ -524,7 +524,7 @@ class InboundMeasurementServiceTest {
|
||||
InboundMeasurementResponses.MeasurementSubmitted result = service.submit(1L, 10L, 9L,
|
||||
new InboundMeasurementRequests.SubmitRequest(30L, 2));
|
||||
|
||||
assertThat(result.processStatus()).isEqualTo("INBOUNDED");
|
||||
assertThat(result.processStatus()).isEqualTo("WAIT_SPLIT");
|
||||
assertThat(result.measureReviewStatus()).isEqualTo("APPROVED");
|
||||
assertThat(measurement.getStatus()).isEqualTo("APPROVED");
|
||||
verify(repository, never()).updateMeasurement(any());
|
||||
|
||||
+2
-2
@@ -42,7 +42,7 @@ class InboundOrderQueryServiceTest {
|
||||
|
||||
@Test
|
||||
void shouldAcceptInboundedAsThePhase2TerminalStatusFilter() {
|
||||
InboundOrderQuery query = new InboundOrderQuery(null, null, "INBOUNDED", null,
|
||||
InboundOrderQuery query = new InboundOrderQuery(null, null, "WAIT_SPLIT", null,
|
||||
"APPROVED", "CONFIRMED", null, null, null, null, null, null, null,
|
||||
null, null, null, null, 1, 200);
|
||||
PageResult<InboundOrderSummary> expected = new PageResult<>(List.of(), 1, 200, 0, 0);
|
||||
@@ -59,7 +59,7 @@ class InboundOrderQueryServiceTest {
|
||||
assertThat(InboundListTab.WAIT_REVIEW.processStatuses()).containsExactly("MEASURE_PENDING");
|
||||
assertThat(InboundListTab.WAIT_FEE_SEND.processStatuses())
|
||||
.containsExactly("WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING");
|
||||
assertThat(InboundListTab.INBOUNDED.processStatuses()).containsExactly("INBOUNDED");
|
||||
assertThat(InboundListTab.WAIT_SPLIT.processStatuses()).containsExactly("WAIT_SPLIT");
|
||||
assertThat(List.of(InboundListTab.values())).extracting(Enum::name)
|
||||
.doesNotContain("WAIT_CUSTOMER_CONFIRM");
|
||||
}
|
||||
|
||||
+3
-3
@@ -36,13 +36,13 @@ class MerchantInboundBillingServiceTest {
|
||||
|
||||
@Test
|
||||
void shouldHandleCase01() {
|
||||
InboundOrderEntity inbound = inbound("INBOUNDED", "APPROVED", "CONFIRMED");
|
||||
InboundOrderEntity inbound = inbound("WAIT_SPLIT", "APPROVED", "CONFIRMED");
|
||||
InboundOrderDetail detail = new InboundOrderDetail(mock(com.baiyue.firstleg.dto.InboundOrderSummary.class),
|
||||
"客户备注", List.of(), List.of(
|
||||
new InboundOrderDetail.FileSummary(1L, "RELATED", "public.pdf", "PUBLIC", 1, 1L, null),
|
||||
new InboundOrderDetail.FileSummary(2L, "INTERNAL", "internal.pdf", "INTERNAL", 1, 1L, null)),
|
||||
List.of(new InboundOrderDetail.InternalNoteSummary(1L, "内部备注", 1L, "客服", null)),
|
||||
List.of(new InboundOrderDetail.StatusHistorySummary(null, "INBOUNDED", "COMPLETE",
|
||||
List.of(new InboundOrderDetail.StatusHistorySummary(null, "WAIT_SPLIT", "COMPLETE",
|
||||
"内部原因", 1L, "客服", null)));
|
||||
InboundMeasurementResponses.Detail measurement = mock(InboundMeasurementResponses.Detail.class);
|
||||
FeeSnapshotResponse fee = mock(FeeSnapshotResponse.class);
|
||||
@@ -142,7 +142,7 @@ class MerchantInboundBillingServiceTest {
|
||||
|
||||
@Test
|
||||
void shouldRejectHistoricalPublishedStatusCombination() {
|
||||
InboundOrderEntity inbound = inbound("INBOUNDED", "APPROVED", "SENT");
|
||||
InboundOrderEntity inbound = inbound("WAIT_SPLIT", "APPROVED", "SENT");
|
||||
when(orders.findByOrderId(10001L, 77L)).thenReturn(inbound);
|
||||
|
||||
assertThatThrownBy(() -> service.find(10001L, 77L))
|
||||
|
||||
+3
-3
@@ -180,7 +180,7 @@ class PdaPickSettlementServiceTest {
|
||||
void usesTotalPackageCountWhenUnsplitMeasuredInboundHasZeroAvailableColumn() {
|
||||
LoadingPickItemEntity item = item(11L, 21L, 31L, 41L, 20, 20, "PICKED");
|
||||
resolved(List.of(item));
|
||||
when(splits.lockParent(31L)).thenReturn(parent(20, 0, "INBOUNDED", "NOT_SPLIT"));
|
||||
when(splits.lockParent(31L)).thenReturn(parent(20, 0, "WAIT_SPLIT", "NOT_SPLIT"));
|
||||
when(splits.lockLatestApprovedItemsForInbound(10001L, 31L))
|
||||
.thenReturn(List.of(new MeasurementItemRow(51L, 61L, 41L, 20)));
|
||||
when(allocations.lockConfirmed(10001L, 31L))
|
||||
@@ -336,7 +336,7 @@ class PdaPickSettlementServiceTest {
|
||||
"IN-001", "MASTER", 31L, "LOADING", 5, 5, "FULLY_SPLIT", 4));
|
||||
when(splits.lockParent(inboundId)).thenReturn(new ParentRow(inboundId, 10001L, 902L,
|
||||
"ORDER-REMAINDER", "IN-001-" + (sourceVersion - 1), "CHILD", 31L,
|
||||
"INBOUNDED", packageCount, packageCount, "NOT_SPLIT", sourceVersion));
|
||||
"WAIT_SPLIT", packageCount, packageCount, "NOT_SPLIT", sourceVersion));
|
||||
when(splits.lockLatestApprovedItemsForInbound(10001L, inboundId))
|
||||
.thenReturn(List.of(new MeasurementItemRow(measurementItemId, measurementId, detailId, packageCount)));
|
||||
when(allocations.lockConfirmed(10001L, inboundId)).thenAnswer(invocation ->
|
||||
@@ -408,7 +408,7 @@ class PdaPickSettlementServiceTest {
|
||||
}
|
||||
|
||||
private static ParentRow parent(int packageCount) {
|
||||
return parent(packageCount, packageCount, "INBOUNDED", "NOT_SPLIT");
|
||||
return parent(packageCount, packageCount, "WAIT_SPLIT", "NOT_SPLIT");
|
||||
}
|
||||
|
||||
private static ParentRow parent(int totalPackageCount, int packageCount, String processStatus, String splitStatus) {
|
||||
|
||||
+7
-12
@@ -32,7 +32,6 @@ class UnifiedInboundQueryServiceTest {
|
||||
assertThat(UnifiedInboundQueryService.mainStatus("FEE_CONFIRMING")).isEqualTo("WAIT_MEASURE");
|
||||
assertThat(UnifiedInboundQueryService.mainStatus("INBOUNDING")).isEqualTo("WAIT_MEASURE");
|
||||
assertThat(UnifiedInboundQueryService.mainStatus("WAIT_SPLIT")).isEqualTo("WAIT_SPLIT");
|
||||
assertThat(UnifiedInboundQueryService.mainStatus("INBOUNDED")).isEqualTo("INBOUNDED");
|
||||
assertThat(UnifiedInboundQueryService.mainStatus("LOADING")).isEqualTo("LOADING");
|
||||
assertThat(UnifiedInboundQueryService.mainStatus("LOADED")).isEqualTo("LOADED");
|
||||
assertThat(UnifiedInboundQueryService.mainStatus("DELIVERED")).isEqualTo("DELIVERED");
|
||||
@@ -44,25 +43,21 @@ class UnifiedInboundQueryServiceTest {
|
||||
assertThat(UnifiedInboundQueryService.nextAction("WAIT_MEASURE", "MEASURING", null)).isNull();
|
||||
assertThat(UnifiedInboundQueryService.nextAction("WAIT_MEASURE", "MEASURE_PENDING", null))
|
||||
.isEqualTo("CONFIRM_MEASUREMENT");
|
||||
assertThat(UnifiedInboundQueryService.nextAction("INBOUNDED", "INBOUNDED", null))
|
||||
.isEqualTo("SMART_SPLIT");
|
||||
assertThat(UnifiedInboundQueryService.nextAction("INBOUNDED", "INBOUNDED", "PENDING"))
|
||||
.isEqualTo("CONFIRM_PRELOAD");
|
||||
assertThat(UnifiedInboundQueryService.nextAction("INBOUNDED", "INBOUNDING", null)).isNull();
|
||||
assertThat(UnifiedInboundQueryService.nextAction("INBOUNDED", "INBOUNDED", "CONFIRMED")).isNull();
|
||||
assertThat(UnifiedInboundQueryService.nextAction("WAIT_SPLIT", "WAIT_SPLIT", null))
|
||||
.isEqualTo("SMART_SPLIT");
|
||||
assertThat(UnifiedInboundQueryService.nextAction("WAIT_SPLIT", "WAIT_SPLIT", "PENDING"))
|
||||
.isEqualTo("CONFIRM_PRELOAD");
|
||||
assertThat(UnifiedInboundQueryService.nextAction("WAIT_SPLIT", "INBOUNDING", null)).isNull();
|
||||
assertThat(UnifiedInboundQueryService.nextAction("WAIT_SPLIT", "WAIT_SPLIT", "CONFIRMED")).isNull();
|
||||
assertThat(UnifiedInboundQueryService.nextAction("LOADED", "LOADED", null)).isNull();
|
||||
assertThat(UnifiedInboundQueryService.nextAction("DELIVERED", "DELIVERED", null)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void inboundedAndWaitSplitFiltersRemainDistinct() throws Exception {
|
||||
void waitSplitFilterUsesOnlyCanonicalStatus() throws Exception {
|
||||
var method = UnifiedInboundQueryService.class.getDeclaredMethod("internalStatuses", String.class);
|
||||
method.setAccessible(true);
|
||||
|
||||
assertThat(method.invoke(new UnifiedInboundQueryService(), "INBOUNDED"))
|
||||
.isEqualTo(java.util.List.of("INBOUNDED"));
|
||||
assertThat(method.invoke(new UnifiedInboundQueryService(), "WAIT_SPLIT"))
|
||||
.isEqualTo(java.util.List.of("WAIT_SPLIT"));
|
||||
}
|
||||
@@ -70,14 +65,14 @@ class UnifiedInboundQueryServiceTest {
|
||||
@Test
|
||||
void phaseTwoMeasurementStatusesDoNotRequireUndeployedPhaseThreeTables() {
|
||||
assertThat(UnifiedInboundQueryService.requiresPhaseThreeSummary("WAIT_MEASURE")).isFalse();
|
||||
assertThat(UnifiedInboundQueryService.requiresPhaseThreeSummary("INBOUNDED")).isFalse();
|
||||
assertThat(UnifiedInboundQueryService.requiresPhaseThreeSummary("WAIT_SPLIT")).isFalse();
|
||||
assertThat(UnifiedInboundQueryService.requiresPhaseThreeSummary("LOADING")).isTrue();
|
||||
assertThat(UnifiedInboundQueryService.requiresPhaseThreeSummary("LOADED")).isTrue();
|
||||
assertThat(UnifiedInboundQueryService.requiresPhaseThreeSummary("DELIVERED")).isTrue();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"WAIT_MEASURE", "INBOUNDED", "WAIT_SPLIT"})
|
||||
@ValueSource(strings = {"WAIT_MEASURE", "WAIT_SPLIT"})
|
||||
void phaseTwoDetailDoesNotQueryPhaseThreeTables(String processStatus) {
|
||||
InboundOrderRepository inbounds = mock(InboundOrderRepository.class);
|
||||
UnifiedInboundReadRepository operations = mock(UnifiedInboundReadRepository.class);
|
||||
|
||||
@@ -31,7 +31,20 @@ ON DUPLICATE KEY UPDATE
|
||||
UPDATE iam_permission
|
||||
SET status='INACTIVE', deleted=1
|
||||
WHERE tenant_id=0 AND app_code='PLATFORM'
|
||||
AND permission_code IN ('platform:overseas-storage-fee:create','platform:overseas-storage-fee:delete');
|
||||
AND permission_code IN (
|
||||
'platform:overseas-storage-fee:create','platform:overseas-storage-fee:delete',
|
||||
'platform:finance:bill:view','platform:finance:account:view',
|
||||
'platform:finance:fee-source:view','platform:finance:settlement-ar:view',
|
||||
'platform:finance:settlement-ap:view');
|
||||
|
||||
DELETE rp
|
||||
FROM iam_role_permission rp
|
||||
JOIN iam_permission p ON p.id=rp.permission_id AND p.tenant_id=rp.tenant_id
|
||||
WHERE p.tenant_id=0 AND p.app_code='PLATFORM'
|
||||
AND p.permission_code IN (
|
||||
'platform:finance:bill:view','platform:finance:account:view',
|
||||
'platform:finance:fee-source:view','platform:finance:settlement-ar:view',
|
||||
'platform:finance:settlement-ap:view');
|
||||
|
||||
INSERT IGNORE INTO iam_role_permission (tenant_id, role_id, permission_id, created_at)
|
||||
SELECT 0, r.id, p.id, CURRENT_TIMESTAMP(3)
|
||||
@@ -60,9 +73,7 @@ UPDATE iam_permission p
|
||||
WHERE p.tenant_id=0 AND p.app_code='PLATFORM';
|
||||
|
||||
UPDATE iam_permission p
|
||||
JOIN (SELECT 'platform:finance:account:view' code, 5105040 parent_id UNION ALL
|
||||
SELECT 'platform:finance:fee-source:view', 5105041 UNION ALL
|
||||
SELECT 'platform:finance:fee-source:review', 5105041 UNION ALL
|
||||
JOIN (SELECT 'platform:finance:fee-source:review' code, 5105041 parent_id UNION ALL
|
||||
SELECT 'platform:finance:fee-source:settlement', 5105041 UNION ALL
|
||||
SELECT 'platform:finance:reconciliation:create', 5105042 UNION ALL
|
||||
SELECT 'platform:finance:reconciliation:review', 5105042 UNION ALL
|
||||
@@ -70,9 +81,7 @@ UPDATE iam_permission p
|
||||
SELECT 'platform:finance:reconciliation:adjust', 5105042 UNION ALL
|
||||
SELECT 'platform:finance:reconciliation:void', 5105042 UNION ALL
|
||||
SELECT 'platform:finance:settlement-ar:create', 5105043 UNION ALL
|
||||
SELECT 'platform:finance:settlement-ar:view', 5105043 UNION ALL
|
||||
SELECT 'platform:finance:settlement-ap:create', 5105044 UNION ALL
|
||||
SELECT 'platform:finance:settlement-ap:view', 5105044 UNION ALL
|
||||
SELECT 'platform:finance:payment:create', 5105044) mapping
|
||||
ON mapping.code=p.permission_code
|
||||
SET p.parent_id=mapping.parent_id
|
||||
|
||||
@@ -11,16 +11,12 @@ BEGIN
|
||||
SELECT 1
|
||||
FROM iam_permission
|
||||
WHERE (id = 5105000 AND permission_code <> 'platform:finance')
|
||||
OR (id = 5105022 AND permission_code <> 'platform:finance:account:view')
|
||||
OR (id = 5105023 AND permission_code <> 'platform:finance:fee-source:view')
|
||||
OR (id = 5105024 AND permission_code <> 'platform:finance:fee-source:review')
|
||||
OR (id = 5105025 AND permission_code <> 'platform:finance:fee-source:settlement')
|
||||
OR (permission_code = 'platform:finance' AND id <> 5105000)
|
||||
OR (permission_code = 'platform:finance:account:view' AND id <> 5105022)
|
||||
OR (permission_code = 'platform:finance:fee-source:view' AND id <> 5105023)
|
||||
OR (permission_code = 'platform:finance:fee-source:review' AND id <> 5105024)
|
||||
OR (permission_code = 'platform:finance:fee-source:settlement' AND id <> 5105025)
|
||||
OR (id IN (5105000, 5105022, 5105023, 5105024, 5105025)
|
||||
OR (id IN (5105000, 5105024, 5105025)
|
||||
AND (tenant_id <> 0 OR app_code <> 'PLATFORM'))
|
||||
) THEN
|
||||
SIGNAL SQLSTATE '45000'
|
||||
@@ -33,10 +29,6 @@ BEGIN
|
||||
VALUES
|
||||
(5105000, 0, 'PLATFORM', 'MENU', 0, 'platform:finance',
|
||||
'财务管理', '/finance', NULL, 60, 'ACTIVE', 1, 0),
|
||||
(5105022, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:account:view',
|
||||
'查看客户财务账户及流水', NULL, 'GET', 130, 'ACTIVE', 1, 0),
|
||||
(5105023, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:fee-source:view',
|
||||
'查看财务费用来源', NULL, 'GET', 140, 'ACTIVE', 1, 0),
|
||||
(5105024, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:fee-source:review',
|
||||
'审核财务费用来源', NULL, 'POST', 150, 'ACTIVE', 1, 0),
|
||||
(5105025, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:fee-source:settlement',
|
||||
@@ -47,6 +39,11 @@ BEGIN
|
||||
http_method = VALUES(http_method), sort_no = VALUES(sort_no),
|
||||
status = 'ACTIVE', built_in = 1, deleted = 0;
|
||||
|
||||
DELETE rp FROM iam_role_permission rp JOIN iam_permission p ON p.id=rp.permission_id AND p.tenant_id=rp.tenant_id
|
||||
WHERE p.tenant_id=0 AND p.app_code='PLATFORM' AND p.permission_code IN ('platform:finance:account:view','platform:finance:fee-source:view');
|
||||
UPDATE iam_permission SET status='INACTIVE', deleted=1
|
||||
WHERE tenant_id=0 AND app_code='PLATFORM' AND permission_code IN ('platform:finance:account:view','platform:finance:fee-source:view');
|
||||
|
||||
IF (SELECT COUNT(*)
|
||||
FROM iam_permission
|
||||
WHERE tenant_id = 0
|
||||
@@ -57,10 +54,8 @@ BEGIN
|
||||
AND built_in = 1
|
||||
AND deleted = 0
|
||||
AND permission_code IN (
|
||||
'platform:finance:account:view',
|
||||
'platform:finance:fee-source:view',
|
||||
'platform:finance:fee-source:review',
|
||||
'platform:finance:fee-source:settlement')) <> 4 THEN
|
||||
'platform:finance:fee-source:settlement')) <> 2 THEN
|
||||
SIGNAL SQLSTATE '45000'
|
||||
SET MESSAGE_TEXT = 'finance G1/G2 permission catalog is incomplete';
|
||||
END IF;
|
||||
|
||||
@@ -17,8 +17,6 @@ IF EXISTS (
|
||||
OR (id = 5105030 AND permission_code <> 'platform:finance:reconciliation:void')
|
||||
OR (id = 5105031 AND permission_code <> 'platform:finance:settlement-ar:create')
|
||||
OR (id = 5105032 AND permission_code <> 'platform:finance:settlement-ap:create')
|
||||
OR (id = 5105033 AND permission_code <> 'platform:finance:settlement-ar:view')
|
||||
OR (id = 5105034 AND permission_code <> 'platform:finance:settlement-ap:view')
|
||||
OR (id = 5105035 AND permission_code <> 'platform:finance:payment:create')
|
||||
OR (permission_code = 'platform:finance:reconciliation:create' AND id <> 5105026)
|
||||
OR (permission_code = 'platform:finance:reconciliation:review' AND id <> 5105027)
|
||||
@@ -27,8 +25,6 @@ IF EXISTS (
|
||||
OR (permission_code = 'platform:finance:reconciliation:void' AND id <> 5105030)
|
||||
OR (permission_code = 'platform:finance:settlement-ar:create' AND id <> 5105031)
|
||||
OR (permission_code = 'platform:finance:settlement-ap:create' AND id <> 5105032)
|
||||
OR (permission_code = 'platform:finance:settlement-ar:view' AND id <> 5105033)
|
||||
OR (permission_code = 'platform:finance:settlement-ap:view' AND id <> 5105034)
|
||||
OR (permission_code = 'platform:finance:payment:create' AND id <> 5105035)
|
||||
OR (id BETWEEN 5105026 AND 5105035 AND (tenant_id <> 0 OR app_code <> 'PLATFORM'))
|
||||
) THEN
|
||||
@@ -46,8 +42,6 @@ VALUES
|
||||
(5105030, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:reconciliation:void', '作废或重开客户对账单', NULL, 'POST', 210, 'ACTIVE', 1, 0),
|
||||
(5105031, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:settlement-ar:create', '生成应收结算单', NULL, 'POST', 220, 'ACTIVE', 1, 0),
|
||||
(5105032, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:settlement-ap:create', '生成应付结算单', NULL, 'POST', 230, 'ACTIVE', 1, 0),
|
||||
(5105033, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:settlement-ar:view', '查看应收结算单', NULL, 'GET', 240, 'ACTIVE', 1, 0),
|
||||
(5105034, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:settlement-ap:view', '查看应付结算单', NULL, 'GET', 250, 'ACTIVE', 1, 0),
|
||||
(5105035, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:payment:create', '登记应付付款', NULL, 'POST', 260, 'ACTIVE', 1, 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
permission_type = VALUES(permission_type), parent_id = VALUES(parent_id),
|
||||
@@ -55,13 +49,24 @@ ON DUPLICATE KEY UPDATE
|
||||
http_method = VALUES(http_method), sort_no = VALUES(sort_no),
|
||||
status = 'ACTIVE', built_in = 1, deleted = 0;
|
||||
|
||||
DELETE rp
|
||||
FROM iam_role_permission rp
|
||||
JOIN iam_permission p ON p.id=rp.permission_id AND p.tenant_id=rp.tenant_id
|
||||
WHERE p.tenant_id=0 AND p.app_code='PLATFORM'
|
||||
AND p.permission_code IN ('platform:finance:settlement-ar:view','platform:finance:settlement-ap:view');
|
||||
|
||||
UPDATE iam_permission
|
||||
SET status='INACTIVE', deleted=1
|
||||
WHERE tenant_id=0 AND app_code='PLATFORM'
|
||||
AND permission_code IN ('platform:finance:settlement-ar:view','platform:finance:settlement-ap:view');
|
||||
|
||||
IF (SELECT COUNT(*) FROM iam_permission
|
||||
WHERE tenant_id=0 AND app_code='PLATFORM' AND parent_id=5105000
|
||||
AND permission_code IN ('platform:finance:reconciliation:create','platform:finance:reconciliation:review',
|
||||
'platform:finance:reconciliation:issue','platform:finance:reconciliation:adjust','platform:finance:reconciliation:void',
|
||||
'platform:finance:settlement-ar:create','platform:finance:settlement-ap:create',
|
||||
'platform:finance:settlement-ar:view','platform:finance:settlement-ap:view','platform:finance:payment:create')
|
||||
AND status='ACTIVE' AND deleted=0) <> 10 THEN
|
||||
'platform:finance:payment:create')
|
||||
AND status='ACTIVE' AND deleted=0) <> 8 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='finance G3/G4 permission catalog is incomplete';
|
||||
END IF;
|
||||
|
||||
|
||||
+4
@@ -31,6 +31,10 @@ class AdminPrototypeMenuPermissionSqlContractTest {
|
||||
.contains("'platform:finance:settlement-ar:create', 5105043")
|
||||
.contains("'platform:finance:settlement-ap:create', 5105044")
|
||||
.contains("'platform:finance:payment:create', 5105044)")
|
||||
.contains("'platform:finance:bill:view','platform:finance:settlement-ar:view'")
|
||||
.contains("'platform:finance:settlement-ap:view'")
|
||||
.doesNotContain("SELECT 'platform:finance:settlement-ar:view', 5105043")
|
||||
.doesNotContain("SELECT 'platform:finance:settlement-ap:view', 5105044")
|
||||
.contains("role_code='PLATFORM_ADMIN'");
|
||||
}
|
||||
|
||||
|
||||
+5
-4
@@ -62,14 +62,15 @@ class FinanceG1G2PermissionSqlContractTest {
|
||||
.contains("NULL, 'POST', 150, 'ACTIVE', 1, 0")
|
||||
.contains("NULL, 'PUT', 160, 'ACTIVE', 1, 0")
|
||||
.contains("permission_code IN (\n 'platform:finance',")
|
||||
.contains("permission_code LIKE 'platform:finance:%' AND status='ACTIVE' AND deleted=0) <> 27")
|
||||
.contains("permission_code LIKE 'platform:finance:%' AND status='ACTIVE' AND deleted=0) <> 24")
|
||||
.contains("平台管理员 Finance 授权不完整")
|
||||
.contains("Finance G1-G4");
|
||||
assertThat(financeSection)
|
||||
.contains("SELECT 4000001, id, 0 FROM baiyue_iam.iam_permission")
|
||||
.contains("platform:finance:account:view")
|
||||
.contains("platform:finance:settlement-ar:view")
|
||||
.contains("platform:finance:settlement-ap:view");
|
||||
.contains("platform:finance:account:view");
|
||||
assertThat(sql)
|
||||
.contains("platform:finance:settlement-ar:list")
|
||||
.contains("platform:finance:settlement-ap:list");
|
||||
}
|
||||
|
||||
private static Path resolveUnifiedSql() {
|
||||
|
||||
+11
-10
@@ -14,8 +14,7 @@ class FinanceG3G4PermissionSqlContractTest {
|
||||
"platform:finance:reconciliation:create", "platform:finance:reconciliation:review",
|
||||
"platform:finance:reconciliation:issue", "platform:finance:reconciliation:adjust",
|
||||
"platform:finance:reconciliation:void", "platform:finance:settlement-ar:create",
|
||||
"platform:finance:settlement-ap:create", "platform:finance:settlement-ar:view",
|
||||
"platform:finance:settlement-ap:view", "platform:finance:payment:create"
|
||||
"platform:finance:settlement-ap:create", "platform:finance:payment:create"
|
||||
};
|
||||
|
||||
@Test
|
||||
@@ -23,9 +22,7 @@ class FinanceG3G4PermissionSqlContractTest {
|
||||
String sql = new ClassPathResource("db/finance-g3-g4-permissions.sql")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
|
||||
for (int index = 0; index < CODES.length; index++) {
|
||||
assertThat(sql).contains("(" + (5105026 + index) + ", 0, 'PLATFORM', 'BUTTON', 5105000, '" + CODES[index] + "'");
|
||||
}
|
||||
for (String code : CODES) assertThat(sql).contains(code);
|
||||
assertThat(sql).contains("SIGNAL SQLSTATE '45000'")
|
||||
.contains("id = 5105026 AND permission_code <> 'platform:finance:reconciliation:create'")
|
||||
.contains("permission_code = 'platform:finance:payment:create' AND id <> 5105035")
|
||||
@@ -33,7 +30,10 @@ class FinanceG3G4PermissionSqlContractTest {
|
||||
.contains("finance G3/G4 permission id/code conflict")
|
||||
.contains("permission catalog is incomplete")
|
||||
.contains("finance permission parent is invalid")
|
||||
.contains("CALL migrate_finance_g3_g4_permissions()");
|
||||
.contains("CALL migrate_finance_g3_g4_permissions()")
|
||||
.contains("status='INACTIVE', deleted=1")
|
||||
.doesNotContain("'platform:finance:settlement-ar:view', '查看应收结算单'")
|
||||
.doesNotContain("'platform:finance:settlement-ap:view', '查看应付结算单'");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -45,12 +45,13 @@ class FinanceG3G4PermissionSqlContractTest {
|
||||
assertThat(financeSection)
|
||||
.contains("migrate_finance_g1_g4_permissions")
|
||||
.contains("finance G1-G4 permission id/code conflict")
|
||||
.contains("permission_code LIKE 'platform:finance:%' AND status='ACTIVE' AND deleted=0) <> 27");
|
||||
.contains("permission_code LIKE 'platform:finance:%' AND status='ACTIVE' AND deleted=0) <> 24");
|
||||
assertThat(financeSection)
|
||||
.contains("SELECT 4000001, id, 0 FROM baiyue_iam.iam_permission")
|
||||
.contains("platform:finance:reconciliation:create")
|
||||
.contains("platform:finance:settlement-ar:view")
|
||||
.contains("platform:finance:settlement-ap:view");
|
||||
.contains("platform:finance:reconciliation:create");
|
||||
assertThat(sql)
|
||||
.contains("platform:finance:settlement-ar:list")
|
||||
.contains("platform:finance:settlement-ap:list");
|
||||
}
|
||||
|
||||
private static Path resolveUnifiedSql() {
|
||||
|
||||
+29
-4
@@ -1,4 +1,29 @@
|
||||
# 后端全量回归
|
||||
# 全量 HTTP 验收脚本索引
|
||||
|
||||
全量验收只认测试环境 HTTP 脚本。它们通过 Gateway 调用已部署服务,按业务模块执行真实登录、权限、状态、幂等、跨服务和数据链路检查;脚本通过后,剩余工作是 Admin/Client 的真实接口调用与浏览器样式验收,以及 PDA 页面/设备验收。
|
||||
|
||||
| 验收目标 | 唯一入口 | 数据副作用 | 覆盖范围 |
|
||||
|---|---|---|---|
|
||||
| 平台后台全模块 HTTP 验收 | `scripts/admin-test-env-acceptance.mjs` | 会创建、更新或最终停用测试数据 | `--modules` 默认列表中的平台客户、仓储、订单、入库、分柜、装柜、船期、拖车、报关、财务、通知、统计等模块 |
|
||||
| 客户工作台完整 HTTP 链路验收 | `scripts/client-test-env-acceptance.mjs` | 会创建测试企业、订单、入库和权限数据 | 注册/审核、客户隔离、订单、入库计费、文件、财务和客户侧权限链路 |
|
||||
| Client 二期兼容 HTTP 验收 | `scripts/client-phase2-test-env-acceptance.mjs` | 会写入测试环境业务数据 | 二期客户入库、测量、费用和权限兼容场景;不是替代 Client 全量入口 |
|
||||
|
||||
执行前必须确认目标环境、账号和数据清理/停用策略,并获得当前对话的测试环境验收授权。`admin-test-env-modules/*contract.test.mjs` 只验证模块脚本结构,不能代替 HTTP 验收。
|
||||
|
||||
推荐顺序:部署服务 → Admin 全模块 HTTP 验收 → Client 完整 HTTP 链路验收 → 前端真实页面调用和样式验收 → PDA 页面/设备验收。任何本地单测通过都不能替代 HTTP 全量验收。
|
||||
|
||||
## 本地质量检查(非全量验收)
|
||||
|
||||
以下命令只用于代码质量和契约回归,不访问测试环境,不代表服务全量可用:
|
||||
|
||||
| 检查 | 入口 |
|
||||
|---|---|
|
||||
| 后端 Maven 契约/单元测试 | `scripts/backend-full-regression.ps1` |
|
||||
| Admin/Client 本地测试与类型检查 | `scripts/frontend-full-regression.ps1` |
|
||||
| PDA 本地页面和状态测试 | `../apps/pda-uniapp/tests/*.test.mjs` |
|
||||
| 本地数据库 SQL 静态/受控验收 | `scripts/local-db-full-acceptance.ps1` |
|
||||
|
||||
## 后端全量回归
|
||||
|
||||
## 入口
|
||||
|
||||
@@ -21,7 +46,7 @@
|
||||
|
||||
脚本会检查 `pnpm` 和目标应用的 `package.json`;任一命令失败时立即透传退出码,不能同时跳过 Admin 和 Client。
|
||||
|
||||
脚本固定运行:
|
||||
后端脚本固定运行:
|
||||
|
||||
```text
|
||||
mvn -f <仓库根>/pom.xml -pl order,firstleg,iam,gateway,file,warehouse -am test
|
||||
@@ -65,8 +90,8 @@ Dry-run 只检查根 `pom.xml` 和关键回归测试文件并打印 Maven 命令
|
||||
正式入口是 `local-db-full-acceptance.ps1`,只允许连接本机 `127.0.0.1` 或 `localhost`,默认端口 `3306`;测试库 `8.163.30.128` 会立即以退出码 2 拒绝。脚本固定校验统一 SQL SHA-256 `BE7C5C98712D5202F5D187EA9A37C64EAB9F6AA9AE772C89A51F186507B1AE53` 和 14706 行,并静态确认中文注释升级采用校验入队、每表一次 ALTER、结构签名复核、九库覆盖断言及唯一最终 `PASS`。
|
||||
|
||||
```powershell
|
||||
.\local-db-full-acceptance.ps1 -DryRun
|
||||
.\local-db-full-acceptance.ps1
|
||||
.\scripts\local-db-full-acceptance.ps1 -DryRun
|
||||
.\scripts\local-db-full-acceptance.ps1
|
||||
```
|
||||
|
||||
Dry-run 只校验 SQL 输入并打印空库、复跑、两类 picking 旧结构、WAIT_SPLIT/IAM、海外仓旧结构、中断恢复和最终结构的执行顺序,不启动 MySQL、不写库。真实运行每阶段写入 `.codex-tmp/p23-04-acceptance/acceptance-run-*.log`,不使用 `--force`,任一 SQL 错误立即停止并透传退出码,最后执行 `verify-final.sql`。脚本不保存或传入数据库密码,使用本机默认认证环境。
|
||||
|
||||
@@ -510,7 +510,10 @@ export class AcceptanceRun {
|
||||
|
||||
async preparePhase3ContainerFixture() {
|
||||
const originalMarker = this.marker
|
||||
const fixtureMarker = `${originalMarker}-CAB`
|
||||
// Keep the shared orchestration marker stable. Downstream loading,
|
||||
// overview and finance modules select fixtures by context.marker; using a
|
||||
// private -CAB suffix here made the producer invisible to those modules.
|
||||
const fixtureMarker = originalMarker
|
||||
const createdKeys = ['orders', 'ocrInbound', 'testInbound', 'inboundMeasurement']
|
||||
const contextKeys = ['inboundWeightOcr', 'ocrInbound', 'testInbound', 'createdInbound']
|
||||
const createdSnapshot = new Map(createdKeys.map((key) => [key, {
|
||||
@@ -525,7 +528,10 @@ export class AcceptanceRun {
|
||||
await this.executeModule('orders-phase3-container-fixture', () => orders.run(this))
|
||||
const measurement = await import(new URL('./admin-test-env-modules/inbound-measurement.mjs', import.meta.url))
|
||||
await this.executeModule('inbound-measurement-phase3-container-fixture', () => measurement.run(this))
|
||||
const fixture = this.createdResources.inboundMeasurement || this.createdResources.orders
|
||||
const fixture = this.createdResources.inboundMeasurement
|
||||
if (!fixture?.inboundId || fixture.finalStatus !== 'APPROVED' || fixture.processStatus !== 'WAIT_SPLIT') {
|
||||
throw new Error(`独立柜次测量 fixture 未进入 WAIT_SPLIT,禁止回退使用 WAIT_PRICING 订单: processStatus=${fixture?.processStatus || 'missing'}, measurementStatus=${fixture?.finalStatus || 'missing'}`)
|
||||
}
|
||||
this.createdResources.orchestrationFixtures = {
|
||||
...(this.createdResources.orchestrationFixtures || {}),
|
||||
phase3Container: fixture ? {
|
||||
|
||||
@@ -403,6 +403,14 @@ export async function run(context) {
|
||||
'使用订单域真实 waybillId、shipmentId 或 orderNo,不使用数据库主键冒充运单标识', OPTIONS)
|
||||
context.createdResources.financeFullFixtures = {
|
||||
marker: context.marker, customerId,
|
||||
prerequisites: {
|
||||
container: containerOwned && Boolean(containerFixture?.feeSourceId) && allocations.length > 0
|
||||
? { ready: true, category: null }
|
||||
: { ready: false, category: 'container-prerequisite', evidence: '缺少同 marker 已完成柜次、有效 containerBookingId、按柜费用来源或真实分摊' },
|
||||
financeSources: fixtures.length > 0
|
||||
? { ready: true, category: null }
|
||||
: { ready: false, category: 'finance-source-prerequisite', evidence: '本轮 marker 未产生可消费 Finance 费用来源' },
|
||||
},
|
||||
orders: measured.map((item) => ({ orderId: id(item.orderId), orderNo: item.orderNo || null,
|
||||
shipmentId: item.shipmentId ? id(item.shipmentId) : null,
|
||||
waybillId: item.waybillId ? id(item.waybillId) : (item.shipmentId ? id(item.shipmentId) : null), inboundId: id(item.inboundId),
|
||||
|
||||
@@ -323,20 +323,25 @@ export async function run(context) {
|
||||
finish(context, start)
|
||||
return
|
||||
}
|
||||
fixture.root.reconciliationPrerequisite = null
|
||||
const permissionResponse = await context.request('GET', '/api/platform/v1/iam/authorization/permissions')
|
||||
const permissions = context.apiSuccess(MODULE, '查询当前账号 Finance 有效权限', permissionResponse, '', OPTIONS)
|
||||
const granted = new Set(list(permissions).map(String))
|
||||
if (granted.has(LEGACY_STATEMENT_VIEW_PERMISSION)) {
|
||||
fixture.root.reconciliationPrerequisite = { ready: false, category: 'permission-prerequisite',
|
||||
required: [STATEMENT_LIST_PERMISSION], legacy: [LEGACY_STATEMENT_VIEW_PERMISSION] }
|
||||
blocker(context, '客户对账单旧 view 权限兼容阻断',
|
||||
`检测到旧权限=${LEGACY_STATEMENT_VIEW_PERMISSION};列表和详情只能以正式权限=${STATEMENT_LIST_PERMISSION} 验收,旧 view 不得作为通过依据`,
|
||||
`检测到旧权限=${LEGACY_STATEMENT_VIEW_PERMISSION};列表和详情只能以正式权限=${STATEMENT_LIST_PERMISSION} 验收,旧 view 不得作为通过依据;需由 IAM 角色初始化为验收账号授予 ${STATEMENT_LIST_PERMISSION} 并移除旧资源,Finance 脚本不兼容旧权限`,
|
||||
'legacy-customer-statement-view-permission', permissionResponse)
|
||||
finish(context, start)
|
||||
return
|
||||
}
|
||||
const missingPermissions = REQUIRED_PERMISSIONS.filter((permission) => !granted.has(permission))
|
||||
if (missingPermissions.length > 0) {
|
||||
fixture.root.reconciliationPrerequisite = { ready: false, category: 'permission-prerequisite',
|
||||
required: missingPermissions, legacy: [] }
|
||||
blocker(context, 'Finance 写权限门禁',
|
||||
`缺少权限=${missingPermissions.join(',')}; 对账接口将返回 A0403,禁止继续写入`,
|
||||
`缺少正式权限资源=${missingPermissions.join(',')}; 需由 IAM 角色初始化授予验收账号;对账接口将返回 A0403,禁止继续写入`,
|
||||
'missing-finance-write-permissions', permissionResponse)
|
||||
finish(context, start)
|
||||
return
|
||||
@@ -353,6 +358,8 @@ export async function run(context) {
|
||||
const invalid = fixture.sources.filter((source) => !source.feeSourceId || !source.customerId
|
||||
|| !source.businessDate || !source.waybillId || source.marker !== context.marker)
|
||||
if (invalid.length > 0) {
|
||||
fixture.root.reconciliationPrerequisite = { ready: false, category: 'finance-source-prerequisite',
|
||||
invalidSourceCount: invalid.length }
|
||||
blocker(context, 'Fixture 来源契约完整',
|
||||
`存在 ${invalid.length} 条来源缺少 feeSourceId/customerId/businessDate/运单标识/marker`,
|
||||
'invalid-finance-fixture-contract')
|
||||
@@ -364,6 +371,8 @@ export async function run(context) {
|
||||
&& source.direction === 'RECEIVABLE' && source.currency === 'CNY')
|
||||
const businessDates = new Set(receivables.map((source) => source.businessDate))
|
||||
if (receivables.length < 4 || businessDates.size < 2) {
|
||||
fixture.root.reconciliationPrerequisite = { ready: false, category: 'finance-source-prerequisite',
|
||||
receivableCount: receivables.length, businessDateCount: businessDates.size }
|
||||
blocker(context, '三张 AR 与日期范围 Fixture 门禁',
|
||||
`至少需要同客户 4 条 CNY 应收来源且跨 2 个真实 businessDate;actual sources=${receivables.length}, dates=${businessDates.size}`,
|
||||
'insufficient-receivable-date-fixture')
|
||||
|
||||
@@ -125,6 +125,9 @@ test('AR/AP 数组为空或不完整时原子阻断且不查询、不写入', as
|
||||
assert.equal(empty.gaps.length, 1)
|
||||
assert.equal(empty.steps[0].passed, false)
|
||||
assert.match(empty.steps[0].evidence, /actual AR=0, AP=0/)
|
||||
assert.match(empty.steps[0].evidence, /上游 Finance fixture 前置条件未满足,非结算服务故障/)
|
||||
assert.match(empty.gaps[0][4], /prerequisite/)
|
||||
assert.equal(empty.calls.some((call) => call.method === 'POST'), false)
|
||||
|
||||
const partial = fakeContext({ receivableSettlements: [
|
||||
{ settlementId: '101', statementId: '11', direction: 'RECEIVABLE', status: 'ISSUED' },
|
||||
@@ -133,4 +136,6 @@ test('AR/AP 数组为空或不完整时原子阻断且不查询、不写入', as
|
||||
assert.equal(partial.calls.length, 0)
|
||||
assert.equal(partial.gaps.length, 1)
|
||||
assert.match(partial.steps[0].evidence, /actual AR=1, AP=1/)
|
||||
assert.match(partial.steps[0].evidence, /receivable-settlement-prerequisite/)
|
||||
assert.equal(partial.calls.some((call) => call.method === 'POST'), false)
|
||||
})
|
||||
|
||||
@@ -128,9 +128,17 @@ export async function run(context) {
|
||||
const arSettlementIds = new Set(validReceivables.map(settlementId))
|
||||
const arStatementIds = new Set(validReceivables.map(statementId))
|
||||
if (validReceivables.length !== 3 || arSettlementIds.size !== 3 || arStatementIds.size !== 3 || validPayables.length !== 1) {
|
||||
const upstream = fixtures.reconciliationPrerequisite?.ready === false
|
||||
? fixtures.reconciliationPrerequisite
|
||||
: Object.values(fixtures.prerequisites || {}).find((item) => item?.ready === false)
|
||||
const missing = []
|
||||
if (statements.filter((item) => item.status === 'ISSUED').length < 3) missing.push('reconciliation-issued-statement-prerequisite')
|
||||
if (validReceivables.length !== 3) missing.push('receivable-settlement-prerequisite')
|
||||
if (validPayables.length !== 1) missing.push('payable-settlement-prerequisite')
|
||||
const category = upstream?.category || (missing.length === 1 ? missing[0] : 'upstream-finance-fixture-prerequisite')
|
||||
gap(context, '取得完整收付结算 Fixture', 'three AR and one AP settlements',
|
||||
`需要 3 张关联本轮 ISSUED 对账单的独立 AR 和 1 张带 supplierId 的 AP;actual AR=${validReceivables.length}, AP=${validPayables.length}`,
|
||||
'incomplete-settlement-fixture')
|
||||
`上游 Finance fixture 前置条件未满足,非结算服务故障;需要 3 张关联本轮 ISSUED 对账单的独立 AR 和 1 张带 supplierId 的 AP;actual AR=${validReceivables.length}, AP=${validPayables.length}; prerequisite=${category}; missing=${missing.join(',') || 'unknown'}`,
|
||||
category)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,19 @@ function validPdf(response) {
|
||||
&& response.bytes?.length > 4 && new TextDecoder().decode(response.bytes.slice(0, 4)) === '%PDF'
|
||||
}
|
||||
|
||||
async function waitForCompletedMeasurement(context, measurementPath, attempts = 30) {
|
||||
let response = null
|
||||
let measurement = null
|
||||
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
||||
response = await context.request('GET', measurementPath)
|
||||
measurement = response?.http === 200 && String(response?.body?.code) === '200'
|
||||
? response.body.data : null
|
||||
if (measurement?.processStatus === 'WAIT_SPLIT') return { response, measurement, attempts: attempt }
|
||||
if (attempt < attempts) await new Promise((resolve) => setTimeout(resolve, 1_000))
|
||||
}
|
||||
return { response, measurement, attempts }
|
||||
}
|
||||
|
||||
async function provisionWarehouseResources(context, { inboundId, warehouseId, totalVolumeCbm }) {
|
||||
const palletCount = Math.ceil(Number(totalVolumeCbm) / DEFAULT_PALLET_CAPACITY_CBM)
|
||||
if (!warehouseId || !Number.isFinite(palletCount) || palletCount < 1) return false
|
||||
@@ -394,8 +407,13 @@ export async function run(context) {
|
||||
context.apiSuccess(MODULE, 'Admin 审核通过墨家测量', approvalResponse,
|
||||
`measurementId=${asId(draft.measurementId)}`, OPTIONS)
|
||||
|
||||
const finalResponse = await context.request('GET', measurementPath)
|
||||
const finalMeasurement = context.apiSuccess(MODULE, '查询审核后的最终测量状态', finalResponse, '', OPTIONS)
|
||||
// Measurement approval may enqueue pricing/inbound completion. Poll the
|
||||
// public projection instead of treating the transient WAIT_PRICING state as
|
||||
// a smart-loading candidate or manufacturing fixture state in the script.
|
||||
const completed = await waitForCompletedMeasurement(context, measurementPath)
|
||||
const finalResponse = completed.response
|
||||
const finalMeasurement = context.apiSuccess(MODULE, '查询审核后的最终测量状态', finalResponse,
|
||||
`pollAttempts=${completed.attempts}`, OPTIONS)
|
||||
context.assert(MODULE, '审核后入库与测量状态闭环且结果未变化', finalResponse,
|
||||
finalMeasurement?.processStatus === 'WAIT_SPLIT' && finalMeasurement?.measureReviewStatus === 'APPROVED'
|
||||
&& finalMeasurement?.measurementStatus === 'APPROVED'
|
||||
@@ -407,6 +425,7 @@ export async function run(context) {
|
||||
context.createdResources.inboundMeasurement = {
|
||||
marker: context.marker, tenantId, inboundId, measurementId: asId(draft.measurementId),
|
||||
boxCode, finalStatus: finalMeasurement?.measurementStatus || null,
|
||||
processStatus: finalMeasurement?.processStatus || null,
|
||||
}
|
||||
finishScenario(context, start)
|
||||
}
|
||||
|
||||
@@ -34,14 +34,20 @@ async function request(context, method, path, body, headers = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
function fixture(context) {
|
||||
return context.createdResources?.orchestrationFixtures?.phase3Container
|
||||
|| context.createdResources?.inboundMeasurement
|
||||
|| context.createdResources?.orders
|
||||
export function selectContainerLoadingFixture(context) {
|
||||
const candidates = [
|
||||
context.createdResources?.orchestrationFixtures?.phase3Container,
|
||||
context.createdResources?.inboundMeasurement,
|
||||
context.createdResources?.orders,
|
||||
].filter(Boolean)
|
||||
// Finance and downstream modules temporarily switch context.marker for
|
||||
// their own isolated runs. Never select the older CAB fixture merely because
|
||||
// it is first in the object; ownership is the marker contract.
|
||||
return candidates.find((candidate) => candidate.marker === context.marker) || candidates[0]
|
||||
}
|
||||
|
||||
export async function run(context) {
|
||||
const source = fixture(context)
|
||||
const source = selectContainerLoadingFixture(context)
|
||||
const tenantId = id(source?.tenantId)
|
||||
const inboundId = id(source?.inboundId)
|
||||
if (!tenantId || !inboundId || source?.marker !== context.marker) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import test from 'node:test'
|
||||
|
||||
import { selectCustomsFlow } from './phase3-customs.mjs'
|
||||
import { buildFinalizeResource } from './phase3-loading-finalize.mjs'
|
||||
import { selectContainerLoadingFixture } from './phase3-container-loading.mjs'
|
||||
import { AcceptanceRun } from '../admin-test-env-acceptance.mjs'
|
||||
|
||||
function source(name) {
|
||||
@@ -126,6 +127,7 @@ test('PDA inventory snapshot gates batch and records the only safe mutation gap'
|
||||
|
||||
test('container loading creates and confirms booking before scoped candidates', () => {
|
||||
const value = source('./phase3-container-loading.mjs')
|
||||
assert.match(value, /candidates\.find\(\(candidate\) => candidate\.marker === context\.marker\)/)
|
||||
assertOrder(value, [
|
||||
"const bookingCreate = await request",
|
||||
"const submit = await request",
|
||||
@@ -147,6 +149,33 @@ test('container loading creates and confirms booking before scoped candidates',
|
||||
assert.doesNotMatch(value, /POST', CONTAINERS, containerPayload/)
|
||||
})
|
||||
|
||||
test('container loading selects the current marker instead of a stale CAB fixture', () => {
|
||||
const selected = selectContainerLoadingFixture({
|
||||
marker: 'ADMIN-RUN-AUTO1',
|
||||
createdResources: {
|
||||
orchestrationFixtures: {
|
||||
phase3Container: { marker: 'ADMIN-RUN-CAB', inboundId: 'CAB-INBOUND' },
|
||||
},
|
||||
inboundMeasurement: { marker: 'ADMIN-RUN-AUTO1', inboundId: 'FINANCE-INBOUND' },
|
||||
orders: { marker: 'ADMIN-RUN-AUTO1', inboundId: 'FINANCE-ORDER-INBOUND' },
|
||||
},
|
||||
})
|
||||
assert.equal(selected.inboundId, 'FINANCE-INBOUND')
|
||||
assert.equal(selected.marker, 'ADMIN-RUN-AUTO1')
|
||||
})
|
||||
|
||||
test('container loading keeps an unmatched candidate so marker failure stays observable', () => {
|
||||
const selected = selectContainerLoadingFixture({
|
||||
marker: 'ADMIN-RUN-AUTO1',
|
||||
createdResources: {
|
||||
orchestrationFixtures: {
|
||||
phase3Container: { marker: 'ADMIN-RUN-CAB', inboundId: 'CAB-INBOUND' },
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.equal(selected.marker, 'ADMIN-RUN-CAB')
|
||||
})
|
||||
|
||||
test('container loading enforces channel isolation and stable PDA identity across container number changes', () => {
|
||||
const value = source('./phase3-container-loading.mjs')
|
||||
assertOrder(value, [
|
||||
@@ -269,6 +298,20 @@ test('phase3 loading provisions its own marker-isolated WAIT_SPLIT fixture', asy
|
||||
assert.equal(run.marker.includes('-LOAD'), false)
|
||||
})
|
||||
|
||||
test('container fixture keeps the run marker and does not publish incomplete WAIT_PRICING resources', () => {
|
||||
const entry = source('../admin-test-env-acceptance.mjs')
|
||||
assert.match(entry, /const fixtureMarker = originalMarker/)
|
||||
assert.match(entry, /fixture\.finalStatus !== 'APPROVED'/)
|
||||
assert.match(entry, /禁止回退使用 WAIT_PRICING 订单/)
|
||||
})
|
||||
|
||||
test('measurement fixture polls asynchronous completion before exporting loading identity', () => {
|
||||
const measurement = source('./inbound-measurement.mjs')
|
||||
assert.match(measurement, /waitForCompletedMeasurement/)
|
||||
assert.match(measurement, /measurement\?\.processStatus === 'WAIT_SPLIT'/)
|
||||
assert.match(measurement, /setTimeout\(resolve, 1_000\)/)
|
||||
})
|
||||
|
||||
test('customs and finalize consume the same loading inbound identity', () => {
|
||||
const loadingFixture = { marker: 'RUN-1', tenantId: 10001, orderId: 101, inboundId: 201 }
|
||||
const loading = {
|
||||
|
||||
Reference in New Issue
Block a user