This commit is contained in:
2026-08-06 15:48:21 +08:00
parent 4df8b8c355
commit c3a978b497
71 changed files with 6268 additions and 13 deletions
@@ -62,6 +62,16 @@ public class InternalPalletLocationController {
return ApiResponse.success(service.release(request));
}
@PostMapping("/release-selected")
@Operation(summary = "Release selected occupied locations for partial picking",
description = "Explicit IDs release only those locations; otherwise the first N allocations are released")
public ApiResponse<List<PalletAllocationDtos.SelectiveReleaseResult>> releaseSelected(
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
@Valid @RequestBody PalletAllocationDtos.SelectiveReleaseRequest request) {
verify(token);
return ApiResponse.success(service.releaseSelected(request));
}
private void verify(String token) {
byte[] supplied = token == null ? new byte[0] : token.getBytes(StandardCharsets.UTF_8);
if (!MessageDigest.isEqual(expectedToken, supplied)) {
@@ -138,6 +138,30 @@ public final class LocationDtos {
) {
}
public record SelectiveReleaseCommand(
@Min(1) long tenantId,
@NotBlank @Size(max = 40) String sourceService,
@Min(1) long businessId,
@Min(1) long operatorId,
@NotBlank @Size(max = 500) String reason,
@Min(1) int totalPackageCount,
@Size(max = 200) List<@Min(1) Long> locationIds,
@Min(1) Integer releaseLocationCount
) {
}
public record SelectiveReleaseResult(
long reservationId,
long occupancyId,
int allocationNo,
long warehouseId,
long palletId,
long locationId,
String locationCode,
int releasedPackageCount
) {
}
public record ActiveResource(
long reservationId,
String reservationNo,
@@ -52,6 +52,33 @@ public final class PalletAllocationDtos {
) {
}
/**
* Releases occupied locations for a partial pick. When {@code locationIds} is empty,
* the first {@code releaseLocationCount} allocations are selected.
*/
public record SelectiveReleaseRequest(
@Min(1) long tenantId,
@NotBlank @Size(max = 40) String sourceService,
@Min(1) long businessId,
@Min(1) long operatorId,
@NotBlank @Size(max = 500) String reason,
@Min(1) int totalPackageCount,
@Size(max = 200) List<@Min(1) Long> locationIds,
@Min(1) Integer releaseLocationCount
) {
}
public record SelectiveReleaseResult(
long reservationId,
long occupancyId,
int allocationNo,
long palletId,
long locationId,
String locationCode,
int releasedPackageCount
) {
}
public record AllocationResult(
long reservationId,
String reservationNo,
@@ -7,6 +7,8 @@ import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import java.util.List;
/** 库位占用事实 Mapper。 */
@Mapper
public interface LocationOccupancyMapper extends BaseMapper<LocationOccupancyEntity> {
@@ -26,4 +28,15 @@ public interface LocationOccupancyMapper extends BaseMapper<LocationOccupancyEnt
""")
int release(@Param("tenantId") long tenantId, @Param("id") long id,
@Param("version") int version, @Param("reason") String reason);
@Select("""
SELECT * FROM wh_location_occupancy
WHERE tenant_id = #{tenantId} AND source_service = #{sourceService}
AND business_id = #{businessId} AND occupancy_status = 'OCCUPIED'
ORDER BY reservation_id ASC, id ASC
FOR UPDATE
""")
List<LocationOccupancyEntity> lockOccupiedByBusiness(@Param("tenantId") long tenantId,
@Param("sourceService") String sourceService,
@Param("businessId") long businessId);
}
@@ -143,6 +143,11 @@ public class LocationRepository {
.eq(LocationOccupancyEntity::getReservationId, reservationId));
}
public List<LocationOccupancyEntity> lockOccupiedByBusiness(long tenantId, String sourceService,
long businessId) {
return occupancies.lockOccupiedByBusiness(tenantId, sourceService, businessId);
}
public void insertOccupancy(LocationOccupancyEntity entity) {
occupancies.insert(entity);
}
@@ -11,6 +11,8 @@ import com.baiyue.warehouse.dto.LocationDtos.ReleaseCommand;
import com.baiyue.warehouse.dto.LocationDtos.ReservationResult;
import com.baiyue.warehouse.dto.LocationDtos.ReservationTarget;
import com.baiyue.warehouse.dto.LocationDtos.ReserveCommand;
import com.baiyue.warehouse.dto.LocationDtos.SelectiveReleaseCommand;
import com.baiyue.warehouse.dto.LocationDtos.SelectiveReleaseResult;
import com.baiyue.warehouse.entity.LocationEntity;
import com.baiyue.warehouse.entity.LocationOccupancyEntity;
import com.baiyue.warehouse.entity.LocationReservationEntity;
@@ -31,7 +33,6 @@ import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.List;
import java.util.Map;
@@ -46,6 +47,8 @@ import java.util.Set;
public class LocationResourceService {
@Resource
private LocationRepository repository;
@Resource
private PickingPackageAllocator packageAllocator;
public List<LocationEntity> findAvailable(@Min(1) long tenantId, @Min(1) long warehouseId,
@Min(1) @Max(200) int limit) {
@@ -182,6 +185,66 @@ public class LocationResourceService {
return List.copyOf(released);
}
@Transactional
public List<SelectiveReleaseResult> releaseSelected(@Valid SelectiveReleaseCommand command) {
List<LocationOccupancyEntity> active = repository.lockOccupiedByBusiness(
command.tenantId(), command.sourceService(), command.businessId());
if (active.isEmpty()) {
throw new BizException(ResultCode.CONFLICT, "No occupied locations are available for this inbound order");
}
requireLimit(active.size());
List<Integer> packages = packageAllocator.allocate(command.totalPackageCount(), active.size());
Set<Long> requestedIds = command.locationIds() == null
? Set.of() : new HashSet<>(command.locationIds());
if (requestedIds.size() != (command.locationIds() == null ? 0 : command.locationIds().size())) {
throw new BizException(ResultCode.BAD_REQUEST, "Selected location IDs must be unique");
}
List<Integer> selectedIndexes = new ArrayList<>();
if (!requestedIds.isEmpty()) {
for (int index = 0; index < active.size(); index++) {
if (requestedIds.contains(active.get(index).getLocationId())) {
selectedIndexes.add(index);
}
}
if (selectedIndexes.size() != requestedIds.size()) {
throw new BizException(ResultCode.FORBIDDEN,
"A selected location does not belong to the current inbound order");
}
if (command.releaseLocationCount() != null
&& command.releaseLocationCount() != selectedIndexes.size()) {
throw new BizException(ResultCode.BAD_REQUEST,
"releaseLocationCount must match the selected location count");
}
} else {
if (command.releaseLocationCount() == null || command.releaseLocationCount() > active.size()) {
throw new BizException(ResultCode.BAD_REQUEST,
"releaseLocationCount must not exceed the active location count");
}
for (int index = 0; index < command.releaseLocationCount(); index++) {
selectedIndexes.add(index);
}
}
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");
}
released.add(new SelectiveReleaseResult(occupancy.getReservationId(), occupancy.getId(), index + 1,
occupancy.getWarehouseId(), occupancy.getPalletId(), occupancy.getLocationId(),
occupancy.getLocationCode(), packages.get(index)));
}
return List.copyOf(released);
}
@Transactional(readOnly = true)
public List<ActiveResource> findActiveResources(@Min(1) long tenantId,
@NotBlank String sourceService,
@@ -129,6 +129,28 @@ public class PalletLocationAllocationService {
return released;
}
@Transactional
@BusinessAudit(event = "PALLET_LOCATION_RELEASE_SELECTED", resourceType = "INBOUND",
resourceId = "#request.businessId", description = "Release selected occupied locations for picking")
public List<PalletAllocationDtos.SelectiveReleaseResult> releaseSelected(
PalletAllocationDtos.SelectiveReleaseRequest request) {
List<LocationDtos.SelectiveReleaseResult> released = locationResourceService.releaseSelected(
new LocationDtos.SelectiveReleaseCommand(request.tenantId(), request.sourceService(), request.businessId(),
request.operatorId(), request.reason(), request.totalPackageCount(), request.locationIds(),
request.releaseLocationCount()));
List<PalletAllocationDtos.SelectiveReleaseResult> result = new ArrayList<>(released.size());
for (LocationDtos.SelectiveReleaseResult resource : released) {
if (palletRepository.transitionStatus(request.tenantId(), resource.warehouseId(), resource.palletId(),
List.of("OCCUPIED"), "AVAILABLE") != 1) {
throw new BizException(ResultCode.CONFLICT, "Pallet changed concurrently during selected release");
}
result.add(new PalletAllocationDtos.SelectiveReleaseResult(resource.reservationId(),
resource.occupancyId(), resource.allocationNo(), resource.palletId(), resource.locationId(),
resource.locationCode(), resource.releasedPackageCount()));
}
return List.copyOf(result);
}
private List<LocationDtos.ActiveResource> active(long tenantId, String sourceService, long businessId) {
return new ArrayList<>(locationResourceService.findActiveResources(tenantId, sourceService, businessId));
}
@@ -0,0 +1,27 @@
package com.baiyue.warehouse.service;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/** Allocates cartons by location with a ceiling-sized leading share. */
@Component
public class PickingPackageAllocator {
public List<Integer> allocate(int totalPackageCount, int locationCount) {
if (totalPackageCount < 1 || locationCount < 1 || locationCount > 200) {
throw new BizException(ResultCode.BAD_REQUEST, "Package and location counts must be positive");
}
int share = ((totalPackageCount - 1) / locationCount) + 1;
int remaining = totalPackageCount;
List<Integer> result = new ArrayList<>(locationCount);
for (int index = 0; index < locationCount; index++) {
int allocated = Math.min(share, remaining);
result.add(allocated);
remaining -= allocated;
}
return List.copyOf(result);
}
}