From 9b4705870d4a7ce5bb68def91d41e8026a8bcdbc Mon Sep 17 00:00:00 2001 From: cheney Date: Thu, 22 Oct 2026 14:12:24 +0800 Subject: [PATCH] =?UTF-8?q?docs(V0.2):=20=E6=96=B0=E5=A2=9E=E8=AE=BE?= =?UTF-8?q?=E8=AE=A1=E6=96=87=E6=A1=A3=20/=20=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1=E6=96=87=E6=A1=A3=20/=20=E7=95=8C=E9=9D=A2?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1=E9=9D=99=E6=80=81=E5=8E=9F=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 设计文档.md: 架构与分层、目录结构、周期口径与边界规则、前端组件与交互、 后端接口清单、关键流程时序、非功能设计、测试验证、风险与演进 - 数据库设计文档.md: ER、5 张表逐字段、索引与外键级联、关键设计决策 (date 用 VARCHAR(10) 的原因 / 不冗余计数 / seq 幂等 / 归档语义 / 模板 JSON)、 容量估算、典型 SQL、迁移与回滚、V0.3 预留 - 界面设计.html: 420x820 静态高保真原型 8 屏 (打卡/日历/清单/新建弹窗/ 完成特效/明天计划视图/清单完成/空态) + 设计规范、组件状态表、动效与适配约定 --- docs/V0.2/数据库设计文档.md | 325 +++++++++++++ docs/V0.2/界面设计.html | 889 ++++++++++++++++++++++++++++++++++++ docs/V0.2/设计文档.md | 338 ++++++++++++++ 3 files changed, 1552 insertions(+) create mode 100644 docs/V0.2/数据库设计文档.md create mode 100644 docs/V0.2/界面设计.html create mode 100644 docs/V0.2/设计文档.md diff --git a/docs/V0.2/数据库设计文档.md b/docs/V0.2/数据库设计文档.md new file mode 100644 index 0000000..003a9d4 --- /dev/null +++ b/docs/V0.2/数据库设计文档.md @@ -0,0 +1,325 @@ +# 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 今日待办 + 周期进度 + +```sql +-- 步骤 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 打一次卡 + +```sql +SELECT MAX(seq) FROM CheckIn WHERE habitId = ? AND date = ?; -- -> seq +INSERT INTO CheckIn (habitId, userId, date, seq, note, source) VALUES (?, ?, ?, ?, ?, 'user'); +-- 冲突 -> P2002 -> 409 +``` + +撤销(撤销本周期最近一次): + +```sql +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 月历(日历视图数据来源) + +```sql +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 清单列表(带进度) + +```sql +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 数据结构。 diff --git a/docs/V0.2/界面设计.html b/docs/V0.2/界面设计.html new file mode 100644 index 0000000..0ad34c2 --- /dev/null +++ b/docs/V0.2/界面设计.html @@ -0,0 +1,889 @@ + + + + + +iBoard V0.2 界面设计 · 打卡 / 日历 / 清单 + + + +
+ +
+

iBoard V0.2 · 界面设计

+

打卡 / 日历 / 清单 —— 移动端静态高保真原型(设计基线 420 × 820,适配 375~430px)

+
+ 版本 V0.2分支 dev-0.2antd-mobile 风格 + 底部 5 Tab(新增「打卡」)时区 Asia/Shanghai文档日期 2026-10-22 +
+
+ + +

1 设计规范Design Tokens

+

沿用项目既有 :root 变量(src/styles/globals.css),本模块不引入新主题。

+
+
+

语义色

+
+
#1677ff主色 / 今日 / 链接
+
#00b578完成 / 进度条
+
#ffc53d部分完成 / 粒子
+
#a855f7超额完成
+
#ff8f1f连续天数 / 超额徽标
+
#ff3141危险操作
+
#f5f5f5页面底色
+
#f7fbf8已完成卡片底
+
+
+ 日历·全部完成 + 日历·部分完成 + 日历·超额 +
+
+
+

布局与圆角

+
+ 视口420 × 820(375~430 自适应) + NavBar 高45px + TabBar 高72px(含安全区) + 页面内边距左右 12px + 卡片圆角10px + 卡片间距10px 纵向 + 卡片阴影0 1px 2px rgba(0,0,0,.04) + 环形尺寸58 × 58,环宽 6px + 最小点按区≥ 40 × 40 +
+
+
+

字号层级

+
18 / 700日期标题「10月22日」
+
17 / 600NavBar 标题「打卡」
+
16 / 700环形中心数字 / 弹窗标题
+
15 / 600打卡项名 / 清单标题
+
13 / 400正文 / 日历日期数字
+
12 / 400次要信息 / 操作链接
+
11 / 400备注 / 周期范围 / 徽标
+
+
+ + +

