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,76 @@
using Health.Infrastructure.AI;
using Health.Infrastructure.Data;
using Health.Infrastructure.Data.Records;
using Health.Infrastructure.Reports;
using Microsoft.EntityFrameworkCore;
namespace Health.Tests;
public sealed class PersistencePipelineTests
{
[Fact]
public async Task AiConfirmation_CanOnlyBeTakenOnceByOwner()
{
await using var db = CreateDbContext();
var store = new EfAiWriteConfirmationStore(db);
var ownerId = Guid.NewGuid();
var command = await store.CreateAsync(ownerId, "record_health_data", "{}", TimeSpan.FromMinutes(10), CancellationToken.None);
var wrongUser = await store.TakeAsync(command.Id, Guid.NewGuid(), CancellationToken.None);
var firstTake = await store.TakeAsync(command.Id, ownerId, CancellationToken.None);
var secondTake = await store.TakeAsync(command.Id, ownerId, CancellationToken.None);
Assert.Null(wrongUser);
Assert.NotNull(firstTake);
Assert.Null(secondTake);
}
[Fact]
public async Task ReportTask_RetryUsesBackoffBeforeMaxAttempts()
{
await using var db = CreateDbContext();
var task = NewReportTask(attempts: 1);
db.ReportAnalysisTasks.Add(task);
await db.SaveChangesAsync();
var queue = new EfReportAnalysisQueue(db);
var before = DateTime.UtcNow;
await queue.RetryAsync(task.Id, "temporary failure", CancellationToken.None);
Assert.Equal("Pending", task.Status);
Assert.True(task.AvailableAt > before);
Assert.Equal("temporary failure", task.LastError);
}
[Fact]
public async Task ReportTask_RetryMarksFailedAtMaxAttempts()
{
await using var db = CreateDbContext();
var task = NewReportTask(attempts: 3);
db.ReportAnalysisTasks.Add(task);
await db.SaveChangesAsync();
var queue = new EfReportAnalysisQueue(db);
await queue.RetryAsync(task.Id, new string('x', 2500), CancellationToken.None);
Assert.Equal("Failed", task.Status);
Assert.Equal(2000, task.LastError!.Length);
}
private static ReportAnalysisTaskRecord NewReportTask(int attempts) => new()
{
Id = Guid.NewGuid(),
ReportId = Guid.NewGuid(),
FilePath = "report.jpg",
Status = "Processing",
Attempts = attempts,
AvailableAt = DateTime.UtcNow,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow,
};
private static AppDbContext CreateDbContext() => new(
new DbContextOptionsBuilder<AppDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options);
}