先说结论
Odoo 问卷最容易被低估的,不是题型,而是它对“谁可以答、什么时候还能答、失败后还能不能再来一次”这件事控制得非常细。
从 /home/ubuntu/odoo-temp/addons/survey/controllers/main.py 和 models/survey_user_input.py 看,Survey 真正的骨架是:
survey.survey:定义问卷survey.user_input:定义一次具体作答access_token/invite_token:定义访问身份deadline/attempts_limit:定义时间与次数边界
所以 Survey 本质上不是“公开表单”,而是一个带会话和权限约束的答题系统。
第一层:为什么问卷访问判断要分 survey token 和 answer token
控制器里最关键的入口之一是 _fetch_from_access_token() 与 _get_access_data()。
Odoo 会同时区分两种 token:
survey_token:定位哪份问卷answer_token:定位某个人的那次答卷
这两个 token 分开,意义非常大。
因为现实里有两种完全不同的访问场景:
场景一:公开问卷
你只需要知道是哪份问卷,系统可以现场生成一条新的 survey.user_input。
场景二:被邀请答题 / 继续作答 / 打开旧答卷
这时你不能只知道问卷本身,还得知道“你对应的是哪一次答卷”。
如果把这两个概念混在一起,系统就很难同时支持:
- 公开访问
- 定向邀请
- 作答中断后恢复
- 重试时延续同一邀请上下文
第二层:为什么 Odoo 要把 validity_code 做成一整套业务判断,而不是一个布尔值
_check_validity() 返回的不是简单 true/false,而是像下面这样的业务码:
survey_wrongsurvey_authsurvey_closedsurvey_voidtoken_wrongtoken_requiredanswer_deadlineanswer_wrong_user
这说明 Odoo 不只是想判断“能不能进”,而是想知道为什么不能进。
这在问卷场景里特别重要,因为“不能继续答”背后的含义完全不同:
- 问卷不存在
- 需要登录
- 问卷已关闭
- 链接错了
- 这个答案链接过期了
- 当前登录人与答卷绑定人不匹配
如果这些情况都只显示成一句“访问失败”,用户体验和排障体验都会很差。
所以官方在控制器层把错误语义拆细,是很成熟的设计。
第三层:为什么 cookie 也参与作答恢复
survey_start() 里有个很容易被忽略的细节:
- 如果 URL 里没给
answer_token - 系统会尝试读取 cookie 中的
survey_<survey_token>
这意味着 Odoo 把“继续答上次没写完的问卷”视为默认需要支持的用户行为。
这很现实。
因为真实用户经常会:
- 刷新页面
- 关掉浏览器再回来
- 从邮件点进问卷后中途离开
如果没有 cookie 恢复机制,用户就很容易多生成几条 user_input,后台也会多出很多半截答卷。
更妙的是,Odoo 还会在 cookie 对应的 token 已失效、或与当前登录用户不匹配时重新回退判断,而不是盲目相信 cookie。也就是说,系统在“方便恢复”和“避免串号”之间做了平衡。
第四层:为什么 deadline 是挂在 user_input 上,而不是 survey 上
survey.user_input 有一个非常关键的字段:deadline。
控制器里判断访问合法性时,检查的是:
- 当前这条 answer 有没有 deadline
- deadline 是否已经早于现在
这代表 Odoo 的设计不是“整份问卷统一截止”,而是允许每次答卷拥有自己的截止时间。
这个设计非常有价值,因为很多企业问卷根本不是全员同一规则:
- 某批学员三天内完成
- 某个候选人 24 小时内完成
- 某次补考沿用原 invite token 但保留原截止时间
如果 deadline 只在问卷主表上,你几乎做不了这些差异化控制。
第五层:retry 为什么要保留 invite_token、deadline 和 nickname
survey_retry() 会在允许重试时创建一个新的 survey.user_input,但又不会彻底从头来过。
因为 _prepare_retry_additional_values() 明确保留了:
deadlinenickname
再加上创建时还会继续带上:
partneremailinvite_tokentest_entry
这说明 Odoo 对“重试”的理解不是重新生成一个毫无关系的答卷,而是:
在同一邀请上下文、同一用户身份、同一时间边界下,再开一次新的尝试。
这特别适合认证考试和培训测验。
因为你通常想允许用户重答,但并不想:
- 重答后绕开原来的截止时间
- 失去邀请批次的追踪关系
- 把排行榜昵称、参与者身份搞丢
第六层:attempts 统计为什么要同时看 partner、email 和 invite token
_compute_attempts_info() 不是简单按 user_input 数量来计数。
它会关注:
- 问卷是否开启 attempts limit
- 当前答卷是否已
done - 是否是
test_entry partner_id或emailinvite_token
这里最精妙的一点是:
- 同一个联系人 / 邮箱,才可能算同一个人
- 但如果有 invite token,还要结合 invite token 看是不是同一池次
这意味着 Odoo 默认接受这样一种业务现实:
- 同一个邮箱可能来自不同邀请批次
- 同一个人也可能在不同上下文里拥有不同尝试池
所以 attempts 不是前端按钮逻辑,而是后台会话识别逻辑。
第七层:打印答案为什么也要走 token,而不是后台直接打开记录
action_print_answers() 生成的是一个带 answer_token 的网站 URL。
这说明在 Odoo 里,“查看某次答卷”依然被当成 token 驱动的受控访问,而不是纯后台字段展示。
这个思路很统一:
- 答卷对象可以被邀请、被恢复、被重试
- 也可以被打印、被访问
- 这些行为都围绕同一条 user_input 展开
这样一来,前后端的访问语义是一致的。
最容易误解的三个点
误区一:Survey 只是发一个公开链接
不是。 公开链接只是其中一种模式,完整系统还支持邀请式、恢复式、限次式访问。
误区二:截止时间只需要设在问卷上
不够。 很多真实场景需要每次答卷自己的 deadline。
误区三:retry 等于重新开始
也不是。 Odoo 的 retry 更像“保留身份和边界条件,再开新一轮尝试”。
实施和扩展时怎么用最稳
如果你在做 Survey 项目,我很建议把这几条当成默认原则:
- 需要精确控制对象时,优先走 answer token,而不是只发 survey 公链
- 需要补考、重试时,先想清楚要不要沿用 invite token 与原 deadline
- 不要只在前端限制 attempts,真正可信的边界在
survey.user_input - 用户反馈“明明点过链接却进不去”时,优先排查 validity_code 对应的具体原因
- 对登录用户、公开用户、邀请用户三类流程分别做回归测试
最后总结
Odoo Survey 真正厉害的地方,不在题型数量,而在它把问卷访问设计成了一套可控会话:
- survey token 负责找到问卷
- answer token 负责找到这次答卷
- cookie 负责恢复现场
- deadline 负责时间边界
- invite token 与 attempts 负责次数边界
理解这条链之后,你就会发现 Odoo Survey 离“考试系统”和“邀请式测评系统”其实只差配置,不差底层骨架。
DISCUSSION
评论区