# 文件上传功能说明 本文档基于以下代码整理: - `src/main/java/com/ruoyi/basic/utils/FileUtil.java` - `src/main/java/com/ruoyi/basic/enums/ApplicationTypeEnum.java` - `src/main/java/com/ruoyi/basic/enums/RecordTypeEnum.java` - `src/main/java/com/ruoyi/project/common/CommonController.java` - `src/main/java/com/ruoyi/basic/controller/StorageAttachmentController.java` 用于说明本项目中文件上传、附件绑定、文件预览/下载的整体设计,以及 `FileUtil` 中每个方法的作用。 ## 1. 整体设计 本项目的文件体系分成两层: - `storage_blob`:存文件实体信息 - 原始文件名 - 唯一文件名 `uidFilename` - 文件路径 `path` - 文件大小 `byteSize` - 文件类型 `contentType` - 公共访问标识 `resourceKey` - `storage_attachment`:存文件和业务记录的关联关系 - `application`:文件用途 - `recordType`:业务记录类型 - `recordId`:业务记录主键 - `storageBlobId`:关联的文件主表 id 可以理解为: - `storage_blob` 负责“文件本身” - `storage_attachment` 负责“文件挂在哪条业务数据上” ## 2. 上传流程 ### 2.1 普通上传 接口: - `POST /common/upload` 控制器位置: - `src/main/java/com/ruoyi/project/common/CommonController.java` 入参: - 表单字段名:`files` - 类型:`List` 代码逻辑: 1. 前端先调用 `/common/upload` 2. `CommonController.upload()` 调用 `storageBlobService.upload(files, false)` 3. 服务层保存文件元数据到 `storage_blob` 4. 返回 `StorageBlobVO` 列表,里面通常会带: - 文件 id - 原始文件名 - 唯一文件名 - 预览地址 `previewURL` - 下载地址 `downloadURL` 说明: - 此时只是“上传了文件” - 还没有和具体业务单据建立关系 ### 2.2 公共上传 接口: - `POST /common/public/upload` 代码逻辑: - `CommonController.publicUpload()` 调用 `storageBlobService.upload(files, true)` 说明: - 该接口上传的文件走“公共文件”模式 - 控制器注释已明确说明:永久有效,慎用 - 对应 URL 构建时,可能走 `publicKey` 参数,而不是临时 `token` ## 3. 附件绑定流程 上传完成后,如果需要把文件绑定到某条业务记录,需要再调用附件接口。 接口: - `POST /storageAttachment/add` 控制器位置: - `src/main/java/com/ruoyi/basic/controller/StorageAttachmentController.java` 核心请求对象: - `StorageAttachmentDTO` 其中继承了 `StorageAttachment`,并额外包含: - `storageBlobDTOs`:待绑定的文件列表 常用字段含义: - `application`:文件用途 - `recordType`:业务类型 - `recordId`:业务主键 - `storageBlobDTOs[].id`:上传成功后返回的文件 id 示例请求体: ```json { "application": "file", "recordType": "common_file", "recordId": 1001, "storageBlobDTOs": [ { "id": 12, "application": "file" }, { "id": 13, "application": "file" } ] } ``` 绑定逻辑说明: 1. 先上传文件,拿到 `storage_blob.id` 2. 再调用 `/storageAttachment/add` 3. 服务层最终会通过 `FileUtil` 保存 `storage_attachment` 4. 后续即可按业务记录查询出该记录下的附件 ## 4. 查询与删除附件 ### 4.1 查询附件列表 接口: - `GET /storageAttachment/list` 说明: - 按 `StorageAttachmentDTO` 中的条件查询 - 常见条件是 `application`、`recordType`、`recordId` - 返回结果本质上是和业务记录关联后的文件列表 ### 4.2 删除附件 接口: - `DELETE /storageAttachment/delete` 请求体: - `List ids` 说明: - 这里的 `ids` 是附件关联表 id,一般是 `storage_attachment.id` - 删除时通常不仅会删关联关系,也会进一步删除对应文件记录 ## 5. 预览与下载流程 ### 5.1 下载接口 接口: - `GET /common/download/{fileName}` 支持两种访问方式: - 临时链接:`token` - 公共链接:`publicKey` 代码逻辑: 1. 如果请求里有 `publicKey`,走 `storageBlobService.getPublicFile(fileName, publicKey)` 2. 否则走 `storageBlobService.getFileByToken(fileName, token)` 3. 取到实际文件后,调用 `fileUtil.compressFile(file)` 做图片压缩处理 4. 设置下载响应头,输出文件流 ### 5.2 预览接口 接口: - `GET /common/preview/{fileName}` 支持两种访问方式: - 临时链接:`token` - 公共链接:`publicKey` 代码逻辑: 1. 校验 `token` 或 `publicKey` 2. 获取文件 3. 调用 `fileUtil.compressFile(file)` 4. 根据文件内容类型返回 inline 预览 ## 6. 枚举含义 ### 6.1 `ApplicationTypeEnum` 位置: - `src/main/java/com/ruoyi/basic/enums/ApplicationTypeEnum.java` 当前定义值: | 枚举 | type | 说明 | |---|---|---| | `IMAGE` | `image` | 图片类文件 | | `FILE` | `file` | 普通文件 | | `AFTER_FILE` | `after_file` | 售后相关文件 | | `BEFORE_FILE` | `before_file` | 售前/前置相关文件 | | `APK` | `apk` | 安装包文件 | 作用: - 用于区分同一条业务记录下,不同用途的文件 - `FileUtil` 的很多查询、删除、保存方法都会用到该字段 ### 6.2 `RecordTypeEnum` 位置: - `src/main/java/com/ruoyi/basic/enums/RecordTypeEnum.java` 作用: - 用于标记文件属于哪类业务记录 - 例如质检、采购、客户、售后、台账、通知、设备等模块 - 上传完成后,附件最终通过 `recordType + recordId` 和业务数据关联 说明: - 该枚举值很多,文档不逐个展开 - 实际使用时必须传代码中已定义的 `type` 值 - 如: - `common_file` - `after_sales_service` - `quality_inspect` - `product` - `notice` ## 7. `FileUtil` 方法说明 `FileUtil` 是本套文件上传体系的核心工具类,主要负责: - 文件与业务记录绑定 - 文件与附件删除 - 附件查询 - 预览/下载地址生成 - token 使用次数控制 - 图片压缩 下面按功能分组说明每个方法。 ### 7.1 保存附件关系 #### 1. `saveStorageAttachment(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId, List storageBlobDTOS)` 作用: - 按“文件用途 + 记录类型 + 记录 id”保存附件关系 逻辑: 1. 校验 `application`、`recordType`、`recordId` 2. 先删除这组业务记录下的旧附件 3. 把新的 `storageBlobDTOS` 转成 `storage_attachment` 记录后批量插入 适用场景: - 某条业务数据重新保存附件,旧附件整体替换成新附件 #### 2. `saveStorageAttachmentByRecordTypeAndRecordId(String application, RecordTypeEnum recordType, Long recordId, List storageBlobDTOS)` 作用: - 按 `recordType + recordId` 保存附件关系,`application` 可指定,也可从每个文件对象里读取 逻辑特点: - 如果 `application == null`,会根据 `storageBlobDTO.application` 分别删除旧关系 - 如果附件列表为空,会直接删除该业务记录的附件关系 - 插入时会自动回填 `application` 适用场景: - 一次提交里可能包含多种用途的附件 - 或者调用方不方便直接传枚举类型 ### 7.2 删除文件主表 `storage_blob` #### 3. `deleteStorageBlobs(List storageBlobIds)` 作用: - 按文件主表 id 批量删除文件记录 #### 4. `deleteStorageBlobsByStorageAttachmentIds(List storageAttachmentIds)` 作用: - 先根据附件关联 id 查到 `storageBlobId` - 再删除对应的文件主表记录 #### 5. `deleteStorageBlobsByApplicationAndRecordTypeAndRecordIds(ApplicationTypeEnum application, RecordTypeEnum recordType, List recordIds)` 作用: - 根据用途、记录类型、多个业务 id,批量删除对应的文件主表记录 适用场景: - 批量删除某类业务数据时,同时清理附件 #### 6. `deleteStorageBlobsByRecordTypeAndRecordId(RecordTypeEnum recordType, Long recordId)` 作用: - 根据 `recordType + recordId` 删除该业务记录下所有文件主表记录 ### 7.3 删除附件关系 `storage_attachment` #### 7. `deleteStorageAttachmentsByStorageAttachmentIds(List storageAttachmentIds)` 作用: - 先删除附件对应的文件主表记录 - 再删除附件关系表记录 #### 8. `deleteStorageAttachmentsByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId)` 作用: - 删除指定用途、指定业务记录下的附件关系 特点: - 会先删 blob,再删 attachment #### 9. `deleteStorageAttachmentsByRecordTypeAndRecordId(RecordTypeEnum recordType, Long recordId)` 作用: - 删除指定业务记录下全部附件关系,不区分用途 #### 10. `deleteStorageAttachmentsByApplicationAndRecordTypeAndRecordIds(ApplicationTypeEnum application, RecordTypeEnum recordType, List recordIds)` 作用: - 按多个业务 id 批量删除附件关系 ### 7.4 查询附件关系 #### 11. `getStorageAttachmentsByStorageAttachmentIds(List storageAttachmentIds)` 作用: - 根据附件关系 id 查询 `storage_attachment` 记录 #### 12. `getStorageAttachmentsByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId)` 作用: - 按用途、业务类型、业务 id 查询附件关系 #### 13. `getStorageAttachmentsByRecordTypeAndRecordId(RecordTypeEnum recordType, Long recordId)` 作用: - 按业务类型、业务 id 查询附件关系 ### 7.5 查询文件信息 `StorageBlobVO` #### 14. `getStorageBlobVOsByApplicationAndRecordTypeAndRecordId(StorageAttachmentDTO storageAttachmentDTO)` 作用: - 通过 `StorageAttachmentDTO` 条件查询文件列表 特点: - `application` 可选 - 最终返回的是带预览/下载地址的 `StorageBlobVO` #### 15. `getStorageBlobVOsByStorageAttachmentIds(List storageAttachmentIds)` 作用: - 根据附件关系 id 查询文件列表 特点: - 会自动构建: - `previewURL` - `downloadURL` - `storageAttachmentId` #### 16. `getStorageBlobVOsByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId)` 作用: - 按用途、业务类型、业务 id 查询文件列表 #### 17. `getStorageBlobVOsByRecordTypeAndRecordId(RecordTypeEnum recordType, Long recordId)` 作用: - 按业务类型、业务 id 查询文件列表 #### 18. `getStorageBlobVOsByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId, BigDecimal expired)` 作用: - 和第 16 个方法类似,但可以自定义链接过期时间 说明: - `expired` 单位是分钟 - 返回的预览/下载地址会按这个时间生成签名 #### 19. `getStorageBlobVOsByStorageAttachmentIds(List storageAttachmentIds, BigDecimal expired)` 作用: - 根据附件关系 id 查询文件列表,并自定义链接过期时间 ### 7.6 查询附件视图 `StorageAttachmentVO` #### 20. `getStorageAttachmentVOSByStorageAttachmentIds(List storageAttachmentIds)` 作用: - 查询附件视图对象 特点: - 每条附件记录里会嵌套自己的 `storageBlobVOS` #### 21. `getStorageAttachmentVOSByStorageAttachmentIds(List storageAttachmentIds, BigDecimal expired)` 作用: - 根据附件关系 id 查询附件视图,并自定义链接过期时间 #### 22. `getStorageAttachmentVOSByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId)` 作用: - 按业务维度查询附件视图 #### 23. `getStorageAttachmentVOSByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId, BigDecimal expired)` 作用: - 按业务维度查询附件视图,并自定义链接过期时间 ### 7.7 仅获取预览地址 #### 24. `getFilePreviewURLByStorageAttachmentIds(List storageAttachmentIds)` 作用: - 根据附件关系 id 列表,返回预览地址列表 #### 25. `getFilePreviewURLByStorageAttachmentIds(List storageAttachmentIds, BigDecimal expired)` 作用: - 根据附件关系 id 列表,返回带自定义过期时间的预览地址列表 #### 26. `getFilePreviewURLByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId)` 作用: - 按业务维度返回预览地址列表 #### 27. `getFilePreviewURLByApplicationAndRecordTypeAndRecordIdAndExpired(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId, BigDecimal expired)` 作用: - 按业务维度返回带自定义过期时间的预览地址列表 ### 7.8 仅获取下载地址 #### 28. `getFileDownloadURLByStorageAttachmentIds(List storageAttachmentIds)` 作用: - 根据附件关系 id 列表,返回下载地址列表 #### 29. `getFileDownloadURLByStorageAttachmentIds(List storageAttachmentIds, BigDecimal expired)` 作用: - 根据附件关系 id 列表,返回带自定义过期时间的下载地址列表 #### 30. `getFileDownloadURLByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId)` 作用: - 按业务维度返回下载地址列表 #### 31. `getFileDownloadURLByApplicationAndRecordTypeAndRecordIdAndExpired(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId, BigDecimal expired)` 作用: - 按业务维度返回带自定义过期时间的下载地址列表 ### 7.9 构建签名 URL #### 32. `buildSignedPreviewUrl(StorageBlobVO storageBlob)` 作用: - 使用系统默认过期时间,生成预览链接 实际调用: - 内部等价于调用 `buildSignedUrl(storageBlob, "/preview/", properties.getExpired())` #### 33. `buildSignedDownloadUrl(StorageBlobVO storageBlob)` 作用: - 使用系统默认过期时间,生成下载链接 实际调用: - 内部等价于调用 `buildSignedUrl(storageBlob, "/download/", properties.getExpired())` #### 34. `buildSignedUrl(StorageBlobVO storageBlob, String actionPath, BigDecimal expired)` 作用: - 构建统一的带签名预览/下载地址 支持: - `actionPath = "/preview/"` - `actionPath = "/download/"` 核心逻辑: 1. 校验路径参数和文件信息 2. 拼接基础访问地址 3. 如果 `expired == -1`,不生成 token,直接走 `publicKey` 4. 否则生成带过期时间的 JWT token 5. 把 token 的使用次数信息写入 Redis 6. 返回最终 URL 重要说明: - `expired` 单位为分钟 - 默认过期时间为 120 分钟 - 非永久链接会受“过期时间 + 使用次数限制”双重控制 ### 7.10 token 使用控制 #### 35. `cacheTokenUsage(String token, long expiredMillis)` 作用: - 把 token 使用次数初始化到 Redis 特点: - 初始值写入为 `0` - TTL 与 token 过期时间保持一致 说明: - 这是私有方法,供 `buildSignedUrl()` 内部调用 #### 36. `buildTokenUsageKey(String token)` 作用: - 统一生成 Redis key 格式: - `file:token:usage:{token}` 说明: - 这是私有方法 #### 37. `validateTokenUsage(String token)` 作用: - 校验 token 是否还能继续使用 核心逻辑: 1. 从 Redis 读取当前使用次数 2. 如果没有值,认为链接已过期或已失效 3. 如果达到上限,立即删除 Redis 记录并报错 4. 否则自增一次使用次数 5. 如果自增后达到上限,再删除 Redis 记录 说明: - 该方法通常会在实际访问文件时由服务层调用 #### 38. `resolveLimit()` 作用: - 解析 token 可使用次数上限 规则: - `properties.getUseLimit() <= 0` 时,默认返回 `10` 说明: - 这是私有方法 ### 7.11 路径与压缩 #### 39. `buildRelativePath()` 作用: - 生成文件存储相对路径 格式: - `yyyy/MMdd` 例如: - `2026/0430` 用途: - 一般用于按日期分目录保存上传文件 #### 40. `compressFile(File file)` 作用: - 对图片进行压缩,非图片或不满足条件时返回原文件 压缩条件: 1. 开启了 `properties.getCompress()` 2. 文件是图片 3. 文件大小大于 `properties.getNeedCompressSize()` 处理逻辑: 1. 目标文件名为 `thumb_原文件名` 2. 如果压缩文件已存在,直接复用 3. 使用 `Thumbnailator` 按原尺寸压缩画质 4. 如果压缩失败,降级返回原文件 说明: - 当前下载和预览接口都会调用这个方法 #### 41. `isImage(String fileName)` 作用: - 简单判断文件是否是图片 支持后缀: - `jpg` - `jpeg` - `png` 说明: - 这是私有方法,供 `compressFile()` 使用 ## 8. 推荐使用顺序 业务上最常见的接入顺序如下: 1. 前端上传文件到 `/common/upload` 2. 拿到返回结果中的文件 id 3. 业务保存时调用 `/storageAttachment/add` 4. 传入 `application + recordType + recordId + storageBlobDTOs` 5. 后续页面回显时按业务条件调用附件查询 6. 前端使用返回的 `previewURL` 或 `downloadURL` ## 9. 常见注意点 ### 9.1 先上传,再绑定 - `/common/upload` 只负责文件入库 - `/storageAttachment/add` 才是和业务数据建立关系 ### 9.2 `application` 很重要 - 同一条 `recordId` 下可能有多组不同用途附件 - 删除和查询时,经常依赖 `application` ### 9.3 下载链接不是永久有效 - 普通链接一般通过 JWT token 控制 - 同时受过期时间和使用次数限制 ### 9.4 公共文件要慎用 - `public/upload` 上传的文件可走永久公开访问 - 适合公开资源,不适合敏感文件 ### 9.5 图片预览/下载可能返回压缩文件 - 当前控制器在下载和预览前都会调用 `compressFile()` - 大图在访问时可能使用压缩后的副本 ## 10. 一句话总结 本项目的文件上传方案是“两阶段模型”: - 第一阶段上传文件,生成 `storage_blob` - 第二阶段绑定业务,生成 `storage_attachment` 而 `FileUtil` 则负责把“上传后的文件”变成“可查询、可预览、可下载、可删除、可控时效”的完整附件能力。