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
he 2026-01-15 10:28:02 +08:00
commit 6874459046
28 changed files with 34128 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

@ -0,0 +1,101 @@
using IRaCIS.Core.Domain.Models;
using IRaCIS.Core.Infra.EFCore;
using Microsoft.EntityFrameworkCore;
namespace IRaCIS.Core.SCP.Service;
public static class CacheKeys
{
//项目缓存
public static string Trial(string trialIdStr) => $"TrialId:{trialIdStr}";
//检查编号递增锁
public static string TrialStudyMaxCode(Guid trialId) => $"TrialStudyMaxCode:{trialId}";
public static string TrialStudyUidUploading(Guid trialId, string studyUid) => $"TrialStudyUid:{trialId}_{studyUid}";
//CRC上传影像提交锁key
public static string TrialStudyUidDBLock(Guid trialId, string studyUid) => $"TrialStudyUidDBLock:{trialId}_{studyUid}";
public static string TrialTaskStudyUidUploading(Guid trialId, Guid visiTaskId, string studyUid) => $"TrialStudyUid:{trialId}_{visiTaskId}_{studyUid}";
//影像后处理上传提交锁key
public static string TrialTaskStudyUidDBLock(Guid trialId, Guid visiTaskId, string studyUid) => $"TrialTaskStudyUidDBLock:{trialId}_{visiTaskId}_{studyUid}";
//系统匿名化
public static string SystemAnonymization => $"SystemAnonymization";
//前端国际化
public static string FrontInternational => $"FrontInternationalList";
//登录挤账号
public static string UserToken(Guid userId) => $"UserToken:{userId}";
//超时没请求接口自动退出
public static string UserAutoLoginOut(Guid userId) => $"UserAutoLoginOut:{userId}";
public static string UserDisable(Guid userId) => $"UserDisable:{userId}";
public static string UserRoleDisable(Guid userRoleId) => $"UserRoleDisable:{userRoleId}";
/// <summary>
/// 用户登录错误 限制登录
/// </summary>
/// <param name="userName"></param>
/// <returns></returns>
public static string UserLoginError(string userName) => $"login-failures:{userName}";
/// <summary>
/// 跳过阅片
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
public static string SkipReadingCacheKey(Guid userId) => $"{userId}SkipReadingCache";
/// <summary>
/// 开始阅片时间
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
public static string StartReadingTimeKey(Guid userId) => $"{userId}StartReadingTime";
/// <summary>
/// 开始休息时间
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
public static string StartRestTime(Guid userId) => $"{userId}StartRestTime";
//每个用户 每个浏览器独立时间
public static string UserMFAVerifyPass(Guid userId, string browserFingerprint) => $"UserMFAVerifyPass:{userId}:{browserFingerprint}";
public static string TrialSiteInfo(Guid trialSiteId) => $"{trialSiteId}TrialSiteInfo";
}
public static class CacheHelper
{
public static async Task<string?> GetTrialStatusAsync(Guid trialId, IRepository<Trial> _trialRepository)
{
var statusStr = await _trialRepository.Where(t => t.Id == trialId, ignoreQueryFilters: true).Select(t => t.TrialStatusStr).FirstOrDefaultAsync();
return statusStr;
}
public class TrialSiteInfoDTO
{
public string TrialSiteCode { get; set; }
public string TrialCode { get; set; }
}
public static async Task<TrialSiteInfoDTO> GetTrialSiteInfo(Guid trialSiteId, IRepository<TrialSite> _trialSiteRepository)
{
var obj = await _trialSiteRepository.Where(t => t.Id == trialSiteId, ignoreQueryFilters: true).Select(t => new TrialSiteInfoDTO { TrialCode= t.Trial.TrialCode, TrialSiteCode= t.TrialSiteCode }).FirstOrDefaultAsync();
return obj??new TrialSiteInfoDTO();
}
public static async Task<List<SystemAnonymization>> GetSystemAnonymizationListAsync(IRepository<SystemAnonymization> _systemAnonymizationRepository)
{
var list = await _systemAnonymizationRepository.Where(t => t.IsEnable).ToListAsync();
return list;
}
}

View File

