Merge branch 'test-02huogui' into test
# Conflicts: # firstleg/src/main/java/com/baiyue/firstleg/service/InboundMeasurementService.java # firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java
This commit is contained in:
@@ -47,7 +47,9 @@ public class InternalOrderCargoController {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "订单货物明细不存在或不属于该订单");
|
||||
}
|
||||
return ApiResponse.success(new CargoBinding(cargo.getId(), cargo.getOrderId(), cargo.getItemNo(),
|
||||
cargo.getCargoName(), cargo.getPackageCount(), cargo.getGrossWeightKg(), cargo.getVolumeCbm()));
|
||||
cargo.getCargoName(), cargo.getPackageCount(), cargo.getGrossWeightKg(), cargo.getVolumeCbm(),
|
||||
cargo.getSkuNo(), cargo.getCargoNameEn(), cargo.getUnitsPerPackage(), cargo.getTotalQuantity(),
|
||||
cargo.getNetWeightKg(), cargo.getCargoRemark()));
|
||||
}
|
||||
|
||||
@GetMapping("/{tenantId}/{orderId}")
|
||||
@@ -75,7 +77,15 @@ public class InternalOrderCargoController {
|
||||
public record CargoBinding(Long cargoId, Long orderId, Integer itemNo, String cargoName,
|
||||
Integer originalPackageCount,
|
||||
java.math.BigDecimal originalGrossWeightKg,
|
||||
java.math.BigDecimal originalVolumeCbm) {
|
||||
java.math.BigDecimal originalVolumeCbm, String skuNo, String cargoNameEn,
|
||||
Integer unitsPerPackage, Integer totalQuantity,
|
||||
java.math.BigDecimal netWeightKg, String cargoRemark) {
|
||||
public CargoBinding(Long cargoId, Long orderId, Integer itemNo, String cargoName,
|
||||
Integer originalPackageCount, java.math.BigDecimal originalGrossWeightKg,
|
||||
java.math.BigDecimal originalVolumeCbm) {
|
||||
this(cargoId, orderId, itemNo, cargoName, originalPackageCount,
|
||||
originalGrossWeightKg, originalVolumeCbm, null, null, null, null, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
public record CargoLabelBinding(Long cargoId, Long orderId, Integer itemNo, String cargoName,
|
||||
|
||||
@@ -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_REJECT", resourceType = "FIRST_LEG_ORDER", resourceId = "#id", description = "Reject order / 驳回订单")
|
||||
@Operation(summary = "Reject / 驳回", description = "Rejects a submitted order with a required reason. / 使用必填原因驳回已提交订单。")
|
||||
@PostMapping("/{id}/reject")
|
||||
@@ -166,6 +181,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")
|
||||
@@ -242,4 +269,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
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -167,6 +167,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)
|
||||
|
||||
@@ -12,7 +12,7 @@ import java.time.LocalDateTime;
|
||||
public record UnifiedOrderQuery(
|
||||
@Size(max = 64) String keyword,
|
||||
@Size(max = 64) String orderNo,
|
||||
@Schema(allowableValues = {"PENDING_REVIEW", "WAIT_MEASURE", "INBOUNDED", "WAIT_SPLIT",
|
||||
@Schema(allowableValues = {"PENDING_REVIEW", "WAIT_MEASURE", "INBOUNDED",
|
||||
"LOADING", "LOADED", "DELIVERED"})
|
||||
@Size(max = 32) String mainStatus,
|
||||
Long customerId,
|
||||
|
||||
@@ -2,4 +2,21 @@ package com.baiyue.order.entity;
|
||||
/**
|
||||
* 头程订单生命周期状态,约束草稿、提交、审核、受理、处理、完成及取消等状态流转。
|
||||
*/
|
||||
public enum OrderStatus { DRAFT, SUBMITTED, APPROVED, ACCEPTED, REJECTED, CANCELLED, PROCESSING, COMPLETED }
|
||||
public enum OrderStatus {
|
||||
/** 草稿:客户可继续编辑,尚未提交审核。 */
|
||||
DRAFT,
|
||||
/** 已提交:等待平台审核。 */
|
||||
SUBMITTED,
|
||||
/** 审核通过:可由平台接单或进入头程履约。 */
|
||||
APPROVED,
|
||||
/** 已接单:平台已受理订单。 */
|
||||
ACCEPTED,
|
||||
/** 审核驳回:不允许继续进入履约。 */
|
||||
REJECTED,
|
||||
/** 已取消:订单生命周期终止。 */
|
||||
CANCELLED,
|
||||
/** 履约中:订单正在执行头程业务。 */
|
||||
PROCESSING,
|
||||
/** 已完成:订单全流程结束。 */
|
||||
COMPLETED
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ import java.util.stream.Collectors;
|
||||
@Service
|
||||
public class UnifiedOrderQueryService {
|
||||
private static final Set<String> MAIN_STATUSES = Set.of(
|
||||
"PENDING_REVIEW", "WAIT_MEASURE", "INBOUNDED", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED");
|
||||
"PENDING_REVIEW", "WAIT_MEASURE", "INBOUNDED", "LOADING", "LOADED", "DELIVERED");
|
||||
private static final Set<String> REVIEW_STATUSES = Set.of("UNRECORDED", "PENDING", "APPROVED", "REJECTED");
|
||||
private static final List<OrderStatus> ACTIVE_ORDER_STATUSES = List.copyOf(EnumSet.of(
|
||||
OrderStatus.SUBMITTED, OrderStatus.APPROVED, OrderStatus.ACCEPTED,
|
||||
@@ -199,8 +199,9 @@ public class UnifiedOrderQueryService {
|
||||
if (status == null) return null;
|
||||
return switch (status) {
|
||||
case "WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED" -> "WAIT_MEASURE";
|
||||
case "WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING", "WAIT_SPLIT" -> "WAIT_SPLIT";
|
||||
case "INBOUNDED", "LOADING", "LOADED", "DELIVERED" -> status;
|
||||
// WAIT_SPLIT is retained only to normalize records created before status unification.
|
||||
case "WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING", "WAIT_SPLIT", "INBOUNDED" -> "INBOUNDED";
|
||||
case "LOADING", "LOADED", "DELIVERED" -> status;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -311,7 +311,7 @@ components:
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
enum: [PENDING_REVIEW, WAIT_MEASURE, INBOUNDED, WAIT_SPLIT, LOADING, LOADED, DELIVERED]
|
||||
enum: [PENDING_REVIEW, WAIT_MEASURE, INBOUNDED, LOADING, LOADED, DELIVERED]
|
||||
UnifiedInboundCode:
|
||||
{name: inboundCode, in: query, required: false, description: 客户提交后生成的入仓码,待审核订单同样可查询, schema: {type: string, maxLength: 40}}
|
||||
UnifiedInternalOrderStatus:
|
||||
|
||||
@@ -33,6 +33,12 @@ class InternalOrderCargoControllerTest {
|
||||
cargo.setItemNo(1);
|
||||
cargo.setCargoName("测试货物");
|
||||
cargo.setPackageCount(50);
|
||||
cargo.setSkuNo("SKU-31");
|
||||
cargo.setCargoNameEn("Test cargo");
|
||||
cargo.setUnitsPerPackage(4);
|
||||
cargo.setTotalQuantity(200);
|
||||
cargo.setNetWeightKg(new java.math.BigDecimal("18.500"));
|
||||
cargo.setCargoRemark("内部货物备注");
|
||||
when(orders.findCargo(11L, 21L, 31L)).thenReturn(cargo);
|
||||
|
||||
var response = controller.detail("internal-token", 11L, 21L, 31L);
|
||||
@@ -40,6 +46,12 @@ class InternalOrderCargoControllerTest {
|
||||
assertThat(response.successful()).isTrue();
|
||||
assertThat(response.data().cargoId()).isEqualTo(31L);
|
||||
assertThat(response.data().itemNo()).isEqualTo(1);
|
||||
assertThat(response.data().skuNo()).isEqualTo("SKU-31");
|
||||
assertThat(response.data().cargoNameEn()).isEqualTo("Test cargo");
|
||||
assertThat(response.data().unitsPerPackage()).isEqualTo(4);
|
||||
assertThat(response.data().totalQuantity()).isEqualTo(200);
|
||||
assertThat(response.data().netWeightKg()).isEqualByComparingTo("18.500");
|
||||
assertThat(response.data().cargoRemark()).isEqualTo("内部货物备注");
|
||||
verify(orders).findCargo(11L, 21L, 31L);
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,10 @@ class OrderControllerPermissionTest {
|
||||
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);
|
||||
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());
|
||||
}
|
||||
|
||||
@@ -120,15 +120,15 @@ class UnifiedOrderQueryServiceTest {
|
||||
|
||||
@Test
|
||||
void fulfillmentStatusFilterUsesFirstlegPagingWithoutPostPageFiltering() {
|
||||
UnifiedOrderQuery query = query("WAIT_SPLIT");
|
||||
var summary = summary("WAIT_SPLIT", "CONFIRM_PRELOAD");
|
||||
UnifiedOrderQuery query = query("INBOUNDED");
|
||||
var summary = summary("INBOUNDED", "CONFIRM_PRELOAD");
|
||||
when(firstleg.page(eq("internal-token"), any()))
|
||||
.thenReturn(ApiResponse.success(new PageResult<>(List.of(summary), 1, 20, 1, 1)));
|
||||
|
||||
var result = service.page(null, query);
|
||||
|
||||
assertThat(result.records()).singleElement().satisfies(row -> {
|
||||
assertThat(row.mainStatus()).isEqualTo("WAIT_SPLIT");
|
||||
assertThat(row.mainStatus()).isEqualTo("INBOUNDED");
|
||||
assertThat(row.nextAction()).isEqualTo("CONFIRM_PRELOAD");
|
||||
assertThat(row.inboundId()).isEqualTo(88L);
|
||||
assertThat(row.inboundVersion()).isEqualTo(7);
|
||||
@@ -197,7 +197,7 @@ class UnifiedOrderQueryServiceTest {
|
||||
|
||||
private FirstlegUnifiedOrderClient.Summary summary(String status, String action) {
|
||||
var inbound = new FirstlegUnifiedOrderClient.Inbound(
|
||||
88L, 10001L, 77L, "FL-77", "RK-77", "WAIT_SPLIT", "APPROVED", "APPROVED", 7,
|
||||
88L, 10001L, 77L, "FL-77", "RK-77", "INBOUNDED", "APPROVED", "APPROVED", 7,
|
||||
30001L, "测试客户", 11L, "深圳仓", 22L, "ONT8", "美森", 10, null, null, null,
|
||||
null, null, null, null, null, null, null, null, null,
|
||||
LocalDateTime.of(2026, 8, 6, 10, 0), LocalDateTime.of(2026, 8, 6, 11, 0));
|
||||
|
||||
Reference in New Issue
Block a user