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)
This commit is contained in:
MingNian
2026-05-20 16:18:56 +08:00
commit 435af55c4a
215 changed files with 18595 additions and 0 deletions

View File

@@ -0,0 +1,120 @@
using System.Security.Claims;
using HealthManager.Application.DTOs.Auth;
using HealthManager.Domain.Interfaces;
using HealthManager.Application.Services;
using HealthManager.Domain.Entities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace HealthManager.WebApi.Controllers;
[ApiController]
[Route("api/auth")]
public class AuthController(
AuthService authService,
IJwtProvider jwtProvider) : ControllerBase
{
[HttpPost("send-sms")]
public IActionResult SendSms([FromBody] SendSmsRequest request)
{
// Demo: always succeed
return Ok(new { message = "验证码已发送" });
}
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginRequest request)
{
var user = await authService.GetUserByPhoneAsync(request.Phone);
if (user == null)
return Unauthorized(new { message = "用户不存在" });
// Demo: accept any SMS code
var accessToken = jwtProvider.GenerateAccessToken(user.Id, user.Name, user.Role);
var refreshToken = jwtProvider.GenerateRefreshToken();
await authService.SaveRefreshTokenAsync(user.Id, refreshToken, DateTime.UtcNow.AddDays(7));
return Ok(new AuthResponse(user.Id, user.Name, user.Role, accessToken, refreshToken));
}
[HttpPost("register")]
public async Task<IActionResult> Register([FromBody] RegisterRequest request)
{
var existing = await authService.GetUserByPhoneAsync(request.Phone);
if (existing != null)
return Conflict(new { message = "该手机号已注册" });
var user = new User
{
Phone = request.Phone,
Name = request.Name,
Role = "patient",
PasswordHash = AuthService.HashPassword("demo123"),
};
// Access DbContext via DI
var db = HttpContext.RequestServices.GetRequiredService<Infrastructure.Data.AppDbContext>();
db.Users.Add(user);
await db.SaveChangesAsync();
var accessToken = jwtProvider.GenerateAccessToken(user.Id, user.Name, user.Role);
var refreshToken = jwtProvider.GenerateRefreshToken();
await authService.SaveRefreshTokenAsync(user.Id, refreshToken, DateTime.UtcNow.AddDays(7));
return Ok(new AuthResponse(user.Id, user.Name, user.Role, accessToken, refreshToken));
}
[HttpPost("refresh")]
public async Task<IActionResult> RefreshToken([FromBody] RefreshTokenRequest request)
{
var saved = await authService.GetRefreshTokenAsync(request.RefreshToken);
if (saved == null)
return Unauthorized(new { message = "无效的刷新令牌" });
await authService.RevokeRefreshTokenAsync(saved.UserId);
var accessToken = jwtProvider.GenerateAccessToken(saved.User.Id, saved.User.Name, saved.User.Role);
var refreshToken = jwtProvider.GenerateRefreshToken();
await authService.SaveRefreshTokenAsync(saved.UserId, refreshToken, DateTime.UtcNow.AddDays(7));
return Ok(new AuthResponse(saved.User.Id, saved.User.Name, saved.User.Role, accessToken, refreshToken));
}
[HttpGet("me")]
[Authorize]
public async Task<IActionResult> GetProfile()
{
var userId = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
var db = HttpContext.RequestServices.GetRequiredService<Infrastructure.Data.AppDbContext>();
var user = await db.Users.FindAsync(userId);
if (user == null) return NotFound();
return Ok(new UserProfileResponse(
user.Id, user.Name, user.Phone, user.Role,
user.Gender, user.Birthday, user.HeightCm, user.WeightKg,
user.MedicalHistory, user.StentDate, user.StentType,
user.Department, user.Title, user.Specialty, user.Introduction));
}
[HttpPut("me")]
[Authorize]
public async Task<IActionResult> UpdateProfile([FromBody] UpdateProfileRequest request)
{
var userId = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
var db = HttpContext.RequestServices.GetRequiredService<Infrastructure.Data.AppDbContext>();
var user = await db.Users.FindAsync(userId);
if (user == null) return NotFound();
if (request.Name != null) user.Name = request.Name;
if (request.Gender != null) user.Gender = request.Gender;
if (request.Birthday.HasValue) user.Birthday = request.Birthday;
if (request.HeightCm.HasValue) user.HeightCm = request.HeightCm;
if (request.WeightKg.HasValue) user.WeightKg = request.WeightKg;
if (request.MedicalHistory != null) user.MedicalHistory = request.MedicalHistory;
user.UpdatedAt = DateTime.UtcNow;
await db.SaveChangesAsync();
return Ok(new { message = "更新成功" });
}
}
public record RefreshTokenRequest(string RefreshToken);

