This commit is contained in:
2026-08-10 11:28:57 +08:00
parent e33fb923e4
commit 047f267d4f
17 changed files with 290 additions and 67 deletions
@@ -15,11 +15,13 @@ class LoadingDeliveryOpenApiContractTest {
assertThat(yaml)
.contains("/api/platform/v1/inbounds/{inboundId}/loading/evidence:")
.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("enum: [PHOTO_STEP_1, PHOTO_STEP_2, PHOTO_STEP_3, PHOTO_STEP_4, SEAL]")
.contains("同类型可多次追加")
.contains("Required when evidenceType is SEAL")
.contains("compatibility projection")
.contains("must exactly match the server-derived container-task aggregate")
@@ -5,6 +5,7 @@ import com.baiyue.firstleg.loading.dto.LoadingRequests.CompleteDeliveryRequest;
import com.baiyue.firstleg.loading.dto.LoadingRequests.CompleteLoadingRequest;
import com.baiyue.firstleg.loading.dto.LoadingRequests.PickingRequest;
import com.baiyue.firstleg.loading.dto.LoadingRequests.UpsertEvidenceRequest;
import com.baiyue.firstleg.loading.dto.LoadingRequests.VoidEvidenceRequest;
import org.junit.jupiter.api.Test;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
@@ -27,6 +28,7 @@ class PlatformLoadingDeliveryControllerContractTest {
"platform:firstleg:inbound:loading:edit", UpsertEvidenceRequest.class);
get("evidence", "/{inboundId}/loading/evidence",
"platform:firstleg:inbound:loading:view");
postVoidEvidence();
post("completeLoading", "/{inboundId}/loading/complete",
"platform:firstleg:inbound:loading:complete", CompleteLoadingRequest.class);
post("completeDelivery", "/{inboundId}/delivery/complete",
@@ -39,6 +41,17 @@ class PlatformLoadingDeliveryControllerContractTest {
"platform:firstleg:inbound:picking:view");
}
private static void postVoidEvidence() throws Exception {
Method method = PlatformLoadingDeliveryController.class.getDeclaredMethod("voidEvidence",
AuthenticatedUser.class, long.class, long.class, long.class, String.class, VoidEvidenceRequest.class);
assertThat(method.getAnnotation(PostMapping.class).value())
.containsExactly("/{inboundId}/loading/evidence/{evidenceId}/void");
assertAuthority(method, "platform:firstleg:inbound:loading:edit");
assertThat(method.getParameters()[1].getAnnotation(RequestParam.class)).isNotNull();
assertThat(method.getParameters()[4].getAnnotation(RequestHeader.class).value())
.isEqualTo("Idempotency-Key");
}
private static void post(String name, String path, String authority, Class<?> requestType) throws Exception {
Method method = PlatformLoadingDeliveryController.class.getDeclaredMethod(name,
AuthenticatedUser.class, long.class, long.class, String.class, requestType);
@@ -9,14 +9,20 @@ import static org.assertj.core.api.Assertions.assertThat;
class LoadingEvidenceSqlContractTest {
@Test
void newSchemaRequiresRealFilesAndUniqueSealWithinCustomer() throws Exception {
void schemaSupportsAppendOnlyEvidenceAndVoidAudit() throws Exception {
String sql = new ClassPathResource("db/phase3-loading-delivery-schema.sql")
.getContentAsString(StandardCharsets.UTF_8);
assertThat(sql)
.contains("file_id BIGINT UNSIGNED NOT NULL")
.contains("UNIQUE KEY uk_loading_evidence_type (tenant_id, inbound_id, evidence_type)")
.contains("UNIQUE KEY uk_loading_evidence_seal (tenant_id, seal_no)")
.contains("KEY idx_loading_evidence_type (tenant_id, inbound_id, evidence_type, status)")
.contains("status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE'")
.contains("void_reason VARCHAR(500)")
.contains("voided_by BIGINT UNSIGNED")
.contains("voided_at DATETIME(3)")
.contains("DROP INDEX uk_loading_evidence_type")
.contains("DROP INDEX uk_loading_evidence_seal")
.contains("idx_loading_evidence_seal (tenant_id, seal_no, status)")
.contains("fl_inbound_loading_pick_task");
}
}
@@ -6,6 +6,7 @@ import com.baiyue.firstleg.entity.InboundStatusHistoryEntity;
import com.baiyue.firstleg.loading.dto.LoadingRequests.CompleteLoadingRequest;
import com.baiyue.firstleg.loading.dto.LoadingRequests.PickingRequest;
import com.baiyue.firstleg.loading.dto.LoadingRequests.UpsertEvidenceRequest;
import com.baiyue.firstleg.loading.dto.LoadingRequests.VoidEvidenceRequest;
import com.baiyue.firstleg.loading.dto.LoadingResponses.LoadingActionResponse;
import com.baiyue.firstleg.loading.entity.LoadingEvidenceEntity;
import com.baiyue.firstleg.loading.mapper.LoadingInboundMapper.Snapshot;
@@ -98,7 +99,6 @@ class LoadingDeliveryServiceTest {
void evidenceCanBeEditedAndDoesNotChangeMainStatus() {
when(repository.claimOperation(anyLong(), anyLong(), anyLong(), anyString(), anyString(), anyString())).thenReturn(true);
when(repository.findInbound(10, 20)).thenReturn(new Snapshot(20L, 10L, "LOADING", 3));
when(repository.findEvidence(10, 20, "PHOTO_STEP_1")).thenReturn(null);
when(repository.listEvidence(10, 20)).thenReturn(List.of());
when(files.validate("token", 10, 20, 1)).thenReturn(ApiResponse.success(
new LoadingEvidenceFileClient.FileMetadata(1L, "photo.jpg", "LOADING_PHOTO_STEP_1", "image/jpeg")));
@@ -109,6 +109,42 @@ class LoadingDeliveryServiceTest {
verify(repository, never()).completeLoading(anyLong(), anyLong(), anyLong(), anyInt());
}
@Test
void appendsMultiplePhotosOfTheSameType() {
when(repository.claimOperation(anyLong(), anyLong(), anyLong(), anyString(), anyString(), anyString())).thenReturn(true);
when(repository.findInbound(10, 20)).thenReturn(new Snapshot(20L, 10L, "LOADING", 3));
when(repository.listEvidence(10, 20)).thenReturn(List.of());
when(files.validate(eq("token"), eq(10L), eq(20L), anyLong())).thenAnswer(invocation -> {
long id = invocation.getArgument(3);
return ApiResponse.success(new LoadingEvidenceFileClient.FileMetadata(id, "photo-" + id + ".jpg",
"LOADING_PHOTO_STEP_1", "image/jpeg"));
});
service.upsertEvidence(10, 20, 7, "admin", "append-1",
new UpsertEvidenceRequest(0, "PHOTO_STEP_1", 11L, null, null, null));
service.upsertEvidence(10, 20, 7, "admin", "append-2",
new UpsertEvidenceRequest(0, "PHOTO_STEP_1", 12L, null, null, null));
verify(repository, times(2)).createEvidence(argThat(value ->
"PHOTO_STEP_1".equals(value.getEvidenceType()) && "ACTIVE".equals(value.getStatus())));
}
@Test
void voidsPhotoWithReasonAndKeepsTheRecord() {
when(repository.claimOperation(anyLong(), anyLong(), anyLong(), anyString(), anyString(), anyString())).thenReturn(true);
when(repository.findInbound(10, 20)).thenReturn(new Snapshot(20L, 10L, "LOADING", 3));
LoadingEvidenceEntity target = evidence("PHOTO_STEP_1", 11L, null);
target.setId(99L); target.setStatus("ACTIVE"); target.setVersion(2);
when(repository.listEvidence(10, 20)).thenReturn(List.of(target));
when(repository.voidEvidence(10, 20, 99L, "模糊", 7L, "admin", 2)).thenReturn(1);
LoadingActionResponse result = service.voidEvidence(10, 20, 99L, 7L, "admin", "void-1",
new VoidEvidenceRequest(2, "模糊"));
assertThat(result.mainStatus()).isEqualTo("LOADING");
verify(repository).voidEvidence(10, 20, 99L, "模糊", 7L, "admin", 2);
}
@Test
void rejectsDuplicateSealUsedByAnotherInbound() {
when(repository.claimOperation(anyLong(), anyLong(), anyLong(), anyString(), anyString(), anyString())).thenReturn(true);