Files
iboard/docs/V0.2/数据库设计文档.md
T
cheney 9b4705870d docs(V0.2): 新增设计文档 / 数据库设计文档 / 界面设计静态原型
- 设计文档.md: 架构与分层、目录结构、周期口径与边界规则、前端组件与交互、
  后端接口清单、关键流程时序、非功能设计、测试验证、风险与演进
- 数据库设计文档.md: ER、5 张表逐字段、索引与外键级联、关键设计决策
  (date 用 VARCHAR(10) 的原因 / 不冗余计数 / seq 幂等 / 归档语义 / 模板 JSON)、
  容量估算、典型 SQL、迁移与回滚、V0.3 预留
- 界面设计.html: 420x820 静态高保真原型 8 屏 (打卡/日历/清单/新建弹窗/
  完成特效/明天计划视图/清单完成/空态) + 设计规范、组件状态表、动效与适配约定
2026-10-22 14:12:24 +08:00

16 KiB
Raw Blame History

iBoard V0.2 数据库设计文档

项 内容
版本 V0.2
文档状态 已实现(与代码一致)
数据库 MariaDB / MySQL 8(Prisma provider = "mysql")
字符集 utf8mb4 / utf8mb4_unicode_ci
时区约定 应用层统一 Asia/Shanghai,库内 DATETIME 存 UTC
相关文件 prisma/schema.prisma、prisma/migrations/20261022120000_add_checkin_checklist/migration.sql
关联文档 需求文档.md、设计文档.md、界面设计.html

1. 概述

V0.2 新增「打卡 / 日历 / 清单」三个功能域,共引入 5 张表:

表 作用 归属域
Habit 打卡项(每天/每周/每月/每年 n 次的目标定义) 打卡
CheckIn 打卡记录(一次打卡 = 一条) 打卡
CheckList 检查清单(出差/回老家等场景的一次性清单) 清单
CheckListItem 检查项(勾选即完成) 清单
CheckListTemplate 清单模板(系统预置 + 用户自定义) 清单

日历视图不新增表:完全由 Habit + CheckIn 按月聚合得出(见 §7.3)。

1.1 设计原则

  1. 记录为真,进度不落库。 环形进度、完成率、streak 一律由 CheckIn 现场聚合,不在 Habit 上冗余 doneCount 字段——避免并发计数不一致、以及用户中途把「每周 3 次」改成「每周 5 次」后需要回填历史的问题。
  2. 日期以本地日历日字符串存储。 CheckIn.date 用 VARCHAR(10) 存 "YYYY-MM-DD"(Asia/Shanghai 下的日历日),而不是 DATETIME:范围查询走索引、字典序等于时间序、天然幂等边界,且彻底规避「UTC 存 + 本地读」导致的跨零点归属错误。
  3. 软删除优先。 打卡项删除默认走 archived 归档(保留历史统计),只有显式 ?hard=1 才物理删除。
  4. 一切按 userId 隔离。 所有业务表都带 userId,Habit/CheckIn/CheckList/CheckListTemplate 外键 ON DELETE CASCADE 到 User。
  5. 迁移只增不改。 新增需求一律新迁移文件,不修改已执行的迁移(部署走 prisma migrate deploy)。

2. 概念模型(ER)

                    ┌──────────┐
                    │   User   │   (既有表, 本次仅新增反向关系)
                    └────┬─────┘
         ┌───────────────┼────────────────────┬─────────────────┐
         │               │                    │                 │
    ┌────▼────┐     ┌────▼────┐         ┌─────▼─────┐    ┌──────▼──────┐
    │  Habit  │1───*│ CheckIn │         │ CheckList │1─* │CheckListItem│
    └─────────┘     └─────────┘         └───────────┘    └─────────────┘
     打卡项           打卡记录            场景清单           检查项
    (周期+目标n)   (habitId,date,seq)                     (done, doneAt)
                                              ▲
                                              │ 生成(复制条目, 不建外键)
                                        ┌─────┴──────────┐
                                        │CheckListTemplate│
                                        │ (builtin/自定义) │
                                        └────────────────┘

