编辑 | blame | 历史 | 原始文档

文件上传功能说明

本文档基于以下代码整理:

  • 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<MultipartFile>

代码逻辑:

  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

示例请求体:

{
  "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 中的条件查询
  • 常见条件是 applicationrecordTyperecordId
  • 返回结果本质上是和业务记录关联后的文件列表

4.2 删除附件

接口:

  • DELETE /storageAttachment/delete

请求体:

  • List<Long> 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. 校验 tokenpublicKey
  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<StorageBlobDTO> storageBlobDTOS)

作用:

  • 按“文件用途 + 记录类型 + 记录 id”保存附件关系

逻辑:

  1. 校验 applicationrecordTyperecordId
  2. 先删除这组业务记录下的旧附件
  3. 把新的 storageBlobDTOS 转成 storage_attachment 记录后批量插入

适用场景:

  • 某条业务数据重新保存附件,旧附件整体替换成新附件

2. saveStorageAttachmentByRecordTypeAndRecordId(String application, RecordTypeEnum recordType, Long recordId, List<StorageBlobDTO> storageBlobDTOS)

作用:

  • recordType + recordId 保存附件关系,application 可指定,也可从每个文件对象里读取

逻辑特点:

  • 如果 application == null,会根据 storageBlobDTO.application 分别删除旧关系
  • 如果附件列表为空,会直接删除该业务记录的附件关系
  • 插入时会自动回填 application

适用场景:

  • 一次提交里可能包含多种用途的附件
  • 或者调用方不方便直接传枚举类型

7.2 删除文件主表 storage_blob

3. deleteStorageBlobs(List<Long> storageBlobIds)

作用:

  • 按文件主表 id 批量删除文件记录

4. deleteStorageBlobsByStorageAttachmentIds(List<Long> storageAttachmentIds)

作用:

  • 先根据附件关联 id 查到 storageBlobId
  • 再删除对应的文件主表记录

5. deleteStorageBlobsByApplicationAndRecordTypeAndRecordIds(ApplicationTypeEnum application, RecordTypeEnum recordType, List<Long> recordIds)

作用:

  • 根据用途、记录类型、多个业务 id,批量删除对应的文件主表记录

适用场景:

  • 批量删除某类业务数据时,同时清理附件

6. deleteStorageBlobsByRecordTypeAndRecordId(RecordTypeEnum recordType, Long recordId)

作用:

  • 根据 recordType + recordId 删除该业务记录下所有文件主表记录

7.3 删除附件关系 storage_attachment

7. deleteStorageAttachmentsByStorageAttachmentIds(List<Long> storageAttachmentIds)

作用:

  • 先删除附件对应的文件主表记录
  • 再删除附件关系表记录

8. deleteStorageAttachmentsByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId)

作用:

  • 删除指定用途、指定业务记录下的附件关系

特点:

  • 会先删 blob,再删 attachment

9. deleteStorageAttachmentsByRecordTypeAndRecordId(RecordTypeEnum recordType, Long recordId)

作用:

  • 删除指定业务记录下全部附件关系,不区分用途

10. deleteStorageAttachmentsByApplicationAndRecordTypeAndRecordIds(ApplicationTypeEnum application, RecordTypeEnum recordType, List<Long> recordIds)

作用:

  • 按多个业务 id 批量删除附件关系

7.4 查询附件关系

11. getStorageAttachmentsByStorageAttachmentIds(List<Long> 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<Long> 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<Long> storageAttachmentIds, BigDecimal expired)

作用:

  • 根据附件关系 id 查询文件列表,并自定义链接过期时间

7.6 查询附件视图 StorageAttachmentVO

20. getStorageAttachmentVOSByStorageAttachmentIds(List<Long> storageAttachmentIds)

作用:

  • 查询附件视图对象

特点:

  • 每条附件记录里会嵌套自己的 storageBlobVOS

21. getStorageAttachmentVOSByStorageAttachmentIds(List<Long> 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<Long> storageAttachmentIds)

作用:

  • 根据附件关系 id 列表,返回预览地址列表

25. getFilePreviewURLByStorageAttachmentIds(List<Long> 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<Long> storageAttachmentIds)

作用:

  • 根据附件关系 id 列表,返回下载地址列表

29. getFileDownloadURLByStorageAttachmentIds(List<Long> 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. 前端使用返回的 previewURLdownloadURL

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 则负责把“上传后的文件”变成“可查询、可预览、可下载、可删除、可控时效”的完整附件能力。