Skip to content

Latest commit

 

History

History
479 lines (370 loc) · 11.9 KB

File metadata and controls

479 lines (370 loc) · 11.9 KB

数据库设计文档

一、设计概述

当前系统已经完成从 phase1 到 phase2 的演化。
phase1 的核心模型是“问卷包含题目”,phase2 的核心模型变为“问卷引用题目版本”。

因此数据库设计也从最初的 3 个核心集合,扩展为当前的 6 个核心集合:

  • users
  • surveys
  • responses
  • question_roots
  • question_versions
  • question_banks

设计原则可以概括为:

  1. 题目资产独立存在,支持复用、共享和版本演化
  2. 问卷保留自己的编排语义,包括顺序、跳转和快照
  3. 答卷既保留 phase1 兼容结构,也支持 phase2 跨问卷统计
  4. 已发布问卷必须保持稳定,不受题目后续修改影响

二、集合设计总览

集合 主要职责
users 用户认证与对象归属
question_roots 题目资产身份层
question_versions 题目具体内容版本层
question_banks 题目组织容器
surveys 问卷元信息与题目引用编排
responses 答卷与统计数据来源

三、users 集合

3.1 Schema

{
  "_id": ObjectId,
  "username": "alice",
  "password_hash": "$2b$12$...",
  "created_at": ISODate("2026-01-01T00:00:00Z")
}

3.2 字段说明

字段 类型 说明
_id ObjectId 主键
username String 用户名,全局唯一
password_hash String bcrypt 哈希
created_at Date 注册时间

3.3 索引

db.users.createIndex({ "username": 1 }, { unique: true })

3.4 设计说明

用户是独立实体,问卷、题目资产和题库都通过 owner_id / creator_id 指向用户。
不嵌入其它集合的原因是:一个用户可能拥有多个问卷、题目和题库,嵌入会产生大量冗余。


四、question_roots 集合

4.1 作用

question_root 表示“同一道题”的长期身份,是 phase2 引入的最关键对象。

它负责解决的问题是:

  • 这道题归谁所有
  • 这道题共享给谁
  • 这道题最新版本是谁
  • 这道题在哪些题库中

4.2 Schema

{
  "_id": ObjectId,
  "owner_id": ObjectId,
  "visibility": "private",
  "shared_with": [ObjectId],
  "latest_version_id": ObjectId,
  "bank_ids": [ObjectId],
  "created_at": ISODate,
  "updated_at": ISODate
}

4.3 字段说明

字段 类型 说明
_id ObjectId 题目资产主键
owner_id ObjectId 所有者
visibility String private / shared / public
shared_with Array<ObjectId> 被共享给的用户列表
latest_version_id ObjectId 当前最新版本
bank_ids Array<ObjectId> 所属题库列表
created_at Date 创建时间
updated_at Date 最近更新时间

4.4 索引建议

db.question_roots.createIndex({ "owner_id": 1, "updated_at": -1 })
db.question_roots.createIndex({ "visibility": 1 })
db.question_roots.createIndex({ "shared_with": 1 })

4.5 设计说明

question_rootquestion_version 分离,是 phase2 支持“同题多版本并存”的基础。
如果把身份和内容揉在一条记录里,就很难同时满足:

  • 同一道题不断演化
  • 历史版本可追溯
  • 不同问卷使用不同版本

五、question_versions 集合

5.1 作用

question_version 表示某一道题在某个时间点的具体内容。

5.2 Schema

{
  "_id": ObjectId,
  "root_id": ObjectId,
  "version": 3,
  "parent_version_id": ObjectId,
  "title": "你的年龄(周岁)",
  "type": "number_input",
  "required": true,
  "options": null,
  "validation": {
    "min_select": null,
    "max_select": null,
    "min_length": null,
    "max_length": null,
    "min_value": 0,
    "max_value": 120,
    "is_integer": true
  },
  "created_at": ISODate
}

5.3 字段说明

字段 类型 说明
_id ObjectId 版本主键
root_id ObjectId 所属题目资产
version Int 版本号,从 1 开始递增
parent_version_id ObjectId | null 父版本,用于构建版本链
title String 题目标题
type String single_choice / multi_choice / text_input / number_input
required Boolean 是否必答
options Array | null 选择题选项
validation Object 校验规则
created_at Date 版本创建时间

5.4 索引建议

db.question_versions.createIndex({ "root_id": 1, "version": 1 }, { unique: true })
db.question_versions.createIndex({ "root_id": 1, "created_at": -1 })

5.5 设计说明

题目内容修改不再是 update 旧记录,而是新增一条 question_version
这样可以保证:

  • 已发布问卷引用的旧版本保持不变
  • 可以查看版本历史
  • 可以从旧版本恢复出一个新的最新版本

六、question_banks 集合

6.1 作用

题库是题目资产的组织容器,不是题目本体。

6.2 Schema

{
  "_id": ObjectId,
  "owner_id": ObjectId,
  "name": "基础人口信息题库",
  "description": "常用的人口统计题目",
  "shared_with": [ObjectId],
  "question_root_ids": [ObjectId],
  "created_at": ISODate,
  "updated_at": ISODate
}

6.3 字段说明

字段 类型 说明
_id ObjectId 题库主键
owner_id ObjectId 所有者
name String 题库名称
description String 描述
shared_with Array<ObjectId> 共享给的用户
question_root_ids Array<ObjectId> 收录的题目资产
created_at Date 创建时间
updated_at Date 最近更新时间

6.4 索引建议

db.question_banks.createIndex({ "owner_id": 1, "updated_at": -1 })
db.question_banks.createIndex({ "shared_with": 1 })

6.5 设计说明

题库只做“组织”,不做“内容所有者”。
题目资产仍由 question_root 统一管理,这样可以避免题目内容、题库组织和问卷编排三种语义混在一起。