基数说明:

关系 基数 级联
User → Habit 1 : N Cascade
User → CheckIn 1 : N Cascade
Habit → CheckIn 1 : N Cascade
User → CheckList 1 : N Cascade
CheckList → CheckListItem 1 : N Cascade
User → CheckListTemplate 1 : N(可空,系统预置为空) Cascade
CheckListTemplate → CheckList 无外键,创建时复制条目内容 —

3. 表结构

3.1 Habit — 打卡项

周期目标的定义表,不含任何计数。

字段 类型 空 默认 说明
id INT PK 否 AUTO_INCREMENT 主键
userId INT FK 否 — 所属用户
name VARCHAR(191) 否 — 名称,接口校验 1~20 字
period VARCHAR(191) 否 'day' day | week | month | year
target INT 否 1 周期内目标次数 n(校验 ≥1)
color VARCHAR(191) 否 '#1677ff' 环形与标记主色(7 色预设之一)
icon VARCHAR(191) 否 '' emoji / 图标名,V0.2 预留不展示
note VARCHAR(191) 否 '' 备注
archived BOOLEAN 否 false 归档后不出现在待办,历史保留
sortOrder INT 否 0 排序权重(升序)
createdAt DATETIME(3) 否 now() 用于「创建之前的日期不计入月历期望」
updatedAt DATETIME(3) 否 自动 —

索引:INDEX(userId, archived, sortOrder) — 覆盖「取当前用户未归档打卡项」主查询。

3.2 CheckIn — 打卡记录

一次打卡 = 一条记录,同日多次用 seq 递增。

字段 类型 空 默认 说明
id INT PK 否 AUTO_INCREMENT 主键
habitId INT FK 否 — 所属打卡项
userId INT FK 否 — 冗余用户(用于单用户全量区间查询,避免 join)
date VARCHAR(10) 否 — 本地日历日 "YYYY-MM-DD"
seq INT 否 1 当日第几次(1..n,超额继续累加)
note VARCHAR(191) 否 '' 单次备注(接口截断 200 字)
source VARCHAR(191) 否 'user' user | backfill(补打)
createdAt DATETIME(3) 否 now() 打卡时刻(UTC)

约束与索引:

名称 类型 字段 作用
CheckIn_habitId_date_seq_key UNIQUE (habitId, date, seq) 并发重复提交的幂等防线(冲突 → P2002 → 409)
CheckIn_userId_date_idx INDEX (userId, date) 月历按月区间扫全量记录
CheckIn_habitId_date_idx INDEX (habitId, date) 单打卡项周期区间聚合

3.3 CheckList — 检查清单

字段 类型 空 默认 说明
id INT PK 否 AUTO_INCREMENT 主键
userId INT FK 否 — 所属用户
title VARCHAR(191) 否 — 清单名,如「出差 · 杭州」
scene VARCHAR(191) 否 '' 场景:出差 | 回老家 | 其他
note VARCHAR(191) 否 '' 备注
archived BOOLEAN 否 false 归档
createdAt / updatedAt DATETIME(3) 否 — 列表默认按 updatedAt 倒序

索引:INDEX(userId, archived) — 对应「进行中 / 已归档」两个分段列表。

3.4 CheckListItem — 检查项

字段 类型 空 默认 说明
id INT PK 否 AUTO_INCREMENT 主键
listId INT FK 否 — 所属清单
title VARCHAR(191) 否 — 条目名(校验 ≤40 字)
note VARCHAR(191) 否 '' 条目备注
done BOOLEAN 否 false 是否已完成
doneAt DATETIME(3) 是 NULL 勾选时刻;取消勾选置回 NULL
sortOrder INT 否 0 排序(升序,同值按 id)
createdAt / updatedAt DATETIME(3) 否 — —

索引:INDEX(listId, sortOrder)。

3.5 CheckListTemplate — 清单模板

