☰
课程观看权限精确绑定到用户组:模型与实现解析
2026/10/13 5:51:27 网站建设 项目流程

很多人做在线课程系统的时候,前期最常干的一件事就是把课程直接挂出来,谁注册谁看。等业务跑起来才发现,压根不是这么回事——企业内训的课只能给内部员工看,VIP会员的进阶课不能开放给免费用户,和渠道合作方的课程包要单独授权给对应团队的账号。这时候"指定用户组观看权限"就成了刚需。

但真到做这套权限功能的时候,坑比想象中多得多。我最近刚帮一套视频课程系统做完用户组权限改造,从权限模型设计到接口实现再到前后端联动,踩了不少坑,也总结了一套比较顺的方案。这篇就围绕"怎么把课程观看权限精确绑定到指定用户组",把完整思路和可落地的实现过程拆开聊,适合正在做在线教育系统、知识付费平台、企业内训系统的开发者和产品同学参考。

1. 为什么权限控制要落在"用户组"而不是"单个用户"上

1.1 按用户逐个授权,前期爽,后期就是灾难

很多系统第一版做的都是"用户-课程"直挂。新用户注册进来,后台管理员进编辑页,勾选一下这个用户能看哪些课,完事。用户少的时候这套逻辑非常直观,十来个用户,每个人看哪几门课,管理员心里门儿清。

但用户量一旦上来,这个模式立刻暴露出问题。我在做权限改造之前,客户那边某企业版账号下有三百多个员工,每个员工要开通的课程还不一样,有按部门分的、有按职级分的、还有项目制临时开通的。管理员每次开通权限都要从三百多人里挨个找人,选完人再选课,碰上重复操作率高的时候,一天的时间全砸在点鼠标上了。

更要命的是权限到期和变更。某员工调岗了,原来部门的课程权限要不要保留?某课程迭代了,旧版本的学习记录和观看权限要迁移到新版本吗?如果你还是"用户-课程"二维表,每次变动都要手动去改每个人的记录,改错一条那就是权限事故。

1.2 用户组授权的本质:把"人"和"权限"解耦

用户组授权的核心思路,和办公室门禁卡是一个道理。你入职某家公司,行政不会单独给你录入"这个人能开3楼会议室门、能开5楼办公室门、能进地下车库",而是把你划入"普通员工组",门禁规则写在组上。以后人员进出,管理员只需管组,不用管人。

对应到课程系统里,就是三层模型:用户 → 用户组 → 课程权限。人去组,组去关联课程。这样一来,管理员的日常操作从"给张三开A课、给李四开B课、给王五开A课和B课"变成了"销售组开A课和B课,运营组开C课",操作量和出错概率都直线下降。

用户量大的场景下还有一个隐藏收益:新员工入职只需要加入对应部门组,课程权限自动生效,完全不需要管理员介入。反过来,员工离职时移出用户组,所有关联权限一并回收,不会出现"人走了但还能看内部培训视频"的权限漏洞。

1.3 哪些业务场景最需要用户组权限

从我接触到的实际项目来看,需要这套能力的场景大致有这几类:

  • 企业内训平台:按部门、职级、岗位划分用户组,不同组看不同的培训课程包。
  • 会员制知识付费:普通用户组、付费会员组、超级会员组,不同组对应不同层级的课程池。
  • 渠道分发与代理:给不同代理商分配专属的用户组,后台上传课程后指定给某个渠道组可见,互不干扰。
  • 预售/测试/白名单场景:正式上线前的内测课程只对内部测试组开放,公开访问一律挡住。
  • 多校区/多组织架构:同一套系统给多个独立机构使用,机构A的课程不能出现在机构B的用户账号里。

这些场景的共同点是:用户和课程之间是一对多的关系,且这种关系的管理频率很高、颗粒度经常变化,必须靠一个中间层来承接。

2. 权限模型搭建:三张核心表和两组关联关系的设计

