ARTICLE · 1227422
企业文档权限设计:上传、分享、收藏、在线编辑,如何各有边界?
企业文档权限设计:上传、分享、收藏、在线编辑,如何各有边界?
一份部门模板上传到云盘,分享给同事,有人收藏,有人在线修改。看起来都是围绕一个文件点按钮,背后却是四种不同关系:谁拥有文件,谁获得访问资格,谁把它加入个人入口,以及哪个会话可以写回内容。
如果把这些关系都归到一个“可编辑”字段里,问题很快就会出现:收藏是否等于长期授权?可管理的人能不能继续转分享?分享被撤销以后,已经打开的文档会怎样?
RuoYi Office的OA企业云盘把文件信息、分享权限、收藏和版本分别管理。我们沿着文件进入平台、被分发、被使用,再到编辑回存这条链,讲清这些设计,也说明当前实现中仍需补齐的边界。

▲ 收藏关系不等于访问授权。分享配置、打开时的权限检查和编辑会话回存,是三个需要分别处理的时点。
先看产品:同一个文件,有三种不同的入口
云盘提供“我的文件”“共享文件”“我的收藏”。它们分别以所有者、分享关系和个人收藏关系组织文件,不应该各复制一份文档内容。

▲ 本地系统既有文件状态,保留顶栏、页签、OA菜单和完整业务区。“上传文件”“分享”“收藏”“在线编辑”服务于不同动作;截图中的历史数量与大小不是本轮上传测试结果。手机可点开原图查看。
例如,同一份劳动合同模板可以出现在所有者的文件列表中,也可以成为分享对象和收藏对象。一个人收藏它,应该只是多了一条到原文件的入口;文件当前版本仍由原来的业务记录承载。
因此,页面上的星标只说明收藏状态,不能说明文件永远可访问。在线编辑按钮也只能提供入口,服务端仍要判断当前用户是否有资格打开、是否允许编辑,以及文件类型和版本是否适用。
分享配置表达的是授权对象和操作范围
分享界面支持按人员或组织选择对象,并区分仅查看、可编辑和可管理。文件夹还有继承选项,需要明确子文件是否沿用授权。

▲ “企业通用劳动合同模板 (2).docx”的既有分享记录显示“宇擎源码/可管理”。本轮只打开查看,没有新增或移除授权;当前截图不是另一名接收者登录后的效果。
这三个级别有明确业务语义:仅查看在当前枚举中包含预览、下载;可编辑增加PC在线编辑;可管理再增加重命名、删除等管理动作。“仅查看”不是“只许在屏幕看、禁止下载”。 如果企业制度需要后者,应另外设计下载控制,不能把名称理解成已经提供了这一承诺。
能打开预览,不代表已经进入编辑会话
下面是同一份文档的真实只读预览。它展示文件内容已经能在系统中阅读,没有把一张静态文档图片当成在线编辑效果。