View File

@@ -0,0 +1,71 @@
using System.Security.Claims;
using HealthManager.Application.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace HealthManager.WebApi.Controllers;
[ApiController]
[Route("api/consultations")]
[Authorize]
public class ConsultationController(ConsultationService consultationService) : ControllerBase
{
private Guid UserId => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
private string Role => User.FindFirstValue(ClaimTypes.Role)!;
[HttpGet("doctors")]
public async Task<IActionResult> GetDoctors([FromQuery] string? department)
{
var doctors = await consultationService.GetDoctorsAsync(department);
return Ok(doctors.Select(d => new
{
d.Id, d.Name, d.Department, d.Title, d.Specialty, d.Introduction, d.AvatarUrl,
}));
}
[HttpGet]
public async Task<IActionResult> GetConsultations()
{
var consultations = Role == "doctor"
? await consultationService.GetDoctorConsultationsAsync(UserId)
: await consultationService.GetPatientConsultationsAsync(UserId);
return Ok(consultations.Select(c => new
{
c.Id, c.PatientId, c.DoctorId,
PatientName = c.Patient?.Name,
DoctorName = c.Doctor?.Name,
c.Subject, c.Status, c.StartedAt, c.ClosedAt, c.Summary,
}));
}
[HttpPost]
public async Task<IActionResult> StartConsultation([FromBody] StartConsultationRequest request)
{
var consultation = await consultationService.StartAsync(UserId, request.DoctorId, request.Subject);
return Ok(new { consultation.Id, consultation.PatientId, consultation.DoctorId, consultation.Status });
}
[HttpGet("{id:guid}/messages")]
public async Task<IActionResult> GetMessages(Guid id)
{
var messages = await consultationService.GetMessagesAsync(id);
return Ok(messages.Select(m => new
{
m.Id, m.SenderId, m.SenderRole, m.ContentType, m.Content,
m.ImageUrl, m.IsRead, m.CreatedAt,
SenderName = m.Sender?.Name,
}));
}
[HttpPost("{id:guid}/messages")]
public async Task<IActionResult> SendMessage(Guid id, [FromBody] SendMessageRequest request)
{
var message = await consultationService.SendMessageAsync(id, UserId, Role, request.Content,
request.ContentType, request.ImageUrl);
return Ok(new { message.Id, message.SenderId, message.SenderRole, message.Content, message.CreatedAt });
}
}
public record StartConsultationRequest(Guid DoctorId, string Subject);
public record SendMessageRequest(string Content, string ContentType = "text", string? ImageUrl = null);

View File

