feat: 后台管理页全量重构 + backoffice 共享模块 + 启动脚本优化

## 后台管理页重构
- admin 端: add_doctor / doctors / home / patients 四页重构
- doctor 端: consultations / dashboard / followup_edit / followups / home / patient_detail / patients / profile / report_detail / reports / settings 十一页重构
- 新增 backoffice 共享模块: backoffice_refresh_providers + backoffice_formatters + backoffice_ui

## 后端
- AdminService 增强(分页/搜索/统计)
- doctor_endpoints 微调
- appsettings.Development 调整
- 新增 admin_service_tests

## 前端其他
- 今日健康卡片 taskRow 行高 5->7(每行 44->48px)
- 健康仪表盘配色定 #4FACFE 纯色蓝
- admin_drawer / doctor_drawer 微调
- api_client IP 适配

## 启动脚本
- start-dev.bat: 加后端就绪检测(轮询 openapi 端点, 最多等 30s)

## 清理
- 删除旧品牌图(agent_welcome_abstract / login_background_v1)
- 删除 app_status_badge 组件
- 删除旧 HANDOFF 文档, 新增 07-17 版

## 测试
- 新增 backoffice_formatters / backoffice_refresh / backoffice_ui 三个测试
- app_router_test 扩展
This commit is contained in:
MingNian
2026-07-18 17:48:44 +08:00
parent e1f4a4b91f
commit ae94ced2d5
41 changed files with 1610 additions and 820 deletions

View File

