diff --git a/file/AGENTS.md b/file/AGENTS.md index 247f16d6a..b6dabc818 100644 --- a/file/AGENTS.md +++ b/file/AGENTS.md @@ -25,7 +25,7 @@ ## 修改约束 - 上传先写真实文件再登记 IAM 元数据;异常结果未知时保留文件供重试,禁止伪造 URL。 -- 客户业务文件按客户、业务 ID 和文件类型校验归属;重量图片可 OCR,第三方尺寸证据不得触发重量 OCR。装柜四步照片与铅封是跨客户物理柜证据,所有者固定为平台(`tenantId=0`),业务 ID 固定为 `containerBookingId`,File 按柜与五类 `LOADING_*` 精确校验且不得跨柜复用;PDA `taskId` 只由 Firstleg 保存为来源/审计,不进入 File 所有权。 +- 客户业务文件按客户、业务 ID 和文件类型校验归属;重量图片可 OCR,第三方尺寸证据不得触发重量 OCR。装柜四步照片与铅封是跨客户物理柜证据,所有者固定为平台(`tenantId=0`),业务 ID 固定为 `containerBookingId`,File 按柜与五类 `LOADING_*` 精确校验且不得跨柜复用;PDA `taskId` 只由 Firstleg 保存为来源/审计,不进入 File 所有权。国内 PDA 禁止直连 File 平台接口,必须经 Firstleg 校验任务、仓库和操作者后,由 Firstleg 使用内部服务令牌访问 File。 - 商品图片平台读取必须同时校验平台身份、`platform:order:list`、客户范围、文件业务类型与正整数 ID;不得提供匿名 URL、跨客户读取或把内部存储路径返回页面。 - 公共导出只接受 `ExportDefinitionRegistry` 明确登记的 `exportType`,由 File 调用所属业务服务的内部 Provider 分批取数并生成文件;业务页面不得直连 Provider。新增或修改导出类型时同步检查业务服务 Provider、File Feign 客户端/响应上限、权限、Admin 下载中心标签与重试映射及契约测试。 - 生产环境显式配置 `FILE_STORAGE_ROOT`;磁盘故障返回 `S0503`,不得包装成业务错误。 diff --git a/file/src/main/java/com/baiyue/file/controller/PlatformLoadingEvidenceController.java b/file/src/main/java/com/baiyue/file/controller/PlatformLoadingEvidenceController.java index f6483b82a..2bcffa826 100644 --- a/file/src/main/java/com/baiyue/file/controller/PlatformLoadingEvidenceController.java +++ b/file/src/main/java/com/baiyue/file/controller/PlatformLoadingEvidenceController.java @@ -21,7 +21,7 @@ public class PlatformLoadingEvidenceController { @Resource private LoadingEvidenceService service; @PostMapping(value = "/{containerBookingId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - @PreAuthorize("hasAnyAuthority('platform:firstleg:inbound:loading:edit','platform:firstleg:pda:work')") + @PreAuthorize("hasAuthority('platform:firstleg:inbound:loading:edit')") public ApiResponse upload( @AuthenticationPrincipal AuthenticatedUser user, @PathVariable long containerBookingId, @RequestParam String evidenceType, @RequestPart("file") MultipartFile file) { @@ -30,7 +30,7 @@ public class PlatformLoadingEvidenceController { } @GetMapping("/{containerBookingId}/{fileId}") - @PreAuthorize("hasAnyAuthority('platform:firstleg:inbound:loading:view','platform:firstleg:pda:work')") + @PreAuthorize("hasAuthority('platform:firstleg:inbound:loading:view')") public ResponseEntity download(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable long containerBookingId, @PathVariable long fileId) { requirePlatform(user); requirePositive(containerBookingId, fileId); diff --git a/file/src/main/resources/openapi/loading-evidence-v1.yaml b/file/src/main/resources/openapi/loading-evidence-v1.yaml index 235088853..f6f73f5ff 100644 --- a/file/src/main/resources/openapi/loading-evidence-v1.yaml +++ b/file/src/main/resources/openapi/loading-evidence-v1.yaml @@ -13,11 +13,11 @@ paths: /api/platform/v1/files/loading-evidence/{containerBookingId}: post: tags: [LoadingEvidence] - summary: 平台或PDA上传柜级装柜证据 - description: 不接受客户ID或入仓单ID;同一文件不得跨containerBookingId复用。 - x-required-any-permission: - - platform:firstleg:inbound:loading:edit - - platform:firstleg:pda:work + summary: Admin上传柜级装柜证据 + description: >- + 仅Admin装柜证据维护权限可直连;国内PDA禁止直连File,必须先调用Firstleg任务证据接口。 + 不接受客户ID或入仓单ID;同一文件不得跨containerBookingId复用。 + x-required-permission: platform:firstleg:inbound:loading:edit parameters: - $ref: '#/components/parameters/ContainerBookingId' - $ref: '#/components/parameters/EvidenceType' @@ -39,11 +39,11 @@ paths: /api/platform/v1/files/loading-evidence/{containerBookingId}/{fileId}: get: tags: [LoadingEvidence] - summary: 平台或PDA下载柜级装柜证据 - description: 必须同时命中平台所有权、containerBookingId、fileId、LOADING_*类型和ACTIVE状态;响应禁止缓存。 - x-required-any-permission: - - platform:firstleg:inbound:loading:view - - platform:firstleg:pda:work + summary: Admin下载柜级装柜证据 + description: >- + 仅Admin装柜证据读取权限可直连;国内PDA必须经Firstleg任务、仓库和操作者校验后,由Firstleg使用内部服务令牌下载。 + 文件必须同时命中平台所有权、containerBookingId、fileId、五类LOADING业务类型和ACTIVE状态;响应禁止缓存。 + x-required-permission: platform:firstleg:inbound:loading:view parameters: - $ref: '#/components/parameters/ContainerBookingId' - $ref: '#/components/parameters/FileId' diff --git a/file/src/test/java/com/baiyue/file/config/LoadingEvidenceFileContractTest.java b/file/src/test/java/com/baiyue/file/config/LoadingEvidenceFileContractTest.java index 24f1c9f71..6597b2159 100644 --- a/file/src/test/java/com/baiyue/file/config/LoadingEvidenceFileContractTest.java +++ b/file/src/test/java/com/baiyue/file/config/LoadingEvidenceFileContractTest.java @@ -20,7 +20,10 @@ class LoadingEvidenceFileContractTest { .contains("description: 物理containerBookingId") .contains("LOADING_PHOTO_STEP_1", "LOADING_PHOTO_STEP_2", "LOADING_PHOTO_STEP_3", "LOADING_PHOTO_STEP_4", "LOADING_SEAL") - .contains("x-required-any-permission:", "platform:firstleg:pda:work", "Cache-Control固定no-store") + .contains("x-required-permission: platform:firstleg:inbound:loading:edit") + .contains("x-required-permission: platform:firstleg:inbound:loading:view") + .contains("国内PDA禁止直连File", "Cache-Control固定no-store") + .doesNotContain("platform:firstleg:pda:work") .doesNotContain("/{tenantId}/{inboundId}", "businessId=inboundId"); } } diff --git a/file/src/test/java/com/baiyue/file/controller/PlatformLoadingEvidenceControllerTest.java b/file/src/test/java/com/baiyue/file/controller/PlatformLoadingEvidenceControllerTest.java index 98f66f018..cd83d7ced 100644 --- a/file/src/test/java/com/baiyue/file/controller/PlatformLoadingEvidenceControllerTest.java +++ b/file/src/test/java/com/baiyue/file/controller/PlatformLoadingEvidenceControllerTest.java @@ -5,6 +5,8 @@ import com.baiyue.common.security.AuthenticatedUser; import com.baiyue.file.service.LoadingEvidenceService; import org.junit.jupiter.api.Test; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.multipart.MultipartFile; import java.util.Set; @@ -45,6 +47,21 @@ class PlatformLoadingEvidenceControllerTest { .isInstanceOf(BizException.class).hasMessageContaining("平台端身份"); } + @Test + void platformEndpointsExcludePdaWorkPermission() throws Exception { + var upload = PlatformLoadingEvidenceController.class.getMethod("upload", AuthenticatedUser.class, + long.class, String.class, MultipartFile.class); + var download = PlatformLoadingEvidenceController.class.getMethod("download", AuthenticatedUser.class, + long.class, long.class); + + assertThat(upload.getAnnotation(PreAuthorize.class).value()) + .isEqualTo("hasAuthority('platform:firstleg:inbound:loading:edit')") + .doesNotContain("pda:work"); + assertThat(download.getAnnotation(PreAuthorize.class).value()) + .isEqualTo("hasAuthority('platform:firstleg:inbound:loading:view')") + .doesNotContain("pda:work"); + } + private static AuthenticatedUser platform() { return new AuthenticatedUser(7L, 0L, 0L, 7L, "operator", "PLATFORM", "PDA", Set.of()); } diff --git a/file/src/test/java/com/baiyue/file/service/LoadingEvidenceServiceTest.java b/file/src/test/java/com/baiyue/file/service/LoadingEvidenceServiceTest.java index 261c18594..563c7db47 100644 --- a/file/src/test/java/com/baiyue/file/service/LoadingEvidenceServiceTest.java +++ b/file/src/test/java/com/baiyue/file/service/LoadingEvidenceServiceTest.java @@ -61,4 +61,10 @@ class LoadingEvidenceServiceTest { assertThat(metadata.tenantId()).isZero(); assertThat(metadata.businessId()).isEqualTo("20"); } + + @Test + void rejectsFileWhenContainerBookingDoesNotMatch() { + assertThatThrownBy(() -> service.validate(21, 30)) + .isInstanceOf(BizException.class).hasMessageContaining("装柜证据文件不存在"); + } }