diff --git a/IRC.Core.SCP/HostConfig/SyncFileRecoveryService.cs b/IRC.Core.SCP/HostConfig/SyncFileRecoveryService.cs
index 894d36696..089465d10 100644
--- a/IRC.Core.SCP/HostConfig/SyncFileRecoveryService.cs
+++ b/IRC.Core.SCP/HostConfig/SyncFileRecoveryService.cs
@@ -7,6 +7,7 @@ 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.Threading;
@@ -20,40 +21,50 @@ public class SyncFileRecoveryService(IServiceScopeFactory _scopeFactory, FileSyn
private readonly int _pageSize = 500;
+ ///
+ /// 多个程序,如果恢复同一份数据,造成重复同步,SCP服务不恢复任务
+ ///
+ ///
+ ///
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
- using var scope = _scopeFactory.CreateScope();
- var fileUploadRecordRepository = scope.ServiceProvider.GetRequiredService>();
- // 延迟启动,保证主机快速启动
- await Task.Delay(5000, stoppingToken);
+ await Task.CompletedTask;
- int page = 0;
+ //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);
+ //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; // 扫描完毕,退出循环
+ // 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); // 缓解数据库压力
+ //}
- foreach (var file in pending)
- {
- //file.IsQueued = true; // 避免重复入队
- _fileSyncQueue.Enqueue(file.Id, file.Priority ?? 0); // 放入队列
- }
- page++; // 下一页
- await Task.Delay(200, stoppingToken); // 缓解数据库压力
- }
}
}
@@ -85,12 +96,29 @@ public class FileSyncWorker(IServiceScopeFactory _scopeFactory, ILogger>();
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);
- if (file == null || file.IsNeedSync != true)
+ // ✅ 不要 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
{
@@ -107,7 +135,7 @@ public class FileSyncWorker(IServiceScopeFactory _scopeFactory, ILogger _fileUploadRe
-
+
public async Task AddOrUpdateFileUploadRecord(FileUploadRecordAddOrEdit addOrEditFileUploadRecord)
{
@@ -98,6 +99,18 @@ public class FileUploadRecordService(IRepository _fileUploadRe
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)
@@ -122,7 +135,9 @@ public class FileUploadRecordService(IRepository _fileUploadRe
}
else
{
- addOrEditFileUploadRecord.TargetRegion = "";
+ addOrEditFileUploadRecord.IsNeedSync = false;
+
+ //addOrEditFileUploadRecord.TargetRegion = "";
}
@@ -154,40 +169,149 @@ public class FileUploadRecordService(IRepository _fileUploadRe
-///
-/// 同步队列 信号量
-///
-public class FileSyncQueue
+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)
{
- // priority 越大越优先
+ // 如果正在执行,忽略(防止重复)
+ if (_running.Contains(id))
+ return;
+
+ // 是否新任务(用于减少 signal 风暴)
+ if (!_waiting.ContainsKey(id))
+ needSignal = true;
+
+ // 更新为最新优先级(最后一次为准)
+ _waiting[id] = priority; //等价于添加或者更新
+
+ // PriorityQueue 无法更新节点
+ // 允许旧节点存在,Dequeue 时过滤
_queue.Enqueue(id, -priority);
}
- //类似于计数器,不会产生通知风暴,可消费资源 +1
- //if (有等待线程) 唤醒一个 else 仅增加计数
- _signal.Release(); // 唤醒一个 worker
+ // 只有新增任务才唤醒 worker
+ if (needSignal)
+ _signal.Release();
}
+ // ============================================================
+ // Dequeue
+ // ============================================================
+
///
- /// 如果没有任务 → 挂起等待 有任务 → 被唤醒并返回
+ /// 获取一个待执行任务(无任务时自动等待)
///
- ///
- ///
public async Task DequeueAsync(CancellationToken ct)
{
- await _signal.WaitAsync(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)
{
- return _queue.Dequeue();
+ _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/IRaCIS.Core.API/HostService/SyncFileRecoveryService.cs b/IRaCIS.Core.API/HostService/SyncFileRecoveryService.cs
index da3a7be84..ad82da60d 100644
--- a/IRaCIS.Core.API/HostService/SyncFileRecoveryService.cs
+++ b/IRaCIS.Core.API/HostService/SyncFileRecoveryService.cs
@@ -20,6 +20,11 @@ public class SyncFileRecoveryService(IServiceScopeFactory _scopeFactory, FileSyn
private readonly int _pageSize = 500;
+ ///
+ /// 多个程序,如果恢复同一份数据,造成重复同步,SCP服务不恢复任务
+ ///
+ ///
+ ///
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var scope = _scopeFactory.CreateScope();
@@ -73,7 +78,6 @@ public class FileSyncWorker(IServiceScopeFactory _scopeFactory, ILogger t.Id == id);
+ // ✅ 不要 return
if (file == null || file.IsNeedSync != true || syncConfig.IsOpenStoreSync == false)
- return;
+ {
+ 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))
@@ -116,7 +128,7 @@ public class FileSyncWorker(IServiceScopeFactory _scopeFactory, ILogger options,
GetObjectStoreTempToken(objectUse: config.Primary);
- await DeleteFromPrefixInternal(prefix, isCache);
+ await DeleteFromPrefixInternal(config.Primary,prefix, isCache);
}
}
else
{
GetObjectStoreTempToken();
- await DeleteFromPrefixInternal(prefix, isCache);
+ await DeleteFromPrefixInternal(ObjectStoreServiceOptions.ObjectStoreUse,prefix, isCache);
}
}
- private async Task DeleteFromPrefixInternal(string prefix, bool isCache = false)
+ private async Task DeleteFromPrefixInternal(string objectUse ,string prefix, bool isCache = false)
{
- if (ObjectStoreServiceOptions.ObjectStoreUse == "AliyunOSS")
+ if (objectUse == "AliyunOSS")
{
var aliConfig = ObjectStoreServiceOptions.AliyunOSS;
@@ -1577,7 +1578,7 @@ public class OSSService(IOptionsMonitor options,
}
- else if (ObjectStoreServiceOptions.ObjectStoreUse == "MinIO")
+ else if (objectUse == "MinIO")
{
var minIOConfig = ObjectStoreServiceOptions.MinIO;
@@ -1614,7 +1615,7 @@ public class OSSService(IOptionsMonitor options,
}
- else if (ObjectStoreServiceOptions.ObjectStoreUse == "AWS")
+ else if (objectUse == "AWS")
{
var awsConfig = ObjectStoreServiceOptions.AWS;
diff --git a/IRaCIS.Core.Application/IRaCIS.Core.Application.xml b/IRaCIS.Core.Application/IRaCIS.Core.Application.xml
index b46aba366..177c3aba5 100644
--- a/IRaCIS.Core.Application/IRaCIS.Core.Application.xml
+++ b/IRaCIS.Core.Application/IRaCIS.Core.Application.xml
@@ -1556,31 +1556,75 @@
+
+
+ 按照 subject visit studyCode 三个维度进行分组的查询列表 (subject相关)
+
+
+
+
- 上传记录表--里面包含待同步任务
+ 上传记录表--里面包含待同步任务 DataFileType= 0 :代表系统文件 1:Subject相关 2:项目相关,但是和subject 没关系
- 任务具体执行记录表 (需要同步的才可以点击)
+ 任务具体执行记录表
-
+
- 同步队列 信号量
+ 批量设置为需要同步,并且设置优先级
+
+
+
+
+
+
+ 优先级队列(仅负责排序)
+
+
+
+
+ 当前等待中的任务(唯一真实数据)
+ key = Guid
+ value = 最新 priority
+
+
+
+
+ 正在执行的任务(防止重复执行)
+
+
+
+
+ worker 等待信号
+
+
+
+
+ 入队(同 Guid 会覆盖优先级)
- 如果没有任务 → 挂起等待 有任务 → 被唤醒并返回
+ 获取一个待执行任务(无任务时自动等待)
+
+
+
+
+ 任务执行完成(必须调用)
+
+
+
+
+ 当前等待中的任务快照
-
-
@@ -17086,17 +17130,17 @@
- ����
+ 质疑
- һ���Ժ˲�
+ 一致性核查
- ����
+ 复制
diff --git a/IRaCIS.Core.Application/Service/Common/DTO/FileUploadRecordViewModel.cs b/IRaCIS.Core.Application/Service/Common/DTO/FileUploadRecordViewModel.cs
index 34853c3db..d0c43d664 100644
--- a/IRaCIS.Core.Application/Service/Common/DTO/FileUploadRecordViewModel.cs
+++ b/IRaCIS.Core.Application/Service/Common/DTO/FileUploadRecordViewModel.cs
@@ -9,6 +9,31 @@ 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
{
@@ -16,12 +41,16 @@ public class FileUploadRecordView : FileUploadRecordAddOrEdit
public DateTime UpdateTime { get; set; }
- public bool? IsSync { get; set; }
[Comment("同步结束时间-最后一个任务的时间")]
public DateTime? SyncFinishedTime { get; set; }
+
+ public string? SubjectCode { get; set; }
+
+ public string? VisitName { get; set; }
+
}
@@ -42,6 +71,7 @@ public class FileUploadRecordAddOrEdit
public string UploadBatchId { get; set; }
public BatchDataType BatchDataType { get; set; }
+ public string StudyCode { get; set; }
public Guid? TrialId { get; set; }
@@ -62,15 +92,49 @@ public class FileUploadRecordAddOrEdit
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 bool? IsSystermFile { 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; }
@@ -112,6 +176,7 @@ public class FileUploadRecordQuery : PageInput
public class UploadFileSyncRecordView
{
+ public Guid Id { get; set; }
public DateTime? StartTime { get; set; }
@@ -122,13 +187,48 @@ public class UploadFileSyncRecordView
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 class UploadFileSyncRecordQuery : PageInput
{
public Guid? FileUploadRecordId { get; set; }
public jobState? JobState { get; set; }
-}
\ No newline at end of file
+
+ 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/FileUploadRecordService.cs b/IRaCIS.Core.Application/Service/Common/FileUploadRecordService.cs
index c70d5d1a2..3c367627c 100644
--- a/IRaCIS.Core.Application/Service/Common/FileUploadRecordService.cs
+++ b/IRaCIS.Core.Application/Service/Common/FileUploadRecordService.cs
@@ -29,8 +29,57 @@ public class FileUploadRecordService(IRepository _fileUploadRe
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 没关系
///
///
///
@@ -39,16 +88,32 @@ public class FileUploadRecordService(IRepository _fileUploadRe
{
var fileUploadRecordQueryable = _fileUploadRecordRepository
- .WhereIf(inQuery.BatchDataType != null, t => t.BatchDataType == inQuery.BatchDataType)
.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.IsSystermFile == true, t => t.TrialId == null)
+
+ .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.Contains(inQuery.UploadRegion))
- .WhereIf(!string.IsNullOrEmpty(inQuery.TargetRegion), t => t.TargetRegion.Contains(inQuery.TargetRegion))
+ .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)
@@ -64,7 +129,7 @@ public class FileUploadRecordService(IRepository _fileUploadRe
}
///
- /// 任务具体执行记录表 (需要同步的才可以点击)
+ /// 任务具体执行记录表
///
///
///
@@ -73,9 +138,11 @@ public class FileUploadRecordService(IRepository _fileUploadRe
{
var fileUploadRecordQueryable = _uploadFileSyncRecordRepository
-
- .WhereIf(inQuery.FileUploadRecordId != null, t => t.FileUploadRecordId >= inQuery.FileUploadRecordId)
-
+ .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);
@@ -85,6 +152,29 @@ public class FileUploadRecordService(IRepository _fileUploadRe
}
+ ///
+ /// 批量设置为需要同步,并且设置优先级
+ ///
+ ///
+ ///
+ 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();
+ }
+
@@ -134,11 +224,13 @@ public class FileUploadRecordService(IRepository _fileUploadRe
addOrEditFileUploadRecord.Priority = 0;
-
+ addOrEditFileUploadRecord.IsSync = false;
}
else
{
- addOrEditFileUploadRecord.TargetRegion = "";
+ addOrEditFileUploadRecord.IsNeedSync = false;
+
+ //addOrEditFileUploadRecord.TargetRegion = "";
}
@@ -148,6 +240,8 @@ public class FileUploadRecordService(IRepository _fileUploadRe
//系统文件,默认同步
addOrEditFileUploadRecord.IsNeedSync = true;
+ addOrEditFileUploadRecord.IsSync = false;
+
addOrEditFileUploadRecord.Priority = 0;
}
@@ -176,45 +270,206 @@ public class FileUploadRecordService(IRepository _fileUploadRe
#region 同步队列
-
-///
-/// 同步队列 信号量
-///
-public class FileSyncQueue
+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)
{
- // priority 越大越优先
+ // 如果正在执行,忽略(防止重复)
+ if (_running.Contains(id))
+ return;
+
+ // 是否新任务(用于减少 signal 风暴)
+ if (!_waiting.ContainsKey(id))
+ needSignal = true;
+
+ // 更新为最新优先级(最后一次为准)
+ _waiting[id] = priority; //等价于添加或者更新
+
+ // PriorityQueue 无法更新节点
+ // 允许旧节点存在,Dequeue 时过滤
_queue.Enqueue(id, -priority);
}
- //类似于计数器,不会产生通知风暴,可消费资源 +1
- //if (有等待线程) 唤醒一个 else 仅增加计数
- _signal.Release(); // 唤醒一个 worker
+ // 只有新增任务才唤醒 worker
+ if (needSignal)
+ _signal.Release();
}
+ // ============================================================
+ // Dequeue
+ // ============================================================
+
///
- /// 如果没有任务 → 挂起等待 有任务 → 被唤醒并返回
+ /// 获取一个待执行任务(无任务时自动等待)
///
- ///
- ///
public async Task DequeueAsync(CancellationToken ct)
{
- await _signal.WaitAsync(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)
{
- return _queue.Dequeue();
+ _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
{
diff --git a/IRaCIS.Core.Application/Service/Common/_MapConfig.cs b/IRaCIS.Core.Application/Service/Common/_MapConfig.cs
index e27ac47e0..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();
@@ -125,12 +125,20 @@ 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();
-
+ 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/ImageAndDoc/DTO/UnionStudyViewDodel.cs b/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/UnionStudyViewDodel.cs
index c56dfb668..6622be5fa 100644
--- a/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/UnionStudyViewDodel.cs
+++ b/IRaCIS.Core.Application/Service/ImageAndDoc/DTO/UnionStudyViewDodel.cs
@@ -265,6 +265,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 +294,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;
diff --git a/IRaCIS.Core.Application/Service/ImageAndDoc/DownloadAndUploadService.cs b/IRaCIS.Core.Application/Service/ImageAndDoc/DownloadAndUploadService.cs
index 2e16b6dd4..f7267956f 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 _studyMonitorRepository,
@@ -803,6 +804,8 @@ namespace IRaCIS.Core.Application.Service.ImageAndDoc
{
await _taskStudyRepository.SaveChangesAsync();
}
+
+ await _fileUploadRecordRepository.BatchUpdateNoTrackingAsync(t => t.UploadBatchId == incommand.UploadBatchId, u => new FileUploadRecord() { StudyCode = findStudy.StudyCode });
}
catch (Exception ex)
{
diff --git a/IRaCIS.Core.Application/Service/ImageAndDoc/StudyService.cs b/IRaCIS.Core.Application/Service/ImageAndDoc/StudyService.cs
index 07fd93a50..110d5e0b1 100644
--- a/IRaCIS.Core.Application/Service/ImageAndDoc/StudyService.cs
+++ b/IRaCIS.Core.Application/Service/ImageAndDoc/StudyService.cs
@@ -20,6 +20,7 @@ namespace IRaCIS.Core.Application.Service.ImageAndDoc
IRepository _trialRepository,
IRepository _visitTaskRepository,
IRepository _scpStudyRepository,
+ IRepository _fileUploadRecordRepository,
IRepository _subjectRepository,
IRepository _studyMonitorRepository,
IRepository _systemAnonymizationRepository,
@@ -332,6 +333,8 @@ namespace IRaCIS.Core.Application.Service.ImageAndDoc
{
await _dicomInstanceRepository.SaveChangesAsync();
}
+
+ await _fileUploadRecordRepository.BatchUpdateNoTrackingAsync(t => t.UploadBatchId == incommand.UploadBatchId, u => new FileUploadRecord() { StudyCode = findStudy.StudyCode });
}
catch (Exception ex)
{
diff --git a/IRaCIS.Core.Application/Service/SiteSurvey/TrialSiteSurveyService.cs b/IRaCIS.Core.Application/Service/SiteSurvey/TrialSiteSurveyService.cs
index 097891d95..b6900a32f 100644
--- a/IRaCIS.Core.Application/Service/SiteSurvey/TrialSiteSurveyService.cs
+++ b/IRaCIS.Core.Application/Service/SiteSurvey/TrialSiteSurveyService.cs
@@ -1191,7 +1191,7 @@ namespace IRaCIS.Core.Application.Contracts
var userEmail = item.Key;
- var userTypeIdList = item.Select(t => t.UserTypeId).ToList();
+ var userTypeIdList = item.Select(t => t.UserTypeId).Distinct().ToList();
var existSysUser = await _identityUserRepository.Where(t => t.EMail == userEmail, true).Include(t => t.UserRoleList).FirstOrDefaultAsync();
diff --git a/IRaCIS.Core.Domain/Common/FileUploadRecord.cs b/IRaCIS.Core.Domain/Common/FileUploadRecord.cs
index 44181ae53..69bd04b34 100644
--- a/IRaCIS.Core.Domain/Common/FileUploadRecord.cs
+++ b/IRaCIS.Core.Domain/Common/FileUploadRecord.cs
@@ -27,6 +27,8 @@ public class FileUploadRecord : BaseFullAuditEntity
public Guid? NoneDicomStudyId { get; set; }
+ public string StudyCode { get; set; }
+
[Comment("文件标识ID")]
public string FileMarkId { get; set; }
diff --git a/IRaCIS.Core.Infra.EFCore/Migrations/20260402090118_addStudyCode.Designer.cs b/IRaCIS.Core.Infra.EFCore/Migrations/20260402090118_addStudyCode.Designer.cs
new file mode 100644
index 000000000..bae59ce20
--- /dev/null
+++ b/IRaCIS.Core.Infra.EFCore/Migrations/20260402090118_addStudyCode.Designer.cs
@@ -0,0 +1,22019 @@
+//
+using System;
+using IRaCIS.Core.Infra.EFCore;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace IRaCIS.Core.Infra.EFCore.Migrations
+{
+ [DbContext(typeof(IRaCISDBContext))]
+ [Migration("20260402090118_addStudyCode")]
+ partial class addStudyCode
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.19")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("IRaCIS.Core.Domain.Models.Attachment", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(400)
+ .HasColumnType("nvarchar(400)")
+ .HasComment("编码");
+
+ b.Property("CreateTime")
+ .HasColumnType("datetime2");
+
+ b.Property("CreateUserId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("DoctorId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("ExpiryDate")
+ .HasColumnType("datetime2")
+ .HasComment("过期时间");
+
+ b.Property("FileName")
+ .IsRequired()
+ .HasMaxLength(400)
+ .HasColumnType("nvarchar(400)");
+
+ b.Property("IsAuthorizedView")
+ .HasColumnType("bit");
+
+ b.Property("IsOfficial")
+ .HasColumnType("bit")
+ .HasComment("是否正式简历");
+
+ b.Property("Language")
+ .HasColumnType("int")
+ .HasComment("1 中文 2为英文");
+
+ b.Property("Path")
+ .IsRequired()
+ .HasMaxLength(1000)
+ .HasColumnType("nvarchar(1000)");
+
+ b.Property("TrialId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasMaxLength(400)
+ .HasColumnType("nvarchar(400)")
+ .HasComment("文件类型名");
+
+ b.Property("UpdateTime")
+ .HasColumnType("datetime2");
+
+ b.Property("UpdateUserId")
+ .HasColumnType("uniqueidentifier");
+
+ b.HasKey("Id");
+
+ SqlServerKeyBuilderExtensions.IsClustered(b.HasKey("Id"), false);
+
+ b.HasIndex("CreateTime");
+
+ SqlServerIndexBuilderExtensions.IsClustered(b.HasIndex("CreateTime"));
+
+ b.ToTable("Attachment", t =>
+ {
+ t.HasComment("医生 - 简历|证书 文档表");
+ });
+ });
+
+ modelBuilder.Entity("IRaCIS.Core.Domain.Models.AuditDocument", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("AuditDocumentTypeEnum")
+ .HasColumnType("int");
+
+ b.Property("CreateTime")
+ .HasColumnType("datetime2");
+
+ b.Property("CreateUserId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("FileFormat")
+ .IsRequired()
+ .HasMaxLength(400)
+ .HasColumnType("nvarchar(400)");
+
+ b.Property("FilePath")
+ .IsRequired()
+ .HasMaxLength(1000)
+ .HasColumnType("nvarchar(1000)");
+
+ b.Property("FileSize")
+ .HasPrecision(18, 2)
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("IsAuthorization")
+ .HasColumnType("bit");
+
+ b.Property("MainFileId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(400)
+ .HasColumnType("nvarchar(400)");
+
+ b.Property("ParentId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("UpdateTime")
+ .HasColumnType("datetime2");
+
+ b.Property("UpdateUserId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Version")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ SqlServerKeyBuilderExtensions.IsClustered(b.HasKey("Id"), false);
+
+ b.HasIndex("CreateTime");
+
+ SqlServerIndexBuilderExtensions.IsClustered(b.HasIndex("CreateTime"));
+
+ b.ToTable("AuditDocument", t =>
+ {
+ t.HasComment("稽查文档管理");
+ });
+ });
+
+ modelBuilder.Entity("IRaCIS.Core.Domain.Models.AuditDocumentClosure", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("AncestorId")
+ .HasColumnType("uniqueidentifier")
+ .HasComment("祖先");
+
+ b.Property("Depth")
+ .HasColumnType("int");
+
+ b.Property("DescendantId")
+ .HasColumnType("uniqueidentifier")
+ .HasComment("后代");
+
+ b.HasKey("Id");
+
+ SqlServerKeyBuilderExtensions.IsClustered(b.HasKey("Id"), false);
+
+ b.ToTable("AuditDocumentClosure");
+ });
+
+ modelBuilder.Entity("IRaCIS.Core.Domain.Models.AuditRecord", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("AuditContent")
+ .IsRequired()
+ .HasMaxLength(400)
+ .HasColumnType("nvarchar(400)");
+
+ b.Property("AuditState")
+ .HasColumnType("int")
+ .HasComment("稽查状态");
+
+ b.Property("AuditTime")
+ .HasColumnType("date")
+ .HasComment("稽查日期");
+
+ b.Property("AuditType")
+ .HasColumnType("int")
+ .HasComment("稽查形式");
+
+ b.Property("BeginTime")
+ .HasColumnType("datetime2");
+
+ b.Property("CompanyName")
+ .IsRequired()
+ .HasMaxLength(400)
+ .HasColumnType("nvarchar(400)");
+
+ b.Property("CreateTime")
+ .HasColumnType("datetime2");
+
+ b.Property("CreateUserId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("EndTime")
+ .HasColumnType("datetime2");
+
+ b.Property("IsViewTrainingRecord")
+ .HasColumnType("bit");
+
+ b.Property("UpdateTime")
+ .HasColumnType("datetime2");
+
+ b.Property("UpdateUserId")
+ .HasColumnType("uniqueidentifier");
+
+ b.HasKey("Id");
+
+ SqlServerKeyBuilderExtensions.IsClustered(b.HasKey("Id"), false);
+
+ b.HasIndex("CreateTime");
+
+ SqlServerIndexBuilderExtensions.IsClustered(b.HasIndex("CreateTime"));
+
+ b.ToTable("AuditRecord");
+ });
+
+ modelBuilder.Entity("IRaCIS.Core.Domain.Models.AuditRecordIdentityUser", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("AuditRecordId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("CreateTime")
+ .HasColumnType("datetime2");
+
+ b.Property("CreateUserId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("IdentityUserId")
+ .HasColumnType("uniqueidentifier");
+
+ b.HasKey("Id");
+
+ SqlServerKeyBuilderExtensions.IsClustered(b.HasKey("Id"), false);
+
+ b.HasIndex("CreateTime");
+
+ SqlServerIndexBuilderExtensions.IsClustered(b.HasIndex("CreateTime"));
+
+ b.ToTable("AuditRecordIdentityUser");
+ });
+
+ modelBuilder.Entity("IRaCIS.Core.Domain.Models.AuditRecordPermission", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("AuditDocumentId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("AuditRecordId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("CreateTime")
+ .HasColumnType("datetime2");
+
+ b.Property("CreateUserId")
+ .HasColumnType("uniqueidentifier");
+
+ b.HasKey("Id");
+
+ SqlServerKeyBuilderExtensions.IsClustered(b.HasKey("Id"), false);
+
+ b.HasIndex("CreateTime");
+
+ SqlServerIndexBuilderExtensions.IsClustered(b.HasIndex("CreateTime"));
+
+ b.ToTable("AuditRecordPermission");
+ });
+
+ modelBuilder.Entity("IRaCIS.Core.Domain.Models.CRO", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("CROCode")
+ .IsRequired()
+ .HasMaxLength(400)
+ .HasColumnType("nvarchar(400)");
+
+ b.Property("CROName")
+ .IsRequired()
+ .HasMaxLength(400)
+ .HasColumnType("nvarchar(400)");
+
+ b.Property("CRONameCN")
+ .IsRequired()
+ .HasMaxLength(400)
+ .HasColumnType("nvarchar(400)");
+
+ b.Property("CreateTime")
+ .HasColumnType("datetime2");
+
+ b.Property("CreateUserId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("IsTrialLevel")
+ .HasColumnType("bit")
+ .HasComment("是否是项目级别");
+
+ b.Property("TrialId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("UpdateTime")
+ .HasColumnType("datetime2");
+
+ b.Property("UpdateUserId")
+ .HasColumnType("uniqueidentifier");
+
+ b.HasKey("Id");
+
+ SqlServerKeyBuilderExtensions.IsClustered(b.HasKey("Id"), false);
+
+ b.HasIndex("CreateTime");
+
+ SqlServerIndexBuilderExtensions.IsClustered(b.HasIndex("CreateTime"));
+
+ b.ToTable("CROCompany", t =>
+ {
+ t.HasComment("机构 - CRO");
+ });
+ });
+
+ modelBuilder.Entity("IRaCIS.Core.Domain.Models.CheckChallengeDialog", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("CreateTime")
+ .HasColumnType("datetime2");
+
+ b.Property("CreateUserId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("IsCRCNeedReply")
+ .HasColumnType("bit")
+ .HasComment("CRC是否需要回复 前端使用");
+
+ b.Property("ParamInfo")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)")
+ .HasComment("核查的检查信息Json");
+
+ b.Property("SubjectVisitId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("TalkContent")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("UserTypeEnum")
+ .HasColumnType("int")
+ .HasComment("核查过程中的操作用户类型");
+
+ b.HasKey("Id");
+
+ SqlServerKeyBuilderExtensions.IsClustered(b.HasKey("Id"), false);
+
+ b.HasIndex("CreateTime");
+
+ SqlServerIndexBuilderExtensions.IsClustered(b.HasIndex("CreateTime"));
+
+ b.ToTable("CheckChallengeDialog", t =>
+ {
+ t.HasComment("一致性核查 - 对话记录表");
+ });
+ });
+
+ modelBuilder.Entity("IRaCIS.Core.Domain.Models.ClinicalAnswerRowInfo", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("ClinicalFormId")
+ .HasColumnType("uniqueidentifier")
+ .HasComment("表单Id");
+
+ b.Property("CreateTime")
+ .HasColumnType("datetime2");
+
+ b.Property("CreateUserId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("QuestionId")
+ .HasColumnType("uniqueidentifier")
+ .HasComment("问题Id");
+
+ b.Property("RowIndex")
+ .HasColumnType("int");
+
+ b.Property("SubjectId")
+ .HasColumnType("uniqueidentifier")
+ .HasComment("受试者Id");
+
+ b.HasKey("Id");
+
+ SqlServerKeyBuilderExtensions.IsClustered(b.HasKey("Id"), false);
+
+ b.HasIndex("CreateTime");
+
+ SqlServerIndexBuilderExtensions.IsClustered(b.HasIndex("CreateTime"));
+
+ b.ToTable("ClinicalAnswerRowInfo", t =>
+ {
+ t.HasComment("受试者 - 临床表单表格问题行记录");
+ });
+ });
+
+ modelBuilder.Entity("IRaCIS.Core.Domain.Models.ClinicalDataSystemSet", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("ClinicalDataLevel")
+ .HasColumnType("int");
+
+ b.Property("ClinicalDataSetEnName")
+ .IsRequired()
+ .HasMaxLength(400)
+ .HasColumnType("nvarchar(400)");
+
+ b.Property("ClinicalDataSetEnum")
+ .HasColumnType("int")
+ .HasComment("枚举(字典里面取的)");
+
+ b.Property("ClinicalDataSetName")
+ .IsRequired()
+ .HasMaxLength(400)
+ .HasColumnType("nvarchar(400)");
+
+ b.Property("ClinicalUploadType")
+ .HasColumnType("int")
+ .HasComment("上传方式");
+
+ b.Property("CreateTime")
+ .HasColumnType("datetime2");
+
+ b.Property("CreateUserId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("CriterionEnumListStr")
+ .IsRequired()
+ .HasMaxLength(1000)
+ .HasColumnType("nvarchar(1000)");
+
+ b.Property("EnFileName")
+ .IsRequired()
+ .HasMaxLength(400)
+ .HasColumnType("nvarchar(400)");
+
+ b.Property("EnPath")
+ .IsRequired()
+ .HasMaxLength(400)
+ .HasColumnType("nvarchar(400)");
+
+ b.Property("FileName")
+ .IsRequired()
+ .HasMaxLength(400)
+ .HasColumnType("nvarchar(400)");
+
+ b.Property("IsApply")
+ .HasColumnType("bit")
+ .HasComment("是否应用");
+
+ b.Property("IsEnable")
+ .HasColumnType("bit");
+
+ b.Property("Path")
+ .IsRequired()
+ .HasMaxLength(1000)
+ .HasColumnType("nvarchar(1000)");
+
+ b.Property("UploadRole")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ SqlServerKeyBuilderExtensions.IsClustered(b.HasKey("Id"), false);
+
+ b.HasIndex("CreateTime");
+
+ SqlServerIndexBuilderExtensions.IsClustered(b.HasIndex("CreateTime"));
+
+ b.ToTable("ClinicalDataSystemSet", t =>
+ {
+ t.HasComment("系统 - 临床数据配置");
+ });
+ });
+
+ modelBuilder.Entity("IRaCIS.Core.Domain.Models.ClinicalDataTrialSet", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("ClinicalDataLevel")
+ .HasColumnType("int")
+ .HasComment("临床级别");
+
+ b.Property("ClinicalDataSetEnName")
+ .IsRequired()
+ .HasMaxLength(400)
+ .HasColumnType("nvarchar(400)");
+
+ b.Property("ClinicalDataSetName")
+ .IsRequired()
+ .HasMaxLength(400)
+ .HasColumnType("nvarchar(400)");
+
+ b.Property("ClinicalUploadType")
+ .HasColumnType("int")
+ .HasComment("上传方式");
+
+ b.Property("CreateTime")
+ .HasColumnType("datetime2");
+
+ b.Property("CreateUserId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("CriterionEnumListStr")
+ .IsRequired()
+ .HasMaxLength(1000)
+ .HasColumnType("nvarchar(1000)");
+
+ b.Property("EnFileName")
+ .IsRequired()
+ .HasMaxLength(400)
+ .HasColumnType("nvarchar(400)");
+
+ b.Property("EnPath")
+ .IsRequired()
+ .HasMaxLength(400)
+ .HasColumnType("nvarchar(400)");
+
+ b.Property("FileName")
+ .IsRequired()
+ .HasMaxLength(400)
+ .HasColumnType("nvarchar(400)");
+
+ b.Property("IsApply")
+ .HasColumnType("bit")
+ .HasComment("是否应用");
+
+ b.Property("IsConfirm")
+ .HasColumnType("bit");
+
+ b.Property("Path")
+ .IsRequired()
+ .HasMaxLength(1000)
+ .HasColumnType("nvarchar(1000)");
+
+ b.Property("SystemClinicalDataSetId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("TrialId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("UploadRole")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ SqlServerKeyBuilderExtensions.IsClustered(b.HasKey("Id"), false);
+
+ b.HasIndex("CreateTime");
+
+ SqlServerIndexBuilderExtensions.IsClustered(b.HasIndex("CreateTime"));
+
+ b.ToTable("ClinicalDataTrialSet", t =>
+ {
+ t.HasComment("项目 - 临床数据适应标准配置");
+ });
+ });
+
+ modelBuilder.Entity("IRaCIS.Core.Domain.Models.ClinicalForm", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("CheckDate")
+ .HasColumnType("datetime2")
+ .HasComment("检查日期");
+
+ b.Property("ClinicalDataTrialSetId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("CreateTime")
+ .HasColumnType("datetime2");
+
+ b.Property("CreateUserId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("PicturePath")
+ .IsRequired()
+ .HasMaxLength(1000)
+ .HasColumnType("nvarchar(1000)")
+ .HasComment("截图地址");
+
+ b.Property("ReadingId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("SubjectId")
+ .HasColumnType("uniqueidentifier")
+ .HasComment("受试者Id");
+
+ b.Property("TrialId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("VisitId")
+ .HasColumnType("uniqueidentifier");
+
+ b.HasKey("Id");
+
+ SqlServerKeyBuilderExtensions.IsClustered(b.HasKey("Id"), false);
+
+ b.HasIndex("CreateTime");
+
+ SqlServerIndexBuilderExtensions.IsClustered(b.HasIndex("CreateTime"));
+
+ b.ToTable("ClinicalForm", t =>
+ {
+ t.HasComment("受试者 - 临床表单");
+ });
+ });
+
+ modelBuilder.Entity("IRaCIS.Core.Domain.Models.ClinicalQuestionAnswer", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Answer")
+ .IsRequired()
+ .HasMaxLength(1000)
+ .HasColumnType("nvarchar(1000)");
+
+ b.Property("ClinicalDataTrialSetId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("ClinicalFormId")
+ .HasColumnType("uniqueidentifier")
+ .HasComment("表单Id");
+
+ b.Property("CreateTime")
+ .HasColumnType("datetime2");
+
+ b.Property("CreateUserId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("QuestionId")
+ .HasColumnType("uniqueidentifier")
+ .HasComment("问题Id");
+
+ b.Property("SubjectId")
+ .HasColumnType("uniqueidentifier")
+ .HasComment("受试者Id");
+
+ b.HasKey("Id");
+
+ SqlServerKeyBuilderExtensions.IsClustered(b.HasKey("Id"), false);
+
+ b.HasIndex("CreateTime");
+
+ SqlServerIndexBuilderExtensions.IsClustered(b.HasIndex("CreateTime"));
+
+ b.ToTable("ClinicalQuestionAnswer", t =>
+ {
+ t.HasComment("受试者 - 临床表单问题答案");
+ });
+ });
+
+ modelBuilder.Entity("IRaCIS.Core.Domain.Models.ClinicalTableAnswer", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Answer")
+ .IsRequired()
+ .HasMaxLength(1000)
+ .HasColumnType("nvarchar(1000)")
+ .HasComment("答案");
+
+ b.Property("ClinicalFormId")
+ .HasColumnType("uniqueidentifier")
+ .HasComment("表单Id");
+
+ b.Property("CreateTime")
+ .HasColumnType("datetime2");
+
+ b.Property("CreateUserId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("QuestionId")
+ .HasColumnType("uniqueidentifier")
+ .HasComment("问题Id");
+
+ b.Property("RowId")
+ .HasColumnType("uniqueidentifier")
+ .HasComment("答案行的Id");
+
+ b.Property("SubjectId")
+ .HasColumnType("uniqueidentifier")
+ .HasComment("受试者Id");
+
+ b.Property("TableQuestionId")
+ .HasColumnType("uniqueidentifier");
+
+ b.HasKey("Id");
+
+ SqlServerKeyBuilderExtensions.IsClustered(b.HasKey("Id"), false);
+
+ b.HasIndex("CreateTime");
+
+ SqlServerIndexBuilderExtensions.IsClustered(b.HasIndex("CreateTime"));
+
+ b.ToTable("ClinicalTableAnswer", t =>
+ {
+ t.HasComment("受试者 - 临床表单表格问题答案");
+ });
+ });
+
+ modelBuilder.Entity("IRaCIS.Core.Domain.Models.CommonDocument", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("BusinessScenarioEnum")
+ .HasColumnType("int")
+ .HasComment("业务场景");
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(400)
+ .HasColumnType("nvarchar(400)");
+
+ b.Property("CreateTime")
+ .HasColumnType("datetime2");
+
+ b.Property("CreateUserId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("CriterionTypeEnum")
+ .HasColumnType("int")
+ .HasComment("系统标准枚举");
+
+ b.Property("DeleteUserId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("DeletedTime")
+ .HasColumnType("datetime2");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasMaxLength(400)
+ .HasColumnType("nvarchar(400)");
+
+ b.Property("FileTypeEnum")
+ .HasColumnType("int")
+ .HasComment("类型-上传|导出|邮件附件");
+
+ b.Property("IsDeleted")
+ .HasColumnType("bit");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(400)
+ .HasColumnType("nvarchar(400)");
+
+ b.Property("NameCN")
+ .IsRequired()
+ .HasMaxLength(400)
+ .HasColumnType("nvarchar(400)");
+
+ b.Property("Path")
+ .IsRequired()
+ .HasMaxLength(400)
+ .HasColumnType("nvarchar(400)");
+
+ b.Property("UpdateTime")
+ .HasColumnType("datetime2");
+
+ b.Property("UpdateUserId")
+ .HasColumnType("uniqueidentifier");
+
+ b.HasKey("Id");
+
+ SqlServerKeyBuilderExtensions.IsClustered(b.HasKey("Id"), false);
+
+ b.HasIndex("CreateTime");
+
+ SqlServerIndexBuilderExtensions.IsClustered(b.HasIndex("CreateTime"));
+
+ b.ToTable("CommonDocument", t =>
+ {
+ t.HasComment("数据上传 | 数据导出 | 邮件附件 文件记录表 (需要同步)");
+ });
+ });
+
+ modelBuilder.Entity("IRaCIS.Core.Domain.Models.CriterionKeyFileRead", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("CreateTime")
+ .HasColumnType("datetime2");
+
+ b.Property("CreateUserId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("IdentityUserId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("TrialCriterionId")
+ .HasColumnType("uniqueidentifier");
+
+ b.HasKey("Id");
+
+ SqlServerKeyBuilderExtensions.IsClustered(b.HasKey("Id"), false);
+
+ b.HasIndex("CreateTime");
+
+ SqlServerIndexBuilderExtensions.IsClustered(b.HasIndex("CreateTime"));
+
+ b.ToTable("CriterionKeyFileRead", t =>
+ {
+ t.HasComment("标准阅读关键点");
+ });
+ });
+
+ modelBuilder.Entity("IRaCIS.Core.Domain.Models.CriterionNidusSystem", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("CreateTime")
+ .HasColumnType("datetime2");
+
+ b.Property("CreateUserId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("CriterionId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("IsSystemCriterion")
+ .HasColumnType("bit");
+
+ b.Property("LesionType")
+ .HasColumnType("int")
+ .HasComment("病灶类型");
+
+ b.Property("OrganType")
+ .HasColumnType("int")
+ .HasComment("器官类型");
+
+ b.HasKey("Id");
+
+ SqlServerKeyBuilderExtensions.IsClustered(b.HasKey("Id"), false);
+
+ b.HasIndex("CreateTime");
+
+ SqlServerIndexBuilderExtensions.IsClustered(b.HasIndex("CreateTime"));
+
+ b.ToTable("CriterionNidusSystem", t =>
+ {
+ t.HasComment("系统标准 - 病灶器官表 (需要同步)");
+ });
+ });
+
+ modelBuilder.Entity("IRaCIS.Core.Domain.Models.CriterionNidusTrial", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("CreateTime")
+ .HasColumnType("datetime2");
+
+ b.Property("CreateUserId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("CriterionId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("LesionType")
+ .HasColumnType("int");
+
+ b.Property("OrganType")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ SqlServerKeyBuilderExtensions.IsClustered(b.HasKey("Id"), false);
+
+ b.HasIndex("CreateTime");
+
+ SqlServerIndexBuilderExtensions.IsClustered(b.HasIndex("CreateTime"));
+
+ b.ToTable("CriterionNidusTrial", t =>
+ {
+ t.HasComment("项目标准 - 病灶器官表");
+ });
+ });
+
+ modelBuilder.Entity("IRaCIS.Core.Domain.Models.DataInspection", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("BatchId")
+ .HasColumnType("uniqueidentifier")
+ .HasComment("批次Id");
+
+ b.Property("ChildrenTypeId")
+ .HasColumnType("uniqueidentifier")
+ .HasComment("子类");
+
+ b.Property("CreateTime")
+ .HasColumnType("datetime2");
+
+ b.Property("CreateUserId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("CreateUserName")
+ .IsRequired()
+ .HasMaxLength(400)
+ .HasColumnType("nvarchar(400)")
+ .HasComment("创建人姓名");
+
+ b.Property