fix(firstleg): cancel coload groups by inbound code
This commit is contained in:
+3
-3
@@ -3,7 +3,7 @@ package com.baiyue.firstleg.loading.coload;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.firstleg.loading.coload.controller.PlatformColoadGroupController;
|
||||
import com.baiyue.firstleg.loading.coload.controller.InternalColoadGroupController;
|
||||
import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.CancelRequest;
|
||||
import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.CancelByInboundCodeRequest;
|
||||
import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.CreateRequest;
|
||||
import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.InternalBatchQueryRequest;
|
||||
import com.baiyue.firstleg.loading.coload.port.ColoadGroupAssignmentPort;
|
||||
@@ -25,8 +25,8 @@ class ColoadGroupContractTest {
|
||||
assertThat(base.value()).containsExactly("/api/platform/v1/first-leg/coload-groups");
|
||||
|
||||
assertEndpoint("create", "", AuthenticatedUser.class, String.class, CreateRequest.class);
|
||||
assertEndpoint("cancel", "/{groupId}/cancel", AuthenticatedUser.class, long.class,
|
||||
String.class, CancelRequest.class);
|
||||
assertEndpoint("cancel", "/by-inbound-code/{inboundCode}/cancel", AuthenticatedUser.class,
|
||||
String.class, String.class, CancelByInboundCodeRequest.class);
|
||||
Method read = PlatformColoadGroupController.class.getDeclaredMethod(
|
||||
"byInboundCode", AuthenticatedUser.class, String.class);
|
||||
assertThat(read.getAnnotation(GetMapping.class).value())
|
||||
|
||||
+2
-2
@@ -25,7 +25,7 @@ class ColoadGroupOpenApiContractTest {
|
||||
|
||||
assertThat(paths).containsKeys(
|
||||
"/api/platform/v1/first-leg/coload-groups",
|
||||
"/api/platform/v1/first-leg/coload-groups/{groupId}/cancel",
|
||||
"/api/platform/v1/first-leg/coload-groups/by-inbound-code/{inboundCode}/cancel",
|
||||
"/api/platform/v1/first-leg/coload-groups/by-inbound-code/{inboundCode}");
|
||||
assertThat(paths).doesNotContainKey("/api/platform/v1/first-leg/coload-groups/batch-query");
|
||||
assertThat(source)
|
||||
@@ -35,7 +35,6 @@ class ColoadGroupOpenApiContractTest {
|
||||
.contains("pattern: '^[1-9][0-9]*$'")
|
||||
.contains("enum: [ACTIVE, LOCKED, CONSUMED, CANCELLED]")
|
||||
.contains("Idempotency-Key")
|
||||
.contains("expectedVersion")
|
||||
.contains("fullPackageCount")
|
||||
.contains("platform:firstleg:smart-loading:assign")
|
||||
.contains("platform:firstleg:smart-loading:list")
|
||||
@@ -45,6 +44,7 @@ class ColoadGroupOpenApiContractTest {
|
||||
.doesNotContain("destinationType")
|
||||
.doesNotContain("destinationWarehouseId")
|
||||
.doesNotContain("destinationCode")
|
||||
.doesNotContain("expectedVersion")
|
||||
.doesNotContain("platform:firstleg:coload");
|
||||
assertThat(summaryProperties).containsOnlyKeys("status", "memberCount", "members");
|
||||
assertThat(memberProperties).containsOnlyKeys("inboundCode");
|
||||
|
||||
+90
-2
@@ -3,6 +3,7 @@ package com.baiyue.firstleg.loading.coload;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.loading.coload.persistence.ColoadGroupMapper;
|
||||
import com.baiyue.firstleg.loading.coload.persistence.ColoadPersistenceRows.GroupRow;
|
||||
import com.baiyue.firstleg.loading.coload.persistence.ColoadPersistenceRows.CommandRow;
|
||||
import com.baiyue.firstleg.loading.coload.persistence.ColoadPersistenceRows.InboundCandidateRow;
|
||||
import com.baiyue.firstleg.loading.coload.persistence.ColoadPersistenceRows.MemberRow;
|
||||
import com.baiyue.firstleg.loading.coload.persistence.ColoadPersistenceRows.InboundGroupLookupRow;
|
||||
@@ -25,6 +26,7 @@ import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -192,11 +194,13 @@ class ColoadGroupServiceTest {
|
||||
|
||||
@Test
|
||||
void lockedGroupCannotBeCancelled() {
|
||||
when(mapper.groupsByInboundCode("IN-A")).thenReturn(List.of(lookup(500L, "LOCKED")));
|
||||
when(mapper.insertCommand(anyLong(), eq(7L), eq("CANCEL"), eq("cancel-key"), anyString()))
|
||||
.thenReturn(1);
|
||||
when(mapper.lockGroup(500L)).thenReturn(group(500L, "LOCKED", 700L, 2));
|
||||
|
||||
assertThatThrownBy(() -> service.cancel(500L, 2, "change", "cancel-key", 7L, "operator"))
|
||||
assertThatThrownBy(() -> service.cancelByInboundCode(
|
||||
"IN-A", "change", "cancel-key", 7L, "operator"))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("未锁定");
|
||||
verify(mapper, never()).cancel(anyLong(), anyInt(), anyLong(), any());
|
||||
@@ -204,6 +208,7 @@ class ColoadGroupServiceTest {
|
||||
|
||||
@Test
|
||||
void activeGroupCancelsAsAWholeAndReleasesEveryMembership() {
|
||||
when(mapper.groupsByInboundCode("IN-A")).thenReturn(List.of(lookup(500L, "ACTIVE")));
|
||||
when(mapper.insertCommand(anyLong(), eq(7L), eq("CANCEL"), eq("cancel-all"), anyString()))
|
||||
.thenReturn(1);
|
||||
when(mapper.lockGroup(500L)).thenReturn(group(500L, "ACTIVE", null, 2));
|
||||
@@ -217,10 +222,73 @@ class ColoadGroupServiceTest {
|
||||
when(mapper.members(500L)).thenReturn(List.of(
|
||||
member(101L, 11L, 1001L, 30), member(202L, 12L, 1002L, 50)));
|
||||
|
||||
var result = service.cancel(500L, 2, "rebuild", "cancel-all", 7L, "operator");
|
||||
var result = service.cancelByInboundCode(
|
||||
"IN-A", "rebuild", "cancel-all", 7L, "operator");
|
||||
|
||||
assertThat(result.status()).isEqualTo("CANCELLED");
|
||||
verify(mapper).closeMembers(500L);
|
||||
verify(mapper).insertHistory(anyLong(), eq(500L), eq("CANCEL"), eq("ACTIVE"),
|
||||
eq("CANCELLED"), isNull(), eq("cancel-all"), eq("rebuild"), eq(7L), eq("operator"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void cancelReplayReturnsOriginalResultWithoutResolvingOrMutatingCurrentGroup() {
|
||||
CommandRow command = command("SUCCESS", 500L);
|
||||
when(mapper.command(7L, "CANCEL", "cancel-replay")).thenReturn(command);
|
||||
when(mapper.group(500L)).thenReturn(group(500L, "CANCELLED", null, 3));
|
||||
when(mapper.members(500L)).thenReturn(List.of(
|
||||
member(101L, 11L, 1001L, 30), member(202L, 12L, 1002L, 50)));
|
||||
|
||||
var result = service.cancelByInboundCode(
|
||||
" IN-A ", "rebuild", "cancel-replay", 7L, "operator");
|
||||
|
||||
assertThat(result.status()).isEqualTo("CANCELLED");
|
||||
verify(mapper, never()).groupsByInboundCode(anyString());
|
||||
verify(mapper, never()).cancel(anyLong(), anyInt(), anyLong(), any());
|
||||
verify(mapper, never()).insertHistory(anyLong(), anyLong(), anyString(), any(), anyString(),
|
||||
any(), anyString(), any(), anyLong(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void cancelReplayRejectsDifferentPayloadAndConcurrentProcessing() {
|
||||
CommandRow differentPayload = command("SUCCESS", 500L);
|
||||
differentPayload.setRequestHash("different-hash");
|
||||
when(mapper.command(7L, "CANCEL", "same-key")).thenReturn(differentPayload);
|
||||
when(mapper.command(7L, "CANCEL", "processing-key"))
|
||||
.thenReturn(command("PROCESSING", null));
|
||||
|
||||
assertThatThrownBy(() -> service.cancelByInboundCode(
|
||||
"IN-A", "changed", "same-key", 7L, "operator"))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("不同请求");
|
||||
assertThatThrownBy(() -> service.cancelByInboundCode(
|
||||
"IN-A", "rebuild", "processing-key", 7L, "operator"))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("正在处理中");
|
||||
verify(mapper, never()).cancel(anyLong(), anyInt(), anyLong(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void concurrentStateChangeOrPartialMemberCloseRollsBackWithoutAuditCompletion() {
|
||||
when(mapper.groupsByInboundCode("IN-A")).thenReturn(List.of(lookup(500L, "ACTIVE")));
|
||||
when(mapper.insertCommand(anyLong(), eq(7L), eq("CANCEL"), anyString(), anyString()))
|
||||
.thenReturn(1);
|
||||
when(mapper.lockGroup(500L))
|
||||
.thenReturn(group(500L, "CANCELLED", null, 3), group(500L, "ACTIVE", null, 2));
|
||||
|
||||
assertThatThrownBy(() -> service.cancelByInboundCode(
|
||||
"IN-A", "rebuild", "lost-race", 7L, "operator"))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("未锁定");
|
||||
|
||||
when(mapper.members(500L)).thenReturn(List.of(
|
||||
member(101L, 11L, 1001L, 30), member(202L, 12L, 1002L, 50)));
|
||||
when(mapper.cancel(500L, 2, 7L, "operator")).thenReturn(1);
|
||||
when(mapper.closeMembers(500L)).thenReturn(1);
|
||||
assertThatThrownBy(() -> service.cancelByInboundCode(
|
||||
"IN-A", "rebuild", "partial-close", 7L, "operator"))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("成员状态已变化");
|
||||
|
||||
verify(mapper, never()).insertHistory(anyLong(), anyLong(), eq("CANCEL"), any(), anyString(),
|
||||
any(), anyString(), any(), anyLong(), any());
|
||||
verify(mapper, never()).completeCommand(anyLong(), anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -237,6 +305,8 @@ class ColoadGroupServiceTest {
|
||||
InboundCandidateRow second = candidate(202L, 12L, 1002L, 50); second.setActiveGroupId(500L);
|
||||
when(mapper.lockCandidates(List.of(11L, 12L))).thenReturn(List.of(first, fragment, second));
|
||||
when(mapper.lockToPlan(500L, 700L, 7L, "operator")).thenReturn(1);
|
||||
when(mapper.insertHistory(anyLong(), eq(500L), eq("LOCK"), eq("ACTIVE"), eq("LOCKED"),
|
||||
eq(700L), eq("assign-whole"), any(), eq(7L), eq("operator"))).thenReturn(1);
|
||||
when(mapper.completeCommand(anyLong(), eq(500L))).thenReturn(1);
|
||||
when(mapper.group(500L)).thenReturn(group(500L, "LOCKED", 700L, 1));
|
||||
|
||||
@@ -318,6 +388,24 @@ class ColoadGroupServiceTest {
|
||||
return value;
|
||||
}
|
||||
|
||||
private static CommandRow command(String status, Long resultGroupId) {
|
||||
CommandRow value = new CommandRow();
|
||||
value.setId(900L);
|
||||
value.setRequestHash(sha256("CANCEL_BY_INBOUND_CODE|IN-A|rebuild"));
|
||||
value.setCommandStatus(status);
|
||||
value.setResultGroupId(resultGroupId);
|
||||
return value;
|
||||
}
|
||||
|
||||
private static String sha256(String value) {
|
||||
try {
|
||||
return java.util.HexFormat.of().formatHex(java.security.MessageDigest.getInstance("SHA-256")
|
||||
.digest(value.getBytes(java.nio.charset.StandardCharsets.UTF_8)));
|
||||
} catch (java.security.NoSuchAlgorithmException exception) {
|
||||
throw new IllegalStateException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static org.assertj.core.groups.Tuple tuple(Object... values) {
|
||||
return org.assertj.core.groups.Tuple.tuple(values);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user