优化
This commit is contained in:
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
+15
@@ -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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user