Merge branch 'dev-lyhfive' of http://git.buggoglobal.com/java-project/baiyue-firstleg into dev
This commit is contained in:
+10
@@ -12,6 +12,7 @@ import com.baiyue.lastleg.inboundplan.dto.InboundPlanListResponse;
|
||||
import com.baiyue.lastleg.inboundplan.dto.InboundPlanRequests;
|
||||
import com.baiyue.lastleg.inboundplan.dto.SupplementPalletRequest;
|
||||
import com.baiyue.lastleg.inboundplan.dto.UpdateFbaAppointmentRequest;
|
||||
import com.baiyue.lastleg.inboundplan.dto.UpdateInboundPlanRequest;
|
||||
import com.baiyue.lastleg.inboundplan.service.InboundFbaAppointmentService;
|
||||
import com.baiyue.lastleg.inboundplan.service.InboundPlanService;
|
||||
import com.baiyue.lastleg.security.OverseasPermissions;
|
||||
@@ -75,6 +76,15 @@ public class InboundPlanController {
|
||||
return ApiResponse.success(service.detailByPlanNo(scope(user, customerId), planNo));
|
||||
}
|
||||
|
||||
@PutMapping("/{planNo}")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.INBOUND_ORDER_EDIT + "')")
|
||||
@Operation(summary = "编辑入仓计划")
|
||||
public ApiResponse<InboundPlanResponse> update(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String planNo,
|
||||
@Valid @RequestBody UpdateInboundPlanRequest request) {
|
||||
return ApiResponse.success(service.update(scope(user, request.customerId()), planNo, request));
|
||||
}
|
||||
|
||||
@PostMapping("/{planNo}/pallets/supplement")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.INBOUND_ORDER_EDIT + "')")
|
||||
/** 为计划目标仓新增一个可打印的补充托盘。 */
|
||||
|
||||
+16
-1
@@ -17,6 +17,7 @@ public record InboundPlanListResponse(
|
||||
String fbaWarehouseCode,
|
||||
String customerName,
|
||||
Integer packageCount,
|
||||
Integer pickedPackageCount,
|
||||
BigDecimal volumeCbm,
|
||||
Integer palletCount,
|
||||
String status,
|
||||
@@ -25,6 +26,20 @@ public record InboundPlanListResponse(
|
||||
LocalDateTime appointmentAt,
|
||||
LocalDateTime expectedLoadingAt,
|
||||
String dispatchStatus,
|
||||
LocalDateTime createdAt
|
||||
LocalDateTime createdAt,
|
||||
Integer version
|
||||
) {
|
||||
public InboundPlanListResponse(Long lineId, Long planId, String planNo, String customerId,
|
||||
Long orderId, String inboundCode, String orderType,
|
||||
String warehouseId, String warehouseCode, String warehouseName,
|
||||
String fbaWarehouseCode, String customerName, Integer packageCount,
|
||||
BigDecimal volumeCbm, Integer palletCount, String status,
|
||||
String appointmentNo, String appointmentStatus,
|
||||
LocalDateTime appointmentAt, LocalDateTime expectedLoadingAt,
|
||||
String dispatchStatus, LocalDateTime createdAt) {
|
||||
this(lineId, planId, planNo, customerId, orderId, inboundCode, orderType, warehouseId,
|
||||
warehouseCode, warehouseName, fbaWarehouseCode, customerName, packageCount, 0,
|
||||
volumeCbm, palletCount, status, appointmentNo, appointmentStatus, appointmentAt,
|
||||
expectedLoadingAt, dispatchStatus, createdAt, 0);
|
||||
}
|
||||
}
|
||||
|
||||
+20
-1
@@ -14,10 +14,29 @@ public record InboundPlanResponse(
|
||||
int lineCount,
|
||||
int palletCount,
|
||||
BigDecimal totalVolumeCbm,
|
||||
int expectedPackageCount,
|
||||
int pickedPackageCount,
|
||||
List<Line> lines,
|
||||
List<Pallet> pallets,
|
||||
List<Allocation> allocations
|
||||
List<Allocation> allocations,
|
||||
int version
|
||||
) {
|
||||
public InboundPlanResponse(Long id, String planNo, String customerId, Long sourceOrderId,
|
||||
String containerNo, String status, int lineCount, int palletCount,
|
||||
BigDecimal totalVolumeCbm, List<Line> lines, List<Pallet> pallets,
|
||||
List<Allocation> allocations) {
|
||||
this(id, planNo, customerId, sourceOrderId, containerNo, status, lineCount, palletCount,
|
||||
totalVolumeCbm, lines, pallets, allocations, 0);
|
||||
}
|
||||
|
||||
public InboundPlanResponse(Long id, String planNo, String customerId, Long sourceOrderId,
|
||||
String containerNo, String status, int lineCount, int palletCount,
|
||||
BigDecimal totalVolumeCbm, List<Line> lines, List<Pallet> pallets,
|
||||
List<Allocation> allocations, int version) {
|
||||
this(id, planNo, customerId, sourceOrderId, containerNo, status, lineCount, palletCount,
|
||||
totalVolumeCbm, lines.stream().mapToInt(Line::requiredPackageCount).sum(),
|
||||
allocations.stream().mapToInt(Allocation::pickedPackageCount).sum(), lines, pallets, allocations, version);
|
||||
}
|
||||
public record Line(Long id, Long orderId, String inboundCode, String fbaWarehouseCode, String destinationWarehouseId,
|
||||
String destinationWarehouseCode, String destinationWarehouseName,
|
||||
BigDecimal volumeCbm, int requiredPackageCount, int plannedPalletCount) {
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package com.baiyue.lastleg.inboundplan.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
public record UpdateInboundPlanRequest(
|
||||
@NotBlank String customerId,
|
||||
@NotNull Long overseasWarehouseId,
|
||||
String warehouseCode,
|
||||
String warehouseName,
|
||||
@NotNull Integer version
|
||||
) {}
|
||||
@@ -3,6 +3,7 @@ package com.baiyue.lastleg.inboundplan.entity;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.annotation.Version;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
@@ -24,4 +25,6 @@ public class InboundPlan {
|
||||
private BigDecimal totalVolumeCbm;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
@Version
|
||||
private Integer version;
|
||||
}
|
||||
|
||||
+5
-7
@@ -24,19 +24,17 @@ public interface InboundPlanOrderMapper {
|
||||
@Select("SELECT o.id AS orderId,o.customer_id AS customerId,o.container_no AS containerNo," +
|
||||
"o.inbound_code AS inboundCode,o.current_status AS currentStatus,o.inbound_order_no AS inboundOrderNo," +
|
||||
"COALESCE(o.volume_cbm,0) AS volumeCbm,COALESCE(o.package_count,0) AS packageCount," +
|
||||
"COALESCE(o.pallet_count,0) AS recommendedPalletCount," +
|
||||
"NULLIF(o.fba_warehouse_code,'') AS fbaWarehouseCode," +
|
||||
"COALESCE(NULLIF(o.transfer_warehouse_id,''),MIN(NULLIF(d.destination_warehouse_id,''))) AS destinationWarehouseId," +
|
||||
"COALESCE(NULLIF(o.transfer_warehouse_code,''),MIN(NULLIF(d.destination_warehouse_code,''))) AS destinationWarehouseCode," +
|
||||
"COALESCE(NULLIF(o.transfer_warehouse_name,''),MIN(NULLIF(d.destination_warehouse_name,''))) AS destinationWarehouseName," +
|
||||
"COUNT(DISTINCT COALESCE(NULLIF(d.destination_warehouse_id,''),NULLIF(d.destination_warehouse_code,''),'#EMPTY#')) AS destinationCount," +
|
||||
"SUM(CASE WHEN d.id IS NOT NULL " +
|
||||
"AND COALESCE(NULLIF(o.transfer_warehouse_id,''),NULLIF(o.transfer_warehouse_code,'')) IS NOT NULL " +
|
||||
"AND COALESCE(NULLIF(d.destination_warehouse_id,''),NULLIF(d.destination_warehouse_code,''),'#EMPTY#') " +
|
||||
"<> COALESCE(NULLIF(o.transfer_warehouse_id,''),NULLIF(o.transfer_warehouse_code,'')) THEN 1 ELSE 0 END) " +
|
||||
"AS destinationMismatchCount " +
|
||||
"CASE WHEN COALESCE(NULLIF(o.transfer_warehouse_id,''),NULLIF(o.transfer_warehouse_code,'')) IS NOT NULL THEN 1 " +
|
||||
"ELSE COUNT(DISTINCT COALESCE(NULLIF(d.destination_warehouse_id,''),NULLIF(d.destination_warehouse_code,''),'#EMPTY#')) END AS destinationCount," +
|
||||
"0 AS destinationMismatchCount " +
|
||||
"FROM ll_order o LEFT JOIN ll_order_detail d ON d.order_id=o.id " +
|
||||
"WHERE o.customer_id=#{customerId} AND o.id=#{orderId} " +
|
||||
"GROUP BY o.id,o.customer_id,o.container_no,o.inbound_code,o.current_status,o.inbound_order_no,o.volume_cbm,o.package_count,o.fba_warehouse_code," +
|
||||
"GROUP BY o.id,o.customer_id,o.container_no,o.inbound_code,o.current_status,o.inbound_order_no,o.volume_cbm,o.package_count,o.pallet_count,o.fba_warehouse_code," +
|
||||
"o.transfer_warehouse_id,o.transfer_warehouse_code,o.transfer_warehouse_name " +
|
||||
"ORDER BY o.id FOR UPDATE")
|
||||
List<SourceOrderSnapshot> lockOrder(@Param("customerId") String customerId,
|
||||
|
||||
+8
-5
@@ -30,8 +30,8 @@ public interface InboundPlanQueryMapper {
|
||||
"<if test='q.fbaWarehouseCode != null and q.fbaWarehouseCode != \"\"'> AND l.fba_warehouse_code LIKE CONCAT('%',#{q.fbaWarehouseCode},'%')</if>" +
|
||||
"<choose><when test='q.appointmentStatus == \"UNAPPOINTED\"'> AND ai.id IS NULL</when>" +
|
||||
"<when test='q.appointmentStatus != null and q.appointmentStatus != \"\"'> AND a.appointment_status=#{q.appointmentStatus}</when></choose>" +
|
||||
"<choose><when test='q.dispatchStatus == \"SCHEDULED\"'> AND EXISTS (SELECT 1 FROM ll_dispatch_item di WHERE di.order_id=l.order_id <if test='customerId != null and customerId != \"\"'> AND di.customer_id=#{customerId}</if>)</when>" +
|
||||
"<when test='q.dispatchStatus == \"UNSCHEDULED\"'> AND NOT EXISTS (SELECT 1 FROM ll_dispatch_item di WHERE di.order_id=l.order_id <if test='customerId != null and customerId != \"\"'> AND di.customer_id=#{customerId}</if>)</when></choose>" +
|
||||
"<choose><when test='q.dispatchStatus == \"SCHEDULED\"'> AND EXISTS (SELECT 1 FROM ll_dispatch_item di WHERE <if test='customerId != null and customerId != \"\"'>di.customer_id=#{customerId} AND </if>di.order_id=l.order_id)</when>" +
|
||||
"<when test='q.dispatchStatus == \"UNSCHEDULED\"'> AND NOT EXISTS (SELECT 1 FROM ll_dispatch_item di WHERE <if test='customerId != null and customerId != \"\"'>di.customer_id=#{customerId} AND </if>di.order_id=l.order_id)</when></choose>" +
|
||||
"<if test='q.createdFrom != null'> AND p.created_at >=#{q.createdFrom}</if>" +
|
||||
"<if test='q.createdTo != null'> AND p.created_at <=#{q.createdTo}</if>";
|
||||
|
||||
@@ -39,12 +39,15 @@ public interface InboundPlanQueryMapper {
|
||||
"l.order_id orderId,l.inbound_code inboundCode,o.order_type orderType," +
|
||||
"l.destination_warehouse_id warehouseId,l.destination_warehouse_code warehouseCode," +
|
||||
"l.destination_warehouse_name warehouseName,l.fba_warehouse_code fbaWarehouseCode," +
|
||||
"o.customer_name customerName,l.required_package_count packageCount,l.volume_cbm volumeCbm," +
|
||||
"o.customer_name customerName,l.required_package_count packageCount," +
|
||||
"COALESCE((SELECT SUM(pa.picked_package_count) FROM ll_inbound_plan_pallet_allocation pa WHERE pa.line_id=l.id),0) pickedPackageCount," +
|
||||
"l.volume_cbm volumeCbm," +
|
||||
"l.planned_pallet_count palletCount,p.status status,a.appointment_no appointmentNo," +
|
||||
"a.appointment_status appointmentStatus,a.appointment_at appointmentAt," +
|
||||
"a.expected_loading_at expectedLoadingAt," +
|
||||
"CASE WHEN EXISTS (SELECT 1 FROM ll_dispatch_item di WHERE di.customer_id=#{customerId} AND di.order_id=l.order_id)" +
|
||||
" THEN 'SCHEDULED' ELSE 'UNSCHEDULED' END dispatchStatus,p.created_at createdAt" +
|
||||
"CASE WHEN EXISTS (SELECT 1 FROM ll_dispatch_item di WHERE " +
|
||||
"<if test='customerId != null and customerId != \"\"'>di.customer_id=#{customerId} AND </if>di.order_id=l.order_id)" +
|
||||
" THEN 'SCHEDULED' ELSE 'UNSCHEDULED' END dispatchStatus,p.created_at createdAt,p.version version" +
|
||||
FROM_AND_FILTER + " ORDER BY p.created_at DESC,l.id DESC LIMIT #{limit} OFFSET #{offset}</script>")
|
||||
List<InboundPlanListResponse> rows(@Param("customerId") String customerId,
|
||||
@Param("q") InboundPlanRequests.Query query,
|
||||
|
||||
+12
-1
@@ -11,6 +11,7 @@ public record SourceOrderSnapshot(
|
||||
String inboundOrderNo,
|
||||
BigDecimal volumeCbm,
|
||||
int packageCount,
|
||||
int recommendedPalletCount,
|
||||
String fbaWarehouseCode,
|
||||
String destinationWarehouseId,
|
||||
String destinationWarehouseCode,
|
||||
@@ -18,13 +19,23 @@ public record SourceOrderSnapshot(
|
||||
int destinationCount,
|
||||
int destinationMismatchCount
|
||||
) {
|
||||
public SourceOrderSnapshot(Long orderId, String customerId, String containerNo, String inboundCode,
|
||||
String currentStatus, String inboundOrderNo, BigDecimal volumeCbm,
|
||||
int packageCount, String fbaWarehouseCode, String destinationWarehouseId,
|
||||
String destinationWarehouseCode, String destinationWarehouseName,
|
||||
int destinationCount, int destinationMismatchCount) {
|
||||
this(orderId, customerId, containerNo, inboundCode, currentStatus, inboundOrderNo, volumeCbm,
|
||||
packageCount, 0, fbaWarehouseCode, destinationWarehouseId, destinationWarehouseCode,
|
||||
destinationWarehouseName, destinationCount, destinationMismatchCount);
|
||||
}
|
||||
|
||||
public SourceOrderSnapshot(Long orderId, String customerId, String containerNo, String inboundCode,
|
||||
String currentStatus, String inboundOrderNo, BigDecimal volumeCbm,
|
||||
int packageCount, String destinationWarehouseId,
|
||||
String destinationWarehouseCode, String destinationWarehouseName,
|
||||
int destinationCount, int destinationMismatchCount) {
|
||||
this(orderId, customerId, containerNo, inboundCode, currentStatus, inboundOrderNo, volumeCbm,
|
||||
packageCount, null, destinationWarehouseId, destinationWarehouseCode,
|
||||
packageCount, 0, null, destinationWarehouseId, destinationWarehouseCode,
|
||||
destinationWarehouseName, destinationCount, destinationMismatchCount);
|
||||
}
|
||||
}
|
||||
|
||||
+13
-2
@@ -17,11 +17,22 @@ public final class PdaRequests {
|
||||
public record PackageOperation(@Positive int packageCount, @NotBlank String idempotencyKey,
|
||||
String operatorId, String operatorName) {}
|
||||
|
||||
public record WholePalletOperation(@NotBlank String idempotencyKey,
|
||||
String operatorId, String operatorName) {}
|
||||
|
||||
public record ConfirmMerge(@Positive long sourceAllocationId, @Positive int packageCount,
|
||||
@NotBlank String idempotencyKey, String operatorId, String operatorName) {}
|
||||
|
||||
public record ScanCarton(@NotBlank String cartonMark, @Positive long planPalletId,
|
||||
@NotBlank String idempotencyKey, String operatorId, String operatorName) {}
|
||||
public record ScanCarton(@NotBlank String cartonMark, Long planPalletId, Integer packageCount,
|
||||
@NotBlank String idempotencyKey, String operatorId, String operatorName) {
|
||||
/** Backward-compatible single-carton request with an explicitly selected pallet. */
|
||||
public ScanCarton(String cartonMark, long planPalletId, String idempotencyKey,
|
||||
String operatorId, String operatorName) {
|
||||
this(cartonMark, planPalletId, 1, idempotencyKey, operatorId, operatorName);
|
||||
}
|
||||
}
|
||||
|
||||
public record ForceComplete(@NotBlank String idempotencyKey, String operatorId, String operatorName) {}
|
||||
|
||||
public record RemoveCarton(@NotBlank String cartonMark, @NotBlank String idempotencyKey,
|
||||
String operatorId, String operatorName) {}
|
||||
|
||||
+33
-3
@@ -10,9 +10,34 @@ public final class PdaResponses {
|
||||
public record Task(long taskId, String taskNo, String planNo, String status,
|
||||
LocalDateTime pushedAt, LocalDateTime startedAt, LocalDateTime completedAt,
|
||||
List<Long> unloadPhotoFileIds, LocalDateTime unloadPhotoUploadedAt,
|
||||
LocalDateTime estimatedEmptyReturnAt,
|
||||
LocalDateTime estimatedEmptyReturnAt, int expectedPackageCount,
|
||||
int pickedPackageCount, String orderType, boolean wholePalletInbound,
|
||||
List<DropshipPallet> dropshipPallets, String inboundMode,
|
||||
List<Pallet> pallets, List<Allocation> allocations,
|
||||
List<ExpectedCarton> expectedCartons) {}
|
||||
List<ExpectedCarton> expectedCartons) {
|
||||
public Task(long taskId, String taskNo, String planNo, String status,
|
||||
LocalDateTime pushedAt, LocalDateTime startedAt, LocalDateTime completedAt,
|
||||
List<Long> unloadPhotoFileIds, LocalDateTime unloadPhotoUploadedAt,
|
||||
LocalDateTime estimatedEmptyReturnAt, int expectedPackageCount,
|
||||
int pickedPackageCount) {
|
||||
this(taskId, taskNo, planNo, status, pushedAt, startedAt, completedAt,
|
||||
unloadPhotoFileIds, unloadPhotoUploadedAt, estimatedEmptyReturnAt,
|
||||
expectedPackageCount, pickedPackageCount, null, false, List.of(), null,
|
||||
List.of(), List.of(), List.of());
|
||||
}
|
||||
|
||||
public Task(long taskId, String taskNo, String planNo, String status,
|
||||
LocalDateTime pushedAt, LocalDateTime startedAt, LocalDateTime completedAt,
|
||||
List<Long> unloadPhotoFileIds, LocalDateTime unloadPhotoUploadedAt,
|
||||
LocalDateTime estimatedEmptyReturnAt) {
|
||||
this(taskId, taskNo, planNo, status, pushedAt, startedAt, completedAt,
|
||||
unloadPhotoFileIds, unloadPhotoUploadedAt, estimatedEmptyReturnAt, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public record DropshipPallet(String sourcePalletNo, long planPalletId, long allocationId,
|
||||
long packageCount, List<DropshipSku> skus) {}
|
||||
public record DropshipSku(String skuCode, String productName, long packageCount, long productQuantity) {}
|
||||
|
||||
public record Pallet(long planPalletId, String palletNo, String destinationWarehouseId,
|
||||
String destinationWarehouseCode, String destinationWarehouseName,
|
||||
@@ -46,5 +71,10 @@ public final class PdaResponses {
|
||||
boolean sourcePalletScrapped) {}
|
||||
|
||||
public record Carton(String result, String cartonMark, long planPalletId, long allocationId,
|
||||
int palletActualPackageCount) {}
|
||||
int palletActualPackageCount, String palletNo, int scannedPackageCount) {
|
||||
public Carton(String result, String cartonMark, long planPalletId, long allocationId,
|
||||
int palletActualPackageCount) {
|
||||
this(result, cartonMark, planPalletId, allocationId, palletActualPackageCount, null, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+25
-3
@@ -30,14 +30,28 @@ public final class ReceivingContainerResponses {
|
||||
String destinationWarehouseId,
|
||||
String destinationWarehouseCode, String destinationWarehouseName,
|
||||
String fbaWarehouseCode, int plannedPalletCount, int totalPackages,
|
||||
int receivedPackages, int pendingPackages) { }
|
||||
int receivedPackages, int pendingPackages,
|
||||
boolean palletMergeRequired, String palletMergeHint) { }
|
||||
|
||||
public record CartonResolved(String cartonMark, String inboundCode,
|
||||
String destinationType, String overseasWarehouseId,
|
||||
String overseasWarehouseName, String destinationWarehouseId,
|
||||
String destinationWarehouseCode, String destinationWarehouseName,
|
||||
String fbaWarehouseCode, int plannedPalletCount, int totalPackages,
|
||||
int receivedPackages, int pendingPackages) { }
|
||||
int receivedPackages, int pendingPackages,
|
||||
boolean palletMergeRequired, String palletMergeHint) {
|
||||
public CartonResolved(String cartonMark, String inboundCode, String destinationType,
|
||||
String overseasWarehouseId, String overseasWarehouseName,
|
||||
String destinationWarehouseId, String destinationWarehouseCode,
|
||||
String destinationWarehouseName, String fbaWarehouseCode,
|
||||
int plannedPalletCount, int totalPackages, int receivedPackages,
|
||||
int pendingPackages) {
|
||||
this(cartonMark, inboundCode, destinationType, overseasWarehouseId, overseasWarehouseName,
|
||||
destinationWarehouseId, destinationWarehouseCode, destinationWarehouseName,
|
||||
fbaWarehouseCode, plannedPalletCount, totalPackages, receivedPackages,
|
||||
pendingPackages, false, null);
|
||||
}
|
||||
}
|
||||
|
||||
public record PalletResolved(String palletCode, long warehouseId, String status,
|
||||
String fbaWarehouseCode, String inboundCode,
|
||||
@@ -45,7 +59,15 @@ public final class ReceivingContainerResponses {
|
||||
|
||||
public record Receipt(String result, String containerNo, String inboundCode,
|
||||
String destinationType, String palletCode, int acceptedPackages,
|
||||
int receivedPackages, int pendingPackages, String sessionStatus) { }
|
||||
int receivedPackages, int pendingPackages, String sessionStatus,
|
||||
boolean palletMergeRequired, String palletMergeHint) {
|
||||
public Receipt(String result, String containerNo, String inboundCode,
|
||||
String destinationType, String palletCode, int acceptedPackages,
|
||||
int receivedPackages, int pendingPackages, String sessionStatus) {
|
||||
this(result, containerNo, inboundCode, destinationType, palletCode, acceptedPackages,
|
||||
receivedPackages, pendingPackages, sessionStatus, false, null);
|
||||
}
|
||||
}
|
||||
|
||||
public record Photo(String fileId, String fileName, String contentType, long sizeBytes) { }
|
||||
}
|
||||
|
||||
+5
@@ -17,6 +17,8 @@ import com.baiyue.lastleg.inboundplan.pda.entity.InboundPdaTask;
|
||||
import com.baiyue.lastleg.inboundplan.pda.mapper.InboundPalletMergeMapper;
|
||||
import com.baiyue.lastleg.inboundplan.pda.mapper.InboundPdaOperationMapper;
|
||||
import com.baiyue.lastleg.inboundplan.pda.mapper.InboundPdaTaskMapper;
|
||||
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
@@ -32,6 +34,7 @@ public class PdaWorkflowRepository {
|
||||
@Resource private InboundPdaOperationMapper operations;
|
||||
@Resource private InboundPalletMergeMapper merges;
|
||||
@Resource private InboundPlanCartonMapper cartons;
|
||||
@Resource private LastlegOrderMapper orders;
|
||||
|
||||
public InboundPlan lockPlan(String customerId, String planNo) {
|
||||
return plans.selectOne(new LambdaQueryWrapper<InboundPlan>()
|
||||
@@ -93,6 +96,8 @@ public class PdaWorkflowRepository {
|
||||
.eq(InboundPlanPalletAllocation::getPlanId, planId));
|
||||
}
|
||||
|
||||
public LastlegOrder order(Long orderId) { return orderId == null ? null : orders.selectById(orderId); }
|
||||
|
||||
public void insert(InboundPdaTask value) { tasks.insert(value); }
|
||||
public void insert(InboundPdaOperation value) { operations.insert(value); }
|
||||
public void insert(InboundPalletMerge value) { merges.insert(value); }
|
||||
|
||||
+211
-21
@@ -14,6 +14,8 @@ import com.baiyue.lastleg.inboundplan.pda.entity.InboundPdaTask;
|
||||
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
|
||||
import com.baiyue.lastleg.inboundplan.pda.gateway.UnloadPhotoFileGateway;
|
||||
import com.baiyue.lastleg.inboundplan.pda.repository.PdaWorkflowRepository;
|
||||
import com.baiyue.lastleg.dropship.model.DropshipPalletSnapshot;
|
||||
import com.baiyue.lastleg.dropship.service.DropshipOrderService;
|
||||
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
@@ -54,6 +56,7 @@ public class InboundPdaWorkflowService {
|
||||
@Resource private InboundUnloadCompletedOutboxService unloadCompletedOutbox;
|
||||
@Resource private LastlegOrderMapper orders;
|
||||
@Resource private UnloadPhotoFileGateway unloadPhotoFiles;
|
||||
@Resource private DropshipOrderService dropshipOrders;
|
||||
@Resource private ObjectMapper sourceObjectMapper;
|
||||
|
||||
private ObjectMapper objectMapper;
|
||||
@@ -77,6 +80,9 @@ public class InboundPdaWorkflowService {
|
||||
}
|
||||
InboundPdaTask task = repository.lockTask(customerId, plan.getId());
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
if (task != null && terminal(task.getStatus())) {
|
||||
throw conflict("PDA inbound task is already completed");
|
||||
}
|
||||
if (task == null) {
|
||||
task = new InboundPdaTask();
|
||||
task.setId(IdWorker.getId());
|
||||
@@ -95,7 +101,7 @@ public class InboundPdaWorkflowService {
|
||||
plan.setStatus("PDA_PENDING");
|
||||
plan.setUpdatedAt(now);
|
||||
repository.update(plan);
|
||||
PdaResponses.Task result = task(task, planNo);
|
||||
PdaResponses.Task result = taskResponse(task, plan, planNo);
|
||||
saveOperation(customerId, task.getId(), null, "PUSH", request.idempotencyKey(), request, result);
|
||||
return result;
|
||||
}
|
||||
@@ -136,7 +142,7 @@ public class InboundPdaWorkflowService {
|
||||
InboundPlan plan = requirePlan(customerId, planNo);
|
||||
InboundPdaTask task = repository.task(customerId, plan.getId());
|
||||
if (task == null) throw notFound("PDA task not found");
|
||||
return task(task, planNo);
|
||||
return taskResponse(task, plan, planNo);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -255,7 +261,7 @@ public class InboundPdaWorkflowService {
|
||||
public PdaResponses.Operation count(String customerId, String planNo, long allocationId,
|
||||
PdaRequests.PackageOperation request) {
|
||||
InboundPlan plan = requirePlan(customerId, planNo);
|
||||
requireLegacyCounting(plan);
|
||||
requireCountMode(plan);
|
||||
InboundPdaOperation replay = replay(customerId, "COUNT", request.idempotencyKey(), request);
|
||||
if (replay != null) return read(replay, PdaResponses.Operation.class);
|
||||
InboundPdaTask task = requireActiveTask(customerId, plan);
|
||||
@@ -283,7 +289,7 @@ public class InboundPdaWorkflowService {
|
||||
public PdaResponses.Operation pick(String customerId, String planNo, long allocationId,
|
||||
PdaRequests.PackageOperation request) {
|
||||
InboundPlan plan = requirePlan(customerId, planNo);
|
||||
requireLegacyCounting(plan);
|
||||
requireCountMode(plan);
|
||||
InboundPdaOperation replay = replay(customerId, "PICK", request.idempotencyKey(), request);
|
||||
if (replay != null) return read(replay, PdaResponses.Operation.class);
|
||||
InboundPdaTask task = requireActiveTask(customerId, plan);
|
||||
@@ -322,10 +328,44 @@ public class InboundPdaWorkflowService {
|
||||
return result;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public PdaResponses.Operation pickWholePallet(String customerId, String planNo, long allocationId,
|
||||
PdaRequests.WholePalletOperation request) {
|
||||
InboundPlan plan = requirePlan(customerId, planNo);
|
||||
requireDropship(plan);
|
||||
InboundPdaOperation replay = replay(customerId, "PICK_WHOLE_PALLET", request.idempotencyKey(), request);
|
||||
if (replay != null) return read(replay, PdaResponses.Operation.class);
|
||||
InboundPdaTask task = requireActiveTask(customerId, plan);
|
||||
InboundPlanPalletAllocation allocation = requireAllocation(plan.getId(), allocationId);
|
||||
InboundPlanLine line = requireLine(plan.getId(), allocation.getLineId());
|
||||
int planned = value(allocation.getPlannedPackageCount());
|
||||
if (planned <= 0) throw conflict("One-click dropship pallet has no planned packages");
|
||||
if (value(allocation.getPickedPackageCount()) != 0) {
|
||||
throw conflict("One-click dropship pallet has already been partially operated");
|
||||
}
|
||||
allocation.setCountedPackageCount(planned);
|
||||
allocation.setPickedPackageCount(planned);
|
||||
allocation.setActualVolumeCbm(volume(allocation.getAllocatedVolumeCbm()));
|
||||
allocation.setStatus("PICKED");
|
||||
bump(allocation);
|
||||
repository.update(allocation);
|
||||
InboundPlanPallet pallet = requirePallet(plan.getId(), allocation.getPalletId());
|
||||
pallet.setActualPackageCount(planned);
|
||||
pallet.setActualVolumeCbm(volume(allocation.getAllocatedVolumeCbm()));
|
||||
repository.update(pallet);
|
||||
startPicking(task, plan, request.operatorId(), request.operatorName());
|
||||
PdaResponses.Operation result = operation("WHOLE_PALLET_PICKED", allocation, line,
|
||||
List.of(), 0, planned, 0);
|
||||
saveOperation(customerId, task.getId(), allocationId, "PICK_WHOLE_PALLET",
|
||||
request.idempotencyKey(), request, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 海外首次扫描箱唛:仅接受本柜国内冻结且已归属本计划的箱。 */
|
||||
@Transactional
|
||||
public PdaResponses.Carton scanCarton(String customerId, String planNo, PdaRequests.ScanCarton request) {
|
||||
InboundPlan plan = requirePlan(customerId, planNo);
|
||||
requireCartonScanMode(plan);
|
||||
InboundPdaOperation replay = replay(customerId, "SCAN_CARTON", request.idempotencyKey(), request);
|
||||
if (replay != null) return read(replay, PdaResponses.Carton.class);
|
||||
InboundPdaTask task = requireActiveTask(customerId, plan);
|
||||
@@ -336,16 +376,23 @@ public class InboundPdaWorkflowService {
|
||||
if (!("UNASSIGNED".equals(carton.getStatus()) || "EXPECTED".equals(carton.getStatus()))) {
|
||||
throw conflict("Carton mark has already been scanned; remove it before scanning again");
|
||||
}
|
||||
InboundPlanPallet pallet = requirePallet(plan.getId(), request.planPalletId());
|
||||
long requestedPalletId = request.planPalletId() == null ? 0L : request.planPalletId();
|
||||
InboundPlanPallet pallet = requestedPalletId > 0
|
||||
? requirePallet(plan.getId(), requestedPalletId)
|
||||
: autoPallet(plan, carton);
|
||||
if (isNotAssigned(pallet)) throw conflict("Planned pallet must be assigned and locked before carton scanning");
|
||||
if (!sameWarehouse(carton, pallet)) throw conflict("Carton and pallet destination warehouses must match");
|
||||
InboundPlanLine line = carton.getLineId() == null
|
||||
? repository.lineForCarton(plan.getId(), carton)
|
||||
: requireLine(plan.getId(), carton.getLineId());
|
||||
if (line == null) throw notFound("Carton mark does not belong to cargo in this inbound plan");
|
||||
if (repository.scannedCartons(plan.getId(), line.getId()) >= value(line.getRequiredPackageCount())) {
|
||||
int requestedCount = request.packageCount() == null ? 1 : request.packageCount();
|
||||
if (requestedCount != 1) throw conflict("Overseas warehouse carton scanning accepts exactly one carton per scan");
|
||||
int alreadyScanned = (int) repository.scannedCartons(plan.getId(), line.getId());
|
||||
if (alreadyScanned + requestedCount > value(line.getRequiredPackageCount())) {
|
||||
throw conflict("This inbound plan line has reached its required carton count");
|
||||
}
|
||||
List<InboundPlanCarton> cartonsToScan = List.of(carton);
|
||||
InboundPlanPalletAllocation allocation = repository.lockAllocation(plan.getId(), line.getId(), pallet.getId());
|
||||
if (allocation == null) {
|
||||
allocation = new InboundPlanPalletAllocation(); allocation.setId(IdWorker.getId());
|
||||
@@ -353,23 +400,29 @@ public class InboundPdaWorkflowService {
|
||||
allocation.setLineId(line.getId()); allocation.setPalletId(pallet.getId()); allocation.setAllocatedVolumeCbm(ZERO_VOLUME);
|
||||
allocation.setPlannedPackageCount(0); allocation.setCountedPackageCount(0); allocation.setPickedPackageCount(0);
|
||||
allocation.setActualVolumeCbm(ZERO_VOLUME); allocation.setStatus("PICKING"); allocation.setVersion(0); repository.insert(allocation);
|
||||
} else if (value(allocation.getPlannedPackageCount()) > 0
|
||||
&& value(allocation.getPickedPackageCount()) + requestedCount > value(allocation.getPlannedPackageCount())) {
|
||||
throw conflict("Requested carton quantity exceeds the recommended pallet package count");
|
||||
}
|
||||
BigDecimal perPackage = perPackageVolume(line);
|
||||
carton.setOrderId(line.getOrderId()); carton.setPlanId(plan.getId()); carton.setLineId(line.getId());
|
||||
carton.setPlanPalletId(pallet.getId()); carton.setWarehousePalletId(pallet.getWarehousePalletId());
|
||||
carton.setWarehouseLocationId(pallet.getWarehouseLocationId()); carton.setPdaTaskId(task.getId());
|
||||
carton.setStatus("ON_PALLET"); carton.setBoundAt(LocalDateTime.now()); carton.setUpdatedAt(LocalDateTime.now());
|
||||
carton.setVersion(value(carton.getVersion()) + 1); repository.update(carton);
|
||||
allocation.setCountedPackageCount(value(allocation.getCountedPackageCount()) + 1);
|
||||
allocation.setPickedPackageCount(value(allocation.getPickedPackageCount()) + 1);
|
||||
allocation.setActualVolumeCbm(volume(allocation.getActualVolumeCbm()).add(perPackage).setScale(4, RoundingMode.HALF_UP));
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
for (InboundPlanCarton item : cartonsToScan) {
|
||||
item.setOrderId(line.getOrderId()); item.setPlanId(plan.getId()); item.setLineId(line.getId());
|
||||
item.setPlanPalletId(pallet.getId()); item.setWarehousePalletId(pallet.getWarehousePalletId());
|
||||
item.setWarehouseLocationId(pallet.getWarehouseLocationId()); item.setPdaTaskId(task.getId());
|
||||
item.setStatus("ON_PALLET"); item.setBoundAt(now); item.setUpdatedAt(now);
|
||||
item.setVersion(value(item.getVersion()) + 1); repository.update(item);
|
||||
}
|
||||
allocation.setCountedPackageCount(value(allocation.getCountedPackageCount()) + requestedCount);
|
||||
allocation.setPickedPackageCount(value(allocation.getPickedPackageCount()) + requestedCount);
|
||||
allocation.setActualVolumeCbm(volume(allocation.getActualVolumeCbm()).add(perPackage.multiply(BigDecimal.valueOf(requestedCount))).setScale(4, RoundingMode.HALF_UP));
|
||||
allocation.setStatus(value(allocation.getPickedPackageCount()) >= value(allocation.getPlannedPackageCount()) ? "PICKED" : "PICKING");
|
||||
bump(allocation); repository.update(allocation);
|
||||
pallet.setActualPackageCount(value(pallet.getActualPackageCount()) + 1);
|
||||
pallet.setActualVolumeCbm(volume(pallet.getActualVolumeCbm()).add(perPackage).setScale(4, RoundingMode.HALF_UP));
|
||||
pallet.setActualPackageCount(value(pallet.getActualPackageCount()) + requestedCount);
|
||||
pallet.setActualVolumeCbm(volume(pallet.getActualVolumeCbm()).add(perPackage.multiply(BigDecimal.valueOf(requestedCount))).setScale(4, RoundingMode.HALF_UP));
|
||||
repository.update(pallet); startPicking(task, plan, request.operatorId(), request.operatorName());
|
||||
PdaResponses.Carton result = new PdaResponses.Carton("SCANNED", carton.getCartonMark(), pallet.getId(),
|
||||
allocation.getId(), pallet.getActualPackageCount());
|
||||
allocation.getId(), pallet.getActualPackageCount(), pallet.getPalletNo(), requestedCount);
|
||||
saveOperation(customerId, task.getId(), allocation.getId(), "SCAN_CARTON", request.idempotencyKey(), request, result);
|
||||
return result;
|
||||
}
|
||||
@@ -377,6 +430,7 @@ public class InboundPdaWorkflowService {
|
||||
@Transactional
|
||||
public PdaResponses.Carton removeCarton(String customerId, String planNo, PdaRequests.RemoveCarton request) {
|
||||
InboundPlan plan = requirePlan(customerId, planNo);
|
||||
requireCartonScanMode(plan);
|
||||
InboundPdaOperation replay = replay(customerId, "REMOVE_CARTON", request.idempotencyKey(), request);
|
||||
if (replay != null) return read(replay, PdaResponses.Carton.class);
|
||||
InboundPdaTask task = requireActiveTask(customerId, plan);
|
||||
@@ -542,6 +596,7 @@ public class InboundPdaWorkflowService {
|
||||
if ("COMPLETED".equals(task.getStatus())) {
|
||||
return new CompletionContext(plan, task, List.of(), List.of(), List.of(), List.of());
|
||||
}
|
||||
if ("PARTIAL_INBOUND".equals(task.getStatus())) throw conflict("PDA inbound task was partially completed");
|
||||
List<Long> unloadPhotoFileIds = unloadPhotoFileIds(task);
|
||||
if (unloadPhotoFileIds.isEmpty() || task.getUnloadPhotoUploadedAt() == null
|
||||
|| task.getEstimatedEmptyReturnAt() == null) {
|
||||
@@ -589,6 +644,41 @@ public class InboundPdaWorkflowService {
|
||||
repository.update(plan);
|
||||
}
|
||||
|
||||
/** Explicit operator decision to receive only the cartons actually picked. */
|
||||
@Transactional
|
||||
public PdaResponses.Task partialComplete(String customerId, String planNo, PdaRequests.ForceComplete request) {
|
||||
InboundPlan plan = requirePlan(customerId, planNo);
|
||||
InboundPdaOperation replay = replay(customerId, "PARTIAL_COMPLETE", request.idempotencyKey(), request);
|
||||
if (replay != null) return read(replay, PdaResponses.Task.class);
|
||||
InboundPdaTask task = requireActiveTask(customerId, plan);
|
||||
List<Long> unloadPhotoFileIds = unloadPhotoFileIds(task);
|
||||
if (unloadPhotoFileIds.isEmpty() || task.getUnloadPhotoUploadedAt() == null
|
||||
|| task.getEstimatedEmptyReturnAt() == null) {
|
||||
throw conflict("Upload unload photos before partially completing the PDA inbound task");
|
||||
}
|
||||
List<InboundPlanLine> lines = repository.lines(plan.getId());
|
||||
List<InboundPlanPalletAllocation> allocations = repository.allocations(plan.getId());
|
||||
int expected = lines.stream().mapToInt(line -> value(line.getRequiredPackageCount())).sum();
|
||||
int picked = allocations.stream().mapToInt(allocation -> value(allocation.getPickedPackageCount())).sum();
|
||||
if (picked <= 0) throw conflict("At least one carton must be picked before partial inbound completion");
|
||||
if (picked >= expected) throw conflict("All cartons are picked; use normal inbound completion");
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
stockReceipts.stockIn(customerId, plan, task, lines, allocations, repository.pallets(plan.getId()), now, true);
|
||||
for (InboundPlanCarton carton : repository.cartons(plan.getId())) {
|
||||
if ("ON_PALLET".equals(carton.getStatus())) {
|
||||
carton.setStatus("AVAILABLE"); carton.setReceivedAt(now); carton.setUpdatedAt(now);
|
||||
carton.setVersion(value(carton.getVersion()) + 1); repository.update(carton);
|
||||
}
|
||||
}
|
||||
task.setStatus("PARTIAL_INBOUND"); task.setCompletedAt(now); task.setUpdatedAt(now);
|
||||
task.setVersion(value(task.getVersion()) + 1); repository.update(task);
|
||||
plan.setStatus("PARTIAL_INBOUND"); plan.setUpdatedAt(now); repository.update(plan);
|
||||
unloadCompletedOutbox.publish(plan, task, unloadPhotoFileIds);
|
||||
PdaResponses.Task result = taskResponse(task, plan, planNo);
|
||||
saveOperation(customerId, task.getId(), null, "PARTIAL_COMPLETE", request.idempotencyKey(), request, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public PdaResponses.Task uploadUnloadPhotos(String customerId, String planNo, PdaRequests.UnloadPhotos request) {
|
||||
InboundPlan plan = requirePlan(customerId, planNo);
|
||||
@@ -605,7 +695,7 @@ public class InboundPdaWorkflowService {
|
||||
task.setUpdatedAt(uploadedAt);
|
||||
task.setVersion(value(task.getVersion()) + 1);
|
||||
repository.update(task);
|
||||
PdaResponses.Task result = task(task, planNo);
|
||||
PdaResponses.Task result = taskResponse(task, plan, planNo);
|
||||
saveOperation(customerId, task.getId(), null, "UPLOAD_UNLOAD_PHOTOS", request.idempotencyKey(), request, result);
|
||||
return result;
|
||||
}
|
||||
@@ -749,12 +839,34 @@ public class InboundPdaWorkflowService {
|
||||
repository.update(plan);
|
||||
}
|
||||
|
||||
private void requireCountMode(InboundPlan plan) {
|
||||
InboundMode mode = mode(plan);
|
||||
if (mode == InboundMode.DROPSHIP) {
|
||||
throw conflict("One-click dropship inbound must be picked by complete source pallet");
|
||||
}
|
||||
if (mode == InboundMode.OWN_OVERSEAS) {
|
||||
throw conflict("Overseas warehouse inbound must be operated by one-carton scanning");
|
||||
}
|
||||
}
|
||||
|
||||
/** Backward-compatible guard for pallet merge, which remains a legacy quantity workflow. */
|
||||
private void requireLegacyCounting(InboundPlan plan) {
|
||||
if (isDropship(plan)) {
|
||||
throw conflict("One-click dropship inbound must be picked by complete source pallet");
|
||||
}
|
||||
if (repository.hasCartonLabelPool(plan.getId())) {
|
||||
throw conflict("This inbound plan has a carton manifest and must be operated by carton scanning");
|
||||
}
|
||||
}
|
||||
|
||||
private void requireCartonScanMode(InboundPlan plan) {
|
||||
if (mode(plan) != InboundMode.OWN_OVERSEAS) {
|
||||
throw conflict(mode(plan) == InboundMode.FBA
|
||||
? "FBA inbound must be operated by package quantity, not carton scanning"
|
||||
: "One-click dropship inbound must be operated by complete source pallet");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isNotAssigned(InboundPlanPallet pallet) {
|
||||
return pallet.getWarehousePalletId() == null || pallet.getWarehouseLocationId() == null
|
||||
|| pallet.getWarehouseResourceLockId() == null
|
||||
@@ -770,7 +882,7 @@ public class InboundPdaWorkflowService {
|
||||
private InboundPdaTask requireActiveTask(String customerId, InboundPlan plan) {
|
||||
InboundPdaTask task = repository.lockTask(customerId, plan.getId());
|
||||
if (task == null) throw conflict("Push the inbound plan to PDA before operating");
|
||||
if ("COMPLETED".equals(task.getStatus())) throw conflict("PDA task is already completed");
|
||||
if (terminal(task.getStatus())) throw conflict("PDA task is already completed");
|
||||
return task;
|
||||
}
|
||||
|
||||
@@ -786,6 +898,24 @@ public class InboundPdaWorkflowService {
|
||||
return value;
|
||||
}
|
||||
|
||||
private InboundPlanPallet autoPallet(InboundPlan plan, InboundPlanCarton carton) {
|
||||
List<InboundPlanPallet> candidates = repository.pallets(plan.getId()).stream()
|
||||
.filter(pallet -> sameWarehouse(carton, pallet))
|
||||
.filter(pallet -> !isNotAssigned(pallet))
|
||||
.sorted(Comparator.comparingInt(pallet -> value(pallet.getSequenceNo())))
|
||||
.toList();
|
||||
if (candidates.isEmpty()) throw conflict("No assigned pallet exists for the carton destination warehouse");
|
||||
Map<Long, Integer> plannedByPallet = new HashMap<>();
|
||||
for (InboundPlanPalletAllocation allocation : repository.allocations(plan.getId())) {
|
||||
plannedByPallet.merge(allocation.getPalletId(), value(allocation.getPlannedPackageCount()), Integer::sum);
|
||||
}
|
||||
InboundPlanPallet selected = candidates.stream()
|
||||
.filter(pallet -> value(pallet.getActualPackageCount()) < plannedByPallet.getOrDefault(pallet.getId(), Integer.MAX_VALUE))
|
||||
.findFirst().orElse(null);
|
||||
if (selected == null) throw conflict("All assigned pallets for the carton destination warehouse are full");
|
||||
return repository.lockPallet(plan.getId(), selected.getId());
|
||||
}
|
||||
|
||||
private InboundPlanLine requireLine(Long planId, Long id) {
|
||||
InboundPlanLine value = repository.line(planId, id);
|
||||
if (value == null) throw notFound("Inbound plan line not found");
|
||||
@@ -801,6 +931,30 @@ public class InboundPdaWorkflowService {
|
||||
volume(allocation.getActualVolumeCbm()), remaining, requested, merge, candidates);
|
||||
}
|
||||
|
||||
private PdaResponses.Task taskResponse(InboundPdaTask task, InboundPlan plan, String planNo) {
|
||||
int expected = repository.lines(plan.getId()).stream().mapToInt(line -> value(line.getRequiredPackageCount())).sum();
|
||||
List<InboundPlanPalletAllocation> allocations = repository.allocations(plan.getId());
|
||||
int picked = allocations.stream().mapToInt(allocation -> value(allocation.getPickedPackageCount())).sum();
|
||||
LastlegOrder order = repository.order(plan.getSourceOrderId());
|
||||
String orderType = order == null ? null : order.getOrderType();
|
||||
boolean wholePalletInbound = "ONE_CLICK_DROPSHIP".equals(orderType);
|
||||
Map<String, InboundPlanPallet> palletsByNo = wholePalletInbound
|
||||
? repository.pallets(plan.getId()).stream().collect(java.util.stream.Collectors.toMap(
|
||||
InboundPlanPallet::getPalletNo, value -> value)) : Map.of();
|
||||
Map<Long, InboundPlanPalletAllocation> allocationsByPallet = wholePalletInbound
|
||||
? allocations.stream().collect(java.util.stream.Collectors.toMap(
|
||||
InboundPlanPalletAllocation::getPalletId, value -> value)) : Map.of();
|
||||
List<PdaResponses.DropshipPallet> dropshipPallets = wholePalletInbound
|
||||
? dropshipOrders.palletSnapshots(task.getCustomerId(), order.getId()).stream()
|
||||
.map(pallet -> dropshipPallet(pallet, palletsByNo, allocationsByPallet)).toList() : List.of();
|
||||
PdaResponses.Task workInstructions = task(task, planNo);
|
||||
return new PdaResponses.Task(task.getId(), task.getTaskNo(), planNo, task.getStatus(),
|
||||
task.getPushedAt(), task.getStartedAt(), task.getCompletedAt(), unloadPhotoFileIds(task),
|
||||
task.getUnloadPhotoUploadedAt(), task.getEstimatedEmptyReturnAt(), expected, picked,
|
||||
orderType, wholePalletInbound, dropshipPallets, mode(plan).name(),
|
||||
workInstructions.pallets(), workInstructions.allocations(), workInstructions.expectedCartons());
|
||||
}
|
||||
|
||||
private PdaResponses.Task task(InboundPdaTask task, String planNo) {
|
||||
List<InboundPlanPallet> planPallets = repository.pallets(task.getPlanId());
|
||||
List<InboundPlanPalletAllocation> planAllocations = repository.allocations(task.getPlanId());
|
||||
@@ -830,8 +984,10 @@ public class InboundPdaWorkflowService {
|
||||
.toList();
|
||||
return new PdaResponses.Task(task.getId(), task.getTaskNo(), planNo, task.getStatus(),
|
||||
task.getPushedAt(), task.getStartedAt(), task.getCompletedAt(), unloadPhotoFileIds(task),
|
||||
task.getUnloadPhotoUploadedAt(), task.getEstimatedEmptyReturnAt(), pallets, allocations,
|
||||
expectedCartons);
|
||||
task.getUnloadPhotoUploadedAt(), task.getEstimatedEmptyReturnAt(),
|
||||
lines.values().stream().mapToInt(line -> value(line.getRequiredPackageCount())).sum(),
|
||||
planAllocations.stream().mapToInt(value -> InboundPdaWorkflowService.value(value.getPickedPackageCount())).sum(),
|
||||
null, false, List.of(), null, pallets, allocations, expectedCartons);
|
||||
}
|
||||
|
||||
private long recommendedPallet(InboundPlanCarton carton, List<InboundPlanPallet> pallets) {
|
||||
@@ -842,6 +998,39 @@ public class InboundPdaWorkflowService {
|
||||
.map(InboundPlanPallet::getId).findFirst().orElse(0L);
|
||||
}
|
||||
|
||||
private PdaResponses.DropshipPallet dropshipPallet(DropshipPalletSnapshot pallet,
|
||||
Map<String, InboundPlanPallet> palletsByNo,
|
||||
Map<Long, InboundPlanPalletAllocation> allocationsByPallet) {
|
||||
InboundPlanPallet planPallet = palletsByNo.get(pallet.sourcePalletNo());
|
||||
if (planPallet == null) throw conflict("Dropship source pallet is missing from the inbound plan");
|
||||
InboundPlanPalletAllocation allocation = allocationsByPallet.get(planPallet.getId());
|
||||
if (allocation == null) throw conflict("Dropship source pallet has no inbound allocation");
|
||||
return new PdaResponses.DropshipPallet(pallet.sourcePalletNo(), planPallet.getId(), allocation.getId(),
|
||||
pallet.packageCount(), pallet.skus().stream()
|
||||
.map(sku -> new PdaResponses.DropshipSku(sku.skuCode(), sku.productName(),
|
||||
sku.packageCount(), sku.productQuantity())).toList());
|
||||
}
|
||||
|
||||
private void requireDropship(InboundPlan plan) {
|
||||
if (!isDropship(plan)) {
|
||||
throw conflict("Whole-pallet picking is only available for one-click dropship inbound plans");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isDropship(InboundPlan plan) {
|
||||
LastlegOrder order = repository.order(plan.getSourceOrderId());
|
||||
return order != null && "ONE_CLICK_DROPSHIP".equals(order.getOrderType());
|
||||
}
|
||||
|
||||
private InboundMode mode(InboundPlan plan) {
|
||||
if (isDropship(plan)) return InboundMode.DROPSHIP;
|
||||
List<InboundPlanLine> lines = repository.lines(plan.getId());
|
||||
return lines.stream().anyMatch(line -> line.getFbaWarehouseCode() != null
|
||||
&& !line.getFbaWarehouseCode().isBlank()) ? InboundMode.FBA : InboundMode.OWN_OVERSEAS;
|
||||
}
|
||||
|
||||
private enum InboundMode { DROPSHIP, OWN_OVERSEAS, FBA }
|
||||
|
||||
private List<Long> unloadPhotoFileIds(InboundPdaTask task) {
|
||||
if (task.getUnloadPhotoFileIds() == null || task.getUnloadPhotoFileIds().isBlank()) return List.of();
|
||||
try {
|
||||
@@ -945,6 +1134,7 @@ public class InboundPdaWorkflowService {
|
||||
List<InboundPlanCarton> cartons) { }
|
||||
|
||||
private static int value(Integer value) { return value == null ? 0 : value; }
|
||||
private static boolean terminal(String status) { return "COMPLETED".equals(status) || "PARTIAL_INBOUND".equals(status); }
|
||||
private static BigDecimal volume(BigDecimal value) { return value == null ? ZERO_VOLUME : value; }
|
||||
private static BigDecimal nonNegative(BigDecimal value) { return value.signum() < 0 ? ZERO_VOLUME : value.setScale(4, RoundingMode.HALF_UP); }
|
||||
private static ResponseStatusException conflict(String message) { return new ResponseStatusException(HttpStatus.CONFLICT, message); }
|
||||
|
||||
+49
-23
@@ -20,6 +20,7 @@ import com.baiyue.lastleg.mapper.LastlegOrderDetailMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
|
||||
import com.baiyue.lastleg.entity.LastlegOrderStatusHistory;
|
||||
import com.baiyue.lastleg.dropship.service.DropshipOrderService;
|
||||
import com.baiyue.lastleg.dropship.model.DropshipPalletSnapshot;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -49,6 +50,13 @@ public class InboundStockReceiptService {
|
||||
public void stockIn(String customerId, InboundPlan plan, InboundPdaTask task,
|
||||
List<InboundPlanLine> lines, List<InboundPlanPalletAllocation> allocations,
|
||||
List<InboundPlanPallet> pallets, LocalDateTime receivedAt) {
|
||||
stockIn(customerId, plan, task, lines, allocations, pallets, receivedAt, false);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void stockIn(String customerId, InboundPlan plan, InboundPdaTask task,
|
||||
List<InboundPlanLine> lines, List<InboundPlanPalletAllocation> allocations,
|
||||
List<InboundPlanPallet> pallets, LocalDateTime receivedAt, boolean allowPartial) {
|
||||
Map<Long, LastlegOrder> orderById = orders.selectBatchIds(lines.stream()
|
||||
.map(InboundPlanLine::getOrderId).collect(Collectors.toSet())).stream()
|
||||
.filter(order -> customerId.equals(order.getCustomerId()))
|
||||
@@ -70,8 +78,8 @@ public class InboundStockReceiptService {
|
||||
Set<Long> receivedAllocationIds = existing.stream().map(InboundStockReceipt::getAllocationId)
|
||||
.filter(Objects::nonNull).collect(Collectors.toSet());
|
||||
if (receivedAllocationIds.equals(expectedAllocationIds)) {
|
||||
activateDropship(customerId, task.getId(), orderById, existing);
|
||||
completeOrders(customerId, plan, orderById, receivedAt);
|
||||
if (!allowPartial) activateDropship(customerId, task.getId(), orderById, existing);
|
||||
completeOrders(customerId, plan, orderById, receivedAt, allowPartial);
|
||||
return;
|
||||
}
|
||||
throw conflict("PDA stock-in receipts are incomplete; repair the task receipts before retrying");
|
||||
@@ -85,7 +93,9 @@ public class InboundStockReceiptService {
|
||||
.getOrDefault(line.getId(), List.of());
|
||||
long pickedPackages = lineAllocations.stream()
|
||||
.mapToLong(allocation -> value(allocation.getPickedPackageCount())).sum();
|
||||
if (pickedPackages != value(line.getRequiredPackageCount())) {
|
||||
if (pickedPackages == 0 && allowPartial) continue;
|
||||
if ((!allowPartial && pickedPackages != value(line.getRequiredPackageCount()))
|
||||
|| (allowPartial && pickedPackages > value(line.getRequiredPackageCount()))) {
|
||||
throw conflict("PDA picked packages must equal the inbound order package total");
|
||||
}
|
||||
List<ReceivingSkuReceipt> exactSkuReceipts = skuReceiptsByLine.getOrDefault(line.getId(), List.of());
|
||||
@@ -95,12 +105,25 @@ public class InboundStockReceiptService {
|
||||
continue;
|
||||
}
|
||||
List<SkuRemainder> skuRemainders = "ONE_CLICK_DROPSHIP".equals(order.getOrderType())
|
||||
? List.of() : skuRemainders(order.getId(), pickedPackages);
|
||||
? List.of() : skuRemainders(order.getId(), value(line.getRequiredPackageCount()));
|
||||
Map<String, DropshipPalletSnapshot> dropshipPallets = "ONE_CLICK_DROPSHIP".equals(order.getOrderType())
|
||||
? dropshipOrders.palletSnapshots(customerId, order.getId()).stream()
|
||||
.collect(Collectors.toMap(DropshipPalletSnapshot::sourcePalletNo, value -> value))
|
||||
: Map.of();
|
||||
for (InboundPlanPalletAllocation allocation : lineAllocations) {
|
||||
InboundPlanPallet pallet = requirePallet(palletById, allocation);
|
||||
if ("ONE_CLICK_DROPSHIP".equals(order.getOrderType())) {
|
||||
created.add(persist(customerId, plan, task, line, allocation, pallet, order,
|
||||
SkuSnapshot.unspecified(value(allocation.getPickedPackageCount())), receivedAt));
|
||||
if (value(allocation.getPickedPackageCount()) != value(allocation.getPlannedPackageCount())) {
|
||||
throw conflict("One-click dropship inbound must receive a complete source pallet");
|
||||
}
|
||||
DropshipPalletSnapshot sourcePallet = dropshipPallets.get(pallet.getPalletNo());
|
||||
if (sourcePallet == null || sourcePallet.packageCount() != value(allocation.getPickedPackageCount())) {
|
||||
throw conflict("One-click dropship source pallet SKU package total does not match PDA receipt");
|
||||
}
|
||||
for (DropshipPalletSnapshot.Sku sku : sourcePallet.skus()) {
|
||||
created.add(persist(customerId, plan, task, line, allocation, pallet, order,
|
||||
new SkuSnapshot(sku.skuCode(), sku.productName(), null, sku.packageCount()), receivedAt));
|
||||
}
|
||||
} else {
|
||||
long allocationPackages = value(allocation.getPickedPackageCount());
|
||||
for (SkuSnapshot sku : consume(skuRemainders, allocationPackages)) {
|
||||
@@ -108,12 +131,12 @@ public class InboundStockReceiptService {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (skuRemainders.stream().anyMatch(value -> value.remaining > 0)) {
|
||||
if (!allowPartial && skuRemainders.stream().anyMatch(value -> value.remaining > 0)) {
|
||||
throw conflict("Inbound SKU package snapshots were not fully received");
|
||||
}
|
||||
}
|
||||
activateDropship(customerId, task.getId(), orderById, created);
|
||||
completeOrders(customerId, plan, orderById, receivedAt);
|
||||
if (!allowPartial) activateDropship(customerId, task.getId(), orderById, created);
|
||||
completeOrders(customerId, plan, orderById, receivedAt, allowPartial);
|
||||
}
|
||||
|
||||
private List<InboundStockReceipt> persistExactSkuReceipts(String customerId, InboundPlan plan,
|
||||
@@ -159,14 +182,16 @@ public class InboundStockReceiptService {
|
||||
}
|
||||
|
||||
private void completeOrders(String customerId, InboundPlan plan, Map<Long, LastlegOrder> orderById,
|
||||
LocalDateTime completedAt) {
|
||||
LocalDateTime completedAt, boolean partial) {
|
||||
String subOrderStatus = partial ? "INBOUND_PLAN:PARTIAL_INBOUND" : "INBOUND_PLAN:COMPLETED";
|
||||
for (LastlegOrder snapshot : orderById.values()) {
|
||||
LastlegOrder order = orders.lockById(customerId, snapshot.getId());
|
||||
if (order == null) throw conflict("Inbound tail-leg order disappeared while completing stock-in");
|
||||
if ("INBOUND_COMPLETED".equals(order.getCurrentStatus())) continue;
|
||||
if ("INBOUND_COMPLETED".equals(order.getCurrentStatus())
|
||||
&& subOrderStatus.equals(order.getSubOrderStatus())) continue;
|
||||
String before = order.getCurrentStatus();
|
||||
order.setCurrentStatus("INBOUND_COMPLETED");
|
||||
order.setSubOrderStatus("INBOUND_PLAN:COMPLETED");
|
||||
order.setSubOrderStatus(subOrderStatus);
|
||||
order.setUpdatedAt(completedAt);
|
||||
orders.updateById(order);
|
||||
LastlegOrderStatusHistory history = new LastlegOrderStatusHistory();
|
||||
@@ -174,8 +199,11 @@ public class InboundStockReceiptService {
|
||||
history.setOrderId(order.getId());
|
||||
history.setFromStatus(before);
|
||||
history.setToStatus("INBOUND_COMPLETED");
|
||||
history.setEventId("PDA_INBOUND_COMPLETE:" + plan.getPlanNo());
|
||||
history.setRemark("PDA inbound completed and inventory stocked in");
|
||||
history.setEventId((partial ? "PDA_PARTIAL_INBOUND_COMPLETE:" : "PDA_INBOUND_COMPLETE:")
|
||||
+ plan.getPlanNo());
|
||||
history.setRemark(partial
|
||||
? "PDA inbound partially completed and actual inventory stocked in"
|
||||
: "PDA inbound completed and inventory stocked in");
|
||||
history.setCreatedAt(completedAt);
|
||||
orderHistories.insert(history);
|
||||
}
|
||||
@@ -207,15 +235,17 @@ public class InboundStockReceiptService {
|
||||
if (overseasWarehouseId.equals(trim(order.getSourceWarehouseId()))) {
|
||||
throw conflict("Actual overseas warehouse must not use the domestic source warehouse");
|
||||
}
|
||||
InventoryCommandResponse result = inventory.stockIn(customerId, task.getOperatorId(),
|
||||
new InventoryCommandRequests.StockIn(null, customerName, overseasWarehouseId,
|
||||
null, overseasWarehouseName, line.getDestinationWarehouseId(), destinationCode,
|
||||
InventoryCommandRequests.StockIn stockIn = new InventoryCommandRequests.StockIn(null, customerName,
|
||||
overseasWarehouseId, null, overseasWarehouseName, line.getDestinationWarehouseId(), destinationCode,
|
||||
line.getDestinationWarehouseName(), order.getId(), plan.getPlanNo(), line.getInboundCode(),
|
||||
sku.skuCode(), sku.productNameZh(), sku.productNameEn(),
|
||||
pallet.getWarehousePalletId(), pallet.getPalletNo(), plan.getPlanNo(),
|
||||
pallet.getWarehouseLocationId(), null, packages, order.getOrderNo(),
|
||||
order.getSourceEventId(), operationKey,
|
||||
"PDA inbound completion for " + line.getInboundCode()));
|
||||
"PDA inbound completion for " + line.getInboundCode());
|
||||
InventoryCommandResponse result = Boolean.TRUE.equals(order.getDetained())
|
||||
? inventory.stockInHeld(customerId, task.getOperatorId(), stockIn)
|
||||
: inventory.stockIn(customerId, task.getOperatorId(), stockIn);
|
||||
|
||||
InboundStockReceipt receipt = new InboundStockReceipt();
|
||||
receipt.setId(IdWorker.getId());
|
||||
@@ -334,9 +364,5 @@ public class InboundStockReceiptService {
|
||||
}
|
||||
}
|
||||
|
||||
private record SkuSnapshot(String skuCode, String productNameZh, String productNameEn, long packages) {
|
||||
private static SkuSnapshot unspecified(long packages) {
|
||||
return new SkuSnapshot("UNSPECIFIED", null, null, packages);
|
||||
}
|
||||
}
|
||||
private record SkuSnapshot(String skuCode, String productNameZh, String productNameEn, long packages) {}
|
||||
}
|
||||
|
||||
+3
-3
@@ -15,7 +15,7 @@ import org.springframework.stereotype.Component;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
/** 至少一次投递海外卸柜完成事件;国内事件桥按 eventId 幂等接收。 */
|
||||
/** 至少一次投递海外到国内业务事件;国内事件桥按 eventId 幂等接收。 */
|
||||
@Component
|
||||
@Slf4j
|
||||
public class InboundUnloadCompletedOutboxRelay {
|
||||
@@ -44,7 +44,7 @@ public class InboundUnloadCompletedOutboxRelay {
|
||||
} catch (Exception exception) {
|
||||
repository.retry(row.getId(), workerId, LocalDateTime.now().plusSeconds(Math.max(retrySeconds, 1)),
|
||||
exception.getMessage());
|
||||
log.warn("海外卸柜完成事件投递失败: eventKey={}, reason={}", row.getEventKey(), exception.getMessage());
|
||||
log.warn("海外到国内事件投递失败: eventKey={}, reason={}", row.getEventKey(), exception.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,7 @@ public class InboundUnloadCompletedOutboxRelay {
|
||||
com.baiyue.common.core.api.ApiResponse<EventBridgeClient.Acknowledgement> response) {
|
||||
if (response == null) throw new IllegalStateException("国内事件桥返回空响应");
|
||||
if (!response.successful()) {
|
||||
throw new IllegalStateException("国内事件桥拒绝海外卸柜完成事件: code=" + response.code()
|
||||
throw new IllegalStateException("国内事件桥拒绝海外事件: code=" + response.code()
|
||||
+ ", message=" + response.message() + ", requestId=" + response.requestId());
|
||||
}
|
||||
if (response.data() == null) throw new IllegalStateException("国内事件桥成功响应缺少确认数据");
|
||||
|
||||
+18
-4
@@ -61,6 +61,7 @@ import java.util.stream.Collectors;
|
||||
public class ReceivingContainerService {
|
||||
private static final String PHOTO_DOWNLOAD_PATH =
|
||||
"/api/pda/v1/last-leg/receiving-containers/current/unload-photos/files/{fileId}/download";
|
||||
private static final String PALLET_MERGE_HINT = "该入仓单已标记合托,请按合托要求拣货";
|
||||
|
||||
@Resource private InboundPlanMapper plans;
|
||||
@Resource private InboundPlanLineMapper lines;
|
||||
@@ -258,7 +259,8 @@ public class ReceivingContainerService {
|
||||
inbound.inboundCode(), inbound.destinationType(), inbound.overseasWarehouseId(),
|
||||
inbound.overseasWarehouseName(), inbound.destinationWarehouseId(),
|
||||
inbound.destinationWarehouseCode(), inbound.destinationWarehouseName(), inbound.fbaWarehouseCode(),
|
||||
inbound.plannedPalletCount(), inbound.totalPackages(), inbound.receivedPackages(), inbound.pendingPackages());
|
||||
inbound.plannedPalletCount(), inbound.totalPackages(), inbound.receivedPackages(), inbound.pendingPackages(),
|
||||
inbound.palletMergeRequired(), inbound.palletMergeHint());
|
||||
}
|
||||
|
||||
public ReceivingContainerResponses.PalletResolved resolvePallet(String operatorKey,
|
||||
@@ -309,7 +311,8 @@ public class ReceivingContainerService {
|
||||
int total = fbaPlanRefs.stream().mapToInt(refValue -> value(refValue.line().getRequiredPackageCount())).sum();
|
||||
ReceivingContainerResponses.Receipt result = new ReceivingContainerResponses.Receipt("RECEIVED",
|
||||
session.getContainerNo(), ref.line().getInboundCode(), "FBA", resource.palletNo(), accepted,
|
||||
received, Math.max(0, total - received), session.getStatus());
|
||||
received, Math.max(0, total - received), session.getStatus(), palletMergeRequired(ref.order()),
|
||||
palletMergeHint(ref.order()));
|
||||
save(session, "BULK_RECEIVE", request.idempotencyKey(), request, result);
|
||||
return result;
|
||||
} catch (RuntimeException failure) {
|
||||
@@ -347,7 +350,8 @@ public class ReceivingContainerService {
|
||||
ReceivingContainerResponses.Inbound inbound = inbound(ref);
|
||||
ReceivingContainerResponses.Receipt result = new ReceivingContainerResponses.Receipt("RECEIVED",
|
||||
session.getContainerNo(), inbound.inboundCode(), inbound.destinationType(), resource.palletNo(), 1,
|
||||
inbound.receivedPackages(), inbound.pendingPackages(), session.getStatus());
|
||||
inbound.receivedPackages(), inbound.pendingPackages(), session.getStatus(),
|
||||
inbound.palletMergeRequired(), inbound.palletMergeHint());
|
||||
save(session, "CARTON_RECEIVE", request.idempotencyKey(), request, result);
|
||||
return result;
|
||||
} catch (RuntimeException failure) {
|
||||
@@ -666,7 +670,17 @@ public class ReceivingContainerService {
|
||||
: String.valueOf(ref.plan().getOverseasWarehouseId()), ref.order().getOverseasWarehouseName(),
|
||||
ref.line().getDestinationWarehouseId(), ref.line().getDestinationWarehouseCode(),
|
||||
ref.line().getDestinationWarehouseName(), ref.line().getFbaWarehouseCode(),
|
||||
value(ref.line().getPlannedPalletCount()), total, received, Math.max(0, total - received));
|
||||
value(ref.line().getPlannedPalletCount()), total, received, Math.max(0, total - received),
|
||||
palletMergeRequired(ref.order()), palletMergeHint(ref.order()));
|
||||
}
|
||||
|
||||
private boolean palletMergeRequired(LastlegOrder order) {
|
||||
return order != null && ("REQUIRED".equals(order.getPalletMergeFlag())
|
||||
|| "MERGED".equals(order.getPalletMergeFlag()));
|
||||
}
|
||||
|
||||
private String palletMergeHint(LastlegOrder order) {
|
||||
return palletMergeRequired(order) ? PALLET_MERGE_HINT : null;
|
||||
}
|
||||
|
||||
private Totals totals(List<InboundPlan> containerPlans) {
|
||||
|
||||
+150
-18
@@ -7,6 +7,7 @@ import com.baiyue.lastleg.inboundplan.dto.InboundPlanListResponse;
|
||||
import com.baiyue.lastleg.inboundplan.dto.InboundPlanRequests;
|
||||
import com.baiyue.lastleg.inboundplan.dto.InboundPlanResponse;
|
||||
import com.baiyue.lastleg.inboundplan.dto.SupplementPalletRequest;
|
||||
import com.baiyue.lastleg.inboundplan.dto.UpdateInboundPlanRequest;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlan;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanLine;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanPallet;
|
||||
@@ -33,10 +34,12 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class InboundPlanService {
|
||||
@Resource private com.baiyue.lastleg.service.LastlegOrderNoGenerator orderNoGenerator;
|
||||
@Resource private InboundPlanMapper planMapper;
|
||||
@Resource private InboundPlanLineMapper lineMapper;
|
||||
@Resource private InboundPlanPalletMapper palletMapper;
|
||||
@@ -61,6 +64,37 @@ public class InboundPlanService {
|
||||
|
||||
@Transactional
|
||||
public InboundPlanResponse convert(String customerId, Long selectedOrderId) {
|
||||
return convert(customerId, selectedOrderId, List.of());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InboundPlanResponse convert(String customerId, Long selectedOrderId, List<String> sourcePalletNos) {
|
||||
return convert(customerId, selectedOrderId, sourcePalletNos, null);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InboundPlanResponse convertDropship(String customerId, Long selectedOrderId,
|
||||
List<WholePalletInput> sourcePallets) {
|
||||
if (sourcePallets == null || sourcePallets.isEmpty()) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||
"One-click dropship inbound requires source pallets");
|
||||
}
|
||||
HashSet<String> palletNos = new HashSet<>();
|
||||
for (WholePalletInput pallet : sourcePallets) {
|
||||
if (pallet == null || trimToNull(pallet.palletNo()) == null || pallet.packageCount() <= 0) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||
"One-click dropship source pallet number and package count are required");
|
||||
}
|
||||
if (!palletNos.add(pallet.palletNo().trim())) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||
"Duplicate one-click dropship source pallet number: " + pallet.palletNo());
|
||||
}
|
||||
}
|
||||
return convert(customerId, selectedOrderId, List.of(), List.copyOf(sourcePallets));
|
||||
}
|
||||
|
||||
private InboundPlanResponse convert(String customerId, Long selectedOrderId, List<String> sourcePalletNos,
|
||||
List<WholePalletInput> wholePallets) {
|
||||
if (customerId == null || customerId.isBlank() || "0".equals(customerId.trim())) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "平台生成入库计划必须指定客户");
|
||||
}
|
||||
@@ -75,16 +109,17 @@ public class InboundPlanService {
|
||||
return detail(customerId, existing);
|
||||
}
|
||||
validateOrders(orders);
|
||||
return createPlan(customerId, selectedOrderId, orders);
|
||||
return createPlan(customerId, selectedOrderId, orders, sourcePalletNos, wholePallets);
|
||||
}
|
||||
|
||||
private InboundPlanResponse createPlan(String customerId, Long selectedOrderId,
|
||||
List<SourceOrderSnapshot> orders) {
|
||||
List<SourceOrderSnapshot> orders, List<String> sourcePalletNos,
|
||||
List<WholePalletInput> wholePallets) {
|
||||
String containerNo = requiredContainerNo(orders.get(0).containerNo());
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
InboundPlan plan = new InboundPlan();
|
||||
plan.setId(IdWorker.getId());
|
||||
plan.setPlanNo("LIP" + plan.getId());
|
||||
plan.setPlanNo(orderNoGenerator.next("INBOUND_PLAN", orders.get(0).destinationWarehouseCode(), plan.getId()));
|
||||
plan.setCustomerId(customerId);
|
||||
plan.setSourceOrderId(selectedOrderId);
|
||||
plan.setContainerNo(containerNo);
|
||||
@@ -98,20 +133,27 @@ public class InboundPlanService {
|
||||
Map<Long, SourceOrderSnapshot> orderById = orders.stream()
|
||||
.collect(Collectors.toMap(SourceOrderSnapshot::orderId, value -> value));
|
||||
Map<Long, InboundPlanLine> lineByOrder = createLines(plan, orders);
|
||||
PalletAllocator.Result allocation = PalletAllocator.allocate(orders.stream()
|
||||
PalletAllocator.Result allocation = wholePallets == null ? PalletAllocator.allocate(orders.stream()
|
||||
.map(order -> new PalletAllocator.LineInput(order.orderId(), warehouseKey(order), volume(order),
|
||||
order.packageCount()))
|
||||
.toList());
|
||||
plan.setPalletCount(allocation.pallets().size());
|
||||
order.packageCount(), order.recommendedPalletCount())).toList()) : null;
|
||||
if (wholePallets != null) validateWholePallets(orders, wholePallets);
|
||||
plan.setPalletCount(wholePallets == null ? allocation.pallets().size() : wholePallets.size());
|
||||
planMapper.insert(plan);
|
||||
|
||||
lineByOrder.values().forEach(lineMapper::insert);
|
||||
Map<PalletAllocator.PalletDraft, InboundPlanPallet> persistedPallets = createPallets(plan, allocation, orderById);
|
||||
createAllocations(plan, allocation, lineByOrder, persistedPallets);
|
||||
|
||||
Map<Long, Integer> palletCounts = allocation.allocations().stream().collect(Collectors.groupingBy(
|
||||
PalletAllocator.AllocationDraft::orderId,
|
||||
Collectors.collectingAndThen(Collectors.mapping(item -> item.pallet().sequenceNo(), Collectors.toSet()), java.util.Set::size)));
|
||||
Map<Long, Integer> palletCounts;
|
||||
if (wholePallets == null) {
|
||||
Map<PalletAllocator.PalletDraft, InboundPlanPallet> persistedPallets = createPallets(plan, allocation,
|
||||
orderById, sourcePalletNos == null ? List.of() : sourcePalletNos);
|
||||
createAllocations(plan, allocation, lineByOrder, persistedPallets);
|
||||
palletCounts = allocation.allocations().stream().collect(Collectors.groupingBy(
|
||||
PalletAllocator.AllocationDraft::orderId, Collectors.collectingAndThen(Collectors.mapping(
|
||||
item -> item.pallet().sequenceNo(), Collectors.toSet()), java.util.Set::size)));
|
||||
} else {
|
||||
SourceOrderSnapshot order = orders.get(0);
|
||||
createWholePallets(plan, order, lineByOrder.get(order.orderId()), wholePallets);
|
||||
palletCounts = Map.of(order.orderId(), wholePallets.size());
|
||||
}
|
||||
for (SourceOrderSnapshot order : orders) {
|
||||
int count = palletCounts.getOrDefault(order.orderId(), 0);
|
||||
InboundPlanLine line = lineByOrder.get(order.orderId());
|
||||
@@ -180,7 +222,7 @@ public class InboundPlanService {
|
||||
List<InboundPlanResponse> created = new ArrayList<>(refs.size());
|
||||
for (int index = 0; index < refs.size(); index++) {
|
||||
InboundPlanOrderMapper.ContainerOrderRef ref = refs.get(index);
|
||||
created.add(createPlan(ref.customerId(), ref.orderId(), snapshots.get(index)));
|
||||
created.add(createPlan(ref.customerId(), ref.orderId(), snapshots.get(index), List.of(), null));
|
||||
}
|
||||
return List.copyOf(created);
|
||||
}
|
||||
@@ -201,6 +243,53 @@ public class InboundPlanService {
|
||||
}
|
||||
}
|
||||
|
||||
private void validateWholePallets(List<SourceOrderSnapshot> orders, List<WholePalletInput> pallets) {
|
||||
if (orders.size() != 1) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||
"One-click dropship inbound plan must belong to one inbound order");
|
||||
}
|
||||
int packages = pallets.stream().mapToInt(WholePalletInput::packageCount).sum();
|
||||
if (packages != orders.get(0).packageCount()) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||
"One-click dropship pallet package total must equal inbound order package count");
|
||||
}
|
||||
}
|
||||
|
||||
private void createWholePallets(InboundPlan plan, SourceOrderSnapshot order, InboundPlanLine line,
|
||||
List<WholePalletInput> sourcePallets) {
|
||||
BigDecimal remainingVolume = volume(order);
|
||||
int remainingPackages = order.packageCount();
|
||||
for (int index = 0; index < sourcePallets.size(); index++) {
|
||||
WholePalletInput source = sourcePallets.get(index);
|
||||
BigDecimal palletVolume = index == sourcePallets.size() - 1 ? remainingVolume
|
||||
: volume(order).multiply(BigDecimal.valueOf(source.packageCount()))
|
||||
.divide(BigDecimal.valueOf(order.packageCount()), 4, java.math.RoundingMode.HALF_UP);
|
||||
if (palletVolume.compareTo(remainingVolume) > 0) palletVolume = remainingVolume;
|
||||
InboundPlanPallet pallet = new InboundPlanPallet();
|
||||
pallet.setId(IdWorker.getId()); pallet.setCustomerId(plan.getCustomerId()); pallet.setPlanId(plan.getId());
|
||||
pallet.setPalletNo(source.palletNo().trim()); pallet.setDestinationWarehouseId(order.destinationWarehouseId());
|
||||
pallet.setDestinationWarehouseCode(order.destinationWarehouseCode());
|
||||
pallet.setDestinationWarehouseName(order.destinationWarehouseName());
|
||||
pallet.setCapacityCbm(PalletAllocator.PALLET_CAPACITY_CBM); pallet.setAllocatedVolumeCbm(palletVolume);
|
||||
pallet.setSequenceNo(index + 1); pallet.setAssignmentStatus("UNASSIGNED");
|
||||
pallet.setActualVolumeCbm(BigDecimal.ZERO); pallet.setActualPackageCount(0); palletMapper.insert(pallet);
|
||||
|
||||
InboundPlanPalletAllocation allocation = new InboundPlanPalletAllocation();
|
||||
allocation.setId(IdWorker.getId()); allocation.setCustomerId(plan.getCustomerId()); allocation.setPlanId(plan.getId());
|
||||
allocation.setLineId(line.getId()); allocation.setPalletId(pallet.getId());
|
||||
allocation.setAllocatedVolumeCbm(palletVolume); allocation.setPlannedPackageCount(source.packageCount());
|
||||
allocation.setCountedPackageCount(0); allocation.setPickedPackageCount(0);
|
||||
allocation.setActualVolumeCbm(BigDecimal.ZERO); allocation.setStatus("PENDING_COUNT"); allocation.setVersion(0);
|
||||
allocationMapper.insert(allocation);
|
||||
remainingVolume = remainingVolume.subtract(palletVolume);
|
||||
remainingPackages -= source.packageCount();
|
||||
}
|
||||
if (remainingPackages != 0 || remainingVolume.signum() != 0) {
|
||||
throw new IllegalStateException("Failed to conserve one-click dropship pallet totals");
|
||||
}
|
||||
}
|
||||
|
||||
public record WholePalletInput(String palletNo, int packageCount) {}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public InboundPlanResponse detailByPlanNo(String customerId, String planNo) {
|
||||
@@ -214,6 +303,44 @@ public class InboundPlanService {
|
||||
return detail(customerId, plan);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InboundPlanResponse update(String customerId, String planNo, UpdateInboundPlanRequest request) {
|
||||
InboundPlan plan = planMapper.selectOne(new LambdaQueryWrapper<InboundPlan>()
|
||||
.eq(InboundPlan::getPlanNo, planNo).eq(customerId != null, InboundPlan::getCustomerId, customerId).last("FOR UPDATE"));
|
||||
if (plan == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Inbound plan not found");
|
||||
if (!java.util.Set.of("PENDING_ASSIGNMENT", "ASSIGNED").contains(plan.getStatus()))
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "Inbound plan has started and cannot be edited");
|
||||
int currentVersion = plan.getVersion() == null ? 0 : plan.getVersion();
|
||||
if (request.version() != currentVersion)
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "Inbound plan version is stale; reload before editing");
|
||||
plan.setOverseasWarehouseId(request.overseasWarehouseId());
|
||||
plan.setUpdatedAt(LocalDateTime.now());
|
||||
planMapper.updateById(plan);
|
||||
// Keep the denormalized destination snapshot used by list queries in sync with the plan.
|
||||
List<InboundPlanLine> lines = lineMapper.selectList(new LambdaQueryWrapper<InboundPlanLine>()
|
||||
.eq(InboundPlanLine::getPlanId, plan.getId())
|
||||
.eq(InboundPlanLine::getCustomerId, plan.getCustomerId()));
|
||||
for (InboundPlanLine line : lines) {
|
||||
line.setDestinationWarehouseId(String.valueOf(request.overseasWarehouseId()));
|
||||
if (trimToNull(request.warehouseCode()) != null) line.setDestinationWarehouseCode(request.warehouseCode().trim());
|
||||
if (trimToNull(request.warehouseName()) != null) line.setDestinationWarehouseName(request.warehouseName().trim());
|
||||
lineMapper.updateById(line);
|
||||
}
|
||||
// Pallets carry the same destination snapshot and are consumed by PDA/resource assignment.
|
||||
// Update them in this transaction so a plan edit cannot leave lines and pallets pointing at
|
||||
// different warehouses.
|
||||
List<InboundPlanPallet> pallets = palletMapper.selectList(new LambdaQueryWrapper<InboundPlanPallet>()
|
||||
.eq(InboundPlanPallet::getPlanId, plan.getId())
|
||||
.eq(InboundPlanPallet::getCustomerId, plan.getCustomerId()));
|
||||
for (InboundPlanPallet pallet : pallets) {
|
||||
pallet.setDestinationWarehouseId(String.valueOf(request.overseasWarehouseId()));
|
||||
if (trimToNull(request.warehouseCode()) != null) pallet.setDestinationWarehouseCode(request.warehouseCode().trim());
|
||||
if (trimToNull(request.warehouseName()) != null) pallet.setDestinationWarehouseName(request.warehouseName().trim());
|
||||
palletMapper.updateById(pallet);
|
||||
}
|
||||
return detail(customerId, plan);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InboundPlanResponse.Pallet supplementPallet(String customerId, String planNo, SupplementPalletRequest request) {
|
||||
InboundPlan plan = planMapper.selectOne(new LambdaQueryWrapper<InboundPlan>()
|
||||
@@ -265,7 +392,8 @@ public class InboundPlanService {
|
||||
}
|
||||
|
||||
private Map<PalletAllocator.PalletDraft, InboundPlanPallet> createPallets(
|
||||
InboundPlan plan, PalletAllocator.Result allocation, Map<Long, SourceOrderSnapshot> orderById) {
|
||||
InboundPlan plan, PalletAllocator.Result allocation, Map<Long, SourceOrderSnapshot> orderById,
|
||||
List<String> sourcePalletNos) {
|
||||
Map<String, SourceOrderSnapshot> warehouseSamples = orderById.values().stream()
|
||||
.collect(Collectors.toMap(this::warehouseKey, value -> value, (left, right) -> left));
|
||||
Map<PalletAllocator.PalletDraft, InboundPlanPallet> result = new HashMap<>();
|
||||
@@ -275,7 +403,10 @@ public class InboundPlanService {
|
||||
pallet.setId(IdWorker.getId());
|
||||
pallet.setCustomerId(plan.getCustomerId());
|
||||
pallet.setPlanId(plan.getId());
|
||||
pallet.setPalletNo(plan.getPlanNo() + "-P" + String.format("%03d", result.size() + 1));
|
||||
int palletIndex = result.size();
|
||||
String sourcePalletNo = palletIndex < sourcePalletNos.size() ? sourcePalletNos.get(palletIndex) : null;
|
||||
pallet.setPalletNo(sourcePalletNo == null || sourcePalletNo.isBlank()
|
||||
? plan.getPlanNo() + "-P" + String.format("%03d", palletIndex + 1) : sourcePalletNo.trim());
|
||||
pallet.setDestinationWarehouseId(sample.destinationWarehouseId());
|
||||
pallet.setDestinationWarehouseCode(sample.destinationWarehouseCode());
|
||||
pallet.setDestinationWarehouseName(sample.destinationWarehouseName());
|
||||
@@ -319,7 +450,7 @@ public class InboundPlanService {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT,
|
||||
"Inbound code " + order.inboundCode() + " is not ready for an inbound plan");
|
||||
}
|
||||
if (order.destinationCount() != 1 || order.destinationMismatchCount() != 0 || warehouseKey(order) == null) {
|
||||
if (order.destinationCount() != 1 || warehouseKey(order) == null) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||
"Inbound code " + order.inboundCode()
|
||||
+ " must have one destination warehouse consistent with all cargo details");
|
||||
@@ -365,7 +496,8 @@ public class InboundPlanService {
|
||||
pallet.getActualPackageCount())).toList(),
|
||||
allocations.stream().map(item -> new InboundPlanResponse.Allocation(item.getLineId(), item.getPalletId(),
|
||||
item.getAllocatedVolumeCbm(), item.getPlannedPackageCount(), item.getCountedPackageCount(),
|
||||
item.getPickedPackageCount(), item.getActualVolumeCbm(), item.getStatus(), item.getVersion())).toList());
|
||||
item.getPickedPackageCount(), item.getActualVolumeCbm(), item.getStatus(), item.getVersion())).toList(),
|
||||
plan.getVersion() == null ? 0 : plan.getVersion());
|
||||
}
|
||||
|
||||
private BigDecimal volume(SourceOrderSnapshot order) {
|
||||
|
||||
+22
-1
@@ -34,6 +34,23 @@ public final class PalletAllocator {
|
||||
PalletDraft current = null;
|
||||
int sequence = 0;
|
||||
for (LineInput line : group) {
|
||||
if (line.recommendedPalletCount() > 0) {
|
||||
int minimumByVolume = line.volumeCbm().divide(PALLET_CAPACITY_CBM, 0, RoundingMode.CEILING).intValue();
|
||||
int count = Math.max(line.recommendedPalletCount(), minimumByVolume);
|
||||
BigDecimal remaining = line.volumeCbm();
|
||||
for (int index = 0; index < count; index++) {
|
||||
PalletDraft recommended = new PalletDraft(line.warehouseKey(), ++sequence, BigDecimal.ZERO);
|
||||
pallets.add(recommended);
|
||||
int remainingPallets = count - index;
|
||||
BigDecimal allocated = remainingPallets == 1 ? remaining
|
||||
: remaining.divide(BigDecimal.valueOf(remainingPallets), 4, RoundingMode.HALF_UP);
|
||||
recommended.add(allocated);
|
||||
allocations.add(new AllocationDraft(line.orderId(), recommended, allocated, 0));
|
||||
remaining = remaining.subtract(allocated);
|
||||
}
|
||||
current = null;
|
||||
continue;
|
||||
}
|
||||
BigDecimal remaining = line.volumeCbm();
|
||||
while (remaining.signum() > 0) {
|
||||
if (current == null || current.remainingCapacity().signum() == 0) {
|
||||
@@ -85,7 +102,11 @@ public final class PalletAllocator {
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
public record LineInput(Long orderId, String warehouseKey, BigDecimal volumeCbm, int packageCount) {
|
||||
public record LineInput(Long orderId, String warehouseKey, BigDecimal volumeCbm, int packageCount,
|
||||
int recommendedPalletCount) {
|
||||
public LineInput(Long orderId, String warehouseKey, BigDecimal volumeCbm, int packageCount) {
|
||||
this(orderId, warehouseKey, volumeCbm, packageCount, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class PalletDraft {
|
||||
|
||||
Reference in New Issue
Block a user