From 01cd1324761c64b269221f91bc8e5aa884eb66c8 Mon Sep 17 00:00:00 2001
From: MingNian <1281442923@qq.com>
Date: Sun, 14 Jun 2026 23:28:14 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8C=BB=E7=94=9F=E7=AB=AF=E5=AE=8C?=
=?UTF-8?q?=E6=95=B4=E5=AE=9E=E7=8E=B0=20+=20=E5=8F=8C=E8=A7=92=E8=89=B2?=
=?UTF-8?q?=E7=99=BB=E5=BD=95=20+=20=E5=81=A5=E5=BA=B7=E6=97=A5=E5=8E=86?=
=?UTF-8?q?=20+=20=E9=A5=AE=E9=A3=9F=E8=AE=B0=E5=BD=95=E9=87=8D=E5=81=9A?=
=?UTF-8?q?=20+=20=E9=85=8D=E8=89=B2=E7=B3=BB=E7=BB=9F=E9=87=8D=E6=9E=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
后端:
- 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页面完整配色设计+图标底色对照表)
---
.../Health.Domain/Entities/doctor_profile.cs | 21 +
backend/src/Health.Domain/Entities/user.cs | 4 +-
.../Data/app_db_context.cs | 9 +
.../Health.Infrastructure/Data/data_seeder.cs | 36 +-
.../Services/jwt_provider.cs | 4 +-
.../Health.WebApi/Endpoints/auth_endpoints.cs | 149 +++-
.../Endpoints/calendar_endpoints.cs | 104 ++-
.../Health.WebApi/Endpoints/diet_endpoints.cs | 15 +
.../Endpoints/doctor_endpoints.cs | 425 ++++-----
.../Health.WebApi/Endpoints/user_endpoints.cs | 2 +-
docs/color_design_system.md | 819 ++++++++++++++++++
health_app/lib/app.dart | 26 +-
health_app/lib/core/app_colors.dart | 127 +--
health_app/lib/core/app_router.dart | 96 +-
health_app/lib/core/app_theme.dart | 4 +-
health_app/lib/pages/auth/login_page.dart | 312 +++----
.../doctor/doctor_consultations_page.dart | 61 ++
.../pages/doctor/doctor_dashboard_page.dart | 196 +++++
.../doctor/doctor_followup_edit_page.dart | 159 ++++
.../pages/doctor/doctor_followups_page.dart | 111 +++
.../lib/pages/doctor/doctor_home_page.dart | 67 ++
.../doctor/doctor_patient_detail_page.dart | 151 ++++
.../pages/doctor/doctor_patients_page.dart | 117 +++
.../lib/pages/doctor/doctor_profile_page.dart | 81 ++
.../doctor/doctor_report_detail_page.dart | 273 ++++++
.../lib/pages/doctor/doctor_reports_page.dart | 81 ++
.../pages/doctor/doctor_settings_page.dart | 58 ++
health_app/lib/pages/home/home_page.dart | 2 +-
.../home/widgets/chat_messages_view.dart | 18 +-
health_app/lib/pages/remaining_pages.dart | 801 ++++++++++-------
health_app/lib/providers/auth_provider.dart | 71 +-
health_app/lib/services/health_service.dart | 4 +
health_app/lib/widgets/doctor_drawer.dart | 105 +++
health_app/lib/widgets/health_drawer.dart | 7 +-
.../lib/widgets/service_package_card.dart | 52 +-
35 files changed, 3473 insertions(+), 1095 deletions(-)
create mode 100644 backend/src/Health.Domain/Entities/doctor_profile.cs
create mode 100644 docs/color_design_system.md
create mode 100644 health_app/lib/pages/doctor/doctor_consultations_page.dart
create mode 100644 health_app/lib/pages/doctor/doctor_dashboard_page.dart
create mode 100644 health_app/lib/pages/doctor/doctor_followup_edit_page.dart
create mode 100644 health_app/lib/pages/doctor/doctor_followups_page.dart
create mode 100644 health_app/lib/pages/doctor/doctor_home_page.dart
create mode 100644 health_app/lib/pages/doctor/doctor_patient_detail_page.dart
create mode 100644 health_app/lib/pages/doctor/doctor_patients_page.dart
create mode 100644 health_app/lib/pages/doctor/doctor_profile_page.dart
create mode 100644 health_app/lib/pages/doctor/doctor_report_detail_page.dart
create mode 100644 health_app/lib/pages/doctor/doctor_reports_page.dart
create mode 100644 health_app/lib/pages/doctor/doctor_settings_page.dart
create mode 100644 health_app/lib/widgets/doctor_drawer.dart
diff --git a/backend/src/Health.Domain/Entities/doctor_profile.cs b/backend/src/Health.Domain/Entities/doctor_profile.cs
new file mode 100644
index 0000000..3b58741
--- /dev/null
+++ b/backend/src/Health.Domain/Entities/doctor_profile.cs
@@ -0,0 +1,21 @@
+namespace Health.Domain.Entities;
+
+///
+/// 医生扩展信息
+///
+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!;
+}
diff --git a/backend/src/Health.Domain/Entities/user.cs b/backend/src/Health.Domain/Entities/user.cs
index dcfa7c1..b872adf 100644
--- a/backend/src/Health.Domain/Entities/user.cs
+++ b/backend/src/Health.Domain/Entities/user.cs
@@ -1,12 +1,13 @@
namespace Health.Domain.Entities;
///
-/// 用户(患者)
+/// 用户(支持用户/医生双角色)
///
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 DeviceTokens { get; set; } = [];
public HealthArchive? HealthArchive { get; set; }
public NotificationPreference? NotificationPreference { get; set; }
+ public DoctorProfile? DoctorProfile { get; set; }
}
diff --git a/backend/src/Health.Infrastructure/Data/app_db_context.cs b/backend/src/Health.Infrastructure/Data/app_db_context.cs
index 09d8420..df3a934 100644
--- a/backend/src/Health.Infrastructure/Data/app_db_context.cs
+++ b/backend/src/Health.Infrastructure/Data/app_db_context.cs
@@ -24,6 +24,7 @@ public sealed class AppDbContext(DbContextOptions options) : DbCon
public DbSet Consultations => Set();
public DbSet ConsultationMessages => Set();
public DbSet Doctors => Set();
+ public DbSet DoctorProfiles => Set();
public DbSet FollowUps => Set();
public DbSet HealthArchives => Set();
@@ -41,6 +42,14 @@ public sealed class AppDbContext(DbContextOptions options) : DbCon
builder.Entity(e =>
{
e.HasIndex(u => u.Phone).IsUnique();
+ e.Property(u => u.Role).HasMaxLength(32).HasDefaultValue("User");
+ });
+
+ // ---- DoctorProfile ----
+ builder.Entity(e =>
+ {
+ e.HasIndex(d => d.UserId).IsUnique();
+ e.HasOne(d => d.User).WithOne(u => u.DoctorProfile).HasForeignKey(d => d.UserId);
});
// ---- HealthRecord ----
diff --git a/backend/src/Health.Infrastructure/Data/data_seeder.cs b/backend/src/Health.Infrastructure/Data/data_seeder.cs
index 314e7eb..5212481 100644
--- a/backend/src/Health.Infrastructure/Data/data_seeder.cs
+++ b/backend/src/Health.Infrastructure/Data/data_seeder.cs
@@ -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;
}
}
diff --git a/backend/src/Health.Infrastructure/Services/jwt_provider.cs b/backend/src/Health.Infrastructure/Services/jwt_provider.cs
index 5000567..72eb17a 100644
--- a/backend/src/Health.Infrastructure/Services/jwt_provider.cs
+++ b/backend/src/Health.Infrastructure/Services/jwt_provider.cs
@@ -18,12 +18,14 @@ public sealed class JwtProvider(IConfiguration configuration)
///
/// 生成 access_token(30 分钟有效)
///
- 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()),
};
diff --git a/backend/src/Health.WebApi/Endpoints/auth_endpoints.cs b/backend/src/Health.WebApi/Endpoints/auth_endpoints.cs
index c97f049..df03640 100644
--- a/backend/src/Health.WebApi/Endpoints/auth_endpoints.cs
+++ b/backend/src/Health.WebApi/Endpoints/auth_endpoints.cs
@@ -7,6 +7,11 @@ namespace Health.WebApi.Endpoints;
///
public static class AuthEndpoints
{
+ ///
+ /// 医生注册审核码(后期可移到配置表)
+ ///
+ 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);
diff --git a/backend/src/Health.WebApi/Endpoints/calendar_endpoints.cs b/backend/src/Health.WebApi/Endpoints/calendar_endpoints.cs
index 099c19e..49d2b53 100644
--- a/backend/src/Health.WebApi/Endpoints/calendar_endpoints.cs
+++ b/backend/src/Health.WebApi/Endpoints/calendar_endpoints.cs
@@ -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