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:
19
backend/src/HealthManager.Application/DTOs/AuthDtos.cs
Normal file
19
backend/src/HealthManager.Application/DTOs/AuthDtos.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace HealthManager.Application.DTOs.Auth;
|
||||
|
||||
public record LoginRequest(string Phone, string SmsCode);
|
||||
|
||||
public record RegisterRequest(string Phone, string SmsCode, string Name);
|
||||
|
||||
public record SendSmsRequest(string Phone);
|
||||
|
||||
public record AuthResponse(Guid UserId, string Name, string Role, string AccessToken, string RefreshToken);
|
||||
|
||||
public record UserProfileResponse(
|
||||
Guid Id, string Name, string Phone, string Role,
|
||||
string? Gender, DateOnly? Birthday, decimal? HeightCm, decimal? WeightKg,
|
||||
List<string>? MedicalHistory, DateOnly? StentDate, string? StentType,
|
||||
string? Department, string? Title, List<string>? Specialty, string? Introduction);
|
||||
|
||||
public record UpdateProfileRequest(
|
||||
string? Name, string? Gender, DateOnly? Birthday,
|
||||
decimal? HeightCm, decimal? WeightKg, List<string>? MedicalHistory);
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace HealthManager.Application.DTOs.Consultation;
|
||||
|
||||
public record ConsultationCreateRequest(Guid DoctorId, string Subject);
|
||||
|
||||
public record ConsultationResponse(
|
||||
Guid Id, Guid PatientId, Guid DoctorId,
|
||||
string PatientName, string DoctorName,
|
||||
string? Subject, string Status, DateTime StartedAt,
|
||||
DateTime? ClosedAt, string? Summary);
|
||||
|
||||
public record SendMessageRequest(string Content, string ContentType = "text", string? ImageUrl = null);
|
||||
|
||||
public record MessageResponse(
|
||||
Guid Id, Guid SenderId, string SenderRole,
|
||||
string ContentType, string Content,
|
||||
string? ImageUrl, bool IsRead, DateTime CreatedAt);
|
||||
12
backend/src/HealthManager.Application/DTOs/FollowUpDtos.cs
Normal file
12
backend/src/HealthManager.Application/DTOs/FollowUpDtos.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace HealthManager.Application.DTOs.FollowUp;
|
||||
|
||||
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);
|
||||
|
||||
public record FollowUpResponse(
|
||||
Guid Id, Guid PatientId, string PatientName, Guid? DoctorId, string? DoctorName,
|
||||
string Title, string? Description, DateTime ScheduledAt, string Status,
|
||||
string? Notes, bool ReminderEnabled, DateTime CreatedAt);
|
||||
14
backend/src/HealthManager.Application/DTOs/HealthDtos.cs
Normal file
14
backend/src/HealthManager.Application/DTOs/HealthDtos.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace HealthManager.Application.DTOs.Health;
|
||||
|
||||
public record HealthRecordCreateRequest(
|
||||
string Type, string ValueJson, string Unit, DateTime RecordedAt, string? Notes);
|
||||
|
||||
public record HealthRecordResponse(
|
||||
Guid Id, string Type, string ValueJson, string Unit,
|
||||
DateTime RecordedAt, string Source, string? Notes, DateTime CreatedAt);
|
||||
|
||||
public record HealthStatsResponse(
|
||||
string Type, string Unit,
|
||||
HealthRecordResponse? Latest,
|
||||
decimal? WeekAvg, decimal? MonthAvg,
|
||||
string Trend); // up, down, stable
|
||||
18
backend/src/HealthManager.Application/DTOs/MedicationDtos.cs
Normal file
18
backend/src/HealthManager.Application/DTOs/MedicationDtos.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace HealthManager.Application.DTOs.Medication;
|
||||
|
||||
public record MedicationCreateRequest(
|
||||
string DrugName, string Dosage, string Frequency,
|
||||
List<string> TimeSlots, DateOnly StartDate, DateOnly? EndDate, string? Notes);
|
||||
|
||||
public record MedicationResponse(
|
||||
Guid Id, Guid UserId, string DrugName, string Dosage,
|
||||
string Frequency, List<string> TimeSlots,
|
||||
DateOnly StartDate, DateOnly? EndDate, string? Notes,
|
||||
string Status, DateTime CreatedAt);
|
||||
|
||||
public record MedicationRecordResponse(
|
||||
Guid Id, Guid MedicationId, string TimeSlot, DateTime? TakenAt,
|
||||
bool IsTaken, string? SkippedReason, DateTime CreatedAt);
|
||||
|
||||
public record AdherenceResponse(
|
||||
Guid MedicationId, string DrugName, decimal Rate, int TakenDays, int TotalDays);
|
||||
15
backend/src/HealthManager.Application/DTOs/ReportDtos.cs
Normal file
15
backend/src/HealthManager.Application/DTOs/ReportDtos.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace HealthManager.Application.DTOs.Report;
|
||||
|
||||
public record ReportUploadRequest(string Title, string Category, List<string> ImageUrls);
|
||||
|
||||
public record ReportInterpretRequest(string Summary, List<ReportItemDto> Items, string RiskLevel, string? Suggestions);
|
||||
|
||||
public record ReportResponse(
|
||||
Guid Id, Guid PatientId, string PatientName, string? DoctorName,
|
||||
string Title, string Category, List<string> ImageUrls,
|
||||
string Status, string? RiskLevel, string? Summary, string? Suggestions,
|
||||
List<ReportItemDto> Items, DateTime UploadedAt, DateTime? CompletedAt);
|
||||
|
||||
public record ReportItemDto(
|
||||
string ItemName, string ResultValue, string? Unit,
|
||||
string? ReferenceRange, bool IsAbnormal);
|
||||
@@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\HealthManager.Domain\HealthManager.Domain.csproj" />
|
||||
<ProjectReference Include="..\HealthManager.Infrastructure\HealthManager.Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,52 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using HealthManager.Domain.Entities;
|
||||
using HealthManager.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace HealthManager.Application.Services;
|
||||
|
||||
public class AuthService(AppDbContext db)
|
||||
{
|
||||
public async Task<User?> GetUserByPhoneAsync(string phone)
|
||||
{
|
||||
return await db.Users.FirstOrDefaultAsync(u => u.Phone == phone && !u.IsDeleted);
|
||||
}
|
||||
|
||||
public async Task<RefreshToken?> GetRefreshTokenAsync(string token)
|
||||
{
|
||||
return await db.RefreshTokens
|
||||
.Include(rt => rt.User)
|
||||
.FirstOrDefaultAsync(rt => rt.Token == token && rt.RevokedAt == null && rt.ExpiresAt > DateTime.UtcNow);
|
||||
}
|
||||
|
||||
public async Task RevokeRefreshTokenAsync(Guid userId)
|
||||
{
|
||||
var tokens = await db.RefreshTokens
|
||||
.Where(rt => rt.UserId == userId && rt.RevokedAt == null)
|
||||
.ToListAsync();
|
||||
foreach (var t in tokens)
|
||||
t.RevokedAt = DateTime.UtcNow;
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task SaveRefreshTokenAsync(Guid userId, string token, DateTime expiresAt)
|
||||
{
|
||||
db.RefreshTokens.Add(new RefreshToken
|
||||
{
|
||||
UserId = userId,
|
||||
Token = token,
|
||||
ExpiresAt = expiresAt,
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public static string HashPassword(string password)
|
||||
{
|
||||
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(password));
|
||||
return Convert.ToHexStringLower(bytes);
|
||||
}
|
||||
|
||||
public static bool VerifyPassword(string password, string hash)
|
||||
=> HashPassword(password) == hash;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using HealthManager.Domain.Entities;
|
||||
using HealthManager.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace HealthManager.Application.Services;
|
||||
|
||||
public class ConsultationService(AppDbContext db)
|
||||
{
|
||||
public async Task<List<Consultation>> GetPatientConsultationsAsync(Guid patientId)
|
||||
=> await db.Consultations
|
||||
.Include(c => c.Doctor)
|
||||
.Where(c => c.PatientId == patientId)
|
||||
.OrderByDescending(c => c.StartedAt)
|
||||
.ToListAsync();
|
||||
|
||||
public async Task<List<Consultation>> GetDoctorConsultationsAsync(Guid doctorId)
|
||||
=> await db.Consultations
|
||||
.Include(c => c.Patient)
|
||||
.Where(c => c.DoctorId == doctorId)
|
||||
.OrderByDescending(c => c.StartedAt)
|
||||
.ToListAsync();
|
||||
|
||||
public async Task<Consultation?> GetByIdAsync(Guid id)
|
||||
=> await db.Consultations
|
||||
.Include(c => c.Patient)
|
||||
.Include(c => c.Doctor)
|
||||
.FirstOrDefaultAsync(c => c.Id == id);
|
||||
|
||||
public async Task<Consultation> StartAsync(Guid patientId, Guid doctorId, string subject)
|
||||
{
|
||||
var consultation = new Consultation
|
||||
{
|
||||
PatientId = patientId,
|
||||
DoctorId = doctorId,
|
||||
Subject = subject,
|
||||
};
|
||||
db.Consultations.Add(consultation);
|
||||
await db.SaveChangesAsync();
|
||||
return consultation;
|
||||
}
|
||||
|
||||
public async Task<List<ConsultationMessage>> GetMessagesAsync(Guid consultationId)
|
||||
=> await db.ConsultationMessages
|
||||
.Include(m => m.Sender)
|
||||
.Where(m => m.ConsultationId == consultationId)
|
||||
.OrderBy(m => m.CreatedAt)
|
||||
.ToListAsync();
|
||||
|
||||
public async Task<ConsultationMessage> SendMessageAsync(Guid consultationId, Guid senderId, string senderRole,
|
||||
string content, string contentType = "text", string? imageUrl = null)
|
||||
{
|
||||
var message = new ConsultationMessage
|
||||
{
|
||||
ConsultationId = consultationId,
|
||||
SenderId = senderId,
|
||||
SenderRole = senderRole,
|
||||
Content = content,
|
||||
ContentType = contentType,
|
||||
ImageUrl = imageUrl,
|
||||
};
|
||||
db.ConsultationMessages.Add(message);
|
||||
await db.SaveChangesAsync();
|
||||
return message;
|
||||
}
|
||||
|
||||
public async Task<List<User>> GetDoctorsAsync(string? department = null)
|
||||
{
|
||||
var query = db.Users.Where(u => u.Role == "doctor" && u.IsAvailable && !u.IsDeleted);
|
||||
if (!string.IsNullOrEmpty(department))
|
||||
query = query.Where(u => u.Department == department);
|
||||
return await query.ToListAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using HealthManager.Domain.Entities;
|
||||
using HealthManager.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace HealthManager.Application.Services;
|
||||
|
||||
public class FollowUpService(AppDbContext db)
|
||||
{
|
||||
public async Task<List<FollowUp>> GetPatientFollowUpsAsync(Guid patientId)
|
||||
=> await db.FollowUps
|
||||
.Include(f => f.Doctor)
|
||||
.Where(f => f.PatientId == patientId)
|
||||
.OrderBy(f => f.ScheduledAt)
|
||||
.ToListAsync();
|
||||
|
||||
public async Task<List<FollowUp>> GetDoctorFollowUpsAsync(Guid doctorId)
|
||||
=> await db.FollowUps
|
||||
.Include(f => f.Patient)
|
||||
.Where(f => f.DoctorId == doctorId)
|
||||
.OrderBy(f => f.ScheduledAt)
|
||||
.ToListAsync();
|
||||
|
||||
public async Task<FollowUp> AddAsync(Guid patientId, string title, string? description, DateTime scheduledAt, bool reminderEnabled, Guid? doctorId = null)
|
||||
{
|
||||
var followUp = new FollowUp
|
||||
{
|
||||
PatientId = patientId,
|
||||
DoctorId = doctorId,
|
||||
Title = title,
|
||||
Description = description,
|
||||
ScheduledAt = DateTime.SpecifyKind(scheduledAt, DateTimeKind.Utc),
|
||||
ReminderEnabled = reminderEnabled,
|
||||
};
|
||||
db.FollowUps.Add(followUp);
|
||||
await db.SaveChangesAsync();
|
||||
return followUp;
|
||||
}
|
||||
|
||||
public async Task<FollowUp?> GetByIdAsync(Guid id)
|
||||
=> await db.FollowUps
|
||||
.Include(f => f.Patient)
|
||||
.Include(f => f.Doctor)
|
||||
.FirstOrDefaultAsync(f => f.Id == id);
|
||||
|
||||
public async Task<FollowUp?> UpdateAsync(Guid id, Guid doctorId, string? title, string? description,
|
||||
DateTime? scheduledAt, string? status, string? notes)
|
||||
{
|
||||
var followUp = await db.FollowUps.FindAsync(id);
|
||||
if (followUp == null) return null;
|
||||
|
||||
if (title != null) followUp.Title = title;
|
||||
if (description != null) followUp.Description = description;
|
||||
if (scheduledAt.HasValue) followUp.ScheduledAt = DateTime.SpecifyKind(scheduledAt.Value, DateTimeKind.Utc);
|
||||
if (status != null) followUp.Status = status;
|
||||
if (notes != null) followUp.Notes = notes;
|
||||
followUp.DoctorId = doctorId;
|
||||
followUp.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
return followUp;
|
||||
}
|
||||
}
|
||||
114
backend/src/HealthManager.Application/Services/HealthService.cs
Normal file
114
backend/src/HealthManager.Application/Services/HealthService.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
using System.Text.Json;
|
||||
using HealthManager.Domain.Entities;
|
||||
using HealthManager.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace HealthManager.Application.Services;
|
||||
|
||||
public class HealthService(AppDbContext db)
|
||||
{
|
||||
public async Task<List<HealthRecord>> GetRecordsAsync(Guid userId, string? type, int days = 30)
|
||||
{
|
||||
var cutoff = DateTime.UtcNow.AddDays(-days);
|
||||
var query = db.HealthRecords.Where(hr => hr.UserId == userId && hr.RecordedAt >= cutoff);
|
||||
|
||||
if (!string.IsNullOrEmpty(type))
|
||||
query = query.Where(hr => hr.Type == type);
|
||||
|
||||
return await query.OrderByDescending(hr => hr.RecordedAt).ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<HealthRecord?> GetLatestAsync(Guid userId, string type)
|
||||
{
|
||||
return await db.HealthRecords
|
||||
.Where(hr => hr.UserId == userId && hr.Type == type)
|
||||
.OrderByDescending(hr => hr.RecordedAt)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<HealthRecord> AddRecordAsync(Guid userId, string type, string valueJson, string unit, DateTime recordedAt, string? notes)
|
||||
{
|
||||
var record = new HealthRecord
|
||||
{
|
||||
UserId = userId,
|
||||
Type = type,
|
||||
Value = JsonDocument.Parse(valueJson),
|
||||
Unit = unit,
|
||||
RecordedAt = DateTime.SpecifyKind(recordedAt, DateTimeKind.Utc),
|
||||
Notes = notes,
|
||||
Source = "manual",
|
||||
};
|
||||
db.HealthRecords.Add(record);
|
||||
await db.SaveChangesAsync();
|
||||
return record;
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, object>> GetStatsAsync(Guid userId)
|
||||
{
|
||||
var types = new[] { "blood_pressure", "heart_rate", "blood_sugar", "spo2", "weight", "steps" };
|
||||
var result = new Dictionary<string, object>();
|
||||
|
||||
foreach (var type in types)
|
||||
{
|
||||
var latest = await GetLatestAsync(userId, type);
|
||||
var records = await db.HealthRecords
|
||||
.Where(hr => hr.UserId == userId && hr.Type == type)
|
||||
.OrderByDescending(hr => hr.RecordedAt)
|
||||
.ToListAsync();
|
||||
|
||||
// Calculate trend from last 7 days vs previous 7 days
|
||||
var now = DateTime.UtcNow;
|
||||
var weekAgo = now.AddDays(-7);
|
||||
var twoWeeksAgo = now.AddDays(-14);
|
||||
|
||||
var recentValues = records.Where(r => r.RecordedAt >= weekAgo).ToList();
|
||||
var olderValues = records.Where(r => r.RecordedAt >= twoWeeksAgo && r.RecordedAt < weekAgo).ToList();
|
||||
|
||||
var recentAvg = CalculateAvg(recentValues);
|
||||
var olderAvg = CalculateAvg(olderValues);
|
||||
|
||||
string trend = "stable";
|
||||
if (olderAvg > 0 && recentAvg > 0)
|
||||
{
|
||||
if (recentAvg > olderAvg * 1.03m) trend = "up";
|
||||
else if (recentAvg < olderAvg * 0.97m) trend = "down";
|
||||
}
|
||||
|
||||
result[type] = new
|
||||
{
|
||||
Latest = latest,
|
||||
WeekAvg = recentAvg > 0 ? recentAvg : (decimal?)null,
|
||||
MonthAvg = records.Count > 0 ? CalculateAvg(records) : (decimal?)null,
|
||||
Trend = trend,
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static decimal CalculateAvg(List<HealthRecord> records)
|
||||
{
|
||||
if (records.Count == 0) return 0;
|
||||
|
||||
var sum = 0m;
|
||||
var count = 0;
|
||||
foreach (var r in records)
|
||||
{
|
||||
// Blood pressure: { systolic, diastolic, pulse }
|
||||
if (r.Value.RootElement.TryGetProperty("systolic", out var sys) &&
|
||||
r.Value.RootElement.TryGetProperty("diastolic", out var dia))
|
||||
{
|
||||
sum += sys.GetDecimal(); // Use systolic for trend
|
||||
count++;
|
||||
}
|
||||
// All other types: { value } — heart_rate, blood_sugar, spo2, weight, steps
|
||||
else if (r.Value.RootElement.TryGetProperty("value", out var val))
|
||||
{
|
||||
sum += val.GetDecimal();
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return count > 0 ? sum / count : 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
using HealthManager.Domain.Entities;
|
||||
using HealthManager.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace HealthManager.Application.Services;
|
||||
|
||||
public class MedicationService(AppDbContext db)
|
||||
{
|
||||
public async Task<List<Medication>> GetUserMedicationsAsync(Guid userId)
|
||||
=> await db.Medications
|
||||
.Where(m => m.UserId == userId)
|
||||
.OrderByDescending(m => m.CreatedAt)
|
||||
.ToListAsync();
|
||||
|
||||
public async Task<Medication?> GetByIdAsync(Guid id)
|
||||
=> await db.Medications.FindAsync(id);
|
||||
|
||||
public async Task<Medication> AddAsync(Guid userId, string drugName, string dosage, string frequency,
|
||||
List<string> timeSlots, DateOnly startDate, DateOnly? endDate, string? notes, Guid? doctorId = null)
|
||||
{
|
||||
var med = new Medication
|
||||
{
|
||||
UserId = userId,
|
||||
DoctorId = doctorId,
|
||||
DrugName = drugName,
|
||||
Dosage = dosage,
|
||||
Frequency = frequency,
|
||||
TimeSlots = timeSlots,
|
||||
StartDate = startDate,
|
||||
EndDate = endDate,
|
||||
Notes = notes,
|
||||
};
|
||||
db.Medications.Add(med);
|
||||
await db.SaveChangesAsync();
|
||||
return med;
|
||||
}
|
||||
|
||||
public async Task<List<MedicationRecord>> GetRecordsAsync(Guid medicationId)
|
||||
=> await db.MedicationRecords
|
||||
.Where(mr => mr.MedicationId == medicationId)
|
||||
.OrderByDescending(mr => mr.CreatedAt)
|
||||
.ToListAsync();
|
||||
|
||||
public async Task<MedicationRecord> MarkTakenAsync(Guid medicationId, Guid userId, string timeSlot)
|
||||
{
|
||||
// Find existing record for today's slot, update it instead of creating duplicate
|
||||
var today = DateTime.UtcNow.Date;
|
||||
var existing = await db.MedicationRecords
|
||||
.FirstOrDefaultAsync(mr =>
|
||||
mr.MedicationId == medicationId &&
|
||||
mr.UserId == userId &&
|
||||
mr.TimeSlot == timeSlot &&
|
||||
mr.TakenAt.HasValue &&
|
||||
mr.TakenAt.Value.Date == today);
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
existing.TakenAt = DateTime.UtcNow;
|
||||
existing.IsTaken = true;
|
||||
existing.SkippedReason = null;
|
||||
await db.SaveChangesAsync();
|
||||
return existing;
|
||||
}
|
||||
|
||||
var record = new MedicationRecord
|
||||
{
|
||||
MedicationId = medicationId,
|
||||
UserId = userId,
|
||||
TimeSlot = timeSlot,
|
||||
IsTaken = true,
|
||||
TakenAt = DateTime.UtcNow,
|
||||
};
|
||||
db.MedicationRecords.Add(record);
|
||||
await db.SaveChangesAsync();
|
||||
return record;
|
||||
}
|
||||
|
||||
public async Task<decimal> GetAdherenceRateAsync(Guid medicationId, int days = 30)
|
||||
{
|
||||
var medication = await db.Medications.FindAsync(medicationId);
|
||||
if (medication == null) return 0;
|
||||
|
||||
var cutoff = DateTime.UtcNow.AddDays(-days);
|
||||
var allRecords = await db.MedicationRecords
|
||||
.Where(mr => mr.MedicationId == medicationId && mr.CreatedAt >= cutoff)
|
||||
.ToListAsync();
|
||||
|
||||
var takenCount = allRecords.Count(mr => mr.IsTaken);
|
||||
var totalCount = allRecords.Count;
|
||||
|
||||
return totalCount > 0 ? Math.Round((decimal)takenCount / totalCount * 100, 1) : 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using HealthManager.Domain.Entities;
|
||||
using HealthManager.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace HealthManager.Application.Services;
|
||||
|
||||
public class NotificationService(AppDbContext db)
|
||||
{
|
||||
public async Task<List<Notification>> GetUserNotificationsAsync(Guid userId)
|
||||
=> await db.Notifications
|
||||
.Where(n => n.UserId == userId)
|
||||
.OrderByDescending(n => n.CreatedAt)
|
||||
.ToListAsync();
|
||||
|
||||
public async Task<int> GetUnreadCountAsync(Guid userId)
|
||||
=> await db.Notifications.CountAsync(n => n.UserId == userId && !n.IsRead);
|
||||
|
||||
public async Task MarkAsReadAsync(Guid notificationId)
|
||||
{
|
||||
var notification = await db.Notifications.FindAsync(notificationId);
|
||||
if (notification != null && !notification.IsRead)
|
||||
{
|
||||
notification.IsRead = true;
|
||||
notification.ReadAt = DateTime.UtcNow;
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task MarkAllAsReadAsync(Guid userId)
|
||||
{
|
||||
var unread = await db.Notifications.Where(n => n.UserId == userId && !n.IsRead).ToListAsync();
|
||||
foreach (var n in unread)
|
||||
{
|
||||
n.IsRead = true;
|
||||
n.ReadAt = DateTime.UtcNow;
|
||||
}
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task CreateAsync(Guid userId, string type, string title, string content, Guid? relatedId = null)
|
||||
{
|
||||
db.Notifications.Add(new Notification
|
||||
{
|
||||
UserId = userId,
|
||||
Type = type,
|
||||
Title = title,
|
||||
Content = content,
|
||||
RelatedId = relatedId,
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using HealthManager.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace HealthManager.Application.Services;
|
||||
|
||||
public class PatientService(AppDbContext db)
|
||||
{
|
||||
public async Task<List<object>> GetPatientsAsync(string? search, string? department, int page = 1, int pageSize = 20)
|
||||
{
|
||||
var query = db.Users.Where(u => u.Role == "patient" && !u.IsDeleted);
|
||||
|
||||
if (!string.IsNullOrEmpty(search))
|
||||
query = query.Where(u => u.Name.Contains(search) || u.Phone.Contains(search));
|
||||
|
||||
var patients = await query
|
||||
.OrderByDescending(u => u.CreatedAt)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.Select(u => new
|
||||
{
|
||||
u.Id, u.Name, u.Phone, u.Gender, u.Birthday,
|
||||
u.StentDate, u.StentType,
|
||||
u.CreatedAt,
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return patients.Cast<object>().ToList();
|
||||
}
|
||||
|
||||
public async Task<object?> GetPatientDetailAsync(Guid patientId)
|
||||
{
|
||||
return await db.Users
|
||||
.Where(u => u.Id == patientId && u.Role == "patient" && !u.IsDeleted)
|
||||
.Select(u => new
|
||||
{
|
||||
u.Id, u.Name, u.Phone, u.Gender, u.Birthday,
|
||||
u.HeightCm, u.WeightKg, u.MedicalHistory,
|
||||
u.StentDate, u.StentType,
|
||||
u.CreatedAt,
|
||||
})
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using HealthManager.Domain.Entities;
|
||||
using HealthManager.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace HealthManager.Application.Services;
|
||||
|
||||
public class ReportService(AppDbContext db)
|
||||
{
|
||||
public async Task<List<Report>> GetPatientReportsAsync(Guid patientId)
|
||||
=> await db.Reports
|
||||
.Include(r => r.Doctor)
|
||||
.Include(r => r.Items)
|
||||
.Where(r => r.PatientId == patientId)
|
||||
.OrderByDescending(r => r.UploadedAt)
|
||||
.ToListAsync();
|
||||
|
||||
public async Task<Report?> GetByIdAsync(Guid id)
|
||||
=> await db.Reports
|
||||
.Include(r => r.Patient)
|
||||
.Include(r => r.Doctor)
|
||||
.Include(r => r.Items)
|
||||
.FirstOrDefaultAsync(r => r.Id == id);
|
||||
|
||||
public async Task<Report> UploadAsync(Guid patientId, string title, string category, List<string> imageUrls)
|
||||
{
|
||||
var report = new Report
|
||||
{
|
||||
PatientId = patientId,
|
||||
Title = title,
|
||||
Category = category,
|
||||
ImageUrls = imageUrls,
|
||||
UploadedBy = patientId,
|
||||
};
|
||||
db.Reports.Add(report);
|
||||
await db.SaveChangesAsync();
|
||||
return report;
|
||||
}
|
||||
|
||||
public async Task<List<Report>> GetPendingAsync()
|
||||
=> await db.Reports
|
||||
.Include(r => r.Patient)
|
||||
.Where(r => r.Status == "pending")
|
||||
.OrderByDescending(r => r.UploadedAt)
|
||||
.ToListAsync();
|
||||
|
||||
public async Task<Report> InterpretAsync(Guid reportId, Guid doctorId, string summary,
|
||||
List<(string name, string value, string? unit, string? range, bool abnormal)> items,
|
||||
string riskLevel, string? suggestions)
|
||||
{
|
||||
var report = await db.Reports.FindAsync(reportId) ?? throw new InvalidOperationException("Report not found");
|
||||
report.DoctorId = doctorId;
|
||||
report.Status = "completed";
|
||||
report.Summary = summary;
|
||||
report.RiskLevel = riskLevel;
|
||||
report.Suggestions = suggestions;
|
||||
report.CompletedAt = DateTime.UtcNow;
|
||||
|
||||
var order = 0;
|
||||
foreach (var (name, value, unit, range, abnormal) in items)
|
||||
{
|
||||
db.ReportItems.Add(new ReportItem
|
||||
{
|
||||
ReportId = reportId,
|
||||
ItemName = name,
|
||||
ResultValue = value,
|
||||
Unit = unit,
|
||||
ReferenceRange = range,
|
||||
IsAbnormal = abnormal,
|
||||
SortOrder = order++,
|
||||
});
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
return report;
|
||||
}
|
||||
}
|
||||
19
backend/src/HealthManager.Domain/Entities/Consultation.cs
Normal file
19
backend/src/HealthManager.Domain/Entities/Consultation.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace HealthManager.Domain.Entities;
|
||||
|
||||
public class Consultation
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid PatientId { get; set; }
|
||||
public Guid DoctorId { get; set; }
|
||||
public string? Subject { get; set; }
|
||||
public string Status { get; set; } = "active"; // active, closed
|
||||
public DateTime StartedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime? ClosedAt { get; set; }
|
||||
public Guid? ClosedBy { get; set; }
|
||||
public string? Summary { get; set; }
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public User Patient { get; set; } = null!;
|
||||
public User Doctor { get; set; } = null!;
|
||||
public ICollection<ConsultationMessage> Messages { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace HealthManager.Domain.Entities;
|
||||
|
||||
public class ConsultationMessage
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid ConsultationId { get; set; }
|
||||
public Guid SenderId { get; set; }
|
||||
public string SenderRole { get; set; } = string.Empty; // patient, doctor, system
|
||||
public string ContentType { get; set; } = "text"; // text, image, file, template
|
||||
public string Content { get; set; } = string.Empty;
|
||||
public string? ImageUrl { get; set; }
|
||||
public string? FileUrl { get; set; }
|
||||
public bool IsRead { get; set; }
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public Consultation Consultation { get; set; } = null!;
|
||||
public User Sender { get; set; } = null!;
|
||||
}
|
||||
18
backend/src/HealthManager.Domain/Entities/Device.cs
Normal file
18
backend/src/HealthManager.Domain/Entities/Device.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace HealthManager.Domain.Entities;
|
||||
|
||||
public class Device
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid? UserId { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Type { get; set; } = string.Empty;
|
||||
public string? MacAddress { get; set; }
|
||||
public string? SerialNumber { get; set; }
|
||||
public bool IsBound { get; set; }
|
||||
public DateTime? BoundAt { get; set; }
|
||||
public DateTime? LastSyncAt { get; set; }
|
||||
public int? BatteryLevel { get; set; }
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public User? User { get; set; }
|
||||
}
|
||||
16
backend/src/HealthManager.Domain/Entities/DietRecord.cs
Normal file
16
backend/src/HealthManager.Domain/Entities/DietRecord.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace HealthManager.Domain.Entities;
|
||||
|
||||
public class DietRecord
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid UserId { get; set; }
|
||||
public string MealType { get; set; } = string.Empty; // breakfast, lunch, dinner, snack
|
||||
public string FoodName { get; set; } = string.Empty;
|
||||
public string? Portion { get; set; }
|
||||
public int? Calories { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public DateOnly RecordedAt { get; set; }
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public User User { get; set; } = null!;
|
||||
}
|
||||
16
backend/src/HealthManager.Domain/Entities/ExerciseRecord.cs
Normal file
16
backend/src/HealthManager.Domain/Entities/ExerciseRecord.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace HealthManager.Domain.Entities;
|
||||
|
||||
public class ExerciseRecord
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid UserId { get; set; }
|
||||
public string ExerciseType { get; set; } = string.Empty;
|
||||
public int DurationMin { get; set; }
|
||||
public string? Intensity { get; set; } // low, moderate, high
|
||||
public int? CaloriesBurned { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public DateOnly RecordedAt { get; set; }
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public User User { get; set; } = null!;
|
||||
}
|
||||
19
backend/src/HealthManager.Domain/Entities/FollowUp.cs
Normal file
19
backend/src/HealthManager.Domain/Entities/FollowUp.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace HealthManager.Domain.Entities;
|
||||
|
||||
public class FollowUp
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid PatientId { get; set; }
|
||||
public Guid? DoctorId { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public DateTime ScheduledAt { get; set; }
|
||||
public string Status { get; set; } = "upcoming"; // upcoming, completed, cancelled, rescheduled
|
||||
public string? Notes { get; set; }
|
||||
public bool ReminderEnabled { get; set; } = true;
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public User Patient { get; set; } = null!;
|
||||
public User? Doctor { get; set; }
|
||||
}
|
||||
18
backend/src/HealthManager.Domain/Entities/HealthRecord.cs
Normal file
18
backend/src/HealthManager.Domain/Entities/HealthRecord.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace HealthManager.Domain.Entities;
|
||||
|
||||
public class HealthRecord
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid UserId { get; set; }
|
||||
public string Type { get; set; } = string.Empty; // blood_pressure, heart_rate, blood_sugar, spo2, weight, steps
|
||||
public JsonDocument Value { get; set; } = null!;
|
||||
public string Unit { get; set; } = string.Empty;
|
||||
public DateTime RecordedAt { get; set; }
|
||||
public string Source { get; set; } = "manual"; // manual, device, doctor
|
||||
public string? Notes { get; set; }
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public User User { get; set; } = null!;
|
||||
}
|
||||
22
backend/src/HealthManager.Domain/Entities/Medication.cs
Normal file
22
backend/src/HealthManager.Domain/Entities/Medication.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
namespace HealthManager.Domain.Entities;
|
||||
|
||||
public class Medication
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid UserId { get; set; }
|
||||
public Guid? DoctorId { get; set; }
|
||||
public string DrugName { get; set; } = string.Empty;
|
||||
public string Dosage { get; set; } = string.Empty;
|
||||
public string Frequency { get; set; } = string.Empty;
|
||||
public List<string> TimeSlots { get; set; } = [];
|
||||
public DateOnly StartDate { get; set; }
|
||||
public DateOnly? EndDate { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public string Status { get; set; } = "active"; // active, completed, stopped
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public User User { get; set; } = null!;
|
||||
public User? Doctor { get; set; }
|
||||
public ICollection<MedicationRecord> Records { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace HealthManager.Domain.Entities;
|
||||
|
||||
public class MedicationRecord
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid MedicationId { get; set; }
|
||||
public Guid UserId { get; set; }
|
||||
public string TimeSlot { get; set; } = string.Empty;
|
||||
public DateTime? TakenAt { get; set; }
|
||||
public bool IsTaken { get; set; }
|
||||
public string? SkippedReason { get; set; }
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public Medication Medication { get; set; } = null!;
|
||||
public User User { get; set; } = null!;
|
||||
}
|
||||
16
backend/src/HealthManager.Domain/Entities/Notification.cs
Normal file
16
backend/src/HealthManager.Domain/Entities/Notification.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace HealthManager.Domain.Entities;
|
||||
|
||||
public class Notification
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid UserId { get; set; }
|
||||
public string Type { get; set; } = string.Empty; // medication_reminder, followup_reminder, consultation_new, report_completed, health_alert, system
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Content { get; set; } = string.Empty;
|
||||
public Guid? RelatedId { get; set; }
|
||||
public bool IsRead { get; set; }
|
||||
public DateTime? ReadAt { get; set; }
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public User User { get; set; } = null!;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace HealthManager.Domain.Entities;
|
||||
|
||||
public class QuickReplyTemplate
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid DoctorId { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Content { get; set; } = string.Empty;
|
||||
public string? Category { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public User Doctor { get; set; } = null!;
|
||||
}
|
||||
13
backend/src/HealthManager.Domain/Entities/RefreshToken.cs
Normal file
13
backend/src/HealthManager.Domain/Entities/RefreshToken.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace HealthManager.Domain.Entities;
|
||||
|
||||
public class RefreshToken
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid UserId { get; set; }
|
||||
public string Token { get; set; } = string.Empty;
|
||||
public DateTime ExpiresAt { get; set; }
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime? RevokedAt { get; set; }
|
||||
|
||||
public User User { get; set; } = null!;
|
||||
}
|
||||
22
backend/src/HealthManager.Domain/Entities/Report.cs
Normal file
22
backend/src/HealthManager.Domain/Entities/Report.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
namespace HealthManager.Domain.Entities;
|
||||
|
||||
public class Report
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid PatientId { get; set; }
|
||||
public Guid? DoctorId { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Category { get; set; } = string.Empty;
|
||||
public List<string> ImageUrls { get; set; } = [];
|
||||
public string Status { get; set; } = "pending"; // pending, interpreting, completed
|
||||
public string? RiskLevel { get; set; } // normal, attention, abnormal
|
||||
public string? Summary { get; set; }
|
||||
public string? Suggestions { get; set; }
|
||||
public Guid UploadedBy { get; set; }
|
||||
public DateTime UploadedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime? CompletedAt { get; set; }
|
||||
|
||||
public User Patient { get; set; } = null!;
|
||||
public User? Doctor { get; set; }
|
||||
public ICollection<ReportItem> Items { get; set; } = [];
|
||||
}
|
||||
15
backend/src/HealthManager.Domain/Entities/ReportItem.cs
Normal file
15
backend/src/HealthManager.Domain/Entities/ReportItem.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace HealthManager.Domain.Entities;
|
||||
|
||||
public class ReportItem
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid ReportId { get; set; }
|
||||
public string ItemName { get; set; } = string.Empty;
|
||||
public string ResultValue { get; set; } = string.Empty;
|
||||
public string? Unit { get; set; }
|
||||
public string? ReferenceRange { get; set; }
|
||||
public bool IsAbnormal { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
|
||||
public Report Report { get; set; } = null!;
|
||||
}
|
||||
41
backend/src/HealthManager.Domain/Entities/User.cs
Normal file
41
backend/src/HealthManager.Domain/Entities/User.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
namespace HealthManager.Domain.Entities;
|
||||
|
||||
public class User
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Role { get; set; } = string.Empty; // patient, doctor, admin
|
||||
public string Phone { get; set; } = string.Empty;
|
||||
public string PasswordHash { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? AvatarUrl { get; set; }
|
||||
|
||||
// Patient fields
|
||||
public string? Gender { get; set; }
|
||||
public DateOnly? Birthday { get; set; }
|
||||
public decimal? HeightCm { get; set; }
|
||||
public decimal? WeightKg { get; set; }
|
||||
public List<string>? MedicalHistory { get; set; }
|
||||
public DateOnly? StentDate { get; set; }
|
||||
public string? StentType { get; set; }
|
||||
|
||||
// Doctor fields
|
||||
public string? Department { get; set; }
|
||||
public string? Title { get; set; }
|
||||
public List<string>? Specialty { get; set; }
|
||||
public string? Introduction { get; set; }
|
||||
public bool IsAvailable { get; set; } = true;
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||
public bool IsDeleted { get; set; }
|
||||
public DateTime? DeletedAt { get; set; }
|
||||
|
||||
// Navigation
|
||||
public ICollection<RefreshToken> RefreshTokens { get; set; } = [];
|
||||
public ICollection<HealthRecord> HealthRecords { get; set; } = [];
|
||||
public ICollection<Medication> Medications { get; set; } = [];
|
||||
public ICollection<MedicationRecord> MedicationRecords { get; set; } = [];
|
||||
public ICollection<ExerciseRecord> ExerciseRecords { get; set; } = [];
|
||||
public ICollection<DietRecord> DietRecords { get; set; } = [];
|
||||
public ICollection<Notification> Notifications { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace HealthManager.Domain.Interfaces;
|
||||
|
||||
public interface IJwtProvider
|
||||
{
|
||||
string GenerateAccessToken(Guid userId, string name, string role);
|
||||
string GenerateRefreshToken();
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\HealthManager.Domain\HealthManager.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.18.0" />
|
||||
<PackageReference Include="Minio" Version="7.0.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" />
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.13.1" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.18.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,39 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using HealthManager.Domain.Interfaces;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace HealthManager.Infrastructure.Services;
|
||||
|
||||
public class JwtProvider(IConfiguration configuration) : IJwtProvider
|
||||
{
|
||||
private static readonly JwtSecurityTokenHandler TokenHandler = new();
|
||||
public string GenerateAccessToken(Guid userId, string name, string role)
|
||||
{
|
||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:Secret"]!));
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim(ClaimTypes.NameIdentifier, userId.ToString()),
|
||||
new Claim(ClaimTypes.Name, name),
|
||||
new Claim(ClaimTypes.Role, role),
|
||||
};
|
||||
|
||||
var token = new JwtSecurityToken(
|
||||
issuer: configuration["Jwt:Issuer"],
|
||||
audience: configuration["Jwt:Audience"],
|
||||
claims: claims,
|
||||
expires: DateTime.UtcNow.AddMinutes(30),
|
||||
signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256));
|
||||
|
||||
return TokenHandler.WriteToken(token);
|
||||
}
|
||||
|
||||
public string GenerateRefreshToken()
|
||||
{
|
||||
var bytes = RandomNumberGenerator.GetBytes(64);
|
||||
return Convert.ToBase64String(bytes);
|
||||
}
|
||||
}
|
||||
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