SpringBoot+Vue3+Onlyoffice 企业文件生命周期:上传、业务关联、权限、版本与安全清理

SpringBoot+Vue3+Onlyoffice 企业文件生命周期:上传、业务关联、权限、版本与安全清理

🌐 文档地址 :https://ruoyioffice.com

👇 文章底部获取源码和演示地址 👇

💬 :17156169080(获取产品咨询)

"把上传控件放到表单里"很容易,"附件移除了,文件能不能删"才是企业系统真正开始复杂的地方。一份合同可能被合同台账引用,也被项目资料引用;报销单移除附件后,文件对象仍可能被其他单据使用。若把业务引用、附件记录和存储对象混成一张表,删除、权限和版本很快就会互相影响。

▲ 文件本体居中,多张业务单据形成引用关系;解除一个引用,不代表文件无人使用。

本文区分当前代码已经具备的附件记录与文件存储能力,以及需要按模块继续落地的统一鉴权、引用保护和清理任务。

一、先把"文件"拆成四种对象

文件管理里最常见的误解,是把文件名、下载地址和业务附件当成同一个实体。

实际至少有四层:

层次 说明 变化原因
业务引用 合同、报销、项目资料中"使用了哪个文件" 单据编辑、解绑、换版本
附件记录 文件名、排序、上传时间、业务类型和业务 ID 业务保存或删除附件
存储对象 实际二进制、路径、大小和 MIME 类型 上传、迁移、物理删除
访问授权 谁能在什么业务上下文下载 角色、部门、租户、单据状态

这四层可以落在不同的表和服务中。

它们之间有关系,但不能共享同一个删除按钮语义。

1.1 "删除附件"到底删了什么

用户在合同编辑页点击移除,通常只表达"合同不再引用这个文件"。

它不一定表达"立即从对象存储销毁文件"。

如果文件只被这一条业务引用,系统可以在确认没有其他引用后进入回收流程。

如果文件被多条业务引用,直接删除对象会造成其他单据下载失败。

因此,前端文案也要区分"移除附件"和"永久删除文件",不要都写成删除。

1.2 文件地址不是权限

一个可访问的 URL 只能说明存储层找到了对象。

它不能证明当前用户有权查看该合同或报销单。

常见风险是上传接口做了登录校验,下载接口却只按文件 ID 查询。

一旦用户猜中连续 ID,就可能绕过业务权限。

下载接口必须回到业务引用,或者使用受控的短期地址和一次性授权。

二、业务引用层:谁在使用这个文件

▲ 业务引用指向附件记录,附件记录再指向存储对象;版本与审计是加强清理安全性的建议层。

通用附件服务已经提供按 businessType 和 businessId 查询附件列表的能力。

这为业务引用提供了统一入口。

保存附件列表时,服务会读取现有附件 ID,处理新增和更新,再删除本次提交中已经不存在的附件记录。

java 复制代码
@Transactional(rollbackFor = Exception.class)
public void saveAttachmentList(String businessType,
        Long businessId, List<AttachmentSaveReqVO> attachments) {
    Set<Long> existingIds = getAttachmentListByBusiness(businessType,
            businessId).stream()
            .map(AttachmentDO::getId)
            .collect(Collectors.toSet());
    Set<Long> processedIds = new HashSet<>();
    if (attachments != null) {
        for (AttachmentSaveReqVO req : attachments) {
            AttachmentDO item = BeanUtils.toBean(req, AttachmentDO.class);
            item.setBusinessType(businessType);
            item.setBusinessId(businessId);
            if (item.getId() == null) {
                attachmentMapper.insert(item);
            } else {
                attachmentMapper.updateById(item);
            }
            processedIds.add(item.getId());
        }
    }
    existingIds.removeAll(processedIds);
    if (!existingIds.isEmpty()) {
        attachmentMapper.deleteBatchIds(existingIds);
    }
}

这段代码说明了"附件记录列表"的增量保存逻辑。

它并没有自动证明底层文件对象已经安全删除,因为附件记录和文件存储可能由不同服务负责。

2.1 业务类型要稳定

businessType 是跨模块查询和清理的重要索引。

合同附件、报销附件和项目资料不能一会儿使用中文菜单名、一会儿使用前端路由名。

建议为业务类型建立稳定编码,并为每种类型定义:

  • 允许的文件类型和大小;
  • 上传者与查看者;
  • 是否允许多业务引用;
  • 解绑后保留期限;
  • 是否允许版本化;
  • 删除需要的审批级别。

2.2 排序是展示属性,不是版本号

附件列表中的 sortOrder 适合控制页面顺序。