@@ -0,0 +1,67 @@
using System.Security.Claims;
using HealthManager.Application.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace HealthManager.WebApi.Controllers;
[ApiController]
[Route("api/follow-ups")]
[Authorize]
public class FollowUpController(FollowUpService followUpService) : ControllerBase
{
private Guid UserId => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
private string Role => User.FindFirstValue(ClaimTypes.Role)!;
[HttpGet]
public async Task<IActionResult> GetFollowUps()
{
var followUps = Role == "doctor"
? await followUpService.GetDoctorFollowUpsAsync(UserId)
: await followUpService.GetPatientFollowUpsAsync(UserId);
return Ok(followUps.Select(f => new
{
f.Id, f.PatientId, f.DoctorId, f.Title, f.Description,
f.ScheduledAt, f.Status, f.Notes, f.ReminderEnabled, f.CreatedAt,
PatientName = f.Patient?.Name,
DoctorName = f.Doctor?.Name,
}));
}
[HttpGet("{id:guid}")]
public async Task<IActionResult> GetFollowUp(Guid id)
{
var followUp = await followUpService.GetByIdAsync(id);
if (followUp == null) return NotFound(new { message = "复查不存在" });
return Ok(new
{
followUp.Id, followUp.PatientId, followUp.DoctorId, followUp.Title,
followUp.Description, followUp.ScheduledAt, followUp.Status,
followUp.Notes, followUp.ReminderEnabled, followUp.CreatedAt,
});
}
[HttpPost]
public async Task<IActionResult> AddFollowUp([FromBody] FollowUpCreateRequest request)
{
var followUp = await followUpService.AddAsync(UserId, request.Title, request.Description,
request.ScheduledAt, request.ReminderEnabled);
return Ok(new { followUp.Id, followUp.Title, followUp.Status });
}
[HttpPut("{id:guid}")]
[Authorize(Roles = "doctor")]
public async Task<IActionResult> UpdateFollowUp(Guid id, [FromBody] FollowUpUpdateRequest request)
{
var followUp = await followUpService.UpdateAsync(id, UserId, request.Title, request.Description,
request.ScheduledAt, request.Status, request.Notes);
if (followUp == null) return NotFound(new { message = "复查不存在" });
return Ok(new { followUp.Id, followUp.Title, followUp.Status });
}
}
public record FollowUpCreateRequest(string Title, string? Description, DateTime ScheduledAt, bool ReminderEnabled = true);
public record FollowUpUpdateRequest(
string? Title, string? Description, DateTime? ScheduledAt, string? Status, string? Notes);

View File

@@ -0,0 +1,61 @@
using System.Security.Claims;
using System.Text.Json;
using HealthManager.Application.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace HealthManager.WebApi.Controllers;
[ApiController]
[Route("api/health-records")]
[Authorize]
public class HealthController(HealthService healthService) : ControllerBase
{
private Guid UserId => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
private string Role => User.FindFirstValue(ClaimTypes.Role)!;
[HttpGet]
public async Task<IActionResult> GetRecords([FromQuery] string? type, [FromQuery] int days = 30)
{
var targetUserId = UserId;
// Doctors can query any patient
if (Role == "doctor" && Request.Query.ContainsKey("patientId"))
targetUserId = Guid.Parse(Request.Query["patientId"]!);
var records = await healthService.GetRecordsAsync(targetUserId, type, days);
return Ok(records.Select(r => new
{
r.Id, r.Type, Value = r.Value.RootElement.GetRawText(), r.Unit,
r.RecordedAt, r.Source, r.Notes, r.CreatedAt,
}));
}
[HttpGet("stats")]
public async Task<IActionResult> GetStats()
{
var stats = await healthService.GetStatsAsync(UserId);
return Ok(stats);
}
[HttpGet("latest/{type}")]
public async Task<IActionResult> GetLatest(string type)
{
var record = await healthService.GetLatestAsync(UserId, type);
if (record == null) return Ok((object?)null);
return Ok(new { record.Id, record.Type, Value = record.Value.RootElement.GetRawText(), record.Unit, record.RecordedAt, record.Source });
}
[HttpPost]
public async Task<IActionResult> AddRecord([FromBody] HealthRecordCreateRequest request)
{
// Validate JSON
try { JsonDocument.Parse(request.ValueJson); }
catch (JsonException) { return BadRequest(new { message = "无效的数据格式" }); }
var record = await healthService.AddRecordAsync(UserId, request.Type, request.ValueJson, request.Unit, request.RecordedAt, request.Notes);
return Ok(new { record.Id, record.Type, Value = record.Value.RootElement.GetRawText(), record.Unit, record.RecordedAt, record.Source });
}
}
public record HealthRecordCreateRequest(string Type, string ValueJson, string Unit, DateTime RecordedAt, string? Notes);

View File

