优化
This commit is contained in:
+2
-1
@@ -41,7 +41,8 @@ public class InternalPalletLocationController {
|
||||
}
|
||||
|
||||
@PostMapping("/reservations")
|
||||
@Operation(summary = "按托盘主数据拆分并预占库位")
|
||||
@Operation(summary = "按已确认称重图片逐条预占库位",
|
||||
description = "一张图片对应一条库位预占;托盘仅作为可重复引用的主数据,不按容量拆分或限制")
|
||||
public ApiResponse<PalletAllocationDtos.BatchResult> reserve(@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||
@Valid @RequestBody PalletAllocationDtos.ReserveRequest request) {
|
||||
verify(token);
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.warehouse.dto.LocationDtos;
|
||||
import com.baiyue.warehouse.dto.PalletAllocationDtos;
|
||||
import com.baiyue.warehouse.dto.LocationDtos.ActiveResource;
|
||||
import com.baiyue.warehouse.dto.LocationDtos.ConfirmCommand;
|
||||
import com.baiyue.warehouse.dto.LocationDtos.ExactSelectiveReleaseCommand;
|
||||
@@ -284,7 +285,7 @@ public class LocationResourceService {
|
||||
continue;
|
||||
}
|
||||
Integer allocatedPackageCount = occupancy.getAllocatedPackageCount();
|
||||
if (allocatedPackageCount == null || allocatedPackageCount < 1) {
|
||||
if (allocatedPackageCount == null || allocatedPackageCount < 0) {
|
||||
throw new BizException(ResultCode.CONFLICT,
|
||||
"Occupied location has no valid allocated package count and cannot be released exactly");
|
||||
}
|
||||
@@ -322,7 +323,7 @@ public class LocationResourceService {
|
||||
if (!requested.contains(occupancy.getReservationId())) continue;
|
||||
Integer allocated = occupancy.getAllocatedPackageCount();
|
||||
LocationEntity location = repository.findOwned(command.tenantId(), occupancy.getWarehouseId(), occupancy.getLocationId());
|
||||
if (allocated == null || allocated < 1 || location == null || repository.releaseOccupancy(occupancy, command.reason()) != 1
|
||||
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)) {
|
||||
@@ -567,7 +568,7 @@ public class LocationResourceService {
|
||||
|
||||
private static int requireAllocatedPackageCount(LocationOccupancyEntity occupancy) {
|
||||
Integer allocated = occupancy.getAllocatedPackageCount();
|
||||
if (allocated == null || allocated < 1) {
|
||||
if (allocated == null || allocated < 0) {
|
||||
throw new BizException(ResultCode.CONFLICT, "已占用资源缺少有效分配箱数");
|
||||
}
|
||||
return allocated;
|
||||
@@ -593,6 +594,33 @@ public class LocationResourceService {
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
/** 活动分配重放必须与原始称重图片幂等键及托盘引用完全一致。 */
|
||||
@Transactional(readOnly = true)
|
||||
public void requireSameActiveReservations(long tenantId, long warehouseId, String sourceService,
|
||||
long businessId, String requestIdempotencyKey,
|
||||
List<PalletAllocationDtos.PalletReference> references) {
|
||||
Map<String, Long> expected = references.stream().collect(Collectors.toMap(
|
||||
item -> requestIdempotencyKey + ":" + item.weightResultId(),
|
||||
PalletAllocationDtos.PalletReference::palletId,
|
||||
(first, duplicate) -> {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "同一张称重图片不能重复预占库位");
|
||||
}));
|
||||
List<LocationReservationEntity> active = repository.findReservationsByBusiness(
|
||||
tenantId, sourceService, businessId).stream()
|
||||
.filter(item -> "RESERVED".equals(item.getReservationStatus())
|
||||
|| "CONFIRMED".equals(item.getReservationStatus()))
|
||||
.toList();
|
||||
Map<String, Long> actual = active.stream().collect(Collectors.toMap(
|
||||
LocationReservationEntity::getIdempotencyKey, LocationReservationEntity::getPalletId,
|
||||
(first, duplicate) -> {
|
||||
throw new BizException(ResultCode.CONFLICT, "活动库位分配存在重复幂等键");
|
||||
}));
|
||||
boolean sameWarehouse = active.stream().allMatch(item -> item.getWarehouseId() == warehouseId);
|
||||
if (!sameWarehouse || !actual.equals(expected)) {
|
||||
throw new BizException(ResultCode.CONFLICT, "活动库位分配与本次称重图片引用不一致");
|
||||
}
|
||||
}
|
||||
|
||||
private LocationReservationEntity reservation(ReserveCommand command, ReservationTarget target,
|
||||
LocationEntity location) {
|
||||
long id = IdWorker.getId();
|
||||
|
||||
+15
-7
@@ -53,10 +53,12 @@ public class PalletLocationAllocationService {
|
||||
@BusinessAudit(event = "PALLET_LOCATION_RESERVE", resourceType = "INBOUND", resourceId = "#request.businessId", description = "复用托盘主数据并预占库位", captureBefore = false)
|
||||
public PalletAllocationDtos.BatchResult reserve(PalletAllocationDtos.ReserveRequest request) {
|
||||
if (!Boolean.TRUE.equals(request.measurementCompleted())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "只能使用已完成测量的总体积拆分托盘并占用库位");
|
||||
throw new BizException(ResultCode.CONFLICT, "只能为已确认的称重图片预占库位");
|
||||
}
|
||||
List<LocationDtos.ActiveResource> existing = active(request.tenantId(), request.sourceService(), request.businessId());
|
||||
if (!existing.isEmpty()) {
|
||||
locationResourceService.requireSameActiveReservations(request.tenantId(), request.warehouseId(),
|
||||
request.sourceService(), request.businessId(), request.idempotencyKey(), request.pallets());
|
||||
return fromActive(request.tenantId(), request.warehouseId(), existing, request.totalVolumeCbm());
|
||||
}
|
||||
|
||||
@@ -69,12 +71,7 @@ public class PalletLocationAllocationService {
|
||||
pallets.add(pallet);
|
||||
}
|
||||
|
||||
List<LocationDtos.ReservationTarget> targets = new ArrayList<>(pallets.size());
|
||||
for (int index = 0; index < pallets.size(); index++) {
|
||||
PalletEntity pallet = pallets.get(index);
|
||||
targets.add(new LocationDtos.ReservationTarget(pallet.getId(), pallet.getPalletCode(),
|
||||
request.idempotencyKey() + ":" + request.pallets().get(index).weightResultId()));
|
||||
}
|
||||
List<LocationDtos.ReservationTarget> targets = reservationTargets(request, pallets);
|
||||
|
||||
List<LocationDtos.ReservationResult> reservations = locationResourceService.reserveAvailable(
|
||||
new LocationDtos.ReserveCommand(request.tenantId(), request.warehouseId(), request.sourceService(),
|
||||
@@ -523,6 +520,17 @@ public class PalletLocationAllocationService {
|
||||
return new ArrayList<>(locationResourceService.findActiveResources(tenantId, sourceService, businessId));
|
||||
}
|
||||
|
||||
private static List<LocationDtos.ReservationTarget> reservationTargets(
|
||||
PalletAllocationDtos.ReserveRequest request, List<PalletEntity> pallets) {
|
||||
List<LocationDtos.ReservationTarget> targets = new ArrayList<>(pallets.size());
|
||||
for (int index = 0; index < pallets.size(); index++) {
|
||||
PalletEntity pallet = pallets.get(index);
|
||||
targets.add(new LocationDtos.ReservationTarget(pallet.getId(), pallet.getPalletCode(),
|
||||
request.idempotencyKey() + ":" + request.pallets().get(index).weightResultId()));
|
||||
}
|
||||
return targets;
|
||||
}
|
||||
|
||||
public List<LocationDtos.OccupiedResource> occupiedResources(long tenantId, String sourceService,
|
||||
long businessId) {
|
||||
return locationResourceService.findOccupiedResources(tenantId, sourceService, businessId);
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
SET NAMES utf8mb4;
|
||||
USE baiyue_warehouse;
|
||||
|
||||
-- 编码由平台维护接口自动生成;两个国内作业仓各初始化一条可复用托盘主数据。
|
||||
-- 两个国内作业仓各初始化木托盘、塑料托盘各一条,共四条可复用托盘主数据。
|
||||
INSERT INTO wh_pallet(id,tenant_id,warehouse_id,pallet_code,pallet_type,capacity_cbm,weight_kg,color,remark,created_by,updated_by)
|
||||
VALUES
|
||||
(190000000000000101,0,190000000000000001,'TP-001','木托盘',1.200000,8.000,'黄色',NULL,0,0),
|
||||
(190000000000000102,0,190000000000000002,'TP-002','胶托盘',2.400000,12.000,'黑色',NULL,0,0)
|
||||
(190000000000000102,0,190000000000000002,'TP-002','塑料托盘',2.400000,12.000,'黑色',NULL,0,0),
|
||||
(190000000000000103,0,190000000000000001,'YW-TP-002','塑料托盘',2.400000,12.000,'黑色',NULL,0,0),
|
||||
(190000000000000104,0,190000000000000002,'DG-TP-002','木托盘',1.200000,8.000,'黄色',NULL,0,0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
tenant_id=0,
|
||||
warehouse_id=VALUES(warehouse_id),
|
||||
@@ -16,6 +18,31 @@ ON DUPLICATE KEY UPDATE
|
||||
color=VALUES(color),
|
||||
remark=VALUES(remark);
|
||||
|
||||
DROP PROCEDURE IF EXISTS assert_domestic_pallet_seed;
|
||||
DELIMITER //
|
||||
CREATE PROCEDURE assert_domestic_pallet_seed()
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT expected.warehouse_id
|
||||
FROM (
|
||||
SELECT 190000000000000001 AS warehouse_id
|
||||
UNION ALL SELECT 190000000000000002
|
||||
) expected
|
||||
LEFT JOIN wh_pallet pallet
|
||||
ON pallet.tenant_id=0
|
||||
AND pallet.warehouse_id=expected.warehouse_id
|
||||
AND pallet.pallet_type IN ('木托盘','塑料托盘')
|
||||
AND pallet.deleted=0
|
||||
GROUP BY expected.warehouse_id
|
||||
HAVING COUNT(DISTINCT pallet.pallet_type)<>2
|
||||
) THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='国内作业仓木托盘、塑料托盘初始化不完整';
|
||||
END IF;
|
||||
END//
|
||||
DELIMITER ;
|
||||
CALL assert_domestic_pallet_seed();
|
||||
DROP PROCEDURE IF EXISTS assert_domestic_pallet_seed;
|
||||
|
||||
-- 义乌仓初始化 A-F 六个库区,每区 A1-A15 共 90 个普通库位。
|
||||
-- A0 仍是系统共享溢出位;东莞仓继续保留最小 A1 + A0 基线。
|
||||
-- 这里只创建公共资源,不初始化预占、占用或业务单据。
|
||||
|
||||
+7
-2
@@ -28,8 +28,13 @@ class InitialWarehouseResourceSeedSqlContractTest {
|
||||
|
||||
private static void assertSeed(String sql, String yiwuId, String dongguanId, String idPrefix) {
|
||||
assertThat(sql)
|
||||
.contains("0," + yiwuId + ",'TP-001'")
|
||||
.contains("0," + dongguanId + ",'TP-002'")
|
||||
.contains("0," + yiwuId + ",'TP-001','木托盘'")
|
||||
.contains("0," + dongguanId + ",'TP-002','塑料托盘'")
|
||||
.contains("0," + yiwuId + ",'YW-TP-002','塑料托盘'")
|
||||
.contains("0," + dongguanId + ",'DG-TP-002','木托盘'")
|
||||
.contains("COUNT(DISTINCT pallet.pallet_type)")
|
||||
.contains("国内作业仓木托盘、塑料托盘初始化不完整")
|
||||
.doesNotContain("'胶托盘'")
|
||||
.contains("WHEN zones.zone_index = 0 AND slots.slot_number = 1 THEN 201")
|
||||
.contains("ELSE 203 + zones.zone_index * 15 + slots.slot_number")
|
||||
.contains("SELECT zone_index + 1 FROM zones WHERE zone_index < 5")
|
||||
|
||||
@@ -72,6 +72,43 @@ class LocationResourceServiceTest {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void preservesZeroPackageOccupiedResourceForImageBasedAllocation() {
|
||||
LocationOccupancyEntity active = occupancy(201L, 101L, 9L, "A1", 501L);
|
||||
active.setAllocatedPackageCount(0);
|
||||
when(repository.findOccupanciesByBusiness(10001L, "FIRSTLEG", 301L)).thenReturn(List.of(active));
|
||||
|
||||
assertThat(service.findOccupiedResources(10001L, "FIRSTLEG", 301L)).singleElement()
|
||||
.extracting(item -> item.allocatedPackageCount()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsReplayWhenConfirmedImageReferencesChanged() {
|
||||
LocationReservationEntity reservation = reservation("RESERVED");
|
||||
reservation.setIdempotencyKey("measure-001:7001");
|
||||
when(repository.findReservationsByBusiness(10001L, "FIRSTLEG", 301L))
|
||||
.thenReturn(List.of(reservation));
|
||||
|
||||
assertThatThrownBy(() -> service.requireSameActiveReservations(10001L, 88L, "FIRSTLEG", 301L,
|
||||
"measure-001", List.of(new com.baiyue.warehouse.dto.PalletAllocationDtos.PalletReference(7002L, 501L))))
|
||||
.isInstanceOf(com.baiyue.common.core.exception.BizException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void allowsReplayWhenMultipleImagesReuseSamePalletMaster() {
|
||||
LocationReservationEntity first = reservation("RESERVED");
|
||||
first.setIdempotencyKey("measure-001:7001");
|
||||
LocationReservationEntity second = reservation("RESERVED");
|
||||
second.setId(102L);
|
||||
second.setIdempotencyKey("measure-001:7002");
|
||||
when(repository.findReservationsByBusiness(10001L, "FIRSTLEG", 301L))
|
||||
.thenReturn(List.of(first, second));
|
||||
|
||||
service.requireSameActiveReservations(10001L, 88L, "FIRSTLEG", 301L, "measure-001", List.of(
|
||||
new com.baiyue.warehouse.dto.PalletAllocationDtos.PalletReference(7001L, 501L),
|
||||
new com.baiyue.warehouse.dto.PalletAllocationDtos.PalletReference(7002L, 501L)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleCase01() {
|
||||
when(repository.findWarehouse(10001L, 88L)).thenReturn(warehouse());
|
||||
@@ -257,6 +294,22 @@ class LocationResourceServiceTest {
|
||||
verify(repository, never()).releaseOccupancy(first, "PDA full pick");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReleaseZeroPackageLocationCreatedForConfirmedImage() {
|
||||
LocationOccupancyEntity occupancy = occupancy(201L, 101L, 9L, "A1", 501L);
|
||||
occupancy.setAllocatedPackageCount(0);
|
||||
when(repository.lockOccupiedByBusiness(10001L, "FIRSTLEG", 301L)).thenReturn(List.of(occupancy));
|
||||
when(repository.findOwned(10001L, 88L, 9L)).thenReturn(occupiedLocation(9L, "A1"));
|
||||
when(repository.releaseOccupancy(occupancy, "PDA full pick")).thenReturn(1);
|
||||
when(repository.changeStatus(10001L, 88L, 9L, "OCCUPIED", "AVAILABLE", 0, 7L)).thenReturn(1);
|
||||
|
||||
var result = service.releaseSelectedExactly(new com.baiyue.warehouse.dto.LocationDtos.ExactSelectiveReleaseCommand(
|
||||
10001L, "FIRSTLEG", 301L, 7L, "PDA full pick", List.of(9L)));
|
||||
|
||||
assertThat(result).singleElement()
|
||||
.extracting(item -> item.allocatedPackageCount()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReleasePublicResourcesInAllocationOrderWhenLocationsAreNotSpecified() {
|
||||
LocationOccupancyEntity allocationThree = occupancy(203L, 103L, 11L, "A3", 503L);
|
||||
|
||||
+21
@@ -122,6 +122,7 @@ class PalletLocationAllocationServiceTest {
|
||||
LocationDtos.ActiveResource resource = new LocationDtos.ActiveResource(
|
||||
101, null, 20, 30, "TP-001", 40, "A-01", false, "RESERVED");
|
||||
when(locations.findActiveResources(10001, "FIRSTLEG", 88)).thenReturn(List.of(resource));
|
||||
when(pallets.findOwnedIncludingDeleted(10001, 20, 30)).thenReturn(pallet(30, "TP-001"));
|
||||
|
||||
var result = service.reserve(reserve(88));
|
||||
|
||||
@@ -130,6 +131,26 @@ class PalletLocationAllocationServiceTest {
|
||||
verify(pallets, never()).findOwned(anyLong(), anyLong(), anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void validatesConfirmedImageReferencesBeforeReplayingAllocation() {
|
||||
LocationDtos.ActiveResource resource = new LocationDtos.ActiveResource(
|
||||
101L, null, 20L, 30L, "TP-001", 40L, "A-01", false, "RESERVED");
|
||||
when(locations.findActiveResources(10001L, "FIRSTLEG", 88L)).thenReturn(List.of(resource));
|
||||
when(pallets.findOwnedIncludingDeleted(10001L, 20L, 30L)).thenReturn(pallet(30L, "TP-001"));
|
||||
|
||||
service.reserve(reserve(88L));
|
||||
|
||||
org.mockito.ArgumentCaptor<List<PalletAllocationDtos.PalletReference>> targets =
|
||||
org.mockito.ArgumentCaptor.forClass(List.class);
|
||||
verify(locations).requireSameActiveReservations(eq(10001L), eq(20L), eq("FIRSTLEG"), eq(88L),
|
||||
eq("measure-001"), targets.capture());
|
||||
assertThat(targets.getValue()).singleElement().satisfies(item -> {
|
||||
assertThat(item.weightResultId()).isEqualTo(7001L);
|
||||
assertThat(item.palletId()).isEqualTo(30L);
|
||||
});
|
||||
verify(locations, never()).reserveAvailable(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmPersistsPackageCountOnEveryOccupancyTarget() {
|
||||
LocationDtos.ActiveResource resource = new LocationDtos.ActiveResource(
|
||||
|
||||
Reference in New Issue
Block a user