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 { /// /// 项目影像后台下载,不打包 /// /// /// [ApiExplorerSettings(GroupName = "Common")] public class TrialImageDownloadService(IRepository _trialRepository, IOSSService _oSSService, IWebHostEnvironment _hostEnvironment, IRepository _studyRepository, IRepository _seriesRepository, IRepository _instanceRepository) : BaseService { /// /// 后端api swagger 下载项目影像 /// /// /// [HttpPost] [AllowAnonymous] public async Task DownloadTrialImage(Guid trialId) { //var subjectCodeList = new List() { "05002", "07006", "07026" }; var downloadInfo = _trialRepository.Where(t => t.Id == trialId).Select(t => new { t.ResearchProgramNo, VisitList = t.SubjectVisitList //.Where(t=>subjectCodeList.Contains(t.Subject.Code)) .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.Where(t => t.IsReading).Select(z => new { z.Modality, InstancePathList = z.DicomInstanceList.Where(t => t.IsReading).Select(k => new { k.Path }).ToList() }) }).ToList(), NoneDicomStudyList = sv.NoneDicomStudyList.Where(t => t.IsReading).Select(nd => new { nd.Modality, nd.StudyCode, nd.ImageDate, FileList = nd.NoneDicomFileList.Where(t => t.IsReading).Select(file => new { file.FileName, file.Path, file.FileType }).ToList() }).ToList() }).ToList() }).FirstOrDefault(); var count = downloadInfo.VisitList.SelectMany(t => t.NoneDicomStudyList).SelectMany(t => t.FileList).Count(); var count2 = downloadInfo.VisitList.SelectMany(t => t.StudyList).SelectMany(t => t.SeriesList).SelectMany(t => t.InstancePathList).Count(); Console.WriteLine($"下载总数量:{count}+{count2}={count + count2}"); if (downloadInfo != null) { var downloadJobs = new List>(); 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 && visitItem.NoneDicomStudyList.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(siteFolderPath, $"{visitItem.SubjectCode}_{visitItem.VisitName}_DICOM", $"{studyInfo.StudyCode}_{studyInfo.StudyTime?.ToString("yyyy-MM-dd")}_{seriesInfo.Modality}"); // 创建 影像 文件夹 Directory.CreateDirectory(studyDicomFolderPath); // 遍历 InstancePathList foreach (var instanceInfo in seriesInfo.InstancePathList) { // 复制文件到相应的文件夹 string destinationPath = Path.Combine(studyDicomFolderPath, Path.GetFileName(instanceInfo.Path)); //加入到下载任务里 downloadJobs.Add(() => _oSSService.DownLoadFromOSSAsync(instanceInfo.Path, destinationPath)); //下载到当前目录 //await _oSSService.DownLoadFromOSSAsync(instanceInfo.Path, destinationPath); } } } foreach (var noneDicomStudy in visitItem.NoneDicomStudyList) { string studyNoneDicomFolderPath = Path.Combine(siteFolderPath, $"{visitItem.SubjectCode}_{visitItem.VisitName}_Non-DICOM", $"{noneDicomStudy.StudyCode}_{noneDicomStudy.ImageDate.ToString("yyyy-MM-dd")}_{noneDicomStudy.Modality}"); Directory.CreateDirectory(studyNoneDicomFolderPath); foreach (var file in noneDicomStudy.FileList) { string destinationPath = Path.Combine(studyNoneDicomFolderPath, Path.GetFileName(file.FileName)); //加入到下载任务里 downloadJobs.Add(() => _oSSService.DownLoadFromOSSAsync(HttpUtility.UrlDecode(file.Path), destinationPath)); //下载到当前目录 //await _oSSService.DownLoadFromOSSAsync(HttpUtility.UrlDecode(file.Path), destinationPath); } } } #region 异步方式处理 int totalCount = downloadJobs.Count; int downloadedCount = 0; foreach (var job in downloadJobs) { try { await job(); } catch (Exception ex) { Console.WriteLine($"下载失败: {ex.Message}"); } 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(); } /// /// 下载影像 维护dir信息 并回传到OSS /// /// /// [HttpGet] [AllowAnonymous] 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") .Select(t => new { t.SeriesId, t.StudyId, t.Id, t.Path }).ToListAsync(); int totalCount = list.Count; int dealCount = 0; foreach (var item in list) { var stream = await _oSSService.GetStreamFromOSSAsync(item.Path); var dicomFile = DicomFile.Open(stream); // 获取 Pixel Data 标签 var pixelData = DicomPixelData.Create(dicomFile.Dataset); //获取像素是否为封装形式 var syntax = dicomFile.Dataset.InternalTransferSyntax; //对于封装像素的文件做转换 if (syntax.IsEncapsulated) { // 创建一个新的片段序列 var newFragments = new DicomOtherByteFragment(DicomTag.PixelData); // 获取每帧数据并封装为单独的片段 for (int n = 0; n < pixelData.NumberOfFrames; n++) { var frameData = pixelData.GetFrame(n); newFragments.Fragments.Add(new MemoryByteBuffer(frameData.Data)); } // 替换原有的片段序列 dicomFile.Dataset.AddOrUpdate(newFragments); } #region 获取dir信息 维护数据库三个表数据 var dirInfo = DicomDIRHelper.ReadDicomDIRInfo(dicomFile); await _instanceRepository.BatchUpdateNoTrackingAsync(t => t.Id == item.Id, u => new DicomInstance() { IsEncapsulated = syntax.IsEncapsulated, TransferSytaxUID = dirInfo.TransferSytaxUID, SOPClassUID = dirInfo.SOPClassUID, MediaStorageSOPClassUID = dirInfo.MediaStorageSOPClassUID, MediaStorageSOPInstanceUID = dirInfo.MediaStorageSOPInstanceUID }, false); await _seriesRepository.BatchUpdateNoTrackingAsync(t => t.Id == item.SeriesId, u => new DicomSeries() { DicomSeriesDate = dirInfo.DicomSeriesDate, DicomSeriesTime = dirInfo.DicomSeriesTime, }, false); await _studyRepository.BatchUpdateNoTrackingAsync(t => t.Id == item.StudyId, u => new DicomStudy() { DicomStudyDate = dirInfo.DicomStudyDate, DicomStudyTime = dirInfo.DicomStudyTime }, false); #endregion //保存到内存流 using var memoryStream = new MemoryStream(); dicomFile.Save(memoryStream); memoryStream.Position = 0; //获取原始目录 和文件名 var folder = item.Path.Substring(0, item.Path.LastIndexOf('/')).TrimStart('/'); var fileName = Path.GetFileName(item.Path); //dicomFile.Save($"download_{Guid.NewGuid()}"); await _oSSService.UploadToOSSAsync(memoryStream, folder, fileName, false); dealCount++; Console.WriteLine($"{DateTime.Now}已下载 {dealCount} / {totalCount} 个文件,完成 {(dealCount * 100.0 / totalCount):F2}%"); } 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); } } } }