字段 类型 空 默认 说明
id INT PK 否 AUTO_INCREMENT 主键
userId INT FK 是 NULL NULL = 系统预置(builtin=true)
name VARCHAR(191) 否 — 模板名
scene VARCHAR(191) 否 '' 场景标签
items JSON 否 — [{ title, note }],创建清单时展开为 CheckListItem
builtin BOOLEAN 否 false 系统预置:只读、不可删(接口层限制)
createdAt / updatedAt DATETIME(3) 否 — —

索引:INDEX(userId, builtin) — 查询条件为 builtin=true OR userId=me。

系统预置数据(首次访问 GET /api/checklists/templates 时幂等写入):

模板 条目数 条目
出差 11 身份证/证件、手机充电器+充电宝、笔记本电脑+电源、耳机、换洗衣物、洗漱包、常用药、雨伞、车票/机票/酒店确认、工牌/名片、报销单据
回老家 12 身份证/证件、钥匙、手机充电器+充电宝、换洗衣物、洗漱包、常用药、给家人的礼物/伴手礼、长辈的药品/营养品、口罩、现金零钱、车票/加油/行程确认、家里门窗水电检查

4. 外键与级联

子表.字段 父表.字段 ON DELETE ON UPDATE
Habit.userId User.id CASCADE CASCADE
CheckIn.habitId Habit.id CASCADE CASCADE
CheckIn.userId User.id CASCADE CASCADE
CheckList.userId User.id CASCADE CASCADE
CheckListItem.listId CheckList.id CASCADE CASCADE
CheckListTemplate.userId User.id CASCADE CASCADE

删除链路:删用户 → 级联清 Habit/CheckIn/CheckList/Template;删 Habit → 清其全部 CheckIn;删 CheckList → 清其全部 Item。业务层删打卡项默认只置 archived=true,物理删需 ?hard=1(此时先 deleteMany 记录再删主表)。


5. 关键设计决策

5.1 为什么 date 用 VARCHAR(10) 而不是 DATETIME

维度 VARCHAR(10) 本地日历日 DATETIME(UTC)
跨零点归属 明确:由应用层按 Asia/Shanghai 计算一次,落库即定 读时需要再转时区,易错
范围查询 date >= '2026-10-01' AND date <= '2026-10-31' 直接走索引 需函数/边界换算,易失效
幂等边界 UNIQUE(habitId, date, seq) 天然按「日」收敛 需按日截取
字典序 等于时间序,可安全字符串比较 —
存储 10 字节 5 字节(略优,但不足以抵消上述成本)

代价:跨时区展示需自行换算——本项目全链路 Asia/Shanghai,不成为问题。

5.2 为什么不冗余 doneCount / streak

  • 并发:计数需事务 + 行锁,聚合查询天然一致。
  • 改配置:用户把「每周 3 次」改成「每周 5 次」,冗余计数立刻失真,需要全量重算;现方案改 target 立即生效。
  • 撤销:撤销即删一条记录,无需维护计数器。
  • 成本:单个用户打卡项数量在 10~30 量级,月历查询一次拉取整月记录(≤ 数百行)在内存聚合,开销可接受。

5.3 seq 与幂等

同日多次打卡用 seq = max(seq)+1。唯一约束 (habitId, date, seq) 使并发重复提交必然撞 P2002,服务端转 409「该次打卡已记录」,前端幂等收敛,不会出现「点两下加两次」的体验事故。

5.4 归档 vs 物理删除

操作 语义 数据影响
DELETE /api/checkin/habits/:id 归档(默认) archived=true,记录全保留
DELETE /api/checkin/habits/:id?hard=1 物理删除 先删 CheckIn,再删 Habit
DELETE /api/checklists/:id 物理删除(一次性清单,不留历史) 先删 Item,再删 List

理由:打卡是长期行为数据,历史构成 streak 与完成率;清单是短期事务,用完即归档/删除。

5.5 模板用 JSON 而非明细表

模板条目结构固定(title + note)、只在「创建清单」时整体读取并展开、不需要按条目检索或统计 —— 用 JSON 列避免额外的 TemplateItem 表与 join。展开后落到 CheckListItem,之后模板改动不影响已生成清单。


