From 81b729929cabb1b72976104d1114a35df407fa24 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 9 Sep 2026 11:21:36 +0800 Subject: [PATCH] fix(file): converge delivery documents to three categories --- file/AGENTS.md | 2 +- file/delivery-documents-handoff-20260909.md | 8 ++--- .../file/service/BusinessFileCategory.java | 8 ++--- .../resources/openapi/delivery-files-v1.yaml | 19 +++++----- .../service/DeliveryBusinessFileTest.java | 13 +++++-- .../service/DeliveryFileContractTest.java | 35 +++++++++++++++++++ 6 files changed, 62 insertions(+), 23 deletions(-) create mode 100644 file/src/test/java/com/baiyue/file/service/DeliveryFileContractTest.java diff --git a/file/AGENTS.md b/file/AGENTS.md index 0a2318652..a9deab837 100644 --- a/file/AGENTS.md +++ b/file/AGENTS.md @@ -22,7 +22,7 @@ | 订舱文件 | 通用内部业务文件入口,契约 `src/main/resources/openapi/ocean-booking-file-v1.yaml`;类型固定 `OCEAN_BOOKING_DOCUMENT`、`tenantId=0`(平台专属,客户可选快照不改变文件归属,拒绝客户作用域)、`businessId=oceanBookingId`,任意格式且最多 50MiB,不得绑定拖车;`BOOKING_CONFIRMATION/BILL_OF_LADING` 文档类型及有效状态由 Firstleg 保存 | | 拖车预约附件 | 通用内部业务文件入口;固定 `tenantId=0`、`businessType=TRUCK_APPOINTMENT_ATTACHMENT`、`businessId=appointmentId`,只允许派车单、司机/车辆资料和其他平台附件;提单文件归订舱 `OCEAN_BOOKING_DOCUMENT`,不得复制为拖车附件 | | 存储与实体 | `service/LocalQualificationStorage.java`、`MeasurementImageStorage.java`、`entity/FileObjectEntity.java` | -| 派送单七类资料 | `src/main/resources/openapi/delivery-files-v1.yaml`;七类 `DELIVERY_*` 对象均按客户、派送内部ID、类别隔离,任意格式单文件50MiB,元数据返回真实createdAt;Lastleg持有分类绑定、保存、替换删除和历史可见性,File保留不可变对象 | +| 派送单三类资料 | `src/main/resources/openapi/delivery-files-v1.yaml`;依次为装车照片 `DELIVERY_LOADING_PHOTO`、BOL文件(含签名)`DELIVERY_BOL`、签收文件(POD签收/快递签收)`DELIVERY_RECEIPT`,按客户、派送内部ID、类别隔离,任意格式单文件50MiB;Lastleg持有分类绑定、保存、替换删除和历史可见性,按非空类别计数x/3,不强制必传或识别签名;File保留不可变对象与真实createdAt | | 下游 OCR | `client/WeightOcrClient.java` | | SQL/配置 | `src/main/resources/db/file.sql`、`application.yml` | | 测试/验证 | `src/test/java/com/baiyue/file/`;`mvn -pl file -am test` | diff --git a/file/delivery-documents-handoff-20260909.md b/file/delivery-documents-handoff-20260909.md index a532c99a7..efbcfc9b3 100644 --- a/file/delivery-documents-handoff-20260909.md +++ b/file/delivery-documents-handoff-20260909.md @@ -6,12 +6,12 @@ | 能力 | 状态与证据 | |---|---| -| 七类上传 | 新增七类白名单,映射详见 `src/main/resources/openapi/delivery-files-v1.yaml`;任意格式,非空且每文件≤52428800字节 | +| 三类上传 | 按序 LOADING_PHOTO→DELIVERY_LOADING_PHOTO(装车照片)、BOL→DELIVERY_BOL(BOL文件(含签名))、RECEIPT→DELIVERY_RECEIPT(签收文件(POD签收/快递签收));任意格式,非空且每文件≤52428800字节 | | 所有权与元数据 | 复用 InternalBusinessFileService:tenantId=真实客户>0、businessId=deliveryOrderId内部Long十进制字符串、businessType和fileId精确过滤;同时只读ACTIVE且未删除对象 | | 上传时间 | Metadata增加真实持久化createdAt,LocalDateTime无偏移;已有其他通用业务文件OpenAPI同步字段 | | 下载 | 内部服务令牌必需,复合归属查询成功后读取真实对象;attachment、UTF-8文件名、no-store | | 替换与删除 | 新上传产生独立对象并保留旧内容;当前/历史引用、原子保存、版本和可见性归Lastleg。File未新增物理删除入口 | -| 类别计数 | Lastleg每类最多20文件,计数按已绑定非空类别x/7,不要求全部必传;File不复制绑定或状态 | +| 类别计数 | Lastleg每类0至20文件,totalCategoryCount=3,计数按已绑定非空类别x/3,不按文件数计数、不要求全部必传;无新增完成门禁、签名识别或电子签名;File不复制绑定或状态 | | 数据库 | 现有business_type VARCHAR(32)、business_id VARCHAR(64)、created_at足够,无结构变更或SQL执行 | ## 打印 BOL 现状 @@ -21,12 +21,12 @@ - `print/src/main/java/com/baiyue/print/controller/InternalPdfController.java` 提供内部 inbound-marks、freight-a4-documents 两个入口,校验内部令牌,返回真实PDF、页数、Content-Disposition和no-store。 - `print/src/main/java/com/baiyue/print/dto/FreightA4PdfDtos.java` 仅定义海运托运书和提单确认件A4模板;没有派送BOL专用模板。 - `print/src/test/java/com/baiyue/print/service/FreightA4PdfServiceGenerationTest.java` 实际生成字节并用PDFBox解析页数、尺寸与业务字段。 -- 定向检索 `lastleg-service/src/main/java/com/baiyue/lastleg/delivery/{controller,service}` 未发现调用Print的派送BOL生成链;当前BOL号和七类资料绑定不能视为生成打印能力。已上传BOL的鉴权下载也不等于生成派送BOL。 +- 本轮定向检索 `lastleg-service/src/main/java/com/baiyue/lastleg/delivery/{controller,service}` 未发现调用Print的派送BOL生成链;当前BOL号和三类资料绑定不能视为生成打印能力。已上传BOL的鉴权下载也不等于生成派送BOL。 - 派送专属打印权限、状态门禁、字段映射、模板是否复用和输出入口尚不能由现有File/Print证明,本次未新增打印方案。其余五个按钮由Admin/后端16核查。 ## 验证 -- `mvn -pl file -am test`:File 200项,0失败/错误/跳过;其中新增DeliveryBusinessFileTest 21项,覆盖七类各自不可变上传、真实时间、50MiB边界和完整查询隔离。 +- 2026-09-09 11:21(UTC+8),`mvn -pl file -am test`:BUILD SUCCESS,File 195项,0失败/错误/跳过。DeliveryBusinessFileTest 14项覆盖三类各自不可变上传、真实时间、50MiB边界和完整查询隔离,以及五个已移除类型的拒绝;DeliveryFileContractTest 1项锁定顺序、名称、映射及运行枚举一致性。仅本轮三类结果作为当前验收证据。 - `mvn -pl print -am -Dtest=FreightA4PdfServiceGenerationTest,InternalPdfControllerTest -Dsurefire.failIfNoSpecifiedTests=false test`:10项通过,含PDFBox真实解析。没有把它作为派送业务打印已实现的证据。 - AGENTS索引合同未通过:现有柜费分摊金额守恒、整组操作契约两项索引缺失;不属于本次File变更。 - Lastleg绑定/替换删除后的用户可见性需由后端16专项测试及后续获准环境验收确认。 diff --git a/file/src/main/java/com/baiyue/file/service/BusinessFileCategory.java b/file/src/main/java/com/baiyue/file/service/BusinessFileCategory.java index 8602f9184..73613938b 100644 --- a/file/src/main/java/com/baiyue/file/service/BusinessFileCategory.java +++ b/file/src/main/java/com/baiyue/file/service/BusinessFileCategory.java @@ -7,13 +7,9 @@ import java.util.Arrays; /** File-owned storage policy. Business visibility and lifecycle remain in the calling service. */ enum BusinessFileCategory { - DELIVERY_BOL(true, FormatPolicy.ARBITRARY, Limits.CUSTOMS, BusinessIdPolicy.POSITIVE_LONG), - DELIVERY_CUSTOMER_DOCUMENT(true, FormatPolicy.ARBITRARY, Limits.CUSTOMS, BusinessIdPolicy.POSITIVE_LONG), - DELIVERY_BILL_OF_LADING(true, FormatPolicy.ARBITRARY, Limits.CUSTOMS, BusinessIdPolicy.POSITIVE_LONG), - DELIVERY_POD(true, FormatPolicy.ARBITRARY, Limits.CUSTOMS, BusinessIdPolicy.POSITIVE_LONG), - DELIVERY_PHOTO(true, FormatPolicy.ARBITRARY, Limits.CUSTOMS, BusinessIdPolicy.POSITIVE_LONG), DELIVERY_LOADING_PHOTO(true, FormatPolicy.ARBITRARY, Limits.CUSTOMS, BusinessIdPolicy.POSITIVE_LONG), - DELIVERY_EXPRESS_RECEIPT(true, FormatPolicy.ARBITRARY, Limits.CUSTOMS, BusinessIdPolicy.POSITIVE_LONG), + DELIVERY_BOL(true, FormatPolicy.ARBITRARY, Limits.CUSTOMS, BusinessIdPolicy.POSITIVE_LONG), + DELIVERY_RECEIPT(true, FormatPolicy.ARBITRARY, Limits.CUSTOMS, BusinessIdPolicy.POSITIVE_LONG), TRUCK_APPOINTMENT_ATTACHMENT(false, FormatPolicy.STANDARD, Limits.STANDARD, BusinessIdPolicy.POSITIVE_LONG), OCEAN_BOOKING_DOCUMENT(false, FormatPolicy.ARBITRARY, Limits.CUSTOMS, BusinessIdPolicy.POSITIVE_LONG), CUSTOMS_DOCUMENT(false, FormatPolicy.STANDARD, Limits.STANDARD, BusinessIdPolicy.POSITIVE_LONG), diff --git a/file/src/main/resources/openapi/delivery-files-v1.yaml b/file/src/main/resources/openapi/delivery-files-v1.yaml index 68337c84f..a28ea6390 100644 --- a/file/src/main/resources/openapi/delivery-files-v1.yaml +++ b/file/src/main/resources/openapi/delivery-files-v1.yaml @@ -1,20 +1,21 @@ openapi: 3.0.3 info: - title: 派送单七类资料 File 内部契约 + title: 派送单三类资料 File 内部契约 version: 1.0.0 description: >- tenantId为派送单所属客户,businessId为deliveryOrderId内部Long的十进制字符串,不是派送单号或BOL号。 - 任意格式,非空单文件最多50MiB(52428800字节)。每类最多20文件由Lastleg控制,七类不强制必传。 + 任意格式,非空单文件最多50MiB(52428800字节)。每类0至20文件由Lastleg控制,三类不强制必传。 + totalCategoryCount固定3,计数为非空类别x/3,不是文件数;不增加完成门禁或签名识别。 上传只创建不可变对象,不自动绑定。Lastleg原子保存分类绑定与版本,替换/删除仅解绑并保留历史证据, 用户下载前由Lastleg检查当前引用或获准历史引用和权限,再调用File精确归属下载。不得按BOL号跨客户复用。 x-category-mapping: - BOL: DELIVERY_BOL - CUSTOMER_DOCUMENT: DELIVERY_CUSTOMER_DOCUMENT - BILL_OF_LADING: DELIVERY_BILL_OF_LADING - POD: DELIVERY_POD - DELIVERY_PHOTO: DELIVERY_PHOTO LOADING_PHOTO: DELIVERY_LOADING_PHOTO - EXPRESS_RECEIPT: DELIVERY_EXPRESS_RECEIPT + BOL: DELIVERY_BOL + RECEIPT: DELIVERY_RECEIPT + x-category-titles: + LOADING_PHOTO: 装车照片 + BOL: BOL文件(含签名) + RECEIPT: 签收文件(POD签收/快递签收) paths: /internal/file/v1/business-files/{tenantId}/{businessType}/{businessId}: parameters: @@ -75,7 +76,7 @@ components: required: true schema: type: string - enum: [DELIVERY_BOL, DELIVERY_CUSTOMER_DOCUMENT, DELIVERY_BILL_OF_LADING, DELIVERY_POD, DELIVERY_PHOTO, DELIVERY_LOADING_PHOTO, DELIVERY_EXPRESS_RECEIPT] + enum: [DELIVERY_LOADING_PHOTO, DELIVERY_BOL, DELIVERY_RECEIPT] responses: Metadata: description: 业务成功/失败使用HTTP200,成功code为字符串200;无效内部令牌拒绝 diff --git a/file/src/test/java/com/baiyue/file/service/DeliveryBusinessFileTest.java b/file/src/test/java/com/baiyue/file/service/DeliveryBusinessFileTest.java index c7b2f9d8b..0d562b5b1 100644 --- a/file/src/test/java/com/baiyue/file/service/DeliveryBusinessFileTest.java +++ b/file/src/test/java/com/baiyue/file/service/DeliveryBusinessFileTest.java @@ -28,7 +28,7 @@ class DeliveryBusinessFileTest { } @ParameterizedTest - @ValueSource(strings={"DELIVERY_BOL","DELIVERY_CUSTOMER_DOCUMENT","DELIVERY_BILL_OF_LADING","DELIVERY_POD","DELIVERY_PHOTO","DELIVERY_LOADING_PHOTO","DELIVERY_EXPRESS_RECEIPT"}) + @ValueSource(strings={"DELIVERY_LOADING_PHOTO","DELIVERY_BOL","DELIVERY_RECEIPT"}) void immutableUploadsPreserveOldContentAndRealTimestamp(String type) { var storage = storage(); var files = mock(FileObjectMapper.class); @@ -50,7 +50,7 @@ class DeliveryBusinessFileTest { } @ParameterizedTest - @ValueSource(strings={"DELIVERY_BOL","DELIVERY_CUSTOMER_DOCUMENT","DELIVERY_BILL_OF_LADING","DELIVERY_POD","DELIVERY_PHOTO","DELIVERY_LOADING_PHOTO","DELIVERY_EXPRESS_RECEIPT"}) + @ValueSource(strings={"DELIVERY_LOADING_PHOTO","DELIVERY_BOL","DELIVERY_RECEIPT"}) void enforcesFiftyMibAndNonemptyBoundary(String type) throws Exception { var storage = storage(); var category = BusinessFileCategory.require(type); assertThatThrownBy(() -> storage.store(11,"71",new MockMultipartFile("file",new byte[0]),category)).isInstanceOf(BizException.class); @@ -65,7 +65,7 @@ class DeliveryBusinessFileTest { } @ParameterizedTest - @ValueSource(strings={"DELIVERY_BOL","DELIVERY_CUSTOMER_DOCUMENT","DELIVERY_BILL_OF_LADING","DELIVERY_POD","DELIVERY_PHOTO","DELIVERY_LOADING_PHOTO","DELIVERY_EXPRESS_RECEIPT"}) + @ValueSource(strings={"DELIVERY_LOADING_PHOTO","DELIVERY_BOL","DELIVERY_RECEIPT"}) void downloadQueriesEveryOwnershipKeyAndRejectsUnauthorizedAccess(String type) { var files=mock(FileObjectMapper.class); var storage=mock(BusinessFileStorage.class); var service=service(files,storage); when(files.selectOne(any())).thenAnswer(call -> { @@ -82,6 +82,13 @@ class DeliveryBusinessFileTest { assertThatThrownBy(() -> controller.download("bad",11,type,"71",801)).isInstanceOf(BizException.class); verifyNoInteractions(storage); } + @ParameterizedTest + @ValueSource(strings={"DELIVERY_CUSTOMER_DOCUMENT","DELIVERY_BILL_OF_LADING","DELIVERY_POD","DELIVERY_PHOTO","DELIVERY_EXPRESS_RECEIPT"}) + void rejectsRemovedCategoriesWithoutAliases(String type) { + assertThatThrownBy(() -> BusinessFileCategory.require(type)) + .isInstanceOf(BizException.class).hasMessageContaining("不支持的附件业务类型"); + } + private BusinessFileStorage storage() { var storage=new BusinessFileStorage(); ReflectionTestUtils.setField(storage,"configuredRoot",root); storage.initializeRoot(); return storage; } diff --git a/file/src/test/java/com/baiyue/file/service/DeliveryFileContractTest.java b/file/src/test/java/com/baiyue/file/service/DeliveryFileContractTest.java new file mode 100644 index 000000000..4cc89e060 --- /dev/null +++ b/file/src/test/java/com/baiyue/file/service/DeliveryFileContractTest.java @@ -0,0 +1,35 @@ +package com.baiyue.file.service; + +import org.junit.jupiter.api.Test; +import org.yaml.snakeyaml.Yaml; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import static org.assertj.core.api.Assertions.assertThat; + +class DeliveryFileContractTest { + @Test + @SuppressWarnings("unchecked") + void freezesThreeOrderedCategoriesAndMatchesRuntimePolicy() throws Exception { + String text = Files.readString(Path.of("src/main/resources/openapi/delivery-files-v1.yaml")); + Map yaml = new Yaml().load(text); + Map info = (Map) yaml.get("info"); + Map mapping = (Map) info.get("x-category-mapping"); + assertThat(mapping.keySet()).containsExactly("LOADING_PHOTO", "BOL", "RECEIPT"); + List types = List.of("DELIVERY_LOADING_PHOTO", "DELIVERY_BOL", "DELIVERY_RECEIPT"); + assertThat(mapping.values()).containsExactlyElementsOf(types); + Map titles = (Map) info.get("x-category-titles"); + assertThat(titles.keySet()).containsExactlyElementsOf(mapping.keySet()); + assertThat(titles.values()).containsExactly("装车照片", "BOL文件(含签名)", "签收文件(POD签收/快递签收)"); + Map components = (Map) yaml.get("components"); + Map parameters = (Map) components.get("parameters"); + Map type = (Map) parameters.get("Type"); + Map schema = (Map) type.get("schema"); + assertThat((List) schema.get("enum")).containsExactlyElementsOf(types); + assertThat(Arrays.stream(BusinessFileCategory.values()).map(Enum::name) + .filter(name -> name.startsWith("DELIVERY_")).toList()).containsExactlyElementsOf(types); + assertThat(text).contains("totalCategoryCount固定3", "非空类别x/3", "每类0至20", "不强制必传", "不增加完成门禁或签名识别"); + } +}