2 主界面三个子视图(SwipeTabs 左右滑动切换)

+ +
+ + +
+
+ +
+
+
+
10月22日 周四
+
已完成 2 / 4
+
+
+
特效开
+
+ 新建
+
+
+ +
+ +
+
+ + + + +
0/2
+
+
+
喝水
+
每天 2 次连续 12
+
今日
+
+
打卡
+
编辑归档
+
+ + +
+
+ + + + +
1/3
+
+
+
健身
+
每周 3 次连续 5
+
10-19 ~ 10-25
+
+
打卡
+
撤销编辑归档
+
+ + +
+
+ + + + +
+ + + +
+
+
+
读书
+
每月 12 次连续 3
+
10-01 ~ 10-31
+
+
再打一次
+
撤销编辑归档
+
+ + +
+
+ + + + +
+ + + +
+ +1 +
+
+
吃药
+
每天 1 次连续 30
+
今日超额 1
+
+
再打一次
+
撤销编辑归档
+
+
+
+
+
+ 首页
+
+ 宏观
+
+ 监控
+
+ 打卡
+
+ 我
+
+
+
+ 2.1 打卡视图 · 今天默认进入 +

头部日期 + 完成数;未完成卡片排在前面。环形中心未达标显示 done/target,达标显示对勾(每天 1 次打完即刻闭合);超额在右上角显示 +n。点环或「打卡」按钮均可打卡,卡片底部提供撤销 / 编辑 / 归档。

+
+
+ + +
+
+ +
+
+
打卡
日历
清单
+
+
+ ‹2026年10月› + 回到今天 +
+
+
昨天
今天
明天
+
+ +
+
一二三四五六日
+
+
28
+
29
+
30
+
1
+
2
+
3
+
4
+ +
5
+
6
+
7
+
8
+
9
+
10
+
11
+ +
12
+
13
+
14
+
15
+
16
+
17
+
18
+ +
19
+
20
+
21
+
22
+
23
+
24
+
25
+ +
26
+
27
+
28
+
29
+
30
+
31
+
1
+
+
+ +
本月完成率 82%
+
今天 · 2026-10-22
+ +
+ 喝水 + 每天 2 次 · 当日 2/2 + 已达标 +
+
+ 健身 + 每周 3 次 · 当日 0/1 + 打卡 +
+
+ 读书 + 每月 12 次 · 当日 1/1 + 已达标 +
+
+ 吃药 + 每天 1 次 · 当日 2/1 + 已达标 +
+
+
+
首页
+
宏观
+
监控
+
打卡
+
我
+
+
+
+ 2.2 日历视图 · 今天选中 +

周一起始月历 + 「昨天 / 今天 / 明天」快捷分段;日期下方状态点:绿=全部完成、黄=部分完成、紫=超额、无点=未完成或未来。今天用主色数字并选中高亮,未来日期数字弱化。下方为该日明细,可直接打卡;底部展示本月完成率。

+
+
+ + +
+
+ +
+
+
打卡
日历
清单
+
+
+
进行中
已归档
+
+ 新建
+
+ +
+
出差 · 杭州出差
+
+
8/12› +
+
+
身份证 / 证件×
+
手机充电器 + 充电宝×
+
笔记本电脑 + 电源×
+
耳机×
+
换洗衣物×
+
洗漱包×
+
常用药×
+
车票 / 机票 / 酒店确认×
+
雨伞×
+
工牌 / 名片×
+
报销单据×
+
现金零钱×
+ +
添加检查项
添加
+
+ 全选全不选复制 + 存为模板归档删除 +
+
+
+ +
+
回老家 · 国庆回老家
+
+
0/12› +
+
+
+
+
首页
+
宏观
+
监控
+
打卡
+
我
+
+
+
+ 2.3 清单视图 · 展开态 +

卡片头部常显标题、场景标签、进度条与 8/12;点击展开逐条勾选(勾选后绿色圆底、文字加删除线并弱化)。底部可继续添加检查项,并提供全选 / 全不选 / 复制 / 存为模板 / 归档 / 删除。收起态只保留头部一行。

+
+
+ + +
+
+ +
+
+
+
10月22日 周四
+
已完成 2 / 4
+
+
特效开
+ 新建
+
+
+
+
+ + + +
0/2
+
+
喝水
每天 2 次
今日
+
打卡
+
编辑归档
+
+
+
+
+
+
新建打卡项
+
例如:喝水 / 健身 / 读书
+
+
每天
每周
每月
每年
+
+
+
−1+
+
+
+
+ + + + +
+
+
补充说明
+
保存
+
取消
+
+
+
首页
+
宏观
+
监控
+
打卡
+
我
+
+
+
+ 2.4 新建 / 编辑打卡项(底部弹层) +

