开发

Odoo 评分请求为什么不是“发封邮件等用户点星”:rating.mixin、token 与反馈回写链路讲透

基于 Odoo 官方 `rating.mixin`、`mail.thread`、`rating.rating` 与控制器源码,讲清评分请求怎样生成 access token、为什么未消费评分会被复用、用户提交后如何落库并回写到 chatter。

Odoo 开发
进阶 开发者 3 分钟阅读
0 评论 0 点赞 0 收藏 4 阅读

很多人第一次看 Odoo 的满意度功能,会把它理解成一件很轻的事:

  1. 发一封“请给服务打分”的邮件;
  2. 客户点一下笑脸或哭脸;
  3. 系统保存分数;
  4. 结束。

但如果你真的顺着 addons/rating/models/mail_thread.pyrating_mixin.pyrating.pyportal_rating/models/rating_rating.pycontrollers/main.py 看一遍,会发现它并不是“随手发个表单链接”。

Odoo 实际上设计了一条比较完整的链路:

  • 业务对象通过 mail.thread 暴露评分能力;
  • rating.mixin 负责统计字段与展示语义;
  • rating.rating 存明细、token、反馈、父级对象引用;
  • 公共控制器用 token 找到目标评分记录;
  • 最后再把结果回写到 chatter,形成业务可追踪的反馈事实。

这套设计值得讲透,因为它解释了几个日常高频问题:

  • 为什么同一客户多次触发评分邮件,不一定会生成多条草稿评分;
  • 为什么评分链接能公开访问,但又不等于任何人都能替别人评分;
  • 为什么评分提交后,业务对象里会自动出现一条 chatter 消息;
  • 为什么你看到的 rating_avgrating_last_value 这些字段,并不是“表里直接存好的最终答案”。

一、入口不在 rating.mixin,而在 mail.thread

很多人一看到 rating.mixin 就以为它负责整个评分流程。其实不是。

真正让业务对象“能发评分请求”的关键入口,在 addons/rating/models/mail_thread.py

  • rating_ids = fields.One2many('rating.rating', 'res_id', ...)
  • _rating_get_access_token()
  • rating_send_request()
  • rating_apply()

也就是说,评分是先挂在 mail.thread 的交互能力上,再由 rating.mixin 去补统计字段

这很符合 Odoo 一贯的设计风格:

  • mail.thread 负责消息、通知、chatter 语义;
  • rating 模块把“评分”当作一种与消息系统强相关的反馈动作;
  • rating.mixin 更像统计视图层,而不是流程入口层。

所以实战里如果你要排查“为什么评分邮件没发出去”或“为什么提交后 chatter 没动”,第一眼不该只盯 rating.mixin,而应该先看 mail_thread.py

二、access token 不是每次都新建,它优先复用未消费评分

_rating_get_access_token() 这段逻辑很关键。

它先做一件安全上很重要的事:

self.check_access('read')

也就是说,哪怕后面很多动作会 sudo()调用这个方法的人本身也得对业务记录有读取权限。这一步避免了模板、自动化或别的调用方越权把任意记录的评分链接生成出来。

然后它会:

  1. 找评分发起对象(通常是 partner_id);
  2. 找被评分对象(通常是 user_id.partner_id);
  3. 在当前记录的 rating_ids 里找同一 partner 且 consumed = False 的草稿评分;
  4. 找到就复用;
  5. 找不到才新建 rating.rating

这解释了一个常见现象:

为什么同一张工单/任务/订阅记录,多次发评分邀请,token 看起来没变?

因为官方默认认为:同一个客户对同一个业务对象,如果之前的邀请还没真正提交,就没必要反复生成新草稿。

这不是偷懒,而是避免:

  • 产生一堆没人消费的无效 rating 记录;
  • 一个对象对应多个并行 token,最后难以判断哪条才是“真实反馈”;
  • 邮件提醒频繁重发时把统计口径弄乱。

三、rating.rating 才是真正的事实表

