feat(file): support seven delivery document categories
This commit is contained in:
@@ -22,6 +22,7 @@
|
|||||||
| 订舱文件 | 通用内部业务文件入口,契约 `src/main/resources/openapi/ocean-booking-file-v1.yaml`;类型固定 `OCEAN_BOOKING_DOCUMENT`、`tenantId=订舱customerId`、`businessId=oceanBookingId`,任意格式且最多 50MiB,不得绑定拖车;`BOOKING_CONFIRMATION/BILL_OF_LADING` 文档类型及有效状态由 Firstleg 保存 |
|
| 订舱文件 | 通用内部业务文件入口,契约 `src/main/resources/openapi/ocean-booking-file-v1.yaml`;类型固定 `OCEAN_BOOKING_DOCUMENT`、`tenantId=订舱customerId`、`businessId=oceanBookingId`,任意格式且最多 50MiB,不得绑定拖车;`BOOKING_CONFIRMATION/BILL_OF_LADING` 文档类型及有效状态由 Firstleg 保存 |
|
||||||
| 拖车预约附件 | 通用内部业务文件入口;固定 `tenantId=0`、`businessType=TRUCK_APPOINTMENT_ATTACHMENT`、`businessId=appointmentId`,只允许派车单、司机/车辆资料和其他平台附件;提单文件归订舱 `OCEAN_BOOKING_DOCUMENT`,不得复制为拖车附件 |
|
| 拖车预约附件 | 通用内部业务文件入口;固定 `tenantId=0`、`businessType=TRUCK_APPOINTMENT_ATTACHMENT`、`businessId=appointmentId`,只允许派车单、司机/车辆资料和其他平台附件;提单文件归订舱 `OCEAN_BOOKING_DOCUMENT`,不得复制为拖车附件 |
|
||||||
| 存储与实体 | `service/LocalQualificationStorage.java`、`MeasurementImageStorage.java`、`entity/FileObjectEntity.java` |
|
| 存储与实体 | `service/LocalQualificationStorage.java`、`MeasurementImageStorage.java`、`entity/FileObjectEntity.java` |
|
||||||
|
| 派送单七类资料 | `src/main/resources/openapi/delivery-files-v1.yaml`;七类 `DELIVERY_*` 对象均按客户、派送内部ID、类别隔离,任意格式单文件50MiB,元数据返回真实createdAt;Lastleg持有分类绑定、保存、替换删除和历史可见性,File保留不可变对象 |
|
||||||
| 下游 OCR | `client/WeightOcrClient.java` |
|
| 下游 OCR | `client/WeightOcrClient.java` |
|
||||||
| SQL/配置 | `src/main/resources/db/file.sql`、`application.yml` |
|
| SQL/配置 | `src/main/resources/db/file.sql`、`application.yml` |
|
||||||
| 测试/验证 | `src/test/java/com/baiyue/file/`;`mvn -pl file -am test` |
|
| 测试/验证 | `src/test/java/com/baiyue/file/`;`mvn -pl file -am test` |
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# 派送资料 File 交付及打印 BOL 核查
|
||||||
|
|
||||||
|
范围:测试工作区当前 test 分支;File/Print。未执行部署、真实 HTTP 或数据库操作。
|
||||||
|
|
||||||
|
## 能力矩阵
|
||||||
|
|
||||||
|
| 能力 | 状态与证据 |
|
||||||
|
|---|---|
|
||||||
|
| 七类上传 | 新增七类白名单,映射详见 `src/main/resources/openapi/delivery-files-v1.yaml`;任意格式,非空且每文件≤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不复制绑定或状态 |
|
||||||
|
| 数据库 | 现有business_type VARCHAR(32)、business_id VARCHAR(64)、created_at足够,无结构变更或SQL执行 |
|
||||||
|
|
||||||
|
## 打印 BOL 现状
|
||||||
|
|
||||||
|
判定:Print通用渲染能力已实现,派送BOL业务打印链路未实现/待业务契约确认,未作环境验收。
|
||||||
|
|
||||||
|
- `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。
|
||||||
|
- 派送专属打印权限、状态门禁、字段映射、模板是否复用和输出入口尚不能由现有File/Print证明,本次未新增打印方案。其余五个按钮由Admin/后端16核查。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
- `mvn -pl file -am test`:File 200项,0失败/错误/跳过;其中新增DeliveryBusinessFileTest 21项,覆盖七类各自不可变上传、真实时间、50MiB边界和完整查询隔离。
|
||||||
|
- `mvn -pl print -am -Dtest=FreightA4PdfServiceGenerationTest,InternalPdfControllerTest -Dsurefire.failIfNoSpecifiedTests=false test`:10项通过,含PDFBox真实解析。没有把它作为派送业务打印已实现的证据。
|
||||||
|
- AGENTS索引合同未通过:现有柜费分摊金额守恒、整组操作契约两项索引缺失;不属于本次File变更。
|
||||||
|
- Lastleg绑定/替换删除后的用户可见性需由后端16专项测试及后续获准环境验收确认。
|
||||||
@@ -7,6 +7,13 @@ import java.util.Arrays;
|
|||||||
|
|
||||||
/** File-owned storage policy. Business visibility and lifecycle remain in the calling service. */
|
/** File-owned storage policy. Business visibility and lifecycle remain in the calling service. */
|
||||||
enum BusinessFileCategory {
|
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),
|
||||||
TRUCK_APPOINTMENT_ATTACHMENT(false, FormatPolicy.STANDARD, Limits.STANDARD, BusinessIdPolicy.POSITIVE_LONG),
|
TRUCK_APPOINTMENT_ATTACHMENT(false, FormatPolicy.STANDARD, Limits.STANDARD, BusinessIdPolicy.POSITIVE_LONG),
|
||||||
OCEAN_BOOKING_DOCUMENT(true, FormatPolicy.ARBITRARY, Limits.CUSTOMS, BusinessIdPolicy.POSITIVE_LONG),
|
OCEAN_BOOKING_DOCUMENT(true, FormatPolicy.ARBITRARY, Limits.CUSTOMS, BusinessIdPolicy.POSITIVE_LONG),
|
||||||
CUSTOMS_DOCUMENT(false, FormatPolicy.STANDARD, Limits.STANDARD, BusinessIdPolicy.POSITIVE_LONG),
|
CUSTOMS_DOCUMENT(false, FormatPolicy.STANDARD, Limits.STANDARD, BusinessIdPolicy.POSITIVE_LONG),
|
||||||
|
|||||||
@@ -84,10 +84,11 @@ public class InternalBusinessFileService {
|
|||||||
private static Metadata metadata(FileObjectEntity value) {
|
private static Metadata metadata(FileObjectEntity value) {
|
||||||
return new Metadata(value.getId(), value.getTenantId() == 0 ? "PLATFORM" : "CUSTOMER",
|
return new Metadata(value.getId(), value.getTenantId() == 0 ? "PLATFORM" : "CUSTOMER",
|
||||||
value.getTenantId(), value.getBusinessType(), value.getBusinessId(),
|
value.getTenantId(), value.getBusinessType(), value.getBusinessId(),
|
||||||
value.getOriginalName(), value.getSha256(), value.getContentType(), value.getSizeBytes());
|
value.getOriginalName(), value.getSha256(), value.getContentType(), value.getSizeBytes(), value.getCreatedAt());
|
||||||
}
|
}
|
||||||
|
|
||||||
public record Metadata(Long fileId, String ownerScope, Long tenantId, String businessType, String businessId,
|
public record Metadata(Long fileId, String ownerScope, Long tenantId, String businessType, String businessId,
|
||||||
String fileName, String sha256, String contentType, Long sizeBytes) {}
|
String fileName, String sha256, String contentType, Long sizeBytes,
|
||||||
|
LocalDateTime createdAt) {}
|
||||||
public record Download(String fileName, String contentType, byte[] content) {}
|
public record Download(String fileName, String contentType, byte[] content) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ components:
|
|||||||
sha256: {type: string}
|
sha256: {type: string}
|
||||||
contentType: {type: string}
|
contentType: {type: string}
|
||||||
sizeBytes: {type: integer, format: int64, maximum: 52428800}
|
sizeBytes: {type: integer, format: int64, maximum: 52428800}
|
||||||
|
createdAt: {type: string, description: 持久化上传时间LocalDateTime,无时区偏移}
|
||||||
ExportBatchRequest:
|
ExportBatchRequest:
|
||||||
type: object
|
type: object
|
||||||
required: [taskId, tenantId, appCode, requestedBy, dispatchVersion, parameters, limit]
|
required: [taskId, tenantId, appCode, requestedBy, dispatchVersion, parameters, limit]
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
openapi: 3.0.3
|
||||||
|
info:
|
||||||
|
title: 派送单七类资料 File 内部契约
|
||||||
|
version: 1.0.0
|
||||||
|
description: >-
|
||||||
|
tenantId为派送单所属客户,businessId为deliveryOrderId内部Long的十进制字符串,不是派送单号或BOL号。
|
||||||
|
任意格式,非空单文件最多50MiB(52428800字节)。每类最多20文件由Lastleg控制,七类不强制必传。
|
||||||
|
上传只创建不可变对象,不自动绑定。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
|
||||||
|
paths:
|
||||||
|
/internal/file/v1/business-files/{tenantId}/{businessType}/{businessId}:
|
||||||
|
parameters:
|
||||||
|
- {$ref: '#/components/parameters/Token'}
|
||||||
|
- {$ref: '#/components/parameters/Tenant'}
|
||||||
|
- {$ref: '#/components/parameters/Type'}
|
||||||
|
- {$ref: '#/components/parameters/Business'}
|
||||||
|
post:
|
||||||
|
summary: 上传派送资料不可变对象
|
||||||
|
parameters:
|
||||||
|
- {name: X-Operator-Id, in: header, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
multipart/form-data:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required: [file]
|
||||||
|
properties:
|
||||||
|
file: {type: string, format: binary}
|
||||||
|
responses:
|
||||||
|
'200': {$ref: '#/components/responses/Metadata'}
|
||||||
|
/internal/file/v1/business-files/{tenantId}/{businessType}/{businessId}/{fileId}:
|
||||||
|
parameters:
|
||||||
|
- {$ref: '#/components/parameters/Token'}
|
||||||
|
- {$ref: '#/components/parameters/Tenant'}
|
||||||
|
- {$ref: '#/components/parameters/Type'}
|
||||||
|
- {$ref: '#/components/parameters/Business'}
|
||||||
|
- {$ref: '#/components/parameters/File'}
|
||||||
|
get:
|
||||||
|
summary: 校验客户、派送单、类别和文件ID,读取有效未删除文件元数据
|
||||||
|
responses:
|
||||||
|
'200': {$ref: '#/components/responses/Metadata'}
|
||||||
|
/internal/file/v1/business-files/{tenantId}/{businessType}/{businessId}/{fileId}/content:
|
||||||
|
parameters:
|
||||||
|
- {$ref: '#/components/parameters/Token'}
|
||||||
|
- {$ref: '#/components/parameters/Tenant'}
|
||||||
|
- {$ref: '#/components/parameters/Type'}
|
||||||
|
- {$ref: '#/components/parameters/Business'}
|
||||||
|
- {$ref: '#/components/parameters/File'}
|
||||||
|
get:
|
||||||
|
summary: 按完整归属下载;内部令牌必需,不提供匿名下载
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 二进制成功响应;失败按统一JSON code判断
|
||||||
|
headers:
|
||||||
|
Cache-Control: {schema: {type: string, enum: [no-store]}}
|
||||||
|
Content-Disposition: {schema: {type: string}, description: attachment及UTF-8原文件名}
|
||||||
|
components:
|
||||||
|
parameters:
|
||||||
|
Token: {name: X-Internal-Service-Token, in: header, required: true, schema: {type: string}}
|
||||||
|
Tenant: {name: tenantId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||||
|
Business: {name: businessId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||||
|
File: {name: fileId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||||
|
Type:
|
||||||
|
name: businessType
|
||||||
|
in: path
|
||||||
|
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]
|
||||||
|
responses:
|
||||||
|
Metadata:
|
||||||
|
description: 业务成功/失败使用HTTP200,成功code为字符串200;无效内部令牌拒绝
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
code: {type: string}
|
||||||
|
message: {type: string}
|
||||||
|
data:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
fileId: {type: string}
|
||||||
|
ownerScope: {type: string, enum: [CUSTOMER]}
|
||||||
|
tenantId: {type: string}
|
||||||
|
businessType: {type: string}
|
||||||
|
businessId: {type: string}
|
||||||
|
fileName: {type: string}
|
||||||
|
sha256: {type: string}
|
||||||
|
contentType: {type: string}
|
||||||
|
sizeBytes: {type: integer, format: int64, maximum: 52428800}
|
||||||
|
createdAt: {type: string, description: 持久化created_at原值,LocalDateTime格式yyyy-MM-ddTHH:mm:ss,无时区偏移}
|
||||||
@@ -112,3 +112,4 @@ components:
|
|||||||
sha256: {type: string, pattern: '^[0-9a-f]{64}$'}
|
sha256: {type: string, pattern: '^[0-9a-f]{64}$'}
|
||||||
contentType: {type: string}
|
contentType: {type: string}
|
||||||
sizeBytes: {type: integer, format: int64, minimum: 1, maximum: 52428800}
|
sizeBytes: {type: integer, format: int64, minimum: 1, maximum: 52428800}
|
||||||
|
createdAt: {type: string, description: 持久化上传时间LocalDateTime,无时区偏移}
|
||||||
|
|||||||
@@ -134,3 +134,4 @@ components:
|
|||||||
sha256: {type: string, pattern: '^[0-9a-f]{64}$'}
|
sha256: {type: string, pattern: '^[0-9a-f]{64}$'}
|
||||||
contentType: {type: string, enum: [application/vnd.openxmlformats-officedocument.spreadsheetml.sheet]}
|
contentType: {type: string, enum: [application/vnd.openxmlformats-officedocument.spreadsheetml.sheet]}
|
||||||
sizeBytes: {type: integer, format: int64, minimum: 1, maximum: 5242880}
|
sizeBytes: {type: integer, format: int64, minimum: 1, maximum: 5242880}
|
||||||
|
createdAt: {type: string, description: 持久化上传时间LocalDateTime,无时区偏移}
|
||||||
|
|||||||
@@ -107,3 +107,4 @@ components:
|
|||||||
sha256: {type: string, pattern: '^[0-9a-f]{64}$'}
|
sha256: {type: string, pattern: '^[0-9a-f]{64}$'}
|
||||||
contentType: {type: string}
|
contentType: {type: string}
|
||||||
sizeBytes: {type: integer, format: int64, minimum: 1, maximum: 10485760}
|
sizeBytes: {type: integer, format: int64, minimum: 1, maximum: 10485760}
|
||||||
|
createdAt: {type: string, description: 持久化上传时间LocalDateTime,无时区偏移}
|
||||||
|
|||||||
+1
-1
@@ -24,7 +24,7 @@ class InternalBusinessFileControllerTest {
|
|||||||
@Test void zeroTenantUploadAndMetadataRemainBehindInternalToken() {
|
@Test void zeroTenantUploadAndMetadataRemainBehindInternalToken() {
|
||||||
var file = new MockMultipartFile("file", "clearance.pdf", "application/pdf", "%PDF-1.4".getBytes());
|
var file = new MockMultipartFile("file", "clearance.pdf", "application/pdf", "%PDF-1.4".getBytes());
|
||||||
var metadata = new InternalBusinessFileService.Metadata(81L, "PLATFORM", 0L, "CONTAINER_CLEARANCE_DOCUMENT",
|
var metadata = new InternalBusinessFileService.Metadata(81L, "PLATFORM", 0L, "CONTAINER_CLEARANCE_DOCUMENT",
|
||||||
"41", "clearance.pdf", "hash", "application/pdf", file.getSize());
|
"41", "clearance.pdf", "hash", "application/pdf", file.getSize(), java.time.LocalDateTime.of(2026, 9, 9, 12, 0));
|
||||||
when(service.upload(0L, "CONTAINER_CLEARANCE_DOCUMENT", "41", 7L, file)).thenReturn(metadata);
|
when(service.upload(0L, "CONTAINER_CLEARANCE_DOCUMENT", "41", 7L, file)).thenReturn(metadata);
|
||||||
when(service.metadata(0L, "CONTAINER_CLEARANCE_DOCUMENT", "41", 81L)).thenReturn(metadata);
|
when(service.metadata(0L, "CONTAINER_CLEARANCE_DOCUMENT", "41", 81L)).thenReturn(metadata);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package com.baiyue.file.service;
|
||||||
|
|
||||||
|
import com.baiyue.file.entity.FileObjectEntity;
|
||||||
|
import com.baiyue.file.mapper.FileObjectMapper;
|
||||||
|
import com.baiyue.file.controller.InternalBusinessFileController;
|
||||||
|
import com.baiyue.common.core.exception.BizException;
|
||||||
|
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.ValueSource;
|
||||||
|
import org.springframework.mock.web.MockMultipartFile;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
import static org.assertj.core.api.Assertions.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
import static org.mockito.ArgumentMatchers.*;
|
||||||
|
|
||||||
|
class DeliveryBusinessFileTest {
|
||||||
|
@TempDir Path root;
|
||||||
|
@BeforeAll static void metadata() {
|
||||||
|
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), "delivery-files"), FileObjectEntity.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@ValueSource(strings={"DELIVERY_BOL","DELIVERY_CUSTOMER_DOCUMENT","DELIVERY_BILL_OF_LADING","DELIVERY_POD","DELIVERY_PHOTO","DELIVERY_LOADING_PHOTO","DELIVERY_EXPRESS_RECEIPT"})
|
||||||
|
void immutableUploadsPreserveOldContentAndRealTimestamp(String type) {
|
||||||
|
var storage = storage();
|
||||||
|
var files = mock(FileObjectMapper.class);
|
||||||
|
var service = service(files, storage);
|
||||||
|
var ids = new AtomicLong(800);
|
||||||
|
when(files.insert(any(FileObjectEntity.class))).thenAnswer(call -> {
|
||||||
|
FileObjectEntity row = call.getArgument(0); row.setId(ids.incrementAndGet()); return 1;
|
||||||
|
});
|
||||||
|
var first = service.upload(11, type, "71", 9, new MockMultipartFile("file", "资料 🚢.bin", "application/octet-stream", new byte[]{0,1,2}));
|
||||||
|
var second = service.upload(11, type, "71", 9, new MockMultipartFile("file", "资料 🚢.bin", "application/octet-stream", new byte[]{3,4}));
|
||||||
|
var captor = org.mockito.ArgumentCaptor.forClass(FileObjectEntity.class);
|
||||||
|
verify(files, times(2)).insert(captor.capture());
|
||||||
|
assertThat(first.fileId()).isNotEqualTo(second.fileId());
|
||||||
|
assertThat(first.createdAt()).isEqualTo(captor.getAllValues().get(0).getCreatedAt()).isNotNull();
|
||||||
|
assertThat(first.ownerScope()).isEqualTo("CUSTOMER");
|
||||||
|
assertThat(storage.read(captor.getAllValues().get(0).getObjectKey())).containsExactly(0,1,2);
|
||||||
|
assertThat(storage.read(captor.getAllValues().get(1).getObjectKey())).containsExactly(3,4);
|
||||||
|
verify(files, never()).updateById(any(FileObjectEntity.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@ValueSource(strings={"DELIVERY_BOL","DELIVERY_CUSTOMER_DOCUMENT","DELIVERY_BILL_OF_LADING","DELIVERY_POD","DELIVERY_PHOTO","DELIVERY_LOADING_PHOTO","DELIVERY_EXPRESS_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);
|
||||||
|
var file = mock(MultipartFile.class);
|
||||||
|
when(file.isEmpty()).thenReturn(false); when(file.getSize()).thenReturn(52428801L);
|
||||||
|
assertThatThrownBy(() -> storage.store(11,"71",file,category)).isInstanceOf(BizException.class).hasMessageContaining("50MB");
|
||||||
|
when(file.getSize()).thenReturn(52428800L); when(file.getContentType()).thenReturn("application/x-custom");
|
||||||
|
when(file.getOriginalFilename()).thenReturn("boundary.custom");
|
||||||
|
doAnswer(call -> { try (var out=new java.io.RandomAccessFile(((Path)call.getArgument(0)).toFile(),"rw")) { out.setLength(52428800L); } return null; }).when(file).transferTo(any(Path.class));
|
||||||
|
var stored=storage.store(11,"71",file,category);
|
||||||
|
assertThat(java.nio.file.Files.size(root.resolve(stored.objectKey()))).isEqualTo(52428800L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@ValueSource(strings={"DELIVERY_BOL","DELIVERY_CUSTOMER_DOCUMENT","DELIVERY_BILL_OF_LADING","DELIVERY_POD","DELIVERY_PHOTO","DELIVERY_LOADING_PHOTO","DELIVERY_EXPRESS_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 -> {
|
||||||
|
LambdaQueryWrapper<FileObjectEntity> query=call.getArgument(0);
|
||||||
|
assertThat(query.getSqlSegment()).contains("tenant_id", "business_type", "business_id", "id", "status", "deleted");
|
||||||
|
assertThat(query.getParamNameValuePairs().values()).containsExactlyInAnyOrder(12L,type,"72",802L,"ACTIVE",false);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
assertThatThrownBy(() -> service.download(12,type,"72",802)).isInstanceOf(BizException.class).hasMessageContaining("归属不匹配");
|
||||||
|
assertThatThrownBy(() -> service.metadata(0,type,"71",801)).isInstanceOf(BizException.class);
|
||||||
|
assertThatThrownBy(() -> service.metadata(11,type,"not-an-id",801)).isInstanceOf(BizException.class);
|
||||||
|
var controller=new InternalBusinessFileController();
|
||||||
|
ReflectionTestUtils.setField(controller,"expected","test-internal"); ReflectionTestUtils.setField(controller,"service",service);
|
||||||
|
assertThatThrownBy(() -> controller.download("bad",11,type,"71",801)).isInstanceOf(BizException.class);
|
||||||
|
verifyNoInteractions(storage);
|
||||||
|
}
|
||||||
|
private BusinessFileStorage storage() {
|
||||||
|
var storage=new BusinessFileStorage(); ReflectionTestUtils.setField(storage,"configuredRoot",root); storage.initializeRoot(); return storage;
|
||||||
|
}
|
||||||
|
private InternalBusinessFileService service(FileObjectMapper files, BusinessFileStorage storage) {
|
||||||
|
var service=new InternalBusinessFileService(); ReflectionTestUtils.setField(service,"files",files); ReflectionTestUtils.setField(service,"storage",storage); return service;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user