优化
This commit is contained in:
@@ -34,7 +34,12 @@ public class OrderRepository {
|
||||
@Resource private OrderIdempotencyRecordMapper idempotencyMapper;
|
||||
|
||||
public void insertOrder(OrderEntity order) { orderMapper.insert(order); }
|
||||
public int saveOrder(OrderEntity order) { return orderMapper.updateById(order); }
|
||||
public int saveOrder(long tenantId, OrderEntity order) {
|
||||
if (order.getTenantId() == null || order.getTenantId() != tenantId) return 0;
|
||||
return orderMapper.update(order, new LambdaUpdateWrapper<OrderEntity>()
|
||||
.eq(OrderEntity::getId, order.getId())
|
||||
.eq(OrderEntity::getTenantId, tenantId));
|
||||
}
|
||||
public void insertCargo(OrderCargoEntity cargo) { cargoMapper.insert(cargo); }
|
||||
public void insertParty(OrderPartyEntity party) { partyMapper.insert(party); }
|
||||
public void insertNote(OrderInternalNoteEntity note) { noteMapper.insert(note); }
|
||||
@@ -42,27 +47,38 @@ public class OrderRepository {
|
||||
public void insertOutbox(OutboxEventEntity event) { outboxMapper.insert(event); }
|
||||
public List<OutboxEventEntity> pendingInboundEvents(int limit) {
|
||||
return outboxMapper.selectList(new LambdaQueryWrapper<OutboxEventEntity>()
|
||||
.eq(OutboxEventEntity::getEventType, "FIRST_LEG_ORDER_APPROVED")
|
||||
.eq(OutboxEventEntity::getAggregateType, "FIRST_LEG_ORDER")
|
||||
.eq(OutboxEventEntity::getPublishStatus, "PENDING")
|
||||
.and(v -> v.isNull(OutboxEventEntity::getNextRetryAt)
|
||||
.or().le(OutboxEventEntity::getNextRetryAt, LocalDateTime.now()))
|
||||
.orderByAsc(OutboxEventEntity::getCreatedAt)
|
||||
.last("LIMIT " + Math.min(Math.max(limit, 1), 100)));
|
||||
}
|
||||
public void markOutboxPublished(long id, LocalDateTime publishedAt) {
|
||||
public void markOutboxPublished(long tenantId, long id, LocalDateTime publishedAt) {
|
||||
outboxMapper.update(null, new LambdaUpdateWrapper<OutboxEventEntity>()
|
||||
.eq(OutboxEventEntity::getId, id)
|
||||
.eq(OutboxEventEntity::getTenantId, tenantId)
|
||||
.eq(OutboxEventEntity::getPublishStatus, "PENDING")
|
||||
.set(OutboxEventEntity::getPublishStatus, "PUBLISHED")
|
||||
.set(OutboxEventEntity::getPublishedAt, publishedAt));
|
||||
}
|
||||
public void markOutboxRetry(long id, LocalDateTime nextRetryAt) {
|
||||
public void markOutboxRetry(long tenantId, long id, LocalDateTime nextRetryAt) {
|
||||
outboxMapper.update(null, new LambdaUpdateWrapper<OutboxEventEntity>()
|
||||
.eq(OutboxEventEntity::getId, id)
|
||||
.eq(OutboxEventEntity::getTenantId, tenantId)
|
||||
.eq(OutboxEventEntity::getPublishStatus, "PENDING")
|
||||
.setSql("retry_count = retry_count + 1")
|
||||
.set(OutboxEventEntity::getNextRetryAt, nextRetryAt));
|
||||
}
|
||||
public void markOutboxFailed(Long tenantId, long id) {
|
||||
outboxMapper.update(null, new LambdaUpdateWrapper<OutboxEventEntity>()
|
||||
.eq(OutboxEventEntity::getId, id)
|
||||
.eq(tenantId != null, OutboxEventEntity::getTenantId, tenantId)
|
||||
.isNull(tenantId == null, OutboxEventEntity::getTenantId)
|
||||
.eq(OutboxEventEntity::getPublishStatus, "PENDING")
|
||||
.set(OutboxEventEntity::getPublishStatus, "FAILED")
|
||||
.set(OutboxEventEntity::getNextRetryAt, null));
|
||||
}
|
||||
public void insertIdempotency(OrderIdempotencyRecordEntity record) { idempotencyMapper.insert(record); }
|
||||
|
||||
public OrderIdempotencyRecordEntity findIdempotency(
|
||||
@@ -74,11 +90,15 @@ public class OrderRepository {
|
||||
.eq(OrderIdempotencyRecordEntity::getIdempotencyKey, idempotencyKey));
|
||||
}
|
||||
|
||||
public int completeIdempotency(OrderIdempotencyRecordEntity record, Long resultOrderId) {
|
||||
public int completeIdempotency(long tenantId, OrderIdempotencyRecordEntity record, Long resultOrderId) {
|
||||
if (record.getTenantId() == null || record.getTenantId() != tenantId) return 0;
|
||||
record.setStatus("COMPLETED");
|
||||
record.setResultOrderId(resultOrderId);
|
||||
record.setCompletedAt(java.time.LocalDateTime.now());
|
||||
return idempotencyMapper.updateById(record);
|
||||
return idempotencyMapper.update(record, new LambdaUpdateWrapper<OrderIdempotencyRecordEntity>()
|
||||
.eq(OrderIdempotencyRecordEntity::getId, record.getId())
|
||||
.eq(OrderIdempotencyRecordEntity::getTenantId, tenantId)
|
||||
.eq(OrderIdempotencyRecordEntity::getStatus, "PROCESSING"));
|
||||
}
|
||||
|
||||
public OrderEntity findOwned(long tenantId, long orderId) {
|
||||
|
||||
@@ -67,7 +67,7 @@ public class OrderApplicationService {
|
||||
throw new BizException("Only draft or rejected orders can be edited");
|
||||
}
|
||||
apply(order, request, snapshots); order.setStatus(OrderStatus.DRAFT); order.setUpdatedBy(userId);
|
||||
if (repository.saveOrder(order) != 1) throw new BizException("Concurrent order update");
|
||||
if (repository.saveOrder(tenantId, order) != 1) throw new BizException("Concurrent order update");
|
||||
repository.replaceDraftChildren(tenantId, id);
|
||||
saveChildren(tenantId, userId, id, request);
|
||||
}
|
||||
@@ -90,7 +90,7 @@ public class OrderApplicationService {
|
||||
order.setStatus(target); order.setUpdatedBy(userId);
|
||||
if (target == OrderStatus.SUBMITTED) order.setSubmittedAt(LocalDateTime.now());
|
||||
if (target == OrderStatus.COMPLETED) order.setCompletedAt(LocalDateTime.now());
|
||||
if (repository.saveOrder(order) != 1) throw new BizException("Concurrent order update");
|
||||
if (repository.saveOrder(tenantId, order) != 1) throw new BizException("Concurrent order update");
|
||||
record(tenantId, id, from, target, target.name(), reason, userId, username);
|
||||
if (target == OrderStatus.APPROVED || target == OrderStatus.CANCELLED) writeOutbox(order, target);
|
||||
}
|
||||
@@ -142,7 +142,7 @@ public class OrderApplicationService {
|
||||
order.setExpressCompany(request.expressCompany().trim());
|
||||
order.setExpressTrackingNo(request.expressTrackingNo().trim());
|
||||
order.setUpdatedBy(userId);
|
||||
if (repository.saveOrder(order) != 1) throw new BizException(com.baiyue.common.core.api.ResultCode.CONFLICT, "订单已被并发修改");
|
||||
if (repository.saveOrder(tenantId, order) != 1) throw new BizException(com.baiyue.common.core.api.ResultCode.CONFLICT, "订单已被并发修改");
|
||||
}
|
||||
|
||||
private void validatePrototypeFields(OrderRequests.SaveRequest request) {
|
||||
@@ -248,7 +248,7 @@ public class OrderApplicationService {
|
||||
|
||||
private void completeIdempotency(IdempotencyContext context, Long resultOrderId) {
|
||||
if (context.enabled() && !context.replayed()) {
|
||||
if (repository.completeIdempotency(context.record(), resultOrderId) != 1) {
|
||||
if (repository.completeIdempotency(context.record().getTenantId(), context.record(), resultOrderId) != 1) {
|
||||
throw new BizException(com.baiyue.common.core.api.ResultCode.CONFLICT,
|
||||
"幂等记录已被并发修改");
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.baiyue.order.client.FirstlegInboundClient;
|
||||
import com.baiyue.order.entity.OutboxEventEntity;
|
||||
import com.baiyue.order.repository.OrderRepository;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -15,6 +16,7 @@ import java.time.LocalDateTime;
|
||||
|
||||
/** 可靠投递订单审核事件;接收端以订单唯一键保证重复投递安全。 */
|
||||
@Component
|
||||
@Slf4j
|
||||
public class OrderInboundOutboxPublisher {
|
||||
@Resource private OrderRepository repository;
|
||||
@Resource private FirstlegInboundClient client;
|
||||
@@ -24,17 +26,40 @@ public class OrderInboundOutboxPublisher {
|
||||
@Scheduled(fixedDelayString = "${baiyue.order.inbound-outbox.fixed-delay-ms:3000}")
|
||||
public void publishApprovedOrders() {
|
||||
for (OutboxEventEntity event : repository.pendingInboundEvents(50)) {
|
||||
if (!"FIRST_LEG_ORDER".equals(event.getAggregateType())
|
||||
|| !"FIRST_LEG_ORDER_APPROVED".equals(event.getEventType())) {
|
||||
failPermanently(event, "unsupported order event");
|
||||
continue;
|
||||
}
|
||||
FirstlegInboundClient.OrderApprovedInboundCommand command;
|
||||
try {
|
||||
FirstlegInboundClient.OrderApprovedInboundCommand command = json.readValue(
|
||||
command = json.readValue(
|
||||
event.getPayloadJson(), FirstlegInboundClient.OrderApprovedInboundCommand.class);
|
||||
} catch (Exception exception) {
|
||||
failPermanently(event, "invalid event payload");
|
||||
continue;
|
||||
}
|
||||
if (event.getTenantId() == null || event.getAggregateId() == null
|
||||
|| event.getTenantId() != command.tenantId()
|
||||
|| event.getAggregateId() != command.orderId()) {
|
||||
failPermanently(event, "event ownership does not match payload");
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
ApiResponse<Long> response = client.create(internalToken, command);
|
||||
if (response == null || !response.successful()) {
|
||||
throw new IllegalStateException("头程入库服务返回失败");
|
||||
}
|
||||
repository.markOutboxPublished(event.getId(), LocalDateTime.now());
|
||||
repository.markOutboxPublished(event.getTenantId(), event.getId(), LocalDateTime.now());
|
||||
} catch (Exception exception) {
|
||||
repository.markOutboxRetry(event.getId(), LocalDateTime.now().plusSeconds(10));
|
||||
repository.markOutboxRetry(event.getTenantId(), event.getId(), LocalDateTime.now().plusSeconds(10));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void failPermanently(OutboxEventEntity event, String reason) {
|
||||
log.warn("Order outbox event rejected: reason={}, eventId={}, tenantId={}, aggregateId={}, eventType={}",
|
||||
reason, event.getId(), event.getTenantId(), event.getAggregateId(), event.getEventType());
|
||||
repository.markOutboxFailed(event.getTenantId(), event.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,11 +23,11 @@ public class OrderMasterDataGuard {
|
||||
@Resource private WarehouseClient warehouses;
|
||||
@Resource private PartnerClient partners;
|
||||
@Value("${baiyue.security.internal-service-token}") private String token;
|
||||
@Value("${baiyue.warehouse.public-tenant-id:10001}")
|
||||
private long publicTenantId = 10001L;
|
||||
@Value("${baiyue.warehouse.public-tenant-id:0}")
|
||||
private long publicTenantId = 0L;
|
||||
public Snapshots validate(long tenantId, OrderRequests.SaveRequest request) {
|
||||
try {
|
||||
PartnerClient.Partner partner=request.customerId()==null ? null : loadPartner(tenantId,request.customerId());
|
||||
PartnerClient.Partner partner=request.customerId()==null ? null : loadPartner(request.customerId());
|
||||
var receiving=loadWarehouse(request.receivingWarehouseId(),"收货仓库");
|
||||
requireWarehouseType(receiving, "DOMESTIC", "收货仓库");
|
||||
if (receiving.addresses()==null || receiving.addresses().stream().noneMatch(Objects::nonNull))
|
||||
@@ -59,7 +59,7 @@ public class OrderMasterDataGuard {
|
||||
public void validatePersisted(long tenantId, Long partnerId, Long receivingWarehouseId,
|
||||
Long destinationWarehouseId, String destinationType) {
|
||||
try {
|
||||
if (partnerId!=null) loadPartner(tenantId,partnerId);
|
||||
if (partnerId!=null) loadPartner(partnerId);
|
||||
if (receivingWarehouseId==null) throw new BizException(ResultCode.NOT_FOUND,"收货仓库已停用或不可用");
|
||||
WarehouseClient.Detail receiving=loadWarehouse(receivingWarehouseId,"收货仓库");
|
||||
requireWarehouseType(receiving,"DOMESTIC","收货仓库");
|
||||
@@ -78,18 +78,18 @@ public class OrderMasterDataGuard {
|
||||
throw downstreamFailure(exception);
|
||||
}
|
||||
}
|
||||
private PartnerClient.Partner loadPartner(long tenantId,long id) {
|
||||
private PartnerClient.Partner loadPartner(long id) {
|
||||
ApiResponse<PartnerClient.Partner> response;
|
||||
try {
|
||||
response=partners.active(token,tenantId,id);
|
||||
response=partners.active(token,publicTenantId,id);
|
||||
} catch (FeignException.NotFound exception) {
|
||||
throw new BizException(ResultCode.NOT_FOUND,"\u672a\u7ed1\u5b9a\u53ef\u7528\u5f80\u6765\u5ba2\u6237");
|
||||
throw new BizException(ResultCode.NOT_FOUND,"平台往来单位不存在或不可用");
|
||||
}
|
||||
requireSuccessful(response,"往来单位");
|
||||
if (response.data()==null) throw unavailable("往来单位");
|
||||
var value=response.data();
|
||||
if (!Long.valueOf(tenantId).equals(value.tenantId()))
|
||||
throw new BizException(ResultCode.FORBIDDEN,"往来单位不属于当前企业");
|
||||
if (!Long.valueOf(publicTenantId).equals(value.tenantId()))
|
||||
throw new BizException(ResultCode.FORBIDDEN,"往来单位不属于平台公共主数据");
|
||||
if (!"ACTIVE".equals(value.status()))
|
||||
throw new BizException(ResultCode.CONFLICT,"往来单位已停用");
|
||||
if (value.types() == null || !value.types().contains("SHIPPER"))
|
||||
|
||||
@@ -5,7 +5,8 @@ import com.baiyue.common.core.api.ResultCode; import com.baiyue.common.core.exce
|
||||
*/
|
||||
public final class OrderScopeGuard { private OrderScopeGuard(){}
|
||||
public static long merchant(AuthenticatedUser u){requireApp(u,"MERCHANT");if(u.tenantId()==null||u.tenantId()<=0)throw new BizException(ResultCode.FORBIDDEN,"Merchant tenant is required");return u.tenantId();}
|
||||
public static void platform(AuthenticatedUser u){requireApp(u,"PLATFORM");}
|
||||
public static long platform(AuthenticatedUser u,long targetTenantId){requireApp(u,"PLATFORM");if(targetTenantId<=0)throw new BizException(ResultCode.BAD_REQUEST,"Target tenant is required");return targetTenantId;}
|
||||
public static void platform(AuthenticatedUser u){requireApp(u,"PLATFORM");requirePlatformTenant(u);}
|
||||
public static long platform(AuthenticatedUser u,long targetTenantId){platform(u);if(targetTenantId<=0)throw new BizException(ResultCode.BAD_REQUEST,"Target tenant is required");return targetTenantId;}
|
||||
private static void requireApp(AuthenticatedUser u,String expected){if(u==null||!expected.equals(u.appCode()))throw new BizException(ResultCode.FORBIDDEN,"Wrong application scope");}
|
||||
private static void requirePlatformTenant(AuthenticatedUser u){if(u.tenantId()==null||u.tenantId()!=0)throw new BizException(ResultCode.FORBIDDEN,"Platform tenant must be 0");}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ paths:
|
||||
/api/merchant/v1/orders/first-leg:
|
||||
get:
|
||||
summary: 客户端入库订单聚合列表
|
||||
description: 按当前登录租户分页返回订单、货物汇总、收货方快照及头程入仓编码,需要 merchant:order:list 权限。
|
||||
description: 按当前登录客户分页返回订单、货物汇总、收货方快照及头程入仓编码,需要 merchant:order:list 权限;客户 ID 从 JWT 的 tenantId 兼容字段取得。
|
||||
parameters:
|
||||
- {name: keyword, in: query, required: false, schema: {type: string, maxLength: 64}}
|
||||
- {name: orderNo, in: query, required: false, schema: {type: string, maxLength: 64}}
|
||||
@@ -53,12 +53,12 @@ paths:
|
||||
/api/platform/v1/orders/first-leg:
|
||||
get:
|
||||
summary: 平台分页查询头程订单
|
||||
description: tenantId 省略时查询全部客户租户订单,传入时仅查询指定租户。
|
||||
description: tenantId 是目标客户 ID 的兼容参数名;省略时查询全部客户订单,传入时仅查询指定客户。
|
||||
parameters:
|
||||
- name: tenantId
|
||||
in: query
|
||||
required: false
|
||||
description: 可选的目标租户筛选
|
||||
description: 可选的目标客户筛选;tenantId 为历史兼容参数名
|
||||
schema: {type: integer, format: int64, minimum: 1}
|
||||
responses:
|
||||
'200': {description: 查询成功或业务失败,按响应体 code 判断}
|
||||
@@ -79,7 +79,7 @@ paths:
|
||||
/api/platform/v1/orders/first-leg/tenant-counts:
|
||||
get:
|
||||
summary: 批量查询客户运单数
|
||||
description: 按客户租户 ID 返回真实头程运单数量,最多 200 个租户。
|
||||
description: 按客户 ID 返回真实头程运单数量,tenantIds 为历史兼容参数名;最多 200 个客户。
|
||||
parameters:
|
||||
- name: tenantIds
|
||||
in: query
|
||||
@@ -133,5 +133,5 @@ components:
|
||||
name: Idempotency-Key
|
||||
in: header
|
||||
required: true
|
||||
description: 同一租户和操作者重试同一请求时必须保持不变
|
||||
description: 同一客户和操作者重试同一请求时必须保持不变
|
||||
schema: {type: string, minLength: 1, maxLength: 128}
|
||||
|
||||
@@ -13,6 +13,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@SpringJUnitConfig(InternalOrderCargoController.class)
|
||||
@@ -45,4 +46,16 @@ class InternalOrderCargoControllerTest {
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("凭证无效");
|
||||
}
|
||||
|
||||
@Test
|
||||
void cargoFromAnotherCustomerIsNotExposed() {
|
||||
when(orders.findCargo(12L, 21L, 31L)).thenReturn(null);
|
||||
|
||||
assertThatThrownBy(() -> controller.detail("internal-token", 12L, 21L, 31L))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("不存在或不属于该订单");
|
||||
|
||||
verify(orders).findCargo(12L, 21L, 31L);
|
||||
verifyNoMoreInteractions(orders);
|
||||
}
|
||||
}
|
||||
|
||||
+14
-6
@@ -27,7 +27,7 @@ class PlatformOrderPageControllerTest {
|
||||
@Test
|
||||
void omittedTenantQueriesAcrossAllTenantsForPlatformUser() {
|
||||
|
||||
controller.page(user("PLATFORM"), null, query);
|
||||
controller.page(user("PLATFORM", 0L), null, query);
|
||||
|
||||
verify(service).page(null, query);
|
||||
}
|
||||
@@ -35,7 +35,7 @@ class PlatformOrderPageControllerTest {
|
||||
@Test
|
||||
void suppliedTenantFiltersOneTenantForPlatformUser() {
|
||||
|
||||
controller.page(user("PLATFORM"), 2084455952401629185L, query);
|
||||
controller.page(user("PLATFORM", 0L), 2084455952401629185L, query);
|
||||
|
||||
verify(service).page(2084455952401629185L, query);
|
||||
}
|
||||
@@ -43,7 +43,7 @@ class PlatformOrderPageControllerTest {
|
||||
@Test
|
||||
void invalidTenantFilterIsRejectedBeforeQuery() {
|
||||
|
||||
assertThatThrownBy(() -> controller.page(user("PLATFORM"), 0L, query))
|
||||
assertThatThrownBy(() -> controller.page(user("PLATFORM", 0L), 0L, query))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("Target tenant is required");
|
||||
verifyNoInteractions(service);
|
||||
@@ -52,13 +52,21 @@ class PlatformOrderPageControllerTest {
|
||||
@Test
|
||||
void merchantCannotUseCrossTenantPlatformList() {
|
||||
|
||||
assertThatThrownBy(() -> controller.page(user("MERCHANT"), null, query))
|
||||
assertThatThrownBy(() -> controller.page(user("MERCHANT", 10001L), null, query))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("Wrong application scope");
|
||||
verifyNoInteractions(service);
|
||||
}
|
||||
|
||||
private AuthenticatedUser user(String appCode) {
|
||||
return new AuthenticatedUser(1L, null, null, null, "operator", appCode, "WEB", Set.of());
|
||||
@Test
|
||||
void platformTokenWithCustomerTenantIsRejected() {
|
||||
assertThatThrownBy(() -> controller.page(user("PLATFORM", 10001L), null, query))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("Platform tenant must be 0");
|
||||
verifyNoInteractions(service);
|
||||
}
|
||||
|
||||
private AuthenticatedUser user(String appCode, Long tenantId) {
|
||||
return new AuthenticatedUser(1L, tenantId, null, null, "operator", appCode, "WEB", Set.of());
|
||||
}
|
||||
}
|
||||
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
package com.baiyue.order.repository;
|
||||
|
||||
import com.baiyue.order.entity.OrderEntity;
|
||||
import com.baiyue.order.entity.OrderIdempotencyRecordEntity;
|
||||
import com.baiyue.order.entity.OutboxEventEntity;
|
||||
import com.baiyue.order.mapper.OrderIdempotencyRecordMapper;
|
||||
import com.baiyue.order.mapper.OrderMapper;
|
||||
import com.baiyue.order.mapper.OutboxEventMapper;
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.conditions.AbstractWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class OrderRepositoryIsolationUpdateTest {
|
||||
@BeforeAll
|
||||
static void initializeTableMetadata() {
|
||||
initialize(OrderEntity.class, "order-update-test");
|
||||
initialize(OrderIdempotencyRecordEntity.class, "idempotency-update-test");
|
||||
initialize(OutboxEventEntity.class, "outbox-update-test");
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
void orderUpdateIsLimitedByCustomerAndOrderId() {
|
||||
OrderMapper mapper = mock(OrderMapper.class);
|
||||
OrderRepository repository = repository("orderMapper", mapper);
|
||||
OrderEntity order = new OrderEntity();
|
||||
order.setId(21L);
|
||||
order.setTenantId(11L);
|
||||
when(mapper.update(any(OrderEntity.class), any(Wrapper.class))).thenReturn(1);
|
||||
|
||||
repository.saveOrder(11L, order);
|
||||
|
||||
ArgumentCaptor<Wrapper<OrderEntity>> wrapper = ArgumentCaptor.forClass(Wrapper.class);
|
||||
verify(mapper).update(any(OrderEntity.class), wrapper.capture());
|
||||
assertScope(wrapper.getValue(), 11L, 21L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void mismatchedEntityCustomerCannotBeMovedByUpdate() {
|
||||
OrderMapper mapper = mock(OrderMapper.class);
|
||||
OrderRepository repository = repository("orderMapper", mapper);
|
||||
OrderEntity order = new OrderEntity();
|
||||
order.setId(21L);
|
||||
order.setTenantId(12L);
|
||||
|
||||
assertThat(repository.saveOrder(11L, order)).isZero();
|
||||
|
||||
verifyNoInteractions(mapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
void idempotencyCompletionIsLimitedByCustomerRecordAndProcessingState() {
|
||||
OrderIdempotencyRecordMapper mapper = mock(OrderIdempotencyRecordMapper.class);
|
||||
OrderRepository repository = repository("idempotencyMapper", mapper);
|
||||
OrderIdempotencyRecordEntity record = new OrderIdempotencyRecordEntity();
|
||||
record.setId(31L);
|
||||
record.setTenantId(11L);
|
||||
record.setStatus("PROCESSING");
|
||||
when(mapper.update(any(OrderIdempotencyRecordEntity.class), any(Wrapper.class))).thenReturn(1);
|
||||
|
||||
repository.completeIdempotency(11L, record, 21L);
|
||||
|
||||
ArgumentCaptor<Wrapper<OrderIdempotencyRecordEntity>> wrapper = ArgumentCaptor.forClass(Wrapper.class);
|
||||
verify(mapper).update(any(OrderIdempotencyRecordEntity.class), wrapper.capture());
|
||||
assertScope(wrapper.getValue(), 11L, 31L);
|
||||
assertThat(wrapper.getValue().getSqlSegment()).contains("status");
|
||||
assertThat(params(wrapper.getValue()).values()).contains("PROCESSING");
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
void everyOutboxStateUpdateIsLimitedByCustomerEventAndPendingState() {
|
||||
OutboxEventMapper mapper = mock(OutboxEventMapper.class);
|
||||
OrderRepository repository = repository("outboxMapper", mapper);
|
||||
|
||||
repository.markOutboxPublished(11L, 41L, LocalDateTime.now());
|
||||
repository.markOutboxRetry(11L, 42L, LocalDateTime.now());
|
||||
repository.markOutboxFailed(11L, 43L);
|
||||
|
||||
ArgumentCaptor<Wrapper<OutboxEventEntity>> wrappers = ArgumentCaptor.forClass(Wrapper.class);
|
||||
verify(mapper, times(3)).update(isNull(), wrappers.capture());
|
||||
List<Wrapper<OutboxEventEntity>> values = wrappers.getAllValues();
|
||||
assertScope(values.get(0), 11L, 41L);
|
||||
assertScope(values.get(1), 11L, 42L);
|
||||
assertScope(values.get(2), 11L, 43L);
|
||||
assertThat(values).allSatisfy(value -> {
|
||||
assertThat(value.getSqlSegment()).contains("publish_status");
|
||||
assertThat(params(value).values()).contains("PENDING");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
void pendingOrderEventsIncludeCancellationForTerminalHandling() {
|
||||
OutboxEventMapper mapper = mock(OutboxEventMapper.class);
|
||||
OrderRepository repository = repository("outboxMapper", mapper);
|
||||
when(mapper.selectList(any(Wrapper.class))).thenReturn(List.of());
|
||||
|
||||
repository.pendingInboundEvents(50);
|
||||
|
||||
ArgumentCaptor<Wrapper<OutboxEventEntity>> wrapper = ArgumentCaptor.forClass(Wrapper.class);
|
||||
verify(mapper).selectList(wrapper.capture());
|
||||
assertThat(wrapper.getValue().getSqlSegment())
|
||||
.contains("aggregate_type", "publish_status")
|
||||
.doesNotContain("event_type");
|
||||
assertThat(params(wrapper.getValue()).values())
|
||||
.contains("FIRST_LEG_ORDER", "PENDING");
|
||||
}
|
||||
|
||||
private static <T> void initialize(Class<T> entityType, String resource) {
|
||||
TableInfoHelper.initTableInfo(
|
||||
new MapperBuilderAssistant(new MybatisConfiguration(), resource), entityType);
|
||||
}
|
||||
|
||||
private OrderRepository repository(String field, Object mapper) {
|
||||
OrderRepository repository = new OrderRepository();
|
||||
ReflectionTestUtils.setField(repository, field, mapper);
|
||||
return repository;
|
||||
}
|
||||
|
||||
private void assertScope(Wrapper<?> wrapper, long tenantId, long id) {
|
||||
assertThat(wrapper.getSqlSegment()).contains("tenant_id", "id");
|
||||
assertThat(params(wrapper).values()).contains(tenantId, id);
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
private static java.util.Map<String, Object> params(Wrapper<?> wrapper) {
|
||||
return ((AbstractWrapper) wrapper).getParamNameValuePairs();
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ class OrderApplicationServiceStateTest {
|
||||
OrderMasterDataGuard masterData = mock(OrderMasterDataGuard.class);
|
||||
OrderRequests.SaveRequest request = request();
|
||||
when(masterData.validate(10001L, request)).thenReturn(snapshots());
|
||||
when(repository.completeIdempotency(any(), anyLong())).thenReturn(1);
|
||||
when(repository.completeIdempotency(anyLong(), any(), anyLong())).thenReturn(1);
|
||||
|
||||
Long orderId = service(repository, eligibility, masterData)
|
||||
.createDraft(10001L, 2L, request, "draft-before-approval");
|
||||
@@ -69,13 +69,13 @@ class OrderApplicationServiceStateTest {
|
||||
order.setStatus(OrderStatus.DRAFT);
|
||||
when(repository.findOwned(10001L, 1L)).thenReturn(order);
|
||||
when(masterData.validate(10001L, request)).thenReturn(snapshots());
|
||||
when(repository.saveOrder(order)).thenReturn(1);
|
||||
when(repository.saveOrder(10001L, order)).thenReturn(1);
|
||||
|
||||
service(repository, eligibility, masterData).updateDraft(10001L, 2L, 1L, request);
|
||||
|
||||
verify(eligibility, never()).requireApproved(anyLong());
|
||||
verify(repository).replaceDraftChildren(10001L, 1L);
|
||||
verify(repository).saveOrder(argThat(saved -> saved.getStatus() == OrderStatus.DRAFT));
|
||||
verify(repository).saveOrder(eq(10001L), argThat(saved -> saved.getStatus() == OrderStatus.DRAFT));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -97,7 +97,7 @@ class OrderApplicationServiceStateTest {
|
||||
.isEqualTo(ResultCode.FORBIDDEN);
|
||||
|
||||
verify(eligibility).requireApproved(10001L);
|
||||
verify(repository, never()).saveOrder(any());
|
||||
verify(repository, never()).saveOrder(anyLong(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -113,7 +113,7 @@ class OrderApplicationServiceStateTest {
|
||||
ReflectionTestUtils.setField(service,"json",new ObjectMapper());
|
||||
assertThatThrownBy(() -> service.transit(10001,2,"u",1,OrderStatus.SUBMITTED,null))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("重复提交");
|
||||
verify(repository,never()).saveOrder(any());
|
||||
verify(repository,never()).saveOrder(anyLong(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -123,7 +123,7 @@ class OrderApplicationServiceStateTest {
|
||||
OrderMasterDataGuard masterData = mock(OrderMasterDataGuard.class);
|
||||
OrderRequests.SaveRequest request = request(null, "SELF_EXPRESS", null, null, "OVERSEAS");
|
||||
when(masterData.validate(20002L, request)).thenReturn(snapshots(null));
|
||||
when(repository.completeIdempotency(any(), anyLong())).thenReturn(1);
|
||||
when(repository.completeIdempotency(anyLong(), any(), anyLong())).thenReturn(1);
|
||||
|
||||
service(repository, eligibility, masterData)
|
||||
.createDraft(20002L, 2L, request, "public-destination-no-parties");
|
||||
@@ -175,7 +175,21 @@ class OrderApplicationServiceStateTest {
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("上门提货时必须填写发货信息");
|
||||
|
||||
verify(repository, never()).saveOrder(any());
|
||||
verify(repository, never()).saveOrder(anyLong(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void updatingAnotherCustomersOrderStopsBeforePersistence() {
|
||||
OrderRepository repository = mock(OrderRepository.class);
|
||||
|
||||
assertThatThrownBy(() -> service(repository, mock(OrderEligibilityGuard.class),
|
||||
mock(OrderMasterDataGuard.class)).updateDraft(20002L, 2L, 1L, request()))
|
||||
.isInstanceOf(BizException.class)
|
||||
.extracting(exception -> ((BizException) exception).getResultCode())
|
||||
.isEqualTo(ResultCode.NOT_FOUND);
|
||||
|
||||
verify(repository).findOwned(20002L, 1L);
|
||||
verify(repository, never()).saveOrder(anyLong(), any());
|
||||
}
|
||||
|
||||
private OrderApplicationService service(OrderRepository repository, OrderEligibilityGuard eligibility,
|
||||
@@ -215,11 +229,11 @@ class OrderApplicationServiceStateTest {
|
||||
}
|
||||
|
||||
private OrderMasterDataGuard.Snapshots snapshots() {
|
||||
return snapshots(new PartnerClient.Partner(7L, 10001L, "客户", "ACTIVE", "SHIPPER", List.of("SHIPPER")));
|
||||
return snapshots(new PartnerClient.Partner(7L, 0L, "客户", "ACTIVE", "SHIPPER", List.of("SHIPPER")));
|
||||
}
|
||||
|
||||
private OrderMasterDataGuard.Snapshots snapshots(PartnerClient.Partner partner) {
|
||||
var warehouse = new WarehouseClient.Warehouse(8L, 10001L, "CN-SZX-01", "收货仓", "DOMESTIC", "CN", "ACTIVE");
|
||||
var warehouse = new WarehouseClient.Warehouse(8L, 0L, "CN-SZX-01", "收货仓", "DOMESTIC", "CN", "ACTIVE");
|
||||
var address = new WarehouseClient.Address("CN", "广东", "深圳", "南山", "518000", "测试路1号", null);
|
||||
var capability = new WarehouseClient.Capability("RECEIVING", true, LocalTime.of(16, 0));
|
||||
var receiving = new WarehouseClient.Detail(warehouse, List.of(address), List.of(capability));
|
||||
|
||||
@@ -118,7 +118,7 @@ class OrderIdempotencyServiceTest {
|
||||
service(repository).submit(10001L, 20001L, "operator", 998L, "submit-key");
|
||||
|
||||
verify(repository, never()).findOwned(anyLong(), anyLong());
|
||||
verify(repository, never()).saveOrder(any());
|
||||
verify(repository, never()).saveOrder(anyLong(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.baiyue.order.service;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.order.client.FirstlegInboundClient;
|
||||
import com.baiyue.order.entity.OutboxEventEntity;
|
||||
import com.baiyue.order.repository.OrderRepository;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class OrderInboundOutboxPublisherTest {
|
||||
@Test
|
||||
void matchingApprovedEventIsDeliveredAndPublished() {
|
||||
Fixture fixture = fixture(event("FIRST_LEG_ORDER_APPROVED", 11L, 21L, payload(11L, 21L)));
|
||||
when(fixture.client.create(eq("internal-token"), any())).thenReturn(ApiResponse.success(99L));
|
||||
|
||||
fixture.publisher.publishApprovedOrders();
|
||||
|
||||
verify(fixture.client).create(eq("internal-token"), any());
|
||||
verify(fixture.repository).markOutboxPublished(eq(11L), eq(41L), any(LocalDateTime.class));
|
||||
verify(fixture.repository, never()).markOutboxRetry(anyLong(), anyLong(), any());
|
||||
verify(fixture.repository, never()).markOutboxFailed(any(), anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void mismatchedCustomerIsPermanentlyFailedBeforeDelivery() {
|
||||
Fixture fixture = fixture(event("FIRST_LEG_ORDER_APPROVED", 11L, 21L, payload(12L, 21L)));
|
||||
|
||||
fixture.publisher.publishApprovedOrders();
|
||||
|
||||
verifyNoInteractions(fixture.client);
|
||||
verify(fixture.repository).markOutboxFailed(11L, 41L);
|
||||
verify(fixture.repository, never()).markOutboxRetry(anyLong(), anyLong(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void mismatchedAggregateIsPermanentlyFailedBeforeDelivery() {
|
||||
Fixture fixture = fixture(event("FIRST_LEG_ORDER_APPROVED", 11L, 21L, payload(11L, 22L)));
|
||||
|
||||
fixture.publisher.publishApprovedOrders();
|
||||
|
||||
verifyNoInteractions(fixture.client);
|
||||
verify(fixture.repository).markOutboxFailed(11L, 41L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void unsupportedOrderEventReachesFailedTerminalState() {
|
||||
Fixture fixture = fixture(event("FIRST_LEG_ORDER_CANCELLED", 11L, 21L, payload(11L, 21L)));
|
||||
|
||||
fixture.publisher.publishApprovedOrders();
|
||||
|
||||
verifyNoInteractions(fixture.client);
|
||||
verify(fixture.repository).markOutboxFailed(11L, 41L);
|
||||
verify(fixture.repository, never()).markOutboxRetry(anyLong(), anyLong(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void unknownOrderEventReachesFailedTerminalState() {
|
||||
Fixture fixture = fixture(event("FIRST_LEG_ORDER_UNKNOWN", 11L, 21L, payload(11L, 21L)));
|
||||
|
||||
fixture.publisher.publishApprovedOrders();
|
||||
|
||||
verifyNoInteractions(fixture.client);
|
||||
verify(fixture.repository).markOutboxFailed(11L, 41L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void transientFirstlegFailureRemainsPendingForRetry() {
|
||||
Fixture fixture = fixture(event("FIRST_LEG_ORDER_APPROVED", 11L, 21L, payload(11L, 21L)));
|
||||
when(fixture.client.create(eq("internal-token"), any()))
|
||||
.thenThrow(new IllegalStateException("temporary failure"));
|
||||
|
||||
fixture.publisher.publishApprovedOrders();
|
||||
|
||||
verify(fixture.repository).markOutboxRetry(eq(11L), eq(41L), any(LocalDateTime.class));
|
||||
verify(fixture.repository, never()).markOutboxPublished(anyLong(), anyLong(), any());
|
||||
verify(fixture.repository, never()).markOutboxFailed(any(), anyLong());
|
||||
}
|
||||
|
||||
private Fixture fixture(OutboxEventEntity event) {
|
||||
OrderRepository repository = mock(OrderRepository.class);
|
||||
FirstlegInboundClient client = mock(FirstlegInboundClient.class);
|
||||
when(repository.pendingInboundEvents(50)).thenReturn(List.of(event));
|
||||
OrderInboundOutboxPublisher publisher = new OrderInboundOutboxPublisher();
|
||||
ReflectionTestUtils.setField(publisher, "repository", repository);
|
||||
ReflectionTestUtils.setField(publisher, "client", client);
|
||||
ReflectionTestUtils.setField(publisher, "json", new ObjectMapper());
|
||||
ReflectionTestUtils.setField(publisher, "internalToken", "internal-token");
|
||||
return new Fixture(publisher, repository, client);
|
||||
}
|
||||
|
||||
private OutboxEventEntity event(String eventType, Long tenantId, Long aggregateId, String payload) {
|
||||
OutboxEventEntity event = new OutboxEventEntity();
|
||||
event.setId(41L);
|
||||
event.setTenantId(tenantId);
|
||||
event.setAggregateType("FIRST_LEG_ORDER");
|
||||
event.setAggregateId(aggregateId);
|
||||
event.setEventType(eventType);
|
||||
event.setPayloadJson(payload);
|
||||
event.setPublishStatus("PENDING");
|
||||
return event;
|
||||
}
|
||||
|
||||
private String payload(long tenantId, long orderId) {
|
||||
return "{\"tenantId\":" + tenantId + ",\"orderId\":" + orderId + "}";
|
||||
}
|
||||
|
||||
private record Fixture(OrderInboundOutboxPublisher publisher, OrderRepository repository,
|
||||
FirstlegInboundClient client) {
|
||||
}
|
||||
}
|
||||
@@ -30,29 +30,29 @@ class OrderMasterDataGuardTest {
|
||||
@MockBean private WarehouseClient warehouses;
|
||||
@MockBean private PartnerClient partners;
|
||||
@Resource private OrderMasterDataGuard guard;
|
||||
private static final long PUBLIC_TENANT_ID = 10001L;
|
||||
private static final long PUBLIC_TENANT_ID = 0L;
|
||||
|
||||
@Test
|
||||
void validMasterDataReturnsTrustedServerSnapshot() {
|
||||
var partner=new PartnerClient.Partner(7L,10001L,"服务端客户名","ACTIVE","SHIPPER",List.of("SHIPPER"));
|
||||
var partner=new PartnerClient.Partner(7L,PUBLIC_TENANT_ID,"服务端客户名","ACTIVE","SHIPPER",List.of("SHIPPER"));
|
||||
var address=new WarehouseClient.Address("CN","广东省","深圳市","宝安区","518000","示例路1号",null);
|
||||
var capability=new WarehouseClient.Capability("RECEIVING",true,java.time.LocalTime.of(16,0));
|
||||
var wh=new WarehouseClient.Detail(new WarehouseClient.Warehouse(8L,10001L,"CN-SZX-01","深圳仓","DOMESTIC","CN","ACTIVE"),List.of(address),List.of(capability));
|
||||
when(partners.active("token",10001,7)).thenReturn(ApiResponse.success(partner));
|
||||
when(warehouses.active("token",10001,8)).thenReturn(ApiResponse.success(wh));
|
||||
var wh=new WarehouseClient.Detail(new WarehouseClient.Warehouse(8L,PUBLIC_TENANT_ID,"CN-SZX-01","深圳仓","DOMESTIC","CN","ACTIVE"),List.of(address),List.of(capability));
|
||||
when(partners.active("token",PUBLIC_TENANT_ID,7)).thenReturn(ApiResponse.success(partner));
|
||||
when(warehouses.active("token",PUBLIC_TENANT_ID,8)).thenReturn(ApiResponse.success(wh));
|
||||
var result=guard.validate(10001,request(7L,8L));
|
||||
assertThat(result.partner().partnerName()).isEqualTo("服务端客户名");
|
||||
assertThat(result.receiving().warehouse().warehouseName()).isEqualTo("深圳仓");
|
||||
}
|
||||
@Test
|
||||
void inactiveOrCrossTenantMasterDataIsRejected() {
|
||||
when(partners.active("token",10002,7)).thenReturn(ApiResponse.success(null));
|
||||
when(partners.active("token",PUBLIC_TENANT_ID,7)).thenReturn(ApiResponse.success(null));
|
||||
assertThatThrownBy(() -> guard.validate(10002,request(7L,8L)))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("服务暂时不可用");
|
||||
}
|
||||
@Test
|
||||
void downstreamHttp200BusinessFailureKeepsBusinessCodeAndMessage() {
|
||||
when(partners.active("token",10001,7)).thenReturn(
|
||||
when(partners.active("token",PUBLIC_TENANT_ID,7)).thenReturn(
|
||||
ApiResponse.<PartnerClient.Partner>failure(ResultCode.NOT_FOUND,"往来单位不存在",null,null));
|
||||
|
||||
assertThatThrownBy(() -> guard.validate(10001,request(7L,8L)))
|
||||
@@ -64,11 +64,11 @@ class OrderMasterDataGuardTest {
|
||||
}
|
||||
@Test
|
||||
void masterDataResponseWithNullElementReturnsControlledBusinessError() {
|
||||
var partner=new PartnerClient.Partner(7L,10001L,"服务端客户名","ACTIVE","SHIPPER",List.of("SHIPPER"));
|
||||
var wh=new WarehouseClient.Detail(new WarehouseClient.Warehouse(8L,10001L,"CN-SZX-01","深圳仓","DOMESTIC","CN","ACTIVE"),
|
||||
var partner=new PartnerClient.Partner(7L,PUBLIC_TENANT_ID,"服务端客户名","ACTIVE","SHIPPER",List.of("SHIPPER"));
|
||||
var wh=new WarehouseClient.Detail(new WarehouseClient.Warehouse(8L,PUBLIC_TENANT_ID,"CN-SZX-01","深圳仓","DOMESTIC","CN","ACTIVE"),
|
||||
Collections.singletonList(null), Collections.singletonList(null));
|
||||
when(partners.active("token",10001,7)).thenReturn(ApiResponse.success(partner));
|
||||
when(warehouses.active("token",10001,8)).thenReturn(ApiResponse.success(wh));
|
||||
when(partners.active("token",PUBLIC_TENANT_ID,7)).thenReturn(ApiResponse.success(partner));
|
||||
when(warehouses.active("token",PUBLIC_TENANT_ID,8)).thenReturn(ApiResponse.success(wh));
|
||||
|
||||
assertThatThrownBy(() -> guard.validate(10001,request(7L,8L)))
|
||||
.isInstanceOf(BizException.class)
|
||||
@@ -77,7 +77,7 @@ class OrderMasterDataGuardTest {
|
||||
}
|
||||
@Test
|
||||
void masterDataResponseMissingSnapshotNameIsServiceUnavailable() {
|
||||
when(partners.active("token",10001,7)).thenReturn(ApiResponse.success(new PartnerClient.Partner(7L,10001L,null,"ACTIVE","SHIPPER",List.of("SHIPPER"))));
|
||||
when(partners.active("token",PUBLIC_TENANT_ID,7)).thenReturn(ApiResponse.success(new PartnerClient.Partner(7L,PUBLIC_TENANT_ID,null,"ACTIVE","SHIPPER",List.of("SHIPPER"))));
|
||||
|
||||
assertThatThrownBy(() -> guard.validate(10001,request(7L,8L)))
|
||||
.isInstanceOf(BizException.class)
|
||||
@@ -87,7 +87,7 @@ class OrderMasterDataGuardTest {
|
||||
}
|
||||
@Test
|
||||
void downstreamNonFeignRuntimeExceptionMapsToServiceUnavailable() {
|
||||
when(partners.active("token",10001,7)).thenThrow(new IllegalStateException("解码失败"));
|
||||
when(partners.active("token",PUBLIC_TENANT_ID,7)).thenThrow(new IllegalStateException("解码失败"));
|
||||
|
||||
assertThatThrownBy(() -> guard.validate(10001,request(7L,8L)))
|
||||
.isInstanceOf(BizException.class)
|
||||
@@ -96,7 +96,7 @@ class OrderMasterDataGuardTest {
|
||||
}
|
||||
@Test
|
||||
void downstreamForbiddenMapsToServiceUnavailable() {
|
||||
when(partners.active("token",10001,7)).thenThrow(feign(403));
|
||||
when(partners.active("token",PUBLIC_TENANT_ID,7)).thenThrow(feign(403));
|
||||
|
||||
assertThatThrownBy(() -> guard.validate(10001,request(7L,8L)))
|
||||
.isInstanceOf(BizException.class)
|
||||
@@ -104,18 +104,18 @@ class OrderMasterDataGuardTest {
|
||||
.isEqualTo(ResultCode.SERVICE_UNAVAILABLE);
|
||||
}
|
||||
@Test
|
||||
void downstreamHttpNotFoundReturnsUnboundCustomerMessage() {
|
||||
when(partners.active("token",10001,7)).thenThrow(feign(404));
|
||||
void downstreamHttpNotFoundReturnsPlatformPartnerUnavailableMessage() {
|
||||
when(partners.active("token",PUBLIC_TENANT_ID,7)).thenThrow(feign(404));
|
||||
|
||||
assertThatThrownBy(() -> guard.validate(10001,request(7L,8L)))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessage("\u672a\u7ed1\u5b9a\u53ef\u7528\u5f80\u6765\u5ba2\u6237")
|
||||
.hasMessage("平台往来单位不存在或不可用")
|
||||
.extracting(exception -> ((BizException) exception).getResultCode())
|
||||
.isEqualTo(ResultCode.NOT_FOUND);
|
||||
}
|
||||
@Test
|
||||
void wrappedDownstreamHttpConflictMapsToServiceUnavailable() {
|
||||
when(partners.active("token",10001,7)).thenThrow(new IllegalStateException("wrapped", feign(409)));
|
||||
when(partners.active("token",PUBLIC_TENANT_ID,7)).thenThrow(new IllegalStateException("wrapped", feign(409)));
|
||||
|
||||
assertThatThrownBy(() -> guard.validate(10001,request(7L,8L)))
|
||||
.isInstanceOf(BizException.class)
|
||||
@@ -124,7 +124,7 @@ class OrderMasterDataGuardTest {
|
||||
}
|
||||
@Test
|
||||
void downstreamServerErrorMapsToServiceUnavailable() {
|
||||
when(partners.active("token",10001,7)).thenThrow(feign(500));
|
||||
when(partners.active("token",PUBLIC_TENANT_ID,7)).thenThrow(feign(500));
|
||||
|
||||
assertThatThrownBy(() -> guard.validate(10001,request(7L,8L)))
|
||||
.isInstanceOf(BizException.class)
|
||||
@@ -133,8 +133,8 @@ class OrderMasterDataGuardTest {
|
||||
}
|
||||
@Test
|
||||
void inactiveMasterDataInSuccessfulResponseMapsToConflict() {
|
||||
when(partners.active("token",10001,7))
|
||||
.thenReturn(ApiResponse.success(new PartnerClient.Partner(7L,10001L,"客户","INACTIVE","SHIPPER",List.of("SHIPPER"))));
|
||||
when(partners.active("token",PUBLIC_TENANT_ID,7))
|
||||
.thenReturn(ApiResponse.success(new PartnerClient.Partner(7L,PUBLIC_TENANT_ID,"客户","INACTIVE","SHIPPER",List.of("SHIPPER"))));
|
||||
|
||||
assertThatThrownBy(() -> guard.validate(10001,request(7L,8L)))
|
||||
.isInstanceOf(BizException.class)
|
||||
@@ -143,8 +143,8 @@ class OrderMasterDataGuardTest {
|
||||
}
|
||||
@Test
|
||||
void partnerWithoutShipperTypeIsRejected() {
|
||||
when(partners.active("token",10001,7)).thenReturn(ApiResponse.success(
|
||||
new PartnerClient.Partner(7L,10001L,"收货单位","ACTIVE","CONSIGNEE",List.of("CONSIGNEE"))));
|
||||
when(partners.active("token",PUBLIC_TENANT_ID,7)).thenReturn(ApiResponse.success(
|
||||
new PartnerClient.Partner(7L,PUBLIC_TENANT_ID,"收货单位","ACTIVE","CONSIGNEE",List.of("CONSIGNEE"))));
|
||||
|
||||
assertThatThrownBy(() -> guard.validate(10001,request(7L,8L)))
|
||||
.isInstanceOf(BizException.class)
|
||||
@@ -154,8 +154,8 @@ class OrderMasterDataGuardTest {
|
||||
}
|
||||
@Test
|
||||
void customerTenantCanUseDomesticWarehouseFromPublicTenant() {
|
||||
when(partners.active("token",20002,7)).thenReturn(ApiResponse.success(
|
||||
new PartnerClient.Partner(7L,20002L,"客户","ACTIVE","SHIPPER",List.of("SHIPPER"))));
|
||||
when(partners.active("token",PUBLIC_TENANT_ID,7)).thenReturn(ApiResponse.success(
|
||||
new PartnerClient.Partner(7L,PUBLIC_TENANT_ID,"客户","ACTIVE","SHIPPER",List.of("SHIPPER"))));
|
||||
when(warehouses.active("token",PUBLIC_TENANT_ID,8)).thenReturn(ApiResponse.success(
|
||||
warehouse(8L,"DOMESTIC",true)));
|
||||
|
||||
@@ -164,6 +164,19 @@ class OrderMasterDataGuardTest {
|
||||
verify(warehouses).active("token",PUBLIC_TENANT_ID,8);
|
||||
}
|
||||
@Test
|
||||
void customersAAndBUseTheSamePlatformWarehouseAndPartner() {
|
||||
when(partners.active("token",PUBLIC_TENANT_ID,7)).thenReturn(ApiResponse.success(
|
||||
new PartnerClient.Partner(7L,PUBLIC_TENANT_ID,"平台共享单位","ACTIVE","SHIPPER",List.of("SHIPPER"))));
|
||||
when(warehouses.active("token",PUBLIC_TENANT_ID,8)).thenReturn(ApiResponse.success(
|
||||
warehouse(8L,"DOMESTIC",true)));
|
||||
|
||||
guard.validate(20002,request(7L,8L));
|
||||
guard.validate(30003,request(7L,8L));
|
||||
|
||||
verify(partners,times(2)).active("token",PUBLIC_TENANT_ID,7);
|
||||
verify(warehouses,times(2)).active("token",PUBLIC_TENANT_ID,8);
|
||||
}
|
||||
@Test
|
||||
void orderWithoutPartnerUsesPublicWarehouseWithoutPartnerLookup() {
|
||||
when(warehouses.active("token",PUBLIC_TENANT_ID,8)).thenReturn(ApiResponse.success(
|
||||
warehouse(8L,"DOMESTIC",true)));
|
||||
@@ -177,8 +190,8 @@ class OrderMasterDataGuardTest {
|
||||
}
|
||||
@Test
|
||||
void fbaDestinationMustReferenceMatchingPublicWarehouse() {
|
||||
when(partners.active("token",20002,7)).thenReturn(ApiResponse.success(
|
||||
new PartnerClient.Partner(7L,20002L,"客户","ACTIVE","SHIPPER",List.of("SHIPPER"))));
|
||||
when(partners.active("token",PUBLIC_TENANT_ID,7)).thenReturn(ApiResponse.success(
|
||||
new PartnerClient.Partner(7L,PUBLIC_TENANT_ID,"客户","ACTIVE","SHIPPER",List.of("SHIPPER"))));
|
||||
when(warehouses.active("token",PUBLIC_TENANT_ID,8)).thenReturn(ApiResponse.success(
|
||||
warehouse(8L,"DOMESTIC",true)));
|
||||
when(warehouses.active("token",PUBLIC_TENANT_ID,9)).thenReturn(ApiResponse.success(
|
||||
@@ -191,8 +204,8 @@ class OrderMasterDataGuardTest {
|
||||
}
|
||||
@Test
|
||||
void privateDestinationCannotReferencePublicWarehouse() {
|
||||
when(partners.active("token",20002,7)).thenReturn(ApiResponse.success(
|
||||
new PartnerClient.Partner(7L,20002L,"客户","ACTIVE","SHIPPER",List.of("SHIPPER"))));
|
||||
when(partners.active("token",PUBLIC_TENANT_ID,7)).thenReturn(ApiResponse.success(
|
||||
new PartnerClient.Partner(7L,PUBLIC_TENANT_ID,"客户","ACTIVE","SHIPPER",List.of("SHIPPER"))));
|
||||
when(warehouses.active("token",PUBLIC_TENANT_ID,8)).thenReturn(ApiResponse.success(
|
||||
warehouse(8L,"DOMESTIC",true)));
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.baiyue.order.service;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
class OrderScopeGuardTest {
|
||||
@Test
|
||||
void merchantRequiresPositiveCustomerIsolationKey() {
|
||||
assertThat(OrderScopeGuard.merchant(user("MERCHANT", 10001L))).isEqualTo(10001L);
|
||||
assertThatThrownBy(() -> OrderScopeGuard.merchant(user("MERCHANT", 0L)))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("Merchant tenant is required");
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformRequiresZeroTenantInToken() {
|
||||
OrderScopeGuard.platform(user("PLATFORM", 0L));
|
||||
|
||||
assertThatThrownBy(() -> OrderScopeGuard.platform(user("PLATFORM", null)))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("Platform tenant must be 0");
|
||||
assertThatThrownBy(() -> OrderScopeGuard.platform(user("PLATFORM", 10001L)))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("Platform tenant must be 0");
|
||||
}
|
||||
|
||||
@Test
|
||||
void validPlatformCanSelectPositiveCustomer() {
|
||||
assertThat(OrderScopeGuard.platform(user("PLATFORM", 0L), 20002L)).isEqualTo(20002L);
|
||||
}
|
||||
|
||||
private AuthenticatedUser user(String appCode, Long tenantId) {
|
||||
return new AuthenticatedUser(1L, tenantId, null, null, "operator", appCode, "WEB", Set.of());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user