diff --git a/deploy/sql/README.md b/deploy/sql/README.md index 3ebd532c5..406fc1eb1 100644 --- a/deploy/sql/README.md +++ b/deploy/sql/README.md @@ -186,27 +186,11 @@ SELECT 上述三个结果 `public_scope_violations`、`customer_scope_violations`、`parent_child_scope_mismatches` 必须都为 `0`。 -## 船司 FBA 价格公共作用域迁移 +## 船司 FBA 价格公共作用域 -- `fl_carrier_fba_rate` 的船司 FBA 价格固定归平台作用域 `tenant_id = 0`;客户计费规则仍按客户 `tenant_id > 0` 隔离。 -- 固定入口会先检查存量价格的 `(rate_code, rate_version)` 重复,以及相同匹配维度的有效期重叠;发现冲突时通过 `SQLSTATE 45000` 中止,不会自动覆盖价格。 -- 无冲突时,固定入口才会把存量船司价格迁移到 `tenant_id = 0`,并在末尾验证不存在非平台价格作用域。迁移完成后再启动或重启 `firstleg-service`。 - -执行前可只读盘点: - -```sql -SELECT tenant_id, COUNT(*) AS total - FROM baiyue_firstleg.fl_carrier_fba_rate - GROUP BY tenant_id - ORDER BY tenant_id; - -SELECT rate_code, rate_version, COUNT(*) AS total - FROM baiyue_firstleg.fl_carrier_fba_rate - GROUP BY rate_code, rate_version -HAVING COUNT(*) > 1; -``` - -如果固定入口因价格冲突中止,应先由业务确认保留版本、有效期和价格金额,再清理冲突后重新完整执行脚本;不要直接手工把冲突记录改为 `0`。 +- 当前测试环境 `fl_carrier_fba_rate` 为空表,不执行存量价格迁移。 +- 初始化表结构将船司 FBA 价格定义为平台公共价格,Firstleg 创建、查询和计费匹配固定使用 `tenant_id = 0`;客户计费规则仍按客户 `tenant_id > 0` 隔离。 +- 初始化或重建测试库后,再启动或重启 `firstleg-service`。 ## 海外仓阶梯费用增量 diff --git a/deploy/sql/头程数据库统一增量-MySQL8.sql b/deploy/sql/头程数据库统一增量-MySQL8.sql index accdd4f6e..905ab22c9 100644 --- a/deploy/sql/头程数据库统一增量-MySQL8.sql +++ b/deploy/sql/头程数据库统一增量-MySQL8.sql @@ -3053,61 +3053,6 @@ PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; DROP PROCEDURE IF EXISTS phase2_fba_add_column; --- 船司 FBA 价格是平台公共主数据;迁移前检查版本和有效期冲突,避免静默覆盖客户价格。 -DROP PROCEDURE IF EXISTS phase2_fba_migrate_carrier_rate_scope; -DELIMITER $$ -CREATE PROCEDURE phase2_fba_migrate_carrier_rate_scope() -BEGIN - DECLARE v_conflicts INT DEFAULT 0; - - SELECT COUNT(*) INTO v_conflicts - FROM ( - SELECT rate_code, rate_version - FROM fl_carrier_fba_rate - GROUP BY rate_code, rate_version - HAVING COUNT(*) > 1 - ) AS duplicate_versions; - IF v_conflicts > 0 THEN - SIGNAL SQLSTATE '45000' - SET MESSAGE_TEXT = 'FBA价格迁移失败:不同客户存在重复价格编码版本,请先合并'; - END IF; - - SELECT COUNT(*) INTO v_conflicts - FROM ( - SELECT first_rate.id - FROM fl_carrier_fba_rate first_rate - JOIN fl_carrier_fba_rate second_rate - ON first_rate.id < second_rate.id - AND first_rate.status = 'ACTIVE' - AND second_rate.status = 'ACTIVE' - AND first_rate.origin_warehouse_id <=> second_rate.origin_warehouse_id - AND first_rate.business_type <=> second_rate.business_type - AND first_rate.channel_code <=> second_rate.channel_code - AND first_rate.carrier_id <=> second_rate.carrier_id - AND first_rate.destination_code <=> second_rate.destination_code - AND first_rate.charge_unit <=> second_rate.charge_unit - AND first_rate.effective_from < COALESCE(second_rate.effective_to, '9999-12-31 23:59:59.999') - AND second_rate.effective_from < COALESCE(first_rate.effective_to, '9999-12-31 23:59:59.999') - LIMIT 1 - ) AS overlapping_rates; - IF v_conflicts > 0 THEN - SIGNAL SQLSTATE '45000' - SET MESSAGE_TEXT = 'FBA价格迁移失败:公共匹配维度存在有效期重叠,请先合并'; - END IF; - - UPDATE fl_carrier_fba_rate - SET tenant_id = 0 - WHERE tenant_id <> 0; - - IF EXISTS (SELECT 1 FROM fl_carrier_fba_rate WHERE tenant_id <> 0) THEN - SIGNAL SQLSTATE '45000' - SET MESSAGE_TEXT = 'FBA价格迁移失败:仍存在非平台价格作用域'; - END IF; -END$$ -DELIMITER ; -CALL phase2_fba_migrate_carrier_rate_scope(); -DROP PROCEDURE IF EXISTS phase2_fba_migrate_carrier_rate_scope; - -- 旧索引可能缺少业务类型、船司和结束时间;用新索引名并存,避免破坏线上依赖。 SET @idx_exists = (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'fl_carrier_fba_rate' AND INDEX_NAME = 'idx_fba_rate_unique_match'); diff --git a/firstleg/src/main/resources/db/phase2-fba-pricing-migration.sql b/firstleg/src/main/resources/db/phase2-fba-pricing-migration.sql index 5c5cb23d0..5024450c5 100644 --- a/firstleg/src/main/resources/db/phase2-fba-pricing-migration.sql +++ b/firstleg/src/main/resources/db/phase2-fba-pricing-migration.sql @@ -56,61 +56,6 @@ SET @sql = IF(@resource_tenant_needs_modify > 0, 'SELECT 1'); PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; --- 船司 FBA 价格是平台公共主数据;迁移前检查版本和有效期冲突,避免静默覆盖客户价格。 -DROP PROCEDURE IF EXISTS phase2_fba_migrate_carrier_rate_scope; -DELIMITER $$ -CREATE PROCEDURE phase2_fba_migrate_carrier_rate_scope() -BEGIN - DECLARE v_conflicts INT DEFAULT 0; - - SELECT COUNT(*) INTO v_conflicts - FROM ( - SELECT rate_code, rate_version - FROM fl_carrier_fba_rate - GROUP BY rate_code, rate_version - HAVING COUNT(*) > 1 - ) AS duplicate_versions; - IF v_conflicts > 0 THEN - SIGNAL SQLSTATE '45000' - SET MESSAGE_TEXT = 'FBA价格迁移失败:不同客户存在重复价格编码版本,请先合并'; - END IF; - - SELECT COUNT(*) INTO v_conflicts - FROM ( - SELECT first_rate.id - FROM fl_carrier_fba_rate first_rate - JOIN fl_carrier_fba_rate second_rate - ON first_rate.id < second_rate.id - AND first_rate.status = 'ACTIVE' - AND second_rate.status = 'ACTIVE' - AND first_rate.origin_warehouse_id <=> second_rate.origin_warehouse_id - AND first_rate.business_type <=> second_rate.business_type - AND first_rate.channel_code <=> second_rate.channel_code - AND first_rate.carrier_id <=> second_rate.carrier_id - AND first_rate.destination_code <=> second_rate.destination_code - AND first_rate.charge_unit <=> second_rate.charge_unit - AND first_rate.effective_from < COALESCE(second_rate.effective_to, '9999-12-31 23:59:59.999') - AND second_rate.effective_from < COALESCE(first_rate.effective_to, '9999-12-31 23:59:59.999') - LIMIT 1 - ) AS overlapping_rates; - IF v_conflicts > 0 THEN - SIGNAL SQLSTATE '45000' - SET MESSAGE_TEXT = 'FBA价格迁移失败:公共匹配维度存在有效期重叠,请先合并'; - END IF; - - UPDATE fl_carrier_fba_rate - SET tenant_id = 0 - WHERE tenant_id <> 0; - - IF EXISTS (SELECT 1 FROM fl_carrier_fba_rate WHERE tenant_id <> 0) THEN - SIGNAL SQLSTATE '45000' - SET MESSAGE_TEXT = 'FBA价格迁移失败:仍存在非平台价格作用域'; - END IF; -END$$ -DELIMITER ; -CALL phase2_fba_migrate_carrier_rate_scope(); -DROP PROCEDURE IF EXISTS phase2_fba_migrate_carrier_rate_scope; - DROP PROCEDURE IF EXISTS phase2_fba_add_column; -- 旧索引可能缺少业务类型、船司和结束时间;用新索引名并存,避免破坏线上依赖。 diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/InboundWorkflowSqlContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/InboundWorkflowSqlContractTest.java index fec46d05c..36a47d521 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/repository/InboundWorkflowSqlContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/InboundWorkflowSqlContractTest.java @@ -38,18 +38,4 @@ class InboundWorkflowSqlContractTest { .contains("SET warehouse_resource_tenant_id = 0") .contains("MODIFY COLUMN warehouse_resource_tenant_id BIGINT UNSIGNED NOT NULL"); } - - @Test - @DisplayName("船司价格存量迁移固定到平台作用域并拒绝冲突") - void shouldMigrateCarrierRatesToPublicScopeWithoutOverwritingConflicts() throws Exception { - String migration = new ClassPathResource("db/phase2-fba-pricing-migration.sql") - .getContentAsString(StandardCharsets.UTF_8); - - assertThat(migration) - .contains("phase2_fba_migrate_carrier_rate_scope") - .contains("GROUP BY rate_code, rate_version") - .contains("公共匹配维度存在有效期重叠") - .contains("SET tenant_id = 0") - .contains("WHERE tenant_id <> 0"); - } } diff --git a/reports/二期测试环境API验收-20260806180457.json b/reports/二期测试环境API验收-20260806180457.json new file mode 100644 index 000000000..2f0f04838 --- /dev/null +++ b/reports/二期测试环境API验收-20260806180457.json @@ -0,0 +1,96 @@ +{ + "reportName": "二期测试环境 API 验收", + "scope": [ + "头程下单", + "商户提交", + "平台审核", + "异步生成入库单", + "测量草稿", + "称重图片与结果", + "人工修正与确认", + "测量提交与审核", + "计费试算" + ], + "excluded": [ + "分柜", + "预装", + "PDA 拣货", + "装柜证据", + "装柜完成", + "派送" + ], + "gateway": "http://8.163.30.128:9000", + "marker": "API-E2E-20260806180457", + "startedAt": "2026-08-06T18:04:57.0000000", + "finishedAt": "2026-08-06T18:04:59.2138202+08:00", + "success": false, + "passedCount": 4, + "failedCount": 1, + "ids": { + "carrierId": "2085306128758538241", + "orderId": null, + "inboundId": null, + "measurementId": null, + "weightResultId": null, + "fileId": null + }, + "versions": { + "inbound": null, + "measurement": null, + "weightResult": null + }, + "fatal": "创建头程订单草稿 失败:HTTP=200 code=A0422 message=Validation failed", + "billingDefect": null, + "steps": [ + { + "name": "平台登录", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "" + }, + { + "name": "商户登录", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "" + }, + { + "name": "Firstleg 服务健康检查", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "" + }, + { + "name": "创建公共测试承运商", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "" + }, + { + "name": "创建头程订单草稿", + "passed": false, + "http": 200, + "code": "A0422", + "message": "Validation failed", + "requestId": "347014a0-f9e9-466a-8cc2-ff947cf946d3", + "traceId": "946f25aaf3824d029c5228b1fd4b52f8", + "evidence": "" + } + ] +} diff --git a/reports/二期测试环境API验收-20260806180542.json b/reports/二期测试环境API验收-20260806180542.json new file mode 100644 index 000000000..36e440c3f --- /dev/null +++ b/reports/二期测试环境API验收-20260806180542.json @@ -0,0 +1,57 @@ +{ + "reportName": "二期测试环境 API 验收", + "scope": [ + "头程下单", + "商户提交", + "平台审核", + "异步生成入库单", + "测量草稿", + "称重图片与结果", + "人工修正与确认", + "测量提交与审核", + "计费试算" + ], + "excluded": [ + "分柜", + "预装", + "PDA 拣货", + "装柜证据", + "装柜完成", + "派送" + ], + "gateway": "http://8.163.30.128:9000", + "marker": "API-E2E-20260806180542", + "startedAt": "2026-08-06T18:05:42.0000000", + "finishedAt": "2026-08-06T18:05:42.1924377+08:00", + "success": false, + "passedCount": 0, + "failedCount": 1, + "ids": { + "carrierId": null, + "orderId": null, + "inboundId": null, + "measurementId": null, + "weightResultId": null, + "fileId": null + }, + "versions": { + "inbound": null, + "measurement": null, + "weightResult": null + }, + "fatal": "平台登录 失败:HTTP=503 code= message=", + "billingDefect": null, + "steps": [ + { + "name": "平台登录", + "passed": false, + "http": 503, + "code": "", + "message": "", + "requestId": "93c3a0a1-684", + "traceId": null, + "evidence": "", + "details": null + } + ] +} diff --git a/reports/二期测试环境API验收-20260806180635.json b/reports/二期测试环境API验收-20260806180635.json new file mode 100644 index 000000000..c8ca59d8b --- /dev/null +++ b/reports/二期测试环境API验收-20260806180635.json @@ -0,0 +1,45 @@ +{ + "reportName": "二期测试环境 API 验收", + "scope": [ + "头程下单", + "商户提交", + "平台审核", + "异步生成入库单", + "测量草稿", + "称重图片与结果", + "人工修正与确认", + "测量提交与审核", + "计费试算" + ], + "excluded": [ + "分柜", + "预装", + "PDA 拣货", + "装柜证据", + "装柜完成", + "派送" + ], + "gateway": "http://8.163.30.128:9000", + "marker": "API-E2E-20260806180635", + "startedAt": "2026-08-06T18:06:35.0000000", + "finishedAt": "2026-08-06T18:06:37.8507638+08:00", + "success": false, + "passedCount": 0, + "failedCount": 0, + "ids": { + "carrierId": null, + "orderId": null, + "inboundId": null, + "measurementId": null, + "weightResultId": null, + "fileId": null + }, + "versions": { + "inbound": null, + "measurement": null, + "weightResult": null + }, + "fatal": "由于目标计算机积极拒绝,无法连接。 (8.163.30.128:9000)", + "billingDefect": null, + "steps": [] +} diff --git a/reports/二期测试环境API验收-20260806180746.json b/reports/二期测试环境API验收-20260806180746.json new file mode 100644 index 000000000..d6ee9f0c2 --- /dev/null +++ b/reports/二期测试环境API验收-20260806180746.json @@ -0,0 +1,101 @@ +{ + "reportName": "二期测试环境 API 验收", + "scope": [ + "头程下单", + "商户提交", + "平台审核", + "异步生成入库单", + "测量草稿", + "称重图片与结果", + "人工修正与确认", + "测量提交与审核", + "计费试算" + ], + "excluded": [ + "分柜", + "预装", + "PDA 拣货", + "装柜证据", + "装柜完成", + "派送" + ], + "gateway": "http://8.163.30.128:9000", + "marker": "API-E2E-20260806180746", + "startedAt": "2026-08-06T18:07:46.0000000", + "finishedAt": "2026-08-06T18:07:49.7438673+08:00", + "success": false, + "passedCount": 4, + "failedCount": 1, + "ids": { + "carrierId": "2085306838682238977", + "orderId": null, + "inboundId": null, + "measurementId": null, + "weightResultId": null, + "fileId": null + }, + "versions": { + "inbound": null, + "measurement": null, + "weightResult": null + }, + "fatal": "创建头程订单草稿 失败:HTTP=200 code=A0422 message=仓库未启用收货能力", + "billingDefect": null, + "steps": [ + { + "name": "平台登录", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "商户登录", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "Firstleg 服务健康检查", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "创建公共测试承运商", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "创建头程订单草稿", + "passed": false, + "http": 200, + "code": "A0422", + "message": "仓库未启用收货能力", + "requestId": "879f056b-40c2-486b-b643-c6ff7ab1a224", + "traceId": "282c6a4652d649608deaa0989f1109f8", + "evidence": "", + "details": null + } + ] +} diff --git a/reports/二期测试环境API验收-20260806180915.json b/reports/二期测试环境API验收-20260806180915.json new file mode 100644 index 000000000..df2299ccc --- /dev/null +++ b/reports/二期测试环境API验收-20260806180915.json @@ -0,0 +1,201 @@ +{ + "reportName": "二期测试环境 API 验收", + "scope": [ + "头程下单", + "商户提交", + "平台审核", + "异步生成入库单", + "测量草稿", + "称重图片与结果", + "人工修正与确认", + "测量提交与审核", + "计费试算" + ], + "excluded": [ + "分柜", + "预装", + "PDA 拣货", + "装柜证据", + "装柜完成", + "派送" + ], + "gateway": "http://8.163.30.128:9000", + "marker": "API-E2E-20260806180915", + "startedAt": "2026-08-06T18:09:15.0000000", + "finishedAt": "2026-08-06T18:09:18.4509682+08:00", + "success": false, + "passedCount": 13, + "failedCount": 1, + "ids": { + "warehouseId": "2085307213141311489", + "carrierId": "2085307212440862722", + "orderId": "2085307214373949442", + "inboundId": null, + "measurementId": null, + "weightResultId": null, + "fileId": null + }, + "versions": { + "inbound": null, + "measurement": null, + "weightResult": null + }, + "fatal": "商户提交订单 失败:HTTP=200 code=A0403 message=All required enterprise qualifications must be approved", + "billingDefect": null, + "steps": [ + { + "name": "平台登录", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "商户登录", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "Firstleg 服务健康检查", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "创建公共测试承运商", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "创建启用收货能力的公共测试仓", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "创建头程订单草稿", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "重放创建订单幂等请求", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "创建订单幂等结果一致", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "orderId=2085307214373949442", + "details": null + }, + { + "name": "查询订单草稿", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "订单初始状态为 DRAFT", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "status=DRAFT", + "details": null + }, + { + "name": "编辑头程订单草稿", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "回查编辑后的订单草稿", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "订单编辑内容已保存", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "customerRemark=API-E2E-20260806180915 编辑后草稿", + "details": null + }, + { + "name": "商户提交订单", + "passed": false, + "http": 200, + "code": "A0403", + "message": "All required enterprise qualifications must be approved", + "requestId": "43731de1-a837-47ed-944e-74a52c778c0d", + "traceId": "d65dce17b1974c0e85987a1f5b7b38c0", + "evidence": "", + "details": null + } + ] +} diff --git a/reports/二期测试环境API验收-20260806181135.json b/reports/二期测试环境API验收-20260806181135.json new file mode 100644 index 000000000..af961b56e --- /dev/null +++ b/reports/二期测试环境API验收-20260806181135.json @@ -0,0 +1,92 @@ +{ + "reportName": "二期测试环境 API 验收", + "scope": [ + "头程下单", + "商户提交", + "平台审核", + "异步生成入库单", + "测量草稿", + "称重图片与结果", + "人工修正与确认", + "测量提交与审核", + "计费试算" + ], + "excluded": [ + "分柜", + "预装", + "PDA 拣货", + "装柜证据", + "装柜完成", + "派送" + ], + "gateway": "http://8.163.30.128:9000", + "marker": "API-E2E-20260806181135", + "startedAt": "2026-08-06T18:11:35.0000000", + "finishedAt": "2026-08-06T18:11:36.8065276+08:00", + "success": false, + "passedCount": 4, + "failedCount": 0, + "ids": { + "qualificationReviewId": null, + "warehouseId": null, + "carrierId": null, + "orderId": null, + "inboundId": null, + "measurementId": null, + "weightResultId": null, + "fileId": null + }, + "versions": { + "inbound": null, + "measurement": null, + "weightResult": null + }, + "fatal": "The process cannot access the file 'C:\\Users\\BY-OY\\Desktop\\code\\chongchong-global\\apps\\pda-uniapp\\static\\c7.png' because it is being used by another process.", + "billingDefect": null, + "steps": [ + { + "name": "平台登录", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "商户登录", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "Firstleg 服务健康检查", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "查询商户必备资质", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + } + ] +} diff --git a/reports/二期测试环境API验收-20260806181211.json b/reports/二期测试环境API验收-20260806181211.json new file mode 100644 index 000000000..b454c8b20 --- /dev/null +++ b/reports/二期测试环境API验收-20260806181211.json @@ -0,0 +1,103 @@ +{ + "reportName": "二期测试环境 API 验收", + "scope": [ + "头程下单", + "商户提交", + "平台审核", + "异步生成入库单", + "测量草稿", + "称重图片与结果", + "人工修正与确认", + "测量提交与审核", + "计费试算" + ], + "excluded": [ + "分柜", + "预装", + "PDA 拣货", + "装柜证据", + "装柜完成", + "派送" + ], + "gateway": "http://8.163.30.128:9000", + "marker": "API-E2E-20260806181211", + "startedAt": "2026-08-06T18:12:11.0000000", + "finishedAt": "2026-08-06T18:12:12.6225155+08:00", + "success": false, + "passedCount": 4, + "failedCount": 1, + "ids": { + "qualificationReviewId": null, + "warehouseId": null, + "carrierId": null, + "orderId": null, + "inboundId": null, + "measurementId": null, + "weightResultId": null, + "fileId": null + }, + "versions": { + "inbound": null, + "measurement": null, + "weightResult": null + }, + "fatal": "补齐商户营业执照与法人身份证测试包 失败:HTTP=200 code=B0001 message=Only PDF, JPG and PNG files are supported / 仅支持 PDF、JPG、PNG 文件", + "billingDefect": null, + "steps": [ + { + "name": "平台登录", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "商户登录", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "Firstleg 服务健康检查", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "查询商户必备资质", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "补齐商户营业执照与法人身份证测试包", + "passed": false, + "http": 200, + "code": "B0001", + "message": "Only PDF, JPG and PNG files are supported / 仅支持 PDF、JPG、PNG 文件", + "requestId": "a3322a13-862f-4176-b36a-7f5bf48dabbc", + "traceId": "158c6783070f4605b966a8bbb5fd624d", + "evidence": "", + "details": null + } + ] +} diff --git a/reports/二期测试环境API验收-20260806181252.json b/reports/二期测试环境API验收-20260806181252.json new file mode 100644 index 000000000..b89eb5aa9 --- /dev/null +++ b/reports/二期测试环境API验收-20260806181252.json @@ -0,0 +1,103 @@ +{ + "reportName": "二期测试环境 API 验收", + "scope": [ + "头程下单", + "商户提交", + "平台审核", + "异步生成入库单", + "测量草稿", + "称重图片与结果", + "人工修正与确认", + "测量提交与审核", + "计费试算" + ], + "excluded": [ + "分柜", + "预装", + "PDA 拣货", + "装柜证据", + "装柜完成", + "派送" + ], + "gateway": "http://8.163.30.128:9000", + "marker": "API-E2E-20260806181252", + "startedAt": "2026-08-06T18:12:52.0000000", + "finishedAt": "2026-08-06T18:12:53.8062380+08:00", + "success": false, + "passedCount": 4, + "failedCount": 1, + "ids": { + "qualificationReviewId": null, + "warehouseId": null, + "carrierId": null, + "orderId": null, + "inboundId": null, + "measurementId": null, + "weightResultId": null, + "fileId": null + }, + "versions": { + "inbound": null, + "measurement": null, + "weightResult": null + }, + "fatal": "补齐商户营业执照与法人身份证测试包 失败:HTTP=200 code=A0409 message=Qualifications can only be uploaded before review / 仅待完善或已驳回客户可上传资质", + "billingDefect": null, + "steps": [ + { + "name": "平台登录", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "商户登录", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "Firstleg 服务健康检查", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "查询商户必备资质", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "补齐商户营业执照与法人身份证测试包", + "passed": false, + "http": 200, + "code": "A0409", + "message": "Qualifications can only be uploaded before review / 仅待完善或已驳回客户可上传资质", + "requestId": "c3dcb898-0edd-4244-97a0-e3b399a334ed", + "traceId": "18334f1be9ca4ee8ae0a88f98724cc51", + "evidence": "", + "details": null + } + ] +} diff --git a/reports/二期测试环境API验收-20260806181400.json b/reports/二期测试环境API验收-20260806181400.json new file mode 100644 index 000000000..3fb479e61 --- /dev/null +++ b/reports/二期测试环境API验收-20260806181400.json @@ -0,0 +1,390 @@ +{ + "reportName": "二期测试环境 API 验收", + "scope": [ + "头程下单", + "商户提交", + "平台审核", + "异步生成入库单", + "测量草稿", + "称重图片与结果", + "人工修正与确认", + "测量提交与审核", + "计费试算" + ], + "excluded": [ + "分柜", + "预装", + "PDA 拣货", + "装柜证据", + "装柜完成", + "派送" + ], + "gateway": "http://8.163.30.128:9000", + "marker": "API-E2E-20260806181400", + "startedAt": "2026-08-06T18:14:00.0000000", + "finishedAt": "2026-08-06T18:14:07.3773798+08:00", + "success": false, + "passedCount": 30, + "failedCount": 1, + "ids": { + "tenantId": "2085308406756851714", + "qualificationReviewId": "2085308409122439169", + "warehouseId": "2085308410753515522", + "carrierId": "2085308410510245890", + "orderId": "2085308411180847106", + "inboundId": "2085308426690875393", + "measurementId": "2085308428624449538", + "weightResultId": null, + "fileId": null + }, + "versions": { + "inbound": 0, + "measurement": 0, + "weightResult": null + }, + "fatal": "上传称重图片并触发 OCR 失败:HTTP=200 code=A0400 message=称重图片仅支持JPG、PNG、BMP", + "billingDefect": null, + "steps": [ + { + "name": "平台登录", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "商户登录", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "Firstleg 服务健康检查", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "查询商户必备资质", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "注册隔离的 API E2E 测试企业", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "登录隔离的 API E2E 测试企业", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "补齐商户营业执照与法人身份证测试包", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "提交商户资质审核", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "查询平台待审核企业", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "平台审核通过商户资质", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "创建公共测试承运商", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "创建启用收货能力的公共测试仓", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "创建头程订单草稿", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "重放创建订单幂等请求", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "创建订单幂等结果一致", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "orderId=2085308411180847106", + "details": null + }, + { + "name": "查询订单草稿", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "订单初始状态为 DRAFT", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "status=DRAFT", + "details": null + }, + { + "name": "编辑头程订单草稿", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "回查编辑后的订单草稿", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "订单编辑内容已保存", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "customerRemark=API-E2E-20260806181400 编辑后草稿", + "details": null + }, + { + "name": "商户提交订单", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "统一订单详情查询待审核状态", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "统一订单主状态为 PENDING_REVIEW", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "mainStatus=PENDING_REVIEW", + "details": null + }, + { + "name": "平台指派承运商", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "平台审核通过订单", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "异步生成入库单", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "inboundId=2085308426690875393", + "details": null + }, + { + "name": "查询入库单详情", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "入库单进入待测量状态", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "processStatus=WAIT_MEASURE, version=0", + "details": null + }, + { + "name": "查询初始测量详情", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "保存测量草稿", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "", + "details": null + }, + { + "name": "上传称重图片并触发 OCR", + "passed": false, + "http": 200, + "code": "A0400", + "message": "称重图片仅支持JPG、PNG、BMP", + "requestId": "b527475c-3624-4050-8cb1-13ec7ec48fa5", + "traceId": "81f92a40cd554334a987d4c2ff4a8679", + "evidence": "", + "details": null + } + ] +} diff --git a/scripts/phase2-test-env-acceptance.ps1 b/scripts/phase2-test-env-acceptance.ps1 new file mode 100644 index 000000000..5bae36ab7 --- /dev/null +++ b/scripts/phase2-test-env-acceptance.ps1 @@ -0,0 +1,431 @@ +param( + [string]$GatewayBaseUrl = 'http://8.163.30.128:9000', + [string]$PlatformUsername = 'platform_admin', + [string]$MerchantUsername = 'merchant_admin', + [string]$Password = 'ChangeMe123!', + [long]$TenantId = 10001, + [string]$OutputPath = '' +) + +$ErrorActionPreference = 'Stop' +$repoRoot = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path) +$stamp = Get-Date -Format 'yyyyMMddHHmmss' +$marker = "API-E2E-$stamp" +if ([string]::IsNullOrWhiteSpace($OutputPath)) { + $OutputPath = Join-Path $repoRoot "reports\二期测试环境API验收-$stamp.json" +} + +$steps = [System.Collections.Generic.List[object]]::new() +$ids = [ordered]@{ tenantId = [string]$TenantId; qualificationReviewId = $null; warehouseId = $null; carrierId = $null; orderId = $null; inboundId = $null; measurementId = $null; weightResultId = $null; fileId = $null } +$versions = [ordered]@{ inbound = $null; measurement = $null; weightResult = $null } +$fatal = $null +$billingDefect = $null + +function New-Key([string]$Action) { + return "$marker-$Action-$([guid]::NewGuid().ToString('N'))" +} + +function Invoke-Api( + [string]$Method, + [string]$Path, + [string]$Token = '', + [object]$Body = $null, + [hashtable]$ExtraHeaders = @{} +) { + $headers = @{} + if (-not [string]::IsNullOrWhiteSpace($Token)) { $headers.Authorization = "Bearer $Token" } + foreach ($entry in $ExtraHeaders.GetEnumerator()) { $headers[$entry.Key] = $entry.Value } + $args = @{ Method = $Method; Uri = "$GatewayBaseUrl$Path"; Headers = $headers; TimeoutSec = 30; SkipHttpErrorCheck = $true } + if ($null -ne $Body) { + $args.ContentType = 'application/json; charset=utf-8' + $jsonText = $Body | ConvertTo-Json -Depth 20 -Compress + $args.Body = [Text.Encoding]::UTF8.GetBytes($jsonText) + } + $response = Invoke-WebRequest @args + $json = $null + if (-not [string]::IsNullOrWhiteSpace($response.Content)) { + try { $json = $response.Content | ConvertFrom-Json } catch { } + } + return [pscustomobject]@{ + http = [int]$response.StatusCode + body = $json + raw = if ($null -eq $json) { $response.Content } else { $null } + requestId = if ($null -ne $json) { $json.requestId } else { $response.Headers['X-Request-Id'] } + traceId = if ($null -ne $json) { $json.traceId } else { $response.Headers['X-Trace-Id'] } + } +} + +function Invoke-Upload([string]$Path, [string]$Token, [string]$FilePath) { + $response = Invoke-WebRequest -Method POST -Uri "$GatewayBaseUrl$Path" ` + -Headers @{ Authorization = "Bearer $Token" } -Form @{ file = Get-Item -LiteralPath $FilePath } ` + -TimeoutSec 60 -SkipHttpErrorCheck + $json = $null + if (-not [string]::IsNullOrWhiteSpace($response.Content)) { + try { $json = $response.Content | ConvertFrom-Json } catch { } + } + return [pscustomobject]@{ + http = [int]$response.StatusCode + body = $json + raw = if ($null -eq $json) { $response.Content } else { $null } + requestId = if ($null -ne $json) { $json.requestId } else { $response.Headers['X-Request-Id'] } + traceId = if ($null -ne $json) { $json.traceId } else { $response.Headers['X-Trace-Id'] } + } +} + +function Invoke-RequiredQualificationUpload([string]$Token, [string]$FilePath, [string]$CreditCode) { + $query = "unifiedSocialCreditCode=$CreditCode&replaceBusinessLicense=true&replaceLegalPersonId=false" + $businessFile = Get-Item -LiteralPath $FilePath + $legalPersonFile = Get-Item -LiteralPath (Join-Path $businessFile.DirectoryName 'c8.png') + $content = & curl.exe -sS -X POST "$GatewayBaseUrl/api/merchant/v1/files/qualifications/required-package?$query" ` + -H "Authorization: Bearer $Token" ` + -F "businessLicense=@$($businessFile.FullName);type=image/png" ` + -F "legalPersonId=@$($legalPersonFile.FullName);type=image/png" + if ($LASTEXITCODE -ne 0) { throw '必备资质包上传发生传输错误' } + $json = $null + if (-not [string]::IsNullOrWhiteSpace($content)) { + try { $json = $content | ConvertFrom-Json } catch { } + } + return [pscustomobject]@{ + http = 200 + body = $json + raw = if ($null -eq $json) { [string]$content } else { $null } + requestId = if ($null -ne $json) { $json.requestId } else { $null } + traceId = if ($null -ne $json) { $json.traceId } else { $null } + } +} + +function Add-Step([string]$Name, $Response, [bool]$Passed, [string]$Evidence = '') { + $code = if ($null -ne $Response.body) { [string]$Response.body.code } else { $null } + $message = if ($null -ne $Response.body) { [string]$Response.body.message } else { [string]$Response.raw } + $steps.Add([pscustomobject]@{ + name = $Name + passed = $Passed + http = $Response.http + code = $code + message = $message + requestId = $Response.requestId + traceId = $Response.traceId + evidence = $Evidence + details = if ($Passed -or $null -eq $Response.body) { $null } else { $Response.body.data } + }) + Write-Host "[$(if ($Passed) { '通过' } else { '失败' })] $Name HTTP=$($Response.http) code=$code $Evidence" +} + +function Require-Success([string]$Name, $Response, [string]$Evidence = '') { + $passed = $Response.http -eq 200 -and [string]$Response.body.code -eq '200' + Add-Step $Name $Response $passed $Evidence + if (-not $passed) { throw "$Name 失败:HTTP=$($Response.http) code=$($Response.body.code) message=$($Response.body.message)" } + return $Response.body.data +} + +function Require-Condition([string]$Name, $Response, [bool]$Condition, [string]$Evidence) { + $passed = $Response.http -eq 200 -and [string]$Response.body.code -eq '200' -and $Condition + Add-Step $Name $Response $passed $Evidence + if (-not $passed) { throw "$Name 断言失败:$Evidence" } +} + +try { + $platformLoginResponse = Invoke-Api POST '/api/auth/login' '' @{ username = $PlatformUsername; password = $Password } + $platformLogin = Require-Success '平台登录' $platformLoginResponse + $platformToken = $platformLogin.accessToken + + $merchantLoginResponse = Invoke-Api POST '/api/auth/login' '' @{ username = $MerchantUsername; password = $Password } + $merchantLogin = Require-Success '商户登录' $merchantLoginResponse + $merchantToken = $merchantLogin.accessToken + + $healthResponse = Invoke-Api GET '/api/platform/v1/first-leg/status' $platformToken + $null = Require-Success 'Firstleg 服务健康检查' $healthResponse + + $qualificationsResponse = Invoke-Api GET '/api/merchant/v1/iam/tenant/qualifications' $merchantToken + $qualifications = @(Require-Success '查询商户必备资质' $qualificationsResponse) + $approvedTypes = @($qualifications | Where-Object { $_.status -eq 'APPROVED' } | ForEach-Object { $_.qualificationType }) + if ('BUSINESS_LICENSE' -notin $approvedTypes -or 'LEGAL_PERSON_ID' -notin $approvedTypes) { + $e2eUsername = "api_e2e_$stamp" + $e2ePassword = 'ApiE2e123!' + $registerResponse = Invoke-Api POST '/api/auth/register' '' @{ + username = $e2eUsername; password = $e2ePassword; displayName = 'API E2E Tester' + companyName = "API E2E 测试企业 $stamp"; legalPerson = 'API Tester'; legalPhone = '13800138000' + contactEmail = "api-e2e-$stamp@example.com"; countryCode = 'CN'; address = 'API E2E 测试地址' + } + $registration = Require-Success '注册隔离的 API E2E 测试企业' $registerResponse + $TenantId = [long]$registration.tenantId + $ids.tenantId = [string]$TenantId + + $e2eLoginResponse = Invoke-Api POST '/api/auth/login' '' @{ username = $e2eUsername; password = $e2ePassword } + $e2eLogin = Require-Success '登录隔离的 API E2E 测试企业' $e2eLoginResponse + $merchantToken = $e2eLogin.accessToken + + $qualificationImage = Join-Path (Split-Path -Parent $repoRoot) 'apps\pda-uniapp\static\c7.png' + $creditCode = "91${stamp}A1" + $qualificationUploadResponse = Invoke-RequiredQualificationUpload $merchantToken $qualificationImage $creditCode + $null = Require-Success '补齐商户营业执照与法人身份证测试包' $qualificationUploadResponse + + $reviewSubmitResponse = Invoke-Api POST '/api/merchant/v1/iam/tenant/qualifications/submit-review' $merchantToken + $null = Require-Success '提交商户资质审核' $reviewSubmitResponse + $reviewsResponse = Invoke-Api GET '/api/platform/v1/iam/reviews' $platformToken + $reviews = @(Require-Success '查询平台待审核企业' $reviewsResponse) + $review = @($reviews | Where-Object { [string]$_.tenantId -eq [string]$TenantId } | Select-Object -First 1) + if ($review.Count -eq 0) { throw "未找到 tenantId=$TenantId 的资质审核单" } + $ids.qualificationReviewId = [string]$review[0].id + $reviewApproveResponse = Invoke-Api POST "/api/platform/v1/iam/reviews/$($ids.qualificationReviewId)/approve" $platformToken @{ comment = "$marker 测试环境验收补齐" } + $null = Require-Success '平台审核通过商户资质' $reviewApproveResponse + } + + $carrierPayload = [ordered]@{ + partnerCode = "API-E2E-CARRIER-$stamp" + partnerName = "API E2E 测试承运商 $stamp" + partnerType = 'CARRIER' + types = @('CARRIER') + contactName = 'API Tester' + contactPhone = '13800138000' + contactEmail = 'api-e2e@example.com' + countryCode = 'CN' + province = '广东省' + city = '深圳市' + district = '南山区' + addressLine = 'API E2E 测试地址' + postalCode = '518000' + status = 'ACTIVE' + remark = $marker + } + $carrierResponse = Invoke-Api POST '/api/platform/v1/partners?tenantId=0' $platformToken $carrierPayload + $ids.carrierId = [string](Require-Success '创建公共测试承运商' $carrierResponse) + + $warehousePayload = [ordered]@{ + tenantId = 0 + warehouseCode = "API-E2E-WH-$stamp" + warehouseName = "API E2E 收货仓 $stamp" + warehouseType = 'DOMESTIC' + countryCode = 'CN' + timezone = 'UTC+8' + contactName = 'API Tester' + contactPhone = '13800138000' + contactEmail = 'api-e2e@example.com' + status = 'ACTIVE' + remark = $marker + address = [ordered]@{ + addressType = 'WAREHOUSE'; countryCode = 'CN'; stateProvince = '广东省'; city = '深圳市' + district = '南山区'; postalCode = '518000'; addressLine1 = 'API E2E 测试收货仓地址' + addressLine2 = $null; latitude = $null; longitude = $null + } + capabilities = @([ordered]@{ + capabilityCode = 'RECEIVING'; capabilityName = '头程收货'; enabled = $true + cutoffTime = '16:00:00'; description = $marker + }) + } + $warehouseResponse = Invoke-Api POST '/api/platform/v1/warehouse-admin' $platformToken $warehousePayload + $ids.warehouseId = [string](Require-Success '创建启用收货能力的公共测试仓' $warehouseResponse) + + $sender = [ordered]@{ + companyName = '深圳示例贸易有限公司'; contactName = 'API Tester'; phone = '13800138000' + email = 'api-e2e@example.com'; countryCode = 'CN'; province = '广东省'; city = '深圳市' + district = '南山区'; addressLine = 'API E2E 测试地址'; postalCode = '518000' + } + $orderPayload = [ordered]@{ + customerId = '191000000000000001' + customerName = '深圳示例贸易有限公司' + transportMode = 'SEA' + tradeTerm = 'FOB' + destinationCountryCode = 'US' + destinationWarehouseId = '193000000000000001' + destinationWarehouseName = 'ABE2' + expectedShipDate = (Get-Date).AddDays(10).ToString('yyyy-MM-dd') + customerRemark = "$marker 初始草稿" + sender = $sender + receiver = $null + cargoes = @([ordered]@{ + cargoName = 'API E2E 测试货物'; cargoNameEn = 'API E2E Test Cargo'; hsCode = '8518210000' + customsHsCode = '8518210000'; packageType = 'CARTON'; packageCount = 1 + grossWeightKg = 10; netWeightKg = 9; volumeCbm = 0.06 + lengthCm = 50; widthCm = 40; heightCm = 30; declaredValue = 100; unitDeclaredValue = 100 + currency = 'USD'; dangerousGoods = $false; cargoRemark = $marker + skuNo = "SKU-$stamp"; upcNo = "UPC-$stamp"; unitsPerPackage = 1; totalQuantity = 1 + material = '塑料'; usage = '测试'; model = 'E2E'; brand = 'API-E2E' + }) + pickupMethod = 'PICKUP' + receivingWarehouseId = $ids.warehouseId + receivingWarehouseName = $warehousePayload.warehouseName + channelCode = 'API-E2E-SEA' + channelName = 'API E2E 海运渠道' + carrierId = $null + carrierName = $null + taxMode = 'BUY_DECLARATION' + containerMode = 'LCL' + customsInspection = $false + destinationType = 'FBA' + amazonWarehouseCode = 'ABE2' + shipmentId = "SHIP-$stamp" + referenceId = $marker + } + + $createKey = New-Key 'order-create' + $createResponse = Invoke-Api POST '/api/merchant/v1/orders/first-leg' $merchantToken $orderPayload @{ 'Idempotency-Key' = $createKey } + $ids.orderId = [string](Require-Success '创建头程订单草稿' $createResponse) + + $replayResponse = Invoke-Api POST '/api/merchant/v1/orders/first-leg' $merchantToken $orderPayload @{ 'Idempotency-Key' = $createKey } + $replayedOrderId = [string](Require-Success '重放创建订单幂等请求' $replayResponse) + Require-Condition '创建订单幂等结果一致' $replayResponse ($replayedOrderId -eq $ids.orderId) "orderId=$replayedOrderId" + + $draftDetailResponse = Invoke-Api GET "/api/merchant/v1/orders/first-leg/$($ids.orderId)" $merchantToken + $draftDetail = Require-Success '查询订单草稿' $draftDetailResponse + Require-Condition '订单初始状态为 DRAFT' $draftDetailResponse ($draftDetail.order.status -eq 'DRAFT') "status=$($draftDetail.order.status)" + + $orderPayload.customerRemark = "$marker 编辑后草稿" + $updateResponse = Invoke-Api PUT "/api/merchant/v1/orders/first-leg/$($ids.orderId)" $merchantToken $orderPayload + $null = Require-Success '编辑头程订单草稿' $updateResponse + $editedDetailResponse = Invoke-Api GET "/api/merchant/v1/orders/first-leg/$($ids.orderId)" $merchantToken + $editedDetail = Require-Success '回查编辑后的订单草稿' $editedDetailResponse + Require-Condition '订单编辑内容已保存' $editedDetailResponse ($editedDetail.order.customerRemark -eq $orderPayload.customerRemark) "customerRemark=$($editedDetail.order.customerRemark)" + + $submitResponse = Invoke-Api POST "/api/merchant/v1/orders/first-leg/$($ids.orderId)/submit" $merchantToken $null @{ 'Idempotency-Key' = (New-Key 'order-submit') } + $null = Require-Success '商户提交订单' $submitResponse + + $unifiedPendingResponse = Invoke-Api GET "/api/platform/v1/orders/$($ids.orderId)?tenantId=$TenantId" $platformToken + $unifiedPending = Require-Success '统一订单详情查询待审核状态' $unifiedPendingResponse + Require-Condition '统一订单主状态为 PENDING_REVIEW' $unifiedPendingResponse ($unifiedPending.summary.mainStatus -eq 'PENDING_REVIEW') "mainStatus=$($unifiedPending.summary.mainStatus)" + + $carrierAssignResponse = Invoke-Api PUT "/api/platform/v1/orders/first-leg/$($ids.orderId)/carrier?tenantId=$TenantId" $platformToken @{ carrierId = $ids.carrierId } + $null = Require-Success '平台指派承运商' $carrierAssignResponse + + $approveOrderResponse = Invoke-Api POST "/api/platform/v1/orders/first-leg/$($ids.orderId)/approve?tenantId=$TenantId" $platformToken + $null = Require-Success '平台审核通过订单' $approveOrderResponse + + $unifiedApprovedResponse = $null + $unifiedApproved = $null + for ($attempt = 1; $attempt -le 20; $attempt++) { + $unifiedApprovedResponse = Invoke-Api GET "/api/platform/v1/orders/$($ids.orderId)?tenantId=$TenantId" $platformToken + if ($unifiedApprovedResponse.http -eq 200 -and [string]$unifiedApprovedResponse.body.code -eq '200' -and $null -ne $unifiedApprovedResponse.body.data.summary.inboundId) { + $unifiedApproved = $unifiedApprovedResponse.body.data + break + } + Start-Sleep -Seconds 3 + } + $inboundReady = $null -ne $unifiedApproved -and $null -ne $unifiedApproved.summary.inboundId + Add-Step '异步生成入库单' $unifiedApprovedResponse $inboundReady $(if ($inboundReady) { "inboundId=$($unifiedApproved.summary.inboundId)" } else { '60 秒内未取得 inboundId' }) + if (-not $inboundReady) { throw '订单审核后 60 秒内未异步生成入库单' } + $ids.inboundId = [string]$unifiedApproved.summary.inboundId + $versions.inbound = [int]$unifiedApproved.summary.inboundVersion + + $inboundDetailResponse = Invoke-Api GET "/api/platform/inbounds/$($ids.inboundId)?tenantId=$TenantId" $platformToken + $inboundDetail = Require-Success '查询入库单详情' $inboundDetailResponse + $versions.inbound = [int]$inboundDetail.summary.version + Require-Condition '入库单进入待测量状态' $inboundDetailResponse ($inboundDetail.summary.processStatus -eq 'WAIT_MEASURE') "processStatus=$($inboundDetail.summary.processStatus), version=$($versions.inbound)" + + $measurementDetailResponse = Invoke-Api GET "/api/platform/v1/first-leg/inbounds/$($ids.inboundId)/measurement?tenantId=$TenantId" $platformToken + $null = Require-Success '查询初始测量详情' $measurementDetailResponse + + $draftPayload = [ordered]@{ + measurementId = $null; version = 0; originalDocumentNo = $marker + originalGrossWeightKg = 10; originalVolumeCbm = 0.06; actualGoodsName = 'API E2E 测试货物' + items = @([ordered]@{ + itemNo = 'ITEM-1'; boxSpecification = '50x40x30cm'; packageCount = 1 + lengthCm = 50; widthCm = 40; heightCm = 30; unitWeightKg = 10 + intelligentImageFileId = $null; overLimitWarning = $null + outerLengthCm = 50; outerWidthCm = 40; outerHeightCm = 30; remark = $marker + }) + } + $saveDraftResponse = Invoke-Api PUT "/api/platform/v1/first-leg/inbounds/$($ids.inboundId)/measurement/draft?tenantId=$TenantId" $platformToken $draftPayload + $savedDraft = Require-Success '保存测量草稿' $saveDraftResponse + $ids.measurementId = [string]$savedDraft.measurementId + $versions.measurement = [int]$savedDraft.version + + $imagePath = Join-Path (Split-Path -Parent $repoRoot) 'apps\pda-uniapp\static\c7.png' + if (-not (Test-Path -LiteralPath $imagePath)) { throw "找不到测试称重图片:$imagePath" } + $uploadResponse = Invoke-Upload "/api/platform/v1/files/measurements/$TenantId/$($ids.inboundId)" $platformToken $imagePath + $uploaded = Require-Success '上传称重图片并触发 OCR' $uploadResponse + $ids.fileId = [string]$uploaded.fileId + + $weightPayload = [ordered]@{ + fileId = $ids.fileId + ocrProvider = 'API_E2E_MANUAL' + ocrRawText = if ($uploaded.weightValue) { [string]$uploaded.weightValue } else { 'OCR unavailable; manual verification required' } + ocrWeightValue = $uploaded.weightValue + ocrWeightUnit = if ($uploaded.weightValue) { 'KG' } else { $null } + confidence = $uploaded.confidence + } + $weightCreateResponse = Invoke-Api POST "/api/platform/v1/first-leg/inbounds/$($ids.inboundId)/weight-results?tenantId=$TenantId" $platformToken $weightPayload + $weightResult = Require-Success '登记称重结果' $weightCreateResponse + $ids.weightResultId = [string]$weightResult.id + $versions.weightResult = [int]$weightResult.version + + $correctResponse = Invoke-Api PUT "/api/platform/v1/first-leg/inbounds/$($ids.inboundId)/weight-results/$($ids.weightResultId)?tenantId=$TenantId" $platformToken @{ + version = $versions.weightResult; adjustedWeightValue = 10; adjustedWeightUnit = 'KG'; adjustmentReason = "$marker 人工确认" + } + $corrected = Require-Success '人工修正称重结果' $correctResponse + $versions.weightResult = [int]$corrected.version + + $confirmResponse = Invoke-Api POST "/api/platform/v1/first-leg/inbounds/$($ids.inboundId)/weight-results/$($ids.weightResultId)/confirm?tenantId=$TenantId" $platformToken @{ + version = $versions.weightResult + } + $confirmed = Require-Success '确认称重结果' $confirmResponse + $versions.weightResult = [int]$confirmed.version + + $measurementBeforeSubmitResponse = Invoke-Api GET "/api/platform/v1/first-leg/inbounds/$($ids.inboundId)/measurement?tenantId=$TenantId" $platformToken + $measurementBeforeSubmit = Require-Success '提交前回查测量详情' $measurementBeforeSubmitResponse + $versions.measurement = [int]$measurementBeforeSubmit.version + Require-Condition '确认重量汇总为 10KG' $measurementBeforeSubmitResponse ([decimal]$measurementBeforeSubmit.actualGrossWeightKg -eq 10) "actualGrossWeightKg=$($measurementBeforeSubmit.actualGrossWeightKg)" + + $measurementSubmitResponse = Invoke-Api POST "/api/platform/v1/first-leg/inbounds/$($ids.inboundId)/measurement/submit?tenantId=$TenantId" $platformToken @{ + measurementId = $ids.measurementId; version = $versions.measurement + } + $measurementSubmitted = Require-Success '提交测量审核' $measurementSubmitResponse + $versions.measurement = [int]$measurementSubmitted.version + Require-Condition '测量进入待审核状态' $measurementSubmitResponse ($measurementSubmitted.processStatus -eq 'MEASURE_PENDING' -and $measurementSubmitted.measureReviewStatus -eq 'PENDING') "processStatus=$($measurementSubmitted.processStatus), reviewStatus=$($measurementSubmitted.measureReviewStatus)" + + $pendingInboundResponse = Invoke-Api GET "/api/platform/inbounds/$($ids.inboundId)?tenantId=$TenantId" $platformToken + $pendingInbound = Require-Success '审核前回查入库单版本' $pendingInboundResponse + $versions.inbound = [int]$pendingInbound.summary.version + + $measurementApproveResponse = Invoke-Api POST "/api/platform/v1/inbounds/$($ids.inboundId)/measurements/approve?tenantId=$TenantId" $platformToken @{ + measurementId = $ids.measurementId; expectedVersion = $versions.inbound; reason = $null + } @{ 'Idempotency-Key' = (New-Key 'measurement-approve') } + $null = Require-Success '平台审核通过测量' $measurementApproveResponse + + $approvedInboundResponse = Invoke-Api GET "/api/platform/inbounds/$($ids.inboundId)?tenantId=$TenantId" $platformToken + $approvedInbound = Require-Success '审核后回查入库单' $approvedInboundResponse + $versions.inbound = [int]$approvedInbound.summary.version + Require-Condition '二期边界停在 WAIT_SPLIT' $approvedInboundResponse ($approvedInbound.summary.processStatus -eq 'WAIT_SPLIT' -and $approvedInbound.summary.measureReviewStatus -eq 'APPROVED') "processStatus=$($approvedInbound.summary.processStatus), reviewStatus=$($approvedInbound.summary.measureReviewStatus)" + + $feeResponse = Invoke-Api POST "/api/platform/v1/inbounds/$($ids.inboundId)/fees/calculate?tenantId=$TenantId" $platformToken @{} @{ 'Idempotency-Key' = (New-Key 'fee-calculate') } + $feePassed = $feeResponse.http -eq 200 -and [string]$feeResponse.body.code -eq '200' + Add-Step '二期计费接口试算' $feeResponse $feePassed "inboundStatus=$($approvedInbound.summary.processStatus)" + if (-not $feePassed) { + $billingDefect = [ordered]@{ + severity = 'P0' + summary = '测量审核后进入 WAIT_SPLIT,但计费接口不接受该状态,二期正常链路无法计费' + http = $feeResponse.http + code = [string]$feeResponse.body.code + message = [string]$feeResponse.body.message + requestId = $feeResponse.requestId + traceId = $feeResponse.traceId + } + } +} +catch { + $fatal = $_.Exception.Message + Write-Host "[中止] $fatal" +} +finally { + $passedCount = @($steps | Where-Object passed).Count + $failedCount = @($steps | Where-Object { -not $_.passed }).Count + $report = [ordered]@{ + reportName = '二期测试环境 API 验收' + scope = @('头程下单', '商户提交', '平台审核', '异步生成入库单', '测量草稿', '称重图片与结果', '人工修正与确认', '测量提交与审核', '计费试算') + excluded = @('分柜', '预装', 'PDA 拣货', '装柜证据', '装柜完成', '派送') + gateway = $GatewayBaseUrl + marker = $marker + startedAt = [datetime]::ParseExact($stamp, 'yyyyMMddHHmmss', $null).ToString('o') + finishedAt = (Get-Date).ToString('o') + success = $null -eq $fatal -and $null -eq $billingDefect -and $failedCount -eq 0 + passedCount = $passedCount + failedCount = $failedCount + ids = $ids + versions = $versions + fatal = $fatal + billingDefect = $billingDefect + steps = $steps + } + $report | ConvertTo-Json -Depth 20 | Set-Content -LiteralPath $OutputPath -Encoding utf8 + Write-Host "报告:$OutputPath" + $report | ConvertTo-Json -Depth 20 +}