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

@@ -1,281 +1,30 @@
using Health.Infrastructure.Services;
using Health.Application.Auth;
namespace Health.WebApi.Endpoints;
/// <summary>
/// 认证相关 API 端点
/// </summary>
public static class AuthEndpoints
{
private const string AdminPhone = "12345678910";
private const string AdminSmsCode = "000000";
public static void MapAuthEndpoints(this WebApplication app)
{
// 发送短信验证码
app.MapPost("/api/auth/send-sms", async (
SendSmsRequest request,
AppDbContext db,
SmsService sms,
CancellationToken ct) =>
app.MapPost("/api/auth/send-sms", async (SendSmsRequest request, IAuthService auth, CancellationToken ct) =>
ToResult(await auth.SendCodeAsync(request.Phone, app.Environment.IsDevelopment(), ct)));
app.MapPost("/api/auth/register", async (RegisterRequest request, IAuthService auth, CancellationToken ct) =>
ToResult(await auth.RegisterAsync(new RegisterCommand(request.Phone, request.SmsCode, request.Name, request.DoctorId), ct)));
app.MapPost("/api/auth/login", async (LoginRequest request, IAuthService auth, CancellationToken ct) =>
ToResult(await auth.LoginAsync(request.Phone, request.SmsCode, ct)));
app.MapPost("/api/auth/refresh", async (RefreshRequest request, IAuthService auth, CancellationToken ct) =>
ToResult(await auth.RefreshAsync(request.RefreshToken, ct)));
app.MapPost("/api/auth/logout", async (RefreshRequest request, IAuthService auth, CancellationToken ct) =>
{
var code = request.Phone == AdminPhone ? AdminSmsCode : sms.GenerateCode();
var vc = new VerificationCode
{
Id = Guid.NewGuid(),
Phone = request.Phone,
Code = code,
ExpiresAt = DateTime.UtcNow.AddMinutes(5),
};
db.VerificationCodes.Add(vc);
await db.SaveChangesAsync(ct);
if (request.Phone != AdminPhone)
await sms.SendCodeAsync(request.Phone, code);
return Results.Ok(new { code = 0, data = new { success = true, devCode = code }, message = (string?)null });
});
// ── 注册(仅限用户注册,需选择医生)──
app.MapPost("/api/auth/register", async (
RegisterRequest request,
AppDbContext db,
JwtProvider jwt,
CancellationToken ct) =>
{
// 验证码
var validCode = await db.VerificationCodes
.Where(v => v.Phone == request.Phone
&& v.Code == request.SmsCode
&& v.ExpiresAt > DateTime.UtcNow
&& !v.IsUsed)
.OrderByDescending(v => v.CreatedAt)
.FirstOrDefaultAsync(ct);
if (validCode == null)
return Results.Ok(new { code = 40001, data = (object?)null, message = "验证码错误或已过期" });
validCode.IsUsed = true;
// 检查手机号是否已注册
var existing = await db.Users.FirstOrDefaultAsync(u => u.Phone == request.Phone, ct);
if (existing != null)
return Results.Ok(new { code = 40002, data = (object?)null, message = "该手机号已注册,请直接登录" });
// 校验姓名
if (string.IsNullOrWhiteSpace(request.Name))
return Results.Ok(new { code = 40003, data = (object?)null, message = "请输入姓名" });
// 校验医生
var doctor = await db.Doctors.FirstOrDefaultAsync(d => d.Id == request.DoctorId && d.IsActive, ct);
if (doctor == null)
return Results.Ok(new { code = 40004, data = (object?)null, message = "所选医生不存在或已停诊" });
var user = new User
{
Id = Guid.NewGuid(),
Phone = request.Phone,
Role = "User",
Name = request.Name,
DoctorId = request.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 });
await db.SaveChangesAsync(ct);
var accessToken = jwt.GenerateAccessToken(user.Id, user.Phone, "User");
var refreshToken = jwt.GenerateRefreshToken();
db.RefreshTokens.Add(new RefreshToken
{
Id = Guid.NewGuid(),
UserId = user.Id,
Token = refreshToken,
ExpiresAt = DateTime.UtcNow.AddDays(30),
});
await db.SaveChangesAsync(ct);
return Results.Ok(new
{
code = 0,
data = new
{
accessToken,
refreshToken,
user = new { user.Id, user.Phone, user.Role, isNew = true }
},
message = (string?)null
});
});
// ── 登录(已有账号 / 管理员)──
app.MapPost("/api/auth/login", async (
LoginRequest request,
AppDbContext db,
JwtProvider jwt,
CancellationToken ct) =>
{
var validCode = await db.VerificationCodes
.Where(v => v.Phone == request.Phone
&& v.Code == request.SmsCode
&& v.ExpiresAt > DateTime.UtcNow
&& !v.IsUsed)
.OrderByDescending(v => v.CreatedAt)
.FirstOrDefaultAsync(ct);
if (validCode == null)
return Results.Ok(new { code = 40001, data = (object?)null, message = "验证码错误或已过期" });
validCode.IsUsed = true;
// 管理员登录
if (request.Phone == AdminPhone)
{
var adminAccessToken = jwt.GenerateAccessToken(
Guid.Parse("00000000-0000-0000-0000-000000000001"), AdminPhone, "Admin");
var adminRefreshToken = jwt.GenerateRefreshToken();
db.RefreshTokens.Add(new RefreshToken
{
Id = Guid.NewGuid(),
UserId = Guid.Parse("00000000-0000-0000-0000-000000000001"),
Token = adminRefreshToken,
ExpiresAt = DateTime.UtcNow.AddDays(30),
});
await db.SaveChangesAsync(ct);
return Results.Ok(new
{
code = 0,
data = new
{
accessToken = adminAccessToken,
refreshToken = adminRefreshToken,
user = new { id = "00000000-0000-0000-0000-000000000001", phone = AdminPhone, role = "Admin", name = "管理员", isNew = false }
},
message = (string?)null
});
}
var user = await db.Users.FirstOrDefaultAsync(u => u.Phone == request.Phone, ct);
if (user == null)
return Results.Ok(new { code = 40004, data = (object?)null, message = "该手机号未注册,请先登录" });
user.UpdatedAt = DateTime.UtcNow;
await db.SaveChangesAsync(ct);
var accessToken = jwt.GenerateAccessToken(user.Id, user.Phone, user.Role);
var refreshToken = jwt.GenerateRefreshToken();
db.RefreshTokens.Add(new RefreshToken
{
Id = Guid.NewGuid(),
UserId = user.Id,
Token = refreshToken,
ExpiresAt = DateTime.UtcNow.AddDays(30),
});
await db.SaveChangesAsync(ct);
return Results.Ok(new
{
code = 0,
data = new
{
accessToken,
refreshToken,
user = new {
user.Id, user.Phone, user.Role, user.Name,
user.Gender, user.AvatarUrl,
BirthDate = user.BirthDate?.ToString("yyyy-MM-dd"),
isNew = false
}
},
message = (string?)null
});
});
// 刷新 token
app.MapPost("/api/auth/refresh", async (
RefreshRequest request,
AppDbContext db,
JwtProvider jwt,
CancellationToken ct) =>
{
var oldToken = await db.RefreshTokens
.FirstOrDefaultAsync(t => t.Token == request.RefreshToken && !t.IsRevoked, ct);
if (oldToken == null || oldToken.ExpiresAt < DateTime.UtcNow)
return Results.Ok(new { code = 40002, data = (object?)null, message = "登录已过期,请重新登录" });
oldToken.IsRevoked = true;
var user = await db.Users.FindAsync([oldToken.UserId], ct);
// 管理员刷新:无 User 记录,直接用固定信息
if (user == null && oldToken.UserId == Guid.Parse("00000000-0000-0000-0000-000000000001"))
{
var adminAccessToken = jwt.GenerateAccessToken(
Guid.Parse("00000000-0000-0000-0000-000000000001"), AdminPhone, "Admin");
var adminNewRefresh = jwt.GenerateRefreshToken();
db.RefreshTokens.Add(new RefreshToken
{
Id = Guid.NewGuid(),
UserId = Guid.Parse("00000000-0000-0000-0000-000000000001"),
Token = adminNewRefresh,
ExpiresAt = DateTime.UtcNow.AddDays(30),
});
await db.SaveChangesAsync(ct);
return Results.Ok(new
{
code = 0,
data = new { accessToken = adminAccessToken, refreshToken = adminNewRefresh, user = new { role = "Admin" } },
message = (string?)null
});
}
if (user == null)
return Results.Ok(new { code = 40002, data = (object?)null, message = "用户不存在" });
var accessToken = jwt.GenerateAccessToken(user.Id, user.Phone, user.Role);
var newRefreshToken = jwt.GenerateRefreshToken();
db.RefreshTokens.Add(new RefreshToken
{
Id = Guid.NewGuid(),
UserId = user.Id,
Token = newRefreshToken,
ExpiresAt = DateTime.UtcNow.AddDays(30),
});
await db.SaveChangesAsync(ct);
return Results.Ok(new
{
code = 0,
data = new { accessToken, refreshToken = newRefreshToken, user = new { user.Role } },
message = (string?)null
});
});
// 登出
app.MapPost("/api/auth/logout", async (
RefreshRequest request,
AppDbContext db,
CancellationToken ct) =>
{
var token = await db.RefreshTokens
.FirstOrDefaultAsync(t => t.Token == request.RefreshToken, ct);
if (token != null) token.IsRevoked = true;
await db.SaveChangesAsync(ct);
await auth.LogoutAsync(request.RefreshToken, ct);
return Results.Ok(new { code = 0, data = new { success = true }, message = (string?)null });
});
}
private static IResult ToResult(AuthResult result) =>
Results.Ok(new { code = result.Code, data = result.Data, message = result.Message });
}
// ---- 请求 DTO ----
public sealed record SendSmsRequest(string Phone);
public sealed record RegisterRequest(string Phone, string SmsCode, string Name, Guid DoctorId);
public sealed record LoginRequest(string Phone, string SmsCode);