From c00b98daf8670143e401160f0814ff075714dfce Mon Sep 17 00:00:00 2001 From: hang <872297557@qq.com> Date: Thu, 29 May 2025 17:56:19 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BF=83=E5=8A=A8=E8=B6=85=E5=A3=B0=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=E5=A4=84=E7=90=86=E4=B8=8B=E8=BD=BD=EF=BC=8C=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0=EF=BC=8C=E9=A2=84=E5=A4=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BusinessFilter/ModelActionFilter .cs | 37 + .../BusinessFilter/ProjectExceptionFilter.cs | 61 ++ .../BusinessFilter/UnifiedApiResultFilter.cs | 120 +++ .../HostConfig/AutofacModuleSetup.cs | 57 ++ IRC.Core.Dicom/HostConfig/EFSetup.cs | 64 ++ .../HostConfig/NewtonsoftJsonSetup.cs | 59 ++ .../HostConfig/NullToEmptyStringResolver.cs | 36 + .../NullToEmptyStringValueProvider.cs | 42 + IRC.Core.Dicom/IRC.Core.Dicom.csproj | 45 + IRC.Core.Dicom/Program.cs | 186 +++++ IRC.Core.Dicom/Properties/launchSettings.json | 31 + IRC.Core.Dicom/Service/BaseService.cs | 111 +++ IRC.Core.Dicom/Service/CStoreSCPService.cs | 376 +++++++++ IRC.Core.Dicom/Service/DicomArchiveService.cs | 356 ++++++++ .../Service/Interface/IDicomArchiveService.cs | 11 + IRC.Core.Dicom/Service/OSSService.cs | 770 ++++++++++++++++++ IRC.Core.Dicom/appsettings.Development.json | 8 + IRC.Core.Dicom/appsettings.json | 9 + IRaCIS.Core.API.sln | 6 + 19 files changed, 2385 insertions(+) create mode 100644 IRC.Core.Dicom/BusinessFilter/ModelActionFilter .cs create mode 100644 IRC.Core.Dicom/BusinessFilter/ProjectExceptionFilter.cs create mode 100644 IRC.Core.Dicom/BusinessFilter/UnifiedApiResultFilter.cs create mode 100644 IRC.Core.Dicom/HostConfig/AutofacModuleSetup.cs create mode 100644 IRC.Core.Dicom/HostConfig/EFSetup.cs create mode 100644 IRC.Core.Dicom/HostConfig/NewtonsoftJsonSetup.cs create mode 100644 IRC.Core.Dicom/HostConfig/NullToEmptyStringResolver.cs create mode 100644 IRC.Core.Dicom/HostConfig/NullToEmptyStringValueProvider.cs create mode 100644 IRC.Core.Dicom/IRC.Core.Dicom.csproj create mode 100644 IRC.Core.Dicom/Program.cs create mode 100644 IRC.Core.Dicom/Properties/launchSettings.json create mode 100644 IRC.Core.Dicom/Service/BaseService.cs create mode 100644 IRC.Core.Dicom/Service/CStoreSCPService.cs create mode 100644 IRC.Core.Dicom/Service/DicomArchiveService.cs create mode 100644 IRC.Core.Dicom/Service/Interface/IDicomArchiveService.cs create mode 100644 IRC.Core.Dicom/Service/OSSService.cs create mode 100644 IRC.Core.Dicom/appsettings.Development.json create mode 100644 IRC.Core.Dicom/appsettings.json diff --git a/IRC.Core.Dicom/BusinessFilter/ModelActionFilter .cs b/IRC.Core.Dicom/BusinessFilter/ModelActionFilter .cs new file mode 100644 index 000000000..2f0ec4873 --- /dev/null +++ b/IRC.Core.Dicom/BusinessFilter/ModelActionFilter .cs @@ -0,0 +1,37 @@ +using IRaCIS.Core.Infrastructure.Extention; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.Localization; +using Newtonsoft.Json; + + +namespace IRaCIS.Core.SCP.Filter +{ + + + public class ModelActionFilter : ActionFilterAttribute, IActionFilter + { + public IStringLocalizer _localizer; + public ModelActionFilter(IStringLocalizer localizer) + { + _localizer = localizer; + } + + public override void OnActionExecuting(ActionExecutingContext context) + { + if (!context.ModelState.IsValid) + { + + var validationErrors = context.ModelState + .Keys + .SelectMany(k => context.ModelState[k]!.Errors) + .Select(e => e.ErrorMessage) + .ToArray(); + + //---提供给接口的参数无效。 + context.Result = new JsonResult(ResponseOutput.NotOk(_localizer["ModelAction_InvalidAPIParameter"] + JsonConvert.SerializeObject( validationErrors))); + } + } + } + +} diff --git a/IRC.Core.Dicom/BusinessFilter/ProjectExceptionFilter.cs b/IRC.Core.Dicom/BusinessFilter/ProjectExceptionFilter.cs new file mode 100644 index 000000000..0952bc457 --- /dev/null +++ b/IRC.Core.Dicom/BusinessFilter/ProjectExceptionFilter.cs @@ -0,0 +1,61 @@ +using IRaCIS.Core.Infrastructure; +using IRaCIS.Core.Infrastructure.Extention; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging; + +namespace IRaCIS.Core.SCP.Filter +{ + public class ProjectExceptionFilter : Attribute, IExceptionFilter + { + private readonly ILogger _logger; + + public IStringLocalizer _localizer; + + public ProjectExceptionFilter(IStringLocalizer localizer, ILogger logger) + { + _logger = logger; + _localizer = localizer; + } + public void OnException(ExceptionContext context) + { + //context.ExceptionHandled;//记录当前这个异常是否已经被处理过了 + + if (!context.ExceptionHandled) + { + if (context.Exception.GetType().Name == "DbUpdateConcurrencyException") + { + //---并发更新,当前不允许该操作 + context.Result = new JsonResult(ResponseOutput.NotOk(_localizer["ProjectException_ConcurrentUpdateNotAllowed"] + context.Exception.Message)); + } + + if (context.Exception.GetType() == typeof(BusinessValidationFailedException)) + { + var error = context.Exception as BusinessValidationFailedException; + + context.Result = new JsonResult(ResponseOutput.NotOk(context.Exception.Message, error!.Code)); + } + else if(context.Exception.GetType() == typeof(QueryBusinessObjectNotExistException)) + { + context.Result = new JsonResult(ResponseOutput.NotOk( context.Exception.Message, ApiResponseCodeEnum.DataNotExist)); + } + else + { + context.Result = new JsonResult(ResponseOutput.NotOk(_localizer["Project_ExceptionContactDeveloper"] + (context.Exception.InnerException is null ? (context.Exception.Message /*+ context.Exception.StackTrace*/) + : (context.Exception.InnerException?.Message /*+ context.Exception.InnerException?.StackTrace*/)), ApiResponseCodeEnum.ProgramException)); + } + + + _logger.LogError(context.Exception.InnerException is null ? (context.Exception.Message + context.Exception.StackTrace) : (context.Exception.InnerException?.Message + context.Exception.InnerException?.StackTrace)); + + + } + else + { + //继续 + } + context.ExceptionHandled = true;//标记当前异常已经被处理过了 + } + } +} diff --git a/IRC.Core.Dicom/BusinessFilter/UnifiedApiResultFilter.cs b/IRC.Core.Dicom/BusinessFilter/UnifiedApiResultFilter.cs new file mode 100644 index 000000000..69af4a5f9 --- /dev/null +++ b/IRC.Core.Dicom/BusinessFilter/UnifiedApiResultFilter.cs @@ -0,0 +1,120 @@ +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc; +using IRaCIS.Core.Domain.Share; +using IRaCIS.Core.Infrastructure.Extention; + +namespace IRaCIS.Core.Application.Service.BusinessFilter +{ + /// + /// 统一返回前端数据包装,之前在控制器包装,现在修改为动态Api 在ResultFilter这里包装,减少重复冗余代码 + /// by zhouhang 2021.09.12 周末 + /// + public class UnifiedApiResultFilter : Attribute, IAsyncResultFilter + { + /// + /// 异步版本 + /// + /// + /// + /// + public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) + { + + if (context.Result is ObjectResult objectResult) + { + var statusCode = objectResult.StatusCode ?? context.HttpContext.Response.StatusCode; + + //是200 并且没有包装 那么包装结果 + if (statusCode == 200 && !(objectResult.Value is IResponseOutput)) + { + //if (objectResult.Value == null) + //{ + // var apiResponse = ResponseOutput.DBNotExist(); + + // objectResult.Value = apiResponse; + // objectResult.DeclaredType = apiResponse.GetType(); + //} + //else + //{ + + var type = objectResult.Value?.GetType(); + + + if ( type!=null&& type.IsGenericType&&(type.GetGenericTypeDefinition()==typeof(ValueTuple<,>)|| type.GetGenericTypeDefinition()==typeof(Tuple<,>))) + { + + //报错 + //var tuple = (object, object))objectResult.Value; + + //var (val1, val2) = ((dynamic, dynamic))objectResult.Value; + //var apiResponse = ResponseOutput.Ok(val1, val2); + + //OK + var tuple = (dynamic)objectResult.Value; + var apiResponse = ResponseOutput.Ok(tuple.Item1, tuple.Item2); + + + objectResult.Value = apiResponse; + objectResult.DeclaredType = apiResponse.GetType(); + } + else + { + var apiResponse = ResponseOutput.Ok(objectResult.Value); + + objectResult.Value = apiResponse; + objectResult.DeclaredType = apiResponse.GetType(); + } + + + //} + + } + //如果不是200 是IResponseOutput 不处理 + else if (statusCode != 200 && (objectResult.Value is IResponseOutput)) + { + } + + else if(statusCode != 200&&!(objectResult.Value is IResponseOutput)) + { + //---程序错误,请联系开发人员。 + var apiResponse = ResponseOutput.NotOk(I18n.T("UnifiedAPI_ProgramError")); + + objectResult.Value = apiResponse; + objectResult.DeclaredType = apiResponse.GetType(); + } + + } + + await next.Invoke(); + + } + + public static bool IsTupleType(Type type, bool checkBaseTypes = false) + { + if (type == null) + throw new ArgumentNullException(nameof(type)); + + if (type == typeof(Tuple)) + return true; + + while (type != null) + { + if (type.IsGenericType) + { + var genType = type.GetGenericTypeDefinition(); + if (genType == typeof(Tuple<>) + || genType == typeof(Tuple<,>) + || genType == typeof(Tuple<,>)) + return true; + } + + if (!checkBaseTypes) + break; + + type = type.BaseType; + } + + return false; + } + } +} diff --git a/IRC.Core.Dicom/HostConfig/AutofacModuleSetup.cs b/IRC.Core.Dicom/HostConfig/AutofacModuleSetup.cs new file mode 100644 index 000000000..717d06fad --- /dev/null +++ b/IRC.Core.Dicom/HostConfig/AutofacModuleSetup.cs @@ -0,0 +1,57 @@ +using Autofac; +using IRaCIS.Core.Infra.EFCore; +using Microsoft.AspNetCore.Http; +using Panda.DynamicWebApi; +using System; +using System.Linq; +using System.Reflection; +using IRaCIS.Core.Domain.Models; +using IRaCIS.Core.Domain.Share; +using IRaCIS.Core.Application.Service; +using AutoMapper; +using IRaCIS.Core.SCP.Service; + +namespace IRaCIS.Core.SCP +{ + // ReSharper disable once IdentifierTypo + public class AutofacModuleSetup : Autofac.Module + { + protected override void Load(ContainerBuilder containerBuilder) + { + + #region byzhouhang 20210917 此处注册泛型仓储 可以减少Domain层 和Infra.EFcore 两层 空的仓储接口定义和 仓储文件定义 + + containerBuilder.RegisterGeneric(typeof(Repository<>)) + .As(typeof(IRepository<>)).InstancePerLifetimeScope();//注册泛型仓储 + + containerBuilder.RegisterType().As().InstancePerLifetimeScope(); + + + #endregion + + #region 指定控制器也由autofac 来进行实例获取 https://www.cnblogs.com/xwhqwer/p/15320838.html + + //获取所有控制器类型并使用属性注入 + containerBuilder.RegisterAssemblyTypes(typeof(BaseService).Assembly) + .Where(type => typeof(IDynamicWebApi).IsAssignableFrom(type)) + .PropertiesAutowired(); + + #endregion + + + + Assembly application = Assembly.LoadFrom(AppDomain.CurrentDomain.BaseDirectory + typeof(BaseService).Assembly.GetName().Name+".dll"); + containerBuilder.RegisterAssemblyTypes(application).Where(t => t.FullName.Contains("Service")) + .PropertiesAutowired().AsImplementedInterfaces(); + + + //containerBuilder.RegisterType().As().SingleInstance(); + //containerBuilder.RegisterType().As().InstancePerLifetimeScope(); + + + + + + } + } +} \ No newline at end of file diff --git a/IRC.Core.Dicom/HostConfig/EFSetup.cs b/IRC.Core.Dicom/HostConfig/EFSetup.cs new file mode 100644 index 000000000..709942cd6 --- /dev/null +++ b/IRC.Core.Dicom/HostConfig/EFSetup.cs @@ -0,0 +1,64 @@ +using EntityFramework.Exceptions.SqlServer; +using IRaCIS.Core.Domain.Share; +using IRaCIS.Core.Infra.EFCore; +using Medallion.Threading; +using Medallion.Threading.SqlServer; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace IRaCIS.Core.SCP +{ + public static class EFSetup + { + public static void AddEFSetup( this IServiceCollection services, IConfiguration configuration) + { + services.AddHttpContextAccessor(); + services.AddScoped(); + services.AddScoped(); + + + //这个注入没有成功--注入是没问题的,构造函数也只是支持参数就好,错在注入的地方不能写DbContext + //Web程序中通过重用池中DbContext实例可提高高并发场景下的吞吐量, 这在概念上类似于ADO.NET Provider原生的连接池操作方式,具有节省DbContext实例化成本的优点 + services.AddDbContext((sp, options) => + { + // 在控制台 + //public static readonly ILoggerFactory MyLoggerFactory = LoggerFactory.Create(builder => { builder.AddConsole(); }); + var logFactory = LoggerFactory.Create(builder => { builder.AddDebug(); }); + + options.UseSqlServer(configuration.GetSection("ConnectionStrings:RemoteNew").Value, + contextOptionsBuilder => contextOptionsBuilder.EnableRetryOnFailure()); + + options.UseLoggerFactory(logFactory); + + options.UseExceptionProcessor(); + + options.EnableSensitiveDataLogging(); + + options.AddInterceptors(new QueryWithNoLockDbCommandInterceptor()); + options.AddInterceptors(sp.GetServices()); + + options.UseProjectables(); + + + + }); + + //// Register an additional context factory as a Scoped service, which gets a pooled context from the Singleton factory we registered above, + //services.AddScoped(); + + //// Finally, arrange for a context to get injected from our Scoped factory: + //services.AddScoped(sp => sp.GetRequiredService().CreateDbContext()); + + //注意区分 easy caching 也有 IDistributedLockProvider + services.AddSingleton(sp => + { + //var connection = ConnectionMultiplexer.Connect(configuration["Redis:Configuration"]!); + + return new SqlDistributedSynchronizationProvider(configuration.GetSection("ConnectionStrings:RemoteNew").Value); + }); + + } + } +} diff --git a/IRC.Core.Dicom/HostConfig/NewtonsoftJsonSetup.cs b/IRC.Core.Dicom/HostConfig/NewtonsoftJsonSetup.cs new file mode 100644 index 000000000..00d5ae329 --- /dev/null +++ b/IRC.Core.Dicom/HostConfig/NewtonsoftJsonSetup.cs @@ -0,0 +1,59 @@ + + +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using System; + +namespace IRaCIS.Core.SCP +{ + public static class NewtonsoftJsonSetup + { + public static void AddNewtonsoftJsonSetup(this IMvcBuilder builder, IServiceCollection services) + { + services.AddHttpContextAccessor(); + services.AddScoped(); + + builder.AddNewtonsoftJson(options => + { + //options.SerializerSettings.PreserveReferencesHandling = PreserveReferencesHandling.Objects; + // 忽略循环引用 + options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; + //options.SerializerSettings.TypeNameHandling = TypeNameHandling.All; + + //处理返回给前端 可空类型 给出默认值 比如in? 为null 设置 默认值0 + options.SerializerSettings.ContractResolver = new NullToEmptyStringResolver(); //new DefaultContractResolver();// new NullToEmptyStringResolver(); + // 设置时间格式 + options.SerializerSettings.DateFormatString = "yyyy-MM-dd HH:mm:ss"; + + options.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.RoundtripKind; + + //options.SerializerSettings.Converters.Add(new JSONCustomDateConverter()) ; + + //options.SerializerSettings.Converters.Add(services.BuildServiceProvider().GetService()); + + + + }) + .AddControllersAsServices()//动态webApi属性注入需要 + .ConfigureApiBehaviorOptions(o => + { + o.SuppressModelStateInvalidFilter = true; //自己写验证 + + }); + + + Newtonsoft.Json.JsonSerializerSettings setting = new Newtonsoft.Json.JsonSerializerSettings(); + JsonConvert.DefaultSettings = new Func(() => + { + //日期类型默认格式化处理 + setting.DateFormatString = "yyyy-MM-dd HH:mm:ss"; + setting.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; + + return setting; + }); + + + } + } +} diff --git a/IRC.Core.Dicom/HostConfig/NullToEmptyStringResolver.cs b/IRC.Core.Dicom/HostConfig/NullToEmptyStringResolver.cs new file mode 100644 index 000000000..f4bd469a6 --- /dev/null +++ b/IRC.Core.Dicom/HostConfig/NullToEmptyStringResolver.cs @@ -0,0 +1,36 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace IRaCIS.Core.SCP +{ + public class NullToEmptyStringResolver : DefaultContractResolver + { + + + protected override IList CreateProperties(Type type, MemberSerialization memberSerialization) + { + IList properties = base.CreateProperties(type, memberSerialization); + + var list= type.GetProperties() + .Select(p => + { + var jp = base.CreateProperty(p, memberSerialization); + jp.ValueProvider = new NullToEmptyStringValueProvider(p); + return jp; + }).ToList(); + + var uu = list.Select(t => t.PropertyName).ToList(); + + //获取复杂对象属性 + properties = properties.TakeWhile(t => !uu.Contains(t.PropertyName)).ToList(); + + list.AddRange(properties); + return list; + } + + + } +} diff --git a/IRC.Core.Dicom/HostConfig/NullToEmptyStringValueProvider.cs b/IRC.Core.Dicom/HostConfig/NullToEmptyStringValueProvider.cs new file mode 100644 index 000000000..10e7e613d --- /dev/null +++ b/IRC.Core.Dicom/HostConfig/NullToEmptyStringValueProvider.cs @@ -0,0 +1,42 @@ +using System; +using System.Reflection; +using Newtonsoft.Json.Serialization; + +namespace IRaCIS.Core.SCP +{ + + public class NullToEmptyStringValueProvider : IValueProvider + { + PropertyInfo _MemberInfo; + public NullToEmptyStringValueProvider(PropertyInfo memberInfo) + { + _MemberInfo = memberInfo; + } + public object GetValue(object target) + { + object result = _MemberInfo.GetValue(target); + if (_MemberInfo.PropertyType == typeof(string) && result == null) result = ""; + else if (_MemberInfo.PropertyType == typeof(String[]) && result == null) result = new string[] { }; + //else if (_MemberInfo.PropertyType == typeof(Nullable) && result == null) result = 0; + else if (_MemberInfo.PropertyType == typeof(Nullable) && result == null) result = 0.00M; + + return result; + } + public void SetValue(object target, object value) + { + + if(_MemberInfo.PropertyType == typeof(string)) + { + //去掉前后空格 + _MemberInfo.SetValue(target, value==null?string.Empty: value.ToString()==string.Empty? value:value.ToString().Trim()); + + } + else + { + _MemberInfo.SetValue(target, value); + } + + } + } + +} diff --git a/IRC.Core.Dicom/IRC.Core.Dicom.csproj b/IRC.Core.Dicom/IRC.Core.Dicom.csproj new file mode 100644 index 000000000..d30f32218 --- /dev/null +++ b/IRC.Core.Dicom/IRC.Core.Dicom.csproj @@ -0,0 +1,45 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + + + + + + true + + + + + + + + + + + + + + + + + + + diff --git a/IRC.Core.Dicom/Program.cs b/IRC.Core.Dicom/Program.cs new file mode 100644 index 000000000..924632fd8 --- /dev/null +++ b/IRC.Core.Dicom/Program.cs @@ -0,0 +1,186 @@ + +using Autofac; +using Autofac.Extensions.DependencyInjection; +using AutoMapper.EquivalencyExpression; +using FellowOakDicom; +using FellowOakDicom.Imaging; +using FellowOakDicom.Imaging.NativeCodec; +using FellowOakDicom.Network; +using IRaCIS.Core.Infra.EFCore; +using IRaCIS.Core.SCP; +using IRaCIS.Core.SCP.Filter; +using IRaCIS.Core.SCP.Service; +using MassTransit; +using MassTransit.NewIdProviders; +using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.Extensions.DependencyInjection; +using Panda.DynamicWebApi; +using Serilog; +using Serilog.Events; +using System.Runtime.InteropServices; + + +//ļΪ׼ urlȡֵ(дݲļ˾ͲҪݻ) +var config = new ConfigurationBuilder() + .AddEnvironmentVariables() + .Build(); + +var enviromentName = config["ASPNETCORE_ENVIRONMENT"]; + +var builder = WebApplication.CreateBuilder(new WebApplicationOptions +{ + EnvironmentName = enviromentName +}); + + + +#region + +NewId.SetProcessIdProvider(new CurrentProcessIdProvider()); + +builder.Configuration.AddJsonFile("appsettings.json", false, true) + .AddJsonFile($"appsettings.{enviromentName}.json", false, true); +builder.Host + .UseServiceProviderFactory(new AutofacServiceProviderFactory()) + .ConfigureContainer(containerBuilder => + { + containerBuilder.RegisterModule(); + }) + .UseSerilog(); +#endregion + +#region ÷ +var _configuration = builder.Configuration; + +// +builder.Services.AddHealthChecks(); + +//ػ +builder.Services.AddJsonLocalization(options => options.ResourcesPath = "Resources"); + + +// 쳣ͳһ֤JsonлáַͳһTrim() +builder.Services.AddControllers(options => +{ + options.Filters.Add(); + options.Filters.Add(); + options.Filters.Add(); + + +}) + .AddNewtonsoftJsonSetup(builder.Services); // NewtonsoftJson л + + +builder.Services.AddOptions().Configure(_configuration.GetSection("AliyunOSS")); +builder.Services.AddOptions().Configure(_configuration.GetSection("ObjectStoreService")); +builder.Services.AddOptions().Configure(_configuration.GetSection("DicomSCPServiceConfig")); + + +//̬WebApi + UnifiedApiResultFilter ʡ +//̬webApi ĿǰڵΨһСapiϷϵĶ̬AOPʧЧ ӵòӰ +builder.Services + .AddDynamicWebApi(dynamicWebApiOption => + { + //Ĭ api + dynamicWebApiOption.DefaultApiPrefix = ""; + //ĸСд + dynamicWebApiOption.GetRestFulActionName = (actionName) => char.ToLower(actionName[0]) + actionName.Substring(1); + //ɾ Service׺ + dynamicWebApiOption.RemoveControllerPostfixes.Add("Service"); + + }); + +//AutoMapper +builder.Services.AddAutoMapper(automapper => +{ + + automapper.AddCollectionMappers(); + + +}, typeof(BaseService).Assembly); + +//EF ORM QueryWithNoLock +builder.Services.AddEFSetup(_configuration); + +builder.Services.AddMediator(cfg => +{ + +}); + + +//תͷ ȡʵIP +builder.Services.Configure(options => +{ + options.ForwardedHeaders = + ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; +}); + + +builder.Services.AddFellowOakDicom().AddTranscoderManager() + //.AddTranscoderManager() + .AddImageManager(); + + + +#endregion + +// Add services to the container. + +builder.Services.AddControllers(); +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +//if (app.Environment.IsDevelopment()) +//{ +app.UseSwagger(); +app.UseSwaggerUI(); +//} + +app.UseAuthorization(); + +app.MapControllers(); + +#region ־ + + +Log.Logger = new LoggerConfiguration() + //.MinimumLevel.Information() + .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) + .WriteTo.Console() + .WriteTo.File($"{AppContext.BaseDirectory}Serilogs/.log", rollingInterval: RollingInterval.Day) + .CreateLogger(); + +#endregion + + +#region л ƽ̨ + +Log.Logger.Warning($"ǰ{enviromentName}"); + +if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) +{ + Log.Logger.Warning($"ǰƽ̨windows"); +} +else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) +{ + Log.Logger.Warning($"ǰƽ̨linux"); +} +else +{ + Log.Logger.Warning($"ǰƽ̨OSX or FreeBSD"); +} + +#endregion + +DicomSetupBuilder.UseServiceProvider(app.Services); + +var logger = app.Services.GetService>(); + +var server = DicomServerFactory.Create(_configuration.GetSection("DicomSCPServiceConfig").GetValue("ServerPort"), userState: app.Services, logger: logger); + + +app.Run(); diff --git a/IRC.Core.Dicom/Properties/launchSettings.json b/IRC.Core.Dicom/Properties/launchSettings.json new file mode 100644 index 000000000..6ee896604 --- /dev/null +++ b/IRC.Core.Dicom/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:11224", + "sslPort": 0 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5127", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/IRC.Core.Dicom/Service/BaseService.cs b/IRC.Core.Dicom/Service/BaseService.cs new file mode 100644 index 000000000..f410074c3 --- /dev/null +++ b/IRC.Core.Dicom/Service/BaseService.cs @@ -0,0 +1,111 @@ +using AutoMapper; +using IRaCIS.Core.Application.Service.BusinessFilter; +using IRaCIS.Core.Infra.EFCore; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Localization; +using Panda.DynamicWebApi; +using Panda.DynamicWebApi.Attributes; +using System.Diagnostics.CodeAnalysis; +using IRaCIS.Core.Domain.Share; +using IRaCIS.Core.Infrastructure.Extention; +using IRaCIS.Core.Domain.Models; + +namespace IRaCIS.Core.SCP.Service +{ + +#pragma warning disable CS8618 + + + #region 非泛型版本 + + [Authorize, DynamicWebApi, UnifiedApiResultFilter] + public class BaseService : IBaseService, IDynamicWebApi + { + public IMapper _mapper { get; set; } + + public IUserInfo _userInfo { get; set; } + + + public IStringLocalizer _localizer { get; set; } + + public IWebHostEnvironment _hostEnvironment { get; set; } + + + + + public static IResponseOutput Null404NotFound(TEntity? businessObject) where TEntity : class + { + return new ResponseOutput() + .NotOk($"The query object {typeof(TEntity).Name} does not exist , or was deleted by someone else, or an incorrect parameter query caused", code: ApiResponseCodeEnum.DataNotExist); + } + } + + + public interface IBaseService + { + [MemberNotNull(nameof(_mapper))] + public IMapper _mapper { get; set; } + + [MemberNotNull(nameof(_userInfo))] + public IUserInfo _userInfo { get; set; } + + [MemberNotNull(nameof(_localizer))] + public IStringLocalizer _localizer { get; set; } + + [MemberNotNull(nameof(_hostEnvironment))] + public IWebHostEnvironment _hostEnvironment { get; set; } + + } + #endregion + + + #region 泛型版本测试 + + + public interface IBaseServiceTest where T : Entity + { + [MemberNotNull(nameof(_mapper))] + public IMapper _mapper { get; set; } + + [MemberNotNull(nameof(_userInfo))] + public IUserInfo _userInfo { get; set; } + + + + [MemberNotNull(nameof(_localizer))] + public IStringLocalizer _localizer { get; set; } + + + + } + + + [Authorize, DynamicWebApi, UnifiedApiResultFilter] + public class BaseServiceTest : IBaseServiceTest, IDynamicWebApi where T : Entity + { + public IMapper _mapper { get; set; } + + public IUserInfo _userInfo { get; set; } + + public IStringLocalizer _localizer { get; set; } + + public static IResponseOutput Null404NotFound(TEntity? businessObject) where TEntity : class + { + return new ResponseOutput() + .NotOk($"The query object {typeof(TEntity).Name} does not exist , or was deleted by someone else, or an incorrect parameter query caused", code: ApiResponseCodeEnum.DataNotExist); + } + + + } + + + #endregion + + + + + + + +} diff --git a/IRC.Core.Dicom/Service/CStoreSCPService.cs b/IRC.Core.Dicom/Service/CStoreSCPService.cs new file mode 100644 index 000000000..1fdecfc21 --- /dev/null +++ b/IRC.Core.Dicom/Service/CStoreSCPService.cs @@ -0,0 +1,376 @@ +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 CalledAEList { get; set; } + + public string ServerPort { get; set; } + } + + + + + public class CStoreSCPService : DicomService, IDicomServiceProvider, IDicomCStoreProvider, IDicomCEchoProvider + { + private IServiceProvider _serviceProvider { get; set; } + + private List _SCPStudyIdList { get; set; } = new List(); + + 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>(); + + + 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>(); + + + 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>(); + + _upload.EndTime = DateTime.Now; + _upload.StudyCount = _SCPStudyIdList.Count; + _upload.TrialId = _trialId; + _upload.TrialSiteId = _trialSiteId; + + await _SCPImageUploadRepository.AddAsync(_upload, true); + + + var _studyRepository = _serviceProvider.GetService>(); + //将检查设置为传输结束 + 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>(); + var _seriesRepository = _serviceProvider.GetService>(); + var _studyRepository = _serviceProvider.GetService>(); + + 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>(); + ////将检查设置为传输结束 + //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 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(); + var dicomArchiveService = _serviceProvider.GetService(); + var _seriesRepository = _serviceProvider.GetService>(); + + var _distributedLockProvider = _serviceProvider.GetService(); + + 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 OnCEchoRequestAsync(DicomCEchoRequest request) + { + return Task.FromResult(new DicomCEchoResponse(request, DicomStatus.Success)); + } + + } +} diff --git a/IRC.Core.Dicom/Service/DicomArchiveService.cs b/IRC.Core.Dicom/Service/DicomArchiveService.cs new file mode 100644 index 000000000..e522f6690 --- /dev/null +++ b/IRC.Core.Dicom/Service/DicomArchiveService.cs @@ -0,0 +1,356 @@ +using IRaCIS.Core.Domain.Share; +using System.Text; +using Microsoft.AspNetCore.Hosting; +using IRaCIS.Core.Infrastructure; +using Medallion.Threading; +using FellowOakDicom; +using FellowOakDicom.Imaging.Codec; +using System.Data; +using IRaCIS.Core.Domain.Models; +using FellowOakDicom.Network; +using IRaCIS.Core.SCP.Service; +using IRaCIS.Core.Infra.EFCore; +using MassTransit; +using System.Runtime.Intrinsics.X86; +using Serilog.Sinks.File; + +namespace IRaCIS.Core.SCP.Service +{ + public class DicomArchiveService : BaseService, IDicomArchiveService + { + private readonly IRepository _patientRepository; + private readonly IRepository _studyRepository; + private readonly IRepository _seriesRepository; + private readonly IRepository _instanceRepository; + private readonly IRepository _dictionaryRepository; + private readonly IDistributedLockProvider _distributedLockProvider; + + + private List _instanceIdList = new List(); + + public DicomArchiveService(IRepository patientRepository, IRepository studyRepository, + IRepository seriesRepository, + IRepository instanceRepository, + IRepository dictionaryRepository, + IDistributedLockProvider distributedLockProvider) + { + _distributedLockProvider = distributedLockProvider; + _studyRepository = studyRepository; + _patientRepository = patientRepository; + _seriesRepository = seriesRepository; + _instanceRepository = instanceRepository; + _dictionaryRepository = dictionaryRepository; + + } + + + + + /// + /// 单个文件接收 归档 + /// + /// + /// + /// + public async Task ArchiveDicomFileAsync(DicomDataset dataset, Guid trialId, Guid trialSiteId, string fileRelativePath, string callingAE, string calledAE,long fileSize) + { + string studyInstanceUid = dataset.GetString(DicomTag.StudyInstanceUID); + string seriesInstanceUid = dataset.GetString(DicomTag.SeriesInstanceUID); + string sopInstanceUid = dataset.GetString(DicomTag.SOPInstanceUID); + + string patientIdStr = dataset.GetSingleValueOrDefault(DicomTag.PatientID,string.Empty); + + //Guid patientId= IdentifierHelper.CreateGuid(patientIdStr); + 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 isStudyNeedAdd = false; + var isSeriesNeedAdd = false; + var isInstanceNeedAdd = false; + var isPatientNeedAdd = false; + + //var @lock = _distributedLockProvider.CreateLock($"{studyInstanceUid}"); + + //using (@lock.Acquire()) + { + var findPatient = await _patientRepository.FirstOrDefaultAsync(t => t.PatientIdStr == patientIdStr && t.TrialSiteId==trialSiteId ); + var findStudy = await _studyRepository.FirstOrDefaultAsync(t=>t.Id== studyId); + var findSerice = await _seriesRepository.FirstOrDefaultAsync(t => t.Id == seriesId); + var findInstance = await _instanceRepository.FirstOrDefaultAsync(t => t.Id == instanceId); + + DateTime? studyTime = dataset.GetSingleValueOrDefault(DicomTag.StudyDate, string.Empty) == string.Empty ? null : dataset.GetSingleValue(DicomTag.StudyDate).Add(dataset.GetSingleValueOrDefault(DicomTag.StudyTime, string.Empty) == string.Empty ? TimeSpan.Zero : dataset.GetSingleValue(DicomTag.StudyTime).TimeOfDay); + + //先传输了修改了患者编号的,又传输了没有修改患者编号的,导致后传输的没有修改患者编号的下面的检查为0 + if (findPatient == null && findStudy==null) + { + isPatientNeedAdd = true; + + + findPatient = new SCPPatient() + { + Id = NewId.NextSequentialGuid(), + TrialId=trialId, + TrialSiteId=trialSiteId, + PatientIdStr = dataset.GetSingleValueOrDefault(DicomTag.PatientID, string.Empty), + PatientName = dataset.GetSingleValueOrDefault(DicomTag.PatientName, string.Empty), + PatientAge = dataset.GetSingleValueOrDefault(DicomTag.PatientAge, string.Empty), + PatientSex = dataset.GetSingleValueOrDefault(DicomTag.PatientSex, string.Empty), + PatientBirthDate = dataset.GetSingleValueOrDefault(DicomTag.PatientBirthDate, string.Empty), + + EarliestStudyTime = studyTime, + LatestStudyTime = studyTime, + LatestPushTime = DateTime.Now, + }; + + if (findPatient.PatientBirthDate.Length == 8) + { + var birthDateStr = $"{findPatient.PatientBirthDate[0]}{findPatient.PatientBirthDate[1]}{findPatient.PatientBirthDate[2]}{findPatient.PatientBirthDate[3]}-{findPatient.PatientBirthDate[4]}{findPatient.PatientBirthDate[5]}-{findPatient.PatientBirthDate[6]}{findPatient.PatientBirthDate[7]}"; + + var yearStr = $"{findPatient.PatientBirthDate[0]}{findPatient.PatientBirthDate[1]}{findPatient.PatientBirthDate[2]}{findPatient.PatientBirthDate[3]}"; + + int year = 0; + + var canParse = int.TryParse(yearStr, out year); + + if (canParse && year > 1900) + { + findPatient.PatientBirthDate = birthDateStr; + + DateTime birthDate; + + if (findPatient.PatientAge == string.Empty && studyTime.HasValue && DateTime.TryParse(findPatient.PatientBirthDate,out birthDate)) + { + var patientAge = studyTime.Value.Year - birthDate.Year; + // 如果生日还未到,年龄减去一岁 + if (studyTime.Value < birthDate.AddYears(patientAge)) + { + patientAge--; + } + + findPatient.PatientAge = patientAge.ToString(); + } + + } + else + { + findPatient.PatientBirthDate = string.Empty; + } + + } + } + else + { + if (studyTime < findPatient.EarliestStudyTime) + { + findPatient.EarliestStudyTime = studyTime; + } + if (studyTime > findPatient.LatestStudyTime) + { + findPatient.LatestStudyTime = studyTime; + } + + findPatient.LatestPushTime = DateTime.Now; + } + + if (findStudy == null) + { + isStudyNeedAdd = true; + findStudy = new SCPStudy + { + CalledAE = calledAE, + CallingAE = callingAE, + + PatientId = findPatient.Id, + Id = studyId, + TrialId = trialId, + TrialSiteId = trialSiteId, + StudyInstanceUid = studyInstanceUid, + StudyTime = studyTime, + Modalities = dataset.GetSingleValueOrDefault(DicomTag.Modality, string.Empty), + //ModalityForEdit = modalityForEdit, + Description = dataset.GetSingleValueOrDefault(DicomTag.StudyDescription, string.Empty), + InstitutionName = dataset.GetSingleValueOrDefault(DicomTag.InstitutionName, string.Empty), + PatientIdStr = dataset.GetSingleValueOrDefault(DicomTag.PatientID, string.Empty), + PatientName = dataset.GetSingleValueOrDefault(DicomTag.PatientName, string.Empty), + PatientAge = dataset.GetSingleValueOrDefault(DicomTag.PatientAge, string.Empty), + PatientSex = dataset.GetSingleValueOrDefault(DicomTag.PatientSex, string.Empty), + BodyPartExamined = dataset.GetSingleValueOrDefault(DicomTag.BodyPartExamined, string.Empty), + + StudyId = dataset.GetSingleValueOrDefault(DicomTag.StudyID, string.Empty), + AccessionNumber = dataset.GetSingleValueOrDefault(DicomTag.AccessionNumber, string.Empty), + + //需要特殊处理 + PatientBirthDate = dataset.GetSingleValueOrDefault(DicomTag.PatientBirthDate, string.Empty), + + + AcquisitionTime = dataset.GetSingleValueOrDefault(DicomTag.AcquisitionTime, string.Empty), + AcquisitionNumber = dataset.GetSingleValueOrDefault(DicomTag.AcquisitionNumber, string.Empty), + TriggerTime = dataset.GetSingleValueOrDefault(DicomTag.TriggerTime, string.Empty), + + + + //IsDoubleReview = addtionalInfo.IsDoubleReview, + SeriesCount = 0, + InstanceCount = 0 + }; + + + if (findStudy.PatientBirthDate.Length == 8) + { + findStudy.PatientBirthDate = $"{findStudy.PatientBirthDate[0]}{findStudy.PatientBirthDate[1]}{findStudy.PatientBirthDate[2]}{findStudy.PatientBirthDate[3]}-{findStudy.PatientBirthDate[4]}{findStudy.PatientBirthDate[5]}-{findStudy.PatientBirthDate[6]}{findStudy.PatientBirthDate[7]}"; + } + } + + + if (findSerice == null) + { + isSeriesNeedAdd = true; + + findSerice = new SCPSeries + { + Id = seriesId, + StudyId = findStudy.Id, + + StudyInstanceUid = findStudy.StudyInstanceUid, + SeriesInstanceUid = seriesInstanceUid, + SeriesNumber = dataset.GetSingleValueOrDefault(DicomTag.SeriesNumber, 1), + //SeriesTime = dataset.GetSingleValueOrDefault(DicomTag.SeriesDate, DateTime.Now).Add(dataset.GetSingleValueOrDefault(DicomTag.SeriesTime, DateTime.Now).TimeOfDay), + //SeriesTime = DateTime.TryParse(dataset.GetSingleValue(DicomTag.SeriesDate) + dataset.GetSingleValue(DicomTag.SeriesTime), out DateTime dt) ? dt : null, + SeriesTime = dataset.GetSingleValueOrDefault(DicomTag.SeriesDate, string.Empty) == string.Empty ? null : dataset.GetSingleValue(DicomTag.SeriesDate).Add(dataset.GetSingleValueOrDefault(DicomTag.SeriesTime, string.Empty) == string.Empty ? TimeSpan.Zero : dataset.GetSingleValue(DicomTag.SeriesTime).TimeOfDay), + Modality = dataset.GetSingleValueOrDefault(DicomTag.Modality, string.Empty), + Description = dataset.GetSingleValueOrDefault(DicomTag.SeriesDescription, string.Empty), + SliceThickness = dataset.GetSingleValueOrDefault(DicomTag.SliceThickness, string.Empty), + + ImagePositionPatient = dataset.GetSingleValueOrDefault(DicomTag.ImagePositionPatient, string.Empty), + ImageOrientationPatient = dataset.GetSingleValueOrDefault(DicomTag.ImageOrientationPatient, string.Empty), + BodyPartExamined = dataset.GetSingleValueOrDefault(DicomTag.BodyPartExamined, string.Empty), + SequenceName = dataset.GetSingleValueOrDefault(DicomTag.SequenceName, string.Empty), + ProtocolName = dataset.GetSingleValueOrDefault(DicomTag.ProtocolName, string.Empty), + ImagerPixelSpacing = dataset.GetSingleValueOrDefault(DicomTag.ImagerPixelSpacing, string.Empty), + + AcquisitionTime = dataset.GetSingleValueOrDefault(DicomTag.AcquisitionTime, string.Empty), + AcquisitionNumber = dataset.GetSingleValueOrDefault(DicomTag.AcquisitionNumber, string.Empty), + TriggerTime = dataset.GetSingleValueOrDefault(DicomTag.TriggerTime, string.Empty), + + + InstanceCount = 0 + }; + + ++findStudy.SeriesCount; + } + + + if (findInstance == null) + { + isInstanceNeedAdd = true; + findInstance = new SCPInstance + { + Id = instanceId, + StudyId = findStudy.Id, + SeriesId = findSerice.Id, + StudyInstanceUid = findStudy.StudyInstanceUid, + SeriesInstanceUid = findSerice.SeriesInstanceUid, + + SopInstanceUid = sopInstanceUid, + InstanceNumber = dataset.GetSingleValueOrDefault(DicomTag.InstanceNumber, 1), + InstanceTime = dataset.GetSingleValueOrDefault(DicomTag.ContentDate, string.Empty) == string.Empty ? null : dataset.GetSingleValue(DicomTag.ContentDate).Add(dataset.GetSingleValueOrDefault(DicomTag.ContentTime, string.Empty) == string.Empty ? TimeSpan.Zero : dataset.GetSingleValue(DicomTag.ContentTime).TimeOfDay), + //InstanceTime = DateTime.TryParse(dataset.GetSingleValue(DicomTag.ContentDate) + dataset.GetSingleValue(DicomTag.ContentTime), out DateTime dt) ? dt : null, + //InstanceTime = dataset.GetSingleValueOrDefault(DicomTag.ContentDate,(DateTime?)null)?.Add(dataset.GetSingleValueOrDefault(DicomTag.ContentTime, TimeSpan.Zero)), + //dataset.GetSingleValueOrDefault(DicomTag.ContentDate,DateTime.Now);//, DicomTag.ContentTime) + CPIStatus = false, + ImageRows = dataset.GetSingleValueOrDefault(DicomTag.Rows, 0), + ImageColumns = dataset.GetSingleValueOrDefault(DicomTag.Columns, 0), + SliceLocation = dataset.GetSingleValueOrDefault(DicomTag.SliceLocation, 0), + + SliceThickness = dataset.GetSingleValueOrDefault(DicomTag.SliceThickness, string.Empty), + NumberOfFrames = dataset.GetSingleValueOrDefault(DicomTag.NumberOfFrames, 0), + PixelSpacing = dataset.GetSingleValueOrDefault(DicomTag.PixelSpacing, string.Empty), + ImagerPixelSpacing = dataset.GetSingleValueOrDefault(DicomTag.ImagerPixelSpacing, string.Empty), + FrameOfReferenceUID = dataset.GetSingleValueOrDefault(DicomTag.FrameOfReferenceUID, string.Empty), + WindowCenter = dataset.GetSingleValueOrDefault(DicomTag.WindowCenter, string.Empty), + WindowWidth = dataset.GetSingleValueOrDefault(DicomTag.WindowWidth, string.Empty), + + Path = fileRelativePath, + + FileSize= fileSize, + + }; + + ++findStudy.InstanceCount; + ++findSerice.InstanceCount; + } + + if (isPatientNeedAdd) + { + var ss = await _patientRepository.AddAsync(findPatient); + } + if (isStudyNeedAdd) + { + var dd = await _studyRepository.AddAsync(findStudy); + } + else + { + await _studyRepository.BatchUpdateNoTrackingAsync(t => t.Id == findStudy.Id, t => new SCPStudy() { IsUploadFinished = false }); + } + + if (isSeriesNeedAdd) + { + await _seriesRepository.AddAsync(findSerice); + } + if (isInstanceNeedAdd) + { + await _instanceRepository.AddAsync(findInstance); + } + else + { + await _instanceRepository.BatchUpdateNoTrackingAsync(t => t.Id == instanceId, u => new SCPInstance() { Path = fileRelativePath,FileSize=fileSize }); + } + + await _studyRepository.SaveChangesAsync(); + + return findStudy.Id; + } + + } + + + // 从DICOM文件中获取使用的字符集 + private string GetEncodingVaulueFromDicomFile(DicomDataset dataset, DicomTag dicomTag) + { + + // 获取DICOM文件的特定元素,通常用于指示使用的字符集 + var charset = dataset.GetSingleValueOrDefault(DicomTag.SpecificCharacterSet, string.Empty); + + var dicomEncoding = DicomEncoding.GetEncoding(charset); + + + var dicomStringElement = dataset.GetDicomItem(dicomTag); + + var bytes = dicomStringElement.Buffer.Data; + + + return dicomEncoding.GetString(bytes); + + + //// 从DICOM文件中获取使用的字符集 + //string filePath = "C:\\Users\\hang\\Documents\\WeChat Files\\wxid_r2imdzb7j3q922\\FileStorage\\File\\2024-05\\1.2.840.113619.2.80.169103990.5390.1271401378.4.dcm"; + //DicomFile dicomFile = DicomFile.Open(filePath); + + //// 获取DICOM文件的特定元素,通常用于指示使用的字符集 + //var charset = dicomFile.Dataset.GetSingleValueOrDefault(DicomTag.SpecificCharacterSet, string.Empty); + + //var dicomEncoding = DicomEncoding.GetEncoding(charset); + + //var value = dicomFile.Dataset.GetSingleValueOrDefault(DicomTag.PatientName, string.Empty); + + //var dicomStringElement = dicomFile.Dataset.GetDicomItem(DicomTag.PatientName); + + //var bytes = dicomStringElement.Buffer.Data; + + //var aa= dicomEncoding.GetString(bytes); + + + } + } +} diff --git a/IRC.Core.Dicom/Service/Interface/IDicomArchiveService.cs b/IRC.Core.Dicom/Service/Interface/IDicomArchiveService.cs new file mode 100644 index 000000000..99312ef4f --- /dev/null +++ b/IRC.Core.Dicom/Service/Interface/IDicomArchiveService.cs @@ -0,0 +1,11 @@ +using FellowOakDicom; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion.Internal; + +namespace IRaCIS.Core.SCP.Service +{ + public interface IDicomArchiveService + { + Task ArchiveDicomFileAsync(DicomDataset dicomDataset,Guid trialId,Guid trialSiteId, string fileRelativePath,string callingAE,string calledAE,long fileSize); + + } +} diff --git a/IRC.Core.Dicom/Service/OSSService.cs b/IRC.Core.Dicom/Service/OSSService.cs new file mode 100644 index 000000000..1ac5e80ee --- /dev/null +++ b/IRC.Core.Dicom/Service/OSSService.cs @@ -0,0 +1,770 @@ +using AlibabaCloud.SDK.Sts20150401; +using Aliyun.OSS; +using Amazon; +using Amazon.Runtime; +using Amazon.S3; +using Amazon.S3.Model; +using Amazon.SecurityToken; +using Amazon.SecurityToken.Model; +using IRaCIS.Core.Infrastructure; +using IRaCIS.Core.Infrastructure.NewtonsoftJson; +using MassTransit; +using Microsoft.Extensions.Options; +using Minio; +using Minio.DataModel.Args; +using System.Reactive.Linq; +using System.Runtime.InteropServices; + +namespace IRaCIS.Core.SCP; + +#region 绑定和返回模型 + +[LowerCamelCaseJson] +public class MinIOOptions : AWSOptions +{ + public int Port { get; set; } + +} + + +public class AWSOptions +{ + public string EndPoint { get; set; } + public bool UseSSL { get; set; } + public string AccessKeyId { get; set; } + public string RoleArn { get; set; } + public string SecretAccessKey { get; set; } + public string BucketName { get; set; } + public string ViewEndpoint { get; set; } + public int DurationSeconds { get; set; } + public string Region { get; set; } +} + +public class AliyunOSSOptions +{ + public string RegionId { get; set; } + public string AccessKeyId { get; set; } + public string AccessKeySecret { get; set; } + + public string InternalEndpoint { get; set; } + + public string EndPoint { get; set; } + public string BucketName { get; set; } + + public string RoleArn { get; set; } + + public string Region { get; set; } + + public string ViewEndpoint { get; set; } + + public int DurationSeconds { get; set; } + + + +} + +public class ObjectStoreServiceOptions +{ + public string ObjectStoreUse { get; set; } + + public AliyunOSSOptions AliyunOSS { get; set; } + + + public MinIOOptions MinIO { get; set; } + + public AWSOptions AWS { get; set; } + +} + +public class ObjectStoreDTO +{ + public string ObjectStoreUse { get; set; } + + + public AliyunOSSTempToken AliyunOSS { get; set; } + + public MinIOOptions MinIO { get; set; } + + public AWSTempToken AWS { get; set; } + +} + +[LowerCamelCaseJson] +public class AliyunOSSTempToken +{ + public string AccessKeyId { get; set; } + public string AccessKeySecret { get; set; } + + public string EndPoint { get; set; } + public string BucketName { get; set; } + + public string Region { get; set; } + + public string ViewEndpoint { get; set; } + + public string SecurityToken { get; set; } + public DateTime Expiration { get; set; } + + +} + +[LowerCamelCaseJson] +public class AWSTempToken +{ + public string Region { get; set; } + public string SessionToken { get; set; } + public string EndPoint { get; set; } + public string AccessKeyId { get; set; } + public string SecretAccessKey { get; set; } + public string BucketName { get; set; } + public string ViewEndpoint { get; set; } + public DateTime Expiration { get; set; } +} + +public enum ObjectStoreUse +{ + AliyunOSS = 0, + MinIO = 1, + AWS = 2, +} + +#endregion + +// aws 参考链接 https://github.com/awsdocs/aws-doc-sdk-examples/tree/main/dotnetv3/S3/S3_Basics + +public interface IOSSService +{ + public Task UploadToOSSAsync(Stream fileStream, string oosFolderPath, string fileRealName, bool isFileNameAddGuid = true); + public Task UploadToOSSAsync(string localFilePath, string oosFolderPath, bool isFileNameAddGuid = true); + + public Task DownLoadFromOSSAsync(string ossRelativePath, string localFilePath); + + public ObjectStoreServiceOptions ObjectStoreServiceOptions { get; set; } + + public Task GetSignedUrl(string ossRelativePath); + + public Task DeleteFromPrefix(string prefix); + + public ObjectStoreDTO GetObjectStoreTempToken(); +} + + +public class OSSService : IOSSService +{ + public ObjectStoreServiceOptions ObjectStoreServiceOptions { get; set; } + + private AliyunOSSTempToken AliyunOSSTempToken { get; set; } + + private AWSTempToken AWSTempToken { get; set; } + + + public OSSService(IOptionsMonitor options) + { + ObjectStoreServiceOptions = options.CurrentValue; + + } + + + + /// + /// oosFolderPath 不要 "/ "开头 应该: TempFolder/ChildFolder + /// + /// + /// + /// + /// + /// + public async Task UploadToOSSAsync(Stream fileStream, string oosFolderPath, string fileRealName, bool isFileNameAddGuid = true) + { + GetObjectStoreTempToken(); + + var ossRelativePath = isFileNameAddGuid ? $"{oosFolderPath}/{Guid.NewGuid()}_{fileRealName}" : $"{oosFolderPath}/{fileRealName}"; + + try + { + using (var memoryStream = new MemoryStream()) + { + fileStream.Seek(0, SeekOrigin.Begin); + + fileStream.CopyTo(memoryStream); + + memoryStream.Seek(0, SeekOrigin.Begin); + + + if (ObjectStoreServiceOptions.ObjectStoreUse == "AliyunOSS") + { + var aliConfig = ObjectStoreServiceOptions.AliyunOSS; + + var _ossClient = new OssClient(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? aliConfig.EndPoint : aliConfig.InternalEndpoint, AliyunOSSTempToken.AccessKeyId, AliyunOSSTempToken.AccessKeySecret, AliyunOSSTempToken.SecurityToken); + + + + // 上传文件 + var result = _ossClient.PutObject(aliConfig.BucketName, ossRelativePath, memoryStream); + + } + else if (ObjectStoreServiceOptions.ObjectStoreUse == "MinIO") + { + var minIOConfig = ObjectStoreServiceOptions.MinIO; + + + var minioClient = new MinioClient().WithEndpoint($"{minIOConfig.EndPoint}:{minIOConfig.Port}") + .WithCredentials(minIOConfig.AccessKeyId, minIOConfig.SecretAccessKey).WithSSL(minIOConfig.UseSSL) + .Build(); + + var putObjectArgs = new PutObjectArgs() + .WithBucket(minIOConfig.BucketName) + .WithObject(ossRelativePath) + .WithStreamData(memoryStream) + .WithObjectSize(memoryStream.Length); + + await minioClient.PutObjectAsync(putObjectArgs); + } + else if (ObjectStoreServiceOptions.ObjectStoreUse == "AWS") + { + var awsConfig = ObjectStoreServiceOptions.AWS; + + var credentials = new SessionAWSCredentials(AWSTempToken.AccessKeyId, AWSTempToken.SecretAccessKey, AWSTempToken.SessionToken); + + + + //提供awsEndPoint(域名)进行访问配置 + var clientConfig = new AmazonS3Config + { + RegionEndpoint = RegionEndpoint.USEast1, + UseHttp = true, + }; + + var amazonS3Client = new AmazonS3Client(credentials, clientConfig); + + var putObjectRequest = new Amazon.S3.Model.PutObjectRequest() + { + BucketName = awsConfig.BucketName, + InputStream = memoryStream, + Key = ossRelativePath, + }; + + await amazonS3Client.PutObjectAsync(putObjectRequest); + } + else + { + throw new BusinessValidationFailedException("未定义的存储介质类型"); + } + } + } + catch (Exception ex) + { + + throw new BusinessValidationFailedException($"上传发生异常:{ex.Message}"); + } + + + + + return "/" + ossRelativePath; + + } + + + + /// + /// oosFolderPath 不要 "/ "开头 应该: TempFolder/ChildFolder + /// + /// + /// + /// + /// + /// + public async Task UploadToOSSAsync(string localFilePath, string oosFolderPath, bool isFileNameAddGuid = true) + { + GetObjectStoreTempToken(); + + var localFileName = Path.GetFileName(localFilePath); + + var ossRelativePath = isFileNameAddGuid ? $"{oosFolderPath}/{Guid.NewGuid()}_{localFileName}" : $"{oosFolderPath}/{localFileName}"; + + + if (ObjectStoreServiceOptions.ObjectStoreUse == "AliyunOSS") + { + var aliConfig = ObjectStoreServiceOptions.AliyunOSS; + + var _ossClient = new OssClient(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? aliConfig.EndPoint : aliConfig.InternalEndpoint, AliyunOSSTempToken.AccessKeyId, AliyunOSSTempToken.AccessKeySecret, AliyunOSSTempToken.SecurityToken); + + // 上传文件 + var result = _ossClient.PutObject(aliConfig.BucketName, ossRelativePath, localFilePath); + + } + else if (ObjectStoreServiceOptions.ObjectStoreUse == "MinIO") + { + var minIOConfig = ObjectStoreServiceOptions.MinIO; + + + var minioClient = new MinioClient().WithEndpoint($"{minIOConfig.EndPoint}:{minIOConfig.Port}") + .WithCredentials(minIOConfig.AccessKeyId, minIOConfig.SecretAccessKey).WithSSL(minIOConfig.UseSSL) + .Build(); + + var putObjectArgs = new PutObjectArgs() + .WithBucket(minIOConfig.BucketName) + .WithObject(ossRelativePath) + .WithFileName(localFilePath); + + await minioClient.PutObjectAsync(putObjectArgs); + } + else if (ObjectStoreServiceOptions.ObjectStoreUse == "AWS") + { + var awsConfig = ObjectStoreServiceOptions.AWS; + + // 提供awsAccessKeyId和awsSecretAccessKey构造凭证 + var credentials = new SessionAWSCredentials(AWSTempToken.AccessKeyId, AWSTempToken.SecretAccessKey, AWSTempToken.SessionToken); + + //提供awsEndPoint(域名)进行访问配置 + var clientConfig = new AmazonS3Config + { + RegionEndpoint = RegionEndpoint.USEast1, + UseHttp = true, + }; + + var amazonS3Client = new AmazonS3Client(credentials, clientConfig); + + var putObjectRequest = new Amazon.S3.Model.PutObjectRequest() + { + BucketName = awsConfig.BucketName, + FilePath = localFilePath, + Key = ossRelativePath, + }; + + await amazonS3Client.PutObjectAsync(putObjectRequest); + + } + else + { + throw new BusinessValidationFailedException("未定义的存储介质类型"); + } + return "/" + ossRelativePath; + + } + + + public async Task DownLoadFromOSSAsync(string ossRelativePath, string localFilePath) + { + GetObjectStoreTempToken(); + + ossRelativePath = ossRelativePath.TrimStart('/'); + try + { + + + if (ObjectStoreServiceOptions.ObjectStoreUse == "AliyunOSS") + { + var aliConfig = ObjectStoreServiceOptions.AliyunOSS; + + var _ossClient = new OssClient(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? aliConfig.EndPoint : aliConfig.InternalEndpoint, AliyunOSSTempToken.AccessKeyId, AliyunOSSTempToken.AccessKeySecret, AliyunOSSTempToken.SecurityToken); + + // 上传文件 + var result = _ossClient.GetObject(aliConfig.BucketName, ossRelativePath); + + // 将下载的文件流保存到本地文件 + using (var fs = File.OpenWrite(localFilePath)) + { + result.Content.CopyTo(fs); + fs.Close(); + } + + } + else if (ObjectStoreServiceOptions.ObjectStoreUse == "MinIO") + { + var minIOConfig = ObjectStoreServiceOptions.MinIO; + + var minioClient = new MinioClient().WithEndpoint($"{minIOConfig.EndPoint}:{minIOConfig.Port}") + .WithCredentials(minIOConfig.AccessKeyId, minIOConfig.SecretAccessKey).WithSSL(minIOConfig.UseSSL) + .Build(); + + var getObjectArgs = new GetObjectArgs() + .WithBucket(minIOConfig.BucketName) + .WithObject(ossRelativePath) + .WithFile(localFilePath); + + await minioClient.GetObjectAsync(getObjectArgs); + + } + else if (ObjectStoreServiceOptions.ObjectStoreUse == "AWS") + { + var awsConfig = ObjectStoreServiceOptions.AWS; + + // 提供awsAccessKeyId和awsSecretAccessKey构造凭证 + var credentials = new SessionAWSCredentials(AWSTempToken.AccessKeyId, AWSTempToken.SecretAccessKey, AWSTempToken.SessionToken); + + //提供awsEndPoint(域名)进行访问配置 + var clientConfig = new AmazonS3Config + { + RegionEndpoint = RegionEndpoint.USEast1, + UseHttp = true, + }; + + var amazonS3Client = new AmazonS3Client(credentials, clientConfig); + + var getObjectArgs = new Amazon.S3.Model.GetObjectRequest() + { + BucketName = awsConfig.BucketName, + Key = ossRelativePath, + }; + + + await (await amazonS3Client.GetObjectAsync(getObjectArgs)).WriteResponseStreamToFileAsync(localFilePath, true, CancellationToken.None); + + + } + else + { + throw new BusinessValidationFailedException("未定义的存储介质类型"); + } + } + catch (Exception ex) + { + + throw new BusinessValidationFailedException("oss下载失败!" + ex.Message); + } + + + + + + } + + public async Task GetSignedUrl(string ossRelativePath) + { + GetObjectStoreTempToken(); + + ossRelativePath = ossRelativePath.TrimStart('/'); + try + { + + + if (ObjectStoreServiceOptions.ObjectStoreUse == "AliyunOSS") + { + var aliConfig = ObjectStoreServiceOptions.AliyunOSS; + + var _ossClient = new OssClient(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? aliConfig.EndPoint : aliConfig.InternalEndpoint, AliyunOSSTempToken.AccessKeyId, AliyunOSSTempToken.AccessKeySecret, AliyunOSSTempToken.SecurityToken); + + // 生成签名URL。 + var req = new GeneratePresignedUriRequest(aliConfig.BucketName, ossRelativePath, SignHttpMethod.Get) + { + // 设置签名URL过期时间,默认值为3600秒。 + Expiration = DateTime.Now.AddHours(1), + }; + var uri = _ossClient.GeneratePresignedUri(req); + + return uri.PathAndQuery; + + } + else if (ObjectStoreServiceOptions.ObjectStoreUse == "MinIO") + { + var minIOConfig = ObjectStoreServiceOptions.MinIO; + + var minioClient = new MinioClient().WithEndpoint($"{minIOConfig.EndPoint}:{minIOConfig.Port}") + .WithCredentials(minIOConfig.AccessKeyId, minIOConfig.SecretAccessKey).WithSSL(minIOConfig.UseSSL) + .Build(); + + + var args = new PresignedGetObjectArgs() + .WithBucket(minIOConfig.BucketName) + .WithObject(ossRelativePath) + .WithExpiry(3600) + /*.WithHeaders(reqParams)*/; + + var presignedUrl = await minioClient.PresignedGetObjectAsync(args); + + Uri uri = new Uri(presignedUrl); + + string relativePath = uri.PathAndQuery; + + + return relativePath; + + } + else if (ObjectStoreServiceOptions.ObjectStoreUse == "AWS") + { + var awsConfig = ObjectStoreServiceOptions.AWS; + + + // 提供awsAccessKeyId和awsSecretAccessKey构造凭证 + var credentials = new SessionAWSCredentials(AWSTempToken.AccessKeyId, AWSTempToken.SecretAccessKey, AWSTempToken.SessionToken); + + //提供awsEndPoint(域名)进行访问配置 + var clientConfig = new AmazonS3Config + { + RegionEndpoint = RegionEndpoint.USEast1, + UseHttp = true, + }; + + var amazonS3Client = new AmazonS3Client(credentials, clientConfig); + + var presignedUrl = await amazonS3Client.GetPreSignedURLAsync(new GetPreSignedUrlRequest() + { + BucketName = awsConfig.BucketName, + Key = ossRelativePath, + Expires = DateTime.UtcNow.AddMinutes(120) + }); + + Uri uri = new Uri(presignedUrl); + + string relativePath = uri.PathAndQuery; + + + return relativePath; + } + else + { + throw new BusinessValidationFailedException("未定义的存储介质类型"); + } + } + catch (Exception ex) + { + + throw new BusinessValidationFailedException("oss授权url失败!" + ex.Message); + } + + } + + /// + /// 删除某个目录的文件 + /// + /// + /// + public async Task DeleteFromPrefix(string prefix) + { + GetObjectStoreTempToken(); + + if (ObjectStoreServiceOptions.ObjectStoreUse == "AliyunOSS") + { + var aliConfig = ObjectStoreServiceOptions.AliyunOSS; + + var _ossClient = new OssClient(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? aliConfig.EndPoint : aliConfig.InternalEndpoint, AliyunOSSTempToken.AccessKeyId, AliyunOSSTempToken.AccessKeySecret, AliyunOSSTempToken.SecurityToken); + + + try + { + ObjectListing objectListing = null; + string nextMarker = null; + do + { + // 使用 prefix 模拟目录结构,设置 MaxKeys 和 NextMarker + objectListing = _ossClient.ListObjects(new Aliyun.OSS.ListObjectsRequest(aliConfig.BucketName) + { + Prefix = prefix, + MaxKeys = 1000, + Marker = nextMarker + }); + + List keys = objectListing.ObjectSummaries.Select(t => t.Key).ToList(); + + // 删除获取到的文件 + if (keys.Count > 0) + { + _ossClient.DeleteObjects(new Aliyun.OSS.DeleteObjectsRequest(aliConfig.BucketName, keys, false)); + } + + // 设置 NextMarker 以获取下一页的数据 + nextMarker = objectListing.NextMarker; + + } while (objectListing.IsTruncated); + } + catch (Exception ex) + { + Console.WriteLine($"Error: {ex.Message}"); + } + + + } + else if (ObjectStoreServiceOptions.ObjectStoreUse == "MinIO") + { + var minIOConfig = ObjectStoreServiceOptions.MinIO; + + + var minioClient = new MinioClient().WithEndpoint($"{minIOConfig.EndPoint}:{minIOConfig.Port}") + .WithCredentials(minIOConfig.AccessKeyId, minIOConfig.SecretAccessKey).WithSSL(minIOConfig.UseSSL) + .Build(); + + + var listArgs = new ListObjectsArgs().WithBucket(minIOConfig.BucketName).WithPrefix(prefix).WithRecursive(true); + + + + // 创建一个空列表用于存储对象键 + var objects = new List(); + + // 使用 await foreach 来异步迭代对象列表 + await foreach (var item in minioClient.ListObjectsEnumAsync(listArgs)) + { + objects.Add(item.Key); + } + + + if (objects.Count > 0) + { + var objArgs = new RemoveObjectsArgs() + .WithBucket(minIOConfig.BucketName) + .WithObjects(objects); + + // 删除对象 + await minioClient.RemoveObjectsAsync(objArgs); + } + + + + } + else if (ObjectStoreServiceOptions.ObjectStoreUse == "AWS") + { + + var awsConfig = ObjectStoreServiceOptions.AWS; + + + // 提供awsAccessKeyId和awsSecretAccessKey构造凭证 + var credentials = new SessionAWSCredentials(AWSTempToken.AccessKeyId, AWSTempToken.SecretAccessKey, AWSTempToken.SessionToken); + + //提供awsEndPoint(域名)进行访问配置 + var clientConfig = new AmazonS3Config + { + RegionEndpoint = RegionEndpoint.USEast1, + UseHttp = true, + }; + + var amazonS3Client = new AmazonS3Client(credentials, clientConfig); + + // 列出指定前缀下的所有对象 + var listObjectsRequest = new ListObjectsV2Request + { + BucketName = awsConfig.BucketName, + Prefix = prefix + }; + + var listObjectsResponse = await amazonS3Client.ListObjectsV2Async(listObjectsRequest); + + if (listObjectsResponse.S3Objects.Count > 0) + { + // 准备删除请求 + var deleteObjectsRequest = new Amazon.S3.Model.DeleteObjectsRequest + { + BucketName = awsConfig.BucketName, + Objects = new List() + }; + + foreach (var s3Object in listObjectsResponse.S3Objects) + { + deleteObjectsRequest.Objects.Add(new KeyVersion + { + Key = s3Object.Key + }); + } + + // 批量删除对象 + var deleteObjectsResponse = await amazonS3Client.DeleteObjectsAsync(deleteObjectsRequest); + } + + + + } + else + { + throw new BusinessValidationFailedException("未定义的存储介质类型"); + } + } + + + public ObjectStoreDTO GetObjectStoreTempToken() + { + + var ossOptions = ObjectStoreServiceOptions.AliyunOSS; + + if (ObjectStoreServiceOptions.ObjectStoreUse == "AliyunOSS") + { + var client = new Client(new AlibabaCloud.OpenApiClient.Models.Config() + { + AccessKeyId = ossOptions.AccessKeyId, + AccessKeySecret = ossOptions.AccessKeySecret, + //AccessKeyId = "LTAI5tJV76pYX5yPg1N9QVE8", + //AccessKeySecret = "roRNLa9YG1of4pYruJGCNKBXEWTAWa", + + Endpoint = "sts.cn-hangzhou.aliyuncs.com" + }); + + var assumeRoleRequest = new AlibabaCloud.SDK.Sts20150401.Models.AssumeRoleRequest(); + // 将设置为自定义的会话名称,例如oss-role-session。 + assumeRoleRequest.RoleSessionName = $"session-name-{NewId.NextGuid()}"; + // 将替换为拥有上传文件到指定OSS Bucket权限的RAM角色的ARN。 + assumeRoleRequest.RoleArn = ossOptions.RoleArn; + //assumeRoleRequest.RoleArn = "acs:ram::1899121822495495:role/webdirect"; + assumeRoleRequest.DurationSeconds = ossOptions.DurationSeconds; + var runtime = new AlibabaCloud.TeaUtil.Models.RuntimeOptions(); + var response = client.AssumeRoleWithOptions(assumeRoleRequest, runtime); + var credentials = response.Body.Credentials; + + var tempToken = new AliyunOSSTempToken() + { + AccessKeyId = credentials.AccessKeyId, + AccessKeySecret = credentials.AccessKeySecret, + + //转为服务器时区,最后统一转为客户端时区 + Expiration = TimeZoneInfo.ConvertTimeFromUtc(DateTime.Parse(credentials.Expiration), TimeZoneInfo.Local), + SecurityToken = credentials.SecurityToken, + + + Region = ossOptions.Region, + BucketName = ossOptions.BucketName, + EndPoint = ossOptions.EndPoint, + ViewEndpoint = ossOptions.ViewEndpoint, + + }; + + AliyunOSSTempToken = tempToken; + + return new ObjectStoreDTO() { ObjectStoreUse = ObjectStoreServiceOptions.ObjectStoreUse, AliyunOSS = tempToken }; + } + else if (ObjectStoreServiceOptions.ObjectStoreUse == "MinIO") + { + return new ObjectStoreDTO() { ObjectStoreUse = ObjectStoreServiceOptions.ObjectStoreUse, MinIO = ObjectStoreServiceOptions.MinIO }; + } + else if (ObjectStoreServiceOptions.ObjectStoreUse == "AWS") + { + var awsOptions = ObjectStoreServiceOptions.AWS; + + //aws 临时凭证 + // 创建 STS 客户端 + var stsClient = new AmazonSecurityTokenServiceClient(awsOptions.AccessKeyId, awsOptions.SecretAccessKey); + + // 使用 AssumeRole 请求临时凭证 + var assumeRoleRequest = new AssumeRoleRequest + { + + RoleArn = awsOptions.RoleArn, // 角色 ARN + RoleSessionName = $"session-name-{NewId.NextGuid()}", + DurationSeconds = awsOptions.DurationSeconds // 临时凭证有效期 + }; + + var assumeRoleResponse = stsClient.AssumeRoleAsync(assumeRoleRequest).Result; + + var credentials = assumeRoleResponse.Credentials; + + var tempToken = new AWSTempToken() + { + AccessKeyId = credentials.AccessKeyId, + SecretAccessKey = credentials.SecretAccessKey, + SessionToken = credentials.SessionToken, + Expiration = credentials.Expiration, + Region = awsOptions.Region, + BucketName = awsOptions.BucketName, + EndPoint = awsOptions.EndPoint, + ViewEndpoint = awsOptions.ViewEndpoint, + + }; + + AWSTempToken = tempToken; + return new ObjectStoreDTO() { ObjectStoreUse = ObjectStoreServiceOptions.ObjectStoreUse, AWS = tempToken }; + } + else + { + throw new BusinessValidationFailedException("未定义的存储介质类型"); + } + } + +} diff --git a/IRC.Core.Dicom/appsettings.Development.json b/IRC.Core.Dicom/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/IRC.Core.Dicom/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/IRC.Core.Dicom/appsettings.json b/IRC.Core.Dicom/appsettings.json new file mode 100644 index 000000000..10f68b8c8 --- /dev/null +++ b/IRC.Core.Dicom/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/IRaCIS.Core.API.sln b/IRaCIS.Core.API.sln index 6fabe24b3..a104bd025 100644 --- a/IRaCIS.Core.API.sln +++ b/IRaCIS.Core.API.sln @@ -19,6 +19,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IRaCIS.Core.Infrastructure" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IRC.Core.SCP", "IRC.Core.SCP\IRC.Core.SCP.csproj", "{ECD08F47-DC1A-484E-BB91-6CDDC8823CC5}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IRC.Core.Dicom", "IRC.Core.Dicom\IRC.Core.Dicom.csproj", "{0545F0A5-D97B-4A47-92A6-A8A02A181322}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -57,6 +59,10 @@ Global {ECD08F47-DC1A-484E-BB91-6CDDC8823CC5}.Debug|Any CPU.Build.0 = Debug|Any CPU {ECD08F47-DC1A-484E-BB91-6CDDC8823CC5}.Release|Any CPU.ActiveCfg = Release|Any CPU {ECD08F47-DC1A-484E-BB91-6CDDC8823CC5}.Release|Any CPU.Build.0 = Release|Any CPU + {0545F0A5-D97B-4A47-92A6-A8A02A181322}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0545F0A5-D97B-4A47-92A6-A8A02A181322}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0545F0A5-D97B-4A47-92A6-A8A02A181322}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0545F0A5-D97B-4A47-92A6-A8A02A181322}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE