Merge branch 'Test_IRC_Net8' of https://gitea.frp.extimaging.com/XCKJ/irc-netcore-api into Test_IRC_Net8
continuous-integration/drone/push Build is passing Details

Test_IRC_Net8
hang 2026-01-27 15:18:51 +08:00
commit e6b3c662e2
51 changed files with 55375 additions and 1242 deletions

View File

@ -194,6 +194,7 @@ app.MapControllers();
Log.Logger = new LoggerConfiguration()
//.MinimumLevel.Information()
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.MinimumLevel.Override("ZiggyCreatures.Caching.Fusion", LogEventLevel.Warning)
.WriteTo.Console()
.WriteTo.File($"{AppContext.BaseDirectory}Serilogs/.log", rollingInterval: RollingInterval.Day)
.CreateLogger();

View File

@ -1,28 +1,29 @@
using FellowOakDicom.Network;
using FellowOakDicom;
using FellowOakDicom;
using FellowOakDicom.Imaging;
using FellowOakDicom.IO.Buffer;
using FellowOakDicom.Network;
using IRaCIS.Core.Domain.Models;
using IRaCIS.Core.Domain.Share;
using IRaCIS.Core.Infra.EFCore;
using IRaCIS.Core.Infrastructure;
using IRaCIS.Core.Infrastructure.Extention;
using IRaCIS.Core.SCP.Service;
using Medallion.Threading;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion.Internal;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Serilog;
using SharpCompress.Common;
using SixLabors.ImageSharp.Formats.Jpeg;
using System;
using System.Collections.Generic;
using System.Data;
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;
using IRaCIS.Core.Infrastructure.Extention;
using FellowOakDicom.IO.Buffer;
using ZiggyCreatures.Caching.Fusion;
namespace IRaCIS.Core.SCP.Service
{
@ -120,7 +121,7 @@ namespace IRaCIS.Core.SCP.Service
var _trialSiteDicomAERepository = _serviceProvider.GetService<IRepository<TrialSiteDicomAE>>();
var findTrialSiteAE = _trialSiteDicomAERepository.Where(t => t.CallingAE == association.CallingAE && t.TrialId==_trialId).FirstOrDefault();
var findTrialSiteAE = _trialSiteDicomAERepository.Where(t => t.CallingAE == association.CallingAE && t.TrialId == _trialId).FirstOrDefault();
if (findTrialSiteAE != null)
{
@ -328,6 +329,11 @@ namespace IRaCIS.Core.SCP.Service
var _seriesRepository = _serviceProvider.GetService<IRepository<SCPSeries>>();
var _distributedLockProvider = _serviceProvider.GetService<IDistributedLockProvider>();
var _fusionCache = _serviceProvider.GetService<IFusionCache>();
var _trialSiteRepository = _serviceProvider.GetService<IRepository<TrialSite>>();
var _systemAnonymizationRepository = _serviceProvider.GetService<IRepository<SystemAnonymization>>();
var storeRelativePath = string.Empty;
var ossFolderPath = $"{_trialId}/Image/PACS/{_trialSiteId}/{studyInstanceUid}";
@ -336,337 +342,383 @@ namespace IRaCIS.Core.SCP.Service
long fileSize = 0;
try
{
using (MemoryStream ms = new MemoryStream())
// 直接拿 Dataset已经完整
var dataset = request.Dataset;
#region 匿名化
var anonymizeList = await _fusionCache.GetOrSetAsync(CacheKeys.SystemAnonymization, _ => CacheHelper.GetSystemAnonymizationListAsync(_systemAnonymizationRepository), TimeSpan.FromDays(7));
var trialSiteInfo = await _fusionCache.GetOrSetAsync(CacheKeys.TrialSiteInfo(_trialSiteId), _ => CacheHelper.GetTrialSiteInfo(_trialSiteId, _trialSiteRepository), TimeSpan.FromMinutes(2));
var fixedFiledList = anonymizeList.Where(t => t.IsFixed).ToList();
var ircFiledList = anonymizeList.Where(t => t.IsFixed == false).ToList();
foreach (var item in fixedFiledList)
{
await request.File.SaveAsync(ms);
#region 1帧拆成多个固定大小的方便移动端浏览
var dicomTag = new DicomTag(Convert.ToUInt16(item.Group, 16), Convert.ToUInt16(item.Element, 16));
// 回到开头,读取 dicom
ms.Position = 0;
var dicomFile = DicomFile.Open(ms);
dataset.AddOrUpdate(dicomTag, item.ReplaceValue);
}
var numberOfFrames = dicomFile.Dataset.GetSingleValueOrDefault(DicomTag.NumberOfFrames, 1);
foreach (var item in ircFiledList)
{
//多帧处理逻辑
if (numberOfFrames > 1)
var dicomTag = new DicomTag(Convert.ToUInt16(item.Group, 16), Convert.ToUInt16(item.Element, 16));
if (dicomTag == DicomTag.ClinicalTrialProtocolID)
{
//一定要有像素数据才处理
var pixelData = DicomPixelData.Create(dicomFile.Dataset);
dataset.AddOrUpdate(DicomTag.ClinicalTrialProtocolID, trialSiteInfo.TrialCode);
if (pixelData != null)
{
try
{
}
if (dicomTag == DicomTag.ClinicalTrialSiteID)
{
dataset.AddOrUpdate(DicomTag.ClinicalTrialSiteID, trialSiteInfo.TrialSiteCode);
Log.Logger.Warning($"CallingAE:{Association.CallingAE} CalledAE:{Association.CalledAE} 开始处理多帧instanceId:{instanceId}");
var syntax = pixelData.Syntax;
// 每个 fragment 固定大小 (64KB 示例,可以自己调整)
int fragmentSize = 20 * 1024;
var frag = dicomFile.Dataset.GetDicomItem<DicomOtherByteFragment>(DicomTag.PixelData);
int fragmentCount = frag?.Fragments?.Count() ?? 0;
var originOffsetTable = frag?.OffsetTable; //有可能没有表,需要自己重建
var bot = new List<uint>();
uint botOffset = 0;
//需要拆成固定片段的
if (syntax.IsEncapsulated && fragmentCount == pixelData.NumberOfFrames && numberOfFrames > 1)
{
var newFragments = new DicomOtherByteFragment(DicomTag.PixelData);
#region test
//var newDicomFile = dicomFile.Clone();
//var newDataset = newDicomFile.Dataset;
//var dstPd = DicomPixelData.Create(newDataset, true);
//for (int i = 0; i < pixelData.NumberOfFrames; i++)
//{
// var frame = pixelData.GetFrame(i);
// dstPd.AddFrame(frame);
// var data = frame.Data;
// int offset = 0;
// while (offset < data.Length)
// {
// int size = Math.Min(fragmentSize, data.Length - offset);
// var buffer = new byte[size];
// Buffer.BlockCopy(data, offset, buffer, 0, size);
// newFragments.Fragments.Add(new MemoryByteBuffer(buffer));
// offset += size;
// }
//}
//var newOffsetTable = newDataset.GetDicomItem<DicomOtherByteFragment>(DicomTag.PixelData).OffsetTable;
//newFragments.OffsetTable.AddRange(newOffsetTable.ToArray());
#endregion
#region test fo-dicom auto bot
//var newDicomFile = dicomFile.Clone();
//var newDataset = newDicomFile.Dataset;
//var dstPd = DicomPixelData.Create(newDataset, true);
//for (int i = 0; i < pixelData.NumberOfFrames; i++)
//{
// var frame = pixelData.GetFrame(i);
// dstPd.AddFrame(frame);
//}
//var newOffsetTable = newDataset.GetDicomItem<DicomOtherByteFragment>(DicomTag.PixelData).OffsetTable;
//Console.WriteLine(newOffsetTable.ToJsonStr());
#endregion
#region 最终使用
for (int n = 0; n < pixelData.NumberOfFrames; n++)
{
var frameData = pixelData.GetFrame(n); // 获取完整一帧
var data = frameData.Data;
int offset = 0;
bot.Add(botOffset);
botOffset += (uint)data.Length;
while (offset < data.Length)
{
botOffset += 8;
int size = Math.Min(fragmentSize, data.Length - offset);
var buffer = new byte[size];
Buffer.BlockCopy(data, offset, buffer, 0, size);
newFragments.Fragments.Add(new MemoryByteBuffer(buffer));
offset += size;
}
}
//保留原始偏移表
if (originOffsetTable.Count == pixelData.NumberOfFrames)
{
newFragments.OffsetTable.AddRange(originOffsetTable.ToArray());
}
else
{
newFragments.OffsetTable.AddRange(bot.ToArray());
//Console.WriteLine(bot.ToJsonStr());
}
#endregion
dicomFile.Dataset.AddOrUpdate(newFragments);
// 重新保存 dicom 到流
ms.SetLength(0);
dicomFile.Save(ms);
}
//传递过来的就是拆分的,但是是没有偏移表的,我需要自己创建偏移表,不然生成缩略图失败
else if (syntax.IsEncapsulated && fragmentCount > pixelData.NumberOfFrames && originOffsetTable.Count == 0)
{
uint offset = 0;
bot.Add(offset);
var fistSize = frag.Fragments.FirstOrDefault()?.Size ?? 0;
//和上一个大小不一样
var isDiffrentBefore = false;
uint count = 0;
// 假设你知道每帧对应的 fragment 数量
foreach (var frameFragments in frag.Fragments)
{
count++;
if (frameFragments.Size == fistSize)
{
isDiffrentBefore = false;
// 累加这一帧所有 fragment 的大小
offset += (uint)frameFragments.Size;
continue;
}
else
{
offset += (uint)frameFragments.Size;
isDiffrentBefore = true;
}
if (isDiffrentBefore)
{
//每个Fragment 也占用字节
offset += 8 * count;
bot.Add(offset);
count = 0;
}
}
bot.RemoveAt(bot.Count - 1);
// 设置到新的 PixelData
frag.OffsetTable.AddRange(bot.ToArray());
// 重新保存 DICOM 到流
ms.SetLength(0);
dicomFile.Save(ms);
}
}
catch (Exception mutiEx)
{
Log.Logger.Warning($"CallingAE:{Association.CallingAE} CalledAE:{Association.CalledAE} 处理多帧失败,上传原始文件:{mutiEx.ToString()}");
}
}
}
if (dicomTag == DicomTag.ClinicalTrialSubjectID)
{
dataset.AddOrUpdate(DicomTag.ClinicalTrialSubjectID, "");
}
if (dicomTag == DicomTag.ClinicalTrialTimePointID)
{
dataset.AddOrUpdate(DicomTag.ClinicalTrialTimePointID, "");
}
if (dicomTag == DicomTag.PatientID)
{
var pid = dataset.GetSingleValueOrDefault(DicomTag.PatientID, string.Empty);
dataset.AddOrUpdate(DicomTag.PatientID, trialSiteInfo.TrialCode + "-" + pid);
}
}
#endregion
// 构造 DicomFile不用 Open
var dicomFile = new DicomFile(dataset);
#region 1帧拆成多个固定大小的方便移动端浏览
var numberOfFrames = dicomFile.Dataset.GetSingleValueOrDefault(DicomTag.NumberOfFrames, 1);
//多帧处理逻辑
if (numberOfFrames > 1)
{
//一定要有像素数据才处理
var pixelData = DicomPixelData.Create(dicomFile.Dataset);
#endregion
#region 本地测试
//// --- 保存到本地文件测试 ---
//var localPath = @"D:\TestDicom.dcm";
//using (var fs = new FileStream(localPath, FileMode.Create, FileAccess.Write))
//{
// ms.CopyTo(fs);
//}
//return new DicomCStoreResponse(request, DicomStatus.Success);
#endregion
ms.Position = 0;
//irc 从路径最后一截取Guid
storeRelativePath = await ossService.UploadToOSSAsync(ms, ossFolderPath, instanceId.ToString(), false);
fileSize = ms.Length;
var @lock = _distributedLockProvider.CreateLock($"{studyInstanceUid}");
using (await @lock.AcquireAsync())
if (pixelData != null)
{
try
{
var scpStudyId = await dicomArchiveService.ArchiveDicomFileAsync(dicomFile, _trialId, _trialSiteId, storeRelativePath, Association.CallingAE, Association.CalledAE, fileSize);
Log.Logger.Warning($"CallingAE:{Association.CallingAE} CalledAE:{Association.CalledAE} 开始处理多帧instanceId:{instanceId}");
var syntax = pixelData.Syntax;
// 每个 fragment 固定大小 (64KB 示例,可以自己调整)
int fragmentSize = 20 * 1024;
var series = await _seriesRepository.FirstOrDefaultAsync(t => t.Id == seriesId);
//没有缩略图
if (series != null && string.IsNullOrEmpty(series.ImageResizePath))
var frag = dicomFile.Dataset.GetDicomItem<DicomOtherByteFragment>(DicomTag.PixelData);
int fragmentCount = frag?.Fragments?.Count() ?? 0;
var originOffsetTable = frag?.OffsetTable; //有可能没有表,需要自己重建
var bot = new List<uint>();
uint botOffset = 0;
//需要拆成固定片段的
if (syntax.IsEncapsulated && fragmentCount == pixelData.NumberOfFrames && numberOfFrames > 1)
{
// 生成缩略图
using (var memoryStream = new MemoryStream())
var newFragments = new DicomOtherByteFragment(DicomTag.PixelData);
#region test
//var newDicomFile = dicomFile.Clone();
//var newDataset = newDicomFile.Dataset;
//var dstPd = DicomPixelData.Create(newDataset, true);
//for (int i = 0; i < pixelData.NumberOfFrames; i++)
//{
// var frame = pixelData.GetFrame(i);
// dstPd.AddFrame(frame);
// var data = frame.Data;
// int offset = 0;
// while (offset < data.Length)
// {
// int size = Math.Min(fragmentSize, data.Length - offset);
// var buffer = new byte[size];
// Buffer.BlockCopy(data, offset, buffer, 0, size);
// newFragments.Fragments.Add(new MemoryByteBuffer(buffer));
// offset += size;
// }
//}
//var newOffsetTable = newDataset.GetDicomItem<DicomOtherByteFragment>(DicomTag.PixelData).OffsetTable;
//newFragments.OffsetTable.AddRange(newOffsetTable.ToArray());
#endregion
#region test fo-dicom auto bot
//var newDicomFile = dicomFile.Clone();
//var newDataset = newDicomFile.Dataset;
//var dstPd = DicomPixelData.Create(newDataset, true);
//for (int i = 0; i < pixelData.NumberOfFrames; i++)
//{
// var frame = pixelData.GetFrame(i);
// dstPd.AddFrame(frame);
//}
//var newOffsetTable = newDataset.GetDicomItem<DicomOtherByteFragment>(DicomTag.PixelData).OffsetTable;
//Console.WriteLine(newOffsetTable.ToJsonStr());
#endregion
#region 最终使用
for (int n = 0; n < pixelData.NumberOfFrames; n++)
{
DicomImage image = new DicomImage(dicomFile.Dataset);
var frameData = pixelData.GetFrame(n); // 获取完整一帧
var data = frameData.Data;
int offset = 0;
var sharpimage = image.RenderImage().AsSharpImage();
sharpimage.Save(memoryStream, new JpegEncoder());
bot.Add(botOffset);
// 上传缩略图到 OSS
var seriesPath = await ossService.UploadToOSSAsync(memoryStream, ossFolderPath, $"{seriesId.ToString()}_{instanceId.ToString()}.preview.jpg", false);
botOffset += (uint)data.Length;
series.ImageResizePath = seriesPath;
while (offset < data.Length)
{
botOffset += 8;
int size = Math.Min(fragmentSize, data.Length - offset);
var buffer = new byte[size];
Buffer.BlockCopy(data, offset, buffer, 0, size);
newFragments.Fragments.Add(new MemoryByteBuffer(buffer));
offset += size;
}
}
//保留原始偏移表
if (originOffsetTable.Count == pixelData.NumberOfFrames)
{
newFragments.OffsetTable.AddRange(originOffsetTable.ToArray());
}
else
{
newFragments.OffsetTable.AddRange(bot.ToArray());
//Console.WriteLine(bot.ToJsonStr());
}
#endregion
dicomFile.Dataset.AddOrUpdate(newFragments);
}
await _seriesRepository.SaveChangesAsync();
if (_ImageUploadList.Any(t => t.StudyInstanceUid == studyInstanceUid))
//传递过来的就是拆分的,但是是没有偏移表的,我需要自己创建偏移表,不然生成缩略图失败
else if (syntax.IsEncapsulated && fragmentCount > pixelData.NumberOfFrames && originOffsetTable.Count == 0)
{
var find = _ImageUploadList.FirstOrDefault(t => t.StudyInstanceUid.Equals(studyInstanceUid));
find.SuccessImageCount++;
if (!find.PatientNameList.Any(t => t == patientIdStr) && patientIdStr.IsNotNullOrEmpty())
uint offset = 0;
bot.Add(offset);
var fistSize = frag.Fragments.FirstOrDefault()?.Size ?? 0;
//和上一个大小不一样
var isDiffrentBefore = false;
uint count = 0;
// 假设你知道每帧对应的 fragment 数量
foreach (var frameFragments in frag.Fragments)
{
find.PatientNameList.Add(patientIdStr);
count++;
if (frameFragments.Size == fistSize)
{
isDiffrentBefore = false;
// 累加这一帧所有 fragment 的大小
offset += (uint)frameFragments.Size;
continue;
}
else
{
offset += (uint)frameFragments.Size;
isDiffrentBefore = true;
}
if (isDiffrentBefore)
{
//每个Fragment 也占用字节
offset += 8 * count;
bot.Add(offset);
count = 0;
}
}
//首次 默认是Guid 空数据库归档出了Id
if (find.SCPStudyId != scpStudyId)
{
find.SCPStudyId = scpStudyId;
bot.RemoveAt(bot.Count - 1);
// 设置到新的 PixelData
frag.OffsetTable.AddRange(bot.ToArray());
}
}
//监控信息设置
_upload.FileCount++;
_upload.FileSize = _upload.FileSize + fileSize;
}
catch (Exception ex)
catch (Exception mutiEx)
{
Log.Logger.Warning($"CallingAE:{Association.CallingAE} CalledAE:{Association.CalledAE} 处理多帧失败,上传原始文件:{mutiEx.ToString()}");
}
}
}
#endregion
#region 本地测试
//// --- 保存到本地文件测试 ---
//var localPath = @"D:\TestDicom.dcm";
//using (var fs = new FileStream(localPath, FileMode.Create, FileAccess.Write))
//{
// ms.CopyTo(fs);
//}
//return new DicomCStoreResponse(request, DicomStatus.Success);
#endregion
// 直接写入内存
await using var ms = new MemoryStream();
await dicomFile.SaveAsync(ms);
ms.Position = 0;
//irc 从路径最后一截取Guid
storeRelativePath = await ossService.UploadToOSSAsync(ms, ossFolderPath, instanceId.ToString(), false);
fileSize = ms.Length;
var @lock = _distributedLockProvider.CreateLock($"{studyInstanceUid}");
using (await @lock.AcquireAsync())
{
try
{
var scpStudyId = await dicomArchiveService.ArchiveDicomFileAsync(dicomFile, _trialId, _trialSiteId, storeRelativePath, Association.CallingAE, Association.CalledAE, fileSize);
var series = await _seriesRepository.FirstOrDefaultAsync(t => t.Id == seriesId);
//没有缩略图
if (series != null && string.IsNullOrEmpty(series.ImageResizePath))
{
Log.Logger.Warning($"CallingAE:{Association.CallingAE} CalledAE:{Association.CalledAE} 传输处理异常:{ex.ToString()}");
if (_ImageUploadList.Any(t => t.StudyInstanceUid == studyInstanceUid))
// 生成缩略图
using (var memoryStream = new MemoryStream())
{
var find = _ImageUploadList.FirstOrDefault(t => t.StudyInstanceUid.Equals(studyInstanceUid));
DicomImage image = new DicomImage(dicomFile.Dataset);
find.FailedImageCount++;
var sharpimage = image.RenderImage().AsSharpImage();
sharpimage.Save(memoryStream, new JpegEncoder());
// 上传缩略图到 OSS
var seriesPath = await ossService.UploadToOSSAsync(memoryStream, ossFolderPath, $"{seriesId.ToString()}_{instanceId.ToString()}.preview.jpg", false);
series.ImageResizePath = seriesPath;
}
}
await _seriesRepository.SaveChangesAsync();
if (_ImageUploadList.Any(t => t.StudyInstanceUid == studyInstanceUid))
{
var find = _ImageUploadList.FirstOrDefault(t => t.StudyInstanceUid.Equals(studyInstanceUid));
find.SuccessImageCount++;
if (!find.PatientNameList.Any(t => t == patientIdStr) && patientIdStr.IsNotNullOrEmpty())
{
find.PatientNameList.Add(patientIdStr);
}
//首次 默认是Guid 空数据库归档出了Id
if (find.SCPStudyId != scpStudyId)
{
find.SCPStudyId = scpStudyId;
}
}
//监控信息设置
_upload.FileCount++;
_upload.FileSize = _upload.FileSize + fileSize;
}
catch (Exception ex)
{
Log.Logger.Warning($"CallingAE:{Association.CallingAE} CalledAE:{Association.CalledAE} 传输处理异常:{ex.ToString()}");
if (_ImageUploadList.Any(t => t.StudyInstanceUid == studyInstanceUid))
{
var find = _ImageUploadList.FirstOrDefault(t => t.StudyInstanceUid.Equals(studyInstanceUid));
find.FailedImageCount++;
}
}
}
Log.Logger.Information($"CallingAE:{Association.CallingAE} CalledAE:{Association.CalledAE} {request.SOPInstanceUID} 上传完成 ");

View File

@ -22,11 +22,7 @@ namespace IRaCIS.Core.SCP.Service
public class DicomArchiveService(IRepository<SCPPatient> _patientRepository,
IRepository<SCPStudy> _studyRepository,
IRepository<SCPSeries> _seriesRepository,
IRepository<SCPInstance> _instanceRepository,
IDistributedLockProvider _distributedLockProvider,
IRepository<SystemAnonymization> _systemAnonymizationRepository,
IRepository<TrialSite> _trialSiteRepository,
IFusionCache _fusionCache
IRepository<SCPInstance> _instanceRepository
) : BaseService, IDicomArchiveService
{
@ -45,66 +41,6 @@ namespace IRaCIS.Core.SCP.Service
var dataset = dicomFile.Dataset;
#region 匿名化
var anonymizeList = await _fusionCache.GetOrSetAsync(CacheKeys.SystemAnonymization, _ => CacheHelper.GetSystemAnonymizationListAsync(_systemAnonymizationRepository), TimeSpan.FromDays(7));
var trialSiteInfo = await _fusionCache.GetOrSetAsync(CacheKeys.TrialSiteInfo(trialSiteId), _ => CacheHelper.GetTrialSiteInfo(trialSiteId, _trialSiteRepository), TimeSpan.FromMinutes(2));
var fixedFiledList = anonymizeList.Where(t => t.IsFixed).ToList();
var ircFiledList = anonymizeList.Where(t => t.IsFixed == false).ToList();
foreach (var item in fixedFiledList)
{
var dicomTag = new DicomTag(Convert.ToUInt16(item.Group, 16), Convert.ToUInt16(item.Element, 16));
dataset.AddOrUpdate(dicomTag, item.ReplaceValue);
}
foreach (var item in ircFiledList)
{
var dicomTag = new DicomTag(Convert.ToUInt16(item.Group, 16), Convert.ToUInt16(item.Element, 16));
if (dicomTag == DicomTag.ClinicalTrialProtocolID)
{
dataset.AddOrUpdate(dicomTag, trialSiteInfo.TrialCode);
}
if (dicomTag == DicomTag.ClinicalTrialSiteID)
{
dataset.AddOrUpdate(dicomTag, trialSiteInfo.TrialSiteCode);
}
if (dicomTag == DicomTag.ClinicalTrialSubjectID)
{
dataset.AddOrUpdate(dicomTag, "");
}
if (dicomTag == DicomTag.ClinicalTrialTimePointID)
{
dataset.AddOrUpdate(dicomTag, "");
}
if (dicomTag == DicomTag.PatientID)
{
var pid = dataset.GetSingleValueOrDefault(DicomTag.PatientID, string.Empty);
dataset.AddOrUpdate(dicomTag, trialSiteInfo.TrialCode + "_" + pid);
}
}
#endregion
string studyInstanceUid = dataset.GetString(DicomTag.StudyInstanceUID);
string seriesInstanceUid = dataset.GetString(DicomTag.SeriesInstanceUID);
string sopInstanceUid = dataset.GetString(DicomTag.SOPInstanceUID);
@ -293,6 +229,7 @@ namespace IRaCIS.Core.SCP.Service
findStudy.DicomStudyTime = dataset.GetSingleValueOrDefault(DicomTag.StudyTime, string.Empty);
findStudy.CalledAE = calledAE;
findStudy.CallingAE = callingAE;
findStudy.PatientIdStr = patientIdStr;
findStudy.PatientName = dataset.GetSingleValueOrDefault(DicomTag.PatientName, string.Empty);
findStudy.PatientSex = dataset.GetSingleValueOrDefault(DicomTag.PatientSex, string.Empty);
findStudy.PatientAge = dataset.GetSingleValueOrDefault(DicomTag.PatientAge, string.Empty);

View File

@ -96,6 +96,12 @@ builder.Services.AddJsonLocalization(options => options.ResourcesPath = "Resourc
// 异常、参数统一验证过滤器、Json序列化配置、字符串参数绑型统一Trim()
builder.Services.AddControllers(options =>
{
// 关键配置:禁用不可空引用类型的自动 Required 验证
options.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = true;
// 插到最前,抢在默认绑定器之前
//options.ModelBinderProviders.Insert(0, new SpecificNullableBinderProvider());
options.Filters.Add<ModelActionFilter>();
options.Filters.Add<ProjectExceptionFilter>();
options.Filters.Add<UnitOfWorkFilter>();

View File

@ -71,29 +71,27 @@ namespace IRaCIS.Core.API
DateTime dateTime;
if (reader.ValueType == typeof(DateTime) || reader.ValueType == typeof(DateTime?))
// 2. 检查目标类型是否可空
bool isNullable = objectType == typeof(DateTime?);
var canConvert = DateTime.TryParse(reader.Value?.ToString(), out dateTime);
if (isNullable == false && canConvert == false)
{
DateTime? nullableDateTime = reader.Value as DateTime?;
if (nullableDateTime != null && nullableDateTime.HasValue)
{
dateTime = nullableDateTime.Value;
}
else
{
return null;
}
throw new JsonSerializationException($"Could not convert string to DateTime: {reader.Value} Path {reader.Path}");
}
else
{
if (DateTime.TryParse((string)reader.Value, out dateTime) == false)
if (canConvert == false)
{
return null;
}
}
////如果前端传递带时区的那么转换会报错需要DateTimeKind.Unspecified
//dateTime = DateTime.SpecifyKind(dateTime, DateTimeKind.Unspecified);
// 将客户端时间转换为服务器时区的时间
var serverZoneTime = TimeZoneInfo.ConvertTime(dateTime, _clientTimeZone, TimeZoneInfo.Local);
@ -113,7 +111,10 @@ namespace IRaCIS.Core.API
//第一个参数默认使用系统本地时区 也就是应用服务器的时区
DateTime clientZoneTime = TimeZoneInfo.ConvertTime(nullableDateTime.Value, _clientTimeZone);
//writer.WriteValue(clientZoneTime);
//// 最简单的方式:创建 DateTimeOffset
//DateTimeOffset dateTimeOffset = new DateTimeOffset(clientZoneTime, _clientTimeZone.GetUtcOffset(clientZoneTime));
//writer.WriteValue(dateTimeOffset.ToString("yyyy-MM-dd HH:mm:sszzz"));
writer.WriteValue(clientZoneTime.ToString(_dateFormat));
}

View File

@ -3,8 +3,10 @@ using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Reflection.Metadata;
namespace IRaCIS.Core.API
{
@ -51,34 +53,90 @@ namespace IRaCIS.Core.API
}
public class NullToEmptyStringValueProvider : IValueProvider
{
PropertyInfo _MemberInfo;
PropertyInfo _memberInfo;
private readonly bool _isNullable;
public NullToEmptyStringValueProvider(PropertyInfo memberInfo)
{
_MemberInfo = memberInfo;
_memberInfo = memberInfo;
}
// DTO → JSON 返回前端的时候 处理null 变为"" 方便前端判断
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<Int32>) && result == null) result = 0;
//else if (_MemberInfo.PropertyType == typeof(Nullable<Decimal>) && result == null) result = 0.00M;
var result = _memberInfo.GetValue(target);
// 检查类型是否为string或string?
if (_memberInfo.PropertyType == typeof(string) && result == null)
{
#region 返回的时候处理 string string? null 为"" 不区分处理
//// 如果是string?类型返回null 可以做到,但是得反射,因为 string string? 是编译层区分的
//var isNullable1 = _memberInfo.CustomAttributes
// .Any(a => a.AttributeType.Name == "NullableAttribute");
////var isNullable2 = _memberInfo.CustomAttributes
//// .Any(a => a.AttributeType.FullName == "System.Runtime.CompilerServices.NullableAttribute");
//if (isNullable1)
//{
// return result;
//}
#endregion
// 如果是string类型返回空字符串
return string.Empty;
}
return result;
}
//影响 模型绑定时接收前端的值,同时影响 正常 JSON 序列化
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()*/);
_memberInfo.SetValue(target, value);
#region 前端针对 string 类型的变量如果传递null 会报错必传
//if (_memberInfo.PropertyType == typeof(string))
//{
// _memberInfo.SetValue(target, value == null ? string.Empty : value);
//}
//else
//{
// _memberInfo.SetValue(target, value);
//}
#endregion
#region 处理模型验证区分 string string?
//////接收模型的时候 定义的明明是string 但是上面也有该属性,判断不准的 比如阅片跟踪列表查询
//var isNullable1 = _memberInfo.CustomAttributes.Any(a => a.AttributeType.Name == "NullableAttribute");
////不影响 string? 传递null 变为""
//if (_memberInfo.PropertyType == typeof(string) && isNullable1 == false)
//{
// //如果不处理 前段传递null string不会接收说前段没传递会验证报错
// _memberInfo.SetValue(target, value == null ? string.Empty : value);
//}
//else
//{
// _memberInfo.SetValue(target, value);
//}
#endregion
}
else
{
_MemberInfo.SetValue(target, value);
}
}
}

