## 后台管理页重构 - 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 扩展
151 lines
6.8 KiB
C#
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);
|
|
}
|