Files
RuoYi-Vue3-FastAPI/ruoyi-fastapi-backend/docs/file_management_usage_guide.md
T
insistence 25acc9427c feat: 新增文件管理功能并增强文件服务安全性 (#111)
* feat: 新增文件管理功能,提升文件服务安全性

* docs: 新增文件管理使用文档

* docs: 更新README文档

* feat: 新增业务附件组件

* feat: 新增文件存储对账中心

* docs: 更新README文档

* feat: 新增到期文件处置功能

* perf: 完善上传人权限显式化

* feat: 新增Range 与断点下载

* docs: 更新README文档
2026-07-26 19:21:09 +08:00

13 KiB
Raw Blame History

文件管理使用与业务接入指南

本文说明文件管理功能怎么使用,以及业务模块怎么接入。

1. 怎么选择上传方式

场景 使用方式 业务保存内容
头像、Logo、富文本图片等公开资源 /common/upload 或 ImageUpload /profile/... URL
旧业务的普通公开附件 默认 FileUpload URL 字符串
简单私有附件,不需要业务引用保护 FileUpload :is-private="true" 鉴权下载 URL
合同、审批材料等正式业务附件 BusinessFileUpload { fileId, name, url } 列表

正式业务附件推荐使用最后一种方式,它支持:

  • 文件下载鉴权。
  • 用户、角色、部门 ACL。
  • 业务引用保护,防止文件仍在使用时被删除。
  • 按业务类型应用保留策略。
  • 文件操作审计和回收站。

公开文件通过 /profile 直接访问,不适合存放需要权限保护的内容。

2. 文件管理页面怎么使用

文件管理页面主要用于:

  • 查询文件、占用空间、所有者、所属部门和文件状态。
  • 查看文件详情、业务引用和操作审计。
  • 为私有文件授权用户、角色或部门下载。
  • 转移文件所有者和所属部门。
  • 将没有业务引用的文件移入回收站。
  • 恢复文件或永久清理回收站文件。
  • 配置业务保留策略和查看到期提醒。
  • 执行存储对账并处理文件缺失、错位、孤立或内容不一致。

文件管理操作同时受菜单权限和数据权限限制。部门管理员只能管理其文件数据范围内的文件。

私有文件下载规则可以简单理解为:

  1. 文件到期后直接拒绝。
  2. 管理员或文件所有者允许。
  3. 命中的 deny ACL 优先拒绝。
  4. 上传者允许。
  5. 命中的 allow ACL 允许。
  6. 其他情况默认拒绝。

2.1 存储对账怎么使用

系统管理员在文件管理页面点击“对账”,可以执行文件信息表与本地存储的双向检查。默认校验文件是否存在、所在区域和大小;需要确认文件内容没有被替换时,再开启 SHA-256 校验。摘要校验会读取全部文件内容,建议在低峰期执行。

异常明细会给出预期位置、实际位置和服务端允许的处理动作。常用处理方式如下:

  • 文件位于错误区域时移动到预期区域。
  • 有效文件误入回收站时恢复,已删除文件仍在正式区时移入回收站。
  • 重复文件或孤立文件先隔离,核实后恢复或永久删除。
  • 确认当前物理文件正确时,接受当前大小和摘要。
  • 确认孤立文件属于系统时,将其登记为当前管理员所有的文件。
  • 暂时无需处理时忽略,之后可以重新打开。

所有处理都必须填写原因。隔离区不提供静态访问;永久删除隔离文件不可恢复。定时任务中的“文件存储对账”默认暂停,可在任务管理中确认运行时段后启用。

3. 前端接入

3.1 通用约定

Vue2 和 Vue3 使用相同的受保护文件上传接口:

POST /common/files/upload
Content-Type: multipart/form-data
Authorization: Bearer <token>

上传成功后会返回:

{
  "code": 200,
  "fileId": "8e5787b4-daf7-4e31-bf04-f1cc16e0f65a",
  "originalFilename": "合同.pdf",
  "accessType": "private",
  "downloadUrl": "/common/files/8e5787b4-daf7-4e31-bf04-f1cc16e0f65a/download/合同.pdf"
}

业务表单都应保存 { fileId, name, url },提交业务接口时传完整的 fileId 列表。不要从下载 URL 中截取 fileId。

3.2 Vue3 写法

Vue3 项目已经全局注册 BusinessFileUpload:

<template>
  <business-file-upload
    ref="attachmentUploadRef"
    v-model="form.attachmentList"
    :limit="10"
    :file-size="20"
  />
</template>

<script setup>
const attachmentUploadRef = ref();
const form = reactive({
  attachmentList: []
});

function buildSubmitData() {
  return {
    ...form,
    attachmentFileIds: attachmentUploadRef.value.getFileIds()
  };
}
</script>

3.3 Vue2 写法

Vue2 项目同样全局注册 BusinessFileUpload:

<template>
  <business-file-upload
    ref="attachmentUpload"
    v-model="form.attachmentList"
    :limit="10"
    :file-size="20"
  />
</template>

<script>
export default {
  data() {
    return {
      form: {
        attachmentList: []
      }
    };
  },
  methods: {
    buildSubmitData() {
      return {
        ...this.form,
        attachmentFileIds: this.$refs.attachmentUpload.getFileIds()
      };
    }
  }
};
</script>

组件支持结构化数据回显、上传失败重试、删除、拖动排序和鉴权下载。v-model 的数据格式始终为:

[
  {
    "fileId": "8e5787b4-daf7-4e31-bf04-f1cc16e0f65a",
    "name": "合同.pdf",
    "url": "/common/files/8e5787b4-daf7-4e31-bf04-f1cc16e0f65a/download/存储文件名.pdf"
  }
]

也可以监听 change 事件,事件参数依次为结构化文件列表和文件 ID 列表。

3.4 上传组件的适用范围

Vue2 和 Vue3 都可以使用下面的写法上传简单私有附件:

<file-upload v-model="form.attachment" :is-private="true" />

两个版本的 FileUpload 都只把下载 URL 写入 v-model,不会保留 fileId。因此它们只适合旧业务或不需要引用保护的简单附件。正式业务附件统一使用 BusinessFileUpload。

两个版本的 ImageUpload 都继续用于公开图片。受保护文件下载应调用 $download.file(),不要使用普通 <a> 标签直接打开。

3.5 Range 与断点下载

Vue2 和 Vue3 的 $download.file()、$download.resource() 会按 8 MB 分段下载,并在分段请求失败时从当前分段重新请求。业务组件继续使用原有调用方式,不需要自行处理 Range。

自定义客户端可以向业务附件下载、文件管理下载或资源下载接口发送标准单区间请求:

Range: bytes=8388608-16777215

服务端返回 206 Partial Content,并携带 Accept-Ranges、Content-Range 和 Content-Length。起止范围、开放结束范围和后缀范围均受支持;不支持多区间请求,无效或越界范围返回 416。

/common/download?delete=true 用于下载后删除临时导出文件,始终整文件返回,不参与断点下载。需要断点下载时必须使用不会在响应结束后删除文件的下载地址。

4. 后端业务接入

下面以“合同”业务为例。

4.1 定义业务类型

每个业务模块定义一个稳定的业务类型:

CONTRACT_FILE_BUSINESS_TYPE = 'contract'

该值用于关联业务引用和保留策略。上线后不要随意修改,也不要使用中文显示名称。

4.2 请求模型接收文件 ID

class ContractModel(BaseModel):
    model_config = ConfigDict(alias_generator=to_camel)

    contract_id: int | None = Field(default=None, description='合同ID')
    contract_name: str = Field(description='合同名称')
    attachment_file_ids: list[str] = Field(
        default_factory=list,
        max_length=100,
        description='附件文件ID列表',
    )

修改接口必须传“修改后需要保留的完整文件 ID 列表”,空列表表示移除全部附件。

4.3 注入文件数据权限

业务控制器需要注入文件数据权限,并传给业务 Service:

file_data_scope_sql: Annotated[
    ColumnElement,
    DataScopeDependency(
        SysFileInfo,
        user_alias='owner_user_id',
        dept_alias='dept_id',
    ),
]

引用服务会校验文件存在、状态正常,并且位于当前用户的文件数据范围内。

4.4 新增和修改时同步引用

新增业务记录取得业务 ID 后,在同一个事务中写入文件引用:

try:
    db_contract = await ContractDao.add_contract_dao(query_db, contract)
    await FileReferenceService.replace_business_file_references_services(
        query_db=query_db,
        business_type=CONTRACT_FILE_BUSINESS_TYPE,
        business_id=str(db_contract.contract_id),
        file_ids=contract.attachment_file_ids,
        create_by=current_user.user.user_name,
        file_data_scope_sql=file_data_scope_sql,
        business_name=contract.contract_name,
    )
    await query_db.commit()
except Exception:
    await query_db.rollback()
    raise

修改业务时调用同一个方法,传入修改后的完整列表:

await ContractDao.edit_contract_dao(query_db, contract)
await FileReferenceService.replace_business_file_references_services(
    query_db=query_db,
    business_type=CONTRACT_FILE_BUSINESS_TYPE,
    business_id=str(contract.contract_id),
    file_ids=contract.attachment_file_ids,
    create_by=current_user.user.user_name,
    file_data_scope_sql=file_data_scope_sql,
    business_name=contract.contract_name,
)
await query_db.commit()

replace_business_file_references_services 是全量替换:

  • 原来是 [A, B],现在传 [B, C]:解除 A,保留 B,新增 C。
  • 传空列表:解除该业务对象的全部引用。

业务 DAO 应使用 flush() 获取新增 ID,不要提前 commit()。业务数据和文件引用必须由业务 Service 统一提交或回滚。

4.5 删除业务时解除引用

先用业务模块自身的数据权限确认业务对象可以删除,再在同一事务中解除引用:

try:
    db_contract = await ContractDao.get_contract_by_id_for_update(
        query_db,
        contract_id,
        contract_data_scope_sql,
    )
    if db_contract is None:
        raise ServiceException(message='合同不存在或超出数据权限')

    await FileReferenceService.remove_business_file_references_services(
        query_db,
        CONTRACT_FILE_BUSINESS_TYPE,
        str(contract_id),
    )
    await ContractDao.delete_contract_dao(query_db, contract_id)
    await query_db.commit()
except Exception:
    await query_db.rollback()
    raise

remove_business_file_references_services 不负责校验合同权限,所以必须在业务模块完成鉴权后调用。

解除引用不会立即删除文件。没有其他业务引用后,文件管理员才能将文件移入回收站。

4.6 业务详情回显附件

业务详情接口应按以下条件查询 sys_file_reference 并关联 sys_file_info:

business_type = CONTRACT_FILE_BUSINESS_TYPE
business_id = str(contract_id)

建议返回:

{
  "fileId": "8e5787b4-daf7-4e31-bf04-f1cc16e0f65a",
  "name": "合同.pdf",
  "downloadUrl": "/common/files/8e5787b4-daf7-4e31-bf04-f1cc16e0f65a/download/合同.pdf"
}

不要向前端返回 storage_key、物理路径或私有目录信息。

5. 业务引用、下载权限和保留策略

这三项作用不同:

能力 作用
业务引用 记录文件正在被哪个业务使用,并阻止误删
文件 ACL 决定除所有者、上传者外,还有谁可以下载
保留策略 决定新业务引用什么时候到期

业务引用不会自动授予下载权限。其他能查看合同的用户如果也要下载附件,需要:

  • 在文件管理页面手工配置用户、角色或部门 ACL;或
  • 由业务模块在参与人、角色、部门变化时同步文件 ACL。

保留策略按 business_type 生效,不是在文件上直接选择策略。例如:

保留策略:business_type=contract,retention_days=365
业务引用:business_type=contract,business_id=1001
结果:该引用在创建时得到 365 天的保留期限

因此,只有合同模块实际调用引用服务,并传入相同的 business_type='contract',策略才会作用到附件。

还需要注意:

  • 策略只应用于新建或重新写入的引用,不会自动修改历史引用。
  • 配置保留策略的业务只能引用私有文件。
  • 到期后文件不能下载,但不会自动删除,也不会自动解除引用。
  • 一个文件有多个引用时,只要存在永久引用,文件就不会到期。

6. 接入检查

一个业务模块完成以下内容即视为接入完成:

  • 使用 /common/files/upload 上传正式业务附件。
  • 前端保存上传响应中的 fileId。
  • 新增和修改接口传递完整的文件 ID 列表。
  • 控制器注入文件数据权限。
  • 业务新增、修改与引用更新使用同一个事务。
  • 业务删除先鉴权,再在同一事务中解除引用。
  • 业务详情返回结构化附件和鉴权下载地址。
  • 明确非所有者用户通过什么 ACL 下载。
  • 如果配置保留策略,策略的 business_type 与代码常量一致。

完成这些步骤后,文件管理页面才能正确显示业务引用,删除保护和保留策略也才会真正生效。