优化
This commit is contained in:
+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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -14,9 +14,11 @@ public record InboundOrderDetail(
|
||||
List<StatusHistorySummary> 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
|
||||
) {
|
||||
|
||||
+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;
|
||||
|
||||
+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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,7 +253,7 @@ public class LoadingPickService {
|
||||
Task replay = replayTaskOperation(taskId, operationId, requestHash, operatorId);
|
||||
if (replay != null) return replay;
|
||||
LoadingPickTaskEntity task = requiredLoadingTaskForUpdate(taskId);
|
||||
replay = replayTaskOperation(taskId, operationId, requestHash, operatorId);
|
||||
replay = replayTaskOperationForUpdate(taskId, operationId, requestHash, operatorId);
|
||||
if (replay != null) return replay;
|
||||
LoadingPickAssignmentEntity existing = repository.assignment(taskId);
|
||||
if (existing == null) {
|
||||
@@ -284,7 +284,7 @@ public class LoadingPickService {
|
||||
Task replay = replayTaskOperation(taskId, operationId, requestHash, operatorId);
|
||||
if (replay != null) return replay;
|
||||
LoadingPickTaskEntity task = requiredLoadingTaskForUpdate(taskId);
|
||||
replay = replayTaskOperation(taskId, operationId, requestHash, operatorId);
|
||||
replay = replayTaskOperationForUpdate(taskId, operationId, requestHash, operatorId);
|
||||
if (replay != null) return replay;
|
||||
LoadingPickAssignmentEntity existing = repository.assignment(taskId);
|
||||
int changed = existing == null
|
||||
@@ -300,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) {
|
||||
@@ -314,8 +313,8 @@ public class LoadingPickService {
|
||||
return view(requiredTask(taskId), request.warehouseId());
|
||||
}
|
||||
LoadingPickTaskEntity task = requiredAssignedLoadingTaskForUpdate(taskId, operatorId);
|
||||
requireNoTaskOperation(operationId);
|
||||
replay = repository.history(operationId);
|
||||
requireNoTaskOperationForUpdate(operationId);
|
||||
replay = repository.historyForUpdate(operationId);
|
||||
if (replay != null) {
|
||||
assertReplay(replay, taskId, itemId, requestHash);
|
||||
return view(task, request.warehouseId());
|
||||
@@ -324,8 +323,7 @@ public class LoadingPickService {
|
||||
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();
|
||||
@@ -346,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) {
|
||||
@@ -357,15 +354,15 @@ public class LoadingPickService {
|
||||
return view(requiredTask(taskId), request.warehouseId());
|
||||
}
|
||||
LoadingPickTaskEntity task = requiredAssignedLoadingTaskForUpdate(taskId, operatorId);
|
||||
requireNoTaskOperation(operationId);
|
||||
replay = repository.history(operationId);
|
||||
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",
|
||||
@@ -388,8 +385,8 @@ public class LoadingPickService {
|
||||
return view(requiredTask(taskId), request.warehouseId());
|
||||
}
|
||||
LoadingPickTaskEntity task = requiredAssignedLoadingTaskForUpdate(taskId, operatorId);
|
||||
requireNoTaskOperation(operationId);
|
||||
replay = repository.history(operationId);
|
||||
requireNoTaskOperationForUpdate(operationId);
|
||||
replay = repository.historyForUpdate(operationId);
|
||||
if (replay != null) {
|
||||
assertReplay(replay, taskId, itemId, requestHash);
|
||||
return view(task, request.warehouseId());
|
||||
@@ -420,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("装柜任务已完成,不能重复提交整批拣货");
|
||||
@@ -990,10 +987,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
|
||||
@@ -1002,7 +997,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
|
||||
@@ -1011,10 +1005,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必须填写异常原因");
|
||||
@@ -1027,7 +1019,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;
|
||||
@@ -1130,12 +1122,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());
|
||||
@@ -1145,8 +1135,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);
|
||||
}
|
||||
@@ -1190,6 +1179,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())
|
||||
@@ -1215,6 +1216,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不能为空");
|
||||
@@ -1270,35 +1275,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();
|
||||
|
||||
+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);
|
||||
@@ -186,19 +180,11 @@ public class PdaPickSettlementService {
|
||||
throw conflict("主入库单测量明细剩余箱数与未装柜箱数不一致");
|
||||
}
|
||||
|
||||
if (childPackageCount > 0 && transferItems.isEmpty()) {
|
||||
throw conflict("父入库单可转移库位剩余箱数与未装柜箱数不一致");
|
||||
}
|
||||
|
||||
if (root == null || !"MASTER".equals(root.inboundRole())) throw conflict("根主入库单不存在");
|
||||
OrderFulfillmentSplitClient.Response orderSplit = createOrderSplit(parent, root, measurementLines,
|
||||
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();
|
||||
@@ -269,15 +255,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;
|
||||
}
|
||||
|
||||
@@ -409,28 +394,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) {
|
||||
@@ -441,36 +415,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(),
|
||||
@@ -493,40 +464,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("子入库单拆分序号不合法");
|
||||
|
||||
@@ -847,7 +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("调整箱数超过可用箱数");
|
||||
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));
|
||||
|
||||
Reference in New Issue
Block a user