订单批量审核提交

This commit is contained in:
2026-08-07 14:52:21 +08:00
parent 0dc60d835b
commit 77dc32a69a
5 changed files with 131 additions and 0 deletions
@@ -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<BatchOrderReviewResponse> 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<BatchOrderReviewResponse> 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<BatchOrderReviewResponse> batchTransit(
AuthenticatedUser user, long tenantId, List<Long> orderIds, OrderStatus target, String reason) {
long scopedTenantId = OrderScopeGuard.platform(user, tenantId);
List<Long> successOrderIds = new ArrayList<>();
List<BatchOrderReviewResponse.FailedOrder> 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)));
}
}
@@ -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<Long> successOrderIds,
@Schema(description = "失败订单及原因") List<FailedOrder> failedOrders
) {
@Schema(description = "单笔失败结果")
public record FailedOrder(
@Schema(description = "订单 ID") long orderId,
@Schema(description = "业务错误码") String code,
@Schema(description = "失败原因") String message
) {
}
}
@@ -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)
@@ -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
@@ -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());
}