Merge branch 'Test_IRC_Net8' of https://gitea.frp.extimaging.com/XCKJ/irc-netcore-api into Test_IRC_Net8
continuous-integration/drone/push Build is failing
Details
continuous-integration/drone/push Build is failing
Details
commit
8e2d04808f
|
|
@ -3,6 +3,7 @@ using FellowOakDicom;
|
||||||
using FellowOakDicom.Media;
|
using FellowOakDicom.Media;
|
||||||
using IRaCIS.Core.Application.ViewModel;
|
using IRaCIS.Core.Application.ViewModel;
|
||||||
using IRaCIS.Core.Domain.Models;
|
using IRaCIS.Core.Domain.Models;
|
||||||
|
using NPOI.Util;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
|
|
@ -160,7 +161,7 @@ namespace IRaCIS.Core.Application.Helper
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static async Task GenerateStudyDIR(List<StudyDIRInfo> list, Dictionary<string, string> dic,string dirSavePath)
|
public static async Task GenerateStudyDIR(List<StudyDIRInfo> list, Dictionary<string, string> dic, string? dirSavePath = null, Stream? outputStream = null)
|
||||||
{
|
{
|
||||||
var mappings = new List<string>();
|
var mappings = new List<string>();
|
||||||
int index = 1;
|
int index = 1;
|
||||||
|
|
@ -228,9 +229,18 @@ namespace IRaCIS.Core.Application.Helper
|
||||||
|
|
||||||
//有实际的文件
|
//有实际的文件
|
||||||
if (mappings.Count > 0)
|
if (mappings.Count > 0)
|
||||||
{
|
{
|
||||||
// 保存 DICOMDIR 到临时文件 不能直接写入到流种
|
// 保存 DICOMDIR 到临时文件 不能直接写入到流种
|
||||||
await dicomDir.SaveAsync(dirSavePath);
|
|
||||||
|
if (dirSavePath.IsNotNullOrEmpty())
|
||||||
|
{
|
||||||
|
await dicomDir.SaveAsync(dirSavePath);
|
||||||
|
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await dicomDir.SaveAsync(outputStream);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -220,7 +220,7 @@ public class OSSService(IOptionsMonitor<ObjectStoreServiceOptions> options,
|
||||||
public object result { get; private set; }
|
public object result { get; private set; }
|
||||||
|
|
||||||
|
|
||||||
|
private static readonly object _tokenLock = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 将指定前缀下的所有现有文件立即转为目标存储类型
|
/// 将指定前缀下的所有现有文件立即转为目标存储类型
|
||||||
|
|
@ -934,32 +934,52 @@ public class OSSService(IOptionsMonitor<ObjectStoreServiceOptions> options,
|
||||||
//后端批量上传 或者下载,不每个文件获取临时token
|
//后端批量上传 或者下载,不每个文件获取临时token
|
||||||
private void BackBatchGetToken()
|
private void BackBatchGetToken()
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|
||||||
|
// 过期时间 ≤ 当前时间 + 15分钟 时需要续期
|
||||||
|
|
||||||
if (ObjectStoreServiceOptions.ObjectStoreUse == "AliyunOSS")
|
if (ObjectStoreServiceOptions.ObjectStoreUse == "AliyunOSS")
|
||||||
{
|
{
|
||||||
if (AliyunOSSTempToken == null)
|
if (AliyunOSSTempToken != null && AliyunOSSTempToken.Expiration > DateTime.Now.AddMinutes(15))
|
||||||
{
|
{
|
||||||
GetObjectStoreTempToken();
|
return; // 还有15分钟以上,不需要续期
|
||||||
}
|
|
||||||
//token 过期了
|
|
||||||
if (AliyunOSSTempToken?.Expiration.AddSeconds(10) <= DateTime.Now)
|
|
||||||
{
|
|
||||||
GetObjectStoreTempToken();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lock (_tokenLock)
|
||||||
|
{
|
||||||
|
if (AliyunOSSTempToken == null ||
|
||||||
|
AliyunOSSTempToken.Expiration <= DateTime.Now.AddMinutes(15))
|
||||||
|
{
|
||||||
|
GetObjectStoreTempToken();
|
||||||
|
|
||||||
|
Log.Logger.Warning("后端获取阿里云临时 Token");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
else if (ObjectStoreServiceOptions.ObjectStoreUse == "AWS")
|
else if (ObjectStoreServiceOptions.ObjectStoreUse == "AWS")
|
||||||
{
|
{
|
||||||
if (AWSTempToken == null)
|
|
||||||
|
|
||||||
|
if (AWSTempToken != null && AWSTempToken.Expiration > DateTime.Now.AddMinutes(15))
|
||||||
{
|
{
|
||||||
GetObjectStoreTempToken();
|
return;
|
||||||
}
|
}
|
||||||
//token 过期了
|
|
||||||
if (AWSTempToken.Expiration?.AddSeconds(10) <= DateTime.Now)
|
lock (_tokenLock)
|
||||||
{
|
{
|
||||||
GetObjectStoreTempToken();
|
if (AWSTempToken == null ||
|
||||||
|
AWSTempToken.Expiration <= DateTime.Now.AddMinutes(15))
|
||||||
|
{
|
||||||
|
GetObjectStoreTempToken();
|
||||||
|
|
||||||
|
Log.Logger.Warning("后端获取s3 临时 Token");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using Aliyun.OSS;
|
using Aliyun.OSS;
|
||||||
|
using CommunityToolkit.HighPerformance;
|
||||||
using DocumentFormat.OpenXml.EMMA;
|
using DocumentFormat.OpenXml.EMMA;
|
||||||
using DocumentFormat.OpenXml.Office.CustomUI;
|
using DocumentFormat.OpenXml.Office.CustomUI;
|
||||||
using DocumentFormat.OpenXml.Office2010.Excel;
|
using DocumentFormat.OpenXml.Office2010.Excel;
|
||||||
|
|
@ -20,6 +21,7 @@ using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using MiniExcelLibs;
|
using MiniExcelLibs;
|
||||||
|
using NPOI.Util;
|
||||||
using Org.BouncyCastle.Utilities.Zlib;
|
using Org.BouncyCastle.Utilities.Zlib;
|
||||||
using SharpCompress.Common;
|
using SharpCompress.Common;
|
||||||
using System;
|
using System;
|
||||||
|
|
@ -60,6 +62,14 @@ namespace IRaCIS.Core.Application.Service
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
||||||
|
#region 方式一 有的必须在内存中,不能用这种
|
||||||
|
//await using var source = await sourceFactory();
|
||||||
|
//// 如果你是从 stream 打开
|
||||||
|
//var dicomFile = await DicomFile.OpenAsync(source);
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 方式二
|
||||||
await using var source = await sourceFactory();
|
await using var source = await sourceFactory();
|
||||||
|
|
||||||
// 【关键修复】将 OSS 流缓冲到 MemoryStream
|
// 【关键修复】将 OSS 流缓冲到 MemoryStream
|
||||||
|
|
@ -79,6 +89,8 @@ namespace IRaCIS.Core.Application.Service
|
||||||
|
|
||||||
// 如果你是从 stream 打开
|
// 如果你是从 stream 打开
|
||||||
var dicomFile = await DicomFile.OpenAsync(source.CanSeek ? source : bufferedStream);
|
var dicomFile = await DicomFile.OpenAsync(source.CanSeek ? source : bufferedStream);
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
//获取像素是否为封装形式
|
//获取像素是否为封装形式
|
||||||
var syntax = dicomFile.Dataset.InternalTransferSyntax;
|
var syntax = dicomFile.Dataset.InternalTransferSyntax;
|
||||||
|
|
@ -120,11 +132,393 @@ 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<Stream, Task>? CustomWriter { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private async Task CreateVisitZipAsync(string zipPath, List<ZipItem> 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} ossPath: {item.OssPath}");
|
||||||
|
|
||||||
|
throw new Exception($"合并多帧失败-终止当前zip包:{item.ZipEntryPath} ossPath: {item.OssPath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
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<IResponseOutput> 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<SubjectVisitExcel>(Path.Combine(rootFolder, "Old.xlsx")).ToList();
|
||||||
|
|
||||||
|
var downloadVisits = MiniExcel.Query<SubjectVisitExcel>(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)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region 排除已经下载的
|
||||||
|
|
||||||
|
var logFilePath = Path.Combine(rootFolder, $"{trialId}_{regexNo}_download_log.csv");
|
||||||
|
|
||||||
|
if (File.Exists(logFilePath))
|
||||||
|
{
|
||||||
|
var existVisits = MiniExcel.Query<SubjectVisitExcel>(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<ZipItem>();
|
||||||
|
|
||||||
|
var visitFolderName = $"{visitItem.SubjectCode}_{visitItem.VisitName.Trim()}";
|
||||||
|
|
||||||
|
#region DICOM
|
||||||
|
|
||||||
|
foreach (var studyInfo in visitItem.StudyList)
|
||||||
|
{
|
||||||
|
var dirDic = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
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 }))
|
||||||
|
{
|
||||||
|
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
|
||||||
|
await DicomDIRHelper.GenerateStudyDIR(group.ToList(), dirDic, outputStream: ms);
|
||||||
|
|
||||||
|
var dicomDirBytes = ms.ToArray();
|
||||||
|
|
||||||
|
zipItems.Add(new ZipItem
|
||||||
|
{
|
||||||
|
ZipEntryPath = $"{visitFolderName}/{studyFolderName}/DICOMDIR",
|
||||||
|
|
||||||
|
CustomWriter = async entryStream =>
|
||||||
|
{
|
||||||
|
await entryStream.WriteAsync(dicomDirBytes);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
#region zip
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Logger.Error(ex, $"导出失败:{downloadVisit.SubjectCode} {downloadVisit.VisitName} {downloadVisit.VisitNum}");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseOutput.Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
#region Excel 下载 先下载,然后再压缩,删除方式
|
||||||
|
|
||||||
public static class DownloadLogger
|
public static class DownloadLogger
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|
||||||
public static void Write(
|
public static void Write(
|
||||||
string logFilePath,
|
string logFilePath,
|
||||||
string subjectCode,
|
string subjectCode,
|
||||||
|
|
@ -132,27 +526,34 @@ namespace IRaCIS.Core.Application.Service
|
||||||
string visitName,
|
string visitName,
|
||||||
string? message = null)
|
string? message = null)
|
||||||
{
|
{
|
||||||
|
|
||||||
bool fileExists = File.Exists(logFilePath);
|
bool fileExists = File.Exists(logFilePath);
|
||||||
|
|
||||||
|
// 一次性打开文件流
|
||||||
using var stream = new FileStream(
|
using var stream = new FileStream(
|
||||||
logFilePath,
|
logFilePath,
|
||||||
FileMode.Append,
|
FileMode.Append,
|
||||||
FileAccess.Write,
|
FileAccess.Write,
|
||||||
FileShare.ReadWrite);
|
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)
|
if (!fileExists)
|
||||||
{
|
{
|
||||||
writer.WriteLine("Time,SubjectCode,VisitNum,VisitName,Message");
|
writer.WriteLine("Time,SubjectCode,VisitNum,VisitName,Message");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 写入数据
|
||||||
string line = string.Join(",",
|
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,
|
subjectCode,
|
||||||
visitNum,
|
visitNum,
|
||||||
visitName,
|
visitName,
|
||||||
|
|
@ -162,14 +563,12 @@ namespace IRaCIS.Core.Application.Service
|
||||||
writer.WriteLine(line);
|
writer.WriteLine(line);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 防止逗号、换行导致 CSV 错乱
|
|
||||||
private static string Escape(string? value)
|
private static string Escape(string? value)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(value))
|
if (string.IsNullOrEmpty(value))
|
||||||
return "";
|
return "";
|
||||||
|
|
||||||
value = value.Replace("\"", "\"\"");
|
value = value.Replace("\"", "\"\"");
|
||||||
|
|
||||||
return $"\"{value}\"";
|
return $"\"{value}\"";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -211,13 +610,20 @@ namespace IRaCIS.Core.Application.Service
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
var oldVisits = MiniExcel.Query<SubjectVisitExcel>(Path.Combine(rootFolder, "Old.xlsx")).ToList();
|
||||||
|
|
||||||
var downloadVisits = MiniExcel.Query<SubjectVisitExcel>(Path.Combine(rootFolder, "download.xlsx")).ToList().Where(t => t.SubjectCode.IsNotNullOrEmpty() && t.VisitName.IsNotNullOrEmpty());
|
var downloadVisits = MiniExcel.Query<SubjectVisitExcel>(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<DownloadJob>();
|
var downloadJobs = new List<DownloadJob>();
|
||||||
|
|
||||||
|
var skipCount = 0;
|
||||||
|
var visitIndex = 0;
|
||||||
foreach (var downloadVisit in downloadVisits)
|
foreach (var downloadVisit in downloadVisits)
|
||||||
{
|
{
|
||||||
|
visitIndex++;
|
||||||
var downloadInfo = _trialRepository.Where(t => t.Id == trialId).Select(t => new
|
var downloadInfo = _trialRepository.Where(t => t.Id == trialId).Select(t => new
|
||||||
{
|
{
|
||||||
t.ResearchProgramNo,
|
t.ResearchProgramNo,
|
||||||
|
|
@ -291,6 +697,8 @@ namespace IRaCIS.Core.Application.Service
|
||||||
if (existVisits.Any(old => old.VisitNum == downloadVisit.VisitNum && old.SubjectCode == downloadVisit.SubjectCode &&
|
if (existVisits.Any(old => old.VisitNum == downloadVisit.VisitNum && old.SubjectCode == downloadVisit.SubjectCode &&
|
||||||
old.VisitName.Trim().ToLower() == downloadVisit.VisitName.Trim().ToLower()))
|
old.VisitName.Trim().ToLower() == downloadVisit.VisitName.Trim().ToLower()))
|
||||||
{
|
{
|
||||||
|
Log.Logger.Warning($"[{visitIndex}] Excel显示已下载,跳过当前访视:{downloadVisit.SubjectCode} {downloadVisit.VisitName} {downloadVisit.VisitNum}");
|
||||||
|
skipCount++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -305,9 +713,12 @@ namespace IRaCIS.Core.Application.Service
|
||||||
{
|
{
|
||||||
if (visitItem.StudyList.Count() == 0 && visitItem.NoneDicomStudyList.Count() == 0)
|
if (visitItem.StudyList.Count() == 0 && visitItem.NoneDicomStudyList.Count() == 0)
|
||||||
{
|
{
|
||||||
|
Log.Logger.Warning($"[{visitIndex}]查询无检查,跳过当前访视:{downloadVisit.SubjectCode} {downloadVisit.VisitName} {downloadVisit.VisitNum}");
|
||||||
|
skipCount++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Log.Logger.Warning($"[{visitIndex}]开始获取访视信息准备下载任务:{visitItem.SubjectCode} {visitItem.VisitName} {visitItem.VisitNum}");
|
||||||
|
|
||||||
foreach (var studyInfo in visitItem.StudyList)
|
foreach (var studyInfo in visitItem.StudyList)
|
||||||
{
|
{
|
||||||
|
|
@ -457,6 +868,8 @@ namespace IRaCIS.Core.Application.Service
|
||||||
//建立压缩包
|
//建立压缩包
|
||||||
string visitFolderPath = Path.Combine(trialFolderPath, $"{visitItem.SubjectCode}_{visitItem.VisitName.Trim()}");
|
string visitFolderPath = Path.Combine(trialFolderPath, $"{visitItem.SubjectCode}_{visitItem.VisitName.Trim()}");
|
||||||
|
|
||||||
|
var currentIndex = visitIndex;
|
||||||
|
|
||||||
downloadJobs.Add(new DownloadJob()
|
downloadJobs.Add(new DownloadJob()
|
||||||
{
|
{
|
||||||
Name = $"{visitItem.SubjectCode}_{visitItem.VisitName.Trim()}_Zip",
|
Name = $"{visitItem.SubjectCode}_{visitItem.VisitName.Trim()}_Zip",
|
||||||
|
|
@ -465,7 +878,7 @@ namespace IRaCIS.Core.Application.Service
|
||||||
|
|
||||||
Action = async () =>
|
Action = async () =>
|
||||||
{
|
{
|
||||||
Log.Logger.Warning($" {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";
|
string zipPath = visitFolderPath + ".zip";
|
||||||
|
|
||||||
|
|
@ -502,6 +915,7 @@ namespace IRaCIS.Core.Application.Service
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -515,14 +929,14 @@ namespace IRaCIS.Core.Application.Service
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Log.Logger.Warning($"{downloadVisits.Count}个访视信息核对准备完毕, 跳过{skipCount} 个,后端开始下载任务......");
|
||||||
|
|
||||||
#region 异步方式处理
|
#region 异步方式处理
|
||||||
|
|
||||||
int totalCount = downloadJobs.Count;
|
int totalCount = downloadJobs.Count;
|
||||||
int downloadedCount = 0;
|
int downloadedCount = 0;
|
||||||
|
|
||||||
Log.Logger.Warning($"开始下载总数: {totalCount}");
|
Log.Logger.Warning($"下载文件总数: {totalCount}");
|
||||||
foreach (var job in downloadJobs)
|
foreach (var job in downloadJobs)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
@ -975,6 +1389,8 @@ namespace IRaCIS.Core.Application.Service
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 下载影像 维护dir信息 并回传到OSS
|
/// 下载影像 维护dir信息 并回传到OSS
|
||||||
|
|
|
||||||
|
|
@ -82,9 +82,35 @@ namespace IRaCIS.Core.Application.Service.ImageAndDoc
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
#region 方式一 有的必须在内存中,不能用这种
|
||||||
|
//await using var source = await sourceFactory();
|
||||||
|
//// 如果你是从 stream 打开
|
||||||
|
//var dicomFile = await DicomFile.OpenAsync(source);
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 方式二
|
||||||
|
|
||||||
await using var source = await sourceFactory();
|
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 打开
|
// 如果你是从 stream 打开
|
||||||
var dicomFile = await DicomFile.OpenAsync(source);
|
var dicomFile = await DicomFile.OpenAsync(source.CanSeek ? source : bufferedStream);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
//获取像素是否为封装形式
|
//获取像素是否为封装形式
|
||||||
var syntax = dicomFile.Dataset.InternalTransferSyntax;
|
var syntax = dicomFile.Dataset.InternalTransferSyntax;
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,17 @@
|
||||||
using Aliyun.OSS;
|
using Aliyun.OSS;
|
||||||
using DocumentFormat.OpenXml.Spreadsheet;
|
|
||||||
using FellowOakDicom;
|
using FellowOakDicom;
|
||||||
using FellowOakDicom.Imaging;
|
using FellowOakDicom.Imaging;
|
||||||
using IRaCIS.Application.Contracts;
|
using IRaCIS.Application.Contracts;
|
||||||
using IRaCIS.Core.Application.BusinessFilter;
|
|
||||||
using IRaCIS.Core.Application.Contracts;
|
using IRaCIS.Core.Application.Contracts;
|
||||||
using IRaCIS.Core.Application.Helper;
|
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.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.Infra.EFCore.Context;
|
||||||
using IRaCIS.Core.Infrastructure;
|
using IRaCIS.Core.Infrastructure;
|
||||||
using IRaCIS.Core.Infrastructure.Encryption;
|
using IRaCIS.Core.Infrastructure.Encryption;
|
||||||
using IRaCIS.Core.Infrastructure.NewtonsoftJson;
|
using IRaCIS.Core.Infrastructure.NewtonsoftJson;
|
||||||
using MassTransit;
|
using MassTransit;
|
||||||
using MassTransit.Caching.Internals;
|
|
||||||
using MassTransit.Mediator;
|
|
||||||
using MathNet.Numerics;
|
|
||||||
using MaxMind.GeoIP2;
|
|
||||||
using Medallion.Threading;
|
using Medallion.Threading;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Builder;
|
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
@ -32,25 +19,15 @@ using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using MiniExcelLibs;
|
using MiniExcelLibs;
|
||||||
using Minio.DataModel;
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using NPOI.SS.Formula.Functions;
|
|
||||||
using NPOI.XWPF.UserModel;
|
using NPOI.XWPF.UserModel;
|
||||||
using SharpCompress.Common;
|
|
||||||
using SixLabors.ImageSharp;
|
using SixLabors.ImageSharp;
|
||||||
using SixLabors.ImageSharp.Formats.Jpeg;
|
using SixLabors.ImageSharp.Formats.Jpeg;
|
||||||
using SixLabors.ImageSharp.Processing;
|
using SixLabors.ImageSharp.Processing;
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq.Dynamic.Core;
|
using System.Linq.Dynamic.Core;
|
||||||
using System.Reactive.Subjects;
|
|
||||||
using System.Reflection.Metadata.Ecma335;
|
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using static IRaCIS.Core.Domain.Share.StaticData;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue