220 lines
12 KiB
Java
220 lines
12 KiB
Java
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<String> 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(); }
|
|
}
|