feat: strengthen backup and mobile workflows
ci / gitleaks (push) Successful in 1m19s
ci / docker (push) Successful in 5m48s

This commit is contained in:
2026-09-16 21:12:52 +08:00
parent 6f38190c92
commit 6c234d7d82
64 changed files with 5059 additions and 635 deletions
+55 -39
View File
@@ -1,59 +1,75 @@
# API
Base path: `/api/v1`. 除初始化、登录和健康检查外均需 `dodo_session` Cookie。
Base path: `/api/v1`除初始化、登录和健康检查外,接口均要求 `dodo_session` Cookie;同源写请求使用双提交 CSRF 校验。登录后可访问 `/api/docs``/api/openapi.json`
## Setup / Auth
## 账户与会话
- `GET /setup/status`
- `POST /setup/initialize`
- `POST /auth/login`
- `POST /auth/logout`
- `GET /me`
- `GET /setup/status``POST /setup/initialize`
- `POST /auth/login``POST /auth/logout``POST /auth/change-password`
- `GET/PATCH /me`
- `GET /sessions``DELETE /sessions/{session_id}``DELETE /sessions/others`
- `GET /audit-logs`
## Folders
## 文件夹、清单与任务
- `GET /folders` — 仅返回未删除文件夹
- `POST /folders` — 创建文件夹
- `PATCH /folders/{folder_id}` — 重命名
- `DELETE /folders/{folder_id}` — 软删除;其清单移到根层级
- 文件夹:`GET/POST /folders``PATCH/DELETE /folders/{id}`,以及排序接口
- 清单:`GET/POST /lists``PATCH/DELETE /lists/{id}`,归档恢复、永久删除、移动与排序接口
- 归档清单只标记 `deleted_at`,不会把任务移到收集箱;清单归档期间,其任务在普通列表和详情中不可见,恢复清单后重新出现
- 任务:`GET/POST /tasks``GET/PATCH/DELETE /tasks/{id}``POST /tasks/{id}/restore``DELETE /trash/{id}`
- `GET /tasks``GET /trash` 使用不透明游标;`limit` 为 1100
- `POST /tasks/batch` 支持批量完成、移动、截止时间与软删除;任务写入使用 `version` 乐观锁
- 一层子任务必须与父任务同清单;任务可带日期型或具体时间型截止时间
## Lists
## 重复任务
- `GET /lists` — 仅返回未删除清单,系统收集箱排首位
- `POST /lists` — 创建清单,可指定 `folder_id`
- `PATCH /lists/{list_id}` — 重命名;系统收集箱返回 409
- `DELETE /lists/{list_id}` — 软删除并将任务移入系统收集箱;系统收集箱返回 409
- `GET /tasks/{task_id}/recurrence`
- `POST /recurrences``PATCH/DELETE /recurrences/{id}`
- `POST /recurrences/{id}/complete`
- 支持 RRULE 计划重复与按用户本地完成日期计算的“完成后重复”
## Tasks
## 习惯、倒数日与备忘录
- `GET /tasks?q=&limit=&cursor=` — 顶层未删除任务的游标分页;`q` 匹配标题、描述和清单名
- `POST /tasks` — 创建任务;`parent_id` 只允许指向同清单顶层任务
- `GET /tasks/{task_id}` — 返回任务和一层子任务
- `PATCH /tasks/{task_id}` — 必须携带当前 `version`,原子比较更新;版本冲突返回 409
- `DELETE /tasks/{task_id}` — 软删除任务及其直接子任务
- `POST /tasks/{task_id}/restore` — 恢复任务及其直接子任务
- `POST /tasks/batch` — 原子批量完成、移动、设置截止时间或软删除
- 习惯:创建、列表/周网格、部分更新、排序、归档/恢复/永久删除、日志、暂停和统计
- 倒数日:创建、列表、编辑、置顶、归档/恢复/永久删除;支持公历/农历及周/月/年重复
- 备忘录:游标列表、创建、读取、乐观锁更新、软删除、恢复和归档后永久删除
批量请求字段:`task_ids``completed``list_id``due_at``soft_delete`。所有任务和目标清单在写入前完成归属校验;任一不存在则整批不修改。
## 附件
## Countdowns
- `GET/POST /tasks/{task_id}/attachments`
- `GET/DELETE /attachments/{attachment_id}`
- 文件保存在服务端附件目录;上传受大小与类型限制,访问始终校验当前用户归属
- `GET /countdowns?archived=false` — 查询倒数日;置顶项优先,其余按下一次发生日期排序
- `POST /countdowns` — 创建倒数日、纪念日或生日;支持 `none/weekly/monthly/yearly` 重复
- `PATCH /countdowns/{countdown_id}` — 编辑名称、日期、类型、重复与图标
- `POST /countdowns/{countdown_id}/pin` — 单一置顶,自动取消其他置顶项
- `DELETE /countdowns/{countdown_id}` — 归档
- `POST /countdowns/{countdown_id}/restore` — 恢复归档项
- `DELETE /countdowns/{countdown_id}/purge` — 永久删除已归档项
## 完整备份 ZIP v2
## Recycle bin
### `GET /backup/export.zip`
- `GET /trash?limit=&cursor=` — 已删除顶层任务的游标分页
- `DELETE /trash/{task_id}` — 永久删除任务及其子任务
导出 `dodo-backup` version 2 ZIP。归档包含 `manifest.json`、每类实体的 `data/*.json`、附件元数据和附件原始字节。manifest 声明实体数量及每个条目的 SHA-256。
游标是不透明字符串。无效游标返回 422;`limit` 范围为 1100
实体范围:`folders``lists``tasks``recurrences``recurrence_exceptions``habits``habit_logs``habit_pauses``countdowns``memos``attachments`。会话、密码散列、审计日志及备份内部账本不导出
## Health
### `POST /backup/preflight?mode=merge|replace`
以 multipart 字段 `file` 上传 ZIP。服务端流式暂存,并在返回令牌前验证:ZIP 路径与条目、压缩比/容量、manifest 版本与计数、全部校验和、字段与业务约束、关系拓扑、一层任务树、用户隔离以及附件元数据/字节一致性。
成功返回 `valid`、短期 `preflight_token``backup_id`、归档摘要和各实体数量。预检有每用户待处理数量/容量配额和过期时间;令牌绑定用户、文件摘要和恢复模式。
### `POST /backup/restore`
请求体:
```json
{"preflight_token":"...","mode":"merge"}
```
`merge` 使用持久化 source→target ID/内容摘要账本实现可重试合并;跨用户或同 ID 不同内容冲突会拒绝。`replace` 在事务内替换当前用户业务实体,并通过同文件系统隔离区协调附件删除和失败补偿。令牌单次消费;若数据库已提交但隔离区清理失败,同一令牌仅重试清理,不重复导入。
## 旧格式兼容
- `GET /export``GET /export.csv`:旧版 JSON/UTF-8-BOM CSV v1 轻量导出,不是完整备份
- `POST /restore?mode=merge|replace``POST /restore.csv?mode=merge|replace`:兼容旧 JSON/CSV v1
- 旧格式只覆盖文件夹、清单、任务、重复模板、习惯、倒数日和备忘录;不包含日志、暂停、重复例外和附件字节
## 健康检查
- `GET /health/live`
- `GET /health/ready`
+29 -80
View File
@@ -1,97 +1,46 @@
# dodo 数据模型
所有业务实体使用 UUIDv7 主键并按 `user_id` 隔离。生产使用 PostgreSQL,测试使用 SQLite;模型保持两者兼容
业务主键使用 UUIDv7;带 `user_id` 的实体按用户隔离。时间点按 UTC 保存,用户时区用于日期语义与展示。生产使用 PostgreSQL,测试使用 SQLite。
## app_state
## 账户
- `key` 主键
- `created_at`
- `app_state`:初始化状态
- `users`:用户名、密码散列、时区
- `sessions`:会话令牌散列、过期/最近访问时间、IP 与 User-Agent
- `audit_logs`:用户、动作、实体、非敏感摘要和时间
## users
## 任务域
- `id`
- `username` 唯一
- `password_hash`
- `timezone`
- `created_at`
- `folders`:名称、位置、软删除时间;删除文件夹只解除清单分组
- `task_lists`:文件夹、名称、收集箱标记、位置、软删除时间
- `tasks`:清单、可空父任务、标题、Markdown 描述、优先级、完成/完成时间、截止时间、`due_has_time`、位置、版本、软删除与外部 ID
- `recurrence_templates`:任务的一对一重复规则、开始/结束、计划或完成后触发模式、完成后间隔与最近完成时间
- `recurrence_exceptions`:模板发生时间及标题/截止/完成/删除覆盖
- `attachments`:任务、原始文件名、服务端存储名、MIME、大小和创建时间
- `purge_operations`:清单永久删除时附件隔离区清理的补偿状态
## sessions
任务树只允许一层,父子任务属于同一清单。清单归档仅设置清单 `deleted_at`,保留所有任务的 `list_id`;查询隐藏归档清单内任务,恢复清单后原任务和完成状态重新可见。任务与子任务软删除/恢复按生命周期规则处理,永久删除会清理关联重复数据和附件。
- `id`
- `token_hash` 唯一
- `user_id` → users,级联删除
- `expires_at`
- `created_at`
## 习惯、倒数日与备忘录
## folders
- `habits`:完成型/数值型、目标/上限、日/周/月/间隔计划、开始日、排序和归档
- `habit_logs`:习惯与日期唯一的数值记录
- `habit_pauses`:习惯暂停区间
- `countdowns`:标题、日期、公历/农历字段、类型、重复、兼容保留的图标字段、置顶与归档
- `memos`:标题、Markdown 内容、乐观锁版本、创建/更新时间和软删除时间
- `id`
- `user_id` → users
- `name`
- `position`
- `created_at`
- `deleted_at`,非空表示软删除
## 完整备份 v2 内部状态
删除文件夹不会删除清单;清单的 `folder_id` 被置空。
- `backup_preflights`:令牌散列、用户、备份 ID/摘要/大小、暂存路径、模式、状态、过期/消费时间和待清理路径
- `backup_imports`:每用户已导入备份 ID、归档摘要、模式和时间;用于幂等判断
- `backup_import_entities`:源实体 ID 到目标 ID 的映射及内容摘要;用于 merge 冲突检测与可重试导入
## task_lists
这些表和账户/会话/审计信息不属于用户可迁移业务实体。ZIP v2 只导出:
- `id`
- `user_id` → users
- `folder_id` → folders,可空
- `name`
- `is_inbox`,每个用户初始化时创建一个受保护的系统收集箱
- `position`
- `created_at`
- `deleted_at`,非空表示软删除
`folders``lists``tasks``recurrences``recurrence_exceptions``habits``habit_logs``habit_pauses``countdowns``memos``attachments`
删除普通清单时,其未删除任务原子移动到系统收集箱。系统收集箱不可重命名或删除
## tasks
- `id`
- `user_id` → users
- `list_id` → task_lists
- `parent_id` → tasks,可空;仅允许一层子任务且必须与父任务同清单
- `title`
- `description`
- `priority`03
- `completed`
- `due_at`,可空
- `version`,乐观锁版本;单任务更新用 `id + user_id + version` 原子比较更新
- `position`
- `created_at`
- `updated_at`
- `deleted_at`,非空表示进入回收站
顶层任务软删除、恢复或永久删除时同步处理直接子任务。列表与回收站使用 `(created_at, id)` 作为稳定游标排序键。
## countdowns
- `id`
- `user_id` → users,级联删除
- `title`
- `event_date`,仅日期
- `kind``countdown` / `anniversary` / `birthday`
- `repeat_rule``none` / `weekly` / `monthly` / `yearly`
- `icon`
- `pinned`,每个用户仅保留一个置顶项
- `archived_at`,非空表示归档
- `created_at` / `updated_at`
附件导出时去掉内部 `storage_name`,改用归档内安全路径并携带真实字节;恢复时生成目标存储名。每个实体文件和附件字节均由 manifest SHA-256 覆盖
## 迁移
- `0001_initial.py`:已部署的初始模式,不修改
- `0002_task_management.py`:新增文件夹/清单软删除列、历史标签表及游标/回收站索引
- `0008_remove_calendar_subscriptions.py`:移除日历订阅表
- `0009_remove_tags.py`:移除历史标签表及任务标签关联表
- `0010_countdowns.py`:新增倒数日、纪念日与生日表
## 后续阶段预留
- task_reminders
- task_recurrence_templates
- task_recurrence_exceptions
- habits / habit_logs / habit_reminders
- attachments
- audit_logs
迁移按 `0001``0019` 顺序应用;当前最新 `0019_backup_imports.py` 增加完整备份预检、导入及实体映射账本。历史迁移还覆盖任务管理、会话元数据、查询索引、习惯排序、倒数日/农历、日期型截止语义、重复触发模式、备忘录与 `completed_at` 等演进。
+31 -102
View File
@@ -1,117 +1,46 @@
# dodo 产品与技术决策记录
## 定位
## 定位与当前范围
dodo 是一个纯自托管的 TickTick-like 任务与习惯管理工具。目标不是一比一复刻 TickTick,而是做一个数据归自己、界面温暖紧凑、适合个人长期使用的任务系统
dodo 是纯自托管、面向个人长期使用的任务与生活管理 PWA。当前包含任务/子任务、文件夹与清单、今日视图、重复任务、习惯、倒数日、Markdown 备忘录、附件、会话管理、审计和数据备份;不提供番茄钟、自然语言建任务或外部通知渠道
## 当前边界
## 技术与数据
- 首版只做本地开发验证,不部署。
- 首版不做通知渠道:Web Push、Telegram、SMTP 暂不实现。
- 首版附件只做本地存储,不实现 S3。
- 首版不做番茄钟。
- 首版不做自然语言创建任务。
## 技术栈
- Monorepo`frontend/``backend/`
- 前端:Vue 3 + TypeScript + Vite + Tailwind CSS + Shadcn-vue/Reka UI
- 后端:FastAPI + Pydantic v2 + SQLAlchemy 2 Async + Alembic
- 数据库:PostgreSQL,主键 UUIDv7,时间统一 UTC,用户配置时区
- 包管理:uv + pnpm
- 交付:单 Docker 镜像,外部 PostgreSQL
- 许可证:AGPL-3.0
## 数据库
本地开发数据库:
```text
postgresql+asyncpg://postgres:***@10.10.100.99:5433/dodo
```
已从默认 `postgres` 库迁移到独立 `dodo` 库。误建在 `postgres.public` 的 dodo 表已清理。
- MonorepoVue 3 + TypeScript + Vite 前端,FastAPI + Pydantic v2 + SQLAlchemy 2 Async + Alembic 后端
- PostgreSQL 生产、SQLite 测试;UUIDv7 主键,时间点使用 UTC,日历语义使用用户时区
- 单 Docker 镜像,外部 PostgreSQLAGPL-3.0
- 用户业务读写必须按归属过滤;更新任务/备忘录使用乐观锁
## 产品模型
### 任务
- 文件夹 → 清单 → 一层任务树;系统收集箱受保护
- 清单删除定义为归档:保留任务成员关系,归档期间隐藏,恢复后原样出现;永久删除仅允许作用于已归档清单
- 截止日期区分全天日期和具体时间;重复任务支持 RFC 5545 计划重复及“完成后重复”
- 习惯支持完成型/数值型、日/周/月/间隔计划、暂停、历史和归档
- 倒数日支持公历/农历、生日/纪念日、重复、置顶与归档
- 备忘录使用 Markdown,支持软删除、恢复及归档后永久删除
- 文件夹 → 清单 → 任务
- 系统内置收集箱,不允许删除
- 任务支持一层子任务
- 状态:未完成 / 已完成
- 优先级:无 / 低 / 中 / 高
- Markdown 描述
- 截止日期 + 具体时间
- 多提醒设计预留
- 删除为软删除,回收站手动清空
- 支持创建、修改、完成、恢复、删除操作历史
- 并发编辑使用原子乐观锁
## UI 决策
### 重复任务
- 桌面保留左导航/内容/可选详情三栏;移动端使用底部导航
- 手机底栏固定为“今天、习惯、倒数日、设置”,精确匹配当前页面;不使用“更多”中转
- 新建入口使用同一个普通圆形 Plus FAB,禁止装饰性光环或吉祥物
- 设置页使用连续分组:数据、账户与安全、登录设备、活动、危险操作
- 任务、习惯、倒数日、备忘录、操作菜单和确认框统一走 `AppSheet` / `AppDialog` 覆盖层栈;共享背景 inert、焦点陷阱、Escape、忙碌态和嵌套焦点恢复
- 桌面任务/备忘录详情可保持非模态,移动端由同一组件切为底部模态弹层
- RFC 5545 RRULE
- 模板 + 实例
- 修改范围:仅本次 / 本次及以后 / 全部
- 删除单次保存为例外
- 每月 31 日在无 31 日月份跳过
- 逾期完成不影响下次计划日期
## 备份决策
### 习惯
- 完成型 + 数值型
- 每天 / 每周 / 每月 / 间隔天数
- 数值型当日累计,达标后封顶
- 允许补打和修改历史
- 支持暂停区间,暂停期不破坏连续记录
- 归档后保留历史统计
### 倒数纪念日
- 支持倒数日、纪念日、生日
- 支持不重复、每周、每月、每年重复
- 未来显示“还有 N 天”,当天显示“就是今天”,过去显示“已经 N 天”
- 支持单一置顶、归档恢复、编辑和删除
## UI 方向
- 手账生活感
- 中高信息密度
- 1012px 中等圆角
- 细分割线为主,少量浅底色
- 强调色:`#F15A29`
- 只做浅色模式
- 系统字体栈
- 不使用猫猫元素
- 轻微动效
## 页面结构
- 桌面三栏:左导航 / 中任务列表 / 右任务详情
- 手机底部导航
- 顶部快速输入,手机悬浮新增按钮
- 桌面右侧详情栏,手机底部弹层
- 习惯首页:今日习惯列表 + 一周打卡格
- 搜索:顶部搜索框 + 全局搜索快捷键
- “完整备份”专指 `dodo-backup` ZIP version 2,而不是旧 JSON/CSV
- v2 覆盖全部用户业务实体、历史/例外、附件元数据与附件字节;manifest 记录实体数量和每个条目的 SHA-256
- 恢复必须先预检,再用绑定用户、文件摘要和模式的短期单次令牌执行
- 预检拒绝未知/缺失实体、不安全 ZIP 路径、重复条目、异常压缩比/容量、校验和错误、非法字段、破坏关系拓扑或一层任务树的数据
- `merge` 通过持久化 ID/摘要账本保证幂等与冲突可见;`replace` 仅替换当前用户业务数据
- 附件恢复采用同文件系统暂存/隔离与补偿;数据库提交后的清理失败可用同一令牌重试清理,不会再次导入
- 保留 JSON/CSV v1 恢复兼容,但明确其不包含日志、暂停、重复例外和附件字节,仅用于旧数据迁移
## 工程质量
- `/api/v1` API 路径
- 统一错误码、可读提示和字段详情
- 页面内诊断信息 + Toast
- readiness 检查数据库
- 首版不提供 Prometheus metrics
- 审计日志记录操作人、实体、动作、时间和变更摘要
- 手动 JSON 全量导出
- 附件默认 20MB,可用环境变量调整
- 默认允许常用文档与图片,拒绝危险文件类型
## 第一阶段验收
- 可初始化管理员
- 可登录
- 可创建清单和任务
- 后端测试通过
- 前端生产构建通过
- PostgreSQL 迁移成功
- 本地应用可以启动并访问
- API 基路径 `/api/v1`Cookie Session、同源 CSRF、安全响应头、登录限流
- 附件与备份均有容量限制、路径包含检查和用户归属校验
- 后端使用 pytest + ruff,前端使用 Vitest + vue-tsc/Vite;变更结束运行全量测试、构建和 `git diff --check`