- 核心业务拆分为 Endpoint → Application Service → Repository 三层 - AI写入操作必须用户确认后才写库(确认卡片机制) - 报告/饮食/用药分析改为持久化任务队列(原子领取/重试/重启恢复) - 运动计划修复: 连续真实日期替代周模板 - 用药提醒去重 + 通知Outbox预留 - 认证收拢到AuthService, 管理员收拢到AdminService - AI会话加用户归属校验防串号 - 提示词调整为患者视角 - 开发假数据已关闭 - 21/21测试通过, 0警告0错误
99 lines
3.7 KiB
C#
99 lines
3.7 KiB
C#
using Health.Application.AI;
|
|
using Health.Infrastructure.Data.Records;
|
|
|
|
namespace Health.Infrastructure.AI;
|
|
|
|
public sealed class EfAiWriteConfirmationStore(AppDbContext db) : IAiWriteConfirmationStore
|
|
{
|
|
private readonly AppDbContext _db = db;
|
|
|
|
public async Task<PendingAiWriteCommand> CreateAsync(
|
|
Guid userId,
|
|
string toolName,
|
|
string arguments,
|
|
TimeSpan lifetime,
|
|
CancellationToken ct)
|
|
{
|
|
var now = DateTime.UtcNow;
|
|
if (IsInMemory())
|
|
{
|
|
var expired = await _db.AiWriteCommands.Where(x => x.Status == "Pending" && x.ExpiresAt <= now).ToListAsync(ct);
|
|
foreach (var item in expired) { item.Status = "Expired"; item.UpdatedAt = now; }
|
|
}
|
|
else
|
|
{
|
|
await _db.AiWriteCommands
|
|
.Where(x => x.Status == "Pending" && x.ExpiresAt <= now)
|
|
.ExecuteUpdateAsync(setters => setters
|
|
.SetProperty(x => x.Status, "Expired")
|
|
.SetProperty(x => x.UpdatedAt, now), ct);
|
|
}
|
|
|
|
var command = new AiWriteCommandRecord
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
UserId = userId,
|
|
ToolName = toolName,
|
|
Arguments = arguments,
|
|
Status = "Pending",
|
|
ExpiresAt = now.Add(lifetime),
|
|
CreatedAt = now,
|
|
UpdatedAt = now,
|
|
};
|
|
await _db.AiWriteCommands.AddAsync(command, ct);
|
|
await _db.SaveChangesAsync(ct);
|
|
return ToCommand(command);
|
|
}
|
|
|
|
public async Task<PendingAiWriteCommand?> TakeAsync(Guid commandId, Guid userId, CancellationToken ct)
|
|
{
|
|
var now = DateTime.UtcNow;
|
|
if (IsInMemory())
|
|
{
|
|
var tracked = await _db.AiWriteCommands.FirstOrDefaultAsync(
|
|
x => x.Id == commandId && x.UserId == userId && x.Status == "Pending" && x.ExpiresAt > now, ct);
|
|
if (tracked == null) return null;
|
|
tracked.Status = "Processing";
|
|
tracked.UpdatedAt = now;
|
|
await _db.SaveChangesAsync(ct);
|
|
}
|
|
else
|
|
{
|
|
var updated = await _db.AiWriteCommands
|
|
.Where(x => x.Id == commandId && x.UserId == userId && x.Status == "Pending" && x.ExpiresAt > now)
|
|
.ExecuteUpdateAsync(setters => setters
|
|
.SetProperty(x => x.Status, "Processing")
|
|
.SetProperty(x => x.UpdatedAt, now), ct);
|
|
if (updated == 0) return null;
|
|
}
|
|
|
|
var record = await _db.AiWriteCommands.AsNoTracking().FirstAsync(x => x.Id == commandId, ct);
|
|
return ToCommand(record);
|
|
}
|
|
|
|
public async Task CompleteAsync(PendingAiWriteCommand command, CancellationToken ct)
|
|
{
|
|
if (IsInMemory())
|
|
{
|
|
var tracked = await _db.AiWriteCommands.FirstOrDefaultAsync(
|
|
x => x.Id == command.Id && x.UserId == command.UserId && x.Status == "Processing", ct);
|
|
if (tracked != null) { tracked.Status = "Completed"; tracked.UpdatedAt = DateTime.UtcNow; await _db.SaveChangesAsync(ct); }
|
|
return;
|
|
}
|
|
await _db.AiWriteCommands
|
|
.Where(x => x.Id == command.Id && x.UserId == command.UserId && x.Status == "Processing")
|
|
.ExecuteUpdateAsync(setters => setters
|
|
.SetProperty(x => x.Status, "Completed")
|
|
.SetProperty(x => x.UpdatedAt, DateTime.UtcNow), ct);
|
|
}
|
|
|
|
private bool IsInMemory() => _db.Database.ProviderName == "Microsoft.EntityFrameworkCore.InMemory";
|
|
|
|
private static PendingAiWriteCommand ToCommand(AiWriteCommandRecord record) => new(
|
|
record.Id,
|
|
record.UserId,
|
|
record.ToolName,
|
|
record.Arguments,
|
|
record.ExpiresAt);
|
|
}
|