▲ 本轮实际打开的是系统文件预览弹窗,文档已加载;这不是OnlyOffice在线编辑画面,也没有进行输入、保存或版本回写。后面的编辑与回存规则依据对应源码展开。
这一步区分很有必要。预览组件、在线编辑器和文件存储服务是不同参与者。业务系统不能因为前端已经把文件显示出来,就推断后续所有写入都受到同样的控制。
上传先解决归属,分享再解决协作资格
上传入口先检查功能权限,再由文件服务完成大小、空间等检查,调用统一FileApi存储内容,登记文件信息和初始版本。文件记录包含目录parentId、所有者ownerId、部门、文件类型和地址等字段。
其中,文件存储与业务登记是两件事:存储服务保存字节,云盘记录负责说明这份内容属于谁、放在哪个目录、当前引用哪个版本。只得到一个存储地址,无法自动形成完整的文档协作规则。
当前上传实现将登录用户记为所有者,并使用传入parentId登记目录。但“有上传功能权限”和“能写入指定目录”仍是不同判断;不能只看到parentId存在就宣称目录级写入鉴权已经覆盖全部路径。文件/文件夹登记也有独立入口,其约束需要单独核对。
分享则要求当前用户是文件所有者。可管理的协作者能执行某些管理动作,并不因此自动获得再次分享资格:当前分享与取消分享服务都明确检查所有者,共享列表也将接收方canShare设为false。
将分享限制在所有者,有利于控制授权传播范围。如果业务需要协作者转授权,应为转授权另建规则和来源记录,而不是直接把“可管理”当成可以无限扩散文件的权限。
权限怎样求值:所有者、直接授权与目录继承
当前权限级别采用0、1、2递增表示查看、编辑、管理,无权限为-1。求值先判断文件是否存在,再判断所有者,然后查询本文件上对用户及其组织的授权;没有直接有效结果时,才向父目录寻找继承权限。
下面摘录真实求值逻辑,省略说明性日志和注释:
@Overridepublic Integer checkFilePermission(Long fileId, Long userId) {FileInfoDO fileInfo = fileInfoMapper.selectById(fileId);if (fileInfo == null) {return -1;}if (userId != null && userId.equals(fileInfo.getOwnerId())) {return FileSharePermissionEnum.MANAGE.getPermission();}List<Long> deptIds = systemService.getUserDeptIds(userId);Integer maxPermission = filePermissionMapper.selectMaxPermissionByFileIdAndUser(fileId, userId, deptIds);if (maxPermission != null && maxPermission >= 0) {return maxPermission;}return checkInheritedPermission(fileId, userId, deptIds);}
本文件上的人员授权与组织授权会取最大级别。组织范围使用用户部门及其上级部门集合;没有本文件有效授权时,继承逻辑再检查父目录的分享和继承标记,继续向上查找。
注意这里的先后关系:它不是把所有祖先与当前文件的授权汇总后统一取最大值,也没有单独的拒绝规则覆盖允许规则。直接授权与父级授权同时存在时,必须按实际求值顺序理解结果,不能套用另一种ACL模型的经验。
文件夹分享开启继承时,服务还会递归处理已有子项;取消分享时清理相应继承权限。对新建文件、目录移动、独立子项授权等情况,需要继续核对传播与运行时求值的一致性。共享列表展示了哪些条目,也不能替代具体文件打开时的校验。
收藏表只记录“我常用它”,不记录“它授权给我”

▲ 文件是共同对象。分享表保存授权对象与级别,收藏表保存用户和文件的关联;编辑版本另关联文件ID。图为逻辑关系,不宣称物理外键约束。
收藏服务检查文件存在、检查是否已收藏,再写用户与文件关系。它没有向分享表补授权,也没有把所有者改成收藏人。
当前收藏列表的实现值得单独看:按用户取收藏记录,再批量读取文件,最后标记星标。下面保留完整读取规则:
@Overridepublic List<FileInfoRespVO> getFavoriteFileList(Long userId) {List<FileFavoriteDO> favorites = fileFavoriteMapper.selectListByUserId(userId);if (CollUtil.isEmpty(favorites)) {return Collections.emptyList();}List<Long> fileIds = favorites.stream().map(FileFavoriteDO::getFileId).collect(Collectors.toList());List<FileInfoDO> files = fileInfoMapper.selectByIds(fileIds);List<FileInfoRespVO> result = BeanUtils.toBean(files, FileInfoRespVO.class);result.forEach(item -> item.setIsFavorite(true));return result;}
这段路径没有逐条重新过滤当前分享资格。因此,“收藏不产生授权”是数据模型事实;“撤销分享后,收藏列表一定立即隐藏失权文件”却不是这段实现已经提供的保证。
文件详情接口和OnlyOffice配置入口会再做预览权限检查,但不能由这两个入口推出所有列表、原始文件地址和下载路径都已形成统一访问网关。若要求严格失权,应同时检查列表元数据、文件读取和缓存链接,避免只修一个按钮。
在线编辑需要四个条件,历史版本另有只读规则
编辑配置由服务端计算,前端传editable=true表达的是请求,不是授权结论。当前服务将四个条件合并:不是历史版本、请求编辑、用户权限可编辑、文件扩展名受支持。
历史版本先校验归属文件,再强制只读。以下摘录来自编辑器配置构建方法:
boolean historyView = versionId != null;if (historyView) {var version = fileVersionService.getVersionDO(versionId);if (!fileId.equals(version.getFileId())) {throw ServiceExceptionUtil.exception(FILE_VERSION_NOT_EXISTS);}fileUrl = version.getFileUrl();fileName = StrUtil.blankToDefault(version.getFileName(), fileName);editable = false;}boolean canEdit = !historyView&& editable&& FileSharePermissionEnum.canOnlineEdit(permission)&& FileOnlineEditUtils.isEditableExtension(StrUtil.blankToDefault(fileInfo.getFileSuffix(), fileInfo.getFileName()));
文件本身还需存在、属于文件类型,且当前用户拥有预览资格。前端则用相同业务意图决定按钮和页面模式,服务端保留最终判断。文件ID、版本ID和用户资格缺一不可,不能仅凭猜到一个版本编号就打开任意历史内容。
配置层也必须拆开读:当前permissions.edit采用canEdit,但download与print均设为true。因此不能把“只读模式”宣传成防下载、防打印,更不能把它等同防截屏、防复制的完整防泄漏方案。
为了让规则可复用,我们把几个设计决策分别落在对应层:
这比在每个页面手写不同的“管理员可操作”条件更便于对齐行为,但前提是各接口确实接入这些规则,而不是只有前端引用了共同枚举。
回存是另一道门:验证会话,还要考虑授权是否改变

