using Health.Domain.Entities; using Health.Domain.Enums; using Health.Application.Notifications; using System.Security.Cryptography; using System.Text; namespace Health.Application.HealthRecords; public sealed class HealthRecordService( IHealthRecordRepository records, IUserNotificationProducer? notifications = null) : IHealthRecordService { private readonly IHealthRecordRepository _records = records; private readonly IUserNotificationProducer? _notifications = notifications; public async Task> GetRecordsAsync(Guid userId, string? type, int? days, CancellationToken ct) { HealthMetricType? metricType = null; if (!string.IsNullOrEmpty(type) && Enum.TryParse(type, ignoreCase: true, out var parsed)) metricType = parsed; var recordedAfter = days.HasValue ? DateTime.UtcNow.AddDays(-days.Value) : (DateTime?)null; var result = await _records.ListAsync(userId, metricType, recordedAfter, 100, ct); return result.Select(ToDto).ToList(); } public async Task CreateAsync(Guid userId, HealthRecordUpsertRequest request, CancellationToken ct) { HealthRecordRules.Validate(request); var record = new HealthRecord { Id = Guid.NewGuid(), UserId = userId, MetricType = request.Type, Systolic = request.Systolic, Diastolic = request.Diastolic, Value = request.Value, Unit = request.Unit, Source = request.Source, RecordedAt = request.RecordedAt ?? DateTime.UtcNow, CreatedAt = DateTime.UtcNow, IsAbnormal = HealthRecordRules.CheckAbnormal(request), }; await _records.AddAsync(record, ct); await _records.SaveChangesAsync(ct); await EnqueueAbnormalNotificationAsync(record, ct); return record.Id; } public async Task> CreateManyAsync( Guid userId, IReadOnlyList requests, CancellationToken ct) { if (requests.Count == 0) throw new ArgumentException("至少需要一条健康记录", nameof(requests)); foreach (var request in requests) HealthRecordRules.Validate(request); var now = DateTime.UtcNow; var records = requests.Select(request => new HealthRecord { Id = Guid.NewGuid(), UserId = userId, MetricType = request.Type, Systolic = request.Systolic, Diastolic = request.Diastolic, Value = request.Value, Unit = request.Unit, Source = request.Source, RecordedAt = request.RecordedAt ?? now, CreatedAt = now, IsAbnormal = HealthRecordRules.CheckAbnormal(request), }).ToList(); foreach (var record in records) await _records.AddAsync(record, ct); await _records.SaveChangesAsync(ct); foreach (var record in records) await EnqueueAbnormalNotificationAsync(record, ct); return records.Select(record => record.Id).ToList(); } public async Task UpdateAsync(Guid userId, Guid id, HealthRecordUpsertRequest request, CancellationToken ct) { var record = await _records.GetOwnedAsync(userId, id, ct); if (record == null) return false; HealthRecordRules.Validate(request); record.MetricType = request.Type; record.Systolic = request.Systolic; record.Diastolic = request.Diastolic; record.Value = request.Value; record.Unit = request.Unit; record.Source = request.Source; record.RecordedAt = request.RecordedAt ?? record.RecordedAt; record.IsAbnormal = HealthRecordRules.CheckAbnormal(request); await _records.SaveChangesAsync(ct); await EnqueueAbnormalNotificationAsync(record, ct); return true; } public async Task DeleteAsync(Guid userId, Guid id, CancellationToken ct) { var record = await _records.GetOwnedAsync(userId, id, ct); if (record == null) return false; _records.Delete(record); await _records.SaveChangesAsync(ct); return true; } public async Task> GetLatestAsync(Guid userId, CancellationToken ct) { var types = new[] { HealthMetricType.BloodPressure, HealthMetricType.HeartRate, HealthMetricType.Glucose, HealthMetricType.SpO2, HealthMetricType.Weight }; var result = new Dictionary(); foreach (var type in types) { var latest = await _records.GetLatestByTypeAsync(userId, type, ct); result[type.ToString()] = latest == null ? null : new { latest.Systolic, latest.Diastolic, latest.Value, latest.Unit, latest.IsAbnormal, latest.RecordedAt }; } return result; } public async Task> GetTrendAsync(Guid userId, HealthMetricType type, int period, CancellationToken ct) { var days = period switch { 7 => 7, 30 => 30, 90 => 90, 365 => 365, _ => 7 }; var records = await _records.GetTrendAsync(userId, type, DateTime.UtcNow.AddDays(-days), ct); return records.Select(r => new { r.Id, r.Systolic, r.Diastolic, r.Value, r.Unit, r.IsAbnormal, r.RecordedAt }).Cast().ToList(); } public static HealthRecordDto ToDto(HealthRecord record) => new( record.Id, record.MetricType.ToString(), record.Systolic, record.Diastolic, record.Value, record.Unit, record.Source.ToString(), record.IsAbnormal, record.RecordedAt); private async Task EnqueueAbnormalNotificationAsync(HealthRecord record, CancellationToken ct) { if (!record.IsAbnormal || _notifications == null) return; var (title, message, severity, target) = record.MetricType switch { HealthMetricType.BloodPressure when record.Systolic >= 140 || record.Diastolic >= 90 => ("血压偏高提醒", $"本次血压为 {record.Systolic}/{record.Diastolic} mmHg,高于参考范围。建议休息后复测;如伴明显不适,请及时就医。", "warning", "blood_pressure"), HealthMetricType.BloodPressure => ("血压偏低提醒", $"本次血压为 {record.Systolic}/{record.Diastolic} mmHg,低于参考范围。请留意头晕、乏力等不适,必要时及时就医。", "warning", "blood_pressure"), HealthMetricType.HeartRate when record.Value > 100 => ("心率偏高提醒", $"本次心率为 {record.Value:0.#} 次/分,高于参考范围。建议安静休息后复测。", "warning", "heart_rate"), HealthMetricType.HeartRate => ("心率偏低提醒", $"本次心率为 {record.Value:0.#} 次/分,低于参考范围。如伴明显不适,请及时就医。", "warning", "heart_rate"), HealthMetricType.Glucose when record.Value > 6.1m => ("血糖偏高提醒", $"本次血糖为 {record.Value:0.#} mmol/L,高于参考范围。请结合测量时段并按计划复测。", "warning", "glucose"), HealthMetricType.Glucose => ("血糖偏低提醒", $"本次血糖为 {record.Value:0.#} mmol/L,低于参考范围。请及时关注身体状况。", "critical", "glucose"), HealthMetricType.SpO2 => ("血氧偏低提醒", $"本次血氧为 {record.Value:0.#}%,低于参考范围。建议立即复测;如持续偏低或伴呼吸不适,请及时就医。", "critical", "spo2"), _ => ("健康指标提醒", "检测到一项健康指标超出参考范围,请查看详情。", "warning", "") }; var window = DateTime.UtcNow.Ticks / TimeSpan.FromMinutes(30).Ticks; var sourceId = DeterministicGuid($"{record.UserId}:{record.MetricType}:{severity}:{window}"); await _notifications.EnqueueAsync( record.UserId, sourceId, new NotificationMessage("HealthMetricAlert", title, message, severity, "health", target), ct); } private static Guid DeterministicGuid(string value) { var hash = SHA256.HashData(Encoding.UTF8.GetBytes(value)); return new Guid(hash.AsSpan(0, 16)); } }