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:
191
backend/tests/Health.Tests/admin_service_tests.cs
Normal file
191
backend/tests/Health.Tests/admin_service_tests.cs
Normal file
@@ -0,0 +1,191 @@
|
||||
using Health.Application.Admin;
|
||||
using Health.Domain.Entities;
|
||||
using Health.Infrastructure.Admin;
|
||||
using Health.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Health.Tests;
|
||||
|
||||
public sealed class AdminServiceTests
|
||||
{
|
||||
private static AppDbContext CreateDbContext()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<AppDbContext>()
|
||||
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||||
.Options;
|
||||
return new AppDbContext(options);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddDoctor_CreatesDoctorLoginAndProfileTogether()
|
||||
{
|
||||
await using var db = CreateDbContext();
|
||||
var service = new AdminService(db);
|
||||
|
||||
var result = await service.AddDoctorAsync(
|
||||
new AddDoctorCommand("13800138001", "王医生", "主任医师", "心内科", "高血压"),
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.Equal(0, result.Code);
|
||||
var doctor = await db.Doctors.SingleAsync();
|
||||
var user = await db.Users.SingleAsync();
|
||||
var profile = await db.DoctorProfiles.SingleAsync();
|
||||
Assert.Equal("Doctor", user.Role);
|
||||
Assert.Equal(doctor.Id, profile.DoctorId);
|
||||
Assert.Equal(user.Id, profile.UserId);
|
||||
Assert.Equal(doctor.Name, user.Name);
|
||||
Assert.Equal(doctor.Name, profile.Name);
|
||||
Assert.Equal(doctor.Title, profile.Title);
|
||||
Assert.Equal(doctor.Department, profile.Department);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddDoctor_RejectsAPhoneAlreadyUsedByAnyAccount()
|
||||
{
|
||||
await using var db = CreateDbContext();
|
||||
db.Users.Add(new User
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Phone = "13800138002",
|
||||
Role = "User",
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow,
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
var service = new AdminService(db);
|
||||
|
||||
var result = await service.AddDoctorAsync(
|
||||
new AddDoctorCommand("13800138002", "李医生", null, null, null),
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.NotEqual(0, result.Code);
|
||||
Assert.Empty(db.Doctors);
|
||||
Assert.Empty(db.DoctorProfiles);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateDoctor_SynchronizesDoctorLoginAndProfile()
|
||||
{
|
||||
await using var db = CreateDbContext();
|
||||
var (doctor, user, profile) = await SeedDoctorAsync(db);
|
||||
var service = new AdminService(db);
|
||||
|
||||
var result = await service.UpdateDoctorAsync(
|
||||
doctor.Id,
|
||||
new UpdateDoctorCommand("13800138999", "新姓名", "副主任医师", "神经内科", "脑血管"),
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.Equal(0, result.Code);
|
||||
Assert.Equal("13800138999", doctor.Phone);
|
||||
Assert.Equal("13800138999", user.Phone);
|
||||
Assert.Equal("新姓名", doctor.Name);
|
||||
Assert.Equal("新姓名", user.Name);
|
||||
Assert.Equal("新姓名", profile.Name);
|
||||
Assert.Equal("副主任医师", profile.Title);
|
||||
Assert.Equal("神经内科", profile.Department);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ToggleDoctor_SynchronizesAvailabilityButKeepsLoginEnabled()
|
||||
{
|
||||
await using var db = CreateDbContext();
|
||||
var (doctor, user, profile) = await SeedDoctorAsync(db);
|
||||
var service = new AdminService(db);
|
||||
|
||||
var result = await service.ToggleDoctorAsync(doctor.Id, CancellationToken.None);
|
||||
|
||||
Assert.Equal(0, result.Code);
|
||||
Assert.False(doctor.IsActive);
|
||||
Assert.False(profile.IsActive);
|
||||
Assert.Equal("Doctor", user.Role);
|
||||
Assert.NotNull(await db.Users.FindAsync(user.Id));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteDoctor_IsRejectedWhilePatientsAreBound()
|
||||
{
|
||||
await using var db = CreateDbContext();
|
||||
var (doctor, user, profile) = await SeedDoctorAsync(db);
|
||||
db.Users.Add(new User
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Phone = "13800138088",
|
||||
Name = "患者",
|
||||
Role = "User",
|
||||
DoctorId = doctor.Id,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow,
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
var service = new AdminService(db);
|
||||
|
||||
var result = await service.DeleteDoctorAsync(doctor.Id, CancellationToken.None);
|
||||
|
||||
Assert.NotEqual(0, result.Code);
|
||||
Assert.NotNull(await db.Doctors.FindAsync(doctor.Id));
|
||||
Assert.NotNull(await db.Users.FindAsync(user.Id));
|
||||
Assert.NotNull(await db.DoctorProfiles.FindAsync(profile.Id));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteUnusedDoctor_RemovesLoginProfileAndRefreshTokens()
|
||||
{
|
||||
await using var db = CreateDbContext();
|
||||
var (doctor, user, _) = await SeedDoctorAsync(db);
|
||||
db.RefreshTokens.Add(new RefreshToken
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = user.Id,
|
||||
Token = "refresh-token",
|
||||
ExpiresAt = DateTime.UtcNow.AddDays(1),
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
var service = new AdminService(db);
|
||||
|
||||
var result = await service.DeleteDoctorAsync(doctor.Id, CancellationToken.None);
|
||||
|
||||
Assert.Equal(0, result.Code);
|
||||
Assert.Empty(db.Doctors);
|
||||
Assert.Empty(db.DoctorProfiles);
|
||||
Assert.Empty(db.Users);
|
||||
Assert.Empty(db.RefreshTokens);
|
||||
}
|
||||
|
||||
private static async Task<(Doctor Doctor, User User, DoctorProfile Profile)> SeedDoctorAsync(AppDbContext db)
|
||||
{
|
||||
var doctor = new Doctor
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Phone = "13800138003",
|
||||
Name = "原姓名",
|
||||
Title = "医师",
|
||||
Department = "内科",
|
||||
IsActive = true,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
};
|
||||
var user = new User
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Phone = doctor.Phone,
|
||||
Name = doctor.Name,
|
||||
Role = "Doctor",
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow,
|
||||
};
|
||||
var profile = new DoctorProfile
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
DoctorId = doctor.Id,
|
||||
UserId = user.Id,
|
||||
Name = doctor.Name,
|
||||
Title = doctor.Title,
|
||||
Department = doctor.Department,
|
||||
IsActive = true,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow,
|
||||
};
|
||||
db.AddRange(doctor, user, profile);
|
||||
await db.SaveChangesAsync();
|
||||
return (doctor, user, profile);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user