rating.rating 模型里真正定义了评分的事实粒度。字段里最值得注意的有:

  • res_model / res_id:这条评分具体对应哪个业务对象;
  • parent_res_model / parent_res_id:如果当前对象还要汇总到父对象,父对象是谁;
  • partner_id:谁来打分;
  • rated_partner_id:被谁服务、也就是谁被评分;
  • access_token:公开链接识别用;
  • rating:0 到 5;
  • consumed:这条邀请是否真的被提交;
  • feedback:文本评价;
  • message_id:回写到哪条 chatter 消息。

这里最重要的设计,不是“有个 rating 字段”,而是 它把“邀请”和“已完成评分”放在同一张表里,用 consumed 区分状态

这带来两个好处:

1)token 生命周期统一

链接先指向一条已存在但未消费的 rating 记录,提交后只是把它从草稿状态改成已消费状态,而不是“点完再临时创建一条新评分”。

2)统计可以天然过滤未完成邀请

无论 rating.mixin 还是 rating.parent.mixin,统计时都会显式要求 consumed = True。因此催评中的草稿不会污染平均分和满意度。

四、parent 数据不是手填,而是自动向上找

rating.rating.create() / write() 里如果看到 res_model_idres_id,会调用 _find_parent_data()

它会检查当前业务对象是否实现了:

_rating_get_parent_field_name()

如果实现了,系统就会自动把父对象信息写入:

  • parent_res_model_id
  • parent_res_id

这意味着 Odoo 不是把评分只当成“当前单据的一次反馈”,而是允许它继续向上汇总到更高层级的业务实体。

例如:

  • 某个消息线程上的评分,可能最终要汇总到工单;
  • 某个子对象上的评分,可能要上卷到项目或服务请求。

这就是为什么评分体系里既有面向当前对象的 rating.mixin,又有面向父级汇总的 rating.parent.mixin

五、提交评分时,控制器并不直接随便写数据库

公开评分入口在 addons/rating/controllers/main.py

有两个关键路由:

  • /rate/<token>/<rate>:打开提交页;
  • /rate/<token>/submit_feedback:真正提交反馈。

先看打开页这步。

控制器会先校验 rate 只能是 1、3、5 这三档。也就是说,前台入口默认不是任意小数星级,而是离散的 unhappy / neutral / happy

然后 _get_rating_and_record(token) 做两件事:

  1. 用 token 查 rating.rating
  2. sudo() 找到对应业务记录,并确认记录还存在。

这里很重要的一点是:

  • token 找不到 → 404;
  • 业务记录已经没了 → 404。

所以公开链接并不是“只要拿到 token 就一定还能提交”。底层业务对象被删掉,这条评分链路也就终止了。

六、为什么公开路由是 public,但又不是完全裸奔

很多人会担心:auth="public" 不就等于任何人都能乱写?

其实官方做的是“令牌授权”,不是“登录授权”。

另外在 action_open_rating() 里还有一层补充校验:

如果当前访问者不是公共用户,而是一个已登录用户,那么它的商业伙伴必须和评分里的 partner_id 对得上;否则展示的是 invalid partner 页面,而不是正常提交页。

这代表 Odoo 在兼顾两种场景:

  • 邮件收件人直接点公开链接,无需先登录;
  • 如果用户已经登录到站点,也不能借这个页面替别的 partner 提交反馈。

换句话说,公开入口靠 token 放行,已登录态则还要补一层“你是不是这位客户”的约束。

七、真正落库的关键动作,是 rating_apply()

action_submit_rating() 在 POST 时不会自己手写一堆字段,而是调用业务对象上的:

record_sudo.rating_apply(...)

rating_apply() 做的事情很集中:

  1. 校验评分值必须在 0 到 5;
  2. 根据 token 或传入 record 找到 rating.rating
  3. rating.write({'rating': rate, 'feedback': feedback, 'consumed': True})
  4. 如果当前对象属于 mail.thread,再把反馈回写到 chatter。

这一步特别关键,因为它说明:

Odoo 不把评分看成“只存在于 rating 表里的孤立数字”,而是把它同步成消息历史的一部分。

所以你在界面上看到的效果,往往是:

  • 数据层有一条 consumed rating;
  • chatter 里出现一条带表情图片和评论的消息;
  • 统计字段随之重算。

