This commit is contained in:
2026-08-06 15:32:11 +08:00
parent f3cfbdf59b
commit 5f8b1f64da
21 changed files with 435 additions and 36 deletions
@@ -8,6 +8,7 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
@@ -29,6 +30,10 @@ public interface FirstlegUnifiedOrderClient {
record Query(Long tenantId, String keyword, String orderNo, String mainStatus, Long customerId,
Long receivingWarehouseId, Long destinationWarehouseId,
String inboundCode, String internalOrderStatus, LocalDate expectedArrivalDate,
String feeReviewStatus, String channelName, String containerNo, String cabinetNo,
LocalDate sailingDate, Boolean loaded, Boolean fullContainer,
String measurementReviewStatus, List<Long> candidateOrderIds,
LocalDateTime createdFrom, LocalDateTime createdTo, long page, long size) {}
record BatchRequest(Long tenantId, List<Long> orderIds) {}
record DetailRequest(long tenantId, long orderId) {}
@@ -5,6 +5,7 @@ import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Size;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDate;
import java.time.LocalDateTime;
/** Unified product order list filters shared by platform and merchant entry points. */
@@ -17,6 +18,18 @@ public record UnifiedOrderQuery(
Long customerId,
Long receivingWarehouseId,
Long destinationWarehouseId,
@Size(max = 64) String inboundCode,
@Size(max = 32) String internalOrderStatus,
LocalDate expectedArrivalDate,
@Size(max = 32) String feeReviewStatus,
@Size(max = 128) String channelName,
@Size(max = 64) String containerNo,
@Size(max = 64) String cabinetNo,
LocalDate sailingDate,
Boolean loaded,
Boolean fullContainer,
@Size(max = 32) String measurementReviewStatus,
@Size(max = 64) String fbaCode,
LocalDateTime createdFrom,
LocalDateTime createdTo,
@Min(1) long page,
@@ -166,6 +166,15 @@ public class OrderRepository {
query.receivingWarehouseId())
.eq(query.destinationWarehouseId() != null, OrderEntity::getDestinationWarehouseId,
query.destinationWarehouseId())
.eq(StringUtils.hasText(query.internalOrderStatus()), OrderEntity::getStatus,
StringUtils.hasText(query.internalOrderStatus()) ? OrderStatus.valueOf(query.internalOrderStatus()) : null)
.eq(StringUtils.hasText(query.channelName()), OrderEntity::getChannelName, query.channelName())
.eq(query.fullContainer() != null, OrderEntity::getContainerMode,
query.fullContainer() == null ? null : query.fullContainer() ? "FULL_CONTAINER" : "LCL")
.apply(StringUtils.hasText(query.fbaCode()),
"EXISTS (SELECT 1 FROM ord_order_cargo c WHERE c.tenant_id = ord_order.tenant_id "
+ "AND c.order_id = ord_order.id AND c.deleted = 0 AND c.sku_no LIKE CONCAT('%', {0}, '%'))",
query.fbaCode())
.ge(query.createdFrom() != null, OrderEntity::getCreatedAt, query.createdFrom())
.le(query.createdTo() != null, OrderEntity::getCreatedAt, query.createdTo())
.and(StringUtils.hasText(query.keyword()), nested -> nested
@@ -175,6 +184,15 @@ public class OrderRepository {
return PageResult.from(orderMapper.selectPage(new Page<>(query.page(), query.size()), wrapper));
}
public List<Long> findOrderIdsBySkuNo(Long tenantId, String fbaCode) {
if (!StringUtils.hasText(fbaCode)) return List.of();
QueryWrapper<OrderCargoEntity> wrapper = new QueryWrapper<OrderCargoEntity>()
.select("DISTINCT order_id")
.eq(tenantId != null, "tenant_id", tenantId)
.like("sku_no", fbaCode);
return cargoMapper.selectObjs(wrapper).stream().map(value -> ((Number) value).longValue()).toList();
}
public List<OrderCargoEntity> findCargoByOrderIds(long tenantId, List<Long> orderIds) {
if (orderIds.isEmpty()) return List.of();
return cargoMapper.selectList(new LambdaQueryWrapper<OrderCargoEntity>()
@@ -41,7 +41,10 @@ public class UnifiedOrderQueryService {
public PageResult<UnifiedOrderResponse.Row> page(Long tenantId, UnifiedOrderQuery query) {
validate(query);
if (StringUtils.hasText(query.mainStatus()) && !"PENDING_REVIEW".equals(query.mainStatus())) {
if (requiresFirstlegQuery(query)) {
if ("PENDING_REVIEW".equals(query.mainStatus())) {
return new PageResult<>(List.of(), query.page(), query.size(), 0, 0);
}
return pageFromFirstleg(tenantId, query);
}
List<OrderStatus> statuses = "PENDING_REVIEW".equals(query.mainStatus())
@@ -77,9 +80,17 @@ public class UnifiedOrderQueryService {
}
private PageResult<UnifiedOrderResponse.Row> pageFromFirstleg(Long tenantId, UnifiedOrderQuery query) {
List<Long> candidateOrderIds = StringUtils.hasText(query.fbaCode())
? orders.findOrderIdsBySkuNo(tenantId, query.fbaCode()) : null;
if (candidateOrderIds != null && candidateOrderIds.isEmpty()) {
return new PageResult<>(List.of(), query.page(), query.size(), 0, 0);
}
FirstlegUnifiedOrderClient.Query request = new FirstlegUnifiedOrderClient.Query(
tenantId, query.keyword(), query.orderNo(), query.mainStatus(), query.customerId(),
query.receivingWarehouseId(), query.destinationWarehouseId(), query.createdFrom(), query.createdTo(),
query.receivingWarehouseId(), query.destinationWarehouseId(), query.inboundCode(),
query.internalOrderStatus(), query.expectedArrivalDate(), query.feeReviewStatus(), query.channelName(),
query.containerNo(), query.cabinetNo(), query.sailingDate(), query.loaded(), query.fullContainer(),
query.measurementReviewStatus(), candidateOrderIds, query.createdFrom(), query.createdTo(),
query.page(), query.size());
PageResult<FirstlegUnifiedOrderClient.Summary> source = require(
call(() -> firstleg.page(internalToken, request)), "统一订单履约查询响应无效");
@@ -184,6 +195,20 @@ public class UnifiedOrderQueryService {
&& query.createdFrom().isAfter(query.createdTo())) {
throw new BizException(ResultCode.BAD_REQUEST, "创建时间起点不能晚于终点");
}
if (StringUtils.hasText(query.internalOrderStatus())) {
try { OrderStatus.valueOf(query.internalOrderStatus()); }
catch (IllegalArgumentException exception) {
throw new BizException(ResultCode.BAD_REQUEST, "订单内部状态不合法");
}
}
}
private boolean requiresFirstlegQuery(UnifiedOrderQuery query) {
return StringUtils.hasText(query.mainStatus()) && !"PENDING_REVIEW".equals(query.mainStatus())
|| StringUtils.hasText(query.inboundCode()) || query.expectedArrivalDate() != null
|| StringUtils.hasText(query.feeReviewStatus()) || StringUtils.hasText(query.containerNo())
|| StringUtils.hasText(query.cabinetNo()) || query.sailingDate() != null || query.loaded() != null
|| StringUtils.hasText(query.measurementReviewStatus());
}
private <T> ApiResponse<T> call(java.util.concurrent.Callable<ApiResponse<T>> invocation) {
@@ -23,6 +23,18 @@ paths:
- {name: customerId, in: query, required: false, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
- {name: receivingWarehouseId, in: query, required: false, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
- {name: destinationWarehouseId, in: query, required: false, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
- $ref: '#/components/parameters/UnifiedInboundCode'
- $ref: '#/components/parameters/UnifiedInternalOrderStatus'
- $ref: '#/components/parameters/UnifiedExpectedArrivalDate'
- $ref: '#/components/parameters/UnifiedFeeReviewStatus'
- $ref: '#/components/parameters/UnifiedChannelName'
- $ref: '#/components/parameters/UnifiedContainerNo'
- $ref: '#/components/parameters/UnifiedCabinetNo'
- $ref: '#/components/parameters/UnifiedSailingDate'
- $ref: '#/components/parameters/UnifiedLoaded'
- $ref: '#/components/parameters/UnifiedFullContainer'
- $ref: '#/components/parameters/UnifiedMeasurementReviewStatus'
- $ref: '#/components/parameters/UnifiedFbaCode'
- $ref: '#/components/parameters/UnifiedCreatedFrom'
- $ref: '#/components/parameters/UnifiedCreatedTo'
- $ref: '#/components/parameters/UnifiedPage'
@@ -54,6 +66,18 @@ paths:
- {name: customerId, in: query, required: false, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
- {name: receivingWarehouseId, in: query, required: false, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
- {name: destinationWarehouseId, in: query, required: false, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
- $ref: '#/components/parameters/UnifiedInboundCode'
- $ref: '#/components/parameters/UnifiedInternalOrderStatus'
- $ref: '#/components/parameters/UnifiedExpectedArrivalDate'
- $ref: '#/components/parameters/UnifiedFeeReviewStatus'
- $ref: '#/components/parameters/UnifiedChannelName'
- $ref: '#/components/parameters/UnifiedContainerNo'
- $ref: '#/components/parameters/UnifiedCabinetNo'
- $ref: '#/components/parameters/UnifiedSailingDate'
- $ref: '#/components/parameters/UnifiedLoaded'
- $ref: '#/components/parameters/UnifiedFullContainer'
- $ref: '#/components/parameters/UnifiedMeasurementReviewStatus'
- $ref: '#/components/parameters/UnifiedFbaCode'
- $ref: '#/components/parameters/UnifiedCreatedFrom'
- $ref: '#/components/parameters/UnifiedCreatedTo'
- $ref: '#/components/parameters/UnifiedPage'
@@ -200,6 +224,54 @@ components:
schema:
type: string
enum: [PENDING_REVIEW, WAIT_MEASURE, INBOUNDED, WAIT_SPLIT, LOADING, LOADED, DELIVERED]
UnifiedInboundCode:
{name: inboundCode, in: query, required: false, schema: {type: string, maxLength: 64}}
UnifiedInternalOrderStatus:
name: internalOrderStatus
in: query
required: false
description: Order 内部生命周期状态;不是七态产品主状态
schema: {type: string, enum: [DRAFT, SUBMITTED, APPROVED, ACCEPTED, REJECTED, CANCELLED, PROCESSING, COMPLETED]}
UnifiedExpectedArrivalDate:
{name: expectedArrivalDate, in: query, required: false, schema: {type: string, format: date}}
UnifiedFeeReviewStatus:
name: feeReviewStatus
in: query
required: false
description: 当前最高版本入库费用审核状态;UNRECORDED 表示尚无费用记录
schema: {type: string, maxLength: 32}
UnifiedChannelName:
{name: channelName, in: query, required: false, schema: {type: string, maxLength: 128}}
UnifiedContainerNo:
{name: containerNo, in: query, required: false, schema: {type: string, maxLength: 64}}
UnifiedCabinetNo:
{name: cabinetNo, in: query, required: false, schema: {type: string, maxLength: 64}}
UnifiedSailingDate:
{name: sailingDate, in: query, required: false, schema: {type: string, format: date}}
UnifiedLoaded:
name: loaded
in: query
required: false
description: true 表示运输装载状态存在且不是 UNLOADED/PENDING;false 表示无该状态
schema: {type: boolean}
UnifiedFullContainer:
name: fullContainer
in: query
required: false
description: true 对应 FULL_CONTAINER,false 对应 LCL
schema: {type: boolean}
UnifiedMeasurementReviewStatus:
name: measurementReviewStatus
in: query
required: false
description: 测量审核状态;UNRECORDED 表示字段为空
schema: {type: string, maxLength: 32}
UnifiedFbaCode:
name: fbaCode
in: query
required: false
description: 兼容匹配订单货物 sku_no
schema: {type: string, maxLength: 64}
UnifiedCreatedFrom:
name: createdFrom
in: query
@@ -9,11 +9,13 @@ import com.baiyue.order.entity.OrderStatus;
import com.baiyue.order.repository.OrderRepository;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import java.time.LocalDateTime;
import java.time.LocalDate;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@@ -68,8 +70,39 @@ class UnifiedOrderQueryServiceTest {
verify(orders, never()).unifiedPage(any(), any(), any());
}
@Test
void expandedFulfillmentFiltersAreForwardedBeforeDatabasePaging() {
UnifiedOrderQuery query = new UnifiedOrderQuery(
null, null, "LOADING", 30001L, 11L, 22L, "RK-77", "APPROVED",
LocalDate.of(2026, 8, 6), "APPROVED", "美森", "MSCU", "CAB-1",
LocalDate.of(2026, 8, 8), true, true, "UNRECORDED", "FBA-1",
null, null, 1, 20);
when(orders.findOrderIdsBySkuNo(10001L, "FBA-1")).thenReturn(List.of(77L));
when(firstleg.page(eq("internal-token"), any()))
.thenReturn(ApiResponse.success(new PageResult<>(List.of(), 1, 20, 0, 0)));
service.page(10001L, query);
ArgumentCaptor<FirstlegUnifiedOrderClient.Query> captor =
ArgumentCaptor.forClass(FirstlegUnifiedOrderClient.Query.class);
verify(firstleg).page(eq("internal-token"), captor.capture());
assertThat(captor.getValue()).satisfies(request -> {
assertThat(request.inboundCode()).isEqualTo("RK-77");
assertThat(request.internalOrderStatus()).isEqualTo("APPROVED");
assertThat(request.expectedArrivalDate()).isEqualTo(LocalDate.of(2026, 8, 6));
assertThat(request.containerNo()).isEqualTo("MSCU");
assertThat(request.sailingDate()).isEqualTo(LocalDate.of(2026, 8, 8));
assertThat(request.loaded()).isTrue();
assertThat(request.fullContainer()).isTrue();
assertThat(request.measurementReviewStatus()).isEqualTo("UNRECORDED");
assertThat(request.candidateOrderIds()).containsExactly(77L);
});
}
private UnifiedOrderQuery query(String status) {
return new UnifiedOrderQuery(null, null, status, null, null, null, null, null, 1, 20);
return new UnifiedOrderQuery(null, null, status, null, null, null,
null, null, null, null, null, null, null, null, null, null, null, null,
null, null, 1, 20);
}
private OrderEntity order(OrderStatus status) {