View File

@ -57,8 +57,8 @@
// 1 Elevate 2 Extensive
"TemplateType": 2,
//MFA
"UserMFAVerifyDays": 1
"UserMFAVerifyMinutes": 1440
},
"SystemEmailSendConfig": {
"Port": 465,

View File

@ -56,7 +56,7 @@
// 1 Elevate 2 Extensive
"TemplateType": 2,
//MFA
"UserMFAVerifyDays": 1
"UserMFAVerifyMinutes": 1440
},
"SystemEmailSendConfig": {

View File

@ -59,8 +59,8 @@
"TemplateType": 1,
"OpenTrialRelationDelete": false,
//MFA
"UserMFAVerifyDays": 1
//MFA
"UserMFAVerifyMinutes": 1440
},

View File

@ -69,7 +69,8 @@
"TemplateType": 1,
"OpenLoginMFA": true,
//MFA
"UserMFAVerifyDays": 1
"UserMFAVerifyMinutes": 1440
},
"SystemEmailSendConfig": {

View File

@ -68,7 +68,7 @@
// 1 Elevate 2 Extensive
"TemplateType": 1,
//MFA
"UserMFAVerifyDays": 1
"UserMFAVerifyMinutes": 1440
},
"SystemEmailSendConfig": {

View File

@ -75,7 +75,7 @@
// 1 Elevate 2 Extensive
"TemplateType": 2,
//MFA
"UserMFAVerifyDays": 1
"UserMFAVerifyMinutes": 1440
},
"SystemEmailSendConfig": {

View File

@ -1,7 +1,11 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.Extensions.Localization;
using Newtonsoft.Json;
using System.Reflection;
namespace IRaCIS.Core.Application.Filter;
@ -28,3 +32,7 @@ public class ModelActionFilter(IStringLocalizer _localizer) : ActionFilterAttrib
}
}
}

