diff --git a/IRC.Core.Dicom/IRC.Core.Dicom.csproj b/IRC.Core.Dicom/IRC.Core.Dicom.csproj
index d30f32218..3a9fd6fda 100644
--- a/IRC.Core.Dicom/IRC.Core.Dicom.csproj
+++ b/IRC.Core.Dicom/IRC.Core.Dicom.csproj
@@ -12,8 +12,9 @@
-
-
+
+
+
diff --git a/IRC.Core.Dicom/Service/OSSService.cs b/IRC.Core.Dicom/Service/OSSService.cs
index 1ac5e80ee..0884215bf 100644
--- a/IRC.Core.Dicom/Service/OSSService.cs
+++ b/IRC.Core.Dicom/Service/OSSService.cs
@@ -118,7 +118,7 @@ public class AWSTempToken
public string SecretAccessKey { get; set; }
public string BucketName { get; set; }
public string ViewEndpoint { get; set; }
- public DateTime Expiration { get; set; }
+ public DateTime? Expiration { get; set; }
}
public enum ObjectStoreUse
@@ -231,8 +231,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);
@@ -320,8 +320,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);
@@ -397,8 +397,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);
@@ -493,8 +493,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);
@@ -625,8 +625,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);
diff --git a/IRC.Core.SCP/HostConfig/SyncFileRecoveryService.cs b/IRC.Core.SCP/HostConfig/SyncFileRecoveryService.cs
new file mode 100644
index 000000000..1a994d722
--- /dev/null
+++ b/IRC.Core.SCP/HostConfig/SyncFileRecoveryService.cs
@@ -0,0 +1,175 @@
+using IRaCIS.Core.Application.Helper;
+using IRaCIS.Core.Application.Service;
+using IRaCIS.Core.Domain.Models;
+using IRaCIS.Core.Infra.EFCore;
+using IRaCIS.Core.SCP.Service;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using System;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace IRaCIS.Core.SCP;
+
+
+public class SyncFileRecoveryService(IServiceScopeFactory _scopeFactory, FileSyncQueue _fileSyncQueue) : BackgroundService
+{
+
+ private readonly int _pageSize = 500;
+
+ ///
+ /// 多个程序,如果恢复同一份数据,造成重复同步,SCP服务不恢复任务
+ ///
+ ///
+ ///
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ //在本地调试的时候,不干涉部署同步任务
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ return;
+ }
+
+
+ await Task.CompletedTask;
+
+ //using var scope = _scopeFactory.CreateScope();
+ //var fileUploadRecordRepository = scope.ServiceProvider.GetRequiredService>();
+
+ //// 延迟启动,保证主机快速启动
+ //await Task.Delay(5000, stoppingToken);
+
+ //int page = 0;
+
+
+ //while (!stoppingToken.IsCancellationRequested)
+ //{
+ // // 分页获取未入队任务
+ // var pending = await fileUploadRecordRepository
+ // .Where(x => x.IsNeedSync == true && (x.IsSync == false || x.IsSync == null))
+ // .OrderByDescending(x => x.Priority)
+ // .Select(t => new { t.Id, t.Priority })
+ // .Skip(page * _pageSize)
+ // .Take(_pageSize)
+ // .ToListAsync(stoppingToken);
+
+ // if (!pending.Any())
+ // break; // 扫描完毕,退出循环
+
+ // foreach (var file in pending)
+ // {
+ // //file.IsQueued = true; // 避免重复入队
+ // _fileSyncQueue.Enqueue(file.Id, file.Priority ?? 0); // 放入队列
+ // }
+
+ // page++; // 下一页
+ // await Task.Delay(200, stoppingToken); // 缓解数据库压力
+ //}
+
+
+ }
+}
+
+public class FileSyncWorker(IServiceScopeFactory _scopeFactory, ILogger _logger, FileSyncQueue _fileSyncQueue) : BackgroundService
+{
+
+ // ⭐ 自动根据服务器CPU
+ private readonly int _workerCount = Math.Max(1, Environment.ProcessorCount - 1);
+
+
+
+ protected override Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ for (int i = 0; i < _workerCount; i++)
+ Task.Run(() => WorkerLoop(stoppingToken));
+
+ return Task.CompletedTask;
+ }
+
+
+ private async Task WorkerLoop(CancellationToken stoppingToken)
+ {
+ while (!stoppingToken.IsCancellationRequested)
+ {
+ var id = await _fileSyncQueue.DequeueAsync(stoppingToken);
+
+ try
+ {
+ using var scope = _scopeFactory.CreateScope();
+ var _fileUploadRecordRepository = scope.ServiceProvider.GetRequiredService>();
+ var _uploadFileSyncRecordRepository = scope.ServiceProvider.GetRequiredService>();
+
+ var syncConfig = (scope.ServiceProvider.GetRequiredService>()).CurrentValue;
+
+ var oss = scope.ServiceProvider.GetRequiredService();
+
+ var file = await _fileUploadRecordRepository.FirstOrDefaultAsync(t => t.Id == id);
+
+ // ✅ 不要 return
+ if (file == null || file.IsNeedSync != true || syncConfig.IsOpenStoreSync == false)
+ {
+ continue;
+ }
+
+ if (syncConfig.SyncConfigList.Any(t => t.UploadRegion == file.UploadRegion && t.IsOpenSync == false))
+ {
+ continue;
+ }
+
+ //如果发现系统配置某一边同步进行了关闭,那么就直接返回,不执行任务
+ if (syncConfig.SyncConfigList.Any(t => t.UploadRegion == file.UploadRegion && t.IsOpenSync == false))
+ {
+ return;
+ }
+
+ var log = new UploadFileSyncRecord
+ {
+ FileUploadRecordId = id,
+ StartTime = DateTime.Now,
+ JobState = jobState.RUNNING
+ };
+
+ await _uploadFileSyncRecordRepository.AddAsync(log);
+ await _uploadFileSyncRecordRepository.SaveChangesAsync(stoppingToken);
+
+ try
+ {
+ await oss.SyncFileAsync(file.Path.TrimStart('/'), file.UploadRegion == "CN" ? ObjectStoreUse.AliyunOSS : ObjectStoreUse.AWS, file.UploadRegion == "CN" ? ObjectStoreUse.AWS : ObjectStoreUse.AliyunOSS);
+
+ file.IsSync = true;
+ file.SyncFinishedTime = DateTime.Now;
+
+ log.JobState = jobState.SUCCESS;
+ }
+ catch (Exception ex)
+ {
+ log.JobState = jobState.FAILED;
+
+ log.Msg = ex.Message.Length > 300 ? ex.Message.Substring(0, 300) : ex.Message;
+ }
+
+ log.EndTime = DateTime.Now;
+
+ await _uploadFileSyncRecordRepository.SaveChangesAsync(stoppingToken);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Sync failed {Id}", id);
+ }
+ finally
+ {
+ // ⭐⭐⭐ 永远执行
+ _fileSyncQueue.Complete(id);
+ }
+ }
+
+
+ }
+}
+
+
diff --git a/IRC.Core.SCP/IRC.Core.SCP.csproj b/IRC.Core.SCP/IRC.Core.SCP.csproj
index ca9724a3b..b44373939 100644
--- a/IRC.Core.SCP/IRC.Core.SCP.csproj
+++ b/IRC.Core.SCP/IRC.Core.SCP.csproj
@@ -7,28 +7,28 @@
-
-
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
+
+
true
-
-
-
-
-
+
+
+
+
+
diff --git a/IRC.Core.SCP/Program.cs b/IRC.Core.SCP/Program.cs
index dfffed1ec..db4c1450e 100644
--- a/IRC.Core.SCP/Program.cs
+++ b/IRC.Core.SCP/Program.cs
@@ -63,6 +63,10 @@ builder.Host
#region 配置服务
var _configuration = builder.Configuration;
+builder.Services.AddSingleton();
+builder.Services.AddHostedService();
+builder.Services.AddHostedService();
+
//健康检查
builder.Services.AddHealthChecks();
diff --git a/IRC.Core.SCP/Service/CStoreSCPService.cs b/IRC.Core.SCP/Service/CStoreSCPService.cs
index 820e5c7f8..e7c4f18f9 100644
--- a/IRC.Core.SCP/Service/CStoreSCPService.cs
+++ b/IRC.Core.SCP/Service/CStoreSCPService.cs
@@ -8,6 +8,7 @@ using IRaCIS.Core.Infra.EFCore;
using IRaCIS.Core.Infrastructure;
using IRaCIS.Core.Infrastructure.Extention;
using IRaCIS.Core.SCP.Service;
+using MassTransit;
using Medallion.Threading;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion.Internal;
using Microsoft.Extensions.DependencyInjection;
@@ -95,7 +96,7 @@ namespace IRaCIS.Core.SCP.Service
public Task OnReceiveAssociationRequestAsync(DicomAssociation association)
{
- _upload = new SCPImageUpload() { StartTime = DateTime.Now, CallingAE = association.CallingAE, CalledAE = association.CalledAE, CallingAEIP = association.RemoteHost };
+ _upload = new SCPImageUpload() { Id = NewId.NextSequentialGuid(), StartTime = DateTime.Now, CallingAE = association.CallingAE, CalledAE = association.CalledAE, CallingAEIP = association.RemoteHost };
Log.Logger.Warning($"接收到来自{association.CallingAE}的连接");
@@ -540,12 +541,12 @@ namespace IRaCIS.Core.SCP.Service
//保留原始偏移表
- if (originOffsetTable.Count == pixelData.NumberOfFrames)
- {
- newFragments.OffsetTable.AddRange(originOffsetTable.ToArray());
+ //if (originOffsetTable.Count == pixelData.NumberOfFrames)
+ //{
+ // newFragments.OffsetTable.AddRange(originOffsetTable.ToArray());
- }
- else
+ //}
+ //else
{
newFragments.OffsetTable.AddRange(bot.ToArray());
@@ -644,7 +645,7 @@ namespace IRaCIS.Core.SCP.Service
ms.Position = 0;
//irc 从路径最后一截取Guid
- storeRelativePath = await ossService.UploadToOSSAsync(ms, ossFolderPath, instanceId.ToString(), false);
+ storeRelativePath = await ossService.UploadToOSSAsync(ms, ossFolderPath, instanceId.ToString(), false, uploadInfo: new FileUploadRecordAddOrEdit() { TrialId = _trialId, BatchDataType = BatchDataType.PACSReceive, UploadBatchId = _upload.Id.ToString() });
fileSize = ms.Length;
@@ -674,7 +675,7 @@ namespace IRaCIS.Core.SCP.Service
// 上传缩略图到 OSS
- var seriesPath = await ossService.UploadToOSSAsync(memoryStream, ossFolderPath, $"{seriesId.ToString()}_{instanceId.ToString()}.preview.jpg", false);
+ var seriesPath = await ossService.UploadToOSSAsync(memoryStream, ossFolderPath, $"{seriesId.ToString()}_{instanceId.ToString()}.preview.jpg", false, uploadInfo: new FileUploadRecordAddOrEdit() { TrialId = _trialId, BatchDataType = BatchDataType.PACSReceive });
series.ImageResizePath = seriesPath;
diff --git a/IRC.Core.SCP/Service/CacheHelper.cs b/IRC.Core.SCP/Service/CacheHelper.cs
index 590cd6a20..a0a1ba7fe 100644
--- a/IRC.Core.SCP/Service/CacheHelper.cs
+++ b/IRC.Core.SCP/Service/CacheHelper.cs
@@ -69,6 +69,8 @@ public static class CacheKeys
public static string UserMFAVerifyPass(Guid userId, string browserFingerprint) => $"UserMFAVerifyPass:{userId}:{browserFingerprint}";
public static string TrialSiteInfo(Guid trialSiteId) => $"{trialSiteId}TrialSiteInfo";
+
+ public static string TrialDataStoreType(Guid trialId) => $"TrialDataStoreType:{trialId}";
}
public static class CacheHelper
diff --git a/IRC.Core.SCP/Service/DicomArchiveService.cs b/IRC.Core.SCP/Service/DicomArchiveService.cs
index 2d0a54d8c..e907754e0 100644
--- a/IRC.Core.SCP/Service/DicomArchiveService.cs
+++ b/IRC.Core.SCP/Service/DicomArchiveService.cs
@@ -204,6 +204,12 @@ namespace IRaCIS.Core.SCP.Service
AcquisitionNumber = dataset.GetSingleValueOrDefault(DicomTag.AcquisitionNumber, string.Empty),
TriggerTime = dataset.GetSingleValueOrDefault(DicomTag.TriggerTime, string.Empty),
+ RadionuclideTotalDose= dataset.GetSingleValueOrDefault(DicomTag.RadionuclideTotalDose, string.Empty),
+
+ RadionuclideHalfLife= dataset.GetSingleValueOrDefault(DicomTag.RadionuclideHalfLife, string.Empty),
+ RadiopharmaceuticalStartTime = dataset.GetSingleValueOrDefault(DicomTag.RadiopharmaceuticalStartTime, string.Empty),
+
+
Manufacturer = dataset.GetSingleValueOrDefault(DicomTag.Manufacturer, string.Empty),
ManufacturerModelName = dataset.GetSingleValueOrDefault(DicomTag.ManufacturerModelName, string.Empty),
DeviceSerialNumber = dataset.GetSingleValueOrDefault(DicomTag.DeviceSerialNumber, string.Empty),
diff --git a/IRC.Core.SCP/Service/FileUploadRecordService.cs b/IRC.Core.SCP/Service/FileUploadRecordService.cs
new file mode 100644
index 000000000..cc5affc4d
--- /dev/null
+++ b/IRC.Core.SCP/Service/FileUploadRecordService.cs
@@ -0,0 +1,334 @@
+
+//--------------------------------------------------------------------
+// 此代码由liquid模板自动生成 byzhouhang 20240909
+// 生成时间 2026-03-10 06:15:17Z
+// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。
+//--------------------------------------------------------------------
+using AutoMapper;
+using IRaCIS.Core.Application.Helper;
+
+using IRaCIS.Core.Domain.Models;
+using IRaCIS.Core.Domain.Share;
+using IRaCIS.Core.Infra.EFCore;
+using IRaCIS.Core.Infra.EFCore.Common;
+using IRaCIS.Core.Infrastructure.Extention;
+using IRaCIS.Core.SCP;
+using IRaCIS.Core.SCP.Service;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Localization;
+using Microsoft.Extensions.Options;
+using System.Drawing;
+using System.Threading.Channels;
+using System.Threading.Tasks;
+using ZiggyCreatures.Caching.Fusion;
+
+namespace IRaCIS.Core.SCP.Service;
+
+public class FileUploadRecordAddOrEdit
+{
+ public Guid? Id { get; set; }
+
+
+ public string FileName { get; set; }
+
+ public long FileSize { get; set; }
+
+ public string FileType { get; set; }
+
+ public string Path { get; set; }
+
+
+ public string UploadBatchId { get; set; }
+ public BatchDataType BatchDataType { get; set; }
+
+ public string StudyCode { get; set; }
+
+ public Guid? TrialId { get; set; }
+
+ public Guid? SubjectId { get; set; }
+
+ public Guid? SubjectVisitId { get; set; }
+
+ public Guid? DicomStudyId { get; set; }
+
+ public Guid? NoneDicomStudyId { get; set; }
+
+
+
+ public string FileMarkId { get; set; }
+
+ public int? Priority { get; set; }
+ public string IP { get; set; }
+ public bool? IsNeedSync { get; set; }
+ public string UploadRegion { get; set; }
+ public string TargetRegion { get; set; }
+
+ public bool? IsSync { get; set; }
+}
+public interface IFileUploadRecordService
+{
+
+
+ Task AddOrUpdateFileUploadRecord(FileUploadRecordAddOrEdit addOrEditFileUploadRecord);
+
+}
+
+[ApiExplorerSettings(GroupName = "Common")]
+public class FileUploadRecordService(IRepository _fileUploadRecordRepository,
+ IMapper _mapper, IUserInfo _userInfo, IStringLocalizer _localizer, IOptionsMonitor options,
+ IFusionCache _fusionCache, IRepository _trialRepository, FileSyncQueue _fileSyncQueue) : BaseService, IFileUploadRecordService
+{
+
+ ObjectStoreServiceOptions ObjectStoreServiceConfig = options.CurrentValue;
+
+
+
+
+
+
+ public async Task AddOrUpdateFileUploadRecord(FileUploadRecordAddOrEdit addOrEditFileUploadRecord)
+ {
+
+ addOrEditFileUploadRecord.IP = _userInfo.IP;
+
+ if (ObjectStoreServiceConfig.IsOpenStoreSync && _userInfo.Domain.IsNotNullOrEmpty())
+ {
+ var find = ObjectStoreServiceConfig.SyncConfigList.FirstOrDefault(t => t.Domain == _userInfo.Domain);
+ if (find != null)
+ {
+ addOrEditFileUploadRecord.UploadRegion = find.UploadRegion;
+ addOrEditFileUploadRecord.TargetRegion = find.TargetRegion;
+
+ }
+ else
+ {
+ //前后端调试的时候,上传的时候域名不对应,自动按照后端配置设置上传区域和同步区域
+ var apiDefalut = ObjectStoreServiceConfig.SyncConfigList.FirstOrDefault(t => t.UploadRegion == ObjectStoreServiceConfig.ApiDeployRegion);
+
+ if (apiDefalut != null)
+ {
+ addOrEditFileUploadRecord.UploadRegion = apiDefalut.UploadRegion;
+ addOrEditFileUploadRecord.TargetRegion = apiDefalut.TargetRegion;
+
+ }
+ }
+ }
+
+ //SCP推送过来IP为空,后端归档的,设置区域
+ if (_userInfo.IP.IsNullOrEmpty() && _userInfo.Domain.IsNullOrEmpty())
+ {
+ var apiDefalut = ObjectStoreServiceConfig.SyncConfigList.FirstOrDefault(t => t.UploadRegion == ObjectStoreServiceConfig.ApiDeployRegion);
+
+ if (apiDefalut != null)
+ {
+ addOrEditFileUploadRecord.UploadRegion = apiDefalut.UploadRegion;
+ addOrEditFileUploadRecord.TargetRegion = apiDefalut.TargetRegion;
+
+ }
+ }
+
+ if (addOrEditFileUploadRecord.TrialId != null)
+ {
+
+ var trialDataStore = await _fusionCache.GetOrSetAsync(CacheKeys.TrialDataStoreType(addOrEditFileUploadRecord.TrialId.Value), async _ =>
+ {
+ return await _trialRepository.Where(t => t.Id == addOrEditFileUploadRecord.TrialId).Select(t => t.TrialDataStoreType)
+ .FirstOrDefaultAsync();
+ },
+ TimeSpan.FromDays(7)
+ );
+
+ //项目配置了,那么就设置需要同步
+ if (trialDataStore == TrialDataStore.MUtiCenter)
+ {
+ addOrEditFileUploadRecord.IsNeedSync = true;
+
+ addOrEditFileUploadRecord.Priority = addOrEditFileUploadRecord.Priority ?? 0;
+
+ addOrEditFileUploadRecord.IsSync = false;
+ }
+ else
+ {
+ addOrEditFileUploadRecord.IsNeedSync = false;
+
+ //addOrEditFileUploadRecord.TargetRegion = "";
+
+ }
+
+ }
+ else
+ {
+ //系统文件,默认同步
+ addOrEditFileUploadRecord.IsNeedSync = true;
+
+ addOrEditFileUploadRecord.IsSync = false;
+
+ addOrEditFileUploadRecord.Priority = addOrEditFileUploadRecord.Priority ?? 0;
+ }
+
+ var entity = await _fileUploadRecordRepository.InsertOrUpdateAsync(addOrEditFileUploadRecord, true);
+
+ if (addOrEditFileUploadRecord.IsNeedSync == true)
+ {
+ _fileSyncQueue.Enqueue(entity.Id, addOrEditFileUploadRecord.Priority ?? 0);
+ }
+
+ return ResponseOutput.Ok(entity.Id.ToString());
+
+ }
+
+
+
+
+}
+
+
+
+
+public sealed class FileSyncQueue
+{
+ ///
+ /// 优先级队列(仅负责排序)
+ ///
+ private readonly PriorityQueue _queue = new();
+
+ ///
+ /// 当前等待中的任务(唯一真实数据)
+ /// key = Guid
+ /// value = 最新 priority
+ ///
+ private readonly Dictionary _waiting = new();
+
+ ///
+ /// 正在执行的任务(防止重复执行)
+ ///
+ private readonly HashSet _running = new();
+
+ ///
+ /// worker 等待信号
+ ///
+ private readonly SemaphoreSlim _signal = new(0);
+
+
+ private readonly object _lock = new();
+
+ // ============================================================
+ // Enqueue
+ // ============================================================
+
+ ///
+ /// 入队(同 Guid 会覆盖优先级)
+ ///
+ public void Enqueue(Guid id, int priority)
+ {
+ bool needSignal = false;
+
+ lock (_lock)
+ {
+ // 如果正在执行,忽略(防止重复)
+ if (_running.Contains(id))
+ return;
+
+ // 是否新任务(用于减少 signal 风暴)
+ if (!_waiting.ContainsKey(id))
+ needSignal = true;
+
+ // 更新为最新优先级(最后一次为准)
+ _waiting[id] = priority; //等价于添加或者更新
+
+ // PriorityQueue 无法更新节点
+ // 允许旧节点存在,Dequeue 时过滤
+ _queue.Enqueue(id, -priority);
+ }
+
+ // 只有新增任务才唤醒 worker
+ if (needSignal)
+ _signal.Release();
+ }
+
+ // ============================================================
+ // Dequeue
+ // ============================================================
+
+ ///
+ /// 获取一个待执行任务(无任务时自动等待)
+ ///
+ public async Task DequeueAsync(CancellationToken ct)
+ {
+ while (true)
+ {
+ await _signal.WaitAsync(ct);
+
+ lock (_lock)
+ {
+ while (_queue.Count > 0)
+ {
+ var id = _queue.Dequeue();
+
+ // 已被覆盖或取消
+ if (!_waiting.TryGetValue(id, out _))
+ continue;
+
+ // 标记为运行中
+ _waiting.Remove(id);
+ _running.Add(id);
+
+ return id;
+ }
+ }
+ }
+ }
+
+ // ============================================================
+ // Complete
+ // ============================================================
+
+ ///
+ /// 任务执行完成(必须调用)
+ ///
+ public void Complete(Guid id)
+ {
+ lock (_lock)
+ {
+ _running.Remove(id);
+ }
+ }
+
+ // ============================================================
+ // Snapshot
+ // ============================================================
+
+ ///
+ /// 当前等待中的任务快照
+ ///
+ public Guid[] Snapshot()
+ {
+ lock (_lock)
+ {
+ return _waiting.Keys.ToArray();
+ }
+ }
+
+ // ============================================================
+ // 状态信息(调试用)
+ // ============================================================
+
+ public int WaitingCount
+ {
+ get
+ {
+ lock (_lock)
+ return _waiting.Count;
+ }
+ }
+
+ public int RunningCount
+ {
+ get
+ {
+ lock (_lock)
+ return _running.Count;
+ }
+ }
+}
\ No newline at end of file
diff --git a/IRC.Core.SCP/Service/OSSService.cs b/IRC.Core.SCP/Service/OSSService.cs
index 1ac5e80ee..35e58939d 100644
--- a/IRC.Core.SCP/Service/OSSService.cs
+++ b/IRC.Core.SCP/Service/OSSService.cs
@@ -1,5 +1,9 @@
using AlibabaCloud.SDK.Sts20150401;
using Aliyun.OSS;
+using Aliyun.OSS.Common;
+using Amazon;
+using AlibabaCloud.SDK.Sts20150401;
+using Aliyun.OSS;
using Amazon;
using Amazon.Runtime;
using Amazon.S3;
@@ -14,6 +18,11 @@ using Minio;
using Minio.DataModel.Args;
using System.Reactive.Linq;
using System.Runtime.InteropServices;
+using Minio.Exceptions;
+using IRaCIS.Core.SCP.Service;
+using Serilog;
+using System.Web;
+using IRaCIS.Core.Infrastructure.Extention;
namespace IRaCIS.Core.SCP;
@@ -59,7 +68,7 @@ public class AliyunOSSOptions
public int DurationSeconds { get; set; }
-
+ public string PreviewEndpoint { get; set; }
}
@@ -74,6 +83,28 @@ public class ObjectStoreServiceOptions
public AWSOptions AWS { get; set; }
+ public bool IsOpenStoreSync { get; set; }
+
+ public string ApiDeployRegion { get; set; }
+
+ public List SyncConfigList { get; set; } = new List();
+
+}
+
+public class SyncStoreConfig
+{
+ public string Domain { get; set; }
+
+ public string UploadRegion { get; set; }
+
+ public string TargetRegion { get; set; }
+
+ public string Primary { get; set; }
+
+ public string Target { get; set; }
+
+ public bool IsOpenSync { get; set; }
+
}
public class ObjectStoreDTO
@@ -87,6 +118,10 @@ public class ObjectStoreDTO
public AWSTempToken AWS { get; set; }
+ public bool IsOpenStoreSync { get; set; }
+
+ public List SyncConfigList { get; set; }
+
}
[LowerCamelCaseJson]
@@ -105,6 +140,9 @@ public class AliyunOSSTempToken
public string SecurityToken { get; set; }
public DateTime Expiration { get; set; }
+ public string PreviewEndpoint { get; set; }
+
+ public string DownloadEndPoint => EndPoint.Insert(EndPoint.IndexOf("//") + 2, BucketName + ".");
}
@@ -118,7 +156,7 @@ public class AWSTempToken
public string SecretAccessKey { get; set; }
public string BucketName { get; set; }
public string ViewEndpoint { get; set; }
- public DateTime Expiration { get; set; }
+ public DateTime? Expiration { get; set; }
}
public enum ObjectStoreUse
@@ -128,42 +166,570 @@ public enum ObjectStoreUse
AWS = 2,
}
+
#endregion
// aws 参考链接 https://github.com/awsdocs/aws-doc-sdk-examples/tree/main/dotnetv3/S3/S3_Basics
public interface IOSSService
{
- public Task UploadToOSSAsync(Stream fileStream, string oosFolderPath, string fileRealName, bool isFileNameAddGuid = true);
- public Task UploadToOSSAsync(string localFilePath, string oosFolderPath, bool isFileNameAddGuid = true);
+ public Task SetImmediateArchiveRule(string prefix, string ruleId = "immediate-archive", bool isDelete = false);
+ public Task RestoreFilesByPrefixAsync(string prefix, int restoreDays = 3, int batchSize = 100);
+
+
+ public Task UploadToOSSAsync(Stream fileStream, string oosFolderPath, string fileRealName, bool isFileNameAddGuid = true, FileUploadRecordAddOrEdit? uploadInfo = null);
+ public Task UploadToOSSAsync(string localFilePath, string oosFolderPath, bool isFileNameAddGuid = true, bool randomFileName = false, FileUploadRecordAddOrEdit? uploadInfo = null);
public Task DownLoadFromOSSAsync(string ossRelativePath, string localFilePath);
+ public Task GetStreamFromOSSAsync(string ossRelativePath);
+
public ObjectStoreServiceOptions ObjectStoreServiceOptions { get; set; }
public Task GetSignedUrl(string ossRelativePath);
- public Task DeleteFromPrefix(string prefix);
+ public Task DeleteFromPrefix(string prefix, bool isCache = false);
- public ObjectStoreDTO GetObjectStoreTempToken();
+ public Task DeleteObjects(List objectKeys, bool isCache = false);
+
+ List GetRootFolderNames();
+
+ public ObjectStoreDTO GetObjectStoreTempToken(string? domain = null, bool? isGetAllTempToken = null, string? objectUse = null);
+
+ public Task MoveObject(string sourcePath, string destPath, bool overwrite = true);
+
+ public Task GetObjectSizeAsync(string sourcePath);
+
+ public Task SyncFileAsync(string objectKey, ObjectStoreUse source, ObjectStoreUse destination, CancellationToken ct = default);
}
-public class OSSService : IOSSService
+public class OSSService(IOptionsMonitor options,
+ IFileUploadRecordService _fileUploadRecordService) : IOSSService
{
- public ObjectStoreServiceOptions ObjectStoreServiceOptions { get; set; }
+ public ObjectStoreServiceOptions ObjectStoreServiceOptions { get; set; } = options.CurrentValue;
private AliyunOSSTempToken AliyunOSSTempToken { get; set; }
private AWSTempToken AWSTempToken { get; set; }
- public OSSService(IOptionsMonitor options)
- {
- ObjectStoreServiceOptions = options.CurrentValue;
+
+ ///
+ /// 将指定前缀下的所有现有文件立即转为目标存储类型
+ ///
+ /// 要转换的文件前缀,如 "project-a/logs/"
+ /// 规则ID,默认为"immediate-archive"
+ /// 默认是添加/更新
+ public async Task SetImmediateArchiveRule(string prefix, string ruleId = "immediate-archive", bool isDelete = false)
+ {
+
+ 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);
+
+ try
+ {
+ // 1. 先获取现有的所有生命周期规则(避免覆盖)
+ var existingRules = new List();
+ try
+ {
+ 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
+ {
+
+ LifeCycleExpiration =
+ {
+ Days = 1
+ },
+ StorageClass = StorageClass.IA
+ },
+ new Aliyun.OSS.LifecycleRule.LifeCycleTransition
+ {
+
+ LifeCycleExpiration =
+ {
+ Days = 30 //最后一次修改时间
+ },
+ StorageClass = StorageClass.Archive
+ }
+ }
+ };
+
+
+
+
+ // 3. 移除同名的旧规则(如果存在)
+ existingRules.RemoveAll(r => r.ID == ruleId);
+
+ // 4. 添加新规则到规则列表
+ if (isDelete == false)
+ {
+ existingRules.Add(immediateRule);
+
+ }
+
+
+
+ var request = new SetBucketLifecycleRequest(aliConfig.BucketName)
+ {
+ LifecycleRules = existingRules
+ };
+
+
+ _ossClient.SetBucketLifecycle(request);
+
+
+ }
+ catch (OssException ex)
+ {
+ Log.Logger.Error($"❌ 设置失败 [错误码: {ex.ErrorCode}] 详细: {ex.Message}");
+
+ }
+ catch (Exception ex)
+ {
+ Log.Logger.Error($"❌ 发生未知错误: {ex.Message}");
+ }
+ }
+ else if (ObjectStoreServiceOptions.ObjectStoreUse == "AWS")
+ {
+ 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
+ {
+ // 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();
+ 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("未定义的存储介质类型");
+ }
}
+ /// 解冻指定前缀下的所有归档/冷归档文件
+ ///
+ /// 要解冻的文件前缀
+ /// 解冻后文件保持可读的天数(默认3天)
+ /// 解冻优先级(仅AWS有效)
+ /// 批量处理大小(默认100)
+ 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();
+
+ // 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();
+
+ // 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 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
+ }
+ }
+ };
+
+
+ //会清空之前历史的规则,不能用。。。
+ 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);
+
+
+
+ //提供awsEndPoint(域名)进行访问配置
+ var clientConfig = new AmazonS3Config
+ {
+ RegionEndpoint = RegionEndpoint.GetBySystemName(awsConfig.Region)
+ //,UseHttp = true,
+ };
+
+ var amazonS3Client = new AmazonS3Client(credentials, clientConfig);
+
+
+ }
+ else
+ {
+ throw new BusinessValidationFailedException("未定义的存储介质类型");
+ }
+ }
+
+
///
@@ -173,83 +739,78 @@ public class OSSService : IOSSService
///
///
///
+ /// 只用赋值业务参数Id 和批次信息即可,其他信息不用传递
///
- public async Task UploadToOSSAsync(Stream fileStream, string oosFolderPath, string fileRealName, bool isFileNameAddGuid = true)
+ public async Task UploadToOSSAsync(Stream fileStream, string oosFolderPath, string fileRealName, bool isFileNameAddGuid = true, FileUploadRecordAddOrEdit? uploadInfo = null)
{
- GetObjectStoreTempToken();
+ BackBatchGetToken();
var ossRelativePath = isFileNameAddGuid ? $"{oosFolderPath}/{Guid.NewGuid()}_{fileRealName}" : $"{oosFolderPath}/{fileRealName}";
try
{
- using (var memoryStream = new MemoryStream())
- {
+ if (fileStream.CanSeek)
fileStream.Seek(0, SeekOrigin.Begin);
- fileStream.CopyTo(memoryStream);
- memoryStream.Seek(0, SeekOrigin.Begin);
+ if (ObjectStoreServiceOptions.ObjectStoreUse == "AliyunOSS")
+ {
+ var aliConfig = ObjectStoreServiceOptions.AliyunOSS;
+
+ var _ossClient = new OssClient(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? aliConfig.EndPoint : aliConfig.InternalEndpoint, AliyunOSSTempToken.AccessKeyId, AliyunOSSTempToken.AccessKeySecret, AliyunOSSTempToken.SecurityToken);
- if (ObjectStoreServiceOptions.ObjectStoreUse == "AliyunOSS")
+
+ // 上传文件
+ var result = _ossClient.PutObject(aliConfig.BucketName, ossRelativePath, fileStream);
+
+ }
+ else if (ObjectStoreServiceOptions.ObjectStoreUse == "MinIO")
+ {
+ var minIOConfig = ObjectStoreServiceOptions.MinIO;
+
+
+ var minioClient = new MinioClient().WithEndpoint($"{minIOConfig.EndPoint}:{minIOConfig.Port}")
+ .WithCredentials(minIOConfig.AccessKeyId, minIOConfig.SecretAccessKey).WithSSL(minIOConfig.UseSSL)
+ .Build();
+
+ var putObjectArgs = new PutObjectArgs()
+ .WithBucket(minIOConfig.BucketName)
+ .WithObject(ossRelativePath)
+ .WithStreamData(fileStream)
+ .WithObjectSize(fileStream.Length);
+
+ await minioClient.PutObjectAsync(putObjectArgs);
+ }
+ else if (ObjectStoreServiceOptions.ObjectStoreUse == "AWS")
+ {
+ var awsConfig = ObjectStoreServiceOptions.AWS;
+
+ var credentials = new SessionAWSCredentials(AWSTempToken.AccessKeyId, AWSTempToken.SecretAccessKey, AWSTempToken.SessionToken);
+
+
+
+ //提供awsEndPoint(域名)进行访问配置
+ var clientConfig = new AmazonS3Config
{
- var aliConfig = ObjectStoreServiceOptions.AliyunOSS;
+ RegionEndpoint = RegionEndpoint.GetBySystemName(awsConfig.Region)
+ //,UseHttp = true,
+ };
- var _ossClient = new OssClient(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? aliConfig.EndPoint : aliConfig.InternalEndpoint, AliyunOSSTempToken.AccessKeyId, AliyunOSSTempToken.AccessKeySecret, AliyunOSSTempToken.SecurityToken);
+ var amazonS3Client = new AmazonS3Client(credentials, clientConfig);
-
-
- // 上传文件
- var result = _ossClient.PutObject(aliConfig.BucketName, ossRelativePath, memoryStream);
-
- }
- else if (ObjectStoreServiceOptions.ObjectStoreUse == "MinIO")
+ var putObjectRequest = new Amazon.S3.Model.PutObjectRequest()
{
- var minIOConfig = ObjectStoreServiceOptions.MinIO;
+ BucketName = awsConfig.BucketName,
+ InputStream = fileStream,
+ Key = ossRelativePath,
+ };
-
- var minioClient = new MinioClient().WithEndpoint($"{minIOConfig.EndPoint}:{minIOConfig.Port}")
- .WithCredentials(minIOConfig.AccessKeyId, minIOConfig.SecretAccessKey).WithSSL(minIOConfig.UseSSL)
- .Build();
-
- var putObjectArgs = new PutObjectArgs()
- .WithBucket(minIOConfig.BucketName)
- .WithObject(ossRelativePath)
- .WithStreamData(memoryStream)
- .WithObjectSize(memoryStream.Length);
-
- await minioClient.PutObjectAsync(putObjectArgs);
- }
- else if (ObjectStoreServiceOptions.ObjectStoreUse == "AWS")
- {
- var awsConfig = ObjectStoreServiceOptions.AWS;
-
- var credentials = new SessionAWSCredentials(AWSTempToken.AccessKeyId, AWSTempToken.SecretAccessKey, AWSTempToken.SessionToken);
-
-
-
- //提供awsEndPoint(域名)进行访问配置
- var clientConfig = new AmazonS3Config
- {
- RegionEndpoint = RegionEndpoint.USEast1,
- UseHttp = true,
- };
-
- var amazonS3Client = new AmazonS3Client(credentials, clientConfig);
-
- var putObjectRequest = new Amazon.S3.Model.PutObjectRequest()
- {
- BucketName = awsConfig.BucketName,
- InputStream = memoryStream,
- Key = ossRelativePath,
- };
-
- await amazonS3Client.PutObjectAsync(putObjectRequest);
- }
- else
- {
- throw new BusinessValidationFailedException("未定义的存储介质类型");
- }
+ await amazonS3Client.PutObjectAsync(putObjectRequest);
+ }
+ else
+ {
+ throw new BusinessValidationFailedException("未定义的存储介质类型");
}
}
catch (Exception ex)
@@ -259,30 +820,85 @@ public class OSSService : IOSSService
}
+ var returnPath = "/" + ossRelativePath;
- return "/" + ossRelativePath;
+ if (ObjectStoreServiceOptions.IsOpenStoreSync && uploadInfo != null)
+ {
+ var fileType = Path.GetExtension(returnPath).TrimStart('.');
+
+ uploadInfo.FileSize = fileStream.CanSeek ? fileStream.Length : 0;
+ uploadInfo.Path = returnPath;
+ uploadInfo.FileName = fileRealName;
+ uploadInfo.FileType = fileType.IsNullOrEmpty()?"dcm": fileType;
+
+
+ await _fileUploadRecordService.AddOrUpdateFileUploadRecord(uploadInfo);
+ }
+
+
+ return returnPath;
}
+ //后端批量上传 或者下载,不每个文件获取临时token
+ private void BackBatchGetToken()
+ {
+ if (ObjectStoreServiceOptions.ObjectStoreUse == "AliyunOSS")
+ {
+ if (AliyunOSSTempToken == null)
+ {
+ GetObjectStoreTempToken();
+ }
+ //token 过期了
+ if (AliyunOSSTempToken?.Expiration.AddSeconds(10) <= DateTime.Now)
+ {
+ GetObjectStoreTempToken();
+ }
+
+
+ }
+ else if (ObjectStoreServiceOptions.ObjectStoreUse == "AWS")
+ {
+ if (AWSTempToken == null)
+ {
+ GetObjectStoreTempToken();
+ }
+ //token 过期了
+ if (AWSTempToken.Expiration?.AddSeconds(10) <= DateTime.Now)
+ {
+ GetObjectStoreTempToken();
+ }
+ }
+ }
+
+
///
/// oosFolderPath 不要 "/ "开头 应该: TempFolder/ChildFolder
///
///
///
///
+ /// 随机文件名
///
///
- public async Task UploadToOSSAsync(string localFilePath, string oosFolderPath, bool isFileNameAddGuid = true)
+ public async Task UploadToOSSAsync(string localFilePath, string oosFolderPath, bool isFileNameAddGuid = true, bool randomFileName = false, FileUploadRecordAddOrEdit? uploadInfo = null)
{
- GetObjectStoreTempToken();
+ BackBatchGetToken();
+
+ long fileSize = 0;
var localFileName = Path.GetFileName(localFilePath);
var ossRelativePath = isFileNameAddGuid ? $"{oosFolderPath}/{Guid.NewGuid()}_{localFileName}" : $"{oosFolderPath}/{localFileName}";
+ if (randomFileName)
+ {
+ var fileExtension = localFileName.Split(".").LastOrDefault();
+ ossRelativePath = $"{oosFolderPath}/{Guid.NewGuid()}.{fileExtension}";
+ }
if (ObjectStoreServiceOptions.ObjectStoreUse == "AliyunOSS")
{
@@ -293,6 +909,8 @@ public class OSSService : IOSSService
// 上传文件
var result = _ossClient.PutObject(aliConfig.BucketName, ossRelativePath, localFilePath);
+ fileSize = result.ContentLength;
+
}
else if (ObjectStoreServiceOptions.ObjectStoreUse == "MinIO")
{
@@ -308,7 +926,9 @@ public class OSSService : IOSSService
.WithObject(ossRelativePath)
.WithFileName(localFilePath);
- await minioClient.PutObjectAsync(putObjectArgs);
+ var result = await minioClient.PutObjectAsync(putObjectArgs);
+
+ fileSize = result.Size;
}
else if (ObjectStoreServiceOptions.ObjectStoreUse == "AWS")
{
@@ -320,8 +940,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,21 +953,41 @@ public class OSSService : IOSSService
Key = ossRelativePath,
};
- await amazonS3Client.PutObjectAsync(putObjectRequest);
-
+ var result = await amazonS3Client.PutObjectAsync(putObjectRequest);
+ fileSize = result.ContentLength;
}
else
{
throw new BusinessValidationFailedException("未定义的存储介质类型");
}
- return "/" + ossRelativePath;
+
+ var returnPath = "/" + ossRelativePath;
+
+
+ if (ObjectStoreServiceOptions.IsOpenStoreSync && uploadInfo != null)
+ {
+
+ var fileType = Path.GetExtension(returnPath).TrimStart('.');
+
+ uploadInfo.FileSize = fileSize;
+ uploadInfo.Path = returnPath;
+ uploadInfo.FileName = Path.GetFileName(localFilePath);
+ uploadInfo.FileType = fileType.IsNullOrEmpty() ? "dcm" : fileType;
+
+
+
+ await _fileUploadRecordService.AddOrUpdateFileUploadRecord(uploadInfo);
+ }
+
+
+ return returnPath;
}
public async Task DownLoadFromOSSAsync(string ossRelativePath, string localFilePath)
{
- GetObjectStoreTempToken();
+ BackBatchGetToken();
ossRelativePath = ossRelativePath.TrimStart('/');
try
@@ -360,14 +1000,12 @@ public class OSSService : IOSSService
var _ossClient = new OssClient(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? aliConfig.EndPoint : aliConfig.InternalEndpoint, AliyunOSSTempToken.AccessKeyId, AliyunOSSTempToken.AccessKeySecret, AliyunOSSTempToken.SecurityToken);
- // 上传文件
var result = _ossClient.GetObject(aliConfig.BucketName, ossRelativePath);
// 将下载的文件流保存到本地文件
using (var fs = File.OpenWrite(localFilePath))
{
- result.Content.CopyTo(fs);
- fs.Close();
+ await result.Content.CopyToAsync(fs);
}
}
@@ -397,8 +1035,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);
@@ -431,6 +1069,107 @@ public class OSSService : IOSSService
}
+ public async Task GetStreamFromOSSAsync(string ossRelativePath)
+ {
+ BackBatchGetToken();
+ ossRelativePath = ossRelativePath.TrimStart('/');
+
+ try
+ {
+ 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
+ );
+
+ var result = _ossClient.GetObject(aliConfig.BucketName, ossRelativePath);
+
+ // 直接返回流
+ return result.Content;
+
+ }
+ else if (ObjectStoreServiceOptions.ObjectStoreUse == "MinIO")
+ {
+ var minIOConfig = ObjectStoreServiceOptions.MinIO;
+
+ var minioClient = new MinioClient()
+ .WithEndpoint($"{minIOConfig.EndPoint}:{minIOConfig.Port}")
+ .WithCredentials(minIOConfig.AccessKeyId, minIOConfig.SecretAccessKey)
+ .WithSSL(minIOConfig.UseSSL)
+ .Build();
+
+ var pipe = new System.IO.Pipelines.Pipe();
+
+ _ = Task.Run(async () =>
+ {
+ try
+ {
+ var args = new GetObjectArgs()
+ .WithBucket(minIOConfig.BucketName)
+ .WithObject(ossRelativePath)
+ .WithCallbackStream(stream =>
+ {
+ stream.CopyTo(pipe.Writer.AsStream());
+ });
+
+ await minioClient.GetObjectAsync(args);
+ await pipe.Writer.CompleteAsync();
+ }
+ catch (Exception ex)
+ {
+ await pipe.Writer.CompleteAsync(ex);
+ }
+ });
+
+ return pipe.Reader.AsStream();
+ }
+ 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,
+ };
+
+ var amazonS3Client = new AmazonS3Client(credentials, clientConfig);
+
+ var getObjectRequest = new Amazon.S3.Model.GetObjectRequest
+ {
+ BucketName = awsConfig.BucketName,
+ Key = ossRelativePath
+ };
+
+ var response = await amazonS3Client.GetObjectAsync(getObjectRequest);
+
+ // ⭐ 直接返回流
+ return response.ResponseStream;
+
+ }
+ else
+ {
+ throw new BusinessValidationFailedException("未定义的存储介质类型");
+ }
+ }
+ catch (Exception ex)
+ {
+ throw new BusinessValidationFailedException("oss流获取失败! " + ex.Message);
+ }
+ }
+
+
public async Task GetSignedUrl(string ossRelativePath)
{
GetObjectStoreTempToken();
@@ -493,8 +1232,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);
@@ -526,21 +1265,287 @@ public class OSSService : IOSSService
}
+
///
- /// 删除某个目录的文件
+ /// 移动OSS文件到新路径
///
- ///
- ///
- public async Task DeleteFromPrefix(string prefix)
+ /// 原文件路径(格式:bucket/key)
+ /// 新文件路径(格式:bucket/key)
+ /// 是否覆盖已存在的目标文件(默认true)
+ public async Task MoveObject(string sourcePath, string destPath, bool overwrite = true)
{
GetObjectStoreTempToken();
+ switch (ObjectStoreServiceOptions.ObjectStoreUse)
+ {
+ case "AliyunOSS":
+ #region 阿里云
+ var aliConfig = ObjectStoreServiceOptions.AliyunOSS;
+ var client = new OssClient(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? aliConfig.EndPoint : aliConfig.InternalEndpoint, AliyunOSSTempToken.AccessKeyId, AliyunOSSTempToken.AccessKeySecret, AliyunOSSTempToken.SecurityToken);
+ if (sourcePath.StartsWith("/"))
+ {
+ sourcePath = sourcePath.Substring(1);
+ }
+
+
+ if (destPath.StartsWith("/"))
+ {
+ destPath = destPath.Substring(1);
+ }
+
+ var sourceBucket = aliConfig.BucketName;
+ var sourceKey = sourcePath;
+
+ var destBucket = aliConfig.BucketName;
+
+ var destKey = destPath;
+
+
+ try
+ {
+ // 检查目标是否存在(当不允许覆盖时)
+ if (!overwrite && client.DoesObjectExist(destBucket, destKey))
+ {
+ throw new InvalidOperationException("File Exist");
+ }
+
+
+ //var copyRequest = new Aliyun.OSS.CopyObjectRequest(sourceBucket, sourceKey, sourceBucket, destKey);
+ //var result = client.CopyObject(copyRequest);
+
+ //// 2. 删除原文件(可选,根据是否需要保留原文件)
+ //client.DeleteObject(sourceBucket, sourceKey);
+
+ // 执行复制
+ var copyRequestAli = new Aliyun.OSS.CopyObjectRequest(
+ sourceBucket, sourceKey,
+ destBucket, destKey);
+
+ // 保持原文件元数据
+ copyRequestAli.NewObjectMetadata = new ObjectMetadata
+ {
+ ContentType = client.GetObjectMetadata(sourceBucket, sourceKey).ContentType
+ };
+
+ var result = client.CopyObject(copyRequestAli);
+
+ // 删除原文件(仅在复制成功后)
+ client.DeleteObject(sourceBucket, sourceKey);
+ }
+ catch (OssException ex)
+ {
+ throw new Exception($"[{ex.ErrorCode}] {ex.Message}", ex);
+ }
+ #endregion
+
+ break;
+ case "MinIO":
+ #region MinIO
+ var minIOConfig = ObjectStoreServiceOptions.MinIO;
+ var minioClient = new MinioClient()
+ .WithEndpoint($"{minIOConfig.EndPoint}:{minIOConfig.Port}")
+ .WithCredentials(minIOConfig.AccessKeyId, minIOConfig.SecretAccessKey)
+ .WithSSL(minIOConfig.UseSSL)
+ .Build();
+
+ // 定义源路径和目标路径
+ string destinationKey = "b路径/文件名";
+
+ try
+ {
+ // 1. 复制文件到新路径[2,5](@ref)
+ using (var memoryStream = new MemoryStream())
+ {
+ // 下载源文件流
+ await minioClient.GetObjectAsync(new GetObjectArgs()
+ .WithBucket(minIOConfig.BucketName)
+ .WithObject(sourcePath)
+ .WithCallbackStream(stream => stream.CopyTo(memoryStream)));
+
+ memoryStream.Position = 0; // 重置流位置
+
+ // 上传到新路径
+ await minioClient.PutObjectAsync(new PutObjectArgs()
+ .WithBucket(minIOConfig.BucketName)
+ .WithObject(destinationKey)
+ .WithStreamData(memoryStream)
+ .WithObjectSize(memoryStream.Length));
+ }
+
+ // 2. 删除原文件[1,6](@ref)
+ await minioClient.RemoveObjectAsync(new RemoveObjectArgs()
+ .WithBucket(minIOConfig.BucketName)
+ .WithObject(sourcePath));
+ }
+ catch (MinioException ex)
+ {
+ // 处理异常(例如:记录日志或抛出)
+ throw new Exception();
+ }
+ #endregion
+
+ break;
+ case "AWS":
+ #region 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,
+ };
+
+ var amazonS3Client = new AmazonS3Client(credentials, clientConfig);
+
+ // 定义原路径和目标路径
+
+
+ // 1. 复制对象到新路径
+ var copyRequest = new Amazon.S3.Model.CopyObjectRequest
+ {
+ SourceBucket = awsConfig.BucketName,
+ SourceKey = sourcePath,
+ DestinationBucket = awsConfig.BucketName,
+ DestinationKey = destPath
+ };
+
+ try
+ {
+ // 执行复制操作
+ await amazonS3Client.CopyObjectAsync(copyRequest);
+
+ // 2. 删除原对象
+ var deleteRequest = new Amazon.S3.Model.DeleteObjectRequest
+ {
+ BucketName = awsConfig.BucketName,
+ Key = sourcePath
+ };
+ await amazonS3Client.DeleteObjectAsync(deleteRequest);
+
+
+ }
+ catch (AmazonS3Exception ex)
+ {
+ Console.WriteLine($"ERROR: {ex.Message}");
+ // 可根据异常类型细化处理(如文件不存在、权限问题等)
+ }
+ #endregion
+ break;
+ default:
+ throw new BusinessValidationFailedException("ERROR");
+
+ }
+
+ }
+
+ ///
+ /// 获取所有根目录名称
+ ///
+ ///
+ public List GetRootFolderNames()
+ {
+ GetObjectStoreTempToken();
+ var aliConfig = ObjectStoreServiceOptions.AliyunOSS;
+ var _ossClient = new OssClient(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? aliConfig.EndPoint : aliConfig.InternalEndpoint,
+ AliyunOSSTempToken.AccessKeyId,
+ AliyunOSSTempToken.AccessKeySecret,
+ AliyunOSSTempToken.SecurityToken);
+
+ List rootFolders = new List();
+ string nextMarker = null;
+
+ try
+ {
+ ObjectListing objectListing = null;
+ do
+ {
+ // 列出根目录下的对象和文件夹
+ objectListing = _ossClient.ListObjects(new Aliyun.OSS.ListObjectsRequest(aliConfig.BucketName)
+ {
+
+ MaxKeys = 1000,
+ Marker = nextMarker,
+ Delimiter = "/" // 使用分隔符来模拟文件夹
+ });
+
+ // 遍历 CommonPrefixes 获取根文件夹名称
+ foreach (var prefix in objectListing.CommonPrefixes)
+ {
+ rootFolders.Add(prefix.TrimEnd('/')); // 去掉末尾的斜杠
+ }
+
+ // 设置 NextMarker 以获取下一页的数据
+ nextMarker = objectListing.NextMarker;
+
+ } while (objectListing.IsTruncated);
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error: {ex.Message}");
+ }
+
+ return rootFolders;
+ }
+
+ ///
+ /// 删除某个目录的文件 (包含单个文件,oss单个文件需要去除前缀/)
+ ///
+ ///
+ ///
+ public async Task DeleteFromPrefix(string prefix, bool isCache = false)
+ {
+
+ //打开了同步的,删除的时候,一起删除
+ if (ObjectStoreServiceOptions.IsOpenStoreSync && ObjectStoreServiceOptions.SyncConfigList.Any(t => t.IsOpenSync))
+ {
+ foreach (var config in ObjectStoreServiceOptions.SyncConfigList.Where(t => t.IsOpenSync))
+ {
+ ObjectStoreServiceOptions.ObjectStoreUse = config.Primary;
+
+ GetObjectStoreTempToken();
+
+ await DeleteFromPrefixInternal(prefix, isCache);
+ }
+ }
+ else
+ {
+ GetObjectStoreTempToken();
+
+ await DeleteFromPrefixInternal(prefix, isCache);
+ }
+
+ }
+
+
+ private async Task DeleteFromPrefixInternal(string prefix, bool isCache = false)
+ {
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);
+ 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;
+ }
+
+
try
{
@@ -549,7 +1554,7 @@ public class OSSService : IOSSService
do
{
// 使用 prefix 模拟目录结构,设置 MaxKeys 和 NextMarker
- objectListing = _ossClient.ListObjects(new Aliyun.OSS.ListObjectsRequest(aliConfig.BucketName)
+ objectListing = _ossClient.ListObjects(new Aliyun.OSS.ListObjectsRequest(bucketName)
{
Prefix = prefix,
MaxKeys = 1000,
@@ -561,7 +1566,7 @@ public class OSSService : IOSSService
// 删除获取到的文件
if (keys.Count > 0)
{
- _ossClient.DeleteObjects(new Aliyun.OSS.DeleteObjectsRequest(aliConfig.BucketName, keys, false));
+ _ossClient.DeleteObjects(new Aliyun.OSS.DeleteObjectsRequest(bucketName, keys, false));
}
// 设置 NextMarker 以获取下一页的数据
@@ -618,15 +1623,14 @@ public class OSSService : IOSSService
var awsConfig = ObjectStoreServiceOptions.AWS;
-
// 提供awsAccessKeyId和awsSecretAccessKey构造凭证
var credentials = new SessionAWSCredentials(AWSTempToken.AccessKeyId, AWSTempToken.SecretAccessKey, AWSTempToken.SessionToken);
//提供awsEndPoint(域名)进行访问配置
var clientConfig = new AmazonS3Config
{
- RegionEndpoint = RegionEndpoint.USEast1,
- UseHttp = true,
+ RegionEndpoint = RegionEndpoint.GetBySystemName(awsConfig.Region)
+ //,UseHttp = true,
};
var amazonS3Client = new AmazonS3Client(credentials, clientConfig);
@@ -640,7 +1644,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
@@ -671,13 +1675,229 @@ public class OSSService : IOSSService
}
- public ObjectStoreDTO GetObjectStoreTempToken()
+ public async Task DeleteObjects(List objectKeys, bool isCache = false)
+ {
+ //打开了同步的,删除的时候,一起删除
+ if (ObjectStoreServiceOptions.IsOpenStoreSync && ObjectStoreServiceOptions.SyncConfigList.Any(t => t.IsOpenSync))
+ {
+ foreach (var config in ObjectStoreServiceOptions.SyncConfigList.Where(t => t.IsOpenSync))
+ {
+ ObjectStoreServiceOptions.ObjectStoreUse = config.Primary;
+
+ GetObjectStoreTempToken();
+
+ await DeleteObjectsInternal(objectKeys, isCache);
+ }
+ }
+ else
+ {
+ GetObjectStoreTempToken();
+
+ await DeleteObjectsInternal(objectKeys, isCache);
+ }
+ }
+ public async Task DeleteObjectsInternal(List objectKeys, bool isCache = false)
{
- var ossOptions = ObjectStoreServiceOptions.AliyunOSS;
+
+ GetObjectStoreTempToken();
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);
+
+ 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(bucketName, objectKeys, false));
+
+ }
+ }
+ else if (ObjectStoreServiceOptions.ObjectStoreUse == "MinIO")
+ {
+ var minIOConfig = ObjectStoreServiceOptions.MinIO;
+
+
+ var minioClient = new MinioClient().WithEndpoint($"{minIOConfig.EndPoint}:{minIOConfig.Port}")
+ .WithCredentials(minIOConfig.AccessKeyId, minIOConfig.SecretAccessKey).WithSSL(minIOConfig.UseSSL)
+ .Build();
+
+
+ if (objectKeys.Count > 0)
+ {
+ var objArgs = new RemoveObjectsArgs()
+ .WithBucket(minIOConfig.BucketName)
+ .WithObjects(objectKeys);
+
+ // 删除对象
+ await minioClient.RemoveObjectsAsync(objArgs);
+ }
+ }
+ else if (ObjectStoreServiceOptions.ObjectStoreUse == "AWS")
+ {
+
+ var awsConfig = ObjectStoreServiceOptions.AWS;
+
+
+ // 提供awsAccessKeyId和awsSecretAccessKey构造凭证
+ 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);
+
+ if (objectKeys.Count > 0)
+ {
+ // 准备删除请求
+ var deleteObjectsRequest = new Amazon.S3.Model.DeleteObjectsRequest
+ {
+ BucketName = awsConfig.BucketName,
+ Objects = objectKeys.Select(t => new KeyVersion() { Key = t }).ToList()
+ };
+
+
+ // 批量删除对象
+ var deleteObjectsResponse = await amazonS3Client.DeleteObjectsAsync(deleteObjectsRequest);
+ }
+
+ }
+ else
+ {
+ throw new BusinessValidationFailedException("未定义的存储介质类型");
+ }
+ }
+
+ public async Task GetObjectSizeAsync(string sourcePath)
+ {
+ BackBatchGetToken();
+
+
+ var objectkey = sourcePath.Trim('/');
+
+ 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);
+
+ var key = HttpUtility.UrlDecode(objectkey);
+ var metadata = _ossClient.GetObjectMetadata(aliConfig.BucketName, key);
+
+ long fileSize = metadata?.ContentLength ?? 0; // 文件大小(字节)
+
+ return fileSize;
+ }
+ else if (ObjectStoreServiceOptions.ObjectStoreUse == "MinIO")
+ {
+ var minIOConfig = ObjectStoreServiceOptions.MinIO;
+
+
+ var minioClient = new MinioClient().WithEndpoint($"{minIOConfig.EndPoint}:{minIOConfig.Port}")
+ .WithCredentials(minIOConfig.AccessKeyId, minIOConfig.SecretAccessKey).WithSSL(minIOConfig.UseSSL)
+ .Build();
+
+
+ var stat = await minioClient.StatObjectAsync(new Minio.DataModel.Args.StatObjectArgs()
+ .WithBucket(minIOConfig.BucketName)
+ .WithObject(objectkey));
+
+ return stat.Size; // 文件大小(字节)
+ }
+ else if (ObjectStoreServiceOptions.ObjectStoreUse == "AWS")
+ {
+
+ var awsConfig = ObjectStoreServiceOptions.AWS;
+
+
+ // 提供awsAccessKeyId和awsSecretAccessKey构造凭证
+ var credentials = new SessionAWSCredentials(AWSTempToken.AccessKeyId, AWSTempToken.SecretAccessKey, AWSTempToken.SessionToken);
+
+ //提供awsEndPoint(域名)进行访问配置
+ var clientConfig = new AmazonS3Config
+ {
+ RegionEndpoint = RegionEndpoint.GetBySystemName(awsConfig.Region)
+ //,UseHttp = true,
+ };
+
+ var request = new Amazon.S3.Model.GetObjectMetadataRequest
+ {
+ BucketName = awsConfig.BucketName,
+ Key = objectkey
+ };
+
+ var amazonS3Client = new AmazonS3Client(credentials, clientConfig);
+
+ var response = await amazonS3Client.GetObjectMetadataAsync(request);
+
+ long fileSize = response.ContentLength; // 文件大小(字节)
+
+ return fileSize;
+
+ }
+ else
+ {
+ throw new BusinessValidationFailedException("未定义的存储介质类型");
+ }
+ }
+
+
+
+ public ObjectStoreDTO GetObjectStoreTempToken(string? domain = null, bool? isGetAllTempToken = null, string? objectUse = null)
+ {
+ string objectStoreUse = string.Empty;
+ //使用指定配置
+ if (objectUse != null)
+ {
+ objectStoreUse = objectUse?.Trim() ?? string.Empty;
+ }
+ //根据域名动态判断
+ else
+ {
+ //如果传递了域名,并且打开了存储同步,根据域名使用的具体存储覆盖之前的配置,否则就用固定的配置
+ if (ObjectStoreServiceOptions.IsOpenStoreSync && domain.IsNotNullOrEmpty() && ObjectStoreServiceOptions.SyncConfigList.Any(t => t.Domain == domain))
+ {
+
+ var find = ObjectStoreServiceOptions.SyncConfigList.FirstOrDefault(t => t.Domain == domain);
+ if (find != null)
+ {
+ objectStoreUse = find.Primary;
+ }
+
+ }
+ else
+ {
+ //兜底,如果是本地测试环境,那就使用部署默认配置
+ objectStoreUse = ObjectStoreServiceOptions.ObjectStoreUse;
+ }
+ }
+
+
+ var objectStoreDTO = new ObjectStoreDTO() { ObjectStoreUse = objectStoreUse, IsOpenStoreSync = ObjectStoreServiceOptions.IsOpenStoreSync, SyncConfigList = ObjectStoreServiceOptions.SyncConfigList };
+
+ if (objectStoreUse == "AliyunOSS" || isGetAllTempToken == true)
+ {
+ var ossOptions = ObjectStoreServiceOptions.AliyunOSS;
+
var client = new Client(new AlibabaCloud.OpenApiClient.Models.Config()
{
AccessKeyId = ossOptions.AccessKeyId,
@@ -713,24 +1933,33 @@ public class OSSService : IOSSService
BucketName = ossOptions.BucketName,
EndPoint = ossOptions.EndPoint,
ViewEndpoint = ossOptions.ViewEndpoint,
+ PreviewEndpoint = ossOptions.PreviewEndpoint
};
AliyunOSSTempToken = tempToken;
- return new ObjectStoreDTO() { ObjectStoreUse = ObjectStoreServiceOptions.ObjectStoreUse, AliyunOSS = tempToken };
+ objectStoreDTO.AliyunOSS = tempToken;
+
}
- else if (ObjectStoreServiceOptions.ObjectStoreUse == "MinIO")
+ if (objectStoreUse == "MinIO")
{
- return new ObjectStoreDTO() { ObjectStoreUse = ObjectStoreServiceOptions.ObjectStoreUse, MinIO = ObjectStoreServiceOptions.MinIO };
+ objectStoreDTO.MinIO = ObjectStoreServiceOptions.MinIO;
}
- else if (ObjectStoreServiceOptions.ObjectStoreUse == "AWS")
+ if (objectStoreUse == "AWS" || isGetAllTempToken == true)
{
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
@@ -759,12 +1988,110 @@ public class OSSService : IOSSService
};
AWSTempToken = tempToken;
- return new ObjectStoreDTO() { ObjectStoreUse = ObjectStoreServiceOptions.ObjectStoreUse, AWS = tempToken };
+
+ objectStoreDTO.AWS = tempToken;
}
- else
+
+ if (objectStoreUse.IsNullOrEmpty())
{
throw new BusinessValidationFailedException("未定义的存储介质类型");
}
+
+ return objectStoreDTO;
+ }
+
+
+ public async Task SyncFileAsync(string objectKey, ObjectStoreUse source, ObjectStoreUse destination, CancellationToken ct = default)
+ {
+ var tempConfig = GetObjectStoreTempToken(isGetAllTempToken: true);
+
+
+ var aliConfig = ObjectStoreServiceOptions.AliyunOSS;
+
+ var _ossClient = new OssClient(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? aliConfig.EndPoint : aliConfig.InternalEndpoint, AliyunOSSTempToken.AccessKeyId, AliyunOSSTempToken.AccessKeySecret, AliyunOSSTempToken.SecurityToken);
+
+ var awsConfig = ObjectStoreServiceOptions.AWS;
+
+ var credentials = new SessionAWSCredentials(AWSTempToken.AccessKeyId, AWSTempToken.SecretAccessKey, AWSTempToken.SessionToken);
+
+ //提供awsEndPoint(域名)进行访问配置
+ var clientConfig = new AmazonS3Config
+ {
+ RegionEndpoint = RegionEndpoint.GetBySystemName(awsConfig.Region)
+ };
+
+ var amazonS3Client = new AmazonS3Client(credentials, clientConfig);
+
+ // ⭐ 关键变量
+ IDisposable? owner = null;
+ Stream sourceStream;
+ long contentLength;
+
+ // ========= 获取流 + 长度 =========
+ switch (source)
+ {
+ case ObjectStoreUse.AliyunOSS:
+ {
+
+ var obj = _ossClient.GetObject(
+ aliConfig.BucketName,
+ objectKey);
+
+ owner = obj;
+ sourceStream = obj.Content;
+ contentLength = obj.ContentLength;
+ break;
+ }
+
+ case ObjectStoreUse.AWS:
+ {
+ var response = await amazonS3Client.GetObjectAsync(
+ awsConfig.BucketName,
+ objectKey,
+ ct);
+
+ owner = response;
+ sourceStream = response.ResponseStream;
+ contentLength = response.Headers.ContentLength;
+ break;
+ }
+
+ default:
+ throw new BusinessValidationFailedException("未定义的同步类型");
+ }
+
+ try
+ {
+ // ========= 上传 =========
+ if (destination == ObjectStoreUse.AWS)
+ {
+ var putRequest = new Amazon.S3.Model.PutObjectRequest
+ {
+ BucketName = awsConfig.BucketName,
+ Key = objectKey,
+ InputStream = sourceStream,
+ Headers = { ContentLength = contentLength }
+ };
+
+ await amazonS3Client.PutObjectAsync(putRequest, ct);
+ }
+ else if (destination == ObjectStoreUse.AliyunOSS)
+ {
+ _ossClient.PutObject(
+ aliConfig.BucketName,
+ objectKey,
+ sourceStream);
+ }
+ else
+ {
+ throw new BusinessValidationFailedException("未定义的同步类型");
+ }
+ }
+ finally
+ {
+ // ⭐⭐⭐ 真正释放 HTTP 连接
+ owner?.Dispose();
+ }
}
}
diff --git a/IRC.Core.SCP/Service/_MapConfig.cs b/IRC.Core.SCP/Service/_MapConfig.cs
new file mode 100644
index 000000000..d61e18c25
--- /dev/null
+++ b/IRC.Core.SCP/Service/_MapConfig.cs
@@ -0,0 +1,16 @@
+using AutoMapper;
+using IRaCIS.Core.Domain.Models;
+using IRaCIS.Core.SCP.Service;
+
+
+namespace IRaCIS.Core.Application.Service
+{
+ public class CommonConfig : Profile
+ {
+ public CommonConfig()
+ {
+ CreateMap().ReverseMap();
+ }
+ }
+
+}
diff --git a/IRC.Core.SCP/appsettings.Prod_Event_IRC_SCP.json b/IRC.Core.SCP/appsettings.Prod_Event_IRC_SCP.json
new file mode 100644
index 000000000..c87c34ac6
--- /dev/null
+++ b/IRC.Core.SCP/appsettings.Prod_Event_IRC_SCP.json
@@ -0,0 +1,37 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ },
+ "ObjectStoreService": {
+ "ObjectStoreUse": "AliyunOSS",
+ "AliyunOSS": {
+ "RegionId": "cn-shanghai",
+ "InternalEndpoint": "https://oss-cn-shanghai-internal.aliyuncs.com",
+ "EndPoint": "https://oss-cn-shanghai.aliyuncs.com",
+ "AccessKeyId": "LTAI5tFUCCmz5TwghZHsj45Y",
+ "AccessKeySecret": "8evrBy1fVfzJG25i67Jm0xqn9Xcw2T",
+ "RoleArn": "acs:ram::1078130221702011:role/uat-oss-access",
+ "BucketName": "tl-med-irc-event-store",
+ "ViewEndpoint": "https://tl-med-irc-event-store.oss-cn-shanghai.aliyuncs.com",
+ "Region": "oss-cn-shanghai",
+ "DurationSeconds": 7200
+ }
+ },
+
+ "ConnectionStrings": {
+ "RemoteNew": "Server=101.132.253.119,1435;Database=irc_Prpd_bak;User ID=sa;Password=xc@123456;TrustServerCertificate=true",
+ "Hangfire": "Server=101.132.253.119,1435;Database=irc_Hangfire_bak;User ID=sa;Password=xc@123456;TrustServerCertificate=true"
+ },
+
+ "DicomSCPServiceConfig": {
+ "CalledAEList": [
+ "STORESCP"
+ ],
+ "ServerPort": 11112
+ }
+
+}
diff --git a/IRC.Core.SCP/appsettings.Test_IRC_SCP.json b/IRC.Core.SCP/appsettings.Test_IRC_SCP.json
index 022a47cd3..289de42a4 100644
--- a/IRC.Core.SCP/appsettings.Test_IRC_SCP.json
+++ b/IRC.Core.SCP/appsettings.Test_IRC_SCP.json
@@ -7,7 +7,28 @@
}
},
"ObjectStoreService": {
+ // 使用的对象存储服务类型
"ObjectStoreUse": "AliyunOSS",
+ "IsOpenStoreSync": true,
+ "ApiDeployRegion": "CN",
+ "SyncConfigList": [
+ {
+ "Domain": "irc.test.extimaging.com",
+ "Primary": "AliyunOSS",
+ "Target": "AWS",
+ "UploadRegion": "CN",
+ "TargetRegion": "US",
+ "IsOpenSync": true
+ },
+ {
+ "Domain": "lili.test.extimaging.com",
+ "Primary": "AWS",
+ "Target": "AliyunOSS",
+ "UploadRegion": "US",
+ "TargetRegion": "CN",
+ "IsOpenSync": true
+ }
+ ],
"AliyunOSS": {
"RegionId": "cn-shanghai",
"InternalEndpoint": "https://oss-cn-shanghai-internal.aliyuncs.com",
@@ -29,6 +50,27 @@
"secretKey": "TzgvyA3zGXMUnpilJNUlyMYHfosl1hBMl6lxPmjy",
"bucketName": "hir-test",
"viewEndpoint": "http://106.14.89.110:9001/hir-test/"
+ },
+ // AWS S3 对象存储服务的配置
+ "AWS": {
+ // AWS S3 的Region
+ "Region": "us-east-1",
+ // AWS S3 的内部访问端点
+ "EndPoint": "s3.us-east-1.amazonaws.com",
+ // 是否使用 SSL
+ "UseSSL": true,
+ // AWS S3 的角色 ARN
+ "RoleArn": "arn:aws:iam::471112624751:role/uat_s3_access",
+ // AWS S3 的访问密钥 ID
+ "AccessKeyId": "AKIAW3MEAFJX7IPXISP4",
+ // AWS S3 的访问密钥 Secret
+ "SecretAccessKey": "Pgrg3le5jPxZQ7MR1yYNS30J0XRyJeKVyIIjElXc",
+ // AWS S3 的Bucket名称
+ "BucketName": "ei-med-s3-lili-uat-store",
+ // AWS S3 的访问端点
+ "ViewEndpoint": "https://ei-med-s3-lili-uat-store.s3.amazonaws.com",
+ // AWS S3 的持续数秒
+ "DurationSeconds": 7200
}
},
diff --git a/IRaCIS.Core.API/Controllers/ExtraController.cs b/IRaCIS.Core.API/Controllers/ExtraController.cs
index 66ef3bc6d..d6fabd6c8 100644
--- a/IRaCIS.Core.API/Controllers/ExtraController.cs
+++ b/IRaCIS.Core.API/Controllers/ExtraController.cs
@@ -25,6 +25,7 @@ using Microsoft.Extensions.Options;
using Org.BouncyCastle.Tls;
using RestSharp;
using RestSharp.Authenticators;
+using Serilog;
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -137,9 +138,14 @@ namespace IRaCIS.Api.Controllers
public async Task GetObjectStoreTokenAsync([FromServices] IOptionsMonitor options, [FromServices] IOSSService _oSSService)
{
- var result = _oSSService.GetObjectStoreTempToken();
+ var domain = HttpContext.Request.Host.Host;
- //result.AWS = await GetAWSTemToken(options.CurrentValue);
+
+ var result = _oSSService.GetObjectStoreTempToken(domain);
+
+
+
+ Log.Logger.Information($"使用域名:{domain}请求token.返回{result.ToJsonStr()}");
return ResponseOutput.Ok(result);
diff --git a/IRaCIS.Core.API/Controllers/InspectionController.cs b/IRaCIS.Core.API/Controllers/InspectionController.cs
index 771c08514..88534140b 100644
--- a/IRaCIS.Core.API/Controllers/InspectionController.cs
+++ b/IRaCIS.Core.API/Controllers/InspectionController.cs
@@ -26,6 +26,7 @@ namespace IRaCIS.Core.API.Controllers
ITrialDocumentService _trialDocumentService,
IReadingImageTaskService _iReadingImageTaskService,
ITrialConfigService _trialConfigService,
+ IStudyService _studyService,
IClinicalAnswerService _clinicalAnswerService,
IReadingClinicalDataService _readingClinicalDataService,
IQCOperationService _qCOperationService,
@@ -146,6 +147,26 @@ namespace IRaCIS.Core.API.Controllers
}
+
+
+ ///
+ /// 修正患者基本信息
+ ///
+ ///
+ ///
+ [HttpPost, Route("Inspection/Study/AmendmentPatientInfo")]
+ [TrialGlobalLimit("AfterStopCannNotOpt")]
+
+ [UnitOfWork]
+ public async Task AmendmentPatientInfo(DataInspectionDto opt)
+ {
+
+ var singid = await _inspectionService.RecordSing(opt.SignInfo);
+ var result = await _studyService.AmendmentPatientInfo(opt.Data);
+ await _inspectionService.CompletedSign(singid, result);
+ return result;
+ }
+
///
/// 医学审核完成
///
@@ -155,10 +176,19 @@ namespace IRaCIS.Core.API.Controllers
[TrialGlobalLimit("AfterStopCannNotOpt")]
[UnitOfWork]
- public async Task FinishMedicalReview(DataInspectionDto opt)
+ public async Task FinishMedicalReview(DataInspectionDto opt)
{
var singid = await _inspectionService.RecordSing(opt.SignInfo);
- var result = await _readingMedicalReviewService.FinishMedicalReview(opt.Data);
+
+ await _readingMedicalReviewService.ClosedMedicalReviewDialog(new ClosedMedicalReviewDialogInDto() {
+ DialogCloseReason=opt.Data.DialogCloseReason,
+ IsClosedDialog=opt.Data.IsClosedDialog,
+ MedicalDialogCloseEnum=opt.Data.MedicalDialogCloseEnum,
+ TaskMedicalReviewId=opt.Data.TaskMedicalReviewId
+ });
+ var result = await _readingMedicalReviewService.FinishMedicalReview(new FinishMedicalReviewInDto() {
+ TaskMedicalReviewId=opt.Data.TaskMedicalReviewId
+ });
await _inspectionService.CompletedSign(singid, result);
return result;
}
diff --git a/IRaCIS.Core.API/Controllers/UploadDownLoadController.cs b/IRaCIS.Core.API/Controllers/UploadDownLoadController.cs
index 8327b9e93..fe6f7ec12 100644
--- a/IRaCIS.Core.API/Controllers/UploadDownLoadController.cs
+++ b/IRaCIS.Core.API/Controllers/UploadDownLoadController.cs
@@ -9,6 +9,7 @@ using IRaCIS.Core.Application.Filter;
using IRaCIS.Core.Application.Helper;
using IRaCIS.Core.Application.MassTransit.Command;
using IRaCIS.Core.Application.Service;
+using IRaCIS.Core.Application.ViewModel;
using IRaCIS.Core.Domain.Models;
using IRaCIS.Core.Domain.Share;
using IRaCIS.Core.Infra.EFCore;
@@ -598,7 +599,7 @@ namespace IRaCIS.Core.API.Controllers
templateFileStream.Seek(0, SeekOrigin.Begin);
- var ossRelativePath = await oSSService.UploadToOSSAsync(fileStream, $"{trialId.ToString()}/InspectionUpload/DataReconciliation", realFileName);
+ var ossRelativePath = await oSSService.UploadToOSSAsync(fileStream, $"{trialId.ToString()}/InspectionUpload/DataReconciliation", realFileName, uploadInfo: new FileUploadRecordAddOrEdit() { TrialId = trialId, BatchDataType = BatchDataType.DataReconciliation });
var addEntity = await _inspectionFileRepository.AddAsync(new InspectionFile() { FileName = realFileName, RelativePath = ossRelativePath, TrialId = trialId }, true);
@@ -856,7 +857,7 @@ namespace IRaCIS.Core.API.Controllers
throw new BusinessValidationFailedException(_localizer["UploadDownLoad_TemplateUploadData"]);
}
- var ossRelativePath = await oSSService.UploadToOSSAsync(fileStream, $"{trialId.ToString()}/InspectionUpload/SiteSurvey", realFileName);
+ var ossRelativePath = await oSSService.UploadToOSSAsync(fileStream, $"{trialId.ToString()}/InspectionUpload/SiteSurvey", realFileName, uploadInfo: new FileUploadRecordAddOrEdit() { TrialId = trialId, BatchDataType = BatchDataType.SiteUserSurvey });
await _inspectionFileRepository.AddAsync(new InspectionFile() { FileName = realFileName, RelativePath = ossRelativePath, TrialId = trialId }, true);
@@ -932,7 +933,7 @@ namespace IRaCIS.Core.API.Controllers
}
//处理好 用户类型 和用户类型枚举
var sysUserTypeList = _usertypeRepository.Where(t => t.UserTypeEnum == UserTypeEnum.CRA || t.UserTypeEnum == UserTypeEnum.ClinicalResearchCoordinator).Select(t => new { UserTypeId = t.Id, t.UserTypeEnum }).ToList();
- var siteList = _trialSiteRepository.Where(t => t.TrialId == trialId && siteCodeList.Contains(t.TrialSiteCode)).Select(t => new { t.TrialSiteCode, TrialSiteId = t.Id }).ToList();
+ var siteList = _trialSiteRepository.Where(t => t.TrialId == trialId && siteCodeList.Contains(t.TrialSiteCode)).Select(t => new { t.TrialSiteCode, TrialSiteId = t.Id, t.Country }).ToList();
foreach (var item in excelList)
{
@@ -954,6 +955,8 @@ namespace IRaCIS.Core.API.Controllers
}
item.TrialSiteId = siteList.FirstOrDefault(t => t.TrialSiteCode.Trim().ToUpper() == item.TrialSiteCode.Trim().ToUpper()).TrialSiteId;
+
+ item.Country = siteList.FirstOrDefault(t => t.TrialSiteCode.Trim().ToUpper() == item.TrialSiteCode.Trim().ToUpper()).Country;
}
var list = excelList.Where(t => t.UserTypeEnum == UserTypeEnum.ClinicalResearchCoordinator || t.UserTypeEnum == UserTypeEnum.CRA).ToList();
@@ -1008,7 +1011,7 @@ namespace IRaCIS.Core.API.Controllers
Other = 6,
-
+
}
///
diff --git a/IRaCIS.Core.API/HostService/SyncFileRecoveryService.cs b/IRaCIS.Core.API/HostService/SyncFileRecoveryService.cs
new file mode 100644
index 000000000..33e6521bd
--- /dev/null
+++ b/IRaCIS.Core.API/HostService/SyncFileRecoveryService.cs
@@ -0,0 +1,168 @@
+using IRaCIS.Core.Application.Helper;
+using IRaCIS.Core.Application.Service;
+using IRaCIS.Core.Domain.Models;
+using IRaCIS.Core.Infra.EFCore;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using System;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace IRaCIS.Core.API.HostService;
+
+
+public class SyncFileRecoveryService(IServiceScopeFactory _scopeFactory, FileSyncQueue _fileSyncQueue) : BackgroundService
+{
+
+ private readonly int _pageSize = 500;
+
+ ///
+ /// 多个程序,如果恢复同一份数据,造成重复同步,SCP服务不恢复任务
+ ///
+ ///
+ ///
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ //在本地调试的时候,不干涉部署同步任务
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ return;
+ }
+
+ using var scope = _scopeFactory.CreateScope();
+ var fileUploadRecordRepository = scope.ServiceProvider.GetRequiredService>();
+
+ // 延迟启动,保证主机快速启动
+ await Task.Delay(5000, stoppingToken);
+
+ int page = 0;
+
+
+ while (!stoppingToken.IsCancellationRequested)
+ {
+ // 分页获取未入队任务
+ var pending = await fileUploadRecordRepository
+ .Where(x => x.IsNeedSync == true && (x.IsSync == false || x.IsSync == null))
+ .OrderByDescending(x => x.Priority)
+ .Select(t => new { t.Id, t.Priority })
+ .Skip(page * _pageSize)
+ .Take(_pageSize)
+ .ToListAsync(stoppingToken);
+
+ if (!pending.Any())
+ break; // 扫描完毕,退出循环
+
+ foreach (var file in pending)
+ {
+ //file.IsQueued = true; // 避免重复入队
+ _fileSyncQueue.Enqueue(file.Id, file.Priority ?? 0); // 放入队列
+ }
+
+ page++; // 下一页
+ await Task.Delay(200, stoppingToken); // 缓解数据库压力
+ }
+ }
+}
+
+public class FileSyncWorker(IServiceScopeFactory _scopeFactory, ILogger _logger, FileSyncQueue _fileSyncQueue) : BackgroundService
+{
+
+ // ⭐ 自动根据服务器CPU
+ private readonly int _workerCount = Math.Max(1, Environment.ProcessorCount - 1);
+
+
+
+ protected override Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ for (int i = 0; i < _workerCount; i++)
+ Task.Run(() => WorkerLoop(stoppingToken));
+
+ return Task.CompletedTask;
+ }
+
+ private async Task WorkerLoop(CancellationToken stoppingToken)
+ {
+ while (!stoppingToken.IsCancellationRequested)
+ {
+ var id = await _fileSyncQueue.DequeueAsync(stoppingToken);
+
+ try
+ {
+ using var scope = _scopeFactory.CreateScope();
+ var _fileUploadRecordRepository = scope.ServiceProvider.GetRequiredService>();
+ var _uploadFileSyncRecordRepository = scope.ServiceProvider.GetRequiredService>();
+
+ var syncConfig = (scope.ServiceProvider.GetRequiredService>()).CurrentValue;
+
+ var oss = scope.ServiceProvider.GetRequiredService();
+
+ var file = await _fileUploadRecordRepository.FirstOrDefaultAsync(t => t.Id == id);
+
+ // ✅ 不要 return
+ if (file == null || file.IsNeedSync != true || syncConfig.IsOpenStoreSync == false)
+ {
+ continue;
+ }
+
+ if (syncConfig.SyncConfigList.Any(t => t.UploadRegion == file.UploadRegion && t.IsOpenSync == false))
+ {
+ continue;
+ }
+
+ //如果发现系统配置某一边同步进行了关闭,那么就直接返回,不执行任务
+ if (syncConfig.SyncConfigList.Any(t => t.UploadRegion == file.UploadRegion && t.IsOpenSync == false))
+ {
+ return;
+ }
+
+ var log = new UploadFileSyncRecord
+ {
+ FileUploadRecordId = id,
+ StartTime = DateTime.Now,
+ JobState = jobState.RUNNING
+ };
+
+ await _uploadFileSyncRecordRepository.AddAsync(log);
+ await _uploadFileSyncRecordRepository.SaveChangesAsync(stoppingToken);
+
+ try
+ {
+ await oss.SyncFileAsync(file.Path.TrimStart('/'), file.UploadRegion == "CN" ? ObjectStoreUse.AliyunOSS : ObjectStoreUse.AWS, file.UploadRegion == "CN" ? ObjectStoreUse.AWS : ObjectStoreUse.AliyunOSS);
+
+ file.IsSync = true;
+ file.SyncFinishedTime = DateTime.Now;
+
+ log.JobState = jobState.SUCCESS;
+ }
+ catch (Exception ex)
+ {
+ log.JobState = jobState.FAILED;
+
+ log.Msg = ex.Message.Length > 300 ? ex.Message.Substring(0, 300) : ex.Message;
+ }
+
+ log.EndTime = DateTime.Now;
+
+ await _uploadFileSyncRecordRepository.SaveChangesAsync(stoppingToken);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Sync failed {Id}", id);
+ }
+ finally
+ {
+ // ⭐⭐⭐ 永远执行
+ _fileSyncQueue.Complete(id);
+ }
+ }
+
+
+ }
+}
+
+
diff --git a/IRaCIS.Core.API/IRaCIS.Core.API.xml b/IRaCIS.Core.API/IRaCIS.Core.API.xml
index 62deac025..bdefda92d 100644
--- a/IRaCIS.Core.API/IRaCIS.Core.API.xml
+++ b/IRaCIS.Core.API/IRaCIS.Core.API.xml
@@ -136,7 +136,14 @@
-
+
+
+ 修正患者基本信息
+
+
+
+
+
医学审核完成
@@ -344,6 +351,13 @@
+
+
+ 多个程序,如果恢复同一份数据,造成重复同步,SCP服务不恢复任务
+
+
+
+
IPLimit限流 启动服务
diff --git a/IRaCIS.Core.API/Progranm.cs b/IRaCIS.Core.API/Progranm.cs
index e45bd4215..0ff83fb8f 100644
--- a/IRaCIS.Core.API/Progranm.cs
+++ b/IRaCIS.Core.API/Progranm.cs
@@ -1,4 +1,5 @@
-using IRaCIS.Core.API;
+using FellowOakDicom;
+using IRaCIS.Core.API;
using IRaCIS.Core.API.HostService;
using IRaCIS.Core.Application.BusinessFilter;
using IRaCIS.Core.Application.BusinessFilter.LegacyController.Database.Api;
@@ -14,6 +15,7 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
@@ -88,6 +90,10 @@ builder.Services.ConfigureServices(_configuration);
builder.Services.AddHostedService();
+builder.Services.AddSingleton();
+builder.Services.AddHostedService();
+builder.Services.AddHostedService();
+
//minimal api 异常处理
builder.Services.AddExceptionHandler();
//builder.Services.AddProblemDetails();
@@ -173,6 +179,7 @@ var env = app.Environment;
#region 配置中间件
+DicomSetupBuilder.UseServiceProvider(app.Services);
app.UseMiddleware();
diff --git a/IRaCIS.Core.API/_ServiceExtensions/DicomSetup.cs b/IRaCIS.Core.API/_ServiceExtensions/DicomSetup.cs
index c32dbd4c6..1039fa735 100644
--- a/IRaCIS.Core.API/_ServiceExtensions/DicomSetup.cs
+++ b/IRaCIS.Core.API/_ServiceExtensions/DicomSetup.cs
@@ -8,13 +8,21 @@ namespace IRaCIS.Core.API
{
public static void AddDicomSetup(this IServiceCollection services)
{
+
+ // ⭐ 先做全局 DICOM 配置
new DicomSetupBuilder()
- .RegisterServices(s => s.AddFellowOakDicom()
- .AddTranscoderManager()
- .AddImageManager()
- )
- .SkipValidation()
- .Build();
+ .SkipValidation() // 👈 在这里设置
+ .Build();
+
+ services.AddFellowOakDicom().AddTranscoderManager().AddImageManager();
+
+ // new DicomSetupBuilder()
+ // .RegisterServices(s => s.AddFellowOakDicom()
+ //.AddTranscoderManager()
+ // .AddImageManager()
+ // )
+ // .SkipValidation()
+ // .Build();
}
}
}
diff --git a/IRaCIS.Core.API/_ServiceExtensions/EFSetup.cs b/IRaCIS.Core.API/_ServiceExtensions/EFSetup.cs
index 8dde1dfb0..e9a80c846 100644
--- a/IRaCIS.Core.API/_ServiceExtensions/EFSetup.cs
+++ b/IRaCIS.Core.API/_ServiceExtensions/EFSetup.cs
@@ -40,12 +40,12 @@ namespace IRaCIS.Core.API
var dbType = configuration.GetSection("ConnectionStrings:Db_Type").Value;
if (!string.IsNullOrWhiteSpace(dbType) && dbType == "pgsql")
{
- options.UseNpgsql(configuration.GetSection("ConnectionStrings:RemoteNew").Value, contextOptionsBuilder => contextOptionsBuilder.EnableRetryOnFailure());
+ options.UseNpgsql(configuration.GetSection("ConnectionStrings:RemoteNew").Value, contextOptionsBuilder => contextOptionsBuilder.EnableRetryOnFailure().CommandTimeout(90));
}
else
{
- options.UseSqlServer(configuration.GetSection("ConnectionStrings:RemoteNew").Value, contextOptionsBuilder => contextOptionsBuilder.EnableRetryOnFailure()/*.CommandTimeout(60)*/);
+ options.UseSqlServer(configuration.GetSection("ConnectionStrings:RemoteNew").Value, contextOptionsBuilder => contextOptionsBuilder.EnableRetryOnFailure().CommandTimeout(90));
}
diff --git a/IRaCIS.Core.API/_ServiceExtensions/SerilogSetup.cs b/IRaCIS.Core.API/_ServiceExtensions/SerilogSetup.cs
index 99ec49653..6706cc580 100644
--- a/IRaCIS.Core.API/_ServiceExtensions/SerilogSetup.cs
+++ b/IRaCIS.Core.API/_ServiceExtensions/SerilogSetup.cs
@@ -46,19 +46,19 @@ namespace IRaCIS.Core.API
var emailConfig = new SystemEmailSendConfig();
configuration.GetSection("SystemEmailSendConfig").Bind(emailConfig);
- if (emailConfig.IsOpenErrorNoticeEmail)
- {
- config.WriteTo.Email(options: new Serilog.Sinks.Email.EmailSinkOptions()
- {
- From = emailConfig.FromEmail,
- To = emailConfig.ErrorNoticeEmailList,
- Host = emailConfig.Host,
- Port = emailConfig.Port,
- Subject = new MessageTemplateTextFormatter("Log Alert - 系统发生了异常,请核查"),
- Credentials = new NetworkCredential(emailConfig.FromEmail, emailConfig.AuthorizationCode)
+ //if (emailConfig.IsOpenErrorNoticeEmail)
+ //{
+ // config.WriteTo.Email(options: new Serilog.Sinks.Email.EmailSinkOptions()
+ // {
+ // From = emailConfig.FromEmail,
+ // To = emailConfig.ErrorNoticeEmailList,
+ // Host = emailConfig.Host,
+ // Port = emailConfig.Port,
+ // Subject = new MessageTemplateTextFormatter("Log Alert - 系统发生了异常,请核查"),
+ // Credentials = new NetworkCredential(emailConfig.FromEmail, emailConfig.AuthorizationCode)
- }, restrictedToMinimumLevel: Serilog.Events.LogEventLevel.Error);
- }
+ // }, restrictedToMinimumLevel: Serilog.Events.LogEventLevel.Error);
+ //}
#endregion
Log.Logger = config.CreateLogger();
diff --git a/IRaCIS.Core.API/_ServiceExtensions/SwaggerSetup.cs b/IRaCIS.Core.API/_ServiceExtensions/SwaggerSetup.cs
index 153bfe38d..ebba8c3ca 100644
--- a/IRaCIS.Core.API/_ServiceExtensions/SwaggerSetup.cs
+++ b/IRaCIS.Core.API/_ServiceExtensions/SwaggerSetup.cs
@@ -1,4 +1,4 @@
-using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
@@ -116,6 +116,9 @@ public static class SwaggerSetup
//DocExpansion设置为none可折叠所有方法
options.DocExpansion(DocExpansion.None);
+ // 开启Swagger UI的搜索/过滤功能
+ options.EnableFilter();
+
//DefaultModelsExpandDepth设置为 - 1 可不显示models
options.DefaultModelsExpandDepth(-1);
});
diff --git a/IRaCIS.Core.API/appsettings.Event_IRC.json b/IRaCIS.Core.API/appsettings.Prod_Event_IRC.json
similarity index 53%
rename from IRaCIS.Core.API/appsettings.Event_IRC.json
rename to IRaCIS.Core.API/appsettings.Prod_Event_IRC.json
index 72220f23f..cf8f44b59 100644
--- a/IRaCIS.Core.API/appsettings.Event_IRC.json
+++ b/IRaCIS.Core.API/appsettings.Prod_Event_IRC.json
@@ -10,8 +10,16 @@
"RemoteNew": "Server=101.132.253.119,1435;Database=irc_Prpd_bak;User ID=sa;Password=xc@123456;TrustServerCertificate=true",
"Hangfire": "Server=101.132.253.119,1435;Database=irc_Hangfire_bak;User ID=sa;Password=xc@123456;TrustServerCertificate=true"
},
+ "WeComNoticeConfig": {
+ "IsOpenWeComNotice": true,
+ "WebhookUrl": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=4355b98e-1e72-4678-8dfb-2fc6ad0bf449", //4355b98e-1e72-4678-8dfb-2fc6ad0bf449 //cdd97aab-d256-4f07-9145-a0a2b1555322
+ "APINoticeUserList": [ "u", "wait..." ],
+ "VueNoticeUserList": [ "wangxiaoshuang", "6b7717a31647293621b97b96f74e6f3d" ]
+ },
"ObjectStoreService": {
+
"ObjectStoreUse": "AliyunOSS",
+
"AliyunOSS": {
"RegionId": "cn-shanghai",
"InternalEndpoint": "https://oss-cn-shanghai-internal.aliyuncs.com",
@@ -24,13 +32,30 @@
"Region": "oss-cn-shanghai",
"DurationSeconds": 7200
},
+
"MinIO": {
- "endpoint": "http://192.168.3.68",
- "port": "8001",
+ "endPoint": "hir-oss.uat.extimaging.com",
+ "port": "80",
"useSSL": false,
- "accessKey": "IDFkwEpWej0b4DtiuThL",
- "secretKey": "Lhuu83yMhVwu7c1SnjvGY6lq74jzpYqifK6Qtj4h",
- "bucketName": "test"
+ "viewEndpoint": "http://hir-oss.uat.extimaging.com/irc-uat",
+ //"port": "443",
+ //"useSSL": true,
+ //"viewEndpoint": "https://hir-oss.uat.extimaging.com/irc-uat",
+ "accessKey": "b9Ul0e98xPzt6PwRXA1Q",
+ "secretKey": "DzMaU2L4OXl90uytwOmDXF2encN0Jf4Nxu2XkYqQ",
+ "bucketName": "irc-uat"
+
+ },
+ "AWS": {
+ "Region": "us-east-1",
+ "EndPoint": "s3.us-east-1.amazonaws.com",
+ "UseSSL": true,
+ "RoleArn": "arn:aws:iam::471112624751:role/sts_s3_upload",
+ "AccessKeyId": "AKIAW3MEAFJXWRCGSX5Z",
+ "SecretAccessKey": "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",
+ "BucketName": "ei-med-s3-lili-uat-store",
+ "ViewEndpoint": "https://ei-med-s3-lili-uat-store.s3.amazonaws.com/",
+ "DurationSeconds": 7200
}
},
@@ -38,27 +63,26 @@
// 启用质控风险控制功能
"QCRiskControl": true,
"OpenUserComplexPassword": true,
-
"OpenSignDocumentBeforeWork": true,
"OpenLoginLimit": true,
"LoginMaxFailCount": 5,
"LoginFailLockMinutes": 30,
- "AutoLoginOutMinutes": 60,
+ "AutoLoginOutMinutes": 120,
+
+ "OpenLoginMFA": false,
"ContinuousReadingTimeMin": 120,
-
"ReadingRestTimeMin": 10,
"IsNeedChangePassWord": true,
-
"ChangePassWordDays": 90,
-
// 模板类型 1 Elevate 2 Extensive
"TemplateType": 2,
-
+ //MFA免验证发送天数
"UserMFAVerifyMinutes": 1440
+
},
"SystemEmailSendConfig": {
"Port": 465,
@@ -66,14 +90,29 @@
"Imap": "imap.qiye.aliyun.com",
"ImapPort": 993,
"FromEmail": "uat@extimaging.com",
- "FromName": "UAT_IRC",
+ "FromName": "Uat IRC Imaging System",
"AuthorizationCode": "SHzyyl2021",
- "SiteUrl": "http://irc.event.extimaging.com/login",
+ "SiteUrl": "http://irc.uat.extimaging.com/login",
+
+ "PlatformName": "EICS",
+ "PlatformNameCN": "展影云平台",
+ "SystemShortName": "IRC",
+ "OrganizationName": "ExtImaging",
+ "OrganizationNameCN": "ExtImaging",
"CompanyName": "Extensive Imaging",
"CompanyNameCN": "上海展影医疗科技有限公司",
"CompanyShortName": "Extensive Imaging",
"CompanyShortNameCN": "展影医疗",
- "IsEnv_US": false
+ "IsEnv_US": false,
+ "IsOpenErrorNoticeEmail": false,
+ "EmailRegexStr": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
+ "CronEmailDefaultCulture": "zh-CN",
+ "ErrorNoticeEmailList": [ "872297557@qq.com" ]
+ },
+
+ "SystemPacsConfig": {
+ "Port": "11116",
+ "IP": "101.132.253.119"
},
"RequestDuplicationOptions": {
"IsEnabled": true,
@@ -82,5 +121,4 @@
"ExcludedPaths": [
]
}
-
}
diff --git a/IRaCIS.Core.API/appsettings.Prod_IRC.json b/IRaCIS.Core.API/appsettings.Prod_IRC.json
index 2baac3bf2..f41e258ba 100644
--- a/IRaCIS.Core.API/appsettings.Prod_IRC.json
+++ b/IRaCIS.Core.API/appsettings.Prod_IRC.json
@@ -7,8 +7,8 @@
}
},
"ConnectionStrings": {
- "RemoteNew": "Server=101.132.193.237,1434;Database=Prod_IRC;User ID=sa;Password=zhanying@2021;TrustServerCertificate=true",
- "Hangfire": "Server=101.132.193.237,1434;Database=Prod_IRC_Hangfire;User ID=sa;Password=zhanying@2021;TrustServerCertificate=true"
+ "RemoteNew": "Server=10.10.10.49,1434;Database=Prod_IRC;User ID=sa;Password=zhanying@2021;TrustServerCertificate=true",
+ "Hangfire": "Server=10.10.10.49,1434;Database=Prod_IRC_Hangfire;User ID=sa;Password=zhanying@2021;TrustServerCertificate=true"
//"RemoteNew": "Server=prod_mssql_standard,1433;Database=Prod_IRC;User ID=sa;Password=zhanying@2021;TrustServerCertificate=true",
//"Hangfire": "Server=prod_mssql_standard,1433;Database=Prod_IRC_Hangfire;User ID=sa;Password=zhanying@2021;TrustServerCertificate=true"
},
@@ -16,7 +16,7 @@
"IsOpenWeComNotice": true,
"WebhookUrl": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=4355b98e-1e72-4678-8dfb-2fc6ad0bf449", //4355b98e-1e72-4678-8dfb-2fc6ad0bf449 //cdd97aab-d256-4f07-9145-a0a2b1555322
"APINoticeUserList": [ "u", "wait..." ],
- "VueNoticeUserList": [ "wangxiaoshuang", "6b7717a31647293621697b96f74e6f3d" ]
+ "VueNoticeUserList": [ "wangxiaoshuang", "6b7717a31647293621b97b96f74e6f3d" ]
},
"ObjectStoreService": {
"ObjectStoreUse": "AliyunOSS",
@@ -85,10 +85,8 @@
"CompanyShortName": "Extensive Imaging",
"CompanyShortNameCN": "展影医疗",
"IsEnv_US": false,
- "IsOpenErrorNoticeEmail": false,
"EmailRegexStr": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
- "CronEmailDefaultCulture": "zh-CN",
- "ErrorNoticeEmailList": [ "872297557@qq.com" ]
+ "CronEmailDefaultCulture": "zh-CN"
},
"SystemPacsConfig": {
"Port": "11113",
diff --git a/IRaCIS.Core.API/appsettings.Test_IRC.json b/IRaCIS.Core.API/appsettings.Test_IRC.json
index d4cce6913..3e3eea57b 100644
--- a/IRaCIS.Core.API/appsettings.Test_IRC.json
+++ b/IRaCIS.Core.API/appsettings.Test_IRC.json
@@ -22,12 +22,32 @@
"IsOpenWeComNotice": true,
"WebhookUrl": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=4355b98e-1e72-4678-8dfb-2fc6ad0bf449", //4355b98e-1e72-4678-8dfb-2fc6ad0bf449 //cdd97aab-d256-4f07-9145-a0a2b1555322
"APINoticeUserList": [ "u", "wait..." ],
- "VueNoticeUserList": [ "wangxiaoshuang", "6b7717a31647293621697b96f74e6f3d" ]
+ "VueNoticeUserList": [ "wangxiaoshuang", "6b7717a31647293621b97b96f74e6f3d" ]
},
// 对象存储服务配置
"ObjectStoreService": {
// 使用的对象存储服务类型
"ObjectStoreUse": "AliyunOSS",
+ "IsOpenStoreSync": true,
+ "ApiDeployRegion": "CN",
+ "SyncConfigList": [
+ {
+ "Domain": "irc.test.extimaging.com",
+ "Primary": "AliyunOSS",
+ "Target": "AWS",
+ "UploadRegion": "CN",
+ "TargetRegion": "US",
+ "IsOpenSync": true
+ },
+ {
+ "Domain": "lili.test.extimaging.com",
+ "Primary": "AWS",
+ "Target": "AliyunOSS",
+ "UploadRegion": "US",
+ "TargetRegion": "CN",
+ "IsOpenSync": true
+ }
+ ],
// 阿里云对象存储服务的配置
"AliyunOSS": {
// 阿里云 OSS 的 Region ID
@@ -129,14 +149,15 @@
},
// 邮件服务配置(用于系统通知、找回密码、错误报警等)
"SystemEmailSendConfig": {
- // SMTP端口
- "Port": 465,
+
// 企业邮箱SMTP服务器地址
"Host": "smtp.qiye.aliyun.com",
+ // SMTP端口
+ "Port": 465,
"Imap": "imap.qiye.aliyun.com",
-
"ImapPort": 993,
+
// 发件人邮箱地址
"FromEmail": "test@extimaging.com",
// 发件人显示名称
@@ -146,14 +167,16 @@
// 系统对外访问地址
"SiteUrl": "http://irc.test.extimaging.com/login",
- "PlatformName": "EICS",
- "PlatformNameCN": "展影云平台",
- // 系统简称一致性核查使用
+
+ // 系统简称 - 一致性核查使用
"SystemShortName": "IRC",
- // 组织英文名称
+ // 组织英文名称-添加用户默认值
"OrganizationName": "ExtImaging",
// 组织中文名称
"OrganizationNameCN": "ExtImaging",
+
+ "PlatformName": "EICS",
+ "PlatformNameCN": "展影云平台",
// 公司英文全称
"CompanyName": "Extensive Imaging",
// 公司中文全称
@@ -162,15 +185,14 @@
"CompanyShortName": "Extensive Imaging",
// 公司中文简称
"CompanyShortNameCN": "展影医疗",
- // 是否为国际版环境
+
+ // 是否为国际版环境 方便前端区分,用于区分lili 还是irc 做一些事情
"IsEnv_US": false,
- // 是否开启系统异常邮件报警
- "IsOpenErrorNoticeEmail": false,
// 邮箱格式校验正则表达式
"EmailRegexStr": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
- "CronEmailDefaultCulture": "zh-CN",
- // 接收系统异常报警的邮箱列表
- "ErrorNoticeEmailList": [ "872297557@qq.com" ]
+
+ //后端周期性邮件默认语言
+ "CronEmailDefaultCulture": "zh-CN"
},
// PACS 连接配置
"SystemPacsConfig": {
diff --git a/IRaCIS.Core.API/appsettings.US_Prod_IRC.json b/IRaCIS.Core.API/appsettings.US_Prod_IRC.json
index fcc011556..41720417e 100644
--- a/IRaCIS.Core.API/appsettings.US_Prod_IRC.json
+++ b/IRaCIS.Core.API/appsettings.US_Prod_IRC.json
@@ -16,7 +16,7 @@
"IsOpenWeComNotice": true,
"WebhookUrl": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=4355b98e-1e72-4678-8dfb-2fc6ad0bf449", //4355b98e-1e72-4678-8dfb-2fc6ad0bf449 //cdd97aab-d256-4f07-9145-a0a2b1555322
"APINoticeUserList": [ "u", "wait..." ],
- "VueNoticeUserList": [ "wangxiaoshuang", "6b7717a31647293621697b96f74e6f3d" ]
+ "VueNoticeUserList": [ "wangxiaoshuang", "6b7717a31647293621b97b96f74e6f3d" ]
},
"ObjectStoreService": {
"ObjectStoreUse": "AWS",
@@ -90,10 +90,8 @@
"CompanyShortNameCN": "展影医疗",
"SiteUrl": "https://lili.elevateimaging.ai/login",
"IsEnv_US": true,
- "IsOpenErrorNoticeEmail": false,
"EmailRegexStr": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
- "CronEmailDefaultCulture": "en-US",
- "ErrorNoticeEmailList": [ "872297557@qq.com" ]
+ "CronEmailDefaultCulture": "en-US"
},
"SystemPacsConfig": {
diff --git a/IRaCIS.Core.API/appsettings.US_Test_IRC.json b/IRaCIS.Core.API/appsettings.US_Test_IRC.json
index 367320716..b455bdf8c 100644
--- a/IRaCIS.Core.API/appsettings.US_Test_IRC.json
+++ b/IRaCIS.Core.API/appsettings.US_Test_IRC.json
@@ -93,9 +93,7 @@
"CompanyShortName": "Elevate Imaging",
"CompanyShortNameCN": "展影医疗",
"SiteUrl": "https://lili.test.elevateimaging.ai/login",
- "IsEnv_US": true,
- "IsOpenErrorNoticeEmail": false,
- "ErrorNoticeEmailList": [ "872297557@qq.com" ]
+ "IsEnv_US": true
},
"SystemPacsConfig": {
diff --git a/IRaCIS.Core.API/appsettings.US_Uat_IRC.json b/IRaCIS.Core.API/appsettings.US_Uat_IRC.json
index 2300abca3..d9408c07b 100644
--- a/IRaCIS.Core.API/appsettings.US_Uat_IRC.json
+++ b/IRaCIS.Core.API/appsettings.US_Uat_IRC.json
@@ -17,7 +17,7 @@
"IsOpenWeComNotice": true,
"WebhookUrl": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=4355b98e-1e72-4678-8dfb-2fc6ad0bf449", //4355b98e-1e72-4678-8dfb-2fc6ad0bf449 //cdd97aab-d256-4f07-9145-a0a2b1555322
"APINoticeUserList": [ "u", "wait..." ],
- "VueNoticeUserList": [ "wangxiaoshuang", "6b7717a31647293621697b96f74e6f3d" ]
+ "VueNoticeUserList": [ "wangxiaoshuang", "6b7717a31647293621b97b96f74e6f3d" ]
},
"ObjectStoreService": {
@@ -97,10 +97,8 @@
"CompanyShortNameCN": "展影医疗",
"SiteUrl": "https://lili.uat.elevateimaging.ai/login",
"IsEnv_US": true,
- "IsOpenErrorNoticeEmail": false,
"EmailRegexStr": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
- "CronEmailDefaultCulture": "en-US",
- "ErrorNoticeEmailList": [ "872297557@qq.com" ]
+ "CronEmailDefaultCulture": "en-US"
},
"SystemPacsConfig": {
diff --git a/IRaCIS.Core.API/appsettings.Uat_IRC.json b/IRaCIS.Core.API/appsettings.Uat_IRC.json
index 06099ad7a..7d7226ba2 100644
--- a/IRaCIS.Core.API/appsettings.Uat_IRC.json
+++ b/IRaCIS.Core.API/appsettings.Uat_IRC.json
@@ -14,11 +14,31 @@
"IsOpenWeComNotice": true,
"WebhookUrl": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=4355b98e-1e72-4678-8dfb-2fc6ad0bf449", //4355b98e-1e72-4678-8dfb-2fc6ad0bf449 //cdd97aab-d256-4f07-9145-a0a2b1555322
"APINoticeUserList": [ "u", "wait..." ],
- "VueNoticeUserList": [ "wangxiaoshuang", "6b7717a31647293621697b96f74e6f3d" ]
+ "VueNoticeUserList": [ "wangxiaoshuang", "6b7717a31647293621b97b96f74e6f3d" ]
},
"ObjectStoreService": {
"ObjectStoreUse": "AliyunOSS",
+ "IsOpenStoreSync": true,
+ "ApiDeployRegion": "CN",
+ "SyncConfigList": [
+ {
+ "Domain": "irc.uat.extimaging.com",
+ "Primary": "AliyunOSS",
+ "Target": "AWS",
+ "UploadRegion": "CN",
+ "TargetRegion": "US",
+ "IsOpenSync": true
+ },
+ {
+ "Domain": "lili.uat.extimaging.com",
+ "Primary": "AWS",
+ "Target": "AliyunOSS",
+ "UploadRegion": "US",
+ "TargetRegion": "CN",
+ "IsOpenSync": true
+ }
+ ],
"AliyunOSS": {
"RegionId": "cn-shanghai",
@@ -104,10 +124,8 @@
"CompanyShortName": "Extensive Imaging",
"CompanyShortNameCN": "展影医疗",
"IsEnv_US": false,
- "IsOpenErrorNoticeEmail": false,
"EmailRegexStr": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
- "CronEmailDefaultCulture": "zh-CN",
- "ErrorNoticeEmailList": [ "872297557@qq.com" ]
+ "CronEmailDefaultCulture": "zh-CN"
},
"SystemPacsConfig": {
diff --git a/IRaCIS.Core.API/wwwroot/swagger/ui/Index.html b/IRaCIS.Core.API/wwwroot/swagger/ui/Index.html
index fff5ae586..0943ce0e3 100644
--- a/IRaCIS.Core.API/wwwroot/swagger/ui/Index.html
+++ b/IRaCIS.Core.API/wwwroot/swagger/ui/Index.html
@@ -1,4 +1,4 @@
-
+
@@ -112,8 +112,23 @@
}, info: function () {
return null;
}
+ },
+ fn: {
+ opsFilter: function (taggedOps, phrase) {
+ var normalPhrase = phrase.toLowerCase();
+ return taggedOps.map(function (tagObj, tag) {
+ var operations = tagObj.get("operations").filter(function (op) {
+ var summary = op.getIn(["operation", "summary"]) || "";
+ var path = op.get("path") || "";
+ var tagMatch = tag.toLowerCase().indexOf(normalPhrase) !== -1;
+ return tagMatch || summary.toLowerCase().indexOf(normalPhrase) !== -1 || path.toLowerCase().indexOf(normalPhrase) !== -1;
+ });
+ return tagObj.set("operations", operations);
+ }).filter(function (tagObj) {
+ return tagObj.get("operations").size > 0;
+ });
+ }
}
-
}
}
];
diff --git a/IRaCIS.Core.Application/Auth/TokenService.cs b/IRaCIS.Core.Application/Auth/TokenService.cs
index 6b4f908a4..8f00c77c5 100644
--- a/IRaCIS.Core.Application/Auth/TokenService.cs
+++ b/IRaCIS.Core.Application/Auth/TokenService.cs
@@ -39,7 +39,8 @@ namespace IRaCIS.Core.Application.Auth
new Claim(JwtIRaCISClaimType.UserTypeShortName,user.UserTypeShortName),
new Claim(JwtIRaCISClaimType.PermissionStr,user.PermissionStr),
new Claim(JwtIRaCISClaimType.IsZhiZhun,user.IsZhiZhun.ToString()),
- new Claim(JwtIRaCISClaimType.IsTestUser,user.IsTestUser.ToString())
+ new Claim(JwtIRaCISClaimType.IsTestUser,user.IsTestUser.ToString()),
+ new Claim(JwtIRaCISClaimType.UserWorkLanguage,user.UserWorkLanguage.ToString())
};
////创建令牌
diff --git a/IRaCIS.Core.Application/Auth/UserTokenInfo.cs b/IRaCIS.Core.Application/Auth/UserTokenInfo.cs
index ab104ae58..44a9394e4 100644
--- a/IRaCIS.Core.Application/Auth/UserTokenInfo.cs
+++ b/IRaCIS.Core.Application/Auth/UserTokenInfo.cs
@@ -23,5 +23,7 @@ namespace IRaCIS.Core.Application.Auth
public bool IsZhiZhun { get; set; }
public string UserTypeShortName { get; set; } = string.Empty;
+
+ public UserWorkLanguage UserWorkLanguage { get; set; }
}
}
\ No newline at end of file
diff --git a/IRaCIS.Core.Application/BusinessFilter/_Config/_AppSettings.cs b/IRaCIS.Core.Application/BusinessFilter/_Config/_AppSettings.cs
index ac487b773..757a1d7ae 100644
--- a/IRaCIS.Core.Application/BusinessFilter/_Config/_AppSettings.cs
+++ b/IRaCIS.Core.Application/BusinessFilter/_Config/_AppSettings.cs
@@ -79,11 +79,12 @@ public class SystemEmailSendConfig
public bool IsEnv_US { get; set; }
- public bool IsOpenErrorNoticeEmail { get; set; }
-
public string EmailRegexStr { get; set; }
- public List ErrorNoticeEmailList { get; set; } = new List();
+ //public bool IsOpenErrorNoticeEmail { get; set; }
+
+
+ //public List ErrorNoticeEmailList { get; set; } = new List();
}
public class SystemEmailSendConfigView
diff --git a/IRaCIS.Core.Application/Helper/CacheHelper.cs b/IRaCIS.Core.Application/Helper/CacheHelper.cs
index 15bf0008b..5d70a0928 100644
--- a/IRaCIS.Core.Application/Helper/CacheHelper.cs
+++ b/IRaCIS.Core.Application/Helper/CacheHelper.cs
@@ -1,4 +1,6 @@
-namespace IRaCIS.Core.Application.Helper;
+using DocumentFormat.OpenXml.Spreadsheet;
+
+namespace IRaCIS.Core.Application.Helper;
public static class CacheKeys
@@ -66,6 +68,10 @@ public static class CacheKeys
public static string UserMFATag(Guid userId) => $"UserMFAVerifyPass:{userId}";
+ public static string TrialDataStoreType(Guid trialId) => $"TrialDataStoreType:{trialId}";
+
+
+ public static string UserQCSkipTask(Guid userId) => $"UserSKipQCTask:{userId}";
}
public static class CacheHelper
@@ -77,6 +83,9 @@ public static class CacheHelper
return statusStr;
}
+
+
+
public static async Task> GetSystemAnonymizationListAsync(IRepository _systemAnonymizationRepository)
{
var list = await _systemAnonymizationRepository.Where(t => t.IsEnable).ToListAsync();
diff --git a/IRaCIS.Core.Application/Helper/DicomDIRHelper.cs b/IRaCIS.Core.Application/Helper/DicomDIRHelper.cs
index 12c387440..cba05d034 100644
--- a/IRaCIS.Core.Application/Helper/DicomDIRHelper.cs
+++ b/IRaCIS.Core.Application/Helper/DicomDIRHelper.cs
@@ -1,6 +1,8 @@
using DocumentFormat.OpenXml.Office.CustomUI;
using FellowOakDicom;
using FellowOakDicom.Media;
+using IRaCIS.Core.Application.ViewModel;
+using IRaCIS.Core.Domain.Models;
using System;
using System.Collections.Generic;
using System.Data;
@@ -58,7 +60,11 @@ namespace IRaCIS.Core.Application.Helper
var mappings = new List();
int index = 1;
- var studyUid=list.FirstOrDefault()?.StudyInstanceUid;
+ var trialId = Guid.Empty;
+
+ Guid.TryParse(ossFolder.Split('/', StringSplitOptions.RemoveEmptyEntries)[0], out trialId);
+
+ var studyUid = list.FirstOrDefault()?.StudyInstanceUid;
var dicomDir = new DicomDirectory();
@@ -130,9 +136,9 @@ namespace IRaCIS.Core.Application.Helper
// 重置流位置
memoryStream.Position = 0;
- var relativePath= await _oSSService.UploadToOSSAsync(memoryStream, ossFolder, "DICOMDIR", true);
+ var relativePath = await _oSSService.UploadToOSSAsync(memoryStream, ossFolder, "DICOMDIR", true, uploadInfo: new FileUploadRecordAddOrEdit() { TrialId = trialId, BatchDataType = BatchDataType.DICOMDIR });
- dic.Add($"{studyUid}_DICOMDIR" , relativePath.Split('/').Last());
+ dic.Add($"{studyUid}_DICOMDIR", relativePath.Split('/').Last());
}
//清理临时文件
@@ -146,7 +152,7 @@ namespace IRaCIS.Core.Application.Helper
var mappingText = string.Join(Environment.NewLine, mappings);
await using var mappingStream = new MemoryStream(Encoding.UTF8.GetBytes(mappingText));
- await _oSSService.UploadToOSSAsync(mappingStream, ossFolder, $"Download_{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}", false);
+ await _oSSService.UploadToOSSAsync(mappingStream, ossFolder, $"Download_{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}", false, uploadInfo: new FileUploadRecordAddOrEdit() { TrialId = trialId, BatchDataType = BatchDataType.DICOMDIR });
#endregion
}
diff --git a/IRaCIS.Core.Application/Helper/Email/SendEmailHelper.cs b/IRaCIS.Core.Application/Helper/Email/SendEmailHelper.cs
index b87896b90..8597cbf7d 100644
--- a/IRaCIS.Core.Application/Helper/Email/SendEmailHelper.cs
+++ b/IRaCIS.Core.Application/Helper/Email/SendEmailHelper.cs
@@ -17,7 +17,7 @@ public static class SendEmailHelper
//没有收件人 那么不发送
if (messageToSend.To.Count == 0)
- {
+ {
return string.Empty;
}
@@ -147,7 +147,7 @@ public static class SendEmailHelper
return true;
}
- public static async Task SendEmailAsync(SMTPEmailConfig sMTPEmailConfig,Trial? trial, EventHandler? messageSentSuccess = null)
+ public static async Task SendEmailAsync(SMTPEmailConfig sMTPEmailConfig, Trial? trial, EventHandler? messageSentSuccess = null)
{
var messageToSend = new MimeMessage();
@@ -161,8 +161,9 @@ public static class SendEmailHelper
if (sMTPEmailConfig.ToMailAddressList.Count == 0)
{
+ return;
//---没有收件人
- throw new ArgumentException(I18n.T("SendEmail_NoRecipient"));
+ //throw new ArgumentException(I18n.T("SendEmail_NoRecipient"));
}
else
{
diff --git a/IRaCIS.Core.Application/Helper/OSSService.cs b/IRaCIS.Core.Application/Helper/OSSService.cs
index 082959569..ec04c3f08 100644
--- a/IRaCIS.Core.Application/Helper/OSSService.cs
+++ b/IRaCIS.Core.Application/Helper/OSSService.cs
@@ -7,6 +7,10 @@ using Amazon.S3;
using Amazon.S3.Model;
using Amazon.SecurityToken;
using Amazon.SecurityToken.Model;
+using DocumentFormat.OpenXml.Bibliography;
+using IRaCIS.Application.Contracts;
+using IRaCIS.Core.Application.Interfaces;
+using IRaCIS.Core.Application.ViewModel;
using IRaCIS.Core.Infrastructure;
using IRaCIS.Core.Infrastructure.NewtonsoftJson;
using MassTransit;
@@ -16,6 +20,8 @@ using Minio;
using Minio.DataModel;
using Minio.DataModel.Args;
using Minio.Exceptions;
+using Serilog.Parsing;
+using SkiaSharp;
using System.IO;
using System.Reactive.Linq;
using System.Runtime.InteropServices;
@@ -80,6 +86,28 @@ public class ObjectStoreServiceOptions
public AWSOptions AWS { get; set; }
+ public bool IsOpenStoreSync { get; set; }
+
+ public string ApiDeployRegion { get; set; }
+
+ public List SyncConfigList { get; set; } = new List();
+
+}
+
+public class SyncStoreConfig
+{
+ public string Domain { get; set; }
+
+ public string UploadRegion { get; set; }
+
+ public string TargetRegion { get; set; }
+
+ public string Primary { get; set; }
+
+ public string Target { get; set; }
+
+ public bool IsOpenSync { get; set; }
+
}
public class ObjectStoreDTO
@@ -93,6 +121,10 @@ public class ObjectStoreDTO
public AWSTempToken AWS { get; set; }
+ public bool IsOpenStoreSync { get; set; }
+
+ public List SyncConfigList { get; set; }
+
}
[LowerCamelCaseJson]
@@ -137,6 +169,7 @@ public enum ObjectStoreUse
AWS = 2,
}
+
#endregion
// aws 参考链接 https://github.com/awsdocs/aws-doc-sdk-examples/tree/main/dotnetv3/S3/S3_Basics
@@ -147,8 +180,8 @@ public interface IOSSService
public Task RestoreFilesByPrefixAsync(string prefix, int restoreDays = 3, int batchSize = 100);
- public Task UploadToOSSAsync(Stream fileStream, string oosFolderPath, string fileRealName, bool isFileNameAddGuid = true);
- public Task UploadToOSSAsync(string localFilePath, string oosFolderPath, bool isFileNameAddGuid = true, bool randomFileName = false);
+ public Task UploadToOSSAsync(Stream fileStream, string oosFolderPath, string fileRealName, bool isFileNameAddGuid = true, FileUploadRecordAddOrEdit? uploadInfo = null);
+ public Task UploadToOSSAsync(string localFilePath, string oosFolderPath, bool isFileNameAddGuid = true, bool randomFileName = false, FileUploadRecordAddOrEdit? uploadInfo = null);
public Task DownLoadFromOSSAsync(string ossRelativePath, string localFilePath);
@@ -164,28 +197,30 @@ public interface IOSSService
List GetRootFolderNames();
- public ObjectStoreDTO GetObjectStoreTempToken();
+ public ObjectStoreDTO GetObjectStoreTempToken(string? domain = null, bool? isGetAllTempToken = null, string? objectUse = null);
public Task MoveObject(string sourcePath, string destPath, bool overwrite = true);
public Task GetObjectSizeAsync(string sourcePath);
+
+ public Task SyncFileAsync(string objectKey, ObjectStoreUse source, ObjectStoreUse destination, CancellationToken ct = default);
+
+ public void ConvertPrefixToStandard(string prefix);
}
-public class OSSService : IOSSService
+public class OSSService(IOptionsMonitor options,
+ IFileUploadRecordService _fileUploadRecordService) : IOSSService
{
- public ObjectStoreServiceOptions ObjectStoreServiceOptions { get; set; }
+ public ObjectStoreServiceOptions ObjectStoreServiceOptions { get; set; } = options.CurrentValue;
private AliyunOSSTempToken AliyunOSSTempToken { get; set; }
private AWSTempToken AWSTempToken { get; set; }
+ public object result { get; private set; }
- public OSSService(IOptionsMonitor options)
- {
- ObjectStoreServiceOptions = options.CurrentValue;
- }
///
/// 将指定前缀下的所有现有文件立即转为目标存储类型
@@ -506,8 +541,8 @@ public class OSSService : IOSSService
var clientConfig = new AmazonS3Config
{
- RegionEndpoint = RegionEndpoint.GetBySystemName(awsConfig.Region),
- UseHttp = true,
+ RegionEndpoint = RegionEndpoint.GetBySystemName(awsConfig.Region)
+ //,UseHttp = true,
};
using var client = new AmazonS3Client(credentials, clientConfig);
@@ -620,6 +655,90 @@ public class OSSService : IOSSService
}
+ ///
+ /// 将某个路径下的归档的文件 转为标准存储
+ ///
+ ///
+ public void ConvertPrefixToStandard(string prefix)
+ {
+
+ 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
+ );
+ var bucketName = aliConfig.BucketName;
+
+ try
+ {
+ ObjectListing objectListing = null;
+ string nextMarker = null;
+ do
+ {
+ // 使用 prefix 模拟目录结构,设置 MaxKeys 和 NextMarker
+ objectListing = _ossClient.ListObjects(new Aliyun.OSS.ListObjectsRequest(bucketName)
+ {
+ Prefix = prefix,
+ MaxKeys = 1000,
+ Marker = nextMarker
+ });
+
+
+ foreach (var obj in objectListing.ObjectSummaries)
+ {
+ try
+ {
+ // 👇 跳过已经是标准存储的(优化)
+ if (obj.StorageClass == StorageClass.Standard.ToString())
+ continue;
+
+ var metadata = new ObjectMetadata();
+
+ // 👇 关键:手动加 Header
+ metadata.AddHeader("x-oss-storage-class", "Standard");
+
+ var copyRequest = new Aliyun.OSS.CopyObjectRequest(bucketName, obj.Key, bucketName, obj.Key) { NewObjectMetadata = metadata };
+
+
+ _ossClient.CopyObject(copyRequest);
+
+ }
+ catch (Exception ex)
+ {
+ Log.Logger.Error($"❌ 失败: {obj.Key}, 错误: {ex.Message}");
+ }
+ }
+
+
+
+ // 设置 NextMarker 以获取下一页的数据
+ nextMarker = objectListing.NextMarker;
+
+ } while (objectListing.IsTruncated);
+ }
+ catch (Exception ex)
+ {
+ Log.Logger.Error($"Error: {ex.Message}");
+ }
+
+
+ }
+ else if (ObjectStoreServiceOptions.ObjectStoreUse == "AWS")
+ {
+ throw new BusinessValidationFailedException("未定义的存储介质类型");
+ }
+
+
+ }
+
+
///
/// 坑方法,会清空之前的规则
///
@@ -686,8 +805,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);
@@ -700,6 +819,9 @@ public class OSSService : IOSSService
}
}
+
+
+
///
/// oosFolderPath 不要 "/ "开头 应该: TempFolder/ChildFolder
///
@@ -707,8 +829,9 @@ public class OSSService : IOSSService
///
///
///
+ /// 只用赋值业务参数Id 和批次信息即可,其他信息不用传递
///
- public async Task UploadToOSSAsync(Stream fileStream, string oosFolderPath, string fileRealName, bool isFileNameAddGuid = true)
+ public async Task UploadToOSSAsync(Stream fileStream, string oosFolderPath, string fileRealName, bool isFileNameAddGuid = true, FileUploadRecordAddOrEdit? uploadInfo = null)
{
BackBatchGetToken();
@@ -716,74 +839,68 @@ public class OSSService : IOSSService
try
{
- using (var memoryStream = new MemoryStream())
- {
+ if (fileStream.CanSeek)
fileStream.Seek(0, SeekOrigin.Begin);
- fileStream.CopyTo(memoryStream);
- memoryStream.Seek(0, SeekOrigin.Begin);
+ if (ObjectStoreServiceOptions.ObjectStoreUse == "AliyunOSS")
+ {
+ var aliConfig = ObjectStoreServiceOptions.AliyunOSS;
+
+ var _ossClient = new OssClient(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? aliConfig.EndPoint : aliConfig.InternalEndpoint, AliyunOSSTempToken.AccessKeyId, AliyunOSSTempToken.AccessKeySecret, AliyunOSSTempToken.SecurityToken);
- if (ObjectStoreServiceOptions.ObjectStoreUse == "AliyunOSS")
+
+ // 上传文件
+ var result = _ossClient.PutObject(aliConfig.BucketName, ossRelativePath, fileStream);
+
+ }
+ else if (ObjectStoreServiceOptions.ObjectStoreUse == "MinIO")
+ {
+ var minIOConfig = ObjectStoreServiceOptions.MinIO;
+
+
+ var minioClient = new MinioClient().WithEndpoint($"{minIOConfig.EndPoint}:{minIOConfig.Port}")
+ .WithCredentials(minIOConfig.AccessKeyId, minIOConfig.SecretAccessKey).WithSSL(minIOConfig.UseSSL)
+ .Build();
+
+ var putObjectArgs = new PutObjectArgs()
+ .WithBucket(minIOConfig.BucketName)
+ .WithObject(ossRelativePath)
+ .WithStreamData(fileStream)
+ .WithObjectSize(fileStream.Length);
+
+ await minioClient.PutObjectAsync(putObjectArgs);
+ }
+ else if (ObjectStoreServiceOptions.ObjectStoreUse == "AWS")
+ {
+ var awsConfig = ObjectStoreServiceOptions.AWS;
+
+ var credentials = new SessionAWSCredentials(AWSTempToken.AccessKeyId, AWSTempToken.SecretAccessKey, AWSTempToken.SessionToken);
+
+
+
+ //提供awsEndPoint(域名)进行访问配置
+ var clientConfig = new AmazonS3Config
{
- var aliConfig = ObjectStoreServiceOptions.AliyunOSS;
+ RegionEndpoint = RegionEndpoint.GetBySystemName(awsConfig.Region)
+ //,UseHttp = true,
+ };
- var _ossClient = new OssClient(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? aliConfig.EndPoint : aliConfig.InternalEndpoint, AliyunOSSTempToken.AccessKeyId, AliyunOSSTempToken.AccessKeySecret, AliyunOSSTempToken.SecurityToken);
+ var amazonS3Client = new AmazonS3Client(credentials, clientConfig);
-
-
- // 上传文件
- var result = _ossClient.PutObject(aliConfig.BucketName, ossRelativePath, memoryStream);
-
- }
- else if (ObjectStoreServiceOptions.ObjectStoreUse == "MinIO")
+ var putObjectRequest = new Amazon.S3.Model.PutObjectRequest()
{
- var minIOConfig = ObjectStoreServiceOptions.MinIO;
+ BucketName = awsConfig.BucketName,
+ InputStream = fileStream,
+ Key = ossRelativePath,
+ };
-
- var minioClient = new MinioClient().WithEndpoint($"{minIOConfig.EndPoint}:{minIOConfig.Port}")
- .WithCredentials(minIOConfig.AccessKeyId, minIOConfig.SecretAccessKey).WithSSL(minIOConfig.UseSSL)
- .Build();
-
- var putObjectArgs = new PutObjectArgs()
- .WithBucket(minIOConfig.BucketName)
- .WithObject(ossRelativePath)
- .WithStreamData(memoryStream)
- .WithObjectSize(memoryStream.Length);
-
- await minioClient.PutObjectAsync(putObjectArgs);
- }
- else if (ObjectStoreServiceOptions.ObjectStoreUse == "AWS")
- {
- var 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);
-
- var putObjectRequest = new Amazon.S3.Model.PutObjectRequest()
- {
- BucketName = awsConfig.BucketName,
- InputStream = memoryStream,
- Key = ossRelativePath,
- };
-
- await amazonS3Client.PutObjectAsync(putObjectRequest);
- }
- else
- {
- throw new BusinessValidationFailedException("未定义的存储介质类型");
- }
+ await amazonS3Client.PutObjectAsync(putObjectRequest);
+ }
+ else
+ {
+ throw new BusinessValidationFailedException("未定义的存储介质类型");
}
}
catch (Exception ex)
@@ -793,13 +910,27 @@ public class OSSService : IOSSService
}
+ var returnPath = "/" + ossRelativePath;
- return "/" + ossRelativePath;
+ if (ObjectStoreServiceOptions.IsOpenStoreSync && uploadInfo != null)
+ {
+ uploadInfo.FileSize = fileStream.CanSeek ? fileStream.Length : 0;
+ uploadInfo.Path = returnPath;
+ uploadInfo.FileName = fileRealName;
+ uploadInfo.FileType = Path.GetExtension(returnPath).TrimStart('.');
+
+
+ await _fileUploadRecordService.AddOrUpdateFileUploadRecord(uploadInfo);
+ }
+
+
+ return returnPath;
}
+
//后端批量上传 或者下载,不每个文件获取临时token
private void BackBatchGetToken()
{
@@ -841,10 +972,12 @@ public class OSSService : IOSSService
/// 随机文件名
///
///
- public async Task UploadToOSSAsync(string localFilePath, string oosFolderPath, bool isFileNameAddGuid = true, bool randomFileName = false)
+ public async Task UploadToOSSAsync(string localFilePath, string oosFolderPath, bool isFileNameAddGuid = true, bool randomFileName = false, FileUploadRecordAddOrEdit? uploadInfo = null)
{
BackBatchGetToken();
+ long fileSize = 0;
+
var localFileName = Path.GetFileName(localFilePath);
var ossRelativePath = isFileNameAddGuid ? $"{oosFolderPath}/{Guid.NewGuid()}_{localFileName}" : $"{oosFolderPath}/{localFileName}";
@@ -864,6 +997,8 @@ public class OSSService : IOSSService
// 上传文件
var result = _ossClient.PutObject(aliConfig.BucketName, ossRelativePath, localFilePath);
+ fileSize = result.ContentLength;
+
}
else if (ObjectStoreServiceOptions.ObjectStoreUse == "MinIO")
{
@@ -879,7 +1014,9 @@ public class OSSService : IOSSService
.WithObject(ossRelativePath)
.WithFileName(localFilePath);
- await minioClient.PutObjectAsync(putObjectArgs);
+ var result = await minioClient.PutObjectAsync(putObjectArgs);
+
+ fileSize = result.Size;
}
else if (ObjectStoreServiceOptions.ObjectStoreUse == "AWS")
{
@@ -904,14 +1041,30 @@ public class OSSService : IOSSService
Key = ossRelativePath,
};
- await amazonS3Client.PutObjectAsync(putObjectRequest);
-
+ var result = await amazonS3Client.PutObjectAsync(putObjectRequest);
+ fileSize = result.ContentLength;
}
else
{
throw new BusinessValidationFailedException("未定义的存储介质类型");
}
- return "/" + ossRelativePath;
+
+ var returnPath = "/" + ossRelativePath;
+
+
+ if (ObjectStoreServiceOptions.IsOpenStoreSync && uploadInfo != null)
+ {
+ uploadInfo.FileSize = fileSize;
+ uploadInfo.Path = returnPath;
+ uploadInfo.FileName = Path.GetFileName(localFilePath);
+ uploadInfo.FileType = Path.GetExtension(returnPath).TrimStart(".");
+
+
+ await _fileUploadRecordService.AddOrUpdateFileUploadRecord(uploadInfo);
+ }
+
+
+ return returnPath;
}
@@ -920,6 +1073,13 @@ public class OSSService : IOSSService
{
BackBatchGetToken();
+ // 确保目标目录存在
+ string directory = Path.GetDirectoryName(localFilePath);
+ if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
+ {
+ Directory.CreateDirectory(directory);
+ }
+
ossRelativePath = ossRelativePath.TrimStart('/');
try
{
@@ -1023,11 +1183,6 @@ public class OSSService : IOSSService
// 直接返回流
return result.Content;
- //// 将OSS返回的流复制到内存流中并返回
- //var memoryStream = new MemoryStream();
- //await result.Content.CopyToAsync(memoryStream);
- //memoryStream.Position = 0; // 重置位置以便读取
- //return memoryStream;
}
else if (ObjectStoreServiceOptions.ObjectStoreUse == "MinIO")
{
@@ -1076,8 +1231,8 @@ public class OSSService : IOSSService
var clientConfig = new AmazonS3Config
{
- RegionEndpoint = RegionEndpoint.USEast1,
- UseHttp = true,
+ RegionEndpoint = RegionEndpoint.GetBySystemName(awsConfig.Region)
+ //,UseHttp = true,
};
var amazonS3Client = new AmazonS3Client(credentials, clientConfig);
@@ -1093,10 +1248,6 @@ public class OSSService : IOSSService
// ⭐ 直接返回流
return response.ResponseStream;
- //var memoryStream = new MemoryStream();
- //await response.ResponseStream.CopyToAsync(memoryStream);
- //memoryStream.Position = 0;
- //return memoryStream;
}
else
{
@@ -1337,8 +1488,8 @@ public class OSSService : IOSSService
var clientConfig = new AmazonS3Config
{
- RegionEndpoint = RegionEndpoint.USEast1,
- UseHttp = true,
+ RegionEndpoint = RegionEndpoint.GetBySystemName(awsConfig.Region)
+ //,UseHttp = true,
};
var amazonS3Client = new AmazonS3Client(credentials, clientConfig);
@@ -1440,9 +1591,36 @@ public class OSSService : IOSSService
///
public async Task DeleteFromPrefix(string prefix, bool isCache = false)
{
- GetObjectStoreTempToken();
+ if (prefix.StartsWith("/"))
+ {
+ prefix = prefix.TrimStart("/");
+ }
- if (ObjectStoreServiceOptions.ObjectStoreUse == "AliyunOSS")
+ //打开了同步的,删除的时候,一起删除
+ if (ObjectStoreServiceOptions.IsOpenStoreSync && ObjectStoreServiceOptions.SyncConfigList.Any(t => t.IsOpenSync))
+ {
+ foreach (var config in ObjectStoreServiceOptions.SyncConfigList.Where(t => t.IsOpenSync))
+ {
+
+
+ GetObjectStoreTempToken(objectUse: config.Primary);
+
+ await DeleteFromPrefixInternal(config.Primary, prefix, isCache);
+ }
+ }
+ else
+ {
+ GetObjectStoreTempToken();
+
+ await DeleteFromPrefixInternal(ObjectStoreServiceOptions.ObjectStoreUse, prefix, isCache);
+ }
+
+ }
+
+
+ private async Task DeleteFromPrefixInternal(string objectUse, string prefix, bool isCache = false)
+ {
+ if (objectUse == "AliyunOSS")
{
var aliConfig = ObjectStoreServiceOptions.AliyunOSS;
@@ -1498,7 +1676,7 @@ public class OSSService : IOSSService
}
- else if (ObjectStoreServiceOptions.ObjectStoreUse == "MinIO")
+ else if (objectUse == "MinIO")
{
var minIOConfig = ObjectStoreServiceOptions.MinIO;
@@ -1535,7 +1713,7 @@ public class OSSService : IOSSService
}
- else if (ObjectStoreServiceOptions.ObjectStoreUse == "AWS")
+ else if (objectUse == "AWS")
{
var awsConfig = ObjectStoreServiceOptions.AWS;
@@ -1591,8 +1769,31 @@ public class OSSService : IOSSService
}
}
+
public async Task DeleteObjects(List objectKeys, bool isCache = false)
{
+ //打开了同步的,删除的时候,一起删除
+ if (ObjectStoreServiceOptions.IsOpenStoreSync && ObjectStoreServiceOptions.SyncConfigList.Any(t => t.IsOpenSync))
+ {
+ foreach (var config in ObjectStoreServiceOptions.SyncConfigList.Where(t => t.IsOpenSync))
+ {
+
+ GetObjectStoreTempToken(objectUse: config.Primary);
+
+ await DeleteObjectsInternal(objectKeys, isCache);
+ }
+ }
+ else
+ {
+ GetObjectStoreTempToken();
+
+ await DeleteObjectsInternal(objectKeys, isCache);
+ }
+ }
+ public async Task DeleteObjectsInternal(List objectKeys, bool isCache = false)
+ {
+
+
GetObjectStoreTempToken();
if (ObjectStoreServiceOptions.ObjectStoreUse == "AliyunOSS")
@@ -1755,13 +1956,42 @@ public class OSSService : IOSSService
- public ObjectStoreDTO GetObjectStoreTempToken()
+ public ObjectStoreDTO GetObjectStoreTempToken(string? domain = null, bool? isGetAllTempToken = null, string? objectUse = null)
{
-
- var ossOptions = ObjectStoreServiceOptions.AliyunOSS;
-
- if (ObjectStoreServiceOptions.ObjectStoreUse == "AliyunOSS")
+ string objectStoreUse = string.Empty;
+ //使用指定配置
+ if (objectUse != null)
{
+ objectStoreUse = objectUse?.Trim() ?? string.Empty;
+ }
+ //根据域名动态判断
+ else
+ {
+ //如果传递了域名,并且打开了存储同步,根据域名使用的具体存储覆盖之前的配置,否则就用固定的配置
+ if (ObjectStoreServiceOptions.IsOpenStoreSync && domain.IsNotNullOrEmpty() && ObjectStoreServiceOptions.SyncConfigList.Any(t => t.Domain == domain))
+ {
+
+ var find = ObjectStoreServiceOptions.SyncConfigList.FirstOrDefault(t => t.Domain == domain);
+ if (find != null)
+ {
+ objectStoreUse = find.Primary;
+ }
+
+ }
+ else
+ {
+ //兜底,如果是本地测试环境,那就使用部署默认配置
+ objectStoreUse = ObjectStoreServiceOptions.ObjectStoreUse;
+ }
+ }
+
+
+ var objectStoreDTO = new ObjectStoreDTO() { ObjectStoreUse = objectStoreUse, IsOpenStoreSync = ObjectStoreServiceOptions.IsOpenStoreSync, SyncConfigList = ObjectStoreServiceOptions.SyncConfigList };
+
+ if (objectStoreUse == "AliyunOSS" || isGetAllTempToken == true)
+ {
+ var ossOptions = ObjectStoreServiceOptions.AliyunOSS;
+
var client = new Client(new AlibabaCloud.OpenApiClient.Models.Config()
{
AccessKeyId = ossOptions.AccessKeyId,
@@ -1803,13 +2033,14 @@ public class OSSService : IOSSService
AliyunOSSTempToken = tempToken;
- return new ObjectStoreDTO() { ObjectStoreUse = ObjectStoreServiceOptions.ObjectStoreUse, AliyunOSS = tempToken };
+ objectStoreDTO.AliyunOSS = tempToken;
+
}
- else if (ObjectStoreServiceOptions.ObjectStoreUse == "MinIO")
+ if (objectStoreUse == "MinIO")
{
- return new ObjectStoreDTO() { ObjectStoreUse = ObjectStoreServiceOptions.ObjectStoreUse, MinIO = ObjectStoreServiceOptions.MinIO };
+ objectStoreDTO.MinIO = ObjectStoreServiceOptions.MinIO;
}
- else if (ObjectStoreServiceOptions.ObjectStoreUse == "AWS")
+ if (objectStoreUse == "AWS" || isGetAllTempToken == true)
{
var awsOptions = ObjectStoreServiceOptions.AWS;
@@ -1851,12 +2082,111 @@ public class OSSService : IOSSService
};
AWSTempToken = tempToken;
- return new ObjectStoreDTO() { ObjectStoreUse = ObjectStoreServiceOptions.ObjectStoreUse, AWS = tempToken };
+
+ objectStoreDTO.AWS = tempToken;
}
- else
+
+ if (objectStoreUse.IsNullOrEmpty())
{
throw new BusinessValidationFailedException("未定义的存储介质类型");
}
+
+ return objectStoreDTO;
}
+
+ public async Task SyncFileAsync(string objectKey, ObjectStoreUse source, ObjectStoreUse destination, CancellationToken ct = default)
+ {
+ var tempConfig = GetObjectStoreTempToken(isGetAllTempToken: true);
+
+
+ var aliConfig = ObjectStoreServiceOptions.AliyunOSS;
+
+ var _ossClient = new OssClient(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? aliConfig.EndPoint : aliConfig.InternalEndpoint, AliyunOSSTempToken.AccessKeyId, AliyunOSSTempToken.AccessKeySecret, AliyunOSSTempToken.SecurityToken);
+
+ var awsConfig = ObjectStoreServiceOptions.AWS;
+
+ var credentials = new SessionAWSCredentials(AWSTempToken.AccessKeyId, AWSTempToken.SecretAccessKey, AWSTempToken.SessionToken);
+
+ //提供awsEndPoint(域名)进行访问配置
+ var clientConfig = new AmazonS3Config
+ {
+ RegionEndpoint = RegionEndpoint.GetBySystemName(awsConfig.Region)
+ };
+
+ var amazonS3Client = new AmazonS3Client(credentials, clientConfig);
+
+ // ⭐ 关键变量
+ IDisposable? owner = null;
+ Stream sourceStream;
+ long contentLength;
+
+ // ========= 获取流 + 长度 =========
+ switch (source)
+ {
+ case ObjectStoreUse.AliyunOSS:
+ {
+
+ var obj = _ossClient.GetObject(
+ aliConfig.BucketName,
+ objectKey);
+
+ owner = obj;
+ sourceStream = obj.Content;
+ contentLength = obj.ContentLength;
+ break;
+ }
+
+ case ObjectStoreUse.AWS:
+ {
+ var response = await amazonS3Client.GetObjectAsync(
+ awsConfig.BucketName,
+ objectKey,
+ ct);
+
+ owner = response;
+ sourceStream = response.ResponseStream;
+ contentLength = response.Headers.ContentLength;
+ break;
+ }
+
+ default:
+ throw new BusinessValidationFailedException("未定义的同步类型");
+ }
+
+ try
+ {
+ // ========= 上传 =========
+ if (destination == ObjectStoreUse.AWS)
+ {
+ var putRequest = new Amazon.S3.Model.PutObjectRequest
+ {
+ BucketName = awsConfig.BucketName,
+ Key = objectKey,
+ InputStream = sourceStream,
+ Headers = { ContentLength = contentLength }
+ };
+
+ await amazonS3Client.PutObjectAsync(putRequest, ct);
+ }
+ else if (destination == ObjectStoreUse.AliyunOSS)
+ {
+ _ossClient.PutObject(
+ aliConfig.BucketName,
+ objectKey,
+ sourceStream);
+ }
+ else
+ {
+ throw new BusinessValidationFailedException("未定义的同步类型");
+ }
+ }
+ finally
+ {
+ // ⭐⭐⭐ 真正释放 HTTP 连接
+ owner?.Dispose();
+ }
+ }
+
+
}
diff --git a/IRaCIS.Core.Application/Helper/OtherTool/DicomSortHelper.cs b/IRaCIS.Core.Application/Helper/OtherTool/DicomSortHelper.cs
new file mode 100644
index 000000000..7ae24842d
--- /dev/null
+++ b/IRaCIS.Core.Application/Helper/OtherTool/DicomSortHelper.cs
@@ -0,0 +1,1169 @@
+
+using FellowOakDicom;
+using FellowOakDicom.Imaging;
+using FellowOakDicom.Imaging.Codec;
+using FellowOakDicom.IO.Buffer;
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.Formats.Jpeg;
+using SixLabors.ImageSharp.Processing;
+using System.Data;
+
+namespace IRaCIS.Core.Application.Helper;
+
+#region 废弃
+
+public static class DicomSliceSorterFast
+{
+ public static List Sort(
+ IEnumerable source,
+ Func ippSelector,
+ Func iopSelector,
+ Func instanceSelector)
+ {
+ var items = source.ToList();
+ if (items.Count < 2)
+ return items;
+
+ var refIPP = Parse(ippSelector(items[0]));
+ var refIOP = Parse(iopSelector(items[0]));
+
+ if (refIPP == null || refIOP == null || refIOP.Length != 6)
+ return items.OrderBy(instanceSelector).ToList();
+
+ // normal
+ var nx = refIOP[1] * refIOP[5] - refIOP[2] * refIOP[4];
+ var ny = refIOP[2] * refIOP[3] - refIOP[0] * refIOP[5];
+ var nz = refIOP[0] * refIOP[4] - refIOP[1] * refIOP[3];
+
+ var projections = new (T item, double dist)[items.Count];
+
+ double min = double.MaxValue;
+ double max = double.MinValue;
+
+ // ---------- projection pass ----------
+ for (int i = 0; i < items.Count; i++)
+ {
+ var ipp = Parse(ippSelector(items[i]));
+ if (ipp == null)
+ return items.OrderBy(instanceSelector).ToList();
+
+ var dx = refIPP[0] - ipp[0];
+ var dy = refIPP[1] - ipp[1];
+ var dz = refIPP[2] - ipp[2];
+
+ var dist = dx * nx + dy * ny + dz * nz;
+
+ projections[i] = (items[i], dist);
+
+ if (dist < min) min = dist;
+ if (dist > max) max = dist;
+ }
+
+ // ---------- estimate spacing ----------
+ var spacing = (max - min) / (items.Count - 1);
+ if (Math.Abs(spacing) < 1e-6)
+ return items.OrderBy(instanceSelector).ToList();
+
+ var result = new T[items.Count];
+
+ // ---------- O(n) placement ----------
+ foreach (var p in projections)
+ {
+ var index = (int)Math.Round((p.dist - min) / spacing);
+
+ index = Math.Clamp(index, 0, items.Count - 1);
+
+ // collision fallback(极少发生)
+ while (result[index] != null!)
+ index = Math.Min(index + 1, items.Count - 1);
+
+ result[index] = p.item;
+ }
+
+ return result.ToList();
+ }
+
+ private static double[]? Parse(string? value)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ return null;
+
+ var s = value.Split('\\');
+ var r = new double[s.Length];
+
+ for (int i = 0; i < s.Length; i++)
+ if (!double.TryParse(s[i], out r[i]))
+ return null;
+
+ return r;
+ }
+}
+
+#endregion
+
+public class MaskRegion
+{
+ public int X { get; }
+ public int Y { get; }
+ public int Width { get; }
+ public int Height { get; }
+ public MaskRegion(int x, int y, int width, int height)
+ {
+ if (width <= 0) throw new ArgumentOutOfRangeException(nameof(width));
+ if (height <= 0) throw new ArgumentOutOfRangeException(nameof(height));
+ X = x;
+ Y = y;
+ Width = width;
+ Height = height;
+ }
+}
+
+
+public sealed class DicomMaskOptions
+{
+ ///
+ /// 要处理的帧,null 或空表示全部帧。
+ ///
+ public IReadOnlyCollection? FrameIndices { get; init; }
+
+ ///
+ /// 灰度图像的遮挡像素值,默认 0。
+ /// 注意:这表示写入的原始像素值,不保证视觉上一定为黑色。
+ ///
+ //public ushort GrayscaleMaskValue { get; init; } = 0;
+
+ ///
+ /// 彩色图像的遮挡值,默认 [0,0,0]。
+ /// 对 RGB 表示黑色;对 YBR 则表示三个分量都写 0。
+ ///
+ public byte[] ColorMaskValue { get; init; } = new byte[] { 0, 0, 0 };
+
+ ///
+ /// 严格保持原始 TransferSyntax。
+ /// 如果无法重新编码到原语法,则抛异常。
+ ///
+ public bool StrictKeepTransferSyntax { get; init; } = true;
+
+ ///
+ /// 是否更新 BurnedInAnnotation 为 NO。
+ ///
+ public bool UpdateBurnedInAnnotationToNo { get; init; } = false;
+
+
+ ///
+ /// 是否启用自动灰度遮挡值。
+ /// true 时,根据 PhotometricInterpretation + BitsStored + PixelRepresentation 自动选择更合理的值。
+ ///
+ public bool AutoSelectGrayscaleMaskValue { get; init; } = true;
+
+ ///
+ /// 当 AutoSelectGrayscaleMaskValue=false 时,使用该值。
+ /// 当为 null 且 AutoSelectGrayscaleMaskValue=false 时,默认 0。
+ /// 灰度图像的遮挡像素值,默认 0。
+ /// 注意:这表示写入的原始像素值,不保证视觉上一定为黑色。
+ ///
+ public int? GrayscaleMaskValue { get; init; }
+
+ ///
+ /// PALETTE COLOR 图像使用的遮盖索引。
+ /// null 表示自动从调色板中选择最暗的一个索引。
+ ///
+ public int? PaletteColorMaskIndex { get; init; }
+}
+
+
+
+public static class DicomPixelMasker
+{
+ public static async Task MaskAsync(
+ Stream input,
+ IEnumerable regions,
+ DicomMaskOptions? options = null)
+ {
+ var output = new MemoryStream();
+ await MaskAsync(input, output, regions, options).ConfigureAwait(false);
+ output.Position = 0;
+ return output;
+ }
+
+ public static async Task MaskAsync(
+ Stream input,
+ Stream output,
+ IEnumerable regions,
+ DicomMaskOptions? options = null,
+ CancellationToken cancellationToken = default)
+ {
+ if (input == null) throw new ArgumentNullException(nameof(input));
+ if (output == null) throw new ArgumentNullException(nameof(output));
+ var regionList = regions?.ToList() ?? throw new ArgumentNullException(nameof(regions));
+ if (regionList.Count == 0)
+ throw new ArgumentException("At least one mask region is required.", nameof(regions));
+ options ??= new DicomMaskOptions();
+ if (input.CanSeek)
+ input.Position = 0;
+ var originalFile = await DicomFile.OpenAsync(input, FileReadOption.ReadAll).ConfigureAwait(false);
+ var originalDataset = originalFile.Dataset;
+ ValidateDataset(originalDataset);
+ var originalTs = originalFile.FileMetaInfo.TransferSyntax;
+ // 先解压到工作格式
+ var workingFile = await EnsureUncompressedAsync(originalFile, cancellationToken).ConfigureAwait(false);
+ var workingDataset = workingFile.Dataset;
+
+ var originalPhotometric = originalDataset.GetSingleValueOrDefault(DicomTag.PhotometricInterpretation, string.Empty);
+
+ Console.WriteLine($"Original Photometric={originalPhotometric}, Original TS={originalTs.UID.UID}");
+
+ var rows = workingDataset.GetSingleValue(DicomTag.Rows);
+ var cols = workingDataset.GetSingleValue(DicomTag.Columns);
+ var bitsAllocated = workingDataset.GetSingleValue(DicomTag.BitsAllocated);
+ var bitsStored = workingDataset.GetSingleValueOrDefault(DicomTag.BitsStored, bitsAllocated);
+ var samplesPerPixel = workingDataset.GetSingleValue(DicomTag.SamplesPerPixel);
+ var pixelRepresentation = workingDataset.GetSingleValueOrDefault(DicomTag.PixelRepresentation, (ushort)0);
+ var planarConfiguration = workingDataset.GetSingleValueOrDefault(DicomTag.PlanarConfiguration, (ushort)0);
+ var workingPhotometric = workingDataset.GetSingleValueOrDefault(DicomTag.PhotometricInterpretation, string.Empty);
+
+ Console.WriteLine($"Working Photometric={workingPhotometric}, Working TS={workingFile.FileMetaInfo.TransferSyntax.UID.UID}");
+
+ Console.WriteLine($"Working Rows={rows}, Cols={cols}, BitsAllocated={bitsAllocated}, BitsStored={bitsStored}, SamplesPerPixel={samplesPerPixel}, PixelRepresentation={pixelRepresentation}, PlanarConfiguration={planarConfiguration}, Photometric={workingPhotometric}, TransferSyntax={workingFile.FileMetaInfo.TransferSyntax.UID.UID}");
+ var isSupport = IsSupportedPhotometric(workingPhotometric, samplesPerPixel);
+ if (!isSupport)
+ {
+ throw new NotSupportedException(
+ $"Unsupported PhotometricInterpretation after decode: {workingPhotometric}, SamplesPerPixel={samplesPerPixel}");
+ }
+ // 修改 working dataset 的像素
+ MaskPixelDataInPlace(workingDataset, regionList, options);
+ if (options.UpdateBurnedInAnnotationToNo)
+ {
+ workingDataset.AddOrUpdate(DicomTag.BurnedInAnnotation, "NO");
+ }
+
+ //// 转 JPEG Baseline(最稳定)
+ //var transcoder = new DicomTranscoder(
+ // DicomTransferSyntax.ExplicitVRLittleEndian,
+ // DicomTransferSyntax.JPEGProcess1);
+
+ //var finalFile = await Task.Run(
+ // () => transcoder.Transcode(workingFile),
+ // cancellationToken).ConfigureAwait(false);
+
+ //if (output.CanSeek)
+ // output.SetLength(0);
+ //await finalFile.SaveAsync(output).ConfigureAwait(false);
+
+ // 不要把 original photometric 强行写回
+ var finalFile = await ReEncodeToOriginalTransferSyntaxAsync(
+ workingFile,
+ originalTs,
+ options.StrictKeepTransferSyntax,
+ cancellationToken).ConfigureAwait(false);
+ finalFile.FileMetaInfo.TransferSyntax = originalTs;
+ if (output.CanSeek)
+ output.SetLength(0);
+ await finalFile.SaveAsync(output).ConfigureAwait(false);
+ if (output.CanSeek)
+ output.Position = 0;
+
+
+ }
+
+ ///
+ /// 验证dicom tag
+ ///
+ ///
+ ///
+ private static void ValidateDataset(DicomDataset dataset)
+ {
+ if (!dataset.Contains(DicomTag.PixelData))
+ throw new NotSupportedException("DICOM dataset does not contain Pixel Data.");
+
+ _ = dataset.GetSingleValue(DicomTag.Rows);
+ _ = dataset.GetSingleValue(DicomTag.Columns);
+ _ = dataset.GetSingleValue(DicomTag.BitsAllocated);
+ _ = dataset.GetSingleValue(DicomTag.SamplesPerPixel);
+ }
+
+ private static bool IsSupportedPhotometric(string photometric, int samplesPerPixel)
+ {
+ if (samplesPerPixel == 1)
+ {
+ return string.Equals(photometric, "MONOCHROME1", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(photometric, "MONOCHROME2", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(photometric, "PALETTE COLOR", StringComparison.OrdinalIgnoreCase);
+ }
+
+ if (samplesPerPixel == 3)
+ {
+ return string.Equals(photometric, "RGB", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(photometric, "YBR_FULL", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(photometric, "YBR_FULL_422", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(photometric, "YBR_PARTIAL_422", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(photometric, "YBR_PARTIAL_420", StringComparison.OrdinalIgnoreCase);
+ }
+
+ return false;
+ }
+
+ ///
+ /// 转为工作用的未压缩 DICOM
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static async Task EnsureUncompressedAsync(DicomFile sourceFile, CancellationToken cancellationToken)
+ {
+ var ts = sourceFile.FileMetaInfo.TransferSyntax;
+
+ if (!ts.IsEncapsulated)
+ {
+ return new DicomFile(sourceFile.Dataset.Clone());
+ }
+
+ try
+ {
+ var transcoder = new DicomTranscoder(ts, DicomTransferSyntax.ExplicitVRLittleEndian);
+ var transcoded = await Task.Run(() => transcoder.Transcode(sourceFile), cancellationToken).ConfigureAwait(false);
+ return transcoded;
+ }
+ catch (Exception ex)
+ {
+ throw new InvalidOperationException(
+ $"Failed to decode compressed DICOM from TransferSyntax {ts.UID.UID}. " +
+ "Please ensure fo-dicom codecs are available in the current Linux runtime.",
+ ex);
+ }
+ }
+
+ ///
+ /// 转回原始 TransferSyntax
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static async Task ReEncodeToOriginalTransferSyntaxAsync(
+ DicomFile workingFile,
+ DicomTransferSyntax originalTransferSyntax,
+ bool strictKeepTransferSyntax,
+ CancellationToken cancellationToken)
+ {
+ if (workingFile.FileMetaInfo.TransferSyntax == originalTransferSyntax)
+ return workingFile;
+ try
+ {
+ var currentTs = workingFile.FileMetaInfo.TransferSyntax;
+ var transcoder = new DicomTranscoder(currentTs, originalTransferSyntax);
+ return await Task.Run(() => transcoder.Transcode(workingFile), cancellationToken).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ if (strictKeepTransferSyntax)
+ {
+ throw new InvalidOperationException(
+ $"Failed to re-encode DICOM back to original TransferSyntax {originalTransferSyntax.UID.UID}.",
+ ex);
+ }
+ return workingFile;
+ }
+ }
+
+ ///
+ /// 修改像素
+ ///
+ ///
+ ///
+ ///
+ private static void MaskPixelDataInPlace(
+ DicomDataset dataset,
+ IReadOnlyList regions,
+ DicomMaskOptions options)
+ {
+ var rows = dataset.GetSingleValue(DicomTag.Rows);
+ var cols = dataset.GetSingleValue(DicomTag.Columns);
+ var bitsAllocated = dataset.GetSingleValue(DicomTag.BitsAllocated);
+ var bitsStored = dataset.GetSingleValueOrDefault(DicomTag.BitsStored, bitsAllocated);
+ var samplesPerPixel = dataset.GetSingleValue(DicomTag.SamplesPerPixel);
+ var pixelRepresentation = dataset.GetSingleValueOrDefault(DicomTag.PixelRepresentation, (ushort)0);
+ var photometric = dataset.GetSingleValueOrDefault(DicomTag.PhotometricInterpretation, string.Empty);
+ var planarConfiguration = dataset.GetSingleValueOrDefault(DicomTag.PlanarConfiguration, (ushort)0);
+ var pixelData = DicomPixelData.Create(dataset, false);
+ var frameCount = pixelData.NumberOfFrames;
+ var framesToProcess = ResolveFrames(frameCount, options.FrameIndices);
+ var replacementFrames = new List(frameCount);
+ for (int frameIndex = 0; frameIndex < frameCount; frameIndex++)
+ {
+ var frame = pixelData.GetFrame(frameIndex);
+ var buffer = frame.Data;
+ var bytes = buffer.ToArray();
+ if (framesToProcess.Contains(frameIndex))
+ {
+ ApplyMaskToFrame(
+ frameData: bytes,
+ rows: rows,
+ cols: cols,
+ bitsAllocated: bitsAllocated,
+ bitsStored: bitsStored,
+ samplesPerPixel: samplesPerPixel,
+ pixelRepresentation: pixelRepresentation,
+ photometric: photometric,
+ planarConfiguration: planarConfiguration,
+ regions: regions,
+ options: options, dataset);
+ }
+ replacementFrames.Add(new MemoryByteBuffer(bytes));
+ }
+ ReplacePixelDataFrames(dataset, pixelData, replacementFrames);
+ }
+
+ private static HashSet ResolveFrames(int frameCount, IReadOnlyCollection? frameIndices)
+ {
+ if (frameIndices == null || frameIndices.Count == 0)
+ return Enumerable.Range(0, frameCount).ToHashSet();
+ var result = new HashSet();
+ foreach (var index in frameIndices)
+ {
+ if (index < 0 || index >= frameCount)
+ throw new ArgumentOutOfRangeException(nameof(frameIndices), $"Frame index {index} out of range [0, {frameCount - 1}]");
+ result.Add(index);
+ }
+ return result;
+ }
+ private static void ReplacePixelDataFrames(
+ DicomDataset dataset,
+ DicomPixelData sourcePixelData,
+ IReadOnlyList frames)
+ {
+ //dataset.Remove(DicomTag.PixelData);
+ //var newPixelData = DicomPixelData.Create(dataset, true);
+ ////newPixelData.BitsAllocated = sourcePixelData.BitsAllocated;
+ //newPixelData.BitsStored = sourcePixelData.BitsStored;
+ //newPixelData.HighBit = sourcePixelData.HighBit;
+ //newPixelData.SamplesPerPixel = sourcePixelData.SamplesPerPixel;
+ //newPixelData.PixelRepresentation = sourcePixelData.PixelRepresentation;
+ //newPixelData.PlanarConfiguration = sourcePixelData.PlanarConfiguration;
+ //newPixelData.Height = sourcePixelData.Height;
+ //newPixelData.Width = sourcePixelData.Width;
+ //newPixelData.PhotometricInterpretation = sourcePixelData.PhotometricInterpretation;
+ //foreach (var frame in frames)
+ //{
+ // newPixelData.AddFrame(frame);
+ //}
+
+ dataset.Remove(DicomTag.PixelData);
+
+ var newPixelData = DicomPixelData.Create(dataset, true);
+
+ foreach (var frame in frames)
+ {
+ newPixelData.AddFrame(frame);
+ }
+ }
+
+ private static void ApplyMaskToFrame(
+ byte[] frameData,
+ int rows,
+ int cols,
+ int bitsAllocated,
+ int bitsStored,
+ int samplesPerPixel,
+ ushort pixelRepresentation,
+ string photometric,
+ ushort planarConfiguration,
+ IReadOnlyList regions,
+ DicomMaskOptions options,
+ DicomDataset dataset)
+ {
+ if (samplesPerPixel == 1)
+ {
+
+ if (string.Equals(photometric, "PALETTE COLOR", StringComparison.OrdinalIgnoreCase))
+ {
+ if (pixelRepresentation != 0)
+ {
+ throw new NotSupportedException("PALETTE COLOR with signed pixel representation is not supported.");
+ }
+ if (bitsAllocated == 8)
+ {
+ byte maskIndex = (byte)ResolvePaletteColorMaskIndex(dataset, bitsStored, options);
+ ApplyMask_SingleSample8(frameData, rows, cols, regions, maskIndex);
+ return;
+ }
+ if (bitsAllocated == 16)
+ {
+ ushort maskIndex = ResolvePaletteColorMaskIndex(dataset, bitsStored, options);
+ ApplyMask_SingleSample16(frameData, rows, cols, regions, maskIndex);
+ return;
+ }
+ throw new NotSupportedException(
+ $"Unsupported PALETTE COLOR image: BitsAllocated={bitsAllocated}");
+ }
+ else
+ {
+ if (bitsAllocated == 8)
+ {
+ byte maskValue8 = ResolveGrayscaleMaskValue8(photometric, options);
+ ApplyMask_SingleSample8(frameData, rows, cols, regions, maskValue8);
+ return;
+ }
+ if (bitsAllocated == 16)
+ {
+ ushort maskValue16 = ResolveGrayscaleMaskValue16(
+ photometric,
+ bitsStored,
+ pixelRepresentation,
+ options);
+ ApplyMask_SingleSample16(frameData, rows, cols, regions, maskValue16);
+ return;
+ }
+ throw new NotSupportedException(
+ $"Unsupported grayscale image: BitsAllocated={bitsAllocated}, Photometric={photometric}");
+ }
+
+
+ }
+ if (samplesPerPixel == 3)
+ {
+ if (bitsAllocated != 8)
+ {
+ throw new NotSupportedException(
+ $"Unsupported color image: SamplesPerPixel=3, BitsAllocated={bitsAllocated}, Photometric={photometric}");
+ }
+ int expectedFull = rows * cols * 3;
+ int expected422 = rows * cols * 2;
+ int chromaRows = (rows + 1) / 2;
+ int chromaCols = (cols + 1) / 2;
+ int expected420 = rows * cols + 2 * chromaRows * chromaCols;
+ // 根据 photometric 自动解析默认颜色
+ var colorMask = ResolveColorMaskValue(photometric, options);
+ // 1) 先处理能通过长度明确识别的 subsampled YBR
+ if (frameData.Length == expected422)
+ {
+ if (string.Equals(photometric, "YBR_FULL_422", StringComparison.OrdinalIgnoreCase))
+ {
+ ApplyMask_YbrFull422(frameData, rows, cols, regions);
+ return;
+ }
+ if (string.Equals(photometric, "YBR_PARTIAL_422", StringComparison.OrdinalIgnoreCase))
+ {
+ ApplyMask_YbrPartial422(frameData, rows, cols, regions);
+ return;
+ }
+ throw new NotSupportedException(
+ $"Frame length matches 4:2:2 layout, but Photometric={photometric} is not supported for 422 masking.");
+ }
+ if (frameData.Length == expected420)
+ {
+ if (string.Equals(photometric, "YBR_PARTIAL_420", StringComparison.OrdinalIgnoreCase))
+ {
+ ApplyMask_YbrPartial420(frameData, rows, cols, regions);
+ return;
+ }
+ throw new NotSupportedException(
+ $"Frame length matches 4:2:0 layout, but Photometric={photometric} is not supported for 420 masking.");
+ }
+ // 2) full-resolution 三通道数据,长度无法区分 planar / interleaved
+ // 必须依赖 PlanarConfiguration
+ if (frameData.Length == expectedFull)
+ {
+ if (planarConfiguration == 1)
+ {
+ ApplyMask_Color8_Planar(frameData, rows, cols, regions, colorMask);
+ return;
+ }
+ // 包括 planarConfiguration == 0 以及很多解码后默认输出
+ ApplyMask_Color8_Interleaved(frameData, rows, cols, regions, colorMask);
+ return;
+ }
+ // 3) 最后兜底:如果长度异常,尝试按 PlanarConfiguration 处理
+ if (planarConfiguration == 1)
+ {
+ ApplyMask_Color8_Planar(frameData, rows, cols, regions, colorMask);
+ return;
+ }
+ if (planarConfiguration == 0)
+ {
+ ApplyMask_Color8_Interleaved(frameData, rows, cols, regions, colorMask);
+ return;
+ }
+ throw new NotSupportedException(
+ $"Unsupported color frame layout: SamplesPerPixel={samplesPerPixel}, BitsAllocated={bitsAllocated}, " +
+ $"Photometric={photometric}, PlanarConfiguration={planarConfiguration}, FrameLength={frameData.Length}");
+ }
+ throw new NotSupportedException(
+ $"Unsupported format: SamplesPerPixel={samplesPerPixel}, BitsAllocated={bitsAllocated}, Photometric={photometric}");
+ }
+
+ private static byte[] ResolveColorMaskValue(string photometric, DicomMaskOptions options)
+ {
+ if (options.ColorMaskValue != null && options.ColorMaskValue.Length >= 3)
+ return options.ColorMaskValue;
+
+ if (string.Equals(photometric, "YBR_FULL", StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(photometric, "YBR_FULL_422", StringComparison.OrdinalIgnoreCase))
+ {
+ // 黑色 in YCbCr full range
+ return new byte[] { 0, 128, 128 };
+ }
+
+ if (string.Equals(photometric, "YBR_PARTIAL_422", StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(photometric, "YBR_PARTIAL_420", StringComparison.OrdinalIgnoreCase))
+ {
+ // 黑色 in YCbCr video range
+ return new byte[] { 16, 128, 128 };
+ }
+
+ // RGB 默认黑
+ return new byte[] { 0, 0, 0 };
+ }
+
+ #region ybr 422...
+ private static void ApplyMask_YbrFull422(
+ byte[] frameData,
+ int rows,
+ int cols,
+ IReadOnlyList regions)
+ {
+ const byte maskY = 0;
+ const byte maskCb = 128;
+ const byte maskCr = 128;
+
+ int bytesPerRow = cols * 2; // 2 pixels -> 4 bytes
+
+ foreach (var region in regions)
+ {
+ var (left, top, right, bottom) = ClipRegion(region, cols, rows);
+ if (left >= right || top >= bottom)
+ continue;
+
+ int alignedLeft = left & ~1;
+ int alignedRight = (right + 1) & ~1;
+ if (alignedRight > cols) alignedRight = cols;
+
+ for (int y = top; y < bottom; y++)
+ {
+ int rowOffset = y * bytesPerRow;
+
+ for (int x = alignedLeft; x < alignedRight; x += 2)
+ {
+ int offset = rowOffset + (x / 2) * 4;
+
+ // Y0 Cb Y1 Cr
+ frameData[offset + 0] = maskY;
+ frameData[offset + 1] = maskCb;
+ frameData[offset + 2] = maskY;
+ frameData[offset + 3] = maskCr;
+ }
+ }
+ }
+ }
+
+ private static void ApplyMask_YbrPartial422(
+ byte[] frameData,
+ int rows,
+ int cols,
+ IReadOnlyList regions)
+ {
+ const byte maskY = 16;
+ const byte maskCb = 128;
+ const byte maskCr = 128;
+
+ int bytesPerRow = cols * 2; // 2 pixels -> 4 bytes
+
+ foreach (var region in regions)
+ {
+ var (left, top, right, bottom) = ClipRegion(region, cols, rows);
+ if (left >= right || top >= bottom)
+ continue;
+
+ int alignedLeft = left & ~1;
+ int alignedRight = (right + 1) & ~1;
+ if (alignedRight > cols) alignedRight = cols;
+
+ for (int y = top; y < bottom; y++)
+ {
+ int rowOffset = y * bytesPerRow;
+
+ for (int x = alignedLeft; x < alignedRight; x += 2)
+ {
+ int offset = rowOffset + (x / 2) * 4;
+
+ // Y0 Cb Y1 Cr
+ frameData[offset + 0] = maskY;
+ frameData[offset + 1] = maskCb;
+ frameData[offset + 2] = maskY;
+ frameData[offset + 3] = maskCr;
+ }
+ }
+ }
+ }
+
+ private static void ApplyMask_YbrPartial420(
+ byte[] frameData,
+ int rows,
+ int cols,
+ IReadOnlyList regions)
+ {
+ const byte maskY = 16;
+ const byte maskCb = 128;
+ const byte maskCr = 128;
+
+ int yPlaneSize = rows * cols;
+ int chromaRows = (rows + 1) / 2;
+ int chromaCols = (cols + 1) / 2;
+ int chromaPlaneSize = chromaRows * chromaCols;
+
+ int cbBase = yPlaneSize;
+ int crBase = yPlaneSize + chromaPlaneSize;
+
+ foreach (var region in regions)
+ {
+ var (left, top, right, bottom) = ClipRegion(region, cols, rows);
+ if (left >= right || top >= bottom)
+ continue;
+
+ int alignedLeft = left & ~1;
+ int alignedTop = top & ~1;
+ int alignedRight = (right + 1) & ~1;
+ int alignedBottom = (bottom + 1) & ~1;
+
+ if (alignedRight > cols) alignedRight = cols;
+ if (alignedBottom > rows) alignedBottom = rows;
+
+ // Y plane
+ for (int y = top; y < bottom; y++)
+ {
+ int rowOffset = y * cols;
+ for (int x = left; x < right; x++)
+ {
+ frameData[rowOffset + x] = maskY;
+ }
+ }
+
+ // Cb / Cr plane, one chroma sample for each 2x2 block
+ for (int y = alignedTop; y < alignedBottom; y += 2)
+ {
+ int chromaY = y / 2;
+
+ for (int x = alignedLeft; x < alignedRight; x += 2)
+ {
+ int chromaX = x / 2;
+ int chromaIndex = chromaY * chromaCols + chromaX;
+
+ frameData[cbBase + chromaIndex] = maskCb;
+ frameData[crBase + chromaIndex] = maskCr;
+ }
+ }
+ }
+ }
+
+
+ private static bool TryApplyMaskForSubsampledYbr(
+ byte[] frameData,
+ int rows,
+ int cols,
+ string photometric,
+ IReadOnlyList regions)
+ {
+ if (string.IsNullOrWhiteSpace(photometric))
+ return false;
+
+ if (string.Equals(photometric, "YBR_FULL_422", StringComparison.OrdinalIgnoreCase))
+ {
+ ApplyMask_YbrFull422(frameData, rows, cols, regions);
+ return true;
+ }
+
+ if (string.Equals(photometric, "YBR_PARTIAL_422", StringComparison.OrdinalIgnoreCase))
+ {
+ ApplyMask_YbrPartial422(frameData, rows, cols, regions);
+ return true;
+ }
+
+ if (string.Equals(photometric, "YBR_PARTIAL_420", StringComparison.OrdinalIgnoreCase))
+ {
+ ApplyMask_YbrPartial420(frameData, rows, cols, regions);
+ return true;
+ }
+
+ return false;
+ }
+
+ #endregion
+
+ private static ushort ResolvePaletteColorMaskIndex(
+ DicomDataset dataset,
+ int bitsStored,
+ DicomMaskOptions options)
+ {
+ int pixelMin = 0;
+ int pixelMax = bitsStored == 16 ? ushort.MaxValue : ((1 << bitsStored) - 1);
+
+
+ if (options.PaletteColorMaskIndex.HasValue)
+ {
+ int manual = options.PaletteColorMaskIndex.Value;
+ if (manual < pixelMin || manual > pixelMax)
+ {
+ throw new ArgumentOutOfRangeException(
+ nameof(options.PaletteColorMaskIndex),
+ $"PaletteColorMaskIndex must be in range [{pixelMin}, {pixelMax}] for BitsStored={bitsStored}.");
+ }
+ return (ushort)manual;
+ }
+
+ // 2. 尝试从 Descriptor 读取 FirstMappedPixelValue
+ if (TryGetPaletteFirstMappedPixelValue(dataset, out int firstMapped))
+ {
+ if (firstMapped < pixelMin || firstMapped > pixelMax)
+ {
+ throw new InvalidDataException(
+ $"Palette LUT FirstMappedPixelValue={firstMapped} is out of pixel range [{pixelMin}, {pixelMax}].");
+ }
+ return (ushort)firstMapped;
+ }
+ // 3. 实在没有就退化为 0(可改成直接抛异常)
+ if (0 >= pixelMin && 0 <= pixelMax)
+ {
+ return 0;
+ }
+
+ throw new InvalidDataException(
+ "Cannot resolve palette color mask index because palette LUT descriptor is missing.");
+
+ //int autoIndex = FindDarkestPaletteIndex(dataset, maxValue);
+ //return (byte)autoIndex;
+ }
+
+ private static bool TryGetPaletteFirstMappedPixelValue(DicomDataset dataset, out int firstMappedPixelValue)
+ {
+ firstMappedPixelValue = 0;
+
+ if (TryGetPaletteDescriptor(dataset, DicomTag.RedPaletteColorLookupTableDescriptor, out var redDesc))
+ {
+ firstMappedPixelValue = redDesc.FirstMappedPixelValue;
+ return true;
+ }
+
+ if (TryGetPaletteDescriptor(dataset, DicomTag.GreenPaletteColorLookupTableDescriptor, out var greenDesc))
+ {
+ firstMappedPixelValue = greenDesc.FirstMappedPixelValue;
+ return true;
+ }
+
+ if (TryGetPaletteDescriptor(dataset, DicomTag.BluePaletteColorLookupTableDescriptor, out var blueDesc))
+ {
+ firstMappedPixelValue = blueDesc.FirstMappedPixelValue;
+ return true;
+ }
+
+ return false;
+ }
+
+ private sealed class PaletteLutDescriptor
+ {
+ public int NumberOfEntries { get; init; }
+ public int FirstMappedPixelValue { get; init; }
+ public int BitsPerEntry { get; init; }
+ }
+
+ private static bool TryGetPaletteDescriptor(
+ DicomDataset dataset,
+ DicomTag tag,
+ out PaletteLutDescriptor descriptor)
+ {
+ descriptor = null!;
+
+ if (!dataset.Contains(tag))
+ return false;
+
+ // LUT Descriptor 可能是 US 或 SS,fo-dicom 通常可直接按 short/ushort 读
+ if (dataset.TryGetValues(tag, out short[]? ssValues) && ssValues != null && ssValues.Length >= 3)
+ {
+ descriptor = new PaletteLutDescriptor
+ {
+ NumberOfEntries = ssValues[0] == 0 ? 65536 : ssValues[0],
+ FirstMappedPixelValue = ssValues[1],
+ BitsPerEntry = ssValues[2]
+ };
+ return true;
+ }
+
+ if (dataset.TryGetValues(tag, out ushort[]? usValues) && usValues != null && usValues.Length >= 3)
+ {
+ descriptor = new PaletteLutDescriptor
+ {
+ NumberOfEntries = usValues[0] == 0 ? 65536 : usValues[0],
+ FirstMappedPixelValue = usValues[1],
+ BitsPerEntry = usValues[2]
+ };
+ return true;
+ }
+
+ return false;
+ }
+
+
+
+ private static byte ResolveGrayscaleMaskValue8(string photometric, DicomMaskOptions options)
+ {
+ if (!options.AutoSelectGrayscaleMaskValue)
+ {
+ var value = options.GrayscaleMaskValue ?? 0;
+ return (byte)Math.Clamp(value, byte.MinValue, byte.MaxValue);
+ }
+ if (string.Equals(photometric, "MONOCHROME1", StringComparison.OrdinalIgnoreCase))
+ return byte.MaxValue;
+ return byte.MinValue;
+ }
+ private static ushort ResolveGrayscaleMaskValue16(
+ string photometric,
+ int bitsStored,
+ ushort pixelRepresentation,
+ DicomMaskOptions options)
+ {
+ if (!options.AutoSelectGrayscaleMaskValue)
+ {
+ var value = options.GrayscaleMaskValue ?? 0;
+ return unchecked((ushort)value);
+ }
+ bool isSigned = pixelRepresentation == 1;
+ bool mono1 = string.Equals(photometric, "MONOCHROME1", StringComparison.OrdinalIgnoreCase);
+ bitsStored = Math.Clamp(bitsStored, 1, 16);
+ if (!isSigned)
+ {
+ ushort min = 0;
+ ushort max = bitsStored == 16
+ ? ushort.MaxValue
+ : (ushort)((1 << bitsStored) - 1);
+ return mono1 ? max : min;
+ }
+ else
+ {
+ // signed range based on BitsStored
+ // minSigned = -(1 << (bitsStored - 1))
+ // maxSigned = (1 << (bitsStored - 1)) - 1
+ int minSigned = -(1 << (bitsStored - 1));
+ int maxSigned = (1 << (bitsStored - 1)) - 1;
+ short selected = mono1 ? (short)maxSigned : (short)minSigned;
+ return unchecked((ushort)selected);
+ }
+ }
+ private static void ApplyMask_SingleSample8(
+ byte[] data,
+ int rows,
+ int cols,
+ IReadOnlyList regions,
+ byte maskValue)
+ {
+ foreach (var region in regions)
+ {
+ var (left, top, right, bottom) = ClipRegion(region, cols, rows);
+ if (left >= right || top >= bottom) continue;
+ for (int y = top; y < bottom; y++)
+ {
+ int offset = y * cols;
+ for (int x = left; x < right; x++)
+ {
+ data[offset + x] = maskValue;
+ }
+ }
+ }
+ }
+ private static void ApplyMask_SingleSample16(
+ byte[] data,
+ int rows,
+ int cols,
+ IReadOnlyList regions,
+ ushort maskValue)
+ {
+ foreach (var region in regions)
+ {
+ var (left, top, right, bottom) = ClipRegion(region, cols, rows);
+ if (left >= right || top >= bottom) continue;
+ for (int y = top; y < bottom; y++)
+ {
+ int offset = y * cols;
+ for (int x = left; x < right; x++)
+ {
+ int pixelIndex = offset + x;
+ int byteIndex = pixelIndex * 2;
+ data[byteIndex] = (byte)(maskValue & 0xFF);
+ data[byteIndex + 1] = (byte)((maskValue >> 8) & 0xFF);
+ }
+ }
+ }
+ }
+ private static void ApplyMask_Color8_Interleaved(
+ byte[] data,
+ int rows,
+ int cols,
+ IReadOnlyList regions,
+ byte[] color)
+ {
+ if (color == null || color.Length < 3)
+ throw new ArgumentException("ColorMaskValue must contain at least 3 bytes.");
+ byte c0 = color[0];
+ byte c1 = color[1];
+ byte c2 = color[2];
+ foreach (var region in regions)
+ {
+ var (left, top, right, bottom) = ClipRegion(region, cols, rows);
+ if (left >= right || top >= bottom) continue;
+ for (int y = top; y < bottom; y++)
+ {
+ for (int x = left; x < right; x++)
+ {
+ int pixelIndex = y * cols + x;
+ int byteIndex = pixelIndex * 3;
+ data[byteIndex] = c0;
+ data[byteIndex + 1] = c1;
+ data[byteIndex + 2] = c2;
+ }
+ }
+ }
+ }
+ private static void ApplyMask_Color8_Planar(
+ byte[] data,
+ int rows,
+ int cols,
+ IReadOnlyList regions,
+ byte[] color)
+ {
+ if (color == null || color.Length < 3)
+ throw new ArgumentException("ColorMaskValue must contain at least 3 bytes.");
+ int planeSize = rows * cols;
+ byte c0 = color[0];
+ byte c1 = color[1];
+ byte c2 = color[2];
+ foreach (var region in regions)
+ {
+ var (left, top, right, bottom) = ClipRegion(region, cols, rows);
+ if (left >= right || top >= bottom) continue;
+ for (int y = top; y < bottom; y++)
+ {
+ int rowOffset = y * cols;
+ for (int x = left; x < right; x++)
+ {
+ int idx = rowOffset + x;
+ data[idx] = c0;
+ data[planeSize + idx] = c1;
+ data[2 * planeSize + idx] = c2;
+ }
+ }
+ }
+ }
+ private static (int left, int top, int right, int bottom) ClipRegion(
+ MaskRegion region,
+ int imageWidth,
+ int imageHeight)
+ {
+ int left = Math.Max(0, region.X);
+ int top = Math.Max(0, region.Y);
+ int right = Math.Min(imageWidth, region.X + region.Width);
+ int bottom = Math.Min(imageHeight, region.Y + region.Height);
+ return (left, top, right, bottom);
+ }
+}
+
+
+
+public static class DicomSortHelper
+{
+ ///
+ /// DICOM Slice 排序(IPP + IOP)
+ /// 自动 fallback InstanceNumber
+ ///
+ public static List SortSlices(
+ IEnumerable source,
+ Func ippSelector,
+ Func iopSelector,
+ Func instanceNumberSelector)
+ {
+ var list = source.ToList();
+ if (list.Count < 2)
+ return list;
+
+ var first = list[0];
+
+ var reference = ParseVector(ippSelector(first));
+ var iop = ParseVector(iopSelector(first));
+
+ // ===== fallback 条件 =====
+ if (reference == null || iop == null || iop.Length != 6)
+ return list.OrderBy(instanceNumberSelector).ToList();
+
+ // row / column direction
+ var row = new[] { iop[0], iop[1], iop[2] };
+ var col = new[] { iop[3], iop[4], iop[5] };
+
+ // normal = row × col
+ var normal = new[]
+ {
+ row[1]*col[2] - row[2]*col[1],
+ row[2]*col[0] - row[0]*col[2],
+ row[0]*col[1] - row[1]*col[0]
+ };
+
+ // 如果法向量异常 → fallback
+ if (IsZeroVector(normal))
+ return list.OrderBy(instanceNumberSelector).ToList();
+
+ // ===== 计算距离 =====
+ var sorted = list
+ .Select(item =>
+ {
+ var ipp = ParseVector(ippSelector(item));
+
+ if (ipp == null)
+ return (item, distance: double.MinValue);
+
+ var vec0 = reference[0] - ipp[0];
+ var vec1 = reference[1] - ipp[1];
+ var vec2 = reference[2] - ipp[2];
+
+ var distance =
+ vec0 * normal[0] +
+ vec1 * normal[1] +
+ vec2 * normal[2];
+
+ return (item, distance);
+ })
+ .OrderByDescending(x => x.distance)
+ .Select(x => x.item)
+ .ToList();
+
+ return sorted;
+ }
+
+ // ---------------- helpers ----------------
+
+ private static double[]? ParseVector(string? value)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ return null;
+
+ var parts = value.Split('\\');
+ var result = new double[parts.Length];
+
+ for (int i = 0; i < parts.Length; i++)
+ {
+ if (!double.TryParse(parts[i], out result[i]))
+ return null;
+ }
+
+ return result;
+ }
+
+ private static bool IsZeroVector(double[] v)
+ {
+ const double eps = 1e-6;
+ return Math.Abs(v[0]) < eps &&
+ Math.Abs(v[1]) < eps &&
+ Math.Abs(v[2]) < eps;
+ }
+}
+
+
diff --git a/IRaCIS.Core.Application/Helper/OtherTool/WeComNotifier.cs b/IRaCIS.Core.Application/Helper/OtherTool/WeComNotifier.cs
index 7af0b625c..c95bd63a9 100644
--- a/IRaCIS.Core.Application/Helper/OtherTool/WeComNotifier.cs
+++ b/IRaCIS.Core.Application/Helper/OtherTool/WeComNotifier.cs
@@ -1,13 +1,14 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
+using IdentityModel;
using Newtonsoft.Json;
using RestSharp;
+using System;
+using System.Collections.Generic;
+using System.Linq;
using System.Net;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading.Tasks;
using System.Threading.Tasks;
-using IdentityModel;
namespace IRaCIS.Core.Application.Helper.OtherTool;
@@ -31,6 +32,13 @@ public static class WeComNotifier
public static async Task SendAlertAsync(string webhook, WeComAlert alert)
{
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ await Task.CompletedTask;
+
+ return;
+ }
+
try
{
var client = new RestClient();
diff --git a/IRaCIS.Core.Application/IRaCIS.Core.Application.csproj b/IRaCIS.Core.Application/IRaCIS.Core.Application.csproj
index 913392d43..932767881 100644
--- a/IRaCIS.Core.Application/IRaCIS.Core.Application.csproj
+++ b/IRaCIS.Core.Application/IRaCIS.Core.Application.csproj
@@ -33,36 +33,36 @@
-
-
+
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
true
-
-
+
+
diff --git a/IRaCIS.Core.Application/IRaCIS.Core.Application.xml b/IRaCIS.Core.Application/IRaCIS.Core.Application.xml
index 1a9191539..22d7b2d86 100644
--- a/IRaCIS.Core.Application/IRaCIS.Core.Application.xml
+++ b/IRaCIS.Core.Application/IRaCIS.Core.Application.xml
@@ -363,7 +363,7 @@
访视读片任务
-
+
访视读片任务
@@ -559,119 +559,32 @@
系统模板文档配置表
-
+
- 数据字典-基础数据维护
+ 获取系统基础配置信息
-
-
-
- 数据字典-基础数据维护
-
-
-
-
- 添加bool
-
-
+
-
+
- 获取所有字典的Key
+ 更新系统基础配置
+
-
+
- 添加字典 的同时 一起添加子项 --New
+ 获取系统邮件配置信息
-
+
-
+
- New 查询条件
+ 更新系统邮件配置
-
-
-
-
-
- 添加和编辑
-
-
-
-
-
-
- 获取子项数组
-
-
-
-
- 删除字典数据
-
-
-
- 传递父亲 code 字符串 数组 返回多个下拉框数据
-
-
-
-
-
-
- 根据父亲Code 获取单个下拉框数据
-
-
-
-
-
-
- 根据父亲字典分组 获取子项
-
-
-
-
-
-
- 获取标准字典
-
-
-
-
-
- 获取标准字典
-
-
-
-
-
- 获取标准指定字典
-
-
-
-
-
-
- 获取所有下拉框 枚举 bool 数据
-
-
-
-
-
- 获取是和否
-
-
-
-
-
-
- 获取审核状态
-
-
-
-
+
@@ -1441,6 +1354,121 @@
RSTEST 疗效评估全称
+
+
+ 数据字典-基础数据维护
+
+
+
+
+ 数据字典-基础数据维护
+
+
+
+
+ 添加bool
+
+
+
+
+
+
+ 获取所有字典的Key
+
+
+
+
+
+ 添加字典 的同时 一起添加子项 --New
+
+
+
+
+
+
+ New 查询条件
+
+
+
+
+
+
+ 添加和编辑
+
+
+
+
+
+
+ 获取子项数组
+
+
+
+
+ 删除字典数据
+
+
+
+ 传递父亲 code 字符串 数组 返回多个下拉框数据
+
+
+
+
+
+
+ 根据父亲Code 获取单个下拉框数据
+
+
+
+
+
+
+ 根据父亲字典分组 获取子项
+
+
+
+
+
+
+ 获取标准字典
+
+
+
+
+
+ 获取标准字典
+
+
+
+
+
+ 获取标准指定字典
+
+
+
+
+
+
+ 获取所有下拉框 枚举 bool 数据
+
+
+
+
+
+ 获取是和否
+
+
+
+
+
+
+ 获取审核状态
+
+
+
+
+
+
邮件日志
@@ -1556,6 +1584,103 @@
+
+
+ 按照 subject visit studyCode 三个维度进行分组的查询列表 (subject相关)
+
+
+
+
+
+
+ 上传记录表--里面包含待同步任务 DataFileType= 0 :代表系统文件 1:Subject相关 2:项目相关,但是和subject 没关系
+
+
+
+
+
+
+ 任务具体执行记录表
+
+
+
+
+
+
+ 批量设置为需要同步,并且设置优先级
+
+
+
+
+
+
+ 优先级队列(仅负责排序)
+
+
+
+
+ 当前等待中的任务(唯一真实数据)
+ key = Guid
+ value = 最新 priority
+
+
+
+
+ 正在执行的任务(防止重复执行)
+
+
+
+
+ worker 等待信号
+
+
+
+
+ 入队(同 Guid 会覆盖优先级)
+
+
+
+
+ 获取一个待执行任务(无任务时自动等待)
+
+
+
+
+ 任务执行完成(必须调用)
+
+
+
+
+ 当前等待中的任务快照
+
+
+
+
+ 入队任务
+
+
+
+
+ Worker 等待并获取任务
+
+
+
+
+ 当前排队数量(调试用)
+
+
+
+
+ 同步调度器
+
+
+
+
+ 如果没有任务 → 挂起等待 有任务 → 被唤醒并返回
+
+
+
+
InternationalizationService
@@ -1599,6 +1724,17 @@
+
+
+ 获取邮件主题和html 通用封装 以用户语言为主,没有的就用当前的请求语言_userInfo.IsEn_Us
+
+
+
+
+
+
+
+
Reviewer简历录入 发送验证码
@@ -2232,7 +2368,7 @@
-
+
获取入组结果
@@ -2240,9 +2376,10 @@
+
-
+
获取PD 结果
@@ -2250,6 +2387,7 @@
任务类型
标准类型
是否是全局产生(区分裁判任务)
+
@@ -2814,6 +2952,13 @@
+
+
+ 获取未阅片完成的访视,方便前端调用下载
+
+
+
+
批量勾选访视 进行下载
@@ -2868,6 +3013,40 @@
+
+
+ 标注遮盖影像 路径后面加了.MaskImage 就是遮盖的新路径
+
+
+
+
+
+ 撤销遮盖的影像,可以单张,也可以整个序列
+
+
+
+
+
+
+ 获取患者基本信息
+
+
+
+
+
+
+ 编辑患者基本信息
+
+
+
+
+
+
+ 修正患者基本信息
+
+
+
+
指定资源Id,渲染Dicom检查的Jpeg预览图像
Dicom检查的Id
@@ -3399,6 +3578,13 @@
UserFeedBackService
+
+
+ 设置用户反馈状态 并且可以提供理由
+
+
+
+
批量更新状态
@@ -5794,6 +5980,82 @@
+
+
+ 阅片导入
+
+
+
+
+
+ 获取阅片的计算数据
+
+
+
+
+
+
+ 删除病灶获取起始病灶序号
+
+
+
+
+
+ 获取阅片报告
+
+
+
+
+
+
+ 将上一次的访视病灶添加到这一次
+
+
+
+
+
+
+ 测试计算
+
+
+
+
+
+
+
+ 计算任务
+
+
+
+
+
+
+ 自动计算
+
+
+
+
+
+
+
+ 获取脂肪分数平均值
+
+
+
+
+
+
+ 获取脂肪肝分级
+
+
+
+
+
+
+ 计算平均值
+
+
+
阅片导入
@@ -7534,6 +7796,13 @@
影像阅片临床数据签名
+
+
+ 修改临床数据后 将签名状态变更为未签名
+
+
+
+
获取访视临床数据名称
@@ -9489,6 +9758,36 @@
单位
+
+
+ 数值类型
+
+
+
+
+ 自定义单位
+
+
+
+
+ 类型
+
+
+
+
+ 数值类型
+
+
+
+
+ 自定义单位
+
+
+
+
+ 类型
+
+
保存表格问题标记
@@ -10124,6 +10423,16 @@
是否是转变的任务(转为IRECIST)
+
+
+ 定圆工具默认半径
+
+
+
+
+ 默认SegmentName
+
+
返回对象
@@ -10889,6 +11198,21 @@
IR阅片页面是否可以查看既往任务结果
+
+
+ 是否关闭
+
+
+
+
+ 医学审核对话关闭原因
+
+
+
+
+ 对话关闭原因
+
+
trials:medicalFeedback:message:msg1
@@ -11634,6 +11958,11 @@
影像标记
+
+
+ 影像标记类型
+
+
是否预设
@@ -12040,6 +12369,11 @@
影像标记
+
+
+ 影像标记类型
+
+
影像工具
@@ -12980,6 +13314,11 @@
影像标记
+
+
+ 影像标记类型
+
+
影像工具
@@ -14249,6 +14588,20 @@
+
+
+ 根据项目问题id获取项目问题信息
+
+
+
+
+
+
+ 根据项目表格问题id获取项目表格问题信息
+
+
+
+
系统标准阅片关键点文件服务
@@ -14381,7 +14734,7 @@
-
+
IR影像阅片
@@ -15159,6 +15512,142 @@
+
+
+ 分割
+
+
+
+
+
+
+
+
+ 分割
+
+
+
+
+
+
+
+
+ 获取分割组
+
+
+
+
+
+
+ 新增修改分割组
+
+
+
+
+
+
+ 添加新版本
+
+
+
+
+
+
+ 获取分割组历史版本
+
+
+
+
+
+
+ 恢复分割组历史版本
+
+
+
+
+
+
+ 删除分割组
+
+
+
+
+
+
+ 修改分割组的保存状态
+
+
+
+
+
+
+ 获取分割
+
+
+
+
+
+
+ 新增修改分割
+
+
+
+
+
+
+ 删除分割
+
+
+
+
+
+
+ 锁定解锁分割
+
+
+
+
+
+
+ 获取分割绑定
+
+
+
+
+
+
+ 新增修改分割绑定
+
+
+
+
+
+
+ 删除分割
+
+
+
+
+
+
+ 保存分割绑定和答案
+
+
+
+
+
+
+ 删除分割组和分割时的关联数据删除逻辑
+
+
+
+
+
+
+
+ 递归清除依赖此问题的计算问题答案
+
+
快捷键服务
@@ -15200,7 +15689,7 @@
用户WL模板
-
+
用户WL模板
@@ -15226,7 +15715,7 @@
-
+
获取自动切换任务配置
@@ -15495,7 +15984,7 @@
-
+
提交 患者检查和访视的绑定
@@ -15653,6 +16142,32 @@
+
+
+ 维护中心调研设备默认配置
+
+
+
+
+
+
+ 遮挡影像
+
+
+
+
+
+ 设置生命周期
+
+
+
+
+
+
+ 归档直读
+
+
+
测试疗效评估
@@ -15836,6 +16351,12 @@
默认是添加/更新
+
+
+ 将某个路径下的归档的文件 转为标准存储
+
+
+
坑方法,会清空之前的规则
@@ -15845,7 +16366,7 @@
-
+
oosFolderPath 不要 "/ "开头 应该: TempFolder/ChildFolder
@@ -15853,9 +16374,10 @@
+ 只用赋值业务参数Id 和批次信息即可,其他信息不用传递
-
+
oosFolderPath 不要 "/ "开头 应该: TempFolder/ChildFolder
@@ -15887,6 +16409,89 @@
+
+
+ 要处理的帧,null 或空表示全部帧。
+
+
+
+
+ 彩色图像的遮挡值,默认 [0,0,0]。
+ 对 RGB 表示黑色;对 YBR 则表示三个分量都写 0。
+
+
+
+
+ 严格保持原始 TransferSyntax。
+ 如果无法重新编码到原语法,则抛异常。
+
+
+
+
+ 是否更新 BurnedInAnnotation 为 NO。
+
+
+
+
+ 是否启用自动灰度遮挡值。
+ true 时,根据 PhotometricInterpretation + BitsStored + PixelRepresentation 自动选择更合理的值。
+
+
+
+
+ 当 AutoSelectGrayscaleMaskValue=false 时,使用该值。
+ 当为 null 且 AutoSelectGrayscaleMaskValue=false 时,默认 0。
+ 灰度图像的遮挡像素值,默认 0。
+ 注意:这表示写入的原始像素值,不保证视觉上一定为黑色。
+
+
+
+
+ PALETTE COLOR 图像使用的遮盖索引。
+ null 表示自动从调色板中选择最暗的一个索引。
+
+
+
+
+ 验证dicom tag
+
+
+
+
+
+
+ 转为工作用的未压缩 DICOM
+
+
+
+
+
+
+
+
+ 转回原始 TransferSyntax
+
+
+
+
+
+
+
+
+
+
+ 修改像素
+
+
+
+
+
+
+
+ DICOM Slice 排序(IPP + IOP)
+ 自动 fallback InstanceNumber
+
+
github 链接:https://github.com/lanceliao/china-holiday-calender?tab=readme-ov-file
@@ -15898,23 +16503,25 @@
全量一致性核查
-
+
系统默认邮件 + 项目默认邮件 (不用添加到项目邮件配置中,才发送)
+
-
+
项目手动邮件 (需要添加到项目邮件配置中,才发送)
+
@@ -16285,12 +16892,12 @@
定时过期提醒
-
+
生效通知
-
+
生效通知
@@ -16897,12 +17504,22 @@
UserFeedBackView 列表视图模型
+
+
+ 原因
+
+
UserFeedBackQuery 列表查询参数模型
UserFeedBackAddOrEdit 列表查询参数模型
+
+
+ 原因
+
+
UserLogView 列表视图模型
@@ -17660,6 +18277,56 @@
任务类型
+
+
+ 是否保存
+
+
+
+
+ 是否保存
+
+
+
+
+ 文件大小,单位字节
+
+
+
+
+ SEGUpdateTime 更新时间
+
+
+
+
+ 版本开始时间
+
+
+
+
+ 是否锁定
+
+
+
+
+ 分割分组名称
+
+
+
+
+ SegmentName
+
+
+
+
+ 是否锁定
+
+
+
+
+ 分割的Json
+
+
UserWLTemplateView 列表视图模型
@@ -17978,6 +18645,116 @@
ISystemDocumentService
+
+
+ 体重
+
+
+
+
+ 总剂量
+
+
+
+
+ 半衰期
+
+
+
+
+ 注射时间
+
+
+
+
+ 成像 / 采集时间
+
+
+
+
+ 是否存在空字符串字段(PatientSex、PatientWeight、RadionuclideTotalDose、RadionuclideHalfLife、RadiopharmaceuticalStartTime、AcquisitionTime 任意一个为空/空字符串)
+
+
+
+
+ 性别
+
+
+
+
+ 体重
+
+
+
+
+ 总剂量
+
+
+
+
+ 半衰期
+
+
+
+
+ 注射时间
+
+
+
+
+ 成像 / 采集时间
+
+
+
+
+ 修改原因
+
+
+
+
+ 性别
+
+
+
+
+ 体重
+
+
+
+
+ 总剂量
+
+
+
+
+ 半衰期
+
+
+
+
+ 注射时间
+
+
+
+
+ 成像 / 采集时间
+
+
+
+
+ 总剂量
+
+
+
+
+ 半衰期
+
+
+
+
+ 注射时间
+
+
NoneDicomStudyService
@@ -18156,6 +18933,46 @@
受试者ID
+
+
+ QC质控下载
+
+
+
+
+ 性别
+
+
+
+
+ 体重
+
+
+
+
+ 总剂量
+
+
+
+
+ 半衰期
+
+
+
+
+ 注射时间
+
+
+
+
+ 成像 / 采集时间
+
+
+
+
+ 是否存在空字符串字段(PatientSex、PatientWeight、RadionuclideTotalDose、RadionuclideHalfLife、RadiopharmaceuticalStartTime、AcquisitionTime 任意一个为空/空字符串)
+
+
关闭一致性质疑Dto
@@ -18581,6 +19398,16 @@
检查名称列表
+
+
+ QC质控下载
+
+
+
+
+ 打开失访可读
+
+
影像质控风险控制
@@ -18691,11 +19518,26 @@
文件路径
+
+
+ 定圆工具默认半径
+
+
+
+
+ 默认SegmentName
+
+
阅片工具
+
+
+ 分割工具
+
+
项目ID
@@ -18956,6 +19798,21 @@
表单类型
+
+
+ 默认SegmentName
+
+
+
+
+ 定圆工具默认半径
+
+
+
+
+ 分割工具
+
+
项目标准ID
@@ -19056,6 +19913,41 @@
影像质控风险控制
+
+
+ 性别
+
+
+
+
+ 体重
+
+
+
+
+ 总剂量
+
+
+
+
+ 半衰期
+
+
+
+
+ 注射时间
+
+
+
+
+ 成像 / 采集时间
+
+
+
+
+ 是否存在空字符串字段(PatientSex、PatientWeight、RadionuclideTotalDose、RadionuclideHalfLife、RadiopharmaceuticalStartTime、AcquisitionTime 任意一个为空/空字符串)
+
+
SystemBasicDataService
@@ -19148,7 +20040,7 @@
TrialDocumentService
-
+
TrialDocumentService
diff --git a/IRaCIS.Core.Application/MassTransit/Command/ConsistenCheckCommand.cs b/IRaCIS.Core.Application/MassTransit/Command/ConsistenCheckCommand.cs
index 899ce5d77..0860bfa4b 100644
--- a/IRaCIS.Core.Application/MassTransit/Command/ConsistenCheckCommand.cs
+++ b/IRaCIS.Core.Application/MassTransit/Command/ConsistenCheckCommand.cs
@@ -34,6 +34,8 @@ namespace IRaCIS.Core.Application.MassTransit.Command
public Guid SubjectVisitId { get; set; }
public Guid StudyId { get; set; }
+
+ public string SiteCounty { get; set; }
}
public class FullCheckResult: CheckViewModel
diff --git a/IRaCIS.Core.Application/MassTransit/Consumer/CommonEmailHelper.cs b/IRaCIS.Core.Application/MassTransit/Consumer/CommonEmailHelper.cs
index f02d835e0..f628bb5ea 100644
--- a/IRaCIS.Core.Application/MassTransit/Consumer/CommonEmailHelper.cs
+++ b/IRaCIS.Core.Application/MassTransit/Consumer/CommonEmailHelper.cs
@@ -21,12 +21,14 @@ public static class CommonEmailHelper
///
///
///
+ ///
///
///
public static async Task GetEmailSubejctAndHtmlInfoAndBuildAsync(EmailNoticeConfig configInfo, MimeMessage messageToSend,
-Func<(string topicStr, string htmlBodyStr), (string topicStr, string htmlBodyStr)> emailFunc)
+Func<(string topicStr, string htmlBodyStr), (string topicStr, string htmlBodyStr)> emailFunc, UserWorkLanguage? userWorkLanguage = null)
{
- var isEn_US = CultureInfo.CurrentCulture.Name == StaticData.CultureInfo.en_US;
+
+ bool isEn_US = userWorkLanguage.HasValue ? userWorkLanguage.Value == UserWorkLanguage.US : CultureInfo.CurrentCulture.Name == StaticData.CultureInfo.en_US;
var (topicStr, htmlBodyStr) = isEn_US ? (configInfo.EmailTopic, configInfo.EmailHtmlContent) : (configInfo.EmailTopicCN, configInfo.EmailHtmlContentCN);
@@ -60,13 +62,14 @@ Func<(string topicStr, string htmlBodyStr), (string topicStr, string htmlBodyStr
///
///
///
+ ///
///
///
public static async Task GetTrialEmailSubejctAndHtmlInfoAndBuildAsync(TrialEmailNoticeConfig configInfo, MimeMessage messageToSend,
- Func<(string topicStr, string htmlBodyStr), (string topicStr, string htmlBodyStr)> emailFunc)
+ Func<(string topicStr, string htmlBodyStr), (string topicStr, string htmlBodyStr)> emailFunc, UserWorkLanguage? userWorkLanguage = null)
{
- var isEn_US = CultureInfo.CurrentCulture.Name == StaticData.CultureInfo.en_US;
+ bool isEn_US = userWorkLanguage.HasValue ? userWorkLanguage.Value == UserWorkLanguage.US : CultureInfo.CurrentCulture.Name == StaticData.CultureInfo.en_US;
var (topicStr, htmlBodyStr) = isEn_US ? (configInfo.EmailTopic, configInfo.EmailHtmlContent) : (configInfo.EmailTopicCN, configInfo.EmailHtmlContentCN);
@@ -94,12 +97,20 @@ Func<(string topicStr, string htmlBodyStr), (string topicStr, string htmlBodyStr
return configInfo;
}
-
-
-
- public static string ReplaceCompanyName(SystemEmailSendConfig _systemEmailConfig, string needDealtxt)
+ public static string ReplacePlatformName(SystemEmailSendConfig _systemEmailConfig, string needDealtxt, UserWorkLanguage? userWorkLanguage = null)
{
- var isEn_US = CultureInfo.CurrentCulture.Name == StaticData.CultureInfo.en_US;
+ var isEn_US = userWorkLanguage.HasValue ? userWorkLanguage.Value == UserWorkLanguage.US : CultureInfo.CurrentCulture.Name == StaticData.CultureInfo.en_US;
+
+ var platformName = isEn_US ? _systemEmailConfig.PlatformName : _systemEmailConfig.PlatformNameCN;
+
+ var str = needDealtxt.Replace("{platformName}", platformName);
+ return str;
+ }
+
+
+ public static string ReplaceCompanyName(SystemEmailSendConfig _systemEmailConfig, string needDealtxt, UserWorkLanguage? userWorkLanguage = null)
+ {
+ var isEn_US = userWorkLanguage.HasValue ? userWorkLanguage.Value == UserWorkLanguage.US : CultureInfo.CurrentCulture.Name == StaticData.CultureInfo.en_US;
var str = needDealtxt.Replace("{company}", isEn_US ? _systemEmailConfig.CompanyName : _systemEmailConfig.CompanyNameCN)
.Replace("{company abbreviation}", isEn_US ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN);
diff --git a/IRaCIS.Core.Application/MassTransit/Consumer/ConsistencyCheckConsumer.cs b/IRaCIS.Core.Application/MassTransit/Consumer/ConsistencyCheckConsumer.cs
index 0038ac6aa..312ee375b 100644
--- a/IRaCIS.Core.Application/MassTransit/Consumer/ConsistencyCheckConsumer.cs
+++ b/IRaCIS.Core.Application/MassTransit/Consumer/ConsistencyCheckConsumer.cs
@@ -6,15 +6,18 @@ using IRaCIS.Core.Application.Contracts;
using IRaCIS.Core.Application.Contracts.DTO;
using IRaCIS.Core.Application.Helper;
using IRaCIS.Core.Application.MassTransit.Command;
+using IRaCIS.Core.Application.ViewModel;
using IRaCIS.Core.Domain.Models;
using IRaCIS.Core.Domain.Share;
using MassTransit;
+using MaxMind.GeoIP2.Model;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
+using System.Globalization;
using System.IO;
using System.Text;
@@ -78,6 +81,8 @@ namespace IRaCIS.Core.Application.MassTransit.Consumer
Modality = study.ModalityForEdit,
SubjectCode = subject.Code,
VisitName = sv.VisitName,
+
+ SiteCounty = sv.TrialSite.Country,
};
var noneDicomQuey = from sv in _subjectVisitRepository.Where(subjectVisitLambda)
@@ -92,6 +97,8 @@ namespace IRaCIS.Core.Application.MassTransit.Consumer
Modality = noneDicomStudy.Modality,
SubjectCode = subject.Code,
VisitName = sv.VisitName,
+
+ SiteCounty = sv.TrialSite.Country,
};
var dbList = (await dicomQuery.ToListAsync()).Union(await noneDicomQuey.ToListAsync()).ToList();
@@ -119,6 +126,21 @@ namespace IRaCIS.Core.Application.MassTransit.Consumer
var dbVisitStudyList = dbList.Where(t => t.SubjectCode == sv.SubjectCode && t.SiteCode == sv.SiteCode && t.VisitName == sv.VisitName).ToList();
+ //设置当前中心语言
+
+ var isEn_us = dbVisitStudyList.First().SiteCounty == StaticData.SiteCountry.US;
+
+ var cultureInfoName = StaticData.CultureInfo.en_US;
+
+ if (isEn_us == false)
+ {
+ cultureInfoName = StaticData.CultureInfo.zh_CN;
+ }
+
+ CultureInfo.CurrentCulture = new CultureInfo(cultureInfoName);
+ CultureInfo.CurrentUICulture = new CultureInfo(cultureInfoName);
+
+
//找到etc 当前visit site 和subject 一致的检查列表
var etcVisitStudyList = etcList.Where(t => t.SubjectCode == sv.SubjectCode && t.SiteCode == sv.SiteCode && t.VisitName == sv.VisitName).ToList();
@@ -320,7 +342,7 @@ namespace IRaCIS.Core.Application.MassTransit.Consumer
join study in _studyRepository.AsQueryable() on sv.Id equals study.SubjectVisitId
select new CheckDBModel()
{
- SubjectStatus = sv.Subject.Status,
+ SubjectStatus = sv.Subject.Status,
SubjectVisitId = sv.Id,
SiteCode = sv.TrialSite.TrialSiteCode,
StudyDate = study.StudyTime == null ? string.Empty : ((DateTime)study.StudyTime).ToString("yyyy-MM-dd"),
@@ -403,7 +425,7 @@ namespace IRaCIS.Core.Application.MassTransit.Consumer
{
SubjectStatus = dbCurrentVisitFirst.SubjectStatus,
CheckTime = DateTime.Now,
- CheckState=CheckStateEnum.CVPassed,
+ CheckState = CheckStateEnum.CVPassed,
SiteCode = dbCurrentVisitFirst.SiteCode,
SubjectCode = dbCurrentVisitFirst.SubjectCode,
VisitName = dbCurrentVisitFirst.VisitName,
@@ -448,7 +470,7 @@ namespace IRaCIS.Core.Application.MassTransit.Consumer
var fileStreamResult = (FileStreamResult)await ExcelExportHelper.DataExportAsync(StaticData.Export.TrialConsistentFUllCheckList_Export, exportInfo, exportInfo.TrialCode, _commonDocumentRepository, _hostEnvironment, _dictionaryService, typeof(FullCheckResult));
- var ossRelativePath = await _oSSService.UploadToOSSAsync(fileStreamResult.FileStream, $"{trialId.ToString()}/InspectionUpload/DataReconciliation", "DataReconciliation");
+ var ossRelativePath = await _oSSService.UploadToOSSAsync(fileStreamResult.FileStream, $"{trialId.ToString()}/InspectionUpload/DataReconciliation", "DataReconciliation", uploadInfo: new FileUploadRecordAddOrEdit() { TrialId = trialId, BatchDataType = BatchDataType.DataReconciliation });
//var add = await _inspectionFileRepository.FindAsync(inspectionFileId);
diff --git a/IRaCIS.Core.Application/MassTransit/Consumer/Dto/SendImageReuploadEmailDto.cs b/IRaCIS.Core.Application/MassTransit/Consumer/Dto/SendImageReuploadEmailDto.cs
index 263f606e4..9fa2f0410 100644
--- a/IRaCIS.Core.Application/MassTransit/Consumer/Dto/SendImageReuploadEmailDto.cs
+++ b/IRaCIS.Core.Application/MassTransit/Consumer/Dto/SendImageReuploadEmailDto.cs
@@ -27,5 +27,16 @@ namespace IRaCIS.Core.Application.MassTransit.Consumer.Dto
+ }
+
+ public class SendEmailUserDto
+ {
+ public UserTypeEnum UserTypeEnum { get; set; }
+
+ public string FullName { get; set; }
+
+ public UserWorkLanguage UserWorkLanguage { get; set; }
+
+ public string EMail { get; set; }
}
}
diff --git a/IRaCIS.Core.Application/MassTransit/Consumer/ImageConsumer.cs b/IRaCIS.Core.Application/MassTransit/Consumer/ImageConsumer.cs
index f45d703d5..832e6b638 100644
--- a/IRaCIS.Core.Application/MassTransit/Consumer/ImageConsumer.cs
+++ b/IRaCIS.Core.Application/MassTransit/Consumer/ImageConsumer.cs
@@ -273,15 +273,30 @@ public class ImageConsumer(
};
// 根据不同场景获取不同角色的用户 先排除CRC和CRA
- var trialUserList = await _trialUseRoleRepository.Where(x => x.TrialId == inDto.TrialId && !x.TrialUser.IsDeleted && !filterUserTypeList.Contains(x.UserRole.UserTypeEnum) ).Include(x => x.UserRole).Select(x => x.UserRole).ToListAsync();
+ var trialUserList = await _trialUseRoleRepository.Where(x => x.TrialId == inDto.TrialId && !x.TrialUser.IsDeleted && !filterUserTypeList.Contains(x.UserRole.UserTypeEnum) ).Include(x => x.UserRole).Select(x => x.UserRole)
+ .Select(x => new SendEmailUserDto()
+ {
+ UserTypeEnum = x.UserTypeEnum,
+ FullName = x.FullName,
+ UserWorkLanguage = x.IdentityUser.UserWorkLanguage,
+ EMail=x.EMail,
+ })
+ .ToListAsync();
// CRC和CRA单独取
- var crcAndcraUserList = await _trialSiteRepository.Where(x => x.Id == inDto.SubjectVisit.TrialSiteId).SelectMany(x => x.CRCUserList.Select(y => y.UserRole)).ToListAsync();
+ var crcAndcraUserList = await _trialSiteRepository.Where(x => x.Id == inDto.SubjectVisit.TrialSiteId).SelectMany(x => x.CRCUserList.Select(y => y.UserRole))
+ .Select(x => new SendEmailUserDto()
+ {
+ UserTypeEnum = x.UserTypeEnum,
+ FullName = x.FullName,
+ UserWorkLanguage = x.IdentityUser.UserWorkLanguage,
+ EMail = x.EMail,
+ }).ToListAsync();
trialUserList.AddRange(crcAndcraUserList);
// 根据场景确定收件人
- List toUserList = new List();
- List ccUserList = new List();
+ List toUserList = new List();
+ List ccUserList = new List();
var emailNoticeUserList = await _emailNoticeUserTypeRepository.Where(x => x.EmailNoticeConfigId == inDto.EmailNoticeConfig.Id).ToListAsync();
var userTypeEnumList = emailNoticeUserList.Select(x => x.UserType).ToList();
@@ -334,40 +349,53 @@ public class ImageConsumer(
DictionaryList = dictionaryDtos
});
- foreach (var userinfo in toUserList)
+
+ var userWorkLanguageList = toUserList.Select(x => x.UserWorkLanguage).Distinct().ToList();
+
+ foreach (var workLanguage in userWorkLanguageList)
{
+
+ var userinfoList = toUserList.Where(x => x.UserWorkLanguage == workLanguage).ToList();
var messageToSend = new MimeMessage();
// 发件地址
messageToSend.From.Add(new MailboxAddress(_systemEmailConfig.FromName, _systemEmailConfig.FromEmail));
- messageToSend.To.Add(new MailboxAddress(userinfo.FullName, userinfo.EMail));
+
+ foreach (var userinfo in userinfoList)
+ {
+ messageToSend.To.Add(new MailboxAddress(userinfo.FullName, userinfo.EMail));
+ }
+
// 添加抄送
foreach (var ccUser in ccUserList)
{
messageToSend.Cc.Add(new MailboxAddress(ccUser.FullName, ccUser.EMail));
}
+
+ var userNames = userinfoList.Select(x => x.FullName).ToList();
+
// 格式化邮件内容
Func<(string topicStr, string htmlBodyStr), (string topicStr, string htmlBodyStr)> emailConfigFunc = input =>
- {
- var subjectCode = inDto.SubjectVisit.Subject.Code;
- var topicStr = string.Format(input.topicStr, trialInfo.ResearchProgramNo, subjectCode, inDto.SubjectVisit.VisitName);
- var htmlBodyStr = string.Format(
- CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr),
- userinfo.FullName, // 用户名 {0}
- trialInfo.ExperimentName, // 项目 {1}
- subjectCode, // 受试者 {2}
- inDto.SubjectVisit.VisitName, // 访视 {3}
- dictionValue[0], // 是否加急 {4}
- dictionValue[1], // 审批结果 {5}
- _systemEmailConfig.SiteUrl // 链接 {6}
- );
+ {
+ var subjectCode = inDto.SubjectVisit.Subject.Code;
+ var topicStr = string.Format(input.topicStr, trialInfo.ResearchProgramNo, subjectCode, inDto.SubjectVisit.VisitName);
+ var htmlBodyStr = string.Format(
+ CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr, workLanguage),
+ string.Join(',', userNames), // 用户名 {0}
+ trialInfo.ExperimentName, // 项目 {1}
+ subjectCode, // 受试者 {2}
+ inDto.SubjectVisit.VisitName, // 访视 {3}
+ dictionValue[0], // 是否加急 {4}
+ dictionValue[1], // 审批结果 {5}
+ _systemEmailConfig.SiteUrl // 链接 {6}
+ );
- return (topicStr, htmlBodyStr);
- };
+ return (topicStr, htmlBodyStr);
+ };
-
-
- await CommonEmailHelper.GetEmailSubejctAndHtmlInfoAndBuildAsync(inDto.EmailNoticeConfig, messageToSend, emailConfigFunc);
+
+
+ await CommonEmailHelper.GetEmailSubejctAndHtmlInfoAndBuildAsync(inDto.EmailNoticeConfig, messageToSend, emailConfigFunc, workLanguage);
await SendEmailHelper.SendEmailAsync(messageToSend, trialInfo);
}
@@ -446,11 +474,19 @@ public class ImageConsumer(
var trialInfo = await _trialRepository.FirstOrDefaultAsync(t => t.Id == trialId);
// 根据不同场景获取不同角色的用户
- var trialUser = await _trialUseRoleRepository.Where(x => x.TrialId == trialId && !x.TrialUser.IsDeleted).Include(x => x.UserRole).Select(x => x.UserRole).ToListAsync();
+ var trialUser = await _trialUseRoleRepository.Where(x => x.TrialId == trialId && !x.TrialUser.IsDeleted).Include(x => x.UserRole).Select(x => x.UserRole)
+ .Select(x => new SendEmailUserDto()
+ {
+ UserTypeEnum = x.UserTypeEnum,
+ FullName = x.FullName,
+ UserWorkLanguage = x.IdentityUser.UserWorkLanguage,
+ EMail = x.EMail,
+ })
+ .ToListAsync();
// 根据场景确定收件人
- List toUserList = new List();
- List ccUserList = new List();
+ List toUserList = new List();
+ List ccUserList = new List();
var emailNoticeUserList = await _emailNoticeUserTypeRepository.Where(x => x.EmailNoticeConfigId == emailNoticeConfig.Id).ToListAsync();
@@ -469,12 +505,19 @@ public class ImageConsumer(
return;
}
- foreach (var userinfo in toUserList)
+ var userWorkLanguageList = toUserList.Select(x => x.UserWorkLanguage).Distinct().ToList();
+
+ foreach (var workLanguage in userWorkLanguageList)
{
+ var userinfoList = toUserList.Where(x => x.UserWorkLanguage == workLanguage).ToList();
var messageToSend = new MimeMessage();
// 发件地址
messageToSend.From.Add(new MailboxAddress(_systemEmailConfig.FromName, _systemEmailConfig.FromEmail));
- messageToSend.To.Add(new MailboxAddress(userinfo.FullName, userinfo.EMail));
+
+ foreach (var userinfo in userinfoList)
+ {
+ messageToSend.To.Add(new MailboxAddress(userinfo.FullName, userinfo.EMail));
+ }
// 添加抄送
foreach (var ccUser in ccUserList)
@@ -482,13 +525,15 @@ public class ImageConsumer(
messageToSend.Cc.Add(new MailboxAddress(ccUser.FullName, ccUser.EMail));
}
+ var userNames = userinfoList.Select(x => x.FullName).ToList();
+
// 格式化邮件内容
Func<(string topicStr, string htmlBodyStr), (string topicStr, string htmlBodyStr)> emailConfigFunc = input =>
{
var topicStr = string.Format(input.topicStr, trialInfo.ResearchProgramNo);
var htmlBodyStr = string.Format(
- CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr),
- userinfo.FullName, // 用户名 {0}
+ CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr, workLanguage),
+ string.Join(',', userNames), // 用户名 {0}
trialInfo.ExperimentName, // 项目 {1}
"", // 受试者 {2} - 阅片人筛选不涉及受试者
"", // 访视 {3} - 阅片人筛选不涉及访视
@@ -500,7 +545,7 @@ public class ImageConsumer(
return (topicStr, htmlBodyStr);
};
- await CommonEmailHelper.GetEmailSubejctAndHtmlInfoAndBuildAsync(emailNoticeConfig, messageToSend, emailConfigFunc);
+ await CommonEmailHelper.GetEmailSubejctAndHtmlInfoAndBuildAsync(emailNoticeConfig, messageToSend, emailConfigFunc, workLanguage);
await SendEmailHelper.SendEmailAsync(messageToSend, trialInfo);
}
diff --git a/IRaCIS.Core.Application/MassTransit/Consumer/ReadingRelationEmailConsumer.cs b/IRaCIS.Core.Application/MassTransit/Consumer/ReadingRelationEmailConsumer.cs
index c494a2ccc..f620b1f11 100644
--- a/IRaCIS.Core.Application/MassTransit/Consumer/ReadingRelationEmailConsumer.cs
+++ b/IRaCIS.Core.Application/MassTransit/Consumer/ReadingRelationEmailConsumer.cs
@@ -1,5 +1,6 @@
-using IRaCIS.Core.Application.Helper;
+using IRaCIS.Core.Application.Helper;
using IRaCIS.Core.Application.MassTransit.Command;
+using IRaCIS.Core.Application.MassTransit.Consumer.Dto;
using IRaCIS.Core.Domain;
using IRaCIS.Core.Domain.BaseModel;
using IRaCIS.Core.Domain.Models;
@@ -68,7 +69,7 @@ public class UrgentMedicalReviewAddedEventConsumer(
var criterion = await _readingQuestionCriterionTrialRepository.FirstOrDefaultAsync(x => x.Id == medicalReview.VisitTask.TrialReadingCriterionId);
- var userinfo = await _userRoleRepository.Where(x => x.Id == medicalReview.MedicalManagerUserId).FirstOrDefaultAsync();
+ var userinfo = await _userRoleRepository.Where(x => x.Id == medicalReview.MedicalManagerUserId).Include(x => x.IdentityUser).FirstOrDefaultAsync();
var taskInfo = await _visitTaskRepository.Where(x => x.Id == medicalReview.VisitTaskId).Include(x => x.SourceSubjectVisit).Include(x => x.ReadModule).Include(x => x.Subject).FirstNotNullAsync();
@@ -78,7 +79,8 @@ public class UrgentMedicalReviewAddedEventConsumer(
var subjectVisit = await _subjectVisitRepository.Where(x => x.Id == visitid).FirstOrDefaultAsync();
-
+ var workLanguage = userinfo.IdentityUser?.UserWorkLanguage;
+ var userIsEn_US = workLanguage.HasValue ? workLanguage.Value == UserWorkLanguage.US : isEn_US;
var messageToSend = new MimeMessage();
//发件地址
@@ -86,14 +88,14 @@ public class UrgentMedicalReviewAddedEventConsumer(
messageToSend.To.Add(new MailboxAddress(userinfo.FullName, userinfo.EMail));
var trialInfo = await _trialRepository.FirstOrDefaultAsync(t => t.Id == medicalReview.TrialId);
- var companyName = isEn_US ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
+ var companyName = userIsEn_US ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
var dictionValue = await CommonEmailHelper.TranslationDictionary(new TranslationDictionaryDto()
{
DictionaryRepository = _dictionaryRepository,
- IsEn_US = isEn_US,
+ IsEn_US = userIsEn_US,
DictionaryList = new List()
@@ -110,7 +112,7 @@ public class UrgentMedicalReviewAddedEventConsumer(
var subjectName = taskInfo.BlindSubjectCode.IsNullOrEmpty() ? taskInfo.Subject.Code : taskInfo.BlindSubjectCode;
var topicStr = string.Format(input.topicStr, trialInfo.ResearchProgramNo, subjectName, taskInfo.TaskBlindName);
var htmlBodyStr = string.Format(
- CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr),
+ CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr, workLanguage),
userinfo.FullName, // 用户名 {0}
trialInfo.ExperimentName, // 项目 {1}
criterion.CriterionName, // 阅片标准 {2}
@@ -126,7 +128,7 @@ public class UrgentMedicalReviewAddedEventConsumer(
return (topicStr, htmlBodyStr);
};
- await CommonEmailHelper.GetTrialEmailSubejctAndHtmlInfoAndBuildAsync(trialEmailConfig, messageToSend, emailConfigFunc);
+ await CommonEmailHelper.GetTrialEmailSubejctAndHtmlInfoAndBuildAsync(trialEmailConfig, messageToSend, emailConfigFunc, workLanguage);
await SendEmailHelper.SendEmailAsync(messageToSend, trialInfo);
@@ -177,7 +179,7 @@ public class UrgentIRRepliedMedicalReviewConsumer(
var criterion = await _readingQuestionCriterionTrialRepository.FirstOrDefaultAsync(x => x.Id == medicalReview.VisitTask.TrialReadingCriterionId);
- var userinfo = await _userRoleRepository.Where(x => x.Id == medicalReview.MedicalManagerUserId).FirstOrDefaultAsync();
+ var userinfo = await _userRoleRepository.Where(x => x.Id == medicalReview.MedicalManagerUserId).Include(x => x.IdentityUser).FirstOrDefaultAsync();
var taskInfo = await _visitTaskRepository.Where(x => x.Id == medicalReview.VisitTaskId).Include(x => x.SourceSubjectVisit).Include(x => x.ReadModule).Include(x => x.Subject).FirstNotNullAsync();
@@ -187,6 +189,8 @@ public class UrgentIRRepliedMedicalReviewConsumer(
var subjectVisit = await _subjectVisitRepository.Where(x => x.Id == visitid).FirstOrDefaultAsync();
+ var workLanguage = userinfo.IdentityUser?.UserWorkLanguage;
+ var userIsEn_US = workLanguage.HasValue ? workLanguage.Value == UserWorkLanguage.US : isEn_US;
var messageToSend = new MimeMessage();
@@ -195,14 +199,14 @@ public class UrgentIRRepliedMedicalReviewConsumer(
messageToSend.To.Add(new MailboxAddress(userinfo.FullName, userinfo.EMail));
var trialInfo = await _trialRepository.FirstOrDefaultAsync(t => t.Id == medicalReview.TrialId);
- var companyName = isEn_US ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
+ var companyName = userIsEn_US ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
var dictionValue = await CommonEmailHelper.TranslationDictionary(new TranslationDictionaryDto()
{
DictionaryRepository = _dictionaryRepository,
- IsEn_US = isEn_US,
+ IsEn_US = userIsEn_US,
DictionaryList = new List()
@@ -219,7 +223,7 @@ public class UrgentIRRepliedMedicalReviewConsumer(
var subjectCode = taskInfo.BlindSubjectCode.IsNullOrEmpty() ? taskInfo.Subject.Code : taskInfo.BlindSubjectCode;
var topicStr = string.Format(input.topicStr, trialInfo.ResearchProgramNo, subjectCode, taskInfo.TaskBlindName);
var htmlBodyStr = string.Format(
- CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr),
+ CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr, workLanguage),
userinfo.FullName, // 用户名 {0}
trialInfo.ExperimentName, // 项目 {1}
criterion.CriterionName, // 阅片标准 {2}
@@ -234,7 +238,7 @@ public class UrgentIRRepliedMedicalReviewConsumer(
return (topicStr, htmlBodyStr);
};
- await CommonEmailHelper.GetTrialEmailSubejctAndHtmlInfoAndBuildAsync(trialEmailConfig, messageToSend, emailConfigFunc);
+ await CommonEmailHelper.GetTrialEmailSubejctAndHtmlInfoAndBuildAsync(trialEmailConfig, messageToSend, emailConfigFunc, workLanguage);
await SendEmailHelper.SendEmailAsync(messageToSend, trialInfo);
}
@@ -293,7 +297,7 @@ public class UrgentMIMRepliedMedicalReviewConsumer(
var criterion = await _readingQuestionCriterionTrialRepository.FirstOrDefaultAsync(x => x.Id == medicalReview.VisitTask.TrialReadingCriterionId);
var taskInfo = await _visitTaskRepository.Where(x => x.Id == medicalReview.VisitTaskId).Include(x => x.SourceSubjectVisit).Include(x => x.ReadModule).Include(x => x.Subject).FirstNotNullAsync();
- var userinfo = await _userRoleRepository.Where(x => x.Id == taskInfo.DoctorUserId).FirstOrDefaultAsync();
+ var userinfo = await _userRoleRepository.Where(x => x.Id == taskInfo.DoctorUserId).Include(x => x.IdentityUser).FirstOrDefaultAsync();
@@ -303,6 +307,9 @@ public class UrgentMIMRepliedMedicalReviewConsumer(
var subjectVisit = await _subjectVisitRepository.Where(x => x.Id == visitid).FirstOrDefaultAsync();
+ var workLanguage = userinfo.IdentityUser?.UserWorkLanguage;
+ var userIsEn_US = workLanguage.HasValue ? workLanguage.Value == UserWorkLanguage.US : isEn_US;
+
var messageToSend = new MimeMessage();
@@ -311,14 +318,14 @@ public class UrgentMIMRepliedMedicalReviewConsumer(
messageToSend.To.Add(new MailboxAddress(userinfo.FullName, userinfo.EMail));
var trialInfo = await _trialRepository.FirstOrDefaultAsync(t => t.Id == medicalReview.TrialId);
- var companyName = isEn_US ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
+ var companyName = userIsEn_US ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
var dictionValue = await CommonEmailHelper.TranslationDictionary(new TranslationDictionaryDto()
{
DictionaryRepository = _dictionaryRepository,
- IsEn_US = isEn_US,
+ IsEn_US = userIsEn_US,
DictionaryList = new List()
@@ -335,7 +342,7 @@ public class UrgentMIMRepliedMedicalReviewConsumer(
var subjectCode = taskInfo.BlindSubjectCode.IsNullOrEmpty() ? taskInfo.Subject.Code : taskInfo.BlindSubjectCode;
var topicStr = string.Format(input.topicStr, trialInfo.ResearchProgramNo, subjectCode, taskInfo.TaskBlindName);
var htmlBodyStr = string.Format(
- CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr),
+ CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr, workLanguage),
userinfo.FullName, // 用户名 {0}
trialInfo.ExperimentName, // 项目 {1}
subjectCode, // 受试者 {2}
@@ -352,7 +359,7 @@ public class UrgentMIMRepliedMedicalReviewConsumer(
return (topicStr, htmlBodyStr);
};
- await CommonEmailHelper.GetTrialEmailSubejctAndHtmlInfoAndBuildAsync(trialEmailConfig, messageToSend, emailConfigFunc);
+ await CommonEmailHelper.GetTrialEmailSubejctAndHtmlInfoAndBuildAsync(trialEmailConfig, messageToSend, emailConfigFunc, workLanguage);
await SendEmailHelper.SendEmailAsync(messageToSend, trialInfo);
}
}
@@ -399,7 +406,15 @@ public class UrgentIRApplyedReReadingConsumer(
var doctorInfo = await _userRoleRepository.Where(x => x.Id == taskInfo.DoctorUserId).FirstOrDefaultAsync();
- var trialUserList = await _trialUseRoleRepository.Where(x => x.TrialId == taskInfo.TrialId && x.TrialUser.IsDeleted==false).Include(x => x.UserRole).Select(x => x.UserRole).ToListAsync();
+ var trialUserList = await _trialUseRoleRepository.Where(x => x.TrialId == taskInfo.TrialId && x.TrialUser.IsDeleted==false).Include(x => x.UserRole).Select(x => x.UserRole)
+ .Select(x => new SendEmailUserDto()
+ {
+ UserTypeEnum = x.UserTypeEnum,
+ FullName = x.FullName,
+ UserWorkLanguage = x.IdentityUser.UserWorkLanguage,
+ EMail = x.EMail,
+ })
+ .ToListAsync();
var userinfoList = trialUserList.Where(x => x.UserTypeEnum == UserTypeEnum.ProjectManager || x.UserTypeEnum == UserTypeEnum.APM).ToList();
if (context.Message.ReReadingApplyState == ReReadingApplyState.TrialGroupHaveApplyed)
@@ -412,37 +427,48 @@ public class UrgentIRApplyedReReadingConsumer(
var subjectVisit = await _subjectVisitRepository.Where(x => x.Id == visitid).FirstOrDefaultAsync();
+ var trialInfo = await _trialRepository.FirstOrDefaultAsync(t => t.Id == taskInfo.TrialId);
- foreach (var userinfo in userinfoList)
+ var userWorkLanguageList = userinfoList.Select(x => x.UserWorkLanguage).Distinct().ToList();
+
+ foreach (var workLanguage in userWorkLanguageList)
{
- var messageToSend = new MimeMessage();
- //发件地址
- messageToSend.From.Add(new MailboxAddress(_systemEmailConfig.FromName, _systemEmailConfig.FromEmail));
- messageToSend.To.Add(new MailboxAddress(userinfo.FullName, userinfo.EMail));
-
- var trialInfo = await _trialRepository.FirstOrDefaultAsync(t => t.Id == taskInfo.TrialId);
- var companyName = isEn_US ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
+ var langUserinfoList = userinfoList.Where(x => x.UserWorkLanguage == workLanguage).ToList();
+ if (!langUserinfoList.Any()) continue;
+ var userIsEn_US = workLanguage == UserWorkLanguage.US;
var dictionValue = await CommonEmailHelper.TranslationDictionary(new TranslationDictionaryDto()
{
-
DictionaryRepository = _dictionaryRepository,
- IsEn_US = isEn_US,
+ IsEn_US = userIsEn_US,
DictionaryList = new List()
- {
- new DictionaryDto (){DictionaryCode= "ReadingCategory",EnumValue=taskInfo.ReadingCategory.GetEnumInt(), }, //任务类型
- new DictionaryDto (){DictionaryCode= "RequestReReadingResult",EnumValue="0", }, //审批结果 都是待审批
- }
+ {
+ new DictionaryDto (){DictionaryCode= "ReadingCategory",EnumValue=taskInfo.ReadingCategory.GetEnumInt(), }, //任务类型
+ new DictionaryDto (){DictionaryCode= "RequestReReadingResult",EnumValue="0", }, //审批结果 都是待审批
+ }
});
+ var messageToSend = new MimeMessage();
+ //发件地址
+ messageToSend.From.Add(new MailboxAddress(_systemEmailConfig.FromName, _systemEmailConfig.FromEmail));
+
+ foreach (var userinfo in langUserinfoList)
+ {
+ messageToSend.To.Add(new MailboxAddress(userinfo.FullName, userinfo.EMail));
+ }
+
+ var companyName = userIsEn_US ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
+
+ var userNames = langUserinfoList.Select(x => x.FullName).ToList();
+
Func<(string topicStr, string htmlBodyStr), (string topicStr, string htmlBodyStr)> emailConfigFunc = input =>
{
var subjectCode = taskInfo.BlindSubjectCode.IsNullOrEmpty() ? taskInfo.Subject.Code : taskInfo.BlindSubjectCode;
var topicStr = string.Format(input.topicStr, trialInfo.ResearchProgramNo, subjectCode, taskInfo.TaskBlindName);
var htmlBodyStr = string.Format(
- CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr),
- userinfo.FullName, // 用户名 {0}
+ CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr, workLanguage),
+ string.Join(',', userNames), // 用户名 {0}
trialInfo.ExperimentName, // 项目 {1}
subjectCode, // 受试者 {2}
taskInfo.TaskBlindName, // 访视 {3}
@@ -451,16 +477,12 @@ public class UrgentIRApplyedReReadingConsumer(
criterion.CriterionName, // 阅片标准 {6}
dictionValue[1], // 审批结果 {7}
_systemEmailConfig.SiteUrl // 链接 {8}
-
-
-
-
);
return (topicStr, htmlBodyStr);
};
- await CommonEmailHelper.GetTrialEmailSubejctAndHtmlInfoAndBuildAsync(trialEmailConfig, messageToSend, emailConfigFunc);
+ await CommonEmailHelper.GetTrialEmailSubejctAndHtmlInfoAndBuildAsync(trialEmailConfig, messageToSend, emailConfigFunc, workLanguage);
await SendEmailHelper.SendEmailAsync(messageToSend, trialInfo);
}
diff --git a/IRaCIS.Core.Application/MassTransit/Consumer/SiteSurverEmailConsumer.cs b/IRaCIS.Core.Application/MassTransit/Consumer/SiteSurverEmailConsumer.cs
index 327cf090b..0a4a7eb4f 100644
--- a/IRaCIS.Core.Application/MassTransit/Consumer/SiteSurverEmailConsumer.cs
+++ b/IRaCIS.Core.Application/MassTransit/Consumer/SiteSurverEmailConsumer.cs
@@ -57,73 +57,85 @@ public class UserSiteSurveySubmitedEventConsumer(
if (emailConfig != null)
{
var trialInfo = await _trialRepository.FirstOrDefaultAsync(t => t.Id == trialId);
+ var siteInfo = await _trialSiteRepository.FirstOrDefaultAsync(t => t.TrialId == trialId && t.Id == siteSurveyInfo.TrialSiteId, true);
+
var trialUserList = await _trialUserRoleRepository.Where(t => t.TrialId == siteSurveyInfo.TrialId && t.TrialUser.IsDeleted == false)
.Where(t => t.UserRole.UserTypeEnum == UserTypeEnum.SPM || t.UserRole.UserTypeEnum == UserTypeEnum.CPM || t.UserRole.UserTypeEnum == UserTypeEnum.ProjectManager || t.UserRole.UserTypeEnum == UserTypeEnum.APM)
- .Select(t => new { t.UserRole.FullName, t.UserRole.IdentityUser.EMail, t.UserRole.UserTypeEnum }).ToListAsync();
+ .Select(t => new { t.UserRole.FullName, t.UserRole.IdentityUser.EMail, t.UserRole.UserTypeEnum, t.UserRole.IdentityUser.UserWorkLanguage }).ToListAsync();
var sPMOrCPMList = trialUserList.Where(t => t.UserTypeEnum == UserTypeEnum.SPM || t.UserTypeEnum == UserTypeEnum.CPM).ToList();
var pmAndAPMList = trialUserList.Where(t => t.UserTypeEnum == UserTypeEnum.ProjectManager || t.UserTypeEnum == UserTypeEnum.APM).ToList();
- var messageToSend = new MimeMessage();
- var toUserName = string.Empty;
-
- //有SPM 并且参与
- if (trialInfo.IsSPMJoinSiteSurvey && sPMOrCPMList.Count > 0)
+ // 遍历语言枚举 在底层判断收件人是否为空,决定是否跳过邮件发送,避免每个邮件场景的地方都重复判断
+ foreach (UserWorkLanguage workLanguage in Enum.GetValues())
{
- foreach (var user in sPMOrCPMList)
+ //重新设置当前发送邮件的语言
+ isEn_US = workLanguage == UserWorkLanguage.US;
+
+ var messageToSend = new MimeMessage();
+
+ var toUserName = string.Empty;
+
+ //有SPM 并且参与
+ if (trialInfo.IsSPMJoinSiteSurvey && sPMOrCPMList.Count > 0)
{
- messageToSend.To.Add(new MailboxAddress(user.FullName, user.EMail));
+ var lan_SPMOrCPMList = sPMOrCPMList.Where(t => t.UserWorkLanguage == workLanguage);
+
+ foreach (var user in lan_SPMOrCPMList)
+ {
+ messageToSend.To.Add(new MailboxAddress(user.FullName, user.EMail));
+ }
+
+ toUserName = string.Join('、', lan_SPMOrCPMList.Select(t => t.FullName));
+
+ foreach (var user in pmAndAPMList)
+ {
+ messageToSend.Cc.Add(new MailboxAddress(user.FullName, user.EMail));
+ }
+
+ }
+ else
+ {
+ var lan_pmAndAPMList = pmAndAPMList.Where(t => t.UserWorkLanguage == workLanguage);
+
+ foreach (var user in lan_pmAndAPMList)
+ {
+ messageToSend.To.Add(new MailboxAddress(user.FullName, user.EMail));
+ }
+
+ toUserName = string.Join('、', lan_pmAndAPMList.Select(t => t.FullName));
}
- toUserName = string.Join('、', sPMOrCPMList.Select(t => t.FullName));
+ //发件地址
+ messageToSend.From.Add(new MailboxAddress(_systemEmailConfig.FromName, _systemEmailConfig.FromEmail));
- foreach (var user in pmAndAPMList)
+
+ var companyName = isEn_US ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
+
+ Func<(string topicStr, string htmlBodyStr), (string topicStr, string htmlBodyStr)> emailConfigFunc = input =>
{
- messageToSend.Cc.Add(new MailboxAddress(user.FullName, user.EMail));
- }
+ var topicStr = string.Format(input.topicStr, trialInfo.ResearchProgramNo);
+ var htmlBodyStr = string.Format(CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr, workLanguage),
+ toUserName,
+ siteInfo.TrialSiteCode,
+ siteInfo.TrialSiteAliasName,
+ siteSurveyInfo.UserName,
+ siteSurveyInfo.Email,
+ siteSurveyInfo.Phone,
+ _systemEmailConfig.SiteUrl
+ );
+
+ return (topicStr, htmlBodyStr);
+ };
+
+
+ await CommonEmailHelper.GetEmailSubejctAndHtmlInfoAndBuildAsync(emailConfig, messageToSend, emailConfigFunc, workLanguage);
+
+ await SendEmailHelper.SendEmailAsync(messageToSend, trialInfo);
}
- else
- {
- foreach (var user in pmAndAPMList)
- {
- messageToSend.To.Add(new MailboxAddress(user.FullName, user.EMail));
- }
-
- toUserName = string.Join('、', pmAndAPMList.Select(t => t.FullName));
- }
-
- //发件地址
- messageToSend.From.Add(new MailboxAddress(_systemEmailConfig.FromName, _systemEmailConfig.FromEmail));
-
-
- var siteInfo = await _trialSiteRepository.FirstOrDefaultAsync(t => t.TrialId == trialId && t.Id == siteSurveyInfo.TrialSiteId, true);
-
- var companyName = isEn_US ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
-
- Func<(string topicStr, string htmlBodyStr), (string topicStr, string htmlBodyStr)> emailConfigFunc = input =>
- {
- var topicStr = string.Format(input.topicStr, trialInfo.ResearchProgramNo);
- var htmlBodyStr = string.Format(CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr),
- toUserName,
- siteInfo.TrialSiteCode,
- siteInfo.TrialSiteAliasName,
-
- siteSurveyInfo.UserName,
- siteSurveyInfo.Email,
- siteSurveyInfo.Phone,
- _systemEmailConfig.SiteUrl
- );
-
- return (topicStr, htmlBodyStr);
- };
-
-
- await CommonEmailHelper.GetEmailSubejctAndHtmlInfoAndBuildAsync(emailConfig, messageToSend, emailConfigFunc);
-
- await SendEmailHelper.SendEmailAsync(messageToSend, trialInfo);
}
}
@@ -152,61 +164,74 @@ public class SiteSurveySPMSubmitedEventConsumer(
var trialId = siteSurveyInfo.TrialId;
+ var trialInfo = await _trialRepository.FirstOrDefaultAsync(t => t.Id == trialId);
+
+ var siteInfo = await _trialSiteRepository.FirstOrDefaultAsync(t => t.TrialId == trialId && t.Id == siteSurveyInfo.TrialSiteId, true);
+
+ var trialUserList = _trialUserRoleRepository.Where(t => t.TrialId == trialId && t.TrialUser.IsDeleted == false)
+ .Where(t => t.UserRole.UserTypeEnum == UserTypeEnum.SPM || t.UserRole.UserTypeEnum == UserTypeEnum.CPM || t.UserRole.UserTypeEnum == UserTypeEnum.ProjectManager || t.UserRole.UserTypeEnum == UserTypeEnum.APM)
+ .Select(t => new { t.UserRole.IdentityUser.EMail, t.UserRole.FullName, t.UserRole.UserTypeEnum, t.UserRole.IdentityUser.UserWorkLanguage }).ToList();
+
var scenario = EmailBusinessScenario.Approval_SubmitSiteSurvey;
var emailConfig = _emailNoticeConfigrepository.Where(t => t.BusinessScenarioEnum == scenario && t.IsAutoSend && t.IsEnable).FirstOrDefault();
if (emailConfig != null)
{
- var messageToSend = new MimeMessage();
- var trialUserList = _trialUserRoleRepository.Where(t => t.TrialId == trialId && t.TrialUser.IsDeleted == false)
- .Where(t => t.UserRole.UserTypeEnum == UserTypeEnum.SPM || t.UserRole.UserTypeEnum == UserTypeEnum.CPM || t.UserRole.UserTypeEnum == UserTypeEnum.ProjectManager || t.UserRole.UserTypeEnum == UserTypeEnum.APM)
- .Select(t => new { t.UserRole.IdentityUser.EMail, t.UserRole.FullName, t.UserRole.UserTypeEnum }).ToList();
-
- var sPMOrCPMList = trialUserList.Where(t => t.UserTypeEnum == UserTypeEnum.SPM || t.UserTypeEnum == UserTypeEnum.CPM).ToList();
- var pmAndAPMList = trialUserList.Where(t => t.UserTypeEnum == UserTypeEnum.ProjectManager || t.UserTypeEnum == UserTypeEnum.APM).ToList();
-
- var toUserName = string.Empty;
-
- foreach (var item in pmAndAPMList)
+ // 遍历语言枚举 在底层判断收件人是否为空,决定是否跳过邮件发送,避免每个邮件场景的地方都重复判断
+ foreach (UserWorkLanguage workLanguage in Enum.GetValues())
{
- messageToSend.To.Add(new MailboxAddress(item.FullName, item.EMail));
+ //重新设置当前发送邮件的语言
+ isEn_US = workLanguage == UserWorkLanguage.US;
+
+
+ var sPMOrCPMList = trialUserList.Where(t => t.UserTypeEnum == UserTypeEnum.SPM || t.UserTypeEnum == UserTypeEnum.CPM).ToList();
+ var pmAndAPMList = trialUserList.Where(t => t.UserTypeEnum == UserTypeEnum.ProjectManager || t.UserTypeEnum == UserTypeEnum.APM).ToList();
+
+ var messageToSend = new MimeMessage();
+
+ var toUserName = string.Empty;
+
+ var lan_pmAndAPMList = pmAndAPMList.Where(t => t.UserWorkLanguage == workLanguage);
+
+ foreach (var item in lan_pmAndAPMList)
+ {
+ messageToSend.To.Add(new MailboxAddress(item.FullName, item.EMail));
+
+ toUserName = string.Join('、', lan_pmAndAPMList.Select(t => t.FullName));
+ }
+
+ //发件地址
+ messageToSend.From.Add(new MailboxAddress(_systemEmailConfig.FromName, _systemEmailConfig.FromEmail));
+
+ var companyName = isEn_US ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
+
+
+ Func<(string topicStr, string htmlBodyStr), (string topicStr, string htmlBodyStr)> emailConfigFunc = input =>
+ {
+ var topicStr = string.Format(input.topicStr, trialInfo.ResearchProgramNo);
+ var htmlBodyStr = string.Format(CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr, workLanguage),
+ toUserName,
+ siteInfo.TrialSiteCode, //中心编号
+ siteInfo.TrialSiteAliasName,//中心名称
+
+ siteSurveyInfo.UserName, //联系人
+ siteSurveyInfo.Email, //联系邮箱
+ siteSurveyInfo.Phone, //联系电话
+ _systemEmailConfig.SiteUrl
+ );
+
+ return (topicStr, htmlBodyStr);
+ };
+
+ await CommonEmailHelper.GetEmailSubejctAndHtmlInfoAndBuildAsync(emailConfig, messageToSend, emailConfigFunc, workLanguage);
+
+ await SendEmailHelper.SendEmailAsync(messageToSend, trialInfo);
- toUserName = string.Join('、', pmAndAPMList.Select(t => t.FullName));
}
- //发件地址
- messageToSend.From.Add(new MailboxAddress(_systemEmailConfig.FromName, _systemEmailConfig.FromEmail));
- var trialInfo = await _trialRepository.FirstOrDefaultAsync(t => t.Id == trialId);
-
- var siteInfo = await _trialSiteRepository.FirstOrDefaultAsync(t => t.TrialId == trialId && t.Id == siteSurveyInfo.TrialSiteId, true);
-
-
- var companyName = isEn_US ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
-
-
- Func<(string topicStr, string htmlBodyStr), (string topicStr, string htmlBodyStr)> emailConfigFunc = input =>
- {
- var topicStr = string.Format(input.topicStr, trialInfo.ResearchProgramNo);
- var htmlBodyStr = string.Format(CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr),
- toUserName,
- siteInfo.TrialSiteCode, //中心编号
- siteInfo.TrialSiteAliasName,//中心名称
-
- siteSurveyInfo.UserName, //联系人
- siteSurveyInfo.Email, //联系邮箱
- siteSurveyInfo.Phone, //联系电话
- _systemEmailConfig.SiteUrl
- );
-
- return (topicStr, htmlBodyStr);
- };
-
- await CommonEmailHelper.GetEmailSubejctAndHtmlInfoAndBuildAsync(emailConfig, messageToSend, emailConfigFunc);
-
- await SendEmailHelper.SendEmailAsync(messageToSend, trialInfo);
}
@@ -235,92 +260,108 @@ public class SiteSurverRejectedEventConsumer(
var trialSiteSurveyId = context.Message.TrialSiteSurveyId;
- var siteSurveyInfo = _trialSiteSurveyRepository.Where(t => t.Id == trialSiteSurveyId ,ignoreQueryFilters:true).FirstOrDefault().IfNullThrowException();
+ var siteSurveyInfo = _trialSiteSurveyRepository.Where(t => t.Id == trialSiteSurveyId, ignoreQueryFilters: true).FirstOrDefault().IfNullThrowException();
var trialId = siteSurveyInfo.TrialId;
+ var trialInfo = await _trialRepository.FirstOrDefaultAsync(t => t.Id == trialId);
+
+ var siteInfo = await _trialSiteRepository.FirstOrDefaultAsync(t => t.TrialId == trialId && t.Id == siteSurveyInfo.TrialSiteId, true);
+
var scenario = EmailBusinessScenario.SiteSurveyReject;
var emailConfig = _emailNoticeConfigrepository.Where(t => t.BusinessScenarioEnum == scenario && t.IsAutoSend && t.IsEnable).FirstOrDefault();
if (emailConfig != null)
{
-
-
- var messageToSend = new MimeMessage();
-
- var toUserName = siteSurveyInfo.UserName;
-
- if (context.Message.IsHaveSPMOrCPM)
+ // 遍历语言枚举 在底层判断收件人是否为空,决定是否跳过邮件发送,避免每个邮件场景的地方都重复判断
+ foreach (UserWorkLanguage workLanguage in Enum.GetValues())
{
- //PM 驳回到SPM
- if (siteSurveyInfo.State == TrialSiteSurveyEnum.CRCSubmitted)
+ //重新设置当前发送邮件的语言
+ isEn_US = workLanguage == UserWorkLanguage.US;
+
+
+ var messageToSend = new MimeMessage();
+
+ var toUserName = siteSurveyInfo.UserName;
+
+ if (context.Message.IsHaveSPMOrCPM)
{
- //var user = await _userRoleRepository.FirstOrDefaultAsync(t => t.Id == siteSurveyInfo.PreliminaryUserId);
-
- //name = user.FullName;
-
- var sPMOrCPMList = _trialUserRoleRepository.Where(t => t.TrialId == trialId && t.TrialUser.IsDeleted == false)
- .Where(t => t.UserRole.UserTypeEnum == UserTypeEnum.SPM || t.UserRole.UserTypeEnum == UserTypeEnum.CPM)
- .Select(t => new { t.UserRole.IdentityUser.EMail, t.UserRole.FullName, t.UserRole.UserTypeEnum }).ToList();
-
-
- foreach (var user in sPMOrCPMList)
+ //PM 驳回到SPM
+ if (siteSurveyInfo.State == TrialSiteSurveyEnum.CRCSubmitted)
{
- messageToSend.To.Add(new MailboxAddress(user.FullName, user.EMail));
+
+ var sPMOrCPMList = _trialUserRoleRepository.Where(t => t.TrialId == trialId && t.TrialUser.IsDeleted == false)
+ .Where(t => t.UserRole.UserTypeEnum == UserTypeEnum.SPM || t.UserRole.UserTypeEnum == UserTypeEnum.CPM)
+ .Select(t => new { t.UserRole.IdentityUser.EMail, t.UserRole.FullName, t.UserRole.UserTypeEnum, t.UserRole.IdentityUser.UserWorkLanguage }).ToList();
+
+ var lan_SPMOrCPMList = sPMOrCPMList.Where(t => t.UserWorkLanguage == workLanguage);
+
+ foreach (var user in lan_SPMOrCPMList)
+ {
+ messageToSend.To.Add(new MailboxAddress(user.FullName, user.EMail));
+ }
+
+ toUserName = string.Join('、', lan_SPMOrCPMList.Select(t => t.FullName));
+
+ }
+ //SPM 驳回到CRC
+ else if (siteSurveyInfo.State == TrialSiteSurveyEnum.ToSubmit)
+ {
+ //使用中心国家语言 一致才发送
+ if ((siteInfo.Country == StaticData.SiteCountry.US && isEn_US) || (siteInfo.Country == StaticData.SiteCountry.CN && isEn_US == false))
+ {
+ messageToSend.To.Add(new MailboxAddress(toUserName, siteSurveyInfo.Email));
+
+ }
+
}
- toUserName = string.Join('、', sPMOrCPMList.Select(t => t.FullName));
-
}
- //SPM 驳回到CRC
- else if (siteSurveyInfo.State == TrialSiteSurveyEnum.ToSubmit)
+ else
{
- messageToSend.To.Add(new MailboxAddress(toUserName, siteSurveyInfo.Email));
+ //没有SPM PM驳回到CRC
+
+ //使用中心国家语言 一致才发送
+ if ((siteInfo.Country == StaticData.SiteCountry.US && isEn_US) || (siteInfo.Country == StaticData.SiteCountry.CN && isEn_US == false))
+ {
+ messageToSend.To.Add(new MailboxAddress(toUserName, siteSurveyInfo.Email));
+
+ }
}
- }
- else
- {
- //没有SPM PM驳回到CRC
+ //发件地址
+ messageToSend.From.Add(new MailboxAddress(_systemEmailConfig.FromName, _systemEmailConfig.FromEmail));
+
+ var companyName = isEn_US ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
+
+
+ Func<(string topicStr, string htmlBodyStr), (string topicStr, string htmlBodyStr)> emailConfigFunc = input =>
+ {
+ var topicStr = string.Format(input.topicStr, companyName, trialInfo.ResearchProgramNo);
+ var htmlBodyStr = string.Format(CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr, workLanguage),
+ toUserName,
+ trialInfo.TrialCode,
+ trialInfo.ResearchProgramNo,
+ trialInfo.ExperimentName,
+
+ siteInfo.TrialSiteCode, //中心编号
+ siteInfo.TrialSiteAliasName,//中心名称
+ siteSurveyInfo.LatestBackReason, //驳回原因
+ _systemEmailConfig.SiteUrl, //链接
+ (siteSurveyInfo.State == TrialSiteSurveyEnum.ToSubmit ? "inline - block" : "none")
+ );
+
+ return (topicStr, htmlBodyStr);
+ };
+
+ await CommonEmailHelper.GetEmailSubejctAndHtmlInfoAndBuildAsync(emailConfig, messageToSend, emailConfigFunc);
+
+ await SendEmailHelper.SendEmailAsync(messageToSend, trialInfo);
- messageToSend.To.Add(new MailboxAddress(siteSurveyInfo.UserName, siteSurveyInfo.Email));
}
- //发件地址
- messageToSend.From.Add(new MailboxAddress(_systemEmailConfig.FromName, _systemEmailConfig.FromEmail));
-
- var trialInfo = await _trialRepository.FirstOrDefaultAsync(t => t.Id == trialId);
-
- var siteInfo = await _trialSiteRepository.FirstOrDefaultAsync(t => t.TrialId == trialId && t.Id == siteSurveyInfo.TrialSiteId, true);
-
-
- var companyName = isEn_US ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
-
-
- Func<(string topicStr, string htmlBodyStr), (string topicStr, string htmlBodyStr)> emailConfigFunc = input =>
- {
- var topicStr = string.Format(input.topicStr, companyName,trialInfo.ResearchProgramNo);
- var htmlBodyStr = string.Format(CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr),
- toUserName,
- trialInfo.TrialCode,
- trialInfo.ResearchProgramNo,
- trialInfo.ExperimentName,
-
- siteInfo.TrialSiteCode, //中心编号
- siteInfo.TrialSiteAliasName,//中心名称
- siteSurveyInfo.LatestBackReason, //驳回原因
- _systemEmailConfig.SiteUrl, //链接
- (siteSurveyInfo.State == TrialSiteSurveyEnum.ToSubmit ? "inline - block" : "none")
- );
-
- return (topicStr, htmlBodyStr);
- };
-
- await CommonEmailHelper.GetEmailSubejctAndHtmlInfoAndBuildAsync(emailConfig, messageToSend, emailConfigFunc);
-
- await SendEmailHelper.SendEmailAsync(messageToSend, trialInfo);
}
diff --git a/IRaCIS.Core.Application/MassTransit/Consumer/SubjectVisitQCAndCheckConsumer.cs b/IRaCIS.Core.Application/MassTransit/Consumer/SubjectVisitQCAndCheckConsumer.cs
index 100c5b596..2daf610d3 100644
--- a/IRaCIS.Core.Application/MassTransit/Consumer/SubjectVisitQCAndCheckConsumer.cs
+++ b/IRaCIS.Core.Application/MassTransit/Consumer/SubjectVisitQCAndCheckConsumer.cs
@@ -36,7 +36,7 @@ public class CRCSubmitedAndQCToAuditEventConsumer(
private readonly SystemEmailSendConfig _systemEmailConfig = systemEmailConfig.CurrentValue;
public async Task Consume(ConsumeContext context)
{
- Console.WriteLine("发送(005,006) 【加急项目所有IQC待领取质控任务】邮件!!!");
+ Log.Logger.Warning("发送(005,006) 【加急项目所有IQC待领取质控任务】邮件!!!");
var isEn_US = CultureInfo.CurrentCulture.Name == StaticData.CultureInfo.en_US;
var subjectVisitId = context.Message.SubjectVisitId;
@@ -49,80 +49,91 @@ public class CRCSubmitedAndQCToAuditEventConsumer(
var trialEmailConfig = _trialEmailNoticeConfigrepository.Where(t => t.TrialId == trialId && t.BusinessScenarioEnum == scenario && t.IsAutoSend && t.IsEnable).FirstOrDefault();
+ var trialUserList = await _trialUseRoleRepository.Where(x => x.TrialId == subjectVisit.TrialId && x.TrialUser.IsDeleted == false)
+ .Select(t => new { t.UserRole.IdentityUser.EMail, t.UserRole.FullName, t.UserRole.UserTypeEnum, t.UserRole.IdentityUser.UserWorkLanguage }).ToListAsync();
+
+ var trialInfo = await _trialRepository.FirstOrDefaultAsync(t => t.Id == subjectVisit.TrialId);
+
if (trialEmailConfig != null)
{
-
- var trialUserList = await _trialUseRoleRepository.Where(x => x.TrialId == subjectVisit.TrialId && x.TrialUser.IsDeleted == false).Include(x => x.UserRole).Select(x => x.UserRole).ToListAsync();
-
- var userinfoList = trialUserList.Where(x => x.UserTypeEnum == UserTypeEnum.IQC).ToList();
-
-
- var pmandAPm = trialUserList.Where(x => x.UserTypeEnum == UserTypeEnum.APM || x.UserTypeEnum == UserTypeEnum.ProjectManager).ToList();
-
- var trialInfo = await _trialRepository.FirstOrDefaultAsync(t => t.Id == subjectVisit.TrialId);
-
- var auditStateCode = "AuditStatePE";
-
- if (trialInfo.QCProcessEnum == TrialQCProcess.DoubleAudit)
+ // 遍历语言枚举 在底层判断收件人是否为空,决定是否跳过邮件发送,避免每个邮件场景的地方都重复判断
+ foreach (UserWorkLanguage workLanguage in Enum.GetValues())
{
- auditStateCode = "AuditStateRC";
- }
+ //重新设置当前发送邮件的语言
+ isEn_US = workLanguage == UserWorkLanguage.US;
+ var iqcList = trialUserList.Where(x => x.UserTypeEnum == UserTypeEnum.IQC).Where(t => t.UserWorkLanguage == workLanguage).ToList();
- var dictionValue = await CommonEmailHelper.TranslationDictionary(new TranslationDictionaryDto()
- {
- DictionaryRepository = _dictionaryRepository,
- IsEn_US = isEn_US,
- DictionaryList = new List()
+ var pmandAPm = trialUserList.Where(x => x.UserTypeEnum == UserTypeEnum.APM || x.UserTypeEnum == UserTypeEnum.ProjectManager).ToList();
+
+
+ var auditStateCode = "AuditStatePE";
+
+ if (trialInfo.QCProcessEnum == TrialQCProcess.DoubleAudit)
+ {
+ auditStateCode = "AuditStateRC";
+ }
+
+ var dictionValue = await CommonEmailHelper.TranslationDictionary(new TranslationDictionaryDto()
+ {
+
+ DictionaryRepository = _dictionaryRepository,
+ IsEn_US = isEn_US,
+ DictionaryList = new List()
{
new DictionaryDto (){DictionaryCode= auditStateCode,EnumValue=subjectVisit.AuditState.GetEnumInt(), }, //审核状态
}
- });
+ });
- var messageToSend = new MimeMessage();
- //发件地址
- messageToSend.From.Add(new MailboxAddress(_systemEmailConfig.FromName, _systemEmailConfig.FromEmail));
+ var messageToSend = new MimeMessage();
+ //发件地址
+ messageToSend.From.Add(new MailboxAddress(_systemEmailConfig.FromName, _systemEmailConfig.FromEmail));
+
+
+ foreach (var userinfo in iqcList)
+ {
+ messageToSend.To.Add(new MailboxAddress(userinfo.FullName, userinfo.EMail));
+ }
+
+ var userNames = iqcList.Select(x => x.FullName).ToList();
+
+ foreach (var pm in pmandAPm)
+ {
+ messageToSend.Cc.Add(new MailboxAddress(pm.FullName, pm.EMail));
+ }
+
+ var companyName = isEn_US ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
+
+
+
+ Func<(string topicStr, string htmlBodyStr), (string topicStr, string htmlBodyStr)> emailConfigFunc = input =>
+ {
+ var subjectCode = subjectVisit.Subject.Code;
+ var topicStr = string.Format(input.topicStr, trialInfo.ResearchProgramNo, subjectCode, subjectVisit.VisitName);
+ var htmlBodyStr = string.Format(
+ CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr, workLanguage),
+ string.Join(',', userNames), // 用户名 {0}
+ trialInfo.ExperimentName, // 项目 {1}
+ subjectCode, // 受试者 {2}
+ subjectVisit.VisitName, // 访视 {3}
+ dictionValue[0], // 审核状态 {4}
+ _systemEmailConfig.SiteUrl // 链接 {5}
+ );
+
+ return (topicStr, htmlBodyStr);
+ };
+
+ await CommonEmailHelper.GetTrialEmailSubejctAndHtmlInfoAndBuildAsync(trialEmailConfig, messageToSend, emailConfigFunc, workLanguage);
+
+ await SendEmailHelper.SendEmailAsync(messageToSend, trialInfo);
- foreach (var userinfo in userinfoList)
- {
- messageToSend.To.Add(new MailboxAddress(userinfo.FullName, userinfo.EMail));
}
- var userNames = userinfoList.Select(x => x.FullName).ToList();
- foreach (var pm in pmandAPm)
- {
- messageToSend.Cc.Add(new MailboxAddress(pm.FullName, pm.EMail));
- }
-
- var companyName = isEn_US ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
-
-
-
- Func<(string topicStr, string htmlBodyStr), (string topicStr, string htmlBodyStr)> emailConfigFunc = input =>
- {
- var subjectCode = subjectVisit.Subject.Code;
- var topicStr = string.Format(input.topicStr, trialInfo.ResearchProgramNo, subjectCode, subjectVisit.VisitName);
- var htmlBodyStr = string.Format(
- CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr),
- string.Join(',', userNames), // 用户名 {0}
- trialInfo.ExperimentName, // 项目 {1}
- subjectCode, // 受试者 {2}
- subjectVisit.VisitName, // 访视 {3}
- dictionValue[0], // 审核状态 {4}
- _systemEmailConfig.SiteUrl // 链接 {5}
- );
-
- return (topicStr, htmlBodyStr);
- };
-
- await CommonEmailHelper.GetTrialEmailSubejctAndHtmlInfoAndBuildAsync(trialEmailConfig, messageToSend, emailConfigFunc);
-
- await SendEmailHelper.SendEmailAsync(messageToSend, trialInfo);
}
}
@@ -147,7 +158,7 @@ public class CRCRepliedQCChallengeEventConsumer(
private readonly SystemEmailSendConfig _systemEmailConfig = systemEmailConfig.CurrentValue;
public async Task Consume(ConsumeContext context)
{
- Console.WriteLine("发送(Code012,013) 【CRC 回复质控质疑 通知QC】邮件!!!");
+ Log.Logger.Warning("发送(Code012,013) 【CRC 回复质控质疑 通知QC】邮件!!!");
var isEn_US = CultureInfo.CurrentCulture.Name == StaticData.CultureInfo.en_US;
var subjectVisitId = context.Message.SubjectVisitId;
@@ -158,26 +169,29 @@ public class CRCRepliedQCChallengeEventConsumer(
var scenario = context.Message.IsPd ? EmailBusinessScenario.PDVerification_UnderQCQuery : EmailBusinessScenario.EligibilityVerification_Pending;
+
+
var trialEmailConfig = _trialEmailNoticeConfigrepository.Where(t => t.TrialId == trialId && t.BusinessScenarioEnum == scenario && t.IsAutoSend && t.IsEnable).FirstOrDefault();
if (trialEmailConfig != null)
{
-
-
var qCChallengeDialog = await _qCChallengeDialogRepository.Where(x => x.Id == context.Message.QCChallengeDialogId).Include(x => x.QCChallenge).FirstNotNullAsync();
+ var trialUserList = await _trialUseRoleRepository.Where(x => x.TrialId == subjectVisit.TrialId && x.TrialUser.IsDeleted == false)
+ .Select(t => new { t.UserRole.IdentityUser.EMail, t.UserRole.FullName, t.UserRole.UserTypeEnum, t.UserRole.IdentityUser.UserWorkLanguage }).ToListAsync();
- var trialUserList = await _trialUseRoleRepository.Where(x => x.TrialId == subjectVisit.TrialId && x.TrialUser.IsDeleted == false).Include(x => x.UserRole).Select(x => x.UserRole).ToListAsync();
var userinfoList = trialUserList.Where(x => x.UserTypeEnum == UserTypeEnum.IQC).ToList();
var pmandAPm = trialUserList.Where(x => x.UserTypeEnum == UserTypeEnum.APM || x.UserTypeEnum == UserTypeEnum.ProjectManager).ToList();
-
-
var trialInfo = await _trialRepository.FirstOrDefaultAsync(t => t.Id == subjectVisit.TrialId);
var userinfo = subjectVisit.CurrentActionUser;
+ //重新设置当前发送邮件的语言
+ var workLanguage = _userRoleRepository.Where(t => t.Id == userinfo.Id).Select(t => t.IdentityUser.UserWorkLanguage).First();
+ isEn_US = workLanguage == UserWorkLanguage.US;
+
var messageToSend = new MimeMessage();
//发件地址
messageToSend.From.Add(new MailboxAddress(_systemEmailConfig.FromName, _systemEmailConfig.FromEmail));
@@ -207,7 +221,7 @@ public class CRCRepliedQCChallengeEventConsumer(
var subjectCode = subjectVisit.Subject.Code;
var topicStr = string.Format(input.topicStr, trialInfo.ResearchProgramNo, subjectCode, subjectVisit.VisitName);
var htmlBodyStr = string.Format(
- CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr),
+ CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr, workLanguage),
userinfo.FullName, // 用户名 {0}
trialInfo.ExperimentName, // 项目 {1}
subjectCode, // 受试者 {2}
@@ -219,7 +233,7 @@ public class CRCRepliedQCChallengeEventConsumer(
return (topicStr, htmlBodyStr);
};
- await CommonEmailHelper.GetTrialEmailSubejctAndHtmlInfoAndBuildAsync(trialEmailConfig, messageToSend, emailConfigFunc);
+ await CommonEmailHelper.GetTrialEmailSubejctAndHtmlInfoAndBuildAsync(trialEmailConfig, messageToSend, emailConfigFunc, workLanguage);
await SendEmailHelper.SendEmailAsync(messageToSend, trialInfo);
}
@@ -244,7 +258,7 @@ public class QCRepliedQCChallengeEventConsumer(
private readonly SystemEmailSendConfig _systemEmailConfig = systemEmailConfig.CurrentValue;
public async Task Consume(ConsumeContext context)
{
- Console.WriteLine("发送(014,015) 【 QC回复 质控质疑,通知CRC】邮件!!!");
+ Log.Logger.Warning("发送(014,015) 【 QC回复 质控质疑,通知CRC】邮件!!!");
var isEn_US = CultureInfo.CurrentCulture.Name == StaticData.CultureInfo.en_US;
var subjectVisitId = context.Message.SubjectVisitId;
@@ -277,54 +291,61 @@ public class QCRepliedQCChallengeEventConsumer(
}
-
- var trialUserList = await _trialUseRoleRepository.Where(x => x.TrialId == subjectVisit.TrialId && x.TrialUser.IsDeleted == false).Include(x => x.UserRole).Select(x => x.UserRole).ToListAsync();
-
- var userinfoList = trialUserList.Where(x => x.UserTypeEnum == UserTypeEnum.ClinicalResearchCoordinator).ToList();
-
- var craInfo = trialUserList.Where(x => x.UserTypeEnum == UserTypeEnum.CRA).FirstOrDefault();
+ var trialUserList = await _trialUseRoleRepository.Where(x => x.TrialId == subjectVisit.TrialId && x.TrialUser.IsDeleted == false)
+ .Select(t => new { t.UserRole.IdentityUser.EMail, t.UserRole.FullName, t.UserRole.UserTypeEnum, t.UserRole.IdentityUser.UserWorkLanguage }).ToListAsync();
var trialInfo = await _trialRepository.FirstOrDefaultAsync(t => t.Id == subjectVisit.TrialId);
-
-
- var dictionValue = await CommonEmailHelper.TranslationDictionary(new TranslationDictionaryDto()
+ // 遍历语言枚举 在底层判断收件人是否为空,决定是否跳过邮件发送,避免每个邮件场景的地方都重复判断
+ foreach (UserWorkLanguage workLanguage in Enum.GetValues())
{
-
- DictionaryRepository = _dictionaryRepository,
- IsEn_US = isEn_US,
- DictionaryList = new List()
- {
- new DictionaryDto (){DictionaryCode= "YesOrNo",EnumValue=isclose.ToString().ToLower(), }, //是否关闭
-
- }
- });
+ //重新设置当前发送邮件的语言
+ isEn_US = workLanguage == UserWorkLanguage.US;
+
+
+ var userinfoList = trialUserList.Where(x => x.UserTypeEnum == UserTypeEnum.ClinicalResearchCoordinator).Where(t => t.UserWorkLanguage == workLanguage).ToList();
+
+ var craInfoList = trialUserList.Where(x => x.UserTypeEnum == UserTypeEnum.CRA).ToList();
+
+ var dictionValue = await CommonEmailHelper.TranslationDictionary(new TranslationDictionaryDto()
+ {
+
+ DictionaryRepository = _dictionaryRepository,
+ IsEn_US = isEn_US,
+ DictionaryList = new List()
+ {
+ new DictionaryDto (){DictionaryCode= "YesOrNo",EnumValue=isclose.ToString().ToLower(), }, //是否关闭
+
+ }
+ });
- foreach (var userinfo in userinfoList)
- {
var messageToSend = new MimeMessage();
//发件地址
messageToSend.From.Add(new MailboxAddress(_systemEmailConfig.FromName, _systemEmailConfig.FromEmail));
- messageToSend.To.Add(new MailboxAddress(userinfo.FullName, userinfo.EMail));
- if (craInfo != null)
+
+ var toUserName = string.Join('、', userinfoList.Select(t => t.FullName));
+
+ foreach (var userinfo in userinfoList)
{
- messageToSend.Cc.Add(new MailboxAddress(craInfo.FullName, craInfo.EMail));
+ messageToSend.To.Add(new MailboxAddress(userinfo.FullName, userinfo.EMail));
+ }
+
+ foreach (var user in craInfoList)
+ {
+ messageToSend.Cc.Add(new MailboxAddress(user.FullName, user.EMail));
}
var companyName = isEn_US ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
-
-
-
Func<(string topicStr, string htmlBodyStr), (string topicStr, string htmlBodyStr)> emailConfigFunc = input =>
{
var subjectCode = subjectVisit.Subject.Code;
var topicStr = string.Format(input.topicStr, trialInfo.ResearchProgramNo, subjectCode, subjectVisit.VisitName);
var htmlBodyStr = string.Format(
- CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr),
- userinfo.FullName, // 用户名 {0}
+ CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr, workLanguage),
+ toUserName, // 用户名 {0}
trialInfo.ExperimentName, // 项目 {1}
subjectCode, // 受试者 {2}
subjectVisit.VisitName, // 访视 {3}
@@ -337,10 +358,13 @@ public class QCRepliedQCChallengeEventConsumer(
};
- await CommonEmailHelper.GetTrialEmailSubejctAndHtmlInfoAndBuildAsync(trialEmailConfig, messageToSend, emailConfigFunc);
+ await CommonEmailHelper.GetTrialEmailSubejctAndHtmlInfoAndBuildAsync(trialEmailConfig, messageToSend, emailConfigFunc, workLanguage);
await SendEmailHelper.SendEmailAsync(messageToSend, trialInfo);
+
}
+
+
}
}
}
@@ -364,7 +388,7 @@ public class CRCRepliedCheckChallengeEventConsumer(
private readonly SystemEmailSendConfig _systemEmailConfig = systemEmailConfig.CurrentValue;
public async Task Consume(ConsumeContext context)
{
- Console.WriteLine("发送(019,020) 【 CRC 回复一致性核查质疑 通知PM】邮件!!!");
+ Log.Logger.Warning("发送(019,020) 【 CRC 回复一致性核查质疑 通知PM】邮件!!!");
var isEn_US = CultureInfo.CurrentCulture.Name == StaticData.CultureInfo.en_US;
@@ -383,49 +407,58 @@ public class CRCRepliedCheckChallengeEventConsumer(
{
- var trialUserList = await _trialUseRoleRepository.Where(x => x.TrialId == subjectVisit.TrialId && x.TrialUser.IsDeleted == false).Include(x => x.UserRole).Select(x => x.UserRole).ToListAsync();
+ var trialUserList = await _trialUseRoleRepository.Where(x => x.TrialId == subjectVisit.TrialId && x.TrialUser.IsDeleted == false)
+ .Select(t => new { t.UserRole.IdentityUser.EMail, t.UserRole.FullName, t.UserRole.UserTypeEnum, t.UserRole.IdentityUser.UserWorkLanguage }).ToListAsync();
- var userinfoList = trialUserList.Where(x => x.UserTypeEnum == UserTypeEnum.APM || x.UserTypeEnum == UserTypeEnum.ProjectManager).ToList();
var trialInfo = await _trialRepository.FirstOrDefaultAsync(t => t.Id == subjectVisit.TrialId);
-
-
-
- var dictionValue = await CommonEmailHelper.TranslationDictionary(new TranslationDictionaryDto()
+ // 遍历语言枚举 在底层判断收件人是否为空,决定是否跳过邮件发送,避免每个邮件场景的地方都重复判断
+ foreach (UserWorkLanguage workLanguage in Enum.GetValues())
{
-
- DictionaryRepository = _dictionaryRepository,
- IsEn_US = isEn_US,
- DictionaryList = new List()
- {
- new DictionaryDto (){DictionaryCode= "CheckState",EnumValue=subjectVisit.CheckState.GetEnumInt(), }, //审核状态
-
- }
- });
+ //重新设置当前发送邮件的语言
+ isEn_US = workLanguage == UserWorkLanguage.US;
+
+
+
+ var dictionValue = await CommonEmailHelper.TranslationDictionary(new TranslationDictionaryDto()
+ {
+
+ DictionaryRepository = _dictionaryRepository,
+ IsEn_US = isEn_US,
+ DictionaryList = new List()
+ {
+ new DictionaryDto (){DictionaryCode= "CheckState",EnumValue=subjectVisit.CheckState.GetEnumInt(), }, //审核状态
+
+ }
+ });
+
+ var userinfoList = trialUserList.Where(x => x.UserTypeEnum == UserTypeEnum.APM || x.UserTypeEnum == UserTypeEnum.ProjectManager)
+ .Where(t => t.UserWorkLanguage == workLanguage).ToList();
- foreach (var userinfo in userinfoList)
- {
var messageToSend = new MimeMessage();
+
//发件地址
messageToSend.From.Add(new MailboxAddress(_systemEmailConfig.FromName, _systemEmailConfig.FromEmail));
- messageToSend.To.Add(new MailboxAddress(String.Empty, userinfo.EMail));
+ var toUserName = string.Join('、', userinfoList.Select(t => t.FullName));
+ foreach (var userinfo in userinfoList)
+ {
+ messageToSend.To.Add(new MailboxAddress(userinfo.FullName, userinfo.EMail));
+ }
var companyName = isEn_US ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
-
-
Func<(string topicStr, string htmlBodyStr), (string topicStr, string htmlBodyStr)> emailConfigFunc = input =>
{
var subjectCode = subjectVisit.Subject.Code;
var topicStr = string.Format(input.topicStr, trialInfo.ResearchProgramNo, subjectCode, subjectVisit.VisitName);
var htmlBodyStr = string.Format(
- CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr),
- userinfo.FullName, // 用户名 {0}
+ CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr, workLanguage),
+ toUserName, // 用户名 {0}
trialInfo.ExperimentName, // 项目 {1}
subjectCode, // 受试者 {2}
subjectVisit.VisitName, // 访视 {3}
@@ -437,10 +470,17 @@ public class CRCRepliedCheckChallengeEventConsumer(
};
- await CommonEmailHelper.GetTrialEmailSubejctAndHtmlInfoAndBuildAsync(trialEmailConfig, messageToSend, emailConfigFunc);
+ await CommonEmailHelper.GetTrialEmailSubejctAndHtmlInfoAndBuildAsync(trialEmailConfig, messageToSend, emailConfigFunc, workLanguage);
await SendEmailHelper.SendEmailAsync(messageToSend, trialInfo);
+
}
+
+
+
+
+
+
}
}
}
@@ -466,12 +506,12 @@ public class PMRepliedCheckChallengeEventConsumer(
private readonly SystemEmailSendConfig _systemEmailConfig = systemEmailConfig.CurrentValue;
public async Task Consume(ConsumeContext context)
{
- Console.WriteLine("发送(016,017) 【 PM 一致性核查 通知CRC】邮件!!!");
+ Log.Logger.Warning("发送(016,017) 【 PM 一致性核查 通知CRC】邮件!!!");
var isEn_US = CultureInfo.CurrentCulture.Name == StaticData.CultureInfo.en_US;
var subjectVisitId = context.Message.SubjectVisitId;
- var subjectVisit = await _subjectVisitRepository.Where(x => x.Id == subjectVisitId).Include(x => x.NoneDicomStudyList).Include(x => x.StudyList).Include(x => x.Subject).FirstNotNullAsync();
+ var subjectVisit = await _subjectVisitRepository.Where(x => x.Id == subjectVisitId).Include(x => x.NoneDicomStudyList).Include(x => x.StudyList).Include(x => x.Subject).Include(t => t.TrialSite).FirstNotNullAsync();
var trialId = subjectVisit.TrialId;
@@ -486,11 +526,11 @@ public class PMRepliedCheckChallengeEventConsumer(
var checkChallengeDialog = await _checkChallengeDialogRepository.Where(x => x.Id == context.Message.CheckChallengeDialogId).FirstNotNullAsync();
- var trialUserList = await _trialUseRoleRepository.Where(x => x.TrialId == subjectVisit.TrialId && x.TrialUser.IsDeleted == false).Include(x => x.UserRole).Select(x => x.UserRole).ToListAsync();
+ var trialUserList = await _trialUseRoleRepository.Where(x => x.TrialId == subjectVisit.TrialId && x.TrialUser.IsDeleted == false)
+ .Select(t => new { t.UserRole.IdentityUser.EMail, t.UserRole.FullName, t.UserRole.UserTypeEnum, t.UserRole.IdentityUser.UserWorkLanguage }).ToListAsync();
- var userinfoList = trialUserList.Where(x => x.UserTypeEnum == UserTypeEnum.ClinicalResearchCoordinator).ToList();
- var craInfo = trialUserList.Where(x => x.UserTypeEnum == UserTypeEnum.CRA).FirstOrDefault();
+ var craInfoList = trialUserList.Where(x => x.UserTypeEnum == UserTypeEnum.CRA).ToList();
var trialInfo = await _trialRepository.FirstOrDefaultAsync(t => t.Id == subjectVisit.TrialId);
@@ -499,28 +539,46 @@ public class PMRepliedCheckChallengeEventConsumer(
modalities = subjectVisit.NoneDicomStudyList.Select(t => t.Modality)
.Union(subjectVisit.StudyList.Select(k => k.ModalityForEdit)).ToList();
- var dictionValue = await CommonEmailHelper.TranslationDictionary(new TranslationDictionaryDto()
+
+
+ // 遍历语言枚举 在底层判断收件人是否为空,决定是否跳过邮件发送,避免每个邮件场景的地方都重复判断
+ //foreach (UserWorkLanguage workLanguage in Enum.GetValues())
{
+ //重新设置当前发送邮件的语言
- DictionaryRepository = _dictionaryRepository,
- IsEn_US = isEn_US,
- DictionaryList = new List()
- {
- new DictionaryDto (){DictionaryCode= "CheckState",EnumValue=subjectVisit.CheckState.GetEnumInt(), }, //核查状态
+ isEn_US = subjectVisit.TrialSite.Country == StaticData.SiteCountry.US;
- }
- });
+ var workLanguage = isEn_US ? UserWorkLanguage.US : UserWorkLanguage.CN;
+
+ var userinfoList = trialUserList.Where(x => x.UserTypeEnum == UserTypeEnum.ClinicalResearchCoordinator).Where(t => t.UserWorkLanguage == workLanguage).ToList();
- foreach (var userinfo in userinfoList)
- {
+ var dictionValue = await CommonEmailHelper.TranslationDictionary(new TranslationDictionaryDto()
+ {
+
+ DictionaryRepository = _dictionaryRepository,
+ IsEn_US = isEn_US,
+ DictionaryList = new List()
+ {
+ new DictionaryDto (){DictionaryCode= "CheckState",EnumValue=subjectVisit.CheckState.GetEnumInt(), }, //核查状态
+
+ }
+ });
+
var messageToSend = new MimeMessage();
+
//发件地址
messageToSend.From.Add(new MailboxAddress(_systemEmailConfig.FromName, _systemEmailConfig.FromEmail));
- messageToSend.To.Add(new MailboxAddress(userinfo.FullName, userinfo.EMail));
- if (craInfo != null)
+
+ var toUserName = string.Join('、', userinfoList.Select(t => t.FullName));
+ foreach (var userinfo in userinfoList)
{
- messageToSend.Cc.Add(new MailboxAddress(craInfo.FullName, craInfo.EMail));
+ messageToSend.To.Add(new MailboxAddress(userinfo.FullName, userinfo.EMail));
+ }
+
+ foreach (var user in craInfoList)
+ {
+ messageToSend.Cc.Add(new MailboxAddress(user.FullName, user.EMail));
}
var companyName = isEn_US ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
@@ -531,8 +589,8 @@ public class PMRepliedCheckChallengeEventConsumer(
var subjectCode = subjectVisit.Subject.Code;
var topicStr = string.Format(input.topicStr, trialInfo.ResearchProgramNo, subjectCode, subjectVisit.VisitName);
var htmlBodyStr = string.Format(
- CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr),
- userinfo.FullName, // 用户名 {0}
+ CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr, workLanguage),
+ toUserName, // 用户名 {0}
trialInfo.ExperimentName, // 项目 {1}
subjectCode, // 受试者 {2}
@@ -547,10 +605,11 @@ public class PMRepliedCheckChallengeEventConsumer(
};
- await CommonEmailHelper.GetTrialEmailSubejctAndHtmlInfoAndBuildAsync(trialEmailConfig, messageToSend, emailConfigFunc);
+ await CommonEmailHelper.GetTrialEmailSubejctAndHtmlInfoAndBuildAsync(trialEmailConfig, messageToSend, emailConfigFunc, workLanguage);
await SendEmailHelper.SendEmailAsync(messageToSend, trialInfo);
}
+
}
}
}
@@ -592,49 +651,57 @@ public class CheckStateChangedToAuditEventConsumer(
- var trialUserList = await _trialUseRoleRepository.Where(x => x.TrialId == subjectVisit.TrialId && x.TrialUser.IsDeleted == false).Include(x => x.UserRole).Select(x => x.UserRole).ToListAsync();
+ var trialUserList = await _trialUseRoleRepository.Where(x => x.TrialId == subjectVisit.TrialId && x.TrialUser.IsDeleted == false)
+ .Select(t => new { t.UserRole.IdentityUser.EMail, t.UserRole.FullName, t.UserRole.UserTypeEnum, t.UserRole.IdentityUser.UserWorkLanguage }).ToListAsync();
- var userinfoList = trialUserList.Where(x => x.UserTypeEnum == UserTypeEnum.APM || x.UserTypeEnum == UserTypeEnum.ProjectManager).ToList();
var trialInfo = await _trialRepository.FirstOrDefaultAsync(t => t.Id == subjectVisit.TrialId);
-
-
- var dictionValue = await CommonEmailHelper.TranslationDictionary(new TranslationDictionaryDto()
+ // 遍历语言枚举 在底层判断收件人是否为空,决定是否跳过邮件发送,避免每个邮件场景的地方都重复判断
+ foreach (UserWorkLanguage workLanguage in Enum.GetValues())
{
+ //重新设置当前发送邮件的语言
+ isEn_US = workLanguage == UserWorkLanguage.US;
- DictionaryRepository = _dictionaryRepository,
- IsEn_US = isEn_US,
- DictionaryList = new List()
- {
- new DictionaryDto (){DictionaryCode= "CheckState",EnumValue=subjectVisit.CheckState.GetEnumInt(), }, //审核状态
+ var userinfoList = trialUserList.Where(x => x.UserTypeEnum == UserTypeEnum.APM || x.UserTypeEnum == UserTypeEnum.ProjectManager)
+ .Where(t => t.UserWorkLanguage == workLanguage).ToList();
- }
- });
+ var dictionValue = await CommonEmailHelper.TranslationDictionary(new TranslationDictionaryDto()
+ {
+
+ DictionaryRepository = _dictionaryRepository,
+ IsEn_US = isEn_US,
+ DictionaryList = new List()
+ {
+ new DictionaryDto (){DictionaryCode= "CheckState",EnumValue=subjectVisit.CheckState.GetEnumInt(), }, //审核状态
+
+ }
+ });
- foreach (var userinfo in userinfoList)
- {
var messageToSend = new MimeMessage();
//发件地址
messageToSend.From.Add(new MailboxAddress(_systemEmailConfig.FromName, _systemEmailConfig.FromEmail));
- messageToSend.To.Add(new MailboxAddress(userinfo.FullName, userinfo.EMail));
+
+ var toUserName = string.Join('、', userinfoList.Select(t => t.FullName));
+
+ foreach (var userinfo in userinfoList)
+ {
+ messageToSend.To.Add(new MailboxAddress(userinfo.FullName, userinfo.EMail));
+ }
var companyName = isEn_US ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
-
-
-
Func<(string topicStr, string htmlBodyStr), (string topicStr, string htmlBodyStr)> emailConfigFunc = input =>
{
var subjectCode = subjectVisit.Subject.Code;
var topicStr = string.Format(input.topicStr, trialInfo.ResearchProgramNo, subjectCode, subjectVisit.VisitName);
var htmlBodyStr = string.Format(
- CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr),
- userinfo.FullName, // 姓名 {0}
+ CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr, workLanguage),
+ toUserName, // 姓名 {0}
trialInfo.ExperimentName, // 项目 {1}
subjectCode, // 受试者 {2}
subjectVisit.VisitName, // 访视 {3}
@@ -646,10 +713,11 @@ public class CheckStateChangedToAuditEventConsumer(
};
- await CommonEmailHelper.GetTrialEmailSubejctAndHtmlInfoAndBuildAsync(trialEmailConfig, messageToSend, emailConfigFunc);
+ await CommonEmailHelper.GetTrialEmailSubejctAndHtmlInfoAndBuildAsync(trialEmailConfig, messageToSend, emailConfigFunc, workLanguage);
await SendEmailHelper.SendEmailAsync(messageToSend, trialInfo);
}
+
}
}
}
@@ -671,7 +739,7 @@ public class QCClaimTaskEventConsumer(
private readonly SystemEmailSendConfig _systemEmailConfig = systemEmailConfig.CurrentValue;
public async Task Consume(ConsumeContext context)
{
- Console.WriteLine("发送(Code007,008) 【QC 领取了质控任务】邮件!!!");
+ Log.Logger.Warning("发送(Code007,008) 【QC 领取了质控任务】邮件!!!");
var isEn_US = CultureInfo.CurrentCulture.Name == StaticData.CultureInfo.en_US;
@@ -708,6 +776,10 @@ public class QCClaimTaskEventConsumer(
return;
}
+ //重新设置当前发送邮件的语言
+ var workLanguage = _userRoleRepository.Where(t => t.Id == userinfo.Id).Select(t => t.IdentityUser.UserWorkLanguage).First();
+ isEn_US = workLanguage == UserWorkLanguage.US;
+
var messageToSend = new MimeMessage();
//发件地址
messageToSend.From.Add(new MailboxAddress(_systemEmailConfig.FromName, _systemEmailConfig.FromEmail));
@@ -745,7 +817,7 @@ public class QCClaimTaskEventConsumer(
var subjectCode = subjectVisit.Subject.Code;
var topicStr = string.Format(input.topicStr, trialInfo.ResearchProgramNo, subjectCode, subjectVisit.VisitName);
var htmlBodyStr = string.Format(
- CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr),
+ CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr, workLanguage),
userinfo.FullName, // 用户名 {0}
trialInfo.ExperimentName, // 项目 {1}
string.Join(',', subjectcodes), // 受试者 {2}
@@ -759,7 +831,7 @@ public class QCClaimTaskEventConsumer(
};
- await CommonEmailHelper.GetTrialEmailSubejctAndHtmlInfoAndBuildAsync(trialEmailConfig, messageToSend, emailConfigFunc);
+ await CommonEmailHelper.GetTrialEmailSubejctAndHtmlInfoAndBuildAsync(trialEmailConfig, messageToSend, emailConfigFunc, workLanguage);
await SendEmailHelper.SendEmailAsync(messageToSend, trialInfo);
}
diff --git a/IRaCIS.Core.Application/MassTransit/Recurring/IRRecurringConsumer.cs b/IRaCIS.Core.Application/MassTransit/Recurring/IRRecurringConsumer.cs
index 791843c2b..7c62558c1 100644
--- a/IRaCIS.Core.Application/MassTransit/Recurring/IRRecurringConsumer.cs
+++ b/IRaCIS.Core.Application/MassTransit/Recurring/IRRecurringConsumer.cs
@@ -74,13 +74,17 @@ namespace IRaCIS.Core.Application.MassTransit.Recurring
t.UserRole.IdentityUser.EMail,
t.UserRole.IdentityUser.UserName,
t.Trial.TrialCode,
- t.Trial.ResearchProgramNo
+ t.Trial.ResearchProgramNo,
+ t.UserRole.IdentityUser.UserWorkLanguage
//TrialReadingCriterionList = t.Trial.TrialReadingCriterionList.Select(t => new { t.CriterionName, TrialReadingCriterionId = t.Id }).ToList()
});
foreach (var trialUser in trialUserList)
{
+ //重新设置当前发送邮件的语言
+ var workLanguage = trialUser.UserWorkLanguage;
+ isEn_US = workLanguage == UserWorkLanguage.US;
var userId = trialUser.UserId;
@@ -153,7 +157,7 @@ namespace IRaCIS.Core.Application.MassTransit.Recurring
- var htmlBodyStr = string.Format(CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, emailContent),
+ var htmlBodyStr = string.Format(CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, emailContent, workLanguage),
trialUser.FullName,
DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
@@ -167,7 +171,7 @@ namespace IRaCIS.Core.Application.MassTransit.Recurring
return (topicStr, htmlBodyStr);
};
- await CommonEmailHelper.GetTrialEmailSubejctAndHtmlInfoAndBuildAsync(trialEmailConfig, messageToSend, emailConfigFunc);
+ await CommonEmailHelper.GetTrialEmailSubejctAndHtmlInfoAndBuildAsync(trialEmailConfig, messageToSend, emailConfigFunc, workLanguage);
await SendEmailHelper.SendEmailAsync(messageToSend, trialInfo);
diff --git a/IRaCIS.Core.Application/MassTransit/Recurring/QCRecurringEmailConsumer.cs b/IRaCIS.Core.Application/MassTransit/Recurring/QCRecurringEmailConsumer.cs
index b3862f003..028ec03e3 100644
--- a/IRaCIS.Core.Application/MassTransit/Recurring/QCRecurringEmailConsumer.cs
+++ b/IRaCIS.Core.Application/MassTransit/Recurring/QCRecurringEmailConsumer.cs
@@ -44,12 +44,17 @@ public class QCImageQuestionRecurringEventConsumer(IRepository _trialRepo
var trialInfo = await _trialRepository.Where(t => t.Id == trialId).Select(t => new { t.ResearchProgramNo, t.ExperimentName, t.TrialCode, t.TrialStatusStr }).FirstNotNullAsync();
//找到 该项目的CRC 用户Id
- var userList = await _trialUserRoleRepository.Where(t => t.TrialId == trialId).Where(t => t.UserRole.UserTypeEnum == UserTypeEnum.ClinicalResearchCoordinator).Select(t => new { t.UserId, t.UserRole.FullName }).ToListAsync();
+ var userList = await _trialUserRoleRepository.Where(t => t.TrialId == trialId).Where(t => t.UserRole.UserTypeEnum == UserTypeEnum.ClinicalResearchCoordinator).Select(t => new { t.UserId, t.UserRole.FullName, t.UserRole.IdentityUser.UserWorkLanguage }).ToListAsync();
//判断是否任务可以领取 ,可以的话 发送邮件
foreach (var user in userList)
{
+
+ //重新设置当前发送邮件的语言
+ var workLanguage = user.UserWorkLanguage;
+ isEn_us = workLanguage == UserWorkLanguage.US;
+
var userId = user.UserId;
//过滤项目 并且 将 _userInfo.Id 换位 当前发送邮件的Id
var query = _trialRepository.Where(t => t.Id == trialId)
@@ -72,7 +77,7 @@ public class QCImageQuestionRecurringEventConsumer(IRepository _trialRepo
{
var topicStr = string.Format(isEn_us ? trialEmailConfig.EmailTopic : trialEmailConfig.EmailTopicCN, trialInfo.ResearchProgramNo);
var htmlContent = isEn_us ? trialEmailConfig.EmailHtmlContent : trialEmailConfig.EmailHtmlContentCN;
- var htmlBodyStr = string.Format(CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, htmlContent),
+ var htmlBodyStr = string.Format(CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, htmlContent, workLanguage),
user.FullName, DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), sendStat.ToBeDealedCount, _SystemEmailSendConfig.CurrentValue.SiteUrl);
return (topicStr, htmlBodyStr, isEn_us, userId);
@@ -116,12 +121,17 @@ public class CRCImageQuestionRecurringEventConsumer(IRepository _trialRep
var trialInfo = await _trialRepository.Where(t => t.Id == trialId).Select(t => new { t.ResearchProgramNo, t.ExperimentName, t.TrialCode, t.TrialStatusStr, t.DeclarationTypeEnumList }).FirstNotNullAsync();
//找到 该项目的IQC 用户Id
- var userList = await _trialUserRoleRepository.Where(t => t.TrialId == trialId).Where(t => t.UserRole.UserTypeEnum == UserTypeEnum.IQC).Select(t => new { t.UserId, t.UserRole.FullName }).ToListAsync();
+ var userList = await _trialUserRoleRepository.Where(t => t.TrialId == trialId).Where(t => t.UserRole.UserTypeEnum == UserTypeEnum.IQC).Select(t => new { t.UserId, t.UserRole.FullName, t.UserRole.IdentityUser.UserWorkLanguage }).ToListAsync();
//判断是否任务可以领取 ,可以的话 发送邮件
foreach (var user in userList)
{
+
+ //重新设置当前发送邮件的语言
+ var workLanguage = user.UserWorkLanguage;
+ isEn_us = workLanguage == UserWorkLanguage.US;
+
var userId = user.UserId;
//过滤项目 并且 将 _userInfo.Id 换位 当前发送邮件的Id
@@ -147,7 +157,7 @@ public class CRCImageQuestionRecurringEventConsumer(IRepository _trialRep
{
var topicStr = string.Format(isEn_us ? trialEmailConfig.EmailTopic : trialEmailConfig.EmailTopicCN, trialInfo.ResearchProgramNo);
var htmlContent = isEn_us ? trialEmailConfig.EmailHtmlContent : trialEmailConfig.EmailHtmlContentCN;
- var htmlBodyStr = string.Format(CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, htmlContent),
+ var htmlBodyStr = string.Format(CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, htmlContent, workLanguage),
user.FullName, DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), sendStat.ToBeDealedCount - sendStat.ReUploadTobeDealedCount, sendStat.ReUploadTobeDealedCount, _SystemEmailSendConfig.CurrentValue.SiteUrl);
@@ -181,7 +191,7 @@ public class ImageQCRecurringEventConsumer(IRepository _trialRepository,
private readonly SystemEmailSendConfig _systemEmailConfig = systemEmailConfig.CurrentValue;
public async Task Consume(ConsumeContext context)
{
- var trialId=context.Message.TrialId;
+ var trialId = context.Message.TrialId;
var isEn_us = context.Message.CultureInfoName == StaticData.CultureInfo.en_US;
@@ -192,13 +202,17 @@ public class ImageQCRecurringEventConsumer(IRepository _trialRepository,
var trialInfo = await _trialRepository.Where(t => t.Id == trialId).Select(t => new { t.ResearchProgramNo, t.ExperimentName, t.TrialCode, t.TrialStatusStr }).FirstNotNullAsync();
//找到 该项目的IQC 用户Id
- var userList = await _trialUserRoleRepository.Where(t => t.TrialId == trialId).Where(t => t.UserRole.UserTypeEnum == UserTypeEnum.IQC).Select(t => new { t.UserId, t.UserRole.FullName }).ToListAsync();
+ var userList = await _trialUserRoleRepository.Where(t => t.TrialId == trialId).Where(t => t.UserRole.UserTypeEnum == UserTypeEnum.IQC).Select(t => new { t.UserId, t.UserRole.FullName, t.UserRole.IdentityUser.UserWorkLanguage }).ToListAsync();
//判断是否任务可以领取 ,可以的话 发送邮件
var userIdList = userList.Select(t => t.UserId).ToList();
foreach (var user in userList)
{
+ //重新设置当前发送邮件的语言
+ var workLanguage = user.UserWorkLanguage;
+ isEn_us = workLanguage == UserWorkLanguage.US;
+
var userId = user.UserId;
//过滤项目 并且 将 _userInfo.Id 换位 当前发送邮件的Id
@@ -222,11 +236,11 @@ public class ImageQCRecurringEventConsumer(IRepository _trialRepository,
Func topicAndHtmlFunc = trialEmailConfig =>
{
-
+
var topicStr = string.Format(isEn_us ? trialEmailConfig.EmailTopic : trialEmailConfig.EmailTopicCN, trialInfo.ResearchProgramNo);
var htmlContent = isEn_us ? trialEmailConfig.EmailHtmlContent : trialEmailConfig.EmailHtmlContentCN;
- var htmlBodyStr = string.Format(CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, htmlContent),
+ var htmlBodyStr = string.Format(CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, htmlContent, workLanguage),
user.FullName, DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), sendStat.ToBeClaimedCount, sendStat.ToBeReviewedCount, _SystemEmailSendConfig.CurrentValue.SiteUrl);
return (topicStr, htmlBodyStr, false, userId);
diff --git a/IRaCIS.Core.Application/MassTransit/Recurring/SystemDocumentConsumer.cs b/IRaCIS.Core.Application/MassTransit/Recurring/SystemDocumentConsumer.cs
index 4497bf8f6..ddf05061d 100644
--- a/IRaCIS.Core.Application/MassTransit/Recurring/SystemDocumentConsumer.cs
+++ b/IRaCIS.Core.Application/MassTransit/Recurring/SystemDocumentConsumer.cs
@@ -5,6 +5,7 @@ using IRaCIS.Core.Application.Helper;
using IRaCIS.Core.Application.MassTransit.Consumer;
using IRaCIS.Core.Application.Service.Reading.Dto;
using IRaCIS.Core.Domain.Models;
+using IRaCIS.Core.Domain.Share;
using MassTransit;
using Microsoft.Extensions.Options;
using MimeKit;
@@ -84,203 +85,213 @@ namespace IRaCIS.Core.Application.MassTransit.Recurring
var userinfoList = await _identityUserRepository.Where(x => confirmUserIdList.Contains(x.Id)).ToListAsync();
Console.WriteLine("发送定时过期提醒:人员数量" + userinfoList.Count);
- int index = 1;
- foreach (var userinfo in userinfoList)
+
+ var scenario = EmailBusinessScenario.GeneralTraining_ExpirationNotification;
+ var emailConfig = _emailNoticeConfigrepository.Where(t => t.BusinessScenarioEnum == scenario && t.IsAutoSend && t.IsEnable).FirstOrDefault();
+ if (emailConfig == null)
+ {
+ return;
+ }
+
+ var userWorkLanguageList = userinfoList.Select(x => x.UserWorkLanguage).Distinct().ToList();
+ foreach (var workLanguage in userWorkLanguageList)
{
try
{
- Console.WriteLine($"{index}发送定时过期提醒,邮箱:{userinfo.EMail},姓名{userinfo.UserName}");
- index++;
+ var langUserInfoList = userinfoList.Where(x => x.UserWorkLanguage == workLanguage).ToList();
+ if (!langUserInfoList.Any())
+ {
+ continue;
+ }
+
+ var userIsEn_US = workLanguage == UserWorkLanguage.US;
+
var messageToSend = new MimeMessage();
- //发件地址
messageToSend.From.Add(new MailboxAddress(_systemEmailConfig.FromName, _systemEmailConfig.FromEmail));
- messageToSend.To.Add(new MailboxAddress(userinfo.FullName, userinfo.EMail));
+ foreach (var userinfo in langUserInfoList)
+ {
+ messageToSend.To.Add(new MailboxAddress(userinfo.FullName, userinfo.EMail));
+ }
+ var companyName = userIsEn_US ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
+ var userNames = langUserInfoList.Select(x => x.UserName).ToList();
- var companyName = isEn_US ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
Func<(string topicStr, string htmlBodyStr), (string topicStr, string htmlBodyStr)> emailConfigFunc = input =>
{
var topicStr = string.Format(input.topicStr, companyName);
var htmlBodyStr = string.Format(
- CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr),
- userinfo.UserName, // 用户名 {0}
+ CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr, workLanguage),
+ string.Join(',', userNames),
_systemEmailConfig.SiteUrl
);
return (topicStr, htmlBodyStr);
};
- var scenario = EmailBusinessScenario.GeneralTraining_ExpirationNotification;
-
- var emailConfig = _emailNoticeConfigrepository.Where(t => t.BusinessScenarioEnum == scenario && t.IsAutoSend && t.IsEnable).FirstOrDefault();
-
- if (emailConfig != null)
- {
- await CommonEmailHelper.GetEmailSubejctAndHtmlInfoAndBuildAsync(emailConfig, messageToSend, emailConfigFunc);
-
- await SendEmailHelper.SendEmailAsync(messageToSend, _systemEmailConfig);
- }
-
-
+ await CommonEmailHelper.GetEmailSubejctAndHtmlInfoAndBuildAsync(emailConfig, messageToSend, emailConfigFunc, workLanguage);
+ await SendEmailHelper.SendEmailAsync(messageToSend, _systemEmailConfig);
}
catch (Exception)
{
-
}
-
}
-
}
- }
- ///
- /// 生效通知
- ///
- public class SystemDocumentPublishEventConsumer(
- IRepository _trialReadingCriterionRepository,
- IRepository _visitTaskRepository,
- IRepository _systemDocumentRepository,
- IRepository _identityUserRepository,
- IRepository _systemDocConfirmedUserRepository,
- IRepository _dictionaryRepository,
- IRepository _trialUserRoleRepository,
- IRepository _emailNoticeConfigrepository,
+ ///
+ /// 生效通知
+ ///
+ public class SystemDocumentPublishEventConsumer(
+ IRepository _trialReadingCriterionRepository,
+ IRepository _visitTaskRepository,
+ IRepository _systemDocumentRepository,
+ IRepository _identityUserRepository,
+ IRepository _systemDocConfirmedUserRepository,
+ IRepository _dictionaryRepository,
+ IRepository _trialUserRoleRepository,
+ IRepository _emailNoticeConfigrepository,
- IOptionsMonitor systemEmailConfig) : IConsumer
- {
- private readonly SystemEmailSendConfig _systemEmailConfig = systemEmailConfig.CurrentValue;
-
- public async Task Consume(ConsumeContext context)
+ IOptionsMonitor systemEmailConfig) : IConsumer
{
- var isEn_US = context.Message.CultureInfoName == StaticData.CultureInfo.en_US;
+ private readonly SystemEmailSendConfig _systemEmailConfig = systemEmailConfig.CurrentValue;
- //设置当前事件传递过来的语言
- var culture = context.Message.CultureInfoName;
- CultureInfo.CurrentCulture = new CultureInfo(culture);
-
- // 记录是否只发送给新增角色的日志
- if (context.Message.NewUserTypeIds != null && context.Message.NewUserTypeIds.Any())
+ public async Task Consume(ConsumeContext context)
{
- Console.WriteLine($"只发送给新增的角色,角色数量: {context.Message.NewUserTypeIds.Count}");
- }
- // 构建查询
- IQueryable systemDocQuery;
+ var isEn_US = context.Message.CultureInfoName == StaticData.CultureInfo.en_US;
- if (context.Message.NewUserTypeIds != null && context.Message.NewUserTypeIds.Any())
- {
- // 只查询新增角色的用户
- systemDocQuery =
- from sysDoc in _systemDocumentRepository.AsQueryable(false).Where(x => context.Message.Ids.Contains(x.Id))
- from identityUser in _identityUserRepository.AsQueryable(false)
- .Where(t => t.Status == UserStateEnum.Enable &&
- t.UserRoleList.Where(t => t.IsUserRoleDisabled == false)
- .Any(t => context.Message.NewUserTypeIds.Contains(t.UserTypeId) &&
- sysDoc.NeedConfirmedUserTypeList.AsQueryable().Any(c => c.NeedConfirmUserTypeId == t.UserTypeId)))
- select new UnionDocumentWithConfirmInfoView()
+ //设置当前事件传递过来的语言
+ var culture = context.Message.CultureInfoName;
+ CultureInfo.CurrentCulture = new CultureInfo(culture);
+
+ // 记录是否只发送给新增角色的日志
+ if (context.Message.NewUserTypeIds != null && context.Message.NewUserTypeIds.Any())
{
- IsSystemDoc = true,
- Id = sysDoc.Id,
- CreateTime = sysDoc.CreateTime,
- IsDeleted = sysDoc.IsDeleted,
- SignViewMinimumMinutes = sysDoc.SignViewMinimumMinutes,
- Name = sysDoc.Name,
- Path = sysDoc.Path,
- FileTypeId = sysDoc.FileTypeId,
- UpdateTime = sysDoc.UpdateTime,
- ConfirmUserId = identityUser.Id,
- RealName = identityUser.FullName,
- UserName = identityUser.UserName,
- IsNeedSendEmial = identityUser.IsZhiZhun || (!identityUser.IsZhiZhun && sysDoc.DocUserSignType == DocUserSignType.InnerAndOuter),
- FullFilePath = sysDoc.Path
- };
- }
- else
- {
- // 查询所有相关角色的用户
- systemDocQuery =
- from sysDoc in _systemDocumentRepository.AsQueryable(false).Where(x => context.Message.Ids.Contains(x.Id))
- 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)))
- select new UnionDocumentWithConfirmInfoView()
+ Console.WriteLine($"只发送给新增的角色,角色数量: {context.Message.NewUserTypeIds.Count}");
+ }
+ // 构建查询
+ IQueryable systemDocQuery;
+
+ if (context.Message.NewUserTypeIds != null && context.Message.NewUserTypeIds.Any())
{
- IsSystemDoc = true,
- Id = sysDoc.Id,
- CreateTime = sysDoc.CreateTime,
- IsDeleted = sysDoc.IsDeleted,
- SignViewMinimumMinutes = sysDoc.SignViewMinimumMinutes,
- Name = sysDoc.Name,
- Path = sysDoc.Path,
- FileTypeId = sysDoc.FileTypeId,
- UpdateTime = sysDoc.UpdateTime,
- ConfirmUserId = identityUser.Id,
- RealName = identityUser.FullName,
- UserName = identityUser.UserName,
- IsNeedSendEmial = identityUser.IsZhiZhun || (!identityUser.IsZhiZhun && sysDoc.DocUserSignType == DocUserSignType.InnerAndOuter),
- FullFilePath = sysDoc.Path
- };
- }
- var datalist = await systemDocQuery.IgnoreQueryFilters().Where(x => x.IsNeedSendEmial).ToListAsync();
-
- var confirmUserIdList = datalist.Select(t => t.ConfirmUserId).Distinct().ToList();
- var userinfoList = await _identityUserRepository.Where(x => confirmUserIdList.Contains(x.Id)).ToListAsync();
- int index = 1;
- foreach (var userinfo in userinfoList)
- {
- string msg = $"{index}生效通知,邮箱:{userinfo.EMail},姓名{userinfo.UserName},";
- index++;
- try
- {
-
- var messageToSend = new MimeMessage();
- //发件地址
- messageToSend.From.Add(new MailboxAddress(_systemEmailConfig.FromName, _systemEmailConfig.FromEmail));
- messageToSend.To.Add(new MailboxAddress(userinfo.FullName, userinfo.EMail));
-
-
-
- var companyName = isEn_US ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
- Func<(string topicStr, string htmlBodyStr), (string topicStr, string htmlBodyStr)> emailConfigFunc = input =>
+ // 只查询新增角色的用户
+ systemDocQuery =
+ from sysDoc in _systemDocumentRepository.AsQueryable(false).Where(x => context.Message.Ids.Contains(x.Id))
+ from identityUser in _identityUserRepository.AsQueryable(false)
+ .Where(t => t.Status == UserStateEnum.Enable &&
+ t.UserRoleList.Where(t => t.IsUserRoleDisabled == false)
+ .Any(t => context.Message.NewUserTypeIds.Contains(t.UserTypeId) &&
+ sysDoc.NeedConfirmedUserTypeList.AsQueryable().Any(c => c.NeedConfirmUserTypeId == t.UserTypeId)))
+ select new UnionDocumentWithConfirmInfoView()
{
- var topicStr = string.Format(input.topicStr, companyName);
-
- var htmlBodyStr = string.Format(
- CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr),
- userinfo.UserName, // 用户名 {0}
- _systemEmailConfig.SiteUrl
- );
-
- return (topicStr, htmlBodyStr);
+ IsSystemDoc = true,
+ Id = sysDoc.Id,
+ CreateTime = sysDoc.CreateTime,
+ IsDeleted = sysDoc.IsDeleted,
+ SignViewMinimumMinutes = sysDoc.SignViewMinimumMinutes,
+ Name = sysDoc.Name,
+ Path = sysDoc.Path,
+ FileTypeId = sysDoc.FileTypeId,
+ UpdateTime = sysDoc.UpdateTime,
+ ConfirmUserId = identityUser.Id,
+ RealName = identityUser.FullName,
+ UserName = identityUser.UserName,
+ IsNeedSendEmial = identityUser.IsZhiZhun || (!identityUser.IsZhiZhun && sysDoc.DocUserSignType == DocUserSignType.InnerAndOuter),
+ FullFilePath = sysDoc.Path
};
-
- var scenario = EmailBusinessScenario.GeneralTraining_EffectiveNotification;
-
- var emailConfig = _emailNoticeConfigrepository.Where(t => t.BusinessScenarioEnum == scenario && t.IsAutoSend && t.IsEnable).FirstOrDefault();
-
- if (emailConfig != null)
+ }
+ else
+ {
+ // 查询所有相关角色的用户
+ systemDocQuery =
+ from sysDoc in _systemDocumentRepository.AsQueryable(false).Where(x => context.Message.Ids.Contains(x.Id))
+ 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)))
+ select new UnionDocumentWithConfirmInfoView()
{
- await CommonEmailHelper.GetEmailSubejctAndHtmlInfoAndBuildAsync(emailConfig, messageToSend, emailConfigFunc);
+ IsSystemDoc = true,
+ Id = sysDoc.Id,
+ CreateTime = sysDoc.CreateTime,
+ IsDeleted = sysDoc.IsDeleted,
+ SignViewMinimumMinutes = sysDoc.SignViewMinimumMinutes,
+ Name = sysDoc.Name,
+ Path = sysDoc.Path,
+ FileTypeId = sysDoc.FileTypeId,
+ UpdateTime = sysDoc.UpdateTime,
+ ConfirmUserId = identityUser.Id,
+ RealName = identityUser.FullName,
+ UserName = identityUser.UserName,
+ IsNeedSendEmial = identityUser.IsZhiZhun || (!identityUser.IsZhiZhun && sysDoc.DocUserSignType == DocUserSignType.InnerAndOuter),
+ FullFilePath = sysDoc.Path
+ };
+ }
+ var datalist = await systemDocQuery.IgnoreQueryFilters().Where(x => x.IsNeedSendEmial).ToListAsync();
+ var confirmUserIdList = datalist.Select(t => t.ConfirmUserId).Distinct().ToList();
+ var userinfoList = await _identityUserRepository.Where(x => confirmUserIdList.Contains(x.Id)).ToListAsync();
+
+ var scenario = EmailBusinessScenario.GeneralTraining_EffectiveNotification;
+ var emailConfig = _emailNoticeConfigrepository.Where(t => t.BusinessScenarioEnum == scenario && t.IsAutoSend && t.IsEnable).FirstOrDefault();
+ if (emailConfig == null)
+ {
+ return;
+ }
+
+ var userWorkLanguageList = userinfoList.Select(x => x.UserWorkLanguage).Distinct().ToList();
+ foreach (var workLanguage in userWorkLanguageList)
+ {
+ var langUserInfoList = userinfoList.Where(x => x.UserWorkLanguage == workLanguage).ToList();
+ if (!langUserInfoList.Any())
+ {
+ continue;
+ }
+
+ string msg = $"{langUserInfoList.Count}生效通知,";
+ try
+ {
+ var messageToSend = new MimeMessage();
+ messageToSend.From.Add(new MailboxAddress(_systemEmailConfig.FromName, _systemEmailConfig.FromEmail));
+
+ foreach (var userinfo in langUserInfoList)
+ {
+ messageToSend.To.Add(new MailboxAddress(userinfo.FullName, userinfo.EMail));
+ }
+
+ var userIsEn_US = workLanguage == UserWorkLanguage.US;
+ var companyName = userIsEn_US ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
+ var userNames = langUserInfoList.Select(x => x.UserName).ToList();
+
+ Func<(string topicStr, string htmlBodyStr), (string topicStr, string htmlBodyStr)> emailConfigFunc = input =>
+ {
+ var topicStr = string.Format(input.topicStr, companyName);
+
+ var htmlBodyStr = string.Format(
+ CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr, workLanguage),
+ string.Join(',', userNames),
+ _systemEmailConfig.SiteUrl
+ );
+
+ return (topicStr, htmlBodyStr);
+ };
+ await CommonEmailHelper.GetEmailSubejctAndHtmlInfoAndBuildAsync(emailConfig, messageToSend, emailConfigFunc, workLanguage);
await SendEmailHelper.SendEmailAsync(messageToSend, _systemEmailConfig);
msg += "发送成功";
+
+ }
+ catch (Exception)
+ {
+ msg += "发送失败";
+
}
- }
- catch (Exception)
- {
- msg += "发送失败";
-
+ Console.WriteLine(msg);
}
-
- Console.WriteLine(msg);
}
-
}
}
}
diff --git a/IRaCIS.Core.Application/MassTransit/Recurring/TrialDocumentConsumer.cs b/IRaCIS.Core.Application/MassTransit/Recurring/TrialDocumentConsumer.cs
index a0e2607c7..57e78cd53 100644
--- a/IRaCIS.Core.Application/MassTransit/Recurring/TrialDocumentConsumer.cs
+++ b/IRaCIS.Core.Application/MassTransit/Recurring/TrialDocumentConsumer.cs
@@ -5,6 +5,7 @@ using IRaCIS.Core.Application.Helper;
using IRaCIS.Core.Application.MassTransit.Consumer;
using IRaCIS.Core.Application.Service.Reading.Dto;
using IRaCIS.Core.Domain.Models;
+using IRaCIS.Core.Domain.Share;
using MassTransit;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
@@ -108,46 +109,65 @@ namespace IRaCIS.Core.Application.MassTransit.Recurring
var userinfoList = await _identityUserRepository.Where(x => confirmUserIdList.Contains(x.Id)).ToListAsync();
Console.WriteLine("发送定时项目过期提醒:人员数量" + userinfoList.Count);
- int index = 1;
- foreach (var userinfo in userinfoList)
+
+ var scenario = EmailBusinessScenario.TrialTraining_ExpirationNotification;
+ var emailConfig = _emailNoticeConfigrepository.Where(t => t.BusinessScenarioEnum == scenario && t.IsAutoSend && t.IsEnable).FirstOrDefault();
+ if (emailConfig == null)
+ {
+ return;
+ }
+
+ var userTrialIdList = datalist.Select(t => new { t.ConfirmUserId, t.TrialId }).Distinct().ToList();
+ var userWithTrialList = (from ut in userTrialIdList
+ join u in userinfoList on ut.ConfirmUserId equals u.Id
+ select new { ut.TrialId, User = u }).ToList();
+
+ var groupedList = userWithTrialList.GroupBy(x => new { x.TrialId, x.User.UserWorkLanguage }).ToList();
+ foreach (var groupItem in groupedList)
{
try
{
- Console.WriteLine($"{index}发送定时过期提醒,邮箱:{userinfo.EMail},姓名{userinfo.UserName}");
- index++;
- var trialInfo = _trialRepository.Where(x => x.Id == userinfo.TrialId).FirstOrDefault();
+ var langUserInfoList = groupItem.Select(x => x.User).Distinct().ToList();
+ if (!langUserInfoList.Any())
+ {
+ continue;
+ }
+
+ Console.WriteLine($"发送定时过期提醒,数量:{langUserInfoList.Count}");
+
+ var trialInfo = await _trialRepository.Where(x => x.Id == groupItem.Key.TrialId).FirstOrDefaultAsync();
+ if (trialInfo == null)
+ {
+ continue;
+ }
+
+ var workLanguage = groupItem.Key.UserWorkLanguage;
+ var userIsEn_US = workLanguage == UserWorkLanguage.US;
var messageToSend = new MimeMessage();
- //发件地址
messageToSend.From.Add(new MailboxAddress(_systemEmailConfig.FromName, _systemEmailConfig.FromEmail));
- messageToSend.To.Add(new MailboxAddress(userinfo.FullName, userinfo.EMail));
+ foreach (var userinfo in langUserInfoList)
+ {
+ messageToSend.To.Add(new MailboxAddress(userinfo.FullName, userinfo.EMail));
+ }
-
- var companyName = isEn_US ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
+ var companyName = userIsEn_US ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
+ var userNames = langUserInfoList.Select(x => x.UserName).ToList();
Func<(string topicStr, string htmlBodyStr), (string topicStr, string htmlBodyStr)> emailConfigFunc = input =>
{
var topicStr = string.Format(input.topicStr, companyName);
var htmlBodyStr = string.Format(
- CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr),
- userinfo.UserName, // 用户名 {0}
+ CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr, workLanguage),
+ string.Join(',', userNames),
_systemEmailConfig.SiteUrl
);
return (topicStr, htmlBodyStr);
};
- var scenario = EmailBusinessScenario.TrialTraining_ExpirationNotification;
-
- var emailConfig = _emailNoticeConfigrepository.Where(t => t.BusinessScenarioEnum == scenario && t.IsAutoSend && t.IsEnable).FirstOrDefault();
-
- if (emailConfig != null)
- {
- await CommonEmailHelper.GetEmailSubejctAndHtmlInfoAndBuildAsync(emailConfig, messageToSend, emailConfigFunc);
-
- await SendEmailHelper.SendEmailAsync(messageToSend, trialInfo);
- }
-
+ await CommonEmailHelper.GetEmailSubejctAndHtmlInfoAndBuildAsync(emailConfig, messageToSend, emailConfigFunc, workLanguage);
+ await SendEmailHelper.SendEmailAsync(messageToSend, trialInfo);
}
catch (Exception)
@@ -264,52 +284,65 @@ namespace IRaCIS.Core.Application.MassTransit.Recurring
var confirmUserIdList = datalist.Select(t => t.ConfirmUserId).Distinct().ToList();
var userinfoList = await _identityUserRepository.Where(x => confirmUserIdList.Contains(x.Id)).ToListAsync();
- int index = 1;
- foreach (var userinfo in userinfoList)
+
+ var scenario = EmailBusinessScenario.TrialTraining_EffectiveNotification;
+ var emailConfig = _emailNoticeConfigrepository.Where(t => t.BusinessScenarioEnum == scenario && t.IsEnable).FirstOrDefault();
+ if (emailConfig == null)
{
- string msg = $"{index}项目生效通知,邮箱:{userinfo.EMail},姓名{userinfo.UserName},";
- index++;
+ return;
+ }
+
+ var userTrialIdList = datalist.Select(t => new { t.ConfirmUserId, t.TrialId }).Distinct().ToList();
+ var userWithTrialList = (from ut in userTrialIdList
+ join u in userinfoList on ut.ConfirmUserId equals u.Id
+ select new { ut.TrialId, User = u }).ToList();
+
+ var groupedList = userWithTrialList.GroupBy(x => new { x.TrialId, x.User.UserWorkLanguage }).ToList();
+ foreach (var groupItem in groupedList)
+ {
+ var langUserInfoList = groupItem.Select(x => x.User).Distinct().ToList();
+ if (!langUserInfoList.Any())
+ {
+ continue;
+ }
+
+ string msg = $"{langUserInfoList.Count}项目生效通知,";
try
{
-
var messageToSend = new MimeMessage();
- //发件地址
messageToSend.From.Add(new MailboxAddress(_systemEmailConfig.FromName, _systemEmailConfig.FromEmail));
- messageToSend.To.Add(new MailboxAddress(userinfo.FullName, userinfo.EMail));
+ foreach (var userinfo in langUserInfoList)
+ {
+ messageToSend.To.Add(new MailboxAddress(userinfo.FullName, userinfo.EMail));
+ }
-
- var companyName = isEn_US ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
+ var workLanguage = groupItem.Key.UserWorkLanguage;
+ var userIsEn_US = workLanguage == UserWorkLanguage.US;
+ var companyName = userIsEn_US ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
+ var userNames = langUserInfoList.Select(x => x.UserName).ToList();
Func<(string topicStr, string htmlBodyStr), (string topicStr, string htmlBodyStr)> emailConfigFunc = input =>
{
var topicStr = string.Format(input.topicStr, companyName);
var htmlBodyStr = string.Format(
- CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr),
- userinfo.UserName, // 用户名 {0}
+ CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr, workLanguage),
+ string.Join(',', userNames),
_systemEmailConfig.SiteUrl
);
return (topicStr, htmlBodyStr);
};
+ await CommonEmailHelper.GetEmailSubejctAndHtmlInfoAndBuildAsync(emailConfig, messageToSend, emailConfigFunc, workLanguage);
- var scenario = EmailBusinessScenario.TrialTraining_EffectiveNotification;
-
- var emailConfig = _emailNoticeConfigrepository.Where(t => t.BusinessScenarioEnum == scenario && t.IsEnable).FirstOrDefault();
-
- if (emailConfig != null)
+ var trialInfo = await _trialRepository.Where(x => x.Id == groupItem.Key.TrialId).FirstOrDefaultAsync();
+ if (trialInfo == null)
{
- await CommonEmailHelper.GetEmailSubejctAndHtmlInfoAndBuildAsync(emailConfig, messageToSend, emailConfigFunc);
-
- var trial = datalist.Where(x => x.ConfirmUserId == userinfo.Id).FirstOrDefault();
-
- var trialInfo = await _trialRepository.Where(x=>x.Id==trial.TrialId).FirstNotNullAsync();
-
-
- await SendEmailHelper.SendEmailAsync(messageToSend, trialInfo);
- msg += "发送成功";
+ continue;
}
+ await SendEmailHelper.SendEmailAsync(messageToSend, trialInfo);
+ msg += "发送成功";
}
catch (Exception)
diff --git a/IRaCIS.Core.Application/Service/Allocation/TaskMedicalReviewService.cs b/IRaCIS.Core.Application/Service/Allocation/TaskMedicalReviewService.cs
index 2a8acf366..57db0a338 100644
--- a/IRaCIS.Core.Application/Service/Allocation/TaskMedicalReviewService.cs
+++ b/IRaCIS.Core.Application/Service/Allocation/TaskMedicalReviewService.cs
@@ -54,8 +54,8 @@ namespace IRaCIS.Core.Application.Service
.WhereIf(inQuery.DoctorUserIdeaEnum != null, t => t.DoctorUserIdeaEnum == inQuery.DoctorUserIdeaEnum)
.WhereIf(inQuery.TrialReadingCriterionId != null, t => t.VisitTask.TrialReadingCriterionId == inQuery.TrialReadingCriterionId)
- .WhereIf(inQuery.IsInvalid != null, t => t.IsInvalid == inQuery.IsInvalid)
- .WhereIf(inQuery.IsHaveQuestion != null, t => t.IsHaveQuestion == inQuery.IsHaveQuestion)
+ .WhereIf(inQuery.IsInvalid != null, t => t.IsInvalid == inQuery.IsInvalid)
+ .WhereIf(inQuery.IsHaveQuestion != null, t => t.IsHaveQuestion == inQuery.IsHaveQuestion)
.WhereIf(inQuery.BeginAllocateDate != null, t => t.AllocateTime >= inQuery.BeginAllocateDate)
.WhereIf(inQuery.EndAllocateDate != null, t => t.AllocateTime <= inQuery.EndAllocateDate)
@@ -211,23 +211,23 @@ namespace IRaCIS.Core.Application.Service
.WhereIf(inQuery.SubjectId != null, t => t.VisitTask.SubjectId == inQuery.SubjectId)
.WhereIf(inQuery.TrialSiteId != null, t => t.VisitTask.Subject.TrialSiteId == inQuery.TrialSiteId)
.WhereIf(inQuery.IsUrgent != null, t => t.VisitTask.IsUrgent == inQuery.IsUrgent)
- .WhereIf(inQuery.AuditState != null, t => t.AuditState == inQuery.AuditState)
- .WhereIf(inQuery.DoctorUserIdeaEnum != null, t => t.DoctorUserIdeaEnum == inQuery.DoctorUserIdeaEnum)
- .WhereIf(inQuery.TaskState != null, t => t.VisitTask.TaskState == inQuery.TaskState)
- .WhereIf(!string.IsNullOrEmpty(inQuery.SubjectCode), t => t.VisitTask.Subject.Code.Contains(inQuery.SubjectCode) || t.VisitTask.Subject.MedicalNo.Contains(inQuery.SubjectCode))
- .WhereIf(!string.IsNullOrEmpty(inQuery.TaskName), t => t.VisitTask.TaskName.Contains(inQuery.TaskName) || t.VisitTask.TaskBlindName.Contains(inQuery.TaskName))
+ .WhereIf(inQuery.AuditState != null, t => t.AuditState == inQuery.AuditState)
+ .WhereIf(inQuery.DoctorUserIdeaEnum != null, t => t.DoctorUserIdeaEnum == inQuery.DoctorUserIdeaEnum)
+ .WhereIf(inQuery.TaskState != null, t => t.VisitTask.TaskState == inQuery.TaskState)
+ .WhereIf(!string.IsNullOrEmpty(inQuery.SubjectCode), t => t.VisitTask.Subject.Code.Contains(inQuery.SubjectCode) || t.VisitTask.Subject.MedicalNo.Contains(inQuery.SubjectCode))
+ .WhereIf(!string.IsNullOrEmpty(inQuery.TaskName), t => t.VisitTask.TaskName.Contains(inQuery.TaskName) || t.VisitTask.TaskBlindName.Contains(inQuery.TaskName))
.WhereIf(inQuery.DoctorUserId != null, t => t.VisitTask.DoctorUserId == inQuery.DoctorUserId)
.WhereIf(inQuery.ReadingCategory != null, t => t.VisitTask.ReadingCategory == inQuery.ReadingCategory)
.WhereIf(inQuery.ReadingTaskState != null, t => t.VisitTask.ReadingTaskState == inQuery.ReadingTaskState)
- .WhereIf(inQuery.IsInvalid != null, t => t.IsInvalid == inQuery.IsInvalid)
+ .WhereIf(inQuery.IsInvalid != null, t => t.IsInvalid == inQuery.IsInvalid)
.WhereIf(inQuery.IsGetBeRead, x => !x.IsInvalid && x.AuditState != MedicalReviewAuditState.HaveSigned)
- .WhereIf(inQuery.TrialReadingCriterionId != null, t => t.VisitTask.TrialReadingCriterionId == inQuery.TrialReadingCriterionId)
- .WhereIf(inQuery.BeginAuditSignTime != null, t => t.AuditSignTime >= inQuery.BeginAuditSignTime)
- .WhereIf(inQuery.EndAuditSignTime != null, t => t.AuditSignTime <= inQuery.EndAuditSignTime)
- .WhereIf(inQuery.BeginAllocateDate != null, t => t.AllocateTime >= inQuery.BeginAllocateDate)
+ .WhereIf(inQuery.TrialReadingCriterionId != null, t => t.VisitTask.TrialReadingCriterionId == inQuery.TrialReadingCriterionId)
+ .WhereIf(inQuery.BeginAuditSignTime != null, t => t.AuditSignTime >= inQuery.BeginAuditSignTime)
+ .WhereIf(inQuery.EndAuditSignTime != null, t => t.AuditSignTime <= inQuery.EndAuditSignTime)
+ .WhereIf(inQuery.BeginAllocateDate != null, t => t.AllocateTime >= inQuery.BeginAllocateDate)
.WhereIf(inQuery.EndAllocateDate != null, t => t.AllocateTime <= inQuery.EndAllocateDate)
- .WhereIf(inQuery.ArmEnum != null, t => t.VisitTask.ArmEnum == inQuery.ArmEnum)
+ .WhereIf(inQuery.ArmEnum != null, t => t.VisitTask.ArmEnum == inQuery.ArmEnum)
.WhereIf(inQuery.MedicalManagerUserId != null, t => t.MedicalManagerUserId == inQuery.MedicalManagerUserId)
.WhereIf(inQuery.BeginSignTime != null, t => t.VisitTask.SignTime > inQuery.BeginSignTime)
.WhereIf(inQuery.EndSignTime != null, t => t.VisitTask.SignTime < inQuery.EndSignTime)
diff --git a/IRaCIS.Core.Application/Service/Allocation/VisitTaskService.cs b/IRaCIS.Core.Application/Service/Allocation/VisitTaskService.cs
index 027b543aa..ca5e3c1e1 100644
--- a/IRaCIS.Core.Application/Service/Allocation/VisitTaskService.cs
+++ b/IRaCIS.Core.Application/Service/Allocation/VisitTaskService.cs
@@ -43,6 +43,9 @@ public class VisitTaskService(IRepository _visitTaskRepository,
IRepository _trialReadingCriterionRepository,
IRepository _readingQuestionCriterionTrialRepository,
IRepository _readingTaskQuestionAnswerRepository,
+ IRepository _segmentRepository,
+ IRepository _segmentationRepository,
+ IRepository _segmentBindingRepository,
IRepository _dicomInstanceRepository,
IRepository _dicomSeriesRepository,
IRepository _subjectCanceDoctorRepository,
@@ -1013,9 +1016,9 @@ public class VisitTaskService(IRepository _visitTaskRepository,
if (critrion.IsReadingTaskViewInOrder == ReadingOrder.InOrder)
{
- var extralObj = _trialRepository.Where(t => t.Id == trialId).Select(t => new { t.TrialExtraConfigJsonStr }).FirstOrDefault();
+ var extralObj = _trialRepository.Where(t => t.Id == trialId).Select(t => new { t.IsOpenLostVistRead }).FirstOrDefault();
- var extralConfig = JsonConvert.DeserializeObject(extralObj?.TrialExtraConfigJsonStr) ?? new TrialExtraConfig();
+ //var extralConfig = JsonConvert.DeserializeObject(extralObj?.TrialExtraConfigJsonStr) ?? new TrialExtraConfig();
var visitQuery = _visitTaskRepository.Where(x => x.TrialId == inQuery.TrialId && x.DoctorUserId == _userInfo.UserRoleId && x.TaskState == TaskState.Effect && x.Subject.IsSubjectQuit == false)
@@ -1024,7 +1027,7 @@ public class VisitTaskService(IRepository _visitTaskRepository,
.WhereIf(critrion.IsAutoCreate == false, t => !t.Subject.SubjectCriteriaEvaluationVisitFilterList.Where(t => t.TrialReadingCriterionId == trialReadingCriterionId).Any(f => f.IsGeneratedTask == false && t.VisitTaskNum > f.SubjectVisit.VisitNum))
// 前序 不存在 未一致性核查未通过的
- .Where(t => !t.Subject.SubjectVisitList.Where(t => extralConfig.IsOpenLostVistRead ? t.IsLostVisit == false : true).Any(sv => sv.CheckState != CheckStateEnum.CVPassed && t.VisitTaskNum >= sv.VisitNum))
+ .Where(t => !t.Subject.SubjectVisitList.Where(t => extralObj.IsOpenLostVistRead ? t.IsLostVisit == false : true).Any(sv => sv.CheckState != CheckStateEnum.CVPassed && t.VisitTaskNum >= sv.VisitNum))
//.WhereIf(critrion.IsAutoCreate == false, t => t.Subject.SubjectCriteriaEvaluationVisitFilterList.Where(t => t.TrialReadingCriterionId == trialReadingCriterionId).Any(t => t.IsGeneratedTask == false) ?
//t.VisitTaskNum <= t.Subject.SubjectCriteriaEvaluationVisitFilterList.Where(t => t.TrialReadingCriterionId == trialReadingCriterionId && t.IsGeneratedTask == false).Min(t => t.SubjectVisit.VisitNum) : true)
//.Where(t => t.Subject.SubjectVisitList.Any(t => t.CheckState != CheckStateEnum.CVPassed) ? t.VisitTaskNum <= t.Subject.SubjectVisitList.Where(t => t.CheckState != CheckStateEnum.CVPassed).Min(t => t.VisitNum) : true)
@@ -1047,14 +1050,14 @@ public class VisitTaskService(IRepository _visitTaskRepository,
UnReadTaskCount = x.Where(y => y.TrialReadingCriterionId == trialReadingCriterionId && y.ReadingTaskState != ReadingTaskState.HaveSigned).Count(),
//未读 里可读任务量
- UnReadCanReadTaskCount = x.Where(t => extralConfig.IsOpenLostVistRead ? true : t.IsFrontTaskNeedSignButNotSign == false).Where(y => y.TrialReadingCriterionId == trialReadingCriterionId && y.ReadingTaskState != ReadingTaskState.HaveSigned && (y.IsNeedClinicalDataSign == false || y.IsClinicalDataSign == true)
+ UnReadCanReadTaskCount = x.Where(t => extralObj.IsOpenLostVistRead ? true : t.IsFrontTaskNeedSignButNotSign == false).Where(y => y.TrialReadingCriterionId == trialReadingCriterionId && y.ReadingTaskState != ReadingTaskState.HaveSigned && (y.IsNeedClinicalDataSign == false || y.IsClinicalDataSign == true)
//不能对包含聚合或子查询的表达式执行聚合函数
//&& !x.Any(t => t.ReadingTaskState != ReadingTaskState.HaveSigned && t.IsNeedClinicalDataSign == true && t.IsClinicalDataSign == false && t.VisitTaskNum y.TrialReadingCriterionId == trialReadingCriterionId && y.ReadingTaskState != ReadingTaskState.HaveSigned)
- .Where(t => extralConfig.IsOpenLostVistRead ? true : t.IsFrontTaskNeedSignButNotSign == false)
+ .Where(t => extralObj.IsOpenLostVistRead ? true : t.IsFrontTaskNeedSignButNotSign == false)
.Where(y => /*y.IsFrontTaskNeedSignButNotSign == false &&*/ (y.IsNeedClinicalDataSign == false || y.IsClinicalDataSign == true))
.OrderBy(x => x.VisitTaskNum)
.Select(u => new IRUnreadTaskView()
@@ -2220,7 +2223,7 @@ public class VisitTaskService(IRepository _visitTaskRepository,
}
}
- private void CopyForms(VisitTask newTask, VisitTask origenalTask)
+ private async Task CopyForms(VisitTask newTask, VisitTask origenalTask)
{
newTask.IsCopyLesionAnswer = true;
//自定义
@@ -2236,6 +2239,7 @@ public class VisitTaskService(IRepository _visitTaskRepository,
//_ = _readingCustomTagRepository.AddRangeAsync(readingCustomTagList).Result;
+
var readingTaskQuestionAnswerList = _readingTaskQuestionAnswerRepository.Where(t => t.VisitTaskId == origenalTask.Id).ToList();
@@ -2294,6 +2298,64 @@ public class VisitTaskService(IRepository _visitTaskRepository,
}
}
+
+ #region 分割
+ Dictionary segmentationRelationship = new Dictionary() { };
+ var segmentationList = _segmentationRepository.Where(t => t.VisitTaskId == origenalTask.Id).ToList();
+
+ foreach (var item in segmentationList)
+ {
+
+ var newSegmentationId = NewId.NextSequentialGuid();
+ segmentationRelationship.Add(item.Id, newSegmentationId);
+ item.Id = newSegmentationId;
+ item.VisitTaskId = newTask.Id;
+ }
+
+
+ Dictionary segmentRelationship = new Dictionary() { };
+ var segmentList = _segmentRepository.Where(t => t.VisitTaskId == origenalTask.Id).ToList();
+ foreach (var item in segmentList)
+ {
+ var newSegmentationId = NewId.NextSequentialGuid();
+ segmentRelationship.Add(item.Id, newSegmentationId);
+ if (segmentationRelationship.ContainsKey(item.SegmentationId))
+ {
+ item.SegmentationId = segmentationRelationship[item.SegmentationId];
+ }
+ item.Id = newSegmentationId;
+ item.VisitTaskId = newTask.Id;
+ }
+
+ var segmentBindingList = _segmentBindingRepository.Where(x => x.VisitTaskId == origenalTask.Id).ToList();
+
+ foreach (var item in segmentBindingList)
+ {
+ if (segmentationRelationship.ContainsKey(item.SegmentationId))
+ {
+ item.SegmentationId = segmentationRelationship[item.SegmentationId];
+ }
+ if (segmentRelationship.ContainsKey(item.SegmentId))
+ {
+ item.SegmentId = segmentRelationship[item.SegmentId];
+ }
+
+ if (item.RowId != null && lesionRelationship.ContainsKey(item.RowId.Value))
+ {
+ item.RowId = lesionRelationship[item.RowId.Value];
+ }
+ item.Id = NewId.NextSequentialGuid();
+ item.VisitTaskId = newTask.Id;
+ }
+
+
+ _ = _segmentationRepository.AddRangeAsync(segmentationList).Result;
+ _ = _segmentRepository.AddRangeAsync(segmentList).Result;
+ _ = _segmentBindingRepository.AddRangeAsync(segmentBindingList).Result;
+ #endregion
+
+
+
foreach (var item in readingTableAnswerRowInfoList)
{
if (item.SplitRowId != null && lesionRelationship.ContainsKey(item.SplitRowId.Value))
@@ -2654,6 +2716,19 @@ public class VisitTaskService(IRepository _visitTaskRepository,
sv.ReviewAuditUserId = null;
sv.SecondReviewState = SecondReviewState.None;
+
+ // 处理阅片期的临床数据
+ var readModuleIdList =await _readModuleRepository.Where(x => x.SubjectVisitId == sv.Id).Select(x => x.Id).ToListAsync();
+
+
+ await _readingClinicalDataReposiotry.UpdatePartialFromQueryAsync(t => readModuleIdList.Contains(t.ReadingId), c => new ReadingClinicalData()
+ {
+ IsSign = false,
+ ReadingClinicalDataState = ReadingClinicalDataStatus.HaveUploaded,
+ });
+
+
+
if (sv.IsBaseLine)
{
await _readingClinicalDataReposiotry.UpdatePartialFromQueryAsync(t => t.ReadingId == sv.Id && (t.ClinicalDataTrialSet.ClinicalDataLevel == ClinicalLevel.Subject || t.ClinicalDataTrialSet.ClinicalDataLevel == ClinicalLevel.SubjectVisit), c => new ReadingClinicalData() { IsSign = false, ReadingClinicalDataState = ReadingClinicalDataStatus.HaveUploaded });
diff --git a/IRaCIS.Core.Application/Service/Common/DTO/FileUploadRecordViewModel.cs b/IRaCIS.Core.Application/Service/Common/DTO/FileUploadRecordViewModel.cs
new file mode 100644
index 000000000..d0c43d664
--- /dev/null
+++ b/IRaCIS.Core.Application/Service/Common/DTO/FileUploadRecordViewModel.cs
@@ -0,0 +1,234 @@
+
+//--------------------------------------------------------------------
+// 此代码由liquid模板自动生成 byzhouhang 20240909
+// 生成时间 2026-03-10 06:15:31Z
+// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。
+//--------------------------------------------------------------------
+using System;
+using IRaCIS.Core.Domain.Share;
+using System.Collections.Generic;
+namespace IRaCIS.Core.Application.ViewModel;
+
+public class SubjectFileUploadRecordView
+{
+ public string? SubjectCode { get; set; }
+
+ public string? VisitName { get; set; }
+
+ public string StudyCode { get; set; }
+
+ public Guid? SubjectId { get; set; }
+
+ public Guid? SubjectVisitId { get; set; }
+
+ public int FileCount { get; set; }
+
+
+ public string? UploadRegion { get; set; }
+ public string? TargetRegion { get; set; }
+
+
+ public DateTime CreateTime { get; set; }
+ public DateTime? SyncFinishedTime { get; set; }
+
+ public bool IsSync { get; set; }
+}
+
+public class FileUploadRecordView : FileUploadRecordAddOrEdit
+{
+
+ public DateTime CreateTime { get; set; }
+
+ public DateTime UpdateTime { get; set; }
+
+
+
+ [Comment("同步结束时间-最后一个任务的时间")]
+ public DateTime? SyncFinishedTime { get; set; }
+
+
+ public string? SubjectCode { get; set; }
+
+ public string? VisitName { get; set; }
+
+}
+
+
+public class FileUploadRecordAddOrEdit
+{
+ public Guid? Id { get; set; }
+
+
+ public string FileName { get; set; }
+
+ public long FileSize { get; set; }
+
+ public string FileType { get; set; }
+
+ public string Path { get; set; }
+
+
+ public string UploadBatchId { get; set; }
+ public BatchDataType BatchDataType { get; set; }
+
+ public string StudyCode { get; set; }
+
+ public Guid? TrialId { get; set; }
+
+ public Guid? SubjectId { get; set; }
+
+ public Guid? SubjectVisitId { get; set; }
+
+ public Guid? DicomStudyId { get; set; }
+
+ public Guid? NoneDicomStudyId { get; set; }
+
+
+
+ public string FileMarkId { get; set; }
+
+ public int? Priority { get; set; }
+ public string IP { get; set; }
+ public bool? IsNeedSync { get; set; }
+ public string UploadRegion { get; set; }
+ public string TargetRegion { get; set; }
+
+ public bool? IsSync { get; set; }
+}
+
+
+public class SubjectFileUploadRecordQuery : PageInput
+{
+ public Guid TrialId { get; set; }
+
+ public string? SubjectCode { get; set; }
+
+ public string? VisitName { get; set; }
+
+ public string? StudyCode { get; set; }
+
+ public string? UploadRegion { get; set; }
+ public string? TargetRegion { get; set; }
+
+ public bool? IsSync { get; set; }
+
+
+}
+
+public class FileUploadRecordQuery : PageInput
+{
+
+ public BatchDataType? BatchDataType { get; set; }
+
+ public Guid? TrialId { get; set; }
+
+ public int? DataFileType { get; set; }
+
+ public string StudyCode { get; set; }
+
+ public string? SubjectCode { get; set; }
+
+ public string? VisitName { get; set; }
+
+
+ public Guid? SubjectId { get; set; }
+
+ public Guid? SubjectVisitId { get; set; }
+
+
+
+ public string? FileMarkId { get; set; }
+
+ public string? FileName { get; set; }
+
+
+ public string? FileType { get; set; }
+
+ public string? IP { get; set; }
+
+ public bool? IsNeedSync { get; set; }
+
+ public bool? IsSync { get; set; }
+
+
+ public string? Path { get; set; }
+
+ public int? Priority { get; set; }
+
+ public string? TargetRegion { get; set; }
+
+
+ public string? UploadBatchId { get; set; }
+
+ public string? UploadRegion { get; set; }
+
+
+ public DateTime? SyncFinishedStartTime { get; set; }
+
+ public DateTime? SyncFinishedEndTime { get; set; }
+
+ public DateTime? UploadStartTime { get; set; }
+
+ public DateTime? UploadEndTime { get; set; }
+}
+
+
+
+public class UploadFileSyncRecordView
+{
+ public Guid Id { get; set; }
+
+
+ public DateTime? StartTime { get; set; }
+
+ public DateTime? EndTime { get; set; }
+
+ public jobState JobState { get; set; }
+
+
+ public string Msg { get; set; }
+
+ public DateTime CreateTime { get; set; }
+
+ public DateTime UpdateTime { get; set; }
+
+
+ public Guid? FileUploadRecordId { get; set; }
+ public string? FileName { get; set; }
+
+ public string? FileType { get; set; }
+
+ public string? Path { get; set; }
+
+
+ public string StudyCode { get; set; }
+
+ public string? SubjectCode { get; set; }
+
+ public string? VisitName { get; set; }
+
+
+
+}
+
+public class UploadFileSyncRecordQuery : PageInput
+{
+
+ public Guid? FileUploadRecordId { get; set; }
+
+
+ public jobState? JobState { get; set; }
+
+ public string? SubjectCode { get; set; }
+
+ public string? VisitName { get; set; }
+
+ public string? StudyCode { get; set; }
+}
+
+public class BatchAddSyncFileCommand
+{
+ public List FileUploadRecordIdList { get; set; }
+
+ public int? Priority { get; set; }
+}
diff --git a/IRaCIS.Core.Application/Service/Common/DeployConfigService.cs b/IRaCIS.Core.Application/Service/Common/DeployConfigService.cs
new file mode 100644
index 000000000..6edfeeac6
--- /dev/null
+++ b/IRaCIS.Core.Application/Service/Common/DeployConfigService.cs
@@ -0,0 +1,130 @@
+using DocumentFormat.OpenXml.Spreadsheet;
+using IRaCIS.Application.Contracts;
+using Medallion.Threading;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Options;
+using Newtonsoft.Json.Linq;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace IRaCIS.Core.Application.Service.Common;
+
+[ApiExplorerSettings(GroupName = "Common")]
+public class DeployConfigService(IMapper _mapper, IUserInfo _userInfo, IWebHostEnvironment _hostEnvironment, IStringLocalizer _localizer, IFusionCache _fusionCache) : BaseService
+{
+
+ ///
+ /// 获取系统基础配置信息
+ ///
+ ///
+ ///
+ public async Task GetSystemBasicConfigInfo([FromServices] IOptionsMonitor options)
+ {
+ return options.CurrentValue;
+ }
+
+ ///
+ /// 更新系统基础配置
+ ///
+ ///
+ ///
+ [HttpPost]
+ public async Task UpdateSystemBasicConfig(ServiceVerifyConfigOption basicSystemConfigOption)
+ {
+ var path = $"appsettings.{_hostEnvironment.EnvironmentName}.json";
+
+ string text = System.IO.File.ReadAllText(path);
+
+ // 修改
+ JObject obj = JObject.Parse(text);
+ // QCRiskControl 相关配置
+ obj["BasicSystemConfig"][nameof(ServiceVerifyConfigOption.QCRiskControl)] = basicSystemConfigOption.QCRiskControl;
+ obj["BasicSystemConfig"][nameof(ServiceVerifyConfigOption.OpenUserComplexPassword)] = basicSystemConfigOption.OpenUserComplexPassword;
+ obj["BasicSystemConfig"][nameof(ServiceVerifyConfigOption.OpenSignDocumentBeforeWork)] = basicSystemConfigOption.OpenSignDocumentBeforeWork;
+ obj["BasicSystemConfig"][nameof(ServiceVerifyConfigOption.OpenTrialRelationDelete)] = basicSystemConfigOption.OpenTrialRelationDelete;
+ obj["BasicSystemConfig"][nameof(ServiceVerifyConfigOption.OpenLoginLimit)] = basicSystemConfigOption.OpenLoginLimit;
+ obj["BasicSystemConfig"][nameof(ServiceVerifyConfigOption.LoginMaxFailCount)] = basicSystemConfigOption.LoginMaxFailCount;
+ obj["BasicSystemConfig"][nameof(ServiceVerifyConfigOption.LoginFailLockMinutes)] = basicSystemConfigOption.LoginFailLockMinutes;
+ obj["BasicSystemConfig"][nameof(ServiceVerifyConfigOption.AutoLoginOutMinutes)] = basicSystemConfigOption.AutoLoginOutMinutes;
+ obj["BasicSystemConfig"][nameof(ServiceVerifyConfigOption.OpenLoginMFA)] = basicSystemConfigOption.OpenLoginMFA;
+ obj["BasicSystemConfig"][nameof(ServiceVerifyConfigOption.ContinuousReadingTimeMin)] = basicSystemConfigOption.ContinuousReadingTimeMin;
+ obj["BasicSystemConfig"][nameof(ServiceVerifyConfigOption.ReadingRestTimeMin)] = basicSystemConfigOption.ReadingRestTimeMin;
+ obj["BasicSystemConfig"][nameof(ServiceVerifyConfigOption.IsNeedChangePassWord)] = basicSystemConfigOption.IsNeedChangePassWord;
+ obj["BasicSystemConfig"][nameof(ServiceVerifyConfigOption.ChangePassWordDays)] = basicSystemConfigOption.ChangePassWordDays;
+ obj["BasicSystemConfig"][nameof(ServiceVerifyConfigOption.TemplateType)] = ((int)basicSystemConfigOption.TemplateType); // 枚举需要转换为字符串
+ obj["BasicSystemConfig"][nameof(ServiceVerifyConfigOption.ThirdPdfUrl)] = basicSystemConfigOption.ThirdPdfUrl;
+ obj["BasicSystemConfig"][nameof(ServiceVerifyConfigOption.UserMFAVerifyMinutes)] = basicSystemConfigOption.UserMFAVerifyMinutes;
+
+
+ // 重新写入appsettings.json
+ string result = obj.ToString();
+ System.IO.File.WriteAllText(path, result);
+
+
+ return ResponseOutput.Ok();
+ }
+
+ ///
+ /// 获取系统邮件配置信息
+ ///
+ ///
+ ///
+ public async Task GetEmailConfigInfo([FromServices] IOptionsMonitor options)
+ {
+ return options.CurrentValue;
+ }
+
+
+ ///
+ /// 更新系统邮件配置
+ ///
+ ///
+ ///
+ [HttpPost]
+ public async Task UpdateSystemEmailConfig(SystemEmailSendConfig emailConfig)
+ {
+ var path = $"appsettings.{_hostEnvironment.EnvironmentName}.json";
+
+ string text = System.IO.File.ReadAllText(path);
+
+ // 修改
+ JObject obj = JObject.Parse(text);
+
+ // SystemEmailSendConfig 相关配置
+ obj["SystemEmailSendConfig"][nameof(SystemEmailSendConfig.Port)] = emailConfig.Port;
+ obj["SystemEmailSendConfig"][nameof(SystemEmailSendConfig.Host)] = emailConfig.Host;
+ obj["SystemEmailSendConfig"][nameof(SystemEmailSendConfig.Imap)] = emailConfig.Imap;
+ obj["SystemEmailSendConfig"][nameof(SystemEmailSendConfig.ImapPort)] = emailConfig.ImapPort;
+ obj["SystemEmailSendConfig"][nameof(SystemEmailSendConfig.FromEmail)] = emailConfig.FromEmail;
+ obj["SystemEmailSendConfig"][nameof(SystemEmailSendConfig.FromName)] = emailConfig.FromName;
+ obj["SystemEmailSendConfig"][nameof(SystemEmailSendConfig.AuthorizationCode)] = emailConfig.AuthorizationCode;
+ obj["SystemEmailSendConfig"][nameof(SystemEmailSendConfig.SiteUrl)] = emailConfig.SiteUrl;
+ obj["SystemEmailSendConfig"][nameof(SystemEmailSendConfig.PlatformName)] = emailConfig.PlatformName;
+ obj["SystemEmailSendConfig"][nameof(SystemEmailSendConfig.PlatformNameCN)] = emailConfig.PlatformNameCN;
+ obj["SystemEmailSendConfig"][nameof(SystemEmailSendConfig.SystemShortName)] = emailConfig.SystemShortName;
+ obj["SystemEmailSendConfig"][nameof(SystemEmailSendConfig.OrganizationName)] = emailConfig.OrganizationName;
+ obj["SystemEmailSendConfig"][nameof(SystemEmailSendConfig.OrganizationNameCN)] = emailConfig.OrganizationNameCN;
+ obj["SystemEmailSendConfig"][nameof(SystemEmailSendConfig.CompanyName)] = emailConfig.CompanyName;
+ obj["SystemEmailSendConfig"][nameof(SystemEmailSendConfig.CompanyNameCN)] = emailConfig.CompanyNameCN;
+ obj["SystemEmailSendConfig"][nameof(SystemEmailSendConfig.CompanyShortName)] = emailConfig.CompanyShortName;
+ obj["SystemEmailSendConfig"][nameof(SystemEmailSendConfig.CompanyShortNameCN)] = emailConfig.CompanyShortNameCN;
+ obj["SystemEmailSendConfig"][nameof(SystemEmailSendConfig.IsEnv_US)] = emailConfig.IsEnv_US;
+ obj["SystemEmailSendConfig"][nameof(SystemEmailSendConfig.EmailRegexStr)] = emailConfig.EmailRegexStr;
+
+
+
+ // 重新写入appsettings.json
+ string result = obj.ToString();
+ System.IO.File.WriteAllText(path, result);
+
+
+ return ResponseOutput.Ok();
+ }
+
+}
diff --git a/IRaCIS.Core.Application/Service/Common/EmailLogService.cs b/IRaCIS.Core.Application/Service/Common/EmailLogService.cs
index b83532e7e..092c11977 100644
--- a/IRaCIS.Core.Application/Service/Common/EmailLogService.cs
+++ b/IRaCIS.Core.Application/Service/Common/EmailLogService.cs
@@ -155,7 +155,8 @@ public class EmailLogService(IRepository _emailLogRepository,
fileStream: decodeStream,
oosFolderPath: $"EmailAttachment/{emailInfo.Id}", // OSS 虚拟目录
fileRealName: emaliAttachmentInfo.AttachmentName,
- isFileNameAddGuid: true); // 让方法自己在文件名前加 Guid
+ isFileNameAddGuid: true,
+ uploadInfo: new FileUploadRecordAddOrEdit() {TrialId= inDto.TrialId, BatchDataType = BatchDataType.EmailAttach }); // 让方法自己在文件名前加 Guid
attachmentInfos.Add(emaliAttachmentInfo);
}
diff --git a/IRaCIS.Core.Application/Service/Common/FileUploadRecordService.cs b/IRaCIS.Core.Application/Service/Common/FileUploadRecordService.cs
new file mode 100644
index 000000000..78e02246c
--- /dev/null
+++ b/IRaCIS.Core.Application/Service/Common/FileUploadRecordService.cs
@@ -0,0 +1,589 @@
+
+//--------------------------------------------------------------------
+// 此代码由liquid模板自动生成 byzhouhang 20240909
+// 生成时间 2026-03-10 06:15:17Z
+// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。
+//--------------------------------------------------------------------
+using DocumentFormat.OpenXml.Office2010.ExcelAc;
+using IRaCIS.Core.Application.Helper;
+using IRaCIS.Core.Application.Interfaces;
+using IRaCIS.Core.Application.ViewModel;
+using IRaCIS.Core.Domain.Models;
+using IRaCIS.Core.Infra.EFCore;
+using IRaCIS.Core.Infra.EFCore.Common;
+using IRaCIS.Core.Infrastructure.Extention;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Options;
+using Spire.Doc.Interface;
+using System.Drawing;
+using System.Threading.Channels;
+using System.Threading.Tasks;
+
+namespace IRaCIS.Core.Application.Service;
+
+[ApiExplorerSettings(GroupName = "Common")]
+public class FileUploadRecordService(IRepository _fileUploadRecordRepository, IRepository _uploadFileSyncRecordRepository,
+ IMapper _mapper, IUserInfo _userInfo, IStringLocalizer _localizer, IOptionsMonitor options,
+ IFusionCache _fusionCache, IRepository _trialRepository, FileSyncQueue _fileSyncQueue) : BaseService, IFileUploadRecordService
+{
+
+ ObjectStoreServiceOptions ObjectStoreServiceConfig = options.CurrentValue;
+
+
+
+ ///
+ /// 按照 subject visit studyCode 三个维度进行分组的查询列表 (subject相关)
+ ///
+ ///
+ ///
+ [HttpPost]
+ public async Task> GetSubjectUploadRecordList(SubjectFileUploadRecordQuery inQuery)
+ {
+
+ var query = _fileUploadRecordRepository.Where(t => t.TrialId == inQuery.TrialId && t.SubjectId != null)
+ .WhereIf(!string.IsNullOrEmpty(inQuery.VisitName), t => t.SubjectVisit.VisitName.Contains(inQuery.VisitName))
+ .WhereIf(!string.IsNullOrEmpty(inQuery.SubjectCode), t => t.Subject.Code.Contains(inQuery.SubjectCode))
+ .WhereIf(!string.IsNullOrEmpty(inQuery.StudyCode), t => t.StudyCode.Contains(inQuery.StudyCode))
+ .WhereIf(!string.IsNullOrEmpty(inQuery.UploadRegion), t => t.UploadRegion == inQuery.UploadRegion)
+ .WhereIf(!string.IsNullOrEmpty(inQuery.TargetRegion), t => t.TargetRegion == inQuery.TargetRegion)
+ .WhereIf(inQuery.IsSync != null, t => t.IsSync == inQuery.IsSync)
+ .GroupBy(t => new { t.StudyCode, SubjectCode = t.Subject.Code, t.SubjectVisit.VisitName, t.SubjectId, t.SubjectVisitId })
+ .Select(g => new SubjectFileUploadRecordView()
+ {
+ SubjectCode = g.Key.SubjectCode,
+ VisitName = g.Key.VisitName,
+ StudyCode = g.Key.StudyCode,
+ SubjectId = g.Key.SubjectId,
+ SubjectVisitId = g.Key.SubjectVisitId,
+
+ FileCount = g.Count(),
+
+ CreateTime = g.Max(t => t.CreateTime),
+
+ SyncFinishedTime = g.Max(t => t.SyncFinishedTime),
+
+ UploadRegion = g.First().UploadRegion,
+
+ TargetRegion = g.First().TargetRegion,
+
+ IsSync = !g.Any(t => t.IsSync == false || t.IsSync == null)
+
+ });
+
+
+ var pageList = await query.ToPagedListAsync(inQuery);
+
+ return pageList;
+ }
+
+
+
+ ///
+ /// 上传记录表--里面包含待同步任务 DataFileType= 0 :代表系统文件 1:Subject相关 2:项目相关,但是和subject 没关系
+ ///
+ ///
+ ///
+ [HttpPost]
+ public async Task> GetFileUploadRecordList(FileUploadRecordQuery inQuery)
+ {
+
+ var fileUploadRecordQueryable = _fileUploadRecordRepository
+ .WhereIf(!string.IsNullOrEmpty(inQuery.FileName), t => t.FileName.Contains(inQuery.FileName))
+ .WhereIf(!string.IsNullOrEmpty(inQuery.FileType), t => t.FileType.Contains(inQuery.FileType))
+ .WhereIf(inQuery.TrialId != null, t => t.TrialId == inQuery.TrialId)
+
+ .WhereIf(inQuery.SubjectId != null, t => t.SubjectId == inQuery.SubjectId)
+ .WhereIf(inQuery.SubjectVisitId != null, t => t.SubjectVisitId == inQuery.SubjectVisitId)
+ .WhereIf(!string.IsNullOrEmpty(inQuery.StudyCode), t => t.StudyCode.Contains(inQuery.StudyCode))
+ .WhereIf(!string.IsNullOrEmpty(inQuery.VisitName), t => t.SubjectVisit.VisitName.Contains(inQuery.VisitName))
+ .WhereIf(!string.IsNullOrEmpty(inQuery.SubjectCode), t => t.Subject.Code.Contains(inQuery.SubjectCode))
+
+ .WhereIf(inQuery.DataFileType == 1 && inQuery.SubjectId != null && inQuery.SubjectVisitId == null, t => t.SubjectVisitId == null)
+ .WhereIf(inQuery.DataFileType == 1 && inQuery.SubjectVisitId != null && inQuery.StudyCode == "", t => t.StudyCode == "")
+ .WhereIf(inQuery.DataFileType == 1 && inQuery.SubjectCode.IsNotNullOrEmpty() && inQuery.VisitName.IsNullOrEmpty(), t => t.SubjectVisitId == null)
+ .WhereIf(inQuery.DataFileType == 1 && inQuery.VisitName.IsNotNullOrEmpty() && inQuery.StudyCode == "", t => t.StudyCode == "")
+
+ .WhereIf(inQuery.DataFileType == 0, t => t.TrialId == null)
+ .WhereIf(inQuery.DataFileType == 1, t => t.SubjectId != null)
+ .WhereIf(inQuery.DataFileType == 2, t => t.SubjectId == null)
+
+ .WhereIf(inQuery.IsNeedSync != null, t => t.IsNeedSync == inQuery.IsNeedSync)
+
+ .WhereIf(inQuery.IsSync != null, t => t.IsSync == inQuery.IsSync)
+ .WhereIf(inQuery.Priority != null, t => t.Priority == inQuery.Priority)
+ .WhereIf(inQuery.BatchDataType != null, t => t.BatchDataType == inQuery.BatchDataType)
+ .WhereIf(!string.IsNullOrEmpty(inQuery.UploadRegion), t => t.UploadRegion == inQuery.UploadRegion)
+ .WhereIf(!string.IsNullOrEmpty(inQuery.TargetRegion), t => t.TargetRegion == inQuery.TargetRegion)
+ .WhereIf(!string.IsNullOrEmpty(inQuery.UploadBatchId), t => t.UploadBatchId.Contains(inQuery.UploadBatchId))
+ .WhereIf(!string.IsNullOrEmpty(inQuery.Path), t => t.Path.Contains(inQuery.Path))
+ .WhereIf(inQuery.UploadStartTime != null, t => t.CreateTime >= inQuery.UploadStartTime)
+ .WhereIf(inQuery.UploadEndTime != null, t => t.CreateTime <= inQuery.UploadEndTime)
+ .WhereIf(inQuery.SyncFinishedStartTime != null, t => t.SyncFinishedTime >= inQuery.SyncFinishedStartTime)
+ .WhereIf(inQuery.SyncFinishedEndTime != null, t => t.SyncFinishedTime <= inQuery.SyncFinishedEndTime)
+
+ .ProjectTo(_mapper.ConfigurationProvider);
+
+ var pageList = await fileUploadRecordQueryable.ToPagedListAsync(inQuery);
+
+ return pageList;
+ }
+
+ ///
+ /// 任务具体执行记录表
+ ///
+ ///
+ ///
+ [HttpPost]
+ public async Task> GetUploadFileSyncRecordList(UploadFileSyncRecordQuery inQuery)
+ {
+
+ var fileUploadRecordQueryable = _uploadFileSyncRecordRepository
+ .WhereIf(inQuery.JobState != null, t => t.JobState == inQuery.JobState)
+ .WhereIf(inQuery.FileUploadRecordId != null, t => t.FileUploadRecordId == inQuery.FileUploadRecordId)
+ .WhereIf(!string.IsNullOrEmpty(inQuery.StudyCode), t => t.FileUploadRecord.StudyCode.Contains(inQuery.StudyCode))
+ .WhereIf(!string.IsNullOrEmpty(inQuery.VisitName), t => t.FileUploadRecord.SubjectVisit.VisitName.Contains(inQuery.VisitName))
+ .WhereIf(!string.IsNullOrEmpty(inQuery.SubjectCode), t => t.FileUploadRecord.Subject.Code.Contains(inQuery.SubjectCode))
+
+ .ProjectTo(_mapper.ConfigurationProvider);
+
+ var pageList = await fileUploadRecordQueryable.ToPagedListAsync(inQuery);
+
+ return pageList;
+ }
+
+
+ ///
+ /// 批量设置为需要同步,并且设置优先级
+ ///
+ ///
+ ///
+ public async Task BatchAddSyncFileTask(BatchAddSyncFileCommand inComand)
+ {
+
+
+ await _fileUploadRecordRepository.BatchUpdateNoTrackingAsync(t => inComand.FileUploadRecordIdList.Contains(t.Id), u => new FileUploadRecord() { IsNeedSync = true, Priority = inComand.Priority ?? 0 });
+
+ foreach (var item in inComand.FileUploadRecordIdList)
+ {
+ _fileSyncQueue.Enqueue(item, inComand.Priority ?? 0);
+
+ }
+
+
+ await _fileUploadRecordRepository.SaveChangesAsync();
+
+ return ResponseOutput.Ok();
+ }
+
+
+
+
+ public async Task AddOrUpdateFileUploadRecord(FileUploadRecordAddOrEdit addOrEditFileUploadRecord)
+ {
+
+ addOrEditFileUploadRecord.IP = _userInfo.IP;
+
+ if (ObjectStoreServiceConfig.IsOpenStoreSync && _userInfo.Domain.IsNotNullOrEmpty())
+ {
+ var find = ObjectStoreServiceConfig.SyncConfigList.FirstOrDefault(t => t.Domain == _userInfo.Domain);
+ if (find != null)
+ {
+ addOrEditFileUploadRecord.UploadRegion = find.UploadRegion;
+ addOrEditFileUploadRecord.TargetRegion = find.TargetRegion;
+
+ }
+ else
+ {
+ //前后端调试的时候,上传的时候域名不对应,自动按照后端配置设置上传区域和同步区域
+ var apiDefalut = ObjectStoreServiceConfig.SyncConfigList.FirstOrDefault(t => t.UploadRegion == ObjectStoreServiceConfig.ApiDeployRegion);
+
+ if (apiDefalut != null)
+ {
+ addOrEditFileUploadRecord.UploadRegion = apiDefalut.UploadRegion;
+ addOrEditFileUploadRecord.TargetRegion = apiDefalut.TargetRegion;
+
+ }
+ }
+ }
+
+ if (addOrEditFileUploadRecord.TrialId != null)
+ {
+
+ var trialDataStore = await _fusionCache.GetOrSetAsync(CacheKeys.TrialDataStoreType(addOrEditFileUploadRecord.TrialId.Value), async _ =>
+ {
+ return await _trialRepository.Where(t => t.Id == addOrEditFileUploadRecord.TrialId).Select(t => t.TrialDataStoreType)
+ .FirstOrDefaultAsync();
+ },
+ TimeSpan.FromDays(7)
+ );
+
+ //项目配置了,那么就设置需要同步
+ if (trialDataStore == TrialDataStore.MUtiCenter)
+ {
+ addOrEditFileUploadRecord.IsNeedSync = true;
+
+ addOrEditFileUploadRecord.Priority = addOrEditFileUploadRecord.Priority?? 0;
+
+ addOrEditFileUploadRecord.IsSync = false;
+ }
+ else
+ {
+ addOrEditFileUploadRecord.IsNeedSync = false;
+
+ //addOrEditFileUploadRecord.TargetRegion = "";
+
+ }
+
+ }
+ else
+ {
+ //系统文件,默认同步
+ addOrEditFileUploadRecord.IsNeedSync = true;
+
+ addOrEditFileUploadRecord.IsSync = false;
+
+ addOrEditFileUploadRecord.Priority = addOrEditFileUploadRecord.Priority ?? 0;
+ }
+
+ var entity = await _fileUploadRecordRepository.InsertOrUpdateAsync(addOrEditFileUploadRecord, true);
+
+ if (addOrEditFileUploadRecord.IsNeedSync == true)
+ {
+ _fileSyncQueue.Enqueue(entity.Id, addOrEditFileUploadRecord.Priority ?? 0);
+ }
+
+ return ResponseOutput.Ok(entity.Id.ToString());
+
+ }
+
+
+ [HttpDelete("{fileUploadRecordId:guid}")]
+ public async Task DeleteFileUploadRecord(Guid fileUploadRecordId)
+ {
+ var success = await _fileUploadRecordRepository.BatchDeleteNoTrackingAsync(t => t.Id == fileUploadRecordId);
+ return ResponseOutput.Ok();
+ }
+
+}
+
+
+#region 同步队列
+
+
+public sealed class FileSyncQueue
+{
+ ///
+ /// 优先级队列(仅负责排序)
+ ///
+ private readonly PriorityQueue _queue = new();
+
+ ///
+ /// 当前等待中的任务(唯一真实数据)
+ /// key = Guid
+ /// value = 最新 priority
+ ///
+ private readonly Dictionary _waiting = new();
+
+ ///
+ /// 正在执行的任务(防止重复执行)
+ ///
+ private readonly HashSet _running = new();
+
+ ///
+ /// worker 等待信号
+ ///
+ private readonly SemaphoreSlim _signal = new(0);
+
+
+ private readonly object _lock = new();
+
+ // ============================================================
+ // Enqueue
+ // ============================================================
+
+ ///
+ /// 入队(同 Guid 会覆盖优先级)
+ ///
+ public void Enqueue(Guid id, int priority)
+ {
+ bool needSignal = false;
+
+ lock (_lock)
+ {
+ // 如果正在执行,忽略(防止重复)
+ if (_running.Contains(id))
+ return;
+
+ // 是否新任务(用于减少 signal 风暴)
+ if (!_waiting.ContainsKey(id))
+ needSignal = true;
+
+ // 更新为最新优先级(最后一次为准)
+ _waiting[id] = priority; //等价于添加或者更新
+
+ // PriorityQueue 无法更新节点
+ // 允许旧节点存在,Dequeue 时过滤
+ _queue.Enqueue(id, -priority);
+ }
+
+ // 只有新增任务才唤醒 worker
+ if (needSignal)
+ _signal.Release();
+ }
+
+ // ============================================================
+ // Dequeue
+ // ============================================================
+
+ ///
+ /// 获取一个待执行任务(无任务时自动等待)
+ ///
+ public async Task DequeueAsync(CancellationToken ct)
+ {
+ while (true)
+ {
+ await _signal.WaitAsync(ct);
+
+ lock (_lock)
+ {
+ while (_queue.Count > 0)
+ {
+ var id = _queue.Dequeue();
+
+ // 已被覆盖或取消 如果这个任务已经不是“当前最新版任务”,那它只是 PriorityQueue 里的垃圾数据,直接跳过。
+ if (!_waiting.TryGetValue(id, out _)) //能从等待任务中取到,那么就是有效的,不能取到那么就是覆盖的
+ continue;
+
+ // 标记为运行中
+ _waiting.Remove(id);
+ _running.Add(id);
+
+ return id;
+ }
+ }
+ }
+ }
+
+ // ============================================================
+ // Complete
+ // ============================================================
+
+ ///
+ /// 任务执行完成(必须调用)
+ ///
+ public void Complete(Guid id)
+ {
+ lock (_lock)
+ {
+ _running.Remove(id);
+ }
+ }
+
+ // ============================================================
+ // Snapshot
+ // ============================================================
+
+ ///
+ /// 当前等待中的任务快照
+ ///
+ public Guid[] Snapshot()
+ {
+ lock (_lock)
+ {
+ return _waiting.Keys.ToArray();
+ }
+ }
+
+ // ============================================================
+ // 状态信息(调试用)
+ // ============================================================
+
+ public int WaitingCount
+ {
+ get
+ {
+ lock (_lock)
+ return _waiting.Count;
+ }
+ }
+
+ public int RunningCount
+ {
+ get
+ {
+ lock (_lock)
+ return _running.Count;
+ }
+ }
+}
+
+
+/////
+///// 同步队列 信号量
+/////
+//public class FileSyncQueue
+//{
+// private readonly PriorityQueue _queue = new();
+// private readonly SemaphoreSlim _signal = new(0);
+// private readonly object _lock = new();
+
+// public void Enqueue(Guid id, int priority)
+// {
+// lock (_lock)
+// {
+// // priority 越大越优先
+// _queue.Enqueue(id, -priority);
+// }
+
+// //类似于计数器,不会产生通知风暴,可消费资源 +1
+// //if (有等待线程) 唤醒一个 else 仅增加计数
+// _signal.Release(); // 唤醒一个 worker
+// }
+
+// ///
+// /// 如果没有任务 → 挂起等待 有任务 → 被唤醒并返回
+// ///
+// ///
+// ///
+// public async Task DequeueAsync(CancellationToken ct)
+// {
+// await _signal.WaitAsync(ct);
+
+// lock (_lock)
+// {
+// return _queue.Dequeue();
+// }
+// }
+
+// ///
+// /// 获取队列任务Id
+// ///
+// ///
+// public Guid[] Snapshot()
+// {
+// lock (_lock)
+// {
+// return _queue.UnorderedItems
+// .Select(x => x.Element)
+// .ToArray();
+// }
+// }
+//}
+
+#region 这里不用 SyncQueueUseChannel 和调度器 SyncScheduler
+public class SyncQueueUseChannel
+{
+ // 优先级队列(priority 越大越先执行)
+ private readonly PriorityQueue _queue = new();
+
+ // Worker 唤醒信号
+ private readonly Channel _signal =
+ Channel.CreateUnbounded(new UnboundedChannelOptions
+ {
+ SingleReader = false,
+ SingleWriter = false
+ });
+
+ // 队列任务数量(不是CPU数量!)
+ private int _count = 0;
+
+
+ private readonly object _lock = new();
+
+ ///
+ /// 入队任务
+ ///
+ public void Enqueue(Guid id, int priority)
+ {
+ bool needSignal = false;
+
+ lock (_lock)
+ {
+ // priority 越大越优先 → 转负数
+ _queue.Enqueue(id, -priority);
+
+ // 只有从 0 → 1 才需要唤醒 worker
+ if (_count == 0)
+ needSignal = true;
+
+ _count++;
+ }
+
+ // 避免 signal 风暴
+ if (needSignal)
+ _signal.Writer.TryWrite(true);
+ }
+
+ ///
+ /// Worker 等待并获取任务
+ ///
+ public async Task DequeueAsync(CancellationToken ct)
+ {
+
+ // 没任务时挂起(不会占CPU)
+ await _signal.Reader.ReadAsync(ct);
+
+ lock (_lock)
+ {
+ var id = _queue.Dequeue();
+
+ _count--;
+
+ // 如果还有任务,继续唤醒下一个 worker
+ if (_count > 0)
+ _signal.Writer.TryWrite(true);
+
+ return id;
+ }
+ }
+
+ ///
+ /// 当前排队数量(调试用)
+ ///
+ public int Count
+ {
+ get
+ {
+ lock (_lock)
+ return _count;
+ }
+ }
+}
+
+///
+/// 同步调度器
+///
+public class FileSyncScheduler
+{
+ private readonly FileSyncQueue _queue;
+
+ public FileSyncScheduler(FileSyncQueue queue)
+ {
+ _queue = queue;
+ }
+
+ public void Enqueue(FileUploadRecord file)
+ {
+ if (file.IsNeedSync != true)
+ return;
+
+ _queue.Enqueue(file.Id, file.Priority ?? 0);
+ }
+
+ ///
+ /// 如果没有任务 → 挂起等待 有任务 → 被唤醒并返回
+ ///
+ ///
+ ///
+ public Task WaitAsync(CancellationToken ct)
+ => _queue.DequeueAsync(ct);
+}
+#endregion
+
+
+
+
+
+
+#endregion
+
diff --git a/IRaCIS.Core.Application/Service/Common/Interface/IFileUploadRecordService.cs b/IRaCIS.Core.Application/Service/Common/Interface/IFileUploadRecordService.cs
new file mode 100644
index 000000000..eebc3e5cd
--- /dev/null
+++ b/IRaCIS.Core.Application/Service/Common/Interface/IFileUploadRecordService.cs
@@ -0,0 +1,23 @@
+
+//--------------------------------------------------------------------
+// 此代码由liquid模板自动生成 byzhouhang 20240909
+// 生成时间 2026-03-10 06:15:31Z
+// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。
+//--------------------------------------------------------------------
+using System;
+using IRaCIS.Core.Infrastructure.Extention;
+using System.Threading.Tasks;
+using IRaCIS.Core.Application.ViewModel;
+namespace IRaCIS.Core.Application.Interfaces;
+
+public interface IFileUploadRecordService
+{
+
+ Task> GetFileUploadRecordList(FileUploadRecordQuery inQuery);
+
+ Task AddOrUpdateFileUploadRecord(FileUploadRecordAddOrEdit addOrEditFileUploadRecord);
+
+ Task DeleteFileUploadRecord(Guid fileUploadRecordId);
+}
+
+
diff --git a/IRaCIS.Core.Application/Service/Common/MailService.cs b/IRaCIS.Core.Application/Service/Common/MailService.cs
index 3b2a2e427..2e64e5721 100644
--- a/IRaCIS.Core.Application/Service/Common/MailService.cs
+++ b/IRaCIS.Core.Application/Service/Common/MailService.cs
@@ -1,6 +1,8 @@
-using IRaCIS.Application.Contracts;
+using DocumentFormat.OpenXml.Spreadsheet;
+using IRaCIS.Application.Contracts;
using IRaCIS.Core.Application.Auth;
using IRaCIS.Core.Application.Helper;
+using IRaCIS.Core.Application.MassTransit.Consumer;
using IRaCIS.Core.Domain.Models;
using IRaCIS.Core.Domain.Share;
using IRaCIS.Core.Infrastructure;
@@ -66,7 +68,7 @@ namespace IRaCIS.Core.Application.Service
Task AddUserSendEmailAsync(Guid userId, string baseUrl, string routeUrl);
- Task AdminResetPwdSendEmailAsync(Guid userId, string pwdNotMd5 = "123456", string baseUrl="");
+ Task AdminResetPwdSendEmailAsync(Guid userId, string pwdNotMd5 = "123456", string baseUrl = "");
Task SiteSurveyUserJoinEmail(Guid trialId, Guid userId, string userTypes, string baseUrl, string rootUrl);
@@ -104,10 +106,18 @@ namespace IRaCIS.Core.Application.Service
{
private readonly SystemEmailSendConfig _systemEmailConfig = systemEmailConfig.CurrentValue;
-
+ ///
+ /// 获取邮件主题和html 通用封装 以用户语言为主,没有的就用当前的请求语言_userInfo.IsEn_Us
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
private async Task GetEmailSubejctAndHtmlInfoAndBuildAsync(EmailBusinessScenario scenario, MimeMessage messageToSend,
- Func<(string topicStr, string htmlBodyStr), (string topicStr, string htmlBodyStr)> emailFunc)
+ Func<(string topicStr, string htmlBodyStr), (string topicStr, string htmlBodyStr)> emailFunc, UserWorkLanguage? userWorkLanguage = null)
{
var configInfo = await _emailNoticeConfigrepository.Where(t => t.BusinessScenarioEnum == scenario).FirstOrDefaultAsync();
@@ -119,7 +129,9 @@ namespace IRaCIS.Core.Application.Service
}
- var (topicStr, htmlBodyStr) = _userInfo.IsEn_Us ? (configInfo.EmailTopic, configInfo.EmailHtmlContent) : (configInfo.EmailTopicCN, configInfo.EmailHtmlContentCN);
+ bool isEn_Us = userWorkLanguage.HasValue ? userWorkLanguage.Value == UserWorkLanguage.US : _userInfo.IsEn_Us;
+
+ var (topicStr, htmlBodyStr) = isEn_Us ? (configInfo.EmailTopic, configInfo.EmailHtmlContent) : (configInfo.EmailTopicCN, configInfo.EmailHtmlContentCN);
try
{
@@ -182,24 +194,16 @@ namespace IRaCIS.Core.Application.Service
return sucessHandle;
}
- private string ReplaceCompanyName(string needDealtxt)
- {
- var str = needDealtxt.Replace("{company}", _userInfo.IsEn_Us ? _systemEmailConfig.CompanyName : _systemEmailConfig.CompanyNameCN)
- .Replace("{company abbreviation}", _userInfo.IsEn_Us ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN);
- return str;
- }
- private string ReplacePlatformName(string needDealtxt)
- {
- var platformName = _userInfo.IsEn_Us ? _systemEmailConfig.PlatformName : _systemEmailConfig.PlatformNameCN;
-
- var str = needDealtxt.Replace("{platformName}", platformName);
- return str;
- }
//MFA
public async Task SenMFAVerifyEmail(Guid userId, string userName, string emailAddress, int verificationCode, UserMFAType mfaType = UserMFAType.Login)
{
+
+ //设置工作语言
+ var workLanguage = _identityUserRepository.Where(t => t.Id == userId).Select(t => t.UserWorkLanguage).First();
+ var isEn_US = workLanguage == UserWorkLanguage.US;
+
var messageToSend = new MimeMessage();
//发件地址
messageToSend.From.Add(new MailboxAddress(_systemEmailConfig.FromName, _systemEmailConfig.FromEmail));
@@ -207,13 +211,13 @@ namespace IRaCIS.Core.Application.Service
messageToSend.To.Add(new MailboxAddress(userName, emailAddress));
//主题---[来自{0}] 关于MFA邮箱验证的提醒
- var companyName = _userInfo.IsEn_Us ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
+ var companyName = isEn_US ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
Func<(string topicStr, string htmlBodyStr), (string topicStr, string htmlBodyStr)> emailConfigFunc = input =>
{
var topicStr = string.Format(input.topicStr, companyName);
- var htmlBodyStr = string.Format(ReplaceCompanyName(input.htmlBodyStr),
+ var htmlBodyStr = string.Format(CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr, workLanguage),
userName,
verificationCode
);
@@ -222,7 +226,7 @@ namespace IRaCIS.Core.Application.Service
};
- await GetEmailSubejctAndHtmlInfoAndBuildAsync(mfaType == UserMFAType.Login ? EmailBusinessScenario.MFALogin : EmailBusinessScenario.MFAUnlock, messageToSend, emailConfigFunc);
+ await GetEmailSubejctAndHtmlInfoAndBuildAsync(mfaType == UserMFAType.Login ? EmailBusinessScenario.MFALogin : EmailBusinessScenario.MFAUnlock, messageToSend, emailConfigFunc, workLanguage);
var sucessHandle = GetEmailSuccessHandle(userId, verificationCode, emailAddress);
@@ -245,6 +249,11 @@ namespace IRaCIS.Core.Application.Service
public async Task SendEmailVerification(string emailAddress, int verificationCode)
{
+ //设置工作语言
+ var isEn_US = _userInfo.IsEn_Us;
+ var workLanguage = isEn_US ? UserWorkLanguage.US : UserWorkLanguage.CN;
+
+
var messageToSend = new MimeMessage();
//发件地址
messageToSend.From.Add(new MailboxAddress(_systemEmailConfig.FromName, _systemEmailConfig.FromEmail));
@@ -253,13 +262,13 @@ namespace IRaCIS.Core.Application.Service
//主题
//---[来自展影IRC]的提醒
- var companyName = _userInfo.IsEn_Us ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
+ var companyName = isEn_US ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
Func<(string topicStr, string htmlBodyStr), (string topicStr, string htmlBodyStr)> emailConfigFunc = input =>
{
var topicStr = string.Format(input.topicStr, companyName);
- var htmlBodyStr = string.Format(ReplaceCompanyName(input.htmlBodyStr),
+ var htmlBodyStr = string.Format(CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr, workLanguage),
"Sir/Madam",
//---您正在参与展影医疗IRC项目
@@ -271,7 +280,7 @@ namespace IRaCIS.Core.Application.Service
};
- await GetEmailSubejctAndHtmlInfoAndBuildAsync(EmailBusinessScenario.ReviewerLogin, messageToSend, emailConfigFunc);
+ await GetEmailSubejctAndHtmlInfoAndBuildAsync(EmailBusinessScenario.ReviewerLogin, messageToSend, emailConfigFunc, workLanguage);
//此时不知道用户
var sucessHandle = GetEmailSuccessHandle(Guid.Empty, verificationCode, emailAddress);
@@ -286,8 +295,10 @@ namespace IRaCIS.Core.Application.Service
//中心调研 登陆 发送验证码
public async Task AnolymousSendEmail(Guid trialId, string emailAddress, int verificationCode)
{
- //throw new BusinessValidationFailedException("模拟邮件取数据或者发送异常!!!");
+ //设置工作语言
+ var isEn_US = _userInfo.IsEn_Us;
+ var workLanguage = isEn_US ? UserWorkLanguage.US : UserWorkLanguage.CN;
var trialInfo = await _trialRepository.FirstOrDefaultAsync(t => t.Id == trialId);
@@ -299,13 +310,13 @@ namespace IRaCIS.Core.Application.Service
//主题
//$"[来自展影IRC] [{researchProgramNo}] 关于中心调研的提醒";
- var companyName = _userInfo.IsEn_Us ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
+ var companyName = isEn_US ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
Func<(string topicStr, string htmlBodyStr), (string topicStr, string htmlBodyStr)> emailConfigFunc = input =>
{
var topicStr = string.Format(input.topicStr, companyName, trialInfo.ResearchProgramNo);
- var htmlBodyStr = string.Format(ReplaceCompanyName(input.htmlBodyStr),
+ var htmlBodyStr = string.Format(CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr, workLanguage),
"Sir/Madam",
//---您正在参与展影医疗IRC项目中心调研工作
@@ -317,7 +328,7 @@ namespace IRaCIS.Core.Application.Service
};
- await GetEmailSubejctAndHtmlInfoAndBuildAsync(EmailBusinessScenario.SiteSurveyLogin, messageToSend, emailConfigFunc);
+ await GetEmailSubejctAndHtmlInfoAndBuildAsync(EmailBusinessScenario.SiteSurveyLogin, messageToSend, emailConfigFunc, workLanguage);
//此时不知道用户
var sucessHandle = GetEmailSuccessHandle(Guid.Empty, verificationCode, emailAddress);
@@ -335,6 +346,9 @@ namespace IRaCIS.Core.Application.Service
var sysUserInfo = (await _identityUserRepository.Where(t => t.Id == userId).Include(t => t.UserRoleList).ThenInclude(c => c.UserTypeRole).FirstOrDefaultAsync()).IfNullThrowException();
+ //设置工作语言
+ var workLanguage = sysUserInfo.UserWorkLanguage;
+ var isEn_US = workLanguage == UserWorkLanguage.US;
var messageToSend = new MimeMessage();
//发件地址
@@ -349,20 +363,19 @@ namespace IRaCIS.Core.Application.Service
await _identityUserRepository.BatchUpdateNoTrackingAsync(t => t.Id == sysUserInfo.Id, u => new IdentityUser() { EmailToken = token });
- routeUrl = routeUrl + "?UserId=" + sysUserInfo.Id + "&Email=" + sysUserInfo.EMail + "&UserName=" + sysUserInfo.UserName + "&lang=" + (_userInfo.IsEn_Us ? "en" : "zh") + "&access_token=" + token;
+ routeUrl = routeUrl + "?UserId=" + sysUserInfo.Id + "&Email=" + sysUserInfo.EMail + "&UserName=" + sysUserInfo.UserName + "&lang=" + (isEn_US ? "en" : "zh") + "&access_token=" + token;
var domain = baseUrl.Substring(0, baseUrl.IndexOf("/login"));
var redirectUrl = $"{domain}/api/User/UserRedirect?url={System.Web.HttpUtility.UrlEncode(routeUrl)}";
-
- var companyName = _userInfo.IsEn_Us ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
+ var companyName = isEn_US ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
Func<(string topicStr, string htmlBodyStr), (string topicStr, string htmlBodyStr)> emailConfigFunc = input =>
{
var topicStr = string.Format(input.topicStr, companyName);
- var htmlBodyStr = string.Format(ReplaceCompanyName(input.htmlBodyStr),
+ var htmlBodyStr = string.Format(CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr, workLanguage),
sysUserInfo.FullName,
sysUserInfo.EMail,
@@ -374,7 +387,7 @@ namespace IRaCIS.Core.Application.Service
};
- await GetEmailSubejctAndHtmlInfoAndBuildAsync(EmailBusinessScenario.SysCreateUser, messageToSend, emailConfigFunc);
+ await GetEmailSubejctAndHtmlInfoAndBuildAsync(EmailBusinessScenario.SysCreateUser, messageToSend, emailConfigFunc, sysUserInfo.UserWorkLanguage);
await SendEmailHelper.SendEmailAsync(messageToSend, _systemEmailConfig);
@@ -385,6 +398,11 @@ namespace IRaCIS.Core.Application.Service
{
var sysUserInfo = (await _identityUserRepository.Where(t => t.Id == userId).Include(t => t.UserRoleList).ThenInclude(c => c.UserTypeRole).FirstOrDefaultAsync()).IfNullThrowException();
+
+ //设置工作语言
+ var workLanguage = sysUserInfo.UserWorkLanguage;
+ var isEn_US = workLanguage == UserWorkLanguage.US;
+
var messageToSend = new MimeMessage();
//发件地址
messageToSend.From.Add(new MailboxAddress(_systemEmailConfig.FromName, _systemEmailConfig.FromEmail));
@@ -393,13 +411,13 @@ namespace IRaCIS.Core.Application.Service
//主题
//---[来自展影IRC] 关于重置账户密码的提醒
- var companyName = _userInfo.IsEn_Us ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
+ var companyName = isEn_US ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
Func<(string topicStr, string htmlBodyStr), (string topicStr, string htmlBodyStr)> emailConfigFunc = input =>
{
var topicStr = string.Format(input.topicStr, companyName);
- var htmlBodyStr = string.Format(ReplaceCompanyName(input.htmlBodyStr),
+ var htmlBodyStr = string.Format(CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr, workLanguage),
sysUserInfo.FullName,
sysUserInfo.UserName,
@@ -413,7 +431,7 @@ namespace IRaCIS.Core.Application.Service
};
- await GetEmailSubejctAndHtmlInfoAndBuildAsync(EmailBusinessScenario.SysResetPassword, messageToSend, emailConfigFunc);
+ await GetEmailSubejctAndHtmlInfoAndBuildAsync(EmailBusinessScenario.SysResetPassword, messageToSend, emailConfigFunc, sysUserInfo.UserWorkLanguage);
await SendEmailHelper.SendEmailAsync(messageToSend, _systemEmailConfig);
}
@@ -421,6 +439,10 @@ namespace IRaCIS.Core.Application.Service
//用户重置邮箱
public async Task SendMailEditEmail(Guid userId, string userName, string emailAddress, int verificationCode)
{
+ //设置工作语言
+ var workLanguage = _identityUserRepository.Where(t => t.Id == userId).Select(t => t.UserWorkLanguage).First();
+ var isEn_US = workLanguage == UserWorkLanguage.US;
+
var messageToSend = new MimeMessage();
//发件地址
@@ -430,13 +452,13 @@ namespace IRaCIS.Core.Application.Service
//主题
//---[来自展影IRC] 关于重置邮箱的提醒
- var companyName = _userInfo.IsEn_Us ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
+ var companyName = isEn_US ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
Func<(string topicStr, string htmlBodyStr), (string topicStr, string htmlBodyStr)> emailConfigFunc = input =>
{
var topicStr = string.Format(input.topicStr, companyName);
- var htmlBodyStr = string.Format(ReplaceCompanyName(input.htmlBodyStr),
+ var htmlBodyStr = string.Format(CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr, workLanguage),
userName,
//---您正在进行邮箱重置操作
@@ -447,7 +469,7 @@ namespace IRaCIS.Core.Application.Service
};
- await GetEmailSubejctAndHtmlInfoAndBuildAsync(EmailBusinessScenario.UserResetEmail, messageToSend, emailConfigFunc);
+ await GetEmailSubejctAndHtmlInfoAndBuildAsync(EmailBusinessScenario.UserResetEmail, messageToSend, emailConfigFunc, workLanguage);
var sucessHandle = GetEmailSuccessHandle(userId, verificationCode, emailAddress);
@@ -460,6 +482,11 @@ namespace IRaCIS.Core.Application.Service
//不登录 通过邮箱重置密码
public async Task AnolymousSendEmailForResetAccount(string emailAddress, int verificationCode)
{
+
+ //设置工作语言
+ var isEn_US = _userInfo.IsEn_Us;
+ var workLanguage = isEn_US ? UserWorkLanguage.US : UserWorkLanguage.CN;
+
var messageToSend = new MimeMessage();
//发件地址
messageToSend.From.Add(new MailboxAddress(_systemEmailConfig.FromName, _systemEmailConfig.FromEmail));
@@ -467,13 +494,13 @@ namespace IRaCIS.Core.Application.Service
messageToSend.To.Add(new MailboxAddress(String.Empty, emailAddress));
- var companyName = _userInfo.IsEn_Us ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
+ var companyName = isEn_US ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
Func<(string topicStr, string htmlBodyStr), (string topicStr, string htmlBodyStr)> emailConfigFunc = input =>
{
var topicStr = string.Format(input.topicStr, companyName);
- var htmlBodyStr = string.Format(ReplaceCompanyName(input.htmlBodyStr),
+ var htmlBodyStr = string.Format(CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr, workLanguage),
"Sir/Madam",
//---您正在进行邮箱重置密码操作
@@ -484,7 +511,7 @@ namespace IRaCIS.Core.Application.Service
};
- await GetEmailSubejctAndHtmlInfoAndBuildAsync(EmailBusinessScenario.UnloginUseEmailResetPassword, messageToSend, emailConfigFunc);
+ await GetEmailSubejctAndHtmlInfoAndBuildAsync(EmailBusinessScenario.UnloginUseEmailResetPassword, messageToSend, emailConfigFunc, workLanguage);
////此时不知道用户
var sucessHandle = GetEmailSuccessHandle(Guid.Empty, verificationCode, emailAddress);
@@ -500,6 +527,11 @@ namespace IRaCIS.Core.Application.Service
var sysUserInfo = (await _identityUserRepository.Where(t => t.Id == userId).FirstOrDefaultAsync()).IfNullThrowException();
+ //设置工作语言
+ var workLanguage = sysUserInfo.UserWorkLanguage;
+ var isEn_US = workLanguage == UserWorkLanguage.US;
+
+
var messageToSend = new MimeMessage();
//发件地址
messageToSend.From.Add(new MailboxAddress(_systemEmailConfig.FromName, _systemEmailConfig.FromEmail));
@@ -518,18 +550,18 @@ namespace IRaCIS.Core.Application.Service
var domain = baseUrl.Substring(0, baseUrl.IndexOf("/login"));
- var routeUrl = rootUrl + "?UserId=" + sysUserInfo.Id + "&Email=" + sysUserInfo.EMail + "&lang=" + (_userInfo.IsEn_Us ? "en" : "zh") + "&access_token=" + token;
+ var routeUrl = rootUrl + "?UserId=" + sysUserInfo.Id + "&Email=" + sysUserInfo.EMail + "&lang=" + (isEn_US ? "en" : "zh") + "&access_token=" + token;
var redirectUrl = $"{domain}/api/User/UserRedirect?url={System.Web.HttpUtility.UrlEncode(routeUrl)}";
- var companyName = _userInfo.IsEn_Us ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
+ var companyName = isEn_US ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
Func<(string topicStr, string htmlBodyStr), (string topicStr, string htmlBodyStr)> emailConfigFunc = input =>
{
var topicStr = string.Format(input.topicStr, companyName, trialInfo.ResearchProgramNo);
- var htmlBodyStr = string.Format(ReplaceCompanyName(input.htmlBodyStr),
+ var htmlBodyStr = string.Format(CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr, workLanguage),
sysUserInfo.FullName,
trialInfo.ExperimentName,
@@ -544,9 +576,9 @@ namespace IRaCIS.Core.Application.Service
};
- await GetEmailSubejctAndHtmlInfoAndBuildAsync(sysUserInfo.IsFirstAdd ? EmailBusinessScenario.SiteUseOrExternalUserFirstrJoinTrial : EmailBusinessScenario.SiteUserOrExternalUserExistJoinTrial, messageToSend, emailConfigFunc);
+ await GetEmailSubejctAndHtmlInfoAndBuildAsync(sysUserInfo.IsFirstAdd ? EmailBusinessScenario.SiteUseOrExternalUserFirstrJoinTrial : EmailBusinessScenario.SiteUserOrExternalUserExistJoinTrial, messageToSend, emailConfigFunc, workLanguage);
- await SendEmailHelper.SendEmailAsync(messageToSend, trialInfo, null);
+ await SendEmailHelper.SendEmailAsync(messageToSend, trialInfo);
}
@@ -555,10 +587,12 @@ namespace IRaCIS.Core.Application.Service
{
var sysUserInfo = (await _identityUserRepository.Where(t => t.Id == userId).FirstOrDefaultAsync()).IfNullThrowException();
-
-
var trialInfo = await _trialRepository.FirstOrDefaultAsync(t => t.Id == trialId);
+ //设置工作语言
+ var workLanguage = sysUserInfo.UserWorkLanguage;
+ var isEn_US = workLanguage == UserWorkLanguage.US;
+
var messageToSend = new MimeMessage();
//发件地址
messageToSend.From.Add(new MailboxAddress(_systemEmailConfig.FromName, _systemEmailConfig.FromEmail));
@@ -575,20 +609,20 @@ namespace IRaCIS.Core.Application.Service
await _identityUserRepository.BatchUpdateNoTrackingAsync(t => t.Id == sysUserInfo.Id, u => new Domain.Models.IdentityUser() { EmailToken = token });
}
- var routeUrl = rootUrl + "?UserId=" + sysUserInfo.Id + "&Email=" + sysUserInfo.EMail + "&lang=" + (_userInfo.IsEn_Us ? "en" : "zh") + "&access_token=" + token;
+ var routeUrl = rootUrl + "?UserId=" + sysUserInfo.Id + "&Email=" + sysUserInfo.EMail + "&lang=" + (isEn_US ? "en" : "zh") + "&access_token=" + token;
var domain = baseUrl.Substring(0, baseUrl.IndexOf("/login"));
var redirectUrl = $"{domain}/api/User/UserRedirect?url={System.Web.HttpUtility.UrlEncode(routeUrl)}";
- var companyName = _userInfo.IsEn_Us ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
+ var companyName = isEn_US ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
Func<(string topicStr, string htmlBodyStr), (string topicStr, string htmlBodyStr)> emailConfigFunc = input =>
{
var topicStr = string.Format(input.topicStr, companyName, trialInfo.ResearchProgramNo);
- var htmlBodyStr = string.Format(ReplaceCompanyName(input.htmlBodyStr),
+ var htmlBodyStr = string.Format(CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr, workLanguage),
sysUserInfo.FullName,
trialInfo.ExperimentName,
@@ -603,7 +637,7 @@ namespace IRaCIS.Core.Application.Service
};
- await GetEmailSubejctAndHtmlInfoAndBuildAsync(sysUserInfo.IsFirstAdd ? EmailBusinessScenario.SiteUseOrExternalUserFirstrJoinTrial : EmailBusinessScenario.SiteUserOrExternalUserExistJoinTrial, messageToSend, emailConfigFunc);
+ await GetEmailSubejctAndHtmlInfoAndBuildAsync(sysUserInfo.IsFirstAdd ? EmailBusinessScenario.SiteUseOrExternalUserFirstrJoinTrial : EmailBusinessScenario.SiteUserOrExternalUserExistJoinTrial, messageToSend, emailConfigFunc, workLanguage);
await SendEmailHelper.SendEmailAsync(messageToSend, trialInfo);
@@ -644,6 +678,12 @@ namespace IRaCIS.Core.Application.Service
saveItem.IsTestUser = true;
}
+
+ //通过医生信息,设置工作语言
+ var nation = _doctorTypeRepository.Where(t => t.Id == doctorId).Select(t => t.Nation).FirstOrDefault();
+ saveItem.UserWorkLanguage = nation == DoctorNation.CN ? UserWorkLanguage.CN : UserWorkLanguage.US;
+
+
saveItem.UserCeateSource = UserCeateSource.ReviewerSelect;
saveItem.TrialId = trialId;
@@ -686,6 +726,11 @@ namespace IRaCIS.Core.Application.Service
var trialInfo = await _trialRepository.FirstOrDefaultAsync(t => t.Id == trialId);
+
+ //设置工作语言
+ var workLanguage = sysUserInfo.UserWorkLanguage;
+ var isEn_US = workLanguage == UserWorkLanguage.US;
+
var messageToSend = new MimeMessage();
//发件地址
messageToSend.From.Add(new MailboxAddress(_systemEmailConfig.FromName, _systemEmailConfig.FromEmail));
@@ -701,7 +746,7 @@ namespace IRaCIS.Core.Application.Service
await _identityUserRepository.BatchUpdateNoTrackingAsync(t => t.Id == sysUserInfo.Id, u => new Domain.Models.IdentityUser() { EmailToken = token });
}
- var routeUrl = rootUrl + "?UserId=" + sysUserInfo.Id + "&Email=" + sysUserInfo.EMail + "&lang=" + (_userInfo.IsEn_Us ? "en" : "zh") + "&access_token=" + token;
+ var routeUrl = rootUrl + "?UserId=" + sysUserInfo.Id + "&Email=" + sysUserInfo.EMail + "&lang=" + (isEn_US ? "en" : "zh") + "&access_token=" + token;
var domain = baseUrl.Substring(0, baseUrl.IndexOf("/login"));
@@ -711,13 +756,13 @@ namespace IRaCIS.Core.Application.Service
// $"[来自展影IRC] [{trialInfo.ResearchProgramNo}]邀请信";
- var companyName = _userInfo.IsEn_Us ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
+ var companyName = isEn_US ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
Func<(string topicStr, string htmlBodyStr), (string topicStr, string htmlBodyStr)> emailConfigFunc = input =>
{
var topicStr = string.Format(input.topicStr, companyName, trialInfo.ResearchProgramNo);
- var htmlBodyStr = string.Format(ReplaceCompanyName(input.htmlBodyStr),
+ var htmlBodyStr = string.Format(CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr, workLanguage),
sysUserInfo.FullName,
trialInfo.ExperimentName,
@@ -732,7 +777,7 @@ namespace IRaCIS.Core.Application.Service
};
- await GetEmailSubejctAndHtmlInfoAndBuildAsync(sysUserInfo.IsFirstAdd ? EmailBusinessScenario.DoctorUserFirstJoinTrial : EmailBusinessScenario.DoctorUserExistJoinTrial, messageToSend, emailConfigFunc);
+ await GetEmailSubejctAndHtmlInfoAndBuildAsync(sysUserInfo.IsFirstAdd ? EmailBusinessScenario.DoctorUserFirstJoinTrial : EmailBusinessScenario.DoctorUserExistJoinTrial, messageToSend, emailConfigFunc,workLanguage);
await SendEmailHelper.SendEmailAsync(messageToSend, trialInfo, null);
@@ -754,6 +799,10 @@ namespace IRaCIS.Core.Application.Service
{
var feedBack = await _userFeedBackRepository.Where(t => t.Id == feedBackId).Include(t => t.CreateUserRole.UserTypeRole).Include(t => t.CreateUserRole.IdentityUser).FirstNotNullAsync();
+ //设置工作语言
+ var workLanguage = _identityUserRepository.Where(t => t.Id == _userInfo.IdentityUserId).Select(t => t.UserWorkLanguage).First();
+ var isEn_US = workLanguage == UserWorkLanguage.US;
+
var messageToSend = new MimeMessage();
//发件地址
messageToSend.From.Add(new MailboxAddress(_systemEmailConfig.FromName, _systemEmailConfig.FromEmail));
@@ -762,7 +811,7 @@ namespace IRaCIS.Core.Application.Service
var trialinfo = await _trialRepository.Where(x => x.Id == feedBack.TrialId).FirstOrDefaultAsync();
- var companyName = _userInfo.IsEn_Us ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
+ var companyName = isEn_US ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
var emialScenario = feedBack.VisitTaskId != null ? EmailBusinessScenario.IRImageError : (feedBack.SubjectVisitId != null ? EmailBusinessScenario.TrialSubjectVisitFeedBack : (feedBack.TrialId != null ? EmailBusinessScenario.TrialFeedBack : EmailBusinessScenario.SysFeedBack));
@@ -785,7 +834,7 @@ namespace IRaCIS.Core.Application.Service
if (feedBack.VisitTaskId != null)
{
- var emailType = await _dictionaryRepository.Where(t => t.Parent.Code == "FeedBackTypeToIR" && t.ParentId != null && t.Code == ((int)feedBack.QuestionType).ToString()).Select(t => _userInfo.IsEn_Us ? t.Value : t.ValueCN).FirstOrDefaultAsync();
+ var emailType = await _dictionaryRepository.Where(t => t.Parent.Code == "FeedBackTypeToIR" && t.ParentId != null && t.Code == ((int)feedBack.QuestionType).ToString()).Select(t => isEn_US ? t.Value : t.ValueCN).FirstOrDefaultAsync();
var info = await _visitTaskRepository.Where(t => t.Id == feedBack.VisitTaskId).Select(t => new { t.Trial.ResearchProgramNo, t.Trial.TrialCode, SubejctCode = t.Subject.Code, t.SourceSubjectVisit.VisitName }).FirstNotNullAsync();
@@ -794,7 +843,7 @@ namespace IRaCIS.Core.Application.Service
{
var topicStr = string.Format(input.topicStr, info.ResearchProgramNo, info.SubejctCode, info.VisitName);
- var htmlBodyStr = string.Format(ReplaceCompanyName(input.htmlBodyStr),
+ var htmlBodyStr = string.Format(CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr, workLanguage),
userNames,
info.TrialCode,
info.SubejctCode,
@@ -810,12 +859,12 @@ namespace IRaCIS.Core.Application.Service
};
- await GetEmailSubejctAndHtmlInfoAndBuildAsync(EmailBusinessScenario.IRImageError, messageToSend, emailConfigFunc);
+ await GetEmailSubejctAndHtmlInfoAndBuildAsync(EmailBusinessScenario.IRImageError, messageToSend, emailConfigFunc, workLanguage);
}
else if (feedBack.SubjectVisitId != null)
{
- var emailType = await _dictionaryRepository.Where(t => t.Parent.Code == "Email_BusinessScenario" && t.ParentId != null && t.Code == ((int)EmailBusinessScenario.TrialSubjectVisitFeedBack).ToString()).Select(t => _userInfo.IsEn_Us ? t.Value : t.ValueCN).FirstOrDefaultAsync();
+ var emailType = await _dictionaryRepository.Where(t => t.Parent.Code == "Email_BusinessScenario" && t.ParentId != null && t.Code == ((int)EmailBusinessScenario.TrialSubjectVisitFeedBack).ToString()).Select(t => isEn_US ? t.Value : t.ValueCN).FirstOrDefaultAsync();
var info = await _subjectVisitRepository.Where(t => t.Id == feedBack.SubjectVisitId).Select(t => new { t.Trial.ResearchProgramNo, t.Trial.TrialCode, SubejctCode = t.Subject.Code, t.VisitName }).FirstNotNullAsync();
@@ -824,7 +873,7 @@ namespace IRaCIS.Core.Application.Service
{
var topicStr = string.Format(input.topicStr, info.ResearchProgramNo, info.SubejctCode, info.VisitName);
- var htmlBodyStr = string.Format(ReplaceCompanyName(input.htmlBodyStr),
+ var htmlBodyStr = string.Format(CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr, workLanguage),
userNames,
info.TrialCode,
info.SubejctCode,
@@ -840,13 +889,13 @@ namespace IRaCIS.Core.Application.Service
};
- await GetEmailSubejctAndHtmlInfoAndBuildAsync(EmailBusinessScenario.TrialSubjectVisitFeedBack, messageToSend, emailConfigFunc);
+ await GetEmailSubejctAndHtmlInfoAndBuildAsync(EmailBusinessScenario.TrialSubjectVisitFeedBack, messageToSend, emailConfigFunc, workLanguage);
}
//项目相关的反馈 pm admin
else if (feedBack.TrialId != null)
{
- var emailType = await _dictionaryRepository.Where(t => t.Parent.Code == "Email_BusinessScenario" && t.ParentId != null && t.Code == ((int)EmailBusinessScenario.TrialFeedBack).ToString()).Select(t => _userInfo.IsEn_Us ? t.Value : t.ValueCN).FirstOrDefaultAsync();
+ var emailType = await _dictionaryRepository.Where(t => t.Parent.Code == "Email_BusinessScenario" && t.ParentId != null && t.Code == ((int)EmailBusinessScenario.TrialFeedBack).ToString()).Select(t => isEn_US ? t.Value : t.ValueCN).FirstOrDefaultAsync();
var info = await _trialRepository.Where(t => t.Id == feedBack.TrialId).Select(t => new { t.ResearchProgramNo, t.TrialCode }).FirstNotNullAsync();
@@ -855,7 +904,7 @@ namespace IRaCIS.Core.Application.Service
{
var topicStr = string.Format(input.topicStr, info.ResearchProgramNo);
- var htmlBodyStr = string.Format(ReplaceCompanyName(input.htmlBodyStr),
+ var htmlBodyStr = string.Format(CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr, workLanguage),
userNames,
info.TrialCode,
feedBack.CreateUserRole.UserTypeRole.UserTypeShortName,
@@ -869,7 +918,7 @@ namespace IRaCIS.Core.Application.Service
};
- await GetEmailSubejctAndHtmlInfoAndBuildAsync(EmailBusinessScenario.TrialFeedBack, messageToSend, emailConfigFunc);
+ await GetEmailSubejctAndHtmlInfoAndBuildAsync(EmailBusinessScenario.TrialFeedBack, messageToSend, emailConfigFunc, workLanguage);
}
//项目无关的反馈 admin
@@ -881,7 +930,7 @@ namespace IRaCIS.Core.Application.Service
{
var topicStr = string.Format(input.topicStr);
- var htmlBodyStr = string.Format(ReplaceCompanyName(input.htmlBodyStr),
+ var htmlBodyStr = string.Format(CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr, workLanguage),
userNames,
feedBack.CreateUserRole.UserTypeRole.UserTypeShortName,
feedBack.CreateUserRole.IdentityUser.FullName,
@@ -893,7 +942,7 @@ namespace IRaCIS.Core.Application.Service
};
- await GetEmailSubejctAndHtmlInfoAndBuildAsync(EmailBusinessScenario.SysFeedBack, messageToSend, emailConfigFunc);
+ await GetEmailSubejctAndHtmlInfoAndBuildAsync(EmailBusinessScenario.SysFeedBack, messageToSend, emailConfigFunc, workLanguage);
}
if (trialinfo == null)
@@ -904,7 +953,7 @@ namespace IRaCIS.Core.Application.Service
{
await SendEmailHelper.SendEmailAsync(messageToSend, trialinfo);
}
-
+
}
@@ -915,6 +964,10 @@ namespace IRaCIS.Core.Application.Service
var sysUserInfo = (await _identityUserRepository.Where(t => t.Id == userId).FirstOrDefaultAsync()).IfNullThrowException();
+ //设置工作语言
+ var workLanguage = sysUserInfo.UserWorkLanguage;
+ var isEn_US = workLanguage == UserWorkLanguage.US;
+
var messageToSend = new MimeMessage();
//发件地址
messageToSend.From.Add(new MailboxAddress(_systemEmailConfig.FromName, _systemEmailConfig.FromEmail));
@@ -923,13 +976,13 @@ namespace IRaCIS.Core.Application.Service
//主题
//---[来自展影IRC] 关于重置邮箱的提醒
- var companyName = _userInfo.IsEn_Us ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
+ var companyName = isEn_US ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
Func<(string topicStr, string htmlBodyStr), (string topicStr, string htmlBodyStr)> emailConfigFunc = input =>
{
var topicStr = string.Format(input.topicStr, companyName);
- var htmlBodyStr = string.Format(ReplacePlatformName(ReplaceCompanyName(input.htmlBodyStr)) ,
+ var htmlBodyStr = string.Format(CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr, workLanguage),
sysUserInfo.FullName
);
@@ -938,7 +991,7 @@ namespace IRaCIS.Core.Application.Service
};
- await GetEmailSubejctAndHtmlInfoAndBuildAsync(EmailBusinessScenario.IdentityUser_ModifyPassword, messageToSend, emailConfigFunc);
+ await GetEmailSubejctAndHtmlInfoAndBuildAsync(EmailBusinessScenario.IdentityUser_ModifyPassword, messageToSend, emailConfigFunc, workLanguage);
await SendEmailHelper.SendEmailAsync(messageToSend, _systemEmailConfig);
}
@@ -948,6 +1001,10 @@ namespace IRaCIS.Core.Application.Service
{
var trialInfo = await _trialRepository.Where(t => t.Id == trialId).FirstOrDefaultAsync();
+ //设置工作语言
+ var isEn_US = _userInfo.IsEn_Us;
+ var workLanguage = isEn_US ? UserWorkLanguage.US : UserWorkLanguage.CN;
+
var messageToSend = new MimeMessage();
//发件地址
messageToSend.From.Add(new MailboxAddress(_systemEmailConfig.FromName, _systemEmailConfig.FromEmail));
@@ -956,14 +1013,14 @@ namespace IRaCIS.Core.Application.Service
//主题
//---[来自展影IRC] 关于重置邮箱的提醒
- var companyName = _userInfo.IsEn_Us ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
+ var companyName = isEn_US ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
Func<(string topicStr, string htmlBodyStr), (string topicStr, string htmlBodyStr)> emailConfigFunc = input =>
{
var topicStr = string.Format(input.topicStr, companyName, trialInfo.ResearchProgramNo);
- var htmlBodyStr = string.Format(ReplaceCompanyName(input.htmlBodyStr),
+ var htmlBodyStr = string.Format(CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr, workLanguage),
name,
trialInfo.TrialCode,
trialInfo.ResearchProgramNo
@@ -979,9 +1036,15 @@ namespace IRaCIS.Core.Application.Service
public async Task SiteSuervyUpdateUser(Guid trialSiteId, string email, string name, string url)
{
- var siteInfo = await _trialSiteRepository.Where(t => t.Id == trialSiteId).Include(x=>x.Trial).FirstOrDefaultAsync();
+ var siteInfo = await _trialSiteRepository.Where(t => t.Id == trialSiteId).Include(x => x.Trial).FirstOrDefaultAsync();
var trialInfo = siteInfo.Trial;
+
+
+ //设置工作语言
+ var isEn_US = _userInfo.IsEn_Us;
+ var workLanguage = isEn_US ? UserWorkLanguage.US : UserWorkLanguage.CN;
+
var messageToSend = new MimeMessage();
//发件地址
messageToSend.From.Add(new MailboxAddress(_systemEmailConfig.FromName, _systemEmailConfig.FromEmail));
@@ -990,14 +1053,14 @@ namespace IRaCIS.Core.Application.Service
//主题
//---[来自展影IRC] 关于重置邮箱的提醒
- var companyName = _userInfo.IsEn_Us ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
+ var companyName = isEn_US ? _systemEmailConfig.CompanyShortName : _systemEmailConfig.CompanyShortNameCN;
Func<(string topicStr, string htmlBodyStr), (string topicStr, string htmlBodyStr)> emailConfigFunc = input =>
{
var topicStr = string.Format(input.topicStr, companyName, trialInfo.ResearchProgramNo);
- var htmlBodyStr = string.Format(ReplaceCompanyName(input.htmlBodyStr),
+ var htmlBodyStr = string.Format(CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, input.htmlBodyStr, workLanguage),
name,
trialInfo.TrialCode,
siteInfo.TrialSiteCode,
diff --git a/IRaCIS.Core.Application/Service/Common/TrialImageDownloadService.cs b/IRaCIS.Core.Application/Service/Common/TrialImageDownloadService.cs
index f43b076b9..a23703c16 100644
--- a/IRaCIS.Core.Application/Service/Common/TrialImageDownloadService.cs
+++ b/IRaCIS.Core.Application/Service/Common/TrialImageDownloadService.cs
@@ -17,6 +17,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using MiniExcelLibs;
+using NPOI.Util;
using SharpCompress.Common;
using System;
using System.Collections.Generic;
@@ -49,7 +50,51 @@ namespace IRaCIS.Core.Application.Service
+ private static async Task TryWriteMergedDicomAsync(Func> sourceFactory, Stream output)
+ {
+ try
+ {
+ await using var source = await sourceFactory();
+ // 如果你是从 stream 打开
+ var dicomFile = await DicomFile.OpenAsync(source);
+ //获取像素是否为封装形式
+ var syntax = dicomFile.Dataset.InternalTransferSyntax;
+
+ //对于封装像素的文件做转换
+ if (syntax.IsEncapsulated)
+ {
+ // 获取 Pixel Data 标签
+ var pixelData = DicomPixelData.Create(dicomFile.Dataset);
+
+ // 创建一个新的片段序列
+ var newFragments = new DicomOtherByteFragment(DicomTag.PixelData);
+ // 获取每帧数据并封装为单独的片段
+ for (int n = 0; n < pixelData.NumberOfFrames; n++)
+ {
+ var frameData = pixelData.GetFrame(n);
+ newFragments.Fragments.Add(new MemoryByteBuffer(frameData.Data));
+ }
+
+ var frag = dicomFile.Dataset.GetDicomItem(DicomTag.PixelData);
+
+ var originOffsetTable = frag?.OffsetTable;
+
+ newFragments.OffsetTable.AddRange(originOffsetTable?.ToArray());
+ // 替换原有的片段序列
+ dicomFile.Dataset.AddOrUpdate(newFragments);
+ }
+
+ await dicomFile.SaveAsync(output);
+ return true;
+ }
+ catch (Exception ex)
+ {
+ // 只记录,不传播
+ Log.Logger.Warning($"TryWriteMergedDicomAsync failed: {ex.Message}");
+ return false;
+ }
+ }
///
@@ -61,6 +106,9 @@ namespace IRaCIS.Core.Application.Service
[AllowAnonymous]
public async Task DownloadTrialImage(Guid trialId)
{
+ //找到项目里面未阅片的影像
+
+
//var subjectCodeList = new List() { "05002", "07006", "07026" };
var downloadInfo = _trialRepository.Where(t => t.Id == trialId).Select(t => new
{
@@ -85,7 +133,9 @@ namespace IRaCIS.Core.Application.Service
InstancePathList = z.DicomInstanceList.Where(t => t.IsReading).Select(k => new
{
- k.Path
+ k.Path,
+ k.IsEncapsulated,
+ k.NumberOfFrames,
}).ToList()
})
@@ -118,9 +168,9 @@ namespace IRaCIS.Core.Application.Service
{
var downloadJobs = new List>();
- var rootFolder = @"E:\DownloadImage";
+ //var rootFolder = @"E:\DownloadImage";
- //var rootFolder = FileStoreHelper.GetDonwnloadImageFolder(_hostEnvironment);
+ var rootFolder = FileStoreHelper.GetDonwnloadImageFolder(_hostEnvironment);
// 获取无效字符(系统定义的)
string invalidChars = new string(Path.GetInvalidFileNameChars()) + new string(Path.GetInvalidPathChars());
@@ -168,9 +218,18 @@ namespace IRaCIS.Core.Application.Service
// 复制文件到相应的文件夹
string destinationPath = Path.Combine(studyDicomFolderPath, Path.GetFileName(instanceInfo.Path));
-
- //加入到下载任务里
- downloadJobs.Add(() => _oSSService.DownLoadFromOSSAsync(instanceInfo.Path, destinationPath));
+ await using var output = File.Create(destinationPath);
+ if (instanceInfo.IsEncapsulated)
+ {
+ //加入到下载任务里
+ downloadJobs.Add(() => TryWriteMergedDicomAsync(() => _oSSService.GetStreamFromOSSAsync(instanceInfo.Path), output));
+ }
+ else
+ {
+ //加入到下载任务里
+ downloadJobs.Add(() => _oSSService.DownLoadFromOSSAsync(instanceInfo.Path, destinationPath));
+ }
+
//下载到当前目录
//await _oSSService.DownLoadFromOSSAsync(instanceInfo.Path, destinationPath);
@@ -213,7 +272,7 @@ namespace IRaCIS.Core.Application.Service
}
catch (Exception ex)
{
- Console.WriteLine($"下载失败: {ex.Message}");
+ Log.Logger.Error($"下载失败: {ex.Message}");
}
downloadedCount++;
@@ -221,7 +280,9 @@ namespace IRaCIS.Core.Application.Service
// 每处理50个,输出一次进度(或最后一个时也输出)
if (downloadedCount % 50 == 0 || downloadedCount == totalCount)
{
- Console.WriteLine($"已下载 {downloadedCount} / {totalCount} 个文件,完成 {(downloadedCount * 100.0 / totalCount):F2}%");
+
+ Log.Logger.Error($"已下载 {downloadedCount} / {totalCount} 个文件,完成 {(downloadedCount * 100.0 / totalCount):F2}%");
+
}
}
#endregion
diff --git a/IRaCIS.Core.Application/Service/Common/_MapConfig.cs b/IRaCIS.Core.Application/Service/Common/_MapConfig.cs
index cd499bc0a..20114af4a 100644
--- a/IRaCIS.Core.Application/Service/Common/_MapConfig.cs
+++ b/IRaCIS.Core.Application/Service/Common/_MapConfig.cs
@@ -20,7 +20,7 @@ namespace IRaCIS.Core.Application.Service
CreateMap()
.ForMember(t => t.RecipientList, u => u.MapFrom(c => c.EmailRecipientLogList))
- .ForMember(t => t.AttachmentList, u => u.MapFrom(c => c.AttachmentList));
+ .ForMember(t => t.AttachmentList, u => u.MapFrom(c => c.AttachmentList));
CreateMap().ReverseMap();
@@ -41,7 +41,7 @@ namespace IRaCIS.Core.Application.Service
.ForMember(t => t.EmailNoticeUserList, u => u.MapFrom(c => c.EmailNoticeUserTypeList));
CreateMap();
-
+
CreateMap()
.ForMember(t => t.ParentCode, u => u.MapFrom(c => c.Parent.Code));
@@ -89,14 +89,14 @@ namespace IRaCIS.Core.Application.Service
CreateMap().ReverseMap();
CreateMap();
-
+
CreateMap().ReverseMap();
CreateMap();
CreateMap();
-
+
CreateMap();
@@ -116,7 +116,7 @@ namespace IRaCIS.Core.Application.Service
.ForMember(d => d.TrialSiteCode, u => u.MapFrom(s => s.Subject.TrialSite.TrialSiteCode));
- CreateMap();
+ CreateMap();
CreateMap();
CreateMap();
CreateMap();
@@ -124,6 +124,21 @@ namespace IRaCIS.Core.Application.Service
CreateMap();
CreateMap();
+
+ CreateMap()
+ .ForMember(d => d.SubjectCode, u => u.MapFrom(s => s.Subject.Code))
+ .ForMember(d => d.VisitName, u => u.MapFrom(s => s.SubjectVisit.VisitName));
+ CreateMap().ReverseMap();
+
+
+ CreateMap()
+ .ForMember(d => d.FileName, u => u.MapFrom(s => s.FileUploadRecord.FileName))
+ .ForMember(d => d.FileType, u => u.MapFrom(s => s.FileUploadRecord.FileType))
+ .ForMember(d => d.Path, u => u.MapFrom(s => s.FileUploadRecord.Path))
+ .ForMember(d => d.StudyCode, u => u.MapFrom(s => s.FileUploadRecord.StudyCode))
+ .ForMember(d => d.SubjectCode, u => u.MapFrom(s => s.FileUploadRecord.Subject.Code))
+ .ForMember(d => d.VisitName, u => u.MapFrom(s => s.FileUploadRecord.SubjectVisit.VisitName));
+
}
}
diff --git a/IRaCIS.Core.Application/Service/Doctor/DTO/DoctorModel.cs b/IRaCIS.Core.Application/Service/Doctor/DTO/DoctorModel.cs
index f1c84a99d..a1da04273 100644
--- a/IRaCIS.Core.Application/Service/Doctor/DTO/DoctorModel.cs
+++ b/IRaCIS.Core.Application/Service/Doctor/DTO/DoctorModel.cs
@@ -149,7 +149,7 @@ namespace IRaCIS.Application.Contracts
public bool? AcceptingNewTrial { get; set; }//是否接受新的项目
public bool? ActivelyReading { get; set; }// 是否接受新的读片任务
- public int? Nation { get; set; }// 0-中国医生,2-美国医生,3-全部
+ public DoctorNation? Nation { get; set; }// 0-中国医生,2-美国医生,3-全部
}
///
diff --git a/IRaCIS.Core.Application/Service/Doctor/DoctorService.cs b/IRaCIS.Core.Application/Service/Doctor/DoctorService.cs
index a796f1d85..d5ffe7069 100644
--- a/IRaCIS.Core.Application/Service/Doctor/DoctorService.cs
+++ b/IRaCIS.Core.Application/Service/Doctor/DoctorService.cs
@@ -234,6 +234,7 @@ namespace IRaCIS.Core.Application.Service
//重新插入新的 Title记录
updateModel.TitleIds.ForEach(titleId => adddata.Add(new DoctorDictionary() { DoctorId = updateModel.Id.Value, KeyName = StaticData.Title, DictionaryId = titleId }));
+ indto.ReviewerCode = doctor.ReviewerCode;
await _doctorDictionaryRepository.AddRangeAsync(adddata);
_mapper.Map(indto, doctor);
diff --git a/IRaCIS.Core.Application/Service/Document/DTO/SystemDocumentViewModel.cs b/IRaCIS.Core.Application/Service/Document/DTO/SystemDocumentViewModel.cs
index e7ce1734a..02e7e2dde 100644
--- a/IRaCIS.Core.Application/Service/Document/DTO/SystemDocumentViewModel.cs
+++ b/IRaCIS.Core.Application/Service/Document/DTO/SystemDocumentViewModel.cs
@@ -50,6 +50,7 @@ namespace IRaCIS.Core.Application.Contracts
public bool? OffLine { get; set; }
public Guid? SystemDocumentId { get; set; }
+
}
@@ -205,7 +206,7 @@ namespace IRaCIS.Core.Application.Contracts
public DateTime? EndCreateTime { get; set; }
-
+ public DocLanguageType? DocLanguageType { get; set; }
}
public class GetNextUnSignDocumentInDto
@@ -362,6 +363,8 @@ namespace IRaCIS.Core.Application.Contracts
public bool IsPublish { get; set; } = false;
+ public DocLanguageType DocLanguageType { get; set; }
+
}
diff --git a/IRaCIS.Core.Application/Service/Document/EmailSendService.cs b/IRaCIS.Core.Application/Service/Document/EmailSendService.cs
index e00fec414..1952a0a69 100644
--- a/IRaCIS.Core.Application/Service/Document/EmailSendService.cs
+++ b/IRaCIS.Core.Application/Service/Document/EmailSendService.cs
@@ -62,42 +62,51 @@ namespace IRaCIS.Core.Application.Service
}).FirstNotNullAsync();
- var isEn_us = _userInfo.IsEn_Us;
+ //找到当前收件人语言
+ var workLanguageList = _trialUserRoleRepository.Where(t => t.TrialId == taskInfo.TrialId && t.UserRole.UserTypeEnum == UserTypeEnum.ClinicalResearchCoordinator).Select(t => t.UserRole.IdentityUser.UserWorkLanguage).Distinct().ToList();
- var resultStr = isEn_us ? (result == true ? "Yes" : "No") : (result == true ? "是" : "否");
-
- if (isEnrollment == true)
+ foreach (var workLanguage in workLanguageList)
{
- Func topicAndHtmlFunc = trialEmailConfig =>
+ //设置工作语言
+ var isEn_us = workLanguage == UserWorkLanguage.US;
+
+ var resultStr = isEn_us ? (result == true ? "Yes" : "No") : (result == true ? "是" : "否");
+
+
+
+ if (isEnrollment == true)
{
- var topicStr = string.Format(isEn_us ? trialEmailConfig.EmailTopic : trialEmailConfig.EmailTopicCN, taskInfo.ResearchProgramNo, taskInfo.SubjectCode);
+ Func topicAndHtmlFunc = trialEmailConfig =>
+ {
+ var topicStr = string.Format(isEn_us ? trialEmailConfig.EmailTopic : trialEmailConfig.EmailTopicCN, taskInfo.ResearchProgramNo, taskInfo.SubjectCode);
- var htmlBodyStr = string.Format(isEn_us ? trialEmailConfig.EmailHtmlContent : trialEmailConfig.EmailHtmlContentCN,
- EmailNamePlaceholder, taskInfo.ResearchProgramNo, taskInfo.SubjectCode, resultStr);
+ var htmlBodyStr = string.Format(isEn_us ? trialEmailConfig.EmailHtmlContent : trialEmailConfig.EmailHtmlContentCN,
+ EmailNamePlaceholder, taskInfo.ResearchProgramNo, taskInfo.SubjectCode, resultStr);
- return (topicStr, htmlBodyStr, isEn_us, null);
- };
+ return (topicStr, htmlBodyStr, isEn_us, null);
+ };
- await SendTrialEmailAsync(taskInfo.TrialId, taskInfo.CriterionType, businessScenarioEnum, topicAndHtmlFunc, taskInfo.TrialSiteId);
- }
- else
- {
- Func topicAndHtmlFunc = trialEmailConfig =>
+ await SendTrialEmailAsync(taskInfo.TrialId, taskInfo.CriterionType, businessScenarioEnum, topicAndHtmlFunc, taskInfo.TrialSiteId);
+ }
+ else
{
- var topicStr = string.Format(isEn_us ? trialEmailConfig.EmailTopic : trialEmailConfig.EmailTopicCN, taskInfo.ResearchProgramNo, taskInfo.SubjectCode, taskInfo.VisitName);
+ Func topicAndHtmlFunc = trialEmailConfig =>
+ {
+ var topicStr = string.Format(isEn_us ? trialEmailConfig.EmailTopic : trialEmailConfig.EmailTopicCN, taskInfo.ResearchProgramNo, taskInfo.SubjectCode, taskInfo.VisitName);
- var htmlBodyStr = string.Format(isEn_us ? trialEmailConfig.EmailHtmlContent : trialEmailConfig.EmailHtmlContentCN,
- EmailNamePlaceholder, taskInfo.ResearchProgramNo, taskInfo.SubjectCode, taskInfo.VisitName, resultStr);
+ var htmlBodyStr = string.Format(isEn_us ? trialEmailConfig.EmailHtmlContent : trialEmailConfig.EmailHtmlContentCN,
+ EmailNamePlaceholder, taskInfo.ResearchProgramNo, taskInfo.SubjectCode, taskInfo.VisitName, resultStr);
- return (topicStr, htmlBodyStr, isEn_us, null);
- };
+ return (topicStr, htmlBodyStr, isEn_us, null);
+ };
+
+ await SendTrialEmailAsync(taskInfo.TrialId, taskInfo.CriterionType, businessScenarioEnum, topicAndHtmlFunc, taskInfo.TrialSiteId);
+ }
- await SendTrialEmailAsync(taskInfo.TrialId, taskInfo.CriterionType, businessScenarioEnum, topicAndHtmlFunc, taskInfo.TrialSiteId);
}
-
}
@@ -133,8 +142,12 @@ namespace IRaCIS.Core.Application.Service
var (topicStr, htmlBodyStr, isEn_us, onlyToUserId) = topicAndHtmlFunc(trialEmailConfig);
+ //设置工作语言
+ var workLanguage = isEn_us ? UserWorkLanguage.US : UserWorkLanguage.CN;
+
+
//处理替换公司名
- htmlBodyStr = CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, htmlBodyStr);
+ htmlBodyStr = CommonEmailHelper.ReplaceCompanyName(_systemEmailConfig, htmlBodyStr, workLanguage);
sendEmailConfig.TopicDescription = topicStr;
@@ -150,10 +163,10 @@ namespace IRaCIS.Core.Application.Service
var allUserTypeEnumList = toUserTypeEnumList.Union(copyUserTypeEnumList).Distinct().ToList();
- var allUserList = await _trialUserRoleRepository.Where(t => t.TrialId == trialId && allUserTypeEnumList.Contains(t.UserRole.UserTypeEnum)).Select(t => new { t.UserId, t.UserRole.IdentityUser.EMail, t.UserRole.FullName, t.UserRole.UserTypeEnum }).ToListAsync();
+ var allUserList = await _trialUserRoleRepository.Where(t => t.TrialId == trialId && allUserTypeEnumList.Contains(t.UserRole.UserTypeEnum)).Select(t => new { t.UserId, t.UserRole.IdentityUser.EMail, t.UserRole.FullName, t.UserRole.UserTypeEnum, t.UserRole.IdentityUser.UserWorkLanguage }).ToListAsync();
-
- var toUserList = allUserList.Where(t => toUserTypeEnumList.Contains(t.UserTypeEnum))
+ //过滤当前语言
+ var toUserList = allUserList.Where(t => toUserTypeEnumList.Contains(t.UserTypeEnum)).Where(t=>t.UserWorkLanguage==workLanguage)
.ToList();
//收件人 有CRC CRA , CRC CRA的账户要按照中心发送
@@ -191,11 +204,6 @@ namespace IRaCIS.Core.Application.Service
sendEmailConfig.HtmlBodyStr = htmlBodyStr.Replace(EmailNamePlaceholder, string.Join(isEn_us ? ", " : "、", toUserList.Select(t => t.FullName).ToList()));
}
- if (toUserList.Count == 0)
- {
- //---没有收件人,无法发送邮件
- throw new BusinessValidationFailedException(_localizer["TrialEmailN_NoRecipient"]);
- }
if (trialEmailConfig.FromEmail.Contains("@") && !string.IsNullOrEmpty(trialEmailConfig.FromEmail))
@@ -235,20 +243,7 @@ namespace IRaCIS.Core.Application.Service
}
}
- //邮件附件 这里是原格式发送,不是PDF
- //if (trialEmailConfig.AttachCNPath != string.Empty && trialEmailConfig.AttachPath != string.Empty)
- //{
- // var phyPath = FileStoreHelper.GetPhysicalFilePath(_hostEnvironment, isEn_us? trialEmailConfig.AttachName: trialEmailConfig.AttachNameCN);
-
- // //先预先生成了邮件,发送预先生成的邮件
- // sendEmailConfig.EmailAttachMentConfigList.Add(new EmailAttachMentConfig()
- // {
- // FileName = $"{attachPrefix}_{Path.GetFileName(_userInfo.IsEn_Us ? trialEmailConfig.AttachName : trialEmailConfig.AttachNameCN)}",
-
- // FileStream = File.OpenRead(phyPath),
- // });
- //}
return (trialEmailConfig, sendEmailConfig);
diff --git a/IRaCIS.Core.Application/Service/Document/SystemDocumentService.cs b/IRaCIS.Core.Application/Service/Document/SystemDocumentService.cs
index e279c8f91..4e70aa087 100644
--- a/IRaCIS.Core.Application/Service/Document/SystemDocumentService.cs
+++ b/IRaCIS.Core.Application/Service/Document/SystemDocumentService.cs
@@ -42,6 +42,7 @@ namespace IRaCIS.Core.Application.Services
.WhereIf(inQuery.OffLine != null, t => t.OffLine == inQuery.OffLine)
.WhereIf(inQuery.FileName != null, t => t.FileName == inQuery.FileName)
.WhereIf(inQuery.FileFormat != null, t => t.FileFormat == inQuery.FileFormat)
+
.ProjectTo(_mapper.ConfigurationProvider);
var pageList = await systemDocumentAttachmentQueryable.ToPagedListAsync(inQuery);
@@ -95,6 +96,7 @@ namespace IRaCIS.Core.Application.Services
.WhereIf(inQuery.EndCreateTime != null, t => t.CreateTime <= inQuery.EndCreateTime)
.WhereIf(inQuery.UserTypeId != null, t => t.NeedConfirmedUserTypeList.Any(t => t.NeedConfirmUserTypeId == inQuery.UserTypeId))
.WhereIf(inQuery.IsDeleted != null, t => t.IsDeleted == inQuery.IsDeleted)
+ .WhereIf(inQuery.DocLanguageType != null, t => t.DocLanguageType == inQuery.DocLanguageType)
.ProjectTo(_mapper.ConfigurationProvider, new { isEn_Us = _userInfo.IsEn_Us, userId = _userInfo.UserRoleId });
return await systemDocumentQueryable.ToPagedListAsync(inQuery);
@@ -171,23 +173,16 @@ namespace IRaCIS.Core.Application.Services
if (newUserTypeIds.Any()&& newUserTypeIds.Count()>0)
{
// 发送邮件给新增的角色
- Console.WriteLine("开始 发送系统文档更新邮件给新增角色");
- Console.WriteLine(string.Join(",", newUserTypeIds));
+ Log.Logger.Warning("开始 发送系统文档更新邮件给新增角色");
+ Log.Logger.Warning(string.Join(",", newUserTypeIds));
- Task.Run(async () =>
+ // 只发送给新增的角色
+ await _mediatorScoped.Publish(new SystemDocumentPublishEvent
{
- // 创建独立作用域
- using (var scope = serviceScopeFactory.CreateScope())
- {
- // 从新作用域解析服务
- var mediator = scope.ServiceProvider.GetRequiredService();
- // 只发送给新增的角色
- await mediator.Publish(new SystemDocumentPublishEvent {
- Ids = new List { document.Id },
- NewUserTypeIds = newUserTypeIds
- });
- }
+ Ids = new List { document.Id },
+ NewUserTypeIds = newUserTypeIds
});
+
}
}
@@ -212,18 +207,10 @@ namespace IRaCIS.Core.Application.Services
IsDeleted = false,
});
- Console.WriteLine("开始 发布系统文档");
+ Log.Logger.Warning("开始 发布系统文档");
+
+ await _mediatorScoped.Publish(new SystemDocumentPublishEvent { Ids = inDto.Ids });
- Task.Run(async () =>
- {
- // 创建独立作用域
- using (var scope = serviceScopeFactory.CreateScope())
- {
- // 从新作用域解析服务
- var mediator = scope.ServiceProvider.GetRequiredService();
- await mediator.Publish(new SystemDocumentPublishEvent { Ids = inDto.Ids });
- }
- });
return ResponseOutput.Result(true);
@@ -286,9 +273,12 @@ namespace IRaCIS.Core.Application.Services
{
var isInternal = _userInfo.IsZhiZhun;
+ var workLanguage = _userInfo.UserWorkLanguage;
var query = from sysDoc in _systemDocumentRepository.Where(t=>t.IsPublish)
- .Where(t => t.NeedConfirmedUserTypeList.Any(t => t.NeedConfirmUserTypeId == _userInfo.UserTypeId))
+ .WhereIf(workLanguage == UserWorkLanguage.CN, t => t.DocLanguageType == DocLanguageType.CN || t.DocLanguageType == DocLanguageType.CN_US)
+ .WhereIf(workLanguage == UserWorkLanguage.US, t => t.DocLanguageType == DocLanguageType.US || t.DocLanguageType == DocLanguageType.CN_US)
+ .Where(t => t.NeedConfirmedUserTypeList.Any(t => t.NeedConfirmUserTypeId == _userInfo.UserTypeId))
.WhereIf(!string.IsNullOrEmpty(inQuery.Name), t => t.Name.Contains(inQuery.Name))
//外部人员 只签署 外部需要签署的
.WhereIf(isInternal == false, t => t.DocUserSignType == DocUserSignType.InnerAndOuter)
diff --git a/IRaCIS.Core.Application/Service/Document/TrialDocumentService.cs b/IRaCIS.Core.Application/Service/Document/TrialDocumentService.cs
index f7985847d..11381f8ee 100644
--- a/IRaCIS.Core.Application/Service/Document/TrialDocumentService.cs
+++ b/IRaCIS.Core.Application/Service/Document/TrialDocumentService.cs
@@ -27,6 +27,7 @@ namespace IRaCIS.Core.Application.Services
[ApiExplorerSettings(GroupName = "Trial")]
public class TrialDocumentService(IRepository _trialDocumentRepository,
IRepository _trialRepository,
+ IScopedMediator _mediatorScoped,
IRepository _auditRecordRepository,
IRepository _trialDocumentAttachmentRepository,
ISystemDocumentService _systemDocumentService,
@@ -105,18 +106,11 @@ namespace IRaCIS.Core.Application.Services
IsDeleted = false,
}, false, true);
await _trialDocumentRepository.SaveChangesAsync();
- Console.WriteLine("开始 发布项目文档");
+ Serilog.Log.Logger.Warning("开始 发布项目文档");
- Task.Run(async () =>
- {
- // 创建独立作用域
- using (var scope = serviceScopeFactory.CreateScope())
- {
- // 从新作用域解析服务
- var mediator = scope.ServiceProvider.GetRequiredService();
- await mediator.Publish(new TrialDocumentPublishEvent { Ids = inDto.Ids });
- }
- });
+ await _mediatorScoped.Publish(new TrialDocumentPublishEvent { Ids = inDto.Ids });
+
+
return ResponseOutput.Result(true);
@@ -128,32 +122,20 @@ namespace IRaCIS.Core.Application.Services
///
public async Task TestPush()
{
- Task.Run(async () =>
- {
- // 创建独立作用域
- using (var scope = serviceScopeFactory.CreateScope())
- {
- // 从新作用域解析服务
- var mediator = scope.ServiceProvider.GetRequiredService();
- await mediator.Publish(new TrialDocumentErverDayEvent { });
- }
- });
+
+
+ await _mediatorScoped.Publish(new TrialDocumentErverDayEvent { });
+
return ResponseOutput.Result(true);
}
public async Task TestSendEmail()
{
- Task.Run(async () =>
- {
- // 创建独立作用域
- using (var scope = serviceScopeFactory.CreateScope())
- {
- // 从新作用域解析服务
- var mediator = scope.ServiceProvider.GetRequiredService();
- await mediator.Publish(new ImageQCRecurringEvent { TrialId = Guid.Parse("08de2254-5d7d-581a-0242-0a0001000000") });
- }
- });
+
+ await _mediatorScoped.Publish(new ImageQCRecurringEvent { TrialId = Guid.Parse("08de2254-5d7d-581a-0242-0a0001000000") });
+
+
return ResponseOutput.Result(true);
}
@@ -351,12 +333,16 @@ namespace IRaCIS.Core.Application.Services
var isInternal = _userInfo.IsZhiZhun;
+ var workLanguage = _userInfo.UserWorkLanguage;
+
#region 统一用户修改
var systemDocQuery =
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)
+ .WhereIf(workLanguage == UserWorkLanguage.CN, t => t.DocLanguageType == DocLanguageType.CN || t.DocLanguageType == DocLanguageType.CN_US)
+ .WhereIf(workLanguage == UserWorkLanguage.US, t => t.DocLanguageType == DocLanguageType.US || t.DocLanguageType == DocLanguageType.CN_US)
from trialUser in _trialIdentityUserRepository.AsQueryable(false)
.Where(t => t.TrialId == inQuery.TrialId && t.IdentityUserId == _userInfo.IdentityUserId
&& t.TrialUserRoleList.Any(t => sysDoc.NeedConfirmedUserTypeList.Any(c => c.NeedConfirmUserTypeId == t.UserRole.UserTypeId)))
@@ -584,6 +570,7 @@ namespace IRaCIS.Core.Application.Services
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))
@@ -591,7 +578,9 @@ namespace IRaCIS.Core.Application.Services
var needSignSystemDocCount = await _systemDocumentRepository.AsQueryable(true).Where(t => t.IsPublish)
- .WhereIf(isInternal == false, t => t.DocUserSignType == DocUserSignType.InnerAndOuter)
+ .WhereIf(workLanguage == UserWorkLanguage.CN, t => t.DocLanguageType == DocLanguageType.CN || t.DocLanguageType == DocLanguageType.CN_US)
+ .WhereIf(workLanguage == UserWorkLanguage.US, t => t.DocLanguageType == DocLanguageType.US || t.DocLanguageType == DocLanguageType.CN_US)
+ .WhereIf(isInternal == false, t => t.DocUserSignType == DocUserSignType.InnerAndOuter)
.Where(t => t.IsDeleted == false && !t.SystemDocConfirmedUserList.Any(t => t.ConfirmUserId == _userInfo.IdentityUserId && t.ConfirmTime != null) && t.NeedConfirmedUserTypeList.Any(u => u.NeedConfirmUserTypeId == _userInfo.UserTypeId))
.CountAsync();
@@ -969,7 +958,7 @@ namespace IRaCIS.Core.Application.Services
IsSystemDoc = true,
SysDocUserSignType = sysDoc.DocUserSignType,
IsConfirmIdentityUserInner = identityUser.IsZhiZhun,
- IsPublish=sysDoc.IsPublish,
+ IsPublish = sysDoc.IsPublish,
Id = sysDoc.Id,
CreateTime = sysDoc.CreateTime,
IsDeleted = sysDoc.IsDeleted,
@@ -1115,7 +1104,7 @@ namespace IRaCIS.Core.Application.Services
return ResponseOutput.NotOk(_localizer["TrialD_DuplicateFileInProject"]);
}
- var document = (await _trialDocumentRepository.Where(t => t.Id == addOrEditTrialDocument.Id, true,true).Include(t => t.NeedConfirmedUserTypeList).FirstOrDefaultAsync()).IfNullThrowException();
+ var document = (await _trialDocumentRepository.Where(t => t.Id == addOrEditTrialDocument.Id, true, true).Include(t => t.NeedConfirmedUserTypeList).FirstOrDefaultAsync()).IfNullThrowException();
bool beforeIsPublish = document.IsPublish;
bool beforeIsDeleted = document.IsDeleted;
@@ -1163,21 +1152,13 @@ namespace IRaCIS.Core.Application.Services
Console.WriteLine("开始 发送项目文档更新邮件给新增角色");
Console.WriteLine(string.Join(",", newUserTypeIds));
- Task.Run(async () =>
+ await _mediatorScoped.Publish(new TrialDocumentPublishEvent
{
- // 创建独立作用域
- using (var scope = serviceScopeFactory.CreateScope())
- {
- // 从新作用域解析服务
- var mediator = scope.ServiceProvider.GetRequiredService();
- // 只发送给新增的角色
- await mediator.Publish(new TrialDocumentPublishEvent
- {
- Ids = new List { document.Id },
- NewUserTypeIds = newUserTypeIds
- });
- }
+ Ids = new List { document.Id },
+ NewUserTypeIds = newUserTypeIds
});
+
+
}
}
return ResponseOutput.Ok(document.Id.ToString());
diff --git a/IRaCIS.Core.Application/Service/Document/TrialEmailNoticeConfigService.cs b/IRaCIS.Core.Application/Service/Document/TrialEmailNoticeConfigService.cs
index 800feaec9..fd5e34832 100644
--- a/IRaCIS.Core.Application/Service/Document/TrialEmailNoticeConfigService.cs
+++ b/IRaCIS.Core.Application/Service/Document/TrialEmailNoticeConfigService.cs
@@ -24,6 +24,7 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Spire.Doc;
using System.Linq.Dynamic.Core;
+using System.Runtime;
using System.Runtime.InteropServices;
namespace IRaCIS.Core.Application.Service
@@ -137,7 +138,7 @@ namespace IRaCIS.Core.Application.Service
var isNeedSend = true;
//手动发送的时候,也有可能答案是是 此时是 这里不发送,发送已经生成的文件
- if (answer == "是" && isHandSend == null)
+ if ((answer == "是" || answer == "Yes") && isHandSend == null)
{
isNeedSend = true;
@@ -194,7 +195,15 @@ namespace IRaCIS.Core.Application.Service
///
public async Task BaseBusinessScenarioSendEmailAsync(Guid visitTaskId, bool? isHandSend, EmailStoreSendMode emailStoreMode, string sendFileRelativePath)
{
- var isEn_us = _userInfo.IsEn_Us;
+
+ //简单处理,找到该中心的语言 设置工作语言
+
+ var country = _visitTaskRepository.Where(t => t.Id == visitTaskId).Select(t => t.Subject.TrialSite.Country).First();
+
+ var isEn_us = country == StaticData.SiteCountry.US;
+
+ var workLanguage = isEn_us ? UserWorkLanguage.US : UserWorkLanguage.CN;
+
EmailBusinessScenario businessScenarioEnum = EmailBusinessScenario.None;
var enrollReplace = "";
@@ -330,7 +339,7 @@ namespace IRaCIS.Core.Application.Service
{
var findItem = (await _dictionaryService.GetBasicDataSelect("Trial_Enroll_Report")).Where(t => t.Code == ((int)taskInfo.CriterionType).ToString()).FirstOrDefault();
- enrollReplace = _userInfo.IsEn_Us ? findItem.Value : findItem.ValueCN;
+ enrollReplace = isEn_us ? findItem.Value : findItem.ValueCN;
//如果其他阅片人已经做了,说明发送了入组确认报告,第二个人做完就不发送了
@@ -348,7 +357,7 @@ namespace IRaCIS.Core.Application.Service
{
bool isEnroll = false;
- (answer, isEnroll) = await DealEnrollAnswer(visitTaskId, (Guid)taskInfo.SourceSubjectVisitId, taskInfo.CriterionType, taskInfo.TrialReadingCriterionId);
+ (answer, isEnroll) = await DealEnrollAnswer(visitTaskId, (Guid)taskInfo.SourceSubjectVisitId, taskInfo.CriterionType, taskInfo.TrialReadingCriterionId, workLanguage);
isNeedSend = await DealMedicalReviewTasKGenerateAndIsSendAsync(taskInfo.TrialId, isHandSend, answer, new List() { visitTaskId }, minUserIdList);
}
@@ -357,7 +366,7 @@ namespace IRaCIS.Core.Application.Service
{
var findItem = (await _dictionaryService.GetBasicDataSelect("Trial_PD_Report")).Where(t => t.Code == ((int)taskInfo.CriterionType).ToString()).FirstOrDefault();
- PdReplace = _userInfo.IsEn_Us ? findItem.Value : findItem.ValueCN;
+ PdReplace = isEn_us ? findItem.Value : findItem.ValueCN;
//有序
@@ -384,7 +393,7 @@ namespace IRaCIS.Core.Application.Service
if (taskList.Count == 2 && taskList.Count(t => t.ReadingTaskState == ReadingTaskState.HaveSigned && t.ReadingCategory == ReadingCategory.Visit) == 2)
{
- answer = await TranslatePdStateAsync(visitTaskId, taskInfo.ReadingCategory, taskInfo.CriterionType);
+ answer = await TranslatePdStateAsync(visitTaskId, taskInfo.ReadingCategory, taskInfo.CriterionType, workLanguage);
isNeedSend = await DealMedicalReviewTasKGenerateAndIsSendAsync(taskInfo.TrialId, isHandSend, answer, taskList.Select(t => t.Id).ToList(), minUserIdList);
@@ -394,7 +403,7 @@ namespace IRaCIS.Core.Application.Service
else if (taskList.Count == 3 && taskList.Count(t => t.ReadingTaskState == ReadingTaskState.HaveSigned) == 3 && taskList.Where(t => t.ReadingCategory == ReadingCategory.Judge).Count() == 1)
{
var judgeResultId = taskList.Where(t => t.ReadingCategory == ReadingCategory.Judge).First()!.JudgeResultTaskId!.Value;
- answer = await TranslatePdStateAsync(judgeResultId, ReadingCategory.Visit, taskInfo.CriterionType);
+ answer = await TranslatePdStateAsync(judgeResultId, ReadingCategory.Visit, taskInfo.CriterionType, workLanguage);
isNeedSend = await DealMedicalReviewTasKGenerateAndIsSendAsync(taskInfo.TrialId, isHandSend, answer, taskList.Where(t => t.ReadingCategory == ReadingCategory.Judge).Select(t => t.Id).ToList(), minUserIdList);
@@ -431,7 +440,7 @@ namespace IRaCIS.Core.Application.Service
if (taskList.Count == 2 && taskList.Count(t => t.ReadingTaskState == ReadingTaskState.HaveSigned && t.ReadingCategory == ReadingCategory.Global) == 2)
{
- answer = await TranslatePdStateAsync(visitTaskId, taskInfo.ReadingCategory, taskInfo.CriterionType);
+ answer = await TranslatePdStateAsync(visitTaskId, taskInfo.ReadingCategory, taskInfo.CriterionType, workLanguage);
isNeedSend = await DealMedicalReviewTasKGenerateAndIsSendAsync(taskInfo.TrialId, isHandSend, answer, taskList.Select(t => t.Id).ToList(), minUserIdList);
}
@@ -440,7 +449,7 @@ namespace IRaCIS.Core.Application.Service
{
var judgeResultId = taskList.Where(t => t.ReadingCategory == ReadingCategory.Judge).First().JudgeResultTaskId!.Value;
- answer = await TranslatePdStateAsync(judgeResultId, ReadingCategory.Global, taskInfo.CriterionType);
+ answer = await TranslatePdStateAsync(judgeResultId, ReadingCategory.Global, taskInfo.CriterionType, workLanguage);
isNeedSend = await DealMedicalReviewTasKGenerateAndIsSendAsync(taskInfo.TrialId, isHandSend, answer, taskList.Where(t => t.ReadingCategory == ReadingCategory.Judge).Select(t => t.Id).ToList(), minUserIdList);
@@ -613,7 +622,7 @@ namespace IRaCIS.Core.Application.Service
//先预先生成了邮件,发送预先生成的邮件
sendEmailConfig.EmailAttachMentConfigList.Add(new EmailAttachMentConfig()
{
- FileName = $"{attachPrefix}_{Path.GetFileNameWithoutExtension(_userInfo.IsEn_Us ? trialEmailConfig.AttachName : trialEmailConfig!.AttachNameCN)}.pdf",
+ FileName = $"{attachPrefix}_{Path.GetFileNameWithoutExtension(isEn_us ? trialEmailConfig.AttachName : trialEmailConfig!.AttachNameCN)}.pdf",
FileStream = File.OpenRead(phyPath),
});
@@ -639,7 +648,7 @@ namespace IRaCIS.Core.Application.Service
};
- var path = FileStoreHelper.GetPhysicalFilePath(_hostEnvironment, _userInfo.IsEn_Us ? trialEmailConfig.AttachPath : trialEmailConfig.AttachCNPath);
+ var path = FileStoreHelper.GetPhysicalFilePath(_hostEnvironment, isEn_us ? trialEmailConfig.AttachPath : trialEmailConfig.AttachCNPath);
//获取从word 到 PDF的路径
var (serverFilePath, relativePath, fileRealName) = FileStoreHelper.GetSubjectEnrollConfirmOrPDEmailPath(_hostEnvironment, Path.GetFileName(path), taskInfo.TrialId, taskInfo.TrialSiteId, taskInfo.SubjectId, true);
@@ -729,7 +738,7 @@ namespace IRaCIS.Core.Application.Service
pdfMemoryStream.Seek(0, SeekOrigin.Begin);
sendEmailConfig.EmailAttachMentConfigList.Add(new EmailAttachMentConfig()
{
- FileName = $"{taskInfo.SubjectCode}_{Path.GetFileNameWithoutExtension(_userInfo.IsEn_Us ? trialEmailConfig.AttachName : trialEmailConfig.AttachNameCN)}.pdf",
+ FileName = $"{taskInfo.SubjectCode}_{Path.GetFileNameWithoutExtension(isEn_us ? trialEmailConfig.AttachName : trialEmailConfig.AttachNameCN)}.pdf",
FileStream = pdfMemoryStream
});
@@ -1067,11 +1076,14 @@ namespace IRaCIS.Core.Application.Service
///
///
///
+ ///
///
- private async Task<(string enrollAnswer, bool isEnroll)> DealEnrollAnswer(Guid visitTaskId, Guid subjectVisitId, CriterionType criterionType, Guid trialReadingCriterionId)
+ private async Task<(string enrollAnswer, bool isEnroll)> DealEnrollAnswer(Guid visitTaskId, Guid subjectVisitId, CriterionType criterionType, Guid trialReadingCriterionId, UserWorkLanguage userWorkLanguage)
{
- var enrollAnswer = _userInfo.IsEn_Us ? "No" : "否";
+ var isEn_us = userWorkLanguage == UserWorkLanguage.US;
+
+ var enrollAnswer = isEn_us ? "No" : "否";
var isEnroll = false;
switch (criterionType)
@@ -1088,7 +1100,7 @@ namespace IRaCIS.Core.Application.Service
if (await _readingTableQuestionAnswerRepository.Where().AnyAsync(x => x.VisitTaskId == visitTaskId && x.Answer == TargetState.Exist.GetEnumInt() &&
x.ReadingTableQuestionTrial.QuestionMark == QuestionMark.State && x.ReadingQuestionTrial.LesionType == LesionType.TargetLesion))
{
- enrollAnswer = _userInfo.IsEn_Us ? "Yes" : "是";
+ enrollAnswer = isEn_us ? "Yes" : "是";
isEnroll = true;
}
@@ -1102,7 +1114,7 @@ x.ReadingTableQuestionTrial.QuestionMark == QuestionMark.LesionNumber && x.Readi
bool exists = list.Any(s => int.TryParse(s, out var n) && n >= 1);
if (exists)
{
- enrollAnswer = _userInfo.IsEn_Us ? "Yes" : "是";
+ enrollAnswer = isEn_us ? "Yes" : "是";
isEnroll = true;
}
@@ -1168,9 +1180,10 @@ x.ReadingTableQuestionTrial.QuestionMark == QuestionMark.LesionNumber && x.Readi
/// 任务类型
/// 标准类型
/// 是否是全局产生(区分裁判任务)
+ ///
///
///
- private async Task TranslatePdStateAsync(Guid visitTaskId, ReadingCategory readingCategory, CriterionType criterionType, bool? IsGlobalGenerate = null)
+ private async Task TranslatePdStateAsync(Guid visitTaskId, ReadingCategory readingCategory, CriterionType criterionType, UserWorkLanguage userWorkLanguage, bool? IsGlobalGenerate = null)
{
var answer = string.Empty;
@@ -1361,6 +1374,7 @@ x.ReadingTableQuestionTrial.QuestionMark == QuestionMark.LesionNumber && x.Readi
throw new BusinessValidationFailedException(_localizer["TrialEmailN_InvalidTaskTypeForEmailSending"]);
}
+ var isEn_us = userWorkLanguage == UserWorkLanguage.US;
switch (criterionType)
{
@@ -1372,11 +1386,11 @@ x.ReadingTableQuestionTrial.QuestionMark == QuestionMark.LesionNumber && x.Readi
if (answer == OverallAssessment.PD.GetEnumInt())
{
- answer = _userInfo.IsEn_Us ? "Yes" : "是";
+ answer = isEn_us ? "Yes" : "是";
}
else
{
- answer = _userInfo.IsEn_Us ? "No" : "否";
+ answer = isEn_us ? "No" : "否";
}
break;
@@ -1384,11 +1398,11 @@ x.ReadingTableQuestionTrial.QuestionMark == QuestionMark.LesionNumber && x.Readi
if (answer == OverallAssessment.iCPD.GetEnumInt())
{
- answer = _userInfo.IsEn_Us ? "Yes" : "是";
+ answer = isEn_us ? "Yes" : "是";
}
else
{
- answer = _userInfo.IsEn_Us ? "No" : "否";
+ answer = isEn_us ? "No" : "否";
}
break;
@@ -1396,11 +1410,11 @@ x.ReadingTableQuestionTrial.QuestionMark == QuestionMark.LesionNumber && x.Readi
if (answer == CTMRIOverallAssessment.PD.GetEnumInt())
{
- answer = _userInfo.IsEn_Us ? "Yes" : "是";
+ answer = isEn_us ? "Yes" : "是";
}
else
{
- answer = _userInfo.IsEn_Us ? "No" : "否";
+ answer = isEn_us ? "No" : "否";
}
break;
@@ -1408,11 +1422,11 @@ x.ReadingTableQuestionTrial.QuestionMark == QuestionMark.LesionNumber && x.Readi
if (answer == ImagingOverallAssessment_Lugano.PMDPD.GetEnumInt())
{
- answer = _userInfo.IsEn_Us ? "Yes" : "是";
+ answer = isEn_us ? "Yes" : "是";
}
else
{
- answer = _userInfo.IsEn_Us ? "No" : "否";
+ answer = isEn_us ? "No" : "否";
}
break;
@@ -1423,11 +1437,11 @@ x.ReadingTableQuestionTrial.QuestionMark == QuestionMark.LesionNumber && x.Readi
if (answer == VisitTumorEvaluation.PD.GetEnumInt())
{
- answer = _userInfo.IsEn_Us ? "Yes" : "是";
+ answer = isEn_us ? "Yes" : "是";
}
else
{
- answer = _userInfo.IsEn_Us ? "No" : "否";
+ answer = isEn_us ? "No" : "否";
}
break;
diff --git a/IRaCIS.Core.Application/Service/Financial/DTO/PaymentDetailModel.cs b/IRaCIS.Core.Application/Service/Financial/DTO/PaymentDetailModel.cs
index ce7d66944..cd53c580f 100644
--- a/IRaCIS.Core.Application/Service/Financial/DTO/PaymentDetailModel.cs
+++ b/IRaCIS.Core.Application/Service/Financial/DTO/PaymentDetailModel.cs
@@ -105,7 +105,7 @@
public string Reviewer { get; set; } = String.Empty;
public DateTime BeginMonth { get; set; }
public DateTime EndMonth { get; set; }
- public int? Nation { get; set; }
+ public DoctorNation? Nation { get; set; }
}
public class MonthlyPaymentDTO
{
diff --git a/IRaCIS.Core.Application/Service/Financial/DTO/PaymentModel.cs b/IRaCIS.Core.Application/Service/Financial/DTO/PaymentModel.cs
index 15c25799e..ce4d7aa5f 100644
--- a/IRaCIS.Core.Application/Service/Financial/DTO/PaymentModel.cs
+++ b/IRaCIS.Core.Application/Service/Financial/DTO/PaymentModel.cs
@@ -59,7 +59,7 @@
{
public DateTime StatisticsDate { get; set; }
public string KeyWord { get; set; } = String.Empty;
- public int? Nation { get; set; }
+ public DoctorNation? Nation { get; set; }
}
public class MonthlyPaymentDetailQuery
@@ -162,7 +162,7 @@
public DateTime BeginDate { get; set; }
public DateTime EndDate { get; set; }
- public int? Nation { get; set; }
+ public DoctorNation? Nation { get; set; }
}
public class TrialAnalysisQueryDTO
diff --git a/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/DicomInstanceModel.cs b/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/DicomInstanceModel.cs
index a95065a2d..033e402f3 100644
--- a/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/DicomInstanceModel.cs
+++ b/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/DicomInstanceModel.cs
@@ -29,6 +29,9 @@
public string SliceThickness { get; set; } = String.Empty;
+ public string ImagePositionPatient { get; set; }
+ public string ImageOrientationPatient { get; set; }
+
}
diff --git a/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/DicomSeriesModel.cs b/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/DicomSeriesModel.cs
index 5ec4c58db..73ccfb6cc 100644
--- a/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/DicomSeriesModel.cs
+++ b/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/DicomSeriesModel.cs
@@ -13,6 +13,8 @@ namespace IRaCIS.Core.Application.Contracts.Dicom.DTO
public bool IsBeMark { get; set; } = false;
+ public bool IsBeSegment { get; set; } = false;
+
public Guid Id { get; set; }
public Guid StudyId { get; set; }
public string StudyInstanceUid { get; set; } = String.Empty;
@@ -100,6 +102,10 @@ namespace IRaCIS.Core.Application.Contracts.Dicom.DTO
public Guid? SeriesId { get; set; }
+ public bool? IsMasked { get; set; }
+
+ public string? PhotometricInterpretation { get; set; }
+
[JsonIgnore]
public int ShowOrder { get; set; }
[JsonIgnore]
@@ -108,6 +114,10 @@ namespace IRaCIS.Core.Application.Contracts.Dicom.DTO
public string WindowCenter { get; set; }
[JsonIgnore]
public string WindowWidth { get; set; }
+ [JsonIgnore]
+ public string ImagePositionPatient { get; set; }
+ [JsonIgnore]
+ public string ImageOrientationPatient { get; set; }
public DateTime? RowDate { get; set; }
}
diff --git a/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/DicomStudyModel.cs b/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/DicomStudyModel.cs
index eea51dc7d..6974f1743 100644
--- a/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/DicomStudyModel.cs
+++ b/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/DicomStudyModel.cs
@@ -47,6 +47,50 @@
public DateTime UpdateTime { get; set; }
public Guid CreateUserId { get; set; }
public DateTime CreateTime { get; set; }
+
+ #region 核验
+
+ ///
+ /// 体重
+ ///
+ public string PatientWeight { get; set; }
+
+ ///
+ /// 总剂量
+ ///
+ public string RadionuclideTotalDose { get; set; } = null!;
+
+ ///
+ /// 半衰期
+ ///
+ public string RadionuclideHalfLife { get; set; } = null!;
+
+ ///
+ /// 注射时间
+ ///
+ public string RadiopharmaceuticalStartTime { get; set; } = null!;
+
+ ///
+ /// 成像 / 采集时间
+ ///
+ public string AcquisitionTime { get; set; } = null!;
+
+
+ #endregion
+
+ ///
+ /// 是否存在空字符串字段(PatientSex、PatientWeight、RadionuclideTotalDose、RadionuclideHalfLife、RadiopharmaceuticalStartTime、AcquisitionTime 任意一个为空/空字符串)
+ ///
+ public bool IsHasEmptyPatientInfo =>
+ string.IsNullOrWhiteSpace(PatientSex) ||
+ string.IsNullOrWhiteSpace(PatientWeight) ||
+ string.IsNullOrWhiteSpace(RadionuclideTotalDose) ||
+ string.IsNullOrWhiteSpace(RadionuclideHalfLife) ||
+ string.IsNullOrWhiteSpace(RadiopharmaceuticalStartTime) ||
+ string.IsNullOrWhiteSpace(AcquisitionTime);
+
+
+
}
public class RelationVisitDTO
diff --git a/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/UnionStudyViewDodel.cs b/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/UnionStudyViewDodel.cs
index c56dfb668..9edf88c4b 100644
--- a/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/UnionStudyViewDodel.cs
+++ b/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/UnionStudyViewDodel.cs
@@ -1,4 +1,5 @@
using FellowOakDicom;
+using IRaCIS.Core.Application.Helper;
using IRaCIS.Core.Application.Service.ImageAndDoc.DTO;
using IRaCIS.Core.Domain.Share;
using Newtonsoft.Json;
@@ -193,6 +194,86 @@ namespace IRaCIS.Core.Application.Contracts
}
+ public class EditPatientInfoCommand
+ {
+ public Guid Id { get; set; }
+ ///
+ /// 性别
+ ///
+ public string PatientSex { get; set; } = null!;
+
+ ///
+ /// 体重
+ ///
+ public string PatientWeight { get; set; }
+
+ ///
+ /// 总剂量
+ ///
+ public string RadionuclideTotalDose { get; set; } = null!;
+
+ ///
+ /// 半衰期
+ ///
+ public string RadionuclideHalfLife { get; set; } = null!;
+
+ ///
+ /// 注射时间
+ ///
+ public string RadiopharmaceuticalStartTime { get; set; } = null!;
+
+ ///
+ /// 成像 / 采集时间
+ ///
+ public string AcquisitionTime { get; set; } = null!;
+
+ ///
+ /// 修改原因
+ ///
+ public string Reason { get; set; } = null!;
+ }
+
+ public class GetPatientInfoInDto
+ {
+ public Guid StudyId { get; set; }
+ }
+
+ public class PatientInfoDto
+ {
+
+ public Guid Id { get; set; }
+
+ ///
+ /// 性别
+ ///
+ public string PatientSex { get; set; } = null!;
+
+ ///
+ /// 体重
+ ///
+ public string PatientWeight { get; set; }
+
+ ///
+ /// 总剂量
+ ///
+ public string RadionuclideTotalDose { get; set; } = null!;
+
+ ///
+ /// 半衰期
+ ///
+ public string RadionuclideHalfLife { get; set; } = null!;
+
+ ///
+ /// 注射时间
+ ///
+ public string RadiopharmaceuticalStartTime { get; set; } = null!;
+
+ ///
+ /// 成像 / 采集时间
+ ///
+ public string AcquisitionTime { get; set; } = null!;
+ }
+
public class PreArchiveDicomStudyCommand
{
@@ -265,6 +346,11 @@ namespace IRaCIS.Core.Application.Contracts
[NotDefault]
public Guid StudyMonitorId { get; set; }
+ [NotDefault]
+ public string UploadBatchId { get; set; }
+
+
+
public int FailedFileCount { get; set; }
public string RecordPath { get; set; } = string.Empty;
@@ -289,6 +375,10 @@ namespace IRaCIS.Core.Application.Contracts
[NotDefault]
public Guid StudyMonitorId { get; set; }
+ [NotDefault]
+ public string UploadBatchId { get; set; }
+
+
public int FailedFileCount { get; set; }
public string RecordPath { get; set; } = string.Empty;
@@ -352,6 +442,24 @@ namespace IRaCIS.Core.Application.Contracts
#endregion
+ #region 核验
+
+ ///
+ /// 总剂量
+ ///
+ public string RadionuclideTotalDose { get; set; }
+
+ ///
+ /// 半衰期
+ ///
+ public string RadionuclideHalfLife { get; set; }
+
+ ///
+ /// 注射时间
+ ///
+ public string RadiopharmaceuticalStartTime { get; set; }
+ #endregion
+
}
public class AddOrUpdateSeriesDto
@@ -633,6 +741,7 @@ namespace IRaCIS.Core.Application.Contracts
public class DownloadDicomInstanceDto
{
+ public int NumberOfFrames { get; set; }
public bool IsEncapsulated { get; set; }
public Guid InstanceId { get; set; }
public string FileName { get; set; }
@@ -954,7 +1063,7 @@ namespace IRaCIS.Core.Application.Contracts
}
- public class SubjectVisitMarkQuery:PageInput
+ public class SubjectVisitMarkQuery : PageInput
{
public Guid TrialId { get; set; }
@@ -1038,4 +1147,31 @@ namespace IRaCIS.Core.Application.Contracts
public List UploadStudyList { get; set; }
}
+
+ public class StudyMaskImageCommand
+ {
+ public Guid? SeriesId { get; set; }
+
+ public List? InstanceIdList { get; set; }
+
+ public List MaskRegionList { get; set; }
+
+
+ }
+
+ public class StudyUndoMaskImageCommand
+ {
+ public Guid? SeriesId { get; set; }
+
+ public List? InstanceIdList { get; set; }
+ }
+
+ public class InstanceIdPath
+ {
+ public Guid Id { get; set; }
+
+ public string Path { get; set; }
+ }
+
+
}
diff --git a/IRaCIS.Core.Application/Service/ImageAndDoc/DownloadAndUploadService.cs b/IRaCIS.Core.Application/Service/ImageAndDoc/DownloadAndUploadService.cs
index 8125a7723..db29531ad 100644
--- a/IRaCIS.Core.Application/Service/ImageAndDoc/DownloadAndUploadService.cs
+++ b/IRaCIS.Core.Application/Service/ImageAndDoc/DownloadAndUploadService.cs
@@ -41,6 +41,7 @@ namespace IRaCIS.Core.Application.Service.ImageAndDoc
IRepository _visitTaskRepository,
IRepository _subjectVisitRepository,
IOSSService _oSSService,
+ IRepository _fileUploadRecordRepository,
IRepository _dictionaryRepository,
IRepository _trialRepository,
IRepository