using Hangfire; using IRaCIS.Core.Application.MediatR.CommandAndQueries; using IRaCIS.Core.Application.BackGroundJob; using IRaCIS.Core.Application.Contracts; using IRaCIS.Core.Application.Contracts.DTO; using IRaCIS.Core.Application.Filter; using IRaCIS.Core.Domain.Share; using MediatR; using Microsoft.AspNetCore.Mvc; using System.Net.Http.Headers; using Microsoft.AspNetCore.Http; using MiniExcelLibs; using ExcelDataReader; using System.Text; using System.Data; using IRaCIS.Core.Infra.EFCore; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Authorization; using WinSCP; using Magicodes.ExporterAndImporter.Excel; using Newtonsoft.Json; using Magicodes.ExporterAndImporter.Csv; using IRaCIS.Core.Application.Service.Inspection.Interface; namespace IRaCIS.Core.Application.Image.QA { [ApiExplorerSettings(GroupName = "Image")] public class QCOperationService : BaseService, IQCOperationService { private readonly DicomFileStoreHelper _dicomFileStoreHelper; private readonly IRepository _subjectVisitRepository; private readonly IRepository _trialRepository; private readonly IServiceProvider serviceProvider; private readonly IInspectionService _sinspectionService; private object _locker = new object(); public QCOperationService(DicomFileStoreHelper dicomFileStoreHelper, IRepository subjectVisitRepository, IRepository trialRepository, IServiceProvider serviceProvider, IInspectionService sinspectionService, IRepository _trialRepository ) { _dicomFileStoreHelper = dicomFileStoreHelper; _subjectVisitRepository = subjectVisitRepository; this._trialRepository = trialRepository; this.serviceProvider = serviceProvider; this._sinspectionService = sinspectionService; } #region QC质疑 以及回复 关闭 [HttpPost("{trialId:guid}/{subjectVisitId:guid}/{currentQCType:int}")] [TypeFilter(typeof(TrialResourceFilter))] public async Task VerifyQCCanAddChallenge(Guid subjectVisitId, [FromRoute] CurrentQC currentQCType) { if (!await _repository.AnyAsync(t => t.SubjectVisitId == subjectVisitId && t.CurrentQCEnum == currentQCType)) { return ResponseOutput.NotOk("必须填写审核问题,并保存,否则不允许添加质疑"); } return ResponseOutput.Ok(); } /// /// 添加和更新质疑 /// /// /// /// /// /// [HttpPost("{trialId:guid}/{trialQCProcess:int}/{currentQCType:int}")] [TypeFilter(typeof(TrialResourceFilter))] [Authorize(Policy = "ImageQCPolicy")] public async Task AddOrUpdateQCChallenge(QCChallengeCommand qaQuestionCommand, Guid trialId, [FromRoute] TrialQCProcess trialQCProcess, [FromRoute] CurrentQC currentQCType) { if (qaQuestionCommand.Id == null) { if (await _repository.AnyAsync(t => t.IsClosed == false && t.SubjectVisitId == qaQuestionCommand.SubjectVisitId && t.ReuploadEnum == QCChanllengeReuploadEnum.QCAgreeUpload)) { return ResponseOutput.NotOk("当前访视有未关闭的 同意CRC上传的质疑,不允许再次添加质疑"); } QCChallenge? qcChallenge = null; var success = false; lock (_locker) { var trialConfig = _repository.Where(t => t.Id == trialId).Select(t => new { TrialId = t.Id, t.QCProcessEnum, t.IsImageConsistencyVerification }).FirstOrDefault(); if (trialConfig == null) return Null404NotFound(trialConfig); #region 处理访视状态变更 //var dbSubjectVisit = _subjectVisitRepository.FirstOrDefault(t => t.Id == qaQuestionCommand.SubjectVisitId); //if (trialConfig.QCProcessEnum == TrialQCProcess.NotAudit) //{ // return ResponseOutput.NotOk("项目配置为不审,不允许添加QA质疑 "); //} //else if (trialConfig.QCProcessEnum == TrialQCProcess.SingleAudit) //{ // if ((int)dbSubjectVisit.AuditState == (int)SubjectVisitStateEnum.Submitted) // { // dbSubjectVisit.AuditState = AuditStateEnum.InPrimaryQC; // } // else // { // return ResponseOutput.NotOk($"项目配置为单审,当前审核状态不为{SubjectVisitStateEnum.Submitted},不允许添加QA质疑 "); // } //} //else if (trialConfig.QCProcessEnum == TrialQCProcess.DoubleAudit) //{ // if ((int)dbSubjectVisit.AuditState == (int)SubjectVisitStateEnum.Submitted) // { // dbSubjectVisit.AuditState = AuditStateEnum.InPrimaryQC; // } // else if ((int)dbSubjectVisit.AuditState == (int)SubjectVisitStateEnum.PrimaryQCPassed) // { // dbSubjectVisit.AuditState = AuditStateEnum.InSecondaryQC; // } // else // { // return ResponseOutput.NotOk($"项目配置为双审,访视状态为 {SubjectVisitStateEnum.Submitted}或{SubjectVisitStateEnum.PrimaryQCPassed}才允许添加QA质疑 "); // } //} #endregion //获取编号 var code = _repository.Where(t => t.TrialId == trialId).Select(t => t.ChallengeCode).DefaultIfEmpty().Max(); qcChallenge = _mapper.Map(qaQuestionCommand); qcChallenge.QCProcessEnum = trialConfig.QCProcessEnum; qcChallenge.CurrentQCEnum = currentQCType; qcChallenge.TrialId = trialId; qcChallenge.CreateUser = _userInfo.RealName; qcChallenge.ChallengeCode = code + 1; qcChallenge.UserTypeEnum = (UserTypeEnum)_userInfo.UserTypeEnumInt; _ = _repository.AddAsync(qcChallenge).Result; success = _repository.SaveChangesAsync().Result; } //分开两个事务 处理访视质疑状态 await DealChallengeState(qcChallenge.SubjectVisitId); return ResponseOutput.Result(success, qcChallenge.Id); #region 添加的时候把记录给留存 //var templateItems = _mapper.Map>(visitQaCommand.QATrialTemplateItemList); //templateItems.ForEach(u => //{ // u.IQA = _userInfo.RealName; // u.IQACreateTime = DateTime.Now; // u.IQANote = visitQaCommand.QARecord.Note; // u.IQADeadline = visitQaCommand.QARecord.DeadlineTime; // u.QARecordId = qaRecord.Id; // qaRecord.QARecordTemplateItemDetailList.Add(u); //}); ////添加了QA记录 引用了QA模板后就不允许删除和编辑,需要维护项目模板状态 传统做法 //var qaTrailTemplate = _qaTrailTemplateRepository.FirstOrDefault(t => t.Id == visitQaCommand.QARecord.QATrialTemplateId); //qaTrailTemplate.Status = QATrialTemplateStatus.HasQuote; #endregion } else { var qcChallenge = await _repository.FirstOrDefaultAsync(t => t.Id == qaQuestionCommand.Id); if (qcChallenge == null) return Null404NotFound(qcChallenge); _mapper.Map(qaQuestionCommand, qcChallenge); var success = await _repository.SaveChangesAsync(); await DealChallengeState(qcChallenge.SubjectVisitId); return ResponseOutput.Result(success); } } /// /// 关闭质疑,什么情况下允许? /// /// /// /// /// /// [HttpPost("{trialId:guid}/{qcChallengeId:guid}/{subjectVisitId:guid}/{closeEnum}/{closeReason}")] [TypeFilter(typeof(TrialResourceFilter))] //[Authorize(Policy = "ImageQCPolicy")] public async Task CloseQCChallenge(Guid qcChallengeId, Guid subjectVisitId, [FromRoute] QCChallengeCloseEnum closeEnum, [FromRoute] string closeReason) { var dbQCChallenge = await _repository.FirstOrDefaultAsync(t => t.Id == qcChallengeId); if (dbQCChallenge == null) return Null404NotFound(dbQCChallenge); if (dbQCChallenge.ReuploadEnum == QCChanllengeReuploadEnum.CRCRequestReupload || dbQCChallenge.ReuploadEnum == QCChanllengeReuploadEnum.QCAgreeUpload) { return ResponseOutput.NotOk("CRC申请重传的/QC同意重传的质疑,在CRC设置重传完成前不允许关闭质疑"); } dbQCChallenge.CloseResonEnum = closeEnum; dbQCChallenge.IsClosed = true; dbQCChallenge.ClosedTime = DateTime.Now; dbQCChallenge.DialogList.Add(new QCChallengeDialog() { SubjectVisitId = dbQCChallenge.SubjectVisitId, UserTypeEnum = (UserTypeEnum)_userInfo.UserTypeEnumInt, QCChallengeId = dbQCChallenge.Id, TalkContent = "关闭原因: " + closeReason }); var success = await _repository.SaveChangesAsync(); await DealChallengeState(subjectVisitId); return ResponseOutput.Result(success); } /// /// 访视级别统计 质疑最新的状态 /// /// private async Task DealChallengeState(Guid subjectVisitId) { var sv = await _repository.FirstOrDefaultAsync(t => t.Id == subjectVisitId); var closedStateList = await _repository.Where(t => t.SubjectVisitId == subjectVisitId).Select(t => t.IsClosed).ToListAsync(); if (closedStateList.Count == 0) { sv.ChallengeState = ChallengeStateEnum.No; } else if (closedStateList.All(t => t is true)) { sv.ChallengeState = ChallengeStateEnum.HaveAndAllClosed; } else { sv.ChallengeState = ChallengeStateEnum.HaveAndHaveNotClosed; } await _repository.SaveChangesAsync(); } /// /// 删除QC质疑记录 /// /// [HttpDelete("{qcChallengeId:guid}/{trialId:guid}")] [TypeFilter(typeof(TrialResourceFilter))] [Authorize(Policy = "ImageQCPolicy")] public async Task DeleteQCChallenge(Guid qcChallengeId) { if (await _repository.AnyAsync(t => t.QCChallengeId == qcChallengeId)) { ResponseOutput.NotOk("this QC Challenge Has been replied "); } var qaRecord = await _repository.FirstOrDefaultAsync(t => t.Id == qcChallengeId); if (qaRecord == null) return Null404NotFound(qaRecord); await _repository.DeleteAsync(qaRecord); var success1 = await _repository.SaveChangesAsync(); return ResponseOutput.Result(success1 /*|| success2 || success3*/); } /// /// 针对 某条QC质疑 添加回复 /// /// /// [HttpPost("{trialId:guid}")] [TypeFilter(typeof(TrialResourceFilter))] [Authorize(Policy = "ImageQCPolicy")] public async Task AddQCChallengeReply(QADialogCommand qaDialogCommand) { var qaReply = _mapper.Map(qaDialogCommand); await _repository.AddAsync(qaReply); qaReply.CreateUser = _userInfo.RealName; qaReply.UserTypeEnum = (UserTypeEnum)_userInfo.UserTypeEnumInt; var dbQCChallenge = await _repository.FirstOrDefaultAsync(t => t.Id == qaDialogCommand.QCChallengeId); if (dbQCChallenge == null) return Null404NotFound(dbQCChallenge); dbQCChallenge.LatestMsgTime = DateTime.Now; dbQCChallenge.LatestReplyUserId = _userInfo.Id; var success = await _repository.SaveChangesAsync(); return ResponseOutput.Result(success, qaReply); } #endregion #region 一致性核查 /// /// 一致性核查 质疑的添加/回复 /// /// /// [HttpPost("{trialId:guid}")] [TypeFilter(typeof(TrialResourceFilter))] [Authorize(Policy = "ImageCheckPolicy")] public async Task> AddCheckChallengeReply(CheckChallengeDialogCommand checkDialogCommand) { var qaReply = _mapper.Map(checkDialogCommand); qaReply.CreateUser = _userInfo.RealName; qaReply.UserTypeEnum = (UserTypeEnum)_userInfo.UserTypeEnumInt; await _repository.AddAsync(qaReply); //修改一致性核查 质疑状态 var sv = await _subjectVisitRepository.FirstOrDefaultAsync(t => t.Id == checkDialogCommand.SubjectVisitId); if (_userInfo.UserTypeEnumInt == (int)UserTypeEnum.ClinicalResearchCoordinator) { sv.CheckChallengeState = CheckChanllengeTypeEnum.CRCWaitPMReply; } else if (_userInfo.UserTypeEnumInt == (int)UserTypeEnum.ProjectManager) { sv.CheckChallengeState = CheckChanllengeTypeEnum.PMWaitCRCReply; } else { throw new Exception("一致性核查对话操作用户 只允许 CRC/PM"); } var success = await _repository.SaveChangesAsync(); return ResponseOutput.Result(success, qaReply); } /// /// 关闭 一致性核查质疑 /// /// [HttpPut("{trialId:guid}/{subjectVisitId:guid}")] [TypeFilter(typeof(TrialResourceFilter))] public async Task CloseCheckChallenge(Guid subjectVisitId) { var sv = await _subjectVisitRepository.FirstOrDefaultAsync(t => t.Id == subjectVisitId); if (sv == null) return Null404NotFound(sv); if (sv.RequestBackState == RequestBackStateEnum.PM_AgressBack) { ResponseOutput.NotOk("执行一致性核查的访视 不允许关闭质疑!"); } sv.CheckChallengeState = CheckChanllengeTypeEnum.Closed; await _repository.SaveChangesAsync(); return ResponseOutput.Ok(sv); } /// /// 手动设置一致性核查通过 /// /// /// /// [HttpPut("{trialId:guid}/{signId:guid}/{subjectVisitId:guid}")] [TypeFilter(typeof(TrialResourceFilter))] public async Task SetCheckPass(Guid subjectVisitId) { if (_userInfo.UserTypeEnumInt != (int)UserTypeEnum.ProjectManager) { ResponseOutput.NotOk("只允许PM 手动设置一致性核查通过"); } var sv = await _subjectVisitRepository.FirstOrDefaultAsync(t => t.Id == subjectVisitId); if (sv == null) return Null404NotFound(sv); if (sv.RequestBackState == RequestBackStateEnum.PM_AgressBack) { ResponseOutput.NotOk("当前是PM同意回退,不允许设置一致性核查通过"); } if (sv.CheckChallengeState != CheckChanllengeTypeEnum.Closed && sv.AuditState == AuditStateEnum.QCPassed) { ResponseOutput.NotOk("一致性核查质疑未关闭/审核状态不是通过,不允许设置一致性核查通过"); } sv.CheckState = CheckStateEnum.CVPassed; sv.ForwardState = ForwardStateEnum.ToForward; sv.CheckPassedTime = DateTime.Now; await _repository.SaveChangesAsync(); //var signSuccess = await _repository.UpdateFromQueryAsync(t => t.Id == signId, u => new TrialSign() { IsCompleted = true }); return ResponseOutput.Result(true); } /// /// CRC 请求回退 /// /// /// [HttpPut("{trialId:guid}/{subjectVisitId:guid}")] [TypeFilter(typeof(TrialResourceFilter))] public async Task CRCRequstCheckBack(Guid subjectVisitId) { var sv = await _subjectVisitRepository.FirstOrDefaultAsync(t => t.Id == subjectVisitId); if (sv == null) return Null404NotFound(sv); if (sv.CheckState == CheckStateEnum.CVPassed) { return ResponseOutput.NotOk("核查通过的数据不允许申请回退"); } await _subjectVisitRepository.UpdateFromQueryAsync(t => t.Id == subjectVisitId, u => new SubjectVisit() { RequestBackState = RequestBackStateEnum.CRC_RequestBack }); return ResponseOutput.Ok(); } /// /// 一致性核查 回退 对话记录不清除 只允许PM回退 /// /// [HttpPut("{trialId:guid}/{signId:guid}/{subjectVisitId:guid}")] [TypeFilter(typeof(TrialResourceFilter))] public async Task CheckBack(Guid subjectVisitId) { if (_userInfo.UserTypeEnumInt != (int)UserTypeEnum.ProjectManager) { return ResponseOutput.NotOk(" 只允许PM 回退!"); } var sv = await _subjectVisitRepository.FirstOrDefaultAsync(t => t.Id == subjectVisitId); if (sv == null) return Null404NotFound(sv); if (sv.CheckState == CheckStateEnum.CVPassed || sv.CheckState == CheckStateEnum.ToCheck) { return ResponseOutput.NotOk("待核查/核查通过的数据不允许回退"); } //sv.CheckChallengeState = (int)CheckChanllengeTypeEnum.None; //sv.CheckState = CheckStateEnum.None; //sv.ChallengeState = (int)ChallengeStateEnum.No; sv.AuditState = AuditStateEnum.None; sv.SubmitState = SubmitStateEnum.ToSubmit; //回退后,回退状态恢复 sv.RequestBackState = RequestBackStateEnum.NotRequest; sv.IsCheckBack = true; sv.CheckState = CheckStateEnum.None; sv.CheckChallengeState = CheckChanllengeTypeEnum.None; sv.SVENDTC = null; sv.SVSTDTC = null; sv.PreliminaryAuditTime = null; sv.SubmitTime = null; sv.ReviewAuditTime = null; sv.CurrentActionUserExpireTime = null; sv.IsTake = false; sv.CurrentActionUserId = null; sv.PreliminaryAuditUserId = null; sv.ReviewAuditUserId = null; //var success1 = _studyRepository.Delete(t => t.SubjectVisitId == subjectVisitId); //var succeess2 = _instanceRepository.Delete(t => t.SubjectVisitId == subjectVisitId); //var success3 = _seriesRepository.Delete(t => t.SubjectVisitId == subjectVisitId); //_qcChallengeRepository.Delete(t => t.SubjectVisitId == subjectVisitId); //_qcChallengeDialogRepository.Delete(t => t.SubjectVisitId == subjectVisitId); //_checkChallengeDialogRepository.Delete(t => t.SubjectVisitId == subjectVisitId); await _repository.AddAsync(new CheckChallengeDialog() { SubjectVisitId = subjectVisitId, TalkContent = "PM执行了一致性核查回退", UserTypeEnum = (UserTypeEnum)_userInfo.UserTypeEnumInt }); await _repository.DeleteFromQueryAsync(t => t.SubjectVisitId == subjectVisitId); var success = await _repository.SaveChangesAsync(); // var signSuccess = await _repository.UpdateFromQueryAsync(t => t.Id == signId, u => new TrialSign() { IsCompleted = true }); return ResponseOutput.Result( success); } /// /// 一致性核查 excel上传 支持三种格式 /// /// /// /// /// /// [HttpPost("{trialId:guid}")] [TypeFilter(typeof(TrialResourceFilter))] public async Task UploadVisitCheckExcel(IFormFile file, [FromServices] IMediator _mediator, Guid trialId, [FromServices] IWebHostEnvironment _hostEnvironment) { if (_userInfo.UserTypeEnumInt != (int)UserTypeEnum.ProjectManager) { return ResponseOutput.NotOk("只允许PM 操作"); } var rootPath = Directory.GetParent(_hostEnvironment.ContentRootPath.TrimEnd('\\')).IfNullThrowException().FullName; var fileName = ContentDispositionHeaderValue.Parse(file.ContentDisposition).FileName.ToString().Trim('"').ToLower(); if (!fileName.EndsWith(".xlsx") && !fileName.EndsWith(".csv") && !fileName.EndsWith(".xls")) { return ResponseOutput.NotOk("只允许Excel、 CSV!"); } //上传根路径 string uploadFolderPath = Path.Combine(rootPath, "UploadFile", "Check"); if (!Directory.Exists(uploadFolderPath)) { Directory.CreateDirectory(uploadFolderPath); } //存放核对表 var filePath = Path.Combine(uploadFolderPath, DateTime.Now.ToString("yyyy-MM-dd-hh-mm-ss") + fileName); using (FileStream fs = System.IO.File.Create(filePath)) { await file.CopyToAsync(fs); await fs.FlushAsync(); } //获取Excel表内容 var config = new MiniExcelLibs.Csv.CsvConfiguration() { StreamReaderFunc = (stream) => new StreamReader(stream, Encoding.GetEncoding("gb2312")) }; var etcCheckList = new List(); #region MiniExcel 需要自己验证数据格式规范 //if (fileName.EndsWith(".csv")) //{ // //因为csv 需要加配置文件 不然都是null // etcCheckList = MiniExcel.Query(filePath, null, configuration: config).ToList(); //} //else if (fileName.EndsWith(".xlsx")) //{ // // etcCheckList = MiniExcel.Query(filePath).ToList(); //} #endregion //Magicodes 支持自定义特性验证 if (fileName.EndsWith(".xlsx")) { var Importer = new ExcelImporter(); var import = await Importer.Import(File.OpenRead(filePath)); if (import.Exception != null) return ResponseOutput.NotOk(import.Exception.ToString()); if (import.RowErrors.Count > 0) return ResponseOutput.NotOk(JsonConvert.SerializeObject(import.RowErrors)); if (import.TemplateErrors.Count > 0) return ResponseOutput.NotOk(JsonConvert.SerializeObject(import.TemplateErrors)); etcCheckList = import.Data.ToList(); } else if (fileName.EndsWith(".csv")) { #region 临时方案 MiniExcel读取 然后保存为xlsx 再用 Magicodes验证数据 //因为csv 需要加配置文件 不然都是null etcCheckList = MiniExcel.Query(filePath, null, configuration: config).ToList(); var csVToXlsxPath = Path.Combine(uploadFolderPath, DateTime.Now.ToString("yyyy-MM-dd-hh-mm-ss") + Path.GetFileNameWithoutExtension(fileName) + ".xlsx"); await MiniExcel.SaveAsAsync(csVToXlsxPath, etcCheckList, excelType: ExcelType.XLSX); var Importer = new ExcelImporter(); var import = await Importer.Import(File.OpenRead(csVToXlsxPath)); if (import.Exception != null) return ResponseOutput.NotOk(import.Exception.ToString()); if (import.RowErrors.Count > 0) return ResponseOutput.NotOk(JsonConvert.SerializeObject(import.RowErrors)); if (import.TemplateErrors.Count > 0) return ResponseOutput.NotOk(JsonConvert.SerializeObject(import.TemplateErrors)); etcCheckList = import.Data.ToList(); #endregion #region 导入组件有问题 excel编码格式 //var Importer = new CsvImporter(); //var import = await Importer.Import(File.OpenRead(filePath)); //if (import.Exception != null) return ResponseOutput.NotOk(import.Exception.ToString()); //if (import.RowErrors.Count > 0) return ResponseOutput.NotOk(JsonConvert.SerializeObject(import.RowErrors)); //if (import.TemplateErrors.Count > 0) return ResponseOutput.NotOk(JsonConvert.SerializeObject(import.TemplateErrors)); //etcCheckList = import.Data.ToList(); #endregion } //ExcelReaderFactory 需要自己验证数据 并且从固定列取数据 else { //为了支持 xls 引入新的组件库 using (var stream = System.IO.File.Open(filePath, FileMode.Open, FileAccess.Read)) { // Auto-detect format, supports: // - Binary Excel files (2.0-2003 format; *.xls) // - OpenXml Excel files (2007 format; *.xlsx, *.xlsb) using (var reader = ExcelReaderFactory.CreateReader(stream)) { // 2. Use the AsDataSet extension method var dateset = reader.AsDataSet(); foreach (DataRow col in dateset.Tables[0].Rows) { etcCheckList.Add(new CheckViewModel() { SiteCode = col[0].ToString(), SubjectCode = col[1].ToString(), VisitName = col[2].ToString(), StudyDate = col[3].ToString(), Modality = col[4].ToString(), }); } etcCheckList.Remove(etcCheckList[0]); // The result of each spreadsheet is in result.Tables } } } //etcCheckList = etcCheckList.Where(t=>) if (etcCheckList == null || etcCheckList.Count == 0) { return ResponseOutput.NotOk("请上传规定格式的Excel文件,保证有有效数据!"); } else { //处理Excel 有时只是清除某些行的数据 读取也会读到数据,只是数据是null 后面处理的时候转为字符串为报错 etcCheckList.ForEach(t => { t.Modality = t.Modality ?? string.Empty; t.SiteCode = t.SiteCode ?? string.Empty; t.SubjectCode = t.SubjectCode ?? string.Empty; t.VisitName = t.VisitName ?? string.Empty; t.StudyDate = t.StudyDate ?? string.Empty; }); var dt = DateTime.Now; etcCheckList = etcCheckList.Where(t => !(t.Modality == string.Empty && t.SiteCode == string.Empty && t.SubjectCode == string.Empty && t.VisitName == string.Empty && t.StudyDate == string.Empty && DateTime.TryParse(t.StudyDate, out dt))).ToList(); if (etcCheckList.Count == 0) { return ResponseOutput.NotOk("请上传规定格式的Excel文件,保证有有效数据!"); } } await _mediator.Send(new ConsistencyVerificationRequest() { ETCList = etcCheckList, TrialId = trialId }); return ResponseOutput.Ok(); } #endregion #region QC 核对问题 操作检查各种操作 /// /// 添加或者更新 QC核对问题列表 两个人不能同时操作,就算意外进去了,提交数据,也不会覆盖前一个人数据, 后台已经做好判断 /// [HttpPost("{trialId:guid}/{subjectVisitId:guid}/{trialQCProcess:int}/{currentQCType:int}")] [TypeFilter(typeof(TrialResourceFilter))] public async Task AddOrUpdateQCQuestionAnswerList(QCQuestionAnswerCommand[] qcQuestionAnswerCommands, Guid trialId, Guid subjectVisitId, [FromRoute] TrialQCProcess trialQCProcess, [FromRoute] CurrentQC currentQCType) { //验证是否能操作 var verifyResult = await VerifyQCCanOpt(subjectVisitId); if (!verifyResult.IsSuccess) { return verifyResult; } //更新 if (qcQuestionAnswerCommands.Any(t => t.Id != null)) { #region 先删除再添加 //await _repository.DeleteFromQueryAsync(t => t.SubjectVisitId == subjectVisitId && t.QCProcessEnum == trialQCProcess && t.CurrentQCEnum == currentQCType); //var addlist = _mapper.Map>(qcQuestionAnswerCommands); //addlist.ForEach(t => { t.TrialId = trialId; t.SubjectVisitId = subjectVisitId; t.CurrentQCEnum = currentQCType; t.QCProcessEnum = trialQCProcess; }); //await _repository.AddRangeAsync(addlist); #endregion #region 先查询再更新 var questionAnswerList = await _repository.Where(t => t.SubjectVisitId == subjectVisitId && t.QCProcessEnum == trialQCProcess && t.CurrentQCEnum == currentQCType, true).ToListAsync(); qcQuestionAnswerCommands.ToList().ForEach(t => { var temp = questionAnswerList.FirstOrDefault(u => u.Id == t.Id); if (temp != null) { temp.Answer = t.Answer; //temp.ChildAnswer = t.ChildAnswer; } }); //Automapper 映射有问题 automapper 会把Guid? 类型的null 值转为 guid.Empty 导致映射错误 //_mapper.Map(qcQuestionAnswerCommands, questionAnswerList); #endregion return ResponseOutput.Ok(await _repository.SaveChangesAsync()); } else { var addlist = _mapper.Map>(qcQuestionAnswerCommands); addlist.ForEach(t => { t.TrialId = trialId; t.SubjectVisitId = subjectVisitId; t.CurrentQCEnum = currentQCType; t.QCProcessEnum = trialQCProcess; }); await _repository.AddRangeAsync(addlist); return ResponseOutput.Result(await _repository.SaveChangesAsync()); } } /// /// 1、设置为不读片,2 设置为读片(取消 先前设置为不读片) 4 设置为删除(数据库记录软删除) 5 恢复为未删除 /// /// /// /// /// /// [HttpPut("{trialId:guid}/{subjectVisitId:guid}/{studyId:guid}/{seriesId:guid}/{state:int}")] [TypeFilter(typeof(TrialResourceFilter))] public async Task SetSeriesState(Guid subjectVisitId, Guid studyId, Guid seriesId, int state) { //验证是否能操作 var verifyResult = await VerifyQCCanOpt(subjectVisitId); if (!verifyResult.IsSuccess) { return verifyResult; } var series = await _repository.Where(t => t.Id == seriesId, true).IgnoreQueryFilters().FirstOrDefaultAsync(); if (series == null) return Null404NotFound(series); if (state == 1) { series.IsReading = false; } else if (state == 2) { series.IsReading = true; } else if (state == 4) { series.IsDeleted = true; var study = await _repository.Where(t => t.Id == studyId, true).IgnoreQueryFilters().FirstOrDefaultAsync(); if (study == null) return Null404NotFound(study); var instanceIdList = await _repository.Where(t => t.SeriesId == seriesId).Select(t => t.Id).ToListAsync(); //instanceIdList.ForEach(t => //{ // var path = _dicomFileStoreHelper.GetInstanceFilePath(study, seriesId, t.ToString()); // if (System.IO.File.Exists(path)) // { // File.Delete(path); // } //}); study.InstanceCount = study.InstanceCount - instanceIdList.Count; study.SeriesCount = study.SeriesCount - 1; study.IsDeleted = study.SeriesCount == 0; } else if (state == 5) { series.IsDeleted = false; var study = await _repository.Where(t => t.Id == studyId, true).IgnoreQueryFilters().FirstOrDefaultAsync(); if (study == null) return Null404NotFound(study); var instanceIdList = await _repository.Where(t => t.SeriesId == seriesId).Select(t => t.Id).ToListAsync(); study.InstanceCount = study.InstanceCount + instanceIdList.Count; study.SeriesCount = study.SeriesCount + 1; study.IsDeleted = study.SeriesCount == 0; } return ResponseOutput.Ok(await _repository.SaveChangesAsync()); } /// ///type 1 :study 2: series 3:非dicom QC修改检查部位和 拍片类型 /// /// /// /// /// /// [HttpPut("{trialId:guid}/{id:guid}/{type:int}")] [TypeFilter(typeof(TrialResourceFilter))] public async Task UpdateModality(Guid id, int type, [FromQuery] string modality, [FromQuery] string bodyPart) { if (type == 1) { var study = await _repository.FirstOrDefaultAsync(t => t.Id == id); if (study == null) return Null404NotFound(study); study.BodyPartForEdit = bodyPart; study.Modalities = modality; await _repository.UpdateFromQueryAsync(t => t.StudyId == id, r => new DicomSeries() { BodyPartForEdit = bodyPart, Modality = modality }); } else if (type == 2) { var series = await _repository.FirstOrDefaultAsync(t => t.Id == id); if (series == null) return Null404NotFound(series); series.BodyPartForEdit = bodyPart; } else if (type == 3) { } await _repository.SaveChangesAsync(); return ResponseOutput.Ok(); } /// /// 验证是否质疑都关闭了 可以审核通过和不通过 /// /// /// [HttpGet("{subjectVisitId:guid}")] public async Task VerifyCanQCPassedOrFailed(Guid subjectVisitId) { if (await _repository.AnyAsync(t => t.SubjectVisitId == subjectVisitId && t.IsClosed == false)) { return ResponseOutput.NotOk("有质疑未关闭,不允许该操作"); } return ResponseOutput.Ok(); } /// /// 删除检查列表 /// /// /// /// /// SeriesCount [HttpPost, Route("{trialId:guid}/{subjectVisitId:guid}")] [TypeFilter(typeof(TrialResourceFilter))] public async Task DeleteStudyList(Guid[] ids, Guid subjectVisitId, Guid trialId) { //提交了 但是IQC同意的时候 是可以删除的 if (await _repository.AnyAsync(t => t.Id == subjectVisitId && t.SubmitState == SubmitStateEnum.Submitted && (!t.QCChallengeList.Any(u => u.ReuploadEnum == QCChanllengeReuploadEnum.QCAgreeUpload)))) { return ResponseOutput.NotOk("CRC Has Submited Image,can not delete"); } var DicomStudys = await _repository.GetQueryable().Where(x => ids.Contains(x.Id)).ToListAsync(); List datas = new List(); DicomStudys.ForEach(x => { datas.Add(new DataInspection() { SiteId = x.SiteId, SubjectId = x.SubjectId, TrialId = x.TrialId, SubjectVisitId=x.SubjectVisitId, GeneralId=x.Id, Identification= "Delete|DICOM Study|Data|Visit-Image Upload", JsonDetail = JsonConvert.SerializeObject(new { studyUid=x.StudyCode, modality=x.Modalities, bodyPart=x.BodyPartForEdit, seriesNum=x.SeriesCount, fileNum = x.InstanceCount, studyTime=x.StudyTime.ToString("yyyy-MM-dd") }) }); }); #region will calls error wried //ids.ToList().ForEach(async id => //{ // var success1 = await _repository.DeleteFromQueryAsync(t => t.Id == id); // var succeess2 = await _repository.DeleteFromQueryAsync(t => t.StudyId == id); // var success3 = await _repository.DeleteFromQueryAsync(t => t.StudyId == id); //}); #endregion foreach (var id in ids) { var success1 = await _repository.DeleteFromQueryAsync(t => t.Id == id); var succeess2 = await _repository.DeleteFromQueryAsync(t => t.StudyId == id); var success3 = await _repository.DeleteFromQueryAsync(t => t.StudyId == id); //删除 物理文件 var instanceIdList = await _repository.Where(t => t.StudyId == id) .Select(t => new { InstanceId = t.Id, t.SeriesId, t.StudyId, t.SubjectId, t.SiteId }).ToListAsync(); instanceIdList.ForEach(t => { var path = _dicomFileStoreHelper.GetInstanceFilePath(new DicomStudy() { Id = t.StudyId, SubjectId = t.SubjectId, TrialId = trialId, SiteId = t.SiteId, SubjectVisitId = subjectVisitId }, t.SeriesId, t.InstanceId.ToString()); if (System.IO.File.Exists(path)) { File.Delete(path); } }); } //一个访视下面有多个检查,所以需要检测 没有的时候才清空 非dicom 是检查文件 不是表记录 if (await _repository.CountAsync(t => t.SubjectVisitId == subjectVisitId) == 0 && await _repository.CountAsync(t => t.NoneDicomStudy.SubjectVisitId == subjectVisitId) == 0) { await _repository.UpdateFromQueryAsync(t => t.Id == subjectVisitId && t.SubmitState == SubmitStateEnum.ToSubmit, u => new SubjectVisit() { VisitExecuted = 0, SVENDTC = null, SVSTDTC = null, SubmitState = SubmitStateEnum.None }); //_qaNoticeRepository.Delete(t => t.SubjectVisitId == subjectVisitId); //_qaNoticeUserRepository.Delete(t => t.SubjectVisitId == subjectVisitId); //_qcChallengeRepository.Delete(t => t.SubjectVisitId == subjectVisitId); //_qcChallengeDialogRepository.Delete(t => t.SubjectVisitId == subjectVisitId); //_trialQCQuestionAnswerRepository.Delete(t => t.SubjectVisitId == subjectVisitId); //_checkChallengeDialogRepository.Delete(t => t.SubjectVisitId == subjectVisitId); } var svTime = await _repository.Where(t => t.Id == subjectVisitId).Select(t => new { DicomStudyMinStudyTime = t.StudyList.Min(t => (DateTime?)t.StudyTime), DicomStudyMaxStudyTime = t.StudyList.Max(t => (DateTime?)t.StudyTime), NoneDicomStudyMinStudyTime = t.NoneDicomStudyList.Min(t => (DateTime?)t.ImageDate), NoneDicomStudyMaxStudyTime = t.NoneDicomStudyList.Max(t => (DateTime?)t.ImageDate) }).FirstOrDefaultAsync().IfNullThrowException(); var minArray = new DateTime?[] { svTime.DicomStudyMinStudyTime, svTime.NoneDicomStudyMinStudyTime }; var maxArray = new DateTime?[] { svTime.DicomStudyMaxStudyTime, svTime.NoneDicomStudyMaxStudyTime }; await _repository.UpdateFromQueryAsync(t => t.Id == subjectVisitId, u => new SubjectVisit() { EarliestScanDate = minArray.Min(), LatestScanDate = maxArray.Max() }); var subvisit = await _repository.GetQueryable().FirstOrDefaultAsync(x => x.Id == subjectVisitId); datas.Add(new DataInspection() { SiteId = subvisit.SiteId, SubjectId = subvisit.SubjectId, TrialId = subvisit.TrialId, SubjectVisitId = subvisit.Id, Identification = "Edit|Visit|Status|Visit-Image Upload|Add Image", JsonDetail = JsonConvert.SerializeObject(new { SubmitState = "待提交", }) }); await _sinspectionService.AddListInspectionRecordAsync(datas); return ResponseOutput.Ok(); } #endregion #region 临床数据签名 领取、 设置紧急、RequestToQC QC通过、不通过 //[HttpPut("{trialId:guid}/{subjectVisitId:guid}")] //public async Task SignClinicalData(Guid trialId, Guid subjectVisitId) //{ // await _repository.UpdateFromQueryAsync(t => t.Id == subjectVisitId, u => new SubjectVisit() { ClinicalDataSignTime = DateTime.Now, ClinicalDataSignUserId = _userInfo.Id }); // return ResponseOutput.Ok(); //} /// /// 手动领取 或者取消 QC任务 /// /// /// /// true 获取 false是取消领取 /// [HttpPost("{trialId:guid}/{subjectVisitId:guid}/{obtaionOrCancel:bool}")] [TypeFilter(typeof(TrialResourceFilter))] public async Task ObtainOrCancelQCTask(Guid trialId, Guid subjectVisitId, bool obtaionOrCancel) { var dbSubjectVisit = await _subjectVisitRepository.FirstOrDefaultAsync(t => t.Id == subjectVisitId); if (dbSubjectVisit == null) return Null404NotFound(dbSubjectVisit); if (obtaionOrCancel) { if (await _subjectVisitRepository.AnyAsync(t => t.Trial.QCQuestionConfirmedUserId == null && t.Id == subjectVisitId)) { return ResponseOutput.NotOk("QC问题未确认,不允许领取任务"); } if (await _subjectVisitRepository.AnyAsync(t => t.IsTake && t.SubjectId != dbSubjectVisit.SubjectId && t.CurrentActionUserId == _userInfo.Id && t.TrialId == dbSubjectVisit.TrialId)) { return ResponseOutput.NotOk("您已经领取了其他受试者,完成后才允许领取新的受试者"); } #region 处理验证 var trialConfig = await _trialRepository .Select(t => new { TrialId = t.Id, t.QCProcessEnum, t.IsImageConsistencyVerification }) .FirstOrDefaultAsync(t => t.TrialId == trialId) .IfNullThrowException(); if (dbSubjectVisit.IsTake) { return ResponseOutput.NotOk("当前已被领取,不允许领取"); } if (trialConfig.QCProcessEnum == TrialQCProcess.NotAudit) { return ResponseOutput.NotOk("项目配置为不审,没有领取或者取消QC Task"); } else if (trialConfig.QCProcessEnum == TrialQCProcess.SingleAudit) { if (dbSubjectVisit.PreliminaryAuditUserId == _userInfo.Id) { return ResponseOutput.NotOk("初审已通过,不能继续领取"); } if (dbSubjectVisit.SubmitState == SubmitStateEnum.Submitted && dbSubjectVisit.AuditState == AuditStateEnum.ToAudit) { dbSubjectVisit.AuditState = AuditStateEnum.InPrimaryQC; } if (dbSubjectVisit.SubmitState == SubmitStateEnum.Submitted && dbSubjectVisit.AuditState == AuditStateEnum.InPrimaryQC) { //单审QC 释放后 也是可以领取的 } else { return ResponseOutput.NotOk("项目配置为单审,不满足SubmmitState:已提交 或者 AuditState:待审核/审核中, 不允许领取,请刷新界面"); } } else if (trialConfig.QCProcessEnum == TrialQCProcess.DoubleAudit) { if (dbSubjectVisit.PreliminaryAuditUserId == _userInfo.Id) { return ResponseOutput.NotOk("复审不能和初审是同一个人"); } //提交 并且初审通过 那么领取后进入 复审中 if (dbSubjectVisit.SubmitState == SubmitStateEnum.Submitted && dbSubjectVisit.AuditState == AuditStateEnum.PrimaryQCPassed) { dbSubjectVisit.AuditState = AuditStateEnum.InSecondaryQC; } else if (dbSubjectVisit.SubmitState == SubmitStateEnum.Submitted && dbSubjectVisit.AuditState == AuditStateEnum.ToAudit) { dbSubjectVisit.AuditState = AuditStateEnum.InPrimaryQC; } else if (dbSubjectVisit.SubmitState == SubmitStateEnum.Submitted && (dbSubjectVisit.AuditState == AuditStateEnum.InPrimaryQC || dbSubjectVisit.AuditState == AuditStateEnum.InSecondaryQC)) { //初审中 复审中 领取也是ok的 其他人接着做 } else { return ResponseOutput.NotOk("项目配置为复审,不满足提交状态:已提交 或者 审核状态:待审核/QC中, 不允许领取,请刷新界面"); } } #endregion dbSubjectVisit.IsTake = true; dbSubjectVisit.CurrentActionUserId = _userInfo.Id; dbSubjectVisit.CurrentActionUserExpireTime = DateTime.Now.AddHours(1); //启动定时任务 1h后处理 //BackgroundJob.Schedule(t => t.CancelQCObtaion(subjectVisitId, DateTime.Now), TimeSpan.FromHours(1)); } else { dbSubjectVisit.IsTake = false; dbSubjectVisit.CurrentActionUserId = null; dbSubjectVisit.CurrentActionUserExpireTime = null; } var success = await _repository.SaveChangesAsync(); return ResponseOutput.Result(success); } /// /// CRC RequestToQC 批量提交 /// /// [HttpPost] [TypeFilter(typeof(TrialResourceFilter))] public async Task CRCRequestToQC(CRCRequestToQCCommand cRCRequestToQCCommand) { var trialConfig = await _trialRepository .Select(t => new { TrialId = t.Id, t.QCProcessEnum, t.IsImageConsistencyVerification, t.IsUrgent, t.IsHaveFirstGiveMedicineDate, t.ClinicalInformationTransmissionEnum }) .FirstOrDefaultAsync(t => t.TrialId == cRCRequestToQCCommand.TrialId); if (trialConfig == null) return Null404NotFound(trialConfig); var dbSubjectVisitList = await _subjectVisitRepository.Where(t => cRCRequestToQCCommand.SubjectVisitIds.Contains(t.Id), true).Include(t => t.Subject).ToListAsync(); if (dbSubjectVisitList.Any(t => t.SubmitState == SubmitStateEnum.None)) { return ResponseOutput.NotOk("有访视未上传任何Dicom/非Dicom数据 不允许提交"); } foreach (var dbSubjectVisit in dbSubjectVisitList) { //基线不验证 if (trialConfig.IsHaveFirstGiveMedicineDate && !dbSubjectVisit.IsBaseLine && dbSubjectVisit.Subject.FirstGiveMedicineTime == null) { return ResponseOutput.NotOk("项目配置了需要填写首次给药日期 但是受试者没有填写首次给药日期,不允许提交"); } //基线 且配置了临床数据 if (trialConfig.ClinicalInformationTransmissionEnum != 0 && dbSubjectVisit.IsBaseLine/*&&dbSubjectVisit.ClinicalDataSignUserId==null*/) { //已确认临床数据完整性 dbSubjectVisit.IsConfirmedClinicalData = true; //var signSuccess = await _repository.UpdateFromQueryAsync(t => t.Id == cRCRequestToQCCommand.SignId, u => new TrialSign() { IsCompleted = true }); ////现在修改为 提交时 设置签名信息 //dbSubjectVisit.ClinicalDataSignUserId = _userInfo.Id; //dbSubjectVisit.ClinicalDataSignTime = DateTime.Now; //那么没有录入 不允许提交 //if (!await _repository.AnyAsync(t => t.PreviousHistoryList.Any() || t.PreviousOtherList.Any() || t.PreviousSurgeryList.Any())) //{ // return ResponseOutput.NotOk("没有临床数据,不允许提交"); //} //return ResponseOutput.NotOk("没有签名临床数据,不允许提交"); } var maxVisit = await _subjectVisitRepository.Where(t => t.SubjectId == dbSubjectVisit.SubjectId && t.SubmitState == SubmitStateEnum.Submitted) .OrderByDescending(t => t.VisitNum).Select(t => new { t.Id, t.VisitNum }).FirstOrDefaultAsync(); //修改受试者最新访视 dbSubjectVisit.Subject.LatestSubjectVisitId = maxVisit == null ? dbSubjectVisit.Id : maxVisit.VisitNum < dbSubjectVisit.VisitNum ? dbSubjectVisit.Id : maxVisit.Id; //var maxVisitNum = maxVisit == null ? dbSubjectVisit.VisitNum : maxVisit.VisitNum < dbSubjectVisit.VisitNum ? dbSubjectVisit.VisitNum : maxVisit.VisitNum; ////判断是否有缺失影像 //dbSubjectVisit.Subject.IsMissingImages = await _subjectVisitRepository.AnyAsync(t => (t.VisitNum < maxVisitNum && t.SubmitState != SubmitStateEnum.Submitted && t.IsLostVisit == false)); //项目或者Subject IsUrgent 提交时 访视也设置为紧急 if (trialConfig.IsUrgent || dbSubjectVisit.Subject.IsUrgent || (dbSubjectVisit.PDState == PDStateEnum.PDProgress && !dbSubjectVisit.IsBaseLine) || (dbSubjectVisit.IsEnrollmentConfirm && dbSubjectVisit.IsBaseLine)) { dbSubjectVisit.IsUrgent = true; } if (dbSubjectVisit.SubmitState == SubmitStateEnum.ToSubmit || dbSubjectVisit.SubmitState == SubmitStateEnum.Submitted) { dbSubjectVisit.SubmitState = SubmitStateEnum.Submitted; dbSubjectVisit.SubmitTime = DateTime.Now; } //不审 直接QC通过 可能一致性核查 也可能不一致性核查 if (trialConfig.QCProcessEnum == TrialQCProcess.NotAudit) { dbSubjectVisit.AuditState = AuditStateEnum.QCPassed; // 不一致性核查 就CVPassed ToForward 否则就是待核查 dbSubjectVisit.CheckState = trialConfig.IsImageConsistencyVerification ? CheckStateEnum.ToCheck : CheckStateEnum.CVPassed; dbSubjectVisit.ForwardState = trialConfig.IsImageConsistencyVerification ? ForwardStateEnum.None : ForwardStateEnum.ToForward; } else if (trialConfig.QCProcessEnum == TrialQCProcess.SingleAudit) { dbSubjectVisit.AuditState = AuditStateEnum.ToAudit; } else if (trialConfig.QCProcessEnum == TrialQCProcess.DoubleAudit) { dbSubjectVisit.AuditState = AuditStateEnum.ToAudit; } } var success = await _repository.SaveChangesAsync(); List datas = new List(); dbSubjectVisitList.ForEach(x => { datas.Add(new DataInspection() { SiteId = x.SiteId, SubjectId = x.SubjectId, TrialId = x.TrialId, SubjectVisitId = x.Id, Identification= "Edit|Visit|Status|Visit-Image Upload", JsonDetail = JsonConvert.SerializeObject(new { SubmitState = "已提交", }) }); }); dbSubjectVisitList.ForEach(x => { datas.Add(new DataInspection() { SiteId = x.SiteId, SubjectId = x.SubjectId, TrialId = x.TrialId, SubjectVisitId = x.Id, Identification = "Edit|Visit|Status|Visit-Image Upload-1", JsonDetail = JsonConvert.SerializeObject(new { SubmitState = "已提交", }) }); }); await _sinspectionService.AddListInspectionRecordAsync(datas); return ResponseOutput.Result(success); } /// /// 设置QC 通过或者不通过 7:QC failed 8:QC passed /// /// /// /// /// /// [HttpPost("{trialId:guid}/{subjectVisitId:guid}/{signId:guid}/{auditState:int}")] [TypeFilter(typeof(TrialResourceFilter))] public async Task QCPassedOrFailed(Guid trialId, Guid subjectVisitId, Guid signId, [FromRoute] AuditStateEnum auditState) { if (!await _repository.AnyAsync(t => t.TrialId == trialId && t.UserId == _userInfo.Id)) { return ResponseOutput.NotOk("您已经被移出项目,不允许该操作"); } //判断质疑是否都关闭了 if (await _repository.AnyAsync(t => t.SubjectVisitId == subjectVisitId && t.IsClosed == false)) { return ResponseOutput.NotOk("有质疑未关闭,不允许该操作"); } var trialConfig = await _trialRepository .Select(t => new { TrialId = t.Id, t.QCProcessEnum, t.IsImageConsistencyVerification }) .FirstOrDefaultAsync(t => t.TrialId == trialId) .IfNullThrowException(); var dbSubjectVisit = await _subjectVisitRepository.FirstOrDefaultAsync(t => t.Id == subjectVisitId); if (dbSubjectVisit == null) return Null404NotFound(dbSubjectVisit); //有人QC Passed if (auditState == AuditStateEnum.QCPassed) { //判断 QC流程 不审 单审 双审 if (trialConfig.QCProcessEnum == TrialQCProcess.NotAudit) { return ResponseOutput.NotOk("不审状态下,不允许设置为QC Passed"); } else if (trialConfig.QCProcessEnum == TrialQCProcess.SingleAudit) { if (dbSubjectVisit.AuditState == AuditStateEnum.InPrimaryQC) { if (!await _repository.AnyAsync(t => t.SubjectVisitId == subjectVisitId && t.CurrentQCEnum == CurrentQC.First)) { return ResponseOutput.NotOk("必须填写审核问题,并保存,否则不允许提交"); } // 单审 dbSubjectVisit.AuditState = AuditStateEnum.QCPassed; dbSubjectVisit.CheckState = trialConfig.IsImageConsistencyVerification ? CheckStateEnum.ToCheck : CheckStateEnum.CVPassed; dbSubjectVisit.ForwardState = trialConfig.IsImageConsistencyVerification ? ForwardStateEnum.None : ForwardStateEnum.ToForward; dbSubjectVisit.PreliminaryAuditUserId = _userInfo.Id; dbSubjectVisit.PreliminaryAuditTime = DateTime.Now; dbSubjectVisit.IsTake = false; dbSubjectVisit.CurrentActionUserExpireTime = null; } else { return ResponseOutput.NotOk("项目配置为单审 当前审核状态不为 InPrimaryQC,不能变更到 QCPassed"); } } else if (trialConfig.QCProcessEnum == TrialQCProcess.DoubleAudit) { // 双审 如果当前db 状态是 InPrimaryQC 当前操作为 QCPassed 那么设置为 PrimaryQCPassed if (dbSubjectVisit.AuditState == AuditStateEnum.InPrimaryQC) { if (!await _repository.AnyAsync(t => t.SubjectVisitId == subjectVisitId && t.CurrentQCEnum == CurrentQC.First)) { return ResponseOutput.NotOk("必须填写审核问题,并保存,否则不允许提交"); } dbSubjectVisit.AuditState = AuditStateEnum.PrimaryQCPassed; dbSubjectVisit.PreliminaryAuditUserId = _userInfo.Id; dbSubjectVisit.PreliminaryAuditTime = DateTime.Now; dbSubjectVisit.IsTake = false; dbSubjectVisit.CurrentActionUserExpireTime = null; } else if (dbSubjectVisit.AuditState == AuditStateEnum.InSecondaryQC) { if (!await _repository.AnyAsync(t => t.SubjectVisitId == subjectVisitId && t.CurrentQCEnum == CurrentQC.Second)) { return ResponseOutput.NotOk("必须填写审核问题,并保存,否则不允许提交"); } dbSubjectVisit.AuditState = AuditStateEnum.QCPassed; dbSubjectVisit.CheckState = trialConfig.IsImageConsistencyVerification ? CheckStateEnum.ToCheck : CheckStateEnum.CVPassed; dbSubjectVisit.ForwardState = trialConfig.IsImageConsistencyVerification ? ForwardStateEnum.None : ForwardStateEnum.ToForward; dbSubjectVisit.ReviewAuditUserId = _userInfo.Id; dbSubjectVisit.ReviewAuditTime = DateTime.Now; dbSubjectVisit.IsTake = false; dbSubjectVisit.CurrentActionUserExpireTime = null; } else { return ResponseOutput.NotOk($"项目配置为双审 当前审核状态为 {dbSubjectVisit.AuditState},不能变更到 QCPassed"); } } } else if (auditState == AuditStateEnum.QCFailed) { //判断 QC流程 不审 单审 双审 if (trialConfig.QCProcessEnum == TrialQCProcess.NotAudit) { return ResponseOutput.NotOk("不审状态下,不允许设置为QC Failed"); } else if (trialConfig.QCProcessEnum == TrialQCProcess.SingleAudit) { if (dbSubjectVisit.AuditState == AuditStateEnum.InPrimaryQC) { // 单审 dbSubjectVisit.AuditState = AuditStateEnum.QCFailed; } else { return ResponseOutput.NotOk("项目配置为单审 当前审核状态不为 InPrimaryQC,不能变更到 QCFailed"); } } else if (trialConfig.QCProcessEnum == TrialQCProcess.DoubleAudit) { // 双审 if (dbSubjectVisit.AuditState == AuditStateEnum.InPrimaryQC || dbSubjectVisit.AuditState == AuditStateEnum.InSecondaryQC) { dbSubjectVisit.AuditState = AuditStateEnum.QCFailed; } else { return ResponseOutput.NotOk($"项目配置为双审 当前审核状态为 {dbSubjectVisit.AuditState},不能变更到 QCFailed"); } } } //删除 软删除的物理文件 var instanceIdList = await _repository.Where(t => t.DicomSerie.IsDeleted && t.SubjectVisitId == subjectVisitId) .Select(t => new { InstanceId = t.Id, t.SeriesId, t.StudyId, t.SubjectId, t.SiteId }).ToListAsync(); instanceIdList.ForEach(t => { var path = _dicomFileStoreHelper.GetInstanceFilePath(new DicomStudy() { Id = t.StudyId, SubjectId = t.SubjectId, TrialId = trialId, SiteId = t.SiteId, SubjectVisitId = subjectVisitId }, t.SeriesId, t.InstanceId.ToString()); if (System.IO.File.Exists(path)) { File.Delete(path); } }); await _repository.SaveChangesAsync(); var success = await _repository.UpdateFromQueryAsync(t => t.Id == signId, u => new TrialSign() { IsCompleted = true }); return ResponseOutput.Result(success); } /// /// 设置、取消 访视紧急 /// /// /// /// /// [HttpPut("{trialId:guid}/{subjectVisitId:guid}/{setOrCancel:bool}")] [TypeFilter(typeof(TrialResourceFilter))] public async Task SetVisitUrgent(Guid trialId, Guid subjectVisitId, bool setOrCancel) { var sv = await _subjectVisitRepository.FirstOrDefaultAsync(t => t.Id == subjectVisitId); if (sv == null) return Null404NotFound(sv); sv.IsUrgent = setOrCancel; var success = await _repository.SaveChangesAsync(); return ResponseOutput.Ok(); } #endregion #region 重传、重传完设置 /// /// QA设置 需要重传 /// /// /// /// /// [HttpPut("{trialId:guid}/{qcChallengeId:guid}/{signId:guid}")] [TypeFilter(typeof(TrialResourceFilter))] public async Task SetNeedReupload(Guid trialId, Guid signId, Guid qcChallengeId) { if (_userInfo.UserTypeEnumInt != (int)UserTypeEnum.IQC) { return ResponseOutput.NotOk("重传 只允许QA 设置!"); } //获取项目配置 var trialConfig = await _repository.Where(t => t.Id == trialId).Select(t => new { TrialId = t.Id, t.QCProcessEnum, t.IsImageConsistencyVerification }) .FirstOrDefaultAsync().IfNullThrowException(); if (trialConfig.QCProcessEnum == TrialQCProcess.NotAudit) { return ResponseOutput.NotOk("不审操作,不会有需要重传的操作!"); } var qcChallenge = await _repository.FirstOrDefaultAsync(t => t.Id == qcChallengeId); if (qcChallenge == null) return Null404NotFound(qcChallenge); if (await _repository.CountAsync(t => t.ReuploadEnum == QCChanllengeReuploadEnum.QCAgreeUpload && t.SubjectVisitId == qcChallenge.SubjectVisitId && t.IsClosed == false) >= 1) { return ResponseOutput.NotOk("当前访视,有一个未关闭的质疑 QC设置了需要重传,CRC还未完成上传,当前不允许再次设置"); } qcChallenge.ReuploadEnum = QCChanllengeReuploadEnum.QCAgreeUpload; qcChallenge.LatestMsgTime = DateTime.Now; qcChallenge.LatestReplyUserId = _userInfo.Id; await _subjectVisitRepository.UpdateFromQueryAsync(t => t.Id == qcChallenge.SubjectVisitId, c => new SubjectVisit() { IsQCConfirmedReupload = true }); qcChallenge.DialogList.Add(new QCChallengeDialog() { SubjectVisitId = qcChallenge.SubjectVisitId, UserTypeEnum = (UserTypeEnum)_userInfo.UserTypeEnumInt, QCChallengeId = qcChallenge.Id, TalkContent = "QC同意重传" }); //双审 并且是2QC 那么需要回退到1QC 讲1QC数据清除 if (trialConfig.QCProcessEnum == TrialQCProcess.DoubleAudit && await _repository.AnyAsync(t => t.Id == qcChallengeId && t.SubjectVisit.AuditState == AuditStateEnum.InSecondaryQC)) { var sv = await _subjectVisitRepository.FirstOrDefaultAsync(t => t.Id == qcChallenge.SubjectVisitId); if (sv == null) return Null404NotFound(sv); // //一致性核查质疑状态 // sv.CheckChallengeState = CheckChanllengeTypeEnum.None; //// 一致性核查状态 // sv.CheckState = CheckStateEnum.None; // 审核状态 sv.AuditState = AuditStateEnum.InPrimaryQC; sv.CurrentActionUserExpireTime = DateTime.Now.AddHours(1); sv.CurrentActionUserId = _userInfo.Id; BackgroundJob.Schedule(t => t.CancelQCObtaion(qcChallenge.SubjectVisitId, DateTime.Now), TimeSpan.FromHours(1)); sv.IsTake = true; sv.PreliminaryAuditUserId = null; sv.ReviewAuditUserId = null; //删除1QC 填写的问题答案 await _repository.DeleteFromQueryAsync(t => t.SubjectVisitId == qcChallenge.SubjectVisitId && t.CurrentQCEnum == CurrentQC.First); //2QC 数据变为1QC await _repository.UpdateFromQueryAsync(t => t.SubjectVisitId == qcChallenge.SubjectVisitId && t.CurrentQCEnum == CurrentQC.Second, k => new QCChallenge() { CurrentQCEnum = CurrentQC.First }); await _repository.UpdateFromQueryAsync(t => t.SubjectVisitId == qcChallenge.SubjectVisitId && t.CurrentQCEnum == CurrentQC.Second, k => new TrialQCQuestionAnswer() { CurrentQCEnum = CurrentQC.First }); } var success = await _repository.SaveChangesAsync(); var signSuccess = await _repository.UpdateFromQueryAsync(t => t.Id == signId, u => new TrialSign() { IsCompleted = true }); return ResponseOutput.Result(success && signSuccess); } /// /// CRC 设置已经重传完成 /// /// [HttpPost] [TypeFilter(typeof(TrialResourceFilter))] public async Task SetReuploadFinished(CRCReuploadFinishedCommand cRCReuploadFinishedCommand) { if (_userInfo.UserTypeEnumInt != (int)UserTypeEnum.ClinicalResearchCoordinator) { return ResponseOutput.NotOk("重传完成 只允许CRC 设置!"); } var trialConfig = await _trialRepository .Select(t => new { TrialId = t.Id, t.QCProcessEnum, t.IsImageConsistencyVerification, t.IsUrgent, t.IsHaveFirstGiveMedicineDate, t.ClinicalInformationTransmissionEnum }) .FirstOrDefaultAsync(t => t.TrialId == cRCReuploadFinishedCommand.TrialId); var qcChallenge = await _repository.FirstOrDefaultAsync(t => t.Id == cRCReuploadFinishedCommand.QCChallengeId); if (qcChallenge == null) return Null404NotFound(qcChallenge); qcChallenge.ReuploadEnum = QCChanllengeReuploadEnum.CRCReuploaded; qcChallenge.ReUploadedTime = DateTime.Now; qcChallenge.ReUploader = _userInfo.RealName; qcChallenge.LatestMsgTime = DateTime.Now; qcChallenge.LatestReplyUserId = _userInfo.Id; var dbSubjectVisit = await _subjectVisitRepository.FirstOrDefaultAsync(t => t.Id == qcChallenge.SubjectVisitId).IfNullThrowException(); await _subjectVisitRepository.UpdateFromQueryAsync(t => t.Id == qcChallenge.SubjectVisitId, c => new SubjectVisit() { IsQCConfirmedReupload = false }); qcChallenge.DialogList.Add(new QCChallengeDialog() { SubjectVisitId = qcChallenge.SubjectVisitId, UserTypeEnum = (UserTypeEnum)_userInfo.UserTypeEnumInt, QCChallengeId = qcChallenge.Id, TalkContent = "CRC已重传完成" }); //基线 且配置了临床数据 if (trialConfig.ClinicalInformationTransmissionEnum != 0 && dbSubjectVisit.IsBaseLine) { //已确认临床数据完整性 dbSubjectVisit.IsConfirmedClinicalData = true; var signSuccess = await _repository.UpdateFromQueryAsync(t => t.Id == cRCReuploadFinishedCommand.SignId, u => new TrialSign() { IsCompleted = true }); } var success = await _repository.SaveChangesAsync(); return ResponseOutput.Ok(success); } [HttpPut("{trialId:guid}/{qcChallengeId:guid}")] [TypeFilter(typeof(TrialResourceFilter))] public async Task CRCRequestReUpload(Guid qcChallengeId) { var qcChallenge = await _repository.FirstOrDefaultAsync(t => t.Id == qcChallengeId); if (qcChallenge == null) return Null404NotFound(qcChallenge); if (qcChallenge.ReuploadEnum == QCChanllengeReuploadEnum.CRCReuploaded) { qcChallenge.ReUploadedTime = null; } qcChallenge.LatestMsgTime = DateTime.Now; qcChallenge.LatestReplyUserId = _userInfo.Id; qcChallenge.ReuploadEnum = QCChanllengeReuploadEnum.CRCRequestReupload; qcChallenge.DialogList.Add(new QCChallengeDialog() { SubjectVisitId = qcChallenge.SubjectVisitId, UserTypeEnum = (UserTypeEnum)_userInfo.UserTypeEnumInt, QCChallengeId = qcChallenge.Id, TalkContent = "CRC申请重传/上传影像" }); await _repository.SaveChangesAsync(); return ResponseOutput.Ok(); } #endregion /// /// 上传界面 更新受试者首次给药日期 是否入组确认,以及访视 是否PD进展 /// /// /// [HttpPut("{trialId:guid}")] [TypeFilter(typeof(TrialResourceFilter))] public async Task UpdateSubjectAndSVInfo(UploadSubjectAndVisitCommand command) { if (command.IsEnrollmentConfirm != null) { if (await _subjectVisitRepository.Where(t => t.Id == command.SubjectVisitId) .AnyAsync(t => t.SubmitState == SubmitStateEnum.Submitted && t.IsEnrollmentConfirm != command.IsEnrollmentConfirm)) { return ResponseOutput.NotOk("CRC已提交了 不能修改入组确认状态"); } if (await _subjectVisitRepository.Where(t => t.Id == command.SubjectVisitId) .AnyAsync(t => t.IsEnrollmentConfirm != command.IsEnrollmentConfirm && t.RequestBackState == RequestBackStateEnum.PM_AgressBack)) { return ResponseOutput.NotOk("回退的访视,不允许修改PD确认状态"); } await _repository.UpdateFromQueryAsync(t => t.Id == command.SubjectVisitId, u => new SubjectVisit() { IsEnrollmentConfirm = command.IsEnrollmentConfirm.Value, }); } if (command.SubjectFirstGiveMedicineTime != null) { await _repository.UpdateFromQueryAsync(t => t.Id == command.SubjectId, u => new Subject() { FirstGiveMedicineTime = command.SubjectFirstGiveMedicineTime, }); } ////受试者基线 入组确认 或者访视PD 进展 默认加急 //await _repository.UpdateFromQueryAsync(t => t.Id == command.SubjectVisitId, u => new SubjectVisit() //{ // PDState = command.PDState, // IsUrgent = (command.IsEnrollmentConfirm == true) || (command.PDState == PDStateEnum.PDProgress) //}); return ResponseOutput.Ok(); } /// /// 验证QC是否可以操作 数据库查询判断当前QC执行人和登陆的用户是否一致 /// /// /// private async Task VerifyQCCanOpt(Guid subjectVisitId) { var optUser = await _subjectVisitRepository.Where(t => t.Id == subjectVisitId).FirstOrDefaultAsync(); if (optUser == null) return Null404NotFound(optUser); if (optUser.CurrentActionUserId != _userInfo.Id) { return ResponseOutput.NotOk("其他QC正在进行审核,当前操作不允许"); } return ResponseOutput.Ok(); } [HttpPost("{trialId:guid}")] public async Task ForwardSVDicomImage(Guid[] subjectVisitIdList, [FromServices] DicomFileStoreHelper _dicomFileStoreHelper) { foreach (var subjectVisitId in subjectVisitIdList) { var info = await _subjectVisitRepository.Where(t => t.Id == subjectVisitId).ProjectTo(_mapper.ConfigurationProvider).FirstOrDefaultAsync(); var targetPath = "/IMPORT-IMAGES/" + info.TrialCode + "_" + info.SubjectCode + "_" + info.VisitName; var path = _dicomFileStoreHelper.GetSubjectVisitPath(info.TrialId, info.SiteId, 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=" }; using (Session session = new Session()) { var studyFolders = (new DirectoryInfo(path)).GetDirectories(); session.Open(sessionOptions); if (!session.FileExists(targetPath)) { session.CreateDirectory(targetPath); } foreach (var studyFolder in studyFolders) { if (!session.FileExists(Path.Combine(targetPath, studyFolder.Name))) { session.CreateDirectory(targetPath); } foreach (var file in studyFolder.GetFiles()) { if (file.Extension.Contains("dcm", StringComparison.OrdinalIgnoreCase)) { string remoteFilePath = RemotePath.TranslateLocalPathToRemote(file.FullName, studyFolder.FullName, targetPath); var result = session.PutFiles(file.FullName, remoteFilePath, false); if (!result.IsSuccess) { await _subjectVisitRepository.UpdateFromQueryAsync(t => t.Id == subjectVisitId, u => new SubjectVisit() { ForwardState = ForwardStateEnum.ForwardFailed }); return ResponseOutput.NotOk("Forward Failed" + result.Failures.ToString() + result.ToJson()); } } } } } } catch (Exception e) { await _subjectVisitRepository.UpdateFromQueryAsync(t => t.Id == subjectVisitId, u => new SubjectVisit() { ForwardState = ForwardStateEnum.ForwardFailed }); } await _subjectVisitRepository.UpdateFromQueryAsync(t => t.Id == subjectVisitId, u => new SubjectVisit() { ForwardState = ForwardStateEnum.Forwarded,ForwardUserId = _userInfo.Id,ForwardTime = DateTime.Now}); } return ResponseOutput.Ok(); } } }