|
|
|
@@ -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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 将未拣货的已占用库位从主入库单转给二级子入库单。
|
|
|
|
|
* 转移只改变业务归属和剩余箱数,不释放库位;托盘仅作为关联快照。
|
|
|
|
|