先说结论
Odoo Survey 的核心,不是“支持多少题型”,而是它把问卷结构和作答过程分成了两层:
survey.question负责描述题目结构survey.user_input负责记录某个人的一次作答过程
更有意思的是,survey.question 不只表示“问题”,还同时表示“页面 / section”。
从 /home/ubuntu/odoo-temp/addons/survey/models/survey_question.py 与 survey_user_input.py 来看,这个设计虽然一开始会让人绕,但它解决了三个很实际的问题:
- 页面与题目可以放在一条有序链上统一编排
- 随机抽题和触发显示可以围绕同一结构计算
- 同一个问卷可以有多次作答记录,并且精确限制 attempts
所以 Odoo 问卷真正的重点不是“做表单”,而是:
如何把一套问卷结构,稳定地投射成一次次可追踪、可评分、可限次的作答会话。
第一层:为什么页面和问题要共用 survey.question
源码注释已经直接说明了这一点:
- 页面本质上也是问卷结构里的一个节点
- 它只是
is_page = True - 这样页面和问题就能一起挂在
question_and_page_ids里,统一排序和拖拽
这背后最重要的收益,是结构编排变简单。
如果页面和问题分成两张完全独立的表,系统在处理这些场景时会很麻烦:
- 页面之间插题
- 题目拖动到别的 section
- 随机题只在某个页面范围内抽
- 根据上题答案控制后续页面或问题显示
而共用一个模型后,页面与题目都变成“有顺序的节点”,系统只需要维护一条结构链,再通过计算字段把它重新映射成更好理解的视图:
- 一个页面有哪些问题
- 某个问题属于哪一页
- 哪些节点能作为触发题
所以这不是“模型设计偷懒”,而是在用统一结构换更强的编排能力。
第二层:随机抽题为什么离不开“页面也是节点”
survey.question 里有几个字段特别关键:
questions_selectionrandom_questions_countpage_idsequence
这说明 Odoo 的随机抽题不是简单地“全卷乱抽”,而是更接近:
- 先按页面 / section 切分结构
- 再在局部范围内做抽取
- 最终保持页面与问题的展示顺序可解释
这对考试、测评和培训问卷很重要。
因为真正可用的随机化,一般都需要满足两个条件:
1. 题目乱,但结构不能乱
你可以随机抽题,但不能把“说明页、单选题、矩阵题、结果页”打成一锅粥。
2. 随机范围可控
有时只是某一节想随机抽 5 题,而不是整份问卷随机 5 题。
页面和问题共模之后,Odoo 才能比较自然地做这种局部随机化。
第三层:触发显示为什么很强调“问题顺序”
triggering_question_ids、allowed_triggering_question_ids、triggering_answer_ids 这组字段能看出,Odoo 对条件显示不是随便放开的。
源码里对触发题有明显限制:
- 触发题必须在当前题之前
- 同一问卷内部才允许引用
- 主要面向可选题型的答案触发
这背后的产品逻辑很合理。
如果允许一个问题去依赖后面的问题,就会出现前端根本无法稳定渲染的情况:
- 还没答到后题,前题要不要显示?
- 切页时如何回溯?
- 随机抽题后依赖关系会不会失效?
所以 Odoo 其实在强迫实施方接受一个原则:
问卷条件逻辑必须是“从前往后”的。
这让前端展示、会话恢复、随机化处理都更稳。
第四层:为什么一次作答要单独建 survey.user_input
很多人初看 Survey 会疑惑:
- 问卷结构已经有了
- 为啥还要单独弄一张
survey.user_input - 为什么答案不直接挂在问卷上
答案很简单:
一次作答本来就是一个独立业务对象。
在 survey.user_input 里,官方把这些东西都放进来了:
start_datetime/end_datetimedeadlinestateaccess_tokeninvite_tokenpartner_id/emailattempts_count/attempts_numberscoring_total/scoring_percentage
这意味着 Odoo 不把“答题”看成问卷的附属小字段,而是看成一次完整会话。
它可以:
- 被邀请访问
- 被限制时间
- 允许或限制重答
- 输出分数
- 打印答案
- 按联系人追踪历史尝试
这也是为什么 Survey 能同时覆盖:
- 简单收集表单
- 正式测验
- 培训认证
- 限时 live session
第五层:答题次数限制到底是怎么想的
_compute_attempts_info() 非常值得实施时细读。
它不是随便给一条“答过几次”的数字,而是基于:
- 问卷本身是否开启 attempts limit
- 该次答卷是否已经
done - 是否是测试条目
test_entry partner_id/emailinvite_token
来计算同一个人到底是第几次答、总共答了多少次。
这套逻辑说明 Odoo 对“同一个人”的判断其实是业务化的:
- 有联系人就优先按联系人
- 没联系人可以按邮箱
- 某些邀请场景还要结合 invite token
也就是说,attempts 管理不是前端按钮级别的限制,而是后端会话识别策略。
这很重要,因为现实里常见这些情况:
- 同一个人刷新页面继续答
- 邮件邀请链接下允许某一组重答
- 培训测试需要严格识别同一参与者
如果没有 survey.user_input 这一层,这些边界会非常难做干净。
第六层:计时为什么也放在作答对象上
在 survey.user_input 里,系统会根据:
- 问卷级时间限制
- 会话开始时间
- live session 题目级时间限制
来判断:
survey_time_limit_reachedquestion_time_limit_reached
这个设计说明一个关键事实:
时间限制不是题目静态属性,而是“某个人在某次作答里是否已经超时”。
所以它必须依赖作答对象,而不是只靠问卷定义。
换句话说,同一份问卷结构可以保持不变,但不同人的作答状态会完全不同。
最容易误解的三个点
误区一:页面只是前端 UI 概念
不是。 在 Odoo Survey 里,页面是结构节点,直接影响排序、随机化与显示逻辑。
误区二:attempts 限制只是“提交后不让再进”
也不是。 它背后是整套用户识别和作答历史计算机制。
误区三:随机抽题就是把题库打乱
Odoo 的随机化更偏向“在结构内有边界地随机”,不是粗暴洗牌。
实施和开发时最该注意什么
如果你要扩展 Survey,最容易踩坑的地方有三个:
- 不要轻易把页面和问题拆模,否则随机化和触发显示会一起变复杂。
- 不要只在控制器层做 attempts 限制,真正可信的边界在
survey.user_input。 - 做条件显示时一定保证依赖顺序向前,否则前后端都容易出现状态错乱。
最后一句
Odoo Survey 之所以看起来“绕”,不是因为它设计混乱,而是因为它同时在解决三类事情:
- 问卷结构怎么表达
- 单次作答怎么跟踪
- 限时、限次、随机和条件显示怎么共存
当你接受“页面也是问题节点、作答也是独立对象”这两个前提后,整套设计其实就很顺了。
DISCUSSION
评论区