名称 ≤20 字;周期四选一(每天 / 每周 / 每月 / 每年);目标次数用 Stepper,最小 1;颜色 7 色预设;备注可选。编辑态标题变为「编辑打卡项」并回填当前值。

+
+
+ + +
+
+ +
+
+
10月22日 周四
已完成 3 / 4
+
特效开
+ 新建
+
+
+
+
+ +
+
+ + + + + + + + + + + + + + +
+ + + + +
+ +
+
+
吃药
每天 1 次连续 30
今日
+
再打一次
+
撤销编辑归档
+
+
+
+ + + +
1/3
+
+
健身
每周 3 次
10-19 ~ 10-25
+
打卡
+
撤销编辑归档
+
+
+
+
+
首页
+
宏观
+
监控
+
打卡
+
我
+
+
+
+ 2.5 达标瞬间 · 完成特效 +

环形闭合 + 对勾缩放入场(0.32s);同时 14 颗粒子沿圆周扩散(26~58px,交替主色与金色)+ 主色光环脉冲(0.7s),总时长 ≤900ms,pointer-events:none 不阻塞后续操作;并触发 18ms 轻震动。可在头部一键关闭(持久化开关),系统「减弱动效」时自动降级。

+
+
+ + +
+
+ +
+
打卡
日历
清单
+
‹2026年10月›回到今天
+
昨天
今天
明天
+
+
一二三四五六日
+
+
28
29
30
+
1
+
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
13
+
14
+
15
+
16
+
17
+
18
+
19
+
20
+
21
+
22
+
23
+
24
+
25
+
26
27
+
28
29
+
30
31
+
1
+
+
+
本月完成率 82%
+
明天 · 2026-10-23计划(不可打卡)
+
喝水每天 2 次 · 当日 0/2待办
+
健身每周 3 次 · 当日 0/1待办
+
读书每月 12 次 · 当日 0/1待办
+
吃药每天 1 次 · 当日 0/1待办
+
+
+
首页
+
宏观
+
监控
+
打卡
+
我
+
+
+
+ 2.6 日历 · 明天(计划,只读) +

未来日期不允许打卡:标题右侧显示「计划(不可打卡)」徽标,条目右侧只显示「待办」文案而不渲染按钮,月历上未来日期数字弱化且不打点。昨天视图则相反——可补打最近 7 天内的漏打卡。

+
+
+ + +
+
+ +
+
打卡
日历
清单
+
+
进行中
已归档
+
+ 新建
+
+
+
出差 · 杭州出差
+
12/12›
+
+
+ + + + + + + + +
+
+
+
回老家 · 国庆回老家
+
0/12›
+
+
+
+
首页
+
宏观
+
监控
+
打卡
+
我
+
+
+
+ 2.7 清单全部完成(12/12) +

进度条 100%、计数变为 12/12、卡片底色转 #f7fbf8,并播放与打卡一致的完成特效。清单完成后用户通常选择「归档」或「复制」复用。

+
+
+ + +
+
+ +
+
+
10月22日 周四
已完成 0 / 0
+
特效关
+ 新建
+
+
+
○
+

还没有打卡项

+
创建第一个打卡项
+
+
+
+
首页
+
宏观
+
监控
+
打卡
+
我
+
+
+
+ 2.8 空态 +

三个视图均具备加载中 / 空态 / 错误(带重试)三态。空态给出明确引导按钮;头部「特效开 / 特效关」为全局动效开关,关闭后仅保留环形与对勾,不播放粒子与震动。

+
+
+ +
+ + +

3 组件状态定义Component States

+ +

3.1 环形进度(尺寸 58,环宽 6,r=26,周长 163.4)

+ + + + + + + +
状态条件环形中心卡片底按钮
未开始done = 0仅灰底环0/2白打卡(主色)
进行中0 < done < target按 done/target 填充(45°…330°)1/3白打卡(主色)
已达标done = target闭合整圈对勾(主色)#f7fbf8再打一次(灰)
超额done > target闭合 + 右上角 +n 橙标对勾#f7fbf8再打一次(灰)
每日 1 次达标target = 1 且已打卡直接整圈闭合,不做渐增生长期对勾#f7fbf8再打一次
+

环形进度 = 周期完成度(done / n);日历日标记 = 当日份额(当日次数 / 日均期望),两者口径不同,互不影响。

+ +

3.2 日历日期

