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