feat: 后端架构重构 — Endpoint→Service→Repository分层 + AI确认机制 + 异步任务持久化
- 核心业务拆分为 Endpoint → Application Service → Repository 三层 - AI写入操作必须用户确认后才写库(确认卡片机制) - 报告/饮食/用药分析改为持久化任务队列(原子领取/重试/重启恢复) - 运动计划修复: 连续真实日期替代周模板 - 用药提醒去重 + 通知Outbox预留 - 认证收拢到AuthService, 管理员收拢到AdminService - AI会话加用户归属校验防串号 - 提示词调整为患者视角 - 开发假数据已关闭 - 21/21测试通过, 0警告0错误
This commit is contained in:
76
backend/tests/Health.Tests/persistence_pipeline_tests.cs
Normal file
76
backend/tests/Health.Tests/persistence_pipeline_tests.cs
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user