2.1 最基础的数据模型长这样

面对"指定用户组才能看某课程"这个需求,数据模型不用整太复杂,核心就是三张基础表加两张关联表。

基础表:

  • 用户表(user):存账号基本信息,至少包含 user_id、status。
  • 用户组表(user_group):存组名、组描述、创建时间,字段比如 group_id、group_name、is_locked。
  • 课程表(course):存课程信息,至少包含 course_id、title、status(上架/下架)。

关联表:

  • 用户-用户组关联表(user_group_member):user_id + group_id 多对多。一人可以加入多个组,一组可以有多个成员。
  • 用户组-课程授权表(group_course_access):group_id + course_id 多对多,也可以在表里加 expire_at(过期时间)做临时授权。

在实际落地时,我建议不要搞"冗余冗余再冗余"的设计。曾经有个版本,为了查询快,直接在用户表里加了 course_ids 字段,拿逗号拼接课程ID,看起来省了一次关联查询,但课程一多、组一多,拼接串变得巨长,而且改权限的时候要动用户表,数据一致性问题分分钟出事故。老老实实走关联表,配合索引和缓存,性能完全够用。

2.2 继承与覆盖:子组怎么处理才合理

真实业务里用户组不是扁平的一层,往往是树形结构,比如"某公司 → 华东区 → 销售一队"。这时候就面临一个设计决策:子组要不要自动继承父组的课程权限?

我的建议是:设计上支持继承,但实现时要留出"单独踢出"的口子。

具体做法是在用户组表加上 parent_id 字段,授权时如果子组没有显式授权记录,就判断逻辑往上找父组。比如销售一队的组课程授权为空,就沿着 parent_id 找到华东区组,再看华东区组的授权列表。但有些课程华东区组能看,销售一队却因为保密要求不能看,这时候就需要一个排除机制。

我当时的设计是加了一张组课程排除表(group_course_exclude),专门记录"这个组的用户虽然通过继承能看到某课程,但实际要屏蔽"。判断逻辑是:先取所有可达的父组和自身组的授权课程全集,再减掉当前组排除列表里的课程,剩下的才是最终可见课程。这个方案虽然多一张表,但灵活性远超"要么全继承、要么全不继承"的二选一方案。

2.3 最小权限判断:用户最终能不能看一门课

不管模型设计得多花哨,最终落到接口层就是一句话:给定一个 user_id 和 course_id,判断能不能看。核心校验逻辑按下面的优先级走:

  1. 用户是否被封禁/冻结,是则直接拒绝。
  2. 课程是否存在且状态为上架,否则拒绝。
  3. 判断该用户加入的所有用户组(包括递归的父组)是否有某条授权记录覆盖该 course_id,且该记录未过期。
  4. 如果命中了授权记录,再检查当前用户组是否有针对该课程的排除记录,如果有则拒绝。
  5. 再检查课程本身是否设置了"全站公开",如果公开则无需组授权也可访问。

为了描述清楚,我在这里给一段简化的校验逻辑伪代码,实际实现时可根据语言改写:

def check_course_access(user_id, course_id): user = get_user(user_id) if not user or user.status == "blocked": return False, "用户不可用" course = get_course(course_id) if not course or course.status != "published": return False, "课程不存在或未上架" if course.is_public: return True, "公开课程" group_ids = get_all_group_ids(user_id) # 包含递归父组id matched = check_group_course_access(group_ids, course_id) if not matched: return False, "当前用户组无此课程权限" excluded = check_group_course_exclude(group_ids, course_id) if excluded: return False, "该课程已被用户组屏蔽" if matched.is_expired: return False, "课程授权已过期" return True, "允许访问"

这段逻辑最核心的一点是get_all_group_ids 一定要递归收集父组,否则树形组织下子组用户拿到不继承权限,产品体验就会很割裂。

3. 后端接口与前端展示的联动:课程从"看不见"到"能播放"的完整链路

