feat: 后端架构重构 — Endpoint→Service→Repository分层 + AI确认机制 + 异步任务持久化
- 核心业务拆分为 Endpoint → Application Service → Repository 三层 - AI写入操作必须用户确认后才写库(确认卡片机制) - 报告/饮食/用药分析改为持久化任务队列(原子领取/重试/重启恢复) - 运动计划修复: 连续真实日期替代周模板 - 用药提醒去重 + 通知Outbox预留 - 认证收拢到AuthService, 管理员收拢到AdminService - AI会话加用户归属校验防串号 - 提示词调整为患者视角 - 开发假数据已关闭 - 21/21测试通过, 0警告0错误
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
using Health.Application.Reports;
|
||||
using Health.Infrastructure.Data.Records;
|
||||
|
||||
namespace Health.Infrastructure.Reports;
|
||||
|
||||
public sealed class EfReportAnalysisQueue(AppDbContext db) : IReportAnalysisQueue
|
||||
{
|
||||
private const int MaxAttempts = 3;
|
||||
private readonly AppDbContext _db = db;
|
||||
|
||||
public async Task EnqueueAsync(ReportAnalysisJob job, CancellationToken ct = default)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
await _db.ReportAnalysisTasks.AddAsync(new ReportAnalysisTaskRecord
|
||||
{
|
||||
Id = job.TaskId,
|
||||
ReportId = job.ReportId,
|
||||
FilePath = job.FilePath,
|
||||
Status = "Pending",
|
||||
Attempts = 0,
|
||||
AvailableAt = now,
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now,
|
||||
}, ct);
|
||||
await _db.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
public Task RecoverStaleAsync(CancellationToken ct)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
return _db.ReportAnalysisTasks
|
||||
.Where(x => x.Status == "Processing" && x.UpdatedAt < now.AddMinutes(-5) && x.Attempts < MaxAttempts)
|
||||
.ExecuteUpdateAsync(setters => setters
|
||||
.SetProperty(x => x.Status, "Pending")
|
||||
.SetProperty(x => x.AvailableAt, now)
|
||||
.SetProperty(x => x.UpdatedAt, now), ct);
|
||||
}
|
||||
|
||||
public async Task<ReportAnalysisJob?> TryTakeAsync(CancellationToken ct)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var candidate = await _db.ReportAnalysisTasks.AsNoTracking()
|
||||
.Where(x => x.Status == "Pending" && x.AvailableAt <= now && x.Attempts < MaxAttempts)
|
||||
.OrderBy(x => x.CreatedAt)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
if (candidate == null) return null;
|
||||
|
||||
var claimed = await _db.ReportAnalysisTasks
|
||||
.Where(x => x.Id == candidate.Id && x.Status == "Pending")
|
||||
.ExecuteUpdateAsync(setters => setters
|
||||
.SetProperty(x => x.Status, "Processing")
|
||||
.SetProperty(x => x.Attempts, x => x.Attempts + 1)
|
||||
.SetProperty(x => x.UpdatedAt, now), ct);
|
||||
return claimed == 0
|
||||
? null
|
||||
: new ReportAnalysisJob(candidate.Id, candidate.ReportId, candidate.FilePath);
|
||||
}
|
||||
|
||||
public async Task CompleteAsync(Guid taskId, CancellationToken ct)
|
||||
{
|
||||
await _db.ReportAnalysisTasks
|
||||
.Where(x => x.Id == taskId && x.Status == "Processing")
|
||||
.ExecuteUpdateAsync(setters => setters
|
||||
.SetProperty(x => x.Status, "Completed")
|
||||
.SetProperty(x => x.UpdatedAt, DateTime.UtcNow), ct);
|
||||
}
|
||||
|
||||
public async Task RetryAsync(Guid taskId, string error, CancellationToken ct)
|
||||
{
|
||||
var task = await _db.ReportAnalysisTasks.FirstOrDefaultAsync(x => x.Id == taskId, ct);
|
||||
if (task == null) return;
|
||||
|
||||
task.LastError = error.Length > 2000 ? error[..2000] : error;
|
||||
task.UpdatedAt = DateTime.UtcNow;
|
||||
if (task.Attempts >= MaxAttempts)
|
||||
{
|
||||
task.Status = "Failed";
|
||||
}
|
||||
else
|
||||
{
|
||||
task.Status = "Pending";
|
||||
task.AvailableAt = DateTime.UtcNow.AddSeconds(Math.Pow(2, task.Attempts) * 5);
|
||||
}
|
||||
await _db.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user