Files
soft/技术文档.md
MingNian 435af55c4a Initial commit: HealthManager full-stack health management platform
Backend: .NET 10 + PostgreSQL + EF Core + JWT + SignalR
Frontend patient: React 19 + TypeScript + Vite (mobile H5)
Frontend doctor: React 19 + TypeScript + Vite (desktop web)
2026-05-20 16:18:56 +08:00

1518 lines
62 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# HealthManager 健康管理平台 — 超级详细技术文档
> **适用对象**:小白开发者 / 新加入项目的同学
> **文档目标**:看懂每一个文件是干什么的,代码在做什么,为什么要这样写
---
## 目录
1. [项目总览](#1-项目总览)
2. [架构设计:什么是"整洁架构"](#2-架构设计什么是整洁架构)
3. [后端 — Domain 领域层](#3-后端--domain-领域层)
4. [后端 — Application 应用层](#4-后端--application-应用层)
5. [后端 — Infrastructure 基础设施层](#5-后端--infrastructure-基础设施层)
6. [后端 — WebApi 接口层](#6-后端--webapi-接口层)
7. [病人端前端 (frontend-patient)](#7-病人端前端-frontend-patient)
8. [医生端前端 (frontend-doctor)](#8-医生端前端-frontend-doctor)
9. [数据库表结构](#9-数据库表结构)
10. [常见问题 FAQ](#10-常见问题-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`
```csharp
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返回给前端**
1. **安全**实体类可能包含密码哈希等敏感数据DTO 可以过滤掉
2. **灵活**前端需要的数据格式可能和数据库结构不一样DTO 可以自由组合
3. **解耦**:数据库结构变了,只要调整 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 的"总管家"。
**做了什么**
1. **定义 15 个 DbSet**:每个 DbSet 对应一张数据库表
```csharp
public DbSet<User> Users => Set<User>();
public DbSet<HealthRecord> HealthRecords => Set<HealthRecord>();
// ... 共 15 个
```
2. **在 `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` 标记,配置全局过滤器自动排除已删除的用户
---
### 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 令牌。
**两个方法**
1. **`GenerateAccessToken`**:生成访问令牌
- 从配置文件读取密钥(`appsettings.json` 的 `Jwt:Secret`
- 把用户 ID、姓名、角色打包进令牌的"负载"Claims
- 使用 HMAC-SHA256 算法签名
- 设置 30 分钟过期
- 返回签好名的 JWT 字符串
2. **`GenerateRefreshToken`**:生成刷新令牌
- 使用加密安全的随机数生成器产生 64 字节随机数据
- 转成 Base64 字符串返回(就是一个随机字符串,不包含任何信息)
**JWT 是怎么工作的?** 可以理解为"盖了章的通行证"
- 服务器签发令牌时"盖章"(用密钥签名)
- 客户端每次请求带着令牌
- 服务器验证"章"是真的 → 放行
- 如果令牌过期了 → 拒绝,客户端用刷新令牌换新的
---
## 6. 后端 — WebApi 接口层
> 这是最外层,直接接收 HTTP 请求,返回 JSON 响应。
### 6.1 `Program.cs` — 应用程序入口
**这是整个后端启动的唯一入口文件**。它配置了应用运行所需的一切。
**主要做的事(按顺序)**
1. **创建 WebApplication**:初始化 .NET 应用
2. **注册服务(依赖注入)**
- 注册 `AppDbContext`:告诉 EF Core 用 PostgreSQL连接字符串从配置文件读
- 注册 JWT 认证:配置验证规则(密钥、签发者、过期时间等)
- 注册 Swagger配置 API 文档,支持 JWT 认证测试
- 注册 SignalR配置实时通信
- 注册 CORS允许前端跨域访问开发阶段允许所有来源
- 注册业务服务:`AuthService`、`HealthService` 等,让控制器能使用
- 注册 `IJwtProvider` → `JwtProvider`:告诉框架"当需要 IJwtProvider 时,用 JwtProvider"
3. **配置中间件管道**
- 异常处理 → CORS → 认证 → 授权 → 映射控制器 → 映射 SignalR Hub
4. **初始化数据库**
- 确保数据库和表已创建(`EnsureCreatedAsync`
- 调用 `DataSeeder.SeedAsync()` 插入演示数据
5. **启动应用**:开始监听 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)`:发送消息 → 广播给聊天室内的其他人
**流程**
1. 患者发消息 → `SendMessage` → 保存到数据库 → 广播给医生
2. 医生收到实时通知 → 可以在聊天页面看到新消息
---
### 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` 函数。
**核心逻辑**
1. 每次请求自动从 localStorage 读取 JWT 令牌,加到请求头的 `Authorization: Bearer xxx`
2. 发送 HTTP 请求到 `http://localhost:5000`
3. 如果服务器返回 401未认证自动清除本地缓存令牌过期需要重新登录
4. 把服务器返回的 JSON 统一包装成 `{ code, data, message }` 格式
5. 如果服务器返回错误,抛出异常
**四个方法**`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 显示哪个页面组件"。
**路由分为三类**
1. **公开路由**(不需要登录):
- `/login` → 登录页
- `/register` → 注册页
2. **Tab 路由**(需要登录,底部有导航栏):
- `/home` → 首页仪表盘
- `/health` → 健康中心
- `/services` → 服务中心
- `/profile` → 个人中心
3. **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 请求和后端通信,具体流程:
1. 前端 `api-client.ts` 封装了 `fetch` 函数
2. 每次请求自动带上 JWT 令牌(在请求头 `Authorization: Bearer xxx`
3. 后端 `[Authorize]` 属性检查令牌是否有效
4. 如果令牌过期401前端自动清除登录状态
### Q6: 如何启动整个项目?
1. **启动基础设施**:运行 `D:\APP\start-dev.bat`
- 启动 PostgreSQL、Redis、MinIO
- 启动后端 API端口 5000
2. **启动病人前端**`cd frontend-patient && npm run dev`(端口 5173
3. **启动医生前端**`cd frontend-doctor && npm run dev`(端口 5174
### Q7: 数据库在哪里?
PostgreSQL 数据文件在 `D:\APP\data\pgdata\`。数据库名 `HealthManager`,用户名 `postgres`,密码 `postgres123`
### Q8: 如果数据库出问题了怎么重置?
1. 停止后端
2. 连接到 PostgreSQL 删除数据库:`DROP DATABASE "HealthManager";`
3. 重启后端EF Core 会自动重建数据库和表DataSeeder 会重新插入演示数据
---
> 文档版本v1.0
> 最后更新2026-05-20
> 适用项目HealthManager 健康管理平台