它不能替代文件版本号。

用户把附件拖到第一位,不代表文件内容变成了新版本。

版本需要有明确的来源、创建人、变更说明和当前标识。

三、存储对象层:不要让业务表直接承担文件内容

▲ 文件服务页面用于管理实际存储对象和配置;业务附件还需要在业务层建立引用。

存储对象通常包含路径、文件名、大小、类型和上传时间。

二进制可以存本地、对象存储或其他文件服务,业务单据不应依赖具体存储实现。

3.1 路径和展示名称要分开

用户可修改展示名称,但不能因此改变物理路径。

路径最好由系统生成,避免用户输入 ../、脚本扩展名或覆盖其他对象。

下载时以对象 ID 或受控路径查询,再由服务生成展示文件名。

3.2 配置变化不能让历史文件失联

文件存储配置可能从本地切换到对象存储,也可能更换桶或目录。

配置页面的变化影响后续上传,不应默认让历史路径失效。

▲ 存储配置调整需要考虑历史对象迁移、旧路径兼容和失败回滚。

如果必须迁移,建议记录迁移批次、源路径、目标路径和校验结果。

不要只改一个配置字段就宣称文件全部迁移完成。

▲ 在线编辑页展示的是业务体验,底层仍应通过文件对象、版本和授权关系读取内容。

3.3 迁移任务要可暂停、可校验

存储迁移通常不是一次请求完成的动作,而是一个跨天运行的批次。

批次需要记录总对象数、已处理数量、成功数量、失败数量和最近游标。

管理员可以暂停批次修复配置,再从失败对象继续,而不是从第一条重新扫描。

迁移前后应至少校验对象摘要、大小和媒体类型。

如果目标存储只支持最终一致性,校验任务要延迟执行,并区分"暂未可读"和"摘要不一致"。

摘要不一致时禁止自动删除源对象,应先进入人工复核。

迁移期间新上传的对象可以继续写入新存储,也可以进入旧存储等待批次追赶。

无论选择哪种策略,都要把存储版本写进对象元数据,避免应用只根据当前配置拼接历史路径。

四、访问边界:下载动作要回到业务上下文

文件下载至少需要校验:

  1. 当前租户与文件所属租户一致;
  2. 当前用户能看到对应业务单据;
  3. 该业务单据允许当前状态下查看附件;
  4. 文件记录确实属于该业务;
  5. 下载行为满足审计和敏感信息策略。

业务模块可以通过附件服务读取列表,但不能让附件服务在没有业务授权信息时"默认放行"。

4.1 统一鉴权与模块鉴权的取舍

统一鉴权的优点是规则集中,便于审计。

模块鉴权的优点是更了解业务状态,例如合同归档后只有特定角色可以下载。

实践中可以采用两层模型:

  • 文件服务负责租户、对象存在性和基础风险校验;
  • 业务模块负责单据可见性、角色、部门和流程状态。

两层都通过后才生成下载响应。

4.2 共享链接是另一种产品能力

分享链接不能简单复用内部下载接口。

它需要过期时间、可撤销、访问次数、访问日志和脱敏策略。

内部用户的单据权限也不应自动转化为长期外部分享权限。

如果业务暂时不需要分享,就不要为了"以后可能用"开放永久公网 URL。

4.3 预览、缩略图和全文搜索也是访问面

很多系统只保护下载接口,却把缩略图、在线预览和全文搜索结果当成"非敏感数据"。

实际上,文件标题、第一页缩略图和搜索命中片段都可能泄露客户名称、合同金额或员工信息。

建议所有派生内容都携带源文件 ID、版本号和租户信息。

生成预览时再次执行业务授权,缓存键不要只使用文件 ID。

当文件版本被撤回或权限收紧时,缩略图和索引也要失效。

如果使用异步 OCR 或全文索引,队列消息中不要直接携带长期公开地址。

消费者应根据受控对象 ID获取内容,并在处理完成后写入可撤销的索引状态。

访问动作 必要校验 常见风险
下载原件 租户、业务权限、版本状态 猜 ID 越权
在线预览 业务权限、预览令牌、版本 缓存串租户
缩略图 对象授权、派生内容状态 通过首图泄密
全文搜索 索引权限、结果脱敏 片段泄露
外部分享 过期、次数、撤销和水印 链接长期有效

五、版本关系:编辑、替换和引用不是一回事

OnlyOffice 或云盘场景经常需要版本历史,但普通业务附件未必需要完整在线编辑。

文件版本至少要回答:

  • 新版本是否替换旧版本的展示内容;
  • 旧版本是否仍可被历史单据引用;
  • 版本删除是否影响当前业务;
  • 文件哈希是否用于去重;
  • 预览和下载读取哪一个版本。

