fix(firstleg): cancel coload groups by inbound code

This commit is contained in:
2026-08-31 14:53:44 +08:00
parent 314ce09b5d
commit ea615b864a
7 changed files with 142 additions and 34 deletions
@@ -6,7 +6,7 @@ import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException; import com.baiyue.common.core.exception.BizException;
import com.baiyue.common.security.AuthenticatedUser; import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.BusinessSummary; import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.BusinessSummary;
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.CreateRequest;
import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.GroupSnapshot; import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.GroupSnapshot;
import com.baiyue.firstleg.loading.coload.service.ColoadGroupService; import com.baiyue.firstleg.loading.coload.service.ColoadGroupService;
@@ -15,7 +15,6 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Size; import jakarta.validation.constraints.Size;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.annotation.AuthenticationPrincipal;
@@ -52,19 +51,19 @@ public class PlatformColoadGroupController {
user.userId(), user.username())); user.userId(), user.username()));
} }
@PostMapping("/{groupId}/cancel") @PostMapping("/by-inbound-code/{inboundCode}/cancel")
@PreAuthorize("hasAuthority('platform:firstleg:smart-loading:assign')") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:assign')")
@BusinessAudit(event = "COLOAD_GROUP_CANCEL", resourceType = "COLOAD_GROUP", @BusinessAudit(event = "COLOAD_GROUP_CANCEL", resourceType = "COLOAD_GROUP",
resourceId = "#groupId", description = "整组取消同柜不可拆组") resourceId = "#inboundCode", description = "按入仓码整组取消同柜不可拆组")
@Operation(summary = "整组取消同柜不可拆组", @Operation(summary = "整组取消同柜不可拆组",
description = "仅 ACTIVE 组允许取消;不提供单成员编辑,修改成员必须取消后重建。") description = "仅 ACTIVE 组允许取消;不提供单成员编辑,修改成员必须取消后重建。")
public ApiResponse<GroupSnapshot> cancel( public ApiResponse<GroupSnapshot> cancel(
@AuthenticationPrincipal AuthenticatedUser user, @AuthenticationPrincipal AuthenticatedUser user,
@PathVariable @Min(1) long groupId, @PathVariable @Size(min = 1, max = 40) String inboundCode,
@RequestHeader("Idempotency-Key") String idempotencyKey, @RequestHeader("Idempotency-Key") String idempotencyKey,
@Valid @RequestBody CancelRequest request) { @Valid @RequestBody CancelByInboundCodeRequest request) {
requirePlatform(user); requirePlatform(user);
return ApiResponse.success(service.cancel(groupId, request.expectedVersion(), request.reason(), return ApiResponse.success(service.cancelByInboundCode(inboundCode, request.reason(),
idempotencyKey, user.userId(), user.username())); idempotencyKey, user.userId(), user.username()));
} }
@@ -18,8 +18,7 @@ public final class ColoadGroupModels {
) { ) {
} }
public record CancelRequest( public record CancelByInboundCodeRequest(
@NotNull @Min(0) Integer expectedVersion,
@Size(max = 500) String reason @Size(max = 500) String reason
) { ) {
} }
@@ -65,25 +65,30 @@ public class ColoadGroupService implements ColoadGroupAssignmentPort {
} }
@Transactional @Transactional
public GroupSnapshot cancel(long groupId, int expectedVersion, String reason, public GroupSnapshot cancelByInboundCode(String inboundCode, String reason,
String idempotencyKey, long operatorId, String operatorName) { String idempotencyKey, long operatorId,
if (groupId <= 0 || expectedVersion < 0) throw badRequest("组ID或版本不合法"); String operatorName) {
String code = normalizedInboundCode(inboundCode);
String key = key(idempotencyKey); String key = key(idempotencyKey);
String normalizedReason = trim(reason); String normalizedReason = trim(reason);
String requestHash = sha256("CANCEL|" + groupId + '|' + expectedVersion + '|' String requestHash = sha256("CANCEL_BY_INBOUND_CODE|" + code + '|'
+ Objects.toString(normalizedReason, "")); + Objects.toString(normalizedReason, ""));
CommandClaim claim = claim(operatorId, "CANCEL", key, requestHash); CommandClaim claim = claim(operatorId, "CANCEL", key, requestHash);
if (claim.replayGroupId() != null) return requiredSnapshot(claim.replayGroupId()); if (claim.replayGroupId() != null) return requiredSnapshot(claim.replayGroupId());
long groupId = requiredCurrentGroupId(code);
GroupRow group = requiredLocked(groupId); GroupRow group = requiredLocked(groupId);
if (!"ACTIVE".equals(group.getGroupStatus())) { if (!"ACTIVE".equals(group.getGroupStatus())) {
throw conflict("只有未锁定的同柜不可拆组可以整组取消"); throw conflict("只有未锁定的同柜不可拆组可以整组取消");
} }
if (!Objects.equals(group.getVersion(), expectedVersion) GroupSnapshot before = snapshot(group);
|| mapper.cancel(groupId, expectedVersion, operatorId, trim(operatorName)) != 1) { int expectedVersion = group.getVersion();
if (mapper.cancel(groupId, expectedVersion, operatorId, trim(operatorName)) != 1) {
throw conflict("组版本已变化,请刷新后重试"); throw conflict("组版本已变化,请刷新后重试");
} }
mapper.closeMembers(groupId); if (mapper.closeMembers(groupId) != before.members().size()) {
throw conflict("同柜不可拆组成员状态已变化,请刷新后重试");
}
history(groupId, "CANCEL", "ACTIVE", "CANCELLED", null, key, history(groupId, "CANCEL", "ACTIVE", "CANCELLED", null, key,
normalizedReason, operatorId, operatorName); normalizedReason, operatorId, operatorName);
complete(claim.commandId(), groupId); complete(claim.commandId(), groupId);
@@ -92,8 +97,7 @@ public class ColoadGroupService implements ColoadGroupAssignmentPort {
@Transactional(readOnly = true) @Transactional(readOnly = true)
public BusinessSummary queryByInboundCode(String inboundCode) { public BusinessSummary queryByInboundCode(String inboundCode) {
String code = trim(inboundCode); String code = normalizedInboundCode(inboundCode);
if (code == null || code.length() > 40) throw badRequest("inboundCode不能为空且不能超过40字符");
var groups = mapper.groupsByInboundCode(code).stream() var groups = mapper.groupsByInboundCode(code).stream()
.filter(row -> row.getGroupId() != null && CURRENT_GROUP_STATUSES.contains(row.getGroupStatus())) .filter(row -> row.getGroupId() != null && CURRENT_GROUP_STATUSES.contains(row.getGroupStatus()))
.toList(); .toList();
@@ -118,6 +122,24 @@ public class ColoadGroupService implements ColoadGroupAssignmentPort {
return new BusinessSummary(status, members.size(), members); return new BusinessSummary(status, members.size(), members);
} }
private long requiredCurrentGroupId(String inboundCode) {
var groups = mapper.groupsByInboundCode(inboundCode).stream()
.filter(row -> row.getGroupId() != null && CURRENT_GROUP_STATUSES.contains(row.getGroupStatus()))
.toList();
long distinctGroups = groups.stream().map(row -> row.getGroupId()).distinct().limit(2).count();
if (distinctGroups == 0) {
throw new BizException(ResultCode.NOT_FOUND, "入仓码未关联有效同柜不可拆组");
}
if (distinctGroups > 1) throw conflict("入仓码关联多个有效同柜不可拆组");
return groups.get(0).getGroupId();
}
private static String normalizedInboundCode(String inboundCode) {
String code = trim(inboundCode);
if (code == null || code.length() > 40) throw badRequest("inboundCode不能为空且不能超过40字符");
return code;
}
@Transactional(readOnly = true) @Transactional(readOnly = true)
public List<InternalOrderGroupInfo> queryInternalByOrderIds(List<Long> requestedOrderIds) { public List<InternalOrderGroupInfo> queryInternalByOrderIds(List<Long> requestedOrderIds) {
List<Long> orderIds = normalizedOrderIds(requestedOrderIds, 1, 200); List<Long> orderIds = normalizedOrderIds(requestedOrderIds, 1, 200);
@@ -331,8 +353,10 @@ public class ColoadGroupService implements ColoadGroupAssignmentPort {
private void history(long groupId, String action, String fromStatus, String toStatus, Long planId, private void history(long groupId, String action, String fromStatus, String toStatus, Long planId,
String operationId, String remark, long operatorId, String operatorName) { String operationId, String remark, long operatorId, String operatorName) {
mapper.insertHistory(IdWorker.getId(), groupId, action, fromStatus, toStatus, planId, if (mapper.insertHistory(IdWorker.getId(), groupId, action, fromStatus, toStatus, planId,
operationId, trim(remark), operatorId, trim(operatorName)); operationId, trim(remark), operatorId, trim(operatorName)) != 1) {
throw conflict("同柜不可拆组审计历史写入冲突");
}
} }
private static List<Long> normalizedOrderIds(List<Long> source, int minimum, int maximum) { private static List<Long> normalizedOrderIds(List<Long> source, int minimum, int maximum) {
@@ -20,18 +20,18 @@ paths:
responses: responses:
'200': {$ref: '#/components/responses/ColoadGroupResponse'} '200': {$ref: '#/components/responses/ColoadGroupResponse'}
'403': {description: 缺少智能排柜分配权限} '403': {description: 缺少智能排柜分配权限}
/api/platform/v1/first-leg/coload-groups/{groupId}/cancel: /api/platform/v1/first-leg/coload-groups/by-inbound-code/{inboundCode}/cancel:
post: post:
summary: 整组取消同柜不可拆组 summary: 整组取消同柜不可拆组
description: 仅 ACTIVE 组允许取消;不提供单成员编辑,修改成员必须取消后重建。 description: 使用任一成员入仓码整组取消;仅 ACTIVE 组允许取消,不提供单成员编辑。
parameters: parameters:
- $ref: '#/components/parameters/IdempotencyKey' - $ref: '#/components/parameters/IdempotencyKey'
- {name: groupId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}} - {name: inboundCode, in: path, required: true, schema: {type: string, minLength: 1, maxLength: 40}}
requestBody: requestBody:
required: true required: true
content: content:
application/json: application/json:
schema: {$ref: '#/components/schemas/CancelColoadGroupRequest'} schema: {$ref: '#/components/schemas/CancelColoadGroupByInboundCodeRequest'}
responses: responses:
'200': {$ref: '#/components/responses/ColoadGroupResponse'} '200': {$ref: '#/components/responses/ColoadGroupResponse'}
'403': {description: 缺少智能排柜分配权限} '403': {description: 缺少智能排柜分配权限}
@@ -98,12 +98,10 @@ components:
maxItems: 100 maxItems: 100
uniqueItems: true uniqueItems: true
items: {type: string, pattern: '^[1-9][0-9]*$'} items: {type: string, pattern: '^[1-9][0-9]*$'}
CancelColoadGroupRequest: CancelColoadGroupByInboundCodeRequest:
type: object type: object
additionalProperties: false additionalProperties: false
required: [expectedVersion]
properties: properties:
expectedVersion: {type: integer, minimum: 0}
reason: {type: string, maxLength: 500, nullable: true} reason: {type: string, maxLength: 500, nullable: true}
ColoadGroupMember: ColoadGroupMember:
type: object type: object
@@ -3,7 +3,7 @@ package com.baiyue.firstleg.loading.coload;
import com.baiyue.common.security.AuthenticatedUser; import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.firstleg.loading.coload.controller.PlatformColoadGroupController; import com.baiyue.firstleg.loading.coload.controller.PlatformColoadGroupController;
import com.baiyue.firstleg.loading.coload.controller.InternalColoadGroupController; 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.CreateRequest;
import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.InternalBatchQueryRequest; import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.InternalBatchQueryRequest;
import com.baiyue.firstleg.loading.coload.port.ColoadGroupAssignmentPort; 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"); assertThat(base.value()).containsExactly("/api/platform/v1/first-leg/coload-groups");
assertEndpoint("create", "", AuthenticatedUser.class, String.class, CreateRequest.class); assertEndpoint("create", "", AuthenticatedUser.class, String.class, CreateRequest.class);
assertEndpoint("cancel", "/{groupId}/cancel", AuthenticatedUser.class, long.class, assertEndpoint("cancel", "/by-inbound-code/{inboundCode}/cancel", AuthenticatedUser.class,
String.class, CancelRequest.class); String.class, String.class, CancelByInboundCodeRequest.class);
Method read = PlatformColoadGroupController.class.getDeclaredMethod( Method read = PlatformColoadGroupController.class.getDeclaredMethod(
"byInboundCode", AuthenticatedUser.class, String.class); "byInboundCode", AuthenticatedUser.class, String.class);
assertThat(read.getAnnotation(GetMapping.class).value()) assertThat(read.getAnnotation(GetMapping.class).value())
@@ -25,7 +25,7 @@ class ColoadGroupOpenApiContractTest {
assertThat(paths).containsKeys( assertThat(paths).containsKeys(
"/api/platform/v1/first-leg/coload-groups", "/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}"); "/api/platform/v1/first-leg/coload-groups/by-inbound-code/{inboundCode}");
assertThat(paths).doesNotContainKey("/api/platform/v1/first-leg/coload-groups/batch-query"); assertThat(paths).doesNotContainKey("/api/platform/v1/first-leg/coload-groups/batch-query");
assertThat(source) assertThat(source)
@@ -35,7 +35,6 @@ class ColoadGroupOpenApiContractTest {
.contains("pattern: '^[1-9][0-9]*$'") .contains("pattern: '^[1-9][0-9]*$'")
.contains("enum: [ACTIVE, LOCKED, CONSUMED, CANCELLED]") .contains("enum: [ACTIVE, LOCKED, CONSUMED, CANCELLED]")
.contains("Idempotency-Key") .contains("Idempotency-Key")
.contains("expectedVersion")
.contains("fullPackageCount") .contains("fullPackageCount")
.contains("platform:firstleg:smart-loading:assign") .contains("platform:firstleg:smart-loading:assign")
.contains("platform:firstleg:smart-loading:list") .contains("platform:firstleg:smart-loading:list")
@@ -45,6 +44,7 @@ class ColoadGroupOpenApiContractTest {
.doesNotContain("destinationType") .doesNotContain("destinationType")
.doesNotContain("destinationWarehouseId") .doesNotContain("destinationWarehouseId")
.doesNotContain("destinationCode") .doesNotContain("destinationCode")
.doesNotContain("expectedVersion")
.doesNotContain("platform:firstleg:coload"); .doesNotContain("platform:firstleg:coload");
assertThat(summaryProperties).containsOnlyKeys("status", "memberCount", "members"); assertThat(summaryProperties).containsOnlyKeys("status", "memberCount", "members");
assertThat(memberProperties).containsOnlyKeys("inboundCode"); assertThat(memberProperties).containsOnlyKeys("inboundCode");
@@ -3,6 +3,7 @@ package com.baiyue.firstleg.loading.coload;
import com.baiyue.common.core.exception.BizException; import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.loading.coload.persistence.ColoadGroupMapper; 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.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.InboundCandidateRow;
import com.baiyue.firstleg.loading.coload.persistence.ColoadPersistenceRows.MemberRow; import com.baiyue.firstleg.loading.coload.persistence.ColoadPersistenceRows.MemberRow;
import com.baiyue.firstleg.loading.coload.persistence.ColoadPersistenceRows.InboundGroupLookupRow; 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.anyLong;
import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@@ -192,11 +194,13 @@ class ColoadGroupServiceTest {
@Test @Test
void lockedGroupCannotBeCancelled() { 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())) when(mapper.insertCommand(anyLong(), eq(7L), eq("CANCEL"), eq("cancel-key"), anyString()))
.thenReturn(1); .thenReturn(1);
when(mapper.lockGroup(500L)).thenReturn(group(500L, "LOCKED", 700L, 2)); 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) .isInstanceOf(BizException.class)
.hasMessageContaining("未锁定"); .hasMessageContaining("未锁定");
verify(mapper, never()).cancel(anyLong(), anyInt(), anyLong(), any()); verify(mapper, never()).cancel(anyLong(), anyInt(), anyLong(), any());
@@ -204,6 +208,7 @@ class ColoadGroupServiceTest {
@Test @Test
void activeGroupCancelsAsAWholeAndReleasesEveryMembership() { 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())) when(mapper.insertCommand(anyLong(), eq(7L), eq("CANCEL"), eq("cancel-all"), anyString()))
.thenReturn(1); .thenReturn(1);
when(mapper.lockGroup(500L)).thenReturn(group(500L, "ACTIVE", null, 2)); when(mapper.lockGroup(500L)).thenReturn(group(500L, "ACTIVE", null, 2));
@@ -217,10 +222,73 @@ class ColoadGroupServiceTest {
when(mapper.members(500L)).thenReturn(List.of( when(mapper.members(500L)).thenReturn(List.of(
member(101L, 11L, 1001L, 30), member(202L, 12L, 1002L, 50))); 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"); assertThat(result.status()).isEqualTo("CANCELLED");
verify(mapper).closeMembers(500L); 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 @Test
@@ -237,6 +305,8 @@ class ColoadGroupServiceTest {
InboundCandidateRow second = candidate(202L, 12L, 1002L, 50); second.setActiveGroupId(500L); 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.lockCandidates(List.of(11L, 12L))).thenReturn(List.of(first, fragment, second));
when(mapper.lockToPlan(500L, 700L, 7L, "operator")).thenReturn(1); 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.completeCommand(anyLong(), eq(500L))).thenReturn(1);
when(mapper.group(500L)).thenReturn(group(500L, "LOCKED", 700L, 1)); when(mapper.group(500L)).thenReturn(group(500L, "LOCKED", 700L, 1));
@@ -318,6 +388,24 @@ class ColoadGroupServiceTest {
return value; 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) { private static org.assertj.core.groups.Tuple tuple(Object... values) {
return org.assertj.core.groups.Tuple.tuple(values); return org.assertj.core.groups.Tuple.tuple(values);
} }