using AutoMapper; using DocumentFormat.OpenXml.ExtendedProperties; using ExcelDataReader; using FellowOakDicom; using FellowOakDicom.Imaging; using FellowOakDicom.IO.Buffer; using IRaCIS.Application.Contracts; using IRaCIS.Application.Interfaces; using IRaCIS.Core.API._ServiceExtensions.NewtonsoftJson; using IRaCIS.Core.Application.BusinessFilter; using IRaCIS.Core.Application.Contracts; using IRaCIS.Core.Application.Contracts.Dicom; using IRaCIS.Core.Application.Contracts.Dicom.DTO; using IRaCIS.Core.Application.Filter; using IRaCIS.Core.Application.Helper; using IRaCIS.Core.Application.MassTransit.Command; using IRaCIS.Core.Application.Service; using IRaCIS.Core.Domain.Models; using IRaCIS.Core.Domain.Share; using IRaCIS.Core.Infra.EFCore; using IRaCIS.Core.Infrastructure; using IRaCIS.Core.Infrastructure.Extention; using MassTransit.Mediator; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.StaticFiles; using Microsoft.AspNetCore.WebUtilities; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; using MiniExcelLibs; using Newtonsoft.Json; using NPOI.HPSF; using Serilog; using SharpCompress.Archives; using SharpCompress.Common; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Data; using System.IO; using System.IO.Compression; using System.Linq; using System.Text; using System.Threading.Tasks; using Path = System.IO.Path; namespace IRaCIS.Core.API.Controllers { #region 上传基类封装 [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter { public void OnResourceExecuting(ResourceExecutingContext context) { var factories = context.ValueProviderFactories; //factories.RemoveType(); factories.RemoveType(); factories.RemoveType(); context.HttpContext.Request.EnableBuffering(); } public void OnResourceExecuted(ResourceExecutedContext context) { } } [DisableFormValueModelBinding] public abstract class UploadBaseController : ControllerBase { /// 流式上传 直接返回 [Route("SingleFileUpload")] [ApiExplorerSettings(IgnoreApi = true)] public virtual async Task SingleFileUploadAsync(Func filePathFunc) { var boundary = HeaderUtilities.RemoveQuotes(MediaTypeHeaderValue.Parse(Request.ContentType).Boundary).Value; var reader = new MultipartReader(boundary, HttpContext.Request.Body); var section = await reader.ReadNextSectionAsync(); while (section != null) { var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var contentDisposition); if (hasContentDispositionHeader) { var (serverFilePath, relativePath) = filePathFunc(contentDisposition.FileName.Value); await FileStoreHelper.WriteFileAsync(section.Body, serverFilePath); //仅仅返回一个文件,如果多文件上传 在最后返回多个路径 return ResponseOutput.Ok(new { FilePath = relativePath, FullFilePath = relativePath /*+ "?access_token=" + _userInfo.UserToken*/ }); } section = await reader.ReadNextSectionAsync(); } return ResponseOutput.Ok(); } /// 流式上传 通用封装 不返回任何数据,后续还有事情处理 [Route("FileUpload")] [ApiExplorerSettings(IgnoreApi = true)] public virtual async Task FileUploadAsync(Func> filePathFunc) { var boundary = HeaderUtilities.RemoveQuotes(MediaTypeHeaderValue.Parse(Request.ContentType).Boundary).Value; var reader = new MultipartReader(boundary, HttpContext.Request.Body); var section = await reader.ReadNextSectionAsync(); while (section != null) { var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var contentDisposition); if (hasContentDispositionHeader) { var fileName = contentDisposition.FileName.Value; //处理压缩文件 if (fileName.Contains(".Zip", StringComparison.OrdinalIgnoreCase) || fileName.Contains(".rar", StringComparison.OrdinalIgnoreCase)) { var archive = ArchiveFactory.Open(section.Body); foreach (var entry in archive.Entries) { if (!entry.IsDirectory) { var serverFilePath = await filePathFunc(entry.Key); entry.WriteToFile(serverFilePath); } } } //普通单个文件 else { var serverFilePath = await filePathFunc(fileName); await FileStoreHelper.WriteFileAsync(section.Body, serverFilePath); } } section = await reader.ReadNextSectionAsync(); } } [Route("FileUploadToOSS")] [ApiExplorerSettings(IgnoreApi = true)] public virtual async Task FileUploadToOSSAsync(Func> toMemoryStreamFunc) { var boundary = HeaderUtilities.RemoveQuotes(MediaTypeHeaderValue.Parse(Request.ContentType).Boundary).Value; var reader = new MultipartReader(boundary, HttpContext.Request.Body); var section = await reader.ReadNextSectionAsync(); while (section != null) { var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var contentDisposition); if (hasContentDispositionHeader) { var fileName = contentDisposition.FileName.Value; await toMemoryStreamFunc(fileName, section.Body); } section = await reader.ReadNextSectionAsync(); } } /// 流式上传 Dicom上传 [Route("DicomFileUpload")] [ApiExplorerSettings(IgnoreApi = true)] public virtual async Task DicomFileUploadAsync(Func filePathFunc, string boundary) { var fileCount = 0; var reader = new MultipartReader(boundary, HttpContext.Request.Body); var section = await reader.ReadNextSectionAsync(); while (section != null) { var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var contentDisposition); if (hasContentDispositionHeader) { var fileName = contentDisposition.FileName.Value ?? String.Empty; string mediaType = section.ContentType ?? String.Empty; //处理压缩文件 if (fileName.Contains(".Zip", StringComparison.OrdinalIgnoreCase) || fileName.Contains(".rar", StringComparison.OrdinalIgnoreCase)) { var archive = ArchiveFactory.Open(section.Body); foreach (var entry in archive.Entries) { if (!entry.IsDirectory) { ++fileCount; await filePathFunc(entry.Key, entry.OpenEntryStream(), fileCount); } } } //普通单个文件 else { if (mediaType.Contains("octet-stream") || mediaType.Contains("dicom")) { ++fileCount; await filePathFunc(fileName, section.Body, fileCount); } } } section = await reader.ReadNextSectionAsync(); } } } #endregion #region Dicom 影像上传 临床数据 非diocm public class UploadNoneDicomFileCommand { [NotDefault] public Guid SubjectVisitId { get; set; } [NotDefault] public Guid StudyMonitorId { get; set; } public Guid? NoneDicomStudyId { get; set; } //IR 上传的时候跟任务绑定 public Guid? VisitTaskId { get; set; } public string RecordPath { get; set; } public int FailedFileCount { get; set; } public long FileSize { get; set; } public List UploadedFileList { get; set; } = new List(); public class OSSFileDTO { public string FilePath { get; set; } public string FileName { get; set; } public int FileFize { get; set; } public string FileType { get; set; } } } [ApiController, ApiExplorerSettings(GroupName = "Image")] public class StudyController( IMediator _mediator, QCCommon _qCCommon, IUserInfo _userInfo, IRepository _subjectVisitRepository, IStringLocalizer _localizer) : UploadBaseController { /// Dicom 归档 [HttpPost, Route("Study/ArchiveStudy")] [DisableFormValueModelBinding] [DisableRequestSizeLimit] [TrialGlobalLimit("AfterStopCannNotOpt")] public async Task ArchiveStudyNew(Guid trialId, Guid subjectVisitId, string studyInstanceUid, Guid? abandonStudyId, Guid studyMonitorId, [FromServices] ILogger _logger, [FromServices] IStudyService _studyService, [FromServices] IHubContext _uploadHub, [FromServices] IDicomArchiveService _dicomArchiveService, [FromServices] IRepository _studyMonitorRepository ) { if (!HttpContext.Request.HasFormContentType || !MediaTypeHeaderValue.TryParse(HttpContext.Request.ContentType, out var mediaTypeHeader) || string.IsNullOrEmpty(mediaTypeHeader.Boundary.Value)) { //---不支持的MediaType return ResponseOutput.NotOk(_localizer["UploadDownLoad_UnsupportedMedia"]); } var archiveStudyCommand = new ArchiveStudyCommand() { AbandonStudyId = abandonStudyId, StudyInstanceUid = studyInstanceUid, SubjectVisitId = subjectVisitId }; string studycode = string.Empty; var startTime = DateTime.Now; //到了接口,代表上传结束了 var studyMonitor = await _studyMonitorRepository.FirstOrDefaultAsync(t => t.Id == studyMonitorId); studyMonitor.UploadFinishedTime = DateTime.Now; var (archiveResult, archivedStudyIds) = (new DicomArchiveResult(), new List()); var (seriesInstanceUidList, sopInstanceUidList) = (new List(), new List()); //重传的时候,找出当前检查已经上传的series instance if (archiveStudyCommand.AbandonStudyId != null) { (seriesInstanceUidList, sopInstanceUidList) = _studyService.GetHasUploadSeriesAndInstance(archiveStudyCommand.AbandonStudyId.Value); } var savedInfo = _studyService.GetSaveToDicomInfo(archiveStudyCommand.SubjectVisitId); try { await DicomFileUploadAsync(async (fileName, fileStream, receivedCount) => { try { using (var memoryStream = new MemoryStream()) { await fileStream.CopyToAsync(memoryStream); memoryStream.Seek(0, SeekOrigin.Begin); var (studyId, studyCode) = await _dicomArchiveService.ArchiveDicomStreamAsync(memoryStream, savedInfo, seriesInstanceUidList, sopInstanceUidList); if (!archivedStudyIds.Contains(studyId)) { archivedStudyIds.Add(studyId); archiveResult.ArchivedDicomStudies.Add(new DicomStudyBasicDTO() { StudyCode = studyCode, Id = studyId }); } } //await _uploadHub.Clients.All.ReceivProgressAsync(archiveStudyCommand.StudyInstanceUid, receivedCount); await _uploadHub.Clients.User(_userInfo.UserRoleId.ToString()).ReceivProgressAsync(archiveStudyCommand.StudyInstanceUid, receivedCount); archiveResult.ReceivedFileCount = receivedCount; } catch (Exception e) { _logger.LogError(e.Message + e.StackTrace); archiveResult.ErrorFiles.Add(fileName); } }, mediaTypeHeader.Boundary.Value); } catch (Exception) { //---请求异常,请重试! throw new BusinessValidationFailedException(_localizer["UploadDownLoad_RequestError"]); } studyMonitor.FileSize = (long)HttpContext.Request.ContentLength; studyMonitor.FileCount = archiveResult.ReceivedFileCount; studyMonitor.FailedFileCount = archiveResult.ErrorFiles.Count; studyMonitor.IsDicomReUpload = archiveStudyCommand.AbandonStudyId != null; studyMonitor.Note = JsonConvert.SerializeObject(archiveResult); try { if (archivedStudyIds.Count > 0) // 上传成功,处理逻辑 { // 同一个访视 多个线程上传处理 批量保存 可能造成死锁 https://www.cnblogs.com/johnblogs/p/9945767.html await _dicomArchiveService.DicomDBDataSaveChange(); archiveResult.ReuploadNewStudyId = archivedStudyIds[0] == archiveStudyCommand.AbandonStudyId ? archivedStudyIds[0] : Guid.Empty; studyMonitor.IsSuccess = true; } } catch (Exception e) { studyMonitor.Note = JsonConvert.SerializeObject(new { Message = e.Message, Result = archiveResult }); _logger.LogError(e.Message + e.StackTrace); } finally { studyMonitor.StudyId = archiveResult.ArchivedDicomStudies.FirstOrDefault()?.Id ?? Guid.Empty; studyMonitor.StudyCode = archiveResult.ArchivedDicomStudies.FirstOrDefault()?.StudyCode; studyMonitor.ArchiveFinishedTime = DateTime.Now; await _studyMonitorRepository.SaveChangesAsync(); } return ResponseOutput.Result(studyMonitor.IsSuccess, archiveResult); } /// /// 非dicom 上传预上传接口 /// /// /// /// /// [HttpPost, Route("Study/PreArchiveStudy")] [TrialGlobalLimit("AfterStopCannNotOpt")] public async Task PreArchiveStudy(PreArchiveStudyCommand preArchiveStudyCommand, [FromServices] IStudyService _studyService, [FromServices] IRepository _studyMonitorRepository) { var savedInfo = _studyService.GetSaveToDicomInfo(preArchiveStudyCommand.SubjectVisitId); var studyMonitor = new StudyMonitor() { TrialId = savedInfo.TrialId, SubjectId = savedInfo.SubjectId, SubjectVisitId = savedInfo.SubjectVisitId, IsSuccess = false, UploadStartTime = DateTime.Now, FileCount = preArchiveStudyCommand.FileCount, IsDicom = preArchiveStudyCommand.IsDicom, IP = _userInfo.IP }; var addEntity = await _studyMonitorRepository.AddAsync(studyMonitor, true); return ResponseOutput.Ok(addEntity.Id); } /// /// 上传非Dicom 文件 支持压缩包 多文件上传 /// /// /// /// /// /// [HttpPost("NoneDicomStudy/UploadNoneDicomFile")] [TrialGlobalLimit("AfterStopCannNotOpt")] public async Task UploadNoneDicomFile(UploadNoneDicomFileCommand incommand, [FromServices] IRepository _noneDicomStudyRepository, [FromServices] IRepository _studyMonitorRepository, [FromServices] IRepository _noneDicomStudyFileRepository) { var subjectVisitId = incommand.SubjectVisitId; var studyMonitorId = incommand.StudyMonitorId; var noneDicomStudyId = incommand.NoneDicomStudyId; await _qCCommon.VerifyIsCRCSubmmitAsync(_subjectVisitRepository, _userInfo, subjectVisitId); var sv = (await _subjectVisitRepository.Where(t => t.Id == subjectVisitId).Select(t => new { t.TrialId, t.TrialSiteId, t.SubjectId }).FirstOrDefaultAsync()).IfNullThrowConvertException(); var studyMonitor = await _studyMonitorRepository.FirstOrDefaultAsync(t => t.Id == studyMonitorId); studyMonitor.UploadFinishedTime = DateTime.Now; foreach (var item in incommand.UploadedFileList) { //如果是跟任务绑,那么NoneDicomStudyId 设置为空,不影响之前的检查,同时设置 OriginNoneDicomStudyId 保证关系 if (incommand.VisitTaskId != null && incommand.VisitTaskId != Guid.Empty) { await _noneDicomStudyFileRepository.AddAsync(new NoneDicomStudyFile() { FileName = item.FileName, Path = item.FilePath, OriginNoneDicomStudyId = noneDicomStudyId.Value, VisitTaskId = incommand.VisitTaskId, FileType = item.FileType, FileSize = item.FileFize }); } else { await _noneDicomStudyFileRepository.AddAsync(new NoneDicomStudyFile() { FileName = item.FileName, Path = item.FilePath, NoneDicomStudyId = noneDicomStudyId.Value, FileType = item.FileType, FileSize = item.FileFize }); } } var uploadFinishedTime = DateTime.Now; var noneDicomStudy = await _noneDicomStudyRepository.FirstOrDefaultAsync((t => t.Id == noneDicomStudyId)); noneDicomStudy.FileCount = noneDicomStudy.FileCount + (incommand.VisitTaskId != null ? 0 : incommand.UploadedFileList.Count); studyMonitor.RecordPath = incommand.RecordPath; studyMonitor.FailedFileCount = incommand.FailedFileCount; studyMonitor.IsSuccess = incommand.FailedFileCount == 0; studyMonitor.FileSize = incommand.UploadedFileList.Sum(t => t.FileFize); studyMonitor.IsDicom = false; studyMonitor.IsDicomReUpload = false; studyMonitor.StudyId = noneDicomStudyId.Value; studyMonitor.StudyCode = noneDicomStudy.StudyCode; studyMonitor.ArchiveFinishedTime = DateTime.Now; studyMonitor.IP = _userInfo.IP; await _noneDicomStudyRepository.SaveChangesAsync(); return ResponseOutput.Ok(); } /// /// 一致性核查 excel上传 支持三种格式 /// /// /// /// /// /// [HttpPost("QCOperation/UploadVisitCheckExcel/{trialId:guid}")] [TrialGlobalLimit("AfterStopCannNotOpt")] public async Task UploadVisitCheckExcel(Guid trialId, [FromServices] IOSSService oSSService, [FromServices] IRepository _inspectionFileRepository) { var fileName = string.Empty; var templateFileStream = new MemoryStream(); await FileUploadToOSSAsync(async (realFileName, fileStream) => { fileName = realFileName; if (!fileName.EndsWith(".xlsx") && !fileName.EndsWith(".xls")) { //---支持.xlsx、.xls格式的文件上传。 throw new BusinessValidationFailedException(_localizer["UploadDownLoad_SupportedFormats"]); } fileStream.CopyTo(templateFileStream); templateFileStream.Seek(0, SeekOrigin.Begin); var ossRelativePath = await oSSService.UploadToOSSAsync(fileStream, "InspectionUpload/Check", realFileName); await _inspectionFileRepository.AddAsync(new InspectionFile() { FileName = realFileName, RelativePath = ossRelativePath, TrialId = trialId }); return ossRelativePath; }); var etcCheckList = new List(); #region MiniExcel 需要自己验证数据格式规范 if (fileName.EndsWith(".xlsx")) { etcCheckList = MiniExcel.Query(templateFileStream, excelType: ExcelType.XLSX).ToList(); } else if (fileName.EndsWith(".csv")) { //因为csv 需要加配置文件 不然都是null etcCheckList = MiniExcel.Query(templateFileStream, null, configuration: new MiniExcelLibs.Csv.CsvConfiguration() { StreamReaderFunc = (stream) => new StreamReader(stream, Encoding.GetEncoding("gb2312")) }).ToList(); } //if (fileName.EndsWith(".csv")) //{ // //因为csv 需要加配置文件 不然都是null // etcCheckList = MiniExcel.Query(filePath, null, configuration: config).ToList(); //} //else if (fileName.EndsWith(".xlsx")) //{ // // etcCheckList = MiniExcel.Query(filePath).ToList(); //} #endregion #region 升级net8 导入有问题 //Magicodes 支持自定义特性验证 // if (fileName.EndsWith(".xlsx")) //{ // var Importer = new ExcelImporter(); // var import = await Importer.Import(templateFileStream); // if (import.Exception != null) return ResponseOutput.NotOk(import.Exception.ToString()); // //if (import.RowErrors.Count > 0) return ResponseOutput.NotOk(JsonConvert.SerializeObject(import.RowErrors)); // if (import.TemplateErrors.Count > 0) return ResponseOutput.NotOk(_localizer["UploadDownLoad_TemplateErrors"]); // etcCheckList = import.Data.ToList(); //} //else if (fileName.EndsWith(".csv")) //{ // #region 临时方案 MiniExcel读取 然后保存为xlsx 再用 Magicodes验证数据 // //因为csv 需要加配置文件 不然都是null // etcCheckList = MiniExcel.Query(templateFileStream, null, configuration: new MiniExcelLibs.Csv.CsvConfiguration() // { // StreamReaderFunc = (stream) => new StreamReader(stream, Encoding.GetEncoding("gb2312")) // }).ToList(); // var (csVToXlsxPath, csVToXlsxRelativePath) = FileStoreHelper.GetTrialCheckFilePath(_hostEnvironment, Path.GetFileNameWithoutExtension(fileName) + ".xlsx", trialId); // await MiniExcel.SaveAsAsync(csVToXlsxPath, etcCheckList, excelType: ExcelType.XLSX); // var Importer = new ExcelImporter(); // var import = await Importer.Import(System.IO.File.OpenRead(csVToXlsxPath)); // if (import.Exception != null) return ResponseOutput.NotOk(import.Exception.ToString()); // //if (import.RowErrors.Count > 0) return ResponseOutput.NotOk(JsonConvert.SerializeObject(import.RowErrors)); // if (import.TemplateErrors.Count > 0) return ResponseOutput.NotOk(JsonConvert.SerializeObject(import.TemplateErrors)); // etcCheckList = import.Data.ToList(); // #endregion // #region 导入组件有问题 excel编码格式 // //var Importer = new CsvImporter(); // //var import = await Importer.Import(File.OpenRead(filePath)); // //if (import.Exception != null) return ResponseOutput.NotOk(import.Exception.ToString()); // //if (import.RowErrors.Count > 0) return ResponseOutput.NotOk(JsonConvert.SerializeObject(import.RowErrors)); // //if (import.TemplateErrors.Count > 0) return ResponseOutput.NotOk(JsonConvert.SerializeObject(import.TemplateErrors)); // //etcCheckList = import.Data.ToList(); // #endregion //} #endregion //ExcelReaderFactory 需要自己验证数据 并且从固定列取数据 else { //为了支持 xls 引入新的组件库 //using (var stream = System.IO.File.Open(templateFileStream, FileMode.Open, FileAccess.Read)) //{ // Auto-detect format, supports: // - Binary Excel files (2.0-2003 format; *.xls) // - OpenXml Excel files (2007 format; *.xlsx, *.xlsb) using (var reader = ExcelReaderFactory.CreateReader(templateFileStream)) { // 2. Use the AsDataSet extension method var dateset = reader.AsDataSet(); foreach (DataRow col in dateset.Tables[0].Rows) { etcCheckList.Add(new CheckViewModel() { SiteCode = col[0].ToString(), SubjectCode = col[1].ToString(), VisitName = col[2].ToString(), StudyDate = col[3].ToString(), Modality = col[4].ToString(), }); } etcCheckList.Remove(etcCheckList[0]); // The result of each spreadsheet is in result.Tables } } if (etcCheckList == null || etcCheckList.Count == 0) { //---请保证上传数据符合模板文件中的样式,且存在有效数据。 return ResponseOutput.NotOk(_localizer["UploadDownLoad_InvalidData"]); } else { //处理Excel 有时只是清除某些行的数据 读取也会读到数据,只是数据是null 后面处理的时候转为字符串为报错 etcCheckList.ForEach(t => { t.Modality = t.Modality ?? string.Empty; t.SiteCode = t.SiteCode ?? string.Empty; t.SubjectCode = t.SubjectCode ?? string.Empty; t.VisitName = t.VisitName ?? string.Empty; t.StudyDate = t.StudyDate ?? string.Empty; }); var dt = DateTime.Now; etcCheckList = etcCheckList.Where(t => !(t.Modality == string.Empty || t.SiteCode == string.Empty || t.SubjectCode == string.Empty || t.VisitName == string.Empty || t.StudyDate == string.Empty || !DateTime.TryParse(t.StudyDate, out dt))).ToList(); if (etcCheckList.Count == 0) { //---请保证上传数据符合模板文件中的样式,且存在有效数据。 return ResponseOutput.NotOk(_localizer["UploadDownLoad_InvalidData"]); } } // 适合获取结果的 //var client = _mediator.CreateRequestClient(); //await client.GetResponse(new ConsistenCheckCommand() { ETCList = etcCheckList, TrialId = trialId }); //不获取结果,不用定义返回类型 await _mediator.Send(new ConsistenCheckCommand() { ETCList = etcCheckList, TrialId = trialId }); return ResponseOutput.Ok(); } } #endregion #region 项目 系统 基本文件 上传 下载 预览 [ApiController, ApiExplorerSettings(GroupName = "Common")] public class UploadDownLoadController : UploadBaseController { public IMapper _mapper { get; set; } public IUserInfo _userInfo { get; set; } private readonly IMediator _mediator; public IStringLocalizer _localizer { get; set; } private readonly IWebHostEnvironment _hostEnvironment; public UploadDownLoadController(IMapper mapper, IUserInfo userInfo, IStringLocalizer localizer, IMediator mediator, IWebHostEnvironment hostEnvironment) { _hostEnvironment = hostEnvironment; _localizer = localizer; _mediator = mediator; _mapper = mapper; _userInfo = userInfo; } /// 通用文件下载 [AllowAnonymous] [HttpGet("CommonDocument/DownloadCommonDoc")] public async Task DownloadCommonFile(string code, [FromServices] IRepository _commonDocumentRepository) { var (filePath, fileName) = await FileStoreHelper.GetCommonDocPhysicalFilePathAsync(_hostEnvironment, _commonDocumentRepository, code); new FileExtensionContentTypeProvider().Mappings.TryGetValue(Path.GetExtension(filePath), out var contentType); return File(System.IO.File.OpenRead(filePath), contentType ?? "application/octet-stream", fileName); } /// /// 上传通用文档 比如一致性核查的 比如导出的excel 模板 /// /// [HttpPost("CommonDocument/UploadCommonDoc")] [DisableRequestSizeLimit] [DisableFormValueModelBinding] public async Task UploadCommonDoc() { return await SingleFileUploadAsync((fileName) => FileStoreHelper.GetCommonDocPath(_hostEnvironment, fileName)); } public enum UploadFileType { DataUpload = 1, DataDownload = 2, EmailAttachment = 3, EmailBodyHtml = 4, Other = 5 } /// /// 1:数据上传 2:导出、 3:邮件附件 4:邮件Html 通过 ----new /// /// [HttpPost("SystemFile/Upload")] [DisableRequestSizeLimit] [DisableFormValueModelBinding] public async Task Upload(UploadFileType fileType) { IResponseOutput result = null; switch (fileType) { case UploadFileType.DataUpload: result = await SingleFileUploadAsync((fileName) => FileStoreHelper.GetSystemFileUploadPath(_hostEnvironment, StaticData.Folder.DataTemplate, fileName)); break; case UploadFileType.DataDownload: result = await SingleFileUploadAsync((fileName) => FileStoreHelper.GetSystemFileUploadPath(_hostEnvironment, StaticData.Folder.DataTemplate, fileName)); break; case UploadFileType.EmailAttachment: result = await SingleFileUploadAsync((fileName) => FileStoreHelper.GetSystemFileUploadPath(_hostEnvironment, StaticData.Folder.EmailTemplate, fileName)); break; case UploadFileType.EmailBodyHtml: result = await SingleFileUploadAsync((fileName) => FileStoreHelper.GetSystemFileUploadPath(_hostEnvironment, StaticData.Folder.EmailTemplate, fileName)); break; default: result = await SingleFileUploadAsync((fileName) => FileStoreHelper.GetOtherFileUploadPath(_hostEnvironment, StaticData.Folder.TempFile, fileName)); break; } return result; } #endregion [HttpPost("download/PatientStudyBatchDownload")] public async Task DownloadPatientStudyBatch([FromServices] IPatientService _patientService, [FromServices] IOSSService _oSSService, [FromServices] IHubContext _downLoadHub, PatientImageDownloadCommand inCommand) { var rusult = await _patientService.GetDownloadPatientStudyInfo(inCommand); var patientList = rusult.Data; var downloadInfo = (SubejctVisitDownload)rusult.OtherData; long receivedSize = 0; long receivedCount = 0; long totalSize = downloadInfo.ImageSize; long totalCount = downloadInfo.ImageCount; var abortToken = HttpContext.RequestAborted; // -------- SignalR 节流参数 -------- var notifyInterval = TimeSpan.FromSeconds(1); var lastNotify = DateTime.UtcNow; // 用于计算下载速度 long lastReceivedSize = 0; DateTime lastSpeedCheck = DateTime.UtcNow; async Task NotifyProgressAsync(bool force = false) { var now = DateTime.UtcNow; var elapsedSeconds = (now - lastSpeedCheck).TotalSeconds; // 如果没有强制推送,并且未到推送间隔,则返回 if (!force && elapsedSeconds < notifyInterval.TotalSeconds) return; // 计算下载速度(字节/秒) double speedBps = 0; if (elapsedSeconds > 0) { speedBps = (receivedSize - lastReceivedSize) / elapsedSeconds; } lastSpeedCheck = now; lastReceivedSize = receivedSize; lastNotify = DateTime.UtcNow; var progress = new { CountPercent = totalCount > 0 ? Math.Round(receivedCount * 100m / totalCount, 2).ToString() + "%" : "0%", SizePercent = totalSize > 0 ? Math.Round(receivedSize * 100m / totalSize, 2).ToString() + "%" : "0%", Speed = (speedBps / 1024 >= 1024 ? (speedBps / 1024 / 1024).ToString("0.00") + " MB/s" : (speedBps / 1024).ToString("0.00") + " KB/s") }; // 不阻塞下载流程 _ = _downLoadHub.Clients .User(_userInfo.IdentityUserId.ToString()) .ReceivProgressAsync( inCommand.CurrentNoticeId, progress ); } Response.ContentType = "application/zip"; Response.Headers["Content-Disposition"] = $"attachment; filename=Image_{ExportExcelConverterDate.DateTimeInternationalToString(DateTime.Now, _userInfo.TimeZoneId)}.zip"; Response.Headers["Cache-Control"] = "no-store"; try { await using var responseStream = Response.BodyWriter.AsStream(); using var zip = new ZipArchive(responseStream, ZipArchiveMode.Create, leaveOpen: true); foreach (var patient in patientList) { foreach (var study in patient.StudyList) { abortToken.ThrowIfCancellationRequested(); var studyTime = study.StudyTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? "UnknownTime"; var modalitysStr = string.Join('_', study.SeriesList.Select(t => t.Modality).Distinct()); // ---------- DICOMDIR ---------- var dicomDirPath = $"{patient.PatientIdStr}/{studyTime}_{modalitysStr}/DICOMDIR"; var dicomDirEntry = zip.CreateEntry(dicomDirPath, CompressionLevel.Fastest); await using (var entryStream = dicomDirEntry.Open()) await using (var dirStream = await _oSSService.GetStreamFromOSSAsync(study.StudyDIRPath)) { await dirStream.CopyToAsync(entryStream, 32 * 1024, abortToken); } // ---------- IMAGE FILES ---------- foreach (var series in study.SeriesList) { foreach (var instance in series.InstanceList) { //当前完成大小 receivedSize = receivedSize + instance.FileSize ?? 0; receivedCount++; abortToken.ThrowIfCancellationRequested(); var entryPath = $"{patient.PatientIdStr}/{studyTime}_{modalitysStr}/IMAGE/{instance.FileName}"; var entry = zip.CreateEntry(entryPath, CompressionLevel.Fastest); await using var entryStream = entry.Open(); await using var source = await _oSSService.GetStreamFromOSSAsync(instance.Path); #region 将多帧合并为一帧 // 如果你是从 stream 打开 var dicomFile = await DicomFile.OpenAsync(source); // 获取 Pixel Data 标签 var pixelData = DicomPixelData.Create(dicomFile.Dataset); //获取像素是否为封装形式 var syntax = dicomFile.Dataset.InternalTransferSyntax; //对于封装像素的文件做转换 if (syntax.IsEncapsulated) { // 创建一个新的片段序列 var newFragments = new DicomOtherByteFragment(DicomTag.PixelData); // 获取每帧数据并封装为单独的片段 for (int n = 0; n < pixelData.NumberOfFrames; n++) { var frameData = pixelData.GetFrame(n); newFragments.Fragments.Add(new MemoryByteBuffer(frameData.Data)); } // 替换原有的片段序列 dicomFile.Dataset.AddOrUpdate(newFragments); } #endregion await dicomFile.SaveAsync(entryStream); //await source.CopyToAsync(entryStream, 32 * 1024, abortToken); await NotifyProgressAsync(); } } } } // 正常完成 await NotifyProgressAsync(true); await _patientService.DownloadImageSuccess(downloadInfo.Id); } catch (OperationCanceledException) { // ✅ 客户端取消 / 断开 —— 正常情况 Log.Logger.Warning("Download canceled by client"); } catch (IOException ex) when (abortToken.IsCancellationRequested) { // ✅ HttpClient 流在中断时的常见异常 Log.Logger.Warning($"Client disconnected: {ex.Message}"); } catch (NullReferenceException ex) when (abortToken.IsCancellationRequested) { // ✅ HttpConnection.ContentLengthReadStream 已知问题 Log.Logger.Warning($"Stream aborted: {ex.Message}"); } return new EmptyResult(); } } }