feat: 医生端完整实现 + 双角色登录 + 健康日历 + 饮食记录重做 + 配色系统重构
后端: - User表加Role字段 + DoctorProfile表 + 废弃旧Doctor数据 - JWT加Role claim + 注册/登录拆分(/api/auth/register + /api/auth/login) - 医生端点全量加JWT鉴权+Role校验(15个端点) - 日历端点改造(用药/运动/随访按日期+星期匹配) - 饮食记录PUT端点 + 健康记录DELETE端点 - 用户Profile端点返回Role 前端: - 登录页重做(角色选择卡片 用户紫/医生蓝 + 审核码6666 + 注册返登录) - App路由分流(userRole→患者端/医生端) - 医生端: 工作台Dashboard + 患者管理(搜索分页) + 问诊/报告/随访CRUD - 报告审核: AI预分析+指标表格+严重程度4级+模板多选+评语 - 随访编辑: 患者选择器+日期时间+打通患者端 - 健康日历: 可点击日历+当日安排列表+用药/运动/随访颜色标记 - 饮食记录: 热量趋势7/30天+日历(一周)+当日胶囊详情+侧滑编辑删除 - 健康档案: 日期选择器+手术可添加多条+白底渐变保存按钮 - 配色系统: 清理25个未使用色+新增doctorBlue/drawerGradient/pageGrey - 患者端侧边栏: 新渐变+白底按钮+紫色汉堡图标 - AI对话: 用户气泡白底黑字+药管家改蓝色 - App启动闪屏: 防登录页闪烁 文档: color_design_system.md(20页面完整配色设计+图标底色对照表)
This commit is contained in:
21
backend/src/Health.Domain/Entities/doctor_profile.cs
Normal file
21
backend/src/Health.Domain/Entities/doctor_profile.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
namespace Health.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 医生扩展信息
|
||||
/// </summary>
|
||||
public sealed class DoctorProfile
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid UserId { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public string? Title { get; set; }
|
||||
public string? Department { get; set; }
|
||||
public string? Hospital { get; set; }
|
||||
public string? AvatarUrl { get; set; }
|
||||
public bool IsOnline { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public User User { get; set; } = null!;
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
namespace Health.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 用户(患者)
|
||||
/// 用户(支持用户/医生双角色)
|
||||
/// </summary>
|
||||
public sealed class User
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Phone { get; set; } = string.Empty;
|
||||
public string Role { get; set; } = "User"; // "User" | "Doctor"
|
||||
public string? Name { get; set; }
|
||||
public string? Gender { get; set; }
|
||||
public DateOnly? BirthDate { get; set; }
|
||||
@@ -26,4 +27,5 @@ public sealed class User
|
||||
public ICollection<DeviceToken> DeviceTokens { get; set; } = [];
|
||||
public HealthArchive? HealthArchive { get; set; }
|
||||
public NotificationPreference? NotificationPreference { get; set; }
|
||||
public DoctorProfile? DoctorProfile { get; set; }
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options) : DbCon
|
||||
public DbSet<Consultation> Consultations => Set<Consultation>();
|
||||
public DbSet<ConsultationMessage> ConsultationMessages => Set<ConsultationMessage>();
|
||||
public DbSet<Doctor> Doctors => Set<Doctor>();
|
||||
public DbSet<DoctorProfile> DoctorProfiles => Set<DoctorProfile>();
|
||||
public DbSet<FollowUp> FollowUps => Set<FollowUp>();
|
||||
public DbSet<HealthArchive> HealthArchives => Set<HealthArchive>();
|
||||
|
||||
@@ -41,6 +42,14 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options) : DbCon
|
||||
builder.Entity<User>(e =>
|
||||
{
|
||||
e.HasIndex(u => u.Phone).IsUnique();
|
||||
e.Property(u => u.Role).HasMaxLength(32).HasDefaultValue("User");
|
||||
});
|
||||
|
||||
// ---- DoctorProfile ----
|
||||
builder.Entity<DoctorProfile>(e =>
|
||||
{
|
||||
e.HasIndex(d => d.UserId).IsUnique();
|
||||
e.HasOne(d => d.User).WithOne(u => u.DoctorProfile).HasForeignKey<DoctorProfile>(d => d.UserId);
|
||||
});
|
||||
|
||||
// ---- HealthRecord ----
|
||||
|
||||
@@ -7,39 +7,7 @@ public static class DataSeeder
|
||||
{
|
||||
public static async Task SeedAsync(AppDbContext db)
|
||||
{
|
||||
// 种子医生数据(固定 ID,与客户端 fallback 保持一致)
|
||||
if (!db.Doctors.Any())
|
||||
{
|
||||
db.Doctors.AddRange(
|
||||
new Doctor
|
||||
{
|
||||
Id = Guid.Parse("ef0953c9-eb63-4d03-b6d7-050a1897d4a3"),
|
||||
Name = "王建国",
|
||||
Title = "主任医师",
|
||||
Department = "心血管内科",
|
||||
Introduction = "擅长冠心病术后管理、心脏康复指导",
|
||||
IsActive = true
|
||||
},
|
||||
new Doctor
|
||||
{
|
||||
Id = Guid.Parse("d4148733-b538-4398-af17-0c7592fc0c2d"),
|
||||
Name = "李芳",
|
||||
Title = "副主任医师",
|
||||
Department = "营养科",
|
||||
Introduction = "擅长术后营养指导、膳食规划",
|
||||
IsActive = true
|
||||
},
|
||||
new Doctor
|
||||
{
|
||||
Id = Guid.Parse("468b82e2-d95a-4436-bff6-a50eecf99a66"),
|
||||
Name = "张明",
|
||||
Title = "主任医师",
|
||||
Department = "心脏康复科",
|
||||
Introduction = "擅长心脏术后运动康复、心肺功能评估",
|
||||
IsActive = true
|
||||
}
|
||||
);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
// 医生不再使用种子数据,通过注册创建
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,12 +18,14 @@ public sealed class JwtProvider(IConfiguration configuration)
|
||||
/// <summary>
|
||||
/// 生成 access_token(30 分钟有效)
|
||||
/// </summary>
|
||||
public string GenerateAccessToken(Guid userId, string phone)
|
||||
public string GenerateAccessToken(Guid userId, string phone, string role = "User")
|
||||
{
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim(ClaimTypes.NameIdentifier, userId.ToString()),
|
||||
new Claim(ClaimTypes.MobilePhone, phone),
|
||||
new Claim(ClaimTypes.Role, role),
|
||||
new Claim("Role", role),
|
||||
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,11 @@ namespace Health.WebApi.Endpoints;
|
||||
/// </summary>
|
||||
public static class AuthEndpoints
|
||||
{
|
||||
/// <summary>
|
||||
/// 医生注册审核码(后期可移到配置表)
|
||||
/// </summary>
|
||||
private const string DoctorInviteCode = "6666";
|
||||
|
||||
public static void MapAuthEndpoints(this WebApplication app)
|
||||
{
|
||||
// 发送短信验证码
|
||||
@@ -34,14 +39,14 @@ public static class AuthEndpoints
|
||||
return Results.Ok(new { code = 0, data = new { success = true, devCode = code }, message = (string?)null });
|
||||
});
|
||||
|
||||
// 手机号+验证码登录
|
||||
app.MapPost("/api/auth/login", async (
|
||||
LoginRequest request,
|
||||
// ── 注册(新用户,需选身份)──
|
||||
app.MapPost("/api/auth/register", async (
|
||||
RegisterRequest request,
|
||||
AppDbContext db,
|
||||
JwtProvider jwt,
|
||||
CancellationToken ct) =>
|
||||
{
|
||||
// 开发阶段:任意6位数字通过
|
||||
// 验证码
|
||||
var validCode = await db.VerificationCodes
|
||||
.Where(v => v.Phone == request.Phone
|
||||
&& v.Code == request.SmsCode
|
||||
@@ -55,44 +60,49 @@ public static class AuthEndpoints
|
||||
|
||||
validCode.IsUsed = true;
|
||||
|
||||
// 查找或创建用户
|
||||
var user = await db.Users.FirstOrDefaultAsync(u => u.Phone == request.Phone, ct);
|
||||
var isNew = false;
|
||||
if (user == null)
|
||||
// 检查手机号是否已注册
|
||||
var existing = await db.Users.FirstOrDefaultAsync(u => u.Phone == request.Phone, ct);
|
||||
if (existing != null)
|
||||
return Results.Ok(new { code = 40002, data = (object?)null, message = "该手机号已注册,请直接登录" });
|
||||
|
||||
// 校验角色
|
||||
var role = request.Role;
|
||||
if (role != "User" && role != "Doctor")
|
||||
return Results.Ok(new { code = 40003, data = (object?)null, message = "角色参数无效" });
|
||||
|
||||
// 医生注册需校验审核码
|
||||
if (role == "Doctor" && request.InviteCode != DoctorInviteCode)
|
||||
return Results.Ok(new { code = 40004, data = (object?)null, message = "审核码错误" });
|
||||
|
||||
// 创建用户
|
||||
var user = new User
|
||||
{
|
||||
user = new User
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Phone = request.Phone,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow,
|
||||
};
|
||||
db.Users.Add(user);
|
||||
isNew = true;
|
||||
Id = Guid.NewGuid(),
|
||||
Phone = request.Phone,
|
||||
Role = role,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow,
|
||||
};
|
||||
db.Users.Add(user);
|
||||
|
||||
// 创建默认通知偏好
|
||||
db.NotificationPreferences.Add(new NotificationPreference
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = user.Id,
|
||||
});
|
||||
|
||||
// 创建空健康档案
|
||||
db.HealthArchives.Add(new HealthArchive
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = user.Id,
|
||||
});
|
||||
// 用户端:创建通知偏好+健康档案
|
||||
if (role == "User")
|
||||
{
|
||||
db.NotificationPreferences.Add(new NotificationPreference { Id = Guid.NewGuid(), UserId = user.Id });
|
||||
db.HealthArchives.Add(new HealthArchive { Id = Guid.NewGuid(), UserId = user.Id });
|
||||
}
|
||||
|
||||
// 医生端:创建空医生档案
|
||||
if (role == "Doctor")
|
||||
{
|
||||
db.DoctorProfiles.Add(new DoctorProfile { Id = Guid.NewGuid(), UserId = user.Id });
|
||||
}
|
||||
|
||||
user.UpdatedAt = DateTime.UtcNow;
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
// 生成 token
|
||||
var accessToken = jwt.GenerateAccessToken(user.Id, user.Phone);
|
||||
var accessToken = jwt.GenerateAccessToken(user.Id, user.Phone, role);
|
||||
var refreshToken = jwt.GenerateRefreshToken();
|
||||
|
||||
// 保存 refresh token
|
||||
db.RefreshTokens.Add(new RefreshToken
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
@@ -109,15 +119,65 @@ public static class AuthEndpoints
|
||||
{
|
||||
accessToken,
|
||||
refreshToken,
|
||||
user = new
|
||||
{
|
||||
user.Id,
|
||||
user.Phone,
|
||||
user.Name,
|
||||
user.Gender,
|
||||
user = new {
|
||||
user.Id, user.Phone, user.Role, isNew = true
|
||||
}
|
||||
},
|
||||
message = (string?)null
|
||||
});
|
||||
});
|
||||
|
||||
// ── 登录(已有账号)──
|
||||
app.MapPost("/api/auth/login", async (
|
||||
LoginRequest request,
|
||||
AppDbContext db,
|
||||
JwtProvider jwt,
|
||||
CancellationToken ct) =>
|
||||
{
|
||||
var validCode = await db.VerificationCodes
|
||||
.Where(v => v.Phone == request.Phone
|
||||
&& v.Code == request.SmsCode
|
||||
&& v.ExpiresAt > DateTime.UtcNow
|
||||
&& !v.IsUsed)
|
||||
.OrderByDescending(v => v.CreatedAt)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
|
||||
if (validCode == null)
|
||||
return Results.Ok(new { code = 40001, data = (object?)null, message = "验证码错误或已过期" });
|
||||
|
||||
validCode.IsUsed = true;
|
||||
|
||||
var user = await db.Users.FirstOrDefaultAsync(u => u.Phone == request.Phone, ct);
|
||||
if (user == null)
|
||||
return Results.Ok(new { code = 40004, data = (object?)null, message = "该手机号未注册,请先注册" });
|
||||
|
||||
user.UpdatedAt = DateTime.UtcNow;
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
var accessToken = jwt.GenerateAccessToken(user.Id, user.Phone, user.Role);
|
||||
var refreshToken = jwt.GenerateRefreshToken();
|
||||
|
||||
db.RefreshTokens.Add(new RefreshToken
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = user.Id,
|
||||
Token = refreshToken,
|
||||
ExpiresAt = DateTime.UtcNow.AddDays(30),
|
||||
});
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
return Results.Ok(new
|
||||
{
|
||||
code = 0,
|
||||
data = new
|
||||
{
|
||||
accessToken,
|
||||
refreshToken,
|
||||
user = new {
|
||||
user.Id, user.Phone, user.Role, user.Name,
|
||||
user.Gender, user.AvatarUrl,
|
||||
BirthDate = user.BirthDate?.ToString("yyyy-MM-dd"),
|
||||
user.AvatarUrl,
|
||||
isNew
|
||||
isNew = false
|
||||
}
|
||||
},
|
||||
message = (string?)null
|
||||
@@ -145,7 +205,7 @@ public static class AuthEndpoints
|
||||
return Results.Ok(new { code = 40002, data = (object?)null, message = "用户不存在" });
|
||||
|
||||
// 生成新 token(续期)
|
||||
var accessToken = jwt.GenerateAccessToken(user.Id, user.Phone);
|
||||
var accessToken = jwt.GenerateAccessToken(user.Id, user.Phone, user.Role);
|
||||
var newRefreshToken = jwt.GenerateRefreshToken();
|
||||
db.RefreshTokens.Add(new RefreshToken
|
||||
{
|
||||
@@ -159,7 +219,7 @@ public static class AuthEndpoints
|
||||
return Results.Ok(new
|
||||
{
|
||||
code = 0,
|
||||
data = new { accessToken, refreshToken = newRefreshToken },
|
||||
data = new { accessToken, refreshToken = newRefreshToken, user = new { user.Role } },
|
||||
message = (string?)null
|
||||
});
|
||||
});
|
||||
@@ -182,5 +242,6 @@ public static class AuthEndpoints
|
||||
|
||||
// ---- 请求 DTO ----
|
||||
public sealed record SendSmsRequest(string Phone);
|
||||
public sealed record RegisterRequest(string Phone, string SmsCode, string? Role, string? InviteCode);
|
||||
public sealed record LoginRequest(string Phone, string SmsCode);
|
||||
public sealed record RefreshRequest(string RefreshToken);
|
||||
|
||||
@@ -4,60 +4,94 @@ public static class CalendarEndpoints
|
||||
{
|
||||
public static void MapCalendarEndpoints(this WebApplication app)
|
||||
{
|
||||
app.MapGet("/api/calendar", async (
|
||||
int year, int month,
|
||||
HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||||
var group = app.MapGroup("/api/calendar").RequireAuthorization();
|
||||
|
||||
group.MapGet("/", async (int year, int month, HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = GetUserId(http);
|
||||
var start = new DateOnly(year, month, 1);
|
||||
var end = start.AddMonths(1);
|
||||
var startDt = start.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc);
|
||||
var endDt = end.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc);
|
||||
|
||||
// 用药
|
||||
var medications = await db.Medications
|
||||
.Where(m => m.UserId == userId && m.IsActive && m.TimeOfDay.Count > 0)
|
||||
.Select(m => new { m.Id, m.Name, m.TimeOfDay })
|
||||
.ToListAsync(ct);
|
||||
|
||||
// 运动
|
||||
var plans = await db.ExercisePlans
|
||||
.Where(p => p.UserId == userId && p.WeekStartDate < end)
|
||||
.Include(p => p.Items)
|
||||
.ToListAsync(ct);
|
||||
|
||||
// 健康数据日期
|
||||
var healthDates = await db.HealthRecords
|
||||
.Where(r => r.UserId == userId
|
||||
&& r.RecordedAt >= start.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc)
|
||||
&& r.RecordedAt < end.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc))
|
||||
.Select(r => DateOnly.FromDateTime(r.RecordedAt))
|
||||
.Distinct()
|
||||
.ToListAsync(ct);
|
||||
|
||||
// 复查随访
|
||||
.Where(p => p.UserId == userId).Include(p => p.Items).ToListAsync(ct);
|
||||
var followups = await db.FollowUps
|
||||
.Where(f => f.UserId == userId
|
||||
&& f.ScheduledAt >= start.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc)
|
||||
&& f.ScheduledAt < end.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc))
|
||||
.Select(f => new { f.Id, f.Title, f.Department,
|
||||
Date = DateOnly.FromDateTime(f.ScheduledAt) })
|
||||
.Where(f => f.UserId == userId && f.ScheduledAt >= startDt && f.ScheduledAt < endDt)
|
||||
.ToListAsync(ct);
|
||||
|
||||
var result = new List<object>();
|
||||
for (var d = start; d < end; d = d.AddDays(1))
|
||||
{
|
||||
var events = new List<string>();
|
||||
if (medications.Any()) events.Add("medication");
|
||||
if (plans.Any(p => p.Items.Any(i => i.DayOfWeek == (int)d.DayOfWeek && !i.IsRestDay)))
|
||||
events.Add("exercise");
|
||||
if (followups.Any(f => f.Date == d)) events.Add("followup");
|
||||
if (healthDates.Contains(d)) events.Add("health_record");
|
||||
var detailList = new List<object>();
|
||||
|
||||
if (events.Count > 0)
|
||||
result.Add(new { date = d.ToString("yyyy-MM-dd"), events });
|
||||
// 用药:当前日期在用药有效期内
|
||||
var todayMedications = medications
|
||||
.Where(m => m.StartDate <= d && (m.EndDate == null || m.EndDate >= d));
|
||||
foreach (var m in todayMedications)
|
||||
{
|
||||
detailList.Add(new { type = "medication", name = m.Name, dosage = m.Dosage, timeOfDay = m.TimeOfDay.Select(t => t.ToString(@"hh\:mm")).ToList() });
|
||||
}
|
||||
|
||||
// 运动:按计划有效周范围 + 星期几匹配
|
||||
foreach (var p in plans)
|
||||
{
|
||||
var weekEnd = p.WeekStartDate.AddDays(6);
|
||||
if (d >= p.WeekStartDate && d <= weekEnd)
|
||||
{
|
||||
var dayItems = p.Items.Where(i => i.DayOfWeek == (int)d.DayOfWeek && !i.IsRestDay);
|
||||
foreach (var i in dayItems)
|
||||
detailList.Add(new { type = "exercise", name = i.ExerciseType, duration = i.DurationMinutes, isCompleted = i.IsCompleted });
|
||||
}
|
||||
}
|
||||
|
||||
// 随访
|
||||
var todayFollowups = followups.Where(f => DateOnly.FromDateTime(f.ScheduledAt) == d);
|
||||
foreach (var f in todayFollowups)
|
||||
detailList.Add(new { type = "followup", title = f.Title, doctorName = f.DoctorName, department = f.Department, status = f.Status.ToString() });
|
||||
|
||||
if (detailList.Count > 0)
|
||||
result.Add(new { date = d.ToString("yyyy-MM-dd"), events = detailList.Select(x => ((dynamic)x).type as string).Distinct().ToList(), details = detailList });
|
||||
}
|
||||
|
||||
return Results.Ok(new { code = 0, data = result, message = (string?)null });
|
||||
}).RequireAuthorization();
|
||||
});
|
||||
|
||||
// 获取某一天的详细安排
|
||||
group.MapGet("/day", async (string date, HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = GetUserId(http);
|
||||
if (!DateOnly.TryParse(date, out var d))
|
||||
return Results.Ok(new { code = 400, data = (object?)null, message = "日期格式错误" });
|
||||
|
||||
var medications = await db.Medications
|
||||
.Where(m => m.UserId == userId && m.IsActive && m.StartDate <= d && (m.EndDate == null || m.EndDate >= d))
|
||||
.Select(m => new { m.Name, m.Dosage, timeOfDay = m.TimeOfDay.Select(t => t.ToString(@"hh\:mm")).ToList() })
|
||||
.ToListAsync(ct);
|
||||
|
||||
var plans = await db.ExercisePlans
|
||||
.Where(p => p.UserId == userId)
|
||||
.Include(p => p.Items).ToListAsync(ct);
|
||||
var exercises = new List<object>();
|
||||
foreach (var p in plans)
|
||||
{
|
||||
if (d >= p.WeekStartDate && d <= p.WeekStartDate.AddDays(6))
|
||||
{
|
||||
foreach (var i in p.Items.Where(i => i.DayOfWeek == (int)d.DayOfWeek && !i.IsRestDay))
|
||||
exercises.Add(new { type = i.ExerciseType, duration = i.DurationMinutes, isCompleted = i.IsCompleted });
|
||||
}
|
||||
}
|
||||
|
||||
var followUps = await db.FollowUps
|
||||
.Where(f => f.UserId == userId && DateOnly.FromDateTime(f.ScheduledAt) == d)
|
||||
.Select(f => new { f.Title, f.DoctorName, f.Department, status = f.Status.ToString() })
|
||||
.ToListAsync(ct);
|
||||
|
||||
return Results.Ok(new { code = 0, data = new { medications, exercises, followUps }, message = (string?)null });
|
||||
});
|
||||
}
|
||||
|
||||
private static Guid GetUserId(HttpContext http) =>
|
||||
|
||||
@@ -64,6 +64,21 @@ public static class DietEndpoints
|
||||
if (record != null) { db.DietRecords.Remove(record); await db.SaveChangesAsync(ct); }
|
||||
return Results.Ok(new { code = 0, data = new { success = true }, message = (string?)null });
|
||||
});
|
||||
|
||||
group.MapPut("/{id:guid}", async (Guid id, HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var userId = GetUserId(http);
|
||||
var record = await db.DietRecords.FirstOrDefaultAsync(r => r.Id == id && r.UserId == userId, ct);
|
||||
if (record == null) return Results.Ok(new { code = 40004, message = "记录不存在" });
|
||||
|
||||
using var reader = new StreamReader(http.Request.Body);
|
||||
var body = await reader.ReadToEndAsync(ct);
|
||||
var json = System.Text.Json.JsonDocument.Parse(body);
|
||||
if (json.RootElement.TryGetProperty("totalCalories", out var cal)) record.TotalCalories = (int?)cal.GetInt32();
|
||||
if (json.RootElement.TryGetProperty("healthScore", out var hs)) record.HealthScore = (int?)hs.GetInt32();
|
||||
await db.SaveChangesAsync(ct);
|
||||
return Results.Ok(new { code = 0, data = new { success = true }, message = (string?)null });
|
||||
});
|
||||
}
|
||||
|
||||
private static Guid GetUserId(HttpContext http) =>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Security.Claims;
|
||||
using Health.Domain.Entities;
|
||||
using Health.Domain.Enums;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -5,68 +6,109 @@ using Microsoft.EntityFrameworkCore;
|
||||
namespace Health.WebApi.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// 医生端 API(开发阶段 hardcode 王建国为当前医生)
|
||||
/// 医生端 API(需JWT鉴权 + Role=Doctor)
|
||||
/// </summary>
|
||||
public static class DoctorEndpoints
|
||||
{
|
||||
private static async Task<Doctor> GetDoctor(AppDbContext db)
|
||||
private static Guid GetUserId(HttpContext http) =>
|
||||
Guid.TryParse(http.User.FindFirst(ClaimTypes.NameIdentifier)?.Value, out var id) ? id : Guid.Empty;
|
||||
|
||||
private static string GetUserRole(HttpContext http) =>
|
||||
http.User.FindFirst("Role")?.Value ?? http.User.FindFirst(ClaimTypes.Role)?.Value ?? "User";
|
||||
|
||||
private static async Task<DoctorProfile?> GetDoctorProfile(HttpContext http, AppDbContext db)
|
||||
{
|
||||
return await db.Doctors.FirstAsync(d => d.Name == "王建国" && d.IsActive);
|
||||
var userId = GetUserId(http);
|
||||
if (userId == Guid.Empty) return null;
|
||||
return await db.DoctorProfiles.FirstOrDefaultAsync(d => d.UserId == userId);
|
||||
}
|
||||
|
||||
public static void MapDoctorEndpoints(this WebApplication app)
|
||||
{
|
||||
var group = app.MapGroup("/api/doctor");
|
||||
var group = app.MapGroup("/api/doctor").RequireAuthorization();
|
||||
|
||||
// 中间件:校验医生身份
|
||||
group.AddEndpointFilter(async (context, next) =>
|
||||
{
|
||||
var http = context.HttpContext;
|
||||
var role = GetUserRole(http);
|
||||
if (role != "Doctor")
|
||||
return Results.Json(new { code = 403, data = (object?)null, message = "仅医生可访问" }, statusCode: 403);
|
||||
return await next(context);
|
||||
});
|
||||
|
||||
// ===== 工作台 =====
|
||||
group.MapGet("/dashboard", async (AppDbContext db) =>
|
||||
group.MapGet("/dashboard", async (HttpContext http, AppDbContext db) =>
|
||||
{
|
||||
var doctor = await GetDoctor(db);
|
||||
var totalPatients = await db.Users.CountAsync();
|
||||
var activeConsultations = await db.Consultations.CountAsync(c => c.DoctorId == doctor.Id && c.Status != ConsultationStatus.Closed);
|
||||
var profile = await GetDoctorProfile(http, db);
|
||||
if (profile == null)
|
||||
return Results.Ok(new { code = 404, data = (object?)null, message = "医生档案不存在,请先完善个人信息" });
|
||||
|
||||
var totalPatients = await db.Users.CountAsync(u => u.Role == "User");
|
||||
var activeConsultations = await db.Consultations.CountAsync(c => c.Status != ConsultationStatus.Closed);
|
||||
var pendingReports = await db.Reports.CountAsync(r => r.Status == ReportStatus.PendingDoctor);
|
||||
var todayStart = DateTime.UtcNow.Date;
|
||||
var todayEnd = todayStart.AddDays(1);
|
||||
var todayFollowUps = await db.FollowUps.CountAsync(f => f.ScheduledAt >= todayStart && f.ScheduledAt < todayEnd && f.Status == FollowUpStatus.Upcoming);
|
||||
|
||||
// 待办列表
|
||||
var pendingConsultations = await db.Consultations
|
||||
.Where(c => c.Status == ConsultationStatus.WaitingDoctor)
|
||||
.OrderByDescending(c => c.CreatedAt)
|
||||
.Take(5)
|
||||
.Select(c => new { c.Id, PatientName = c.User.Name ?? c.User.Phone, c.CreatedAt })
|
||||
.ToListAsync();
|
||||
|
||||
var pendingReportList = await db.Reports
|
||||
.Where(r => r.Status == ReportStatus.PendingDoctor)
|
||||
.OrderByDescending(r => r.CreatedAt)
|
||||
.Take(5)
|
||||
.Select(r => new { r.Id, PatientName = r.User.Name ?? r.User.Phone, r.Category, r.CreatedAt })
|
||||
.ToListAsync();
|
||||
|
||||
var todayFollowUpList = await db.FollowUps
|
||||
.Where(f => f.ScheduledAt >= todayStart && f.ScheduledAt < todayEnd && f.Status == FollowUpStatus.Upcoming)
|
||||
.OrderBy(f => f.ScheduledAt)
|
||||
.Select(f => new { f.Id, f.Title, PatientName = f.User.Name ?? f.User.Phone, f.ScheduledAt })
|
||||
.ToListAsync();
|
||||
|
||||
return Results.Ok(new
|
||||
{
|
||||
code = 0,
|
||||
data = new
|
||||
{
|
||||
doctorName = doctor.Name,
|
||||
doctorTitle = doctor.Title,
|
||||
doctorDepartment = doctor.Department,
|
||||
totalPatients,
|
||||
activeConsultations,
|
||||
pendingReports,
|
||||
todayFollowUps
|
||||
doctorName = profile.Name,
|
||||
doctorTitle = profile.Title,
|
||||
doctorDepartment = profile.Department,
|
||||
doctorHospital = profile.Hospital,
|
||||
doctorAvatar = profile.AvatarUrl,
|
||||
doctorOnline = profile.IsOnline,
|
||||
stats = new { totalPatients, activeConsultations, pendingReports, todayFollowUps },
|
||||
pendingConsultations,
|
||||
pendingReports = pendingReportList,
|
||||
todayFollowUps = todayFollowUpList
|
||||
},
|
||||
message = (string?)null
|
||||
});
|
||||
});
|
||||
|
||||
// ===== 患者列表 =====
|
||||
group.MapGet("/patients", async (string? search, AppDbContext db) =>
|
||||
group.MapGet("/patients", async (AppDbContext db, string? search, int page = 1, int pageSize = 20) =>
|
||||
{
|
||||
var query = db.Users.AsQueryable();
|
||||
var query = db.Users.Where(u => u.Role == "User").AsQueryable();
|
||||
if (!string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
query = query.Where(u =>
|
||||
(u.Name != null && u.Name.Contains(search)) ||
|
||||
u.Phone.Contains(search));
|
||||
}
|
||||
var patients = await query.OrderByDescending(u => u.CreatedAt).Select(u => new
|
||||
{
|
||||
u.Id,
|
||||
u.Phone,
|
||||
u.Name,
|
||||
u.Gender,
|
||||
u.BirthDate,
|
||||
hasArchive = u.HealthArchive != null
|
||||
}).ToListAsync();
|
||||
var total = await query.CountAsync();
|
||||
var patients = await query.OrderByDescending(u => u.CreatedAt)
|
||||
.Skip((page - 1) * pageSize).Take(pageSize)
|
||||
.Select(u => new { u.Id, u.Phone, u.Name, u.Gender, u.BirthDate, hasArchive = u.HealthArchive != null })
|
||||
.ToListAsync();
|
||||
|
||||
return Results.Ok(new { code = 0, data = patients, message = (string?)null });
|
||||
return Results.Ok(new { code = 0, data = new { total, page, pageSize, items = patients }, message = (string?)null });
|
||||
});
|
||||
|
||||
// ===== 患者详情 =====
|
||||
@@ -78,91 +120,41 @@ public static class DoctorEndpoints
|
||||
if (user == null)
|
||||
return Results.Ok(new { code = 404, data = (object?)null, message = "患者不存在" });
|
||||
|
||||
// 最新健康指标
|
||||
var latestRecords = await db.HealthRecords
|
||||
.Where(r => r.UserId == id)
|
||||
.GroupBy(r => r.MetricType)
|
||||
.Select(g => g.OrderByDescending(r => r.RecordedAt).First())
|
||||
.ToListAsync();
|
||||
|
||||
// 最近30天健康数据(用于趋势图)
|
||||
var thirtyDaysAgo = DateTime.UtcNow.AddDays(-30);
|
||||
var trendRecords = await db.HealthRecords
|
||||
.Where(r => r.UserId == id && r.RecordedAt >= thirtyDaysAgo)
|
||||
.OrderBy(r => r.RecordedAt)
|
||||
.Select(r => new
|
||||
{
|
||||
r.Id,
|
||||
MetricType = r.MetricType.ToString(),
|
||||
r.Systolic,
|
||||
r.Diastolic,
|
||||
r.Value,
|
||||
r.Unit,
|
||||
r.RecordedAt,
|
||||
r.IsAbnormal
|
||||
})
|
||||
.Select(r => new { r.Id, MetricType = r.MetricType.ToString(), r.Systolic, r.Diastolic, r.Value, r.Unit, r.RecordedAt, r.IsAbnormal })
|
||||
.ToListAsync();
|
||||
|
||||
// 用药
|
||||
var medications = await db.Medications
|
||||
.Where(m => m.UserId == id && m.IsActive)
|
||||
.OrderByDescending(m => m.StartDate)
|
||||
.Select(m => new { m.Id, m.Name, m.Dosage, Frequency = m.Frequency.ToString(), m.TimeOfDay, m.StartDate, m.EndDate })
|
||||
.ToListAsync();
|
||||
|
||||
// 最近饮食
|
||||
var dietRecords = await db.DietRecords
|
||||
.Where(d => d.UserId == id)
|
||||
.OrderByDescending(d => d.RecordedAt)
|
||||
.Take(7)
|
||||
.Include(d => d.FoodItems)
|
||||
.Where(d => d.UserId == id).OrderByDescending(d => d.RecordedAt).Take(7).Include(d => d.FoodItems)
|
||||
.ToListAsync();
|
||||
|
||||
// 运动计划
|
||||
var exercisePlans = await db.ExercisePlans
|
||||
.Where(p => p.UserId == id)
|
||||
.OrderByDescending(p => p.WeekStartDate)
|
||||
.Take(1)
|
||||
.Include(p => p.Items)
|
||||
.Where(p => p.UserId == id).OrderByDescending(p => p.WeekStartDate).Take(1).Include(p => p.Items)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
// 报告
|
||||
var reports = await db.Reports
|
||||
.Where(r => r.UserId == id)
|
||||
.OrderByDescending(r => r.CreatedAt)
|
||||
.Select(r => new
|
||||
{
|
||||
r.Id,
|
||||
r.FileUrl,
|
||||
FileType = r.FileType.ToString(),
|
||||
Category = r.Category.ToString(),
|
||||
Status = r.Status.ToString(),
|
||||
r.Severity,
|
||||
r.AiSummary,
|
||||
r.AiIndicators,
|
||||
r.DoctorComment,
|
||||
r.DoctorRecommendation,
|
||||
r.DoctorName,
|
||||
r.ReviewedAt,
|
||||
r.CreatedAt
|
||||
})
|
||||
.Where(r => r.UserId == id).OrderByDescending(r => r.CreatedAt)
|
||||
.Select(r => new { r.Id, r.FileUrl, FileType = r.FileType.ToString(), Category = r.Category.ToString(), Status = r.Status.ToString(), r.Severity, r.AiSummary, r.AiIndicators, r.DoctorComment, r.DoctorRecommendation, r.DoctorName, r.ReviewedAt, r.CreatedAt })
|
||||
.ToListAsync();
|
||||
|
||||
// 随访
|
||||
var followUps = await db.FollowUps
|
||||
.Where(f => f.UserId == id)
|
||||
.OrderByDescending(f => f.ScheduledAt)
|
||||
.Select(f => new
|
||||
{
|
||||
f.Id,
|
||||
f.Title,
|
||||
f.DoctorName,
|
||||
f.Department,
|
||||
f.ScheduledAt,
|
||||
f.Notes,
|
||||
Status = f.Status.ToString(),
|
||||
f.CreatedAt
|
||||
})
|
||||
.Where(f => f.UserId == id).OrderByDescending(f => f.ScheduledAt)
|
||||
.Select(f => new { f.Id, f.Title, f.DoctorName, f.Department, f.ScheduledAt, f.Notes, Status = f.Status.ToString(), f.CreatedAt })
|
||||
.ToListAsync();
|
||||
|
||||
return Results.Ok(new
|
||||
@@ -170,75 +162,28 @@ public static class DoctorEndpoints
|
||||
code = 0,
|
||||
data = new
|
||||
{
|
||||
profile = new
|
||||
{
|
||||
user.Id,
|
||||
user.Phone,
|
||||
user.Name,
|
||||
user.Gender,
|
||||
BirthDate = user.BirthDate?.ToString("yyyy-MM-dd"),
|
||||
user.AvatarUrl
|
||||
},
|
||||
archive = user.HealthArchive == null ? null : new
|
||||
{
|
||||
user.HealthArchive.Diagnosis,
|
||||
user.HealthArchive.SurgeryType,
|
||||
SurgeryDate = user.HealthArchive.SurgeryDate?.ToString("yyyy-MM-dd"),
|
||||
user.HealthArchive.Allergies,
|
||||
user.HealthArchive.DietRestrictions,
|
||||
user.HealthArchive.ChronicDiseases,
|
||||
user.HealthArchive.FamilyHistory
|
||||
},
|
||||
profile = new { user.Id, user.Phone, user.Name, user.Gender, BirthDate = user.BirthDate?.ToString("yyyy-MM-dd"), user.AvatarUrl },
|
||||
archive = user.HealthArchive == null ? null : new { user.HealthArchive.Diagnosis, user.HealthArchive.SurgeryType, SurgeryDate = user.HealthArchive.SurgeryDate?.ToString("yyyy-MM-dd"), user.HealthArchive.Allergies, user.HealthArchive.DietRestrictions, user.HealthArchive.ChronicDiseases, user.HealthArchive.FamilyHistory },
|
||||
latestRecords,
|
||||
trendRecords,
|
||||
medications,
|
||||
dietRecords = dietRecords.Select(d => new
|
||||
{
|
||||
d.Id,
|
||||
MealType = d.MealType.ToString(),
|
||||
d.TotalCalories,
|
||||
d.HealthScore,
|
||||
d.RecordedAt,
|
||||
foods = d.FoodItems.Select(f => new { f.Name, f.Portion, f.Calories, f.Warning })
|
||||
}),
|
||||
exercisePlan = exercisePlans == null ? null : new
|
||||
{
|
||||
exercisePlans.WeekStartDate,
|
||||
items = exercisePlans.Items.Select(i => new
|
||||
{
|
||||
i.DayOfWeek,
|
||||
i.ExerciseType,
|
||||
i.DurationMinutes,
|
||||
i.IsCompleted,
|
||||
i.IsRestDay,
|
||||
i.CompletedAt
|
||||
})
|
||||
},
|
||||
reports,
|
||||
followUps
|
||||
dietRecords = dietRecords.Select(d => new { d.Id, MealType = d.MealType.ToString(), d.TotalCalories, d.HealthScore, d.RecordedAt, foods = d.FoodItems.Select(f => new { f.Name, f.Portion, f.Calories, f.Warning }) }),
|
||||
exercisePlan = exercisePlans == null ? null : new { exercisePlans.WeekStartDate, items = exercisePlans.Items.Select(i => new { i.DayOfWeek, i.ExerciseType, i.DurationMinutes, i.IsCompleted, i.IsRestDay, i.CompletedAt }) },
|
||||
reports, followUps
|
||||
},
|
||||
message = (string?)null
|
||||
});
|
||||
});
|
||||
|
||||
// ===== 问诊列表 =====
|
||||
group.MapGet("/consultations", async (AppDbContext db) =>
|
||||
group.MapGet("/consultations", async (HttpContext http, AppDbContext db) =>
|
||||
{
|
||||
var doctor = await GetDoctor(db);
|
||||
var profile = await GetDoctorProfile(http, db);
|
||||
if (profile == null) return Results.Ok(new { code = 404, data = (object?)null, message = "医生档案不存在" });
|
||||
|
||||
var consultations = await db.Consultations
|
||||
.Where(c => c.DoctorId == doctor.Id)
|
||||
.OrderByDescending(c => c.CreatedAt)
|
||||
.Select(c => new
|
||||
{
|
||||
c.Id,
|
||||
c.UserId,
|
||||
PatientName = c.User.Name,
|
||||
PatientPhone = c.User.Phone,
|
||||
Status = c.Status.ToString(),
|
||||
c.CreatedAt,
|
||||
c.ClosedAt,
|
||||
LastMessage = c.Messages.OrderByDescending(m => m.CreatedAt).Select(m => new { m.Content, m.SenderType, m.CreatedAt }).FirstOrDefault()
|
||||
})
|
||||
.Select(c => new { c.Id, c.UserId, PatientName = c.User.Name, PatientPhone = c.User.Phone, Status = c.Status.ToString(), c.CreatedAt, c.ClosedAt, LastMessage = c.Messages.OrderByDescending(m => m.CreatedAt).Select(m => new { m.Content, SenderType = m.SenderType.ToString(), m.CreatedAt }).FirstOrDefault() })
|
||||
.ToListAsync();
|
||||
|
||||
return Results.Ok(new { code = 0, data = consultations, message = (string?)null });
|
||||
@@ -248,79 +193,47 @@ public static class DoctorEndpoints
|
||||
group.MapGet("/consultations/{id:guid}/messages", async (Guid id, AppDbContext db) =>
|
||||
{
|
||||
var messages = await db.ConsultationMessages
|
||||
.Where(m => m.ConsultationId == id)
|
||||
.OrderBy(m => m.CreatedAt)
|
||||
.Select(m => new
|
||||
{
|
||||
m.Id,
|
||||
SenderType = m.SenderType.ToString(),
|
||||
m.SenderName,
|
||||
m.Content,
|
||||
m.CreatedAt
|
||||
})
|
||||
.Where(m => m.ConsultationId == id).OrderBy(m => m.CreatedAt)
|
||||
.Select(m => new { m.Id, SenderType = m.SenderType.ToString(), m.SenderName, m.Content, m.CreatedAt })
|
||||
.ToListAsync();
|
||||
|
||||
var consultation = await db.Consultations.FirstOrDefaultAsync(c => c.Id == id);
|
||||
|
||||
return Results.Ok(new
|
||||
{
|
||||
code = 0,
|
||||
data = new
|
||||
{
|
||||
status = consultation?.Status.ToString() ?? "Closed",
|
||||
messages
|
||||
},
|
||||
message = (string?)null
|
||||
});
|
||||
return Results.Ok(new { code = 0, data = new { status = consultation?.Status.ToString() ?? "Closed", messages }, message = (string?)null });
|
||||
});
|
||||
|
||||
// ===== 医生发送消息(同时通过 SignalR 广播)=====
|
||||
// ===== 医生发送消息 =====
|
||||
group.MapPost("/consultations/{id:guid}/messages", async (Guid id, HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var doctor = await GetDoctor(db);
|
||||
var consultation = await db.Consultations.FirstOrDefaultAsync(c => c.Id == id, ct);
|
||||
if (consultation == null)
|
||||
return Results.Ok(new { code = 404, data = (object?)null, message = "问诊不存在" });
|
||||
var profile = await GetDoctorProfile(http, db);
|
||||
if (profile == null) return Results.Ok(new { code = 404, data = (object?)null, message = "医生档案不存在" });
|
||||
|
||||
var consultation = await db.Consultations.FirstOrDefaultAsync(c => c.Id == id, ct);
|
||||
if (consultation == null) return Results.Ok(new { code = 404, data = (object?)null, message = "问诊不存在" });
|
||||
|
||||
// 读请求 body
|
||||
using var reader = new StreamReader(http.Request.Body);
|
||||
var body = await reader.ReadToEndAsync(ct);
|
||||
var json = System.Text.Json.JsonDocument.Parse(body);
|
||||
var content = json.RootElement.GetProperty("content").GetString() ?? "";
|
||||
|
||||
// 保存消息
|
||||
var msg = new ConsultationMessage
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
ConsultationId = id,
|
||||
Id = Guid.NewGuid(), ConsultationId = id,
|
||||
SenderType = ConsultationSenderType.Doctor,
|
||||
SenderName = $"医生 · {doctor.Name}",
|
||||
Content = content,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
SenderName = $"医生 · {profile.Name}",
|
||||
Content = content, CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
db.ConsultationMessages.Add(msg);
|
||||
|
||||
// 更新问诊状态
|
||||
if (consultation.Status == ConsultationStatus.AiTalking || consultation.Status == ConsultationStatus.WaitingDoctor)
|
||||
{
|
||||
consultation.Status = ConsultationStatus.DoctorReplied;
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
// 通过 SignalR 广播(如果 Hub 已注册)
|
||||
var hubContext = http.RequestServices.GetService<Microsoft.AspNetCore.SignalR.IHubContext<Hubs.ConsultationHub>>();
|
||||
if (hubContext != null)
|
||||
{
|
||||
await hubContext.Clients.Group(id.ToString()).SendAsync("ReceiveMessage", new
|
||||
{
|
||||
msg.Id,
|
||||
consultationId = id.ToString(),
|
||||
senderType = "Doctor",
|
||||
senderName = msg.SenderName,
|
||||
content,
|
||||
msg.CreatedAt
|
||||
});
|
||||
await hubContext.Clients.Group(id.ToString()).SendAsync("ReceiveMessage", new { msg.Id, consultationId = id.ToString(), senderType = "Doctor", senderName = msg.SenderName, content, msg.CreatedAt });
|
||||
}
|
||||
|
||||
return Results.Ok(new { code = 0, data = new { msg.Id }, message = (string?)null });
|
||||
@@ -333,69 +246,30 @@ public static class DoctorEndpoints
|
||||
if (!string.IsNullOrWhiteSpace(status) && Enum.TryParse<ReportStatus>(status, out var s))
|
||||
query = query.Where(r => r.Status == s);
|
||||
|
||||
var reports = await query
|
||||
.OrderByDescending(r => r.CreatedAt)
|
||||
.Select(r => new
|
||||
{
|
||||
r.Id,
|
||||
r.UserId,
|
||||
PatientName = r.User.Name,
|
||||
r.FileUrl,
|
||||
FileType = r.FileType.ToString(),
|
||||
Category = r.Category.ToString(),
|
||||
Status = r.Status.ToString(),
|
||||
r.Severity,
|
||||
r.AiSummary,
|
||||
r.AiIndicators,
|
||||
r.DoctorComment,
|
||||
r.DoctorRecommendation,
|
||||
r.DoctorName,
|
||||
r.ReviewedAt,
|
||||
r.CreatedAt
|
||||
})
|
||||
var reports = await query.OrderByDescending(r => r.CreatedAt)
|
||||
.Select(r => new { r.Id, r.UserId, PatientName = r.User.Name, r.FileUrl, FileType = r.FileType.ToString(), Category = r.Category.ToString(), Status = r.Status.ToString(), r.Severity, r.AiSummary, r.AiIndicators, r.DoctorComment, r.DoctorRecommendation, r.DoctorName, r.ReviewedAt, r.CreatedAt })
|
||||
.ToListAsync();
|
||||
|
||||
return Results.Ok(new { code = 0, data = reports, message = (string?)null });
|
||||
});
|
||||
|
||||
// ===== 报告详情 =====
|
||||
group.MapGet("/reports/{id:guid}", async (Guid id, AppDbContext db) =>
|
||||
{
|
||||
var report = await db.Reports
|
||||
.Where(r => r.Id == id)
|
||||
.Select(r => new
|
||||
{
|
||||
r.Id,
|
||||
r.UserId,
|
||||
PatientName = r.User.Name,
|
||||
r.FileUrl,
|
||||
FileType = r.FileType.ToString(),
|
||||
Category = r.Category.ToString(),
|
||||
Status = r.Status.ToString(),
|
||||
r.AiSummary,
|
||||
r.AiIndicators,
|
||||
r.Severity,
|
||||
r.DoctorComment,
|
||||
r.DoctorRecommendation,
|
||||
r.DoctorName,
|
||||
r.ReviewedAt,
|
||||
r.CreatedAt
|
||||
})
|
||||
var report = await db.Reports.Where(r => r.Id == id)
|
||||
.Select(r => new { r.Id, r.UserId, PatientName = r.User.Name, r.FileUrl, FileType = r.FileType.ToString(), Category = r.Category.ToString(), Status = r.Status.ToString(), r.Severity, r.AiSummary, r.AiIndicators, r.DoctorComment, r.DoctorRecommendation, r.DoctorName, r.ReviewedAt, r.CreatedAt })
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (report == null)
|
||||
return Results.Ok(new { code = 404, data = (object?)null, message = "报告不存在" });
|
||||
|
||||
if (report == null) return Results.Ok(new { code = 404, data = (object?)null, message = "报告不存在" });
|
||||
return Results.Ok(new { code = 0, data = report, message = (string?)null });
|
||||
});
|
||||
|
||||
// ===== 审阅报告 =====
|
||||
group.MapPost("/reports/{id:guid}/review", async (Guid id, HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var doctor = await GetDoctor(db);
|
||||
var profile = await GetDoctorProfile(http, db);
|
||||
if (profile == null) return Results.Ok(new { code = 404, data = (object?)null, message = "医生档案不存在" });
|
||||
|
||||
var report = await db.Reports.FirstOrDefaultAsync(r => r.Id == id, ct);
|
||||
if (report == null)
|
||||
return Results.Ok(new { code = 404, data = (object?)null, message = "报告不存在" });
|
||||
if (report == null) return Results.Ok(new { code = 404, data = (object?)null, message = "报告不存在" });
|
||||
|
||||
using var reader = new StreamReader(http.Request.Body);
|
||||
var body = await reader.ReadToEndAsync(ct);
|
||||
@@ -404,18 +278,14 @@ public static class DoctorEndpoints
|
||||
report.Severity = json.RootElement.TryGetProperty("severity", out var sv) ? sv.GetString() : null;
|
||||
report.DoctorComment = json.RootElement.TryGetProperty("comment", out var cm) ? cm.GetString() : null;
|
||||
report.DoctorRecommendation = json.RootElement.TryGetProperty("recommendation", out var rc) ? rc.GetString() : null;
|
||||
report.DoctorName = doctor.Name;
|
||||
report.DoctorName = profile.Name;
|
||||
report.ReviewedAt = DateTime.UtcNow;
|
||||
report.Status = ReportStatus.DoctorReviewed;
|
||||
|
||||
// 如果医生提供了修正后的指标
|
||||
if (json.RootElement.TryGetProperty("indicators", out var inds))
|
||||
{
|
||||
report.AiIndicators = inds.GetRawText();
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
return Results.Ok(new { code = 0, data = new { success = true }, message = (string?)null });
|
||||
});
|
||||
|
||||
@@ -428,30 +298,18 @@ public static class DoctorEndpoints
|
||||
if (Guid.TryParse(patientId, out var pid))
|
||||
query = query.Where(f => f.UserId == pid);
|
||||
|
||||
var followUps = await query
|
||||
.OrderByDescending(f => f.ScheduledAt)
|
||||
.Select(f => new
|
||||
{
|
||||
f.Id,
|
||||
f.UserId,
|
||||
PatientName = f.User.Name,
|
||||
f.Title,
|
||||
f.DoctorName,
|
||||
f.Department,
|
||||
f.ScheduledAt,
|
||||
f.Notes,
|
||||
Status = f.Status.ToString(),
|
||||
f.CreatedAt
|
||||
})
|
||||
var followUps = await query.OrderByDescending(f => f.ScheduledAt)
|
||||
.Select(f => new { f.Id, f.UserId, PatientName = f.User.Name, f.Title, f.DoctorName, f.Department, f.ScheduledAt, f.Notes, Status = f.Status.ToString(), f.CreatedAt })
|
||||
.ToListAsync();
|
||||
|
||||
return Results.Ok(new { code = 0, data = followUps, message = (string?)null });
|
||||
});
|
||||
|
||||
// ===== 创建随访 =====
|
||||
group.MapPost("/follow-ups", async (HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var doctor = await GetDoctor(db);
|
||||
var profile = await GetDoctorProfile(http, db);
|
||||
if (profile == null) return Results.Ok(new { code = 404, data = (object?)null, message = "医生档案不存在" });
|
||||
|
||||
using var reader = new StreamReader(http.Request.Body);
|
||||
var body = await reader.ReadToEndAsync(ct);
|
||||
var json = System.Text.Json.JsonDocument.Parse(body);
|
||||
@@ -461,8 +319,8 @@ public static class DoctorEndpoints
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = Guid.Parse(json.RootElement.GetProperty("userId").GetString()!),
|
||||
Title = json.RootElement.GetProperty("title").GetString() ?? "",
|
||||
DoctorName = doctor.Name,
|
||||
Department = doctor.Department,
|
||||
DoctorName = profile.Name,
|
||||
Department = profile.Department,
|
||||
ScheduledAt = DateTime.Parse(json.RootElement.GetProperty("scheduledAt").GetString()!),
|
||||
Notes = json.RootElement.TryGetProperty("notes", out var n) ? n.GetString() : null,
|
||||
Status = FollowUpStatus.Upcoming,
|
||||
@@ -470,7 +328,6 @@ public static class DoctorEndpoints
|
||||
};
|
||||
db.FollowUps.Add(followUp);
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
return Results.Ok(new { code = 0, data = new { followUp.Id }, message = (string?)null });
|
||||
});
|
||||
|
||||
@@ -478,19 +335,15 @@ public static class DoctorEndpoints
|
||||
group.MapPut("/follow-ups/{id:guid}", async (Guid id, HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var followUp = await db.FollowUps.FirstOrDefaultAsync(f => f.Id == id, ct);
|
||||
if (followUp == null)
|
||||
return Results.Ok(new { code = 404, data = (object?)null, message = "随访不存在" });
|
||||
if (followUp == null) return Results.Ok(new { code = 404, data = (object?)null, message = "随访不存在" });
|
||||
|
||||
using var reader = new StreamReader(http.Request.Body);
|
||||
var body = await reader.ReadToEndAsync(ct);
|
||||
var json = System.Text.Json.JsonDocument.Parse(body);
|
||||
|
||||
if (json.RootElement.TryGetProperty("title", out var title)) followUp.Title = title.GetString() ?? followUp.Title;
|
||||
if (json.RootElement.TryGetProperty("scheduledAt", out var sat)) followUp.ScheduledAt = DateTime.Parse(sat.GetString()!);
|
||||
if (json.RootElement.TryGetProperty("notes", out var notes)) followUp.Notes = notes.GetString();
|
||||
if (json.RootElement.TryGetProperty("status", out var st) && Enum.TryParse<FollowUpStatus>(st.GetString(), out var fs))
|
||||
followUp.Status = fs;
|
||||
|
||||
if (json.RootElement.TryGetProperty("title", out var t)) followUp.Title = t.GetString() ?? followUp.Title;
|
||||
if (json.RootElement.TryGetProperty("scheduledAt", out var sa)) followUp.ScheduledAt = DateTime.Parse(sa.GetString()!);
|
||||
if (json.RootElement.TryGetProperty("notes", out var no)) followUp.Notes = no.GetString();
|
||||
if (json.RootElement.TryGetProperty("status", out var st) && Enum.TryParse<FollowUpStatus>(st.GetString(), out var fs)) followUp.Status = fs;
|
||||
await db.SaveChangesAsync(ct);
|
||||
return Results.Ok(new { code = 0, data = new { success = true }, message = (string?)null });
|
||||
});
|
||||
@@ -499,19 +352,45 @@ public static class DoctorEndpoints
|
||||
group.MapDelete("/follow-ups/{id:guid}", async (Guid id, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var followUp = await db.FollowUps.FirstOrDefaultAsync(f => f.Id == id, ct);
|
||||
if (followUp == null)
|
||||
return Results.Ok(new { code = 404, data = (object?)null, message = "随访不存在" });
|
||||
|
||||
if (followUp == null) return Results.Ok(new { code = 404, data = (object?)null, message = "随访不存在" });
|
||||
db.FollowUps.Remove(followUp);
|
||||
await db.SaveChangesAsync(ct);
|
||||
return Results.Ok(new { code = 0, data = new { success = true }, message = (string?)null });
|
||||
});
|
||||
|
||||
// ===== 患者列表(简版,给随访选患者用)=====
|
||||
// ===== 患者列表(简版)=====
|
||||
group.MapGet("/patients-simple", async (AppDbContext db) =>
|
||||
{
|
||||
var patients = await db.Users.OrderBy(u => u.Name).Select(u => new { u.Id, u.Name, u.Phone }).ToListAsync();
|
||||
var patients = await db.Users.Where(u => u.Role == "User").OrderBy(u => u.Name).Select(u => new { u.Id, u.Name, u.Phone }).ToListAsync();
|
||||
return Results.Ok(new { code = 0, data = patients, message = (string?)null });
|
||||
});
|
||||
|
||||
// ===== 医生个人信息 =====
|
||||
group.MapGet("/profile", async (HttpContext http, AppDbContext db) =>
|
||||
{
|
||||
var profile = await GetDoctorProfile(http, db);
|
||||
if (profile == null) return Results.Ok(new { code = 404, data = (object?)null, message = "医生档案不存在" });
|
||||
return Results.Ok(new { code = 0, data = new { profile.Id, profile.Name, profile.Title, profile.Department, profile.Hospital, profile.AvatarUrl, profile.IsOnline }, message = (string?)null });
|
||||
});
|
||||
|
||||
// ===== 更新医生个人信息 =====
|
||||
group.MapPut("/profile", async (HttpContext http, AppDbContext db, CancellationToken ct) =>
|
||||
{
|
||||
var profile = await GetDoctorProfile(http, db);
|
||||
if (profile == null) return Results.Ok(new { code = 404, data = (object?)null, message = "医生档案不存在" });
|
||||
|
||||
using var reader = new StreamReader(http.Request.Body);
|
||||
var body = await reader.ReadToEndAsync(ct);
|
||||
var json = JsonDocument.Parse(body);
|
||||
if (json.RootElement.TryGetProperty("name", out var n)) profile.Name = n.GetString();
|
||||
if (json.RootElement.TryGetProperty("title", out var t)) profile.Title = t.GetString();
|
||||
if (json.RootElement.TryGetProperty("department", out var d)) profile.Department = d.GetString();
|
||||
if (json.RootElement.TryGetProperty("hospital", out var h)) profile.Hospital = h.GetString();
|
||||
if (json.RootElement.TryGetProperty("avatarUrl", out var a)) profile.AvatarUrl = a.GetString();
|
||||
if (json.RootElement.TryGetProperty("isOnline", out var o)) profile.IsOnline = o.GetBoolean();
|
||||
profile.UpdatedAt = DateTime.UtcNow;
|
||||
await db.SaveChangesAsync(ct);
|
||||
return Results.Ok(new { code = 0, data = new { success = true }, message = (string?)null });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ public static class UserEndpoints
|
||||
var userId = GetUserId(http);
|
||||
var user = await db.Users.Select(u => new
|
||||
{
|
||||
u.Id, u.Phone, u.Name, u.Gender, BirthDate = u.BirthDate != null ? u.BirthDate.Value.ToString("yyyy-MM-dd") : null, u.AvatarUrl
|
||||
u.Id, u.Phone, u.Role, u.Name, u.Gender, BirthDate = u.BirthDate != null ? u.BirthDate.Value.ToString("yyyy-MM-dd") : null, u.AvatarUrl
|
||||
}).FirstOrDefaultAsync(u => u.Id == userId, ct);
|
||||
|
||||
return Results.Ok(new { code = 0, data = user, message = (string?)null });
|
||||
|
||||
Reference in New Issue
Block a user