优化
This commit is contained in:
+6
@@ -90,6 +90,12 @@ public class LoadingModelBridgeService {
|
||||
throw conflict("PDA开始前的预装关联不完整,禁止在LOADING后补关联");
|
||||
}
|
||||
for (AllocationScope scope : scopes) {
|
||||
// Internal push replay is pair-idempotent: an existing association has
|
||||
// already advanced both versions, so calling public bind again would
|
||||
// derive a different request hash for the same AUTO key.
|
||||
if (repository.byPair(scope.tenantId(), scope.inboundId(), loadingPlanId) != null) {
|
||||
continue;
|
||||
}
|
||||
var preload = repository.preload(scope.tenantId(), scope.inboundId());
|
||||
var currentPlan = repository.loadingPlan(loadingPlanId);
|
||||
if (preload == null || currentPlan == null) {
|
||||
|
||||
+11
@@ -18,6 +18,7 @@ import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/** 平台端装柜证据和国内装柜完成操作。 */
|
||||
@@ -58,6 +59,16 @@ public class PlatformLoadingDeliveryController {
|
||||
return ApiResponse.success(service.evidence(tenantId, inboundId));
|
||||
}
|
||||
|
||||
@Operation(summary = "PDA查看装柜证据照片")
|
||||
@GetMapping("/{inboundId}/loading/evidence/{evidenceId}/content/pda")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:pda:work')")
|
||||
public ResponseEntity<byte[]> pdaEvidenceContent(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam long tenantId, @PathVariable long inboundId,
|
||||
@PathVariable long evidenceId) {
|
||||
FirstlegScopeGuard.platform(user, tenantId);
|
||||
return service.evidenceContent(tenantId, inboundId, evidenceId);
|
||||
}
|
||||
|
||||
@Operation(summary = "PDA作废错误装柜证据")
|
||||
@PostMapping("/{inboundId}/loading/evidence/{evidenceId}/void/pda")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:pda:work')")
|
||||
|
||||
@@ -7,7 +7,7 @@ public final class LoadingResponses {
|
||||
private LoadingResponses() {}
|
||||
|
||||
public record EvidenceResponse(Long id, Long inboundId, Long tenantId, String evidenceType,
|
||||
Long fileId, String fileName, String sealNo, String remark,
|
||||
Long fileId, String fileName, String previewUrl, String sealNo, String remark,
|
||||
Integer version, String status, String voidReason,
|
||||
Long voidedBy, String voidedByName, LocalDateTime voidedAt,
|
||||
LocalDateTime createdAt, LocalDateTime updatedAt) {}
|
||||
|
||||
+23
-1
@@ -24,6 +24,7 @@ import feign.FeignException;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
@@ -149,6 +150,22 @@ public class LoadingDeliveryService {
|
||||
return evidenceResponse(tenantId, inboundId);
|
||||
}
|
||||
|
||||
public ResponseEntity<byte[]> evidenceContent(long tenantId, long inboundId, long evidenceId) {
|
||||
requireLoadingStage(tenantId, inboundId);
|
||||
LoadingEvidenceEntity evidence = repository.listEvidence(tenantId, inboundId).stream()
|
||||
.filter(item -> Long.valueOf(evidenceId).equals(item.getId()) && isActive(item))
|
||||
.findFirst().orElseThrow(() -> new BizException(ResultCode.NOT_FOUND, "装柜证据不存在"));
|
||||
try {
|
||||
ResponseEntity<byte[]> response = files.download(internalToken, tenantId, inboundId, evidence.getFileId());
|
||||
if (response == null || response.getBody() == null) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "装柜证据文件不存在");
|
||||
}
|
||||
return response;
|
||||
} catch (FeignException ex) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "文件服务暂不可用");
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public LoadingActionResponse voidEvidence(long tenantId, long inboundId, long evidenceId, long operatorId,
|
||||
String operatorName, String key, VoidEvidenceRequest request) {
|
||||
@@ -333,11 +350,16 @@ public class LoadingDeliveryService {
|
||||
private EvidenceListResponse evidenceResponse(long tenantId, long inboundId) {
|
||||
List<EvidenceResponse> values = repository.listEvidence(tenantId, inboundId).stream().map(e ->
|
||||
new EvidenceResponse(e.getId(), e.getInboundId(), e.getTenantId(), e.getEvidenceType(), e.getFileId(),
|
||||
e.getFileName(), e.getSealNo(), e.getRemark(), e.getVersion(), activeStatus(e), e.getVoidReason(),
|
||||
e.getFileName(), evidencePreviewUrl(tenantId, inboundId, e.getId()), e.getSealNo(), e.getRemark(), e.getVersion(), activeStatus(e), e.getVoidReason(),
|
||||
e.getVoidedBy(), e.getVoidedByName(), e.getVoidedAt(), e.getCreatedAt(), e.getUpdatedAt())).toList();
|
||||
return new EvidenceListResponse(inboundId, values);
|
||||
}
|
||||
|
||||
private static String evidencePreviewUrl(long tenantId, long inboundId, long evidenceId) {
|
||||
return "/api/platform/v1/inbounds/" + inboundId + "/loading/evidence/" + evidenceId
|
||||
+ "/content/pda?tenantId=" + tenantId;
|
||||
}
|
||||
|
||||
private static boolean isActive(LoadingEvidenceEntity evidence) {
|
||||
return evidence != null && (evidence.getStatus() == null || "ACTIVE".equals(evidence.getStatus()));
|
||||
}
|
||||
|
||||
@@ -233,20 +233,18 @@ public class SmartLoadingService {
|
||||
public Plan pushPreload(long planId, String idempotencyKey, PushRequest request, long operatorId, String operatorName) {
|
||||
String key = trim(idempotencyKey);
|
||||
if (!StringUtils.hasText(key) || key.length() > 128) throw new BizException(ResultCode.BAD_REQUEST, "必须提供有效 Idempotency-Key");
|
||||
String requestHash = sha256("PUSH_PRELOAD|" + planId + '|' + request.expectedVersion());
|
||||
LoadingPlanEntity plan = repository.plan(planId); if (plan == null) throw new BizException(ResultCode.NOT_FOUND, "装柜计划不存在");
|
||||
ContainerBookingEntity booking = requireBooking(plan.getContainerBookingId());
|
||||
if (!StringUtils.hasText(booking.getContainerNo())) {
|
||||
throw validation("录入真实柜号后才能推送预装清单并开启PDA作业");
|
||||
}
|
||||
if (key.equals(plan.getPushIdempotencyKey())) {
|
||||
if ("EDITING".equals(plan.getPlanStatus())) {
|
||||
ensurePendingPreloads(repository.items(planId), operatorId, operatorName);
|
||||
}
|
||||
loadingModelBridge.bindAllocatedBeforeLoading(plan.getId(), operatorId, operatorName, key);
|
||||
loadingPickService.beginIfReady(plan.getContainerBookingId(), operatorId, operatorName);
|
||||
requirePushReplay(planId, key, requestHash);
|
||||
return view(plan, booking);
|
||||
}
|
||||
if (StringUtils.hasText(plan.getPushIdempotencyKey())) throw conflict("预装清单已推送,不能使用新的幂等键重复推送");
|
||||
if (repository.operation(key) != null) throw conflict("Idempotency-Key已用于不同推送请求");
|
||||
if (!"EDITING".equals(plan.getPlanStatus()) || !Objects.equals(plan.getVersion(), request.expectedVersion())) throw conflict("仅编辑中的计划可开始装柜且版本必须匹配");
|
||||
List<LoadingPlanItemEntity> items = repository.items(planId);
|
||||
ensurePendingPreloads(items, operatorId, operatorName);
|
||||
@@ -254,13 +252,26 @@ public class SmartLoadingService {
|
||||
try {
|
||||
if (repository.push(planId, key, request.expectedVersion(), operatorId, operatorName) != 1) throw conflict("装柜计划已被并发修改");
|
||||
} catch (DuplicateKeyException e) { throw conflict("幂等键已用于其他推送"); }
|
||||
history(planId, null, "PUSH_PRELOAD", "EDITING", "EDITING", key, operatorId, operatorName);
|
||||
history(planId, null, "PUSH_PRELOAD", "EDITING", "EDITING", key,
|
||||
json(values("version", request.expectedVersion())),
|
||||
json(values("requestHash", requestHash)), operatorId, operatorName);
|
||||
LoadingPlanEntity pushed = repository.plan(planId);
|
||||
loadingModelBridge.bindAllocatedBeforeLoading(planId, operatorId, operatorName, key);
|
||||
loadingPickService.beginIfReady(pushed.getContainerBookingId(), operatorId, operatorName);
|
||||
return view(pushed, requireBooking(pushed.getContainerBookingId()));
|
||||
}
|
||||
|
||||
private void requirePushReplay(long planId, String key, String requestHash) {
|
||||
LoadingPlanHistoryEntity history = repository.operation(key);
|
||||
if (history == null
|
||||
|| !Objects.equals(history.getLoadingPlanId(), planId)
|
||||
|| !Objects.equals(history.getActionType(), "PUSH_PRELOAD")
|
||||
|| history.getAfterSnapshot() == null
|
||||
|| !sameRequestHash(history.getAfterSnapshot(), requestHash)) {
|
||||
throw conflict("Idempotency-Key已用于不同推送请求");
|
||||
}
|
||||
}
|
||||
|
||||
private void ensurePendingPreloads(List<LoadingPlanItemEntity> items, long operatorId, String operatorName) {
|
||||
if (items.isEmpty()) throw validation("装柜明细不能为空");
|
||||
if (items.stream().anyMatch(item -> item.getAccountId() == null || item.getInboundId() == null)) {
|
||||
|
||||
@@ -35,6 +35,13 @@ paths:
|
||||
application/json: {schema: {$ref: '#/components/schemas/UpsertEvidenceRequest'}}
|
||||
responses:
|
||||
'200': {description: Business envelope; ownership, type and version failures use business error codes}
|
||||
/api/platform/v1/inbounds/{inboundId}/loading/evidence/{evidenceId}/content/pda:
|
||||
get:
|
||||
summary: Download one loading evidence photo for the active PDA worker
|
||||
description: Uses the existing platform:firstleg:pda:work authority and validates customer, inbound and evidence ownership before proxying File service content.
|
||||
parameters: [{$ref: '#/components/parameters/InboundId'}, {$ref: '#/components/parameters/EvidenceId'}, {$ref: '#/components/parameters/TenantId'}]
|
||||
responses:
|
||||
'200': {description: Authenticated image bytes}
|
||||
/api/platform/v1/inbounds/{inboundId}/loading/evidence/{evidenceId}/void/pda:
|
||||
post:
|
||||
summary: Void an incorrect PDA loading evidence photo
|
||||
@@ -102,6 +109,19 @@ components:
|
||||
EvidenceId: {name: evidenceId, in: path, required: true, schema: {type: integer, format: int64, minimum: 1}}
|
||||
IdempotencyKey: {name: Idempotency-Key, in: header, required: true, schema: {type: string, maxLength: 128}}
|
||||
schemas:
|
||||
EvidenceResponse:
|
||||
type: object
|
||||
required: [id, inboundId, tenantId, evidenceType, fileId, previewUrl, version, status]
|
||||
properties:
|
||||
id: {type: integer, format: int64}
|
||||
inboundId: {type: integer, format: int64}
|
||||
tenantId: {type: integer, format: int64}
|
||||
evidenceType: {type: string, enum: [PHOTO_STEP_1, PHOTO_STEP_2, PHOTO_STEP_3, PHOTO_STEP_4, SEAL]}
|
||||
fileId: {type: integer, format: int64}
|
||||
fileName: {type: string}
|
||||
previewUrl: {type: string, description: Authenticated PDA image-content path; request with the current bearer token}
|
||||
version: {type: integer}
|
||||
status: {type: string}
|
||||
UpsertEvidenceRequest:
|
||||
type: object
|
||||
required: [expectedVersion, evidenceType, fileId]
|
||||
|
||||
+62
@@ -64,6 +64,54 @@ class LoadingModelBridgeServiceTest {
|
||||
verify(repository).touchLoadingPlan(40, 3, 7, "admin");
|
||||
}
|
||||
|
||||
@Test
|
||||
void editingReplayWithEveryPairExistingIsReadOnly() {
|
||||
var plan = new LoadingPlanSnapshot(40L, 50L, "C1", "EDITING", 9);
|
||||
when(repository.loadingPlan(40)).thenReturn(plan);
|
||||
when(repository.allocatedScopes(40)).thenReturn(List.of(
|
||||
new AllocationScope(10L, 20L, 8), new AllocationScope(11L, 21L, 4)));
|
||||
when(repository.byPair(10, 20, 40)).thenReturn(new PreloadLoadingPlanLinkEntity());
|
||||
when(repository.byPair(11, 21, 40)).thenReturn(new PreloadLoadingPlanLinkEntity());
|
||||
Link first = link("PENDING", 3, "EDITING", 9, 8);
|
||||
when(repository.byLoadingPlan(40)).thenReturn(List.of(first));
|
||||
|
||||
assertThat(service.bindAllocatedBeforeLoading(40, 7, "admin", "push-1"))
|
||||
.containsExactly(first);
|
||||
|
||||
verify(repository, never()).preload(anyLong(), anyLong());
|
||||
verify(repository, never()).insert(any());
|
||||
verify(repository, never()).touchPreload(anyLong(), anyLong(), anyLong(), anyInt(), anyLong(), anyString());
|
||||
verify(repository, never()).touchLoadingPlan(anyLong(), anyInt(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void editingReplaySkipsExistingPairAndCreatesOnlyMissingPair() {
|
||||
when(repository.loadingPlan(40)).thenReturn(new LoadingPlanSnapshot(40L, 50L, "C1", "EDITING", 3));
|
||||
when(repository.allocatedScopes(40)).thenReturn(List.of(
|
||||
new AllocationScope(10L, 20L, 8), new AllocationScope(11L, 21L, 4)));
|
||||
when(repository.byPair(10, 20, 40)).thenReturn(new PreloadLoadingPlanLinkEntity());
|
||||
when(repository.byPair(11, 21, 40)).thenReturn(null);
|
||||
when(repository.preload(11, 21)).thenReturn(new PreloadSnapshot(31L, 11L, 21L, "PENDING", 5));
|
||||
when(repository.loadingPlan(40)).thenReturn(
|
||||
new LoadingPlanSnapshot(40L, 50L, "C1", "EDITING", 3),
|
||||
new LoadingPlanSnapshot(40L, 50L, "C1", "EDITING", 3));
|
||||
when(repository.allocatedPackages(11, 21, 40)).thenReturn(4);
|
||||
when(repository.touchPreload(31, 11, 21, 5, 7, "admin")).thenReturn(1);
|
||||
when(repository.touchLoadingPlan(40, 3, 7, "admin")).thenReturn(1);
|
||||
Link linked = link("PENDING", 6, "EDITING", 4, 4);
|
||||
when(repository.byInbound(11, 21)).thenReturn(List.of(linked));
|
||||
when(repository.byLoadingPlan(40)).thenReturn(List.of(linked));
|
||||
|
||||
assertThat(service.bindAllocatedBeforeLoading(40, 7, "admin", "push-1"))
|
||||
.containsExactly(linked);
|
||||
|
||||
verify(repository, never()).preload(10, 20);
|
||||
verify(repository, times(2)).preload(11, 21);
|
||||
verify(repository, times(1)).insert(any());
|
||||
verify(repository, times(1)).touchPreload(31, 11, 21, 5, 7, "admin");
|
||||
verify(repository, times(1)).touchLoadingPlan(40, 3, 7, "admin");
|
||||
}
|
||||
|
||||
@Test
|
||||
void refusesPostHocBindingAfterLoadingStarts() {
|
||||
when(repository.loadingPlan(40)).thenReturn(new LoadingPlanSnapshot(40L, 50L, "C1", "LOADING", 4));
|
||||
@@ -104,6 +152,20 @@ class LoadingModelBridgeServiceTest {
|
||||
verify(repository, never()).byInbound(10, 20);
|
||||
}
|
||||
|
||||
@Test
|
||||
void publicBindSameKeyWithChangedVersionsStillConflicts() {
|
||||
var prior = new PreloadLoadingPlanLinkEntity();
|
||||
prior.setRequestHash("prior-request-hash");
|
||||
when(repository.byIdempotency(7, "manual-key")).thenReturn(prior);
|
||||
|
||||
assertThatThrownBy(() -> service.bind(10, 20, 40, 7, "admin", "manual-key",
|
||||
new BindRequest(3, 4)))
|
||||
.hasMessageContaining("Idempotency-Key已用于不同关联请求");
|
||||
|
||||
verify(repository, never()).byPair(anyLong(), anyLong(), anyLong());
|
||||
verify(repository, never()).insert(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void terminalGateRequiresEveryLinkedContainerPlanCompleted() {
|
||||
when(repository.byInbound(10, 20)).thenReturn(List.of(
|
||||
|
||||
+2
@@ -16,11 +16,13 @@ class LoadingDeliveryOpenApiContractTest {
|
||||
assertThat(yaml)
|
||||
.contains("/api/platform/v1/inbounds/{inboundId}/loading/evidence:")
|
||||
.contains("/api/platform/v1/inbounds/{inboundId}/loading/evidence/pda:")
|
||||
.contains("/api/platform/v1/inbounds/{inboundId}/loading/evidence/{evidenceId}/content/pda:")
|
||||
.contains("/api/platform/v1/inbounds/{inboundId}/loading/evidence/{evidenceId}/void:")
|
||||
.contains("/api/platform/v1/inbounds/{inboundId}/loading/picking/start:")
|
||||
.contains("/api/platform/v1/inbounds/{inboundId}/loading/picking/complete:")
|
||||
.contains("/api/platform/v1/inbounds/{inboundId}/loading/complete:")
|
||||
.contains("required: [expectedVersion, evidenceType, fileId]")
|
||||
.contains("previewUrl:")
|
||||
.contains("enum: [PHOTO_STEP_1, PHOTO_STEP_2, PHOTO_STEP_3, PHOTO_STEP_4, SEAL]")
|
||||
.contains("同类型可多次追加")
|
||||
.contains("Required when evidenceType is SEAL")
|
||||
|
||||
+5
@@ -30,6 +30,11 @@ class PlatformLoadingDeliveryControllerContractTest {
|
||||
"platform:firstleg:pda:work", UpsertEvidenceRequest.class);
|
||||
get("pdaEvidence", "/{inboundId}/loading/evidence/pda",
|
||||
"platform:firstleg:pda:work");
|
||||
Method pdaContent = PlatformLoadingDeliveryController.class.getDeclaredMethod("pdaEvidenceContent",
|
||||
AuthenticatedUser.class, long.class, long.class, long.class);
|
||||
assertThat(pdaContent.getAnnotation(GetMapping.class).value())
|
||||
.containsExactly("/{inboundId}/loading/evidence/{evidenceId}/content/pda");
|
||||
assertAuthority(pdaContent, "platform:firstleg:pda:work");
|
||||
Method pdaVoid = PlatformLoadingDeliveryController.class.getDeclaredMethod("pdaVoidEvidence",
|
||||
AuthenticatedUser.class, long.class, long.class, long.class, String.class, VoidEvidenceRequest.class);
|
||||
assertThat(pdaVoid.getAnnotation(PostMapping.class).value())
|
||||
|
||||
+20
@@ -24,6 +24,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
@@ -54,6 +55,24 @@ class LoadingDeliveryServiceTest {
|
||||
token.set(service, "token");
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnsAuthenticatedPreviewUrlAndProxiesOwnedEvidenceContent() {
|
||||
when(repository.findInbound(10, 20)).thenReturn(new Snapshot(20L, 10L, "LOADING", 3));
|
||||
LoadingEvidenceEntity target = evidence("PHOTO_STEP_1", 11L, null);
|
||||
target.setId(99L); target.setInboundId(20L); target.setTenantId(10L); target.setStatus("ACTIVE");
|
||||
when(repository.listEvidence(10, 20)).thenReturn(List.of(target));
|
||||
when(files.download("token", 10, 20, 11)).thenReturn(ResponseEntity.ok(new byte[]{1, 2, 3}));
|
||||
|
||||
var listed = service.evidence(10, 20);
|
||||
var content = service.evidenceContent(10, 20, 99);
|
||||
|
||||
assertThat(listed.evidences()).singleElement().satisfies(item ->
|
||||
assertThat(item.previewUrl()).isEqualTo(
|
||||
"/api/platform/v1/inbounds/20/loading/evidence/99/content/pda?tenantId=10"));
|
||||
assertThat(content.getBody()).containsExactly(1, 2, 3);
|
||||
verify(files).download("token", 10, 20, 11);
|
||||
}
|
||||
|
||||
@Test
|
||||
void completesLoadingOnlyAfterPickingAndFiveEvidenceFiles() {
|
||||
when(repository.claimOperation(anyLong(), anyLong(), anyLong(), anyString(), anyString(), anyString())).thenReturn(true);
|
||||
@@ -297,6 +316,7 @@ class LoadingDeliveryServiceTest {
|
||||
|
||||
private static LoadingEvidenceEntity evidence(String type, Long fileId, String sealNo) {
|
||||
LoadingEvidenceEntity value = new LoadingEvidenceEntity();
|
||||
value.setId(fileId); value.setInboundId(20L); value.setTenantId(10L);
|
||||
value.setEvidenceType(type); value.setFileId(fileId); value.setSealNo(sealNo);
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -391,6 +391,11 @@ class SmartLoadingServiceTest {
|
||||
|
||||
assertThat(result.status()).isEqualTo("EDITING");
|
||||
verify(repository).push(1, "key", 3, 7, "u");
|
||||
ArgumentCaptor<LoadingPlanHistoryEntity> history = ArgumentCaptor.forClass(LoadingPlanHistoryEntity.class);
|
||||
verify(repository).insert(history.capture());
|
||||
assertThat(history.getValue().getActionType()).isEqualTo("PUSH_PRELOAD");
|
||||
assertThat(history.getValue().getAfterSnapshot())
|
||||
.contains(sha256("PUSH_PRELOAD|1|3"));
|
||||
var ordered = inOrder(splitPreloadService, loadingModelBridge, loadingPickService);
|
||||
ordered.verify(splitPreloadService).ensurePendingForLoadingAllocation(9L, 10L, 7L, "u");
|
||||
ordered.verify(loadingModelBridge).bindAllocatedBeforeLoading(1, 7, "u", "key");
|
||||
@@ -398,19 +403,77 @@ class SmartLoadingServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void editingPushReplayEnsuresPendingPreloadBeforePda() {
|
||||
void exactPushReplayIsReadOnly() {
|
||||
LoadingPlanEntity pushed = plan("EDITING", 4);
|
||||
pushed.setPushIdempotencyKey("key");
|
||||
when(repository.plan(1)).thenReturn(pushed);
|
||||
when(repository.items(1)).thenReturn(List.of(item(8, 9, 20, 1)));
|
||||
when(repository.booking(99)).thenReturn(booking());
|
||||
when(repository.operation("key"))
|
||||
.thenReturn(history(1, null, "PUSH_PRELOAD", sha256("PUSH_PRELOAD|1|3")));
|
||||
|
||||
Plan replay = service.pushPreload(1, "key", new PushRequest(3), 7, "u");
|
||||
|
||||
assertThat(replay.version()).isEqualTo(4);
|
||||
verify(splitPreloadService, never()).ensurePendingForLoadingAllocation(anyLong(), anyLong(), anyLong(), anyString());
|
||||
verify(loadingModelBridge, never()).bindAllocatedBeforeLoading(anyLong(), anyLong(), anyString(), anyString());
|
||||
verify(loadingPickService, never()).beginIfReady(anyLong(), anyLong(), anyString());
|
||||
verify(repository, never()).insert(any(LoadingPlanHistoryEntity.class));
|
||||
verify(repository, never()).push(anyLong(), anyString(), anyInt(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void samePushKeyWithDifferentRequestIsRejectedWithoutWrites() {
|
||||
LoadingPlanEntity pushed = plan("EDITING", 4);
|
||||
pushed.setPushIdempotencyKey("key");
|
||||
when(repository.plan(1)).thenReturn(pushed);
|
||||
when(repository.booking(99)).thenReturn(booking());
|
||||
when(repository.operation("key"))
|
||||
.thenReturn(history(1, null, "PUSH_PRELOAD", sha256("PUSH_PRELOAD|1|3")));
|
||||
|
||||
BizException error = catchThrowableOfType(
|
||||
() -> service.pushPreload(1, "key", new PushRequest(4), 7, "u"), BizException.class);
|
||||
|
||||
assertThat(error.getResultCode()).isEqualTo(ResultCode.CONFLICT);
|
||||
assertThat(error.getMessage()).contains("不同推送请求");
|
||||
verify(repository, never()).items(anyLong());
|
||||
verify(repository, never()).push(anyLong(), anyString(), anyInt(), anyLong(), anyString());
|
||||
verify(repository, never()).insert(any(LoadingPlanHistoryEntity.class));
|
||||
verifyNoPushSideEffects();
|
||||
}
|
||||
|
||||
@Test
|
||||
void newPushKeyAfterSuccessfulPushIsRejectedWithoutWrites() {
|
||||
LoadingPlanEntity pushed = plan("EDITING", 4);
|
||||
pushed.setPushIdempotencyKey("first-key");
|
||||
when(repository.plan(1)).thenReturn(pushed);
|
||||
when(repository.booking(99)).thenReturn(booking());
|
||||
|
||||
service.pushPreload(1, "key", new PushRequest(4), 7, "u");
|
||||
BizException error = catchThrowableOfType(
|
||||
() -> service.pushPreload(1, "new-key", new PushRequest(3), 7, "u"), BizException.class);
|
||||
|
||||
verify(splitPreloadService).ensurePendingForLoadingAllocation(9L, 10L, 7L, "u");
|
||||
verify(loadingModelBridge).bindAllocatedBeforeLoading(1, 7, "u", "key");
|
||||
verify(loadingPickService).beginIfReady(99, 7, "u");
|
||||
assertThat(error.getResultCode()).isEqualTo(ResultCode.CONFLICT);
|
||||
assertThat(error.getMessage()).contains("新的幂等键");
|
||||
verify(repository, never()).push(anyLong(), anyString(), anyInt(), anyLong(), anyString());
|
||||
verifyNoPushSideEffects();
|
||||
}
|
||||
|
||||
@Test
|
||||
void pushKeyUsedByAnotherPlanOperationIsRejectedBeforeWrites() {
|
||||
LoadingPlanEntity editing = plan("EDITING", 3);
|
||||
when(repository.plan(1)).thenReturn(editing);
|
||||
when(repository.booking(99)).thenReturn(booking());
|
||||
when(repository.operation("used-key"))
|
||||
.thenReturn(history(1, 8L, "REMOVE_ITEM", sha256("DELETE|1|8|3|0")));
|
||||
|
||||
BizException error = catchThrowableOfType(
|
||||
() -> service.pushPreload(1, "used-key", new PushRequest(3), 7, "u"), BizException.class);
|
||||
|
||||
assertThat(error.getResultCode()).isEqualTo(ResultCode.CONFLICT);
|
||||
assertThat(error.getMessage()).contains("不同推送请求");
|
||||
verify(repository, never()).items(anyLong());
|
||||
verify(repository, never()).push(anyLong(), anyString(), anyInt(), anyLong(), anyString());
|
||||
verify(repository, never()).insert(any(LoadingPlanHistoryEntity.class));
|
||||
verifyNoPushSideEffects();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -490,6 +553,12 @@ class SmartLoadingServiceTest {
|
||||
verify(loadingPickService, never()).beginIfReady(anyLong(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
private void verifyNoPushSideEffects() {
|
||||
verify(splitPreloadService, never()).ensurePendingForLoadingAllocation(anyLong(), anyLong(), anyLong(), anyString());
|
||||
verify(loadingModelBridge, never()).bindAllocatedBeforeLoading(anyLong(), anyLong(), anyString(), anyString());
|
||||
verify(loadingPickService, never()).beginIfReady(anyLong(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
private void stubAssignment() {
|
||||
LoadingPlanEntity plan = plan("EDITING", 0);
|
||||
when(repository.plan(1)).thenReturn(plan, plan("EDITING", 1));
|
||||
|
||||
Reference in New Issue
Block a user