Files
AI-Health/backend/src/Health.Infrastructure/Admin/AdminService.cs
MingNian ae94ced2d5 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 扩展
2026-07-18 17:48:44 +08:00

151 lines
6.8 KiB
C#

using Health.Application.Admin;
namespace Health.Infrastructure.Admin;
public sealed class AdminService(AppDbContext db) : IAdminService
{
private readonly AppDbContext _db = db;
public async Task<AdminResult> ListDoctorsAsync(CancellationToken ct)
{
var doctors = await _db.Doctors.AsNoTracking().OrderBy(x => x.CreatedAt)
.Select(x => new { x.Id, x.Name, x.Title, x.Department, x.Phone, x.ProfessionalDirection, x.IsActive, x.AvatarUrl, x.Introduction, x.CreatedAt })
.ToListAsync(ct);
return Ok(doctors);
}
public async Task<AdminResult> AddDoctorAsync(AddDoctorCommand command, CancellationToken ct)
{
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 = 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);
_db.Users.Add(user);
_db.DoctorProfiles.Add(new DoctorProfile
{
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 });
}
public async Task<AdminResult> UpdateDoctorAsync(Guid id, UpdateDoctorCommand command, CancellationToken ct)
{
var doctor = await _db.Doctors.FindAsync([id], ct);
if (doctor == null) return Error(404, "医生不存在");
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 });
}
public async Task<AdminResult> ToggleDoctorAsync(Guid id, CancellationToken ct)
{
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 });
}
public async Task<AdminResult> DeleteDoctorAsync(Guid id, CancellationToken ct)
{
var doctor = await _db.Doctors.FindAsync([id], ct);
if (doctor == null) return Error(404, "医生不存在");
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 });
}
public async Task<AdminResult> ListPatientsAsync(string? search, int page, int pageSize, CancellationToken ct)
{
page = Math.Max(page, 1);
pageSize = Math.Clamp(pageSize, 1, 100);
var query = _db.Users.AsNoTracking().Where(x => x.Role == "User");
if (!string.IsNullOrWhiteSpace(search))
query = query.Where(x =>
(x.Name != null && x.Name.Contains(search)) ||
(x.Phone != null && x.Phone.Contains(search)));
var total = await query.CountAsync(ct);
var patients = await query.OrderByDescending(x => x.CreatedAt).Skip((page - 1) * pageSize).Take(pageSize)
.Select(x => new { x.Id, x.Name, x.Phone, x.Gender, x.BirthDate, x.CreatedAt, DoctorName = x.Doctor != null ? x.Doctor.Name : null, DoctorDepartment = x.Doctor != null ? x.Doctor.Department : null })
.ToListAsync(ct);
return Ok(new { patients, total, page, pageSize });
}
private static AdminResult Ok(object data) => new(0, data);
private static AdminResult Error(int code, string message) => new(code, null, message);
}