diff --git a/IRC.Core.SCP/BusinessFilter/ModelActionFilter .cs b/IRC.Core.SCP/BusinessFilter/ModelActionFilter .cs new file mode 100644 index 000000000..2f0ec4873 --- /dev/null +++ b/IRC.Core.SCP/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.SCP/BusinessFilter/ModelBinding.cs b/IRC.Core.SCP/BusinessFilter/ModelBinding.cs new file mode 100644 index 000000000..56e18b418 --- /dev/null +++ b/IRC.Core.SCP/BusinessFilter/ModelBinding.cs @@ -0,0 +1,28 @@ +using System; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace IRaCIS.Core.SCP.Filter +{ + #region snippet_DisableFormValueModelBindingAttribute + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] + public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter + { + public void OnResourceExecuting(ResourceExecutingContext context) + { + + + var factories = context.ValueProviderFactories; + //factories.RemoveType(); + factories.RemoveType(); + factories.RemoveType(); + context.HttpContext.Request.EnableBuffering(); + } + + public void OnResourceExecuted(ResourceExecutedContext context) + { + } + } + #endregion +} diff --git a/IRC.Core.SCP/BusinessFilter/ProjectExceptionFilter.cs b/IRC.Core.SCP/BusinessFilter/ProjectExceptionFilter.cs new file mode 100644 index 000000000..0952bc457 --- /dev/null +++ b/IRC.Core.SCP/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.SCP/BusinessFilter/UnifiedApiResultFilter.cs b/IRC.Core.SCP/BusinessFilter/UnifiedApiResultFilter.cs new file mode 100644 index 000000000..7609ae4bc --- /dev/null +++ b/IRC.Core.SCP/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.Application.Services.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(StaticData.International("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.SCP/Helper/ImageHelper.cs b/IRC.Core.SCP/Helper/ImageHelper.cs new file mode 100644 index 000000000..50f0ab4f6 --- /dev/null +++ b/IRC.Core.SCP/Helper/ImageHelper.cs @@ -0,0 +1,62 @@ + +using FellowOakDicom.Imaging; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; +using System.Security.Cryptography; +using System.Text; + +namespace IRaCIS.Core.Application.Helper; + +public static class ImageHelper +{ + + // 采用ImageSharp组件 跨平台,不依赖windows 平台图像处理组件,这里大坑(net 6 下,之前由于Dicom处理最新组件 依赖了这个库的一个旧版本,导致缩略图bug,单独升级依赖组件才解决) + public static void ResizeSave(string filePath, string? fileStorePath) + { + + fileStorePath = fileStorePath ?? filePath + ".preview.jpeg"; + + using (var image = SixLabors.ImageSharp.Image.Load(filePath)) + { + + image.Mutate(x => x.Resize(500, 500)); + + image.Save(fileStorePath); + + } + } + + + + public static Stream RenderPreviewJpeg(string filePath) + { + string jpegPath = filePath + ".preview.jpg"; + + if (!File.Exists(jpegPath)) + { + using (Stream stream = new FileStream(jpegPath, FileMode.Create)) + { + DicomImage image = new DicomImage(filePath); + + var sharpimage = image.RenderImage().AsSharpImage(); + + sharpimage.Save(stream, new JpegEncoder()); + + } + } + + return new FileStream(jpegPath, FileMode.Open); + } + + public static void RemovePreviewJpeg(string filePath) + { + string jpegPath = filePath + ".preview.jpg"; + if (File.Exists(jpegPath)) File.Delete(jpegPath); + } + + +} + + + diff --git a/IRC.Core.SCP/HostConfig/AutofacModuleSetup.cs b/IRC.Core.SCP/HostConfig/AutofacModuleSetup.cs new file mode 100644 index 000000000..183eb235b --- /dev/null +++ b/IRC.Core.SCP/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.Application.Services; +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 + "IRaCIS.Core.SCP.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.SCP/HostConfig/EFSetup.cs b/IRC.Core.SCP/HostConfig/EFSetup.cs new file mode 100644 index 000000000..b53095cbd --- /dev/null +++ b/IRC.Core.SCP/HostConfig/EFSetup.cs @@ -0,0 +1,44 @@ +using IRaCIS.Core.Infra.EFCore; +using Medallion.Threading; +using Medallion.Threading.SqlServer; +using Microsoft.EntityFrameworkCore; +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.AddScoped(); + + //这个注入没有成功--注入是没问题的,构造函数也只是支持参数就好,错在注入的地方不能写DbContext + //Web程序中通过重用池中DbContext实例可提高高并发场景下的吞吐量, 这在概念上类似于ADO.NET Provider原生的连接池操作方式,具有节省DbContext实例化成本的优点 + services.AddDbContext(options => + { + options.UseSqlServer(configuration.GetSection("ConnectionStrings:RemoteNew").Value, + contextOptionsBuilder => contextOptionsBuilder.EnableRetryOnFailure()); + + options.EnableSensitiveDataLogging(); + + options.AddInterceptors(new QueryWithNoLockDbCommandInterceptor()); + + options.UseProjectables(); + + + + }); + + //注意区分 easy caching 也有 IDistributedLockProvider + services.AddSingleton(sp => + { + //var connection = ConnectionMultiplexer.Connect(configuration["Redis:Configuration"]!); + + return new SqlDistributedSynchronizationProvider(configuration.GetSection("ConnectionStrings:RemoteNew").Value); + }); + + //services.AddAssemblyTriggers(typeof(SubjectVisitImageDateTrigger).Assembly); + } + } +} diff --git a/IRC.Core.SCP/HostConfig/NewtonsoftJsonSetup.cs b/IRC.Core.SCP/HostConfig/NewtonsoftJsonSetup.cs new file mode 100644 index 000000000..00d5ae329 --- /dev/null +++ b/IRC.Core.SCP/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.SCP/HostConfig/NullToEmptyStringResolver.cs b/IRC.Core.SCP/HostConfig/NullToEmptyStringResolver.cs new file mode 100644 index 000000000..f4bd469a6 --- /dev/null +++ b/IRC.Core.SCP/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.SCP/HostConfig/NullToEmptyStringValueProvider.cs b/IRC.Core.SCP/HostConfig/NullToEmptyStringValueProvider.cs new file mode 100644 index 000000000..10e7e613d --- /dev/null +++ b/IRC.Core.SCP/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.SCP/IRC.Core.SCP.csproj b/IRC.Core.SCP/IRC.Core.SCP.csproj new file mode 100644 index 000000000..9407aa2ba --- /dev/null +++ b/IRC.Core.SCP/IRC.Core.SCP.csproj @@ -0,0 +1,38 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + + true + + + + + + + + + + + + + + + + diff --git a/IRC.Core.SCP/Program.cs b/IRC.Core.SCP/Program.cs new file mode 100644 index 000000000..5ad53894d --- /dev/null +++ b/IRC.Core.SCP/Program.cs @@ -0,0 +1,208 @@ + +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 兼容windows 服务命令行的方式 + +int urlsIndex = Array.FindIndex(args, arg => arg != null && arg.StartsWith("--port")); + +if (urlsIndex > -1) +{ + var port = args[urlsIndex].Substring("--port=".Length); + Console.WriteLine(port); + builder.WebHost.UseUrls($"http://0.0.0.0:{port}"); +} + +#endregion + +#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(); + 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); + + +//转发头设置 获取真实IP +builder.Services.Configure(options => +{ + options.ForwardedHeaders = + ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; +}); + +//Dicom影像渲染图片 跨平台 +//builder.Services.AddDicomSetup(); +new DicomSetupBuilder() + .RegisterServices(s => + s.AddFellowOakDicom() + .AddTranscoderManager() + //.AddTranscoderManager() + .AddImageManager()) + .SkipValidation() + .Build(); + + + +#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.Information) + // Filter out ASP.NET Core infrastructre logs that are Information and below 日志太多了 一个请求 记录好几条 + .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning) + .MinimumLevel.Override("Hangfire", LogEventLevel.Warning) + .MinimumLevel.Override("System.Net.Http.HttpClient.HttpReports", LogEventLevel.Warning) + .Enrich.WithClientIp() + + .Enrich.FromLogContext() + + //控制台 方便调试 问题 我们显示记录日志 时 获取上下文的ip 和用户名 用户类型 + .WriteTo.Console(restrictedToMinimumLevel: LogEventLevel.Warning, + outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {SourceContext:l} || {Message} || {Exception} ||end {NewLine}") + + .WriteTo.File($"{AppContext.BaseDirectory}Serilogs/.log", rollingInterval: RollingInterval.Day, + outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {SourceContext:l} || {Message} || {Exception} ||end {NewLine}") + .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 + + +var server = DicomServerFactory.Create(_configuration.GetSection("DicomSCPServiceConfig").GetValue("ServerPort"), userState: app.Services); + + +app.Run(); diff --git a/IRC.Core.SCP/Properties/launchSettings.json b/IRC.Core.SCP/Properties/launchSettings.json new file mode 100644 index 000000000..ec782f999 --- /dev/null +++ b/IRC.Core.SCP/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:36358", + "sslPort": 0 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5243", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/IRC.Core.SCP/Service/BaseService.cs b/IRC.Core.SCP/Service/BaseService.cs new file mode 100644 index 000000000..a10e9bf9c --- /dev/null +++ b/IRC.Core.SCP/Service/BaseService.cs @@ -0,0 +1,118 @@ +using AutoMapper; +using IRaCIS.Application.Services.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 IRepository _repository { 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(_repository))] + public IRepository _repository { 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(_repository))] + public IRepository _repository { 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 IRepository _repository { 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.SCP/Service/CStoreSCPService.cs b/IRC.Core.SCP/Service/CStoreSCPService.cs new file mode 100644 index 000000000..261ca563b --- /dev/null +++ b/IRC.Core.SCP/Service/CStoreSCPService.cs @@ -0,0 +1,358 @@ +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, + 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 + }; + + + public CStoreSCPService(INetworkStream stream, Encoding fallbackEncoding, Microsoft.Extensions.Logging.ILogger log, DicomServiceDependencies dependencies) + : base(stream, fallbackEncoding, log, dependencies) + { + } + + + + + 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(); + + 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 ).FirstOrDefault(); + + if (findTrialSiteAE!=null) + { + _trialSiteId= findTrialSiteAE.TrialSiteId; + + isCanReceiveIamge = true; + } + } + + + if (!trialCalledAEList.Contains(association.CalledAE) || isCanReceiveIamge==false) + { + + Log.Logger.Warning($"拒绝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; + + await _SCPImageUploadRepository.AddAsync(_upload, true); + + 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 seriesId = IdentifierHelper.CreateGuid(studyInstanceUid, seriesInstanceUid); + Guid instanceId = IdentifierHelper.CreateGuid(studyInstanceUid, seriesInstanceUid, sopInstanceUid); + + + 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); + + if (!_SCPStudyIdList.Contains(scpStudyId)) + { + _SCPStudyIdList.Add(scpStudyId); + } + + var series = await _seriesRepository.FindAsync(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.SCP/Service/DicomArchiveService.cs b/IRC.Core.SCP/Service/DicomArchiveService.cs new file mode 100644 index 000000000..6eb9a092a --- /dev/null +++ b/IRC.Core.SCP/Service/DicomArchiveService.cs @@ -0,0 +1,337 @@ +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; + +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) + { + string studyInstanceUid = dataset.GetString(DicomTag.StudyInstanceUID); + string seriesInstanceUid = dataset.GetString(DicomTag.SeriesInstanceUID); + string sopInstanceUid = dataset.GetString(DicomTag.SOPInstanceUID); + + string patientIdStr = dataset.GetString(DicomTag.PatientID); + + //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); + var findStudy = await _studyRepository.FindAsync(studyId); + var findSerice = await _seriesRepository.FindAsync(seriesId); + var findInstance = await _instanceRepository.FindAsync(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; + + } + 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 + }; + + ++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 }); + } + + //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.SCP/Service/Interface/IDicomArchiveService.cs b/IRC.Core.SCP/Service/Interface/IDicomArchiveService.cs new file mode 100644 index 000000000..1c107e94b --- /dev/null +++ b/IRC.Core.SCP/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); + + } +} diff --git a/IRC.Core.SCP/Service/OSSService.cs b/IRC.Core.SCP/Service/OSSService.cs new file mode 100644 index 000000000..cfbe4b3f8 --- /dev/null +++ b/IRC.Core.SCP/Service/OSSService.cs @@ -0,0 +1,427 @@ +using Aliyun.OSS; +using IRaCIS.Core.Infrastructure; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Minio.DataModel.Args; +using Minio; +using SharpCompress.Common; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Security.AccessControl; +using System.Text; +using System.Threading.Tasks; + +namespace IRaCIS.Core.SCP +{ + public class MinIOOptions : AWSOptions + { + public int port { get; set; } + + } + + public class AWSOptions + { + public string endPoint { get; set; } + public bool useSSL { get; set; } + public string accessKey { get; set; } + public string secretKey { get; set; } + public string bucketName { get; set; } + public string viewEndpoint { get; set; } + } + + public class AliyunOSSOptions + { + public string regionId { get; set; } + public string accessKeyId { get; set; } + public string accessKeySecret { 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 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 AliyunOSSOptions AliyunOSS { get; set; } + + public MinIOOptions MinIO { get; set; } + + public AWSOptions AWS { get; set; } + + } + + public class AliyunOSSTempToken + { + public string AccessKeyId { get; set; } + public string AccessKeySecret { get; set; } + public string SecurityToken { get; set; } + public string Expiration { get; set; } + + public string Region { get; set; } + public string BucketName { get; set; } + public string ViewEndpoint { get; set; } + } + + + + public enum ObjectStoreUse + { + AliyunOSS = 0, + MinIO = 1, + AWS = 2, + } + + 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 class OSSService : IOSSService + { + public ObjectStoreServiceOptions ObjectStoreServiceOptions { 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) + { + + + var ossRelativePath = isFileNameAddGuid? $"{oosFolderPath}/{Guid.NewGuid()}_{fileRealName}": $"{oosFolderPath}/{fileRealName}"; + //var ossRelativePath = 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(aliConfig.endPoint, aliConfig.accessKeyId, aliConfig.accessKeySecret); + + + + // 上传文件 + 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.accessKey, minIOConfig.secretKey).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 minIOConfig = ObjectStoreServiceOptions.AWS; + + + var minioClient = new MinioClient().WithEndpoint($"{minIOConfig.endPoint}") + .WithCredentials(minIOConfig.accessKey, minIOConfig.secretKey).WithSSL(minIOConfig.useSSL) + .Build(); + + var putObjectArgs = new PutObjectArgs() + .WithBucket(minIOConfig.bucketName) + .WithObject(ossRelativePath) + .WithStreamData(memoryStream) + .WithObjectSize(memoryStream.Length); + + await minioClient.PutObjectAsync(putObjectArgs); + } + 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) + { + var localFileName = Path.GetFileName(localFilePath); + + var ossRelativePath = isFileNameAddGuid? $"{oosFolderPath}/{Guid.NewGuid()}_{localFileName}" : $"{oosFolderPath}/{localFileName}"; + + //var ossRelativePath = oosFolderPath + "/" + localFileName; + + + if (ObjectStoreServiceOptions.ObjectStoreUse == "AliyunOSS") + { + var aliConfig = ObjectStoreServiceOptions.AliyunOSS; + + var _ossClient = new OssClient(aliConfig.endPoint, aliConfig.accessKeyId, aliConfig.accessKeySecret); + + // 上传文件 + 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.accessKey, minIOConfig.secretKey).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 minIOConfig = ObjectStoreServiceOptions.AWS; + + + var minioClient = new MinioClient().WithEndpoint($"{minIOConfig.endPoint}") + .WithCredentials(minIOConfig.accessKey, minIOConfig.secretKey).WithSSL(minIOConfig.useSSL) + .Build(); + + var putObjectArgs = new PutObjectArgs() + .WithBucket(minIOConfig.bucketName) + .WithObject(ossRelativePath) + .WithFileName(localFilePath); + + await minioClient.PutObjectAsync(putObjectArgs); + } + else + { + throw new BusinessValidationFailedException("未定义的存储介质类型"); + } + return "/" + ossRelativePath; + + } + + public async Task DownLoadFromOSSAsync(string ossRelativePath, string localFilePath) + { + + ossRelativePath = ossRelativePath.TrimStart('/'); + try + { + + + if (ObjectStoreServiceOptions.ObjectStoreUse == "AliyunOSS") + { + var aliConfig = ObjectStoreServiceOptions.AliyunOSS; + + var _ossClient = new OssClient(aliConfig.endPoint, aliConfig.accessKeyId, aliConfig.accessKeySecret); + + // 上传文件 + 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.accessKey, minIOConfig.secretKey).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 minIOConfig = ObjectStoreServiceOptions.AWS; + + var minioClient = new MinioClient().WithEndpoint($"{minIOConfig.endPoint}") + .WithCredentials(minIOConfig.accessKey, minIOConfig.secretKey).WithSSL(minIOConfig.useSSL) + .Build(); + + var getObjectArgs = new GetObjectArgs() + .WithBucket(minIOConfig.bucketName) + .WithObject(ossRelativePath) + .WithFile(localFilePath); + + await minioClient.GetObjectAsync(getObjectArgs); + } + else + { + throw new BusinessValidationFailedException("未定义的存储介质类型"); + } + } + catch (Exception ex) + { + + throw new BusinessValidationFailedException("oss下载失败!" + ex.Message); + } + + + + + + } + + public async Task GetSignedUrl(string ossRelativePath) + { + ossRelativePath = ossRelativePath.TrimStart('/'); + try + { + + + if (ObjectStoreServiceOptions.ObjectStoreUse == "AliyunOSS") + { + var aliConfig = ObjectStoreServiceOptions.AliyunOSS; + + var _ossClient = new OssClient(aliConfig.endPoint, aliConfig.accessKeyId, aliConfig.accessKeySecret); + + // 生成签名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.accessKey, minIOConfig.secretKey).WithSSL(minIOConfig.useSSL) + .Build(); + + //var reqParams = new Dictionary(StringComparer.Ordinal) + // { + // { "response-content-type", "application/json" } + // }; + + 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 minIOConfig = ObjectStoreServiceOptions.AWS; + + var minioClient = new MinioClient().WithEndpoint($"{minIOConfig.endPoint}") + .WithCredentials(minIOConfig.accessKey, minIOConfig.secretKey).WithSSL(minIOConfig.useSSL) + .Build(); + + return string.Empty; + } + else + { + throw new BusinessValidationFailedException("未定义的存储介质类型"); + } + } + catch (Exception ex) + { + + throw new BusinessValidationFailedException("oss授权url失败!" + ex.Message); + } + + } + } + + +} diff --git a/IRC.Core.SCP/appsettings.IRC_Prod_SCP.json b/IRC.Core.SCP/appsettings.IRC_Prod_SCP.json new file mode 100644 index 000000000..32ee00af6 --- /dev/null +++ b/IRC.Core.SCP/appsettings.IRC_Prod_SCP.json @@ -0,0 +1,63 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "ObjectStoreService": { + "ObjectStoreUse": "AliyunOSS", + "AliyunOSS": { + "regionId": "cn-shanghai", + "endpoint": "https://oss-cn-shanghai.aliyuncs.com", + "accessKeyId": "LTAI5tKvzs7ed3UfSpNk3xwQ", + "accessKeySecret": "zTIceGEShlZDGnLrCFfIGFE7TXVRio", + "bucketName": "zy-irc-store", + "roleArn": "acs:ram::1899121822495495:role/oss-upload", + "viewEndpoint": "https://zy-irc-cache.oss-cn-shanghai.aliyuncs.com", + "region": "oss-cn-shanghai" + }, + + "MinIO": { + "endpoint": "http://192.168.3.68", + "port": "8001", + "useSSL": false, + "accessKey": "IDFkwEpWej0b4DtiuThL", + "secretKey": "Lhuu83yMhVwu7c1SnjvGY6lq74jzpYqifK6Qtj4h", + "bucketName": "test" + } + }, + + "ConnectionStrings": { + //"RemoteNew": "Server=47.117.165.18,1434;Database=Prod_Study;User ID=sa;Password=zhanying@2021;TrustServerCertificate=true", + //"Hangfire": "Server=47.117.165.18,1434;Database=Prod_Study_Hangfire;User ID=sa;Password=zhanying@2021;TrustServerCertificate=true", + "RemoteNew": "Server=prod_mssql_standard,1433;Database=Prod_Study;User ID=sa;Password=zhanying@2021;TrustServerCertificate=true", + "Hangfire": "Server=prod_mssql_standard,1433;Database=Prod_Study_Hangfire;User ID=sa;Password=zhanying@2021;TrustServerCertificate=true" + }, + + "BasicSystemConfig": { + + "OpenUserComplexPassword": true, + + "OpenSignDocumentBeforeWork": true, + + "OpenTrialRelationDelete": true, + + "OpenLoginLimit": true, + "LoginMaxFailCount": 5, + + "LoginFailLockMinutes": 30 + + }, + + "SystemEmailSendConfig": { + "Port": 465, + "Host": "smtp.qiye.aliyun.com", + "FromEmail": "study@extimaging.com", + "FromName": "研究单位阅片系统", + "AuthorizationCode": "zhanying123", + "SiteUrl": "https://study.extimaging.com/login" + } + +} diff --git a/IRC.Core.SCP/appsettings.IRC_Test_SCP.json b/IRC.Core.SCP/appsettings.IRC_Test_SCP.json new file mode 100644 index 000000000..5e93530c7 --- /dev/null +++ b/IRC.Core.SCP/appsettings.IRC_Test_SCP.json @@ -0,0 +1,79 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "ObjectStoreService": { + "ObjectStoreUse": "AliyunOSS", + "AliyunOSS": { + "regionId": "cn-shanghai", + "endPoint": "https://oss-cn-shanghai.aliyuncs.com", + "accessKeyId": "LTAI5tKvzs7ed3UfSpNk3xwQ", + "accessKeySecret": "zTIceGEShlZDGnLrCFfIGFE7TXVRio", + "bucketName": "zy-irc-test-store", + "roleArn": "acs:ram::1899121822495495:role/oss-upload", + "viewEndpoint": "https://zy-irc-test-store.oss-cn-shanghai.aliyuncs.com", + "region": "oss-cn-shanghai" + }, + + "MinIO": { + "endPoint": "106.14.89.110", + "port": "9001", + "useSSL": false, + "accessKey": "fbStsVYCIPKHQneeqMwD", + "secretKey": "TzgvyA3zGXMUnpilJNUlyMYHfosl1hBMl6lxPmjy", + "bucketName": "hir-test", + "viewEndpoint": "http://106.14.89.110:9001/hir-test/" + }, + + "AWS": { + "endPoint": "s3.us-east-1.amazonaws.com", + "useSSL": false, + "accessKey": "AKIAZQ3DRSOHFPJJ6FEU", + "secretKey": "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", + "bucketName": "ei-irc-test-store", + "viewEndpoint": "https://ei-irc-test-store.s3.amazonaws.com/" + } + }, + + "ConnectionStrings": { + "RemoteNew": "Server=106.14.89.110,1435;Database=Test_HIR;User ID=sa;Password=xc@123456;TrustServerCertificate=true", + "Hangfire": "Server=106.14.89.110,1435;Database=Test_HIR_Hangfire;User ID=sa;Password=xc@123456;TrustServerCertificate=true" + }, + "BasicSystemConfig": { + + "OpenUserComplexPassword": false, + + "OpenSignDocumentBeforeWork": false, + + "OpenTrialRelationDelete": true, + + "OpenLoginLimit": false, + "LoginMaxFailCount": 5, + + "LoginFailLockMinutes": 30 + + }, + + "SystemEmailSendConfig": { + "Port": 465, + "Host": "smtp.qiye.aliyun.com", + "FromEmail": "test-study@extimaging.com", + "FromName": "Test_Study", + "AuthorizationCode": "zhanying123", + + "SiteUrl": "http://study.test.extimaging.com/login" + }, + "DicomSCPServiceConfig": { + "CalledAEList": [ + "STORESCP", + "Value1", + "Value2", + "Value3" + ], + "ServerPort": 11112 + } +} diff --git a/IRC.Core.SCP/appsettings.IRC_US_SCP.json b/IRC.Core.SCP/appsettings.IRC_US_SCP.json new file mode 100644 index 000000000..7821a16b8 --- /dev/null +++ b/IRC.Core.SCP/appsettings.IRC_US_SCP.json @@ -0,0 +1,76 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "ObjectStoreService": { + "ObjectStoreUse": "MinIO", + "AliyunOSS": { + "regionId": "cn-shanghai", + "endpoint": "https://oss-cn-shanghai.aliyuncs.com", + "accessKeyId": "LTAI5tKvzs7ed3UfSpNk3xwQ", + "accessKeySecret": "zTIceGEShlZDGnLrCFfIGFE7TXVRio", + "bucketName": "zy-sir-test-store", + "roleArn": "acs:ram::1899121822495495:role/oss-upload", + "viewEndpoint": "https://zy-sir-test-store.oss-cn-shanghai.aliyuncs.com", + "region": "oss-cn-shanghai" + }, + + "MinIO": { + "endPoint": "44.218.11.19", + "port": "9001", + "useSSL": false, + "accessKey": "lH8DkKskLuDqPaiubuSQ", + "secretKey": "pdPdicvvLeH7xAC5yFUrI7odMyBfOXxvVWMvKYV4", + "bucketName": "hir-us", + "viewEndpoint": "http://hir.us.extimaging.com/oss/hir-us" + }, + + "AWS": { + "endPoint": "s3.us-east-1.amazonaws.com", + "useSSL": false, + "accessKey": "AKIAZQ3DRSOHFPJJ6FEU", + "secretKey": "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", + "bucketName": "ei-irc-test-store", + "viewEndpoint": "https://ei-irc-test-store.s3.amazonaws.com/" + } + }, + "ConnectionStrings": { + "RemoteNew": "Server=44.218.11.19,1435;Database=US_HIR;User ID=sa;Password=xc@123456;TrustServerCertificate=true", + "Hangfire": "Server=44.218.11.19,1435;Database=US_HIR_Hangfire;User ID=sa;Password=xc@123456;TrustServerCertificate=true" + }, + "BasicSystemConfig": { + + "OpenUserComplexPassword": false, + + "OpenSignDocumentBeforeWork": false, + + "OpenTrialRelationDelete": true, + + "OpenLoginLimit": false, + "LoginMaxFailCount": 5, + + "LoginFailLockMinutes": 30 + }, + + "SystemEmailSendConfig": { + "Port": 465, + "Host": "smtp.qiye.aliyun.com", + "FromEmail": "uat-study@extimaging.com", + "FromName": "Uat_Study", + "AuthorizationCode": "zhanying123", + "SiteUrl": "http://study.uat.extimaging.com/login" + }, + "DicomSCPServiceConfig": { + "CalledAEList": [ + "STORESCP", + "Value1", + "Value2", + "Value3" + ], + "ServerPort": 11112 + } +} diff --git a/IRC.Core.SCP/appsettings.IRC_Uat_SCP.json b/IRC.Core.SCP/appsettings.IRC_Uat_SCP.json new file mode 100644 index 000000000..f50eedf66 --- /dev/null +++ b/IRC.Core.SCP/appsettings.IRC_Uat_SCP.json @@ -0,0 +1,76 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "ObjectStoreService": { + "ObjectStoreUse": "MinIO", + "AliyunOSS": { + "regionId": "cn-shanghai", + "endpoint": "https://oss-cn-shanghai.aliyuncs.com", + "accessKeyId": "LTAI5tKvzs7ed3UfSpNk3xwQ", + "accessKeySecret": "zTIceGEShlZDGnLrCFfIGFE7TXVRio", + "bucketName": "zy-irc-uat-store", + "roleArn": "acs:ram::1899121822495495:role/oss-upload", + "viewEndpoint": "https://zy-irc-uat-store.oss-cn-shanghai.aliyuncs.com", + "region": "oss-cn-shanghai" + }, + + "MinIO": { + "endPoint": "47.117.164.182", + "port": "9001", + "useSSL": false, + "accessKey": "b9Ul0e98xPzt6PwRXA1Q", + "secretKey": "DzMaU2L4OXl90uytwOmDXF2encN0Jf4Nxu2XkYqQ", + "bucketName": "hir-uat", + "viewEndpoint": "http://hir.uat.extimaging.com/oss/hir-uat" + }, + + "AWS": { + "endPoint": "s3.us-east-1.amazonaws.com", + "useSSL": false, + "accessKey": "AKIAZQ3DRSOHFPJJ6FEU", + "secretKey": "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", + "bucketName": "ei-irc-test-store", + "viewEndpoint": "https://ei-irc-test-store.s3.amazonaws.com/" + } + }, + "ConnectionStrings": { + "RemoteNew": "Server=47.117.164.182,1434;Database=Uat_HIR;User ID=sa;Password=xc@123456;TrustServerCertificate=true", + "Hangfire": "Server=47.117.164.182,1434;Database=Uat_HIR_Hangfire;User ID=sa;Password=xc@123456;TrustServerCertificate=true" + }, + "BasicSystemConfig": { + + "OpenUserComplexPassword": false, + + "OpenSignDocumentBeforeWork": false, + + "OpenTrialRelationDelete": true, + + "OpenLoginLimit": false, + "LoginMaxFailCount": 5, + + "LoginFailLockMinutes": 30 + }, + + "SystemEmailSendConfig": { + "Port": 465, + "Host": "smtp.qiye.aliyun.com", + "FromEmail": "uat-study@extimaging.com", + "FromName": "Uat_Study", + "AuthorizationCode": "zhanying123", + "SiteUrl": "http://study.uat.extimaging.com/login" + }, + "DicomSCPServiceConfig": { + "CalledAEList": [ + "STORESCP", + "Value1", + "Value2", + "Value3" + ], + "ServerPort": 11112 + } +} diff --git a/IRC.Core.SCP/appsettings.json b/IRC.Core.SCP/appsettings.json new file mode 100644 index 000000000..10f68b8c8 --- /dev/null +++ b/IRC.Core.SCP/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 d5afa55fa..9511e66e3 100644 --- a/IRaCIS.Core.API.sln +++ b/IRaCIS.Core.API.sln @@ -17,6 +17,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IRaCIS.Core.Infra.EFCore", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IRaCIS.Core.Infrastructure", "IRaCIS.Core.Infrastructure\IRaCIS.Core.Infrastructure.csproj", "{07EED0F8-08E6-46F3-ACBE-17BC1391BD4C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IRC.Core.SCP", "IRC.Core.SCP\IRC.Core.SCP.csproj", "{ECD08F47-DC1A-484E-BB91-6CDDC8823CC5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -51,6 +53,10 @@ Global {07EED0F8-08E6-46F3-ACBE-17BC1391BD4C}.Debug|Any CPU.Build.0 = Debug|Any CPU {07EED0F8-08E6-46F3-ACBE-17BC1391BD4C}.Release|Any CPU.ActiveCfg = Release|Any CPU {07EED0F8-08E6-46F3-ACBE-17BC1391BD4C}.Release|Any CPU.Build.0 = Release|Any CPU + {ECD08F47-DC1A-484E-BB91-6CDDC8823CC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/IRaCIS.Core.API/Controllers/InspectionController.cs b/IRaCIS.Core.API/Controllers/InspectionController.cs index 7de33aaae..e9deab588 100644 --- a/IRaCIS.Core.API/Controllers/InspectionController.cs +++ b/IRaCIS.Core.API/Controllers/InspectionController.cs @@ -402,6 +402,19 @@ namespace IRaCIS.Core.API.Controllers return result; } + + [HttpPost, Route("Inspection/configTrialBasicInfo/ConfigTrialPACSInfoConfirm")] + [UnitOfWork] + [TypeFilter(typeof(TrialResourceFilter), Arguments = new object[] { "BeforeOngoingCantOpt" })] + public async Task ConfigTrialPACSInfoConfirm(DataInspectionDto opt) + { + opt.Data.IsTrialPACSConfirmed = true; + var singid = await _inspectionService.RecordSing(opt.SignInfo); + var result = await _trialConfigService.ConfigTrialPACSInfo(opt.Data); + await _inspectionService.CompletedSign(singid, result); + return result; + } + /// /// 签名确认 /// diff --git a/IRaCIS.Core.API/IRaCIS.Core.API.csproj b/IRaCIS.Core.API/IRaCIS.Core.API.csproj index f6e1614ef..a434c6d2b 100644 --- a/IRaCIS.Core.API/IRaCIS.Core.API.csproj +++ b/IRaCIS.Core.API/IRaCIS.Core.API.csproj @@ -64,22 +64,21 @@ - true - + - + - + - + diff --git a/IRaCIS.Core.Application/IRaCIS.Core.Application.xml b/IRaCIS.Core.Application/IRaCIS.Core.Application.xml index ed3163fab..ebd172a4c 100644 --- a/IRaCIS.Core.Application/IRaCIS.Core.Application.xml +++ b/IRaCIS.Core.Application/IRaCIS.Core.Application.xml @@ -707,6 +707,11 @@ + + + ExploreRecommendService + + InternationalizationService @@ -9808,6 +9813,24 @@ + + + DicomAEService + + + + + 获取项目dicom AE 配置信息,otherinfo里面有IsPACSConnect IsTrialPACSConfirmed + + + + + + + 测试scp server 是否可以连接 + + + 项目外部人员 录入流程相关 @@ -9820,6 +9843,11 @@ + + + TrialSiteDicomAEService + + TaskAllocationRuleView 列表视图模型 @@ -9960,6 +9988,15 @@ CommonDocumentAddOrEdit 列表查询参数模型 + + ExploreRecommendView 列表视图模型 + + + ExploreRecommendQuery 列表查询参数模型 + + + ExploreRecommendAddOrEdit 列表查询参数模型 + FrontAuditConfigView 列表视图模型 @@ -10959,6 +10996,15 @@ UserWLTemplateAddOrEdit 列表查询参数模型 + + DicomAEView 列表视图模型 + + + DicomAEQuery 列表查询参数模型 + + + DicomAEAddOrEdit 列表查询参数模型 + TrialExternalUserView 列表视图模型 @@ -10968,6 +11014,15 @@ TrialExternalUserAddOrEdit 列表查询参数模型 + + TrialSiteDicomAEView 列表视图模型 + + + TrialSiteDicomAEQuery 列表查询参数模型 + + + TrialSiteDicomAEAddOrEdit 列表查询参数模型 + TrialUserPreparation View 列表视图模型 @@ -11002,6 +11057,11 @@ ICommonDocumentService + + + IExploreRecommendService + + IFrontAuditConfigService @@ -11052,11 +11112,21 @@ IOrganInfoService + + + IDicomAEService + + ITrialExternalUserService + + + ITrialSiteDicomAEService + + EmailNoticeConfigView 列表视图模型 @@ -13040,6 +13110,13 @@ + + + 配置pacs信息 + + + + 更新项目状态 @@ -14971,6 +15048,41 @@ 9-拒绝入组,10-确认入组 + + + scp 影像推送记录表 + + + + + + + 影像检查列表-患者为维度组织 + + + + + + + 影像检查列表-> 获取患者的检查列表 + + + + + + + 影像访视上传 检查列表 + + + + + + + 提交 患者检查和访视的绑定 + + + + 添加或更新受试者信息[New] diff --git a/IRaCIS.Core.Application/Service/Common/DTO/ExploreRecommendViewModel.cs b/IRaCIS.Core.Application/Service/Common/DTO/ExploreRecommendViewModel.cs new file mode 100644 index 000000000..c61cb31a8 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Common/DTO/ExploreRecommendViewModel.cs @@ -0,0 +1,61 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2024-07-02 09:29:36 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- +using System; +using IRaCIS.Core.Domain.Share; +using System.Collections.Generic; +namespace IRaCIS.Core.Application.ViewModel +{ + /// ExploreRecommendView 列表视图模型 + public class ExploreRecommendView: ExploreRecommendAddOrEdit + { + + public DateTime CreateTime { get; set; } + public Guid CreateUserId { get; set; } + public Guid UpdateUserId { get; set; } + public DateTime UpdateTime { get; set; } + + public DateTime? DeleteTime { get; set; } + public Guid? DeleteUserId { get; set; } + } + + ///ExploreRecommendQuery 列表查询参数模型 + public class ExploreRecommendQuery:PageInput + { + + public string? Version { get; set; } + + + public string? Title { get; set; } + + + public string? DownloadUrl { get; set; } + + + public string? FileName { get; set; } + + public bool? IsDeleted { get; set; } + + } + + /// ExploreRecommendAddOrEdit 列表查询参数模型 + public class ExploreRecommendAddOrEdit + { + public Guid? Id { get; set; } + public string ExploreType { get; set; } + + public string Version { get; set; } + public string Title { get; set; } + + public bool IsDeleted { get; set; } + public string DownloadUrl { get; set; } + public string Path { get; set; } + public string FileName { get; set; } + } + + +} + + diff --git a/IRaCIS.Core.Application/Service/Common/ExploreRecommendService.cs b/IRaCIS.Core.Application/Service/Common/ExploreRecommendService.cs new file mode 100644 index 000000000..163b211a1 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Common/ExploreRecommendService.cs @@ -0,0 +1,92 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2024-07-02 09:26:59 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- + +using IRaCIS.Core.Domain.Models; +using Microsoft.AspNetCore.Mvc; +using IRaCIS.Core.Application.Interfaces; +using IRaCIS.Core.Application.ViewModel; +using Microsoft.AspNetCore.Authorization; +namespace IRaCIS.Core.Application.Service +{ + /// + /// ExploreRecommendService + /// + [ApiExplorerSettings(GroupName = "Common")] + public class ExploreRecommendService : BaseService, IExploreRecommendService + { + + private readonly IRepository _exploreRecommendRepository; + + public ExploreRecommendService(IRepository exploreRecommendRepository) + { + _exploreRecommendRepository = exploreRecommendRepository; + } + + [HttpPost] + public async Task> GetExploreRecommendList(ExploreRecommendQuery inQuery) + { + + var exploreRecommendQueryable = + + _exploreRecommendRepository.Where().IgnoreQueryFilters() + .WhereIf(string.IsNullOrEmpty(inQuery.Title), t => t.Title.Contains(inQuery.Title)) + .WhereIf(string.IsNullOrEmpty(inQuery.FileName), t => t.Title.Contains(inQuery.FileName)) + .WhereIf(string.IsNullOrEmpty(inQuery.DownloadUrl), t => t.Title.Contains(inQuery.DownloadUrl)) + .WhereIf(string.IsNullOrEmpty(inQuery.Version), t => t.Title.Contains(inQuery.Version)) + .WhereIf(inQuery.IsDeleted != null, t => t.IsDeleted == t.IsDeleted) + .ProjectTo(_mapper.ConfigurationProvider); + + var pageList = await exploreRecommendQueryable + .ToPagedListAsync(inQuery.PageIndex, inQuery.PageSize, string.IsNullOrWhiteSpace(inQuery.SortField) ? nameof(ExploreRecommendView.Id) : inQuery.SortField, + inQuery.Asc); + + return pageList; + } + + + public async Task AddOrUpdateExploreRecommend(ExploreRecommendAddOrEdit addOrEditExploreRecommend) + { + var verifyExp2 = new EntityVerifyExp() + { + VerifyExp = u => u.IsDeleted == addOrEditExploreRecommend.IsDeleted && u.ExploreType == addOrEditExploreRecommend.ExploreType, + + VerifyMsg = "当前浏览器启用版本只允许有一个", + + IsVerify = addOrEditExploreRecommend.IsDeleted == false + }; + + var entity = await _exploreRecommendRepository.InsertOrUpdateAsync(addOrEditExploreRecommend, true, verifyExp2); + + return ResponseOutput.Ok(entity.Id.ToString()); + + } + + + [HttpDelete("{exploreRecommendId:guid}")] + public async Task DeleteExploreRecommend(Guid exploreRecommendId) + { + var success = await _exploreRecommendRepository.DeleteFromQueryAsync(t => t.Id == exploreRecommendId, true); + return ResponseOutput.Ok(); + } + + [AllowAnonymous] + public async Task > GetExploreRecommentInfo() + { + + + var result = await _exploreRecommendRepository.Where(t => t.IsDeleted == false).ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + + if (result .Count==0) + { + throw new QueryBusinessObjectNotExistException("系统浏览器版本推荐未维护,请联系维护人员"); + } + + return result; + } + + + } +} diff --git a/IRaCIS.Core.Application/Service/Common/Interface/IExploreRecommendService.cs b/IRaCIS.Core.Application/Service/Common/Interface/IExploreRecommendService.cs new file mode 100644 index 000000000..2dc5556b2 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Common/Interface/IExploreRecommendService.cs @@ -0,0 +1,24 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2024-07-02 09:27:36 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- + +using IRaCIS.Core.Application.ViewModel; +namespace IRaCIS.Core.Application.Interfaces +{ + /// + /// IExploreRecommendService + /// + public interface IExploreRecommendService + { + + Task> GetExploreRecommendList(ExploreRecommendQuery inQuery); + + Task AddOrUpdateExploreRecommend(ExploreRecommendAddOrEdit addOrEditExploreRecommend); + + Task DeleteExploreRecommend(Guid exploreRecommendId); + + + } +} diff --git a/IRaCIS.Core.Application/Service/Common/_MapConfig.cs b/IRaCIS.Core.Application/Service/Common/_MapConfig.cs index 7312de9f9..916cabf1f 100644 --- a/IRaCIS.Core.Application/Service/Common/_MapConfig.cs +++ b/IRaCIS.Core.Application/Service/Common/_MapConfig.cs @@ -80,8 +80,9 @@ namespace IRaCIS.Core.Application.Service CreateMap().ReverseMap(); CreateMap(); - + CreateMap(); + CreateMap().ReverseMap(); } } diff --git a/IRaCIS.Core.Application/Service/ImageAndDoc/DownloadAndUploadService.cs b/IRaCIS.Core.Application/Service/ImageAndDoc/DownloadAndUploadService.cs index b733298cb..f8e3c2606 100644 --- a/IRaCIS.Core.Application/Service/ImageAndDoc/DownloadAndUploadService.cs +++ b/IRaCIS.Core.Application/Service/ImageAndDoc/DownloadAndUploadService.cs @@ -506,6 +506,39 @@ namespace IRaCIS.Core.Application.Service.ImageAndDoc } + + public async Task GetSubejectVisitPathInfo(Guid subjectVisitId) + { + var query = from sv in _subjectVisitRepository.Where(t => t.Id == subjectVisitId) + + select new + { + SubjectCode = sv.Subject.Code, + VisitName = sv.VisitName, + StudyList = sv.StudyList.Select(u => new + { + u.PatientId, + u.StudyTime, + u.StudyCode, + + SeriesList = u.SeriesList.Select(z => new + { + z.Modality, + + InstancePathList = z.DicomInstanceList.Select(k => new + { + k.Path + }) + }) + + }) + }; + + var info = query.FirstOrDefault(); + + return ResponseOutput.Ok(info); + } + /// /// 后台任务调用,前端忽略该接口 /// diff --git a/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/DicomAEViewModel.cs b/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/DicomAEViewModel.cs new file mode 100644 index 000000000..6e335b36d --- /dev/null +++ b/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/DicomAEViewModel.cs @@ -0,0 +1,66 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2024-03-22 15:44:37 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- +using System; +using IRaCIS.Core.Domain.Share; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +namespace IRaCIS.Core.Application.ViewModel +{ + /// DicomAEView 列表视图模型 + public class DicomAEView : DicomAEAddOrEdit + { + public DateTime CreateTime { get; set; } + public Guid CreateUserId { get; set; } + public Guid UpdateUserId { get; set; } + public DateTime UpdateTime { get; set; } + + public DateTime? LatestTestTime { get; set; } + + public bool IsTestOK { get; set; } + + public bool IsPACSConnect { get; set; } + + public bool IsTrialPACSConfirmed { get; set; } + + } + + ///DicomAEQuery 列表查询参数模型 + public class DicomAEQuery : PageInput + { + public Guid? TrialId { get; set; } + + public string? CalledAE { get; set; } + + public string? IP { get; set; } + + + public int? Port { get; set; } + + + public string? Modality { get; set; } + + public string? Description { get; set; } + } + + /// DicomAEAddOrEdit 列表查询参数模型 + public class DicomAEAddOrEdit + { + public Guid? Id { get; set; } + + [NotDefault] + public Guid TrialId { get; set; } + + public string CalledAE { get; set; } + public string IP { get; set; } + public int Port { get; set; } + public string Modality { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + } + + +} + + diff --git a/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/TrialConfigDTO.cs b/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/TrialConfigDTO.cs index dc0d0ded9..2508214e5 100644 --- a/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/TrialConfigDTO.cs +++ b/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/TrialConfigDTO.cs @@ -313,6 +313,16 @@ namespace IRaCIS.Core.Application.Contracts } + public class TrialPACSConfig + { + [NotDefault] + public Guid TrialId { get; set; } + + public bool IsPACSConnect { get; set; } + + public bool IsTrialPACSConfirmed { get; set; } = true; + } + public class TrialStateChangeDTO { public Guid Id { get; set; } diff --git a/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/TrialSiteDicomAEViewModel.cs b/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/TrialSiteDicomAEViewModel.cs new file mode 100644 index 000000000..568583c26 --- /dev/null +++ b/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/TrialSiteDicomAEViewModel.cs @@ -0,0 +1,60 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2024-07-02 16:53:52 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- +using System; +using IRaCIS.Core.Domain.Share; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +namespace IRaCIS.Core.Application.ViewModel +{ + /// TrialSiteDicomAEView 列表视图模型 + public class TrialSiteDicomAEView : TrialSiteDicomAEAddOrEdit + { + + public Guid UpdateUserId { get; set; } + public Guid? DeleteUserId { get; set; } + public DateTime CreateTime { get; set; } + public Guid CreateUserId { get; set; } + public DateTime UpdateTime { get; set; } + + } + + ///TrialSiteDicomAEQuery 列表查询参数模型 + public class TrialSiteDicomAEQuery /*: PageInput*/ + { + [NotDefault] + + public Guid TrialSiteId { get; set; } + + public string? CallingAE { get; set; } + + public string? IP { get; set; } + + public string? Port { get; set; } + + public string? Description { get; set; } + + } + + /// TrialSiteDicomAEAddOrEdit 列表查询参数模型 + public class TrialSiteDicomAEAddOrEdit + { + public Guid? Id { get; set; } + public Guid TrialId { get; set; } + + + public Guid TrialSiteId { get; set; } + public string CallingAE { get; set; } + public string IP { get; set; } + public string Port { get; set; } + public string Description { get; set; } + + //public bool IsDeleted { get; set; } + } + + +} + + diff --git a/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/UserTrialViewModel.cs b/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/UserTrialViewModel.cs index a3cd5fa36..a2fdd4c03 100644 --- a/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/UserTrialViewModel.cs +++ b/IRaCIS.Core.Application/Service/TrialSiteUser/DTO/UserTrialViewModel.cs @@ -259,7 +259,7 @@ namespace IRaCIS.Application.Contracts //public string ContactPhone { get; set; } = String.Empty; //public string Address { get; set; } = String.Empty; - + public List CallingAEList { get; set; } public List UserNameList { get; set; } = new List(); public int? VisitCount { get; set; } diff --git a/IRaCIS.Core.Application/Service/TrialSiteUser/Interface/IDicomAEService.cs b/IRaCIS.Core.Application/Service/TrialSiteUser/Interface/IDicomAEService.cs new file mode 100644 index 000000000..71f56aeb4 --- /dev/null +++ b/IRaCIS.Core.Application/Service/TrialSiteUser/Interface/IDicomAEService.cs @@ -0,0 +1,24 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2024-03-22 15:44:27 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- + +using IRaCIS.Core.Application.ViewModel; +namespace IRaCIS.Core.Application.Interfaces +{ + /// + /// IDicomAEService + /// + public interface IDicomAEService + { + + Task>> GetDicomAEList(DicomAEQuery inQuery); + + Task AddOrUpdateDicomAE(DicomAEAddOrEdit addOrEditDicomAE); + + Task DeleteDicomAE(Guid dicomAEId); + + + } +} diff --git a/IRaCIS.Core.Application/Service/TrialSiteUser/Interface/ITrialConfigService.cs b/IRaCIS.Core.Application/Service/TrialSiteUser/Interface/ITrialConfigService.cs index 7a06d0fb5..1de70d035 100644 --- a/IRaCIS.Core.Application/Service/TrialSiteUser/Interface/ITrialConfigService.cs +++ b/IRaCIS.Core.Application/Service/TrialSiteUser/Interface/ITrialConfigService.cs @@ -18,7 +18,7 @@ namespace IRaCIS.Application.Interfaces Task ConfigTrialUrgentInfo(TrialUrgentConfig trialConfig); - + Task ConfigTrialPACSInfo(TrialPACSConfig trialConfig); Task TrialConfigSignatureConfirm(SignConfirmDTO signConfirmDTO); Task AsyncTrialCriterionDictionary(AsyncTrialCriterionDictionaryInDto inDto); diff --git a/IRaCIS.Core.Application/Service/TrialSiteUser/Interface/ITrialSiteDicomAEService.cs b/IRaCIS.Core.Application/Service/TrialSiteUser/Interface/ITrialSiteDicomAEService.cs new file mode 100644 index 000000000..c6c6fc713 --- /dev/null +++ b/IRaCIS.Core.Application/Service/TrialSiteUser/Interface/ITrialSiteDicomAEService.cs @@ -0,0 +1,24 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2024-07-02 16:53:55 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- + +using IRaCIS.Core.Application.ViewModel; +namespace IRaCIS.Core.Application.Interfaces +{ + /// + /// ITrialSiteDicomAEService + /// + public interface ITrialSiteDicomAEService + { + + Task> GetTrialSiteDicomAEList(TrialSiteDicomAEQuery inQuery); + + Task AddOrUpdateTrialSiteDicomAE(TrialSiteDicomAEAddOrEdit addOrEditTrialSiteDicomAE); + + Task DeleteTrialSiteDicomAE(Guid trialSiteDicomAEId); + + + } +} diff --git a/IRaCIS.Core.Application/Service/TrialSiteUser/TrialConfigService.cs b/IRaCIS.Core.Application/Service/TrialSiteUser/TrialConfigService.cs index 573b0b300..4be695071 100644 --- a/IRaCIS.Core.Application/Service/TrialSiteUser/TrialConfigService.cs +++ b/IRaCIS.Core.Application/Service/TrialSiteUser/TrialConfigService.cs @@ -604,7 +604,7 @@ namespace IRaCIS.Core.Application await _readingQuestionCriterionTrialRepository.UpdatePartialFromQueryAsync(inDto.TrialReadingCriterionId, x => new ReadingQuestionCriterionTrial() { - IsImageFilter=inDto.IsImageFilter, + IsImageFilter = inDto.IsImageFilter, ImageDownloadEnum = inDto.ImageDownloadEnum, ImageUploadEnum = inDto.ImageUploadEnum, CriterionModalitys = inDto.CriterionModalitys, @@ -954,7 +954,7 @@ namespace IRaCIS.Core.Application trialInfo.UpdateTime = DateTime.Now; - //await _readingQuestionCriterionTrialRepository.BatchUpdateNoTrackingAsync(t => t.TrialId == trialConfig.TrialId && t.IsSigned == false, u => new ReadingQuestionCriterionTrial() { CriterionModalitys = trialConfig.Modalitys }); + //await _readingQuestionCriterionTrialRepository.BatchUpdateNoTrackingAsync(t => t.TrialId == trialConfig.TrialId && t.IsSigned == false, u => new ReadingQuestionCriterionTrial() { CriterionModalitys = trialConfig.Modalitys }); return ResponseOutput.Ok(await _repository.SaveChangesAsync()); } @@ -1151,6 +1151,25 @@ namespace IRaCIS.Core.Application return ResponseOutput.Ok(await _repository.SaveChangesAsync()); } + /// + /// 配置pacs信息 + /// + /// + /// + [HttpPut] + [TypeFilter(typeof(TrialResourceFilter), Arguments = new object[] { "BeforeOngoingCantOpt", "AfterStopCannNotOpt" })] + public async Task ConfigTrialPACSInfo(TrialPACSConfig trialConfig) + { + var trialInfo = (await _trialRepository.FirstOrDefaultAsync(t => t.Id == trialConfig.TrialId)).IfNullThrowException(); + trialInfo.IsPACSConnect = trialConfig.IsPACSConnect; + trialConfig.IsTrialPACSConfirmed = trialConfig.IsTrialPACSConfirmed; + trialInfo.UpdateTime = DateTime.Now; + await _trialRepository.SaveChangesAsync(); + + return ResponseOutput.Ok(await _repository.SaveChangesAsync()); + } + + [HttpGet("{trialId:guid}")] public async Task IfTrialCanOngoing(Guid trialId) { @@ -1318,7 +1337,7 @@ namespace IRaCIS.Core.Application public async Task>> GetTrialBodyPartList(Guid trialId) { - var list = await _trialRepository.Where(t => t.Id == trialId).SelectMany(t => t.TrialBodyPartList).Select(t => new TrialBodyPartView() { Code = t.Code, Name = _userInfo.IsEn_Us ? t.Name : t.NameCN ,Id=t.Id,IsHandAdd=t.IsHandAdd}).ToListAsync(); + var list = await _trialRepository.Where(t => t.Id == trialId).SelectMany(t => t.TrialBodyPartList).Select(t => new TrialBodyPartView() { Code = t.Code, Name = _userInfo.IsEn_Us ? t.Name : t.NameCN, Id = t.Id, IsHandAdd = t.IsHandAdd }).ToListAsync(); return ResponseOutput.Ok(list); } diff --git a/IRaCIS.Core.Application/Service/TrialSiteUser/TrialDicomAEService.cs b/IRaCIS.Core.Application/Service/TrialSiteUser/TrialDicomAEService.cs new file mode 100644 index 000000000..982bab572 --- /dev/null +++ b/IRaCIS.Core.Application/Service/TrialSiteUser/TrialDicomAEService.cs @@ -0,0 +1,149 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2024-03-22 15:44:31 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- + +using IRaCIS.Core.Domain.Models; +using Microsoft.AspNetCore.Mvc; +using IRaCIS.Core.Application.Interfaces; +using IRaCIS.Core.Application.ViewModel; +using FellowOakDicom.Network.Client; +using FellowOakDicom.Network; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Domain.Share; +namespace IRaCIS.Core.Application.Service +{ + /// + /// DicomAEService + /// + [ApiExplorerSettings(GroupName = "Trial")] + public class TrialDicomAEService : BaseService, IDicomAEService + { + + private readonly IRepository _dicomAERepository; + private readonly IRepository _trialRepository; + + public TrialDicomAEService(IRepository dicomAERepository, IRepository trialRepository) + { + _trialRepository = trialRepository; + _dicomAERepository = dicomAERepository; + } + + [HttpPost] + public async Task>> GetDicomAEList(DicomAEQuery inQuery) + { + + var dicomAEQueryable = _dicomAERepository + .WhereIf(!string.IsNullOrWhiteSpace(inQuery.IP), t => t.IP.Contains(inQuery.IP)) + .WhereIf(inQuery.Port != null, t => t.Port == inQuery.Port) + .WhereIf(!string.IsNullOrWhiteSpace(inQuery.CalledAE), t => t.CalledAE.Contains(inQuery.CalledAE)) + .WhereIf(!string.IsNullOrWhiteSpace(inQuery.Description), t => t.Description.Contains(inQuery.Description)) + .WhereIf(!string.IsNullOrWhiteSpace(inQuery.Modality), t => t.Modality.Contains(inQuery.Modality)) + .ProjectTo(_mapper.ConfigurationProvider); + + + + + var pageList = await dicomAEQueryable.ToPagedListAsync(inQuery.PageIndex, inQuery.PageSize, inQuery.SortField == string.Empty ? nameof(DicomAEView.CalledAE) : inQuery.SortField, inQuery.Asc); + + + return ResponseOutput.Ok(pageList); + } + + + /// + /// 获取项目dicom AE 配置信息,otherinfo里面有IsPACSConnect IsTrialPACSConfirmed + /// + /// + /// + public async Task> GetTrialDicomAE(Guid trialId) + { + var dicomAE = _dicomAERepository.Where(t => t.TrialId == trialId).ProjectTo(_mapper.ConfigurationProvider).FirstOrDefault(); + var trialConfig = _trialRepository.Where(t => t.Id == trialId).Select(t => new { t.IsPACSConnect, t.IsTrialPACSConfirmed }); + return ResponseOutput.Ok(dicomAE, trialConfig); + + } + + + public async Task AddOrUpdateDicomAE(DicomAEAddOrEdit addOrEditDicomAE) + { + var verifyExp1 = new EntityVerifyExp() + { + VerifyExp = u => u.IP == addOrEditDicomAE.IP && u.Port == addOrEditDicomAE.Port && u.TrialId == addOrEditDicomAE.TrialId, + + VerifyMsg = "不允许添加相同的IP和端口的记录" + }; + + //var verifyExp2 = new EntityVerifyExp() + //{ + // VerifyExp = u => u.TrialId == addOrEditDicomAE.TrialId, + + // VerifyMsg = "只允许配置一条记录", + // IsVerify=addOrEditDicomAE.Id==null + //}; + + // 在此处拷贝automapper 映射 + var entity = await _dicomAERepository.InsertOrUpdateAsync(addOrEditDicomAE, true, verifyExp1); + + return ResponseOutput.Ok(entity.Id.ToString()); + + } + + + [HttpDelete("{dicomAEId:guid}")] + public async Task DeleteDicomAE(Guid dicomAEId) + { + var success = await _dicomAERepository.DeleteFromQueryAsync(t => t.Id == dicomAEId, true); + return ResponseOutput.Ok(); + } + + + /// + /// 测试scp server 是否可以连接 + /// + /// + [HttpGet("{dicomAEId:guid}")] + public async Task TestSCPServerConnect(Guid dicomAEId) + { + var find = await _dicomAERepository.FirstOrDefaultAsync(t => t.Id == dicomAEId); + + if (find == null) + { + + return false; + } + else + { + find.LatestTestTime = DateTime.Now; + + try + { + var client = DicomClientFactory.Create(find.IP, find.Port, false, "test-callingAE", find.CalledAE); + + client.NegotiateAsyncOps(); + + await client.AddRequestAsync(new DicomCEchoRequest()); + + await client.SendAsync(); + + find.IsTestOK = true; + await _dicomAERepository.SaveChangesAsync(); + + return true; + } + catch (Exception ex) + { + find.IsTestOK = false; + await _dicomAERepository.SaveChangesAsync(); + + return false; + } + } + + + } + + + } +} diff --git a/IRaCIS.Core.Application/Service/TrialSiteUser/TrialSiteDicomAEService.cs b/IRaCIS.Core.Application/Service/TrialSiteUser/TrialSiteDicomAEService.cs new file mode 100644 index 000000000..7b5318d44 --- /dev/null +++ b/IRaCIS.Core.Application/Service/TrialSiteUser/TrialSiteDicomAEService.cs @@ -0,0 +1,76 @@ +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2024-07-02 16:53:58 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +//-------------------------------------------------------------------- + +using IRaCIS.Core.Domain.Models; +using Microsoft.AspNetCore.Mvc; +using IRaCIS.Core.Application.Interfaces; +using IRaCIS.Core.Application.ViewModel; +namespace IRaCIS.Core.Application.Service +{ + /// + /// TrialSiteDicomAEService + /// + [ApiExplorerSettings(GroupName = "Trial")] + public class TrialSiteDicomAEService : BaseService, ITrialSiteDicomAEService + { + + private readonly IRepository _trialSiteDicomAERepository; + + public TrialSiteDicomAEService(IRepository trialSiteDicomAERepository) + { + _trialSiteDicomAERepository = trialSiteDicomAERepository; + } + + [HttpPost] + public async Task> GetTrialSiteDicomAEList(TrialSiteDicomAEQuery inQuery) + { + + var trialSiteDicomAEQueryable = + + _trialSiteDicomAERepository.Where(t=>t.TrialSiteId==inQuery.TrialSiteId) + .WhereIf(!string.IsNullOrWhiteSpace(inQuery.IP), t => t.IP.Contains(inQuery.IP)) + .WhereIf(!string.IsNullOrWhiteSpace(inQuery.Port), t => t.Port.Contains(inQuery.Port)) + .WhereIf(!string.IsNullOrWhiteSpace(inQuery.Description), t => t.Description.Contains(inQuery.Description)) + .WhereIf(!string.IsNullOrWhiteSpace(inQuery.CallingAE), t => t.CallingAE.Contains(inQuery.CallingAE)) + .ProjectTo(_mapper.ConfigurationProvider); + + //var pageList = await trialSiteDicomAEQueryable + //.ToPagedListAsync(inQuery.PageIndex, inQuery.PageSize, string.IsNullOrWhiteSpace(inQuery.SortField) ? nameof(TrialSiteDicomAEView.Id) : inQuery.SortField, + //inQuery.Asc); + + var list = await trialSiteDicomAEQueryable.ToListAsync(); + + return list; + } + + + public async Task AddOrUpdateTrialSiteDicomAE(TrialSiteDicomAEAddOrEdit addOrEditTrialSiteDicomAE) + { + var verifyExp1 = new EntityVerifyExp() + { + VerifyExp = u => u.IP == addOrEditTrialSiteDicomAE.IP && u.Port == addOrEditTrialSiteDicomAE.Port &&u.CallingAE==addOrEditTrialSiteDicomAE.CallingAE && u.TrialId == addOrEditTrialSiteDicomAE.TrialId, + + VerifyMsg = "不允许添加相同的IP和端口的记录" + }; + + + var entity = await _trialSiteDicomAERepository.InsertOrUpdateAsync(addOrEditTrialSiteDicomAE, true, verifyExp1); + + return ResponseOutput.Ok(entity.Id.ToString()); + + } + + + [HttpDelete("{trialSiteDicomAEId:guid}")] + public async Task DeleteTrialSiteDicomAE(Guid trialSiteDicomAEId) + { + var success = await _trialSiteDicomAERepository.DeleteFromQueryAsync(t => t.Id == trialSiteDicomAEId, true); + return ResponseOutput.Ok(); + } + + + } +} diff --git a/IRaCIS.Core.Application/Service/TrialSiteUser/_MapConfig.cs b/IRaCIS.Core.Application/Service/TrialSiteUser/_MapConfig.cs index 2e38d4476..461523bfa 100644 --- a/IRaCIS.Core.Application/Service/TrialSiteUser/_MapConfig.cs +++ b/IRaCIS.Core.Application/Service/TrialSiteUser/_MapConfig.cs @@ -141,7 +141,8 @@ namespace IRaCIS.Core.Application.Service .ForMember(d => d.UserCount, u => u.MapFrom(s => s.CRCUserList.Count())) .ForMember(d => d.VisitCount, u => u.MapFrom(s => s.SubjectVisitList.Count())) .ForMember(d => d.SubjectCount, u => u.MapFrom(s => s.SubjectList.Count())) - .ForMember(d => d.UserNameList, u => u.MapFrom(s => s.CRCUserList.Where(t => t.IsDeleted == false).Select(u => u.User.FullName))); + .ForMember(d => d.UserNameList, u => u.MapFrom(s => s.CRCUserList.Where(t => t.IsDeleted == false).Select(u => u.User.FullName))) + .ForMember(d => d.CallingAEList, u => u.MapFrom(s => s.TrialSiteDicomAEList.Select(u => u.CallingAE))); //CreateMap(); diff --git a/IRaCIS.Core.Application/Service/TrialSiteUser/_MapConfig2.cs b/IRaCIS.Core.Application/Service/TrialSiteUser/_MapConfig2.cs index b608fb229..96041148e 100644 --- a/IRaCIS.Core.Application/Service/TrialSiteUser/_MapConfig2.cs +++ b/IRaCIS.Core.Application/Service/TrialSiteUser/_MapConfig2.cs @@ -45,7 +45,11 @@ namespace IRaCIS.Core.Application.Service CreateMap(); - + + CreateMap(); + CreateMap().ReverseMap(); + + } } diff --git a/IRaCIS.Core.Application/Service/Visit/DTO/PatientViewModel.cs b/IRaCIS.Core.Application/Service/Visit/DTO/PatientViewModel.cs new file mode 100644 index 000000000..0c3c515f8 --- /dev/null +++ b/IRaCIS.Core.Application/Service/Visit/DTO/PatientViewModel.cs @@ -0,0 +1,1145 @@ +using IRaCIS.Core.Application.ViewModel; +using IRaCIS.Core.Domain.Share; +using IRaCIS.Core.Infrastructure.Extention; +using Newtonsoft.Json; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Web; + +namespace IRaCIS.Application.Contracts +{ + public class SystemHospitalOption + { + public string HospitalName { get; set; } + public string HospitalAliasName { get; set; } + public string Country { get; set; } + public string City { get; set; } + public string Province { get; set; } + public string Address { get; set; } + public string Phone { get; set; } + + public bool IsCanConnectInternet { get; set; } + + public string HospitalCode { get; set; } + public string HospitalLogoPath { get; set; } + public int TrialKeepCount { get; set; } + + } + + + public class NewTrialView : PatientJoinTrialInitView + { + public Guid Id { get; set; } + //授权年限 + public int AuthorizationDuration { get; set; } + + public DateTime? AuthorizationDate { get; set; } + + public string AuthorizationEncrypt { get; set; } + + public string CriterionTypes { get; set; } + public List CriterionTypeList => CriterionTypes.Split('|', StringSplitOptions.RemoveEmptyEntries) + .Select(s => Enum.Parse(typeof(CriterionType), s)).Cast().ToList(); + + public int? UnSubmitCount { get; set; } + + public int? UnReadCount { get; set; } + } + + public class NewTrialQuery : PageInput + { + public string? ExperimentName { get; set; } + public string? ResearchProgramNo { get; set; } + public TrialType? TrialType { get; set; } + + public string? SponsorName { get; set; } + + public string? TrialCode { get; set; } + } + + public class TrialInfoDTO : AddOrUpdateTrialCommand + { + [JsonIgnore] + public List DictionaryList { get; set; } = new List(); + + public new List ModalityIds => DictionaryList.Where(t => t.ParentCode == StaticData.Modality).OrderBy(t => t.ShowOrder).Select(t => t.Id).ToList(); + + public string CriterionTypes { get; set; } = string.Empty; + + + public new List CriterionTypeList => CriterionTypes.Split('|', StringSplitOptions.RemoveEmptyEntries) + .Select(s => Enum.Parse(typeof(CriterionType), s)).Cast().ToList(); + + public string AuthorizationEncrypt { get; set; } + + public TrialAuthorizationInfo AuthorizationInfo { get; set; } + + public string TrialStatusStr { get; set; } + + public DateTime CreateTime { get; set; } + } + + public class AddOrUpdateTrialCommand + { + + public Guid? Id { get; set; } + public string TrialCode { get; set; } = string.Empty; + + public TrialType TrialType { get; set; } + + + //研究方案号 + public string ResearchProgramNo { get; set; } = string.Empty; + + //实验名称 + public string ExperimentName { get; set; } = string.Empty; + + // 负责人PI + public string HeadPI { get; set; } = string.Empty; + + public string CRO { get; set; } = string.Empty; + + public string Sponsor { get; set; } = string.Empty; + + //药物名称 + public string MedicineName { get; set; } = string.Empty; + + + //临床分期 + public Guid? PhaseId { get; set; } + + //适应症 + public string Indication { get; set; } = string.Empty; + + //检查技术 + public List ModalityIds { get; set; } = new List(); + + //阅片标准 + public List CriterionTypeList { get; set; } + + //联系人 + public string ContactUser { get; set; } = string.Empty; + //联系电话 + public string ContactPhone { get; set; } = string.Empty; + + //授权年限 + public int AuthorizationDuration { get; set; } + + public DateTime? AuthorizationDate { get; set; } + + } + + public class PatientJoinTrialInitQuery : PageInput + { + [NotDefault] + public Guid PatientId { get; set; } + + public string? Filter { get; set; } + + } + + public class PatientJoinedTrialQuery + { + [NotDefault] + public Guid PatientId { get; set; } + } + + public class PatientJoinedTrialView : PatientJoinTrialInitView + { + public Guid SubjectId { get; set; } + public string Code { get; set; } + + public string ShortName { get; set; } + + public string Sex { get; set; } + + public int? Age { get; set; } + } + + public class PatientJoinTrialSelectView + { + public Guid TrialId { get; set; } + public string ExperimentName { get; set; } + public string ResearchProgramNo { get; set; } + public string Sponsor { get; set; } + } + + + public class TrialCacheInfo + { + public Guid TrialId { get; set; } + public Guid CreateUserId { get; set; } + public string TrialCode { get; set; } + + public DateTime? AuthorizationDate { get; set; } + public string TrialStatusStr { get; set; } + + public string AuthorizationEncrypt { get; set; } + + public string CriterionTypes { get; set; } + public List CriterionTypeList => CriterionTypes.Split('|', StringSplitOptions.RemoveEmptyEntries) + .Select(s => Enum.Parse(typeof(CriterionType), s)).Cast().ToList(); + } + public class TrialAuthorizationInfo + { + public Guid TrialId { get; set; } + public Guid CreateUserId { get; set; } + public string TrialCode { get; set; } + + public string HospitalName { get; set; } + + public string HospitalCode { get; set; } + + public int PurchaseDuration { get; set; } + + + + public List CriterionTypeList { get; set; } + + public DateTime? AuthorizationDeadLineDate { get; set; } + + public DateTime? ActiveDeadLineDate { get; set; } + + public DateTime? ActiveTime { get; set; } + + } + public class PatientJoinTrialInitView + { + public Guid TrialId { get; set; } + + public string TrialCode { get; set; } + public string ExperimentName { get; set; } + public string ResearchProgramNo { get; set; } + public TrialType TrialType { get; set; } + + public string Sponsor { get; set; } + + public string TrialStatusStr { get; set; } + + public DateTime CreateTime { get; set; } + } + + + public class PatientTrialQuery : PageInput + { + public string? PatientIdStr { get; set; } + public string? PatientName { get; set; } + //public List CalledAEList { get; set; } = new List(); + + public string? CallingAE { get; set; } + public string? CalledAE { get; set; } + + public DateTime? BeginPushTime { get; set; } + public DateTime? EndPushTime { get; set; } + + public string SubejctCode { get; set; } + + public string TrialSiteKeyInfo { get; set; } + + } + + public class PatientSubjectView : PatientQueryView + { + + public int? StudyCount { get; set; } + + + + public Guid? SubejctId { get; set; } + + public Guid TrialId { get; set; } + + public string? SubjectCode { get; set; } + + public string? TrialSiteCode { get; set; } + + public string? TrialSiteName { get; set; } + + public string? TrialSiteAliasName { get; set; } + } + + + + public class PatientQuery : PageInput + { + [NotDefault] + public Guid TrialId { get; set; } + public Guid? SubjectId { get; set; } + public string? PatientIdStr { get; set; } + public string? PatientName { get; set; } + public List CalledAEList { get; set; } = new List(); + + public string? CallingAE { get; set; } + + public DateTime? EarliestStudyTime { get; set; } + + public DateTime? LatestStudyTime { get; set; } + + } + + public class PatientQueryView + { + public Guid PatientId { get; set; } + public string PatientIdStr { get; set; } = string.Empty; + public string PatientName { get; set; } = string.Empty; + public string PatientAge { get; set; } = string.Empty; + public string PatientSex { get; set; } = string.Empty; + public string PatientBirthDate { get; set; } = string.Empty; + + public DateTime? EarliestStudyTime { get; set; } + + public DateTime? LatestStudyTime { get; set; } + + public DateTime LatestPushTime { get; set; } + + public List CallingAEList { get; set; } = new List(); + + public List CalledAEList { get; set; } = new List(); + + public Guid UpdateUserId { get; set; } + public DateTime UpdateTime { get; set; } = DateTime.Now; + public Guid CreateUserId { get; set; } + public DateTime CreateTime { get; set; } = DateTime.Now; + } + + public class UpdateSubjectVisitStudyBindingCommand + { + [NotDefault] + public Guid TrialId { get; set; } + + [NotDefault] + public Guid SubjectId { get; set; } + [NotDefault] + public Guid SCPStudyId { get; set; } + + [NotDefault] + public Guid SubjectVisitId { get; set; } + + public bool IsAdd { get; set; } + + + } + + public class DeleteSubejctPatientCommand + { + [NotDefault] + public Guid SubjectId { get; set; } + [NotDefault] + public Guid PatientId { get; set; } + + [NotDefault] + public Guid TrialId { get; set; } + } + + + public class VisitStudyVerifyTimeQuery + { + public Guid SCPStudyId { get; set; } + + public Guid SubjectId { get; set; } + public Guid TrialId { get; set; } + } + + public class VerifyTrialSubjectCommand + { + [NotDefault] + public Guid TrialId { get; set; } + public string SubjectCode { get; set; } = string.Empty; + } + public class AddSubjectPatientCommand + { + [NotDefault] + public Guid TrialId { get; set; } + public string SubjectCode { get; set; } = string.Empty; + + public Guid? SubjectId { get; set; } + + public List PatientIdList { get; set; } + } + + public class VerifyVisitStudyDTO + { + public Guid SubjectVisitId { get; set; } + public decimal VisitNum { get; set; } + public DateTime? StudyTime { get; set; } + public Guid SCPStudyId { get; set; } + } + public class AddSubjectPatientStudyVisitCommand + { + [NotDefault] + public Guid TrialId { get; set; } + [NotDefault] + public Guid SubjectId { get; set; } + [NotDefault] + public Guid SubjectVisitId { get; set; } + [NotDefault] + public Guid SCPStudyId { get; set; } + + //public string VisitName { get; set; } + + } + + public class SubmitVisitStudyBindingCommand + { + [NotDefault] + public Guid TrialId { get; set; } + + [NotDefault] + public Guid SubjectId { get; set; } + + [NotDefault] + public Guid SubjectVisitId { get; set; } + + public List SCPStudyIdList { get; set; } + } + + public class SubjectVisitSelectQuery + { + [NotDefault] + public Guid TrialId { get; set; } + [NotDefault] + public Guid SubjectId { get; set; } + + public Guid? SCPStudyId { get; set; } + } + + public class SubjectVisitSelectDto + { + public Guid Id { get; set; } + + public Guid TrialId { get; set; } + public Guid SubjectId { get; set; } + + public string VisitNum { get; set; } + + public string VisitName { get; set; } + + public SubmitStateEnum SubmitState { get; set; } + + public DateTime? VisitMaxStudyTime { get; set; } + + public DateTime? VisitMinStudyTime { get; set; } + } + public class AddOrUpdateSubjectVisitCommand + { + public Guid? Id { get; set; } + public Guid TrialId { get; set; } + public Guid SubjectId { get; set; } + + public decimal VisitNum { get; set; } + + public string VisitName { get; set; } + } + public class SubjectSelectQuery + { + public string? SubjectCode { get; set; } + + + public Guid? SubjectId { get; set; } + + [NotDefault] + public Guid TrialId { get; set; } + } + + public class SubjectPatientSelectQuery + { + public string? SubjectCode { get; set; } + public string? PatientIdStr { get; set; } + + public Guid? SubjectId { get; set; } + + public Guid? PatientId { get; set; } + [NotDefault] + public Guid TrialId { get; set; } + } + public class SubjectSelectDto + { + public Guid SubejctId { get; set; } + + public string SubjectCode { get; set; } + + public string ShortName { get; set; } + + public string Sex { get; set; } + + public int? Age { get; set; } + + public DateTime? BirthDate { get; set; } + + public SubjectStatus Status { get; set; } + + public List PatientList { get; set; } + } + + public class PatienBasicInfo + { + public Guid PatientId { get; set; } + public string PatientIdStr { get; set; } + } + + public class SubjectPatientSelectDto + { + public Guid Id { get; set; } + public Guid PatientId { get; set; } + public Guid TrialId { get; set; } + public Guid SubejctId { get; set; } + public string? PatientIdStr { get; set; } + public string SubjectCode { get; set; } + } + + public class PatientStudyInfoQuery : PageInput + { + [NotDefault] + public Guid PatientId { get; set; } + + public DateTime? EarliestStudyTime { get; set; } + + public DateTime? LatestStudyTime { get; set; } + public string? Modalities { get; set; } + } + + public class PatientStudyQuery : PageInput + { + [NotDefault] + public Guid TrialId { get; set; } + + public List PatientIdList { get; set; } + + + + public DateTime? EarliestStudyTime { get; set; } + + public DateTime? LatestStudyTime { get; set; } + } + + + public class PatientVisitTaskQuery : PageInput + { + [NotDefault] + public Guid TrialId { get; set; } + + + public Guid? SubjectId { get; set; } + + public string SubjectCode { get; set; } = String.Empty; + + public string? SubjectShortName { get; set; } + + public string TaskName { get; set; } = String.Empty; + + public Guid? DoctorUserId { get; set; } + + public ReadingCategory? ReadingCategory { get; set; } + + public ReadingTaskState? ReadingTaskState { get; set; } + + public TaskState? TaskState { get; set; } + + + public string? TaskCode { get; set; } + + public Arm? ArmEnum { get; set; } + + public Guid? TrialReadingCriterionId { get; set; } + + + public DateTime? BeginSignTime { get; set; } + public DateTime? EndSignTime { get; set; } + + public DateTime? BeginTaskCreateTime { get; set; } + public DateTime? EndTaskCreateTime { get; set; } + + + public string? PatientName { get; set; } + public string? PatientIdStr { get; set; } + + public string? PatientSex { get; set; } + + } + + public class PatientVisitTaskDTO + { + public List PatientList { get; set; } + + public Guid Id { get; set; } + public Guid TrialId { get; set; } + + public string TaskCode { get; set; } + + public string TaskName { get; set; } + public string TaskBlindName { get; set; } + + public decimal VisitTaskNum { get; set; } + + public Guid? SourceSubjectVisitId { get; set; } + + public ReadingCategory ReadingCategory { get; set; } + + + public TaskState TaskState { get; set; } + + public DateTime? SignTime { get; set; } + + public DateTime? AllocateTime { get; set; } + public Guid SubjectId { get; set; } + + public string SubjectCode { get; set; } = String.Empty; + + public string SubjectShortName { get; set; } + + public string VisitImageZipPath { get; set; } + + public PackState PackState { get; set; } + + public Arm ArmEnum { get; set; } + public Guid? DoctorUserId { get; set; } + + + public Guid TrialReadingCriterionId { get; set; } + + public string TrialReadingCriterionName { get; set; } + + public CriterionType CriterionType { get; set; } + + public DateTime CreateTime { get; set; } + + public UserSimpleInfo DoctorUser { get; set; } + + //任务阅片状态 + public ReadingTaskState ReadingTaskState { get; set; } + + //public bool IsUrgent { get; set; } + + ///// + ///// 加急类型 + ///// + //public TaskUrgentType? TaskUrgentType { get; set; } + + + //public string TaskUrgentRemake { get; set; } = string.Empty; + + + //public TaskAllocationState TaskAllocationState { get; set; } + //public bool IsNeedClinicalDataSign { get; set; } + + //public bool IsClinicalDataSign { get; set; } + + } + + public class AutoBindingPatientStudyVisitCommand + { + [NotDefault] + public Guid TrialId { get; set; } + + + } + public class TrialPatientStudyQuery : PageInput + { + [NotDefault] + public Guid TrialId { get; set; } + + public string? SubjectCode { get; set; } + + + public Guid? PatientId { get; set; } + + public string? PatientIdStr { get; set; } + + public string? PatientSex { get; set; } + + public string? SubjectShortName { get; set; } + + public int? SubjectAge { get; set; } + public string? SubjectSex { get; set; } + + public DateTime? BeginStudyTime { get; set; } + + public DateTime? EndStudyTime { get; set; } + + public string? VisitName { get; set; } + + public bool? IsBindedVisit { get; set; } + } + + public class AddOrUpdateSubjectCommand + { + + public Guid? Id { get; set; } + + [NotDefault] + public Guid TrialId { get; set; } + public string Code { get; set; } = String.Empty; + + public int? Age { get; set; } + public string Sex { get; set; } = string.Empty; + + public string ShortName { get; set; } = string.Empty; + + + public DateTime? BirthDate { get; set; } + + + } + public class PatientSubjectQuery : PageInput + { + [NotDefault] + public Guid TrialId { get; set; } + + public string? Code { get; set; } + + public string? ShortName { get; set; } + + public string? Sex { get; set; } + + public SubjectStatus? Status { get; set; } + + public string? PatientName { get; set; } + public string? PatientIdStr { get; set; } + + public string? PatientSex { get; set; } + + } + + public class PatienSubejctView + { + public List PatientList { get; set; } + + public Guid TrialId { get; set; } + public Guid Id { get; set; } + public string Code { get; set; } + + + public string ShortName { get; set; } + + public string Sex { get; set; } + + public int? Age { get; set; } + + public SubjectStatus Status { get; set; } + + public DateTime? BirthDate { get; set; } + + public int? VisitCount { get; set; } + + public DateTime CreateTime { get; set; } + + public string LatestVisitName { get; set; } + + public DateTime? VisitOverTime { get; set; } + + public Guid? FinalSubjectVisitId { get; set; } + + public string Reason { get; set; } + } + + public class PatientSubejctVisitQuery : PageInput + { + [NotDefault] + public Guid TrialId { get; set; } + public string? SubjectCode { get; set; } + + + public string? SubjectShortName { get; set; } + + public string? SubjectSex { get; set; } + + public string? PatientName { get; set; } + public string? PatientIdStr { get; set; } + + public string? PatientSex { get; set; } + + public string? VisitName { get; set; } + + public SubmitStateEnum? SubmitState { get; set; } + + public DateTime? BeginStudyTime { get; set; } + + public DateTime? EndStudyTime { get; set; } + } + + public class PatientStudyBeforeConfirmView : PatientStudyView + { + public Guid PatientId { get; set; } + public string CalledAE { get; set; } = string.Empty; + + public string CallingAE { get; set; } = string.Empty; + } + + public class PatientStudyView + { + + public string PatientName { get; set; } + public string PatientIdStr { get; set; } + + public string PatientSex { get; set; } + + public string PatientAge { get; set; } = string.Empty; + public string PatientBirthDate { get; set; } = string.Empty; + + + public Guid StudyId { get; set; } + public string Modalities { get; set; } + + public string ModalityForEdit { get; set; } + + public DateTime? StudyTime { get; set; } + + public string Description { get; set; } = string.Empty; + public int SeriesCount { get; set; } = 0; + public int InstanceCount { get; set; } = 0; + + + + public Guid SubjectId { get; set; } + + public string SubjectCode { get; set; } + + + public string SubjectShortName { get; set; } = String.Empty; + + public int? SubjectAge { get; set; } + public string SubjectSex { get; set; } = string.Empty; + + public Guid? SubjectVisitId { get; set; } + public string? VisitName { get; set; } + + public decimal? VisitNum { get; set; } + + public DateTime? VisitEarliestStudyTime { get; set; } + + public DateTime? VisitLatestStudyTime { get; set; } + + public DateTime? SubmitTime { get; set; } + + public SubmitStateEnum? SubmitState { get; set; } + + } + + public class PatientBasicInfo + { + public Guid PatientId { get; set; } + + public string PatientIdStr { get; set; } + + public string PatientSex { get; set; } + + public string PatientName { get; set; } + + public string PatientAge { get; set; } = string.Empty; + public string PatientBirthDate { get; set; } = string.Empty; + } + + + public class PatientSubjectVisitView + { + public Guid TrialId { get; set; } + public Guid SubjectId { get; set; } + + public string SubjectCode { get; set; } + + public List PatientList { get; set; } + + public string SubjectShortName { get; set; } = String.Empty; + + public int? SubjectAge { get; set; } + public string SubjectSex { get; set; } = string.Empty; + + public Guid SubjectVisitId { get; set; } + public string VisitName { get; set; } + + public decimal VisitNum { get; set; } + + public DateTime? VisitEarliestStudyTime { get; set; } + + public DateTime? VisitLatestStudyTime { get; set; } + + public DateTime? SubmitTime { get; set; } + + public SubmitStateEnum SubmitState { get; set; } + + public string VisitImageZipPath { get; set; } + public PackState PackState { get; set; } + } + + public class SubjectPatientStudyView : PatientStudySimpleView + { + public Guid SubjectId { get; set; } + + public string SubjectCode { get; set; } + + public List PatientList { get; set; } + + public string SubjectShortName { get; set; } = String.Empty; + + public int? SubjectAge { get; set; } + public string SubjectSex { get; set; } = string.Empty; + + } + + public class SubjectVisitStudyQuery : PageInput + { + [NotDefault] + public Guid SujectVisitId { get; set; } + + public SubmitStateEnum? SubmitState { get; set; } + } + + public class PatientStudyOtherQuery : PageInput + { + [NotDefault] + public Guid TrialId { get; set; } + public List PatientIdList { get; set; } + + [NotDefault] + public Guid SujectVisitId { get; set; } + + public DateTime? EarliestStudyTime { get; set; } + + public DateTime? LatestStudyTime { get; set; } + + public string? PatientIdStr { get; set; } + + } + + public class VisitImageDownloadQuery : PageInput + { + [NotDefault] + public Guid TrialId { get; set; } + + public string? IP { get; set; } + + public string? SubjectCode { get; set; } + + public string? VisitName { get; set; } + + public string? Name { get; set; } + + public DateTime? BeginDownloadTime { get; set; } + + public DateTime? EndDownloadTime { get; set; } + + public UserTypeEnum? UserTypeEnum { get; set; } + } + + public class VisitImageDownloadView + { + public string SubjectCode { get; set; } + + public string VisitName { get; set; } + + public DateTime DownloadTime { get; set; } + + public string DownloadUserName { get; set; } + + public string DownLoadUserFullName { get; set; } + + public string IP { get; set; } + + public long VisitImageZipSize { get; set; } + + //文件大小 + public int VisitImageFileCount { get; set; } + + public string VisitImageZipPath { get; set; } = string.Empty; + + public int? StudyCount { get; set; } + + public UserTypeEnum UserTypeEnum { get; set; } + + } + + + public class SCPImageUploadQuery : PageInput + { + public string TrialSiteKeyInfo { get; set; } + + public string? CallingAE { get; set; } + + public string? CalledAE { get; set; } + + public string? CallingAEIP { get; set; } + + public DateTime? StartTime { get; set; } + + public DateTime? EndTime { get; set; } + } + + public class SCPImageUploadView + { + public string CallingAE { get; set; } = string.Empty; + + public string CalledAE { get; set; } = string.Empty; + + public string CallingAEIP { get; set; } = string.Empty; + + public DateTime StartTime { get; set; } + + public DateTime EndTime { get; set; } + + public int FileCount { get; set; } + + public long FileSize { get; set; } + + public int StudyCount { get; set; } + + + public Guid TrialId { get; set; } + public Guid TrialSiteId { get; set; } + + + public string TrialSiteCode { get; set; } + + public string TrialSiteName { get; set; } + + public string TrialSiteAliasName { get; set; } + + } + public class VisitPatientStudyView : PatientStudySelectDto + { + + public string VisitName { get; set; } + } + + public class UnbindStudyView : VisitPatientStudyView + { + public Guid TrialId { get; set; } + + public Guid SubjectId { get; set; } + public string SubjectCode { get; set; } + + public string SubjectShortName { get; set; } = String.Empty; + + public int? SubjectAge { get; set; } + public string SubjectSex { get; set; } = string.Empty; + + public DateTime? SubjectBirthDate { get; set; } + + } + + public class VisitPatientStudyFilterQuery : PageInput + { + [NotDefault] + public Guid SubjectId { get; set; } + + [NotDefault] + public Guid SubjectVisitId { get; set; } + + public DateTime? EarliestStudyTime { get; set; } + + public DateTime? LatestStudyTime { get; set; } + public string? Modalities { get; set; } + } + public class VisitPatientStudyFilterView + { + public Guid SCPStudyId { get; set; } + + public Guid PatientId { get; set; } + + public DateTime? StudyTime { get; set; } + public string Modalities { get; set; } = string.Empty; + + public string Description { get; set; } = string.Empty; + public int SeriesCount { get; set; } = 0; + public int InstanceCount { get; set; } = 0; + + public string CalledAE { get; set; } = string.Empty; + + public string CallingAE { get; set; } = string.Empty; + } + + public class PatientStudySimpleView: VisitPatientStudyFilterView + { + + + + public Guid? SubjectVisitId { get; set; } + public string? VisitName { get; set; } + + public string? BlindName { get; set; } = string.Empty; + + + //public SubjectVisitInfo SubejectVisit { get; set; } + + } + + public class SubjectVisitInfo + { + public Guid Id { get; set; } + + public string VisitName { get; set; } + + public decimal VisitNum { get; set; } + public string BlindName { get; set; } = string.Empty; + } + + + public class PatientSeriesDTO + { + + public bool IsDicom { get; set; } = true; + + + public Guid Id { get; set; } + public Guid StudyId { get; set; } + public string StudyInstanceUid { get; set; } = String.Empty; + public string SeriesInstanceUid { get; set; } = String.Empty; + public int SeriesNumber { get; set; } + public DateTime? SeriesTime { get; set; } + public string Modality { get; set; } = String.Empty; + public string Description { get; set; } = String.Empty; + public int InstanceCount { get; set; } + public string SliceThickness { get; set; } = String.Empty; + + public DateTime CreateTime { get; set; } + public DateTime UpdateTime { get; set; } + + public bool IsDeleted { get; set; } + public bool IsReading { get; set; } = true; + + public List InstanceList { get; set; } = new List(); + + public List InstancePathList { get; set; } = new List(); + + public string ImageResizePath { get; set; } + } + + public class PatientStudySelectDto + { + public Guid SCPStudyId { get; set; } + + public Guid PatientId { get; set; } + public string PatientIdStr { get; set; } = string.Empty; + public string PatientName { get; set; } = string.Empty; + public string PatientAge { get; set; } = string.Empty; + public string PatientSex { get; set; } = string.Empty; + public string PatientBirthDate { get; set; } = string.Empty; + + public DateTime? StudyTime { get; set; } + public string Modalities { get; set; } = string.Empty; + + public string Description { get; set; } = string.Empty; + public int SeriesCount { get; set; } = 0; + public int InstanceCount { get; set; } = 0; + + public string CalledAE { get; set; } = string.Empty; + + public string CallingAE { get; set; } + public Guid? SubjectVisitId { get; set; } + + public SubmitStateEnum? SubmitState { get; set; } + + //public Guid? SubjectVisitId { get; set; } + + //public string CallingAE { get; set; } = string.Empty; + + //public string BodyPartExamined { get; set; } = string.Empty; + + //public string BodyPartForEdit { get; set; } = string.Empty; + + //public string ModalityForEdit { get; set; } = string.Empty; + + + + } + +} diff --git a/IRaCIS.Core.Application/Service/Visit/PatientService.cs b/IRaCIS.Core.Application/Service/Visit/PatientService.cs new file mode 100644 index 000000000..1f0468c0e --- /dev/null +++ b/IRaCIS.Core.Application/Service/Visit/PatientService.cs @@ -0,0 +1,374 @@ +using IRaCIS.Application.Interfaces; +using IRaCIS.Application.Contracts; +using IRaCIS.Core.Application.Filter; +using IRaCIS.Core.Domain.Share; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using IRaCIS.Core.Application.Auth; +using MassTransit; +using Panda.DynamicWebApi.Attributes; +using DocumentFormat.OpenXml.Spreadsheet; +using AutoMapper.EntityFrameworkCore; +using IRaCIS.Core.Domain.Models; +using IRaCIS.Core.Application.Service.Reading.Dto; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Json; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using SharpCompress.Common; +using System.Reactive.Subjects; +using Subject = IRaCIS.Core.Domain.Models.Subject; +using IRaCIS.Core.Application.ViewModel; +using Medallion.Threading; +using IRaCIS.Core.Infrastructure; +using EasyCaching.Core; +using Pipelines.Sockets.Unofficial.Arenas; +using IRaCIS.Core.Application.Contracts; +using MailKit.Search; +using DocumentFormat.OpenXml.Office2010.Excel; +using IRaCIS.Core.Application.Contracts.Dicom.DTO; +using IRaCIS.Core.Application.Helper; +using NPOI.SS.Formula.Functions; +using System.Linq; +using System.Linq.Dynamic.Core; +using System.Text; +using DocumentFormat.OpenXml.EMMA; +using Azure; +using System.IO.Compression; +using static IRaCIS.Core.Domain.Share.StaticData; +using FellowOakDicom; +using DocumentFormat.OpenXml.Office2010.Drawing; +using EasyCaching.Core.DistributedLock; +using IDistributedLockProvider = Medallion.Threading.IDistributedLockProvider; +using DocumentFormat.OpenXml.InkML; + +namespace IRaCIS.Application.Services +{ + [ApiExplorerSettings(GroupName = "Trial")] + public class PatientService : BaseService + { + + private readonly IRepository _trialRepository; + private readonly IRepository _patientRepository; + private readonly IRepository _scpStudyRepository; + private readonly IRepository _subjectRepository; + private readonly IRepository _subjectVisitRepository; + private readonly IDistributedLockProvider _distributedLockProvider; + + public PatientService(IRepository studyRepository, IRepository trialRepository, IRepository patientRepository, IRepository subjectRepository, IRepository subjectVisitRepository, IDistributedLockProvider distributedLockProvider) + { + _scpStudyRepository = studyRepository; + _trialRepository = trialRepository; + _patientRepository = patientRepository; + _subjectRepository = subjectRepository; + _subjectVisitRepository = subjectVisitRepository; + _distributedLockProvider = distributedLockProvider; + } + + + /// + /// scp 影像推送记录表 + /// + /// + /// + [HttpPost] + public async Task>> GetSCPImageUploadList(SCPImageUploadQuery inQuery) + { + var query = _repository.Where() + .WhereIf(!string.IsNullOrWhiteSpace(inQuery.CalledAE), t => t.CalledAE.Contains(inQuery.CalledAE)) + .WhereIf(!string.IsNullOrWhiteSpace(inQuery.CallingAEIP), t => t.CallingAEIP.Contains(inQuery.CallingAEIP)) + .WhereIf(!string.IsNullOrWhiteSpace(inQuery.CallingAE), t => t.CallingAE.Contains(inQuery.CallingAE)) + .WhereIf(inQuery.StartTime != null, t => t.StartTime >= inQuery.StartTime) + .WhereIf(inQuery.EndTime != null, t => t.EndTime <= inQuery.EndTime) + .WhereIf(!string.IsNullOrWhiteSpace(inQuery.TrialSiteKeyInfo), t => t.TrialSite.TrialSiteCode.Contains(inQuery.TrialSiteKeyInfo) + || t.TrialSite.TrialSiteAliasName.Contains(inQuery.TrialSiteKeyInfo) || t.TrialSite.TrialSiteName.Contains(inQuery.TrialSiteKeyInfo)) + .ProjectTo(_mapper.ConfigurationProvider); + + + var pageList = await query.ToPagedListAsync(inQuery.PageIndex, inQuery.PageSize, inQuery.SortField == string.Empty ? nameof(SCPImageUploadView.CallingAE) : inQuery.SortField, inQuery.Asc); + + + return ResponseOutput.Ok(pageList); + } + + + #region 患者检查管理 + + /// + ///影像检查列表-患者为维度组织 + /// + /// + /// + [HttpPost] + public async Task>> GetPatientList(PatientTrialQuery inQuery) + { + + + #region new ok + var query = _patientRepository + .WhereIf(!string.IsNullOrWhiteSpace(inQuery.PatientIdStr), t => t.PatientIdStr.Contains(inQuery.PatientIdStr)) + .WhereIf(!string.IsNullOrWhiteSpace(inQuery.PatientName), t => t.PatientName.Contains(inQuery.PatientName)) + .WhereIf(!string.IsNullOrWhiteSpace(inQuery.SubejctCode), t => t.Subject.Code.Contains(inQuery.SubejctCode)) + .WhereIf(!string.IsNullOrWhiteSpace(inQuery.TrialSiteKeyInfo), t => t.TrialSite.TrialSiteCode.Contains(inQuery.TrialSiteKeyInfo) + || t.TrialSite.TrialSiteAliasName.Contains(inQuery.TrialSiteKeyInfo)|| t.TrialSite.TrialSiteName.Contains(inQuery.TrialSiteKeyInfo)) + .WhereIf(!string.IsNullOrWhiteSpace(inQuery.CallingAE), t => t.SCPStudyList.Any(t => t.CallingAE == inQuery.CallingAE)) + .WhereIf(!string.IsNullOrWhiteSpace(inQuery.CalledAE), t => t.SCPStudyList.Any(t => t.CalledAE == inQuery.CalledAE)) + .WhereIf(inQuery.BeginPushTime != null, t => t.LatestPushTime >= inQuery.BeginPushTime) + .WhereIf(inQuery.EndPushTime != null, t => t.LatestPushTime <= inQuery.EndPushTime); + + //foreach (var calledAE in inQuery.CalledAEList) + //{ + // query = query.Where(t => t.SCPStudyList.Select(c => c.CalledAE).Contains(calledAE)); + //} + + + var resultQuery = from patient in query + + select new PatientSubjectView() + { + PatientId = patient.Id, + PatientBirthDate = patient.PatientBirthDate, + CreateTime = patient.CreateTime, + CalledAEList = patient.SCPStudyList.Select(t => t.CalledAE).Distinct().ToList(), + CallingAEList = patient.SCPStudyList.Select(t => t.CallingAE).Distinct().ToList(), + CreateUserId = patient.CreateUserId, + UpdateTime = patient.UpdateTime, + UpdateUserId = patient.UpdateUserId, + + EarliestStudyTime = patient.EarliestStudyTime, + LatestStudyTime = patient.LatestStudyTime, + LatestPushTime = patient.LatestPushTime, + PatientAge = patient.PatientAge, + PatientName = patient.PatientName, + PatientIdStr = patient.PatientIdStr, + PatientSex = patient.PatientSex, + + StudyCount = patient.SCPStudyList.Count(), + + TrialId=patient.TrialId, + SubejctId=patient.SubjectId, + SubjectCode=patient.Subject.Code, + TrialSiteAliasName=patient.TrialSite.TrialSiteAliasName, + TrialSiteCode=patient.TrialSite.TrialSiteCode, + TrialSiteName=patient.TrialSite.TrialSiteName + + }; + + + + + var pageList = await resultQuery.ToPagedListAsync(inQuery.PageIndex, inQuery.PageSize, inQuery.SortField == string.Empty ? nameof(PatientQueryView.PatientIdStr) : inQuery.SortField, inQuery.Asc); + #endregion + + + return ResponseOutput.Ok(pageList); + } + + + + + + /// + /// 影像检查列表-> 获取患者的检查列表 + /// + /// + /// + [HttpPost] + public async Task> GetPatientStudyList(PatientStudyInfoQuery inQuery) + { + var query = from scpStudy in _scpStudyRepository.Where(t => t.PatientId == inQuery.PatientId) + .WhereIf(inQuery.EarliestStudyTime != null, t => t.StudyTime >= inQuery.EarliestStudyTime) + .WhereIf(inQuery.LatestStudyTime != null, t => t.StudyTime <= inQuery.LatestStudyTime) + .WhereIf(!string.IsNullOrWhiteSpace(inQuery.Modalities), t => t.Modalities.Contains(inQuery.Modalities)) + select new PatientStudySimpleView() + { + Description = scpStudy.Description, + CalledAE = scpStudy.CalledAE, + CallingAE = scpStudy.CallingAE, + InstanceCount = scpStudy.InstanceCount, + Modalities = scpStudy.Modalities, + PatientId = scpStudy.PatientId, + SCPStudyId = scpStudy.Id, + SeriesCount = scpStudy.SeriesCount, + StudyTime = scpStudy.StudyTime, + + SubjectVisitId= scpStudy.SubjectVisitId, + VisitName=scpStudy.SubjectVisit.VisitName, + BlindName=scpStudy.SubjectVisit.BlindName + }; + + + //var sortField = string.IsNullOrWhiteSpace(inQuery.SortField) ? nameof(PatientStudySimpleView.StudyTime) : inQuery.SortField; + //var orderQuery = inQuery.Asc ? query.OrderBy(sortField) : query.OrderBy(sortField + " desc"); + + //var list = await orderQuery.ToListAsync(); + + var pageList = await query.ToPagedListAsync(inQuery.PageIndex, inQuery.PageSize, inQuery.SortField == string.Empty ? nameof(PatientStudySimpleView.StudyTime) : inQuery.SortField, inQuery.Asc); + + return pageList; + } + + + public async Task> GetDicomCalledAEList() + { + var list = await _scpStudyRepository.Select(t => t.CalledAE).Distinct().ToListAsync(); + + return list; + } + + public async Task> GetDicomCallingAEList() + { + var list = await _scpStudyRepository.Select(t => t.CallingAE).Distinct().ToListAsync(); + + return list; + } + + /// + /// 影像访视上传 检查列表 + /// + /// + /// + [HttpPost] + public async Task> GetVisitPatientStudyFilterList(VisitPatientStudyFilterQuery inQuery) + { + + var trialSiteId=_subjectRepository.Where(t=>t.Id==inQuery.SubjectId).Select(t=>t.TrialSiteId).FirstOrDefault(); + + var query = from scpStudy in _scpStudyRepository + //未绑定的患者,或者自己已绑定但是未绑定访视的 + .Where(t => t.Patient.SubjectId == null|| (t.Patient.SubjectId == inQuery.SubjectId && t.SubjectVisitId==null)) + //中心 + .Where(t=>t.TrialSiteId==trialSiteId) + .WhereIf(inQuery.EarliestStudyTime != null, t => t.StudyTime >= inQuery.EarliestStudyTime) + .WhereIf(inQuery.LatestStudyTime != null, t => t.StudyTime <= inQuery.LatestStudyTime) + .WhereIf(!string.IsNullOrWhiteSpace(inQuery.Modalities), t => t.Modalities.Contains(inQuery.Modalities)) + select new VisitPatientStudyFilterView() + { + Description = scpStudy.Description, + CalledAE = scpStudy.CalledAE, + CallingAE = scpStudy.CallingAE, + InstanceCount = scpStudy.InstanceCount, + Modalities = scpStudy.Modalities, + PatientId = scpStudy.PatientId, + SCPStudyId = scpStudy.Id, + SeriesCount = scpStudy.SeriesCount, + StudyTime = scpStudy.StudyTime, + }; + + + var pageList = await query.ToPagedListAsync(inQuery.PageIndex, inQuery.PageSize, inQuery.SortField == string.Empty ? nameof(PatientStudySimpleView.StudyTime) : inQuery.SortField, inQuery.Asc); + + return pageList; + } + + + + /// + /// 提交 患者检查和访视的绑定 + /// + /// + /// + [HttpPost] + [UnitOfWork] + [TypeFilter(typeof(TrialResourceFilter), Arguments = new object[] { "AfterStopCannNotOpt" })] + public async Task SubmitVisitStudyBinding(SubmitVisitStudyBindingCommand inCommand) + { + + var subjectId = inCommand.SubjectId; + var subjectVisitId=inCommand.SubjectVisitId; + var trialId = inCommand.TrialId; + + + + var @lock = _distributedLockProvider.CreateLock($"StudyCode"); + + using (await @lock.AcquireAsync()) + { + var dbStudyCodeIntMax = _repository.Where(s => s.TrialId == inCommand.TrialId).Select(t => t.Code).DefaultIfEmpty().Max(); + + int currentNextCodeInt = dbStudyCodeIntMax + 1; + + foreach (var scpStudyId in inCommand.SCPStudyIdList) + { + + var find = _scpStudyRepository.Where(t => t.Id == scpStudyId).Include(t => t.SeriesList).Include(t => t.InstanceList).FirstOrDefault(); + + if (find != null) + { + + var newStuty = _mapper.Map(find); + + await _repository.AddAsync(newStuty); + + newStuty.SeqId = Guid.Empty; + newStuty.Code = currentNextCodeInt; + newStuty.StudyCode = AppSettings.GetCodeStr(currentNextCodeInt, nameof(DicomStudy)); + newStuty.IsFromPACS = true; + newStuty.TrialId = trialId; + newStuty.SubjectId = subjectId; + newStuty.SubjectVisitId = subjectVisitId; + + var newSeriesList = _mapper.Map>(find.SeriesList); + + foreach (var series in newSeriesList) + { + + series.SeqId = Guid.Empty; + series.TrialId = trialId; + series.SubjectId = subjectId; + series.SubjectVisitId = subjectVisitId; + } + + await _repository.AddRangeAsync(newSeriesList); + + var newInstanceList = _mapper.Map>(find.InstanceList); + + foreach (var instance in newInstanceList) + { + + + instance.SeqId = Guid.Empty; + instance.TrialId = trialId; + instance.SubjectId = subjectId; + instance.SubjectVisitId = subjectVisitId; + + } + await _repository.AddRangeAsync(newInstanceList); + } + + currentNextCodeInt++; + + } + + + } + + + + await _repository.SaveChangesAsync(); + + return ResponseOutput.Ok(); + } + + #endregion + + + + + + + + + + + + + + + + } + + + +} diff --git a/IRaCIS.Core.Application/Service/Visit/_MapConfig.cs b/IRaCIS.Core.Application/Service/Visit/_MapConfig.cs index 35a01056d..717c22d8e 100644 --- a/IRaCIS.Core.Application/Service/Visit/_MapConfig.cs +++ b/IRaCIS.Core.Application/Service/Visit/_MapConfig.cs @@ -112,6 +112,14 @@ namespace IRaCIS.Core.Application.Service .ForMember(d => d.InstanceInfoList, u => u.MapFrom(s => s.InstanceList)); CreateMap(); + + + CreateMap() + .ForMember(d => d.TrialSiteCode, u => u.MapFrom(s => s.TrialSite.TrialSiteCode)) + .ForMember(d => d.TrialSiteAliasName, u => u.MapFrom(s => s.TrialSite.TrialSiteAliasName)) + .ForMember(d => d.TrialSiteName, u => u.MapFrom(s => s.TrialSite.TrialSiteName)) +; + } } diff --git a/IRaCIS.Core.Application/_MediatR/Handlers/ConsistencyVerificationHandler.cs b/IRaCIS.Core.Application/_MediatR/Handlers/ConsistencyVerificationHandler.cs index b2cb55016..975656660 100644 --- a/IRaCIS.Core.Application/_MediatR/Handlers/ConsistencyVerificationHandler.cs +++ b/IRaCIS.Core.Application/_MediatR/Handlers/ConsistencyVerificationHandler.cs @@ -213,7 +213,7 @@ namespace IRaCIS.Core.Application.MediatR.Handlers else { //"Problems are as follows: - dialogMsg.AppendLine($"
{_localizer["ConsistencyVerification_Prob"]}
"); + dialogMsg.AppendLine($"
{_localizer["ConsistencyVerification_Prob"]}
"); num = 0; foreach (var item in dbExceptExcel) diff --git a/IRaCIS.Core.Domain/Common/ExploreRecommend.cs b/IRaCIS.Core.Domain/Common/ExploreRecommend.cs new file mode 100644 index 000000000..b58cd06b5 --- /dev/null +++ b/IRaCIS.Core.Domain/Common/ExploreRecommend.cs @@ -0,0 +1,55 @@ + +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2024-07-02 09:26:43 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +using System; +using IRaCIS.Core.Domain.Share; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +namespace IRaCIS.Core.Domain.Models +{ + /// + ///ExploreRecommend + /// + [Table("ExploreRecommend")] + public class ExploreRecommend : Entity, IAuditUpdate, IAuditAdd,ISoftDelete + { + public string ExploreType { get; set; } = string.Empty; + + + public string Version { get; set; }=string.Empty; + + + public string Title { get; set; } = string.Empty; + + + public DateTime CreateTime { get; set; } + + + public Guid CreateUserId { get; set; } + + + public Guid UpdateUserId { get; set; } + + + public DateTime UpdateTime { get; set; } + + + public bool IsDeleted { get; set; } + + + public string DownloadUrl { get; set; } = string.Empty; + + + public string Path { get; set; } = string.Empty; + + public string FileName { get; set; } = string.Empty; + + public DateTime? DeletedTime { get; set; } + + public Guid? DeleteUserId { get; set; } + + } + +} diff --git a/IRaCIS.Core.Domain/Image/DicomStudy.cs b/IRaCIS.Core.Domain/Image/DicomStudy.cs index e5193846b..b0d6526b5 100644 --- a/IRaCIS.Core.Domain/Image/DicomStudy.cs +++ b/IRaCIS.Core.Domain/Image/DicomStudy.cs @@ -99,5 +99,8 @@ namespace IRaCIS.Core.Domain.Models public Guid? DeleteUserId { get; set; } + public bool IsFromPACS { get; set; } + + } } diff --git a/IRaCIS.Core.Domain/Image/SCPImageUpload.cs b/IRaCIS.Core.Domain/Image/SCPImageUpload.cs new file mode 100644 index 000000000..10e6b4ef0 --- /dev/null +++ b/IRaCIS.Core.Domain/Image/SCPImageUpload.cs @@ -0,0 +1,74 @@ + +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2024-05-24 14:31:45 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +using System; +using IRaCIS.Core.Domain.Share; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Collections.Generic; +namespace IRaCIS.Core.Domain.Models +{ + /// + ///SCPImageUpload + /// + [Table("SCPImageUpload")] + public class SCPImageUpload : Entity, IAuditAdd + { + + + + [Required] + public DateTime CreateTime { get; set; } + + + [Required] + public Guid CreateUserId { get; set; } + + + [Required] + public string CallingAE { get; set; }=string.Empty; + + [Required] + public string CalledAE { get; set; } = string.Empty; + + + [Required] + public string CallingAEIP { get; set; } = string.Empty; + + + [Required] + public DateTime StartTime { get; set; } + + [Required] + public DateTime EndTime { get; set; } + + + [Required] + public int FileCount { get; set; } + + + [Required] + public long FileSize { get; set; } + + + public int StudyCount { get; set; } + + + + + public Guid TrialId { get; set; } + public Guid TrialSiteId { get; set; } + + [JsonIgnore] + + public Trial Trial { get; set; } + + [JsonIgnore] + public TrialSite TrialSite { get; set; } + + } + + +} diff --git a/IRaCIS.Core.Domain/Image/SCPInstance.cs b/IRaCIS.Core.Domain/Image/SCPInstance.cs new file mode 100644 index 000000000..f0eb643f6 --- /dev/null +++ b/IRaCIS.Core.Domain/Image/SCPInstance.cs @@ -0,0 +1,53 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("SCPInstance")] + public class SCPInstance : Entity, IAuditAdd, IAuditUpdate + { + [JsonIgnore] + [ForeignKey("SeriesId")] + public SCPSeries SCPSeries { get; set; } + + [JsonIgnore] + [ForeignKey("StudyId")] + public SCPStudy SCPStudy { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + + public Guid SeqId { get; set; } + public Guid StudyId { get; set; } + public Guid SeriesId { get; set; } + public string StudyInstanceUid { get; set; } + public string SeriesInstanceUid { get; set; } + public string SopInstanceUid { get; set; } + public int InstanceNumber { get; set; } + public DateTime? InstanceTime { get; set; } + public bool CPIStatus { get; set; } + public int ImageRows { get; set; } + public int ImageColumns { get; set; } + public int SliceLocation { get; set; } + + + public string SliceThickness { get; set; } + public int NumberOfFrames { get; set; } + public string PixelSpacing { get; set; } + + public string ImagerPixelSpacing { get; set; } + public string FrameOfReferenceUID { get; set; } + public string WindowCenter { get; set; } + public string WindowWidth { get; set; } + + public bool Anonymize { get; set; } + public string Path { get; set; } = string.Empty; + + public Guid CreateUserId { get; set; } + public DateTime CreateTime { get; set; } = DateTime.Now; + public Guid UpdateUserId { get; set; } + public DateTime UpdateTime { get; set; } = DateTime.Now; + + + } +} diff --git a/IRaCIS.Core.Domain/Image/SCPPatient.cs b/IRaCIS.Core.Domain/Image/SCPPatient.cs new file mode 100644 index 000000000..0c75de6d4 --- /dev/null +++ b/IRaCIS.Core.Domain/Image/SCPPatient.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("SCPPatient")] + public class SCPPatient : Entity, IAuditUpdate, IAuditAdd + { + public List SCPStudyList { get; set; } + + public string PatientIdStr { get; set; } = string.Empty; + public string PatientName { get; set; } = string.Empty; + public string PatientAge { get; set; } = string.Empty; + public string PatientSex { get; set; } = string.Empty; + public string PatientBirthDate { get; set; } = string.Empty; + + public Guid UpdateUserId { get; set; } + public DateTime UpdateTime { get; set; } = DateTime.Now; + public Guid CreateUserId { get; set; } + public DateTime CreateTime { get; set; } = DateTime.Now; + + + public DateTime? EarliestStudyTime { get; set; } + + public DateTime? LatestStudyTime { get; set; } + + public DateTime LatestPushTime { get; set; } + + + public Guid? SubjectId { get; set; } + public Guid TrialId { get; set; } + public Guid TrialSiteId { get; set; } + + + [JsonIgnore] + public Subject Subject { get; set; } + [JsonIgnore] + public Trial Trial { get; set; } + [JsonIgnore] + public TrialSite TrialSite { get; set; } + + } +} diff --git a/IRaCIS.Core.Domain/Image/SCPSeries.cs b/IRaCIS.Core.Domain/Image/SCPSeries.cs new file mode 100644 index 000000000..5c2213c20 --- /dev/null +++ b/IRaCIS.Core.Domain/Image/SCPSeries.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("SCPSeries")] + public class SCPSeries : Entity, IAuditAdd, IAuditUpdate, ISoftDelete + { + [JsonIgnore] + [ForeignKey("StudyId")] + public SCPStudy SCPStudy { get; set; } + + [JsonIgnore] + public List SCPInstanceList { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public Guid SeqId { get; set; } + public Guid StudyId { get; set; } + public string StudyInstanceUid { get; set; } + public string SeriesInstanceUid { get; set; } + public int SeriesNumber { get; set; } + public DateTime? SeriesTime { get; set; } + public string Modality { get; set; } + public string Description { get; set; } + public int InstanceCount { get; set; } + public string SliceThickness { get; set; } + + public string ImagePositionPatient { get; set; } + public string ImageOrientationPatient { get; set; } + public string BodyPartExamined { get; set; } + public string SequenceName { get; set; } + public string ProtocolName { get; set; } + public string ImagerPixelSpacing { get; set; } + + public string AcquisitionTime { get; set; } = string.Empty; + public string AcquisitionNumber { get; set; } = string.Empty; + public string TriggerTime { get; set; } = string.Empty; + + public string BodyPartForEdit { get; set; } = string.Empty; + + public Guid CreateUserId { get; set; } + public DateTime CreateTime { get; set; } = DateTime.Now; + public Guid UpdateUserId { get; set; } + public DateTime UpdateTime { get; set; } = DateTime.Now; + + public DateTime? DeletedTime { get; set; } + + public Guid? DeleteUserId { get; set; } + public bool IsDeleted {get;set;} + public bool IsReading { get; set; } = true; + + public string ImageResizePath { get; set; }=string.Empty; + + } +} diff --git a/IRaCIS.Core.Domain/Image/SCPStudy.cs b/IRaCIS.Core.Domain/Image/SCPStudy.cs new file mode 100644 index 000000000..4af3686cd --- /dev/null +++ b/IRaCIS.Core.Domain/Image/SCPStudy.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace IRaCIS.Core.Domain.Models +{ + [Table("SCPStudy")] + public class SCPStudy : Entity, IAuditUpdate, IAuditAdd, ISoftDelete + { + + + + + + //0 未知 1 单重 2 双重 + public bool IsDoubleReview { get; set; } + + [JsonIgnore] + public List InstanceList { get; set; } + + [JsonIgnore] + public List SeriesList { get; set; } + + + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public Guid SeqId { get; set; } + + [ForeignKey("PatientId")] + [JsonIgnore] + public SCPPatient Patient { get; set; } + public Guid PatientId { get; set; } + + + + + public string StudyInstanceUid { get; set; } = string.Empty; + public DateTime? StudyTime { get; set; } + public string Modalities { get; set; } = string.Empty; + + public string Description { get; set; } = string.Empty; + public int SeriesCount { get; set; } = 0; + public int InstanceCount { get; set; } = 0; + + + public string InstitutionName { get; set; } = string.Empty; + public string PatientIdStr { get; set; } = string.Empty; + public string PatientName { get; set; } = string.Empty; + public string PatientAge { get; set; } = string.Empty; + public string PatientSex { get; set; } = string.Empty; + + public string StudyId { get; set; } = string.Empty; + public string AccessionNumber { get; set; } = string.Empty; + public string PatientBirthDate { get; set; } = string.Empty; + public string AcquisitionTime { get; set; } = string.Empty; + public string AcquisitionNumber { get; set; } = string.Empty; + public string TriggerTime { get; set; } = string.Empty; + + public string BodyPartExamined { get; set; } = string.Empty; + + public string BodyPartForEdit { get; set; } = string.Empty; + + public string ModalityForEdit { get; set; } = string.Empty; + + + + public Guid UpdateUserId { get; set; } + public DateTime UpdateTime { get; set; } = DateTime.Now; + public Guid CreateUserId { get; set; } + public DateTime CreateTime { get; set; } = DateTime.Now; + + + //软删除 + public bool IsDeleted { get; set; } + + public DateTime? DeletedTime { get; set; } + + public Guid? DeleteUserId { get; set; } + + public string CallingAE { get; set; } = string.Empty; + + public string CalledAE { get; set; } = string.Empty; + + public bool IsUploadFinished { get; set; } + + + public Guid TrialId { get; set; } + public Guid TrialSiteId { get; set; } + + public Guid? SubjectVisitId { get; set; } + + [JsonIgnore] + public SubjectVisit SubjectVisit { get; set; } + + [JsonIgnore] + + public Trial Trial { get; set; } + + [JsonIgnore] + public TrialSite TrialSite { get; set; } + + } +} diff --git a/IRaCIS.Core.Domain/Image/TrialDicomAE.cs b/IRaCIS.Core.Domain/Image/TrialDicomAE.cs new file mode 100644 index 000000000..0d91a9c77 --- /dev/null +++ b/IRaCIS.Core.Domain/Image/TrialDicomAE.cs @@ -0,0 +1,52 @@ + +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2024-03-22 15:44:11 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +using System; +using IRaCIS.Core.Domain.Share; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +namespace IRaCIS.Core.Domain.Models +{ + /// + ///DicomAE + /// + [Table("TrialDicomAE")] + public class TrialDicomAE : Entity, IAuditUpdate, IAuditAdd + { + + public Guid TrialId { get; set; } + + public DateTime CreateTime { get; set; } + + + public Guid CreateUserId { get; set; } + + + public Guid UpdateUserId { get; set; } + + + public DateTime UpdateTime { get; set; } + + public string CalledAE { get; set; } = string.Empty; + + public string IP { get; set; } + + + public int Port { get; set; } + + + public string Modality { get; set; } = string.Empty; + + + public string Description { get; set; }=string.Empty; + + + public DateTime? LatestTestTime { get; set; } + + public bool IsTestOK { get; set; } + + } + +} diff --git a/IRaCIS.Core.Domain/Trial/Trial.cs b/IRaCIS.Core.Domain/Trial/Trial.cs index e63ca4315..d00d43d55 100644 --- a/IRaCIS.Core.Domain/Trial/Trial.cs +++ b/IRaCIS.Core.Domain/Trial/Trial.cs @@ -401,7 +401,9 @@ namespace IRaCIS.Core.Domain.Models #endregion + public bool IsPACSConnect { get; set; } + public bool IsTrialPACSConfirmed { get; set; } ///// ///// 图像是否有标注 diff --git a/IRaCIS.Core.Domain/TrialSiteUser/TrialSite.cs b/IRaCIS.Core.Domain/TrialSiteUser/TrialSite.cs index 63da78434..8812dd62b 100644 --- a/IRaCIS.Core.Domain/TrialSiteUser/TrialSite.cs +++ b/IRaCIS.Core.Domain/TrialSiteUser/TrialSite.cs @@ -55,7 +55,9 @@ namespace IRaCIS.Core.Domain.Models [JsonIgnore] public List SubjectList { get; set; } + [JsonIgnore] + public List TrialSiteDicomAEList { get; set; } } } \ No newline at end of file diff --git a/IRaCIS.Core.Domain/TrialSiteUser/TrialSiteDicomAE.cs b/IRaCIS.Core.Domain/TrialSiteUser/TrialSiteDicomAE.cs new file mode 100644 index 000000000..1ef1446f5 --- /dev/null +++ b/IRaCIS.Core.Domain/TrialSiteUser/TrialSiteDicomAE.cs @@ -0,0 +1,63 @@ + +//-------------------------------------------------------------------- +// 此代码由T4模板自动生成 byzhouhang 20210918 +// 生成时间 2024-07-02 16:53:49 +// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。 +using System; +using IRaCIS.Core.Domain.Share; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +namespace IRaCIS.Core.Domain.Models +{ + /// + ///TrialSiteDicomAE + /// + [Table("TrialSiteDicomAE")] + public class TrialSiteDicomAE : Entity, IAuditUpdate, IAuditAdd + { + + + + public DateTime? DeletedTime { get; set; } + + + public Guid TrialId { get; set; } + + + public Guid UpdateUserId { get; set; } + + + public Guid? DeleteUserId { get; set; } + + + public DateTime CreateTime { get; set; } + + + public Guid CreateUserId { get; set; } + + public bool IsDeleted { get; set; } + + public DateTime UpdateTime { get; set; } + + + public Guid TrialSiteId { get; set; } + + + public string CallingAE { get; set; } + + + public string IP { get; set; } + + + public string Port { get; set; } + + + public string Description { get; set; } + + + + public TrialSite TrialSite { get; set; } + + } + +} diff --git a/IRaCIS.Core.Infra.EFCore/Context/IRaCISDBContext.cs b/IRaCIS.Core.Infra.EFCore/Context/IRaCISDBContext.cs index 913ae7a94..f4dcffdd9 100644 --- a/IRaCIS.Core.Infra.EFCore/Context/IRaCISDBContext.cs +++ b/IRaCIS.Core.Infra.EFCore/Context/IRaCISDBContext.cs @@ -481,6 +481,21 @@ namespace IRaCIS.Core.Infra.EFCore public virtual DbSet TrialBodyPart { get; set; } + public virtual DbSet ExploreRecommend { get; set; } + + public virtual DbSet SCPPatient { get; set; } + public virtual DbSet SCPStudy { get; set; } + public virtual DbSet SCPSeries { get; set; } + public virtual DbSet SCPInstance { get; set; } + public virtual DbSet TrialDicomAE { get; set; } + + + public virtual DbSet TrialSiteDicomAE { get; set; } + + + public virtual DbSet SCPImageUpload { get; set; } + + public override async Task SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken()) { // 采用触发器的方式 设置 CreateUserId CreateTime UpdateTime UpdateUserId 稽查实体里面没有这四个字段的值 因为先后顺序的原因 diff --git a/IRaCIS.Core.Infrastructure/IRaCIS.Core.Infrastructure.csproj b/IRaCIS.Core.Infrastructure/IRaCIS.Core.Infrastructure.csproj index f2772d9ce..f2b268e49 100644 --- a/IRaCIS.Core.Infrastructure/IRaCIS.Core.Infrastructure.csproj +++ b/IRaCIS.Core.Infrastructure/IRaCIS.Core.Infrastructure.csproj @@ -9,13 +9,13 @@ - - + + - - + + - + diff --git a/IRaCIS.Core.Test/DbHelper.ttinclude b/IRaCIS.Core.Test/DbHelper.ttinclude index 93d6bd2d6..8790fcb50 100644 --- a/IRaCIS.Core.Test/DbHelper.ttinclude +++ b/IRaCIS.Core.Test/DbHelper.ttinclude @@ -4,7 +4,7 @@ public static readonly string ConnectionString = "Server=106.14.89.110,1435;Database=Test_IRC;User ID=sa;Password=xc@123456;TrustServerCertificate=true"; public static readonly string DbDatabase = "Test_IRC"; //表名称用字符串,拼接 - public static readonly string TableName = "TrialBodyPart"; + public static readonly string TableName = "TrialSiteDicomAE"; //具体文件里面 例如service 可以配置是否分页 } #> diff --git a/IRaCIS.Core.Test/TT_Template/IRaCIS .Core.ServiceAsync.tt b/IRaCIS.Core.Test/TT_Template/IRaCIS .Core.ServiceAsync.tt index fa5c367b7..5a1998a61 100644 --- a/IRaCIS.Core.Test/TT_Template/IRaCIS .Core.ServiceAsync.tt +++ b/IRaCIS.Core.Test/TT_Template/IRaCIS .Core.ServiceAsync.tt @@ -65,6 +65,7 @@ namespace IRaCIS.Core.Application.Service } <# if(isPage){#> + [HttpPost] public async TaskView>> Get<#=tableName#>List(<#=tableName#>Query inQuery) { @@ -74,13 +75,13 @@ namespace IRaCIS.Core.Application.Service .ProjectTo<<#=tableName#>View>(_mapper.ConfigurationProvider); var pageList= await <#=char.ToLower(tableName[0]) + tableName.Substring(1)#>Queryable - .ToPagedListAsync(inQuery.PageIndex, inQuery.PageSize, string.IsNullOrWhiteSpace(inQuery.SortField) ? "Id" : inQuery.SortField, + .ToPagedListAsync(inQuery.PageIndex, inQuery.PageSize, string.IsNullOrWhiteSpace(inQuery.SortField) ? nameof(<#=tableName#>View.Id) : inQuery.SortField, inQuery.Asc); return pageList; } <# } else {#> - + [HttpPost] public async TaskView>> Get<#=tableName#>List(<#=tableName#>Query inQuery) {