先说结论
很多系统一做邮件名单,就把“联系人”和“名单”之间做成一张平平无奇的关联表。但 Odoo 明显不这么想,它给这层关系起了正式模型名,还让它承担退订原因、退订时间等状态语义。
第一层:为什么 mailing.subscription 必须是显式模型
只要名单关系里出现“是否退订”“何时退订”“为什么退订”,它就不再是纯连接关系,而是业务事实。Odoo 把 mailing_subscription 独立成模型,本质上是在承认:联系人和名单之间的关系本身,也是需要被管理和审计的。
第二层:opt_out 为什么依赖上下文里的 active list
mailing.contact 上的 opt_out 是个计算字段,而且明确要求当前上下文里只有一个 default_list_ids。这说明 Odoo 不把“联系人是否退订”理解成全局属性,而是理解成“针对某一张名单是否退订”。如果没锁定当前名单,这个问题本身就没有清晰答案。
第三层:为什么 create 要同步 default_list_ids 与 subscription_ids
mailing.contact.create() 里有一段看似啰嗦的同步逻辑:如果上下文已经带了默认 list,而你只传了 subscription_ids,它会帮你补齐缺失名单。这是在给 UI 和 ORM 打补丁,避免因为入口不同,最终联系人落到的名单关系不一致。
第四层:退订时间为什么自动写入
mailing.subscription 在 opt_out 为真时会自动写 opt_out_datetime,创建和写入时如果给了退订原因或退订时间,也会反推 opt_out=True。这说明退订不是一个单独布尔值,而是一条带时间语义的事件。后续做合规审计或名单清洗时,这个时间点非常有价值。
第五层:这套模型最适合防什么坑
它主要防两类坑:一类是同一联系人在不同名单上的状态混淆;另一类是导入、快速创建、名单页操作等不同入口造成的关系不一致。只有把 subscription 当成正式对象,这些坑才好堵。
最容易误解的三个点
- 误区一:联系人退订就是全局退订。很多业务其实需要按名单分别管理。
- 误区二:many2many 足够了。只要关系上挂状态,就不该再把它当纯连接表。
- 误区三:退订时间不重要。没有时间,你就很难证明某次退订何时发生。
实战上怎么用更稳
- 做邮件名单系统时,优先考虑“联系人-名单关系”是否需要独立建模。
- 如果 UI 入口很多,最好像 Odoo 一样做默认名单同步,减少漏挂名单。
- 做合规或运营复盘时,退订时间和退订原因往往比布尔值更有用。
最后总结
Mailing Contact 真正复杂的地方,不是联系人字段,而是联系人与名单之间那层带状态、带时间、带上下文的关系。Odoo 把它单独建模,是非常清醒的设计。
DISCUSSION
评论区