From f6e70ea5d0a179d2ce17e5b649c4f3883365461d Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 28 Sep 2026 09:49:53 +0800 Subject: [PATCH] feat(firstleg): expose customs receipt history --- .../customs/container/dto/ContainerCustomsDtos.java | 6 +++++- .../customs/container/mapper/ContainerCustomsMapper.java | 8 ++++++++ .../container/service/ContainerCustomsService.java | 8 +++++++- firstleg/src/main/resources/openapi/customs-v1.yaml | 3 ++- .../container/ContainerCustomsMapperContractTest.java | 1 + 5 files changed, 23 insertions(+), 3 deletions(-) diff --git a/firstleg/src/main/java/com/baiyue/firstleg/customs/container/dto/ContainerCustomsDtos.java b/firstleg/src/main/java/com/baiyue/firstleg/customs/container/dto/ContainerCustomsDtos.java index aeea14fe5..e99febf5e 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/customs/container/dto/ContainerCustomsDtos.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/customs/container/dto/ContainerCustomsDtos.java @@ -17,10 +17,14 @@ public final class ContainerCustomsDtos { public record FileView(long fileId, String fileName, String fileHash, String contentType, long sizeBytes, int version, LocalDateTime uploadedAt) {} + public record ReceiptVersionView(long version, FileView file, String operatorName, + LocalDateTime createdAt) {} + public record DocumentView(long documentId, long tenantId, FileView source, List inboundIds, List inboundCodes, boolean frozen, FileView receipt, String receiptStatus, - String receiptRejectReason, FileView finalDocument) {} + String receiptRejectReason, FileView finalDocument, + List receiptHistory) {} public record ContainerView(long aggregateId, long loadingPlanId, long containerBookingId, String planNo, String cabinetNo, String containerNo, String status, boolean hasActualCargo, 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 15eaf48d6..aafb2231c 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 @@ -340,6 +340,12 @@ public interface ContainerCustomsMapper { "WHERE r.source_document_id=#{documentId} FOR UPDATE") CompletionRow lockCompletion(@Param("documentId") long documentId); + @Select("SELECT v.document_version documentVersion,v.file_id fileId,v.file_name fileName,v.file_hash fileHash," + + "v.content_type contentType,v.size_bytes sizeBytes,v.operator_id operatorId,v.operator_name operatorName,v.created_at createdAt " + + "FROM fl_customs_completion_document_version v WHERE v.completion_document_id=#{completionId} " + + "ORDER BY v.document_version") + List completionVersions(@Param("completionId") long completionId); + @Insert(""" INSERT INTO fl_customs_completion_document (id,source_document_id,file_id,file_name,file_hash,content_type,size_bytes,uploaded_by,uploaded_by_name, @@ -553,6 +559,8 @@ public interface ContainerCustomsMapper { record CompletionRow(Long id,Long sourceDocumentId,Long fileId,String fileName,String fileHash,String contentType, Long sizeBytes,Long uploadedBy,String uploadedByName,LocalDateTime uploadedAt,Integer version, String customerConfirmStatus,String customerRejectReason) {} + record CompletionVersionRow(Integer documentVersion,Long fileId,String fileName,String fileHash,String contentType, + Long sizeBytes,Long operatorId,String operatorName,LocalDateTime createdAt) {} record FinalRow(Long id,Long sourceDocumentId,Long fileId,String fileName,String fileHash,String contentType, Long sizeBytes,Long uploadedBy,String uploadedByName,LocalDateTime uploadedAt,Integer version) {} record CommandRow(Long id,Long actorId,String action,String idempotencyKey,String requestHash,String status, diff --git a/firstleg/src/main/java/com/baiyue/firstleg/customs/container/service/ContainerCustomsService.java b/firstleg/src/main/java/com/baiyue/firstleg/customs/container/service/ContainerCustomsService.java index 8d7997b84..5d6b7eb12 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/customs/container/service/ContainerCustomsService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/customs/container/service/ContainerCustomsService.java @@ -581,11 +581,13 @@ public class ContainerCustomsService { FinalRow finalDocument = mapper.finalDocument(source.id()); ContainerRow container = mapper.containerById(source.customsContainerId()); FileView result = completion == null ? null : file(completion); + List receiptHistory = completion == null ? List.of() : Optional.ofNullable(mapper.completionVersions(completion.id())).orElseGet(List::of).stream() + .map(version -> new ReceiptVersionView(version.documentVersion(), file(version), version.operatorName(), version.createdAt())).toList(); return new DocumentView(source.id(), source.tenantId(), file(source), scopes.stream().map(ScopeRow::inboundId).toList(), scopes.stream().map(ScopeRow::inboundCode).toList(), source.frozenAt() != null, result, completion == null ? null : completion.customerConfirmStatus(), completion == null ? null : completion.customerRejectReason(), - finalDocument == null ? null : file(finalDocument)); + finalDocument == null ? null : file(finalDocument), receiptHistory); } private void completeContainer(ContainerRow container, long operatorId, String operatorName, String remark) { @@ -657,6 +659,10 @@ public class ContainerCustomsService { private static FileView file(DocumentRow row) { return new FileView(row.fileId(),row.fileName(),row.fileHash(),row.contentType(),row.sizeBytes(),row.version(),row.uploadedAt()); } private static FileView file(CompletionRow row) { return new FileView(row.fileId(),row.fileName(),row.fileHash(),row.contentType(),row.sizeBytes(),row.version(),row.uploadedAt()); } + private static FileView file(ContainerCustomsMapper.CompletionVersionRow row) { + return new FileView(row.fileId(), row.fileName(), row.fileHash(), row.contentType(), row.sizeBytes(), + row.documentVersion(), row.createdAt()); + } private static FileView file(FinalRow row) { return new FileView(row.fileId(),row.fileName(),row.fileHash(),row.contentType(),row.sizeBytes(),row.version(),row.uploadedAt()); } private static long size(CustomsFileClient.Metadata value) { return value.sizeBytes() == null ? 0 : value.sizeBytes(); } private static void validateFile(MultipartFile file) { if (file == null || file.isEmpty()) throw bad("请选择文件"); if (file.getSize() > MAX_FILE_BYTES) throw bad("单文件不能超过50MB"); } diff --git a/firstleg/src/main/resources/openapi/customs-v1.yaml b/firstleg/src/main/resources/openapi/customs-v1.yaml index a45f41509..0c0519f64 100644 --- a/firstleg/src/main/resources/openapi/customs-v1.yaml +++ b/firstleg/src/main/resources/openapi/customs-v1.yaml @@ -221,7 +221,8 @@ components: ScopeRequest: {type: object, required: [inboundIds, expectedVersion], additionalProperties: false, properties: {inboundIds: {type: array, minItems: 1, maxItems: 200, items: {$ref: '#/components/schemas/LongId'}}, expectedVersion: {type: integer, minimum: 1}}} FileView: {type: object, required: [fileId, fileName, fileHash, sizeBytes, version, uploadedAt], properties: {fileId: {$ref: '#/components/schemas/LongId'}, fileName: {type: string}, fileHash: {type: string}, contentType: {type: string, nullable: true}, sizeBytes: {type: integer, format: int64}, version: {type: integer}, uploadedAt: {type: string, format: date-time}}} InboundView: {type: object, required: [inboundId, orderId, orderNo, inboundCode, tenantId, customerName, taxMode, covered], properties: {inboundId: {$ref: '#/components/schemas/LongId'}, orderId: {$ref: '#/components/schemas/LongId'}, orderNo: {type: string}, inboundCode: {type: string}, tenantId: {$ref: '#/components/schemas/LongId'}, customerName: {type: string}, taxMode: {type: string, enum: [TAX_REFUND, BUY_DECLARATION]}, covered: {type: boolean}}} - DocumentView: {type: object, required: [documentId, tenantId, source, inboundIds, inboundCodes, frozen], properties: {documentId: {$ref: '#/components/schemas/LongId'}, tenantId: {$ref: '#/components/schemas/LongId'}, source: {$ref: '#/components/schemas/FileView'}, inboundIds: {type: array, items: {$ref: '#/components/schemas/LongId'}}, inboundCodes: {type: array, items: {type: string}}, frozen: {type: boolean}, receipt: {allOf: [{$ref: '#/components/schemas/FileView'}], nullable: true, description: 报关行回执文件}, receiptStatus: {type: string, enum: [PENDING, CONFIRMED, REJECTED], nullable: true}, receiptRejectReason: {type: string, nullable: true}, finalDocument: {allOf: [{$ref: '#/components/schemas/FileView'}], nullable: true, description: 真实报关完税文件}}} + ReceiptVersionView: {type: object, required: [version, file, createdAt], properties: {version: {type: integer}, file: {$ref: '#/components/schemas/FileView'}, operatorName: {type: string, nullable: true}, createdAt: {type: string, format: date-time}}} + DocumentView: {type: object, required: [documentId, tenantId, source, inboundIds, inboundCodes, frozen, receiptHistory], properties: {documentId: {$ref: '#/components/schemas/LongId'}, tenantId: {$ref: '#/components/schemas/LongId'}, source: {$ref: '#/components/schemas/FileView'}, inboundIds: {type: array, items: {$ref: '#/components/schemas/LongId'}}, inboundCodes: {type: array, items: {type: string}}, frozen: {type: boolean}, receipt: {allOf: [{$ref: '#/components/schemas/FileView'}], nullable: true, description: 当前报关行回执文件}, receiptStatus: {type: string, enum: [PENDING, CONFIRMED, REJECTED], nullable: true}, receiptRejectReason: {type: string, nullable: true}, finalDocument: {allOf: [{$ref: '#/components/schemas/FileView'}], nullable: true, description: 真实报关完税文件}, receiptHistory: {type: array, items: {$ref: '#/components/schemas/ReceiptVersionView'}, description: 报关行回执历次上传记录,按版本升序}}} ContainerView: {type: object, required: [aggregateId, loadingPlanId, containerBookingId, status, hasActualCargo, version, taxRefundCount, coveredTaxRefundCount, actualBoxCount, inbounds, documents], properties: {aggregateId: {$ref: '#/components/schemas/LongId'}, loadingPlanId: {$ref: '#/components/schemas/LongId'}, containerBookingId: {$ref: '#/components/schemas/LongId'}, planNo: {type: string}, cabinetNo: {type: string}, containerNo: {type: string, nullable: true}, status: {type: string, enum: [WAITING_CUSTOMER_DOCUMENTS, READY_TO_START, DECLARING, COMPLETED]}, hasActualCargo: {type: boolean, description: 当前完成装柜计划是否仍有实际装柜货物;false时查询成功返回空明细且无需发起报关}, merchantStatus: {type: string, enum: [WAITING_UPLOAD, DOCUMENT_UPLOADED, DECLARING, CUSTOMS_COMPLETED], nullable: true}, version: {type: integer}, taxRefundCount: {type: integer}, coveredTaxRefundCount: {type: integer}, actualBoxCount: {type: integer, format: int64, minimum: 0, description: 当前视角可见的实际装箱数;部分装柜只展示该数量,线下处理后续}, inbounds: {type: array, items: {$ref: '#/components/schemas/InboundView'}}, documents: {type: array, items: {$ref: '#/components/schemas/DocumentView'}}, startedAt: {type: string, format: date-time, nullable: true}, brokerSubmittedAt: {type: string, format: date-time, nullable: true, description: 已将客户确认后的回执发送给报关行执行的时间;不表示报关完成}, completedAt: {type: string, format: date-time, nullable: true}}} DeclarationBatch: {type: object, required: [id, customsContainerId, loadingPlanId, status, sourceVersion, version, results], properties: {id: {$ref: '#/components/schemas/LongId'}, customsContainerId: {$ref: '#/components/schemas/LongId'}, loadingPlanId: {$ref: '#/components/schemas/LongId'}, batchNo: {type: string}, status: {type: string, enum: [READY, SUBMITTED, ACKNOWLEDGED, RESULT_PENDING, CLOSED, CANCELLED]}, sourceVersion: {type: integer}, customsEntryNo: {type: string, nullable: true}, version: {type: integer}, submittedAt: {type: string, format: date-time, nullable: true}, submittedByName: {type: string, nullable: true}, acknowledgedAt: {type: string, format: date-time, nullable: true}, acknowledgedByName: {type: string, nullable: true}, remark: {type: string, nullable: true}, results: {type: array, items: {$ref: '#/components/schemas/ClearanceResult'}}}} ClearanceResult: {type: object, required: [id, declarationBatchId, customsContainerId, resultType, resultStatus, occurredAt, recordedAt], properties: {id: {$ref: '#/components/schemas/LongId'}, declarationBatchId: {$ref: '#/components/schemas/LongId'}, customsContainerId: {$ref: '#/components/schemas/LongId'}, resultType: {type: string, enum: [INSPECTION, SUPPLEMENT_REQUIRED, RELEASED, DETAINED, OTHER]}, resultStatus: {type: string, enum: [OPEN, RESOLVED, CANCELLED]}, customsEntryNo: {type: string, nullable: true}, occurredAt: {type: string, format: date-time}, resolution: {type: string, nullable: true}, evidenceFileId: {$ref: '#/components/schemas/LongId', nullable: true}, recordedBy: {$ref: '#/components/schemas/LongId'}, recordedByName: {type: string, nullable: true}, recordedAt: {type: string, format: date-time}, previousResultId: {$ref: '#/components/schemas/LongId', nullable: true}}} 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 ee9306812..8f9e16cea 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 @@ -45,6 +45,7 @@ class ContainerCustomsMapperContractTest { @Test void completionQueriesExplicitlyMapEveryRecordComponentIncludingVersion() throws Exception { assertRecordProjection("completion", ContainerCustomsMapper.CompletionRow.class, long.class); assertRecordProjection("lockCompletion", ContainerCustomsMapper.CompletionRow.class, long.class); + assertRecordProjectionWithoutVersion("completionVersions", ContainerCustomsMapper.CompletionVersionRow.class, long.class); } @Test void declarationAndClearanceQueriesMapExplicitHistoricalFacts() throws Exception {