@ -1,49 +1,32 @@
using IRaCIS.Core.Domain.Share;
using System.Text;
using Microsoft.AspNetCore.Hosting;
using IRaCIS.Core.Infrastructure;
using Medallion.Threading;
using FellowOakDicom;
using FellowOakDicom;
using FellowOakDicom.Imaging.Codec;
using System.Data;
using IRaCIS.Core.Domain.Models;
using FellowOakDicom.Network;
using IRaCIS.Core.SCP.Service;
using IRaCIS.Core.Domain.Models;
using IRaCIS.Core.Domain.Share;
using IRaCIS.Core.Infra.EFCore;
using MassTransit;
using System.Runtime.Intrinsics.X86;
using Serilog.Sinks.File;
using IRaCIS.Core.Infrastructure;
using IRaCIS.Core.Infrastructure.Extention;
using IRaCIS.Core.SCP.Service;
using MassTransit;
using Medallion.Threading;
using Microsoft.AspNetCore.Hosting;
using Serilog.Sinks.File;
using System.Data;
using System.Runtime.Intrinsics.X86;
using System.Text;
using ZiggyCreatures.Caching.Fusion;
using static IRaCIS.Core.SCP.Service.CacheHelper;
namespace IRaCIS.Core.SCP.Service
{
public class DicomArchiveService : BaseService, IDicomArchiveService
public class DicomArchiveService(IRepository<SCPPatient> _patientRepository,
IRepository<SCPStudy> _studyRepository,
IRepository<SCPSeries> _seriesRepository,
IRepository<SCPInstance> _instanceRepository
) : BaseService, IDicomArchiveService
{
private readonly IRepository<SCPPatient> _patientRepository;
private readonly IRepository<SCPStudy> _studyRepository;
private readonly IRepository<SCPSeries> _seriesRepository;
private readonly IRepository<SCPInstance> _instanceRepository;
private readonly IRepository<Dictionary> _dictionaryRepository;
private readonly IDistributedLockProvider _distributedLockProvider;
private List<Guid> _instanceIdList = new List<Guid>();
public DicomArchiveService(IRepository<SCPPatient> patientRepository, IRepository<SCPStudy> studyRepository,
IRepository<SCPSeries> seriesRepository,
IRepository<SCPInstance> instanceRepository,
IRepository<Dictionary> dictionaryRepository,
IDistributedLockProvider distributedLockProvider)
{
_distributedLockProvider = distributedLockProvider;
_studyRepository = studyRepository;
_patientRepository = patientRepository;
_seriesRepository = seriesRepository;
_instanceRepository = instanceRepository;
_dictionaryRepository = dictionaryRepository;
}
@ -53,18 +36,19 @@ namespace IRaCIS.Core.SCP.Service
/// <param name="dataset"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public async Task<Guid> ArchiveDicomFileAsync(DicomFile dicomFile, Guid trialId, Guid trialSiteId, string fileRelativePath, string callingAE, string calledAE,long fileSize)
public async Task<Guid> ArchiveDicomFileAsync(DicomFile dicomFile, Guid trialId, Guid trialSiteId, string fileRelativePath, string callingAE, string calledAE, long fileSize)
{
var dataset = dicomFile.Dataset;
string studyInstanceUid = dataset.GetString(DicomTag.StudyInstanceUID);
string seriesInstanceUid = dataset.GetString(DicomTag.SeriesInstanceUID);
string sopInstanceUid = dataset.GetString(DicomTag.SOPInstanceUID);
string patientIdStr = dataset.GetSingleValueOrDefault(DicomTag.PatientID,string.Empty);
string patientIdStr = dataset.GetSingleValueOrDefault(DicomTag.PatientID, string.Empty);
//Guid patientId= IdentifierHelper.CreateGuid(patientIdStr);
Guid studyId = IdentifierHelper.CreateGuid(studyInstanceUid,trialId.ToString());
Guid studyId = IdentifierHelper.CreateGuid(studyInstanceUid, trialId.ToString());
Guid seriesId = IdentifierHelper.CreateGuid(studyInstanceUid, seriesInstanceUid, trialId.ToString());
Guid instanceId = IdentifierHelper.CreateGuid(studyInstanceUid, seriesInstanceUid, sopInstanceUid, trialId.ToString());
@ -77,9 +61,9 @@ namespace IRaCIS.Core.SCP.Service
//using (@lock.Acquire())
{
var findPatient = await _patientRepository.FirstOrDefaultAsync(t => t.PatientIdStr == patientIdStr && t.TrialSiteId==trialSiteId );
var findStudy = await _studyRepository.FirstOrDefaultAsync(t=>t.Id== studyId);
var findSerice = await _seriesRepository.FirstOrDefaultAsync(t => t.Id == seriesId);
var findPatient = await _patientRepository.FirstOrDefaultAsync(t => t.PatientIdStr == patientIdStr && t.TrialSiteId == trialSiteId);
var findStudy = await _studyRepository.FirstOrDefaultAsync(t => t.Id == studyId);
var findSerice = await _seriesRepository.FirstOrDefaultAsync(t => t.Id == seriesId);
var findInstance = await _instanceRepository.FirstOrDefaultAsync(t => t.Id == instanceId);
DateTime? studyTime = dataset.GetSingleValueOrDefault(DicomTag.StudyDate, string.Empty) == string.Empty ? null : dataset.GetSingleValue<DateTime>(DicomTag.StudyDate).Add(dataset.GetSingleValueOrDefault(DicomTag.StudyTime, string.Empty) == string.Empty ? TimeSpan.Zero : dataset.GetSingleValue<DateTime>(DicomTag.StudyTime).TimeOfDay);
@ -93,8 +77,8 @@ namespace IRaCIS.Core.SCP.Service
findPatient = new SCPPatient()
{
Id = NewId.NextSequentialGuid(),
TrialId=trialId,
TrialSiteId=trialSiteId,
TrialId = trialId,
TrialSiteId = trialSiteId,
PatientIdStr = dataset.GetSingleValueOrDefault(DicomTag.PatientID, string.Empty),
PatientName = dataset.GetSingleValueOrDefault(DicomTag.PatientName, string.Empty),
PatientAge = dataset.GetSingleValueOrDefault(DicomTag.PatientAge, string.Empty),
@ -122,7 +106,7 @@ namespace IRaCIS.Core.SCP.Service
DateTime birthDate;
if (findPatient.PatientAge == string.Empty && studyTime.HasValue && DateTime.TryParse(findPatient.PatientBirthDate,out birthDate))
if (findPatient.PatientAge == string.Empty && studyTime.HasValue && DateTime.TryParse(findPatient.PatientBirthDate, out birthDate))
{
var patientAge = studyTime.Value.Year - birthDate.Year;
// 如果生日还未到,年龄减去一岁
@ -245,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);
@ -358,7 +343,7 @@ namespace IRaCIS.Core.SCP.Service
Path = fileRelativePath,
FileSize= fileSize,
FileSize = fileSize,
};
@ -398,7 +383,7 @@ namespace IRaCIS.Core.SCP.Service
}
else
{
await _instanceRepository.BatchUpdateNoTrackingAsync(t => t.Id == instanceId, u => new SCPInstance() { Path = fileRelativePath,FileSize=fileSize });
await _instanceRepository.BatchUpdateNoTrackingAsync(t => t.Id == instanceId, u => new SCPInstance() { Path = fileRelativePath, FileSize = fileSize });
}
await _studyRepository.SaveChangesAsync();

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

@ -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

@ -128,7 +128,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("DICOMDIR" , relativePath.Split('/').Last());
}
//清理临时文件

View File

@ -143,9 +143,8 @@ public enum ObjectStoreUse
public interface IOSSService
{
public void SetImmediateArchiveRule(string prefix,
StorageClass targetStorageClass,
string ruleId = "immediate-archive");
public Task SetImmediateArchiveRule(string prefix, string ruleId = "immediate-archive", bool isDelete = false);
public Task RestoreFilesByPrefixAsync(string prefix, int restoreDays = 3, int batchSize = 100);
public Task<string> UploadToOSSAsync(Stream fileStream, string oosFolderPath, string fileRealName, bool isFileNameAddGuid = true);
@ -161,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();
@ -190,51 +189,52 @@ public class OSSService : IOSSService
/// <summary>
/// 将指定前缀下的所有现有文件立即转为目标存储类型
/// 核心Days = 0 表示对所有存量文件立即生效
/// </summary>
/// <param name="prefix">要转换的文件前缀,如 "project-a/logs/"</param>
/// <param name="targetStorageClass">目标存储类型</param>
/// <param name="ruleId">规则ID默认为"immediate-archive"</param>
public void SetImmediateArchiveRule(string prefix,
StorageClass targetStorageClass,
string ruleId = "immediate-archive")
/// <param name="isDelete">默认是添加/更新 </param>
public async Task SetImmediateArchiveRule(string prefix, string ruleId = "immediate-archive", bool isDelete = false)
{
BackBatchGetToken();
var aliConfig = ObjectStoreServiceOptions.AliyunOSS;
var _ossClient = new OssClient(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? aliConfig.EndPoint : aliConfig.InternalEndpoint, AliyunOSSTempToken.AccessKeyId, AliyunOSSTempToken.AccessKeySecret, AliyunOSSTempToken.SecurityToken);
try
if (ObjectStoreServiceOptions.ObjectStoreUse == "AliyunOSS")
{
// 1. 先获取现有的所有生命周期规则(避免覆盖)
var existingRules = new List<Aliyun.OSS.LifecycleRule>();
var aliConfig = ObjectStoreServiceOptions.AliyunOSS;
var _ossClient = new OssClient(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? aliConfig.EndPoint : aliConfig.InternalEndpoint, AliyunOSSTempToken.AccessKeyId, AliyunOSSTempToken.AccessKeySecret, AliyunOSSTempToken.SecurityToken);
try
{
var existingRuleList = _ossClient.GetBucketLifecycle(aliConfig.BucketName);
if (existingRuleList != null)
// 1. 先获取现有的所有生命周期规则(避免覆盖)
var existingRules = new List<Aliyun.OSS.LifecycleRule>();
try
{
existingRules.AddRange(existingRuleList);
Console.WriteLine($"找到 {existingRules.Count} 条现有规则");
}
}
catch (OssException ex) when (ex.ErrorCode == "NoSuchLifecycle")
{
// 如果没有生命周期规则,继续创建新规则
Console.WriteLine("当前Bucket无生命周期规则将创建新规则");
}
// 2. 创建立即生效的转换规则
ruleId = $"{ruleId}_{prefix}";
var immediateRule = new Aliyun.OSS.LifecycleRule
{
ID = ruleId,
Prefix = prefix,
Status = RuleStatus.Enabled,
Transitions = new Aliyun.OSS.LifecycleRule.LifeCycleTransition[]
var existingRuleList = _ossClient.GetBucketLifecycle(aliConfig.BucketName);
if (existingRuleList != null)
{
existingRules.AddRange(existingRuleList);
Console.WriteLine($"找到 {existingRules.Count} 条现有规则");
}
}
catch (OssException ex) when (ex.ErrorCode == "NoSuchLifecycle")
{
// 如果没有生命周期规则,继续创建新规则
Console.WriteLine("当前Bucket无生命周期规则将创建新规则");
}
// 2. 创建立即生效的转换规则
ruleId = $"{ruleId}_{prefix}";
var immediateRule = new Aliyun.OSS.LifecycleRule
{
ID = ruleId,
Prefix = prefix,
Status = RuleStatus.Enabled,
Transitions = new Aliyun.OSS.LifecycleRule.LifeCycleTransition[]
{
new Aliyun.OSS.LifecycleRule.LifeCycleTransition
{
@ -242,130 +242,463 @@ public class OSSService : IOSSService
{
Days = 1
},
StorageClass = targetStorageClass
StorageClass = StorageClass.IA
},
new Aliyun.OSS.LifecycleRule.LifeCycleTransition
{
LifeCycleExpiration =
{
Days = 30 //最后一次修改时间
},
StorageClass = StorageClass.Archive
}
}
};
}
};
// 3. 移除同名的旧规则(如果存在)
existingRules.RemoveAll(r => r.ID == ruleId);
// 3. 移除同名的旧规则(如果存在)
existingRules.RemoveAll(r => r.ID == ruleId);
// 4. 添加新规则到规则列表
existingRules.Add(immediateRule);
// 4. 添加新规则到规则列表
if (isDelete == false)
{
existingRules.Add(immediateRule);
}
var request = new SetBucketLifecycleRequest(aliConfig.BucketName)
var request = new SetBucketLifecycleRequest(aliConfig.BucketName)
{
LifecycleRules = existingRules
};
_ossClient.SetBucketLifecycle(request);
}
catch (OssException ex)
{
LifecycleRules= existingRules
};
Log.Logger.Error($"❌ 设置失败 [错误码: {ex.ErrorCode}] 详细: {ex.Message}");
_ossClient.SetBucketLifecycle(request);
Console.WriteLine("✅ 立即归档规则设置成功!");
Console.WriteLine($" 规则ID: {ruleId}");
Console.WriteLine($" 前缀: {prefix}");
Console.WriteLine($" 目标存储类型: {targetStorageClass}");
Console.WriteLine($" 生效时间: 将在下次生命周期扫描时生效通常24小时内");
}
catch (OssException ex)
{
Console.WriteLine($"❌ 设置失败 [错误码: {ex.ErrorCode}]");
Console.WriteLine($" 详细: {ex.Message}");
// 处理特定错误
if (ex.ErrorCode == "InvalidArgument")
}
catch (Exception ex)
{
Console.WriteLine(" 可能原因:存储类型不支持或参数格式错误");
Log.Logger.Error($"❌ 发生未知错误: {ex.Message}");
}
}
catch (Exception ex)
else if (ObjectStoreServiceOptions.ObjectStoreUse == "AWS")
{
Console.WriteLine($"❌ 发生未知错误: {ex.Message}");
var awsConfig = ObjectStoreServiceOptions.AWS;
var credentials = new SessionAWSCredentials(AWSTempToken.AccessKeyId, AWSTempToken.SecretAccessKey, AWSTempToken.SessionToken);
//提供awsEndPoint域名进行访问配置
var clientConfig = new AmazonS3Config
{
RegionEndpoint = RegionEndpoint.GetBySystemName(awsConfig.Region)
//,UseHttp = true,
};
var amazonS3Client = new AmazonS3Client(credentials, clientConfig);
// 1. 获取现有的生命周期配置(避免覆盖)
LifecycleConfiguration existingConfig = null;
var getRequest = new GetLifecycleConfigurationRequest { BucketName = awsConfig.BucketName };
var response = await amazonS3Client.GetLifecycleConfigurationAsync(getRequest);
existingConfig = response.Configuration;
Console.WriteLine($"找到 {existingConfig?.Rules?.Count ?? 0} 条现有规则");
// 2. 生成唯一的规则ID
ruleId = $"{ruleId}_{prefix.Replace('/', '_').Trim('_')}";
// 3. 创建新的生命周期规则
var immediateRule = new Amazon.S3.Model.LifecycleRule
{
Id = ruleId,
Filter = new LifecycleFilter
{
// 使用前缀筛选对象
LifecycleFilterPredicate = new LifecyclePrefixPredicate { Prefix = prefix }
},
Status = LifecycleRuleStatus.Enabled,
// 定义多个转换阶段
Transitions = new List<LifecycleTransition>
{
// 1天后转为低频访问 (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, //创建时间
StorageClass = S3StorageClass.GlacierInstantRetrieval // 对应归档(即时检索)
}
// 如果需要更深的归档,可以继续添加:
// new LifecycleTransition { Days = 90, StorageClass = S3StorageClass.GlacierFlexibleRetrieval },
// new LifecycleTransition { Days = 180, StorageClass = S3StorageClass.DeepArchive }
}
// 注意S3的生命周期规则不支持设置“立即生效Days=0”。
// 如果要对存量文件立即生效,需要配合其他方法(如批量修改存储类型)。
};
// 4. 更新规则列表(移除同名旧规则,添加新规则)
var existingRules = existingConfig.Rules ?? new List<Amazon.S3.Model.LifecycleRule>();
existingRules.RemoveAll(r => r.Id == ruleId);
if (isDelete == false)
{
existingRules.Add(immediateRule);
}
// 5. 提交新的生命周期配置
var putRequest = new PutLifecycleConfigurationRequest
{
BucketName = awsConfig.BucketName,
Configuration = new LifecycleConfiguration { Rules = existingRules }
};
await amazonS3Client.PutLifecycleConfigurationAsync(putRequest);
}
else
{
throw new BusinessValidationFailedException("未定义的存储介质类型");
}
}
/// 解冻指定前缀下的所有归档/冷归档文件
/// </summary>
/// <param name="prefix">要解冻的文件前缀</param>
/// <param name="restoreDays">解冻后文件保持可读的天数默认3天</param>
/// <param name="restoreTier">解冻优先级仅AWS有效</param>
/// <param name="batchSize">批量处理大小默认100</param>
public async Task RestoreFilesByPrefixAsync(string prefix, int restoreDays = 3, int batchSize = 100)
{
BackBatchGetToken();
if (ObjectStoreServiceOptions.ObjectStoreUse == "AliyunOSS")
{
var aliConfig = ObjectStoreServiceOptions.AliyunOSS;
var client = new OssClient(
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? aliConfig.EndPoint : aliConfig.InternalEndpoint,
AliyunOSSTempToken.AccessKeyId,
AliyunOSSTempToken.AccessKeySecret,
AliyunOSSTempToken.SecurityToken
);
var bucketName = aliConfig.BucketName;
int totalRestored = 0;
int totalSkipped = 0;
int totalFailed = 0;
try
{
Console.WriteLine($"开始解冻阿里云OSS文件前缀: {prefix}");
var allObjects = new List<OssObjectSummary>();
// 1. 分页列举文件
string nextMarker = null;
ObjectListing result = null;
do
{
var listRequest = new Aliyun.OSS.ListObjectsRequest(bucketName)
{
Prefix = prefix,
Marker = nextMarker,
MaxKeys = batchSize
};
result = client.ListObjects(listRequest);
allObjects.AddRange(result.ObjectSummaries);
nextMarker = result.NextMarker;
} while (result.IsTruncated);
// 2⃣ 并行解冻(控制并发)
Parallel.ForEach(
allObjects,
new ParallelOptions
{
MaxDegreeOfParallelism = 5 // ⭐ 推荐 5~10
},
obj =>
{
// 只处理归档
if (obj.StorageClass != StorageClass.Archive.ToString())
{
Interlocked.Increment(ref totalSkipped);
return;
}
try
{
var restoreRequest = new Aliyun.OSS.RestoreObjectRequest(bucketName, obj.Key)
{
Days = restoreDays
};
client.RestoreObject(restoreRequest);
Interlocked.Increment(ref totalRestored);
Console.WriteLine($"✅ 提交解冻: {obj.Key}");
}
catch (OssException ex) when (ex.ErrorCode == "RestoreAlreadyInProgress")
{
// 已在解冻中,算成功
Interlocked.Increment(ref totalSkipped);
Console.WriteLine($"⚠️ 已在解冻中: {obj.Key}");
}
catch (Exception ex)
{
Interlocked.Increment(ref totalFailed);
Console.WriteLine($"❌ 解冻失败: {obj.Key} - {ex.Message}");
}
}
);
// 3. 输出统计结果
Console.WriteLine("\n================ 解冻完成 ================");
Console.WriteLine($"总计处理: {totalRestored + totalSkipped + totalFailed} 个文件");
Console.WriteLine($"成功解冻: {totalRestored} 个");
Console.WriteLine($"跳过文件: {totalSkipped} 个 (非归档类型)");
Console.WriteLine($"解冻失败: {totalFailed} 个");
if (totalRestored > 0)
{
Console.WriteLine($"\n📋 解冻说明:");
Console.WriteLine($" • 解冻任务已提交,文件将在后台处理");
Console.WriteLine($" • 解冻完成后,文件将保持可读状态 {restoreDays} 天");
Console.WriteLine($" • 归档文件约需1分钟冷归档需数小时");
}
}
catch (Exception ex)
{
Log.Logger.Error($"❌ 阿里云解冻操作失败: {ex.Message}");
throw;
}
}
else if (ObjectStoreServiceOptions.ObjectStoreUse == "AWS")
{
var awsConfig = ObjectStoreServiceOptions.AWS;
var credentials = new SessionAWSCredentials(
AWSTempToken.AccessKeyId,
AWSTempToken.SecretAccessKey,
AWSTempToken.SessionToken
);
var clientConfig = new AmazonS3Config
{
RegionEndpoint = RegionEndpoint.GetBySystemName(awsConfig.Region),
UseHttp = true,
};
using var client = new AmazonS3Client(credentials, clientConfig);
var bucketName = awsConfig.BucketName;
int totalRestored = 0;
int totalSkipped = 0;
int totalFailed = 0;
try
{
Console.WriteLine($"开始解冻AWS S3文件前缀: {prefix}");
var allObjects = new List<S3Object>();
// 1. 分页列举文件
string continuationToken = null;
ListObjectsV2Response response = null;
do
{
var listRequest = new ListObjectsV2Request
{
BucketName = bucketName,
Prefix = prefix,
ContinuationToken = continuationToken,
MaxKeys = batchSize
};
response = await client.ListObjectsV2Async(listRequest);
allObjects.AddRange(response.S3Objects);
continuationToken = response.NextContinuationToken;
} while (response.IsTruncated == true);
// 2⃣ 并行解冻(控制并发)
await Parallel.ForEachAsync(
allObjects,
new ParallelOptions
{
MaxDegreeOfParallelism = 5 // ⭐ 推荐 5~10
},
async (obj, ct) =>
{
// 只处理归档
if (obj.StorageClass != S3StorageClass.Glacier)
{
Interlocked.Increment(ref totalSkipped);
return;
}
try
{
var restoreRequest = new Amazon.S3.Model.RestoreObjectRequest
{
BucketName = bucketName,
Key = obj.Key,
Days = restoreDays,
};
await client.RestoreObjectAsync(restoreRequest);
Interlocked.Increment(ref totalRestored);
Console.WriteLine($"✅ 提交解冻: {obj.Key}");
}
catch (OssException ex) when (ex.ErrorCode == "RestoreAlreadyInProgress")
{
// 已在解冻中,算成功
Interlocked.Increment(ref totalSkipped);
Console.WriteLine($"⚠️ 已在解冻中: {obj.Key}");
}
catch (Exception ex)
{
Interlocked.Increment(ref totalFailed);
Console.WriteLine($"❌ 解冻失败: {obj.Key} - {ex.Message}");
}
}
);
// 3. 输出统计结果
Console.WriteLine("\n================ 解冻完成 ================");
Console.WriteLine($"总计处理: {totalRestored + totalSkipped + totalFailed} 个文件");
Console.WriteLine($"成功解冻: {totalRestored} 个");
Console.WriteLine($"跳过文件: {totalSkipped} 个 (非归档类型)");
Console.WriteLine($"解冻失败: {totalFailed} 个");
if (totalRestored > 0)
{
Console.WriteLine($"\n📋 AWS解冻说明:");
Console.WriteLine($" • 解冻任务已提交到Glacier服务");
Console.WriteLine($" • 标准解冻: 3-5小时 (Glacier Flexible Retrieval)");
Console.WriteLine($" • 加急解冻: 1-5分钟 (额外收费)");
Console.WriteLine($" • 解冻后文件可读 {restoreDays} 天");
}
}
catch (Exception ex)
{
Log.Logger.Error($"❌ AWS解冻操作失败: {ex.Message}");
throw;
}
}
else
{
throw new BusinessValidationFailedException("未定义的存储介质类型");
}
}
//public async Task SetLifecycle(string lifecycle)
//{
/// <summary>
/// 坑方法,会清空之前的规则
/// </summary>
/// <param name="prefix"></param>
/// <param name="ruleId"></param>
/// <returns></returns>
/// <exception cref="BusinessValidationFailedException"></exception>
public async Task SetLifecycle(string prefix, string ruleId = "immediate-archive")
{
BackBatchGetToken();
if (ObjectStoreServiceOptions.ObjectStoreUse == "AliyunOSS")
{
var aliConfig = ObjectStoreServiceOptions.AliyunOSS;
var _ossClient = new OssClient(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? aliConfig.EndPoint : aliConfig.InternalEndpoint, AliyunOSSTempToken.AccessKeyId, AliyunOSSTempToken.AccessKeySecret, AliyunOSSTempToken.SecurityToken);
ruleId = $"{ruleId}_{prefix}";
var rule = new Aliyun.OSS.LifecycleRule
{
ID = ruleId,
Prefix = prefix,
Status = RuleStatus.Enabled,
Transitions = new Aliyun.OSS.LifecycleRule.LifeCycleTransition[]
{
new Aliyun.OSS.LifecycleRule.LifeCycleTransition
{
LifeCycleExpiration =
{
Days = 1
},
StorageClass = StorageClass.IA
},
new Aliyun.OSS.LifecycleRule.LifeCycleTransition
{
LifeCycleExpiration =
{
Days = 30
},
StorageClass = StorageClass.Archive
}
}
};
// if (ObjectStoreServiceOptions.ObjectStoreUse == "AliyunOSS")
// {
// var aliConfig = ObjectStoreServiceOptions.AliyunOSS;
//会清空之前历史的规则,不能用。。。
var request = new SetBucketLifecycleRequest(aliConfig.BucketName);
request.AddLifecycleRule(rule);
// var _ossClient = new OssClient(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? aliConfig.EndPoint : aliConfig.InternalEndpoint, AliyunOSSTempToken.AccessKeyId, AliyunOSSTempToken.AccessKeySecret, AliyunOSSTempToken.SecurityToken);
_ossClient.SetBucketLifecycle(request);
// var rule = new Aliyun.OSS.LifecycleRule
// {
// ID = "ArchiveOldFiles",
// Prefix = "", // 全 bucket 生效
// Status = RuleStatus.Enabled
// };
}
else if (ObjectStoreServiceOptions.ObjectStoreUse == "AWS")
{
var awsConfig = ObjectStoreServiceOptions.AWS;
// // 30 天转低频
// rule.Transitions.Add(new Aliyun.OSS.LifecycleRule.LifeCycleTransition
// {
// Days = 30,
// StorageClass = StorageClass.IA
// });
// // 180 天转归档
// rule.Transitions.Add(new Aliyun.OSS.LifecycleRule.LifeCycleTransition
// {
// Days = 180,
// StorageClass = StorageClass.Archive
// });
// // 365 天转冷归档
// rule.Transitions.Add(new Aliyun.OSS.LifecycleRule.LifeCycleTransition
// {
// Days = 365,
// StorageClass = StorageClass.ColdArchive
// });
// // 730 天转深度归档
// rule.Transitions.Add(new Aliyun.OSS.LifecycleRule.LifeCycleTransition
// {
// Days = 730,
// StorageClass = StorageClass.DeepColdArchive
// });
// var request = new SetBucketLifecycleRequest(aliConfig.BucketName);
// request.AddLifecycleRule(rule);
// _ossClient.SetBucketLifecycle(request);
// }
// else if (ObjectStoreServiceOptions.ObjectStoreUse == "AWS")
// {
// var awsConfig = ObjectStoreServiceOptions.AWS;
// var credentials = new SessionAWSCredentials(AWSTempToken.AccessKeyId, AWSTempToken.SecretAccessKey, AWSTempToken.SessionToken);
var credentials = new SessionAWSCredentials(AWSTempToken.AccessKeyId, AWSTempToken.SecretAccessKey, AWSTempToken.SessionToken);
// //提供awsEndPoint域名进行访问配置
// var clientConfig = new AmazonS3Config
// {
// RegionEndpoint = RegionEndpoint.USEast1,
// UseHttp = true,
// };
//提供awsEndPoint域名进行访问配置
var clientConfig = new AmazonS3Config
{
RegionEndpoint = RegionEndpoint.USEast1,
UseHttp = true,
};
// var amazonS3Client = new AmazonS3Client(credentials, clientConfig);
var amazonS3Client = new AmazonS3Client(credentials, clientConfig);
// }
// else
// {
// throw new BusinessValidationFailedException("未定义的存储介质类型");
// }
//}
}
else
{
throw new BusinessValidationFailedException("未定义的存储介质类型");
}
}
/// <summary>
/// oosFolderPath 不要 "/ "开头 应该: TempFolder/ChildFolder
@ -432,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);
@ -558,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);
@ -633,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);
@ -839,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);
@ -1213,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);
@ -1228,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
@ -1258,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();
@ -1268,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));
}
}
@ -1306,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);
@ -1381,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
@ -1466,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

