This commit is contained in:
2026-08-06 18:34:52 +08:00
parent d2e3fef8f8
commit 74db8e165d
10 changed files with 717 additions and 43 deletions
@@ -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();
@@ -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<InboundPricingEntity> update = new LambdaUpdateWrapper<InboundPricingEntity>()
.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<InboundPricingEntity>()
.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"));
}
@@ -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<CustomerChargeRuleEntity> 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("当前入库单状态不允许费用试算");
}
@@ -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:
@@ -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("应付")
@@ -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);
}
@@ -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
}
]
}
+44 -26
View File
@@ -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
@@ -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();
@@ -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));
}