八、为什么 chatter 更新而不是重复发很多消息

rating_apply() 在回写时还考虑了 rating.message_id

  • 如果评分原本已经绑定了一条消息,就更新这条消息内容;
  • 否则 message_post() 新建一条消息。

这背后的好处很实际:

1)用户修改反馈时,不必不断制造重复消息

系统可以把同一条评分对应的消息内容刷新掉,而不是每次再插一条新的 chatter 记录。

2)评分与消息之间形成稳定引用

后续如果删掉 rating,unlink() 甚至还会把相关 chatter 消息一起清掉,保持业务历史一致。

这也是为什么 rating.rating 上要保存 message_id

九、rating.mixin 主要解决的是“怎么展示统计”

回到很多人最先注意到的 rating.mixin

它暴露了这些常见字段:

  • rating_last_value
  • rating_count
  • rating_avg
  • rating_avg_text
  • rating_percentage_satisfaction
  • rating_last_feedback
  • rating_last_text

但这些字段并不是简单 related。

rating_last_value 为什么单独跑 SQL

源码里这里直接用了 SQL 和 array_agg(order by write_date desc, id desc),本质上是为了高效地拿到“最近一条已消费评分”的值。

官方没有偷懒循环 Python,也没有随便 search(limit=1) 一条条取,而是一次性聚合。

rating_countrating_avg 为什么一起算

_compute_rating_stats()_read_group() 一次拿 count 和 avg,避免两个字段分开打两轮查询。

这说明官方默认判断:

  • 页面上通常会同时显示平均分和评分数量;
  • 它们应该作为一组统计结果来计算。

为什么满意度百分比单独算

rating_percentage_satisfaction 并不是平均分换算,而是把评分划到 great / okay / bad 三档后,再看 happy 占比。

这在业务上很重要,因为:

  • 平均分适合看总体质量;
  • 满意度更适合看“好评率”;
  • 两者不是一回事。

十、实战里最容易踩的 4 个误区

误区 1:以为每次发邀请都会新建 rating

实际上官方优先复用同一 partner 的未消费 rating。排查“为什么 token 一样”时别先怀疑缓存,先看有没有草稿评分被复用了。

误区 2:以为 rating_avg 包含所有评分草稿

不会。统计口径都严格过滤 consumed = True,没提交的不算。

误区 3:以为评分提交只是改一张表

不止。标准链路还会更新或新增 chatter 消息,所以排错时既要看 rating.rating,也要看 mail.message

误区 4:以为 token 链接公开,就没有权限边界

也不对。token 是第一层授权;已登录用户如果 partner 不匹配,官方仍会拦。

十一、给开发和实施的落地建议

如果你要在项目里扩展评分功能,我更建议这样理解它:

1)把 rating 当“反馈事件”,不是“一个字段”

不要只盯平均分。真正稳定的是:邀请 → token → 提交 → chatter → 聚合。

2)如果有父级 KPI 需求,优先复用 parent 机制

不要自己再造一张“汇总表”,先看能否通过 _rating_get_parent_field_name() 把上卷关系接起来。

3)调试时按链路排,不要只看 UI

建议顺序:

  • 是否生成 rating.rating 草稿;
  • consumed 是否变为 true;
  • message_id 是否建立;
  • rating_avg / rating_count 是否重算;
  • 前端模板是否正确读取统计字段。

结语

Odoo 的评分系统,表面上像一个“邮件打分小功能”,但源码其实把它做成了一条完整反馈链:

  • mail.thread 接入业务对象;
  • 用 token 驱动公开访问;
  • rating.rating 统一承载邀请与结果;
  • 用 chatter 把结果沉淀为业务历史;
  • rating.mixin 把统计字段暴露给界面。

所以它真正解决的,不只是“怎么收一颗星”,而是:

怎样让一次客户反馈,既能安全提交、又能被业务对象追踪,还能被统计系统稳定消费。

DISCUSSION

评论区

想参与讨论?先 登录 再发表评论。
还没有评论,你可以成为第一个留言的人。