提交
This commit is contained in:
@@ -24,6 +24,18 @@ public interface FirstlegUnifiedOrderClient {
|
||||
record WorkbenchFilters(List<Long> matchingStatusOrderIds, List<Long> feeOrderIds,
|
||||
List<Long> customsOrderIds, List<com.baiyue.order.dto.MerchantOrderProgress> progress) {}
|
||||
|
||||
/** 当前页候选订单按原筛选批量读取摘要。 */
|
||||
@PostMapping("/status-summaries")
|
||||
ApiResponse<List<Summary>> statusSummaries(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestBody Query request);
|
||||
|
||||
/** 批量读取符合筛选的去重订单状态,不返回履约明细。 */
|
||||
@PostMapping("/status-rows")
|
||||
ApiResponse<List<StatusRow>> statusRows(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestBody Query request);
|
||||
|
||||
record StatusRow(Long tenantId, Long orderId, String mainStatus) {}
|
||||
|
||||
@PostMapping("/query")
|
||||
ApiResponse<PageResult<Summary>> page(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestBody Query request);
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.audit.BusinessAudit;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.order.dto.UnifiedOrderQuery;
|
||||
import com.baiyue.order.dto.UnifiedOrderStatusCountQuery;
|
||||
import com.baiyue.order.dto.UnifiedOrderResponse;
|
||||
import com.baiyue.order.dto.OrderRequests;
|
||||
import com.baiyue.order.dto.OrderDeclarationDtos;
|
||||
@@ -52,6 +53,19 @@ public class PlatformUnifiedOrderController {
|
||||
return ApiResponse.success(service.platformPage(tenantId, query));
|
||||
}
|
||||
|
||||
/** 与订单列表共用平台权限及客户范围,不提供全部汇总数量。 */
|
||||
@GetMapping("/status-counts")
|
||||
@PreAuthorize("hasAuthority('platform:order:list')")
|
||||
@Operation(summary = "统一订单页签数量", description = "忽略mainStatus和分页,按其余列表筛选全量去重统计;不返回全部数量。")
|
||||
public ApiResponse<UnifiedOrderQueryService.StatusCounts> statusCounts(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) Long tenantId,
|
||||
@Valid UnifiedOrderStatusCountQuery query) {
|
||||
OrderScopeGuard.platform(user);
|
||||
if (tenantId != null) OrderScopeGuard.platform(user, tenantId);
|
||||
return ApiResponse.success(service.statusCounts(tenantId, query.toOrderQuery()));
|
||||
}
|
||||
|
||||
@GetMapping("/{orderId}")
|
||||
@PreAuthorize("hasAuthority('platform:order:list')")
|
||||
@Operation(summary = "统一订单详情", description = "合并订单和履约时间线;平台响应包含内部备注。")
|
||||
|
||||
@@ -13,7 +13,7 @@ public record UnifiedOrderQuery(
|
||||
@Size(max = 64) String keyword,
|
||||
@Size(max = 64) String orderNo,
|
||||
@Schema(allowableValues = {"PENDING_REVIEW", "WAIT_MEASURE", "WAIT_PALLETIZE", "INBOUNDED",
|
||||
"WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED", "COMPLETED"})
|
||||
"WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED", "COMPLETED", "WAIT_WEIGHT_CONFIRM", "WEIGHT_CONFIRMED", "WAIT_CUSTOMS_DOCS", "EXPORT_CUSTOMS", "SAILING_CLEARANCE", "WAIT_UNLOAD", "WAIT_DISPATCH", "OUTBOUND_DOCUMENT", "DELIVERING", "SIGNED", "MEASUREMENT_REVIEW"})
|
||||
@Size(max = 32) String mainStatus,
|
||||
Long customerId,
|
||||
Long receivingWarehouseId,
|
||||
|
||||
@@ -297,9 +297,28 @@ public class OrderRepository {
|
||||
return PageResult.from(orderMapper.selectPage(new Page<>(query.page(), query.size()), wrapper));
|
||||
}
|
||||
|
||||
public PageResult<OrderEntity> unifiedPage(Long tenantId, UnifiedOrderQuery query,
|
||||
List<OrderStatus> statuses) {
|
||||
LambdaQueryWrapper<OrderEntity> wrapper = new LambdaQueryWrapper<OrderEntity>()
|
||||
/** 统一订单列表按共享筛选分页。 */
|
||||
public PageResult<OrderEntity> unifiedPage(Long tenantId, UnifiedOrderQuery query, List<OrderStatus> statuses) {
|
||||
return PageResult.from(orderMapper.selectPage(new Page<>(query.page(), query.size()), unifiedFilter(tenantId, query, statuses)));
|
||||
}
|
||||
|
||||
/** 全量轻量投影只读取订单身份和权威交易状态。 */
|
||||
public List<OrderEntity> unifiedStatusOrders(Long tenantId, UnifiedOrderQuery query, List<OrderStatus> statuses) {
|
||||
return orderMapper.selectList(unifiedFilter(tenantId, query, statuses)
|
||||
.select(OrderEntity::getId, OrderEntity::getTenantId, OrderEntity::getStatus));
|
||||
}
|
||||
|
||||
/** 状态覆盖只接受本客户范围内未删除订单的权威状态。 */
|
||||
public List<OrderEntity> statusAuthorities(Long tenantId, List<Long> orderIds) {
|
||||
if (orderIds.isEmpty()) return List.of();
|
||||
return orderMapper.selectList(new LambdaQueryWrapper<OrderEntity>()
|
||||
.select(OrderEntity::getId, OrderEntity::getTenantId, OrderEntity::getStatus)
|
||||
.eq(tenantId != null, OrderEntity::getTenantId, tenantId).in(OrderEntity::getId, orderIds));
|
||||
}
|
||||
|
||||
/** 列表和统计共用非分页筛选及客户隔离。 */
|
||||
private LambdaQueryWrapper<OrderEntity> unifiedFilter(Long tenantId, UnifiedOrderQuery query, List<OrderStatus> statuses) {
|
||||
return new LambdaQueryWrapper<OrderEntity>()
|
||||
.eq(tenantId != null, OrderEntity::getTenantId, tenantId)
|
||||
.eq(query.taxMode() != null, OrderEntity::getTaxMode, query.taxMode())
|
||||
.in(OrderEntity::getStatus, statuses)
|
||||
@@ -325,7 +344,6 @@ public class OrderRepository {
|
||||
.like(OrderEntity::getOrderNo, query.keyword())
|
||||
.or().like(OrderEntity::getCustomerName, query.keyword()))
|
||||
.orderByDesc(OrderEntity::getCreatedAt).orderByDesc(OrderEntity::getId);
|
||||
return PageResult.from(orderMapper.selectPage(new Page<>(query.page(), query.size()), wrapper));
|
||||
}
|
||||
|
||||
public List<Long> findOrderIdsBySkuNo(Long tenantId, String fbaCode) {
|
||||
|
||||
@@ -42,6 +42,7 @@ public class UnifiedOrderQueryService {
|
||||
private static final Set<String> MAIN_STATUSES = Set.of(
|
||||
"PENDING_REVIEW", "WAIT_MEASURE", "WAIT_PALLETIZE", "INBOUNDED", "WAIT_SPLIT",
|
||||
"LOADING", "LOADED", "DELIVERED", "COMPLETED");
|
||||
private static final List<String> WORKFLOW_STATUSES = List.of("WAIT_WEIGHT_CONFIRM", "WEIGHT_CONFIRMED", "WAIT_CUSTOMS_DOCS", "EXPORT_CUSTOMS", "SAILING_CLEARANCE", "WAIT_UNLOAD", "WAIT_DISPATCH", "OUTBOUND_DOCUMENT", "DELIVERING", "SIGNED", "MEASUREMENT_REVIEW");
|
||||
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,
|
||||
@@ -60,6 +61,102 @@ public class UnifiedOrderQueryService {
|
||||
return page(tenantId, query, true);
|
||||
}
|
||||
|
||||
/** 页签统计忽略主状态和分页,返回筛选全量订单的准确去重数量。 */
|
||||
public StatusCounts statusCounts(Long tenantId, UnifiedOrderQuery query) {
|
||||
query = withoutMainStatus(query);
|
||||
validate(query);
|
||||
Map<String, Long> counts = new java.util.LinkedHashMap<>();
|
||||
for (String key : List.of("PENDING_REVIEW", "WAIT_MEASURE", "WAIT_PALLETIZE", "WAIT_SPLIT",
|
||||
"LOADING", "LOADED", "DELIVERED", "COMPLETED")) counts.put(key, 0L);
|
||||
WORKFLOW_STATUSES.forEach(key -> counts.put(key, 0L));
|
||||
boolean operational = requiresFirstlegQuery(query);
|
||||
if (!operational) {
|
||||
// 审核与完成由 Order 权威状态决定,不依赖 Firstleg 是否存在投影。
|
||||
for (OrderEntity order : orders.unifiedStatusOrders(tenantId, query,
|
||||
List.of(OrderStatus.SUBMITTED, OrderStatus.COMPLETED))) {
|
||||
counts.merge(orderMainStatus(order.getStatus()), 1L, Long::sum);
|
||||
}
|
||||
}
|
||||
for (FirstlegUnifiedOrderClient.StatusRow row : matchingStatusRows(tenantId, query)) {
|
||||
String status = row.mainStatus();
|
||||
if (!operational && Set.of("PENDING_REVIEW", "COMPLETED").contains(status)) continue;
|
||||
if (counts.containsKey(status)) counts.merge(status, 1L, Long::sum);
|
||||
}
|
||||
return new StatusCounts(java.util.Collections.unmodifiableMap(counts));
|
||||
}
|
||||
|
||||
public record StatusCounts(
|
||||
@com.fasterxml.jackson.databind.annotation.JsonSerialize(contentUsing = CountNumberSerializer.class)
|
||||
Map<String, Long> counts) {}
|
||||
|
||||
/** 数量保持 JSON number;不能被全局雪花 ID 的 Long 字符串序列化规则改写。 */
|
||||
public static final class CountNumberSerializer extends com.fasterxml.jackson.databind.JsonSerializer<Long> {
|
||||
/** 按数值输出数量,不影响系统其他 Long ID 的安全传输。 */
|
||||
@Override
|
||||
public void serialize(Long value, com.fasterxml.jackson.core.JsonGenerator generator,
|
||||
com.fasterxml.jackson.databind.SerializerProvider provider) throws java.io.IOException {
|
||||
generator.writeNumber(value);
|
||||
}
|
||||
}
|
||||
|
||||
/** 统计不受当前页签及分页字段影响;其他条件原样保留。 */
|
||||
private UnifiedOrderQuery withoutMainStatus(UnifiedOrderQuery query) {
|
||||
return new UnifiedOrderQuery(query.keyword(), query.orderNo(), null, query.customerId(),
|
||||
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(), query.fbaCode(), query.createdFrom(), query.createdTo(), 1, 200, query.taxMode());
|
||||
}
|
||||
|
||||
/** 在分页前按订单去重并覆盖审核/完成状态,不能用履约快照冒充订单权威状态。 */
|
||||
private List<FirstlegUnifiedOrderClient.StatusRow> matchingStatusRows(Long tenantId, UnifiedOrderQuery query) {
|
||||
List<Long> candidates = StringUtils.hasText(query.fbaCode())
|
||||
? orders.findOrderIdsBySkuNo(tenantId, query.fbaCode()) : null;
|
||||
if (candidates != null && candidates.isEmpty()) return List.of();
|
||||
var request = statusRequest(tenantId, withoutMainStatus(query), candidates);
|
||||
List<FirstlegUnifiedOrderClient.StatusRow> source = require(
|
||||
call(() -> firstleg.statusRows(internalToken, request)), "统一订单状态查询响应无效");
|
||||
Map<String, FirstlegUnifiedOrderClient.StatusRow> distinct = new java.util.LinkedHashMap<>();
|
||||
for (var row : source) {
|
||||
if (row == null || row.orderId() == null || row.tenantId() == null
|
||||
|| tenantId != null && !tenantId.equals(row.tenantId())) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "统一订单状态查询响应无效");
|
||||
}
|
||||
distinct.putIfAbsent(row.orderId() + ":" + row.mainStatus(), row);
|
||||
}
|
||||
Map<Long, OrderEntity> authority = new HashMap<>();
|
||||
List<Long> ids = distinct.values().stream().map(FirstlegUnifiedOrderClient.StatusRow::orderId).distinct().toList();
|
||||
// 有界批量读取权威状态,避免逐票远程调用及超长 IN 条件。
|
||||
for (int offset = 0; offset < ids.size(); offset += 500) {
|
||||
orders.statusAuthorities(tenantId, ids.subList(offset, Math.min(offset + 500, ids.size())))
|
||||
.forEach(order -> authority.put(order.getId(), order));
|
||||
}
|
||||
List<FirstlegUnifiedOrderClient.StatusRow> result = new ArrayList<>();
|
||||
for (var row : distinct.values()) {
|
||||
OrderEntity order = authority.get(row.orderId());
|
||||
if (order == null || !Objects.equals(order.getTenantId(), row.tenantId())
|
||||
|| !ACTIVE_ORDER_STATUSES.contains(order.getStatus())) continue;
|
||||
String status = order.getStatus() == OrderStatus.SUBMITTED ? "PENDING_REVIEW"
|
||||
: order.getStatus() == OrderStatus.COMPLETED && !WORKFLOW_STATUSES.contains(row.mainStatus()) ? "COMPLETED" : row.mainStatus();
|
||||
if (status != null) result.add(new FirstlegUnifiedOrderClient.StatusRow(row.tenantId(), row.orderId(), status));
|
||||
}
|
||||
// 同一订单的多个分片覆盖为待审核/已完成后仍只计一次。
|
||||
Map<String, FirstlegUnifiedOrderClient.StatusRow> uniqueStatuses = new java.util.LinkedHashMap<>();
|
||||
result.forEach(row -> uniqueStatuses.putIfAbsent(row.orderId() + ":" + row.mainStatus(), row));
|
||||
return List.copyOf(uniqueStatuses.values());
|
||||
}
|
||||
|
||||
/** 状态投影和当前页摘要共用原筛选,候选订单用于服务内批量收敛范围。 */
|
||||
private FirstlegUnifiedOrderClient.Query statusRequest(Long tenantId, UnifiedOrderQuery query, List<Long> candidates) {
|
||||
String status = Set.of("PENDING_REVIEW", "COMPLETED").contains(
|
||||
query.mainStatus() == null ? "" : query.mainStatus()) ? null : query.mainStatus();
|
||||
return new FirstlegUnifiedOrderClient.Query(tenantId, query.keyword(), query.orderNo(), status,
|
||||
query.customerId(), 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(), candidates, query.createdFrom(), query.createdTo(), 1, 200, query.taxMode());
|
||||
}
|
||||
|
||||
/** 平台导出选择项按业务订单号读取;不暴露内部 ID 作为选择契约。 */
|
||||
public List<UnifiedOrderResponse.Row> platformRowsByOrderNos(List<String> orderNos) {
|
||||
if (orderNos == null || orderNos.isEmpty()) return List.of();
|
||||
@@ -83,13 +180,11 @@ public class UnifiedOrderQueryService {
|
||||
return selected.stream().map(order->row(order,summaries.get(order.getId()),null,children,cargo.get(order.getId()))).toList();
|
||||
}
|
||||
|
||||
/** 依条件选择订单查询或履约筛选,保留订单审核优先规则。 */
|
||||
private PageResult<UnifiedOrderResponse.Row> page(Long tenantId, UnifiedOrderQuery query,
|
||||
boolean includeCoLoadGroups) {
|
||||
validate(query);
|
||||
if (requiresFirstlegQuery(query)) {
|
||||
if ("PENDING_REVIEW".equals(query.mainStatus())) {
|
||||
return new PageResult<>(List.of(), query.page(), query.size(), 0, 0);
|
||||
}
|
||||
return pageFromFirstleg(tenantId, query, includeCoLoadGroups);
|
||||
}
|
||||
List<OrderStatus> statuses = "PENDING_REVIEW".equals(query.mainStatus())
|
||||
@@ -162,25 +257,33 @@ public class UnifiedOrderQueryService {
|
||||
return new UnifiedOrderResponse.Detail(summary, orderDetail, timeline, fragments);
|
||||
}
|
||||
|
||||
/** 先校正权威状态并去重,再对匹配订单分页和批量补齐当前页摘要。 */
|
||||
private PageResult<UnifiedOrderResponse.Row> pageFromFirstleg(Long tenantId, UnifiedOrderQuery query,
|
||||
boolean includeCoLoadGroups) {
|
||||
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.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(), query.taxMode());
|
||||
PageResult<FirstlegUnifiedOrderClient.Summary> source = require(
|
||||
call(() -> firstleg.page(internalToken, request)), "统一订单履约查询响应无效");
|
||||
List<FirstlegUnifiedOrderClient.Summary> summaries = validateSummaries(
|
||||
source.records(), "统一订单履约查询响应无效");
|
||||
List<Long> orderIds = summaries.stream().map(item -> item.inbound().orderId()).toList();
|
||||
List<FirstlegUnifiedOrderClient.StatusRow> matching = matchingStatusRows(tenantId, query).stream()
|
||||
.filter(row -> !StringUtils.hasText(query.mainStatus()) || query.mainStatus().equals(row.mainStatus()))
|
||||
.collect(Collectors.collectingAndThen(Collectors.toMap(FirstlegUnifiedOrderClient.StatusRow::orderId,
|
||||
Function.identity(), (left, right) -> left, java.util.LinkedHashMap::new),
|
||||
values -> List.copyOf(values.values())));
|
||||
long total = matching.size();
|
||||
long start = query.page() - 1 > Long.MAX_VALUE / query.size()
|
||||
? total : Math.min((query.page() - 1) * query.size(), total);
|
||||
List<Long> orderIds = matching.subList((int) start, (int) Math.min(start + query.size(), total))
|
||||
.stream().map(FirstlegUnifiedOrderClient.StatusRow::orderId).toList();
|
||||
Map<Long, FirstlegUnifiedOrderClient.Summary> byOrder = orderIds.isEmpty() ? Map.of()
|
||||
: validateSummaries(require(call(() -> firstleg.statusSummaries(internalToken,
|
||||
statusRequest(tenantId, query, orderIds))), "统一订单履约摘要响应无效"),
|
||||
"统一订单履约摘要响应无效").stream().collect(Collectors.toMap(
|
||||
item -> item.inbound().orderId(), Function.identity(), (left, right) -> left));
|
||||
Map<Long, Long> expectedTenants = matching.stream().collect(Collectors.toMap(
|
||||
FirstlegUnifiedOrderClient.StatusRow::orderId, FirstlegUnifiedOrderClient.StatusRow::tenantId));
|
||||
List<FirstlegUnifiedOrderClient.Summary> summaries = orderIds.stream().map(id -> {
|
||||
var summary = byOrder.get(id);
|
||||
if (summary == null || !Objects.equals(summary.inbound().tenantId(), expectedTenants.get(id))) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "统一订单履约摘要缺失或客户不匹配");
|
||||
}
|
||||
return summary;
|
||||
}).toList();
|
||||
Map<Long, OrderEntity> orderById = orders.findByIds(tenantId, orderIds).stream()
|
||||
.collect(Collectors.toMap(OrderEntity::getId, Function.identity(), (left, right) -> left));
|
||||
Map<Long, CargoSummary> cargoSummaries = cargoSummaries(tenantId, List.copyOf(orderById.values()));
|
||||
@@ -193,7 +296,8 @@ public class UnifiedOrderQueryService {
|
||||
cargoSummaries.get(summary.inbound().orderId()),
|
||||
coLoadByOrder.get(summary.inbound().orderId())))
|
||||
.toList();
|
||||
return new PageResult<>(records, source.page(), source.size(), source.total(), source.pages());
|
||||
return new PageResult<>(records, query.page(), query.size(), total,
|
||||
total / query.size() + (total % query.size() == 0 ? 0 : 1));
|
||||
}
|
||||
|
||||
private Map<Long, FirstlegUnifiedOrderClient.Summary> summaries(Long tenantId, List<Long> orderIds) {
|
||||
@@ -391,14 +495,17 @@ public class UnifiedOrderQueryService {
|
||||
order.getCreatedAt(), order.getUpdatedAt(), inbound.requestedPickupAt());
|
||||
}
|
||||
|
||||
/** 将当前页匹配履约记录映射为订单行,审核与完成优先使用 Order 权威状态。 */
|
||||
private UnifiedOrderResponse.Row row(FirstlegUnifiedOrderClient.Summary summary, OrderEntity order,
|
||||
String latestInternalNote, Map<Long, Integer> childCounts,
|
||||
CargoSummary cargo,
|
||||
UnifiedOrderResponse.CoLoadGroupSummary coLoadGroup) {
|
||||
var inbound = summary.inbound();
|
||||
boolean completed = order != null && order.getStatus() == OrderStatus.COMPLETED;
|
||||
boolean pending = order != null && order.getStatus() == OrderStatus.SUBMITTED;
|
||||
return new UnifiedOrderResponse.Row(inbound.orderId(), order == null ? null : order.getVersion(), inbound.tenantId(), inbound.orderNo(),
|
||||
completed ? "COMPLETED" : summary.mainStatus(), completed ? null : summary.nextAction(), Boolean.TRUE.equals(summary.customerFeeConfirmationRequired()), relation(order, childCounts),
|
||||
pending ? "PENDING_REVIEW" : completed ? "COMPLETED" : summary.mainStatus(),
|
||||
pending ? "APPROVE_ORDER" : completed ? null : summary.nextAction(), Boolean.TRUE.equals(summary.customerFeeConfirmationRequired()), relation(order, childCounts),
|
||||
parseOrderStatus(inbound.orderStatusSnapshot()),
|
||||
inbound.customerId(), inbound.customerName(), inbound.channelCode(), inbound.channelName(),
|
||||
order != null && Boolean.TRUE.equals(order.getChannelChanged()), inbound.warehouseId(),
|
||||
@@ -577,6 +684,7 @@ public class UnifiedOrderQueryService {
|
||||
}
|
||||
if (StringUtils.hasText(query.mainStatus())
|
||||
&& !MAIN_STATUSES.contains(query.mainStatus())
|
||||
&& !WORKFLOW_STATUSES.contains(query.mainStatus())
|
||||
) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "统一订单主状态不合法");
|
||||
}
|
||||
|
||||
@@ -378,7 +378,7 @@ paths:
|
||||
inboundId、inboundVersion、loadingVersion、pickingVersion;inboundVersion
|
||||
才是 C04 loading/complete 的 expectedVersion,loadingVersion 当前等于 pickingVersion(兼容别名)。
|
||||
tenantId 省略时跨全部客户;mainStatus 省略时按订单创建时间分页,指定 PENDING_REVIEW 时由 Order 查询,
|
||||
指定其他主状态时由 Firstleg 精确分页。旧 /orders/first-leg 接口保持兼容。
|
||||
指定履约主状态时先按 Order 权威状态校正并去重,再分页读取 Firstleg 摘要。旧 /orders/first-leg 接口保持兼容。
|
||||
列表行同时返回 customerRemark(客户下单备注)、latestInternalNote(最新一条平台内部备注)和
|
||||
relation(仅有关联时返回;PARENT 含 childCount,CHILD 含 rootOrderId、parentOrderId 与 splitSequence)。
|
||||
coLoadGroup 仅平台响应返回,由 Order 经 Firstleg 内部批量只读契约聚合;成员业务标识由 Order 权威订单片段补齐。
|
||||
@@ -414,6 +414,55 @@ paths:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/UnifiedOrderPageEnvelope'}
|
||||
'403': {description: 需要平台身份和 platform:order:list 权限}
|
||||
/api/platform/v1/orders/status-counts:
|
||||
get:
|
||||
summary: 平台统一订单页签数量
|
||||
description: >-
|
||||
需要平台身份和 platform:order:list;tenantId 省略时按授权平台范围查询全部客户。
|
||||
按统一列表非分页、非 mainStatus 筛选全量去重;忽略主状态和分页,不返回全部数量。
|
||||
订单审核与完成状态优先于履约投影,依赖异常返回业务错误,不能以零代替失败。
|
||||
MEASUREMENT_REVIEW 为独立任务,暂不返回该 key。
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/UnifiedTaxMode'
|
||||
- {name: tenantId, in: query, required: false, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
- $ref: '#/components/parameters/UnifiedKeyword'
|
||||
- $ref: '#/components/parameters/UnifiedOrderNo'
|
||||
- {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'
|
||||
responses:
|
||||
'200':
|
||||
description: 标准 ApiResponse;仅 code 为字符串 200 时可使用数量
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
code: {type: string}
|
||||
message: {type: string}
|
||||
data:
|
||||
type: object
|
||||
required: [counts]
|
||||
properties:
|
||||
counts:
|
||||
type: object
|
||||
description: PENDING_REVIEW、WAIT_MEASURE、WAIT_PALLETIZE、WAIT_SPLIT、LOADING、LOADED、DELIVERED、COMPLETED
|
||||
additionalProperties: {type: integer, format: int64, minimum: 0}
|
||||
'403': {description: 需要平台身份和 platform:order:list 权限}
|
||||
/api/platform/v1/orders/{orderId}:
|
||||
get:
|
||||
summary: 平台统一订单详情
|
||||
@@ -1038,7 +1087,7 @@ components:
|
||||
tenantId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
keyword: {type: string, maxLength: 64}
|
||||
orderNo: {type: string, maxLength: 64}
|
||||
mainStatus: {type: string, enum: [PENDING_REVIEW, WAIT_MEASURE, WAIT_PALLETIZE, INBOUNDED, WAIT_SPLIT, LOADING, LOADED, DELIVERED, COMPLETED]}
|
||||
mainStatus: {type: string, enum: [PENDING_REVIEW, WAIT_MEASURE, WAIT_PALLETIZE, INBOUNDED, WAIT_SPLIT, LOADING, LOADED, DELIVERED, COMPLETED, WAIT_WEIGHT_CONFIRM, WEIGHT_CONFIRMED, WAIT_CUSTOMS_DOCS, EXPORT_CUSTOMS, SAILING_CLEARANCE, WAIT_UNLOAD, WAIT_DISPATCH, OUTBOUND_DOCUMENT, DELIVERING, SIGNED, MEASUREMENT_REVIEW]}
|
||||
customerId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
receivingWarehouseId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
destinationWarehouseId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
@@ -1694,10 +1743,10 @@ components:
|
||||
name: mainStatus
|
||||
in: query
|
||||
required: false
|
||||
description: 统一订单八态主状态筛选;WAIT_PALLETIZE、WAIT_SPLIT 与存量兼容状态 INBOUNDED 独立查询
|
||||
description: 统一订单主状态及业务页签筛选;WAIT_PALLETIZE、WAIT_SPLIT 与存量兼容状态 INBOUNDED 独立查询。WAIT_CUSTOMS_DOCS 为实际装柜后待上传客户报关资料;EXPORT_CUSTOMS 为本票报关资料已上传且实际装柜完成、尚未实际开船的汇总,买单票无需客户退税资料,不以点击开始报关为前提;SAILING_CLEARANCE 为实际开船后待上传清关资料;清关资料上传后进入 WAIT_UNLOAD,持续到实际卸柜完成。实际开船统一读取手动或拉取的实际离港节点。
|
||||
schema:
|
||||
type: string
|
||||
enum: [PENDING_REVIEW, WAIT_MEASURE, WAIT_PALLETIZE, INBOUNDED, WAIT_SPLIT, LOADING, LOADED, DELIVERED, COMPLETED]
|
||||
enum: [PENDING_REVIEW, WAIT_MEASURE, WAIT_PALLETIZE, INBOUNDED, WAIT_SPLIT, LOADING, LOADED, DELIVERED, COMPLETED, WAIT_WEIGHT_CONFIRM, WEIGHT_CONFIRMED, WAIT_CUSTOMS_DOCS, EXPORT_CUSTOMS, SAILING_CLEARANCE, WAIT_UNLOAD, WAIT_DISPATCH, OUTBOUND_DOCUMENT, DELIVERING, SIGNED, MEASUREMENT_REVIEW]
|
||||
UnifiedInboundCode:
|
||||
{name: inboundCode, in: query, required: false, description: 订单创建时生成的入仓码,所有订单状态均可查询, schema: {type: string, maxLength: 40}}
|
||||
UnifiedInternalOrderStatus:
|
||||
|
||||
@@ -19,6 +19,38 @@ import java.util.Map;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class UnifiedOrderApiContractTest {
|
||||
/** 全局Long ID配置启用时,数量必须仍为JSON数字。 */
|
||||
@Test
|
||||
void statusCountValuesRemainNumbersWithLongIdSerializationEnabled() throws Exception {
|
||||
var mapper = new com.fasterxml.jackson.databind.ObjectMapper();
|
||||
var module = new com.fasterxml.jackson.databind.module.SimpleModule();
|
||||
module.addSerializer(Long.class, com.fasterxml.jackson.databind.ser.std.ToStringSerializer.instance);
|
||||
mapper.registerModule(module);
|
||||
var json = mapper.readTree(mapper.writeValueAsString(new com.baiyue.order.service.UnifiedOrderQueryService.StatusCounts(
|
||||
Map.of("LOADED", 1205L))));
|
||||
assertThat(json.path("counts").path("LOADED").isIntegralNumber()).isTrue();
|
||||
assertThat(json.path("counts").path("LOADED").longValue()).isEqualTo(1205L);
|
||||
}
|
||||
|
||||
/** 页签数量沿用正式列表权限,不额外开放查询域。 */
|
||||
@Test
|
||||
void statusCountsKeepListPermissionAndFilterContract() throws Exception {
|
||||
Method method = PlatformUnifiedOrderController.class.getMethod("statusCounts",
|
||||
com.baiyue.common.security.AuthenticatedUser.class, Long.class, com.baiyue.order.dto.UnifiedOrderStatusCountQuery.class);
|
||||
assertThat(method.getAnnotation(org.springframework.web.bind.annotation.GetMapping.class).value())
|
||||
.containsExactly("/status-counts");
|
||||
assertThat(method.getAnnotation(org.springframework.security.access.prepost.PreAuthorize.class).value())
|
||||
.isEqualTo("hasAuthority('platform:order:list')");
|
||||
try (var input = new ClassPathResource("openapi/order-v1.yaml").getInputStream()) {
|
||||
Map<String, Object> document = new Yaml().load(input);
|
||||
var paths = stringMap(document.get("paths"));
|
||||
var counts = stringMap(stringMap(paths.get("/api/platform/v1/orders/status-counts")).get("get"));
|
||||
String parameters = counts.get("parameters").toString();
|
||||
assertThat(parameters).contains("UnifiedTaxMode", "UnifiedFbaCode", "UnifiedCreatedFrom", "UnifiedMeasurementReviewStatus")
|
||||
.doesNotContain("UnifiedMainStatus", "UnifiedPage", "UnifiedSize");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsPlatformAndMerchantBoundaries() {
|
||||
assertThat(PlatformUnifiedOrderController.class.getAnnotation(RequestMapping.class).value())
|
||||
@@ -43,7 +75,9 @@ class UnifiedOrderApiContractTest {
|
||||
Map<String, Object> schema = stringMap(mainStatus.get("schema"));
|
||||
assertThat(stringList(schema.get("enum"))).containsExactly(
|
||||
"PENDING_REVIEW", "WAIT_MEASURE", "WAIT_PALLETIZE", "INBOUNDED", "WAIT_SPLIT",
|
||||
"LOADING", "LOADED", "DELIVERED", "COMPLETED");
|
||||
"LOADING", "LOADED", "DELIVERED", "COMPLETED",
|
||||
"WAIT_WEIGHT_CONFIRM", "WEIGHT_CONFIRMED", "WAIT_CUSTOMS_DOCS", "EXPORT_CUSTOMS",
|
||||
"SAILING_CLEARANCE", "WAIT_UNLOAD", "WAIT_DISPATCH", "OUTBOUND_DOCUMENT", "DELIVERING", "SIGNED", "MEASUREMENT_REVIEW");
|
||||
assertThat(mainStatus.get("description").toString())
|
||||
.contains("WAIT_PALLETIZE", "WAIT_SPLIT", "INBOUNDED");
|
||||
|
||||
@@ -59,7 +93,9 @@ class UnifiedOrderApiContractTest {
|
||||
assertThat(queryAnnotation).isNotNull();
|
||||
assertThat(queryAnnotation.allowableValues()).containsExactly(
|
||||
"PENDING_REVIEW", "WAIT_MEASURE", "WAIT_PALLETIZE", "INBOUNDED", "WAIT_SPLIT",
|
||||
"LOADING", "LOADED", "DELIVERED", "COMPLETED");
|
||||
"LOADING", "LOADED", "DELIVERED", "COMPLETED",
|
||||
"WAIT_WEIGHT_CONFIRM", "WEIGHT_CONFIRMED", "WAIT_CUSTOMS_DOCS", "EXPORT_CUSTOMS",
|
||||
"SAILING_CLEARANCE", "WAIT_UNLOAD", "WAIT_DISPATCH", "OUTBOUND_DOCUMENT", "DELIVERING", "SIGNED", "MEASUREMENT_REVIEW");
|
||||
|
||||
Map<String, Object> row = stringMap(schemas.get("UnifiedOrderRow"));
|
||||
assertThat(stringMap(row.get("properties"))).containsKeys(
|
||||
|
||||
+11
@@ -75,6 +75,17 @@ class UnifiedOrderTaxModePaginationTest {
|
||||
assertPage(null, 2, 4, 4);
|
||||
}
|
||||
|
||||
/** 轻量统计使用完整匹配集,并保留税种、客户及软删除条件。 */
|
||||
@Test
|
||||
void statusProjectionIgnoresPagingWithoutLeakingDeletedOrForeignOrders() {
|
||||
var query = new UnifiedOrderQuery(null, null, null, null, null, null, null, null, null,
|
||||
null, null, null, null, null, null, null, null, null, null, null, 2, 1, "TAX_REFUND");
|
||||
assertThat(repository.unifiedStatusOrders(11L, query, List.of(com.baiyue.order.entity.OrderStatus.APPROVED)))
|
||||
.extracting(OrderEntity::getId).containsExactly(6L, 2L);
|
||||
assertThat(repository.statusAuthorities(11L, List.of(2L, 3L, 7L)))
|
||||
.extracting(OrderEntity::getId).containsExactly(2L);
|
||||
}
|
||||
|
||||
@org.junit.jupiter.params.ParameterizedTest
|
||||
@org.junit.jupiter.params.provider.ValueSource(strings = {"TAX_REFUND", "BUY_DECLARATION"})
|
||||
void realSplitChildIsPersistedAndIncludedInTaxFilteredParentChildPages(String taxMode) {
|
||||
|
||||
@@ -586,8 +586,7 @@ class UnifiedOrderQueryServiceTest {
|
||||
void fulfillmentStatusFilterUsesFirstlegPagingWithoutPostPageFiltering() {
|
||||
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)));
|
||||
stubStatusProjection(List.of(summary), 1);
|
||||
|
||||
var result = service.page(null, query);
|
||||
|
||||
@@ -617,8 +616,7 @@ class UnifiedOrderQueryServiceTest {
|
||||
UnifiedOrderQuery query = query("INBOUNDED");
|
||||
var summary = summary("INBOUNDED", null);
|
||||
when(summary.inbound().actualNetWeightKg()).thenReturn(null);
|
||||
when(firstleg.page(eq("internal-token"), any())).thenReturn(
|
||||
ApiResponse.success(new PageResult<>(List.of(summary), 1, 20, 1, 1)));
|
||||
stubStatusProjection(List.of(summary), 1);
|
||||
|
||||
var row = service.page(null, query).records().get(0);
|
||||
|
||||
@@ -631,8 +629,7 @@ class UnifiedOrderQueryServiceTest {
|
||||
UnifiedOrderQuery query = query("INBOUNDED");
|
||||
var summary = summary("INBOUNDED", null);
|
||||
when(summary.inbound().locationCodes()).thenReturn("A0、 A0, A0,A0");
|
||||
when(firstleg.page(eq("internal-token"), any())).thenReturn(
|
||||
ApiResponse.success(new PageResult<>(List.of(summary), 1, 20, 1, 1)));
|
||||
stubStatusProjection(List.of(summary), 1);
|
||||
|
||||
var row = service.page(null, query).records().get(0);
|
||||
|
||||
@@ -643,8 +640,7 @@ class UnifiedOrderQueryServiceTest {
|
||||
void waitSplitFilterForwardsAndReturnsWaitSplitIndependently() {
|
||||
UnifiedOrderQuery query = query("WAIT_SPLIT");
|
||||
var summary = summary("WAIT_SPLIT", "CONFIRM_PRELOAD");
|
||||
when(firstleg.page(eq("internal-token"), any()))
|
||||
.thenReturn(ApiResponse.success(new PageResult<>(List.of(summary), 1, 20, 1, 1)));
|
||||
stubStatusProjection(List.of(summary), 1);
|
||||
when(orders.findByIds(eq(10001L), eq(List.of(77L)))).thenReturn(List.of(order(OrderStatus.PROCESSING)));
|
||||
|
||||
var result = service.page(10001L, query);
|
||||
@@ -653,17 +649,16 @@ class UnifiedOrderQueryServiceTest {
|
||||
.extracting(row -> row.mainStatus()).isEqualTo("WAIT_SPLIT");
|
||||
ArgumentCaptor<FirstlegUnifiedOrderClient.Query> captor =
|
||||
ArgumentCaptor.forClass(FirstlegUnifiedOrderClient.Query.class);
|
||||
verify(firstleg).page(eq("internal-token"), captor.capture());
|
||||
verify(firstleg).statusRows(eq("internal-token"), captor.capture());
|
||||
assertThat(captor.getValue().tenantId()).isEqualTo(10001L);
|
||||
assertThat(captor.getValue().mainStatus()).isEqualTo("WAIT_SPLIT");
|
||||
assertThat(captor.getValue().mainStatus()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void waitPalletizeFilterReturnsDropshipProjectionAndForwardsToFirstleg() {
|
||||
UnifiedOrderQuery query = query("WAIT_PALLETIZE");
|
||||
var summary = summary("WAIT_PALLETIZE", "PALLETIZE_DROPSHIP");
|
||||
when(firstleg.page(eq("internal-token"), any()))
|
||||
.thenReturn(ApiResponse.success(new PageResult<>(List.of(summary), 1, 20, 1, 1)));
|
||||
stubStatusProjection(List.of(summary), 1);
|
||||
when(orders.findByIds(eq(10001L), eq(List.of(77L)))).thenReturn(List.of(order(OrderStatus.PROCESSING)));
|
||||
|
||||
var result = service.page(10001L, query);
|
||||
@@ -676,8 +671,8 @@ class UnifiedOrderQueryServiceTest {
|
||||
});
|
||||
ArgumentCaptor<FirstlegUnifiedOrderClient.Query> captor =
|
||||
ArgumentCaptor.forClass(FirstlegUnifiedOrderClient.Query.class);
|
||||
verify(firstleg).page(eq("internal-token"), captor.capture());
|
||||
assertThat(captor.getValue().mainStatus()).isEqualTo("WAIT_PALLETIZE");
|
||||
verify(firstleg).statusRows(eq("internal-token"), captor.capture());
|
||||
assertThat(captor.getValue().mainStatus()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -800,8 +795,7 @@ class UnifiedOrderQueryServiceTest {
|
||||
var summary = summary("INBOUNDED", null);
|
||||
when(summary.inbound().holdFlag()).thenReturn(false);
|
||||
when(summary.inbound().currentHoldReason()).thenReturn(null);
|
||||
when(firstleg.page(eq("internal-token"), any())).thenReturn(
|
||||
ApiResponse.success(new PageResult<>(List.of(summary), 4, 10, 31, 4)));
|
||||
stubStatusProjection(List.of(summary), 31);
|
||||
|
||||
var result = service.page(10001L, query);
|
||||
|
||||
@@ -815,10 +809,10 @@ class UnifiedOrderQueryServiceTest {
|
||||
});
|
||||
ArgumentCaptor<FirstlegUnifiedOrderClient.Query> captor =
|
||||
ArgumentCaptor.forClass(FirstlegUnifiedOrderClient.Query.class);
|
||||
verify(firstleg).page(eq("internal-token"), captor.capture());
|
||||
assertThat(captor.getValue().mainStatus()).isEqualTo("INBOUNDED");
|
||||
assertThat(captor.getValue().page()).isEqualTo(4);
|
||||
assertThat(captor.getValue().size()).isEqualTo(10);
|
||||
verify(firstleg).statusRows(eq("internal-token"), captor.capture());
|
||||
assertThat(captor.getValue().mainStatus()).isNull();
|
||||
assertThat(captor.getValue().page()).isEqualTo(1);
|
||||
assertThat(captor.getValue().size()).isEqualTo(200);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -830,8 +824,7 @@ class UnifiedOrderQueryServiceTest {
|
||||
when(summary.inbound().orderId()).thenReturn(orderId);
|
||||
when(summary.inbound().id()).thenReturn(inboundId);
|
||||
when(summary.inbound().tenantId()).thenReturn(tenantId);
|
||||
when(firstleg.page(eq("internal-token"), any())).thenReturn(
|
||||
ApiResponse.success(new PageResult<>(List.of(summary), 1, 20, 1, 1)));
|
||||
stubStatusProjection(List.of(summary), 1);
|
||||
|
||||
assertThat(service.page(null, query("INBOUNDED")).records()).singleElement().satisfies(row -> {
|
||||
assertThat(row.orderId()).isEqualTo(orderId);
|
||||
@@ -862,8 +855,7 @@ class UnifiedOrderQueryServiceTest {
|
||||
void missingHoldFlagFailsClosed() {
|
||||
var summary = summary("INBOUNDED", null);
|
||||
when(summary.inbound().holdFlag()).thenReturn(null);
|
||||
when(firstleg.page(eq("internal-token"), any())).thenReturn(
|
||||
ApiResponse.success(new PageResult<>(List.of(summary), 1, 20, 1, 1)));
|
||||
stubStatusProjection(List.of(summary), 1);
|
||||
|
||||
assertServiceUnavailable(() -> service.page(null, query("INBOUNDED")));
|
||||
}
|
||||
@@ -872,8 +864,7 @@ class UnifiedOrderQueryServiceTest {
|
||||
void heldInboundWithoutReasonFailsClosed() {
|
||||
var summary = summary("INBOUNDED", null);
|
||||
when(summary.inbound().currentHoldReason()).thenReturn(" ");
|
||||
when(firstleg.page(eq("internal-token"), any())).thenReturn(
|
||||
ApiResponse.success(new PageResult<>(List.of(summary), 1, 20, 1, 1)));
|
||||
stubStatusProjection(List.of(summary), 1);
|
||||
|
||||
assertServiceUnavailable(() -> service.page(null, query("INBOUNDED")));
|
||||
}
|
||||
@@ -883,8 +874,7 @@ class UnifiedOrderQueryServiceTest {
|
||||
var summary = summary("INBOUNDED", null);
|
||||
when(summary.inbound().holdFlag()).thenReturn(false);
|
||||
when(summary.inbound().currentHoldReason()).thenReturn("旧扣货原因");
|
||||
when(firstleg.page(eq("internal-token"), any())).thenReturn(
|
||||
ApiResponse.success(new PageResult<>(List.of(summary), 1, 20, 1, 1)));
|
||||
stubStatusProjection(List.of(summary), 1);
|
||||
|
||||
assertServiceUnavailable(() -> service.page(null, query("INBOUNDED")));
|
||||
}
|
||||
@@ -905,7 +895,7 @@ class UnifiedOrderQueryServiceTest {
|
||||
|
||||
@Test
|
||||
void firstlegCallExceptionFailsClosed() {
|
||||
when(firstleg.page(eq("internal-token"), any())).thenThrow(new IllegalStateException("boom"));
|
||||
when(firstleg.statusRows(eq("internal-token"), any())).thenThrow(new IllegalStateException("boom"));
|
||||
|
||||
assertServiceUnavailable(() -> service.page(null, query("INBOUNDED")));
|
||||
}
|
||||
@@ -914,8 +904,7 @@ class UnifiedOrderQueryServiceTest {
|
||||
void fulfillmentTabsForwardStatusSearchAndPaginationToFirstleg() {
|
||||
List<String> statuses = List.of(
|
||||
"WAIT_MEASURE", "WAIT_PALLETIZE", "INBOUNDED", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED");
|
||||
when(firstleg.page(eq("internal-token"), any()))
|
||||
.thenReturn(ApiResponse.success(new PageResult<>(List.of(), 3, 20, 0, 0)));
|
||||
stubStatusProjection(List.of(), 0);
|
||||
|
||||
statuses.forEach(status -> service.page(null, new UnifiedOrderQuery(
|
||||
"CC-2026", "FL-77", status, null, null, null, null, null,
|
||||
@@ -925,14 +914,14 @@ class UnifiedOrderQueryServiceTest {
|
||||
ArgumentCaptor<FirstlegUnifiedOrderClient.Query> captor =
|
||||
ArgumentCaptor.forClass(FirstlegUnifiedOrderClient.Query.class);
|
||||
verify(firstleg, org.mockito.Mockito.times(statuses.size()))
|
||||
.page(eq("internal-token"), captor.capture());
|
||||
.statusRows(eq("internal-token"), captor.capture());
|
||||
IntStream.range(0, statuses.size()).forEach(index ->
|
||||
assertThat(captor.getAllValues().get(index)).satisfies(request -> {
|
||||
assertThat(request.mainStatus()).isEqualTo(statuses.get(index));
|
||||
assertThat(request.mainStatus()).isNull();
|
||||
assertThat(request.keyword()).isEqualTo("CC-2026");
|
||||
assertThat(request.orderNo()).isEqualTo("FL-77");
|
||||
assertThat(request.page()).isEqualTo(3);
|
||||
assertThat(request.size()).isEqualTo(20);
|
||||
assertThat(request.page()).isEqualTo(1);
|
||||
assertThat(request.size()).isEqualTo(200);
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -944,14 +933,13 @@ class UnifiedOrderQueryServiceTest {
|
||||
LocalDate.of(2026, 8, 8), true, true, "UNRECORDED", "FBA-1",
|
||||
null, null, 1, 20, null);
|
||||
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)));
|
||||
stubStatusProjection(List.of(), 0);
|
||||
|
||||
service.page(10001L, query);
|
||||
|
||||
ArgumentCaptor<FirstlegUnifiedOrderClient.Query> captor =
|
||||
ArgumentCaptor.forClass(FirstlegUnifiedOrderClient.Query.class);
|
||||
verify(firstleg).page(eq("internal-token"), captor.capture());
|
||||
verify(firstleg).statusRows(eq("internal-token"), captor.capture());
|
||||
assertThat(captor.getValue()).satisfies(request -> {
|
||||
assertThat(request.inboundCode()).isEqualTo("RK-77");
|
||||
assertThat(request.internalOrderStatus()).isEqualTo("APPROVED");
|
||||
@@ -978,6 +966,101 @@ class UnifiedOrderQueryServiceTest {
|
||||
verify(firstleg, never()).page(any(), any());
|
||||
}
|
||||
|
||||
/** 全量重复投影和审核/完成优先不能污染其他页签,也不能受当前页限制。 */
|
||||
@Test
|
||||
void statusCountsDeduplicateAndHonorAuthoritativeStatesBeyondCurrentPage() {
|
||||
var pending = order(OrderStatus.SUBMITTED);
|
||||
var completed = order(OrderStatus.COMPLETED); completed.setId(78L);
|
||||
var active = order(OrderStatus.PROCESSING); active.setId(79L);
|
||||
when(orders.unifiedStatusOrders(eq(10001L), any(), any())).thenReturn(List.of(pending, completed));
|
||||
when(orders.statusAuthorities(eq(10001L), any())).thenReturn(List.of(pending, completed, active));
|
||||
var rows = new java.util.ArrayList<FirstlegUnifiedOrderClient.StatusRow>();
|
||||
rows.add(new FirstlegUnifiedOrderClient.StatusRow(10001L, 77L, "WAIT_MEASURE"));
|
||||
rows.add(new FirstlegUnifiedOrderClient.StatusRow(10001L, 77L, "LOADING"));
|
||||
rows.add(new FirstlegUnifiedOrderClient.StatusRow(10001L, 78L, "LOADED"));
|
||||
rows.add(new FirstlegUnifiedOrderClient.StatusRow(10001L, 79L, "WAIT_SPLIT"));
|
||||
rows.add(new FirstlegUnifiedOrderClient.StatusRow(10001L, 79L, "LOADING"));
|
||||
rows.add(new FirstlegUnifiedOrderClient.StatusRow(10001L, 80L, "LOADED"));
|
||||
when(firstleg.statusRows(eq("internal-token"), any())).thenReturn(ApiResponse.success(rows));
|
||||
var result = service.statusCounts(10001L, taxQuery("LOADED", "TAX_REFUND"));
|
||||
assertThat(result.counts()).containsEntry("PENDING_REVIEW", 1L).containsEntry("COMPLETED", 1L)
|
||||
.containsEntry("WAIT_SPLIT", 1L).containsEntry("LOADING", 1L).containsEntry("LOADED", 0L)
|
||||
.containsEntry("WAIT_MEASURE", 0L).containsEntry("MEASUREMENT_REVIEW", 0L).doesNotContainKeys("ALL");
|
||||
verify(firstleg, never()).summaries(any(), any());
|
||||
verify(firstleg, never()).page(any(), any());
|
||||
}
|
||||
|
||||
/** 履约条件下的待审核订单保留在审核页签,先校正再分页。 */
|
||||
@Test
|
||||
void operationalFiltersKeepPendingReviewAndMatchCountsToListTotal() {
|
||||
var query = new UnifiedOrderQuery(null, null, "PENDING_REVIEW", null, null, null, null, null,
|
||||
null, null, null, null, null, null, null, null, "PENDING", null, null, null, 1, 1, null);
|
||||
var pending = order(OrderStatus.SUBMITTED);
|
||||
when(orders.statusAuthorities(eq(10001L), any())).thenReturn(List.of(pending));
|
||||
when(orders.findByIds(eq(10001L), any())).thenReturn(List.of(pending));
|
||||
when(firstleg.statusRows(eq("internal-token"), any())).thenReturn(ApiResponse.success(List.of(
|
||||
new FirstlegUnifiedOrderClient.StatusRow(10001L, 77L, "WAIT_MEASURE"))));
|
||||
var projected = summary("WAIT_MEASURE", null);
|
||||
when(firstleg.statusSummaries(eq("internal-token"), any())).thenReturn(ApiResponse.success(List.of(projected)));
|
||||
var counts = service.statusCounts(10001L, query);
|
||||
var page = service.page(10001L, query);
|
||||
assertThat(counts.counts()).containsEntry("PENDING_REVIEW", 1L).containsEntry("WAIT_MEASURE", 0L);
|
||||
assertThat(page.total()).isEqualTo(counts.counts().get("PENDING_REVIEW"));
|
||||
assertThat(page.records()).singleElement().satisfies(row -> {
|
||||
assertThat(row.mainStatus()).isEqualTo("PENDING_REVIEW");
|
||||
assertThat(row.nextAction()).isEqualTo("APPROVE_ORDER");
|
||||
});
|
||||
}
|
||||
|
||||
/** 错误响应和跨客户投影均失败关闭,不能返回看似可信的零。 */
|
||||
@Test
|
||||
void statusCountsRejectDependencyFailuresAndForeignCustomerRows() {
|
||||
when(firstleg.statusRows(eq("internal-token"), any())).thenReturn(ApiResponse.failure(ResultCode.SERVICE_UNAVAILABLE, "offline", null, null));
|
||||
assertServiceUnavailable(() -> service.statusCounts(10001L, query(null)));
|
||||
when(firstleg.statusRows(eq("internal-token"), any())).thenReturn(ApiResponse.success(List.of(
|
||||
new FirstlegUnifiedOrderClient.StatusRow(20002L, 77L, "LOADED"))));
|
||||
assertServiceUnavailable(() -> service.statusCounts(10001L, query(null)));
|
||||
}
|
||||
|
||||
/** 超过分页上限的匹配订单仍全量计数,权威状态校验采用有界批次。 */
|
||||
@Test
|
||||
void countsAll1205OrdersUsingThreeAuthorityBatches() {
|
||||
var rows = IntStream.range(0, 1205).mapToObj(index ->
|
||||
new FirstlegUnifiedOrderClient.StatusRow(10001L, (long) index, "LOADED")).toList();
|
||||
when(firstleg.statusRows(eq("internal-token"), any())).thenReturn(ApiResponse.success(rows));
|
||||
when(orders.statusAuthorities(eq(10001L), any())).thenAnswer(invocation -> {
|
||||
List<Long> ids = invocation.getArgument(1);
|
||||
return ids.stream().map(id -> {
|
||||
var item = order(OrderStatus.PROCESSING); item.setId(id); return item;
|
||||
}).toList();
|
||||
});
|
||||
assertThat(service.statusCounts(10001L, taxQuery("WAIT_SPLIT", null)).counts())
|
||||
.containsEntry("LOADED", 1205L).containsEntry("WAIT_SPLIT", 0L);
|
||||
verify(orders, org.mockito.Mockito.times(3)).statusAuthorities(eq(10001L), any());
|
||||
}
|
||||
|
||||
/** 使用轻量投影准备全量匹配集,摘要仍只读取当前页。 */
|
||||
private void stubStatusProjection(List<FirstlegUnifiedOrderClient.Summary> summaries, long total) {
|
||||
var rows = new java.util.ArrayList<FirstlegUnifiedOrderClient.StatusRow>();
|
||||
for (int i = summaries.size(); i < total; i++) {
|
||||
var sample = summaries.get(0);
|
||||
rows.add(new FirstlegUnifiedOrderClient.StatusRow(sample.inbound().tenantId(), 1000L + i, sample.mainStatus()));
|
||||
}
|
||||
summaries.forEach(summary -> rows.add(new FirstlegUnifiedOrderClient.StatusRow(
|
||||
summary.inbound().tenantId(), summary.inbound().orderId(), summary.mainStatus())));
|
||||
when(firstleg.statusRows(eq("internal-token"), any())).thenReturn(ApiResponse.success(rows));
|
||||
when(firstleg.statusSummaries(eq("internal-token"), any())).thenReturn(ApiResponse.success(summaries));
|
||||
when(orders.statusAuthorities(any(), any())).thenAnswer(invocation -> {
|
||||
List<Long> ids = invocation.getArgument(1);
|
||||
return rows.stream().filter(row -> ids.contains(row.orderId())).map(row -> {
|
||||
OrderEntity authority = order(OrderStatus.PROCESSING);
|
||||
authority.setId(row.orderId());
|
||||
authority.setTenantId(row.tenantId());
|
||||
return authority;
|
||||
}).toList();
|
||||
});
|
||||
}
|
||||
|
||||
private UnifiedOrderQuery query(String status) {
|
||||
return new UnifiedOrderQuery(null, null, status, null, null, null,
|
||||
null, null, null, null, null, null, null, null, null, null, null, null,
|
||||
@@ -1000,15 +1083,14 @@ class UnifiedOrderQueryServiceTest {
|
||||
service.page(10001L, direct);
|
||||
verify(orders).unifiedPage(eq(10001L), eq(direct), any());
|
||||
org.mockito.Mockito.verify(firstleg, org.mockito.Mockito.never()).page(any(), any());
|
||||
when(firstleg.page(eq("internal-token"), any()))
|
||||
.thenReturn(ApiResponse.success(new PageResult<>(List.of(), 2, 1, 0, 0)));
|
||||
stubStatusProjection(List.of(), 0);
|
||||
service.page(10001L, taxQuery("LOADED", taxMode));
|
||||
ArgumentCaptor<FirstlegUnifiedOrderClient.Query> captured = ArgumentCaptor.forClass(FirstlegUnifiedOrderClient.Query.class);
|
||||
verify(firstleg).page(eq("internal-token"), captured.capture());
|
||||
verify(firstleg).statusRows(eq("internal-token"), captured.capture());
|
||||
assertThat(captured.getValue().taxMode()).isEqualTo(taxMode);
|
||||
assertThat(captured.getValue().tenantId()).isEqualTo(10001L);
|
||||
assertThat(captured.getValue().page()).isEqualTo(2);
|
||||
assertThat(captured.getValue().size()).isEqualTo(1);
|
||||
assertThat(captured.getValue().page()).isEqualTo(1);
|
||||
assertThat(captured.getValue().size()).isEqualTo(200);
|
||||
}
|
||||
|
||||
@org.junit.jupiter.params.ParameterizedTest
|
||||
|
||||
Reference in New Issue
Block a user