diff --git a/IRaCIS.Core.API/Progranm.cs b/IRaCIS.Core.API/Progranm.cs index 0ff83fb8f..01df023b5 100644 --- a/IRaCIS.Core.API/Progranm.cs +++ b/IRaCIS.Core.API/Progranm.cs @@ -65,7 +65,8 @@ SerilogExtension.AddSerilogSetup(enviromentName); var builder = WebApplication.CreateBuilder(new WebApplicationOptions { - EnvironmentName = enviromentName + EnvironmentName = enviromentName, + Args = args }); #region 主机配置 diff --git a/IRaCIS.Core.API/Properties/launchSettings.json b/IRaCIS.Core.API/Properties/launchSettings.json index d3e1c91fe..3238a6f17 100644 --- a/IRaCIS.Core.API/Properties/launchSettings.json +++ b/IRaCIS.Core.API/Properties/launchSettings.json @@ -33,11 +33,11 @@ }, "applicationUrl": "http://0.0.0.0:6100" }, - "IRaCIS.Event_IRC": { + "IRaCIS.Prod_Event_IRC": { "commandName": "Project", "launchBrowser": true, "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Event_IRC" + "ASPNETCORE_ENVIRONMENT": "Prod_Event_IRC" }, "applicationUrl": "http://0.0.0.0:6100" }, diff --git a/IRaCIS.Core.API/appsettings.Prod_Event_IRC.json b/IRaCIS.Core.API/appsettings.Prod_Event_IRC.json index cf8f44b59..47936737a 100644 --- a/IRaCIS.Core.API/appsettings.Prod_Event_IRC.json +++ b/IRaCIS.Core.API/appsettings.Prod_Event_IRC.json @@ -7,8 +7,10 @@ } }, "ConnectionStrings": { - "RemoteNew": "Server=101.132.253.119,1435;Database=irc_Prpd_bak;User ID=sa;Password=xc@123456;TrustServerCertificate=true", - "Hangfire": "Server=101.132.253.119,1435;Database=irc_Hangfire_bak;User ID=sa;Password=xc@123456;TrustServerCertificate=true" + "RemoteNew": "Server=101.132.193.237,1433;Database=Prod_IRC_pidr_restore;User ID=sa;Password=xc@123456;TrustServerCertificate=true", + "Hangfire": "Server=101.132.193.237,1433;Database=irc_Hangfire_bak;User ID=sa;Password=xc@123456;TrustServerCertificate=true" + //"RemoteNew": "Server=prod_mssql_standard,1433;Database=Prod_IRC;User ID=sa;Password=zhanying@2021;TrustServerCertificate=true", + //"Hangfire": "Server=prod_mssql_standard,1433;Database=Prod_IRC_Hangfire;User ID=sa;Password=zhanying@2021;TrustServerCertificate=true" }, "WeComNoticeConfig": { "IsOpenWeComNotice": true, @@ -17,48 +19,51 @@ "VueNoticeUserList": [ "wangxiaoshuang", "6b7717a31647293621b97b96f74e6f3d" ] }, "ObjectStoreService": { - "ObjectStoreUse": "AliyunOSS", - + "IsOpenStoreSync": true, + "ApiDeployRegion": "CN", + "SyncConfigList": [ + { + "Domain": "irc.extimaging.com", + "Primary": "AliyunOSS", + "Target": "AWS", + "UploadRegion": "CN", + "TargetRegion": "US", + "IsOpenSync": true + }, + { + "Domain": "eiirc.extimaging.com", + "Primary": "AWS", + "Target": "AliyunOSS", + "UploadRegion": "US", + "TargetRegion": "CN", + "IsOpenSync": true + } + ], "AliyunOSS": { "RegionId": "cn-shanghai", "InternalEndpoint": "https://oss-cn-shanghai-internal.aliyuncs.com", "EndPoint": "https://oss-cn-shanghai.aliyuncs.com", - "AccessKeyId": "LTAI5tFUCCmz5TwghZHsj45Y", - "AccessKeySecret": "8evrBy1fVfzJG25i67Jm0xqn9Xcw2T", - "RoleArn": "acs:ram::1078130221702011:role/uat-oss-access", - "BucketName": "tl-med-irc-event-store", - "ViewEndpoint": "https://tl-med-irc-event-store.oss-cn-shanghai.aliyuncs.com", + "AccessKeyId": "LTAI5tNRTsqL6aWmHkDmTwoH", + "AccessKeySecret": "7mtGz3qrYWI6JMMBZiLeC119VWicZH", + "RoleArn": "acs:ram::1899121822495495:role/irc-oss-access", + "BucketName": "zy-irc-store", + "ViewEndpoint": "https://zy-irc-cache.oss-cn-shanghai.aliyuncs.com", "Region": "oss-cn-shanghai", "DurationSeconds": 7200 }, - - "MinIO": { - "endPoint": "hir-oss.uat.extimaging.com", - "port": "80", - "useSSL": false, - "viewEndpoint": "http://hir-oss.uat.extimaging.com/irc-uat", - //"port": "443", - //"useSSL": true, - //"viewEndpoint": "https://hir-oss.uat.extimaging.com/irc-uat", - "accessKey": "b9Ul0e98xPzt6PwRXA1Q", - "secretKey": "DzMaU2L4OXl90uytwOmDXF2encN0Jf4Nxu2XkYqQ", - "bucketName": "irc-uat" - - }, "AWS": { "Region": "us-east-1", "EndPoint": "s3.us-east-1.amazonaws.com", "UseSSL": true, - "RoleArn": "arn:aws:iam::471112624751:role/sts_s3_upload", - "AccessKeyId": "AKIAW3MEAFJXWRCGSX5Z", - "SecretAccessKey": "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", - "BucketName": "ei-med-s3-lili-uat-store", - "ViewEndpoint": "https://ei-med-s3-lili-uat-store.s3.amazonaws.com/", + "RoleArn": "arn:aws:iam::471112624751:role/lili_s3_access", + "AccessKeyId": "AKIAW3MEAFJXZ2TZK7GM", + "SecretAccessKey": "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", + "BucketName": "ei-med-s3-irc-store", + "ViewEndpoint": "https://ei-med-s3-irc-store.s3.amazonaws.com", "DurationSeconds": 7200 } }, - "BasicSystemConfig": { // 启用质控风险控制功能 "QCRiskControl": true, @@ -67,7 +72,6 @@ "OpenLoginLimit": true, "LoginMaxFailCount": 5, - "LoginFailLockMinutes": 30, "AutoLoginOutMinutes": 120, @@ -82,17 +86,17 @@ "TemplateType": 2, //MFA免验证发送天数 "UserMFAVerifyMinutes": 1440 - }, + "SystemEmailSendConfig": { "Port": 465, "Host": "smtp.qiye.aliyun.com", "Imap": "imap.qiye.aliyun.com", "ImapPort": 993, - "FromEmail": "uat@extimaging.com", - "FromName": "Uat IRC Imaging System", - "AuthorizationCode": "SHzyyl2021", - "SiteUrl": "http://irc.uat.extimaging.com/login", + "FromEmail": "irc@extimaging.com", + "FromName": "IRC Imaging System", + "AuthorizationCode": "ExtImg@2022", + "SiteUrl": "http://irc.extimaging.com/login", "PlatformName": "EICS", "PlatformNameCN": "展影云平台", @@ -104,15 +108,12 @@ "CompanyShortName": "Extensive Imaging", "CompanyShortNameCN": "展影医疗", "IsEnv_US": false, - "IsOpenErrorNoticeEmail": false, "EmailRegexStr": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", - "CronEmailDefaultCulture": "zh-CN", - "ErrorNoticeEmailList": [ "872297557@qq.com" ] + "CronEmailDefaultCulture": "zh-CN" }, - "SystemPacsConfig": { - "Port": "11116", - "IP": "101.132.253.119" + "Port": "11113", + "IP": "101.132.193.237" }, "RequestDuplicationOptions": { "IsEnabled": true, diff --git a/IRaCIS.Core.Application/Helper/DicomDIRHelper.cs b/IRaCIS.Core.Application/Helper/DicomDIRHelper.cs index cba05d034..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,6 +161,91 @@ namespace IRaCIS.Core.Application.Helper } + public static async Task GenerateStudyDIR(List list, Dictionary dic, string? dirSavePath = null, Stream? outputStream = null) + { + var mappings = new List(); + int index = 1; + + var trialId = Guid.Empty; + + + var studyUid = list.FirstOrDefault()?.StudyInstanceUid; + + var dicomDir = new DicomDirectory(); + + foreach (var item in list.OrderBy(t => t.SeriesNumber).ThenBy(t => t.InstanceNumber)) + { + var dicomUid = DicomUID.Enumerate().FirstOrDefault(uid => uid.UID == item.TransferSytaxUID); + + if (dicomUid != null) + { + var ts = DicomTransferSyntax.Query(dicomUid); + + var dataset = new DicomDataset(ts) + { + { DicomTag.PatientID, item.PatientId ?? string.Empty }, + { DicomTag.PatientName, item.PatientName ?? string.Empty }, + { DicomTag.PatientBirthDate, item.PatientBirthDate ?? string.Empty }, + { DicomTag.PatientSex, item.PatientSex ?? string.Empty }, + + { DicomTag.StudyInstanceUID, item.StudyInstanceUid ?? string.Empty }, + { DicomTag.StudyID, item.StudyId ?? string.Empty }, + { DicomTag.StudyDate, item.DicomStudyDate ?? string.Empty }, + { DicomTag.StudyTime, item.DicomStudyTime ?? string.Empty }, + { DicomTag.AccessionNumber, item.AccessionNumber ?? string.Empty }, + { DicomTag.StudyDescription, item.StudyDescription ?? string.Empty }, + + { DicomTag.SeriesInstanceUID, item.SeriesInstanceUid ?? string.Empty }, + { DicomTag.Modality, item.Modality ?? string.Empty }, + { DicomTag.SeriesDate, item.DicomSeriesDate ?? string.Empty }, + { DicomTag.SeriesTime, item.DicomSeriesTime ?? string.Empty }, + { DicomTag.SeriesNumber, item.SeriesNumber.ToString() ?? string.Empty }, + { DicomTag.SeriesDescription, item.SeriesDescription ?? string.Empty }, + + { DicomTag.SOPInstanceUID, item.SopInstanceUid ?? string.Empty }, + { DicomTag.SOPClassUID, item.SOPClassUID ?? string.Empty }, + { DicomTag.InstanceNumber, item.InstanceNumber.ToString() ?? string.Empty }, + { DicomTag.MediaStorageSOPClassUID, item.MediaStorageSOPClassUID ?? string.Empty }, + { DicomTag.MediaStorageSOPInstanceUID, item.MediaStorageSOPInstanceUID ?? string.Empty }, + { DicomTag.TransferSyntaxUID, item.TransferSytaxUID ?? string.Empty }, + }; + + var dicomFile = new DicomFile(dataset); + + // 文件名递增格式:IM_00001, IM_00002, ... + string filename = $@"IMAGE\IM_{index:D5}"; // :D5 表示补足5位 + + mappings.Add($"{filename} => {item.InstanceId}"); + + dic.Add(item.InstanceId.ToString(), filename.TrimEnd('/', '\\').Split('/', '\\').Last()); + + dicomDir.AddFile(dicomFile, filename); + + index++; + } + + + } + + //有实际的文件 + if (mappings.Count > 0) + { + // 保存 DICOMDIR 到临时文件 不能直接写入到流种 + + if (dirSavePath.IsNotNullOrEmpty()) + { + await dicomDir.SaveAsync(dirSavePath); + + } + else + { + await dicomDir.SaveAsync(outputStream); + } + } + + } + + public static StudyDIRInfo ReadDicomDIRInfo(DicomFile dicomFile) { diff --git a/IRaCIS.Core.Application/Helper/OSSService.cs b/IRaCIS.Core.Application/Helper/OSSService.cs index ec04c3f08..93f96b3f2 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() { + + + // 过期时间 ≤ 当前时间 + 15分钟 时需要续期 + if (ObjectStoreServiceOptions.ObjectStoreUse == "AliyunOSS") { - if (AliyunOSSTempToken == null) + if (AliyunOSSTempToken != null && AliyunOSSTempToken.Expiration > DateTime.Now.AddMinutes(15)) { - GetObjectStoreTempToken(); - } - //token 过期了 - if (AliyunOSSTempToken?.Expiration.AddSeconds(10) <= DateTime.Now) - { - GetObjectStoreTempToken(); + return; // 还有15分钟以上,不需要续期 } + lock (_tokenLock) + { + if (AliyunOSSTempToken == null || + AliyunOSSTempToken.Expiration <= DateTime.Now.AddMinutes(15)) + { + GetObjectStoreTempToken(); + + Log.Logger.Warning("后端获取阿里云临时 Token"); + } + } } 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"); + } } + } + + } diff --git a/IRaCIS.Core.Application/IRaCIS.Core.Application.xml b/IRaCIS.Core.Application/IRaCIS.Core.Application.xml index e5cf503de..cfc8e09af 100644 --- a/IRaCIS.Core.Application/IRaCIS.Core.Application.xml +++ b/IRaCIS.Core.Application/IRaCIS.Core.Application.xml @@ -1760,18 +1760,20 @@ - + 项目影像后台下载,不打包 - + 后端api swagger 下载项目影像 + + @@ -6050,7 +6052,7 @@ - + 计算平均值 @@ -17494,17 +17496,17 @@ - ���� + 质疑 - һ���Ժ˲� + 一致性核查 - ���� + 复制 diff --git a/IRaCIS.Core.Application/Service/Allocation/VisitTaskHelpeService.cs b/IRaCIS.Core.Application/Service/Allocation/VisitTaskHelpeService.cs index ccdae5dfa..5a48de460 100644 --- a/IRaCIS.Core.Application/Service/Allocation/VisitTaskHelpeService.cs +++ b/IRaCIS.Core.Application/Service/Allocation/VisitTaskHelpeService.cs @@ -873,7 +873,7 @@ namespace IRaCIS.Core.Application.Service { arm1.TaskAllocationState = TaskAllocationState.Allocated; arm1.AllocateTime = DateTime.Now; - arm1.DoctorUserId = task1.DoctorUserId; + arm1.DoctorUserId = assignConfigList.FirstOrDefault(t => t.ArmEnum == Arm.DoubleReadingArm1)!.DoctorUserId; arm1.SuggesteFinishedTime = GetSuggessFinishTime(true, UrgentType.NotUrget); } @@ -913,7 +913,7 @@ namespace IRaCIS.Core.Application.Service { taskOne.TaskAllocationState = TaskAllocationState.Allocated; taskOne.AllocateTime = DateTime.Now; - taskOne.DoctorUserId = task1!.DoctorUserId; + taskOne.DoctorUserId = assignConfigList.FirstOrDefault(t => t.ArmEnum == Arm.DoubleReadingArm1)!.DoctorUserId; taskOne.SuggesteFinishedTime = GetSuggessFinishTime(true, UrgentType.NotUrget); } @@ -944,7 +944,7 @@ namespace IRaCIS.Core.Application.Service { arm2.TaskAllocationState = TaskAllocationState.Allocated; arm2.AllocateTime = DateTime.Now; - arm2.DoctorUserId = task2.DoctorUserId; + arm2.DoctorUserId = assignConfigList.FirstOrDefault(t => t.ArmEnum == Arm.DoubleReadingArm2)!.DoctorUserId; arm2.SuggesteFinishedTime = GetSuggessFinishTime(true, UrgentType.NotUrget); } @@ -986,7 +986,7 @@ namespace IRaCIS.Core.Application.Service { taskTwo.TaskAllocationState = TaskAllocationState.Allocated; taskTwo.AllocateTime = DateTime.Now; - taskTwo.DoctorUserId = task2!.DoctorUserId; + taskTwo.DoctorUserId = assignConfigList.FirstOrDefault(t => t.ArmEnum == Arm.DoubleReadingArm2)!.DoctorUserId; taskTwo.SuggesteFinishedTime = GetSuggessFinishTime(true, UrgentType.NotUrget); } diff --git a/IRaCIS.Core.Application/Service/Common/MailService.cs b/IRaCIS.Core.Application/Service/Common/MailService.cs index 50a32982a..3e54c32e9 100644 --- a/IRaCIS.Core.Application/Service/Common/MailService.cs +++ b/IRaCIS.Core.Application/Service/Common/MailService.cs @@ -984,7 +984,8 @@ namespace IRaCIS.Core.Application.Service var htmlBodyStr = string.Format(CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr, workLanguage), - sysUserInfo.FullName + sysUserInfo.FullName, + _systemEmailConfig.PlatformName ); return (topicStr, htmlBodyStr); diff --git a/IRaCIS.Core.Application/Service/Common/TrialImageDownloadService.cs b/IRaCIS.Core.Application/Service/Common/TrialImageDownloadService.cs index a23703c16..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; @@ -7,7 +8,10 @@ using FellowOakDicom; using FellowOakDicom.Imaging; using FellowOakDicom.Imaging.Render; using FellowOakDicom.IO.Buffer; +using Hangfire.Common; +using IRaCIS.Core.Application.Contracts; using IRaCIS.Core.Application.Helper; +using IRaCIS.Core.Application.MassTransit.Command; using IRaCIS.Core.Application.ViewModel; using IRaCIS.Core.Domain.Models; using IRaCIS.Core.Infrastructure; @@ -18,17 +22,21 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; using MiniExcelLibs; using NPOI.Util; +using Org.BouncyCastle.Utilities.Zlib; using SharpCompress.Common; using System; using System.Collections.Generic; using System.Data; using System.Diagnostics; +using System.IO.Compression; 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.Application.Service.TestService; +using static IRaCIS.Core.Application.Service.TrialImageDownloadService; using static IRaCIS.Core.Domain.Share.StaticData; using static Microsoft.EntityFrameworkCore.DbLoggerCategory; @@ -41,7 +49,7 @@ namespace IRaCIS.Core.Application.Service /// /// [ApiExplorerSettings(GroupName = "Common")] - public class TrialImageDownloadService(IRepository _trialRepository, IOSSService _oSSService, IWebHostEnvironment _hostEnvironment, + public class TrialImageDownloadService(IRepository _trialRepository, IRepository _subjectVisitRepository, IOSSService _oSSService, IWebHostEnvironment _hostEnvironment, IRepository _studyRepository, IRepository _seriesRepository, IRepository _instanceRepository) : BaseService @@ -54,9 +62,35 @@ 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 + 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; @@ -64,11 +98,14 @@ namespace IRaCIS.Core.Application.Service //对于封装像素的文件做转换 if (syntax.IsEncapsulated) { + #region 开始方式 + // 获取 Pixel Data 标签 var pixelData = DicomPixelData.Create(dicomFile.Dataset); // 创建一个新的片段序列 var newFragments = new DicomOtherByteFragment(DicomTag.PixelData); + // 获取每帧数据并封装为单独的片段 for (int n = 0; n < pixelData.NumberOfFrames; n++) { @@ -76,13 +113,11 @@ namespace IRaCIS.Core.Application.Service newFragments.Fragments.Add(new MemoryByteBuffer(frameData.Data)); } - var frag = dicomFile.Dataset.GetDicomItem(DicomTag.PixelData); - - var originOffsetTable = frag?.OffsetTable; - - newFragments.OffsetTable.AddRange(originOffsetTable?.ToArray()); // 替换原有的片段序列 dicomFile.Dataset.AddOrUpdate(newFragments); + + #endregion + } await dicomFile.SaveAsync(output); @@ -97,35 +132,896 @@ 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} 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 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) + { + continue; + } + + #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()}"; + + #region DICOM + + 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 })) + { + + 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 void Write( + string logFilePath, + string subjectCode, + decimal visitNum, + string visitName, + string? message = null) + { + bool fileExists = File.Exists(logFilePath); + + // 一次性打开文件流 + using var stream = new FileStream( + logFilePath, + FileMode.Append, + FileAccess.Write, + FileShare.ReadWrite); + + // 首次创建时写入 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")), + subjectCode, + visitNum, + visitName, + Escape(message) + ]); + + writer.WriteLine(line); + } + + private static string Escape(string? value) + { + if (string.IsNullOrEmpty(value)) + return ""; + + value = value.Replace("\"", "\"\""); + return $"\"{value}\""; + } + } + + public class DownloadJob + { + public bool IsZip { get; set; } + public string Name { get; set; } + + + public Func Action { get; set; } + } + + [HttpPost] + [AllowAnonymous] + public async Task DownloadExcelTrialImage(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 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, + 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; + } + + Log.Logger.Warning($"[{visitIndex}]开始获取访视信息准备下载任务:{visitItem.SubjectCode} {visitItem.VisitName} {visitItem.VisitNum}"); + + foreach (var studyInfo in visitItem.StudyList) + { + + var dirDic = new Dictionary(); + + #region 生成DIR + + string studyDicomFolderPath = Path.Combine(trialFolderPath, $"{visitItem.SubjectCode}_{visitItem.VisitName.Trim()}", $"{studyInfo.StudyCode}_{studyInfo.StudyTime?.ToString("yyyy-MM-dd")}_{string.Join('_', studyInfo.SeriesList.Select(t => t.Modality))}"); + + // 创建 影像 文件夹 + Directory.CreateDirectory(studyDicomFolderPath); + + + 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(); + + var pathInfo = await _subjectVisitRepository.Where(t => t.Id == visitItem.SubjectVisitId).Select(t => new { t.TrialId, t.SubjectId, VisitId = t.Id }).FirstNotNullAsync(); + + foreach (var group in list.GroupBy(t => new { t.StudyInstanceUid, t.DicomStudyId })) + { + + var first = group.First(); + + + var dirPath = Path.Combine(studyDicomFolderPath, "DICOMDIR"); + + var isSucess = await SafeBussinessHelper.RunAsync(async () => await DicomDIRHelper.GenerateStudyDIR(group.ToList(), dirDic, dirPath)); + + + + + } + } + #endregion + + // 遍历 Series + foreach (var seriesInfo in studyInfo.SeriesList) + { + + + + // 遍历 InstancePathList + foreach (var instanceInfo in seriesInfo.InstancePathList) + { + + string destinationFolder = Path.Combine(studyDicomFolderPath, "IMAGE"); + + Directory.CreateDirectory(destinationFolder); + + // 复制文件到相应的文件夹 + string destinationPath = Path.Combine(destinationFolder, dirDic[instanceInfo.InstanceId.ToString()]); + + + downloadJobs.Add(new DownloadJob() + { + Name = $"{visitItem.SubjectCode}_{visitItem.VisitNum}_{visitItem.VisitName.Trim()}_DICOM_{destinationPath}", + + Action = async () => + { + + await using var output = File.Create(destinationPath); + + if (instanceInfo.IsEncapsulated) + { + var isSucess = await TryWriteMergedDicomAsync( + () => _oSSService.GetStreamFromOSSAsync(instanceInfo.Path), + output); + + if (isSucess == false) + { + Log.Logger.Warning($"合并多帧失败: failed: {destinationFolder} {destinationPath}"); + } + } + else + { + await using var input = + await _oSSService.GetStreamFromOSSAsync(instanceInfo.Path); + + await input.CopyToAsync(output); + } + } + }); + + } + } + + + } + + foreach (var noneDicomStudy in visitItem.NoneDicomStudyList) + { + string studyNoneDicomFolderPath = Path.Combine(trialFolderPath, $"{visitItem.SubjectCode}_{visitItem.VisitName.Trim()}", $"{noneDicomStudy.StudyCode}_{noneDicomStudy.ImageDate.ToString("yyyy-MM-dd")}_{noneDicomStudy.Modality}"); + + Directory.CreateDirectory(studyNoneDicomFolderPath); + + foreach (var file in noneDicomStudy.FileList) + { + string destinationPath = Path.Combine(studyNoneDicomFolderPath, Path.GetFileName(file.FileName)); + + //加入到下载任务里 + downloadJobs.Add(new DownloadJob() { Name = $"{visitItem.SubjectCode}_{visitItem.VisitNum}_{visitItem.VisitName.Trim()}_NoneDICOM_{destinationPath}", Action = () => _oSSService.DownLoadFromOSSAsync(HttpUtility.UrlDecode(file.Path), destinationPath) }); + + //下载到当前目录 + //await _oSSService.DownLoadFromOSSAsync(HttpUtility.UrlDecode(file.Path), destinationPath); + } + } + + + //建立压缩包 + string visitFolderPath = Path.Combine(trialFolderPath, $"{visitItem.SubjectCode}_{visitItem.VisitName.Trim()}"); + + var currentIndex = visitIndex; + + downloadJobs.Add(new DownloadJob() + { + Name = $"{visitItem.SubjectCode}_{visitItem.VisitName.Trim()}_Zip", + + IsZip = true, + + Action = async () => + { + Log.Logger.Warning($"[{currentIndex}] {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}开启另外线程压缩访视:{visitItem.SubjectCode} {visitItem.VisitName} {visitItem.VisitNum}"); + + string zipPath = visitFolderPath + ".zip"; + + if (File.Exists(zipPath)) + { + File.Delete(zipPath); + } + + ZipFile.CreateFromDirectory( + visitFolderPath, + zipPath, + CompressionLevel.NoCompression, + false); + + Directory.Delete(visitFolderPath, true); + + await Task.CompletedTask; + } + }); + + //记录日志 + downloadJobs.Add(new DownloadJob() + { + Name = $"{visitItem.SubjectCode}_{visitItem.VisitNum}_{visitItem.VisitName.Trim()}_Finished", + Action = () => + { + DownloadLogger.Write( + logFilePath: logFilePath, + subjectCode: visitItem.SubjectCode, + visitNum: visitItem.VisitNum, + visitName: visitItem.VisitName.Trim(), + message: "Success"); + + return Task.CompletedTask; + } + }); + + } + + + + + + + + + } + + + + Log.Logger.Warning($"{downloadVisits.Count}个访视信息核对准备完毕, 跳过{skipCount} 个,后端开始下载任务......"); + + #region 异步方式处理 + + int totalCount = downloadJobs.Count; + int downloadedCount = 0; + + Log.Logger.Warning($"下载文件总数: {totalCount}"); + foreach (var job in downloadJobs) + { + try + { + if (job.IsZip == false) + { + await job.Action(); + + } + else + { + _ = Task.Run(async () => + { + await job.Action(); + }); + } + } + catch (Exception ex) + { + Log.Logger.Error($"{job.Name}下载失败: {ex.Message}"); + } + + downloadedCount++; + + // 每处理50个,输出一次进度(或最后一个时也输出) + if (downloadedCount % 50 == 0 || downloadedCount == totalCount) + { + + Log.Logger.Warning($"已下载 {downloadedCount} / {totalCount} 个文件,完成 {(downloadedCount * 100.0 / totalCount):F2}%"); + + } + } + #endregion + + + return ResponseOutput.Ok(); + + + + + } + + /// /// 后端api swagger 下载项目影像 /// /// + /// + /// /// [HttpPost] [AllowAnonymous] - public async Task DownloadTrialImage(Guid trialId) + public async Task DownloadTrialImage(Guid trialId, int startIndex, int endIndex) { //找到项目里面未阅片的影像 + if (startIndex < 1 || endIndex < 1 || startIndex > endIndex) + { + return ResponseOutput.NotOk("请输入正确的下载区间"); + } + //var subjectCodeList = new List() { "05002", "07006", "07026" }; var downloadInfo = _trialRepository.Where(t => t.Id == trialId).Select(t => new { t.ResearchProgramNo, + t.TrialCode, - VisitList = t.SubjectVisitList + VisitList = t.SubjectVisitList.Where(t => t.VisitTaskList.Any(t => t.TaskState == TaskState.Effect && t.ReadingCategory == ReadingCategory.Visit && t.ReadingTaskState != ReadingTaskState.HaveSigned && t.SourceSubjectVisitId != null && t.DoctorUserId != null)) //.Where(t=>subjectCodeList.Contains(t.Subject.Code)) .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 { @@ -133,6 +1029,7 @@ namespace IRaCIS.Core.Application.Service InstancePathList = z.DicomInstanceList.Where(t => t.IsReading).Select(k => new { + InstanceId = k.Id, k.Path, k.IsEncapsulated, k.NumberOfFrames, @@ -154,164 +1051,346 @@ namespace IRaCIS.Core.Application.Service file.FileType }).ToList() }).ToList() - }).ToList() + }).OrderBy(t => t.SubjectCode).ThenBy(t => t.VisitNum).ToList() }).FirstOrDefault(); - - var count = downloadInfo.VisitList.SelectMany(t => t.NoneDicomStudyList).SelectMany(t => t.FileList).Count(); - var count2 = downloadInfo.VisitList.SelectMany(t => t.StudyList).SelectMany(t => t.SeriesList).SelectMany(t => t.InstancePathList).Count(); - - Console.WriteLine($"下载总数量:{count}+{count2}={count + count2}"); - - if (downloadInfo != null) + if (downloadInfo == null) { - var downloadJobs = new List>(); + return ResponseOutput.Ok(); + } - //var rootFolder = @"E:\DownloadImage"; + #region 设置目录 - var rootFolder = FileStoreHelper.GetDonwnloadImageFolder(_hostEnvironment); + var rootFolder = FileStoreHelper.GetIRaCISRootDataFolder(_hostEnvironment); + Directory.CreateDirectory(rootFolder); - // 获取无效字符(系统定义的) - string invalidChars = new string(Path.GetInvalidFileNameChars()) + new string(Path.GetInvalidPathChars()); + // 获取无效字符(系统定义的) + string invalidChars = new string(Path.GetInvalidFileNameChars()) + new string(Path.GetInvalidPathChars()); - // 用正则表达式替换所有非法字符为下划线或空字符 - string pattern = $"[{Regex.Escape(invalidChars)}]"; + // 用正则表达式替换所有非法字符为下划线或空字符 + string pattern = $"[{Regex.Escape(invalidChars)}]"; - var regexNo = Regex.Replace(downloadInfo.ResearchProgramNo, pattern, "_"); + var regexNo = Regex.Replace(downloadInfo.ResearchProgramNo, pattern, "_"); - // 创建一个临时文件夹来存放文件 - string trialFolderPath = Path.Combine(rootFolder, $"{regexNo}_{NewId.NextGuid()}"); - Directory.CreateDirectory(trialFolderPath); + // 创建一个临时文件夹来存放文件 + string trialFolderPath = Path.Combine(rootFolder, $"{regexNo}");//_{NewId.NextGuid()} + Directory.CreateDirectory(trialFolderPath); - foreach (var visitItem in downloadInfo.VisitList) + var oldVisits = MiniExcel.Query(Path.Combine(rootFolder, "Old.xlsx")).ToList(); + + Log.Logger.Warning($"数据库查询下载访视数量 - 前一批已下载访视数量:{downloadInfo.VisitList.Count} - {oldVisits.Count}"); + + var acturalDownList = downloadInfo.VisitList.Where(t => !oldVisits.Any(old => old.VisitNum == t.VisitNum && old.SubjectCode == t.SubjectCode && + old.VisitName.Trim().ToLower() == t.VisitName.Trim().ToLower())).ToList(); + + #endregion + + #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() { - if (visitItem.StudyList.Count() == 0 && visitItem.NoneDicomStudyList.Count() == 0) + StreamReaderFunc = (stream) => new StreamReader(stream, Encoding.GetEncoding("gb2312")) + }).ToList(); + + Log.Logger.Warning($"download_log.csv访视数量:{existVisits.Count}"); + + Log.Logger.Warning($"csv排除前需要下载数量:{acturalDownList.Count}"); + + acturalDownList = acturalDownList.Where(t => !existVisits.Any(old => old.VisitNum == t.VisitNum && old.SubjectCode == t.SubjectCode && + old.VisitName.Trim().ToLower() == t.VisitName.Trim().ToLower())).ToList(); + + Log.Logger.Warning($"csv排除后需要下载数量:{acturalDownList.Count}"); + + + } + + acturalDownList = acturalDownList.Skip(startIndex - 1).Take(endIndex - startIndex + 1).ToList(); + + Log.Logger.Warning($"该区{startIndex}-{endIndex} 实际需要下载访视数量:{acturalDownList.Count}"); + + #endregion + + var count = acturalDownList.SelectMany(t => t.NoneDicomStudyList).SelectMany(t => t.FileList).Count(); + + var count2 = acturalDownList.SelectMany(t => t.StudyList).SelectMany(t => t.SeriesList).SelectMany(t => t.InstancePathList).Count(); + + Log.Logger.Warning($"下载总数量:{count}+{count2}={count + count2}"); + + var downloadJobs = new List(); + + + foreach (var visitItem in acturalDownList) + { + if (visitItem.StudyList.Count() == 0 && visitItem.NoneDicomStudyList.Count() == 0) + { + continue; + } + + + + foreach (var studyInfo in visitItem.StudyList) + { + + var dirDic = new Dictionary(); + + #region 生成DIR + + string studyDicomFolderPath = Path.Combine(trialFolderPath, $"{visitItem.SubjectCode}_{visitItem.VisitName.Trim()}", $"{studyInfo.StudyCode}_{studyInfo.StudyTime?.ToString("yyyy-MM-dd")}_{string.Join('_', studyInfo.SeriesList.Select(t => t.Modality))}"); + + // 创建 影像 文件夹 + Directory.CreateDirectory(studyDicomFolderPath); + + + if (!_instanceRepository.Where(t => t.IsReading && t.DicomSerie.IsReading) + .Where(t => visitItem.SubjectVisitId == t.SubjectVisitId).Any(c => c.TransferSytaxUID == string.Empty)) { - continue; + 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(); + + var pathInfo = await _subjectVisitRepository.Where(t => t.Id == visitItem.SubjectVisitId).Select(t => new { t.TrialId, t.SubjectId, VisitId = t.Id }).FirstNotNullAsync(); + + foreach (var group in list.GroupBy(t => new { t.StudyInstanceUid, t.DicomStudyId })) + { + + var first = group.First(); + + + var dirPath = Path.Combine(studyDicomFolderPath, "DICOMDIR"); + + var isSucess = await SafeBussinessHelper.RunAsync(async () => await DicomDIRHelper.GenerateStudyDIR(group.ToList(), dirDic, dirPath)); + + + + + } } - - #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) { - // 遍历 Series - foreach (var seriesInfo in studyInfo.SeriesList) + + + + // 遍历 InstancePathList + foreach (var instanceInfo in seriesInfo.InstancePathList) { - string studyDicomFolderPath = Path.Combine(siteFolderPath, $"{visitItem.SubjectCode}_{visitItem.VisitName}_DICOM", $"{studyInfo.StudyCode}_{studyInfo.StudyTime?.ToString("yyyy-MM-dd")}_{seriesInfo.Modality}"); - // 创建 影像 文件夹 - Directory.CreateDirectory(studyDicomFolderPath); + string destinationFolder = Path.Combine(studyDicomFolderPath, "IMAGE"); - // 遍历 InstancePathList - foreach (var instanceInfo in seriesInfo.InstancePathList) + Directory.CreateDirectory(destinationFolder); + + // 复制文件到相应的文件夹 + string destinationPath = Path.Combine(destinationFolder, dirDic[instanceInfo.InstanceId.ToString()]); + + + downloadJobs.Add(new DownloadJob() + { + Name = $"{visitItem.SubjectCode}_{visitItem.VisitNum}_{visitItem.VisitName.Trim()}_DICOM_{destinationPath}", + + Action = async () => { - // 复制文件到相应的文件夹 - string destinationPath = Path.Combine(studyDicomFolderPath, Path.GetFileName(instanceInfo.Path)); await using var output = File.Create(destinationPath); + if (instanceInfo.IsEncapsulated) { - //加入到下载任务里 - downloadJobs.Add(() => TryWriteMergedDicomAsync(() => _oSSService.GetStreamFromOSSAsync(instanceInfo.Path), output)); + var isSucess = await TryWriteMergedDicomAsync( + () => _oSSService.GetStreamFromOSSAsync(instanceInfo.Path), + output); + + if (isSucess == false) + { + Log.Logger.Warning($"合并多帧失败: failed: {destinationFolder} {destinationPath}"); + } } else { - //加入到下载任务里 - downloadJobs.Add(() => _oSSService.DownLoadFromOSSAsync(instanceInfo.Path, destinationPath)); + await using var input = + await _oSSService.GetStreamFromOSSAsync(instanceInfo.Path); + + await input.CopyToAsync(output); } - - - //下载到当前目录 - //await _oSSService.DownLoadFromOSSAsync(instanceInfo.Path, destinationPath); } - } + }); - - } - - foreach (var noneDicomStudy in visitItem.NoneDicomStudyList) - { - string studyNoneDicomFolderPath = Path.Combine(siteFolderPath, $"{visitItem.SubjectCode}_{visitItem.VisitName}_Non-DICOM", $"{noneDicomStudy.StudyCode}_{noneDicomStudy.ImageDate.ToString("yyyy-MM-dd")}_{noneDicomStudy.Modality}"); - - Directory.CreateDirectory(studyNoneDicomFolderPath); - - foreach (var file in noneDicomStudy.FileList) - { - string destinationPath = Path.Combine(studyNoneDicomFolderPath, Path.GetFileName(file.FileName)); - - //加入到下载任务里 - downloadJobs.Add(() => _oSSService.DownLoadFromOSSAsync(HttpUtility.UrlDecode(file.Path), destinationPath)); - //下载到当前目录 - //await _oSSService.DownLoadFromOSSAsync(HttpUtility.UrlDecode(file.Path), destinationPath); } } } - #region 异步方式处理 - - int totalCount = downloadJobs.Count; - int downloadedCount = 0; - - foreach (var job in downloadJobs) + foreach (var noneDicomStudy in visitItem.NoneDicomStudyList) { - try + string studyNoneDicomFolderPath = Path.Combine(trialFolderPath, $"{visitItem.SubjectCode}_{visitItem.VisitName.Trim()}", $"{noneDicomStudy.StudyCode}_{noneDicomStudy.ImageDate.ToString("yyyy-MM-dd")}_{noneDicomStudy.Modality}"); + + Directory.CreateDirectory(studyNoneDicomFolderPath); + + foreach (var file in noneDicomStudy.FileList) { - await job(); - } - catch (Exception ex) - { - Log.Logger.Error($"下载失败: {ex.Message}"); - } + string destinationPath = Path.Combine(studyNoneDicomFolderPath, Path.GetFileName(file.FileName)); - downloadedCount++; - - // 每处理50个,输出一次进度(或最后一个时也输出) - if (downloadedCount % 50 == 0 || downloadedCount == totalCount) - { - - Log.Logger.Error($"已下载 {downloadedCount} / {totalCount} 个文件,完成 {(downloadedCount * 100.0 / totalCount):F2}%"); + //加入到下载任务里 + downloadJobs.Add(new DownloadJob() { Name = $"{visitItem.SubjectCode}_{visitItem.VisitNum}_{visitItem.VisitName.Trim()}_NoneDICOM_{destinationPath}", Action = () => _oSSService.DownLoadFromOSSAsync(HttpUtility.UrlDecode(file.Path), destinationPath) }); + //下载到当前目录 + //await _oSSService.DownLoadFromOSSAsync(HttpUtility.UrlDecode(file.Path), destinationPath); } } - #endregion - #region 多线程测试 - //const int batchSize = 15; - //int totalCount = downloadJobs.Count; - //int downloadedCount = 0; + //建立压缩包 + string visitFolderPath = Path.Combine(trialFolderPath, $"{visitItem.SubjectCode}_{visitItem.VisitName.Trim()}"); - //for (int i = 0; i < downloadJobs.Count; i += batchSize) - //{ - // var batch = downloadJobs.Skip(i).Take(batchSize).Select(job => job()); + downloadJobs.Add(new DownloadJob() + { + Name = $"{visitItem.SubjectCode}_{visitItem.VisitName.Trim()}_Zip", - // await Task.WhenAll(batch); + IsZip = true, - // downloadedCount += batch.Count(); + Action = async () => + { + string zipPath = visitFolderPath + ".zip"; - // Console.WriteLine($"已下载 {downloadedCount} / {totalCount} 个文件,完成 {(downloadedCount * 100.0 / totalCount):F2}%"); - //} - #endregion + if (File.Exists(zipPath)) + { + File.Delete(zipPath); + } + ZipFile.CreateFromDirectory( + visitFolderPath, + zipPath, + CompressionLevel.NoCompression, + false); + + Directory.Delete(visitFolderPath, true); + + await Task.CompletedTask; + } + }); + + //记录日志 + downloadJobs.Add(new DownloadJob() + { + Name = $"{visitItem.SubjectCode}_{visitItem.VisitNum}_{visitItem.VisitName.Trim()}_Finished", + Action = () => + { + DownloadLogger.Write( + logFilePath: logFilePath, + subjectCode: visitItem.SubjectCode, + visitNum: visitItem.VisitNum, + visitName: visitItem.VisitName.Trim(), + message: "Success"); + + return Task.CompletedTask; + } + }); } + #region 异步方式处理 + + int totalCount = downloadJobs.Count; + int downloadedCount = 0; + + foreach (var job in downloadJobs) + { + try + { + if (job.IsZip == false) + { + await job.Action(); + + } + else + { + _ = Task.Run(async () => + { + await job.Action(); + }); + } + } + catch (Exception ex) + { + Log.Logger.Error($"{job.Name}下载失败: {ex.Message}"); + } + + downloadedCount++; + + // 每处理50个,输出一次进度(或最后一个时也输出) + if (downloadedCount % 50 == 0 || downloadedCount == totalCount) + { + + Log.Logger.Warning($"已下载 {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(); } + #endregion + /// /// 下载影像 维护dir信息 并回传到OSS diff --git a/IRaCIS.Core.Application/Service/ImageAndDoc/StudyService.cs b/IRaCIS.Core.Application/Service/ImageAndDoc/StudyService.cs index c38ebff88..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; @@ -104,11 +130,11 @@ namespace IRaCIS.Core.Application.Service.ImageAndDoc newFragments.Fragments.Add(new MemoryByteBuffer(frameData.Data)); } - var frag = dicomFile.Dataset.GetDicomItem(DicomTag.PixelData); + //var frag = dicomFile.Dataset.GetDicomItem(DicomTag.PixelData); - var originOffsetTable = frag?.OffsetTable; + //var originOffsetTable = frag?.OffsetTable; - newFragments.OffsetTable.AddRange(originOffsetTable?.ToArray()); + //newFragments.OffsetTable.AddRange(originOffsetTable?.ToArray()); // 替换原有的片段序列 dicomFile.Dataset.AddOrUpdate(newFragments); } diff --git a/IRaCIS.Core.Application/Service/Management/UserService.cs b/IRaCIS.Core.Application/Service/Management/UserService.cs index 67de9f202..c8cdf0cd7 100644 --- a/IRaCIS.Core.Application/Service/Management/UserService.cs +++ b/IRaCIS.Core.Application/Service/Management/UserService.cs @@ -442,7 +442,16 @@ namespace IRaCIS.Core.Application.Service await _userLogRepository.AddAsync(new UserLog() { IP = _userInfo.IP, ActionIdentityUserId = identityUserId, ActionUserName = _userInfo.UserName, TargetIdentityUserId = identityUserId, OptType = UserOptType.UnloginModifyPasswoed }, true); - await _mailVerificationService.AfterUserModifyPasswordSendEmailAsync(identityUserId); + try + { + await _mailVerificationService.AfterUserModifyPasswordSendEmailAsync(identityUserId); + } + catch (Exception) + { + + + } + var find = await _identityUserRepository.FindAsync(identityUserId); diff --git a/IRaCIS.Core.Application/Service/ReadingCalculate/MRIPDFFAdvanceCalculateService.cs b/IRaCIS.Core.Application/Service/ReadingCalculate/MRIPDFFAdvanceCalculateService.cs index adb586594..06cec87ae 100644 --- a/IRaCIS.Core.Application/Service/ReadingCalculate/MRIPDFFAdvanceCalculateService.cs +++ b/IRaCIS.Core.Application/Service/ReadingCalculate/MRIPDFFAdvanceCalculateService.cs @@ -170,7 +170,7 @@ namespace IRaCIS.Core.Application.Service.ReadingCalculate - var dictionList = await _dictionaryRepository.Where(x => x.Parent.Code == "LiverSegmentation").OrderBy(x => x.ShowOrder).ToListAsync(); + var dictionList = await _dictionaryRepository.Where(x => x.Parent.Code == "Liver4Segmentation").OrderBy(x => x.ShowOrder).ToListAsync(); var tableQuestion = await _readingQuestionTrialRepository.Where(x => x.ReadingQuestionCriterionTrialId == taskinfo.TrialReadingCriterionId && x.LesionType == LesionType.FatFraction).FirstNotNullAsync(); @@ -297,7 +297,7 @@ namespace IRaCIS.Core.Application.Service.ReadingCalculate public async Task ReadingCalculate(ReadingCalculateDto inDto, List? calculateType = null) { - await this.CalculateAvg(inDto); + //await this.CalculateAvg(inDto); inDto = await _generalCalculateService.GetReadingCalculateDto(inDto.VisitTaskId); var needAddList = new List(); List calculateList = new List() @@ -408,7 +408,7 @@ namespace IRaCIS.Core.Application.Service.ReadingCalculate var tableQuestionList = rowInfo.Where(x=>x.TableQuestionList.Any(x=>x.QuestionMark== QuestionMark.IsMeasurable && x.Answer.EqEnum(YesOrNoOrNa.Yes))).SelectMany(x => x.TableQuestionList).ToList(); - if (rowInfo.Any(x => x.TableQuestionList.Any(x => x.QuestionMark == QuestionMark.AverageValue && x.Answer.IsNullOrEmpty()))) + if (rowInfo.Any(x => x.TableQuestionList.Any(x => x.QuestionMark == QuestionMark.MRIPDFF && x.Answer.IsNullOrEmpty()))) { return string.Empty; } @@ -416,7 +416,7 @@ namespace IRaCIS.Core.Application.Service.ReadingCalculate { return "NE"; } - result = tableQuestionList.Where(x => x.QuestionMark == QuestionMark.AverageValue).Average(x => x.Answer.IsNullOrEmptyReturn0()); + result = tableQuestionList.Where(x => x.QuestionMark == QuestionMark.MRIPDFF).Average(x => x.Answer.IsNullOrEmptyReturn0()); return decimal.Round(result, inDto.DigitPlaces, MidpointRounding.AwayFromZero).ToString("F" + inDto.DigitPlaces.ToString()); ; } @@ -456,54 +456,54 @@ namespace IRaCIS.Core.Application.Service.ReadingCalculate /// 计算平均值 /// /// - public async Task CalculateAvg(ReadingCalculateDto inDto) - { - // 脂肪分数的表格问题Id - var questionInfo = inDto.QuestionInfo.Where(x => x.LesionType == LesionType.FatFraction).FirstOrDefault(); + //public async Task CalculateAvg(ReadingCalculateDto inDto) + //{ + // // 脂肪分数的表格问题Id + // var questionInfo = inDto.QuestionInfo.Where(x => x.LesionType == LesionType.FatFraction).FirstOrDefault(); - List tableAnswers = new List(); + // List tableAnswers = new List(); - foreach (var item in questionInfo.TableRowInfoList) - { - var avg = item.TableQuestionList.Where(x => x.QuestionMark == QuestionMark.AverageValue).FirstOrDefault(); - var avgAnswer = string.Empty; - List questionMarks = new List() - { - QuestionMark.FirstMeasurement, - QuestionMark.SecondMeasurement, - QuestionMark.ThirdMeasurement, - QuestionMark.FourthMeasurement, - }; - var answers = item.TableQuestionList.Where(x => questionMarks.Contains(x.QuestionMark)).Select(x=>x.Answer).ToList(); - if (answers.Count() == 4 && !answers.Any(x => x.IsNullOrEmpty())) - { - var avgAnswernum= answers.Select(x=>x.IsNullOrEmptyReturn0()).Average(x=>x); - avgAnswer = decimal.Round(avgAnswernum, inDto.DigitPlaces, MidpointRounding.AwayFromZero).ToString("F" + inDto.DigitPlaces.ToString()); - } - if(item.TableQuestionList.Where(x => x.QuestionMark== QuestionMark.IsMeasurable).Select(x => x.Answer).FirstOrDefault().EqEnum(YesOrNoOrNa.No)) - { - avgAnswer = "NE"; - } - tableAnswers.Add(new ReadingTableQuestionAnswer() - { - Answer = avgAnswer, - VisitTaskId = inDto.VisitTaskId, - QuestionId = avg.QuestionId, - TableQuestionId = avg.TableQuestionId, - TrialId = inDto.TrialId, - RowIndex = avg.RowIndex, - RowId = avg.RowId, + // foreach (var item in questionInfo.TableRowInfoList) + // { + // var avg = item.TableQuestionList.Where(x => x.QuestionMark == QuestionMark.AverageValue).FirstOrDefault(); + // var avgAnswer = string.Empty; + // List questionMarks = new List() + // { + // QuestionMark.FirstMeasurement, + // QuestionMark.SecondMeasurement, + // QuestionMark.ThirdMeasurement, + // QuestionMark.FourthMeasurement, + // }; + // var answers = item.TableQuestionList.Where(x => questionMarks.Contains(x.QuestionMark)).Select(x=>x.Answer).ToList(); + // if (answers.Count() == 4 && !answers.Any(x => x.IsNullOrEmpty())) + // { + // var avgAnswernum= answers.Select(x=>x.IsNullOrEmptyReturn0()).Average(x=>x); + // avgAnswer = decimal.Round(avgAnswernum, inDto.DigitPlaces, MidpointRounding.AwayFromZero).ToString("F" + inDto.DigitPlaces.ToString()); + // } + // if(item.TableQuestionList.Where(x => x.QuestionMark== QuestionMark.IsMeasurable).Select(x => x.Answer).FirstOrDefault().EqEnum(YesOrNoOrNa.No)) + // { + // avgAnswer = "NE"; + // } + // tableAnswers.Add(new ReadingTableQuestionAnswer() + // { + // Answer = avgAnswer, + // VisitTaskId = inDto.VisitTaskId, + // QuestionId = avg.QuestionId, + // TableQuestionId = avg.TableQuestionId, + // TrialId = inDto.TrialId, + // RowIndex = avg.RowIndex, + // RowId = avg.RowId, - }); + // }); - await _readingTableQuestionAnswerRepository.BatchDeleteNoTrackingAsync(x => x.VisitTaskId == inDto.VisitTaskId && x.RowId == item.RowId && x.TableQuestionId == avg.TableQuestionId); - } + // await _readingTableQuestionAnswerRepository.BatchDeleteNoTrackingAsync(x => x.VisitTaskId == inDto.VisitTaskId && x.RowId == item.RowId && x.TableQuestionId == avg.TableQuestionId); + // } - await _readingTableQuestionAnswerRepository.AddRangeAsync(tableAnswers); - await _readingTableQuestionAnswerRepository.SaveChangesAsync(); + // await _readingTableQuestionAnswerRepository.AddRangeAsync(tableAnswers); + // await _readingTableQuestionAnswerRepository.SaveChangesAsync(); - } + //} @@ -523,39 +523,46 @@ namespace IRaCIS.Core.Application.Service.ReadingCalculate var rowInfo = readingData.QuestionInfo.Where(x => x.LesionType == LesionType.FatFraction) .SelectMany(x => x.TableRowInfoList).ToList(); - var tableQuestionList = rowInfo.Where(x => x.TableQuestionList.Any(x => x.QuestionMark == QuestionMark.AverageValue && x.Answer.IsNotNullOrEmpty())).ToList(); + var tableQuestionList = rowInfo.Where(x => x.TableQuestionList.Any(x => x.QuestionMark == QuestionMark.MRIPDFF && x.Answer.IsNotNullOrEmpty())).ToList(); - if (tableQuestionList.Count() != 8) + if (tableQuestionList.Count() != 4) { throw new BusinessValidationFailedException(_localizer["MRIPDFF_AllNeedToBeMark"]); } - try - { - List questionMarkList = new List() { QuestionMark.FirstMeasurement, QuestionMark.SecondMeasurement, QuestionMark.ThirdMeasurement, QuestionMark.FourthMeasurement }; - var measuredValueList = rowInfo.SelectMany(x => x.TableQuestionList).Where(x =>x.Answer.IsNotNullOrEmpty()&& questionMarkList.Contains(x.QuestionMark)).Select(x => decimal.Parse(x.Answer)).ToList(); - if (measuredValueList.Any(x => x > 100)) - { - throw new BusinessValidationFailedException(_localizer["MRIPDFF_MeasurementGT100"]); - } - } - catch (Exception) - { + var instanceCount = await _readingTableAnswerRowInfoRepository.Where(x => x.VisitTaskId == inDto.VisitTaskId && x.InstanceId != null).Select(x => x.InstanceId).Distinct().CountAsync(); - throw new BusinessValidationFailedException(_localizer["MRIPDFF_MeasurementGT100"]); + if (instanceCount > 1) + { + throw new BusinessValidationFailedException(_localizer["MRIPDFFAdvance_MarkMustBeSameInstance"]); } - var notableQuestionList = rowInfo.Where(x => x.TableQuestionList.Any(x => x.QuestionMark == QuestionMark.IsMeasurable && x.Answer.EqEnum(YesOrNoOrNa.No))).ToList(); + //try + //{ + // List questionMarkList = new List() { QuestionMark.FirstMeasurement, QuestionMark.SecondMeasurement, QuestionMark.ThirdMeasurement, QuestionMark.FourthMeasurement }; + // var measuredValueList = rowInfo.SelectMany(x => x.TableQuestionList).Where(x =>x.Answer.IsNotNullOrEmpty()&& questionMarkList.Contains(x.QuestionMark)).Select(x => decimal.Parse(x.Answer)).ToList(); + // if (measuredValueList.Any(x => x > 100)) + // { + // throw new BusinessValidationFailedException(_localizer["MRIPDFF_MeasurementGT100"]); + // } + //} + //catch (Exception) + //{ + + // throw new BusinessValidationFailedException(_localizer["MRIPDFF_MeasurementGT100"]); + //} + + //var notableQuestionList = rowInfo.Where(x => x.TableQuestionList.Any(x => x.QuestionMark == QuestionMark.IsMeasurable && x.Answer.EqEnum(YesOrNoOrNa.No))).ToList(); - foreach (var item in notableQuestionList) - { - if (markList.Any(x => x.RowId == item.RowId && x.MeasureData.IsNotNullOrEmpty())) - { - throw new BusinessValidationFailedException(_localizer["MRIPDFF_NeedClearMark"]); - } - } - + //foreach (var item in notableQuestionList) + //{ + // if (markList.Any(x => x.RowId == item.RowId && x.MeasureData.IsNotNullOrEmpty())) + // { + // throw new BusinessValidationFailedException(_localizer["MRIPDFF_NeedClearMark"]); + // } + //} + } } } diff --git a/IRaCIS.Core.Application/Service/ReadingCalculate/MRIPDFFCalculateService.cs b/IRaCIS.Core.Application/Service/ReadingCalculate/MRIPDFFCalculateService.cs index 0b564dfd5..cbd2039a9 100644 --- a/IRaCIS.Core.Application/Service/ReadingCalculate/MRIPDFFCalculateService.cs +++ b/IRaCIS.Core.Application/Service/ReadingCalculate/MRIPDFFCalculateService.cs @@ -408,7 +408,7 @@ namespace IRaCIS.Core.Application.Service.ReadingCalculate var tableQuestionList = rowInfo.Where(x=>x.TableQuestionList.Any(x=>x.QuestionMark== QuestionMark.IsMeasurable && x.Answer.EqEnum(YesOrNoOrNa.Yes))).SelectMany(x => x.TableQuestionList).ToList(); - if (rowInfo.Any(x => x.TableQuestionList.Any(x => x.QuestionMark == QuestionMark.AverageValue && x.Answer.IsNullOrEmpty()))) + if (rowInfo.Any(x => x.TableQuestionList.Any(x => x.QuestionMark == QuestionMark.MRIPDFF && x.Answer.IsNullOrEmpty()))) { return string.Empty; } @@ -416,7 +416,7 @@ namespace IRaCIS.Core.Application.Service.ReadingCalculate { return "NE"; } - result = tableQuestionList.Where(x => x.QuestionMark == QuestionMark.AverageValue).Average(x => x.Answer.IsNullOrEmptyReturn0()); + result = tableQuestionList.Where(x => x.QuestionMark == QuestionMark.MRIPDFF).Average(x => x.Answer.IsNullOrEmptyReturn0()); return decimal.Round(result, inDto.DigitPlaces, MidpointRounding.AwayFromZero).ToString("F" + inDto.DigitPlaces.ToString()); ; } @@ -466,7 +466,7 @@ namespace IRaCIS.Core.Application.Service.ReadingCalculate foreach (var item in questionInfo.TableRowInfoList) { - var avg = item.TableQuestionList.Where(x => x.QuestionMark == QuestionMark.AverageValue).FirstOrDefault(); + var avg = item.TableQuestionList.Where(x => x.QuestionMark == QuestionMark.MRIPDFF).FirstOrDefault(); var avgAnswer = string.Empty; List questionMarks = new List() { @@ -522,7 +522,7 @@ namespace IRaCIS.Core.Application.Service.ReadingCalculate var rowInfo = readingData.QuestionInfo.Where(x => x.LesionType == LesionType.FatFraction) .SelectMany(x => x.TableRowInfoList).ToList(); - var tableQuestionList = rowInfo.Where(x => x.TableQuestionList.Any(x => x.QuestionMark == QuestionMark.AverageValue && x.Answer.IsNotNullOrEmpty())).ToList(); + var tableQuestionList = rowInfo.Where(x => x.TableQuestionList.Any(x => x.QuestionMark == QuestionMark.MRIPDFF && x.Answer.IsNotNullOrEmpty())).ToList(); if (tableQuestionList.Count() != 8) { diff --git a/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/TrialViewModel.cs b/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/TrialViewModel.cs index 613993009..9fc0f15d2 100644 --- a/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/TrialViewModel.cs +++ b/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/TrialViewModel.cs @@ -88,6 +88,8 @@ namespace IRaCIS.Application.Contracts public string CriterionName { get; set; } = string.Empty; + public DateTime CreateTime { get; set; } + public List TrialObjectNameList { get; set; } = new List(); } diff --git a/IRaCIS.Core.Application/Service/TrialSiteUser/TrialMaintenanceService.cs b/IRaCIS.Core.Application/Service/TrialSiteUser/TrialMaintenanceService.cs index 47d6a8612..6050eed50 100644 --- a/IRaCIS.Core.Application/Service/TrialSiteUser/TrialMaintenanceService.cs +++ b/IRaCIS.Core.Application/Service/TrialSiteUser/TrialMaintenanceService.cs @@ -35,7 +35,10 @@ namespace IRaCIS.Core.Application.Service [HttpPost] public async Task>> GetUserJoinedTrialList(IdentityUserJoinedTrialQuery inQuery) { - var list = await _trialIdentityUserRepository.Where(t => t.IdentityUserId == inQuery.IdentityUserId, false, true) + + var defalutSortArray = new string[] { nameof(IdentityUserJoinedTrialView.TrialCreateTime) + " desc" }; + + var list = await _trialIdentityUserRepository.Where(t => t.IdentityUserId == inQuery.IdentityUserId && t.Trial.IsDeleted==false, false, true) .WhereIf(!string.IsNullOrEmpty(inQuery.TrialCode), o => o.Trial.TrialCode.Contains(inQuery.TrialCode)) .WhereIf(!string.IsNullOrEmpty(inQuery.ResearchProgramNo), o => o.Trial.ResearchProgramNo.Contains(inQuery.ResearchProgramNo)) .WhereIf(!string.IsNullOrWhiteSpace(inQuery.ExperimentName), o => o.Trial.ExperimentName.Contains(inQuery.ExperimentName)) @@ -62,7 +65,7 @@ namespace IRaCIS.Core.Application.Service UpdateTime = t.UpdateTime, }).ToList(), - }).ToPagedListAsync(inQuery); + }).ToPagedListAsync(inQuery, defalutSortArray); var info = await _identityUserRepository.Where(t => t.Id == inQuery.IdentityUserId).Select(t => new { t.CreateTime, t.UserCeateSource, t.Trial.ResearchProgramNo, t.Trial.ExperimentName, t.Trial.TrialCode }).FirstOrDefaultAsync(); diff --git a/IRaCIS.Core.Application/Service/TrialSiteUser/TrialService.cs b/IRaCIS.Core.Application/Service/TrialSiteUser/TrialService.cs index 3dd75ddbd..362ea4ddc 100644 --- a/IRaCIS.Core.Application/Service/TrialSiteUser/TrialService.cs +++ b/IRaCIS.Core.Application/Service/TrialSiteUser/TrialService.cs @@ -144,7 +144,8 @@ namespace IRaCIS.Core.Application.Service .WhereIf(!other.Contains(_userInfo.UserTypeEnumInt), t => t.TrialIdentityUserList.Any(t => t.IdentityUserId == _userInfo.IdentityUserId && t.IsDeleted == false && t.TrialUserRoleList.Any(t => t.UserId == _userInfo.UserRoleId && t.IsDeleted == false)) && t.IsDeleted == false && t.TrialStatusStr == StaticData.TrialState.TrialOngoing) - .ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + .ProjectTo(_mapper.ConfigurationProvider) + .OrderByDescending(x=>x.CreateTime).ToListAsync(); } diff --git a/IRaCIS.Core.Application/TestService.cs b/IRaCIS.Core.Application/TestService.cs index 1d6230106..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; @@ -206,10 +183,18 @@ namespace IRaCIS.Core.Application.Service public Guid? SubjectVisitId { get; set; } public decimal VisitNum { get; set; } public string VisitName { get; set; } + + public string R1 { get; set; } + + public string R1阅片状态 { get; set; } + + public string R2 { get; set; } + + public string R2阅片状态 { get; set; } } [AllowAnonymous] - public async Task ExtralUndownloadImages() + public async Task ExtralUndownloadImages(Guid trialId) { var newVisits = MiniExcel.Query(@"C:\Users\PC\Desktop\New.xlsx").ToList(); @@ -219,7 +204,7 @@ namespace IRaCIS.Core.Application.Service foreach (var item in newVisits) { - if (oldVisits.Any(t => t.VisitNum == item.VisitNum && t.SubjectCode == item.SubjectCode)) + if (oldVisits.Any(t => t.VisitNum == item.VisitNum && t.SubjectCode.Trim() == item.SubjectCode.Trim())) { continue; } @@ -233,6 +218,77 @@ namespace IRaCIS.Core.Application.Service string exportPath = @$"C:\Users\PC\Desktop\newDownload.xlsx"; MiniExcel.SaveAs(exportPath, downloadVisit); + + #region 数据库查询 +// var downloadInfo = _trialRepository.Where(t => t.Id == trialId).Select(t => new +// { +// t.ResearchProgramNo, +// t.TrialCode, + +// VisitList = t.SubjectVisitList.Where(t => t.VisitTaskList.Any(t => t.TaskState == TaskState.Effect && t.ReadingCategory == ReadingCategory.Visit && t.ReadingTaskState != ReadingTaskState.HaveSigned && t.SourceSubjectVisitId != null && t.DoctorUserId != null)) +// //.Where(t=>subjectCodeList.Contains(t.Subject.Code)) +// .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(); + +// var acturalDownList = downloadInfo.VisitList.Where(t => !oldVisits.Any(old => old.VisitNum == t.VisitNum && old.SubjectCode == t.SubjectCode && +//old.VisitName.Trim().ToLower() == t.VisitName.Trim().ToLower())).ToList(); + + +// var diffList = downloadVisit.Where(t => !acturalDownList.Any(old => old.SubjectCode.Trim() == t.SubjectCode.Trim() && +//old.VisitName.Trim().ToLower() == t.VisitName.Trim().ToLower())).ToList(); + + + +// string diffPath = @$"C:\Users\PC\Desktop\diff.xlsx"; +// MiniExcel.SaveAs(diffPath, diffList); + + #endregion + return ResponseOutput.Ok(downloadVisit); } diff --git a/IRaCIS.Core.Domain.Share/Reading/ReadEnum.cs b/IRaCIS.Core.Domain.Share/Reading/ReadEnum.cs index d9ff423a0..6b8592eb7 100644 --- a/IRaCIS.Core.Domain.Share/Reading/ReadEnum.cs +++ b/IRaCIS.Core.Domain.Share/Reading/ReadEnum.cs @@ -2514,7 +2514,7 @@ namespace IRaCIS.Core.Domain.Share /// /// 平均值 /// - AverageValue = 1104, + MRIPDFF = 1104, /// /// 是否可测量 @@ -3237,6 +3237,11 @@ namespace IRaCIS.Core.Domain.Share /// 保存eCRF /// SaveEICRFQuestions = 12, + + /// + /// 保存Advance肝脏分段 + /// + SaveAdvanceLiverSegments = 13, } ///