View File

@ -46,7 +46,7 @@ public class ProjectExceptionFilter(ILogger<ProjectExceptionFilter> _logger, ISt
else
{
context.Result = new JsonResult(ResponseOutput.NotOk(_localizer["Project_ExceptionContactDeveloper"] + (exception.InnerException is null ? (exception.Message)
: (exception.InnerException?.Message )), ApiResponseCodeEnum.ProgramException));
: (exception.Message + "Inner ExceptionMsg:" + exception.InnerException?.Message)), ApiResponseCodeEnum.ProgramException));
}

View File

@ -64,6 +64,8 @@ public static class CacheKeys
//每个用户 每个浏览器独立时间
public static string UserMFAVerifyPass(Guid userId,string browserFingerprint) => $"UserMFAVerifyPass:{userId}:{browserFingerprint}";
public static string UserMFATag(Guid userId) => $"UserMFAVerifyPass:{userId}";
}
public static class CacheHelper

View File

@ -1,4 +1,5 @@
using FellowOakDicom;
using DocumentFormat.OpenXml.Office.CustomUI;
using FellowOakDicom;
using FellowOakDicom.Media;
using System;
using System.Collections.Generic;
@ -57,6 +58,7 @@ namespace IRaCIS.Core.Application.Helper
var mappings = new List<string>();
int index = 1;
var studyUid=list.FirstOrDefault()?.StudyInstanceUid;
var dicomDir = new DicomDirectory();
@ -128,7 +130,9 @@ namespace IRaCIS.Core.Application.Helper
// 重置流位置
memoryStream.Position = 0;
await _oSSService.UploadToOSSAsync(memoryStream, ossFolder, "DICOMDIR", false);
var relativePath= await _oSSService.UploadToOSSAsync(memoryStream, ossFolder, "DICOMDIR", true);
dic.Add($"{studyUid}_DICOMDIR" , relativePath.Split('/').Last());
}
//清理临时文件

View File

