Merge remote-tracking branch 'origin/test' into dev
# Conflicts: # order/AGENTS.md
This commit is contained in:
@@ -23,6 +23,7 @@ public interface PendingReviewOrderClient {
|
||||
@RequestParam(required = false) String inboundCode,
|
||||
@RequestParam(required = false) String createdStart,
|
||||
@RequestParam(required = false) String createdEnd,
|
||||
@RequestParam(required = false) List<Long> excludedOrderIds,
|
||||
@RequestParam long page,
|
||||
@RequestParam long size);
|
||||
|
||||
|
||||
+24
-64
@@ -30,35 +30,21 @@ public interface WarehousePalletLocationClient {
|
||||
@org.springframework.web.bind.annotation.PathVariable("palletId") long palletId,
|
||||
@RequestParam("warehouseId") long warehouseId);
|
||||
|
||||
@PostMapping("/pallet-locations/reservations")
|
||||
ApiResponse<BatchResult> reserve(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestBody ReserveRequest request);
|
||||
@PostMapping("/pallet-locations/blocks/reservations")
|
||||
ApiResponse<BlockResult> reserveBlock(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestBody BlockReserveRequest request);
|
||||
|
||||
@PostMapping("/pallet-locations/confirm")
|
||||
ApiResponse<BatchResult> confirm(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestBody ConfirmRequest request);
|
||||
@PostMapping("/pallet-locations/blocks/confirm")
|
||||
ApiResponse<BlockResult> confirmBlock(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestBody BlockConfirmRequest request);
|
||||
|
||||
@PostMapping("/pallet-locations/release")
|
||||
ApiResponse<List<ActiveResource>> release(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestBody ReleaseRequest request);
|
||||
@PostMapping("/pallet-locations/blocks/transfer")
|
||||
ApiResponse<BlockResult> transferBlock(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestBody BlockTransferRequest request);
|
||||
|
||||
@PostMapping("/pallet-locations/release-selected")
|
||||
ApiResponse<List<SelectiveReleaseResult>> releaseSelected(
|
||||
@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestBody SelectiveReleaseRequest request);
|
||||
|
||||
@PostMapping("/pallet-locations/transfer")
|
||||
ApiResponse<List<TransferResult>> transfer(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestBody TransferRequest request);
|
||||
|
||||
@PostMapping("/pallet-locations/settlements")
|
||||
ApiResponse<SettlementResult> settle(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestBody SettlementRequest request);
|
||||
|
||||
@GetMapping("/pallet-locations/settlements/target")
|
||||
ApiResponse<Long> settlementTarget(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestParam("sourceBusinessId") long sourceBusinessId,
|
||||
@RequestParam("idempotencyKey") String idempotencyKey);
|
||||
@PostMapping("/pallet-locations/blocks/release")
|
||||
ApiResponse<BlockResult> releaseBlock(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestBody BlockReleaseRequest request);
|
||||
|
||||
@GetMapping("/pallet-locations/occupied-resources")
|
||||
ApiResponse<List<OccupiedResource>> occupiedResources(
|
||||
@@ -67,44 +53,18 @@ public interface WarehousePalletLocationClient {
|
||||
@RequestParam("sourceService") String sourceService,
|
||||
@RequestParam("businessId") long businessId);
|
||||
|
||||
record ReserveRequest(long tenantId, long warehouseId, String sourceService, long businessId,
|
||||
String inboundCode, String idempotencyKey, long operatorId,
|
||||
BigDecimal totalVolumeCbm, List<PalletReference> pallets,
|
||||
LocalDateTime expiresAt, boolean measurementCompleted) {}
|
||||
record PalletReference(long weightResultId, long palletId) {}
|
||||
record ConfirmRequest(long tenantId, long warehouseId, String sourceService, long businessId,
|
||||
long operatorId, BigDecimal totalVolumeCbm, int packageCount) {}
|
||||
record ReleaseRequest(long tenantId, String sourceService, long businessId, long operatorId,
|
||||
String reason) {}
|
||||
record SelectiveReleaseRequest(long tenantId, String sourceService, long businessId,
|
||||
String idempotencyKey, long operatorId, String reason,
|
||||
List<Long> locationIds) {}
|
||||
record AllocationResult(long reservationId, String reservationNo, int allocationNo,
|
||||
long palletId, String palletCode, BigDecimal palletCapacityCbm,
|
||||
long locationId, String locationCode,
|
||||
BigDecimal allocatedVolumeCbm, String state) {}
|
||||
record BatchResult(int palletCount, List<AllocationResult> allocations) {}
|
||||
record ActiveResource(long reservationId, long warehouseId, long palletId, String palletCode,
|
||||
long locationId, String locationCode, String state) {}
|
||||
record SelectiveReleaseResult(long reservationId, long occupancyId, int allocationNo,
|
||||
long palletId, long locationId, String locationCode,
|
||||
int allocatedPackageCount) {}
|
||||
record TransferItem(long locationId, int remainingPackageCount) {}
|
||||
record TransferRequest(long tenantId, String sourceService, long sourceBusinessId,
|
||||
long targetBusinessId, String targetInboundCode, String idempotencyKey,
|
||||
long operatorId, List<TransferItem> items) {}
|
||||
record TransferResult(long reservationId, String reservationNo, long occupancyId, int allocationNo,
|
||||
long warehouseResourceTenantId, long warehouseId, long palletId,
|
||||
String palletCode, BigDecimal palletCapacityCbm, long locationId,
|
||||
String locationCode, BigDecimal allocatedVolumeCbm,
|
||||
int remainingPackageCount, String state) {}
|
||||
record SettlementTransferItem(long reservationId, int remainingPackageCount) {}
|
||||
record SettlementRequest(long warehouseId, String sourceService, long sourceBusinessId,
|
||||
long targetBusinessId, int sourcePackageCount, int targetPackageCount,
|
||||
String targetInboundCode, String idempotencyKey, long operatorId,
|
||||
String operatorName, String requestId, List<Long> releasedReservationIds,
|
||||
List<SettlementTransferItem> remainingItems) {}
|
||||
record SettlementResult(List<SelectiveReleaseResult> released, List<TransferResult> transferred) {}
|
||||
record BlockReserveRequest(long warehouseId, String sourceService, long businessId,
|
||||
String inboundCode, int requiredSlotCount, String idempotencyKey,
|
||||
long operatorId, LocalDateTime expiresAt) {}
|
||||
record BlockConfirmRequest(long blockId, long businessId, String idempotencyKey,
|
||||
long operatorId) {}
|
||||
record BlockTransferRequest(long blockId, long sourceBusinessId, long targetBusinessId,
|
||||
String targetInboundCode, String idempotencyKey, long operatorId) {}
|
||||
record BlockReleaseRequest(long blockId, long businessId, String idempotencyKey,
|
||||
long operatorId, String reason) {}
|
||||
record BlockResource(long blockId, long reservationId, long locationId, String locationCode,
|
||||
int slotNo, boolean sharedOverflow, String resourceStatus) {}
|
||||
record BlockResult(long blockId, String blockStatus, List<BlockResource> resources) {}
|
||||
record OccupiedResource(long reservationId, long occupancyId, int allocationNo, long warehouseId,
|
||||
long palletId, long locationId, String locationCode, boolean sharedOverflow,
|
||||
int allocatedPackageCount) {}
|
||||
|
||||
+2
-1
@@ -216,7 +216,8 @@ public class PlatformInboundMeasurementController {
|
||||
|
||||
@BusinessAudit(event = "INBOUND_WEIGHT_IMAGE_DELETE", resourceType = "INBOUND_WEIGHT_RESULT",
|
||||
resourceId = "#resultId", description = "删除称重图片及重量结果")
|
||||
@Operation(summary = "删除称重图片", description = "删除重量结果和文件;已确认图片删除后重新汇总实际总毛重。")
|
||||
@Operation(summary = "删除称重图片",
|
||||
description = "先将图片绑定槽恢复为 RESERVED_EMPTY,再删除重量结果和文件;整票库位块继续归当前入仓码。")
|
||||
@DeleteMapping("/weight-results/{resultId}")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:inbound:weight:edit')")
|
||||
public ApiResponse<Void> deleteWeightResult(
|
||||
|
||||
+2
@@ -38,4 +38,6 @@ public class PlatformSmartLoadingController {
|
||||
public ApiResponse<Plan> warehouseRemark(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long planId,@PathVariable @Positive long itemId,@Valid @RequestBody WarehouseRemarkRequest r){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.updateWarehouseRemark(planId,itemId,r,u.userId(),u.username()));}
|
||||
@DeleteMapping("/plans/{planId}/items/{itemId}") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:delete')")
|
||||
public ApiResponse<Plan> delete(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long planId,@PathVariable @Positive long itemId,@Valid @RequestBody DeleteRequest r){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.delete(planId,itemId,r,u.userId(),u.username()));}
|
||||
@DeleteMapping("/plans/{planId}/unpicked-items") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:delete')")
|
||||
public ApiResponse<Plan> removeUnpicked(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long planId,@Valid @RequestBody RemoveUnpickedRequest r){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.removeUnpicked(planId,r,u.userId(),u.username()));}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,8 @@ public final class InboundMeasurementResponses {
|
||||
@Schema(description = "Order 服务中的最新客户申报,只用于和既有实测事实对比;不得回写测量、箱码或审核结果")
|
||||
DeclarationSnapshot latestDeclaration,
|
||||
List<Item> items,
|
||||
List<WeightResult> weightResults
|
||||
List<WeightResult> weightResults,
|
||||
PalletLocationGuide palletLocationGuide
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -129,10 +130,42 @@ public final class InboundMeasurementResponses {
|
||||
LocalDateTime adjustedAt,
|
||||
Long confirmedBy,
|
||||
LocalDateTime confirmedAt,
|
||||
Integer palletSequence,
|
||||
Long blockId,
|
||||
Integer slotNo,
|
||||
Long reservationId,
|
||||
Long locationId,
|
||||
String locationCode,
|
||||
Boolean sharedOverflow,
|
||||
String slotStatus,
|
||||
Integer version
|
||||
) {
|
||||
}
|
||||
|
||||
/** 服务端权威的整票上架引导;包含尚未绑定称重图片的预留空槽。 */
|
||||
public record PalletLocationGuide(
|
||||
Long blockId,
|
||||
Integer reservedSlotCount,
|
||||
Integer actualPalletCount,
|
||||
Integer reservedEmptySlotCount,
|
||||
List<PalletLocationSlot> slots
|
||||
) {
|
||||
}
|
||||
|
||||
public record PalletLocationSlot(
|
||||
Integer slotNo,
|
||||
Long reservationId,
|
||||
Long locationId,
|
||||
String locationCode,
|
||||
Boolean sharedOverflow,
|
||||
String slotStatus,
|
||||
Long boundWeightResultId,
|
||||
Integer palletSequence,
|
||||
Long palletId,
|
||||
String palletCode
|
||||
) {
|
||||
}
|
||||
|
||||
public record PalletSnapshot(
|
||||
Long palletId,
|
||||
String palletCode,
|
||||
|
||||
@@ -21,9 +21,11 @@ public record InboundOrderDetail(
|
||||
this(summary, publicRemark, List.of(), palletLocations, files, internalNotes, statusHistory);
|
||||
}
|
||||
public record PalletLocationSummary(
|
||||
Integer allocationNo, Long warehouseId, Long palletId, String palletCode,
|
||||
Integer allocationNo, Long blockId, Integer slotNo, Long weightResultId,
|
||||
Integer palletSequence, Long warehouseId, Long palletId, String palletCode,
|
||||
BigDecimal palletCapacityCbm, BigDecimal allocatedVolumeCbm, Integer allocatedPackageCount,
|
||||
Long locationId, String locationCode, String reservationNo, String allocationStatus) {
|
||||
Long reservationId, Long locationId, String locationCode, Boolean sharedOverflow,
|
||||
String slotStatus, String allocationStatus) {
|
||||
}
|
||||
|
||||
public record FileSummary(
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.baiyue.firstleg.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAlias;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
@@ -20,31 +19,17 @@ public final class LoadingPickDtos {
|
||||
public record PickRequest(
|
||||
@NotNull @Min(1) Long warehouseId,
|
||||
@NotNull @Min(1) Integer pickedPackageCount,
|
||||
@Pattern(regexp = "AUTO|MANUAL") String releaseMode,
|
||||
@JsonAlias("reservationIds") List<@Min(1) Long> locationIds,
|
||||
@NotNull @Min(0) Integer version,
|
||||
@NotBlank @Size(max = 128) String operationId
|
||||
) {
|
||||
public PickRequest(Long warehouseId, Integer pickedPackageCount, List<Long> locationIds,
|
||||
Integer version, String operationId) {
|
||||
this(warehouseId, pickedPackageCount, null, locationIds, version, operationId);
|
||||
}
|
||||
}
|
||||
) {}
|
||||
|
||||
/** 部分拣货为该 PDA 明细的最终实际拣出箱数,不是增量。 */
|
||||
public record PartialPickRequest(
|
||||
@NotNull @Min(1) Long warehouseId,
|
||||
@NotNull @Min(1) Integer finalPickedPackageCount,
|
||||
@Pattern(regexp = "AUTO|MANUAL") String releaseMode,
|
||||
@JsonAlias("reservationIds") List<@Min(1) Long> locationIds,
|
||||
@NotNull @Min(0) Integer version,
|
||||
@NotBlank @Size(max = 128) String operationId
|
||||
) {
|
||||
public PartialPickRequest(Long warehouseId, Integer finalPickedPackageCount, List<Long> locationIds,
|
||||
Integer version, String operationId) {
|
||||
this(warehouseId, finalPickedPackageCount, null, locationIds, version, operationId);
|
||||
}
|
||||
}
|
||||
) {}
|
||||
|
||||
/** 跳拣只能发生在尚未拣出任何箱数的 PDA 明细上。 */
|
||||
public record SkipRequest(
|
||||
@@ -70,8 +55,6 @@ public final class LoadingPickDtos {
|
||||
@NotBlank @Pattern(regexp = "PICK|PARTIAL|SKIP") String action,
|
||||
@Min(1) Integer pickedPackageCount,
|
||||
@Min(1) Integer finalPickedPackageCount,
|
||||
@Pattern(regexp = "AUTO|MANUAL") String releaseMode,
|
||||
@JsonAlias("reservationIds") List<@Min(1) Long> locationIds,
|
||||
@NotNull @Min(0) Integer version,
|
||||
@Size(max = 500) String reason
|
||||
) {
|
||||
|
||||
@@ -73,6 +73,8 @@ public final class SmartLoadingDtos { private SmartLoadingDtos(){}
|
||||
@NotNull @Min(0) Integer expectedItemVersion,@NotBlank @Size(max=128) String operationId) {}
|
||||
public record DeleteRequest(@NotNull @Min(0) Integer expectedPlanVersion,
|
||||
@NotNull @Min(0) Integer expectedItemVersion,@NotBlank @Size(max=128) String operationId) {}
|
||||
public record RemoveUnpickedRequest(@NotNull @Min(0) Integer expectedPlanVersion,
|
||||
@NotBlank @Size(max=128) String operationId) {}
|
||||
public record WarehouseRemarkRequest(@Size(max=1000) String warehouseRemark,
|
||||
@NotNull @Min(0) Integer expectedPlanVersion, @NotNull @Min(0) Integer expectedItemVersion,
|
||||
@NotBlank @Size(max=128) String operationId) {}
|
||||
|
||||
+12
-1
@@ -19,6 +19,14 @@ public class InboundPalletAllocationEntity {
|
||||
private Long tenantId;
|
||||
private Long inboundId;
|
||||
private Integer allocationNo;
|
||||
/** 整票连续库位块标识;同一入库单的活动槽位共享该标识。 */
|
||||
private Long blockId;
|
||||
/** 连续块内从 1 开始的稳定槽位序号。 */
|
||||
private Integer slotNo;
|
||||
/** 称重图片对应的稳定重量结果;空槽允许为空。 */
|
||||
private Long weightResultId;
|
||||
/** 已绑定称重托盘在整票内的稳定展示顺序;空槽允许为空。 */
|
||||
private Integer palletSequence;
|
||||
private Long warehouseId;
|
||||
private Long warehouseResourceTenantId;
|
||||
private Long palletId;
|
||||
@@ -29,9 +37,12 @@ public class InboundPalletAllocationEntity {
|
||||
private Integer pickedPackageCount;
|
||||
private Long locationId;
|
||||
private String locationCode;
|
||||
/** 普通连续位不足时由 Warehouse 分配到共享 A0。 */
|
||||
private Boolean sharedOverflow;
|
||||
/** 仓库资源唯一预占标识;A0 多托盘场景不能使用 locationId 作为唯一标识。 */
|
||||
private Long reservationId;
|
||||
private String reservationNo;
|
||||
/** 槽位状态:RESERVED_EMPTY、BOUND、TRANSFERRED、RELEASED。 */
|
||||
private String slotStatus;
|
||||
private String allocationStatus;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@@ -23,10 +23,6 @@ public class LoadingPickItemEntity {
|
||||
private Integer plannedPackageCount;
|
||||
private Integer pickedPackageCount;
|
||||
private String itemStatus;
|
||||
/** AUTO 按比例释放,MANUAL 按指定库位释放。 */
|
||||
private String releaseMode;
|
||||
/** 人工选择的库位 ID,JSON 数组;空数组表示明确不释放。 */
|
||||
private String releaseLocationIds;
|
||||
@Version
|
||||
private Integer version;
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@@ -15,6 +15,7 @@ import com.baiyue.firstleg.loading.mapper.LoadingInboundMapper.CompletionTarget;
|
||||
import com.baiyue.firstleg.loading.bridge.service.LoadingModelBridgeService;
|
||||
import com.baiyue.firstleg.loading.repository.LoadingRepository;
|
||||
import com.baiyue.firstleg.repository.LoadingPickRepository;
|
||||
import com.baiyue.firstleg.service.FbaPricingService;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -40,6 +41,7 @@ public class LoadingDeliveryService {
|
||||
@Resource private LoadingCompletionGateService completionGate;
|
||||
@Resource private InboundStatusHistoryMapper histories;
|
||||
@Resource private CabinetLoadingEvidenceService cabinetEvidenceService;
|
||||
@Resource private FbaPricingService pricingService;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
|
||||
@Transactional
|
||||
@@ -64,6 +66,7 @@ public class LoadingDeliveryService {
|
||||
}
|
||||
if ("LOADED".equals(inbound.processStatus()) || "DELIVERED".equals(inbound.processStatus())) {
|
||||
requireLoadingPlanGate(tenantId, inboundId, acceptedPdaCompletion);
|
||||
pricingService.autoConfirmAfterLoading(tenantId, inboundId);
|
||||
return complete(tenantId, inboundId, operatorId, COMPLETE_LOADING, key,
|
||||
new LoadingActionResponse(inbound.processStatus()));
|
||||
}
|
||||
@@ -74,6 +77,7 @@ public class LoadingDeliveryService {
|
||||
requireCabinetEvidence(tenantId, inboundId);
|
||||
if (repository.completeLoading(tenantId, inboundId, operatorId, request.expectedVersion()) != 1) throw concurrent();
|
||||
addHistory(tenantId, inboundId, "LOADING", "LOADED", COMPLETE_LOADING, request.remark(), operatorId, operatorName);
|
||||
pricingService.autoConfirmAfterLoading(tenantId, inboundId);
|
||||
return complete(tenantId, inboundId, operatorId, COMPLETE_LOADING, key,
|
||||
new LoadingActionResponse("LOADED"));
|
||||
}
|
||||
@@ -116,6 +120,7 @@ public class LoadingDeliveryService {
|
||||
CompletionTarget target,
|
||||
boolean acceptedPdaCompletion) {
|
||||
if ("LOADED".equals(target.loadedChildStatus())) {
|
||||
pricingService.autoConfirmAfterLoading(tenantId, target.completionInboundId());
|
||||
return complete(tenantId, inboundId, operatorId, COMPLETE_LOADING, key,
|
||||
new LoadingActionResponse("LOADED"));
|
||||
}
|
||||
@@ -131,6 +136,7 @@ public class LoadingDeliveryService {
|
||||
repository.aggregateRootLoaded(tenantId, target.rootInboundId(), operatorId);
|
||||
addHistory(tenantId, target.completionInboundId(), "LOADING", "LOADED", COMPLETE_LOADING,
|
||||
request.remark(), operatorId, operatorName);
|
||||
pricingService.autoConfirmAfterLoading(tenantId, target.completionInboundId());
|
||||
return complete(tenantId, inboundId, operatorId, COMPLETE_LOADING, key,
|
||||
new LoadingActionResponse("LOADED"));
|
||||
}
|
||||
|
||||
+55
-12
@@ -9,6 +9,41 @@ import org.apache.ibatis.annotations.Update;
|
||||
|
||||
@Mapper
|
||||
public interface InboundPalletAllocationMapper extends BaseMapper<InboundPalletAllocationEntity> {
|
||||
@Select("""
|
||||
SELECT * FROM fl_inbound_pallet_allocation
|
||||
WHERE tenant_id = #{tenantId} AND inbound_id = #{inboundId}
|
||||
AND slot_status = 'RESERVED_EMPTY'
|
||||
AND allocation_status IN ('RESERVED', 'CONFIRMED')
|
||||
ORDER BY slot_no ASC
|
||||
LIMIT 1 FOR UPDATE
|
||||
""")
|
||||
InboundPalletAllocationEntity selectNextEmptyForUpdate(
|
||||
@Param("tenantId") long tenantId, @Param("inboundId") long inboundId);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_inbound_pallet_allocation
|
||||
SET weight_result_id=#{weightResultId}, pallet_sequence=#{palletSequence},
|
||||
pallet_id=#{palletId}, pallet_code=#{palletCode},
|
||||
pallet_capacity_cbm=#{palletCapacityCbm}, slot_status='BOUND',
|
||||
updated_at=CURRENT_TIMESTAMP(3)
|
||||
WHERE id=#{id} AND tenant_id=#{tenantId} AND inbound_id=#{inboundId}
|
||||
AND slot_status='RESERVED_EMPTY' AND weight_result_id IS NULL
|
||||
""")
|
||||
int bindWeight(@Param("id") long id, @Param("tenantId") long tenantId,
|
||||
@Param("inboundId") long inboundId, @Param("weightResultId") long weightResultId,
|
||||
@Param("palletSequence") int palletSequence, @Param("palletId") long palletId,
|
||||
@Param("palletCode") String palletCode,
|
||||
@Param("palletCapacityCbm") java.math.BigDecimal palletCapacityCbm);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_inbound_pallet_allocation
|
||||
SET weight_result_id=NULL, pallet_sequence=NULL, pallet_id=NULL, pallet_code=NULL,
|
||||
pallet_capacity_cbm=NULL, slot_status='RESERVED_EMPTY', updated_at=CURRENT_TIMESTAMP(3)
|
||||
WHERE id=#{id} AND tenant_id=#{tenantId} AND inbound_id=#{inboundId}
|
||||
AND weight_result_id=#{weightResultId} AND slot_status='BOUND'
|
||||
""")
|
||||
int unbindWeight(@Param("id") long id, @Param("tenantId") long tenantId,
|
||||
@Param("inboundId") long inboundId, @Param("weightResultId") long weightResultId);
|
||||
@Select("""
|
||||
SELECT *
|
||||
FROM fl_inbound_pallet_allocation
|
||||
@@ -39,21 +74,29 @@ public interface InboundPalletAllocationMapper extends BaseMapper<InboundPalletA
|
||||
int consume(@Param("id") long id, @Param("tenantId") long tenantId,
|
||||
@Param("inboundId") long inboundId, @Param("count") int count);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_inbound_pallet_allocation
|
||||
SET allocation_status = 'TRANSFERRED', updated_at = CURRENT_TIMESTAMP(3)
|
||||
WHERE id = #{id} AND tenant_id = #{tenantId} AND inbound_id = #{inboundId}
|
||||
AND allocation_status = 'CONFIRMED' AND picked_package_count < allocated_package_count
|
||||
@Select("""
|
||||
SELECT COUNT(*) FROM fl_inbound_pallet_allocation
|
||||
WHERE tenant_id=#{tenantId} AND inbound_id=#{inboundId} AND block_id=#{blockId}
|
||||
AND allocation_status='CONFIRMED'
|
||||
""")
|
||||
int markTransferred(@Param("id") long id, @Param("tenantId") long tenantId,
|
||||
@Param("inboundId") long inboundId);
|
||||
int countBlock(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId,
|
||||
@Param("blockId") long blockId);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_inbound_pallet_allocation
|
||||
SET allocation_status = 'RELEASED', updated_at = CURRENT_TIMESTAMP(3)
|
||||
WHERE tenant_id = #{tenantId} AND inbound_id = #{inboundId}
|
||||
AND reservation_id = #{reservationId} AND allocation_status = 'CONFIRMED'
|
||||
SET allocation_status='TRANSFERRED', slot_status='TRANSFERRED', updated_at=CURRENT_TIMESTAMP(3)
|
||||
WHERE tenant_id=#{tenantId} AND inbound_id=#{inboundId} AND block_id=#{blockId}
|
||||
AND allocation_status='CONFIRMED'
|
||||
""")
|
||||
int markReleased(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId,
|
||||
@Param("reservationId") long reservationId);
|
||||
int markBlockTransferred(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId,
|
||||
@Param("blockId") long blockId);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_inbound_pallet_allocation
|
||||
SET allocation_status='RELEASED', slot_status='RELEASED', updated_at=CURRENT_TIMESTAMP(3)
|
||||
WHERE tenant_id=#{tenantId} AND inbound_id=#{inboundId} AND block_id=#{blockId}
|
||||
AND allocation_status='CONFIRMED'
|
||||
""")
|
||||
int markBlockReleased(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId,
|
||||
@Param("blockId") long blockId);
|
||||
}
|
||||
|
||||
@@ -171,7 +171,6 @@ public interface LoadingPickItemMapper extends BaseMapper<LoadingPickItemEntity>
|
||||
SET item_status = CASE WHEN picked_package_count + #{count} = planned_package_count
|
||||
THEN 'PICKED' ELSE 'PICKING' END,
|
||||
picked_package_count = picked_package_count + #{count},
|
||||
release_mode = #{releaseMode}, release_location_ids = #{releaseLocationIds},
|
||||
version = version + 1
|
||||
WHERE id = #{id} AND version = #{version}
|
||||
AND EXISTS (SELECT 1 FROM fl_loading_plan_item plan_item
|
||||
@@ -182,15 +181,12 @@ public interface LoadingPickItemMapper extends BaseMapper<LoadingPickItemEntity>
|
||||
AND picked_package_count + #{count} <= planned_package_count
|
||||
""")
|
||||
int pick(@Param("id") long id, @Param("count") int count,
|
||||
@Param("releaseMode") String releaseMode,
|
||||
@Param("releaseLocationIds") String releaseLocationIds,
|
||||
@Param("version") int version,
|
||||
@Param("warehouseId") long warehouseId);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_loading_pick_item
|
||||
SET picked_package_count = #{finalCount}, item_status = 'PARTIAL',
|
||||
release_mode = #{releaseMode}, release_location_ids = #{releaseLocationIds},
|
||||
version = version + 1
|
||||
WHERE id = #{id} AND version = #{version}
|
||||
AND EXISTS (SELECT 1 FROM fl_loading_plan_item plan_item
|
||||
@@ -203,8 +199,6 @@ public interface LoadingPickItemMapper extends BaseMapper<LoadingPickItemEntity>
|
||||
AND #{finalCount} < planned_package_count
|
||||
""")
|
||||
int partial(@Param("id") long id, @Param("finalCount") int finalCount,
|
||||
@Param("releaseMode") String releaseMode,
|
||||
@Param("releaseLocationIds") String releaseLocationIds,
|
||||
@Param("version") int version,
|
||||
@Param("warehouseId") long warehouseId);
|
||||
|
||||
|
||||
@@ -28,6 +28,27 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba
|
||||
""")
|
||||
List<PlanColoadMemberRow> planColoadMembers(@Param("planId") long planId);
|
||||
|
||||
@Select("""
|
||||
<script>
|
||||
SELECT DISTINCT item.order_id
|
||||
FROM fl_loading_plan_item item
|
||||
JOIN fl_loading_plan plan ON plan.id=item.loading_plan_id
|
||||
JOIN fl_inbound_order inbound
|
||||
ON inbound.tenant_id=item.account_id AND inbound.id=item.inbound_id
|
||||
WHERE item.deleted=0 AND plan.plan_status IN ('EDITING','LOADING')
|
||||
AND item.order_id IS NOT NULL
|
||||
AND inbound.channel_code IN
|
||||
<foreach collection="actualChannels" item="channel" open="(" separator="," close=")">#{channel}</foreach>
|
||||
<if test="query.warehouseId != null">AND inbound.warehouse_id=#{query.warehouseId}</if>
|
||||
<if test="query.customerId != null">AND item.account_id=#{query.customerId}</if>
|
||||
<if test="query.inboundCode != null and query.inboundCode.trim() != ''">
|
||||
AND item.inbound_code=#{query.inboundCode}
|
||||
</if>
|
||||
</script>
|
||||
""")
|
||||
List<Long> occupiedOrderIds(@Param("query") InboundCandidateQuery query,
|
||||
@Param("actualChannels") Set<String> actualChannels);
|
||||
|
||||
@Select("""
|
||||
SELECT o.tenant_id accountId,o.id inboundId,i.inbound_detail_id inboundDetailId,
|
||||
o.order_id orderId,o.order_no orderNo,o.inbound_code inboundCode,
|
||||
@@ -134,6 +155,11 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba
|
||||
o.process_status IN ('WAIT_MEASURE','MEASURING','MEASURE_PENDING','MEASURE_REJECTED',
|
||||
'WAIT_PRICING','FEE_CONFIRMING','INBOUNDING','WAIT_SPLIT','LOADING')
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM fl_loading_plan_item active_item
|
||||
WHERE active_item.account_id=o.tenant_id AND active_item.inbound_id=o.id
|
||||
AND active_item.deleted=0
|
||||
)
|
||||
) eligible
|
||||
LEFT JOIN fl_coload_group_member current_member
|
||||
ON current_member.tenant_id=eligible.accountId AND current_member.inbound_id=eligible.inboundId
|
||||
|
||||
@@ -169,7 +169,7 @@ public class FbaPricingRepository {
|
||||
public List<CustomerChargeRuleEntity> matchRules(InboundPricingEntity inbound, LocalDateTime at) {
|
||||
return rules.selectList(new LambdaQueryWrapper<CustomerChargeRuleEntity>()
|
||||
.eq(CustomerChargeRuleEntity::getTenantId, inbound.getTenantId())
|
||||
.eq(CustomerChargeRuleEntity::getCustomerId, inbound.getCustomerId())
|
||||
.eq(CustomerChargeRuleEntity::getCustomerId, inbound.getTenantId())
|
||||
.eq(CustomerChargeRuleEntity::getBusinessType, inbound.getBusinessType())
|
||||
.eq(CustomerChargeRuleEntity::getTaxMode, inbound.getTaxMode())
|
||||
.eq(CustomerChargeRuleEntity::getStatus, "ACTIVE")
|
||||
|
||||
@@ -212,9 +212,11 @@ public class InboundOrderRepository {
|
||||
.orderByAsc(InboundPalletAllocationEntity::getAllocationNo));
|
||||
List<InboundOrderDetail.PalletLocationSummary> palletSummaries = palletAllocations.stream()
|
||||
.map(item -> new InboundOrderDetail.PalletLocationSummary(
|
||||
item.getAllocationNo(), item.getWarehouseId(), item.getPalletId(), item.getPalletCode(),
|
||||
item.getAllocationNo(), item.getBlockId(), item.getSlotNo(), item.getWeightResultId(),
|
||||
item.getPalletSequence(), item.getWarehouseId(), item.getPalletId(), item.getPalletCode(),
|
||||
item.getPalletCapacityCbm(), item.getAllocatedVolumeCbm(), item.getAllocatedPackageCount(),
|
||||
item.getLocationId(), item.getLocationCode(), item.getReservationNo(), item.getAllocationStatus()))
|
||||
item.getReservationId(), item.getLocationId(), item.getLocationCode(), item.getSharedOverflow(),
|
||||
item.getSlotStatus(), item.getAllocationStatus()))
|
||||
.toList();
|
||||
List<InboundOrderDetail.FileSummary> fileSummaries = files.selectList(
|
||||
owned(InboundFileEntity::getTenantId, InboundFileEntity::getInboundId, tenantId, inboundId)
|
||||
|
||||
+88
-75
@@ -3,66 +3,114 @@ package com.baiyue.firstleg.repository;
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baiyue.firstleg.client.WarehousePalletLocationClient.BatchResult;
|
||||
import com.baiyue.firstleg.client.WarehousePalletLocationClient.BlockResult;
|
||||
import com.baiyue.firstleg.entity.InboundPalletAllocationEntity;
|
||||
import com.baiyue.firstleg.entity.InboundWeightResultEntity;
|
||||
import com.baiyue.firstleg.mapper.InboundPalletAllocationMapper;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Repository
|
||||
public class InboundPalletAllocationRepository {
|
||||
@Resource private InboundPalletAllocationMapper mapper;
|
||||
|
||||
public List<InboundPalletAllocationEntity> list(long tenantId, long inboundId) {
|
||||
return mapper.selectList(new LambdaQueryWrapper<InboundPalletAllocationEntity>()
|
||||
.eq(InboundPalletAllocationEntity::getTenantId, tenantId)
|
||||
.eq(InboundPalletAllocationEntity::getInboundId, inboundId)
|
||||
.orderByAsc(InboundPalletAllocationEntity::getSlotNo)
|
||||
.orderByAsc(InboundPalletAllocationEntity::getAllocationNo));
|
||||
}
|
||||
|
||||
public InboundPalletAllocationEntity findByWeightResult(long tenantId, long inboundId,
|
||||
long weightResultId) {
|
||||
return mapper.selectOne(new LambdaQueryWrapper<InboundPalletAllocationEntity>()
|
||||
.eq(InboundPalletAllocationEntity::getTenantId, tenantId)
|
||||
.eq(InboundPalletAllocationEntity::getInboundId, inboundId)
|
||||
.eq(InboundPalletAllocationEntity::getWeightResultId, weightResultId));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void replace(long tenantId, long inboundId, long warehouseId, long warehouseResourceTenantId,
|
||||
int totalPackageCount, BatchResult result) {
|
||||
Map<Long, String> reservationNos = mapper.selectList(new LambdaQueryWrapper<InboundPalletAllocationEntity>()
|
||||
.eq(InboundPalletAllocationEntity::getTenantId, tenantId)
|
||||
.eq(InboundPalletAllocationEntity::getInboundId, inboundId))
|
||||
.stream().collect(Collectors.toMap(InboundPalletAllocationEntity::getReservationId,
|
||||
InboundPalletAllocationEntity::getReservationNo, (left, right) -> left));
|
||||
public void replaceBlock(long tenantId, long inboundId, long warehouseId,
|
||||
int totalPackageCount, BigDecimal totalVolumeCbm, BlockResult result) {
|
||||
mapper.delete(new LambdaQueryWrapper<InboundPalletAllocationEntity>()
|
||||
.eq(InboundPalletAllocationEntity::getTenantId, tenantId)
|
||||
.eq(InboundPalletAllocationEntity::getInboundId, inboundId));
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
int allocationCount = result.allocations().size();
|
||||
int allocationCount = result.resources().size();
|
||||
int basePackageCount = allocationCount == 0 ? 0 : totalPackageCount / allocationCount;
|
||||
int remainder = allocationCount == 0 ? 0 : totalPackageCount % allocationCount;
|
||||
for (int index = 0; index < result.allocations().size(); index++) {
|
||||
var source = result.allocations().get(index);
|
||||
for (int index = 0; index < result.resources().size(); index++) {
|
||||
var source = result.resources().get(index);
|
||||
InboundPalletAllocationEntity value = new InboundPalletAllocationEntity();
|
||||
value.setTenantId(tenantId);
|
||||
value.setInboundId(inboundId);
|
||||
value.setAllocationNo(source.allocationNo());
|
||||
value.setAllocationNo(source.slotNo());
|
||||
value.setBlockId(source.blockId());
|
||||
value.setSlotNo(source.slotNo());
|
||||
value.setWarehouseId(warehouseId);
|
||||
value.setWarehouseResourceTenantId(warehouseResourceTenantId);
|
||||
value.setPalletId(source.palletId());
|
||||
value.setPalletCode(source.palletCode());
|
||||
value.setPalletCapacityCbm(source.palletCapacityCbm());
|
||||
value.setAllocatedVolumeCbm(source.allocatedVolumeCbm());
|
||||
value.setWarehouseResourceTenantId(0L);
|
||||
value.setAllocatedVolumeCbm(totalVolumeCbm.divide(BigDecimal.valueOf(allocationCount), 4,
|
||||
java.math.RoundingMode.HALF_UP));
|
||||
int allocatedPackages = basePackageCount + (index < remainder ? 1 : 0);
|
||||
value.setAllocatedPackageCount(allocatedPackages);
|
||||
value.setPickedPackageCount(0);
|
||||
value.setLocationId(source.locationId());
|
||||
value.setLocationCode(source.locationCode());
|
||||
value.setSharedOverflow(source.sharedOverflow());
|
||||
value.setReservationId(source.reservationId());
|
||||
// 仓储确认响应只返回 reservationId;沿用预占阶段的预占单号快照。
|
||||
value.setReservationNo(source.reservationNo() != null ? source.reservationNo()
|
||||
: reservationNos.get(source.reservationId()));
|
||||
value.setAllocationStatus(normalize(source.state()));
|
||||
value.setSlotStatus("RELEASED".equals(source.resourceStatus()) ? "RELEASED" : "RESERVED_EMPTY");
|
||||
value.setAllocationStatus(normalize(source.resourceStatus()));
|
||||
value.setCreatedAt(now);
|
||||
value.setUpdatedAt(now);
|
||||
mapper.insert(value);
|
||||
}
|
||||
}
|
||||
|
||||
/** 已确认图片按 confirmedAt、weightResultId 顺序绑定块内下一个空槽,不依赖列表下标。 */
|
||||
@Transactional
|
||||
public void bindConfirmedWeights(long tenantId, long inboundId, List<InboundWeightResultEntity> weights) {
|
||||
weights.stream().filter(weight -> "CONFIRMED".equals(weight.getRecognitionStatus()))
|
||||
.sorted(java.util.Comparator.comparing(InboundWeightResultEntity::getConfirmedAt,
|
||||
java.util.Comparator.nullsLast(LocalDateTime::compareTo))
|
||||
.thenComparing(InboundWeightResultEntity::getId))
|
||||
.forEach(weight -> bindNextEmpty(tenantId, inboundId, weight));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InboundPalletAllocationEntity bindNextEmpty(long tenantId, long inboundId,
|
||||
InboundWeightResultEntity weight) {
|
||||
InboundPalletAllocationEntity existing = findByWeightResult(tenantId, inboundId, weight.getId());
|
||||
if (existing != null) return existing;
|
||||
InboundPalletAllocationEntity empty = mapper.selectNextEmptyForUpdate(tenantId, inboundId);
|
||||
if (empty == null) {
|
||||
throw new IllegalStateException("整票预留库位块没有可绑定的空槽");
|
||||
}
|
||||
if (mapper.bindWeight(empty.getId(), tenantId, inboundId, weight.getId(), empty.getSlotNo(),
|
||||
weight.getPalletId(), weight.getPalletCode(), weight.getPalletCapacityCbm()) != 1) {
|
||||
throw new IllegalStateException("整票库位槽已被并发绑定");
|
||||
}
|
||||
empty.setWeightResultId(weight.getId());
|
||||
empty.setPalletSequence(empty.getSlotNo());
|
||||
empty.setPalletId(weight.getPalletId());
|
||||
empty.setPalletCode(weight.getPalletCode());
|
||||
empty.setPalletCapacityCbm(weight.getPalletCapacityCbm());
|
||||
empty.setSlotStatus("BOUND");
|
||||
return empty;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void unbindWeight(long tenantId, long inboundId, long weightResultId) {
|
||||
InboundPalletAllocationEntity existing = findByWeightResult(tenantId, inboundId, weightResultId);
|
||||
if (existing != null && mapper.unbindWeight(existing.getId(), tenantId, inboundId, weightResultId) != 1) {
|
||||
throw new IllegalStateException("整票库位槽解绑失败");
|
||||
}
|
||||
}
|
||||
|
||||
public List<InboundPalletAllocationEntity> lockConfirmed(long tenantId, long inboundId) {
|
||||
return mapper.selectConfirmedForUpdate(tenantId, inboundId);
|
||||
}
|
||||
@@ -73,54 +121,17 @@ public class InboundPalletAllocationRepository {
|
||||
}
|
||||
}
|
||||
|
||||
/** 父单快照在仓库资源转移前标记为已转移,后续锁查询不会再将其当作可用资源。 */
|
||||
public void markTransferred(long tenantId, long inboundId, List<InboundPalletAllocationEntity> allocations,
|
||||
List<Long> releasedReservationIds) {
|
||||
for (InboundPalletAllocationEntity allocation : allocations) {
|
||||
if (releasedReservationIds.contains(allocation.getReservationId())) {
|
||||
continue;
|
||||
}
|
||||
int allocated = allocation.getAllocatedPackageCount() == null ? 0 : allocation.getAllocatedPackageCount();
|
||||
int picked = allocation.getPickedPackageCount() == null ? 0 : allocation.getPickedPackageCount();
|
||||
if (allocated <= picked) {
|
||||
continue;
|
||||
}
|
||||
if (allocation.getId() == null) {
|
||||
throw new IllegalStateException("库位资源转移状态已被并发修改");
|
||||
}
|
||||
int changed = mapper.markTransferred(allocation.getId(), tenantId, inboundId);
|
||||
if (changed != 1 && !isAllocationIdState(allocation.getId(), tenantId, inboundId, "TRANSFERRED")) {
|
||||
throw new IllegalStateException("库位资源转移状态已被并发修改");
|
||||
}
|
||||
}
|
||||
/** 余量子单存在时整票块转移,包含已经拣空和预留空槽。 */
|
||||
public void markBlockTransferred(long tenantId, long inboundId, long blockId) {
|
||||
int expected = mapper.countBlock(tenantId, inboundId, blockId);
|
||||
int changed = mapper.markBlockTransferred(tenantId, inboundId, blockId);
|
||||
if (expected < 1 || changed != expected) throw new IllegalStateException("整票库位块转移状态已被并发修改");
|
||||
}
|
||||
|
||||
public void markReleased(long tenantId, long inboundId, List<Long> reservationIds) {
|
||||
for (Long reservationId : reservationIds) {
|
||||
if (reservationId == null) {
|
||||
throw new IllegalStateException("库位释放状态已被并发修改");
|
||||
}
|
||||
int changed = mapper.markReleased(tenantId, inboundId, reservationId);
|
||||
if (changed != 1 && !isReservationState(reservationId, tenantId, inboundId, "RELEASED")) {
|
||||
throw new IllegalStateException("库位释放状态已被并发修改");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isAllocationIdState(long id, long tenantId, long inboundId, String status) {
|
||||
InboundPalletAllocationEntity current = mapper.selectOne(new LambdaQueryWrapper<InboundPalletAllocationEntity>()
|
||||
.eq(InboundPalletAllocationEntity::getTenantId, tenantId)
|
||||
.eq(InboundPalletAllocationEntity::getInboundId, inboundId)
|
||||
.eq(InboundPalletAllocationEntity::getId, id));
|
||||
return current != null && status.equals(current.getAllocationStatus());
|
||||
}
|
||||
|
||||
private boolean isReservationState(long reservationId, long tenantId, long inboundId, String status) {
|
||||
InboundPalletAllocationEntity current = mapper.selectOne(new LambdaQueryWrapper<InboundPalletAllocationEntity>()
|
||||
.eq(InboundPalletAllocationEntity::getTenantId, tenantId)
|
||||
.eq(InboundPalletAllocationEntity::getInboundId, inboundId)
|
||||
.eq(InboundPalletAllocationEntity::getReservationId, reservationId));
|
||||
return current != null && status.equals(current.getAllocationStatus());
|
||||
public void markBlockReleased(long tenantId, long inboundId, long blockId) {
|
||||
int expected = mapper.countBlock(tenantId, inboundId, blockId);
|
||||
int changed = mapper.markBlockReleased(tenantId, inboundId, blockId);
|
||||
if (expected < 1 || changed != expected) throw new IllegalStateException("整票库位块释放状态已被并发修改");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,7 +139,7 @@ public class InboundPalletAllocationRepository {
|
||||
* 仓库 transfer 是本地事务最后一个外部动作,成功后不再执行本地写入,避免跨服务重试时目标子单变化。
|
||||
*/
|
||||
@Transactional
|
||||
public void copyRemainingToChild(long tenantId, long inboundId, List<InboundPalletAllocationEntity> parent) {
|
||||
public void copyBlockToChild(long tenantId, long inboundId, List<InboundPalletAllocationEntity> parent) {
|
||||
mapper.delete(new LambdaQueryWrapper<InboundPalletAllocationEntity>()
|
||||
.eq(InboundPalletAllocationEntity::getTenantId, tenantId)
|
||||
.eq(InboundPalletAllocationEntity::getInboundId, inboundId));
|
||||
@@ -137,9 +148,6 @@ public class InboundPalletAllocationRepository {
|
||||
int allocated = source.getAllocatedPackageCount() == null ? 0 : source.getAllocatedPackageCount();
|
||||
int picked = source.getPickedPackageCount() == null ? 0 : source.getPickedPackageCount();
|
||||
int remaining = allocated - picked;
|
||||
if (remaining <= 0) {
|
||||
continue;
|
||||
}
|
||||
InboundPalletAllocationEntity value = new InboundPalletAllocationEntity();
|
||||
value.setTenantId(tenantId);
|
||||
value.setInboundId(inboundId);
|
||||
@@ -155,7 +163,12 @@ public class InboundPalletAllocationRepository {
|
||||
value.setLocationId(source.getLocationId());
|
||||
value.setLocationCode(source.getLocationCode());
|
||||
value.setReservationId(source.getReservationId());
|
||||
value.setReservationNo(source.getReservationNo());
|
||||
value.setBlockId(source.getBlockId());
|
||||
value.setSlotNo(source.getSlotNo());
|
||||
value.setWeightResultId(source.getWeightResultId());
|
||||
value.setPalletSequence(source.getPalletSequence());
|
||||
value.setSharedOverflow(source.getSharedOverflow());
|
||||
value.setSlotStatus(source.getSlotStatus());
|
||||
value.setAllocationStatus("CONFIRMED");
|
||||
value.setCreatedAt(now);
|
||||
value.setUpdatedAt(now);
|
||||
|
||||
@@ -135,14 +135,12 @@ public class LoadingPickRepository {
|
||||
.eq(LoadingPickItemEntity::getLoadingPlanItemId, planItemId));
|
||||
}
|
||||
|
||||
public int pick(long itemId, int count, String releaseMode, String releaseLocationIds,
|
||||
int version, long warehouseId) {
|
||||
return items.pick(itemId, count, releaseMode, releaseLocationIds, version, warehouseId);
|
||||
public int pick(long itemId, int count, int version, long warehouseId) {
|
||||
return items.pick(itemId, count, version, warehouseId);
|
||||
}
|
||||
|
||||
public int partial(long itemId, int finalCount, String releaseMode, String releaseLocationIds,
|
||||
int version, long warehouseId) {
|
||||
return items.partial(itemId, finalCount, releaseMode, releaseLocationIds, version, warehouseId);
|
||||
public int partial(long itemId, int finalCount, int version, long warehouseId) {
|
||||
return items.partial(itemId, finalCount, version, warehouseId);
|
||||
}
|
||||
|
||||
public int skip(long itemId, int version, long warehouseId) {
|
||||
@@ -174,6 +172,12 @@ public class LoadingPickRepository {
|
||||
.eq(LoadingPickHistoryEntity::getOperationId, operationId));
|
||||
}
|
||||
|
||||
public LoadingPickHistoryEntity historyForUpdate(String operationId) {
|
||||
return histories.selectOne(new LambdaQueryWrapper<LoadingPickHistoryEntity>()
|
||||
.eq(LoadingPickHistoryEntity::getOperationId, operationId)
|
||||
.last("FOR UPDATE"));
|
||||
}
|
||||
|
||||
public void insert(LoadingPickHistoryEntity history) {
|
||||
histories.insert(history);
|
||||
}
|
||||
@@ -215,6 +219,17 @@ public class LoadingPickRepository {
|
||||
.eq(LoadingPickOperationEntity::getOperationId, operationId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Current read used after the pick-task row lock has serialized concurrent commands.
|
||||
* A normal repeatable-read lookup would keep the transaction's earlier snapshot and
|
||||
* miss an operation committed while this request was waiting for that row lock.
|
||||
*/
|
||||
public LoadingPickOperationEntity operationForUpdate(String operationId) {
|
||||
return operations.selectOne(new LambdaQueryWrapper<LoadingPickOperationEntity>()
|
||||
.eq(LoadingPickOperationEntity::getOperationId, operationId)
|
||||
.last("FOR UPDATE"));
|
||||
}
|
||||
|
||||
public void insert(LoadingPickOperationEntity operation) {
|
||||
operations.insert(operation);
|
||||
}
|
||||
|
||||
@@ -59,6 +59,9 @@ public class SmartLoadingRepository {
|
||||
public List<PlanColoadMemberRow> planColoadMembers(long planId) {
|
||||
return candidates.planColoadMembers(planId);
|
||||
}
|
||||
public List<Long> occupiedOrderIds(InboundCandidateQuery query, Set<String> actualChannels) {
|
||||
return candidates.occupiedOrderIds(query, actualChannels);
|
||||
}
|
||||
public Candidate candidate(long planId, long warehouseId, long inboundId, long detailId) {
|
||||
return candidates.candidate(planId, warehouseId, inboundId, detailId);
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@ import java.util.List;
|
||||
@Service
|
||||
public class FbaPricingService {
|
||||
private static final int MONEY_SCALE = 2;
|
||||
private static final long AUTOMATIC_CONFIRM_OPERATOR_ID = 0L;
|
||||
private static final String AUTOMATIC_CONFIRM_OPERATOR_NAME = "装柜完成自动确认";
|
||||
@Resource
|
||||
private FbaPricingRepository repository;
|
||||
@Resource
|
||||
@@ -91,16 +93,25 @@ public class FbaPricingService {
|
||||
}
|
||||
}
|
||||
var measurement = measurementRepository.findLatestMeasurement(tenantId, inboundId);
|
||||
if ("OUTER".equalsIgnoreCase(volumeSource) && measurement == null) {
|
||||
throw conflict("外箱规测量批次不存在,不能生成对客费用");
|
||||
boolean actualMeasurementSource = "ACTUAL".equalsIgnoreCase(weightSource)
|
||||
|| "ACTUAL".equalsIgnoreCase(volumeSource);
|
||||
boolean outerMeasurementSource = "OUTER".equalsIgnoreCase(weightSource)
|
||||
|| "OUTER".equalsIgnoreCase(volumeSource);
|
||||
if ((actualMeasurementSource || outerMeasurementSource) && measurement == null) {
|
||||
throw conflict(actualMeasurementSource
|
||||
? "实际测量数据不存在,不能生成对客费用"
|
||||
: "外箱规测量批次不存在,不能生成对客费用");
|
||||
}
|
||||
if ("OUTER".equalsIgnoreCase(volumeSource)) {
|
||||
var outerItems = measurementRepository.listItems(tenantId, measurement.getId());
|
||||
boolean incomplete = outerItems.isEmpty() || outerItems.stream().anyMatch(item ->
|
||||
if (actualMeasurementSource || outerMeasurementSource) {
|
||||
var measurementItems = measurementRepository.listItems(tenantId, measurement.getId());
|
||||
if (actualMeasurementSource && measurementItems.isEmpty()) {
|
||||
throw conflict("实际测量明细不存在,不能生成对客费用");
|
||||
}
|
||||
boolean incomplete = outerMeasurementSource && (measurementItems.isEmpty() || measurementItems.stream().anyMatch(item ->
|
||||
item.getOuterLengthCm() == null || item.getOuterLengthCm().signum() <= 0
|
||||
|| item.getOuterWidthCm() == null || item.getOuterWidthCm().signum() <= 0
|
||||
|| item.getOuterHeightCm() == null || item.getOuterHeightCm().signum() <= 0
|
||||
|| !StringUtils.hasText(item.getCalculationRuleVersion()));
|
||||
|| !StringUtils.hasText(item.getCalculationRuleVersion())));
|
||||
if (incomplete) throw conflict("外箱规数据或绑定的计算规则不完整,不能生成对客费用");
|
||||
}
|
||||
ActivePriceBookEstimator.Estimate estimate = priceBookEstimator.estimate(new EstimateRequest(
|
||||
@@ -204,6 +215,21 @@ public class FbaPricingService {
|
||||
return response(tenantId, fee.getId(), null);
|
||||
}
|
||||
|
||||
/** 客户未操作时,装柜完成以系统身份确认最新一版已发送费用。 */
|
||||
@Transactional
|
||||
public void autoConfirmAfterLoading(long tenantId, long inboundId) {
|
||||
InboundFeeEntity current = repository.findLatestFeeForUpdate(tenantId, inboundId);
|
||||
if (current == null || !"SENT".equals(current.getFeeStatus())
|
||||
|| current.getCustomerConfirmedAt() != null) {
|
||||
return;
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now(clock);
|
||||
if (repository.confirmFeeByCustomer(tenantId, current.getId(), current.getFeeVersion(),
|
||||
AUTOMATIC_CONFIRM_OPERATOR_ID, AUTOMATIC_CONFIRM_OPERATOR_NAME, now) != 1) {
|
||||
throw conflict("装柜完成自动确认费用失败,请重试");
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public FeeSnapshotResponse confirmByCustomer(long tenantId, long operatorId, String operatorName,
|
||||
long inboundId, int expectedFeeVersion,
|
||||
@@ -339,9 +365,6 @@ public class FbaPricingService {
|
||||
if (!"FBA".equals(inbound.getDestinationType()) && !"OVERSEAS".equals(inbound.getDestinationType())) {
|
||||
throw conflict("当前目的地类型不支持自动计费");
|
||||
}
|
||||
if (inbound.getCustomerId() == null || inbound.getCustomerId() <= 0) {
|
||||
throw conflict("入库单缺少订单客户归属,不能生成对客费用");
|
||||
}
|
||||
if (inbound.getWarehouseId() == null || !StringUtils.hasText(inbound.getChannelCode())) {
|
||||
throw conflict("入库单缺少起运仓或渠道匹配维度");
|
||||
}
|
||||
@@ -419,7 +442,7 @@ public class FbaPricingService {
|
||||
fee.setChargeRuleId(null); fee.setChargeRuleVersion(null);
|
||||
fee.setCarrierRateId(rate.getId()); fee.setCarrierRateVersion(rate.getRateVersion());
|
||||
fee.setPriceBookVersionId(estimate.priceBookVersionId());
|
||||
fee.setRuleCode(null); fee.setCustomerId(inbound.getCustomerId());
|
||||
fee.setRuleCode(null); fee.setCustomerId(tenantId);
|
||||
fee.setCustomerName(inbound.getCustomerName()); fee.setBusinessType(inbound.getBusinessType());
|
||||
fee.setTaxMode(inbound.getTaxMode()); fee.setPricingTaxMode(inbound.getPricingTaxMode());
|
||||
fee.setRoundingScale(estimate.roundingScale()); fee.setRoundingMode(estimate.roundingMode());
|
||||
|
||||
@@ -100,7 +100,7 @@ public class InboundFeeDetailService {
|
||||
detail.setInboundCode(inbound.getInboundCode());
|
||||
detail.setDirection("RECEIVABLE");
|
||||
detail.setBusinessDate(now.toLocalDate());
|
||||
detail.setCustomerId(inbound.getCustomerId());
|
||||
detail.setCustomerId(tenantId);
|
||||
detail.setCustomerName(inbound.getCustomerName());
|
||||
detail.setInvoiceTitle(inbound.getCompanyName());
|
||||
detail.setFeeCategorySnapshot(feeItem.getCategory());
|
||||
@@ -186,7 +186,7 @@ public class InboundFeeDetailService {
|
||||
detail.setSourceType("FBA_PRICING"); detail.setSourceFeeId(fee.getId());
|
||||
detail.setSourceFeeVersion(fee.getFeeVersion()); detail.setSourceFeeNo(fee.getFeeNo());
|
||||
detail.setInboundCode(inbound.getInboundCode()); detail.setDirection("RECEIVABLE");
|
||||
detail.setBusinessDate(now.toLocalDate()); detail.setCustomerId(inbound.getCustomerId());
|
||||
detail.setBusinessDate(now.toLocalDate()); detail.setCustomerId(tenantId);
|
||||
detail.setCustomerName(inbound.getCustomerName()); detail.setInvoiceTitle(inbound.getCompanyName());
|
||||
detail.setFeeCategorySnapshot(feeItem.getCategory()); detail.setTradeScopeSnapshot(feeItem.getTradeScope());
|
||||
detail.setFeeTypeNameSnapshot(feeItem.getFeeTypeName()); detail.setChannelCode(inbound.getChannelCode());
|
||||
|
||||
@@ -19,6 +19,7 @@ import com.baiyue.firstleg.entity.InboundWeightResultEntity;
|
||||
import com.baiyue.firstleg.entity.InboundWeightRevisionEntity;
|
||||
import com.baiyue.firstleg.entity.MeasureReviewRecordEntity;
|
||||
import com.baiyue.firstleg.repository.InboundMeasurementRepository;
|
||||
import com.baiyue.firstleg.repository.InboundPalletAllocationRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
@@ -30,6 +31,7 @@ import java.math.RoundingMode;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/** 平台端入库测量、OCR 修正、重量确认和提交审核应用服务。 */
|
||||
@@ -63,6 +65,8 @@ public class InboundMeasurementService {
|
||||
private MeasurementImageClient imageClient;
|
||||
@Resource
|
||||
private WarehousePalletLocationClient warehousePallets;
|
||||
@Resource
|
||||
private InboundPalletAllocationRepository palletAllocations;
|
||||
|
||||
public InboundMeasurementResponses.Detail detail(long tenantId, long inboundId) {
|
||||
InboundOrderEntity inbound = requireInbound(tenantId, inboundId);
|
||||
@@ -82,9 +86,18 @@ public class InboundMeasurementService {
|
||||
List<InboundMeasurementItemEntity> items = measurement == null
|
||||
? List.of() : repository.listItems(tenantId, measurement.getId());
|
||||
List<InboundWeightResultEntity> weights = repository.listWeights(tenantId, inbound.getId());
|
||||
List<com.baiyue.firstleg.entity.InboundPalletAllocationEntity> allocationSlots =
|
||||
palletAllocations.list(tenantId, inbound.getId());
|
||||
Map<Long, com.baiyue.firstleg.entity.InboundPalletAllocationEntity> allocationsByWeight =
|
||||
allocationSlots.stream()
|
||||
.filter(item -> item.getWeightResultId() != null)
|
||||
.collect(java.util.stream.Collectors.toMap(
|
||||
com.baiyue.firstleg.entity.InboundPalletAllocationEntity::getWeightResultId,
|
||||
java.util.function.Function.identity(), (left, right) -> left));
|
||||
MeasureReviewRecordEntity review = repository.findLatestReview(tenantId, inbound.getId());
|
||||
InboundMeasurementResponses.DeclarationSnapshot latestDeclaration = loadLatestDeclaration(inbound);
|
||||
return toDetail(inbound, measurement, items, weights, review, latestDeclaration);
|
||||
return toDetail(inbound, measurement, items, weights, allocationSlots, allocationsByWeight,
|
||||
review, latestDeclaration);
|
||||
}
|
||||
|
||||
public PageResult<InboundMeasurementResponses.PalletSnapshot> weightPalletOptions(
|
||||
@@ -311,6 +324,9 @@ public class InboundMeasurementService {
|
||||
weight.setVersion(confirmedVersion);
|
||||
insertRevision(weight, confirmedVersion, "CONFIRM", value, unit, value, unit,
|
||||
grossWeightKg, netWeightKg, null, operatorId, operatorName);
|
||||
if (!palletAllocations.list(tenantId, inboundId).isEmpty()) {
|
||||
palletAllocations.bindNextEmpty(tenantId, inboundId, weight);
|
||||
}
|
||||
recalculateConfirmedWeight(inbound, operatorId);
|
||||
return toWeight(weight);
|
||||
}
|
||||
@@ -322,6 +338,7 @@ public class InboundMeasurementService {
|
||||
requireWeightEditableInbound(inbound);
|
||||
InboundWeightResultEntity weight = requireWeight(tenantId, inboundId, resultId);
|
||||
requireVersion(version, weight.getVersion());
|
||||
palletAllocations.unbindWeight(tenantId, inboundId, resultId);
|
||||
if (!repository.deleteWeight(tenantId, inboundId, resultId, version)) {
|
||||
throw concurrentModification();
|
||||
}
|
||||
@@ -433,6 +450,8 @@ public class InboundMeasurementService {
|
||||
private InboundMeasurementResponses.Detail toDetail(
|
||||
InboundOrderEntity inbound, InboundMeasurementEntity measurement,
|
||||
List<InboundMeasurementItemEntity> items, List<InboundWeightResultEntity> weights,
|
||||
List<com.baiyue.firstleg.entity.InboundPalletAllocationEntity> allocationSlots,
|
||||
Map<Long, com.baiyue.firstleg.entity.InboundPalletAllocationEntity> allocationsByWeight,
|
||||
MeasureReviewRecordEntity review,
|
||||
InboundMeasurementResponses.DeclarationSnapshot latestDeclaration) {
|
||||
return new InboundMeasurementResponses.Detail(
|
||||
@@ -460,7 +479,32 @@ public class InboundMeasurementService {
|
||||
measurement != null && StringUtils.hasText(measurement.getActualGoodsName())
|
||||
? measurement.getActualGoodsName() : latestDeclaration.goodsName(),
|
||||
latestDeclaration,
|
||||
items.stream().map(this::toItem).toList(), weights.stream().map(this::toWeight).toList());
|
||||
items.stream().map(this::toItem).toList(), weights.stream()
|
||||
.map(weight -> toWeight(weight, allocationsByWeight.get(weight.getId()))).toList(),
|
||||
toPalletLocationGuide(allocationSlots));
|
||||
}
|
||||
|
||||
private static InboundMeasurementResponses.PalletLocationGuide toPalletLocationGuide(
|
||||
List<com.baiyue.firstleg.entity.InboundPalletAllocationEntity> slots) {
|
||||
if (slots.isEmpty()) return null;
|
||||
Long blockId = slots.get(0).getBlockId();
|
||||
if (blockId == null || slots.stream().anyMatch(slot -> !java.util.Objects.equals(blockId, slot.getBlockId()))) {
|
||||
throw new BizException(ResultCode.CONFLICT, "入库单整票库位块快照不一致");
|
||||
}
|
||||
List<InboundMeasurementResponses.PalletLocationSlot> ordered = slots.stream()
|
||||
.sorted(java.util.Comparator.comparing(
|
||||
com.baiyue.firstleg.entity.InboundPalletAllocationEntity::getSlotNo,
|
||||
java.util.Comparator.nullsLast(Integer::compareTo)))
|
||||
.map(slot -> new InboundMeasurementResponses.PalletLocationSlot(
|
||||
slot.getSlotNo(), slot.getReservationId(), slot.getLocationId(), slot.getLocationCode(),
|
||||
slot.getSharedOverflow(), slot.getSlotStatus(), slot.getWeightResultId(),
|
||||
slot.getPalletSequence(), slot.getPalletId(), slot.getPalletCode()))
|
||||
.toList();
|
||||
int actualPalletCount = (int) slots.stream().filter(slot -> slot.getWeightResultId() != null).count();
|
||||
int reservedEmptySlotCount = (int) slots.stream()
|
||||
.filter(slot -> "RESERVED_EMPTY".equals(slot.getSlotStatus())).count();
|
||||
return new InboundMeasurementResponses.PalletLocationGuide(
|
||||
blockId, ordered.size(), actualPalletCount, reservedEmptySlotCount, ordered);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -576,6 +620,13 @@ public class InboundMeasurementService {
|
||||
}
|
||||
|
||||
private InboundMeasurementResponses.WeightResult toWeight(InboundWeightResultEntity weight) {
|
||||
return toWeight(weight, weight.getId() == null ? null
|
||||
: palletAllocations.findByWeightResult(weight.getTenantId(), weight.getInboundId(), weight.getId()));
|
||||
}
|
||||
|
||||
private InboundMeasurementResponses.WeightResult toWeight(
|
||||
InboundWeightResultEntity weight,
|
||||
com.baiyue.firstleg.entity.InboundPalletAllocationEntity allocation) {
|
||||
return new InboundMeasurementResponses.WeightResult(
|
||||
weight.getId(), weight.getFileId(), weight.getPalletId(), weight.getPalletCode(),
|
||||
weight.getPalletType(), weight.getPalletCapacityCbm(), weight.getPalletTareWeightKg(),
|
||||
@@ -585,7 +636,15 @@ public class InboundMeasurementService {
|
||||
weight.getAdjustedWeightValue(), weight.getAdjustedWeightUnit(),
|
||||
weight.getNetWeightKg(),
|
||||
weight.getRecognitionStatus(), weight.getAdjustmentReason(), weight.getAdjustedBy(),
|
||||
weight.getAdjustedAt(), weight.getConfirmedBy(), weight.getConfirmedAt(), weight.getVersion());
|
||||
weight.getAdjustedAt(), weight.getConfirmedBy(), weight.getConfirmedAt(),
|
||||
allocation == null ? null : allocation.getPalletSequence(),
|
||||
allocation == null ? null : allocation.getBlockId(),
|
||||
allocation == null ? null : allocation.getSlotNo(),
|
||||
allocation == null ? null : allocation.getReservationId(),
|
||||
allocation == null ? null : allocation.getLocationId(),
|
||||
allocation == null ? null : allocation.getLocationCode(),
|
||||
allocation == null ? null : allocation.getSharedOverflow(),
|
||||
allocation == null ? null : allocation.getSlotStatus(), weight.getVersion());
|
||||
}
|
||||
|
||||
private PageResult<WarehousePalletLocationClient.PalletSnapshot> loadPalletSnapshotPage(
|
||||
|
||||
@@ -253,6 +253,8 @@ public class LoadingPickService {
|
||||
Task replay = replayTaskOperation(taskId, operationId, requestHash, operatorId);
|
||||
if (replay != null) return replay;
|
||||
LoadingPickTaskEntity task = requiredLoadingTaskForUpdate(taskId);
|
||||
replay = replayTaskOperationForUpdate(taskId, operationId, requestHash, operatorId);
|
||||
if (replay != null) return replay;
|
||||
LoadingPickAssignmentEntity existing = repository.assignment(taskId);
|
||||
if (existing == null) {
|
||||
if (request.expectedAssignmentVersion() != 0
|
||||
@@ -282,6 +284,8 @@ public class LoadingPickService {
|
||||
Task replay = replayTaskOperation(taskId, operationId, requestHash, operatorId);
|
||||
if (replay != null) return replay;
|
||||
LoadingPickTaskEntity task = requiredLoadingTaskForUpdate(taskId);
|
||||
replay = replayTaskOperationForUpdate(taskId, operationId, requestHash, operatorId);
|
||||
if (replay != null) return replay;
|
||||
LoadingPickAssignmentEntity existing = repository.assignment(taskId);
|
||||
int changed = existing == null
|
||||
? request.expectedAssignmentVersion() == 0
|
||||
@@ -296,10 +300,9 @@ public class LoadingPickService {
|
||||
|
||||
@Transactional
|
||||
public Task pick(long taskId, long itemId, PickRequest request, long operatorId, String operatorName) {
|
||||
ReleaseSelection release = releaseSelection(request.releaseMode(), request.locationIds());
|
||||
String operationId = request.operationId().trim();
|
||||
String requestHash = hash(taskId + "|" + itemId + "|" + request.warehouseId()
|
||||
+ "|" + request.pickedPackageCount() + "|" + release.mode() + "|" + release.json());
|
||||
+ "|" + request.pickedPackageCount());
|
||||
requireNoTaskOperation(operationId);
|
||||
LoadingPickHistoryEntity replay = repository.history(operationId);
|
||||
if (replay != null) {
|
||||
@@ -310,12 +313,17 @@ public class LoadingPickService {
|
||||
return view(requiredTask(taskId), request.warehouseId());
|
||||
}
|
||||
LoadingPickTaskEntity task = requiredAssignedLoadingTaskForUpdate(taskId, operatorId);
|
||||
requireNoTaskOperationForUpdate(operationId);
|
||||
replay = repository.historyForUpdate(operationId);
|
||||
if (replay != null) {
|
||||
assertReplay(replay, taskId, itemId, requestHash);
|
||||
return view(task, request.warehouseId());
|
||||
}
|
||||
LoadingPickItemEntity item = repository.item(taskId, itemId, request.warehouseId());
|
||||
if (item == null) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "PDA拣货明细不存在");
|
||||
}
|
||||
if (repository.pick(itemId, request.pickedPackageCount(), release.mode(), release.json(),
|
||||
request.version(), request.warehouseId()) != 1) {
|
||||
if (repository.pick(itemId, request.pickedPackageCount(), request.version(), request.warehouseId()) != 1) {
|
||||
throw conflict("拣货数量超过计划箱数或明细版本已变化");
|
||||
}
|
||||
LoadingPickHistoryEntity history = new LoadingPickHistoryEntity();
|
||||
@@ -336,10 +344,9 @@ public class LoadingPickService {
|
||||
@Transactional
|
||||
public Task partial(long taskId, long itemId, PartialPickRequest request,
|
||||
long operatorId, String operatorName) {
|
||||
ReleaseSelection release = releaseSelection(request.releaseMode(), request.locationIds());
|
||||
String operationId = request.operationId().trim();
|
||||
String requestHash = hash("PARTIAL|" + taskId + "|" + itemId + "|" + request.warehouseId()
|
||||
+ "|" + request.finalPickedPackageCount() + "|" + release.mode() + "|" + release.json());
|
||||
+ "|" + request.finalPickedPackageCount());
|
||||
requireNoTaskOperation(operationId);
|
||||
LoadingPickHistoryEntity replay = repository.history(operationId);
|
||||
if (replay != null) {
|
||||
@@ -347,9 +354,15 @@ public class LoadingPickService {
|
||||
return view(requiredTask(taskId), request.warehouseId());
|
||||
}
|
||||
LoadingPickTaskEntity task = requiredAssignedLoadingTaskForUpdate(taskId, operatorId);
|
||||
requireNoTaskOperationForUpdate(operationId);
|
||||
replay = repository.historyForUpdate(operationId);
|
||||
if (replay != null) {
|
||||
assertReplay(replay, taskId, itemId, requestHash);
|
||||
return view(task, request.warehouseId());
|
||||
}
|
||||
requireWarehouseItem(taskId, itemId, request.warehouseId());
|
||||
if (repository.partial(itemId, request.finalPickedPackageCount(), release.mode(), release.json(),
|
||||
request.version(), request.warehouseId()) != 1) {
|
||||
if (repository.partial(itemId, request.finalPickedPackageCount(), request.version(),
|
||||
request.warehouseId()) != 1) {
|
||||
throw conflict("部分拣货箱数必须大于已拣箱数且小于计划箱数,或明细版本已变化");
|
||||
}
|
||||
insertHistory(taskId, itemId, operationId, requestHash, "PARTIAL",
|
||||
@@ -372,6 +385,12 @@ public class LoadingPickService {
|
||||
return view(requiredTask(taskId), request.warehouseId());
|
||||
}
|
||||
LoadingPickTaskEntity task = requiredAssignedLoadingTaskForUpdate(taskId, operatorId);
|
||||
requireNoTaskOperationForUpdate(operationId);
|
||||
replay = repository.historyForUpdate(operationId);
|
||||
if (replay != null) {
|
||||
assertReplay(replay, taskId, itemId, requestHash);
|
||||
return view(task, request.warehouseId());
|
||||
}
|
||||
LoadingPickItemEntity item = requireWarehouseItem(taskId, itemId, request.warehouseId());
|
||||
if (repository.hasPickedOrPartialItem(taskId, item.getInboundId())) {
|
||||
throw conflict("同一入库单已存在已拣或部分拣货明细,不能跳拣并恢复智能装柜候选");
|
||||
@@ -398,7 +417,7 @@ public class LoadingPickService {
|
||||
if (replay != null) return replay;
|
||||
|
||||
LoadingPickTaskEntity task = requiredTaskForUpdate(taskId);
|
||||
replay = replayTaskOperation(taskId, operationId, requestHash, operatorId);
|
||||
replay = replayTaskOperationForUpdate(taskId, operationId, requestHash, operatorId);
|
||||
if (replay != null) return replay;
|
||||
if (!"LOADING".equals(task.getTaskStatus())) {
|
||||
throw conflict("装柜任务已完成,不能重复提交整批拣货");
|
||||
@@ -970,10 +989,8 @@ public class LoadingPickService {
|
||||
throw conflict("PDA明细版本已变化,请刷新后重试");
|
||||
}
|
||||
String action = requestItem.action().trim().toUpperCase();
|
||||
ReleaseSelection release = null;
|
||||
if ("PICK".equals(action)) {
|
||||
requireNoUnexpectedBatchFields(requestItem, false, true);
|
||||
release = releaseSelection(requestItem.releaseMode(), requestItem.locationIds());
|
||||
int current = count(item.getPickedPackageCount());
|
||||
int planned = count(item.getPlannedPackageCount());
|
||||
if (requestItem.pickedPackageCount() == null
|
||||
@@ -982,7 +999,6 @@ public class LoadingPickService {
|
||||
}
|
||||
} else if ("PARTIAL".equals(action)) {
|
||||
requireNoUnexpectedBatchFields(requestItem, true, false);
|
||||
release = releaseSelection(requestItem.releaseMode(), requestItem.locationIds());
|
||||
int current = count(item.getPickedPackageCount());
|
||||
int planned = count(item.getPlannedPackageCount());
|
||||
if (requestItem.finalPickedPackageCount() == null
|
||||
@@ -991,10 +1007,8 @@ public class LoadingPickService {
|
||||
throw conflict("PARTIAL最终箱数必须大于已拣且小于计划箱数");
|
||||
}
|
||||
} else if ("SKIP".equals(action)) {
|
||||
if (requestItem.pickedPackageCount() != null || requestItem.finalPickedPackageCount() != null
|
||||
|| StringUtils.hasText(requestItem.releaseMode())
|
||||
|| requestItem.locationIds() != null && !requestItem.locationIds().isEmpty()) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "SKIP不能携带拣货数量或释放库位");
|
||||
if (requestItem.pickedPackageCount() != null || requestItem.finalPickedPackageCount() != null) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "SKIP不能携带拣货数量");
|
||||
}
|
||||
if (!StringUtils.hasText(requestItem.reason())) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "SKIP必须填写异常原因");
|
||||
@@ -1007,7 +1021,7 @@ public class LoadingPickService {
|
||||
} else {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "action只能是PICK、PARTIAL或SKIP");
|
||||
}
|
||||
result.add(new BatchLine(item, requestItem, action, release, request.warehouseId()));
|
||||
result.add(new BatchLine(item, requestItem, action, request.warehouseId()));
|
||||
}
|
||||
result.sort(Comparator.comparing(line -> line.item().getId()));
|
||||
return result;
|
||||
@@ -1110,12 +1124,10 @@ public class LoadingPickService {
|
||||
int pickedCount;
|
||||
if ("PICK".equals(line.action())) {
|
||||
pickedCount = request.pickedPackageCount();
|
||||
changed = repository.pick(item.getId(), pickedCount, line.release().mode(),
|
||||
line.release().json(), request.version(), line.warehouseId());
|
||||
changed = repository.pick(item.getId(), pickedCount, request.version(), line.warehouseId());
|
||||
} else if ("PARTIAL".equals(line.action())) {
|
||||
pickedCount = request.finalPickedPackageCount();
|
||||
changed = repository.partial(item.getId(), pickedCount, line.release().mode(),
|
||||
line.release().json(), request.version(), line.warehouseId());
|
||||
changed = repository.partial(item.getId(), pickedCount, request.version(), line.warehouseId());
|
||||
} else {
|
||||
pickedCount = 0;
|
||||
changed = repository.skip(item.getId(), request.version(), line.warehouseId());
|
||||
@@ -1125,8 +1137,7 @@ public class LoadingPickService {
|
||||
requireNoTaskOperation(itemOperationId);
|
||||
if (repository.history(itemOperationId) != null) throw conflict("整批明细已被重复提交");
|
||||
String itemHash = hash(line.action() + '|' + item.getId() + '|' + request.version()
|
||||
+ '|' + pickedCount + '|' + (line.release() == null ? "" : line.release().mode())
|
||||
+ '|' + (line.release() == null ? "" : line.release().json()) + '|' + trim(request.reason()));
|
||||
+ '|' + pickedCount + '|' + trim(request.reason()));
|
||||
insertHistory(taskId, item.getId(), itemOperationId, itemHash, line.action(), pickedCount,
|
||||
trim(request.reason()), operatorId, operatorName);
|
||||
}
|
||||
@@ -1170,6 +1181,18 @@ public class LoadingPickService {
|
||||
private Task replayTaskOperation(long taskId, String operationId, String requestHash, long operatorId) {
|
||||
if (repository.history(operationId) != null) throw conflict("operationId已用于明细拣货操作");
|
||||
LoadingPickOperationEntity replay = repository.operation(operationId);
|
||||
return replayTaskOperation(taskId, operationId, requestHash, operatorId, replay);
|
||||
}
|
||||
|
||||
private Task replayTaskOperationForUpdate(long taskId, String operationId, String requestHash,
|
||||
long operatorId) {
|
||||
if (repository.historyForUpdate(operationId) != null) throw conflict("operationId已用于明细拣货操作");
|
||||
LoadingPickOperationEntity replay = repository.operationForUpdate(operationId);
|
||||
return replayTaskOperation(taskId, operationId, requestHash, operatorId, replay);
|
||||
}
|
||||
|
||||
private Task replayTaskOperation(long taskId, String operationId, String requestHash, long operatorId,
|
||||
LoadingPickOperationEntity replay) {
|
||||
if (replay == null) return null;
|
||||
if (!Long.valueOf(taskId).equals(replay.getPickTaskId())
|
||||
|| !Long.valueOf(operatorId).equals(replay.getOperatorId())
|
||||
@@ -1195,6 +1218,10 @@ public class LoadingPickService {
|
||||
if (repository.operation(operationId) != null) throw conflict("operationId已用于PDA任务操作");
|
||||
}
|
||||
|
||||
private void requireNoTaskOperationForUpdate(String operationId) {
|
||||
if (repository.operationForUpdate(operationId) != null) throw conflict("operationId已用于PDA任务操作");
|
||||
}
|
||||
|
||||
private static String operation(String value) {
|
||||
String result = trim(value);
|
||||
if (!StringUtils.hasText(result)) throw new BizException(ResultCode.BAD_REQUEST, "operationId不能为空");
|
||||
@@ -1250,35 +1277,14 @@ public class LoadingPickService {
|
||||
.forEach(item -> {
|
||||
canonical.append('|').append(item.itemId()).append('|').append(trim(item.action()))
|
||||
.append('|').append(item.pickedPackageCount()).append('|')
|
||||
.append(item.finalPickedPackageCount()).append('|').append(trim(item.releaseMode()))
|
||||
.append('|').append(item.version()).append('|').append(trim(item.reason())).append('|');
|
||||
List<Long> locations = item.locationIds() == null ? List.of()
|
||||
: item.locationIds().stream().sorted().toList();
|
||||
canonical.append(locations);
|
||||
.append(item.finalPickedPackageCount()).append('|')
|
||||
.append(item.version()).append('|').append(trim(item.reason()));
|
||||
});
|
||||
return hash(canonical.toString());
|
||||
}
|
||||
|
||||
private static ReleaseSelection releaseSelection(String requestedMode, List<Long> requestedLocationIds) {
|
||||
String mode = requestedMode == null || requestedMode.isBlank()
|
||||
? (requestedLocationIds == null ? "AUTO" : "MANUAL") : requestedMode.trim().toUpperCase();
|
||||
if (!"AUTO".equals(mode) && !"MANUAL".equals(mode)) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "releaseMode只能是AUTO或MANUAL");
|
||||
}
|
||||
List<Long> ids = requestedLocationIds == null ? List.of() : requestedLocationIds.stream().sorted().toList();
|
||||
if (new HashSet<>(ids).size() != ids.size()) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "释放库位不能重复");
|
||||
}
|
||||
if ("AUTO".equals(mode) && !ids.isEmpty()) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "AUTO模式不能指定释放库位");
|
||||
}
|
||||
String json = ids.stream().map(String::valueOf).collect(java.util.stream.Collectors.joining(",", "[", "]"));
|
||||
return new ReleaseSelection(mode, json);
|
||||
}
|
||||
|
||||
private record BatchLine(LoadingPickItemEntity item, BatchItemRequest request, String action,
|
||||
ReleaseSelection release, long warehouseId) {}
|
||||
private record ReleaseSelection(String mode, String json) {}
|
||||
long warehouseId) {}
|
||||
|
||||
private static String trim(String value) {
|
||||
return value == null ? null : value.trim();
|
||||
|
||||
@@ -62,7 +62,7 @@ public class MeasurementBatchService {
|
||||
}
|
||||
validateSource(command);
|
||||
List<InboundMeasurementItemEntity> calculatedItems = calculateItems(
|
||||
command, dimensionWeighting.load(command.tenantId()), inbound.getCustomerId());
|
||||
command, dimensionWeighting.load(command.tenantId()));
|
||||
|
||||
InboundMeasurementEntity measurement;
|
||||
if (command.measurementId() == null) {
|
||||
@@ -176,8 +176,7 @@ public class MeasurementBatchService {
|
||||
}
|
||||
|
||||
private List<InboundMeasurementItemEntity> calculateItems(
|
||||
MeasurementBatchCommand command, CustomerDimensionWeightingService.RuleSet dimensionRules,
|
||||
Long customerId) {
|
||||
MeasurementBatchCommand command, CustomerDimensionWeightingService.RuleSet dimensionRules) {
|
||||
Set<String> itemNumbers = new HashSet<>();
|
||||
return command.items().stream().map(source -> {
|
||||
if (!itemNumbers.add(source.itemNo())) {
|
||||
@@ -199,8 +198,8 @@ public class MeasurementBatchService {
|
||||
baseLength, baseWidth, baseHeight);
|
||||
String requestedMode = normalizeMode(source.coefficientMode());
|
||||
CustomerChargeRuleEntity coefficientRule = "MULTIPLY".equals(requestedMode)
|
||||
? requireCoefficientRule(command.tenantId(), customerId, source.chargeRuleId())
|
||||
: requestedMode == null ? findDefaultCoefficientRule(command.tenantId(), customerId) : null;
|
||||
? requireCoefficientRule(command.tenantId(), source.chargeRuleId())
|
||||
: requestedMode == null ? findDefaultCoefficientRule(command.tenantId()) : null;
|
||||
String coefficientMode = coefficientRule != null ? "MULTIPLY" : requestedMode;
|
||||
boolean useDimensionRule = "ADD".equals(coefficientMode);
|
||||
BigDecimal addition = weighted.edgeAdditionCm() == null
|
||||
@@ -293,15 +292,13 @@ public class MeasurementBatchService {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private CustomerChargeRuleEntity findDefaultCoefficientRule(long tenantId, Long customerId) {
|
||||
if (customerId == null || customerId <= 0) return null;
|
||||
return pricingRepository.findDefaultRule(tenantId, customerId);
|
||||
private CustomerChargeRuleEntity findDefaultCoefficientRule(long tenantId) {
|
||||
return pricingRepository.findDefaultRule(tenantId, tenantId);
|
||||
}
|
||||
|
||||
private CustomerChargeRuleEntity requireCoefficientRule(long tenantId, Long customerId, Long ruleId) {
|
||||
Long effectiveCustomerId = tenantId;
|
||||
private CustomerChargeRuleEntity requireCoefficientRule(long tenantId, Long ruleId) {
|
||||
CustomerChargeRuleEntity rule = ruleId == null
|
||||
? pricingRepository.findDefaultRule(tenantId, effectiveCustomerId)
|
||||
? pricingRepository.findDefaultRule(tenantId, tenantId)
|
||||
: pricingRepository.findRule(tenantId, ruleId);
|
||||
if (rule == null || !"ACTIVE".equals(rule.getStatus())
|
||||
|| !java.util.Objects.equals(rule.getTenantId(), tenantId)) {
|
||||
|
||||
+55
-20
@@ -41,30 +41,33 @@ public class MeasurementLocationOccupationService {
|
||||
int measurementVersion) {
|
||||
InboundOrderEntity order = requireOrder(tenantId, inboundId);
|
||||
long warehouseId = requireWarehouse(order);
|
||||
long resourceTenantId = PLATFORM_WAREHOUSE_RESOURCE_TENANT_ID;
|
||||
BigDecimal volume = requireVolume(order);
|
||||
var palletReferences = confirmedPalletReferences(tenantId, inboundId);
|
||||
// 补偿释放后的预占记录需要保留审计,失败重试必须使用新的物理预占键;
|
||||
// 若仍存在活动预占,仓储侧会按业务单据复用,不会创建新的资源占用。
|
||||
String key = "measure-" + inboundId + "-" + measurementId + "-" + measurementVersion
|
||||
var confirmedWeights = confirmedWeights(tenantId, inboundId);
|
||||
int requiredSlotCount = requiredSlotCount(volume, confirmedWeights.size());
|
||||
// Warehouse retains released blocks for audit. A compensated attempt therefore needs a new
|
||||
// reservation key, otherwise a retry would replay the released block instead of reserving again.
|
||||
String key = "measure-block-" + inboundId + "-" + measurementId + "-" + measurementVersion
|
||||
+ "-" + UUID.randomUUID();
|
||||
WarehousePalletLocationClient.BatchResult reserved = requireSuccess(call(() -> warehouse.reserve(
|
||||
internalToken, new WarehousePalletLocationClient.ReserveRequest(resourceTenantId, warehouseId,
|
||||
SOURCE_SERVICE, inboundId, order.getInboundCode(), key, operatorId, volume,
|
||||
palletReferences,
|
||||
LocalDateTime.now().plus(properties.getReservationTtl()), true))),
|
||||
WarehousePalletLocationClient.BlockResult reserved = requireSuccess(call(() -> warehouse.reserveBlock(
|
||||
internalToken, new WarehousePalletLocationClient.BlockReserveRequest(warehouseId,
|
||||
SOURCE_SERVICE, inboundId, order.getInboundCode(), requiredSlotCount, key, operatorId,
|
||||
LocalDateTime.now().plus(properties.getReservationTtl())))),
|
||||
"测量完成后库位预占失败");
|
||||
int packageCount = requirePackageCount(order);
|
||||
allocations.replace(tenantId, inboundId, warehouseId, resourceTenantId, packageCount, reserved);
|
||||
requireBlockResult(reserved, requiredSlotCount, "RESERVED");
|
||||
allocations.replaceBlock(tenantId, inboundId, warehouseId, packageCount, volume, reserved);
|
||||
allocations.bindConfirmedWeights(tenantId, inboundId, confirmedWeights);
|
||||
try {
|
||||
WarehousePalletLocationClient.BatchResult occupied = requireSuccess(call(() -> warehouse.confirm(
|
||||
internalToken, new WarehousePalletLocationClient.ConfirmRequest(resourceTenantId, warehouseId,
|
||||
SOURCE_SERVICE, inboundId, operatorId, volume, packageCount))), "测量完成后库位占用失败");
|
||||
allocations.replace(tenantId, inboundId, warehouseId, resourceTenantId, packageCount, occupied);
|
||||
WarehousePalletLocationClient.BlockResult occupied = requireSuccess(call(() -> warehouse.confirmBlock(
|
||||
internalToken, new WarehousePalletLocationClient.BlockConfirmRequest(reserved.blockId(), inboundId,
|
||||
key + "-confirm", operatorId))), "测量完成后库位占用失败");
|
||||
requireBlockResult(occupied, requiredSlotCount, "OCCUPIED");
|
||||
allocations.replaceBlock(tenantId, inboundId, warehouseId, packageCount, volume, occupied);
|
||||
allocations.bindConfirmedWeights(tenantId, inboundId, confirmedWeights);
|
||||
} catch (RuntimeException failure) {
|
||||
try {
|
||||
warehouse.release(internalToken, new WarehousePalletLocationClient.ReleaseRequest(
|
||||
resourceTenantId, SOURCE_SERVICE, inboundId, operatorId, "测量完成库位占用失败"));
|
||||
warehouse.releaseBlock(internalToken, new WarehousePalletLocationClient.BlockReleaseRequest(
|
||||
reserved.blockId(), inboundId, key + "-compensate", operatorId, "MANUAL_ADJUSTMENT"));
|
||||
} catch (RuntimeException releaseFailure) {
|
||||
failure.addSuppressed(releaseFailure);
|
||||
}
|
||||
@@ -99,9 +102,9 @@ public class MeasurementLocationOccupationService {
|
||||
return order.getTotalPackageCount();
|
||||
}
|
||||
|
||||
private java.util.List<WarehousePalletLocationClient.PalletReference> confirmedPalletReferences(
|
||||
private java.util.List<InboundWeightResultEntity> confirmedWeights(
|
||||
long tenantId, long inboundId) {
|
||||
java.util.List<WarehousePalletLocationClient.PalletReference> references = new java.util.ArrayList<>();
|
||||
java.util.List<InboundWeightResultEntity> references = new java.util.ArrayList<>();
|
||||
for (InboundWeightResultEntity weight : measurements.listWeights(tenantId, inboundId)) {
|
||||
if (!"CONFIRMED".equals(weight.getRecognitionStatus())) {
|
||||
continue;
|
||||
@@ -118,7 +121,7 @@ public class MeasurementLocationOccupationService {
|
||||
if (weight.getId() == null || weight.getId() <= 0) {
|
||||
throw new BizException(ResultCode.CONFLICT, "已确认称重图片缺少结果标识,无法分配库位");
|
||||
}
|
||||
references.add(new WarehousePalletLocationClient.PalletReference(weight.getId(), weight.getPalletId()));
|
||||
references.add(weight);
|
||||
}
|
||||
if (references.isEmpty()) {
|
||||
throw new BizException(ResultCode.CONFLICT, "至少需要一张已确认的称重图片才能提交测量");
|
||||
@@ -129,6 +132,38 @@ public class MeasurementLocationOccupationService {
|
||||
return references;
|
||||
}
|
||||
|
||||
private int requiredSlotCount(BigDecimal volume, int actualPalletCount) {
|
||||
BigDecimal capacity = properties.getDefaultPalletCapacityCbm();
|
||||
if (capacity == null || capacity.signum() <= 0) {
|
||||
throw new BizException(ResultCode.CONFLICT, "默认托盘容量配置无效,无法计算整票预留库位数");
|
||||
}
|
||||
int byVolume;
|
||||
try {
|
||||
byVolume = volume.divide(capacity, 0, java.math.RoundingMode.CEILING).intValueExact();
|
||||
} catch (ArithmeticException exception) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "整票预留库位数超过系统上限");
|
||||
}
|
||||
int required = Math.max(actualPalletCount, byVolume);
|
||||
if (required < 1 || required > 200) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "整票预留库位数必须在1到200之间");
|
||||
}
|
||||
return required;
|
||||
}
|
||||
|
||||
private static void requireBlockResult(WarehousePalletLocationClient.BlockResult result,
|
||||
int requiredSlotCount, String expectedStatus) {
|
||||
if (result.blockId() <= 0 || !expectedStatus.equals(result.blockStatus())
|
||||
|| result.resources() == null || result.resources().size() != requiredSlotCount
|
||||
|| result.resources().stream().anyMatch(resource -> resource.blockId() != result.blockId())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "Warehouse返回的整票库位块不完整");
|
||||
}
|
||||
java.util.List<Integer> slotNos = result.resources().stream()
|
||||
.map(WarehousePalletLocationClient.BlockResource::slotNo).sorted().toList();
|
||||
if (!slotNos.equals(java.util.stream.IntStream.rangeClosed(1, requiredSlotCount).boxed().toList())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "Warehouse返回的整票库位槽序不连续");
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> ApiResponse<T> call(RemoteCall<T> call) {
|
||||
try { return call.invoke(); }
|
||||
catch (FeignException exception) {
|
||||
|
||||
@@ -153,18 +153,12 @@ public class PdaPickSettlementService {
|
||||
confirmedAllocations.sort(Comparator.comparing(InboundPalletAllocationEntity::getAllocationNo,
|
||||
Comparator.nullsLast(Integer::compareTo)));
|
||||
consumePickedAllocations(parent.tenantId(), parentInboundId, confirmedAllocations, pickedPackageCount);
|
||||
List<Long> releasedReservationIds = releasedReservationIds(confirmedAllocations);
|
||||
List<WarehousePalletLocationClient.SettlementTransferItem> transferItems =
|
||||
remainingTransferItems(confirmedAllocations);
|
||||
int allocationRemaining = transferItems.stream()
|
||||
.mapToInt(WarehousePalletLocationClient.SettlementTransferItem::remainingPackageCount).sum();
|
||||
long blockId = blockId(confirmedAllocations);
|
||||
int allocationRemaining = confirmedAllocations.stream().mapToInt(PdaPickSettlementService::remaining).sum();
|
||||
if (allocationRemaining != childPackageCount) {
|
||||
throw conflict("父入库单库位剩余箱数与未装柜箱数不一致");
|
||||
}
|
||||
long warehouseId = warehouseId(confirmedAllocations);
|
||||
if (childPackageCount == 0 && (!transferItems.isEmpty() || releasedReservationIds.isEmpty())) {
|
||||
throw conflict("全量拣货后库位资源结算不完整");
|
||||
}
|
||||
warehouseId(confirmedAllocations);
|
||||
|
||||
List<MeasurementItemRow> measurements = splits.lockLatestApprovedItemsForInbound(
|
||||
parent.tenantId(), parentInboundId);
|
||||
@@ -193,21 +187,13 @@ public class PdaPickSettlementService {
|
||||
throw conflict("主入库单测量明细剩余箱数与未装柜箱数不一致");
|
||||
}
|
||||
|
||||
if (childPackageCount > 0 && transferItems.isEmpty()) {
|
||||
throw conflict("父入库单可转移库位剩余箱数与未装柜箱数不一致");
|
||||
}
|
||||
|
||||
if (root == null || !"MASTER".equals(root.inboundRole())) throw conflict("根主入库单不存在");
|
||||
Map<Long, Integer> orderQuantities = dropshipWholePallet
|
||||
? new LinkedHashMap<>(pickedByDetail) : measurementQuantities(measurementLines);
|
||||
OrderFulfillmentSplitClient.Response orderSplit = createOrderSplit(parent, root, orderQuantities,
|
||||
pickedPackageCount, childPackageCount, loadingPlanId, containerBookingId, operationId,
|
||||
"pda-task-" + taskId, operatorId, operatorName);
|
||||
String settlementKey = "PDA-SETTLE-" + taskId + '-' + parentInboundId;
|
||||
long remainderChildId = childPackageCount > 0 ? existingSettlementTarget(parentInboundId, settlementKey) : 0L;
|
||||
if (childPackageCount > 0 && remainderChildId == 0L) {
|
||||
remainderChildId = IdWorker.getId();
|
||||
}
|
||||
long remainderChildId = childPackageCount > 0 ? IdWorker.getId() : 0L;
|
||||
long batchId = IdWorker.getId();
|
||||
String idempotencyKey = "pda-task-" + taskId;
|
||||
int rootVersion = root.version() == null ? 0 : root.version();
|
||||
@@ -278,15 +264,14 @@ public class PdaPickSettlementService {
|
||||
"PDA拣货后来源单承接实装并生成余量子入库单"), "拆分审计");
|
||||
|
||||
if (remainderChildId != 0L) {
|
||||
allocations.copyRemainingToChild(parent.tenantId(), remainderChildId, confirmedAllocations);
|
||||
allocations.copyBlockToChild(parent.tenantId(), remainderChildId, confirmedAllocations);
|
||||
allocations.markBlockTransferred(parent.tenantId(), parentInboundId, blockId);
|
||||
} else {
|
||||
allocations.markBlockReleased(parent.tenantId(), parentInboundId, blockId);
|
||||
}
|
||||
allocations.markTransferred(parent.tenantId(), parentInboundId, confirmedAllocations,
|
||||
releasedReservationIds);
|
||||
allocations.markReleased(parent.tenantId(), parentInboundId, releasedReservationIds);
|
||||
settleWarehouseResources(parentInboundId, remainderChildId,
|
||||
settleWarehouseBlock(parentInboundId, remainderChildId,
|
||||
remainderChildCode == null ? root.inboundCode() : remainderChildCode,
|
||||
taskId, operatorId, operatorName,
|
||||
warehouseId, parentAvailable, childPackageCount, releasedReservationIds, transferItems);
|
||||
taskId, operatorId, blockId);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -433,28 +418,17 @@ public class PdaPickSettlementService {
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Long> releasedReservationIds(List<InboundPalletAllocationEntity> allocations) {
|
||||
return allocations.stream().filter(item -> remaining(item) == 0).map(PdaPickSettlementService::reservationId)
|
||||
.distinct().sorted().toList();
|
||||
}
|
||||
|
||||
private static List<WarehousePalletLocationClient.SettlementTransferItem> remainingTransferItems(
|
||||
List<InboundPalletAllocationEntity> allocations) {
|
||||
return allocations.stream().filter(item -> remaining(item) > 0)
|
||||
.map(item -> new WarehousePalletLocationClient.SettlementTransferItem(reservationId(item), remaining(item)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private static int remaining(InboundPalletAllocationEntity allocation) {
|
||||
return count(allocation.getAllocatedPackageCount(), "库位已分配箱数")
|
||||
- (allocation.getPickedPackageCount() == null ? 0 : allocation.getPickedPackageCount());
|
||||
}
|
||||
|
||||
private static long reservationId(InboundPalletAllocationEntity allocation) {
|
||||
if (allocation.getReservationId() == null || allocation.getReservationId() <= 0) {
|
||||
throw conflict("库位资源缺少唯一reservationId,请先重新确认托盘库位分配");
|
||||
private static long blockId(List<InboundPalletAllocationEntity> allocations) {
|
||||
if (allocations.isEmpty() || allocations.get(0).getBlockId() == null || allocations.get(0).getBlockId() <= 0
|
||||
|| allocations.stream().anyMatch(item -> !Objects.equals(allocations.get(0).getBlockId(), item.getBlockId()))) {
|
||||
throw conflict("PDA结算要求父入库单资源属于同一整票库位块");
|
||||
}
|
||||
return allocation.getReservationId();
|
||||
return allocations.get(0).getBlockId();
|
||||
}
|
||||
|
||||
private static long warehouseId(List<InboundPalletAllocationEntity> allocations) {
|
||||
@@ -465,36 +439,33 @@ public class PdaPickSettlementService {
|
||||
return allocations.get(0).getWarehouseId();
|
||||
}
|
||||
|
||||
private void settleWarehouseResources(long parentInboundId, long childInboundId, String childCode,
|
||||
long taskId, long operatorId, String operatorName, long warehouseId,
|
||||
int sourcePackageCount, int targetPackageCount,
|
||||
List<Long> releasedReservationIds,
|
||||
List<WarehousePalletLocationClient.SettlementTransferItem> remainingItems) {
|
||||
ApiResponse<WarehousePalletLocationClient.SettlementResult> response;
|
||||
private void settleWarehouseBlock(long parentInboundId, long childInboundId, String childCode,
|
||||
long taskId, long operatorId, long blockId) {
|
||||
ApiResponse<WarehousePalletLocationClient.BlockResult> response;
|
||||
String key = "PDA-BLOCK-" + taskId + '-' + parentInboundId;
|
||||
try {
|
||||
response = warehouse.settle(internalToken, new WarehousePalletLocationClient.SettlementRequest(
|
||||
warehouseId, SOURCE_SERVICE, parentInboundId, childInboundId,
|
||||
sourcePackageCount, targetPackageCount, childCode, "PDA-SETTLE-" + taskId + '-' + parentInboundId,
|
||||
operatorId, operatorName, "PDA-SETTLE-" + taskId + '-' + parentInboundId,
|
||||
releasedReservationIds, remainingItems));
|
||||
response = childInboundId > 0
|
||||
? warehouse.transferBlock(internalToken, new WarehousePalletLocationClient.BlockTransferRequest(
|
||||
blockId, parentInboundId, childInboundId, childCode, key, operatorId))
|
||||
: warehouse.releaseBlock(internalToken, new WarehousePalletLocationClient.BlockReleaseRequest(
|
||||
blockId, parentInboundId, key, operatorId, "ALL_PACKAGES_PICKED"));
|
||||
} catch (FeignException exception) {
|
||||
log.warn("PDA warehouse settlement transport failure taskId={} sourceInboundId={} operationId={} "
|
||||
+ "httpStatus={} releasedReservationIds={} remainingReservations={}",
|
||||
taskId, parentInboundId, "PDA-SETTLE-" + taskId + '-' + parentInboundId,
|
||||
exception.status(), releasedReservationIds, settlementTransferFacts(remainingItems));
|
||||
log.warn("PDA warehouse block settlement transport failure taskId={} sourceInboundId={} "
|
||||
+ "blockId={} operationId={} httpStatus={}",
|
||||
taskId, parentInboundId, blockId, key, exception.status());
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "仓储服务不可用,无法结算PDA库位资源");
|
||||
}
|
||||
if (response == null || !response.successful() || response.data() == null
|
||||
|| response.data().released() == null || response.data().transferred() == null
|
||||
|| !sameReleasedReservations(releasedReservationIds, response.data().released())
|
||||
|| !sameTransferredResources(remainingItems, response.data().transferred())) {
|
||||
log.warn("PDA warehouse settlement rejected taskId={} sourceInboundId={} operationId={} "
|
||||
+ "responseCode={} requestId={} traceId={} expectedAction=RELEASE_OR_TRANSFER "
|
||||
+ "releasedReservationIds={} remainingReservations={}",
|
||||
taskId, parentInboundId, "PDA-SETTLE-" + taskId + '-' + parentInboundId,
|
||||
response == null ? null : response.code(), response == null ? null : response.requestId(),
|
||||
response == null ? null : response.traceId(), releasedReservationIds,
|
||||
settlementTransferFacts(remainingItems));
|
||||
|| response.data().blockId() != blockId || response.data().resources() == null
|
||||
|| response.data().resources().isEmpty()
|
||||
|| response.data().resources().stream().anyMatch(resource -> resource.blockId() != blockId)
|
||||
|| !(childInboundId > 0 ? "OCCUPIED" : "RELEASED").equals(response.data().blockStatus())
|
||||
|| response.data().resources().stream().anyMatch(resource ->
|
||||
!(childInboundId > 0 ? "OCCUPIED" : "RELEASED").equals(resource.resourceStatus()))) {
|
||||
log.warn("PDA warehouse block settlement rejected taskId={} sourceInboundId={} blockId={} "
|
||||
+ "operationId={} responseCode={} requestId={} traceId={}",
|
||||
taskId, parentInboundId, blockId, key, response == null ? null : response.code(),
|
||||
response == null ? null : response.requestId(), response == null ? null : response.traceId());
|
||||
ResultCode code = response == null ? ResultCode.SERVICE_UNAVAILABLE : ResultCode.of(response.code());
|
||||
throw new SettlementFailure(code, response == null || response.message() == null
|
||||
? "仓储服务原子结算PDA库位资源失败" : response.message(),
|
||||
@@ -517,40 +488,6 @@ public class PdaPickSettlementService {
|
||||
public String traceId() { return traceId; }
|
||||
}
|
||||
|
||||
private static List<String> settlementTransferFacts(
|
||||
List<WarehousePalletLocationClient.SettlementTransferItem> remainingItems) {
|
||||
return remainingItems.stream().map(item -> item.reservationId() + ":" + item.remainingPackageCount())
|
||||
.sorted().toList();
|
||||
}
|
||||
|
||||
private long existingSettlementTarget(long sourceInboundId, String idempotencyKey) {
|
||||
try {
|
||||
ApiResponse<Long> response = warehouse.settlementTarget(internalToken, sourceInboundId, idempotencyKey);
|
||||
return response != null && response.successful() && response.data() != null
|
||||
? response.data() : 0L;
|
||||
} catch (FeignException exception) {
|
||||
return 0L;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean sameReleasedReservations(List<Long> expected,
|
||||
List<WarehousePalletLocationClient.SelectiveReleaseResult> actual) {
|
||||
return expected.equals(actual.stream().map(WarehousePalletLocationClient.SelectiveReleaseResult::reservationId)
|
||||
.sorted().toList());
|
||||
}
|
||||
|
||||
private static boolean sameTransferredResources(List<WarehousePalletLocationClient.SettlementTransferItem> expected,
|
||||
List<WarehousePalletLocationClient.TransferResult> actual) {
|
||||
Map<Long, Integer> expectedByLocation = expected.stream().collect(Collectors.toMap(
|
||||
WarehousePalletLocationClient.SettlementTransferItem::reservationId,
|
||||
WarehousePalletLocationClient.SettlementTransferItem::remainingPackageCount));
|
||||
Map<Long, Integer> actualByLocation = actual.stream().collect(Collectors.toMap(
|
||||
WarehousePalletLocationClient.TransferResult::reservationId,
|
||||
WarehousePalletLocationClient.TransferResult::remainingPackageCount,
|
||||
(left, right) -> -1));
|
||||
return expectedByLocation.equals(actualByLocation);
|
||||
}
|
||||
|
||||
static String childInboundCode(String rootInboundCode, int splitSequence) {
|
||||
if (splitSequence <= 0) {
|
||||
throw conflict("子入库单拆分序号不合法");
|
||||
|
||||
@@ -125,24 +125,28 @@ public class SmartLoadingService {
|
||||
? EnumSet.allOf(CandidateOrderStatus.class)
|
||||
: EnumSet.copyOf(query.orderStatuses());
|
||||
boolean includePending = selected.remove(CandidateOrderStatus.PENDING_REVIEW);
|
||||
InboundCandidateQuery firstlegQuery = withStatuses(query, List.copyOf(selected), 1, 1);
|
||||
if (!includePending) return firstlegCandidates(withStatuses(query, List.copyOf(selected),
|
||||
query.page(), query.size()), actualChannels);
|
||||
if (selected.isEmpty()) return pendingReviewCandidates(query, actualChannels, query.page(), query.size());
|
||||
|
||||
PageResult<InboundCandidate> pendingCount = pendingReviewCandidates(query, actualChannels, 1, 1);
|
||||
PageResult<InboundCandidate> firstlegCount = firstlegCandidates(firstlegQuery, actualChannels);
|
||||
long total = pendingCount.total() + firstlegCount.total();
|
||||
long offset = Math.multiplyExact(query.page() - 1, query.size());
|
||||
PageResult<InboundCandidate> pendingPage = pendingReviewCandidates(query, actualChannels,
|
||||
query.page(), query.size());
|
||||
List<InboundCandidate> records = new ArrayList<>();
|
||||
if (offset < pendingCount.total()) {
|
||||
records.addAll(pendingReviewRange(query, actualChannels, offset, query.size()));
|
||||
}
|
||||
long firstlegOffset = Math.max(0, offset - pendingCount.total());
|
||||
int remaining = Math.toIntExact(query.size() - records.size());
|
||||
if (remaining > 0 && firstlegOffset < firstlegCount.total()) {
|
||||
records.addAll(firstlegRange(query, actualChannels, List.copyOf(selected), firstlegOffset, remaining));
|
||||
PageResult<InboundCandidate> firstlegPage;
|
||||
if (offset < pendingPage.total()) {
|
||||
records.addAll(pendingPage.records());
|
||||
int remaining = Math.toIntExact(query.size() - records.size());
|
||||
firstlegPage = firstlegCandidates(withStatuses(query, List.copyOf(selected), 1,
|
||||
Math.max(1, remaining)), actualChannels);
|
||||
if (remaining > 0) records.addAll(firstlegPage.records());
|
||||
} else {
|
||||
long firstlegOffset = offset - pendingPage.total();
|
||||
firstlegPage = firstlegRange(query, actualChannels, List.copyOf(selected),
|
||||
firstlegOffset, query.size());
|
||||
records.addAll(firstlegPage.records());
|
||||
}
|
||||
long total = pendingPage.total() + firstlegPage.total();
|
||||
long pages = total == 0 ? 0 : (total + query.size() - 1) / query.size();
|
||||
return new PageResult<>(records, query.page(), query.size(), total, pages);
|
||||
}
|
||||
@@ -184,7 +188,7 @@ public class SmartLoadingService {
|
||||
try {
|
||||
response = pendingReviewOrders.page(internalToken, List.copyOf(actualChannels), query.warehouseId(),
|
||||
query.customerId(), query.inboundCode(), isoDate(query.createdStart()), isoDate(query.createdEnd()),
|
||||
page, size);
|
||||
repository.occupiedOrderIds(query, actualChannels), page, size);
|
||||
} catch (RuntimeException exception) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "待审核订单服务暂时不可用");
|
||||
}
|
||||
@@ -205,27 +209,20 @@ public class SmartLoadingService {
|
||||
return value == null ? null : value.toString();
|
||||
}
|
||||
|
||||
private List<InboundCandidate> pendingReviewRange(InboundCandidateQuery query, Set<String> channels,
|
||||
long offset, long limit) {
|
||||
return pageRange(offset, limit, (page, size) -> pendingReviewCandidates(query, channels, page, size));
|
||||
}
|
||||
|
||||
private List<InboundCandidate> firstlegRange(InboundCandidateQuery query, Set<String> channels,
|
||||
List<CandidateOrderStatus> statuses,
|
||||
long offset, long limit) {
|
||||
return pageRange(offset, limit, (page, size) -> firstlegCandidates(
|
||||
withStatuses(query, statuses, page, size), channels));
|
||||
}
|
||||
|
||||
private static List<InboundCandidate> pageRange(long offset, long limit,
|
||||
java.util.function.BiFunction<Long, Long, PageResult<InboundCandidate>> source) {
|
||||
if (limit <= 0) return List.of();
|
||||
private PageResult<InboundCandidate> firstlegRange(InboundCandidateQuery query, Set<String> channels,
|
||||
List<CandidateOrderStatus> statuses,
|
||||
long offset, long limit) {
|
||||
if (limit <= 0) return new PageResult<>(List.of(), 1, 1, 0, 0);
|
||||
final long chunk = 200;
|
||||
long page = offset / chunk + 1;
|
||||
int skip = Math.toIntExact(offset % chunk);
|
||||
List<InboundCandidate> result = new ArrayList<>();
|
||||
long total = 0;
|
||||
while (result.size() < limit) {
|
||||
List<InboundCandidate> rows = source.apply(page++, chunk).records();
|
||||
PageResult<InboundCandidate> sourcePage = firstlegCandidates(
|
||||
withStatuses(query, statuses, page++, chunk), channels);
|
||||
total = sourcePage.total();
|
||||
List<InboundCandidate> rows = sourcePage.records();
|
||||
if (rows.isEmpty()) break;
|
||||
int from = Math.min(skip, rows.size());
|
||||
int take = Math.min(Math.toIntExact(limit - result.size()), rows.size() - from);
|
||||
@@ -233,7 +230,8 @@ public class SmartLoadingService {
|
||||
skip = 0;
|
||||
if (rows.size() < chunk) break;
|
||||
}
|
||||
return List.copyOf(result);
|
||||
return new PageResult<>(List.copyOf(result), query.page(), query.size(), total,
|
||||
total == 0 ? 0 : (total + query.size() - 1) / query.size());
|
||||
}
|
||||
|
||||
private static InboundCandidateQuery withStatuses(InboundCandidateQuery source,
|
||||
@@ -420,7 +418,6 @@ public class SmartLoadingService {
|
||||
request.inboundDetailId());
|
||||
if (candidate == null) throw conflict("入库货物不存在或尚未完成入库");
|
||||
if (!wholeGroupAssignment) requireStandalone(candidate.accountId(), candidate.inboundId());
|
||||
requireSameChannel(booking, candidate);
|
||||
requireSameCustomerScope(booking, candidate);
|
||||
var task = loadingPickService.mutationSnapshot(planId);
|
||||
if (task.taskId() != null && "LOADING".equals(task.taskStatus())) {
|
||||
@@ -525,7 +522,6 @@ public class SmartLoadingService {
|
||||
for (InboundAllocation allocation : allocations) {
|
||||
if (allocation.packageCount() == 0) continue;
|
||||
Candidate candidate = allocation.candidate();
|
||||
requireSameChannel(booking, candidate);
|
||||
requireSameCustomerScope(booking, candidate);
|
||||
checkRouteAndCapacity(booking, current, candidate, allocation.packageCount(), true);
|
||||
OrderCargoClient.CargoBinding cargo = declared.stream()
|
||||
@@ -581,7 +577,7 @@ public class SmartLoadingService {
|
||||
LoadingPlanEntity plan = editing(planId, request.expectedVersion(), operatorId, operatorName);
|
||||
ContainerBookingEntity booking = requireBooking(plan.getContainerBookingId());
|
||||
List<Candidate> details = frozenDetails == null
|
||||
? candidates(plan.getContainerBookingId(), booking.getWarehouseId()).stream()
|
||||
? manualCandidates(booking).stream()
|
||||
.filter(candidate -> Objects.equals(candidate.inboundId(), request.inboundId())).toList()
|
||||
: List.copyOf(frozenDetails);
|
||||
if (details.isEmpty()) throw conflict("入仓码不存在、已分配完或尚未完成入库");
|
||||
@@ -641,7 +637,6 @@ public class SmartLoadingService {
|
||||
for (InboundAllocation allocation : allocations) {
|
||||
if (allocation.packageCount() == 0) continue;
|
||||
Candidate candidate = allocation.candidate();
|
||||
requireSameChannel(booking, candidate);
|
||||
requireSameCustomerScope(booking, candidate);
|
||||
checkRouteAndCapacity(booking, current, candidate, allocation.packageCount(), true);
|
||||
OrderCargoClient.CargoBinding cargo = declared.stream().map(DeclaredCargo::cargo)
|
||||
@@ -852,8 +847,14 @@ public class SmartLoadingService {
|
||||
ContainerBookingEntity booking = requireBooking(plan.getContainerBookingId());
|
||||
Candidate candidate = repository.candidate(planId, booking.getWarehouseId(), item.getInboundId(),
|
||||
item.getInboundDetailId());
|
||||
if (candidate == null || request.packageCount() > candidate.availablePackageCount() + item.getPackageCount()) throw validation("调整箱数超过可用箱数");
|
||||
requireSameChannel(booking, candidate);
|
||||
if (candidate == null) throw validation("调整箱数超过可用箱数");
|
||||
int currentPackageCount = item.getPackageCount() == null ? 0 : item.getPackageCount();
|
||||
int remainingPackageCount = Math.max(0, candidate.availablePackageCount() == null
|
||||
? 0 : candidate.availablePackageCount());
|
||||
if (request.packageCount() > currentPackageCount
|
||||
&& request.packageCount() - currentPackageCount > remainingPackageCount) {
|
||||
throw validation("调整箱数超过可用箱数");
|
||||
}
|
||||
requireSameCustomerScope(booking, candidate);
|
||||
OrderCargoClient.CargoBinding cargo = loadCargo(candidate);
|
||||
List<LoadingPlanItemEntity> others = new ArrayList<>(repository.items(planId));
|
||||
@@ -945,6 +946,84 @@ public class SmartLoadingService {
|
||||
return view(repository.plan(planId), requireBooking(plan.getContainerBookingId()));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@BusinessAudit(event = "SMART_LOADING_UNPICKED_REMOVE", resourceType = "LOADING_PLAN",
|
||||
resourceId = "#planId", description = "批量移除智能装柜未拣明细")
|
||||
public Plan removeUnpicked(long planId, RemoveUnpickedRequest request,
|
||||
long operatorId, String operatorName) {
|
||||
String operation = operation(request.operationId());
|
||||
String requestHash = sha256("REMOVE_UNPICKED|" + planId + '|' + request.expectedPlanVersion());
|
||||
Plan replay = replay(operation, planId, "REMOVE_UNPICKED", null, requestHash);
|
||||
if (replay != null) return replay;
|
||||
LoadingPlanEntity plan = editing(planId, request.expectedPlanVersion(), operatorId, operatorName);
|
||||
ContainerBookingEntity booking = requireBooking(plan.getContainerBookingId());
|
||||
List<LoadingPlanItemEntity> items = repository.items(planId);
|
||||
LoadingPickService.MutationSnapshot pickSnapshot = loadingPickService.mutationSnapshot(planId);
|
||||
Map<Long, Integer> picked = pickSnapshot == null ? Map.of() : pickSnapshot.pickedByPlanItem();
|
||||
|
||||
Map<String, GroupSnapshot> groupByMember = new HashMap<>();
|
||||
Map<Long, GroupSnapshot> groups = new LinkedHashMap<>();
|
||||
for (LoadingPlanItemEntity item : items) {
|
||||
String memberKey = item.getAccountId() + "|" + item.getInboundId();
|
||||
if (!groupByMember.containsKey(memberKey)) {
|
||||
groupByMember.put(memberKey, group(item.getAccountId(), item.getInboundId()));
|
||||
}
|
||||
GroupSnapshot group = groupByMember.get(memberKey);
|
||||
if (group != null) groups.putIfAbsent(group.groupId(), group);
|
||||
}
|
||||
Set<Long> groupsWithPickedItems = new HashSet<>();
|
||||
for (LoadingPlanItemEntity item : items) {
|
||||
if (picked.getOrDefault(item.getId(), 0) <= 0) continue;
|
||||
GroupSnapshot group = groupByMember.get(item.getAccountId() + "|" + item.getInboundId());
|
||||
if (group != null) groupsWithPickedItems.add(group.groupId());
|
||||
}
|
||||
|
||||
List<LoadingPlanItemEntity> removable = items.stream().filter(item -> {
|
||||
if (picked.getOrDefault(item.getId(), 0) > 0) return false;
|
||||
GroupSnapshot group = groupByMember.get(item.getAccountId() + "|" + item.getInboundId());
|
||||
return group == null || !groupsWithPickedItems.contains(group.groupId());
|
||||
}).toList();
|
||||
Set<Long> removableIds = removable.stream().map(LoadingPlanItemEntity::getId)
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
Set<Long> removableGroupIds = groups.values().stream()
|
||||
.filter(group -> !groupsWithPickedItems.contains(group.groupId()))
|
||||
.filter(group -> items.stream().anyMatch(item -> removableIds.contains(item.getId())
|
||||
&& group.members().stream().anyMatch(member -> member.tenantId() == item.getAccountId()
|
||||
&& member.inboundId() == item.getInboundId())))
|
||||
.map(GroupSnapshot::groupId).collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
|
||||
for (Long groupId : removableGroupIds) {
|
||||
requireCompleteGroupAllocation(planId, groups.get(groupId), items);
|
||||
}
|
||||
|
||||
if (!removable.isEmpty()) {
|
||||
touch(plan, operatorId, operatorName);
|
||||
for (LoadingPlanItemEntity item : removable) {
|
||||
loadingPickService.remove(item.getId());
|
||||
if (repository.remove(planId, item.getId(), item.getVersion(), operatorId, operatorName) != 1) {
|
||||
throw conflict("装柜未拣明细已被并发修改");
|
||||
}
|
||||
}
|
||||
Set<String> releasedInbounds = new HashSet<>();
|
||||
for (LoadingPlanItemEntity item : removable) {
|
||||
String key = item.getAccountId() + "|" + item.getInboundId();
|
||||
if (releasedInbounds.add(key)) {
|
||||
loadingModelBridge.removeIfUnallocated(planId, item.getAccountId(), item.getInboundId());
|
||||
}
|
||||
}
|
||||
for (Long groupId : removableGroupIds) {
|
||||
GroupSnapshot group = groups.get(groupId);
|
||||
coloadGroups.unlockWholeGroupAfterRemoval(groupId, planId,
|
||||
"UNLOCK-" + sha256(operation + '|' + groupId).substring(0, 48),
|
||||
operatorId, operatorName);
|
||||
}
|
||||
}
|
||||
history(planId, null, "REMOVE_UNPICKED", plan.getPlanStatus(), plan.getPlanStatus(), operation,
|
||||
json(values("version", request.expectedPlanVersion(), "itemCount", items.size())),
|
||||
json(values("requestHash", requestHash, "removedItemCount", removable.size(),
|
||||
"removedGroupCount", removableGroupIds.size())), operatorId, operatorName);
|
||||
return view(repository.plan(planId), booking);
|
||||
}
|
||||
|
||||
private List<Candidate> enrichColoadCandidates(ContainerBookingEntity booking, LoadingPlanEntity plan,
|
||||
List<Candidate> values) {
|
||||
Map<String, GroupSnapshot> snapshots = new HashMap<>();
|
||||
@@ -1013,7 +1092,7 @@ public class SmartLoadingService {
|
||||
if (!"ACTIVE".equals(group.status()) || group.lockedPlanId() != null) {
|
||||
throw conflict("同柜不可拆组已锁定或已消费");
|
||||
}
|
||||
List<Candidate> available = availableCandidates(booking);
|
||||
List<Candidate> available = manualCandidates(booking);
|
||||
Map<Long, List<Candidate>> result = new LinkedHashMap<>();
|
||||
for (Member member : group.members()) {
|
||||
if (!repository.activeItemsByInbound(member.tenantId(), member.inboundId()).isEmpty()) {
|
||||
@@ -1040,6 +1119,11 @@ public class SmartLoadingService {
|
||||
.filter(candidate -> sameChannel(booking, candidate) && sameCustomerScope(booking, candidate)).toList();
|
||||
}
|
||||
|
||||
private List<Candidate> manualCandidates(ContainerBookingEntity booking) {
|
||||
return repository.candidates(booking.getWarehouseId()).stream()
|
||||
.filter(candidate -> sameCustomerScope(booking, candidate)).toList();
|
||||
}
|
||||
|
||||
private boolean groupFits(ContainerBookingEntity booking, List<LoadingPlanItemEntity> planItems,
|
||||
GroupSnapshot group, List<Candidate> available) {
|
||||
if (booking.getCapacityCbm() == null || booking.getMaxWeightKg() == null) return false;
|
||||
@@ -1189,7 +1273,8 @@ public class SmartLoadingService {
|
||||
private PendingReviewOrderClient.Row requirePendingReviewOrder(AssignPendingReviewRequest request) {
|
||||
try {
|
||||
ApiResponse<PageResult<PendingReviewOrderClient.Row>> response = pendingReviewOrders.page(
|
||||
internalToken, null, null, request.customerId(), request.inboundCode(), null, null, 1, 2);
|
||||
internalToken, null, null, request.customerId(), request.inboundCode(), null, null,
|
||||
null, 1, 2);
|
||||
if (response == null || !response.successful() || response.data() == null) throw unavailable();
|
||||
return response.data().records().stream()
|
||||
.filter(row -> Objects.equals(row.orderId(), request.orderId())
|
||||
@@ -1272,7 +1357,6 @@ public class SmartLoadingService {
|
||||
private record DeclaredCargo(OrderCargoClient.CargoBinding cargo) {}
|
||||
private void checkRouteAndCapacity(ContainerBookingEntity b,List<LoadingPlanItemEntity> lines,Candidate c,
|
||||
int count, boolean requireSameDestination){
|
||||
requireSameChannel(b, c);
|
||||
if (!Objects.equals(b.getWarehouseId(), c.warehouseId())) throw validation("装柜仓库不一致");
|
||||
if (requireSameDestination && StringUtils.hasText(b.getDestinationWarehouseCode())
|
||||
&& !Objects.equals(b.getDestinationWarehouseCode(), c.destinationWarehouseCode())) {
|
||||
@@ -1376,12 +1460,6 @@ public class SmartLoadingService {
|
||||
private static boolean sameCustomerScope(ContainerBookingEntity booking, Candidate candidate) {
|
||||
return booking.getCustomerId() == null || Objects.equals(booking.getCustomerId(), candidate.accountId());
|
||||
}
|
||||
private static void requireSameChannel(ContainerBookingEntity booking, Candidate candidate) {
|
||||
if (!StringUtils.hasText(booking.getChannelCode()) || !StringUtils.hasText(candidate.channelCode())) {
|
||||
throw validation("柜次或入库货物缺少渠道快照,请先完成存量数据回填");
|
||||
}
|
||||
if (!sameChannel(booking, candidate)) throw validation("禁止跨渠道混装");
|
||||
}
|
||||
private static void requireSameCustomerScope(ContainerBookingEntity booking, Candidate candidate) {
|
||||
if (!sameCustomerScope(booking, candidate)) throw forbidden();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user