fix(firstleg): cancel coload groups by inbound code
This commit is contained in:
+6
-7
@@ -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()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-2
@@ -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
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-11
@@ -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
-3
@@ -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())
|
||||||
|
|||||||
+2
-2
@@ -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");
|
||||||
|
|||||||
+90
-2
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user