@ -18,6 +18,7 @@ using NPOI.HSSF.UserModel;
using NPOI.SS.Formula.Functions;
using NPOI.SS.UserModel;
using NPOI.XSSF.UserModel;
using SharpCompress.Common;
using System.Collections;
using System.Globalization;
using Xceed.Document.NET;
@ -870,17 +871,16 @@ public static class ExcelExportHelper
//模板路径
var tplPath = physicalPath;
#region 根据中英文 删除模板sheet
// 打开模板文件
var templateFile = new FileStream(tplPath, FileMode.Open, FileAccess.Read);
// 获取文件流
var templateStream = new MemoryStream();
templateFile.CopyTo(templateStream);
templateStream.Seek(0, SeekOrigin.Begin);
var workbook = new XSSFWorkbook(templateStream);
#region 根据中英文 删除模板sheet
// 打开模板文件
var templateFileStream = new FileStream(tplPath, FileMode.Open, FileAccess.Read);
var workbook = new XSSFWorkbook(templateFileStream);
int sheetCount = workbook.NumberOfSheets;
@ -985,14 +985,8 @@ public static class ExcelExportHelper
}
}
using (var memoryStream2 = new MemoryStream())
{
workbook.Write(memoryStream2, true);
memoryStream2.Seek(0, SeekOrigin.Begin);
templateStream = memoryStream2;
}
workbook.Write(templateStream, leaveOpen: true);
templateStream.Position = 0;
}
@ -1099,6 +1093,63 @@ public static class ExcelExportHelper
//模板路径
var tplPath = physicalPath;
var templateStream = new MemoryStream();
#region npoi 移除某一行
var isEn_US = CultureInfo.CurrentCulture.Name == StaticData.CultureInfo.en_US;
if (isEn_US)
{
// 打开模板文件
using var templateFileStream = new FileStream(tplPath, FileMode.Open, FileAccess.Read);
using var workbook = new XSSFWorkbook(templateFileStream);
int sheetCount = workbook.NumberOfSheets;
int removeRowIndex = 1; // 要删除的行0-based
for (int i = 0; i < sheetCount; i++)
{
var sheet = workbook.GetSheetAt(i);
// 2 删除行
var row = sheet.GetRow(removeRowIndex);
if (row != null)
{
sheet.RemoveRow(row);
}
// 3 上移后续行
if (removeRowIndex < sheet.LastRowNum)
{
sheet.ShiftRows(
removeRowIndex + 1,
sheet.LastRowNum,
-1,
true, // copyRowHeight
false // resetOriginalRowHeight
);
}
}
workbook.Write(templateStream, leaveOpen: true);
templateStream.Position = 0;
}
else
{
using (var fs = new FileStream(tplPath, FileMode.Open, FileAccess.Read))
{
fs.CopyTo(templateStream);
}
templateStream.Position = 0;
}
#endregion
var memoryStream = new MemoryStream();
@ -1107,7 +1158,7 @@ public static class ExcelExportHelper
IgnoreTemplateParameterMissing = true,
};
await MiniExcel.SaveAsByTemplateAsync(memoryStream, tplPath, data, config);
await MiniExcel.SaveAsByTemplateAsync(memoryStream, templateStream.ToArray(), data, config);
memoryStream.Seek(0, SeekOrigin.Begin);

View File

