This commit is contained in:
2026-08-17 19:13:08 +08:00
parent 5199146e97
commit cddd23e4c7
40 changed files with 1203 additions and 71 deletions
@@ -47,8 +47,13 @@ public interface FirstlegUnifiedOrderClient {
Long customerId, String customerName, Long warehouseId, String warehouseName,
Long deliveryWarehouseId, String deliveryWarehouseName, String channelName,
Integer totalPackageCount, String locationCodes,
BigDecimal actualGrossWeightKg, BigDecimal actualVolumeCbm,
LocalDateTime expectedArrivalAt, String feeReviewStatus,
String companyName, String businessType, String taxMode, String containerMode,
Boolean customsInspection, String destinationType, String destinationCode,
BigDecimal actualGrossWeightKg, BigDecimal actualNetWeightKg, BigDecimal actualVolumeCbm,
BigDecimal volumeWeightKg, BigDecimal chargeableVolumeCbm, Integer palletCount,
BigDecimal chargeableWeightKg, LocalDateTime measureCompletedAt,
LocalDateTime inboundedAt, String expressCompanyName, String createdByName,
String loadingStatus, String containerNo, String cabinetNo, String vesselName, String voyageNo,
LocalDateTime sailingAt, LocalDateTime portArrivalAt,
String expressTrackingNo, LocalDateTime createdAt, LocalDateTime updatedAt) {}
@@ -21,6 +21,15 @@ public final class UnifiedOrderResponse {
Long inboundId, Integer inboundVersion, String inboundCode, LocalDate expectedShipDate,
Integer totalPackageCount, String recommendedPalletType, String locationCodes,
BigDecimal actualGrossWeightKg, BigDecimal actualVolumeCbm,
LocalDateTime expectedArrivalAt, String feeReviewStatus,
String companyName, String businessType, String pickupMethod,
String taxMode, String containerMode, Boolean customsInspection,
String destinationType, String destinationCode,
Integer totalPalletCount, BigDecimal chargeableVolumeCbm,
BigDecimal volumetricWeightKg, BigDecimal totalNetWeightKg,
LocalDateTime measurementCompletedAt, LocalDateTime inboundCompletedAt,
String expressCompany, String expressTrackingNo, String createdBy,
List<String> skuNos, Integer totalQuantity, String shipmentId, String referenceId,
MeasurementSummary measurement, PreloadSummary preload,
LoadingSummary loading, DeliverySummary delivery, TransportSummary transport,
String customerRemark, String latestInternalNote,
@@ -9,6 +9,7 @@ import com.baiyue.order.dto.OrderDetailResponse;
import com.baiyue.order.dto.UnifiedOrderQuery;
import com.baiyue.order.dto.UnifiedOrderResponse;
import com.baiyue.order.entity.OrderEntity;
import com.baiyue.order.entity.OrderCargoEntity;
import com.baiyue.order.entity.OrderInternalNoteEntity;
import com.baiyue.order.entity.OrderStatus;
import com.baiyue.order.repository.OrderRepository;
@@ -23,6 +24,7 @@ import java.util.Comparator;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -56,9 +58,12 @@ public class UnifiedOrderQueryService {
? Map.of()
: summaries(tenantId, source.records().stream().map(OrderEntity::getId).toList());
Map<Long, String> latestNotes = latestNotes(tenantId, source.records().stream().map(OrderEntity::getId).toList());
Map<Long, CargoSummary> cargoSummaries = cargoSummaries(tenantId,
source.records().stream().map(OrderEntity::getId).toList());
Map<Long, Integer> childCounts = childCounts(source.records());
List<UnifiedOrderResponse.Row> rows = source.records().stream()
.map(order -> row(order, inboundByOrder.get(order.getId()), latestNotes.get(order.getId()), childCounts)).toList();
.map(order -> row(order, inboundByOrder.get(order.getId()), latestNotes.get(order.getId()),
childCounts, cargoSummaries.get(order.getId()))).toList();
return new PageResult<>(rows, source.page(), source.size(), source.total(), source.pages());
}
@@ -72,7 +77,7 @@ public class UnifiedOrderQueryService {
? null : firstlegDetail(tenantId, orderId);
String latestNote = orderDetail.internalNotes().isEmpty() ? null : orderDetail.internalNotes().get(0).getContent();
UnifiedOrderResponse.Row summary = row(order, inbound == null ? null : inbound.summary(), latestNote,
childCounts(List.of(order)));
childCounts(List.of(order)), CargoSummary.from(orderDetail.cargoes()));
List<UnifiedOrderResponse.TimelineEvent> timeline = new ArrayList<>();
orderDetail.statusHistory().forEach(item -> timeline.add(new UnifiedOrderResponse.TimelineEvent(
"ORDER", "STATUS", orderMainStatus(item.getToStatus()), item.getAction(), item.getReason(),
@@ -106,10 +111,12 @@ public class UnifiedOrderQueryService {
Map<Long, OrderEntity> orderById = orders.findByIds(tenantId, orderIds).stream()
.collect(Collectors.toMap(OrderEntity::getId, Function.identity(), (left, right) -> left));
Map<Long, String> latestNotes = latestNotes(tenantId, orderIds);
Map<Long, CargoSummary> cargoSummaries = cargoSummaries(tenantId, orderIds);
Map<Long, Integer> childCounts = childCounts(List.copyOf(orderById.values()));
List<UnifiedOrderResponse.Row> records = source.records().stream()
.map(summary -> row(summary, orderById.get(summary.inbound().orderId()),
latestNotes.get(summary.inbound().orderId()), childCounts))
latestNotes.get(summary.inbound().orderId()), childCounts,
cargoSummaries.get(summary.inbound().orderId())))
.toList();
return new PageResult<>(records, source.page(), source.size(), source.total(), source.pages());
}
@@ -134,6 +141,13 @@ public class UnifiedOrderQueryService {
OrderInternalNoteEntity::getOrderId, OrderInternalNoteEntity::getContent, (latest, ignored) -> latest));
}
private Map<Long, CargoSummary> cargoSummaries(Long tenantId, List<Long> orderIds) {
if (orderIds.isEmpty()) return Map.of();
return orders.findCargoByOrderIds(tenantId, orderIds).stream()
.collect(Collectors.groupingBy(OrderCargoEntity::getOrderId,
Collectors.collectingAndThen(Collectors.toList(), CargoSummary::from)));
}
private Map<Long, Integer> childCounts(List<OrderEntity> pageOrders) {
if (pageOrders.isEmpty()) return Map.of();
return orders.findChildrenByParentOrders(pageOrders).stream()
@@ -155,7 +169,8 @@ public class UnifiedOrderQueryService {
}
private UnifiedOrderResponse.Row row(OrderEntity order, FirstlegUnifiedOrderClient.Summary summary,
String latestInternalNote, Map<Long, Integer> childCounts) {
String latestInternalNote, Map<Long, Integer> childCounts,
CargoSummary cargo) {
if (summary == null) {
String mainStatus = orderMainStatus(order.getStatus());
return new UnifiedOrderResponse.Row(order.getId(), order.getTenantId(), order.getOrderNo(), mainStatus,
@@ -165,7 +180,12 @@ public class UnifiedOrderQueryService {
order.getChannelName(), order.getReceivingWarehouseId(), order.getReceivingWarehouseName(),
order.getDestinationWarehouseId(), order.getDestinationWarehouseName(), null, null,
order.getInboundCode(),
order.getExpectedShipDate(), null, null, null, null, null, null, null, null, null, null,
order.getExpectedShipDate(), null, null, null, null,
null, null, null, null, null, order.getPickupMethod(), order.getTaxMode(), order.getContainerMode(),
order.getCustomsInspection(), order.getDestinationType(), order.getAmazonWarehouseCode(),
null, null, null, null, null, null, order.getExpressCompany(), order.getExpressTrackingNo(),
string(order.getCreatedBy()), skuNos(cargo), totalQuantity(cargo), order.getShipmentId(), order.getReferenceId(),
null, null, null, null, null,
order.getCustomerRemark(), latestInternalNote,
order.getCreatedAt(), order.getUpdatedAt());
}
@@ -175,15 +195,21 @@ public class UnifiedOrderQueryService {
order.getCustomerId(), order.getCustomerName(),
order.getChannelName(), order.getReceivingWarehouseId(), order.getReceivingWarehouseName(),
order.getDestinationWarehouseId(), order.getDestinationWarehouseName(), inbound.id(), inbound.version(), inbound.inboundCode(),
order.getExpectedShipDate(), inbound.totalPackageCount(), "STANDARD", inbound.locationCodes(),
inbound.actualGrossWeightKg(), inbound.actualVolumeCbm(), measurement(inbound),
order.getExpectedShipDate(), inbound.totalPackageCount(), null, inbound.locationCodes(),
inbound.actualGrossWeightKg(), inbound.actualVolumeCbm(), inbound.expectedArrivalAt(), inbound.feeReviewStatus(),
inbound.companyName(), inbound.businessType(), order.getPickupMethod(), inbound.taxMode(), inbound.containerMode(),
inbound.customsInspection(), inbound.destinationType(), inbound.destinationCode(), inbound.palletCount(),
inbound.chargeableVolumeCbm(), inbound.volumeWeightKg(), inbound.actualNetWeightKg(),
inbound.measureCompletedAt(), inbound.inboundedAt(), inbound.expressCompanyName(), inbound.expressTrackingNo(),
inbound.createdByName(), skuNos(cargo), totalQuantity(cargo), order.getShipmentId(), order.getReferenceId(), measurement(inbound),
preload(summary.preload()), loading(summary.loading()),
delivery(summary.delivery()), transport(inbound), order.getCustomerRemark(), latestInternalNote,
order.getCreatedAt(), order.getUpdatedAt());
}
private UnifiedOrderResponse.Row row(FirstlegUnifiedOrderClient.Summary summary, OrderEntity order,
String latestInternalNote, Map<Long, Integer> childCounts) {
String latestInternalNote, Map<Long, Integer> childCounts,
CargoSummary cargo) {
var inbound = summary.inbound();
return new UnifiedOrderResponse.Row(inbound.orderId(), inbound.tenantId(), inbound.orderNo(),
summary.mainStatus(), summary.nextAction(), relation(order, childCounts),
@@ -191,8 +217,14 @@ public class UnifiedOrderQueryService {
inbound.customerId(), inbound.customerName(), inbound.channelName(), inbound.warehouseId(),
inbound.warehouseName(), inbound.deliveryWarehouseId(), inbound.deliveryWarehouseName(),
inbound.id(), inbound.version(), inbound.inboundCode(), null, inbound.totalPackageCount(),
"STANDARD", inbound.locationCodes(), inbound.actualGrossWeightKg(),
inbound.actualVolumeCbm(), measurement(inbound), preload(summary.preload()), loading(summary.loading()),
null, inbound.locationCodes(), inbound.actualGrossWeightKg(),
inbound.actualVolumeCbm(), inbound.expectedArrivalAt(), inbound.feeReviewStatus(), inbound.companyName(),
inbound.businessType(), order == null ? null : order.getPickupMethod(), inbound.taxMode(), inbound.containerMode(),
inbound.customsInspection(), inbound.destinationType(), inbound.destinationCode(), inbound.palletCount(),
inbound.chargeableVolumeCbm(), inbound.volumeWeightKg(), inbound.actualNetWeightKg(),
inbound.measureCompletedAt(), inbound.inboundedAt(), inbound.expressCompanyName(), inbound.expressTrackingNo(),
inbound.createdByName(), skuNos(cargo), totalQuantity(cargo), order == null ? null : order.getShipmentId(),
order == null ? null : order.getReferenceId(), measurement(inbound), preload(summary.preload()), loading(summary.loading()),
delivery(summary.delivery()), transport(inbound), order == null ? null : order.getCustomerRemark(), latestInternalNote,
inbound.createdAt(), inbound.updatedAt());
}
@@ -225,6 +257,28 @@ public class UnifiedOrderQueryService {
return status == OrderStatus.SUBMITTED ? "PENDING_REVIEW"
: ACTIVE_ORDER_STATUSES.contains(status) ? "WAIT_MEASURE" : null;
}
private String string(Long value) {
return value == null ? null : value.toString();
}
private List<String> skuNos(CargoSummary cargo) {
return cargo == null || cargo.skuNos().isEmpty() ? null : cargo.skuNos();
}
private Integer totalQuantity(CargoSummary cargo) {
return cargo == null ? null : cargo.totalQuantity();
}
private record CargoSummary(List<String> skuNos, Integer totalQuantity) {
private static CargoSummary from(List<OrderCargoEntity> cargoes) {
List<String> skuNos = cargoes.stream().map(OrderCargoEntity::getSkuNo)
.filter(Objects::nonNull).map(String::trim).filter(value -> !value.isEmpty()).distinct().toList();
int totalQuantity = cargoes.stream().map(OrderCargoEntity::getTotalQuantity)
.filter(Objects::nonNull).mapToInt(Integer::intValue).sum();
return new CargoSummary(skuNos, totalQuantity);
}
}
private String inboundMainStatus(String status) {
if (status == null) return null;
return switch (status) {
+24 -1
View File
@@ -424,10 +424,33 @@ components:
inboundCode: {type: string, nullable: true}
expectedShipDate: {type: string, format: date, nullable: true}
totalPackageCount: {type: integer, nullable: true}
recommendedPalletType: {type: string, nullable: true}
recommendedPalletType: {type: string, nullable: true, description: 当前无列表级权威事实时返回 null,不得返回固定托盘类型}
locationCodes: {type: string, nullable: true}
actualGrossWeightKg: {type: number, nullable: true}
actualVolumeCbm: {type: number, nullable: true}
expectedArrivalAt: {type: string, format: date-time, nullable: true}
feeReviewStatus: {type: string, nullable: true, description: Firstleg 入仓费用审核快照;财务状态不在本字段表达}
companyName: {type: string, nullable: true}
businessType: {type: string, nullable: true}
pickupMethod: {type: string, nullable: true}
taxMode: {type: string, nullable: true}
containerMode: {type: string, nullable: true}
customsInspection: {type: boolean, nullable: true}
destinationType: {type: string, nullable: true}
destinationCode: {type: string, nullable: true}
totalPalletCount: {type: integer, nullable: true}
chargeableVolumeCbm: {type: number, nullable: true}
volumetricWeightKg: {type: number, nullable: true}
totalNetWeightKg: {type: number, nullable: true, description: Firstleg 确认后的实际净重;未确认时返回 null}
measurementCompletedAt: {type: string, format: date-time, nullable: true}
inboundCompletedAt: {type: string, format: date-time, nullable: true}
expressCompany: {type: string, nullable: true}
expressTrackingNo: {type: string, nullable: true}
createdBy: {type: string, nullable: true}
skuNos: {type: array, nullable: true, items: {type: string}}
totalQuantity: {type: integer, nullable: true}
shipmentId: {type: string, nullable: true}
referenceId: {type: string, nullable: true}
measurement: {$ref: '#/components/schemas/UnifiedMeasurementSummary'}
preload: {$ref: '#/components/schemas/UnifiedPreloadSummary'}
loading: {$ref: '#/components/schemas/UnifiedLoadingSummary'}
@@ -52,7 +52,12 @@ class UnifiedOrderApiContractTest {
Map<String, Object> row = stringMap(schemas.get("UnifiedOrderRow"));
assertThat(stringMap(row.get("properties"))).containsKeys(
"orderId", "mainStatus", "inboundId", "inboundVersion", "measurement", "preload",
"loading", "delivery", "transport", "customerRemark", "latestInternalNote");
"loading", "delivery", "transport", "customerRemark", "latestInternalNote",
"expectedArrivalAt", "feeReviewStatus", "companyName", "businessType", "pickupMethod",
"taxMode", "containerMode", "customsInspection", "destinationType", "destinationCode",
"totalPalletCount", "chargeableVolumeCbm", "volumetricWeightKg", "totalNetWeightKg",
"measurementCompletedAt", "inboundCompletedAt", "expressCompany", "expressTrackingNo",
"createdBy", "skuNos", "totalQuantity", "shipmentId", "referenceId");
assertThat(responseSchema(paths, "/api/platform/v1/orders", "get"))
.containsEntry("$ref", "#/components/schemas/UnifiedOrderPageEnvelope");
assertThat(responseSchema(paths, "/api/merchant/v1/orders/{orderId}", "get"))
@@ -77,8 +77,8 @@ class UnifiedOrderQueryServiceTest {
void pendingReviewInboundCodeFilterStaysInOrderQuery() {
UnifiedOrderQuery query = new UnifiedOrderQuery(
null, null, "PENDING_REVIEW", null, null, null, "RK-77", null,
null, null, null, null, null, null, null, null, null, null,
null, null, 1, 20);
null, null, null, null, null, null, null, null, null,
null, null, null, 1, 20);
when(orders.unifiedPage(eq(10001L), eq(query), eq(List.of(OrderStatus.SUBMITTED))))
.thenReturn(new PageResult<>(List.of(), 1, 20, 0, 0));
@@ -167,7 +167,7 @@ class UnifiedOrderQueryServiceTest {
assertThat(row.loading().loadingVersion()).isEqualTo(3);
assertThat(row.loading().pickingVersion()).isEqualTo(3);
assertThat(row.inboundCode()).isEqualTo("RK-77");
assertThat(row.recommendedPalletType()).isEqualTo("STANDARD");
assertThat(row.recommendedPalletType()).isNull();
assertThat(row.locationCodes()).isEqualTo("A0-01, B2-03");
});
verify(orders, never()).unifiedPage(any(), any(), any());
@@ -192,6 +192,53 @@ class UnifiedOrderQueryServiceTest {
assertThat(captor.getValue().mainStatus()).isEqualTo("WAIT_SPLIT");
}
@Test
void listKeepsLoadedFulfillmentAheadOfRawApprovedOrderStatus() {
UnifiedOrderQuery query = query(null);
OrderEntity order = order(OrderStatus.APPROVED);
var summary = summary("LOADED", null);
when(orders.unifiedPage(eq(10001L), eq(query), any()))
.thenReturn(new PageResult<>(List.of(order), 1, 20, 1, 1));
when(firstleg.summaries(eq("internal-token"), any()))
.thenReturn(ApiResponse.success(List.of(summary)));
var result = service.page(10001L, query);
assertThat(result.records()).singleElement().satisfies(row -> {
assertThat(row.mainStatus()).isEqualTo("LOADED");
assertThat(row.internalOrderStatus()).isEqualTo(OrderStatus.APPROVED);
assertThat(row.inboundId()).isEqualTo(88L);
});
ArgumentCaptor<FirstlegUnifiedOrderClient.BatchRequest> captor =
ArgumentCaptor.forClass(FirstlegUnifiedOrderClient.BatchRequest.class);
verify(firstleg).summaries(eq("internal-token"), captor.capture());
assertThat(captor.getValue().tenantId()).isEqualTo(10001L);
assertThat(captor.getValue().orderIds()).containsExactly(77L);
}
@Test
void detailKeepsLoadedFulfillmentAheadOfRawApprovedOrderStatus() {
OrderEntity order = order(OrderStatus.APPROVED);
when(orders.findOwned(10001L, 77L)).thenReturn(order);
when(orders.findCargo(10001L, 77L)).thenReturn(List.of());
when(orders.findParties(10001L, 77L)).thenReturn(List.of());
when(orders.findNotes(10001L, 77L)).thenReturn(List.of());
when(orders.findHistory(10001L, 77L)).thenReturn(List.of());
when(firstleg.detail(eq("internal-token"), any())).thenReturn(ApiResponse.success(
new FirstlegUnifiedOrderClient.Detail(summary("LOADED", null), List.of())));
var result = service.detail(10001L, 77L, false);
assertThat(result.summary().mainStatus()).isEqualTo("LOADED");
assertThat(result.summary().internalOrderStatus()).isEqualTo(OrderStatus.APPROVED);
assertThat(result.summary().inboundId()).isEqualTo(88L);
ArgumentCaptor<FirstlegUnifiedOrderClient.DetailRequest> captor =
ArgumentCaptor.forClass(FirstlegUnifiedOrderClient.DetailRequest.class);
verify(firstleg).detail(eq("internal-token"), captor.capture());
assertThat(captor.getValue().tenantId()).isEqualTo(10001L);
assertThat(captor.getValue().orderId()).isEqualTo(77L);
}
@Test
void fulfillmentTabsForwardStatusSearchAndPaginationToFirstleg() {
List<String> statuses = List.of(
@@ -278,6 +325,7 @@ class UnifiedOrderQueryServiceTest {
var inbound = new FirstlegUnifiedOrderClient.Inbound(
88L, 10001L, 77L, "FL-77", "RK-77", "INBOUNDED", "APPROVED", "APPROVED", 7,
30001L, "测试客户", 11L, "深圳仓", 22L, "ONT8", "美森", 10, "A0-01, B2-03", null, null, null,
null, null, null, null, null, null, null, null, null, null, null, null, null, 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));
return new FirstlegUnifiedOrderClient.Summary(inbound, status, action,