using IRaCIS.Application.Interfaces; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using System; using System.Collections.Generic; using IRaCIS.Core.Application.Contracts.Dicom.DTO; using Microsoft.Net.Http.Headers; using Microsoft.AspNetCore.WebUtilities; using System.Threading.Tasks; using IRaCIS.Core.Application.Contracts.Dicom; using System.IO; using System.IO.Compression; using IRaCIS.Core.Application.Dicom; using Microsoft.Extensions.Logging; using IRaCIS.Core.Application.Filter; using IRaCIS.Core.Infrastructure.Extention; using EasyCaching.Core; using IRaCIS.Core.Infra.EFCore; using IRaCIS.Core.Application.Contracts; using IRaCIS.Core.Domain.Models; namespace IRaCIS.Api.Controllers { /// /// Study /// [Route("study")] [ApiController, Authorize, ApiExplorerSettings(GroupName = "Image")] public class StudyController : ControllerBase { private readonly IStudyService _studyService; private readonly IDicomArchiveService _dicomArchiveService; private readonly ILogger _logger; private IEasyCachingProvider _provider; private IUserInfo _userInfo; private static object _locker = new object(); public StudyController(IStudyService studyService, IDicomArchiveService dicomArchiveService, ILogger logger, IEasyCachingProvider provider, IUserInfo userInfo ) { _userInfo = userInfo; _provider = provider; _studyService = studyService; _dicomArchiveService = dicomArchiveService; _logger = logger; } /// 归档 [HttpPost, Route("archiveStudy/{trialId:guid}")] [DisableFormValueModelBinding] [DisableRequestSizeLimit] [TypeFilter(typeof(TrialResourceFilter))] public async Task ArchiveStudy([FromForm] ArchiveStudyCommand archiveStudyCommand) { //Stopwatch sw = new Stopwatch(); var startTime = DateTime.Now; //sw.Start(); if (_provider.Exists("StudyUid_" + archiveStudyCommand.StudyInstanceUid)) { return ResponseOutput.NotOk("当前已有人正在上传和归档该检查!"); } else { _provider.Set("StudyUid_" + archiveStudyCommand.StudyInstanceUid, _userInfo.Id, TimeSpan.FromMinutes(30)); } var archiveResult = new DicomArchiveResult(); var archivedStudyIds = new List(); var seriesInstanceUidList = new List(); var instanceUidList = new List(); //重传的时候,找出当前检查已经上传的series instance if (archiveStudyCommand.AbandonStudyId != null) { _studyService.GetHasUploadSeriesAndInstance(archiveStudyCommand.AbandonStudyId.Value, ref seriesInstanceUidList, ref instanceUidList); } var savedInfo = _studyService.GetSaveToDicomInfo(archiveStudyCommand.SubjectVisitId); 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) { //采用post方式 这里多加一个判断 过滤其他参数 if (string.IsNullOrEmpty(section.ContentType)) { section = await reader.ReadNextSectionAsync(); continue; } var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var contentDisposition); if (hasContentDispositionHeader) { string fileName = contentDisposition.FileName.Value; try { string mediaType = section.ContentType; if (mediaType.Contains("zip")) { var partStream = section.Body; using (var zipArchive = new ZipArchive(partStream, ZipArchiveMode.Read)) { foreach (var entry in zipArchive.Entries) { if (entry.FullName.EndsWith("/")) continue; try { ++archiveResult.ReceivedFileCount; using (var memoryStream = new MemoryStream()) { await section.Body.CopyToAsync(memoryStream); memoryStream.Seek(0, SeekOrigin.Begin); var archiveStudyId = await _dicomArchiveService.ArchiveDicomStreamAsync(memoryStream, savedInfo, seriesInstanceUidList, instanceUidList); if (!archivedStudyIds.Contains(archiveStudyId)) archivedStudyIds.Add(archiveStudyId); } } catch { archiveResult.ErrorFiles.Add($"{fileName}/{entry.FullName}"); } } } } ++archiveResult.ReceivedFileCount; if (mediaType.Contains("octet-stream")) { using (var memoryStream = new MemoryStream()) { await section.Body.CopyToAsync(memoryStream); memoryStream.Seek(0, SeekOrigin.Begin); var archiveStudyId = await _dicomArchiveService.ArchiveDicomStreamAsync(memoryStream, savedInfo, seriesInstanceUidList, instanceUidList); if (!archivedStudyIds.Contains(archiveStudyId)) archivedStudyIds.Add(archiveStudyId); } } } catch (Exception e) { _logger.LogError(e.Message + e.StackTrace); archiveResult.ErrorFiles.Add(fileName); _provider.Remove("StudyUid_" + archiveStudyCommand.StudyInstanceUid); } } section = await reader.ReadNextSectionAsync(); } if (archivedStudyIds.Count > 0) // 上传成功,处理逻辑 { // 同一个访视 多个线程上传处理 批量保存 可能造成死锁 https://www.cnblogs.com/johnblogs/p/9945767.html await _dicomArchiveService.DicomDBDataSaveChange(); //sw.Stop(); _studyService.UploadOrReUploadNeedTodo(archiveStudyCommand, archivedStudyIds, ref archiveResult, new StudyMonitor() { TrialId = savedInfo.TrialId, SiteId = savedInfo.SiteId, SubjectId = savedInfo.SubjectId, SubjectVisitId = savedInfo.SubjectVisitId, StudyId = archivedStudyIds[0], UploadStartTime = startTime, UploadFinishedTime = DateTime.Now, FileSize = (decimal)HttpContext.Request.ContentLength, FileCount = archiveResult.ReceivedFileCount, IsDicom = true, IsDicomReUpload = archiveStudyCommand.AbandonStudyId!=null, IP =_userInfo.IP }); _provider.Remove("StudyUid_" + archiveStudyCommand.StudyInstanceUid); } else { return ResponseOutput.NotOk("未完成该检查的归档", archiveResult); } return ResponseOutput.Ok(archiveResult); } #region 2021.12.14 整理废弃 //[Obsolete] //[HttpGet, Route("forwardStudy/{studyId:guid}/{trialId:guid}")] //[TrialAudit(AuditType.StudyAudit, AuditOptType.Forwarded)] //[TypeFilter(typeof(TrialResourceFilter))] //public IResponseOutput ForwardStudy(Guid studyId) //{ // return _studyService.ForwardStudy(studyId); //} ///// 指定资源Id,获取Dicom检查信息 ///// Dicom检查的Id //[HttpGet, Route("item/{studyId:guid}")] //[Obsolete] //public IResponseOutput GetStudyItem(Guid studyId) //{ // return ResponseOutput.Ok(_studyService.GetStudyItem(studyId)); //} //[Obsolete] //[HttpDelete, Route("deleteStudy/{id:guid}/{trialId:guid}")] //public IResponseOutput DeleteStudy(Guid id) //{ // return _studyService.DeleteStudy(id); //} //[Obsolete] //[HttpPost, Route("getStudyList")] //public IResponseOutput> GetStudyList(StudyQueryDTO queryDto) //{ // return ResponseOutput.Ok(_studyService.GetStudyList(queryDto)); //} /////// 指定资源Id,渲染Dicom检查的Jpeg预览图像 /////// Dicom检查的Id ////[HttpGet, Route("preview/{studyId:guid}")] ////[AllowAnonymous] ////public FileContentResult GetStudyPreview(Guid studyId) ////{ //// string path = _studyService.GetStudyPreview(studyId); //// using (var sw = DicomRenderingHelper.RenderPreviewJpeg(path)) //// { //// var bytes = new byte[sw.Length]; //// sw.Read(bytes, 0, bytes.Length); //// sw.Close(); //// return new FileContentResult(bytes, "image/jpeg"); //// } ////} ///// ///// Dicom匿名化 ///// ///// 需要匿名化的检查Id ///// //[HttpPost, Route("dicomAnonymize/{studyId:guid}/{trialId:guid}")] //[TrialAudit(AuditType.StudyAudit, AuditOptType.Anonymized)] //[Obsolete] //[TypeFilter(typeof(TrialResourceFilter))] //public async Task DicomAnonymize(Guid studyId) //{ // string userName = User.FindFirst("realName").Value; ; // return await _studyService.DicomAnonymize(studyId, userName); //} ///// ///// 获取受试者 这次访视 对应的study modality 列表 ///// ///// ///// ///// ///// ///// //[HttpPost, Route("getSubjectVisitStudyList/{trialId:guid}/{siteId:guid}/{subjectId:guid}/{subjectVisitId:guid}")] //[Obsolete] //[AllowAnonymous] //public IResponseOutput> GetSubjectVisitStudyList(Guid trialId, Guid siteId, Guid subjectId, // Guid subjectVisitId) //{ // return ResponseOutput.Ok(_studyService.GetSubjectVisitStudyList(trialId, siteId, subjectId, subjectVisitId)); //} //[HttpPost, Route("getDistributeStudyList")] //[Obsolete] //public IResponseOutput> GetDistributeStudyList(StudyStatusQueryDTO studyStatusQueryDto) //{ // return ResponseOutput.Ok(_studyService.GetDistributeStudyList(studyStatusQueryDto)); //} /////// 删除检查 ////[HttpDelete, Route("deleteStudy/{id:guid}/{trialId:guid}")] //// ////[TypeFilter(typeof(TrialResourceFilter))] ////public IResponseOutput DeleteStudy(Guid id) ////{ //// return _studyService.DeleteStudy(id); ////} ///// 更新Study状态,并保存状态变更信息 ///// //[HttpPost, Route("updateStudyStatus/{trialId:guid}")] //[TrialAudit(AuditType.StudyAudit, AuditOptType.ChangeStudyStatus)] //[Obsolete] //[TypeFilter(typeof(TrialResourceFilter))] //public IResponseOutput UpdateStudyStatus(StudyStatusDetailCommand studyStatusDetailCommand) //{ // return _studyService.UpdateStudyStatus(studyStatusDetailCommand); //} ///// ///// 根据项目Id 获取可选医生列表 ///// ///// ///// //[HttpGet, Route("GetReviewerList/{trialId:guid}")] //[Obsolete] //public IResponseOutput> GetReviewerListByTrialId(Guid trialId) //{ // var result = _studyService.GetReviewerListByTrialId(trialId); // return ResponseOutput.Ok(result); //} ///// ///// 根据StudyId获取该Study的操作记录,时间倒序 ///// ///// ///// //[HttpGet, Route("getStudyStatusDetailList/{studyId:guid}")] //[Obsolete] //public IResponseOutput> GetStudyStatusDetailList(Guid studyId) //{ // var result = _studyService.GetStudyStatusDetailList(studyId); // return ResponseOutput.Ok(result); //} ///// ///// 获取某个访视的关联访视 ///// 用于获取关联影像(调用之前的接口:/series/list/,根据StudyId,获取访视的序列列表) ///// ///// ///// ///// //[HttpGet, Route("getRelationVisitList/{visitNum}/{tpCode}")] //[Obsolete] //[AllowAnonymous] //public IResponseOutput> GetRelationVisitList(decimal visitNum, string tpCode) //{ // return ResponseOutput.Ok(_studyService.GetRelationVisitList(visitNum, tpCode)); //} ///// ///// 保存标记(跟删除合并,每次保存最新的标记),会删除替换之前的标记 ///// 外层的TPcode 必须传,里面的标记数组可为空数组,表示删除该Study的所有标记 ///// ///// ///// //[HttpPost, Route("saveImageLabelList")] //[AllowAnonymous] //[Obsolete] //public IResponseOutput SaveImageLabelList(ImageLabelCommand imageLabelCommand) //{ // return ResponseOutput.Result(_studyService.SaveImageLabelList(imageLabelCommand)); //} ///// ///// 根据TPCode 获取所有的标记 ///// ///// ///// //[HttpGet, Route("getImageLabelList/{tpCode}")] //[Obsolete] //[AllowAnonymous] //public IResponseOutput> GetImageLabelList(string tpCode) //{ // return ResponseOutput.Ok(_studyService.GetImageLabel(tpCode)); //} #endregion } }