3.1 授权管理接口:批量处理才是管理员关心的

用户组权限的后台接口设计,不能只提供"给某组添加某课程"这种单个接口,管理员的真实需求是批量、快速。我给的接口设计分成三类:

  1. 组课程授权:POST /admin/groups/{group_id}/courses,支持一次传多门课程的 course_id 数组。
  2. 组课程移除:DELETE /admin/groups/{group_id}/courses/{course_id},或者传数组批量解除。
  3. 组课程排除:POST /admin/groups/{group_id}/exclude-courses,用于"继承但不看"的场景。

接口内部要注意事务问题。批量添加的时候必须包在一个事务里,要么全部成功,要么全部回滚。我见过一个线上事故,就是批量授权时中间某条数据因为课程ID不存在抛了异常,但前几条已经 commit 了,结果管理员界面上用户的权限"加了一半",排查半天才搞明白是事务拆太细了。

批量授权还有一个容易被忽略的参数:授权时间。严格的业务管理最好在关联表里加一个expire_at字段,支持"从某天开始生效到某天结束"。这样临时项目组、限时课程的授权就不需要管理员记着去撤销,到期自动失效。

3.2 课程访问鉴权:播放接口不能只靠前端隐藏

课程列表页可以靠前端控制显示哪个课、不显示哪个课,但真正防"懂行的人"得靠后端接口挡。视频播放地址、课程详情接口、课时详情接口,这三类必须做权限校验。

具体实现可以写一个中间件/装饰器,统一在进入课程相关接口之前进行权限校验。比如在 Spring 里做一个拦截器,在 Django 里做一个 middleware,核心逻辑就是拿到当前登录用户的 user_id 和请求里的 course_id,调上面说的 check_course_access 逻辑。

这里要强调:视频文件本身的防盗链也要做。很多系统把视频传在对象存储上,播放地址是带签名的URL。就算课程接口权限校验通过,如果把带签名的播放地址泄露出去,别人拿到这个URL还是能播。所以播放地址的签名有效期要短,一般设10到30分钟,而且签名里最好绑定用户标识,防止同一个URL被多人共用。

3.3 前端"可见性"控制:没有权限的课程怎么呈现

前端要做的事情不只是"接口返回啥我渲染啥"。课程列表页拿到接口数据后,如果某课程没权限,推荐的处理方式是:

  • 卡片上显示课程封面、标题,但播放按钮替换成"无权限"或"加入XX组后观看"。
  • 点击无权限课程时不跳播放页,而是弹出一个引导弹窗,提示用户当前用户组没有该课程权限。
  • 已经授权但已过期的课程,显示"已过期"状态,而不是直接消失,避免用户以为自己课程丢了。

这里有个设计原则:不要对没权限的课程完全不可见。从营销角度讲,展示出来但锁住,会刺激用户去升级、去开通;从产品的角度讲,如果全隐藏,用户可能压根不知道有这门课,运营做的推广活动就白费了。所以"锁住但不隐藏"基本是通行的最优解。

前端鉴权的实现,不建议只依赖v-if或router.beforeEach做拦截。前端控制只能保体验,保不了安全。正确姿势是:页面路由加载时调用后端接口拿"当前用户可访问的课程ID列表",然后前端拿这个列表渲染课程卡片状态;真正播放时再由后端播放接口做二次校验。两层结合,既顺畅又安全。

3.4 管理后台的操作体验:树形选择器和批量操作

后台管理界面是给管理员用的,操作效率直接决定了这套权限系统在业务侧的口碑。我在这块踩过坑,最初版本就是两个下拉框,左边选组、右边选课程,一次只能加一门课。客户管理员反馈操作太慢,后来改成了课程树形选择器。

改进后的交互流程是:左侧选用户组 → 右侧展示两栏,左边是"全部课程(可搜索)",右边是"本组已授权课程",中间放转移按钮,支持多选、全选、拖拽。选完以后点击保存,一次性提交所有变更。

