先说结论
Odoo eLearning 的难点,不在于“课件能不能上传”,而在于一个人和一门课之间的关系,如何持续被运营。
从 /home/ubuntu/odoo-temp/addons/website_slides/models/slide_channel_partner.py 来看,slide.channel.partner 不只是成员中间表,而是一张完整的学习执行记录。它同时负责:
- 邀请链接生成
- 成员状态流转
- 完成度与已完成课件统计
- 下一课自动推荐
- 完课奖励与邮件发送
- 长期未响应邀请的自动清理
所以它真正解决的问题不是“这个人有没有加入课程”,而是:
课程邀请发出去之后,系统怎样一路把人从 invited 推到 joined、ongoing、completed,并在过程中不断告诉他下一步该做什么。
第一层:为什么课程成员关系要单独建表
很多学习系统会把“课程有哪些学员”做成很薄的一张关联表。 但 Odoo 没这么做。
slide.channel.partner 直接挂了:
member_statuscompletioncompleted_slides_countnext_slide_idinvitation_linklast_invitation_date- 课程可见性、报名方式、负责人等 related 信息
这说明官方判断得很明确:
- 课程是课程
- 学员是学员
- 但真正决定运营动作的,是“这个学员此刻以什么状态处在这门课里”
也就是说,Odoo 把课程成员关系当成了一个会变化、会触发动作、会产出下一步建议的业务对象。
第二层:邀请链接为什么不是普通 URL
_compute_invitation_link() 会把课程 URL、partner_id 和一段 hash 拼成邀请地址。
而这个 hash 由 _get_invitation_hash() 生成,本质上是基于 partner 与 channel 的 HMAC。
这背后的设计很值得学。
因为课程邀请并不是随便发一个公开链接就完事了。很多时候你需要的是:
- 让某个人收到一条可直接加入的课程链接
- 这个链接能表达“是针对谁、针对哪门课”
- 同时不能轻易被别人随手伪造
Odoo 在这里没有走重型 token 池,而是做了一个轻量但很实用的绑定方案:
- 链接里有具体对象
- hash 负责校验
- 链接既可点击直达,又保留一定身份约束
这很适合客户学院、内部培训和私有课程发放场景。
第三层:为什么成员状态不是 enrolled / not enrolled 两档
member_status 有四种:
invitedjoinedongoingcompleted
这比很多系统只做“已报名 / 未报名”细得多。
原因很简单:学习运营不是一次性动作,而是一条持续链路。
invited
说明人还在被激活阶段,最重要的是打开课程。
joined
说明已经进入课程,但还没真正开始推进。
ongoing
说明已经在消化内容,需要的是进度感和继续学习的牵引。
completed
说明流程进入收尾,可触发完课通知、奖励、复训等动作。
所以这四个状态不是“字段很多”,而是在表达不同运营阶段需要不同动作。
第四层:下一课为什么要由系统算出来
_compute_next_slide_id() 的逻辑非常典型:
- 只看已发布、active、且不是分类节点的内容
- 排除已经在
slide.slide.partner中标记为completed=True的课件 - 按 sequence 顺序找到第一条未完成内容
- 把它作为
next_slide_id
这说明 Odoo 不想让学员每次都重新思考“我下一步学什么”。
这件事看起来像体验优化,实际上很关键。
因为学习系统里最常见的流失点之一就是:
- 用户知道自己报了课
- 也知道课程还没学完
- 但不知道下一步该点哪里
只要这个摩擦存在,完成率就会明显下滑。
所以 next_slide_id 本质上是在把“课程结构”翻译成“学员此刻的下一动作”。
这正是 LMS 该做的事情。
第五层:完成度为什么不是人工写,而是从真实完成记录反推
_recompute_completion() 会按课程和学员去统计:
- 该学员在该课程里已完成多少条已发布内容
- 占课程总课件数的比例是多少
- 然后把
completion、completed_slides_count和member_status一并更新
更重要的是,它还有两层业务判断:
- 之前没完成、现在完成了,要进入 completed 流程
- 之前完成过、后来因为课程内容变化又没满 100%,也要退回未完成状态
这个设计非常成熟。
因为课程不是永远静止的:
- 可能会新增一节课
- 可能会下架一节课
- 可能某些课件后来才发布
如果完成度只是一次性写死,课程一更新,学员状态就会失真。
Odoo 选择每次从真实完成记录回算,就是为了让“完课”始终保持业务真实性。
第六层:为什么完课后不只是改个状态,还要发邮件和奖励
当学员从未完成转成完成时,系统会:
- 触发
_post_completion_update_hook() - 视课程配置给用户加 karma
- 调
_send_completed_mail()发送完课邮件
这说明在 Odoo 眼里,学习完成不是内部统计数字,而是一个需要被明确反馈的业务事件。
这很重要,因为真正能提高课程完成率的,不只是课件质量,还包括:
- 用户有没有感知到“我完成了”
- 完成后有没有即时反馈
- 平台有没有把这件事转成可见奖励
也就是说,Odoo 不是只做内容承载,而是在做持续激励。
第七层:为什么还要定期清理过期邀请
_gc_slide_channel_partner() 是个很容易被忽略的自动清理逻辑。
它会删除满足这些条件的邀请记录:
member_status = invitedcompletion = 0- 邀请时间缺失或早于 3 个月前
这代表官方默认认为:
长期没响应、也没开始学习的邀请,不应该无限制留在课程成员池里。
这个判断很实用。
因为如果不清理:
- 课程后台会堆满失效邀请
- 运营会误以为课程成员很多
- 实际活跃度会被严重高估
自动清理不是为了“数据库整洁”这么简单,而是为了让课程成员列表继续代表真实的运营状态。
最容易误解的四个点
误区一:成员关系表只是课程和学员的连接
不对。它本质上是学习执行单,承载状态、进度、邀请和下一步动作。
误区二:邀请链接只是邮件方便点击
也不止。它其实是课程准入逻辑的一部分,背后还有 hash 绑定。
误区三:下一课推荐只是 UX 小优化
不是。它直接影响学习摩擦与完成率。
误区四:完课了就永远完课
源码并不这么想。课程内容变化后,完成状态是可以被重新计算的。
实战上怎么把课程运营做得更稳
如果你要把 Odoo eLearning 用在内部培训或客户学院,我建议重点关注:
- 先定义课程是公开报名还是邀请制,再决定是否大量使用 invitation link
- 不要只看课程总人数,更要看 invited、joined、ongoing、completed 各自比例
- 课程内容调整后,记得复核完成率变化,不要假定历史状态永远有效
- 把下一课入口放到最显眼位置,它比你想象中更影响完课率
- 完课邮件和奖励机制最好一起配置,不要只改状态不反馈
最后总结
Odoo eLearning 最值得学的地方,不是“能不能上传视频或文档”,而是它很清楚学习运营真正的难点不在内容本身,而在于:
- 邀请之后有没有真正加入
- 加入之后有没有持续推进
- 推进过程中下一步够不够明确
- 完成之后有没有及时闭环
所以 slide.channel.partner 看似只是成员关系,实际上已经是一台小型学习运营引擎。
DISCUSSION
评论区