This commit is contained in:
2026-08-27 14:30:23 +08:00
parent 3b38fdd33d
commit 0a3ec0b8a6
10 changed files with 283 additions and 46 deletions
@@ -109,6 +109,16 @@ public interface LocationMapper extends BaseMapper<LocationEntity> {
@Param("warehouseId") long warehouseId,
@Param("limit") int limit);
@Select("""
SELECT * FROM wh_location
WHERE id = #{id} AND tenant_id = #{tenantId} AND warehouse_id = #{warehouseId}
AND deleted = 0
FOR UPDATE
""")
LocationEntity lockOwned(@Param("tenantId") long tenantId,
@Param("warehouseId") long warehouseId,
@Param("id") long id);
@Insert("""
INSERT INTO wh_location (
id, tenant_id, warehouse_id, zone_code, zone_name, location_code, location_name,
@@ -61,6 +61,16 @@ public interface LocationReservationMapper extends BaseMapper<LocationReservatio
int release(@Param("tenantId") long tenantId, @Param("id") long id,
@Param("version") int version, @Param("reason") String reason);
@Update("""
UPDATE wh_location_reservation
SET reservation_status = 'RELEASED', released_at = CURRENT_TIMESTAMP(3),
release_reason = #{reason}, updated_at = CURRENT_TIMESTAMP(3), version = version + 1
WHERE id = #{id} AND tenant_id = #{tenantId}
AND reservation_status = 'CONFIRMED' AND version = #{version}
""")
int releaseConfirmed(@Param("tenantId") long tenantId, @Param("id") long id,
@Param("version") int version, @Param("reason") String reason);
@Update("""
UPDATE wh_location_reservation
SET business_id = #{targetBusinessId}, inbound_code = #{targetInboundCode},
@@ -44,6 +44,10 @@ public class LocationRepository {
.eq(LocationEntity::getId, id));
}
public LocationEntity lockOwned(long tenantId, long warehouseId, long id) {
return locations.lockOwned(tenantId, warehouseId, id);
}
public PageResult<LocationEntity> page(Long tenantId, PageQuery query) {
Page<LocationEntity> result = locations.selectLocationPage(new Page<>(query.page(), query.size()),
tenantId, query.warehouseId(), query.zoneCode(), query.locationCode(), query.locationType(), query.status());
@@ -94,6 +98,10 @@ public class LocationRepository {
|| occupancies.countActiveByLocation(tenantId, locationId) > 0;
}
public int countActiveOccupancies(long tenantId, long locationId) {
return occupancies.countActiveByLocation(tenantId, locationId);
}
public List<LocationReservationEntity> findReservationsByKeys(long tenantId, String sourceService,
Collection<String> keys) {
if (keys.isEmpty()) {
@@ -129,6 +137,10 @@ public class LocationRepository {
return reservations.release(entity.getTenantId(), entity.getId(), entity.getVersion(), reason);
}
public int releaseConfirmedReservation(LocationReservationEntity entity, String reason) {
return reservations.releaseConfirmed(entity.getTenantId(), entity.getId(), entity.getVersion(), reason);
}
public int transferConfirmedReservation(LocationReservationEntity entity, String sourceService,
long sourceBusinessId, long targetBusinessId,
String targetInboundCode) {
@@ -24,6 +24,8 @@ import com.baiyue.warehouse.entity.LocationReservationEntity;
import com.baiyue.warehouse.entity.WarehouseEntity;
import com.baiyue.warehouse.repository.LocationRepository;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -42,6 +44,7 @@ import java.util.LinkedHashMap;
import java.util.stream.Collectors;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
@@ -51,6 +54,8 @@ import java.util.Set;
@Service
@Validated
public class LocationResourceService {
private static final Logger log = LoggerFactory.getLogger(LocationResourceService.class);
@Resource
private LocationRepository repository;
@Resource
@@ -165,13 +170,8 @@ public class LocationResourceService {
if (!"OCCUPIED".equals(occupancy.getOccupancyStatus())) {
continue;
}
LocationEntity location = repository.findOwned(command.tenantId(), occupancy.getWarehouseId(), occupancy.getLocationId());
if (location == null || repository.releaseOccupancy(occupancy, command.reason()) != 1
|| (!Boolean.TRUE.equals(occupancy.getSharedOverflow())
&& repository.changeStatus(command.tenantId(), occupancy.getWarehouseId(), occupancy.getLocationId(),
"OCCUPIED", "AVAILABLE", location.getVersion(), command.operatorId()) != 1)) {
throw new BizException(ResultCode.CONFLICT, "释放占用时库位状态已被并发修改");
}
releaseOccupiedResource(occupancy, command.reason(), command.operatorId(),
"释放占用时库位状态已被并发修改");
released.add(active(occupancy, "RELEASED", null));
}
for (LocationReservationEntity reservation : repository.findReservationsByBusiness(
@@ -237,15 +237,8 @@ public class LocationResourceService {
List<SelectiveReleaseResult> released = new ArrayList<>(selectedIndexes.size());
for (int index : selectedIndexes) {
LocationOccupancyEntity occupancy = active.get(index);
LocationEntity location = repository.findOwned(command.tenantId(), occupancy.getWarehouseId(),
occupancy.getLocationId());
if (location == null || repository.releaseOccupancy(occupancy, command.reason()) != 1
|| (!Boolean.TRUE.equals(occupancy.getSharedOverflow())
&& repository.changeStatus(command.tenantId(), occupancy.getWarehouseId(), occupancy.getLocationId(),
"OCCUPIED", "AVAILABLE", location.getVersion(), command.operatorId()) != 1)) {
throw new BizException(ResultCode.CONFLICT,
"Location occupancy changed concurrently while releasing selected locations");
}
releaseOccupiedResource(occupancy, command.reason(), command.operatorId(),
"Location occupancy changed concurrently while releasing selected locations");
released.add(new SelectiveReleaseResult(occupancy.getReservationId(), occupancy.getId(), index + 1,
occupancy.getWarehouseId(), occupancy.getPalletId(), occupancy.getLocationId(),
occupancy.getLocationCode(), packages.get(index)));
@@ -289,15 +282,8 @@ public class LocationResourceService {
throw new BizException(ResultCode.CONFLICT,
"Occupied location has no valid allocated package count and cannot be released exactly");
}
LocationEntity location = repository.findOwned(command.tenantId(), occupancy.getWarehouseId(),
occupancy.getLocationId());
if (location == null || repository.releaseOccupancy(occupancy, command.reason()) != 1
|| (!Boolean.TRUE.equals(occupancy.getSharedOverflow())
&& repository.changeStatus(command.tenantId(), occupancy.getWarehouseId(), occupancy.getLocationId(),
"OCCUPIED", "AVAILABLE", location.getVersion(), command.operatorId()) != 1)) {
throw new BizException(ResultCode.CONFLICT,
"Location occupancy changed concurrently while releasing selected locations");
}
releaseOccupiedResource(occupancy, command.reason(), command.operatorId(),
"Location occupancy changed concurrently while releasing selected locations");
released.add(new ExactSelectiveReleaseResult(occupancy.getReservationId(), occupancy.getId(), index + 1,
occupancy.getWarehouseId(), occupancy.getPalletId(), occupancy.getLocationId(),
occupancy.getLocationCode(), allocatedPackageCount));
@@ -322,19 +308,94 @@ public class LocationResourceService {
LocationOccupancyEntity occupancy = active.get(index);
if (!requested.contains(occupancy.getReservationId())) continue;
Integer allocated = occupancy.getAllocatedPackageCount();
LocationEntity location = repository.findOwned(command.tenantId(), occupancy.getWarehouseId(), occupancy.getLocationId());
if (allocated == null || allocated < 0 || location == null || repository.releaseOccupancy(occupancy, command.reason()) != 1
|| (!Boolean.TRUE.equals(occupancy.getSharedOverflow()) && repository.changeStatus(command.tenantId(),
occupancy.getWarehouseId(), occupancy.getLocationId(), "OCCUPIED", "AVAILABLE", location.getVersion(),
command.operatorId()) != 1)) {
if (allocated == null || allocated < 0) {
throw new BizException(ResultCode.CONFLICT, "释放库位资源时发生并发变更");
}
releaseOccupiedResource(occupancy, command.reason(), command.operatorId(), "释放库位资源时发生并发变更");
result.add(new ExactSelectiveReleaseResult(occupancy.getReservationId(), occupancy.getId(), index + 1,
occupancy.getWarehouseId(), occupancy.getPalletId(), occupancy.getLocationId(), occupancy.getLocationCode(), allocated));
}
return List.copyOf(result);
}
/**
* Occupancy, confirmed reservation and the materialized location status form one resource fact.
* Lock the location before changing either fact so the final availability decision is serialized.
*/
private void releaseOccupiedResource(LocationOccupancyEntity occupancy, String reason, long operatorId,
String conflictMessage) {
LocationEntity location = repository.lockOwned(occupancy.getTenantId(), occupancy.getWarehouseId(),
occupancy.getLocationId());
LocationReservationEntity reservation = repository.lockReservation(
occupancy.getTenantId(), occupancy.getReservationId());
if (!matchesConfirmedResource(occupancy, reservation) || location == null) {
warnReleaseConflict(occupancy, location, reservation, 0, 0, -1, "RESOURCE_FACT_MISMATCH");
throw new BizException(ResultCode.CONFLICT, conflictMessage);
}
int occupancyAffected = repository.releaseOccupancy(occupancy, reason);
if (occupancyAffected != 1) {
warnReleaseConflict(occupancy, location, reservation, occupancyAffected, 0, -1,
"OCCUPANCY_UPDATE_MISSED");
throw new BizException(ResultCode.CONFLICT, conflictMessage);
}
int reservationAffected = repository.releaseConfirmedReservation(reservation, reason);
if (reservationAffected != 1) {
warnReleaseConflict(occupancy, location, reservation, occupancyAffected, reservationAffected, -1,
"RESERVATION_UPDATE_MISSED");
throw new BizException(ResultCode.CONFLICT, conflictMessage);
}
if (Boolean.TRUE.equals(location.getSharedOverflow())) {
return;
}
int remainingActive = repository.countActiveOccupancies(occupancy.getTenantId(), occupancy.getLocationId());
if (remainingActive > 0) {
warnReleaseConflict(occupancy, location, reservation, occupancyAffected, reservationAffected,
remainingActive, "NON_SHARED_LOCATION_STILL_OCCUPIED");
throw new BizException(ResultCode.CONFLICT, conflictMessage);
}
if ("AVAILABLE".equals(location.getStatus())) {
log.warn("warehouse_location_release_converged reservationId={} occupancyId={} locationId={} "
+ "expectedLocationStatus=OCCUPIED actualLocationStatus={} occupancyAffected={} "
+ "reservationAffected={} remainingActiveOccupancies={}",
occupancy.getReservationId(), occupancy.getId(), occupancy.getLocationId(), location.getStatus(),
occupancyAffected, reservationAffected, remainingActive);
return;
}
if (!"OCCUPIED".equals(location.getStatus())
|| repository.changeStatus(occupancy.getTenantId(), occupancy.getWarehouseId(), occupancy.getLocationId(),
"OCCUPIED", "AVAILABLE", location.getVersion(), operatorId) != 1) {
warnReleaseConflict(occupancy, location, reservation, occupancyAffected, reservationAffected,
remainingActive, "LOCATION_STATUS_UPDATE_MISSED");
throw new BizException(ResultCode.CONFLICT, conflictMessage);
}
}
private static boolean matchesConfirmedResource(LocationOccupancyEntity occupancy,
LocationReservationEntity reservation) {
return reservation != null
&& "CONFIRMED".equals(reservation.getReservationStatus())
&& Objects.equals(reservation.getWarehouseId(), occupancy.getWarehouseId())
&& Objects.equals(reservation.getBusinessId(), occupancy.getBusinessId())
&& Objects.equals(reservation.getLocationId(), occupancy.getLocationId())
&& Objects.equals(reservation.getPalletId(), occupancy.getPalletId())
&& Objects.equals(reservation.getSourceService(), occupancy.getSourceService());
}
private static void warnReleaseConflict(LocationOccupancyEntity occupancy, LocationEntity location,
LocationReservationEntity reservation, int occupancyAffected,
int reservationAffected, int remainingActive, String cause) {
log.warn("warehouse_location_release_conflict cause={} reservationId={} occupancyId={} locationId={} "
+ "expectedReservationStatus=CONFIRMED actualReservationStatus={} "
+ "expectedOccupancyStatus=OCCUPIED actualOccupancyStatus={} "
+ "expectedLocationStatus=OCCUPIED actualLocationStatus={} occupancyAffected={} "
+ "reservationAffected={} remainingActiveOccupancies={}",
cause, occupancy.getReservationId(), occupancy.getId(), occupancy.getLocationId(),
reservation == null ? null : reservation.getReservationStatus(), occupancy.getOccupancyStatus(),
location == null ? null : location.getStatus(), occupancyAffected, reservationAffected, remainingActive);
}
/**
* 将未拣货的已占用库位从主入库单转给二级子入库单。
* 转移只改变业务归属和剩余箱数,不释放库位;托盘仅作为关联快照。
@@ -85,9 +85,7 @@ ON DUPLICATE KEY UPDATE
location_type=VALUES(location_type),
max_weight_kg=VALUES(max_weight_kg),
shared_overflow=VALUES(shared_overflow),
status='AVAILABLE',
remark=VALUES(remark),
deleted=0;
remark=VALUES(remark);
INSERT INTO wh_location(
id,tenant_id,warehouse_id,zone_code,zone_name,location_code,location_name,
@@ -106,9 +104,7 @@ ON DUPLICATE KEY UPDATE
location_type=VALUES(location_type),
max_weight_kg=VALUES(max_weight_kg),
shared_overflow=VALUES(shared_overflow),
status='AVAILABLE',
remark=VALUES(remark),
deleted=0;
remark=VALUES(remark);
DROP PROCEDURE IF EXISTS assert_yiwu_location_seed;
DELIMITER //