feat: 后端架构重构 — Endpoint→Service→Repository分层 + AI确认机制 + 异步任务持久化
- 核心业务拆分为 Endpoint → Application Service → Repository 三层 - AI写入操作必须用户确认后才写库(确认卡片机制) - 报告/饮食/用药分析改为持久化任务队列(原子领取/重试/重启恢复) - 运动计划修复: 连续真实日期替代周模板 - 用药提醒去重 + 通知Outbox预留 - 认证收拢到AuthService, 管理员收拢到AdminService - AI会话加用户归属校验防串号 - 提示词调整为患者视角 - 开发假数据已关闭 - 21/21测试通过, 0警告0错误
This commit is contained in:
283
docs/backend_architecture_evolution.md
Normal file
283
docs/backend_architecture_evolution.md
Normal file
@@ -0,0 +1,283 @@
|
||||
# 后端架构演进方案
|
||||
|
||||
日期:2026-06-18
|
||||
|
||||
## 1. 背景
|
||||
|
||||
当前项目已经具备 `Health.Domain`、`Health.Application`、`Health.Infrastructure`、`Health.WebApi` 的分层目录,但实际业务逻辑主要仍写在 `Health.WebApi/Endpoints` 中。多数接口直接注入 `AppDbContext`,在 Endpoint 内完成查询、权限判断、状态流转和 `SaveChanges`。
|
||||
|
||||
这种方式适合早期快速验证,但随着患者端健康管理、AI 分析、报告、饮食、用药、运动、医生端等业务增长,会带来几个问题:
|
||||
|
||||
- 业务规则分散在 Endpoint、AI Agent Handler、后台服务中。
|
||||
- 同一个业务动作可能被普通接口和 AI 工具重复实现。
|
||||
- 权限和资源归属校验容易遗漏。
|
||||
- 异步任务目前存在 `Task.Run` 形式,不便于限流、重试和统一管理。
|
||||
- Application 层没有真正承接业务用例,后续测试和维护成本会升高。
|
||||
|
||||
技术目标是按 DDD 思路逐步演进:让 Endpoint 成为接口适配层,业务流程进入 Application Service,外部能力和数据访问由 Infrastructure 支撑,耗时任务通过生产者-消费者管道处理。
|
||||
|
||||
## 2. 当前业务边界
|
||||
|
||||
### 2.1 当前核心患者端闭环
|
||||
|
||||
以下业务都需要继续做扎实:
|
||||
|
||||
1. AI 健康管家聊天
|
||||
2. 健康指标记录与趋势:血压、心率、血糖、血氧、体重
|
||||
3. 报告上传与 AI 预解读
|
||||
4. 饮食拍照分析与保存
|
||||
5. 用药管理与打卡
|
||||
6. 运动计划
|
||||
|
||||
### 2.2 医生端当前策略
|
||||
|
||||
医生端可以做代码整理和权限边界收拢,但不作为当前核心业务闭环:
|
||||
|
||||
- 医患实时聊天:暂时搁置,后续接入互联网医院后再完善。
|
||||
- 医生审核报告:暂时不是当前真实流程,患者端报告以 AI 预解读为主;界面可显示“医生审核中/待审核”一类状态。
|
||||
- 医生工作台:保留现有页面和基础接口,不主动扩大功能范围,重构时以不影响当前项目运行为目标。
|
||||
|
||||
### 2.3 AI 写入规则
|
||||
|
||||
统一业务规则:
|
||||
|
||||
- AI 纯查询、解释、建议可以直接回复。
|
||||
- AI 只要要写入用户健康相关数据,必须先让用户确认。
|
||||
- 需要确认的写入包括但不限于:健康指标、用药计划、运动计划、健康档案修改。
|
||||
- 饮食记录当前在饮食分析结果页由用户主动保存,保留该方式。
|
||||
|
||||
## 3. 目标架构
|
||||
|
||||
目标不是创建一个巨大的全能 Service,而是按业务模块拆分 Application Service:
|
||||
|
||||
```text
|
||||
Health.WebApi
|
||||
Endpoints
|
||||
Hubs
|
||||
BackgroundServices
|
||||
|
||||
Health.Application
|
||||
Auth
|
||||
Users
|
||||
HealthRecords
|
||||
Medications
|
||||
Diet
|
||||
Reports
|
||||
Exercise
|
||||
Consultations
|
||||
Doctors
|
||||
Ai
|
||||
Common
|
||||
|
||||
Health.Domain
|
||||
Entities
|
||||
Enums
|
||||
DomainRules
|
||||
|
||||
Health.Infrastructure
|
||||
Data
|
||||
AI
|
||||
Services
|
||||
Storage
|
||||
```
|
||||
|
||||
职责边界:
|
||||
|
||||
- `WebApi`:接收 HTTP/SignalR 请求,解析参数,返回响应。
|
||||
- `Application`:承接业务用例、状态流转、权限校验、任务入队。
|
||||
- `Domain`:保存核心实体、枚举和稳定业务规则。
|
||||
- `Infrastructure`:EF Core、AI 客户端、短信、文件存储、推送、未来互联网医院适配。
|
||||
|
||||
## 4. 数据库读写收拢方式
|
||||
|
||||
短期先不强制引入 Repository 抽象,避免过度设计。第一阶段采用:
|
||||
|
||||
```text
|
||||
Endpoint -> Application Service -> AppDbContext
|
||||
```
|
||||
|
||||
也就是说,数据库读写先统一进入对应业务 Service,而不是继续散落在 Endpoint 中。
|
||||
|
||||
后续如果业务复杂度继续提升,再考虑:
|
||||
|
||||
```text
|
||||
Application Service -> Repository / UnitOfWork -> AppDbContext
|
||||
```
|
||||
|
||||
第一阶段优先收拢这些模块:
|
||||
|
||||
1. `ReportService`
|
||||
2. `DietService`
|
||||
3. `MedicationService`
|
||||
4. `ExerciseService`
|
||||
5. `HealthRecordService`
|
||||
6. `ConsultationService`
|
||||
|
||||
## 5. 生产者-消费者管道
|
||||
|
||||
不是所有业务都需要管道。同步、快速、必须立即返回的操作仍走普通 Service。
|
||||
|
||||
适合管道的任务:
|
||||
|
||||
- 报告 AI 分析
|
||||
- 饮食图片识别
|
||||
- 处方图片识别
|
||||
- 用药提醒推送
|
||||
- 健康周报生成
|
||||
- 后期互联网医院数据同步
|
||||
|
||||
当前阶段采用 .NET 内置 `Channel<T>` + `BackgroundService`:
|
||||
|
||||
```text
|
||||
业务 Service = 生产者
|
||||
Channel<TJob> = 内存任务队列
|
||||
BackgroundService = 消费者
|
||||
AI/推送/外部服务 = 实际执行器
|
||||
```
|
||||
|
||||
单服务器和当前用户规模下,内存队列足够作为第一阶段方案。后续如果出现多实例部署、任务不能丢、重试次数和死信队列等要求,再迁移到 Redis Stream、RabbitMQ 或 Hangfire。
|
||||
|
||||
## 6. 第一阶段落地范围
|
||||
|
||||
先从报告模块落地,因为它同时具备上传、AI、异步、状态流转、失败重试等典型场景。
|
||||
|
||||
### 6.1 报告模块目标
|
||||
|
||||
从当前:
|
||||
|
||||
```text
|
||||
ReportEndpoints -> AppDbContext + Task.Run + AI 调用
|
||||
```
|
||||
|
||||
演进为:
|
||||
|
||||
```text
|
||||
ReportEndpoints
|
||||
-> ReportService
|
||||
-> 保存文件
|
||||
-> 创建报告
|
||||
-> 标记状态
|
||||
-> 入队 ReportAnalysisJob
|
||||
|
||||
ReportAnalysisWorker
|
||||
-> 消费 ReportAnalysisQueue
|
||||
-> 调用 ReportAnalysisService / AI Analyzer
|
||||
-> 更新报告状态
|
||||
```
|
||||
|
||||
### 6.2 报告模块要保持的业务行为
|
||||
|
||||
- 上传只支持报告图片。
|
||||
- 上传成功后状态为 `Analyzing`。
|
||||
- AI 成功后进入 `PendingDoctor`,患者端展示 AI 预解读。
|
||||
- AI 失败后进入 `AnalysisFailed`,不生成假摘要、假指标。
|
||||
- 患者端可以查看原始报告图片。
|
||||
- 医生审核不是当前核心闭环,状态可保留但不扩大功能。
|
||||
|
||||
## 7. 当前已落地进展
|
||||
|
||||
截至 2026-06-18,第一轮服务化已经完成以下模块:
|
||||
|
||||
1. `ReportService`:报告上传、图片校验、报告创建、重新分析、删除、AI 分析状态流转进入服务层;报告分析通过 `ReportAnalysisQueue` + `ReportAnalysisWorker` 异步消费。
|
||||
2. `HealthRecordService`:健康指标列表、创建、更新、删除、最新值、趋势查询、异常判断进入服务层;AI 记数据工具复用同一套创建逻辑。
|
||||
3. `ExerciseService`:运动计划创建、列表、详情、删除、打卡、AI 创建/查询/打卡进入服务层。
|
||||
4. `MedicationService`:用药列表、创建、更新、删除、今日服药确认、按剂量打卡、提醒查询、AI 创建/查询/确认进入服务层。
|
||||
5. `DietService`:饮食记录列表、保存、删除、热量/评分更新进入服务层。
|
||||
|
||||
其中以下模块已经继续演进为更严格的 Application Service + Repository 结构:
|
||||
|
||||
```text
|
||||
Endpoint
|
||||
-> Health.Application.*Service
|
||||
-> Health.Application.I*Repository
|
||||
-> Health.Infrastructure.Ef*Repository
|
||||
-> AppDbContext
|
||||
```
|
||||
|
||||
已完成:
|
||||
|
||||
1. `HealthRecordService` + `IHealthRecordRepository` + `EfHealthRecordRepository`
|
||||
2. `ExerciseService` + `IExerciseRepository` + `EfExerciseRepository`
|
||||
3. `DietService` + `IDietRepository` + `EfDietRepository`
|
||||
4. `MedicationService` + `IMedicationRepository` + `EfMedicationRepository`
|
||||
5. `ReportService` + `IReportRepository` + `EfReportRepository` + `IReportFileStorage` + `LocalReportFileStorage`
|
||||
|
||||
报告模块中,`ReportAnalysisQueue` 和 `ReportAnalysisService` 仍属于 Infrastructure:前者是内存队列适配,后者需要调用 AI 客户端并通过 `IReportRepository` 更新报告状态,不再直接依赖 `AppDbContext`。
|
||||
|
||||
AI 写入确认机制已完成第一阶段落地:
|
||||
|
||||
```text
|
||||
AI 写入工具调用
|
||||
-> 创建 10 分钟有效的 PendingAiWriteCommand
|
||||
-> 前端展示确认卡片,此时不写数据库
|
||||
-> 用户点击确认
|
||||
-> POST /api/ai/confirm-write/{commandId}
|
||||
-> 校验当前用户和一次性命令
|
||||
-> 执行对应 Application Service 写入
|
||||
```
|
||||
|
||||
- 健康指标、创建/确认用药、创建/打卡运动、AI 修改健康档案均进入待确认流程。
|
||||
- 纯查询工具继续直接执行。
|
||||
- 命令只能由所属用户执行一次;执行失败时会恢复命令供用户重试,过期或服务重启后自动失效。
|
||||
- AI 待确认命令已迁移到数据库表 `AiWriteCommands`,领取命令、业务写入和完成状态在同一事务内执行,避免重复写入。
|
||||
- 报告分析任务已迁移到数据库表 `ReportAnalysisTasks`,支持服务重启恢复、原子领取、失败重试和最终失败状态。
|
||||
- 项目当前仍使用 `EnsureCreated` 管理原有表;新增 `DatabaseSchemaMigrator` 和 `__AppSchemaMigrations` 版本表,为已有本地数据库安全补充基础设施表。
|
||||
|
||||
患者端其他业务收拢进展:
|
||||
|
||||
1. `HealthArchiveService` + `IHealthArchiveRepository`:健康档案页面、AI 查询和确认写入统一执行。
|
||||
2. `AiConversationService` + `IAiConversationRepository`:会话创建、消息保存、历史列表和删除统一执行。
|
||||
3. `PatientContextService`:统一组合健康档案、近期指标和当前用药。
|
||||
4. `UserService` + `IUserRepository`:个人资料和账号注销统一执行;账号数据清理使用事务。
|
||||
5. `CalendarService` + `ICalendarRepository`:聚合用药、运动、随访日历。
|
||||
|
||||
生产者/消费者管道现状:
|
||||
|
||||
- 报告分析:数据库持久化任务 + `ReportAnalysisWorker`。
|
||||
- 饮食图片识别:任务持久化到 `DietImageAnalysisTasks`,由 `DietImageAnalysisWorker` 原子领取、失败重试和恢复处理中断任务;前端接口协议保持不变。
|
||||
- 用药提醒:`MedicationReminderService` 负责扫描生产任务,任务持久化到 `MedicationReminderTasks` 并按药品/日期/时间唯一去重;`MedicationReminderWorker` 消费后写入 `NotificationOutbox`。真正的手机推送需在选定推送服务后消费 Outbox,目前不会把未发送通知标记为已推送。
|
||||
- 报告、饮食和用药任务消费者均采用 1 到 5 秒的自适应空闲轮询,过期 Processing 任务每分钟恢复一次,避免每秒重复执行恢复更新。
|
||||
- App 内用药提醒通过 `NotificationOutbox` + `/api/notifications/pending` 提供,患者端前台每 30 秒获取并展示,展示后回执去重;暂不接入系统级或厂商推送。
|
||||
- `MaintenanceService` 每小时执行一次维护,自动删除超过 30 天的已完成/失败后台任务、通知 Outbox、过期 AI 写入命令和旧 AI 会话,不删除用户健康记录、饮食记录或用药计划。
|
||||
- 登录、注册、验证码、Token 刷新和退出已收拢到 `IAuthService`;管理员医生管理和患者列表已收拢到 `IAdminService`,Endpoint 不再直接读写数据库。开发环境 `send-sms` 仍返回 `devCode` 供 Flutter 自动填入,非开发环境不返回验证码。
|
||||
- AI 工具查询、确认写入和事务边界已收拢到 `IAiToolExecutionService`,AI Endpoint 不再直接持有 `AppDbContext`。
|
||||
- 运动计划已从 `WeekStartDate + DayOfWeek` 周模板改为 `StartDate + EndDate + ReminderTime`,每日条目使用唯一的 `ScheduledDate`。连续 7 天、10 天或更长计划按真实日期生成,首页今日任务、健康日历、医生端只读详情和 AI 创建均使用同一日期模型。
|
||||
- App 内运动提醒按每日条目的 `ScheduledDate` 和计划 `ReminderTime` 生成到 `NotificationOutbox`;已打卡和休息日不会提醒,同一每日条目只生成一次。
|
||||
|
||||
当前仍保留在 Endpoint 或旧 Handler 中的业务,需要后续逐步收拢:
|
||||
|
||||
- `ConsultationService`:医生聊天暂不作为当前核心业务,但基础权限和数据读写仍可继续服务化。
|
||||
- `DoctorService` / `AdminDoctorService`:医生信息、患者列表、报告查看等目前不作为真实互联网医院流程,后续按老板和互联网医院接入方案调整。
|
||||
- `CalendarService`:健康日历目前聚合运动、用药、饮食等多模块数据,适合在核心模块稳定后单独收拢。
|
||||
- `User/ProfileService`:个人信息、健康档案、账号清理等可继续整理,尤其是 AI 修改健康档案前的确认规则。
|
||||
|
||||
## 8. 后续推广顺序
|
||||
|
||||
1. 报告:Application Service + 分析队列 + Worker
|
||||
2. 饮食:图片识别任务队列,用户修正后保存
|
||||
3. 用药:提醒扫描和推送任务解耦
|
||||
4. 运动:计划创建、打卡规则收拢到 Service
|
||||
5. 健康指标:记录、异常判断、趋势查询收拢到 Service
|
||||
6. 问诊:互联网医院接入前只收拢基础接口,不扩大聊天功能
|
||||
7. 医生端:保留基础接口,后续根据互联网医院和老板决策再完善审核/聊天工作流
|
||||
|
||||
## 9. 不做的事
|
||||
|
||||
当前阶段暂不做:
|
||||
|
||||
- 不一次性重构全项目。
|
||||
- 不立即引入 RabbitMQ/Kafka 等重型组件。
|
||||
- 不强制引入复杂 Repository 层。
|
||||
- 不改变当前患者端主要交互。
|
||||
- 不把医生聊天/医生审核作为当前核心业务流。
|
||||
|
||||
## 10. 验收标准
|
||||
|
||||
第一阶段完成后应满足:
|
||||
|
||||
- 报告 Endpoint 不再直接承载主要业务流程。
|
||||
- 报告 AI 分析不再使用 `Task.Run`。
|
||||
- 报告分析任务通过队列进入后台 Worker。
|
||||
- 报告状态流转集中在 Application Service。
|
||||
- 上传、列表、详情、删除、查看原图、分析失败状态保持可用。
|
||||
- 后端编译通过,前端报告页分析通过。
|
||||
@@ -371,7 +371,186 @@ var conversation = await db.Conversations
|
||||
5. 医生端做成真正可用的工作台:患者筛选、风险排序、随访提醒。
|
||||
6. 报告、饮食、用药、运动做长期趋势关联。
|
||||
|
||||
## 11. 总体建议
|
||||
## 11. 第二轮深挖补充
|
||||
|
||||
这一轮额外对已有 `docs/BUG_REVIEW.md`、后端端点、AI Agent handler、SignalR、Flutter provider 生命周期、饮食/蓝牙/问诊页面做了交叉检查。需要注意:旧 bug 文档中有一些问题已经被修复或情况已经变化,本节以当前代码为准。
|
||||
|
||||
### 11.1 已确认仍然存在的高风险问题
|
||||
|
||||
| 优先级 | 位置 | 当前问题 | 风险说明 | 修复方向 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| P0 | `backend/src/Health.WebApi/Endpoints/auth_endpoints.cs` | `/api/auth/send-sms` 仍返回 `devCode` | 任何客户端都能拿到验证码,短信验证等同失效 | 只在 Development 返回;生产环境完全移除 |
|
||||
| P0 | `health_app/lib/pages/auth/login_page.dart` | 前端拿到 `devCode` 后自动填入验证码 | 把后端安全问题固化成产品行为 | 删除自动填充;开发环境可用 debug banner 或日志 |
|
||||
| P0 | `backend/src/Health.WebApi/Endpoints/ai_chat_endpoints.cs` | SSE token 走 query,且 fallback 用 `ReadJwtToken` 解析 | query token 会进日志;`ReadJwtToken` 不验证签名 | SSE 改标准鉴权,或先换一次性 stream ticket |
|
||||
| P0 | `backend/src/Health.WebApi/Endpoints/ai_chat_endpoints.cs` | 创建/续写 AI 会话时 `FindAsync(conversationId)` 未校验 `UserId` | 可能跨用户写入/读取会话 | `FirstOrDefaultAsync(c => c.Id == id && c.UserId == userId)` |
|
||||
| P0 | `backend/src/Health.WebApi/Hubs/ConsultationHub.cs` | SignalR Hub 没有鉴权,入组只靠 `consultationId` | 任意连接可加入任意问诊房间 | `MapHub().RequireAuthorization()`,Join 前校验用户或医生权限 |
|
||||
| P0 | `backend/src/Health.WebApi/Hubs/ConsultationHub.cs` | `SendMessage` 按传入 `senderType` 和 `consultationId` 写库 | 客户端可冒充 Doctor/User,向任意会话发消息 | senderType 从 Claims/角色推导,不信任客户端 |
|
||||
| P0 | `backend/src/Health.WebApi/Endpoints/consultation_endpoints.cs` | 患者发消息只查会话存在,未校验会话属于当前用户 | 用户 A 可向用户 B 的问诊发 HTTP 消息 | 查询加 `c.UserId == userId` |
|
||||
| P0 | `backend/src/Health.WebApi/Endpoints/exercise_endpoints.cs` | `/items/{itemId}/checkin` 只 `FindAsync(itemId)` | 用户可打卡/取消他人的运动计划项 | Include Plan 后校验 `Plan.UserId` |
|
||||
| P0 | `backend/src/Health.Infrastructure/AI/AgentHandlers/exercise_agent_handler.cs` | AI 运动 checkin 只按 itemId 操作 | 通过 AI 工具也可越权打卡 | 查询 item 时联表校验当前 userId |
|
||||
| P0 | `backend/src/Health.Infrastructure/AI/AgentHandlers/medication_agent_handler.cs` | AI 用药 confirm 直接写 `MedicationLog`,不验证药品归属 | 可给他人药品写入服药记录 | 先查 `Medication.Id == medId && UserId == userId` |
|
||||
| P1 | `backend/src/Health.WebApi/Endpoints/medication_endpoints.cs` | `/medications/{id}/confirm` 查 existing log 未限制 `UserId`,也未先确认药品归属 | 可能误删/写入不属于当前用户药品的日志 | 先查药品归属,再按 `MedicationId + UserId` 查日志 |
|
||||
| P1 | `backend/src/Health.WebApi/Endpoints/doctor_endpoints.cs` | 医生端已鉴权,但患者详情/报告/随访操作多处只按 id 查询 | 医生可能访问或修改非自己负责患者的数据 | 所有医生端资源都按医生关联患者过滤 |
|
||||
| P1 | `backend/src/Health.WebApi/Endpoints/file_endpoints.cs` | 上传缺少大小、类型、内容校验,且返回结构与前端不匹配 | 恶意文件/超大文件风险,前端图片上传链路失败 | 加限制、白名单、URL 返回和访问控制 |
|
||||
| P1 | `health_app/lib/pages/diet/diet_capture_page.dart` | `_fieldCtrls` 缓存 controller,但页面没有 dispose | 多次进入饮食分析页会泄漏 TextEditingController | 在 State `dispose()` 中遍历释放 |
|
||||
| P1 | `health_app/lib/providers/consultation_provider.dart` | Hub/轮询 stop 需要手动调用,provider 自身没有自动释放 | 离开页面后可能继续 SignalR 或 5 秒轮询 | Notifier build 中注册 `ref.onDispose(stop)` |
|
||||
| P1 | `health_app/lib/providers/chat_provider.dart` | SSE `_subscription` 和 timer 没看到 provider dispose 释放 | 聊天流中断/页面销毁后可能残留监听 | 注册 `ref.onDispose`,切换会话时取消旧流 |
|
||||
| P1 | `backend/src/Health.WebApi/Program.cs` | `MapHub("/hubs/consultation")` 未 RequireAuthorization | 即使 API 鉴权,实时通道仍裸露 | Hub 映射处加鉴权并处理 token 传递 |
|
||||
|
||||
### 11.2 旧问题中已经变化或需要修正的判断
|
||||
|
||||
这些点在旧 `BUG_REVIEW.md` 里出现过,但当前代码已经不是原始状态,后续不要按旧结论机械修:
|
||||
|
||||
- `doctor_endpoints.cs` 不是“零授权”了:当前已有 `.RequireAuthorization()` 和医生角色过滤。真正问题是医生端数据授权粒度不够,部分详情/报告/随访接口没有限制到当前医生负责的患者。
|
||||
- `open_ai_compatible_client.cs` 的 Vision content 当前已经是 `Content = contentParts`,不是把多模态数组序列化成字符串。旧的 VLM 序列化 bug 看起来已修复。
|
||||
- `diet_agent_handler.cs` 和 `consultation_agent_handler.cs` 当前没有声明未实现工具,而是主动缩减为档案/记录查询。问题应描述为“AI Agent 能力和首页胶囊/欢迎卡片承诺不一致”,不是“声明工具但未实现”。
|
||||
- `cleanup_service.cs` 当前已经先删 ConversationMessages 再删 Conversations,旧的 FK 删除顺序问题已修。
|
||||
- `device_scan_page.dart` 当前 `dispose()` 已取消 scan/read/connection 订阅,不能继续作为确定泄漏 bug。仍建议检查 `OmronBleService` 的全局 provider 生命周期和断线重连边界。
|
||||
- `widget_test.dart` 中 `primary == primaryLight` 的错误断言已经改掉,目前测试更大的问题是覆盖面太浅。
|
||||
|
||||
### 11.3 后端授权边界需要系统性重查
|
||||
|
||||
项目里很多接口已经 `.RequireAuthorization()`,但“已登录”不等于“有权操作这个资源”。建议建立统一规则:
|
||||
|
||||
| 资源 | 当前风险 | 应该怎么查 |
|
||||
| --- | --- | --- |
|
||||
| AI Conversation | 续写时未绑定 `UserId` | `Conversation.Id == id && Conversation.UserId == currentUserId` |
|
||||
| Consultation HTTP | POST message 未绑定 `UserId` | `Consultation.Id == id && Consultation.UserId == currentUserId` |
|
||||
| Consultation Hub | 入组/发消息无服务端权限判断 | Join 和 SendMessage 都查用户或医生是否有权进入该 consultation |
|
||||
| ExercisePlanItem | checkin 只按 itemId | `ExercisePlanItem.Id == itemId && Item.Plan.UserId == currentUserId` |
|
||||
| Medication confirm | 部分确认接口未先校验药品归属 | `Medication.Id == id && Medication.UserId == currentUserId` |
|
||||
| Doctor patient detail | 医生按任意 patient id 查详情 | `User.Id == patientId && User.DoctorId == currentDoctorId` |
|
||||
| Doctor report review | 医生按任意 report id 审阅 | `Report.User.DoctorId == currentDoctorId` |
|
||||
| Doctor follow-up update/delete | 医生按任意 followUp id 操作 | `FollowUp.User.DoctorId == currentDoctorId` 或 `DoctorName/DoctorId` 绑定 |
|
||||
|
||||
建议在后端加一层可复用 helper,例如:
|
||||
|
||||
```csharp
|
||||
static IQueryable<User> ScopePatientsToDoctor(AppDbContext db, Guid doctorId) =>
|
||||
db.Users.Where(u => u.Role == "User" && u.DoctorId == doctorId);
|
||||
```
|
||||
|
||||
所有医生端接口都从这个 scope 派生,不要每个 endpoint 手写判断。
|
||||
|
||||
### 11.4 API 语义和错误码问题
|
||||
|
||||
现在不少接口用 HTTP 200 包业务错误码,比如 401/403/404/400 都包成 `{ code, message }`。这种风格可以保留,但要注意两个问题:
|
||||
|
||||
- 对认证授权失败,HTTP 状态码最好仍返回 401/403,方便客户端、网关、日志系统识别。
|
||||
- 业务错误码需要统一枚举,否则前端只能靠字符串判断。
|
||||
|
||||
建议定义统一错误码:
|
||||
|
||||
- `0` 成功
|
||||
- `40001` 参数错误
|
||||
- `40002` 登录过期
|
||||
- `40003` 无权限
|
||||
- `40004` 资源不存在
|
||||
- `40005` 业务状态冲突
|
||||
- `50000` 服务端异常
|
||||
|
||||
并让 `ExceptionMiddleware` 只处理意外异常,业务错误由 endpoint 明确返回。
|
||||
|
||||
### 11.5 数据模型和索引补充
|
||||
|
||||
当前 `AppDbContext` 已经配置了不少索引和枚举转换,比旧报告里“完全没有 FK/索引”的描述更好。但仍建议补:
|
||||
|
||||
- `RefreshToken(Token)` 唯一或普通索引:刷新 token 查询会频繁发生。
|
||||
- `RefreshToken(UserId, IsRevoked, ExpiresAt)`:便于清理和会话管理。
|
||||
- `Report(UserId, CreatedAt)`:报告列表按用户和时间查询。
|
||||
- `FollowUp(UserId, ScheduledAt)`:随访日历、医生待办会用到。
|
||||
- `DeviceToken(UserId)`:推送服务上线后需要。
|
||||
- `Consultation(UserId, CreatedAt)` 和 `Consultation(Status, CreatedAt)`:患者历史和医生待办都会用到。
|
||||
|
||||
另外,核心关系建议显式配置删除行为,尤其是 User 删除时关联 Consultation、Report、Conversation、MedicationLog 的级联或手动删除策略。
|
||||
|
||||
### 11.6 AI Agent 产品能力不一致
|
||||
|
||||
现在首页上有多个 agent/胶囊入口,但后端能力不完全一致:
|
||||
|
||||
- 饮食 Agent 当前只保留健康档案查询,真正饮食识别在专门图片接口。
|
||||
- 问诊 Agent 当前只保留健康记录和档案查询,转医生走专门问诊流程。
|
||||
- 用药/运动 Agent 有创建、查询、确认能力,但确认工具存在所有权校验问题。
|
||||
- 通用 Agent 如果聚合多个工具,需要明确“哪些动作会写数据,哪些只是查询”。
|
||||
|
||||
建议 UI 文案和后端能力统一:
|
||||
|
||||
- 欢迎卡片不要暗示当前 agent 能完成它实际上做不到的写操作。
|
||||
- 会写入健康数据、药品、运动计划、档案的 AI 动作,必须有确认卡片。
|
||||
- AI 工具调用结果应返回结构化状态,前端不要只靠自然语言判断成功。
|
||||
|
||||
### 11.7 前端状态和生命周期问题
|
||||
|
||||
前端现在能跑起来,但长期运行会有状态残留风险:
|
||||
|
||||
- `ConsultationChatNotifier` 里 Hub 和轮询 timer 需要自动释放。建议 `build()` 中调用 `ref.onDispose(stop)`。
|
||||
- `ChatNotifier` 的 SSE 订阅、流式响应 timer 需要在 provider 销毁、切换 agent、重新发送时取消。
|
||||
- 饮食页 `_fieldCtrls` 需要 `dispose()`,否则每次识别食物后 controller 累积。
|
||||
- 多个 `FutureProvider` 没有 `autoDispose`,页面级数据会缓存很久。健康最新值、药品提醒、当前运动计划这类数据建议明确刷新策略。
|
||||
- 很多页面删除后只本地 `_load()`,没有 `ref.invalidate(...)`,跨页面缓存可能不同步。
|
||||
|
||||
### 11.8 UI 深层问题:不是再加渐变,而是建立层级
|
||||
|
||||
最近 UI 调整集中在侧边栏、欢迎卡片、饮食页、设置页、个人信息页等。颜色已经比最初丰富,但仍需要注意:
|
||||
|
||||
- 颜色角色要固定:蓝色用于主行动/健康状态,橙色用于饮食,紫色用于报告,绿色用于记录/恢复,青色用于设备或运动,浅红用于提醒/风险。
|
||||
- 欢迎卡片和胶囊按钮的图标必须同语义、同线宽、同背景形状。用户已经多次指出“不只是颜色一样,图标内容也要一样”,这说明视觉一致性比单个渐变更重要。
|
||||
- 健康仪表盘应优先展示数字、单位、状态、更新时间。图标可以弱化,避免抢数字层级。
|
||||
- 设置页不应只是按钮列表,应分为账号、安全、通知、设备、隐私、关于。
|
||||
- 个人信息页应像正式档案:基础信息、医疗信息、健康偏好、绑定医生、账号安全分区展示。
|
||||
- 功能入口两行三列是合理的,但每个入口不要堆摘要,图标 + 名称 + 必要状态即可。
|
||||
|
||||
### 11.9 功能缺口再细化
|
||||
|
||||
| 模块 | 当前缺口 | 建议 |
|
||||
| --- | --- | --- |
|
||||
| 饮食记录 | 已有识别和保存,但历史记录编辑能力弱 | 支持编辑食物、份量、热量、餐次;支持从历史复制 |
|
||||
| 报告管理 | 上传后异步 AI 分析,但失败/处理中状态不够细 | 增加 pending/analyzing/failed/retry 状态和轮询刷新 |
|
||||
| 问诊 | 患者端创建即新会话,历史问诊入口弱 | 增加问诊历史、继续问诊、关闭问诊、评价医生 |
|
||||
| 用药 | 提醒后台只记录日志,未实际推送 | 接入推送,支持漏服/补服/跳过原因 |
|
||||
| 运动 | 有计划和打卡,但计划解释和详情不足 | 计划详情页、运动禁忌、完成趋势 |
|
||||
| 健康日历 | 汇总用药/运动/随访,但和打卡状态联动有限 | 日历上直接展示已完成、未完成、逾期 |
|
||||
| 医生端 | 已有工作台,但患者范围和流程需加强 | 风险患者排序、未读消息、待审报告、随访待办 |
|
||||
| 管理员端 | 医生管理已有基础 | 增加操作审计、禁用账号、数据统计 |
|
||||
|
||||
### 11.10 更细的整改顺序
|
||||
|
||||
第一批必须先修安全:
|
||||
|
||||
1. 移除生产 `devCode` 和前端自动填充。
|
||||
2. 修 SSE 认证,不再解析未验证 JWT。
|
||||
3. AI conversation 按用户归属查询。
|
||||
4. Consultation Hub 加鉴权、入组校验、senderType 服务端判定。
|
||||
5. Consultation HTTP 发消息校验当前用户。
|
||||
6. Exercise/Medication 的普通接口和 AI 工具都补所有权校验。
|
||||
7. 医生端详情、报告、随访接口限制到当前医生负责患者。
|
||||
|
||||
第二批修接口契约和稳定性:
|
||||
|
||||
1. 文件上传返回 `{ id, name, size, url, contentType }`。
|
||||
2. 前端 `uploadFile` 兼容后端 envelope 和 list 返回,失败要提示。
|
||||
3. 饮食页 controller dispose。
|
||||
4. Chat/Consultation provider 注册 `ref.onDispose`。
|
||||
5. 后端补上传大小/类型限制。
|
||||
6. 关闭生产 body 日志。
|
||||
|
||||
第三批做 UI 体系:
|
||||
|
||||
1. 把颜色、渐变、圆角、阴影、图标背景抽成设计 token。
|
||||
2. 侧边栏、个人信息、设置、饮食分析页按同一设计语言重做。
|
||||
3. 首页欢迎卡片和胶囊入口统一图标语义。
|
||||
4. 健康仪表盘增加更新时间、单位、状态文字。
|
||||
5. 对中老年用户做字号、对比度、触控面积检查。
|
||||
|
||||
第四批补测试:
|
||||
|
||||
1. 后端加越权测试:用户 A 不能操作用户 B 的 conversation/consultation/exercise/medication。
|
||||
2. 加短信验证码生产环境不返回测试。
|
||||
3. 加 SignalR Hub 入组权限测试。
|
||||
4. 加文件上传类型/大小测试。
|
||||
5. Flutter 加饮食保存、问诊连接释放、上传失败 UI 的 widget/provider 测试。
|
||||
|
||||
## 12. 总体建议
|
||||
|
||||
这个项目最有价值的方向是“围绕患者长期健康数据做 AI 辅助管理”,不是简单堆功能入口。接下来建议把项目重心从“页面多”转为“核心闭环扎实”:
|
||||
|
||||
@@ -383,4 +562,3 @@ var conversation = await db.Conversations
|
||||
- 安全和隐私经得起真实使用。
|
||||
|
||||
UI 上不要继续单页单独调色,应该先统一设计体系,再逐步替换页面。工程上先修安全和接口契约,再做大面积美化。这样项目会从“原型功能很多”变成“真正像一个可信赖的健康产品”。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user