fix: preserve hold binding replays
This commit is contained in:
+8
-8
@@ -32,7 +32,6 @@ public class LoadingModelBridgeService {
|
|||||||
resourceId = "#loadingPlanId", description = "关联入库预装与柜次装柜计划")
|
resourceId = "#loadingPlanId", description = "关联入库预装与柜次装柜计划")
|
||||||
public Link bind(long tenantId, long inboundId, long loadingPlanId, long operatorId,
|
public Link bind(long tenantId, long inboundId, long loadingPlanId, long operatorId,
|
||||||
String operatorName, String idempotencyKey, BindRequest request) {
|
String operatorName, String idempotencyKey, BindRequest request) {
|
||||||
inboundHoldGuard.requireInboundNotHeld(tenantId, inboundId);
|
|
||||||
String key = validateKey(idempotencyKey);
|
String key = validateKey(idempotencyKey);
|
||||||
String requestHash = hash(tenantId + "|" + inboundId + "|" + loadingPlanId + "|"
|
String requestHash = hash(tenantId + "|" + inboundId + "|" + loadingPlanId + "|"
|
||||||
+ request.expectedPreloadVersion() + "|" + request.expectedLoadingPlanVersion());
|
+ request.expectedPreloadVersion() + "|" + request.expectedLoadingPlanVersion());
|
||||||
@@ -44,6 +43,7 @@ public class LoadingModelBridgeService {
|
|||||||
if (repository.byPair(tenantId, inboundId, loadingPlanId) != null) {
|
if (repository.byPair(tenantId, inboundId, loadingPlanId) != null) {
|
||||||
return requireLink(tenantId, inboundId, loadingPlanId);
|
return requireLink(tenantId, inboundId, loadingPlanId);
|
||||||
}
|
}
|
||||||
|
inboundHoldGuard.requireInboundNotHeld(tenantId, inboundId);
|
||||||
|
|
||||||
var preload = repository.preload(tenantId, inboundId);
|
var preload = repository.preload(tenantId, inboundId);
|
||||||
if (preload == null) throw new BizException(ResultCode.NOT_FOUND, "入库单预装计划不存在");
|
if (preload == null) throw new BizException(ResultCode.NOT_FOUND, "入库单预装计划不存在");
|
||||||
@@ -80,7 +80,6 @@ public class LoadingModelBridgeService {
|
|||||||
@Transactional
|
@Transactional
|
||||||
public List<Link> bindAllocatedBeforeLoading(long loadingPlanId, long operatorId,
|
public List<Link> bindAllocatedBeforeLoading(long loadingPlanId, long operatorId,
|
||||||
String operatorName, String orchestrationKey) {
|
String operatorName, String orchestrationKey) {
|
||||||
inboundHoldGuard.requirePlanNotHeld(loadingPlanId);
|
|
||||||
var loadingPlan = repository.loadingPlan(loadingPlanId);
|
var loadingPlan = repository.loadingPlan(loadingPlanId);
|
||||||
if (loadingPlan == null) throw new BizException(ResultCode.NOT_FOUND, "柜次装柜计划不存在");
|
if (loadingPlan == null) throw new BizException(ResultCode.NOT_FOUND, "柜次装柜计划不存在");
|
||||||
List<AllocationScope> scopes = repository.allocatedScopes(loadingPlanId);
|
List<AllocationScope> scopes = repository.allocatedScopes(loadingPlanId);
|
||||||
@@ -89,14 +88,15 @@ public class LoadingModelBridgeService {
|
|||||||
&& scopes.stream().anyMatch(scope -> !"WAIT_SPLIT".equals(scope.processStatus()))) {
|
&& scopes.stream().anyMatch(scope -> !"WAIT_SPLIT".equals(scope.processStatus()))) {
|
||||||
throw conflict("装柜清单仍有订单未完成审核和测量,不能开始PDA作业");
|
throw conflict("装柜清单仍有订单未完成审核和测量,不能开始PDA作业");
|
||||||
}
|
}
|
||||||
|
boolean allLinked = scopes.stream().allMatch(scope -> repository.byPair(
|
||||||
|
scope.tenantId(), scope.inboundId(), loadingPlanId) != null);
|
||||||
|
if (Set.of("EDITING", "LOADING", "COMPLETED").contains(loadingPlan.status()) && allLinked) {
|
||||||
|
return repository.byLoadingPlan(loadingPlanId);
|
||||||
|
}
|
||||||
if (!"EDITING".equals(loadingPlan.status())) {
|
if (!"EDITING".equals(loadingPlan.status())) {
|
||||||
if (Set.of("LOADING", "COMPLETED").contains(loadingPlan.status())
|
|
||||||
&& scopes.stream().allMatch(scope -> repository.byPair(
|
|
||||||
scope.tenantId(), scope.inboundId(), loadingPlanId) != null)) {
|
|
||||||
return repository.byLoadingPlan(loadingPlanId);
|
|
||||||
}
|
|
||||||
throw conflict("PDA开始前的预装关联不完整,禁止在LOADING后补关联");
|
throw conflict("PDA开始前的预装关联不完整,禁止在LOADING后补关联");
|
||||||
}
|
}
|
||||||
|
inboundHoldGuard.requirePlanNotHeld(loadingPlanId);
|
||||||
for (AllocationScope scope : scopes) {
|
for (AllocationScope scope : scopes) {
|
||||||
// Internal push replay is pair-idempotent: an existing association has
|
// Internal push replay is pair-idempotent: an existing association has
|
||||||
// already advanced both versions, so calling public bind again would
|
// already advanced both versions, so calling public bind again would
|
||||||
@@ -121,8 +121,8 @@ public class LoadingModelBridgeService {
|
|||||||
@Transactional
|
@Transactional
|
||||||
public void bindIncrementalAllocation(long loadingPlanId, long tenantId, long inboundId,
|
public void bindIncrementalAllocation(long loadingPlanId, long tenantId, long inboundId,
|
||||||
long operatorId, String operatorName, String operationId) {
|
long operatorId, String operatorName, String operationId) {
|
||||||
inboundHoldGuard.requireInboundNotHeld(tenantId, inboundId);
|
|
||||||
if (repository.byPair(tenantId, inboundId, loadingPlanId) != null) return;
|
if (repository.byPair(tenantId, inboundId, loadingPlanId) != null) return;
|
||||||
|
inboundHoldGuard.requireInboundNotHeld(tenantId, inboundId);
|
||||||
var plan = repository.loadingPlan(loadingPlanId);
|
var plan = repository.loadingPlan(loadingPlanId);
|
||||||
if (plan == null) throw new BizException(ResultCode.NOT_FOUND, "柜次装柜计划不存在");
|
if (plan == null) throw new BizException(ResultCode.NOT_FOUND, "柜次装柜计划不存在");
|
||||||
if (!Set.of("EDITING", "LOADING").contains(plan.status())) {
|
if (!Set.of("EDITING", "LOADING").contains(plan.status())) {
|
||||||
|
|||||||
+49
-2
@@ -31,7 +31,7 @@ class LoadingModelBridgeServiceTest {
|
|||||||
@InjectMocks LoadingModelBridgeService service;
|
@InjectMocks LoadingModelBridgeService service;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void heldInboundOrPlanBlocksEveryPreloadBindingWriteBoundary() {
|
void heldInboundOrPlanBlocksEveryNewPreloadBindingWriteBoundary() {
|
||||||
doThrow(new BizException(ResultCode.CONFLICT, "入库单已扣货"))
|
doThrow(new BizException(ResultCode.CONFLICT, "入库单已扣货"))
|
||||||
.when(inboundHoldGuard).requireInboundNotHeld(10L, 20L);
|
.when(inboundHoldGuard).requireInboundNotHeld(10L, 20L);
|
||||||
doThrow(new BizException(ResultCode.CONFLICT, "入库单已扣货"))
|
doThrow(new BizException(ResultCode.CONFLICT, "入库单已扣货"))
|
||||||
@@ -45,7 +45,41 @@ class LoadingModelBridgeServiceTest {
|
|||||||
40L, 10L, 20L, 7L, "admin", "incremental-held"))
|
40L, 10L, 20L, 7L, "admin", "incremental-held"))
|
||||||
.isInstanceOf(BizException.class).hasMessageContaining("扣货");
|
.isInstanceOf(BizException.class).hasMessageContaining("扣货");
|
||||||
|
|
||||||
verifyNoInteractions(repository);
|
verify(repository).byIdempotency(7L, "bind-held");
|
||||||
|
verify(repository, times(2)).byPair(10L, 20L, 40L);
|
||||||
|
verify(repository).loadingPlan(40L);
|
||||||
|
verifyNoMoreInteractions(repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void holdDoesNotBreakReadOnlyIdempotentBindingReplays() {
|
||||||
|
var replay = new PreloadLoadingPlanLinkEntity();
|
||||||
|
replay.setRequestHash(hashFor(10L, 20L, 40L, 1, 1));
|
||||||
|
when(repository.byIdempotency(7L, "bind-replay")).thenReturn(replay);
|
||||||
|
Link linked = link("PENDING", 2, "EDITING", 3, 8);
|
||||||
|
when(repository.byInbound(10L, 20L)).thenReturn(List.of(linked));
|
||||||
|
|
||||||
|
assertThat(service.bind(10L, 20L, 40L, 7L, "admin", "bind-replay",
|
||||||
|
new BindRequest(1, 1))).isEqualTo(linked);
|
||||||
|
|
||||||
|
verifyNoInteractions(inboundHoldGuard);
|
||||||
|
verify(repository, never()).insert(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void holdDoesNotBreakReadOnlyPlanOrIncrementalReplays() {
|
||||||
|
when(repository.loadingPlan(40L)).thenReturn(new LoadingPlanSnapshot(40L, 50L, "C1", "LOADING", 4));
|
||||||
|
when(repository.allocatedScopes(40L)).thenReturn(List.of(new AllocationScope(10L, 20L, 8, "WAIT_SPLIT")));
|
||||||
|
when(repository.byPair(10L, 20L, 40L)).thenReturn(new PreloadLoadingPlanLinkEntity());
|
||||||
|
Link linked = link("CONFIRMED", 3, "LOADING", 4, 8);
|
||||||
|
when(repository.byLoadingPlan(40L)).thenReturn(List.of(linked));
|
||||||
|
|
||||||
|
assertThat(service.bindAllocatedBeforeLoading(40L, 7L, "admin", "push-replay"))
|
||||||
|
.containsExactly(linked);
|
||||||
|
service.bindIncrementalAllocation(40L, 10L, 20L, 7L, "admin", "incremental-replay");
|
||||||
|
|
||||||
|
verifyNoInteractions(inboundHoldGuard);
|
||||||
|
verify(repository, never()).insert(any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -244,4 +278,17 @@ class LoadingModelBridgeServiceTest {
|
|||||||
return new Link(1L, 10L, 20L, 30L, preloadStatus, preloadVersion, 40L, 50L, "C1",
|
return new Link(1L, 10L, 20L, 30L, preloadStatus, preloadVersion, 40L, 50L, "C1",
|
||||||
loadingStatus, loadingVersion, packages, LocalDateTime.now());
|
loadingStatus, loadingVersion, packages, LocalDateTime.now());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String hashFor(long tenantId, long inboundId, long loadingPlanId,
|
||||||
|
int preloadVersion, int loadingPlanVersion) {
|
||||||
|
try {
|
||||||
|
var digest = java.security.MessageDigest.getInstance("SHA-256");
|
||||||
|
String value = tenantId + "|" + inboundId + "|" + loadingPlanId + "|"
|
||||||
|
+ preloadVersion + "|" + loadingPlanVersion;
|
||||||
|
return java.util.HexFormat.of().formatHex(digest.digest(
|
||||||
|
value.getBytes(java.nio.charset.StandardCharsets.UTF_8)));
|
||||||
|
} catch (java.security.NoSuchAlgorithmException exception) {
|
||||||
|
throw new AssertionError(exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user