对于"继承但排除"的场景,我推荐单独开一个 Tab,叫"排除课程",同样用课程选择器。入口明确,管理员不用去理解复杂的继承模型,只需要知道"这个组虽然继承了一堆课,但我不想让它看哪几门,就放这里"。

4. 线上实战:权限系统最容易翻车的问题和排查链路

4.1 用户组缓存导致的权限变更延迟

系统上线之后遇到最典型的故障是:管理员在后台给某用户组加了一门新课,用户那边登陆后刷了十几次都看不到新课。查了半天发现,权限判断接口在用户组授权层做了缓存,缓存Key只到组ID,没有把授权版本号放进去。

问题出在哪?组ID不变,但组下的课程授权列表变了。我给后台授权接口加了一个自增的grant_version字段,每次授权变化就 +1,权限判断接口把 grant_version 一起作为缓存的 Key 组成。这样课程授权变更后,旧缓存自动失效,新请求拿到的是最新权限。

这个问题的排查链路值得记一下:

  1. 先确认后台数据:查group_course_access表,确认授权记录确实写进去了。
  2. 再确认用户所在组:查user_group_member,确认用户确实还在这个组里。
  3. 然后检查缓存:把权限缓存手动清掉,再让用户刷新。
  4. 如果清掉缓存就恢复了,基本可以断定是缓存失效策略的问题,去查缓存Key设计。

4.2 直播回放课程的权限复制问题

线上课程系统做直播功能很常见。直播结束后课程自动变成回放,这时候权限判断容易出现一个诡异的问题:直播课在创建时挂在"直播用户组"下,直播结束后生成了回放课程,但回放课程没有自动继承直播课的授权关系,导致原来能看直播的人,回放反而看不了。

解决思路是:直播转为回放时,自动调用一次"复制授权"逻辑,把原直播课程的所有组授权记录复制到回放课程上。复制的时候要注意不要重复插入,最好用INSERT ... ON DUPLICATE KEY UPDATE或者先查询再插入,避免并发执行时生成重复授权记录。

4.3 移动端静默登录导致的权限丢失

还有一个隐蔽的问题出现在移动端App或小程序里。用户在微信里打开课程链接,如果没主动登录,系统会走静默授权,拿到的用户信息是匿名的,这时候权限判断直接走了"未登录用户"分支,返回没权限。明明用户在PC端能看到,手机端却报无权限,一时半会儿很难排查。

排查思路是:在权限中间件入口先打日志,看当前请求的 user_id 是否为空。如果为空,说明不是权限逻辑问题,是登录态传递的问题。移动端登录态一般通过 token 放在请求头里,检查 WebView 或小程序的请求有没有正确携带 token,往往问题就出在某个跳转链路把 token 丢了。

4.4 多用户组叠加时的"伤敌一千自损八百"

一个用户同时属于三个组,组A授权了课程X,组B排除了课程X。按照我之前说的校验逻辑,授权命中了、排除也命中了,最终结果是不允许访问。这个逻辑本身没问题,但产品上要特别注意这个规则的告知。

有一次客户找我,说某员工原来是运营组,后来兼了销售组,结果销售组的课程他能看,运营组的老课程反而看不了了。检查发现是销售组里配了一条课程排除规则,而这条规则本意是"销售组不需要看运营的内训课",但因为用户同时在这两个组,排除规则跨组"误伤"了他。

这类问题没有绝对正确的答案,关键要在产品规则里写清楚:排除记录是仅对当前组的继承权限生效,还是对用户的最终可见范围生效。我当时和客户确认后,把规则改成了"排除仅影响该组自身的继承权限,不跨组叠加",用户在组A有授权、在组B被排除,最终结果还是能看组A的课。这样更符合常识:我加入不同项目组,各自组的权限互不干扰。

