using Health.Domain.Entities; using Health.Domain.Enums; namespace Health.Application.Reports; public sealed class ReportService( IReportRepository reports, IReportFileStorage fileStorage, IReportAnalysisQueue queue) : IReportService { private const long MaxReportFileBytes = 20 * 1024 * 1024; private static readonly HashSet AllowedExtensions = new(StringComparer.OrdinalIgnoreCase) { ".jpg", ".jpeg", ".png", ".webp", ".pdf" }; private readonly IReportRepository _reports = reports; private readonly IReportFileStorage _fileStorage = fileStorage; private readonly IReportAnalysisQueue _queue = queue; public async Task> GetReportsAsync(Guid userId, CancellationToken ct) { var result = await _reports.ListAsync(userId, ct); return result.Select(ToDto).ToList(); } public async Task GetReportAsync(Guid userId, Guid reportId, CancellationToken ct) { var report = await _reports.GetOwnedAsync(userId, reportId, ct); return report == null ? null : ToDto(report); } public async Task UploadReportAsync(Guid userId, ReportUploadFile file, CancellationToken ct) { if (file.Length <= 0) return Fail(400, "未上传文件"); if (file.Length > MaxReportFileBytes) return Fail(400, "报告文件不能超过 20MB,请压缩后重新上传"); var ext = Path.GetExtension(file.FileName).ToLowerInvariant(); if (!AllowedExtensions.Contains(ext)) return Fail(400, "目前支持上传 JPG、PNG、WEBP 图片或 PDF 报告"); var storedFile = await _fileStorage.SaveAsync(file, ext, ct); var report = new Report { Id = Guid.NewGuid(), UserId = userId, FileUrl = storedFile.FileUrl, FileType = ext == ".pdf" ? ReportFileType.Pdf : ReportFileType.Image, Category = ReportCategory.Other, Status = ReportStatus.Analyzing, AiSummary = null, AiIndicators = null, CreatedAt = DateTime.UtcNow, }; await _reports.AddAsync(report, ct); await _queue.EnqueueAsync(new ReportAnalysisJob(Guid.NewGuid(), report.Id, storedFile.FilePath), ct); return new ReportUploadResult(true, 0, "报告已上传,AI 正在分析中", ToDto(report)); } public async Task DeleteReportAsync(Guid userId, Guid reportId, CancellationToken ct) { var report = await _reports.GetOwnedAsync(userId, reportId, ct); if (report == null) return false; var filePath = _fileStorage.GetLocalFilePath(report.FileUrl); if (_fileStorage.Exists(filePath)) _fileStorage.Delete(filePath); _reports.Delete(report); await _reports.SaveChangesAsync(ct); return true; } public async Task ReanalyzeReportAsync(Guid userId, Guid reportId, CancellationToken ct) { var report = await _reports.GetOwnedAsync(userId, reportId, ct); if (report == null) return false; var filePath = _fileStorage.GetLocalFilePath(report.FileUrl); if (!_fileStorage.Exists(filePath)) return false; report.Status = ReportStatus.Analyzing; report.AiSummary = null; report.AiIndicators = null; await _queue.EnqueueAsync(new ReportAnalysisJob(Guid.NewGuid(), report.Id, filePath), ct); return true; } public static ReportDto ToDto(Report report) => new( report.Id, report.UserId, $"/api/reports/{report.Id}/file", report.FileType.ToString(), report.Category.ToString(), report.Status.ToString(), ToAiStatus(report), report.Status == ReportStatus.DoctorReviewed ? "Reviewed" : "Pending", report.Severity, report.AiSummary, report.AiIndicators, report.DoctorComment, report.DoctorRecommendation, report.DoctorName, report.ReviewedAt, report.CreatedAt); private static string ToAiStatus(Report report) => report.Status switch { ReportStatus.Analyzing => "Analyzing", ReportStatus.AnalysisFailed => "Failed", _ when string.IsNullOrWhiteSpace(report.AiSummary) => "Analyzing", _ => "Succeeded" }; private static ReportUploadResult Fail(int code, string message) => new(false, code, message, null); }