EOS低代码平台附件删除权限控制:只能删除本人上传的文件 上周项目上线前业务组长拿着一张测试截图来找我“你们这个附件列表怎么回事我传上去的附件隔壁工位的小王能直接删掉小王传的资料我也能删。合着这个删除按钮就是个摆设”我一看就明白了页面用的还是最初那套“谁登录都能删附件”的删除操作而业务上附件已经支持多选上传同一个业务单据下挂了好几个人传的文件。在EOS 8.3.3这类低代码平台上“只能删除自己上传的附件”这个需求一句话就能说清真要落地却得把数据表、后端校验、前端交互三块一起改少一块都要出问题。这篇文章就把我在项目里这一轮改造的完整思路和踩坑记录整理出来给同样在用EOS做附件管理的朋友做个参照。1. 先把这个需求翻译成人话谁能删、不能删、为什么不能只做前端“附件允许多选附件不是同一个人上传的需要判断只能删除自己上传的。”这句需求原文看起来只讲了一个删除动作但在落到代码之前得先把业务约束掰开揉碎看清楚。需求里没有写明的东西往往比写明的东西更容易让实现跑偏。1.1 业务场景背后的三个隐藏前提第一个隐含前提是系统里的用户看到的附件列表是同一个共享列表。也就是说列表数据不能按上传人过滤显示否则需求就不是“判断能否删除”而是“每个人只能看到自己的附件”了。你们要做的是共享列表上的细粒度操作权限每一行记录都独立判断当前登录人能不能删除这一行。第二个隐含前提是附件表里必须能拿到“上传人”这个信息。很多EOS项目在一开始做附件上传时只往表里写了文件名、路径、大小、上传时间唯独没有写上传人ID等到要做删除权限控制时才发现没有落地的依据。这个坑我在后面专门讲先记住一句话没有uploader_id后面所有判断都是空中楼阁。第三个隐含前提是这个删除权限约束的是普通业务人员管理员的权限要另外说。我见过不少项目经理在这个问题上拍脑袋决定结果上线以后遇到“传错了附件需要清掉”的情况发现普通用户删不掉、管理员也不能删只能去数据库手工操作。正常的做法是先跟业务确认管理员能不能删所有人的附件流程节点上的自动上传附件算谁的这两个问题确认完代码里的分支逻辑才写得下去。1.2 权限控制要同时管住界面层和服务层我见过不少低代码平台上的实现只在页面把删除按钮藏掉就宣布“做了控制”。这在EOS上尤其危险。平台的实体删除操作构件是可以被请求直接调用的前端藏了按钮无非是让普通用户看不看得到知道接口的人直接用工具发一个删除请求照样能删掉别人的附件。所以这个需求必须拆成两层来做界面层让人看不到、点不动自己没有权限删除的附件解决的是操作体验问题。服务层即使绕过界面直接调接口后端在删除之前也会校验一次上传人身份解决的是安全问题。这两层缺了哪一层都不合格。后端是底线前端是体验优先级上后端永远排在前面。很多团队把精力全花在“按钮藏得够不够隐蔽”上却没在后端写一行归属校验这就是本末倒置。2. 摸清EOS 8.3.3里附件落库的底细表结构、实体与上传人字段2.1 附件表怎么建才够支撑“按人删除”这次改造里沿用了一个很常规的附件表结构核心就是给每条附件记录了归属人CREATE TABLE t_attachment ( id VARCHAR(32) NOT NULL COMMENT 附件主键, biz_id VARCHAR(64) NOT NULL COMMENT 业务单据ID如合同ID、工单ID, biz_type VARCHAR(32) DEFAULT COMMON COMMENT 业务类型多业务共用时用于区分, file_name VARCHAR(255) NOT NULL COMMENT 原始文件名, file_path VARCHAR(500) NOT NULL COMMENT 存储路径, file_size BIGINT DEFAULT 0 COMMENT 文件大小(字节), uploader_id VARCHAR(32) NOT NULL COMMENT 上传人ID, uploader_name VARCHAR(64) DEFAULT NULL COMMENT 上传人姓名冗余展示用, upload_time DATETIME DEFAULT NULL COMMENT 上传时间, PRIMARY KEY (id) );在EOS 8.3.3的建模工具里按这张表生成实体和DAO构件以后系统会自动提供按主键查询、按条件查询、插入、删除这些基础操作。我建议再显式加一个“按biz_id查询附件列表”的查询构件返回带上uploader_id、uploader_name、file_name这些字段后面前端判断删除权限就直接用返回结果不需要前端再发额外请求。这里有个设计细节值得展开说为什么uploader_name要冗余存因为列表上要展示“这条附件是谁传的”如果每次都要去join用户表在EOS的数据集构件编排里会多一层关联查询逻辑复杂维护成本也上去了。存一个冗余的姓名展示直接用权限判断只认uploader_id姓名仅仅是给人看的两者职责分开。2.2 在EOS构件里获取当前操作者的几种姿势后端校验的前提是能拿到“当前登录用户是谁”。EOS 8.x不同项目、不同组织权限模块的封装方式差别挺大常见有这么几种使用平台组织构件提供的用户上下文对象例如UserObject、Operator这类登录时由平台注入到上下文。使用项目自己在登录拦截器里塞进Session的用户对象。对接统一门户时从请求头或令牌解析用户编号。不管用哪种我都建议在公共构件里封装一个统一方法不要在每一个删除操作里各写各的public String getCurrentOperatorId() { // 以从会话中获取当前登录用户为例API请替换成你们项目实际的上下文封装 HttpServletRequest request getCurrentRequest(); // EOS请求上下文获取方式按平台实际调整 HttpSession session request.getSession(); Object currentUser session.getAttribute(CURRENT_USER); if (currentUser null) { throw new BusinessException(登录已过期请重新登录后操作); } return ((SysUser) currentUser).getUserId(); }你可能会问为什么非得抽公共方法因为一个项目里要做权限控制的绝对不止附件删除一处后面评论删除、变更记录删除、文件覆盖操作都会用到类似逻辑。统一封装以后后续要是从Session改成令牌解析只需要改一个构件不用每个操作都跟着改。这是低代码平台开发里容易被低估的一件事公共构件的复用价值往往在第二个、第三个调用方出现时才体现出来。3. 后端删除接口校验放在循环里还是循环外后端是这条规则的底线。先讲单条删除再讲多选批量删除因为两者在提交参数和校验顺序上并不完全一样踩过的坑也不一样。3.1 单文件删除的三重校验单文件删除时前端传过来的是附件ID后端做了三重校验public void deleteAttachment(String attachmentId) { // 第一重参数非空 if (attachmentId null || attachmentId.trim().isEmpty()) { throw new BusinessException(附件ID不能为空); } // 第二重附件必须存在 AttachmentFile attach attachmentDao.queryByPK(attachmentId); if (attach null) { throw new BusinessException(附件不存在或已被删除); } // 第三重归属权校验 String currentUserId getCurrentOperatorId(); if (!currentUserId.equals(attach.getUploaderId())) { throw new BusinessException(只能删除本人上传的附件 attach.getFileName()); } attachmentDao.deleteByPK(attachmentId); }第二重很多人会省掉觉得“反正按ID删查不到就直接删没影响”。省掉的坏处是返回信息不可控文件不存在、文件路径已变、记录被逻辑删除这些情况下用户拿到的是笼统的“删除失败”或者数据层抛出的底层异常排查问题要多绕一圈。把这个校验显式写出来提示友好得多排查也快得多。归属权校验里有个容易被新手忽略的细节比较的一定是用户ID不要拿uploader_name和当前用户姓名比较。姓名重名在每个企业系统里都真实存在一旦用姓名做判断你会遇到“两个人同名我到底能不能删他的附件”这种解释不清楚的线上问题。ID才是唯一标识姓名只是给人看的。3.2 多选批量删除的全量预检多选上传之后页面允许勾选多条附件批量删除这时候前端传过来的是一个ID数组。批量删除如果写成“循环里面逐条判断、遇到没权限的就抛异常”会制造一个很尴尬的中间状态前面几条已经删除成功了后面几条因为没权限报错用户一刷新发现删了一半再点一次又报“附件不存在”。我这里的做法是先全量预检、再做删除public void deleteAttachments(String[] attachmentIds) { if (attachmentIds null || attachmentIds.length 0) { return; } String currentUserId getCurrentOperatorId(); ListAttachmentFile list attachmentDao.queryByIds(Arrays.asList(attachmentIds)); ListString forbiddenNames new ArrayList(); for (AttachmentFile attach : list) { if (!currentUserId.equals(attach.getUploaderId())) { forbiddenNames.add(attach.getFileName()); } } if (!forbiddenNames.isEmpty()) { throw new BusinessException(以下附件非本人上传不能删除 String.join(、, forbiddenNames)); } attachmentDao.deleteByIds(Arrays.asList(attachmentIds)); }先把所有待删除附件查出来逐一比对上传人把所有没权限的文件名收集起来。只要有一个没权限整个删除请求就拒绝执行用户得到的是一个完整明确的提示“这几个文件不是您上传的所以整批都没有删除。”而不是删到一半停下来。至于到底是要“全部有权限才批量删”还是“跳过没权限的、只删有权限的”建议开工前找产品确认清楚。两种方案在代码上只差一个分支但业务预期差别很大。我在项目里最终选了全量预检理由很简单避免中间状态。附件删除这种操作一旦出现“删了一半”的情况用户不知道剩下的文件哪些是故意留下的哪些是操作出错了。3.3 一条带归属条件的SQL兜底即使Java层做了归属校验我还是建议在删除SQL上再加一道保险把uploader_id写进删除条件DELETE FROM t_attachment WHERE id #{attachmentId} AND uploader_id #{currentUserId}当这条删除语句返回的受影响行数是0时说明要么附件不存在要么不是本人上传无论哪种都不算“删除成功”。这个做法等于把权限判断下沉到了数据层即使未来某个逻辑构件漏写了Java校验或者有同事图省事直接调用了平台的通用删除构件也删不掉别人的附件。这不算过度设计。附件删除一旦出错恢复成本很高文件没了就是没了不是改一行配置就能回来的。Java层校验是业务规则SQL里的归属条件是最后一道物理防线多一层兜底多一分安心。4. 前端多选列表的删除体验隐藏、置灰还是点击再拦后端校验做得再严前端交互如果做得糙用户还是会觉得功能“不对”。同一个列表里你自己的附件和别人的附件区别必须一眼能看出来不能让人每次都点了删除才知道“哦这个不是我的”。4.1 列表模型里必须多给一个字段uploaderId前端在渲染附件列表时列表数据模型除了fileName、fileSize、uploadTime这些展示字段一定还要把uploaderId原样返回。拿到列表数据后和当前登录人ID逐条比较const currentUserId getCurrentUserId(); // 从登录态中获取 const attachmentList res.data.map(item { return { ...item, canDelete: item.uploaderId currentUserId }; });这里用严格相等避免ID在类型转换上出现误判。canDelete这个布尔值就是后面所有删除按钮控制的核心依据。记住处理undefined和null的情况——历史数据如果uploader_id是空的要按“不可删除”处理不能因为空值就放行。空值放行的后果比空值禁删严重得多。4.2 三种交互方案的对比与选择同一个“非本人上传的附件”前端可以做成隐藏、置灰、点击拦截三种表现我在这段时间里把三种都试过各有各的适用场景交互方案实现方式体验优点潜在问题适合场景隐藏无权限时不渲染删除按钮界面干净不会误点用户可能疑惑“别人的文件怎么没有删除键”权限边界严格、不允许试探的场景置灰删除按钮disabled悬停提示原因规则透明可读性好需要额外写disabled样式和提示文案大多数业务系统推荐点击拦截按钮正常显示点击后才提示无权限实现最简单用户容易反复误点体验差快速上线、后续再优化的过渡方案我最终选了置灰方案删除按钮在无权限时是disabled状态鼠标悬停或旁边用文字提示“该附件由XX上传仅上传人可删除”。这样既避免了隐藏方案带来的“为什么我能看到同一个列表里一个能删一个不能删”的疑问也不至于让用户反复点一个注定点不动的按钮。做这行久了你会发现好的权限交互不是让用户猜规则而是把规则直接摆在他面前。4.3 复选框与批量删除按钮的处理这里有一个容易被忽略的细节如果页面的删除操作是“勾选多条附件 → 点批量删除”那无权限附件的复选框本身也要跟着禁用。否则用户勾了三行其中一行是他人的提交后后端返回“这3个附件里有1个不能删”用户会觉得莫名其妙还会怀疑是系统出bug了。处理办法很简单列表渲染时canDelete为false的那一行复选框直接disabled批量删除按钮的可用状态用“已勾选数量 0”控制。这样用户在勾选阶段就已经规避掉了无权限对象后端校验只是最后一道防线两层各司其职用户的操作路径也最短。5. 实际部署中躲不开的坑上传人丢了、权限边界模糊、删除后引用残留把主流程跑通不算完下面是这一段时间里我实际遇到、且很容易在测试环境被忽略的边界情况。这些坑不是代码逻辑写错而是业务场景天然存在的灰色地带。5.1 上传人信息在流程代理、历史导入时最容易丢第一种情况是附件不是用户手动传的而是流程节点自动带出的。比如审批流程里某个节点调用了“上传附件”构件但处理人是被代理的、被转办的这时候当前登录人ID和附件真正的所有者可能对不上。如果业务要求“谁在这个节点处理过、附件就算谁上传的”那就得在创建附件时记录流程处理人而不是当前登录人——这一点要跟流程构件的上下文确认清楚。我当时就在代理场景下踩了一次单据发起人A把审批委托给BB上传了补充材料附件表记的却是A结果A能删B传的文件业务上完全说不通。第二种情况是历史数据迁移。系统上线初期建的附件表往往没有uploader_id字段或者字段值全是空的。我的处理方式是在迁移脚本里把历史附件的上传人统一刷成单据创建人对于实在无法判断归属的附件标记为“系统导入”并且设置为不可删除。与其让用户看到一堆谁都删不掉的附件也不该出现一个“疑似能删别人文件”的误判机会。宁可不删不能错删。5.2 批量删除“部分有权限部分没有”的收场方式前面提到批量删除建议全量预检但业务侧可能会提另一个诉求“我就想把我的那几份删掉别人传的我不管。”这种诉求在业务上也成立做法就变成前端在提交前自动过滤出canDelete为true的附件ID数组后端仍然保留全量校验逻辑。注意提示文案要写清楚“共勾选5个附件其中2个非本人上传已自动跳过。”我个人建议在小范围内先跟业务对齐口径到底允不允许“部分删除”。这个决定会直接影响前端是“勾选只能选自己的”还是“先全选再过滤”也会影响提示文案的写法。如果产品没有明确态度默认选择全量预检因为它在权限语义上更严格不容易被钻空子也更方便后续追溯操作记录。5.3 删除附件别忘同步业务表引用和物理文件附件表删除只是删除了元数据如果业务表里还存了附件ID列表比如合同表有一个attachment_id_list字段删除后没有同步清理详情页再打开时会出现“拿着ID查不到附件”的空引用轻则列表少一张图重则页面直接报错。我的建议是附件删除接口不要只调用附件表的delete还要在同一个事务里同步清理业务表里的引用字段。如果物理文件存放在远程对象存储或本机目录元数据删除之后还要决定是否立刻物理删除文件。项目里我选用了逻辑删除在附件表加一个status字段先标记为删除状态定时任务在每天凌晨清理超过30天的标记文件。这样既可以随时找回误删的附件也不用担心“元数据删除和物理文件清理不在同一事务里”带来的不一致问题。再补一个容易被忽略的点删除操作最好写审计日志。谁、什么时候、删了哪个文件、这个文件原本属于谁这些信息在出现问题需要回查的时候特别有用。EOS里做这个很简单只要在删除构件里插一个写日志的操作节点就行但很多项目一开始根本没想到这一层等到要追责的时候才发现无据可查。6. 一点个人体会这套权限控制不该只写完接口就收工这次改造下来我个人最深的体会是低代码平台上的权限控制最容易翻车的不是技术上实现不了而是大家默认“平台生成的操作构件已经帮我处理了”。实际上平台生成的删除构件只认主键它不知道也不该知道你的业务规则规则必须由你自己显式写出来。生成构件是通用工具业务规则是专业判断这两件事在EOS里从来没被合并过。6.1 把规则做成可配置而不是写死在构件里现阶段需求是“只能删除自己上传的附件”但业务规则很少会一成不变。我遇到过后续版本提了一个新需求部门负责人可以删除本部门成员上传的附件。如果当初把“uploader_id等于当前用户ID”这个判断硬编码在删除构件里改起来就要动逻辑构件重新发布风险不小。这次我做了个简单的规则配置在系统参数表里维护一个可删除范围字典删除附件时不再直接比对uploader_id而是先查当前用户在指定业务类型下的删除范围——是“仅本人”、“本部门”还是“全部”。默认值仍然是“仅本人”这样原有约束一点不变后续扩展只改配置不改代码。低代码平台的优势就是变更快别让硬编码把这条路堵死。6.2 顺手给列表加个“归属人”展示列最后分享一个操作层面的小技巧给附件列表加一列“上传人”把uploader_name展示出来。用户对附件删除权限的抱怨很多时候不是针对权限本身而是“我不知道这个附件是谁传的为什么删不掉”。当归属人清晰可见时“我只能删自己传的”这件事就变得特别自然几乎没有用户再来问为什么。如果项目允许再给本人上传的附件在文件名旁边加一个浅色标记一眼能认出“哪些是我的”比任何提示文案都管用。如果你们项目里的附件表还没有uploader_id第一步不是写删除校验而是先补齐上传人数据的落库逻辑——这就好比修水管之前先确认水管里真的有水。规则可以写得好看数据基础不牢一切都是空中楼阁。至于那条带上传人条件的DELETE兜底SQL虽然看起来是多写了一个条件但它保证的是即便将来有人把代码改乱了数据也不会因为误操作丢在别人手里。