增加 MassTransit 移除 MediatR
parent
4c9a8aadab
commit
d34eca2cb8
|
@ -10,8 +10,7 @@ using IRaCIS.Core.Application.Contracts.Dicom;
|
|||
using IRaCIS.Core.Application.Contracts.Dicom.DTO;
|
||||
using IRaCIS.Core.Application.Filter;
|
||||
using IRaCIS.Core.Application.Helper;
|
||||
using IRaCIS.Core.Application.MediatR.CommandAndQueries;
|
||||
using IRaCIS.Core.Application.MediatR.Handlers;
|
||||
using IRaCIS.Core.Application.MassTransit.Command;
|
||||
using IRaCIS.Core.Application.Service;
|
||||
using IRaCIS.Core.Application.Service.ImageAndDoc;
|
||||
using IRaCIS.Core.Application.Service.Reading.Dto;
|
||||
|
@ -21,7 +20,7 @@ using IRaCIS.Core.Infra.EFCore;
|
|||
using IRaCIS.Core.Infrastructure;
|
||||
using IRaCIS.Core.Infrastructure.Extention;
|
||||
using MassTransit;
|
||||
using MediatR;
|
||||
using MassTransit.Mediator;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors.Infrastructure;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
|
@ -232,9 +231,12 @@ namespace IRaCIS.Core.API.Controllers
|
|||
{
|
||||
public IMapper _mapper { get; set; }
|
||||
public IUserInfo _userInfo { get; set; }
|
||||
|
||||
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
|
||||
|
||||
public IStringLocalizer _localizer { get; set; }
|
||||
|
||||
|
||||
|
@ -266,11 +268,6 @@ namespace IRaCIS.Core.API.Controllers
|
|||
[FromServices] IRepository<StudyMonitor> _studyMonitorRepository)
|
||||
{
|
||||
|
||||
if (_provider.Get<List<SystemAnonymization>>(StaticData.Anonymize.Anonymize_AddFixedFiled).Value == null)
|
||||
{
|
||||
await _mediator.Send(new AnonymizeCacheRequest());
|
||||
}
|
||||
|
||||
var savedInfo = _studyService.GetSaveToDicomInfo(preArchiveStudyCommand.SubjectVisitId);
|
||||
|
||||
var studyMonitor = new StudyMonitor()
|
||||
|
@ -743,11 +740,16 @@ namespace IRaCIS.Core.API.Controllers
|
|||
//---请保证上传数据符合模板文件中的样式,且存在有效数据。
|
||||
return ResponseOutput.NotOk(_localizer["UploadDownLoad_InvalidData"]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
await _mediator.Send(new ConsistencyVerificationRequest() { ETCList = etcCheckList, TrialId = trialId });
|
||||
|
||||
// 适合获取结果的
|
||||
//var client = _mediator.CreateRequestClient<ConsistenCheckCommand>();
|
||||
//await client.GetResponse<ConsistenCheckResult>(new ConsistenCheckCommand() { ETCList = etcCheckList, TrialId = trialId });
|
||||
|
||||
//不获取结果,不用定义返回类型
|
||||
await _mediator.Send(new ConsistenCheckCommand() { ETCList = etcCheckList, TrialId = trialId });
|
||||
|
||||
return ResponseOutput.Ok();
|
||||
|
||||
|
|
|
@ -4,8 +4,6 @@ using Microsoft.AspNetCore.Hosting;
|
|||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Serilog;
|
||||
using MediatR;
|
||||
using IRaCIS.Core.Application.MediatR.Handlers;
|
||||
using System.Threading.Tasks;
|
||||
using MassTransit;
|
||||
using MassTransit.NewIdProviders;
|
||||
|
@ -33,6 +31,8 @@ using Microsoft.AspNetCore.Http;
|
|||
using IRaCIS.Core.Infrastructure.Extention;
|
||||
using Newtonsoft.Json;
|
||||
using Microsoft.AspNetCore.Diagnostics;
|
||||
using IRaCIS.Core.Application.MassTransit.Command;
|
||||
using IRaCIS.Core.Application.MassTransit.Consumer;
|
||||
|
||||
|
||||
#region 获取环境变量
|
||||
|
@ -138,7 +138,14 @@ builder.Services.AddSwaggerSetup();
|
|||
builder.Services.AddJWTAuthSetup(_configuration);
|
||||
|
||||
// MediatR 进程内消息 事件解耦 从程序集中 注册命令和handler对应关系
|
||||
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblyContaining<ConsistencyVerificationHandler>());
|
||||
//builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblyContaining<ConsistencyVerificationHandler>());
|
||||
|
||||
//masstransit组件 也支持MediatR 中介者模式,但是支持分布式,考虑后续,所以在次替代MediatR
|
||||
builder.Services.AddMediator(cfg =>
|
||||
{
|
||||
cfg.AddConsumer<ConsistencyCheckConsumer>();
|
||||
});
|
||||
|
||||
// EasyCaching 缓存
|
||||
builder.Services.AddEasyCachingSetup(_configuration);
|
||||
|
||||
|
|
|
@ -10,7 +10,6 @@ using System.Linq;
|
|||
using System.Reflection;
|
||||
using IRaCIS.Core.Domain.Models;
|
||||
using IRaCIS.Core.Domain.Share;
|
||||
using MediatR;
|
||||
using IRaCIS.Application.Services;
|
||||
using IRaCIS.Application.Interfaces;
|
||||
using AutoMapper;
|
||||
|
|
|
@ -74,7 +74,6 @@
|
|||
<PackageReference Include="fo-dicom.Codecs" Version="5.14.4" />
|
||||
<PackageReference Include="IP2Region.Net" Version="2.0.2" />
|
||||
<PackageReference Include="MailKit" Version="4.2.0" />
|
||||
<PackageReference Include="MediatR" Version="12.2.0" />
|
||||
<PackageReference Include="MimeKit" Version="4.2.0" />
|
||||
<PackageReference Include="MiniExcel" Version="1.32.0" />
|
||||
<PackageReference Include="Minio" Version="6.0.3" />
|
||||
|
@ -86,7 +85,7 @@
|
|||
<PackageReference Include="Panda.DynamicWebApi" Version="1.2.2" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.5" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="8.0.2" />
|
||||
<PackageReference Include="WinSCP" Version="6.3.3" />
|
||||
<PackageReference Include="MassTransit.AspNetCore" Version="7.3.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -9911,6 +9911,11 @@
|
|||
TrialSiteDicomAEService
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:IRaCIS.Core.Application.MassTransit.Consumer.ConsistencyCheckConsumer.#ctor(IRaCIS.Core.Infra.EFCore.IRepository{IRaCIS.Core.Domain.Models.DicomStudy},IRaCIS.Core.Domain.Share.IUserInfo,IRaCIS.Core.Infra.EFCore.IRepository{IRaCIS.Core.Domain.Models.Subject},IRaCIS.Core.Infra.EFCore.IRepository{IRaCIS.Core.Domain.Models.SubjectVisit},IRaCIS.Core.Infra.EFCore.IRepository{IRaCIS.Core.Domain.Models.TrialSite},IRaCIS.Core.Infra.EFCore.IRepository{IRaCIS.Core.Domain.Models.NoneDicomStudy},AutoMapper.IMapper,Microsoft.Extensions.Localization.IStringLocalizer)">
|
||||
<summary>
|
||||
构造函数注入
|
||||
</summary>
|
||||
</member>
|
||||
<member name="T:IRaCIS.Core.Application.ViewModel.TaskAllocationRuleView">
|
||||
<summary> TaskAllocationRuleView 列表视图模型 </summary>
|
||||
</member>
|
||||
|
@ -13308,21 +13313,6 @@
|
|||
维护 IsFrontTaskNeedSignButNotSign 字段 另外附加评估结果
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:IRaCIS.Core.Application.MediatR.Handlers.AnonymizeCacheHandler.#ctor(IRaCIS.Core.Infra.EFCore.IRepository,EasyCaching.Core.IEasyCachingProvider)">
|
||||
<summary>
|
||||
构造函数注入
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:IRaCIS.Core.Application.MediatR.Handlers.ConsistencyVerificationHandler.#ctor(IRaCIS.Core.Infra.EFCore.IRepository{IRaCIS.Core.Domain.Models.DicomStudy},IRaCIS.Core.Domain.Share.IUserInfo,IRaCIS.Core.Infra.EFCore.IRepository{IRaCIS.Core.Domain.Models.Subject},IRaCIS.Core.Infra.EFCore.IRepository{IRaCIS.Core.Domain.Models.SubjectVisit},IRaCIS.Core.Infra.EFCore.IRepository{IRaCIS.Core.Domain.Models.TrialSite},IRaCIS.Core.Infra.EFCore.IRepository{IRaCIS.Core.Domain.Models.NoneDicomStudy},AutoMapper.IMapper,Microsoft.Extensions.Localization.IStringLocalizer)">
|
||||
<summary>
|
||||
构造函数注入
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:IRaCIS.Core.Application.MediatR.Handlers.TrialStateCacheHandler.#ctor(IRaCIS.Core.Infra.EFCore.IRepository{IRaCIS.Core.Domain.Models.Trial},EasyCaching.Core.IEasyCachingProvider)">
|
||||
<summary>
|
||||
构造函数注入
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:IRaCIS.Application.Services.BackGroundJob.IRaCISCHangfireJob.MemoryCacheTrialStatusAsync">
|
||||
<summary>
|
||||
缓存项目状态
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
using MiniExcelLibs.Attributes;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace IRaCIS.Core.Application.MassTransit.Command
|
||||
{
|
||||
public record ConsistenCheckCommand
|
||||
{
|
||||
public List<CheckViewModel> ETCList { get; set; } = new List<CheckViewModel>();
|
||||
|
||||
public Guid TrialId { get; set; }
|
||||
}
|
||||
|
||||
public record ConsistenCheckResult
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public class CheckDBModel : CheckViewModel
|
||||
{
|
||||
|
||||
public Guid SubjectVisitId { get; set; }
|
||||
|
||||
public Guid StudyId { get; set; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
//[ExcelImporter(/*ImportResultFilter = typeof(ImportResultFilteTest),*/ IsLabelingError = true)]
|
||||
|
||||
public class CheckViewModel
|
||||
{
|
||||
//[Required(ErrorMessage = "中心编号不能为空")]
|
||||
//[ImporterHeader(Name = "Site ID", AutoTrim = true)]
|
||||
[ExcelColumnName("Site ID")]
|
||||
public string SiteCode { get; set; } = string.Empty;
|
||||
|
||||
|
||||
//[Required(ErrorMessage = "受试者筛选号不能为空")]
|
||||
//[ImporterHeader(Name = "Subject ID", AutoTrim = true)]
|
||||
[ExcelColumnName("Subject ID")]
|
||||
public string SubjectCode { get; set; } = string.Empty;
|
||||
|
||||
//[Required(ErrorMessage = "访视名称不能为空")]
|
||||
//[ImporterHeader(Name = "Visit Name", AutoTrim = true)]
|
||||
[ExcelColumnName("Visit Name")]
|
||||
public string VisitName { get; set; } = string.Empty;
|
||||
|
||||
|
||||
|
||||
//[Required(ErrorMessage = "检查日期不能为空")]
|
||||
[CanConvertToTime(ErrorMessage = "Does not conform to Study Date format")]
|
||||
|
||||
//[ImporterHeader(Name = "Study Date", AutoTrim = true)]
|
||||
[ExcelColumnName("Study Date")]
|
||||
public string StudyDate { get; set; } = string.Empty;
|
||||
|
||||
|
||||
|
||||
//[Required(ErrorMessage = "Modality不能为空")]
|
||||
//[ImporterHeader(Name = "Modality", AutoTrim = true)]
|
||||
[ExcelColumnName("Modality")]
|
||||
public string Modality { get; set; } = string.Empty;
|
||||
|
||||
|
||||
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj == null) return false;
|
||||
|
||||
var checkModel = obj as CheckViewModel;
|
||||
|
||||
if (checkModel is not null)
|
||||
{
|
||||
return SiteCode == checkModel.SiteCode && SubjectCode == checkModel.SubjectCode && VisitName == checkModel.VisitName && StudyDate == checkModel.StudyDate && Modality == checkModel.Modality;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return (SiteCode + SubjectCode + VisitName + StudyDate + Modality).GetHashCode();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -1,16 +1,22 @@
|
|||
using AutoMapper;
|
||||
using IRaCIS.Core.Application.MediatR.CommandAndQueries;
|
||||
using IRaCIS.Core.Application.Contracts.DTO;
|
||||
using IRaCIS.Core.Application.MassTransit.Command;
|
||||
using IRaCIS.Core.Domain.Share;
|
||||
using MassTransit;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using Newtonsoft.Json;
|
||||
using MediatR;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace IRaCIS.Core.Application.MediatR.Handlers
|
||||
namespace IRaCIS.Core.Application.MassTransit.Consumer
|
||||
{
|
||||
public class ConsistencyVerificationHandler : IRequestHandler<ConsistencyVerificationRequest, string>
|
||||
public class ConsistencyCheckConsumer : IConsumer<ConsistenCheckCommand>
|
||||
{
|
||||
|
||||
private readonly IRepository<DicomStudy> _studyRepository;
|
||||
private readonly IUserInfo _userInfo;
|
||||
private readonly IRepository<Subject> _subjectRepository;
|
||||
|
@ -24,7 +30,7 @@ namespace IRaCIS.Core.Application.MediatR.Handlers
|
|||
/// 构造函数注入
|
||||
/// </summary>
|
||||
|
||||
public ConsistencyVerificationHandler(IRepository<DicomStudy> studyRepository, IUserInfo userInfo,
|
||||
public ConsistencyCheckConsumer(IRepository<DicomStudy> studyRepository, IUserInfo userInfo,
|
||||
IRepository<Subject> subjectRepository, IRepository<SubjectVisit> subjectVisitRepository,
|
||||
IRepository<TrialSite> trialSiteRepository, IRepository<NoneDicomStudy> noneDicomStudyRepository,
|
||||
IMapper mapper, IStringLocalizer localizer)
|
||||
|
@ -39,19 +45,22 @@ namespace IRaCIS.Core.Application.MediatR.Handlers
|
|||
_localizer = localizer;
|
||||
}
|
||||
|
||||
async Task<string> IRequestHandler<ConsistencyVerificationRequest, string>.Handle(ConsistencyVerificationRequest request, CancellationToken cancellationToken)
|
||||
|
||||
|
||||
public async Task Consume(ConsumeContext<ConsistenCheckCommand> context)
|
||||
{
|
||||
var trialId = request.TrialId;
|
||||
|
||||
var trialId = context.Message.TrialId;
|
||||
|
||||
//处理Excel大小写
|
||||
request.ETCList.ForEach(t => { t.Modality = t.Modality.ToUpper().Trim(); t.StudyDate = Convert.ToDateTime(t.StudyDate).ToString("yyyy-MM-dd"); t.SiteCode = t.SiteCode.ToUpper().Trim(); t.VisitName = t.VisitName.ToUpper().Trim(); t.SubjectCode = t.SubjectCode.ToUpper().Trim(); });
|
||||
var etcList = request.ETCList;
|
||||
context.Message.ETCList.ForEach(t => { t.Modality = t.Modality.ToUpper().Trim(); t.StudyDate = Convert.ToDateTime(t.StudyDate).ToString("yyyy-MM-dd"); t.SiteCode = t.SiteCode.ToUpper().Trim(); t.VisitName = t.VisitName.ToUpper().Trim(); t.SubjectCode = t.SubjectCode.ToUpper().Trim(); });
|
||||
var etcList = context.Message.ETCList;
|
||||
|
||||
//Expression<Func<SubjectVisit, bool>> subjectVisitLambda2 = x => x.TrialId == request.TrialId;
|
||||
|
||||
//subjectVisitLambda2= subjectVisitLambda2.And(x => x.CheckState == CheckStateEnum.ToCheck && x.AuditState == AuditStateEnum.QCPassed || (x.CheckState == CheckStateEnum.CVIng && x.AuditState == AuditStateEnum.QCPassed));
|
||||
|
||||
Expression<Func<SubjectVisit, bool>> subjectVisitLambda = x => x.TrialId == request.TrialId &&
|
||||
Expression<Func<SubjectVisit, bool>> subjectVisitLambda = x => x.TrialId == trialId &&
|
||||
(x.CheckState == CheckStateEnum.ToCheck && x.AuditState == AuditStateEnum.QCPassed || (x.CheckState == CheckStateEnum.CVIng && x.AuditState == AuditStateEnum.QCPassed));
|
||||
|
||||
var dicomQuery = from sv in _subjectVisitRepository.Where(subjectVisitLambda)
|
||||
|
@ -190,7 +199,7 @@ namespace IRaCIS.Core.Application.MediatR.Handlers
|
|||
dbSV.CheckUserId = _userInfo.Id;
|
||||
dbSV.CheckPassedTime = DateTime.Now;
|
||||
dbSV.CheckChallengeState = CheckChanllengeTypeEnum.Closed;
|
||||
|
||||
|
||||
//---核对EDC数据,完全一致
|
||||
dbSV.CheckResult = _localizer["ConsistencyVerification_EDCB"];
|
||||
//---自动核查通过
|
||||
|
@ -274,11 +283,13 @@ namespace IRaCIS.Core.Application.MediatR.Handlers
|
|||
|
||||
}
|
||||
await _subjectVisitRepository.SaveChangesAsync();
|
||||
return "OK";
|
||||
|
||||
//await context.RespondAsync<ConsistenCheckResult>(new
|
||||
//{
|
||||
|
||||
//});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -10,7 +10,6 @@ using IRaCIS.Core.Infrastructure;
|
|||
using DocumentFormat.OpenXml.Presentation;
|
||||
using IRaCIS.Core.Domain.Models;
|
||||
using IRaCIS.Core.Application.Filter;
|
||||
using IRaCIS.Core.Application.MediatR.Handlers;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Newtonsoft.Json;
|
||||
using System.Threading;
|
||||
|
|
|
@ -5,10 +5,8 @@
|
|||
//--------------------------------------------------------------------
|
||||
|
||||
using IRaCIS.Core.Application.Interfaces;
|
||||
using IRaCIS.Core.Application.MediatR.Handlers;
|
||||
using IRaCIS.Core.Application.ViewModel;
|
||||
using IRaCIS.Core.Infra.EFCore;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
namespace IRaCIS.Core.Application.Service
|
||||
{
|
||||
|
@ -18,12 +16,10 @@ namespace IRaCIS.Core.Application.Service
|
|||
[ApiExplorerSettings(GroupName = "Image")]
|
||||
public class SystemAnonymizationService : BaseService, ISystemAnonymizationService
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
private readonly IRepository<SystemAnonymization> systemAnonymizationRepository;
|
||||
|
||||
public SystemAnonymizationService(IMediator mediator, IRepository<SystemAnonymization> systemAnonymizationRepository)
|
||||
public SystemAnonymizationService( IRepository<SystemAnonymization> systemAnonymizationRepository)
|
||||
{
|
||||
_mediator = mediator;
|
||||
this.systemAnonymizationRepository = systemAnonymizationRepository;
|
||||
}
|
||||
|
||||
|
@ -47,7 +43,6 @@ namespace IRaCIS.Core.Application.Service
|
|||
|
||||
var entity = await _repository.InsertOrUpdateAsync<SystemAnonymization, SystemAnonymizationAddOrEdit>(addOrEditSystemAnonymization, true);
|
||||
|
||||
await _mediator.Send(new AnonymizeCacheRequest());
|
||||
|
||||
return ResponseOutput.Ok(entity.Id.ToString());
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
using IRaCIS.Application.Contracts;
|
||||
using IRaCIS.Core.Application.MediatR.CommandAndQueries;
|
||||
using IRaCIS.Core.Domain.Share;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
|
@ -489,7 +488,14 @@ namespace IRaCIS.Core.Application.Contracts.DTO
|
|||
}
|
||||
}
|
||||
|
||||
public class ParamInfoDto
|
||||
{
|
||||
public string Modality { get; set; }
|
||||
|
||||
public string StudyDate { get; set; }
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
public class QCChanllengeDialogDTO : CheckChanllengeDialogDTO
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
using IRaCIS.Core.Application.Contracts.DTO;
|
||||
using IRaCIS.Core.Application.Service.Inspection.DTO;
|
||||
using IRaCIS.Core.Domain.Share;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
@ -40,6 +39,6 @@ namespace IRaCIS.Core.Application.Image.QA
|
|||
Task<IResponseOutput> VerifyCanQCPassedOrFailed(Guid subjectVisitId);
|
||||
|
||||
|
||||
Task<IResponseOutput> ForwardSVDicomImage(Guid[] subjectVisitIdList);
|
||||
//Task<IResponseOutput> ForwardSVDicomImage(Guid[] subjectVisitIdList);
|
||||
}
|
||||
}
|
|
@ -2,11 +2,9 @@
|
|||
using IRaCIS.Core.Application.Contracts.DTO;
|
||||
using IRaCIS.Core.Application.Filter;
|
||||
using IRaCIS.Core.Domain.Share;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Data;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using WinSCP;
|
||||
using Newtonsoft.Json;
|
||||
using IRaCIS.Core.Infrastructure;
|
||||
using IRaCIS.Core.Application.Service.Inspection.DTO;
|
||||
|
@ -2109,109 +2107,113 @@ namespace IRaCIS.Core.Application.Image.QA
|
|||
}
|
||||
|
||||
|
||||
#region 转发影像 暂时不用 废弃
|
||||
//[HttpPost("{trialId:guid}")]
|
||||
////[Authorize(Policy = IRaCISPolicy.PM_APM)]
|
||||
//[TypeFilter(typeof(TrialResourceFilter), Arguments = new object[] { "AfterStopCannNotOpt" })]
|
||||
//public async Task<IResponseOutput> ForwardSVDicomImage(Guid[] subjectVisitIdList)
|
||||
//{
|
||||
|
||||
[HttpPost("{trialId:guid}")]
|
||||
//[Authorize(Policy = IRaCISPolicy.PM_APM)]
|
||||
[TypeFilter(typeof(TrialResourceFilter), Arguments = new object[] { "AfterStopCannNotOpt" })]
|
||||
public async Task<IResponseOutput> ForwardSVDicomImage(Guid[] subjectVisitIdList)
|
||||
{
|
||||
|
||||
bool isSuccess = false;
|
||||
// bool isSuccess = false;
|
||||
|
||||
|
||||
foreach (var subjectVisitId in subjectVisitIdList)
|
||||
{
|
||||
// foreach (var subjectVisitId in subjectVisitIdList)
|
||||
// {
|
||||
|
||||
|
||||
var info = (await _subjectVisitRepository.Where(t => t.Id == subjectVisitId).ProjectTo<DicomTrialSiteSubjectInfo>(_mapper.ConfigurationProvider).FirstOrDefaultAsync()).IfNullThrowException();
|
||||
// var info = (await _subjectVisitRepository.Where(t => t.Id == subjectVisitId).ProjectTo<DicomTrialSiteSubjectInfo>(_mapper.ConfigurationProvider).FirstOrDefaultAsync()).IfNullThrowException();
|
||||
|
||||
|
||||
var targetPath = "/IMPORT-IMAGES/" + info.TrialCode + "_" + info.SubjectCode + "_" + info.VisitName;
|
||||
// var targetPath = "/IMPORT-IMAGES/" + info.TrialCode + "_" + info.SubjectCode + "_" + info.VisitName;
|
||||
|
||||
var path = FileStoreHelper.GetSubjectVisitDicomFolderPhysicalPath(_hostEnvironment, info.TrialId, info.TrialSiteId, info.SubjectId, info.SubjectVisitId);
|
||||
// var path = FileStoreHelper.GetSubjectVisitDicomFolderPhysicalPath(_hostEnvironment, info.TrialId, info.TrialSiteId, info.SubjectId, info.SubjectVisitId);
|
||||
|
||||
try
|
||||
{
|
||||
// 主机及端口信息后面可以改到 配置文件
|
||||
SessionOptions sessionOptions = new SessionOptions
|
||||
{
|
||||
Protocol = Protocol.Sftp,
|
||||
PortNumber = 8022,
|
||||
HostName = "CS-690-sftp.mint-imaging.com",
|
||||
UserName = "zdong",
|
||||
Password = "Everest@2021",
|
||||
SshHostKeyFingerprint = @"ecdsa-sha2-nistp384 384 59gkjJ5lMwv3jsB8Wz2B35tBAIor5pSd8PcJYtoamPo="
|
||||
};
|
||||
// try
|
||||
// {
|
||||
// // 主机及端口信息后面可以改到 配置文件
|
||||
// SessionOptions sessionOptions = new SessionOptions
|
||||
// {
|
||||
// Protocol = Protocol.Sftp,
|
||||
// PortNumber = 8022,
|
||||
// HostName = "CS-690-sftp.mint-imaging.com",
|
||||
// UserName = "zdong",
|
||||
// Password = "Everest@2021",
|
||||
// SshHostKeyFingerprint = @"ecdsa-sha2-nistp384 384 59gkjJ5lMwv3jsB8Wz2B35tBAIor5pSd8PcJYtoamPo="
|
||||
// };
|
||||
|
||||
using (Session session = new Session())
|
||||
{
|
||||
var studyFolders = (new DirectoryInfo(path)).GetDirectories();
|
||||
// using (Session session = new Session())
|
||||
// {
|
||||
// var studyFolders = (new DirectoryInfo(path)).GetDirectories();
|
||||
|
||||
session.Open(sessionOptions);
|
||||
// session.Open(sessionOptions);
|
||||
|
||||
if (!session.FileExists(targetPath))
|
||||
{
|
||||
session.CreateDirectory(targetPath);
|
||||
}
|
||||
// if (!session.FileExists(targetPath))
|
||||
// {
|
||||
// session.CreateDirectory(targetPath);
|
||||
// }
|
||||
|
||||
|
||||
foreach (var studyFolder in studyFolders)
|
||||
{
|
||||
var targetFolder = Path.Combine(targetPath, studyFolder.Name);
|
||||
// foreach (var studyFolder in studyFolders)
|
||||
// {
|
||||
// var targetFolder = Path.Combine(targetPath, studyFolder.Name);
|
||||
|
||||
if (!session.FileExists(targetFolder))
|
||||
{
|
||||
session.CreateDirectory(targetFolder);
|
||||
}
|
||||
// if (!session.FileExists(targetFolder))
|
||||
// {
|
||||
// session.CreateDirectory(targetFolder);
|
||||
// }
|
||||
|
||||
foreach (var file in studyFolder.GetFiles())
|
||||
{
|
||||
if (file.Extension.Contains("dcm", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
string remoteFilePath =
|
||||
RemotePath.TranslateLocalPathToRemote(file.FullName, studyFolder.FullName, targetFolder);
|
||||
// foreach (var file in studyFolder.GetFiles())
|
||||
// {
|
||||
// if (file.Extension.Contains("dcm", StringComparison.OrdinalIgnoreCase))
|
||||
// {
|
||||
// string remoteFilePath =
|
||||
// RemotePath.TranslateLocalPathToRemote(file.FullName, studyFolder.FullName, targetFolder);
|
||||
|
||||
var result = session.PutFiles(file.FullName, remoteFilePath, false);
|
||||
// var result = session.PutFiles(file.FullName, remoteFilePath, false);
|
||||
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
await _subjectVisitRepository.BatchUpdateNoTrackingAsync(t => t.Id == subjectVisitId,
|
||||
u => new SubjectVisit() { ForwardState = ForwardStateEnum.ForwardFailed });
|
||||
// if (!result.IsSuccess)
|
||||
// {
|
||||
// await _subjectVisitRepository.BatchUpdateNoTrackingAsync(t => t.Id == subjectVisitId,
|
||||
// u => new SubjectVisit() { ForwardState = ForwardStateEnum.ForwardFailed });
|
||||
|
||||
//---转发影像失败。
|
||||
return ResponseOutput.NotOk(_localizer["QCOperation_ForwardingFailed"] + result.Failures.ToString() + result.ToJson());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// //---转发影像失败。
|
||||
// return ResponseOutput.NotOk(_localizer["QCOperation_ForwardingFailed"] + result.Failures.ToString() + result.ToJson());
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
}
|
||||
await _subjectVisitRepository.UpdatePartialFromQueryAsync(t => t.Id == subjectVisitId,
|
||||
u => new SubjectVisit() { ForwardState = ForwardStateEnum.Forwarded, ForwardUserId = _userInfo.Id, ForwardTime = DateTime.Now });
|
||||
// }
|
||||
// await _subjectVisitRepository.UpdatePartialFromQueryAsync(t => t.Id == subjectVisitId,
|
||||
// u => new SubjectVisit() { ForwardState = ForwardStateEnum.Forwarded, ForwardUserId = _userInfo.Id, ForwardTime = DateTime.Now });
|
||||
|
||||
isSuccess = true;
|
||||
// isSuccess = true;
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// }
|
||||
// catch (Exception e)
|
||||
// {
|
||||
|
||||
await _subjectVisitRepository.UpdatePartialFromQueryAsync(t => t.Id == subjectVisitId,
|
||||
u => new SubjectVisit() { ForwardState = ForwardStateEnum.ForwardFailed });
|
||||
// await _subjectVisitRepository.UpdatePartialFromQueryAsync(t => t.Id == subjectVisitId,
|
||||
// u => new SubjectVisit() { ForwardState = ForwardStateEnum.ForwardFailed });
|
||||
|
||||
// --转发影像失败
|
||||
return ResponseOutput.NotOk(_localizer["QCOperation_ForwardingFailed"] + e.Message);
|
||||
}
|
||||
// // --转发影像失败
|
||||
// return ResponseOutput.NotOk(_localizer["QCOperation_ForwardingFailed"] + e.Message);
|
||||
// }
|
||||
|
||||
|
||||
}
|
||||
// }
|
||||
|
||||
|
||||
|
||||
await _subjectVisitRepository.SaveChangesAsync();
|
||||
// await _subjectVisitRepository.SaveChangesAsync();
|
||||
|
||||
// //---转发影像失败。
|
||||
// return isSuccess ? ResponseOutput.Ok() : ResponseOutput.NotOk(_localizer["QCOperation_ForwardingFailed"]);
|
||||
//}
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
//---转发影像失败。
|
||||
return isSuccess ? ResponseOutput.Ok() : ResponseOutput.NotOk(_localizer["QCOperation_ForwardingFailed"]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ using AutoMapper.EquivalencyExpression;
|
|||
using IRaCIS.Application.Contracts;
|
||||
using IRaCIS.Core.Application.Contracts;
|
||||
using IRaCIS.Core.Application.Contracts.DTO;
|
||||
using IRaCIS.Core.Application.MediatR.CommandAndQueries;
|
||||
using IRaCIS.Core.Application.MassTransit.Command;
|
||||
using IRaCIS.Core.Domain.Share;
|
||||
using static IRaCIS.Core.Application.Contracts.SubjectProgressDto;
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using IRaCIS.Core.Infrastructure.Extention;
|
||||
using MiniExcelLibs.Attributes;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace IRaCIS.Application.Contracts
|
||||
|
@ -103,5 +104,53 @@ namespace IRaCIS.Application.Contracts
|
|||
public int InconsistentCount { get; set; }
|
||||
}
|
||||
|
||||
|
||||
public class VisitPlanInfluenceSubjectVisitDTO
|
||||
{
|
||||
[ExcelColumn(Ignore = true)]
|
||||
public Guid StudyId { get; set; }
|
||||
|
||||
[ExcelColumn(Ignore = true)]
|
||||
public Guid TrialId { get; set; }
|
||||
|
||||
[ExcelColumn(Ignore = true)]
|
||||
public Guid SubjectVisitId { get; set; }
|
||||
|
||||
[ExcelColumnName("中心编号")]
|
||||
public string TrialSiteCode { get; set; } = string.Empty;
|
||||
|
||||
[ExcelColumnName("受试者")]
|
||||
public string SubjectCode { get; set; } = string.Empty;
|
||||
|
||||
[ExcelColumnName("访视名称")]
|
||||
public string VisitName { get; set; } = string.Empty;
|
||||
|
||||
[ExcelColumn(Name = "检查时间", Format = "yyyy-MM-dd HH:mm:ss")]
|
||||
public DateTime StudyTime { get; set; }
|
||||
|
||||
[ExcelColumnName("检查技术")]
|
||||
public string Modality { get; set; } = string.Empty;
|
||||
|
||||
[ExcelColumn(Ignore = true)]
|
||||
public bool IsDicomStudy { get; set; }
|
||||
|
||||
|
||||
[ExcelColumnName("影像类型")]
|
||||
public string ImageType => IsDicomStudy ? "Dicom" : "非Dicom";
|
||||
|
||||
[ExcelColumnName("历史窗口")]
|
||||
public string HistoryWindow { get; set; } = string.Empty;
|
||||
|
||||
|
||||
[ExcelColumnName("之前超窗调整后没超窗")]
|
||||
|
||||
public bool IsOverWindowNowNotOverWindow { get; set; }
|
||||
|
||||
[ExcelColumnName("目前窗口")]
|
||||
public string NowWindow { get; set; } = string.Empty;
|
||||
|
||||
[ExcelColumn(Ignore = true)]
|
||||
public DateTime CreateTime { get; set; }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ using IRaCIS.Application.Contracts;
|
|||
using IRaCIS.Core.Application.Filter;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using IRaCIS.Core.Domain.Share;
|
||||
using IRaCIS.Core.Application.MediatR.CommandAndQueries;
|
||||
using IRaCIS.Core.Infrastructure;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using IRaCIS.Core.Application.Auth;
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
using IRaCIS.Application.Contracts;
|
||||
using IRaCIS.Core.Application.Contracts;
|
||||
using IRaCIS.Core.Application.Contracts.Dicom.DTO;
|
||||
using IRaCIS.Core.Application.MediatR.CommandAndQueries;
|
||||
using IRaCIS.Core.Domain.Share;
|
||||
|
||||
namespace IRaCIS.Core.Application.Service
|
||||
|
|
|
@ -1,176 +0,0 @@
|
|||
using IRaCIS.Core.Domain.Share;
|
||||
using MediatR;
|
||||
using MiniExcelLibs.Attributes;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
|
||||
namespace IRaCIS.Core.Application.MediatR.CommandAndQueries
|
||||
{
|
||||
public class ConsistencyVerificationRequest : IRequest<string>
|
||||
{
|
||||
public List<CheckViewModel> ETCList { get; set; } = new List<CheckViewModel>();
|
||||
|
||||
public Guid TrialId { get; set; }
|
||||
}
|
||||
|
||||
public class CheckDBModel : CheckViewModel
|
||||
{
|
||||
|
||||
public Guid SubjectVisitId { get; set; }
|
||||
|
||||
public Guid StudyId { get; set; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
//public class ImportResultFilteTest : IImportResultFilter
|
||||
//{
|
||||
|
||||
|
||||
// public ImportResult<T> Filter<T>(ImportResult<T> importResult) where T : class, new()
|
||||
// {
|
||||
// if (typeof(T).IsAssignableFrom(typeof(CheckViewModel)))
|
||||
// {
|
||||
// var data = (List<CheckViewModel>)importResult.Data;
|
||||
|
||||
// var dt = DateTime.Now ;
|
||||
|
||||
// foreach (var item in data)
|
||||
// {
|
||||
|
||||
// var index= data.IndexOf(item);
|
||||
// if ( DateTime.TryParse(item.StudyDate, out dt) == false)
|
||||
// {
|
||||
|
||||
// importResult.RowErrors.Add(new DataRowErrorInfo() { RowIndex = index, FieldErrors = new Dictionary<string, string> { { StaticData.International("ConsistencyVerification_Tech") , StaticData.International("ConsistencyVerification_Time") } } });
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// return importResult;
|
||||
// }
|
||||
//}
|
||||
|
||||
|
||||
public class ParamInfoDto
|
||||
{
|
||||
public string Modality { get; set; }
|
||||
|
||||
public string StudyDate { get; set; }
|
||||
|
||||
//public int ErrorType { get; set; }
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
//[ExcelImporter(/*ImportResultFilter = typeof(ImportResultFilteTest),*/ IsLabelingError = true)]
|
||||
|
||||
public class CheckViewModel
|
||||
{
|
||||
//[Required(ErrorMessage = "中心编号不能为空")]
|
||||
//[ImporterHeader(Name = "Site ID", AutoTrim = true)]
|
||||
[ExcelColumnName("Site ID")]
|
||||
public string SiteCode { get; set; } = string.Empty;
|
||||
|
||||
|
||||
//[Required(ErrorMessage = "受试者筛选号不能为空")]
|
||||
//[ImporterHeader(Name = "Subject ID", AutoTrim = true)]
|
||||
[ExcelColumnName("Subject ID")]
|
||||
public string SubjectCode { get; set; } = string.Empty;
|
||||
|
||||
//[Required(ErrorMessage = "访视名称不能为空")]
|
||||
//[ImporterHeader(Name = "Visit Name", AutoTrim = true)]
|
||||
[ExcelColumnName("Visit Name")]
|
||||
public string VisitName { get; set; } = string.Empty;
|
||||
|
||||
|
||||
|
||||
//[Required(ErrorMessage = "检查日期不能为空")]
|
||||
[CanConvertToTime(ErrorMessage = "Does not conform to Study Date format")]
|
||||
|
||||
//[ImporterHeader(Name = "Study Date", AutoTrim = true)]
|
||||
[ExcelColumnName("Study Date")]
|
||||
public string StudyDate { get; set; } = string.Empty;
|
||||
|
||||
|
||||
|
||||
//[Required(ErrorMessage = "Modality不能为空")]
|
||||
//[ImporterHeader(Name = "Modality", AutoTrim = true)]
|
||||
[ExcelColumnName("Modality")]
|
||||
public string Modality { get; set; } = string.Empty;
|
||||
|
||||
|
||||
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj == null) return false;
|
||||
|
||||
var checkModel = obj as CheckViewModel;
|
||||
|
||||
if (checkModel is not null)
|
||||
{
|
||||
return SiteCode == checkModel.SiteCode && SubjectCode == checkModel.SubjectCode && VisitName == checkModel.VisitName && StudyDate == checkModel.StudyDate && Modality == checkModel.Modality;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return (SiteCode + SubjectCode + VisitName + StudyDate + Modality).GetHashCode();
|
||||
}
|
||||
}
|
||||
|
||||
public class VisitPlanInfluenceSubjectVisitDTO
|
||||
{
|
||||
[ExcelColumn(Ignore = true)]
|
||||
public Guid StudyId { get; set; }
|
||||
|
||||
[ExcelColumn(Ignore = true)]
|
||||
public Guid TrialId { get; set; }
|
||||
|
||||
[ExcelColumn(Ignore = true)]
|
||||
public Guid SubjectVisitId { get; set; }
|
||||
|
||||
[ExcelColumnName("中心编号")]
|
||||
public string TrialSiteCode { get; set; } = string.Empty;
|
||||
|
||||
[ExcelColumnName("受试者")]
|
||||
public string SubjectCode { get; set; } = string.Empty;
|
||||
|
||||
[ExcelColumnName("访视名称")]
|
||||
public string VisitName { get; set; } = string.Empty;
|
||||
|
||||
[ExcelColumn(Name = "检查时间", Format = "yyyy-MM-dd HH:mm:ss")]
|
||||
public DateTime StudyTime { get; set; }
|
||||
|
||||
[ExcelColumnName("检查技术")]
|
||||
public string Modality { get; set; } = string.Empty;
|
||||
|
||||
[ExcelColumn(Ignore = true)]
|
||||
public bool IsDicomStudy { get; set; }
|
||||
|
||||
|
||||
[ExcelColumnName("影像类型")]
|
||||
public string ImageType => IsDicomStudy ? "Dicom" : "非Dicom";
|
||||
|
||||
[ExcelColumnName("历史窗口")]
|
||||
public string HistoryWindow { get; set; } = string.Empty;
|
||||
|
||||
|
||||
[ExcelColumnName("之前超窗调整后没超窗")]
|
||||
|
||||
public bool IsOverWindowNowNotOverWindow { get; set; }
|
||||
|
||||
[ExcelColumnName("目前窗口")]
|
||||
public string NowWindow { get; set; } = string.Empty;
|
||||
|
||||
[ExcelColumn(Ignore =true)]
|
||||
public DateTime CreateTime { get; set; }
|
||||
}
|
||||
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
using EasyCaching.Core;
|
||||
using IRaCIS.Core.Domain.Share;
|
||||
using IRaCIS.Core.Infra.EFCore;
|
||||
using MediatR;
|
||||
|
||||
namespace IRaCIS.Core.Application.MediatR.Handlers
|
||||
{
|
||||
|
||||
public class AnonymizeCacheRequest : IRequest<bool>
|
||||
{
|
||||
|
||||
}
|
||||
public class AnonymizeCacheHandler : IRequestHandler<AnonymizeCacheRequest,bool>
|
||||
{
|
||||
private readonly IRepository _repository;
|
||||
|
||||
private readonly IEasyCachingProvider _provider;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数注入
|
||||
/// </summary>
|
||||
public AnonymizeCacheHandler(IRepository repository, IEasyCachingProvider provider)
|
||||
{
|
||||
_repository = repository;
|
||||
|
||||
_provider = provider;
|
||||
}
|
||||
|
||||
|
||||
public Task<bool> Handle(AnonymizeCacheRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var systemAnonymizationList = _repository.Where<SystemAnonymization>(t => t.IsEnable).ToList();
|
||||
|
||||
_provider.Set(StaticData.Anonymize.Anonymize_AddFixedFiled, systemAnonymizationList.Where(t => t.IsAdd && t.IsFixed).ToList(), TimeSpan.FromDays(7));
|
||||
_provider.Set(StaticData.Anonymize.Anonymize_AddIRCInfoFiled, systemAnonymizationList.Where(t => t.IsAdd && t.IsFixed == false).ToList(), TimeSpan.FromDays(7));
|
||||
_provider.Set(StaticData.Anonymize.Anonymize_FixedField, systemAnonymizationList.Where(t => t.IsAdd == false && t.IsFixed).ToList(), TimeSpan.FromDays(7));
|
||||
_provider.Set(StaticData.Anonymize.Anonymize_IRCInfoField, systemAnonymizationList.Where(t => t.IsAdd == false && t.IsFixed == false).ToList(), TimeSpan.FromDays(7));
|
||||
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
using EasyCaching.Core;
|
||||
using IRaCIS.Core.Domain.Share;
|
||||
using IRaCIS.Core.Infra.EFCore;
|
||||
using MediatR;
|
||||
|
||||
namespace IRaCIS.Core.Application.MediatR.Handlers
|
||||
{
|
||||
|
||||
public class TrialStateCacheRequest : IRequest<bool>
|
||||
{
|
||||
|
||||
}
|
||||
public class TrialStateCacheHandler : IRequestHandler<TrialStateCacheRequest, bool>
|
||||
{
|
||||
private readonly IRepository<Trial> _trialRepository;
|
||||
|
||||
private readonly IEasyCachingProvider _provider;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数注入
|
||||
/// </summary>
|
||||
public TrialStateCacheHandler(IRepository<Trial> trialRepository, IEasyCachingProvider provider)
|
||||
{
|
||||
_trialRepository = trialRepository;
|
||||
|
||||
_provider = provider;
|
||||
|
||||
}
|
||||
|
||||
|
||||
public async Task<bool> Handle(TrialStateCacheRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
//项目启动,将项目状态缓存,因为hangfire 加入后台任务,还是向队列添加任务,执行都有延迟,效果不好
|
||||
var list = await _trialRepository.Select(t => new { TrialId = t.Id, TrialStatusStr = t.TrialStatusStr }).ToListAsync();
|
||||
|
||||
// 每天都会有任务刷新状态,项目编辑 添加时 都会处理到缓存中
|
||||
list.ForEach(t => _provider.Set(t.TrialId.ToString(), t.TrialStatusStr, TimeSpan.FromDays(7)));
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
Loading…
Reference in New Issue