@ -15740,14 +15740,23 @@
利用DocX 库 处理word国际化模板
</summary>
</member>
<member name="M:IRaCIS.Core.Application.Helper.OSSService.SetImmediateArchiveRule(System.String,Aliyun.OSS.StorageClass,System.String)">
<member name="M:IRaCIS.Core.Application.Helper.OSSService.SetImmediateArchiveRule(System.String,System.String,System.Boolean)">
<summary>
将指定前缀下的所有现有文件立即转为目标存储类型
核心Days = 0 表示对所有存量文件立即生效
</summary>
<param name="prefix">要转换的文件前缀,如 "project-a/logs/"</param>
<param name="targetStorageClass">目标存储类型</param>
<param name="ruleId">规则ID默认为"immediate-archive"</param>
<param name="isDelete">默认是添加/更新 </param>
</member>
<!-- Badly formed XML comment ignored for member "M:IRaCIS.Core.Application.Helper.OSSService.RestoreFilesByPrefixAsync(System.String,System.Int32,System.Int32)" -->
<member name="M:IRaCIS.Core.Application.Helper.OSSService.SetLifecycle(System.String,System.String)">
<summary>
坑方法,会清空之前的规则
</summary>
<param name="prefix"></param>
<param name="ruleId"></param>
<returns></returns>
<exception cref="T:IRaCIS.Core.Infrastructure.BusinessValidationFailedException"></exception>
</member>
<member name="M:IRaCIS.Core.Application.Helper.OSSService.UploadToOSSAsync(System.IO.Stream,System.String,System.String,System.Boolean)">
<summary>

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

