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:
120
backend/src/HealthManager.WebApi/Controllers/AuthController.cs
Normal file
120
backend/src/HealthManager.WebApi/Controllers/AuthController.cs
Normal 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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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 = "全部已读" });
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
21
backend/src/HealthManager.WebApi/HealthManager.WebApi.csproj
Normal file
21
backend/src/HealthManager.WebApi/HealthManager.WebApi.csproj
Normal file
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.8" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.8" />
|
||||
<PackageReference Include="Microsoft.OpenApi" Version="2.4.1" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.7" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\HealthManager.Application\HealthManager.Application.csproj" />
|
||||
<ProjectReference Include="..\HealthManager.Infrastructure\HealthManager.Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,6 @@
|
||||
@HealthManager.WebApi_HostAddress = http://localhost:5133
|
||||
|
||||
GET {{HealthManager.WebApi_HostAddress}}/weatherforecast/
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
40
backend/src/HealthManager.WebApi/Hubs/ChatHub.cs
Normal file
40
backend/src/HealthManager.WebApi/Hubs/ChatHub.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using System.Security.Claims;
|
||||
using HealthManager.Application.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace HealthManager.WebApi.Hubs;
|
||||
|
||||
[Authorize]
|
||||
public class ChatHub(ConsultationService consultationService) : Hub
|
||||
{
|
||||
public async Task JoinConsultation(Guid consultationId)
|
||||
{
|
||||
await Groups.AddToGroupAsync(Context.ConnectionId, $"consultation_{consultationId}");
|
||||
}
|
||||
|
||||
public async Task LeaveConsultation(Guid consultationId)
|
||||
{
|
||||
await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"consultation_{consultationId}");
|
||||
}
|
||||
|
||||
public async Task SendMessage(Guid consultationId, string content)
|
||||
{
|
||||
var userId = Guid.Parse(Context.User!.FindFirstValue(ClaimTypes.NameIdentifier)!);
|
||||
var role = Context.User!.FindFirstValue(ClaimTypes.Role)!;
|
||||
|
||||
var message = await consultationService.SendMessageAsync(consultationId, userId, role, content);
|
||||
|
||||
await Clients.Group($"consultation_{consultationId}").SendAsync("ReceiveMessage", new
|
||||
{
|
||||
message.Id,
|
||||
message.SenderId,
|
||||
message.SenderRole,
|
||||
message.ContentType,
|
||||
message.Content,
|
||||
message.ImageUrl,
|
||||
message.IsRead,
|
||||
message.CreatedAt,
|
||||
});
|
||||
}
|
||||
}
|
||||
101
backend/src/HealthManager.WebApi/Program.cs
Normal file
101
backend/src/HealthManager.WebApi/Program.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using System.Text;
|
||||
using HealthManager.Domain.Interfaces;
|
||||
using HealthManager.Application.Services;
|
||||
using HealthManager.Infrastructure.Data;
|
||||
using HealthManager.Infrastructure.Services;
|
||||
using HealthManager.WebApi.Hubs;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Database
|
||||
builder.Services.AddDbContext<AppDbContext>(options =>
|
||||
options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
|
||||
|
||||
// JWT
|
||||
var jwtSecret = builder.Configuration["Jwt:Secret"]!;
|
||||
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
ValidateAudience = true,
|
||||
ValidateLifetime = true,
|
||||
ValidateIssuerSigningKey = true,
|
||||
ValidIssuer = builder.Configuration["Jwt:Issuer"],
|
||||
ValidAudience = builder.Configuration["Jwt:Audience"],
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSecret)),
|
||||
};
|
||||
options.Events = new JwtBearerEvents
|
||||
{
|
||||
OnMessageReceived = context =>
|
||||
{
|
||||
var accessToken = context.Request.Query["access_token"];
|
||||
var path = context.HttpContext.Request.Path;
|
||||
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs"))
|
||||
context.Token = accessToken;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
};
|
||||
});
|
||||
builder.Services.AddAuthorization();
|
||||
|
||||
// Services
|
||||
builder.Services.AddSingleton<IJwtProvider, JwtProvider>();
|
||||
builder.Services.AddScoped<AuthService>();
|
||||
builder.Services.AddScoped<HealthService>();
|
||||
builder.Services.AddScoped<MedicationService>();
|
||||
builder.Services.AddScoped<ConsultationService>();
|
||||
builder.Services.AddScoped<ReportService>();
|
||||
builder.Services.AddScoped<FollowUpService>();
|
||||
builder.Services.AddScoped<PatientService>();
|
||||
builder.Services.AddScoped<NotificationService>();
|
||||
|
||||
// SignalR
|
||||
builder.Services.AddSignalR();
|
||||
|
||||
// Swagger
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
|
||||
// CORS
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("Dev", policy =>
|
||||
{
|
||||
policy.WithOrigins("http://localhost:5173", "http://localhost:5174", "http://localhost:5175")
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod()
|
||||
.AllowCredentials();
|
||||
});
|
||||
});
|
||||
|
||||
builder.Services.AddControllers();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
app.UseCors("Dev");
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.MapControllers();
|
||||
app.MapHub<ChatHub>("/hubs/chat");
|
||||
|
||||
// Auto-migrate and seed on startup
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
await db.Database.EnsureCreatedAsync();
|
||||
await DataSeeder.SeedAsync(db);
|
||||
}
|
||||
|
||||
app.Run();
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5133",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "https://localhost:7241;http://localhost:5133",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
25
backend/src/HealthManager.WebApi/appsettings.json
Normal file
25
backend/src/HealthManager.WebApi/appsettings.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"ConnectionStrings": {
|
||||
"Default": "Host=localhost;Port=5432;Database=health_manager;Username=postgres;Password=postgres123"
|
||||
},
|
||||
"Jwt": {
|
||||
"Secret": "health-manager-jwt-secret-key-2026-super-secure-long-enough!",
|
||||
"Issuer": "HealthManager",
|
||||
"Audience": "HealthManagerApp"
|
||||
},
|
||||
"Redis": {
|
||||
"Connection": "localhost:6379"
|
||||
},
|
||||
"MinIO": {
|
||||
"Endpoint": "localhost:9000",
|
||||
"AccessKey": "minioadmin",
|
||||
"SecretKey": "minioadmin"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user