七、surveys 集合

7.1 作用

问卷在 phase2 中仍然是核心对象,但其内部 questions[] 的语义已经改变:
它不再保存题目本体,而是保存“题目版本在问卷中的引用关系”。

7.2 Schema

{
  "_id": ObjectId,
  "creator_id": ObjectId,
  "title": "用户满意度调查",
  "description": "请花 2 分钟填写",
  "allow_anonymous": false,
  "allow_multiple_submit": true,
  "status": "draft",
  "display_mode": "progressive",
  "deadline": null,
  "share_code": "abc123",
  "response_count": 0,
  "created_at": ISODate,
  "updated_at": ISODate,
  "questions": [
    {
      "qid": "q1",
      "order": 1,
      "question_root_id": ObjectId,
      "question_version_id": ObjectId,
      "snapshot": {
        "title": "你的年龄",
        "type": "number_input",
        "required": true,
        "options": null,
        "validation": {
          "min_select": null,
          "max_select": null,
          "min_length": null,
          "max_length": null,
          "min_value": 0,
          "max_value": 120,
          "is_integer": true
        }
      },
      "jump_rules": [
        {
          "condition": {
            "type": "simple",
            "operator": "gte",
            "value": 18
          },
          "target": "__order__:3"
        }
      ]
    }
  ]
}

7.3 顶层字段说明

字段 类型 说明
_id ObjectId 问卷主键
creator_id ObjectId 创建者
title String 标题
description String 说明
allow_anonymous Boolean 是否允许匿名
allow_multiple_submit Boolean 是否允许重复提交
status String draft / published / closed
display_mode String progressive / all_at_once
deadline Date | null 截止时间
share_code String 分享码
response_count Int 冗余答卷数
created_at Date 创建时间
updated_at Date 更新时间
questions Array 问卷中的题目引用列表

7.4 questions[] 元素字段说明

字段 类型 说明
qid String 问卷内部标识
order Int 当前问卷中的顺序
question_root_id ObjectId | null 引用的题目资产
question_version_id ObjectId | null 引用的题目版本
snapshot Object 当前问卷冻结的题目内容
jump_rules Array 当前问卷中的跳转规则

7.5 snapshot 的意义

问卷中保留 snapshot,而不是只保存引用,原因是:

  1. 发布后的问卷必须稳定
  2. 问卷渲染时不必每次跨集合拼接题目内容
  3. 旧答卷统计时能对应到当时的题目内容

7.6 跳转规则说明

当前跳转目标支持:

  • __end__
  • __order__:N

并且服务端始终限制为前跳,不允许:

  • 跳到当前题
  • 跳到更早的题
  • 形成循环

7.7 索引建议

db.surveys.createIndex({ "creator_id": 1, "updated_at": -1 })
db.surveys.createIndex({ "share_code": 1 }, { unique: true })
db.surveys.createIndex({ "status": 1 })

八、responses 集合

8.1 作用

答卷仍然独立存储,但 phase2 中增加了标准化回答项,以支持跨问卷统计。

8.2 Schema

{
  "_id": ObjectId,
  "survey_id": ObjectId,
  "respondent_id": ObjectId,
  "submitted_at": ISODate,
  "ip_address": "192.168.1.1",
  "duration_seconds": null,
  "answers": {
    "q1": "",
    "q2": ["苹果", "西瓜"],
    "q3": 25
  },
  "answer_items": [
    {
      "qid": "q1",
      "question_root_id": ObjectId,
      "question_version_id": ObjectId,
      "answer": ""
    }
  ]
}

8.3 字段说明

字段 类型 说明
_id ObjectId 答卷主键
survey_id ObjectId 所属问卷
respondent_id ObjectId | null 填写者
submitted_at Date 提交时间
ip_address String 客户端 IP
duration_seconds Int | null 耗时预留
answers Object phase1 兼容答案结构
answer_items Array phase2 统计索引结构

8.4 为什么同时保留 answersanswer_items

字段 作用
answers 保持 phase1 兼容,便于按问卷内部 qid 读取
answer_items 支持按题目资产或版本跨问卷聚合

这种双结构虽然有冗余,但能明显降低 phase1 -> phase2 迁移成本。

8.5 索引建议

db.responses.createIndex({ "survey_id": 1 })
db.responses.createIndex({ "survey_id": 1, "respondent_id": 1 })
db.responses.createIndex({ "answer_items.question_root_id": 1 })
db.responses.createIndex({ "answer_items.question_version_id": 1 })

九、为什么当前设计适合 MongoDB

9.1 适合的原因

MongoDB 在本项目中的优势主要体现在:

场景 MongoDB 优势
问卷内部引用 + 快照 文档嵌套自然表达
题目校验规则 可灵活存储不同题型的 validation
跳转规则 数组 + 对象结构表达清晰
答卷答案 Map 与数组混合结构方便
版本演化 通过多集合引用表达灵活

9.2 为什么不完全关系化

如果使用关系数据库,需要拆成更多表并做多次 JOIN,尤其在:

  • 问卷渲染
  • 版本引用
  • 跳转与快照展示

这些场景下,查询和组装逻辑都会显著复杂。


十、从 Phase1 到 Phase2 的演化总结

phase1 的数据库设计重点是:

  • 问卷完整读取
  • 答卷独立提交
  • 跳转与校验嵌入题目

phase2 的数据库设计重点则变为:

  • 题目资产独立存在
  • 题目版本可演化
  • 问卷保存引用与快照
  • 答卷支持按题目聚合统计

因此当前数据库设计的最准确概括是:

在保留原问卷主链的基础上,
通过引入题目资产、题目版本、题库和标准化回答项,
把系统从“问卷中心”演化为“问卷 + 题目资产双中心”结构。