feat(order): add batch review preview

This commit is contained in:
2026-09-08 17:32:56 +08:00
parent 442518c382
commit eead11e99d
11 changed files with 414 additions and 10 deletions
@@ -0,0 +1,30 @@
package com.baiyue.order.controller;
import org.junit.jupiter.api.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
class BatchReviewOpenApiContractTest {
@Test
void documentsSingleRequestPreviewFieldsAndVersionedPartialApproval() throws Exception {
String yaml = Files.readString(Path.of("src/main/resources/openapi/order-v1.yaml"));
assertThat(yaml)
.contains("/api/platform/v1/orders/first-leg/batch-review-preview:")
.contains("/api/platform/v1/orders/first-leg/batch-approve:")
.contains("inboundCode:")
.contains("customerName:")
.contains("chineseGoodsNames:")
.contains("channelCode:")
.contains("channelName:")
.contains("destinationWarehouseCode:")
.contains("destinationWarehouseName:")
.contains("orderVersion:")
.contains("canApprove:")
.contains("blockReason:")
.contains("允许部分成功");
}
}
@@ -4,10 +4,12 @@ import com.baiyue.common.core.exception.BizException;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.order.dto.BatchOrderReviewResponse;
import com.baiyue.order.dto.BatchOrderReviewPreviewResponse;
import com.baiyue.order.dto.OrderRequests;
import com.baiyue.order.entity.OrderStatus;
import com.baiyue.order.service.OrderApplicationService;
import com.baiyue.order.service.InboundCodeAssociationService;
import com.baiyue.order.service.BatchOrderReviewPreviewService;
import org.junit.jupiter.api.Test;
import jakarta.annotation.Resource;
import org.springframework.boot.test.mock.mockito.MockBean;
@@ -23,11 +25,13 @@ import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
@SpringJUnitConfig(PlatformFirstLegOrderController.class)
class PlatformOrderPageControllerTest {
@MockBean private OrderApplicationService service;
@MockBean private InboundCodeAssociationService inboundCodeAssociations;
@MockBean private BatchOrderReviewPreviewService batchReviewPreview;
@Resource private PlatformFirstLegOrderController controller;
private final OrderRequests.QueryRequest query = new OrderRequests.QueryRequest(
null, null, null, null, null, null, 1, 20);
@@ -77,10 +81,13 @@ class PlatformOrderPageControllerTest {
@Test
void batchApproveReturnsPartialFailureAndContinuesRemainingOrders() {
doThrow(new BizException(ResultCode.CONFLICT, "订单已审核"))
.when(service).transit(10001L, 1L, "operator", 102L, OrderStatus.APPROVED, null);
.when(service).transit(10001L, 1L, "operator", 102L, OrderStatus.APPROVED, null, 4);
var response = controller.batchApprove(user("PLATFORM", 0L), 10001L,
new OrderRequests.BatchReviewRequest(List.of(101L, 102L, 103L)));
new OrderRequests.BatchReviewRequest(List.of(
new OrderRequests.BatchReviewItem(101L, 3),
new OrderRequests.BatchReviewItem(102L, 4),
new OrderRequests.BatchReviewItem(103L, 5))));
BatchOrderReviewResponse result = response.data();
assertThat(result.total()).isEqualTo(3);
@@ -90,8 +97,8 @@ class PlatformOrderPageControllerTest {
assertThat(failure.code()).isEqualTo(ResultCode.CONFLICT.code());
assertThat(failure.message()).isEqualTo("订单已审核");
});
verify(service).transit(10001L, 1L, "operator", 101L, OrderStatus.APPROVED, null);
verify(service).transit(10001L, 1L, "operator", 103L, OrderStatus.APPROVED, null);
verify(service).transit(10001L, 1L, "operator", 101L, OrderStatus.APPROVED, null, 3);
verify(service).transit(10001L, 1L, "operator", 103L, OrderStatus.APPROVED, null, 5);
}
@Test
@@ -103,6 +110,20 @@ class PlatformOrderPageControllerTest {
verify(service).transit(10001L, 1L, "operator", 102L, OrderStatus.REJECTED, "资料不完整");
}
@Test
void batchReviewPreviewUsesOneControllerCallAndExactCustomerScope() {
var request = new OrderRequests.BatchReviewRequest(List.of(
new OrderRequests.BatchReviewItem(101L, 3),
new OrderRequests.BatchReviewItem(102L, 4)));
var preview = new BatchOrderReviewPreviewResponse(2, 1, List.of());
when(batchReviewPreview.preview(10001L, request.items())).thenReturn(preview);
var response = controller.batchReviewPreview(user("PLATFORM", 0L), 10001L, request);
assertThat(response.data()).isSameAs(preview);
verify(batchReviewPreview).preview(10001L, request.items());
}
private AuthenticatedUser user(String appCode, Long tenantId) {
return new AuthenticatedUser(1L, tenantId, null, null, "operator", appCode, "WEB", Set.of());
}
@@ -12,6 +12,20 @@ import static org.assertj.core.api.Assertions.assertThat;
class OrderRequestsValidationTest {
private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
@Test
void batchReviewRequiresDistinctOrderIdsAndNonNegativeVersions() {
var duplicate = new OrderRequests.BatchReviewRequest(List.of(
new OrderRequests.BatchReviewItem(101L, 1),
new OrderRequests.BatchReviewItem(101L, 2)));
var invalidVersion = new OrderRequests.BatchReviewRequest(List.of(
new OrderRequests.BatchReviewItem(102L, -1)));
assertThat(validator.validate(duplicate))
.anyMatch(violation -> violation.getMessage().contains("订单 ID 不能重复"));
assertThat(validator.validate(invalidVersion))
.anyMatch(violation -> violation.getPropertyPath().toString().contains("orderVersion"));
}
@Test
void carrierSnapshotIsOptionalWhenCustomerCreatesAnOrderDraft() {
assertThat(validator.validate(request(null, null, "TAX_INCLUDED"))).isEmpty();
@@ -0,0 +1,93 @@
package com.baiyue.order.service;
import com.baiyue.order.dto.OrderRequests;
import com.baiyue.order.entity.OrderCargoEntity;
import com.baiyue.order.entity.OrderEntity;
import com.baiyue.order.entity.OrderStatus;
import com.baiyue.order.repository.OrderRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
class BatchOrderReviewPreviewServiceTest {
private final OrderRepository orders = mock(OrderRepository.class);
private final BatchOrderReviewPreviewService service = new BatchOrderReviewPreviewService();
@BeforeEach
void setUp() {
org.springframework.test.util.ReflectionTestUtils.setField(service, "orders", orders);
}
@Test
void returnsStableRowsWithOrderedDistinctChineseGoodsUsingTwoBatchQueries() {
OrderEntity eligible = order(101L, 3, OrderStatus.SUBMITTED, "RK-101", "客户甲");
OrderEntity stale = order(102L, 8, OrderStatus.SUBMITTED, "RK-102", "客户乙");
when(orders.findByIds(11L, List.of(101L, 102L))).thenReturn(List.of(stale, eligible));
when(orders.findCargoByOrderIds(11L, List.of(101L, 102L))).thenReturn(List.of(
cargo(101L, " 蓝牙音箱 "), cargo(101L, "蓝牙音箱"), cargo(101L, "充电器"),
cargo(102L, "数据线")));
var result = service.preview(11L, List.of(
new OrderRequests.BatchReviewItem(101L, 3),
new OrderRequests.BatchReviewItem(102L, 7)));
assertThat(result.total()).isEqualTo(2);
assertThat(result.approvableCount()).isEqualTo(1);
assertThat(result.items()).extracting(item -> item.orderId()).containsExactly(101L, 102L);
assertThat(result.items().get(0).chineseGoodsNames()).containsExactly("蓝牙音箱", "充电器");
assertThat(result.items().get(0).destinationWarehouseCode()).isEqualTo("ONT8");
assertThat(result.items().get(0).canApprove()).isTrue();
assertThat(result.items().get(1).orderVersion()).isEqualTo(8);
assertThat(result.items().get(1).blockReason()).contains("版本已变化");
verify(orders).findByIds(11L, List.of(101L, 102L));
verify(orders).findCargoByOrderIds(11L, List.of(101L, 102L));
verifyNoMoreInteractions(orders);
}
@Test
void keepsUnknownAndWrongStatusRowsWithoutLeakingAnotherCustomer() {
OrderEntity approved = order(101L, 3, OrderStatus.APPROVED, "RK-101", "客户甲");
when(orders.findByIds(11L, List.of(999L, 101L))).thenReturn(List.of(approved));
when(orders.findCargoByOrderIds(11L, List.of(999L, 101L))).thenReturn(List.of());
var result = service.preview(11L, List.of(
new OrderRequests.BatchReviewItem(999L, 1),
new OrderRequests.BatchReviewItem(101L, 3)));
assertThat(result.items().get(0).inboundCode()).isNull();
assertThat(result.items().get(0).blockReason()).contains("不存在或不属于当前客户");
assertThat(result.items().get(1).blockReason()).contains("状态不可审核");
assertThat(result.approvableCount()).isZero();
}
private static OrderEntity order(long id, int version, OrderStatus status,
String inboundCode, String customerName) {
OrderEntity order = new OrderEntity();
order.setId(id);
order.setTenantId(11L);
order.setVersion(version);
order.setStatus(status);
order.setInboundCode(inboundCode);
order.setCustomerId(11L);
order.setCustomerName(customerName);
order.setChannelCode("FBA-SEA");
order.setChannelName("美森海运");
order.setAmazonWarehouseCode("ONT8");
order.setDestinationWarehouseName("ONT8 亚马逊仓");
return order;
}
private static OrderCargoEntity cargo(long orderId, String name) {
OrderCargoEntity cargo = new OrderCargoEntity();
cargo.setOrderId(orderId);
cargo.setCargoName(name);
return cargo;
}
}
@@ -235,6 +235,24 @@ class OrderApplicationServiceStateTest {
&& event.getPayloadJson().contains("\"totalPackageCount\":4")));
}
@Test
void batchApprovalRejectsStalePreviewVersionBeforeChangingOrder() {
OrderRepository repository = mock(OrderRepository.class);
OrderEntity order = persistedOrder(OrderStatus.SUBMITTED);
order.setVersion(6);
when(repository.findOwned(10001L, 1L)).thenReturn(order);
assertThatThrownBy(() -> service(repository, mock(OrderEligibilityGuard.class),
mock(OrderMasterDataGuard.class)).transit(
10001L, 2L, "u", 1L, OrderStatus.APPROVED, null, 5))
.isInstanceOf(BizException.class)
.hasMessage("订单版本已变化,请刷新后重试");
assertThat(order.getStatus()).isEqualTo(OrderStatus.SUBMITTED);
verify(repository, never()).saveOrder(anyLong(), any());
verify(repository, never()).insertOutbox(any());
}
@Test
void submittingDraftGeneratesAndPersistsInboundCode() {
OrderRepository repository = mock(OrderRepository.class);