跑腿系统权限控制模型与角色管理架构设计实战

发布时间:2026/9/28 13:23:48
跑腿系统权限控制模型与角色管理架构设计实战 权限这关如果做不好跑腿系统后面的每个功能迭代都会变成一场灾难。我自己在早期做同城配送类项目时一开始只是简单地给前端按钮做了个控制结果订单状态有人乱改、骑手能看到不属于自己的订单、售后审核直接被普通客服放行运营团队每天都在群里喊“又出权限漏洞了”。后来整个权限控制模型重新设计把所有角色、资源、数据范围摆到桌面上梳理了一遍才算真正把系统根基稳住。这篇文章就把我在跑腿系统开发中落地的权限控制模型与角色管理架构设计思路整理出来从角色拆分、RBAC模型落地、数据权限控制到前后端实现细节尽量说得直白一些希望能给正在做类似平台的朋友一些可参考的实操经验。1. 权限诉求拆解跑腿系统到底需要多少种角色很多团队做跑腿系统第一反应是“我有用户端、骑手端、管理后台”然后按端去控制每个端里的角色又糊在一起。这样做的结果就是管理后台里一个运营账号能改价格、能审核骑手、能退款、能看财务报表出事的概率极大。所以第一步要做的事情不是写代码而是把业务角色和对应权限诉求一层层拆清楚。1.1 跑腿系统的角色全景图以我接触过的同城跑腿/即时配送系统为例一个完整闭环跑下来涉及的核心角色至少有这五类平台用户发起跑腿订单的人。他需要的是下单、支付、取消订单、查看订单轨迹、发起售后、评价、管理常用地址和自己的钱包/优惠券。用户端权限相对简单基本是“自己的数据自己管”不能看别人的订单列表不能帮别人发起退款。骑手接单和配送的执行者。骑手端和用户端完全不同他需要有“抢单/派单”、“接单/拒单”、上报取件/送达、查看配送任务列表、查看个人收入明细、申诉处罚记录等功能。这里最容易出问题的点就是数据范围骑手只能看到他分配或抢到的订单不能浏览全城所有订单。商家/门店如果跑腿系统带代买、帮送、团购自提这类业务商家后台会单独存在。商家需要接收订单通知、处理接单/拒单、管理商品上下架如果有商城模块、查看门店收入结算。一个品牌商可能下挂多个门店就需要有商家总账号和门店子账号的层级。平台运营/客服我习惯把这类角色再拆细一点哪怕是同一个后台系统运营人员和客服的功能诉求差很多。客服主要是处理售后工单、退款审核、用户申诉、骑手申诉运营主要看数据报表、发布活动、管理 banner、调整价格策略。超级管理员负责系统配置、角色管理、菜单管理、数据字典、平台全局参数、审核所有类型的变更操作。这个角色的账号必须严格隔离不能拿日常运营账号和超管账号混用。这五类角色是主干但落到一个具体项目里还会衍生出很多分支。比如平台可以按区域分多个城市站每个城市站有自己的城市运营和城市客服审核骑手入驻的审核员和审核商家资质的审核员可能也是不同角色。我建议在设计初期先把这些角色一个个列出来列成一张表格再来决定彼此的权限边界和继承关系。1.2 为什么不能只用“普通用户和管理员”两板斧我见过不少早期项目只设计两套角色普通用户和管理员后来所有后台人员都共用一套管理员权限。表面上开发效率上去了砍单、退款、改价、查账都能干实际上埋了很大的雷。举个例子一个客服接了个用户来电说是骑手超时导致订单取消扣款不合理要求全额退款。权限体系如果允许客服直接发起退款并审核通过这个流程就完全没有复核机制了。能不能退款、退多少、需不需要上级审核这是业务规则层面的事情而权限控制模型要做的是让“具有退款操作权限的人”和“具有退款审核权限的人”分开。如果大家只有一套管理员权限你的审核流根本无从落地。另一方面权限粒度过粗直接导致的风险是越权操作追责难。某天财务发现一笔异常补贴查看操作日志发现是某个运营干的。但你无法判断这是有意操作还是误操作因为这套权限压根没有把“财务补贴发放”和“运营活动配置”拆开。所以说权限设计不是给开发增加工作量而是给业务上保险。凡是涉及钱、涉及服务质量判定、涉及用户敏感数据的操作都应该单独拎出来做权限点控制。宁可配额设计细一些也不要图省事全部揉在一起。1.3 从用户故事反推功能清单我在做权限设计时习惯用“用户故事”去反推功能清单。举个例子“作为城市运营我希望能查看本城市的订单实时大屏但不能看到其他城市的订单数据。”这个故事里包含了两层信息。功能层面需要有“订单实时大屏”这个菜单页面数据层面隐含了“限定城市编码”的条件。这两层信息最后会分别落到数据库菜单表和数据权限规则里。把用户故事收集完之后统一提炼成功能点。每个功能点再标注“谁能访问”逐步形成权限矩阵。这一步如果做扎实了后续实现角色表、菜单表、权限表的字段设计就会非常快。我当时大概收集了一百多个用户故事后来整理成六十多个菜单权限点和二十多个数据权限点整个系统的权限边界就非常清晰了。2. 角色管理架构设计把RBAC模型跑通前面把权限诉求梳理清楚之后就要进入架构层面的设计了。目前业界通用的做法还是RBAC模型。网上关于RBAC模型的概念已经讲得很多了我这里更想聊一聊在跑腿系统这个具体场景里RBAC模型到底怎么落地才不别扭。2.1 用户、角色、权限、资源四层关系RBAC的核心是引入“角色”这层中介让用户与权限解耦。用户不直接绑定一堆具体权限点而是给用户赋予角色角色再关联权限点集合。这样做最大的好处是管理效率高。假设你有一万个骑手如果每个骑手单独配置权限那是不现实的。你只需要建一个“骑手”角色把该角色挂上固定的权限点再让一万个骑手都关联“骑手”角色即可。后续业务调整只需要改角色-权限关系所有骑手的权限就同步更新了。在跑腿系统里我通常把实体拆成这么几个用户表保存账号基础信息比如手机号、密码加密存储、昵称、头像、状态等不直接存储角色信息而是通过中间表关联角色。角色表如“超级管理员”、“城市运营”、“客服专员”、“门店店长”、“骑手”、“注册用户”。角色表里会有角色编码、角色名称、数据范围标识等字段。权限/资源表保存系统里所有受控的资源点包括菜单、页面、按钮、接口URL等。每个资源点唯一编码例如order:refund:audit表示订单退款审核。中间表用户-角色关联表和角色-权限关联表。这里有一个容易被忽略的点用户和角色之间、角色和权限之间都应该是多对多的关系。不能为了“省事”把用户和角色做成一对多。因为实际业务中一个运营可能同时是城市运营又兼任客服主管一个门店店长可能同时是骑手调度员。这种多重身份是常态设计上必须支持多选角色。2.2 角色继承与优先级避免权限混乱的关键当你角色数量越来越多之后会出现一些相似角色比如“客服专员”和“高级客服”。“客服专员”可以接待工单、处理用户咨询“高级客服”在客服基础上多了一个“退款审核”权限。如果为这两个角色各建一套权限点后期维护会非常痛苦。你改了“客服专员”的权限还得记得同步改“高级客服”的权限。解决办法就是引入角色继承。高级客服继承客服专员的一切权限再额外增加自己的扩展权限。在数据模型上可以增加一个字段比如parent_role_id标注角色的父级角色或者在角色表里设计inherit_role_code。每次给子角色计算权限时把父角色的权限一起合并进来。但这里有一个坑我必须提醒你角色继承会导致权限判断变得隐式化出了问题比较难排查。所以使用继承的时候一定要遵循简单原则尽量不要超过两层。我在项目里做的更彻底一点大部分角色都是平级独立配权只有少数像“高级客服”、“高级运营”这种明确包含关系才用继承。越往后你会发现角色体系最大的敌人不是权限不够而是角色关系太复杂。角色优先级则用在“多角色用户”的场景。比如某用户既是骑手又是商家很多个体商户自己接单配送他登录后到底进入哪个工作台我的处理方案是在用户-角色中间表里加一个is_default字段登录后默认进入优先级最高的角色工作台用户也可以自己切换角色身份。这个体验细节一定要在架构设计时预留好否则后面做多身份切换会非常难受。2.3 权限点编码规范与菜单路由设计权限点编码是权限系统里面的“语言体系”编码规范直接影响代码可维护性。我在跑腿系统项目里统一采用的是三段式编码模块:子模块:动作。模块通常是业务域比如order订单、rider骑手、user用户、finance财务、system系统管理。子模块是业务域下的功能模块比如order.refund订单退款。动作是具体操作比如view、create、update、audit、delete、export。组合起来就是order:refund:audit、order:refund:view、rider:settlement:export这样的权限码。这样设计有什么好处第一权限码本身可读性高开发人员看到权限码就知道这个权限是干什么的不需要反复查文档。第二支持模糊控制前端菜单权限、后端接口权限、数据权限范围都能用同一个权限码体系去贯穿。第三方便做权限码复用比如导出操作在很多模块都有但是导出订单和导出骑手收入数据是完全不同的权限点编码上可以自然区分开。菜单路由设计上我一般是用权限码去控制菜单可见性。菜单表结构包括菜单名称、路由地址、组件路径、权限编码、父级ID、排序号、是否显示等字段。后端返回菜单列表的时候会按照当前用户的权限码过滤一次前端再根据返回的菜单列表动态注册路由。这里有一个细节即使某个用户手动在地址栏输入了没有权限的路由地址前端也要有路由守卫去拦截后端接口同样要再次校验。前端控制只是体验层面的隐藏真正的安全边界在后端接口权限。后面我会专门讲这个。3. 权限控制模型落地从功能权限到数据权限角色管理解决的是“谁能做什么”的问题但跑腿系统真正复杂的地方在于“谁能看哪些数据”。这就是我们常说的数据权限。比如同样一个运营角色A城市的运营只能看到A城市的订单B城市的运营只能看到B城市的订单功能权限完全一样数据权限却不一样。3.1 功能权限、数据权限、接口权限的三层划分我会把系统的权限控制拆成三个层次来做。第一层是功能权限也叫菜单/按钮权限。它控制的是用户能看到什么页面、操作哪些按钮。比如“退款审核”这个按钮有的角色能看到有的角色看不到。功能权限通常在菜单表、按钮表中维护前端根据权限码渲染界面。第二层是数据权限它控制的是用户能访问哪些数据行。划分维度一般包括所有数据、本人数据、本部门/本区域数据、指定数据范围等。跑腿系统里最常见的数据权限维度是城市、门店和用户本人。第三层是接口权限它控制的是后端接口允许谁调用。接口权限主要写在后端每次请求都要做一次权限校验防止有人绕过前端直接调用接口。这三层权限层层递进。前端把按钮隐藏了但如果后端接口没校验用户拿工具直接调接口还是可以操作。功能权限隐藏了入口数据权限限制了范围接口权限做了最终拦截。三层都做到位权限体系才真正可靠。我见过很多项目只做了功能权限后端接口“裸奔”。管理员还好普通角色的用户直接调用接口就能看到本不应该看到的数据这个漏洞非常致命。所以接口权限这一层一定不能省。3.2 数据权限的实现方案通用数据规则过滤器数据权限在跑腿系统里的实现方式有很多种。我比较推荐的方式是“数据规则”机制。就是在创建角色的时候除了分配功能权限还会配置一条数据规则用来声明该角色“可以看到哪些单位的数据”。数据规则的字段一般是这样的数据范围类型1-本人数据2-本部门数据3-本区域数据4-全部数据5-自定义数据范围。自定义范围比如指定某些城市编码、某些门店编码。后端在做查询的时候接口层面先解析当前用户的数据范围类型再动态拼接SQL条件。举个例子用户A的角色是“上海城市运营”数据范围类型是“本区域数据”区域编码310100。那他在调用订单列表接口的时候后端会在查询SQL里自动追加AND city_code 310100。通用数据规则过滤器可以在框架层面做统一处理。我通常用一个DataScope注解标注在某个mapper的方法上注解里带上表别名、部门字段名、用户字段名等元数据。过滤器的实现逻辑是进入Mapper之前先从上下文获取当前用户的数据权限信息然后拼装进SQL执行时统一追加的条件中。这样做有一个前提所有业务表必须规范冗余city_code、store_id、create_by这类“归属字段”。如果不存这些字段后面想要按城市、按门店过滤数据就无从谈起。所以建表的时候就要把这些字段考虑进去不要等到做数据权限时再后悔。3.3 接口层面的鉴权设计与登录态接口鉴权我采用的方案是“登录态 权限码”双重校验。登录态指的是用户登录后签发一个token后续每次请求都带上token后端解析出当前用户是谁。在跑腿系统里token的时效性要控制好一般短TokenRefreshToken的机制更合理。骑手端经常长时间在线但是不能因此让token永久有效否则账号泄露的后果很严重。权限码校验则是在登录态基础上判断当前用户是否拥有某个操作权限。我用的方式是注解式在Controller的方法上写PreAuthorize(ss.hasPerm(order:refund:audit))这类注解后端在进入方法前先检查权限码。这里有一个实际体验值得分享权限码校验放在Controller层还是Service层取决于你的系统规模。如果是几十个接口的小系统集中在Controller层用注解很清晰。但如果你有几百个接口并且有一些Service内部调用的场景建议在Service层做关键节点校验避免内部调用绕过Controller直接执行Service方法时跳过权限。接口层面还要处理一个细节角色升级/权限变更以后token里的权限信息必须实时生效。我一开始的设计是在token里直接附带权限码列表后来发现权限变更后要强制用户重新登录体验很差。改成每次请求从数据库重新加载权限之后实时性解决了但性能又成为瓶颈。最后用了缓存方案权限变更时主动删除缓存下一次请求重新加载。这个方案的实时性和性能都满足要求具体实施我会在下一节展开。4. 实操过程完整跑通一条权限链路前面讲了设计层面的思路这一部分我来说说实际代码过程中的关键环节和做法我会从数据库表结构、后端校验、前端动态路由三个方面讲给出一些可以直接参考的落地细节。4.1 数据库表结构与关键字段说明跑腿系统权限模块我会设计成至少这几张表系统用户表字段包括用户ID、登录账号、密码哈希、昵称、手机号、状态、角色ID冗余字段用于查询优化等。注意密码一定要用BCrypt这类带盐的哈希算法存储不要用MD5直接存。角色表角色ID、角色编码、角色名称、数据权限类型、数据权限范围值、父级角色ID、状态、排序、备注。核心就是数据权限类型和范围值这决定了该角色默认能看到哪些范围的数据。菜单权限表菜单ID、父级ID、菜单名称、路由地址、组件路径、权限编码、菜单类型目录/菜单/按钮、是否外链、图标、排序、是否显示。菜单和按钮资源都存这一张表用菜单类型区分。用户角色关联表用户ID、角色ID、是否默认角色、创建时间。这里要支持一个用户多个角色所以这张表是必要的。角色菜单关联表角色ID、菜单ID、权限编码冗余。这里存的是角色真正拥有的权限点集合每次加载权限时主要就是查询这张表。在角色表里数据权限范围值我存的是JSON格式。比如{ cityCodes: [310100, 310200], storeIds: [1, 2, 3] }这样设计的好处是灵活。后续如果增加了区域维度不需要改表结构JSON里加一个字段就行。当然如果你的团队不喜欢JSON这种弱约束存储也可以用三张关联表去规范化存但查询复杂度会高一些。项目早期阶段JSON方案够用。4.2 登录鉴权与权限动态加载用户登录流程大致是这样的用户提交账号密码后端校验密码校验通过后生成token同时把用户ID写入token payload。token的签发用JWT或者服务端Session都行我的经验是如果是多端场景小程序、App、H5JWT更灵活一些如果是纯后台管理系统服务端Session配合Redis做分布式存储也很稳定。登录成功后后端会去数据库加载当前用户的权限信息组装成如下结构返回给前端{ userId: 10001, userName: zhangsan, roles: [city_operator_sh], perms: [order:list:view, order:refund:audit, order:assign] }perms集合由该用户的所有角色合并去重而来。角色里关联了哪些权限码就把这些权限码全部取出。如果角色有继承关系还需要递归取出父级角色的权限码。前端拿到这个结构后把perms缓存到全局状态管理里比如Pinia/Vuex后续所有按钮级别的v-if判断都可以通过hasPerm方法实现。后端则会把perms缓存到Rediskey是userId值为权限码列表。这样做的目的在于用户每次请求接口时后端不用每次都查数据库拼权限直接从Redis取性能会好很多。权限变更时比如管理员修改了某个角色的权限系统会主动删除Redis里所有关联该角色的用户权限缓存。这个过程可以用消息广播或者批量处理实现。我做了个简化版的管理工具就是后台权限变更成功之后再调用一次“清除权限缓存”的接口把相关用户的缓存key全部删掉。实测下来这样处理以后权限变更基本能秒级生效体验比强制重新登录好很多。4.3 后端接口权限校验后端接口校验我一般用AOP拦截器配合注解来做。自定义一个注解比如RequirePerm(order:refund:audit)然后写一个AOP切面在方法执行前进行权限判断。切面逻辑核心如下Aspect Component public class PermAspect { Around(annotation(requirePerm)) public Object checkPerm(ProceedingJoinPoint pjp, RequirePerm requirePerm) throws Throwable { // 获取当前登录用户 LoginUser loginUser SecurityUtils.getLoginUser(); if (loginUser null) { throw new AuthException(未登录); } // 从Redis/缓存中获取用户权限码集合 SetString perms permCacheService.getUserPerms(loginUser.getUserId()); // 校验当前注解声明的权限码 if (!perms.contains(requirePerm.value())) { throw new ForbiddenException(无操作权限); } return pjp.proceed(); } }这里的核心点有几个当前用户信息怎么拿。我是在登录过滤器里把userId塞进ThreadLocal这样在任意线程里都能取到当前登录用户。权限缓存怎么存。我上面提到了Redis存权限码集合key按用户维度。注解里的权限码必须是定死的静态字符串不能拼参数因为拼参数容易造成权限绕过漏洞。还有一个容易踩的坑骑手端和用户端的接口尽量分开。我在项目里是这样做的用户端接口统一走/api/user/**骑手端接口走/api/rider/**管理后台走/api/admin/**。不同的前缀走不同的登录校验过滤器防止用户端token调用管理后台接口。这一点看似简单但实际项目里真的有人不区分导致用户能通过篡改接口路径访问到后台管理功能。4.4 前端动态路由与按钮级权限控制前端我以Vue3 Pinia为例来说明。用户登录以后前端会调用“获取用户信息”接口拿到上面提到的perms集合然后通过动态添加路由的方式只注册当前用户有权访问的页面路由。动态路由的关键步骤定义一份全量路由表每一条路由的meta里带上permission字段。登录后遍历全量路由表过滤出当前用户perms中包含对应权限码的路由用router.addRoute逐条注册。菜单组件根据过滤后的路由自动生成侧边栏菜单。按钮权限则统一封装成一个指令或函数。例如function hasPerm(perm: string) { return userStore.perms.includes(perm); }模板里这样控制按钮是否显示el-button v-ifhasPerm(order:refund:audit)审核退款/el-button有一点必须反复强调前端隐藏按钮只是交互层的事情绝对不能作为安全边界。真正可靠的校验必须是后端接口。前端做的再好也只能提升体验防止普通用户误操作但防不住恶意人员。5. 踩坑实录与排查技巧速查表权限这块开发完以后真正花时间的是测试和线上问题排查。我把我在跑腿系统权限项目里遇到的高频问题整理成速查表全是实操中的经验。现象原因排查思路用户登录后菜单不显示角色未正确关联菜单权限检查角色-菜单关联表是否漏了父级菜单权限接口提示无权限权限码不一致对比数据库菜单表权限编码和后端注解权限码是否完全一致用户能看到其他城市数据数据权限类型未设置检查角色表的数据权限类型和范围值修改角色权限后不生效用户权限缓存未刷新清除Redis中的权限缓存或强制用户重新登录管理后台接口被普通用户访问接口URL未分层校验检查过滤器对接口路径前缀的匹配规则5.1 动态菜单有时不显示但接口权限正常这类问题通常出在菜单树的父子关系维护上。如果只给角色分配了一个子菜单权限但没分配父级菜单权限前端递归生成菜单树的时候子菜单会因为父级不可见而整棵菜单消失。这是我在项目里踩得最深的一个坑。解决方案有两个。要么每次配置角色权限时前端自动勾选父级菜单要么后端起过滤菜单树时自动向上补充父级菜单。我最终选用的是后端处理因为在后端补充父级不会出现前端传参丢失的情况管理起来更可控。5.2 数据权限拼接导致的SQL性能问题数据权限的SQL拼接如果直接写在每个业务查询里会导致代码很散乱后期维护特别痛苦。而且如果不注意优化订单查询列表会多出几个索引没命中数据库压力明显上升。我最终的做法是把数据权限拼接统一到一个自定义SQL过滤器中。这个过滤器会读取ThreadLocal里的用户数据范围信息自动构造好追加条件。所有业务查询不用关心用户是哪个城市的只需要在Mapper方法上标注数据权限注解即可。这里也提醒一句out-of-box省事的前提是所有表都建了对应的归属字段。跑腿系统的订单表、售后表、骑手结算表这些核心表必须建city_code、store_id、create_by这类字段而且创建数据的时候就要落值。如果历史数据没落值做数据权限过滤时会漏数据或者误杀数据很难排查。5.3 多角色用户切换体验的优化前面提到一个用户可能同时有“骑手”和“商家”两个角色我用的是用户-角色关联表加上is_default字段来处理默认进入角色。用户可以在App端的工作台里手动切换身份。切换身份的接口调用流程是传入一个角色ID后端校验该用户确实拥有该角色后重新生成token同时返回对应的角色信息和权限集合。前端拿到新的权限数据后刷新当前工作台的菜单和可操作按钮。这个流程里一开始遇到过一个体验问题骑手切到商家角色后原先骑手端的socket连接还在推送订单信息。后来我在切换角色时前端主动断开旧的socket连接再按新角色的身份重新建立链接后端也通过校验user_agent或者token中的角色标识来确保推送范围和身份匹配。这块涉及实时通信和权限的结合属于比较容易遗漏的环节做的时候要多留意。5.4 权限变更后的实时性问题权限变更的生效方式我经历了三个阶段。第一阶段是改完角色权限后让用户重新登录结果被运营团队吐槽效率太低。第二阶段是把权限码直接塞进JWT里JWT无状态导致权限变更后token里的旧权限无法失效只能等token过期又出问题。第三阶段就是现在用的Redis缓存权限方案权限变更后主动删除该角色下所有用户的缓存用户下一次请求时重新加载权限缓存。如果你也想用Redis缓存权限方案我建议给缓存key做这样的设计auth:perms:{userId} auth:roles:{userId}角色缓存里存放角色编码列表权限缓存里存放权限码列表。删除缓存时只需根据userId找到key删除即可。如果权限变更影响到一批用户可以在角色-用户关联表上查询到这批userId然后批量删除。6. 一些心得体会跑腿系统的权限控制模型最终比的不只是代码写得好不好更是对业务规则理解的深度。一个订单从用户下单到骑手送达涉及用户、平台、骑手、商家多方角色每个角色的权限诉求和数据边界都不同。如果一开始角色拆分不清楚、权限点设计不够细后期打补丁的成本要比现在设计时花的时间多得多。我自己在项目里最受用的做法是以数据表为基础先把角色清单和权限清单穷举出来然后让运营和产品同事一起评审。让业务人员来确认“这个角色到底能不能审核退款”这类问题比开发自己猜要靠谱得多。权限模型搭建好之后再逐步把菜单、接口、数据权限串联起来落地效率会高很多。最后再分享一个小技巧权限系统的操作日志一定要做全量记录。谁在什么时间把哪个用户的角色改了、给哪个角色加了一个什么权限码这些日志不仅用来排查问题在某些时候也可以当作合规审计的依据。做好权限体系不是一步到位的事情但把基础模型定扎实后续不管加新角色还是新业务模块都能稳稳地往里面加。希望这篇文章对正在设计跑腿系统后台的朋友们有所帮助。