@ -160,7 +160,7 @@ public interface IOSSService
public Task DeleteFromPrefix(string prefix, bool isCache = false);
public Task DeleteObjects(List<string> objectKeys);
public Task DeleteObjects(List<string> objectKeys, bool isCache = false);
List<string> GetRootFolderNames();
@ -249,7 +249,7 @@ public class OSSService : IOSSService
LifeCycleExpiration =
{
Days = 30
Days = 30 //最后一次修改时间
},
StorageClass = StorageClass.Archive
}
@ -302,8 +302,8 @@ public class OSSService : IOSSService
//提供awsEndPoint域名进行访问配置
var clientConfig = new AmazonS3Config
{
RegionEndpoint = RegionEndpoint.USEast1,
UseHttp = true,
RegionEndpoint = RegionEndpoint.GetBySystemName(awsConfig.Region)
//,UseHttp = true,
};
var amazonS3Client = new AmazonS3Client(credentials, clientConfig);
@ -333,15 +333,15 @@ public class OSSService : IOSSService
Transitions = new List<LifecycleTransition>
{
// 1天后转为低频访问 (Standard-IA)
new LifecycleTransition
{
Days = 1,
StorageClass = S3StorageClass.StandardInfrequentAccess // 对应S3 Standard-IA
},
//new LifecycleTransition
//{
// Days = 1, //Days' in Transition action must be greater than or equal to 30 for storageClass 'STANDARD_IA'"
// StorageClass = S3StorageClass.StandardInfrequentAccess // 对应S3 Standard-IA
//},
// 30天后转为归档 (Glacier Instant Retrieval)
new LifecycleTransition
{
Days = 30,
Days = 30, //创建时间
StorageClass = S3StorageClass.GlacierInstantRetrieval // 对应归档(即时检索)
}
// 如果需要更深的归档,可以继续添加:
@ -765,8 +765,8 @@ public class OSSService : IOSSService
//提供awsEndPoint域名进行访问配置
var clientConfig = new AmazonS3Config
{
RegionEndpoint = RegionEndpoint.USEast1,
UseHttp = true,
RegionEndpoint = RegionEndpoint.GetBySystemName(awsConfig.Region)
//,UseHttp = true,
};
var amazonS3Client = new AmazonS3Client(credentials, clientConfig);
@ -891,8 +891,8 @@ public class OSSService : IOSSService
//提供awsEndPoint域名进行访问配置
var clientConfig = new AmazonS3Config
{
RegionEndpoint = RegionEndpoint.USEast1,
UseHttp = true,
RegionEndpoint = RegionEndpoint.GetBySystemName(awsConfig.Region)
//,UseHttp = true,
};
var amazonS3Client = new AmazonS3Client(credentials, clientConfig);
@ -966,8 +966,8 @@ public class OSSService : IOSSService
//提供awsEndPoint域名进行访问配置
var clientConfig = new AmazonS3Config
{
RegionEndpoint = RegionEndpoint.USEast1,
UseHttp = true,
RegionEndpoint = RegionEndpoint.GetBySystemName(awsConfig.Region)
//,UseHttp = true,
};
var amazonS3Client = new AmazonS3Client(credentials, clientConfig);
@ -1172,8 +1172,8 @@ public class OSSService : IOSSService
//提供awsEndPoint域名进行访问配置
var clientConfig = new AmazonS3Config
{
RegionEndpoint = RegionEndpoint.USEast1,
UseHttp = true,
RegionEndpoint = RegionEndpoint.GetBySystemName(awsConfig.Region)
//,UseHttp = true,
};
var amazonS3Client = new AmazonS3Client(credentials, clientConfig);
@ -1546,8 +1546,8 @@ public class OSSService : IOSSService
//提供awsEndPoint域名进行访问配置
var clientConfig = new AmazonS3Config
{
RegionEndpoint = RegionEndpoint.USEast1,
UseHttp = true,
RegionEndpoint = RegionEndpoint.GetBySystemName(awsConfig.Region)
//,UseHttp = true,
};
var amazonS3Client = new AmazonS3Client(credentials, clientConfig);
@ -1561,7 +1561,7 @@ public class OSSService : IOSSService
var listObjectsResponse = await amazonS3Client.ListObjectsV2Async(listObjectsRequest);
if (listObjectsResponse.S3Objects.Count > 0)
if (listObjectsResponse.S3Objects?.Count > 0)
{
// 准备删除请求
var deleteObjectsRequest = new Amazon.S3.Model.DeleteObjectsRequest
@ -1591,7 +1591,7 @@ public class OSSService : IOSSService
}
}
public async Task DeleteObjects(List<string> objectKeys)
public async Task DeleteObjects(List<string> objectKeys, bool isCache = false)
{
GetObjectStoreTempToken();
@ -1601,9 +1601,23 @@ public class OSSService : IOSSService
var _ossClient = new OssClient(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? aliConfig.EndPoint : aliConfig.InternalEndpoint, AliyunOSSTempToken.AccessKeyId, AliyunOSSTempToken.AccessKeySecret, AliyunOSSTempToken.SecurityToken);
var bucketName = string.Empty;
if (isCache)
{
Uri uri = new Uri(aliConfig.ViewEndpoint);
string host = uri.Host; // 获取 "zy-irc-test-dev-cache.oss-cn-shanghai.aliyuncs.com"
string[] parts = host.Split('.');
bucketName = parts[0];
}
else
{
bucketName = aliConfig.BucketName;
}
if (objectKeys.Count > 0)
{
var result = _ossClient.DeleteObjects(new Aliyun.OSS.DeleteObjectsRequest(aliConfig.BucketName, objectKeys, false));
var result = _ossClient.DeleteObjects(new Aliyun.OSS.DeleteObjectsRequest(bucketName, objectKeys, false));
}
}
@ -1639,8 +1653,8 @@ public class OSSService : IOSSService
//提供awsEndPoint域名进行访问配置
var clientConfig = new AmazonS3Config
{
RegionEndpoint = RegionEndpoint.USEast1,
UseHttp = true,
RegionEndpoint = RegionEndpoint.GetBySystemName(awsConfig.Region)
//,UseHttp = true,
};
var amazonS3Client = new AmazonS3Client(credentials, clientConfig);
@ -1714,8 +1728,8 @@ public class OSSService : IOSSService
//提供awsEndPoint域名进行访问配置
var clientConfig = new AmazonS3Config
{
RegionEndpoint = RegionEndpoint.USEast1,
UseHttp = true,
RegionEndpoint = RegionEndpoint.GetBySystemName(awsConfig.Region)
//,UseHttp = true,
};
var request = new Amazon.S3.Model.GetObjectMetadataRequest
@ -1799,9 +1813,16 @@ public class OSSService : IOSSService
{
var awsOptions = ObjectStoreServiceOptions.AWS;
// 创建 STS 客户端(考虑使用 RegionEndpoint
var stsConfig = new AmazonSecurityTokenServiceConfig
{
RegionEndpoint = RegionEndpoint.GetBySystemName(awsOptions.Region)
};
//aws 临时凭证
// 创建 STS 客户端
var stsClient = new AmazonSecurityTokenServiceClient(awsOptions.AccessKeyId, awsOptions.SecretAccessKey);
var stsClient = new AmazonSecurityTokenServiceClient(awsOptions.AccessKeyId, awsOptions.SecretAccessKey, stsConfig);
// 使用 AssumeRole 请求临时凭证
var assumeRoleRequest = new AssumeRoleRequest

View File

@ -19195,6 +19195,13 @@
<param name="tpCode"></param>
<param name="key"></param>
</member>
<member name="M:IRaCIS.Core.Application.Services.SeriesService.UpdateImageResizePath(IRaCIS.Core.Application.Contracts.Dicom.DTO.UpdateImageResizeDTO)">
<summary>
更新缩略图路径
</summary>
<param name="dto"></param>
<returns></returns>
</member>
<member name="M:IRaCIS.Core.Application.Services.SeriesService.List(System.Guid,System.Nullable{System.Boolean},System.Nullable{System.Boolean})">
<summary> 指定资源Id获取Dicom检查所属序列信息列表 </summary>
<param name="studyId"> Dicom检查的Id </param>

View File

@ -17,6 +17,7 @@ using MassTransit;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System.Linq;
using System.Linq.Dynamic.Core;
using Subject = IRaCIS.Core.Domain.Models.Subject;
@ -1172,7 +1173,7 @@ public class VisitTaskService(IRepository<VisitTask> _visitTaskRepository,
var visitQuery = _visitTaskRepository.Where(x => x.TrialId == trialId && x.DoctorUserId == _userInfo.UserRoleId && x.TaskState == TaskState.Effect)
.WhereIf(inQuery.SubjectId != null, x => x.SubjectId == inQuery.SubjectId)
.WhereIf(!string.IsNullOrEmpty(subjectCode), t => (t.Subject.Code.Contains(subjectCode!) && t.IsAnalysisCreate == false) || (t.BlindSubjectCode.Contains(subjectCode!) && t.IsAnalysisCreate))
.WhereIf(critrion.CriterionType == CriterionType.OCT, t => t.ReadingCategory == ReadingCategory.Visit ? t.SourceSubjectVisit.NoneDicomStudyList.Where(t=>t.Modality=="OCT").SelectMany(t => t.ImageLabelNoneDicomFileList).Any() : true)
.WhereIf(critrion.CriterionType == CriterionType.OCT, t => t.ReadingCategory == ReadingCategory.Visit ? t.SourceSubjectVisit.NoneDicomStudyList.Where(t => t.Modality == "OCT").SelectMany(t => t.ImageLabelNoneDicomFileList).Any() : true)
.WhereIf(critrion.CriterionType == CriterionType.IVUS, t => t.ReadingCategory == ReadingCategory.Visit ? t.SourceSubjectVisit.NoneDicomStudyList.Where(t => t.Modality == "IVUS").SelectMany(t => t.ImageLabelNoneDicomFileList).Any() : true);
var visitGroupQuery = visitQuery.GroupBy(x => new { x.SubjectId, x.Subject.Code, x.BlindSubjectCode });
@ -2292,12 +2293,12 @@ public class VisitTaskService(IRepository<VisitTask> _visitTaskRepository,
foreach (var item in readingTableAnswerRowInfoList)
{
if (item.SplitRowId!=null&&lesionRelationship.ContainsKey(item.SplitRowId.Value))
if (item.SplitRowId != null && lesionRelationship.ContainsKey(item.SplitRowId.Value))
{
item.SplitRowId = lesionRelationship[item.SplitRowId.Value];
}
if (item.MergeRowId!=null&&lesionRelationship.ContainsKey(item.MergeRowId.Value))
if (item.MergeRowId != null && lesionRelationship.ContainsKey(item.MergeRowId.Value))
{
item.MergeRowId = lesionRelationship[item.MergeRowId.Value];
}
@ -2402,6 +2403,10 @@ public class VisitTaskService(IRepository<VisitTask> _visitTaskRepository,
//另一个阅片人的任务根据任务进度自动进入PM退回或PM申请重阅
filterExpression = filterExpression.And(t => t.VisitTaskNum >= task.VisitTaskNum);
//退回只影响有序的后续所有的,无序的当前访视
filterExpression = filterExpression.And(t => (t.TrialReadingCriterion.IsReadingTaskViewInOrder == ReadingOrder.InOrder) ||
(t.TrialReadingCriterion.IsReadingTaskViewInOrder != ReadingOrder.InOrder && t.SourceSubjectVisitId == task.SourceSubjectVisitId));
var influenceTaskList = await _visitTaskRepository.Where(filterExpression, true).ToListAsync();
@ -2559,9 +2564,12 @@ public class VisitTaskService(IRepository<VisitTask> _visitTaskRepository,
//删除序列数据
await _subjectCriteriaEvaluationVisitStudyFilterRepository.BatchDeleteNoTrackingAsync(t => t.TrialReadingCriterion.CriterionType == CriterionType.RECIST1Pointt1_MB && t.SubjectVisit.SubjectId == task.SubjectId && t.SubjectVisitId == task.SourceSubjectVisitId);
otherVisitIdList = otherVisitIdList.Where(t => t != task.SourceSubjectVisitId.Value).ToList();
}
//BM后续访视 ,筛选状态不变,任务生成状态重置(实际该访视任务状态 可能是重阅重置了或者失效了,需要后续生成,或者取消分配了,需要后续重新分配)
await _subjectCriteriaEvaluationVisitFilterRepository.UpdatePartialFromQueryAsync(t => t.TrialReadingCriterion.CriterionType == CriterionType.RECIST1Pointt1_MB && t.SubjectVisit.SubjectId == task.SubjectId && otherVisitIdList.Contains(t.SubjectVisitId),
t => new SubjectCriteriaEvaluationVisitFilter()
@ -2723,6 +2731,13 @@ public class VisitTaskService(IRepository<VisitTask> _visitTaskRepository,
//默认影响的都是该标准的任务
filterExpression = filterExpression.And(t => t.TrialReadingCriterionId == filterObj.TrialReadingCriterionId);
}
//退回只影响有序的后续所有的,无序的当前访视
if (isReReading == false)
{
filterExpression = filterExpression.And(t => (t.TrialReadingCriterion.IsReadingTaskViewInOrder == ReadingOrder.InOrder) ||
(t.TrialReadingCriterion.IsReadingTaskViewInOrder != ReadingOrder.InOrder && t.SourceSubjectVisitId == filterObj.SourceSubjectVisitId));
}
}

View File

@ -603,16 +603,21 @@ public class Tumor_CDISC_ExportService(IRepository<ReadingQuestionCriterionTrial
var tu = CreatNewTUExport(task, lesion, visitIndexNoDic, translateDataList, isEn_Us);
if (lesion.OrganInfoId.HasValue)
if (!tuList.Any(t => t.SubjectCode == task.SubjectCode && t.ARM_TumorNo == $"{task.ArmEnumStr}_{lesion.LessionCode}"))
{
tu.BodyPart = _userInfo.IsEn_Us ? trialOrganDic[lesion.OrganInfoId.Value].PartEN : trialOrganDic[lesion.OrganInfoId.Value].Part;
if (lesion.OrganInfoId.HasValue)
{
tu.BodyPart = _userInfo.IsEn_Us ? trialOrganDic[lesion.OrganInfoId.Value].PartEN : trialOrganDic[lesion.OrganInfoId.Value].Part;
}
Fill_Resisit_Lugano_TUExport(tu, lesion);
tuList.Add(tu);
}
Fill_Resisit_Lugano_TUExport(tu, lesion);
tuList.Add(tu);
#endregion

View File

@ -92,6 +92,10 @@ namespace IRaCIS.Core.Application.Contracts
public class UnionDocumentWithConfirmInfoView : UnionDocumentView
{
public DocUserSignType SysDocUserSignType { get; set; }
public bool IsConfirmIdentityUserInner { get; set; }
public Guid TrialId { get; set; }
public bool IsNeedSendEmial { get; set; }

View File

@ -30,6 +30,7 @@ namespace IRaCIS.Core.Application.ViewModel
public new List<UserTypeEnum> CopyUserTypeList => TrialEmailNoticeUserList.Where(t => t.EmailUserType == EmailUserType.Copy).Select(t => t.UserType).ToList();
public List<CriterionType>? SysCriterionTypeList { get; set; }
}
@ -127,7 +128,7 @@ namespace IRaCIS.Core.Application.ViewModel
{
public Guid SubjectId { get; set; }
public Guid TrialReadingCriterionId { get; set; }
public CriterionType CriterionType { get; set; }
public EmailBusinessScenario BusinessScenarioEnum { get; set; }
}

View File

@ -19,6 +19,8 @@ namespace IRaCIS.Core.Application.Contracts
Task<IResponseOutput> UserConfirm(UserConfirmCommand userConfirmCommand);
Task<List<TrialUserDto>> GetTrialUserSelect(Guid trialId);
Task<IResponseOutput<PageOutput<UnionDocumentWithConfirmInfoView>>> GetSysDocumentConfirmList(SystemDocQuery inQuery);
//Task<PageOutput<DocumentUnionWithUserStatView>> GetTrialSystemDocumentList(DocumentTrialUnionQuery querySystemDocument);
//List<TrialUserUnionDocumentView> GetTrialUserDocumentList(Guid trialId);

View File

@ -208,6 +208,7 @@ namespace IRaCIS.Core.Application.Services
await _systemDocumentRepository.BatchUpdateNoTrackingAsync(x => inDto.Ids.Contains(x.Id), x => new SystemDocument()
{
IsPublish = true,
PublishDate= DateTime.Now,
IsDeleted = false,
});
@ -301,6 +302,7 @@ namespace IRaCIS.Core.Application.Services
{
AttachmentCount=sysDoc.SystemDocumentAttachmentList.Where(z=>!z.OffLine).Count(),
IsSystemDoc = true,
IsPublish=sysDoc.IsPublish,
CurrentStaffTrainDays=sysDoc.CurrentStaffTrainDays,
NewStaffTrainDays = sysDoc.NewStaffTrainDays,
Id = sysDoc.Id,
@ -326,6 +328,7 @@ namespace IRaCIS.Core.Application.Services
//UserTypeShortName = user.UserTypeRole.UserTypeShortName
};
var list = await query
//过滤掉删除的,并且没有签名的
.Where(t => !(t.IsDeleted == true && t.ConfirmTime == null))

View File

@ -101,8 +101,9 @@ namespace IRaCIS.Core.Application.Services
await _trialDocumentRepository.UpdatePartialFromQueryAsync(x => inDto.Ids.Contains(x.Id), x => new TrialDocument()
{
IsPublish = true,
PublishDate = DateTime.Now,
IsDeleted = false,
},false,true);
}, false, true);
await _trialDocumentRepository.SaveChangesAsync();
Console.WriteLine("开始 发布项目文档");
@ -150,7 +151,7 @@ namespace IRaCIS.Core.Application.Services
{
// 从新作用域解析服务
var mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
await mediator.Publish(new ImageQCRecurringEvent { TrialId= Guid.Parse("08de2254-5d7d-581a-0242-0a0001000000") });
await mediator.Publish(new ImageQCRecurringEvent { TrialId = Guid.Parse("08de2254-5d7d-581a-0242-0a0001000000") });
}
});
return ResponseOutput.Result(true);
@ -171,7 +172,7 @@ namespace IRaCIS.Core.Application.Services
.WhereIf(inQuery.UserTypeId != null, t => t.NeedConfirmedUserTypeList.Any(t => t.NeedConfirmUserTypeId == inQuery.UserTypeId))
.WhereIf(inQuery.IsDeleted != null, t => t.IsDeleted == inQuery.IsDeleted)
.WhereIf(inQuery.IsPublish != null, t => t.IsPublish == inQuery.IsPublish)
.WhereIf(!string.IsNullOrEmpty(inQuery.FileTypeCode), t => t.FileType.Code== inQuery.FileTypeCode)
.WhereIf(!string.IsNullOrEmpty(inQuery.FileTypeCode), t => t.FileType.Code == inQuery.FileTypeCode)
.ProjectTo<TrialDocumentView>(_mapper.ConfigurationProvider, new { token = _userInfo.UserToken, isEn_Us = _userInfo.IsEn_Us });
return await trialDocumentQueryable.ToPagedListAsync(inQuery);
@ -180,7 +181,7 @@ namespace IRaCIS.Core.Application.Services
[HttpPost]
public async Task<PageOutput<TrialSignDocView>> GetTrialSignDocumentList(TrialDocQuery inQuery)
{
var trialDocQueryable = from trialDoc in _trialDocumentRepository.Where(t=>t.IsPublish)
var trialDocQueryable = from trialDoc in _trialDocumentRepository.Where(t => t.IsPublish)
.WhereIf(inQuery.TrialId != null, t => t.TrialId == inQuery.TrialId)
.Where(t => t.NeedConfirmedUserTypeList.Any(t => t.NeedConfirmUserTypeId == _userInfo.UserTypeId))
@ -353,7 +354,7 @@ namespace IRaCIS.Core.Application.Services
#region 统一用户修改
var systemDocQuery =
from sysDoc in _systemDocumentRepository.Where(t=>t.IsPublish).Where(t => t.NeedConfirmedUserTypeList.Any(c => c.NeedConfirmUserTypeId == _userInfo.UserTypeId))
from sysDoc in _systemDocumentRepository.Where(t => t.IsPublish).Where(t => t.NeedConfirmedUserTypeList.Any(c => c.NeedConfirmUserTypeId == _userInfo.UserTypeId))
//外部人员 只签署 外部需要签署的
.WhereIf(isInternal == false, t => t.DocUserSignType == DocUserSignType.InnerAndOuter)
from trialUser in _trialIdentityUserRepository.AsQueryable(false)
@ -392,7 +393,7 @@ namespace IRaCIS.Core.Application.Services
//项目文档查询
var trialDocQuery =
from trialDoc in _trialDocumentRepository.Where(t=>t.IsPublish).Where(t => t.TrialId == inQuery.TrialId).Where(t => t.NeedConfirmedUserTypeList.Any(c => c.NeedConfirmUserTypeId == _userInfo.UserTypeId))
from trialDoc in _trialDocumentRepository.Where(t => t.IsPublish).Where(t => t.TrialId == inQuery.TrialId).Where(t => t.NeedConfirmedUserTypeList.Any(c => c.NeedConfirmUserTypeId == _userInfo.UserTypeId))
from trialUser in _trialIdentityUserRepository.AsQueryable(false).Where(t => t.TrialId == inQuery.TrialId && t.IdentityUserId == _userInfo.IdentityUserId
&& t.TrialUserRoleList.Any(t => trialDoc.NeedConfirmedUserTypeList.Any(c => c.NeedConfirmUserTypeId == t.UserRole.UserTypeId)))
@ -582,7 +583,7 @@ namespace IRaCIS.Core.Application.Services
#endregion
var needSignTrialDocCount = await _trialDocumentRepository.AsQueryable(true).Where(t=>t.IsPublish)
var needSignTrialDocCount = await _trialDocumentRepository.AsQueryable(true).Where(t => t.IsPublish)
.Where(t => t.TrialId == inQuery.TrialId && t.Trial.TrialStatusStr != StaticData.TrialState.TrialStopped)
.Where(t => t.Trial.TrialIdentityUserList.Any(t => t.IdentityUserId == _userInfo.IdentityUserId && t.TrialUserRoleList.Any(t => t.UserRole.UserTypeId == _userInfo.UserTypeId)))
.Where(t => t.IsDeleted == false && !t.TrialDocConfirmedUserList.Any(t => t.ConfirmUserId == _userInfo.IdentityUserId && t.ConfirmTime != null) && t.NeedConfirmedUserTypeList.Any(u => u.NeedConfirmUserTypeId == _userInfo.UserTypeId))
@ -713,7 +714,8 @@ namespace IRaCIS.Core.Application.Services
.WhereIf(inQuery.BeginCreateTime != null, t => t.CreateTime >= inQuery.BeginCreateTime)
.WhereIf(inQuery.EndCreateTime != null, t => t.CreateTime <= inQuery.EndCreateTime)
.WhereIf(!string.IsNullOrEmpty(inQuery.UserName), t => t.UserName.Contains(inQuery.UserName))
.WhereIf(inQuery.IsDeleted != null, t => t.IsDeleted == inQuery.IsDeleted);
.WhereIf(inQuery.IsDeleted != null, t => t.IsDeleted == inQuery.IsDeleted)
.WhereIf(_userInfo.UserTypeEnumInt == (int)UserTypeEnum.EA, t => t.ConfirmTime != null);
var result = await unionQuery.ToPagedListAsync(inQuery);
@ -945,15 +947,18 @@ namespace IRaCIS.Core.Application.Services
var isEA = _userInfo.UserTypeEnumInt == (int)UserTypeEnum.EA;
//EA 但是没有在进行的培训记录查看权限,那么返回空数据
if (isEA && !_auditRecordRepository.Any(t => t.IsViewTrainingRecord && t.AuditState == AuditState.Ongoing && t.AuditRecordIdentityUserList.Any(c=>c.IdentityUserId==_userInfo.IdentityUserId)))
if (isEA && !_auditRecordRepository.Any(t => t.IsViewTrainingRecord && t.AuditState == AuditState.Ongoing && t.AuditRecordIdentityUserList.Any(c => c.IdentityUserId == _userInfo.IdentityUserId)))
{
return ResponseOutput.Ok(new PageOutput<UnionDocumentWithConfirmInfoView>());
}
var systemDocQuery =
from sysDoc in _systemDocumentRepository.AsQueryable(false)
from sysDoc in _systemDocumentRepository.Where(t => t.IsPublish)
.Where(t => inQuery.UserTypeId != null ? t.NeedConfirmedUserTypeList.Any(t => t.NeedConfirmUserTypeId == inQuery.UserTypeId) : true)
from identityUser in _identityUserRepository.AsQueryable(false).Where(t => t.Status == UserStateEnum.Enable && t.UserRoleList.Where(t => t.IsUserRoleDisabled == false).Any(t => sysDoc.NeedConfirmedUserTypeList.AsQueryable().Any(c => c.NeedConfirmUserTypeId == t.UserTypeId)))
from identityUser in _identityUserRepository.AsQueryable(false)
.Where(t => t.Status == UserStateEnum.Enable && t.UserRoleList.Where(t => t.IsUserRoleDisabled == false).Any(t => sysDoc.NeedConfirmedUserTypeList.AsQueryable().Any(c => c.NeedConfirmUserTypeId == t.UserTypeId)))
.Where(t => inQuery.UserId != null ? t.Id == inQuery.UserId : true)
.Where(t => inQuery.UserTypeId != null ? t.UserRoleList.Any(t => t.UserTypeId == inQuery.UserTypeId && t.IsUserRoleDisabled == false) : true)
.Where(t => isEA ? t.IsZhiZhun == true : true) //EA 只能查看内部人员文档
@ -962,6 +967,9 @@ namespace IRaCIS.Core.Application.Services
select new UnionDocumentWithConfirmInfoView()
{
IsSystemDoc = true,
SysDocUserSignType = sysDoc.DocUserSignType,
IsConfirmIdentityUserInner = identityUser.IsZhiZhun,
IsPublish=sysDoc.IsPublish,
Id = sysDoc.Id,
CreateTime = sysDoc.CreateTime,
IsDeleted = sysDoc.IsDeleted,
@ -993,14 +1001,16 @@ namespace IRaCIS.Core.Application.Services
};
var unionQuery = systemDocQuery.IgnoreQueryFilters().Where(t => !(t.IsDeleted == true && t.ConfirmTime == null))
//外部人员 只签署 外部需要签署的
.Where(t => t.IsConfirmIdentityUserInner == false ? t.SysDocUserSignType == DocUserSignType.InnerAndOuter : true)
.WhereIf(!string.IsNullOrEmpty(inQuery.Name), t => t.Name.Contains(inQuery.Name))
.WhereIf(inQuery.FileTypeId != null, t => t.FileTypeId == inQuery.FileTypeId)
.WhereIf(inQuery.IsConfirmed == true, t => t.ConfirmTime != null)
.WhereIf(inQuery.IsConfirmed == false, t => t.ConfirmTime == null)
.WhereIf(inQuery.StartConfirmTime != null, t => t.ConfirmTime >= inQuery.StartConfirmTime.Value)
.WhereIf(inQuery.EndConfirmTime != null, t => t.ConfirmTime <= inQuery.EndConfirmTime.Value)
.WhereIf(inQuery.BeginCreateTime != null, t => t.CreateTime >= inQuery.BeginCreateTime)
.WhereIf(inQuery.EndCreateTime != null, t => t.CreateTime <= inQuery.EndCreateTime)
.WhereIf(inQuery.BeginCreateTime != null, t => t.CreateTime >= inQuery.BeginCreateTime)
.WhereIf(inQuery.EndCreateTime != null, t => t.CreateTime <= inQuery.EndCreateTime)
.WhereIf(!string.IsNullOrEmpty(inQuery.UserName), t => t.UserName.Contains(inQuery.UserName))
.WhereIf(inQuery.IsDeleted != null, t => t.IsDeleted == inQuery.IsDeleted)
.WhereIf(isInternal == false, t => t.ConfirmTime != null); //不是内部的人,看有签名时间的

View File

@ -773,10 +773,12 @@ namespace IRaCIS.Core.Application.Service
{
var subjectId = generateEmailCommand.SubjectId;
var businessScenarioEnum = generateEmailCommand.BusinessScenarioEnum;
var trialReadingCriterionId = generateEmailCommand.TrialReadingCriterionId;
var criterionType = generateEmailCommand.CriterionType;
var trialConfig = await _subjectRepository.Where(t => t.Id == subjectId).Select(t => new { t.Trial.IsEnrollementQualificationConfirm, t.Trial.IsPDProgressView }).FirstNotNullAsync();
var trialConfig = await _subjectRepository.Where(t => t.Id == subjectId).Select(t => new { t.Trial.IsEnrollementQualificationConfirm, t.Trial.IsPDProgressView, t.TrialId }).FirstNotNullAsync();
var trialReadingCriterionId = _readingQuestionCriterionTrialRepository.Where(t => t.CriterionType == criterionType && t.TrialId == trialConfig.TrialId).Select(t => t.Id).FirstOrDefault();
//找到入组确认 或者Pd 进展 已生成任务的 访视
var subjectVisitList = await _subjectVisitRepository.Where(t => t.SubjectId == subjectId & t.CheckState == CheckStateEnum.CVPassed && (t.IsEnrollmentConfirm == true || t.PDState == PDStateEnum.PDProgress)).ToListAsync();
@ -1671,10 +1673,10 @@ x.ReadingTableQuestionTrial.QuestionMark == QuestionMark.LesionNumber && x.Readi
{
//await SyncSystemEmainCofigDocListAsync(inQuery.TrialId);
var trialConfig = _trialRepository.Where(t => t.Id == inQuery.TrialId).Select(t => new { t.IsEnrollementQualificationConfirm, t.IsPDProgressView }).First();
var trialConfig = _trialRepository.Where(t => t.Id == inQuery.TrialId).Select(t => new { t.IsEnrollementQualificationConfirm, t.IsPDProgressView, TrialCriterionTypeList = t.TrialReadingCriterionList.Where(t => t.IsSigned).Select(t => t.CriterionType).ToList() }).First();
var trialEmailNoticeConfigQueryable = _trialEmailNoticeConfigRepository.Where(t => t.TrialId == inQuery.TrialId)
.WhereIf(inQuery.EmailTopic.IsNotNullOrEmpty(), t => t.EmailTopic.Contains(inQuery.EmailTopic)||t.EmailTopicCN.Contains(inQuery.EmailTopic))
.WhereIf(inQuery.EmailTopic.IsNotNullOrEmpty(), t => t.EmailTopic.Contains(inQuery.EmailTopic) || t.EmailTopicCN.Contains(inQuery.EmailTopic))
.WhereIf(inQuery.IsDistinguishCriteria == false, t => t.IsDistinguishCriteria == false)
.WhereIf(inQuery.IsDistinguishCriteria == true, t => t.IsDistinguishCriteria == true)
.WhereIf(inQuery.CriterionTypeEnum != null, t => t.CriterionTypeList.Any(c => c == inQuery.CriterionTypeEnum))
@ -1692,6 +1694,7 @@ x.ReadingTableQuestionTrial.QuestionMark == QuestionMark.LesionNumber && x.Readi
var orderQuery = inQuery.Asc ? trialEmailNoticeConfigQueryable.OrderBy(sortField) : trialEmailNoticeConfigQueryable.OrderBy(sortField + " desc");
var list = await orderQuery.ToListAsync();
return ResponseOutput.Ok(list, trialConfig);
}

View File

@ -117,4 +117,10 @@ namespace IRaCIS.Core.Application.Contracts.Dicom.DTO
public bool HasLabel { get; set; } = false;
public bool KeySeries { get; set; } = false;
}
public class UpdateImageResizeDTO
{
public Guid SeriesId { get; set; }
public string ImageResizePath { get; set; }
}
}

View File

@ -134,6 +134,8 @@ namespace IRaCIS.Core.Application.Contracts
public string Bodypart { get; set; } = string.Empty;
public string BodyPartForEditOther { get; set; } = string.Empty;
public DateTime? StudyTime { get; set; }

View File

@ -1185,6 +1185,9 @@ namespace IRaCIS.Core.Application.Service.ImageAndDoc
foreach (var item in list.GroupBy(t => new { t.StudyInstanceUid, t.DicomStudyId }))
{
var studyUid = item.Key.StudyInstanceUid;
var ossFolder = $"{pathInfo.TrialId}/Image/{pathInfo.SubjectId}/{pathInfo.VisitId}/{item.Key.StudyInstanceUid}";
var isSucess = await SafeBussinessHelper.RunAsync(async () => await DicomDIRHelper.GenerateStudyDIRAndUploadAsync(item.ToList(), dirDic, ossFolder, _oSSService));
@ -1192,7 +1195,7 @@ namespace IRaCIS.Core.Application.Service.ImageAndDoc
if (isSucess)
{
await _dicomStudyRepository.BatchUpdateNoTrackingAsync(t => t.Id == item.Key.DicomStudyId, u => new DicomStudy() { StudyDIRPath = $"/{ossFolder}/DICOMDIR" });
await _dicomStudyRepository.BatchUpdateNoTrackingAsync(t => t.Id == item.Key.DicomStudyId, u => new DicomStudy() { StudyDIRPath = $"/{ossFolder}/{dirDic[$"{studyUid}_DICOMDIR"]}" });
}
}
@ -1625,6 +1628,8 @@ namespace IRaCIS.Core.Application.Service.ImageAndDoc
var ossFolder = $"{pathInfo.TrialId}/Image/{pathInfo.SubjectId}/{visitId}/{item.Key.StudyInstanceUid}";
var studyUid = item.Key.StudyInstanceUid;
var isSucess = await SafeBussinessHelper.RunAsync(async () => await DicomDIRHelper.GenerateStudyDIRAndUploadAsync(item.ToList(), dirDic, ossFolder, _oSSService));
@ -1632,11 +1637,11 @@ namespace IRaCIS.Core.Application.Service.ImageAndDoc
{
if (isTaskStudy)
{
await _taskStudyRepository.BatchUpdateNoTrackingAsync(t => t.Id == item.Key.DicomStudyId, u => new TaskStudy() { StudyDIRPath = $"/{ossFolder}/DICOMDIR" });
await _taskStudyRepository.BatchUpdateNoTrackingAsync(t => t.Id == item.Key.DicomStudyId, u => new TaskStudy() { StudyDIRPath = $"/{ossFolder}/{dirDic[$"{studyUid}_DICOMDIR"]}" });
}
else
{
await _dicomStudyRepository.BatchUpdateNoTrackingAsync(t => t.Id == item.Key.DicomStudyId, u => new DicomStudy() { StudyDIRPath = $"/{ossFolder}/DICOMDIR" });
await _dicomStudyRepository.BatchUpdateNoTrackingAsync(t => t.Id == item.Key.DicomStudyId, u => new DicomStudy() { StudyDIRPath = $"/{ossFolder}/{dirDic[$"{studyUid}_DICOMDIR"]}" });
}
}
@ -2303,6 +2308,7 @@ namespace IRaCIS.Core.Application.Service.ImageAndDoc
var subjectId = item.First().SubjectId;
var studyUid = item.Key.StudyInstanceUid;
var ossFolder = $"{inCommand.TrialId}/Image/{subjectId}/{visitId}/{item.Key.StudyInstanceUid}";
@ -2310,7 +2316,7 @@ namespace IRaCIS.Core.Application.Service.ImageAndDoc
if (isSucess)
{
await _dicomStudyRepository.BatchUpdateNoTrackingAsync(t => t.Id == item.Key.DicomStudyId, u => new DicomStudy() { StudyDIRPath = $"/{ossFolder}/DICOMDIR" });
await _dicomStudyRepository.BatchUpdateNoTrackingAsync(t => t.Id == item.Key.DicomStudyId, u => new DicomStudy() { StudyDIRPath = $"/{ossFolder}/{dirDic[$"{studyUid}_DICOMDIR"]}" });
}
}
}

