feat: 应用内通知系统 + 结构化手术史/用药等相关改动
- 新增用户通知 outbox 流水线(EfUserNotificationPipeline)与后台投递 worker - 通知中心页面及前端通知服务接入 - 健康指标异常、用药/运动提醒等事件统一产出站内通知 - 健康档案结构化手术史、用药提醒扫描、医生/用户端点等配套调整 - AppDbContext 注册通知相关实体
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
using Health.Application.Medications;
|
||||
using Health.Domain.Entities;
|
||||
using Health.Domain.Enums;
|
||||
using Health.Infrastructure.AI;
|
||||
using Health.Infrastructure.Data;
|
||||
using Health.Infrastructure.Data.Records;
|
||||
using Health.Infrastructure.Medications;
|
||||
using Health.Infrastructure.Notifications;
|
||||
using Health.Infrastructure.Reports;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
@@ -57,6 +62,112 @@ public sealed class PersistencePipelineTests
|
||||
Assert.Equal(2000, task.LastError!.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReportTask_OnlyEnqueuesFailureNotificationAtFinalAttempt()
|
||||
{
|
||||
await using var db = CreateDbContext();
|
||||
var userId = Guid.NewGuid();
|
||||
var report = new Report
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = userId,
|
||||
FileUrl = "report.jpg",
|
||||
Status = ReportStatus.Analyzing,
|
||||
};
|
||||
var task = NewReportTask(attempts: 2);
|
||||
task.ReportId = report.Id;
|
||||
db.Reports.Add(report);
|
||||
db.ReportAnalysisTasks.Add(task);
|
||||
await db.SaveChangesAsync();
|
||||
var queue = new EfReportAnalysisQueue(db, new EfUserNotificationProducer(db));
|
||||
|
||||
await queue.RetryAsync(task.Id, "temporary failure", CancellationToken.None);
|
||||
Assert.Empty(db.NotificationOutbox);
|
||||
|
||||
task.Status = "Processing";
|
||||
task.Attempts = 3;
|
||||
await db.SaveChangesAsync();
|
||||
await queue.RetryAsync(task.Id, "final failure", CancellationToken.None);
|
||||
|
||||
var notification = Assert.Single(db.NotificationOutbox);
|
||||
Assert.Equal(task.Id, notification.SourceTaskId);
|
||||
Assert.Equal("ReportAnalysisFailed", notification.Type);
|
||||
Assert.Equal(userId, notification.UserId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MedicationReminder_MorningDoseDoesNotSuppressEveningDose()
|
||||
{
|
||||
await using var db = CreateDbContext();
|
||||
var userId = Guid.NewGuid();
|
||||
var medicationId = Guid.NewGuid();
|
||||
var scanTimeUtc = new DateTime(2026, 6, 20, 12, 2, 0, DateTimeKind.Utc);
|
||||
db.Medications.Add(new Medication
|
||||
{
|
||||
Id = medicationId,
|
||||
UserId = userId,
|
||||
Name = "测试药物",
|
||||
Frequency = MedicationFrequency.TwiceDaily,
|
||||
TimeOfDay = [new TimeOnly(8, 0), new TimeOnly(20, 0)],
|
||||
StartDate = new DateOnly(2026, 6, 20),
|
||||
IsActive = true,
|
||||
});
|
||||
db.MedicationLogs.Add(new MedicationLog
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = userId,
|
||||
MedicationId = medicationId,
|
||||
Status = MedicationLogStatus.Taken,
|
||||
ScheduledTime = new TimeOnly(8, 0),
|
||||
ConfirmedAt = new DateTime(2026, 6, 20, 0, 1, 0, DateTimeKind.Utc),
|
||||
CreatedAt = new DateTime(2026, 6, 20, 0, 1, 0, DateTimeKind.Utc),
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
var scanner = new MedicationReminderScanner(new EfMedicationRepository(db));
|
||||
|
||||
var tasks = await scanner.ScanAsync(scanTimeUtc, CancellationToken.None);
|
||||
|
||||
var reminder = Assert.Single(tasks);
|
||||
Assert.Equal(new TimeOnly(20, 0), reminder.ScheduledTime);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MedicationReminder_ConfirmedDoseIsNotRemindedAgain()
|
||||
{
|
||||
await using var db = CreateDbContext();
|
||||
var userId = Guid.NewGuid();
|
||||
var medicationId = Guid.NewGuid();
|
||||
var confirmedAt = new DateTime(2026, 6, 20, 12, 0, 0, DateTimeKind.Utc);
|
||||
db.Medications.Add(new Medication
|
||||
{
|
||||
Id = medicationId,
|
||||
UserId = userId,
|
||||
Name = "测试药物",
|
||||
Frequency = MedicationFrequency.Daily,
|
||||
TimeOfDay = [new TimeOnly(20, 0)],
|
||||
StartDate = new DateOnly(2026, 6, 20),
|
||||
IsActive = true,
|
||||
});
|
||||
db.MedicationLogs.Add(new MedicationLog
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = userId,
|
||||
MedicationId = medicationId,
|
||||
Status = MedicationLogStatus.Taken,
|
||||
ScheduledTime = new TimeOnly(20, 0),
|
||||
ConfirmedAt = confirmedAt,
|
||||
CreatedAt = confirmedAt,
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
var scanner = new MedicationReminderScanner(new EfMedicationRepository(db));
|
||||
|
||||
var tasks = await scanner.ScanAsync(
|
||||
new DateTime(2026, 6, 20, 12, 2, 0, DateTimeKind.Utc),
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.Empty(tasks);
|
||||
}
|
||||
|
||||
private static ReportAnalysisTaskRecord NewReportTask(int attempts) => new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
|
||||
Reference in New Issue
Block a user