feat: 应用内通知系统 + 结构化手术史/用药等相关改动

- 新增用户通知 outbox 流水线(EfUserNotificationPipeline)与后台投递 worker
- 通知中心页面及前端通知服务接入
- 健康指标异常、用药/运动提醒等事件统一产出站内通知
- 健康档案结构化手术史、用药提醒扫描、医生/用户端点等配套调整
- AppDbContext 注册通知相关实体
This commit is contained in:
MingNian
2026-06-21 21:06:29 +08:00
parent b57d0d16f4
commit 13714d9ed8
34 changed files with 1541 additions and 200 deletions

View File

@@ -2,12 +2,22 @@ using Health.Domain.Entities;
namespace Health.Application.HealthArchives;
public sealed record HealthArchiveSurgeryDto(
Guid Id,
string Type,
DateOnly? Date);
public sealed record HealthArchiveSurgeryInput(
string Type,
DateOnly? Date);
public sealed record HealthArchiveDto(
Guid Id,
Guid UserId,
string? Diagnosis,
string? SurgeryType,
DateOnly? SurgeryDate,
IReadOnlyList<HealthArchiveSurgeryDto> Surgeries,
IReadOnlyList<string> Allergies,
IReadOnlyList<string> DietRestrictions,
IReadOnlyList<string> ChronicDiseases,
@@ -21,7 +31,8 @@ public sealed record HealthArchiveUpdateRequest(
IReadOnlyList<string>? Allergies,
IReadOnlyList<string>? DietRestrictions,
IReadOnlyList<string>? ChronicDiseases,
string? FamilyHistory);
string? FamilyHistory,
IReadOnlyList<HealthArchiveSurgeryInput>? Surgeries = null);
public interface IHealthArchiveService
{

View File

@@ -42,7 +42,8 @@ public sealed class HealthArchiveService(IHealthArchiveRepository archives) : IH
"update_diagnosis" => request with
{
SurgeryType = null, SurgeryDate = null, Allergies = null,
DietRestrictions = null, ChronicDiseases = null, FamilyHistory = null
DietRestrictions = null, ChronicDiseases = null, FamilyHistory = null,
Surgeries = null
},
"update_surgery" => request with
{
@@ -77,8 +78,41 @@ public sealed class HealthArchiveService(IHealthArchiveRepository archives) : IH
private static void Apply(HealthArchive archive, HealthArchiveUpdateRequest request)
{
if (request.Diagnosis != null) archive.Diagnosis = request.Diagnosis;
if (request.SurgeryType != null) archive.SurgeryType = request.SurgeryType;
if (request.SurgeryDate.HasValue) archive.SurgeryDate = request.SurgeryDate.Value;
if (request.Surgeries != null)
{
archive.Surgeries.Clear();
var items = request.Surgeries
.Where(s => !string.IsNullOrWhiteSpace(s.Type))
.Select((s, index) => new HealthArchiveSurgery
{
Id = Guid.NewGuid(),
HealthArchiveId = archive.Id,
Type = s.Type.Trim(),
Date = s.Date,
SortOrder = index,
})
.ToList();
foreach (var item in items) archive.Surgeries.Add(item);
SyncLegacySurgeryFields(archive);
}
else if (request.SurgeryType != null)
{
SeedLegacySurgeryIfNeeded(archive);
var type = request.SurgeryType.Trim();
if (type.Length > 0 && !archive.Surgeries.Any(s =>
s.Type == type && s.Date == request.SurgeryDate))
{
archive.Surgeries.Add(new HealthArchiveSurgery
{
Id = Guid.NewGuid(),
HealthArchiveId = archive.Id,
Type = type,
Date = request.SurgeryDate,
SortOrder = archive.Surgeries.Count,
});
}
SyncLegacySurgeryFields(archive);
}
if (request.Allergies != null) archive.Allergies = request.Allergies.ToList();
if (request.DietRestrictions != null) archive.DietRestrictions = request.DietRestrictions.ToList();
if (request.ChronicDiseases != null) archive.ChronicDiseases = request.ChronicDiseases.ToList();
@@ -91,6 +125,7 @@ public sealed class HealthArchiveService(IHealthArchiveRepository archives) : IH
archive.Diagnosis,
archive.SurgeryType,
SurgeryDate = archive.SurgeryDate?.ToString("yyyy-MM-dd"),
surgeries = archive.Surgeries.Select(s => new { s.Type, date = s.Date?.ToString("yyyy-MM-dd") }),
archive.Allergies,
archive.DietRestrictions,
archive.ChronicDiseases,
@@ -103,9 +138,41 @@ public sealed class HealthArchiveService(IHealthArchiveRepository archives) : IH
archive.Diagnosis,
archive.SurgeryType,
archive.SurgeryDate,
GetSurgeries(archive),
archive.Allergies,
archive.DietRestrictions,
archive.ChronicDiseases,
archive.FamilyHistory,
archive.UpdatedAt);
private static IReadOnlyList<HealthArchiveSurgeryDto> GetSurgeries(HealthArchive archive)
{
var surgeries = archive.Surgeries
.OrderBy(s => s.SortOrder)
.Select(s => new HealthArchiveSurgeryDto(s.Id, s.Type, s.Date))
.ToList();
if (surgeries.Count == 0 && !string.IsNullOrWhiteSpace(archive.SurgeryType))
surgeries.Add(new HealthArchiveSurgeryDto(Guid.Empty, archive.SurgeryType, archive.SurgeryDate));
return surgeries;
}
private static void SyncLegacySurgeryFields(HealthArchive archive)
{
var first = archive.Surgeries.OrderBy(s => s.SortOrder).FirstOrDefault();
archive.SurgeryType = first?.Type;
archive.SurgeryDate = first?.Date;
}
private static void SeedLegacySurgeryIfNeeded(HealthArchive archive)
{
if (archive.Surgeries.Count > 0 || string.IsNullOrWhiteSpace(archive.SurgeryType)) return;
archive.Surgeries.Add(new HealthArchiveSurgery
{
Id = Guid.NewGuid(),
HealthArchiveId = archive.Id,
Type = archive.SurgeryType,
Date = archive.SurgeryDate,
SortOrder = 0,
});
}
}