建议把当前版本和历史版本都建立明确身份,不要只覆盖原文件路径。

5.1 合同正文与合同附件要分开

合同正文可能参与在线编辑、签章和归档。

合同附件可能只是证明材料。

两者的版本策略、预览方式和删除权限不同。

把所有文件都丢到一个云盘目录中,看似统一,实际会让业务状态和文件状态脱钩。

5.2 引用快照适合解释历史

如果项目资料引用了合同附件,合同后来改名,历史项目页面仍可能需要显示当时的名称。

可以在引用记录中保存展示名称快照和引用时版本。

快照不代替对象身份,它只帮助解释历史上下文。

5.3 文件摘要可以帮助去重,但不能代替权限

对大文件计算摘要,可以发现同一租户内重复上传的对象,降低存储成本。

但相同摘要不代表两个业务可以互相查看,去重只影响对象层,不应合并业务引用和访问授权。

推荐把摘要、大小、媒体类型和扫描结果放在文件元数据里。

命中相同摘要时可以复用对象,也要新建业务引用并记录复用关系。

如果文件经过在线编辑,摘要必须随新版本变化,不能继续使用旧值。

六、安全清理:解绑、回收和物理删除分三步

▲ 业务先保存引用,解绑后查询其他引用,确认无人使用后再考虑删除对象;这是设计的安全清理路径。

当前附件服务的 deleteAttachment 删除的是附件记录。

基础文件服务的 deleteFile 会调用存储客户端删除实际路径,再删除文件记录。

这两个入口不能直接当成同一个动作。

java 复制代码
@Override
public void deleteAttachment(Long id) {
    validateAttachmentExists(id);
    attachmentMapper.deleteById(id);
}

@Override
public void deleteFile(Long id) throws Exception {
    FileDO file = validateFileExists(id);
    client.delete(file.getPath());
    fileMapper.deleteById(id);
}

如果业务解绑后立即同时调用这两个方法,必须先确定是否还有其他引用。

否则共享文件会被过早销毁。

6.1 推荐的三阶段清理模型

第一阶段是解绑:业务单据不再引用附件。

第二阶段是回收:对象进入待清理状态,保留一段观察期。

第三阶段是物理删除:清理任务确认无人引用且满足保留策略后删除。

观察期可以处理误删恢复,也方便异步重试。

财务、合同和人事文件还应遵循企业归档与保留要求,不能只按技术方便清理。

6.2 清理任务必须幂等

任务重复执行时,已经删除的对象应被识别为已完成,而不是不断报警。

对象存储删除失败时,应保留错误原因和下次重试时间。

不能因为数据库记录已经删除,就让孤立对象永远没人负责。

下面是清理任务的概念示意,其中查询、状态更新和引用检查接口是设计建议,不是现有实现的直接摘录。

生产实现必须让"新增引用"和"进入删除状态"遵守同一互斥协议,否则检查后又新增引用仍可能误删。

java 复制代码
public void reclaimOrphanFiles(LocalDateTime now) {
    List<FileDO> candidates = fileMapper.selectReclaimable(now);
    for (FileDO file : candidates) {
        if (referenceMapper.countByFileId(file.getId()) > 0) {
            fileMapper.markActive(file.getId());
            continue;
        }
        try {
            client.delete(file.getPath());
            fileMapper.markDeleted(file.getId(), now);
        } catch (Exception ex) {
            fileMapper.markRetry(file.getId(), ex.getMessage(), now);
        }
    }
}

这里的 selectReclaimable 应该同时检查解绑时间、保留期限、归档标记和租户策略。

任务不能只依赖文件表中的"未引用"字段,因为引用关系可能来自不同业务库或历史迁移记录。

6.3 临时上传要有归属期限

上传通常先于业务单据保存。

用户选择文件后关闭弹窗、网络中断或审批草稿被放弃,都会留下暂时没有业务引用的对象。

临时对象可以记录上传会话、操作者、租户、创建时间和预期业务类型。

业务保存成功后,把临时对象转为正式引用;超过期限仍没有引用的对象进入回收队列。

对象状态 是否允许业务下载 是否进入清理
临时上传 仅上传者可见 到期后进入
已引用 按业务权限 不直接进入
已解绑 视保留策略 观察期后进入
已归档 按归档角色 通常禁止自动清理
清理失败 管理员可见 重试或人工处理

状态变化要保留事件,而不是直接覆盖一个布尔字段。

这样可以回答"这个文件为什么在回收队列里""谁在什么时候重新绑定过"。

七、代码之外的治理:谁可以删除

文件删除权限通常高于上传权限。

普通员工可以上传报销凭证,但不一定可以删除已经进入审核的凭证。

合同管理员可以替换正文版本,但不一定可以删除归档版本。

建议把操作拆成:

  • 上传;
  • 查看;
  • 下载;
  • 替换当前版本;
  • 解绑业务引用;
  • 申请物理删除;
  • 审批物理删除;
  • 执行物理删除。

当所有动作都叫"编辑附件",权限配置会变得模糊。

7.1 审计日志要记录"对象"和"业务"

文件审计至少包括对象 ID、业务类型、业务 ID、版本号、操作者、租户、动作、结果和失败原因。

只记录"用户下载了文件 123"不够,因为同一个文件可能被多个合同和项目引用。

对于物理删除,还应保存删除前的路径、摘要和引用检查结果。

审计日志本身不应随着文件删除一并清理,否则发生争议时无法证明删除是否合法。

可以把敏感文件的下载分成普通查看、导出原件和外部分享三类动作。

三类动作的审批级别、日志粒度和水印策略通常不同,最好在产品上明确区分。

八、验收清单

建议准备一份被三个业务单据引用的文件,按顺序验证:

  1. 合同 A 上传文件,项目 B 引用同一对象。
  2. 报销 C 再次引用,查看三处业务权限。
  3. 合同 A 解绑,确认 B、C 仍可访问。
  4. 删除最后一个引用,确认进入待清理而非立即销毁。
  5. 清理任务执行失败,确认错误可见且可重试。
  6. 普通员工猜测文件 ID,确认无法越权下载。
  7. 文件改名,确认存储路径和历史引用稳定。
  8. 新版本上传,确认旧版本是否按策略保留。

8.1 临时上传与草稿放弃

打开合同编辑页,上传两份附件但不保存单据。

等待临时对象超过观察期,运行清理任务。

预期结果是临时对象进入待回收状态,上传者在清理前仍能看到失败原因,任务重试不会重复删除。

再次打开同一单据并保存,验证临时对象能转为正式引用。

如果保存请求重复提交,不能产生两条相同业务引用。

8.2 共享对象和跨业务解绑

使用合同、项目资料和报销单共同引用同一个文件。

先解绑合同,再解绑项目资料,最后解绑报销单。

每次解绑后都检查另外两处是否还能下载,直到最后一个引用消失才进入观察期。

如果三个业务模块使用不同的附件表,还要验证清理引擎能够查询全部引用来源。

不能因为某个模块没有注册引用类型,就把仍在使用的对象当成孤立文件。

8.3 版本替换和历史回看

上传正文 v1,建立项目引用,再上传正文 v2。

合同当前页面显示 v2,历史项目页面根据引用快照显示 v1。

撤回 v2 后,新的下载请求应遵循业务规则,已经归档的项目仍可按权限回看旧版本。

验证文件重命名只改变展示名称,不改变对象路径、摘要和历史链接。

验证同摘要文件去重后,两个业务引用仍然拥有独立的访问授权。

8.4 清理失败与权限收紧

让对象存储模拟删除失败,观察清理任务是否记录错误原因、重试次数和下次执行时间。

在重试期间收紧某个业务角色的下载权限,确认预览缓存、缩略图和分享链接同步失效。

管理员可以看到待处理任务,但普通员工不能通过清理队列反查文件内容。

物理删除审批通过后,系统应保留对象摘要、原路径和引用检查结果。

8.5 多租户隔离

租户 A 和租户 B 分别上传相同名称、相同摘要的文件。

验证对象可以在存储层复用,也必须在业务引用和授权层保持租户隔离。

通过猜测对象 ID、临时 URL 和预览地址,都不能读取另一个租户的内容。

导出审计日志时,也要按当前租户过滤业务 ID、文件名称和操作者信息。

租户管理员只能查看本租户的清理任务和失败记录。

8.6 结果记录要能被复核

每次验收都应保存文件 ID、业务类型、业务 ID、版本号和操作时间。

下载、预览、解绑、清理和重试分别记录结果。

如果动作失败,记录错误码和责任队列,不要只在浏览器控制台打印异常。

验收数据最好包含正常样本、边界样本和恶意样本。

正常样本验证主流程,边界样本验证大文件、重复上传和跨天生效,恶意样本验证越权下载、伪造路径和过期分享链接。

这样形成的记录可以直接转成回归测试输入。

后续改动附件服务、对象存储适配器或前端上传组件时,能够快速确认生命周期约束没有被破坏。

