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);