Files
soft/backend/技术文档-后端.md
MingNian d5f167167a feat: replace Redis with PostgreSQL for caching, rate limiting, SMS codes, and token blacklist
- Add 4 PG entities: VerificationCode, RateLimitEntry, TokenBlacklistEntry, CacheEntry
- Add 4 services: VerificationService, RateLimitService, TokenBlacklistService, CacheService
- Add CleanupBackgroundService for periodic expired data cleanup
- Add MigrationHelper for safe schema migration without data loss
- Update AuthController: real SMS code generation, rate limiting, logout endpoint with JWT blacklist
- Update JwtProvider: add JTI claim for token revocation
- Update Program.cs: register new services, JWT blacklist validation, DB migration
- Remove StackExchange.Redis NuGet package and all Redis config references
- Update start-dev.bat: 6→5 services, remove Redis startup
- Update docs: remove Redis references from all documentation
- Fix: logout button spacing on profile page
- Fix: .gitignore data/→/data/ to not ignore Infrastructure/Data/
2026-05-26 13:48:53 +08:00

487 lines
21 KiB
Markdown
Raw Permalink 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 后端 — 技术文档
> 适用对象:小白开发者 / 新加入项目的同学
> 涵盖项目总览、整洁架构、Domain/Application/Infrastructure/WebApi 四层、数据库、FAQ
---
## 目录
1. [项目总览](#1-项目总览)
2. [架构设计:什么是整洁架构](#2-架构设计什么是整洁架构)
3. [Domain 领域层](#3-domain-领域层)
4. [Application 应用层](#4-application-应用层)
5. [Infrastructure 基础设施层](#5-infrastructure-基础设施层)
6. [WebApi 接口层](#6-webapi-接口层)
7. [数据库表结构](#7-数据库表结构)
8. [常见问题 FAQ](#8-常见问题-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 兼容的对象存储 | 存储图片(报告照片、头像等) |
### 1.3 项目文件结构
```
backend/
├── HealthManager.slnx ← 解决方案文件
├── src/
│ ├── HealthManager.Domain/ ← 领域层(实体 + 接口)
│ ├── HealthManager.Application/ ← 应用层(服务 + DTO
│ ├── HealthManager.Infrastructure/← 基础设施层(数据库 + JWT
│ └── HealthManager.WebApi/ ← Web API 层(控制器 + 启动)
├── start-dev.bat ← 一键启动脚本
└── 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 定义的接口
### 2.2 每一层的职责
| 层 | 能做什么 | 不能做什么 |
|----|----------|-----------|
| **Domain** | 定义实体、定义接口 | 不写具体逻辑、不访问数据库 |
| **Application** | 写业务逻辑、定义 DTO | 不直接访问 HTTP 请求、不直接操作数据库 |
| **Infrastructure** | 实现接口、操作数据库、生成 JWT | 不处理 HTTP 请求 |
| **WebApi** | 接收 HTTP 请求、控制权限、返回响应 | 不写业务逻辑、不直接操作数据库 |
---
## 3. Domain 领域层
> 这是最核心的一层,定义了"数据长什么样"。就像一个房子的地基。
### 3.1 接口 `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 层不应该知道具体是怎么实现的。所以 Domain 定义接口Infrastructure 去实现。
### 3.2 实体类一览
每种实体对应数据库中的一张表。
| 实体文件 | 对应表 | 用途 |
|----------|--------|------|
| `User.cs` | Users | 用户(患者+医生),通过 Role 字段区分 |
| `RefreshToken.cs` | RefreshTokens | JWT 刷新令牌 |
| `Device.cs` | Devices | 智能设备绑定 |
| `HealthRecord.cs` | HealthRecords | 健康测量数据(血压/心率/血糖等Value 用 JSON 存储 |
| `DietRecord.cs` | DietRecords | 饮食记录 |
| `ExerciseRecord.cs` | ExerciseRecords | 运动记录 |
| `Medication.cs` | Medications | 药物方案 |
| `MedicationRecord.cs` | MedicationRecords | 服药打卡记录 |
| `Consultation.cs` | Consultations | 医患咨询会话 |
| `ConsultationMessage.cs` | ConsultationMessages | 咨询消息 |
| `QuickReplyTemplate.cs` | QuickReplyTemplates | 医生快捷回复模板 |
| `Report.cs` | Reports | 检查报告 |
| `ReportItem.cs` | ReportItems | 报告中的检查项目 |
| `FollowUp.cs` | FollowUps | 随访计划 |
| `Notification.cs` | Notifications | 通知 |
### 3.3 关键实体详解
#### User用户表
患者和医生共用一张 `Users` 表,通过 `Role` 字段区分:
- 患者独有字段:`MedicalHistory`, `StentDate`, `StentType`, `HeightCm`, `WeightKg`
- 医生独有字段:`Department`, `Title`, `Specialty`, `Introduction`, `IsAvailable`
- 公共字段:`Id`, `Phone`, `PasswordHash`, `Name`, `Gender`, `Birthday`, `CreatedAt`
- 软删除:`IsDeleted` + `DeletedAt`
#### HealthRecord健康记录
| 属性 | 类型 | 说明 |
|------|------|------|
| Type | string | `blood_pressure`, `heart_rate`, `blood_sugar`, `spo2`, `weight`, `steps` |
| Value | JsonDocument | **关键设计**:不同测量存不同 JSON 格式。血压 `{systolic, diastolic, pulse}`,心率 `{value}` |
| Unit | string | 单位mmHg, bpm, mmol/L, %, kg, 步 |
| Source | string | 来源manual手动, device设备, doctor医生 |
#### Medication药物
| 属性 | 类型 | 说明 |
|------|------|------|
| DrugName | string | 药物名称 |
| Dosage | string | 剂量,如 "100mg" |
| Frequency | string | 频率,如 "每日1次" |
| TimeSlots | List\<string\> | 服药时间点,如 ["08:00", "20:00"],存为 PostgreSQL text[] |
| Status | string | active / completed / stopped |
#### Report报告
| 属性 | 类型 | 说明 |
|------|------|------|
| Category | string | 血液检查 / 心电图 / 影像学 / 尿液检查 |
| ImageUrls | List\<string\> | 报告图片地址列表 |
| Status | string | pending待审核→ interpreting解读中→ completed已完成 |
| RiskLevel | string? | normal / attention / abnormal |
---
## 4. Application 应用层
> 这一层写所有的**业务逻辑**。
### 4.1 DTO数据传输对象
DTO = Data Transfer Object。用于前后端之间传输数据而不是直接把数据库实体暴露给前端。
| 文件 | 包含的 DTO |
|------|-----------|
| `DTOs/AuthDtos.cs` | LoginRequest, RegisterRequest, SendSmsRequest, AuthResponse, UserProfileResponse, UpdateProfileRequest |
| `DTOs/ConsultationDtos.cs` | ConsultationCreateRequest, SendMessageRequest 等 |
| `DTOs/FollowUpDtos.cs` | FollowUpCreateRequest, FollowUpUpdateRequest, FollowUpResponse |
| `DTOs/HealthDtos.cs` | HealthRecordCreateRequest, HealthRecordResponse, HealthStatsResponse |
| `DTOs/MedicationDtos.cs` | MedicationCreateRequest, MedicationResponse, AdherenceResponse |
| `DTOs/ReportDtos.cs` | ReportUploadRequest, ReportInterpretRequest, ReportResponse |
### 4.2 服务类
每个服务类负责一个业务领域的所有逻辑。通过构造函数注入 `AppDbContext`
| 服务文件 | 负责领域 | 核心方法 |
|----------|---------|---------|
| `AuthService.cs` | 用户认证 | GetUserByPhone, SaveRefreshToken, RevokeRefreshToken, HashPassword |
| `HealthService.cs` | 健康数据 | GetRecords, GetLatest, AddRecord, GetStats含趋势计算 |
| `MedicationService.cs` | 药物管理 | GetUserMedications, Add, MarkTaken, GetAdherenceRate |
| `ConsultationService.cs` | 在线问诊 | GetDoctors, Start, SendMessage, GetMessages |
| `PatientService.cs` | 患者管理(医生用) | GetPatients支持搜索分页, GetPatientDetail |
| `ReportService.cs` | 报告管理 | Upload, GetPatientReports, GetAllReports, Interpret |
| `FollowUpService.cs` | 随访管理 | Add, GetPatientFollowUps, GetDoctorFollowUps, Update |
| `NotificationService.cs` | 通知 | GetUserNotifications, GetUnreadCount, MarkAsRead, Create |
#### 关键方法详解
**HealthService.GetStatsAsync**:计算每种健康类型的统计值
- 查最近 7 天记录算周平均 → 和前面 7 天比较 → 判断趋势(上升 >3%、下降 <3%、稳定
- 查全部记录算月平均
- 支持的类型blood_pressure, heart_rate, blood_sugar, spo2, weight, steps
**MedicationService.GetAdherenceRateAsync**计算依从率
- 依从率 = 实际服用次数 / 总应有次数 × 100%
- 30 天内按药物 TimeSlots 数量算总应服药次数
**MedicationService.MarkTakenAsync**标记已服药
- 先查今天该时间点是否已有记录幂等防止重复打卡
- 有则更新无则新建
---
## 5. Infrastructure 基础设施层
> 这一层和"外部世界"打交道数据库、JWT 加密等具体技术实现。
### 5.1 `Data/AppDbContext.cs` — 数据库上下文
**整个项目中最重要的基础设施文件**EF Core "总管家"。
**做了什么**
1. **定义 15 个 DbSet**每个 DbSet 对应一张数据库表
2. **在 `OnModelCreating` 中配置**
- JSON `HealthRecord.Value` PostgreSQL `jsonb` 类型
- 数组列`List<string>` PostgreSQL `text[]` 类型
- 索引Phone唯一)、RoleUserId+TypeStatusScheduledAt
- 外键关系用户健康记录用户药物咨询消息
### 5.2 `Data/DataSeeder.cs` — 种子数据
在数据库首次创建时自动插入演示数据
| 数据 | 数量 | 说明 |
|------|------|------|
| 患者 | 2 | 张明60岁PCI术后)、李芳53岁PCI术后 |
| 医生 | 2 | 王建国主任医师)、陈雪梅副主任医师 |
| 健康记录 | ~93 | 患者1 31 天血压+心率+体重数据 |
| 药物 | 4 | 阿司匹林替格瑞洛瑞舒伐他汀美托洛尔 |
| 服药记录 | ~50 | 8 天服药情况 90% 依从率 |
| 咨询 | 1 | 张明咨询王建国 |
| 消息 | 4 | 完整医患对话 |
| 通知 | 4 | 用药提醒复查提醒等 |
**关键技术点**
- 演示密码 `demo123`SHA256 哈希存储
- 固定随机种子 `new Random(42)`每次数据一致
- **所有时间必须是 UTC**`new DateTime(year, month, day, hour, minute, 0, DateTimeKind.Utc)`
### 5.3 `Services/JwtProvider.cs` — JWT 令牌提供者
实现了 `IJwtProvider` 接口
- `GenerateAccessToken`HMAC-SHA256 签名30 分钟过期包含 userId + name + role
- `GenerateRefreshToken`加密随机数生成器产生 64 字节 Base64 字符串
---
## 6. WebApi 接口层
### 6.1 `Program.cs` — 应用程序入口
**整个后端启动的唯一入口**按顺序配置
1. **注册数据库**EF Core + PostgreSQL连接字符串从 `appsettings.json` 读取
2. **注册 JWT 认证**验证规则密钥签发者过期时间
3. **注册 Swagger**API 文档页面
4. **注册 SignalR**实时通信
5. **注册 CORS**允许前端跨域开发阶段允许 localhost:5173-5175
6. **注册业务服务**AuthServiceHealthService
7. **配置中间件管道**CORS 认证 授权 控制器 SignalR Hub
8. **初始化数据库**`EnsureCreatedAsync` + `SeedAsync`
### 6.2 `Hubs/ChatHub.cs` — SignalR 实时聊天
- `JoinConsultation(consultationId)`加入聊天室
- `LeaveConsultation(consultationId)`离开
- `SendMessage(consultationId, content)`发送消息 广播给聊天室所有人
### 6.3 API 接口完整列表
#### 认证AuthController- `api/auth`
| 端点 | 方法 | 认证 | 说明 |
|------|------|------|------|
| `/send-sms` | POST | 公开 | 发送短信验证码演示版直接返回成功 |
| `/login` | POST | 公开 | 手机号+验证码登录 返回 JWT 令牌对 |
| `/register` | POST | 公开 | 注册新患者 |
| `/refresh` | POST | 公开 | 刷新令牌换新的访问令牌 |
| `/me` | GET | 需登录 | 获取当前用户完整资料 |
| `/me` | PUT | 需登录 | 更新个人资料姓名/性别/身高/体重/病史 |
#### 健康数据HealthController- `api/health-records`
| 端点 | 方法 | 认证 | 说明 |
|------|------|------|------|
| `/` | GET | 需登录 | 查询记录支持 `?type=&days=`医生可加 `?patientId=` |
| `/stats` | GET | 需登录 | 所有类型的统计最新值+周均+月均+趋势 |
| `/latest/{type}` | GET | 需登录 | 某类型最新一条 |
| `/` | POST | 需登录 | 添加健康记录 |
#### 药物管理MedicationController- `api/medications`
| 端点 | 方法 | 认证 | 说明 |
|------|------|------|------|
| `/` | GET | 需登录 | 列出所有药物医生可加 `?patientId=` |
| `/` | POST | 需登录 | 添加新药物 |
| `/{id}` | GET | 需登录 | 药物详情 |
| `/{id}/records` | GET | 需登录 | 服药记录列表 |
| `/{id}/take` | POST | 需登录 | 标记已服药 `{ timeSlot: "08:00" }` |
| `/{id}/adherence` | GET | 需登录 | 依从率百分比 |
#### 在线问诊ConsultationController- `api/consultations`
| 端点 | 方法 | 认证 | 说明 |
|------|------|------|------|
| `/doctors` | GET | 需登录 | 可接诊的医生列表 |
| `/` | GET | 需登录 | 我的咨询列表 |
| `/` | POST | 需登录 | 发起新咨询 |
| `/{id}/messages` | GET | 需登录 | 咨询消息列表 |
| `/{id}/messages` | POST | 需登录 | 发送消息 |
#### 报告管理ReportController- `api/reports`
| 端点 | 方法 | 认证 | 权限 | 说明 |
|------|------|------|------|------|
| `/` | GET | 需登录 | | 患者看自己医生看全部 |
| `/pending` | GET | 需登录 | 仅医生 | 待审核报告 |
| `/{id}` | GET | 需登录 | | 报告详情 |
| `/` | POST | 需登录 | | 上传报告 `{title, category, imageUrls}` |
| `/{id}/interpret` | POST | 需登录 | 仅医生 | 提交解读 `{summary, items, riskLevel, suggestions}` |
#### 随访管理FollowUpController- `api/follow-ups`
| 端点 | 方法 | 认证 | 权限 | 说明 |
|------|------|------|------|------|
| `/` | GET | 需登录 | | 患者看自己医生看分配给自己 |
| `/{id}` | GET | 需登录 | | 随访详情 |
| `/` | POST | 需登录 | | 创建随访 |
| `/{id}` | PUT | 需登录 | 仅医生 | 更新随访 |
#### 患者管理PatientController- `api/patients`(仅医生)
| 端点 | 方法 | 认证 | 说明 |
|------|------|------|------|
| `/` | GET | 仅医生 | 患者列表支持 `?search=&page=&pageSize=` |
| `/{id}` | GET | 仅医生 | 患者详情 |
#### 通知NotificationController- `api/notifications`
| 端点 | 方法 | 认证 | 说明 |
|------|------|------|------|
| `/` | GET | 需登录 | 通知列表 |
| `/unread-count` | GET | 需登录 | 未读数量 |
| `/{id}/read` | PUT | 需登录 | 标记已读 |
| `/read-all` | PUT | 需登录 | 全部已读 |
### 6.4 配置文件
| 文件 | 内容 |
|------|------|
| `appsettings.json` | PostgreSQL 连接串JWT 密钥/签发者MinIO 连接 |
| `appsettings.Development.json` | 开发环境覆盖配置 |
| `Properties/launchSettings.json` | 启动配置端口 5000Development 环境自动开 Swagger |
---
## 7. 数据库表结构
### 7.1 所有表一览
| 表名 | 用途 | 主要查询场景 |
|------|------|-------------|
| `Users` | 用户患者+医生+管理员 | 按手机号查按角色查按姓名搜索 |
| `RefreshTokens` | JWT 刷新令牌 | 按令牌字符串查按用户查 |
| `Devices` | 智能设备 | 按用户查绑定的设备 |
| `HealthRecords` | 健康测量数据 | 按用户+类型查按时间范围查取最新值 |
| `DietRecords` | 饮食记录 | 按用户+日期查 |
| `ExerciseRecords` | 运动记录 | 按用户+日期查 |
| `Medications` | 药物方案 | 按用户查按状态查按医生查 |
| `MedicationRecords` | 服药记录 | 按药物查按用户+时间查 |
| `Consultations` | 咨询会话 | 按患者查按医生查按状态查 |
| `ConsultationMessages` | 咨询消息 | 按咨询查按发送者查 |
| `QuickReplyTemplates` | 快捷回复模板 | 按医生查 |
| `Reports` | 检查报告 | 按患者查按状态查 |
| `ReportItems` | 报告检查项 | 按报告查 |
| `FollowUps` | 随访计划 | 按患者查按医生查按计划时间查 |
| `Notifications` | 通知 | 按用户查按已读/未读查 |
### 7.2 重要索引
| | 索引 | 加速的查询 |
|----|------|-----------|
| `Users` | Phone (唯一) | 登录时按手机号查找 |
| `Users` | Role | 列出所有医生/患者 |
| `HealthRecords` | UserId + Type | 查某人的血压数据 |
| `HealthRecords` | RecordedAt | 按时间排序 |
| `Medications` | UserId, Status | 查药物和状态 |
| `MedicationRecords` | UserId + CreatedAt | 查最近服药记录 |
| `Consultations` | Status, PatientId, DoctorId | 查活跃咨询 |
| `Reports` | Status | 查待审核报告 |
| `FollowUps` | ScheduledAt | 按预约时间排序 |
| `Notifications` | UserId + IsRead | 查未读通知 |
### 7.3 特殊数据类型
| PostgreSQL 类型 | 对应 C# 类型 | 使用场景 |
|-----------------|-------------|---------|
| `uuid` | `Guid` | 所有主键 |
| `jsonb` | `JsonDocument` | HealthRecord.Value |
| `text[]` | `List<string>` | MedicalHistoryTimeSlotsImageUrls |
| `timestamp with time zone` | `DateTime` (UTC) | 所有时间戳 |
| `date` | `DateOnly` | 出生日期开始日期 |
---
## 8. 常见问题 FAQ
### Q1: 为什么所有 ID 都用 Guid 而不是自增数字?
- **安全**自增 ID 会暴露数据量
- **分布式友好**多台服务器不会冲突
- **前端可预生成**不需要等数据库返回 ID
### Q2: 为什么患者和医生放在同一张 Users 表?
- 登录逻辑完全一样简化设计
- 通过 `Role` 字段区分权限
- "不用的字段留空"处理差异
### Q3: JWT 令牌过期了怎么办?
Access Token 30 分钟过期 前端用 Refresh Token 换新的 Access TokenRefresh Token 7 天后也过期 需重新登录
### Q4: 健康数据的 Value 为什么用 JSON
不同测量类型数据结构不同血压 `{systolic, diastolic, pulse}`心率 `{value}`JSON 列提供灵活性PostgreSQL jsonb 支持索引和查询
### Q5: 如何重置数据库?
```sql
-- 连接到 PostgreSQL
DROP DATABASE "HealthManager";
```
重启后端EF Core 自动重建表DataSeeder 自动插入演示数据
### Q6: 数据库连接信息
- 数据库名`HealthManager`
- 用户名`postgres`
- 密码`postgres123`
- 端口`5432`
- 数据目录`D:\APP\data\pgdata\`
### Q7: 为什么 DateTime 必须用 UTC
PostgreSQL `timestamp with time zone` 列类型要求传入的时间必须是 `DateTimeKind.Utc`如果用 `DateTimeKind.Unspecified` `DateTimeKind.Local`Npgsql 会抛异常解决办法`DateTime.SpecifyKind(dt, DateTimeKind.Utc)`
---
> 文档版本v1.0 | 最后更新2026-05-20 | 后端技术文档