先说结论
Odoo 官网上的“Subscribe to Newsletter”并不是一个简单的收邮箱输入框。
website_mass_mailing 实际在处理的是一整条订阅治理链路:
- 判断当前人是不是已经订阅;
- 区分公开 mailing list 和不可再公开加入的名单;
- 复用已有联系人而不是盲目重复创建;
- 遇到历史退订记录时恢复订阅而不是生成脏数据;
- 在公开网站入口前增加 reCAPTCHA / Turnstile 防护;
- 把订阅值记到 session,让后续体验更连续。
所以这个模块真正解决的不是“能不能收集邮箱”,而是:
如何在公开网站上,低摩擦但相对可控地完成订阅收口。
一、为什么订阅前先判断“是不是已经订阅”
/website_mass_mailing/is_subscriber 这个接口非常关键。
它会根据:
- list_id
- subscription_type
- 当前用户邮箱或 session 中记住的邮箱
去查 mailing.subscription,并且只把 opt_out = False 的记录算作已订阅。
这意味着 Odoo 在用户点击订阅前,就尽量先回答一个现实问题:
这个人到底是新订阅者,还是已经在名单里的人?
这样做有两个好处:
- 前端可以直接切到“已订阅/感谢”状态,而不是让人重复提交;
- 运营侧不会因为同一个人反复点按钮而收到一堆噪音动作。
所以在 Odoo 里,“订阅按钮状态”不是纯前端交互,而是基于真实订阅记录的。
二、为什么公开网站也要区分哪些名单可以被加入
controllers/website_form.py 里有一段很有代表性的校验:
如果表单目标是 mailing.contact,系统会把传入的 list_ids 拿出来检查,并拦截 is_public = False 的名单。
换句话说:
- 不是所有邮件名单都适合暴露在官网上;
- 即便前端能传入 list id,后端也会再核验;
- 私有名单不会因为一个公开表单就被随意加入。
这体现了 Odoo 很清晰的边界意识:
官网订阅入口是公开的,但 mailing list 本身不一定应该完全公开。
对于企业来说,这一点很重要。 因为内部客户名单、测试名单、分层运营名单,很多都不应该被官网直接写入。
三、为什么 subscribe 不是单纯 create,而是“查找、复用、恢复”
subscribe_to_newsletter() 的实现非常务实。
它不是见到一个邮箱就直接新建,而是分三步:
第一步:解析输入
如果是 email 订阅,会先 parse_contact_from_email,尽量拆出 name 和 email。
第二步:找现有 subscription
系统会先查:
- 当前 list
- 当前 email
对应的 mailing.subscription 是否已经存在。
第三步:按情况处理
- 没有 subscription: 去找现有
mailing.contact,没有再创建,然后建立 subscription; - 已有 subscription 且 opt_out = True: 直接恢复为重新订阅;
- 已有有效 subscription: 不重复造数据。
这个逻辑背后非常成熟。
它说明 Odoo 把订阅当成“联系人与名单之间的关系”,而不是一条孤立表单记录。
因此它优先考虑的是关系复用和状态恢复,而不是无限新增。
四、为什么 opt-out 恢复比“重新创建一条”更重要
很多系统在处理退订用户重新订阅时,会粗暴再插一条记录,最后导致:
- 同一邮箱出现多条重复 contact;
- 名单关系混乱;
- 历史行为很难解释;
- 后续退订、统计和清洗都变脏。
Odoo 的做法更干净:
如果已有 subscription 只是 opt_out = True,那就把它恢复。
这个动作看起来简单,实际上非常重要。
因为它承认:
用户状态会变化,但业务对象最好保持连续。
对数据治理来说,这比“重新建一条省事”成熟得多。
五、为什么公开订阅入口必须做机器人防护
/website_mass_mailing/subscribe 在真正处理订阅前,会先调用:
request.env['ir.http']._verify_request_recaptcha_token('website_mass_mailing_subscribe')
前端交互里还兼容了 Google reCAPTCHA 和 Turnstile。
这说明 Odoo 非常清楚:
- 官网订阅入口天然暴露在公网;
- 只要能写入联系人或名单,就存在被脚本灌数据的风险;
- 如果没有机器人防护,邮件营销名单会很快被污染。
而名单一旦被污染,后果不只是“多了点垃圾邮箱”,还会影响:
- 投递质量;
- 域名信誉;
- 退信率和投诉率;
- 运营分析可信度。
所以验证码这里不是可有可无的安全附件,而是 mailing 数据质量的一道前门。
六、为什么 session 记忆能明显改善订阅体验
源码在成功订阅后会把值写到 session,例如 mass_mailing_email。
这让后续页面上的订阅组件可以:
- 自动知道当前浏览器对应的邮箱是谁;
- 判断是否已订阅;
- 避免让同一人重复输入。
这类设计经常被忽略,但它对转化体验非常有用。
因为用户不会觉得“每个区块都在重新问我同一件事”,而是会感到网站记得自己刚完成过什么动作。
对于订阅弹窗、页脚表单、专题页嵌入表单并存的站点,这种一致性尤其重要。
七、为什么官网订阅不是孤立功能,而是 Email Marketing 的入口
从模型关系可以看出,官网表单最终写入的是 mailing.contact 和 mailing.subscription。
也就是说,网站层只是入口,真正承接这条数据的,是 Email Marketing 体系。
这会影响你对“订阅成功”的定义。
它不是:
- 前端 toast 显示成功;
- 数据库里多了一行邮箱;
而是:
- 联系人对象干净;
- 名单关系正确;
- 私有名单没有被误写;
- 退订状态被合理恢复;
- 机器人没有轻易灌水进来。
如果这些没处理好,表面上的“收集成功”其实没有太大价值。
八、实施时最容易踩的坑
1. 把所有名单都设成 public
这样最方便,但长期最危险。公开入口应该只连适合公开收集的名单。
2. 只盯着前端样式,不检查后端名单关系
订阅弹窗再漂亮,如果联系人和 subscription 脏了,后面营销质量会一起受损。
3. 忽略机器人防护配置
reCAPTCHA/Turnstile 不只是“防一点垃圾提交”,而是在保护你的发信资产。
4. 不重视 opt-out 的恢复逻辑
如果团队不了解这一层,可能会误以为“重新订阅怎么没新建联系人”,其实这是更正确的做法。
最后总结
website_mass_mailing 在官网侧真正处理的是:
- 订阅身份识别;
- 名单公开边界;
- 联系人与名单关系复用;
- 退订后的状态恢复;
- 公开入口的反机器人保护;
- 多订阅组件之间的连续体验。
所以它不是一个“邮箱收集小表单”,而是:
Odoo 在官网与 Email Marketing 之间设置的一道低摩擦、但尽量不失控的数据闸门。
理解了这一点,你就会明白为什么它的源码既关心 UX,也关心名单权限,还关心 session 和记忆——因为官网订阅真正难的,从来不是按钮文字写“Subscribe”,而是后面的数据要不要还能放心用。
DISCUSSION
评论区