@@ -0,0 +1,80 @@
using System.Security.Claims;
using HealthManager.Application.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace HealthManager.WebApi.Controllers;
[ApiController]
[Route("api/medications")]
[Authorize]
public class MedicationController(MedicationService medicationService) : ControllerBase
{
private Guid UserId => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
private string Role => User.FindFirstValue(ClaimTypes.Role)!;
[HttpGet]
public async Task<IActionResult> GetMedications()
{
var targetUserId = UserId;
if (Role == "doctor" && Request.Query.ContainsKey("patientId"))
targetUserId = Guid.Parse(Request.Query["patientId"]!);
var medications = await medicationService.GetUserMedicationsAsync(targetUserId);
return Ok(medications.Select(m => new
{
m.Id, m.UserId, m.DrugName, m.Dosage, m.Frequency, m.TimeSlots,
m.StartDate, m.EndDate, m.Notes, m.Status, m.CreatedAt,
}));
}
[HttpPost]
public async Task<IActionResult> AddMedication([FromBody] MedicationCreateRequest request)
{
var med = await medicationService.AddAsync(UserId, request.DrugName, request.Dosage,
request.Frequency, request.TimeSlots, request.StartDate, request.EndDate, request.Notes);
return Ok(new { med.Id, med.DrugName, med.Dosage, med.Frequency, med.Status });
}
[HttpGet("{id:guid}")]
public async Task<IActionResult> GetMedication(Guid id)
{
var med = await medicationService.GetByIdAsync(id);
if (med == null) return NotFound(new { message = "药品不存在" });
return Ok(new
{
med.Id, med.UserId, med.DrugName, med.Dosage, med.Frequency, med.TimeSlots,
med.StartDate, med.EndDate, med.Notes, med.Status, med.CreatedAt,
});
}
[HttpGet("{id:guid}/records")]
public async Task<IActionResult> GetRecords(Guid id)
{
var records = await medicationService.GetRecordsAsync(id);
return Ok(records.Select(r => new
{
r.Id, r.MedicationId, r.TimeSlot, r.TakenAt, r.IsTaken, r.SkippedReason, r.CreatedAt,
}));
}
[HttpPost("{id:guid}/take")]
public async Task<IActionResult> MarkTaken(Guid id, [FromBody] MarkTakenRequest request)
{
var record = await medicationService.MarkTakenAsync(id, UserId, request.TimeSlot);
return Ok(new { record.Id, record.TimeSlot, record.TakenAt, record.IsTaken });
}
[HttpGet("{id:guid}/adherence")]
public async Task<IActionResult> GetAdherence(Guid id)
{
var rate = await medicationService.GetAdherenceRateAsync(id);
return Ok(new { medicationId = id, rate });
}
}
public record MedicationCreateRequest(
string DrugName, string Dosage, string Frequency,
List<string> TimeSlots, DateOnly StartDate, DateOnly? EndDate, string? Notes);
public record MarkTakenRequest(string TimeSlot);

View File

@@ -0,0 +1,46 @@
using System.Security.Claims;
using HealthManager.Application.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace HealthManager.WebApi.Controllers;
[ApiController]
[Route("api/notifications")]
[Authorize]
public class NotificationController(NotificationService notificationService) : ControllerBase
{
private Guid UserId => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
[HttpGet]
public async Task<IActionResult> GetNotifications()
{
var notifications = await notificationService.GetUserNotificationsAsync(UserId);
return Ok(notifications.Select(n => new
{
n.Id, n.Type, n.Title, n.Content, n.RelatedId,
n.IsRead, n.ReadAt, n.CreatedAt,
}));
}
[HttpGet("unread-count")]
public async Task<IActionResult> GetUnreadCount()
{
var count = await notificationService.GetUnreadCountAsync(UserId);
return Ok(new { count });
}
[HttpPut("{id:guid}/read")]
public async Task<IActionResult> MarkAsRead(Guid id)
{
await notificationService.MarkAsReadAsync(id);
return Ok(new { message = "已标记为已读" });
}
[HttpPut("read-all")]
public async Task<IActionResult> MarkAllAsRead()
{
await notificationService.MarkAllAsReadAsync(UserId);
return Ok(new { message = "全部已读" });
}
}

