【后端修复】 - 删除diet/consultation agent假工具声明 - 修复DayOfWeek实体注释 - 修复Vision API content序列化 - cleanup_service级联删除修复 - 用药提醒时区偏差修复 - 统一DateTime处理(UtcNow+8) - 新增UTC DateTime JSON转换器 【前端UI重构】 - 配色体系全面更新(#8B5CF6淡紫+#F0ECFF背景) - 登录页重设计 - 首页重设计(透明顶栏、渐变背景、胶囊输入区) - 聊天卡片加白蓝边框、渐变标题 - 侧边栏重构(渐变背景、合并顶部、删除底部设置) - 确认卡片可编辑字段恢复 - 所有子页面加返回按钮 - catch异常加日志 - 删除后refresh provider缓存
20 KiB
健康管家 — 全面代码审查与Bug文档
审查日期:2026-06-10 | 范围:全栈(后端 .NET 10 + 前端 Flutter)
测试结果汇总
| 测试集 | 通过 | 失败 | 说明 |
|---|---|---|---|
| 后端单元测试 (entity_tests) | 10 | 0 | 实体/数据库操作全通过 |
| 后端单元测试 (auth_tests) | 4 | 0 | 认证流程全通过 |
| 后端单元测试 (ai_agent_tests - PromptManager) | 8 | 0 | AI提示词全通过 |
| 后端集成测试 (ai_agent_tests - AI对话) | 0 | 5 | 需要后端运行中 |
| Flutter 测试 (widget_test) | - | - | sqlite3 原生库下载失败,无法运行 |
总计:22/27 通过(5个集成测试需要后端在线)
一、后端 Bug
B1. 缺少 consultation DELETE 端点
- 位置:
consultation_endpoints.cs - 问题:问诊创建后无删除/取消接口。患者无法取消发起的问诊
- 影响:患者发起错误问诊后无法撤销
- 建议:添加
MapDelete("/consultations/{id:guid}", ...)
B2. ai_chat_endpoints 中 unified agent 缺少部分工具
- 位置:
ai_chat_endpoints.cs:340-347 - 问题:unified agent 虽聚合了 7 种工具,但缺少
manage_archive和request_doctor - 影响:用户在 unified 模式无法通过对话修改档案或请求医生
B3. CompressImage 未释放 GDI 资源
- 位置:
ai_chat_endpoints.cs:484-503 - 问题:
Image.FromFile、Bitmap、Graphics均using包裹,但EncoderParameters未释放 - 影响:每次食物识别可能泄漏少量非托管内存
- 修复:
using var parameters = new EncoderParameters(1);
B4. 用药提醒服务可能定时查询过于频繁
- 位置:
BackgroundServices/medication_reminder_service.cs - 问题:需要检查轮询间隔,每分钟查一次可能对数据库压力大
B5. 开发数据种子硬编码 API Key 检查
- 位置:
dev_data_seeder.cs - 问题:
DEVDATA_ENABLED=true创建测试用户,生产环境可能误开启
二、前端 Bug
C1. 聊天列表从顶部跳到底部 ✅ 已修复
- 修复:
chat_messages_view.dart改为reverse:true - 修复:
home_page.dart滚动逻辑简化
C2. SwipeDeleteTile 内层手势冲突 ✅ 已修复
- 修复:
common_widgets.dart滑动状态下用AbsorbPointer屏蔽内部按钮
C3. SwipeDeleteTile 红色溢出 ✅ 已修复
- 修复:margin 参数从 Stack 内部移到外部 Padding
C4. 运动打卡 dayOfWeek 索引偏差 ✅ 已修复
- 修复:
remaining_pages.dart改为weekday % 7与 C# DayOfWeek 对齐
C5. 用药管理 Dismissible 一步删除 ✅ 已修复
- 修复:统一使用
SwipeDeleteTile
C6. Flutter 测试断言错误
- 位置:
health_app/test/widget_test.dart:9 - 代码:
expect(AppTheme.primary, AppTheme.primaryLight); - 问题:
primary(0xFF6366F1) ≠primaryLight(0xFFEEF2FF),这个断言必然失败 - 修复:改为
expect(AppTheme.primary, const Color(0xFF6366F1));
C7. home_page.dart onTap 手误写了 ?.()
- 位置:
home_page.dart:115 - 代码:
onTap: () => pushRoute(ref, 'notificationPrefs')实际没有?.()问题(之前看错了),此处无误
C8. 报告详情返回按钮:popRoute 在 setState 后
- 位置:
report_pages.dart:449-454 - 问题:
clearAnalysis()调用state.copyWith(...)然后立即popRoute(ref),在 pop 过程中可能访问已 dispose 的 provider - 风险:中等
C9. DietCapturePage TextField 内存泄漏
- 位置:
diet_capture_page.dart:465,479,495 - 问题:每个食物项创建
TextEditingController(text: food.name)但从不 dispose - 修复:用
TextFormField+initialValue或缓存 controller
C10. 报告上传错误吞没
- 位置:
report_pages.dart:199 - 代码:
} catch (_) {}— 上传失败无任何反馈 - 修复:至少显示 snackbar 提示
三、前端缺失功能
M1. 饮食记录无编辑/修改功能
- 当前只能查看(
DietRecordDetailPage)和删除,无法修改已有记录
M2. 运动计划详情页缺失
ExercisePlanPage的onTap: () {}为空,点卡片无反应- 没有类似
ExercisePlanDetailPage的页面
M3. 问诊列表无前端入口
- 后端有
/api/consultationsGET,但前端没有问诊历史列表页
M4. 对话无删除/清空功能
- 后端有
DELETE /api/ai/conversations/{id},但前端无对应UI
M5. 无数据导出功能
- 用户无法导出健康数据(血压记录、饮食记录等)
四、安全与代码质量
S1. JWT Secret 开发默认值
- 位置:
Program.cs:46 - 问题:
jwtSecret ??= "dev-secret-key-change-in-production-min-32-chars!!"; - 风险:若忘记设环境变量,生产环境用弱密钥
S2. token 通过 query string 传输(SSE)
- 位置:
ai_chat_endpoints.cs:25,sse_handler.dart:17 - 问题:
token放在 URL query string,会被服务器日志、代理缓存 - 风险:中等(含过期时间的 token,但仍有泄露风险)
S3. CORS 全开
- 位置:
Program.cs:96-98 - 代码:
policy.SetIsOriginAllowed(_ => true) - 风险:生产环境应限定具体 origin
S4. 全局异常中间件可能泄露内部错误
- 位置:
exception_middleware.cs - 需要检查:是否在生产环境返回了调用栈
S5. Flutter 硬编码后端 IP
- 位置:
api_client.dart:6 - 代码:
const String baseUrl = 'http://10.4.164.158:5000'; - 问题:每次换网络都需改代码
五、未使用代码(可清理)
chat_messages_view.dart:_cardFilledBtn,_cardOutlineBtn,_agentColors,_taskRow未使用chat_provider.dart:_parseAgent未使用remaining_pages.dart:shadcn_ui导入未使用report_pages.dart:shadcn_ui导入未使用,reportId变量未使用service_package_detail_page.dart:navigation_provider导入未使用
六、Agent 自动审查新增发现
A1. app_router.dart 无防御 null 断言(🔴严重)
- 位置:
app_router.dart:41-73 - 代码示例:
ReportDetailPage(id: params['id']!)等多处 - 问题:
params['id']!若 params 中无 'id' 键,会抛出 null 断言异常导致崩溃 - 影响:任何路由参数拼写错误或缺少都会 crash
- 修复:使用
params['id'] ?? ''并提供 fallback
A2. device_scan_page.dart BLE 流订阅未取消(🔴严重)
- 位置:
device_scan_page.dart - 问题:BLE stream subscription 在 dispose 时未 cancel,导致蓝牙连接泄漏
- 影响:多次进出扫描页会积累未释放的 BLE 连接
A3. 静默吞错误(🟡中)
- 范围:全前端 28 处
catch (_) {} - 问题:所有 API 错误、解析错误均无日志或用户提示
- 修复:至少加上
debugPrint('Error: $e')或显示 snackbar
A4. 删除后未刷新 Provider(🟡中)
- 范围:多个页面
- 问题:删除操作后调用了
_load()或_refresh()(setState),但未调用ref.invalidate(someProvider),导致 Riverpod 缓存未更新 - 影响:切换到其他页面再回来,旧数据可能仍显示
A5. FutureProvider + setState 双重模式(🟢低)
- 范围:
DietRecordListPage,ExercisePlanPage等 - 问题:同时使用 Riverpod FutureProvider 和本地 setState,导致 provider 失效无效
七、全部问题汇总(含Agent发现)
| # | 优先级 | 位置 | 问题描述 |
|---|---|---|---|
| 1 | 🔴 | app_router.dart |
null 断言无防御,参数缺失即崩溃 |
| 2 | 🔴 | device_scan_page.dart |
BLE 流订阅未取消 |
| 3 | 🔴 | B1 consultation |
缺少 DELETE 端点 |
| 4 | 🔴 | S1 Program.cs |
JWT 开发默认弱密钥 |
| 5 | 🔴 | C9 diet_capture |
TextEditingController 泄漏 |
| 6 | 🟡 | 全局 28处 | catch(_){} 静默吞错 |
| 7 | 🟡 | 多页面 | 删除后无 ref.invalidate |
| 8 | 🟡 | C6 widget_test |
断言 primary == primaryLight 错误 |
| 9 | 🟡 | C10 report |
上传失败无提示 |
| 10 | 🟡 | M2 exercise |
运动详情页缺失,onTap 空 |
| 11 | 🟡 | C8 report |
pop 时序问题 |
| 12 | 🟡 | A5 多页面 |
FutureProvider+setState 双重模式 |
| 13 | 🟢 | B3 ai_chat |
EncoderParameters 未释放 |
| 14 | 🟢 | B4 bg_service |
用药提醒轮询频率需审视 |
| 15 | 🟢 | B5 dev_data |
生产环境可能误开启测试数据 |
| 16 | 🟢 | S2 SSE |
token 走 query string |
| 17 | 🟢 | S3 CORS |
全开 AllowCredentials |
| 18 | 🟢 | S5 api_client |
硬编码 IP |
| 19 | 🟢 | 多处 | 未使用代码可清理 |
| 20 | 🟢 | B2 unified |
unified agent 缺少 manage_archive 工具 |
八、后端 Agent 审查新增发现(关键)
D1. 运动计划 DayOfWeek 跨层不一致(🔴严重)
- 位置:
prompt_manager.csvsexercise_plan.cs实体注释 - 问题:Prompt 告诉 AI
day_of_week: 0-6(周日=0),但实体注释// 0=周一, 6=周日。AI 按周日=0 生成数据,后端按周一=0 解析,运动计划星期全偏一天 - 修复:统一为 C# DayOfWeek 枚举(0=周日)
D2. SMS 验证码使用伪随机数(🔴安全漏洞)
- 位置:
sms_service.cs - 问题:
Random.Shared.Next(100000, 1000000)使用 PRNG,攻击者可预测验证码 - 修复:改用
RandomNumberGenerator.GetInt32(100000, 999999)
D3. checkin 无所有权验证(🔴越权漏洞)
- 位置:
medication_agent_handler.cs:99、exercise_agent_handler.cs:69 - 问题:
confirm_medication和exercise checkin直接通过 itemId 操作,不验证是否属于当前用户。任何认证用户可操作他人数据 - 修复:添加
&& item.Plan.UserId == userId检查
D4. VisionAsync content 序列化错误(🔴严重)
- 位置:
open_ai_compatible_client.cs:136 - 问题:将图片 contentParts 先序列化为 JSON 字符串再赋值给 Content,但 OpenAI 兼容 API 期望 Content 为数组格式
- 影响:食物识别 VLM 调用可能失败
D5. diet/consultation agent 工具声明但未实现(🔴严重)
- 位置:
diet_agent_handler.cs、consultation_agent_handler.cs - 问题:
EstimateFoodTool和RequestDoctorTool在 Tools 列表中声明,但 Execute 方法无对应 case,调用返回"未知工具" - 影响:饮食识别和请求医生功能不可用
D6. CleanupService 删除未级联(🔴严重)
- 位置:
cleanup_service.cs:28 - 问题:
db.Conversations.RemoveRange(oldConversations)未先删除关联的 ConversationMessage,可能因 FK 约束抛异常 - 修复:先删除 Messages 再删 Conversations,或使用 ExecuteDeleteAsync
D7. 用药提醒时区计算 bug(🔴严重)
- 位置:
medication_reminder_service.cs:37 - 问题:
DateTime.SpecifyKind(beijingNow.Date, DateTimeKind.Utc)— 北京时间 00:00 被标记为 UTC 00:00,导致打卡检测有 8 小时偏差 - 影响:提醒时间错位、重复提醒或漏提醒
D8. DbContext 未配置外键和级联删除(🟡中等)
- 位置:
app_db_context.cs - 问题:
OnModelCreating没有任何HasOne/WithMany/HasForeignKey/OnDelete配置,全凭约定 - 影响:数据库无 FK 约束、级联行为不明确、RefreshToken 全表扫描
D9. 缺少多个数据库索引(🟡中等)
- RefreshToken: 无索引,认证查询全表扫描
- FollowUp: 无 (UserId, ScheduledAt) 索引
- DeviceToken: 无 UserId 索引
- Report: 无 (UserId, CreatedAt) 索引
D10. ExceptionMiddleware 统一返回500(🟡中等)
- 位置:
exception_middleware.cs - 问题:所有异常都返回 500,不区分 400/401/404
- 影响:客户端无法根据状态码处理不同类型的错误
D11. 用药提醒未实际推送(🟡中等)
- 位置:
medication_reminder_service.cs - 问题:TODO 注释表明推送尚未实现,只记录日志
D12. Prompt 文本硬编码(🟢低)
- 不支持热更新,修改需重新编译
九、最终汇总(所有发现)
| # | 优先级 | 类别 | 位置 | 问题 |
|---|---|---|---|---|
| 1 | 🔴 | 安全 | sms_service | 验证码用 PRNG 可预测 |
| 2 | 🔴 | 安全 | agent handlers | checkin 越权漏洞 |
| 3 | 🔴 | 逻辑 | prompt/entity | DayOfWeek 跨层不一致 |
| 4 | 🔴 | 功能 | diet/consult agent | 工具声明但未实现 |
| 5 | 🔴 | 功能 | open_ai_client | Vision content 序列化错误 |
| 6 | 🔴 | 稳定性 | cleanup_service | 删除未级联 FK 冲突 |
| 7 | 🔴 | 功能 | reminder_service | 时区计算 8h 偏差 |
| 8 | 🔴 | 崩溃 | app_router.dart | null 断言无防御 |
| 9 | 🔴 | 泄漏 | device_scan_page | BLE 流未取消 |
| 10 | 🔴 | 安全 | Program.cs | CORS 全开+AllowCredentials |
| 11 | 🔴 | 安全 | Program.cs | JWT 默认弱密钥 |
| 12 | 🔴 | 泄漏 | diet_capture | TextEditingController 未释放 |
| 13 | 🟡 | 体验 | 28处 | catch(_){} 静默吞错 |
| 14 | 🟡 | 逻辑 | 多页面 | 删除后未 invalidate provider |
| 15 | 🟡 | 配置 | app_db_context | 无 FK/级联/索引 |
| 16 | 🟡 | 体验 | exception_middleware | 统一返回 500 |
| 17 | 🟡 | 测试 | widget_test | 断言永远失败 |
| 18 | 🟡 | 功能 | 用药提醒 | 推送未实现 |
| 19 | 🟡 | 架构 | 多页面 | FutureProvider+setState 混用 |
| 20 | 🟡 | 缺失 | exercise | 计划详情页缺失 |
| 21 | 🟢 | 代码 | 多处 | 未使用代码/本地时间/冗余 |
十、后端端点 Agent 审查新增发现(关键)
E1. 医生端点零授权(🔴阻断级)
- 位置:
doctor_endpoints.cs:19 - 问题:
MapGroup("/api/doctor")没有.RequireAuthorization(),所有医生端点(患者详情、健康数据、问诊、报告、随访)对公网开放 - 影响:任何人可查看所有患者隐私数据、修改报告、创建/删除随访
E2. consultation POST 消息无所有权检查(🔴阻断级)
- 位置:
consultation_endpoints.cs:48-75 - 问题:发消息只检查 consultation 存在,不验证是否属于当前用户。用户A可向用户B的问诊发消息
E3. exercise checkin 无所有权检查(🔴阻断级)
- 位置:
exercise_endpoints.cs:119 - 问题:
FindAsync([itemId])只按ID查,不验证item.Plan.UserId == userId。用户可操作他人运动计划
E4. SMS验证码在响应中暴露(🔴严重)
- 位置:
auth_endpoints.cs:34 - 问题:
devCode = code将6位验证码直接返回在JSON中,无#if DEBUG守卫
E5. Task.Run 火后不理模式(🔴严重)
- 位置:
report_endpoints.cs:67 - 问题:
_ = Task.Run(async () => { ... }, CancellationToken.None)在请求结束后 scope 可能已释放,后台任务崩溃
E6. System.Drawing 仅Windows(🔴严重)
- 位置:
ai_chat_endpoints.cs:486-494 - 问题:
Image.FromFile/Bitmap/Graphics在Linux容器中崩溃
E7. 健康数据 N+1 查询(🟡中等)
- 位置:
health_endpoints.cs:78-89 - 问题:
/latest对5种指标类型分别发一次SQL查询
E8. 日历用药事件显示错误(🟡中等)
- 位置:
calendar_endpoints.cs:49 - 问题:用户有任意活跃用药就在每月每天标记"用药",不区分具体哪天该吃药
E9. 手动JSON解析绕过模型绑定(🟡中等)
- 范围:diet、medication、exercise、doctor endpoints
- 问题:
JsonDocument.Parse手动解析导致 Swagger 无法文档化、无自动验证、拼写错误抛500
E10. 缺失端点
- health:缺 DELETE
- diet:缺 PUT
- exercise:缺 PUT
- report:缺 DELETE
- file:缺 GET/DELETE/list
- followup:缺 detail/confirm
十一、完整问题排名
| # | 等级 | 文件 | 问题 |
|---|---|---|---|
| 1 | 🔴🔴 | doctor_endpoints | 零授权 — 所有患者数据公开 |
| 2 | 🔴🔴 | consultation | 发消息无所有权检查 |
| 3 | 🔴🔴 | exercise | checkin无所有权检查 |
| 4 | 🔴 | auth | SMS验证码响应暴露 |
| 5 | 🔴 | report | Task.Run火后不理 |
| 6 | 🔴 | ai_chat | System.Drawing仅Windows |
| 7 | 🔴 | sms_service | PRNG可预测验证码 |
| 8 | 🔴 | agent handlers | checkin越权 |
| 9 | 🔴 | prompt_manager | DayOfWeek不一致 |
| 10 | 🔴 | diet/consult agent | 工具声明未实现 |
| 11 | 🔴 | open_ai_client | Vision序列化错误 |
| 12 | 🔴 | cleanup_service | 删除未级联 |
| 13 | 🔴 | reminder_service | 时区8h偏差 |
| 14 | 🔴 | app_router | null断言崩溃 |
| 15 | 🔴 | device_scan | BLE泄漏 |
| 16 | 🔴 | Program.cs | CORS全开 |
| 17 | 🔴 | Program.cs | JWT弱密钥 |
| 18 | 🔴 | diet_capture | Controller泄漏 |
| 19 | 🟡 | 28处 | catch(_){}吞错 |
| 20 | 🟡 | 多页面 | 删除未invalidate |
| 21 | 🟡 | health | N+1查询 |
| 22 | 🟡 | calendar | 用药事件逻辑错误 |
| 23 | 🟡 | app_db_context | 无FK/索引配置 |
| 24 | 🟡 | exception_mw | 统一500 |
| 25 | 🟡 | 提醒服务 | 推送未实现 |
| 26 | 🟡 | 多端点 | 缺PUT/DELETE |
| 27 | 🟡 | 多端点 | 手动JSON无验证 |
优先级排序
| 优先级 | Bug | 影响范围 |
|---|---|---|
| 🔴 高 | C9: TextEditingController 内存泄漏 | 饮食识别页面 |
| 🔴 高 | B1: 缺少 consultation DELETE | 问诊功能 |
| 🔴 高 | S1: JWT 弱密钥默认值 | 安全 |
| 🟡 中 | C6: Flutter 测试断言错误 | CI/CD |
| 🟡 中 | C10: 报告上传错误吞没 | 用户体验 |
| 🟡 中 | M2: 运动计划详情页缺失 | 用户体验 |
| 🟡 中 | C8: report pop 时序问题 | 潜在崩溃 |
| 🟢 低 | B3: EncoderParameters 未释放 | 极少量泄漏 |
| 🟢 低 | S5: 硬编码 IP | 开发体验 |
| 🟢 低 | 未使用代码 | 代码质量 |
共发现 15 个问题,5 个缺失功能。其中 5 个 Bug 已在本轮修复。
第二轮换角度审查(新增 10 个问题)
F1. uploadFile 响应解析崩溃 🔴
api_client.dart:60-69 — 后端返回 data: [{id, name, size}](List),前端做 data['data']?['url'] 对List用字符串索引,运行时异常。聊天图片上传全部无声失败。
F2. consultationChatProvider SignalR 永停不了 🔴
consultation_provider.dart — stop() 定义了但无 Widget dispose 调用。离开问诊页后 SignalR 连接和 5秒轮询永续运行。
F3. chatProvider 流订阅无 dispose 🔴
chat_provider.dart — _subscription/_streamTimer 无 dispose,provider 销毁即泄漏。
F4. ChatMessage 原地修改破坏不可变性 🔴
msg.confirmed = true 直接修改 state 中的对象 → 违反 Riverpod 不可变契约。
F5. 所有 FutureProvider 无 autoDispose 🟡
6 个 FutureProvider 缓存永不过期,页面级数据不释放。
F6. authProvider Token 刷新窗口期 id/phone 为空 🟡
auth_provider.dart:59 — UserInfo(id: '', phone: '') 然后等 _loadProfile() 异步补全。
F7. 医生 Web 零认证 🔴🔴
doctor_web/src/services/api-client.ts — 不发送 Authorization 头。叠加后端 doctor_endpoints 零授权(E1),任何人可访问所有患者数据。
F8. 医生 Web SignalR handler 泄漏 🔴
ChatPage.tsx:28-65 — 组件卸载时若在 Reconnecting 状态,跳过 off('ReceiveMessage'),重复挂载累积 handler。
F9. 6个后端端点前端从未调用 🟢
GET/DELETE /api/ai/conversations、GET /api/consultations、PUT /api/health-records/{id}、全部 /api/doctor/*
F10. 37 对 API 契约中 1 对崩溃 + 多端手动JSON脆弱 🟡
uploadFile 是唯一崩溃的。其余 36 对匹配但 TryGetProperty 区分大小写,依赖前端恰好用 camelCase。
两轮共发现 37 个问题,其中 5 个阻断级、19 个严重、10 个中等、3 个低。