尤其要关注删除入口的变化。

它可能影响合同和报销模块。

也可能影响云盘历史版本。

每次变更都要重新验证引用关系。

文件生命周期设计还应考虑数据导出和客户迁移。

导出业务单据时,不能只导出附件表中的文件名,还要明确导出对象版本、摘要和业务引用。

导入到另一套环境时,目标系统要重新建立租户、业务和文件的映射,不能直接把旧环境路径拼接到新环境。

对于已经归档的合同、财务凭证和人事材料,建议由归档策略决定保留年限和可见角色。

技术清理任务只能处理满足策略的对象,不能绕过合规要求。

如果企业需要强化归档校验,可以保存不可变快照和校验摘要;它们本身不等同于对证据效力的判断。

当文件被复制到备份介质时,也要保留原对象 ID 和版本。

恢复演练要验证业务引用能否重新指向正确对象,而不只是确认磁盘上存在文件。

恢复后还要重新生成预览和搜索索引。

索引重建过程同样要执行权限过滤。

否则文件虽已恢复,派生内容仍可能暴露给不该看到的人。

因此,恢复验收应同时核对对象内容、版本、业务引用和访问权限。记录失效链接、无法恢复的版本与责任人,完成修复后再次验证;不能把"对象存储复制成功"直接当作业务恢复成功。

九、快速体验

在线演示地址为 https://ruoyioffice.com/web/,账号 admin / admin123。

可以从基础设施的文件配置、文件列表,以及 OA、合同、项目等业务页面查看附件使用方式。

云盘和 OnlyOffice 文章已经讨论在线编辑与版本历史,本文关注的是跨业务引用和删除责任。

源码阅读入口包括 AttachmentServiceImpl、FileServiceImpl、前端 attachment-list 和文件配置页面。

当前实现能力与统一清理引擎之间的距离,必须在实施时逐模块核对。

常见问题

删除附件会删除对象存储中的文件吗?

不一定。

删除附件记录和删除实际文件是不同 Service 入口,是否物理删除要看引用关系和清理策略。

文件下载接口为什么不能只校验文件 ID?

因为文件 ID 不代表业务权限。

下载还要校验租户、业务单据可见性、文件引用关系和当前状态。

多个业务单据共用一个文件可以吗?

可以,但必须明确引用关系、版本和删除责任。

解绑一个业务不应影响其他业务的正常访问。

文件版本和附件排序有什么区别?

排序只改变展示顺序,版本代表内容演进。

两者不能共用一个字段。

孤立文件应该立即删除吗?

不建议。

先进入回收状态,经过引用检查、保留期和必要审批后,再执行物理删除。

结语

企业文件管理的核心不是"把文件存起来",而是让文件在业务引用、访问权限、版本变化和删除动作之间保持可解释关系。

当系统区分引用、记录、对象和授权,上传下载只是生命周期中的两个普通节点,清理也不再靠人工猜测。

如果这篇对你有用,点个「在看」或收藏。

🌐 演示地址 :https://ruoyioffice.com/web

📦 GitHub 源码 :https://github.com/yuqing2026/ruoyi-office

📦 Gitee 源码 :https://gitee.com/yqzy1688/ruoyi-office

💬 微信:17156169080(获取产品咨询)

打开演示地址直接查看系统。

相关推荐
阿狗童鞋2 小时前
高并发系统设计实战指南
java·spring boot·spring cloud
十年Java程序媛16 小时前
Java 接口和抽象类对比|Java8 新特性,抛弃老旧八股,正确选型
java·spring boot·后端
for_ever_love__18 小时前
MySQL 高频面试题 30 问:索引、事务、锁、日志、主从与分库分表(MySQL 8.0 版)
java·数据库·spring boot·mysql·spring cloud·面试·总结
SL_staff18 小时前
设备越多系统越崩?根源在三层抽象失效
spring boot·物联网·github
威哥爱编程19 小时前
【AI全栈12-01】Spring Boot 为何是 AI 全栈后端优选
java·spring boot·后端
威哥爱编程19 小时前
【AI全栈12-03】Spring Boot 用多模型路由把智能客服成本降下来
java·spring boot·后端
威哥爱编程19 小时前
【AI全栈12-02】Spring Boot 跑通第一个 AI 对话接口:HR 政策问答机器人实战
java·spring boot·后端
m0_5873830019 小时前
深圳 24 小时自助健身房系统软件开发实战指南与案例解析
java·spring boot·小程序·架构·需求分析
企业数字化笔记21 小时前
AI工具参数很多怎么办?预设、表单校验、危险参数与配置审计
java·spring boot·python·音视频