Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4fe0b5a963 | |||
| 65cee20006 | |||
| 9223ceca50 | |||
| aacc56e952 | |||
| 8b0b2cb197 |
@@ -366,3 +366,5 @@ FodyWeavers.xsd
|
|||||||
/Hua.Todo/Output
|
/Hua.Todo/Output
|
||||||
/src/Hua.Todo.Maui/Output
|
/src/Hua.Todo.Maui/Output
|
||||||
/src/Hua.Todo.Host/Hua.Todo.db
|
/src/Hua.Todo.Host/Hua.Todo.db
|
||||||
|
src/Hua.Todo.Host/Hua.Todo.db-wal
|
||||||
|
src/Hua.Todo.Host/Hua.Todo.db-shm
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
---
|
|
||||||
alwaysApply: true
|
|
||||||
description: 强制项目注释规范(C# / TypeScript):新增或修改代码必须补全必要注释,便于维护与跨平台开发。
|
|
||||||
---
|
|
||||||
|
|
||||||
# 注释规范(必须遵守)
|
|
||||||
|
|
||||||
## 通用
|
|
||||||
|
|
||||||
- 新增或修改的代码必须包含足够注释,使“不了解该模块的人”也能理解其职责、边界与关键决策。
|
|
||||||
- 优先使用 **XML 文档注释**(`///`),而不是随意的行内注释。
|
|
||||||
- 不允许无意义注释(例如“初始化变量”“进入方法”)。注释必须解释“为什么/约束/边界/副作用”。
|
|
||||||
- 不允许出现“TODO/FIXME”但无上下文或无处理方案的注释。
|
|
||||||
|
|
||||||
## C#(.NET / MAUI)
|
|
||||||
|
|
||||||
- 所有 `public` / `protected` 的 **类、接口、方法、属性** 必须提供 XML 文档注释,至少包含:
|
|
||||||
- `summary`:一句话说明用途
|
|
||||||
- 对关键参数/返回值:`param` / `returns`
|
|
||||||
- 对异常或副作用:在 `summary` 中明确说明(例如会注册系统钩子/会启动后台服务)
|
|
||||||
- 对 **跨平台逻辑**:
|
|
||||||
- 禁止在同一文件内混写多个平台的大段 `#if` 实现;应优先使用 `partial`、接口与平台目录分离。
|
|
||||||
- 平台分离后的公共入口处必须说明“平台差异在哪里、默认实现是什么、为什么这么做”。
|
|
||||||
- 对 **异步/后台任务**:
|
|
||||||
- 必须说明启动时机、错误处理策略、是否需要 UI 线程、以及是否可并发/可重入。
|
|
||||||
- 对 **安全/隐私**:
|
|
||||||
- 禁止在日志或注释中输出密钥、Token、用户隐私信息。
|
|
||||||
|
|
||||||
## TypeScript / Vue(前端)
|
|
||||||
|
|
||||||
- 对导出的函数/类型必须有注释,解释用途与输入输出。
|
|
||||||
- 对“与后端/MAUI 交互”的协议字段(例如全局变量、事件名)必须注释说明来源与约束。
|
|
||||||
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
---
|
|
||||||
alwaysApply: true
|
|
||||||
description: 强制文档同步规范:每次变更代码(如新增功能、修改接口、调整架构等)必须同步更新 README.md 和 docs 目录下的相关文档。
|
|
||||||
---
|
|
||||||
|
|
||||||
# 文档同步规范(必须遵守)
|
|
||||||
|
|
||||||
## 通用原则
|
|
||||||
|
|
||||||
- **代码即文档,文档随代码**:文档不是静态的,它必须真实反映当前代码的状态。
|
|
||||||
- **及时性**:在提交代码变更的同时(或紧随其后),必须完成相关文档的更新。
|
|
||||||
- **准确性**:确保文档中的示例代码、接口说明、安装步骤与实际代码完全一致。
|
|
||||||
- **协作友好(局部修改)**:当并行处理多个任务/需求时,更新文档应尽量只修改与本任务直接相关的段落/小节,避免对不相关内容做无意义的重排、改写或格式化;如必须调整非关联内容,应拆分为独立的变更说明清楚原因与影响范围。
|
|
||||||
|
|
||||||
## 更新范围
|
|
||||||
|
|
||||||
- **README.md**:
|
|
||||||
- 如果变更涉及核心功能点(Features)、安装步骤(Installation)、快速开始(Quick Start)或 API 端点(API Endpoints),必须同步更新。
|
|
||||||
- 变更涉及技术栈调整或项目结构变化时需更新。
|
|
||||||
- **docs/ 目录文档**:
|
|
||||||
- **接口变更**:若修改了 API,需同步更新 [技术设计文档](docs/技术设计文档.md) 中的接口部分。
|
|
||||||
- **功能新增/调整**:需在 [产品需求文档](docs/产品需求文档.md) 和 [技术栈与模块](docs/技术栈与模块.md) 中体现。
|
|
||||||
- **架构/模式变更**:需更新 [技术设计文档](docs/技术设计文档.md)。
|
|
||||||
- **代码规范**:若引入了新的编码模式或工具,需更新 [代码规范文档](docs/代码规范文档.md)。
|
|
||||||
- **版本记录**:所有非琐碎的变更必须在 [版本记录.md](docs/版本记录.md) 中添加记录。
|
|
||||||
|
|
||||||
## 检查清单
|
|
||||||
|
|
||||||
1. [ ] 是否有新增的 API 端点?(更新 README 和技术设计文档)
|
|
||||||
2. [ ] 是否修改了现有的业务逻辑或数据结构?(更新技术设计文档)
|
|
||||||
3. [ ] 是否有新增的功能模块?(更新产品需求文档和技术栈说明)
|
|
||||||
4. [ ] 是否调整了开发环境或依赖?(更新 README)
|
|
||||||
5. [ ] 是否在 [版本记录.md](docs/版本记录.md) 中记录了本次变更?
|
|
||||||
6. [ ] 文档变更是否保持“局部修改”,只影响与本任务相关的段落/小节?(避免无关重排/改写)
|
|
||||||
@@ -1,17 +1,46 @@
|
|||||||
# 智能体记忆与规范同步规则(必须遵守)
|
# 智能体记忆与存储规范(必须遵守)
|
||||||
|
|
||||||
> 适用范围:本规则属于 **全局规则**(语义层面跨项目可复用),关注智能体如何维护记忆与同步规范,与具体项目业务无关。
|
> 适用范围:本规则属于 **全局规则**(语义层面跨项目可复用),关注智能体如何维护记忆、存储与同步规范,与具体项目业务无关。
|
||||||
>
|
>
|
||||||
> `.trae/` 整体目录结构与各子目录职责见 [.trae/索引.md](../../索引.md),本文件不再重复描述。
|
> `.trae/` 整体目录结构与各子目录职责见 [.trae/索引.md](../../索引.md),本文件不再重复描述。
|
||||||
|
|
||||||
## 记忆存储
|
## 一、记忆存储
|
||||||
|
|
||||||
|
### 1.1 存储位置与组织
|
||||||
|
|
||||||
- 智能体的记忆必须存放在 `.trae/memory` 文件夹中
|
- 智能体的记忆必须存放在 `.trae/memory` 文件夹中
|
||||||
- 记忆应按对话日期或主题进行组织,便于后续查询和参考
|
- 记忆应按对话日期或主题进行组织,便于后续查询和参考
|
||||||
- 记忆内容应包含对话历史、关键决策、重要代码片段和规范调整等信息
|
- 记忆内容应包含对话历史、关键决策、重要代码片段和规范调整等信息
|
||||||
- **项目即时状态**(当前实现到哪一步、未完结事项、临时决策快照)应同步写入 `.trae/rules/项目/04-即时状态记忆.md`,便于其他智能体或开发者快速对齐
|
- **项目即时状态**(当前实现到哪一步、未完结事项、临时决策快照)应同步写入 `.trae/rules/项目/04-即时状态记忆.md`,便于其他智能体或开发者快速对齐
|
||||||
|
|
||||||
## 规范同步
|
### 1.2 内容规范
|
||||||
|
|
||||||
|
- 记忆文件应保持简洁明了,重点记录重要的开发决策和规范变更
|
||||||
|
- 避免存储冗余信息,只记录对项目有价值的内容
|
||||||
|
- 定期清理过时的记忆文件,保持存储空间的合理使用
|
||||||
|
|
||||||
|
### 1.3 与"项目即时状态"的边界
|
||||||
|
|
||||||
|
- `.trae/memory/` 偏向**长期保留**的对话产物与决策记录
|
||||||
|
- `.trae/rules/项目/04-即时状态记忆.md` 偏向**当前快照**(实现进度、未完结事项),更新频率高
|
||||||
|
- 二者不要重复存放同一份信息;以"是否需要长期沉淀"为判定标准
|
||||||
|
|
||||||
|
### 1.4 memory/ 生命周期
|
||||||
|
|
||||||
|
研发工单验收完成后,对 `memory/` 的处理遵循以下原则:
|
||||||
|
|
||||||
|
- **追加而非覆盖**:将本次工单中产生的、值得**长期沉淀**的内容(架构决策、关键避坑经验、引入的新依赖与版本)追加到对应文件
|
||||||
|
- **不存放过程信息**:实现进度、待办勾选、临时决策这些短期信息应留在 [04-即时状态记忆.md](../项目/04-即时状态记忆.md),不进 `memory/`
|
||||||
|
- **不删长期内容**:除非内容已被证伪或过时,否则不删除既有条目;过时内容用"已废弃 / 已被 XX 取代"的形式保留语义而非物理删除
|
||||||
|
- **新增文件序号化**:当主题足够独立时新建 `NN-名称.md`(序号紧接当前最大值),并在 [.trae/索引.md](../../索引.md) 的 `memory/` 章节同步追加链接
|
||||||
|
|
||||||
|
### 1.5 访问权限
|
||||||
|
|
||||||
|
- 记忆文件仅供开发团队内部参考使用
|
||||||
|
- 确保记忆文件中的敏感信息得到适当保护
|
||||||
|
- 遵循项目的版本控制和代码管理规范
|
||||||
|
|
||||||
|
## 二、规范同步
|
||||||
|
|
||||||
- 每次对话中涉及到的语法或规范相关内容,必须同步整理到 `.trae/rules` 目录下的对应文件中
|
- 每次对话中涉及到的语法或规范相关内容,必须同步整理到 `.trae/rules` 目录下的对应文件中
|
||||||
- **通用规范**(注释、文档同步、工单流程、并行冲突)→ 写入 `.trae/rules/全局/`
|
- **通用规范**(注释、文档同步、工单流程、并行冲突)→ 写入 `.trae/rules/全局/`
|
||||||
@@ -19,11 +48,11 @@
|
|||||||
- 若涉及到新的规范或规则,应创建新的规则文件进行记录
|
- 若涉及到新的规范或规则,应创建新的规则文件进行记录
|
||||||
- 规范同步应及时、准确,确保规则文件能真实反映当前项目的编码规范和最佳实践
|
- 规范同步应及时、准确,确保规则文件能真实反映当前项目的编码规范和最佳实践
|
||||||
|
|
||||||
## 文件命名规则(强制)
|
## 三、文件命名规则(强制)
|
||||||
|
|
||||||
`.trae/` 下所有子目录中**新增的文件必须沿用 `NN-名称.md` 序号格式**,否则视为不合规:
|
`.trae/` 下所有子目录中**新增的文件必须沿用 `NN-名称.md` 序号格式**,否则视为不合规:
|
||||||
|
|
||||||
- **格式**:两位数字 + 连字符 + 中文/英文名称 + `.md`,例如 `08-XXX规范.md`
|
- **格式**:两位数字 + 连字符 + 中文/英文名称 + `.md`,例如 `06-XXX规范.md`
|
||||||
- **序号取值**:紧接当前目录已有最大序号 +1,不得跳号、不得重复
|
- **序号取值**:紧接当前目录已有最大序号 +1,不得跳号、不得重复
|
||||||
- **入口/索引文件例外**:`.trae/索引.md` 这类目录入口文件不带序号
|
- **入口/索引文件例外**:`.trae/索引.md` 这类目录入口文件不带序号
|
||||||
- **重排禁止**:除非整体重构,否则不得重排已有文件的序号;新增只能追加在末尾
|
- **重排禁止**:除非整体重构,否则不得重排已有文件的序号;新增只能追加在末尾
|
||||||
@@ -35,17 +64,17 @@
|
|||||||
| 子目录 | 当前最大序号 | 下一个可用 |
|
| 子目录 | 当前最大序号 | 下一个可用 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `rules/全局/` | 07 | 08 |
|
| `rules/全局/` | 07 | 08 |
|
||||||
| `rules/项目/` | 04 | 05 |
|
| `rules/项目/` | 05 | 06 |
|
||||||
| `memory/` | 01 | 02 |
|
| `memory/` | 01 | 02 |
|
||||||
| `coordination/` | 02 | 03 |
|
| `coordination/` | 02 | 03 |
|
||||||
|
|
||||||
## 实现要求
|
## 四、实现要求
|
||||||
|
|
||||||
- 智能体应定期检查并更新规则文件,确保其与项目实际情况保持一致
|
- 智能体应定期检查并更新规则文件,确保其与项目实际情况保持一致
|
||||||
- 当发现规范冲突或需要调整时,应及时记录并通知相关人员
|
- 当发现规范冲突或需要调整时,应及时记录并通知相关人员
|
||||||
- 记忆存储和规范同步应作为智能体的核心功能,贯穿于整个开发过程
|
- 记忆存储和规范同步应作为智能体的核心功能,贯穿于整个开发过程
|
||||||
|
|
||||||
## 路径规范
|
## 五、路径规范
|
||||||
|
|
||||||
- 所有 Markdown 文档中不应使用绝对路径,应使用相对路径
|
- 所有 Markdown 文档中不应使用绝对路径,应使用相对路径
|
||||||
- 相对路径应以项目根目录为基准,例如 `.trae/memory` 而非绝对路径
|
- 相对路径应以项目根目录为基准,例如 `.trae/memory` 而非绝对路径
|
||||||
@@ -17,9 +17,11 @@ description: 强制项目注释规范(C# / TypeScript):新增或修改代
|
|||||||
## C#(.NET / MAUI)
|
## C#(.NET / MAUI)
|
||||||
|
|
||||||
- 所有 `public` / `protected` 的 **类、接口、方法、属性** 必须提供 XML 文档注释,至少包含:
|
- 所有 `public` / `protected` 的 **类、接口、方法、属性** 必须提供 XML 文档注释,至少包含:
|
||||||
- `summary`:一句话说明用途
|
- `summary`:一句话说明用途,不允许重复嵌套 `<summary>` 标签
|
||||||
- 对关键参数/返回值:`param` / `returns`
|
- 对参数/返回值:构造函数和方法的每个参数都必须有对应 `param`,包括可选参数、`logger` 等基础设施参数;有返回值时补充 `returns`
|
||||||
- 对异常或副作用:在 `summary` 中明确说明(例如会注册系统钩子/会启动后台服务)
|
- 对异常或副作用:在 `summary` 中明确说明(例如会注册系统钩子/会启动后台服务)
|
||||||
|
- XML 文档注释必须紧贴被说明的语言元素;若元素还有特性(如 `[AttributeUsage]`),顺序必须是 XML 注释、特性、类型/成员声明,避免 `///` 落在特性之后导致 CS1587。
|
||||||
|
- `<see cref="..."/>` 只引用当前项目能解析的类型/成员;跨程序集或未引入命名空间时改用普通文本,避免 CS1574。
|
||||||
- 对 **跨平台逻辑**:
|
- 对 **跨平台逻辑**:
|
||||||
- 禁止在同一文件内混写多个平台的大段 `#if` 实现;应优先使用 `partial`、接口与平台目录分离。
|
- 禁止在同一文件内混写多个平台的大段 `#if` 实现;应优先使用 `partial`、接口与平台目录分离。
|
||||||
- 平台分离后的公共入口处必须说明"平台差异在哪里、默认实现是什么、为什么这么做"。
|
- 平台分离后的公共入口处必须说明"平台差异在哪里、默认实现是什么、为什么这么做"。
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
# 记忆存储规范
|
|
||||||
|
|
||||||
> 适用范围:本规则属于 **全局规则**(跨项目通用),约束 `.trae/memory/` 的使用方式。
|
|
||||||
|
|
||||||
## 存储结构
|
|
||||||
- 记忆文件应存放在 `.trae/memory` 文件夹中
|
|
||||||
- 避免使用与日期相关的文件名,使用通用的描述性文件名
|
|
||||||
- 文件命名采用 `NN-名称.md` 序号格式
|
|
||||||
- 记忆内容应包含对话历史、关键决策、重要代码片段和规范调整等信息
|
|
||||||
|
|
||||||
## 内容规范
|
|
||||||
- 记忆文件应保持简洁明了,重点记录重要的开发决策和规范变更
|
|
||||||
- 避免存储冗余信息,只记录对项目有价值的内容
|
|
||||||
- 定期清理过时的记忆文件,保持存储空间的合理使用
|
|
||||||
|
|
||||||
## 与"项目即时状态"的边界
|
|
||||||
- `.trae/memory/` 偏向**长期保留**的对话产物与决策记录
|
|
||||||
- `.trae/rules/项目/04-即时状态记忆.md` 偏向**当前快照**(实现进度、未完结事项),更新频率高
|
|
||||||
- 二者不要重复存放同一份信息;以"是否需要长期沉淀"为判定标准
|
|
||||||
|
|
||||||
## memory/ 生命周期
|
|
||||||
|
|
||||||
研发工单验收完成后,对 `memory/` 的处理遵循以下原则:
|
|
||||||
|
|
||||||
- **追加而非覆盖**:将本次工单中产生的、值得**长期沉淀**的内容(架构决策、关键避坑经验、引入的新依赖与版本)追加到对应文件
|
|
||||||
- **不存放过程信息**:实现进度、待办勾选、临时决策这些短期信息应留在 [04-即时状态记忆.md](../项目/04-即时状态记忆.md),不进 `memory/`
|
|
||||||
- **不删长期内容**:除非内容已被证伪或过时,否则不删除既有条目;过时内容用"已废弃 / 已被 XX 取代"的形式保留语义而非物理删除
|
|
||||||
- **新增文件序号化**:当主题足够独立时新建 `NN-名称.md`(序号紧接当前最大值),并在 [.trae/索引.md](../../索引.md) 的 `memory/` 章节同步追加链接
|
|
||||||
|
|
||||||
## 访问权限
|
|
||||||
- 记忆文件仅供开发团队内部参考使用
|
|
||||||
- 确保记忆文件中的敏感信息得到适当保护
|
|
||||||
- 遵循项目的版本控制和代码管理规范
|
|
||||||
@@ -14,7 +14,7 @@ description: 强制文档同步规范:每次变更代码(如新增功能、
|
|||||||
- **准确性**:确保文档中的示例代码、接口说明、安装步骤与实际代码完全一致。
|
- **准确性**:确保文档中的示例代码、接口说明、安装步骤与实际代码完全一致。
|
||||||
- **协作友好(局部修改)**:当并行处理多个研发工单/需求时,更新文档应尽量只修改与本工单直接相关的段落/小节,避免对不相关内容做无意义的重排、改写或格式化;如必须调整非关联内容,应拆分为独立的变更说明清楚原因与影响范围。
|
- **协作友好(局部修改)**:当并行处理多个研发工单/需求时,更新文档应尽量只修改与本工单直接相关的段落/小节,避免对不相关内容做无意义的重排、改写或格式化;如必须调整非关联内容,应拆分为独立的变更说明清楚原因与影响范围。
|
||||||
|
|
||||||
> 术语澄清:本规范中"研发工单"指编码工作项;项目业务里的"任务/Todo 待办项"是用户域实体,二者不要混淆。详见 [05-研发工单规则.md](./05-研发工单规则.md)。
|
> 术语澄清:本规范中"研发工单"指编码工作项;项目业务里的"任务/Todo 待办项"是用户域实体,二者不要混淆。详见 [04-研发工单全流程规范.md](./04-研发工单全流程规范.md)。
|
||||||
|
|
||||||
## 更新范围
|
## 更新范围
|
||||||
|
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
# 研发工单全流程规范(必须遵守)
|
||||||
|
|
||||||
|
> 适用范围:本规则属于 **全局规则**(跨项目通用),覆盖研发工单从术语定义、拆分输出到新增约束的全流程。
|
||||||
|
|
||||||
|
> ⚠️ 术语澄清(必读)
|
||||||
|
>
|
||||||
|
> 本项目存在两类"任务"概念,必须严格区分:
|
||||||
|
>
|
||||||
|
> | 术语 | 含义 | 适用范围 |
|
||||||
|
> |---|---|---|
|
||||||
|
> | **研发工单(Dev Work Item)** | 智能体 / 开发者执行的**编码工作项** | 本规则的全部内容 |
|
||||||
|
> | **Todo 待办项(Todo Item)** | Hua.Todo 项目**业务领域**中用户创建的待办事项 | 业务代码、产品文档 |
|
||||||
|
>
|
||||||
|
> 在代码、文档与对话中,凡涉及编码侧拆分时,**必须使用"研发工单"或"工单"**,禁止使用"任务"二字。
|
||||||
|
> 业务侧 `Task` / `SubTask` 等代码标识符**保持不变**(已固化于 API、DB、UI)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、研发工单拆分规范
|
||||||
|
|
||||||
|
### 1.1 适用时机
|
||||||
|
|
||||||
|
- 当需求需要先通读项目/产品/技术文档再开始实现时,必须先输出**研发工单拆分文档**
|
||||||
|
|
||||||
|
### 1.2 输出要求
|
||||||
|
|
||||||
|
1. **先读完所有相关文档**:包括 `docs/`、`docs/project/` 下与本次需求相关的内容
|
||||||
|
2. **先写工单拆分,再动手实现**:研发工单拆分产出是后续执行的入口与对齐依据
|
||||||
|
3. **新增专属文件夹**:在 `docs/project` 下新建 `研发工单-<主题>-<日期或版本>` 文件夹
|
||||||
|
4. **可并行工单拆分**:能同步执行的工单必须拆到不同 Markdown 文件中
|
||||||
|
5. **文件带序号**:按执行顺序编号(`01-xxx.md`、`02-xxx.md`)
|
||||||
|
|
||||||
|
### 1.3 每个研发工单文件必须包含
|
||||||
|
|
||||||
|
- 目标 / 范围(做什么、不做什么)
|
||||||
|
- 前置条件(依赖哪些结论 / 接口 / 文档)
|
||||||
|
- 验收标准(可执行的验证点)
|
||||||
|
- 风险与回滚(如有)
|
||||||
|
|
||||||
|
### 1.4 子工单完成标记要求
|
||||||
|
|
||||||
|
- 子工单完成后,必须在 `00-工单总览.md` 中标注"已完成"
|
||||||
|
- 维护"待验证表",记录每个子工单的"待验证 / 已验证"状态
|
||||||
|
|
||||||
|
### 1.5 并行冲突规避要求
|
||||||
|
|
||||||
|
当工单会被分发到多个 solo 窗口并行推进时,每个研发工单文件必须额外包含 Touch List、共享文件策略与编译绿线策略。详细规约见 [05-并行窗口冲突规约.md](./05-并行窗口冲突规约.md)。
|
||||||
|
|
||||||
|
### 1.6 推荐结构
|
||||||
|
|
||||||
|
```
|
||||||
|
docs/project/研发工单-<主题>-<版本>/
|
||||||
|
├── 00-工单总览.md # 背景、目标、关键决策、并行分组、待验证表
|
||||||
|
├── 01-并行工单A.md
|
||||||
|
├── 02-并行工单B.md
|
||||||
|
└── 03-串行工单C.md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、新增工单约束
|
||||||
|
|
||||||
|
> 核心原则:**新增工单时,不得修改、覆盖、重排、删除任何已有工单文件。**
|
||||||
|
|
||||||
|
### 2.1 已有文件不可触碰
|
||||||
|
|
||||||
|
| 操作 | 是否允许 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| 修改已有工单的 `.md` 内容 | ❌ 禁止 | 即使发现格式、措辞可优化 |
|
||||||
|
| 重命名已有工单文件 | ❌ 禁止 | |
|
||||||
|
| 删除已有工单文件 | ❌ 禁止 | |
|
||||||
|
| 重排已有工单的序号 | ❌ 禁止 | 除非用户明确要求整体重构 |
|
||||||
|
| 修改 `00-工单总览.md` 中已有条目 | ❌ 禁止 | 只能追加新条目 |
|
||||||
|
| 在 `00-工单总览.md` 中追加新条目 | ✅ 允许 | |
|
||||||
|
| 修改 `.trae/rules/项目/04-即时状态记忆.md` 中已有快照行 | ❌ 禁止 | 只能追加新版本行 |
|
||||||
|
| 新增章节到 `04-即时状态记忆.md` | ✅ 允许 | |
|
||||||
|
|
||||||
|
### 2.2 子工单拆分格式
|
||||||
|
|
||||||
|
```
|
||||||
|
NN-NN-标题.md
|
||||||
|
```
|
||||||
|
|
||||||
|
- 前两位:主工单序号,后两位:子工单序号
|
||||||
|
- 示例:`03-01-会议数据模型与API.md`
|
||||||
|
|
||||||
|
### 2.3 新增工单的序号确定
|
||||||
|
|
||||||
|
1. 列出目标文件夹中已有文件
|
||||||
|
2. 找出最大主序号
|
||||||
|
3. 新增工单的主序号 = 最大主序号 + 1
|
||||||
|
4. 子工单的子序号从 `01` 开始递增
|
||||||
|
|
||||||
|
### 2.4 可追加修改的文件(例外)
|
||||||
|
|
||||||
|
| 文件 | 允许 | 不允许 |
|
||||||
|
|---|---|---|
|
||||||
|
| `00-工单总览.md` | 追加新条目 | 改写已有条目 |
|
||||||
|
| `.trae/rules/项目/04-即时状态记忆.md` | 新增版本章节 | 修改已有快照行 |
|
||||||
|
| `.trae/索引.md` | 追加新规则链接 | 修改已有条目 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、与业务侧 Todo 待办项的边界
|
||||||
|
|
||||||
|
- 代码、注释、提交信息中描述**编码工作**时:使用"研发工单 / 工单 / 子工单"
|
||||||
|
- 代码、注释、提交信息中描述**业务功能**时:使用"Todo 待办项 / Todo Item / 父子任务(业务实体)"
|
||||||
|
- 文档命名前缀:
|
||||||
|
- 编码侧:`研发工单-<主题>-<版本>/`
|
||||||
|
- 业务侧:遵循 `docs/` 既有命名习惯
|
||||||
|
- 提交信息示例:
|
||||||
|
- ✅ `feat(todo): 新增 Todo 待办项截止日期字段(研发工单 02-后端模型)`
|
||||||
|
- ❌ `feat: 完成任务 02`("任务"歧义)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、检查清单
|
||||||
|
|
||||||
|
### 研发工单拆分检查
|
||||||
|
|
||||||
|
1. [ ] 是否已阅读完所有相关文档?
|
||||||
|
2. [ ] 是否在 `docs/project` 下新建了专属文件夹(命名以"研发工单-"开头)?
|
||||||
|
3. [ ] 是否产出 `00-工单总览.md`?
|
||||||
|
4. [ ] 是否将可并行工单拆分为不同 md 文件?
|
||||||
|
5. [ ] 是否所有 md 文件都带有连续序号?
|
||||||
|
6. [ ] 子工单完成后是否在总览中标注"已完成"并更新待验证表?
|
||||||
|
|
||||||
|
### 新增工单检查
|
||||||
|
|
||||||
|
1. [ ] 新增工单的序号是否为当前最大主序号 + 1?
|
||||||
|
2. [ ] 子工单是否使用了 `NN-NN-标题.md` 格式?
|
||||||
|
3. [ ] 是否**未修改**任何已有工单文件的内容?
|
||||||
|
4. [ ] 是否**未修改** `00-工单总览.md` 和 `04-即时状态记忆.md` 中已有条目?
|
||||||
|
|
||||||
|
### 并行冲突检查
|
||||||
|
|
||||||
|
> 详见 [05-并行窗口冲突规约.md](./05-并行窗口冲突规约.md#最小检查清单)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**关联规则**:[05-并行窗口冲突规约.md](./05-并行窗口冲突规约.md)、[07-代码实现与测试先行规范.md](./07-代码实现与测试先行规范.md)
|
||||||
|
|
||||||
|
> 工单进入代码实现阶段时,必须遵循"测试先行(ATDD)"工作流:先依据本规范的验收标准编写验收测试用例,再实现代码,测试全部通过后才算工单完成。详见 [07-代码实现与测试先行规范.md](./07-代码实现与测试先行规范.md)。
|
||||||
@@ -7,7 +7,7 @@ description:
|
|||||||
> 适用范围:本规则属于 **全局规则**(跨项目通用)。
|
> 适用范围:本规则属于 **全局规则**(跨项目通用)。
|
||||||
|
|
||||||
> ⚠️ 术语澄清:本规范中的「研发工单(Dev Work Item)」专指智能体 / 开发者执行的**编码工作项**,与 Hua.Todo 项目业务领域中的「Todo 待办项」是两个完全不同的概念。
|
> ⚠️ 术语澄清:本规范中的「研发工单(Dev Work Item)」专指智能体 / 开发者执行的**编码工作项**,与 Hua.Todo 项目业务领域中的「Todo 待办项」是两个完全不同的概念。
|
||||||
> 详见 [05-研发工单规则.md](./05-研发工单规则.md)。
|
> 详见 [04-研发工单全流程规范.md](./04-研发工单全流程规范.md)。
|
||||||
> 凡涉及编码侧拆分时,**必须使用「研发工单」或「工单」**,禁止使用「任务」二字以避免与 Todo 待办项混淆。
|
> 凡涉及编码侧拆分时,**必须使用「研发工单」或「工单」**,禁止使用「任务」二字以避免与 Todo 待办项混淆。
|
||||||
|
|
||||||
## 适用范围
|
## 适用范围
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
# 研发工单同步规则汇总(Dev Work Item Rules)
|
|
||||||
|
|
||||||
> 适用范围:本规则属于 **全局规则**(跨项目通用),关注智能体如何拆分编码工作。
|
|
||||||
|
|
||||||
> ⚠️ 术语澄清(必读)
|
|
||||||
>
|
|
||||||
> 本项目存在两类"任务"概念,必须严格区分,避免命名混淆:
|
|
||||||
>
|
|
||||||
> | 术语 | 含义 | 适用范围 |
|
|
||||||
> |---|---|---|
|
|
||||||
> | **研发工单(Dev Work Item)** | 智能体 / 开发者执行的**编码工作项**(拆分需求、并行开发、集成等) | 本规则文档的全部内容 |
|
|
||||||
> | **Todo 待办项(Todo Item)** | Hua.Todo 项目**业务领域**中用户创建的待办事项(数据库实体、API 资源、UI 列表项) | 业务代码、产品需求文档、技术设计文档 |
|
|
||||||
>
|
|
||||||
> 本文档中所有"研发工单 / 工单 / 子工单"均指**编码工作项**,与业务侧的 Todo 待办项无关。
|
|
||||||
> 在代码、文档与对话中,凡涉及编码侧拆分时,**必须使用"研发工单"或"工单"**,禁止再使用"任务"二字以避免与 Todo 待办项混淆。
|
|
||||||
>
|
|
||||||
> 业务侧由于历史原因仍保留 `Task` / `SubTask` 等代码标识符(API、实体、UI),这些属于 Todo 待办项语义,**不在本规范替换范围内**。
|
|
||||||
>
|
|
||||||
> 本汇总文件是 [06-研发工单拆分规范.md](./06-研发工单拆分规范.md) 与 [07-并行窗口冲突规约.md](./07-并行窗口冲突规约.md) 的对外索引,详细规则以这两份源文件为准。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 一、研发工单拆分规范
|
|
||||||
|
|
||||||
### 适用时机
|
|
||||||
- 当需求需要先通读项目/产品/技术文档再开始实现时,必须先输出**研发工单拆分文档**
|
|
||||||
|
|
||||||
### 输出要求
|
|
||||||
1. **先读完所有相关文档**:包括 `docs/`、`docs/project/` 下与本次需求相关的内容
|
|
||||||
2. **先写工单拆分,再动手实现**:研发工单拆分产出是后续执行的入口与对齐依据
|
|
||||||
3. **新增专属文件夹**:在 `docs/project` 下新建 `研发工单-<主题>-<日期或版本>` 文件夹
|
|
||||||
4. **可并行工单拆分**:能同步执行的工单必须拆到不同 Markdown 文件中
|
|
||||||
5. **文件带序号**:按执行顺序编号(`01-xxx.md`、`02-xxx.md`)
|
|
||||||
|
|
||||||
### 每个研发工单文件必须包含
|
|
||||||
- 目标 / 范围(做什么、不做什么)
|
|
||||||
- 前置条件(依赖哪些结论 / 接口 / 文档)
|
|
||||||
- 验收标准(可执行的验证点)
|
|
||||||
- 风险与回滚(如有)
|
|
||||||
|
|
||||||
### 子工单完成标记要求
|
|
||||||
- 子工单完成后,必须在 `00-工单总览.md` 中标注"已完成"
|
|
||||||
- 维护"待验证表",记录每个子工单的"待验证 / 已验证"状态
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 二、并行窗口冲突规约
|
|
||||||
|
|
||||||
### 核心原则
|
|
||||||
1. **先声明后修改**:修改前先声明 Touch List 与共享文件策略
|
|
||||||
2. **文件所有权唯一**:同一时段内一个文件只能由一个窗口修改
|
|
||||||
3. **共享文件单点修改**:高耦合 / 共享入口的改动集中到集成窗口完成
|
|
||||||
4. **绿线优先**:任何可落盘的变更必须保持可编译
|
|
||||||
|
|
||||||
### Touch List 要求
|
|
||||||
- 精确到文件路径
|
|
||||||
- 标注修改类型(新增 / 小改 / 重构 / 接口变更 / 配置变更)
|
|
||||||
- 标注是否为共享文件
|
|
||||||
- 使用相对路径:`src\<module>\<file>`
|
|
||||||
|
|
||||||
### 共享文件判定标准(满足其一即为共享)
|
|
||||||
- 项目入口 / 启动逻辑、依赖注入注册、全局路由
|
|
||||||
- 公共配置、公共协议与 DTO、公共组件 / 样式
|
|
||||||
- 解决方案文件(`.sln`、`.csproj`)、锁文件、全局配置
|
|
||||||
|
|
||||||
### Writer 约束
|
|
||||||
- 非 Writer 窗口不得编辑共享文件
|
|
||||||
- 非 Writer 只能提供"差异建议"给 Writer 落盘
|
|
||||||
|
|
||||||
### 协调目录
|
|
||||||
固定目录:`.trae\coordination\`
|
|
||||||
- `01-ownership.md`:文件所有权登记表
|
|
||||||
- `02-shared-files.md`:共享文件清单(集成窗口维护)
|
|
||||||
- `handoff\`:差异建议 / 交接说明
|
|
||||||
- `wip\`:编译中途状态说明
|
|
||||||
|
|
||||||
### 编译绿线规则
|
|
||||||
- 不得提交破坏编译的变更
|
|
||||||
- 临时隔离手段(按优先级):
|
|
||||||
1. 新功能先放在新文件中,不在入口路径启用
|
|
||||||
2. 通过显式开关控制,默认关闭
|
|
||||||
3. 通过依赖注入分支或特性开关隔离
|
|
||||||
- 接口演进采用"双写 / 兼容期"策略
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 三、推荐文档结构
|
|
||||||
|
|
||||||
```
|
|
||||||
docs/project/研发工单-<主题>-<版本>/
|
|
||||||
├── 00-工单总览.md # 背景、目标、关键决策、并行分组、待验证表
|
|
||||||
├── 01-并行工单A.md
|
|
||||||
├── 02-并行工单B.md
|
|
||||||
└── 03-串行工单C.md
|
|
||||||
```
|
|
||||||
|
|
||||||
> 注意:上述目录与文件名中的"工单"指**研发工单**,与业务侧 Todo 待办项无关。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 四、检查清单
|
|
||||||
|
|
||||||
### 研发工单拆分检查
|
|
||||||
1. [ ] 是否已阅读完所有相关文档?
|
|
||||||
2. [ ] 是否在 `docs/project` 下新建了专属文件夹(命名以"研发工单-"开头)?
|
|
||||||
3. [ ] 是否产出 `00-工单总览.md`?
|
|
||||||
4. [ ] 是否将可并行工单拆分为不同 md 文件?
|
|
||||||
5. [ ] 是否所有 md 文件都带有连续序号?
|
|
||||||
6. [ ] 子工单完成后是否在总览中标注"已完成"并更新待验证表?
|
|
||||||
|
|
||||||
### 并行冲突检查
|
|
||||||
1. [ ] 每个研发工单 md 是否已写 Touch List(精确到文件)?
|
|
||||||
2. [ ] Touch List 中的共享文件是否指定了唯一 Writer?
|
|
||||||
3. [ ] 是否避免了对共享文件的无意义格式化 / 重排?
|
|
||||||
4. [ ] 当前改动是否保持可编译(绿线)?
|
|
||||||
5. [ ] 若涉及接口演进,是否采用兼容期策略?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 五、与业务侧 Todo 待办项的边界
|
|
||||||
|
|
||||||
- 代码、注释、提交信息中描述**编码工作**时:使用"研发工单 / 工单 / 子工单"
|
|
||||||
- 代码、注释、提交信息中描述**业务功能**时:使用"Todo 待办项 / Todo Item / 父子任务(业务实体)"
|
|
||||||
- 文档命名前缀:
|
|
||||||
- 编码侧:`研发工单-<主题>-<版本>/`
|
|
||||||
- 业务侧(如有):遵循 `docs/` 既有命名习惯,禁止使用"研发工单"前缀
|
|
||||||
- 提交信息示例:
|
|
||||||
- ✅ `feat(todo): 新增 Todo 待办项截止日期字段(研发工单 02-后端模型)`
|
|
||||||
- ❌ `feat: 完成任务 02`("任务"歧义,禁用)
|
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# AI沟通记录规范(必须遵守)
|
||||||
|
|
||||||
|
> 适用范围:本规则属于 **全局规则**(跨项目通用),约束用户与智能体之间沟通记录的存储与管理。
|
||||||
|
|
||||||
|
## 一、存储结构
|
||||||
|
|
||||||
|
- AI沟通记录存放在 `docs/AI沟通记录/` 目录下
|
||||||
|
- **按主题归档**:同一主题的多轮对话追加在同一个 `NN-主题摘要.md` 文件中,只有新主题才新建文件
|
||||||
|
- 文件命名格式:`NN-主题摘要.md`(`NN` 为两位序号)
|
||||||
|
- 序号从 `01` 开始,每次新建递增 +1,不得跳号或重复
|
||||||
|
- 智能体在新建沟通记录时,默认使用当前最大序号 +1
|
||||||
|
- 当对话属于已有主题时,应在对应文件的"详细记录"章节追加子主题,而非新建文件
|
||||||
|
|
||||||
|
## 二、目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
docs/AI沟通记录/
|
||||||
|
├── 01-首次项目分析.md
|
||||||
|
├── 02-架构讨论.md
|
||||||
|
└── NN-主题摘要.md
|
||||||
|
```
|
||||||
|
|
||||||
|
- 每个文件即一次对话的完整记录
|
||||||
|
- 如有附件或补充材料,可在同目录下新建 `NN-主题摘要/` 子文件夹存放
|
||||||
|
|
||||||
|
## 三、记录内容规范
|
||||||
|
|
||||||
|
每个沟通记录文件必须包含以下结构:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# AI沟通记录:NN-主题摘要
|
||||||
|
|
||||||
|
- **日期**:YYYY-MM-DD
|
||||||
|
- **参与者**:用户、AI 助手
|
||||||
|
- **会话序号**:NN
|
||||||
|
|
||||||
|
## 讨论主题
|
||||||
|
[简要描述本次对话的核心议题]
|
||||||
|
|
||||||
|
## 关键决策
|
||||||
|
[记录对话中达成的重要决策]
|
||||||
|
|
||||||
|
## 待办事项
|
||||||
|
[对话中确认的后续行动项]
|
||||||
|
|
||||||
|
## 详细记录
|
||||||
|
### NN-子主题1
|
||||||
|
[内容]
|
||||||
|
### NN-子主题2
|
||||||
|
[内容]
|
||||||
|
```
|
||||||
|
|
||||||
|
- **详细记录中的子主题必须带序号前缀**,格式为 `### NN-子主题名称`(`NN` 为两位序号,从 01 开始)
|
||||||
|
- 子主题序号用于精确引用,例如"沟通记录 03 的 02-xxx"
|
||||||
|
|
||||||
|
## 四、序号管理与引用
|
||||||
|
|
||||||
|
- 智能体在对话中可通过序号引用历史沟通记录,例如"参见沟通记录 03"
|
||||||
|
- 用户也可通过序号要求智能体回顾某次对话内容
|
||||||
|
- 序号一旦分配不可变更,即使删除了某个记录文件夹,也不得复用其序号
|
||||||
|
|
||||||
|
## 五、与记忆系统的边界
|
||||||
|
|
||||||
|
| 系统 | 用途 | 更新频率 |
|
||||||
|
|---|---|---|
|
||||||
|
| `docs/AI沟通记录/` | 对话过程与决策的完整记录 | 每次对话新建 |
|
||||||
|
| `.trae/memory/` | 长期沉淀的关键决策与经验 | 按需追加 |
|
||||||
|
| `.trae/rules/项目/04-即时状态记忆.md` | 当前快照(进度、临时决策) | 高频更新 |
|
||||||
|
|
||||||
|
- 三者不要重复存放同一份信息
|
||||||
|
- `docs/AI沟通记录/` 偏向**对话过程的完整存档**
|
||||||
|
- `.trae/memory/` 偏向**长期沉淀的提炼结论**
|
||||||
|
|
||||||
|
## 六、检查清单
|
||||||
|
|
||||||
|
1. [ ] 新建沟通记录时,序号是否为当前最大值 +1?
|
||||||
|
2. [ ] 文件名是否为 `NN-主题摘要.md` 格式?
|
||||||
|
3. [ ] 记录内容是否包含日期、参与者、讨论主题等必要字段?
|
||||||
|
4. [ ] 是否与 `.trae/memory/` 和即时状态记忆避免重复存储?
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
---
|
|
||||||
alwaysApply: false
|
|
||||||
---
|
|
||||||
# 研发工单拆分输出规范(必须遵守)
|
|
||||||
|
|
||||||
> 适用范围:本规则属于 **全局规则**(跨项目通用)。
|
|
||||||
|
|
||||||
> ⚠️ 术语澄清:本规范中的「研发工单(Dev Work Item)」专指智能体 / 开发者执行的**编码工作项**,与 Hua.Todo 项目业务领域中的「Todo 待办项」是两个完全不同的概念。
|
|
||||||
> 详见 [05-研发工单规则.md](./05-研发工单规则.md)。
|
|
||||||
> 凡涉及编码侧拆分时,**必须使用「研发工单」或「工单」**,禁止使用「任务」二字以避免与 Todo 待办项混淆。
|
|
||||||
|
|
||||||
## 适用时机
|
|
||||||
|
|
||||||
- 当需求需要先通读项目/产品/技术文档再开始实现时,必须先输出研发工单拆分文档,再开始写代码或改配置。
|
|
||||||
|
|
||||||
## 输出要求
|
|
||||||
|
|
||||||
- **先读完所有相关文档**:包括但不限于 `docs/`、`docs/project/` 下与本次需求相关的内容。
|
|
||||||
- **先写工单拆分,再动手实现**:研发工单拆分产出是后续执行的入口与对齐依据。
|
|
||||||
- **新增一个专属文件夹**:在 `docs/project` 下新建一个文件夹存放本次研发工单拆分文档。
|
|
||||||
- 文件夹命名建议:`研发工单-<主题>-<日期或版本>`(保持可检索、避免与既有文档冲突)。
|
|
||||||
- **可并行的工单要拆成不同 md**:能同步执行(相互无依赖/弱依赖)的工单,必须拆到不同的 Markdown 文件中,便于并行推进与分工。
|
|
||||||
- **文件必须带序号**:同一文件夹下的 md 文件按执行顺序编号,序号从小到大。
|
|
||||||
- 文件名建议:`01-xxx.md`、`02-xxx.md`、`03-xxx.md`。
|
|
||||||
- **每个研发工单文件至少包含**:
|
|
||||||
- 目标/范围(做什么、不做什么)
|
|
||||||
- 前置条件(依赖哪些结论/接口/文档)
|
|
||||||
- 验收标准(怎么判断完成,包含可执行的验证点)
|
|
||||||
- 风险与回滚(如有)
|
|
||||||
- **子工单完成后的标记要求**:
|
|
||||||
- 当任一子工单(例如 `01-*`/`02-*`/`03-*`)完成实现后,必须在对应版本的 `00-工单总览.md` 中同步标注"已完成"。
|
|
||||||
- 同时必须维护一张"待验证表"(可用 Markdown 表格),对每个子工单给出"待验证/已验证"状态,避免实现完成但验收未闭环。
|
|
||||||
- **并行冲突规避要求**:当工单会被分发到多个 solo 窗口并行推进时,每个研发工单文件必须额外包含:
|
|
||||||
- 触碰文件清单(Touch List,精确到文件)
|
|
||||||
- 共享文件策略(哪些是共享文件、唯一 Writer 是谁、如何与集成窗口对接)
|
|
||||||
- 编译绿线策略(如何确保阶段性交付不破坏编译)
|
|
||||||
|
|
||||||
## 推荐结构(模板)
|
|
||||||
|
|
||||||
- `00-工单总览.md`
|
|
||||||
- 背景与目标
|
|
||||||
- 关键决策与约束
|
|
||||||
- 并行分组说明(哪些文件可同步做)
|
|
||||||
- 待验证表(每个子工单的"待验证/已验证"状态)
|
|
||||||
- `01-<并行工单A>.md`
|
|
||||||
- `02-<并行工单B>.md`
|
|
||||||
- `03-<串行工单C>.md`
|
|
||||||
|
|
||||||
## 最小检查清单
|
|
||||||
|
|
||||||
1. [ ] 是否确认已阅读完所有相关文档?
|
|
||||||
2. [ ] 是否在 `docs/project` 下新建了本次专属文件夹(命名以"研发工单-"开头)?
|
|
||||||
3. [ ] 是否产出 `00-工单总览.md`(或等价总览文件)?
|
|
||||||
4. [ ] 是否将可并行工单拆分为不同 md 文件?
|
|
||||||
5. [ ] 是否所有 md 文件都带有连续序号?
|
|
||||||
6. [ ] 并行工单是否为每个研发工单文件补充了 Touch List/共享文件策略/编译绿线策略?
|
|
||||||
7. [ ] 子工单完成后,是否在对应版本的 `00-工单总览.md` 标注"已完成",并在"待验证表"里更新状态?
|
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# 代码实现与测试先行规范(必须遵守)
|
||||||
|
|
||||||
|
> 全局规则。规定何时必须写测试 + 怎么写(ATDD:验收测试驱动开发)。与 [04-研发工单全流程规范.md](./04-研发工单全流程规范.md) 配套:04 管工单拆分,本规范管实现与验收。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、触发条件(什么时候必须写测试)
|
||||||
|
|
||||||
|
用户表述满足以下任一条件时,**必须**执行测试先行:
|
||||||
|
|
||||||
|
| 触发条件 | 示例 |
|
||||||
|
|---|---|
|
||||||
|
| 明确指出 bug("修正"/"修复"/"不应该"/"404") | "登录按钮点不了""/sync 404""数据丢了" |
|
||||||
|
| 要求新增功能 | "加一个导出按钮""新增 XX 端点" |
|
||||||
|
| 要求修改现有行为 | "把优先级改成默认高""去掉确认弹窗" |
|
||||||
|
|
||||||
|
**豁免**(无需测试先行,但改动后必须跑已有测试):
|
||||||
|
|
||||||
|
| 场景 | 示例 |
|
||||||
|
|---|---|
|
||||||
|
| 纯配置 | `appsettings.json` 默认值、连接字符串 |
|
||||||
|
| 纯格式化/注释 | ESLint 自动修复、XML doc 补充 |
|
||||||
|
| 依赖升级无 API 变更 | NuGet/npm 补丁版本 |
|
||||||
|
| 文档/规则更新 | 新增 `.trae/rules/` 文件 |
|
||||||
|
|
||||||
|
> 判定原则:改动出错用户能感知 → 不可豁免。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、强制流程
|
||||||
|
|
||||||
|
1. **提炼验收点**:从用户描述提取 Given-When-Then 验收场景(主:分支:异常 ≈ 1:2:2)
|
||||||
|
2. **RED**:写验收测试,实跑确认失败(失败原因 = 被测功能缺失,非 setup 错误)
|
||||||
|
3. **GREEN**:写最小量代码使测试通过
|
||||||
|
4. **REFACTOR**:重构优化,重跑测试仍 GREEN
|
||||||
|
|
||||||
|
**不得跳过任何步骤。**
|
||||||
|
|
||||||
|
## 三、验收测试用例规范
|
||||||
|
|
||||||
|
格式(Given-When-Then):
|
||||||
|
|
||||||
|
```
|
||||||
|
Given [前置条件]
|
||||||
|
When [操作]
|
||||||
|
Then [预期结果]
|
||||||
|
```
|
||||||
|
|
||||||
|
示例:`Given 本地 3 个未完成待办 / When POST /api/task 创建"写周报" / Then 返回 201 且共 4 条`
|
||||||
|
|
||||||
|
要求:
|
||||||
|
- 命名体现场景:`CreateTask_EmptyTitle_Returns400`
|
||||||
|
- 一个测试只验证一个行为点
|
||||||
|
- 断言可量化
|
||||||
|
- 至少覆盖集成测试层(不满足于纯单元测试)
|
||||||
|
|
||||||
|
## 四、工单完成判定(全部满足才标记"已完成")
|
||||||
|
|
||||||
|
1. 所有验收测试通过
|
||||||
|
2. 原有测试无回归
|
||||||
|
3. [04-即时状态记忆.md](../项目/04-即时状态记忆.md) 或 `00-工单总览.md` 待验证表标注"已验证"
|
||||||
|
4. 按 [03-文档同步规范.md](./03-文档同步规范.md) 同步文档
|
||||||
|
|
||||||
|
## 五、Git Checkpoint 提交(推荐)
|
||||||
|
|
||||||
|
| 阶段 | 提交信息 |
|
||||||
|
|---|---|
|
||||||
|
| RED | `test(workitem): 为 <工单> 添加失败的验收测试` |
|
||||||
|
| GREEN | `feat(workitem): 实现 <工单> 使验收测试通过` |
|
||||||
|
| REFACTOR | `refactor(workitem): 重构 <模块> 保持测试通过` |
|
||||||
|
|
||||||
|
流程完成前不得 squash。提交信息禁用歧义"任务"二字。
|
||||||
|
|
||||||
|
## 六、检查清单
|
||||||
|
|
||||||
|
- [ ] 触发条件满足?豁免类是否确认无行为变更?
|
||||||
|
- [ ] 提炼了 Given-When-Then 验收场景?
|
||||||
|
- [ ] RED 实跑确认失败,失败原因 = 功能缺失?
|
||||||
|
- [ ] GREEN 最小代码通过测试?
|
||||||
|
- [ ] REFACTOR 后测试仍 GREEN?
|
||||||
|
- [ ] 场景覆盖主/分支/异常?
|
||||||
|
- [ ] 原有测试无回归?
|
||||||
|
- [ ] 工单完成四条件全部满足?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**关联**:[04-研发工单全流程规范.md](./04-研发工单全流程规范.md)、[03-文档同步规范.md](./03-文档同步规范.md)、[05-并行窗口冲突规约.md](./05-并行窗口冲突规约.md)
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
> 适用范围:本规则属于 **项目规则**(仅 Hua.Todo 项目生效)。
|
> 适用范围:本规则属于 **项目规则**(仅 Hua.Todo 项目生效)。
|
||||||
>
|
>
|
||||||
> 描述当前仓库的项目划分、依赖方向、运行模式与跨平台策略,供智能体在做改动时快速对齐架构边界。
|
> 描述当前仓库的项目划分、依赖方向、运行模式与跨平台策略,供智能体在做改动时快速对齐架构边界。
|
||||||
> 完整设计参见 [docs/manual/01-技术设计文档.md](../../../docs/manual/01-技术设计文档.md) 与 [docs/manual/03-技术栈与模块.md](../../../docs/manual/03-技术栈与模块.md)。
|
> 完整设计参见 [docs/manual/05-技术栈与项目结构.md](../../../docs/manual/05-技术栈与项目结构.md) 与 [docs/manual/07-技术架构设计.md](../../../docs/manual/07-技术架构设计.md)。
|
||||||
|
|
||||||
## 一、项目清单(src/)
|
## 一、项目清单(src/)
|
||||||
|
|
||||||
@@ -73,3 +73,32 @@ Hua.Todo.Web (无 .NET 依赖;通过 HTTP/同源调用上述任一宿主)
|
|||||||
- Inno Setup 安装包:`src/Hua.Todo.Maui/setup.iss`、`src/Hua.Todo.Avalonia/setup.iss`
|
- Inno Setup 安装包:`src/Hua.Todo.Maui/setup.iss`、`src/Hua.Todo.Avalonia/setup.iss`
|
||||||
- Linux:`publish-linux.ps1` 产出 `.tar.gz`;`pack/linux/` 含 Flatpak 基础结构
|
- Linux:`publish-linux.ps1` 产出 `.tar.gz`;`pack/linux/` 含 Flatpak 基础结构
|
||||||
- 详见 [docs/project/研发工单-v1.2.0/02.1-版本统一与打包方案.md](../../../docs/project/研发工单-v1.2.0/02.1-版本统一与打包方案.md)
|
- 详见 [docs/project/研发工单-v1.2.0/02.1-版本统一与打包方案.md](../../../docs/project/研发工单-v1.2.0/02.1-版本统一与打包方案.md)
|
||||||
|
|
||||||
|
## 七、测试项目架构
|
||||||
|
|
||||||
|
### 7.1 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
test/ ← 项目根目录下的顶层测试目录
|
||||||
|
├── Hua.Todo.Host.Tests/ ← 服务端/Application 层集成测试
|
||||||
|
│ ├── CloudSync/ ← 云同步模块
|
||||||
|
│ ├── Meeting/ ← 会议模块
|
||||||
|
│ └── Attachments/ ← 附件模块
|
||||||
|
├── Hua.Todo.Maui.Tests/ ← MAUI 平台测试(骨架)
|
||||||
|
└── Hua.Todo.Avalonia.Tests/ ← Avalonia 平台测试(骨架)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 分层规则(严禁跨层)
|
||||||
|
|
||||||
|
| 测试项目 | 可引用的被测项目 | 不得引用 |
|
||||||
|
|---|---|---|
|
||||||
|
| `Hua.Todo.Host.Tests` | `Hua.Todo.Host` / `Application` / `Core` | MAUI / Avalonia 宿主 |
|
||||||
|
| `Hua.Todo.Maui.Tests` | `Hua.Todo.Maui` / `Application` / `Core` | Avalonia 宿主 |
|
||||||
|
| `Hua.Todo.Avalonia.Tests` | `Hua.Todo.Avalonia` / `Application` / `Core` | MAUI 宿主 |
|
||||||
|
|
||||||
|
### 7.3 技术栈与规范
|
||||||
|
|
||||||
|
- 框架:xUnit 2.9+,SQLite In-Memory 模拟 DB
|
||||||
|
- 测试层次:直接测 Application 服务层(DI 测试),必要时用 `WebApplicationFactory`
|
||||||
|
- 命名:类 `{被测类}Tests`、方法 `{方法名}_{场景}_{预期结果}`、命名空间 `Hua.Todo.Host.Tests.{模块名}`
|
||||||
|
- 文件组织:每个模块新建子目录,模块级共用测试放根目录
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
> 适用范围:本规则属于 **项目规则**(仅 Hua.Todo 项目生效)。
|
> 适用范围:本规则属于 **项目规则**(仅 Hua.Todo 项目生效)。
|
||||||
>
|
>
|
||||||
> 本文档规定 **业务实体**(Todo 待办项相关)与 **编码工作项**(研发工单)在代码、文档、提交信息中的命名边界。
|
> 本文档规定 **业务实体**(Todo 待办项相关)与 **编码工作项**(研发工单)在代码、文档、提交信息中的命名边界。
|
||||||
> 全局术语规则参见 [.trae/rules/全局/05-研发工单规则.md](../全局/05-研发工单规则.md)。
|
> 全局术语规则参见 [.trae/rules/全局/04-研发工单全流程规范.md](../全局/04-研发工单全流程规范.md)。
|
||||||
|
|
||||||
## 一、术语对照(核心)
|
## 一、术语对照(核心)
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
### 2.3 HTTP API 路由
|
### 2.3 HTTP API 路由
|
||||||
- 本地动态 API:`/api/task`、`/api/task/{parentTaskId}/subtasks`(由 `Hua.Todo.Application/DynamicApi` 自动暴露)
|
- 本地动态 API:`/api/task`、`/api/task/{parentTaskId}/subtasks`(由 `Hua.Todo.Application/DynamicApi` 自动暴露)
|
||||||
- 云端:`GET /tasks`、`POST /sync`、`POST /cloud-sync/probe`
|
- 云端:`GET /tasks`、`POST /sync`、`POST /cloud-sync/probe`
|
||||||
- 见 [docs/manual/01-技术设计文档.md](../../../docs/manual/01-技术设计文档.md)、[docs/project/研发工单-v1.2.0/04-CloudSync-服务端基础能力.md](../../../docs/project/研发工单-v1.2.0/04-CloudSync-服务端基础能力.md)
|
- 见 [docs/manual/05-技术栈与项目结构.md](../../../docs/manual/05-技术栈与项目结构.md)、[docs/project/研发工单-v1.2.0/04-CloudSync-服务端基础能力.md](../../../docs/project/研发工单-v1.2.0/04-CloudSync-服务端基础能力.md)
|
||||||
|
|
||||||
### 2.4 数据库表
|
### 2.4 数据库表
|
||||||
- `Tasks`、`Users`、`UserSessions`、`SecurityPolicies`、`AuditLogs`
|
- `Tasks`、`Users`、`UserSessions`、`SecurityPolicies`、`AuditLogs`
|
||||||
|
|||||||
@@ -94,6 +94,6 @@
|
|||||||
|
|
||||||
1. [ ] 是否新增了 EF Core 迁移(而非手改快照)?
|
1. [ ] 是否新增了 EF Core 迁移(而非手改快照)?
|
||||||
2. [ ] 迁移名称是否以 `AddXxx`/`UpdateXxx`/`RemoveXxx` 开头?
|
2. [ ] 迁移名称是否以 `AddXxx`/`UpdateXxx`/`RemoveXxx` 开头?
|
||||||
3. [ ] 是否在 [docs/manual/01-技术设计文档.md](../../../docs/manual/01-技术设计文档.md) 中同步更新数据模型描述?
|
3. [ ] 是否在 [docs/manual/07-技术架构设计.md](../../../docs/manual/07-技术架构设计.md) 中同步更新数据模型描述?
|
||||||
4. [ ] 是否在 [docs/manual/06-版本记录.md](../../../docs/manual/06-版本记录.md) 中追加非琐碎变更条目?
|
4. [ ] 是否在 [docs/manual/03-版本记录.md](../../../docs/manual/03-版本记录.md) 中追加非琐碎变更条目?
|
||||||
5. [ ] 是否在嵌入式宿主上验证了启动时 Migrate 不报错?
|
5. [ ] 是否在嵌入式宿主上验证了启动时 Migrate 不报错?
|
||||||
|
|||||||
+100
-5
@@ -11,8 +11,9 @@
|
|||||||
|
|
||||||
## 一、当前活跃版本
|
## 一、当前活跃版本
|
||||||
|
|
||||||
- **进行中版本**:v1.2.0
|
- **进行中版本**:v1.2.0(收尾中)、v1.3.0(规划中)
|
||||||
- **研发工单总览**:[docs/project/研发工单-v1.2.0/00-工单总览.md](../../../docs/project/研发工单-v1.2.0/00-工单总览.md)
|
- **v1.2.0 研发工单总览**:[docs/project/研发工单-v1.2.0/00-工单总览.md](../../../docs/project/研发工单-v1.2.0/00-工单总览.md)
|
||||||
|
- **v1.3.0 研发工单总览**:[docs/project/研发工单-v1.3.0/00-工单总览.md](../../../docs/project/研发工单-v1.3.0/00-工单总览.md)
|
||||||
- **PRD**:[docs/project/产品需求文档-1.2.0.md](../../../docs/project/产品需求文档-1.2.0.md)
|
- **PRD**:[docs/project/产品需求文档-1.2.0.md](../../../docs/project/产品需求文档-1.2.0.md)
|
||||||
|
|
||||||
## 二、v1.2.0 工单状态快照
|
## 二、v1.2.0 工单状态快照
|
||||||
@@ -31,14 +32,27 @@
|
|||||||
| 08 - cloud_sync 重构 | 已设计 | 待实现 | "同源 Host"方案,Vite proxy 补 `/auth` `/tasks` `/sync` `/security` `/cloud-sync` |
|
| 08 - cloud_sync 重构 | 已设计 | 待实现 | "同源 Host"方案,Vite proxy 补 `/auth` `/tasks` `/sync` `/security` `/cloud-sync` |
|
||||||
| 09 - CloudSync 同步策略改进 | 已实现 | 待验证 | TaskEntity 继承 ABP 基类;软删除修复(SaveChangesAsync);前端类型和 cloudSync.ts 已更新;新增 guid.ts |
|
| 09 - CloudSync 同步策略改进 | 已实现 | 待验证 | TaskEntity 继承 ABP 基类;软删除修复(SaveChangesAsync);前端类型和 cloudSync.ts 已更新;新增 guid.ts |
|
||||||
|
|
||||||
## 三、关键临时决策
|
## 三、v1.3.0 工单状态快照
|
||||||
|
|
||||||
|
| 子工单 | 实现状态 | 验证状态 | 简要说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 01 - HTTP 服务转换 MCP 服务 | 进行中 | 待验证 | 将现有 HTTP API 映射为 MCP 工具描述符 |
|
||||||
|
| 02 - 语音控制与 AI 辅助 | 已完成(核心基础设施) | 待验证 | Core 接口(IVoiceInputService/IVoiceOutputService/IVoiceIntentParser)、VoiceIntent 枚举与 DTO、LLM 客户端(LlmClientService)、双策略意图解析器(LlmIntentParser + RuleIntentParser + HybridVoiceIntentParser)、VoiceCommandExecutor、AiBreakdownService、VoiceService(Dynamic API)、DI 注册;26 个单元测试通过;平台 STT/TTS 实现待补 |
|
||||||
|
| 03 - 会议任务拆分 | 待开始 | 待验证 | 会议类型入口 + 录音/文字输入 + AI 拆分建议 + 确认批量创建 |
|
||||||
|
| 03-01 - 会议数据模型与 API | 待开始 | 待验证 | TaskType 枚举、MeetingNotes/AudioDuration 字段、MeetingController |
|
||||||
|
| 03-02 - 音频录制与转写 | 待开始 | 待验证 | 前端 MediaRecorder 录音 + 后端 STT 转写 |
|
||||||
|
| 03-03 - AI 任务拆分服务 | 待开始 | 待验证 | 会议专用 LLM prompt + 批量创建子任务 |
|
||||||
|
| 03-04 | 任务建议与确认 UI | 已完成 | 待验证 | MeetingBreakdownDialog.vue + meeting.ts 拆分/确认 API;待集成到 TaskItem |
|
||||||
|
| 04 | 富文本描述、附件与外部链接 | 已完成 | 待验证 | Description 字段 + AttachmentEntity 模型 + 附件 CRUD API + 外部链接 + 平台文件打开器(Maui/Avalonia)+ 前端类型和 API 模块完善 + EF 迁移 + 19 个单元测试通过 |
|
||||||
|
|
||||||
|
## 四、关键临时决策
|
||||||
|
|
||||||
- **MAUI 端不暴露云同步端点**:`MauiProgram.cs` 仅注册 `AddApplicationServices()`,不调 `AddCloudSyncServer()`。云同步端点只在 `Hua.Todo.Host` 暴露。
|
- **MAUI 端不暴露云同步端点**:`MauiProgram.cs` 仅注册 `AddApplicationServices()`,不调 `AddCloudSyncServer()`。云同步端点只在 `Hua.Todo.Host` 暴露。
|
||||||
- **本地用户 ID 固定为 `"local"`**:嵌入式模式下 `Tasks.UserId = TodoUserIds.LocalUserId`,与云端用户隔离逻辑共存而不冲突。
|
- **本地用户 ID 固定为 `"local"`**:嵌入式模式下 `Tasks.UserId = TodoUserIds.LocalUserId`,与云端用户隔离逻辑共存而不冲突。
|
||||||
- **SQLite WAL 模式**:嵌入式宿主启动时强制开启 WAL,降低锁冲突。
|
- **SQLite WAL 模式**:嵌入式宿主启动时强制开启 WAL,降低锁冲突。
|
||||||
- **数据库路径**:默认 `LocalApplicationData/Hua.Todo/Hua.Todo.db`(避免安装目录无写权限);Host 模式使用 `src/Hua.Todo.Host/Hua.Todo.db`(开发/测试)。
|
- **数据库路径**:默认 `LocalApplicationData/Hua.Todo/Hua.Todo.db`(避免安装目录无写权限);Host 模式使用 `src/Hua.Todo.Host/Hua.Todo.db`(开发/测试)。
|
||||||
|
|
||||||
## 四、已知未完结事项 / 待办
|
## 五、已知未完结事项 / 待办
|
||||||
|
|
||||||
- [ ] 06 客户端"内存模式"在 `allowPersist=false` 时的端到端落盘清理(含 token、同步队列)尚未充分验证
|
- [ ] 06 客户端"内存模式"在 `allowPersist=false` 时的端到端落盘清理(含 token、同步队列)尚未充分验证
|
||||||
- [ ] 06.1 设计中的 Admin 管理后台前端(位于 `Hua.Todo.Host/wwwroot/admin/`)当前仅有 `index.html` 占位,需 Vue 3 + Vite 实现
|
- [ ] 06.1 设计中的 Admin 管理后台前端(位于 `Hua.Todo.Host/wwwroot/admin/`)当前仅有 `index.html` 占位,需 Vue 3 + Vite 实现
|
||||||
@@ -46,7 +60,36 @@
|
|||||||
- [ ] Linux Flatpak/AppImage 自包含产物在干净环境的实测验证(v1.2.0 验收 Linux 部分仍为"待验证")
|
- [ ] Linux Flatpak/AppImage 自包含产物在干净环境的实测验证(v1.2.0 验收 Linux 部分仍为"待验证")
|
||||||
- [x] CloudSync UNIQUE 约束修复(2026-06-14):修复了 `existingTasks` 查询在事务外导致并发重同步时 `T_Tasks.Id` UNIQUE 约束冲突;新增 7 个测试(含 5 个 SQLite 集成测试)
|
- [x] CloudSync UNIQUE 约束修复(2026-06-14):修复了 `existingTasks` 查询在事务外导致并发重同步时 `T_Tasks.Id` UNIQUE 约束冲突;新增 7 个测试(含 5 个 SQLite 集成测试)
|
||||||
|
|
||||||
## 五、最近一次重大重构(如有)
|
- [x] v1.3.0 工单01 - MCP 服务转换已完成(2026-06-16):DynamicMcpToolExtensions 自动扫描所有 IDynamicApiService 接口并生成 MCP 工具;当前覆盖 ITaskService(9 个工具)+ IVoiceService(4 个工具)= 13 个 MCP 工具;新增 4 个测试(描述验证、InputSchema 验证、服务调用、工具调用端到端),共计 17 个测试全部通过;CloudSync 服务因未实现 IDynamicApiService 暂未覆盖,记录为已知缺口
|
||||||
|
|
||||||
|
- [x] MAUI 平台编译修复(2026-06-16):(1) Application.csproj 非 net10.0 目标新增排除 CloudSync/**/*.cs(其依赖 Microsoft.AspNetCore.App);(2) 新增 Microsoft.Extensions.Http 包引用(Voice 服务使用 AddHttpClient);(3) MobileEmbeddedWebServerService.cs Android 平台 int→Guid 适配(TaskEntity ABP 重构遗留);验证通过:MAUI Android/Windows + Host 均 0 错误
|
||||||
|
|
||||||
|
- [x] 测试项目重构(2026-06-17):原 src/Hua.Todo.Tests 拆分为三个宿主对应测试项目,放入 src/test/ 目录:Hua.Todo.Host.Tests(后端服务测试,144 个用例全过)、Hua.Todo.Maui.Tests(骨架)、Hua.Todo.Avalonia.Tests(骨架);更新 .slnx 与 docs 引用
|
||||||
|
|
||||||
|
- [x] EF Core 迁移合并(2026-06-17):10 个历史迁移合并为单一 `20260616203619_InitialCreate`,Migrations 目录从 21 个文件减至 3 个;DatabaseMigrationTests 断言同步更新;157 个测试全部通过
|
||||||
|
|
||||||
|
- [x] MAUI Windows 云同步代理功能修复(2026-06-17):
|
||||||
|
- **问题**:`Hua.Todo.Application.csproj` 第 14-18 行在非 net10.0 目标上排除了整个 CloudSync 目录,导致 MAUI Windows 编译时 CloudSync 代码不存在
|
||||||
|
- **影响**:MAUI Windows 的 `EmbeddedWebServerService` 缺少云同步代理支持(`AddCloudSyncProxy()`、`UseCloudSyncProxy()`、`MapCloudSyncProxySettings()`),前端云同步设置弹窗无法工作
|
||||||
|
- **修复**:
|
||||||
|
1. 从 Application.csproj 移除 CloudSync 的自动排除(保留 SkipCloudSync=true 手动开关)
|
||||||
|
2. MAUI Windows `EmbeddedWebServerService` 添加云同步代理支持(参考 Avalonia 实现)
|
||||||
|
3. `WebServerSettings` 新增 `CloudSyncUrl` 属性
|
||||||
|
- **验证**:MAUI Windows + Host + 152 个测试 全部通过
|
||||||
|
|
||||||
|
- [x] DynamicApi/Mcp/CloudSync ASP.NET Core 依赖隔离(2026-06-17):
|
||||||
|
- **问题**:Application.csproj 在非 net10.0 目标上整体排除 DynamicApi/**/*.cs 和 Mcp/**/*.cs,并用桩文件(Compatibility/DynamicApiStubs.cs)替代;CloudSync 通过 SkipCloudSync=true 手动排除;核心属性类(HttpAttributes、RemoteServiceAttribute 等)在移动平台不可用
|
||||||
|
- **修复**:
|
||||||
|
1. 移除 csproj 中 DynamicApi/Mcp 的整体排除、桩文件排除、SkipCloudSync 开关
|
||||||
|
2. 新增 `ASPNETCORE` 编译符号(仅 net10.0 目标定义)
|
||||||
|
3. DynamicApi 核心属性文件(HttpAttributes.cs、ParameterBindingAttributes.cs、RemoteServiceAttribute.cs)全平台编译
|
||||||
|
4. DynamicApi/Mcp/CloudSync 中 13 个 ASP.NET Core 依赖文件用 `#if ASPNETCORE` 包裹
|
||||||
|
5. ClaimsPrincipalExtensions.cs 用 `FindFirst()?.Value` 替代 ASP.NET Core 的 `FindFirstValue` 扩展方法
|
||||||
|
6. 删除 Compatibility/DynamicApiStubs.cs 桩文件
|
||||||
|
7. 清理测试项目中的 SkipCloudSync 排除
|
||||||
|
- **验证**:Application(net10.0 + net10.0-android) + Host + MAUI(Windows + Android) 全部 0 错误;152 个测试全部通过
|
||||||
|
|
||||||
|
## 六、最近一次重大重构(如有)
|
||||||
|
|
||||||
- **术语统一与目录中文化**(2026-06):
|
- **术语统一与目录中文化**(2026-06):
|
||||||
- `.trae/rules/` 全部中文文件名 + 拆分为 `全局/` 和 `项目/` 两个子目录
|
- `.trae/rules/` 全部中文文件名 + 拆分为 `全局/` 和 `项目/` 两个子目录
|
||||||
@@ -56,6 +99,58 @@
|
|||||||
- **`.trae` 子目录文件序号化**(2026-06):
|
- **`.trae` 子目录文件序号化**(2026-06):
|
||||||
- `.trae/rules/全局/`、`.trae/rules/项目/`、`.trae/coordination/`、`.trae/memory/` 下所有文件加 `NN-` 序号前缀
|
- `.trae/rules/全局/`、`.trae/rules/项目/`、`.trae/coordination/`、`.trae/memory/` 下所有文件加 `NN-` 序号前缀
|
||||||
- `.trae/索引.md` 不带序号(入口文件)
|
- `.trae/索引.md` 不带序号(入口文件)
|
||||||
|
- **`docs/manual/` 指南重组**(2026-06-16):
|
||||||
|
- 新增 `00-目录与导读.md` 双入口导航(普通用户 / 开发者)
|
||||||
|
- 拆分用户面与开发者面:01-02 为用户安装使用;05-10 为开发者架构/构建/规范
|
||||||
|
- 合并重叠与过时内容,精简用户文档篇幅
|
||||||
|
- 同步更新 README.md 与 `.trae/rules/项目/` 中的交叉引用
|
||||||
|
- **全局 Serilog 文件日志**(2026-06-16):
|
||||||
|
- Application 层新增 `LoggingConfiguration.cs` 统一日志配置(Console + 按天滚动文件)
|
||||||
|
- Host / MAUI / Avalonia 宿主层全部接入 Serilog,日志目录 `logs/`(Host)或 `{LocalApplicationData}/Hua.Todo/logs/`(客户端)
|
||||||
|
- 修复 `DynamicApiMiddleware` 及 Application 层 8 个服务类的 catch 块(原静默吞异常→记日志)
|
||||||
|
- 修复 MAUI 端 2 个 WebServer 文件 + Avalonia 端 `App.axaml.cs` / `EmbeddedWebServerService` 的 Console/Debug→Serilog
|
||||||
|
- 修复 `CloudTaskSyncService` SQL UNIQUE 约束冲突重试日志
|
||||||
|
- 144 个测试全部通过
|
||||||
|
|
||||||
|
- **Host 云同步端点补齐 + 测试目录迁移 + 规则新增**(2026-06-17):
|
||||||
|
- **问题**:`Hua.Todo.Host/Program.cs` 未调用 `AddCloudSyncServer()` 和 `MapCloudSyncEndpoints()`,导致 `/auth/*` `/tasks/*` `/sync/*` `/security/*` `/cloud-sync/*` 全部 404
|
||||||
|
- **修复**:Program.cs 新增 `AddCloudSyncServer()` + `MapCloudSyncEndpoints()` + `MapCloudSyncProxySettings()`;`vite.config.ts` 新增 6 条云同步路径代理
|
||||||
|
- **测试**:`CloudSyncEndpointRegistrationTests.cs` 新增 5 个 DI 注册测试(完整 AddApplicationServices + AddCloudSyncServer 链路验证),157→157 全部通过
|
||||||
|
- **目录迁移**:`src/test/` → `test/`(上移一级),更新 3 个 `.csproj` 的 `<ProjectReference>` 路径 + `.slnx`
|
||||||
|
- **规则新增**:
|
||||||
|
- `.trae/rules/全局/08-修正与新功能自动测试门禁.md`:用户明确要求修正 bug 或新功能时自动触发测试先行
|
||||||
|
- `.trae/rules/项目/06-测试项目分层规范.md`:测试目录结构、分层规则(禁止跨层)、技术栈与命名规范
|
||||||
|
- **前端默认值**:`CloudSyncSettingsDialog` 默认地址 `http://localhost:5173`、默认账号 `admin`/`123456`
|
||||||
|
- **前端云同步入口**:`TaskList.vue` 新增登录/登出/同步按钮 + 用户信息 + 服务器地址显示;`App.vue` 修复 `CloudSyncSettingsDialog` 缺少 import 导致弹窗不打开
|
||||||
|
|
||||||
|
- **前端大量补齐**(2026-06-17 v1.3.0):
|
||||||
|
- 新增 7 个文件(AttachmentList/LinkInputDialog/useAttachments/voice.ts/useVoiceInput/规则2个)
|
||||||
|
- 修改 5 个文件(TaskEditDialog/TaskItem/TaskList/tasks.ts/localStorageService)
|
||||||
|
- 删除 1 个无用文件(HelloWorld.vue)
|
||||||
|
- 编译 0 错误,105 个模块构建成功
|
||||||
|
|
||||||
|
- **CloudSync 端点 DynamicApi 化**(2026-06-17):
|
||||||
|
- **问题**:`CloudSyncEndpointExtensions.MapCloudSyncEndpoints()` 手动映射 16 个端点(auth/tasks/sync/security/admin/probe),与项目中 ITaskService 等通过 IDynamicApiService 自动暴露的模式不一致
|
||||||
|
- **修复**:
|
||||||
|
1. 新增 `DynamicApiRouteAttribute`(服务级路由前缀覆盖)和 `RequirePermissionAttribute`(权限检查)
|
||||||
|
2. 扩展 `DynamicApiMiddleware`:支持 `DynamicApiRoute` 自定义路由前缀、`AllowAnonymous` / `RequirePermission` 权限检查、统一错误响应(401/403)
|
||||||
|
3. 创建 5 个 IDynamicApiService 接口:`ICloudAuthService`(`/api/auth`)、`ICloudTaskSyncService`(`/api/tasks`)、`ISecurityPolicyService`(`/api/security`)、`ICloudAdminService`(`/api/admin`)、`ICloudProbeService`(`/api/cloud-sync`)
|
||||||
|
4. 修改 5 个 Service 实现类:添加 `IHttpContextAccessor` 支持、接口方法(无 CancellationToken)、保留原有方法(Guided by CancellationToken)向后兼容
|
||||||
|
5. 移除 `MapCloudSyncEndpoints()` 入口和 16 个 handler 方法,`CloudSyncEndpointExtensions` 仅保留 `MapCloudSyncProxySettings()`
|
||||||
|
6. Host `Program.cs` 新增 `app.UseAuthentication()` 确保 SessionAuthenticationHandler 在 DynamicApi 前运行
|
||||||
|
7. `ResetPasswordRequest` 新增 `UserId` 字段(admin 重置密码路由扁平化)
|
||||||
|
8. `DynamicMcpToolExtensions` 新增 `RemoteServiceAttribute` 过滤(CloudSync 接口标记 `IsEnabled=false` 排除 MCP 暴露)
|
||||||
|
9. 4 个 Service 文件(CloudTaskSync/SecurityPolicy/CloudProbe/CloudAuth-接口方法)新增 `#if ASPNETCORE` 条件编译
|
||||||
|
- **验证**:Application(net10.0+android+ios+maccatalyst) + Host + Tests 全部编译通过,161 个测试全部通过
|
||||||
|
|
||||||
|
- [x] CloudSync Swagger + DynamicApi 路由修复(2026-06-17):
|
||||||
|
- **问题1**:所有 CloudSync 接口标记 `[RemoteService(IsEnabled=false)]`,`DynamicApiSwaggerDocumentFilter` 用 `IsEnabled` 过滤 → CloudSync 接口在 Swagger 中完全不可见(只有 DTO Schema 没有 Path)
|
||||||
|
- **问题2**:同样的 `IsEnabled=false` 导致 `DynamicApiMiddleware` 跳过 CloudSync 请求 → `/auth/*` `/tasks/*` `/security/*` `/admin/*` `/cloud-sync/*` 全部 404
|
||||||
|
- **修复**:
|
||||||
|
1. `DynamicApiSwaggerDocumentFilter.IsRemoteServiceEnabled` 改为检查 `IsMetadataEnabled`(与 `IsEnabled` 解耦:原 `IsEnabled=false` 的接口 Swagger 仍可见)
|
||||||
|
2. 移除 6 个 CloudSync 接口 + `ICloudSyncProxySettingsService` 的 `[RemoteService(IsEnabled=false)]`
|
||||||
|
3. MCP 工具改为命名空间过滤:`IsCloudSyncService(type)` 排除 `Hua.Todo.Application.CloudSync.*` 命名空间
|
||||||
|
- **验证**:编译 0 错误,161 个测试全部通过
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# 多入口功能同步规范(Hua.Todo 专属)
|
||||||
|
|
||||||
|
> 适用范围:本规则属于 **项目规则**(仅 Hua.Todo 项目生效)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、背景
|
||||||
|
|
||||||
|
Hua.Todo 存在两个功能入口:
|
||||||
|
|
||||||
|
| 入口 | 位置 | 方式 |
|
||||||
|
|---|---|---|
|
||||||
|
| **UI 入口** | WebView / 前端界面(Hua.Todo.Web) | 键盘/鼠标/触控交互 |
|
||||||
|
| **语音控制入口** | 语音指令(平台原生 STT + LLM 意图解析) | 语音输入 → 指令执行 |
|
||||||
|
|
||||||
|
v1.3.0 之前,功能开发只关注 UI 入口。v1.3.0 工单 02 落地后语音控制入口正式就绪,此后**所有新增功能必须在需求阶段同步确认两个入口的覆盖情况**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、核心规则
|
||||||
|
|
||||||
|
### 2.1 功能入口检查(强制)
|
||||||
|
|
||||||
|
每次新增功能(含新工单、新特性),智能体必须在需求讨论或工单拆分阶段执行以下检查:
|
||||||
|
|
||||||
|
| 检查项 | 说明 |
|
||||||
|
|---|---|
|
||||||
|
| **UI 入口** | 当前功能是否已有 UI 入口规划?入口在哪个页面/组件?交互方式是什么? |
|
||||||
|
| **语音控制入口** | 当前功能是否已有语音指令规划?对应哪个意图?参数是什么?LLM prompt 是否需要更新? |
|
||||||
|
|
||||||
|
### 2.2 缺失时为通知用户,不自行决策
|
||||||
|
|
||||||
|
- 任一入口缺失时,**智能体必须主动告知用户**,由用户决定:
|
||||||
|
1. 本次就做(补上缺失入口)
|
||||||
|
2. 本次不做(记录为已知缺口,后续版本补)
|
||||||
|
3. 不需要(该入口不适合此功能,如选项过多需可视化交互的操作)
|
||||||
|
- **智能体不得在用户未确认的情况下自行跳过或自行补充入口设计**
|
||||||
|
|
||||||
|
### 2.3 输出格式(检查表)
|
||||||
|
|
||||||
|
智能体在需求讨论阶段,必须输出以下检查表:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## 功能入口覆盖检查
|
||||||
|
|
||||||
|
| 入口 | 已规划 | 方案 |
|
||||||
|
|---|---|---|
|
||||||
|
| UI | ✅ / ❌ | [描述 UI 入口] |
|
||||||
|
| 语音 | ✅ / ❌ | [描述语音指令与意图] |
|
||||||
|
|
||||||
|
> 不可覆盖的入口说明:[如"语音控制暂不支持 XX 操作(原因)"]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、适用场景
|
||||||
|
|
||||||
|
### 3.1 必须检查的场景
|
||||||
|
|
||||||
|
- 新增业务功能(如新增双因素认证、新增标签系统)
|
||||||
|
- 新增 API 端点(如新增导出/导入)
|
||||||
|
- 新增 UI 页面/组件(如新增设置页、新增弹窗)
|
||||||
|
- 新工单拆分阶段
|
||||||
|
|
||||||
|
### 3.2 无需检查的场景
|
||||||
|
|
||||||
|
- 纯 bug 修复
|
||||||
|
- 纯性能优化(不改变用户可感知功能)
|
||||||
|
- 纯基础设施调整(如 CI/CD、打包脚本)
|
||||||
|
- 依赖升级
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、检查清单
|
||||||
|
|
||||||
|
1. [ ] 本次新增功能是否已确认 UI 入口?
|
||||||
|
2. [ ] 本次新增功能是否已确认语音控制入口?
|
||||||
|
3. [ ] 缺失入口是否已通知用户并记录决策?
|
||||||
|
4. [ ] LLM intent parser 的 prompt 是否需要同步更新(新增意图/新增参数)?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、与工单 02 的关系
|
||||||
|
|
||||||
|
本规范的语音入口覆盖检查表来源于 [docs/project/研发工单-v1.3.0/02-语音通话与语音控制.md](../../../docs/project/研发工单-v1.3.0/02-语音通话与语音控制.md) 的第七章"与其他工单的语音入口衔接"。
|
||||||
+14
-12
@@ -8,9 +8,9 @@
|
|||||||
>
|
>
|
||||||
> 文件命名约定:每个子目录内文件以 `NN-名称.md` 格式编号(两位数字),编号反映**阅读优先级 / 依赖顺序**。索引文件本身不带序号。
|
> 文件命名约定:每个子目录内文件以 `NN-名称.md` 格式编号(两位数字),编号反映**阅读优先级 / 依赖顺序**。索引文件本身不带序号。
|
||||||
>
|
>
|
||||||
> ⚠️ **新增文件必须遵守命名规则**:序号 = 当前目录最大值 + 1,不得跳号或重复,并需同步更新本索引文件中的对应章节。详见 [01-智能体记忆.md](./rules/全局/01-智能体记忆.md#文件命名规则强制)。
|
> ⚠️ **新增文件必须遵守命名规则**:序号 = 当前目录最大值 + 1,不得跳号或重复,并需同步更新本索引文件中的对应章节。详见 [01-记忆与存储规范.md](./rules/全局/01-记忆与存储规范.md#三文件命名规则强制)。
|
||||||
>
|
>
|
||||||
> ⚠️ **任务完成后清理义务**:研发工单验收完成后,必须清理 `coordination/` 中的临时记录行(保留文件与表头),并按需追加 `memory/` 的长期记忆条目。详见 [07-并行窗口冲突规约.md](./rules/全局/07-并行窗口冲突规约.md#任务验收后-coordination-清理) 与 [02-记忆存储规范.md](./rules/全局/02-记忆存储规范.md#memory-生命周期)。
|
> ⚠️ **任务完成后清理义务**:研发工单验收完成后,必须清理 `coordination/` 中的临时记录行(保留文件与表头),并按需追加 `memory/` 的长期记忆条目。详见 [05-并行窗口冲突规约.md](./rules/全局/05-并行窗口冲突规约.md#任务验收后-coordination-清理) 与 [01-记忆与存储规范.md](./rules/全局/01-记忆与存储规范.md#14-memory-生命周期)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -32,13 +32,13 @@
|
|||||||
|
|
||||||
| 文件 | 职责 |
|
| 文件 | 职责 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| [01-智能体记忆.md](./rules/全局/01-智能体记忆.md) | 智能体记忆/规范同步机制(**不含目录总览,由本索引文件承担**) |
|
| [01-记忆与存储规范.md](./rules/全局/01-记忆与存储规范.md) | 智能体记忆/存储/规范同步机制 & `.trae/memory/` 使用方式 & 文件命名规则(含序号速查表) |
|
||||||
| [02-记忆存储规范.md](./rules/全局/02-记忆存储规范.md) | `.trae/memory/` 的使用方式 |
|
| [02-注释规范.md](./rules/全局/02-注释规范.md) | C# / TypeScript / Vue 代码注释要求 |
|
||||||
| [03-注释规范.md](./rules/全局/03-注释规范.md) | C# / TypeScript / Vue 代码注释要求 |
|
| [03-文档同步规范.md](./rules/全局/03-文档同步规范.md) | 代码变更同步 README/docs 的硬性要求 |
|
||||||
| [04-文档同步规范.md](./rules/全局/04-文档同步规范.md) | 代码变更同步 README/docs 的硬性要求 |
|
| [04-研发工单全流程规范.md](./rules/全局/04-研发工单全流程规范.md) | 研发工单术语定义 + 拆分输出规范 + 新增工单约束(合并原 05/06/09) |
|
||||||
| [05-研发工单规则.md](./rules/全局/05-研发工单规则.md) | 研发工单术语与边界(编码工作项 vs Todo 待办项) |
|
| [05-并行窗口冲突规约.md](./rules/全局/05-并行窗口冲突规约.md) | 并行 solo 窗口下的 Touch List / Writer / 绿线策略 |
|
||||||
| [06-研发工单拆分规范.md](./rules/全局/06-研发工单拆分规范.md) | 工单拆分目录与文件结构 |
|
| [06-AI沟通记录规范.md](./rules/全局/06-AI沟通记录规范.md) | 用户与智能体沟通记录的存储目录、序号管理与内容规范 |
|
||||||
| [07-并行窗口冲突规约.md](./rules/全局/07-并行窗口冲突规约.md) | 并行 solo 窗口下的 Touch List / Writer / 绿线策略 |
|
| [07-代码实现与测试先行规范.md](./rules/全局/07-代码实现与测试先行规范.md) | 触发条件 + ATDD 工作流(RED→GREEN→REFACTOR)+ 验收测试用例规范 + 工单完成判定(合并原 07+08) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -46,10 +46,12 @@
|
|||||||
|
|
||||||
| 文件 | 职责 |
|
| 文件 | 职责 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| [01-项目架构.md](./rules/项目/01-项目架构.md) | src/ 五大项目划分、依赖方向、两种运行模式 |
|
| [01-项目架构.md](./rules/项目/01-项目架构.md) | src/ 项目清单、依赖方向、两种运行模式、跨平台原则、关键扩展点、测试项目架构 |
|
||||||
| [02-业务命名规范.md](./rules/项目/02-业务命名规范.md) | `Task`/`SubTask`/`TaskEntity` 等代码标识符与"研发工单"边界 |
|
| [02-业务命名规范.md](./rules/项目/02-业务命名规范.md) | `Task`/`SubTask`/`TaskEntity` 等代码标识符与"研发工单"边界 |
|
||||||
| [03-数据模型与迁移约束.md](./rules/项目/03-数据模型与迁移约束.md) | EF Core 实体清单、迁移历史、改 schema 纪律 |
|
| [03-数据模型与迁移约束.md](./rules/项目/03-数据模型与迁移约束.md) | EF Core 实体清单、迁移历史、改 schema 纪律 |
|
||||||
| [04-即时状态记忆.md](./rules/项目/04-即时状态记忆.md) | 当前活跃版本 / 工单状态快照 / 临时决策 / 未完结事项 |
|
| [04-即时状态记忆.md](./rules/项目/04-即时状态记忆.md) | 当前活跃版本 / 工单状态快照 / 临时决策 / 未完结事项 |
|
||||||
|
| [05-多入口功能同步规范.md](./rules/项目/05-多入口功能同步规范.md) | 新增功能时必须同步确认 UI 入口与语音控制入口的覆盖情况 |
|
||||||
|
| [06-测试项目分层规范.md](./rules/项目/06-测试项目分层规范.md) | 测试项目目录结构、分层规则(禁止跨层)、技术栈与命名规范 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -57,7 +59,7 @@
|
|||||||
|
|
||||||
- 用途:长期沉淀的对话产物、关键开发决策、不再频繁更新的项目快照
|
- 用途:长期沉淀的对话产物、关键开发决策、不再频繁更新的项目快照
|
||||||
- 与"项目即时状态"的边界:[04-即时状态记忆.md](./rules/项目/04-即时状态记忆.md) 偏当前快照、更新频率高;`memory/` 偏长期保留
|
- 与"项目即时状态"的边界:[04-即时状态记忆.md](./rules/项目/04-即时状态记忆.md) 偏当前快照、更新频率高;`memory/` 偏长期保留
|
||||||
- 维护规约:详见 [02-记忆存储规范.md](./rules/全局/02-记忆存储规范.md)
|
- 维护规约:详见 [01-记忆与存储规范.md](./rules/全局/01-记忆与存储规范.md)
|
||||||
- 文件:
|
- 文件:
|
||||||
- [01-project_memory.md](./memory/01-project_memory.md):依赖小版本号、历史决策、变更时间轴
|
- [01-project_memory.md](./memory/01-project_memory.md):依赖小版本号、历史决策、变更时间轴
|
||||||
|
|
||||||
@@ -66,7 +68,7 @@
|
|||||||
## coordination/ — 多窗口协作运行时目录
|
## coordination/ — 多窗口协作运行时目录
|
||||||
|
|
||||||
- 用途:并行 solo 窗口下的"谁在改什么"登记表与共享文件清单(**运行时数据,非冷文档**)
|
- 用途:并行 solo 窗口下的"谁在改什么"登记表与共享文件清单(**运行时数据,非冷文档**)
|
||||||
- 协作协议详见 [07-并行窗口冲突规约.md](./rules/全局/07-并行窗口冲突规约.md)
|
- 协作协议详见 [05-并行窗口冲突规约.md](./rules/全局/05-并行窗口冲突规约.md)
|
||||||
- 文件:
|
- 文件:
|
||||||
- [00-README.md](./coordination/00-README.md):目录约定
|
- [00-README.md](./coordination/00-README.md):目录约定
|
||||||
- [01-ownership.md](./coordination/01-ownership.md):文件/目录所有权(Writer)登记
|
- [01-ownership.md](./coordination/01-ownership.md):文件/目录所有权(Writer)登记
|
||||||
|
|||||||
@@ -14,8 +14,11 @@
|
|||||||
|
|
||||||
<!-- 编译优化选项 -->
|
<!-- 编译优化选项 -->
|
||||||
<AccelerateBuildsInVisualStudio>true</AccelerateBuildsInVisualStudio>
|
<AccelerateBuildsInVisualStudio>true</AccelerateBuildsInVisualStudio>
|
||||||
<!-- 禁用 MSBuild 节点重用,避免在多项目并发或调试期间出现“文件正由另一进程使用” (XARDF7024) 的问题。 -->
|
<!-- 禁用 MSBuild 节点重用,避免在多项目并发或调试期间出现"文件正由另一进程使用" (XARDF7024) 的问题。 -->
|
||||||
<MSBuildDisableNodeReuse>true</MSBuildDisableNodeReuse>
|
<MSBuildDisableNodeReuse>true</MSBuildDisableNodeReuse>
|
||||||
|
<!-- 禁止 SDK 自动生成 AssemblyInfo,避免与 Directory.Build.props 中的元数据属性冲突(CS0579)。 -->
|
||||||
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
|
<GenerateTargetFrameworkAttribute>false</GenerateTargetFrameworkAttribute>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
||||||
|
|||||||
+16
-4
@@ -5,14 +5,26 @@
|
|||||||
<Platform Name="x64" />
|
<Platform Name="x64" />
|
||||||
<Platform Name="x86" />
|
<Platform Name="x86" />
|
||||||
</Configurations>
|
</Configurations>
|
||||||
<Folder Name="/src/">
|
<Folder Name="/src/" />
|
||||||
<Project Path="src/Hua.Todo.Application/Hua.Todo.Application.csproj">
|
<Folder Name="/src/Domain/">
|
||||||
<Build Solution="Debug|*" Project="true" />
|
<Project Path="src/Hua.Todo.Application/Hua.Todo.Application.csproj" />
|
||||||
</Project>
|
|
||||||
<Project Path="src/Hua.Todo.Core/Hua.Todo.Core.csproj" />
|
<Project Path="src/Hua.Todo.Core/Hua.Todo.Core.csproj" />
|
||||||
|
</Folder>
|
||||||
|
<Folder Name="/src/Host/">
|
||||||
|
<Project Path="src/Hua.Todo.Avalonia/Hua.Todo.Avalonia.csproj" />
|
||||||
<Project Path="src/Hua.Todo.Host/Hua.Todo.Host.csproj" />
|
<Project Path="src/Hua.Todo.Host/Hua.Todo.Host.csproj" />
|
||||||
<Project Path="src/Hua.Todo.Maui/Hua.Todo.Maui.csproj">
|
<Project Path="src/Hua.Todo.Maui/Hua.Todo.Maui.csproj">
|
||||||
<Build Solution="Debug|*" Project="false" />
|
<Build Solution="Debug|*" Project="false" />
|
||||||
</Project>
|
</Project>
|
||||||
</Folder>
|
</Folder>
|
||||||
|
<Folder Name="/src/HttpApi/">
|
||||||
|
<Project Path="src/Hua.Todo.HttpApi/Hua.Todo.HttpApi.csproj" />
|
||||||
|
<Project Path="src/Hua.Todo.HttpApi.Android/Hua.Todo.HttpApi.Android.csproj" />
|
||||||
|
<Project Path="src/Hua.Todo.HttpApi.AspNetCore/Hua.Todo.HttpApi.AspNetCore.csproj" />
|
||||||
|
</Folder>
|
||||||
|
<Folder Name="/test/">
|
||||||
|
<Project Path="test/Hua.Todo.Host.Tests/Hua.Todo.Host.Tests.csproj" />
|
||||||
|
<Project Path="test/Hua.Todo.Maui.Tests/Hua.Todo.Maui.Tests.csproj" />
|
||||||
|
<Project Path="test/Hua.Todo.Avalonia.Tests/Hua.Todo.Avalonia.Tests.csproj" />
|
||||||
|
</Folder>
|
||||||
</Solution>
|
</Solution>
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ Hua.Todo/
|
|||||||
|
|
||||||
- **QQ 交流群**:2167048911 (Hua.Todo 交流群)
|
- **QQ 交流群**:2167048911 (Hua.Todo 交流群)
|
||||||
- **项目地址**:[Hua.Todo](https://git.we965.cn/Tools/Hua.Todo)
|
- **项目地址**:[Hua.Todo](https://git.we965.cn/Tools/Hua.Todo)
|
||||||
- **贡献指南**:欢迎提交 Pull Request,详见 [其他信息](docs/manual/其他信息.md)
|
- **贡献指南**:欢迎提交 Pull Request,详见 [其他信息](docs/manual/04-其他信息.md)
|
||||||
|
|
||||||
## 📄 开源协议
|
## 📄 开源协议
|
||||||
|
|
||||||
@@ -91,12 +91,20 @@ Hua.Todo/
|
|||||||
|
|
||||||
## 📚 更多文档
|
## 📚 更多文档
|
||||||
|
|
||||||
### 用户与开发者手册
|
### 普通用户
|
||||||
- [技术栈与模块说明](docs/manual/技术栈与模块.md)
|
- [00-目录与导读](docs/manual/00-目录与导读.md) — 文档入口
|
||||||
- [版本更新历史](docs/manual/版本记录.md)
|
- [01-项目介绍](docs/manual/01-项目介绍.md)
|
||||||
- [技术设计文档](docs/manual/技术设计文档.md)
|
- [02-安装指南](docs/manual/02-安装指南.md)
|
||||||
- [代码规范文档](docs/manual/代码规范文档.md)
|
- [03-版本记录](docs/manual/03-版本记录.md)
|
||||||
- [其他信息 (贡献、许可证、联系方式)](docs/manual/其他信息.md)
|
- [04-其他信息](docs/manual/04-其他信息.md)
|
||||||
|
|
||||||
|
### 开发者
|
||||||
|
- [05-技术栈与项目结构](docs/manual/05-技术栈与项目结构.md)
|
||||||
|
- [06-开发环境与构建](docs/manual/06-开发环境与构建.md)
|
||||||
|
- [07-技术架构设计](docs/manual/07-技术架构设计.md)
|
||||||
|
- [08-云同步规则](docs/manual/08-云同步规则.md)
|
||||||
|
- [09-代码规范](docs/manual/09-代码规范.md)
|
||||||
|
- [10-MCP服务集成](docs/manual/10-MCP服务集成.md)
|
||||||
|
|
||||||
### 项目进度与需求
|
### 项目进度与需求
|
||||||
- [产品需求文档](docs/project/产品需求文档.md)
|
- [产品需求文档](docs/project/产品需求文档.md)
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# AI沟通记录:01-规则沟通记录体系建立
|
||||||
|
|
||||||
|
- **日期**:2026-06-16
|
||||||
|
- **参与者**:用户、AI 助手
|
||||||
|
- **会话序号**:01
|
||||||
|
|
||||||
|
## 讨论主题
|
||||||
|
|
||||||
|
建立 AI 沟通记录体系:在全局规则中新增规范,在 `docs/` 下建立存储目录。
|
||||||
|
|
||||||
|
## 关键决策
|
||||||
|
|
||||||
|
1. AI沟通记录规则文件编号为 `08-AI沟通记录规范.md`,存放在 `.trae/rules/全局/` 下
|
||||||
|
2. AI沟通记录存放在 `docs/AI沟通记录/` 目录下
|
||||||
|
3. 按主题归档:同一主题的多轮对话追加在同一个 `NN-主题摘要.md` 文件中,而非每轮对话新建文件
|
||||||
|
4. 新主题才新建文件,序号从 01 递增,不可复用
|
||||||
|
5. 详细记录中的子主题必须带序号前缀(如 `01-xxx`、`02-xxx`),便于精确引用
|
||||||
|
|
||||||
|
## 待办事项
|
||||||
|
|
||||||
|
- [x] 创建 `.trae/rules/全局/08-AI沟通记录规范.md`
|
||||||
|
- [x] 创建 `docs/AI沟通记录/01-规则沟通记录体系建立.md`
|
||||||
|
- [x] 更新 `.trae/索引.md` 同步新增内容
|
||||||
|
- [x] 更新 `.trae/rules/全局/01-智能体记忆.md` 中的序号速查表
|
||||||
|
|
||||||
|
## 详细记录
|
||||||
|
|
||||||
|
### 01-用户初始请求
|
||||||
|
|
||||||
|
用户提出了两个明确要求:
|
||||||
|
1. 将AI沟通记录相关规则添加到全局规则目录
|
||||||
|
2. 建立 `docs/` 下的AI沟通记录存储体系,带序号编号,方便后续引用
|
||||||
|
|
||||||
|
### 02-过程中的纠正
|
||||||
|
|
||||||
|
AI 助手初始方向偏离,误以为需要读取源代码进行分析。用户明确纠正:此项工作与源代码无关,应直接操作规则文件等内容。
|
||||||
|
|
||||||
|
### 03-命名修正
|
||||||
|
|
||||||
|
用户指出"通信记录"名称不准确,应改为"AI沟通记录"。随即统一更新了规则文件名、目录名、索引引用及记录内容中的所有措辞。
|
||||||
|
|
||||||
|
### 04-目录结构修正
|
||||||
|
|
||||||
|
用户指出当前的子文件夹结构(`NN-主题摘要/记录.md`)不正确,应改为直接的单文件结构(`NN-主题摘要.md`)。
|
||||||
|
同时文件夹名"规则与沟通记录体系建立"应去掉"与"字,改为"规则沟通记录体系建立"。
|
||||||
|
|
||||||
|
### 05-归档方式修正
|
||||||
|
|
||||||
|
用户指出沟通记录应按主题归档,而非按对话次数新建文件。同一主题的多轮对话应追加在同一个文件中,只有新主题才新建文件。
|
||||||
|
此前错误地创建了 `02-沟通记录目录结构修正.md`,这属于同一主题的后续对话,应合并到 `01-规则沟通记录体系建立.md` 中。
|
||||||
|
|
||||||
|
### 06-最终方案
|
||||||
|
|
||||||
|
- 全局规则文件:`.trae/rules/全局/08-AI沟通记录规范.md`
|
||||||
|
- AI沟通记录目录:`docs/AI沟通记录/`
|
||||||
|
- 记录格式:`NN-主题摘要.md`(按主题归档,同主题多轮对话追加在同一文件)
|
||||||
|
- 引用方式:通过序号引用,如"参见沟通记录 01"
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
# AI沟通记录:02-云同步链路问题分析
|
||||||
|
|
||||||
|
- **日期**:2026-06-21
|
||||||
|
- **参与者**:用户、AI 助手
|
||||||
|
- **会话序号**:02
|
||||||
|
|
||||||
|
## 讨论主题
|
||||||
|
|
||||||
|
用户反馈云同步(CloudSync)存在问题,对完整链路进行分析定位根因。
|
||||||
|
|
||||||
|
## 关键决策
|
||||||
|
|
||||||
|
无(本次为问题分析,未涉及改动决策)。
|
||||||
|
|
||||||
|
## 待办事项
|
||||||
|
|
||||||
|
- [ ] 配置 MAUI 端 `CloudSyncUrl` 指向 Host 地址(通过 UI 或配置文件)
|
||||||
|
- [ ] 考虑在 dev 模式下默认配置合理值,降低首次使用门槛
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 详细记录
|
||||||
|
|
||||||
|
### 01-当前运行环境
|
||||||
|
|
||||||
|
启动了三个服务:
|
||||||
|
|
||||||
|
| 服务 | 端口 | 终端 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Vite dev server | 5174 | terminal 2 | `npm run dev`,proxy `/api` → `http://localhost:5057` |
|
||||||
|
| Host | 5173 | terminal 4 | `dotnet run src/Hua.Todo.Host`,完整 CloudSync 端点(`AddCloudSyncServer`) |
|
||||||
|
| MAUI (Windows) | 5057 | terminal 5 | `dotnet run src/Hua.Todo.Maui`,内嵌 WebServer,仅有 CloudSync 代理(`AddCloudSyncProxy`) |
|
||||||
|
|
||||||
|
### 02-云同步请求链路(当前状态)
|
||||||
|
|
||||||
|
```
|
||||||
|
Browser (localhost:5174)
|
||||||
|
│ POST /api/auth/login
|
||||||
|
│ GET /api/tasks/
|
||||||
|
│ POST /api/tasks
|
||||||
|
│ GET /api/security/policy
|
||||||
|
│ POST /api/cloud-sync/probe
|
||||||
|
▼
|
||||||
|
Vite dev server (5174)
|
||||||
|
│ vite.config.ts: proxy '/api' → target: 'http://localhost:5057'
|
||||||
|
▼
|
||||||
|
MAUI 内嵌 WebServer (5057)
|
||||||
|
│ UseCloudSyncProxy() 中间件拦截路径:
|
||||||
|
│ /api/auth, /api/tasks, /api/sync, /api/security, /api/cloud-sync
|
||||||
|
│
|
||||||
|
├─ CloudSyncUrl 已配置 → 代理转发到 Host(5173) → ✓ 正常
|
||||||
|
└─ CloudSyncUrl 为空 → 直接返回 503 → ✗ 当前状态
|
||||||
|
```
|
||||||
|
|
||||||
|
### 03-根因定位
|
||||||
|
|
||||||
|
**MAUI 的 `appsettings.json` 中未配置 `CloudSyncUrl`**。
|
||||||
|
|
||||||
|
- [appsettings.json](file:///d:/Proj/6.Hua.Todo/src/Hua.Todo.Maui/appsettings.json) 中 `WebServer` 节点缺少 `CloudSyncUrl` 字段
|
||||||
|
- `CloudSyncProxyService.CloudSyncUrl` 默认值为空字符串,setter 中 `string.IsNullOrWhiteSpace` 将其转为 `null`
|
||||||
|
- [CloudSyncProxyService.cs](file:///d:/Proj/6.Hua.Todo/src/Hua.Todo.Application/Services/CloudSync/Services/CloudSyncProxyService.cs#L142-L148) 在 `UseCloudSyncProxy` 中间件中,`CloudSyncUrl` 为空时直接返回 **503 "cloud sync server URL not configured"**
|
||||||
|
|
||||||
|
### 04-本地 Todo API 不受影响
|
||||||
|
|
||||||
|
`/api/task`(本地 Todo CRUD)不在 CloudSyncProxy 的 `ProxyPathRoots` 中,由 `UseDynamicApi()` 直接处理,走 MAUI 本地 SQLite,正常工作。
|
||||||
|
|
||||||
|
### 05-完整架构图
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 请求路径分流 │
|
||||||
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ /api/task/* ──────────→ DynamicApi (MAUI 本地) → MAUI SQLite │
|
||||||
|
│ (本地 Todo CRUD) ✓ 正常 │
|
||||||
|
│ │
|
||||||
|
│ /api/auth/* UseCloudSyncProxy 拦截 │
|
||||||
|
│ /api/tasks/* ├─ CloudSyncUrl 为空 → 503 ✗ │
|
||||||
|
│ /api/security/* └─ CloudSyncUrl 已配 → 转发 Host ✓ │
|
||||||
|
│ /api/cloud-sync/* │
|
||||||
|
│ │
|
||||||
|
│ /api/cloudSyncProxySettings ──→ DynamicApi (MAUI 本地) │
|
||||||
|
│ (设置 CloudSyncUrl 用) ✓ 正常 │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 06-修复方案
|
||||||
|
|
||||||
|
**方案一(推荐,已有 UI 支持)**:通过 CloudSyncSettingsDialog 配置
|
||||||
|
1. 打开云同步设置弹窗
|
||||||
|
2. 输入服务端地址:`http://localhost:5173`
|
||||||
|
3. 点击"保存并探测"
|
||||||
|
4. 后续云同步请求经 MAUI 代理→Host,链路贯通
|
||||||
|
|
||||||
|
**方案二**:直接修改 MAUI 的 `appsettings.json`
|
||||||
|
在 `WebServer` 节点添加:`"CloudSyncUrl": "http://localhost:5173"`
|
||||||
|
|
||||||
|
**方案三**(纯 dev 模式):切换 Vite proxy 目标
|
||||||
|
设置环境变量 `VITE_API_TARGET=http://localhost:5173`,让 Vite 直连 Host,绕过 MAUI 内嵌服务器。
|
||||||
|
注意:此模式下 Todo CRUD 走 Host 的 DB(`src/Hua.Todo.Host/Hua.Todo.db`),而非 MAUI 的本地 DB。
|
||||||
|
|
||||||
|
### 07-设计层面的潜在改进点
|
||||||
|
|
||||||
|
1. **默认值优化**:在 dev 模式下,`CloudSyncUrl` 可默认指向 `http://localhost:5173`(Host 默认端口),减少首次手动配置。
|
||||||
|
2. **错误提示增强**:503 响应可携带更友好的错误信息,前端弹窗提示用户去云同步设置中配置服务端地址。
|
||||||
|
3. **端口冲突风险**:当 Host(5173) + MAUI(5057) + Avalonia(5057) 同时运行时,需注意数据库隔离与端口分配。
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# Hua.Todo 文档指南
|
||||||
|
|
||||||
|
> 本文档是 Hua.Todo 项目手册的入口。根据您的身份选择对应的阅读路线。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 我是普通用户
|
||||||
|
|
||||||
|
只想安装和使用 Hua.Todo?从这里开始:
|
||||||
|
|
||||||
|
| 序号 | 文档 | 内容 |
|
||||||
|
|---|---|---|
|
||||||
|
| 01 | [项目介绍](./01-项目介绍.md) | 这是什么、能做什么、支持哪些平台 |
|
||||||
|
| 02 | [安装指南](./02-安装指南.md) | 如何安装、配置云同步、常见问题 |
|
||||||
|
| 03 | [版本记录](./03-版本记录.md) | 更新了哪些内容 |
|
||||||
|
| 04 | [其他信息](./04-其他信息.md) | 贡献指南、许可证、联系方式 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 我是开发者
|
||||||
|
|
||||||
|
需要编译源码、了解架构或参与开发?从这里开始:
|
||||||
|
|
||||||
|
| 序号 | 文档 | 内容 |
|
||||||
|
|---|---|---|
|
||||||
|
| 05 | [技术栈与项目结构](./05-技术栈与项目结构.md) | 用了哪些技术、项目如何划分、依赖关系 |
|
||||||
|
| 06 | [开发环境与构建](./06-开发环境与构建.md) | 如何搭建环境、构建、部署服务端 |
|
||||||
|
| 07 | [技术架构设计](./07-技术架构设计.md) | 目录结构、API 设计、数据库、通信机制 |
|
||||||
|
| 08 | [云同步规则](./08-云同步规则.md) | 认证鉴权、同步工作流、安全策略 |
|
||||||
|
| 09 | [代码规范](./09-代码规范.md) | C# / TypeScript / Vue 编码规范 |
|
||||||
|
| 10 | [MCP服务集成](./10-MCP服务集成.md) | MCP 接口规范与前端集成 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 项目简介
|
||||||
|
|
||||||
|
Hua.Todo 是一个跨平台待办事项管理工具,支持:
|
||||||
|
|
||||||
|
- **多平台**:Windows / macOS / Linux / Android / iOS
|
||||||
|
- **云同步**:多端数据同步,安全可控
|
||||||
|
- **语音控制**:语音指令操作待办项
|
||||||
|
- **AI 辅助**:通过 MCP 协议接入 AI 客户端
|
||||||
|
|
||||||
|
> 源码仓库:[https://git.we965.cn/Tools/Hua.Todo](https://git.we965.cn/Tools/Hua.Todo)
|
||||||
@@ -1,414 +0,0 @@
|
|||||||
# Hua.Todo 技术设计文档 v1.1.0
|
|
||||||
|
|
||||||
## 1. 项目概述
|
|
||||||
本文档描述 Hua.Todo v1.1.0 的技术设计方案,包括项目文件目录结构、模块划分、技术选型和实现细节。
|
|
||||||
|
|
||||||
## 2. 技术栈
|
|
||||||
|
|
||||||
### 2.1 后端技术栈
|
|
||||||
- **开发语言**: C# 10
|
|
||||||
- **框架**: .NET 10
|
|
||||||
- **UI 框架**: MAUI (Multi-platform App UI) + Avalonia (Linux 支持)
|
|
||||||
- **Web 服务器**: Kestrel (ASP.NET Core 内置)
|
|
||||||
- **API 框架**: ASP.NET Core Web API
|
|
||||||
- **数据访问**: Entity Framework Core
|
|
||||||
- **数据库**: SQLite (本地存储)
|
|
||||||
- **日志**: Serilog
|
|
||||||
- **依赖注入**: Microsoft.Extensions.DependencyInjection
|
|
||||||
|
|
||||||
### 2.2 前端技术栈
|
|
||||||
- **开发语言**: JavaScript/TypeScript
|
|
||||||
- **框架**: Vue.js 3
|
|
||||||
- **构建工具**: Vite
|
|
||||||
- **HTTP 客户端**: Axios
|
|
||||||
- **状态管理**: Pinia
|
|
||||||
- **UI 组件库**: Element Plus / Vant (移动端)
|
|
||||||
- **CSS 预处理器**: SCSS
|
|
||||||
|
|
||||||
## 3. 项目目录结构
|
|
||||||
|
|
||||||
```
|
|
||||||
Hua.Todo/
|
|
||||||
├── docs/ # 文档目录
|
|
||||||
│ ├── manual/ # 用户/开发者手册
|
|
||||||
│ │ ├── 03-技术栈与模块.md
|
|
||||||
│ │ ├── 06-版本记录.md
|
|
||||||
│ │ ├── 01-技术设计文档.md(本文件)
|
|
||||||
│ │ ├── 04-代码规范文档.md
|
|
||||||
│ │ └── 05-部署文档.md
|
|
||||||
│ └── project/ # 项目进度/需求文档
|
|
||||||
│ ├── 产品需求文档.md
|
|
||||||
│ └── ...
|
|
||||||
│
|
|
||||||
├── src/ # 源代码目录
|
|
||||||
│ ├── Hua.Todo.Maui/ # MAUI 主项目(跨平台入口)
|
|
||||||
│ │ ├── Platforms/ # 平台特定代码
|
|
||||||
│ │ │ ├── Windows/ # Windows 平台代码
|
|
||||||
│ │ │ │ ├── App.xaml # Windows 应用入口
|
|
||||||
│ │ │ │ └── Services/ # Windows 平台服务
|
|
||||||
│ │ │ │ └── HotKeyService.cs
|
|
||||||
│ │ │ ├── MacCatalyst/ # macOS 平台代码
|
|
||||||
│ │ │ │ ├── App.xaml
|
|
||||||
│ │ │ │ └── Services/
|
|
||||||
│ │ │ │ └── HotKeyService.cs
|
|
||||||
│ │ │ ├── Android/ # Android 平台代码
|
|
||||||
│ │ │ │ ├── MainActivity.cs
|
|
||||||
│ │ │ │ └── Services/
|
|
||||||
│ │ │ │ └── NotificationService.cs
|
|
||||||
│ │ │ └── iOS/ # iOS 平台代码
|
|
||||||
│ │ │ ├── AppDelegate.cs
|
|
||||||
│ │ │ └── Services/
|
|
||||||
│ │ │ └── NotificationService.cs
|
|
||||||
│ │ ├── Resources/ # 资源文件
|
|
||||||
│ │ │ ├── Images/ # 图片资源
|
|
||||||
│ │ │ ├── Styles/ # 样式资源
|
|
||||||
│ │ │ └── Fonts/ # 字体资源
|
|
||||||
│ │ ├── Controls/ # 自定义控件
|
|
||||||
│ │ │ └── WebViewContainer.xaml
|
|
||||||
│ │ ├── Services/ # 服务层
|
|
||||||
│ │ │ ├── IHotKeyService.cs
|
|
||||||
│ │ │ ├── IPlatformService.cs
|
|
||||||
│ │ │ └── AppLifecycleService.cs
|
|
||||||
│ │ ├── App.xaml # MAUI 应用入口
|
|
||||||
│ │ ├── App.xaml.cs
|
|
||||||
│ │ ├── MauiProgram.cs # MAUI 程序配置
|
|
||||||
│ │ └── Hua.Todo.Maui.csproj # MAUI 项目文件
|
|
||||||
│ │
|
|
||||||
│ ├── Hua.Todo.Avalonia/ # Avalonia 项目(Linux 支持)
|
|
||||||
│ │ ├── Assets/ # 资源文件
|
|
||||||
│ │ │ └── icon.ico # 应用图标
|
|
||||||
│ │ ├── Services/ # 服务层
|
|
||||||
│ │ │ ├── EmbeddedWebServerServiceFactory.cs
|
|
||||||
│ │ │ ├── GlobalHotKeyServiceFactory.cs
|
|
||||||
│ │ │ ├── NoopEmbeddedWebServerService.cs
|
|
||||||
│ │ │ └── Platforms/ # 平台特定服务
|
|
||||||
│ │ │ ├── AndroidGlobalHotKeyService.cs
|
|
||||||
│ │ │ └── LinuxGlobalHotKeyService.cs
|
|
||||||
│ │ ├── Views/ # 视图
|
|
||||||
│ │ │ ├── MainView.axaml
|
|
||||||
│ │ │ ├── MainView.axaml.cs
|
|
||||||
│ │ │ ├── MainWindow.axaml
|
|
||||||
│ │ │ └── MainWindow.axaml.cs
|
|
||||||
│ │ ├── App.axaml # Avalonia 应用入口
|
|
||||||
│ │ ├── App.axaml.cs
|
|
||||||
│ │ ├── Program.cs # Avalonia 程序配置
|
|
||||||
│ │ ├── appsettings.json # 配置文件
|
|
||||||
│ │ ├── setup.iss # 安装脚本
|
|
||||||
│ │ ├── wwwroot/ # 前端静态资源
|
|
||||||
│ │ └── Hua.Todo.Avalonia.csproj # Avalonia 项目文件
|
|
||||||
│ │
|
|
||||||
│ ├── Hua.Todo.Host/ # 后端 API 项目
|
|
||||||
│ │ ├── Controllers/ # API 控制器
|
|
||||||
│ │ │ ├── TasksController.cs
|
|
||||||
│ │ │ ├── SettingsController.cs
|
|
||||||
│ │ │ └── SyncController.cs
|
|
||||||
│ │ ├── Models/ # 数据模型
|
|
||||||
│ │ │ ├── Task.cs
|
|
||||||
│ │ │ ├── TaskDto.cs
|
|
||||||
│ │ │ └── ApiResponse.cs
|
|
||||||
│ │ ├── Services/ # 业务服务
|
|
||||||
│ │ │ ├── ITaskService.cs
|
|
||||||
│ │ │ ├── TaskService.cs
|
|
||||||
│ │ │ ├── ISyncService.cs
|
|
||||||
│ │ │ └── SyncService.cs
|
|
||||||
│ │ ├── Data/ # 数据访问层
|
|
||||||
│ │ │ ├── TodoDbContext.cs
|
|
||||||
│ │ │ ├── Converters/ # 类型转换器
|
|
||||||
│ │ │ │ └── LenientUtcDateTimeStringConverter.cs
|
|
||||||
│ │ │ ├── Repositories/
|
|
||||||
│ │ │ │ ├── ITaskRepository.cs
|
|
||||||
│ │ │ │ └── TaskRepository.cs
|
|
||||||
│ │ │ └── Migrations/ # 数据库迁移
|
|
||||||
│ │ ├── Middleware/ # 中间件
|
|
||||||
│ │ │ └── ExceptionMiddleware.cs
|
|
||||||
│ │ ├── Extensions/ # 扩展方法
|
|
||||||
│ │ │ └── ServiceCollectionExtensions.cs
|
|
||||||
│ │ ├── Program.cs # API 入口
|
|
||||||
│ │ ├── appsettings.json # 配置文件
|
|
||||||
│ │ └── Hua.Todo.Host.csproj # Host 项目文件
|
|
||||||
│ │
|
|
||||||
│ ├── Hua.Todo.Application/ # 应用层
|
|
||||||
│ │ ├── Data/ # 数据访问
|
|
||||||
│ │ │ ├── TodoDbContext.cs
|
|
||||||
│ │ │ └── Converters/ # 类型转换器
|
|
||||||
│ │ │ └── LenientUtcDateTimeStringConverter.cs
|
|
||||||
│ │ └── Hua.Todo.Application.csproj # Application 项目文件
|
|
||||||
│ │
|
|
||||||
│ ├── Hua.Todo.Core/ # 核心业务逻辑层
|
|
||||||
│ │ ├── Entities/ # 实体类
|
|
||||||
│ │ │ ├── Task.cs
|
|
||||||
│ │ │ └── TaskPriority.cs
|
|
||||||
│ │ ├── Interfaces/ # 接口定义
|
|
||||||
│ │ │ ├── ITaskRepository.cs
|
|
||||||
│ │ │ └── IUnitOfWork.cs
|
|
||||||
│ │ ├── ValueObjects/ # 值对象
|
|
||||||
│ │ │ └── TaskTitle.cs
|
|
||||||
│ │ ├── Specifications/ # 规范模式
|
|
||||||
│ │ │ └── TaskSpecifications.cs
|
|
||||||
│ │ └── Hua.Todo.Core.csproj # Core 项目文件
|
|
||||||
│ │
|
|
||||||
│ ├── Hua.Todo.Web/ # 前端 Web 项目 (Vue.js)
|
|
||||||
│ │ ├── public/ # 静态资源
|
|
||||||
│ │ │ └── favicon.svg # 网站图标
|
|
||||||
│ │ ├── src/ # 源代码
|
|
||||||
│ │ │ ├── api/ # API 调用
|
|
||||||
│ │ │ │ ├── client.ts # HTTP 客户端配置
|
|
||||||
│ │ │ │ ├── tasks.ts # 任务相关 API
|
|
||||||
│ │ │ │ └── settings.ts # 设置相关 API
|
|
||||||
│ │ │ ├── assets/ # 资源文件
|
|
||||||
│ │ │ │ ├── images/
|
|
||||||
│ │ │ │ └── styles/
|
|
||||||
│ │ │ ├── components/ # Vue 组件
|
|
||||||
│ │ │ │ ├── TaskList.vue
|
|
||||||
│ │ │ │ ├── TaskItem.vue
|
|
||||||
│ │ │ │ ├── QuickEntry.vue
|
|
||||||
│ │ │ │ └── Settings.vue
|
|
||||||
│ │ │ ├── composables/ # 组合式函数
|
|
||||||
│ │ │ │ ├── useTasks.ts
|
|
||||||
│ │ │ │ └── useHotKey.ts
|
|
||||||
│ │ │ ├── stores/ # 状态管理 (Pinia)
|
|
||||||
│ │ │ │ ├── tasks.ts
|
|
||||||
│ │ │ │ └── settings.ts
|
|
||||||
│ │ │ ├── types/ # TypeScript 类型定义
|
|
||||||
│ │ │ │ ├── task.ts
|
|
||||||
│ │ │ │ └── api.ts
|
|
||||||
│ │ │ ├── utils/ # 工具函数
|
|
||||||
│ │ │ │ ├── date.ts
|
|
||||||
│ │ │ │ └── storage.ts
|
|
||||||
│ │ │ ├── App.vue # 根组件
|
|
||||||
│ │ │ └── main.ts # 应用入口
|
|
||||||
│ │ ├── package.json # 依赖配置
|
|
||||||
│ │ ├── vite.config.ts # Vite 配置
|
|
||||||
│ │ ├── tsconfig.json # TypeScript 配置
|
|
||||||
│ │ └── index.html # HTML 模板
|
|
||||||
│ │
|
|
||||||
│ └── Hua.Todo.Tests/ # 测试项目
|
|
||||||
│ ├── Unit/ # 单元测试
|
|
||||||
│ │ ├── Services/
|
|
||||||
│ │ │ └── TaskServiceTests.cs
|
|
||||||
│ │ └── Controllers/
|
|
||||||
│ │ └── TasksControllerTests.cs
|
|
||||||
│ ├── Integration/ # 集成测试
|
|
||||||
│ │ └── ApiIntegrationTests.cs
|
|
||||||
│ └── Hua.Todo.Tests.csproj
|
|
||||||
│
|
|
||||||
├── .gitignore # Git 忽略文件
|
|
||||||
├── Hua.Todo.sln # 解决方案文件
|
|
||||||
└── README.md # 项目说明文档
|
|
||||||
```
|
|
||||||
|
|
||||||
## 4. 模块设计
|
|
||||||
|
|
||||||
### 4.1 MAUI 主项目 (Hua.Todo.Maui)
|
|
||||||
**职责**:
|
|
||||||
- 应用程序入口和生命周期管理
|
|
||||||
- 平台特定功能封装
|
|
||||||
- WebView 容器管理
|
|
||||||
- 本地 HTTP 服务器启动
|
|
||||||
|
|
||||||
**调试与接口文档**:
|
|
||||||
- Windows Debug 模式下,内嵌 WebServer 默认提供 Swagger UI(`{HostUrl}/swagger`)与 OpenAPI JSON(`{HostUrl}/swagger/v1/swagger.json`),用于本地接口联调。
|
|
||||||
|
|
||||||
**关键组件**:
|
|
||||||
- `MauiProgram.cs`: 配置 MAUI 应用和依赖注入
|
|
||||||
- `App.xaml.cs`: 应用程序主入口
|
|
||||||
- `WebViewContainer`: 封装 WebView 控件
|
|
||||||
- 平台特定服务: 快捷键、通知等
|
|
||||||
|
|
||||||
### 4.2 Avalonia 项目 (Hua.Todo.Avalonia)
|
|
||||||
**职责**:
|
|
||||||
- Linux 平台支持
|
|
||||||
- 桌面交互功能(托盘菜单、全局热键等)
|
|
||||||
- WebView 容器管理
|
|
||||||
- 本地 HTTP 服务器启动
|
|
||||||
|
|
||||||
**关键组件**:
|
|
||||||
- `Program.cs`: 配置 Avalonia 应用和依赖注入
|
|
||||||
- `App.axaml.cs`: 应用程序主入口
|
|
||||||
- `MainWindow.axaml.cs`: 主窗口管理
|
|
||||||
- `MainView.axaml.cs`: 主视图管理
|
|
||||||
- `GlobalHotKeyServiceFactory`: 全局热键服务工厂
|
|
||||||
- `EmbeddedWebServerServiceFactory`: 内嵌 Web 服务器服务工厂
|
|
||||||
- 平台特定服务: Linux 全局热键等
|
|
||||||
|
|
||||||
### 4.3 后端 API 项目 (Hua.Todo.Host)
|
|
||||||
**职责**:
|
|
||||||
- 提供 RESTful API 接口
|
|
||||||
- 业务逻辑处理
|
|
||||||
- 数据访问和持久化
|
|
||||||
- 本地 HTTP 服务器托管
|
|
||||||
|
|
||||||
**接口文档**:
|
|
||||||
- 开发环境(`ASPNETCORE_ENVIRONMENT=Development`)下提供 Swagger UI:`http://localhost:5173/swagger`(或 `https://localhost:7175/swagger`)。
|
|
||||||
|
|
||||||
**关键组件**:
|
|
||||||
- `CloudSync`: 云同步 Minimal APIs(`/auth`、`/tasks`、`/sync`、`/security`)
|
|
||||||
- `DynamicApiMiddleware`: 任务管理等业务接口通过 Dynamic API(`/api/{service}/...`)对外暴露
|
|
||||||
- `Data`: 数据访问层和数据库上下文
|
|
||||||
- `Program.cs`: API 服务器配置和启动
|
|
||||||
|
|
||||||
### 4.4 核心业务层 (Hua.Todo.Core)
|
|
||||||
**职责**:
|
|
||||||
- 定义领域模型和业务规则
|
|
||||||
- 提供核心业务接口
|
|
||||||
- 实现领域驱动设计模式
|
|
||||||
|
|
||||||
**关键组件**:
|
|
||||||
- `Entities`: 领域实体
|
|
||||||
- `Interfaces`: 业务接口定义
|
|
||||||
- `ValueObjects`: 值对象
|
|
||||||
- `Specifications`: 业务规范
|
|
||||||
|
|
||||||
### 4.5 前端 Web 项目 (Hua.Todo.Web)
|
|
||||||
**职责**:
|
|
||||||
- 用户界面展示
|
|
||||||
- 用户交互处理
|
|
||||||
- HTTP API 调用
|
|
||||||
- 状态管理
|
|
||||||
|
|
||||||
**关键组件**:
|
|
||||||
- `components`: Vue 组件
|
|
||||||
- `api`: API 调用封装
|
|
||||||
- `stores`: 状态管理
|
|
||||||
- `composables`: 组合式函数
|
|
||||||
|
|
||||||
## 5. HTTP API 设计
|
|
||||||
|
|
||||||
### 5.1 API 基础配置
|
|
||||||
- **基础 URL(开发三件套 / Host 模式)**: `http://localhost:5173/api`(或 `https://localhost:7175/api`)
|
|
||||||
- **基础 URL(MAUI 内嵌模式)**: `{HostUrl}/api`(`HostUrl` 来自 `appsettings.json: WebServer.HostUrl`,默认 `http://localhost:5057`)
|
|
||||||
- **数据格式**: JSON
|
|
||||||
- **认证方式**: 以实际端点为准(如云同步相关端点可能需要认证)
|
|
||||||
- **跨域配置**: 允许本地跨域请求
|
|
||||||
|
|
||||||
### 5.2 API 端点设计
|
|
||||||
|
|
||||||
#### 任务管理 API
|
|
||||||
```
|
|
||||||
GET /api/task # 获取任务列表(默认:全部)
|
|
||||||
GET /api/task/active # 获取未完成任务
|
|
||||||
GET /api/task/completed # 获取已完成任务
|
|
||||||
GET /api/task/{id} # 获取单个任务
|
|
||||||
POST /api/task # 创建任务
|
|
||||||
PUT /api/task # 更新任务(通过 Body 内的 id 定位)
|
|
||||||
DELETE /api/task/{id} # 删除任务
|
|
||||||
PATCH /api/task/{id}/toggle # 切换完成状态
|
|
||||||
GET /api/task/{parentTaskId}/subtasks # 获取子任务列表
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 云同步 API(Host 模式)
|
|
||||||
```
|
|
||||||
POST /auth/bootstrap # 初始化管理员(仅首次)
|
|
||||||
POST /auth/login # 登录
|
|
||||||
POST /auth/step-up # 二次验证(提升权限)
|
|
||||||
GET /tasks/ # 获取云端任务(只读)
|
|
||||||
POST /sync/ # 推送/拉取合并同步
|
|
||||||
GET /security/policy # 获取安全策略
|
|
||||||
PUT /security/policy # 更新安全策略
|
|
||||||
```
|
|
||||||
|
|
||||||
## 6. 数据库设计
|
|
||||||
|
|
||||||
### 6.1 数据库表结构
|
|
||||||
|
|
||||||
#### Tasks 表
|
|
||||||
```sql
|
|
||||||
CREATE TABLE Tasks (
|
|
||||||
Id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
Title TEXT NOT NULL,
|
|
||||||
Priority INTEGER NOT NULL DEFAULT 0,
|
|
||||||
IsCompleted INTEGER NOT NULL DEFAULT 0,
|
|
||||||
CreatedAt TEXT NOT NULL,
|
|
||||||
UpdatedAt TEXT NOT NULL
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Settings 表
|
|
||||||
```sql
|
|
||||||
CREATE TABLE Settings (
|
|
||||||
Key TEXT PRIMARY KEY,
|
|
||||||
Value TEXT NOT NULL,
|
|
||||||
UpdatedAt TEXT NOT NULL
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6.2 数据访问策略
|
|
||||||
- 使用 Entity Framework Core 进行数据访问
|
|
||||||
- 采用 Repository 模式封装数据访问
|
|
||||||
- 支持 LINQ 查询和异步操作
|
|
||||||
- 数据库迁移管理
|
|
||||||
|
|
||||||
## 7. 通信机制
|
|
||||||
|
|
||||||
### 7.1 HTTP 通信流程
|
|
||||||
1. **C# 后端启动**: MAUI 应用启动时启动本地 Kestrel 服务器
|
|
||||||
2. **Vue 前端加载**: WebView 加载 Vue 应用
|
|
||||||
3. **API 调用**: Vue 通过 Axios 调用本地 HTTP API
|
|
||||||
4. **数据处理**: C# 后端处理请求并返回 JSON 数据
|
|
||||||
5. **界面更新**: Vue 接收响应并更新界面
|
|
||||||
|
|
||||||
### 7.2 错误处理
|
|
||||||
- 统一的错误响应格式
|
|
||||||
- 异常中间件捕获和处理
|
|
||||||
- 前端错误提示和重试机制
|
|
||||||
|
|
||||||
## 8. 部署和打包
|
|
||||||
|
|
||||||
### 8.1 开发环境
|
|
||||||
- **后端调试**: 使用 Visual Studio 调试 MAUI 应用
|
|
||||||
- **前端调试**: 使用 Vite 开发服务器
|
|
||||||
- **热重载**: 支持前后端热重载
|
|
||||||
|
|
||||||
### 8.2 生产构建
|
|
||||||
- **前端构建**: `npm run build` 生成静态文件
|
|
||||||
- **后端打包**: MAUI 发布各平台应用
|
|
||||||
- **静态文件嵌入**: 将前端静态文件嵌入到 MAUI 应用中
|
|
||||||
|
|
||||||
### 8.3 平台特定配置
|
|
||||||
- **Windows**: WebView2 运行时要求
|
|
||||||
- **macOS**: 代码签名和公证
|
|
||||||
- **移动端**: 应用商店发布配置
|
|
||||||
- **Linux**: .NET MAUI 无官方 Linux 目标,需要引入独立桌面宿主(例如基于 WebKitGTK 的方案)并处理运行时依赖与打包格式
|
|
||||||
|
|
||||||
## 9. 性能优化
|
|
||||||
|
|
||||||
### 9.1 前端优化
|
|
||||||
- 组件懒加载
|
|
||||||
- 虚拟滚动(长列表)
|
|
||||||
- 图片懒加载
|
|
||||||
- 缓存策略
|
|
||||||
|
|
||||||
### 9.2 后端优化
|
|
||||||
- 数据库查询优化
|
|
||||||
- 响应缓存
|
|
||||||
- 异步处理
|
|
||||||
- 连接池管理
|
|
||||||
|
|
||||||
## 10. 安全考虑
|
|
||||||
|
|
||||||
### 10.1 本地安全
|
|
||||||
- 本地服务器仅监听 localhost
|
|
||||||
- 防止外部访问
|
|
||||||
- 数据加密存储(可选)
|
|
||||||
|
|
||||||
### 10.2 数据安全
|
|
||||||
- 数据库文件权限控制
|
|
||||||
- 定期备份机制
|
|
||||||
- 敏感数据保护
|
|
||||||
|
|
||||||
## 11. 测试策略
|
|
||||||
|
|
||||||
### 11.1 单元测试
|
|
||||||
- 核心业务逻辑测试
|
|
||||||
- 服务层测试
|
|
||||||
- 工具函数测试
|
|
||||||
|
|
||||||
### 11.2 集成测试
|
|
||||||
- API 集成测试
|
|
||||||
- 数据库集成测试
|
|
||||||
- 前后端集成测试
|
|
||||||
|
|
||||||
### 11.3 端到端测试
|
|
||||||
- 跨平台功能测试
|
|
||||||
- 用户流程测试
|
|
||||||
- 性能测试
|
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# 项目介绍
|
||||||
|
|
||||||
|
> 本文档面向普通用户,介绍 Hua.Todo 是什么、能做什么、支持哪些平台。
|
||||||
|
|
||||||
|
## 1. 什么是 Hua.Todo
|
||||||
|
|
||||||
|
Hua.Todo 是一个**跨平台待办事项管理工具**,帮助您记录和管理日常任务。
|
||||||
|
|
||||||
|
### 核心功能
|
||||||
|
|
||||||
|
| 功能 | 说明 |
|
||||||
|
|---|---|
|
||||||
|
| 待办管理 | 创建、编辑、删除、标记完成;支持父子任务层级 |
|
||||||
|
| 多平台 | Windows、macOS、Linux、Android、iOS 均可使用 |
|
||||||
|
| 云同步 | 登录后自动同步多端数据,随时随地查看 |
|
||||||
|
| 语音控制 | 通过语音指令快速操作待办项 |
|
||||||
|
| 关键词搜索 | 主界面搜索框按标题实时过滤 |
|
||||||
|
| 托盘常驻 | 最小化到系统托盘,全局热键快速唤起 |
|
||||||
|
|
||||||
|
### 界面一览
|
||||||
|
|
||||||
|
- **主界面**:任务列表 + 搜索框 + 同步按钮 + 快速创建入口
|
||||||
|
- **云同步设置**:配置服务端地址、登录/登出、安全策略查看
|
||||||
|
- **任务编辑**:标题、优先级、父子关系、完成状态
|
||||||
|
|
||||||
|
## 2. 支持的平台
|
||||||
|
|
||||||
|
| 平台 | 说明 |
|
||||||
|
|---|---|
|
||||||
|
| Windows | 安装包(`.exe`)或绿色版,需 WebView2 Runtime |
|
||||||
|
| Linux | `.tar.gz` 压缩包,需 WebKitGTK |
|
||||||
|
| macOS | 通过 MAUI 构建(开发中) |
|
||||||
|
| Android / iOS | 通过 MAUI 构建(开发中) |
|
||||||
|
|
||||||
|
## 3. 两种使用方式
|
||||||
|
|
||||||
|
### 方式一:单机使用(默认)
|
||||||
|
|
||||||
|
下载安装后直接使用,所有数据存储在本地。无需网络、无需注册。
|
||||||
|
|
||||||
|
### 方式二:启用云同步
|
||||||
|
|
||||||
|
配置云同步服务端地址并登录后,数据自动同步到服务端,可在多台设备间共享。
|
||||||
|
|
||||||
|
> 云同步需要额外部署 Hua.Todo.Host 服务端(可自行部署或使用第三方提供的服务)。
|
||||||
|
|
||||||
|
## 4. 下一步
|
||||||
|
|
||||||
|
- 如何安装 → [02-安装指南](./02-安装指南.md)
|
||||||
|
- 更新了什么 → [03-版本记录](./03-版本记录.md)
|
||||||
|
- 了解技术细节 → [05-技术栈与项目结构](./05-技术栈与项目结构.md)(开发者)
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
# 安装指南
|
||||||
|
|
||||||
|
> 本文档介绍如何安装和使用 Hua.Todo 客户端,以及如何部署云同步服务端。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Windows 安装
|
||||||
|
|
||||||
|
### 1.1 安装包(推荐)
|
||||||
|
|
||||||
|
1. 下载 `Hua.Todo_Setup_vX.Y.Z.exe`
|
||||||
|
2. 运行安装程序,按向导完成安装
|
||||||
|
3. 安装完成后会在桌面和开始菜单创建快捷方式
|
||||||
|
|
||||||
|
> 静默安装:`Hua.Todo_Setup_vX.Y.Z.exe /VERYSILENT /SUPPRESSMSGBOXES`
|
||||||
|
|
||||||
|
### 1.2 绿色版
|
||||||
|
|
||||||
|
1. 下载绿色版压缩包并解压
|
||||||
|
2. 确保系统已安装 [WebView2 Runtime](https://developer.microsoft.com/microsoft-edge/webview2/)
|
||||||
|
3. 运行 `Hua.Todo.Maui.exe`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Linux 安装
|
||||||
|
|
||||||
|
### 2.1 解压部署
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tar -xzf hua.todo-{version}-linux-x64.tar.gz -C /opt/hua-todo
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 环境依赖
|
||||||
|
|
||||||
|
- 确保系统安装了 `libwebkit2gtk-4.0-37`
|
||||||
|
- 如果版本未自带 .NET Runtime,需安装 .NET 10 Runtime
|
||||||
|
|
||||||
|
### 2.3 启动
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x /opt/hua-todo/Hua.Todo.Avalonia
|
||||||
|
/opt/hua-todo/Hua.Todo.Avalonia
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 云同步服务端部署
|
||||||
|
|
||||||
|
如需使用云同步功能,需要部署 `Hua.Todo.Host` 服务端。
|
||||||
|
|
||||||
|
### 3.1 Docker 部署(推荐)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t hua-todo-server -f src/Hua.Todo.Host/Dockerfile .
|
||||||
|
docker run -d \
|
||||||
|
--name hua-todo-server \
|
||||||
|
-p 5173:5173 \
|
||||||
|
-v /data/hua-todo/db:/app/data \
|
||||||
|
-e ASPNETCORE_ENVIRONMENT=Production \
|
||||||
|
hua-todo-server
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 直接部署
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet publish src/Hua.Todo.Host -c Release -o ./dist
|
||||||
|
# 将 ./dist 内容同步到服务器
|
||||||
|
# 可选:配置 Systemd 服务
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 配置云同步
|
||||||
|
|
||||||
|
1. 打开 Hua.Todo → 点击"云同步设置"
|
||||||
|
2. 输入服务端地址(如 `http://your-server:5173`)
|
||||||
|
3. 点击"保存并探测"确认可达
|
||||||
|
4. 输入用户名和密码登录
|
||||||
|
5. 登录成功后即可使用同步功能
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 关键配置
|
||||||
|
|
||||||
|
### 5.1 数据位置
|
||||||
|
|
||||||
|
- **客户端**:数据默认存储在用户目录 `Hua.Todo/todo.db`
|
||||||
|
- **服务端**:数据存储在容器挂载卷或数据库连接字符串指定位置
|
||||||
|
|
||||||
|
### 5.2 端口
|
||||||
|
|
||||||
|
- 客户端本地端口默认 `5057`,可在 `appsettings.json` 中修改
|
||||||
|
- 服务端端口默认 `5173`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 常见问题
|
||||||
|
|
||||||
|
| 问题 | 解决方法 |
|
||||||
|
|---|---|
|
||||||
|
| Windows 客户端无法加载 UI | 安装 [WebView2 Runtime](https://developer.microsoft.com/microsoft-edge/webview2/) |
|
||||||
|
| Linux 客户端无法启动 | 安装 `libwebkit2gtk-4.0-37`;执行 `chmod +x` |
|
||||||
|
| 同步连接失败 | 确认服务端可达(访问 `/swagger`);确认地址格式含 `http://` 或 `https://` |
|
||||||
|
| 端口被占用 | 修改 `appsettings.json` 中的 `WebServer.HostUrl` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> 开发者如需从源码构建,见 [06-开发环境与构建](./06-开发环境与构建.md)。
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
# 技术栈与模块说明
|
|
||||||
|
|
||||||
## 🛠️ 技术栈
|
|
||||||
|
|
||||||
### 后端技术栈
|
|
||||||
- **开发语言**:C# 13 (基于 .NET 10)
|
|
||||||
- **框架**:.NET 10 (Preview/Early Adopter)
|
|
||||||
- **UI 框架**:MAUI(移动端/部分桌面) + Avalonia(桌面端)
|
|
||||||
- **Web 服务器**:Kestrel (ASP.NET Core 内置)
|
|
||||||
- **API 框架**:ASP.NET Core Web API (支持动态 API 生成)
|
|
||||||
- **数据访问**:Entity Framework Core 10.0
|
|
||||||
- **数据库**:SQLite (本地存储)
|
|
||||||
- **依赖注入**:Microsoft.Extensions.DependencyInjection
|
|
||||||
|
|
||||||
### 前端技术栈
|
|
||||||
- **开发语言**:TypeScript 5+
|
|
||||||
- **框架**:Vue.js 3
|
|
||||||
- **构建工具**:Vite 5+
|
|
||||||
- **HTTP 客户端**:Axios
|
|
||||||
- **状态管理**:Pinia
|
|
||||||
- **UI 组件库**:Element Plus / Vant (移动端)
|
|
||||||
- **CSS 预处理器**:SCSS
|
|
||||||
|
|
||||||
## 🎯 核心模块说明
|
|
||||||
|
|
||||||
### Hua.Todo.Core
|
|
||||||
领域实体层,定义核心实体(TaskEntity)、安全策略实体(SecurityPolicyEntity)、枚举(TaskPriority)以及仓储接口(ITaskRepository)。
|
|
||||||
|
|
||||||
### Hua.Todo.Application
|
|
||||||
应用层实现,包含:
|
|
||||||
- **业务逻辑**:TaskService 实现。
|
|
||||||
- **动态 API**:基于 Middleware 的动态 API 生成逻辑。
|
|
||||||
- **云同步 (CloudSync)**:包含 Auth 验证、同步服务、安全策略管理等。
|
|
||||||
- **数据访问**:EF Core 数据库上下文(TodoDbContext)与迁移。
|
|
||||||
|
|
||||||
### Hua.Todo.Host
|
|
||||||
后端 API 宿主,作为独立 Server 运行时提供运行环境和配置。
|
|
||||||
|
|
||||||
### Hua.Todo.Web
|
|
||||||
前端 Web 项目,基于 Vue.js 3 + TypeScript + Vite,提供用户界面,通过 HTTP API 与后端通信。
|
|
||||||
|
|
||||||
### Hua.Todo.Maui
|
|
||||||
跨平台客户端项目,将 Web 内容嵌入到原生容器中,支持 Windows、Android、iOS 和 macOS。
|
|
||||||
|
|
||||||
### Hua.Todo.Avalonia
|
|
||||||
桌面客户端项目(Avalonia + WebView),用于提供 Linux/Windows/macOS 桌面形态;同样通过嵌入式 WebServer + WebView 承载前端 UI。
|
|
||||||
@@ -1,17 +1,24 @@
|
|||||||
# 版本更新历史
|
# 版本更新历史
|
||||||
|
|
||||||
## 🔄 版本更新
|
## 版本更新
|
||||||
|
|
||||||
### 版本策略
|
### 版本策略
|
||||||
|
|
||||||
- 采用语义化版本号:`MAJOR.MINOR.PATCH`
|
- 采用语义化版本号:`MAJOR.MINOR.PATCH`
|
||||||
- v1.0.0:初始 WPF 版本
|
- v1.0.0:初始 WPF 版本
|
||||||
- v1.1.0:MAUI + WebView 跨平台版本
|
- v1.1.0:MAUI + WebView 跨平台版本
|
||||||
- v1.2.0 (规划中):Linux 支持与增强功能
|
- v1.2.0:Linux 支持与增强功能
|
||||||
|
- v1.3.0(开发中):MCP 服务与语音控制
|
||||||
|
|
||||||
|
### v1.3.0 (2026-06-16)
|
||||||
|
|
||||||
|
- **MCP 服务**:新增 MCP 协议端点(`/mcp`),DynamicMcpToolExtensions 自动扫描 IDynamicApiService 接口生成 MCP 工具;当前覆盖 ITaskService(9 个)+ IVoiceService(4 个)= 13 个 MCP 工具
|
||||||
|
- **语音控制核心**:新增 Voice 模块(Core 接口、LLM 客户端、双策略意图解析器、指令执行器、AiBreakdownService);26 个单元测试通过
|
||||||
|
- **文档重组**:`docs/manual/` 目录按普通用户/开发者分离重组,新增双入口导航
|
||||||
|
|
||||||
### v1.2.8 (2026-06-14)
|
### v1.2.8 (2026-06-14)
|
||||||
|
|
||||||
- **文档**:新增 [任务同步规则.md](./02-任务同步规则.md),汇总 Todo 待办项云同步的架构、API 契约、认证鉴权、同步工作流、安全策略与可控落盘等完整规则。
|
- **文档**:新增 [云同步规则](./08-云同步规则.md),汇总 Todo 待办项云同步的架构、API 契约、认证鉴权、同步工作流、安全策略与可控落盘等完整规则。
|
||||||
|
|
||||||
### v1.2.8 (2026-04-13)
|
### v1.2.8 (2026-04-13)
|
||||||
|
|
||||||
@@ -25,30 +32,29 @@
|
|||||||
|
|
||||||
- **Linux 官方支持**:新增 `Hua.Todo.Avalonia` 项目,正式适配 Linux 平台,同时支持 Windows 和 macOS。
|
- **Linux 官方支持**:新增 `Hua.Todo.Avalonia` 项目,正式适配 Linux 平台,同时支持 Windows 和 macOS。
|
||||||
- **Avalonia 桌面交互**:增加托盘菜单(显示/退出)、关闭隐藏到托盘、Windows 全局热键唤起主窗口、热键配置本地持久化;并对齐 Avalonia 的 appsettings 默认值。
|
- **Avalonia 桌面交互**:增加托盘菜单(显示/退出)、关闭隐藏到托盘、Windows 全局热键唤起主窗口、热键配置本地持久化;并对齐 Avalonia 的 appsettings 默认值。
|
||||||
- **关键词检索**:主界面增加搜索框,按任务标题实时过滤;采用“命中即显示(含上下文)”策略;支持 Esc 清空;英文大小写不敏感。
|
- **关键词检索**:主界面增加搜索框,按任务标题实时过滤;采用"命中即显示(含上下文)"策略;支持 Esc 清空;英文大小写不敏感。
|
||||||
- **云同步(基础可用)**:新增“云同步设置”弹窗,支持手动配置服务端地址(格式校验 + 保存时可达性/风险提示);登录成功后拉取云端任务并刷新主界面(v1.2.0 为只读展示);401/403 时会自动清会话并弹出登录入口。
|
- **云同步(基础可用)**:新增"云同步设置"弹窗,支持手动配置服务端地址(格式校验 + 保存时可达性/风险提示);登录成功后拉取云端任务并刷新主界面(v1.2.0 为只读展示);401/403 时会自动清会话并弹出登录入口。
|
||||||
- **MAUI(Windows)内嵌 API 文档**:Debug 模式下,内嵌 WebServer 默认提供 Swagger UI(`{HostUrl}/swagger`)与 OpenAPI JSON(`{HostUrl}/swagger/v1/swagger.json`),便于本地接口调试。
|
- **MAUI(Windows)内嵌 API 文档**:Debug 模式下,内嵌 WebServer 默认提供 Swagger UI(`{HostUrl}/swagger`)与 OpenAPI JSON(`{HostUrl}/swagger/v1/swagger.json`),便于本地接口调试。
|
||||||
- **Android 启动稳定性修复**:在 AndroidManifest 中移除 `androidx.startup.InitializationProvider` 自动初始化入口,规避 `androidx.lifecycle.ProcessLifecycleInitializer` 缺失导致的启动崩溃(`NoClassDefFoundError`)。
|
- **Android 启动稳定性修复**:在 AndroidManifest 中移除 `androidx.startup.InitializationProvider` 自动初始化入口,规避 `androidx.lifecycle.ProcessLifecycleInitializer` 缺失导致的启动崩溃(`NoClassDefFoundError`)。
|
||||||
- **MAUI Android 调试配置修复**:在 `Hua.Todo.Maui.csproj` 中显式启用 `AndroidApplication`,并将调试架构配置从 `AndroidSupportedAbis` 切换为 `RuntimeIdentifiers=android-x64`,减少 Visual Studio 启动 Android 调试时的项目识别与模拟器架构问题。
|
- **MAUI Android 调试配置修复**:在 `Hua.Todo.Maui.csproj` 中显式启用 `AndroidApplication`,并将调试架构配置从 `AndroidSupportedAbis` 切换为 `RuntimeIdentifiers=android-x64`,减少 Visual Studio 启动 Android 调试时的项目识别与模拟器架构问题。
|
||||||
- **Swagger 输出补齐 Dynamic API**:任务管理等 Dynamic API 端点会出现在 `swagger.json` 中,避免“接口缺失”导致联调困难。
|
- **Swagger 输出补齐 Dynamic API**:任务管理等 Dynamic API 端点会出现在 `swagger.json` 中,避免"接口缺失"导致联调困难。
|
||||||
- **SQLite DateTime 兼容修复**:新增 `LenientUtcDateTimeStringConverter`,本地数据库中若存在历史遗留的 DateTime “ticks/时间戳字符串”脏数据,读取时将被兼容解析,避免 `/api/task` 等查询因单条坏数据整体失败。
|
- **SQLite DateTime 兼容修复**:新增 `LenientUtcDateTimeStringConverter`,本地数据库中若存在历史遗留的 DateTime "ticks/时间戳字符串"脏数据,读取时将被兼容解析,避免 `/api/task` 等查询因单条坏数据整体失败。
|
||||||
- **SPA 路由回落行为修复**:当 Release/非 Debug 未启用 Swagger 时,`/swagger` 不再被当作“后端专用路径”排除,访问会按 SPA 路由规则回落到 `/index.html`,避免直接 404。
|
- **SPA 路由回落行为修复**:当 Release/非 Debug 未启用 Swagger 时,`/swagger` 不再被当作"后端专用路径"排除,访问会按 SPA 路由规则回落到 `/index.html`,避免直接 404。
|
||||||
- **MAUI 多平台构建开关**:在 Windows 开发机上默认仅构建 Android + Windows 目标,避免 iOS/MacCatalyst 目标在非 macOS 环境触发运行时包缺失(NETSDK1082);在 macOS 上仍会包含 iOS/MacCatalyst 目标。
|
- **MAUI 多平台构建开关**:在 Windows 开发机上默认仅构建 Android + Windows 目标,避免 iOS/MacCatalyst 目标在非 macOS 环境触发运行时包缺失(NETSDK1082);在 macOS 上仍会包含 iOS/MacCatalyst 目标。
|
||||||
- **发布脚本整理**:拆分/对齐各平台发布入口,新增 `publish.ps1` 作为统一入口(默认发布 Windows + Linux),Windows 发布脚本支持开关打包与版本自增,发布产物会落盘到 `artifacts/`。
|
- **发布脚本整理**:拆分/对齐各平台发布入口,新增 `publish.ps1` 作为统一入口(默认发布 Windows + Linux),Windows 发布脚本支持开关打包与版本自增,发布产物会落盘到 `artifacts/`。
|
||||||
- **Windows 发布打包修复**:Inno Setup 安装包文件名带版本号(Hua.Todo_Setup_vX.Y.Z.exe);安装后快捷方式/启动项指向 Hua.Todo.Maui.exe;发布产物强制 IsUsingStatic=true。
|
- **Windows 发布打包修复**:Inno Setup 安装包文件名带版本号(Hua.Todo_Setup_vX.Y.Z.exe);安装后快捷方式/启动项指向 Hua.Todo.Maui.exe;发布产物强制 IsUsingStatic=true。
|
||||||
- **Windows WebView2 数据目录调整**:MAUI(Unpackaged)默认会在安装目录生成 `Hua.Todo.Maui.exe.WebView2`;现改为写入 `%LocalAppData%\Hua.Todo\WebView2`,避免污染安装目录。
|
- **Windows WebView2 数据目录调整**:MAUI(Unpackaged)默认会在安装目录生成 `Hua.Todo.Maui.exe.WebView2`;现改为写入 `%LocalAppData%\Hua.Todo\WebView2`,避免污染安装目录。
|
||||||
- **Windows WebView2 Runtime 误判修复**:当系统已安装 WebView2 Runtime 但发布产物缺少/裁剪 WebView2 托管程序集时,旧检测逻辑会误判为“未安装”;现改为优先从常见安装目录探测 Evergreen 版本,避免阻断主界面加载。
|
- **Windows WebView2 Runtime 误判修复**:当系统已安装 WebView2 Runtime 但发布产物缺少/裁剪 WebView2 托管程序集时,旧检测逻辑会误判为"未安装";现改为优先从常见安装目录探测 Evergreen 版本,避免阻断主界面加载。
|
||||||
- **Windows 三件套开发体验**:新增 `start-host.ps1` / `start-dev.ps1`,并在 MAUI 中约定 `IsUsingStatic=false` 时不启动内置 WebServer,避免注入覆盖 Vite 的 `/api -> 5173` 代理配置。
|
- **Windows 三件套开发体验**:新增 `start-host.ps1` / `start-dev.ps1`,并在 MAUI 中约定 `IsUsingStatic=false` 时不启动内置 WebServer,避免注入覆盖 Vite 的 `/api -> 5173` 代理配置。
|
||||||
- **文档与部署指南**:新增 `docs/manual/05-部署文档.md`,详细说明开发环境搭建、多平台发布流程(Windows/Linux/Docker)以及关键配置项;并在技术设计文档中建立链接。
|
- **文档与部署指南**:新增部署文档,详细说明开发环境搭建、多平台发布流程(Windows/Linux/Docker)以及关键配置项。
|
||||||
- **用户文档完善**:在规划中新增了 `docs/manual/08-新手指南.md` 和 `docs/manual/09-用户指南.md`。
|
|
||||||
27→
|
### v1.1.1 (2026-04-06)
|
||||||
28→### v1.1.1 (2026-04-06)
|
|
||||||
|
|
||||||
- **文档规范增强**:新增文档同步规则,强制代码变更与文档更新保持同步。
|
- **文档规范增强**:新增文档同步规则,强制代码变更与文档更新保持同步。
|
||||||
- **项目结构说明校准**:修正 README.md 和技术文档中对 `Hua.Todo.Host`、`Hua.Todo.Application` 等模块的路径与职责描述。
|
- **项目结构说明校准**:修正 README.md 和技术文档中对 `Hua.Todo.Host`、`Hua.Todo.Application` 等模块的路径与职责描述。
|
||||||
- **端口配置校准**:修正文档中关于前端与后端 API 的端口说明(5173/5174)。
|
- **端口配置校准**:修正文档中关于前端与后端 API 的端口说明(5173/5174)。
|
||||||
- **PRD 校准**:移除 v1.2.0 PRD 中“本地迭代不支持”表述与“数据迁移(导入/导出)”小节。
|
- **PRD 校准**:移除 v1.2.0 PRD 中"本地迭代不支持"表述与"数据迁移(导入/导出)"小节。
|
||||||
- **PRD 校准**:移除 v1.2.0 PRD 中“云同步”需求。
|
- **PRD 校准**:移除 v1.2.0 PRD 中"云同步"需求。
|
||||||
|
|
||||||
### v1.1.0 更新内容
|
### v1.1.0 更新内容
|
||||||
|
|
||||||
@@ -59,11 +65,6 @@
|
|||||||
- 使用 SQLite 作为本地数据库
|
- 使用 SQLite 作为本地数据库
|
||||||
- 实现子任务支持
|
- 实现子任务支持
|
||||||
|
|
||||||
### v1.2.0 规划内容 (即将推出)
|
### v1.0.0 初始版本
|
||||||
|
|
||||||
- **Linux 官方支持**:正式适配 Linux 平台。
|
- 初始 WPF 版本。
|
||||||
- **Linux 打包与交付**:新增 `.tar.gz` 发布脚本与 Flatpak(manifest/desktop entry/AppStream)基础结构。
|
|
||||||
- **关键词检索**:支持按任务标题关键词搜索。
|
|
||||||
- **标签系统**:引入多标签支持,提升任务组织效率。
|
|
||||||
- **暗色模式**:全平台适配暗色/深色主题。
|
|
||||||
- **数据导出导入(后续)**:支持 JSON 格式数据备份与迁移(延期到后续版本)。
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# 其他信息
|
# 其他信息
|
||||||
|
|
||||||
## 🤝 贡献指南
|
## 贡献指南
|
||||||
|
|
||||||
1. Fork 项目
|
1. Fork 项目
|
||||||
2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
|
2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
|
||||||
@@ -8,11 +8,11 @@
|
|||||||
4. 推送到分支 (`git push origin feature/AmazingFeature`)
|
4. 推送到分支 (`git push origin feature/AmazingFeature`)
|
||||||
5. 打开 Pull Request
|
5. 打开 Pull Request
|
||||||
|
|
||||||
## 📄 许可证
|
## 许可证
|
||||||
|
|
||||||
本项目采用 AGPL-3.0 许可证 - 查看 [LICENSE](LICENSE) (英文) 或 [LICENSE.zh-CN](LICENSE.zh-CN) (中文) 文件了解详情
|
本项目采用 AGPL-3.0 许可证 - 查看 [LICENSE](LICENSE) (英文) 或 [LICENSE.zh-CN](LICENSE.zh-CN) (中文) 文件了解详情
|
||||||
|
|
||||||
## 📞 联系方式
|
## 联系方式
|
||||||
|
|
||||||
- 项目作者:ShaoHua
|
- 项目作者:ShaoHua
|
||||||
- 项目地址:https://git.we965.cn/Tools/Hua.Todo
|
- 项目地址:https://git.we965.cn/Tools/Hua.Todo
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
# 技术栈与项目结构
|
||||||
|
|
||||||
|
> 本文档面向开发者,介绍 Hua.Todo 的技术选型、项目划分、模块职责与依赖关系。
|
||||||
|
|
||||||
|
## 1. 技术栈
|
||||||
|
|
||||||
|
### 1.1 后端
|
||||||
|
|
||||||
|
| 技术 | 版本/说明 |
|
||||||
|
|---|---|
|
||||||
|
| 开发语言 | C# 13 |
|
||||||
|
| 框架 | .NET 10 |
|
||||||
|
| UI 框架 | MAUI(移动端/部分桌面)+ Avalonia(桌面端) |
|
||||||
|
| Web 服务器 | Kestrel(ASP.NET Core 内置) |
|
||||||
|
| API 框架 | ASP.NET Core Web API(含动态 API 生成) |
|
||||||
|
| ORM | Entity Framework Core 10.0 |
|
||||||
|
| 数据库 | SQLite(本地存储) |
|
||||||
|
| 依赖注入 | Microsoft.Extensions.DependencyInjection |
|
||||||
|
| 日志 | Serilog |
|
||||||
|
|
||||||
|
### 1.2 前端
|
||||||
|
|
||||||
|
| 技术 | 版本/说明 |
|
||||||
|
|---|---|
|
||||||
|
| 开发语言 | TypeScript 5+ |
|
||||||
|
| 框架 | Vue.js 3 |
|
||||||
|
| 构建工具 | Vite 5+ |
|
||||||
|
| HTTP 客户端 | Axios |
|
||||||
|
| 状态管理 | Pinia |
|
||||||
|
| UI 组件库 | Element Plus / Vant(移动端) |
|
||||||
|
| CSS 预处理器 | SCSS |
|
||||||
|
|
||||||
|
## 2. 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
Hua.Todo/
|
||||||
|
├── src/
|
||||||
|
│ ├── Hua.Todo.Core/ # 领域实体、枚举、仓储接口
|
||||||
|
│ ├── Hua.Todo.Application/ # 业务逻辑、EF Core、动态 API、云同步
|
||||||
|
│ ├── Hua.Todo.Host/ # 独立服务端宿主(ASP.NET)
|
||||||
|
│ ├── Hua.Todo.Maui/ # MAUI 客户端(Windows/macOS/Android/iOS)
|
||||||
|
│ ├── Hua.Todo.Avalonia/ # Avalonia 客户端(Linux/Windows 桌面)
|
||||||
|
│ ├── Hua.Todo.Web/ # Vue 3 前端(Vite)
|
||||||
|
│ └── test/ # 测试项目
|
||||||
|
│ ├── Hua.Todo.Host.Tests/ # Host 服务端测试
|
||||||
|
│ ├── Hua.Todo.Maui.Tests/ # MAUI 客户端测试
|
||||||
|
│ └── Hua.Todo.Avalonia.Tests/ # Avalonia 客户端测试
|
||||||
|
├── docs/
|
||||||
|
│ ├── manual/ # 项目手册
|
||||||
|
│ ├── project/ # 产品需求文档与研发工单
|
||||||
|
│ └── AI沟通记录/ # AI 对话记录
|
||||||
|
└── .trae/ # 智能体规则与记忆
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 核心模块说明
|
||||||
|
|
||||||
|
### 3.1 Hua.Todo.Core
|
||||||
|
领域实体层,定义核心实体与接口:
|
||||||
|
- **实体**:`TaskEntity`、`UserEntity`、`SecurityPolicyEntity`、`AuditLogEntity`
|
||||||
|
- **枚举**:`TaskPriority`
|
||||||
|
- **仓储接口**:`ITaskRepository`
|
||||||
|
- **语音控制接口**:`IVoiceInputService`、`IVoiceOutputService`、`IVoiceIntentParser`
|
||||||
|
|
||||||
|
### 3.2 Hua.Todo.Application
|
||||||
|
应用层实现,所有业务逻辑的集中地:
|
||||||
|
- **TaskService**:待办项 CRUD 业务逻辑
|
||||||
|
- **动态 API**(`DynamicApi/`):基于接口自动生成 RESTful API
|
||||||
|
- **云同步**(`CloudSync/`):认证服务、同步服务、安全策略管理
|
||||||
|
- **语音控制**(`Voice/`):LLM 客户端、双策略意图解析器、指令执行器
|
||||||
|
- **MCP 服务**(`Mcp/`):自动扫描并暴露 MCP 工具
|
||||||
|
- **数据访问**:`TodoDbContext` + EF Core 迁移
|
||||||
|
|
||||||
|
### 3.3 Hua.Todo.Host
|
||||||
|
独立服务端宿主,同时注册业务服务与云同步端点:
|
||||||
|
- `AddApplicationServices()`:待办项 CRUD + 动态 API
|
||||||
|
- `AddCloudSyncServer()`:认证、同步、安全策略端点
|
||||||
|
- `AddMcpServerServices()`:MCP 协议端点(`/mcp`)
|
||||||
|
- `AddVoiceServices()`:语音控制端点
|
||||||
|
|
||||||
|
### 3.4 Hua.Todo.Maui
|
||||||
|
跨平台客户端,内嵌 Kestrel WebServer + WebView 承载前端:
|
||||||
|
- 仅注册 `AddApplicationServices()`,不暴露云同步端点
|
||||||
|
- 平台特定服务(快捷键、通知等)
|
||||||
|
|
||||||
|
### 3.5 Hua.Todo.Avalonia
|
||||||
|
桌面客户端(Avalonia + WebView),提供 Linux/Windows 桌面形态:
|
||||||
|
- 同样通过嵌入式 WebServer + WebView 承载前端
|
||||||
|
- 托盘菜单、全局热键等桌面交互功能
|
||||||
|
|
||||||
|
### 3.6 Hua.Todo.Web
|
||||||
|
Vue 3 前端项目,同一份构建产物被多个宿主以静态资源形式承载:
|
||||||
|
- **组件**:TaskList、TaskItem、TaskEditDialog、CloudSyncSettings 等
|
||||||
|
- **状态管理**:Pinia stores
|
||||||
|
- **API 层**:`api/tasks.ts`、`api/cloudSync.ts`、`api/mcp.ts`
|
||||||
|
|
||||||
|
## 4. 依赖方向
|
||||||
|
|
||||||
|
```
|
||||||
|
Hua.Todo.Core
|
||||||
|
↑
|
||||||
|
Hua.Todo.Application
|
||||||
|
↑
|
||||||
|
├── Hua.Todo.Host (服务端:全部能力)
|
||||||
|
├── Hua.Todo.Maui (客户端:仅业务服务)
|
||||||
|
└── Hua.Todo.Avalonia (客户端:仅业务服务)
|
||||||
|
|
||||||
|
Hua.Todo.Web (无 .NET 依赖;通过 HTTP 调用上述任一宿主)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **禁止反向依赖**:Core 不得引用 Application;Application 不得引用任何宿主项目
|
||||||
|
- **客户端不暴露云同步端点**:MAUI / Avalonia 只调用 `AddApplicationServices()`
|
||||||
|
|
||||||
|
## 5. 关键扩展点
|
||||||
|
|
||||||
|
| 扩展点 | 位置 |
|
||||||
|
|---|---|
|
||||||
|
| DI 注册总入口 | `Hua.Todo.Application/ServiceCollectionExtensions.cs` |
|
||||||
|
| 云同步 DI | `Hua.Todo.Application/CloudSync/CloudSyncServiceCollectionExtensions.cs` |
|
||||||
|
| 动态 API 中间件 | `Hua.Todo.Application/DynamicApi/DynamicApiMiddleware.cs` |
|
||||||
|
| MCP 工具注册 | `Hua.Todo.Application/Mcp/DynamicMcpToolExtensions.cs` |
|
||||||
|
| 语音意图解析 | `Hua.Todo.Application/Voice/HybridVoiceIntentParser.cs` |
|
||||||
|
| 嵌入式 WebServer | `Hua.Todo.{Maui,Avalonia}/Services/EmbeddedWebServerService.cs` |
|
||||||
|
|
||||||
|
## 6. 下一步
|
||||||
|
|
||||||
|
- 搭建开发环境 → [06-开发环境与构建](./06-开发环境与构建.md)
|
||||||
|
- 深入架构设计 → [07-技术架构设计](./07-技术架构设计.md)
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
# Hua.Todo 构建与部署手册
|
|
||||||
|
|
||||||
本文档提供 Hua.Todo 项目的版本构建指南与多平台部署流程,侧重于如何产出可分发的版本并将其安装或部署到目标机器。
|
|
||||||
|
|
||||||
## 1. 🏗️ 版本构建流程 (Release Build)
|
|
||||||
|
|
||||||
在进行任何部署之前,必须先通过自动化脚本构建出 Release 版本的产物。
|
|
||||||
|
|
||||||
### 1.1 前提要求
|
|
||||||
|
|
||||||
- **环境检查**:已安装 .NET 10 SDK、Node.js 18+ 以及 Inno Setup 6(仅 Windows 打包需要)。
|
|
||||||
- **脚本入口**:位于根目录的 `publish.ps1` 系列脚本。
|
|
||||||
|
|
||||||
### 1.2 执行构建
|
|
||||||
|
|
||||||
根据目标平台执行对应的构建命令:
|
|
||||||
|
|
||||||
- **全平台一键构建**:
|
|
||||||
```powershell
|
|
||||||
.\publish.ps1 -Windows -Linux
|
|
||||||
```
|
|
||||||
- **Windows 版本构建**:
|
|
||||||
```powershell
|
|
||||||
.\publish-windows.ps1
|
|
||||||
```
|
|
||||||
- **Linux 版本构建**:
|
|
||||||
```powershell
|
|
||||||
.\publish-linux.ps1 -RuntimeIdentifier linux-x64 -SelfContained
|
|
||||||
```
|
|
||||||
|
|
||||||
### 1.3 产物输出位置
|
|
||||||
|
|
||||||
所有构建产物将按平台归类在根目录的 `artifacts/` 文件夹下:
|
|
||||||
|
|
||||||
- **Windows**: `artifacts\windows\win-x64\installer\` (安装包) 与 `publish\` (解压即用版)
|
|
||||||
- **Linux**: `artifacts\linux\linux-x64\` (`.tar.gz` 压缩包)
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
## 2. 🪟 Windows 客户端部署
|
|
||||||
|
|
||||||
### 2.1 安装包分发 (Recommended)
|
|
||||||
|
|
||||||
1. **分发文件**:将 `hua.todo-{version}-win-x64-setup.exe` 提供给终端用户。
|
|
||||||
2. **安装流程**:用户运行安装程序,程序将自动:
|
|
||||||
- 安装到 `%ProgramFiles%\Hua.Todo`(或用户自定义路径)。
|
|
||||||
- 创建桌面与开始菜单快捷方式。
|
|
||||||
- 写入注册表以支持卸载。
|
|
||||||
3. **静默安装**:支持 Inno Setup 标准静默参数 `/VERYSILENT /SUPPRESSMSGBOXES`。
|
|
||||||
|
|
||||||
### 2.2 绿色版部署
|
|
||||||
|
|
||||||
1. **分发文件**:将 `artifacts\windows\win-x64\publish\` 文件夹整体打包。
|
|
||||||
2. **运行要求**:目标机器需安装 **WebView2 Runtime**。
|
|
||||||
3. **启动程序**:运行 `Hua.Todo.Maui.exe`。
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
## 3. 🐧 Linux 客户端部署
|
|
||||||
|
|
||||||
目前提供基于 Avalonia 的 Linux 交付版本,采用 `.tar.gz` 压缩包形式。
|
|
||||||
|
|
||||||
### 3.1 解压部署
|
|
||||||
|
|
||||||
1. **解压**:
|
|
||||||
```bash
|
|
||||||
tar -xzf hua.todo-{version}-linux-x64.tar.gz -C /opt/hua-todo
|
|
||||||
```
|
|
||||||
2. **运行环境**:
|
|
||||||
- 确保系统安装了 `libwebkit2gtk-4.0-37`。
|
|
||||||
- 如果构建时未开启 `-SelfContained`,则需安装 .NET 10 Runtime。
|
|
||||||
3. **权限设置**:
|
|
||||||
```bash
|
|
||||||
chmod +x /opt/hua-todo/Hua.Todo.Avalonia
|
|
||||||
```
|
|
||||||
4. **启动**:直接运行 `/opt/hua-todo/Hua.Todo.Avalonia`。
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
## 4. ☁️ 云同步服务端部署 (Server)
|
|
||||||
|
|
||||||
`Hua.Todo.Host` 可作为中心服务端部署,用于任务的云端同步。
|
|
||||||
|
|
||||||
### 4.1 Docker 部署 (Recommended)
|
|
||||||
|
|
||||||
1. **上传代码**或将 `src/Hua.Todo.Host/Dockerfile` 复制到服务器。
|
|
||||||
2. **构建与启动**:
|
|
||||||
```bash
|
|
||||||
docker build -t hua-todo-server -f src/Hua.Todo.Host/Dockerfile .
|
|
||||||
docker run -d \
|
|
||||||
--name hua-todo-server \
|
|
||||||
-p 5173:5173 \
|
|
||||||
-v /data/hua-todo/db:/app/data \
|
|
||||||
-e ASPNETCORE_ENVIRONMENT=Production \
|
|
||||||
hua-todo-server
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.2 直接部署 (Binary)
|
|
||||||
|
|
||||||
1. **构建 Host**:`dotnet publish src/Hua.Todo.Host -c Release -o ./dist`。
|
|
||||||
2. **同步产物**:将 `./dist` 内容同步到服务器。
|
|
||||||
3. **配置 Systemd 服务**(可选):
|
|
||||||
创建一个 `hua-todo.service` 文件,指向可执行程序并设置工作目录。
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
## 5. ⚙️ 关键配置校准
|
|
||||||
|
|
||||||
### 5.1 数据持久化
|
|
||||||
|
|
||||||
- **SQLite 路径**:客户端数据默认存储在用户目录下的 `Hua.Todo/todo.db`;服务端数据存储在容器挂载卷或指定 `appsettings.json` 的连接字符串中。
|
|
||||||
- **备份建议**:定期备份 `todo.db` 文件。
|
|
||||||
|
|
||||||
### 5.2 端口与访问
|
|
||||||
|
|
||||||
- **客户端本地端口**:默认 `5057`。如果被占用,可在 `appsettings.json` 中修改 `WebServer.HostUrl`。
|
|
||||||
- **服务端访问**:确保服务端防火墙已开放对应的 API 端口(默认 5173)。
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
## 6. 🛠️ 常见问题排查
|
|
||||||
|
|
||||||
- **客户端无法加载 UI**:检查目标机器是否安装了 WebView2 Runtime(Windows)或 WebKitGTK(Linux)。
|
|
||||||
- **同步连接失败**:
|
|
||||||
1. 确认服务端 API 是否可达(访问 `/swagger` 验证)。
|
|
||||||
2. 确认客户端配置的服务端地址格式(需包含 `http://` 或 `https://`)。
|
|
||||||
- **权限问题**:在 Linux 下运行前,务必执行 `chmod +x` 给主程序执行权限。
|
|
||||||
|
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
# 开发环境与构建
|
||||||
|
|
||||||
|
> 本文档面向开发者,介绍如何搭建开发环境、从源码构建和部署。
|
||||||
|
|
||||||
|
## 1. 开发环境搭建
|
||||||
|
|
||||||
|
### 1.1 前提要求
|
||||||
|
|
||||||
|
- .NET 10 SDK
|
||||||
|
- Node.js 18+
|
||||||
|
- Visual Studio 2022+(推荐)或 VS Code
|
||||||
|
- Inno Setup 6(仅 Windows 打包需要)
|
||||||
|
|
||||||
|
### 1.2 克隆与安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://git.we965.cn/Tools/Hua.Todo.git
|
||||||
|
cd Hua.Todo
|
||||||
|
```
|
||||||
|
|
||||||
|
前端依赖安装:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd src/Hua.Todo.Web
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 启动开发环境
|
||||||
|
|
||||||
|
**方式一:Windows 三件套(推荐)**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# 终端 1:启动 Host(API 服务端)
|
||||||
|
.\start-host.ps1
|
||||||
|
|
||||||
|
# 终端 2:启动前端开发服务器
|
||||||
|
.\start-dev.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
**方式二:MAUI 嵌入模式**
|
||||||
|
|
||||||
|
在 Visual Studio 中直接调试运行 `Hua.Todo.Maui` 项目,MAUI 会自动启动内嵌 WebServer 并加载前端。
|
||||||
|
|
||||||
|
## 2. 版本构建 (Release Build)
|
||||||
|
|
||||||
|
### 2.1 脚本入口
|
||||||
|
|
||||||
|
位于根目录的 `publish.ps1` 系列脚本:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# 全平台一键构建
|
||||||
|
.\publish.ps1 -Windows -Linux
|
||||||
|
|
||||||
|
# Windows 单独构建
|
||||||
|
.\publish-windows.ps1
|
||||||
|
|
||||||
|
# Linux 单独构建
|
||||||
|
.\publish-linux.ps1 -RuntimeIdentifier linux-x64 -SelfContained
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 产物输出位置
|
||||||
|
|
||||||
|
| 平台 | 路径 |
|
||||||
|
|---|---|
|
||||||
|
| Windows 安装包 | `artifacts\windows\win-x64\installer\` |
|
||||||
|
| Windows 绿色版 | `artifacts\windows\win-x64\publish\` |
|
||||||
|
| Linux | `artifacts\linux\linux-x64\`(`.tar.gz`) |
|
||||||
|
|
||||||
|
## 3. 服务端部署
|
||||||
|
|
||||||
|
### 3.1 Docker 部署(推荐)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t hua-todo-server -f src/Hua.Todo.Host/Dockerfile .
|
||||||
|
docker run -d \
|
||||||
|
--name hua-todo-server \
|
||||||
|
-p 5173:5173 \
|
||||||
|
-v /data/hua-todo/db:/app/data \
|
||||||
|
-e ASPNETCORE_ENVIRONMENT=Production \
|
||||||
|
hua-todo-server
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 直接部署
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet publish src/Hua.Todo.Host -c Release -o ./dist
|
||||||
|
# 将 ./dist 同步到服务器,可选配置 Systemd 服务
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 数据库
|
||||||
|
|
||||||
|
- **开发/测试**:SQLite,路径 `src/Hua.Todo.Host/Hua.Todo.db`
|
||||||
|
- **生产**:SQLite(默认)或切换其他 EF Core 支持的数据库
|
||||||
|
- **迁移**:嵌入式宿主启动时自动执行 `db.Database.Migrate()`
|
||||||
|
|
||||||
|
## 5. 下一步
|
||||||
|
|
||||||
|
- 了解架构细节 → [07-技术架构设计](./07-技术架构设计.md)
|
||||||
|
- 了解云同步 → [08-云同步规则](./08-云同步规则.md)
|
||||||
|
- 编码规范 → [09-代码规范](./09-代码规范.md)
|
||||||
@@ -0,0 +1,390 @@
|
|||||||
|
# 技术架构设计
|
||||||
|
|
||||||
|
> 本文档面向开发者,描述 Hua.Todo 的详细技术架构,包括目录结构、模块设计、API 端点、数据库设计与通信机制。
|
||||||
|
|
||||||
|
## 1. 项目目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
Hua.Todo/
|
||||||
|
├── docs/ # 文档目录
|
||||||
|
│ ├── manual/ # 用户/开发者手册
|
||||||
|
│ │ ├── 00-目录与导读.md
|
||||||
|
│ │ ├── 01-项目介绍.md
|
||||||
|
│ │ ├── 02-安装指南.md
|
||||||
|
│ │ ├── 03-版本记录.md
|
||||||
|
│ │ ├── 04-其他信息.md
|
||||||
|
│ │ ├── 05-技术栈与项目结构.md
|
||||||
|
│ │ ├── 06-开发环境与构建.md
|
||||||
|
│ │ ├── 07-技术架构设计.md(本文件)
|
||||||
|
│ │ ├── 08-云同步规则.md
|
||||||
|
│ │ ├── 09-代码规范.md
|
||||||
|
│ │ └── 10-MCP服务集成.md
|
||||||
|
│ └── project/ # 项目进度/需求文档
|
||||||
|
│
|
||||||
|
├── src/ # 源代码目录
|
||||||
|
│ ├── Hua.Todo.Maui/ # MAUI 主项目(跨平台入口)
|
||||||
|
│ │ ├── Platforms/ # 平台特定代码
|
||||||
|
│ │ │ ├── Windows/ # Windows 平台代码
|
||||||
|
│ │ │ │ ├── App.xaml
|
||||||
|
│ │ │ │ └── Services/
|
||||||
|
│ │ │ │ └── HotKeyService.cs
|
||||||
|
│ │ │ ├── MacCatalyst/ # macOS 平台代码
|
||||||
|
│ │ │ ├── Android/ # Android 平台代码
|
||||||
|
│ │ │ └── iOS/ # iOS 平台代码
|
||||||
|
│ │ ├── Resources/ # 资源文件
|
||||||
|
│ │ ├── Controls/ # 自定义控件
|
||||||
|
│ │ │ └── WebViewContainer.xaml
|
||||||
|
│ │ ├── Services/ # 服务层
|
||||||
|
│ │ │ ├── IHotKeyService.cs
|
||||||
|
│ │ │ ├── IPlatformService.cs
|
||||||
|
│ │ │ └── AppLifecycleService.cs
|
||||||
|
│ │ ├── App.xaml / App.xaml.cs
|
||||||
|
│ │ ├── MauiProgram.cs
|
||||||
|
│ │ └── Hua.Todo.Maui.csproj
|
||||||
|
│ │
|
||||||
|
│ ├── Hua.Todo.Avalonia/ # Avalonia 项目(Linux 支持)
|
||||||
|
│ │ ├── Services/
|
||||||
|
│ │ │ ├── EmbeddedWebServerServiceFactory.cs
|
||||||
|
│ │ │ ├── GlobalHotKeyServiceFactory.cs
|
||||||
|
│ │ │ └── Platforms/
|
||||||
|
│ │ ├── Views/
|
||||||
|
│ │ │ ├── MainView.axaml / MainView.axaml.cs
|
||||||
|
│ │ │ └── MainWindow.axaml / MainWindow.axaml.cs
|
||||||
|
│ │ ├── App.axaml / App.axaml.cs
|
||||||
|
│ │ ├── Program.cs
|
||||||
|
│ │ ├── appsettings.json
|
||||||
|
│ │ ├── setup.iss
|
||||||
|
│ │ ├── wwwroot/
|
||||||
|
│ │ └── Hua.Todo.Avalonia.csproj
|
||||||
|
│ │
|
||||||
|
│ ├── Hua.Todo.Host/ # 独立服务端
|
||||||
|
│ │ ├── Program.cs # API 入口
|
||||||
|
│ │ ├── appsettings.json
|
||||||
|
│ │ └── Hua.Todo.Host.csproj
|
||||||
|
│ │
|
||||||
|
│ ├── Hua.Todo.Application/ # 应用层
|
||||||
|
│ │ ├── Data/
|
||||||
|
│ │ │ ├── TodoDbContext.cs
|
||||||
|
│ │ │ └── Migrations/
|
||||||
|
│ │ ├── DynamicApi/
|
||||||
|
│ │ ├── CloudSync/
|
||||||
|
│ │ ├── Voice/
|
||||||
|
│ │ ├── Mcp/
|
||||||
|
│ │ └── Hua.Todo.Application.csproj
|
||||||
|
│ │
|
||||||
|
│ ├── Hua.Todo.Core/ # 核心业务逻辑层
|
||||||
|
│ │ ├── Entities/
|
||||||
|
│ │ │ ├── TaskEntity.cs
|
||||||
|
│ │ │ ├── UserEntity.cs
|
||||||
|
│ │ │ ├── SecurityPolicyEntity.cs
|
||||||
|
│ │ │ └── AuditLogEntity.cs
|
||||||
|
│ │ ├── Interfaces/
|
||||||
|
│ │ └── Hua.Todo.Core.csproj
|
||||||
|
│ │
|
||||||
|
│ ├── Hua.Todo.Web/ # 前端 Web 项目 (Vue.js)
|
||||||
|
│ │ ├── src/
|
||||||
|
│ │ │ ├── api/ # API 调用
|
||||||
|
│ │ │ │ ├── client.ts
|
||||||
|
│ │ │ │ ├── tasks.ts
|
||||||
|
│ │ │ │ ├── cloudSync.ts
|
||||||
|
│ │ │ │ └── mcp.ts
|
||||||
|
│ │ │ ├── components/ # Vue 组件
|
||||||
|
│ │ │ │ ├── TaskList.vue
|
||||||
|
│ │ │ │ ├── TaskItem.vue
|
||||||
|
│ │ │ │ └── TaskEditDialog.vue
|
||||||
|
│ │ │ ├── composables/ # 组合式函数
|
||||||
|
│ │ │ ├── stores/ # 状态管理 (Pinia)
|
||||||
|
│ │ │ ├── types/ # TypeScript 类型定义
|
||||||
|
│ │ │ ├── utils/ # 工具函数
|
||||||
|
│ │ │ ├── App.vue
|
||||||
|
│ │ │ └── main.ts
|
||||||
|
│ │ ├── package.json
|
||||||
|
│ │ ├── vite.config.ts
|
||||||
|
│ │ ├── tsconfig.json
|
||||||
|
│ │ └── index.html
|
||||||
|
│ │
|
||||||
|
│ └── test/ # 测试项目
|
||||||
|
│ ├── Hua.Todo.Host.Tests/ # Host 服务端测试
|
||||||
|
│ ├── Hua.Todo.Maui.Tests/ # MAUI 客户端测试
|
||||||
|
│ └── Hua.Todo.Avalonia.Tests/ # Avalonia 客户端测试
|
||||||
|
│
|
||||||
|
├── publish.ps1 / publish-windows.ps1 / publish-linux.ps1
|
||||||
|
├── Directory.Build.props
|
||||||
|
├── Hua.Todo.sln
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 模块设计
|
||||||
|
|
||||||
|
### 2.1 Hua.Todo.Maui(跨平台客户端)
|
||||||
|
|
||||||
|
**职责**:
|
||||||
|
- 应用程序入口和生命周期管理
|
||||||
|
- 平台特定功能封装
|
||||||
|
- WebView 容器管理
|
||||||
|
- 本地 HTTP 服务器启动
|
||||||
|
|
||||||
|
**关键组件**:
|
||||||
|
- `MauiProgram.cs`:配置 MAUI 应用和依赖注入
|
||||||
|
- `App.xaml.cs`:应用程序主入口
|
||||||
|
- `WebViewContainer`:封装 WebView 控件
|
||||||
|
- 平台特定服务:快捷键、通知等
|
||||||
|
|
||||||
|
### 2.2 Hua.Todo.Avalonia(桌面客户端)
|
||||||
|
|
||||||
|
**职责**:
|
||||||
|
- Linux 平台支持
|
||||||
|
- 桌面交互功能(托盘菜单、全局热键等)
|
||||||
|
- WebView 容器管理
|
||||||
|
- 本地 HTTP 服务器启动
|
||||||
|
|
||||||
|
**关键组件**:
|
||||||
|
- `Program.cs`:配置 Avalonia 应用和依赖注入
|
||||||
|
- `App.axaml.cs`:应用程序主入口
|
||||||
|
- `MainWindow.axaml.cs`:主窗口管理
|
||||||
|
- `GlobalHotKeyServiceFactory`:全局热键服务工厂
|
||||||
|
- `EmbeddedWebServerServiceFactory`:内嵌 Web 服务器服务工厂
|
||||||
|
|
||||||
|
### 2.3 Hua.Todo.Host(独立服务端)
|
||||||
|
|
||||||
|
**职责**:
|
||||||
|
- 提供 RESTful API 接口
|
||||||
|
- 云同步端点(认证、同步、安全策略)
|
||||||
|
- MCP 协议端点
|
||||||
|
- 语音控制端点
|
||||||
|
|
||||||
|
**接口文档**:
|
||||||
|
- 开发环境下提供 Swagger UI:`http://localhost:5173/swagger`
|
||||||
|
|
||||||
|
**关键组件**:
|
||||||
|
- `CloudSync`:云同步 Minimal APIs(`/auth`、`/tasks`、`/sync`、`/security`)
|
||||||
|
- `DynamicApiMiddleware`:业务接口通过 Dynamic API(`/api/{service}/...`)对外暴露
|
||||||
|
- `MCP`:MCP 协议端点(`/mcp`)
|
||||||
|
- `Program.cs`:API 服务器配置和启动
|
||||||
|
|
||||||
|
### 2.4 Hua.Todo.Core(核心业务层)
|
||||||
|
|
||||||
|
**职责**:
|
||||||
|
- 定义领域模型和业务规则
|
||||||
|
- 提供核心业务接口
|
||||||
|
- 实现领域驱动设计模式
|
||||||
|
|
||||||
|
**关键组件**:
|
||||||
|
- `Entities`:领域实体(TaskEntity、UserEntity、SecurityPolicyEntity、AuditLogEntity)
|
||||||
|
- `Interfaces`:业务接口定义
|
||||||
|
- `Voice`:语音控制相关接口与枚举
|
||||||
|
|
||||||
|
### 2.5 Hua.Todo.Application(应用层)
|
||||||
|
|
||||||
|
**职责**:
|
||||||
|
- 业务逻辑实现
|
||||||
|
- EF Core 数据访问与迁移
|
||||||
|
- 动态 API 生成
|
||||||
|
- 云同步服务
|
||||||
|
- 语音控制服务
|
||||||
|
- MCP 工具注册
|
||||||
|
|
||||||
|
### 2.6 Hua.Todo.Web(前端)
|
||||||
|
|
||||||
|
**职责**:
|
||||||
|
- 用户界面展示
|
||||||
|
- 用户交互处理
|
||||||
|
- HTTP API 调用
|
||||||
|
- 状态管理
|
||||||
|
|
||||||
|
## 3. HTTP API 设计
|
||||||
|
|
||||||
|
### 3.1 API 基础配置
|
||||||
|
|
||||||
|
| 模式 | 基础 URL |
|
||||||
|
|---|---|
|
||||||
|
| 开发 / Host 模式 | `http://localhost:5173/api` |
|
||||||
|
| MAUI 内嵌模式 | `{HostUrl}/api`(默认 `http://localhost:5057/api`) |
|
||||||
|
|
||||||
|
- 数据格式:JSON
|
||||||
|
- 认证方式:云同步端点需要 Bearer Token,本地端点无需认证
|
||||||
|
|
||||||
|
### 3.2 任务管理 API(Dynamic API)
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/task # 获取任务列表
|
||||||
|
GET /api/task/active # 获取未完成任务
|
||||||
|
GET /api/task/completed # 获取已完成任务
|
||||||
|
GET /api/task/{id} # 获取单个任务
|
||||||
|
POST /api/task # 创建任务
|
||||||
|
PUT /api/task # 更新任务
|
||||||
|
DELETE /api/task/{id} # 删除任务
|
||||||
|
PATCH /api/task/{id}/toggle # 切换完成状态
|
||||||
|
GET /api/task/{parentTaskId}/subtasks # 获取子任务列表
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 云同步 API(Host 模式)
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /auth/bootstrap # 初始化管理员(仅首次)
|
||||||
|
POST /auth/login # 登录
|
||||||
|
POST /auth/step-up # 二次验证
|
||||||
|
GET /tasks/ # 获取云端任务
|
||||||
|
POST /sync/ # 推送/拉取合并同步
|
||||||
|
GET /security/policy # 获取安全策略
|
||||||
|
PUT /security/policy # 更新安全策略
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 MCP 端点
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /mcp # MCP JSON-RPC 2.0 端点
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 数据库设计
|
||||||
|
|
||||||
|
### 4.1 核心表结构
|
||||||
|
|
||||||
|
#### Tasks 表
|
||||||
|
```sql
|
||||||
|
CREATE TABLE T_Tasks (
|
||||||
|
Id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
UserId TEXT NOT NULL,
|
||||||
|
Title TEXT NOT NULL,
|
||||||
|
Priority INTEGER NOT NULL DEFAULT 0,
|
||||||
|
IsCompleted INTEGER NOT NULL DEFAULT 0,
|
||||||
|
ParentTaskId INTEGER,
|
||||||
|
CreatedAt TEXT NOT NULL,
|
||||||
|
UpdatedAt TEXT NOT NULL
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Users 表
|
||||||
|
```sql
|
||||||
|
CREATE TABLE T_Users (
|
||||||
|
Id TEXT PRIMARY KEY,
|
||||||
|
UserName TEXT NOT NULL UNIQUE,
|
||||||
|
PasswordHash TEXT NOT NULL,
|
||||||
|
PasswordSalt TEXT NOT NULL,
|
||||||
|
Role TEXT NOT NULL,
|
||||||
|
MustChangePassword INTEGER NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### SecurityPolicies 表
|
||||||
|
```sql
|
||||||
|
CREATE TABLE T_SecurityPolicies (
|
||||||
|
Id TEXT PRIMARY KEY,
|
||||||
|
UserId TEXT NOT NULL,
|
||||||
|
AllowPersist INTEGER NOT NULL DEFAULT 1,
|
||||||
|
AllowSync INTEGER NOT NULL DEFAULT 1,
|
||||||
|
SecondFactorExpiryMinutes INTEGER DEFAULT 0,
|
||||||
|
IsTrustedDeviceOnly INTEGER NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### AuditLogs 表
|
||||||
|
```sql
|
||||||
|
CREATE TABLE T_AuditLogs (
|
||||||
|
Id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
UserId TEXT,
|
||||||
|
Action TEXT NOT NULL,
|
||||||
|
OccurredAtUtc TEXT NOT NULL,
|
||||||
|
Details TEXT
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 数据访问策略
|
||||||
|
|
||||||
|
- 使用 Entity Framework Core 进行数据访问
|
||||||
|
- 采用 Repository 模式封装数据访问
|
||||||
|
- 支持 LINQ 查询和异步操作
|
||||||
|
- EF Core 迁移管理(见 `Hua.Todo.Application/Migrations/`)
|
||||||
|
- 表名遵循 ABP 规范:`T_{实体名}s`
|
||||||
|
|
||||||
|
## 5. 通信机制
|
||||||
|
|
||||||
|
### 5.1 HTTP 通信流程
|
||||||
|
|
||||||
|
1. **C# 后端启动**:MAUI/Avalonia 应用启动时启动本地 Kestrel 服务器
|
||||||
|
2. **Vue 前端加载**:WebView 加载 Vue 应用
|
||||||
|
3. **API 调用**:Vue 通过 Axios 调用本地 HTTP API
|
||||||
|
4. **数据处理**:C# 后端处理请求并返回 JSON 数据
|
||||||
|
5. **界面更新**:Vue 接收响应并更新界面
|
||||||
|
|
||||||
|
### 5.2 错误处理
|
||||||
|
|
||||||
|
- 统一的错误响应格式
|
||||||
|
- 异常中间件捕获和处理
|
||||||
|
- 前端错误提示和重试机制
|
||||||
|
|
||||||
|
### 5.3 WebView 注入
|
||||||
|
|
||||||
|
前端通过 `window` 对象接收宿主注入的信息:
|
||||||
|
- `window.__API_BASE_URL__`:API 基础路径
|
||||||
|
- `window.mauiInterop`:MAUI 平台互操作对象
|
||||||
|
|
||||||
|
## 6. 部署和打包
|
||||||
|
|
||||||
|
### 6.1 开发环境
|
||||||
|
|
||||||
|
- **后端调试**:使用 Visual Studio 调试 MAUI/Avalonia 应用
|
||||||
|
- **前端调试**:使用 Vite 开发服务器
|
||||||
|
- **热重载**:支持前后端热重载
|
||||||
|
|
||||||
|
### 6.2 生产构建
|
||||||
|
|
||||||
|
- **前端构建**:`npm run build` 生成静态文件
|
||||||
|
- **后端打包**:各平台发布脚本产出可分发包
|
||||||
|
- **静态文件嵌入**:将前端静态文件嵌入到桌面应用中
|
||||||
|
|
||||||
|
详细构建流程见 [06-开发环境与构建](./06-开发环境与构建.md)。
|
||||||
|
|
||||||
|
### 6.3 平台特定配置
|
||||||
|
|
||||||
|
- **Windows**:WebView2 运行时要求
|
||||||
|
- **macOS**:代码签名和公证
|
||||||
|
- **移动端**:应用商店发布配置
|
||||||
|
- **Linux**:需要 GTK + WebKitGTK 运行时依赖
|
||||||
|
|
||||||
|
## 7. 性能优化
|
||||||
|
|
||||||
|
### 7.1 前端优化
|
||||||
|
- 组件懒加载
|
||||||
|
- 虚拟滚动(长列表)
|
||||||
|
- 图片懒加载
|
||||||
|
- 缓存策略
|
||||||
|
|
||||||
|
### 7.2 后端优化
|
||||||
|
- 数据库查询优化(含索引策略)
|
||||||
|
- 响应缓存
|
||||||
|
- 异步处理
|
||||||
|
- SQLite WAL 模式(嵌入式宿主自动开启)
|
||||||
|
|
||||||
|
## 8. 安全考虑
|
||||||
|
|
||||||
|
### 8.1 本地安全
|
||||||
|
- 本地服务器仅监听 localhost
|
||||||
|
- 防止外部访问
|
||||||
|
- 数据加密存储(可选)
|
||||||
|
|
||||||
|
### 8.2 数据安全
|
||||||
|
- 数据库文件权限控制
|
||||||
|
- 定期备份机制
|
||||||
|
- 敏感数据保护
|
||||||
|
|
||||||
|
### 8.3 云同步安全
|
||||||
|
|
||||||
|
详见 [08-云同步规则](./08-云同步规则.md)。
|
||||||
|
|
||||||
|
## 9. 测试策略
|
||||||
|
|
||||||
|
### 9.1 单元测试
|
||||||
|
- 核心业务逻辑测试
|
||||||
|
- 服务层测试
|
||||||
|
- 工具函数测试
|
||||||
|
|
||||||
|
### 9.2 集成测试
|
||||||
|
- API 集成测试
|
||||||
|
- 数据库集成测试
|
||||||
|
- 前后端集成测试
|
||||||
|
|
||||||
|
### 9.3 端到端测试
|
||||||
|
- 跨平台功能测试
|
||||||
|
- 用户流程测试
|
||||||
|
- 性能测试
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Todo 待办项云同步规则
|
# Todo 待办项云同步规则
|
||||||
|
|
||||||
> 本文档汇总 Hua.Todo 项目的 Todo 待办项云同步完整规则,涵盖架构、API 契约、认证鉴权、同步工作流、安全策略与可控落盘等。
|
> 本文档面向开发者,汇总 Hua.Todo 项目的云同步完整规则,涵盖架构、API 契约、认证鉴权、同步工作流、安全策略与可控落盘等。
|
||||||
>
|
>
|
||||||
> 术语:本文中"任务 / Todo 待办项"均为业务实体(对应代码 `Task` / `SubTask` / `TaskEntity`),与编码侧的"研发工单"无关。
|
> 术语:本文中"任务 / Todo 待办项"均为业务实体(对应代码 `Task` / `SubTask` / `TaskEntity`),与编码侧的"研发工单"无关。
|
||||||
|
|
||||||
@@ -509,4 +509,4 @@ Hua.Todo.Application (共享层)
|
|||||||
> - 安全与可控落盘:[06-CloudSync-安全与可控落盘.md](../project/研发工单-v1.2.0/06-CloudSync-安全与可控落盘.md)
|
> - 安全与可控落盘:[06-CloudSync-安全与可控落盘.md](../project/研发工单-v1.2.0/06-CloudSync-安全与可控落盘.md)
|
||||||
> - 安全设计方案:[06.1-CloudSync-服务端安全设计方案.md](../project/研发工单-v1.2.0/06.1-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)
|
> - 同源 Host 重构:[08-cloud_sync_refactor_plan.md](../project/研发工单-v1.2.0/08-cloud_sync_refactor_plan.md)
|
||||||
> - 技术设计文档:[技术设计文档.md](./01-技术设计文档.md)
|
> - 技术架构设计:[07-技术架构设计](./07-技术架构设计.md)
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
# Hua.Todo 代码规范文档 v1.2.8
|
# 代码规范
|
||||||
|
|
||||||
|
> 本文档面向开发者,定义 Hua.Todo 项目的编码规范。
|
||||||
|
|
||||||
## 1. 概述
|
## 1. 概述
|
||||||
本文档定义 Hua.Todo 项目的代码规范,包括 C#、JavaScript/TypeScript、Vue.js 和其他相关技术的编码标准。遵循这些规范有助于提高代码质量、可读性和可维护性。
|
本文档定义 Hua.Todo 项目的代码规范,包括 C#、JavaScript/TypeScript、Vue.js 和其他相关技术的编码标准。遵循这些规范有助于提高代码质量、可读性和可维护性。
|
||||||
@@ -12,9 +14,11 @@
|
|||||||
|
|
||||||
### 2.2 注释规范 (强制)
|
### 2.2 注释规范 (强制)
|
||||||
- **公共 API 必须添加 XML 文档注释** (包括 `public` / `protected` 的类、接口、方法、属性)
|
- **公共 API 必须添加 XML 文档注释** (包括 `public` / `protected` 的类、接口、方法、属性)
|
||||||
- ** summary**:一句话说明用途
|
- **summary**:一句话说明用途,不允许重复嵌套 `<summary>` 标签
|
||||||
- ** param / returns**:关键参数/返回值说明
|
- **param / returns**:构造函数和方法的每个参数都必须有对应 `param`,包括可选参数、`logger` 等基础设施参数;有返回值时补充 `returns`
|
||||||
- **异常或副作用**:在 summary 中明确说明(例如会注册系统钩子/会启动后台服务)
|
- **异常或副作用**:在 summary 中明确说明(例如会注册系统钩子/会启动后台服务)
|
||||||
|
- **XML 注释位置**:`///` 文档注释必须紧贴被说明的语言元素;若元素还有特性(如 `[AttributeUsage]`),顺序必须是 XML 注释、特性、类型/成员声明
|
||||||
|
- **cref 使用**:`<see cref="..."/>` 只引用当前项目能解析的类型/成员;跨程序集或未引入命名空间时改用普通文本,避免 CS1574 警告
|
||||||
- **复杂逻辑添加行内注释**
|
- **复杂逻辑添加行内注释**
|
||||||
- **禁止在日志或注释中输出密钥、Token、用户隐私信息**
|
- **禁止在日志或注释中输出密钥、Token、用户隐私信息**
|
||||||
|
|
||||||
@@ -29,7 +33,7 @@
|
|||||||
### 3.1 跨平台逻辑规范
|
### 3.1 跨平台逻辑规范
|
||||||
- **禁止混写 `#if`**:禁止在同一文件内混写多个平台的大段 `#if` 实现。
|
- **禁止混写 `#if`**:禁止在同一文件内混写多个平台的大段 `#if` 实现。
|
||||||
- **优先使用 partial/接口**:应优先使用 `partial` 类、接口与平台目录分离。
|
- **优先使用 partial/接口**:应优先使用 `partial` 类、接口与平台目录分离。
|
||||||
- **说明平台差异**:平台分离后的公共入口处必须说明“平台差异在哪里、默认实现是什么、为什么这么做”。
|
- **说明平台差异**:平台分离后的公共入口处必须说明"平台差异在哪里、默认实现是什么、为什么这么做"。
|
||||||
|
|
||||||
### 3.2 异步/后台任务
|
### 3.2 异步/后台任务
|
||||||
- **必须说明启动时机、错误处理策略、是否需要 UI 线程、以及是否可并发/可重入**。
|
- **必须说明启动时机、错误处理策略、是否需要 UI 线程、以及是否可并发/可重入**。
|
||||||
@@ -78,7 +82,7 @@ public void CreateTask(string title, TaskPriority priority)
|
|||||||
public const int MaxTaskTitleLength = 200;
|
public const int MaxTaskTitleLength = 200;
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3.2 代码组织
|
### 3.3 代码组织
|
||||||
|
|
||||||
#### 文件结构
|
#### 文件结构
|
||||||
```csharp
|
```csharp
|
||||||
@@ -90,10 +94,11 @@ using System.Threading.Tasks;
|
|||||||
// 2. 命名空间
|
// 2. 命名空间
|
||||||
namespace Hua.Todo.Api.Services;
|
namespace Hua.Todo.Api.Services;
|
||||||
|
|
||||||
// 3. XML 文档注释
|
// 3. XML 文档注释(必须位于特性和声明之前)
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 任务服务实现
|
/// 任务服务实现。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[SomeAttribute]
|
||||||
public class TaskService : ITaskService
|
public class TaskService : ITaskService
|
||||||
{
|
{
|
||||||
// 4. 私有字段
|
// 4. 私有字段
|
||||||
@@ -124,7 +129,7 @@ public class TaskService : ITaskService
|
|||||||
- 命名空间结构应与目录结构一致
|
- 命名空间结构应与目录结构一致
|
||||||
- 使用 `.` 分隔层级
|
- 使用 `.` 分隔层级
|
||||||
|
|
||||||
### 3.3 编码规范
|
### 3.4 编码规范
|
||||||
|
|
||||||
#### 异步编程
|
#### 异步编程
|
||||||
```csharp
|
```csharp
|
||||||
@@ -191,7 +196,7 @@ var query = from task in tasks
|
|||||||
select task;
|
select task;
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3.4 文档注释
|
### 3.5 文档注释
|
||||||
```csharp
|
```csharp
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取指定 ID 的任务
|
/// 获取指定 ID 的任务
|
||||||
@@ -0,0 +1,311 @@
|
|||||||
|
# MCP 服务集成
|
||||||
|
|
||||||
|
> 本文档涵盖 Hua.Todo MCP 服务的两部分内容:面向外部系统的接口规范(Part A)与面向内部前端的集成指南(Part B)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part A:MCP 服务接口规范(外部系统接入)
|
||||||
|
|
||||||
|
### A.1 概述
|
||||||
|
|
||||||
|
Hua.Todo 提供了基于 **Model Context Protocol (MCP)** 的服务端,允许外部 MCP 客户端通过 Streamable HTTP 传输协议发现并调用待办项管理工具。
|
||||||
|
|
||||||
|
#### A.1.1 协议与传输
|
||||||
|
|
||||||
|
| 项目 | 说明 |
|
||||||
|
|---|---|
|
||||||
|
| 协议版本 | MCP 2025-03-26(Streamable HTTP) |
|
||||||
|
| 传输方式 | Streamable HTTP(无状态模式) |
|
||||||
|
| 内容格式 | JSON-RPC 2.0 |
|
||||||
|
| 端点路径 | `/mcp` |
|
||||||
|
| 认证 | 暂无(本地模式);生产环境建议通过反向代理添加认证 |
|
||||||
|
|
||||||
|
#### A.1.2 服务端信息
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Hua.Todo MCP Server",
|
||||||
|
"version": "1.0.0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### A.1.3 连接地址
|
||||||
|
|
||||||
|
| 运行模式 | 默认地址 |
|
||||||
|
|---|---|
|
||||||
|
| Hua.Todo.Host(独立服务端) | `http://localhost:5173/mcp` |
|
||||||
|
| MAUI / Avalonia(嵌入式) | `http://localhost:5057/mcp` |
|
||||||
|
|
||||||
|
### A.2 接入方式
|
||||||
|
|
||||||
|
#### A.2.1 MCP 客户端配置示例
|
||||||
|
|
||||||
|
**Claude Desktop / Cursor / 其他 MCP 客户端** 配置文件:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"hua-todo": {
|
||||||
|
"url": "http://localhost:5173/mcp",
|
||||||
|
"transport": "streamable-http"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### A.2.2 手动调用示例(curl)
|
||||||
|
|
||||||
|
**初始化连接**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:5173/mcp \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "MCP-Protocol-Version: 2025-03-26" \
|
||||||
|
-d '{
|
||||||
|
"jsonrpc": "2.0", "id": 1, "method": "initialize",
|
||||||
|
"params": {
|
||||||
|
"protocolVersion": "2025-03-26",
|
||||||
|
"capabilities": {},
|
||||||
|
"clientInfo": { "name": "my-client", "version": "1.0.0" }
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**列出可用工具**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:5173/mcp \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "MCP-Protocol-Version: 2025-03-26" \
|
||||||
|
-d '{"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**调用工具**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:5173/mcp \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "MCP-Protocol-Version: 2025-03-26" \
|
||||||
|
-d '{
|
||||||
|
"jsonrpc": "2.0", "id": 3, "method": "tools/call",
|
||||||
|
"params": { "name": "CreateTodo", "arguments": { "title": "完成项目报告", "priority": "High" } }
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### A.3 工具清单
|
||||||
|
|
||||||
|
#### A.3.1 查询类工具
|
||||||
|
|
||||||
|
**ListAllTodos** — 获取所有待办项列表(含已完成和未完成),无参数。
|
||||||
|
|
||||||
|
**ListActiveTodos** — 获取未完成的待办项列表,无参数。
|
||||||
|
|
||||||
|
**ListCompletedTodos** — 获取已完成的待办项列表,无参数。
|
||||||
|
|
||||||
|
**GetTodoById** — 根据 ID 获取单个待办项详情(含子任务)。
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `id` | `int` | 是 | 待办项 ID |
|
||||||
|
|
||||||
|
**返回示例**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1, "title": "完成项目报告", "priority": "High",
|
||||||
|
"isCompleted": false, "createdAt": "2026-06-16T08:30:00Z",
|
||||||
|
"updatedAt": "2026-06-16T08:30:00Z", "parentTaskId": null,
|
||||||
|
"subTasks": [{ "id": 4, "title": "收集数据", "priority": "Medium", "isCompleted": true, "parentTaskId": 1, "subTasks": [] }]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**ListSubTodos** — 获取指定父待办项下的所有子待办项。
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `parentTaskId` | `int` | 是 | 父待办项 ID |
|
||||||
|
|
||||||
|
#### A.3.2 写入类工具
|
||||||
|
|
||||||
|
**CreateTodo** — 创建新的待办项。
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `title` | `string` | 是 | - | 待办项标题 |
|
||||||
|
| `priority` | `string` | 否 | `"Medium"` | 优先级:`Low` / `Medium` / `High` |
|
||||||
|
| `parentTaskId` | `int` | 否 | `null` | 父待办项 ID |
|
||||||
|
|
||||||
|
**UpdateTodo** — 更新已有待办项的标题或优先级。
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `id` | `int` | 是 | - | 待办项 ID |
|
||||||
|
| `title` | `string` | 否 | `null` | 新标题 |
|
||||||
|
| `priority` | `string` | 否 | `null` | 新优先级 |
|
||||||
|
|
||||||
|
**ToggleTodoComplete** — 切换待办项的完成状态。
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `id` | `int` | 是 | 待办项 ID |
|
||||||
|
|
||||||
|
**DeleteTodo** — 删除指定待办项。
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `id` | `int` | 是 | 待办项 ID |
|
||||||
|
|
||||||
|
### A.4 数据类型定义
|
||||||
|
|
||||||
|
#### TaskDto(待办项)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `id` | `int` | 唯一标识符 |
|
||||||
|
| `title` | `string` | 标题 |
|
||||||
|
| `priority` | `string` | `"Low"` / `"Medium"` / `"High"` |
|
||||||
|
| `isCompleted` | `bool` | 是否已完成 |
|
||||||
|
| `createdAt` | `string` | 创建时间(ISO 8601 UTC) |
|
||||||
|
| `updatedAt` | `string` | 更新时间(ISO 8601 UTC) |
|
||||||
|
| `parentTaskId` | `int?` | 父待办项 ID |
|
||||||
|
| `subTasks` | `TaskDto[]` | 子任务列表 |
|
||||||
|
|
||||||
|
### A.5 与 HTTP API 的对照
|
||||||
|
|
||||||
|
| MCP 工具 | HTTP API | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `ListAllTodos` | `GET /api/task` | 获取全部 |
|
||||||
|
| `ListActiveTodos` | `GET /api/task/active` | 获取未完成 |
|
||||||
|
| `ListCompletedTodos` | `GET /api/task/completed` | 获取已完成 |
|
||||||
|
| `GetTodoById` | `GET /api/task/{id}` | 按 ID 查询 |
|
||||||
|
| `CreateTodo` | `POST /api/task` | 创建 |
|
||||||
|
| `UpdateTodo` | `PUT /api/task` | 更新 |
|
||||||
|
| `ToggleTodoComplete` | `PATCH /api/task/{id}/toggle` | 切换完成 |
|
||||||
|
| `DeleteTodo` | `DELETE /api/task/{id}` | 删除 |
|
||||||
|
| `ListSubTodos` | `GET /api/task/{parentTaskId}/subtasks` | 子任务 |
|
||||||
|
|
||||||
|
> 两套 API 共享同一 `ITaskService` 实现,数据完全一致。
|
||||||
|
|
||||||
|
### A.6 安全建议(生产部署)
|
||||||
|
|
||||||
|
1. **网络隔离**:MCP 端点默认无认证,建议仅在可信网络内暴露,或通过反向代理添加认证
|
||||||
|
2. **CORS 限制**:生产环境应将 CORS 策略从 `AllowAll` 改为指定来源
|
||||||
|
3. **HTTPS**:生产环境必须启用 HTTPS
|
||||||
|
4. **速率限制**:建议对 MCP 端点添加请求速率限制
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part B:MCP 前端集成指南(内部使用)
|
||||||
|
|
||||||
|
> 适用范围:Hua.Todo 前端(Vue / TypeScript)团队。
|
||||||
|
|
||||||
|
### B.1 背景
|
||||||
|
|
||||||
|
v1.3.0 起,Hua.Todo 后端在原有 Dynamic API(`/api/*`)基础上,新增了 MCP 服务端点。前端可根据场景选择:
|
||||||
|
|
||||||
|
| 通道 | 端点 | 适用场景 |
|
||||||
|
|---|---|---|
|
||||||
|
| Dynamic API | `/api/task/*` | WebView 内常规 CRUD、已有逻辑兼容 |
|
||||||
|
| MCP | `/mcp` | AI 辅助、语音指令、外部工具集成 |
|
||||||
|
|
||||||
|
两套通道共享同一 `ITaskService` 业务层,数据一致。
|
||||||
|
|
||||||
|
### B.2 连接方式
|
||||||
|
|
||||||
|
| 模式 | MCP 端点 |
|
||||||
|
|---|---|
|
||||||
|
| 嵌入式(MAUI / Avalonia) | `http://localhost:5057/mcp` |
|
||||||
|
| Host(独立服务端) | `http://<host>:5173/mcp` |
|
||||||
|
|
||||||
|
### B.3 前端 MCP 客户端
|
||||||
|
|
||||||
|
推荐使用官方 TypeScript MCP SDK:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @modelcontextprotocol/sdk
|
||||||
|
```
|
||||||
|
|
||||||
|
**连接示例**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||||
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamablehttp.js";
|
||||||
|
|
||||||
|
const transport = new StreamableHTTPClientTransport(
|
||||||
|
new URL("http://localhost:5057/mcp")
|
||||||
|
);
|
||||||
|
|
||||||
|
const client = new Client({ name: "hua-todo-frontend", version: "1.3.0" });
|
||||||
|
await client.connect(transport);
|
||||||
|
|
||||||
|
// 列出所有可用工具
|
||||||
|
const tools = await client.listTools();
|
||||||
|
|
||||||
|
// 调用工具
|
||||||
|
const result = await client.callTool({ name: "ListActiveTodos", arguments: {} });
|
||||||
|
```
|
||||||
|
|
||||||
|
**封装建议**(`src/api/mcpClient.ts`):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||||
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamablehttp.js";
|
||||||
|
|
||||||
|
const MCP_BASE_URL = window.__API_BASE_URL__
|
||||||
|
? window.__API_BASE_URL__.replace("/api", "/mcp")
|
||||||
|
: "/mcp";
|
||||||
|
|
||||||
|
let client: Client | null = null;
|
||||||
|
|
||||||
|
export async function getMcpClient(): Promise<Client> {
|
||||||
|
if (!client) {
|
||||||
|
const transport = new StreamableHTTPClientTransport(
|
||||||
|
new URL(MCP_BASE_URL, window.location.origin)
|
||||||
|
);
|
||||||
|
client = new Client({ name: "hua-todo-frontend", version: "1.3.0" });
|
||||||
|
await client.connect(transport);
|
||||||
|
}
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function disconnectMcp(): Promise<void> {
|
||||||
|
if (client) { await client.close(); client = null; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### B.4 工具调用映射
|
||||||
|
|
||||||
|
| 业务操作 | Dynamic API | MCP Tool |
|
||||||
|
|---|---|---|
|
||||||
|
| 获取所有待办项 | `GET /api/task` | `ListAllTodos` |
|
||||||
|
| 获取未完成待办项 | `GET /api/task/active` | `ListActiveTodos` |
|
||||||
|
| 获取已完成待办项 | `GET /api/task/completed` | `ListCompletedTodos` |
|
||||||
|
| 获取单个待办项 | `GET /api/task/{id}` | `GetTodoById` |
|
||||||
|
| 创建待办项 | `POST /api/task` | `CreateTodo` |
|
||||||
|
| 更新待办项 | `PUT /api/task` | `UpdateTodo` |
|
||||||
|
| 切换完成状态 | `PATCH /api/task/{id}/toggle` | `ToggleTodoComplete` |
|
||||||
|
| 删除待办项 | `DELETE /api/task/{id}` | `DeleteTodo` |
|
||||||
|
| 获取子待办项 | `GET /api/task/{pid}/subtasks` | `ListSubTodos` |
|
||||||
|
|
||||||
|
### B.5 返回格式差异
|
||||||
|
|
||||||
|
- **列表类** MCP 工具返回可读文本(如 `"未完成待办项(共 2 项):[1] 完成报告..."`)
|
||||||
|
- **单条/创建/更新类** MCP 工具返回 JSON
|
||||||
|
- 前端如需结构化数据,建议仍使用 Dynamic API;MCP 通道主要用于 AI 场景和文本交互
|
||||||
|
|
||||||
|
### B.6 典型场景
|
||||||
|
|
||||||
|
**AI 对话式操作**:用户通过 AI 助手用自然语言操作待办项 → LLM 调用 MCP 工具 → 返回结果。
|
||||||
|
|
||||||
|
**语音指令**:语音 → STT → 文本 → LLM 解析意图 → MCP Tool 调用 → TTS 播报结果。
|
||||||
|
|
||||||
|
### B.7 注意事项
|
||||||
|
|
||||||
|
1. **无状态模式**:当前不支持服务端→客户端通知
|
||||||
|
2. **CORS**:开发环境 `AllowAll` 策略已覆盖 `/mcp`
|
||||||
|
3. **认证**:当前 MCP 端点未接入认证中间件
|
||||||
|
4. **生命周期**:建议在组件 `onUnmounted` 时调用 `disconnectMcp()`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> MCP 服务端实现细节见 [07-技术架构设计](./07-技术架构设计.md) 中 MCP 工具注册部分。
|
||||||
@@ -711,7 +711,7 @@ foreach (var id in request.Deletes)
|
|||||||
| `src/Hua.Todo.Web/src/api/cloudSync.ts` | API 响应类型对齐 | 否 |
|
| `src/Hua.Todo.Web/src/api/cloudSync.ts` | API 响应类型对齐 | 否 |
|
||||||
| `src/Hua.Todo.Web/src/stores/taskStore.ts` | pendingUpserts/pendingDeletes 类型修正 | 否 |
|
| `src/Hua.Todo.Web/src/stores/taskStore.ts` | pendingUpserts/pendingDeletes 类型修正 | 否 |
|
||||||
| `src/Hua.Todo.Web/src/utils/guid.ts` | **新增 Guid 工具函数** | 否 |
|
| `src/Hua.Todo.Web/src/utils/guid.ts` | **新增 Guid 工具函数** | 否 |
|
||||||
| `docs/manual/02-任务同步规则.md` | 文档修正(字段命名) | 是(文档) |
|
| `docs/manual/04-云同步规则.md` | 文档修正(字段命名) | 是(文档) |
|
||||||
| `.trae/rules/项目/03-数据模型与迁移约束.md` | 实体清单更新 | 是(规则) |
|
| `.trae/rules/项目/03-数据模型与迁移约束.md` | 实体清单更新 | 是(规则) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -6,12 +6,13 @@
|
|||||||
|
|
||||||
## 一、背景与目标
|
## 一、背景与目标
|
||||||
|
|
||||||
Hua.Todo v1.3.0 版本聚焦于两个核心能力的升级:
|
Hua.Todo v1.3.0 版本聚焦于三个核心能力的升级:
|
||||||
|
|
||||||
| 序号 | 能力 | 描述 |
|
| 序号 | 能力 | 描述 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 1 | **MCP 服务映射** | 将现有 HTTP 服务转换为 MCP(Model Context Protocol)服务,提升服务调用效率与可扩展性 |
|
| 1 | **MCP 服务映射** | 将现有 HTTP 服务转换为 MCP(Model Context Protocol)服务,提升服务调用效率与可扩展性 |
|
||||||
| 2 | **语音交互** | 实现语音通话数据传输与语音控制功能,支持通过语音指令操作 Todo 待办项 |
|
| 2 | **语音控制与 AI 辅助** | 通过语音指令操作 Todo 待办项(CRUD + 子任务),通过 LLM 提供 AI 辅助任务拆分建议 |
|
||||||
|
| 3 | **会议任务拆分** | 以"会议"为入口记录会议内容(录音/文字),通过 AI 分析自动生成待办项拆分建议,用户确认后批量创建 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -20,11 +21,22 @@ Hua.Todo v1.3.0 版本聚焦于两个核心能力的升级:
|
|||||||
### 2.1 并行工单(可同步执行)
|
### 2.1 并行工单(可同步执行)
|
||||||
|
|
||||||
| 工单编号 | 标题 | 负责人 | 状态 |
|
| 工单编号 | 标题 | 负责人 | 状态 |
|
||||||
|---|---|---|---|
|
|---|---|---|---|---|---|---|
|
||||||
| 01 | HTTP 服务转换为 MCP 服务 | - | 待开始 |
|
| 01 | HTTP 服务转换为 MCP 服务 | - | 已完成 |
|
||||||
| 02 | 语音通话与语音控制功能 | - | 待开始 |
|
| 02 | 语音控制与 AI 辅助 | - | 已完成 |
|
||||||
|
| 03 | 会议任务拆分 | - | 待开始 |
|
||||||
|
| 04 | 富文本描述、附件与外部链接 | - | 已完成 |
|
||||||
|
|
||||||
### 2.2 串行工单(依赖前置工单完成)
|
### 2.2 03 子工单拆分
|
||||||
|
|
||||||
|
| 子工单 | 标题 | 依赖 | 状态 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 03-01 | 会议数据模型与 API | 无 | 待开始 |
|
||||||
|
| 03-02 | 音频录制与转写 | 03-01 | 待开始 |
|
||||||
|
| 03-03 | AI 任务拆分服务 | 03-01、工单 02 LlmClientService | 待开始 |
|
||||||
|
| 03-04 | 任务建议与确认 UI | 03-01、03-03 | 已完成 |
|
||||||
|
|
||||||
|
### 2.3 串行工单(依赖前置工单完成)
|
||||||
|
|
||||||
当前版本暂无串行工单依赖。
|
当前版本暂无串行工单依赖。
|
||||||
|
|
||||||
@@ -45,19 +57,71 @@ Hua.Todo v1.3.0 版本聚焦于两个核心能力的升级:
|
|||||||
- MCP 服务可正常对外提供接口
|
- MCP 服务可正常对外提供接口
|
||||||
- 所有原有 HTTP API 功能在 MCP 服务中可正常使用
|
- 所有原有 HTTP API 功能在 MCP 服务中可正常使用
|
||||||
|
|
||||||
### 3.2 工单 02 - 语音通话与语音控制功能
|
### 3.2 工单 02 - 语音控制与 AI 辅助
|
||||||
|
|
||||||
**目标**:实现语音通话数据传输与语音控制功能
|
**目标**:实现语音控制 Todo 待办项能力与 AI 辅助任务拆分功能
|
||||||
|
|
||||||
**核心需求**:
|
**核心需求**:
|
||||||
- 语音通话数据传输能力
|
- 语音输入(STT):平台原生语音识别 → 文字
|
||||||
- 语音指令识别与解析
|
- 语音播报(TTS):执行结果语音反馈
|
||||||
- 语音控制 Todo 待办项操作(创建、编辑、删除、完成等)
|
- 语音指令解析与执行(CRUD + 子任务 + 歧义处理)
|
||||||
- 与现有业务系统对接
|
- AI 辅助任务拆分(LLM 生成子任务建议,用户确认后批量创建)
|
||||||
|
|
||||||
|
**不包含**:
|
||||||
|
- 语音通话功能(场景不明确,本期不做)
|
||||||
|
|
||||||
**验收标准**:
|
**验收标准**:
|
||||||
- 语音通话功能可正常使用
|
- 各平台 STT/TTS 可正常工作
|
||||||
- 语音控制可准确执行 Todo 业务操作
|
- 语音指令可准确执行 Todo 业务操作
|
||||||
|
- 歧义场景返回候选列表而非直接执行
|
||||||
|
- AI 拆分建议需用户确认后才创建子任务
|
||||||
|
|
||||||
|
### 3.3 工单 03 - 会议任务拆分
|
||||||
|
|
||||||
|
**目标**:以"会议"为入口,通过录音/文字记录会议内容,AI 自动提取行动项生成待办项建议
|
||||||
|
|
||||||
|
**核心需求**:
|
||||||
|
- 新增"会议"类型标记(TaskType.Meeting)
|
||||||
|
- 录音:前端 MediaRecorder → 后端 STT 转写
|
||||||
|
- 文字:直接输入/粘贴会议纪要
|
||||||
|
- AI 会议拆分(LLM 分析会议内容 → 待办项建议列表)
|
||||||
|
- 建议审阅与确认 UI(勾选、编辑、批量创建子任务)
|
||||||
|
|
||||||
|
**不包含**:
|
||||||
|
- 实时语音转写(本期不做)
|
||||||
|
- 音频持久存储(转写完成后删除音频)
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- 可通过"会议"类型创建待办项,显示会议图标
|
||||||
|
- 录音可正常录制并提交转写
|
||||||
|
- 文字纪要可保存/编辑
|
||||||
|
- AI 拆分返回 3-10 条结构化建议,含优先级和原因
|
||||||
|
- 用户可审阅、勾选、编辑建议后批量创建为子任务
|
||||||
|
- 离线模式拒绝 AI 拆分(提示降级)
|
||||||
|
|
||||||
|
### 3.4 工单 04 - 富文本描述、附件与外部链接
|
||||||
|
|
||||||
|
**目标**:为 Todo 待办项新增多行描述、文件附件管理与外部程序/链接启动能力(仅桌面端)
|
||||||
|
|
||||||
|
**核心需求**:
|
||||||
|
- `TaskEntity` 新增 `Description` 多行描述字段
|
||||||
|
- 新增 `AttachmentEntity` 数据模型,支持本地文件上传、下载、删除
|
||||||
|
- 支持外部链接(URL)作为附件,点击在默认浏览器打开
|
||||||
|
- 桌面端通过系统关联程序打开本地附件(`Process.Start` / `xdg-open`)
|
||||||
|
- 前端编辑对话框扩展描述 textarea + 附件管理区域
|
||||||
|
|
||||||
|
**不包含**:
|
||||||
|
- 移动端附件管理(本期仅 Windows/Linux 桌面端)
|
||||||
|
- 附件云同步(后续版本规划)
|
||||||
|
- 附件预览(如图片缩略图,本期不做)
|
||||||
|
- 富文本编辑器(本期仅纯文本)
|
||||||
|
|
||||||
|
**验收标准**:
|
||||||
|
- 描述字段可正常编辑和保存
|
||||||
|
- 附件可上传、下载、删除,文件完整性校验
|
||||||
|
- 外部链接可添加并在浏览器中打开
|
||||||
|
- 本地附件可通过系统关联程序打开
|
||||||
|
- 附件数量(20个)和大小(50MB)限制生效
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -65,12 +129,26 @@ Hua.Todo v1.3.0 版本聚焦于两个核心能力的升级:
|
|||||||
|
|
||||||
| 子工单 | 验证项 | 状态 | 备注 |
|
| 子工单 | 验证项 | 状态 | 备注 |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| 01 | MCP 服务契约文档生成 | 待验证 | - |
|
| 01 | MCP 服务契约文档生成 | 已验证 | 动态工具自动生成,工具名/描述/参数 schema 均已覆盖 |
|
||||||
| 01 | MCP 服务可用性测试 | 待验证 | - |
|
| 01 | MCP 服务可用性测试 | 已验证 | 17 个单元测试全部通过(含工具调用端到端验证) |
|
||||||
| 01 | 原有 API 功能兼容性 | 待验证 | - |
|
| 01 | 原有 API 功能兼容性 | 已验证 | 所有 ITaskService 方法均生成 MCP 工具,覆盖 CRUD 全部 9 个 API |
|
||||||
| 02 | 语音通话连接测试 | 待验证 | - |
|
| 01 | CloudSync MCP 工具 | 已知缺口 | CloudSync 服务未实现 IDynamicApiService,需手动映射或重构 |
|
||||||
|
| 02 | STT/TTS 平台适配 | 待验证 | Windows 优先,其他平台后续 |
|
||||||
| 02 | 语音指令识别准确率 | 待验证 | - |
|
| 02 | 语音指令识别准确率 | 待验证 | - |
|
||||||
| 02 | Todo 业务操作覆盖度 | 待验证 | - |
|
| 02 | 歧义处理正确性 | 待验证 | - |
|
||||||
|
| 02 | AI 拆分建议质量 | 待验证 | - |
|
||||||
|
| 02 | Todo 业务操作覆盖度 | 待验证 | CRUD + 子任务 + 取消完成 |
|
||||||
|
| 03 | 会议数据模型迁移 | 待验证 | TaskType 字段 + DB 迁移 |
|
||||||
|
| 03 | 录音与转写链路 | 待验证 | 录制 → 上传 → 转写 → 保存 |
|
||||||
|
| 03 | AI 会议拆分质量 | 待验证 | 建议含标题+优先级+原因 |
|
||||||
|
| 03 | 建议审阅与批量创建 | 待验证 | MeetingBreakdownDialog 已实现:勾选/编辑/确认后创建子任务 |
|
||||||
|
| 04 | 描述字段编辑与保存 | 待验证 | - |
|
||||||
|
| 04 | 附件上传/下载/删除 | 待验证 | - |
|
||||||
|
| 04 | 外部链接添加与打开 | 待验证 | - |
|
||||||
|
| 04 | 本地文件通过系统程序打开 | 待验证 | Process.Start / xdg-open |
|
||||||
|
| 04 | 附件数量/大小限制 | 待验证 | 20 个 / 50MB |
|
||||||
|
| 04 | 待办项删除时附件级联清理 | 待验证 | - |
|
||||||
|
| 04 | 跨平台编辑安全(移动端不覆盖描述/附件) | 待验证 | 桌面设值 → 移动端改标题 → 桌面验证不丢失 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -79,8 +157,21 @@ Hua.Todo v1.3.0 版本聚焦于两个核心能力的升级:
|
|||||||
| 决策点 | 结论 |
|
| 决策点 | 结论 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| MCP 框架选择 | 使用 TRAE 平台内置的 MCP 服务框架 |
|
| MCP 框架选择 | 使用 TRAE 平台内置的 MCP 服务框架 |
|
||||||
| 语音识别方案 | 集成平台语音识别能力 |
|
| STT/TTS 分层 | 接口在 Core,实现在各平台目录(同全局快捷键模式) |
|
||||||
| 服务注册方式 | 遵循平台标准注册流程 |
|
| 语音指令解析 | A+C 混合方案:在线走 LLM 意图解析(LlmIntentParser),离线降级到规则匹配(RuleIntentParser),双策略通过 HybridVoiceIntentParser 自动切换 |
|
||||||
|
| 歧义处理策略 | 目标不唯一时返回候选列表;LLM confidence < 0.8 时触发确认;confidence < 0.5 按 UNKNOWN 处理 |
|
||||||
|
| AI 拆分安全性 | LLM 调用在 Host 端,建议需用户确认后才创建 |
|
||||||
|
| LLM 复用 | 意图解析与 AI 拆分共用同一 LLM 基础设施(LlmClientService) |
|
||||||
|
| 语音通话 | 本期不做,场景不明确 |
|
||||||
|
| 会议类型 | 新增 `TaskType` 枚举区分普通待办项与会议,会议有专属录音/纪要/拆分 UI |
|
||||||
|
| 录音存储 | 转写完成后删除原始音频文件,节省空间 |
|
||||||
|
| 音频转写 | 优先服务端 Whisper API;后续补各平台原生 STT |
|
||||||
|
| LLM 拆分 prompt | 会议专用 prompt,侧重"提取行动项",输出优先级+原因 |
|
||||||
|
| 工单 02 复用 | `LlmClientService` 直接复用;会议拆分 prompt 独立于语音意图解析 |
|
||||||
|
| 多入口覆盖规则 | 后续每项新增功能必须在需求阶段确认 UI 入口 + 语音控制入口覆盖情况,详见 [05-多入口功能同步规范.md](../../../.trae/rules/项目/05-多入口功能同步规范.md) |
|
||||||
|
| 附件存储策略 | 附件存储在应用数据目录 `Attachments/` 子目录;外部链接不复制文件仅存 URL;单文件 50MB / 每待办项 20 个上限 |
|
||||||
|
| 外部程序启动 | 通过 `IPlatformAttachmentOpener` 接口实现平台差异:Windows 用 `Process.Start`,Linux 用 `xdg-open` |
|
||||||
|
| 工单 04 与 03 共享文件 | `TaskEntity.cs`、`TodoDbContext.cs`、`task.ts`、`TaskEditDialog.vue` 为共享文件,工单 03 先写入,04 后续追加 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -89,8 +180,11 @@ Hua.Todo v1.3.0 版本聚焦于两个核心能力的升级:
|
|||||||
| 依赖项 | 状态 | 来源 |
|
| 依赖项 | 状态 | 来源 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| TRAE MCP SDK | 已就绪 | 平台内置 |
|
| TRAE MCP SDK | 已就绪 | 平台内置 |
|
||||||
| 语音识别服务 | 已就绪 | 平台内置 |
|
| 各平台原生 STT/TTS API | 已就绪 | 平台内置 |
|
||||||
| Hua.Todo v1.2.0 | 已完成 | 上一版本 |
|
| Hua.Todo v1.2.0 | 已完成 | 上一版本 |
|
||||||
|
| LLM API(AI 拆分) | 待确认 | Host 端调用 |
|
||||||
|
| 浏览器 MediaRecorder API | 已就绪 | 前端录音 |
|
||||||
|
| 工单 02 LlmClientService | 待实现 | 会议拆分复用 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -99,9 +193,15 @@ Hua.Todo v1.3.0 版本聚焦于两个核心能力的升级:
|
|||||||
| 风险 | 影响 | 应对策略 |
|
| 风险 | 影响 | 应对策略 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| MCP 服务注册失败 | 无法对外提供服务 | 保留 HTTP API 作为降级方案 |
|
| MCP 服务注册失败 | 无法对外提供服务 | 保留 HTTP API 作为降级方案 |
|
||||||
| 语音识别准确率不足 | 用户体验下降 | 提供文字输入作为备选方案 |
|
| 平台 STT 识别准确率不足 | 用户体验下降 | 提供文字输入作为备选方案 |
|
||||||
|
| Linux STT 可用性差 | Linux 语音控制不可用 | Web Speech API 降级;或 `vosk` 离线模型 |
|
||||||
|
| LLM API 不稳定 | AI 拆分功能不可用 | 功能降级,语音指令其他部分不受影响 |
|
||||||
|
| 会议录音文件过大 | 上传超时/存储压力 | 前端限制最长 2 小时;压缩音频格式 |
|
||||||
|
| 浏览器 MediaRecorder 兼容性 | 部分平台录音不可用 | 降级提示使用文字输入 |
|
||||||
|
| STT 转写准确率不足 | 会议纪要质量差 | 转写后支持用户编辑修正 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**创建日期**:2026-06-15
|
**创建日期**:2026-06-15
|
||||||
|
**修订日期**:2026-06-16
|
||||||
**版本**:v1.3.0
|
**版本**:v1.3.0
|
||||||
|
|||||||
@@ -1,23 +1,25 @@
|
|||||||
# 研发工单 v1.3.0 - 02 语音通话与语音控制功能
|
# 研发工单 v1.3.0 - 02 语音控制与 AI 辅助
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 一、目标与范围
|
## 一、目标与范围
|
||||||
|
|
||||||
### 1.1 目标
|
### 1.1 目标
|
||||||
实现语音通话数据传输与语音控制功能,支持通过语音指令操作 Todo 待办项,提升用户交互体验。
|
实现语音控制 Todo 待办项能力与 AI 辅助任务拆分功能,通过平台原生 STT/TTS 实现语音输入输出,通过 LLM 意图解析处理自然语言指令,离线降级到规则匹配,通过 LLM 提供 AI 拆分建议。
|
||||||
|
|
||||||
### 1.2 范围
|
### 1.2 范围
|
||||||
|
|
||||||
**包含**:
|
**包含**:
|
||||||
- 语音通话数据传输能力
|
- 语音输入(STT):平台原生语音识别 → 文字
|
||||||
- 语音指令识别与解析
|
- 语音播报(TTS):执行结果语音反馈
|
||||||
- 语音控制 Todo 待办项操作
|
- 语音指令解析与执行(CRUD + 子任务操作)
|
||||||
- 与现有业务系统对接
|
- 指令歧义处理(目标不唯一时返回候选列表)
|
||||||
|
- AI 辅助任务拆分(LLM 生成子任务建议,用户确认后批量创建)
|
||||||
|
|
||||||
**不包含**:
|
**不包含**:
|
||||||
- 语音通话 UI 界面设计(仅提供能力层)
|
- 语音通话功能(场景不明确,本期不做)
|
||||||
- 第三方语音服务集成(使用平台内置能力)
|
- 前端语音 UI 设计(仅提供能力层与 API)
|
||||||
|
- 第三方语音服务集成(使用平台原生能力)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -26,109 +28,426 @@
|
|||||||
| 条件 | 说明 |
|
| 条件 | 说明 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Hua.Todo v1.2.0 | 已完成,提供基础业务能力 |
|
| Hua.Todo v1.2.0 | 已完成,提供基础业务能力 |
|
||||||
| 平台语音服务 | 已就绪 |
|
| 各平台原生 STT/TTS | Windows(`Windows.Media.SpeechRecognition`/`SpeechSynthesis`)、Android(`SpeechRecognizer`/`TextToSpeech`)、iOS/macOS(`SFSpeechRecognizer`/`AVSpeechSynthesizer`)、Linux(WebKitGTK Web Speech API / `vosk` 离线模型) |
|
||||||
| 工单 01(可选) | MCP 服务就绪后可通过 MCP 调用 |
|
| LLM API | 在线模式意图解析 + AI 拆分共用;API Key 在 Host 端管理 |
|
||||||
|
| 工单 01(可选) | MCP 服务就绪后,语音指令与 AI 拆分也可通过 MCP 暴露 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 三、需求规格
|
## 三、架构设计
|
||||||
|
|
||||||
### 3.1 语音通话数据传输
|
### 3.1 整体链路
|
||||||
|
|
||||||
**功能描述**:支持语音通话数据的实时传输
|
|
||||||
|
|
||||||
**接口设计**:
|
|
||||||
```
|
|
||||||
工具名:startVoiceCall
|
|
||||||
参数:
|
|
||||||
- target: string - 通话目标标识
|
|
||||||
返回:
|
|
||||||
- callId: string - 通话 ID
|
|
||||||
- status: string - 通话状态(connected/disconnected)
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
```
|
||||||
工具名:endVoiceCall
|
平台 STT(语音→文字)
|
||||||
参数:
|
↓
|
||||||
- callId: string - 通话 ID
|
IVoiceInputService(Core 接口,各平台实现)
|
||||||
返回:
|
↓ 回调文字到前端
|
||||||
- success: boolean - 是否成功结束
|
WebView → POST /api/voice/command { text: "帮我把那个开会的删了吧" }
|
||||||
|
↓
|
||||||
|
Host API → IVoiceIntentParser(双策略)
|
||||||
|
├─ 在线:LlmIntentParser(调 LLM,输出结构化意图+参数)
|
||||||
|
└─ 离线:RuleIntentParser(关键词规则匹配,覆盖高频指令)
|
||||||
|
↓
|
||||||
|
意图 + 参数 → 判断歧义
|
||||||
|
├─ 无歧义 → 调用 TaskService 执行 → TTS 播报结果
|
||||||
|
├─ 有歧义 → 返回候选列表 → 前端展示 → 用户确认 → 再执行
|
||||||
|
└─ UNKNOWN → TTS 播报"没听懂,请再说一次"
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3.2 语音指令控制
|
### 3.2 STT/TTS 分层策略
|
||||||
|
|
||||||
**功能描述**:支持通过语音指令操作 Todo 待办项
|
遵循与全局快捷键相同的平台分离模式(接口+平台目录):
|
||||||
|
|
||||||
**支持的语音指令**:
|
| 层 | 职责 | 位置 |
|
||||||
|
|
||||||
| 指令类型 | 示例指令 | 对应操作 |
|
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 创建任务 | "创建任务 开会" | 创建标题为"开会"的任务 |
|
| `IVoiceInputService` / `IVoiceOutputService` | 接口定义 | `src/Hua.Todo.Core/Services/` |
|
||||||
| 创建任务(带优先级) | "创建高优先级任务 提交报告" | 创建高优先级任务 |
|
| MAUI 平台实现 | 各平台原生 STT/TTS | `src/Hua.Todo.Maui/Platforms/{Windows,Android,macOS,iOS}/VoiceInputService.cs` |
|
||||||
| 完成任务 | "完成任务 开会" | 标记任务"开会"为已完成 |
|
| Avalonia 平台实现 | Linux 桌面 STT/TTS | `src/Hua.Todo.Avalonia/Services/Platforms/VoiceInputService.cs` |
|
||||||
| 删除任务 | "删除任务 开会" | 删除任务"开会" |
|
| Web 降级方案 | 浏览器 Web Speech API | 前端 `voiceInput.ts` |
|
||||||
| 更新任务 | "更新任务 开会 改为 团队会议" | 更新任务标题 |
|
|
||||||
| 查询任务 | "查询未完成任务" | 获取未完成任务列表 |
|
### 3.3 意图解析双策略(A+C 方案)
|
||||||
| 添加子任务 | "给任务开会添加子任务 准备PPT" | 为任务添加子任务 |
|
|
||||||
|
采用 **LLM 主解析 + 规则降级** 的混合方案:
|
||||||
|
|
||||||
|
#### 策略 A:在线 — LLM 意图解析(LlmIntentParser)
|
||||||
|
|
||||||
|
在线模式下,将用户原始文字发送给 LLM,通过 system prompt 约束输出为结构化 JSON:
|
||||||
|
|
||||||
**接口设计**:
|
|
||||||
```
|
```
|
||||||
工具名:executeVoiceCommand
|
System Prompt(精简版):
|
||||||
参数:
|
你是 Hua.Todo 的语音指令解析器。根据用户输入,输出以下 JSON 格式:
|
||||||
- command: string - 语音指令文本
|
{
|
||||||
返回:
|
"intent": "CREATE|UPDATE|DELETE|COMPLETE|UNCOMPLETE|QUERY|ADD_SUBTASK|AI_BREAKDOWN|UNKNOWN",
|
||||||
- success: boolean - 是否执行成功
|
"params": { ... },
|
||||||
- message: string - 执行结果描述
|
"confidence": 0.0-1.0
|
||||||
- data: object - 返回数据(如任务列表)
|
}
|
||||||
|
|
||||||
|
意图说明:
|
||||||
|
- CREATE: 创建任务,params: { title, priority? }
|
||||||
|
- UPDATE: 修改任务,params: { targetTitle, newTitle }
|
||||||
|
- DELETE: 删除任务,params: { targetTitle }
|
||||||
|
- COMPLETE: 完成任务,params: { targetTitle }
|
||||||
|
- UNCOMPLETE: 取消完成,params: { targetTitle }
|
||||||
|
- QUERY: 查询任务,params: { filter? }
|
||||||
|
- ADD_SUBTASK: 添加子任务,params: { parentTitle, subTitle }
|
||||||
|
- AI_BREAKDOWN: AI辅助拆分,params: { targetTitle }
|
||||||
|
- UNKNOWN: 无法识别
|
||||||
|
|
||||||
|
只输出 JSON,不要解释。
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3.3 语音状态管理
|
**示例调用**:
|
||||||
|
|
||||||
**接口设计**:
|
|
||||||
```
|
```
|
||||||
工具名:getVoiceStatus
|
输入:"帮我把那个开会的任务删了吧"
|
||||||
参数:无
|
LLM 输出:
|
||||||
返回:
|
{
|
||||||
- isListening: boolean - 是否正在监听
|
"intent": "DELETE",
|
||||||
- isSpeaking: boolean - 是否正在播报
|
"params": { "targetTitle": "开会" },
|
||||||
|
"confidence": 0.95
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
```
|
```
|
||||||
工具名:speakText
|
输入:"这个项目太大了,帮我拆一下"
|
||||||
参数:
|
LLM 输出:
|
||||||
- text: string - 要播报的文本
|
{
|
||||||
返回:
|
"intent": "AI_BREAKDOWN",
|
||||||
- success: boolean - 是否成功
|
"params": { "targetTitle": "这个项目" },
|
||||||
|
"confidence": 0.88
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**优势**:
|
||||||
|
- 无需维护同义词表,LLM 天然理解各种自然语言表述
|
||||||
|
- 扩展新意图只需修改 prompt,不改代码
|
||||||
|
- 歧义场景 LLM 也能判断(如 confidence < 阈值时触发确认)
|
||||||
|
|
||||||
|
#### 策略 C:离线 — 规则匹配降级(RuleIntentParser)
|
||||||
|
|
||||||
|
离线/嵌入式模式下,降级到关键词规则匹配,只覆盖高频指令:
|
||||||
|
|
||||||
|
| 意图 | 匹配规则 | 示例 |
|
||||||
|
|---|---|---|
|
||||||
|
| `CREATE` | 正则:`/^(创建|新增|添加|加)\s*(任务|待办)?\s*(.+)/` | "创建任务 开会" |
|
||||||
|
| `DELETE` | 正则:`/^(删除|删掉|移除)\s*(任务|待办)?\s*(.+)/` | "删除任务 开会" |
|
||||||
|
| `COMPLETE` | 正则:`/^(完成|做完)\s*(任务|待办)?\s*(.+)/` | "完成任务 开会" |
|
||||||
|
| `UNCOMPLETE` | 正则:`/^(取消完成|重新打开|恢复)\s*(.+)/` | "取消完成 开会" |
|
||||||
|
| `UPDATE` | 正则:`/^(修改|更新|编辑)\s*(.+?)\s*(改为|改成|改成)\s*(.+)/` | "修改任务 开会 改为 团队会议" |
|
||||||
|
| `QUERY` | 正则:`/^(查询|查看|列出|显示)\s*(.+)/` | "查询未完成任务" |
|
||||||
|
| `ADD_SUBTASK` | 正则:`/^(给|为)\s*(.+?)\s*(添加|加)\s*(子任务|子项)?\s*(.+)/` | "给任务开会添加子任务 准备PPT" |
|
||||||
|
| `AI_BREAKDOWN` | 正则:`/^(帮我拆分|AI拆分|智能拆分)\s*(.+)/` | "帮我拆分 开会" |
|
||||||
|
|
||||||
|
**降级策略**:
|
||||||
|
- 离线模式下 `AI_BREAKDOWN` 意图不可用(需要 LLM),TTS 播报"离线模式不支持 AI 拆分"
|
||||||
|
- 规则匹配失败时返回 `UNKNOWN`
|
||||||
|
|
||||||
|
#### 策略切换逻辑
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
/// <summary>语音意图解析器接口</summary>
|
||||||
|
public interface IVoiceIntentParser
|
||||||
|
{
|
||||||
|
/// <summary>解析语音指令文本,返回意图与参数</summary>
|
||||||
|
Task<VoiceIntentResult> ParseAsync(string text, CancellationToken ct = default);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>双策略解析器:在线走 LLM,离线走规则</summary>
|
||||||
|
public class HybridVoiceIntentParser : IVoiceIntentParser
|
||||||
|
{
|
||||||
|
private readonly LlmIntentParser _llmParser;
|
||||||
|
private readonly RuleIntentParser _ruleParser;
|
||||||
|
private readonly IConnectivityService _connectivity;
|
||||||
|
|
||||||
|
public async Task<VoiceIntentResult> ParseAsync(string text, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (_connectivity.IsOnline)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _llmParser.ParseAsync(text, ct);
|
||||||
|
if (result.Intent != VoiceIntent.UNKNOWN)
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// LLM 调用失败,降级到规则
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _ruleParser.Parse(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 语音指令处理完整流程
|
||||||
|
|
||||||
|
```
|
||||||
|
原始文字 → HybridVoiceIntentParser
|
||||||
|
├─ 在线:LlmIntentParser(LLM 结构化输出)
|
||||||
|
└─ 离线/降级:RuleIntentParser(关键词正则匹配)
|
||||||
|
↓
|
||||||
|
意图 + 参数 + confidence
|
||||||
|
├─ confidence >= 阈值 且 目标唯一 → 执行 → TTS 播报
|
||||||
|
├─ 目标匹配多个 → 返回候选列表 → 用户确认 → 执行
|
||||||
|
├─ confidence < 阈值 → TTS "您是要...吗?" → 用户确认
|
||||||
|
└─ UNKNOWN → TTS "没听懂,请再说一次"
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 四、验收标准
|
## 四、需求规格
|
||||||
|
|
||||||
|
### 4.1 语音输入(STT)
|
||||||
|
|
||||||
|
各平台通过原生 API 将语音转为文字,通过 `IVoiceInputService` 接口统一暴露:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
/// <summary>语音输入服务接口,各平台实现原生 STT</summary>
|
||||||
|
public interface IVoiceInputService
|
||||||
|
{
|
||||||
|
/// <summary>是否正在监听</summary>
|
||||||
|
bool IsListening { get; }
|
||||||
|
|
||||||
|
/// <summary>开始语音识别,识别结果通过回调返回</summary>
|
||||||
|
Task StartListeningAsync(Action<string> onResult, Action<string>? onError = null);
|
||||||
|
|
||||||
|
/// <summary>停止语音识别</summary>
|
||||||
|
Task StopListeningAsync();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 语音播报(TTS)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
/// <summary>语音输出服务接口,各平台实现原生 TTS</summary>
|
||||||
|
public interface IVoiceOutputService
|
||||||
|
{
|
||||||
|
/// <summary>是否正在播报</summary>
|
||||||
|
bool IsSpeaking { get; }
|
||||||
|
|
||||||
|
/// <summary>播报文本</summary>
|
||||||
|
Task SpeakAsync(string text);
|
||||||
|
|
||||||
|
/// <summary>停止播报</summary>
|
||||||
|
Task StopSpeakingAsync();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 语音指令解析
|
||||||
|
|
||||||
|
#### 意图定义
|
||||||
|
|
||||||
|
| 意图 (Intent) | 参数 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `CREATE` | `title`(必填)、`priority`(可选:High/Medium/Low) | 创建任务 |
|
||||||
|
| `UPDATE` | `targetTitle`(必填)、`newTitle`(必填) | 修改任务标题 |
|
||||||
|
| `DELETE` | `targetTitle`(必填) | 删除任务 |
|
||||||
|
| `COMPLETE` | `targetTitle`(必填) | 标记完成 |
|
||||||
|
| `UNCOMPLETE` | `targetTitle`(必填) | 取消完成 |
|
||||||
|
| `QUERY` | `filter`(可选:如"未完成"、"高优先级") | 查询任务 |
|
||||||
|
| `ADD_SUBTASK` | `parentTitle`(必填)、`subTitle`(必填) | 添加子任务 |
|
||||||
|
| `AI_BREAKDOWN` | `targetTitle`(必填) | AI 辅助拆分(仅在线) |
|
||||||
|
| `UNKNOWN` | 无 | 无法识别 |
|
||||||
|
|
||||||
|
#### 歧义处理规则
|
||||||
|
|
||||||
|
当指令中 `targetTitle` 匹配到多个 Todo 待办项时:
|
||||||
|
1. **不直接执行**,返回歧义结果(包含匹配的候选列表)
|
||||||
|
2. 前端展示候选项,用户点选或语音确认后再执行
|
||||||
|
3. 匹配规则:标题完全匹配优先,包含匹配次之
|
||||||
|
|
||||||
|
#### 置信度阈值
|
||||||
|
|
||||||
|
- LLM 返回 `confidence >= 0.8`:直接执行
|
||||||
|
- `0.5 <= confidence < 0.8`:确认后执行(TTS "您是要...吗?")
|
||||||
|
- `confidence < 0.5`:按 UNKNOWN 处理
|
||||||
|
|
||||||
|
### 4.4 语音指令 API
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/voice/command
|
||||||
|
请求体:
|
||||||
|
{
|
||||||
|
"text": "帮我把那个开会的删了吧" // STT 识别后的原始文字
|
||||||
|
}
|
||||||
|
响应体:
|
||||||
|
{
|
||||||
|
"intent": "DELETE", // 解析出的意图
|
||||||
|
"params": { // 提取的参数
|
||||||
|
"targetTitle": "开会"
|
||||||
|
},
|
||||||
|
"confidence": 0.95, // 置信度
|
||||||
|
"result": { // 执行结果
|
||||||
|
"success": true,
|
||||||
|
"message": "已删除任务:开会",
|
||||||
|
"data": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
歧义响应:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"intent": "COMPLETE",
|
||||||
|
"params": { "targetTitle": "开会" },
|
||||||
|
"confidence": 0.92,
|
||||||
|
"result": {
|
||||||
|
"success": false,
|
||||||
|
"message": "找到多个匹配的任务,请确认",
|
||||||
|
"ambiguity": true,
|
||||||
|
"candidates": [
|
||||||
|
{ "id": 1, "title": "开会" },
|
||||||
|
{ "id": 5, "title": "开会讨论方案" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
用户确认后二次请求:
|
||||||
|
```
|
||||||
|
POST /api/voice/command/confirm
|
||||||
|
请求体:
|
||||||
|
{
|
||||||
|
"intent": "COMPLETE",
|
||||||
|
"targetId": 5 // 用户选择的候选 ID
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.5 AI 辅助任务拆分
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/voice/ai-breakdown
|
||||||
|
请求体:
|
||||||
|
{
|
||||||
|
"taskId": 5 // 要拆分的目标任务 ID
|
||||||
|
}
|
||||||
|
响应体:
|
||||||
|
{
|
||||||
|
"suggestions": [ // LLM 生成的子任务建议
|
||||||
|
{ "title": "准备会议议程", "priority": "Medium" },
|
||||||
|
{ "title": "发送会议邀请", "priority": "High" },
|
||||||
|
{ "title": "预定会议室", "priority": "Low" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
用户确认后批量创建:
|
||||||
|
```
|
||||||
|
POST /api/voice/ai-breakdown/confirm
|
||||||
|
请求体:
|
||||||
|
{
|
||||||
|
"parentTaskId": 5,
|
||||||
|
"subTasks": [ // 用户勾选后的子任务列表
|
||||||
|
{ "title": "准备会议议程", "priority": "Medium" },
|
||||||
|
{ "title": "发送会议邀请", "priority": "High" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键约束**:
|
||||||
|
- LLM 调用在 Host 端执行,API Key 不暴露到客户端
|
||||||
|
- 意图解析与 AI 拆分共用同一 LLM 基础设施
|
||||||
|
- 建议≠直接执行,必须用户确认后才创建子任务
|
||||||
|
- 离线模式下 AI 拆分不可用,TTS 播报"离线模式不支持 AI 拆分"
|
||||||
|
- MCP 服务就绪后,`aiBreakdown` 可同步暴露为 MCP 工具
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、验收标准
|
||||||
|
|
||||||
| 验收项 | 验证方法 | 预期结果 |
|
| 验收项 | 验证方法 | 预期结果 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 创建任务指令 | 说出"创建任务 测试" | 成功创建标题为"测试"的任务 |
|
| STT 语音输入 | 说话后检查回调文字 | 文字识别正确 |
|
||||||
| 创建带优先级任务 | 说出"创建高优先级任务 紧急" | 成功创建高优先级任务 |
|
| TTS 语音播报 | 执行指令后检查播报 | 播报内容与执行结果一致 |
|
||||||
| 完成任务指令 | 说出"完成任务 测试" | 任务状态变为已完成 |
|
| 创建任务(在线) | "帮我把开会的任务加上" | LLM 解析为 CREATE,成功创建 |
|
||||||
| 删除任务指令 | 说出"删除任务 测试" | 任务被成功删除 |
|
| 创建任务(离线降级) | "创建任务 测试" | 规则匹配为 CREATE,成功创建 |
|
||||||
| 查询任务指令 | 说出"查询未完成任务" | 返回未完成任务列表 |
|
| 创建带优先级任务 | "建一个高优先级任务 紧急" | 成功创建高优先级任务 |
|
||||||
| 语音播报 | 调用 speakText | 成功播放指定文本 |
|
| 完成任务指令 | "把那个开会的事标完成" | LLM 解析为 COMPLETE,执行成功 |
|
||||||
| 语音通话 | 调用 startVoiceCall | 通话连接成功 |
|
| 取消完成指令 | "取消完成 测试" | 任务状态恢复为未完成 |
|
||||||
|
| 删除任务指令 | "帮我把测试删了" | LLM 解析为 DELETE,删除成功 |
|
||||||
|
| 更新任务指令 | "把测试改成验收" | 任务标题更新 |
|
||||||
|
| 查询任务指令 | "有哪些没做完的" | LLM 解析为 QUERY,返回列表 |
|
||||||
|
| 添加子任务指令 | "给开会加个子任务 准备PPT" | 子任务创建成功 |
|
||||||
|
| 歧义处理 | 多个任务含"开会"时说"完成开会" | 返回候选列表,不直接执行 |
|
||||||
|
| AI 拆分建议 | "帮我拆分 开会" | 返回子任务建议列表 |
|
||||||
|
| AI 拆分确认 | 用户勾选后确认 | 仅创建勾选的子任务 |
|
||||||
|
| 离线 AI 拆分 | 离线时说"帮我拆分" | 播报"离线模式不支持 AI 拆分" |
|
||||||
|
| 无法识别指令 | 说出无关内容 | 播报"没听懂,请再说一次" |
|
||||||
|
| LLM 降级 | 断网时使用语音 | 自动降级到规则匹配,基本 CRUD 可用 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 五、Touch List
|
## 六、Touch List
|
||||||
|
|
||||||
| 文件路径 | 修改类型 | 说明 |
|
| 文件路径 | 修改类型 | 说明 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `src/Hua.Todo.Application/Voice/` | 新增 | 语音服务目录 |
|
| `src/Hua.Todo.Core/Services/IVoiceInputService.cs` | 新增 | STT 接口定义 |
|
||||||
| `src/Hua.Todo.Application/Voice/VoiceService.cs` | 新增 | 语音服务实现 |
|
| `src/Hua.Todo.Core/Services/IVoiceOutputService.cs` | 新增 | TTS 接口定义 |
|
||||||
| `src/Hua.Todo.Application/Voice/VoiceCommandParser.cs` | 新增 | 语音指令解析器 |
|
| `src/Hua.Todo.Core/Services/IVoiceIntentParser.cs` | 新增 | 意图解析器接口 |
|
||||||
| `src/Hua.Todo.Application/Voice/Models/` | 新增 | 语音相关 DTO |
|
| `src/Hua.Todo.Application/Voice/LlmIntentParser.cs` | 新增 | LLM 意图解析器(在线策略) |
|
||||||
| `src/Hua.Todo.Application/Voice/VoiceServiceCollectionExtensions.cs` | 新增 | 服务注册扩展 |
|
| `src/Hua.Todo.Application/Voice/RuleIntentParser.cs` | 新增 | 规则匹配解析器(离线降级策略) |
|
||||||
|
| `src/Hua.Todo.Application/Voice/HybridVoiceIntentParser.cs` | 新增 | 双策略解析器(在线走 LLM,离线走规则) |
|
||||||
|
| `src/Hua.Todo.Application/Voice/VoiceCommandIntent.cs` | 新增 | 意图枚举定义 |
|
||||||
|
| `src/Hua.Todo.Application/Voice/VoiceCommandExecutor.cs` | 新增 | 意图执行器(分发到 TaskService) |
|
||||||
|
| `src/Hua.Todo.Application/Voice/VoiceController.cs` | 新增 | 语音指令 API 端点(`/api/voice/command`、`/api/voice/ai-breakdown`) |
|
||||||
|
| `src/Hua.Todo.Application/Voice/AiBreakdownService.cs` | 新增 | AI 辅助拆分服务(调用 LLM 生成建议) |
|
||||||
|
| `src/Hua.Todo.Application/Voice/LlmClientService.cs` | 新增 | LLM 调用封装(意图解析 + AI 拆分共用) |
|
||||||
|
| `src/Hua.Todo.Application/Voice/Models/` | 新增 | 语音相关 DTO(VoiceIntentResult、VoiceCommandRequest 等) |
|
||||||
|
| `src/Hua.Todo.Application/Voice/VoiceServiceCollectionExtensions.cs` | 新增 | 语音服务 DI 注册 |
|
||||||
|
| `src/Hua.Todo.Maui/Platforms/Windows/VoiceInputService.cs` | 新增 | Windows STT 实现 |
|
||||||
|
| `src/Hua.Todo.Maui/Platforms/Windows/VoiceOutputService.cs` | 新增 | Windows TTS 实现 |
|
||||||
|
| `src/Hua.Todo.Maui/Platforms/Android/VoiceInputService.cs` | 新增 | Android STT 实现 |
|
||||||
|
| `src/Hua.Todo.Maui/Platforms/Android/VoiceOutputService.cs` | 新增 | Android TTS 实现 |
|
||||||
|
| `src/Hua.Todo.Maui/Platforms/MacCatalyst/VoiceInputService.cs` | 新增 | macOS STT 实现 |
|
||||||
|
| `src/Hua.Todo.Maui/Platforms/MacCatalyst/VoiceOutputService.cs` | 新增 | macOS TTS 实现 |
|
||||||
|
| `src/Hua.Todo.Maui/Platforms/iOS/VoiceInputService.cs` | 新增 | iOS STT 实现 |
|
||||||
|
| `src/Hua.Todo.Maui/Platforms/iOS/VoiceOutputService.cs` | 新增 | iOS TTS 实现 |
|
||||||
|
| `src/Hua.Todo.Avalonia/Services/Platforms/VoiceInputService.cs` | 新增 | Linux STT 实现 |
|
||||||
|
| `src/Hua.Todo.Avalonia/Services/Platforms/VoiceOutputService.cs` | 新增 | Linux TTS 实现 |
|
||||||
|
| `src/Hua.Todo.Web/src/api/voice.ts` | 新增 | 前端语音 API 模块 |
|
||||||
|
| `src/Hua.Todo.Web/src/composables/useVoiceInput.ts` | 新增 | 前端语音输入组合式函数(含 Web Speech API 降级) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**工单编号**:02
|
**工单编号**:02
|
||||||
**标题**:语音通话与语音控制功能
|
**标题**:语音控制与 AI 辅助
|
||||||
**版本**:v1.3.0
|
**版本**:v1.3.0
|
||||||
|
**修订日期**:2026-06-16
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、与其他工单的语音入口衔接
|
||||||
|
|
||||||
|
### 7.1 背景
|
||||||
|
|
||||||
|
Hua.Todo 目前有两个功能入口:**UI 入口**(WebView / 前端界面)和**语音控制入口**(本工单产出)。后续每项新增功能在需求阶段都必须确认这两个入口的覆盖情况。
|
||||||
|
|
||||||
|
### 7.2 工单 03(会议任务拆分)的语音入口
|
||||||
|
|
||||||
|
工单 03 的会议功能需要通过语音控制入口提供以下指令:
|
||||||
|
|
||||||
|
| 语音指令 | 意图 | 参数 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| "新建会议 项目评审" | `CREATE_MEETING` | `title`(必填)、`priority`(可选) | 创建会议类型任务(TaskType.Meeting) |
|
||||||
|
| "记录会议内容" | `RECORD_MEETING` | `targetTitle`(必填) | 开始录制当前会议 |
|
||||||
|
| "停止录制" | `STOP_RECORDING` | 无 | 停止录制,触发转写 |
|
||||||
|
| "帮我拆分这个会议" | `AI_BREAKDOWN` | `targetTitle`(必填,目标为会议类型) | 对会议内容进行 AI 拆分(复用 AI_BREAKDOWN 意图,参数携带 TaskType=Meeting) |
|
||||||
|
| "查看会议待办项" | `QUERY` | `filter="会议"` | 查询所有会议类型任务 |
|
||||||
|
|
||||||
|
**关键点**:
|
||||||
|
- `AI_BREAKDOWN` 意图需扩展以区分普通任务拆分和会议拆分(通过 `TaskType` 字段),会议拆分的 prompt 侧重"提取行动项"
|
||||||
|
- `CREATE_MEETING` 是 `CREATE` 意图的子类型,创建时字段 `taskType = Meeting`,需要 LLM intent parser 的 prompt 增加说明
|
||||||
|
- 录音控制(`RECORD_MEETING` / `STOP_RECORDING`)是新意图,对应前端 MediaRecorder 的启动/停止
|
||||||
|
|
||||||
|
### 7.3 语音入口覆盖检查表
|
||||||
|
|
||||||
|
后续每个新增功能/工单,智能体必须在需求讨论阶段输出以下检查表:
|
||||||
|
|
||||||
|
| 检查项 | 状态 | 备注 |
|
||||||
|
|---|---|---|
|
||||||
|
| UI 入口是否已规划 | ✅ / ❌ | 描述 UI 入口 |
|
||||||
|
| 语音控制入口是否已规划 | ✅ / ❌ | 描述对应的语音指令与意图 |
|
||||||
|
| 当前不可覆盖的入口 | 说明原因 | 如:语音控制暂不支持 XX 操作(选项过多/需可视化交互) |
|
||||||
|
|
||||||
|
此规则已固化为项目规范,详见 [.trae/rules/项目/05-多入口功能同步规范.md](../../../.trae/rules/项目/05-多入口功能同步规范.md)。
|
||||||
|
|||||||
@@ -0,0 +1,241 @@
|
|||||||
|
# 研发工单 v1.3.0 - 03-01 会议数据模型与 API
|
||||||
|
|
||||||
|
> 父工单:[03-会议任务拆分](./03-会议任务拆分.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、目标
|
||||||
|
|
||||||
|
为"会议任务拆分"功能建立数据模型:扩展 `TaskEntity` 新增 `TaskType` 枚举和 `MeetingNotes`/`AudioDuration` 字段,定义完整的会议相关 API 端点与 DTO,完成数据库迁移。
|
||||||
|
|
||||||
|
## 二、范围
|
||||||
|
|
||||||
|
**包含**:
|
||||||
|
- 新增 `TaskType` 枚举(Normal / Meeting)
|
||||||
|
- `TaskEntity` 新增字段:`TaskType`、`MeetingNotes`、`AudioDuration`
|
||||||
|
- EF Core 数据库迁移
|
||||||
|
- DTO 扩展:`CreateTaskDto`、`UpdateTaskDto`、`TaskDto` 新增对应字段
|
||||||
|
- 会议 API 端点骨架:`MeetingController` + `MeetingService`
|
||||||
|
- API 路径:`/api/meeting/{taskId}/transcribe`、`/api/meeting/{taskId}/notes`
|
||||||
|
- 会议相关 DTO 定义:`TranscribeRequest`、`TranscribeResponse`、`MeetingNotesRequest`
|
||||||
|
|
||||||
|
**不包含**:
|
||||||
|
- AI 拆分逻辑(由 03-03 完成)
|
||||||
|
- STT 转写实现(由 03-02 完成)
|
||||||
|
- 前端组件(由 03-04 完成)
|
||||||
|
|
||||||
|
## 三、前置条件
|
||||||
|
|
||||||
|
| 条件 | 说明 |
|
||||||
|
|---|---|
|
||||||
|
| Hua.Todo v1.2.0 | 基础 Task CRUD 能力、父子任务、DynamicApi 中间件 |
|
||||||
|
| ABP 基类迁移(可选) | 若工单 09(v1.2.0)已完成,TaskEntity 已转为 ABP 基类,字段新增方式不同 |
|
||||||
|
|
||||||
|
## 四、详细规格
|
||||||
|
|
||||||
|
### 4.1 TaskType 枚举
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
/// <summary>待办项类型,区分普通待办项与会议</summary>
|
||||||
|
public enum TaskType
|
||||||
|
{
|
||||||
|
/// <summary>普通待办项(默认)</summary>
|
||||||
|
Normal = 0,
|
||||||
|
/// <summary>会议:包含录音/纪要,支持 AI 任务拆分</summary>
|
||||||
|
Meeting = 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
文件位置:`src/Hua.Todo.Core/Entities/TaskType.cs`
|
||||||
|
|
||||||
|
### 4.2 TaskEntity 新增字段
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
/// <summary>待办项类型</summary>
|
||||||
|
public TaskType TaskType { get; set; } = TaskType.Normal;
|
||||||
|
|
||||||
|
/// <summary>会议纪要/转写文字(仅 Meeting 类型有值)</summary>
|
||||||
|
[MaxLength(20000)]
|
||||||
|
public string? MeetingNotes { get; set; }
|
||||||
|
|
||||||
|
/// <summary>录音时长(秒),仅 Meeting 类型有值</summary>
|
||||||
|
public double? AudioDuration { get; set; }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 EF Core 配置(TodoDbContext.cs)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
builder.Entity<TaskEntity>(b =>
|
||||||
|
{
|
||||||
|
// ... 现有配置 ...
|
||||||
|
|
||||||
|
b.Property(x => x.TaskType)
|
||||||
|
.HasDefaultValue(TaskType.Normal)
|
||||||
|
.HasConversion<int>(); // 枚举存为整数
|
||||||
|
|
||||||
|
b.Property(x => x.MeetingNotes)
|
||||||
|
.HasMaxLength(20000); // 最多约 20000 字符
|
||||||
|
|
||||||
|
b.Property(x => x.AudioDuration)
|
||||||
|
.IsRequired(false);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 数据库迁移
|
||||||
|
|
||||||
|
```
|
||||||
|
dotnet ef migrations add AddMeetingFieldsToTasks
|
||||||
|
```
|
||||||
|
|
||||||
|
迁移应在 `Hua.Todo.Application/Migrations/` 目录生成。
|
||||||
|
|
||||||
|
### 4.5 DTO 扩展
|
||||||
|
|
||||||
|
**CreateTaskDto** 新增:
|
||||||
|
```csharp
|
||||||
|
/// <summary>待办项类型(0=Normal, 1=Meeting),默认 Normal</summary>
|
||||||
|
public TaskType TaskType { get; set; } = TaskType.Normal;
|
||||||
|
```
|
||||||
|
|
||||||
|
**TaskDto** 新增:
|
||||||
|
```csharp
|
||||||
|
/// <summary>待办项类型</summary>
|
||||||
|
public TaskType TaskType { get; set; }
|
||||||
|
/// <summary>会议纪要/转写文字</summary>
|
||||||
|
public string? MeetingNotes { get; set; }
|
||||||
|
/// <summary>录音时长(秒)</summary>
|
||||||
|
public double? AudioDuration { get; set; }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.6 会议 DTO
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
/// <summary>转写请求</summary>
|
||||||
|
public class TranscribeRequest
|
||||||
|
{
|
||||||
|
public IFormFile Audio { get; set; } = null!;
|
||||||
|
public string? Format { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>转写响应</summary>
|
||||||
|
public class TranscribeResponse
|
||||||
|
{
|
||||||
|
public int TaskId { get; set; }
|
||||||
|
public string Transcript { get; set; } = string.Empty;
|
||||||
|
public double AudioDuration { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>保存会议纪要请求</summary>
|
||||||
|
public class MeetingNotesRequest
|
||||||
|
{
|
||||||
|
public string Notes { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>会议纪要响应</summary>
|
||||||
|
public class MeetingNotesResponse
|
||||||
|
{
|
||||||
|
public int TaskId { get; set; }
|
||||||
|
public string MeetingNotes { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.7 API 端点
|
||||||
|
|
||||||
|
#### POST /api/meeting/{taskId}/transcribe
|
||||||
|
|
||||||
|
- 接收:multipart/form-data(音频文件)
|
||||||
|
- 返回:`TranscribeResponse`
|
||||||
|
- 业务:音频上传后异步转写,结果存回 `TaskEntity.MeetingNotes`
|
||||||
|
- 当前骨架:返回占位文字,具体转写逻辑由 03-02 实现
|
||||||
|
|
||||||
|
#### POST /api/meeting/{taskId}/notes
|
||||||
|
|
||||||
|
- 接收:`MeetingNotesRequest`
|
||||||
|
- 返回:`MeetingNotesResponse`
|
||||||
|
- 业务:保存/更新会议纪要
|
||||||
|
|
||||||
|
### 4.8 MeetingService 骨架
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
/// <summary>会议业务服务</summary>
|
||||||
|
public class MeetingService
|
||||||
|
{
|
||||||
|
private readonly ITaskRepository _taskRepo;
|
||||||
|
|
||||||
|
public MeetingService(ITaskRepository taskRepo)
|
||||||
|
{
|
||||||
|
_taskRepo = taskRepo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>验证 taskId 对应的待办项存在且为 Meeting 类型</summary>
|
||||||
|
public async Task<TaskEntity> GetMeetingTaskOrThrow(int taskId)
|
||||||
|
{
|
||||||
|
var task = await _taskRepo.GetAsync(taskId);
|
||||||
|
if (task == null)
|
||||||
|
throw new NotFoundException($"任务 {taskId} 不存在");
|
||||||
|
if (task.TaskType != TaskType.Meeting)
|
||||||
|
throw new BusinessException($"任务 {taskId} 不是会议类型");
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>保存会议纪要</summary>
|
||||||
|
public async Task<TaskEntity> SaveNotes(int taskId, string notes)
|
||||||
|
{
|
||||||
|
var task = await GetMeetingTaskOrThrow(taskId);
|
||||||
|
task.MeetingNotes = notes;
|
||||||
|
task.UpdatedAt = DateTime.UtcNow;
|
||||||
|
await _taskRepo.UpdateAsync(task);
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>保存转写结果与音频时长</summary>
|
||||||
|
public async Task<TaskEntity> SaveTranscript(int taskId, string transcript, double audioDuration)
|
||||||
|
{
|
||||||
|
var task = await GetMeetingTaskOrThrow(taskId);
|
||||||
|
task.MeetingNotes = transcript;
|
||||||
|
task.AudioDuration = audioDuration;
|
||||||
|
task.UpdatedAt = DateTime.UtcNow;
|
||||||
|
await _taskRepo.UpdateAsync(task);
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 五、验收标准
|
||||||
|
|
||||||
|
| 验收项 | 验证方法 | 预期结果 |
|
||||||
|
|---|---|---|
|
||||||
|
| TaskType 枚举可用 | 编译通过 | `TaskType.Normal` / `TaskType.Meeting` 可正常赋值 |
|
||||||
|
| DB 迁移可执行 | `dotnet ef database update` | `Tasks` 表新增 `TaskType`/`MeetingNotes`/`AudioDuration` 列 |
|
||||||
|
| 创建 Meeting 类型任务 | `POST /api/task` 带 `taskType:1` | 返回的 TaskDto 中 `taskType=1` |
|
||||||
|
| 保存会议纪要 | `POST /api/meeting/{id}/notes` | `meetingNotes` 字段更新成功 |
|
||||||
|
| 非 Meeting 类型调用会议 API | 普通任务调 `/api/meeting/{id}/notes` | 返回业务异常 |
|
||||||
|
| 现有代码兼容 | 运行所有已有测试 | 不破坏现有业务 |
|
||||||
|
|
||||||
|
## 六、风险
|
||||||
|
|
||||||
|
| 风险 | 影响 | 应对 |
|
||||||
|
|---|---|---|
|
||||||
|
| TaskEntity 已重构为 ABP 基类 | 字段新增方式不同 | 通过 ABP 的 `ExtraProperties` 或标准字段新增,迁移方式略有调整 |
|
||||||
|
| 字段长度限制 | 超长会议纪要截断 | `MeetingNotes` 设 `MaxLength(20000)`,前端也做长度限制 |
|
||||||
|
|
||||||
|
## 七、Touch List
|
||||||
|
|
||||||
|
| 文件路径 | 修改类型 | 是否共享 |
|
||||||
|
|---|---|---|
|
||||||
|
| `src/Hua.Todo.Core/Entities/TaskType.cs` | 新增 | 否 |
|
||||||
|
| `src/Hua.Todo.Core/Entities/TaskEntity.cs` | 修改 | 是 |
|
||||||
|
| `src/Hua.Todo.Application/Data/TodoDbContext.cs` | 修改 | 是 |
|
||||||
|
| `src/Hua.Todo.Application/Models/TaskModels.cs` | 修改 | 是 |
|
||||||
|
| `src/Hua.Todo.Application/Meeting/MeetingController.cs` | 新增 | 否 |
|
||||||
|
| `src/Hua.Todo.Application/Meeting/MeetingService.cs` | 新增 | 否 |
|
||||||
|
| `src/Hua.Todo.Application/Meeting/Models/MeetingDtos.cs` | 新增 | 否 |
|
||||||
|
| `src/Hua.Todo.Web/src/types/task.ts` | 修改 | 是 |
|
||||||
|
| `Migrations/AddMeetingFieldsToTasks.cs` | 新增 | 是 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**工单编号**:03-01
|
||||||
|
**标题**:会议数据模型与 API
|
||||||
|
**版本**:v1.3.0
|
||||||
|
**创建日期**:2026-06-16
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
# 研发工单 v1.3.0 - 03-02 音频录制与转写
|
||||||
|
|
||||||
|
> 父工单:[03-会议任务拆分](./03-会议任务拆分.md)
|
||||||
|
>
|
||||||
|
> 依赖:03-01(API 契约)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、目标
|
||||||
|
|
||||||
|
实现前端音频录制(MediaRecorder API)和后端音频转写(STT)能力,打通"录音 → 上传 → 转写文字 → 保存为会议纪要"的完整链路。
|
||||||
|
|
||||||
|
## 二、范围
|
||||||
|
|
||||||
|
**包含**:
|
||||||
|
- 前端录音控件(`AudioRecorder.vue`)
|
||||||
|
- 前端录音组合式函数(`useAudioRecorder.ts`)
|
||||||
|
- 录音状态管理(录制中/暂停/停止/时长显示)
|
||||||
|
- 音频上传 API 交互(POST multipart/form-data)
|
||||||
|
- 后端音频文件临时存储
|
||||||
|
- 后端 STT 转写服务(`SttService.cs`)
|
||||||
|
- 转写结果回写 `MeetingNotes`
|
||||||
|
|
||||||
|
**不包含**:
|
||||||
|
- 实时转写(边录边转)
|
||||||
|
- 音频持久存储(转写完成后删除音频文件)
|
||||||
|
- 音频云同步
|
||||||
|
- 语音指令输入(属于工单 02)
|
||||||
|
|
||||||
|
## 三、前置条件
|
||||||
|
|
||||||
|
| 条件 | 说明 |
|
||||||
|
|---|---|
|
||||||
|
| 03-01 完成 | `MeetingController`、`MeetingService` 基础骨架就绪 |
|
||||||
|
|
||||||
|
## 四、详细规格
|
||||||
|
|
||||||
|
### 4.1 前端录音控件
|
||||||
|
|
||||||
|
**AudioRecorder.vue**:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────┐
|
||||||
|
│ 🎤 会议录音 │
|
||||||
|
│ │
|
||||||
|
│ ● 录制中... 00:15:23 │
|
||||||
|
│ ┌──────────────────────────┐ │
|
||||||
|
│ │ ▁▃▂▄▅▂▁▃▄▅▃▂▁▂▄▅▃▁ │ │ ← 简易波形
|
||||||
|
│ └──────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ [⏹ 停止录音] │
|
||||||
|
│ │
|
||||||
|
│ 录音时长限制:最长 2 小时 │
|
||||||
|
└────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
状态:
|
||||||
|
- **就绪**:显示录音按钮
|
||||||
|
- **录制中**:显示停止按钮 + 时长计时 + 简易波形条
|
||||||
|
- **已停止**:显示"提交转写" / "重新录制"
|
||||||
|
|
||||||
|
### 4.2 useAudioRecorder 组合式函数
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/// <summary>音频录制器组合式函数,封装 MediaRecorder API</summary>
|
||||||
|
export function useAudioRecorder() {
|
||||||
|
// 状态
|
||||||
|
const isRecording = ref(false)
|
||||||
|
const isPaused = ref(false)
|
||||||
|
const duration = ref(0) // 秒
|
||||||
|
const audioBlob = ref<Blob | null>(null)
|
||||||
|
const audioUrl = ref<string | null>(null) // 用于预览播放
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
async function startRecording(): Promise<void> // 请求麦克风权限,开始录制
|
||||||
|
function stopRecording(): void // 停止录制,生成 Blob
|
||||||
|
function resetRecording(): void // 重置状态
|
||||||
|
function getAudioBlob(): Blob | null // 获取录制结果
|
||||||
|
|
||||||
|
// 内部
|
||||||
|
let mediaRecorder: MediaRecorder | null = null
|
||||||
|
let timerInterval: number | null = null
|
||||||
|
|
||||||
|
// 音频格式:webm(Chrome/Firefox)、mp4(Safari)
|
||||||
|
// 时长限制:最长 2 小时(7200 秒)
|
||||||
|
|
||||||
|
return { isRecording, isPaused, duration, audioBlob, audioUrl,
|
||||||
|
startRecording, stopRecording, resetRecording, getAudioBlob }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 前端 API 模块
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/Hua.Todo.Web/src/api/meeting.ts
|
||||||
|
|
||||||
|
/// <summary>上传音频文件并请求转写</summary>
|
||||||
|
async function transcribeAudio(taskId: number, audioBlob: Blob): Promise<TranscribeResponse>
|
||||||
|
{
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('audio', audioBlob, 'meeting.webm');
|
||||||
|
|
||||||
|
const apiBaseUrl = window.__API_BASE_URL__ || 'http://localhost:5173/api';
|
||||||
|
const resp = await fetch(`${apiBaseUrl}/meeting/${taskId}/transcribe`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
if (!resp.ok) throw new Error(`转写请求失败: ${resp.status}`);
|
||||||
|
return resp.json();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 后端 SttService
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
/// <summary>语音转写服务接口</summary>
|
||||||
|
public interface ISttService
|
||||||
|
{
|
||||||
|
/// <summary>将音频文件转写为文字</summary>
|
||||||
|
/// <returns>转写文字</returns>
|
||||||
|
Task<string> TranscribeAsync(Stream audioStream, string format, CancellationToken ct = default);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
实现策略(按优先级):
|
||||||
|
|
||||||
|
| 平台/环境 | 实现方式 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| Windows MAUI | `Windows.Media.SpeechRecognition` 文件识别 | 系统自带,离线可用 |
|
||||||
|
| macOS/iOS | `SFSpeechRecognizer` 文件识别 | 需在线 |
|
||||||
|
| Android | `SpeechRecognizer` | 需在线 |
|
||||||
|
| Linux | WebKit 在线;本地 `whisper.cpp` 降级 | 多策略 |
|
||||||
|
| 通用服务端 | 扩展 `LlmClientService` 调用 Whisper API | Host 端部署 |
|
||||||
|
|
||||||
|
> 初期(v1.3.0)优先实现服务端 Whisper API 调用方式(通过 `LlmClientService` 扩展),后续版本各平台原生逐补。
|
||||||
|
|
||||||
|
### 4.5 音频上传处理流程
|
||||||
|
|
||||||
|
```
|
||||||
|
前端 AudioRecorder → stopRecording → Blob (webm/mp4)
|
||||||
|
↓
|
||||||
|
POST /api/meeting/{taskId}/transcribe (multipart/form-data)
|
||||||
|
↓
|
||||||
|
MeetingController.Transcribe()
|
||||||
|
↓ 保存音频临时文件到 meetings/ 目录
|
||||||
|
↓ 调用 ISttService.TranscribeAsync()
|
||||||
|
↓ 得到文字结果
|
||||||
|
↓ 删除临时音频文件
|
||||||
|
↓ 调用 MeetingService.SaveTranscript() 保存到数据库
|
||||||
|
↓
|
||||||
|
返回 TranscribeResponse { taskId, transcript, audioDuration }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.6 错误处理
|
||||||
|
|
||||||
|
| 场景 | 处理 |
|
||||||
|
|---|---|
|
||||||
|
| 音频格式不支持 | 返回 400 "不支持的音频格式,支持 webm/wav/mp3" |
|
||||||
|
| 音频文件太大 | 返回 400 "音频文件过大,请控制录音在 2 小时以内" |
|
||||||
|
| STT 服务不可用 | 返回 503 "转写服务暂不可用,请稍后重试" |
|
||||||
|
| taskId 不是会议类型 | 返回 400 "该待办项不是会议类型" |
|
||||||
|
|
||||||
|
## 五、验收标准
|
||||||
|
|
||||||
|
| 验收项 | 验证方法 | 预期结果 |
|
||||||
|
|---|---|---|
|
||||||
|
| 录音按钮可用 | 点击录音按钮 | 浏览器弹出麦克风权限请求 |
|
||||||
|
| 录制过程 | 授权后开始录制 | 显示录音时长,波形条有变化 |
|
||||||
|
| 停止录音 | 点击停止按钮 | 时长停止,显示"提交转写"按钮 |
|
||||||
|
| 重新录制 | 点击"重新录制" | 状态重置,可再次录制 |
|
||||||
|
| 上传转写 | 提交录音文件 | 返回转写文字 |
|
||||||
|
| 纪要保存 | 转写完成后查看任务 | `meetingNotes` 字段有转写文字 |
|
||||||
|
| 权限拒绝 | 浏览器拒绝麦克风 | 提示"无法访问麦克风,请使用文字输入" |
|
||||||
|
| 浏览器不支持 | IE/Safari 旧版等 | 提示"当前浏览器不支持录音,请使用文字输入" |
|
||||||
|
|
||||||
|
## 六、风险
|
||||||
|
|
||||||
|
| 风险 | 影响 | 应对 |
|
||||||
|
|---|---|---|
|
||||||
|
| Safari 不支持 webm | 无法录制 | 使用 mp4 格式(Safari 支持);MIME type 自动适配 |
|
||||||
|
| STT 准确率不足 | 转写错误多 | 支持用户编辑修正转写结果 |
|
||||||
|
| 长音频转写耗时长 | 用户等待 | 前端显示转写进度或"转写中"加载状态 |
|
||||||
|
|
||||||
|
## 七、Touch List
|
||||||
|
|
||||||
|
| 文件路径 | 修改类型 |
|
||||||
|
|---|---|
|
||||||
|
| `src/Hua.Todo.Web/src/components/AudioRecorder.vue` | 新增 |
|
||||||
|
| `src/Hua.Todo.Web/src/composables/useAudioRecorder.ts` | 新增 |
|
||||||
|
| `src/Hua.Todo.Web/src/api/meeting.ts` | 新增 |
|
||||||
|
| `src/Hua.Todo.Application/Meeting/SttService.cs` | 新增 |
|
||||||
|
| `src/Hua.Todo.Application/Meeting/MeetingController.cs` | 修改 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**工单编号**:03-02
|
||||||
|
**标题**:音频录制与转写
|
||||||
|
**版本**:v1.3.0
|
||||||
|
**创建日期**:2026-06-16
|
||||||
@@ -0,0 +1,281 @@
|
|||||||
|
# 研发工单 v1.3.0 - 03-03 AI 任务拆分服务
|
||||||
|
|
||||||
|
> 父工单:[03-会议任务拆分](./03-会议任务拆分.md)
|
||||||
|
>
|
||||||
|
> 依赖:03-01(API 契约)、工单 02(`LlmClientService`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、目标
|
||||||
|
|
||||||
|
实现基于 LLM 的会议内容分析服务:接收会议纪要文字,通过 LLM prompt 工程提取行动项,生成结构化的待办项建议列表(含标题、优先级、理由)。
|
||||||
|
|
||||||
|
## 二、范围
|
||||||
|
|
||||||
|
**包含**:
|
||||||
|
- `MeetingAiBreakdownService`:会议文本 → LLM → 结构化建议列表
|
||||||
|
- 会议拆分专用 prompt 设计与迭代
|
||||||
|
- AI 拆分 API 端点:`POST /api/meeting/{taskId}/breakdown`
|
||||||
|
- 批量创建确认端点:`POST /api/meeting/{taskId}/breakdown/confirm`
|
||||||
|
- 在线可用性检测(离线时拒绝 AI 拆分请求)
|
||||||
|
- 复用工单 02 的 `LlmClientService`
|
||||||
|
|
||||||
|
**不包含**:
|
||||||
|
- 单任务拆分(属于工单 02 `AI_BREAKDOWN` 意图)
|
||||||
|
- LLM 基础设施搭建(由工单 02 提供)
|
||||||
|
- 前端审阅 UI(由 03-04 完成)
|
||||||
|
|
||||||
|
## 三、前置条件
|
||||||
|
|
||||||
|
| 条件 | 说明 |
|
||||||
|
|---|---|
|
||||||
|
| 03-01 完成 | `MeetingController` 骨架就绪 |
|
||||||
|
| 工单 02 `LlmClientService` | LLM 调用封装可用 |
|
||||||
|
|
||||||
|
## 四、详细规格
|
||||||
|
|
||||||
|
### 4.1 MeetingAiBreakdownService
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
/// <summary>会议 AI 拆分服务,将会议文字内容分析为待办项建议</summary>
|
||||||
|
public class MeetingAiBreakdownService
|
||||||
|
{
|
||||||
|
private readonly ILlmClientService _llmClient;
|
||||||
|
private readonly MeetingService _meetingService;
|
||||||
|
private readonly ITaskService _taskService;
|
||||||
|
|
||||||
|
public MeetingAiBreakdownService(
|
||||||
|
ILlmClientService llmClient,
|
||||||
|
MeetingService meetingService,
|
||||||
|
ITaskService taskService) { ... }
|
||||||
|
|
||||||
|
/// <summary>分析会议内容,返回待办项建议列表</summary>
|
||||||
|
public async Task<List<MeetingTaskSuggestion>> AnalyzeAsync(
|
||||||
|
int taskId, string? notes = null, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
// 1. 验证 taskId 为 Meeting 类型
|
||||||
|
var meeting = await _meetingService.GetMeetingTaskOrThrow(taskId);
|
||||||
|
|
||||||
|
// 2. 获取会议文字(参数优先,否则取数据库中的 meetingNotes)
|
||||||
|
var meetingText = notes ?? meeting.MeetingNotes;
|
||||||
|
if (string.IsNullOrWhiteSpace(meetingText))
|
||||||
|
throw new BusinessException("会议内容为空,请先输入纪要或上传录音");
|
||||||
|
|
||||||
|
// 3. 构建 prompt,调用 LLM
|
||||||
|
var prompt = BuildBreakdownPrompt(meetingText, meeting.Title);
|
||||||
|
var response = await _llmClient.SendAsync(prompt, ct);
|
||||||
|
|
||||||
|
// 4. 解析 LLM 返回的 JSON 为建议列表
|
||||||
|
return ParseSuggestions(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>确认建议并批量创建子任务</summary>
|
||||||
|
public async Task<BatchCreateResult> ConfirmAndCreateAsync(
|
||||||
|
int taskId, List<SubTaskCreateItem> subTasks, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
// 1. 验证 taskId 为 Meeting 类型
|
||||||
|
await _meetingService.GetMeetingTaskOrThrow(taskId);
|
||||||
|
|
||||||
|
// 2. 批量创建子任务
|
||||||
|
var created = new List<TaskDto>();
|
||||||
|
foreach (var item in subTasks)
|
||||||
|
{
|
||||||
|
var dto = new CreateTaskDto
|
||||||
|
{
|
||||||
|
Title = item.Title,
|
||||||
|
Priority = item.Priority,
|
||||||
|
ParentTaskId = taskId
|
||||||
|
};
|
||||||
|
created.Add(await _taskService.CreateTaskAsync(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new BatchCreateResult { CreatedCount = created.Count, SubTasks = created };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 LLM Prompt 设计
|
||||||
|
|
||||||
|
```
|
||||||
|
System Prompt:
|
||||||
|
你是专业的项目管理和会议纪要分析助手。根据用户提供的会议内容,提取所有需要
|
||||||
|
后续行动的事项,生成待办项建议列表。
|
||||||
|
|
||||||
|
要求:
|
||||||
|
1. 每条建议包含标题(title)、优先级(priority)、原因(reason)
|
||||||
|
2. 标题应简洁明确(15 字以内),如"整理需求文档"、"安排评审会议"
|
||||||
|
3. 优先级:High(2)=紧急重要,Medium(1)=一般,Low(0)=可延迟
|
||||||
|
4. 原因应解释为什么需要这个待办项,引用会议中的具体决策或讨论
|
||||||
|
5. 只提取明确需要执行的事项,不要生成模糊或无来源的建议
|
||||||
|
6. 建议数量根据会议内容合理判断,通常 3-10 条
|
||||||
|
7. 如果会议内容无法提取出明确的行动项,返回空列表
|
||||||
|
|
||||||
|
输出格式(只输出 JSON,不要解释):
|
||||||
|
{
|
||||||
|
"suggestions": [
|
||||||
|
{
|
||||||
|
"title": "待办项标题",
|
||||||
|
"priority": 1,
|
||||||
|
"reason": "原因说明"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 DTO 定义
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
/// <summary>会议任务拆分建议</summary>
|
||||||
|
public class MeetingTaskSuggestion
|
||||||
|
{
|
||||||
|
/// <summary>建议的待办项标题</summary>
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>建议优先级</summary>
|
||||||
|
public TaskPriority Priority { get; set; } = TaskPriority.Medium;
|
||||||
|
|
||||||
|
/// <summary>拆分原因(LLM 输出,用于 UI 展示)</summary>
|
||||||
|
public string Reason { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>拆分请求</summary>
|
||||||
|
public class BreakdownRequest
|
||||||
|
{
|
||||||
|
/// <summary>会议纪要文字(可选,不传则使用已保存的 meetingNotes)</summary>
|
||||||
|
public string? Notes { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>拆分响应</summary>
|
||||||
|
public class BreakdownResponse
|
||||||
|
{
|
||||||
|
public int TaskId { get; set; }
|
||||||
|
public List<MeetingTaskSuggestion> Suggestions { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>批量创建请求中的单个子任务</summary>
|
||||||
|
public class SubTaskCreateItem
|
||||||
|
{
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
public TaskPriority Priority { get; set; } = TaskPriority.Medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>批量确认请求</summary>
|
||||||
|
public class ConfirmBreakdownRequest
|
||||||
|
{
|
||||||
|
public List<SubTaskCreateItem> SubTasks { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>批量创建结果</summary>
|
||||||
|
public class BatchCreateResult
|
||||||
|
{
|
||||||
|
public int CreatedCount { get; set; }
|
||||||
|
public List<TaskDto> SubTasks { get; set; } = new();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 API 端点
|
||||||
|
|
||||||
|
#### POST /api/meeting/{taskId}/breakdown
|
||||||
|
|
||||||
|
- 接收:`BreakdownRequest`
|
||||||
|
- 返回:`ApiResponse<BreakdownResponse>`
|
||||||
|
- 处理:
|
||||||
|
1. 获取会议文字(参数优先于数据库)
|
||||||
|
2. 调用 `MeetingAiBreakdownService.AnalyzeAsync()`
|
||||||
|
3. 返回建议列表
|
||||||
|
- 错误:
|
||||||
|
- 内容为空 → 400 "会议内容为空"
|
||||||
|
- 离线 → 503 "AI 拆分仅在线可用"
|
||||||
|
- taskId 非 Meeting 类型 → 400
|
||||||
|
|
||||||
|
#### POST /api/meeting/{taskId}/breakdown/confirm
|
||||||
|
|
||||||
|
- 接收:`ConfirmBreakdownRequest`
|
||||||
|
- 返回:`ApiResponse<BatchCreateResult>`
|
||||||
|
- 处理:
|
||||||
|
1. 验证 taskId 为 Meeting 类型
|
||||||
|
2. 逐条创建子任务(通过 `TaskService.CreateTaskAsync`)
|
||||||
|
3. 返回创建结果
|
||||||
|
|
||||||
|
### 4.5 LLM 响应解析
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
/// <summary>解析 LLM 返回的 JSON 为建议列表</summary>
|
||||||
|
private List<MeetingTaskSuggestion> ParseSuggestions(string llmResponse)
|
||||||
|
{
|
||||||
|
// 1. 清理 LLM 响应(移除可能的 markdown 代码块包裹、前后空白)
|
||||||
|
var json = CleanJsonResponse(llmResponse);
|
||||||
|
|
||||||
|
// 2. 反序列化
|
||||||
|
var result = JsonSerializer.Deserialize<LlBreakdownResponse>(json);
|
||||||
|
|
||||||
|
// 3. 验证
|
||||||
|
if (result?.Suggestions == null || result.Suggestions.Count == 0)
|
||||||
|
return new List<MeetingTaskSuggestion>();
|
||||||
|
|
||||||
|
// 4. 过滤无效条目(标题为空)
|
||||||
|
return result.Suggestions
|
||||||
|
.Where(s => !string.IsNullOrWhiteSpace(s.Title))
|
||||||
|
.Select(s => new MeetingTaskSuggestion
|
||||||
|
{
|
||||||
|
Title = s.Title.Trim(),
|
||||||
|
Priority = s.Priority,
|
||||||
|
Reason = s.Reason?.Trim() ?? string.Empty
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>LLM 原始响应结构</summary>
|
||||||
|
private class LlBreakdownResponse
|
||||||
|
{
|
||||||
|
public List<MeetingTaskSuggestion> Suggestions { get; set; } = new();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.6 错误处理
|
||||||
|
|
||||||
|
| 场景 | HTTP 状态码 | message |
|
||||||
|
|---|---|---|
|
||||||
|
| 会议文字内容为空 | 400 | 会议内容为空,请先输入纪要或上传录音 |
|
||||||
|
| taskId 不存在 | 404 | 待办项不存在 |
|
||||||
|
| taskId 不是 Meeting 类型 | 400 | 该待办项不是会议类型 |
|
||||||
|
| LLM API 调用失败 | 502 | AI 拆分服务暂时不可用,请稍后重试 |
|
||||||
|
| LLM 返回格式异常 | 500 | AI 拆分结果解析失败 |
|
||||||
|
| 离线 | 503 | AI 拆分仅在线可用 |
|
||||||
|
| 确认时子任务列表为空 | 400 | 请至少选择一个待办项 |
|
||||||
|
|
||||||
|
## 五、验收标准
|
||||||
|
|
||||||
|
| 验收项 | 验证方法 | 预期结果 |
|
||||||
|
|---|---|---|
|
||||||
|
| 会议内容分析 | 提交一段会议纪要文字 | 返回 3-10 条结构化建议 |
|
||||||
|
| 建议质量 | 检查建议的标题、优先级、原因 | 标题明确、优先级合理、原因引用会议内容 |
|
||||||
|
| 空内容拒绝 | 不传 notes 且 meetingNotes 为空 | 返回 400 |
|
||||||
|
| 非 Meeting 类型拒绝 | 传普通待办项 ID | 返回 400 |
|
||||||
|
| 在线依赖 | 离线时请求拆分 | 返回 503 |
|
||||||
|
| 批量创建 | 确认勾选的建议 | 子任务批量创建成功 |
|
||||||
|
| LLM 异常降级 | 模拟 LLM 调用失败 | 返回 502,不影响其他功能 |
|
||||||
|
| 空结果处理 | 会议内容无可提取的待办项 | 返回空建议列表 + 提示信息 |
|
||||||
|
|
||||||
|
## 六、风险
|
||||||
|
|
||||||
|
| 风险 | 影响 | 应对 |
|
||||||
|
|---|---|---|
|
||||||
|
| LLM 返回格式不符 | 建议解析失败 | 增加 JSON 清理逻辑(移除 markdown 包裹、前后文本) |
|
||||||
|
| LLM Token 限制 | 长会议纪要超出上下文窗口 | 限制文本长度(如 4000 字);超长时截断并告知用户 |
|
||||||
|
| Prompt 需要迭代 | 建议质量不达预期 | prompt 作为配置项,支持热更新 |
|
||||||
|
|
||||||
|
## 七、Touch List
|
||||||
|
|
||||||
|
| 文件路径 | 修改类型 |
|
||||||
|
|---|---|
|
||||||
|
| `src/Hua.Todo.Application/Meeting/MeetingAiBreakdownService.cs` | 新增 |
|
||||||
|
| `src/Hua.Todo.Application/Meeting/MeetingController.cs` | 修改 |
|
||||||
|
| `src/Hua.Todo.Application/Meeting/Models/MeetingDtos.cs` | 修改 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**工单编号**:03-03
|
||||||
|
**标题**:AI 任务拆分服务
|
||||||
|
**版本**:v1.3.0
|
||||||
|
**创建日期**:2026-06-16
|
||||||
@@ -0,0 +1,295 @@
|
|||||||
|
# 研发工单 v1.3.0 - 03-04 任务建议与确认 UI
|
||||||
|
|
||||||
|
> 父工单:[03-会议任务拆分](./03-会议任务拆分.md)
|
||||||
|
>
|
||||||
|
> 依赖:03-01、03-03(后端 API 就绪)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、目标
|
||||||
|
|
||||||
|
实现会议任务拆分的完整前端 UI:在 TaskItem 中为 Meeting 类型待办项提供录音/纪要/拆分入口,实现建议审阅与确认对话框,打通从会议内容输入到批量创建子任务的端到端用户体验。
|
||||||
|
|
||||||
|
## 二、范围
|
||||||
|
|
||||||
|
**包含**:
|
||||||
|
- Meeting 类型待办项的特殊 UI(图标、录音/纪要/拆分入口)
|
||||||
|
- 录音控件的嵌入(调用 03-02 的 `AudioRecorder.vue` + `useAudioRecorder`)
|
||||||
|
- 纪要输入区域(textarea 保存会议文字内容)
|
||||||
|
- AI 拆分请求与加载状态
|
||||||
|
- 建议审阅对话框(`MeetingBreakdownDialog.vue`)
|
||||||
|
- 建议勾选、编辑、调整优先级、手动补充
|
||||||
|
- 确认后批量创建子任务
|
||||||
|
- 前端类型扩展(`taskType`、`meetingNotes`、`audioDuration`)
|
||||||
|
|
||||||
|
**不包含**:
|
||||||
|
- 录音控件本身(由 03-02 提供)
|
||||||
|
- 后端逻辑(由 03-01、03-03 提供)
|
||||||
|
|
||||||
|
## 三、前置条件
|
||||||
|
|
||||||
|
| 条件 | 说明 |
|
||||||
|
|---|---|
|
||||||
|
| 03-01 完成 | 后端 API 就绪(meeting 端点 + TaskType) |
|
||||||
|
| 03-02 完成 | AudioRecorder 组件和 useAudioRecorder 可用 |
|
||||||
|
| 03-03 完成 | breakdown/breakdown/confirm 端点可用 |
|
||||||
|
|
||||||
|
## 四、详细规格
|
||||||
|
|
||||||
|
### 4.1 前端类型扩展
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/Hua.Todo.Web/src/types/task.ts
|
||||||
|
|
||||||
|
/// <summary>待办项类型</summary>
|
||||||
|
type TaskType = 0 | 1; // 0=Normal, 1=Meeting
|
||||||
|
|
||||||
|
interface Task {
|
||||||
|
// ... 已有字段 ...
|
||||||
|
/** 待办项类型 */
|
||||||
|
taskType?: TaskType;
|
||||||
|
/** 会议纪要/转写文字 */
|
||||||
|
meetingNotes?: string;
|
||||||
|
/** 录音时长(秒) */
|
||||||
|
audioDuration?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateTaskDto {
|
||||||
|
title: string;
|
||||||
|
priority: TaskPriority;
|
||||||
|
parentTaskId?: number;
|
||||||
|
/** 待办项类型,默认 0 */
|
||||||
|
taskType?: TaskType;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 会议相关类型
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 新增或扩展 meeting.ts 中的类型
|
||||||
|
|
||||||
|
/// <summary>会议任务拆分建议</summary>
|
||||||
|
interface MeetingTaskSuggestion {
|
||||||
|
title: string;
|
||||||
|
priority: TaskPriority;
|
||||||
|
reason: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>拆分响应</summary>
|
||||||
|
interface BreakdownResponse {
|
||||||
|
taskId: number;
|
||||||
|
suggestions: MeetingTaskSuggestion[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>待确认的子任务项</summary>
|
||||||
|
interface PendingSubTask {
|
||||||
|
title: string;
|
||||||
|
priority: TaskPriority;
|
||||||
|
/** 是否被用户勾选 */
|
||||||
|
checked: boolean;
|
||||||
|
/** LLM 拆分的原始建议(含原因) */
|
||||||
|
suggestion?: MeetingTaskSuggestion;
|
||||||
|
/** 是否为用户手动添加的条目 */
|
||||||
|
isManual?: boolean;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 TaskItem.vue 扩展
|
||||||
|
|
||||||
|
为 Meeting 类型的待办项显示额外操作入口:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────┐
|
||||||
|
│ 📋 ☐ 周一产品评审会 🔴 H ··· │
|
||||||
|
│ 🎙 会议 · 📝 有纪要 · 0个子任务 │
|
||||||
|
│ [🎤] [📝] [🤖] │
|
||||||
|
│ ┌─ ✅ 整理需求文档 🔴 │ ← 已有子任务(若有)
|
||||||
|
│ └─ ☐ 安排评审会议 🟡 │
|
||||||
|
└──────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
[🎤] = 录音按钮
|
||||||
|
[📝] = 打开纪要编辑区
|
||||||
|
[🤖] = AI 拆分(需有会议内容)
|
||||||
|
```
|
||||||
|
|
||||||
|
条件渲染逻辑:
|
||||||
|
- `task.taskType === 1`(Meeting)时,显示会议图标 `🎙` 标签
|
||||||
|
- 显示三个操作按钮在任务行右侧
|
||||||
|
- 拆分子任务入口:点击 `[🤖]` → 触发 `handleAiBreakdown()`
|
||||||
|
|
||||||
|
### 4.4 会议纪要编辑区(内联)
|
||||||
|
|
||||||
|
点击 `[📝]` 后展开内联编辑区:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────┐
|
||||||
|
│ 📋 ☐ 周一产品评审会 │
|
||||||
|
│ │
|
||||||
|
│ 会议纪要: │
|
||||||
|
│ ┌──────────────────────────────────────┐ │
|
||||||
|
│ │ 今天的产品评审会主要讨论了三个议题: │ │
|
||||||
|
│ │ 第一,确定 Q3 产品路线图... │ │
|
||||||
|
│ │ 第二,... │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ └──────────────────────────────────────┘ │
|
||||||
|
│ 录音时长:30分15秒 │
|
||||||
|
│ │
|
||||||
|
│ [🔊 开始录音] [🤖 AI拆分] [💾 保存纪要] │
|
||||||
|
└──────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.5 AI 拆分流程(前端)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 在 TaskItem.vue 或拆分逻辑中
|
||||||
|
|
||||||
|
async function handleAiBreakdown(task: Task) {
|
||||||
|
// 1. 检查是否有会议内容
|
||||||
|
if (!task.meetingNotes) {
|
||||||
|
showToast('请先输入纪要或上传录音', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 调用 AI 拆分
|
||||||
|
isLoading.value = true;
|
||||||
|
try {
|
||||||
|
const resp = await meetingApi.requestBreakdown(task.id);
|
||||||
|
if (resp.data.suggestions.length === 0) {
|
||||||
|
showToast('未从会议内容中提取到待办项,请确认纪要内容', 'info');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 打开审阅对话框
|
||||||
|
openBreakdownDialog(task, resp.data.suggestions);
|
||||||
|
} catch (err) {
|
||||||
|
showToast('AI拆分失败,请稍后重试', 'error');
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.6 MeetingBreakdownDialog.vue
|
||||||
|
|
||||||
|
审阅确认对话框:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────┐
|
||||||
|
│ 会议任务拆分 — 周一产品评审会 [✕] │
|
||||||
|
│ │
|
||||||
|
│ AI 根据会议内容生成了以下待办项建议: │
|
||||||
|
│ 请勾选需要创建的条目,可编辑标题和优先级。 │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────────────────────────────┐ │
|
||||||
|
│ │ ☑ 整理产品需求文档 [🔴 高 ▼] │ │
|
||||||
|
│ │ 📝 会议中提到需要在本周五前完成 │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ☑ 安排技术方案评审 [🟡 中 ▼] │ │
|
||||||
|
│ │ 📝 张三负责在下周三前输出技术方案 │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ☑ 跟进客户反馈 [🟢 低 ▼] │ │
|
||||||
|
│ │ 📝 李四反馈了三个客户问题 │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ☐ 项目总结报告 [🟡 中 ▼] │ │
|
||||||
|
│ │ 📝 月末需要汇总各模块进度 │ │
|
||||||
|
│ └──────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ [+ 手动添加待办项] │
|
||||||
|
│ │
|
||||||
|
│ 已选 3 项 │
|
||||||
|
│ [取消] [✅ 确认创建 (3)] │
|
||||||
|
└──────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
交互:
|
||||||
|
- 每条建议默认勾选
|
||||||
|
- 点击标题可编辑(内联 input)
|
||||||
|
- 优先级下拉可调整
|
||||||
|
- 取消勾选的条目不会被创建
|
||||||
|
- `[+ 手动添加]` 新增空白条目
|
||||||
|
- 底部显示已选数量
|
||||||
|
|
||||||
|
### 4.7 确认创建
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function confirmBreakdown() {
|
||||||
|
const selected = pendingSubTasks.value
|
||||||
|
.filter(s => s.checked)
|
||||||
|
.map(s => ({ title: s.title, priority: s.priority }));
|
||||||
|
|
||||||
|
if (selected.length === 0) {
|
||||||
|
showToast('请至少选择一个待办项', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isCreating.value = true;
|
||||||
|
try {
|
||||||
|
const resp = await meetingApi.confirmBreakdown(task.id, selected);
|
||||||
|
showToast(`已成功创建 ${resp.data.createdCount} 个待办项`, 'success');
|
||||||
|
closeDialog();
|
||||||
|
emit('updated'); // 刷新任务列表
|
||||||
|
} catch (err) {
|
||||||
|
showToast('创建失败,请重试', 'error');
|
||||||
|
} finally {
|
||||||
|
isCreating.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.8 创建任务时的类型选择
|
||||||
|
|
||||||
|
在 `TaskList.vue` 的快速创建表单中增加类型选择:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────┐
|
||||||
|
│ [+ 新待办项] [_____输入标题_____] │
|
||||||
|
│ 优先级: [🟡 中 ▼] 类型: [📋 普通 ▼] │
|
||||||
|
│ [🎙 会议] │
|
||||||
|
│ [+ 添加] │
|
||||||
|
└──────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
默认类型为 `Normal`(普通),选择 `Meeting` 后创建的待办项为会议类型。
|
||||||
|
|
||||||
|
## 五、验收标准
|
||||||
|
|
||||||
|
| 验收项 | 验证方法 | 预期结果 |
|
||||||
|
|---|---|---|
|
||||||
|
| 创建 Meeting 类型 | 新建时选择"会议"类型 | 列表中显示 🎙 图标 |
|
||||||
|
| 会议操作按钮 | 展开 Meeting 类型任务 | 显示录音/纪要/拆分三个按钮 |
|
||||||
|
| 普通类型不显示 | 查看普通待办项 | 无会议相关按钮 |
|
||||||
|
| 录音入口 | 点击录音按钮 | 弹出/展开 AudioRecorder 组件 |
|
||||||
|
| 纪要编辑 | 输入会议纪要后保存 | `meetingNotes` 保存成功 |
|
||||||
|
| AI 拆分请求 | 点击 AI 拆分按钮 | 加载状态 → 审阅对话框弹出 |
|
||||||
|
| 无内容拒绝拆分 | 无纪要时点击拆分 | 提示"请先输入纪要" |
|
||||||
|
| 建议审阅 | 审阅对话框中勾选/取消/编辑 | 操作流畅,状态正确 |
|
||||||
|
| 手动添加 | 点击"+ 手动添加" | 新增空白条目,可编辑 |
|
||||||
|
| 批量创建 | 确认勾选的条目 | 子任务创建到父任务下 |
|
||||||
|
| 离线降级提示 | 离线时点击拆分 | 提示"AI 拆分仅在线可用" |
|
||||||
|
| 响应式 | 缩小浏览器窗口 | 对话框和控件正常显示 |
|
||||||
|
|
||||||
|
## 六、风险
|
||||||
|
|
||||||
|
| 风险 | 影响 | 应对 |
|
||||||
|
|---|---|---|
|
||||||
|
| 组件嵌套复杂 | TaskItem 已有子任务递归,再加会议 UI 后层级深 | 会议功能用可选插槽方式注入,不影响普通任务渲染 |
|
||||||
|
| 录音与拆分同时操作 | 用户可能在转写期间又点拆分 | 操作按钮在加载中时禁用 |
|
||||||
|
| 大量建议渲染 | LLM 返回 10+ 条建议时列表长 | 审阅列表设最大高度 + 滚动;建议上限 15 条 |
|
||||||
|
|
||||||
|
## 七、Touch List
|
||||||
|
|
||||||
|
| 文件路径 | 修改类型 |
|
||||||
|
|---|---|
|
||||||
|
| `src/Hua.Todo.Web/src/types/task.ts` | 修改(新增 TaskType、meetingNotes、audioDuration) |
|
||||||
|
| `src/Hua.Todo.Web/src/api/meeting.ts` | 新增 |
|
||||||
|
| `src/Hua.Todo.Web/src/components/AudioRecorder.vue` | 新增(由 03-02 产出,本工单集成) |
|
||||||
|
| `src/Hua.Todo.Web/src/components/MeetingBreakdownDialog.vue` | 新增 |
|
||||||
|
| `src/Hua.Todo.Web/src/components/TaskItem.vue` | 修改(Meeting 类型条件渲染) |
|
||||||
|
| `src/Hua.Todo.Web/src/components/TaskList.vue` | 修改(新建任务时类型选择) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**工单编号**:03-04
|
||||||
|
**标题**:任务建议与确认 UI
|
||||||
|
**版本**:v1.3.0
|
||||||
|
**创建日期**:2026-06-16
|
||||||
@@ -0,0 +1,341 @@
|
|||||||
|
# 研发工单 v1.3.0 - 03 会议任务拆分
|
||||||
|
|
||||||
|
> 术语澄清:本文件中"研发工单"指智能体/开发者执行的**编码工作项**;"任务/待办项/Todo"指 Hua.Todo 业务领域的 **Todo 待办项**(Task 实体),二者请勿混淆。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、目标与范围
|
||||||
|
|
||||||
|
### 1.1 目标
|
||||||
|
|
||||||
|
实现"会议内容 → AI 任务拆分"功能:用户创建一个标记为"会议"类型的 Todo 待办项作为父目录,通过录音或文字输入会议内容,由 AI 分析后生成待办项建议列表,用户确认后批量创建为子任务。
|
||||||
|
|
||||||
|
### 1.2 核心流程
|
||||||
|
|
||||||
|
```
|
||||||
|
用户创建"会议"类型父任务(指定标题,如"周一产品评审会")
|
||||||
|
↓
|
||||||
|
选择输入方式:
|
||||||
|
├─ 录音:录制会议音频 → 系统转写为文字
|
||||||
|
└─ 文字:直接输入/粘贴会议纪要
|
||||||
|
↓
|
||||||
|
AI 分析会议内容 → 生成待办项建议列表
|
||||||
|
(每条建议含:标题、优先级、可选截止日期)
|
||||||
|
↓
|
||||||
|
用户审阅建议列表:
|
||||||
|
├─ 勾选/取消勾选
|
||||||
|
├─ 编辑建议内容(修改标题、调整优先级)
|
||||||
|
└─ 手动补充新条目
|
||||||
|
↓
|
||||||
|
确认 → 批量创建为父任务的子待办项
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 范围
|
||||||
|
|
||||||
|
**包含**:
|
||||||
|
- "会议"类型标记:创建 Todo 时可指定为会议类型,UI 上以图标/标签区分
|
||||||
|
- 音频录制:前端录音控件,录制会议音频
|
||||||
|
- 音频转写:将录音发送至后端,调用 STT 服务转为文字
|
||||||
|
- 文字输入:直接输入/粘贴会议纪要文本
|
||||||
|
- AI 任务拆分:将会议文字内容发送给 LLM,生成结构化待办项建议
|
||||||
|
- 建议审阅与确认 UI:用户勾选、编辑、补充建议后批量创建子任务
|
||||||
|
- 与工单 02 的 LLM 基础设施复用(`LlmClientService`)
|
||||||
|
|
||||||
|
**不包含**:
|
||||||
|
- 实时语音转写(边录边转,后续版本考虑)
|
||||||
|
- 多人协作/会议纪要共享
|
||||||
|
- 音频文件持久存储(录音转写完成后不保留音频,节省空间;如需保留为后续版本需求)
|
||||||
|
- 会议录音的云同步(后续版本)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、前置条件
|
||||||
|
|
||||||
|
| 条件 | 说明 | 状态 |
|
||||||
|
|---|---|---|
|
||||||
|
| Hua.Todo v1.2.0 | 基础业务能力(Task CRUD、父子任务) | 已完成 |
|
||||||
|
| 工单 02 LLM 基础设施 | `LlmClientService`、AI 拆分服务 | 待实现 |
|
||||||
|
| 浏览器 MediaRecorder API | 前端音频录制 | 已就绪(主流浏览器支持) |
|
||||||
|
| 后端 STT 服务 | 音频转文字(可复用工单 02 的 STT 能力或调用第三方 API) | 待实现 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、子工单拆分
|
||||||
|
|
||||||
|
| 子工单 | 标题 | 依赖 | 可并行 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 03-01 | 会议数据模型与 API | 无 | 是 |
|
||||||
|
| 03-02 | 音频录制与转写 | 03-01(API 契约) | 部分(前端录音 UI 可并行) |
|
||||||
|
| 03-03 | AI 任务拆分服务 | 03-01、工单 02 LlmClientService | 部分(prompt 设计可并行) |
|
||||||
|
| 03-04 | 任务建议与确认 UI | 03-01、03-03 | 否(依赖后端 API 就绪) |
|
||||||
|
|
||||||
|
### 执行顺序建议
|
||||||
|
|
||||||
|
```
|
||||||
|
03-01(模型与API) ──────┐
|
||||||
|
├──→ 03-04(确认UI)
|
||||||
|
03-02(录制与转写) ──────┤
|
||||||
|
│
|
||||||
|
03-03(AI拆分服务) ──────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
03-01、03-02、03-03 可部分并行推进;03-04 需等前三者 API 就绪后开始。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、架构设计
|
||||||
|
|
||||||
|
### 4.1 整体架构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────┐
|
||||||
|
│ Vue 前端 │
|
||||||
|
│ ┌─────────────┐ ┌─────────────┐ ┌────────────┐ │
|
||||||
|
│ │ 录音控件 │ │ 文字输入区 │ │ 建议审阅 │ │
|
||||||
|
│ │ MediaRecorder│ │ Textarea │ │ 确认对话框 │ │
|
||||||
|
│ └──────┬──────┘ └──────┬──────┘ └─────┬──────┘ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ ▼ ▼ │ │
|
||||||
|
│ ┌──────────────────────────────┐ │ │
|
||||||
|
│ │ meetingApi (前端 API 模块) │ │ │
|
||||||
|
│ └──────────────┬───────────────┘ │ │
|
||||||
|
└─────────────────┼──────────────────────┼─────────┘
|
||||||
|
│ HTTP │
|
||||||
|
▼ │
|
||||||
|
┌─────────────────────────────────────────┤
|
||||||
|
│ 后端 (Application 层) │
|
||||||
|
│ ┌────────────┐ ┌─────────────────┐ │
|
||||||
|
│ │ Meeting │ │ MeetingAi │ │
|
||||||
|
│ │ Controller │ │ BreakdownService│ │
|
||||||
|
│ └─────┬──────┘ └───────┬─────────┘ │
|
||||||
|
│ │ │ │
|
||||||
|
│ ┌─────▼──────┐ ┌──────▼─────────┐ │
|
||||||
|
│ │ Meeting │ │ LlmClient │ │
|
||||||
|
│ │ Service │ │ Service (复用02)│ │
|
||||||
|
│ └─────┬──────┘ └────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌─────▼──────┐ │
|
||||||
|
│ │ STT 服务 │ │
|
||||||
|
│ │ (转写音频) │ │
|
||||||
|
│ └────────────┘ │
|
||||||
|
└─────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 数据模型扩展
|
||||||
|
|
||||||
|
在现有 `TaskEntity` 上新增 `TaskType` 字段,区分普通待办项与会议:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
/// <summary>待办项类型枚举</summary>
|
||||||
|
public enum TaskType
|
||||||
|
{
|
||||||
|
/// <summary>普通待办项(默认)</summary>
|
||||||
|
Normal = 0,
|
||||||
|
/// <summary>会议(可包含录音、纪要,支持 AI 任务拆分)</summary>
|
||||||
|
Meeting = 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`TaskEntity` 新增字段:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 默认值 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `TaskType` | `TaskType` | `Normal` | 待办项类型 |
|
||||||
|
| `MeetingNotes` | `string?` | null | 会议纪要/转写文字(仅 Meeting 类型有值) |
|
||||||
|
| `AudioDuration` | `double?` | null | 录音时长(秒),仅 Meeting 类型有值 |
|
||||||
|
|
||||||
|
> 注意:音频文件本身不持久存储,转写完成后仅保留文字结果。`AudioDuration` 用于 UI 展示录音时长。
|
||||||
|
|
||||||
|
### 4.3 API 设计
|
||||||
|
|
||||||
|
#### 4.3.1 创建会议类型待办项
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/task
|
||||||
|
请求体:
|
||||||
|
{
|
||||||
|
"title": "周一产品评审会",
|
||||||
|
"priority": 1,
|
||||||
|
"taskType": 1 // TaskType.Meeting = 1
|
||||||
|
}
|
||||||
|
响应体:
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"id": 42,
|
||||||
|
"title": "周一产品评审会",
|
||||||
|
"taskType": 1,
|
||||||
|
"meetingNotes": null,
|
||||||
|
"audioDuration": null,
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.3.2 上传录音并转写
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/meeting/{taskId}/transcribe
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
字段:
|
||||||
|
- audio: 音频文件(webm/wav/mp3)
|
||||||
|
- format: 音频格式(可选,默认从文件扩展名推断)
|
||||||
|
|
||||||
|
响应体:
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"taskId": 42,
|
||||||
|
"transcript": "今天的产品评审会主要讨论了三个议题:第一...",
|
||||||
|
"audioDuration": 1830.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.3.3 保存会议纪要(文字输入)
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/meeting/{taskId}/notes
|
||||||
|
请求体:
|
||||||
|
{
|
||||||
|
"notes": "今天的产品评审会主要讨论了三个议题:第一..."
|
||||||
|
}
|
||||||
|
响应体:
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"taskId": 42,
|
||||||
|
"meetingNotes": "今天的产品评审会主要讨论了三个议题:第一..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.3.4 AI 任务拆分建议
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/meeting/{taskId}/breakdown
|
||||||
|
请求体:
|
||||||
|
{
|
||||||
|
"notes": "..." // 可选,若不传则使用已保存的 meetingNotes
|
||||||
|
}
|
||||||
|
响应体:
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"taskId": 42,
|
||||||
|
"suggestions": [
|
||||||
|
{
|
||||||
|
"title": "整理产品需求文档",
|
||||||
|
"priority": 2,
|
||||||
|
"reason": "会议中提到需要在本周五前完成需求文档的整理"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "安排技术方案评审",
|
||||||
|
"priority": 1,
|
||||||
|
"reason": "张三负责在下周三前输出技术方案"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "跟进客户反馈",
|
||||||
|
"priority": 0,
|
||||||
|
"reason": "李四反馈了三个客户问题,需后续跟进"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.3.5 确认并批量创建子任务
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/meeting/{taskId}/breakdown/confirm
|
||||||
|
请求体:
|
||||||
|
{
|
||||||
|
"subTasks": [
|
||||||
|
{ "title": "整理产品需求文档", "priority": 2 },
|
||||||
|
{ "title": "安排技术方案评审", "priority": 1 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
响应体:
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"createdCount": 2,
|
||||||
|
"subTasks": [
|
||||||
|
{ "id": 43, "title": "整理产品需求文档", "priority": 2, "parentTaskId": 42 },
|
||||||
|
{ "id": 44, "title": "安排技术方案评审", "priority": 1, "parentTaskId": 42 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、与工单 02 的边界
|
||||||
|
|
||||||
|
| 能力 | 工单 02 | 工单 03 |
|
||||||
|
|---|---|---|
|
||||||
|
| LLM 调用 | `LlmClientService` 基础设施 | 复用 `LlmClientService`,新增会议拆分专用 prompt |
|
||||||
|
| 语音输入 | STT 平台原生能力(语音指令) | 前端 MediaRecorder 录音 → 后端 STT 转写(场景不同) |
|
||||||
|
| AI 拆分 | 对已有单个任务做拆分(`AI_BREAKDOWN` 意图) | 对会议内容做拆分(输入为长文本,输出为多条建议) |
|
||||||
|
| 确认流程 | 语音确认/点选候选 | 专门的审阅 UI(勾选、编辑、补充) |
|
||||||
|
|
||||||
|
**复用关系**:
|
||||||
|
- `LlmClientService`:03 直接复用 02 的 LLM 调用封装
|
||||||
|
- `AiBreakdownService`:02 的单任务拆分服务,03 不直接复用(输入形式和输出结构不同),但设计上保持一致的调用模式
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、验收标准
|
||||||
|
|
||||||
|
| 验收项 | 验证方法 | 预期结果 |
|
||||||
|
|---|---|---|
|
||||||
|
| 创建会议类型待办项 | 创建 Todo 时选择"会议"类型 | 创建成功,列表中显示会议图标/标签 |
|
||||||
|
| 录音功能 | 点击录音按钮,录制一段音频 | 录音控件正常工作,显示录音时长 |
|
||||||
|
| 录音转写 | 录音完成后提交 | 后端返回转写文字,文字内容基本准确 |
|
||||||
|
| 文字输入纪要 | 在会议待办项中粘贴会议纪要 | 保存成功,再次打开可见纪要内容 |
|
||||||
|
| AI 拆分建议 | 提交会议内容请求 AI 拆分 | 返回 3-10 条建议,每条含标题+优先级+理由 |
|
||||||
|
| 建议审阅 | 勾选/取消/编辑建议 | UI 支持勾选、编辑标题和优先级 |
|
||||||
|
| 批量创建 | 确认勾选的建议 | 子任务批量创建到父任务下 |
|
||||||
|
| 离线降级 | 离线时请求 AI 拆分 | 提示"离线模式不支持 AI 拆分" |
|
||||||
|
| 非会议类型 | 普通待办项不显示录音/拆分入口 | 功能入口仅对 Meeting 类型显示 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、风险与回滚
|
||||||
|
|
||||||
|
| 风险 | 影响 | 应对策略 |
|
||||||
|
|---|---|---|
|
||||||
|
| STT 转写准确率不足 | 会议纪要质量差,影响 AI 拆分效果 | 支持文字输入作为备选;转写后允许用户编辑修正 |
|
||||||
|
| LLM API 不稳定 | AI 拆分功能不可用 | 功能降级,其他会议功能(录音、纪要)不受影响 |
|
||||||
|
| 录音文件过大 | 上传超时/存储压力大 | 前端限制录音时长(建议最长 2 小时);压缩音频格式 |
|
||||||
|
| 浏览器 MediaRecorder 兼容性 | 部分平台录音功能不可用 | 降级提示"当前浏览器不支持录音,请使用文字输入" |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、Touch List
|
||||||
|
|
||||||
|
| 文件路径 | 修改类型 | 是否共享 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `src/Hua.Todo.Core/Entities/TaskType.cs` | 新增 | 否 | TaskType 枚举 |
|
||||||
|
| `src/Hua.Todo.Core/Entities/TaskEntity.cs` | 修改 | 是 | 新增 TaskType/MeetingNotes/AudioDuration 字段 |
|
||||||
|
| `src/Hua.Todo.Application/Data/TodoDbContext.cs` | 修改 | 是 | 新增字段映射 + 迁移 |
|
||||||
|
| `src/Hua.Todo.Application/Models/TaskModels.cs` | 修改 | 是 | DTO 扩展(CreateTaskDto/TaskDto 新增字段) |
|
||||||
|
| `src/Hua.Todo.Application/Meeting/` | 新增目录 | 否 | 会议相关服务目录 |
|
||||||
|
| `src/Hua.Todo.Application/Meeting/MeetingController.cs` | 新增 | 否 | 会议 API 端点 |
|
||||||
|
| `src/Hua.Todo.Application/Meeting/MeetingService.cs` | 新增 | 否 | 会议业务逻辑 |
|
||||||
|
| `src/Hua.Todo.Application/Meeting/MeetingAiBreakdownService.cs` | 新增 | 否 | 会议 AI 拆分服务 |
|
||||||
|
| `src/Hua.Todo.Application/Meeting/SttService.cs` | 新增 | 否 | 音频转写服务 |
|
||||||
|
| `src/Hua.Todo.Application/Meeting/Models/` | 新增 | 否 | 会议相关 DTO |
|
||||||
|
| `src/Hua.Todo.Web/src/api/meeting.ts` | 新增 | 否 | 前端会议 API 模块 |
|
||||||
|
| `src/Hua.Todo.Web/src/composables/useAudioRecorder.ts` | 新增 | 否 | 前端录音组合式函数 |
|
||||||
|
| `src/Hua.Todo.Web/src/components/MeetingBreakdownDialog.vue` | 新增 | 否 | 会议任务拆分审阅对话框 |
|
||||||
|
| `src/Hua.Todo.Web/src/components/MeetingTaskItem.vue` | 新增 | 否 | 会议类型待办项(含录音/纪要入口) |
|
||||||
|
| `src/Hua.Todo.Web/src/components/AudioRecorder.vue` | 新增 | 否 | 录音控件 |
|
||||||
|
| `src/Hua.Todo.Web/src/types/task.ts` | 修改 | 是 | 类型扩展(taskType/meetingNotes) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**工单编号**:03
|
||||||
|
**标题**:会议任务拆分
|
||||||
|
**版本**:v1.3.0
|
||||||
|
**创建日期**:2026-06-16
|
||||||
@@ -0,0 +1,554 @@
|
|||||||
|
# 研发工单 v1.3.0 - 04 富文本描述、附件与外部链接
|
||||||
|
|
||||||
|
> 术语澄清:本文件中"研发工单"指智能体/开发者执行的**编码工作项**;"任务/待办项/Todo"指 Hua.Todo 业务领域的 **Todo 待办项**(Task 实体),二者请勿混淆。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、目标与范围
|
||||||
|
|
||||||
|
### 1.1 目标
|
||||||
|
|
||||||
|
当前 Todo 待办项仅包含单行标题(`Title`),无法满足"一个待办项关联更多上下文信息"的需求。本工单为 Todo 待办项新增三项能力:
|
||||||
|
|
||||||
|
| 能力 | 描述 |
|
||||||
|
|---|---|
|
||||||
|
| **多行描述(Description)** | 每个待办项可附带一段多行文字描述,用于记录详细说明、步骤、备注等 |
|
||||||
|
| **文件附件(Attachments)** | 可为一个待办项关联多个本地文件,支持上传、查看、下载、删除 |
|
||||||
|
| **外部链接与程序启动** | 附件可以是外部 URL(点击在系统默认浏览器打开)或本地文件路径/可执行文件(点击通过系统关联程序打开) |
|
||||||
|
|
||||||
|
### 1.2 平台范围
|
||||||
|
|
||||||
|
| 平台 | 是否支持 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| Windows(MAUI) | ✅ 支持 | 全功能:描述编辑、附件管理、外部程序/链接打开 |
|
||||||
|
| Linux(Avalonia) | ✅ 支持 | 全功能;`xdg-open` 打开外部资源 |
|
||||||
|
| macOS(MAUI) | 待定 | 视为 Windows 同级,但本期不单独投入 |
|
||||||
|
| iOS / Android | ❌ 不支持 | 移动端不提供附件/描述编辑入口,但**移动端编辑待办项时不得覆盖/清空已有描述和附件数据**(详见 1.4 节) |
|
||||||
|
|
||||||
|
### 1.3 范围
|
||||||
|
|
||||||
|
**包含**:
|
||||||
|
|
||||||
|
- `TaskEntity` 新增 `Description` 字段(多行文本)
|
||||||
|
- 新增 `AttachmentEntity` 数据模型(附件元数据)
|
||||||
|
- 附件 CRUD API(上传、列表、下载、删除、打开)
|
||||||
|
- 前端编辑对话框扩展(描述 textarea + 附件管理区域)
|
||||||
|
- 前端待办项列表/详情展示(描述预览、附件数量角标)
|
||||||
|
- 桌面端通过系统关联程序打开附件(URL → 浏览器,文件路径 → 关联应用)
|
||||||
|
|
||||||
|
**不包含**:
|
||||||
|
|
||||||
|
- 附件云同步(本期不涉及,后续版本规划)
|
||||||
|
- 附件预览(如图片缩略图、PDF 内嵌预览,本期不做)
|
||||||
|
- 移动端附件管理(本期仅桌面端)
|
||||||
|
- 富文本编辑器(如 Markdown 渲染,本期仅纯文本多行)
|
||||||
|
|
||||||
|
### 1.4 跨平台数据安全:移动端编辑不得破坏已有数据(强制)
|
||||||
|
|
||||||
|
**背景**:移动端(iOS/Android)不提供描述/附件的编辑 UI,但用户仍可在移动端修改待办项标题、优先级、完成状态等基础字段。如果没有防护,移动端发起更新请求时会用"空值"覆盖掉桌面端已设置的 `Description` 和 `Attachments`,导致数据丢失。
|
||||||
|
|
||||||
|
**约束**:
|
||||||
|
|
||||||
|
| 约束项 | 要求 |
|
||||||
|
|---|---|
|
||||||
|
| **后端 API 必须支持真正的部分更新** | `PUT /api/task/{id}` 的请求体中,**未传递的字段保持原值不变**,不得将缺失字段视为"置空"。即:`description` 不在 JSON 中 → 不修改数据库中的 `Description`;`description` 为 `null` → 清空 |
|
||||||
|
| **UpdateTaskDto 所有扩展字段均为 optional** | `description`、`attachments` 相关字段在 DTO 中均标记为可选(`string?` / `null`),与必填字段(`id`)区分 |
|
||||||
|
| **移动端前端不传递未知字段** | 移动端构建 `UpdateTaskDto` 时只传 `id`、`title`、`priority`、`isCompleted`,不传 `description`、`attachments`。后端对这些字段视为"不修改" |
|
||||||
|
| **Attachment 实体独立于 Task 更新** | 附件 CRUD 走独立端点(`/api/task/{id}/attachments`),不通过 `PUT /api/task/{id}` 携带。移动端不调附件端点,自然不会破坏附件数据 |
|
||||||
|
|
||||||
|
**验证方式**:
|
||||||
|
1. 桌面端创建待办项,添加描述 + 上传附件
|
||||||
|
2. 移动端编辑同一待办项(只改标题),保存
|
||||||
|
3. 回到桌面端查看 → 描述和附件完整保留,未被覆盖
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、前置条件
|
||||||
|
|
||||||
|
| 条件 | 说明 | 状态 |
|
||||||
|
|---|---|---|
|
||||||
|
| Hua.Todo v1.2.0 | 基础业务能力(Task CRUD、父子任务) | 已完成 |
|
||||||
|
| 工单 03 TaskType 枚举 | `TaskEntity` 已有字段扩展模式可参考 | 待实现(03 先于 04 或并行) |
|
||||||
|
| 桌面 WebView 文件选择 | 前端 `<input type="file">` 能力 | 已就绪 |
|
||||||
|
| 桌面程序启动 | 通过后端 `Process.Start` / `xdg-open` 实现 | 需新增 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、功能入口覆盖检查
|
||||||
|
|
||||||
|
> 依据 [.trae/rules/项目/05-多入口功能同步规范.md](../../../.trae/rules/项目/05-多入口功能同步规范.md),新功能必须检查两个入口覆盖情况。
|
||||||
|
|
||||||
|
| 入口 | 已规划 | 方案 |
|
||||||
|
|---|---|---|
|
||||||
|
| UI | ✅ | 编辑对话框新增描述 textarea + 附件列表区域;TaskItem 卡片显示描述预览与附件数量角标 |
|
||||||
|
| 语音 | ⚠️ 部分 | 语音可追加/编辑描述文本(通过"给「标题」添加描述"意图);附件上传与外部程序启动不适合语音入口 |
|
||||||
|
|
||||||
|
> 语音入口覆盖说明:
|
||||||
|
> - **支持**:通过语音添加/修改待办项描述("给「XXX」添加备注:...")
|
||||||
|
> - **不支持**:语音上传附件、语音打开外部程序 —— 两者本质上是可视化交互,语音不是合适的入口。本期不覆盖。
|
||||||
|
> - TODO:工单 02 的 LLM intent parser prompt 需预留 `SET_DESCRIPTION` 意图,参数为 `targetTask` + `description`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、数据模型设计
|
||||||
|
|
||||||
|
### 4.1 TaskEntity 扩展
|
||||||
|
|
||||||
|
在现有 `TaskEntity` 上新增一个字段:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
/// <summary>
|
||||||
|
/// 多行描述文本(可为空)。
|
||||||
|
/// 用于记录待办项相关的详细说明、步骤或备注。
|
||||||
|
/// </summary>
|
||||||
|
public string? Description { get; set; }
|
||||||
|
```
|
||||||
|
|
||||||
|
| 字段 | 类型 | 默认值 | 约束 | 说明 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `Description` | `string?` | null | 最大 5000 字符 | 多行描述文本 |
|
||||||
|
|
||||||
|
### 4.2 AttachmentEntity(新增)
|
||||||
|
|
||||||
|
在 `Hua.Todo.Core/Entities/` 下新增 `AttachmentEntity.cs`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
/// <summary>附件类型枚举</summary>
|
||||||
|
public enum AttachmentType
|
||||||
|
{
|
||||||
|
/// <summary>本地文件(已复制到应用数据目录)</summary>
|
||||||
|
LocalFile = 0,
|
||||||
|
/// <summary>外部链接(URL)</summary>
|
||||||
|
ExternalLink = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>附件实体,表示待办项关联的文件或外部链接</summary>
|
||||||
|
public class AttachmentEntity
|
||||||
|
{
|
||||||
|
/// <summary>附件唯一标识符</summary>
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>所属待办项ID</summary>
|
||||||
|
public int TaskId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>显示名称(用户可见的文件名或链接标题)</summary>
|
||||||
|
public string FileName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>存储路径(本地文件的相对路径)或外部URL</summary>
|
||||||
|
public string FilePath { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>文件大小(字节),外部链接为 0</summary>
|
||||||
|
public long FileSize { get; set; }
|
||||||
|
|
||||||
|
/// <summary>MIME 类型(如 text/plain、application/pdf),外部链接为空字符串</summary>
|
||||||
|
public string ContentType { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>附件类型</summary>
|
||||||
|
public AttachmentType AttachmentType { get; set; } = AttachmentType.LocalFile;
|
||||||
|
|
||||||
|
/// <summary>创建时间(UTC)</summary>
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
// 导航属性
|
||||||
|
public TaskEntity Task { get; set; } = null!;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 字段 | 类型 | 默认值 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `Id` | `int` | 自增 | 主键 |
|
||||||
|
| `TaskId` | `int` | - | FK → Tasks.Id |
|
||||||
|
| `FileName` | `string` | - | 显示名称,最大 256 字符 |
|
||||||
|
| `FilePath` | `string` | - | 存储路径或外部 URL,最大 1024 字符 |
|
||||||
|
| `FileSize` | `long` | 0 | 字节数 |
|
||||||
|
| `ContentType` | `string` | "" | MIME 类型 |
|
||||||
|
| `AttachmentType` | `AttachmentType` | `LocalFile` | 本地文件 vs 外部链接 |
|
||||||
|
| `CreatedAt` | `DateTime` | `UtcNow` | 创建时间 |
|
||||||
|
|
||||||
|
`TaskEntity` 新增导航属性:
|
||||||
|
```csharp
|
||||||
|
public List<AttachmentEntity> Attachments { get; set; } = new();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 数据库表
|
||||||
|
|
||||||
|
| 表名 | 说明 |
|
||||||
|
|---|---|
|
||||||
|
| `T_Attachments` | 附件表 |
|
||||||
|
|
||||||
|
> 注:表名沿用 ABP 模板规范 `T_{实体名}s`(见 [.trae/rules/项目/03-数据模型与迁移约束.md](../../../.trae/rules/项目/03-数据模型与迁移约束.md))。
|
||||||
|
|
||||||
|
### 4.4 附件存储策略
|
||||||
|
|
||||||
|
| 策略项 | 决定 |
|
||||||
|
|---|---|
|
||||||
|
| 存储位置 | 应用数据目录下 `Attachments/` 子目录(与数据库 `.db` 同级) |
|
||||||
|
| 文件命名 | `{attachmentId}_{originalFileName}`,避免重名冲突 |
|
||||||
|
| 文件大小上限 | 单文件 50MB(`appsettings.json` 可配置) |
|
||||||
|
| 总附件数上限 | 每个待办项最多 20 个附件 |
|
||||||
|
| 外部链接 | 不复制文件,仅存储 URL 字符串;打开时调用系统默认浏览器 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、API 设计
|
||||||
|
|
||||||
|
### 5.1 已修改:创建/更新待办项(扩展 Description)
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/task
|
||||||
|
请求体新增字段:
|
||||||
|
{
|
||||||
|
"title": "...",
|
||||||
|
"priority": 1,
|
||||||
|
"description": "多行描述文本(可选)" // ← 新增
|
||||||
|
}
|
||||||
|
|
||||||
|
PUT /api/task/{id}
|
||||||
|
请求体新增字段(所有扩展字段均为可选):
|
||||||
|
{
|
||||||
|
"id": 42,
|
||||||
|
"title": "...",
|
||||||
|
"priority": 1,
|
||||||
|
"description": "..." // ← 可选;不传=保持原值,传 null=清空
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **部分更新语义(强制)**:`PUT` 端点必须实现"仅更新已传递字段"的逻辑。
|
||||||
|
> - `description` 字段**不在 JSON 中** → 数据库 `Description` 保持原值
|
||||||
|
> - `description` 字段**为 `null`** → 数据库 `Description` 清空为 `null`
|
||||||
|
> - 此设计确保移动端不传递 `description` 时不会意外清空已有描述
|
||||||
|
|
||||||
|
### 5.2 附件上传
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/task/{taskId}/attachments
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
字段:
|
||||||
|
- file: 文件内容(必填,最大 50MB)
|
||||||
|
|
||||||
|
响应体:
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"id": 1,
|
||||||
|
"taskId": 42,
|
||||||
|
"fileName": "需求文档.pdf",
|
||||||
|
"fileSize": 204800,
|
||||||
|
"contentType": "application/pdf",
|
||||||
|
"attachmentType": 0,
|
||||||
|
"createdAt": "2026-06-16T10:00:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 添加外部链接
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/task/{taskId}/attachments/link
|
||||||
|
请求体:
|
||||||
|
{
|
||||||
|
"url": "https://example.com/doc",
|
||||||
|
"fileName": "参考文档" // 可选,不传则使用 URL 作为显示名
|
||||||
|
}
|
||||||
|
|
||||||
|
响应体:
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"id": 2,
|
||||||
|
"taskId": 42,
|
||||||
|
"fileName": "参考文档",
|
||||||
|
"filePath": "https://example.com/doc",
|
||||||
|
"fileSize": 0,
|
||||||
|
"contentType": "",
|
||||||
|
"attachmentType": 1,
|
||||||
|
"createdAt": "2026-06-16T10:00:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.4 获取附件列表
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/task/{taskId}/attachments
|
||||||
|
|
||||||
|
响应体:
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"fileName": "需求文档.pdf",
|
||||||
|
"fileSize": 204800,
|
||||||
|
"contentType": "application/pdf",
|
||||||
|
"attachmentType": 0,
|
||||||
|
"createdAt": "2026-06-16T10:00:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"fileName": "参考文档",
|
||||||
|
"filePath": "https://example.com/doc",
|
||||||
|
"attachmentType": 1,
|
||||||
|
"createdAt": "2026-06-16T10:05:00Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.5 下载附件
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/attachments/{attachmentId}/download
|
||||||
|
|
||||||
|
响应:文件流(Content-Disposition: attachment; filename="需求文档.pdf")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.6 打开附件(桌面端)
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/attachments/{attachmentId}/open
|
||||||
|
|
||||||
|
响应体:
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"opened": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 后端行为:
|
||||||
|
// - AttachmentType.LocalFile → Process.Start(filePath)(Windows)或 xdg-open(Linux)
|
||||||
|
// - AttachmentType.ExternalLink → Process.Start(url)(在默认浏览器打开)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.7 删除附件
|
||||||
|
|
||||||
|
```
|
||||||
|
DELETE /api/task/{taskId}/attachments/{attachmentId}
|
||||||
|
|
||||||
|
响应体:
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "附件已删除"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、前端设计
|
||||||
|
|
||||||
|
### 6.1 类型扩展
|
||||||
|
|
||||||
|
`src/Hua.Todo.Web/src/types/task.ts` 新增 / 修改:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Task 接口新增字段
|
||||||
|
export interface Task {
|
||||||
|
// ... 既有字段
|
||||||
|
description?: string; // 多行描述
|
||||||
|
attachments?: AttachmentItem[]; // 附件列表
|
||||||
|
attachmentCount?: number; // 附件数量(列表视图用,不传完整列表)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AttachmentItem {
|
||||||
|
id: number;
|
||||||
|
taskId: number;
|
||||||
|
fileName: string;
|
||||||
|
filePath: string;
|
||||||
|
fileSize: number;
|
||||||
|
contentType: string;
|
||||||
|
attachmentType: AttachmentType; // 0=本地文件, 1=外部链接
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AttachmentType = 0 | 1;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 组件变更
|
||||||
|
|
||||||
|
| 组件 | 变更类型 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `TaskEditDialog.vue` | 修改 | 新增描述 textarea + 附件管理区域(上传按钮、附件列表) |
|
||||||
|
| `TaskItem.vue` | 修改 | 卡片底部显示描述预览(单行截断)+ 附件数量角标 |
|
||||||
|
| `AttachmentList.vue` | 新增 | 附件列表组件:文件名、大小、类型图标、删除按钮、打开按钮 |
|
||||||
|
| `LinkInputDialog.vue` | 新增 | 外部链接输入弹出框(URL + 显示名称) |
|
||||||
|
|
||||||
|
### 6.3 新增 API 模块
|
||||||
|
|
||||||
|
`src/Hua.Todo.Web/src/api/attachments.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// uploadAttachment(taskId, file) — POST multipart/form-data
|
||||||
|
// addLink(taskId, url, fileName?) — POST /api/task/{id}/attachments/link
|
||||||
|
// getAttachments(taskId) — GET
|
||||||
|
// deleteAttachment(taskId, attId) — DELETE
|
||||||
|
// openAttachment(attId) — POST /api/attachments/{id}/open
|
||||||
|
// downloadAttachment(attId) — GET /api/attachments/{id}/download
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.4 交互流程
|
||||||
|
|
||||||
|
```
|
||||||
|
编辑待办项对话框
|
||||||
|
├─ 标题输入框(现有)
|
||||||
|
├─ 优先级选择(现有)
|
||||||
|
├─ 描述 textarea(新增:多行文本,placeholder "添加备注、步骤说明...")
|
||||||
|
├─ 附件区域(新增)
|
||||||
|
│ ├─ [+ 上传文件] 按钮 → 触发文件选择器 → 上传至后端
|
||||||
|
│ ├─ [+ 添加链接] 按钮 → 弹出 URL 输入框 → 保存为外部链接附件
|
||||||
|
│ └─ 附件列表(每项显示:图标 + 文件名 + 大小 + [打开] [删除])
|
||||||
|
│ ├─ 本地文件:[打开] → 调用后端 open API → 系统关联程序打开
|
||||||
|
│ ├─ 外部链接:[打开] → 调用后端 open API → 浏览器打开
|
||||||
|
│ └─ [删除] → 确认 → 删除附件
|
||||||
|
└─ [取消] [保存]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.5 列表卡片展示
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ ☐ 整理产品需求文档 │
|
||||||
|
│ 高优先级 │
|
||||||
|
│ 需要在本周五前完成需求文档的整理... │ ← 描述预览(单行,超长截断)
|
||||||
|
│ 📎 2 个附件 │ ← 附件数量角标
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、后端架构
|
||||||
|
|
||||||
|
### 7.1 新增服务
|
||||||
|
|
||||||
|
| 服务/类 | 位置 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `AttachmentEntity` | `Hua.Todo.Core/Entities/` | 附件实体 |
|
||||||
|
| `AttachmentType` | `Hua.Todo.Core/Entities/` | 附件类型枚举 |
|
||||||
|
| `IAttachmentRepository` | `Hua.Todo.Core/Repositories/` | 仓储接口 |
|
||||||
|
| `AttachmentRepository` | `Hua.Todo.Application/Data/` | EF Core 仓储实现 |
|
||||||
|
| `AttachmentService` | `Hua.Todo.Application/Services/` | 附件业务逻辑(上传、下载、打开、清理) |
|
||||||
|
| `IAttachmentService` | `Hua.Todo.Application/Services/` | 服务接口 |
|
||||||
|
| `AttachmentController` | `Hua.Todo.Application/DynamicApi/` 或独立 Controller | 附件动态 API 端点(或手动 Controller) |
|
||||||
|
| `PlatformAttachmentOpener` | `Hua.Todo.{Maui,Avalonia}/Services/` | 平台特定文件/链接打开实现(`Process.Start` / `xdg-open`) |
|
||||||
|
|
||||||
|
### 7.2 平台差异:外部程序启动
|
||||||
|
|
||||||
|
| 平台 | 实现方式 |
|
||||||
|
|---|---|
|
||||||
|
| Windows(MAUI) | `System.Diagnostics.Process.Start(new ProcessStartInfo { FileName = path, UseShellExecute = true })` |
|
||||||
|
| Linux(Avalonia) | `System.Diagnostics.Process.Start("xdg-open", path)` |
|
||||||
|
|
||||||
|
通过 `IPlatformAttachmentOpener` 接口在 Core 定义,MAUI / Avalonia 宿主各自实现并注册。
|
||||||
|
|
||||||
|
### 7.3 附件文件管理
|
||||||
|
|
||||||
|
- 上传的本地文件复制到 `<AppData>/Hua.Todo/Attachments/{attachmentId}_{originalFileName}`
|
||||||
|
- 删除附件时同步删除磁盘文件
|
||||||
|
- 删除待办项时级联删除其所有附件(实体 + 文件)
|
||||||
|
- 启动时验证附件文件完整性(数据库有记录但文件缺失 → 标记为失效,UI 提示)
|
||||||
|
|
||||||
|
### 7.4 安全约束
|
||||||
|
|
||||||
|
| 约束 | 说明 |
|
||||||
|
|---|---|
|
||||||
|
| 文件类型白名单 | 不限制(本地工具场景,用户自行管理) |
|
||||||
|
| 文件大小上限 | 50MB(可配置) |
|
||||||
|
| 路径遍历防护 | 文件名去除 `../`、`..\` 等危险路径片段 |
|
||||||
|
| 外部链接验证 | 仅允许 `http://`、`https://` 协议 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、子工单拆分
|
||||||
|
|
||||||
|
本工单体积适中,不拆分子工单,单文件完整实现。
|
||||||
|
|
||||||
|
如需与工单 03 并行推进,注意以下共享文件:
|
||||||
|
|
||||||
|
| 共享文件 | Writer | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `TaskEntity.cs` | **工单 03** 先写入 | 03 新增 TaskType/MeetingNotes/AudioDuration;04 后续追加 Description/Attachments 导航属性 |
|
||||||
|
| `todoDbContext.cs` | **工单 03** 先写入 | 03 新增 TaskType 映射;04 后续追加 Attachments DbSet + 关系映射 |
|
||||||
|
| `task.ts` | **工单 03** 先写入 | 03 新增 taskType;04 后追加 description、attachments |
|
||||||
|
| `TaskEditDialog.vue` | 协调 | 03 新增 MeetingType 条件渲染;04 追加描述 + 附件区域 |
|
||||||
|
|
||||||
|
> 执行顺序:优先完成 03-01(数据模型),再开始 04 的后端模型部分。前端组件部分可独立并行。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、验收标准
|
||||||
|
|
||||||
|
| 验收项 | 验证方法 | 预期结果 |
|
||||||
|
|---|---|---|
|
||||||
|
| 描述编辑 | 编辑待办项,在描述 textarea 输入多行文字,保存 | 再次打开编辑框,描述内容完整保留 |
|
||||||
|
| 描述显示 | 查看待办项列表/详情 | 列表卡片显示描述预览(单行截断),详情显示完整描述 |
|
||||||
|
| 附件上传 | 编辑待办项,点击上传文件,选择一个本地文件 | 文件上传成功,附件列表显示文件名和大小 |
|
||||||
|
| 附件下载 | 在附件列表点击下载按钮 | 文件以原始文件名下载到本地 |
|
||||||
|
| 附件删除 | 点击附件删除按钮 | 附件从列表移除,磁盘文件同步清理 |
|
||||||
|
| 外部链接添加 | 点击添加链接,输入 URL | 链接保存为附件,类型标记为"外部链接" |
|
||||||
|
| 打开本地文件 | 点击本地附件"打开" | 系统关联程序打开该文件(如 PDF → PDF 阅读器) |
|
||||||
|
| 打开外部链接 | 点击外部链接附件"打开" | 系统默认浏览器打开该 URL |
|
||||||
|
| 打开可执行文件 | 附件为 `.exe`(Windows)或可执行脚本(Linux) | 系统正常运行该程序 |
|
||||||
|
| 附件数量限制 | 超过 20 个附件后继续上传 | 提示"每个待办项最多 20 个附件" |
|
||||||
|
| 文件大小限制 | 上传超过 50MB 的文件 | 提示"文件大小不能超过 50MB" |
|
||||||
|
| 待办项删除级联 | 删除有附件的待办项 | 附件记录和磁盘文件同步清理 |
|
||||||
|
| 空描述 | 创建待办项时不填描述 | 正常创建,列表中不显示描述行 |
|
||||||
|
| 跨平台安全 | 桌面端设置描述+附件 → 移动端只改标题并保存 → 桌面端查看 | 描述和附件完整保留,未被覆盖(详见 1.4 节) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十、风险与回滚
|
||||||
|
|
||||||
|
| 风险 | 影响 | 应对策略 |
|
||||||
|
|---|---|---|
|
||||||
|
| `Process.Start` 在不同 Linux 发行版行为差异 | 部分 Linux 文件打开失败 | 用 `xdg-open` 兜底;提供错误提示"无法打开文件:{原因}" |
|
||||||
|
| 附件文件被用户手动删除 | 数据库有记录但文件不存在 | 启动时校验,缺失文件标记为"失效",UI 用灰色 + 感叹号提示 |
|
||||||
|
| 大文件上传耗尽磁盘空间 | 应用数据目录磁盘满 | 单文件 50MB + 每待办项 20 个上限 = 最多 1GB;检查和提示剩余空间 |
|
||||||
|
| 并发编辑附件冲突 | 多窗口同时删除/添加附件 | 附件操作为即时保存(非批量提交),利用数据库事务隔离 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十一、Touch List
|
||||||
|
|
||||||
|
| 文件路径 | 修改类型 | 是否共享 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `src/Hua.Todo.Core/Entities/TaskEntity.cs` | 修改 | 是(与 03 共享) | 新增 Description 字段 + Attachments 导航属性 |
|
||||||
|
| `src/Hua.Todo.Core/Entities/AttachmentEntity.cs` | 新增 | 否 | 附件实体 |
|
||||||
|
| `src/Hua.Todo.Core/Entities/AttachmentType.cs` | 新增 | 否 | 附件类型枚举 |
|
||||||
|
| `src/Hua.Todo.Core/Repositories/IAttachmentRepository.cs` | 新增 | 否 | 附件仓储接口 |
|
||||||
|
| `src/Hua.Todo.Application/Data/TodoDbContext.cs` | 修改 | 是(与 03 共享) | 新增 Attachments DbSet + 关系映射 |
|
||||||
|
| `src/Hua.Todo.Application/Services/IAttachmentService.cs` | 新增 | 否 | 附件服务接口 |
|
||||||
|
| `src/Hua.Todo.Application/Services/AttachmentService.cs` | 新增 | 否 | 附件业务逻辑 |
|
||||||
|
| `src/Hua.Todo.Application/Controllers/AttachmentController.cs` | 新增 | 否 | 附件 API 端点 |
|
||||||
|
| `src/Hua.Todo.Application/Models/AttachmentModels.cs` | 新增 | 否 | 附件 DTO |
|
||||||
|
| `src/Hua.Todo.Core/Services/IPlatformAttachmentOpener.cs` | 新增 | 否 | 平台文件打开接口 |
|
||||||
|
| `src/Hua.Todo.Maui/Services/Platforms/MauiAttachmentOpener.cs` | 新增 | 否 | Windows MAUI 文件打开实现 |
|
||||||
|
| `src/Hua.Todo.Avalonia/Services/Platforms/AvaloniaAttachmentOpener.cs` | 新增 | 否 | Linux Avalonia 文件打开实现 |
|
||||||
|
| `src/Hua.Todo.Web/src/api/attachments.ts` | 新增 | 否 | 前端附件 API 模块 |
|
||||||
|
| `src/Hua.Todo.Web/src/types/task.ts` | 修改 | 是(与 03 共享) | 新增 description、AttachmentItem 等类型 |
|
||||||
|
| `src/Hua.Todo.Web/src/components/TaskEditDialog.vue` | 修改 | 是(与 03 共享) | 新增描述 textarea + 附件管理区域 |
|
||||||
|
| `src/Hua.Todo.Web/src/components/TaskItem.vue` | 修改 | 否 | 描述预览 + 附件角标 |
|
||||||
|
| `src/Hua.Todo.Web/src/components/AttachmentList.vue` | 新增 | 否 | 附件列表组件 |
|
||||||
|
| `src/Hua.Todo.Web/src/composables/useAttachments.ts` | 新增 | 否 | 附件管理组合式函数 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十二、与工单 03 的交互
|
||||||
|
|
||||||
|
| 维度 | 工单 03 | 工单 04 |
|
||||||
|
|---|---|---|
|
||||||
|
| 描述文本 | `MeetingNotes`(会议纪要,仅会议类型) | `Description`(通用多行描述,所有类型) |
|
||||||
|
| 附件 | 不涉及 | 通用附件管理 |
|
||||||
|
| 外部链接 | 不涉及 | 通用外部链接 |
|
||||||
|
|
||||||
|
> `MeetingNotes` 与 `Description` 是两个独立字段:
|
||||||
|
> - `MeetingNotes`:会议转写/文字纪要,可能很长(数千字),仅在会议类型下有值
|
||||||
|
> - `Description`:通用简短备注(上限 5000 字符),所有类型的待办项都可以有
|
||||||
|
>
|
||||||
|
> 两者可共存:会议类型的待办项可以同时有 `MeetingNotes`(AI 拆分的输入)和 `Description`(用户的简短备注)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**工单编号**:04
|
||||||
|
**标题**:富文本描述、附件与外部链接
|
||||||
|
**版本**:v1.3.0
|
||||||
|
**创建日期**:2026-06-16
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Kill processes occupying specified ports
|
||||||
|
param(
|
||||||
|
[int[]]$Ports = @(5173, 5174,5057)
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($port in $Ports) {
|
||||||
|
$connections = Get-NetTCPConnection -LocalPort $port -ErrorAction SilentlyContinue
|
||||||
|
if (-not $connections) {
|
||||||
|
Write-Host "Port $port : no process listening" -ForegroundColor Gray
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$procIds = $connections | Select-Object -ExpandProperty OwningProcess -Unique
|
||||||
|
foreach ($procId in $procIds) {
|
||||||
|
$proc = Get-Process -Id $procId -ErrorAction SilentlyContinue
|
||||||
|
if ($proc) {
|
||||||
|
Write-Host "Port $port : killing $($proc.ProcessName) (PID $procId)" -ForegroundColor Yellow
|
||||||
|
Stop-Process -Id $procId -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Done." -ForegroundColor Green
|
||||||
@@ -57,7 +57,14 @@ if (-not (Test-Path ".\.codegraph")) {
|
|||||||
} else {
|
} else {
|
||||||
Write-Host "[信息] 正在执行增量同步…" -ForegroundColor Cyan
|
Write-Host "[信息] 正在执行增量同步…" -ForegroundColor Cyan
|
||||||
codegraph sync
|
codegraph sync
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Host "[警告] 增量同步失败,可能未正确初始化,尝试重新初始化…" -ForegroundColor Yellow
|
||||||
|
codegraph init -i
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "codegraph init 失败" }
|
||||||
|
Write-Host "[完成] 重新初始化完毕,重新执行同步…" -ForegroundColor Green
|
||||||
|
codegraph sync
|
||||||
if ($LASTEXITCODE -ne 0) { throw "codegraph sync 失败" }
|
if ($LASTEXITCODE -ne 0) { throw "codegraph sync 失败" }
|
||||||
|
}
|
||||||
Write-Host "[完成] 增量同步完毕" -ForegroundColor Green
|
Write-Host "[完成] 增量同步完毕" -ForegroundColor Green
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,379 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Builder;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using System.Text.Json;
|
|
||||||
using Hua.Todo.Application.CloudSync.Auth;
|
|
||||||
using Hua.Todo.Application.CloudSync.Models;
|
|
||||||
using Hua.Todo.Application.CloudSync.Services;
|
|
||||||
|
|
||||||
namespace Hua.Todo.Application.CloudSync;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 云同步服务端 API 路由注册扩展。
|
|
||||||
/// </summary>
|
|
||||||
public static class CloudSyncEndpointExtensions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 映射云同步相关 API 端点。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="app">Web 应用。</param>
|
|
||||||
/// <returns>Web 应用。</returns>
|
|
||||||
public static WebApplication MapCloudSyncEndpoints(this WebApplication app)
|
|
||||||
{
|
|
||||||
var auth = app.MapGroup("/auth").WithTags("CloudSync - Auth");
|
|
||||||
auth.MapPost("/bootstrap", BootstrapAdminAsync).AllowAnonymous();
|
|
||||||
auth.MapPost("/login", LoginAsync).AllowAnonymous();
|
|
||||||
auth.MapPost("/logout", LogoutAsync).RequireAuthorization();
|
|
||||||
auth.MapPost("/change-password", ChangePasswordAsync).RequireAuthorization();
|
|
||||||
|
|
||||||
var tasks = app.MapGroup("/tasks").WithTags("CloudSync - Tasks");
|
|
||||||
tasks.MapGet("/", GetTasksAsync).RequireAuthorization("tasks:read");
|
|
||||||
|
|
||||||
var sync = app.MapGroup("/sync").WithTags("CloudSync - Sync");
|
|
||||||
sync.MapPost("/", SyncAsync).RequireAuthorization("sync:write");
|
|
||||||
|
|
||||||
var security = app.MapGroup("/security").WithTags("CloudSync - Security");
|
|
||||||
security.MapGet("/policy", GetPolicyAsync).RequireAuthorization("policy:read");
|
|
||||||
security.MapPut("/policy", UpdatePolicyAsync).RequireAuthorization("policy:write");
|
|
||||||
|
|
||||||
var admin = app.MapGroup("/admin").WithTags("CloudSync - Admin").RequireAuthorization("users:manage");
|
|
||||||
admin.MapGet("/users", GetUsersAsync);
|
|
||||||
admin.MapPost("/users", CreateUserAsync);
|
|
||||||
admin.MapPost("/users/{userId}/reset-password", ResetPasswordAsync);
|
|
||||||
admin.MapDelete("/users/{userId}", DeleteUserAsync);
|
|
||||||
admin.MapGet("/sessions", GetSessionsAsync);
|
|
||||||
admin.MapDelete("/sessions/{sessionId}", RevokeSessionAsync);
|
|
||||||
admin.MapGet("/audit-logs", GetAuditLogsAsync);
|
|
||||||
|
|
||||||
// 服务端探测端点(允许匿名访问,用于探测用户配置的外部服务端)
|
|
||||||
var probe = app.MapGroup("/cloud-sync").WithTags("CloudSync - Setup");
|
|
||||||
probe.MapPost("/probe", ProbeAsync).AllowAnonymous();
|
|
||||||
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static (string? ip, string? ua) GetClientInfo(HttpContext httpContext)
|
|
||||||
{
|
|
||||||
var ip = httpContext.Connection.RemoteIpAddress?.ToString();
|
|
||||||
var ua = httpContext.Request.Headers.UserAgent.ToString();
|
|
||||||
return (ip, ua);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<IResult> BootstrapAdminAsync(
|
|
||||||
BootstrapAdminRequest request,
|
|
||||||
CloudAuthService authService,
|
|
||||||
HttpContext httpContext,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (request == null || string.IsNullOrWhiteSpace(request.UserName))
|
|
||||||
{
|
|
||||||
return CloudApiErrors.BadRequest("UserName is required.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var (ip, ua) = GetClientInfo(httpContext);
|
|
||||||
var result = await authService.BootstrapAdminAsync(request.UserName, request.Password, ip, ua, cancellationToken);
|
|
||||||
if (result == null)
|
|
||||||
{
|
|
||||||
return CloudApiErrors.Forbidden("Bootstrap is not allowed (already initialized or invalid input).");
|
|
||||||
}
|
|
||||||
|
|
||||||
return Results.Json(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<IResult> LoginAsync(
|
|
||||||
LoginRequest request,
|
|
||||||
CloudAuthService authService,
|
|
||||||
HttpContext httpContext,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (request == null || string.IsNullOrWhiteSpace(request.UserName) || string.IsNullOrWhiteSpace(request.Password))
|
|
||||||
{
|
|
||||||
return CloudApiErrors.BadRequest("UserName and Password are required.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var (ip, ua) = GetClientInfo(httpContext);
|
|
||||||
var response = await authService.LoginAsync(request.UserName, request.Password, TimeSpan.FromDays(7), ip, ua, cancellationToken);
|
|
||||||
if (response == null)
|
|
||||||
{
|
|
||||||
return CloudApiErrors.Unauthorized("Invalid credentials.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return Results.Json(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<IResult> ChangePasswordAsync(
|
|
||||||
ChangePasswordRequest request,
|
|
||||||
CloudAuthService authService,
|
|
||||||
HttpContext httpContext,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var sessionId = httpContext.User.GetSessionId();
|
|
||||||
if (sessionId == null)
|
|
||||||
{
|
|
||||||
return CloudApiErrors.Unauthorized();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request == null || string.IsNullOrWhiteSpace(request.CurrentPassword) || string.IsNullOrWhiteSpace(request.NewPassword))
|
|
||||||
{
|
|
||||||
return CloudApiErrors.BadRequest("CurrentPassword and NewPassword are required.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.NewPassword.Length < 8)
|
|
||||||
{
|
|
||||||
return CloudApiErrors.BadRequest("NewPassword must be at least 8 characters.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var (ip, ua) = GetClientInfo(httpContext);
|
|
||||||
var ok = await authService.ChangePasswordAsync(sessionId.Value, request.CurrentPassword, request.NewPassword, ip, ua, cancellationToken);
|
|
||||||
if (!ok)
|
|
||||||
{
|
|
||||||
return CloudApiErrors.BadRequest("Invalid current password or session expired.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return Results.Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<IResult> LogoutAsync(
|
|
||||||
CloudAuthService authService,
|
|
||||||
HttpContext httpContext,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var sessionId = httpContext.User.GetSessionId();
|
|
||||||
if (sessionId == null)
|
|
||||||
{
|
|
||||||
return CloudApiErrors.Unauthorized();
|
|
||||||
}
|
|
||||||
|
|
||||||
await authService.LogoutAsync(sessionId.Value, cancellationToken);
|
|
||||||
return Results.Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<IResult> GetTasksAsync(
|
|
||||||
CloudTaskSyncService taskService,
|
|
||||||
HttpContext httpContext,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var userId = httpContext.User.GetUserId();
|
|
||||||
if (userId == null)
|
|
||||||
{
|
|
||||||
return CloudApiErrors.Unauthorized();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!httpContext.User.HasPermission(CloudPermissions.TasksRead))
|
|
||||||
{
|
|
||||||
return CloudApiErrors.Forbidden();
|
|
||||||
}
|
|
||||||
|
|
||||||
var tasks = await taskService.GetTasksAsync(userId.Value, cancellationToken);
|
|
||||||
return Results.Json(tasks);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<IResult> SyncAsync(
|
|
||||||
SyncRequest request,
|
|
||||||
CloudTaskSyncService taskService,
|
|
||||||
HttpContext httpContext,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var userId = httpContext.User.GetUserId();
|
|
||||||
if (userId == null)
|
|
||||||
{
|
|
||||||
return CloudApiErrors.Unauthorized();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!httpContext.User.HasPermission(CloudPermissions.SyncWrite))
|
|
||||||
{
|
|
||||||
return CloudApiErrors.Forbidden();
|
|
||||||
}
|
|
||||||
|
|
||||||
var response = await taskService.SyncAsync(userId.Value, request, cancellationToken);
|
|
||||||
return Results.Json(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<IResult> GetPolicyAsync(
|
|
||||||
SecurityPolicyService policyService,
|
|
||||||
HttpContext httpContext,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var userId = httpContext.User.GetUserId();
|
|
||||||
if (userId == null)
|
|
||||||
{
|
|
||||||
return CloudApiErrors.Unauthorized();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!httpContext.User.HasPermission(CloudPermissions.PolicyRead))
|
|
||||||
{
|
|
||||||
return CloudApiErrors.Forbidden();
|
|
||||||
}
|
|
||||||
|
|
||||||
var policy = await policyService.GetPolicyAsync(userId.Value, cancellationToken);
|
|
||||||
return Results.Json(policy);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<IResult> UpdatePolicyAsync(
|
|
||||||
UpdateSecurityPolicyRequest request,
|
|
||||||
SecurityPolicyService policyService,
|
|
||||||
HttpContext httpContext,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var userId = httpContext.User.GetUserId();
|
|
||||||
if (userId == null)
|
|
||||||
{
|
|
||||||
return CloudApiErrors.Unauthorized();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!httpContext.User.HasPermission(CloudPermissions.PolicyWrite))
|
|
||||||
{
|
|
||||||
return CloudApiErrors.Forbidden();
|
|
||||||
}
|
|
||||||
|
|
||||||
var policy = await policyService.UpdatePolicyAsync(userId.Value, request.AllowPersist, request.AllowSync, request.IsTrustedDeviceOnly, cancellationToken);
|
|
||||||
return Results.Json(policy);
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Admin Endpoints
|
|
||||||
|
|
||||||
private static async Task<IResult> GetUsersAsync(
|
|
||||||
CloudAdminService adminService,
|
|
||||||
HttpContext httpContext,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
// TODO: 启用权限校验
|
|
||||||
// if (!httpContext.User.HasPermission(CloudPermissions.UsersManage)) return CloudApiErrors.Forbidden();
|
|
||||||
|
|
||||||
var users = await adminService.GetUsersAsync(cancellationToken);
|
|
||||||
return Results.Json(users);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<IResult> CreateUserAsync(
|
|
||||||
CreateUserRequest request,
|
|
||||||
CloudAdminService adminService,
|
|
||||||
HttpContext httpContext,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
// if (!httpContext.User.HasPermission(CloudPermissions.UsersManage)) return CloudApiErrors.Forbidden();
|
|
||||||
|
|
||||||
if (request == null || string.IsNullOrWhiteSpace(request.UserName) || string.IsNullOrWhiteSpace(request.Password))
|
|
||||||
{
|
|
||||||
return CloudApiErrors.BadRequest("UserName and Password are required.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var user = await adminService.CreateUserAsync(request, cancellationToken);
|
|
||||||
if (user == null) return CloudApiErrors.BadRequest("User already exists or creation failed.");
|
|
||||||
|
|
||||||
return Results.Json(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<IResult> ResetPasswordAsync(
|
|
||||||
Guid userId,
|
|
||||||
ResetPasswordRequest request,
|
|
||||||
CloudAdminService adminService,
|
|
||||||
HttpContext httpContext,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
// if (!httpContext.User.HasPermission(CloudPermissions.UsersManage)) return CloudApiErrors.Forbidden();
|
|
||||||
|
|
||||||
if (request == null || string.IsNullOrWhiteSpace(request.NewPassword))
|
|
||||||
{
|
|
||||||
return CloudApiErrors.BadRequest("NewPassword is required.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var ok = await adminService.ResetPasswordAsync(userId, request.NewPassword, cancellationToken);
|
|
||||||
return ok ? Results.Ok() : CloudApiErrors.NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<IResult> DeleteUserAsync(
|
|
||||||
Guid userId,
|
|
||||||
CloudAdminService adminService,
|
|
||||||
HttpContext httpContext,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
// if (!httpContext.User.HasPermission(CloudPermissions.UsersManage)) return CloudApiErrors.Forbidden();
|
|
||||||
|
|
||||||
var ok = await adminService.DeleteUserAsync(userId, cancellationToken);
|
|
||||||
return ok ? Results.Ok() : CloudApiErrors.BadRequest("User not found or deletion not allowed.");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<IResult> GetSessionsAsync(
|
|
||||||
CloudAdminService adminService,
|
|
||||||
HttpContext httpContext,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
// if (!httpContext.User.HasPermission(CloudPermissions.UsersManage)) return CloudApiErrors.Forbidden();
|
|
||||||
|
|
||||||
var sessions = await adminService.GetSessionsAsync(cancellationToken);
|
|
||||||
return Results.Json(sessions);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<IResult> RevokeSessionAsync(
|
|
||||||
Guid sessionId,
|
|
||||||
CloudAdminService adminService,
|
|
||||||
HttpContext httpContext,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
// if (!httpContext.User.HasPermission(CloudPermissions.UsersManage)) return CloudApiErrors.Forbidden();
|
|
||||||
|
|
||||||
var ok = await adminService.RevokeSessionAsync(sessionId, cancellationToken);
|
|
||||||
return ok ? Results.Ok() : CloudApiErrors.NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<IResult> GetAuditLogsAsync(
|
|
||||||
int count,
|
|
||||||
CloudAdminService adminService,
|
|
||||||
HttpContext httpContext,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
// if (!httpContext.User.HasPermission(CloudPermissions.UsersManage)) return CloudApiErrors.Forbidden();
|
|
||||||
|
|
||||||
if (count <= 0) count = 100;
|
|
||||||
var logs = await adminService.GetAuditLogsAsync(count, cancellationToken);
|
|
||||||
return Results.Json(logs);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<IResult> ProbeAsync(
|
|
||||||
ProbeRequest request,
|
|
||||||
CloudProbeService probeService,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (request == null || string.IsNullOrWhiteSpace(request.TargetUrl))
|
|
||||||
{
|
|
||||||
return CloudApiErrors.BadRequest("TargetUrl is required.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = await probeService.ProbeAsync(request.TargetUrl, cancellationToken);
|
|
||||||
return Results.Json(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 注册云同步代理设置 API(仅嵌入式 WebServer 使用)。
|
|
||||||
/// GET /api/cloud-sync/settings → 返回当前 URL
|
|
||||||
/// POST /api/cloud-sync/settings → 设置 URL
|
|
||||||
/// </summary>
|
|
||||||
public static WebApplication MapCloudSyncProxySettings(this WebApplication app)
|
|
||||||
{
|
|
||||||
var group = app.MapGroup("/api/cloud-sync").WithTags("CloudSync - Proxy");
|
|
||||||
|
|
||||||
group.MapGet("/settings", (CloudSyncProxySettings settings) =>
|
|
||||||
{
|
|
||||||
return Results.Json(new { serverUrl = settings.GetUrl() ?? string.Empty });
|
|
||||||
});
|
|
||||||
|
|
||||||
group.MapPost("/settings", async (HttpContext context, CloudSyncProxySettings settings) =>
|
|
||||||
{
|
|
||||||
using var doc = await JsonDocument.ParseAsync(context.Request.Body);
|
|
||||||
var serverUrl = doc.RootElement.TryGetProperty("serverUrl", out var el) ? el.GetString() : null;
|
|
||||||
settings.SetUrl(serverUrl);
|
|
||||||
|
|
||||||
// 持久化到 appsettings.json
|
|
||||||
if (context.RequestServices.GetService<CloudSyncProxySettingsPersistence>() is { } persistence)
|
|
||||||
{
|
|
||||||
try { persistence.Save(settings.GetUrl() ?? string.Empty); } catch { /* 保存失败不影响前端 */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
return Results.Json(new { serverUrl = settings.GetUrl() ?? string.Empty });
|
|
||||||
});
|
|
||||||
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
using System.Net.Http.Headers;
|
|
||||||
using Microsoft.AspNetCore.Builder;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
|
|
||||||
namespace Hua.Todo.Application.CloudSync;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 云同步反向代理中间件。
|
|
||||||
/// 将 /auth/、/tasks/、/sync/、/security/、/cloud-sync/ 请求转发到远程 Host。
|
|
||||||
/// 仅在嵌入式 MAUI/Avalonia WebServer 中注册。
|
|
||||||
/// </summary>
|
|
||||||
public class CloudSyncProxyMiddleware
|
|
||||||
{
|
|
||||||
private readonly RequestDelegate _next;
|
|
||||||
private readonly IHttpClientFactory _httpClientFactory;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 需要代理的路径前缀集合。
|
|
||||||
/// </summary>
|
|
||||||
private static readonly string[] ProxyPrefixes = { "/auth/", "/tasks/", "/sync/", "/security/", "/cloud-sync/" };
|
|
||||||
|
|
||||||
public CloudSyncProxyMiddleware(RequestDelegate next, IHttpClientFactory httpClientFactory)
|
|
||||||
{
|
|
||||||
_next = next;
|
|
||||||
_httpClientFactory = httpClientFactory;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 处理请求。如果匹配代理前缀则转发,否则放行到下一中间件。
|
|
||||||
/// </summary>
|
|
||||||
public async Task InvokeAsync(HttpContext context)
|
|
||||||
{
|
|
||||||
var requestPath = context.Request.Path.Value ?? string.Empty;
|
|
||||||
|
|
||||||
// 仅拦截云同步相关路径
|
|
||||||
if (!ProxyPrefixes.Any(p => requestPath.StartsWith(p, StringComparison.OrdinalIgnoreCase)))
|
|
||||||
{
|
|
||||||
await _next(context);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var settings = context.RequestServices.GetRequiredService<CloudSyncProxySettings>();
|
|
||||||
var targetUrl = settings.GetUrl();
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(targetUrl))
|
|
||||||
{
|
|
||||||
context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
|
|
||||||
context.Response.ContentType = "application/json";
|
|
||||||
await context.Response.WriteAsync("{\"error\":\"cloud sync server URL not configured\"}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await ProxyRequestAsync(context, targetUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 将当前请求转发到目标地址,并回写响应。
|
|
||||||
/// </summary>
|
|
||||||
private async Task ProxyRequestAsync(HttpContext context, string targetUrl)
|
|
||||||
{
|
|
||||||
var client = _httpClientFactory.CreateClient("CloudSyncProxy");
|
|
||||||
client.Timeout = TimeSpan.FromSeconds(30);
|
|
||||||
|
|
||||||
var request = context.Request;
|
|
||||||
var targetUri = $"{targetUrl}{request.Path}{request.QueryString}";
|
|
||||||
|
|
||||||
var proxyRequest = new HttpRequestMessage(new HttpMethod(request.Method), targetUri);
|
|
||||||
|
|
||||||
// 复制请求体
|
|
||||||
if (request.ContentLength > 0 || request.Headers.ContainsKey("Transfer-Encoding"))
|
|
||||||
{
|
|
||||||
request.EnableBuffering();
|
|
||||||
request.Body.Position = 0;
|
|
||||||
using var reader = new StreamReader(request.Body, leaveOpen: true);
|
|
||||||
var body = await reader.ReadToEndAsync();
|
|
||||||
request.Body.Position = 0;
|
|
||||||
proxyRequest.Content = new StringContent(body, System.Text.Encoding.UTF8, request.ContentType ?? "application/json");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 复制 Authorization 头
|
|
||||||
if (request.Headers.TryGetValue("Authorization", out var authHeader))
|
|
||||||
{
|
|
||||||
proxyRequest.Headers.TryAddWithoutValidation("Authorization", authHeader.ToArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 复制 Content-Type(StringContent 已设置,但确保不被覆盖)
|
|
||||||
if (request.Headers.TryGetValue("Content-Type", out var ctHeader) && proxyRequest.Content != null)
|
|
||||||
{
|
|
||||||
proxyRequest.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(ctHeader.First()!);
|
|
||||||
}
|
|
||||||
|
|
||||||
using var response = await client.SendAsync(proxyRequest, HttpCompletionOption.ResponseHeadersRead, context.RequestAborted);
|
|
||||||
|
|
||||||
context.Response.StatusCode = (int)response.StatusCode;
|
|
||||||
context.Response.ContentType = response.Content.Headers.ContentType?.ToString() ?? "application/json";
|
|
||||||
|
|
||||||
// 回写响应体
|
|
||||||
var responseBody = await response.Content.ReadAsStringAsync(context.RequestAborted);
|
|
||||||
await context.Response.WriteAsync(responseBody, context.RequestAborted);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// CloudSyncProxyMiddleware 的 IApplicationBuilder 扩展。
|
|
||||||
/// </summary>
|
|
||||||
public static class CloudSyncProxyMiddlewareExtensions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 注册云同步反向代理中间件。
|
|
||||||
/// </summary>
|
|
||||||
public static IApplicationBuilder UseCloudSyncProxy(this IApplicationBuilder builder)
|
|
||||||
{
|
|
||||||
return builder.UseMiddleware<CloudSyncProxyMiddleware>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
namespace Hua.Todo.Application.CloudSync;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 云同步代理 URL 管理服务。
|
|
||||||
/// 嵌入式 WebServer 启动时从 appsettings 读取初始值,
|
|
||||||
/// 前端通过 /api/cloud-sync/settings 读写。
|
|
||||||
/// </summary>
|
|
||||||
public class CloudSyncProxySettings
|
|
||||||
{
|
|
||||||
private string? _url;
|
|
||||||
private readonly object _lock = new();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取当前配置的云同步服务端地址,未配置时返回 null。
|
|
||||||
/// </summary>
|
|
||||||
public string? GetUrl()
|
|
||||||
{
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
return _url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 设置云同步服务端地址。
|
|
||||||
/// </summary>
|
|
||||||
public void SetUrl(string? url)
|
|
||||||
{
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
_url = string.IsNullOrWhiteSpace(url) ? null : url.TrimEnd('/');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
namespace Hua.Todo.Application.CloudSync;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 云同步代理 URL 的 appsettings.json 持久化帮助类。
|
|
||||||
/// 由 MAUI/Avalonia 宿主创建并注册,注入 appsettings 文件路径。
|
|
||||||
/// </summary>
|
|
||||||
public class CloudSyncProxySettingsPersistence
|
|
||||||
{
|
|
||||||
private readonly string _settingsFilePath;
|
|
||||||
|
|
||||||
public CloudSyncProxySettingsPersistence(string settingsFilePath)
|
|
||||||
{
|
|
||||||
_settingsFilePath = settingsFilePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 将 CloudSyncUrl 写回 appsettings.json。
|
|
||||||
/// </summary>
|
|
||||||
public void Save(string url)
|
|
||||||
{
|
|
||||||
var json = File.ReadAllText(_settingsFilePath);
|
|
||||||
using var doc = JsonDocument.Parse(json);
|
|
||||||
using var stream = new FileStream(_settingsFilePath, FileMode.Create, FileAccess.Write);
|
|
||||||
using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true });
|
|
||||||
|
|
||||||
writer.WriteStartObject();
|
|
||||||
foreach (var prop in doc.RootElement.EnumerateObject())
|
|
||||||
{
|
|
||||||
if (prop.NameEquals("WebServer"))
|
|
||||||
{
|
|
||||||
writer.WritePropertyName("WebServer");
|
|
||||||
writer.WriteStartObject();
|
|
||||||
foreach (var wsProp in prop.Value.EnumerateObject())
|
|
||||||
{
|
|
||||||
writer.WritePropertyName(wsProp.Name);
|
|
||||||
if (wsProp.NameEquals("CloudSyncUrl"))
|
|
||||||
{
|
|
||||||
writer.WriteStringValue(url ?? string.Empty);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
wsProp.Value.WriteTo(writer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
writer.WriteEndObject();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
writer.WritePropertyName(prop.Name);
|
|
||||||
prop.Value.WriteTo(writer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
writer.WriteEndObject();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Authentication;
|
|
||||||
using Microsoft.AspNetCore.Identity;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Hua.Todo.Application.CloudSync.Auth;
|
|
||||||
using Hua.Todo.Application.CloudSync.Services;
|
|
||||||
using Hua.Todo.Core.Entities;
|
|
||||||
|
|
||||||
namespace Hua.Todo.Application.CloudSync;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 云同步服务端能力的依赖注入扩展。
|
|
||||||
/// </summary>
|
|
||||||
public static class CloudSyncServiceCollectionExtensions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 注册云同步相关的认证、授权与业务服务。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="services">服务集合。</param>
|
|
||||||
/// <returns>服务集合。</returns>
|
|
||||||
public static IServiceCollection AddCloudSyncServer(this IServiceCollection services)
|
|
||||||
{
|
|
||||||
services.AddHttpContextAccessor();
|
|
||||||
|
|
||||||
services.AddSingleton<IRolePermissionMapper, DefaultRolePermissionMapper>();
|
|
||||||
services.AddScoped<IPasswordHasher<UserEntity>, PasswordHasher<UserEntity>>();
|
|
||||||
|
|
||||||
services.AddScoped<CloudAuthService>();
|
|
||||||
services.AddScoped<CloudAdminService>();
|
|
||||||
services.AddScoped<CloudTaskSyncService>();
|
|
||||||
services.AddScoped<SecurityPolicyService>();
|
|
||||||
|
|
||||||
// 注册探测服务专用的 HttpClient(仅限服务端,MAUI 端不注册 AddCloudSyncServer)
|
|
||||||
// 关闭 SSL 证书校验:探测目标可能是自签证书或内网服务器
|
|
||||||
services.AddHttpClient("ProbeClient", client =>
|
|
||||||
{
|
|
||||||
client.DefaultRequestHeaders.UserAgent.ParseAdd("HuaTodo-Probe/1.0");
|
|
||||||
})
|
|
||||||
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
|
|
||||||
{
|
|
||||||
ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
|
|
||||||
});
|
|
||||||
services.AddScoped<CloudProbeService>();
|
|
||||||
|
|
||||||
services.AddAuthentication(SessionAuthenticationDefaults.Scheme)
|
|
||||||
.AddScheme<AuthenticationSchemeOptions, SessionAuthenticationHandler>(SessionAuthenticationDefaults.Scheme, _ => { });
|
|
||||||
|
|
||||||
services.AddAuthorization(options =>
|
|
||||||
{
|
|
||||||
options.AddPolicy("tasks:read", p => p.RequireClaim(CloudClaims.Permission, CloudPermissions.TasksRead));
|
|
||||||
options.AddPolicy("tasks:write", p => p.RequireClaim(CloudClaims.Permission, CloudPermissions.TasksWrite));
|
|
||||||
options.AddPolicy("sync:write", p => p.RequireClaim(CloudClaims.Permission, CloudPermissions.SyncWrite));
|
|
||||||
options.AddPolicy("policy:read", p => p.RequireClaim(CloudClaims.Permission, CloudPermissions.PolicyRead));
|
|
||||||
options.AddPolicy("policy:write", p => p.RequireClaim(CloudClaims.Permission, CloudPermissions.PolicyWrite));
|
|
||||||
options.AddPolicy("users:manage", p => p.RequireClaim(CloudClaims.Permission, CloudPermissions.UsersManage));
|
|
||||||
});
|
|
||||||
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 注册云同步反向代理所需的服务(轻量级,供嵌入式 MAUI/Avalonia WebServer 使用)。
|
|
||||||
/// 仅注册 URL 管理服务与转发专用 HttpClient,不包含认证/授权/业务服务。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="services">服务集合。</param>
|
|
||||||
/// <returns>服务集合。</returns>
|
|
||||||
public static IServiceCollection AddCloudSyncProxy(this IServiceCollection services)
|
|
||||||
{
|
|
||||||
services.AddSingleton<CloudSyncProxySettings>();
|
|
||||||
|
|
||||||
// 代理专用的 HttpClient:不跟随重定向,允许转发到自签/内网服务器
|
|
||||||
services.AddHttpClient("CloudSyncProxy", client =>
|
|
||||||
{
|
|
||||||
client.DefaultRequestHeaders.UserAgent.ParseAdd("HuaTodo-Proxy/1.0");
|
|
||||||
})
|
|
||||||
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
|
|
||||||
{
|
|
||||||
AllowAutoRedirect = false,
|
|
||||||
ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
|
|
||||||
});
|
|
||||||
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
|
|
||||||
namespace Hua.Todo.Application.CloudSync.Models;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 云同步 API 错误响应生成器。
|
|
||||||
/// </summary>
|
|
||||||
public static class CloudApiErrors
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 生成标准错误响应。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="statusCode">HTTP 状态码。</param>
|
|
||||||
/// <param name="code">业务错误码。</param>
|
|
||||||
/// <param name="message">错误消息。</param>
|
|
||||||
/// <returns>最小 API 结果。</returns>
|
|
||||||
public static IResult Error(int statusCode, string code, string message)
|
|
||||||
{
|
|
||||||
return Results.Json(
|
|
||||||
new ApiErrorResponse { Code = code, Message = message },
|
|
||||||
statusCode: statusCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 未认证。
|
|
||||||
/// </summary>
|
|
||||||
public static IResult Unauthorized(string message = "Unauthorized.")
|
|
||||||
=> Error(StatusCodes.Status401Unauthorized, "UNAUTHORIZED", message);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 权限不足。
|
|
||||||
/// </summary>
|
|
||||||
public static IResult Forbidden(string message = "Forbidden.")
|
|
||||||
=> Error(StatusCodes.Status403Forbidden, "FORBIDDEN", message);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 请求非法。
|
|
||||||
/// </summary>
|
|
||||||
public static IResult BadRequest(string message = "Bad request.")
|
|
||||||
=> Error(StatusCodes.Status400BadRequest, "BAD_REQUEST", message);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 资源不存在。
|
|
||||||
/// </summary>
|
|
||||||
public static IResult NotFound(string message = "Not found.")
|
|
||||||
=> Error(StatusCodes.Status404NotFound, "NOT_FOUND", message);
|
|
||||||
}
|
|
||||||
|
|
||||||
+2
-2
@@ -1,11 +1,11 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
namespace Hua.Todo.Application.Data.Converters;
|
namespace Hua.Todo.Application.Common.Converters;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 将 <see cref="DateTime"/> 以 UTC 的 ISO 8601(Round-trip)字符串形式持久化到 SQLite,
|
/// 将 <see cref="DateTime"/> 以 UTC 的 ISO 8601(Round-trip)字符串形式持久化到 SQLite,
|
||||||
/// 并在读取时兼容历史遗留的 “ticks/Unix 时间戳” 数字字符串,避免因脏数据导致查询失败。
|
/// 并在读取时兼容历史遗留的 "ticks/Unix 时间戳" 数字字符串,避免因脏数据导致查询失败。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class LenientUtcDateTimeStringConverter : ValueConverter<DateTime, string>
|
public sealed class LenientUtcDateTimeStringConverter : ValueConverter<DateTime, string>
|
||||||
{
|
{
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using Serilog;
|
||||||
|
using Serilog.Events;
|
||||||
|
|
||||||
|
namespace Hua.Todo.Application.Common;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Serilog 日志配置工厂。
|
||||||
|
/// 提供统一的日志输出模板和默认配置,由各宿主项目调用以初始化文件日志。
|
||||||
|
/// </summary>
|
||||||
|
public static class LoggingConfiguration
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 统一日志输出模板。
|
||||||
|
/// </summary>
|
||||||
|
private const string OutputTemplate =
|
||||||
|
"[{Timestamp:yyyy-MM-dd HH:mm:ss.fff} {Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建基础 LoggerConfiguration。
|
||||||
|
/// 包含控制台输出(Debug 及以上)和按天滚动的文件输出(Information 及以上)。
|
||||||
|
/// 日志文件路径为 <paramref name="logDirectory"/> 下的 hua-todo-.log。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logDirectory">日志文件目录路径。</param>
|
||||||
|
public static LoggerConfiguration Create(string logDirectory)
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(logDirectory))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(logDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
var logFilePath = Path.Combine(logDirectory, "hua-todo-.log");
|
||||||
|
|
||||||
|
return new LoggerConfiguration()
|
||||||
|
.MinimumLevel.Debug()
|
||||||
|
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
|
||||||
|
.MinimumLevel.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Information)
|
||||||
|
.MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
|
||||||
|
.MinimumLevel.Override("System.Net.Http", LogEventLevel.Warning)
|
||||||
|
.WriteTo.Console(
|
||||||
|
restrictedToMinimumLevel: LogEventLevel.Debug,
|
||||||
|
outputTemplate: OutputTemplate)
|
||||||
|
.WriteTo.File(
|
||||||
|
path: logFilePath,
|
||||||
|
rollingInterval: RollingInterval.Day,
|
||||||
|
retainedFileCountLimit: 30,
|
||||||
|
restrictedToMinimumLevel: LogEventLevel.Information,
|
||||||
|
outputTemplate: OutputTemplate,
|
||||||
|
encoding: System.Text.Encoding.UTF8);
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
-4
@@ -1,13 +1,13 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Hua.Todo.Application.Data;
|
|
||||||
using Hua.Todo.Application.Interfaces;
|
|
||||||
using Hua.Todo.Application.Repositories;
|
using Hua.Todo.Application.Repositories;
|
||||||
using Hua.Todo.Application.Services;
|
using Hua.Todo.Application.Services;
|
||||||
|
using Hua.Todo.Application.Services.Meeting;
|
||||||
|
using Hua.Todo.Application.Services.Voice;
|
||||||
using Hua.Todo.Core.Interfaces;
|
using Hua.Todo.Core.Interfaces;
|
||||||
using ITaskService = Hua.Todo.Application.Interfaces.ITaskService;
|
using ITaskService = Hua.Todo.Application.Services.Interfaces.ITaskService;
|
||||||
|
|
||||||
namespace Hua.Todo.Application;
|
namespace Hua.Todo.Application.Common;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 应用层依赖注入扩展。
|
/// 应用层依赖注入扩展。
|
||||||
@@ -25,7 +25,13 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddDbContext<TodoDbContext>(options =>
|
services.AddDbContext<TodoDbContext>(options =>
|
||||||
options.UseSqlite(connectionString, b => b.MigrationsAssembly("Hua.Todo.Application")));
|
options.UseSqlite(connectionString, b => b.MigrationsAssembly("Hua.Todo.Application")));
|
||||||
services.AddScoped<ITaskRepository, TaskRepository>();
|
services.AddScoped<ITaskRepository, TaskRepository>();
|
||||||
|
services.AddScoped<IAttachmentRepository, AttachmentRepository>();
|
||||||
services.AddScoped<ITaskService, TaskService>();
|
services.AddScoped<ITaskService, TaskService>();
|
||||||
|
services.AddScoped<IAttachmentService, AttachmentService>();
|
||||||
|
services.AddScoped<IMeetingService, MeetingService>();
|
||||||
|
services.AddScoped<MeetingAiBreakdownService>();
|
||||||
|
services.AddScoped<ISttService, SttService>();
|
||||||
|
services.AddVoiceServices();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Hua.Todo.Core.Entities;
|
|
||||||
|
|
||||||
namespace Hua.Todo.Application.Data;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 应用程序数据库上下文(EF Core)。
|
|
||||||
/// </summary>
|
|
||||||
public class TodoDbContext : DbContext
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 创建 <see cref="TodoDbContext"/>。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="options">数据库上下文配置。</param>
|
|
||||||
public TodoDbContext(DbContextOptions<TodoDbContext> options) : base(options)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 任务集合。
|
|
||||||
/// </summary>
|
|
||||||
public DbSet<TaskEntity> Tasks { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 配置实体模型映射。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="modelBuilder">模型构建器。</param>
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
base.OnModelCreating(modelBuilder);
|
|
||||||
|
|
||||||
modelBuilder.Entity<TaskEntity>(entity =>
|
|
||||||
{
|
|
||||||
entity.ToTable("Tasks");
|
|
||||||
entity.HasKey(e => e.Id);
|
|
||||||
entity.Property(e => e.Title).IsRequired().HasMaxLength(200);
|
|
||||||
entity.Property(e => e.Priority).HasDefaultValue(TaskPriority.Medium);
|
|
||||||
entity.Property(e => e.IsCompleted).HasDefaultValue(false);
|
|
||||||
entity.Property(e => e.CreatedAt).HasDefaultValueSql("datetime('now')");
|
|
||||||
entity.Property(e => e.UpdatedAt).HasDefaultValueSql("datetime('now')");
|
|
||||||
|
|
||||||
entity.HasOne(e => e.ParentTask)
|
|
||||||
.WithMany(e => e.SubTasks)
|
|
||||||
.HasForeignKey(e => e.ParentTaskId)
|
|
||||||
.OnDelete(DeleteBehavior.Restrict);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,22 +5,34 @@
|
|||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<OutputType>Library</OutputType>
|
<OutputType>Library</OutputType>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<!-- ASP.NET Core 仅 net10.0(移动端通过 csproj 排除依赖文件) -->
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'net10.0'">
|
<ItemGroup Condition="'$(TargetFramework)' == 'net10.0'">
|
||||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- 移动端排除依赖 ASP.NET Core 的 CloudSync I/O 文件 -->
|
||||||
<ItemGroup Condition="'$(TargetFramework)' != 'net10.0'">
|
<ItemGroup Condition="'$(TargetFramework)' != 'net10.0'">
|
||||||
<Compile Remove="DynamicApi\\**\\*.cs" />
|
<Compile Remove="Services\CloudSync\Services\CloudSyncProxyService.cs" />
|
||||||
|
<Compile Remove="Services\CloudSync\Services\CloudSyncServerService.cs" />
|
||||||
|
<Compile Remove="Services\CloudSync\Services\CloudSyncProxySettingsService.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.5" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.5" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.5" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="10.0.5" />
|
||||||
|
<PackageReference Include="Serilog" Version="4.3.2-dev-02433" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Hua.Todo.Core\Hua.Todo.Core.csproj" />
|
<ProjectReference Include="..\Hua.Todo.Core\Hua.Todo.Core.csproj" />
|
||||||
|
<ProjectReference Include="..\Hua.Todo.HttpApi\Hua.Todo.HttpApi.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
-61
@@ -1,61 +0,0 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using Hua.Todo.Application.Data;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Hua.Todo.Application.Migrations
|
|
||||||
{
|
|
||||||
[DbContext(typeof(TodoDbContext))]
|
|
||||||
[Migration("20260313044926_InitialCreate")]
|
|
||||||
partial class InitialCreate
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.5");
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.TaskEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT")
|
|
||||||
.HasDefaultValueSql("datetime('now')");
|
|
||||||
|
|
||||||
b.Property<bool>("IsCompleted")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(false);
|
|
||||||
|
|
||||||
b.Property<int>("Priority")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(1);
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(200)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<DateTime>("UpdatedAt")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT")
|
|
||||||
.HasDefaultValueSql("datetime('now')");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("Tasks");
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Hua.Todo.Application.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class InitialCreate : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Tasks",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
|
||||||
.Annotation("Sqlite:Autoincrement", true),
|
|
||||||
Title = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
|
||||||
Priority = table.Column<int>(type: "INTEGER", nullable: false, defaultValue: 1),
|
|
||||||
IsCompleted = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: false),
|
|
||||||
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false, defaultValueSql: "datetime('now')"),
|
|
||||||
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: false, defaultValueSql: "datetime('now')")
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Tasks", x => x.Id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Tasks");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-81
@@ -1,81 +0,0 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using Hua.Todo.Application.Data;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Hua.Todo.Application.Migrations
|
|
||||||
{
|
|
||||||
[DbContext(typeof(TodoDbContext))]
|
|
||||||
[Migration("20260313092658_AddParentTaskId")]
|
|
||||||
partial class AddParentTaskId
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.5");
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.TaskEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT")
|
|
||||||
.HasDefaultValueSql("datetime('now')");
|
|
||||||
|
|
||||||
b.Property<bool>("IsCompleted")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(false);
|
|
||||||
|
|
||||||
b.Property<int?>("ParentTaskId")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<int>("Priority")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(1);
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(200)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<DateTime>("UpdatedAt")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT")
|
|
||||||
.HasDefaultValueSql("datetime('now')");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("ParentTaskId");
|
|
||||||
|
|
||||||
b.ToTable("Tasks");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.TaskEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Hua.Todo.Core.Entities.TaskEntity", "ParentTask")
|
|
||||||
.WithMany("SubTasks")
|
|
||||||
.HasForeignKey("ParentTaskId")
|
|
||||||
.OnDelete(DeleteBehavior.Restrict);
|
|
||||||
|
|
||||||
b.Navigation("ParentTask");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.TaskEntity", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("SubTasks");
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Hua.Todo.Application.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class AddParentTaskId : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.AddColumn<int>(
|
|
||||||
name: "ParentTaskId",
|
|
||||||
table: "Tasks",
|
|
||||||
type: "INTEGER",
|
|
||||||
nullable: true);
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Tasks_ParentTaskId",
|
|
||||||
table: "Tasks",
|
|
||||||
column: "ParentTaskId");
|
|
||||||
|
|
||||||
migrationBuilder.AddForeignKey(
|
|
||||||
name: "FK_Tasks_Tasks_ParentTaskId",
|
|
||||||
table: "Tasks",
|
|
||||||
column: "ParentTaskId",
|
|
||||||
principalTable: "Tasks",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Restrict);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropForeignKey(
|
|
||||||
name: "FK_Tasks_Tasks_ParentTaskId",
|
|
||||||
table: "Tasks");
|
|
||||||
|
|
||||||
migrationBuilder.DropIndex(
|
|
||||||
name: "IX_Tasks_ParentTaskId",
|
|
||||||
table: "Tasks");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "ParentTaskId",
|
|
||||||
table: "Tasks");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-198
@@ -1,198 +0,0 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using Hua.Todo.Application.Data;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Hua.Todo.Application.Migrations
|
|
||||||
{
|
|
||||||
[DbContext(typeof(TodoDbContext))]
|
|
||||||
[Migration("20260406172936_AddCloudSyncCoreEntities")]
|
|
||||||
partial class AddCloudSyncCoreEntities
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.5");
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.SecurityPolicyEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<bool>("AllowPersist")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(true);
|
|
||||||
|
|
||||||
b.Property<Guid>("UserId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("UserId")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("SecurityPolicies", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.TaskEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT")
|
|
||||||
.HasDefaultValueSql("datetime('now')");
|
|
||||||
|
|
||||||
b.Property<bool>("IsCompleted")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(false);
|
|
||||||
|
|
||||||
b.Property<int?>("ParentTaskId")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<int>("Priority")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(1);
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(200)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<DateTime>("UpdatedAt")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT")
|
|
||||||
.HasDefaultValueSql("datetime('now')");
|
|
||||||
|
|
||||||
b.Property<Guid>("UserId")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT")
|
|
||||||
.HasDefaultValue(new Guid("00000000-0000-0000-0000-000000000001"));
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("ParentTaskId");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.ToTable("Tasks", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.UserEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("PasswordHash")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Role")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(32)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("UserName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("UserName")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("Users", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.UserSessionEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAtUtc")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<DateTime>("ExpiresAtUtc")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("StepUpExpiresAtUtc")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<Guid>("UserId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.ToTable("UserSessions", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.SecurityPolicyEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Hua.Todo.Core.Entities.UserEntity", "User")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("User");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.TaskEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Hua.Todo.Core.Entities.TaskEntity", "ParentTask")
|
|
||||||
.WithMany("SubTasks")
|
|
||||||
.HasForeignKey("ParentTaskId")
|
|
||||||
.OnDelete(DeleteBehavior.Restrict);
|
|
||||||
|
|
||||||
b.HasOne("Hua.Todo.Core.Entities.UserEntity", "User")
|
|
||||||
.WithMany("Tasks")
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("ParentTask");
|
|
||||||
|
|
||||||
b.Navigation("User");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.UserSessionEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Hua.Todo.Core.Entities.UserEntity", "User")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("User");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.TaskEntity", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("SubTasks");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.UserEntity", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("Tasks");
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Hua.Todo.Application.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class AddCloudSyncCoreEntities : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.AddColumn<Guid>(
|
|
||||||
name: "UserId",
|
|
||||||
table: "Tasks",
|
|
||||||
type: "TEXT",
|
|
||||||
nullable: false,
|
|
||||||
defaultValue: new Guid("00000000-0000-0000-0000-000000000001"));
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Users",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
|
||||||
UserName = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
|
||||||
PasswordHash = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
Role = table.Column<string>(type: "TEXT", maxLength: 32, nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Users", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.InsertData(
|
|
||||||
table: "Users",
|
|
||||||
columns: new[] { "Id", "UserName", "PasswordHash", "Role" },
|
|
||||||
values: new object[] { new Guid("00000000-0000-0000-0000-000000000001"), "local", "", "local" });
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "SecurityPolicies",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
|
||||||
UserId = table.Column<Guid>(type: "TEXT", nullable: false),
|
|
||||||
AllowPersist = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_SecurityPolicies", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_SecurityPolicies_Users_UserId",
|
|
||||||
column: x => x.UserId,
|
|
||||||
principalTable: "Users",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "UserSessions",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
|
||||||
UserId = table.Column<Guid>(type: "TEXT", nullable: false),
|
|
||||||
CreatedAtUtc = table.Column<DateTime>(type: "TEXT", nullable: false),
|
|
||||||
ExpiresAtUtc = table.Column<DateTime>(type: "TEXT", nullable: false),
|
|
||||||
StepUpExpiresAtUtc = table.Column<DateTime>(type: "TEXT", nullable: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_UserSessions", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_UserSessions_Users_UserId",
|
|
||||||
column: x => x.UserId,
|
|
||||||
principalTable: "Users",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Tasks_UserId",
|
|
||||||
table: "Tasks",
|
|
||||||
column: "UserId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_SecurityPolicies_UserId",
|
|
||||||
table: "SecurityPolicies",
|
|
||||||
column: "UserId",
|
|
||||||
unique: true);
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Users_UserName",
|
|
||||||
table: "Users",
|
|
||||||
column: "UserName",
|
|
||||||
unique: true);
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_UserSessions_UserId",
|
|
||||||
table: "UserSessions",
|
|
||||||
column: "UserId");
|
|
||||||
|
|
||||||
migrationBuilder.AddForeignKey(
|
|
||||||
name: "FK_Tasks_Users_UserId",
|
|
||||||
table: "Tasks",
|
|
||||||
column: "UserId",
|
|
||||||
principalTable: "Users",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropForeignKey(
|
|
||||||
name: "FK_Tasks_Users_UserId",
|
|
||||||
table: "Tasks");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "SecurityPolicies");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "UserSessions");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Users");
|
|
||||||
|
|
||||||
migrationBuilder.DropIndex(
|
|
||||||
name: "IX_Tasks_UserId",
|
|
||||||
table: "Tasks");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "UserId",
|
|
||||||
table: "Tasks");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Generated
-203
@@ -1,203 +0,0 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using Hua.Todo.Application.Data;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Hua.Todo.Application.Migrations
|
|
||||||
{
|
|
||||||
[DbContext(typeof(TodoDbContext))]
|
|
||||||
[Migration("20260406173734_AddAllowSyncToSecurityPolicy")]
|
|
||||||
partial class AddAllowSyncToSecurityPolicy
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.5");
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.SecurityPolicyEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<bool>("AllowPersist")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(true);
|
|
||||||
|
|
||||||
b.Property<bool>("AllowSync")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(true);
|
|
||||||
|
|
||||||
b.Property<Guid>("UserId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("UserId")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("SecurityPolicies", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.TaskEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT")
|
|
||||||
.HasDefaultValueSql("datetime('now')");
|
|
||||||
|
|
||||||
b.Property<bool>("IsCompleted")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(false);
|
|
||||||
|
|
||||||
b.Property<int?>("ParentTaskId")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<int>("Priority")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(1);
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(200)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<DateTime>("UpdatedAt")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT")
|
|
||||||
.HasDefaultValueSql("datetime('now')");
|
|
||||||
|
|
||||||
b.Property<Guid>("UserId")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT")
|
|
||||||
.HasDefaultValue(new Guid("00000000-0000-0000-0000-000000000001"));
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("ParentTaskId");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.ToTable("Tasks", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.UserEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("PasswordHash")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Role")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(32)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("UserName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("UserName")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("Users", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.UserSessionEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAtUtc")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<DateTime>("ExpiresAtUtc")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("StepUpExpiresAtUtc")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<Guid>("UserId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.ToTable("UserSessions", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.SecurityPolicyEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Hua.Todo.Core.Entities.UserEntity", "User")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("User");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.TaskEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Hua.Todo.Core.Entities.TaskEntity", "ParentTask")
|
|
||||||
.WithMany("SubTasks")
|
|
||||||
.HasForeignKey("ParentTaskId")
|
|
||||||
.OnDelete(DeleteBehavior.Restrict);
|
|
||||||
|
|
||||||
b.HasOne("Hua.Todo.Core.Entities.UserEntity", "User")
|
|
||||||
.WithMany("Tasks")
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("ParentTask");
|
|
||||||
|
|
||||||
b.Navigation("User");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.UserSessionEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Hua.Todo.Core.Entities.UserEntity", "User")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("User");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.TaskEntity", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("SubTasks");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.UserEntity", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("Tasks");
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Hua.Todo.Application.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class AddAllowSyncToSecurityPolicy : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.AddColumn<bool>(
|
|
||||||
name: "AllowSync",
|
|
||||||
table: "SecurityPolicies",
|
|
||||||
type: "INTEGER",
|
|
||||||
nullable: false,
|
|
||||||
defaultValue: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "AllowSync",
|
|
||||||
table: "SecurityPolicies");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-219
@@ -1,219 +0,0 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using Hua.Todo.Application.Data;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Hua.Todo.Application.Migrations
|
|
||||||
{
|
|
||||||
[DbContext(typeof(TodoDbContext))]
|
|
||||||
[Migration("20260413140347_UpdateSecurityEntities")]
|
|
||||||
partial class UpdateSecurityEntities
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.5");
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.SecurityPolicyEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<bool>("AllowPersist")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(true);
|
|
||||||
|
|
||||||
b.Property<bool>("AllowSync")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(true);
|
|
||||||
|
|
||||||
b.Property<bool>("IsTrustedDeviceOnly")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<int>("SecondFactorExpiryMinutes")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<Guid>("UserId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("UserId")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("SecurityPolicies", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.TaskEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("CreatedAt")
|
|
||||||
.IsRequired()
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT")
|
|
||||||
.HasDefaultValueSql("datetime('now')");
|
|
||||||
|
|
||||||
b.Property<bool>("IsCompleted")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(false);
|
|
||||||
|
|
||||||
b.Property<int?>("ParentTaskId")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<int>("Priority")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(1);
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(200)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("UpdatedAt")
|
|
||||||
.IsRequired()
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT")
|
|
||||||
.HasDefaultValueSql("datetime('now')");
|
|
||||||
|
|
||||||
b.Property<Guid>("UserId")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT")
|
|
||||||
.HasDefaultValue(new Guid("00000000-0000-0000-0000-000000000001"));
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("ParentTaskId");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.ToTable("Tasks", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.UserEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAtUtc")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("PasswordHash")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Role")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(32)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<DateTime>("UpdatedAtUtc")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("UserName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("UserName")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("Users", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.UserSessionEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("CreatedAtUtc")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("ExpiresAtUtc")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("StepUpExpiresAtUtc")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<Guid>("UserId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.ToTable("UserSessions", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.SecurityPolicyEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Hua.Todo.Core.Entities.UserEntity", "User")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("User");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.TaskEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Hua.Todo.Core.Entities.TaskEntity", "ParentTask")
|
|
||||||
.WithMany("SubTasks")
|
|
||||||
.HasForeignKey("ParentTaskId")
|
|
||||||
.OnDelete(DeleteBehavior.Restrict);
|
|
||||||
|
|
||||||
b.HasOne("Hua.Todo.Core.Entities.UserEntity", "User")
|
|
||||||
.WithMany("Tasks")
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("ParentTask");
|
|
||||||
|
|
||||||
b.Navigation("User");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.UserSessionEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Hua.Todo.Core.Entities.UserEntity", "User")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("User");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.TaskEntity", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("SubTasks");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.UserEntity", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("Tasks");
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Hua.Todo.Application.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class UpdateSecurityEntities : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.AddColumn<DateTime>(
|
|
||||||
name: "CreatedAtUtc",
|
|
||||||
table: "Users",
|
|
||||||
type: "TEXT",
|
|
||||||
nullable: false,
|
|
||||||
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
|
|
||||||
|
|
||||||
migrationBuilder.AddColumn<DateTime>(
|
|
||||||
name: "UpdatedAtUtc",
|
|
||||||
table: "Users",
|
|
||||||
type: "TEXT",
|
|
||||||
nullable: false,
|
|
||||||
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
|
|
||||||
|
|
||||||
migrationBuilder.AddColumn<bool>(
|
|
||||||
name: "IsTrustedDeviceOnly",
|
|
||||||
table: "SecurityPolicies",
|
|
||||||
type: "INTEGER",
|
|
||||||
nullable: false,
|
|
||||||
defaultValue: false);
|
|
||||||
|
|
||||||
migrationBuilder.AddColumn<int>(
|
|
||||||
name: "SecondFactorExpiryMinutes",
|
|
||||||
table: "SecurityPolicies",
|
|
||||||
type: "INTEGER",
|
|
||||||
nullable: false,
|
|
||||||
defaultValue: 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "CreatedAtUtc",
|
|
||||||
table: "Users");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "UpdatedAtUtc",
|
|
||||||
table: "Users");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "IsTrustedDeviceOnly",
|
|
||||||
table: "SecurityPolicies");
|
|
||||||
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "SecondFactorExpiryMinutes",
|
|
||||||
table: "SecurityPolicies");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-269
@@ -1,269 +0,0 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using Hua.Todo.Application.Data;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Hua.Todo.Application.Migrations
|
|
||||||
{
|
|
||||||
[DbContext(typeof(TodoDbContext))]
|
|
||||||
[Migration("20260413140753_AddAuditLogs")]
|
|
||||||
partial class AddAuditLogs
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.5");
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.AuditLogEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("ClientIp")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(500)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("EventType")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<bool>("IsSuccess")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("TimestampUtc")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("UserAgent")
|
|
||||||
.HasMaxLength(500)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<Guid?>("UserId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("UserName")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("TimestampUtc");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.ToTable("AuditLogs", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.SecurityPolicyEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<bool>("AllowPersist")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(true);
|
|
||||||
|
|
||||||
b.Property<bool>("AllowSync")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(true);
|
|
||||||
|
|
||||||
b.Property<bool>("IsTrustedDeviceOnly")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(false);
|
|
||||||
|
|
||||||
b.Property<int>("SecondFactorExpiryMinutes")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(30);
|
|
||||||
|
|
||||||
b.Property<Guid>("UserId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("UserId")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("SecurityPolicies", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.TaskEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("CreatedAt")
|
|
||||||
.IsRequired()
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT")
|
|
||||||
.HasDefaultValueSql("datetime('now')");
|
|
||||||
|
|
||||||
b.Property<bool>("IsCompleted")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(false);
|
|
||||||
|
|
||||||
b.Property<int?>("ParentTaskId")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<int>("Priority")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(1);
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(200)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("UpdatedAt")
|
|
||||||
.IsRequired()
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT")
|
|
||||||
.HasDefaultValueSql("datetime('now')");
|
|
||||||
|
|
||||||
b.Property<Guid>("UserId")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT")
|
|
||||||
.HasDefaultValue(new Guid("00000000-0000-0000-0000-000000000001"));
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("ParentTaskId");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.ToTable("Tasks", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.UserEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAtUtc")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("PasswordHash")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Role")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(32)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<DateTime>("UpdatedAtUtc")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("UserName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("UserName")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("Users", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.UserSessionEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("CreatedAtUtc")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("ExpiresAtUtc")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("StepUpExpiresAtUtc")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<Guid>("UserId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.ToTable("UserSessions", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.SecurityPolicyEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Hua.Todo.Core.Entities.UserEntity", "User")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("User");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.TaskEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Hua.Todo.Core.Entities.TaskEntity", "ParentTask")
|
|
||||||
.WithMany("SubTasks")
|
|
||||||
.HasForeignKey("ParentTaskId")
|
|
||||||
.OnDelete(DeleteBehavior.Restrict);
|
|
||||||
|
|
||||||
b.HasOne("Hua.Todo.Core.Entities.UserEntity", "User")
|
|
||||||
.WithMany("Tasks")
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("ParentTask");
|
|
||||||
|
|
||||||
b.Navigation("User");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.UserSessionEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Hua.Todo.Core.Entities.UserEntity", "User")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("User");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.TaskEntity", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("SubTasks");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.UserEntity", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("Tasks");
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Hua.Todo.Application.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class AddAuditLogs : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.AlterColumn<int>(
|
|
||||||
name: "SecondFactorExpiryMinutes",
|
|
||||||
table: "SecurityPolicies",
|
|
||||||
type: "INTEGER",
|
|
||||||
nullable: false,
|
|
||||||
defaultValue: 30,
|
|
||||||
oldClrType: typeof(int),
|
|
||||||
oldType: "INTEGER");
|
|
||||||
|
|
||||||
migrationBuilder.AlterColumn<bool>(
|
|
||||||
name: "IsTrustedDeviceOnly",
|
|
||||||
table: "SecurityPolicies",
|
|
||||||
type: "INTEGER",
|
|
||||||
nullable: false,
|
|
||||||
defaultValue: false,
|
|
||||||
oldClrType: typeof(bool),
|
|
||||||
oldType: "INTEGER");
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "AuditLogs",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
|
||||||
TimestampUtc = table.Column<string>(type: "TEXT", nullable: false),
|
|
||||||
UserId = table.Column<Guid>(type: "TEXT", nullable: true),
|
|
||||||
UserName = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
EventType = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
|
||||||
Description = table.Column<string>(type: "TEXT", maxLength: 500, nullable: false),
|
|
||||||
ClientIp = table.Column<string>(type: "TEXT", maxLength: 64, nullable: true),
|
|
||||||
UserAgent = table.Column<string>(type: "TEXT", maxLength: 500, nullable: true),
|
|
||||||
IsSuccess = table.Column<bool>(type: "INTEGER", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_AuditLogs", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_AuditLogs_TimestampUtc",
|
|
||||||
table: "AuditLogs",
|
|
||||||
column: "TimestampUtc");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_AuditLogs_UserId",
|
|
||||||
table: "AuditLogs",
|
|
||||||
column: "UserId");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "AuditLogs");
|
|
||||||
|
|
||||||
migrationBuilder.AlterColumn<int>(
|
|
||||||
name: "SecondFactorExpiryMinutes",
|
|
||||||
table: "SecurityPolicies",
|
|
||||||
type: "INTEGER",
|
|
||||||
nullable: false,
|
|
||||||
oldClrType: typeof(int),
|
|
||||||
oldType: "INTEGER",
|
|
||||||
oldDefaultValue: 30);
|
|
||||||
|
|
||||||
migrationBuilder.AlterColumn<bool>(
|
|
||||||
name: "IsTrustedDeviceOnly",
|
|
||||||
table: "SecurityPolicies",
|
|
||||||
type: "INTEGER",
|
|
||||||
nullable: false,
|
|
||||||
oldClrType: typeof(bool),
|
|
||||||
oldType: "INTEGER",
|
|
||||||
oldDefaultValue: false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-273
@@ -1,273 +0,0 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using Hua.Todo.Application.Data;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Hua.Todo.Application.Migrations
|
|
||||||
{
|
|
||||||
[DbContext(typeof(TodoDbContext))]
|
|
||||||
[Migration("20260424164713_AddPasswordSaltToUsers")]
|
|
||||||
partial class AddPasswordSaltToUsers
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.5");
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.AuditLogEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("ClientIp")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(500)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("EventType")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<bool>("IsSuccess")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("TimestampUtc")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("UserAgent")
|
|
||||||
.HasMaxLength(500)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<Guid?>("UserId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("UserName")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("TimestampUtc");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.ToTable("AuditLogs", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.SecurityPolicyEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<bool>("AllowPersist")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(true);
|
|
||||||
|
|
||||||
b.Property<bool>("AllowSync")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(true);
|
|
||||||
|
|
||||||
b.Property<bool>("IsTrustedDeviceOnly")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(false);
|
|
||||||
|
|
||||||
b.Property<int>("SecondFactorExpiryMinutes")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(30);
|
|
||||||
|
|
||||||
b.Property<Guid>("UserId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("UserId")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("SecurityPolicies", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.TaskEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("CreatedAt")
|
|
||||||
.IsRequired()
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT")
|
|
||||||
.HasDefaultValueSql("datetime('now')");
|
|
||||||
|
|
||||||
b.Property<bool>("IsCompleted")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(false);
|
|
||||||
|
|
||||||
b.Property<int?>("ParentTaskId")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<int>("Priority")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(1);
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(200)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("UpdatedAt")
|
|
||||||
.IsRequired()
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT")
|
|
||||||
.HasDefaultValueSql("datetime('now')");
|
|
||||||
|
|
||||||
b.Property<Guid>("UserId")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT")
|
|
||||||
.HasDefaultValue(new Guid("00000000-0000-0000-0000-000000000001"));
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("ParentTaskId");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.ToTable("Tasks", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.UserEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAtUtc")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("PasswordHash")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("PasswordSalt")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Role")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(32)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<DateTime>("UpdatedAtUtc")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("UserName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("UserName")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("Users", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.UserSessionEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("CreatedAtUtc")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("ExpiresAtUtc")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("StepUpExpiresAtUtc")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<Guid>("UserId")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.ToTable("UserSessions", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.SecurityPolicyEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Hua.Todo.Core.Entities.UserEntity", "User")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("User");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.TaskEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Hua.Todo.Core.Entities.TaskEntity", "ParentTask")
|
|
||||||
.WithMany("SubTasks")
|
|
||||||
.HasForeignKey("ParentTaskId")
|
|
||||||
.OnDelete(DeleteBehavior.Restrict);
|
|
||||||
|
|
||||||
b.HasOne("Hua.Todo.Core.Entities.UserEntity", "User")
|
|
||||||
.WithMany("Tasks")
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("ParentTask");
|
|
||||||
|
|
||||||
b.Navigation("User");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.UserSessionEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Hua.Todo.Core.Entities.UserEntity", "User")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("User");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.TaskEntity", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("SubTasks");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.UserEntity", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("Tasks");
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Hua.Todo.Application.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class AddPasswordSaltToUsers : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.AddColumn<string>(
|
|
||||||
name: "PasswordSalt",
|
|
||||||
table: "Users",
|
|
||||||
type: "TEXT",
|
|
||||||
nullable: false,
|
|
||||||
defaultValue: "");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "PasswordSalt",
|
|
||||||
table: "Users");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Hua.Todo.Application.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class AddMustChangePasswordToUsers : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.AddColumn<bool>(
|
|
||||||
name: "MustChangePassword",
|
|
||||||
table: "Users",
|
|
||||||
type: "INTEGER",
|
|
||||||
nullable: false,
|
|
||||||
defaultValue: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropColumn(
|
|
||||||
name: "MustChangePassword",
|
|
||||||
table: "Users");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Hua.Todo.Application.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
/// <summary>
|
|
||||||
/// 重构 TaskEntity 继承 ABP 风格审计基类。
|
|
||||||
/// - 主键从 int 改为 Guid
|
|
||||||
/// - ParentTaskId 从 int? 改为 Guid?
|
|
||||||
/// - 新增 ABP 审计字段:ExtraProperties, ConcurrencyStamp, CreationTime, CreatorId,
|
|
||||||
/// LastModificationTime, LastModifierId, IsDeleted, DeletionTime, DeleterId
|
|
||||||
/// - 重命名 CreatedAt -> CreationTime, UpdatedAt -> LastModificationTime
|
|
||||||
/// - 表名从 Tasks 改为 T_Tasks
|
|
||||||
/// </summary>
|
|
||||||
public partial class MakeTaskEntityAbpCompatible : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
// 1. 创建临时表 T_Tasks(新结构)
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "T_Tasks",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
|
||||||
ExtraProperties = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
ConcurrencyStamp = table.Column<string>(type: "TEXT", nullable: true),
|
|
||||||
CreationTime = table.Column<DateTime>(type: "TEXT", nullable: false, defaultValueSql: "datetime('now')"),
|
|
||||||
CreatorId = table.Column<Guid>(type: "TEXT", nullable: true),
|
|
||||||
LastModificationTime = table.Column<DateTime>(type: "TEXT", nullable: true),
|
|
||||||
LastModifierId = table.Column<Guid>(type: "TEXT", nullable: true),
|
|
||||||
IsDeleted = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: false),
|
|
||||||
DeletionTime = table.Column<DateTime>(type: "TEXT", nullable: true),
|
|
||||||
DeleterId = table.Column<Guid>(type: "TEXT", nullable: true),
|
|
||||||
UserId = table.Column<Guid>(type: "TEXT", nullable: false, defaultValue: "local"),
|
|
||||||
Title = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
|
||||||
Priority = table.Column<int>(type: "INTEGER", nullable: false, defaultValue: 1),
|
|
||||||
IsCompleted = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: false),
|
|
||||||
Code = table.Column<string>(type: "TEXT", nullable: false, defaultValue: ""),
|
|
||||||
ParentTaskId = table.Column<Guid>(type: "TEXT", nullable: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_T_Tasks", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_T_Tasks_Users_UserId",
|
|
||||||
column: x => x.UserId,
|
|
||||||
principalTable: "Users",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_T_Tasks_T_Tasks_ParentTaskId",
|
|
||||||
column: x => x.ParentTaskId,
|
|
||||||
principalTable: "T_Tasks",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Restrict);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 2. 创建索引
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_T_Tasks_ParentTaskId",
|
|
||||||
table: "T_Tasks",
|
|
||||||
column: "ParentTaskId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_T_Tasks_UserId",
|
|
||||||
table: "T_Tasks",
|
|
||||||
column: "UserId");
|
|
||||||
|
|
||||||
// 3. 数据迁移(int Id -> Guid,CreatedAt -> CreationTime,UpdatedAt -> LastModificationTime)
|
|
||||||
// 注意:原有数据会丢失,因为 int 无法直接转换为 Guid
|
|
||||||
// 如需保留数据,需先导出再导入
|
|
||||||
|
|
||||||
// 4. 先禁用外键检查,再重命名旧表(避免外键约束失败)
|
|
||||||
migrationBuilder.Sql("PRAGMA foreign_keys = OFF;");
|
|
||||||
migrationBuilder.RenameTable(name: "Tasks", newName: "Tasks_Old");
|
|
||||||
migrationBuilder.Sql("PRAGMA foreign_keys = ON;");
|
|
||||||
|
|
||||||
// 注意:不重命名 T_Tasks 为 Tasks,因为 TodoDbContext 配置的表名是 T_Tasks
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
// 此回滚为破坏性操作,数据会丢失
|
|
||||||
// 重建旧结构 Tasks 表(仅用于回滚,实际数据不可恢复)
|
|
||||||
|
|
||||||
// 1. 禁用外键检查
|
|
||||||
migrationBuilder.Sql("PRAGMA foreign_keys = OFF;");
|
|
||||||
|
|
||||||
// 2. 删除新表
|
|
||||||
migrationBuilder.DropTable(name: "T_Tasks");
|
|
||||||
|
|
||||||
// 3. 重建旧结构 Tasks 表
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Tasks",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<int>(type: "INTEGER", nullable: false),
|
|
||||||
UserId = table.Column<Guid>(type: "TEXT", nullable: false, defaultValue: "local"),
|
|
||||||
Title = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
|
||||||
Priority = table.Column<int>(type: "INTEGER", nullable: false, defaultValue: 1),
|
|
||||||
IsCompleted = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: false),
|
|
||||||
CreatedAt = table.Column<string>(type: "TEXT", nullable: false, defaultValueSql: "datetime('now')"),
|
|
||||||
UpdatedAt = table.Column<string>(type: "TEXT", nullable: false, defaultValueSql: "datetime('now')"),
|
|
||||||
ParentTaskId = table.Column<int>(type: "INTEGER", nullable: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Tasks", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 4. 如果有重命名的旧表,也删除
|
|
||||||
migrationBuilder.Sql("DROP TABLE IF EXISTS Tasks_Old;");
|
|
||||||
|
|
||||||
// 5. 重新启用外键检查
|
|
||||||
migrationBuilder.Sql("PRAGMA foreign_keys = ON;");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using Hua.Todo.Application.Data;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Hua.Todo.Application.Migrations
|
|
||||||
{
|
|
||||||
[DbContext(typeof(TodoDbContext))]
|
|
||||||
partial class TodoDbContextModelSnapshot : ModelSnapshot
|
|
||||||
{
|
|
||||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
#pragma warning disable 612, 618
|
|
||||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.5");
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.TaskEntity", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreatedAt")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT")
|
|
||||||
.HasDefaultValueSql("datetime('now')");
|
|
||||||
|
|
||||||
b.Property<bool>("IsCompleted")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(false);
|
|
||||||
|
|
||||||
b.Property<int?>("ParentTaskId")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<int>("Priority")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(1);
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(200)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<DateTime>("UpdatedAt")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT")
|
|
||||||
.HasDefaultValueSql("datetime('now')");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("ParentTaskId");
|
|
||||||
|
|
||||||
b.ToTable("Tasks");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.TaskEntity", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Hua.Todo.Core.Entities.TaskEntity", "ParentTask")
|
|
||||||
.WithMany("SubTasks")
|
|
||||||
.HasForeignKey("ParentTaskId")
|
|
||||||
.OnDelete(DeleteBehavior.Restrict);
|
|
||||||
|
|
||||||
b.Navigation("ParentTask");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.TaskEntity", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("SubTasks");
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
using Hua.Todo.Core.Entities;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Hua.Todo.Application.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 附件返回 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public class AttachmentDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 附件 ID。
|
||||||
|
/// </summary>
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 所属待办项 ID。
|
||||||
|
/// </summary>
|
||||||
|
public Guid TaskId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 显示名称。
|
||||||
|
/// </summary>
|
||||||
|
public string FileName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 文件路径或外部 URL。
|
||||||
|
/// </summary>
|
||||||
|
public string FilePath { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 文件大小(字节),外部链接为 0。
|
||||||
|
/// </summary>
|
||||||
|
public long FileSize { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MIME 类型。
|
||||||
|
/// </summary>
|
||||||
|
public string ContentType { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 附件类型(0=本地文件, 1=外部链接)。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||||
|
public AttachmentType AttachmentType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建时间(UTC)。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 上传附件请求 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public class UploadAttachmentRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 所属待办项 ID。
|
||||||
|
/// </summary>
|
||||||
|
public Guid TaskId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 原始文件名。
|
||||||
|
/// </summary>
|
||||||
|
public string FileName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base64 编码的文件内容。
|
||||||
|
/// </summary>
|
||||||
|
public string Base64Content { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 文件 MIME 类型。
|
||||||
|
/// </summary>
|
||||||
|
public string ContentType { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 添加外部链接请求 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public class AddLinkRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 所属待办项 ID。
|
||||||
|
/// </summary>
|
||||||
|
public Guid TaskId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 外部链接 URL,仅允许 http:// 或 https:// 协议。
|
||||||
|
/// </summary>
|
||||||
|
public string Url { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 链接显示名称,不传则使用 URL 作为名称。
|
||||||
|
/// </summary>
|
||||||
|
public string? FileName { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 打开附件响应 DTO。
|
||||||
|
/// </summary>
|
||||||
|
public class OpenAttachmentResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 是否成功打开。
|
||||||
|
/// </summary>
|
||||||
|
public bool Opened { get; set; }
|
||||||
|
}
|
||||||
@@ -13,38 +13,61 @@ public class CreateTaskDto
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string Title { get; set; } = string.Empty;
|
public string Title { get; set; } = string.Empty;
|
||||||
|
|
||||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 任务优先级。
|
/// 任务优先级。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||||
public TaskPriority Priority { get; set; } = TaskPriority.Medium;
|
public TaskPriority Priority { get; set; } = TaskPriority.Medium;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 父任务 ID(用于创建子任务)。
|
/// 父任务 ID(用于创建子任务)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? ParentTaskId { get; set; }
|
public Guid? ParentTaskId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 待办项类型(0=Normal, 1=Meeting),默认 Normal。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||||
|
public TaskType TaskType { get; set; } = TaskType.Normal;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 多行描述文本,最大 5000 字符。
|
||||||
|
/// </summary>
|
||||||
|
public string? Description { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 更新任务请求 DTO。
|
/// 更新任务请求 DTO。
|
||||||
|
/// 未传递的字段保持原值不变,传递 null 表示清空。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class UpdateTaskDto
|
public class UpdateTaskDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 任务 ID。
|
/// 任务 ID。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 新标题(可选)。
|
/// 新标题(可选,不传则保持原值)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? Title { get; set; }
|
public string? Title { get; set; }
|
||||||
|
|
||||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 新优先级(可选)。
|
/// 新优先级(可选,不传则保持原值)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||||
public TaskPriority? Priority { get; set; }
|
public TaskPriority? Priority { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 待办项类型(可选,不传则保持原值)。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||||
|
public TaskType? TaskType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 多行描述文本(可选,不传则保持原值,传 null 则清空)。
|
||||||
|
/// </summary>
|
||||||
|
public string? Description { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -55,16 +78,22 @@ public class TaskDto
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 任务 ID。
|
/// 任务 ID。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 任务编号(用户级自增字符串,用于展示)。
|
||||||
|
/// </summary>
|
||||||
|
public string Code { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 任务标题。
|
/// 任务标题。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Title { get; set; } = string.Empty;
|
public string Title { get; set; } = string.Empty;
|
||||||
|
|
||||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 任务优先级。
|
/// 任务优先级。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||||
public TaskPriority Priority { get; set; }
|
public TaskPriority Priority { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -82,11 +111,42 @@ public class TaskDto
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 父任务 ID(可选)。
|
/// 父任务 ID(可选)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? ParentTaskId { get; set; }
|
public Guid? ParentTaskId { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 子任务列表。
|
/// 子任务列表。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<TaskDto> SubTasks { get; set; } = new();
|
public List<TaskDto> SubTasks { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 待办项类型。
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||||
|
public TaskType TaskType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 会议纪要/转写文字。
|
||||||
|
/// </summary>
|
||||||
|
public string? MeetingNotes { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 录音时长(秒)。
|
||||||
|
/// </summary>
|
||||||
|
public double? AudioDuration { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 多行描述文本。
|
||||||
|
/// </summary>
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 附件列表(详情视图使用)。
|
||||||
|
/// </summary>
|
||||||
|
public List<AttachmentDto>? Attachments { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 附件数量(列表视图使用,不传完整附件列表)。
|
||||||
|
/// </summary>
|
||||||
|
public int? AttachmentCount { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Hua.Todo.Core.Entities;
|
||||||
|
using Hua.Todo.Core.Interfaces;
|
||||||
|
|
||||||
|
namespace Hua.Todo.Application.Repositories;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 附件仓储实现(EF Core)。
|
||||||
|
/// </summary>
|
||||||
|
public class AttachmentRepository : IAttachmentRepository
|
||||||
|
{
|
||||||
|
private readonly TodoDbContext _context;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建 <see cref="AttachmentRepository"/>。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">数据库上下文。</param>
|
||||||
|
public AttachmentRepository(TodoDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<List<AttachmentEntity>> GetByTaskIdAsync(Guid taskId)
|
||||||
|
{
|
||||||
|
return await _context.Attachments
|
||||||
|
.Where(a => a.TaskId == taskId)
|
||||||
|
.OrderByDescending(a => a.CreatedAt)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<AttachmentEntity?> GetByIdAsync(Guid id)
|
||||||
|
{
|
||||||
|
return await _context.Attachments
|
||||||
|
.FirstOrDefaultAsync(a => a.Id == id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<AttachmentEntity> AddAsync(AttachmentEntity attachment)
|
||||||
|
{
|
||||||
|
_context.Attachments.Add(attachment);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
return attachment;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task DeleteAsync(Guid id)
|
||||||
|
{
|
||||||
|
var attachment = await _context.Attachments.FindAsync(id);
|
||||||
|
if (attachment != null)
|
||||||
|
{
|
||||||
|
_context.Attachments.Remove(attachment);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<int> GetCountByTaskIdAsync(Guid taskId)
|
||||||
|
{
|
||||||
|
return await _context.Attachments
|
||||||
|
.CountAsync(a => a.TaskId == taskId);
|
||||||
|
}
|
||||||
|
}
|
||||||
+89
-22
@@ -1,6 +1,6 @@
|
|||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Hua.Todo.Application.Data;
|
using Hua.Todo.Application.Repositories;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
@@ -11,8 +11,8 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|||||||
namespace Hua.Todo.Application.Migrations
|
namespace Hua.Todo.Application.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(TodoDbContext))]
|
[DbContext(typeof(TodoDbContext))]
|
||||||
[Migration("MakeTaskEntityAbpCompatible")]
|
[Migration("20260616203619_InitialCreate")]
|
||||||
partial class MakeTaskEntityAbpCompatible
|
partial class InitialCreate
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
@@ -20,6 +20,50 @@ namespace Hua.Todo.Application.Migrations
|
|||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.5");
|
modelBuilder.HasAnnotation("ProductVersion", "10.0.5");
|
||||||
|
|
||||||
|
modelBuilder.Entity("Hua.Todo.Core.Entities.AttachmentEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("AttachmentType")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(0);
|
||||||
|
|
||||||
|
b.Property<string>("ContentType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValueSql("datetime('now')");
|
||||||
|
|
||||||
|
b.Property<string>("FileName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("FilePath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<long>("FileSize")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<Guid>("TaskId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TaskId");
|
||||||
|
|
||||||
|
b.ToTable("Attachments", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.AuditLogEntity", b =>
|
modelBuilder.Entity("Hua.Todo.Core.Entities.AuditLogEntity", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@@ -43,8 +87,7 @@ namespace Hua.Todo.Application.Migrations
|
|||||||
b.Property<bool>("IsSuccess")
|
b.Property<bool>("IsSuccess")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<string>("TimestampUtc")
|
b.Property<DateTime>("TimestampUtc")
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("UserAgent")
|
b.Property<string>("UserAgent")
|
||||||
@@ -83,9 +126,7 @@ namespace Hua.Todo.Application.Migrations
|
|||||||
.HasDefaultValue(true);
|
.HasDefaultValue(true);
|
||||||
|
|
||||||
b.Property<bool>("IsTrustedDeviceOnly")
|
b.Property<bool>("IsTrustedDeviceOnly")
|
||||||
.ValueGeneratedOnAdd()
|
.HasColumnType("INTEGER");
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(false);
|
|
||||||
|
|
||||||
b.Property<int>("SecondFactorExpiryMinutes")
|
b.Property<int>("SecondFactorExpiryMinutes")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
@@ -109,6 +150,14 @@ namespace Hua.Todo.Application.Migrations
|
|||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<double?>("AudioDuration")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<string>("Code")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("ConcurrencyStamp")
|
b.Property<string>("ConcurrencyStamp")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
@@ -120,10 +169,14 @@ namespace Hua.Todo.Application.Migrations
|
|||||||
b.Property<Guid?>("CreatorId")
|
b.Property<Guid?>("CreatorId")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid?>("DeleterId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<DateTime?>("DeletionTime")
|
b.Property<DateTime?>("DeletionTime")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<Guid?>("DeleterId")
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(5000)
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("ExtraProperties")
|
b.Property<string>("ExtraProperties")
|
||||||
@@ -134,10 +187,6 @@ namespace Hua.Todo.Application.Migrations
|
|||||||
.HasColumnType("INTEGER")
|
.HasColumnType("INTEGER")
|
||||||
.HasDefaultValue(false);
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
b.Property<string>("Code")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<bool>("IsDeleted")
|
b.Property<bool>("IsDeleted")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("INTEGER")
|
.HasColumnType("INTEGER")
|
||||||
@@ -149,6 +198,10 @@ namespace Hua.Todo.Application.Migrations
|
|||||||
b.Property<Guid?>("LastModifierId")
|
b.Property<Guid?>("LastModifierId")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("MeetingNotes")
|
||||||
|
.HasMaxLength(20000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<Guid?>("ParentTaskId")
|
b.Property<Guid?>("ParentTaskId")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
@@ -157,15 +210,18 @@ namespace Hua.Todo.Application.Migrations
|
|||||||
.HasColumnType("INTEGER")
|
.HasColumnType("INTEGER")
|
||||||
.HasDefaultValue(1);
|
.HasDefaultValue(1);
|
||||||
|
|
||||||
|
b.Property<int>("TaskType")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(0);
|
||||||
|
|
||||||
b.Property<string>("Title")
|
b.Property<string>("Title")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(200)
|
.HasMaxLength(200)
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<Guid>("UserId")
|
b.Property<Guid>("UserId")
|
||||||
.ValueGeneratedOnAdd()
|
.HasColumnType("TEXT");
|
||||||
.HasColumnType("TEXT")
|
|
||||||
.HasDefaultValue(new Guid("00000000-0000-0000-0000-000000000001"));
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
@@ -223,15 +279,13 @@ namespace Hua.Todo.Application.Migrations
|
|||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("CreatedAtUtc")
|
b.Property<DateTime>("CreatedAtUtc")
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("ExpiresAtUtc")
|
b.Property<DateTime>("ExpiresAtUtc")
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("StepUpExpiresAtUtc")
|
b.Property<DateTime?>("StepUpExpiresAtUtc")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<Guid>("UserId")
|
b.Property<Guid>("UserId")
|
||||||
@@ -244,6 +298,17 @@ namespace Hua.Todo.Application.Migrations
|
|||||||
b.ToTable("UserSessions", (string)null);
|
b.ToTable("UserSessions", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Hua.Todo.Core.Entities.AttachmentEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Hua.Todo.Core.Entities.TaskEntity", "Task")
|
||||||
|
.WithMany("Attachments")
|
||||||
|
.HasForeignKey("TaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Task");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.SecurityPolicyEntity", b =>
|
modelBuilder.Entity("Hua.Todo.Core.Entities.SecurityPolicyEntity", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Hua.Todo.Core.Entities.UserEntity", "User")
|
b.HasOne("Hua.Todo.Core.Entities.UserEntity", "User")
|
||||||
@@ -286,6 +351,8 @@ namespace Hua.Todo.Application.Migrations
|
|||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.TaskEntity", b =>
|
modelBuilder.Entity("Hua.Todo.Core.Entities.TaskEntity", b =>
|
||||||
{
|
{
|
||||||
|
b.Navigation("Attachments");
|
||||||
|
|
||||||
b.Navigation("SubTasks");
|
b.Navigation("SubTasks");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Hua.Todo.Application.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class InitialCreate : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AuditLogs",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||||
|
TimestampUtc = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
UserId = table.Column<Guid>(type: "TEXT", nullable: true),
|
||||||
|
UserName = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
EventType = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||||
|
Description = table.Column<string>(type: "TEXT", maxLength: 500, nullable: false),
|
||||||
|
ClientIp = table.Column<string>(type: "TEXT", maxLength: 64, nullable: true),
|
||||||
|
UserAgent = table.Column<string>(type: "TEXT", maxLength: 500, nullable: true),
|
||||||
|
IsSuccess = table.Column<bool>(type: "INTEGER", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_AuditLogs", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Users",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||||
|
UserName = table.Column<string>(type: "TEXT", maxLength: 64, nullable: false),
|
||||||
|
PasswordHash = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
PasswordSalt = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
Role = table.Column<string>(type: "TEXT", maxLength: 32, nullable: false),
|
||||||
|
CreatedAtUtc = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
UpdatedAtUtc = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
MustChangePassword = table.Column<bool>(type: "INTEGER", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Users", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "SecurityPolicies",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||||
|
UserId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||||
|
AllowPersist = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: true),
|
||||||
|
AllowSync = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: true),
|
||||||
|
SecondFactorExpiryMinutes = table.Column<int>(type: "INTEGER", nullable: false, defaultValue: 30),
|
||||||
|
IsTrustedDeviceOnly = table.Column<bool>(type: "INTEGER", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_SecurityPolicies", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_SecurityPolicies_Users_UserId",
|
||||||
|
column: x => x.UserId,
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "T_Tasks",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||||
|
UserId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||||
|
Title = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||||
|
Priority = table.Column<int>(type: "INTEGER", nullable: false, defaultValue: 1),
|
||||||
|
Code = table.Column<string>(type: "TEXT", maxLength: 32, nullable: false),
|
||||||
|
IsCompleted = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: false),
|
||||||
|
ParentTaskId = table.Column<Guid>(type: "TEXT", nullable: true),
|
||||||
|
TaskType = table.Column<int>(type: "INTEGER", nullable: false, defaultValue: 0),
|
||||||
|
MeetingNotes = table.Column<string>(type: "TEXT", maxLength: 20000, nullable: true),
|
||||||
|
AudioDuration = table.Column<double>(type: "REAL", nullable: true),
|
||||||
|
Description = table.Column<string>(type: "TEXT", maxLength: 5000, nullable: true),
|
||||||
|
ExtraProperties = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
ConcurrencyStamp = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
CreationTime = table.Column<DateTime>(type: "TEXT", nullable: false, defaultValueSql: "datetime('now')"),
|
||||||
|
CreatorId = table.Column<Guid>(type: "TEXT", nullable: true),
|
||||||
|
LastModificationTime = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||||
|
LastModifierId = table.Column<Guid>(type: "TEXT", nullable: true),
|
||||||
|
IsDeleted = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: false),
|
||||||
|
DeletionTime = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||||
|
DeleterId = table.Column<Guid>(type: "TEXT", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_T_Tasks", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_T_Tasks_T_Tasks_ParentTaskId",
|
||||||
|
column: x => x.ParentTaskId,
|
||||||
|
principalTable: "T_Tasks",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_T_Tasks_Users_UserId",
|
||||||
|
column: x => x.UserId,
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "UserSessions",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||||
|
UserId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||||
|
CreatedAtUtc = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
ExpiresAtUtc = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
StepUpExpiresAtUtc = table.Column<DateTime>(type: "TEXT", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_UserSessions", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_UserSessions_Users_UserId",
|
||||||
|
column: x => x.UserId,
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Attachments",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||||
|
TaskId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||||
|
FileName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false),
|
||||||
|
FilePath = table.Column<string>(type: "TEXT", maxLength: 1024, nullable: false),
|
||||||
|
FileSize = table.Column<long>(type: "INTEGER", nullable: false),
|
||||||
|
ContentType = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
|
||||||
|
AttachmentType = table.Column<int>(type: "INTEGER", nullable: false, defaultValue: 0),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false, defaultValueSql: "datetime('now')")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Attachments", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Attachments_T_Tasks_TaskId",
|
||||||
|
column: x => x.TaskId,
|
||||||
|
principalTable: "T_Tasks",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Attachments_TaskId",
|
||||||
|
table: "Attachments",
|
||||||
|
column: "TaskId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_AuditLogs_TimestampUtc",
|
||||||
|
table: "AuditLogs",
|
||||||
|
column: "TimestampUtc");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_AuditLogs_UserId",
|
||||||
|
table: "AuditLogs",
|
||||||
|
column: "UserId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_SecurityPolicies_UserId",
|
||||||
|
table: "SecurityPolicies",
|
||||||
|
column: "UserId",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_T_Tasks_ParentTaskId",
|
||||||
|
table: "T_Tasks",
|
||||||
|
column: "ParentTaskId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_T_Tasks_UserId",
|
||||||
|
table: "T_Tasks",
|
||||||
|
column: "UserId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Users_UserName",
|
||||||
|
table: "Users",
|
||||||
|
column: "UserName",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_UserSessions_UserId",
|
||||||
|
table: "UserSessions",
|
||||||
|
column: "UserId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Attachments");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "AuditLogs");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "SecurityPolicies");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "UserSessions");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "T_Tasks");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Users");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+118
-31
@@ -1,9 +1,8 @@
|
|||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Hua.Todo.Application.Data;
|
using Hua.Todo.Application.Repositories;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
@@ -11,15 +10,57 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|||||||
namespace Hua.Todo.Application.Migrations
|
namespace Hua.Todo.Application.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(TodoDbContext))]
|
[DbContext(typeof(TodoDbContext))]
|
||||||
[Migration("20260510171230_AddMustChangePasswordToUsers")]
|
partial class TodoDbContextModelSnapshot : ModelSnapshot
|
||||||
partial class AddMustChangePasswordToUsers
|
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.5");
|
modelBuilder.HasAnnotation("ProductVersion", "10.0.5");
|
||||||
|
|
||||||
|
modelBuilder.Entity("Hua.Todo.Core.Entities.AttachmentEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("AttachmentType")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(0);
|
||||||
|
|
||||||
|
b.Property<string>("ContentType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValueSql("datetime('now')");
|
||||||
|
|
||||||
|
b.Property<string>("FileName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("FilePath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<long>("FileSize")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<Guid>("TaskId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TaskId");
|
||||||
|
|
||||||
|
b.ToTable("Attachments", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.AuditLogEntity", b =>
|
modelBuilder.Entity("Hua.Todo.Core.Entities.AuditLogEntity", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@@ -43,8 +84,7 @@ namespace Hua.Todo.Application.Migrations
|
|||||||
b.Property<bool>("IsSuccess")
|
b.Property<bool>("IsSuccess")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<string>("TimestampUtc")
|
b.Property<DateTime>("TimestampUtc")
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("UserAgent")
|
b.Property<string>("UserAgent")
|
||||||
@@ -83,9 +123,7 @@ namespace Hua.Todo.Application.Migrations
|
|||||||
.HasDefaultValue(true);
|
.HasDefaultValue(true);
|
||||||
|
|
||||||
b.Property<bool>("IsTrustedDeviceOnly")
|
b.Property<bool>("IsTrustedDeviceOnly")
|
||||||
.ValueGeneratedOnAdd()
|
.HasColumnType("INTEGER");
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(false);
|
|
||||||
|
|
||||||
b.Property<int>("SecondFactorExpiryMinutes")
|
b.Property<int>("SecondFactorExpiryMinutes")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
@@ -105,44 +143,82 @@ namespace Hua.Todo.Application.Migrations
|
|||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.TaskEntity", b =>
|
modelBuilder.Entity("Hua.Todo.Core.Entities.TaskEntity", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<Guid>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("CreatedAt")
|
b.Property<double?>("AudioDuration")
|
||||||
|
.HasColumnType("REAL");
|
||||||
|
|
||||||
|
b.Property<string>("Code")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreationTime")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
.HasDefaultValueSql("datetime('now')");
|
.HasDefaultValueSql("datetime('now')");
|
||||||
|
|
||||||
|
b.Property<Guid?>("CreatorId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid?>("DeleterId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DeletionTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(5000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("ExtraProperties")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<bool>("IsCompleted")
|
b.Property<bool>("IsCompleted")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("INTEGER")
|
.HasColumnType("INTEGER")
|
||||||
.HasDefaultValue(false);
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
b.Property<int?>("ParentTaskId")
|
b.Property<bool>("IsDeleted")
|
||||||
.HasColumnType("INTEGER");
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastModificationTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid?>("LastModifierId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("MeetingNotes")
|
||||||
|
.HasMaxLength(20000)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid?>("ParentTaskId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<int>("Priority")
|
b.Property<int>("Priority")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("INTEGER")
|
.HasColumnType("INTEGER")
|
||||||
.HasDefaultValue(1);
|
.HasDefaultValue(1);
|
||||||
|
|
||||||
|
b.Property<int>("TaskType")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasDefaultValue(0);
|
||||||
|
|
||||||
b.Property<string>("Title")
|
b.Property<string>("Title")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(200)
|
.HasMaxLength(200)
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("UpdatedAt")
|
|
||||||
.IsRequired()
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("TEXT")
|
|
||||||
.HasDefaultValueSql("datetime('now')");
|
|
||||||
|
|
||||||
b.Property<Guid>("UserId")
|
b.Property<Guid>("UserId")
|
||||||
.ValueGeneratedOnAdd()
|
.HasColumnType("TEXT");
|
||||||
.HasColumnType("TEXT")
|
|
||||||
.HasDefaultValue(new Guid("00000000-0000-0000-0000-000000000001"));
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
@@ -150,7 +226,7 @@ namespace Hua.Todo.Application.Migrations
|
|||||||
|
|
||||||
b.HasIndex("UserId");
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
b.ToTable("Tasks", (string)null);
|
b.ToTable("T_Tasks", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.UserEntity", b =>
|
modelBuilder.Entity("Hua.Todo.Core.Entities.UserEntity", b =>
|
||||||
@@ -200,15 +276,13 @@ namespace Hua.Todo.Application.Migrations
|
|||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("CreatedAtUtc")
|
b.Property<DateTime>("CreatedAtUtc")
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("ExpiresAtUtc")
|
b.Property<DateTime>("ExpiresAtUtc")
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("StepUpExpiresAtUtc")
|
b.Property<DateTime?>("StepUpExpiresAtUtc")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<Guid>("UserId")
|
b.Property<Guid>("UserId")
|
||||||
@@ -221,6 +295,17 @@ namespace Hua.Todo.Application.Migrations
|
|||||||
b.ToTable("UserSessions", (string)null);
|
b.ToTable("UserSessions", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Hua.Todo.Core.Entities.AttachmentEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Hua.Todo.Core.Entities.TaskEntity", "Task")
|
||||||
|
.WithMany("Attachments")
|
||||||
|
.HasForeignKey("TaskId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Task");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.SecurityPolicyEntity", b =>
|
modelBuilder.Entity("Hua.Todo.Core.Entities.SecurityPolicyEntity", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Hua.Todo.Core.Entities.UserEntity", "User")
|
b.HasOne("Hua.Todo.Core.Entities.UserEntity", "User")
|
||||||
@@ -263,6 +348,8 @@ namespace Hua.Todo.Application.Migrations
|
|||||||
|
|
||||||
modelBuilder.Entity("Hua.Todo.Core.Entities.TaskEntity", b =>
|
modelBuilder.Entity("Hua.Todo.Core.Entities.TaskEntity", b =>
|
||||||
{
|
{
|
||||||
|
b.Navigation("Attachments");
|
||||||
|
|
||||||
b.Navigation("SubTasks");
|
b.Navigation("SubTasks");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Hua.Todo.Application.Data;
|
|
||||||
using Hua.Todo.Core.Entities;
|
using Hua.Todo.Core.Entities;
|
||||||
using Hua.Todo.Core.Interfaces;
|
using Hua.Todo.Core.Interfaces;
|
||||||
|
|
||||||
@@ -28,7 +27,7 @@ public class TaskRepository : ITaskRepository
|
|||||||
public async Task<List<TaskEntity>> GetAllAsync()
|
public async Task<List<TaskEntity>> GetAllAsync()
|
||||||
{
|
{
|
||||||
return await _context.Tasks
|
return await _context.Tasks
|
||||||
.Include(t => t.SubTasks)
|
.Include(t => t.SubTasks!)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,10 +36,10 @@ public class TaskRepository : ITaskRepository
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id">任务 ID。</param>
|
/// <param name="id">任务 ID。</param>
|
||||||
/// <returns>匹配的任务实体;如果不存在则返回 null。</returns>
|
/// <returns>匹配的任务实体;如果不存在则返回 null。</returns>
|
||||||
public async Task<TaskEntity?> GetByIdAsync(int id)
|
public async Task<TaskEntity?> GetByIdAsync(Guid id)
|
||||||
{
|
{
|
||||||
return await _context.Tasks
|
return await _context.Tasks
|
||||||
.Include(t => t.SubTasks)
|
.Include(t => t.SubTasks!)
|
||||||
.FirstOrDefaultAsync(t => t.Id == id);
|
.FirstOrDefaultAsync(t => t.Id == id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +51,7 @@ public class TaskRepository : ITaskRepository
|
|||||||
{
|
{
|
||||||
return await _context.Tasks
|
return await _context.Tasks
|
||||||
.Where(t => !t.IsCompleted)
|
.Where(t => !t.IsCompleted)
|
||||||
.OrderByDescending(t => t.CreatedAt)
|
.OrderByDescending(t => t.CreationTime)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +63,7 @@ public class TaskRepository : ITaskRepository
|
|||||||
{
|
{
|
||||||
return await _context.Tasks
|
return await _context.Tasks
|
||||||
.Where(t => t.IsCompleted)
|
.Where(t => t.IsCompleted)
|
||||||
.OrderByDescending(t => t.UpdatedAt)
|
.OrderByDescending(t => t.LastModificationTime)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +86,7 @@ public class TaskRepository : ITaskRepository
|
|||||||
/// <returns>更新后的任务实体。</returns>
|
/// <returns>更新后的任务实体。</returns>
|
||||||
public async Task<TaskEntity> UpdateAsync(TaskEntity taskEntity)
|
public async Task<TaskEntity> UpdateAsync(TaskEntity taskEntity)
|
||||||
{
|
{
|
||||||
taskEntity.UpdatedAt = DateTime.UtcNow;
|
taskEntity.LastModificationTime = DateTime.UtcNow;
|
||||||
_context.Tasks.Update(taskEntity);
|
_context.Tasks.Update(taskEntity);
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
return taskEntity;
|
return taskEntity;
|
||||||
@@ -98,7 +97,7 @@ public class TaskRepository : ITaskRepository
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id">要删除的任务 ID。</param>
|
/// <param name="id">要删除的任务 ID。</param>
|
||||||
/// <returns>表示删除操作的任务。</returns>
|
/// <returns>表示删除操作的任务。</returns>
|
||||||
public async Task DeleteAsync(int id)
|
public async Task DeleteAsync(Guid id)
|
||||||
{
|
{
|
||||||
var task = await _context.Tasks.FindAsync(id);
|
var task = await _context.Tasks.FindAsync(id);
|
||||||
if (task != null)
|
if (task != null)
|
||||||
@@ -113,11 +112,35 @@ public class TaskRepository : ITaskRepository
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="parentTaskId">父任务 ID。</param>
|
/// <param name="parentTaskId">父任务 ID。</param>
|
||||||
/// <returns>子任务实体的列表。</returns>
|
/// <returns>子任务实体的列表。</returns>
|
||||||
public async Task<List<TaskEntity>> GetSubTasksAsync(int parentTaskId)
|
public async Task<List<TaskEntity>> GetSubTasksAsync(Guid parentTaskId)
|
||||||
{
|
{
|
||||||
return await _context.Tasks
|
return await _context.Tasks
|
||||||
.Where(t => t.ParentTaskId == parentTaskId)
|
.Where(t => t.ParentTaskId == parentTaskId)
|
||||||
.OrderByDescending(t => t.CreatedAt)
|
.OrderByDescending(t => t.CreationTime)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取指定用户的任务中最大的数字型 Code。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">用户 ID。</param>
|
||||||
|
/// <returns>最大数字 Code;若无任务或无可解析 Code 则返回 0。</returns>
|
||||||
|
public async Task<int> GetMaxCodeAsync(Guid userId)
|
||||||
|
{
|
||||||
|
var codes = await _context.Tasks
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(t => t.UserId == userId)
|
||||||
|
.Select(t => t.Code)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
int max = 0;
|
||||||
|
foreach (var code in codes)
|
||||||
|
{
|
||||||
|
if (int.TryParse(code, out var value) && value > max)
|
||||||
|
{
|
||||||
|
max = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return max;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,187 @@
|
|||||||
|
using Hua.Todo.Core.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Hua.Todo.Application.Repositories;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 应用程序数据库上下文(EF Core)。
|
||||||
|
/// </summary>
|
||||||
|
public class TodoDbContext : DbContext
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 创建 <see cref="TodoDbContext"/>。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="options">数据库上下文配置。</param>
|
||||||
|
public TodoDbContext(DbContextOptions<TodoDbContext> options) : base(options)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 配置 DbContext 行为,抑制开发期间模型变化导致的迁移验证警告。
|
||||||
|
/// </summary>
|
||||||
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
|
{
|
||||||
|
optionsBuilder.ConfigureWarnings(w => w.Ignore(
|
||||||
|
Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 任务集合。
|
||||||
|
/// </summary>
|
||||||
|
public DbSet<TaskEntity> Tasks { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 附件集合。
|
||||||
|
/// </summary>
|
||||||
|
public DbSet<AttachmentEntity> Attachments { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户集合(云同步)。
|
||||||
|
/// </summary>
|
||||||
|
public DbSet<UserEntity> Users { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户会话集合(云同步)。
|
||||||
|
/// </summary>
|
||||||
|
public DbSet<UserSessionEntity> UserSessions { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 安全策略集合(云同步)。
|
||||||
|
/// </summary>
|
||||||
|
public DbSet<SecurityPolicyEntity> SecurityPolicies { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 审计日志集合(云同步)。
|
||||||
|
/// </summary>
|
||||||
|
public DbSet<AuditLogEntity> AuditLogs { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 配置实体模型映射。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="modelBuilder">模型构建器。</param>
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
base.OnModelCreating(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity<TaskEntity>(entity =>
|
||||||
|
{
|
||||||
|
entity.ToTable("T_Tasks");
|
||||||
|
entity.HasKey(e => e.Id);
|
||||||
|
entity.Property(e => e.Id).ValueGeneratedOnAdd();
|
||||||
|
entity.Property(e => e.Title).IsRequired().HasMaxLength(200);
|
||||||
|
entity.Property(e => e.Priority).HasDefaultValue(TaskPriority.Medium).HasSentinel(TaskPriority.Low);
|
||||||
|
entity.Property(e => e.IsCompleted).HasDefaultValue(false);
|
||||||
|
|
||||||
|
// ABP 审计字段
|
||||||
|
entity.Property(e => e.CreationTime).HasDefaultValueSql("datetime('now')");
|
||||||
|
entity.Property(e => e.LastModificationTime).IsRequired(false);
|
||||||
|
entity.Property(e => e.CreatorId).IsRequired(false);
|
||||||
|
entity.Property(e => e.LastModifierId).IsRequired(false);
|
||||||
|
entity.Property(e => e.IsDeleted).HasDefaultValue(false);
|
||||||
|
entity.Property(e => e.DeletionTime).IsRequired(false);
|
||||||
|
entity.Property(e => e.DeleterId).IsRequired(false);
|
||||||
|
entity.Property(e => e.ConcurrencyStamp).IsRequired(false);
|
||||||
|
|
||||||
|
// 用户隔离字段
|
||||||
|
entity.Property(e => e.UserId).IsRequired();
|
||||||
|
entity.Property(e => e.Code).HasMaxLength(32);
|
||||||
|
|
||||||
|
entity.Property(e => e.TaskType)
|
||||||
|
.HasDefaultValue(TaskType.Normal)
|
||||||
|
.HasConversion<int>();
|
||||||
|
|
||||||
|
entity.Property(e => e.MeetingNotes)
|
||||||
|
.HasMaxLength(20000);
|
||||||
|
|
||||||
|
entity.Property(e => e.AudioDuration)
|
||||||
|
.IsRequired(false);
|
||||||
|
|
||||||
|
entity.Property(e => e.Description)
|
||||||
|
.HasMaxLength(5000);
|
||||||
|
|
||||||
|
entity.HasIndex(e => e.UserId);
|
||||||
|
|
||||||
|
entity.HasOne(e => e.ParentTask)
|
||||||
|
.WithMany(e => e.SubTasks)
|
||||||
|
.HasForeignKey(e => e.ParentTaskId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
entity.HasOne(e => e.User)
|
||||||
|
.WithMany(e => e.Tasks)
|
||||||
|
.HasForeignKey(e => e.UserId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<AttachmentEntity>(entity =>
|
||||||
|
{
|
||||||
|
entity.ToTable("Attachments");
|
||||||
|
entity.HasKey(e => e.Id);
|
||||||
|
entity.Property(e => e.Id).ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
entity.Property(e => e.FileName)
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256);
|
||||||
|
|
||||||
|
entity.Property(e => e.FilePath)
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024);
|
||||||
|
|
||||||
|
entity.Property(e => e.ContentType)
|
||||||
|
.HasMaxLength(128);
|
||||||
|
|
||||||
|
entity.Property(e => e.AttachmentType)
|
||||||
|
.HasConversion<int>()
|
||||||
|
.HasDefaultValue(AttachmentType.LocalFile);
|
||||||
|
|
||||||
|
entity.Property(e => e.CreatedAt)
|
||||||
|
.HasDefaultValueSql("datetime('now')");
|
||||||
|
|
||||||
|
entity.HasOne(e => e.Task)
|
||||||
|
.WithMany(e => e.Attachments)
|
||||||
|
.HasForeignKey(e => e.TaskId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<UserEntity>(entity =>
|
||||||
|
{
|
||||||
|
entity.ToTable("Users");
|
||||||
|
entity.HasKey(e => e.Id);
|
||||||
|
entity.HasIndex(e => e.UserName).IsUnique();
|
||||||
|
entity.Property(e => e.UserName).IsRequired().HasMaxLength(64);
|
||||||
|
entity.Property(e => e.PasswordHash).IsRequired();
|
||||||
|
entity.Property(e => e.PasswordSalt).IsRequired();
|
||||||
|
entity.Property(e => e.Role).HasMaxLength(32);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<UserSessionEntity>(entity =>
|
||||||
|
{
|
||||||
|
entity.ToTable("UserSessions");
|
||||||
|
entity.HasKey(e => e.Id);
|
||||||
|
entity.HasIndex(e => e.UserId);
|
||||||
|
entity.HasOne(e => e.User).WithMany().HasForeignKey(e => e.UserId).OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<SecurityPolicyEntity>(entity =>
|
||||||
|
{
|
||||||
|
entity.ToTable("SecurityPolicies");
|
||||||
|
entity.HasKey(e => e.Id);
|
||||||
|
entity.HasIndex(e => e.UserId).IsUnique();
|
||||||
|
entity.Property(e => e.AllowPersist).HasDefaultValue(true);
|
||||||
|
entity.Property(e => e.AllowSync).HasDefaultValue(true);
|
||||||
|
entity.Property(e => e.SecondFactorExpiryMinutes).HasDefaultValue(30);
|
||||||
|
entity.HasOne(e => e.User).WithMany().HasForeignKey(e => e.UserId).OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<AuditLogEntity>(entity =>
|
||||||
|
{
|
||||||
|
entity.ToTable("AuditLogs");
|
||||||
|
entity.HasKey(e => e.Id);
|
||||||
|
entity.HasIndex(e => e.TimestampUtc);
|
||||||
|
entity.HasIndex(e => e.UserId);
|
||||||
|
entity.Property(e => e.EventType).HasMaxLength(64);
|
||||||
|
entity.Property(e => e.Description).HasMaxLength(500);
|
||||||
|
entity.Property(e => e.ClientIp).HasMaxLength(64);
|
||||||
|
entity.Property(e => e.UserAgent).HasMaxLength(500);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,274 @@
|
|||||||
|
using Hua.Todo.Application.Models;
|
||||||
|
using Hua.Todo.Core.Entities;
|
||||||
|
using Hua.Todo.Core.Interfaces;
|
||||||
|
using Hua.Todo.Core.Services;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Hua.Todo.Application.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 附件管理服务实现。
|
||||||
|
/// </summary>
|
||||||
|
public class AttachmentService : IAttachmentService
|
||||||
|
{
|
||||||
|
private readonly IAttachmentRepository _attachmentRepository;
|
||||||
|
private readonly ITaskRepository _taskRepository;
|
||||||
|
private readonly IPlatformAttachmentOpener? _platformOpener;
|
||||||
|
private readonly ILogger<AttachmentService> _logger;
|
||||||
|
|
||||||
|
private const long MaxFileSize = 50 * 1024 * 1024; // 50MB
|
||||||
|
private const int MaxAttachmentsPerTask = 20;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取附件存储根目录(与数据库同级)。
|
||||||
|
/// </summary>
|
||||||
|
private static string AttachmentsRootPath =>
|
||||||
|
Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||||
|
"Hua.Todo",
|
||||||
|
"Attachments");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化附件服务。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="attachmentRepository">附件仓储。</param>
|
||||||
|
/// <param name="taskRepository">任务仓储。</param>
|
||||||
|
/// <param name="platformOpener">平台文件打开服务(可选,MAUI/Avalonia 注入)。</param>
|
||||||
|
/// <param name="logger">日志记录器;未注入时使用空日志实现。</param>
|
||||||
|
public AttachmentService(
|
||||||
|
IAttachmentRepository attachmentRepository,
|
||||||
|
ITaskRepository taskRepository,
|
||||||
|
IPlatformAttachmentOpener? platformOpener = null,
|
||||||
|
ILogger<AttachmentService>? logger = null)
|
||||||
|
{
|
||||||
|
_attachmentRepository = attachmentRepository;
|
||||||
|
_taskRepository = taskRepository;
|
||||||
|
_platformOpener = platformOpener;
|
||||||
|
_logger = logger ?? Microsoft.Extensions.Logging.Abstractions.NullLogger<AttachmentService>.Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<AttachmentDto> UploadAsync(UploadAttachmentRequest request)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(request.FileName))
|
||||||
|
throw new ArgumentException("文件名不能为空");
|
||||||
|
if (string.IsNullOrEmpty(request.Base64Content))
|
||||||
|
throw new ArgumentException("文件内容不能为空");
|
||||||
|
|
||||||
|
// 验证待办项存在
|
||||||
|
var task = await _taskRepository.GetByIdAsync(request.TaskId);
|
||||||
|
if (task == null)
|
||||||
|
throw new KeyNotFoundException($"待办项 {request.TaskId} 不存在");
|
||||||
|
|
||||||
|
// 检查附件数量限制
|
||||||
|
var currentCount = await _attachmentRepository.GetCountByTaskIdAsync(request.TaskId);
|
||||||
|
if (currentCount >= MaxAttachmentsPerTask)
|
||||||
|
throw new InvalidOperationException($"每个待办项最多 {MaxAttachmentsPerTask} 个附件");
|
||||||
|
|
||||||
|
// 解码 Base64
|
||||||
|
byte[] fileBytes;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
fileBytes = Convert.FromBase64String(request.Base64Content);
|
||||||
|
}
|
||||||
|
catch (FormatException)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("文件内容 Base64 编码无效");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileBytes.Length == 0)
|
||||||
|
throw new ArgumentException("文件内容为空");
|
||||||
|
|
||||||
|
if (fileBytes.Length > MaxFileSize)
|
||||||
|
throw new ArgumentException($"文件大小不能超过 {MaxFileSize / (1024 * 1024)}MB");
|
||||||
|
|
||||||
|
// 清理文件名中的危险路径片段
|
||||||
|
var safeFileName = SanitizeFileName(request.FileName);
|
||||||
|
|
||||||
|
// 先保存实体获取 ID
|
||||||
|
var attachment = new AttachmentEntity
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
TaskId = request.TaskId,
|
||||||
|
FileName = safeFileName,
|
||||||
|
FilePath = string.Empty, // 待定,拿到 ID 后填充
|
||||||
|
FileSize = fileBytes.Length,
|
||||||
|
ContentType = string.IsNullOrEmpty(request.ContentType) ? "application/octet-stream" : request.ContentType,
|
||||||
|
AttachmentType = AttachmentType.LocalFile,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
var created = await _attachmentRepository.AddAsync(attachment);
|
||||||
|
|
||||||
|
// 用 ID 构造文件路径
|
||||||
|
var storageDir = AttachmentsRootPath;
|
||||||
|
Directory.CreateDirectory(storageDir);
|
||||||
|
var storagePath = Path.Combine(storageDir, $"{created.Id}_{safeFileName}");
|
||||||
|
await File.WriteAllBytesAsync(storagePath, fileBytes);
|
||||||
|
|
||||||
|
// 更新 FilePath
|
||||||
|
created.FilePath = storagePath;
|
||||||
|
await _attachmentRepository.AddAsync(created); // 再保存一次以更新路径字段
|
||||||
|
|
||||||
|
return MapToDto(created);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<AttachmentDto> AddLinkAsync(AddLinkRequest request)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(request.Url))
|
||||||
|
throw new ArgumentException("URL 不能为空");
|
||||||
|
|
||||||
|
var uri = new Uri(request.Url);
|
||||||
|
if (uri.Scheme != "http" && uri.Scheme != "https")
|
||||||
|
throw new ArgumentException("仅支持 http:// 或 https:// 协议的链接");
|
||||||
|
|
||||||
|
// 验证待办项存在
|
||||||
|
var task = await _taskRepository.GetByIdAsync(request.TaskId);
|
||||||
|
if (task == null)
|
||||||
|
throw new KeyNotFoundException($"待办项 {request.TaskId} 不存在");
|
||||||
|
|
||||||
|
// 检查附件数量限制
|
||||||
|
var currentCount = await _attachmentRepository.GetCountByTaskIdAsync(request.TaskId);
|
||||||
|
if (currentCount >= MaxAttachmentsPerTask)
|
||||||
|
throw new InvalidOperationException($"每个待办项最多 {MaxAttachmentsPerTask} 个附件");
|
||||||
|
|
||||||
|
var attachment = new AttachmentEntity
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
TaskId = request.TaskId,
|
||||||
|
FileName = string.IsNullOrEmpty(request.FileName) ? request.Url : request.FileName,
|
||||||
|
FilePath = request.Url,
|
||||||
|
FileSize = 0,
|
||||||
|
ContentType = string.Empty,
|
||||||
|
AttachmentType = AttachmentType.ExternalLink,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
var created = await _attachmentRepository.AddAsync(attachment);
|
||||||
|
return MapToDto(created);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<List<AttachmentDto>> GetAttachmentsAsync(Guid taskId)
|
||||||
|
{
|
||||||
|
var attachments = await _attachmentRepository.GetByTaskIdAsync(taskId);
|
||||||
|
return attachments.Select(MapToDto).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task DeleteAsync(Guid id)
|
||||||
|
{
|
||||||
|
var attachment = await _attachmentRepository.GetByIdAsync(id);
|
||||||
|
if (attachment == null)
|
||||||
|
throw new KeyNotFoundException($"附件 {id} 不存在");
|
||||||
|
|
||||||
|
// 删除磁盘文件(仅 LocalFile 类型)
|
||||||
|
if (attachment.AttachmentType == AttachmentType.LocalFile && !string.IsNullOrEmpty(attachment.FilePath))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(attachment.FilePath))
|
||||||
|
File.Delete(attachment.FilePath);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "删除附件文件失败: {FilePath}", attachment.FilePath);
|
||||||
|
// 文件删除失败不阻断数据库删除,仅记录日志
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await _attachmentRepository.DeleteAsync(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<OpenAttachmentResponse> OpenAsync(Guid id)
|
||||||
|
{
|
||||||
|
var attachment = await _attachmentRepository.GetByIdAsync(id);
|
||||||
|
if (attachment == null)
|
||||||
|
throw new KeyNotFoundException($"附件 {id} 不存在");
|
||||||
|
|
||||||
|
string pathToOpen;
|
||||||
|
if (attachment.AttachmentType == AttachmentType.ExternalLink)
|
||||||
|
{
|
||||||
|
// 外部链接:验证 URL 有效性后打开
|
||||||
|
var uri = new Uri(attachment.FilePath);
|
||||||
|
if (uri.Scheme != "http" && uri.Scheme != "https")
|
||||||
|
throw new InvalidOperationException("外部链接仅支持 http:// 或 https:// 协议");
|
||||||
|
pathToOpen = attachment.FilePath;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(attachment.FilePath) || !File.Exists(attachment.FilePath))
|
||||||
|
throw new FileNotFoundException($"附件文件不存在: {attachment.FileName}");
|
||||||
|
pathToOpen = attachment.FilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_platformOpener != null)
|
||||||
|
{
|
||||||
|
await _platformOpener.OpenAsync(pathToOpen);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 回退:直接使用 Process.Start
|
||||||
|
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = pathToOpen,
|
||||||
|
UseShellExecute = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new OpenAttachmentResponse { Opened = true };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 清理文件名中的危险路径片段,防止路径遍历攻击。
|
||||||
|
/// </summary>
|
||||||
|
private static string SanitizeFileName(string fileName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(fileName))
|
||||||
|
return "unnamed";
|
||||||
|
|
||||||
|
// 去除路径分隔符和危险字符
|
||||||
|
var sanitized = fileName
|
||||||
|
.Replace("..\\", "")
|
||||||
|
.Replace("../", "")
|
||||||
|
.Replace("..", "")
|
||||||
|
.Replace("/", "_")
|
||||||
|
.Replace("\\", "_")
|
||||||
|
.Replace(":", "_")
|
||||||
|
.Replace("*", "_")
|
||||||
|
.Replace("?", "_")
|
||||||
|
.Replace("\"", "_")
|
||||||
|
.Replace("<", "_")
|
||||||
|
.Replace(">", "_")
|
||||||
|
.Replace("|", "_")
|
||||||
|
.Replace("\0", "");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(sanitized))
|
||||||
|
return "unnamed";
|
||||||
|
|
||||||
|
if (sanitized.Length > 240) // 留空间给 ID 前缀
|
||||||
|
sanitized = sanitized[..240];
|
||||||
|
|
||||||
|
return sanitized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 实体转 DTO 映射。
|
||||||
|
/// </summary>
|
||||||
|
private static AttachmentDto MapToDto(AttachmentEntity attachment)
|
||||||
|
{
|
||||||
|
return new AttachmentDto
|
||||||
|
{
|
||||||
|
Id = attachment.Id,
|
||||||
|
TaskId = attachment.TaskId,
|
||||||
|
FileName = attachment.FileName,
|
||||||
|
FilePath = attachment.AttachmentType == AttachmentType.LocalFile ? string.Empty : attachment.FilePath,
|
||||||
|
FileSize = attachment.FileSize,
|
||||||
|
ContentType = attachment.ContentType,
|
||||||
|
AttachmentType = attachment.AttachmentType,
|
||||||
|
CreatedAt = attachment.CreatedAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
|
|
||||||
namespace Hua.Todo.Application.CloudSync.Auth;
|
namespace Hua.Todo.Application.Services.CloudSync.Auth;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 云同步鉴权相关的 <see cref="ClaimsPrincipal"/> 扩展方法。
|
/// 云同步鉴权相关的 <see cref="ClaimsPrincipal"/> 扩展方法。
|
||||||
@@ -14,7 +14,7 @@ public static class ClaimsPrincipalExtensions
|
|||||||
/// <returns>用户 ID。</returns>
|
/// <returns>用户 ID。</returns>
|
||||||
public static Guid? GetUserId(this ClaimsPrincipal user)
|
public static Guid? GetUserId(this ClaimsPrincipal user)
|
||||||
{
|
{
|
||||||
var value = user.FindFirstValue(ClaimTypes.NameIdentifier);
|
var value = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||||
return Guid.TryParse(value, out var id) ? id : null;
|
return Guid.TryParse(value, out var id) ? id : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ public static class ClaimsPrincipalExtensions
|
|||||||
/// <returns>会话 ID。</returns>
|
/// <returns>会话 ID。</returns>
|
||||||
public static Guid? GetSessionId(this ClaimsPrincipal user)
|
public static Guid? GetSessionId(this ClaimsPrincipal user)
|
||||||
{
|
{
|
||||||
var value = user.FindFirstValue(ClaimTypes.Sid);
|
var value = user.FindFirst(ClaimTypes.Sid)?.Value;
|
||||||
return Guid.TryParse(value, out var id) ? id : null;
|
return Guid.TryParse(value, out var id) ? id : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace Hua.Todo.Application.CloudSync.Auth;
|
namespace Hua.Todo.Application.Services.CloudSync.Auth;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 云同步鉴权使用的 Claim 名称约定。
|
/// 云同步鉴权使用的 Claim 名称约定。
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace Hua.Todo.Application.CloudSync.Auth;
|
namespace Hua.Todo.Application.Services.CloudSync.Auth;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 云同步权限点常量。
|
/// 云同步权限点常量。
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace Hua.Todo.Application.CloudSync.Auth;
|
namespace Hua.Todo.Application.Services.CloudSync.Auth;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 默认的角色-权限映射(内置最小集合)。
|
/// 默认的角色-权限映射(内置最小集合)。
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace Hua.Todo.Application.CloudSync.Auth;
|
namespace Hua.Todo.Application.Services.CloudSync.Auth;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 角色到权限集合的映射器(RBAC 最小落地)。
|
/// 角色到权限集合的映射器(RBAC 最小落地)。
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace Hua.Todo.Application.CloudSync.Auth;
|
namespace Hua.Todo.Application.Services.CloudSync.Auth;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 云同步会话鉴权默认配置。
|
/// 云同步会话鉴权默认配置。
|
||||||
+9
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace Hua.Todo.Application.CloudSync.Models;
|
namespace Hua.Todo.Application.Services.CloudSync.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 用户信息 DTO(管理端使用)。
|
/// 用户信息 DTO(管理端使用)。
|
||||||
@@ -27,6 +27,14 @@ public class CreateUserRequest
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class ResetPasswordRequest
|
public class ResetPasswordRequest
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 目标用户 ID。
|
||||||
|
/// </summary>
|
||||||
|
public Guid UserId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新密码。
|
||||||
|
/// </summary>
|
||||||
public string NewPassword { get; set; } = string.Empty;
|
public string NewPassword { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace Hua.Todo.Application.CloudSync.Models;
|
namespace Hua.Todo.Application.Services.CloudSync.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 云同步 API 统一错误响应结构。
|
/// 云同步 API 统一错误响应结构。
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace Hua.Todo.Application.CloudSync.Models;
|
namespace Hua.Todo.Application.Services.CloudSync.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 登录请求。
|
/// 登录请求。
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace Hua.Todo.Application.CloudSync.Models;
|
namespace Hua.Todo.Application.Services.CloudSync.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 服务端探测请求。
|
/// 服务端探测请求。
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace Hua.Todo.Application.CloudSync.Models;
|
namespace Hua.Todo.Application.Services.CloudSync.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 安全策略下发 DTO。
|
/// 安全策略下发 DTO。
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Hua.Todo.Core.Entities;
|
using Hua.Todo.Core.Entities;
|
||||||
|
|
||||||
namespace Hua.Todo.Application.CloudSync.Models;
|
namespace Hua.Todo.Application.Services.CloudSync.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 云同步任务条目(ABP 标准)。
|
/// 云同步任务条目(ABP 标准)。
|
||||||
+33
-9
@@ -1,15 +1,15 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Hua.Todo.Application.Repositories;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Hua.Todo.Application.Services.CloudSync.Models;
|
||||||
using Hua.Todo.Application.CloudSync.Models;
|
|
||||||
using Hua.Todo.Application.Data;
|
|
||||||
using Hua.Todo.Core.Entities;
|
using Hua.Todo.Core.Entities;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace Hua.Todo.Application.CloudSync.Services;
|
namespace Hua.Todo.Application.Services.CloudSync.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 管理端服务(账户、会话、审计、全局策略管理)。
|
/// 管理端服务(账户、会话、审计、全局策略管理)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CloudAdminService
|
public class CloudAdminService : ICloudAdminService
|
||||||
{
|
{
|
||||||
private readonly TodoDbContext _dbContext;
|
private readonly TodoDbContext _dbContext;
|
||||||
private readonly IPasswordHasher<UserEntity> _passwordHasher;
|
private readonly IPasswordHasher<UserEntity> _passwordHasher;
|
||||||
@@ -65,7 +65,18 @@ public class CloudAdminService
|
|||||||
return new UserDto { Id = user.Id, UserName = user.UserName, Role = user.Role, CreatedAtUtc = user.CreatedAtUtc, UpdatedAtUtc = user.UpdatedAtUtc };
|
return new UserDto { Id = user.Id, UserName = user.UserName, Role = user.Role, CreatedAtUtc = user.CreatedAtUtc, UpdatedAtUtc = user.UpdatedAtUtc };
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> ResetPasswordAsync(Guid userId, string newPassword, CancellationToken cancellationToken)
|
public async Task<bool> ResetPasswordAsync(ResetPasswordRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (request == null || string.IsNullOrWhiteSpace(request.NewPassword))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return await ResetPasswordAsync(request.UserId, request.NewPassword, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 重置用户密码(内部方法)。
|
||||||
|
/// </summary>
|
||||||
|
private async Task<bool> ResetPasswordAsync(Guid userId, string newPassword, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var user = await _dbContext.Users.FirstOrDefaultAsync(u => u.Id == userId, cancellationToken);
|
var user = await _dbContext.Users.FirstOrDefaultAsync(u => u.Id == userId, cancellationToken);
|
||||||
if (user == null) return false;
|
if (user == null) return false;
|
||||||
@@ -122,12 +133,13 @@ public class CloudAdminService
|
|||||||
|
|
||||||
#region Audit Logs
|
#region Audit Logs
|
||||||
|
|
||||||
public async Task<List<AuditLogDto>> GetAuditLogsAsync(int count, CancellationToken cancellationToken)
|
public async Task<List<AuditLogDto>> GetAuditLogsAsync(int? count = 100, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
var take = count ?? 100;
|
||||||
return await _dbContext.AuditLogs
|
return await _dbContext.AuditLogs
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.OrderByDescending(l => l.TimestampUtc)
|
.OrderByDescending(l => l.TimestampUtc)
|
||||||
.Take(count)
|
.Take(take)
|
||||||
.Select(l => new AuditLogDto
|
.Select(l => new AuditLogDto
|
||||||
{
|
{
|
||||||
Id = l.Id,
|
Id = l.Id,
|
||||||
@@ -144,4 +156,16 @@ public class CloudAdminService
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region ICloudAdminService 接口方法(无 CancellationToken,供 DynamicApi 调用)
|
||||||
|
|
||||||
|
Task<List<UserDto>> ICloudAdminService.GetUsersAsync() => GetUsersAsync(CancellationToken.None);
|
||||||
|
Task<UserDto?> ICloudAdminService.CreateUserAsync(CreateUserRequest request) => CreateUserAsync(request, CancellationToken.None);
|
||||||
|
Task<bool> ICloudAdminService.ResetPasswordAsync(ResetPasswordRequest request) => ResetPasswordAsync(request, CancellationToken.None);
|
||||||
|
Task<bool> ICloudAdminService.DeleteUserAsync(Guid userId) => DeleteUserAsync(userId, CancellationToken.None);
|
||||||
|
Task<List<SessionDto>> ICloudAdminService.GetSessionsAsync() => GetSessionsAsync(CancellationToken.None);
|
||||||
|
Task<bool> ICloudAdminService.RevokeSessionAsync(Guid sessionId) => RevokeSessionAsync(sessionId, CancellationToken.None);
|
||||||
|
Task<List<AuditLogDto>> ICloudAdminService.GetAuditLogsAsync(int? count) => GetAuditLogsAsync(count, CancellationToken.None);
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user