377 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			C#
		
	
	
			
		
		
	
	
			377 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			C#
		
	
	
| using FellowOakDicom.Network;
 | ||
| using FellowOakDicom;
 | ||
| using System;
 | ||
| using System.Collections.Generic;
 | ||
| using System.Linq;
 | ||
| using System.Text;
 | ||
| using System.Threading.Tasks;
 | ||
| using Microsoft.Extensions.Logging;
 | ||
| using Microsoft.Extensions.DependencyInjection;
 | ||
| using Microsoft.Extensions.Hosting;
 | ||
| using IRaCIS.Core.SCP.Service;
 | ||
| using IRaCIS.Core.Domain.Models;
 | ||
| using IRaCIS.Core.Infra.EFCore;
 | ||
| using Medallion.Threading;
 | ||
| using IRaCIS.Core.Domain.Share;
 | ||
| using Serilog;
 | ||
| using Microsoft.EntityFrameworkCore.Storage.ValueConversion.Internal;
 | ||
| using Microsoft.Extensions.Options;
 | ||
| using System.Data;
 | ||
| using FellowOakDicom.Imaging;
 | ||
| using SharpCompress.Common;
 | ||
| using SixLabors.ImageSharp.Formats.Jpeg;
 | ||
| using IRaCIS.Core.Infrastructure;
 | ||
| 
 | ||
| namespace IRaCIS.Core.SCP.Service
 | ||
