diff --git a/IRaCIS.Core.Application/Service/Common/TrialImageDownloadService.cs b/IRaCIS.Core.Application/Service/Common/TrialImageDownloadService.cs index d06a9d8f3..f43b076b9 100644 --- a/IRaCIS.Core.Application/Service/Common/TrialImageDownloadService.cs +++ b/IRaCIS.Core.Application/Service/Common/TrialImageDownloadService.cs @@ -1,22 +1,35 @@ -using DocumentFormat.OpenXml.EMMA; +using Aliyun.OSS; +using DocumentFormat.OpenXml.EMMA; +using DocumentFormat.OpenXml.Office.CustomUI; +using DocumentFormat.OpenXml.Office2010.Excel; +using DocumentFormat.OpenXml.Office2013.Drawing.ChartStyle; using FellowOakDicom; using FellowOakDicom.Imaging; using FellowOakDicom.Imaging.Render; using FellowOakDicom.IO.Buffer; using IRaCIS.Core.Application.Helper; +using IRaCIS.Core.Application.ViewModel; +using IRaCIS.Core.Domain.Models; +using IRaCIS.Core.Infrastructure; using MassTransit; +using Medallion.Threading; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; +using MiniExcelLibs; using SharpCompress.Common; using System; using System.Collections.Generic; +using System.Data; +using System.Diagnostics; using System.Linq; +using System.Runtime.InteropServices; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Web; using static IRaCIS.Core.Domain.Share.StaticData; +using static Microsoft.EntityFrameworkCore.DbLoggerCategory; namespace IRaCIS.Core.Application.Service { @@ -27,7 +40,10 @@ namespace IRaCIS.Core.Application.Service /// /// [ApiExplorerSettings(GroupName = "Common")] - public class TrialImageDownloadService(IRepository _trialRepository, IOSSService _oSSService, IWebHostEnvironment _hostEnvironment) : BaseService + public class TrialImageDownloadService(IRepository _trialRepository, IOSSService _oSSService, IWebHostEnvironment _hostEnvironment, + IRepository _studyRepository, + IRepository _seriesRepository, + IRepository _instanceRepository) : BaseService { @@ -243,11 +259,12 @@ namespace IRaCIS.Core.Application.Service /// [HttpGet] [AllowAnonymous] - public async Task DownloadAndUploadTrialData(Guid trialId, [FromServices] IRepository _instanceRepository, + public async Task DownloadAndUploadTrialData(Guid trialId, + [FromServices] IRepository _instanceRepository, [FromServices] IRepository _studyRepository, [FromServices] IRepository _seriesRepository) { - var list = await _instanceRepository.Where(t => t.TrialId == trialId && t.SubjectVisitId == Guid.Parse("01000000-0a00-0242-bd20-08dcce543ded" ) && t.DicomStudy.ModalityForEdit == "IVUS") + var list = await _instanceRepository.Where(t => t.TrialId == trialId && t.SubjectVisitId == Guid.Parse("01000000-0a00-0242-bd20-08dcce543ded") && t.DicomStudy.ModalityForEdit == "IVUS") .Select(t => new { t.SeriesId, t.StudyId, t.Id, t.Path }).ToListAsync(); int totalCount = list.Count; @@ -288,7 +305,7 @@ namespace IRaCIS.Core.Application.Service await _instanceRepository.BatchUpdateNoTrackingAsync(t => t.Id == item.Id, u => new DicomInstance() { - IsEncapsulated= syntax.IsEncapsulated, + IsEncapsulated = syntax.IsEncapsulated, TransferSytaxUID = dirInfo.TransferSytaxUID, SOPClassUID = dirInfo.SOPClassUID, MediaStorageSOPClassUID = dirInfo.MediaStorageSOPClassUID, @@ -334,6 +351,1483 @@ namespace IRaCIS.Core.Application.Service return ResponseOutput.Ok(); } - } + /// + /// 维护dir 需求新增的字段 + + /// + /// + /// + /// + /// + /// + public async Task TrialImageAddExtralField(Guid trialId, + [FromServices] IRepository _instanceRepository, + [FromServices] IRepository _studyRepository, + [FromServices] IRepository _seriesRepository) + { + // UPDATE DicomStudy + //SET DicomStudyDate = CONVERT(char(8), StudyTime, 112), --yyyyMMdd + // DicomStudyTime = REPLACE(CONVERT(char(8), StudyTime, 108), ':', ''); --HHmmss + // where DicomStudyDate = '' + + + //instance 找到传输语法为空的,然后分组 + var seriesList = _instanceRepository.Where(t => t.TrialId == trialId && t.TransferSytaxUID == "") + //按照序列 和 NumberOfFrames 分组 + .GroupBy(t => new { t.NumberOfFrames, t.SeriesId }) + // 每个分组 取数据最小的一条 + .Select(g => new { g.Key.SeriesId, g.Key.NumberOfFrames, g.OrderBy(t => t.FileSize).First().Path }).ToList(); + + foreach (var item in seriesList) + { + var stream = await _oSSService.GetStreamFromOSSAsync(item.Path); + + var dicomFile = DicomFile.Open(stream); + + var pixelData = DicomPixelData.Create(dicomFile.Dataset); + + //获取像素是否为封装形式 + var syntax = dicomFile.Dataset.InternalTransferSyntax; + + //读取需要维护的值 + var transferSyntaxUID = dicomFile.FileMetaInfo.GetSingleValueOrDefault(DicomTag.TransferSyntaxUID, string.Empty); + var mediaStorageSOPClassUID = dicomFile.FileMetaInfo.GetSingleValueOrDefault(DicomTag.MediaStorageSOPClassUID, string.Empty); + var mediaStorageSOPInstanceUID = dicomFile.FileMetaInfo.GetSingleValueOrDefault(DicomTag.MediaStorageSOPInstanceUID, string.Empty); + var sOPClassUID = dicomFile.Dataset.GetSingleValueOrDefault(DicomTag.SOPClassUID, string.Empty); + + //维护序列层级四个字段 后再用sql 维护study series 时间拆分 和 MediaStorageSOPInstanceUID + await _instanceRepository.BatchUpdateNoTrackingAsync(t => t.SeriesId == item.SeriesId, t => new DicomInstance() + { + IsEncapsulated = syntax.IsEncapsulated, + TransferSytaxUID = transferSyntaxUID, + MediaStorageSOPClassUID = mediaStorageSOPClassUID, + SOPClassUID = sOPClassUID, + }); + + } + + return ResponseOutput.Ok(); + + } + + + /// + /// 下载已经删除的影像 + /// + /// + /// + [HttpPost] + [AllowAnonymous] + public async Task DownloadDeleteTrialImage(Guid trialId) + { + trialId = Guid.Parse("01000000-ac13-0242-6397-08dcd2d2a091"); + + var downloadInfo = _trialRepository.Where(t => t.Id == trialId, ignoreQueryFilters: true).Select(t => new + { + t.ResearchProgramNo, + + VisitList = t.SubjectVisitList + .Select(sv => new + { + TrialSiteCode = sv.TrialSite.TrialSiteCode, + SubjectCode = sv.Subject.Code, + VisitName = sv.VisitName, + StudyList = sv.StudyList.Select(u => new + { + u.PatientId, + u.StudyTime, + u.StudyCode, + + SeriesList = u.SeriesList.Select(z => new + { + z.Modality, + z.SeriesNumber, + + InstancePathList = z.DicomInstanceList.Where(t => t.IsDeleted == true || t.DicomSerie.IsDeleted == true || t.IsReading == false || t.DicomSerie.IsReading == false).Select(k => new + { + k.Path, + k.FileSize + }).ToList() + }) + + }).ToList() + }).ToList() + + }).FirstOrDefault(); + + + var filesizes = downloadInfo.VisitList.SelectMany(t => t.StudyList).SelectMany(t => t.SeriesList).SelectMany(t => t.InstancePathList).Sum(t => t.FileSize); + var count2 = downloadInfo.VisitList.SelectMany(t => t.StudyList).SelectMany(t => t.SeriesList).SelectMany(t => t.InstancePathList).Count(); + + Console.WriteLine($"下载总数量:{count2},总大小{filesizes}"); + + if (downloadInfo != null) + { + var downloadJobs = new List<(string Path, Func Job)>(); + + var rootFolder = @"E:\DownloadImage"; + + //var rootFolder = FileStoreHelper.GetDonwnloadImageFolder(_hostEnvironment); + + // 获取无效字符(系统定义的) + string invalidChars = new string(Path.GetInvalidFileNameChars()) + new string(Path.GetInvalidPathChars()); + + // 用正则表达式替换所有非法字符为下划线或空字符 + string pattern = $"[{Regex.Escape(invalidChars)}]"; + + var regexNo = Regex.Replace(downloadInfo.ResearchProgramNo, pattern, "_"); + + // 创建一个临时文件夹来存放文件 + string trialFolderPath = Path.Combine(rootFolder, $"{regexNo}_{NewId.NextGuid()}"); + Directory.CreateDirectory(trialFolderPath); + + foreach (var visitItem in downloadInfo.VisitList) + { + if (visitItem.StudyList.Count() == 0) + { + continue; + } + + #region 处理 中心,受试者dicom non-dicom 文件夹层级 + + //var siteFolderPath = Path.Combine(trialFolderPath, visitItem.TrialSiteCode); + //if (!Directory.Exists(siteFolderPath)) + //{ + // Directory.CreateDirectory(siteFolderPath); + //} + + #endregion + + + foreach (var studyInfo in visitItem.StudyList) + { + // 遍历 Series + foreach (var seriesInfo in studyInfo.SeriesList) + { + string studyDicomFolderPath = Path.Combine(trialFolderPath, $"{visitItem.SubjectCode}_{visitItem.VisitName}", $"{studyInfo.StudyCode}_{studyInfo.StudyTime?.ToString("yyyy-MM-dd")}", $"{seriesInfo.SeriesNumber}"); + + // 创建 影像 文件夹 + Directory.CreateDirectory(studyDicomFolderPath); + + // 遍历 InstancePathList + foreach (var instanceInfo in seriesInfo.InstancePathList) + { + // 复制文件到相应的文件夹 + string destinationPath = Path.Combine(studyDicomFolderPath, Path.GetFileName(instanceInfo.Path)); + + + //加入到下载任务里 + downloadJobs.Add((instanceInfo.Path, () => _oSSService.DownLoadFromOSSAsync(instanceInfo.Path, destinationPath))); + + //下载到当前目录 + //await _oSSService.DownLoadFromOSSAsync(instanceInfo.Path, destinationPath); + } + } + + + } + } + + #region 异步方式处理 + + int totalCount = downloadJobs.Count; + int downloadedCount = 0; + + // 在 trialFolderPath 下面放一个失败记录文件 + string failedLogPath = Path.Combine(trialFolderPath, "failed_downloads.txt"); + + // 确保文件存在(如果之前有就清空) + File.WriteAllText(failedLogPath, ""); + + foreach (var job in downloadJobs) + { + try + { + await job.Job(); + } + catch (Exception ex) + { + + + string errorMessage = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 下载失败: {job.Path}, 错误: {ex.Message}\r\n"; + + Console.WriteLine(errorMessage); + + await File.AppendAllTextAsync(failedLogPath, errorMessage); + } + + downloadedCount++; + + // 每处理50个,输出一次进度(或最后一个时也输出) + if (downloadedCount % 50 == 0 || downloadedCount == totalCount) + { + Console.WriteLine($"已下载 {downloadedCount} / {totalCount} 个文件,完成 {(downloadedCount * 100.0 / totalCount):F2}%"); + } + } + #endregion + + #region 多线程测试 + + //const int batchSize = 15; + //int totalCount = downloadJobs.Count; + //int downloadedCount = 0; + + //for (int i = 0; i < downloadJobs.Count; i += batchSize) + //{ + // var batch = downloadJobs.Skip(i).Take(batchSize).Select(job => job()); + + // await Task.WhenAll(batch); + + // downloadedCount += batch.Count(); + + // Console.WriteLine($"已下载 {downloadedCount} / {totalCount} 个文件,完成 {(downloadedCount * 100.0 / totalCount):F2}%"); + //} + #endregion + + } + + return ResponseOutput.Ok(); + + + } + + + [AllowAnonymous] + public async Task ReadDicomDataWriteDB([FromServices] IRepository _instanceRepository) + { + var testPath = @"E:\WXT001"; + var path = @"E:\WXT001"; + + var files = Directory.GetFiles(testPath, "*", SearchOption.AllDirectories) + // 只要没有后缀(Windows 显示类型是 .file) + .Where(f => string.IsNullOrEmpty(Path.GetExtension(f))) + .Where(f => Guid.TryParse(Path.GetFileNameWithoutExtension(f), out _)) + .ToList(); + + Console.WriteLine($"找到 {files.Count} 个 DICOM 文件"); + + int total = files.Count; + int processed = 0; + double lastPercent = 0; + + var options = new ParallelOptions { MaxDegreeOfParallelism = 12 }; + + // 输出文件路径 + var outputFile = Path.Combine(@"D:\dicomWrite", $"{Guid.NewGuid()}_dicom_info.txt"); + + var outputErrorFile = Path.Combine(@"D:\dicomWrite", $"{Guid.NewGuid()}_dicom_info_error.txt"); + + // 用并发安全的写法(锁保护) + var fileLock = new object(); + + foreach (var file in files) + { + try + { + var id = Guid.Parse(Path.GetFileNameWithoutExtension(file)); + var dicomFile = DicomFile.Open(file); + + var dataset = dicomFile.Dataset; + var fileMeta = dicomFile.FileMetaInfo; + var syntax = dataset.InternalTransferSyntax; + + + + //单位 设备 PatientId Visit 检查UId 帧数 + + var stationName = dataset.GetSingleValueOrDefault(DicomTag.StationName, string.Empty); + + var institutionName = dataset.GetSingleValueOrDefault(DicomTag.InstitutionName, string.Empty); + + var manufacturer = dataset.GetSingleValueOrDefault(DicomTag.Manufacturer, string.Empty); + + //PatientID TrialCode_SubjectCode + //var patientID = dataset.GetSingleValueOrDefault(DicomTag.PatientID, string.Empty); + + //SubjectCode + var clinicalTrialSubjectID = dataset.GetSingleValueOrDefault(DicomTag.ClinicalTrialSubjectID, string.Empty); + //访视visitNum + var clinicalTrialTimePointID = dataset.GetSingleValueOrDefault(DicomTag.ClinicalTrialTimePointID, string.Empty); + var studyInstanceUID = dataset.GetSingleValueOrDefault(DicomTag.StudyInstanceUID, string.Empty); + var seriesInstanceUID = dataset.GetSingleValueOrDefault(DicomTag.SeriesInstanceUID, string.Empty); + var sOPInstanceUID = dataset.GetSingleValueOrDefault(DicomTag.SOPInstanceUID, string.Empty); + + var numberOfFrames = dataset.GetSingleValueOrDefault(DicomTag.NumberOfFrames, 1); + + + // 传输语法 + var transferSyntaxUID = fileMeta.GetSingleValueOrDefault(DicomTag.TransferSyntaxUID, string.Empty); + var sOPClassUID = dataset.GetSingleValueOrDefault(DicomTag.SOPClassUID, string.Empty); + var mediaStorageSOPClassUID = fileMeta.GetSingleValueOrDefault(DicomTag.MediaStorageSOPClassUID, string.Empty); + + var mediaStorageSOPInstanceUID = fileMeta.GetSingleValueOrDefault(DicomTag.MediaStorageSOPInstanceUID, string.Empty); + + + // 拼接一行 CSV 格式 + var line = string.Join(",", + id, + stationName, + institutionName, + manufacturer, + clinicalTrialSubjectID, + clinicalTrialTimePointID, + studyInstanceUID, + seriesInstanceUID, + sOPInstanceUID, + numberOfFrames, + transferSyntaxUID, + sOPClassUID, + mediaStorageSOPClassUID, + mediaStorageSOPInstanceUID + + ); + + + await File.AppendAllTextAsync(outputFile, line + Environment.NewLine); + + await _instanceRepository.BatchUpdateNoTrackingAsync( + t => t.Id == id, + t => new DicomInstance + { + IsEncapsulated = syntax.IsEncapsulated, + TransferSytaxUID = transferSyntaxUID, + MediaStorageSOPClassUID = mediaStorageSOPClassUID, + SOPClassUID = sOPClassUID, + MediaStorageSOPInstanceUID = mediaStorageSOPInstanceUID + }, false); + } + catch (Exception ex) + { + var errorMsg = $"{DateTime.Now}❌ {file} 解析失败: {ex.Message}"; + Console.WriteLine(errorMsg); + + await File.AppendAllTextAsync(outputErrorFile, errorMsg + Environment.NewLine); + } + finally + { + var done = Interlocked.Increment(ref processed); + double percent = done * 100.0 / total; + + // 只在进度提升 >= 1% 时打印 + if (percent - lastPercent >= 5.0 || done == total) + { + lastPercent = percent; + Console.WriteLine($"{DateTime.Now} 进度: {done}/{total} ({percent:F2}%)"); + } + } + + } + + + + + + + return ResponseOutput.Ok(); + } + + #region 维护已经下载本地的数据 + + [AllowAnonymous] + public async Task ReadExcelData([FromServices] IRepository _instanceRepository) + { + var rows = await MiniExcel.QueryAsync(@"C:\Users\hang\Desktop\维护数据读取.xlsx"); + + rows = rows.Where(t => !string.IsNullOrEmpty(t.InstanceId.ToString())).ToList(); + + int total = rows.Count(); + int processed = 0; + double lastPercent = 0; + + var outputErrorFile = Path.Combine(@"D:\dicomWrite", $"{Guid.NewGuid()}_dicom_info_error.txt"); + + foreach (var item in rows) + { + + + try + { + await _instanceRepository.BatchUpdateNoTrackingAsync( + t => t.Id == item.InstanceId, + t => new DicomInstance + { + + IsEncapsulated = item.IsEncapsulated, + TransferSytaxUID = item.TransferSyntaxUID, + MediaStorageSOPClassUID = item.MediaStorageSOPClassUID, + SOPClassUID = item.SOPClassUID, + MediaStorageSOPInstanceUID = item.MediaStorageSOPInstanceUID + }, false); + } + catch (Exception ex) + { + + var errorMsg = $"{item.InstanceId} {DateTime.Now} 更新失败: {ex.Message}"; + Console.WriteLine(errorMsg); + + await File.AppendAllTextAsync(outputErrorFile, errorMsg + Environment.NewLine); + } + finally + { + processed++; + double percent = processed * 100.0 / total; + + // 每提升 5% 或完成时输出 + if (percent - lastPercent >= 2.0 || processed == total) + { + lastPercent = percent; + Console.WriteLine($"{DateTime.Now} 进度: {processed}/{total} ({percent:F2}%)"); + } + } + + } + + + + + return ResponseOutput.Ok(); + } + + public class DicomSOPInfo + { + public Guid InstanceId { get; set; } + + + public string TransferSyntaxUID { get; set; } + + public string SOPClassUID { get; set; } + + public string MediaStorageSOPClassUID { get; set; } + + public string MediaStorageSOPInstanceUID { get; set; } + + public bool IsEncapsulated => DicomTransferSyntax.Lookup(DicomUID.Parse(TransferSyntaxUID)).IsEncapsulated; + + + + } + + + [AllowAnonymous] + public async Task ReadExcelImageDataInstanceIsReading([FromServices] IRepository _instanceRepository, + [FromServices] IRepository _seriesRepository, + [FromServices] IRepository _studyRepository) + { + + var trialId = Guid.Parse("01000000-ac13-0242-6397-08dcd2d2a091"); + + var rows = await MiniExcel.QueryAsync(@"C:\Users\hang\Desktop\instanceReading.xlsx"); + + rows = rows.Where(t => !string.IsNullOrEmpty(t.SopInstanceUid) && t.SopInstanceUid.Length > 15).ToList(); + + var outputErrorFile = Path.Combine(@"D:\dicomWrite", $"{Guid.NewGuid()}_dicom_info_error.txt"); + + + //foreach (var batch in rows.Chunk(20)) + //{ + // var sopUids = batch.Select(x => x.SopInstanceUid).ToList(); + + // try + // { + // await _instanceRepository.BatchUpdateNoTrackingAsync( + // t => sopUids.Contains(t.SopInstanceUid) && t.TrialId == trialId, + // t => new DicomInstance + // { + // IsReading = true, + // IsDeleted = false + // }, false); + + + // await _seriesRepository.BatchUpdateNoTrackingAsync( + // t => t.DicomInstanceList.Any(t => sopUids.Contains(t.SopInstanceUid)) && t.TrialId == trialId, + // t => new DicomSeries + // { + // IsReading = true, + // IsDeleted = false + // }, false); + // } + // catch (Exception ex) + // { + // var errorMsg = $"{string.Join(",", sopUids)} {DateTime.Now} 批量更新失败: {ex.Message}"; + // Console.WriteLine(errorMsg); + // await File.AppendAllTextAsync(outputErrorFile, errorMsg + Environment.NewLine); + // } + //} + + //找到该项目的检查,实时统计数量,并且回更数据库 + + var studyList = _studyRepository.Where(t => t.TrialId == trialId && (t.SeriesCount != t.SeriesList.Count() || t.InstanceCount != t.InstanceList.Count())) + .Select(t => new + { + t.Id, + t.StudyCode, + DBSeriesCount = t.SeriesCount, + DBInstanceCount = t.InstanceCount, + + ActrualSeriesCount = t.SeriesList.Count(), + + ActrualInstanceCount = t.InstanceList.Count(), + + }).ToList(); + + + var seriesList = _seriesRepository.Where(t => t.TrialId == trialId && t.InstanceCount != t.DicomInstanceList.Count()) + .Select(t => new + { + SeriesId = t.Id, + t.DicomStudy.StudyCode, + DBInstanceCount = t.InstanceCount, + ActrualInstanceCount = t.DicomInstanceList.Count(), + + }).ToList(); + + + await File.AppendAllTextAsync(outputErrorFile, studyList.ToJsonStr() + Environment.NewLine); + + await File.AppendAllTextAsync(outputErrorFile, seriesList.ToJsonStr() + Environment.NewLine); + + return ResponseOutput.Ok(); + } + + public class DicomSOPInstanceInfo + { + public string SopInstanceUid { get; set; } + + + } + + #endregion + + + #region 通过Excel 读取未下载的,边下载边维护数据 + + [AllowAnonymous] + public async Task WriteNeedDealData([FromServices] IRepository _instanceRepository) + { + + #region 获取差集数据 + //var rows = await MiniExcel.QueryAsync(@"C:\Users\hang\Desktop\维护数据读取.xlsx"); + + //rows = rows.Where(t => !string.IsNullOrEmpty(t.InstanceId.ToString())).ToList(); + + //var allRows = await MiniExcel.QueryAsync(@"C:\Users\hang\Desktop\AllData.xlsx"); + + //allRows = allRows.Where(t => !string.IsNullOrEmpty(t.InstanceId.ToString())).ToList(); + + //var needDealRows = allRows.Where(t => !rows.Select(c => c.InstanceId).Contains(t.InstanceId)).ToList(); + + //var outputFile = Path.Combine(@"D:\dicomWrite", $"{Guid.NewGuid()}_dicom_info.txt"); + + //foreach (var item in needDealRows) + //{ + // var line = string.Join(",", item.InstanceId, item.Path); + + // await File.AppendAllTextAsync(outputFile, line + Environment.NewLine); + //} + + #endregion + + var folder = FileStoreHelper.GetIRaCISRootDataFolder(_hostEnvironment); + + var needDealRows = await MiniExcel.QueryAsync(Path.Combine(folder, "needDownload.xlsx")); + + needDealRows = needDealRows.Where(t => !string.IsNullOrEmpty(t.InstanceId.ToString())).ToList(); + + + + var outputFile = Path.Combine(folder, $"{Guid.NewGuid()}_dicom_info.txt"); + + var outputErrorFile = Path.Combine(folder, $"{Guid.NewGuid()}_dicom_info_error.txt"); + + int total = needDealRows.Count(); + + Console.WriteLine($"需要处理数量{total}"); + + int processed = 0; + double lastPercent = 0; + + foreach (var item in needDealRows) + { + + try + { + await using var stream = await _oSSService.GetStreamFromOSSAsync(item.Path); + + var dicomFile = DicomFile.Open(stream); + + var dataset = dicomFile.Dataset; + var fileMeta = dicomFile.FileMetaInfo; + + var pixelData = DicomPixelData.Create(dicomFile.Dataset); + + //获取像素是否为封装形式 + var syntax = dicomFile.Dataset.InternalTransferSyntax; + + var stationName = dataset.GetSingleValueOrDefault(DicomTag.StationName, string.Empty); + + var institutionName = dataset.GetSingleValueOrDefault(DicomTag.InstitutionName, string.Empty); + + var manufacturer = dataset.GetSingleValueOrDefault(DicomTag.Manufacturer, string.Empty); + + //PatientID TrialCode_SubjectCode + //var patientID = dataset.GetSingleValueOrDefault(DicomTag.PatientID, string.Empty); + + //SubjectCode + var clinicalTrialSubjectID = dataset.GetSingleValueOrDefault(DicomTag.ClinicalTrialSubjectID, string.Empty); + //访视visitNum + var clinicalTrialTimePointID = dataset.GetSingleValueOrDefault(DicomTag.ClinicalTrialTimePointID, string.Empty); + var studyInstanceUID = dataset.GetSingleValueOrDefault(DicomTag.StudyInstanceUID, string.Empty); + var seriesInstanceUID = dataset.GetSingleValueOrDefault(DicomTag.SeriesInstanceUID, string.Empty); + var sOPInstanceUID = dataset.GetSingleValueOrDefault(DicomTag.SOPInstanceUID, string.Empty); + + var numberOfFrames = dataset.GetSingleValueOrDefault(DicomTag.NumberOfFrames, 1); + + + // 传输语法 + var transferSyntaxUID = fileMeta.GetSingleValueOrDefault(DicomTag.TransferSyntaxUID, string.Empty); + var sOPClassUID = dataset.GetSingleValueOrDefault(DicomTag.SOPClassUID, string.Empty); + var mediaStorageSOPClassUID = fileMeta.GetSingleValueOrDefault(DicomTag.MediaStorageSOPClassUID, string.Empty); + + var mediaStorageSOPInstanceUID = fileMeta.GetSingleValueOrDefault(DicomTag.MediaStorageSOPInstanceUID, string.Empty); + + + var line = string.Join(",", + item.InstanceId, + stationName, + institutionName, + manufacturer, + clinicalTrialSubjectID, + clinicalTrialTimePointID, + studyInstanceUID, + seriesInstanceUID, + sOPInstanceUID, + numberOfFrames, + transferSyntaxUID, + sOPClassUID, + mediaStorageSOPClassUID, + mediaStorageSOPInstanceUID + + ); + + + await File.AppendAllTextAsync(outputFile, line + Environment.NewLine); + + //维护序列层级四个字段 后再用sql 维护study series 时间拆分 和 MediaStorageSOPInstanceUID + await _instanceRepository.BatchUpdateNoTrackingAsync( + t => t.Id == item.InstanceId, + t => new DicomInstance + { + IsEncapsulated = syntax.IsEncapsulated, + TransferSytaxUID = transferSyntaxUID, + MediaStorageSOPClassUID = mediaStorageSOPClassUID, + SOPClassUID = sOPClassUID, + MediaStorageSOPInstanceUID = mediaStorageSOPInstanceUID + }, false); + } + catch (Exception ex) + { + + var errorMsg = $"{DateTime.Now} ❌ 失败: {ex.Message} | InstanceId={item.InstanceId}, Path={item.Path}"; + + Console.WriteLine(errorMsg); + + await File.AppendAllTextAsync(outputErrorFile, errorMsg + Environment.NewLine); + } + finally + { + processed++; + double percent = processed * 100.0 / total; + + // 每提升 5% 或完成时输出 + if (percent - lastPercent >= 2.0 || processed == total) + { + lastPercent = percent; + Console.WriteLine($"{DateTime.Now} 进度: {processed}/{total} ({percent:F2}%)"); + } + } + + + } + + return ResponseOutput.Ok(); + } + + public class NeedDealInstanceInfo + { + public Guid InstanceId { get; set; } + + public string Path { get; set; } + } + + /// + /// 读取该项目的数据,进行维护 + /// + /// + /// + /// + [AllowAnonymous] + public async Task WriteTrialNeedDealData([FromServices] IRepository _instanceRepository, Guid trialId) + { + + #region 获取差集数据 + //var rows = await MiniExcel.QueryAsync(@"C:\Users\hang\Desktop\维护数据读取.xlsx"); + + //rows = rows.Where(t => !string.IsNullOrEmpty(t.InstanceId.ToString())).ToList(); + + //var allRows = await MiniExcel.QueryAsync(@"C:\Users\hang\Desktop\AllData.xlsx"); + + //allRows = allRows.Where(t => !string.IsNullOrEmpty(t.InstanceId.ToString())).ToList(); + + //var needDealRows = allRows.Where(t => !rows.Select(c => c.InstanceId).Contains(t.InstanceId)).ToList(); + + //var outputFile = Path.Combine(@"D:\dicomWrite", $"{Guid.NewGuid()}_dicom_info.txt"); + + //foreach (var item in needDealRows) + //{ + // var line = string.Join(",", item.InstanceId, item.Path); + + // await File.AppendAllTextAsync(outputFile, line + Environment.NewLine); + //} + + #endregion + + var folder = FileStoreHelper.GetIRaCISRootDataFolder(_hostEnvironment); + + var needDealRows = _instanceRepository.Where(t => t.TrialId == trialId && t.TransferSytaxUID == "").Select(t => new NeedDealInstanceInfo() { InstanceId = t.Id, Path = t.Path }); + + + + var outputFile = Path.Combine(folder, $"{Guid.NewGuid()}_dicom_info.txt"); + + var outputErrorFile = Path.Combine(folder, $"{Guid.NewGuid()}_dicom_info_error.txt"); + + int total = needDealRows.Count(); + + Console.WriteLine($"需要处理数量{total}"); + + int processed = 0; + double lastPercent = 0; + + foreach (var item in needDealRows) + { + + try + { + await using var stream = await _oSSService.GetStreamFromOSSAsync(item.Path); + + var dicomFile = DicomFile.Open(stream); + + var dataset = dicomFile.Dataset; + var fileMeta = dicomFile.FileMetaInfo; + + var pixelData = DicomPixelData.Create(dicomFile.Dataset); + + //获取像素是否为封装形式 + var syntax = dicomFile.Dataset.InternalTransferSyntax; + + var stationName = dataset.GetSingleValueOrDefault(DicomTag.StationName, string.Empty); + + var institutionName = dataset.GetSingleValueOrDefault(DicomTag.InstitutionName, string.Empty); + + var manufacturer = dataset.GetSingleValueOrDefault(DicomTag.Manufacturer, string.Empty); + + //PatientID TrialCode_SubjectCode + //var patientID = dataset.GetSingleValueOrDefault(DicomTag.PatientID, string.Empty); + + //SubjectCode + var clinicalTrialSubjectID = dataset.GetSingleValueOrDefault(DicomTag.ClinicalTrialSubjectID, string.Empty); + //访视visitNum + var clinicalTrialTimePointID = dataset.GetSingleValueOrDefault(DicomTag.ClinicalTrialTimePointID, string.Empty); + var studyInstanceUID = dataset.GetSingleValueOrDefault(DicomTag.StudyInstanceUID, string.Empty); + var seriesInstanceUID = dataset.GetSingleValueOrDefault(DicomTag.SeriesInstanceUID, string.Empty); + var sOPInstanceUID = dataset.GetSingleValueOrDefault(DicomTag.SOPInstanceUID, string.Empty); + + var numberOfFrames = dataset.GetSingleValueOrDefault(DicomTag.NumberOfFrames, 1); + + + // 传输语法 + var transferSyntaxUID = fileMeta.GetSingleValueOrDefault(DicomTag.TransferSyntaxUID, string.Empty); + var sOPClassUID = dataset.GetSingleValueOrDefault(DicomTag.SOPClassUID, string.Empty); + var mediaStorageSOPClassUID = fileMeta.GetSingleValueOrDefault(DicomTag.MediaStorageSOPClassUID, string.Empty); + + var mediaStorageSOPInstanceUID = fileMeta.GetSingleValueOrDefault(DicomTag.MediaStorageSOPInstanceUID, string.Empty); + + + var line = string.Join(",", + item.InstanceId, + stationName, + institutionName, + manufacturer, + clinicalTrialSubjectID, + clinicalTrialTimePointID, + studyInstanceUID, + seriesInstanceUID, + sOPInstanceUID, + numberOfFrames, + transferSyntaxUID, + sOPClassUID, + mediaStorageSOPClassUID, + mediaStorageSOPInstanceUID + + ); + + + await File.AppendAllTextAsync(outputFile, line + Environment.NewLine); + + //维护序列层级四个字段 后再用sql 维护study series 时间拆分 和 MediaStorageSOPInstanceUID + await _instanceRepository.BatchUpdateNoTrackingAsync( + t => t.Id == item.InstanceId, + t => new DicomInstance + { + IsEncapsulated = syntax.IsEncapsulated, + TransferSytaxUID = transferSyntaxUID, + MediaStorageSOPClassUID = mediaStorageSOPClassUID, + SOPClassUID = sOPClassUID, + MediaStorageSOPInstanceUID = mediaStorageSOPInstanceUID + }, false); + } + catch (Exception ex) + { + + var errorMsg = $"{DateTime.Now} ❌ 失败: {ex.Message} | InstanceId={item.InstanceId}, Path={item.Path}"; + + Console.WriteLine(errorMsg); + + await File.AppendAllTextAsync(outputErrorFile, errorMsg + Environment.NewLine); + } + finally + { + processed++; + double percent = processed * 100.0 / total; + + // 每提升 5% 或完成时输出 + if (percent - lastPercent >= 2.0 || processed == total) + { + lastPercent = percent; + Console.WriteLine($"{DateTime.Now} 进度: {processed}/{total} ({percent:F2}%)"); + } + } + + + } + + return ResponseOutput.Ok(); + } + #endregion + + + #region oss 下载删除影像 ,并且恢复数据 + [AllowAnonymous] + public async Task RestoreDBOSSDate( + [FromServices] IOSSService _oSSService, [FromServices] IWebHostEnvironment _hostEnvironment, [FromServices] IRepository _studyRepository) + { + var folder = FileStoreHelper.GetIRaCISRootDataFolder(_hostEnvironment); + + var outputFile = Path.Combine(folder, $"{Guid.NewGuid()}_deleteKey_info.txt"); + var outputFile2 = Path.Combine(folder, $"{Guid.NewGuid()}_deleteKeyExport_info.txt"); + + var outputErrorFile = Path.Combine(folder, $"{Guid.NewGuid()}_deleteKeyerror.txt"); + var outputErrorFile2 = Path.Combine(folder, $"{Guid.NewGuid()}_deleteKeyerrorStudy.txt"); + + + var aliConfig = _oSSService.ObjectStoreServiceOptions.AliyunOSS; + + var tempToken = _oSSService.GetObjectStoreTempToken(); + + var _ossClient = new OssClient(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? aliConfig.EndPoint : aliConfig.InternalEndpoint, + tempToken.AliyunOSS.AccessKeyId, + tempToken.AliyunOSS.AccessKeySecret, + tempToken.AliyunOSS.SecurityToken); + + + var allVersions = new List(); + var allDeleteMarkers = new List(); + + var request = new ListObjectVersionsRequest(tempToken.AliyunOSS.BucketName) + { + Prefix = "01000000-ac13-0242-6397-08dcd2d2a091/Image", + //Prefix = "01000000-ac13-0242-6397-08dcd2d2a091/Image/08dd9c04-c1b2-c2da-0242-ac1301000000/01000000-ac13-0242-235b-08dd9c04c1b3", + MaxKeys = 1000, + }; + + ObjectVersionList result; + do + { + + result = _ossClient.ListObjectVersions(request); + + if (result.ObjectVersionSummaries != null) + allVersions.AddRange(result.ObjectVersionSummaries); + + if (result.DeleteMarkerSummaries != null) + allDeleteMarkers.AddRange(result.DeleteMarkerSummaries); + + request.KeyMarker = result.NextKeyMarker; + request.VersionIdMarker = result.NextVersionIdMarker; + + } while (result.IsTruncated); + + Console.WriteLine($"共找到 {allDeleteMarkers.Count} 个删除标记"); + + + + int total = allDeleteMarkers.Count; + + int processed = 0; + double lastPercent = 0; + + + // 按 Key 分组,找每个删除标记前的最近版本 + var versionsByKey = allVersions + .GroupBy(v => v.Key) + .ToDictionary(g => g.Key, g => g.OrderByDescending(x => x.LastModified).ToList()); + + foreach (var del in allDeleteMarkers) + { + #region 防止阿里云过期 + if (tempToken.AliyunOSS.Expiration.AddSeconds(10) <= DateTime.Now) + { + tempToken = _oSSService.GetObjectStoreTempToken(); + + _ossClient = new OssClient(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? aliConfig.EndPoint : aliConfig.InternalEndpoint, + tempToken.AliyunOSS.AccessKeyId, + tempToken.AliyunOSS.AccessKeySecret, + tempToken.AliyunOSS.SecurityToken); + } + + #endregion + + if (!versionsByKey.TryGetValue(del.Key, out var versions)) + continue; // 没有历史版本无法恢复 + + var prevVersion = versions.FirstOrDefault(v => v.LastModified < del.LastModified); + if (prevVersion == null) + continue; // 没找到可恢复版本 + + if (Path.GetExtension(prevVersion.Key).IsNotNullOrEmpty()) + { + continue;//不是dicom 文件 + } + + + try + { + //await File.AppendAllTextAsync(outputFile, $"{prevVersion.Key},{prevVersion.VersionId}" + Environment.NewLine); + + var getReq = new GetObjectRequest(tempToken.AliyunOSS.BucketName, prevVersion.Key) + { + VersionId = prevVersion.VersionId + }; + + using (var getResult = _ossClient.GetObject(getReq)) + using (var memStream = new MemoryStream()) + { + // 先把 OSS 流复制到内存流 + getResult.Content.CopyTo(memStream); + memStream.Position = 0; + + // 读取 DICOM 信息 + var dicomFile = DicomFile.Open(memStream); + var studyInstanceUID = dicomFile.Dataset.GetString(DicomTag.StudyInstanceUID); + + var findInfo = await _studyRepository.Where(t => t.StudyInstanceUid == studyInstanceUID && t.TrialId == Guid.Parse("01000000-ac13-0242-6397-08dcd2d2a091")) + .Select(t => new { t.StudyInstanceUid, t.Subject.Code, t.SubjectVisit.VisitName, t.SubjectId, t.SubjectVisitId }).FirstOrDefaultAsync(); + + if (findInfo != null) + { + + // 再保存到另一个路径(可以使用 fo-dicom 保存) + + var fileName = Path.GetFileNameWithoutExtension(prevVersion.Key); + var anotherPath = Path.Combine(folder, findInfo.Code, findInfo.VisitName, studyInstanceUID, fileName); + // 去掉 folder 部分,得到相对路径 + var relativePath = Path.GetRelativePath(folder, anotherPath); + Directory.CreateDirectory(Path.GetDirectoryName(anotherPath)); + dicomFile.Save(anotherPath); + + await File.AppendAllTextAsync(outputFile2, $"{findInfo.SubjectId},{findInfo.SubjectVisitId},{prevVersion.Key},{prevVersion.VersionId},{relativePath},{findInfo.Code},{findInfo.VisitName},{findInfo.StudyInstanceUid},{fileName}" + Environment.NewLine); + } + else + { + await File.AppendAllTextAsync(outputErrorFile2, $"{studyInstanceUID},{prevVersion.Key},{prevVersion.VersionId}" + Environment.NewLine); + } + + //Console.WriteLine($"读取到 studyInstanceUID: {studyInstanceUID}"); + + //var localPath = Path.Combine(folder, prevVersion.Key.Trim('/').Replace('/', Path.DirectorySeparatorChar)); + //Directory.CreateDirectory(Path.GetDirectoryName(localPath)); + //// 保存到原本路径 + //memStream.Position = 0; + //using (var fs = File.Create(localPath)) + //{ + // memStream.CopyTo(fs); + //} + + + } + + + + //Console.WriteLine($"✅ 下载成功: {prevVersion.Key} (version={prevVersion.VersionId})"); + } + catch (Exception ex) + { + Console.WriteLine($"❌ 下载失败: {prevVersion.Key}, 错误: {ex.Message}"); + + await File.AppendAllTextAsync(outputErrorFile, $"{prevVersion.Key},{prevVersion.VersionId}" + Environment.NewLine); + } + finally + { + processed++; + double percent = processed * 100.0 / total; + + // 每提升 5% 或完成时输出 + if (percent - lastPercent >= 2.0 || processed == total) + { + lastPercent = percent; + Console.WriteLine($"{DateTime.Now} 进度: {processed}/{total} ({percent:F2}%)"); + } + } + + + // 使用 CopyObject 把历史版本拷贝为最新版本(恢复) + //var copyReq = new CopyObjectRequest + //{ + // Bucket = bucketName, + // Key = prevVersion.Key, + // SourceBucket = bucketName, + // SourceKey = prevVersion.Key, + // SourceVersionId = prevVersion.VersionId + //}; + + //try + //{ + // var copyResult = client.CopyObject(copyReq); + // Console.WriteLine($"✅ 恢复成功: {prevVersion.Key} -> newVersionId={copyResult.VersionId}"); + //} + //catch (Exception ex) + //{ + // Console.WriteLine($"❌ 恢复失败: {prevVersion.Key}, 错误: {ex.Message}"); + //} + } + + + + return ResponseOutput.Ok(); + } + + + + public async Task OSSDeleteReStorre([FromServices] IOSSService _oSSService, [FromServices] IWebHostEnvironment _hostEnvironment) + { + var aliConfig = _oSSService.ObjectStoreServiceOptions.AliyunOSS; + + var tempToken = _oSSService.GetObjectStoreTempToken(); + + var _ossClient = new OssClient(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? aliConfig.EndPoint : aliConfig.InternalEndpoint, + tempToken.AliyunOSS.AccessKeyId, + tempToken.AliyunOSS.AccessKeySecret, + tempToken.AliyunOSS.SecurityToken); + + + var allVersions = new List(); + var allDeleteMarkers = new List(); + + var request = new ListObjectVersionsRequest(tempToken.AliyunOSS.BucketName) + { + Prefix = "test-delete-restore", + //Prefix = "01000000-ac13-0242-6397-08dcd2d2a091/Image/08dd9c04-c1b2-c2da-0242-ac1301000000/01000000-ac13-0242-235b-08dd9c04c1b3", + MaxKeys = 1000, + }; + + ObjectVersionList result; + do + { + + result = _ossClient.ListObjectVersions(request); + + if (result.ObjectVersionSummaries != null) + allVersions.AddRange(result.ObjectVersionSummaries); + + if (result.DeleteMarkerSummaries != null) + allDeleteMarkers.AddRange(result.DeleteMarkerSummaries); + + request.KeyMarker = result.NextKeyMarker; + request.VersionIdMarker = result.NextVersionIdMarker; + + } while (result.IsTruncated); + + Console.WriteLine($"共找到 {allDeleteMarkers.Count} 个删除标记"); + + var versionsByKey = allVersions + .GroupBy(v => v.Key) + .ToDictionary(g => g.Key, g => g.OrderByDescending(x => x.LastModified).ToList()); + + + foreach (var del in allDeleteMarkers) + { + #region 防止阿里云过期 + if (tempToken.AliyunOSS.Expiration.AddSeconds(10) <= DateTime.Now) + { + tempToken = _oSSService.GetObjectStoreTempToken(); + + _ossClient = new OssClient(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? aliConfig.EndPoint : aliConfig.InternalEndpoint, + tempToken.AliyunOSS.AccessKeyId, + tempToken.AliyunOSS.AccessKeySecret, + tempToken.AliyunOSS.SecurityToken); + } + + #endregion + + if (!versionsByKey.TryGetValue(del.Key, out var versions)) + continue; // 没有历史版本无法恢复 + + var prevVersion = versions.FirstOrDefault(v => v.LastModified < del.LastModified); + if (prevVersion == null) + continue; // 没找到可恢复版本 + + + + + // 创建 CopyObject 请求 + // 先用构造函数指定源和目标 + var copyReq = new CopyObjectRequest( + sourceBucketName: tempToken.AliyunOSS.BucketName, + sourceKey: prevVersion.Key, + destinationBucketName: tempToken.AliyunOSS.BucketName, + destinationKey: prevVersion.Key // 覆盖到同名 Key,达到“恢复”的效果 + ); + + // 再设置版本号 + copyReq.SourceVersionId = prevVersion.VersionId; + + + + try + { + var copyResult = _ossClient.CopyObject(copyReq); + Console.WriteLine($"✅ 恢复成功: {prevVersion.Key}, 新版本ID={copyResult.VersionId}"); + } + catch (Exception ex) + { + Console.WriteLine($"❌ 恢复失败: {prevVersion.Key}, 错误: {ex.Message}"); + } + + + } + + return ResponseOutput.Ok(); + + + } + + /// + /// 读取excel 恢复oss 数据,读取dicom 恢复序列和Instance + /// + /// + /// + /// + [AllowAnonymous] + public async Task ReadExcelReStorreOSSDeleteDataAndDBData([FromServices] IOSSService _oSSService, [FromServices] IWebHostEnvironment _hostEnvironment, string subjectCode) + { + var folder = FileStoreHelper.GetIRaCISRootDataFolder(_hostEnvironment); + + var trialId = Guid.Parse("01000000-ac13-0242-6397-08dcd2d2a091"); + + var rows = await MiniExcel.QueryAsync(Path.Combine(folder, "删除恢复.xlsx")); + + var restoreRows = await MiniExcel.QueryAsync(Path.Combine(folder, "删除需要恢复的数据.xlsx")); + + rows = rows.Where(t => t.Key.IsNotNullOrEmpty()).WhereIf(subjectCode.IsNotNullOrEmpty(), t => t.SubjectCode == subjectCode.TrimStart('0')).ToList(); + + restoreRows = restoreRows.Where(t => t.SOPInstanceUID.IsNotNullOrEmpty()).WhereIf(subjectCode.IsNotNullOrEmpty(), t => t.SubjectCode == subjectCode).ToList(); + + Console.WriteLine($"恢复数量: {restoreRows.Count()}"); + + int total = rows.Count(); + int processed = 0; + double lastPercent = 0; + + var outputErrorFile = Path.Combine(folder, $"{Guid.NewGuid()}_dicom_info_error.txt"); + + var aliConfig = _oSSService.ObjectStoreServiceOptions.AliyunOSS; + + var tempToken = _oSSService.GetObjectStoreTempToken(); + + var _ossClient = new OssClient(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? aliConfig.EndPoint : aliConfig.InternalEndpoint, + tempToken.AliyunOSS.AccessKeyId, + tempToken.AliyunOSS.AccessKeySecret, + tempToken.AliyunOSS.SecurityToken); + + + var restoreCount = 0; + foreach (var item in rows) + { + //不包含该subject的忽略 + if (!restoreRows.Any(t => t.SubjectCode.TrimStart('0') == item.SubjectCode)) + { + continue; + } + + try + { + + //根据本地文件匹配studyInstanceUid 不匹配忽略 + + var localPath = Path.Combine(folder, item.RelativePath); + + + #region 读取本地dicom ,判断数据库是否存在该序列 + + var dicomFilePath = Path.Combine(folder, item.RelativePath); + + + var dicomFile = await DicomFile.OpenAsync(dicomFilePath); + + string sopInstanceUid = dicomFile.Dataset.GetSingleValueOrDefault(DicomTag.SOPInstanceUID, string.Empty); + + + if (!restoreRows.Any(t => t.SOPInstanceUID == sopInstanceUid)) + { + continue; + } + + restoreCount++; + + Console.WriteLine($"恢复SOPInstanceUID{sopInstanceUid},当前数量{restoreCount}"); + + await ArchiveDicomFileAsync(dicomFile.Dataset, trialId, item.SubjectId, item.SubjectVisitId); + + #endregion + + + #region 防止阿里云过期 + if (tempToken.AliyunOSS.Expiration.AddSeconds(10) <= DateTime.Now) + { + tempToken = _oSSService.GetObjectStoreTempToken(); + + _ossClient = new OssClient(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? aliConfig.EndPoint : aliConfig.InternalEndpoint, + tempToken.AliyunOSS.AccessKeyId, + tempToken.AliyunOSS.AccessKeySecret, + tempToken.AliyunOSS.SecurityToken); + } + + #endregion + + + // 创建 CopyObject 请求 + // 先用构造函数指定源和目标 + var copyReq = new CopyObjectRequest( + sourceBucketName: tempToken.AliyunOSS.BucketName, + sourceKey: item.Key, + destinationBucketName: tempToken.AliyunOSS.BucketName, + destinationKey: item.Key // 覆盖到同名 Key,达到“恢复”的效果 + ); + + // 再设置版本号 + copyReq.SourceVersionId = item.VersionId; + + var copyResult = _ossClient.CopyObject(copyReq); + + } + + catch (Exception ex) + { + + var errorMsg = $"❌ 恢复失败: {item.Key}, 错误: {ex.Message}"; + Console.WriteLine(errorMsg); + + await File.AppendAllTextAsync(outputErrorFile, errorMsg + Environment.NewLine); + } + finally + { + processed++; + double percent = processed * 100.0 / total; + + if (percent - lastPercent >= 2.0 || processed == total) + { + lastPercent = percent; + Console.WriteLine($"{DateTime.Now} 进度: {processed}/{total} ({percent:F2}%)"); + } + } + + } + + + await _studyRepository.SaveChangesAsync(); + + return ResponseOutput.Ok(); + } + + public class RestoreOSSDeleteDataDTO + { + public Guid SubjectId { get; set; } + + public Guid SubjectVisitId { get; set; } + + public string Key { get; set; } + + public string VersionId { get; set; } + + public string RelativePath { get; set; } + + public string SubjectCode { get; set; } + public string FileName { get; set; } + + #endregion + } + + public class RestoreInstanceInfo + { + public string SubjectCode { get; set; } + + public string VisitName { get; set; } + + public string SOPInstanceUID { get; set; } + } + + /// + /// 单个文件接收 归档 + /// + /// + /// + /// + /// + /// + /// + public async Task ArchiveDicomFileAsync(DicomDataset dataset, Guid trialId, Guid subjectId, Guid subjectVisitId) + { + string studyInstanceUid = dataset.GetString(DicomTag.StudyInstanceUID); + string seriesInstanceUid = dataset.GetString(DicomTag.SeriesInstanceUID); + string sopInstanceUid = dataset.GetString(DicomTag.SOPInstanceUID); + + Guid studyId = IdentifierHelper.CreateGuid(studyInstanceUid, trialId.ToString()); + Guid seriesId = IdentifierHelper.CreateGuid(studyInstanceUid, seriesInstanceUid, trialId.ToString()); + Guid instanceId = IdentifierHelper.CreateGuid(studyInstanceUid, seriesInstanceUid, sopInstanceUid, trialId.ToString()); + + var isSeriesNeedAdd = false; + var isInstanceNeedAdd = false; + + + var findStudy = await _studyRepository.FirstOrDefaultAsync(t => t.Id == studyId); + var findSerice = await _seriesRepository.FirstOrDefaultAsync(t => t.Id == seriesId); + var findInstance = await _instanceRepository.FirstOrDefaultAsync(t => t.Id == instanceId); + + + + if (findSerice == null) + { + isSeriesNeedAdd = true; + + findSerice = new DicomSeries + { + + + + Id = seriesId, + StudyId = findStudy.Id, + + StudyInstanceUid = findStudy.StudyInstanceUid, + SeriesInstanceUid = seriesInstanceUid, + SeriesNumber = dataset.GetSingleValueOrDefault(DicomTag.SeriesNumber, 1), + //SeriesTime = dataset.GetSingleValueOrDefault(DicomTag.SeriesDate, DateTime.Now).Add(dataset.GetSingleValueOrDefault(DicomTag.SeriesTime, DateTime.Now).TimeOfDay), + //SeriesTime = DateTime.TryParse(dataset.GetSingleValue(DicomTag.SeriesDate) + dataset.GetSingleValue(DicomTag.SeriesTime), out DateTime dt) ? dt : null, + SeriesTime = dataset.GetSingleValueOrDefault(DicomTag.SeriesDate, string.Empty) == string.Empty ? null : dataset.GetSingleValue(DicomTag.SeriesDate).Add(dataset.GetSingleValueOrDefault(DicomTag.SeriesTime, string.Empty) == string.Empty ? TimeSpan.Zero : dataset.GetSingleValue(DicomTag.SeriesTime).TimeOfDay), + + DicomSeriesDate = dataset.GetSingleValueOrDefault(DicomTag.SeriesDate, string.Empty), + DicomSeriesTime = dataset.GetSingleValueOrDefault(DicomTag.SeriesTime, string.Empty), + + Modality = dataset.GetSingleValueOrDefault(DicomTag.Modality, string.Empty), + Description = dataset.GetSingleValueOrDefault(DicomTag.SeriesDescription, string.Empty), + SliceThickness = dataset.GetSingleValueOrDefault(DicomTag.SliceThickness, string.Empty), + + ImagePositionPatient = dataset.GetSingleValueOrDefault(DicomTag.ImagePositionPatient, string.Empty), + ImageOrientationPatient = dataset.GetSingleValueOrDefault(DicomTag.ImageOrientationPatient, string.Empty), + BodyPartExamined = dataset.GetSingleValueOrDefault(DicomTag.BodyPartExamined, string.Empty), + SequenceName = dataset.GetSingleValueOrDefault(DicomTag.SequenceName, string.Empty), + ProtocolName = dataset.GetSingleValueOrDefault(DicomTag.ProtocolName, string.Empty), + ImagerPixelSpacing = dataset.GetSingleValueOrDefault(DicomTag.ImagerPixelSpacing, string.Empty), + + AcquisitionTime = dataset.GetSingleValueOrDefault(DicomTag.AcquisitionTime, string.Empty), + AcquisitionNumber = dataset.GetSingleValueOrDefault(DicomTag.AcquisitionNumber, string.Empty), + TriggerTime = dataset.GetSingleValueOrDefault(DicomTag.TriggerTime, string.Empty), + + TrialId = trialId, + SubjectId = subjectId, + SubjectVisitId = subjectVisitId, + + InstanceCount = 0 + }; + + ++findStudy.SeriesCount; + } + + + var transferSyntaxUID = dataset.GetSingleValueOrDefault(DicomTag.TransferSyntaxUID, string.Empty); + + var isEncapsulated = false; + if (transferSyntaxUID.IsNotNullOrEmpty()) + { + isEncapsulated = DicomTransferSyntax.Lookup(DicomUID.Parse(transferSyntaxUID)).IsEncapsulated; + } + + if (findInstance == null) + { + isInstanceNeedAdd = true; + findInstance = new DicomInstance + { + Id = instanceId, + StudyId = findStudy.Id, + SeriesId = findSerice.Id, + StudyInstanceUid = findStudy.StudyInstanceUid, + SeriesInstanceUid = findSerice.SeriesInstanceUid, + + TrialId = trialId, + SubjectId = subjectId, + SubjectVisitId = subjectVisitId, + + + + SopInstanceUid = sopInstanceUid, + SOPClassUID = dataset.GetSingleValueOrDefault(DicomTag.SOPClassUID, string.Empty), + MediaStorageSOPClassUID = dataset.GetSingleValueOrDefault(DicomTag.MediaStorageSOPClassUID, string.Empty), + TransferSytaxUID = transferSyntaxUID, + MediaStorageSOPInstanceUID = dataset.GetSingleValueOrDefault(DicomTag.MediaStorageSOPInstanceUID, string.Empty), + IsEncapsulated = isEncapsulated, + + InstanceNumber = dataset.GetSingleValueOrDefault(DicomTag.InstanceNumber, 1), + InstanceTime = dataset.GetSingleValueOrDefault(DicomTag.ContentDate, string.Empty) == string.Empty ? null : dataset.GetSingleValue(DicomTag.ContentDate).Add(dataset.GetSingleValueOrDefault(DicomTag.ContentTime, string.Empty) == string.Empty ? TimeSpan.Zero : dataset.GetSingleValue(DicomTag.ContentTime).TimeOfDay), + + CPIStatus = false, + ImageRows = dataset.GetSingleValueOrDefault(DicomTag.Rows, 0), + ImageColumns = dataset.GetSingleValueOrDefault(DicomTag.Columns, 0), + SliceLocation = dataset.GetSingleValueOrDefault(DicomTag.SliceLocation, 0), + + SliceThickness = dataset.GetSingleValueOrDefault(DicomTag.SliceThickness, string.Empty), + NumberOfFrames = dataset.GetSingleValueOrDefault(DicomTag.NumberOfFrames, 0), + PixelSpacing = dataset.GetSingleValueOrDefault(DicomTag.PixelSpacing, string.Empty), + ImagerPixelSpacing = dataset.GetSingleValueOrDefault(DicomTag.ImagerPixelSpacing, string.Empty), + FrameOfReferenceUID = dataset.GetSingleValueOrDefault(DicomTag.FrameOfReferenceUID, string.Empty), + WindowCenter = dataset.GetSingleValueOrDefault(DicomTag.WindowCenter, string.Empty), + WindowWidth = dataset.GetSingleValueOrDefault(DicomTag.WindowWidth, string.Empty), + }; + + ++findStudy.InstanceCount; + ++findSerice.InstanceCount; + } + + + if (isSeriesNeedAdd) + { + await _seriesRepository.AddAsync(findSerice); + } + if (isInstanceNeedAdd) + { + await _instanceRepository.AddAsync(findInstance); + } + + } + + } } \ No newline at end of file