From c9ffa64560163392d6e051ea003a2e6717423f76 Mon Sep 17 00:00:00 2001 From: hang <872297557@qq.com> Date: Wed, 3 Jun 2026 13:54:10 +0800 Subject: [PATCH 1/7] =?UTF-8?q?=E9=81=AE=E7=9B=96=E5=BD=B1=E5=83=8F?= =?UTF-8?q?=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Common/TrialImageDownloadService.cs | 24 +++++++++++++--- .../Service/ImageAndDoc/StudyService.cs | 28 ++++++++++++++++++- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/IRaCIS.Core.Application/Service/Common/TrialImageDownloadService.cs b/IRaCIS.Core.Application/Service/Common/TrialImageDownloadService.cs index 6ba580e20..ac35dd052 100644 --- a/IRaCIS.Core.Application/Service/Common/TrialImageDownloadService.cs +++ b/IRaCIS.Core.Application/Service/Common/TrialImageDownloadService.cs @@ -60,6 +60,14 @@ namespace IRaCIS.Core.Application.Service { try { + + #region 方式一 有的必须在内存中,不能用这种 + //await using var source = await sourceFactory(); + //// 如果你是从 stream 打开 + //var dicomFile = await DicomFile.OpenAsync(source); + #endregion + + #region 方式二 await using var source = await sourceFactory(); // 【关键修复】将 OSS 流缓冲到 MemoryStream @@ -79,6 +87,8 @@ namespace IRaCIS.Core.Application.Service // 如果你是从 stream 打开 var dicomFile = await DicomFile.OpenAsync(source.CanSeek ? source : bufferedStream); + #endregion + //获取像素是否为封装形式 var syntax = dicomFile.Dataset.InternalTransferSyntax; @@ -211,8 +221,12 @@ namespace IRaCIS.Core.Application.Service #endregion + var oldVisits = MiniExcel.Query(Path.Combine(rootFolder, "Old.xlsx")).ToList(); - var downloadVisits = MiniExcel.Query(Path.Combine(rootFolder, "download.xlsx")).ToList().Where(t => t.SubjectCode.IsNotNullOrEmpty() && t.VisitName.IsNotNullOrEmpty()); + var downloadVisits = MiniExcel.Query(Path.Combine(rootFolder, "download.xlsx")).ToList().Where(t => t.SubjectCode.IsNotNullOrEmpty() && t.VisitName.IsNotNullOrEmpty()).ToList(); + + downloadVisits= downloadVisits.Where(t => !oldVisits.Any(old => old.VisitNum == t.VisitNum && old.SubjectCode == t.SubjectCode && + old.VisitName.Trim().ToLower() == t.VisitName.Trim().ToLower())).ToList(); var downloadJobs = new List(); @@ -308,6 +322,7 @@ namespace IRaCIS.Core.Application.Service continue; } + Log.Logger.Warning($"开始获取访视信息准备下载任务:{visitItem.SubjectCode} {visitItem.VisitName} {visitItem.VisitNum}"); foreach (var studyInfo in visitItem.StudyList) { @@ -502,6 +517,7 @@ namespace IRaCIS.Core.Application.Service return Task.CompletedTask; } }); + } @@ -515,14 +531,14 @@ namespace IRaCIS.Core.Application.Service - - + Log.Logger.Warning($"访视信息准备完毕: {downloadVisits.Count},后端开始下载任务......"); + #region 异步方式处理 int totalCount = downloadJobs.Count; int downloadedCount = 0; - Log.Logger.Warning($"开始下载总数: {totalCount}"); + Log.Logger.Warning($"下载文件总数: {totalCount}"); foreach (var job in downloadJobs) { try diff --git a/IRaCIS.Core.Application/Service/ImageAndDoc/StudyService.cs b/IRaCIS.Core.Application/Service/ImageAndDoc/StudyService.cs index 70458f08a..cb899be04 100644 --- a/IRaCIS.Core.Application/Service/ImageAndDoc/StudyService.cs +++ b/IRaCIS.Core.Application/Service/ImageAndDoc/StudyService.cs @@ -82,9 +82,35 @@ namespace IRaCIS.Core.Application.Service.ImageAndDoc { try { + #region 方式一 有的必须在内存中,不能用这种 + //await using var source = await sourceFactory(); + //// 如果你是从 stream 打开 + //var dicomFile = await DicomFile.OpenAsync(source); + #endregion + + #region 方式二 + await using var source = await sourceFactory(); + + // 【关键修复】将 OSS 流缓冲到 MemoryStream + using var bufferedStream = new MemoryStream(); + + if (source.CanSeek) + { + source.Position = 0; + + } + else + { + // 完全复制到内存流 + await source.CopyToAsync(bufferedStream); + bufferedStream.Position = 0; // 重置位置 + } + // 如果你是从 stream 打开 - var dicomFile = await DicomFile.OpenAsync(source); + var dicomFile = await DicomFile.OpenAsync(source.CanSeek ? source : bufferedStream); + + #endregion //获取像素是否为封装形式 var syntax = dicomFile.Dataset.InternalTransferSyntax; From ff2c619df7e51209ff9eff909558a8980251a5f4 Mon Sep 17 00:00:00 2001 From: hang <872297557@qq.com> Date: Wed, 3 Jun 2026 14:09:12 +0800 Subject: [PATCH 2/7] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=B8=8B=E8=BD=BD?= =?UTF-8?q?=E9=A2=9D=E5=A4=96=E6=97=A5=E5=BF=97=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Service/Common/TrialImageDownloadService.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/IRaCIS.Core.Application/Service/Common/TrialImageDownloadService.cs b/IRaCIS.Core.Application/Service/Common/TrialImageDownloadService.cs index ac35dd052..2323b74b4 100644 --- a/IRaCIS.Core.Application/Service/Common/TrialImageDownloadService.cs +++ b/IRaCIS.Core.Application/Service/Common/TrialImageDownloadService.cs @@ -230,6 +230,7 @@ namespace IRaCIS.Core.Application.Service var downloadJobs = new List(); + var skipCount = 0; foreach (var downloadVisit in downloadVisits) { var downloadInfo = _trialRepository.Where(t => t.Id == trialId).Select(t => new @@ -305,6 +306,8 @@ namespace IRaCIS.Core.Application.Service if (existVisits.Any(old => old.VisitNum == downloadVisit.VisitNum && old.SubjectCode == downloadVisit.SubjectCode && old.VisitName.Trim().ToLower() == downloadVisit.VisitName.Trim().ToLower())) { + Log.Logger.Warning($"Excel显示已下载,跳过当前访视:{downloadVisit.SubjectCode} {downloadVisit.VisitName} {downloadVisit.VisitNum}"); + skipCount++; continue; } @@ -319,6 +322,8 @@ namespace IRaCIS.Core.Application.Service { if (visitItem.StudyList.Count() == 0 && visitItem.NoneDicomStudyList.Count() == 0) { + Log.Logger.Warning($"查询无检查,跳过当前访视:{downloadVisit.SubjectCode} {downloadVisit.VisitName} {downloadVisit.VisitNum}"); + skipCount++; continue; } @@ -480,7 +485,7 @@ namespace IRaCIS.Core.Application.Service Action = async () => { - Log.Logger.Warning($" {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}压缩访视:{visitItem.SubjectCode} {visitItem.VisitName} {visitItem.VisitNum}"); + Log.Logger.Warning($" {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}开启另外线程压缩访视:{visitItem.SubjectCode} {visitItem.VisitName} {visitItem.VisitNum}"); string zipPath = visitFolderPath + ".zip"; @@ -531,7 +536,7 @@ namespace IRaCIS.Core.Application.Service - Log.Logger.Warning($"访视信息准备完毕: {downloadVisits.Count},后端开始下载任务......"); + Log.Logger.Warning($"{downloadVisits.Count}个访视信息核对准备完毕, 跳过{skipCount} 个,后端开始下载任务......"); #region 异步方式处理 From 8b046e61ab4738041e14498163c04662d74f6a72 Mon Sep 17 00:00:00 2001 From: hang <872297557@qq.com> Date: Wed, 3 Jun 2026 14:34:25 +0800 Subject: [PATCH 3/7] =?UTF-8?q?=E5=90=8E=E7=AB=AF=E9=95=BF=E6=97=B6?= =?UTF-8?q?=E9=97=B4=E4=B8=8B=E8=BD=BD=EF=BC=8C=E4=B8=B4=E6=97=B6token=20?= =?UTF-8?q?=E7=BB=AD=E6=9C=9Fbug=20=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- IRaCIS.Core.Application/Helper/OSSService.cs | 46 +++++++++++++------ .../Common/TrialImageDownloadService.cs | 14 +++--- 2 files changed, 41 insertions(+), 19 deletions(-) diff --git a/IRaCIS.Core.Application/Helper/OSSService.cs b/IRaCIS.Core.Application/Helper/OSSService.cs index ec04c3f08..1114481db 100644 --- a/IRaCIS.Core.Application/Helper/OSSService.cs +++ b/IRaCIS.Core.Application/Helper/OSSService.cs @@ -220,7 +220,7 @@ public class OSSService(IOptionsMonitor options, public object result { get; private set; } - + private static readonly object _tokenLock = new(); /// /// 将指定前缀下的所有现有文件立即转为目标存储类型 @@ -934,32 +934,52 @@ public class OSSService(IOptionsMonitor options, //后端批量上传 或者下载,不每个文件获取临时token private void BackBatchGetToken() { + + + + if (ObjectStoreServiceOptions.ObjectStoreUse == "AliyunOSS") { - if (AliyunOSSTempToken == null) + if (AliyunOSSTempToken != null && AliyunOSSTempToken.Expiration > DateTime.UtcNow.AddMinutes(15)) { - GetObjectStoreTempToken(); - } - //token 过期了 - if (AliyunOSSTempToken?.Expiration.AddSeconds(10) <= DateTime.Now) - { - GetObjectStoreTempToken(); + return; } + lock (_tokenLock) + { + if (AliyunOSSTempToken == null || + AliyunOSSTempToken.Expiration <= DateTime.UtcNow.AddMinutes(15)) + { + GetObjectStoreTempToken(); + + Log.Logger.Warning("后端获取阿里云临时 Token"); + } + } } else if (ObjectStoreServiceOptions.ObjectStoreUse == "AWS") { - if (AWSTempToken == null) + + + if (AWSTempToken != null && AWSTempToken.Expiration > DateTime.UtcNow.AddMinutes(15)) { - GetObjectStoreTempToken(); + return; } - //token 过期了 - if (AWSTempToken.Expiration?.AddSeconds(10) <= DateTime.Now) + + lock (_tokenLock) { - GetObjectStoreTempToken(); + if (AWSTempToken == null || + AWSTempToken.Expiration <= DateTime.UtcNow.AddMinutes(15)) + { + GetObjectStoreTempToken(); + + Log.Logger.Warning("后端获取s3 临时 Token"); + } } + } + + } diff --git a/IRaCIS.Core.Application/Service/Common/TrialImageDownloadService.cs b/IRaCIS.Core.Application/Service/Common/TrialImageDownloadService.cs index 2323b74b4..971eef75e 100644 --- a/IRaCIS.Core.Application/Service/Common/TrialImageDownloadService.cs +++ b/IRaCIS.Core.Application/Service/Common/TrialImageDownloadService.cs @@ -225,14 +225,16 @@ namespace IRaCIS.Core.Application.Service var downloadVisits = MiniExcel.Query(Path.Combine(rootFolder, "download.xlsx")).ToList().Where(t => t.SubjectCode.IsNotNullOrEmpty() && t.VisitName.IsNotNullOrEmpty()).ToList(); - downloadVisits= downloadVisits.Where(t => !oldVisits.Any(old => old.VisitNum == t.VisitNum && old.SubjectCode == t.SubjectCode && + downloadVisits = downloadVisits.Where(t => !oldVisits.Any(old => old.VisitNum == t.VisitNum && old.SubjectCode == t.SubjectCode && old.VisitName.Trim().ToLower() == t.VisitName.Trim().ToLower())).ToList(); var downloadJobs = new List(); var skipCount = 0; + var visitIndex = 0; foreach (var downloadVisit in downloadVisits) { + visitIndex++; var downloadInfo = _trialRepository.Where(t => t.Id == trialId).Select(t => new { t.ResearchProgramNo, @@ -306,7 +308,7 @@ namespace IRaCIS.Core.Application.Service if (existVisits.Any(old => old.VisitNum == downloadVisit.VisitNum && old.SubjectCode == downloadVisit.SubjectCode && old.VisitName.Trim().ToLower() == downloadVisit.VisitName.Trim().ToLower())) { - Log.Logger.Warning($"Excel显示已下载,跳过当前访视:{downloadVisit.SubjectCode} {downloadVisit.VisitName} {downloadVisit.VisitNum}"); + Log.Logger.Warning($"[{visitIndex}] Excel显示已下载,跳过当前访视:{downloadVisit.SubjectCode} {downloadVisit.VisitName} {downloadVisit.VisitNum}"); skipCount++; continue; } @@ -322,12 +324,12 @@ namespace IRaCIS.Core.Application.Service { if (visitItem.StudyList.Count() == 0 && visitItem.NoneDicomStudyList.Count() == 0) { - Log.Logger.Warning($"查询无检查,跳过当前访视:{downloadVisit.SubjectCode} {downloadVisit.VisitName} {downloadVisit.VisitNum}"); + Log.Logger.Warning($"[{visitIndex}]查询无检查,跳过当前访视:{downloadVisit.SubjectCode} {downloadVisit.VisitName} {downloadVisit.VisitNum}"); skipCount++; continue; } - Log.Logger.Warning($"开始获取访视信息准备下载任务:{visitItem.SubjectCode} {visitItem.VisitName} {visitItem.VisitNum}"); + Log.Logger.Warning($"[{visitIndex}]开始获取访视信息准备下载任务:{visitItem.SubjectCode} {visitItem.VisitName} {visitItem.VisitNum}"); foreach (var studyInfo in visitItem.StudyList) { @@ -485,7 +487,7 @@ namespace IRaCIS.Core.Application.Service Action = async () => { - Log.Logger.Warning($" {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}开启另外线程压缩访视:{visitItem.SubjectCode} {visitItem.VisitName} {visitItem.VisitNum}"); + Log.Logger.Warning($"[{visitIndex}] {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}开启另外线程压缩访视:{visitItem.SubjectCode} {visitItem.VisitName} {visitItem.VisitNum}"); string zipPath = visitFolderPath + ".zip"; @@ -537,7 +539,7 @@ namespace IRaCIS.Core.Application.Service Log.Logger.Warning($"{downloadVisits.Count}个访视信息核对准备完毕, 跳过{skipCount} 个,后端开始下载任务......"); - + #region 异步方式处理 int totalCount = downloadJobs.Count; From 99b6c63de76ddf5949612974653ddd57cee344a0 Mon Sep 17 00:00:00 2001 From: hang <872297557@qq.com> Date: Wed, 3 Jun 2026 15:33:03 +0800 Subject: [PATCH 4/7] =?UTF-8?q?=E5=BC=80=E5=90=AF=E5=8E=8B=E7=BC=A9?= =?UTF-8?q?=E5=8C=85=E4=BB=BB=E5=8A=A1=E7=B4=A2=E5=BC=95=E5=A2=9E=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Service/Common/TrialImageDownloadService.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/IRaCIS.Core.Application/Service/Common/TrialImageDownloadService.cs b/IRaCIS.Core.Application/Service/Common/TrialImageDownloadService.cs index 971eef75e..cc1d910ce 100644 --- a/IRaCIS.Core.Application/Service/Common/TrialImageDownloadService.cs +++ b/IRaCIS.Core.Application/Service/Common/TrialImageDownloadService.cs @@ -479,6 +479,8 @@ namespace IRaCIS.Core.Application.Service //建立压缩包 string visitFolderPath = Path.Combine(trialFolderPath, $"{visitItem.SubjectCode}_{visitItem.VisitName.Trim()}"); + var currentIndex = visitIndex; + downloadJobs.Add(new DownloadJob() { Name = $"{visitItem.SubjectCode}_{visitItem.VisitName.Trim()}_Zip", @@ -487,7 +489,7 @@ namespace IRaCIS.Core.Application.Service Action = async () => { - Log.Logger.Warning($"[{visitIndex}] {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}开启另外线程压缩访视:{visitItem.SubjectCode} {visitItem.VisitName} {visitItem.VisitNum}"); + Log.Logger.Warning($"[{currentIndex}] {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}开启另外线程压缩访视:{visitItem.SubjectCode} {visitItem.VisitName} {visitItem.VisitNum}"); string zipPath = visitFolderPath + ".zip"; From bb3f61277213001882c908213364fc038b6a0b35 Mon Sep 17 00:00:00 2001 From: hang <872297557@qq.com> Date: Wed, 3 Jun 2026 17:44:31 +0800 Subject: [PATCH 5/7] =?UTF-8?q?=E4=B8=B4=E6=97=B6token=20=E7=BB=AD?= =?UTF-8?q?=E6=9C=9F=E4=BB=A5=E6=9C=8D=E5=8A=A1=E5=99=A8=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E6=AF=94=E8=BE=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Helper/DicomDIRHelper.cs | 16 +- IRaCIS.Core.Application/Helper/OSSService.cs | 6 +- .../Common/TrialImageDownloadService.cs | 355 ++++++++++++++++++ 3 files changed, 371 insertions(+), 6 deletions(-) diff --git a/IRaCIS.Core.Application/Helper/DicomDIRHelper.cs b/IRaCIS.Core.Application/Helper/DicomDIRHelper.cs index dd3b7c140..f41bd13cd 100644 --- a/IRaCIS.Core.Application/Helper/DicomDIRHelper.cs +++ b/IRaCIS.Core.Application/Helper/DicomDIRHelper.cs @@ -3,6 +3,7 @@ using FellowOakDicom; using FellowOakDicom.Media; using IRaCIS.Core.Application.ViewModel; using IRaCIS.Core.Domain.Models; +using NPOI.Util; using System; using System.Collections.Generic; using System.Data; @@ -160,7 +161,7 @@ namespace IRaCIS.Core.Application.Helper } - public static async Task GenerateStudyDIR(List list, Dictionary dic,string dirSavePath) + public static async Task GenerateStudyDIR(List list, Dictionary dic, string? dirSavePath = null, Stream? outputStream = null) { var mappings = new List(); int index = 1; @@ -228,9 +229,18 @@ namespace IRaCIS.Core.Application.Helper //有实际的文件 if (mappings.Count > 0) - { + { // 保存 DICOMDIR 到临时文件 不能直接写入到流种 - await dicomDir.SaveAsync(dirSavePath); + + if (dirSavePath.IsNotNullOrEmpty()) + { + await dicomDir.SaveAsync(dirSavePath); + + } + else + { + await dicomDir.SaveAsync(outputStream); + } } } diff --git a/IRaCIS.Core.Application/Helper/OSSService.cs b/IRaCIS.Core.Application/Helper/OSSService.cs index 1114481db..57c28727c 100644 --- a/IRaCIS.Core.Application/Helper/OSSService.cs +++ b/IRaCIS.Core.Application/Helper/OSSService.cs @@ -948,7 +948,7 @@ public class OSSService(IOptionsMonitor options, lock (_tokenLock) { if (AliyunOSSTempToken == null || - AliyunOSSTempToken.Expiration <= DateTime.UtcNow.AddMinutes(15)) + AliyunOSSTempToken.Expiration <= DateTime.Now.AddMinutes(15)) { GetObjectStoreTempToken(); @@ -961,7 +961,7 @@ public class OSSService(IOptionsMonitor options, { - if (AWSTempToken != null && AWSTempToken.Expiration > DateTime.UtcNow.AddMinutes(15)) + if (AWSTempToken != null && AWSTempToken.Expiration > DateTime.Now.AddMinutes(15)) { return; } @@ -969,7 +969,7 @@ public class OSSService(IOptionsMonitor options, lock (_tokenLock) { if (AWSTempToken == null || - AWSTempToken.Expiration <= DateTime.UtcNow.AddMinutes(15)) + AWSTempToken.Expiration <= DateTime.Now.AddMinutes(15)) { GetObjectStoreTempToken(); diff --git a/IRaCIS.Core.Application/Service/Common/TrialImageDownloadService.cs b/IRaCIS.Core.Application/Service/Common/TrialImageDownloadService.cs index cc1d910ce..bba480b31 100644 --- a/IRaCIS.Core.Application/Service/Common/TrialImageDownloadService.cs +++ b/IRaCIS.Core.Application/Service/Common/TrialImageDownloadService.cs @@ -20,6 +20,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; using MiniExcelLibs; +using NPOI.Util; using Org.BouncyCastle.Utilities.Zlib; using SharpCompress.Common; using System; @@ -130,6 +131,358 @@ namespace IRaCIS.Core.Application.Service } + #region zip 流方式 直接下载 + + + public sealed class ZipItem + { + public string ZipEntryPath { get; set; } = ""; + + public string? OssPath { get; set; } + + public bool IsEncapsulated { get; set; } + + public Func? CustomWriter { get; set; } + } + + + private async Task CreateVisitZipAsync(string zipPath, List zipItems) + { + Directory.CreateDirectory( + Path.GetDirectoryName(zipPath)!); + + await using var zipFileStream = + new FileStream(zipPath, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 4 * 1024 * 1024, useAsync: true); + + using var archive = new ZipArchive(zipFileStream, ZipArchiveMode.Create, leaveOpen: false); + + foreach (var item in zipItems) + { + var entry = archive.CreateEntry(item.ZipEntryPath.Replace("\\", "/"), CompressionLevel.NoCompression); + + await using var entryStream = entry.Open(); + + if (item.CustomWriter != null) + { + await item.CustomWriter(entryStream); + continue; + } + + if (string.IsNullOrWhiteSpace(item.OssPath)) + continue; + + if (item.IsEncapsulated) + { + var success = await TryWriteMergedDicomAsync(() => _oSSService.GetStreamFromOSSAsync(item.OssPath), entryStream); + + if (!success) + { + Log.Logger.Warning($"合并多帧失败:{item.ZipEntryPath}"); + } + + } + else + { + await using var ossStream = await _oSSService.GetStreamFromOSSAsync(item.OssPath); + + await ossStream.CopyToAsync(entryStream, 4 * 1024 * 1024); + } + + + } + } + + //查询一个访视 + // ↓ + //生成 DICOMDIR + // ↓ + //创建 ZipArchive + // ↓ + //OSS流直接写 ZipEntry + // ↓ + //完成一个访视.zip + // ↓ + //记录日志 + // ↓ + //处理下一个访视 + + [HttpPost] + [AllowAnonymous] + public async Task DownloadExcelTrialImageZIPStream(Guid trialId) + { + var trialInfo = _trialRepository.Where(t => t.Id == trialId).Select(t => new { t.ResearchProgramNo }).FirstOrDefault(); + + #region 设置目录 + + var rootFolder = FileStoreHelper.GetIRaCISRootDataFolder(_hostEnvironment); + Directory.CreateDirectory(rootFolder); + + // 获取无效字符(系统定义的) + string invalidChars = new string(Path.GetInvalidFileNameChars()) + new string(Path.GetInvalidPathChars()); + + // 用正则表达式替换所有非法字符为下划线或空字符 + string pattern = $"[{Regex.Escape(invalidChars)}]"; + + var regexNo = Regex.Replace(trialInfo.ResearchProgramNo, pattern, "_"); + + // 创建一个临时文件夹来存放文件 + string trialFolderPath = Path.Combine(rootFolder, $"{regexNo}");//_{NewId.NextGuid()} + Directory.CreateDirectory(trialFolderPath); + + #endregion + + var oldVisits = MiniExcel.Query(Path.Combine(rootFolder, "Old.xlsx")).ToList(); + + var downloadVisits = MiniExcel.Query(Path.Combine(rootFolder, "download.xlsx")).ToList().Where(t => t.SubjectCode.IsNotNullOrEmpty() && t.VisitName.IsNotNullOrEmpty()).ToList(); + + downloadVisits = downloadVisits.Where(t => !oldVisits.Any(old => old.VisitNum == t.VisitNum && old.SubjectCode == t.SubjectCode && + old.VisitName.Trim().ToLower() == t.VisitName.Trim().ToLower())).ToList(); + + var visitIndex = 0; + var skipCount = 0; + foreach (var downloadVisit in downloadVisits) + { + var downloadInfo = _trialRepository.Where(t => t.Id == trialId).Select(t => new + { + t.ResearchProgramNo, + t.TrialCode, + + VisitList = t.SubjectVisitList.Where(t => t.VisitName.Trim() == downloadVisit.VisitName.Trim() && t.Subject.Code.Trim() == downloadVisit.SubjectCode.Trim() && t.VisitNum == downloadVisit.VisitNum) + .Select(sv => new + { + SubjectVisitId = sv.Id, + TrialSiteCode = sv.TrialSite.TrialSiteCode, + SubjectCode = sv.Subject.Code, + VisitName = sv.VisitName, + VisitNum = sv.VisitNum, + StudyList = sv.StudyList.Select(u => new + { + StudyId = u.Id, + u.PatientId, + u.StudyTime, + u.StudyCode, + u.StudyInstanceUid, + u.StudyDIRPath, + + SeriesList = u.SeriesList.Where(t => t.IsReading).Select(z => new + { + z.Modality, + + InstancePathList = z.DicomInstanceList.Where(t => t.IsReading).Select(k => new + { + InstanceId = k.Id, + k.Path, + k.IsEncapsulated, + k.NumberOfFrames, + }).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() + }).OrderBy(t => t.SubjectCode).ThenBy(t => t.VisitNum).ToList() + + }).FirstOrDefault(); + + if (downloadInfo == null) + { + return ResponseOutput.Ok(); + } + + #region 排除已经下载的 + + var logFilePath = Path.Combine(rootFolder, $"{trialId}_{regexNo}_download_log.csv"); + + if (File.Exists(logFilePath)) + { + var existVisits = MiniExcel.Query(logFilePath, configuration: new MiniExcelLibs.Csv.CsvConfiguration() + { + StreamReaderFunc = (stream) => new StreamReader(stream, Encoding.GetEncoding("gb2312")) + }).ToList(); + + if (existVisits.Any(old => old.VisitNum == downloadVisit.VisitNum && old.SubjectCode == downloadVisit.SubjectCode && + old.VisitName.Trim().ToLower() == downloadVisit.VisitName.Trim().ToLower())) + { + Log.Logger.Warning($"[{visitIndex}] Excel显示已下载,跳过当前访视:{downloadVisit.SubjectCode} {downloadVisit.VisitName} {downloadVisit.VisitNum}"); + skipCount++; + continue; + } + + } + + + #endregion + + foreach (var visitItem in downloadInfo.VisitList) + { + if (visitItem.StudyList.Count() == 0 && visitItem.NoneDicomStudyList.Count() == 0) + { + Log.Logger.Warning($"[{visitIndex}]查询无检查,跳过当前访视:{downloadVisit.SubjectCode} {downloadVisit.VisitName} {downloadVisit.VisitNum}"); + skipCount++; + continue; + } + + try + { + #region 导出访视 + + var zipItems = new List(); + + var visitFolderName = $"{visitItem.SubjectCode}_{visitItem.VisitName.Trim()}"; + + foreach (var studyInfo in visitItem.StudyList) + { + var dirDic = new Dictionary(); + + var studyFolderName = $"{studyInfo.StudyCode}_{studyInfo.StudyTime:yyyy-MM-dd}_{string.Join('_', studyInfo.SeriesList.Select(t => t.Modality))}"; + + #region DICOMDIR + + + if (!_instanceRepository.Where(t => t.IsReading && t.DicomSerie.IsReading) + .Where(t => visitItem.SubjectVisitId == t.SubjectVisitId).Any(c => c.TransferSytaxUID == string.Empty)) + { + var list = _subjectVisitRepository.Where(t => t.Id == visitItem.SubjectVisitId).SelectMany(t => t.StudyList) + .SelectMany(t => t.InstanceList.Where(t => t.IsReading && t.DicomSerie.IsReading && t.StudyId == studyInfo.StudyId)) + .Select(t => new StudyDIRInfo() + { + + DicomStudyId = t.DicomStudy.Id, + + PatientId = downloadInfo.TrialCode + "-" + t.DicomStudy.Subject.Code, + PatientName = t.DicomStudy.PatientName, + PatientBirthDate = t.DicomStudy.PatientBirthDate, + PatientSex = t.DicomStudy.PatientSex, + + StudyInstanceUid = t.StudyInstanceUid, + StudyId = t.DicomStudy.StudyId, + DicomStudyDate = t.DicomStudy.DicomStudyDate, + DicomStudyTime = t.DicomStudy.DicomStudyTime, + AccessionNumber = t.DicomStudy.AccessionNumber, + + StudyDescription = t.DicomStudy.Description, + + SeriesInstanceUid = t.DicomSerie.SeriesInstanceUid, + Modality = t.DicomSerie.Modality, + DicomSeriesDate = t.DicomSerie.DicomSeriesDate, + DicomSeriesTime = t.DicomSerie.DicomSeriesTime, + SeriesNumber = t.DicomSerie.SeriesNumber, + SeriesDescription = t.DicomSerie.Description, + + InstanceId = t.Id, + SopInstanceUid = t.SopInstanceUid, + SOPClassUID = t.SOPClassUID, + InstanceNumber = t.InstanceNumber, + MediaStorageSOPClassUID = t.MediaStorageSOPClassUID, + MediaStorageSOPInstanceUID = t.MediaStorageSOPInstanceUID, + TransferSytaxUID = t.TransferSytaxUID, + + }).ToList(); + + foreach (var group in list.GroupBy(t => new { t.StudyInstanceUid, t.DicomStudyId })) + { + + + zipItems.Add(new ZipItem + { + ZipEntryPath = $"{visitFolderName}/{studyFolderName}/DICOMDIR", + + CustomWriter = async entryStream => + { + await DicomDIRHelper.GenerateStudyDIR(group.ToList(), dirDic, outputStream: entryStream); + } + }); + } + + } + + + #endregion + + + #region DICOM + + foreach (var seriesInfo in studyInfo.SeriesList) + { + foreach (var instanceInfo in seriesInfo.InstancePathList) + { + zipItems.Add(new ZipItem + { + OssPath = instanceInfo.Path, + + IsEncapsulated = instanceInfo.IsEncapsulated, + + ZipEntryPath = $"{visitFolderName}/" + $"{studyFolderName}/" + $"IMAGE/" + $"{dirDic[instanceInfo.InstanceId.ToString()]}" + }); + } + } + + #endregion + + + } + + #region NoneDicom + + foreach (var study in visitItem.NoneDicomStudyList) + { + var studyFolderName = $"{study.StudyCode}_" + $"{study.ImageDate:yyyy-MM-dd}_" + $"{study.Modality}"; + + foreach (var file in study.FileList) + { + zipItems.Add(new ZipItem + { + OssPath = HttpUtility.UrlDecode(file.Path), + + ZipEntryPath = $"{visitFolderName}/" + $"{studyFolderName}/" + $"{Path.GetFileName(file.FileName)}" + }); + } + } + + #endregion + + var zipPath = Path.Combine(rootFolder, visitFolderName + ".zip"); + + Log.Logger.Warning($"开始压缩访视:{visitFolderName}"); + + await CreateVisitZipAsync(zipPath, zipItems); + + DownloadLogger.Write(logFilePath, visitItem.SubjectCode, visitItem.VisitNum, visitItem.VisitName, "Success"); + + Log.Logger.Warning($"访视压缩完成:{visitFolderName}"); + + #endregion + + } + + catch (Exception ex) + { + Log.Logger.Error(ex, $"导出失败:{downloadVisit.SubjectCode} {downloadVisit.VisitName} {downloadVisit.VisitNum}"); + } + + + } + } + + return ResponseOutput.Ok(); + } + + #endregion + + + #region Excel 下载 先下载,然后再压缩,删除方式 public static class DownloadLogger { @@ -1000,6 +1353,8 @@ namespace IRaCIS.Core.Application.Service } + #endregion + /// /// 下载影像 维护dir信息 并回传到OSS From 73fa62e6a2ac0785c1afb539844e5fc4fec76244 Mon Sep 17 00:00:00 2001 From: hang <872297557@qq.com> Date: Wed, 3 Jun 2026 17:56:07 +0800 Subject: [PATCH 6/7] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=BB=AD=E6=9C=9F?= =?UTF-8?q?=E5=A4=87=E6=B3=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- IRaCIS.Core.Application/Helper/OSSService.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/IRaCIS.Core.Application/Helper/OSSService.cs b/IRaCIS.Core.Application/Helper/OSSService.cs index 57c28727c..93f96b3f2 100644 --- a/IRaCIS.Core.Application/Helper/OSSService.cs +++ b/IRaCIS.Core.Application/Helper/OSSService.cs @@ -936,13 +936,13 @@ public class OSSService(IOptionsMonitor options, { - + // 过期时间 ≤ 当前时间 + 15分钟 时需要续期 if (ObjectStoreServiceOptions.ObjectStoreUse == "AliyunOSS") { - if (AliyunOSSTempToken != null && AliyunOSSTempToken.Expiration > DateTime.UtcNow.AddMinutes(15)) + if (AliyunOSSTempToken != null && AliyunOSSTempToken.Expiration > DateTime.Now.AddMinutes(15)) { - return; + return; // 还有15分钟以上,不需要续期 } lock (_tokenLock) From d719015fa2e28a6792192697eac7a5472aee333c Mon Sep 17 00:00:00 2001 From: hang <872297557@qq.com> Date: Thu, 4 Jun 2026 13:08:37 +0800 Subject: [PATCH 7/7] =?UTF-8?q?zip=20=E6=B5=81=E6=96=B9=E5=BC=8Fdemo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Common/TrialImageDownloadService.cs | 88 +++++++++++++------ IRaCIS.Core.Application/TestService.cs | 23 ----- 2 files changed, 62 insertions(+), 49 deletions(-) diff --git a/IRaCIS.Core.Application/Service/Common/TrialImageDownloadService.cs b/IRaCIS.Core.Application/Service/Common/TrialImageDownloadService.cs index bba480b31..5bf9183b6 100644 --- a/IRaCIS.Core.Application/Service/Common/TrialImageDownloadService.cs +++ b/IRaCIS.Core.Application/Service/Common/TrialImageDownloadService.cs @@ -1,4 +1,5 @@ using Aliyun.OSS; +using CommunityToolkit.HighPerformance; using DocumentFormat.OpenXml.EMMA; using DocumentFormat.OpenXml.Office.CustomUI; using DocumentFormat.OpenXml.Office2010.Excel; @@ -148,11 +149,9 @@ namespace IRaCIS.Core.Application.Service private async Task CreateVisitZipAsync(string zipPath, List zipItems) { - Directory.CreateDirectory( - Path.GetDirectoryName(zipPath)!); + Directory.CreateDirectory(Path.GetDirectoryName(zipPath)!); - await using var zipFileStream = - new FileStream(zipPath, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 4 * 1024 * 1024, useAsync: true); + await using var zipFileStream = new FileStream(zipPath, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 4 * 1024 * 1024, useAsync: true); using var archive = new ZipArchive(zipFileStream, ZipArchiveMode.Create, leaveOpen: false); @@ -177,7 +176,9 @@ namespace IRaCIS.Core.Application.Service if (!success) { - Log.Logger.Warning($"合并多帧失败:{item.ZipEntryPath}"); + Log.Logger.Warning($"合并多帧失败:{item.ZipEntryPath} ossPath: {item.OssPath}"); + + throw new Exception($"合并多帧失败-终止当前zip包:{item.ZipEntryPath} ossPath: {item.OssPath}"); } } @@ -235,8 +236,7 @@ namespace IRaCIS.Core.Application.Service var downloadVisits = MiniExcel.Query(Path.Combine(rootFolder, "download.xlsx")).ToList().Where(t => t.SubjectCode.IsNotNullOrEmpty() && t.VisitName.IsNotNullOrEmpty()).ToList(); - downloadVisits = downloadVisits.Where(t => !oldVisits.Any(old => old.VisitNum == t.VisitNum && old.SubjectCode == t.SubjectCode && - old.VisitName.Trim().ToLower() == t.VisitName.Trim().ToLower())).ToList(); + downloadVisits = downloadVisits.Where(t => !oldVisits.Any(old => old.VisitNum == t.VisitNum && old.SubjectCode == t.SubjectCode && old.VisitName.Trim().ToLower() == t.VisitName.Trim().ToLower())).ToList(); var visitIndex = 0; var skipCount = 0; @@ -298,7 +298,7 @@ namespace IRaCIS.Core.Application.Service if (downloadInfo == null) { - return ResponseOutput.Ok(); + continue; } #region 排除已经下载的 @@ -312,8 +312,7 @@ namespace IRaCIS.Core.Application.Service StreamReaderFunc = (stream) => new StreamReader(stream, Encoding.GetEncoding("gb2312")) }).ToList(); - if (existVisits.Any(old => old.VisitNum == downloadVisit.VisitNum && old.SubjectCode == downloadVisit.SubjectCode && - old.VisitName.Trim().ToLower() == downloadVisit.VisitName.Trim().ToLower())) + if (existVisits.Any(old => old.VisitNum == downloadVisit.VisitNum && old.SubjectCode == downloadVisit.SubjectCode && old.VisitName.Trim().ToLower() == downloadVisit.VisitName.Trim().ToLower())) { Log.Logger.Warning($"[{visitIndex}] Excel显示已下载,跳过当前访视:{downloadVisit.SubjectCode} {downloadVisit.VisitName} {downloadVisit.VisitNum}"); skipCount++; @@ -342,6 +341,8 @@ namespace IRaCIS.Core.Application.Service var visitFolderName = $"{visitItem.SubjectCode}_{visitItem.VisitName.Trim()}"; + #region DICOM + foreach (var studyInfo in visitItem.StudyList) { var dirDic = new Dictionary(); @@ -394,6 +395,11 @@ namespace IRaCIS.Core.Application.Service foreach (var group in list.GroupBy(t => new { t.StudyInstanceUid, t.DicomStudyId })) { + using var ms = new MemoryStream(); + + await DicomDIRHelper.GenerateStudyDIR(group.ToList(), dirDic, outputStream: ms); + + var dicomDirBytes = ms.ToArray(); zipItems.Add(new ZipItem { @@ -401,7 +407,7 @@ namespace IRaCIS.Core.Application.Service CustomWriter = async entryStream => { - await DicomDIRHelper.GenerateStudyDIR(group.ToList(), dirDic, outputStream: entryStream); + await entryStream.WriteAsync(dicomDirBytes); } }); } @@ -412,7 +418,7 @@ namespace IRaCIS.Core.Application.Service #endregion - #region DICOM + foreach (var seriesInfo in studyInfo.SeriesList) { @@ -429,11 +435,13 @@ namespace IRaCIS.Core.Application.Service } } - #endregion } + #endregion + + #region NoneDicom foreach (var study in visitItem.NoneDicomStudyList) @@ -453,15 +461,40 @@ namespace IRaCIS.Core.Application.Service #endregion - var zipPath = Path.Combine(rootFolder, visitFolderName + ".zip"); - Log.Logger.Warning($"开始压缩访视:{visitFolderName}"); + #region zip - await CreateVisitZipAsync(zipPath, zipItems); - DownloadLogger.Write(logFilePath, visitItem.SubjectCode, visitItem.VisitNum, visitItem.VisitName, "Success"); + var zipPath = Path.Combine(trialFolderPath, visitFolderName + ".zip"); + + Log.Logger.Warning($"[{visitIndex}] {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}开始打包下载访视:{visitFolderName}"); + + try + { + await CreateVisitZipAsync(zipPath, zipItems); + + //Log.Logger.Warning($"zip exists={File.Exists(zipPath)} size={new FileInfo(zipPath).Length}"); + + Log.Logger.Warning($"[{visitIndex}] {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}访视打包下载完成:{visitFolderName}"); + + DownloadLogger.Write(logFilePath, visitItem.SubjectCode, visitItem.VisitNum, visitItem.VisitName, "Success"); + + } + catch (Exception ex) + { + Log.Logger.Warning($"出现异常{ex}删除压缩包:{visitFolderName}"); + //如果有异常,删除失败的压缩包 + if (File.Exists(zipPath)) + { + File.Delete(zipPath); + } + + } + + + #endregion + - Log.Logger.Warning($"访视压缩完成:{visitFolderName}"); #endregion @@ -486,8 +519,6 @@ namespace IRaCIS.Core.Application.Service public static class DownloadLogger { - - public static void Write( string logFilePath, string subjectCode, @@ -495,27 +526,34 @@ namespace IRaCIS.Core.Application.Service string visitName, string? message = null) { - bool fileExists = File.Exists(logFilePath); + // 一次性打开文件流 using var stream = new FileStream( logFilePath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite); - using var writer = new StreamWriter(stream, Encoding.UTF8); + // 首次创建时写入 BOM + if (!fileExists) + { + var bom = new UTF8Encoding(true).GetPreamble(); + stream.Write(bom, 0, bom.Length); + } + using var writer = new StreamWriter(stream, new UTF8Encoding(false)); - // 首次写入表头 + // 写入表头 if (!fileExists) { writer.WriteLine("Time,SubjectCode,VisitNum,VisitName,Message"); } + // 写入数据 string line = string.Join(",", [ - Escape(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")), + Escape(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")), subjectCode, visitNum, visitName, @@ -525,14 +563,12 @@ namespace IRaCIS.Core.Application.Service writer.WriteLine(line); } - // 防止逗号、换行导致 CSV 错乱 private static string Escape(string? value) { if (string.IsNullOrEmpty(value)) return ""; value = value.Replace("\"", "\"\""); - return $"\"{value}\""; } } diff --git a/IRaCIS.Core.Application/TestService.cs b/IRaCIS.Core.Application/TestService.cs index 34e6f3364..83ca9053e 100644 --- a/IRaCIS.Core.Application/TestService.cs +++ b/IRaCIS.Core.Application/TestService.cs @@ -1,30 +1,17 @@ using Aliyun.OSS; -using DocumentFormat.OpenXml.Spreadsheet; using FellowOakDicom; using FellowOakDicom.Imaging; using IRaCIS.Application.Contracts; -using IRaCIS.Core.Application.BusinessFilter; using IRaCIS.Core.Application.Contracts; using IRaCIS.Core.Application.Helper; -using IRaCIS.Core.Application.Helper.OtherTool; -using IRaCIS.Core.Application.Service.BusinessFilter; using IRaCIS.Core.Application.ViewModel; -using IRaCIS.Core.Domain; -using IRaCIS.Core.Domain.Models; -using IRaCIS.Core.Domain.Share; -using IRaCIS.Core.Infra.EFCore; using IRaCIS.Core.Infra.EFCore.Context; using IRaCIS.Core.Infrastructure; using IRaCIS.Core.Infrastructure.Encryption; using IRaCIS.Core.Infrastructure.NewtonsoftJson; using MassTransit; -using MassTransit.Caching.Internals; -using MassTransit.Mediator; -using MathNet.Numerics; -using MaxMind.GeoIP2; using Medallion.Threading; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -32,25 +19,15 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using MiniExcelLibs; -using Minio.DataModel; using Newtonsoft.Json; -using NPOI.SS.Formula.Functions; using NPOI.XWPF.UserModel; -using SharpCompress.Common; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Processing; -using System.Collections.Concurrent; using System.ComponentModel.DataAnnotations; -using System.Diagnostics; -using System.Globalization; -using System.IO; using System.Linq.Dynamic.Core; -using System.Reactive.Subjects; -using System.Reflection.Metadata.Ecma335; using System.Runtime.InteropServices; using System.Text; -using static IRaCIS.Core.Domain.Share.StaticData;