From 77dc32a69a6aaa68c680734c14471ac9078b01cb Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Fri, 7 Aug 2026 14:52:21 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AE=A2=E5=8D=95=E6=89=B9=E9=87=8F=E5=AE=A1?= =?UTF-8?q?=E6=A0=B8=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PlatformFirstLegOrderController.java | 45 +++++++++++++++++++ .../order/dto/BatchOrderReviewResponse.java | 23 ++++++++++ .../com/baiyue/order/dto/OrderRequests.java | 24 ++++++++++ .../OrderControllerPermissionTest.java | 4 ++ .../PlatformOrderPageControllerTest.java | 35 +++++++++++++++ 5 files changed, 131 insertions(+) create mode 100644 order/src/main/java/com/baiyue/order/dto/BatchOrderReviewResponse.java diff --git a/order/src/main/java/com/baiyue/order/controller/PlatformFirstLegOrderController.java b/order/src/main/java/com/baiyue/order/controller/PlatformFirstLegOrderController.java index 6a7b85119..678c06aaa 100644 --- a/order/src/main/java/com/baiyue/order/controller/PlatformFirstLegOrderController.java +++ b/order/src/main/java/com/baiyue/order/controller/PlatformFirstLegOrderController.java @@ -3,7 +3,9 @@ package com.baiyue.order.controller; import com.baiyue.common.audit.BusinessAudit; import com.baiyue.common.core.api.ApiResponse; import com.baiyue.common.core.api.PageResult; +import com.baiyue.common.core.exception.BizException; import com.baiyue.common.security.AuthenticatedUser; +import com.baiyue.order.dto.BatchOrderReviewResponse; import com.baiyue.order.dto.OrderDetailResponse; import com.baiyue.order.dto.OrderRequests; import com.baiyue.order.dto.TenantOrderCountResponse; @@ -28,6 +30,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.PutMapping; +import java.util.ArrayList; import java.util.List; /** @@ -155,6 +158,18 @@ public class PlatformFirstLegOrderController { return transit(user, tenantId, id, OrderStatus.APPROVED, null); } + @BusinessAudit(event = "ORDER_BATCH_APPROVE", resourceType = "FIRST_LEG_ORDER_BATCH", + resourceId = "#request.orderIds", tenantId = "#tenantId", description = "批量审核通过订单") + @Operation(summary = "批量审核通过订单", description = "单租户模式下逐单独立审核,返回成功和失败明细。") + @PostMapping("/batch-approve") + @PreAuthorize("hasAuthority('platform:order:approve')") + public ApiResponse batchApprove( + @AuthenticationPrincipal AuthenticatedUser user, + @RequestParam long tenantId, + @Valid @RequestBody OrderRequests.BatchReviewRequest request) { + return batchTransit(user, tenantId, request.orderIds(), OrderStatus.APPROVED, null); + } + @BusinessAudit(event = "ORDER_ASSIGN_CARRIER", resourceType = "FIRST_LEG_ORDER", resourceId = "#id", description = "指派订单承运商") @Operation(summary = "指派承运商", description = "可选地为待审核订单选择启用的公共承运商,名称快照由服务端主数据回填;该动作不是审核前置条件。") @PutMapping("/{id}/carrier") @@ -179,6 +194,18 @@ public class PlatformFirstLegOrderController { return transit(user, tenantId, id, OrderStatus.REJECTED, request.reason()); } + @BusinessAudit(event = "ORDER_BATCH_REJECT", resourceType = "FIRST_LEG_ORDER_BATCH", + resourceId = "#request.orderIds", tenantId = "#tenantId", description = "批量驳回订单") + @Operation(summary = "批量驳回订单", description = "单租户模式下使用统一原因逐单独立驳回,返回成功和失败明细。") + @PostMapping("/batch-reject") + @PreAuthorize("hasAuthority('platform:order:reject')") + public ApiResponse batchReject( + @AuthenticationPrincipal AuthenticatedUser user, + @RequestParam long tenantId, + @Valid @RequestBody OrderRequests.BatchRejectRequest request) { + return batchTransit(user, tenantId, request.orderIds(), OrderStatus.REJECTED, request.reason()); + } + @BusinessAudit(event = "ORDER_ACCEPT", resourceType = "FIRST_LEG_ORDER", resourceId = "#id", description = "Accept order / 受理订单") @Operation(summary = "Accept / 受理订单", description = "Accepts an approved order for fulfillment processing. / 受理已审核通过的订单,进入履约处理。") @PostMapping("/{id}/accept") @@ -255,4 +282,22 @@ public class PlatformFirstLegOrderController { ); return ApiResponse.success(null); } + + private ApiResponse batchTransit( + AuthenticatedUser user, long tenantId, List orderIds, OrderStatus target, String reason) { + long scopedTenantId = OrderScopeGuard.platform(user, tenantId); + List successOrderIds = new ArrayList<>(); + List failedOrders = new ArrayList<>(); + for (Long orderId : orderIds) { + try { + service.transit(scopedTenantId, user.userId(), user.username(), orderId, target, reason); + successOrderIds.add(orderId); + } catch (BizException exception) { + failedOrders.add(new BatchOrderReviewResponse.FailedOrder( + orderId, exception.getResultCode().code(), exception.getMessage())); + } + } + return ApiResponse.success(new BatchOrderReviewResponse(orderIds.size(), successOrderIds.size(), + failedOrders.size(), List.copyOf(successOrderIds), List.copyOf(failedOrders))); + } } diff --git a/order/src/main/java/com/baiyue/order/dto/BatchOrderReviewResponse.java b/order/src/main/java/com/baiyue/order/dto/BatchOrderReviewResponse.java new file mode 100644 index 000000000..981c6c034 --- /dev/null +++ b/order/src/main/java/com/baiyue/order/dto/BatchOrderReviewResponse.java @@ -0,0 +1,23 @@ +package com.baiyue.order.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.List; + +/** 单租户订单批量审核结果。 */ +@Schema(description = "订单批量审核结果") +public record BatchOrderReviewResponse( + @Schema(description = "请求订单总数") int total, + @Schema(description = "成功数量") int successCount, + @Schema(description = "失败数量") int failureCount, + @Schema(description = "成功订单 ID") List successOrderIds, + @Schema(description = "失败订单及原因") List failedOrders +) { + @Schema(description = "单笔失败结果") + public record FailedOrder( + @Schema(description = "订单 ID") long orderId, + @Schema(description = "业务错误码") String code, + @Schema(description = "失败原因") String message + ) { + } +} diff --git a/order/src/main/java/com/baiyue/order/dto/OrderRequests.java b/order/src/main/java/com/baiyue/order/dto/OrderRequests.java index ab1d9fc81..153fb394f 100644 --- a/order/src/main/java/com/baiyue/order/dto/OrderRequests.java +++ b/order/src/main/java/com/baiyue/order/dto/OrderRequests.java @@ -166,6 +166,30 @@ public final class OrderRequests { ) { } + @Schema(name = "FirstLegOrderBatchReviewRequest", description = "单租户订单批量审核请求") + public record BatchReviewRequest( + @Schema(description = "订单 ID 列表,单次最多 100 条", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty @Size(max = 100) List<@NotNull @Min(1) Long> orderIds + ) { + @AssertTrue(message = "订单 ID 不能重复") + public boolean hasDistinctOrderIds() { + return orderIds == null || orderIds.size() == orderIds.stream().distinct().count(); + } + } + + @Schema(name = "FirstLegOrderBatchRejectRequest", description = "单租户订单批量驳回请求") + public record BatchRejectRequest( + @Schema(description = "订单 ID 列表,单次最多 100 条", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty @Size(max = 100) List<@NotNull @Min(1) Long> orderIds, + @Schema(description = "统一驳回原因", maxLength = 500, requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank @Size(max = 500) String reason + ) { + @AssertTrue(message = "订单 ID 不能重复") + public boolean hasDistinctOrderIds() { + return orderIds == null || orderIds.size() == orderIds.stream().distinct().count(); + } + } + @Schema(name = "OrderInternalNoteRequest", description = "Platform internal order note / 平台内部订单备注") public record NoteRequest( @Schema(description = "Internal note content / 内部备注内容", example = "Customer confirmed the shipping schedule / 客户已确认船期", maxLength = 2000, requiredMode = Schema.RequiredMode.REQUIRED) diff --git a/order/src/test/java/com/baiyue/order/controller/OrderControllerPermissionTest.java b/order/src/test/java/com/baiyue/order/controller/OrderControllerPermissionTest.java index f1716f81d..2f9f6cbd2 100644 --- a/order/src/test/java/com/baiyue/order/controller/OrderControllerPermissionTest.java +++ b/order/src/test/java/com/baiyue/order/controller/OrderControllerPermissionTest.java @@ -43,6 +43,10 @@ class OrderControllerPermissionTest { AuthenticatedUser.class, long.class, long.class, String.class); assertAuthority(PlatformFirstLegOrderController.class, "assignCarrier", "hasAuthority('platform:order:approve')", AuthenticatedUser.class, long.class, long.class, OrderRequests.CarrierRequest.class); + assertAuthority(PlatformFirstLegOrderController.class, "batchApprove", "hasAuthority('platform:order:approve')", + AuthenticatedUser.class, long.class, OrderRequests.BatchReviewRequest.class); + assertAuthority(PlatformFirstLegOrderController.class, "batchReject", "hasAuthority('platform:order:reject')", + AuthenticatedUser.class, long.class, OrderRequests.BatchRejectRequest.class); } @Test diff --git a/order/src/test/java/com/baiyue/order/controller/PlatformOrderPageControllerTest.java b/order/src/test/java/com/baiyue/order/controller/PlatformOrderPageControllerTest.java index fe6b5970b..61e043825 100644 --- a/order/src/test/java/com/baiyue/order/controller/PlatformOrderPageControllerTest.java +++ b/order/src/test/java/com/baiyue/order/controller/PlatformOrderPageControllerTest.java @@ -1,8 +1,11 @@ package com.baiyue.order.controller; 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.OrderRequests; +import com.baiyue.order.entity.OrderStatus; import com.baiyue.order.service.OrderApplicationService; import org.junit.jupiter.api.Test; import jakarta.annotation.Resource; @@ -11,8 +14,11 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.test.util.ReflectionTestUtils; import java.util.Set; +import java.util.List; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; @@ -66,6 +72,35 @@ class PlatformOrderPageControllerTest { verifyNoInteractions(service); } + @Test + void batchApproveReturnsPartialFailureAndContinuesRemainingOrders() { + doThrow(new BizException(ResultCode.CONFLICT, "订单已审核")) + .when(service).transit(10001L, 1L, "operator", 102L, OrderStatus.APPROVED, null); + + var response = controller.batchApprove(user("PLATFORM", 0L), 10001L, + new OrderRequests.BatchReviewRequest(List.of(101L, 102L, 103L))); + + BatchOrderReviewResponse result = response.data(); + assertThat(result.total()).isEqualTo(3); + assertThat(result.successOrderIds()).containsExactly(101L, 103L); + assertThat(result.failedOrders()).singleElement().satisfies(failure -> { + assertThat(failure.orderId()).isEqualTo(102L); + 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); + } + + @Test + void batchRejectUsesSharedReasonForEveryOrder() { + controller.batchReject(user("PLATFORM", 0L), 10001L, + new OrderRequests.BatchRejectRequest(List.of(101L, 102L), "资料不完整")); + + verify(service).transit(10001L, 1L, "operator", 101L, OrderStatus.REJECTED, "资料不完整"); + verify(service).transit(10001L, 1L, "operator", 102L, OrderStatus.REJECTED, "资料不完整"); + } + private AuthenticatedUser user(String appCode, Long tenantId) { return new AuthenticatedUser(1L, tenantId, null, null, "operator", appCode, "WEB", Set.of()); }