+ + + + + + + + + +
状态判定视觉
全部完成 full所有打卡项当日次数 ≥ 日均期望,且无超额绿色圆点
超额 over全部达标且存在任一打卡项当日次数 > 期望紫色圆点
部分完成 partial有完成但未全部完成黄色圆点
无 empty无记录 / 当天无打卡项 / 未来日期不显示圆点
今天date = today数字主色加粗
选中用户点击主色实心底,数字与圆点转白
非本月 / 未来月份不符 / date > today数字弱化(#c8c8c8 / #b0b0b0)
+ +

3.3 清单检查项

+ + + + + +
状态勾选框文字触发
未完成1.5px 灰边圆圈,对勾透明#333点击圆圈或文字切换
已完成绿底绿边,白色对勾显现(0.2s)#a0a0a0 + 删除线同上,写 doneAt
整单完成—进度条 100%、卡片转绿最后一项勾选时播放特效
+ + +

4 交互与动效Motion

+ + + + + + + + + +
动效时长曲线 / 实现说明
环形增长450mscubic-bezier(.4,0,.2,1),stroke-dashoffset 过渡12 点方向顺时针增长
对勾入场320msscale(.4) → 1 + 透明度达标瞬间出现
完成粒子≤900ms14 颗粒子按角度 --a、距离 --d 扩散,交错 40ms 延迟不阻塞点击
光环脉冲700ms主色圆环放大淡出与粒子同时开始
触感反馈18msnavigator.vibrate(18)WebView 不支持则静默跳过
进度条填充400mswidth 过渡清单勾选后更新
视图切换—SwipeTabs 跟手横滑与既有模块一致
+ +

4.1 打卡时序

+
用户点环 / 点「打卡」 + └─ 前端乐观更新:done+1,重算 percent / completed / over,环形立即增长 + └─ POST /api/checkin/habits/:id/check + ├─ 200 → 用服务端 item 覆盖本地;若 completed → 触发特效 + 震动 + ├─ 400 → 回滚 + Toast(未来日期 / 超出 7 天补打窗口) + ├─ 409 → 视为已打卡,刷新一次(幂等) + └─ 网络失败 → 回滚 + Toast「打卡失败」
+ + +

5 适配与可达性

+ + + + + + + +
项约定
视口设计稿 420×820;375~430px 内元素不换行、不裁切;底部 TabBar 5 项每项 ≥ 40px 可点宽度
安全区TabBar 使用 antd-mobile safeArea,页面底部留出 tabbar-h + safe-bottom
减弱动效系统开启「减弱动效」或用户关闭特效开关 → 不播放粒子 / 光环 / 震动,只保留环形与对勾
大字号卡片采用 grid 自适应列宽,文字超长换行不截断;名称限制 20 字、检查项 40 字
深色模式V0.2 沿用现有浅色主题,暂不适配
+ +

本文档为静态设计稿,不含脚本逻辑;实现对应 src/pages/checkin/*.jsx、src/components/RingProgress.jsx 与样式段(src/styles/globals.css)。接口与数据口径见《设计文档.md》与《数据库设计文档.md》。

+ +
+ + diff --git a/docs/V0.2/设计文档.md b/docs/V0.2/设计文档.md new file mode 100644 index 0000000..3ea15dd --- /dev/null +++ b/docs/V0.2/设计文档.md @@ -0,0 +1,338 @@ +# iBoard V0.2 设计文档 — 打卡 / 日历 / 清单 + +| 项 | 内容 | +| --- | --- | +| 版本 | V0.2(对应分支 `dev-0.2`) | +| 文档状态 | 已实现(与代码一致) | +| 上游文档 | `需求文档.md`(需求编号 F-CK / F-CL / F-LS / F-SY / U / D / N / E 沿用本文) | +| 配套文档 | `数据库设计文档.md`、`界面设计.html` | +| 技术栈 | Tauri 2 + React 18 + antd-mobile + React Router v6 + Express 4 + Prisma/MariaDB | +| 设计基线 | 移动端优先,视口 420×820,375~430px 自适应;时区统一 `Asia/Shanghai` | + +--- + +## 1. 总体设计 + +### 1.1 目标与范围 + +在既有 iBoard(首页 / 宏观 / 监控 / 我)之上新增**第 5 个底部 Tab「打卡」**,页面内以三个子视图承载三类需求: + +| 子视图 | 解决问题 | 核心隐喻 | +| --- | --- | --- | +| **打卡** | 周期性习惯的量化坚持 | 环形进度,点环即打卡 | +| **日历** | 回看昨天、执行今天、预知明天 | 月历网格 + 日状态点 | +| **清单** | 出差/回老家等一次性场景的漏项检查 | 勾选清单 + 进度条 | + +**不纳入 V0.2**:多用户共享清单、提醒推送、数据导出、打卡项分组、年视图热力图(见 §9 演进)。 + +### 1.2 架构总览 + +``` +┌──────────────────────── 前端 (React + antd-mobile, 打包进 Tauri / OTA) ────────────────────────┐ +│ App.jsx 路由 │ +│ └ /checkin ──► CheckinPage ── SwipeTabs ──┬── HabitView (打卡) │ +│ ├── CalendarView (日历) │ +│ └── ListView (清单) │ +│ 组件: RingProgress(环形) / Celebrate(特效) / BottomNav / TabIcon │ +│ 工具: src/pages/checkin/date.js (本地日期, 与后端同口径) apiClient: src/api/checkin.js │ +└──────────────────────────────────┬─────────────────────────────────────────────────────────────┘ + │ HTTPS + JWT (Authorization: Bearer) +┌──────────────────────────────────▼─────────────────────────────────────────────────────────────┐ +│ Express (server/index.js) │ +│ /api/checkin ─► routes/checkin.js ─┐ │ +│ /api/checklists─► routes/checklists.js ─┤─► middleware.authRequired ─► Prisma Client │ +│ └─► checkin/period.js (周期口径唯一来源) │ +└──────────────────────────────────┬─────────────────────────────────────────────────────────────┘ + ▼ + MariaDB: Habit / CheckIn / CheckList / CheckListItem / CheckListTemplate +``` + +### 1.3 分层与职责 + +| 层 | 位置 | 职责 | +| --- | --- | --- | +| 页面 | `src/pages/checkin/*.jsx` | 视图编排、本地状态、乐观更新与回滚 | +| 组件 | `src/components/RingProgress.jsx` | 纯展示:SVG 环形 + 完成特效 + 特效开关 | +| 日期工具 | `src/pages/checkin/date.js` | 与后端同口径的本地日期运算(**改一处需同步另一处**) | +| API 封装 | `src/api/checkin.js` | 统一走 `api()`(自带 token / 错误解析) | +| 路由 | `server/routes/checkin.js`、`checklists.js` | 参数校验、鉴权、组装展示态 | +| 领域逻辑 | `server/checkin/period.js` | 周期窗口、日均期望、streak 判定 | +| 持久化 | Prisma Client(`generated/prisma`) | 表操作 | + +### 1.4 目录结构(本次新增/修改) + +``` +prisma/ + schema.prisma # +5 个 model,User 加反向关系 + migrations/20261022120000_add_checkin_checklist/ # 新建表 + 外键 +server/ + checkin/period.js # 周期计算(唯一口径) + routes/checkin.js # /api/checkin/* + routes/checklists.js # /api/checklists/* + index.js # 挂载两个路由 +src/ + api/checkin.js # checkinApi / checklistApi + components/RingProgress.jsx # 环形进度 + Celebrate 特效 + components/BottomNav.jsx # 新增「打卡」Tab + components/TabIcon.jsx # 新增 checkin 图标 + pages/checkin/CheckinPage.jsx # 容器:三视图切换 + pages/checkin/HabitView.jsx # 打卡视图 + pages/checkin/CalendarView.jsx # 日历视图 + pages/checkin/ListView.jsx # 清单视图 + pages/checkin/date.js # 前端日期工具 + App.jsx # 新增 /checkin 路由 + styles/globals.css # 打卡相关样式段 +docs/V0.2/ # 需求 / 设计 / 数据库设计 / 界面设计 +``` + +--- + +## 2. 领域模型与核心口径 + +### 2.1 实体 + +`Habit`(打卡项定义)· `CheckIn`(一次打卡)· `CheckList`(场景清单)· `CheckListItem`(检查项)· `CheckListTemplate`(模板)。字段与索引见《数据库设计文档》。 + +### 2.2 周期口径(唯一来源:`server/checkin/period.js`) + +| 概念 | 定义 | +| --- | --- | +| 本地日历日 | `Intl.DateTimeFormat('en-CA', { timeZone: 'Asia/Shanghai' })` → `"YYYY-MM-DD"` | +| 周起始 | **周一**(`startOfWeek` 把周日映射到第 7 天) | +| 周期窗口 | day = [d, d];week = [周一, 周日];month = [1 号, 月末];year = [1/1, 12/31] | +| **环形进度** | `percent = 周期内累计次数 / n`,上限 1 —— 回答「这周期做了多少」 | +| **日均期望次数** | day→n;week→`ceil(n/7)`;month→`ceil(n/当月天数)`;year→`ceil(n/365)` —— 回答「今天该做多少」 | +| 完成 | `done >= target`(超额允许,`over = done - target` 继续累加) | +| streak | 从当前周期向前回溯:达标 +1;**当前周期未结束且未达标 → 跳过而非中断**;已结束周期未达标 → 终止 | + +>两种口径并存是有意为之:环形看「周期整体」,日历日标记看「当天份额」。例如「每周 3 次」,周三打 1 次 → 环形 33.3%,当天已达标(1 ≥ ceil(3/7)=1)。 + +### 2.3 边界规则 + +| 场景 | 规则 | 编号 | +| --- | --- | --- | +| 提前打卡(未来日期) | 拒绝,返回 400「不能为未来日期打卡」 | F-CK-11 | +| 补打 | 仅限最近 **7 天**,记录 `source='backfill'` | F-CK-10 | +| 跨零点 | 以服务端 Asia/Shanghai 日历日为准,前端不自行判断 | E-02 | +| 周期中途新建 | 按完整 n 计算,不按剩余天数折算 | E-01 | +| 编辑 `target` | 立即生效,历史记录不改动 | F-CK-13 | +| 删除打卡项 | 默认归档(`archived=true`),`?hard=1` 物理删并清记录 | E-05 | +| 清单删除 | 物理删(一次性场景不留历史) | F-LS-10 | + +--- + +## 3. 前端设计 + +### 3.1 路由与容器 + +``` +/checkin → CheckinPage + ├─ NavBar「打卡」 + └─ SwipeTabs(左右滑动切换,与既有模块交互一致) + ├─ habit → HabitView (受 date 影响) + ├─ calendar → CalendarView (持有选中 date,可回写容器) + └─ list → ListView (独立) +``` + +`date` 状态提升到 `CheckinPage`:日历里点某天 → 切回打卡视图时即为该日,实现「日历与打卡联动」。 + +### 3.2 组件树与关键状态 + +``` +CheckinPage +├── useState: active('habit'|'calendar'|'list'), date('YYYY-MM-DD') +HabitView(date) +├── data: { today, date, items[], summary:{total,done} } +├── bursts: { habitId: n } // 特效触发计数(+1 即播放一次) +├── form / showForm // 新建/编辑弹窗 +└── effects: boolean // 特效开关(持久化 localStorage) +CalendarView(date, onDateChange) +├── month, cal(月历), detail(当日), bursts +└── quickValue: -1|0|1|null // 昨天/今天/明天 分段控件 +ListView() +├── archived, lists, openId(展开的清单), newItem, templates, form/showForm +└── bursts +``` + +统一约定: +- 加载中 → `DotLoading`;错误 → `error-banner` + 重试;空 → antd-mobile `Empty` + 引导按钮(三态齐全,U-06)。 +- 页面重新可见时静默刷新(`useRefreshOnVisible`),与既有模块一致。 + +### 3.3 环形进度组件 `RingProgress` + +| 项 | 设计 | +| --- | --- | +| 实现 | 双 ``:灰底环 + `stroke-dasharray/dashoffset` 进度环,`rotate(-90)` 从 12 点顺时针 | +| 尺寸 | `size=58`,`stroke=6`,`r=26`,周长 `2πr≈163.4` | +| 动画 | `transition: stroke-dashoffset .45s cubic-bezier(.4,0,.2,1)`(400ms 级,U-01) | +| 中心内容 | 未达标 → `done / target` 数字;**达标 → 对勾 SVG**(`ring-check-in` 缩放入场);超额 → 右上角 `+n` 徽标 | +| 交互 | 整个环可点 = 打卡(`clickable`),同时卡片右侧保留显式「打卡」按钮,降低发现成本 | +| 无障碍/降级 | `disabled` 态半透明;系统「减弱动效」时特效自动关闭 | + +**完成特效 `Celebrate`**(F-CK-07 / U-02):`fire` 计数器 +1 即触发一次 → 14 个粒子沿圆周角度扩散(`--a/--d` CSS 变量)+ 主色光环脉冲 0.7s,总时长 ≤900ms,`pointer-events:none` 不阻塞后续操作;同时 `navigator.vibrate(18)` 轻震动(U-03)。开关持久化在 `localStorage.iboard_celebrate`,`prefers-reduced-motion` 自动降级(U-07)。 + +### 3.4 交互清单 + +| 视图 | 主要交互 | +| --- | --- | +| 打卡 | 点环/点按钮打卡(乐观更新,失败回滚 + Toast)、撤销、编辑、归档、新建弹窗(名称/周期/目标次数/颜色/备注)、特效开关 | +| 日历 | 月份左右切换、回到今天、「昨天/今天/明天」分段切换、点日期选中、当日详情内打卡(未来日期只读显示「计划」)、本月完成率 | +| 清单 | 进行中/已归档切换、展开/收起、逐条勾选、添加检查项、全选/全不选、复制、存为模板、归档/恢复、删除、从模板新建 | + +排序:打卡项列表**未完成在前**(F-CK-16);清单按 `updatedAt` 倒序。 + +### 3.5 视觉规范 + +| 类别 | 取值 | +| --- | --- | +| 主色 | `#1677ff`;成功 `#00b578`;警告/部分 `#ffc53d`;超额 `#a855f7`;危险 `#ff3141` | +| 背景 / 卡片 | `#f5f5f5` / `#ffffff`,卡片 `radius 10px` + `0 1px 2px rgba(0,0,0,.04)` | +| 文字 | 主 `#333`、次 `#666`、弱 `#999` | +| 日历日状态点 | full 绿 / partial 黄 / over 紫 / empty 无 | +| 打卡项预设色 | `#1677ff #00b578 #ff8f1f #ff3141 #a855f7 #14b8a6 #f43f9e` | + +完整高保真稿见 `界面设计.html`。 + +--- + +## 4. 后端设计 + +### 4.1 通用约定 + +- 两个路由文件首行 `router.use(authRequired)`,**全部接口需登录**;用户维度一律取 `req.user.sub`,不信任 body 中的 `userId`。 +- 响应统一 `{ item }` / `{ items }` / `{ ok }`;错误统一 `{ error: '中文提示' }` + 语义化状态码。 +- 展示态(percent / completed / streak / dayDone …)由服务端计算后下发,前端不做二次口径推导,避免两端不一致。 + +### 4.2 接口清单 + +#### 打卡 `/api/checkin` + +| 方法 | 路径 | 说明 | 关键响应字段 | +| --- | --- | --- | --- | +| GET | `/habits?date=` | 指定日待办 + 周期进度 | `items[]`(含 `done/target/percent/completed/over/streak/expectedPerDay/periodStart,End`)、`summary` | +| POST | `/habits` | 新建打卡项 | `item` | +| PUT | `/habits/:id` | 编辑(名称/周期/目标/颜色/备注/归档/排序) | `item` | +| DELETE | `/habits/:id?hard=1` | 默认归档;`hard=1` 物理删 | `{ ok, removed }` | +| POST | `/habits/:id/check` | 打一次卡(`{ date?, note? }`) | `item`(最新展示态) | +| DELETE | `/habits/:id/check?date=` | 撤销本周期最近一次 | `item` | +| GET | `/day?date=` | 某日每个打卡项明细 | `items[]`(含 `canCheck`)、`isToday`、`isFuture` | +| GET | `/calendar?month=YYYY-MM` | 月历 + 完成率 | `days[]`(`status/expected/actual/isToday/isFuture/habits[]`)、`rate`、`habits[]` | + +#### 清单 `/api/checklists` + +| 方法 | 路径 | 说明 | +| --- | --- | --- | +| GET | `/?archived=0|1` | 清单列表(带 `done/total/percent/completed`) | +| POST | `/` | 新建(`{ title, scene, note?, items?, templateId? }`) | +| GET | `/:id` | 单条详情 | +| PUT | `/:id` | 改名称/场景/备注/归档 | +| DELETE | `/:id` | 物理删除(连带检查项) | +| POST | `/:id/duplicate` | 复制为新清单(勾选状态重置) | +| POST | `/:id/items` | 新增检查项 | +| PUT | `/items/:id` | 改标题/备注/勾选(`done` 变更时写 `doneAt`) | +| DELETE | `/items/:id` | 删除检查项 | +| POST | `/:id/check-all` | 一键全选/全不选(`{ done }`) | +| GET | `/templates` | 模板列表(含系统预置,首次访问幂等写入) | +| POST | `/templates` | 另存为模板 | +| DELETE | `/templates/:id` | 删除**自建**模板(预置模板拒绝) | + +### 4.3 关键流程:打卡 + +``` +用户点环 + │ + ├─ 前端: 本地 +1,重算 percent/completed/over(乐观更新) + │ + └─ POST /api/checkin/habits/:id/check + ├─ 校验 habit 归属 → 404 + ├─ date > today → 400 不能为未来日期打卡 + ├─ date < today-7 → 400 最多只能补打最近 7 天 + ├─ seq = max(seq)+1 → insert + │ └─ P2002 → 409 该次打卡已记录(幂等) + └─ 返回最新 item + │ + ├─ 成功: 用服务端 item 覆盖本地;若 completed → fire 特效 + └─ 失败: setData(before) 回滚 + Toast +``` + +### 4.4 关键流程:月历聚合 + +1. 取全部 Habit(含归档)→ 过滤 `archived=false` 得 `active`。 +2. 一次查询当月 `[01, 月末]` 全部 CheckIn,按 `habitId → date → count` 分组。 +3. 逐日遍历:仅统计 `Habit.createdAt <= 该日` 的项;累加 `expected += expectedPerDay(...)`、`actual += min(count, expected)`;任一 `count > expected` 记 `over`。 +4. `status`:全完成且无超额 → `full`;全完成且有超额 → `over`;有完成未全完成 → `partial`;否则 `empty`。 +5. 完成率 `rate = Σactual / Σexpected`(**只统计非未来日期**)。 + +复杂度:`O(天数 × 打卡项数)`,30×20=600 次内存运算,无 N+1 查询。 + +### 4.5 校验与错误码 + +| 状态码 | 场景 | +| --- | --- | +| 400 | 名称为空/超长、周期非法、target < 1、未来日期打卡、超出补打窗口、本周期无记录可撤销、模板缺条目 | +| 404 | 资源不存在或不属于当前用户 | +| 409 | 并发重复打卡(P2002) | +| 401 | 未登录 / token 失效(`authRequired`) | + +所有写操作均用 `prisma.xxx.findFirst({ where: { id, userId } })` 或 `where: { id, list: { userId } }` 做归属校验,杜绝越权改他人数据。 + +--- + +## 5. 非功能设计 + +| 维度 | 设计 | +| --- | --- | +| 首屏 | 三视图懒切换,只有激活视图发请求;单视图首屏 ≤ 1 次列表请求(N-01) | +| 弱网/失败 | 乐观更新 + 失败回滚 + Toast;错误横幅带重试;`useRefreshOnVisible` 静默刷新 | +| 幂等 | 唯一约束 `(habitId, date, seq)` + 前端按钮 loading,防连点重复计次 | +| 时区 | 全链路 Asia/Shanghai;库内 `DATETIME` 存 UTC,业务日期用 `VARCHAR(10)` 本地日历日 | +| 兼容 | 375~430px 自适应;Tauri Android WebView 支持 `navigator.vibrate`,不支持时静默跳过 | +| 性能 | 月历/周期统计均为「一次范围查询 + 内存聚合」,避免逐项查询 | +| 安全 | 全接口 `authRequired`;输入长度截断(名称 20、检查项 40、备注 200);无 SQL 拼接(Prisma 参数化) | +| 可维护 | 周期口径单点(`server/checkin/period.js`),前端 `date.js` 与之对齐,改动需同步两处(已在文档与注释中标注) | + +--- + +## 6. 测试与验证 + +| 层级 | 内容 | 结果 | +| --- | --- | --- | +| 单元 | 周期工具 23 条断言(窗口边界、周一起始、月末、闰年、日均期望、streak 跳过未结束周期) | 全通过 | +| 接口冒烟 | 自签 JWT 端到端:每天 1 次闭合、每周 3 次 33.3%、超额 +1、撤销归零、未来日期 400、月历 31 天、模板生成 11 项、全选完成、复制重置 | 全通过,测试数据已清理 | +| 构建 | `npm run build` | 通过 | +| 迁移 | `prisma migrate deploy` 到目标库 | 成功 | + +--- + +## 7. 里程碑(对应需求文档 M1~M5) + +| 里程碑 | 内容 | 状态 | +| --- | --- | --- | +| M1 | 数据模型 + 迁移 + 周期模块 + 全部后端接口 | 完成 | +| M2 | 打卡视图:环形进度、特效、撤销/补打、新建编辑 | 完成 | +| M3 | 日历视图:月历 + 昨天/今天/明天 + 完成率 | 完成 | +| M4 | 清单视图:CRUD、勾选、进度、模板复用 | 完成 | +| M5 | 文档(需求/设计/数据库/界面)+ 构建校验 + 提交 | 完成 | + +--- + +## 8. 风险与权衡 + +| 项 | 权衡 | 缓解 | +| --- | --- | --- | +| 周期口径前后端各实现一份 | 前端需离线即时反馈,不能等接口 | 两端算法同源同注释;口径变更必须同步 `period.js` 与 `date.js`(已写入项目记忆) | +| 进度实时聚合而非冗余计数 | 换取一致性与配置即时生效,牺牲少量 CPU | 单用户数据量极小(月历 30 天 × 20 项),实测无感;V0.3 可加汇总表 | +| 未来日期禁止打卡 | 防止「提前打完一周」刷进度,符合「日历看明天=计划」语义 | 明天视图只读展示「计划」,明确文案 | +| 特效可能打扰 | 部分用户不喜动效 | 一键开关 + `prefers-reduced-motion` 自动降级 | +| 新增第 5 个 Tab | 底部空间变拥挤 | 图标线性简洁;`TabBar` 原生 safe-area,5 项在 375px 下仍在可点区(≥44px) | + +--- + +## 9. 后续演进(V0.3+) + +1. **提醒与通知**:`Habit.remindAt` + Tauri 本地通知,解决「忘打卡」。 +2. **补打放宽 / 历史编辑**:当前 7 天限制可配置化。 +3. **统计视图**:连续天数榜单、月/年热力图(届时引入 `HabitDailyStat` 汇总表)。 +4. **打卡项分组与排序拖拽**:现有 `sortOrder` 已预留,仅需交互。 +5. **清单协作/分享**:引入 `CheckListShare`。 +6. **数据导出**:清单完成后导出为图片或文本(便于发群里确认)。