Backend: .NET 10 + PostgreSQL + EF Core + JWT + SignalR Frontend patient: React 19 + TypeScript + Vite (mobile H5) Frontend doctor: React 19 + TypeScript + Vite (desktop web)
62 KiB
HealthManager 健康管理平台 — 超级详细技术文档
适用对象:小白开发者 / 新加入项目的同学 文档目标:看懂每一个文件是干什么的,代码在做什么,为什么要这样写
目录
- 项目总览
- 架构设计:什么是"整洁架构"
- 后端 — Domain 领域层
- 后端 — Application 应用层
- 后端 — Infrastructure 基础设施层
- 后端 — WebApi 接口层
- 病人端前端 (frontend-patient)
- 医生端前端 (frontend-doctor)
- 数据库表结构
- 常见问题 FAQ
1. 项目总览
1.1 这是什么项目?
HealthManager 是一个面向心血管术后患者的健康管理平台。它有两个前端和一个后端:
| 部分 | 技术 | 谁用 | 干什么 |
|---|---|---|---|
| 后端 API | .NET 10 + PostgreSQL | 为两个前端提供数据 | 用户认证、健康数据存储、用药管理、在线问诊、报告解读、随访管理 |
| 病人 H5 前端 | React 19 + TypeScript + Vite | 术后患者 | 在手机上记录血压/心率/用药,咨询医生,查看报告 |
| 医生 PC 前端 | React 19 + TypeScript + Vite | 心血管医生 | 在电脑上管理患者、回复咨询、解读报告、安排随访 |
1.2 涉及的技术
| 技术 | 是什么 | 在本项目中的用途 |
|---|---|---|
| .NET 10 | 微软的后端开发框架 | 写 API 接口,处理业务逻辑 |
| C# | .NET 的编程语言 | 所有后端代码都用 C# 写 |
| Entity Framework Core (EF Core) | .NET 的数据库操作框架 | 用 C# 对象操作数据库,不用手写 SQL |
| PostgreSQL | 开源关系型数据库 | 存储用户、健康数据、药物、咨询等所有数据 |
| Npgsql | PostgreSQL 的 .NET 驱动 | 让 EF Core 能连接和操作 PostgreSQL |
| JWT (JSON Web Token) | 一种身份验证令牌 | 用户登录后,凭令牌访问 API,不用每次输密码 |
| SignalR | 微软的实时通信框架 | 实现医生和患者之间的实时聊天 |
| Swagger | API 文档工具 | 自动生成 API 文档页面,可以直接在浏览器里测试接口 |
| MinIO | S3 兼容的对象存储 | 存储图片(报告照片、头像等) |
| Redis | 内存缓存数据库 | 缓存常用数据,加速访问 |
| React | Facebook 出的前端框架 | 构建用户界面,组件化开发 |
| TypeScript | JavaScript 的超集,加了类型系统 | 让前端代码更安全、更好维护 |
| Vite | 新一代前端构建工具 | 开发时热更新快,打包也快 |
| Zustand | 轻量级状态管理库 | 管理登录状态、用户信息等全局数据 |
| React Router | React 路由库 | 实现页面跳转,URL 和页面组件对应 |
| ECharts | 百度出品的数据可视化库 | 画趋势图、饼图,展示健康数据变化 |
1.3 项目文件结构
D:\APP\
├── backend\ ← 后端 .NET 项目
│ ├── HealthManager.slnx ← 解决方案文件(管理多个项目)
│ ├── start-dev.bat ← 一键启动脚本
│ └── src\
│ ├── HealthManager.Domain\ ← 领域层(实体 + 接口)
│ ├── HealthManager.Application\← 应用层(服务 + DTO)
│ ├── HealthManager.Infrastructure\← 基础设施层(数据库 + JWT)
│ └── HealthManager.WebApi\ ← Web API 层(控制器 + 启动)
├── frontend-patient\ ← 病人 H5 前端
│ └── src\
│ ├── pages\ ← 页面组件
│ ├── services\ ← API 调用函数
│ ├── stores\ ← 状态管理
│ ├── components\ ← 公共组件
│ ├── types\ ← 类型定义
│ ├── utils\ ← 工具函数
│ └── router\ ← 路由配置
├── frontend-doctor\ ← 医生 PC 前端
│ └── src\
│ ├── pages\ ← 页面组件
│ ├── services\ ← API 调用函数
│ ├── stores\ ← 状态管理
│ ├── types\ ← 类型定义
│ └── router\ ← 路由配置
└── data\ ← 数据目录
├── pgdata\ ← PostgreSQL 数据文件
└── minio\ ← MinIO 文件存储
2. 架构设计:什么是"整洁架构"
2.1 为什么分成四层?
传统的写法是把所有代码写在一起(比如把数据库查询、业务逻辑、API 接口写在一个文件里)。这样写久了会越来越乱,改一个地方可能影响其他功能。
整洁架构(Clean Architecture) 的核心思想是:把代码按照职责分成不同的层,内层的代码不依赖外层的代码。
┌──────────────────────────────────────────┐
│ WebApi (接口层) │ ← 接收 HTTP 请求,返回 JSON
│ 接收请求 → 调用服务 → 返回结果 │
├──────────────────────────────────────────┤
│ Application (应用层) │ ← 业务逻辑、数据转换
│ 具体的业务操作:登录、查健康数据等 │
├──────────────────────────────────────────┤
│ Domain (领域层) │ ← 核心:定义"什么是用户"、"什么是健康记录"
│ 实体类(User、HealthRecord...)+ 接口 │
├──────────────────────────────────────────┤
│ Infrastructure (基础设施层) │ ← 和外部系统打交道
│ 数据库操作、JWT 生成、发邮件等 │
└──────────────────────────────────────────┘
依赖方向:WebApi → Application → Domain ← Infrastructure
- 所有层都依赖 Domain(因为 Domain 定了"数据长什么样")
- Infrastructure 实现了 Domain 定义的接口(比如
IJwtProvider)
2.2 每一层的职责
| 层 | 能做什么 | 不能做什么 |
|---|---|---|
| Domain | 定义实体、定义接口 | 不写具体逻辑、不访问数据库 |
| Application | 写业务逻辑、定义 DTO | 不直接访问 HTTP 请求、不直接操作数据库 |
| Infrastructure | 实现接口、操作数据库、生成 JWT | 不处理 HTTP 请求 |
| WebApi | 接收 HTTP 请求、控制权限、返回响应 | 不写业务逻辑、不直接操作数据库 |
3. 后端 — Domain 领域层
这是最核心的一层,定义了"数据长什么样"。就像一个房子的地基。
3.1 项目文件 HealthManager.Domain.csproj
这是 .NET 项目的配置文件,告诉编译器:
- 这是一个类库项目(不是可运行的程序)
- 使用 .NET 10
- 引用了哪些 NuGet 包(这里没有额外依赖,是最纯净的)
3.2 接口 Interfaces/IJwtProvider.cs
namespace HealthManager.Domain.Interfaces;
public interface IJwtProvider
{
string GenerateAccessToken(Guid userId, string name, string role);
string GenerateRefreshToken();
}
这是干什么的? 定义了一个"契约"——任何实现这个接口的类,必须提供两个方法:
GenerateAccessToken:生成访问令牌(30分钟有效,用完后要用刷新令牌换新的)GenerateRefreshToken:生成刷新令牌(长期有效,用来换新的访问令牌)
为什么放在 Domain 层? 因为 Application 层需要使用这个功能,但 Application 层不应该知道具体是怎么实现的(可能是 JWT,也可能是其他方式)。所以 Domain 定义接口,Infrastructure 去实现。
3.3 实体类
实体类 = 数据库表的 C# 映射。每个实体对应数据库中的一张表,类的属性对应表的列。
Entities/User.cs — 用户表
对应数据库表:Users
属性说明:
| 属性 | 类型 | 说明 |
|---|---|---|
| Id | Guid | 唯一标识,全局不重复的 ID(不用自增数字,避免暴露用户量) |
| Role | string | 角色:"patient"(患者)、"doctor"(医生)、"admin"(管理员) |
| Phone | string | 手机号,用于登录(相当于用户名) |
| PasswordHash | string | 密码的 SHA256 哈希值,不存明文 |
| Name | string | 真实姓名 |
| AvatarUrl | string? | 头像图片地址(可空) |
| Gender | string? | 性别 |
| Birthday | DateOnly? | 出生日期 |
| HeightCm | decimal? | 身高(厘米) |
| WeightKg | decimal? | 体重(公斤) |
| MedicalHistory | List<string>? | 病史列表,如 ["高血压", "冠心病"],在数据库存为数组 |
| StentDate | DateOnly? | 支架植入日期(患者特有) |
| StentType | string? | 支架类型(患者特有) |
| Department | string? | 科室(医生特有) |
| Title | string? | 职称,如 "主任医师"(医生特有) |
| Specialty | List<string>? | 擅长领域列表(医生特有) |
| Introduction | string? | 医生简介(医生特有) |
| IsAvailable | bool | 是否可接诊(医生特有) |
| CreatedAt | DateTime | 账号创建时间 |
| UpdatedAt | DateTime | 最后更新时间 |
| IsDeleted | bool | 软删除标记(不真删数据) |
| DeletedAt | DateTime? | 删除时间 |
设计特点:患者和医生共用一张 Users 表,通过 Role 字段区分。患者独有的字段(如病史、支架日期)对医生为空;医生独有的字段(如科室、职称)对患者为空。这叫"单表继承"模式。
导航属性:User 还包含多个"一对多"关系,表示一个用户可以有多个健康记录、多种药物等。
Entities/RefreshToken.cs — 刷新令牌表
对应数据库表:RefreshTokens
干什么的? 用户登录后,后端发两个令牌:
- Access Token:短期有效(30分钟),用于访问 API
- Refresh Token:长期有效(7天),用于在 Access Token 过期后换新的
属性说明:
| 属性 | 类型 | 说明 |
|---|---|---|
| Id | Guid | 主键 |
| UserId | Guid | 属于哪个用户 |
| Token | string | 令牌字符串(唯一的) |
| ExpiresAt | DateTime | 过期时间 |
| CreatedAt | DateTime | 创建时间 |
| RevokedAt | DateTime? | 吊销时间(如果被吊销了就不能再用了) |
Entities/Device.cs — 设备表
对应数据库表:Devices
干什么的? 患者可以绑定智能设备(血压计、手环等),设备数据自动同步。
属性说明:
| 属性 | 类型 | 说明 |
|---|---|---|
| Id | Guid | 主键 |
| UserId | Guid? | 绑定的用户(可空,未绑定时为空) |
| Name | string | 设备名称,如 "欧姆龙血压计" |
| Type | string | 设备类型:血压计、手环、血糖仪、体重秤 |
| MacAddress | string? | MAC 地址 |
| SerialNumber | string? | 序列号 |
| IsBound | bool | 是否已绑定用户 |
| BoundAt | DateTime? | 绑定时间 |
| LastSyncAt | DateTime? | 最后同步时间 |
| BatteryLevel | int? | 电量百分比 |
| CreatedAt | DateTime | 创建时间 |
Entities/HealthRecord.cs — 健康记录表
对应数据库表:HealthRecords
干什么的? 存储用户的健康测量数据(血压、心率、血糖、血氧、体重、步数)。
属性说明:
| 属性 | 类型 | 说明 |
|---|---|---|
| Id | Guid | 主键 |
| UserId | Guid | 属于哪个用户 |
| Type | string | 类型:blood_pressure(血压)、heart_rate(心率)、blood_sugar(血糖)、spo2(血氧)、weight(体重)、steps(步数) |
| Value | JsonDocument | 关键设计:不同测量的数据格式不一样。血压是 {systolic:120, diastolic:80, pulse:72},心率是 {value:72}。用 JSON 列存储,灵活适应不同格式 |
| Unit | string | 单位:mmHg、bpm、mmol/L、%、kg、步 |
| RecordedAt | DateTime | 测量时间 |
| Source | string | 数据来源:manual(手动输入)、device(设备同步)、doctor(医生录入) |
| Notes | string? | 备注 |
| CreatedAt | DateTime | 创建时间 |
为什么 Value 用 JSON 而不是分别建列? 因为不同测量类型的数据结构不同。如果每种类型建一张表,会有很多表。JSON 列让数据库结构简单,同时保持灵活性。
Entities/DietRecord.cs — 饮食记录表
对应数据库表:DietRecords
干什么的? 记录患者的饮食情况。
| 属性 | 类型 | 说明 |
|---|---|---|
| Id | Guid | 主键 |
| UserId | Guid | 属于哪个用户 |
| MealType | string | 餐类型:breakfast、lunch、dinner、snack |
| FoodName | string | 食物名称 |
| Portion | string? | 份量描述 |
| Calories | int? | 卡路里估算 |
| Notes | string? | 备注 |
| RecordedAt | DateOnly | 记录日期 |
| CreatedAt | DateTime | 创建时间 |
Entities/ExerciseRecord.cs — 运动记录表
对应数据库表:ExerciseRecords
干什么的? 记录患者的运动情况。
| 属性 | 类型 | 说明 |
|---|---|---|
| Id | Guid | 主键 |
| UserId | Guid | 属于哪个用户 |
| ExerciseType | string | 运动类型:步行、慢跑、游泳、骑行 |
| DurationMin | int | 运动时长(分钟) |
| Intensity | string? | 强度:low、moderate、high |
| CaloriesBurned | int? | 消耗卡路里 |
| Notes | string? | 备注 |
| RecordedAt | DateOnly | 记录日期 |
| CreatedAt | DateTime | 创建时间 |
Entities/Medication.cs — 药物表
对应数据库表:Medications
干什么的? 存储医生为患者开的药。
| 属性 | 类型 | 说明 |
|---|---|---|
| Id | Guid | 主键 |
| UserId | Guid | 患者 ID |
| DoctorId | Guid? | 开药医生 ID |
| DrugName | string | 药物名称,如 "阿司匹林肠溶片" |
| Dosage | string | 剂量,如 "100mg" |
| Frequency | string | 频率,如 "每日1次" |
| TimeSlots | List<string> | 服药时间点列表,如 ["08:00", "20:00"],存为数组 |
| StartDate | DateOnly | 开始日期 |
| EndDate | DateOnly? | 结束日期(为空表示长期服用) |
| Notes | string? | 备注(如注意事项) |
| Status | string | 状态:active(服用中)、completed(已结束)、stopped(已停用) |
| CreatedAt | DateTime | 创建时间 |
| UpdatedAt | DateTime | 更新时间 |
Entities/MedicationRecord.cs — 服药记录表
对应数据库表:MedicationRecords
干什么的? 记录每一次服药情况(吃了还是没吃)。用来计算"依从率"(有没有按时吃药)。
| 属性 | 类型 | 说明 |
|---|---|---|
| Id | Guid | 主键 |
| MedicationId | Guid | 属于哪个药物 |
| UserId | Guid | 患者 ID |
| TimeSlot | string | 哪个时间点,如 "08:00" |
| TakenAt | DateTime? | 实际服药时间(为空表示没吃) |
| IsTaken | bool | 是否已服用 |
| SkippedReason | string? | 未服用的原因,如 "忘记" |
| CreatedAt | DateTime | 创建时间 |
Entities/Consultation.cs — 咨询表
对应数据库表:Consultations
干什么的? 存储一次医患在线咨询会话。
| 属性 | 类型 | 说明 |
|---|---|---|
| Id | Guid | 主键 |
| PatientId | Guid | 患者 ID |
| DoctorId | Guid | 医生 ID |
| Subject | string? | 咨询主题 |
| Status | string | 状态:active(进行中)、closed(已结束) |
| StartedAt | DateTime | 开始时间 |
| ClosedAt | DateTime? | 关闭时间 |
| ClosedBy | Guid? | 谁关闭的 |
| Summary | string? | 总结 |
| CreatedAt | DateTime | 创建时间 |
Entities/ConsultationMessage.cs — 咨询消息表
对应数据库表:ConsultationMessages
干什么的? 存储咨询中的每一条消息。
| 属性 | 类型 | 说明 |
|---|---|---|
| Id | Guid | 主键 |
| ConsultationId | Guid | 属于哪个咨询 |
| SenderId | Guid | 发送者 ID |
| SenderRole | string | 发送者角色:patient、doctor、system |
| ContentType | string | 内容类型:text、image、file、template |
| Content | string | 消息正文 |
| ImageUrl | string? | 图片地址 |
| FileUrl | string? | 文件地址 |
| IsRead | bool | 对方是否已读 |
| CreatedAt | DateTime | 发送时间 |
Entities/QuickReplyTemplate.cs — 快捷回复模板表
对应数据库表:QuickReplyTemplates
干什么的? 医生可以设置常用回复模板,问诊时一键发送,提高效率。
| 属性 | 类型 | 说明 |
|---|---|---|
| Id | Guid | 主键 |
| DoctorId | Guid | 医生 ID |
| Title | string | 模板标题 |
| Content | string | 模板内容 |
| Category | string? | 分类 |
| SortOrder | int | 排序 |
| CreatedAt | DateTime | 创建时间 |
Entities/Report.cs — 报告表
对应数据库表:Reports
干什么的? 患者上传的检查报告(化验单、心电图等),由医生审核解读。
| 属性 | 类型 | 说明 |
|---|---|---|
| Id | Guid | 主键 |
| PatientId | Guid | 患者 ID |
| DoctorId | Guid? | 解读医生 ID |
| Title | string | 报告标题 |
| Category | string | 类别:血液检查、心电图、影像学、尿液检查 |
| ImageUrls | List<string> | 报告图片地址列表,存为数组 |
| Status | string | 状态:pending(待审核)、interpreting(解读中)、completed(已完成) |
| RiskLevel | string? | 风险等级:normal、attention、abnormal |
| Summary | string? | 解读总结 |
| Suggestions | string? | 医生建议 |
| UploadedBy | Guid | 上传者 ID |
| UploadedAt | DateTime | 上传时间 |
| CompletedAt | DateTime? | 完成时间 |
Entities/ReportItem.cs — 报告项目表
对应数据库表:ReportItems
干什么的? 一份报告包含多个检查项目(比如一份化验单有10项指标)。每个项目存一条。
| 属性 | 类型 | 说明 |
|---|---|---|
| Id | Guid | 主键 |
| ReportId | Guid | 属于哪个报告 |
| ItemName | string | 项目名称,如 "低密度脂蛋白胆固醇" |
| ResultValue | string | 检查结果,如 "3.2" |
| Unit | string? | 单位,如 "mmol/L" |
| ReferenceRange | string? | 正常参考范围,如 "<3.4" |
| IsAbnormal | bool | 是否异常 |
| SortOrder | int | 排序 |
Entities/FollowUp.cs — 随访表
对应数据库表:FollowUps
干什么的? 安排患者复查/随访计划。
| 属性 | 类型 | 说明 |
|---|---|---|
| Id | Guid | 主键 |
| PatientId | Guid | 患者 ID |
| DoctorId | Guid? | 医生 ID |
| Title | string | 随访主题 |
| Description | string? | 描述 |
| ScheduledAt | DateTime | 计划时间 |
| Status | string | 状态:upcoming(将到来)、completed(已完成)、cancelled(已取消)、rescheduled(已改期) |
| Notes | string? | 备注 |
| ReminderEnabled | bool | 是否开启提醒 |
| CreatedAt | DateTime | 创建时间 |
| UpdatedAt | DateTime | 更新时间 |
Entities/Notification.cs — 通知表
对应数据库表:Notifications
干什么的? 各类推送通知。
| 属性 | 类型 | 说明 |
|---|---|---|
| Id | Guid | 主键 |
| UserId | Guid | 接收者 ID |
| Type | string | 类型:medication、followup、consultation、report、system |
| Title | string | 标题 |
| Content | string | 内容 |
| RelatedId | Guid? | 关联的业务 ID(比如关联的咨询 ID) |
| IsRead | bool | 是否已读 |
| ReadAt | DateTime? | 阅读时间 |
| CreatedAt | DateTime | 创建时间 |
4. 后端 — Application 应用层
这一层写所有的业务逻辑。它不操作数据库(通过 DbContext 操作),不接收 HTTP 请求(由 WebApi 层接收并调用它)。
4.1 DTO(数据传输对象)
DTO = Data Transfer Object。它是"前端和后端之间传输数据用的对象"。
为什么要用 DTO 而不是直接把实体类(Entity)返回给前端?
- 安全:实体类可能包含密码哈希等敏感数据,DTO 可以过滤掉
- 灵活:前端需要的数据格式可能和数据库结构不一样,DTO 可以自由组合
- 解耦:数据库结构变了,只要调整 DTO 转换逻辑,前端不受影响
DTOs/AuthDtos.cs — 认证相关 DTO
| DTO | 作用 |
|---|---|
LoginRequest |
前端发来的登录请求:{ phone, smsCode } |
RegisterRequest |
前端发来的注册请求:{ phone, smsCode, name } |
SendSmsRequest |
发验证码请求:{ phone } |
AuthResponse |
登录成功后返回:{ userId, name, role, accessToken, refreshToken } |
UserProfileResponse |
用户完整资料(患者和医生字段都有) |
UpdateProfileRequest |
更新个人资料的请求 |
DTOs/ConsultationDtos.cs — 咨询相关 DTO
| DTO | 作用 |
|---|---|
ConsultationCreateRequest |
创建咨询:{ doctorId, subject } |
ConsultationResponse |
咨询详情 |
SendMessageRequest |
发送消息:{ content, contentType, imageUrl } |
MessageResponse |
消息详情 |
DTOs/FollowUpDtos.cs — 随访相关 DTO
| DTO | 作用 |
|---|---|
FollowUpCreateRequest |
创建随访:{ title, description, scheduledAt, reminderEnabled } |
FollowUpUpdateRequest |
更新随访(所有字段都可空,只更新提供的字段) |
FollowUpResponse |
随访详情 |
DTOs/HealthDtos.cs — 健康记录相关 DTO
| DTO | 作用 |
|---|---|
HealthRecordCreateRequest |
添加健康记录:{ type, valueJson, unit, recordedAt, notes } |
HealthRecordResponse |
单条健康记录 |
HealthStatsResponse |
聚合统计:最新值 + 周平均 + 月平均 + 趋势(上升/下降/稳定) |
DTOs/MedicationDtos.cs — 药物相关 DTO
| DTO | 作用 |
|---|---|
MedicationCreateRequest |
添加药物:{ drugName, dosage, frequency, timeSlots, startDate, endDate, notes } |
MedicationResponse |
药物详情 |
MedicationRecordResponse |
服药记录 |
AdherenceResponse |
依从率:{ rate(%), takenDays, totalDays } |
DTOs/ReportDtos.cs — 报告相关 DTO
| DTO | 作用 |
|---|---|
ReportUploadRequest |
上传报告:{ title, category, imageUrls } |
ReportInterpretRequest |
医生解读报告:{ summary, items, riskLevel, suggestions } |
ReportResponse |
报告详情(含解读结果) |
ReportItemDto |
单个检查项目 |
4.2 服务类
每个服务类负责一个业务领域的所有逻辑。它们通过构造函数注入 AppDbContext(数据库)和需要的其他服务。
Services/AuthService.cs — 认证服务
负责:用户登录、注册、Token 刷新、获取资料、更新资料。
主要方法做的事:
- 登录:根据手机号找用户 → 验证密码(演示版跳过) → 生成 JWT 令牌对 → 保存刷新令牌到数据库 → 返回令牌和用户信息
- 注册:检查手机号是否已注册 → 创建新用户 → 生成令牌对 → 保存 → 返回
- 刷新令牌:找到数据库中的刷新令牌 → 检查是否过期/被吊销 → 生成新令牌对 → 吊销旧令牌 → 保存新令牌
- 获取资料:根据用户 ID 查找用户 → 映射成 UserProfileResponse 返回
- 更新资料:找到用户 → 更新可修改的字段 → 保存
Services/HealthService.cs — 健康数据服务
负责:健康记录的增删查、统计计算。
主要方法做的事:
- 获取记录列表:支持按类型(血压/心率等)、按天数筛选,也可以医生查看指定患者的记录
- 获取最新记录:查某类型最新的一条
- 添加记录:验证 JSON 格式 → 创建 HealthRecord → 保存
- 获取统计:这是最复杂的方法。查出每种类型的所有记录 → 计算最新值、7天平均、30天平均 → 比较趋势(上升/下降/稳定)
Services/MedicationService.cs — 药物管理服务
负责:药物的增删查、服药打卡、依从率计算。
主要方法做的事:
- 列出药物:查出用户的所有药物,支持医生查看指定患者的
- 添加药物:创建新药物 → 为所有时间段生成服药记录(预生成未来几天的记录)
- 获取详情:根据 ID 查询单条药物
- 获取服药记录:查某药物的所有服药记录
- 标记已服用:用户点"已服药" → 更新对应时间和时间点的服药记录为已服用
- 计算依从率:已服用次数 / 总需服用次数 × 100%
Services/ConsultationService.cs — 咨询问诊服务
负责:列出医生、创建咨询、收发消息。
主要方法做的事:
- 列出可用医生:查出所有
Role=doctor且IsAvailable=true的用户 - 创建咨询:患者选择医生 → 检查是否已有活跃咨询(有的话复用) → 创建新 Consultation
- 获取咨询列表:患者看自己的,医生看分配给自己的
- 发送消息:创建 ConsultationMessage → 保存 → 通过 SignalR 实时推送给对方
- 获取消息:查某咨询的所有消息,按时间排序
Services/PatientService.cs — 患者管理服务(医生用)
负责:医生查看患者列表和详情。
主要方法做的事:
- 患者列表:查出所有
Role=patient的用户,支持按姓名/手机号搜索,支持分页 - 患者详情:根据 ID 查出患者完整资料
Services/ReportService.cs — 报告管理服务
负责:报告的增删查、医生解读。
主要方法做的事:
- 上传报告:患者上传检查报告图片 → 创建 Report 记录(状态为 pending)
- 列出报告:患者看自己的,医生可看所有或筛选特定患者
- 待审核列表:列出所有状态为 pending 的报告(医生用)
- 报告详情:查询单条报告及其所有 ReportItem
- 解读报告:医生提交解读 → 保存总结、风险等级、建议 → 创建所有 ReportItem → 更新报告状态为 completed
Services/FollowUpService.cs — 随访管理服务
负责:随访计划的增删改查。
主要方法做的事:
- 创建随访:患者或医生创建随访计划
- 列出随访:患者看自己的,医生看分配给自己的
- 更新随访:医生修改随访信息(改时间、改状态、加备注)
Services/NotificationService.cs — 通知服务
负责:通知的查询、已读标记、创建。
主要方法做的事:
- 列出通知:查出用户所有通知,按时间倒序
- 未读计数:统计
IsRead=false的通知数量 - 标记已读:单条或全部标为已读
- 创建通知:其他服务调用此方法创建通知(如"医生回复了你的咨询")
5. 后端 — Infrastructure 基础设施层
这一层和"外部世界"打交道:数据库、JWT 加密等具体技术实现。
5.1 Data/AppDbContext.cs — 数据库上下文
这是整个项目中最重要的基础设施文件。它是 EF Core 的"总管家"。
做了什么:
-
定义 15 个 DbSet:每个 DbSet 对应一张数据库表
public DbSet<User> Users => Set<User>(); public DbSet<HealthRecord> HealthRecords => Set<HealthRecord>(); // ... 共 15 个 -
在
OnModelCreating中配置表结构:- JSON 列映射:HealthRecord.Value 类型是
JsonDocument,配置为 PostgreSQL 的jsonb类型 - 数组列映射:
List<string>类型的属性(如 MedicalHistory、TimeSlots、Specialty)映射为 PostgreSQL 的text[]类型 - 索引配置:为常查询的列创建数据库索引(加速查询)
Users.Phone建唯一索引(手机号不能重复)Users.Role建普通索引(经常按角色查询)HealthRecords.UserId + Type建复合索引("查某用户的血压数据"这种查询)Consultations.Status建索引(查活跃的咨询)- 等等...
- 软删除过滤器:User 有
IsDeleted标记,配置全局过滤器自动排除已删除的用户
- JSON 列映射:HealthRecord.Value 类型是
5.2 Data/DataSeeder.cs — 种子数据
干什么的? 在数据库首次创建时,自动插入演示数据,方便开发和测试。
插入了什么数据:
| 数据 | 数量 | 说明 |
|---|---|---|
| 患者 | 2个 | 张明(男,60岁,高血压+冠心病+PCI术后)、李芳(女,53岁,高血脂+冠心病+PCI术后) |
| 医生 | 2个 | 王建国(主任医师,心血管内科)、陈雪梅(副主任医师,心血管内科) |
| 健康记录 | 31天×3条/天 ≈ 93条 | 患者1的血压、心率、体重数据(有随机波动) |
| 药物 | 4种 | 阿司匹林、替格瑞洛、瑞舒伐他汀、美托洛尔 |
| 服药记录 | 4药×8天×1~2次/天 ≈ 50条 | 近8天的服药情况,约90%依从率 |
| 咨询 | 1个 | 张明咨询王建国关于术后复查 |
| 咨询消息 | 4条 | 一次完整的医患对话 |
| 通知 | 4条 | 用药提醒、复查提醒、医生回复、健康提示 |
关键技术点:
- 所有演示密码都是
demo123,使用 SHA256 哈希存储 - 使用固定随机种子
new Random(42),每次生成的数据一样 - 时间戳使用
DateTimeKind.Utc,因为 PostgreSQL 的timestamp with time zone要求 UTC 时间 - 健康数据的 Value 使用
JsonDocument.Parse()创建 JSON 格式
5.3 Services/JwtProvider.cs — JWT 令牌提供者
干什么的? 实现了 Domain 层定义的 IJwtProvider 接口,生成 JWT 令牌。
两个方法:
-
GenerateAccessToken:生成访问令牌- 从配置文件读取密钥(
appsettings.json的Jwt:Secret) - 把用户 ID、姓名、角色打包进令牌的"负载"(Claims)
- 使用 HMAC-SHA256 算法签名
- 设置 30 分钟过期
- 返回签好名的 JWT 字符串
- 从配置文件读取密钥(
-
GenerateRefreshToken:生成刷新令牌- 使用加密安全的随机数生成器产生 64 字节随机数据
- 转成 Base64 字符串返回(就是一个随机字符串,不包含任何信息)
JWT 是怎么工作的? 可以理解为"盖了章的通行证":
- 服务器签发令牌时"盖章"(用密钥签名)
- 客户端每次请求带着令牌
- 服务器验证"章"是真的 → 放行
- 如果令牌过期了 → 拒绝,客户端用刷新令牌换新的
6. 后端 — WebApi 接口层
这是最外层,直接接收 HTTP 请求,返回 JSON 响应。
6.1 Program.cs — 应用程序入口
这是整个后端启动的唯一入口文件。它配置了应用运行所需的一切。
主要做的事(按顺序):
- 创建 WebApplication:初始化 .NET 应用
- 注册服务(依赖注入):
- 注册
AppDbContext:告诉 EF Core 用 PostgreSQL,连接字符串从配置文件读 - 注册 JWT 认证:配置验证规则(密钥、签发者、过期时间等)
- 注册 Swagger:配置 API 文档,支持 JWT 认证测试
- 注册 SignalR:配置实时通信
- 注册 CORS:允许前端跨域访问(开发阶段允许所有来源)
- 注册业务服务:
AuthService、HealthService等,让控制器能使用 - 注册
IJwtProvider→JwtProvider:告诉框架"当需要 IJwtProvider 时,用 JwtProvider"
- 注册
- 配置中间件管道:
- 异常处理 → CORS → 认证 → 授权 → 映射控制器 → 映射 SignalR Hub
- 初始化数据库:
- 确保数据库和表已创建(
EnsureCreatedAsync) - 调用
DataSeeder.SeedAsync()插入演示数据
- 确保数据库和表已创建(
- 启动应用:开始监听 HTTP 请求
6.2 控制器
控制器 = 处理 HTTP 请求的"接待员"。每个控制器负责一组相关的 API。
Controllers/AuthController.cs — 认证接口
路由前缀:api/auth
| 端点 | 方法 | 认证 | 做什么 |
|---|---|---|---|
POST /api/auth/send-sms |
公开 | ✗ | 模拟发送短信验证码(演示版直接返回成功) |
POST /api/auth/login |
公开 | ✗ | 手机号+验证码登录,返回 JWT 令牌对 |
POST /api/auth/register |
公开 | ✗ | 注册新患者账号 |
POST /api/auth/refresh |
公开 | ✗ | 用刷新令牌换新的访问令牌对 |
GET /api/auth/me |
需登录 | ✓ | 获取当前登录用户的完整资料 |
PUT /api/auth/me |
需登录 | ✓ | 更新当前用户的个人资料 |
细节:登录接口接收 { phone, smsCode },验证码固定为 1234(演示版)。成功后返回 { userId, name, role, accessToken, refreshToken }。
Controllers/HealthController.cs — 健康数据接口
路由前缀:api/health-records
| 端点 | 方法 | 认证 | 做什么 |
|---|---|---|---|
GET /api/health-records |
需登录 | ✓ | 查询健康记录,可按类型和天数筛选 |
GET /api/health-records/stats |
需登录 | ✓ | 获取所有类型的统计(最新值、周/月平均、趋势) |
GET /api/health-records/latest/{type} |
需登录 | ✓ | 获取某类型最新一条记录 |
POST /api/health-records |
需登录 | ✓ | 添加一条健康记录 |
细节:查询支持 ?type=blood_pressure&days=30 参数。医生可以加 ?patientId=xxx 查看指定患者的数据。
Controllers/MedicationController.cs — 药物管理接口
路由前缀:api/medications
| 端点 | 方法 | 认证 | 做什么 |
|---|---|---|---|
GET /api/medications |
需登录 | ✓ | 列出所有药物 |
POST /api/medications |
需登录 | ✓ | 添加新药物 |
GET /api/medications/{id} |
需登录 | ✓ | 获取药物详情 |
GET /api/medications/{id}/records |
需登录 | ✓ | 获取该药物的所有服药记录 |
POST /api/medications/{id}/take |
需登录 | ✓ | 标记某时间点已服药 |
GET /api/medications/{id}/adherence |
需登录 | ✓ | 获取该药物的依从率 |
细节:take 接口接收 { timeSlot: "08:00" },在相应的时间点记录服药。
Controllers/ConsultationController.cs — 在线问诊接口
路由前缀:api/consultations
| 端点 | 方法 | 认证 | 做什么 |
|---|---|---|---|
GET /api/consultations/doctors |
需登录 | ✓ | 列出可接诊的医生 |
GET /api/consultations |
需登录 | ✓ | 列出我的咨询(患者看自己的,医生看分配给自己的) |
POST /api/consultations |
需登录 | ✓ | 发起新咨询 |
GET /api/consultations/{id}/messages |
需登录 | ✓ | 获取咨询的所有消息 |
POST /api/consultations/{id}/messages |
需登录 | ✓ | 发送消息 |
Controllers/ReportController.cs — 报告管理接口
路由前缀:api/reports
| 端点 | 方法 | 认证 | 权限 | 做什么 |
|---|---|---|---|---|
GET /api/reports |
需登录 | ✓ | — | 列出报告 |
GET /api/reports/pending |
需登录 | ✓ | 仅医生 | 列出待审核的报告 |
GET /api/reports/{id} |
需登录 | ✓ | — | 获取报告详情 |
POST /api/reports |
需登录 | ✓ | — | 上传报告 |
POST /api/reports/{id}/interpret |
需登录 | ✓ | 仅医生 | 医生提交解读 |
Controllers/FollowUpController.cs — 随访管理接口
路由前缀:api/follow-ups
| 端点 | 方法 | 认证 | 权限 | 做什么 |
|---|---|---|---|---|
GET /api/follow-ups |
需登录 | ✓ | — | 列出随访计划 |
GET /api/follow-ups/{id} |
需登录 | ✓ | — | 获取随访详情 |
POST /api/follow-ups |
需登录 | ✓ | — | 创建随访 |
PUT /api/follow-ups/{id} |
需登录 | ✓ | 仅医生 | 更新随访 |
Controllers/PatientController.cs — 患者管理接口(医生专用)
路由前缀:api/patients
| 端点 | 方法 | 认证 | 权限 | 做什么 |
|---|---|---|---|---|
GET /api/patients |
需登录 | ✓ | 仅医生 | 列出患者(支持搜索和分页) |
GET /api/patients/{id} |
需登录 | ✓ | 仅医生 | 获取患者详情 |
Controllers/NotificationController.cs — 通知接口
路由前缀:api/notifications
| 端点 | 方法 | 认证 | 做什么 |
|---|---|---|---|
GET /api/notifications |
需登录 | ✓ | 列出所有通知 |
GET /api/notifications/unread-count |
需登录 | ✓ | 获取未读数量 |
PUT /api/notifications/{id}/read |
需登录 | ✓ | 标记单条已读 |
PUT /api/notifications/read-all |
需登录 | ✓ | 全部标记已读 |
6.3 Hubs/ChatHub.cs — SignalR 实时聊天枢纽
干什么的? 实现医生和患者的实时消息推送。
技术原理:
- 传统 HTTP:客户端问 → 服务器答(一问一答,服务器不能主动推送)
- SignalR:建立 WebSocket 长连接 → 服务器可以随时推消息给客户端
主要方法:
JoinConsultation(consultationId):加入一个咨询的"聊天室"LeaveConsultation(consultationId):离开聊天室SendMessage(consultationId, content):发送消息 → 广播给聊天室内的其他人
流程:
- 患者发消息 →
SendMessage→ 保存到数据库 → 广播给医生 - 医生收到实时通知 → 可以在聊天页面看到新消息
6.4 配置文件
appsettings.json — 主配置文件
包含:
- PostgreSQL 连接字符串:
Host=localhost;Port=5432;Database=HealthManager;Username=postgres;Password=postgres123 - JWT 配置:密钥、签发者、受众、过期时间
- Redis 连接字符串
- MinIO 连接字符串
appsettings.Development.json — 开发环境配置
覆盖主配置中的某些值,仅在开发环境生效。
Properties/launchSettings.json — 启动配置
定义 Visual Studio / dotnet run 启动时的行为:
- 使用
http://localhost:5000地址 - 环境设为 Development
- 自动打开 Swagger 页面
7. 病人端前端 (frontend-patient)
移动端 H5 应用,患者用手机浏览器打开。
7.1 技术选型
| 库 | 版本 | 做什么 |
|---|---|---|
| React | 19 | UI 框架 |
| TypeScript | 6 | 类型安全 |
| Vite | 8 | 构建工具 |
| Zustand | 5 | 状态管理 |
| React Router | 7 | 路由 |
| ECharts | 6 | 图表 |
| Framer Motion | 12 | 动画 |
| dayjs | 2 | 日期处理 |
7.2 文件结构说明
src/
├── main.tsx ← React 应用入口,把 App 组件挂到页面上
├── App.tsx ← 根组件,渲染路由
├── router/ ← 路由配置
│ ├── index.tsx ← 定义所有 URL 和页面的映射关系
│ └── AuthGuard.tsx ← 路由守卫:没登录就跳转到登录页
├── stores/ ← 状态管理
│ ├── auth.store.ts ← 认证状态:用户信息、令牌、登录/登出
│ └── notification.store.ts ← 通知状态:通知列表、未读数
├── services/ ← API 调用
│ ├── api-client.ts ← 底层 HTTP 请求(封装 fetch)
│ ├── auth.service.ts ← 认证 API 调用
│ ├── health.service.ts ← 健康数据 API 调用
│ ├── medication.service.ts ← 药物 API 调用
│ ├── consultation.service.ts ← 咨询 API 调用
│ ├── report.service.ts ← 报告 API 调用
│ ├── followup.service.ts ← 随访 API 调用
│ ├── notification.service.ts ← 通知 API 调用
│ ├── exercise-diet.service.ts ← 运动/饮食 API 调用
│ └── device.service.ts ← 设备 API 调用(目前部分用假数据)
├── types/ ← TypeScript 类型定义
│ ├── user.ts ← 用户相关类型
│ ├── health.ts ← 健康数据相关类型
│ ├── medication.ts ← 药物相关类型
│ ├── consultation.ts ← 咨询相关类型
│ ├── report.ts ← 报告相关类型
│ ├── followup.ts ← 随访相关类型
│ ├── notification.ts ← 通知相关类型
│ ├── device.ts ← 设备相关类型
│ ├── calendar.ts ← 日历相关类型
│ ├── exercise-diet.ts ← 运动/饮食相关类型
│ └── index.ts ← 统一导出所有类型
├── utils/ ← 工具函数
│ ├── format.ts ← 日期、数字格式化
│ ├── storage.ts ← localStorage 封装
│ ├── validator.ts ← 输入验证(手机号、验证码格式)
│ └── constants.ts ← 常量(测量类型、导航项、健康贴士、运动推荐等)
├── hooks/ ← 自定义 Hooks
│ ├── useAuth.ts ← 获取认证状态的便捷 Hook
│ └── useCountdown.ts ← 短信验证码倒计时
├── components/ ← 公共组件
│ ├── common/ ← 通用 UI 组件
│ │ ├── Button.tsx ← 按钮(主按钮/次按钮/文字按钮,加载中状态)
│ │ ├── Card.tsx ← 卡片容器
│ │ ├── Badge.tsx ← 角标(红点/数字)
│ │ ├── Input.tsx ← 输入框(带标签和错误提示)
│ │ ├── Toast.tsx ← 轻提示(成功/失败,自动消失)
│ │ └── Empty.tsx ← 空状态占位
│ ├── layout/ ← 布局组件
│ │ ├── AppLayout.tsx ← 主布局:TabBar + 内容区
│ │ ├── StackLayout.tsx ← 子页面布局:返回按钮 + 内容区(无 TabBar)
│ │ ├── TabBar.tsx ← 底部导航栏(首页/健康/服务/我的)
│ │ └── PageHeader.tsx ← 页面标题栏(带返回按钮)
│ └── charts/ ← 图表组件
│ ├── LineChart.tsx ← 折线图(展示趋势)
│ ├── BarChart.tsx ← 柱状图
│ └── PieChart.tsx ← 饼图(展示依从率等)
├── pages/ ← 页面组件
│ ├── auth/ ← 认证页面
│ │ ├── LoginPage.tsx ← 登录页
│ │ └── RegisterPage.tsx ← 注册页
│ ├── home/ ← 首页
│ │ ├── HomePage.tsx ← 仪表盘首页
│ │ └── DeviceBindingPage.tsx ← 设备绑定页
│ ├── health/ ← 健康模块
│ │ ├── HealthHubPage.tsx ← 健康中心入口
│ │ ├── HealthRecordListPage.tsx ← 健康记录列表
│ │ ├── ManualEntryPage.tsx ← 手动输入健康数据
│ │ ├── TrendChartPage.tsx ← 趋势图
│ │ └── HealthCalendarPage.tsx ← 健康日历
│ ├── medication/ ← 用药模块
│ │ ├── MedicationListPage.tsx ← 药物列表
│ │ ├── MedicationEditPage.tsx ← 添加/编辑药物
│ │ └── MedicationDetailPage.tsx ← 药物详情(含依从率饼图)
│ ├── services/ ← 服务模块
│ │ ├── ServicesHubPage.tsx ← 服务中心
│ │ ├── DoctorListPage.tsx ← 医生列表
│ │ ├── ChatPage.tsx ← 在线问诊聊天
│ │ ├── ReportListPage.tsx ← 报告列表
│ │ ├── ReportUploadPage.tsx ← 上传报告
│ │ ├── ReportDetailPage.tsx ← 报告详情
│ │ ├── FollowUpListPage.tsx ← 随访列表
│ │ └── FollowUpEditPage.tsx ← 添加/编辑随访
│ ├── exercise-diet/ ← 运动饮食模块
│ │ └── ExerciseDietPage.tsx ← 运动/饮食记录和推荐
│ ├── profile/ ← 个人中心
│ │ ├── ProfilePage.tsx ← 个人资料页
│ │ ├── SettingsPage.tsx ← 设置页
│ │ └── staticPages.tsx ← 静态页面(通知设置、隐私政策、关于)
│ └── notifications/ ← 通知
│ └── NotificationListPage.tsx ← 通知列表
└── mock/ ← 假数据(开发用,部分已被真实 API 替代)
├── index.ts ← Mock 工具(延迟模拟、ID 生成)
├── users.ts ← 假用户数据
├── health-records.ts ← 假健康数据
├── medications.ts ← 假药物数据
├── devices.ts ← 假设备数据
├── consultations.ts ← 假咨询数据
├── followups.ts ← 假随访数据
├── reports.ts ← 假报告数据
├── notifications.ts ← 假通知数据
└── exercise-diet.ts ← 假运动饮食数据
7.3 关键文件详细说明
services/api-client.ts — HTTP 请求客户端
干什么的? 所有前端 API 调用的基础。封装了原生的 fetch 函数。
核心逻辑:
- 每次请求自动从 localStorage 读取 JWT 令牌,加到请求头的
Authorization: Bearer xxx - 发送 HTTP 请求到
http://localhost:5000 - 如果服务器返回 401(未认证),自动清除本地缓存(令牌过期,需要重新登录)
- 把服务器返回的 JSON 统一包装成
{ code, data, message }格式 - 如果服务器返回错误,抛出异常
四个方法:get、post、put、del,对应 HTTP 的 GET、POST、PUT、DELETE。
stores/auth.store.ts — 认证状态管理
干什么的? 管理"用户是否登录"这个核心状态。
状态数据:
user:当前用户信息(姓名、手机号、角色等)token:JWT 访问令牌isAuthenticated:是否已登录(根据 token 是否存在判断)
操作方法:
login(phone, code):调登录 API → 保存令牌和用户信息register(phone, code, name):调注册 API → 保存令牌和用户信息logout():清除所有数据,回到登录页updateProfile(data):更新用户资料
持久化:使用 Zustand 的 persist 中间件,数据自动存到 localStorage 的 hrt_auth 键。即使刷新页面或关闭浏览器,登录状态也不会丢。
router/index.tsx — 路由配置
干什么的? 定义了"哪个 URL 显示哪个页面组件"。
路由分为三类:
-
公开路由(不需要登录):
/login→ 登录页/register→ 注册页
-
Tab 路由(需要登录,底部有导航栏):
/home→ 首页仪表盘/health→ 健康中心/services→ 服务中心/profile→ 个人中心
-
Stack 路由(需要登录,无底部导航栏,有返回按钮):
/health/records→ 健康记录列表/health/records/add→ 手动录入/health/trends/:type→ 趋势图/services/consultation→ 医生列表/services/consultation/chat/:doctorId→ 聊天- ... 等等
AuthGuard 组件:包裹在需要登录的路由外面。如果用户没登录访问这些页面,自动跳转到 /login。
各页面说明
登录页 pages/auth/LoginPage.tsx
- 输入手机号 + 短信验证码
- 点击"获取验证码"按钮触发 60 秒倒计时
- 输入框中自动填充演示账号手机号
13800138000 - 验证码任意输入(演示版不验证)
- 登录成功后跳到首页
首页 pages/home/HomePage.tsx
- 健康概览卡片:显示最新血压和心率数据
- 快捷操作按钮(6个):录入健康数据、用药打卡、在线问诊、查看报告、运动饮食、我的随访
- 健康小贴士:随机展示一条心血管健康建议
- 通知角标:右上角铃铛图标,有未读通知时显示红点
健康中心 pages/health/HealthHubPage.tsx
- 6 种测量类型的卡片:血压、心率、血糖、血氧、体重、步数
- 点击任一卡片进入该类别的记录列表
- 底部链接:健康日历、用药管理、运动饮食
手动录入 pages/health/ManualEntryPage.tsx
- 根据测量类型显示不同的输入表单
- 血压:收缩压 + 舒张压 + 心率,三个输入框
- 其他类型:单个数值输入框
- 日期和时间选择器
趋势图 pages/health/TrendChartPage.tsx
- 使用 ECharts 折线图
- 可选择 7天 / 14天 / 30天 / 90天 范围
- 血压类型同时显示收缩压和舒张压两条线
- 收缩压有 140 的红色警戒线
药物列表 pages/medication/MedicationListPage.tsx
- 分"服用中"和"已结束"两个标签页
- 每种药显示名称、剂量、服药时间
- 右下角浮动按钮,点击添加新药
药物详情 pages/medication/MedicationDetailPage.tsx
- 药物完整信息:名称、剂量、频率、时间点、开始日期、注意事项
- 依从率饼图:绿色=已服用,灰色=未服用
- 近30天服药日历视图
医生列表 pages/services/DoctorListPage.tsx
- 按科室筛选(全部/心血管内科/心外科等)
- 每个医生卡片:头像、姓名、职称、科室、擅长领域、简介
- 点击进入聊天页面
聊天页 pages/services/ChatPage.tsx
- 对话气泡界面
- 患者消息靠右(绿色气泡),医生消息靠左(白色气泡)
- 底部输入框 + 发送按钮
- 新消息自动滚到底部
报告列表 pages/services/ReportListPage.tsx
- 分"全部/待审核/已解读"三个标签
- 每项显示标题、类别、状态、上传时间
- 点击查看详情
报告详情 pages/services/ReportDetailPage.tsx
- 报告基本信息
- 如果有解读结果,显示风险等级、总结、各检查项目、医生建议
个人中心 pages/profile/ProfilePage.tsx
- 用户头像、姓名、手机号
- 身体数据展示(身高、体重、病史)
- 菜单列表:用药管理、我的通知、设备绑定、设置、关于
- 退出登录按钮
7.4 状态管理方案
病人端有两个 Zustand Store:
| Store | 存储键 | 持久化 | 内容 |
|---|---|---|---|
auth.store |
hrt_auth |
✓ localStorage | user, token, isAuthenticated |
notification.store |
— | ✗ 内存 | notifications[], unreadCount, loading |
为什么通知不持久化? 通知数据变化频繁,每次打开应用重新从服务器获取最新的即可。
8. 医生端前端 (frontend-doctor)
PC 端 Web 应用,医生在电脑浏览器中使用。
8.1 文件结构
src/
├── main.tsx ← 应用入口
├── App.tsx ← 根组件
├── router/
│ ├── index.tsx ← 路由配置(定义 URL 和页面映射)
│ └── AuthGuard.tsx ← 路由守卫
├── stores/
│ └── auth.store.ts ← 认证状态(唯一的状态管理)
├── services/
│ └── api-client.ts ← HTTP 客户端(封装 fetch)
├── types/
│ └── index.ts ← 所有类型定义
├── components/
│ └── layout/
│ └── DoctorLayout.tsx ← 主布局:侧边栏 + 内容区
└── pages/
├── auth/
│ └── LoginPage.tsx ← 登录页
├── dashboard/
│ └── DashboardPage.tsx ← 工作台首页
├── patients/
│ ├── PatientListPage.tsx ← 患者列表
│ └── PatientDetailPage.tsx ← 患者详情
├── consultations/
│ ├── ConsultationListPage.tsx ← 咨询列表
│ └── ChatPage.tsx ← 聊天页
├── reports/
│ ├── ReportListPage.tsx ← 报告列表
│ └── ReportDetailPage.tsx ← 报告解读
├── followups/
│ ├── FollowUpListPage.tsx ← 随访列表
│ └── FollowUpEditPage.tsx ← 创建/编辑随访
└── settings/
└── ProfilePage.tsx ← 个人资料设置
8.2 关键文件详细说明
components/layout/DoctorLayout.tsx — 主布局
干什么的? 医生端所有页面的"壳"。左边是固定的侧边栏,右边是页面内容。
侧边栏内容:
- Logo/标题:"健康管理平台"
- 导航菜单(5项):
- 📊 工作台 →
/dashboard - 👥 患者管理 →
/patients - 💬 在线问诊 →
/consultations - 📋 报告审核 →
/reports - 📅 随访管理 →
/follow-ups
- 📊 工作台 →
- 底部个人信息:医生名字、科室、职称
- 退出登录按钮
技术细节:左侧侧边栏宽度 220px,深色背景(#1a1a2e)。右侧内容区用 React Router 的 <Outlet /> 渲染子路由页面。使用 NavLink 组件高亮当前活跃的菜单项。
stores/auth.store.ts — 认证状态
和病人端的区别:
- 存储键是
doc_auth(不是hrt_auth) - 登录后验证角色必须是
doctor,如果患者尝试登录医生端会报错"仅限医生账号登录" - 登录成功后额外调一次
GET /api/auth/me获取医生完整资料(科室、职称等)
router/index.tsx — 路由配置
| 路由 | 页面 | 说明 |
|---|---|---|
/login |
LoginPage | 登录(公开,无需认证) |
/ |
→ 重定向到 /dashboard |
— |
/dashboard |
DashboardPage | 工作台首页 |
/patients |
PatientListPage | 患者列表 |
/patients/:id |
PatientDetailPage | 患者详情 + 健康数据 |
/consultations |
ConsultationListPage | 咨询列表 |
/consultations/:id |
ChatPage | 聊天页 |
/reports |
ReportListPage | 报告列表 |
/reports/:id |
ReportDetailPage | 报告详情 + 解读 |
/follow-ups |
FollowUpListPage | 随访列表 |
/follow-ups/:id/edit |
FollowUpEditPage | 新建/编辑随访 |
/profile |
ProfilePage | 个人设置 |
所有路由(除 /login)都包裹在 <AuthGuard> + <DoctorLayout> 中,确保必须登录才能访问。
各页面详细说明
登录页 pages/auth/LoginPage.tsx
- 自动填充演示医生账号手机号
13700137000 - 输入验证码(任意输入即可,演示版不验证)
- 成功后跳到工作台
工作台 pages/dashboard/DashboardPage.tsx
- 顶部:问候语 + 医生姓名
- 4 个统计卡片(一排显示):
- 患者总数
- 活跃咨询数
- 待审核报告数
- 今日随访数
- 快捷操作区域:查看患者、查看咨询、审核报告、管理随访
- 今日任务列表:列出当天的随访安排
患者列表 pages/patients/PatientListPage.tsx
- 表格展示:姓名、手机号、性别、病史、支架日期
- 顶部搜索框,支持按姓名或手机号过滤
- 点击"查看详情"进入患者详情页
患者详情 pages/patients/PatientDetailPage.tsx
- 基本信息区:姓名、手机号、性别、出生日期、身高体重
- 病史区:疾病列表、支架日期、支架类型
- 健康数据区:最近30天的血压、心率、血糖等数据卡片,显示最新值和趋势
咨询列表 pages/consultations/ConsultationListPage.tsx
- 显示分配给当前医生的所有咨询
- 每条:患者姓名、咨询主题、状态标签(进行中/已结束)、开始时间
- 点击进入聊天页
聊天页 pages/consultations/ChatPage.tsx
- 和病人端类似的对话界面
- 医生消息靠右(蓝色气泡),患者消息靠左(白色气泡)
- 输入框 + 发送按钮,也支持 Enter 键发送
- 发送后自动滚到底部
报告列表 pages/reports/ReportListPage.tsx
- 表格展示:患者姓名、报告标题、类别、状态(待审核/已完成)、上传日期、操作
- 点击"查看"进入报告详情
报告详情 + 解读 pages/reports/ReportDetailPage.tsx
- 上半部分:报告的基本信息(标题、类别、状态、上传时间)
- 如果有上传的图片链接,显示链接
- 下半部分:解读表单
- 解读结果文本框
- 提交后报告状态变为"已完成"
随访列表 pages/followups/FollowUpListPage.tsx
- 每条随访显示:标题、患者姓名、计划时间、状态(待随访/已完成/已错过)
- 右上角"+ 新建随访"按钮
- 每条旁边有"编辑"按钮
新建/编辑随访 pages/followups/FollowUpEditPage.tsx
- 如果路由参数
:id为"new":创建新随访(POST) - 否则:编辑已有随访(PUT),先预填现有数据
- 表单字段:标题、患者(下拉选择)、计划时间、备注
- 保存后跳转回随访列表
个人设置 pages/settings/ProfilePage.tsx
- 预填当前医生的姓名、手机号(不可修改)、科室、职称、简介
- 修改后保存,同时更新本地缓存
8.3 状态管理
医生端只有一个 Zustand Store(auth.store),比病人端更简单。因为医生端的核心需求是"管理数据",不像患者端有通知等高频变化的状态。
8.4 样式方案
医生端使用内联样式(inline style),每个组件直接在 JSX 中写 style={{...}}。这样做的好处是:
- 样式和组件紧密绑定,不会出现样式冲突
- 不需要额外的 CSS 文件
- 适合组件数量不多的项目
全局基础样式在 src/index.css 中定义(CSS 变量、字体、基础布局)。
9. 数据库表结构
9.1 所有表一览
| 表名 | 用途 | 主要查询场景 |
|---|---|---|
Users |
用户(患者+医生+管理员) | 按手机号查、按角色查、按姓名搜索 |
RefreshTokens |
JWT 刷新令牌 | 按令牌字符串查、按用户查 |
Devices |
智能设备 | 按用户查绑定的设备 |
HealthRecords |
健康测量数据 | 按用户+类型查、按时间范围查、取最新值 |
DietRecords |
饮食记录 | 按用户+日期查 |
ExerciseRecords |
运动记录 | 按用户+日期查 |
Medications |
药物方案 | 按用户查、按状态查、按医生查 |
MedicationRecords |
服药记录 | 按药物查、按用户+时间查 |
Consultations |
咨询会话 | 按患者查、按医生查、按状态查 |
ConsultationMessages |
咨询消息 | 按咨询查、按发送者查 |
QuickReplyTemplates |
快捷回复模板 | 按医生查 |
Reports |
检查报告 | 按患者查、按医生查、按状态查 |
ReportItems |
报告检查项 | 按报告查 |
FollowUps |
随访计划 | 按患者查、按医生查、按计划时间查 |
Notifications |
通知 | 按用户查、按已读/未读查 |
9.2 重要索引
数据库索引就像书的目录,能大幅加速查询。
| 表 | 索引 | 加速的查询 |
|---|---|---|
Users |
Phone (唯一索引) | 登录时按手机号查找用户 |
Users |
Role | 列出所有医生/患者 |
HealthRecords |
UserId + Type | 查某人的血压数据 |
HealthRecords |
RecordedAt | 按时间排序/筛选 |
Medications |
UserId | 查某人的药物 |
Medications |
Status | 查活跃的药物 |
MedicationRecords |
UserId + CreatedAt | 查最近的服药记录 |
Consultations |
Status | 查活跃的咨询 |
Consultations |
PatientId, DoctorId | 查某人参与的咨询 |
Reports |
Status | 查待审核的报告 |
FollowUps |
ScheduledAt | 按预约时间排序 |
Notifications |
UserId + IsRead | 查某人的未读通知 |
RefreshTokens |
Token (唯一) | 刷新令牌时查找 |
9.3 特殊数据类型
PostgreSQL 有一些 MySQL 没有的特殊类型,本项目充分利用了它们:
| PostgreSQL 类型 | 对应 C# 类型 | 使用场景 |
|---|---|---|
uuid |
Guid |
所有主键 |
jsonb |
JsonDocument |
HealthRecord.Value(灵活存储不同格式的测量数据) |
text[] |
List<string> |
MedicalHistory、TimeSlots、Specialty、ImageUrls |
timestamp with time zone |
DateTime (UTC) |
所有时间戳字段 |
date |
DateOnly |
出生日期、开始日期、记录日期 |
10. 常见问题 FAQ
Q1: 为什么所有 ID 都用 Guid 而不是自增数字?
- 安全:自增 ID 会暴露数据量(用户可以猜 ID 访问别人的数据)
- 分布式友好:如果将来要多台服务器,Guid 不会冲突
- 前端可以预生成:不需要等数据库返回 ID
Q2: 为什么患者和医生放在同一张 Users 表?
- 简化设计:登录逻辑完全一样
- 通过
Role字段区分权限 - 用"不用的字段留空"的方式处理差异(患者字段对医生为空,反之亦然)
Q3: JWT 令牌过期了怎么办?
Access Token 有效期只有 30 分钟。过期后,前端自动用 Refresh Token 去换新的 Access Token。如果 Refresh Token 也过期了(7天后),用户需要重新登录。
Q4: 健康数据的 Value 为什么用 JSON?
不同测量类型数据结构不同:
- 血压:
{systolic: 120, diastolic: 80, pulse: 72} - 心率:
{value: 72} - 步数:
{value: 8500}
如果每种类型建一张表,维护成本高。JSON 列提供灵活性,同时 PostgreSQL 的 jsonb 类型支持索引和查询。
Q5: 前端怎么和后端通信?
前端通过 HTTP 请求和后端通信,具体流程:
- 前端
api-client.ts封装了fetch函数 - 每次请求自动带上 JWT 令牌(在请求头
Authorization: Bearer xxx) - 后端
[Authorize]属性检查令牌是否有效 - 如果令牌过期(401),前端自动清除登录状态
Q6: 如何启动整个项目?
- 启动基础设施:运行
D:\APP\start-dev.bat- 启动 PostgreSQL、Redis、MinIO
- 启动后端 API(端口 5000)
- 启动病人前端:
cd frontend-patient && npm run dev(端口 5173) - 启动医生前端:
cd frontend-doctor && npm run dev(端口 5174)
Q7: 数据库在哪里?
PostgreSQL 数据文件在 D:\APP\data\pgdata\。数据库名 HealthManager,用户名 postgres,密码 postgres123。
Q8: 如果数据库出问题了怎么重置?
- 停止后端
- 连接到 PostgreSQL 删除数据库:
DROP DATABASE "HealthManager"; - 重启后端,EF Core 会自动重建数据库和表,DataSeeder 会重新插入演示数据
文档版本:v1.0 最后更新:2026-05-20 适用项目:HealthManager 健康管理平台