- 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/
21 KiB
HealthManager 后端 — 技术文档
适用对象:小白开发者 / 新加入项目的同学 涵盖:项目总览、整洁架构、Domain/Application/Infrastructure/WebApi 四层、数据库、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
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 的"总管家"。
做了什么:
- 定义 15 个 DbSet:每个 DbSet 对应一张数据库表
- 在
OnModelCreating中配置:- JSON 列:
HealthRecord.Value→ PostgreSQLjsonb类型 - 数组列:
List<string>→ PostgreSQLtext[]类型 - 索引:Phone(唯一)、Role、UserId+Type、Status、ScheduledAt 等
- 外键关系:用户→健康记录、用户→药物、咨询→消息 等
- JSON 列:
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 + roleGenerateRefreshToken:加密随机数生成器产生 64 字节 → Base64 字符串
6. WebApi 接口层
6.1 Program.cs — 应用程序入口
整个后端启动的唯一入口。按顺序配置:
- 注册数据库:EF Core + PostgreSQL,连接字符串从
appsettings.json读取 - 注册 JWT 认证:验证规则(密钥、签发者、过期时间)
- 注册 Swagger:API 文档页面
- 注册 SignalR:实时通信
- 注册 CORS:允许前端跨域(开发阶段允许 localhost:5173-5175)
- 注册业务服务:AuthService、HealthService 等
- 配置中间件管道:CORS → 认证 → 授权 → 控制器 → SignalR Hub
- 初始化数据库:
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 |
启动配置:端口 5000,Development 环境,自动开 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> |
MedicalHistory、TimeSlots、ImageUrls |
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 Token。Refresh Token 7 天后也过期 → 需重新登录。
Q4: 健康数据的 Value 为什么用 JSON?
不同测量类型数据结构不同:血压 {systolic, diastolic, pulse}、心率 {value}。JSON 列提供灵活性,PostgreSQL jsonb 支持索引和查询。
Q5: 如何重置数据库?
-- 连接到 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 | 后端技术文档