feat: complete dropship palletization workflow

This commit is contained in:
2026-09-08 17:24:48 +08:00
parent e9182e8cfe
commit 6c2d169798
61 changed files with 1242 additions and 1358 deletions
@@ -63,12 +63,13 @@ public interface OrderCargoClient {
}
record CargoLabelBinding(Long cargoId, Long orderId, Integer itemNo, String cargoName,
String skuNo, Integer packageCount, BigDecimal lengthCm, BigDecimal widthCm,
String skuNo, Integer packageCount, Integer unitsPerCarton,
BigDecimal lengthCm, BigDecimal widthCm,
BigDecimal heightCm) {
public CargoLabelBinding(Long cargoId, Long orderId, Integer itemNo, String cargoName,
Integer packageCount, BigDecimal lengthCm, BigDecimal widthCm,
BigDecimal heightCm) {
this(cargoId, orderId, itemNo, cargoName, null, packageCount, lengthCm, widthCm, heightCm);
this(cargoId, orderId, itemNo, cargoName, null, packageCount, null, lengthCm, widthCm, heightCm);
}
}
}
@@ -62,23 +62,44 @@ public class PlatformDropshipPalletizationController {
return ApiResponse.success(service.claim(taskId, request.warehouseId(), user.userId(), user.username()));
}
@PostMapping("/tasks/{taskId}/pallets/open")
@PreAuthorize("hasAuthority('platform:firstleg:pda:work')")
public ApiResponse<Pallet> open(@AuthenticationPrincipal AuthenticatedUser user,
@GetMapping("/tasks/{taskId}/cartons/resolve")
@PreAuthorize("hasAuthority('platform:firstleg:pda:view')")
public ApiResponse<DropshipPalletizationDtos.Carton> resolveCarton(
@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable @Positive long taskId,
@Valid @RequestBody DropshipPalletizationDtos.OpenPalletRequest request) {
@RequestParam @Positive long warehouseId,
@RequestParam @Size(max = 80) String boxCode) {
FirstlegScopeGuard.platform(user);
return ApiResponse.success(service.open(taskId, request, user.userId(), user.username()));
return ApiResponse.success(service.resolveCarton(taskId, warehouseId, boxCode, user.userId()));
}
@PostMapping("/tasks/{taskId}/pallets/{palletId}/scan")
@PostMapping("/tasks/{taskId}/cartons/count")
@PreAuthorize("hasAuthority('platform:firstleg:pda:work')")
public ApiResponse<DropshipPalletizationDtos.ScanResult> scan(
public ApiResponse<DropshipPalletizationDtos.CountResult> countCarton(
@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable @Positive long taskId, @PathVariable @Positive long palletId,
@Valid @RequestBody DropshipPalletizationDtos.ScanRequest request) {
@PathVariable @Positive long taskId,
@Valid @RequestBody DropshipPalletizationDtos.CountCartonRequest request) {
FirstlegScopeGuard.platform(user);
return ApiResponse.success(service.scan(taskId, palletId, request, user.userId(), user.username()));
return ApiResponse.success(service.countCarton(taskId, request, user.userId(), user.username()));
}
@PostMapping("/tasks/{taskId}/pallets/bind-pending")
@PreAuthorize("hasAuthority('platform:firstleg:pda:work')")
public ApiResponse<Task> bindPending(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable @Positive long taskId,
@Valid @RequestBody DropshipPalletizationDtos.BindPendingRequest request) {
FirstlegScopeGuard.platform(user);
return ApiResponse.success(service.bindPending(taskId, request, user.userId(), user.username()));
}
@PostMapping("/tasks/{taskId}/cartons/{cartonId}/move")
@PreAuthorize("hasAuthority('platform:firstleg:pda:work')")
public ApiResponse<Task> moveCarton(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable @Positive long taskId,
@PathVariable @Positive long cartonId,
@Valid @RequestBody DropshipPalletizationDtos.MoveCartonRequest request) {
FirstlegScopeGuard.platform(user);
return ApiResponse.success(service.moveCarton(taskId, cartonId, request, user.userId(), user.username()));
}
@PostMapping("/tasks/{taskId}/pallets/{palletId}/seal")
@@ -3,6 +3,7 @@ package com.baiyue.firstleg.controller;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.firstleg.dto.LoadingPickDtos.PickRequest;
import com.baiyue.firstleg.dto.LoadingPickDtos.PalletPickRequest;
import com.baiyue.firstleg.dto.LoadingPickDtos.PartialPickRequest;
import com.baiyue.firstleg.dto.LoadingPickDtos.SkipRequest;
import com.baiyue.firstleg.dto.LoadingPickDtos.ClaimRequest;
@@ -24,11 +25,6 @@ import org.springframework.http.ResponseEntity;
import org.springframework.http.MediaType;
import org.springframework.web.multipart.MultipartFile;
import jakarta.validation.constraints.PositiveOrZero;
import com.baiyue.firstleg.service.LoadingWorkPalletService;
import com.baiyue.firstleg.dto.LoadingPalletDtos.OpenRequest;
import com.baiyue.firstleg.dto.LoadingPalletDtos.ScanRequest;
import com.baiyue.firstleg.dto.LoadingPalletDtos.SealRequest;
import com.baiyue.firstleg.dto.LoadingPalletDtos.Pallet;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
@@ -56,8 +52,6 @@ public class PlatformLoadingPdaController {
private LoadingPickService service;
@Resource
private CabinetLoadingEvidenceService evidenceService;
@Resource
private LoadingWorkPalletService palletService;
@GetMapping("/tasks")
@PreAuthorize("hasAuthority('platform:firstleg:pda:view')")
@@ -168,6 +162,15 @@ public class PlatformLoadingPdaController {
return ApiResponse.success(service.pick(taskId, itemId, request, user.userId(), user.username()));
}
@PostMapping("/tasks/{taskId}/pallets/pick")
@PreAuthorize("hasAuthority('platform:firstleg:pda:work')")
public ApiResponse<Task> pickPallet(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable @Positive long taskId,
@Valid @RequestBody PalletPickRequest request) {
FirstlegScopeGuard.platform(user);
return ApiResponse.success(service.pickPallet(taskId, request, user.userId(), user.username()));
}
@PostMapping("/tasks/{taskId}/items/{itemId}/partial")
@PreAuthorize("hasAuthority('platform:firstleg:pda:work')")
public ApiResponse<Task> partial(@AuthenticationPrincipal AuthenticatedUser user,
@@ -226,44 +229,4 @@ public class PlatformLoadingPdaController {
return ApiResponse.success(service.operations(taskId));
}
@GetMapping("/tasks/{taskId}/pallets")
@PreAuthorize("hasAuthority('platform:firstleg:pda:view')")
public ApiResponse<List<Pallet>> pallets(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable @Positive long taskId,
@RequestParam @Positive long warehouseId) {
FirstlegScopeGuard.platform(user);
return ApiResponse.success(palletService.list(taskId, warehouseId));
}
@PostMapping("/tasks/{taskId}/pallets/open")
@PreAuthorize("hasAuthority('platform:firstleg:pda:work')")
public ApiResponse<Pallet> openPallet(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable @Positive long taskId,
@Valid @RequestBody OpenRequest request) {
FirstlegScopeGuard.platform(user);
return ApiResponse.success(palletService.open(taskId, request, user.userId(), user.username()));
}
/** 扫到其他未封托托盘上的箱唛时,同时完成按箱移托。 */
@PostMapping("/tasks/{taskId}/pallets/{palletId}/cartons/scan")
@PreAuthorize("hasAuthority('platform:firstleg:pda:work')")
public ApiResponse<Pallet> scanCarton(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable @Positive long taskId,
@PathVariable @Positive long palletId,
@Valid @RequestBody ScanRequest request) {
FirstlegScopeGuard.platform(user);
return ApiResponse.success(palletService.scan(taskId, palletId, request,
user.userId(), user.username()));
}
@PostMapping("/tasks/{taskId}/pallets/{palletId}/seal")
@PreAuthorize("hasAuthority('platform:firstleg:pda:work')")
public ApiResponse<Pallet> sealPallet(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable @Positive long taskId,
@PathVariable @Positive long palletId,
@Valid @RequestBody SealRequest request) {
FirstlegScopeGuard.platform(user);
return ApiResponse.success(palletService.seal(taskId, palletId, request,
user.userId(), user.username()));
}
}
@@ -24,6 +24,10 @@ public class PlatformSmartLoadingController {
public ApiResponse<Plan> assign(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long planId,@Valid @RequestBody AssignRequest r){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.assign(planId,r,u.userId(),u.username()));}
@PostMapping("/plans/{planId}/inbound-assignments") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:assign')")
public ApiResponse<Plan> assignInbound(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long planId,@Valid @RequestBody AssignInboundRequest r){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.assignInbound(planId,r,u.userId(),u.username()));}
@PostMapping("/plans/{planId}/pallet-assignments") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:assign')")
public ApiResponse<Plan> assignPallet(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long planId,@Valid @RequestBody AssignPalletRequest r){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.assignPallet(planId,r,u.userId(),u.username()));}
@DeleteMapping("/plans/{planId}/pallet-assignments/{palletId}") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:delete')")
public ApiResponse<Plan> removePallet(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long planId,@PathVariable @Positive long palletId,@Valid @RequestBody RemovePalletRequest r){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.removePallet(planId,palletId,r,u.userId(),u.username()));}
@PostMapping("/plans/{planId}/pending-review-assignments") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:assign')")
public ApiResponse<Plan> assignPendingReview(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long planId,@Valid @RequestBody AssignPendingReviewRequest r){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.assignPendingReview(planId,r,u.userId(),u.username()));}
@PostMapping("/plans/{planId}/group-assignments") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:assign')")
@@ -1,7 +1,8 @@
package com.baiyue.firstleg.dto;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.Size;
@@ -14,13 +15,23 @@ public final class DropshipPalletizationDtos {
public record ClaimRequest(@Positive long warehouseId) {}
public record OpenPalletRequest(@Positive long warehouseId,
@NotBlank @Size(max = 128) String operationId) {}
public enum DiscrepancyAction { MATCH, ACCEPT_VARIANCE }
public record ScanRequest(@Positive long warehouseId,
@NotBlank @Size(max = 64) String boxCode,
@Positive @Max(2000) int cartonCount,
@NotBlank @Size(max = 128) String operationId) {}
public record CountCartonRequest(@Positive long warehouseId,
@NotBlank @Size(max = 80) String boxCode,
@Positive int actualSkuQuantity,
@NotNull DiscrepancyAction discrepancyAction,
@Size(max = 500) String discrepancyRemark,
@Pattern(regexp = "DP-[0-9]{6}-[0-9]{4}") String palletNo,
@NotBlank @Size(max = 128) String operationId) {}
public record BindPendingRequest(@Positive long warehouseId,
@NotBlank @Pattern(regexp = "DP-[0-9]{6}-[0-9]{4}") String palletNo,
@NotBlank @Size(max = 128) String operationId) {}
public record MoveCartonRequest(@Positive long warehouseId,
@Pattern(regexp = "DP-[0-9]{6}-[0-9]{4}") String targetPalletNo,
@NotBlank @Size(max = 128) String operationId) {}
public record SealPalletRequest(@Positive long warehouseId,
@NotBlank @Size(max = 128) String operationId,
@@ -28,24 +39,34 @@ public final class DropshipPalletizationDtos {
public record PendingTask(Long taskId, Long inboundId, String inboundCode, Long tenantId,
String customerName, Long warehouseId, String warehouseName,
Integer totalCartons, Integer allocatedCartons, String taskStatus,
Integer totalCartons, Integer countedCartons, Integer boundCartons,
Integer unboundCartons, Integer discrepancyCartons, String taskStatus,
LocalDateTime createdAt) {}
public record Task(Long taskId, Long inboundId, String inboundCode, Long tenantId,
String customerName, Long warehouseId, String warehouseName,
Integer totalCartons, Integer allocatedCartons, String taskStatus,
Integer totalCartons, Integer countedCartons, Integer boundCartons,
Integer unboundCartons, Integer discrepancyCartons, String taskStatus,
Long assigneeId, String assigneeName, Integer version,
List<Pallet> pallets) {}
List<Pallet> pallets, List<Carton> unbound) {}
public record Pallet(Long palletId, String palletNo, String palletStatus,
Integer cartonCount, Integer skuCount, Integer version,
List<SkuItem> items, LocalDateTime completedAt) {}
public record Pallet(Long palletId, String palletNo, String status,
Integer cartonCount, Integer skuCount, Integer discrepancyCartons,
java.math.BigDecimal weightKg, java.math.BigDecimal volumeCbm,
Integer version, List<SkuItem> items, List<Carton> cartons,
LocalDateTime sealedAt) {}
public record SkuItem(Long inboundId, String inboundCode, Long inboundDetailId,
String skuCode, String cargoName, Integer cartonCount,
String skuCode, String cargoName, Integer cartonCount, Integer skuQuantity,
List<String> cartonMarks) {}
public record ScanResult(String skuCode, String cargoName, String inboundCode,
int addedCartons, int allocatedCartons, int remainingCartons,
Pallet pallet) {}
public record Carton(Long cartonId, String boxCode, Long inboundId, String inboundCode,
Long inboundDetailId, String skuCode, String cargoName,
Integer expectedSkuQuantity, Integer actualSkuQuantity,
String discrepancyStatus, String discrepancyRemark,
String countStatus, Long palletId, String palletNo,
Boolean canCount, String blockReason) {}
public record CountResult(Carton carton, Integer countedCartons, Integer boundCartons,
Integer unboundCartons, Integer remainingCartons, Pallet pallet) {}
}
@@ -188,6 +188,10 @@ public final class LoadingListDtos {
String chineseName,
String englishName,
String goodsImageUrl,
String unitType,
Long palletId,
String palletNo,
Boolean indivisible,
int packageCount,
int palletCount,
BigDecimal grossWeightKg,
@@ -1,41 +0,0 @@
package com.baiyue.firstleg.dto;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.util.List;
/** 一件代发PDA现场打托接口模型。 */
public final class LoadingPalletDtos {
private LoadingPalletDtos() {}
public record OpenRequest(
@NotNull @Min(1) Long warehouseId,
@Min(1) Long warehousePalletId,
@NotBlank @Size(max = 64) String palletCode,
@NotBlank @Size(max = 128) String operationId
) {}
public record ScanRequest(
@NotNull @Min(1) Long warehouseId,
@NotBlank @Size(max = 80) String boxCode,
@NotBlank @Size(max = 128) String operationId
) {}
public record SealRequest(
@NotNull @Min(1) Long warehouseId,
@NotNull @Min(0) Integer expectedVersion,
@NotBlank @Size(max = 128) String operationId
) {}
public record Carton(Long id, String boxCode, String skuCode, Long tenantId,
Long inboundId, Long inboundDetailId, Integer unitsPerCarton) {}
public record SkuSummary(String skuCode, Integer cartonCount, Long productQuantity) {}
public record Pallet(Long id, String palletCode, Long warehousePalletId, String status,
Integer version, Integer cartonCount, Integer skuCount,
Integer customerCount, List<SkuSummary> skus, List<Carton> cartons) {}
}
@@ -23,6 +23,13 @@ public final class LoadingPickDtos {
@NotBlank @Size(max = 128) String operationId
) {}
public record PalletPickRequest(
@NotNull @Min(1) Long warehouseId,
@NotBlank @Pattern(regexp = "DP-[0-9]{6}-[0-9]{4}") String palletNo,
@NotNull @Min(0) Integer expectedTaskVersion,
@NotBlank @Size(max = 128) String operationId
) {}
/** 部分拣货为该 PDA 明细的最终实际拣出箱数,不是增量。 */
public record PartialPickRequest(
@NotNull @Min(1) Long warehouseId,
@@ -22,11 +22,14 @@ public final class SmartLoadingDtos { private SmartLoadingDtos(){}
public record GroupMemberCandidate(Long accountId, Long inboundId, Long orderId, String orderNo,
String inboundCode, String customerName, Integer availablePackageCount,
String orderStatusSnapshot, LocalDateTime expectedArrivalAt) {}
public record PalletCandidate(Long palletId,String palletNo,String unitType,Boolean indivisible,
Integer cartonCount,Integer skuCount,BigDecimal weightKg,BigDecimal volumeCbm) {}
public record InboundCandidate(Long accountId, Long inboundId, Long orderId, String orderNo, String inboundCode,
Long warehouseId, String channelCode, String channelName, Long destinationWarehouseId,
String destinationWarehouseCode, String destinationWarehouseName, String customerName, String goodsSummary,
Integer availablePackageCount, BigDecimal unitGrossWeightKg, BigDecimal unitNetWeightKg,
BigDecimal averageUnitVolumeCbm, BigDecimal availableVolumeCbm,
String unitType,Long palletId,String palletNo,Boolean indivisible,
Long coLoadGroupId, String groupNo, Integer memberCount, Integer groupTotalBoxes,
Boolean locked, Boolean canAssign, String blockReason,
String orderStatusSnapshot, LocalDateTime expectedArrivalAt,
@@ -66,6 +69,10 @@ public final class SmartLoadingDtos { private SmartLoadingDtos(){}
public record AssignGroupRequest(@NotNull @Positive Long inboundId,
@NotNull @Min(0) Integer expectedVersion,
@NotBlank @Size(max=128) String operationId) {}
public record AssignPalletRequest(@NotNull @Positive Long palletId,@NotNull @Min(0) Integer expectedVersion,
@NotBlank @Size(max=128) String operationId) {}
public record RemovePalletRequest(@NotNull @Min(0) Integer expectedPlanVersion,
@NotBlank @Size(max=128) String operationId) {}
public record RemoveGroupRequest(@NotNull @Min(0) Integer expectedPlanVersion,
@NotBlank @Size(max=128) String operationId) {}
public record RecommendRequest(@NotNull @Min(0) Integer expectedVersion,
@@ -23,6 +23,7 @@ public class InboundCartonLabelEntity {
private Integer cargoItemNo;
private String cargoName;
private String skuCode;
private Integer unitsPerCarton;
private Integer detailPackageCount;
private String inboundCode;
private String boxCode;
@@ -6,6 +6,7 @@ public class LoadingPlanItemEntity {
private Long inboundId; private Long inboundDetailId; private Long orderId; private String orderNo; private String inboundCode; private Long destinationWarehouseId;
private String destinationWarehouseCode; private String destinationWarehouseName; private String skuCode; private String productNameZh; private String productNameEn;
private Long productImageFileId; private String productImageUrl; private Integer packageCount; private Integer productQuantity; private Integer unitsPerCarton;
private String unitType; private Long palletId; private String palletNo; private Boolean indivisible;
private Integer palletCount; private BigDecimal grossWeightKg; private BigDecimal netWeightKg; private BigDecimal dimensionalWeightKg; private BigDecimal volumeCbm;
private String fbaTrackingNo; private String shipmentNo; private String shippingMark; private Integer loadSequence; private String positionSnapshot;
private String internalRemark; private String customerRemark; private String warehouseRemark; private LocalDateTime frozenAt; private Long frozenBy;
@@ -1,37 +0,0 @@
package com.baiyue.firstleg.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.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
/** 一件代发箱唛与现场实际托盘的当前绑定事实。 */
@Getter
@Setter
@TableName("fl_loading_work_pallet_carton")
public class LoadingWorkPalletCartonEntity {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long pickTaskId;
private Long workPalletId;
private Long loadingPlanItemId;
private Long pickItemId;
private Long tenantId;
private Long inboundId;
private Long inboundDetailId;
private Long cartonLabelId;
private String boxCode;
private String skuCode;
private Integer unitsPerCarton;
private Long boundBy;
private String boundByName;
private String operationId;
@Version
private Integer version;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -1,36 +0,0 @@
package com.baiyue.firstleg.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.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
/** 一件代发装柜现场的实际托盘。 */
@Getter
@Setter
@TableName("fl_loading_work_pallet")
public class LoadingWorkPalletEntity {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long pickTaskId;
private Long loadingPlanId;
private Long warehouseId;
private Long warehousePalletId;
private String palletCode;
private String openOperationId;
private String sealOperationId;
private String palletStatus;
private Long createdBy;
private String createdByName;
private Long sealedBy;
private String sealedByName;
private LocalDateTime sealedAt;
@Version
private Integer version;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -1,203 +1,182 @@
package com.baiyue.firstleg.mapper;
import com.baiyue.firstleg.dto.DropshipPalletizationDtos.PendingTask;
import com.baiyue.firstleg.dto.SmartLoadingDtos.PalletCandidate;
import lombok.Getter;
import lombok.Setter;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import org.apache.ibatis.annotations.*;
import java.time.LocalDateTime;
import java.math.BigDecimal;
import java.util.List;
/** 一件代发合托持久化;任务、托盘和逐箱绑定均独立于装柜拣货。 */
/** 一件代发逐箱清点、待绑托与封托事实。 */
@Mapper
public interface DropshipPalletizationMapper {
@Insert("""
INSERT IGNORE INTO fl_dropship_pallet_task
(id,tenant_id,inbound_id,warehouse_id,task_status,total_cartons,allocated_cartons,version,created_at,updated_at)
VALUES(#{id},#{tenantId},#{inboundId},#{warehouseId},'PENDING',#{totalCartons},0,1,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3))
""")
int insertTask(@Param("id") long id, @Param("tenantId") long tenantId,
@Param("inboundId") long inboundId, @Param("warehouseId") long warehouseId,
@Param("totalCartons") int totalCartons);
@Insert("""
INSERT IGNORE INTO fl_dropship_pallet_task
(id,tenant_id,inbound_id,warehouse_id,task_status,total_cartons,counted_cartons,allocated_cartons,discrepancy_cartons,version,created_at,updated_at)
VALUES(#{id},#{tenantId},#{inboundId},#{warehouseId},'PENDING',#{totalCartons},0,0,0,1,CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3))""")
int insertTask(@Param("id") long id,@Param("tenantId") long tenantId,@Param("inboundId") long inboundId,
@Param("warehouseId") long warehouseId,@Param("totalCartons") int totalCartons);
@Select("""
SELECT t.id task_id,t.inbound_id,o.inbound_code,t.tenant_id,o.customer_name,t.warehouse_id,
o.warehouse_name,t.total_cartons,t.allocated_cartons,t.task_status,t.created_at
FROM fl_dropship_pallet_task t JOIN fl_inbound_order o ON o.id=t.inbound_id AND o.tenant_id=t.tenant_id
WHERE t.task_status IN ('PENDING','CLAIMED')
AND (t.assignee_id IS NULL OR t.assignee_id=#{operatorId})
AND (#{keyword} IS NULL OR o.inbound_code LIKE CONCAT('%',#{keyword},'%')
OR o.customer_name LIKE CONCAT('%',#{keyword},'%'))
ORDER BY t.created_at,t.id
""")
List<PendingTask> pending(@Param("operatorId") long operatorId, @Param("keyword") String keyword);
@Select("""
SELECT t.id task_id,t.inbound_id,o.inbound_code,t.tenant_id,o.customer_name,t.warehouse_id,
o.warehouse_name,t.total_cartons,t.counted_cartons,t.allocated_cartons bound_cartons,
t.counted_cartons-t.allocated_cartons unbound_cartons,t.discrepancy_cartons,t.task_status,t.created_at
FROM fl_dropship_pallet_task t JOIN fl_inbound_order o ON o.id=t.inbound_id AND o.tenant_id=t.tenant_id
WHERE t.task_status IN ('PENDING','CLAIMED') AND (t.assignee_id IS NULL OR t.assignee_id=#{operatorId})
AND (#{keyword} IS NULL OR o.inbound_code LIKE CONCAT('%',#{keyword},'%') OR o.customer_name LIKE CONCAT('%',#{keyword},'%'))
ORDER BY t.created_at,t.id""")
List<PendingTask> pending(@Param("operatorId") long operatorId,@Param("keyword") String keyword);
@Select("""
SELECT t.*,o.inbound_code,o.customer_name,o.warehouse_name,o.process_status,o.fulfillment_type
FROM fl_dropship_pallet_task t JOIN fl_inbound_order o ON o.id=t.inbound_id AND o.tenant_id=t.tenant_id
WHERE t.id=#{taskId} AND t.warehouse_id=#{warehouseId}
""")
TaskRow task(@Param("taskId") long taskId, @Param("warehouseId") long warehouseId);
@Select("""
SELECT t.*,o.inbound_code,o.customer_name,o.warehouse_name,o.process_status,o.fulfillment_type
FROM fl_dropship_pallet_task t JOIN fl_inbound_order o ON o.id=t.inbound_id AND o.tenant_id=t.tenant_id
WHERE t.id=#{taskId} AND t.warehouse_id=#{warehouseId}""")
TaskRow task(@Param("taskId") long taskId,@Param("warehouseId") long warehouseId);
@Update("""
UPDATE fl_dropship_pallet_task
SET task_status='CLAIMED',assignee_id=#{operatorId},assignee_name=#{operatorName},
claimed_at=COALESCE(claimed_at,CURRENT_TIMESTAMP(3)),version=version+1,updated_at=CURRENT_TIMESTAMP(3)
WHERE id=#{taskId} AND warehouse_id=#{warehouseId} AND task_status IN ('PENDING','CLAIMED')
AND (assignee_id IS NULL OR assignee_id=#{operatorId})
""")
int claim(@Param("taskId") long taskId, @Param("warehouseId") long warehouseId,
@Param("operatorId") long operatorId, @Param("operatorName") String operatorName);
@Update("""
UPDATE fl_dropship_pallet_task SET task_status='CLAIMED',assignee_id=#{operatorId},assignee_name=#{operatorName},
claimed_at=COALESCE(claimed_at,CURRENT_TIMESTAMP(3)),version=version+1,updated_at=CURRENT_TIMESTAMP(3)
WHERE id=#{taskId} AND warehouse_id=#{warehouseId} AND task_status IN ('PENDING','CLAIMED')
AND (assignee_id IS NULL OR assignee_id=#{operatorId})""")
int claim(@Param("taskId") long taskId,@Param("warehouseId") long warehouseId,
@Param("operatorId") long operatorId,@Param("operatorName") String operatorName);
@Insert("""
INSERT INTO fl_dropship_pallet
(id,task_id,tenant_id,inbound_id,warehouse_id,pallet_status,open_operation_id,version,created_by,created_by_name,created_at,updated_at)
VALUES(#{id},#{taskId},#{tenantId},#{inboundId},#{warehouseId},'OPEN',#{operationId},1,
#{operatorId},#{operatorName},CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3))
""")
int insertPallet(@Param("id") long id, @Param("taskId") long taskId,
@Param("tenantId") long tenantId, @Param("inboundId") long inboundId,
@Param("warehouseId") long warehouseId, @Param("operationId") String operationId,
@Param("operatorId") long operatorId, @Param("operatorName") String operatorName);
@Insert("""
INSERT INTO fl_dropship_pallet
(id,task_id,tenant_id,inbound_id,warehouse_id,pallet_no,pallet_status,open_operation_id,version,created_by,created_by_name,created_at,updated_at)
VALUES(#{id},#{taskId},#{tenantId},#{inboundId},#{warehouseId},#{palletNo},'OPEN',#{operationId},1,
#{operatorId},#{operatorName},CURRENT_TIMESTAMP(3),CURRENT_TIMESTAMP(3))""")
int insertPallet(@Param("id") long id,@Param("taskId") long taskId,@Param("tenantId") long tenantId,
@Param("inboundId") long inboundId,@Param("warehouseId") long warehouseId,@Param("palletNo") String palletNo,
@Param("operationId") String operationId,@Param("operatorId") long operatorId,@Param("operatorName") String operatorName);
@Select("SELECT * FROM fl_dropship_pallet WHERE open_operation_id=#{operationId}")
PalletRow palletByOpenOperation(@Param("operationId") String operationId);
@Select("SELECT * FROM fl_dropship_pallet WHERE pallet_no=#{palletNo}") PalletRow palletByNo(@Param("palletNo") String palletNo);
@Select("SELECT * FROM fl_dropship_pallet WHERE seal_operation_id=#{operationId}") PalletRow palletBySealOperation(@Param("operationId") String operationId);
@Select("SELECT * FROM fl_dropship_pallet WHERE id=#{palletId} AND task_id=#{taskId} AND warehouse_id=#{warehouseId}")
PalletRow pallet(@Param("taskId") long taskId,@Param("palletId") long palletId,@Param("warehouseId") long warehouseId);
@Select("SELECT * FROM fl_dropship_pallet WHERE task_id=#{taskId} ORDER BY id") List<PalletRow> pallets(@Param("taskId") long taskId);
@Select("SELECT p.* FROM fl_dropship_pallet p JOIN fl_dropship_pallet_task t ON t.id=p.task_id WHERE t.tenant_id=#{tenantId} AND t.inbound_id=#{inboundId} ORDER BY p.id")
List<PalletRow> palletsByInbound(@Param("tenantId") long tenantId,@Param("inboundId") long inboundId);
@Select("SELECT inbound_code FROM fl_inbound_order WHERE tenant_id=#{tenantId} AND id=#{inboundId} AND deleted=0")
String inboundCode(@Param("tenantId") long tenantId,@Param("inboundId") long inboundId);
@Select("SELECT * FROM fl_dropship_pallet WHERE seal_operation_id=#{operationId}")
PalletRow palletBySealOperation(@Param("operationId") String operationId);
@Select("""
SELECT l.id carton_label_id,l.inbound_detail_id,l.sku_code,l.cargo_name,l.units_per_carton,l.inbound_code,l.box_code
FROM fl_inbound_carton_label l JOIN fl_dropship_pallet_task t ON t.inbound_id=l.inbound_id AND t.tenant_id=l.tenant_id
WHERE t.id=#{taskId} AND l.box_code=#{boxCode} AND l.batch_status='ACTIVE'""")
LabelRow label(@Param("taskId") long taskId,@Param("boxCode") String boxCode);
@Select("SELECT c.*,p.pallet_no FROM fl_dropship_pallet_carton c LEFT JOIN fl_dropship_pallet p ON p.id=c.pallet_id WHERE c.count_operation_id=#{operationId}") CartonRow cartonByCountOperation(@Param("operationId") String operationId);
@Select("SELECT c.*,p.pallet_no FROM fl_dropship_pallet_carton c LEFT JOIN fl_dropship_pallet p ON p.id=c.pallet_id WHERE c.task_id=#{taskId} AND c.id=#{cartonId}") CartonRow carton(@Param("taskId") long taskId,@Param("cartonId") long cartonId);
@Select("SELECT c.*,p.pallet_no FROM fl_dropship_pallet_carton c LEFT JOIN fl_dropship_pallet p ON p.id=c.pallet_id WHERE c.task_id=#{taskId} AND c.carton_label_id=#{labelId}")
CartonRow countedByLabel(@Param("taskId") long taskId,@Param("labelId") long labelId);
@Select("SELECT * FROM fl_dropship_pallet WHERE id=#{palletId} AND task_id=#{taskId} AND warehouse_id=#{warehouseId}")
PalletRow pallet(@Param("taskId") long taskId, @Param("palletId") long palletId,
@Param("warehouseId") long warehouseId);
@Insert("""
INSERT INTO fl_dropship_pallet_carton
(id,task_id,pallet_id,tenant_id,inbound_id,inbound_detail_id,carton_label_id,sku_code,cargo_name,box_code,
expected_sku_quantity,actual_sku_quantity,discrepancy_status,discrepancy_remark,count_operation_id,last_operation_id,
counted_by,counted_by_name,counted_at,bound_by,bound_by_name,bound_at)
VALUES(#{id},#{taskId},#{palletId},#{tenantId},#{inboundId},#{detailId},#{labelId},#{skuCode},#{cargoName},#{boxCode},
#{expectedQuantity},#{actualQuantity},#{discrepancyStatus},#{discrepancyRemark},#{operationId},#{operationId},
#{operatorId},#{operatorName},CURRENT_TIMESTAMP(3),CASE WHEN #{palletId} IS NULL THEN NULL ELSE #{operatorId} END,
CASE WHEN #{palletId} IS NULL THEN NULL ELSE #{operatorName} END,CASE WHEN #{palletId} IS NULL THEN NULL ELSE CURRENT_TIMESTAMP(3) END)""")
int insertCarton(@Param("id") long id,@Param("taskId") long taskId,@Param("palletId") Long palletId,
@Param("tenantId") long tenantId,@Param("inboundId") long inboundId,@Param("detailId") long detailId,
@Param("labelId") long labelId,@Param("skuCode") String skuCode,@Param("cargoName") String cargoName,
@Param("boxCode") String boxCode,@Param("expectedQuantity") int expectedQuantity,@Param("actualQuantity") int actualQuantity,
@Param("discrepancyStatus") String discrepancyStatus,@Param("discrepancyRemark") String discrepancyRemark,
@Param("operationId") String operationId,@Param("operatorId") long operatorId,@Param("operatorName") String operatorName);
@Select("SELECT * FROM fl_dropship_pallet WHERE task_id=#{taskId} ORDER BY id")
List<PalletRow> pallets(@Param("taskId") long taskId);
@Update("""
UPDATE fl_dropship_pallet_task SET counted_cartons=counted_cartons+1,allocated_cartons=allocated_cartons+#{boundCount},
discrepancy_cartons=discrepancy_cartons+#{discrepancyCount},version=version+1,updated_at=CURRENT_TIMESTAMP(3)
WHERE id=#{taskId} AND task_status='CLAIMED' AND assignee_id=#{operatorId} AND counted_cartons<total_cartons
AND allocated_cartons+#{boundCount}<=total_cartons""")
int addCounted(@Param("taskId") long taskId,@Param("operatorId") long operatorId,
@Param("boundCount") int boundCount,@Param("discrepancyCount") int discrepancyCount);
@Select("SELECT COUNT(*) FROM fl_dropship_pallet_carton WHERE task_id=#{taskId} AND pallet_id IS NULL") int unboundCount(@Param("taskId") long taskId);
@Select("SELECT COUNT(*) FROM fl_dropship_pallet_carton WHERE task_id=#{taskId} AND bind_operation_id=#{operationId}")
int bindOperationCount(@Param("taskId") long taskId,@Param("operationId") String operationId);
@Update("""
UPDATE fl_dropship_pallet_carton SET pallet_id=#{palletId},bind_operation_id=#{operationId},last_operation_id=#{operationId},
bound_by=#{operatorId},bound_by_name=#{operatorName},bound_at=CURRENT_TIMESTAMP(3) WHERE task_id=#{taskId} AND pallet_id IS NULL""")
int bindPending(@Param("taskId") long taskId,@Param("palletId") long palletId,@Param("operationId") String operationId,
@Param("operatorId") long operatorId,@Param("operatorName") String operatorName);
@Update("UPDATE fl_dropship_pallet_task SET allocated_cartons=allocated_cartons+#{delta},version=version+1,updated_at=CURRENT_TIMESTAMP(3) WHERE id=#{taskId} AND assignee_id=#{operatorId} AND allocated_cartons+#{delta} BETWEEN 0 AND counted_cartons")
int adjustBound(@Param("taskId") long taskId,@Param("operatorId") long operatorId,@Param("delta") int delta);
@Update("""
UPDATE fl_dropship_pallet_carton SET pallet_id=#{targetPalletId},last_operation_id=#{operationId},
bound_by=CASE WHEN #{targetPalletId} IS NULL THEN NULL ELSE #{operatorId} END,
bound_by_name=CASE WHEN #{targetPalletId} IS NULL THEN NULL ELSE #{operatorName} END,
bound_at=CASE WHEN #{targetPalletId} IS NULL THEN NULL ELSE CURRENT_TIMESTAMP(3) END
WHERE id=#{cartonId} AND task_id=#{taskId} AND last_operation_id<>#{operationId}""")
int moveCarton(@Param("taskId") long taskId,@Param("cartonId") long cartonId,@Param("targetPalletId") Long targetPalletId,
@Param("operationId") String operationId,@Param("operatorId") long operatorId,@Param("operatorName") String operatorName);
@Select("""
SELECT p.* FROM fl_dropship_pallet p JOIN fl_dropship_pallet_task t ON t.id=p.task_id
WHERE t.tenant_id=#{tenantId} AND t.inbound_id=#{inboundId} ORDER BY p.id
""")
List<PalletRow> palletsByInbound(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId);
@Update("""
UPDATE fl_dropship_pallet SET pallet_status='SEALED',seal_operation_id=#{operationId},completed_by=#{operatorId},
completed_by_name=#{operatorName},completed_at=CURRENT_TIMESTAMP(3),version=version+1,updated_at=CURRENT_TIMESTAMP(3)
WHERE id=#{palletId} AND task_id=#{taskId} AND pallet_status='OPEN' AND version=#{expectedVersion}
AND EXISTS(SELECT 1 FROM fl_dropship_pallet_carton c WHERE c.pallet_id=#{palletId})
AND NOT EXISTS(SELECT 1 FROM fl_dropship_pallet_carton c WHERE c.task_id=#{taskId} AND c.pallet_id IS NULL)""")
int seal(@Param("taskId") long taskId,@Param("palletId") long palletId,@Param("expectedVersion") int expectedVersion,
@Param("operationId") String operationId,@Param("operatorId") long operatorId,@Param("operatorName") String operatorName);
@Select("SELECT COUNT(*) FROM fl_dropship_pallet WHERE task_id=#{taskId} AND pallet_status='OPEN'") int openPalletCount(@Param("taskId") long taskId);
@Update("""
UPDATE fl_dropship_pallet_task SET task_status='COMPLETED',completed_at=CURRENT_TIMESTAMP(3),version=version+1,updated_at=CURRENT_TIMESTAMP(3)
WHERE id=#{taskId} AND task_status='CLAIMED' AND counted_cartons=total_cartons AND allocated_cartons=total_cartons
AND NOT EXISTS(SELECT 1 FROM fl_dropship_pallet p WHERE p.task_id=#{taskId} AND p.pallet_status='OPEN')""") int completeTask(@Param("taskId") long taskId);
@Update("""
UPDATE fl_inbound_order o SET o.process_status='WAIT_SPLIT',o.pallet_count=(SELECT COUNT(*) FROM fl_dropship_pallet p
WHERE p.inbound_id=o.id AND p.pallet_status='SEALED'),o.updated_by=#{operatorId},o.version=o.version+1
WHERE o.id=#{inboundId} AND o.tenant_id=#{tenantId} AND o.process_status='WAIT_PALLETIZE'
AND EXISTS(SELECT 1 FROM fl_dropship_pallet_task t WHERE t.id=#{taskId} AND t.task_status='COMPLETED')""")
int completeInbound(@Param("taskId") long taskId,@Param("tenantId") long tenantId,@Param("inboundId") long inboundId,@Param("operatorId") long operatorId);
@Select("SELECT inbound_code FROM fl_inbound_order WHERE tenant_id=#{tenantId} AND id=#{inboundId} AND deleted=0")
String inboundCode(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId);
@Select("SELECT c.*,p.pallet_no FROM fl_dropship_pallet_carton c LEFT JOIN fl_dropship_pallet p ON p.id=c.pallet_id WHERE c.pallet_id=#{palletId} ORDER BY c.sku_code,c.inbound_detail_id,c.id") List<CartonRow> cartons(@Param("palletId") long palletId);
@Select("SELECT c.*,p.pallet_no FROM fl_dropship_pallet_carton c LEFT JOIN fl_dropship_pallet p ON p.id=c.pallet_id WHERE c.task_id=#{taskId} AND c.pallet_id IS NULL ORDER BY c.counted_at,c.id") List<CartonRow> unboundCartons(@Param("taskId") long taskId);
@Select("""
SELECT COALESCE(o.actual_gross_weight_kg*COUNT(c.id)/NULLIF(o.total_package_count,0),0) weight_kg,
COALESCE(o.actual_volume_cbm*COUNT(c.id)/NULLIF(o.total_package_count,0),0) volume_cbm
FROM fl_dropship_pallet p JOIN fl_inbound_order o ON o.id=p.inbound_id AND o.tenant_id=p.tenant_id
LEFT JOIN fl_dropship_pallet_carton c ON c.pallet_id=p.id WHERE p.id=#{palletId}
GROUP BY p.id,o.actual_gross_weight_kg,o.actual_volume_cbm,o.total_package_count""")
PalletMetrics metrics(@Param("palletId") long palletId);
@Select("""
SELECT p.id pallet_id,p.pallet_no,'SEALED_PALLET' unit_type,TRUE indivisible,COUNT(c.id) carton_count,
COUNT(DISTINCT c.sku_code) sku_count,
COALESCE(o.actual_gross_weight_kg*COUNT(c.id)/NULLIF(o.total_package_count,0),0) weight_kg,
COALESCE(o.actual_volume_cbm*COUNT(c.id)/NULLIF(o.total_package_count,0),0) volume_cbm
FROM fl_dropship_pallet p JOIN fl_inbound_order o ON o.id=p.inbound_id AND o.tenant_id=p.tenant_id
JOIN fl_dropship_pallet_carton c ON c.pallet_id=p.id
WHERE p.inbound_id=#{inboundId} AND p.pallet_status='SEALED'
AND NOT EXISTS(SELECT 1 FROM fl_loading_plan_item i WHERE i.pallet_id=p.id AND i.deleted=0)
GROUP BY p.id,p.pallet_no,o.actual_gross_weight_kg,o.actual_volume_cbm,o.total_package_count ORDER BY p.id
""") List<PalletCandidate> palletCandidates(@Param("inboundId") long inboundId);
@Select("""
SELECT l.id carton_label_id,l.inbound_detail_id,l.sku_code,l.cargo_name,l.detail_package_count,
l.inbound_code,l.box_code
FROM fl_inbound_carton_label l
JOIN fl_dropship_pallet_task t ON t.inbound_id=l.inbound_id AND t.tenant_id=l.tenant_id
WHERE t.id=#{taskId} AND l.box_code=#{boxCode} AND l.batch_status='ACTIVE'
""")
LabelRow label(@Param("taskId") long taskId, @Param("boxCode") String boxCode);
@Select("""
SELECT p.id pallet_id,p.pallet_no,p.pallet_status,p.tenant_id account_id,p.inbound_id,o.order_id,o.order_no,
o.inbound_code,o.warehouse_id,o.channel_code,o.channel_name,o.destination_warehouse_id,
o.destination_code destination_warehouse_code,o.destination_name destination_warehouse_name,o.customer_name,
c.inbound_detail_id,c.sku_code,MAX(c.cargo_name) cargo_name,COUNT(c.id) carton_count,
SUM(c.actual_sku_quantity) actual_sku_quantity,
COALESCE(o.actual_gross_weight_kg/NULLIF(o.total_package_count,0),0) unit_gross_weight_kg,
COALESCE(o.actual_net_weight_kg/NULLIF(o.total_package_count,0),0) unit_net_weight_kg,
COALESCE(o.actual_volume_cbm/NULLIF(o.total_package_count,0),0) unit_volume_cbm
FROM fl_dropship_pallet p JOIN fl_inbound_order o ON o.id=p.inbound_id AND o.tenant_id=p.tenant_id
JOIN fl_dropship_pallet_carton c ON c.pallet_id=p.id WHERE p.id=#{palletId}
GROUP BY p.id,p.pallet_no,p.pallet_status,p.tenant_id,p.inbound_id,o.order_id,o.order_no,o.inbound_code,
o.warehouse_id,o.channel_code,o.channel_name,o.destination_warehouse_id,o.destination_code,o.destination_name,
o.customer_name,c.inbound_detail_id,c.sku_code,o.actual_gross_weight_kg,o.actual_net_weight_kg,o.actual_volume_cbm,o.total_package_count
ORDER BY c.inbound_detail_id
""") List<PalletLine> sealedPalletLines(@Param("palletId") long palletId);
@Select("""
SELECT l.id carton_label_id,l.inbound_detail_id,l.sku_code,l.cargo_name,l.detail_package_count,
l.inbound_code,l.box_code
FROM fl_inbound_carton_label l
WHERE l.tenant_id=#{tenantId} AND l.inbound_id=#{inboundId}
AND l.inbound_detail_id=#{detailId} AND l.batch_status='ACTIVE'
AND NOT EXISTS (SELECT 1 FROM fl_dropship_pallet_carton c
WHERE c.task_id=#{taskId} AND c.carton_label_id=l.id)
ORDER BY CASE WHEN l.box_code=#{preferredBoxCode} THEN 0 ELSE 1 END,l.carton_index,l.id
LIMIT #{count}
""")
List<LabelRow> unboundLabels(@Param("taskId") long taskId, @Param("tenantId") long tenantId,
@Param("inboundId") long inboundId, @Param("detailId") long detailId,
@Param("preferredBoxCode") String preferredBoxCode,
@Param("count") int count);
@Select("SELECT COUNT(*) FROM fl_dropship_pallet_carton WHERE operation_id=#{operationId}")
int operationCount(@Param("operationId") String operationId);
@Insert("""
INSERT INTO fl_dropship_pallet_carton
(id,task_id,pallet_id,tenant_id,inbound_id,inbound_detail_id,carton_label_id,sku_code,cargo_name,
box_code,operation_id,bound_by,bound_by_name,bound_at)
VALUES(#{id},#{taskId},#{palletId},#{tenantId},#{inboundId},#{detailId},#{labelId},#{skuCode},#{cargoName},
#{boxCode},#{operationId},#{operatorId},#{operatorName},CURRENT_TIMESTAMP(3))
""")
int insertCarton(@Param("id") long id, @Param("taskId") long taskId,
@Param("palletId") long palletId, @Param("tenantId") long tenantId,
@Param("inboundId") long inboundId, @Param("detailId") long detailId,
@Param("labelId") long labelId, @Param("skuCode") String skuCode,
@Param("cargoName") String cargoName, @Param("boxCode") String boxCode,
@Param("operationId") String operationId, @Param("operatorId") long operatorId,
@Param("operatorName") String operatorName);
@Update("""
UPDATE fl_dropship_pallet_task
SET allocated_cartons=allocated_cartons+#{count},version=version+1,updated_at=CURRENT_TIMESTAMP(3)
WHERE id=#{taskId} AND task_status='CLAIMED' AND assignee_id=#{operatorId}
AND allocated_cartons+#{count}<=total_cartons
""")
int addAllocated(@Param("taskId") long taskId, @Param("operatorId") long operatorId,
@Param("count") int count);
@Update("""
UPDATE fl_dropship_pallet
SET pallet_no=#{palletNo},pallet_status='COMPLETED',seal_operation_id=#{operationId},
completed_by=#{operatorId},completed_by_name=#{operatorName},completed_at=CURRENT_TIMESTAMP(3),
version=version+1,updated_at=CURRENT_TIMESTAMP(3)
WHERE id=#{palletId} AND task_id=#{taskId} AND pallet_status='OPEN' AND version=#{expectedVersion}
AND EXISTS(SELECT 1 FROM fl_dropship_pallet_carton c WHERE c.pallet_id=#{palletId})
""")
int seal(@Param("taskId") long taskId, @Param("palletId") long palletId,
@Param("expectedVersion") int expectedVersion, @Param("palletNo") String palletNo,
@Param("operationId") String operationId, @Param("operatorId") long operatorId,
@Param("operatorName") String operatorName);
@Select("SELECT COUNT(*) FROM fl_dropship_pallet WHERE task_id=#{taskId} AND pallet_status='OPEN'")
int openPalletCount(@Param("taskId") long taskId);
@Update("""
UPDATE fl_dropship_pallet_task
SET task_status='COMPLETED',completed_at=CURRENT_TIMESTAMP(3),version=version+1,updated_at=CURRENT_TIMESTAMP(3)
WHERE id=#{taskId} AND task_status='CLAIMED' AND allocated_cartons=total_cartons
AND NOT EXISTS(SELECT 1 FROM fl_dropship_pallet p WHERE p.task_id=#{taskId} AND p.pallet_status='OPEN')
""")
int completeTask(@Param("taskId") long taskId);
@Update("""
UPDATE fl_inbound_order o
SET o.process_status='WAIT_SPLIT',o.pallet_count=(SELECT COUNT(*) FROM fl_dropship_pallet p
WHERE p.inbound_id=o.id AND p.pallet_status='COMPLETED'),o.updated_by=#{operatorId},o.version=o.version+1
WHERE o.id=#{inboundId} AND o.tenant_id=#{tenantId} AND o.process_status='WAIT_PALLETIZE'
AND EXISTS(SELECT 1 FROM fl_dropship_pallet_task t WHERE t.id=#{taskId} AND t.task_status='COMPLETED')
""")
int completeInbound(@Param("taskId") long taskId, @Param("tenantId") long tenantId,
@Param("inboundId") long inboundId, @Param("operatorId") long operatorId);
@Select("""
SELECT c.* FROM fl_dropship_pallet_carton c WHERE c.pallet_id=#{palletId}
ORDER BY c.sku_code,c.inbound_detail_id,c.id
""")
List<CartonRow> cartons(@Param("palletId") long palletId);
@Getter @Setter class TaskRow {
private Long id; private Long tenantId; private Long inboundId; private Long warehouseId;
private String inboundCode; private String customerName; private String warehouseName;
private String taskStatus; private Integer totalCartons; private Integer allocatedCartons;
private Long assigneeId; private String assigneeName; private Integer version;
private String processStatus; private String fulfillmentType;
}
@Getter @Setter class PalletRow {
private Long id; private Long taskId; private Long tenantId; private Long inboundId;
private Long warehouseId; private String palletNo; private String palletStatus;
private String openOperationId; private String sealOperationId; private Integer version;
private LocalDateTime completedAt;
}
@Getter @Setter class LabelRow {
private Long cartonLabelId; private Long inboundDetailId; private String skuCode;
private String cargoName; private Integer detailPackageCount; private String inboundCode;
private String boxCode;
}
@Getter @Setter class CartonRow {
private Long inboundId; private Long inboundDetailId; private String skuCode;
private String cargoName; private String boxCode;
}
@Getter @Setter class TaskRow { private Long id,tenantId,inboundId,warehouseId,assigneeId; private String inboundCode,customerName,warehouseName,taskStatus,assigneeName,processStatus,fulfillmentType; private Integer totalCartons,countedCartons,allocatedCartons,discrepancyCartons,version; }
@Getter @Setter class PalletRow { private Long id,taskId,tenantId,inboundId,warehouseId; private String palletNo,palletStatus,sealOperationId; private Integer version; private LocalDateTime completedAt; }
@Getter @Setter class LabelRow { private Long cartonLabelId,inboundDetailId; private String skuCode,cargoName,inboundCode,boxCode; private Integer unitsPerCarton; }
@Getter @Setter class CartonRow { private Long id,taskId,palletId,inboundId,inboundDetailId; private String palletNo,skuCode,cargoName,boxCode,discrepancyStatus,discrepancyRemark; private Integer expectedSkuQuantity,actualSkuQuantity; }
record PalletMetrics(BigDecimal weightKg,BigDecimal volumeCbm) {}
@Getter @Setter class PalletLine { private Long palletId,tenantId,accountId,inboundId,orderId,warehouseId,destinationWarehouseId,inboundDetailId; private String palletNo,palletStatus,orderNo,inboundCode,channelCode,channelName,destinationWarehouseCode,destinationWarehouseName,customerName,skuCode,cargoName; private Integer cartonCount,actualSkuQuantity; private BigDecimal unitGrossWeightKg,unitNetWeightKg,unitVolumeCbm; }
}
@@ -91,7 +91,7 @@ public interface LoadingCompletedEventMapper {
JOIN fl_loading_plan_item item ON item.loading_plan_id=#{planId}
AND item.account_id=carton.tenant_id AND item.inbound_id=carton.inbound_id
AND item.inbound_detail_id=carton.inbound_detail_id AND item.deleted=0
WHERE pallet.pallet_status='COMPLETED'
WHERE pallet.pallet_status='SEALED'
ORDER BY carton.tenant_id,carton.inbound_id,pallet.id,item.inbound_detail_id,carton.id
""")
List<PalletRow> actualPalletRows(@Param("planId") long planId);
@@ -99,7 +99,7 @@ public interface LoadingCompletedEventMapper {
@Select("""
SELECT COUNT(DISTINCT p.id) FROM fl_dropship_pallet p
JOIN fl_loading_plan_item i ON i.loading_plan_id=#{planId} AND i.account_id=p.tenant_id
AND i.inbound_id=p.inbound_id AND i.deleted=0 WHERE p.pallet_status='COMPLETED'
AND i.inbound_id=p.inbound_id AND i.deleted=0 WHERE p.pallet_status='SEALED'
""")
int workPalletCount(@Param("planId") long planId);
@@ -171,7 +171,8 @@ public interface LoadingListQueryMapper {
SELECT i.id AS item_id, i.account_id, i.customer_name, i.order_id, i.order_no,
i.inbound_id, i.inbound_detail_id, i.inbound_code, b.container_no,
i.sku_code, i.destination_warehouse_name, i.product_name_zh,
product_name_en, product_image_url, i.package_count,
product_name_en, product_image_url, i.unit_type, i.pallet_id,
i.pallet_no, i.indivisible, i.package_count,
COALESCE(NULLIF(i.pallet_count,0),
CEIL(COALESCE(NULLIF(inbound.pallet_count,0),
(SELECT COUNT(*) FROM fl_inbound_weight_result weight
@@ -420,6 +421,10 @@ public interface LoadingListQueryMapper {
private String productNameZh;
private String productNameEn;
private String productImageUrl;
private String unitType;
private Long palletId;
private String palletNo;
private Boolean indivisible;
private Integer packageCount;
private Integer palletCount;
private BigDecimal grossWeightKg;
@@ -1,90 +0,0 @@
package com.baiyue.firstleg.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baiyue.firstleg.entity.LoadingWorkPalletCartonEntity;
import lombok.Getter;
import lombok.Setter;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import java.util.List;
@Mapper
public interface LoadingWorkPalletCartonMapper extends BaseMapper<LoadingWorkPalletCartonEntity> {
@Select("""
SELECT label.id carton_label_id,label.tenant_id,label.inbound_id,label.inbound_detail_id,
label.box_code,plan_item.id loading_plan_item_id,pick_item.id pick_item_id,
plan_item.sku_code,plan_item.units_per_carton
FROM fl_inbound_carton_label label
JOIN fl_loading_plan_item plan_item
ON plan_item.account_id=label.tenant_id AND plan_item.inbound_id=label.inbound_id
AND plan_item.inbound_detail_id=label.inbound_detail_id AND plan_item.deleted=0
JOIN fl_loading_pick_item pick_item ON pick_item.loading_plan_item_id=plan_item.id
JOIN fl_loading_pick_task task ON task.id=pick_item.pick_task_id
AND task.loading_plan_id=plan_item.loading_plan_id
JOIN fl_loading_plan plan ON plan.id=task.loading_plan_id
JOIN fl_container_booking booking ON booking.id=plan.container_booking_id
JOIN fl_inbound_order inbound_order ON inbound_order.id=plan_item.inbound_id
AND inbound_order.tenant_id=plan_item.account_id AND inbound_order.deleted=0
WHERE task.id=#{taskId} AND task.task_status='LOADING' AND booking.warehouse_id=#{warehouseId}
AND label.box_code=#{boxCode} AND label.batch_status='ACTIVE'
AND inbound_order.fulfillment_type='ONE_CLICK_DROPSHIP'
AND pick_item.item_status<>'SKIPPED'
ORDER BY label.tenant_id,label.id
""")
List<CartonCandidate> selectCandidates(@Param("taskId") long taskId,
@Param("warehouseId") long warehouseId,
@Param("boxCode") String boxCode);
@Select("SELECT * FROM fl_loading_work_pallet_carton WHERE pick_task_id=#{taskId} AND carton_label_id=#{labelId}")
LoadingWorkPalletCartonEntity selectBinding(@Param("taskId") long taskId,
@Param("labelId") long labelId);
@Select("SELECT * FROM fl_loading_work_pallet_carton WHERE operation_id=#{operationId}")
LoadingWorkPalletCartonEntity selectByOperation(@Param("operationId") String operationId);
@Select("SELECT * FROM fl_loading_work_pallet_carton WHERE work_pallet_id=#{palletId} ORDER BY id")
List<LoadingWorkPalletCartonEntity> selectPalletCartons(@Param("palletId") long palletId);
@Select("SELECT COUNT(*) FROM fl_loading_work_pallet_carton WHERE work_pallet_id=#{palletId}")
int countPalletCartons(@Param("palletId") long palletId);
@Select("""
SELECT COUNT(*) FROM (
SELECT carton.pick_item_id
FROM fl_loading_work_pallet_carton carton
JOIN fl_loading_pick_item pick_item ON pick_item.id=carton.pick_item_id
WHERE carton.pick_task_id=#{taskId}
GROUP BY carton.pick_item_id,pick_item.picked_package_count
HAVING COUNT(*)>pick_item.picked_package_count
) unreconciled
""")
int countUnreconciled(@Param("taskId") long taskId);
@Update("""
UPDATE fl_loading_work_pallet_carton
SET work_pallet_id=#{targetPalletId},bound_by=#{operatorId},bound_by_name=#{operatorName},
operation_id=#{operationId},version=version+1
WHERE id=#{id} AND version=#{version}
""")
int move(@Param("id") long id, @Param("version") int version,
@Param("targetPalletId") long targetPalletId,
@Param("operatorId") long operatorId, @Param("operatorName") String operatorName,
@Param("operationId") String operationId);
@Getter
@Setter
class CartonCandidate {
private Long cartonLabelId;
private Long tenantId;
private Long inboundId;
private Long inboundDetailId;
private String boxCode;
private Long loadingPlanItemId;
private Long pickItemId;
private String skuCode;
private Integer unitsPerCarton;
}
}
@@ -1,77 +0,0 @@
package com.baiyue.firstleg.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baiyue.firstleg.entity.LoadingWorkPalletEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import java.util.List;
@Mapper
public interface LoadingWorkPalletMapper extends BaseMapper<LoadingWorkPalletEntity> {
@Select("""
SELECT task.id task_id,task.loading_plan_id,task.task_status,booking.warehouse_id,
assignment.assignee_id,
SUM(CASE WHEN inbound_order.fulfillment_type='ONE_CLICK_DROPSHIP' THEN 1 ELSE 0 END) dropship_item_count
FROM fl_loading_pick_task task
JOIN fl_loading_plan plan ON plan.id=task.loading_plan_id
JOIN fl_container_booking booking ON booking.id=plan.container_booking_id
JOIN fl_loading_pick_item pick_item ON pick_item.pick_task_id=task.id
JOIN fl_loading_plan_item plan_item ON plan_item.id=pick_item.loading_plan_item_id AND plan_item.deleted=0
JOIN fl_inbound_order inbound_order ON inbound_order.id=plan_item.inbound_id
AND inbound_order.tenant_id=plan_item.account_id AND inbound_order.deleted=0
LEFT JOIN fl_loading_pick_assignment assignment ON assignment.pick_task_id=task.id
WHERE task.id=#{taskId} AND booking.warehouse_id=#{warehouseId}
GROUP BY task.id,task.loading_plan_id,task.task_status,booking.warehouse_id,assignment.assignee_id
""")
TaskContext selectTaskContext(@Param("taskId") long taskId, @Param("warehouseId") long warehouseId);
@Select("SELECT * FROM fl_loading_work_pallet WHERE open_operation_id=#{operationId}")
LoadingWorkPalletEntity selectByOpenOperation(@Param("operationId") String operationId);
@Select("SELECT * FROM fl_loading_work_pallet WHERE seal_operation_id=#{operationId}")
LoadingWorkPalletEntity selectBySealOperation(@Param("operationId") String operationId);
@Select("""
SELECT pallet.*
FROM fl_loading_work_pallet pallet
JOIN fl_loading_pick_task task ON task.id=pallet.pick_task_id
JOIN fl_loading_plan plan ON plan.id=task.loading_plan_id
JOIN fl_container_booking booking ON booking.id=plan.container_booking_id
WHERE pallet.pick_task_id=#{taskId} AND pallet.id=#{palletId}
AND booking.warehouse_id=#{warehouseId}
""")
LoadingWorkPalletEntity selectScoped(@Param("taskId") long taskId,
@Param("palletId") long palletId,
@Param("warehouseId") long warehouseId);
@Select("""
SELECT pallet.*
FROM fl_loading_work_pallet pallet
JOIN fl_loading_pick_task task ON task.id=pallet.pick_task_id
JOIN fl_loading_plan plan ON plan.id=task.loading_plan_id
JOIN fl_container_booking booking ON booking.id=plan.container_booking_id
WHERE pallet.pick_task_id=#{taskId} AND booking.warehouse_id=#{warehouseId}
AND pallet.pallet_status<>'CANCELLED'
ORDER BY pallet.id
""")
List<LoadingWorkPalletEntity> selectTaskPallets(@Param("taskId") long taskId,
@Param("warehouseId") long warehouseId);
@Update("""
UPDATE fl_loading_work_pallet
SET pallet_status='SEALED',seal_operation_id=#{operationId},sealed_by=#{operatorId},
sealed_by_name=#{operatorName},sealed_at=CURRENT_TIMESTAMP(3),version=version+1
WHERE id=#{palletId} AND pick_task_id=#{taskId} AND warehouse_id=#{warehouseId}
AND pallet_status='OPEN' AND version=#{expectedVersion}
""")
int seal(@Param("taskId") long taskId, @Param("palletId") long palletId,
@Param("warehouseId") long warehouseId, @Param("expectedVersion") int expectedVersion,
@Param("operationId") String operationId, @Param("operatorId") long operatorId,
@Param("operatorName") String operatorName);
record TaskContext(Long taskId, Long loadingPlanId, String taskStatus, Long warehouseId,
Long assigneeId, Integer dropshipItemCount) {}
}
@@ -112,7 +112,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba
SELECT carton.tenant_id,carton.inbound_id,batch.id,carton.inbound_detail_id,COUNT(*),
COALESCE(batch.total_volume_cbm/NULLIF(source.total_package_count,0),0)
FROM fl_dropship_pallet_carton carton
JOIN fl_dropship_pallet pallet ON pallet.id=carton.pallet_id AND pallet.pallet_status='COMPLETED'
JOIN fl_dropship_pallet pallet ON pallet.id=carton.pallet_id AND pallet.pallet_status='SEALED'
JOIN fl_inbound_order source ON source.id=carton.inbound_id AND source.tenant_id=carton.tenant_id
AND source.fulfillment_type='ONE_CLICK_DROPSHIP'
JOIN fl_inbound_measurement batch ON batch.inbound_id=source.id AND batch.tenant_id=source.tenant_id
@@ -399,7 +399,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba
SELECT carton.tenant_id,carton.inbound_id,batch.id,carton.inbound_detail_id,COUNT(*),
COALESCE(batch.total_volume_cbm/NULLIF(source.total_package_count,0),0)
FROM fl_dropship_pallet_carton carton
JOIN fl_dropship_pallet pallet ON pallet.id=carton.pallet_id AND pallet.pallet_status='COMPLETED'
JOIN fl_dropship_pallet pallet ON pallet.id=carton.pallet_id AND pallet.pallet_status='SEALED'
JOIN fl_inbound_order source ON source.id=carton.inbound_id AND source.tenant_id=carton.tenant_id
AND source.fulfillment_type='ONE_CLICK_DROPSHIP'
JOIN fl_inbound_measurement batch ON batch.inbound_id=source.id AND batch.tenant_id=source.tenant_id
@@ -187,7 +187,8 @@ public class LoadingListRepository {
row.getOrderId(), row.getOrderNo(), row.getInboundId(), row.getInboundDetailId(),
row.getInboundCode(), row.getContainerNo(), row.getSkuCode(),
row.getDestinationWarehouseName(), row.getProductNameZh(), row.getProductNameEn(),
row.getProductImageUrl(), integer(row.getPackageCount()), integer(row.getPalletCount()),
row.getProductImageUrl(), row.getUnitType(), row.getPalletId(), row.getPalletNo(),
Boolean.TRUE.equals(row.getIndivisible()), integer(row.getPackageCount()), integer(row.getPalletCount()),
decimal(row.getGrossWeightKg()), decimal(row.getNetWeightKg()), decimal(row.getVolumeCbm()),
decimal(row.getDimensionalWeightKg()), value(row.getProductQuantity()),
integer(row.getUnitsPerCarton()), row.getFbaTrackingNo(), row.getShipmentNo(), row.getShippingMark(),
@@ -97,6 +97,11 @@ public class SmartLoadingRepository {
.eq(LoadingPlanItemEntity::getInboundId, inboundId)
.eq(LoadingPlanItemEntity::getDeleted, false));
}
public List<LoadingPlanItemEntity> activeItemsByPallet(long palletId) {
return items.selectList(new LambdaQueryWrapper<LoadingPlanItemEntity>()
.eq(LoadingPlanItemEntity::getPalletId, palletId)
.eq(LoadingPlanItemEntity::getDeleted, false));
}
public LoadingPlanEntity plan(long planId) { return plans.selectById(planId); }
public LoadingPlanEntity planByBooking(long bookingId) {
return plans.selectOne(new LambdaQueryWrapper<LoadingPlanEntity>()
@@ -121,6 +126,13 @@ public class SmartLoadingRepository {
.eq(LoadingPlanItemEntity::getInboundDetailId, detailId)
.eq(LoadingPlanItemEntity::getDeleted, false));
}
public LoadingPlanItemEntity activePalletItem(long planId, long palletId, long detailId) {
return items.selectOne(new LambdaQueryWrapper<LoadingPlanItemEntity>()
.eq(LoadingPlanItemEntity::getLoadingPlanId, planId)
.eq(LoadingPlanItemEntity::getPalletId, palletId)
.eq(LoadingPlanItemEntity::getInboundDetailId, detailId)
.eq(LoadingPlanItemEntity::getDeleted, false));
}
public LoadingPlanHistoryEntity operation(String operationId) {
return histories.selectOne(new LambdaQueryWrapper<LoadingPlanHistoryEntity>()
.eq(LoadingPlanHistoryEntity::getOperationId, operationId));
@@ -4,194 +4,193 @@ import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.dto.DropshipPalletizationDtos;
import com.baiyue.firstleg.dto.DropshipPalletizationDtos.Pallet;
import com.baiyue.firstleg.dto.DropshipPalletizationDtos.ScanResult;
import com.baiyue.firstleg.dto.DropshipPalletizationDtos.SkuItem;
import com.baiyue.firstleg.dto.DropshipPalletizationDtos.Task;
import com.baiyue.firstleg.dto.DropshipPalletizationDtos.*;
import com.baiyue.firstleg.entity.InboundOrderEntity;
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper;
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper.CartonRow;
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper.LabelRow;
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper.PalletRow;
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper.TaskRow;
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper.*;
import jakarta.annotation.Resource;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.math.BigDecimal;
import java.util.*;
/** 测量完成即下发的一件代发合托,不创建国内仓库托盘、库位或库存。 */
/** 一件代发测量后的逐箱清点、待绑托和封托,不创建第二套装柜工作托盘。 */
@Service
public class DropshipPalletizationService {
@Resource private DropshipPalletizationMapper mapper;
@Resource private FirstlegDocumentNumberGenerator numbers;
@Resource private DropshipPalletizationMapper mapper;
public void createTask(InboundOrderEntity inbound) {
if (!"ONE_CLICK_DROPSHIP".equals(inbound.getFulfillmentType())) return;
if (inbound.getTotalPackageCount() == null || inbound.getTotalPackageCount() < 1) {
throw invalid("一件代发测量完成箱数无效,不能下发合托任务");
}
mapper.insertTask(IdWorker.getId(), inbound.getTenantId(), inbound.getId(),
inbound.getWarehouseId(), inbound.getTotalPackageCount());
}
public void createTask(InboundOrderEntity inbound) {
if (!"ONE_CLICK_DROPSHIP".equals(inbound.getFulfillmentType())) return;
if (inbound.getTotalPackageCount()==null || inbound.getTotalPackageCount()<1) throw invalid("一件代发测量完成箱数无效,不能下发清点组托任务");
mapper.insertTask(IdWorker.getId(),inbound.getTenantId(),inbound.getId(),inbound.getWarehouseId(),inbound.getTotalPackageCount());
}
public List<DropshipPalletizationDtos.PendingTask> pending(long operatorId, String keyword) {
return mapper.pending(operatorId, normalize(keyword));
}
public List<PendingTask> pending(long operatorId,String keyword){ return mapper.pending(operatorId,normalize(keyword)); }
@Transactional
public Task claim(long taskId, long warehouseId, long operatorId, String operatorName) {
if (mapper.claim(taskId, warehouseId, operatorId, operatorName) != 1) {
throw conflict("任务已被其他人员领取或状态已变化");
}
return task(taskId, warehouseId, operatorId);
}
@Transactional
public Task claim(long taskId,long warehouseId,long operatorId,String operatorName){
if(mapper.claim(taskId,warehouseId,operatorId,operatorName)!=1) throw conflict("任务已被其他人员领取或状态已变化");
return task(taskId,warehouseId,operatorId);
}
public Task task(long taskId, long warehouseId, long operatorId) {
TaskRow row = requireTask(taskId, warehouseId);
if (row.getAssigneeId() != null && row.getAssigneeId() != operatorId) {
throw conflict("任务已由其他人员领取");
}
return view(row);
}
public Task task(long taskId,long warehouseId,long operatorId){
TaskRow row=requireTask(taskId,warehouseId);
if(row.getAssigneeId()!=null && row.getAssigneeId()!=operatorId) throw conflict("任务已由其他人员领取");
return view(row);
}
@Transactional
public Pallet open(long taskId, DropshipPalletizationDtos.OpenPalletRequest request,
long operatorId, String operatorName) {
TaskRow task = requireWorkingTask(taskId, request.warehouseId(), operatorId);
String operationId = request.operationId().trim();
PalletRow replay = mapper.palletByOpenOperation(operationId);
if (replay != null) {
if (replay.getTaskId() != taskId) throw conflict("开托幂等号已用于其他任务");
return palletView(replay, task.getInboundCode());
}
long id = IdWorker.getId();
mapper.insertPallet(id, taskId, task.getTenantId(), task.getInboundId(), task.getWarehouseId(),
operationId, operatorId, operatorName);
return palletView(mapper.pallet(taskId, id, request.warehouseId()), task.getInboundCode());
}
public Carton resolveCarton(long taskId,long warehouseId,String boxCode,long operatorId){
TaskRow task=requireTask(taskId,warehouseId);
if(task.getAssigneeId()!=null && task.getAssigneeId()!=operatorId) throw conflict("任务已由其他人员领取");
LabelRow label=requireLabel(taskId,boxCode);
CartonRow counted=mapper.countedByLabel(taskId,label.getCartonLabelId());
if(counted!=null) return cartonView(counted,task.getInboundCode(),false,"箱码已清点");
String block=null;
if(!"CLAIMED".equals(task.getTaskStatus())) block="请先领取任务";
else if(!Objects.equals(task.getAssigneeId(),operatorId)) block="任务不属于当前操作员";
else if(!"WAIT_PALLETIZE".equals(task.getProcessStatus())) block="入仓单已不在待清点组托状态";
return new Carton(null,label.getBoxCode(),task.getInboundId(),label.getInboundCode(),label.getInboundDetailId(),
label.getSkuCode(),label.getCargoName(),label.getUnitsPerCarton(),null,null,null,"UNCOUNTED",null,null,
block==null,block);
}
@Transactional
public ScanResult scan(long taskId, long palletId, DropshipPalletizationDtos.ScanRequest request,
long operatorId, String operatorName) {
TaskRow task = requireWorkingTask(taskId, request.warehouseId(), operatorId);
PalletRow pallet = requireOpenPallet(taskId, palletId, request.warehouseId());
String operationId = request.operationId().trim();
LabelRow scanned = mapper.label(taskId, request.boxCode().trim());
if (scanned == null) throw new BizException(ResultCode.NOT_FOUND, "箱唛不属于当前一件代发入仓单");
if (!StringUtils.hasText(scanned.getSkuCode())) throw conflict("箱唛缺少SKU,不能合托");
if (mapper.operationCount(operationId) > 0) {
return scanResult(scanned, request.cartonCount(), task, pallet);
}
List<LabelRow> labels = mapper.unboundLabels(taskId, task.getTenantId(), task.getInboundId(),
scanned.getInboundDetailId(), scanned.getBoxCode(), request.cartonCount());
if (labels.size() != request.cartonCount()) {
throw conflict("本SKU剩余未上托箱数不足,请刷新任务后重试");
}
for (LabelRow label : labels) {
mapper.insertCarton(IdWorker.getId(), taskId, palletId, task.getTenantId(), task.getInboundId(),
label.getInboundDetailId(), label.getCartonLabelId(), label.getSkuCode(), label.getCargoName(),
label.getBoxCode(), operationId, operatorId, operatorName);
}
if (mapper.addAllocated(taskId, operatorId, labels.size()) != 1) {
throw conflict("任务箱数已变化,请刷新后重试");
}
TaskRow refreshed = requireTask(taskId, request.warehouseId());
return scanResult(scanned, labels.size(), refreshed,
mapper.pallet(taskId, palletId, request.warehouseId()));
}
@Transactional
public CountResult countCarton(long taskId,CountCartonRequest request,long operatorId,String operatorName){
TaskRow task=requireWorkingTask(taskId,request.warehouseId(),operatorId);
String operation=operation(request.operationId());
CartonRow replay=mapper.cartonByCountOperation(operation);
if(replay!=null){
if(!Objects.equals(replay.getTaskId(),taskId) || !Objects.equals(replay.getBoxCode(),request.boxCode().trim())
|| !Objects.equals(replay.getActualSkuQuantity(),request.actualSkuQuantity())) throw conflict("清点幂等号已用于不同操作");
return countResult(task,replay);
}
LabelRow label=requireLabel(taskId,request.boxCode());
if(mapper.countedByLabel(taskId,label.getCartonLabelId())!=null) throw conflict("箱码已清点,不能重复清点");
int expected=requireExpectedQuantity(label);
boolean mismatch=expected!=request.actualSkuQuantity();
String remark=normalize(request.discrepancyRemark());
if(!mismatch && request.discrepancyAction()!=DiscrepancyAction.MATCH) throw invalid("实收数量一致时差异动作必须为 MATCH");
if(mismatch && request.discrepancyAction()!=DiscrepancyAction.ACCEPT_VARIANCE) throw conflict("实收数量存在差异,请确认差异处理结果");
if(mismatch && !StringUtils.hasText(remark)) throw invalid("接受数量差异时必须填写处理说明");
PalletRow pallet=StringUtils.hasText(request.palletNo())
? getOrCreatePallet(task,request.palletNo().trim(),operation,operatorId,operatorName):null;
long cartonId=IdWorker.getId();
mapper.insertCarton(cartonId,taskId,pallet==null?null:pallet.getId(),task.getTenantId(),task.getInboundId(),
label.getInboundDetailId(),label.getCartonLabelId(),label.getSkuCode(),label.getCargoName(),label.getBoxCode(),
expected,request.actualSkuQuantity(),mismatch?"ACCEPTED_VARIANCE":"MATCH",remark,operation,operatorId,operatorName);
if(mapper.addCounted(taskId,operatorId,pallet==null?0:1,mismatch?1:0)!=1) throw conflict("任务清点数量已变化,请刷新后重试");
TaskRow refreshed=requireTask(taskId,request.warehouseId());
return countResult(refreshed,mapper.carton(taskId,cartonId));
}
@Transactional
public Task seal(long taskId, long palletId, DropshipPalletizationDtos.SealPalletRequest request,
long operatorId, String operatorName) {
TaskRow task = requireWorkingTask(taskId, request.warehouseId(), operatorId);
PalletRow replay = mapper.palletBySealOperation(request.operationId().trim());
if (replay != null) {
if (replay.getTaskId() != taskId || replay.getId() != palletId) {
throw conflict("封托幂等号已用于其他托盘");
}
return view(requireTask(taskId, request.warehouseId()));
}
requireOpenPallet(taskId, palletId, request.warehouseId());
String palletNo = numbers.next("DP");
if (mapper.seal(taskId, palletId, request.expectedVersion(), palletNo, request.operationId().trim(),
operatorId, operatorName) != 1) {
throw conflict("空托盘、托盘状态或版本已变化,请刷新后重试");
}
TaskRow refreshed = requireTask(taskId, request.warehouseId());
if (refreshed.getAllocatedCartons().equals(refreshed.getTotalCartons())
&& mapper.openPalletCount(taskId) == 0) {
if (mapper.completeTask(taskId) != 1
|| mapper.completeInbound(taskId, refreshed.getTenantId(), refreshed.getInboundId(), operatorId) != 1) {
throw conflict("合托完成状态已变化,请刷新后重试");
}
}
return view(requireTask(taskId, request.warehouseId()));
}
@Transactional
public Task bindPending(long taskId,BindPendingRequest request,long operatorId,String operatorName){
TaskRow task=requireWorkingTask(taskId,request.warehouseId(),operatorId);
String operation=operation(request.operationId());
if(mapper.bindOperationCount(taskId,operation)>0) return view(requireTask(taskId,request.warehouseId()));
PalletRow pallet=getOrCreatePallet(task,request.palletNo().trim(),operation,operatorId,operatorName);
int expected=mapper.unboundCount(taskId);
if(expected==0) throw conflict("当前没有待绑托箱");
int bound=mapper.bindPending(taskId,pallet.getId(),operation,operatorId,operatorName);
if(bound!=expected || mapper.adjustBound(taskId,operatorId,bound)!=1) throw conflict("待绑托箱已变化,请刷新后重试");
return view(requireTask(taskId,request.warehouseId()));
}
public List<Pallet> inboundPallets(long tenantId, long inboundId) {
String inboundCode = mapper.inboundCode(tenantId, inboundId);
if (inboundCode == null) throw new BizException(ResultCode.NOT_FOUND, "入库单不存在");
return mapper.palletsByInbound(tenantId, inboundId).stream()
.map(row -> palletView(row, inboundCode)).toList();
}
@Transactional
public Task moveCarton(long taskId,long cartonId,MoveCartonRequest request,long operatorId,String operatorName){
TaskRow task=requireWorkingTask(taskId,request.warehouseId(),operatorId);
CartonRow carton=mapper.carton(taskId,cartonId);
if(carton==null) throw new BizException(ResultCode.NOT_FOUND,"清点箱不存在");
if(carton.getPalletId()!=null) requireOpenPallet(taskId,carton.getPalletId(),request.warehouseId());
PalletRow target=StringUtils.hasText(request.targetPalletNo())
? getOrCreatePallet(task,request.targetPalletNo().trim(),operation(request.operationId()),operatorId,operatorName):null;
if(Objects.equals(carton.getPalletId(),target==null?null:target.getId())) return view(task);
if(mapper.moveCarton(taskId,cartonId,target==null?null:target.getId(),operation(request.operationId()),operatorId,operatorName)!=1)
throw conflict("清点箱已被其他操作修改,请刷新后重试");
int delta=carton.getPalletId()==null?1:(target==null?-1:0);
if(delta!=0 && mapper.adjustBound(taskId,operatorId,delta)!=1) throw conflict("任务绑托数量已变化,请刷新后重试");
return view(requireTask(taskId,request.warehouseId()));
}
private ScanResult scanResult(LabelRow label, int added, TaskRow task, PalletRow pallet) {
return new ScanResult(label.getSkuCode(), label.getCargoName(), label.getInboundCode(), added,
task.getAllocatedCartons(), task.getTotalCartons() - task.getAllocatedCartons(),
palletView(pallet, task.getInboundCode()));
}
@Transactional
public Task seal(long taskId,long palletId,SealPalletRequest request,long operatorId,String operatorName){
TaskRow task=requireWorkingTask(taskId,request.warehouseId(),operatorId);
PalletRow replay=mapper.palletBySealOperation(operation(request.operationId()));
if(replay!=null){ if(!Objects.equals(replay.getTaskId(),taskId)||!Objects.equals(replay.getId(),palletId)) throw conflict("封托幂等号已用于其他托盘"); return view(requireTask(taskId,request.warehouseId())); }
requireOpenPallet(taskId,palletId,request.warehouseId());
if(mapper.unboundCount(taskId)>0) throw conflict("仍有已清点待绑托箱,不能封托");
if(mapper.seal(taskId,palletId,request.expectedVersion(),operation(request.operationId()),operatorId,operatorName)!=1)
throw conflict("空托盘、托盘状态或版本已变化,请刷新后重试");
TaskRow refreshed=requireTask(taskId,request.warehouseId());
if(Objects.equals(refreshed.getCountedCartons(),refreshed.getTotalCartons()) &&
Objects.equals(refreshed.getAllocatedCartons(),refreshed.getTotalCartons()) && mapper.openPalletCount(taskId)==0){
if(mapper.completeTask(taskId)!=1 || mapper.completeInbound(taskId,refreshed.getTenantId(),refreshed.getInboundId(),operatorId)!=1)
throw conflict("组托完成状态已变化,请刷新后重试");
}
return view(requireTask(taskId,request.warehouseId()));
}
private Task view(TaskRow row) {
return new Task(row.getId(), row.getInboundId(), row.getInboundCode(), row.getTenantId(),
row.getCustomerName(), row.getWarehouseId(), row.getWarehouseName(), row.getTotalCartons(),
row.getAllocatedCartons(), row.getTaskStatus(), row.getAssigneeId(), row.getAssigneeName(),
row.getVersion(), mapper.pallets(row.getId()).stream()
.map(pallet -> palletView(pallet, row.getInboundCode())).toList());
}
public List<Pallet> inboundPallets(long tenantId,long inboundId){
String code=mapper.inboundCode(tenantId,inboundId);
if(code==null) throw new BizException(ResultCode.NOT_FOUND,"入库单不存在");
return mapper.palletsByInbound(tenantId,inboundId).stream().map(p->palletView(p,code)).toList();
}
private Pallet palletView(PalletRow row, String inboundCode) {
List<CartonRow> cartons = mapper.cartons(row.getId());
Map<String, List<CartonRow>> groups = new LinkedHashMap<>();
for (CartonRow carton : cartons) {
groups.computeIfAbsent(carton.getInboundDetailId() + "|" + carton.getSkuCode(), ignored -> new ArrayList<>())
.add(carton);
}
List<SkuItem> items = groups.values().stream().map(values -> {
CartonRow first = values.get(0);
return new SkuItem(first.getInboundId(), inboundCode, first.getInboundDetailId(), first.getSkuCode(),
first.getCargoName(), values.size(), values.stream().map(CartonRow::getBoxCode).toList());
}).toList();
return new Pallet(row.getId(), row.getPalletNo(), row.getPalletStatus(), cartons.size(), items.size(),
row.getVersion(), items, row.getCompletedAt());
}
private PalletRow getOrCreatePallet(TaskRow task,String palletNo,String operation,long operatorId,String operatorName){
PalletRow existing=mapper.palletByNo(palletNo);
if(existing!=null){
if(!Objects.equals(existing.getTaskId(),task.getId()) || !Objects.equals(existing.getTenantId(),task.getTenantId())) throw conflict("托盘码已被其他客户或入仓任务占用");
if(!"OPEN".equals(existing.getPalletStatus())) throw conflict("托盘已封托,不能继续绑定箱");
return existing;
}
try{ mapper.insertPallet(IdWorker.getId(),task.getId(),task.getTenantId(),task.getInboundId(),task.getWarehouseId(),palletNo,operation,operatorId,operatorName); }
catch(DuplicateKeyException e){ existing=mapper.palletByNo(palletNo); if(existing==null) throw conflict("托盘码已被并发占用"); }
existing=mapper.palletByNo(palletNo);
if(existing==null || !Objects.equals(existing.getTaskId(),task.getId())) throw conflict("托盘码已被其他任务占用");
return existing;
}
private TaskRow requireWorkingTask(long taskId, long warehouseId, long operatorId) {
TaskRow task = requireTask(taskId, warehouseId);
if (!"CLAIMED".equals(task.getTaskStatus())) throw conflict("请先领取待合托任务");
if (task.getAssigneeId() == null || task.getAssigneeId() != operatorId) throw conflict("任务不属于当前操作员");
if (!"WAIT_PALLETIZE".equals(task.getProcessStatus())) throw conflict("入仓单已不在待合托状态");
return task;
}
private CountResult countResult(TaskRow task,CartonRow carton){
Pallet p=carton.getPalletId()==null?null:palletView(mapper.pallet(task.getId(),carton.getPalletId(),task.getWarehouseId()),task.getInboundCode());
return new CountResult(cartonView(carton,task.getInboundCode(),false,null),task.getCountedCartons(),task.getAllocatedCartons(),
task.getCountedCartons()-task.getAllocatedCartons(),task.getTotalCartons()-task.getCountedCartons(),p);
}
private TaskRow requireTask(long taskId, long warehouseId) {
TaskRow task = mapper.task(taskId, warehouseId);
if (task == null) throw new BizException(ResultCode.NOT_FOUND, "未找到当前仓库的一件代发合托任务");
return task;
}
private Task view(TaskRow row){
return new Task(row.getId(),row.getInboundId(),row.getInboundCode(),row.getTenantId(),row.getCustomerName(),row.getWarehouseId(),row.getWarehouseName(),
row.getTotalCartons(),row.getCountedCartons(),row.getAllocatedCartons(),row.getCountedCartons()-row.getAllocatedCartons(),row.getDiscrepancyCartons(),
row.getTaskStatus(),row.getAssigneeId(),row.getAssigneeName(),row.getVersion(),mapper.pallets(row.getId()).stream().map(p->palletView(p,row.getInboundCode())).toList(),
mapper.unboundCartons(row.getId()).stream().map(c->cartonView(c,row.getInboundCode(),false,null)).toList());
}
private PalletRow requireOpenPallet(long taskId, long palletId, long warehouseId) {
PalletRow pallet = mapper.pallet(taskId, palletId, warehouseId);
if (pallet == null) throw new BizException(ResultCode.NOT_FOUND, "托盘不存在");
if (!"OPEN".equals(pallet.getPalletStatus())) throw conflict("托盘已完成,不能继续上托");
return pallet;
}
private Pallet palletView(PalletRow row,String inboundCode){
List<CartonRow> cartons=mapper.cartons(row.getId()); Map<String,List<CartonRow>> groups=new LinkedHashMap<>();
for(CartonRow c:cartons) groups.computeIfAbsent(c.getInboundDetailId()+"|"+c.getSkuCode(),ignored->new ArrayList<>()).add(c);
List<SkuItem> items=groups.values().stream().map(values->{CartonRow first=values.get(0);return new SkuItem(first.getInboundId(),inboundCode,
first.getInboundDetailId(),first.getSkuCode(),first.getCargoName(),values.size(),values.stream().mapToInt(v->v.getActualSkuQuantity()==null?0:v.getActualSkuQuantity()).sum(),
values.stream().map(CartonRow::getBoxCode).toList());}).toList();
int discrepancies=(int)cartons.stream().filter(c->"ACCEPTED_VARIANCE".equals(c.getDiscrepancyStatus())).count();
PalletMetrics metrics=mapper.metrics(row.getId());
return new Pallet(row.getId(),row.getPalletNo(),row.getPalletStatus(),cartons.size(),items.size(),discrepancies,
metrics==null?BigDecimal.ZERO:metrics.weightKg(),metrics==null?BigDecimal.ZERO:metrics.volumeCbm(),row.getVersion(),items,
cartons.stream().map(c->cartonView(c,inboundCode,false,null)).toList(),row.getCompletedAt());
}
private static String normalize(String value) { return StringUtils.hasText(value) ? value.trim() : null; }
private static BizException conflict(String message) { return new BizException(ResultCode.CONFLICT, message); }
private static BizException invalid(String message) { return new BizException(ResultCode.VALIDATION_FAILED, message); }
private Carton cartonView(CartonRow row,String inboundCode,boolean canCount,String blockReason){
return new Carton(row.getId(),row.getBoxCode(),row.getInboundId(),inboundCode,row.getInboundDetailId(),row.getSkuCode(),row.getCargoName(),
row.getExpectedSkuQuantity(),row.getActualSkuQuantity(),row.getDiscrepancyStatus(),row.getDiscrepancyRemark(),
row.getPalletId()==null?"COUNTED_UNBOUND":"BOUND",row.getPalletId(),row.getPalletNo(),canCount,blockReason);
}
private LabelRow requireLabel(long taskId,String boxCode){ LabelRow label=mapper.label(taskId,boxCode.trim()); if(label==null) throw new BizException(ResultCode.NOT_FOUND,"箱码不属于当前一件代发入仓单"); if(!StringUtils.hasText(label.getSkuCode())) throw conflict("箱码缺少SKU快照,不能清点"); requireExpectedQuantity(label); return label; }
private int requireExpectedQuantity(LabelRow label){ if(label.getUnitsPerCarton()==null||label.getUnitsPerCarton()<1) throw conflict("箱码缺少每箱SKU数量快照,不能清点"); return label.getUnitsPerCarton(); }
private TaskRow requireWorkingTask(long taskId,long warehouseId,long operatorId){ TaskRow task=requireTask(taskId,warehouseId); if(!"CLAIMED".equals(task.getTaskStatus())) throw conflict("请先领取待清点组托任务"); if(!Objects.equals(task.getAssigneeId(),operatorId)) throw conflict("任务不属于当前操作员"); if(!"WAIT_PALLETIZE".equals(task.getProcessStatus())) throw conflict("入仓单已不在待清点组托状态"); return task; }
private TaskRow requireTask(long taskId,long warehouseId){ TaskRow task=mapper.task(taskId,warehouseId); if(task==null) throw new BizException(ResultCode.NOT_FOUND,"未找到当前仓库的一件代发清点组托任务"); return task; }
private PalletRow requireOpenPallet(long taskId,long palletId,long warehouseId){ PalletRow p=mapper.pallet(taskId,palletId,warehouseId); if(p==null) throw new BizException(ResultCode.NOT_FOUND,"托盘不存在"); if(!"OPEN".equals(p.getPalletStatus())) throw conflict("托盘已封托,不能修改"); return p; }
private static String operation(String v){String n=normalize(v);if(!StringUtils.hasText(n))throw invalid("操作幂等号不能为空");return n;}
private static String normalize(String v){return StringUtils.hasText(v)?v.trim():null;}
private static BizException conflict(String m){return new BizException(ResultCode.CONFLICT,m);}
private static BizException invalid(String m){return new BizException(ResultCode.VALIDATION_FAILED,m);}
}
@@ -54,6 +54,10 @@ public class InboundCartonLabelService implements CartonLabelBindingResolver {
&& cargoes.stream().anyMatch(cargo -> cargo.skuNo() == null || cargo.skuNo().isBlank())) {
throw new BizException(ResultCode.VALIDATION_FAILED, "一件代发货物明细缺少SKU,不能生成箱唛");
}
if ("ONE_CLICK_DROPSHIP".equals(inbound.getFulfillmentType())
&& cargoes.stream().anyMatch(cargo -> cargo.unitsPerCarton() == null || cargo.unitsPerCarton() < 1)) {
throw new BizException(ResultCode.VALIDATION_FAILED, "一件代发货物明细缺少每箱SKU数量,不能生成箱唛");
}
existing.forEach(label -> cargoes.stream()
.filter(cargo -> java.util.Objects.equals(cargo.cargoId(), label.getInboundDetailId()))
.findFirst().ifPresent(cargo -> repository.initializeLegacySku(tenantId, inboundId,
@@ -211,6 +215,7 @@ public class InboundCartonLabelService implements CartonLabelBindingResolver {
label.setCargoItemNo(cargo.itemNo());
label.setCargoName(cargo.cargoName());
label.setSkuCode(cargo.skuNo());
label.setUnitsPerCarton(cargo.unitsPerCarton());
label.setDetailPackageCount(cargo.packageCount());
label.setInboundCode(inbound.getInboundCode());
label.setBoxCode(inbound.getInboundCode() + "#" + total + "-" + index);
@@ -267,6 +272,7 @@ public class InboundCartonLabelService implements CartonLabelBindingResolver {
String source = cargoes.stream().map(cargo -> String.join("|", String.valueOf(cargo.itemNo()),
cargo.cargoName() == null ? "" : cargo.cargoName().trim(),
cargo.skuNo() == null ? "" : cargo.skuNo().trim(), String.valueOf(cargo.packageCount()),
String.valueOf(cargo.unitsPerCarton()),
decimalOrEmpty(cargo.lengthCm()), decimalOrEmpty(cargo.widthCm()), decimalOrEmpty(cargo.heightCm())))
.collect(java.util.stream.Collectors.joining("\n"));
try {
@@ -4,6 +4,7 @@ import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.dto.LoadingPickDtos.PickItem;
import com.baiyue.firstleg.dto.LoadingPickDtos.PickRequest;
import com.baiyue.firstleg.dto.LoadingPickDtos.PalletPickRequest;
import com.baiyue.firstleg.dto.LoadingPickDtos.PartialPickRequest;
import com.baiyue.firstleg.dto.LoadingPickDtos.SkipRequest;
import com.baiyue.firstleg.dto.LoadingPickDtos.Task;
@@ -106,6 +107,8 @@ public class LoadingPickService {
private InboundPalletAllocationRepository palletAllocations;
@Resource
private InboundHoldGuard inboundHoldGuard;
@Resource
private com.baiyue.firstleg.mapper.DropshipPalletizationMapper dropshipPalletizationMapper;
/** Opens the field task after planning and ocean release; the real container number may be filled later. */
@Transactional
@@ -326,6 +329,7 @@ public class LoadingPickService {
if (item == null) {
throw new BizException(ResultCode.NOT_FOUND, "PDA拣货明细不存在");
}
requirePackageUnit(item);
if (repository.pick(itemId, request.pickedPackageCount(), request.version(), request.warehouseId()) != 1) {
throw conflict("拣货数量超过计划箱数或明细版本已变化");
}
@@ -344,6 +348,41 @@ public class LoadingPickService {
return view(requiredTask(taskId), request.warehouseId());
}
/** 一件代发只扫描已封托托盘码,并在同一事务把该托全部计划行结算为已拣。 */
@Transactional
public Task pickPallet(long taskId,PalletPickRequest request,long operatorId,String operatorName){
inboundHoldGuard.requireTaskNotHeld(taskId);
String operationId=operation(request.operationId());
String requestHash=hash("PICK_PALLET|"+taskId+'|'+request.warehouseId()+'|'+request.palletNo()
+'|'+request.expectedTaskVersion());
Task replay=replayTaskOperation(taskId,operationId,requestHash,operatorId); if(replay!=null)return replay;
LoadingPickTaskEntity task=requiredAssignedLoadingTaskForUpdate(taskId,operatorId);
replay=replayTaskOperationForUpdate(taskId,operationId,requestHash,operatorId); if(replay!=null)return replay;
if(!Objects.equals(task.getVersion(),request.expectedTaskVersion()))throw conflict("PDA任务版本已变化,请刷新后重试");
List<LoadingPlanItemEntity> planItems=smartLoadingRepository.items(task.getLoadingPlanId()).stream()
.filter(item->request.palletNo().equals(item.getPalletNo()) && item.getPalletId()!=null
&& Boolean.TRUE.equals(item.getIndivisible())).toList();
if(planItems.isEmpty())throw new BizException(ResultCode.NOT_FOUND,"托盘不属于当前装柜任务");
Long palletId=planItems.get(0).getPalletId();
if(planItems.stream().anyMatch(item->!Objects.equals(item.getPalletId(),palletId)))throw conflict("托盘装柜快照不一致");
var physical=dropshipPalletizationMapper.sealedPalletLines(palletId);
if(physical.isEmpty()||physical.stream().anyMatch(line->!"SEALED".equals(line.getPalletStatus())))throw conflict("托盘已失效或尚未封托");
for(LoadingPlanItemEntity planItem:planItems){
LoadingPickItemEntity pickItem=repository.itemByPlanItem(planItem.getId());
if(pickItem==null||!"PENDING".equals(pickItem.getItemStatus())||count(pickItem.getPickedPackageCount())!=0)
throw conflict("托盘已有部分作业或明细已变化,禁止拆托处理");
if(repository.pick(pickItem.getId(),pickItem.getPlannedPackageCount(),pickItem.getVersion(),request.warehouseId())!=1)
throw conflict("托盘明细版本已变化,请刷新后重试");
insertHistory(taskId,pickItem.getId(),"PAL-"+hash(operationId+'|'+pickItem.getId()).substring(0,48),
requestHash,"PICK_PALLET",pickItem.getPlannedPackageCount(),null,operatorId,operatorName);
}
startPickingIfNeeded(task,operatorId,operatorName);
if(repository.touchTask(taskId,request.expectedTaskVersion())!=1)throw conflict("PDA任务版本已变化,请刷新后重试");
insertOperation(taskId,operationId,requestHash,"PICK_PALLET",operatorId,operatorName);
settleAndFinish(task,operatorId,operatorName); repository.syncInboundProjections(taskId,operatorId,operatorName);
return view(requiredTask(taskId),request.warehouseId());
}
@Transactional
public Task partial(long taskId, long itemId, PartialPickRequest request,
long operatorId, String operatorName) {
@@ -364,7 +403,8 @@ public class LoadingPickService {
assertReplay(replay, taskId, itemId, requestHash);
return view(task, request.warehouseId());
}
requireWarehouseItem(taskId, itemId, request.warehouseId());
LoadingPickItemEntity item = requireWarehouseItem(taskId, itemId, request.warehouseId());
requirePackageUnit(item);
if (repository.partial(itemId, request.finalPickedPackageCount(), request.version(),
request.warehouseId()) != 1) {
throw conflict("部分拣货箱数必须大于已拣箱数且小于计划箱数,或明细版本已变化");
@@ -397,6 +437,7 @@ public class LoadingPickService {
return view(task, request.warehouseId());
}
LoadingPickItemEntity item = requireWarehouseItem(taskId, itemId, request.warehouseId());
requirePackageUnit(item);
if (repository.hasPickedOrPartialItem(taskId, item.getInboundId())) {
throw conflict("同一入库单已存在已拣或部分拣货明细,不能跳拣并恢复智能装柜候选");
}
@@ -443,6 +484,7 @@ public class LoadingPickService {
List<BatchLine> lines = validateBatchLines(task, request);
validateInventorySnapshot(task, request, lines);
lines.forEach(line -> requirePackageUnit(line.item()));
for (BatchLine line : lines) {
applyBatchLine(taskId, line, operationId, operatorId, operatorName);
}
@@ -1213,6 +1255,16 @@ public class LoadingPickService {
/**
* 逐项拣货后仅更新任务版本与投影,不能自动结算或推进装柜计划。
*/
private void requirePackageUnit(LoadingPickItemEntity item) {
if (item.getLoadingPlanItemId() == null) return;
LoadingPlanItemEntity planItem = smartLoadingRepository.item(
requiredTask(item.getPickTaskId()).getLoadingPlanId(), item.getLoadingPlanItemId());
if (planItem != null && (Boolean.TRUE.equals(planItem.getIndivisible())
|| planItem.getPalletId() != null)) {
throw conflict("一件代发已封托货物必须扫描托盘码整体拣货,不能按箱数处理");
}
}
private void touchSettleFinishAndSync(LoadingPickTaskEntity task, long operatorId, String operatorName) {
if (repository.touchTask(task.getId(), task.getVersion()) != 1) {
throw conflict("PDA任务版本已变化");
@@ -1,210 +0,0 @@
package com.baiyue.firstleg.service;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.dto.LoadingPalletDtos.Carton;
import com.baiyue.firstleg.dto.LoadingPalletDtos.OpenRequest;
import com.baiyue.firstleg.dto.LoadingPalletDtos.Pallet;
import com.baiyue.firstleg.dto.LoadingPalletDtos.ScanRequest;
import com.baiyue.firstleg.dto.LoadingPalletDtos.SealRequest;
import com.baiyue.firstleg.dto.LoadingPalletDtos.SkuSummary;
import com.baiyue.firstleg.entity.LoadingWorkPalletCartonEntity;
import com.baiyue.firstleg.entity.LoadingWorkPalletEntity;
import com.baiyue.firstleg.mapper.LoadingWorkPalletCartonMapper;
import com.baiyue.firstleg.mapper.LoadingWorkPalletCartonMapper.CartonCandidate;
import com.baiyue.firstleg.mapper.LoadingWorkPalletMapper;
import com.baiyue.firstleg.mapper.LoadingWorkPalletMapper.TaskContext;
import jakarta.annotation.Resource;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/** 一件代发现场打托旁路;不改变既有拣货数量和仓库资源结算。 */
@Service
public class LoadingWorkPalletService {
@Resource private LoadingWorkPalletMapper pallets;
@Resource private LoadingWorkPalletCartonMapper cartons;
@Resource private InboundHoldGuard inboundHoldGuard;
@Transactional
public Pallet open(long taskId, OpenRequest request, long operatorId, String operatorName) {
inboundHoldGuard.requireTaskNotHeld(taskId);
TaskContext context = requireWorkTask(taskId, request.warehouseId(), operatorId);
String code = request.palletCode().trim();
String operationId = request.operationId().trim();
LoadingWorkPalletEntity replay = pallets.selectByOpenOperation(operationId);
if (replay != null) {
if (replay.getPickTaskId() != taskId || !replay.getPalletCode().equals(code)) {
throw conflict("开托幂等号已用于其他请求");
}
return view(replay);
}
LoadingWorkPalletEntity value = new LoadingWorkPalletEntity();
value.setPickTaskId(taskId);
value.setLoadingPlanId(context.loadingPlanId());
value.setWarehouseId(request.warehouseId());
value.setWarehousePalletId(request.warehousePalletId());
value.setPalletCode(code);
value.setOpenOperationId(operationId);
value.setPalletStatus("OPEN");
value.setCreatedBy(operatorId);
value.setCreatedByName(operatorName);
value.setVersion(0);
try {
pallets.insert(value);
} catch (DuplicateKeyException exception) {
replay = pallets.selectByOpenOperation(operationId);
if (replay == null || replay.getPickTaskId() != taskId || !replay.getPalletCode().equals(code)) {
throw conflict("托盘号已存在或开托操作重复");
}
value = replay;
}
return view(value);
}
@Transactional
public Pallet scan(long taskId, long palletId, ScanRequest request,
long operatorId, String operatorName) {
inboundHoldGuard.requireTaskNotHeld(taskId);
requireWorkTask(taskId, request.warehouseId(), operatorId);
LoadingWorkPalletEntity target = requireOpenPallet(taskId, palletId, request.warehouseId());
String operationId = request.operationId().trim();
LoadingWorkPalletCartonEntity replay = cartons.selectByOperation(operationId);
if (replay != null) {
if (replay.getPickTaskId() != taskId || replay.getWorkPalletId() != palletId
|| !replay.getBoxCode().equals(request.boxCode().trim())) {
throw conflict("箱唛操作幂等号已用于其他请求");
}
return view(target);
}
List<CartonCandidate> candidates = cartons.selectCandidates(taskId, request.warehouseId(),
request.boxCode().trim());
if (candidates.isEmpty()) throw new BizException(ResultCode.NOT_FOUND, "箱唛不属于当前任务的一件代发货物");
if (candidates.size() > 1) throw conflict("箱唛在当前多客户任务中不唯一,请检查箱唛数据");
CartonCandidate candidate = candidates.get(0);
if (candidate.getSkuCode() == null || candidate.getSkuCode().isBlank()) {
throw conflict("一件代发箱唛缺少SKU,不能上托");
}
LoadingWorkPalletCartonEntity current = cartons.selectBinding(taskId, candidate.getCartonLabelId());
if (current == null) {
current = binding(taskId, palletId, candidate, operationId, operatorId, operatorName);
try {
cartons.insert(current);
} catch (DuplicateKeyException exception) {
LoadingWorkPalletCartonEntity concurrent = cartons.selectBinding(taskId, candidate.getCartonLabelId());
if (concurrent == null || concurrent.getWorkPalletId() != palletId) {
throw conflict("箱唛已被其他托盘绑定");
}
}
return view(target);
}
if (current.getWorkPalletId() == palletId) return view(target);
LoadingWorkPalletEntity source = pallets.selectScoped(taskId, current.getWorkPalletId(), request.warehouseId());
if (source == null || !"OPEN".equals(source.getPalletStatus())) {
throw conflict("来源托盘已封托,不能直接移箱");
}
if (cartons.move(current.getId(), current.getVersion(), palletId, operatorId, operatorName, operationId) != 1) {
throw conflict("箱唛绑定已变化,请刷新后重试");
}
return view(target);
}
@Transactional
public Pallet seal(long taskId, long palletId, SealRequest request,
long operatorId, String operatorName) {
inboundHoldGuard.requireTaskNotHeld(taskId);
requireWorkTask(taskId, request.warehouseId(), operatorId);
LoadingWorkPalletEntity replay = pallets.selectBySealOperation(request.operationId().trim());
if (replay != null) {
if (replay.getId() != palletId || replay.getPickTaskId() != taskId) {
throw conflict("封托幂等号已用于其他请求");
}
return view(replay);
}
requireOpenPallet(taskId, palletId, request.warehouseId());
if (cartons.countPalletCartons(palletId) == 0) throw conflict("空托盘不能封托");
if (cartons.countUnreconciled(taskId) > 0) {
throw conflict("现场上托箱数超过已提交拣货箱数,请先完成原PDA拣货提交");
}
if (pallets.seal(taskId, palletId, request.warehouseId(), request.expectedVersion(),
request.operationId().trim(), operatorId, operatorName) != 1) {
throw conflict("托盘状态或版本已变化,请刷新后重试");
}
return view(pallets.selectScoped(taskId, palletId, request.warehouseId()));
}
@Transactional(readOnly = true)
public List<Pallet> list(long taskId, long warehouseId) {
TaskContext context = pallets.selectTaskContext(taskId, warehouseId);
if (context == null) throw new BizException(ResultCode.NOT_FOUND, "未找到当前仓库的PDA任务");
return pallets.selectTaskPallets(taskId, warehouseId).stream().map(this::view).toList();
}
private TaskContext requireWorkTask(long taskId, long warehouseId, long operatorId) {
TaskContext context = pallets.selectTaskContext(taskId, warehouseId);
if (context == null) throw new BizException(ResultCode.NOT_FOUND, "未找到当前仓库的PDA任务");
if (!"LOADING".equals(context.taskStatus())) throw conflict("PDA任务已完成,不能继续打托");
if (context.dropshipItemCount() == null || context.dropshipItemCount() == 0) {
throw conflict("当前任务没有一件代发货物");
}
if (context.assigneeId() == null || context.assigneeId() != operatorId) {
throw conflict("请先领取PDA任务后再执行打托");
}
return context;
}
private LoadingWorkPalletEntity requireOpenPallet(long taskId, long palletId, long warehouseId) {
LoadingWorkPalletEntity pallet = pallets.selectScoped(taskId, palletId, warehouseId);
if (pallet == null) throw new BizException(ResultCode.NOT_FOUND, "现场托盘不存在");
if (!"OPEN".equals(pallet.getPalletStatus())) throw conflict("托盘不是打托中状态");
return pallet;
}
private static LoadingWorkPalletCartonEntity binding(long taskId, long palletId,
CartonCandidate source, String operationId,
long operatorId, String operatorName) {
LoadingWorkPalletCartonEntity value = new LoadingWorkPalletCartonEntity();
value.setPickTaskId(taskId);
value.setWorkPalletId(palletId);
value.setLoadingPlanItemId(source.getLoadingPlanItemId());
value.setPickItemId(source.getPickItemId());
value.setTenantId(source.getTenantId());
value.setInboundId(source.getInboundId());
value.setInboundDetailId(source.getInboundDetailId());
value.setCartonLabelId(source.getCartonLabelId());
value.setBoxCode(source.getBoxCode());
value.setSkuCode(source.getSkuCode());
value.setUnitsPerCarton(source.getUnitsPerCarton());
value.setBoundBy(operatorId);
value.setBoundByName(operatorName);
value.setOperationId(operationId);
value.setVersion(0);
return value;
}
private Pallet view(LoadingWorkPalletEntity pallet) {
List<LoadingWorkPalletCartonEntity> values = cartons.selectPalletCartons(pallet.getId());
Map<String, List<LoadingWorkPalletCartonEntity>> bySku = values.stream().collect(
java.util.stream.Collectors.groupingBy(LoadingWorkPalletCartonEntity::getSkuCode,
LinkedHashMap::new, java.util.stream.Collectors.toList()));
List<SkuSummary> skus = bySku.entrySet().stream().map(entry -> new SkuSummary(entry.getKey(),
entry.getValue().size(), entry.getValue().stream().map(LoadingWorkPalletCartonEntity::getUnitsPerCarton)
.filter(Objects::nonNull).mapToLong(Integer::longValue).sum())).toList();
List<Carton> cartonViews = values.stream().map(value -> new Carton(value.getId(), value.getBoxCode(),
value.getSkuCode(), value.getTenantId(), value.getInboundId(), value.getInboundDetailId(),
value.getUnitsPerCarton())).toList();
return new Pallet(pallet.getId(), pallet.getPalletCode(), pallet.getWarehousePalletId(),
pallet.getPalletStatus(), pallet.getVersion(), values.size(), bySku.size(),
(int) values.stream().map(LoadingWorkPalletCartonEntity::getTenantId).distinct().count(),
skus, cartonViews);
}
private static BizException conflict(String message) {
return new BizException(ResultCode.CONFLICT, message);
}
}
@@ -23,6 +23,7 @@ import java.math.BigDecimal;
@Service
public class MerchantInboundBillingService {
private static final Set<String> PUBLISHED_BILLING_PROCESS_STATUSES = Set.of(
InboundProcessStatus.WAIT_PALLETIZE,
InboundProcessStatus.WAIT_SPLIT,
InboundProcessStatus.LOADING,
InboundProcessStatus.LOADED,
@@ -54,6 +54,7 @@ public class SmartLoadingService {
"OA_NEW_YORK_TRUCK", Set.of("OA_NEW_YORK_TRUCK")
);
@Resource private SmartLoadingRepository repository;
@Resource private com.baiyue.firstleg.mapper.DropshipPalletizationMapper dropshipPallets;
@Resource private OrderCargoClient orderCargoClient;
@Resource private PendingReviewOrderClient pendingReviewOrders;
@Resource private InboundOrderCreationService inboundOrderCreationService;
@@ -111,7 +112,7 @@ public class SmartLoadingService {
Map<Long, List<Candidate>> grouped = candidates(bookingId, warehouseId).stream()
.collect(java.util.stream.Collectors.groupingBy(Candidate::inboundId, LinkedHashMap::new,
java.util.stream.Collectors.toList()));
return grouped.values().stream().map(this::inboundCandidate).toList();
return grouped.values().stream().flatMap(details -> inboundCandidates(details).stream()).toList();
}
private List<InboundCandidate> recommendationCandidates(ContainerBookingEntity booking) {
@@ -121,7 +122,7 @@ public class SmartLoadingService {
repository.planByBooking(booking.getId()), values).stream()
.collect(java.util.stream.Collectors.groupingBy(Candidate::inboundId, LinkedHashMap::new,
java.util.stream.Collectors.toList()));
return grouped.values().stream().map(this::inboundCandidate).toList();
return grouped.values().stream().flatMap(details -> inboundCandidates(details).stream()).toList();
}
/** Global workbench candidates. A co-load group is one paging unit and can never be split across pages. */
@@ -175,7 +176,7 @@ public class SmartLoadingService {
List<InboundCandidate> all = repository.globalCandidatesByUnits(unitPage.records()).stream()
.collect(java.util.stream.Collectors.groupingBy(Candidate::inboundId, LinkedHashMap::new,
java.util.stream.Collectors.toList()))
.values().stream().map(this::inboundCandidate).toList();
.values().stream().flatMap(details -> inboundCandidates(details).stream()).toList();
Map<String, InboundCandidate> units = new HashMap<>();
Map<Long, List<InboundCandidate>> grouped = all.stream()
.filter(candidate -> candidate.coLoadGroupId() != null)
@@ -216,6 +217,7 @@ public class SmartLoadingService {
new InboundCandidate(row.customerId(), null, row.orderId(), row.orderNo(), row.inboundCode(),
row.warehouseId(), row.channelCode(), row.channelName(), null, null, null,
row.customerName(), null, row.totalPackageCount(), null, null, null, null,
"PACKAGE", null, null, false,
null, null, null, null, false, true, null,
"PENDING_REVIEW", row.expectedArrivalAt(), List.of())).toList();
return new PageResult<>(records, source.page(), source.size(), source.total(), source.pages());
@@ -266,7 +268,7 @@ public class SmartLoadingService {
return repository.workbenchContainers(query, actualChannels(query.channelGroupCodes()));
}
private InboundCandidate inboundCandidate(List<Candidate> details) {
private List<InboundCandidate> inboundCandidates(List<Candidate> details) {
Candidate first = details.get(0);
int packages = details.stream().map(Candidate::availablePackageCount)
.filter(Objects::nonNull).mapToInt(Integer::intValue).sum();
@@ -279,13 +281,23 @@ public class SmartLoadingService {
BigDecimal.valueOf(packages), 6, RoundingMode.HALF_UP);
String goods = details.stream().map(Candidate::goodsName).filter(StringUtils::hasText)
.map(String::trim).distinct().collect(java.util.stream.Collectors.joining("、"));
return new InboundCandidate(first.accountId(), first.inboundId(), first.orderId(), first.orderNo(),
List<PalletCandidate> pallets=dropshipPallets.palletCandidates(first.inboundId());
if(!pallets.isEmpty()) return pallets.stream().map(pallet -> new InboundCandidate(first.accountId(), first.inboundId(), first.orderId(), first.orderNo(),
first.inboundCode(), first.warehouseId(), first.channelCode(), first.channelName(),
first.destinationWarehouseId(), first.destinationWarehouseCode(), first.destinationWarehouseName(),
first.customerName(), goods, pallet.cartonCount(),
pallet.cartonCount()==null||pallet.cartonCount()==0?null:pallet.weightKg().divide(BigDecimal.valueOf(pallet.cartonCount()),6,RoundingMode.HALF_UP),
first.unitNetWeightKg(), pallet.cartonCount()==null||pallet.cartonCount()==0?null:pallet.volumeCbm().divide(BigDecimal.valueOf(pallet.cartonCount()),6,RoundingMode.HALF_UP),
pallet.volumeCbm(),"SEALED_PALLET",pallet.palletId(),pallet.palletNo(),true,
first.coLoadGroupId(), first.groupNo(), first.memberCount(),first.groupTotalBoxes(), first.locked(), canAssign(first), capabilityBlock(first),
first.orderStatusSnapshot(),first.expectedArrivalAt(),List.of())).toList();
return List.of(new InboundCandidate(first.accountId(), first.inboundId(), first.orderId(), first.orderNo(),
first.inboundCode(), first.warehouseId(), first.channelCode(), first.channelName(),
first.destinationWarehouseId(), first.destinationWarehouseCode(), first.destinationWarehouseName(),
first.customerName(), goods, packages, first.unitGrossWeightKg(), first.unitNetWeightKg(),
averageVolume, volume, first.coLoadGroupId(), first.groupNo(), first.memberCount(),
averageVolume, volume,"PACKAGE",null,null,false, first.coLoadGroupId(), first.groupNo(), first.memberCount(),
first.groupTotalBoxes(), first.locked(), canAssign(first), capabilityBlock(first), first.orderStatusSnapshot(),
first.expectedArrivalAt(), List.of());
first.expectedArrivalAt(), List.of()));
}
private InboundCandidate globalGroupCandidate(List<InboundCandidate> availableMembers) {
@@ -317,7 +329,7 @@ public class SmartLoadingService {
return new InboundCandidate(first.accountId(), first.inboundId(), first.orderId(), first.orderNo(),
first.inboundCode(), first.warehouseId(), first.channelCode(), first.channelName(),
first.destinationWarehouseId(), first.destinationWarehouseCode(), first.destinationWarehouseName(),
customers, goods, packages, gross, net, averageVolume, volume, metadata.coLoadGroupId(), metadata.groupNo(),
customers, goods, packages, gross, net, averageVolume, volume,"PACKAGE",null,null,false, metadata.coLoadGroupId(), metadata.groupNo(),
metadata.memberCount(), metadata.groupTotalBoxes(),
occupied,
ordered.stream().allMatch(candidate -> Boolean.TRUE.equals(candidate.canAssign())),
@@ -418,11 +430,52 @@ public class SmartLoadingService {
@BusinessAudit(event = "SMART_LOADING_ITEM_ASSIGN", resourceType = "LOADING_PLAN",
resourceId = "#planId", description = "分配智能装柜明细")
public Plan assign(long planId, AssignRequest request, long operatorId, String operatorName) {
return assign(planId, request, operatorId, operatorName, false);
return assign(planId, request, operatorId, operatorName, false, null, null, null, false, null);
}
@Transactional
@BusinessAudit(event = "SMART_LOADING_PALLET_ASSIGN", resourceType = "LOADING_PLAN",
resourceId = "#planId", description = "按已封托托盘分配智能装柜明细")
public Plan assignPallet(long planId, AssignPalletRequest request, long operatorId, String operatorName) {
String operation = operation(request.operationId());
String requestHash = sha256("ASSIGN_PALLET|" + planId + '|' + request.palletId() + '|'
+ request.expectedVersion());
Plan replay = replay(operation, planId, "ADD_PALLET", null, requestHash);
if (replay != null) return replay;
LoadingPlanEntity original = editing(planId, request.expectedVersion(), operatorId, operatorName);
List<com.baiyue.firstleg.mapper.DropshipPalletizationMapper.PalletLine> lines =
dropshipPallets.sealedPalletLines(request.palletId());
if (lines.isEmpty() || lines.stream().anyMatch(line -> !"SEALED".equals(line.getPalletStatus()))) {
throw conflict("托盘不存在或尚未封托");
}
var first = lines.get(0);
inboundHoldGuard.requireInboundNotHeld(first.getAccountId(), first.getInboundId());
if (!repository.activeItemsByPallet(request.palletId()).isEmpty()) throw conflict("托盘已被有效柜次占用");
int sequence = 0;
for (var line : lines) {
LoadingPlanEntity current = repository.plan(planId);
Candidate candidate = new Candidate(line.getAccountId(), line.getInboundId(), line.getInboundDetailId(),
line.getOrderId(), line.getOrderNo(), line.getInboundCode(), line.getWarehouseId(), line.getChannelCode(),
line.getChannelName(), line.getDestinationWarehouseId(), line.getDestinationWarehouseCode(),
line.getDestinationWarehouseName(), line.getCustomerName(), line.getCargoName(), line.getCartonCount(),
line.getUnitGrossWeightKg(), line.getUnitNetWeightKg(), line.getUnitVolumeCbm(), null, null, null, null,
false, null, "WAIT_SPLIT", null);
String childOperation = "PAL-" + sha256(operation + '|' + line.getInboundDetailId()).substring(0, 48);
assign(planId, new AssignRequest(line.getInboundId(), line.getInboundDetailId(), line.getCartonCount(),
current.getVersion(), childOperation), operatorId, operatorName, true, request.palletId(),
line.getPalletNo(), line.getActualSkuQuantity(), sequence++ == 0, candidate);
}
LoadingPlanEntity assigned = repository.plan(planId);
history(planId, null, "ADD_PALLET", original.getPlanStatus(), assigned.getPlanStatus(), operation,
json(values("palletId", request.palletId(), "version", request.expectedVersion())),
json(values("requestHash", requestHash, "palletNo", first.getPalletNo(), "lineCount", lines.size())),
operatorId, operatorName);
return view(assigned, requireBooking(assigned.getContainerBookingId()));
}
private Plan assign(long planId, AssignRequest request, long operatorId, String operatorName,
boolean wholeGroupAssignment) {
boolean wholeGroupAssignment, Long palletId, String palletNo,
Integer actualSkuQuantity, boolean primaryPalletLine, Candidate frozenCandidate) {
String operation = operation(request.operationId());
String requestHash = sha256("ASSIGN|" + planId + '|'
+ request.inboundId() + '|' + request.inboundDetailId() + '|' + request.packageCount()
@@ -431,8 +484,14 @@ public class SmartLoadingService {
if (replay != null) return replay;
LoadingPlanEntity plan = editing(planId, request.expectedVersion(), operatorId, operatorName);
ContainerBookingEntity booking = requireBooking(plan.getContainerBookingId());
Candidate candidate = repository.candidate(planId, request.inboundId(), request.inboundDetailId());
Candidate candidate = frozenCandidate == null
? repository.candidate(planId, request.inboundId(), request.inboundDetailId()) : frozenCandidate;
if (candidate == null) throw conflict("入库货物不存在或尚未完成入库");
InboundOrderEntity candidateInbound = repository.inbound(candidate.accountId(), candidate.inboundId());
if (!wholeGroupAssignment && candidateInbound != null
&& "ONE_CLICK_DROPSHIP".equals(candidateInbound.getFulfillmentType())) {
throw conflict("一件代发必须按已封托托盘整体加入,不能按箱数拆分");
}
inboundHoldGuard.requireInboundNotHeld(candidate.accountId(), candidate.inboundId());
if (!wholeGroupAssignment) requireStandalone(candidate.accountId(), candidate.inboundId());
requireSameCustomerScope(booking, candidate);
@@ -445,7 +504,9 @@ public class SmartLoadingService {
operatorId, operatorName);
}
OrderCargoClient.CargoBinding cargo = loadCargo(candidate);
LoadingPlanItemEntity existing = repository.activeItem(planId, candidate.accountId(), request.inboundDetailId());
LoadingPlanItemEntity existing = palletId == null
? repository.activeItem(planId, candidate.accountId(), request.inboundDetailId())
: repository.activePalletItem(planId, palletId, request.inboundDetailId());
int total = request.packageCount() + (existing == null ? 0 : existing.getPackageCount());
int availableIncludingCurrent = candidate.availablePackageCount()
+ (existing == null ? 0 : existing.getPackageCount());
@@ -457,6 +518,14 @@ public class SmartLoadingService {
LoadingPlanItemEntity changed;
if (existing == null) {
LoadingPlanItemEntity item = snapshot(planId, candidate, cargo, request.packageCount(), operatorId, operatorName, lines.size() + 1);
if (palletId != null) {
item.setUnitType("SEALED_PALLET");
item.setPalletId(palletId);
item.setPalletNo(palletNo);
item.setIndivisible(true);
item.setPalletCount(primaryPalletLine ? 1 : 0);
item.setProductQuantity(actualSkuQuantity);
}
try { repository.insert(item); } catch (DuplicateKeyException e) { throw conflict("货物明细已被并发锁定"); }
changed = item;
} else {
@@ -617,7 +686,7 @@ public class SmartLoadingService {
.substring(0, 48);
assign(planId, new AssignRequest(request.inboundId(), allocation.candidate().inboundDetailId(),
allocation.packageCount(), current.getVersion(), childOperation), operatorId, operatorName,
wholeGroupAssignment);
wholeGroupAssignment, null, null, null, false, null);
sequence++;
}
LoadingPlanEntity assigned = repository.plan(planId);
@@ -859,6 +928,9 @@ public class SmartLoadingService {
LoadingPlanEntity plan = editing(planId, request.expectedPlanVersion(), operatorId, operatorName);
LoadingPlanItemEntity item = repository.item(planId, itemId);
if (item == null) throw new BizException(ResultCode.NOT_FOUND, "装柜明细不存在");
if (Boolean.TRUE.equals(item.getIndivisible()) || item.getPalletId() != null) {
throw conflict("已封托托盘不可调整箱数,只能整托移除");
}
inboundHoldGuard.requireInboundNotHeld(item.getAccountId(), item.getInboundId());
requireStandalone(item.getAccountId(), item.getInboundId());
if (!Objects.equals(item.getVersion(), request.expectedItemVersion())) throw conflict("装柜明细版本冲突");
@@ -971,6 +1043,9 @@ public class SmartLoadingService {
LoadingPlanEntity plan = editing(planId, request.expectedPlanVersion(), operatorId, operatorName);
LoadingPlanItemEntity item = repository.item(planId, itemId);
if (item == null) throw new BizException(ResultCode.NOT_FOUND, "装柜明细不存在");
if (Boolean.TRUE.equals(item.getIndivisible()) || item.getPalletId() != null) {
throw conflict("已封托托盘不可删除单条明细,只能整托移除");
}
requireStandalone(item.getAccountId(), item.getInboundId());
loadingPickService.remove(itemId);
touch(plan, operatorId, operatorName);
@@ -982,6 +1057,34 @@ public class SmartLoadingService {
return view(repository.plan(planId), requireBooking(plan.getContainerBookingId()));
}
@Transactional
@BusinessAudit(event = "SMART_LOADING_PALLET_REMOVE", resourceType = "LOADING_PLAN",
resourceId = "#planId", description = "整托移除一件代发装柜明细")
public Plan removePallet(long planId,long palletId,RemovePalletRequest request,long operatorId,String operatorName){
String operation=operation(request.operationId());
String requestHash=sha256("REMOVE_PALLET|"+planId+'|'+palletId+'|'+request.expectedPlanVersion());
Plan replay=replay(operation,planId,"REMOVE_PALLET",null,requestHash); if(replay!=null)return replay;
LoadingPlanEntity plan=editing(planId,request.expectedPlanVersion(),operatorId,operatorName);
List<LoadingPlanItemEntity> palletItems=repository.activeItemsByPallet(palletId).stream()
.filter(item->Objects.equals(item.getLoadingPlanId(),planId)).toList();
if(palletItems.isEmpty())throw new BizException(ResultCode.NOT_FOUND,"装柜计划中不存在该托盘");
LoadingPickService.MutationSnapshot picks=loadingPickService.mutationSnapshot(planId);
if(palletItems.stream().anyMatch(item->picks.pickedByPlanItem().getOrDefault(item.getId(),0)>0))
throw conflict("托盘已发生实物拣货,不能移除");
for(LoadingPlanItemEntity item:palletItems)loadingPickService.remove(item.getId());
touch(plan,operatorId,operatorName);
for(LoadingPlanItemEntity item:palletItems){
if(repository.remove(planId,item.getId(),item.getVersion(),operatorId,operatorName)!=1)
throw conflict("整托装柜明细已被并发修改");
}
LoadingPlanItemEntity first=palletItems.get(0);
loadingModelBridge.removeIfUnallocated(planId,first.getAccountId(),first.getInboundId());
history(planId,null,"REMOVE_PALLET",plan.getPlanStatus(),plan.getPlanStatus(),operation,
json(values("palletId",palletId,"version",request.expectedPlanVersion())),
json(values("requestHash",requestHash,"removedLineCount",palletItems.size())),operatorId,operatorName);
return view(repository.plan(planId),requireBooking(plan.getContainerBookingId()));
}
@Transactional
@BusinessAudit(event = "SMART_LOADING_UNPICKED_REMOVE", resourceType = "LOADING_PLAN",
resourceId = "#planId", description = "批量移除智能装柜未拣明细")
@@ -1461,7 +1564,40 @@ public class SmartLoadingService {
: "COMPLETED".equals(task.taskStatus()) ? "COMPLETED" : "IN_PROGRESS";
return new Plan(p.getId(),p.getPlanNo(),p.getPlanStatus(),p.getContainerBookingId(),
b.getContainerNo(),b.getVoyageNo(),p.getVersion(),mutable,mutable,blockedReason,finalStatus,
task.taskId(),task.taskStatus(),task.taskVersion(),planCoLoadGroups(p.getId()),items);
task.taskId(),task.taskStatus(),task.taskVersion(),planCoLoadGroups(p.getId()),collapsePalletUnits(items));
}
private List<LoadingPlanItemEntity> collapsePalletUnits(List<LoadingPlanItemEntity> items) {
List<LoadingPlanItemEntity> result = new ArrayList<>();
Map<Long,List<LoadingPlanItemEntity>> pallets = items.stream().filter(i -> i.getPalletId() != null)
.collect(java.util.stream.Collectors.groupingBy(LoadingPlanItemEntity::getPalletId,
LinkedHashMap::new, java.util.stream.Collectors.toList()));
items.stream().filter(i -> i.getPalletId() == null).forEach(result::add);
for (List<LoadingPlanItemEntity> lines : pallets.values()) {
LoadingPlanItemEntity unit = lines.get(0);
unit.setPackageCount(lines.stream().map(LoadingPlanItemEntity::getPackageCount)
.filter(Objects::nonNull).mapToInt(Integer::intValue).sum());
unit.setProductQuantity(lines.stream().map(LoadingPlanItemEntity::getProductQuantity)
.filter(Objects::nonNull).mapToInt(Integer::intValue).sum());
unit.setPalletCount(1);
unit.setGrossWeightKg(sumDecimal(lines, LoadingPlanItemEntity::getGrossWeightKg));
unit.setNetWeightKg(sumDecimal(lines, LoadingPlanItemEntity::getNetWeightKg));
unit.setVolumeCbm(sumDecimal(lines, LoadingPlanItemEntity::getVolumeCbm));
unit.setPickedPackageCount(lines.stream().map(LoadingPlanItemEntity::getPickedPackageCount)
.filter(Objects::nonNull).mapToInt(Integer::intValue).sum());
unit.setTotalPackageCount(unit.getPackageCount());
unit.setRemainingPackageCount(0);
unit.setCanAdjust(false);
unit.setCanRemove(lines.stream().allMatch(i -> Boolean.TRUE.equals(i.getCanRemove())));
result.add(unit);
}
return result.stream().sorted(Comparator
.comparing(LoadingPlanItemEntity::getLoadSequence, Comparator.nullsLast(Integer::compareTo))
.thenComparing(LoadingPlanItemEntity::getId, Comparator.nullsLast(Long::compareTo)))
.toList();
}
private static BigDecimal sumDecimal(List<LoadingPlanItemEntity> lines,
java.util.function.Function<LoadingPlanItemEntity,BigDecimal> getter) {
return lines.stream().map(getter).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
}
private int adjustmentTotal(LoadingPlanItemEntity item) {
int current = item.getPackageCount() == null ? 0 : item.getPackageCount();