优化
This commit is contained in:
+7
@@ -42,6 +42,13 @@ public class PdaPalletOperationController {
|
||||
return ApiResponse.success(service.disassemble(PdaWarehouseIdentity.require(user), request));
|
||||
}
|
||||
|
||||
@PostMapping("/relabel-disassemble")
|
||||
public ApiResponse<PalletOperationResponses.Operation> relabelDisassemble(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody PalletOperationRequests.RelabelDisassemble request) {
|
||||
return ApiResponse.success(service.relabelDisassemble(PdaWarehouseIdentity.require(user), request));
|
||||
}
|
||||
|
||||
@PostMapping("/swap")
|
||||
public ApiResponse<PalletOperationResponses.Operation> swap(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
|
||||
+8
@@ -31,6 +31,14 @@ public final class PalletOperationRequests {
|
||||
@NotEmpty @Size(max = 20) List<@Valid TargetMove> targets,
|
||||
@Size(max = 500) String reason) { }
|
||||
|
||||
/** 换标场景按箱唛拆托并绑定到一个目标托盘。箱唛位置由服务端从换标单快照解析。 */
|
||||
public record RelabelDisassemble(@NotBlank @Size(max = 128) String idempotencyKey,
|
||||
@NotBlank @Size(max = 64) String relabelNo,
|
||||
@Valid PalletExpectation source,
|
||||
@Valid PalletExpectation target,
|
||||
@NotEmpty @Size(max = 500) List<@NotBlank @Size(max = 255) String> cartonMarks,
|
||||
@Size(max = 500) String reason) { }
|
||||
|
||||
public record Swap(@NotBlank @Size(max = 128) String idempotencyKey,
|
||||
@Valid PalletExpectation source,
|
||||
@Valid PalletExpectation target,
|
||||
|
||||
+117
-14
@@ -13,6 +13,11 @@ import com.baiyue.lastleg.palletops.entity.PdaPalletOperation;
|
||||
import com.baiyue.lastleg.palletops.gateway.PalletPhysicalGateway;
|
||||
import com.baiyue.lastleg.palletops.mapper.PdaPalletOperationMapper;
|
||||
import com.baiyue.lastleg.palletops.model.PalletInventoryRow;
|
||||
import com.baiyue.lastleg.relabel.entity.RelabelApplication;
|
||||
import com.baiyue.lastleg.relabel.entity.RelabelItem;
|
||||
import com.baiyue.lastleg.relabel.mapper.RelabelApplicationMapper;
|
||||
import com.baiyue.lastleg.relabel.mapper.RelabelItemMapper;
|
||||
import com.baiyue.lastleg.relabel.service.RelabelPalletMutationService;
|
||||
import com.baiyue.lastleg.pda.security.PdaWarehouseIdentity;
|
||||
import com.baiyue.lastleg.pda.warehouse.service.PdaWarehouseService;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
@@ -52,6 +57,9 @@ public class PdaPalletOperationService {
|
||||
@Resource private PalletPhysicalGateway physical;
|
||||
@Resource private PdaWarehouseService history;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
@Resource private RelabelApplicationMapper relabelApplications;
|
||||
@Resource private RelabelItemMapper relabelItems;
|
||||
@Resource private RelabelPalletMutationService relabelPalletMutations;
|
||||
private TransactionTemplate transaction;
|
||||
private TransactionTemplate newTransaction;
|
||||
|
||||
@@ -113,6 +121,64 @@ public class PdaPalletOperationService {
|
||||
false));
|
||||
}
|
||||
|
||||
public PalletOperationResponses.Operation relabelDisassemble(PdaWarehouseIdentity identity,
|
||||
PalletOperationRequests.RelabelDisassemble request) {
|
||||
PalletOperationResponses.Operation replay = replayIfPresent(identity, "RELABEL_PALLET_DISASSEMBLE",
|
||||
request.idempotencyKey(), request);
|
||||
if (replay != null) return replay;
|
||||
String relabelNo = required(request.relabelNo(), "换标单号不能为空");
|
||||
RelabelApplication application = relabelApplications.findAnyByRelabelNo(relabelNo);
|
||||
if (application == null || !"READY_FOR_OUTBOUND".equals(application.getStatus())) {
|
||||
throw conflict("换标单尚未完成客户确认,不能执行拆托");
|
||||
}
|
||||
if (!Objects.equals(application.getOverseasWarehouseId(), String.valueOf(identity.overseasWarehouseId()))) {
|
||||
throw conflict("换标单不属于 Token 冻结的海外仓");
|
||||
}
|
||||
Map<String, PalletPhysicalGateway.PalletReference> pallets = resolvePallets(identity.overseasWarehouseId(),
|
||||
request.source(), List.of(request.target()));
|
||||
PalletPhysicalGateway.PalletReference source = pallets.get(code(request.source().palletCode()));
|
||||
PalletPhysicalGateway.PalletReference target = pallets.get(code(request.target().palletCode()));
|
||||
if (source.palletId() == target.palletId()) throw conflict("原托盘和目标托盘不能相同");
|
||||
List<String> marks = request.cartonMarks().stream().map(String::trim).filter(value -> !value.isEmpty()).toList();
|
||||
if (marks.size() != request.cartonMarks().size() || new HashSet<>(marks).size() != marks.size()) {
|
||||
throw badRequest("箱唛不能为空且不能重复");
|
||||
}
|
||||
List<RelabelItem> selected = relabelItems.selectList(new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<RelabelItem>()
|
||||
.eq("application_id", application.getId()).eq("customer_id", application.getCustomerId())
|
||||
.in("carton_mark", marks));
|
||||
if (selected.size() != marks.size()
|
||||
|| selected.stream().map(RelabelItem::getCartonMark).distinct().count() != marks.size()) {
|
||||
throw conflict("提交的箱唛不属于当前换标单");
|
||||
}
|
||||
Map<Long, Long> packagesByPosition = new LinkedHashMap<>();
|
||||
Map<Long, List<RelabelItem>> itemsByPosition = new LinkedHashMap<>();
|
||||
for (RelabelItem item : selected) {
|
||||
if (item.getSourceInventoryPositionId() == null || item.getNotifiedPackageCount() == null
|
||||
|| item.getNotifiedPackageCount() <= 0) throw conflict("换标箱唛缺少库存位置或箱数快照");
|
||||
InventoryPosition position = positions.selectById(item.getSourceInventoryPositionId());
|
||||
if (position == null || !Objects.equals(position.getCustomerId(), application.getCustomerId())
|
||||
|| !Objects.equals(position.getPalletId(), source.palletId())
|
||||
|| !Objects.equals(position.getOverseasWarehouseId(), String.valueOf(identity.overseasWarehouseId()))) {
|
||||
throw conflict("扫描的原托盘与换标箱唛实际库存位置不一致");
|
||||
}
|
||||
packagesByPosition.merge(position.getId(), item.getNotifiedPackageCount().longValue(), Long::sum);
|
||||
itemsByPosition.computeIfAbsent(position.getId(), ignored -> new ArrayList<>()).add(item);
|
||||
}
|
||||
List<PlannedMove> moves = packagesByPosition.entrySet().stream().map(entry -> {
|
||||
InventoryPosition position = positions.selectById(entry.getKey());
|
||||
if (value(position.getHoldPackages()) < entry.getValue()) throw conflict("换标冻结库存箱数不足");
|
||||
return new PlannedMove(entry.getKey(), entry.getValue(), value(position.getVersion()), source.palletId(),
|
||||
source.palletCode(), target.palletId(), target.palletCode(), target.locationId(),
|
||||
locationCode(identity.overseasWarehouseId(), target.locationId()), InventoryBucket.HOLD);
|
||||
}).toList();
|
||||
requireCommonDestination(identity.overseasWarehouseId(), moves, List.of(target));
|
||||
Plan base = plan(identity.overseasWarehouseId(), pallets, moves, Set.of(target.palletId()), false);
|
||||
Plan relabelPlan = new Plan(base.pallets(), base.moves(), base.changes(),
|
||||
new RelabelMutation(application, itemsByPosition));
|
||||
return execute(identity, "RELABEL_PALLET_DISASSEMBLE", request.idempotencyKey(), request,
|
||||
request.reason(), relabelPlan);
|
||||
}
|
||||
|
||||
public PalletOperationResponses.Operation swap(PdaWarehouseIdentity identity,
|
||||
PalletOperationRequests.Swap request) {
|
||||
PalletOperationResponses.Operation replay = replayIfPresent(identity, "PALLET_SWAP",
|
||||
@@ -130,7 +196,7 @@ public class PdaPalletOperationService {
|
||||
requireMovable(row, value(row.getAvailablePackages()));
|
||||
return new PlannedMove(row.getPositionId(), value(row.getAvailablePackages()), value(row.getVersion()),
|
||||
source.palletId(), source.palletCode(), target.palletId(), target.palletCode(), target.locationId(),
|
||||
locationCode(identity.overseasWarehouseId(), target.locationId()));
|
||||
locationCode(identity.overseasWarehouseId(), target.locationId()), InventoryBucket.AVAILABLE);
|
||||
}).toList();
|
||||
return execute(identity, "PALLET_SWAP", request.idempotencyKey(), request, request.reason(),
|
||||
plan(identity.overseasWarehouseId(), pallets, moves, Set.of(target.palletId()), false));
|
||||
@@ -149,7 +215,7 @@ public class PdaPalletOperationService {
|
||||
requireMovable(row, value(row.getAvailablePackages()));
|
||||
return new PlannedMove(row.getPositionId(), value(row.getAvailablePackages()), value(row.getVersion()),
|
||||
source.palletId(), source.palletCode(), 0L, "UNBOUND", source.locationId(),
|
||||
locationCode(identity.overseasWarehouseId(), source.locationId()));
|
||||
locationCode(identity.overseasWarehouseId(), source.locationId()), InventoryBucket.AVAILABLE);
|
||||
}).toList();
|
||||
return execute(identity, "PALLET_UNBIND", request.idempotencyKey(), request, request.reason(),
|
||||
plan(identity.overseasWarehouseId(), pallets, moves, Set.of(), true));
|
||||
@@ -183,8 +249,10 @@ public class PdaPalletOperationService {
|
||||
|
||||
private PalletOperationResponses.Operation mutate(PdaWarehouseIdentity identity, PdaPalletOperation operation,
|
||||
Plan plan, String key, String requestHash, String reason) {
|
||||
if (plan.relabelMutation() != null) lockRelabelMutation(plan.relabelMutation(), identity.overseasWarehouseId());
|
||||
Set<Long> seen = new HashSet<>();
|
||||
LinkedHashSet<String> customers = new LinkedHashSet<>();
|
||||
Map<Long, InventoryPosition> relabelTargets = new LinkedHashMap<>();
|
||||
for (PlannedMove move : plan.moves()) {
|
||||
if (!seen.add(move.positionId())) throw conflict("同一库存行不能在一次托盘作业中重复移动");
|
||||
PalletInventoryRow locked = mapper.lockPosition(String.valueOf(identity.overseasWarehouseId()),
|
||||
@@ -193,10 +261,14 @@ public class PdaPalletOperationService {
|
||||
throw conflict("库存所在托盘已变化,请重新扫码");
|
||||
}
|
||||
if (value(locked.getVersion()) != move.expectedVersion()) throw conflict("库存版本已变化,请重新扫码");
|
||||
requireMovable(locked, move.packages());
|
||||
if (move.bucket() == InventoryBucket.HOLD) requireHeld(locked, move.packages());
|
||||
else requireMovable(locked, move.packages());
|
||||
customers.add(locked.getCustomerId());
|
||||
moveInventory(identity, operation, locked, move, key, reason);
|
||||
InventoryPosition target = moveInventory(identity, operation, locked, move, key, reason);
|
||||
if (plan.relabelMutation() != null) relabelTargets.put(move.positionId(), target);
|
||||
}
|
||||
if (plan.relabelMutation() != null) relabelPalletMutations.moveSelectedItems(
|
||||
plan.relabelMutation().application(), plan.relabelMutation().itemsByPosition(), relabelTargets);
|
||||
for (PalletChange change : plan.changes()) {
|
||||
long actual = mapper.palletPackages(String.valueOf(identity.overseasWarehouseId()), change.pallet().palletId());
|
||||
boolean shouldBeEmpty = "IDLE".equals(change.targetStatus());
|
||||
@@ -219,12 +291,22 @@ public class PdaPalletOperationService {
|
||||
return result;
|
||||
}
|
||||
|
||||
private void moveInventory(PdaWarehouseIdentity identity, PdaPalletOperation operation,
|
||||
PalletInventoryRow sourceRow, PlannedMove move, String key, String reason) {
|
||||
private void lockRelabelMutation(RelabelMutation mutation, long warehouseId) {
|
||||
RelabelApplication locked = relabelApplications.lockAnyByRelabelNo(mutation.application().getRelabelNo());
|
||||
if (locked == null || !"READY_FOR_OUTBOUND".equals(locked.getStatus())
|
||||
|| !Objects.equals(locked.getCustomerId(), mutation.application().getCustomerId())
|
||||
|| !Objects.equals(locked.getOverseasWarehouseId(), String.valueOf(warehouseId))
|
||||
|| !Objects.equals(locked.getOutboundOrderNo(), mutation.application().getOutboundOrderNo())) {
|
||||
throw conflict("换标单状态或履约单据已变化,请重新扫码");
|
||||
}
|
||||
}
|
||||
|
||||
private InventoryPosition moveInventory(PdaWarehouseIdentity identity, PdaPalletOperation operation,
|
||||
PalletInventoryRow sourceRow, PlannedMove move, String key, String reason) {
|
||||
InventoryPosition source = positions.selectById(sourceRow.getPositionId());
|
||||
if (source == null) throw conflict("库存行不存在");
|
||||
long sourceBefore = value(source.getAvailablePackages());
|
||||
source.setAvailablePackages(sourceBefore - move.packages());
|
||||
long sourceBefore = bucketValue(source, move.bucket());
|
||||
setBucketValue(source, move.bucket(), sourceBefore - move.packages());
|
||||
source.setStatus(positionPackages(source) == 0 ? "EMPTY" : "OCCUPIED");
|
||||
source.setVersion(value(source.getVersion()) + 1); source.setUpdatedAt(LocalDateTime.now());
|
||||
positions.updateById(source);
|
||||
@@ -248,8 +330,8 @@ public class PdaPalletOperationService {
|
||||
if (target == null) throw duplicate;
|
||||
}
|
||||
}
|
||||
long targetBefore = value(target.getAvailablePackages());
|
||||
target.setAvailablePackages(targetBefore + move.packages()); target.setStatus("OCCUPIED");
|
||||
long targetBefore = bucketValue(target, move.bucket());
|
||||
setBucketValue(target, move.bucket(), targetBefore + move.packages()); target.setStatus("OCCUPIED");
|
||||
target.setWarehouseLocationId(move.targetLocationId());
|
||||
target.setWarehouseLocationCode(move.targetLocationCode());
|
||||
target.setVersion(value(target.getVersion()) + 1); target.setUpdatedAt(LocalDateTime.now());
|
||||
@@ -261,6 +343,7 @@ public class PdaPalletOperationService {
|
||||
sourceBefore, -move.packages(), sourceBefore - move.packages(), move.sourcePalletCode(), move.targetPalletCode(), reason);
|
||||
ledger(identity, operation, balance, target, key + ":" + source.getId() + ":IN",
|
||||
targetBefore, move.packages(), targetBefore + move.packages(), move.sourcePalletCode(), move.targetPalletCode(), reason);
|
||||
return target;
|
||||
}
|
||||
|
||||
private void ledger(PdaWarehouseIdentity identity, PdaPalletOperation operation, InventoryBalance balance,
|
||||
@@ -302,7 +385,7 @@ public class PdaPalletOperationService {
|
||||
}
|
||||
changes.sort(Comparator.comparingInt((PalletChange change) -> "IDLE".equals(change.targetStatus()) ? 0 : 1)
|
||||
.thenComparingLong(change -> change.pallet().palletId()));
|
||||
return new Plan(pallets, moves, changes);
|
||||
return new Plan(pallets, moves, changes, null);
|
||||
}
|
||||
|
||||
private List<PlannedMove> plannedLines(long warehouseId, List<PalletOperationRequests.MoveLine> lines,
|
||||
@@ -325,7 +408,7 @@ public class PdaPalletOperationService {
|
||||
requireMovable(row, line.packages());
|
||||
result.add(new PlannedMove(row.getPositionId(), line.packages(), line.expectedPositionVersion(),
|
||||
row.getPalletId(), row.getPalletNo(), target.palletId(), target.palletCode(), target.locationId(),
|
||||
locationCode(warehouseId, target.locationId())));
|
||||
locationCode(warehouseId, target.locationId()), InventoryBucket.AVAILABLE));
|
||||
}
|
||||
requireUniqueLines(result);
|
||||
return result;
|
||||
@@ -494,6 +577,21 @@ public class PdaPalletOperationService {
|
||||
throw conflict("冻结、已分配、已拣或收货中的库存不能执行托盘作业");
|
||||
}
|
||||
}
|
||||
private static void requireHeld(PalletInventoryRow row, long packages) {
|
||||
if (packages <= 0 || value(row.getHoldPackages()) < packages) throw conflict("换标冻结库存箱数不足");
|
||||
if (value(row.getAllocatedPackages()) > 0 || value(row.getPickedPackages()) > 0
|
||||
|| value(row.getReceivingPackages()) > 0) {
|
||||
throw conflict("已分配、已拣或收货中的库存不能执行换标拆托");
|
||||
}
|
||||
}
|
||||
private static long bucketValue(InventoryPosition position, InventoryBucket bucket) {
|
||||
return bucket == InventoryBucket.HOLD ? value(position.getHoldPackages())
|
||||
: value(position.getAvailablePackages());
|
||||
}
|
||||
private static void setBucketValue(InventoryPosition position, InventoryBucket bucket, long packages) {
|
||||
if (bucket == InventoryBucket.HOLD) position.setHoldPackages(packages);
|
||||
else position.setAvailablePackages(packages);
|
||||
}
|
||||
private static void requireUniqueLines(List<PlannedMove> moves) {
|
||||
Set<Long> ids = new HashSet<>();
|
||||
if (moves.stream().anyMatch(move -> !ids.add(move.positionId()))) throw badRequest("库存行不能重复提交");
|
||||
@@ -517,6 +615,7 @@ public class PdaPalletOperationService {
|
||||
private static String first(String first, String second) { String value = trim(first); return value == null ? trim(second) : value; }
|
||||
private static String limit(String value, int size) { if (value == null) return null; return value.length() <= size ? value : value.substring(0, size); }
|
||||
private static long value(Long value) { return value == null ? 0L : value; }
|
||||
private static int value(Integer value) { return value == null ? 0 : value; }
|
||||
private static ResponseStatusException conflict(String message) { return new ResponseStatusException(HttpStatus.CONFLICT, message); }
|
||||
private static ResponseStatusException badRequest(String message) { return new ResponseStatusException(HttpStatus.BAD_REQUEST, message); }
|
||||
private static ResponseStatusException notFound(String message) { return new ResponseStatusException(HttpStatus.NOT_FOUND, message); }
|
||||
@@ -524,9 +623,13 @@ public class PdaPalletOperationService {
|
||||
private record PlannedMove(long positionId, long packages, long expectedVersion,
|
||||
long sourcePalletId, String sourcePalletCode,
|
||||
long targetPalletId, String targetPalletCode, long targetLocationId,
|
||||
String targetLocationCode) { }
|
||||
String targetLocationCode, InventoryBucket bucket) { }
|
||||
private record PalletChange(PalletPhysicalGateway.PalletReference pallet, String targetStatus,
|
||||
long targetLocationId) { }
|
||||
private record Plan(Map<String, PalletPhysicalGateway.PalletReference> pallets,
|
||||
List<PlannedMove> moves, List<PalletChange> changes) { }
|
||||
List<PlannedMove> moves, List<PalletChange> changes,
|
||||
RelabelMutation relabelMutation) { }
|
||||
private record RelabelMutation(RelabelApplication application,
|
||||
Map<Long, List<RelabelItem>> itemsByPosition) { }
|
||||
private enum InventoryBucket { AVAILABLE, HOLD }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user