先说结论
Odoo Survey 的统计页并不是“统一拿答案表 count 一遍”。
它的真实设计是:题型先决定统计结构,统计结构再决定图表长相。
也就是说,结果页看起来“每道题长得不一样”,不是因为前端随便画,而是因为后端在 survey.question 里先把不同题型的数据组织成了不同的数据骨架。
第一层:为什么统计入口放在 question 上,而不是 report 上
survey_question.py 里的 _prepare_statistics() 很关键。
它不是从“整份问卷报告”出发,而是逐题处理:
- 找到当前题目对应的 user input lines
- 区分答案行、评论行、跳过行
- 计算 summary 数据
- 再按题型生成 table_data 和 graph_data
这意味着 Odoo 把“统计”理解成题目模型自身的行为,而不是一个独立报表引擎。
这样做的好处是:每增加一种题型,就可以在题目模型内部定义自己的统计语义,而不必把所有特殊逻辑堆到统一报表层。
第二层:为什么 _get_stats_data() 是整个分流器
_get_stats_data() 本质上是统计分发器。
它会根据 question_type 走不同分支:
simple_choice:按建议答案统计multiple_choice:也是按答案统计,但图表包一层 question keymatrix:走矩阵专属 graph builderscale:走量表专属计数逻辑- 其他文本 / 数值 / 日期题:不做统一图表,而是保留原始答案列表
这说明 Odoo 非常清楚:题型不同,统计对象也不同。
例如:
- choice 题统计的是“选项被选次数”
- matrix 题统计的是“行 × 列组合次数”
- scale 题统计的是“数值刻度分布”
- text 题统计的是“回答明细”,不是频数图
如果硬要用同一种结构去装,最后一定会丢信息。
第三层:为什么矩阵题必须单独走 _get_stats_graph_data_matrix()
矩阵题最能体现 Odoo 的分层思路。
在 _get_stats_graph_data_matrix() 里,系统先拿到:
- 所有列候选
suggested_answer_ids - 所有行
matrix_row_ids
然后建立 (row, answer) 二维计数字典。
这就把矩阵题从普通单选题里彻底区分出来了。
因为矩阵题真正想表达的不是“哪一个选项最常被选”,而是:
- 每一行分别被怎样评价
- 每一列在所有行里的分布如何
所以最后产出的:
table_data是按行展开,每行内再包含若干列计数graph_data是按列分组,每组里再列出所有行的计数
这不是“写得复杂”,而是矩阵题天然就是二维结构。
第四层:为什么量表题不复用 suggested answers
_get_stats_data_scale() 里有个很明显的设计判断:scale 题直接按 range(scale_min, scale_max + 1) 生成统计桶,而不是去读 survey.question.answer。
这代表在 Odoo 看来,量表题和普通选择题虽然界面都像“选一个值”,但数据语义并不一样:
- choice 的核心是“选项对象”
- scale 的核心是“数值区间”
所以 scale 统计天然更接近分布图,而不是标签计数图。
这也是为什么 scale 题还能直接复用 _get_stats_summary_data_numerical() 去算最大值、最小值、平均值。
第五层:为什么 summary data 不是每题都一样
_get_stats_summary_data() 会继续根据题型追加不同摘要:
- choice 题:正确人数、部分正确人数
- numerical / scale:最大值、最小值、平均值
- numerical / date / datetime / scale:常见值与 scored 汇总
这里最重要的信号是:Odoo 不追求一个“统一摘要模板”,而是让摘要跟着题型能力走。
这比很多系统强行让所有题共享同一套 KPI 更合理。
因为对文本题来说,“平均值”毫无意义;但对量表题来说,平均值和最常见评分就非常关键。
第六层:为什么 survey.user_input 还要再做一次 _prepare_statistics()
很多人看到 survey.question 已经准备统计,就会疑惑:为什么 survey_user_input.py 里还有一个 _prepare_statistics()?
因为这两个统计层级根本不一样。
survey.question._prepare_statistics()
它处理的是按题聚合后的整体报告,也就是管理员在后台看到的结果分布。
survey.user_input._prepare_statistics()
它处理的是单份答卷的得分拆解,重点是:
- 每个 section 对应多少题
- 正确 / partial / incorrect / skipped 各多少
- 整份答卷 totals 如何展示
换句话说:
- question 统计回答“大家整体怎么答”
- user_input 统计回答“某个人这次答得怎么样”
这两者不是重复,而是两种完全不同的观察视角。
第七层:为什么 partial 这种状态必须单独存在
在 survey.user_input._prepare_statistics() 里,多选题会专门判断 partial。
这点很重要,因为现实考试并不总是“全对或全错”。
如果一个用户选中了部分正确答案,但没有全选对:
- 统计上它不该被算成完全正确
- 也不该被粗暴打成完全错误
partial 的存在,让 Odoo 能在测评题里表达“部分掌握”。
这对培训考试、认证试卷、内部测评都很有价值。
实战里最容易误解的地方
1. 以为所有题都应该出柱状图
文本题、日期题本来就更适合明细展示,而不是统一画 chart。
2. 以为 scale 和 single choice 可以共用同一套统计
界面像,不代表数据语义像。量表题是数值分布,不是标签对象分布。
3. 以为后台题目报告和个人答卷报告是一回事
前者按题聚合,后者按答卷评分,目标完全不同。
4. 忽略 skipped 与 comment 行
Odoo 在统计时会显式区分跳过答案、评论行、有效答案行,不是所有 user_input_line 都一锅炖。
结论
Odoo Survey 结果页之所以成熟,不是因为图表多,而是因为它先把不同题型的统计语义拆清楚了。
如果你要做二开、导出、外部 BI 接口,最该尊重的是这几层边界:
_prepare_statistics():逐题装配统计上下文_get_stats_data():按题型分流_get_stats_graph_data_matrix():处理二维矩阵结构_get_stats_data_scale():处理数值量表分布survey.user_input._prepare_statistics():处理单份答卷得分视角
只要把这条链理解透,Survey 的统计为什么“每题长得不一样”,你就不会再觉得它是前端随机发挥了。
DISCUSSION
评论区