@@ -16,25 +16,32 @@ public sealed class AdminService(AppDbContext db) : IAdminService
public async Task<AdminResult> AddDoctorAsync(AddDoctorCommand command, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(command.Phone)) return Error(40001, "手机号不能为空");
if (string.IsNullOrWhiteSpace(command.Name)) return Error(40002, "姓名不能为空");
var phone = command.Phone.Trim();
var name = command.Name.Trim();
if (string.IsNullOrWhiteSpace(phone)) return Error(40001, "手机号不能为空");
if (string.IsNullOrWhiteSpace(name)) return Error(40002, "姓名不能为空");
if (await _db.Users.AnyAsync(x => x.Phone == phone, ct) ||
await _db.Doctors.AnyAsync(x => x.Phone == phone, ct))
return Error(40003, "该手机号已被其他账号使用");
var doctor = new Doctor
{
Id = Guid.NewGuid(), Name = command.Name, Title = command.Title, Department = command.Department,
Phone = command.Phone, ProfessionalDirection = command.ProfessionalDirection, IsActive = true, CreatedAt = DateTime.UtcNow,
Id = Guid.NewGuid(), Name = name, Title = command.Title?.Trim(), Department = command.Department?.Trim(),
Phone = phone, ProfessionalDirection = command.ProfessionalDirection?.Trim(), IsActive = true, CreatedAt = DateTime.UtcNow,
};
var user = new User
{
Id = Guid.NewGuid(), Phone = phone, Role = "Doctor", Name = name,
CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow
};
_db.Doctors.Add(doctor);
if (!await _db.Users.AnyAsync(x => x.Phone == command.Phone, ct))
_db.Users.Add(user);
_db.DoctorProfiles.Add(new DoctorProfile
{
var user = new User { Id = Guid.NewGuid(), Phone = command.Phone, Role = "Doctor", Name = command.Name, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow };
_db.Users.Add(user);
_db.DoctorProfiles.Add(new DoctorProfile
{
Id = Guid.NewGuid(), UserId = user.Id, DoctorId = doctor.Id, Name = command.Name,
Title = command.Title, Department = command.Department, IsActive = true,
CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow,
});
}
Id = Guid.NewGuid(), UserId = user.Id, DoctorId = doctor.Id, Name = name,
Title = doctor.Title, Department = doctor.Department, IsActive = true,
CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow,
});
await _db.SaveChangesAsync(ct);
return Ok(new { doctor.Id, doctor.Name });
}
@@ -43,11 +50,43 @@ public sealed class AdminService(AppDbContext db) : IAdminService
{
var doctor = await _db.Doctors.FindAsync([id], ct);
if (doctor == null) return Error(404, "医生不存在");
if (command.Name != null) doctor.Name = command.Name;
if (command.Title != null) doctor.Title = command.Title;
if (command.Department != null) doctor.Department = command.Department;
if (command.Phone != null) doctor.Phone = command.Phone;
if (command.ProfessionalDirection != null) doctor.ProfessionalDirection = command.ProfessionalDirection;
var profile = await _db.DoctorProfiles.Include(x => x.User)
.FirstOrDefaultAsync(x => x.DoctorId == id, ct);
if (profile == null) return Error(40901, "医生登录资料不完整,请联系技术人员修复");
var user = profile.User;
if (command.Phone != null)
{
var phone = command.Phone.Trim();
if (phone.Length == 0) return Error(40001, "手机号不能为空");
if (await _db.Users.AnyAsync(x => x.Id != user.Id && x.Phone == phone, ct) ||
await _db.Doctors.AnyAsync(x => x.Id != id && x.Phone == phone, ct))
return Error(40003, "该手机号已被其他账号使用");
doctor.Phone = phone;
user.Phone = phone;
}
if (command.Name != null)
{
var name = command.Name.Trim();
if (name.Length == 0) return Error(40002, "姓名不能为空");
doctor.Name = name;
user.Name = name;
profile.Name = name;
}
if (command.Title != null)
{
doctor.Title = command.Title.Trim();
profile.Title = doctor.Title;
}
if (command.Department != null)
{
doctor.Department = command.Department.Trim();
profile.Department = doctor.Department;
}
if (command.ProfessionalDirection != null)
doctor.ProfessionalDirection = command.ProfessionalDirection.Trim();
user.UpdatedAt = DateTime.UtcNow;
profile.UpdatedAt = DateTime.UtcNow;
await _db.SaveChangesAsync(ct);
return Ok(new { doctor.Id });
}
@@ -56,7 +95,11 @@ public sealed class AdminService(AppDbContext db) : IAdminService
{
var doctor = await _db.Doctors.FindAsync([id], ct);
if (doctor == null) return Error(404, "医生不存在");
var profile = await _db.DoctorProfiles.FirstOrDefaultAsync(x => x.DoctorId == id, ct);
if (profile == null) return Error(40901, "医生登录资料不完整,请联系技术人员修复");
doctor.IsActive = !doctor.IsActive;
profile.IsActive = doctor.IsActive;
profile.UpdatedAt = DateTime.UtcNow;
await _db.SaveChangesAsync(ct);
return Ok(new { doctor.Id, doctor.IsActive });
}
@@ -65,7 +108,22 @@ public sealed class AdminService(AppDbContext db) : IAdminService
{
var doctor = await _db.Doctors.FindAsync([id], ct);
if (doctor == null) return Error(404, "医生不存在");
await _db.Users.Where(x => x.DoctorId == id).ExecuteUpdateAsync(s => s.SetProperty(x => x.DoctorId, (Guid?)null), ct);
if (await _db.Users.AnyAsync(x => x.Role == "User" && x.DoctorId == id, ct))
return Error(40004, "该医生仍有绑定患者,请先停用,不能直接删除");
if (await _db.Consultations.AnyAsync(x => x.DoctorId == id, ct))
return Error(40005, "该医生已有问诊记录,只能停用,不能删除");
var profile = await _db.DoctorProfiles.Include(x => x.User)
.FirstOrDefaultAsync(x => x.DoctorId == id, ct);
if (profile != null)
{
var user = profile.User;
var refreshTokens = await _db.RefreshTokens.Where(x => x.UserId == user.Id).ToListAsync(ct);
_db.RefreshTokens.RemoveRange(refreshTokens);
_db.DoctorProfiles.Remove(profile);
_db.Users.Remove(user);
}
_db.Doctors.Remove(doctor);
await _db.SaveChangesAsync(ct);
return Ok(new { success = true });

View File

@@ -105,6 +105,7 @@ public static class DoctorEndpoints
doctorHospital = profile.Hospital,
doctorAvatar = profile.AvatarUrl,
doctorOnline = profile.IsOnline,
doctorActive = profile.IsActive,
stats = new { totalPatients, activeConsultations, pendingReports, todayFollowUps },
pendingConsultations,
pendingReports = pendingReportList,

View File

@@ -2,7 +2,8 @@
"Logging": {
"LogLevel": {
"Default": "Warning",
"Microsoft.AspNetCore": "Warning"
"Microsoft.AspNetCore": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB