package com.baiyue.warehouse.service; import com.baomidou.mybatisplus.core.toolkit.IdWorker; import com.baiyue.common.core.api.ResultCode; import com.baiyue.common.core.exception.BizException; import com.baiyue.warehouse.dto.PalletTransitionRequests; import com.baiyue.warehouse.dto.PalletTransitionResponse; import com.baiyue.warehouse.entity.WarehouseLocationEntity; import com.baiyue.warehouse.entity.WarehousePalletEntity; import com.baiyue.warehouse.entity.WarehousePalletOperationEntity; import com.baiyue.warehouse.mapper.WarehousePalletTransitionMapper; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.time.LocalDateTime; import java.util.HexFormat; import java.util.Set; @Service public class WarehousePalletTransitionService { private static final Set MUTABLE_STATUSES = Set.of("IDLE", "OCCUPIED"); @Resource private WarehousePalletTransitionMapper mapper; @Transactional public PalletTransitionResponse apply(long palletId, PalletTransitionRequests.Apply request) { String key = request.operationKey().trim(); String status = request.targetStatus().trim().toUpperCase(); if (!MUTABLE_STATUSES.contains(status)) { throw new BizException(ResultCode.BAD_REQUEST, "托盘作业状态只允许 IDLE 或 OCCUPIED"); } String requestHash = hash(request.warehouseId() + "|" + palletId + "|" + request.expectedVersion() + "|" + status + "|" + request.targetLocationId()); WarehousePalletOperationEntity existing = mapper.findOperation(key); if (existing != null) return replay(existing, requestHash); WarehousePalletEntity pallet = mapper.lockPublicPallet(request.warehouseId(), palletId); if (pallet == null) throw new BizException(ResultCode.NOT_FOUND, "当前冻结仓内托盘不存在"); WarehousePalletOperationEntity concurrent = mapper.lockOperation(key); if (concurrent != null) return replay(concurrent, requestHash); int version = pallet.getVersion() == null ? 0 : pallet.getVersion(); if (version != request.expectedVersion()) { throw new BizException(ResultCode.CONFLICT, "托盘版本已变化,请重新扫码"); } // 当前海外尾程按托盘作业,不要求业务端选择库位。未传目标库位时保留托盘现有 // 技术库位;历史无库位托盘则只流转托盘状态,不能因此阻断 PDA 作业。 Long targetLocationId = request.targetLocationId() == null ? pallet.getLocationId() : request.targetLocationId(); transition(pallet, status, targetLocationId, version); WarehousePalletOperationEntity operation = new WarehousePalletOperationEntity(); operation.setId(IdWorker.getId()); operation.setOperationKey(key); operation.setRequestHash(requestHash); operation.setWarehouseId(request.warehouseId()); operation.setPalletId(palletId); operation.setPalletCode(pallet.getPalletCode()); operation.setBeforeStatus(pallet.getStatus()); operation.setAfterStatus(status); operation.setBeforeLocationId(pallet.getLocationId()); operation.setAfterLocationId(targetLocationId); operation.setBeforeVersion(version); operation.setAfterVersion(version + 1); operation.setOperationStatus("APPLIED"); operation.setReason(trim(request.reason())); operation.setCreatedAt(LocalDateTime.now()); operation.setUpdatedAt(operation.getCreatedAt()); mapper.insert(operation); return response(operation, false); } @Transactional public PalletTransitionResponse compensate(long palletId, String operationKey, PalletTransitionRequests.Compensate request) { WarehousePalletOperationEntity operation = mapper.lockOperation(operationKey); if (operation == null || !Long.valueOf(palletId).equals(operation.getPalletId()) || !Long.valueOf(request.warehouseId()).equals(operation.getWarehouseId())) { throw new BizException(ResultCode.NOT_FOUND, "托盘状态变更记录不存在"); } if ("COMPENSATED".equals(operation.getOperationStatus())) return response(operation, true); if (!"APPLIED".equals(operation.getOperationStatus())) { throw new BizException(ResultCode.CONFLICT, "托盘状态变更不可补偿"); } WarehousePalletEntity pallet = mapper.lockPublicPallet(request.warehouseId(), palletId); if (pallet == null || !operation.getAfterStatus().equals(pallet.getStatus()) || !java.util.Objects.equals(operation.getAfterLocationId(), pallet.getLocationId())) { throw new BizException(ResultCode.CONFLICT, "托盘已被后续作业修改,自动补偿失败"); } transition(pallet, operation.getBeforeStatus(), operation.getBeforeLocationId(), operation.getAfterVersion()); operation.setOperationStatus("COMPENSATED"); operation.setUpdatedAt(LocalDateTime.now()); mapper.updateById(operation); return response(operation, false); } private void transition(WarehousePalletEntity pallet, String targetStatus, Long targetLocationId, int expectedVersion) { int currentVersion = pallet.getVersion() == null ? 0 : pallet.getVersion(); if (currentVersion != expectedVersion || !MUTABLE_STATUSES.contains(pallet.getStatus())) { throw new BizException(ResultCode.CONFLICT, "托盘版本或状态已变化,请重新扫码"); } long warehouseId = pallet.getWarehouseId(); Long sourceLocationId = pallet.getLocationId(); if (sourceLocationId == null || targetLocationId == null) { transitionWithOptionalLocation(pallet, sourceLocationId, targetLocationId, targetStatus, expectedVersion); return; } WarehouseLocationEntity source; WarehouseLocationEntity target; if (sourceLocationId <= targetLocationId) { source = requireLocation(warehouseId, sourceLocationId); target = sourceLocationId == targetLocationId ? source : requireLocation(warehouseId, targetLocationId); } else { target = requireLocation(warehouseId, targetLocationId); source = requireLocation(warehouseId, sourceLocationId); } requireLocationMatchesPallet(source, pallet.getStatus()); if (sourceLocationId != targetLocationId) requireAvailableTarget(target, targetStatus); if (sourceLocationId == targetLocationId) { transitionSameLocation(warehouseId, source, pallet.getStatus(), targetStatus); } else { releaseSourceLocation(warehouseId, source, pallet.getStatus()); occupyTargetLocation(warehouseId, target, targetStatus); } if (mapper.transition(warehouseId, pallet.getId(), targetStatus, targetLocationId, expectedVersion) != 1) { throw new BizException(ResultCode.CONFLICT, "托盘版本已变化,请重新扫码"); } } private void transitionWithOptionalLocation(WarehousePalletEntity pallet, Long sourceLocationId, Long targetLocationId, String targetStatus, int expectedVersion) { long warehouseId = pallet.getWarehouseId(); if (sourceLocationId != null) { WarehouseLocationEntity source = requireLocation(warehouseId, sourceLocationId); requireLocationMatchesPallet(source, pallet.getStatus()); releaseSourceLocation(warehouseId, source, pallet.getStatus()); } if (targetLocationId != null) { WarehouseLocationEntity target = requireLocation(warehouseId, targetLocationId); requireAvailableTarget(target, targetStatus); occupyTargetLocation(warehouseId, target, targetStatus); } if (mapper.transition(warehouseId, pallet.getId(), targetStatus, targetLocationId, expectedVersion) != 1) { throw new BizException(ResultCode.CONFLICT, "托盘版本已变化,请重新扫码"); } } private WarehouseLocationEntity requireLocation(long warehouseId, long locationId) { WarehouseLocationEntity location = mapper.lockPublicLocation(warehouseId, locationId); if (location == null || "INACTIVE".equals(location.getStatus())) { throw new BizException(ResultCode.CONFLICT, "目标库位无效或已停用"); } return location; } private void requireLocationMatchesPallet(WarehouseLocationEntity location, String palletStatus) { String expected = expectedLocationStatus(location, palletStatus); if (!expected.equals(location.getStatus())) { throw new BizException(ResultCode.CONFLICT, "托盘与库位状态不一致,请刷新资源状态"); } } private void requireAvailableTarget(WarehouseLocationEntity location, String targetStatus) { String expected = isVirtual(location) ? "ACTIVE" : "IDLE"; if (!expected.equals(location.getStatus())) { throw new BizException(ResultCode.CONFLICT, "目标库位不可用"); } if (!MUTABLE_STATUSES.contains(targetStatus)) { throw new BizException(ResultCode.BAD_REQUEST, "托盘作业状态只允许 IDLE 或 OCCUPIED"); } } private void transitionSameLocation(long warehouseId, WarehouseLocationEntity location, String beforeStatus, String afterStatus) { if (beforeStatus.equals(afterStatus) || isVirtual(location)) return; changeLocationStatus(warehouseId, location.getId(), expectedLocationStatus(location, beforeStatus), expectedLocationStatus(location, afterStatus)); } private void releaseSourceLocation(long warehouseId, WarehouseLocationEntity source, String palletStatus) { if (isVirtual(source) || "IDLE".equals(palletStatus)) return; changeLocationStatus(warehouseId, source.getId(), "OCCUPIED", "IDLE"); } private void occupyTargetLocation(long warehouseId, WarehouseLocationEntity target, String palletStatus) { if (isVirtual(target) || "IDLE".equals(palletStatus)) return; changeLocationStatus(warehouseId, target.getId(), "IDLE", "OCCUPIED"); } private void changeLocationStatus(long warehouseId, long locationId, String before, String after) { if (before.equals(after)) return; if (mapper.transitionLocation(warehouseId, locationId, before, after) != 1) { throw new BizException(ResultCode.CONFLICT, "库位状态已变化,请重新扫码"); } } private static String expectedLocationStatus(WarehouseLocationEntity location, String palletStatus) { return isVirtual(location) ? "ACTIVE" : palletStatus; } private static boolean isVirtual(WarehouseLocationEntity location) { return "VIRTUAL".equals(location.getLocationType()); } private PalletTransitionResponse replay(WarehousePalletOperationEntity operation, String hash) { if (!hash.equals(operation.getRequestHash())) { throw new BizException(ResultCode.CONFLICT, "操作幂等键已被不同托盘请求使用"); } return response(operation, true); } private static PalletTransitionResponse response(WarehousePalletOperationEntity value, boolean replayed) { return new PalletTransitionResponse(value.getOperationKey(), value.getPalletId(), value.getPalletCode(), value.getWarehouseId(), value.getBeforeStatus(), value.getAfterStatus(), value.getBeforeLocationId(), value.getAfterLocationId(), value.getBeforeVersion(), value.getAfterVersion(), value.getOperationStatus(), replayed); } private static String hash(String value) { try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256") .digest(value.getBytes(StandardCharsets.UTF_8))); } catch (NoSuchAlgorithmException exception) { throw new IllegalStateException(exception); } } private static String trim(String value) { return value == null || value.isBlank() ? null : value.trim(); } }