docs: consolidate project documentation
This commit is contained in:
290
README.md
290
README.md
@@ -1,163 +1,215 @@
|
||||
# Elysia ToDo - 爱莉希雅待办事项
|
||||
|
||||
一款全栈个人信息管理应用,集待办任务、习惯打卡、纪念日提醒于一体。
|
||||
Elysia ToDo 是一个全栈个人信息管理应用,当前代码包含待办任务、习惯打卡、纪念日、长期目标、证书管理、数据备份导入、WebDAV 同步和单用户密码登录能力。
|
||||
|
||||
## 功能概览
|
||||
|
||||
### 任务管理
|
||||
- **待办列表** — 创建、编辑、删除任务,支持分类、标签、优先级
|
||||
- **四象限视图** — 基于艾森豪威尔矩阵(重要/紧急)的四象限优先级模型
|
||||
- **日历视图** — 按月/周/日查看任务排布
|
||||
- **拼音搜索** — 支持中文拼音快速检索任务和分类
|
||||
|
||||
- 待办列表:创建、编辑、删除、完成切换、分类、标签、截止时间和优先级。
|
||||
- 四象限视图:使用 `q1` 到 `q4` 表示重要/紧急优先级。
|
||||
- 日历视图:按日历方式查看任务排布。
|
||||
- 拼音搜索:前端使用 `pinyin-pro` 支持中文拼音检索。
|
||||
|
||||
### 习惯打卡
|
||||
- 习惯分组管理(学习、运动、生活等)
|
||||
- 每日打卡记录,支持周期配置与休息日
|
||||
- 周视图打卡进度展示,一目了然
|
||||
|
||||
### 纪念日管理
|
||||
- 自定义纪念日分类
|
||||
- 支持农历/公历日期
|
||||
- 倒计时提醒,不错过重要日子
|
||||
- 习惯分组管理,支持颜色、图标和排序。
|
||||
- 习惯支持每日/每周频率、目标次数、指定活跃日期和归档状态。
|
||||
- 打卡记录按日期保存,可用于周视图/月视图展示。
|
||||
|
||||
### 系统功能
|
||||
- 偏好设置(站点名称、默认视图等)
|
||||
- 可折叠侧边栏
|
||||
- 响应式布局
|
||||
- SPA 单页应用,History 路由模式
|
||||
### 纪念日
|
||||
|
||||
- 纪念日分类管理。
|
||||
- 纪念日支持日期、年份、循环标记、提前提醒天数和说明。
|
||||
- 前端提供倒计时、分类筛选和编辑弹窗。
|
||||
|
||||
### 目标管理
|
||||
|
||||
- 长期目标支持 `milestone` 里程碑模式和 `cumulative` 累计打卡模式。
|
||||
- 目标可设置状态、目标值、输入单位、换算率、目标日期、分类、颜色、图标和排序。
|
||||
- 目标详情支持阶段/里程碑、复盘、关联任务和目标打卡记录。
|
||||
|
||||
### 证书管理
|
||||
|
||||
- 证书分类管理。
|
||||
- 证书支持图片、颁发机构、颁发日期、到期日期、说明和排序。
|
||||
|
||||
### 系统与数据
|
||||
|
||||
- 首次设置密码、登录、退出、修改密码,使用 Cookie 中的 JWT 做会话认证。
|
||||
- 用户偏好设置:昵称、头像、签名、生日、邮箱、站点名称、主题、默认视图和默认排序。
|
||||
- 数据备份导出和导入:通过 `/api/backup/export` 与 `/api/backup/import` 处理 JSON 备份。
|
||||
- WebDAV 同步:支持配置、测试连接、推送、拉取、双向同步、状态查询和清空远端同步数据。
|
||||
- SPA 单页应用,后端静态服务支持 History 路由回退。
|
||||
|
||||
## 技术栈
|
||||
|
||||
| 层级 | 技术 |
|
||||
|------|------|
|
||||
| 层级 | 当前实现 |
|
||||
|------|----------|
|
||||
| 前端框架 | Vue 3 + TypeScript |
|
||||
| UI 组件库 | Element Plus |
|
||||
| 状态管理 | Pinia |
|
||||
| 路由 | Vue Router 4 |
|
||||
| 构建工具 | Vite |
|
||||
| 构建工具 | Vite 7 |
|
||||
| HTTP 客户端 | Axios |
|
||||
| 后端框架 | FastAPI |
|
||||
| ORM | SQLAlchemy |
|
||||
| 数据库 | SQLite |
|
||||
| ORM | SQLAlchemy 2 |
|
||||
| 数据库 | PostgreSQL,默认由 `DATABASE_URL` 指定 |
|
||||
| ASGI 服务器 | Uvicorn |
|
||||
| 密码与令牌 | bcrypt + python-jose |
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
```text
|
||||
ToDoList/
|
||||
├── main.py # 启动入口(编译前端 + 启动后端)
|
||||
├── requirements.txt # Python 依赖
|
||||
├── .gitignore
|
||||
├── api/ # 后端
|
||||
│ └── app/
|
||||
│ ├── config.py # 配置(端口、路径、CORS 等)
|
||||
│ ├── database.py # 数据库引擎与会话管理
|
||||
│ ├── main.py # FastAPI 应用(路由、中间件、静态文件)
|
||||
│ ├── models/ # SQLAlchemy 数据模型
|
||||
│ ├── schemas/ # Pydantic 请求/响应模型
|
||||
│ ├── routers/ # API 路由
|
||||
│ └── utils/ # 工具函数(CRUD、日志、日期)
|
||||
├── WebUI/ # 前端
|
||||
│ ├── package.json
|
||||
│ ├── vite.config.ts
|
||||
│ └── src/
|
||||
│ ├── api/ # Axios 接口封装
|
||||
│ ├── components/ # 通用组件
|
||||
│ ├── views/ # 页面视图
|
||||
│ ├── stores/ # Pinia 状态管理
|
||||
│ ├── router/ # 路由配置
|
||||
│ ├── styles/ # 全局样式 (SCSS)
|
||||
│ └── utils/ # 前端工具(拼音、优先级、日期)
|
||||
└── tests/ # 测试
|
||||
├── main.py # 编译前端、复制静态资源、启动后端
|
||||
├── requirements.txt # Python 依赖锁定
|
||||
├── Dockerfile # 后端镜像,依赖预构建的 api/webui
|
||||
├── docker-compose.yml # PostgreSQL + 应用服务
|
||||
├── scripts/
|
||||
│ ├── start.bat # Windows CMD 启动脚本
|
||||
│ └── stop.bat # Windows CMD 停止脚本
|
||||
├── api/
|
||||
│ ├── app/
|
||||
│ │ ├── config.py # 数据库、端口、CORS、日志和 JWT 配置
|
||||
│ │ ├── database.py # SQLAlchemy 引擎、会话、建表和轻量迁移
|
||||
│ │ ├── main.py # FastAPI 应用、中间件、路由和静态文件
|
||||
│ │ ├── models/ # SQLAlchemy 数据模型
|
||||
│ │ ├── schemas/ # Pydantic 请求/响应模型
|
||||
│ │ ├── routers/ # API 路由
|
||||
│ │ └── utils/ # 日志、认证、同步、WebDAV、加密等工具
|
||||
│ └── webui/ # 已构建前端静态资源,启动时由 main.py 覆盖
|
||||
└── WebUI/
|
||||
├── package.json
|
||||
├── package-lock.json
|
||||
├── vite.config.ts
|
||||
└── src/
|
||||
├── api/ # Axios 接口封装
|
||||
├── components/ # 复用组件和弹窗
|
||||
├── router/ # 路由和认证守卫
|
||||
├── stores/ # Pinia 状态
|
||||
├── styles/ # 全局 SCSS
|
||||
├── utils/ # 日期、拼音、优先级工具
|
||||
└── views/ # 页面视图
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
当前仓库只保留根目录 `README.md` 作为统一文档入口;没有 `tests/` 目录,也没有独立的测试脚本。
|
||||
|
||||
### 环境要求
|
||||
## 环境要求
|
||||
|
||||
- Python 3.10+
|
||||
- Node.js 18+
|
||||
- npm
|
||||
- Windows CMD 是默认运行环境。
|
||||
- Python 3.10+。当前本机检查到的解释器是 Python 3.11.0。
|
||||
- Node.js 满足 Vite 7 要求:`^20.19.0` 或 `>=22.12.0`。
|
||||
- npm。Windows 下优先使用 `npm.cmd`,避免 PowerShell shim 路径问题。
|
||||
- PostgreSQL。默认连接由 `api/app/config.py` 中的 `DATABASE_URL` 读取,推荐通过环境变量覆盖。
|
||||
|
||||
### 安装与运行
|
||||
## 启动与停止
|
||||
|
||||
```bash
|
||||
# 1. 克隆项目
|
||||
git clone <your-repo-url>
|
||||
cd ToDoList
|
||||
官方启动入口是脚本,不建议把 `python`、`npm`、`uvicorn` 作为日常手工启动方式。
|
||||
|
||||
# 2. 安装 Python 依赖
|
||||
pip install -r requirements.txt
|
||||
```cmd
|
||||
scripts\start.bat
|
||||
```
|
||||
|
||||
# 3. 一键启动(自动编译前端 + 启动后端)
|
||||
python main.py
|
||||
`scripts\start.bat` 会执行以下动作:
|
||||
|
||||
1. 切换到项目根目录。
|
||||
2. 检查 Python 和项目文件。
|
||||
3. 首次运行时安装 Python 依赖。
|
||||
4. 检查并释放 `23994` 端口。
|
||||
5. 运行 `main.py`。
|
||||
6. `main.py` 会按需执行前端依赖安装、`npm.cmd run build`、复制 `WebUI/dist` 到 `api/webui`,再启动 FastAPI。
|
||||
|
||||
停止服务:
|
||||
|
||||
```cmd
|
||||
scripts\stop.bat
|
||||
```
|
||||
|
||||
启动后访问:
|
||||
|
||||
- 前端页面:http://localhost:23994
|
||||
- API 文档:http://localhost:23994/docs
|
||||
|
||||
### 前端开发模式
|
||||
|
||||
如果需要前后端分离开发(热更新):
|
||||
|
||||
```bash
|
||||
# 终端 1 — 启动后端
|
||||
cd api
|
||||
uvicorn app.main:app --host 0.0.0.0 --port 23994
|
||||
|
||||
# 终端 2 — 启动前端开发服务器
|
||||
cd WebUI
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
前端开发服务器运行在 http://localhost:5173,已配置 API 代理到后端。
|
||||
|
||||
## API 概览
|
||||
|
||||
所有接口均以 `/api` 为前缀,启动后端后访问 `/docs` 查看 Swagger 文档。
|
||||
|
||||
| 模块 | 前缀 | 说明 |
|
||||
|------|------|------|
|
||||
| 任务 | `/api/tasks` | 任务 CRUD、状态切换、批量操作 |
|
||||
| 分类 | `/api/categories` | 分类 CRUD |
|
||||
| 标签 | `/api/tags` | 标签 CRUD |
|
||||
| 习惯 | `/api/habits` | 习惯、习惯组、打卡记录 |
|
||||
| 纪念日 | `/api/anniversaries` | 纪念日、纪念日分类 |
|
||||
| 设置 | `/api/user-settings` | 用户偏好设置 |
|
||||
| 健康检查 | `/health` | 服务状态检查 |
|
||||
|
||||
## 数据模型关系
|
||||
|
||||
```
|
||||
Category ──< Task >── Tag
|
||||
│
|
||||
HabitGroup ──< Habit ──< HabitCheckin
|
||||
|
||||
AnniversaryCategory ──< Anniversary
|
||||
|
||||
UserSettings (单例)
|
||||
```
|
||||
- 健康检查:http://localhost:23994/health
|
||||
|
||||
## 配置说明
|
||||
|
||||
在 `api/app/config.py` 中可以修改:
|
||||
主要配置位于 `api/app/config.py`。
|
||||
|
||||
| 配置项 | 默认值 | 说明 |
|
||||
|--------|--------|------|
|
||||
| HOST | `0.0.0.0` | 监听地址 |
|
||||
| PORT | `23994` | 服务端口 |
|
||||
| DATABASE_PATH | `api/data/todo.db` | SQLite 数据库路径 |
|
||||
| WEBUI_PATH | `api/webui` | 前端静态文件目录 |
|
||||
| CORS_ORIGINS | `localhost:5173, 23994` | 允许的跨域来源 |
|
||||
| 配置项 | 当前默认值 | 说明 |
|
||||
|--------|------------|------|
|
||||
| `DATABASE_URL` | 从环境变量读取,否则使用内置 PostgreSQL 连接 | SQLAlchemy 数据库连接串 |
|
||||
| `WEBUI_PATH` | `api/webui` | 后端挂载的前端静态资源目录 |
|
||||
| `CORS_ORIGINS` | `http://localhost:5173`、`http://localhost:23994` | 允许的跨域来源 |
|
||||
| `LOG_LEVEL` | `INFO` | 应用日志级别 |
|
||||
| `LOG_DIR` | `api/logs` | 文件日志目录 |
|
||||
| `HOST` | `0.0.0.0` | 服务监听地址 |
|
||||
| `PORT` | `23994` | 服务端口 |
|
||||
| `JWT_SECRET` | 首次启动写入 `api/data/.jwt_secret` | JWT 和 WebDAV 密码加密派生密钥 |
|
||||
|
||||
数据库文件和日志文件会在首次运行时自动创建,无需手动初始化。
|
||||
运行时目录:
|
||||
|
||||
## 部署
|
||||
- `api/logs/app.log`:应用日志,按天轮转。
|
||||
- `api/data/.jwt_secret`:本机 JWT 密钥文件。
|
||||
- `api/data/backups/`:WebDAV 拉取前的本地 JSON 快照。
|
||||
|
||||
项目支持在 Windows 和 Linux 上运行。`main.py` 会自动处理平台差异(npm 命令、端口占用检测等)。
|
||||
## API 概览
|
||||
|
||||
对于生产环境部署,建议:
|
||||
- 使用 `gunicorn` + `uvicorn worker` 替代直接运行
|
||||
- 配置反向代理(Nginx)
|
||||
- 数据库可替换为 PostgreSQL 或 MySQL(修改 `config.py` 中的 `DATABASE_URL`)
|
||||
除 `/health` 外,业务接口均在 `/api` 下。大多数 `/api/*` 接口需要登录 Cookie。
|
||||
|
||||
| 模块 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| 认证 | `/api/auth/status`、`/api/auth/setup`、`/api/auth/login`、`/api/auth/logout`、`/api/auth/me`、`/api/auth/change-password` | 首次设置、登录状态、登录退出和改密 |
|
||||
| 任务 | `/api/tasks`、`/api/tasks/{task_id}/toggle` | 任务 CRUD 和完成切换 |
|
||||
| 分类 | `/api/categories` | 任务分类 CRUD |
|
||||
| 标签 | `/api/tags` | 标签列表、创建、删除 |
|
||||
| 习惯分组 | `/api/habit-groups` | 习惯分组 CRUD |
|
||||
| 习惯 | `/api/habits`、`/api/habits/{habit_id}/checkins` | 习惯 CRUD、归档和打卡 |
|
||||
| 纪念日 | `/api/anniversary-categories`、`/api/anniversaries` | 纪念日分类和纪念日 CRUD |
|
||||
| 目标 | `/api/goals` | 目标、步骤、复盘、关联任务和目标打卡 |
|
||||
| 证书 | `/api/certificate-categories`、`/api/certificates` | 证书分类和证书 CRUD |
|
||||
| 用户设置 | `/api/user-settings` | 用户偏好获取和更新 |
|
||||
| 备份 | `/api/backup/export`、`/api/backup/import` | JSON 备份导出和覆盖导入 |
|
||||
| WebDAV 同步 | `/api/sync/config`、`/api/sync/test`、`/api/sync/push`、`/api/sync/pull`、`/api/sync/sync`、`/api/sync/status`、`/api/sync/remote` | 同步配置和操作 |
|
||||
| 健康检查 | `/health` | 服务状态 |
|
||||
|
||||
## 数据模型关系
|
||||
|
||||
```text
|
||||
Category 1 -> N Task
|
||||
Task N <-> N Tag
|
||||
|
||||
HabitGroup 1 -> N Habit
|
||||
Habit 1 -> N HabitCheckin
|
||||
|
||||
AnniversaryCategory 1 -> N Anniversary
|
||||
|
||||
Category 1 -> N Goal
|
||||
Goal 1 -> N GoalStep
|
||||
GoalStep 1 -> N GoalStep
|
||||
Goal 1 -> N GoalReview
|
||||
Goal 1 -> N GoalCheckin
|
||||
Goal N <-> N Task
|
||||
|
||||
CertificateCategory 1 -> N Certificate
|
||||
|
||||
UserSettings 1 -> 1
|
||||
SyncSettings 1 -> 1
|
||||
```
|
||||
|
||||
可同步模型使用 `uuid`、`sync_version`、`is_deleted` 支持 WebDAV 同步和软删除传播。
|
||||
|
||||
## Docker 部署
|
||||
|
||||
`docker-compose.yml` 包含两个服务:
|
||||
|
||||
- `db`:PostgreSQL,容器名 `elysia-todo-db`。
|
||||
- `elysia-todo`:应用服务,容器名 `elysia-todo`,对外暴露 `23994`。
|
||||
|
||||
注意:`Dockerfile` 只复制 `api/` 和已经存在的 `api/webui/`,不会在镜像构建时编译 `WebUI/`。构建镜像前需要先通过启动流程或构建流程生成前端产物并复制到 `api/webui`。
|
||||
|
||||
## 当前已知差异
|
||||
|
||||
- `requirements.txt` 当前未显式列出 `requests`,但 `api/app/utils/webdav.py` 会导入并使用它。干净环境如果报 `ModuleNotFoundError: No module named 'requests'`,需要先补齐依赖配置。
|
||||
- 连接串、容器数据库密码等敏感配置当前存在于仓库配置文件中。对外发布或多人协作前,建议改为环境变量和 `.env.example`。
|
||||
- 仓库当前没有测试目录和测试脚本。新增功能时应先补 `scripts/` 下的验证脚本,再通过脚本执行验证。
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# Vue 3 + TypeScript + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
|
||||
@@ -1,431 +0,0 @@
|
||||
# WebDAV 同步功能设计文档
|
||||
|
||||
日期: 2026-05-17
|
||||
|
||||
## 1. 概述
|
||||
|
||||
**目标**: 支持通过 WebDAV(Alist)在多设备间同步所有待办数据。
|
||||
|
||||
**成功指标**:
|
||||
- 可以配置 Alist WebDAV 连接并测试连通性
|
||||
- 支持 push(本地→远端)、pull(远端→本地)、sync(双向合并)三种同步方向
|
||||
- 同步期间禁止所有前端写操作,显示同步遮罩
|
||||
- 数据不会因同步而丢失(自动备份机制)
|
||||
|
||||
**范围内**: 所有数据模型的全量同步
|
||||
**范围外**: 增量同步、实时同步、冲突解决 UI、多用户协作
|
||||
|
||||
## 2. 架构
|
||||
|
||||
```
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ 前端 (Vue) │────▶│ 后端 (FastAPI) │────▶│ Alist WebDAV │
|
||||
│ 同步设置页面 │ │ 同步 API + 锁 │ │ JSON 文件存储 │
|
||||
└──────────────┘ └──────────────┘ └──────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ PostgreSQL │
|
||||
│ (本地数据) │
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
### 数据流
|
||||
|
||||
1. 前端发起同步请求 (push/pull/sync)
|
||||
2. 后端获取同步锁 → 禁止其他写操作
|
||||
3. 从 PostgreSQL 读取/写入本地数据
|
||||
4. 通过 WebDAV HTTP 客户端与 Alist 交互(读写 JSON 文件)
|
||||
5. 释放同步锁 → 前端恢复正常操作
|
||||
6. 前端轮询同步状态,显示进度
|
||||
|
||||
## 3. 数据模型变更
|
||||
|
||||
### 3.1 所有可同步模型新增字段
|
||||
|
||||
| 字段 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `uuid` | `String(36)` | UUID4 自动生成 | 全局唯一标识,用于同步匹配 |
|
||||
| `sync_version` | `Integer` | `1` | 每次修改 +1,用于 LWW 判定 |
|
||||
| `is_deleted` | `Boolean` | `False` | 软删除墓碑标记 |
|
||||
|
||||
**需要变更的模型**:
|
||||
- `Task`
|
||||
- `Category`
|
||||
- `Tag`
|
||||
- `HabitGroup`
|
||||
- `Habit`
|
||||
- `HabitCheckin`
|
||||
- `AnniversaryCategory`
|
||||
- `Anniversary`
|
||||
- `Goal`
|
||||
- `GoalStep`
|
||||
- `GoalReview`
|
||||
|
||||
**关联表** (`task_tags`, `goal_tasks`): 不加字段,序列化时带上两边实体的 uuid,反序列化时用 uuid 查找对应本地 ID 重建关联。
|
||||
|
||||
### 3.2 新增 SyncSettings 模型
|
||||
|
||||
```python
|
||||
class SyncSettings(Base):
|
||||
__tablename__ = "sync_settings"
|
||||
|
||||
id = Column(Integer, primary_key=True, default=1)
|
||||
|
||||
# WebDAV 连接配置
|
||||
webdav_url = Column(String(500), nullable=True)
|
||||
webdav_username = Column(String(200), nullable=True)
|
||||
webdav_password = Column(String(500), nullable=True) # AES-256-GCM 加密存储
|
||||
webdav_path = Column(String(200), default="/elysia-todo/")
|
||||
|
||||
# 同步状态
|
||||
sync_enabled = Column(Boolean, default=False)
|
||||
last_sync_at = Column(DateTime, nullable=True)
|
||||
last_sync_version = Column(Integer, default=0)
|
||||
auto_sync = Column(Boolean, default=False)
|
||||
auto_sync_interval = Column(Integer, default=300) # 秒
|
||||
|
||||
# 创建时间
|
||||
created_at = Column(DateTime, default=utcnow)
|
||||
updated_at = Column(DateTime, default=utcnow, onupdate=utcnow)
|
||||
```
|
||||
|
||||
### 3.3 现有模型自动迁移
|
||||
|
||||
`init_db()` 已有的 `ALTER TABLE ADD COLUMN` 机制会自动为现有表添加新列。需要注意:
|
||||
- `uuid` 列需要为已有记录回填 UUID4
|
||||
- `sync_version` 列默认设为 1
|
||||
- `is_deleted` 列默认设为 False
|
||||
|
||||
## 4. AES 加密方案
|
||||
|
||||
```python
|
||||
# api/app/utils/crypto.py
|
||||
|
||||
算法: AES-256-GCM (认证加密)
|
||||
密钥派生: PBKDF2-SHA256(JWT_SECRET, salt="elysia-todo-sync", iterations=480000)
|
||||
存储格式: base64(iv[12] + ciphertext + tag[16])
|
||||
```
|
||||
|
||||
- 密钥从现有的 `JWT_SECRET` 派生,无需额外的密钥管理
|
||||
- 旋转 JWT_SECRET 时旧密码解密失败 → 需要重新配置 WebDAV 密码
|
||||
- 解密失败时返回 Null,前端提示 "请重新配置 WebDAV 密码"
|
||||
|
||||
## 5. WebDAV 文件结构
|
||||
|
||||
```
|
||||
/elysia-todo/
|
||||
├── manifest.json # 同步元数据
|
||||
├── data/
|
||||
│ ├── user_settings.json # 仅偏好字段,不含密码
|
||||
│ ├── categories.json
|
||||
│ ├── tasks.json
|
||||
│ ├── tags.json
|
||||
│ ├── task_tags.json # [{task_uuid, tag_uuid}]
|
||||
│ ├── habit_groups.json
|
||||
│ ├── habits.json
|
||||
│ ├── habit_checkins.json
|
||||
│ ├── anniversary_categories.json
|
||||
│ ├── anniversaries.json
|
||||
│ ├── goals.json
|
||||
│ ├── goal_steps.json
|
||||
│ ├── goal_reviews.json
|
||||
│ └── goal_tasks.json # [{goal_uuid, task_uuid}]
|
||||
└── backups/
|
||||
└── 2026-05-17T10-00-00/ # push 前自动备份远端数据
|
||||
└── data/
|
||||
└── ... (被覆盖前的快照)
|
||||
```
|
||||
|
||||
### manifest.json 格式
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"last_sync_at": "2026-05-17T10:00:00Z",
|
||||
"collections": {
|
||||
"categories": { "count": 5, "updated_at": "2026-05-17T10:00:00Z" },
|
||||
"tasks": { "count": 42, "updated_at": "2026-05-17T10:00:00Z" },
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### data/*.json 格式
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"collection": "tasks",
|
||||
"updated_at": "2026-05-17T10:00:00Z",
|
||||
"items": [
|
||||
{
|
||||
"uuid": "a1b2c3d4-...",
|
||||
"sync_version": 3,
|
||||
"is_deleted": false,
|
||||
"id": 1,
|
||||
"title": "买牛奶",
|
||||
...
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 6. 同步协议
|
||||
|
||||
### 6.1 Push(本地 → 远端)
|
||||
|
||||
```
|
||||
1. 获取同步锁
|
||||
2. 备份远端当前数据到 /backups/{timestamp}/
|
||||
3. 从 PostgreSQL 读取所有本地数据
|
||||
4. 序列化为 JSON 文件
|
||||
5. 逐个 PUT 到 WebDAV(先 data/,再 manifest.json)
|
||||
6. 更新本地 sync_settings.last_sync_at
|
||||
7. 释放同步锁
|
||||
```
|
||||
|
||||
### 6.2 Pull(远端 → 本地)
|
||||
|
||||
```
|
||||
1. 获取同步锁
|
||||
2. 备份本地数据到 api/data/backups/{timestamp}/ (JSON 快照)
|
||||
3. 从 WebDAV GET manifest.json
|
||||
4. 逐个 GET data/*.json
|
||||
5. 清空本地数据库(DELETE 所有表)
|
||||
6. 按 FK 依赖顺序插入远端数据:
|
||||
user_settings → categories → tags → habits/habit_groups → ... → task_tags/goal_tasks
|
||||
7. 为缺少 uuid 的记录生成 uuid
|
||||
8. 更新 sync_settings.last_sync_at
|
||||
9. 释放同步锁
|
||||
```
|
||||
|
||||
### 6.3 Sync(双向合并,LWW)
|
||||
|
||||
```
|
||||
1. 获取同步锁
|
||||
2. 从 WebDAV GET manifest.json + data/*.json (远端快照)
|
||||
3. 从 PostgreSQL 读取本地数据 (本地快照)
|
||||
4. 对每个 collection 做合并:
|
||||
a. 以 uuid 为 key 建立两边的索引
|
||||
b. 遍历所有 uuid 的并集:
|
||||
- 仅本地有 → 推送到远端
|
||||
- 仅远端有 → 插入本地(分配新本地 ID)
|
||||
- 两边都有:
|
||||
- compare sync_version: 大的覆盖小的
|
||||
- sync_version 相同 → 以远端为准
|
||||
- 任何一边 is_deleted=True → 在两边都标记删除
|
||||
c. 关联表: 合并去重 (以 uuid 对组合为 key)
|
||||
5. 将合并结果写回本地 DB 和远端 WebDAV
|
||||
6. 更新 sync_settings.last_sync_at
|
||||
7. 释放同步锁
|
||||
```
|
||||
|
||||
### 6.4 冲突策略
|
||||
|
||||
- **LWW (Last Write Wins)**: 比较 `sync_version`,数值大的赢
|
||||
- **同版本冲突**: 以远端为准
|
||||
- **删除传播**: `is_deleted=True` 的墓碑会在双向同步中传播,不对已删除记录做内容合并
|
||||
- **墓碑清理**: 不自动清理,后续可加手动清理功能
|
||||
|
||||
## 7. 全局同步锁
|
||||
|
||||
```python
|
||||
# api/app/utils/sync_lock.py
|
||||
|
||||
_sync_lock = threading.Lock()
|
||||
_sync_in_progress = False
|
||||
|
||||
def acquire_sync_lock() -> bool:
|
||||
"""非阻塞获取同步锁"""
|
||||
acquired = _sync_lock.acquire(blocking=False)
|
||||
if acquired:
|
||||
global _sync_in_progress
|
||||
_sync_in_progress = True
|
||||
return acquired
|
||||
|
||||
def release_sync_lock():
|
||||
global _sync_in_progress
|
||||
_sync_in_progress = False
|
||||
_sync_lock.release()
|
||||
|
||||
def is_syncing() -> bool:
|
||||
return _sync_in_progress
|
||||
```
|
||||
|
||||
### 中间件拦截
|
||||
|
||||
在 `auth_middleware` 之后、路由处理之前,检查同步状态:
|
||||
|
||||
```python
|
||||
# 如果正在同步,对所有 /api/* 写请求(非 /api/sync/*)返回 503
|
||||
if is_syncing() and request.method in ("POST", "PUT", "PATCH", "DELETE"):
|
||||
if not path.startswith("/api/sync"):
|
||||
return JSONResponse(status_code=503, content={"detail": "正在同步,请稍后"})
|
||||
```
|
||||
|
||||
## 8. API 端点
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/api/sync/config` | 获取 WebDAV 配置(密码脱敏为 `***`) |
|
||||
| PUT | `/api/sync/config` | 保存 WebDAV 配置(密码 AES 加密存储) |
|
||||
| POST | `/api/sync/test` | 测试 WebDAV 连接 |
|
||||
| POST | `/api/sync/push` | 推送本地数据到远端 |
|
||||
| POST | `/api/sync/pull` | 从远端拉取数据覆盖本地 |
|
||||
| POST | `/api/sync/sync` | 双向合并同步 |
|
||||
| GET | `/api/sync/status` | 查询同步状态(是否正在同步、上次时间、版本号) |
|
||||
| DELETE | `/api/sync/remote` | 清空远端数据(需二次确认) |
|
||||
|
||||
### 请求/响应示例
|
||||
|
||||
**PUT /api/sync/config**
|
||||
```json
|
||||
{
|
||||
"webdav_url": "https://alist.example.com/dav",
|
||||
"webdav_username": "user",
|
||||
"webdav_password": "mypassword",
|
||||
"webdav_path": "/elysia-todo/",
|
||||
"auto_sync": false,
|
||||
"auto_sync_interval": 300
|
||||
}
|
||||
```
|
||||
|
||||
**GET /api/sync/config** (响应,密码脱敏)
|
||||
```json
|
||||
{
|
||||
"webdav_url": "https://alist.example.com/dav",
|
||||
"webdav_username": "user",
|
||||
"webdav_password": "***",
|
||||
"webdav_path": "/elysia-todo/",
|
||||
"sync_enabled": true,
|
||||
"last_sync_at": "2026-05-17T10:00:00Z",
|
||||
"auto_sync": false,
|
||||
"auto_sync_interval": 300
|
||||
}
|
||||
```
|
||||
|
||||
**GET /api/sync/status**
|
||||
```json
|
||||
{
|
||||
"syncing": false,
|
||||
"last_sync_at": "2026-05-17T10:00:00Z",
|
||||
"last_sync_version": 15,
|
||||
"sync_enabled": true
|
||||
}
|
||||
```
|
||||
|
||||
## 9. user_settings 同步范围
|
||||
|
||||
**同步**: nickname, avatar, signature, birthday, email, site_name, theme, language, default_view, default_sort_by, default_sort_order
|
||||
|
||||
**不同步**: password_hash, token_version, id, created_at, updated_at
|
||||
|
||||
## 10. 前端设计
|
||||
|
||||
### 设置页面新增 "数据同步" 标签页
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ 数据同步 │
|
||||
├─────────────────────────────────────────┤
|
||||
│ WebDAV 配置 │
|
||||
│ ┌─────────────────────────────────────┐ │
|
||||
│ │ 服务器地址: [_____________________] │ │
|
||||
│ │ 用户名: [_____________________] │ │
|
||||
│ │ 密码: [••••••••••] [测试连接] │ │
|
||||
│ │ 远端路径: [/elysia-todo/______] │ │
|
||||
│ └─────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ 同步操作 │
|
||||
│ ┌─────────────────────────────────────┐ │
|
||||
│ │ ○ 推送 (本地 → 远端) [危险操作] │ │
|
||||
│ │ ○ 拉取 (远端 → 本地) [危险操作] │ │
|
||||
│ │ ● 双向合并 (推荐) │ │
|
||||
│ │ │ │
|
||||
│ │ [开始同步] │ │
|
||||
│ └─────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ 自动同步 │
|
||||
│ ┌─────────────────────────────────────┐ │
|
||||
│ │ [开关] 自动同步 间隔: [300] 秒 │ │
|
||||
│ └─────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ 上次同步: 2026-05-17 10:00:00 │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 同步遮罩 UI
|
||||
|
||||
同步进行中时,全屏半透明遮罩:
|
||||
- "正在同步数据,请勿关闭页面..."
|
||||
- 进度指示:正在处理哪个 collection
|
||||
- 禁止所有操作
|
||||
|
||||
### 前端 Store
|
||||
|
||||
`useSyncStore.ts`:
|
||||
- `config`: WebDAV 配置
|
||||
- `syncing`: 是否正在同步
|
||||
- `progress`: 同步进度信息
|
||||
- `fetchConfig()`: GET /api/sync/config
|
||||
- `saveConfig(config)`: PUT /api/sync/config
|
||||
- `testConnection()`: POST /api/sync/test
|
||||
- `startSync(direction)`: POST /api/sync/{direction}
|
||||
- `pollStatus()`: 轮询 GET /api/sync/status
|
||||
|
||||
## 11. 依赖项
|
||||
|
||||
### Python 新增依赖
|
||||
|
||||
```
|
||||
webdavclient3>=4.0 # WebDAV 客户端
|
||||
pycryptodome>=3.20 # AES-256-GCM 加密 (如不用 hashlib + cryptography)
|
||||
```
|
||||
|
||||
实际上可以用 `requests` 手写 WebDAV 操作(PUT/GET/PROPFIND/MKCOL),避免 `webdavclient3` 的兼容性问题。Alist 的 WebDAV 实现比较标准,用 `requests` 就够了。
|
||||
|
||||
**决定**: 使用 `requests` + 手写 WebDAV 操作,不引入额外 WebDAV 库。
|
||||
|
||||
## 12. 文件结构
|
||||
|
||||
### 后端新增文件
|
||||
|
||||
```
|
||||
api/app/
|
||||
├── models/
|
||||
│ └── sync_settings.py # 新增
|
||||
├── schemas/
|
||||
│ └── sync.py # 新增
|
||||
├── routers/
|
||||
│ └── sync.py # 新增
|
||||
└── utils/
|
||||
├── crypto.py # 新增: AES-256-GCM 加解密
|
||||
├── sync_lock.py # 新增: 全局同步锁
|
||||
├── webdav.py # 新增: WebDAV 客户端
|
||||
└── sync_service.py # 新增: 同步核心逻辑
|
||||
```
|
||||
|
||||
### 前端新增文件
|
||||
|
||||
```
|
||||
WebUI/src/
|
||||
├── api/
|
||||
│ └── sync.ts # 新增: 同步 API
|
||||
├── stores/
|
||||
│ └── useSyncStore.ts # 新增: 同步状态管理
|
||||
└── views/
|
||||
└── settings/
|
||||
└── SyncView.vue # 新增: 同步设置页面
|
||||
```
|
||||
|
||||
## 13. 风险与注意
|
||||
|
||||
| 风险 | 缓解措施 |
|
||||
|------|----------|
|
||||
| 同步锁粒度过粗(锁住全部写操作) | 单用户 App,锁住期间显示遮罩,体验可接受 |
|
||||
| 大数据量同步超时 | 设置合理的 requests timeout,分片上传 |
|
||||
| Alist WebDAV 兼容性 | 用标准 HTTP 方法 (PUT/GET/MKCOL/DELETE),避免非标准扩展 |
|
||||
| JWT_SECRET 旋转导致密码解密失败 | 前端提示 "请重新配置 WebDAV 密码" |
|
||||
| 并发同步 | 非阻塞锁,同一时间只允许一个同步过程 |
|
||||
| pull 操作清空本地数据 | pull 前自动备份到 api/data/backups/ |
|
||||
Reference in New Issue
Block a user