优化
This commit is contained in:
+1
-1
@@ -102,7 +102,7 @@ public class InternalPalletLocationController {
|
||||
}
|
||||
|
||||
@PostMapping("/rebind-temporary-location")
|
||||
@Operation(summary = "A0临时库位换绑", description = "将一条具体A0托盘占用换绑到同仓空闲普通库位")
|
||||
@Operation(summary = "A0临时库位换绑", description = "将一条具体A0库位占用记录换绑到同仓空闲普通库位")
|
||||
public ApiResponse<PalletAllocationDtos.RebindTemporaryLocationResult> rebindTemporaryLocation(
|
||||
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||
@Valid @RequestBody PalletAllocationDtos.RebindTemporaryLocationRequest request) {
|
||||
|
||||
+1
-1
@@ -137,7 +137,7 @@ public class PlatformLocationController {
|
||||
|
||||
@PostMapping("/temporary-occupancies/{occupancyId}/rebind")
|
||||
@PreAuthorize("hasAuthority('platform:location:edit')")
|
||||
@Operation(summary = "A0临时库位换绑", description = "将指定A0托盘占用换绑到同仓空闲普通库位")
|
||||
@Operation(summary = "A0临时库位换绑", description = "将指定A0库位占用记录换绑到同仓空闲普通库位")
|
||||
public ApiResponse<PalletAllocationDtos.RebindTemporaryLocationResult> rebindTemporaryLocation(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Min(1) long occupancyId,
|
||||
@Valid @RequestBody RebindRequest request) {
|
||||
|
||||
@@ -67,7 +67,7 @@ public final class LocationDtos {
|
||||
public record Detail(LocationEntity location, String warehouseCode, String warehouseName) {
|
||||
}
|
||||
|
||||
/** 单个托盘的幂等预占目标。 */
|
||||
/** 单条库位预占目标;托盘字段仅引用可复用主数据快照。 */
|
||||
public record ReservationTarget(
|
||||
@NotNull @Min(1) Long palletId,
|
||||
@NotBlank @Size(max = 50) String palletCode,
|
||||
|
||||
@@ -131,7 +131,7 @@ public final class PalletAllocationDtos {
|
||||
) {
|
||||
}
|
||||
|
||||
/** 将共享临时库位 A0 中的一条具体托盘占用换绑到同仓空闲普通库位。 */
|
||||
/** 将共享临时库位 A0 中的一条具体库位占用换绑到同仓空闲普通库位。 */
|
||||
public record RebindTemporaryLocationRequest(
|
||||
@Min(0) long tenantId,
|
||||
@Min(1) long occupancyId,
|
||||
@@ -155,7 +155,7 @@ public final class PalletAllocationDtos {
|
||||
) {
|
||||
}
|
||||
|
||||
/** 仅转移占用归属和未拣出箱数,不释放库位或托盘。 */
|
||||
/** 仅转移库位占用归属和未拣出箱数;托盘仅作为主数据快照关联。 */
|
||||
public record TransferRequest(
|
||||
@Min(0) long tenantId,
|
||||
@NotBlank @Size(max = 40) String sourceService,
|
||||
@@ -168,7 +168,7 @@ public final class PalletAllocationDtos {
|
||||
) {
|
||||
}
|
||||
|
||||
/** 子入库单保存托盘库位快照所需的完整仓储分配结果。 */
|
||||
/** 子入库单保存托盘主数据和库位快照所需的完整仓储分配结果。 */
|
||||
public record TransferResult(
|
||||
long reservationId,
|
||||
String reservationNo,
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
/** 已占用托盘库位选择性释放的幂等操作 Mapper。 */
|
||||
/** 已占用库位选择性释放的幂等操作 Mapper。 */
|
||||
@Mapper
|
||||
public interface PalletLocationReleaseOperationMapper extends BaseMapper<PalletLocationReleaseOperationEntity> {
|
||||
@Insert("""
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
/** 托盘库位转移幂等操作 Mapper。 */
|
||||
/** 库位归属转移幂等操作 Mapper。 */
|
||||
@Mapper
|
||||
public interface PalletLocationTransferOperationMapper extends BaseMapper<PalletLocationTransferOperationEntity> {
|
||||
@Insert("""
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import com.baiyue.warehouse.mapper.PalletLocationReleaseOperationMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
/** 已占用托盘库位选择性释放的持久化幂等边界。 */
|
||||
/** 已占用库位选择性释放的持久化幂等边界。 */
|
||||
@Repository
|
||||
public class PalletLocationReleaseOperationRepository {
|
||||
@Resource
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import com.baiyue.warehouse.mapper.PalletLocationTransferOperationMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
/** 托盘库位归属转移的幂等操作持久化边界。 */
|
||||
/** 库位归属转移的幂等操作持久化边界。 */
|
||||
@Repository
|
||||
public class PalletLocationTransferOperationRepository {
|
||||
@Resource
|
||||
|
||||
@@ -108,7 +108,7 @@ public class LocationResourceService {
|
||||
result.add(toReservationResult(reservation));
|
||||
}
|
||||
} catch (DataIntegrityViolationException exception) {
|
||||
throw new BizException(ResultCode.CONFLICT, "库位预占幂等键、托盘或库位已被占用");
|
||||
throw new BizException(ResultCode.CONFLICT, "库位预占幂等键重复或库位已存在活动预占");
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
@@ -151,7 +151,7 @@ public class LocationResourceService {
|
||||
result.add(toOccupancyResult(occupancy));
|
||||
}
|
||||
} catch (DataIntegrityViolationException exception) {
|
||||
throw new BizException(ResultCode.CONFLICT, "库位或托盘已存在活动占用");
|
||||
throw new BizException(ResultCode.CONFLICT, "库位已存在活动占用或预占记录已确认");
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
@@ -336,7 +336,7 @@ public class LocationResourceService {
|
||||
|
||||
/**
|
||||
* 将未拣货的已占用库位从主入库单转给二级子入库单。
|
||||
* 转移只改变业务归属和剩余箱数,不释放库位或托盘。
|
||||
* 转移只改变业务归属和剩余箱数,不释放库位;托盘仅作为关联快照。
|
||||
*/
|
||||
@Transactional
|
||||
public List<LocationDtos.TransferResult> transferToChild(@Valid LocationDtos.TransferCommand command) {
|
||||
@@ -394,7 +394,7 @@ public class LocationResourceService {
|
||||
return List.copyOf(results);
|
||||
}
|
||||
|
||||
/** PDA 结算按预约记录转移未拣货资源,物理托盘和库位继续保持占用。 */
|
||||
/** PDA 结算按预约记录转移未拣货资源,库位继续保持占用,托盘仅作为主数据快照关联。 */
|
||||
@Transactional
|
||||
public List<LocationDtos.TransferResult> transferToChildByReservation(
|
||||
@Valid LocationDtos.ReservationTransferCommand command) {
|
||||
@@ -471,7 +471,7 @@ public class LocationResourceService {
|
||||
.toList();
|
||||
}
|
||||
|
||||
/** 将 A0 的一条托盘占用换绑到同仓空闲普通库位。 */
|
||||
/** 将 A0 的一条库位占用记录换绑到同仓空闲普通库位,保留托盘主数据关联。 */
|
||||
@Transactional
|
||||
public LocationDtos.RebindTemporaryLocationResult rebindTemporaryLocation(
|
||||
@Valid LocationDtos.RebindTemporaryLocationCommand command) {
|
||||
@@ -648,7 +648,7 @@ public class LocationResourceService {
|
||||
|| reservation.getWarehouseId() != command.warehouseId()
|
||||
|| reservation.getPalletId() != target.palletId()
|
||||
|| !reservation.getPalletCode().equals(target.palletCode())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "库位预占幂等键已用于其他业务或托盘");
|
||||
throw new BizException(ResultCode.CONFLICT, "库位预占幂等键已用于其他业务或托盘主数据");
|
||||
}
|
||||
if ("RELEASED".equals(reservation.getReservationStatus()) || "EXPIRED".equals(reservation.getReservationStatus())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "库位预占幂等请求已释放或过期");
|
||||
@@ -684,11 +684,12 @@ public class LocationResourceService {
|
||||
}
|
||||
|
||||
private static void validateUniqueTargets(List<ReservationTarget> targets) {
|
||||
Set<Long> pallets = new HashSet<>();
|
||||
Set<String> keys = new HashSet<>();
|
||||
for (ReservationTarget target : targets) {
|
||||
if (!pallets.add(target.palletId()) || !keys.add(target.idempotencyKey())) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "同批预占的托盘和幂等键不得重复");
|
||||
// Pallets are reusable master-data references. Only the idempotency key
|
||||
// identifies a unique reservation target within one batch.
|
||||
if (!keys.add(target.idempotencyKey())) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "同批预占的幂等键不得重复");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -185,7 +185,7 @@ public class PalletLocationAllocationService {
|
||||
*/
|
||||
@Transactional
|
||||
@BusinessAudit(event = "PALLET_LOCATION_SETTLEMENT", resourceType = "INBOUND",
|
||||
resourceId = "#request.sourceBusinessId", description = "PDA拣货后原子结算托盘库位资源")
|
||||
resourceId = "#request.sourceBusinessId", description = "PDA拣货后原子结算库位资源")
|
||||
public PalletAllocationDtos.SettlementResult settle(PalletAllocationDtos.SettlementRequest request) {
|
||||
validateSettlementScope(request);
|
||||
String key = request.idempotencyKey().trim();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
-- PDA 已拣满库位/托盘精确释放的持久化幂等契约;可在二期库位表建表后重复执行。
|
||||
-- PDA 已拣满库位精确释放的持久化幂等契约;可在二期库位表建表后重复执行。
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS wh_pallet_location_release_operation (
|
||||
@@ -16,4 +16,4 @@ CREATE TABLE IF NOT EXISTS wh_pallet_location_release_operation (
|
||||
UNIQUE KEY uk_pallet_location_release_idempotency
|
||||
(tenant_id, source_service, business_id, idempotency_key),
|
||||
KEY idx_pallet_location_release_business (tenant_id, source_service, business_id, operation_status)
|
||||
) ENGINE=InnoDB COMMENT='PDA已拣满托盘库位精确释放的持久化幂等操作记录';
|
||||
) ENGINE=InnoDB COMMENT='PDA已拣满库位精确释放的持久化幂等操作记录';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
-- PDA 拣货资源原子结算幂等契约;同一数据库事务内完成精确释放和剩余资源转移。
|
||||
-- PDA 拣货库位资源原子结算幂等契约;同一数据库事务内完成精确释放和剩余资源转移。
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS wh_pallet_location_settlement_operation (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
-- 托盘库位转移的持久化幂等契约;可在二期托盘库位表建表后重复执行。
|
||||
-- 库位归属转移的持久化幂等契约;可在二期托盘库位表建表后重复执行。
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS wh_pallet_location_transfer_operation (
|
||||
@@ -17,4 +17,4 @@ CREATE TABLE IF NOT EXISTS wh_pallet_location_transfer_operation (
|
||||
UNIQUE KEY uk_pallet_location_transfer_idempotency
|
||||
(tenant_id, source_service, source_business_id, target_business_id, idempotency_key),
|
||||
KEY idx_pallet_location_transfer_source (tenant_id, source_service, source_business_id, operation_status)
|
||||
) ENGINE=InnoDB COMMENT='托盘库位归属转移的持久化幂等操作记录';
|
||||
) ENGINE=InnoDB COMMENT='库位归属转移的持久化幂等操作记录';
|
||||
|
||||
@@ -14,6 +14,7 @@ import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
@@ -25,6 +26,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
@@ -117,6 +119,37 @@ class LocationResourceServiceTest {
|
||||
.containsExactly("inbound-1-p1", "inbound-1-p2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void allowsReusablePalletMasterInMultipleReservationTargets() {
|
||||
when(repository.findWarehouse(10001L, 88L)).thenReturn(warehouse());
|
||||
when(repository.lockAvailable(10001L, 88L, 2))
|
||||
.thenReturn(List.of(location(9L, "A1"), location(10L, "A2")));
|
||||
when(repository.changeStatus(10001L, 88L, 9L, "AVAILABLE", "RESERVED", 0, 7L)).thenReturn(1);
|
||||
when(repository.changeStatus(10001L, 88L, 10L, "AVAILABLE", "RESERVED", 0, 7L)).thenReturn(1);
|
||||
|
||||
var command = new ReserveCommand(10001L, 88L, "FIRSTLEG", 301L, "RK001", 7L,
|
||||
LocalDateTime.now().plusMinutes(30), List.of(
|
||||
new ReservationTarget(501L, "P1", "same-pallet-1"),
|
||||
new ReservationTarget(501L, "P1", "same-pallet-2")));
|
||||
|
||||
assertThat(service.reserveAvailable(command)).hasSize(2);
|
||||
verify(repository, times(2)).insertReservation(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void reservationConstraintFailureDoesNotDescribePalletAsAResource() {
|
||||
when(repository.findWarehouse(10001L, 88L)).thenReturn(warehouse());
|
||||
when(repository.lockAvailable(10001L, 88L, 2))
|
||||
.thenReturn(List.of(location(9L, "A1"), location(10L, "A2")));
|
||||
when(repository.changeStatus(10001L, 88L, 9L, "AVAILABLE", "RESERVED", 0, 7L)).thenReturn(1);
|
||||
doThrow(new DataIntegrityViolationException("duplicate location reservation"))
|
||||
.when(repository).insertReservation(any());
|
||||
|
||||
assertThatThrownBy(() -> service.reserveAvailable(reserveCommand()))
|
||||
.hasMessageContaining("库位预占幂等键重复或库位已存在活动预占")
|
||||
.hasMessageNotContaining("托盘");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleCase04() {
|
||||
when(repository.findWarehouse(10001L, 88L)).thenReturn(warehouse());
|
||||
|
||||
Reference in New Issue
Block a user