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:
2026-08-07 23:46:44 +08:00
135 changed files with 9649 additions and 370 deletions
@@ -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));