using AutoMapper.Execution; using DocumentFormat.OpenXml.Bibliography; using FellowOakDicom; using FellowOakDicom.Network; using FellowOakDicom.Network.Client; using IRaCIS.Core.Application.Contracts; using IRaCIS.Core.Domain.Models; using IRaCIS.Core.Infra.EFCore; using IRaCIS.Core.Infrastructure.Extention; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Org.BouncyCastle.Bcpg; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Text; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; namespace IRaCIS.Core.API.HostService { public class DicomSCPService : DicomService, IDicomServiceProvider, IDicomCFindProvider, IDicomCEchoProvider, IDicomCMoveProvider { private static readonly DicomTransferSyntax[] _acceptedTransferSyntaxes = new DicomTransferSyntax[] { DicomTransferSyntax.ExplicitVRLittleEndian, DicomTransferSyntax.ExplicitVRBigEndian, DicomTransferSyntax.ImplicitVRLittleEndian }; private static readonly DicomTransferSyntax[] _acceptedImageTransferSyntaxes = new DicomTransferSyntax[] { // Lossless DicomTransferSyntax.JPEGLSLossless, DicomTransferSyntax.JPEG2000Lossless, DicomTransferSyntax.JPEGProcess14SV1, DicomTransferSyntax.JPEGProcess14, DicomTransferSyntax.RLELossless, // Lossy DicomTransferSyntax.JPEGLSNearLossless, DicomTransferSyntax.JPEG2000Lossy, DicomTransferSyntax.JPEGProcess1, DicomTransferSyntax.JPEGProcess2_4, // Uncompressed DicomTransferSyntax.ExplicitVRLittleEndian, DicomTransferSyntax.ExplicitVRBigEndian, DicomTransferSyntax.ImplicitVRLittleEndian }; private IServiceProvider _serviceProvider { get; set; } private DicomSCPServiceOption DicomSCPServiceConfig { get; set; } public string CallingAE { get; protected set; } public string CalledAE { get; protected set; } public DicomSCPService(INetworkStream stream, Encoding fallbackEncoding, Microsoft.Extensions.Logging.ILogger log, DicomServiceDependencies dependencies, IServiceProvider injectServiceProvider) : base(stream, fallbackEncoding, log, dependencies) { _serviceProvider = injectServiceProvider.CreateScope().ServiceProvider; } public void OnConnectionClosed(Exception exception) { if (exception != null) { Logger.LogError($"Closed, exception is {exception.Message}"); } } public void OnReceiveAbort(DicomAbortSource source, DicomAbortReason reason) { Logger.LogError($"Received abort from {source}, reason is {reason}"); } public Task OnReceiveAssociationReleaseRequestAsync() { return SendAssociationReleaseResponseAsync(); } public Task OnReceiveAssociationRequestAsync(DicomAssociation association) { CallingAE = association.CallingAE; CalledAE = association.CalledAE; Logger.LogInformation($"Received association request from AE: {CallingAE} with IP: {association.RemoteHost} "); DicomSCPServiceConfig = _serviceProvider.GetService>().CurrentValue; var calledAEList = DicomSCPServiceConfig.CalledAEList; //不支持三方服务 或者CallAE不对,那么拒绝连接 if (!calledAEList.Contains(CalledAE) || DicomSCPServiceConfig.IsSupportThirdService == false || CallingAE != DicomSCPServiceConfig.ThirdCallningAE) { Logger.LogError($"Association with {CallingAE} rejected since called aet {CalledAE} is unknown"); return SendAssociationRejectAsync(DicomRejectResult.Permanent, DicomRejectSource.ServiceUser, DicomRejectReason.CalledAENotRecognized); } foreach (var pc in association.PresentationContexts) { if (pc.AbstractSyntax == DicomUID.Verification || pc.AbstractSyntax == DicomUID.PatientRootQueryRetrieveInformationModelFind || pc.AbstractSyntax == DicomUID.PatientRootQueryRetrieveInformationModelMove || pc.AbstractSyntax == DicomUID.StudyRootQueryRetrieveInformationModelFind || pc.AbstractSyntax == DicomUID.StudyRootQueryRetrieveInformationModelMove) { pc.AcceptTransferSyntaxes(_acceptedTransferSyntaxes); } else if (pc.AbstractSyntax == DicomUID.PatientRootQueryRetrieveInformationModelGet || pc.AbstractSyntax == DicomUID.StudyRootQueryRetrieveInformationModelGet) { pc.AcceptTransferSyntaxes(_acceptedImageTransferSyntaxes); } else if (pc.AbstractSyntax.StorageCategory != DicomStorageCategory.None) { pc.AcceptTransferSyntaxes(_acceptedImageTransferSyntaxes); } else { Logger.LogWarning($"Requested abstract syntax {pc.AbstractSyntax} from {CallingAE} not supported"); pc.SetResult(DicomPresentationContextResult.RejectAbstractSyntaxNotSupported); } } Logger.LogInformation($"Accepted association request from {CallingAE}"); return SendAssociationAcceptAsync(association); } public Task OnCEchoRequestAsync(DicomCEchoRequest request) { Logger.LogInformation("Received verification request from AE {0} with IP: {1}", CallingAE, Association.RemoteHost); return Task.FromResult(new DicomCEchoResponse(request, DicomStatus.Success)); } public async IAsyncEnumerable OnCFindRequestAsync(DicomCFindRequest request) { Console.WriteLine("Received C-FIND request, forwarding to real PACS..."); var cts = new CancellationTokenSource(); var _dicomAERepository = _serviceProvider.GetService>(); var find = await _dicomAERepository.FirstOrDefaultAsync(t => t.PacsTypeEnum == PacsType.PacsServer && t.CalledAE == DicomSCPServiceConfig.ThirdSearchPacsAE); var hirClient = await _dicomAERepository.FirstOrDefaultAsync(t => t.PacsTypeEnum == PacsType.HIRClient); if (find == null || hirClient == null) { Logger.LogInformation("客户端和Pacs配置未查询到"); yield return new DicomCFindResponse(request, DicomStatus.ProcessingFailure); yield break; } string patientID = request.Dataset.GetSingleValueOrDefault(DicomTag.PatientID, string.Empty); string patientName = request.Dataset.GetSingleValueOrDefault(DicomTag.PatientName, string.Empty); string studyDate = request.Dataset.GetSingleValueOrDefault(DicomTag.StudyDate, string.Empty); string studyInstanceUID = request.Dataset.GetSingleValueOrDefault(DicomTag.StudyInstanceUID, string.Empty); string accessionNumber = request.Dataset.GetSingleValueOrDefault(DicomTag.AccessionNumber, string.Empty); if (patientID.IsNullOrEmpty() && patientName.IsNullOrEmpty() && studyInstanceUID.IsNullOrEmpty() && studyDate.IsNullOrEmpty() && accessionNumber.IsNullOrEmpty()) { yield return new DicomCFindResponse(request, DicomStatus.MissingAttribute); yield break; } // 创建 channel 用于异步传递响应 var channel = Channel.CreateUnbounded(); // 克隆 dataset 避免线程/状态冲突 var clonedDataset = request.Dataset?.Clone() ?? new DicomDataset(); var forward = new DicomCFindRequest(request.SOPClassUID, request.Level) { Dataset = clonedDataset }; var receivedCount = 0; // 标记是否已收到 final 状态(Success/Failure/Cancel) var finalReceived = false; // 当远端 PACS 返回响应时,异步写入 channel forward.OnResponseReceived += (rq, rp) => { #region 取消,现在不行 ////100条的时候直接取消 //if (receivedCount >= 10) //{ // rp.Status = DicomStatus.Cancel; // cts.Cancel(); // 触发取消 // Logger.LogWarning("超过100条,剩余的取消!"); //} //receivedCount++; #endregion var dsCopy = rp.Dataset?.Clone(); var proxyResp = new DicomCFindResponse(request, rp.Status) { Dataset = dsCopy }; channel.Writer.TryWrite(proxyResp); if (!rp.Status.Equals(DicomStatus.Pending)) { finalReceived = true; } }; // 异步发送到真实 PACS _ = Task.Run(async () => { try { var client = DicomClientFactory.Create(find.IP, find.Port, false, hirClient.CalledAE, find.CalledAE); await client.AddRequestAsync(forward); await client.SendAsync(cancellationToken: cts.Token); } catch (Exception ex) { Console.WriteLine("Error forwarding C-FIND: " + ex.Message); } finally { channel.Writer.Complete(); } }); // 异步 yield 返回给上游 await foreach (var resp in channel.Reader.ReadAllAsync()) { yield return resp; } // 兜底:如果没有 final 响应,返回 Success if (!finalReceived) { yield return new DicomCFindResponse(request, DicomStatus.Success); } } public async IAsyncEnumerable OnCMoveRequestAsync(DicomCMoveRequest request) { Console.WriteLine("Received C-Move request, forwarding to real PACS..."); var _dicomAERepository = _serviceProvider.GetService>(); var _cmoveStudyRepository = _serviceProvider.GetService>(); var find = await _dicomAERepository.FirstOrDefaultAsync(t => t.PacsTypeEnum == PacsType.PacsServer && t.CalledAE == DicomSCPServiceConfig.ThirdSearchPacsAE); var hirServer = await _dicomAERepository.FirstOrDefaultAsync(t => t.PacsTypeEnum == PacsType.HIRServer); var hirClient = await _dicomAERepository.FirstOrDefaultAsync(t => t.PacsTypeEnum == PacsType.HIRClient); if (find == null || hirClient == null || hirServer == null) { Logger.LogInformation("客户端和Pacs配置未查询到"); } var studyInstanceUid = request.Dataset?.GetSingleValueOrDefault(DicomTag.StudyInstanceUID, string.Empty); if (studyInstanceUid.IsNotNullOrEmpty()) { await _cmoveStudyRepository.AddAsync(new CmoveStudy() { CallingAE = CallingAE, CalledAE = CalledAE, DestinationAE = request.DestinationAE, StudyInstanceUIDList = new List() { studyInstanceUid }, HopitalGroupIdList = new List() }, true); } var channel = Channel.CreateUnbounded(); var clonedDataset = request.Dataset?.Clone() ?? new DicomDataset(); var forward = new DicomCMoveRequest(hirServer.CalledAE, studyInstanceUid) { Dataset = clonedDataset }; bool finalReceived = false; // PACS 返回响应时写入 channel forward.OnResponseReceived += (rq, rp) => { var dsCopy = rp.Dataset?.Clone(); var proxyResp = new DicomCMoveResponse(request, rp.Status) { Dataset = dsCopy, Remaining = rp.Remaining, Completed = rp.Completed, Failures = rp.Failures, Warnings = rp.Warnings, }; Logger.LogInformation($"Completed:{rp.Completed}"); channel.Writer.TryWrite(proxyResp); if (!rp.Status.Equals(DicomStatus.Pending)) { finalReceived = true; } }; // 异步发送到真实 PACS _ = Task.Run(async () => { try { var client = DicomClientFactory.Create(find.IP, find.Port, false, hirClient.CalledAE, find.CalledAE); await client.AddRequestAsync(forward); await client.SendAsync(); } catch (Exception ex) { Console.WriteLine("Error forwarding C-MOVE: " + ex.Message); } finally { channel.Writer.Complete(); } }); // 异步 yield 回上游 await foreach (var resp in channel.Reader.ReadAllAsync()) { yield return resp; } // 兜底 if (!finalReceived) { yield return new DicomCMoveResponse(request, DicomStatus.Success); } } } }