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

21 KiB
Raw Blame History

HealthManager 后端 — 技术文档

适用对象:小白开发者 / 新加入项目的同学 涵盖项目总览、整洁架构、Domain/Application/Infrastructure/WebApi 四层、数据库、FAQ


目录

  1. 项目总览
  2. 架构设计:什么是整洁架构
  3. Domain 领域层
  4. Application 应用层
  5. Infrastructure 基础设施层
  6. WebApi 接口层
  7. 数据库表结构
  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

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唯一、Role、UserId+Type、Status、ScheduledAt 等
    • 外键关系:用户→健康记录、用户→药物、咨询→消息 等

5.2 Data/DataSeeder.cs — 种子数据

在数据库首次创建时自动插入演示数据:

数据 数量 说明
患者 2 个 张明60岁PCI术后、李芳53岁PCI术后
医生 2 个 王建国(主任医师)、陈雪梅(副主任医师)
健康记录 ~93 条 患者1 的 31 天血压+心率+体重数据
药物 4 种 阿司匹林、替格瑞洛、瑞舒伐他汀、美托洛尔
服药记录 ~50 条 8 天服药情况,约 90% 依从率
咨询 1 个 张明咨询王建国
消息 4 条 完整医患对话
通知 4 条 用药提醒、复查提醒等

关键技术点

  • 演示密码 demo123SHA256 哈希存储
  • 固定随机种子 new Random(42),每次数据一致
  • 所有时间必须是 UTCnew DateTime(year, month, day, hour, minute, 0, DateTimeKind.Utc)

5.3 Services/JwtProvider.cs — JWT 令牌提供者

实现了 IJwtProvider 接口:

  • GenerateAccessTokenHMAC-SHA256 签名30 分钟过期,包含 userId + name + role
  • GenerateRefreshToken:加密随机数生成器产生 64 字节 → Base64 字符串

6. WebApi 接口层

6.1 Program.cs — 应用程序入口

整个后端启动的唯一入口。按顺序配置:

  1. 注册数据库EF Core + PostgreSQL连接字符串从 appsettings.json 读取
  2. 注册 JWT 认证:验证规则(密钥、签发者、过期时间)
  3. 注册 SwaggerAPI 文档页面
  4. 注册 SignalR:实时通信
  5. 注册 CORS:允许前端跨域(开发阶段允许 localhost:5173-5175
  6. 注册业务服务AuthService、HealthService 等
  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> 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.UnspecifiedDateTimeKind.LocalNpgsql 会抛异常。解决办法:DateTime.SpecifyKind(dt, DateTimeKind.Utc)


文档版本v1.0 | 最后更新2026-05-20 | 后端技术文档