5. 性能优化与扩展思路:权限量大了以后怎么办

5.1 大数据量下的权限查询优化

用户量十万级、课程量千级时,关联查询的耗时开始有人会注意到。尤其在课程列表页,需要在一次请求里返回几十门课,每门课都做一次"判断当前用户是否有权限"的查询,N+1问题就会冒出来。

我的优化方案分两步:

第一步,批量查询。在课程列表接口里把用户的所有组ID拿到,然后用一条 SQL 查出这批组ID对所有课程ID的授权关系,一次性组装成一个Map<courseId, 授权记录>,内存里判断每门课是否可见。SQL类似:

SELECT course_id, expire_at FROM group_course_access WHERE group_id IN (:groupIds) AND course_id IN (:courseIds);

第二步,把用户的可访问课程集合做缓存。对权限实时性要求不那么苛刻的系统,可以在用户登录后把其所有可见课程ID缓存10分钟,管理端做了授权变更时通过版本号机制让缓存失效。这样课程列表接口完全不需要实时查权限表,性能提升非常直观。

5.2 权限变更后的主动通知

权限变更不只是缓存的事,还牵扯到已经打开的页面。比如用户正在看课程详情页,管理员后台把这个课程从用户组里移除了,这时候用户如果继续播就会在播放中途收到"无权限"的报错,体验很差。

更平滑的方案是:后台授权变更时发布一个事件(比如走消息队列),事件里带着变更的 group_id;在线用户的前端通过 WebSocket 或者在页面轮询时收到"权限变更通知",前端收到通知后主动检查当前正在访问的课程是否还在自己的可见列表里,如果不在,弹提示并停止播放。

这个机制不是必须的,但如果平台承载的是付费课程,建议一定要做。付费用户正看到一半突然被切断,如果没有提示,用户第一反应是系统出bug了,而不是权限被回收,客服压力会很大。

5.3 扩展方向:套餐、渠道、地域维度

用户组这套模型稳定跑起来之后,可以很自然地往其他维度扩展。我做过的几个扩展方向供参考:

  • 按套餐维度授权:一个用户组绑定一个"课程套餐",套餐里有课程集合,后续课程更新只需在套餐层面加课,不用再去每个组里加。
  • 按渠道维度授权:把渠道商映射为用户组,课程上架时选择对某些渠道组可见,形成渠道隔离。
  • 按会员等级维度授权:把等级映射为组,Banner位推荐的课程直接判断用户所在等级组是否授权,运营配置起来也很方便。

这些扩展本质上都是"组"这个中间层在发挥作用。只要组模型设计得够灵活,后面无论来什么授权维度,都可以复用同一套权限判断链路,不需要为每个新场景单独写一套判断逻辑。

6. 上线之后,我的几点实操体会

这套用户组权限系统从设计到上线,我最有体会的一点是:权限模型的设计一定要往"多一层"想。当时客户最初的诉求只是"让指定的两个部门能看某几门课",如果只按这个需求做,直接建两个组、绑定课程就完了。但后来业务扩张到渠道代理、限时白名单、回放权限继承,全靠当初做的那组继承和排除规则撑住了,没有返工重写。

另外一个体会是:权限判断的逻辑一定要收敛成统一的校验函数,不要让每个接口自己写 if 判断。我见过有些系统的权限代码散落在各个 service 里,有的接口用的是用户组校验,有的接口直接判断课程是否公开,有的接口压根没校验,最后测试时才发现好几个接口都能绕过权限。统一收敛到一个 checker 里,所有入口都走同一个逻辑,后续加缓存、加日志、加排查工具都方便得多。

如果你正在规划课程权限相关功能,我建议先把用户组模型设计清楚,再想接口和页面的事。组模型是地基,地基歪了,后面所有上层功能都会跟着歪。画清楚用户、组、课程、排除规则之间的关联,再动手写第一行代码,这样整个权限体系的开发过程中会少走很多弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询