diff --git a/IRaCIS.Core.Application/Helper/OSSService.cs b/IRaCIS.Core.Application/Helper/OSSService.cs index f7b36cdd8..42b804947 100644 --- a/IRaCIS.Core.Application/Helper/OSSService.cs +++ b/IRaCIS.Core.Application/Helper/OSSService.cs @@ -16,6 +16,7 @@ using Minio; using Minio.DataModel; using Minio.DataModel.Args; using Minio.Exceptions; +using System.IO; using System.Reactive.Linq; using System.Runtime.InteropServices; using System.Web; @@ -142,6 +143,11 @@ public enum ObjectStoreUse public interface IOSSService { + public void SetImmediateArchiveRule(string prefix, + StorageClass targetStorageClass, + string ruleId = "immediate-archive"); + + 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); @@ -182,7 +188,185 @@ public class OSSService : IOSSService } + /// + /// 将指定前缀下的所有现有文件立即转为目标存储类型 + /// 核心:Days = 0 表示对所有存量文件立即生效 + /// + /// 要转换的文件前缀,如 "project-a/logs/" + /// 目标存储类型 + /// 规则ID,默认为"immediate-archive" + public void SetImmediateArchiveRule(string prefix, + StorageClass targetStorageClass, + string ruleId = "immediate-archive") + { + BackBatchGetToken(); + + 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 + { + + // 关键设置:Days = 0 表示对最后修改时间≤当前的所有文件生效 + LifeCycleExpiration = + { + Days = 1 + }, + StorageClass = targetStorageClass + } + } + }; + + + + + // 3. 移除同名的旧规则(如果存在) + existingRules.RemoveAll(r => r.ID == ruleId); + + // 4. 添加新规则到规则列表 + existingRules.Add(immediateRule); + + + var request = new SetBucketLifecycleRequest(aliConfig.BucketName) + { + LifecycleRules= existingRules + }; + + + _ossClient.SetBucketLifecycle(request); + + Console.WriteLine("✅ 立即归档规则设置成功!"); + Console.WriteLine($" 规则ID: {ruleId}"); + Console.WriteLine($" 前缀: {prefix}"); + Console.WriteLine($" 目标存储类型: {targetStorageClass}"); + Console.WriteLine($" 生效时间: 将在下次生命周期扫描时生效(通常24小时内)"); + } + catch (OssException ex) + { + Console.WriteLine($"❌ 设置失败 [错误码: {ex.ErrorCode}]"); + Console.WriteLine($" 详细: {ex.Message}"); + + // 处理特定错误 + if (ex.ErrorCode == "InvalidArgument") + { + Console.WriteLine(" 可能原因:存储类型不支持或参数格式错误"); + } + } + catch (Exception ex) + { + Console.WriteLine($"❌ 发生未知错误: {ex.Message}"); + } + } + + + //public async Task SetLifecycle(string lifecycle) + //{ + + + // 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 rule = new Aliyun.OSS.LifecycleRule + // { + // ID = "ArchiveOldFiles", + // Prefix = "", // 全 bucket 生效 + // Status = RuleStatus.Enabled + // }; + + // // 30 天转低频 + // rule.Transitions.Add(new Aliyun.OSS.LifecycleRule.LifeCycleTransition + // { + // Days = 30, + // StorageClass = StorageClass.IA + // }); + + // // 180 天转归档 + // rule.Transitions.Add(new Aliyun.OSS.LifecycleRule.LifeCycleTransition + // { + // Days = 180, + // StorageClass = StorageClass.Archive + // }); + + // // 365 天转冷归档 + // rule.Transitions.Add(new Aliyun.OSS.LifecycleRule.LifeCycleTransition + // { + // Days = 365, + // StorageClass = StorageClass.ColdArchive + // }); + + // // 730 天转深度归档 + // rule.Transitions.Add(new Aliyun.OSS.LifecycleRule.LifeCycleTransition + // { + // Days = 730, + // StorageClass = StorageClass.DeepColdArchive + // }); + + // var request = new SetBucketLifecycleRequest(aliConfig.BucketName); + // request.AddLifecycleRule(rule); + + // _ossClient.SetBucketLifecycle(request); + + + // } + // else if (ObjectStoreServiceOptions.ObjectStoreUse == "AWS") + // { + // var awsConfig = ObjectStoreServiceOptions.AWS; + + // var credentials = new SessionAWSCredentials(AWSTempToken.AccessKeyId, AWSTempToken.SecretAccessKey, AWSTempToken.SessionToken); + + + + // //提供awsEndPoint(域名)进行访问配置 + // var clientConfig = new AmazonS3Config + // { + // RegionEndpoint = RegionEndpoint.USEast1, + // UseHttp = true, + // }; + + // var amazonS3Client = new AmazonS3Client(credentials, clientConfig); + + + // } + // else + // { + // throw new BusinessValidationFailedException("未定义的存储介质类型"); + // } + //} /// /// oosFolderPath 不要 "/ "开头 应该: TempFolder/ChildFolder diff --git a/IRaCIS.Core.Application/IRaCIS.Core.Application.xml b/IRaCIS.Core.Application/IRaCIS.Core.Application.xml index 85135d99d..b128a3fe2 100644 --- a/IRaCIS.Core.Application/IRaCIS.Core.Application.xml +++ b/IRaCIS.Core.Application/IRaCIS.Core.Application.xml @@ -15538,6 +15538,13 @@ + + + 测试疗效评估 + + + + 重建闭包表 @@ -15705,6 +15712,15 @@ 利用DocX 库 处理word国际化模板 + + + 将指定前缀下的所有现有文件立即转为目标存储类型 + 核心:Days = 0 表示对所有存量文件立即生效 + + 要转换的文件前缀,如 "project-a/logs/" + 目标存储类型 + 规则ID,默认为"immediate-archive" + oosFolderPath 不要 "/ "开头 应该: TempFolder/ChildFolder diff --git a/IRaCIS.Core.Application/Service/Common/MailService.cs b/IRaCIS.Core.Application/Service/Common/MailService.cs index cc3caed14..86d56f9cd 100644 --- a/IRaCIS.Core.Application/Service/Common/MailService.cs +++ b/IRaCIS.Core.Application/Service/Common/MailService.cs @@ -53,7 +53,7 @@ namespace IRaCIS.Core.Application.Service public interface IMailVerificationService { - Task AnolymousSendEmail(string researchProgramNo, string emailAddress, int verificationCode); + Task AnolymousSendEmail(Guid trialId, string emailAddress, int verificationCode); Task SendEmailVerification(string emailAddress, int verificationCode); @@ -276,10 +276,13 @@ namespace IRaCIS.Core.Application.Service //中心调研 登陆 发送验证码 - public async Task AnolymousSendEmail(string researchProgramNo, string emailAddress, int verificationCode) + public async Task AnolymousSendEmail(Guid trialId, string emailAddress, int verificationCode) { //throw new BusinessValidationFailedException("模拟邮件取数据或者发送异常!!!"); + + var trialInfo = await _trialRepository.FirstOrDefaultAsync(t => t.Id == trialId); + var messageToSend = new MimeMessage(); //发件地址 messageToSend.From.Add(new MailboxAddress(_systemEmailConfig.FromName, _systemEmailConfig.FromEmail)); @@ -292,7 +295,7 @@ namespace IRaCIS.Core.Application.Service Func<(string topicStr, string htmlBodyStr), (string topicStr, string htmlBodyStr)> emailConfigFunc = input => { - var topicStr = string.Format(input.topicStr, companyName, researchProgramNo); + var topicStr = string.Format(input.topicStr, companyName, trialInfo.ResearchProgramNo); var htmlBodyStr = string.Format(ReplaceCompanyName(input.htmlBodyStr), diff --git a/IRaCIS.Core.Application/Service/SiteSurvey/TrialSiteSurveyService.cs b/IRaCIS.Core.Application/Service/SiteSurvey/TrialSiteSurveyService.cs index 26932c113..88a0e24aa 100644 --- a/IRaCIS.Core.Application/Service/SiteSurvey/TrialSiteSurveyService.cs +++ b/IRaCIS.Core.Application/Service/SiteSurvey/TrialSiteSurveyService.cs @@ -287,9 +287,8 @@ namespace IRaCIS.Core.Application.Contracts //验证码 6位 int verificationCode = new Random().Next(100000, 1000000); - var trialInfo = await _trialRepository.FirstOrDefaultAsync(t => t.Id == userInfo.TrialId); - await SafeMailHelper.Run(async () => await _mailVerificationService.AnolymousSendEmail(trialInfo.ResearchProgramNo, userInfo.Email, verificationCode)); + await SafeMailHelper.Run(async () => await _mailVerificationService.AnolymousSendEmail(userInfo.TrialId, userInfo.Email, verificationCode)); diff --git a/IRaCIS.Core.Application/Service/TrialSiteUser/TrialStatService.cs b/IRaCIS.Core.Application/Service/TrialSiteUser/TrialStatService.cs index 3eb13abe8..53ca77c94 100644 --- a/IRaCIS.Core.Application/Service/TrialSiteUser/TrialStatService.cs +++ b/IRaCIS.Core.Application/Service/TrialSiteUser/TrialStatService.cs @@ -197,13 +197,14 @@ public class TrialStatService( var exceptVisit = list.Where(t => t.ReadingCategory == ReadingCategory.Visit) .GroupBy(t => new { t.SubjectCode, t.TaskName }).Where(g => g.Count() == 1).Select(g => new { g.Key.SubjectCode, g.Key.TaskName }).ToList(); + //将一个人做的访视任务过滤掉 list = list.Where(t => !exceptVisit.Any(ev => ev.SubjectCode == t.SubjectCode && ev.TaskName == t.TaskName)).ToList(); } list = list.OrderBy(t => t.SubjectCode).ThenBy(t => t.ArmEnum).ThenBy(t => t.VisitTaskNum).ToList(); - //处理裁判标记 + //处理裁判标记 是否触发裁判,裁判选择标记 list = DealJudgeMark(criterion.ArbitrationRule, criterion.IsGlobalReading, list); //基线(最晚拍片日期)-首次PD(所有触发PD的病灶的检查的最早拍片日期)的中位数,单位是天 @@ -215,7 +216,7 @@ public class TrialStatService( g => g.First().LatestScanDate // 如果同一个 SubjectCode 有多条记录,只取第一条 ); - //一定要两个人做完了,产生了裁判并且有结果的,否则就不算,并且排除基线 + //找到裁判选择了的,否则就不算,并且排除基线 按照每个subject分组,取最后一次访视 list = list.Where(t => t.IsJudgeSelect == true && t.VisitTaskNum != 0 && t.ReadingCategory==ReadingCategory.Visit)//全局的答案已经放在对应访视上了 .GroupBy(t => t.SubjectCode).Select(g => g.OrderByDescending(t => t.VisitTaskNum).First()).ToList(); diff --git a/IRaCIS.Core.Application/TestService.cs b/IRaCIS.Core.Application/TestService.cs index edb262600..cbcdfde06 100644 --- a/IRaCIS.Core.Application/TestService.cs +++ b/IRaCIS.Core.Application/TestService.cs @@ -75,6 +75,77 @@ namespace IRaCIS.Core.Application.Service { public static int IntValue = 100; + [AllowAnonymous] + public async Task TestOSS() + { + _IOSSService.SetImmediateArchiveRule("Test-Archive", StorageClass.Archive); + + return ResponseOutput.Ok(); + } + + + public class TestEfficacyEvaluation + { + public string SubjectCode { get; set; } + + public string TaskName { get; set; } + + public decimal VisitTaskNum { get; set; } + + public string Arm { get; set; } + + public bool? IsJudgeSelect { get; set; } + + public bool? IsTrigerJudge { get; set; } + + public string OverallTumorEvaluation { get; set; } + + } + + + + public async Task TestUploadTrialEfficacyEvaluation(IFormFile file) + { + using var stream = file.OpenReadStream(); + + var list = MiniExcel + .Query(stream, excelType: ExcelType.XLSX) + .ToList(); + + return await TestTrialEfficacyEvaluationStat(list); + } + + /// + /// 测试疗效评估 + /// + /// + /// + public async Task TestTrialEfficacyEvaluationStat(List list) + { + + //找到只有一个人阅片的受试者 和访视 + var exceptVisit = list + .GroupBy(t => new { t.SubjectCode, t.TaskName }).Where(g => g.Count() == 1).Select(g => new { g.Key.SubjectCode, g.Key.TaskName }).ToList(); + + //将一个人做的访视任务过滤掉 + list = list.Where(t => !exceptVisit.Any(ev => ev.SubjectCode == t.SubjectCode && ev.TaskName == t.TaskName)).ToList(); + + + //找到裁判选择了的,否则就不算,并且排除基线 按照每个subject分组,取最后一次访视 + list = list.Where(t => t.IsJudgeSelect == true && t.VisitTaskNum != 0)//全局的答案已经放在对应访视上了 + .GroupBy(t => t.SubjectCode).Select(g => g.OrderByDescending(t => t.VisitTaskNum).First()).ToList(); + + + var result = list.GroupBy(t => t.OverallTumorEvaluation).Select(t => new + { + OverallTumorEvaluation = t.Key, + SubjectList = t.Select(t => t.SubjectCode).ToList() + }).ToList(); + + return ResponseOutput.Ok(result); + } + + [AllowAnonymous] public async Task DealIVUSOCTDicomTag(string subjectCode, bool? isUploadOss) { @@ -102,7 +173,7 @@ namespace IRaCIS.Core.Application.Service { Console.WriteLine(item.Path); - await using var stream = await _IOSSService.GetStreamFromOSSAsync(item.Path); + await using var stream = await _IOSSService.GetStreamFromOSSAsync(item.Path); var dicomFile = DicomFile.Open(stream, FileReadOption.ReadLargeOnDemand);