▲ 打开配置与内容回存发生在不同时点。当前回存使用签发时的editable标记,不代表分享撤销后已有会话立即失效。
OnlyOffice由文档服务拉取文件,编辑后回调业务后端。当前实现为下载和回调分别签发带过期时间的令牌,回调令牌包含文件身份、可编辑标记、用户和租户等信息。
回存只处理状态2和6;令牌只读则拒绝写回。下面保留状态、只读标记和结果地址判断,后续业务身份校验、下载编辑结果和存储处理省略:
public void handleCallback(String token, Map<String, Object> body) {Map<String, Object> payload = verifyToken(token);Integer status = MapUtil.getInt(body, "status");if (status == null || (status != 2 && status != 6)) {return;}Boolean editable = MapUtil.getBool(payload, "editable");if (!Boolean.TRUE.equals(editable)) {// 原实现记录只读会话拒绝回存日志return;}String editedUrl = MapUtil.getStr(body, "url");if (StrUtil.isBlank(editedUrl)) {throw new IllegalArgumentException("回调缺少文件地址");}// 业务类型、文件ID校验及下载、存储、回写步骤见下文}
随后,服务校验业务类型和文件ID,保存编辑结果,恢复租户上下文,再更新云盘当前地址并登记版本。更新前会用旧地址补充必要的基线版本,避免先替换引用再丢失旧内容。
这里有一个容易误判的时效边界:当前回调主要依据打开时签发的editable标记,没有在此处再次按当前分享关系检查编辑资格;下载令牌也基于签名、有效期和内部文件地址取内容。令牌有效期机制不等同实时撤权。
所以,取消分享会删除对应分享记录,但不能直接承诺已打开的编辑会话立即退出、旧令牌立即失效。若制度要求实时撤权,需要增加会话撤销或权限版本机制,并覆盖配置、下载、回存等路径。这个扩展应明确处理同一文件多人协作,不能只关闭当前浏览器按钮。
一套规则要能解释动作,也要说明不负责什么
对企业管理一体化平台而言,文件权限最终要回答的是具体动作:谁能把文件放进哪个目录,谁能向哪个对象分享,谁能取内容,谁能提交新版本。角色菜单权限、文件对象授权与编辑会话权限协作,才能把这些动作接起来。
当前云盘已有所有者约束、分级分享、组织范围、目录继承、独立收藏和编辑配置判断;同时,收藏失权过滤、目录写入资格、既有会话撤权和统一内容读取仍应逐条核验。不能因为某一层有权限检查,就为整条链作无边界保证。
把这些职责分开之后,上传、分享、收藏、在线编辑就不再是一串彼此借权限的按钮。文件归属有来源,协作资格有依据,常用入口不扩大授权,内容写回也有自己的身份与时效规则,后续扩展才容易保持一致。















































