@ -102,7 +102,7 @@ namespace IRaCIS.Core.Application.Services
{
IsPublish = true,
IsDeleted = false,
},false,true);
}, false, true);
await _trialDocumentRepository.SaveChangesAsync();
Console.WriteLine("开始 发布项目文档");
@ -150,7 +150,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 +171,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 +180,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 +353,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 +392,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 +582,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 +713,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,7 +946,7 @@ 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>());
}

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

@ -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

@ -1192,7 +1192,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["DICOMDIR"]}" });
}
}
@ -1632,11 +1632,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["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["DICOMDIR"]}" });
}
}
@ -2310,7 +2310,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["DICOMDIR"]}" });
}
}
}

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

@ -1,4 +1,5 @@
using IRaCIS.Application.Contracts;
using Aliyun.OSS;
using IRaCIS.Application.Contracts;
using IRaCIS.Application.Interfaces;
using IRaCIS.Core.Application.Contracts;
using IRaCIS.Core.Application.Filter;
@ -32,7 +33,7 @@ namespace IRaCIS.Core.Application
IRepository<ClinicalDataTrialSet> _clinicalDataTrialSetRepository,
IRepository<ReadingCriterionPage> _readingCriterionPageRepository,
IRepository<SystemCriterionKeyFile> _systemCriterionKeyFileRepository,
IOSSService oSSService,
IOSSService _oSSService,
IRepository<TrialCriterionKeyFile> _trialCriterionKeyFileRepository,
IOrganInfoService _iOrganInfoService,
IRepository<TrialBodyPart> _trialBodyPartRepository,
@ -331,22 +332,22 @@ namespace IRaCIS.Core.Application
public async Task SynchronizeKeyFile(Guid TrialReadingCriterionId)
{
var trialCriterion = await _readingQuestionCriterionTrialRepository.Where(x => x.Id == TrialReadingCriterionId).AsNoTracking().FirstOrDefaultAsync();
if (trialCriterion != null && trialCriterion.ReadingQuestionCriterionSystemId!=null)
if (trialCriterion != null && trialCriterion.ReadingQuestionCriterionSystemId != null)
{
var systemCriterionKeyFile = await _systemCriterionKeyFileRepository.Where(x => x.SystemCriterionId == trialCriterion.ReadingQuestionCriterionSystemId).ToListAsync();
List<TrialCriterionKeyFile> trialCriterionKeyFiles= new List<TrialCriterionKeyFile>();
List<TrialCriterionKeyFile> trialCriterionKeyFiles = new List<TrialCriterionKeyFile>();
foreach (var item in systemCriterionKeyFile)
{
var path= await oSSService.UploadToOSSAsync(item.FilePath, $"{trialCriterion.TrialId}/ReadingModule/{trialCriterion.CriterionName}", true,true);
var path = await _oSSService.UploadToOSSAsync(item.FilePath, $"{trialCriterion.TrialId}/ReadingModule/{trialCriterion.CriterionName}", true, true);
trialCriterionKeyFiles.Add(new TrialCriterionKeyFile
{
TrialCriterionId= TrialReadingCriterionId,
FileName= item.FileName,
FilePath= path
TrialCriterionId = TrialReadingCriterionId,
FileName = item.FileName,
FilePath = path
});
}
@ -1325,6 +1326,18 @@ namespace IRaCIS.Core.Application
await _trialRepository.BatchUpdateNoTrackingAsync(u => u.Id == trialId, s => new Trial { TrialFinishedTime = DateTime.Now });
if (_readingQuestionCriterionTrialRepository.Any(t => t.IsSigned && t.ImageUploadEnum != ReadingImageUpload.None))
{
await _oSSService.SetImmediateArchiveRule($"{trial.Id}/Image/");
await _oSSService.SetImmediateArchiveRule($"{trial.Id}/TaskImage/");
}
else
{
await _oSSService.SetImmediateArchiveRule($"{trial.Id}/Image/");
}
}
await _fusionCache.SetAsync(CacheKeys.Trial(trial.Id.ToString()), trialStatusStr, TimeSpan.FromDays(7));
@ -1546,7 +1559,7 @@ namespace IRaCIS.Core.Application
[AllowAnonymous]
public async Task<TrialConfigInfo> GetTrialExtralConfig(Guid trialId)
{
var extralObj = _trialRepository.Where(t => t.Id == trialId).Select(t => new { t.TrialExtraConfigJsonStr,t.IsExternalViewTrialChart, t.TrialObjectNameList, t.CollectImagesEnum, t.IsIQCAutoNextTask }).FirstOrDefault();
var extralObj = _trialRepository.Where(t => t.Id == trialId).Select(t => new { t.TrialExtraConfigJsonStr, t.IsExternalViewTrialChart, t.TrialObjectNameList, t.CollectImagesEnum, t.IsIQCAutoNextTask }).FirstOrDefault();
var extralConfig = JsonConvert.DeserializeObject<TrialExtraConfig>(extralObj?.TrialExtraConfigJsonStr) ?? new TrialExtraConfig();

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;
@ -75,12 +76,45 @@ namespace IRaCIS.Core.Application.Service
{
public static int IntValue = 100;
[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)
{
_IOSSService.SetImmediateArchiveRule($"Test-Archive/Archive{(int)storageClass}", storageClass);
await _IOSSService.SetImmediateArchiveRule($"Test-Archive/Archive{(int)storageClass}/");
//await _IOSSService.RestoreFilesByPrefixAsync($"Test-Archive/Archive{(int)storageClass}/");
}

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