phase1 的问题模型是:
问卷包含题目
phase2 的问题模型是:
问卷引用题目版本
这意味着当前系统里有三层不同语义:
question_root- 表示“同一道题”的稳定身份
question_version- 表示该题某个具体版本的内容
survey.questions[]- 表示某问卷里使用了哪个版本,以及问卷内的顺序和跳转关系
用户要求同时满足:
- 题目可复用
- 题目可修改
- 旧问卷不能被污染
- 可以找回旧版本
因此系统不能原地覆盖题目内容,只能新增版本。
- 创建题目时同时创建
root + version1 - 修改题目内容时创建新版本
- 恢复旧版本时也会创建新版本
question_root.latest_version_id指向当前最新版本- 问卷始终引用具体
question_version_id
问卷中的每道题保存:
{
"qid": "q1",
"order": 1,
"question_root_id": "...",
"question_version_id": "...",
"snapshot": {
"title": "...",
"type": "...",
"options": [],
"validation": {}
},
"jump_rules": []
}这样即使题目资产继续演化:
- 已发布问卷仍保留原来的
snapshot - 已收集的数据仍能对应当时的题目内容
题目支持三种可见性:
privatesharedpublic
权限规则:
- owner 可查看、改共享、创建版本、恢复版本
- shared user 可查看、可在自己的问卷里引用
- public user 可查看、可引用
- 非 owner 进入题目详情页时只展示只读界面
题库是题目资产的组织容器,不是题目内容本身的一部分。
题库当前支持:
- 创建题库
- 向题库加入题目
- 从题库移出题目
- 从题库页进入问卷编排
- 共享用户只读查看题库
问卷编辑页不再直接维护题目本体内容,而只负责:
- 问卷元信息
- 选题
- 排序
- 跳转
- 切换引用版本
问卷可以从两类来源选题:
- 题目资产列表
- 题库
加入问卷时默认引用所选题目的最新版本,也允许后续切换到同一题的其他版本。
排序由后端统一处理,而不是前端自己改 order。
当前行为:
- 上移/下移会整体重排
questions[] order始终被重新编号为1..n- 排序后所有
__order__:N类型的跳转目标会被重写到新的题序 - 若排序会导致某条跳转变成后退跳转,则操作直接拒绝
系统现在支持两种跳转目标:
__end____order__:N
phase2 前端问卷编排页默认使用“跳到第 N 题”的形式,而不再暴露对内部 qid 的依赖。
跳转规则始终是前跳:
- 不允许跳到自己
- 不允许跳到当前题之前
- 不允许形成循环
这套约束在:
- 新增题目
- 修改跳转
- 排序
- 删题
这些路径上都会重新校验。
填写页的跳转引擎仍由:
jump_service.pystatic/js/fill.js
共同实现,并保持一致语义。
phase2 下答卷同时保留两套信息:
answers- 保持 phase1 兼容
answer_items- 为跨问卷单题统计提供稳定索引
answer_items 中记录:
qidquestion_root_idquestion_version_idanswer
当前支持两类统计:
GET /api/surveys/{survey_id}/statsGET /api/surveys/{survey_id}/stats/{qid}
GET /api/questions/{root_id}/stats- 可选
version_id参数按版本筛选
- 单选题:每个选项人数、总回答人数
- 多选题:每个选项被选次数
- 文本题:返回填写内容列表
- 数字题:返回填写内容与平均值
校验逻辑的目标不是简单判断“有没有填”,而是保证:
- 不同题型的约束能统一表达
- 被跳过的题目不被错误校验
- 前后端保持一致语义
- phase1 与 phase2 问卷结构都能继续工作
系统当前校验以下内容:
- 单选题是否作答
- 多选题最少/最多选择数量
- 文本题最短/最长长度
- 数字题最小值/最大值
- 数字题是否必须为整数
校验规则来自题目内容本身:
- phase1 内嵌题直接来自
questions[].validation - phase2 引用题来自
snapshot.validation
这保证已发布问卷永远按问卷冻结时的规则校验,而不是按题目资产最新版本校验。
- 前端负责即时提示和基础交互体验
- 后端负责最终校验与拒绝非法提交
这样即使前端被绕过,后端仍然能保证数据合法。
在存在跳转逻辑的情况下,一部分题目本来就不会出现在当前填写路径中。
因此系统在提交时会先根据跳转路径判断“哪些题是本次实际经过的题”,然后只校验这些题。
否则会出现:
- 用户根本没看到某题
- 但提交时却因为该题必答而报错
- 后端校验主逻辑:
app/services/validation_service.py - 填写页联动提示:
static/js/fill.js
当前实现已经把以下约束固化成代码和回归测试:
- 题目必须独立存在
- 题目内容必须通过版本演化
- 已发布问卷不因新版本出现而改变
- 不同问卷可以同时使用同一道题的不同版本
- 共享用户可以使用共享题目,但不能修改题目资产
- 共享用户查看共享题目页和题库页时只看到只读界面
- 题目资产与版本:
app/services/question_service.py - 题库:
app/services/question_bank_service.py - 问卷引用与排序:
app/services/survey_service.py - 答卷与
answer_items:app/services/response_service.py - 统计:
app/services/stats_service.py - 问卷填写跳转:
app/services/jump_service.py、static/js/fill.js - 问卷编排前端:
templates/survey_edit.html、static/js/survey_edit.js