diff --git a/firstleg/src/main/java/com/baiyue/firstleg/customs/container/mapper/ContainerCustomsMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/customs/container/mapper/ContainerCustomsMapper.java index 3854af353..8778d08e3 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/customs/container/mapper/ContainerCustomsMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/customs/container/mapper/ContainerCustomsMapper.java @@ -8,6 +8,18 @@ import java.util.List; @Mapper public interface ContainerCustomsMapper { + String DOCUMENT_COLUMNS = """ + d.id id,d.customs_container_id customsContainerId,d.tenant_id tenantId,d.file_id fileId, + d.file_name fileName,d.file_hash fileHash,d.content_type contentType,d.size_bytes sizeBytes, + d.status status,d.uploaded_by uploadedBy,d.uploaded_at uploadedAt,d.frozen_version frozenVersion, + d.frozen_at frozenAt,d.version version + """; + String COMPLETION_COLUMNS = """ + r.id id,r.source_document_id sourceDocumentId,r.file_id fileId,r.file_name fileName, + r.file_hash fileHash,r.content_type contentType,r.size_bytes sizeBytes,r.uploaded_by uploadedBy, + r.uploaded_by_name uploadedByName,r.uploaded_at uploadedAt,r.version version + """; + @Select(""" SELECT DISTINCT p.id loadingPlanId,p.plan_no planNo,p.container_booking_id containerBookingId, p.plan_status loadingPlanStatus,b.cabinet_no cabinetNo,b.container_no containerNo, @@ -87,21 +99,20 @@ public interface ContainerCustomsMapper { @Param("fileId") long fileId,@Param("fileName") String fileName,@Param("fileHash") String fileHash, @Param("contentType") String contentType,@Param("sizeBytes") long sizeBytes,@Param("operatorId") long operatorId); - @Select("SELECT * FROM fl_customs_customer_document WHERE id=#{id}") + @Select("SELECT " + DOCUMENT_COLUMNS + " FROM fl_customs_customer_document d WHERE d.id=#{id}") DocumentRow document(@Param("id") long id); - @Select("SELECT * FROM fl_customs_customer_document WHERE id=#{id} FOR UPDATE") + @Select("SELECT " + DOCUMENT_COLUMNS + " FROM fl_customs_customer_document d WHERE d.id=#{id} FOR UPDATE") DocumentRow lockDocument(@Param("id") long id); - @Select(""" - - """) + @Select("") List documents(@Param("containerId") long containerId,@Param("tenantId") Long tenantId); - @Select("SELECT * FROM fl_customs_customer_document WHERE customs_container_id=#{containerId} AND status='ACTIVE' ORDER BY id FOR UPDATE") + @Select("SELECT " + DOCUMENT_COLUMNS + " FROM fl_customs_customer_document d " + + "WHERE d.customs_container_id=#{containerId} AND d.status='ACTIVE' ORDER BY d.id FOR UPDATE") List lockActiveDocuments(@Param("containerId") long containerId); @Update(""" @@ -186,10 +197,12 @@ public interface ContainerCustomsMapper { """) List frozenScopes(@Param("documentId") long documentId); - @Select("SELECT * FROM fl_customs_completion_document WHERE source_document_id=#{documentId}") + @Select("SELECT " + COMPLETION_COLUMNS + " FROM fl_customs_completion_document r " + + "WHERE r.source_document_id=#{documentId}") CompletionRow completion(@Param("documentId") long documentId); - @Select("SELECT * FROM fl_customs_completion_document WHERE source_document_id=#{documentId} FOR UPDATE") + @Select("SELECT " + COMPLETION_COLUMNS + " FROM fl_customs_completion_document r " + + "WHERE r.source_document_id=#{documentId} FOR UPDATE") CompletionRow lockCompletion(@Param("documentId") long documentId); @Insert(""" diff --git a/firstleg/src/test/java/com/baiyue/firstleg/customs/container/ContainerCustomsMapperContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/customs/container/ContainerCustomsMapperContractTest.java index 0c29d018b..bdf2b1967 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/customs/container/ContainerCustomsMapperContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/customs/container/ContainerCustomsMapperContractTest.java @@ -4,6 +4,10 @@ import com.baiyue.firstleg.customs.container.mapper.ContainerCustomsMapper; import org.apache.ibatis.annotations.Select; import org.junit.jupiter.api.Test; +import java.lang.reflect.RecordComponent; +import java.util.Arrays; +import java.util.List; + import static org.assertj.core.api.Assertions.assertThat; class ContainerCustomsMapperContractTest { @@ -27,6 +31,31 @@ class ContainerCustomsMapperContractTest { assertThat(value).contains("p.plan_status='COMPLETED'","i.tax_mode='BUY_DECLARATION'","item.deleted=0") .contains("item.id>#{cursor}","item.id<=#{maximumId}"); } + + @Test void documentQueriesExplicitlyMapEveryRecordComponentIncludingVersion() throws Exception { + assertRecordProjection("document", ContainerCustomsMapper.DocumentRow.class, long.class); + assertRecordProjection("lockDocument", ContainerCustomsMapper.DocumentRow.class, long.class); + assertRecordProjection("documents", ContainerCustomsMapper.DocumentRow.class, long.class, Long.class); + assertRecordProjection("lockActiveDocuments", ContainerCustomsMapper.DocumentRow.class, long.class); + } + + @Test void completionQueriesExplicitlyMapEveryRecordComponentIncludingVersion() throws Exception { + assertRecordProjection("completion", ContainerCustomsMapper.CompletionRow.class, long.class); + assertRecordProjection("lockCompletion", ContainerCustomsMapper.CompletionRow.class, long.class); + } + + private static void assertRecordProjection(String method,Class recordType,Class... parameterTypes) throws Exception { + String value=sql(method,parameterTypes).replaceAll("\\s+"," ").trim(); + assertThat(value).doesNotContainIgnoringCase("SELECT *"); + String projection=value.substring(value.indexOf("SELECT ")+7,value.indexOf(" FROM ")); + List aliases=Arrays.stream(projection.split(",")) + .map(String::trim) + .map(column->column.substring(column.lastIndexOf(' ')+1)) + .toList(); + List components=Arrays.stream(recordType.getRecordComponents()).map(RecordComponent::getName).toList(); + assertThat(aliases).containsExactlyElementsOf(components).contains("version"); + } + private static String sql(String name,Class... types) throws Exception { return String.join(" ",ContainerCustomsMapper.class.getMethod(name,types).getAnnotation(Select.class).value()); } diff --git a/firstleg/src/test/java/com/baiyue/firstleg/customs/container/ContainerCustomsServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/customs/container/ContainerCustomsServiceTest.java index 718d52b78..c9c3ca36f 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/customs/container/ContainerCustomsServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/customs/container/ContainerCustomsServiceTest.java @@ -13,6 +13,7 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.mock.web.MockMultipartFile; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -51,6 +52,34 @@ class ContainerCustomsServiceTest { assertThat(view.inbounds()).extracting(value->value.orderId()).containsExactly(21L); } + @Test void uploadedSourceViewReturnsPersistedNonNullVersion(){ + var inbound=new ContainerCustomsMapper.InboundRow(9L,"LP9",8L,"COMPLETED","CAB","C",11L,21L,"O","I",7L,"客户","TAX_REFUND"); + var aggregate=container(ContainerCustomsService.READY,2); + var persisted=new ContainerCustomsMapper.DocumentRow(31L,2L,7L,10L,"source.bin","a".repeat(64), + "application/octet-stream",1L,"ACTIVE",5L,java.time.LocalDateTime.now(),null,null,1); + var file=new MockMultipartFile("file","source.bin","application/octet-stream",new byte[]{1}); + var requestHash=new AtomicReference(); + when(mapper.planInbounds(9L)).thenReturn(List.of(inbound)); + when(mapper.container(9L)).thenReturn(aggregate); + when(mapper.lockContainer(9L)).thenReturn(aggregate); + when(mapper.missingRefundCount(9L,2L)).thenReturn(0); + when(mapper.claimCommand(anyLong(),eq(5L),eq("UPLOAD_SOURCE"),eq("k"),anyString())).thenAnswer(invocation->{ + requestHash.set(invocation.getArgument(4)); + return 0; + }); + when(mapper.command(5L,"UPLOAD_SOURCE","k")) + .thenAnswer(invocation->new ContainerCustomsMapper.CommandRow(41L,5L,"UPLOAD_SOURCE","k", + requestHash.get(),"SUCCESS",31L,null,null)); + when(mapper.document(anyLong())).thenReturn(persisted); + when(mapper.scopes(31L)).thenReturn(List.of(new ContainerCustomsMapper.ScopeRow(31L,11L,21L,"I"))); + when(mapper.containerById(2L)).thenReturn(aggregate); + + var view=service.uploadSource(7L,9L,5L,"k",List.of(11L),file); + + assertThat(view.source().version()).isEqualTo(1); + verifyNoInteractions(files); + } + @Test void startFailsClosedWhenAnyRefundInboundHasNoActiveDocumentCoverage(){ var row=new ContainerCustomsMapper.InboundRow(9L,"LP9",8L,"COMPLETED","CAB","C",11L,21L,"O","I",7L,"客户","TAX_REFUND"); var container=new ContainerCustomsMapper.ContainerRow(1L,9L,8L,ContainerCustomsService.READY,null,null,null,null,null,null,null,null,2);