# 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\? | 病史列表,如 `["高血压", "冠心病"]`,在数据库存为数组 | | StentDate | DateOnly? | 支架植入日期(患者特有) | | StentType | string? | 支架类型(患者特有) | | Department | string? | 科室(医生特有) | | Title | string? | 职称,如 `"主任医师"`(医生特有) | | Specialty | List\? | 擅长领域列表(医生特有) | | 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\ | 服药时间点列表,如 `["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\ | 报告图片地址列表,存为数组 | | 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 Users => Set(); public DbSet HealthRecords => Set(); // ... 共 15 个 ``` 2. **在 `OnModelCreating` 中配置表结构**: - **JSON 列映射**:HealthRecord.Value 类型是 `JsonDocument`,配置为 PostgreSQL 的 `jsonb` 类型 - **数组列映射**:`List` 类型的属性(如 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 的 `` 渲染子路由页面。使用 `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`)都包裹在 `` + `` 中,确保必须登录才能访问。 --- #### 各页面详细说明 ##### 登录页 `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` | 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 健康管理平台