From 74db8e165db75b47ef1a53c2c0bcb896a5793789 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Thu, 6 Aug 2026 18:34:52 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/sql/头程数据库统一增量-MySQL8.sql | 28 + .../repository/FbaPricingRepository.java | 10 +- .../firstleg/service/FbaPricingService.java | 15 +- .../resources/openapi/fba-pricing-v1.yaml | 7 +- .../controller/FbaPricingContractTest.java | 5 +- .../service/FbaPricingServiceTest.java | 52 +- .../二期测试环境API验收-20260806181448.json | 530 ++++++++++++++++++ scripts/phase2-test-env-acceptance.ps1 | 70 ++- .../db/warehouse-public-master-data.sql | 28 + .../WarehouseSchemaContractTest.java | 15 + 10 files changed, 717 insertions(+), 43 deletions(-) create mode 100644 reports/二期测试环境API验收-20260806181448.json diff --git a/deploy/sql/头程数据库统一增量-MySQL8.sql b/deploy/sql/头程数据库统一增量-MySQL8.sql index 905ab22c9..c735d3801 100644 --- a/deploy/sql/头程数据库统一增量-MySQL8.sql +++ b/deploy/sql/头程数据库统一增量-MySQL8.sql @@ -3961,6 +3961,24 @@ JOIN wh_warehouse warehouse ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id); +-- 国内固定作业仓必须启用收货能力,FBA 目的地址仍不得创建作业能力。 +INSERT INTO wh_service_capability ( + id, tenant_id, warehouse_id, capability_code, capability_name, enabled, cutoff_time, + description, created_by, created_at, updated_by, updated_at, deleted, version +) +VALUES + (193000000000400001,0,193000000000200001,'RECEIVING','头程收货',1,'16:00:00', + '义乌仓头程订单收货能力',NULL,CURRENT_TIMESTAMP(3),NULL,CURRENT_TIMESTAMP(3),0,0), + (193000000000400002,0,193000000000200002,'RECEIVING','头程收货',1,'16:00:00', + '东莞仓头程订单收货能力',NULL,CURRENT_TIMESTAMP(3),NULL,CURRENT_TIMESTAMP(3),0,0) +ON DUPLICATE KEY UPDATE + capability_name = VALUES(capability_name), + enabled = VALUES(enabled), + cutoff_time = VALUES(cutoff_time), + description = VALUES(description), + deleted = 0, + updated_at = CURRENT_TIMESTAMP(3); + -- 平台托盘初始化主数据;内部状态仅供预占/占用,不作为平台维护字段。 INSERT INTO wh_pallet ( id, tenant_id, warehouse_id, pallet_code, pallet_type, capacity_cbm, @@ -4471,6 +4489,16 @@ BEGIN ) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '固定平台仓地址父子解析失败'; END IF; + + IF (SELECT COUNT(*) + FROM wh_service_capability capability + WHERE capability.tenant_id = 0 + AND capability.warehouse_id IN (193000000000200001,193000000000200002) + AND capability.capability_code = 'RECEIVING' + AND capability.enabled = 1 + AND capability.deleted = 0) <> 2 THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '国内固定作业仓收货能力写入校验失败'; + END IF; END// DELIMITER ; CALL verify_wh_fixed_platform_warehouse(); diff --git a/firstleg/src/main/java/com/baiyue/firstleg/repository/FbaPricingRepository.java b/firstleg/src/main/java/com/baiyue/firstleg/repository/FbaPricingRepository.java index 82e14bcac..c110d52c0 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/repository/FbaPricingRepository.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/repository/FbaPricingRepository.java @@ -201,15 +201,17 @@ public class FbaPricingRepository { .set(InboundFeeEntity::getUpdatedAt, LocalDateTime.now())); } - public int markInboundCalculated(long tenantId, long inboundId, String chargeMode, + public int markInboundCalculated(long tenantId, long inboundId, String processStatus, String chargeMode, java.math.BigDecimal chargeable) { LambdaUpdateWrapper update = new LambdaUpdateWrapper() .eq(InboundPricingEntity::getTenantId, tenantId) .eq(InboundPricingEntity::getId, inboundId) - .in(InboundPricingEntity::getProcessStatus, List.of("WAIT_PRICING", "FEE_CONFIRMING")) + .eq(InboundPricingEntity::getProcessStatus, processStatus) .in(InboundPricingEntity::getFeeStatus, List.of("UNPRICED", "CALCULATED")) - .set(InboundPricingEntity::getProcessStatus, "FEE_CONFIRMING") .set(InboundPricingEntity::getFeeStatus, "CALCULATED"); + if (!"WAIT_SPLIT".equals(processStatus)) { + update.set(InboundPricingEntity::getProcessStatus, "FEE_CONFIRMING"); + } if ("KG".equals(chargeMode)) update.set(InboundPricingEntity::getChargeableWeightKg, chargeable); else update.set(InboundPricingEntity::getChargeableVolumeCbm, chargeable); return inbounds.update(null, update); @@ -227,7 +229,7 @@ public class FbaPricingRepository { public int markInboundConfirmed(long tenantId, long inboundId) { return inbounds.update(null, new LambdaUpdateWrapper() .eq(InboundPricingEntity::getTenantId, tenantId).eq(InboundPricingEntity::getId, inboundId) - .eq(InboundPricingEntity::getProcessStatus, "FEE_CONFIRMING") + .in(InboundPricingEntity::getProcessStatus, List.of("FEE_CONFIRMING", "WAIT_SPLIT")) .eq(InboundPricingEntity::getFeeStatus, "CALCULATED") .set(InboundPricingEntity::getFeeStatus, "CONFIRMED")); } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/FbaPricingService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/FbaPricingService.java index 60bef7c98..450a2d49a 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/FbaPricingService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/FbaPricingService.java @@ -47,7 +47,7 @@ public class FbaPricingService { String hash = hash("CALCULATE", inboundId, requestedAt == null ? "CURRENT" : requestedAt.toString()); InboundPricingIdempotencyEntity token = beginIdempotency( tenantId, operatorId, inboundId, "FEE_CALCULATE", idempotencyKey, hash); - if ("COMPLETED".equals(token.getStatus())) return response(tenantId, token.getResultResourceId(), "FEE_CONFIRMING"); + if ("COMPLETED".equals(token.getStatus())) return response(tenantId, token.getResultResourceId(), null); InboundPricingEntity inbound = requireEligibleInbound(tenantId, inboundId); List matchedRules = repository.matchRules(inbound, pricingAt); @@ -88,11 +88,14 @@ public class FbaPricingService { repository.insertFee(fee); repository.insertItem(baseItem(tenantId, fee, rate, chargeable, subtotal, now)); if (additional.signum() > 0) repository.insertItem(additionalItem(tenantId, fee, additional, now)); - if (repository.markInboundCalculated(tenantId, inboundId, rule.getChargeMode(), chargeable) != 1) { + if (repository.markInboundCalculated(tenantId, inboundId, inbound.getProcessStatus(), + rule.getChargeMode(), chargeable) != 1) { throw conflict("入库单状态已变化,费用试算未保存"); } complete(token, fee.getId(), now); - return response(inbound, fee, rate.getUnitPrice(), "FEE_CONFIRMING"); + String nextStatus = "WAIT_SPLIT".equals(inbound.getProcessStatus()) + ? "WAIT_SPLIT" : "FEE_CONFIRMING"; + return response(inbound, fee, rate.getUnitPrice(), nextStatus); } @Transactional @@ -103,14 +106,14 @@ public class FbaPricingService { String hash = hash("CONFIRM", inboundId, current.getId().toString()); InboundPricingIdempotencyEntity token = beginIdempotency( tenantId, operatorId, inboundId, "FEE_CONFIRM", idempotencyKey, hash); - if ("COMPLETED".equals(token.getStatus())) return response(tenantId, token.getResultResourceId(), "FEE_CONFIRMING"); + if ("COMPLETED".equals(token.getStatus())) return response(tenantId, token.getResultResourceId(), null); LocalDateTime now = LocalDateTime.now(clock); if (repository.confirmFee(tenantId, current.getId(), operatorId, now) != 1 || repository.markInboundConfirmed(tenantId, inboundId) != 1) { throw conflict("费用或入库单状态已变化,不能重复确认"); } complete(token, current.getId(), now); - return response(tenantId, current.getId(), "FEE_CONFIRMING"); + return response(tenantId, current.getId(), null); } @Transactional @@ -232,7 +235,7 @@ public class FbaPricingService { private InboundPricingEntity requireEligibleInbound(long tenantId, long inboundId) { InboundPricingEntity inbound = requireApprovedInbound(tenantId, inboundId); - if (!List.of("WAIT_PRICING", "FEE_CONFIRMING").contains(inbound.getProcessStatus()) + if (!List.of("WAIT_PRICING", "FEE_CONFIRMING", "WAIT_SPLIT").contains(inbound.getProcessStatus()) || !List.of("UNPRICED", "CALCULATED").contains(inbound.getFeeStatus())) { throw conflict("当前入库单状态不允许费用试算"); } diff --git a/firstleg/src/main/resources/openapi/fba-pricing-v1.yaml b/firstleg/src/main/resources/openapi/fba-pricing-v1.yaml index cdf5048db..796de5b83 100644 --- a/firstleg/src/main/resources/openapi/fba-pricing-v1.yaml +++ b/firstleg/src/main/resources/openapi/fba-pricing-v1.yaml @@ -54,19 +54,20 @@ paths: /api/platform/v1/inbounds/{inboundId}/fees/calculate: post: summary: 按审核通过测量快照试算 FBA 费用 - description: 精确匹配唯一客户规则和唯一船司价格;需要 firstleg:fba-pricing:calculate 权限。 + description: 精确匹配唯一客户规则和唯一船司价格;测量审核后即使主流程已到 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: 确认费用只更新费用状态;主流程已到 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: post: - summary: 手工恢复发送费用快照 - description: 仅作为异常恢复接口;正常业务由客服测量审核通过后自动计费、发布并完成入库。 + summary: 发送已确认费用快照 + description: 该动作涉及后续仓储流程,本轮二期验收不调用。 parameters: [{$ref: '#/components/parameters/TenantId'}, {$ref: '#/components/parameters/InboundId'}, {$ref: '#/components/parameters/IdempotencyKey'}] responses: {'200': {description: 成功或业务失败}, '403': {description: 无权限}} /api/platform/v1/inbounds/{inboundId}/fees/latest: diff --git a/firstleg/src/test/java/com/baiyue/firstleg/controller/FbaPricingContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/controller/FbaPricingContractTest.java index c689185fe..1b2365bd7 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/controller/FbaPricingContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/controller/FbaPricingContractTest.java @@ -36,7 +36,7 @@ class FbaPricingContractTest { } @Test - @DisplayName("契约声明自动发布、已发布费用调整和幂等头") + @DisplayName("契约声明二期边界计费、已发布费用调整和幂等头") void documentsPricingLifecycle() throws Exception { String openapi = new ClassPathResource("openapi/fba-pricing-v1.yaml") .getContentAsString(StandardCharsets.UTF_8); @@ -48,7 +48,8 @@ class FbaPricingContractTest { .contains("/api/platform/v1/inbounds/{inboundId}/fees/send:") .contains("/api/platform/v1/inbounds/{inboundId}/fees/published:") .contains("Idempotency-Key") - .contains("客服测量审核通过后自动计费、发布并完成入库") + .contains("主流程已到 WAIT_SPLIT,也可独立试算且不回退主流程状态") + .contains("该动作涉及后续仓储流程,本轮二期验收不调用") .contains("不回退 INBOUNDED 状态") .doesNotContain("应收") .doesNotContain("应付") diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/FbaPricingServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/FbaPricingServiceTest.java index 57aa55e9f..9ddda2d3c 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/FbaPricingServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/FbaPricingServiceTest.java @@ -100,6 +100,51 @@ class FbaPricingServiceTest { assertThat(response.totalAmount()).isEqualByComparingTo("100.01"); } + @Test + @DisplayName("待分柜边界可独立计费且不回退主流程状态") + void calculatesAtWaitSplitWithoutRollingBackProcessStatus() { + readyRepository(); + InboundPricingEntity inbound = inbound("100.000", "1.0000"); + inbound.setProcessStatus("WAIT_SPLIT"); + CustomerChargeRuleEntity rule = rule("KG", "167.000000", 3, "CEILING"); + CarrierFbaRateEntity rate = rate("KG", "2.000000", "0.00"); + when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound); + when(repository.matchRules(inbound, NOW)).thenReturn(List.of(rule)); + when(repository.matchRates(inbound, "KG", NOW)).thenReturn(List.of(rate)); + + FeeSnapshotResponse response = service.calculate(TENANT, OPERATOR, INBOUND, NOW, "wait-split"); + + assertThat(response.feeStatus()).isEqualTo("CALCULATED"); + assertThat(response.nextProcessStatus()).isEqualTo("WAIT_SPLIT"); + verify(repository).markInboundCalculated(TENANT, INBOUND, "WAIT_SPLIT", "KG", + new BigDecimal("167.000")); + } + + @Test + @DisplayName("待分柜边界确认费用后仍保持主流程状态") + void confirmsAtWaitSplitWithoutRollingBackProcessStatus() { + InboundPricingEntity inbound = inbound("10.000", "1.0000"); + inbound.setProcessStatus("WAIT_SPLIT"); + inbound.setFeeStatus("CALCULATED"); + InboundFeeEntity fee = savedFee("CALCULATED"); + when(repository.findLatestFee(TENANT, INBOUND)).thenReturn(fee); + when(repository.findIdempotency(TENANT, OPERATOR, "FEE_CONFIRM", "confirm-wait-split")) + .thenReturn(null); + doAnswer(invocation -> { ((InboundPricingIdempotencyEntity) invocation.getArgument(0)).setId(900L); return null; }) + .when(repository).insertIdempotency(any()); + when(repository.confirmFee(TENANT, 501L, OPERATOR, NOW)).thenReturn(1); + when(repository.markInboundConfirmed(TENANT, INBOUND)).thenReturn(1); + when(repository.completeIdempotency(any(), eq(501L), eq(NOW))).thenReturn(1); + when(repository.findFee(TENANT, 501L)).thenReturn(fee); + when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound); + + FeeSnapshotResponse response = service.confirm( + TENANT, OPERATOR, INBOUND, "confirm-wait-split"); + + assertThat(response.nextProcessStatus()).isEqualTo("WAIT_SPLIT"); + verify(repository).markInboundConfirmed(TENANT, INBOUND); + } + @Test @DisplayName("审核未通过的测量快照禁止计费") void rejectsUnapprovedMeasurementSnapshot() { @@ -137,15 +182,18 @@ class FbaPricingServiceTest { @DisplayName("同一幂等键重复试算直接返回原费用快照") void returnsOriginalSnapshotForRepeatedCalculation() { InboundPricingIdempotencyEntity completed = completed("FEE_CALCULATE", 501L); + InboundPricingEntity inbound = inbound("10", "1"); + 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("10", "1")); + when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound); InboundFeeItemEntity item = new InboundFeeItemEntity(); item.setUnitPrice(new BigDecimal("2.00")); when(repository.findBaseItem(TENANT, 501L)).thenReturn(item); FeeSnapshotResponse response = service.calculate(TENANT, OPERATOR, INBOUND, NOW, "same"); assertThat(response.feeId()).isEqualTo(501L); + assertThat(response.nextProcessStatus()).isEqualTo("WAIT_SPLIT"); verify(repository, never()).matchRules(any(), any()); verify(repository, never()).insertFee(any()); } @@ -233,7 +281,7 @@ class FbaPricingServiceTest { doAnswer(invocation -> { ((InboundFeeEntity) invocation.getArgument(0)).setId(501L); return null; }) .when(repository).insertFee(any()); when(repository.nextFeeVersion(TENANT, INBOUND)).thenReturn(1); - when(repository.markInboundCalculated(eq(TENANT), eq(INBOUND), any(), any())).thenReturn(1); + when(repository.markInboundCalculated(eq(TENANT), eq(INBOUND), any(), any(), any())).thenReturn(1); when(repository.completeIdempotency(any(), eq(501L), eq(NOW))).thenReturn(1); } diff --git a/reports/二期测试环境API验收-20260806181448.json b/reports/二期测试环境API验收-20260806181448.json new file mode 100644 index 000000000..efd745555 --- /dev/null +++ b/reports/二期测试环境API验收-20260806181448.json @@ -0,0 +1,530 @@ +{ + "reportName": "二期测试环境 API 验收", + "scope": [ + "头程下单", + "商户提交", + "平台审核", + "异步生成入库单", + "测量草稿", + "称重图片与结果", + "人工修正与确认", + "测量提交与审核", + "计费试算" + ], + "excluded": [ + "分柜", + "预装", + "PDA 拣货", + "装柜证据", + "装柜完成", + "派送" + ], + "gateway": "http://8.163.30.128:9000", + "marker": "API-E2E-20260806181448", + "startedAt": "2026-08-06T18:14:48.0000000", + "finishedAt": "2026-08-06T18:14:56.1155716+08:00", + "success": false, + "passedCount": 42, + "failedCount": 1, + "ids": { + "tenantId": "2085308606829346818", + "qualificationReviewId": "2085308609115242497", + "warehouseId": "2085308610297528322", + "carrierId": "2085308609974566913", + "orderId": "2085308610682916865", + "inboundId": "2085308616747372546", + "measurementId": "2085308626759176194", + "weightResultId": "2085308631419047938", + "fileId": "2085308627513511938" + }, + "versions": { + "inbound": 3, + "measurement": 1, + "weightResult": 2 + }, + "fatal": null, + "billingDefect": { + "severity": "P0", + "summary": "测量审核后进入 WAIT_SPLIT,但计费接口不接受该状态,二期正常链路无法计费", + "http": 200, + "code": "A0409", + "message": "当前入库单状态不允许费用试算", + "requestId": "b55826a9-c964-4bb5-91c6-1b862a469eee", + "traceId": "44c08299046a4c5ab095b39463091218" + }, + "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=2085308610682916865", + "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-20260806181448 编辑后草稿", + "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=2085308616747372546", + "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": 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": "确认重量汇总为 10KG", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "actualGrossWeightKg=10", + "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=MEASURE_PENDING, reviewStatus=PENDING", + "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": "二期边界停在 WAIT_SPLIT", + "passed": true, + "http": 200, + "code": "200", + "message": "success", + "requestId": null, + "traceId": null, + "evidence": "processStatus=WAIT_SPLIT, reviewStatus=APPROVED", + "details": null + }, + { + "name": "二期计费接口试算", + "passed": false, + "http": 200, + "code": "A0409", + "message": "当前入库单状态不允许费用试算", + "requestId": "b55826a9-c964-4bb5-91c6-1b862a469eee", + "traceId": "44c08299046a4c5ab095b39463091218", + "evidence": "inboundStatus=WAIT_SPLIT", + "details": null + } + ] +} diff --git a/scripts/phase2-test-env-acceptance.ps1 b/scripts/phase2-test-env-acceptance.ps1 index 5bae36ab7..73f36eb68 100644 --- a/scripts/phase2-test-env-acceptance.ps1 +++ b/scripts/phase2-test-env-acceptance.ps1 @@ -2,7 +2,8 @@ param( [string]$GatewayBaseUrl = 'http://8.163.30.128:9000', [string]$PlatformUsername = 'platform_admin', [string]$MerchantUsername = 'merchant_admin', - [string]$Password = 'ChangeMe123!', + [string]$PlatformPassword = 'ChangeMe123!', + [string]$MerchantPassword = 'ChangeMe123!', [long]$TenantId = 10001, [string]$OutputPath = '' ) @@ -16,7 +17,7 @@ if ([string]::IsNullOrWhiteSpace($OutputPath)) { } $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 } +$ids = [ordered]@{ tenantId = [string]$TenantId; qualificationReviewId = $null; warehouseId = $null; carrierId = $null; customerRuleId = $null; carrierRateId = $null; orderId = $null; inboundId = $null; measurementId = $null; weightResultId = $null; fileId = $null; feeId = $null } $versions = [ordered]@{ inbound = $null; measurement = $null; weightResult = $null } $fatal = $null $billingDefect = $null @@ -56,19 +57,20 @@ function Invoke-Api( } 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 + $file = Get-Item -LiteralPath $FilePath + $content = & curl.exe -sS -X POST "$GatewayBaseUrl$Path" ` + -H "Authorization: Bearer $Token" -F "file=@$($file.FullName);type=image/png" + if ($LASTEXITCODE -ne 0) { throw '称重图片上传发生传输错误' } $json = $null - if (-not [string]::IsNullOrWhiteSpace($response.Content)) { - try { $json = $response.Content | ConvertFrom-Json } catch { } + if (-not [string]::IsNullOrWhiteSpace($content)) { + try { $json = $content | ConvertFrom-Json } catch { } } return [pscustomobject]@{ - http = [int]$response.StatusCode + http = 200 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'] } + 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 } } } @@ -125,11 +127,11 @@ function Require-Condition([string]$Name, $Response, [bool]$Condition, [string]$ } try { - $platformLoginResponse = Invoke-Api POST '/api/auth/login' '' @{ username = $PlatformUsername; password = $Password } + $platformLoginResponse = Invoke-Api POST '/api/auth/login' '' @{ username = $PlatformUsername; password = $PlatformPassword } $platformLogin = Require-Success '平台登录' $platformLoginResponse $platformToken = $platformLogin.accessToken - $merchantLoginResponse = Invoke-Api POST '/api/auth/login' '' @{ username = $MerchantUsername; password = $Password } + $merchantLoginResponse = Invoke-Api POST '/api/auth/login' '' @{ username = $MerchantUsername; password = $MerchantPassword } $merchantLogin = Require-Success '商户登录' $merchantLoginResponse $merchantToken = $merchantLogin.accessToken @@ -216,6 +218,28 @@ try { $warehouseResponse = Invoke-Api POST '/api/platform/v1/warehouse-admin' $platformToken $warehousePayload $ids.warehouseId = [string](Require-Success '创建启用收货能力的公共测试仓' $warehouseResponse) + $effectiveFrom = (Get-Date).AddDays(-1).ToString('yyyy-MM-ddTHH:mm:ss') + $customerRuleResponse = Invoke-Api POST "/api/platform/v1/fba-pricing/customer-rules/versions?tenantId=$TenantId" $platformToken @{ + ruleCode = "API-E2E-RULE-$stamp"; customerId = '191000000000000001' + customerName = '深圳示例贸易有限公司'; businessType = 'FBA_SEA'; chargeMode = 'KG' + taxMode = 'TAX_INCLUDED'; conversionFactor = 167; roundingScale = 3 + roundingMode = 'HALF_UP'; currency = 'CNY'; effectiveFrom = $effectiveFrom + effectiveTo = $null; remark = $marker + } + $customerRule = Require-Success '创建客户计费规则版本' $customerRuleResponse + $ids.customerRuleId = [string]$customerRule.id + + $carrierRateResponse = Invoke-Api POST '/api/platform/v1/fba-pricing/carrier-rates/versions' $platformToken @{ + rateCode = "API-E2E-RATE-$stamp"; originWarehouseId = $ids.warehouseId + originWarehouseName = $warehousePayload.warehouseName; businessType = 'FBA_SEA' + channelCode = 'API-E2E-SEA'; carrierId = $ids.carrierId + carrierName = $carrierPayload.partnerName; destinationCode = 'ABE2'; chargeUnit = 'KG' + unitPrice = 2; currency = 'CNY'; additionalFee = 10; effectiveFrom = $effectiveFrom + effectiveTo = $null; remark = $marker + } + $carrierRate = Require-Success '创建承运商 FBA 费率版本' $carrierRateResponse + $ids.carrierRateId = [string]$carrierRate.id + $sender = [ordered]@{ companyName = '深圳示例贸易有限公司'; contactName = 'API Tester'; phone = '13800138000' email = 'api-e2e@example.com'; countryCode = 'CN'; province = '广东省'; city = '深圳市' @@ -387,19 +411,13 @@ try { 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 - } - } + $calculatedFee = Require-Success '二期计费接口试算' $feeResponse + $ids.feeId = [string]$calculatedFee.feeId + Require-Condition '计费试算保持 WAIT_SPLIT 主流程边界' $feeResponse ($calculatedFee.nextProcessStatus -eq 'WAIT_SPLIT') "nextProcessStatus=$($calculatedFee.nextProcessStatus)" + + $feeConfirmResponse = Invoke-Api POST "/api/platform/v1/inbounds/$($ids.inboundId)/fees/confirm?tenantId=$TenantId" $platformToken $null @{ 'Idempotency-Key' = (New-Key 'fee-confirm') } + $confirmedFee = Require-Success '确认二期费用快照' $feeConfirmResponse + Require-Condition '费用确认后仍保持 WAIT_SPLIT 主流程边界' $feeConfirmResponse ($confirmedFee.feeStatus -eq 'CONFIRMED' -and $confirmedFee.nextProcessStatus -eq 'WAIT_SPLIT') "feeStatus=$($confirmedFee.feeStatus), nextProcessStatus=$($confirmedFee.nextProcessStatus)" } catch { $fatal = $_.Exception.Message diff --git a/warehouse/src/main/resources/db/warehouse-public-master-data.sql b/warehouse/src/main/resources/db/warehouse-public-master-data.sql index 942fd0a05..6ea011f7c 100644 --- a/warehouse/src/main/resources/db/warehouse-public-master-data.sql +++ b/warehouse/src/main/resources/db/warehouse-public-master-data.sql @@ -162,6 +162,24 @@ JOIN wh_warehouse warehouse ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id); +-- 国内固定作业仓必须启用收货能力,FBA 目的地址仍不得创建作业能力。 +INSERT INTO wh_service_capability ( + id, tenant_id, warehouse_id, capability_code, capability_name, enabled, cutoff_time, + description, created_by, created_at, updated_by, updated_at, deleted, version +) +VALUES + (193000000000400001,0,193000000000200001,'RECEIVING','头程收货',1,'16:00:00', + '义乌仓头程订单收货能力',NULL,CURRENT_TIMESTAMP(3),NULL,CURRENT_TIMESTAMP(3),0,0), + (193000000000400002,0,193000000000200002,'RECEIVING','头程收货',1,'16:00:00', + '东莞仓头程订单收货能力',NULL,CURRENT_TIMESTAMP(3),NULL,CURRENT_TIMESTAMP(3),0,0) +ON DUPLICATE KEY UPDATE + capability_name = VALUES(capability_name), + enabled = VALUES(enabled), + cutoff_time = VALUES(cutoff_time), + description = VALUES(description), + deleted = 0, + updated_at = CURRENT_TIMESTAMP(3); + DROP TEMPORARY TABLE IF EXISTS tmp_wh_fba_public_master_data; CREATE TEMPORARY TABLE tmp_wh_fba_public_master_data ( warehouse_id_seed BIGINT UNSIGNED NOT NULL, @@ -655,6 +673,16 @@ BEGIN ) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '固定平台仓地址父子解析失败'; END IF; + + IF (SELECT COUNT(*) + FROM wh_service_capability capability + WHERE capability.tenant_id = 0 + AND capability.warehouse_id IN (193000000000200001,193000000000200002) + AND capability.capability_code = 'RECEIVING' + AND capability.enabled = 1 + AND capability.deleted = 0) <> 2 THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '国内固定作业仓收货能力写入校验失败'; + END IF; END// DELIMITER ; CALL verify_wh_fixed_platform_warehouse(); diff --git a/warehouse/src/test/java/com/baiyue/warehouse/repository/WarehouseSchemaContractTest.java b/warehouse/src/test/java/com/baiyue/warehouse/repository/WarehouseSchemaContractTest.java index 3894f5d18..0a90de248 100644 --- a/warehouse/src/test/java/com/baiyue/warehouse/repository/WarehouseSchemaContractTest.java +++ b/warehouse/src/test/java/com/baiyue/warehouse/repository/WarehouseSchemaContractTest.java @@ -97,6 +97,21 @@ class WarehouseSchemaContractTest { .contains("idx_wh_express_tenant_status"); } + @Test + void shouldSeedReceivingCapabilityForDomesticOperatingWarehouses() throws IOException { + String masterData = read("warehouse-public-master-data.sql"); + String unified = Files.readString(Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql")); + + for (String sql : java.util.List.of(masterData, unified)) { + assertThat(sql) + .contains("193000000000200001,'RECEIVING','头程收货',1") + .contains("193000000000200002,'RECEIVING','头程收货',1") + .contains("国内固定作业仓收货能力写入校验失败") + .contains("warehouse.warehouse_type = 'FBA'") + .contains("FBA公共地址不得创建平台作业能力"); + } + } + private static String read(String file) throws IOException { return Files.readString(Path.of("src/main/resources/db", file)); }