暂存提交

This commit is contained in:
2026-07-31 16:53:07 +08:00
parent 45358ef40f
commit 704cb15ccf
98 changed files with 1944 additions and 134 deletions
@@ -22,6 +22,7 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@@ -40,6 +41,71 @@ public class PlatformFirstLegOrderController {
@Resource
private OrderApplicationService service;
@BusinessAudit(event = "ORDER_CREATE", resourceType = "FIRST_LEG_ORDER", description = "平台代客创建草稿")
@Operation(summary = "平台代客创建草稿", description = "在明确指定的目标租户内创建头程订单草稿,必须携带幂等键。")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "创建成功或幂等重放成功"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "请求参数或幂等键无效"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "无平台创建权限"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "409", description = "幂等键冲突或请求处理中"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "503", description = "主数据下游服务不可用")
})
@PostMapping
@PreAuthorize("hasAuthority('platform:order:create')")
public ApiResponse<Long> create(
@AuthenticationPrincipal AuthenticatedUser user,
@Parameter(description = "目标租户 ID", required = true) @RequestParam long tenantId,
@Parameter(description = "创建草稿幂等键,同键同请求返回首次结果", required = true)
@RequestHeader("Idempotency-Key") String idempotencyKey,
@Valid @RequestBody OrderRequests.SaveRequest request) {
long scopedTenant = OrderScopeGuard.platform(user, tenantId);
return ApiResponse.success(service.createDraft(scopedTenant, user.userId(), request, idempotencyKey));
}
@BusinessAudit(event = "ORDER_UPDATE", resourceType = "FIRST_LEG_ORDER", resourceId = "#id", description = "平台编辑草稿")
@Operation(summary = "平台编辑草稿", description = "在明确指定的目标租户内编辑草稿或驳回订单。")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "编辑成功"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "无平台编辑权限"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "订单不存在"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "409", description = "订单当前不可编辑"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "503", description = "主数据下游服务不可用")
})
@PutMapping("/{id}")
@PreAuthorize("hasAuthority('platform:order:edit')")
public ApiResponse<Void> updateDraft(
@AuthenticationPrincipal AuthenticatedUser user,
@Parameter(description = "目标租户 ID", required = true) @RequestParam long tenantId,
@Parameter(description = "订单 ID", required = true) @PathVariable long id,
@Valid @RequestBody OrderRequests.SaveRequest request) {
long scopedTenant = OrderScopeGuard.platform(user, tenantId);
service.updateDraft(scopedTenant, user.userId(), id, request);
return ApiResponse.success(null);
}
@BusinessAudit(event = "ORDER_SUBMIT", resourceType = "FIRST_LEG_ORDER", resourceId = "#id", description = "平台代客提交订单")
@Operation(summary = "平台代客提交订单", description = "在明确指定的目标租户内提交草稿订单,必须携带幂等键。")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "提交成功或幂等重放成功"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "请求参数或幂等键无效"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "无平台提交权限"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "订单不存在"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "409", description = "幂等键冲突或订单状态不可提交"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "503", description = "主数据下游服务不可用")
})
@PostMapping("/{id}/submit")
@PreAuthorize("hasAuthority('platform:order:submit')")
public ApiResponse<Void> submit(
@AuthenticationPrincipal AuthenticatedUser user,
@Parameter(description = "目标租户 ID", required = true) @RequestParam long tenantId,
@Parameter(description = "订单 ID", required = true) @PathVariable long id,
@Parameter(description = "提交订单幂等键,同键同请求返回首次结果", required = true)
@RequestHeader("Idempotency-Key") String idempotencyKey) {
long scopedTenant = OrderScopeGuard.platform(user, tenantId);
service.submit(scopedTenant, user.userId(), user.username(), id, idempotencyKey);
return ApiResponse.success(null);
}
@Operation(summary = "Tenant order query / 租户订单查询", description = "Queries a page of first-leg orders in the explicit target tenant. / 分页查询明确目标租户下的头程订单。")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Query succeeded / 查询成功"),
@@ -25,6 +25,11 @@ public class OrderEligibilityGuard {
}
} catch (FeignException exception) {
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "IAM eligibility service is unavailable");
} catch (BizException exception) {
throw exception;
} catch (RuntimeException exception) {
// Feign 解码或负载契约漂移不能泄露为通用 500,统一转换为可追踪的下游不可用。
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "IAM 下单资格响应无效");
}
}
}
+4
View File
@@ -22,6 +22,8 @@ CREATE TABLE IF NOT EXISTS ord_order (
pickup_method VARCHAR(24) NULL COMMENT '取货方式',
receiving_warehouse_id BIGINT UNSIGNED NULL COMMENT '国内收货仓ID',
receiving_warehouse_name VARCHAR(128) NULL COMMENT '国内收货仓快照',
receiving_warehouse_address VARCHAR(512) NULL COMMENT '国内收货仓完整地址快照',
receiving_time VARCHAR(128) NULL COMMENT '国内收货仓收货时间快照',
channel_code VARCHAR(64) NULL COMMENT '渠道编码',
channel_name VARCHAR(128) NULL COMMENT '渠道名称快照',
tax_mode VARCHAR(24) NULL COMMENT '买单或退税',
@@ -31,6 +33,8 @@ CREATE TABLE IF NOT EXISTS ord_order (
amazon_warehouse_code VARCHAR(32) NULL COMMENT '亚马逊仓库编码',
shipment_id VARCHAR(64) NULL COMMENT 'Amazon Shipment ID',
reference_id VARCHAR(64) NULL COMMENT 'Amazon Reference ID',
express_company VARCHAR(64) NULL COMMENT '履约快递公司名称快照',
express_tracking_no VARCHAR(128) NULL COMMENT '履约快递单号',
submitted_at DATETIME(3) NULL COMMENT '提交时间',
completed_at DATETIME(3) NULL COMMENT '完成时间',
created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID',
@@ -28,6 +28,8 @@ DELIMITER ;
CALL phase1_add_column('ord_order', 'pickup_method', "VARCHAR(24) NULL COMMENT 'Pickup method'");
CALL phase1_add_column('ord_order', 'receiving_warehouse_id', "BIGINT UNSIGNED NULL COMMENT 'Domestic receiving warehouse ID'");
CALL phase1_add_column('ord_order', 'receiving_warehouse_name', "VARCHAR(128) NULL COMMENT 'Domestic receiving warehouse snapshot'");
CALL phase1_add_column('ord_order', 'receiving_warehouse_address', "VARCHAR(512) NULL COMMENT '国内收货仓完整地址快照'");
CALL phase1_add_column('ord_order', 'receiving_time', "VARCHAR(128) NULL COMMENT '国内收货仓收货时间快照'");
CALL phase1_add_column('ord_order', 'channel_code', "VARCHAR(64) NULL COMMENT 'Shipping channel code'");
CALL phase1_add_column('ord_order', 'channel_name', "VARCHAR(128) NULL COMMENT 'Shipping channel snapshot'");
CALL phase1_add_column('ord_order', 'tax_mode', "VARCHAR(24) NULL COMMENT 'BUY_DECLARATION or TAX_REFUND'");
@@ -37,6 +39,8 @@ CALL phase1_add_column('ord_order', 'destination_type', "VARCHAR(24) NULL COMMEN
CALL phase1_add_column('ord_order', 'amazon_warehouse_code', "VARCHAR(32) NULL COMMENT 'Amazon warehouse code'");
CALL phase1_add_column('ord_order', 'shipment_id', "VARCHAR(64) NULL COMMENT 'Amazon Shipment ID'");
CALL phase1_add_column('ord_order', 'reference_id', "VARCHAR(64) NULL COMMENT 'Amazon Reference ID'");
CALL phase1_add_column('ord_order', 'express_company', "VARCHAR(64) NULL COMMENT '履约快递公司名称快照'");
CALL phase1_add_column('ord_order', 'express_tracking_no', "VARCHAR(128) NULL COMMENT '履约快递单号'");
CALL phase1_add_column('ord_order_cargo', 'sku_no', "VARCHAR(64) NULL COMMENT 'SKU number'");
CALL phase1_add_column('ord_order_cargo', 'upc_no', "VARCHAR(64) NULL COMMENT 'UPC number'");
@@ -52,3 +56,22 @@ CALL phase1_add_column('ord_order_cargo', 'model', "VARCHAR(64) NULL COMMENT 'Mo
CALL phase1_add_column('ord_order_cargo', 'brand', "VARCHAR(64) NULL COMMENT 'Brand'");
DROP PROCEDURE phase1_add_column;
-- 已有订单库的幂等记录迁移,和订单写入处于同一个本地事务。
CREATE TABLE IF NOT EXISTS ord_idempotency_record (
id BIGINT UNSIGNED NOT NULL COMMENT '幂等记录 ID',
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '租户 ID',
operator_id BIGINT UNSIGNED NOT NULL COMMENT '操作人 ID',
operation VARCHAR(32) NOT NULL COMMENT '操作类型',
resource_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '资源 ID',
idempotency_key VARCHAR(128) NOT NULL COMMENT '客户端幂等键',
request_hash CHAR(64) NOT NULL COMMENT '请求内容摘要',
status VARCHAR(16) NOT NULL COMMENT '处理状态',
result_order_id BIGINT UNSIGNED NULL COMMENT '重放结果订单 ID',
completed_at DATETIME(3) NULL COMMENT '完成时间',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
PRIMARY KEY (id),
UNIQUE KEY uk_order_idempotency_scope (tenant_id, operator_id, operation, idempotency_key),
KEY idx_order_idempotency_created (created_at)
) ENGINE=InnoDB COMMENT='订单写接口持久化幂等记录';
@@ -42,6 +42,62 @@ paths:
description: 幂等键冲突、请求处理中或订单状态冲突
'503':
description: 仓库或往来单位下游服务异常
/api/platform/v1/orders/first-leg:
post:
summary: 平台代客创建订单草稿
parameters:
- $ref: '#/components/parameters/TenantId'
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content:
application/json:
schema:
type: object
responses:
'200':
description: 创建成功或同请求重放成功
'403':
description: 需要 platform:order:create 权限
'409':
description: 幂等键冲突或请求仍在处理
/api/platform/v1/orders/first-leg/{id}/submit:
post:
summary: 平台代客提交指定订单
parameters:
- $ref: '#/components/parameters/OrderId'
- $ref: '#/components/parameters/TenantId'
- $ref: '#/components/parameters/IdempotencyKey'
responses:
'200':
description: 提交成功或同请求重放成功
'403':
description: 需要 platform:order:submit 权限
'404':
description: 订单不存在
'409':
description: 幂等键冲突或当前订单不可提交
/api/platform/v1/orders/first-leg/{id}:
put:
summary: 平台编辑指定订单草稿
parameters:
- $ref: '#/components/parameters/OrderId'
- $ref: '#/components/parameters/TenantId'
requestBody:
required: true
content:
application/json:
schema:
type: object
responses:
'200':
description: 编辑成功
'403':
description: 需要 platform:order:edit 权限
'404':
description: 订单不存在
'409':
description: 当前订单不可编辑
components:
securitySchemes:
bearerAuth:
@@ -49,6 +105,13 @@ components:
scheme: bearer
bearerFormat: JWT
parameters:
TenantId:
name: tenantId
in: query
required: true
schema:
type: integer
format: int64
OrderId:
name: id
in: path
@@ -2,6 +2,7 @@ package com.baiyue.order.controller;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.order.dto.OrderRequests;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.security.access.prepost.PreAuthorize;
@@ -9,9 +10,10 @@ import java.lang.reflect.Method;
import static org.assertj.core.api.Assertions.assertThat;
@DisplayName("订单接口权限控制")
class OrderControllerPermissionTest {
@Test
void 商户订单列表和详情具有对应查询权限码() throws Exception {
void merchantOrderListAndDetailRequireCorrespondingPermissions() throws Exception {
assertAuthority(MerchantFirstLegOrderController.class, "page", "hasAuthority('merchant:order:list')",
AuthenticatedUser.class, OrderRequests.QueryRequest.class);
assertAuthority(MerchantFirstLegOrderController.class, "detail", "hasAuthority('merchant:order:view')",
@@ -19,13 +21,23 @@ class OrderControllerPermissionTest {
}
@Test
void 平台订单列表和详情使用已配置的订单列表权限码() throws Exception {
void platformOrderListAndDetailUseConfiguredListPermission() throws Exception {
assertAuthority(PlatformFirstLegOrderController.class, "page", "hasAuthority('platform:order:list')",
AuthenticatedUser.class, long.class, OrderRequests.QueryRequest.class);
assertAuthority(PlatformFirstLegOrderController.class, "detail", "hasAuthority('platform:order:list')",
AuthenticatedUser.class, long.class, long.class);
}
@Test
void platformAssistedCreateAndEditUseDedicatedPermissions() throws Exception {
assertAuthority(PlatformFirstLegOrderController.class, "create", "hasAuthority('platform:order:create')",
AuthenticatedUser.class, long.class, String.class, OrderRequests.SaveRequest.class);
assertAuthority(PlatformFirstLegOrderController.class, "updateDraft", "hasAuthority('platform:order:edit')",
AuthenticatedUser.class, long.class, long.class, OrderRequests.SaveRequest.class);
assertAuthority(PlatformFirstLegOrderController.class, "submit", "hasAuthority('platform:order:submit')",
AuthenticatedUser.class, long.class, long.class, String.class);
}
private void assertAuthority(Class<?> controller, String methodName, String expected, Class<?>... parameterTypes)
throws NoSuchMethodException {
Method method = controller.getDeclaredMethod(methodName, parameterTypes);
@@ -5,14 +5,16 @@ import com.baiyue.order.entity.OrderEntity;
import com.baiyue.order.entity.OrderStatus;
import com.baiyue.order.repository.OrderRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
@DisplayName("订单状态流转")
class OrderApplicationServiceStateTest {
@Test
void 重复提交返回明确冲突且不重复写库() {
void repeatedSubmissionReturnsConflictWithoutDuplicatePersistence() {
OrderRepository repository=mock(OrderRepository.class);
OrderEntity order=new OrderEntity(); order.setId(1L); order.setTenantId(10001L); order.setStatus(OrderStatus.SUBMITTED);
when(repository.findOwned(10001,1)).thenReturn(order);
@@ -0,0 +1,32 @@
package com.baiyue.order.service;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.order.client.TenantEligibilityClient;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@DisplayName("订单下单资格校验")
class OrderEligibilityGuardTest {
@Test
void downstreamResponseDecodingFailureReturnsServiceUnavailable() {
TenantEligibilityClient client = mock(TenantEligibilityClient.class);
when(client.check("internal-token", 10001L))
.thenThrow(new IllegalStateException("响应契约漂移"));
OrderEligibilityGuard guard = new OrderEligibilityGuard();
ReflectionTestUtils.setField(guard, "client", client);
ReflectionTestUtils.setField(guard, "internalToken", "internal-token");
assertThatThrownBy(() -> guard.requireApproved(10001L))
.isInstanceOf(BizException.class)
.extracting(exception -> ((BizException) exception).getResultCode())
.isEqualTo(ResultCode.SERVICE_UNAVAILABLE);
}
}
@@ -6,6 +6,7 @@ import com.baiyue.order.dto.OrderRequests;
import com.baiyue.order.entity.OrderIdempotencyRecordEntity;
import com.baiyue.order.repository.OrderRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.dao.DuplicateKeyException;
@@ -21,12 +22,14 @@ import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@DisplayName("订单幂等处理")
class OrderIdempotencyServiceTest {
@Test
void 同键同请求返回首次创建的订单ID() {
void sameKeyAndRequestReturnsFirstCreatedOrderId() {
OrderRepository repository = mock(OrderRepository.class);
OrderIdempotencyRecordEntity existing = completed(998L);
AtomicReference<OrderIdempotencyRecordEntity> attempted = new AtomicReference<>();
@@ -47,7 +50,7 @@ class OrderIdempotencyServiceTest {
}
@Test
void 同键不同请求返回冲突() {
void sameKeyWithDifferentRequestReturnsConflict() {
OrderRepository repository = mock(OrderRepository.class);
doAnswer(invocation -> { throw new DuplicateKeyException("并发唯一键冲突"); })
.when(repository).insertIdempotency(any());
@@ -64,7 +67,7 @@ class OrderIdempotencyServiceTest {
}
@Test
void 并发请求尚未完成时返回冲突() {
void inProgressConcurrentRequestReturnsConflict() {
OrderRepository repository = mock(OrderRepository.class);
OrderIdempotencyRecordEntity existing = new OrderIdempotencyRecordEntity();
existing.setStatus("PROCESSING");
@@ -84,7 +87,25 @@ class OrderIdempotencyServiceTest {
}
@Test
void 不存在订单统一返回404() {
void submitOrderSameKeyReplayDoesNotEnterStateTransition() {
OrderRepository repository = mock(OrderRepository.class);
OrderIdempotencyRecordEntity existing = completed(998L);
doAnswer(invocation -> {
OrderIdempotencyRecordEntity attempted = invocation.getArgument(0);
existing.setRequestHash(attempted.getRequestHash());
throw new DuplicateKeyException("并发唯一键冲突");
}).when(repository).insertIdempotency(any());
when(repository.findIdempotency(10001L, 20001L, "SUBMIT_ORDER", "submit-key"))
.thenReturn(existing);
service(repository).submit(10001L, 20001L, "operator", 998L, "submit-key");
verify(repository, never()).findOwned(anyLong(), anyLong());
verify(repository, never()).saveOrder(any());
}
@Test
void missingOrderReturnsNotFound() {
OrderRepository repository = mock(OrderRepository.class);
assertThatThrownBy(() -> service(repository).detail(10001L, 404L))
@@ -94,23 +115,42 @@ class OrderIdempotencyServiceTest {
}
@Test
void SQL唯一键包含租户操作者操作和幂等键() throws Exception {
void sqlUniqueKeyContainsTenantOperatorActionAndIdempotencyKey() throws Exception {
String sql = new ClassPathResource("db/order.sql").getContentAsString(StandardCharsets.UTF_8);
assertThat(sql).contains(
"UNIQUE KEY uk_order_idempotency_scope (tenant_id, operator_id, operation, idempotency_key)");
}
@Test
void OpenAPI声明创建和提交均要求幂等键() throws Exception {
void openApiDeclaresIdempotencyKeyForCreateAndSubmit() throws Exception {
String openapi = new ClassPathResource("openapi/order-v1.yaml")
.getContentAsString(StandardCharsets.UTF_8);
assertThat(openapi)
.contains("/api/merchant/v1/orders/first-leg:")
.contains("/api/merchant/v1/orders/first-leg/{id}/submit:")
.contains("/api/platform/v1/orders/first-leg:")
.contains("/api/platform/v1/orders/first-leg/{id}/submit:")
.contains("platform:order:create")
.contains("platform:order:edit")
.contains("platform:order:submit")
.contains("name: Idempotency-Key")
.contains("required: true");
}
@Test
void newAndExistingDatabaseScriptsContainActualOrderWriteFields() throws Exception {
String baseline = new ClassPathResource("db/order.sql").getContentAsString(StandardCharsets.UTF_8);
String migration = new ClassPathResource("db/phase1-order-extension.sql")
.getContentAsString(StandardCharsets.UTF_8);
for (String column : new String[] {
"receiving_warehouse_address", "receiving_time", "express_company", "express_tracking_no"
}) {
assertThat(baseline).contains(column);
assertThat(migration).contains(column);
}
}
private OrderApplicationService service(OrderRepository repository) {
OrderApplicationService service = new OrderApplicationService();
ReflectionTestUtils.setField(service, "repository", repository);
@@ -6,6 +6,7 @@ import com.baiyue.common.core.exception.BizException;
import com.baiyue.order.client.PartnerClient;
import com.baiyue.order.client.WarehouseClient;
import com.baiyue.order.dto.OrderRequests;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.util.Collections;
@@ -13,9 +14,10 @@ import java.util.List;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
@DisplayName("订单主数据校验")
class OrderMasterDataGuardTest {
@Test
void 正常校验返回服务端可信快照() {
void validMasterDataReturnsTrustedServerSnapshot() {
WarehouseClient warehouses=mock(WarehouseClient.class); PartnerClient partners=mock(PartnerClient.class);
var partner=new PartnerClient.Partner(7L,10001L,"服务端客户名","ACTIVE");
var address=new WarehouseClient.Address("CN","广东省","深圳市","宝安区","518000","示例路1号",null);
@@ -28,14 +30,14 @@ class OrderMasterDataGuardTest {
assertThat(result.receiving().warehouse().warehouseName()).isEqualTo("深圳仓");
}
@Test
void 停用或跨租户主数据被拒绝() {
void inactiveOrCrossTenantMasterDataIsRejected() {
WarehouseClient warehouses=mock(WarehouseClient.class); PartnerClient partners=mock(PartnerClient.class);
when(partners.active("token",10002,7)).thenReturn(ApiResponse.success(null));
assertThatThrownBy(() -> new OrderMasterDataGuard(warehouses,partners,"token").validate(10002,request(7L,8L)))
.isInstanceOf(BizException.class).hasMessageContaining("服务暂时不可用");
}
@Test
void 主数据响应含空元素时返回受控业务错误而不是空指针异常() {
void masterDataResponseWithNullElementReturnsControlledBusinessError() {
WarehouseClient warehouses=mock(WarehouseClient.class); PartnerClient partners=mock(PartnerClient.class);
var partner=new PartnerClient.Partner(7L,10001L,"服务端客户名","ACTIVE");
var wh=new WarehouseClient.Detail(new WarehouseClient.Warehouse(8L,10001L,"深圳仓","ACTIVE"),
@@ -49,7 +51,7 @@ class OrderMasterDataGuardTest {
.isEqualTo(ResultCode.VALIDATION_FAILED);
}
@Test
void 缺失快照名称的主数据响应按服务不可用处理() {
void masterDataResponseMissingSnapshotNameIsServiceUnavailable() {
WarehouseClient warehouses=mock(WarehouseClient.class); PartnerClient partners=mock(PartnerClient.class);
when(partners.active("token",10001,7)).thenReturn(ApiResponse.success(new PartnerClient.Partner(7L,10001L,null,"ACTIVE")));
@@ -60,7 +62,7 @@ class OrderMasterDataGuardTest {
verifyNoInteractions(warehouses);
}
@Test
void 下游非Feign运行时异常映射为服务不可用() {
void downstreamNonFeignRuntimeExceptionMapsToServiceUnavailable() {
WarehouseClient warehouses=mock(WarehouseClient.class); PartnerClient partners=mock(PartnerClient.class);
when(partners.active("token",10001,7)).thenThrow(new IllegalStateException("解码失败"));