View File

@ -17,7 +17,18 @@ namespace IRaCIS.Core.Application.Services
{
/// <summary>
/// 更新缩略图路径
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[HttpPost]
public async Task<IResponseOutput> UpdateImageResizePath(UpdateImageResizeDTO dto)
{
await _seriesRepository.UpdatePartialFromQueryAsync(t => t.Id == dto.SeriesId, u => new DicomSeries() { ImageResizePath = dto.ImageResizePath }, true);
return ResponseOutput.Ok();
}
//医生读片那一块有耦合,关键序列 这里暂时留存
/// <summary> 指定资源Id获取Dicom检查所属序列信息列表 </summary>

View File

@ -382,7 +382,8 @@ namespace IRaCIS.Core.Application.Service.ImageAndDoc
Id = t.Id,
Bodypart = t.BodyPartExamined,
Bodypart = t.BodyPartForEdit,
BodyPartForEditOther = t.BodyPartForEditOther,
Modalities = t.Modalities,
@ -433,6 +434,7 @@ namespace IRaCIS.Core.Application.Service.ImageAndDoc
Id = t.Id,
Bodypart = t.BodyPart,
BodyPartForEditOther=t.BodyPartForEditOther,
Modalities = t.Modality,

View File

@ -308,6 +308,8 @@ namespace IRaCIS.Core.Application.Service
await _fusionCache.RemoveAsync(CacheKeys.UserLoginError(userName));
await _fusionCache.RemoveByTagAsync(CacheKeys.UserMFATag(identityUserId));
await _userLogRepository.AddAsync(new UserLog() { IP = _userInfo.IP, ActionIdentityUserId = _userInfo.IdentityUserId, ActionUserName = _userInfo.UserName, TargetIdentityUserId = identityUserId, OptType = UserOptType.ResetPassword }, true);
return ResponseOutput.Ok();
@ -316,7 +318,7 @@ namespace IRaCIS.Core.Application.Service
/// <summary>
/// 重置密码发邮件 (未登陆修改
/// 重置密码发邮件 (未登陆修改-忘记密码
/// </summary>
/// <param name="email"></param>
/// <returns></returns>
@ -357,6 +359,8 @@ namespace IRaCIS.Core.Application.Service
await _mailVerificationService.AnolymousSendEmailForResetAccount(email, verificationCode);
await _fusionCache.RemoveByTagAsync(CacheKeys.UserMFATag(existUser.Id));
return ResponseOutput.Ok();
}
@ -438,6 +442,10 @@ namespace IRaCIS.Core.Application.Service
await _mailVerificationService.AfterUserModifyPasswordSendEmailAsync(identityUserId);
var find = await _identityUserRepository.FindAsync(identityUserId);
await _fusionCache.RemoveAsync(CacheKeys.UserLoginError(find.UserName));
return ResponseOutput.Ok();
}
@ -485,6 +493,8 @@ namespace IRaCIS.Core.Application.Service
await _mailVerificationService.AfterUserModifyPasswordSendEmailAsync(_userInfo.IdentityUserId);
await _fusionCache.RemoveByTagAsync(CacheKeys.UserMFATag(_userInfo.IdentityUserId));
return ResponseOutput.Result(success);
@ -861,8 +871,9 @@ namespace IRaCIS.Core.Application.Service
if (isRemember)
{
await _fusionCache.SetAsync(CacheKeys.UserMFAVerifyPass(identityUserId, _userInfo.BrowserFingerprint), _userInfo.BrowserFingerprint, TimeSpan.FromMinutes(_serviceVerifyConfigConfig.UserMFAVerifyMinutes));
await _fusionCache.SetAsync(CacheKeys.UserMFAVerifyPass(identityUserId, _userInfo.BrowserFingerprint), _userInfo.BrowserFingerprint,
TimeSpan.FromMinutes(_serviceVerifyConfigConfig.UserMFAVerifyMinutes), new[] { CacheKeys.UserMFATag(identityUserId) });
Log.Logger.Warning($"MFA登录记录:{_userInfo.UserName} 浏览器标识: {_userInfo.BrowserFingerprint} 设置缓存分钟{_serviceVerifyConfigConfig.UserMFAVerifyMinutes}");
}
@ -1066,7 +1077,7 @@ namespace IRaCIS.Core.Application.Service
//异地登录
loginUser.LoginState = 2;
await _fusionCache.RemoveByTagAsync(CacheKeys.UserMFATag(loginUser.IdentityUserId));
}
}
}
@ -1176,15 +1187,20 @@ namespace IRaCIS.Core.Application.Service
if (_verifyConfig.CurrentValue.OpenLoginMFA)
{
if ((await _fusionCache.GetOrDefaultAsync(CacheKeys.UserMFAVerifyPass(identityUserId, _userInfo.BrowserFingerprint), "")) == _userInfo.BrowserFingerprint)
{
userLoginReturnModel.IsMFA = false;
Log.Logger.Warning($"MFA登录:{userName} 浏览器标识: {_userInfo.BrowserFingerprint},判断缓存里存在 ");
}
else
{
//MFA 发送邮件
userLoginReturnModel.IsMFA = true;
Log.Logger.Warning($"MFA登录:{userName} 浏览器标识: {_userInfo.BrowserFingerprint} 判断缓存已经不存在");
}
var email = userLoginReturnModel.BasicInfo.EMail;

View File

@ -20,6 +20,8 @@ namespace IRaCIS.Core.Application.Contracts
public bool IsDeleted { get; set; }
public bool IsBeMark { get; set; }
public DateTime? MarkTime { get; set; }
}

