先说结论
Odoo 里的问卷邀请,不是“生成一个公开链接,然后群发出去”那么简单。
从 /home/ubuntu/odoo-temp/addons/survey/wizard/survey_invite.py 和 models/survey_survey.py 看,真正发生的链路是:
- 向导先根据 survey access mode 判断可不可以走邮件邀请
- 再把 partner 与手填 email 做一轮归并
- 已存在的答卷对象要区分是重发还是新建
- 对每个有效接收者创建或复用
survey.user_input - 每个答卷对象再拿到自己的
answer_token/invite_token语义 - 最后模板按语言上下文渲染出真正发送的邮件
所以 Survey Invite 本质上是一个“收件人解析 + 答卷实例生成 + 按对象渲染邮件”流程,不是单纯 mail merge。
第一层:send_email 为什么不是随便都能勾
向导里 send_email 不是自由布尔值,而是由 _compute_send_email() 根据 survey_access_mode 算出来:
- 当
survey_access_mode == 'token'时,默认走邮件邀请
这已经说明一个设计原则:
真正适合被“定向邀请”的,是 token 型问卷,而不是所有问卷。
如果问卷本来是 public 访问,系统并不强调“给每个人单独发一个受控入口”;但 token 模式就不同了,每个受邀者最好对应一个明确的答卷对象。
第二层:为什么外部邮箱有时一输就报错
_onchange_emails() 和 _onchange_partner_ids() 都在做资格校验。
典型限制包括:
- 如果问卷要求登录,且不允许 signup,就不能随便填外部邮箱
- 如果选择了 partner,但这些 partner 没有关联 user account,同样可能被拒
这里背后的业务边界非常明确:
- authentication / internal 这类问卷,不是简单地“谁有链接谁都能答”
- 邀请向导必须先替你把访问资格守住
所以很多现场报错并不是邮箱格式问题,而是:
- 这个问卷压根不允许外部参与
- 或者当前参与者没有账号,不满足登录约束
第三层:Odoo 为什么要先把 email 反查成 partner
action_invite() 里一个很值得注意的动作是:
- 对手填邮箱先做
email_normalize - 再去
res.partner里搜索相同邮箱 - 如果找到了 partner,就把它并入
valid_partners - 找不到的,才留在
valid_emails
这背后的意义不只是“数据更整洁”,而是尽量把邀请对象绑定回业务主体。
一旦绑定成 partner,后续你就能更稳定地拿到:
- 语言
- 姓名
- 历史邀请记录
- 已有答卷关联
这比把所有人都当匿名邮箱强得多。
第四层:为什么重复邀请不会无限造新 token
_prepare_answers() 会先查 survey.user_input:
- 同一个 survey 下
- partner_id 命中,或 email 命中
然后 _get_done_partners_emails() 再根据 existing_mode 决定怎么处理。
如果 existing_mode == 'resend':
- 不会为同一个对象无限新建答卷
- 而是挑该对象最新的一条 existing answer 来发
这个设计很实用,因为它避免了两个问题:
1. 一个客户被你重复邀请 5 次,就出现 5 个平行答卷对象
后续统计和追踪会很乱。
2. 同一个人每次重发都换链接
这样客户不知道该用哪个,内部也很难追踪“到底是哪个邀请完成了”。
所以默认重发模式本质上是在说:
重发是提醒,不是重新开一条完全新的答卷人生。
第五层:真正的 token 是在哪一步生成的
答案在 survey.survey._create_answer()。
这个方法会先做 _check_answer_creation(),确保:
- 问卷还 active
- access mode 与当前对象身份相符
- attempts left 还没用完
然后才构造 survey.user_input。
这里有两个容易混淆的概念:
1. answer_token
它属于答卷对象本身,最终问卷开始链接通常会带:
?answer_token=...
2. invite_token
当问卷限制尝试次数,且访问模式不是 public 时,系统还可能生成单独的 invite_token 来约束邀请语义。
也就是说,发出去的不是“共享一个问卷地址”,而是每个接收者对应一个答卷身份入口。
这正是 Odoo 能做:
- 邀请级追踪
- 对象级统计
- attempts 控制
- 已答 / 未答状态区分
的基础。
第六层:邮件模板为什么会因收件人不同而不同
向导会把 render_model 指到 survey.user_input,然后 _send_mail() 不是按 survey 渲染,而是按 answer 渲染。
这件事特别关键,因为只有这样模板里像 object.get_start_url() 这类表达式,才能拿到当前答卷对象自己的专属链接。
此外,向导还会:
- 根据 partner 语言上下文切换模板语言
- 用 answer 级别渲染 subject / body
- 把 attachment、layout 一起包进去
所以它不是“一个模板发给很多人”,而是“同一模板针对很多答卷对象逐个实例化”。
第七层:为什么有时明明写了邮箱,最后却提示没有有效接收者
action_invite() 的最后会检查:
valid_partnersvalid_emails
如果两边都空,就直接报:Please enter at least one valid recipient.
出现这种情况,常见原因有:
- 邮箱格式在 normalize 后无效
- 问卷访问规则不允许这些对象
- 你填的是重复值,但 existing 处理方式又没给出可发送对象
- 手工输入内容被分号 / 换行拆开后,实际上没有留下合法邮箱
所以排错时不要只盯着“我明明填了东西”,而要看“填进去的东西最后有没有成功落到 partner 或 email 两类有效集合里”。
第八层:实施时的实用建议
1. 需要对象级追踪,就优先使用 token 模式邀请
这样每个受邀者都有独立答卷对象,后续统计最稳。
2. 有客户主数据时,尽量让邮箱命中 partner
不要把所有受访者都当匿名邮件地址。
3. 对重复提醒,默认用 resend 思路
除非你就是想重开一次独立作答机会,否则别轻易制造平行答卷。
4. 访问资格要和问卷模式一起设计
internal、authentication、public、token,这些模式决定的不是 UI 文案,而是整条邀请链能不能成立。
最后一句
Odoo Survey Invite 真正高明的地方,不是“会发邮件”,而是它把问卷邀请拆成了四件事:
- 识别谁在被邀请
- 判断他有没有资格被邀请
- 为他创建或复用哪一个答卷对象
- 再把这份对象级入口渲染成邮件发出去
所以你应该把它理解成:
邀请系统不是在发送链接,而是在发送“属于这个接收者的答卷入口”。
DISCUSSION
评论区