feat(lastleg): add container plan preview
This commit is contained in:
+14
@@ -4,6 +4,7 @@ import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.inboundplan.dto.ConvertContainerInboundPlanRequest;
|
||||
import com.baiyue.lastleg.inboundplan.dto.ContainerInboundPlanPreviewResponse;
|
||||
import com.baiyue.lastleg.inboundplan.dto.ManualInboundPlanRequest;
|
||||
import com.baiyue.lastleg.inboundplan.dto.BatchFbaAppointmentRequest;
|
||||
import com.baiyue.lastleg.inboundplan.dto.FbaAppointmentResponse;
|
||||
@@ -71,6 +72,19 @@ public class InboundPlanController {
|
||||
return ApiResponse.success(service.convertContainer(request.sourceContainerBookingId()));
|
||||
}
|
||||
|
||||
@GetMapping("/container-preview")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.INBOUND_ORDER_LIST + "')")
|
||||
@Operation(summary = "预览按柜生成入库计划", description = "按稳定 sourceContainerBookingId 返回全柜服务端分组、托数、就绪与阻断事实;确认时必须回传 snapshotVersion/snapshotHash。Required authority: "
|
||||
+ OverseasPermissions.INBOUND_ORDER_LIST)
|
||||
public ApiResponse<ContainerInboundPlanPreviewResponse> previewContainer(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam String sourceContainerBookingId) {
|
||||
if (user == null || user.tenantId() == null || user.tenantId() != 0L || !"PLATFORM".equals(user.appCode())) {
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "仅平台账号可预览整柜入库计划");
|
||||
}
|
||||
return ApiResponse.success(service.previewContainer(sourceContainerBookingId));
|
||||
}
|
||||
|
||||
@GetMapping("/{planNo}")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.INBOUND_ORDER_LIST + "')")
|
||||
/** 查询入仓计划及其明细、托盘和分配信息。 */
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package com.baiyue.lastleg.inboundplan.dto;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/** Server-calculated cabinet conversion preview; callers must echo version/hash when confirming. */
|
||||
public record ContainerInboundPlanPreviewResponse(
|
||||
String sourceContainerBookingId,
|
||||
String containerNo,
|
||||
String overseasWarehouseId,
|
||||
String overseasWarehouseCode,
|
||||
String overseasWarehouseName,
|
||||
long snapshotVersion,
|
||||
String snapshotHash,
|
||||
int totalOrderCount,
|
||||
int readyOrderCount,
|
||||
int blockedOrderCount,
|
||||
int pendingOrderCount,
|
||||
int pendingPalletCount,
|
||||
int existingPlannedPalletCount,
|
||||
int totalSuggestedPalletCount,
|
||||
int totalPlannedPalletCount,
|
||||
boolean ready,
|
||||
List<Group> groups
|
||||
) {
|
||||
public record Group(
|
||||
String destinationType,
|
||||
String destinationWarehouseId,
|
||||
String destinationWarehouseCode,
|
||||
String destinationWarehouseName,
|
||||
int orderCount,
|
||||
int pendingOrderCount,
|
||||
int pendingPalletCount,
|
||||
int existingPlannedPalletCount,
|
||||
int suggestedPalletCount,
|
||||
int plannedPalletCount,
|
||||
List<Order> orders
|
||||
) { }
|
||||
|
||||
public record Order(
|
||||
String customerId,
|
||||
String customerName,
|
||||
String orderId,
|
||||
String businessOrderNo,
|
||||
String inboundCode,
|
||||
String orderType,
|
||||
String destinationType,
|
||||
String destinationWarehouseId,
|
||||
String destinationWarehouseCode,
|
||||
String destinationWarehouseName,
|
||||
int packageCount,
|
||||
BigDecimal volumeCbm,
|
||||
Integer suggestedPalletCount,
|
||||
Integer plannedPalletCount,
|
||||
String existingPlanNo,
|
||||
String planStatus,
|
||||
int orderVersion,
|
||||
String readiness,
|
||||
List<String> blockers
|
||||
) { }
|
||||
}
|
||||
+27
@@ -18,6 +18,33 @@ public interface InboundPlanOrderMapper {
|
||||
String overseasWarehouseName) { }
|
||||
record ContainerWarehouseRef(String customerId, Long orderId, String sourceContainerBookingId,
|
||||
String overseasWarehouseId) { }
|
||||
record ContainerPreviewOrderRow(
|
||||
String customerId, Long orderId, String orderNo, String sourceOrderNo, String customerName,
|
||||
String orderType, String destinationType, String inboundCode, String currentStatus,
|
||||
String inboundOrderNo, String containerNo, int recommendedPalletCount, int packageCount,
|
||||
java.math.BigDecimal volumeCbm, String overseasWarehouseId, String overseasWarehouseCode,
|
||||
String overseasWarehouseName, String fbaWarehouseCode, String destinationWarehouseId,
|
||||
String destinationWarehouseCode, String destinationWarehouseName, int destinationCount,
|
||||
boolean detained, int version, String planNo, String planStatus, Integer plannedPalletCount
|
||||
) { }
|
||||
|
||||
@Select("SELECT o.customer_id AS customerId,o.id AS orderId,o.order_no AS orderNo," +
|
||||
"o.source_order_no AS sourceOrderNo,o.customer_name AS customerName,o.order_type AS orderType," +
|
||||
"o.destination_type AS destinationType,o.inbound_code AS inboundCode,o.current_status AS currentStatus," +
|
||||
"o.inbound_order_no AS inboundOrderNo,o.container_no AS containerNo,COALESCE(o.pallet_count,0) AS recommendedPalletCount," +
|
||||
"COALESCE(o.package_count,0) AS packageCount,COALESCE(o.volume_cbm,0) AS volumeCbm," +
|
||||
"o.overseas_warehouse_id AS overseasWarehouseId,o.overseas_warehouse_code AS overseasWarehouseCode," +
|
||||
"o.overseas_warehouse_name AS overseasWarehouseName,o.fba_warehouse_code AS fbaWarehouseCode," +
|
||||
"COALESCE(NULLIF(o.transfer_warehouse_id,''),(SELECT MIN(NULLIF(d.destination_warehouse_id,'')) FROM ll_order_detail d WHERE d.order_id=o.id)) AS destinationWarehouseId," +
|
||||
"COALESCE(NULLIF(o.transfer_warehouse_code,''),(SELECT MIN(NULLIF(d.destination_warehouse_code,'')) FROM ll_order_detail d WHERE d.order_id=o.id)) AS destinationWarehouseCode," +
|
||||
"COALESCE(NULLIF(o.transfer_warehouse_name,''),(SELECT MIN(NULLIF(d.destination_warehouse_name,'')) FROM ll_order_detail d WHERE d.order_id=o.id)) AS destinationWarehouseName," +
|
||||
"CASE WHEN COALESCE(NULLIF(o.transfer_warehouse_id,''),NULLIF(o.transfer_warehouse_code,'')) IS NOT NULL THEN 1 ELSE " +
|
||||
"(SELECT COUNT(DISTINCT COALESCE(NULLIF(d.destination_warehouse_id,''),NULLIF(d.destination_warehouse_code,''),'#EMPTY#')) FROM ll_order_detail d WHERE d.order_id=o.id) END AS destinationCount," +
|
||||
"o.detained AS detained,o.version AS version,p.plan_no AS planNo,p.status AS planStatus,p.pallet_count AS plannedPalletCount " +
|
||||
"FROM ll_order o LEFT JOIN ll_inbound_plan p ON p.customer_id=o.customer_id AND p.source_order_id=o.id " +
|
||||
"WHERE o.source_container_booking_id=#{sourceContainerBookingId} ORDER BY o.customer_id,o.id")
|
||||
List<ContainerPreviewOrderRow> containerPreviewOrders(
|
||||
@Param("sourceContainerBookingId") String sourceContainerBookingId);
|
||||
|
||||
@Select("SELECT c.customer_id AS customerId,c.id AS orderId," +
|
||||
"c.source_container_booking_id AS sourceContainerBookingId,c.overseas_warehouse_id AS overseasWarehouseId " +
|
||||
|
||||
+147
@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.lastleg.inboundplan.dto.InboundPlanListResponse;
|
||||
import com.baiyue.lastleg.inboundplan.dto.ContainerInboundPlanPreviewResponse;
|
||||
import com.baiyue.lastleg.inboundplan.dto.InboundPlanRequests;
|
||||
import com.baiyue.lastleg.inboundplan.dto.InboundPlanResponse;
|
||||
import com.baiyue.lastleg.inboundplan.dto.ManualInboundPlanRequest;
|
||||
@@ -46,7 +47,9 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@@ -134,6 +137,150 @@ public class InboundPlanService {
|
||||
return new PageResult<>(records, query.page(), query.size(), total, pages);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public ContainerInboundPlanPreviewResponse previewContainer(String sourceContainerBookingId) {
|
||||
String bookingId = trimToNull(sourceContainerBookingId);
|
||||
if (bookingId == null) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "来源柜预订 ID 不能为空");
|
||||
}
|
||||
List<InboundPlanOrderMapper.ContainerPreviewOrderRow> rows = orderMapper.containerPreviewOrders(bookingId);
|
||||
if (rows == null || rows.isEmpty()) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "来源柜没有尾程订单");
|
||||
}
|
||||
String containerNo = rows.stream().map(InboundPlanOrderMapper.ContainerPreviewOrderRow::containerNo)
|
||||
.map(InboundPlanService::trimToNull).filter(Objects::nonNull).findFirst().orElse(null);
|
||||
String warehouseId = singleValue(rows.stream()
|
||||
.map(InboundPlanOrderMapper.ContainerPreviewOrderRow::overseasWarehouseId).toList());
|
||||
String warehouseCode = singleValue(rows.stream()
|
||||
.map(InboundPlanOrderMapper.ContainerPreviewOrderRow::overseasWarehouseCode).toList());
|
||||
String warehouseName = singleValue(rows.stream()
|
||||
.map(InboundPlanOrderMapper.ContainerPreviewOrderRow::overseasWarehouseName).toList());
|
||||
|
||||
List<ContainerInboundPlanPreviewResponse.Order> orders = rows.stream()
|
||||
.map(row -> previewOrder(row, containerNo, warehouseId)).toList();
|
||||
Comparator<ContainerInboundPlanPreviewResponse.Order> orderComparator = Comparator
|
||||
.comparing((ContainerInboundPlanPreviewResponse.Order item) -> typeRank(item.destinationType()))
|
||||
.thenComparing(item -> nullSafe(item.destinationWarehouseCode()))
|
||||
.thenComparing(item -> nullSafe(item.destinationWarehouseName()))
|
||||
.thenComparing(item -> nullSafe(item.destinationWarehouseId()))
|
||||
.thenComparing(ContainerInboundPlanPreviewResponse.Order::customerId)
|
||||
.thenComparing(ContainerInboundPlanPreviewResponse.Order::orderId);
|
||||
orders = orders.stream().sorted(orderComparator).toList();
|
||||
|
||||
Map<String, List<ContainerInboundPlanPreviewResponse.Order>> grouped = new LinkedHashMap<>();
|
||||
for (ContainerInboundPlanPreviewResponse.Order order : orders) {
|
||||
String key = order.destinationType() + "|" + nullSafe(order.destinationWarehouseId()) + "|"
|
||||
+ nullSafe(order.destinationWarehouseCode()) + "|" + nullSafe(order.destinationWarehouseName());
|
||||
grouped.computeIfAbsent(key, ignored -> new ArrayList<>()).add(order);
|
||||
}
|
||||
List<ContainerInboundPlanPreviewResponse.Group> groups = grouped.values().stream().map(items -> {
|
||||
ContainerInboundPlanPreviewResponse.Order first = items.get(0);
|
||||
return new ContainerInboundPlanPreviewResponse.Group(first.destinationType(),
|
||||
first.destinationWarehouseId(), first.destinationWarehouseCode(), first.destinationWarehouseName(),
|
||||
items.size(), (int) items.stream().filter(item -> "READY".equals(item.readiness())).count(),
|
||||
items.stream().filter(item -> "READY".equals(item.readiness()))
|
||||
.map(ContainerInboundPlanPreviewResponse.Order::suggestedPalletCount)
|
||||
.filter(Objects::nonNull).mapToInt(Integer::intValue).sum(),
|
||||
items.stream().filter(item -> "PLANNED".equals(item.readiness()))
|
||||
.map(ContainerInboundPlanPreviewResponse.Order::plannedPalletCount)
|
||||
.filter(Objects::nonNull).mapToInt(Integer::intValue).sum(),
|
||||
items.stream().map(ContainerInboundPlanPreviewResponse.Order::suggestedPalletCount)
|
||||
.filter(Objects::nonNull).mapToInt(Integer::intValue).sum(),
|
||||
items.stream().map(ContainerInboundPlanPreviewResponse.Order::plannedPalletCount)
|
||||
.filter(Objects::nonNull).mapToInt(Integer::intValue).sum(), List.copyOf(items));
|
||||
}).toList();
|
||||
int blocked = (int) orders.stream().filter(item -> "BLOCKED".equals(item.readiness())).count();
|
||||
int pendingOrders = (int) orders.stream().filter(item -> "READY".equals(item.readiness())).count();
|
||||
int pendingPallets = orders.stream().filter(item -> "READY".equals(item.readiness()))
|
||||
.map(ContainerInboundPlanPreviewResponse.Order::suggestedPalletCount)
|
||||
.filter(Objects::nonNull).mapToInt(Integer::intValue).sum();
|
||||
int existingPallets = orders.stream().filter(item -> "PLANNED".equals(item.readiness()))
|
||||
.map(ContainerInboundPlanPreviewResponse.Order::plannedPalletCount)
|
||||
.filter(Objects::nonNull).mapToInt(Integer::intValue).sum();
|
||||
long snapshotVersion = rows.stream().mapToLong(InboundPlanOrderMapper.ContainerPreviewOrderRow::version).max().orElse(0);
|
||||
String snapshotHash = sha256(List.of(bookingId, containerNo, warehouseId, snapshotVersion, orders));
|
||||
return new ContainerInboundPlanPreviewResponse(bookingId, containerNo, warehouseId, warehouseCode,
|
||||
warehouseName, snapshotVersion, snapshotHash, orders.size(), orders.size() - blocked, blocked,
|
||||
pendingOrders, pendingPallets, existingPallets,
|
||||
orders.stream().map(ContainerInboundPlanPreviewResponse.Order::suggestedPalletCount)
|
||||
.filter(Objects::nonNull).mapToInt(Integer::intValue).sum(),
|
||||
orders.stream().map(ContainerInboundPlanPreviewResponse.Order::plannedPalletCount)
|
||||
.filter(Objects::nonNull).mapToInt(Integer::intValue).sum(), blocked == 0, groups);
|
||||
}
|
||||
|
||||
private ContainerInboundPlanPreviewResponse.Order previewOrder(
|
||||
InboundPlanOrderMapper.ContainerPreviewOrderRow row, String containerNo, String warehouseId) {
|
||||
List<String> blockers = new ArrayList<>();
|
||||
boolean planned = trimToNull(row.planNo()) != null;
|
||||
if (!planned) {
|
||||
if (trimToNull(row.inboundCode()) == null) blockers.add("MISSING_INBOUND_CODE");
|
||||
if (row.detained()) blockers.add("ORDER_DETAINED");
|
||||
if (!("PENDING_ASSIGNMENT".equals(row.currentStatus())
|
||||
|| "DESTINATION_ASSIGNED".equals(row.currentStatus()))) blockers.add("ORDER_STATUS_NOT_READY");
|
||||
if (containerNo == null || !Objects.equals(containerNo, trimToNull(row.containerNo()))) {
|
||||
blockers.add("CONTAINER_NO_INCONSISTENT");
|
||||
}
|
||||
if (warehouseId == null || !Objects.equals(warehouseId, trimToNull(row.overseasWarehouseId()))) {
|
||||
blockers.add("RECEIVING_WAREHOUSE_INCONSISTENT");
|
||||
}
|
||||
boolean fba = "FBA".equals(destinationType(row));
|
||||
if (fba ? trimToNull(row.fbaWarehouseCode()) == null
|
||||
: row.destinationCount() != 1 || trimToNull(row.destinationWarehouseId()) == null
|
||||
&& trimToNull(row.destinationWarehouseCode()) == null) blockers.add("DESTINATION_NOT_READY");
|
||||
if (row.packageCount() < 0 || row.volumeCbm() == null || row.volumeCbm().signum() < 0
|
||||
|| row.packageCount() > 0 && row.volumeCbm().signum() == 0) blockers.add("CARGO_FACT_INVALID");
|
||||
}
|
||||
Integer suggested = suggestedPalletCount(row, blockers);
|
||||
String destinationType = destinationType(row);
|
||||
String destinationCode = "FBA".equals(destinationType) && trimToNull(row.fbaWarehouseCode()) != null
|
||||
? row.fbaWarehouseCode().trim() : row.destinationWarehouseCode();
|
||||
String readiness = planned ? "PLANNED" : blockers.isEmpty() ? "READY" : "BLOCKED";
|
||||
return new ContainerInboundPlanPreviewResponse.Order(row.customerId(), row.customerName(),
|
||||
String.valueOf(row.orderId()), trimToNull(row.orderNo()) == null ? row.sourceOrderNo() : row.orderNo(),
|
||||
row.inboundCode(), row.orderType(), destinationType, row.destinationWarehouseId(),
|
||||
destinationCode, row.destinationWarehouseName(), row.packageCount(), row.volumeCbm(),
|
||||
suggested, row.plannedPalletCount(), row.planNo(), row.planStatus(), row.version(), readiness,
|
||||
List.copyOf(blockers));
|
||||
}
|
||||
|
||||
private Integer suggestedPalletCount(InboundPlanOrderMapper.ContainerPreviewOrderRow row, List<String> blockers) {
|
||||
if ("ONE_CLICK_DROPSHIP".equals(row.orderType())) {
|
||||
List<DropshipPalletSnapshot> pallets = dropshipOrders.palletSnapshots(row.customerId(), row.orderId());
|
||||
if (pallets == null || pallets.isEmpty()) {
|
||||
if (trimToNull(row.planNo()) == null) blockers.add("DROPSHIP_PALLET_SNAPSHOT_MISSING");
|
||||
return null;
|
||||
}
|
||||
return pallets.size();
|
||||
}
|
||||
if (row.volumeCbm() == null || row.volumeCbm().signum() < 0 || row.packageCount() < 0
|
||||
|| row.packageCount() > 0 && row.volumeCbm().signum() == 0) return null;
|
||||
try {
|
||||
return PalletAllocator.allocate(List.of(new PalletAllocator.LineInput(row.orderId(),
|
||||
nullSafe(row.destinationWarehouseId()) + "|" + nullSafe("FBA".equals(destinationType(row))
|
||||
? row.fbaWarehouseCode() : row.destinationWarehouseCode()),
|
||||
row.volumeCbm(), row.packageCount(), row.recommendedPalletCount()))).pallets().size();
|
||||
} catch (IllegalArgumentException exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String destinationType(InboundPlanOrderMapper.ContainerPreviewOrderRow row) {
|
||||
if ("ONE_CLICK_DROPSHIP".equals(row.orderType())) return "ONE_CLICK_DROPSHIP";
|
||||
if ("FBA".equals(row.destinationType()) || trimToNull(row.fbaWarehouseCode()) != null) return "FBA";
|
||||
return "PRIVATE_ADDRESS";
|
||||
}
|
||||
|
||||
private static int typeRank(String type) {
|
||||
return switch (type) { case "FBA" -> 0; case "ONE_CLICK_DROPSHIP" -> 1; default -> 2; };
|
||||
}
|
||||
|
||||
private static String singleValue(List<String> values) {
|
||||
List<String> normalized = values.stream().map(InboundPlanService::trimToNull).distinct().toList();
|
||||
return normalized.size() == 1 ? normalized.get(0) : null;
|
||||
}
|
||||
|
||||
private static String nullSafe(String value) { return value == null ? "" : value; }
|
||||
|
||||
@Transactional
|
||||
public InboundPlanResponse convert(String customerId, Long selectedOrderId) {
|
||||
return convert(customerId, selectedOrderId, List.of());
|
||||
|
||||
@@ -445,6 +445,16 @@ paths:
|
||||
properties:
|
||||
sourceContainerBookingId: { type: string, minLength: 1, maxLength: 128 }
|
||||
responses: { '200': { description: 统一 ApiResponse 包装的入库计划数组 } }
|
||||
/api/platform/v1/last-leg/inbound-plans/container-preview:
|
||||
get:
|
||||
summary: 预览按柜生成入库计划
|
||||
description: 权限 platform:overseas-warehouse:inbound-order:list;按稳定 sourceContainerBookingId 返回不受订单分页影响的全柜事实。分组固定为 FBA、一件代发、私人地址,FBA 按目的仓编码/名称/ID 稳定排序;托数及 readiness/blockers 只由服务端计算。确认时必须原样回传 snapshotVersion 与 snapshotHash,服务端仍会锁柜重算。
|
||||
security: [{ bearerAuth: [] }]
|
||||
parameters:
|
||||
- { name: sourceContainerBookingId, in: query, required: true, schema: { type: string, minLength: 1, maxLength: 128 } }
|
||||
responses:
|
||||
'200': { description: 柜级入库计划转换预览, content: { application/json: { schema: { $ref: '#/components/schemas/ContainerInboundPlanPreviewApiResponse' } } } }
|
||||
'404': { description: 来源柜没有尾程订单 }
|
||||
/api/platform/v1/last-leg/dispatches:
|
||||
get:
|
||||
summary: 分页查询排车单
|
||||
@@ -2000,6 +2010,11 @@ components:
|
||||
- { $ref: '#/components/schemas/MerchantApiResponse' }
|
||||
- type: object
|
||||
properties: { data: { $ref: '#/components/schemas/InboundPlanDetail' } }
|
||||
ContainerInboundPlanPreviewApiResponse:
|
||||
allOf:
|
||||
- { $ref: '#/components/schemas/MerchantApiResponse' }
|
||||
- type: object
|
||||
properties: { data: { $ref: '#/components/schemas/ContainerInboundPlanPreview' } }
|
||||
LastlegOrderHoldRequest:
|
||||
type: object
|
||||
required: [held, chargeable, reason, expectedVersion]
|
||||
@@ -2289,6 +2304,65 @@ components:
|
||||
pallets: { type: array, items: { $ref: '#/components/schemas/InboundPlanPallet' } }
|
||||
allocations: { type: array, items: { $ref: '#/components/schemas/InboundPlanAllocation' } }
|
||||
version: { type: integer }
|
||||
ContainerInboundPlanPreview:
|
||||
type: object
|
||||
required: [sourceContainerBookingId, snapshotVersion, snapshotHash, totalOrderCount, readyOrderCount, blockedOrderCount, pendingOrderCount, pendingPalletCount, existingPlannedPalletCount, totalSuggestedPalletCount, totalPlannedPalletCount, ready, groups]
|
||||
properties:
|
||||
sourceContainerBookingId: { type: string }
|
||||
containerNo: { type: string, nullable: true }
|
||||
overseasWarehouseId: { type: string, nullable: true }
|
||||
overseasWarehouseCode: { type: string, nullable: true }
|
||||
overseasWarehouseName: { type: string, nullable: true }
|
||||
snapshotVersion: { type: integer, format: int64, minimum: 0 }
|
||||
snapshotHash: { type: string, pattern: '^[0-9a-f]{64}$' }
|
||||
totalOrderCount: { type: integer }
|
||||
readyOrderCount: { type: integer }
|
||||
blockedOrderCount: { type: integer }
|
||||
pendingOrderCount: { type: integer, description: 本次确认将新建计划的 READY 订单数 }
|
||||
pendingPalletCount: { type: integer, description: 本次确认将向资源服务批量预留的新托盘数 }
|
||||
existingPlannedPalletCount: { type: integer, description: 已有有效计划的托盘总数,不计入本次新增 }
|
||||
totalSuggestedPalletCount: { type: integer }
|
||||
totalPlannedPalletCount: { type: integer }
|
||||
ready: { type: boolean }
|
||||
groups: { type: array, items: { $ref: '#/components/schemas/ContainerInboundPlanPreviewGroup' } }
|
||||
ContainerInboundPlanPreviewGroup:
|
||||
type: object
|
||||
required: [destinationType, orderCount, pendingOrderCount, pendingPalletCount, existingPlannedPalletCount, suggestedPalletCount, plannedPalletCount, orders]
|
||||
properties:
|
||||
destinationType: { type: string, enum: [FBA, ONE_CLICK_DROPSHIP, PRIVATE_ADDRESS] }
|
||||
destinationWarehouseId: { type: string, nullable: true }
|
||||
destinationWarehouseCode: { type: string, nullable: true }
|
||||
destinationWarehouseName: { type: string, nullable: true }
|
||||
orderCount: { type: integer }
|
||||
pendingOrderCount: { type: integer }
|
||||
pendingPalletCount: { type: integer }
|
||||
existingPlannedPalletCount: { type: integer }
|
||||
suggestedPalletCount: { type: integer }
|
||||
plannedPalletCount: { type: integer }
|
||||
orders: { type: array, items: { $ref: '#/components/schemas/ContainerInboundPlanPreviewOrder' } }
|
||||
ContainerInboundPlanPreviewOrder:
|
||||
type: object
|
||||
required: [customerId, orderId, businessOrderNo, orderType, destinationType, packageCount, volumeCbm, orderVersion, readiness, blockers]
|
||||
properties:
|
||||
customerId: { type: string }
|
||||
customerName: { type: string, nullable: true }
|
||||
orderId: { type: string, pattern: '^[0-9]+$' }
|
||||
businessOrderNo: { type: string }
|
||||
inboundCode: { type: string, nullable: true }
|
||||
orderType: { type: string }
|
||||
destinationType: { type: string, enum: [FBA, ONE_CLICK_DROPSHIP, PRIVATE_ADDRESS] }
|
||||
destinationWarehouseId: { type: string, nullable: true }
|
||||
destinationWarehouseCode: { type: string, nullable: true }
|
||||
destinationWarehouseName: { type: string, nullable: true }
|
||||
packageCount: { type: integer }
|
||||
volumeCbm: { type: number }
|
||||
suggestedPalletCount: { type: integer, nullable: true }
|
||||
plannedPalletCount: { type: integer, nullable: true }
|
||||
existingPlanNo: { type: string, nullable: true }
|
||||
planStatus: { type: string, nullable: true }
|
||||
orderVersion: { type: integer, minimum: 0 }
|
||||
readiness: { type: string, enum: [READY, PLANNED, BLOCKED] }
|
||||
blockers: { type: array, items: { type: string } }
|
||||
InboundPlanLine:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
package com.baiyue.lastleg.inboundplan.service;
|
||||
|
||||
import com.baiyue.lastleg.dropship.service.DropshipOrderService;
|
||||
import com.baiyue.lastleg.dropship.model.DropshipPalletSnapshot;
|
||||
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanOrderMapper;
|
||||
import com.fasterxml.jackson.databind.json.JsonMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ContainerInboundPlanPreviewTest {
|
||||
@Mock InboundPlanOrderMapper orderMapper;
|
||||
@Mock DropshipOrderService dropshipOrders;
|
||||
private InboundPlanService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new InboundPlanService();
|
||||
ReflectionTestUtils.setField(service, "orderMapper", orderMapper);
|
||||
ReflectionTestUtils.setField(service, "dropshipOrders", dropshipOrders);
|
||||
ReflectionTestUtils.setField(service, "objectMapper", JsonMapper.builder().findAndAddModules().build());
|
||||
}
|
||||
|
||||
@Test
|
||||
void groupsFbaByStableDestinationAndReturnsServerReadinessAndPalletTotals() {
|
||||
when(orderMapper.containerPreviewOrders("BOOKING-1")).thenReturn(List.of(
|
||||
row(2L, "LM-2", "FIRST_LEG", "FBA", "B-02", "仓B", 1, null, null, 3),
|
||||
row(1L, "LM-1", "FIRST_LEG", "FBA", "A-01", "仓A", 2, null, null, 2),
|
||||
row(4L, "LM-4", "FIRST_LEG", "FBA", "A-01", "仓A", 3, null, null, 4),
|
||||
row(5L, "LM-5", "FIRST_LEG", "FBA", "C-03", "仓C", 2, "IP-5", 2, 5),
|
||||
row(6L, "LM-6", "ONE_CLICK_DROPSHIP", "OVERSEAS", "DROPSHIP", "一件代发", 0, null, null, 6),
|
||||
row(3L, "LM-3", "FIRST_LEG", "OVERSEAS", null, "私人地址", 0, null, null, 7)
|
||||
));
|
||||
when(dropshipOrders.palletSnapshots("100", 6L)).thenReturn(List.of(
|
||||
new DropshipPalletSnapshot("SRC-P1", 10, List.of())));
|
||||
|
||||
var result = service.previewContainer("BOOKING-1");
|
||||
|
||||
assertEquals("BOOKING-1", result.sourceContainerBookingId());
|
||||
assertEquals(6, result.totalOrderCount());
|
||||
assertEquals(7, result.snapshotVersion());
|
||||
assertNotNull(result.snapshotHash());
|
||||
assertEquals(64, result.snapshotHash().length());
|
||||
assertFalse(result.ready());
|
||||
assertEquals(4, result.pendingOrderCount());
|
||||
assertEquals(7, result.pendingPalletCount());
|
||||
assertEquals(2, result.existingPlannedPalletCount());
|
||||
assertEquals(List.of("A-01", "B-02", "C-03", "DROPSHIP", ""), result.groups().stream()
|
||||
.map(group -> group.destinationWarehouseCode() == null ? "" : group.destinationWarehouseCode()).toList());
|
||||
assertEquals(5, result.groups().get(0).pendingPalletCount());
|
||||
assertEquals(2, result.groups().get(2).existingPlannedPalletCount());
|
||||
assertEquals("PLANNED", result.groups().get(2).orders().get(0).readiness());
|
||||
assertEquals("ONE_CLICK_DROPSHIP", result.groups().get(3).destinationType());
|
||||
assertEquals("PRIVATE_ADDRESS", result.groups().get(4).destinationType());
|
||||
assertEquals(List.of("DESTINATION_NOT_READY"), result.groups().get(4).orders().get(0).blockers());
|
||||
}
|
||||
|
||||
private InboundPlanOrderMapper.ContainerPreviewOrderRow row(
|
||||
long id, String orderNo, String orderType, String destinationType, String destinationCode, String destinationName,
|
||||
int recommendedPallets, String planNo, Integer plannedPallets, int version) {
|
||||
return new InboundPlanOrderMapper.ContainerPreviewOrderRow("100", id, orderNo, "FO-" + id,
|
||||
"测试客户", orderType, destinationType, "IN-" + id, "PENDING_ASSIGNMENT", planNo,
|
||||
"MSCU6639870", recommendedPallets, 10, new BigDecimal("1.0000"),
|
||||
"193000000000200003", "US-LAX-01", "洛杉矶仓", "FBA".equals(destinationType) ? destinationCode : null,
|
||||
"FBA".equals(destinationType) || destinationCode == null ? null : "WH-" + id,
|
||||
"FBA".equals(destinationType) ? null : destinationCode, destinationName,
|
||||
"FBA".equals(destinationType) || destinationCode == null ? 0 : 1, false, version, planNo,
|
||||
planNo == null ? null : "PENDING_ASSIGNMENT", plannedPallets);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user