View File

@ -983,6 +983,8 @@ namespace IRaCIS.Core.Application.Service.Reading.Dto
/// </summary>
public ImageMark? ImageMarkEnum { get; set; }
public string QuestionGroupName { get; set; }
/// <summary>
/// 影像工具
/// </summary>

View File

@ -485,13 +485,16 @@ namespace IRaCIS.Core.Application.Service
List<NoneDicomStudyView> result = await noneDicomStudyQueryable.OrderBy(x => x.ImageDate).ThenBy(x => x.CreateTime).ToListAsync();
// 获取非DIOCM标记
var nonoDicomStudyFileIds = await _readingNoneDicomMarkRepository.Where(x => x.VisitTaskId == inDto.VisistTaskId).ToListAsync();
var markFileId = nonoDicomStudyFileIds.Select(x => x.NoneDicomFileId).ToList();
if (nonoDicomStudyFileIds.Count > 0 && taskinfo.ReadingTaskState == ReadingTaskState.HaveSigned)
{
var studyId = nonoDicomStudyFileIds.Select(x => x.StudyId).FirstOrDefault();
var noneDicomids = nonoDicomStudyFileIds.Select(x => x.NoneDicomFileId).ToList();
var noneDicomStudyViewMark = new NoneDicomStudyView() { Id = Guid.NewGuid() };
noneDicomStudyViewMark.IsCriticalSequence = true;
noneDicomStudyViewMark.NoneDicomStudyFileList = await _noneDicomStudyFileRepository.Where(x => noneDicomids.Contains(x.Id)).ProjectTo<NoneDicomStudyFileView>(_mapper.ConfigurationProvider).ToListAsync();
@ -506,6 +509,14 @@ namespace IRaCIS.Core.Application.Service
}
foreach (var item in result)
{
foreach (var item1 in item.NoneDicomStudyFileList)
{
item1.IsBeMark= markFileId.Contains(item1.Id);
}
}
var trialInfo = await _trialRepository.Where(x => x.Id == inDto.TrialId).Select(x => new
{

View File

@ -374,7 +374,7 @@ namespace IRaCIS.Core.Application.Service
CreateMap<AddOrUpdateReadingQuestionTrialInDto, ReadingQuestionTrial>()
.ForMember(dest => dest.CreateUserRole, opt => opt.Ignore());
CreateMap<ReadingQuestionTrial, ReadingQuestionTrialView>()
.ForMember(d => d.QuestionGroupName, u => u.MapFrom(s => s.GroupInfo == null ? s.GroupName : s.GroupInfo.GroupName))
.ForMember(d => d.GroupName, u => u.MapFrom(s => s.GroupInfo == null ? s.GroupName : s.GroupInfo.GroupName))
.ForMember(d => d.GroupEnName, u => u.MapFrom(s => s.GroupInfo == null ? s.GroupEnName : s.GroupInfo.GroupEnName))
.ForMember(d => d.ParentQuestionName, u => u.MapFrom(s => s.ParentReadingQuestionTrial == null ? string.Empty : s.ParentReadingQuestionTrial.QuestionName))

View File

@ -338,6 +338,7 @@ namespace IRaCIS.Core.Application.Service
trial.DeclarationTypes = $"|{string.Join('|', updateModel.DeclarationTypeEnumList.Select(x => ((int)x).ToString()).ToList())}|";
trial.AttendedReviewerTypes = $"|{string.Join('|', updateModel.AttendedReviewerTypeEnumList.Select(x => ((int)x).ToString()).ToList())}|";
trial.EmailFromName = $"{_systemEmailConfig.FromName}-{trial.TrialCode}";
trial.UpdateTime = DateTime.Now;

View File

@ -12,6 +12,7 @@ using IRaCIS.Core.Domain;
using IRaCIS.Core.Domain.Models;
using IRaCIS.Core.Domain.Share;
using IRaCIS.Core.Infra.EFCore;
using IRaCIS.Core.Infra.EFCore.Context;
using IRaCIS.Core.Infrastructure;
using IRaCIS.Core.Infrastructure.Encryption;
using IRaCIS.Core.Infrastructure.NewtonsoftJson;
@ -38,6 +39,7 @@ using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;
using System.Collections.Concurrent;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.Globalization;
using System.IO;
@ -75,14 +77,85 @@ namespace IRaCIS.Core.Application.Service
{
public static int IntValue = 100;
public class ModelVerifyCommand
{
public int? IntNUllValue { get; set; }
public int IntValue { get; set; }
public string StringValue { get; set; }
public string? StringNUllValue { get; set; }
public string StringBackDefaultValue { get; set; } = string.Empty;
public Guid GuidValue { get; set; } = NewId.NextSequentialGuid();
public Guid? GuidNUllValue { get; set; }
[NotDefault]
public Guid GuidValueNotDefault { get; set; }
public bool BoolValue { get; set; }
public bool? BoolNUllValue { get; set; }
public DateTime DateTimeValue { get; set; }
public DateTime? DateTimeNUllValue { get; set; }
}
//创建一个模型验证的方法
[AllowAnonymous]
[HttpPost]
public async Task<IResponseOutput> PostModelVerify(ModelVerifyCommand modelVerify)
{
return ResponseOutput.Ok(modelVerify);
}
[AllowAnonymous]
public async Task<IResponseOutput> CreatNewDBStruct()
{
var factory = new IRaCISDBContextFactory();
using var db = factory.CreateDbContext(Array.Empty<string>());
// ⚠️ 临时用,确认是测试库
// db.Database.EnsureDeleted();
db.Database.EnsureCreated();
Console.WriteLine("数据库结构已创建完成");
return ResponseOutput.Ok();
}
[AllowAnonymous]
public async Task<IResponseOutput> DeleteCacheDIR()
{
var list = _dicomStudyRepository.Where(t => t.StudyDIRPath != "").Select(t => t.StudyDIRPath).ToList();
await _IOSSService.DeleteObjects(list.Select(t => t.TrimStart('/')).ToList(), true);
return ResponseOutput.Ok();
}
[AllowAnonymous]
public async Task<IResponseOutput> TestOSS(StorageClass storageClass)
{
if (storageClass == StorageClass.IA || storageClass == StorageClass.Archive || storageClass == StorageClass.ColdArchive || storageClass == StorageClass.DeepColdArchive)
{
//await _IOSSService.SetImmediateArchiveRule($"Test-Archive/Archive{(int)storageClass}/");
await _IOSSService.SetImmediateArchiveRule($"Test-Archive/Archive{(int)storageClass}/");
await _IOSSService.RestoreFilesByPrefixAsync($"Test-Archive/Archive{(int)storageClass}/");
//await _IOSSService.RestoreFilesByPrefixAsync($"Test-Archive/Archive{(int)storageClass}/");
}

View File

@ -46,6 +46,11 @@ public class SystemDocument : BaseFullDeleteAuditEntity
public bool IsPublish { get; set; } = true;
/// <summary>
/// 发布时间
/// </summary>
public DateTime? PublishDate { get; set; }
}

View File

@ -50,6 +50,11 @@ public class TrialDocument : BaseFullDeleteAuditEntity
/// </summary>
public bool IsPublish { get; set; } = true;
/// <summary>
/// 发布时间
/// </summary>
public DateTime? PublishDate { get; set; }
}
[Comment("项目签署文档附件")]

View File

@ -259,7 +259,7 @@ public partial class Trial : BaseFullDeleteAuditEntity
/// 图像格式
/// </summary>
[StringLength(2000)]
public List<string> ImageFormatList { get; set; } = new List<string>() {"jpg","jpeg","png","bmp","pdf","zip" };
public List<string> ImageFormatList { get; set; } = new List<string>() {"jpg","jpeg","png","bmp","pdf","zip","mp4" };
#endregion
#region 邮件配置

View File

@ -53,3 +53,6 @@
5、以下命令将生成一个从指定 from 迁移到指定 to 迁移的 SQL 脚本。
dotnet ef migrations script from to -p IRaCIS.Core.Infra.EFCore
6、查看迁移列表
dotnet ef migrations list -p IRaCIS.Core.Infra.EFCore

View File

@ -6,6 +6,7 @@ using IRaCIS.Core.Infrastructure.Extention;
using Microsoft.EntityFrameworkCore.Metadata.Conventions;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Microsoft.VisualBasic;
using Newtonsoft.Json;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
@ -60,8 +61,13 @@ public class IRaCISDBContext : DbContext
//针对字符串使用默认的长度配置为200如果标注了StringLength 其他长度就是标注的长度如果标注了MaxLength 那么就是nvarcharMax
configurationBuilder.Conventions.Add(_ => new DefaultStringLengthConvention(400));
//configurationBuilder.Conventions.Add(_ => new RemoveForeignKeyConvention());
//控制外键索引生成与否
//https://learn.microsoft.com/zh-cn/ef/core/modeling/relationships/conventions?utm_source=chatgpt.com
//configurationBuilder.Conventions.Remove(typeof(ForeignKeyIndexConvention));
configurationBuilder.Conventions.Remove(typeof(ForeignKeyIndexConvention));
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
@ -116,6 +122,7 @@ public class IRaCISDBContext : DbContext
#region decimal 自定义精度,适配多种数据库
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
foreach (var property in entityType.GetProperties())
@ -142,6 +149,12 @@ public class IRaCISDBContext : DbContext
}
}
//用户名区分大小写
modelBuilder.Entity<IdentityUser>(entity =>
{
entity.Property(e => e.UserName)
.UseCollation("Chinese_PRC_CS_AS");
});
#endregion
//遍历实体模型手动配置
@ -160,6 +173,26 @@ public class IRaCISDBContext : DbContext
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
#region 修改Id 聚集索引-> Id非聚集CreateTime聚集索引方便分区
// 1 所有 EntityId 为主键(非聚集)
if (typeof(Entity).IsAssignableFrom(entityType.ClrType))
{
modelBuilder.Entity(entityType.ClrType)
.HasKey(nameof(Entity.Id))
.IsClustered(false);
}
// 2 所有 IAuditAddCreateTime 为聚集索引
if (typeof(IAuditAdd).IsAssignableFrom(entityType.ClrType))
{
modelBuilder.Entity(entityType.ClrType)
.HasIndex(nameof(IAuditAdd.CreateTime))
.IsClustered();
}
#endregion
// 软删除配置
if (typeof(ISoftDelete).IsAssignableFrom(entityType.ClrType))
{

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,39 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace IRaCIS.Core.Infra.EFCore.Migrations
{
/// <inheritdoc />
public partial class PublishDate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "PublishDate",
table: "TrialDocument",
type: "datetime2",
nullable: true);
migrationBuilder.AddColumn<DateTime>(
name: "PublishDate",
table: "SystemDocument",
type: "datetime2",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "PublishDate",
table: "TrialDocument");
migrationBuilder.DropColumn(
name: "PublishDate",
table: "SystemDocument");
}
}
}

