Merge remote-tracking branch 'origin/test' into dev
# Conflicts: # scripts/db/incremental.sql
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());
|
||||
|
||||
Reference in New Issue
Block a user