fix(warehouse-overseas): enforce pallet location invariant
This commit is contained in:
+13
-2
@@ -8,11 +8,22 @@ public record WarehousePalletResolveResponse(
|
||||
String status,
|
||||
String fbaWarehouseCode,
|
||||
java.math.BigDecimal unitPrice,
|
||||
int version
|
||||
int version,
|
||||
String locationType,
|
||||
String locationStatus,
|
||||
boolean reservable,
|
||||
String reservationBlockReason
|
||||
) {
|
||||
public WarehousePalletResolveResponse(long palletId, String palletCode, long warehouseId, long locationId,
|
||||
String status, String fbaWarehouseCode, int version) {
|
||||
this(palletId, palletCode, warehouseId, locationId, status, fbaWarehouseCode,
|
||||
java.math.BigDecimal.ZERO, version);
|
||||
java.math.BigDecimal.ZERO, version, null, null, false, "LOCATION_STATE_UNKNOWN");
|
||||
}
|
||||
|
||||
public WarehousePalletResolveResponse(long palletId, String palletCode, long warehouseId, long locationId,
|
||||
String status, String fbaWarehouseCode,
|
||||
java.math.BigDecimal unitPrice, int version) {
|
||||
this(palletId, palletCode, warehouseId, locationId, status, fbaWarehouseCode,
|
||||
unitPrice, version, null, null, false, "LOCATION_STATE_UNKNOWN");
|
||||
}
|
||||
}
|
||||
|
||||
+12
-3
@@ -4,6 +4,7 @@ import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.warehouse.entity.WarehousePalletEntity;
|
||||
import com.baiyue.warehouse.entity.WarehouseLocationEntity;
|
||||
import com.baiyue.warehouse.entity.WarehousePalletOperationEntity;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
@@ -22,9 +23,17 @@ public interface WarehousePalletTransitionMapper extends BaseMapper<WarehousePal
|
||||
WarehousePalletEntity lockPublicPallet(@Param("warehouseId") long warehouseId,
|
||||
@Param("palletId") long palletId);
|
||||
|
||||
@Select("SELECT COUNT(*) FROM wh_location WHERE id=#{locationId} AND customer_id=0 " +
|
||||
"AND warehouse_id=#{warehouseId} AND status<>'INACTIVE' AND deleted=0")
|
||||
long countActiveLocation(@Param("warehouseId") long warehouseId, @Param("locationId") long locationId);
|
||||
@Select("SELECT * FROM wh_location WHERE id=#{locationId} AND customer_id=0 " +
|
||||
"AND warehouse_id=#{warehouseId} AND deleted=0 LIMIT 1 FOR UPDATE")
|
||||
WarehouseLocationEntity lockPublicLocation(@Param("warehouseId") long warehouseId,
|
||||
@Param("locationId") long locationId);
|
||||
|
||||
@Update("UPDATE wh_location SET status=#{targetStatus},version=version+1,updated_at=CURRENT_TIMESTAMP(3) " +
|
||||
"WHERE id=#{locationId} AND customer_id=0 AND warehouse_id=#{warehouseId} " +
|
||||
"AND status=#{expectedStatus} AND deleted=0")
|
||||
int transitionLocation(@Param("warehouseId") long warehouseId, @Param("locationId") long locationId,
|
||||
@Param("expectedStatus") String expectedStatus,
|
||||
@Param("targetStatus") String targetStatus);
|
||||
|
||||
@Update("UPDATE wh_pallet SET status=#{targetStatus},location_id=#{targetLocationId},version=version+1," +
|
||||
"updated_at=CURRENT_TIMESTAMP(3) WHERE id=#{palletId} AND customer_id=0 " +
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ public interface WarehouseResourceLockMapper extends BaseMapper<WarehouseResourc
|
||||
|
||||
@Update("""
|
||||
UPDATE wh_pallet
|
||||
SET status = 'IDLE', location_id = NULL, updated_at = CURRENT_TIMESTAMP(3), version = version + 1
|
||||
SET status = 'IDLE', updated_at = CURRENT_TIMESTAMP(3), version = version + 1
|
||||
WHERE id = #{palletId} AND customer_id = #{customerId} AND warehouse_id = #{warehouseId}
|
||||
AND location_id = #{locationId} AND status = 'OCCUPIED' AND deleted = 0
|
||||
""")
|
||||
|
||||
+97
-8
@@ -5,6 +5,7 @@ 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;
|
||||
@@ -38,19 +39,16 @@ public class WarehousePalletTransitionService {
|
||||
|
||||
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, "托盘版本已变化,请重新扫码");
|
||||
}
|
||||
if (mapper.countActiveLocation(request.warehouseId(), request.targetLocationId()) != 1) {
|
||||
throw new BizException(ResultCode.CONFLICT, "目标库位无效或已停用");
|
||||
}
|
||||
if (pallet.getLocationId() == null) {
|
||||
throw new BizException(ResultCode.CONFLICT, "托盘没有有效库位,不能执行库内作业");
|
||||
}
|
||||
if (mapper.transition(request.warehouseId(), palletId, status, request.targetLocationId(), version) != 1) {
|
||||
throw new BizException(ResultCode.CONFLICT, "托盘版本已变化,请重新扫码");
|
||||
}
|
||||
transition(pallet, status, request.targetLocationId(), version);
|
||||
WarehousePalletOperationEntity operation = new WarehousePalletOperationEntity();
|
||||
operation.setId(IdWorker.getId()); operation.setOperationKey(key); operation.setRequestHash(requestHash);
|
||||
operation.setWarehouseId(request.warehouseId()); operation.setPalletId(palletId);
|
||||
@@ -75,15 +73,106 @@ public class WarehousePalletTransitionService {
|
||||
if (!"APPLIED".equals(operation.getOperationStatus())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "托盘状态变更不可补偿");
|
||||
}
|
||||
if (mapper.transition(request.warehouseId(), palletId, operation.getBeforeStatus(),
|
||||
operation.getBeforeLocationId(), operation.getAfterVersion()) != 1) {
|
||||
WarehousePalletEntity pallet = mapper.lockPublicPallet(request.warehouseId(), palletId);
|
||||
if (pallet == null || !operation.getAfterStatus().equals(pallet.getStatus())
|
||||
|| !Long.valueOf(operation.getAfterLocationId()).equals(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();
|
||||
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 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, "操作幂等键已被不同托盘请求使用");
|
||||
|
||||
+22
-7
@@ -188,19 +188,14 @@ public class WarehouseResourceService {
|
||||
WarehousePalletEntity pallet = matches.get(0);
|
||||
if (pallet.getLocationId() == null) throw new BizException(ResultCode.CONFLICT, "托盘未绑定库位");
|
||||
if ("SCRAPPED".equals(pallet.getStatus())) throw new BizException(ResultCode.CONFLICT, "托盘已报废");
|
||||
return new WarehousePalletResolveResponse(pallet.getId(), pallet.getPalletCode(), pallet.getWarehouseId(),
|
||||
pallet.getLocationId(), pallet.getStatus(), pallet.getFbaWarehouseCode(),
|
||||
pallet.getUnitPrice(),
|
||||
pallet.getVersion() == null ? 0 : pallet.getVersion());
|
||||
return palletReference(pallet);
|
||||
}
|
||||
|
||||
public WarehousePalletResolveResponse resolvePublicPallet(long palletId) {
|
||||
WarehousePalletEntity pallet = repository.findPublicPallet(palletId);
|
||||
if (pallet == null) throw new BizException(ResultCode.NOT_FOUND, "托盘不存在");
|
||||
if (pallet.getLocationId() == null) throw new BizException(ResultCode.CONFLICT, "托盘未绑定库位");
|
||||
return new WarehousePalletResolveResponse(pallet.getId(), pallet.getPalletCode(), pallet.getWarehouseId(),
|
||||
pallet.getLocationId(), pallet.getStatus(), pallet.getFbaWarehouseCode(), pallet.getUnitPrice(),
|
||||
pallet.getVersion() == null ? 0 : pallet.getVersion());
|
||||
return palletReference(pallet);
|
||||
}
|
||||
|
||||
public WarehouseLocationResolveResponse resolvePublicLocation(long warehouseId, String locationCode) {
|
||||
@@ -222,6 +217,26 @@ public class WarehouseResourceService {
|
||||
location.getLocationType(), location.getMaxLoadKg(), location.getStatus());
|
||||
}
|
||||
|
||||
private WarehousePalletResolveResponse palletReference(WarehousePalletEntity pallet) {
|
||||
WarehouseLocationEntity location = repository.findLocation(PUBLIC_RESOURCE_SCOPE,
|
||||
pallet.getWarehouseId(), pallet.getLocationId());
|
||||
String blockReason = reservationBlockReason(pallet, location);
|
||||
return new WarehousePalletResolveResponse(pallet.getId(), pallet.getPalletCode(), pallet.getWarehouseId(),
|
||||
pallet.getLocationId(), pallet.getStatus(), pallet.getFbaWarehouseCode(), pallet.getUnitPrice(),
|
||||
pallet.getVersion() == null ? 0 : pallet.getVersion(),
|
||||
location == null ? null : location.getLocationType(),
|
||||
location == null ? null : location.getStatus(), blockReason == null, blockReason);
|
||||
}
|
||||
|
||||
private static String reservationBlockReason(WarehousePalletEntity pallet, WarehouseLocationEntity location) {
|
||||
if (!"IDLE".equals(pallet.getStatus())) return "PALLET_NOT_IDLE";
|
||||
if (location == null) return "LOCATION_NOT_FOUND";
|
||||
if ("VIRTUAL".equals(location.getLocationType())) {
|
||||
return "ACTIVE".equals(location.getStatus()) ? null : "VIRTUAL_LOCATION_NOT_ACTIVE";
|
||||
}
|
||||
return "IDLE".equals(location.getStatus()) ? null : "LOCATION_NOT_IDLE";
|
||||
}
|
||||
|
||||
public PageResult<WarehousePalletEntity> pallets(long customerId, long warehouseId,
|
||||
PalletRequests.QueryRequest query) {
|
||||
requireWarehouse(customerId, warehouseId);
|
||||
|
||||
Reference in New Issue
Block a user