feat: 后端架构重构 — Endpoint→Service→Repository分层 + AI确认机制 + 异步任务持久化

- 核心业务拆分为 Endpoint → Application Service → Repository 三层
- AI写入操作必须用户确认后才写库(确认卡片机制)
- 报告/饮食/用药分析改为持久化任务队列(原子领取/重试/重启恢复)
- 运动计划修复: 连续真实日期替代周模板
- 用药提醒去重 + 通知Outbox预留
- 认证收拢到AuthService, 管理员收拢到AdminService
- AI会话加用户归属校验防串号
- 提示词调整为患者视角
- 开发假数据已关闭
- 21/21测试通过, 0警告0错误
This commit is contained in:
MingNian
2026-06-20 20:41:42 +08:00
parent c610417e29
commit 4d213b5a44
132 changed files with 6733 additions and 2856 deletions

View 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);
}