feat: 实现v1.2.0云同步与实体重构核心功能
1. 重构用户与任务实体:UserEntity实现IUser<Guid>,TaskEntity继承ABP风格FullAuditedEntityWithUser,主键从int改为Guid 2. 新增云同步代理系统:嵌入式WebServer支持CloudSyncProxy转发云同步请求,新增配置API与持久化 3. 完善前端适配:新增Guid工具函数,更新任务类型定义与API交互逻辑,调整云同步设置弹窗适配本地代理 4. 文档与配置优化:更新文档结构,新增部署文档、版本记录,统一各项目配置项 5. 补充测试与迁移:新增单元测试,更新EF Core数据库迁移快照
This commit is contained in:
@@ -31,11 +31,11 @@
|
||||
Hua.Todo/
|
||||
├── docs/ # 文档目录
|
||||
│ ├── manual/ # 用户/开发者手册
|
||||
│ │ ├── 技术栈与模块.md
|
||||
│ │ ├── 版本记录.md
|
||||
│ │ ├── 技术设计文档.md(本文件)
|
||||
│ │ ├── 代码规范文档.md
|
||||
│ │ └── 部署文档.md
|
||||
│ │ ├── 03-技术栈与模块.md
|
||||
│ │ ├── 06-版本记录.md
|
||||
│ │ ├── 01-技术设计文档.md(本文件)
|
||||
│ │ ├── 04-代码规范文档.md
|
||||
│ │ └── 05-部署文档.md
|
||||
│ └── project/ # 项目进度/需求文档
|
||||
│ ├── 产品需求文档.md
|
||||
│ └── ...
|
||||
@@ -0,0 +1,512 @@
|
||||
# Todo 待办项云同步规则
|
||||
|
||||
> 本文档汇总 Hua.Todo 项目的 Todo 待办项云同步完整规则,涵盖架构、API 契约、认证鉴权、同步工作流、安全策略与可控落盘等。
|
||||
>
|
||||
> 术语:本文中"任务 / Todo 待办项"均为业务实体(对应代码 `Task` / `SubTask` / `TaskEntity`),与编码侧的"研发工单"无关。
|
||||
|
||||
---
|
||||
|
||||
## 一、部署架构与职责边界
|
||||
|
||||
### 1.1 两种运行模式
|
||||
|
||||
| 模式 | 宿主 | 云同步端点 | 说明 |
|
||||
|---|---|---|---|
|
||||
| **嵌入式** | MAUI / Avalonia | 不暴露 | 嵌入 Kestrel + WebView,仅注册 `AddApplicationServices()`,托管本地 `/api/*`;云同步需连接外部 Host |
|
||||
| **独立服务端** | Hua.Todo.Host | 完整暴露 | 同时注册 `AddApplicationServices()` + `AddCloudSyncServer()`,对外提供全部云同步端点 |
|
||||
|
||||
### 1.2 DI 注册边界(强制)
|
||||
|
||||
```
|
||||
Hua.Todo.Application (共享层)
|
||||
├── AddApplicationServices() ← Todo CRUD,两端均注册
|
||||
│ ├── TodoDbContext / TaskRepository / TaskService
|
||||
│ └── DynamicApi (本地 /api/*)
|
||||
│
|
||||
└── AddCloudSyncServer() ← 云同步能力,仅 Host 注册
|
||||
├── CloudAuthService / CloudTaskSyncService
|
||||
├── SecurityPolicyService / CloudAdminService / CloudProbeService
|
||||
├── SessionAuthenticationHandler (Bearer Token 鉴权)
|
||||
└── MapCloudSyncEndpoints() (13 个云同步端点)
|
||||
```
|
||||
|
||||
- **MAUI / Avalonia**:`MauiProgram.cs` / `AvaloniaProgram.cs` **只能**调用 `AddApplicationServices()`,禁止调用 `AddCloudSyncServer()`。
|
||||
- **Hua.Todo.Host**:`Program.cs` 同时调用两者。
|
||||
|
||||
### 1.3 同源 Host 改造(研发工单 08)
|
||||
|
||||
前端云同步请求统一走当前 Host 同源,不再直连外部 `serverUrl`:
|
||||
|
||||
- **Host 模式(Vite dev)**:Vite proxy 将 `/auth`、`/tasks`、`/sync`、`/security`、`/cloud-sync` 转发到 Host(`:5173`)。
|
||||
- **MAUI 模式(WebView)**:Vue 静态部署到嵌入服务器同源,无需代理。
|
||||
- `cloudClient.ts` 不设外部 baseURL,由浏览器自动同源。
|
||||
- 用户配置的 `serverUrl` 仅用于服务端代理探测(`POST /cloud-sync/probe`),不再作为 API 基址。
|
||||
|
||||
---
|
||||
|
||||
## 二、数据隔离规则
|
||||
|
||||
### 2.1 UserId 隔离(强制)
|
||||
|
||||
- `Tasks` 表含 `UserId` 字段(外键),所有云同步 API 按当前登录用户隔离读写。
|
||||
- 本地模式(嵌入式):`Tasks.UserId = TodoUserIds.LocalUserId = "local"`,与云端用户共存不冲突。
|
||||
- 服务端查询/写入必须从会话上下文提取 `UserId`,不得接受客户端传入的 `UserId` 参数。
|
||||
|
||||
### 2.2 同步数据范围
|
||||
|
||||
- 每次同步操作仅涉及**当前登录用户**的 Todo 待办项。
|
||||
- `GET /tasks` 返回该用户全量任务(`CloudTaskItem[]`)。
|
||||
- `POST /sync` 的 upsert/deletes 均限定在当前用户数据范围内执行。
|
||||
|
||||
---
|
||||
|
||||
## 三、认证与会话管理
|
||||
|
||||
### 3.1 认证方式
|
||||
|
||||
- **Bearer Token**:所有云同步端点需携带 `Authorization: Bearer {accessToken}`。
|
||||
- Token 即 `UserSessionEntity.SessionId`(GUID),存储于服务端 `UserSessions` 表,非纯无状态 JWT。
|
||||
|
||||
### 3.2 会话生命周期
|
||||
|
||||
| 操作 | 端点 | 说明 |
|
||||
|---|---|---|
|
||||
| 初始化管理员 | `POST /auth/bootstrap` | 仅当系统无云用户时可调用一次,自动生成随机密码 |
|
||||
| 登录 | `POST /auth/login` | 验证用户名/密码(Argon2id 哈希),创建 `UserSessionEntity`,返回 AccessToken + 权限列表 |
|
||||
| 登出 | `POST /auth/logout` | 删除当前会话记录,Token 立即失效 |
|
||||
| 修改密码 | `POST /auth/change-password` | 需提供当前密码,更新后旧会话不失效 |
|
||||
|
||||
### 3.3 SessionAuthenticationHandler 鉴权流程
|
||||
|
||||
1. 从 `Authorization` 头提取 Bearer Token(GUID 格式 SessionId)
|
||||
2. 查 `UserSessions` 表:校验 SessionId 存在且 `ExpiresAtUtc > DateTime.UtcNow`
|
||||
3. 加载用户角色,通过 `IRolePermissionMapper` 获取权限列表
|
||||
4. 检查 `SecurityPolicies.AllowSync`:若为 `false`,从权限中移除 `sync:write`
|
||||
5. 构造 `ClaimsIdentity`(含 `sub`=UserId、`role`、`perm` Claims),注入 `HttpContext.User`
|
||||
|
||||
---
|
||||
|
||||
## 四、RBAC 权限模型
|
||||
|
||||
### 4.1 权限点定义(6 个)
|
||||
|
||||
| 权限点 | 常量 | 说明 |
|
||||
|---|---|---|
|
||||
| `tasks:read` | CloudPermissions.TasksRead | 读取 Todo 待办项 |
|
||||
| `tasks:write` | CloudPermissions.TasksWrite | 写入 Todo 待办项 |
|
||||
| `sync:write` | CloudPermissions.SyncWrite | 执行同步操作(受 `AllowSync` 策略叠加限制) |
|
||||
| `policy:read` | CloudPermissions.PolicyRead | 读取安全策略 |
|
||||
| `policy:write` | CloudPermissions.PolicyWrite | 修改安全策略 |
|
||||
| `users:manage` | CloudPermissions.UsersManage | 管理用户(Admin 专属) |
|
||||
|
||||
### 4.2 角色与权限映射
|
||||
|
||||
| 角色 | 权限 |
|
||||
|---|---|
|
||||
| `admin` | 全部 6 个权限 |
|
||||
| `user`(默认) | `tasks:read`、`tasks:write`、`sync:write`、`policy:read` |
|
||||
| `readonly` | `tasks:read`、`policy:read` |
|
||||
| `nosync` | `tasks:read`、`tasks:write`、`policy:read` |
|
||||
|
||||
> `sync:write` 权限在鉴权时还会受 `SecurityPolicies.AllowSync` 二次过滤:若策略禁止同步,即使角色拥有 `sync:write` 也会被移除。
|
||||
|
||||
### 4.3 端点权限要求
|
||||
|
||||
| 端点 | 方法 | 所需权限 |
|
||||
|---|---|---|
|
||||
| `/tasks` | GET | `tasks:read` |
|
||||
| `/sync` | POST | `sync:write` |
|
||||
| `/security/policy` | GET | `policy:read` |
|
||||
| `/security/policy` | PUT | `policy:write` |
|
||||
| `/admin/*` | GET/POST/DELETE | `users:manage` |
|
||||
| `/cloud-sync/probe` | POST | —(匿名访问) |
|
||||
|
||||
---
|
||||
|
||||
## 五、安全策略与可控落盘
|
||||
|
||||
### 5.1 SecurityPolicies 表
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `AllowPersist` | bool | 是否允许客户端持久化 Todo 数据到本地存储 |
|
||||
| `AllowSync` | bool | 是否允许该用户执行同步写入 |
|
||||
| `IsTrustedDeviceOnly` | bool | 是否仅限受信任终端(预留) |
|
||||
|
||||
每个用户一条策略记录,与 `UserEntity` 一对一。
|
||||
|
||||
### 5.2 客户端落盘规则(强制)
|
||||
|
||||
| 服务端策略 | 客户端行为 |
|
||||
|---|---|
|
||||
| `allowPersist = true` | 正常落盘:Todo 数据 + 登录凭据(Token)写入 `localStorage` / SQLite |
|
||||
| `allowPersist = false` | **内存模式**:Todo 数据与凭据仅保留在内存中,退出应用后全部清除 |
|
||||
|
||||
**策略切换时的处理**:
|
||||
|
||||
- `true → false`:立即清空已落盘数据,提示用户"已切换为内存模式,退出后数据不保留"
|
||||
- `false → true`:恢复持久化,将当前内存数据写入存储
|
||||
|
||||
### 5.3 落盘数据范围
|
||||
|
||||
"禁止落盘"时,以下数据均不得写入任何持久化介质:
|
||||
|
||||
- Todo 数据(列表/详情)
|
||||
- 登录凭据(accessToken / 会话标识)
|
||||
- 同步状态(上次同步时间、待同步队列)
|
||||
- 安全策略缓存
|
||||
|
||||
---
|
||||
|
||||
## 六、同步工作流
|
||||
|
||||
### 6.1 完整流程
|
||||
|
||||
```
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ 配置地址 │ ──→ │ 登录 │ ──→ │ 拉取全量 │
|
||||
│ (探测可达) │ │ (获取Token) │ │ (GET /tasks) │
|
||||
└──────────────┘ └──────────────┘ └──────┬───────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ 返回全量 │ ←── │ 增删改同步 │ ←── │ 本地编辑 │
|
||||
│ (SyncResponse)│ │ (POST /sync) │ │ (upsert+del)│
|
||||
└──────────────┘ └──────────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
### 6.2 同步策略
|
||||
|
||||
#### 6.2.1 核心原则
|
||||
|
||||
- **任务唯一标识是 `id`**:`id` 是任务实体的唯一稳定标识,不可依赖时间戳做身份判断
|
||||
- **`updatedAtUtc` 是冲突判断字段**:用于解决"同一任务被多端修改时哪个变更更新"的问题
|
||||
- **服务端存储唯一真实源(Source of Truth)**:所有设备最终都收敛到服务端数据
|
||||
- **字段级 Last-Write-Wins(LWW)合并**:同一字段的多设备并发修改,以 `updatedAtUtc` 较新者为准
|
||||
- **逻辑删除(Tombstone)**:删除操作标记为"已删除",而非物理删除,保证多端删除语义一致
|
||||
- **增量同步**:每次同步只上传本端变更(pendingUpserts + pendingDeletes),不上传全量
|
||||
|
||||
#### 6.2.2 变更追踪机制
|
||||
|
||||
每个 `CloudTaskItem` 包含以下时间戳字段:
|
||||
|
||||
| 字段 | 说明 |
|
||||
|---|---|
|
||||
| `createdAtUtc` | 创建时间(服务端分配,永不改变) |
|
||||
| `updatedAtUtc` | 最后一次修改时间(每次字段变更时更新) |
|
||||
| `deletedAtUtc` | 逻辑删除时间(为 `null` 表示未删除) |
|
||||
|
||||
> `updatedAtUtc` 由**请求发起方**(客户端或服务端)在发起变更时写入,存储到服务端后不再覆盖(除非有更新的变更)。
|
||||
|
||||
#### 6.2.3 冲突解决规则
|
||||
|
||||
当客户端提交的任务与服务端现有任务发生 `id` 冲突时,按以下规则处理:
|
||||
|
||||
| 场景 | 解决规则 |
|
||||
|---|---|
|
||||
| 同一 `id`,客户端 `updatedAtUtc` **更新** | 服务端接受客户端版本,覆盖对应字段 |
|
||||
| 同一 `id`,服务端 `updatedAtUtc` **更新** | 服务端拒绝客户端版本,保留服务端版本 |
|
||||
| 同一 `id`,两者 `updatedAtUtc` **相同** | 服务端优先(保守策略) |
|
||||
|
||||
**字段级合并示例**:
|
||||
- 设备 A 修改标题,设备 B 修改截止日期
|
||||
- 服务端对两个字段分别取 `updatedAtUtc` 较新者,合并出最终结果
|
||||
- 两个设备的修改都被保留,不会互相覆盖
|
||||
|
||||
#### 6.2.4 父子任务处理
|
||||
|
||||
- **删除**:删除父任务时,其子任务一并标记为已删除(递归)
|
||||
- **创建**:子任务的 `parentTaskId` 在上传时若为 `null`(新创建任务),服务端分配 ID 后,第一遍返回临时 ID 映射;第二遍利用映射完成父子关系重映射
|
||||
- **重建父子关系**:若原父任务被删除后重建,子任务的 `parentTaskId` 引用仍指向原父 ID,不会自动指向新父
|
||||
|
||||
#### 6.2.5 Tombstone 保留策略
|
||||
|
||||
- `deletedAtUtc` 非 `null` 的任务在服务端保留至少 **30 天**
|
||||
- 超过 30 天后由服务端垃圾收集(物理删除)
|
||||
- 客户端同步时拉取全量(含 Tombstone),本地过滤不展示 `deletedAtUtc != null` 的任务
|
||||
|
||||
#### 6.2.6 服务端处理流程
|
||||
|
||||
`POST /sync` 服务端执行顺序:
|
||||
|
||||
```
|
||||
1. 解析 upserts 和 deletes
|
||||
2. 对 deletes 中的每个 id:
|
||||
- 递归标记该任务及所有子任务 deletedAtUtc = now
|
||||
3. 对 upserts 按 updatedAtUtc 降序排列(较新的先处理)
|
||||
4. 对每个 upsert 任务:
|
||||
a. 若 id 在服务端不存在 → 创建新记录(分配正向 id)
|
||||
b. 若 id 存在但服务端 updatedAtUtc 更新 → 跳过(保留服务端)
|
||||
c. 若 id 存在且客户端 updatedAtUtc >= 服务端 updatedAtUtc → 字段级合并更新
|
||||
5. 返回当前用户全量任务(含 Tombstone)
|
||||
```
|
||||
|
||||
> 排序处理的目的:确保最新的变更优先被采纳。
|
||||
|
||||
#### 6.2.7 关于"服务端为准"的正确理解
|
||||
|
||||
"服务端为准"并不意味着客户端会丢失数据。完整的同步流程是:
|
||||
|
||||
1. **客户端上传阶段**:将 `pendingUpserts` + `pendingDeletes` 提交到服务器
|
||||
2. **服务器合并阶段**:按 LWW 规则合并客户端提交与服务器现有数据
|
||||
3. **客户端覆盖阶段**:用服务器返回的全量数据覆盖本地
|
||||
|
||||
由于客户端在第 1 步已经把本地所有变更提交,服务器在第 2 步已经把客户端的变更合并进权威状态,第 3 步用权威状态覆盖本地是安全的——**不会丢失任何已提交的变更**。
|
||||
|
||||
真正会丢失的场景是:客户端在离线状态下修改了任务 A,但没有在联网后发起同步就直接断开连接。这种情况下,离线修改会保留在本地 `pendingUpserts` 中,下次联网同步时会正常提交。
|
||||
|
||||
### 6.3 SyncRequest / SyncResponse 结构
|
||||
|
||||
**请求**(`POST /sync`):
|
||||
```json
|
||||
{
|
||||
"upserts": [
|
||||
{ "id": 1, "title": "A", "priority": 1, "isCompleted": false, "parentTaskId": null, "updatedAtUtc": "2026-04-06T17:00:00Z" },
|
||||
{ "id": null, "title": "New", "priority": 1, "isCompleted": false, "parentTaskId": null, "updatedAtUtc": "2026-04-06T17:30:00Z" }
|
||||
],
|
||||
"deletes": [2, 3]
|
||||
}
|
||||
```
|
||||
|
||||
- `id` 为服务端已知 ID(`id > 0`)时执行更新;`id` 为 `null` / 0 / 负数时创建新记录
|
||||
- `updatedAtUtc` 必填,客户端在发起变更时写入本地时间(UTC)
|
||||
- `deletes` 为待逻辑删除的任务 ID 数组
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"serverTimeUtc": "2026-04-06T17:39:30.0281279Z",
|
||||
"tasks": [
|
||||
{ "id": 1, "title": "A", "priority": 1, "isCompleted": false, "parentTaskId": null, "createdAtUtc": "2026-04-01T00:00:00Z", "updatedAtUtc": "2026-04-06T17:00:00Z", "deletedAtUtc": null }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- `tasks` 返回当前用户**全量数据**(含 `deletedAtUtc != null` 的 Tombstone)
|
||||
- 客户端用响应全量覆盖本地数据,本地过滤不展示已删除任务
|
||||
|
||||
### 6.4 同步按钮调用规则
|
||||
|
||||
**触发入口**:主界面"同步"按钮(`TaskList.vue`),绑定 `syncNow()` 方法。
|
||||
|
||||
#### 6.4.1 前置检查
|
||||
|
||||
1. 检查 `isSyncing` 状态,防止并发重复调用
|
||||
2. 检查登录状态(未登录则提示先登录)
|
||||
|
||||
#### 6.4.2 变更检测(增量同步核心)
|
||||
|
||||
本地维护 `pendingUpserts` 和 `pendingDeletes` 两个集合:
|
||||
|
||||
| 操作 | 记录时机 | 记录内容 |
|
||||
|---|---|---|
|
||||
| 新增任务 | 创建时 | `{ id: 临时负数, title, priority, ..., updatedAtUtc: 当前时间 }` |
|
||||
| 修改任务 | 任何字段变更时 | `{ id, 全部字段, updatedAtUtc: 当前时间 }` |
|
||||
| 删除任务 | 点击删除时 | `{ id }`(从 pendingUpserts 移除,加入 pendingDeletes)|
|
||||
|
||||
> 每次用户操作后,更新本地 `updatedAtUtc`。使用 `seenIds` 集合去重,同一 `id` 只保留最新一条记录。
|
||||
|
||||
#### 6.4.3 数据准备
|
||||
|
||||
1. **合并 pendingUpserts**:
|
||||
- 调用 `flattenTasksToCloudUpserts(localTasks, pendingUpserts)` 生成 upsert 列表
|
||||
- **ID 映射规则**:
|
||||
- `id > 0`:保留原 ID(服务端已知,执行更新)
|
||||
- `id <= 0` 或未设置:置为 `null`(服务端分配新 ID,执行创建)
|
||||
- 按 `updatedAtUtc` 升序排列(较早的先发送,便于服务端 LWW 判断)
|
||||
|
||||
2. **生成 deletes 列表**:
|
||||
- 从 `pendingDeletes` 提取待删除 ID
|
||||
- 去重:`deletes = [...new Set(pendingDeletes.map(d => d.id))]`
|
||||
|
||||
#### 6.4.4 请求发送
|
||||
|
||||
- 端点:`POST /sync/`
|
||||
- 请求体:`{ upserts: CloudTaskUpsert[], deletes: number[] }`
|
||||
- 携带 `Authorization: Bearer {accessToken}`
|
||||
|
||||
#### 6.4.5 响应处理
|
||||
|
||||
1. **解析全量响应**:
|
||||
- 调用 `buildTaskTreeFromCloudItems(response.tasks)` 将扁平响应转为前端树结构
|
||||
- 过滤 `deletedAtUtc == null` 的任务用于展示
|
||||
|
||||
2. **更新本地状态**:
|
||||
- `tasks.value = cloudTasks`(覆盖式更新,确保与服务端一致)
|
||||
- `LocalStorageService.saveTasks(cloudTasks)`
|
||||
- 清空 `pendingUpserts` 和 `pendingDeletes`
|
||||
|
||||
3. **更新同步状态**:
|
||||
- `lastSyncTime`:使用响应中的 `serverTimeUtc`
|
||||
- `syncError = null`
|
||||
|
||||
#### 6.4.6 错误处理
|
||||
|
||||
| 错误类型 | 处理策略 |
|
||||
|---|---|
|
||||
| 401 未授权 | 清除本地会话,弹出重新登录提示 |
|
||||
| 403 禁止 | Toast 提示权限不足 |
|
||||
| 网络错误 | 重试(指数退避,最多 3 次),仍失败则保留 pending 数据下次同步 |
|
||||
|
||||
#### 6.4.7 完整流程图
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ syncNow() 触发 │
|
||||
└─────────────────────┬───────────────────────────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 1. 前置检查:isSyncing? 已登录? │
|
||||
└─────────────────────┬───────────────────────────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 2. 变更检测:pendingUpserts + pendingDeletes │
|
||||
│ - 遍历 tasks,收集 dirty 项(updatedAtUtc > lastSyncTime)│
|
||||
│ - 新增/删除操作直接追加到 pending │
|
||||
└─────────────────────┬───────────────────────────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 3. 数据准备 │
|
||||
│ - flattenTasksToCloudUpserts() │
|
||||
│ - seenIds 去重 │
|
||||
│ - 按 updatedAtUtc 排序 │
|
||||
└─────────────────────┬───────────────────────────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 4. POST /sync { upserts, deletes } │
|
||||
└─────────────────────┬───────────────────────────────────────┘
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 5. 响应处理 │
|
||||
│ - buildTaskTreeFromCloudItems() │
|
||||
│ - tasks.value = cloudTasks │
|
||||
│ - saveTasks() │
|
||||
│ - 清空 pendingUpserts / pendingDeletes │
|
||||
│ - lastSyncTime = serverTimeUtc │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、API 契约总览
|
||||
|
||||
### 7.1 通用约定
|
||||
|
||||
- Base URL:客户端同源请求,无需外部配置
|
||||
- 认证:`Authorization: Bearer {accessToken}`
|
||||
- 统一错误响应:
|
||||
```json
|
||||
{ "code": "ERROR_CODE", "message": "Human readable message." }
|
||||
```
|
||||
|
||||
### 7.2 错误码
|
||||
|
||||
| 错误码 | HTTP 状态 | 含义 |
|
||||
|---|---|---|
|
||||
| `UNAUTHORIZED` | 401 | 未登录或会话失效 |
|
||||
| `FORBIDDEN` | 403 | 权限不足(RBAC 拒绝 / 策略拒绝) |
|
||||
| `BAD_REQUEST` | 400 | 请求参数不合法 |
|
||||
| `NOT_FOUND` | 404 | 资源不存在 |
|
||||
|
||||
### 7.3 完整端点清单
|
||||
|
||||
| 路由 | 方法 | 权限 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `/auth/bootstrap` | POST | 无(仅首次) | 初始化管理员账号 |
|
||||
| `/auth/login` | POST | 匿名 | 登录,返回 AccessToken |
|
||||
| `/auth/logout` | POST | 登录即可 | 登出,删除会话 |
|
||||
| `/auth/change-password` | POST | 登录即可 | 修改当前用户密码 |
|
||||
| `/tasks` | GET | `tasks:read` | 获取当前用户全量任务 |
|
||||
| `/sync` | POST | `sync:write` | 上传 upsert + deletes,返回最新全量 |
|
||||
| `/security/policy` | GET | `policy:read` | 获取当前用户安全策略 |
|
||||
| `/security/policy` | PUT | `policy:write` | 修改当前用户安全策略 |
|
||||
| `/cloud-sync/probe` | POST | 匿名 | 服务端代理探测目标 URL 可达性 |
|
||||
| `/admin/users` | GET/POST/DELETE | `users:manage` | 用户管理 |
|
||||
| `/admin/sessions` | GET/DELETE | `users:manage` | 会话管理 |
|
||||
| `/admin/audit-logs` | GET | `users:manage` | 审计日志查询 |
|
||||
|
||||
---
|
||||
|
||||
## 八、前端实现规则
|
||||
|
||||
### 8.1 cloudClient.ts(Axios 实例)
|
||||
|
||||
- 与通用 `client.ts`(baseURL 含 `/api`)解耦,云同步专用。
|
||||
- **请求拦截器**:MAUI 模式下从 `CloudSyncStorage` 读取 `serverUrl`,Host 模式不设 baseURL 走同源;自动附加 `Bearer {accessToken}`。
|
||||
- **响应拦截器**:
|
||||
- 401 → 清除本地会话,弹出重新登录提示。
|
||||
- 403 `FORBIDDEN` → Toast 提示权限不足。
|
||||
- 其他错误 → 统一 Toast 通知。
|
||||
|
||||
### 8.2 cloudSync.ts(API 封装)
|
||||
|
||||
提供 `cloudSyncApi` 对象,6 个方法:
|
||||
|
||||
| 方法 | 端点 | 核心逻辑 |
|
||||
|---|---|---|
|
||||
| `login()` | `/auth/login` | 登录后保存 Session 到 `CloudSyncStorage` |
|
||||
| `logout()` | `/auth/logout` | 清除本地会话与缓存 |
|
||||
| `getTasks()` | `/tasks` | 拉取全量 `CloudTaskItem[]`,通过 `buildTaskTreeFromCloudItems` 将扁平数据转为前端 Task 树 |
|
||||
| `syncTasks()` | `/sync` | 通过 `flattenTasksToCloudUpserts` 将 Task 树扁平化为 upsert 列表,携带 deletes 提交 |
|
||||
| `getSecurityPolicy()` | `/security/policy` | 获取策略,驱动落盘/内存模式切换 |
|
||||
| `probeServerUrl()` | `/cloud-sync/probe` | 由服务端代理探测目标 URL |
|
||||
|
||||
### 8.3 CloudSyncSettingsDialog.vue(UI 组件)
|
||||
|
||||
功能区块:
|
||||
|
||||
- **同步开关**:checkbox 控制,校验地址已保存方可开启。
|
||||
- **服务端地址**:输入 + 规范化 + 保存并探测(`probeServerUrl`)。
|
||||
- **登录区**:用户名/密码 → `login()` → 刷新策略与状态。
|
||||
- **已登录区**:会话摘要、连接状态、安全策略展示、登出 + 立即同步。
|
||||
- 通过 CustomEvent(`cloudSyncStateChanged` / `cloudSyncTasksRequested`)与其他组件通信。
|
||||
|
||||
---
|
||||
|
||||
## 九、服务端代理探测规则
|
||||
|
||||
由研发工单 08 引入,`POST /cloud-sync/probe` 由 Host 代理探测目标 URL:
|
||||
|
||||
- 输入:`{ "targetUrl": "https://example.com" }`
|
||||
- 输出:`{ "isReachable", "httpStatus", "isHttps", "title", "description", "type" }`(type: `success` / `warn` / `error`)
|
||||
- 安全约束:
|
||||
- 限制目标 URL 仅允许 HTTP/HTTPS 公网地址,禁止探测 localhost / 内网 IP(防 SSRF)。
|
||||
- 建议加频率限制(如每分钟 3 次)。
|
||||
|
||||
---
|
||||
|
||||
## 十、审计日志
|
||||
|
||||
所有关键安全事件写入 `AuditLogs` 表:
|
||||
|
||||
- 登录成功 / 失败(含 IP、终端信息)
|
||||
- 安全策略变更
|
||||
- 用户管理操作(创建 / 删除 / 重置密码)
|
||||
|
||||
通过 `/admin/audit-logs` 端点查询,支持按时间、用户、事件类型过滤。
|
||||
|
||||
---
|
||||
|
||||
## 十一、约束与风险
|
||||
|
||||
| 约束 / 风险 | 缓解措施 |
|
||||
|---|---|
|
||||
| Token 吊销(会话非纯无状态) | `UserSessions` 表管理;登出即删记录,Token 立即失效 |
|
||||
| HTTPS 强制 | 所有安全通信必须基于 TLS,防中间人攻击窃取 Token |
|
||||
| 暴力破解 | `POST /auth/login` 建议加 Rate Limiting |
|
||||
| MAUI 端不暴露云同步端点 | DI 注册边界严格执行:`AddCloudSyncServer()` 仅 Host 调用 |
|
||||
| `allowPersist = false` 时数据丢失风险 | UI 强提示;退出时若未同步则二次确认 |
|
||||
| 密码存储 | 使用 Argon2id 哈希(含 Salt),不存明文 |
|
||||
| SSRF(探测端点) | `CloudProbeService` 限制目标为公网地址,禁止内网探测 |
|
||||
|
||||
---
|
||||
|
||||
> **相关文档**:
|
||||
> - PRD v1.2.0:[产品需求文档-1.2.0.md](../project/产品需求文档-1.2.0.md)
|
||||
> - 研发工单总览:[00-工单总览.md](../project/研发工单-v1.2.0/00-工单总览.md)
|
||||
> - 服务端基础能力:[04-CloudSync-服务端基础能力.md](../project/研发工单-v1.2.0/04-CloudSync-服务端基础能力.md)
|
||||
> - 客户端工作流:[05-CloudSync-客户端配置与工作流.md](../project/研发工单-v1.2.0/05-CloudSync-客户端配置与工作流.md)
|
||||
> - 安全与可控落盘:[06-CloudSync-安全与可控落盘.md](../project/研发工单-v1.2.0/06-CloudSync-安全与可控落盘.md)
|
||||
> - 安全设计方案:[06.1-CloudSync-服务端安全设计方案.md](../project/研发工单-v1.2.0/06.1-CloudSync-服务端安全设计方案.md)
|
||||
> - 同源 Host 重构:[08-cloud_sync_refactor_plan.md](../project/研发工单-v1.2.0/08-cloud_sync_refactor_plan.md)
|
||||
> - 技术设计文档:[技术设计文档.md](./01-技术设计文档.md)
|
||||
@@ -9,6 +9,10 @@
|
||||
- v1.1.0:MAUI + WebView 跨平台版本
|
||||
- v1.2.0 (规划中):Linux 支持与增强功能
|
||||
|
||||
### v1.2.8 (2026-06-14)
|
||||
|
||||
- **文档**:新增 [任务同步规则.md](./02-任务同步规则.md),汇总 Todo 待办项云同步的架构、API 契约、认证鉴权、同步工作流、安全策略与可控落盘等完整规则。
|
||||
|
||||
### v1.2.8 (2026-04-13)
|
||||
|
||||
- **云同步增强**:在 `Hua.Todo.Application` 中深度集成 `CloudSync` 模块,支持权限验证、安全策略(SecurityPolicy)与任务同步 DTO。
|
||||
@@ -35,8 +39,8 @@
|
||||
- **Windows WebView2 数据目录调整**:MAUI(Unpackaged)默认会在安装目录生成 `Hua.Todo.Maui.exe.WebView2`;现改为写入 `%LocalAppData%\Hua.Todo\WebView2`,避免污染安装目录。
|
||||
- **Windows WebView2 Runtime 误判修复**:当系统已安装 WebView2 Runtime 但发布产物缺少/裁剪 WebView2 托管程序集时,旧检测逻辑会误判为“未安装”;现改为优先从常见安装目录探测 Evergreen 版本,避免阻断主界面加载。
|
||||
- **Windows 三件套开发体验**:新增 `start-host.ps1` / `start-dev.ps1`,并在 MAUI 中约定 `IsUsingStatic=false` 时不启动内置 WebServer,避免注入覆盖 Vite 的 `/api -> 5173` 代理配置。
|
||||
- **文档与部署指南**:新增 `docs/manual/部署文档.md`,详细说明开发环境搭建、多平台发布流程(Windows/Linux/Docker)以及关键配置项;并在技术设计文档中建立链接。
|
||||
- **用户文档完善**:在规划中新增了 `docs/manual/新手指南.md` 和 `docs/manual/用户指南.md`。
|
||||
- **文档与部署指南**:新增 `docs/manual/05-部署文档.md`,详细说明开发环境搭建、多平台发布流程(Windows/Linux/Docker)以及关键配置项;并在技术设计文档中建立链接。
|
||||
- **用户文档完善**:在规划中新增了 `docs/manual/08-新手指南.md` 和 `docs/manual/09-用户指南.md`。
|
||||
27→
|
||||
28→### v1.1.1 (2026-04-06)
|
||||
|
||||
@@ -43,6 +43,8 @@
|
||||
| 05 - 云同步 客户端配置与同步 | 已完成 | 待验证 | 新增"云同步设置"弹窗:地址校验+保存探测、登录/登出、登录后拉取云端 Todo 数据并在主界面只读展示 |
|
||||
| 06 - 安全与落盘策略 | 未标注 | 待验证 | |
|
||||
| 07 - 文档与验收 | 未标注 | 待验证 | |
|
||||
| 08 - 同源 Host 重构 | 已设计 | 待实现 | Vite proxy 补 `/auth` `/tasks` `/sync` `/security` `/cloud-sync` |
|
||||
| 09 - 同步策略改进 | 待实现 | 待验证 | 重构 TaskEntity 继承 ABP 基类(`FullAuditedEntityWithUser<Guid, IdentityUser>`),主键从 `int` 改为 `Guid`,新增 ABP 审计字段(ExtraProperties/ConcurrencyStamp/CreationTime/CreatorId/LastModificationTime/LastModifierId/IsDeleted/DeletionTime/DeleterId) |
|
||||
|
||||
## 交付判定(v1.2.0 Done Definition)
|
||||
|
||||
|
||||
@@ -0,0 +1,734 @@
|
||||
# CloudSync 同步策略改进方案
|
||||
|
||||
> 研发工单序号:09
|
||||
> 依赖:04-CloudSync-服务端基础能力、05-CloudSync-客户端配置与工作流
|
||||
> 状态:待实现
|
||||
|
||||
---
|
||||
|
||||
## 一、背景与问题
|
||||
|
||||
当前同步规则([02-任务同步规则.md](../../manual/02-任务同步规则.md) 6.2 节)存在以下问题:
|
||||
|
||||
1. **TaskEntity 缺少 ABP 审计字段**:未继承 ABP 基类,缺少 `ExtraProperties`、`ConcurrencyStamp`、`CreationTime`、`CreatorId`、`LastModificationTime`、`LastModifierId` 等标准字段
|
||||
2. **主键类型不符合 ABP 规范**:ABP 要求主键为 `Guid`,当前为 `int`
|
||||
3. **软删除字段缺失**:没有 `IsDeleted` 和 `DeletionTime` 字段,无法实现 Tombstone 逻辑删除
|
||||
|
||||
---
|
||||
|
||||
## 二、ABP 审计字段规范(必须遵守)
|
||||
|
||||
根据 [表设计与字段命名规范.md](file:///d:/Codes/CodeSmith/CodeSmith/ShaoHua.CodeSmith.Abp/docs/2.表设计与字段命名规范.md),`FullAuditedEntityWithUser<Guid, IdentityUser>` 基类提供以下字段:
|
||||
|
||||
| ABP 标准字段 | 含义 | 类型 |
|
||||
|---|---|---|
|
||||
| `Id` | 主键 | `Guid`(ABP 强制要求) |
|
||||
| `ExtraProperties` | 扩展属性字典 | `ExtraPropertyDictionary` |
|
||||
| `ConcurrencyStamp` | 并发戳,用于乐观并发控制 | `string?` |
|
||||
| `CreationTime` | 创建时间 | `DateTime` |
|
||||
| `CreatorId` | 创建人Id | `Guid?` |
|
||||
| `LastModificationTime` | 最后修改时间 | `DateTime?` |
|
||||
| `LastModifierId` | 最后修改人Id | `Guid?` |
|
||||
| `IsDeleted` | 软删除标记 | `bool` |
|
||||
| `DeletionTime` | 删除时间 | `DateTime?` |
|
||||
| `DeleterId` | 删除人Id | `Guid?` |
|
||||
|
||||
### 字段忽略规则(ShouldIgnoreList)
|
||||
|
||||
以下字段在实体中**不生成任何属性**:
|
||||
|
||||
| 字段名 | 理由 |
|
||||
|---|---|
|
||||
| `ExtraProperties` | ABP 框架扩展属性,基类已处理 |
|
||||
|
||||
### 不可编辑字段(NotEnableEditList)
|
||||
|
||||
以下字段在更新 DTO 中**不可编辑**:`IsDeleted`、`CreationTime`、`CreatorId`、`LastModificationTime`、`LastModifierId`、`DeletionTime`、`DeleterId`
|
||||
|
||||
---
|
||||
|
||||
## 三、TaskEntity 重构方案
|
||||
|
||||
### 3.1 数据库表名(ABP 规范)
|
||||
|
||||
> ABP 框架约定表名格式为 `T_{实体名}s`,例如实体 `TaskEntity` 对应表名 `T_Tasks`。
|
||||
> EF Core 迁移脚本中的 `table: "Tasks"` 为 DbContext 配置的表名,两者须保持一致。
|
||||
|
||||
### 3.2 继承关系变更
|
||||
|
||||
```csharp
|
||||
// 原
|
||||
public class TaskEntity
|
||||
{
|
||||
public int Id { get; set; }
|
||||
// ...
|
||||
}
|
||||
|
||||
// 改后
|
||||
public class TaskEntity : FullAuditedEntityWithUser<Guid, IdentityUser>
|
||||
{
|
||||
// Id、ExtraProperties、ConcurrencyStamp、CreationTime、CreatorId、
|
||||
// LastModificationTime、LastModifierId、IsDeleted、DeletionTime、DeleterId
|
||||
// 均由基类提供
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 业务字段保留
|
||||
|
||||
| 业务字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `UserId` | `Guid` | 任务所属用户(云端隔离),本地为 `"local"` |
|
||||
| `Title` | `string` | 任务标题 |
|
||||
| `Priority` | `TaskPriority` | 优先级枚举 |
|
||||
| `IsCompleted` | `bool` | 是否完成 |
|
||||
| `ParentTaskId` | `Guid?` | 父任务ID(外键) |
|
||||
|
||||
> **ParentTaskId 类型变更**:从 `int?` 改为 `Guid?`,与主键类型一致。
|
||||
|
||||
### 3.3 导航属性
|
||||
|
||||
```csharp
|
||||
public class TaskEntity : FullAuditedEntityWithUser<Guid, IdentityUser>
|
||||
{
|
||||
public UserEntity? User { get; set; }
|
||||
public TaskEntity? ParentTask { get; set; }
|
||||
public List<TaskEntity> SubTasks { get; set; } = new();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、DTO 重构方案(与数据库保持一致)
|
||||
|
||||
### 4.1 CloudTaskItem(C# → JSON 响应)
|
||||
|
||||
```csharp
|
||||
// src/Hua.Todo.Application/CloudSync/Models/TaskSyncDtos.cs
|
||||
|
||||
/// <summary>
|
||||
/// 云同步任务条目(ABP 标准)。
|
||||
/// </summary>
|
||||
public class CloudTaskItem
|
||||
{
|
||||
/// <summary>
|
||||
/// 任务 ID(服务端分配,Guid)。
|
||||
/// </summary>
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 标题。
|
||||
/// </summary>
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 优先级。
|
||||
/// </summary>
|
||||
public TaskPriority Priority { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否完成。
|
||||
/// </summary>
|
||||
public bool IsCompleted { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 父任务 ID(Guid)。
|
||||
/// </summary>
|
||||
public Guid? ParentTaskId { get; set; }
|
||||
|
||||
// === ABP 审计字段 ===
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间。
|
||||
/// </summary>
|
||||
public DateTime CreationTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建人 ID。
|
||||
/// </summary>
|
||||
public Guid? CreatorId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最后修改时间(用于 LWW 冲突判断)。
|
||||
/// </summary>
|
||||
public DateTime? LastModificationTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最后修改人 ID。
|
||||
/// </summary>
|
||||
public Guid? LastModifierId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 软删除标记。
|
||||
/// </summary>
|
||||
public bool IsDeleted { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 删除时间。
|
||||
/// </summary>
|
||||
public DateTime? DeletionTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 删除人 ID。
|
||||
/// </summary>
|
||||
public Guid? DeleterId { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 CloudTaskUpsert(C# → JSON 请求)
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// 任务 Upsert DTO(客户端提交)。
|
||||
/// </summary>
|
||||
public class CloudTaskUpsert
|
||||
{
|
||||
/// <summary>
|
||||
/// 任务 ID;为 null 表示新建。
|
||||
/// </summary>
|
||||
public Guid? Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 标题。
|
||||
/// </summary>
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 优先级。
|
||||
/// </summary>
|
||||
public TaskPriority Priority { get; set; } = TaskPriority.Medium;
|
||||
|
||||
/// <summary>
|
||||
/// 是否完成。
|
||||
/// </summary>
|
||||
public bool IsCompleted { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 父任务 ID(可选)。
|
||||
/// </summary>
|
||||
public Guid? ParentTaskId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 客户端发起变更时写入的时间戳(UTC),用于 LWW 冲突判断。
|
||||
/// </summary>
|
||||
public DateTime? LastModificationTime { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 SyncRequest / SyncResponse
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// 同步请求(增改删)。
|
||||
/// </summary>
|
||||
public class SyncRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 新增或更新的任务列表。
|
||||
/// </summary>
|
||||
public List<CloudTaskUpsert> Upserts { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 需要逻辑删除的任务 ID 列表。
|
||||
/// </summary>
|
||||
public List<Guid> Deletes { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 同步响应。
|
||||
/// </summary>
|
||||
public class SyncResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 服务端时间(UTC)。
|
||||
/// </summary>
|
||||
public DateTime ServerTimeUtc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前用户的任务全量(含 Tombstone)。
|
||||
/// </summary>
|
||||
public List<CloudTaskItem> Tasks { get; set; } = new();
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 JSON 序列化配置
|
||||
|
||||
DTO 默认使用 PascalCase(ABP 标准),通过 `[JsonPropertyName]` 特性控制 JSON 输出:
|
||||
|
||||
```csharp
|
||||
public class CloudTaskItem
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
[JsonPropertyName("title")]
|
||||
public string Title { get; set; }
|
||||
|
||||
// ... 其他字段
|
||||
}
|
||||
```
|
||||
|
||||
或通过全局配置启用 camelCase:
|
||||
|
||||
```csharp
|
||||
builder.Services.AddControllers()
|
||||
.AddJsonOptions(options =>
|
||||
{
|
||||
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、前端类型定义(与 DTO 保持一致)
|
||||
|
||||
### 5.1 当前状态 vs 目标状态对比
|
||||
|
||||
| 字段 | 当前类型 | 目标类型 | 变更说明 |
|
||||
|---|---|---|---|
|
||||
| `id` | `number` | `string` | 主键从 int 改为 Guid,JSON 序列化为字符串 |
|
||||
| `parentTaskId` | `number \| undefined` | `string \| null` | 外键类型同步变更 |
|
||||
| `createdAt` | `string` | **移除** | 替换为 `creationTime` |
|
||||
| `updatedAt` | `string` | **移除** | 替换为 `lastModificationTime` |
|
||||
| `subTasks` | `Task[]` | `Task[]` | 类型不变,递归结构 |
|
||||
| — | — | `creationTime: string` | 新增:ABP 创建时间 |
|
||||
| — | — | `creatorId: string \| null` | 新增:创建人 ID |
|
||||
| — | — | `lastModificationTime: string \| null` | 新增:最后修改时间(LWW 冲突判断) |
|
||||
| — | — | `lastModifierId: string \| null` | 新增:最后修改人 ID |
|
||||
| — | — | `isDeleted: boolean` | 新增:软删除标记 |
|
||||
| — | — | `deletionTime: string \| null` | 新增:删除时间 |
|
||||
| — | — | `deleterId: string \| null` | 新增:删除人 ID |
|
||||
|
||||
### 5.2 task.ts 目标定义
|
||||
|
||||
```typescript
|
||||
// src/Hua.Todo.Web/src/types/task.ts
|
||||
|
||||
export type TaskPriority = 0 | 1 | 2;
|
||||
|
||||
/**
|
||||
* Todo 待办项(与 CloudTaskItem DTO 一一对应)。
|
||||
* - 主键 id 为 Guid 序列化后的字符串
|
||||
* - 包含 ABP 审计字段,用于云同步冲突判断
|
||||
*/
|
||||
export interface Task {
|
||||
// === 业务字段 ===
|
||||
id: string; // Guid 序列化
|
||||
title: string;
|
||||
priority: TaskPriority;
|
||||
isCompleted: boolean;
|
||||
parentTaskId: string | null; // Guid 类型,null 表示顶级任务
|
||||
|
||||
// === ABP 审计字段 ===
|
||||
/** 创建时间(服务端分配) */
|
||||
creationTime: string;
|
||||
/** 创建人 ID */
|
||||
creatorId: string | null;
|
||||
/** 最后修改时间(用于 LWW 冲突判断) */
|
||||
lastModificationTime: string | null;
|
||||
/** 最后修改人 ID */
|
||||
lastModifierId: string | null;
|
||||
/** 软删除标记(前端本地过滤不展示) */
|
||||
isDeleted: boolean;
|
||||
/** 删除时间(不为 null 时表示已逻辑删除) */
|
||||
deletionTime: string | null;
|
||||
/** 删除人 ID */
|
||||
deleterId: string | null;
|
||||
|
||||
// === 导航属性 ===
|
||||
subTasks: Task[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Todo 待办项请求(客户端 → 服务端,新建时不带 id)
|
||||
*/
|
||||
export interface CreateTaskDto {
|
||||
title: string;
|
||||
priority: TaskPriority;
|
||||
parentTaskId?: string; // Guid 字符串
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 Todo 待办项请求(客户端 → 服务端,必须带 id)
|
||||
*/
|
||||
export interface UpdateTaskDto {
|
||||
id: string; // Guid 字符串
|
||||
title?: string;
|
||||
priority?: TaskPriority;
|
||||
isCompleted?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 前端变更检测逻辑
|
||||
|
||||
```typescript
|
||||
// src/Hua.Todo.Web/src/types/task.ts 或业务层
|
||||
|
||||
/**
|
||||
* 标记任务为脏(本地修改待同步)。
|
||||
* - 每次用户操作(创建/修改/删除)时调用
|
||||
* - 更新 lastModificationTime 为当前 UTC 时间
|
||||
*/
|
||||
export function markTaskDirty(task: Task): void {
|
||||
task.lastModificationTime = new Date().toISOString();
|
||||
task.lastModifierId = getCurrentUserId(); // 若已登录
|
||||
pendingUpserts.set(task.id, task);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记任务为待删除(本地删除待同步)。
|
||||
* - 设置 deletionTime 而非真正从数组移除
|
||||
*/
|
||||
export function markTaskDeleted(task: Task): void {
|
||||
task.isDeleted = true;
|
||||
task.deletionTime = new Date().toISOString();
|
||||
task.deleterId = getCurrentUserId();
|
||||
pendingDeletes.add(task.id);
|
||||
}
|
||||
```
|
||||
|
||||
### 5.4 API 响应处理
|
||||
|
||||
```typescript
|
||||
// src/Hua.Todo.Web/src/api/cloudSync.ts
|
||||
|
||||
import type { Task } from '@/types/task';
|
||||
|
||||
/**
|
||||
* 获取云端全量任务(含 Tombstone)。
|
||||
* 返回数据直接赋值给本地 store,无需字段转换。
|
||||
*/
|
||||
export async function fetchCloudTasks(): Promise<Task[]> {
|
||||
const response = await cloudSyncApi.getTasks();
|
||||
// response.data 类型已是 Task[],服务端返回的 Guid 已序列化为 string
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取全量后,本地过滤不展示已删除任务。
|
||||
*/
|
||||
export function filterVisibleTasks(tasks: Task[]): Task[] {
|
||||
return tasks.filter(task => task.deletionTime === null);
|
||||
}
|
||||
```
|
||||
|
||||
### 5.5 pendingUpserts / pendingDeletes 类型修正
|
||||
|
||||
```typescript
|
||||
// src/Hua.Todo.Web/src/stores/taskStore.ts(示例)
|
||||
|
||||
import type { Task } from '@/types/task';
|
||||
|
||||
// 待上传的脏任务(key: task.id,即 Guid string)
|
||||
const pendingUpserts = reactive(new Map<string, Task>());
|
||||
|
||||
// 待上传的删除任务 ID(Guid string)
|
||||
const pendingDeletes = reactive(new Set<string>());
|
||||
```
|
||||
|
||||
### 5.6 Guid 字符串处理工具
|
||||
|
||||
```typescript
|
||||
// src/Hua.Todo.Web/src/utils/guid.ts
|
||||
|
||||
/**
|
||||
* 生成新的 Guid 字符串(客户端创建临时任务时使用)。
|
||||
* 格式:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx(小写)
|
||||
*/
|
||||
export function generateGuid(): string {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断字符串是否为有效 Guid 格式。
|
||||
*/
|
||||
export function isValidGuid(value: string): boolean {
|
||||
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、迁移脚本
|
||||
|
||||
### 6.1 EF Core 迁移命令
|
||||
|
||||
```powershell
|
||||
cd src/Hua.Todo.Application
|
||||
dotnet ef migrations add MakeTaskEntityAbpCompatible --startup-project ../Hua.Todo.Host
|
||||
```
|
||||
|
||||
### 6.2 迁移内容预期
|
||||
|
||||
此迁移涉及大量字段变更,建议分阶段执行或使用"双写/兼容期"策略:
|
||||
|
||||
```csharp
|
||||
// MakeTaskEntityAbpCompatible.cs
|
||||
// 表名:T_Tasks(ABP 规范),DbContext 配置为 "Tasks"
|
||||
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// 1. 新增 Guid Id 列(临时名)
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "NewId",
|
||||
table: "T_Tasks",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: Guid.NewGuid());
|
||||
|
||||
// 2. 将旧 int Id 数据迁移到 NewId
|
||||
// (需要手动 SQL 脚本或数据迁移)
|
||||
|
||||
// 3. 删除旧 Id 列,重命名 NewId 为 Id
|
||||
migrationBuilder.DropColumn("Id", "T_Tasks");
|
||||
migrationBuilder.RenameColumn("NewId", "T_Tasks", "Id");
|
||||
|
||||
// 4. 新增 ABP 审计字段
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ExtraProperties",
|
||||
table: "T_Tasks",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ConcurrencyStamp",
|
||||
table: "T_Tasks",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "CreationTime",
|
||||
table: "T_Tasks",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: DateTime.UtcNow);
|
||||
|
||||
migrationBuilder.AddColumn<Guid?>(
|
||||
name: "CreatorId",
|
||||
table: "T_Tasks",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime?>(
|
||||
name: "LastModificationTime",
|
||||
table: "T_Tasks",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<Guid?>(
|
||||
name: "LastModifierId",
|
||||
table: "T_Tasks",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsDeleted",
|
||||
table: "T_Tasks",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime?>(
|
||||
name: "DeletionTime",
|
||||
table: "T_Tasks",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<Guid?>(
|
||||
name: "DeleterId",
|
||||
table: "T_Tasks",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
// 5. ParentTaskId 从 int 改为 Guid
|
||||
migrationBuilder.DropColumn("ParentTaskId", "T_Tasks");
|
||||
migrationBuilder.AddColumn<Guid?>(
|
||||
name: "ParentTaskId",
|
||||
table: "T_Tasks",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// 回滚逻辑...
|
||||
}
|
||||
```
|
||||
|
||||
> **注意**:此迁移为破坏性变更,建议在离线环境充分测试后再部署到生产环境。
|
||||
|
||||
---
|
||||
|
||||
## 七、同步策略(与 02-任务同步规则.md 保持一致)
|
||||
|
||||
> 本章节同步策略逻辑完全遵循 [02-任务同步规则.md](../../manual/02-任务同步规则.md) 6.2 节,字段名使用 ABP 标准。
|
||||
|
||||
### 7.1 核心原则
|
||||
|
||||
- **任务唯一标识是 `id`**:`id` 是任务实体的唯一稳定标识,不可依赖时间戳做身份判断
|
||||
- **`lastModificationTime` 是冲突判断字段**:用于解决"同一任务被多端修改时哪个变更更新"的问题
|
||||
- **服务端存储唯一真实源(Source of Truth)**:所有设备最终都收敛到服务端数据
|
||||
- **字段级 Last-Write-Wins(LWW)合并**:同一字段的多设备并发修改,以 `lastModificationTime` 较新者为准
|
||||
- **逻辑删除(Tombstone)**:删除操作标记为"已删除",而非物理删除,保证多端删除语义一致
|
||||
- **增量同步**:每次同步只上传本端变更(pendingUpserts + pendingDeletes),不上传全量
|
||||
|
||||
### 7.2 变更追踪机制
|
||||
|
||||
每个 `CloudTaskItem` 包含以下时间戳字段:
|
||||
|
||||
| ABP 字段 | 说明 |
|
||||
|---|---|
|
||||
| `creationTime` | 创建时间(服务端分配,永不改变) |
|
||||
| `lastModificationTime` | 最后一次修改时间(每次字段变更时更新) |
|
||||
| `isDeleted` | 软删除标记 |
|
||||
| `deletionTime` | 逻辑删除时间(为 `null` 表示未删除) |
|
||||
| `creatorId` | 创建人 ID |
|
||||
| `lastModifierId` | 最后修改人 ID |
|
||||
|
||||
> `lastModificationTime` 由**请求发起方**(客户端或服务端)在发起变更时写入,存储到服务端后不再覆盖(除非有更新的变更)。
|
||||
|
||||
### 7.3 冲突解决规则
|
||||
|
||||
当客户端提交的任务与服务端现有任务发生 `id` 冲突时,按以下规则处理:
|
||||
|
||||
| 场景 | 解决规则 |
|
||||
|---|---|
|
||||
| 同一 `id`,客户端 `lastModificationTime` **更新** | 服务端接受客户端版本,覆盖对应字段 |
|
||||
| 同一 `id`,服务端 `lastModificationTime` **更新** | 服务端拒绝客户端版本,保留服务端版本 |
|
||||
| 同一 `id`,两者 `lastModificationTime` **相同** | 服务端优先(保守策略) |
|
||||
|
||||
**字段级合并示例**:
|
||||
- 设备 A 修改标题,设备 B 修改优先级
|
||||
- 服务端对两个字段分别取 `lastModificationTime` 较新者,合并出最终结果
|
||||
- 两个设备的修改都被保留,不会互相覆盖
|
||||
|
||||
### 7.4 父子任务处理
|
||||
|
||||
- **删除**:删除父任务时,其子任务一并标记为已删除(递归)
|
||||
- **创建**:子任务的 `parentTaskId` 在上传时若为 `null`(新创建任务),服务端分配 ID 后,第一遍返回临时 ID 映射;第二遍利用映射完成父子关系重映射
|
||||
- **重建父子关系**:若原父任务被删除后重建,子任务的 `parentTaskId` 引用仍指向原父 ID,不会自动指向新父
|
||||
|
||||
### 7.5 Tombstone 保留策略
|
||||
|
||||
- `deletionTime` 非 `null` 的任务在服务端保留至少 **30 天**
|
||||
- 超过 30 天后由服务端垃圾收集(物理删除)
|
||||
- 客户端同步时拉取全量(含 Tombstone),本地过滤不展示 `deletionTime != null` 的任务
|
||||
|
||||
### 7.6 服务端处理流程
|
||||
|
||||
`POST /sync` 服务端执行顺序:
|
||||
|
||||
```
|
||||
1. 解析 upserts 和 deletes
|
||||
2. 对 deletes 中的每个 id:
|
||||
- 递归标记该任务及所有子任务 deletionTime = now, isDeleted = true
|
||||
3. 对 upserts 按 lastModificationTime 降序排列(较新的先处理)
|
||||
4. 对每个 upsert 任务:
|
||||
a. 若 id 在服务端不存在 → 创建新记录(分配 Guid)
|
||||
b. 若 id 存在但服务端 lastModificationTime 更新 → 跳过(保留服务端)
|
||||
c. 若 id 存在且客户端 lastModificationTime >= 服务端 lastModificationTime → 字段级合并更新
|
||||
5. 返回当前用户全量任务(含 Tombstone)
|
||||
```
|
||||
|
||||
> 排序处理的目的:确保最新的变更优先被采纳。
|
||||
|
||||
### 7.7 关于"服务端为准"的正确理解
|
||||
|
||||
"服务端为准"并不意味着客户端会丢失数据。完整的同步流程是:
|
||||
|
||||
1. **客户端上传阶段**:将 `pendingUpserts` + `pendingDeletes` 提交到服务器
|
||||
2. **服务器合并阶段**:按 LWW 规则合并客户端提交与服务器现有数据
|
||||
3. **客户端覆盖阶段**:用服务器返回的全量数据覆盖本地
|
||||
|
||||
由于客户端在第 1 步已经把本地所有变更提交,服务器在第 2 步已经把客户端的变更合并进权威状态,第 3 步用权威状态覆盖本地是安全的——**不会丢失任何已提交的变更**。
|
||||
|
||||
真正会丢失的场景是:客户端在离线状态下修改了任务 A,但没有在联网后发起同步就直接断开连接。这种情况下,离线修改会保留在本地 `pendingUpserts` 中,下次联网同步时会正常提交。
|
||||
|
||||
---
|
||||
|
||||
## 九、服务端代码改动
|
||||
|
||||
### 9.1 TaskRepository 查询过滤
|
||||
|
||||
```csharp
|
||||
// 基类已自动过滤 IsDeleted = true 的记录
|
||||
// 但若需要显式查询(含已删除),使用 _context.Tasks.IgnoreQueryFilters()
|
||||
```
|
||||
|
||||
### 9.2 CloudTaskSyncService 逻辑删除处理
|
||||
|
||||
```csharp
|
||||
// POST /sync 处理 deletes
|
||||
foreach (var id in request.Deletes)
|
||||
{
|
||||
var task = await _context.Tasks.FindAsync(id);
|
||||
if (task != null && !task.IsDeleted)
|
||||
{
|
||||
await _taskRepository.DeleteAsync(task); // 调用基类软删除
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 十、验收标准
|
||||
|
||||
1. [ ] TaskEntity 继承 `FullAuditedEntityWithUser<Guid, IdentityUser>`
|
||||
2. [ ] 主键类型从 `int` 迁移到 `Guid`
|
||||
3. [ ] EF Core 迁移成功执行,新增 ABP 审计字段
|
||||
4. [ ] 服务端 `/tasks` 端点返回 ABP 标准字段
|
||||
5. [ ] 服务端 `/sync` 端点正确处理软删除
|
||||
6. [ ] 前端正确适配 Guid 类型和 ABP 字段
|
||||
7. [ ] 同步规则文档(02-任务同步规则.md)字段命名已修正
|
||||
8. [ ] 数据模型约束文档(03-数据模型与迁移约束.md)已更新
|
||||
|
||||
---
|
||||
|
||||
## 十一、风险与缓解
|
||||
|
||||
| 风险 | 缓解措施 |
|
||||
|---|---|
|
||||
| 主键从 int 变 Guid 为破坏性迁移 | 使用双写/兼容期策略;提供数据迁移脚本 |
|
||||
| 迁移破坏嵌入式宿主启动 | 在 MAUI/Avalonia 上验证 `Database.Migrate()` 不报错 |
|
||||
| 前端类型变更导致编译错误 | 同步更新 TypeScript 类型定义 |
|
||||
| 旧客户端不兼容新 API | API 版本化;新字段为可选/默认值 |
|
||||
|
||||
**回滚方案**:
|
||||
- 迁移回滚:`dotnet ef migrations remove`(可能丢失数据)
|
||||
- 代码回滚:恢复 TaskEntity 原定义
|
||||
|
||||
---
|
||||
|
||||
## 十二、Touch List
|
||||
|
||||
| 文件路径 | 改动类型 | 共享文件 |
|
||||
|---|---|---|
|
||||
| `src/Hua.Todo.Core/Entities/TaskEntity.cs` | 重构(继承 ABP 基类) | 否 |
|
||||
| `src/Hua.Todo.Application/CloudSync/Models/TaskSyncDtos.cs` | DTO 字段更新(新增 ABP 审计字段) | 否 |
|
||||
| `src/Hua.Todo.Application/Data/TodoDbContext.cs` | 查询过滤器配置 | 否 |
|
||||
| `src/Hua.Todo.Application/CloudSync/CloudTaskSyncService.cs` | 软删除处理逻辑 | 否 |
|
||||
| `src/Hua.Todo.Application/Migrations/` | 新增迁移文件 | 否 |
|
||||
| `src/Hua.Todo.Web/src/types/task.ts` | **重构(Guid 主键 + ABP 审计字段 + 类型修正)** | **前端共享** |
|
||||
| `src/Hua.Todo.Web/src/api/cloudSync.ts` | API 响应类型对齐 | 否 |
|
||||
| `src/Hua.Todo.Web/src/stores/taskStore.ts` | pendingUpserts/pendingDeletes 类型修正 | 否 |
|
||||
| `src/Hua.Todo.Web/src/utils/guid.ts` | **新增 Guid 工具函数** | 否 |
|
||||
| `docs/manual/02-任务同步规则.md` | 文档修正(字段命名) | 是(文档) |
|
||||
| `.trae/rules/项目/03-数据模型与迁移约束.md` | 实体清单更新 | 是(规则) |
|
||||
|
||||
---
|
||||
|
||||
## 十三、与其他工单的边界
|
||||
|
||||
| 工单 | 边界 |
|
||||
|---|---|
|
||||
| 04-CloudSync-服务端基础能力 | 依赖本工单的 ABP 审计字段 |
|
||||
| 05-CloudSync-客户端配置与工作流 | 依赖本工单的 DTO 字段更新 |
|
||||
| 08-同源 Host 重构 | 无依赖,可并行 |
|
||||
|
||||
> **建议**:本工单与 04/05 存在依赖关系,建议在 04/05 之前完成,或合并为一个工单实现。
|
||||
|
||||
---
|
||||
|
||||
> **相关文档**:
|
||||
> - ABP 表设计规范:[2.表设计与字段命名规范.md](file:///d:/Codes/CodeSmith/CodeSmith/ShaoHua.CodeSmith.Abp/docs/2.表设计与字段命名规范.md)
|
||||
> - 同步规则:[02-任务同步规则.md](../../manual/02-任务同步规则.md)
|
||||
> - 数据模型约束:[03-数据模型与迁移约束.md](../../../.trae/rules/项目/03-数据模型与迁移约束.md)
|
||||
Reference in New Issue
Block a user