提交
This commit is contained in:
+10
@@ -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,
|
||||
|
||||
+22
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user