View File

@@ -0,0 +1,27 @@
using System.Security.Claims;
using HealthManager.Application.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace HealthManager.WebApi.Controllers;
[ApiController]
[Route("api/patients")]
[Authorize(Roles = "doctor")]
public class PatientController(PatientService patientService) : ControllerBase
{
[HttpGet]
public async Task<IActionResult> GetPatients([FromQuery] string? search, [FromQuery] int page = 1, [FromQuery] int pageSize = 20)
{
var patients = await patientService.GetPatientsAsync(search, null, page, pageSize);
return Ok(patients);
}
[HttpGet("{id:guid}")]
public async Task<IActionResult> GetPatientDetail(Guid id)
{
var patient = await patientService.GetPatientDetailAsync(id);
if (patient == null) return NotFound(new { message = "患者不存在" });
return Ok(patient);
}
}

View File

@@ -0,0 +1,87 @@
using System.Security.Claims;
using HealthManager.Application.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace HealthManager.WebApi.Controllers;
[ApiController]
[Route("api/reports")]
[Authorize]
public class ReportController(ReportService reportService) : ControllerBase
{
private Guid UserId => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
private string Role => User.FindFirstValue(ClaimTypes.Role)!;
[HttpGet]
public async Task<IActionResult> GetReports()
{
var targetUserId = UserId;
if (Role == "doctor" && Request.Query.ContainsKey("patientId"))
targetUserId = Guid.Parse(Request.Query["patientId"]!);
var reports = await reportService.GetPatientReportsAsync(targetUserId);
return Ok(reports.Select(r => new
{
r.Id, r.PatientId, r.Title, r.Category, r.ImageUrls, r.Status,
r.RiskLevel, r.UploadedAt, r.CompletedAt,
DoctorName = r.Doctor?.Name,
}));
}
[HttpGet("pending")]
[Authorize(Roles = "doctor")]
public async Task<IActionResult> GetPending()
{
var reports = await reportService.GetPendingAsync();
return Ok(reports.Select(r => new
{
r.Id, r.PatientId, r.Title, r.Category, r.Status, r.UploadedAt,
PatientName = r.Patient?.Name,
}));
}
[HttpGet("{id:guid}")]
public async Task<IActionResult> GetReport(Guid id)
{
var report = await reportService.GetByIdAsync(id);
if (report == null) return NotFound(new { message = "报告不存在" });
return Ok(new
{
report.Id, report.PatientId, report.Title, report.Category, report.ImageUrls,
report.Status, report.RiskLevel, report.Summary, report.Suggestions,
report.UploadedAt, report.CompletedAt,
PatientName = report.Patient?.Name,
DoctorName = report.Doctor?.Name,
Items = report.Items.Select(i => new
{
i.Id, i.ItemName, i.ResultValue, i.Unit, i.ReferenceRange, i.IsAbnormal,
}),
});
}
[HttpPost]
public async Task<IActionResult> UploadReport([FromBody] ReportUploadRequest request)
{
var report = await reportService.UploadAsync(UserId, request.Title, request.Category, request.ImageUrls);
return Ok(new { report.Id, report.Title, report.Status });
}
[HttpPost("{id:guid}/interpret")]
[Authorize(Roles = "doctor")]
public async Task<IActionResult> InterpretReport(Guid id, [FromBody] ReportInterpretRequest request)
{
var items = request.Items.Select(i => (i.ItemName, i.ResultValue, i.Unit, i.ReferenceRange, i.IsAbnormal)).ToList();
var report = await reportService.InterpretAsync(id, UserId, request.Summary, items, request.RiskLevel, request.Suggestions);
return Ok(new { report.Id, report.Status, report.RiskLevel });
}
}
public record ReportUploadRequest(string Title, string Category, List<string> ImageUrls);
public record ReportInterpretRequest(
string Summary, List<ReportItemRequest> Items, string RiskLevel, string? Suggestions);
public record ReportItemRequest(
string ItemName, string ResultValue, string? Unit, string? ReferenceRange, bool IsAbnormal);