feat: 后端架构重构 — Endpoint→Service→Repository分层 + AI确认机制 + 异步任务持久化
- 核心业务拆分为 Endpoint → Application Service → Repository 三层 - AI写入操作必须用户确认后才写库(确认卡片机制) - 报告/饮食/用药分析改为持久化任务队列(原子领取/重试/重启恢复) - 运动计划修复: 连续真实日期替代周模板 - 用药提醒去重 + 通知Outbox预留 - 认证收拢到AuthService, 管理员收拢到AdminService - AI会话加用户归属校验防串号 - 提示词调整为患者视角 - 开发假数据已关闭 - 21/21测试通过, 0警告0错误
This commit is contained in:
115
backend/src/Health.Infrastructure/Auth/AuthService.cs
Normal file
115
backend/src/Health.Infrastructure/Auth/AuthService.cs
Normal file
@@ -0,0 +1,115 @@
|
||||
using Health.Application.Auth;
|
||||
using Health.Infrastructure.Services;
|
||||
|
||||
namespace Health.Infrastructure.Auth;
|
||||
|
||||
public sealed class AuthService(AppDbContext db, JwtProvider jwt, SmsService sms) : IAuthService
|
||||
{
|
||||
private const string AdminPhone = "12345678910";
|
||||
private const string AdminSmsCode = "000000";
|
||||
private static readonly Guid AdminId = Guid.Parse("00000000-0000-0000-0000-000000000001");
|
||||
private readonly AppDbContext _db = db;
|
||||
private readonly JwtProvider _jwt = jwt;
|
||||
private readonly SmsService _sms = sms;
|
||||
|
||||
public async Task<AuthResult> SendCodeAsync(string phone, bool revealDevelopmentCode, CancellationToken ct)
|
||||
{
|
||||
var code = phone == AdminPhone ? AdminSmsCode : _sms.GenerateCode();
|
||||
await _db.VerificationCodes.AddAsync(new VerificationCode
|
||||
{
|
||||
Id = Guid.NewGuid(), Phone = phone, Code = code,
|
||||
ExpiresAt = DateTime.UtcNow.AddMinutes(5),
|
||||
}, ct);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
if (phone != AdminPhone) await _sms.SendCodeAsync(phone, code);
|
||||
return new AuthResult(0, new { success = true, devCode = revealDevelopmentCode ? code : null });
|
||||
}
|
||||
|
||||
public async Task<AuthResult> RegisterAsync(RegisterCommand command, CancellationToken ct)
|
||||
{
|
||||
var code = await TakeCodeAsync(command.Phone, command.SmsCode, ct);
|
||||
if (code == null) return Error(40001, "验证码错误或已过期");
|
||||
if (await _db.Users.AnyAsync(x => x.Phone == command.Phone, ct)) return Error(40002, "该手机号已注册,请直接登录");
|
||||
if (string.IsNullOrWhiteSpace(command.Name)) return Error(40003, "请输入姓名");
|
||||
if (!await _db.Doctors.AnyAsync(x => x.Id == command.DoctorId && x.IsActive, ct)) return Error(40004, "所选医生不存在或已停诊");
|
||||
|
||||
code.IsUsed = true;
|
||||
var user = new User
|
||||
{
|
||||
Id = Guid.NewGuid(), Phone = command.Phone, Role = "User", Name = command.Name,
|
||||
DoctorId = command.DoctorId, 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 });
|
||||
var tokens = AddTokens(user.Id, user.Phone, user.Role);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return new AuthResult(0, new { tokens.accessToken, tokens.refreshToken, user = new { user.Id, user.Phone, user.Role, isNew = true } });
|
||||
}
|
||||
|
||||
public async Task<AuthResult> LoginAsync(string phone, string smsCode, CancellationToken ct)
|
||||
{
|
||||
var code = await TakeCodeAsync(phone, smsCode, ct);
|
||||
if (code == null) return Error(40001, "验证码错误或已过期");
|
||||
code.IsUsed = true;
|
||||
|
||||
if (phone == AdminPhone)
|
||||
{
|
||||
var tokens = AddTokens(AdminId, AdminPhone, "Admin");
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return new AuthResult(0, new { tokens.accessToken, tokens.refreshToken, user = new { id = AdminId, phone = AdminPhone, role = "Admin", name = "管理员", isNew = false } });
|
||||
}
|
||||
|
||||
var user = await _db.Users.FirstOrDefaultAsync(x => x.Phone == phone, ct);
|
||||
if (user == null) return Error(40004, "该手机号未注册,请先注册");
|
||||
user.UpdatedAt = DateTime.UtcNow;
|
||||
var userTokens = AddTokens(user.Id, user.Phone, user.Role);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return new AuthResult(0, new
|
||||
{
|
||||
userTokens.accessToken, userTokens.refreshToken,
|
||||
user = new { user.Id, user.Phone, user.Role, user.Name, user.Gender, user.AvatarUrl, BirthDate = user.BirthDate?.ToString("yyyy-MM-dd"), isNew = false }
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<AuthResult> RefreshAsync(string refreshToken, CancellationToken ct)
|
||||
{
|
||||
var oldToken = await _db.RefreshTokens.FirstOrDefaultAsync(x => x.Token == refreshToken && !x.IsRevoked, ct);
|
||||
if (oldToken == null || oldToken.ExpiresAt < DateTime.UtcNow) return Error(40002, "登录已过期,请重新登录");
|
||||
oldToken.IsRevoked = true;
|
||||
|
||||
if (oldToken.UserId == AdminId)
|
||||
{
|
||||
var tokens = AddTokens(AdminId, AdminPhone, "Admin");
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return new AuthResult(0, new { tokens.accessToken, tokens.refreshToken, user = new { role = "Admin" } });
|
||||
}
|
||||
|
||||
var user = await _db.Users.FindAsync([oldToken.UserId], ct);
|
||||
if (user == null) return Error(40002, "用户不存在");
|
||||
var userTokens = AddTokens(user.Id, user.Phone, user.Role);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
return new AuthResult(0, new { userTokens.accessToken, userTokens.refreshToken, user = new { user.Role } });
|
||||
}
|
||||
|
||||
public async Task LogoutAsync(string refreshToken, CancellationToken ct)
|
||||
{
|
||||
var token = await _db.RefreshTokens.FirstOrDefaultAsync(x => x.Token == refreshToken, ct);
|
||||
if (token != null) token.IsRevoked = true;
|
||||
await _db.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
private Task<VerificationCode?> TakeCodeAsync(string phone, string code, CancellationToken ct) =>
|
||||
_db.VerificationCodes.Where(x => x.Phone == phone && x.Code == code && x.ExpiresAt > DateTime.UtcNow && !x.IsUsed)
|
||||
.OrderByDescending(x => x.CreatedAt).FirstOrDefaultAsync(ct);
|
||||
|
||||
private (string accessToken, string refreshToken) AddTokens(Guid userId, string phone, string role)
|
||||
{
|
||||
var access = _jwt.GenerateAccessToken(userId, phone, role);
|
||||
var refresh = _jwt.GenerateRefreshToken();
|
||||
_db.RefreshTokens.Add(new RefreshToken { Id = Guid.NewGuid(), UserId = userId, Token = refresh, ExpiresAt = DateTime.UtcNow.AddDays(30) });
|
||||
return (access, refresh);
|
||||
}
|
||||
|
||||
private static AuthResult Error(int code, string message) => new(code, null, message);
|
||||
}
|
||||
Reference in New Issue
Block a user