6. 数据量估算

单用户规模假设:打卡项 20 个,平均每天打卡 25 次;清单每月 4 份,每份 12 条。

表 年增量(单用户) 行大小(约) 年占用(约)
CheckIn 25 × 365 ≈ 9,100 ~80 B ~0.7 MB
CheckListItem 4 × 12 × 12 = 576 ~90 B ~0.05 MB
Habit / CheckList / CheckListTemplate < 100 ~200 B 忽略

结论:无需分区、无需归档策略。10 用户 × 5 年 ≈ 45 万行 CheckIn,仍远低于单表瓶颈。若未来接入多租户或做年视图长周期统计,再考虑按月汇总表。


7. 典型查询

7.1 今日待办 + 周期进度

-- 步骤 1: 取未归档打卡项 (走 Habit_userId_archived_sortOrder_idx)
SELECT id, name, period, target, color FROM Habit
WHERE userId = ? AND archived = 0 ORDER BY sortOrder, id;

-- 步骤 2: 取覆盖所有周期窗口的最小区间内的记录 (走 CheckIn_userId_date_idx)
SELECT habitId, date FROM CheckIn
WHERE userId = ? AND date >= ? AND date <= ?;   -- 如 week: '2026-10-19' ~ '2026-10-25'
-- 步骤 3: 内存按 habitId/date 分组 -> 计算 done/percent/streak

7.2 打一次卡

SELECT MAX(seq) FROM CheckIn WHERE habitId = ? AND date = ?;   -- -> seq
INSERT INTO CheckIn (habitId, userId, date, seq, note, source) VALUES (?, ?, ?, ?, ?, 'user');
-- 冲突 -> P2002 -> 409

撤销(撤销本周期最近一次):

SELECT id FROM CheckIn WHERE habitId = ? AND date BETWEEN ? AND ?
ORDER BY date DESC, seq DESC LIMIT 1;
DELETE FROM CheckIn WHERE id = ?;

7.3 月历(日历视图数据来源)

SELECT habitId, date FROM CheckIn
WHERE userId = ? AND date >= '2026-10-01' AND date <= '2026-10-31';

按天聚合后与 Habit.expectedPerDay 比较得出日状态:

状态 条件 展示
full 全部打卡项当日次数 ≥ 日均期望 绿点
over 有任一打卡项当日次数 > 日均期望 紫点
partial 有完成但未全完成 黄点
empty 无记录或当天无打卡项 无点

Habit.createdAt 早于该日的才计入期望,避免新建打卡项把历史日期标成「未完成」。

7.4 清单列表(带进度)

SELECT l.*, i.* FROM CheckList l
LEFT JOIN CheckListItem i ON i.listId = l.id
WHERE l.userId = ? AND l.archived = ?
ORDER BY l.updatedAt DESC, i.sortOrder, i.id;

8. 迁移

项 内容
迁移目录 prisma/migrations/20261022120000_add_checkin_checklist/
内容 5 张 CREATE TABLE + 6 条 ADD CONSTRAINT(见 migration.sql)
生成方式 手写 SQL(与 prisma migrate diff 产出格式一致),已通过 prisma migrate deploy 验证
部署命令 npx prisma migrate deploy && npx prisma generate
回滚 无自动回滚。应急:DROP TABLE CheckIn, CheckListItem, CheckList, CheckListTemplate, Habit;(注意外键顺序)并把 _prisma_migrations 中该记录删除

V0.2 全为新增表与新增字段(User 侧仅新增 Prisma 反向关系,不产生 DDL),对既有数据零影响。


9. V0.3 预留

方向 可能的表变更
打卡提醒 Habit 增加 remindAt TIME、remindDays
打卡统计快照 新增 HabitDailyStat(userId, date, done, expected) 汇总表,用于年视图/热力图加速
清单共享协作 新增 CheckListShare(listId, userId, role)
模板市场 CheckListTemplate 增加 visibility、useCount

以上均可通过「新增表 / 新增可空字段」完成,不影响 V0.2 数据结构。