View File

@ -119,7 +119,10 @@ namespace IRaCIS.Core.Infra.EFCore
/// <summary>EntityState.Detached的实体 修改 部分字段</summary>
public static void EntityModifyPartialFiled<T>(this IRaCISDBContext _dbContext, T waitModifyEntity, Expression<Func<T, T>> updateFactory) where T : Entity
{
var entityEntry = _dbContext.Entry(waitModifyEntity);
//解决重复跟踪问题
var tracked = _dbContext.ChangeTracker.Entries<T>().FirstOrDefault(e => e.Entity.Id.Equals(waitModifyEntity.Id));
var entityEntry = tracked?? _dbContext.Entry(waitModifyEntity);
//entityEntry.State = EntityState.Detached;
var list = ((MemberInitExpression)updateFactory.Body).Bindings.Select(mb => mb.Member.Name)
@ -134,7 +137,7 @@ namespace IRaCIS.Core.Infra.EFCore
foreach (PropertyInfo prop in list)
{
_dbContext.Entry(waitModifyEntity).Property(prop.Name).IsModified = true;
entityEntry.Property(prop.Name).IsModified = true;
object value = prop.GetValue(applyObj);
prop.SetValue(waitModifyEntity, value);

View File

@ -48,29 +48,6 @@ steps:
commands:
- cd /opt/1panel/xc-deploy/Test_IRC/devops-build-publish;sh test-irc-update-or-create-stack.sh v${DRONE_BUILD_NUMBER}
trigger:
branch:
- Test_IRC_Net8
---
kind: pipeline
type: ssh
name: ssh-linux-test-irc-scp-publish
platform:
os: Linux
arch: 386
clone:
disable: true
server:
host: 106.14.89.110
user: root
password:
from_secret: test_ssh_pwd
steps:
- name: publish-test-irc-scp
commands:
- cd /opt/1panel/xc-deploy/Test_IRC_SCP/devops-build-publish;sh pull-build-test-irc-scp-image.sh v${DRONE_BUILD_NUMBER}
@ -78,7 +55,9 @@ steps:
trigger:
branch:
- Test_IRC_Net8—_SCP_Disable
- Test_IRC_Net8
---
kind: pipeline
@ -111,37 +90,7 @@ trigger:
---
kind: pipeline
type: ssh
name: ssh-linux-test-scp-publish
platform:
os: Linux
arch: 386
clone:
disable: true #禁用默认克隆
server:
host: 106.14.89.110
user: root
password:
from_secret: test_ssh_pwd
steps:
- name: publish-test-hir-scp
commands:
- cd /opt/1panel/xc-deploy/Test_HIR_SCP/devops-build-publish;sh pull-build-test-hir-scp-image.sh v${DRONE_BUILD_NUMBER}
- cd /opt/1panel/xc-deploy/Test_HIR_SCP ;sh update-image-if-need-then-pull-aliyun.sh v${DRONE_BUILD_NUMBER}
trigger:
branch:
- Test_HIR
---
kind: pipeline
type: ssh
name: ssh-linux-test-hir-publish
name: ssh-linux-test-hir
platform:
os: Linux
@ -161,8 +110,16 @@ steps:
commands:
- cd /opt/1panel/xc-deploy/Test_HIR/devops-build-publish;sh pull-build-test-hir-image.sh v${DRONE_BUILD_NUMBER}
- cd /opt/1panel/xc-deploy/Test_HIR ; sh update-image-if-need-then-pull-aliyun.sh v${DRONE_BUILD_NUMBER}
- name: publish-test-hir-scp
commands:
- cd /opt/1panel/xc-deploy/Test_HIR_SCP/devops-build-publish;sh pull-build-test-hir-scp-image.sh v${DRONE_BUILD_NUMBER}
- cd /opt/1panel/xc-deploy/Test_HIR_SCP ;sh update-image-if-need-then-pull-aliyun.sh v${DRONE_BUILD_NUMBER}
trigger:
branch:
- Test_HIR