fix(firstleg): map customs document versions explicitly

This commit is contained in:
2026-08-28 21:20:43 +08:00
parent 7a15481e2d
commit f72238e43f
3 changed files with 82 additions and 11 deletions
@@ -8,6 +8,18 @@ import java.util.List;
@Mapper @Mapper
public interface ContainerCustomsMapper { 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("""
SELECT DISTINCT p.id loadingPlanId,p.plan_no planNo,p.container_booking_id containerBookingId, 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, 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("fileId") long fileId,@Param("fileName") String fileName,@Param("fileHash") String fileHash,
@Param("contentType") String contentType,@Param("sizeBytes") long sizeBytes,@Param("operatorId") long operatorId); @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); 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); DocumentRow lockDocument(@Param("id") long id);
@Select(""" @Select("<script>SELECT " + DOCUMENT_COLUMNS + " FROM fl_customs_customer_document d " +
<script>SELECT * FROM fl_customs_customer_document "WHERE d.customs_container_id=#{containerId} AND d.status='ACTIVE' " +
WHERE customs_container_id=#{containerId} AND status='ACTIVE' "<if test='tenantId != null'>AND d.tenant_id=#{tenantId}</if> " +
<if test='tenantId != null'>AND tenant_id=#{tenantId}</if> "ORDER BY d.created_at,d.id</script>")
ORDER BY created_at,id</script>
""")
List<DocumentRow> documents(@Param("containerId") long containerId,@Param("tenantId") Long tenantId); List<DocumentRow> 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<DocumentRow> lockActiveDocuments(@Param("containerId") long containerId); List<DocumentRow> lockActiveDocuments(@Param("containerId") long containerId);
@Update(""" @Update("""
@@ -186,10 +197,12 @@ public interface ContainerCustomsMapper {
""") """)
List<ScopeRow> frozenScopes(@Param("documentId") long documentId); List<ScopeRow> 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); 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); CompletionRow lockCompletion(@Param("documentId") long documentId);
@Insert(""" @Insert("""
@@ -4,6 +4,10 @@ import com.baiyue.firstleg.customs.container.mapper.ContainerCustomsMapper;
import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Select;
import org.junit.jupiter.api.Test; 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; import static org.assertj.core.api.Assertions.assertThat;
class ContainerCustomsMapperContractTest { class ContainerCustomsMapperContractTest {
@@ -27,6 +31,31 @@ class ContainerCustomsMapperContractTest {
assertThat(value).contains("p.plan_status='COMPLETED'","i.tax_mode='BUY_DECLARATION'","item.deleted=0") assertThat(value).contains("p.plan_status='COMPLETED'","i.tax_mode='BUY_DECLARATION'","item.deleted=0")
.contains("item.id>#{cursor}","item.id<=#{maximumId}"); .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<String> aliases=Arrays.stream(projection.split(","))
.map(String::trim)
.map(column->column.substring(column.lastIndexOf(' ')+1))
.toList();
List<String> 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 { private static String sql(String name,Class<?>... types) throws Exception {
return String.join(" ",ContainerCustomsMapper.class.getMethod(name,types).getAnnotation(Select.class).value()); return String.join(" ",ContainerCustomsMapper.class.getMethod(name,types).getAnnotation(Select.class).value());
} }
@@ -13,6 +13,7 @@ import org.springframework.context.ApplicationEventPublisher;
import org.springframework.mock.web.MockMultipartFile; import org.springframework.mock.web.MockMultipartFile;
import java.util.List; 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.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -51,6 +52,34 @@ class ContainerCustomsServiceTest {
assertThat(view.inbounds()).extracting(value->value.orderId()).containsExactly(21L); 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<String>();
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(){ @Test void startFailsClosedWhenAnyRefundInboundHasNoActiveDocumentCoverage(){
var row=new ContainerCustomsMapper.InboundRow(9L,"LP9",8L,"COMPLETED","CAB","C",11L,21L,"O","I",7L,"客户","TAX_REFUND"); 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); var container=new ContainerCustomsMapper.ContainerRow(1L,9L,8L,ContainerCustomsService.READY,null,null,null,null,null,null,null,null,2);