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));
|
||
}
|
||
|
||
}
|
||
}
|