入库修改
This commit is contained in:
+10
-1
@@ -6,6 +6,7 @@ import com.baiyue.lastleg.inboundplan.dto.ConvertInboundPlanRequest;
|
||||
import com.baiyue.lastleg.inboundplan.dto.BatchFbaAppointmentRequest;
|
||||
import com.baiyue.lastleg.inboundplan.dto.FbaAppointmentResponse;
|
||||
import com.baiyue.lastleg.inboundplan.dto.InboundPlanResponse;
|
||||
import com.baiyue.lastleg.inboundplan.dto.SupplementPalletRequest;
|
||||
import com.baiyue.lastleg.inboundplan.service.InboundFbaAppointmentService;
|
||||
import com.baiyue.lastleg.inboundplan.service.InboundPlanService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@@ -25,7 +26,7 @@ public class InboundPlanController {
|
||||
@Resource private InboundFbaAppointmentService appointmentService;
|
||||
|
||||
@PostMapping("/convert")
|
||||
@Operation(summary = "Convert the entire container to one inbound plan")
|
||||
@Operation(summary = "Convert one inbound order to one inbound plan")
|
||||
public ApiResponse<InboundPlanResponse> convert(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody ConvertInboundPlanRequest request) {
|
||||
return ApiResponse.success(service.convert(scope(user, request.customerId()), request.orderId()));
|
||||
@@ -39,6 +40,14 @@ public class InboundPlanController {
|
||||
return ApiResponse.success(service.detailByPlanNo(scope(user, customerId), planNo));
|
||||
}
|
||||
|
||||
@PostMapping("/{planNo}/pallets/supplement")
|
||||
@Operation(summary = "Add one printable supplemental pallet for a plan destination warehouse")
|
||||
public ApiResponse<InboundPlanResponse.Pallet> supplementPallet(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String planNo,
|
||||
@Valid @RequestBody SupplementPalletRequest request) {
|
||||
return ApiResponse.success(service.supplementPallet(scope(user, request.customerId()), planNo, request));
|
||||
}
|
||||
|
||||
@PostMapping("/fba-appointments/batch")
|
||||
@Operation(summary = "Create one FBA appointment for selected inbound lines")
|
||||
public ApiResponse<FbaAppointmentResponse> batchFbaAppointment(
|
||||
|
||||
+1
@@ -8,6 +8,7 @@ public record InboundPlanResponse(
|
||||
Long id,
|
||||
String planNo,
|
||||
String customerId,
|
||||
Long sourceOrderId,
|
||||
String containerNo,
|
||||
String status,
|
||||
int lineCount,
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package com.baiyue.lastleg.inboundplan.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
/** 托盘纸不足时,按计划内目的仓补打一张空托盘标签。 */
|
||||
public record SupplementPalletRequest(@NotBlank String destinationWarehouseId, String destinationWarehouseCode,
|
||||
String destinationWarehouseName, String customerId) {}
|
||||
@@ -15,6 +15,7 @@ public class InboundPlan {
|
||||
private Long id;
|
||||
private String planNo;
|
||||
private String customerId;
|
||||
private Long sourceOrderId;
|
||||
private String containerNo;
|
||||
private String status;
|
||||
private Integer lineCount;
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
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 lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/** 本柜在国内冻结、由海外首次扫码确权的箱级作业事实。 */
|
||||
@Data
|
||||
@TableName("ll_inbound_plan_carton")
|
||||
public class InboundPlanCarton {
|
||||
@TableId(type = IdType.INPUT)
|
||||
private Long id;
|
||||
private String customerId;
|
||||
private Long orderId;
|
||||
private String sourceInboundDetailId;
|
||||
private String skuCode;
|
||||
private String cartonMark;
|
||||
private String destinationWarehouseId;
|
||||
private String destinationWarehouseCode;
|
||||
private String destinationWarehouseName;
|
||||
private Long planId;
|
||||
private Long lineId;
|
||||
private Long planPalletId;
|
||||
private Long warehousePalletId;
|
||||
private Long warehouseLocationId;
|
||||
private Long pdaTaskId;
|
||||
private String status;
|
||||
private Integer version;
|
||||
private LocalDateTime boundAt;
|
||||
private LocalDateTime receivedAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package com.baiyue.lastleg.inboundplan.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanCarton;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
@Mapper
|
||||
public interface InboundPlanCartonMapper extends BaseMapper<InboundPlanCarton> {
|
||||
@Select("""
|
||||
SELECT l.* FROM ll_inbound_plan_line l
|
||||
JOIN ll_order_detail d ON d.order_id=l.order_id
|
||||
WHERE l.plan_id=#{planId}
|
||||
AND d.source_inbound_detail_id=#{sourceInboundDetailId}
|
||||
LIMIT 1
|
||||
""")
|
||||
com.baiyue.lastleg.inboundplan.entity.InboundPlanLine findPlanLine(@Param("planId") Long planId,
|
||||
@Param("sourceInboundDetailId") String sourceInboundDetailId);
|
||||
|
||||
@Select("""
|
||||
SELECT COUNT(*) FROM ll_inbound_plan_carton
|
||||
WHERE plan_id=#{planId} AND line_id=#{lineId} AND status='ON_PALLET'
|
||||
""")
|
||||
long scannedCount(@Param("planId") Long planId, @Param("lineId") Long lineId);
|
||||
|
||||
@Select("""
|
||||
SELECT EXISTS(SELECT 1 FROM ll_inbound_plan_line l
|
||||
JOIN ll_order_detail d ON d.order_id=l.order_id
|
||||
JOIN ll_inbound_plan_carton c ON c.source_inbound_detail_id=d.source_inbound_detail_id
|
||||
WHERE l.plan_id=#{planId})
|
||||
""")
|
||||
boolean hasLabelPool(@Param("planId") Long planId);
|
||||
}
|
||||
+2
-2
@@ -8,6 +8,6 @@ import org.apache.ibatis.annotations.Select;
|
||||
|
||||
@Mapper
|
||||
public interface InboundPlanMapper extends BaseMapper<InboundPlan> {
|
||||
@Select("SELECT * FROM ll_inbound_plan WHERE customer_id=#{customerId} AND container_no=#{containerNo} LIMIT 1")
|
||||
InboundPlan findByContainer(@Param("customerId") String customerId, @Param("containerNo") String containerNo);
|
||||
@Select("SELECT * FROM ll_inbound_plan WHERE customer_id=#{customerId} AND source_order_id=#{sourceOrderId} LIMIT 1")
|
||||
InboundPlan findBySourceOrder(@Param("customerId") String customerId, @Param("sourceOrderId") Long sourceOrderId);
|
||||
}
|
||||
|
||||
+3
-3
@@ -29,12 +29,12 @@ public interface InboundPlanOrderMapper {
|
||||
"<> COALESCE(NULLIF(o.transfer_warehouse_id,''),NULLIF(o.transfer_warehouse_code,'')) THEN 1 ELSE 0 END) " +
|
||||
"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.container_no=#{containerNo} " +
|
||||
"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," +
|
||||
"o.transfer_warehouse_id,o.transfer_warehouse_code,o.transfer_warehouse_name " +
|
||||
"ORDER BY o.id FOR UPDATE")
|
||||
List<SourceOrderSnapshot> lockContainerOrders(@Param("customerId") String customerId,
|
||||
@Param("containerNo") String containerNo);
|
||||
List<SourceOrderSnapshot> lockOrder(@Param("customerId") String customerId,
|
||||
@Param("orderId") Long orderId);
|
||||
|
||||
@Update("UPDATE ll_order SET inbound_order_no=#{planNo},current_status='INBOUND_PLANNED'," +
|
||||
"pallet_merge_flag='MERGED',pallet_count=#{palletCount},updated_at=#{updatedAt} " +
|
||||
|
||||
+18
@@ -62,6 +62,24 @@ public class InboundPdaController {
|
||||
return ApiResponse.success(service.pick(scope(user, customerId), planNo, allocationId, request));
|
||||
}
|
||||
|
||||
@PostMapping("/cartons/scan")
|
||||
public ApiResponse<PdaResponses.Carton> scanCarton(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String planNo,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@Valid @RequestBody PdaRequests.ScanCarton request) {
|
||||
requireOperate(user);
|
||||
return ApiResponse.success(service.scanCarton(scope(user, customerId), planNo, request));
|
||||
}
|
||||
|
||||
@PostMapping("/cartons/remove")
|
||||
public ApiResponse<PdaResponses.Carton> removeCarton(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String planNo,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@Valid @RequestBody PdaRequests.RemoveCarton request) {
|
||||
requireOperate(user);
|
||||
return ApiResponse.success(service.removeCarton(scope(user, customerId), planNo, request));
|
||||
}
|
||||
|
||||
@GetMapping("/allocations/{allocationId}/merge-candidates")
|
||||
public ApiResponse<List<PdaResponses.MergeCandidate>> candidates(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String planNo,
|
||||
|
||||
@@ -16,4 +16,10 @@ public final class PdaRequests {
|
||||
|
||||
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 RemoveCarton(@NotBlank String cartonMark, @NotBlank String idempotencyKey,
|
||||
String operatorId, String operatorName) {}
|
||||
}
|
||||
|
||||
@@ -23,4 +23,7 @@ public final class PdaResponses {
|
||||
public record Merge(String result, String mergeGroupNo, long sourceOrderId, long targetOrderId,
|
||||
int movedPackageCount, BigDecimal movedVolumeCbm,
|
||||
boolean sourcePalletScrapped) {}
|
||||
|
||||
public record Carton(String result, String cartonMark, long planPalletId, long allocationId,
|
||||
int palletActualPackageCount) {}
|
||||
}
|
||||
|
||||
+25
@@ -9,6 +9,8 @@ import com.baiyue.lastleg.inboundplan.mapper.InboundPlanLineMapper;
|
||||
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanMapper;
|
||||
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanPalletAllocationMapper;
|
||||
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanPalletMapper;
|
||||
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanCartonMapper;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanCarton;
|
||||
import com.baiyue.lastleg.inboundplan.pda.entity.InboundPalletMerge;
|
||||
import com.baiyue.lastleg.inboundplan.pda.entity.InboundPdaOperation;
|
||||
import com.baiyue.lastleg.inboundplan.pda.entity.InboundPdaTask;
|
||||
@@ -29,6 +31,7 @@ public class PdaWorkflowRepository {
|
||||
@Resource private InboundPdaTaskMapper tasks;
|
||||
@Resource private InboundPdaOperationMapper operations;
|
||||
@Resource private InboundPalletMergeMapper merges;
|
||||
@Resource private InboundPlanCartonMapper cartons;
|
||||
|
||||
public InboundPlan lockPlan(String customerId, String planNo) {
|
||||
return plans.selectOne(new LambdaQueryWrapper<InboundPlan>()
|
||||
@@ -45,6 +48,27 @@ public class PdaWorkflowRepository {
|
||||
.eq(InboundPlanPalletAllocation::getId, allocationId).last("FOR UPDATE"));
|
||||
}
|
||||
|
||||
public InboundPlanPalletAllocation lockAllocation(Long planId, Long lineId, Long palletId) {
|
||||
return allocations.selectOne(new LambdaQueryWrapper<InboundPlanPalletAllocation>()
|
||||
.eq(InboundPlanPalletAllocation::getPlanId, planId).eq(InboundPlanPalletAllocation::getLineId, lineId)
|
||||
.eq(InboundPlanPalletAllocation::getPalletId, palletId).last("FOR UPDATE"));
|
||||
}
|
||||
|
||||
public InboundPlanCarton lockCarton(String customerId, String cartonMark) {
|
||||
return cartons.selectOne(new LambdaQueryWrapper<InboundPlanCarton>()
|
||||
.eq(InboundPlanCarton::getCustomerId, customerId).eq(InboundPlanCarton::getCartonMark, cartonMark)
|
||||
.last("FOR UPDATE"));
|
||||
}
|
||||
|
||||
public List<InboundPlanCarton> cartons(Long planId) {
|
||||
return cartons.selectList(new LambdaQueryWrapper<InboundPlanCarton>().eq(InboundPlanCarton::getPlanId, planId));
|
||||
}
|
||||
public InboundPlanLine lineForCarton(Long planId, InboundPlanCarton carton) {
|
||||
return cartons.findPlanLine(planId, carton.getSourceInboundDetailId());
|
||||
}
|
||||
public long scannedCartons(Long planId, Long lineId) { return cartons.scannedCount(planId, lineId); }
|
||||
public boolean hasCartonLabelPool(Long planId) { return cartons.hasLabelPool(planId); }
|
||||
|
||||
public InboundPlanPallet lockPallet(Long planId, Long palletId) {
|
||||
return pallets.selectOne(new LambdaQueryWrapper<InboundPlanPallet>()
|
||||
.eq(InboundPlanPallet::getPlanId, planId).eq(InboundPlanPallet::getId, palletId).last("FOR UPDATE"));
|
||||
@@ -76,4 +100,5 @@ public class PdaWorkflowRepository {
|
||||
public void update(InboundPdaTask value) { tasks.updateById(value); }
|
||||
public void update(InboundPlanPallet value) { pallets.updateById(value); }
|
||||
public void update(InboundPlanPalletAllocation value) { allocations.updateById(value); }
|
||||
public void update(InboundPlanCarton value) { cartons.updateById(value); }
|
||||
}
|
||||
|
||||
+123
-2
@@ -5,6 +5,7 @@ import com.baiyue.lastleg.inboundplan.entity.InboundPlan;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanLine;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanPallet;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanPalletAllocation;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanCarton;
|
||||
import com.baiyue.lastleg.inboundplan.pda.dto.PdaRequests;
|
||||
import com.baiyue.lastleg.inboundplan.pda.dto.PdaResponses;
|
||||
import com.baiyue.lastleg.inboundplan.pda.entity.InboundPalletMerge;
|
||||
@@ -99,9 +100,13 @@ public class InboundPdaWorkflowService {
|
||||
&& Objects.equals(pallet.getWarehouseLocationId(), request.locationId())) return;
|
||||
throw conflict("Planned pallet is already assigned to another warehouse resource");
|
||||
}
|
||||
String businessId = planNo + ":" + planPalletId;
|
||||
String destinationKey = destinationKey(pallet);
|
||||
String businessId = plan.getContainerNo() + ":" + destinationKey;
|
||||
// A container is unloaded by destination pallet, not by inbound-plan pallet.
|
||||
// The stable reservation key lets each plan from the same container reuse it.
|
||||
String reservationKey = "INBOUND_CONTAINER_DESTINATION_PALLET:" + businessId;
|
||||
long lockId = warehouse.reserve(customerId, request.warehouseId(), request.palletId(), request.locationId(),
|
||||
"INBOUND_PLAN_PALLET", businessId, request.idempotencyKey());
|
||||
"INBOUND_CONTAINER_DESTINATION_PALLET", businessId, reservationKey);
|
||||
pallet.setWarehousePalletId(request.palletId());
|
||||
pallet.setWarehouseLocationId(request.locationId());
|
||||
pallet.setWarehouseResourceLockId(lockId);
|
||||
@@ -128,6 +133,7 @@ public class InboundPdaWorkflowService {
|
||||
public PdaResponses.Operation count(String customerId, String planNo, long allocationId,
|
||||
PdaRequests.PackageOperation request) {
|
||||
InboundPlan plan = requirePlan(customerId, planNo);
|
||||
requireLegacyCounting(plan);
|
||||
InboundPdaOperation replay = replay(customerId, "COUNT", request.idempotencyKey(), request);
|
||||
if (replay != null) return read(replay, PdaResponses.Operation.class);
|
||||
InboundPdaTask task = requireActiveTask(customerId, plan);
|
||||
@@ -155,6 +161,7 @@ public class InboundPdaWorkflowService {
|
||||
public PdaResponses.Operation pick(String customerId, String planNo, long allocationId,
|
||||
PdaRequests.PackageOperation request) {
|
||||
InboundPlan plan = requirePlan(customerId, planNo);
|
||||
requireLegacyCounting(plan);
|
||||
InboundPdaOperation replay = replay(customerId, "PICK", request.idempotencyKey(), request);
|
||||
if (replay != null) return read(replay, PdaResponses.Operation.class);
|
||||
InboundPdaTask task = requireActiveTask(customerId, plan);
|
||||
@@ -193,6 +200,90 @@ public class InboundPdaWorkflowService {
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 海外首次扫描箱唛:仅接受本柜国内冻结且已归属本计划的箱。 */
|
||||
@Transactional
|
||||
public PdaResponses.Carton scanCarton(String customerId, String planNo, PdaRequests.ScanCarton request) {
|
||||
InboundPlan plan = requirePlan(customerId, planNo);
|
||||
InboundPdaOperation replay = replay(customerId, "SCAN_CARTON", request.idempotencyKey(), request);
|
||||
if (replay != null) return read(replay, PdaResponses.Carton.class);
|
||||
InboundPdaTask task = requireActiveTask(customerId, plan);
|
||||
InboundPlanCarton carton = repository.lockCarton(customerId, request.cartonMark());
|
||||
if (carton == null || (carton.getPlanId() != null && !Objects.equals(carton.getPlanId(), plan.getId()))) {
|
||||
throw notFound("Carton mark is not available to this inbound plan");
|
||||
}
|
||||
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());
|
||||
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())) {
|
||||
throw conflict("This inbound plan line has reached its required carton count");
|
||||
}
|
||||
InboundPlanPalletAllocation allocation = repository.lockAllocation(plan.getId(), line.getId(), pallet.getId());
|
||||
if (allocation == null) {
|
||||
allocation = new InboundPlanPalletAllocation(); allocation.setId(IdWorker.getId()); allocation.setPlanId(plan.getId());
|
||||
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);
|
||||
}
|
||||
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));
|
||||
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));
|
||||
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());
|
||||
saveOperation(customerId, task.getId(), allocation.getId(), "SCAN_CARTON", request.idempotencyKey(), request, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public PdaResponses.Carton removeCarton(String customerId, String planNo, PdaRequests.RemoveCarton request) {
|
||||
InboundPlan plan = requirePlan(customerId, planNo);
|
||||
InboundPdaOperation replay = replay(customerId, "REMOVE_CARTON", request.idempotencyKey(), request);
|
||||
if (replay != null) return read(replay, PdaResponses.Carton.class);
|
||||
InboundPdaTask task = requireActiveTask(customerId, plan);
|
||||
InboundPlanCarton carton = repository.lockCarton(customerId, request.cartonMark());
|
||||
if (carton == null || !Objects.equals(carton.getPlanId(), plan.getId())) throw notFound("Carton mark is not expected by this inbound plan");
|
||||
if (!"ON_PALLET".equals(carton.getStatus()) || carton.getPlanPalletId() == null) throw conflict("Only a carton currently on a pallet may be removed");
|
||||
InboundPlanPallet pallet = requirePallet(plan.getId(), carton.getPlanPalletId());
|
||||
InboundPlanLine line = requireLine(plan.getId(), carton.getLineId());
|
||||
InboundPlanPalletAllocation allocation = repository.lockAllocation(plan.getId(), line.getId(), pallet.getId());
|
||||
if (allocation == null || value(allocation.getPickedPackageCount()) <= 0 || value(pallet.getActualPackageCount()) <= 0) {
|
||||
throw conflict("Carton pallet allocation is inconsistent");
|
||||
}
|
||||
BigDecimal perPackage = perPackageVolume(line);
|
||||
allocation.setCountedPackageCount(Math.max(0, value(allocation.getCountedPackageCount()) - 1));
|
||||
allocation.setPickedPackageCount(value(allocation.getPickedPackageCount()) - 1);
|
||||
allocation.setActualVolumeCbm(nonNegative(volume(allocation.getActualVolumeCbm()).subtract(perPackage)));
|
||||
allocation.setStatus(value(allocation.getPickedPackageCount()) == 0 ? "PENDING_COUNT" : "PICKING");
|
||||
bump(allocation); repository.update(allocation);
|
||||
pallet.setActualPackageCount(value(pallet.getActualPackageCount()) - 1);
|
||||
pallet.setActualVolumeCbm(nonNegative(volume(pallet.getActualVolumeCbm()).subtract(perPackage))); repository.update(pallet);
|
||||
carton.setOrderId(null); carton.setPlanId(null); carton.setLineId(null); carton.setPlanPalletId(null);
|
||||
carton.setWarehousePalletId(null); carton.setWarehouseLocationId(null); carton.setPdaTaskId(null);
|
||||
carton.setStatus("UNASSIGNED"); carton.setBoundAt(null); carton.setVersion(value(carton.getVersion()) + 1);
|
||||
carton.setUpdatedAt(LocalDateTime.now()); repository.update(carton);
|
||||
PdaResponses.Carton result = new PdaResponses.Carton("REMOVED", carton.getCartonMark(), pallet.getId(),
|
||||
allocation.getId(), pallet.getActualPackageCount());
|
||||
saveOperation(customerId, task.getId(), allocation.getId(), "REMOVE_CARTON", request.idempotencyKey(), request, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public List<PdaResponses.MergeCandidate> mergeCandidates(String customerId, String planNo,
|
||||
long allocationId) {
|
||||
@@ -205,6 +296,7 @@ public class InboundPdaWorkflowService {
|
||||
public PdaResponses.Merge merge(String customerId, String planNo, long targetAllocationId,
|
||||
PdaRequests.ConfirmMerge request) {
|
||||
InboundPlan plan = requirePlan(customerId, planNo);
|
||||
requireLegacyCounting(plan);
|
||||
InboundPdaOperation replay = replay(customerId, "MERGE", request.idempotencyKey(), request);
|
||||
if (replay != null) return read(replay, PdaResponses.Merge.class);
|
||||
InboundPdaTask task = requireActiveTask(customerId, plan);
|
||||
@@ -292,8 +384,16 @@ public class InboundPdaWorkflowService {
|
||||
throw conflict("All line picked package totals must exactly equal required package totals");
|
||||
}
|
||||
}
|
||||
List<InboundPlanCarton> cartons = repository.cartons(plan.getId());
|
||||
if (!cartons.isEmpty() && cartons.stream().anyMatch(carton -> !"ON_PALLET".equals(carton.getStatus()))) {
|
||||
throw conflict("All expected carton marks must be scanned onto a pallet before stock-in");
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
stockReceipts.stockIn(customerId, plan, task, lines, allocations, repository.pallets(plan.getId()), now);
|
||||
for (InboundPlanCarton carton : cartons) {
|
||||
carton.setStatus("AVAILABLE"); carton.setReceivedAt(now); carton.setUpdatedAt(now);
|
||||
carton.setVersion(value(carton.getVersion()) + 1); repository.update(carton);
|
||||
}
|
||||
task.setStatus("COMPLETED");
|
||||
task.setCompletedAt(now);
|
||||
task.setUpdatedAt(now);
|
||||
@@ -387,6 +487,21 @@ public class InboundPdaWorkflowService {
|
||||
return leftKey != null && leftKey.equals(rightKey);
|
||||
}
|
||||
|
||||
private boolean sameWarehouse(InboundPlanCarton carton, InboundPlanPallet pallet) {
|
||||
String cartonKey = carton.getDestinationWarehouseId() != null
|
||||
? carton.getDestinationWarehouseId() : carton.getDestinationWarehouseCode();
|
||||
String palletKey = pallet.getDestinationWarehouseId() != null
|
||||
? pallet.getDestinationWarehouseId() : pallet.getDestinationWarehouseCode();
|
||||
return cartonKey != null && cartonKey.equals(palletKey);
|
||||
}
|
||||
|
||||
private String destinationKey(InboundPlanPallet pallet) {
|
||||
String value = pallet.getDestinationWarehouseId() != null
|
||||
? pallet.getDestinationWarehouseId() : pallet.getDestinationWarehouseCode();
|
||||
if (value == null || value.isBlank()) throw conflict("Planned pallet destination warehouse is empty");
|
||||
return value;
|
||||
}
|
||||
|
||||
private void validateDestinationWarehouse(InboundPlanPallet pallet, long warehouseId) {
|
||||
String destinationId = pallet.getDestinationWarehouseId();
|
||||
if (destinationId == null || destinationId.isBlank()) return;
|
||||
@@ -413,6 +528,12 @@ public class InboundPdaWorkflowService {
|
||||
repository.update(plan);
|
||||
}
|
||||
|
||||
private void requireLegacyCounting(InboundPlan plan) {
|
||||
if (repository.hasCartonLabelPool(plan.getId())) {
|
||||
throw conflict("This inbound plan has a carton manifest and must be operated by carton scanning");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isNotAssigned(InboundPlanPallet pallet) {
|
||||
return pallet.getWarehousePalletId() == null || pallet.getWarehouseLocationId() == null
|
||||
|| pallet.getWarehouseResourceLockId() == null
|
||||
|
||||
+38
-12
@@ -3,6 +3,7 @@ package com.baiyue.lastleg.inboundplan.service;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baiyue.lastleg.inboundplan.dto.InboundPlanResponse;
|
||||
import com.baiyue.lastleg.inboundplan.dto.SupplementPalletRequest;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlan;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanLine;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanPallet;
|
||||
@@ -37,17 +38,13 @@ public class InboundPlanService {
|
||||
|
||||
@Transactional
|
||||
public InboundPlanResponse convert(String customerId, Long selectedOrderId) {
|
||||
String containerNo = trimToNull(orderMapper.findContainerNo(customerId, selectedOrderId));
|
||||
if (containerNo == null) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||
"Order does not exist in this tenant or container number is empty");
|
||||
List<SourceOrderSnapshot> orders = orderMapper.lockOrder(customerId, selectedOrderId);
|
||||
if (orders == null || orders.isEmpty()) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Inbound order not found");
|
||||
}
|
||||
|
||||
List<SourceOrderSnapshot> orders = orderMapper.lockContainerOrders(customerId, containerNo);
|
||||
if (orders.isEmpty()) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Container orders not found");
|
||||
}
|
||||
InboundPlan existing = planMapper.findByContainer(customerId, containerNo);
|
||||
String containerNo = trimToNull(orders.get(0).containerNo());
|
||||
if (containerNo == null) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Inbound order container number is empty");
|
||||
InboundPlan existing = planMapper.findBySourceOrder(customerId, selectedOrderId);
|
||||
if (existing != null) {
|
||||
return detail(customerId, existing);
|
||||
}
|
||||
@@ -58,6 +55,7 @@ public class InboundPlanService {
|
||||
plan.setId(IdWorker.getId());
|
||||
plan.setPlanNo("LIP" + plan.getId());
|
||||
plan.setCustomerId(customerId);
|
||||
plan.setSourceOrderId(selectedOrderId);
|
||||
plan.setContainerNo(containerNo);
|
||||
plan.setStatus("PENDING_ASSIGNMENT");
|
||||
plan.setLineCount(orders.size());
|
||||
@@ -92,11 +90,12 @@ public class InboundPlanService {
|
||||
throw new IllegalStateException("Failed to update order " + order.orderId());
|
||||
}
|
||||
orderMapper.insertHistory(order.orderId(), order.currentStatus(),
|
||||
"Container converted to inbound plan " + plan.getPlanNo(), now);
|
||||
"Inbound order converted to inbound plan " + plan.getPlanNo(), now);
|
||||
}
|
||||
return detail(customerId, plan);
|
||||
}
|
||||
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public InboundPlanResponse detailByPlanNo(String customerId, String planNo) {
|
||||
InboundPlan plan = planMapper.selectOne(new LambdaQueryWrapper<InboundPlan>()
|
||||
@@ -107,6 +106,32 @@ public class InboundPlanService {
|
||||
return detail(customerId, plan);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InboundPlanResponse.Pallet supplementPallet(String customerId, String planNo, SupplementPalletRequest request) {
|
||||
InboundPlan plan = planMapper.selectOne(new LambdaQueryWrapper<InboundPlan>()
|
||||
.eq(InboundPlan::getCustomerId, customerId).eq(InboundPlan::getPlanNo, planNo).last("FOR UPDATE"));
|
||||
if (plan == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Inbound plan not found");
|
||||
if ("COMPLETED".equals(plan.getStatus())) throw new ResponseStatusException(HttpStatus.CONFLICT, "Completed plan cannot add pallets");
|
||||
List<InboundPlanLine> lines = lineMapper.selectList(new LambdaQueryWrapper<InboundPlanLine>()
|
||||
.eq(InboundPlanLine::getPlanId, plan.getId()));
|
||||
boolean destinationExists = lines.stream().anyMatch(line -> request.destinationWarehouseId().equals(line.getDestinationWarehouseId()));
|
||||
if (!destinationExists) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Destination warehouse is not part of this inbound plan");
|
||||
int sequence = palletMapper.selectList(new LambdaQueryWrapper<InboundPlanPallet>()
|
||||
.eq(InboundPlanPallet::getPlanId, plan.getId())).stream().map(InboundPlanPallet::getSequenceNo)
|
||||
.filter(Objects::nonNull).max(Integer::compareTo).orElse(0) + 1;
|
||||
InboundPlanPallet pallet = new InboundPlanPallet();
|
||||
pallet.setId(IdWorker.getId()); pallet.setPlanId(plan.getId()); pallet.setPalletNo(plan.getPlanNo() + "-P" + String.format("%03d", sequence));
|
||||
pallet.setDestinationWarehouseId(request.destinationWarehouseId()); pallet.setDestinationWarehouseCode(request.destinationWarehouseCode());
|
||||
pallet.setDestinationWarehouseName(request.destinationWarehouseName()); pallet.setCapacityCbm(PalletAllocator.PALLET_CAPACITY_CBM);
|
||||
pallet.setAllocatedVolumeCbm(BigDecimal.ZERO); pallet.setSequenceNo(sequence); pallet.setAssignmentStatus("UNASSIGNED");
|
||||
pallet.setActualVolumeCbm(BigDecimal.ZERO); pallet.setActualPackageCount(0); palletMapper.insert(pallet);
|
||||
plan.setPalletCount(plan.getPalletCount() + 1); plan.setUpdatedAt(LocalDateTime.now()); planMapper.updateById(plan);
|
||||
return new InboundPlanResponse.Pallet(pallet.getId(), pallet.getPalletNo(), pallet.getDestinationWarehouseId(),
|
||||
pallet.getDestinationWarehouseCode(), pallet.getDestinationWarehouseName(), pallet.getCapacityCbm(),
|
||||
pallet.getAllocatedVolumeCbm(), pallet.getSequenceNo(), null, null, null, pallet.getAssignmentStatus(),
|
||||
null, pallet.getActualVolumeCbm(), pallet.getActualPackageCount());
|
||||
}
|
||||
|
||||
private Map<Long, InboundPlanLine> createLines(InboundPlan plan, List<SourceOrderSnapshot> orders) {
|
||||
Map<Long, InboundPlanLine> result = new HashMap<>();
|
||||
for (SourceOrderSnapshot order : orders) {
|
||||
@@ -211,7 +236,8 @@ public class InboundPlanService {
|
||||
new LambdaQueryWrapper<InboundPlanPalletAllocation>()
|
||||
.eq(InboundPlanPalletAllocation::getPlanId, plan.getId())
|
||||
.orderByAsc(InboundPlanPalletAllocation::getId));
|
||||
return new InboundPlanResponse(plan.getId(), plan.getPlanNo(), plan.getCustomerId(), plan.getContainerNo(),
|
||||
return new InboundPlanResponse(plan.getId(), plan.getPlanNo(), plan.getCustomerId(), plan.getSourceOrderId(),
|
||||
plan.getContainerNo(),
|
||||
plan.getStatus(), plan.getLineCount(), plan.getPalletCount(), plan.getTotalVolumeCbm(),
|
||||
lines.stream().map(line -> new InboundPlanResponse.Line(line.getId(), line.getOrderId(), line.getInboundCode(), line.getFbaWarehouseCode(),
|
||||
line.getDestinationWarehouseId(), line.getDestinationWarehouseCode(), line.getDestinationWarehouseName(),
|
||||
|
||||
+29
-1
@@ -5,6 +5,9 @@ import com.baiyue.lastleg.entity.*;
|
||||
import com.baiyue.lastleg.mapper.*;
|
||||
import com.baiyue.lastleg.dropship.dto.DropshipRequests;
|
||||
import com.baiyue.lastleg.dropship.service.DropshipOrderService;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanCarton;
|
||||
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanCartonMapper;
|
||||
import com.baiyue.lastleg.inboundplan.service.InboundPlanService;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -25,6 +28,8 @@ public class LoadingCompletedConsumer {
|
||||
@Resource private LastlegOrderMapper orderMapper;
|
||||
@Resource private LastlegOrderDetailMapper detailMapper;
|
||||
@Resource private LastlegOrderStatusHistoryMapper historyMapper;
|
||||
@Resource private InboundPlanCartonMapper cartonMapper;
|
||||
@Resource private InboundPlanService inboundPlans;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
@Resource private LastlegOrderNoGenerator orderNoGenerator;
|
||||
@Resource private DropshipOrderService dropshipOrders;
|
||||
@@ -32,7 +37,8 @@ public class LoadingCompletedConsumer {
|
||||
@Transactional
|
||||
public int consume(EventEnvelope envelope) {
|
||||
envelope.requireValid();
|
||||
if(!EventTypes.FIRST_LEG_LOADING_PLAN_COMPLETED.equals(envelope.eventType())||envelope.eventVersion()!=1)
|
||||
if (!EventTypes.FIRST_LEG_LOADING_PLAN_COMPLETED.equals(envelope.eventType())
|
||||
|| (envelope.eventVersion() != 1 && envelope.eventVersion() != 2))
|
||||
throw new IllegalArgumentException("不支持的尾程交接事件");
|
||||
if (envelope.sourceRegion() != RegionCode.CN || envelope.targetRegion() != RegionCode.OVERSEAS)
|
||||
throw new IllegalArgumentException("尾程交接事件区域无效");
|
||||
@@ -71,6 +77,8 @@ public class LoadingCompletedConsumer {
|
||||
try {
|
||||
ObjectMapper compatibleMapper = objectMapper.copy()
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
// V2 only adds immutable item remarks; the current tail-leg snapshot model
|
||||
// intentionally ignores those fields until its schema exposes them.
|
||||
return compatibleMapper.treeToValue(envelope.payload(), FirstLegLoadingPlanCompletedV1.class);
|
||||
}
|
||||
catch(Exception exception) { throw new IllegalArgumentException("尾程事件负载无效",exception); }
|
||||
@@ -133,10 +141,12 @@ public class LoadingCompletedConsumer {
|
||||
detail.setDestinationWarehouseId(item.destinationWarehouseId()); detail.setDestinationWarehouseCode(item.destinationWarehouseCode());
|
||||
detail.setDestinationWarehouseName(item.destinationWarehouseName()); detail.setFbaTrackingNo(item.fbaTrackingNo());
|
||||
detail.setShipmentNo(item.shipmentNo()); detail.setShippingMark(item.shippingMark()); detailMapper.insert(detail);
|
||||
createCartonLabelPool(customerId, item);
|
||||
}
|
||||
LastlegOrderStatusHistory history=new LastlegOrderStatusHistory();
|
||||
history.setOrderId(order.getId()); history.setToStatus("PENDING_ASSIGNMENT"); history.setEventId(event.eventId());
|
||||
history.setRemark("头程装柜完成自动生成"); history.setCreatedAt(LocalDateTime.now()); historyMapper.insert(history);
|
||||
if ("FIRST_LEG".equals(orderType)) inboundPlans.convert(customerId, order.getId());
|
||||
if ("ONE_CLICK_DROPSHIP".equals(orderType)) {
|
||||
long warehouseId;
|
||||
try { warehouseId = Long.parseLong(payload.warehouseId()); }
|
||||
@@ -146,6 +156,24 @@ public class LoadingCompletedConsumer {
|
||||
}
|
||||
}
|
||||
|
||||
private void createCartonLabelPool(String customerId, FirstLegLoadingPlanCompletedV1.Item item) {
|
||||
List<String> marks = item.cartonMarks() == null ? List.of() : item.cartonMarks();
|
||||
if (marks.isEmpty()) return; // 历史 V1 事件保持原有按箱数入库路径。
|
||||
if (marks.stream().anyMatch(mark -> mark == null || mark.isBlank()) || marks.stream().distinct().count() != marks.size()) {
|
||||
throw new IllegalArgumentException("装柜事件的箱唛池不可包含空值或重复箱唛");
|
||||
}
|
||||
for (String mark : marks) {
|
||||
InboundPlanCarton carton = new InboundPlanCarton();
|
||||
carton.setId(IdWorker.getId()); carton.setCustomerId(customerId);
|
||||
carton.setSourceInboundDetailId(item.sourceInboundDetailId()); carton.setSkuCode(item.skuCode());
|
||||
carton.setCartonMark(mark); carton.setDestinationWarehouseId(item.destinationWarehouseId());
|
||||
carton.setDestinationWarehouseCode(item.destinationWarehouseCode());
|
||||
carton.setDestinationWarehouseName(item.destinationWarehouseName()); carton.setStatus("UNASSIGNED");
|
||||
carton.setVersion(0); carton.setUpdatedAt(LocalDateTime.now());
|
||||
try { cartonMapper.insert(carton); } catch (DuplicateKeyException ignored) { }
|
||||
}
|
||||
}
|
||||
|
||||
private List<DropshipRequests.Pallet> pallets(FirstLegLoadingPlanCompletedV1.Order source, JsonNode rawSource) {
|
||||
if (source.pallets() != null && !source.pallets().isEmpty()) {
|
||||
return source.pallets().stream().map(pallet -> new DropshipRequests.Pallet(pallet.palletNo(),
|
||||
|
||||
Reference in New Issue
Block a user