| {
 | ||
| 
 | ||
|     public class DicomSCPServiceOption
 | ||
|     {
 | ||
|         public List<string> CalledAEList { get; set; }
 | ||
| 
 | ||
|         public string ServerPort { get; set; }
 | ||
|     }
 | ||
| 
 | ||
| 
 | ||
| 
 | ||
| 
 | ||
|     public class CStoreSCPService : DicomService, IDicomServiceProvider, IDicomCStoreProvider, IDicomCEchoProvider
 | ||
|     {
 | ||
|         private IServiceProvider _serviceProvider { get; set; }
 | ||
| 
 | ||
|         private List<Guid> _SCPStudyIdList { get; set; } = new List<Guid>();
 | ||
| 
 | ||
|         private SCPImageUpload _upload { get; set; }
 | ||
| 
 | ||
|         private Guid _trialId { get; set; }
 | ||
| 
 | ||
|         private Guid _trialSiteId { get; set; }
 | ||
| 
 | ||
| 
 | ||
| 
 | ||
|         private static readonly DicomTransferSyntax[] _acceptedTransferSyntaxes = new DicomTransferSyntax[]
 | ||
|         {
 | ||
|                DicomTransferSyntax.ExplicitVRLittleEndian,
 | ||
|                DicomTransferSyntax.ExplicitVRBigEndian,
 | ||
|                DicomTransferSyntax.ImplicitVRLittleEndian
 | ||
|         };
 | ||
| 
 | ||
|         private static readonly DicomTransferSyntax[] _acceptedImageTransferSyntaxes = new DicomTransferSyntax[]
 | ||
|         {
 | ||
|                 // Lossless
 | ||
|                DicomTransferSyntax.JPEGLSLossless,   //1.2.840.10008.1.2.4.80
 | ||
|                DicomTransferSyntax.JPEG2000Lossless, //1.2.840.10008.1.2.4.90
 | ||
|                DicomTransferSyntax.JPEGProcess14SV1, //1.2.840.10008.1.2.4.70
 | ||
|                DicomTransferSyntax.JPEGProcess14,    //1.2.840.10008.1.2.4.57 JPEG Lossless, Non-Hierarchical (Process 14)
 | ||
|                DicomTransferSyntax.RLELossless,      //1.2.840.10008.1.2.5
 | ||
|                // Lossy
 | ||
|                DicomTransferSyntax.JPEGLSNearLossless,//1.2.840.10008.1.2.4.81"
 | ||
|                DicomTransferSyntax.JPEG2000Lossy,     //1.2.840.10008.1.2.4.91
 | ||
|                DicomTransferSyntax.JPEGProcess1,      //1.2.840.10008.1.2.4.50
 | ||
|                DicomTransferSyntax.JPEGProcess2_4,    //1.2.840.10008.1.2.4.51
 | ||
|                // Uncompressed
 | ||
|                DicomTransferSyntax.ExplicitVRLittleEndian, //1.2.840.10008.1.2.1
 | ||
|                DicomTransferSyntax.ExplicitVRBigEndian,    //1.2.840.10008.1.2.2
 | ||
|                DicomTransferSyntax.ImplicitVRLittleEndian  //1.2.840.10008.1.2
 | ||
|         };
 | ||
| 
 | ||
| 
 | ||
|         public CStoreSCPService(INetworkStream stream, Encoding fallbackEncoding, Microsoft.Extensions.Logging.ILogger log, DicomServiceDependencies dependencies, IServiceProvider injectServiceProvider)
 | ||
|             : base(stream, fallbackEncoding, log, dependencies)
 | ||
|         {
 | ||
|             _serviceProvider = injectServiceProvider.CreateScope().ServiceProvider;
 | ||
|         }
 | ||
| 
 | ||
| 
 | ||
| 
 | ||
| 
 | ||
|         public Task OnReceiveAssociationRequestAsync(DicomAssociation association)
 | ||
|         {
 | ||
| 
 | ||
|             _upload = new SCPImageUpload() { StartTime = DateTime.Now, CallingAE = association.CallingAE, CalledAE = association.CalledAE, CallingAEIP = association.RemoteHost };
 | ||
| 
 | ||
| 
 | ||
|             Log.Logger.Warning($"接收到来自{association.CallingAE}的连接");
 | ||
| 
 | ||
|             //_serviceProvider = (IServiceProvider)this.UserState;
 | ||
| 
 | ||
|             var _trialDicomAERepository = _serviceProvider.GetService<IRepository<TrialDicomAE>>();
 | ||
| 
 | ||
| 
 | ||
|             var trialDicomAEList = _trialDicomAERepository.Select(t => new { t.CalledAE, t.TrialId }).ToList();
 | ||
|             var trialCalledAEList = trialDicomAEList.Select(t => t.CalledAE).ToList();
 | ||
| 
 | ||
|             Log.Logger.Information("当前系统配置:", string.Join('|', trialDicomAEList));
 | ||
| 
 | ||
|             var findCalledAE = trialDicomAEList.Where(t => t.CalledAE == association.CalledAE).FirstOrDefault();
 | ||
| 
 | ||
|             var isCanReceiveIamge = false;
 | ||
| 
 | ||
|             if (findCalledAE != null)
 | ||
|             {
 | ||
|                 _trialId = findCalledAE.TrialId;
 | ||
| 
 | ||
|                 var _trialSiteDicomAERepository = _serviceProvider.GetService<IRepository<TrialSiteDicomAE>>();
 | ||
| 
 | ||
| 
 | ||
|                 var findTrialSiteAE = _trialSiteDicomAERepository.Where(t => t.CallingAE == association.CallingAE && t.TrialId==_trialId).FirstOrDefault();
 | ||
| 
 | ||
|                 if (findTrialSiteAE != null)
 | ||
|                 {
 | ||
|                     _trialSiteId = findTrialSiteAE.TrialSiteId;
 | ||
| 
 | ||
|                     isCanReceiveIamge = true;
 | ||
|                 }
 | ||
| 
 | ||
|                
 | ||
|             }
 | ||
| 
 | ||
|             if (association.CallingAE == "test-callingAE")
 | ||
|             {
 | ||
|                 isCanReceiveIamge = true;
 | ||
|             }
 | ||
| 
 | ||
|             if (!trialCalledAEList.Contains(association.CalledAE) || isCanReceiveIamge == false)
 | ||
|             {
 | ||
| 
 | ||
|                 Log.Logger.Warning($"拒绝CallingAE:{association.CallingAE}  CalledAE:{association.CalledAE}的连接");
 | ||
| 
 | ||
|                 return SendAssociationRejectAsync(
 | ||
|                     DicomRejectResult.Permanent,
 | ||
|                     DicomRejectSource.ServiceUser,
 | ||
|                     DicomRejectReason.CalledAENotRecognized);
 | ||
|             }
 | ||
| 
 | ||
|             foreach (var pc in association.PresentationContexts)
 | ||
|             {
 | ||
|                 if (pc.AbstractSyntax == DicomUID.Verification)
 | ||
|                 {
 | ||
|                     pc.AcceptTransferSyntaxes(_acceptedTransferSyntaxes);
 | ||
|                 }
 | ||
|                 else if (pc.AbstractSyntax.StorageCategory != DicomStorageCategory.None)
 | ||
|                 {
 | ||
|                     pc.AcceptTransferSyntaxes(_acceptedImageTransferSyntaxes);
 | ||
|                 }
 | ||
|             }
 | ||
| 
 | ||
| 
 | ||
| 
 | ||
|             return SendAssociationAcceptAsync(association);
 | ||
|         }
 | ||
| 
 | ||
| 
 | ||
|         public async Task OnReceiveAssociationReleaseRequestAsync()
 | ||
|         {
 | ||
|             await DataMaintenanceAsaync();
 | ||
| 
 | ||
|             //记录监控
 | ||
| 
 | ||
|             var _SCPImageUploadRepository = _serviceProvider.GetService<IRepository<SCPImageUpload>>();
 | ||
| 
 | ||
|             _upload.EndTime = DateTime.Now;
 | ||
|             _upload.StudyCount = _SCPStudyIdList.Count;
 | ||
|             _upload.TrialId = _trialId;
 | ||
|             _upload.TrialSiteId = _trialSiteId;
 | ||
| 
 | ||
|             await _SCPImageUploadRepository.AddAsync(_upload, true);
 | ||
| 
 | ||
| 
 | ||
|             var _studyRepository = _serviceProvider.GetService<IRepository<SCPStudy>>();
 | ||
|             //将检查设置为传输结束
 | ||
|             await _studyRepository.BatchUpdateNoTrackingAsync(t => _SCPStudyIdList.Contains(t.Id), u => new SCPStudy() { IsUploadFinished = true });
 | ||
| 
 | ||
|             await _studyRepository.SaveChangesAndClearAllTrackingAsync();
 | ||
| 
 | ||
|             await SendAssociationReleaseResponseAsync();
 | ||
|         }
 | ||
| 
 | ||
| 
 | ||
|         private async Task DataMaintenanceAsaync()
 | ||
|         {
 | ||
|             Log.Logger.Warning($"CallingAE:{Association.CallingAE} CalledAE:{Association.CalledAE}传输结束:开始维护数据,处理检查Modality");
 | ||
| 
 | ||
| 
 | ||
| 
 | ||
|             //处理检查Modality
 | ||
|             var _dictionaryRepository = _serviceProvider.GetService<IRepository<Dictionary>>();
 | ||
|             var _seriesRepository = _serviceProvider.GetService<IRepository<SCPSeries>>();
 | ||
|             var _studyRepository = _serviceProvider.GetService<IRepository<SCPStudy>>();
 | ||
| 
 | ||
|             var dicModalityList = _dictionaryRepository.Where(t => t.Code == "Modality").SelectMany(t => t.ChildList.Select(c => c.Value)).ToList();
 | ||
|             var seriesModalityList = _seriesRepository.Where(t => _SCPStudyIdList.Contains(t.StudyId)).Select(t => new { SCPStudyId = t.StudyId, t.Modality }).ToList();
 | ||
| 
 | ||
|             foreach (var g in seriesModalityList.GroupBy(t => t.SCPStudyId))
 | ||
|             {
 | ||
|                 var modality = string.Join('、', g.Select(t => t.Modality).Distinct().ToList());
 | ||
| 
 | ||
|                 //特殊逻辑
 | ||
|                 var modalityForEdit = dicModalityList.Contains(modality) ? modality : String.Empty;
 | ||
| 
 | ||
|                 if (modality == "MR")
 | ||
|                 {
 | ||
|                     modalityForEdit = "MRI";
 | ||
|                 }
 | ||
| 
 | ||
|                 if (modality == "PT")
 | ||
|                 {
 | ||
|                     modalityForEdit = "PET";
 | ||
|                 }
 | ||
|                 if (modality == "PT、CT" || modality == "CT、PT")
 | ||
|                 {
 | ||
|                     modalityForEdit = "PET-CT";
 | ||
|                 }
 | ||
| 
 | ||
|                 await _studyRepository.BatchUpdateNoTrackingAsync(t => t.Id == g.Key, u => new SCPStudy() { Modalities = modality, ModalityForEdit = modalityForEdit });
 | ||
| 
 | ||
|             }
 | ||
| 
 | ||
|             Log.Logger.Warning($"CallingAE:{Association.CallingAE} CalledAE:{Association.CalledAE}维护数据结束");
 | ||
|         }
 | ||
| 
 | ||
|         public void OnReceiveAbort(DicomAbortSource source, DicomAbortReason reason)
 | ||
|         {
 | ||
|             Log.Logger.Warning($"CallingAE:{Association.CallingAE} CalledAE:{Association.CalledAE}接收中断,中断原因:{source.ToString() + reason.ToString()}");
 | ||
|             /* nothing to do here */
 | ||
|         }
 | ||
| 
 | ||
| 
 | ||
|         public async void OnConnectionClosed(Exception exception)
 | ||
|         {
 | ||
|             /* nothing to do here */
 | ||
| 
 | ||
|             //奇怪的bug  上传的时候,用王捷修改的影像,会关闭,重新连接,导致检查id 丢失,然后状态不一致
 | ||
|             if (exception == null)
 | ||
|             {
 | ||
|                 //var _studyRepository = _serviceProvider.GetService<IRepository<SCPStudy>>();
 | ||
|                 ////将检查设置为传输结束
 | ||
|                 //await _studyRepository.BatchUpdateNoTrackingAsync(t => _SCPStudyIdList.Contains(t.Id), u => new SCPStudy() { IsUploadFinished = true });
 | ||
| 
 | ||
|                 //await _studyRepository.SaveChangesAndClearAllTrackingAsync();
 | ||
|             }
 | ||
| 
 | ||
|             Log.Logger.Warning($"连接关闭 {exception?.Message} {exception?.InnerException?.Message}");
 | ||
|         }
 | ||
| 
 | ||
| 
 | ||
| 
 | ||
| 
 | ||
|         public async Task<DicomCStoreResponse> OnCStoreRequestAsync(DicomCStoreRequest request)
 | ||
|         {
 | ||
| 
 | ||
|             string studyInstanceUid = request.Dataset.GetString(DicomTag.StudyInstanceUID);
 | ||
|             string seriesInstanceUid = request.Dataset.GetString(DicomTag.SeriesInstanceUID);
 | ||
|             string sopInstanceUid = request.Dataset.GetString(DicomTag.SOPInstanceUID);
 | ||
| 
 | ||
|             //Guid studyId = IdentifierHelper.CreateGuid(studyInstanceUid, trialId.ToString());
 | ||
|             Guid seriesId = IdentifierHelper.CreateGuid(studyInstanceUid, seriesInstanceUid, _trialId.ToString());
 | ||
|             Guid instanceId = IdentifierHelper.CreateGuid(studyInstanceUid, seriesInstanceUid, sopInstanceUid, _trialId.ToString());
 | ||
| 
 | ||
| 
 | ||
|             var ossService = _serviceProvider.GetService<IOSSService>();
 | ||
|             var dicomArchiveService = _serviceProvider.GetService<IDicomArchiveService>();
 | ||
|             var _seriesRepository = _serviceProvider.GetService<IRepository<SCPSeries>>();
 | ||
| 
 | ||
|             var _distributedLockProvider = _serviceProvider.GetService<IDistributedLockProvider>();
 | ||
| 
 | ||
|             var storeRelativePath = string.Empty;
 | ||
|             var ossFolderPath = $"{_trialId}/Image/PACS/{_trialSiteId}/{studyInstanceUid}";
 | ||
| 
 | ||
| 
 | ||
|             long fileSize = 0;
 | ||
|             try
 | ||
|             {
 | ||
| 
 | ||
|                 using (MemoryStream ms = new MemoryStream())
 | ||
|                 {
 | ||
|                     await request.File.SaveAsync(ms);
 | ||
| 
 | ||
|                     //irc 从路径最后一截取Guid
 | ||
|                     storeRelativePath = await ossService.UploadToOSSAsync(ms, ossFolderPath, instanceId.ToString(), false);
 | ||
| 
 | ||
|                     fileSize = ms.Length;
 | ||
|                 }
 | ||
| 
 | ||
|                 Log.Logger.Information($"CallingAE:{Association.CallingAE} CalledAE:{Association.CalledAE} {request.SOPInstanceUID} 上传完成 ");
 | ||
| 
 | ||
|             }
 | ||
|             catch (Exception ec)
 | ||
|             {
 | ||
|                 Log.Logger.Warning($"CallingAE:{Association.CallingAE} CalledAE:{Association.CalledAE} 上传异常 {ec.Message}");
 | ||
|             }
 | ||
| 
 | ||
| 
 | ||
| 
 | ||
|             var @lock = _distributedLockProvider.CreateLock($"{studyInstanceUid}");
 | ||
| 
 | ||
|             using (await @lock.AcquireAsync())
 | ||
|             {
 | ||
|                 try
 | ||
|                 {
 | ||
|                     var scpStudyId = await dicomArchiveService.ArchiveDicomFileAsync(request.Dataset, _trialId, _trialSiteId, storeRelativePath, Association.CallingAE, Association.CalledAE,fileSize);
 | ||
| 
 | ||
|                     if (!_SCPStudyIdList.Contains(scpStudyId))
 | ||
|                     {
 | ||
|                         _SCPStudyIdList.Add(scpStudyId);
 | ||
|                     }
 | ||
| 
 | ||
|                     var series = await _seriesRepository.FirstOrDefaultAsync(t => t.Id == seriesId);
 | ||
| 
 | ||
|                     //没有缩略图
 | ||
|                     if (series != null && string.IsNullOrEmpty(series.ImageResizePath))
 | ||
|                     {
 | ||
| 
 | ||
|                         // 生成缩略图
 | ||
|                         using (var memoryStream = new MemoryStream())
 | ||
|                         {
 | ||
|                             DicomImage image = new DicomImage(request.Dataset);
 | ||
| 
 | ||
|                             var sharpimage = image.RenderImage().AsSharpImage();
 | ||
|                             sharpimage.Save(memoryStream, new JpegEncoder());
 | ||
| 
 | ||
|                             // 上传缩略图到 OSS
 | ||
| 
 | ||
|                             var seriesPath = await ossService.UploadToOSSAsync(memoryStream, ossFolderPath, seriesId.ToString() + ".preview.jpg", false);
 | ||
| 
 | ||
|                             Console.WriteLine(seriesPath + "  Id:  " + seriesId);
 | ||
| 
 | ||
|                             series.ImageResizePath = seriesPath;
 | ||
| 
 | ||
|                         }
 | ||
|                     }
 | ||
| 
 | ||
| 
 | ||
|                     await _seriesRepository.SaveChangesAsync();
 | ||
|                 }
 | ||
|                 catch (Exception ex)
 | ||
|                 {
 | ||
| 
 | ||
|                     Log.Logger.Warning($"CallingAE:{Association.CallingAE} CalledAE:{Association.CalledAE} 传输处理异常:{ex.ToString()}");
 | ||
| 
 | ||
|                 }
 | ||
| 
 | ||
|             }
 | ||
| 
 | ||
| 
 | ||
| 
 | ||
|             //监控信息设置
 | ||
|             _upload.FileCount++;
 | ||
|             _upload.FileSize = _upload.FileSize + fileSize;
 | ||
|             return new DicomCStoreResponse(request, DicomStatus.Success);
 | ||
|         }
 | ||
| 
 | ||
| 
 | ||
|         public Task OnCStoreRequestExceptionAsync(string tempFileName, Exception e)
 | ||
|         {
 | ||
|             // let library handle logging and error response
 | ||
|             return Task.CompletedTask;
 | ||
|         }
 | ||
| 
 | ||
| 
 | ||
|         public Task<DicomCEchoResponse> OnCEchoRequestAsync(DicomCEchoRequest request)
 | ||
|         {
 | ||
|             return Task.FromResult(new DicomCEchoResponse(request, DicomStatus.Success));
 | ||
|         }
 | ||
| 
 | ||
|     }
 | ||
| }
 |