fix(warehouse-overseas): enforce pallet location invariant

This commit is contained in:
2026-09-08 14:47:35 +08:00
parent 07688005a1
commit a0ad3382af
9 changed files with 276 additions and 25 deletions
@@ -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");
}
}
@@ -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 " +
@@ -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
""")
@@ -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, "操作幂等键已被不同托盘请求使用");
@@ -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);