维护数据初步提交
parent
4b011ee727
commit
7154196667
|
@ -1087,7 +1087,7 @@
|
|||
<param name="_trialRepository"></param>
|
||||
<param name="_oSSService"></param>
|
||||
</member>
|
||||
<member name="M:IRaCIS.Core.Application.Service.TrialImageDownloadService.#ctor(IRaCIS.Core.Infra.EFCore.IRepository{IRaCIS.Core.Domain.Models.Trial},IRaCIS.Core.Application.Helper.IOSSService,Microsoft.AspNetCore.Hosting.IWebHostEnvironment)">
|
||||
<member name="M:IRaCIS.Core.Application.Service.TrialImageDownloadService.#ctor(IRaCIS.Core.Infra.EFCore.IRepository{IRaCIS.Core.Domain.Models.Trial},IRaCIS.Core.Application.Helper.IOSSService,Microsoft.AspNetCore.Hosting.IWebHostEnvironment,IRaCIS.Core.Infra.EFCore.IRepository{IRaCIS.Core.Domain.Models.DicomStudy},IRaCIS.Core.Infra.EFCore.IRepository{IRaCIS.Core.Domain.Models.DicomSeries},IRaCIS.Core.Infra.EFCore.IRepository{IRaCIS.Core.Domain.Models.DicomInstance})">
|
||||
<summary>
|
||||
项目影像后台下载,不打包
|
||||
</summary>
|
||||
|
@ -1124,6 +1124,25 @@
|
|||
<param name="trialId"></param>
|
||||
<returns></returns>
|
||||
</member>
|
||||
<member name="M:IRaCIS.Core.Application.Service.TrialImageDownloadService.ReadExcelReStorreOSSDeleteDataAndDBData(IRaCIS.Core.Application.Helper.IOSSService,Microsoft.AspNetCore.Hosting.IWebHostEnvironment)">
|
||||
<summary>
|
||||
读取excel 恢复oss 数据,读取dicom 恢复序列和Instance
|
||||
</summary>
|
||||
<param name="_oSSService"></param>
|
||||
<param name="_hostEnvironment"></param>
|
||||
<returns></returns>
|
||||
</member>
|
||||
<member name="M:IRaCIS.Core.Application.Service.TrialImageDownloadService.ArchiveDicomFileAsync(FellowOakDicom.DicomDataset,System.Guid,System.Guid,System.Guid)">
|
||||
<summary>
|
||||
单个文件接收 归档
|
||||
</summary>
|
||||
<param name="dataset"></param>
|
||||
<param name="trialId"></param>
|
||||
<param name="subjectId"></param>
|
||||
<param name="subjectVisitId"></param>
|
||||
<returns></returns>
|
||||
<exception cref="T:System.NotImplementedException"></exception>
|
||||
</member>
|
||||
<member name="T:IRaCIS.Core.Application.Service.AttachmentService">
|
||||
<summary>
|
||||
医生文档关联关系维护
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
using DocumentFormat.OpenXml.EMMA;
|
||||
using Aliyun.OSS;
|
||||
using DocumentFormat.OpenXml.EMMA;
|
||||
using DocumentFormat.OpenXml.Office.CustomUI;
|
||||
using DocumentFormat.OpenXml.Office2010.Excel;
|
||||
using DocumentFormat.OpenXml.Office2013.Drawing.ChartStyle;
|
||||
using FellowOakDicom;
|
||||
|
@ -8,7 +10,9 @@ using FellowOakDicom.IO.Buffer;
|
|||
using IRaCIS.Core.Application.Helper;
|
||||
using IRaCIS.Core.Application.ViewModel;
|
||||
using IRaCIS.Core.Domain.Models;
|
||||
using IRaCIS.Core.Infrastructure;
|
||||
using MassTransit;
|
||||
using Medallion.Threading;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
@ -19,6 +23,7 @@ using System.Collections.Generic;
|
|||
using System.Data;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
@ -35,7 +40,10 @@ namespace IRaCIS.Core.Application.Service
|
|||
/// <param name="_trialRepository"></param>
|
||||
/// <param name="_oSSService"></param>
|
||||
[ApiExplorerSettings(GroupName = "Common")]
|
||||
public class TrialImageDownloadService(IRepository<Trial> _trialRepository, IOSSService _oSSService, IWebHostEnvironment _hostEnvironment) : BaseService
|
||||
public class TrialImageDownloadService(IRepository<Trial> _trialRepository, IOSSService _oSSService, IWebHostEnvironment _hostEnvironment,
|
||||
IRepository<DicomStudy> _studyRepository,
|
||||
IRepository<DicomSeries> _seriesRepository,
|
||||
IRepository<DicomInstance> _instanceRepository) : BaseService
|
||||
{
|
||||
|
||||
|
||||
|
@ -1207,6 +1215,581 @@ namespace IRaCIS.Core.Application.Service
|
|||
return ResponseOutput.Ok();
|
||||
}
|
||||
#endregion
|
||||
|
||||
|
||||
#region oss 下载删除影像 ,并且恢复数据
|
||||
[AllowAnonymous]
|
||||
public async Task<IResponseOutput> RestoreDBOSSDate(
|
||||
[FromServices] IOSSService _oSSService, [FromServices] IWebHostEnvironment _hostEnvironment, [FromServices] IRepository<DicomStudy> _studyRepository)
|
||||
{
|
||||
var folder = FileStoreHelper.GetIRaCISRootDataFolder(_hostEnvironment);
|
||||
|
||||
var outputFile = Path.Combine(folder, $"{Guid.NewGuid()}_deleteKey_info.txt");
|
||||
var outputFile2 = Path.Combine(folder, $"{Guid.NewGuid()}_deleteKeyExport_info.txt");
|
||||
|
||||
var outputErrorFile = Path.Combine(folder, $"{Guid.NewGuid()}_deleteKeyerror.txt");
|
||||
var outputErrorFile2 = Path.Combine(folder, $"{Guid.NewGuid()}_deleteKeyerrorStudy.txt");
|
||||
|
||||
|
||||
var aliConfig = _oSSService.ObjectStoreServiceOptions.AliyunOSS;
|
||||
|
||||
var tempToken = _oSSService.GetObjectStoreTempToken();
|
||||
|
||||
var _ossClient = new OssClient(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? aliConfig.EndPoint : aliConfig.InternalEndpoint,
|
||||
tempToken.AliyunOSS.AccessKeyId,
|
||||
tempToken.AliyunOSS.AccessKeySecret,
|
||||
tempToken.AliyunOSS.SecurityToken);
|
||||
|
||||
|
||||
var allVersions = new List<ObjectVersionSummary>();
|
||||
var allDeleteMarkers = new List<DeleteMarkerSummary>();
|
||||
|
||||
var request = new ListObjectVersionsRequest(tempToken.AliyunOSS.BucketName)
|
||||
{
|
||||
Prefix = "01000000-ac13-0242-6397-08dcd2d2a091/Image",
|
||||
//Prefix = "01000000-ac13-0242-6397-08dcd2d2a091/Image/08dd9c04-c1b2-c2da-0242-ac1301000000/01000000-ac13-0242-235b-08dd9c04c1b3",
|
||||
MaxKeys = 1000,
|
||||
};
|
||||
|
||||
ObjectVersionList result;
|
||||
do
|
||||
{
|
||||
|
||||
result = _ossClient.ListObjectVersions(request);
|
||||
|
||||
if (result.ObjectVersionSummaries != null)
|
||||
allVersions.AddRange(result.ObjectVersionSummaries);
|
||||
|
||||
if (result.DeleteMarkerSummaries != null)
|
||||
allDeleteMarkers.AddRange(result.DeleteMarkerSummaries);
|
||||
|
||||
request.KeyMarker = result.NextKeyMarker;
|
||||
request.VersionIdMarker = result.NextVersionIdMarker;
|
||||
|
||||
} while (result.IsTruncated);
|
||||
|
||||
Console.WriteLine($"共找到 {allDeleteMarkers.Count} 个删除标记");
|
||||
|
||||
|
||||
|
||||
int total = allDeleteMarkers.Count;
|
||||
|
||||
int processed = 0;
|
||||
double lastPercent = 0;
|
||||
|
||||
|
||||
// 按 Key 分组,找每个删除标记前的最近版本
|
||||
var versionsByKey = allVersions
|
||||
.GroupBy(v => v.Key)
|
||||
.ToDictionary(g => g.Key, g => g.OrderByDescending(x => x.LastModified).ToList());
|
||||
|
||||
foreach (var del in allDeleteMarkers)
|
||||
{
|
||||
#region 防止阿里云过期
|
||||
if (tempToken.AliyunOSS.Expiration.AddSeconds(10) <= DateTime.Now)
|
||||
{
|
||||
tempToken = _oSSService.GetObjectStoreTempToken();
|
||||
|
||||
_ossClient = new OssClient(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? aliConfig.EndPoint : aliConfig.InternalEndpoint,
|
||||
tempToken.AliyunOSS.AccessKeyId,
|
||||
tempToken.AliyunOSS.AccessKeySecret,
|
||||
tempToken.AliyunOSS.SecurityToken);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
if (!versionsByKey.TryGetValue(del.Key, out var versions))
|
||||
continue; // 没有历史版本无法恢复
|
||||
|
||||
var prevVersion = versions.FirstOrDefault(v => v.LastModified < del.LastModified);
|
||||
if (prevVersion == null)
|
||||
continue; // 没找到可恢复版本
|
||||
|
||||
if (Path.GetExtension(prevVersion.Key).IsNotNullOrEmpty())
|
||||
{
|
||||
continue;//不是dicom 文件
|
||||
}
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
//await File.AppendAllTextAsync(outputFile, $"{prevVersion.Key},{prevVersion.VersionId}" + Environment.NewLine);
|
||||
|
||||
var getReq = new GetObjectRequest(tempToken.AliyunOSS.BucketName, prevVersion.Key)
|
||||
{
|
||||
VersionId = prevVersion.VersionId
|
||||
};
|
||||
|
||||
using (var getResult = _ossClient.GetObject(getReq))
|
||||
using (var memStream = new MemoryStream())
|
||||
{
|
||||
// 先把 OSS 流复制到内存流
|
||||
getResult.Content.CopyTo(memStream);
|
||||
memStream.Position = 0;
|
||||
|
||||
// 读取 DICOM 信息
|
||||
var dicomFile = DicomFile.Open(memStream);
|
||||
var studyInstanceUID = dicomFile.Dataset.GetString(DicomTag.StudyInstanceUID);
|
||||
|
||||
var findInfo = await _studyRepository.Where(t => t.StudyInstanceUid == studyInstanceUID && t.TrialId == Guid.Parse("01000000-ac13-0242-6397-08dcd2d2a091"))
|
||||
.Select(t => new { t.StudyInstanceUid, t.Subject.Code, t.SubjectVisit.VisitName, t.SubjectId, t.SubjectVisitId }).FirstOrDefaultAsync();
|
||||
|
||||
if (findInfo != null)
|
||||
{
|
||||
|
||||
// 再保存到另一个路径(可以使用 fo-dicom 保存)
|
||||
|
||||
var fileName = Path.GetFileNameWithoutExtension(prevVersion.Key);
|
||||
var anotherPath = Path.Combine(folder, findInfo.Code, findInfo.VisitName, studyInstanceUID, fileName);
|
||||
// 去掉 folder 部分,得到相对路径
|
||||
var relativePath = Path.GetRelativePath(folder, anotherPath);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(anotherPath));
|
||||
dicomFile.Save(anotherPath);
|
||||
|
||||
await File.AppendAllTextAsync(outputFile2, $"{findInfo.SubjectId},{findInfo.SubjectVisitId},{prevVersion.Key},{prevVersion.VersionId},{relativePath},{findInfo.Code},{findInfo.VisitName},{findInfo.StudyInstanceUid},{fileName}" + Environment.NewLine);
|
||||
}
|
||||
else
|
||||
{
|
||||
await File.AppendAllTextAsync(outputErrorFile2, $"{studyInstanceUID},{prevVersion.Key},{prevVersion.VersionId}" + Environment.NewLine);
|
||||
}
|
||||
|
||||
//Console.WriteLine($"读取到 studyInstanceUID: {studyInstanceUID}");
|
||||
|
||||
//var localPath = Path.Combine(folder, prevVersion.Key.Trim('/').Replace('/', Path.DirectorySeparatorChar));
|
||||
//Directory.CreateDirectory(Path.GetDirectoryName(localPath));
|
||||
//// 保存到原本路径
|
||||
//memStream.Position = 0;
|
||||
//using (var fs = File.Create(localPath))
|
||||
//{
|
||||
// memStream.CopyTo(fs);
|
||||
//}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
//Console.WriteLine($"✅ 下载成功: {prevVersion.Key} (version={prevVersion.VersionId})");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"❌ 下载失败: {prevVersion.Key}, 错误: {ex.Message}");
|
||||
|
||||
await File.AppendAllTextAsync(outputErrorFile, $"{prevVersion.Key},{prevVersion.VersionId}" + Environment.NewLine);
|
||||
}
|
||||
finally
|
||||
{
|
||||
processed++;
|
||||
double percent = processed * 100.0 / total;
|
||||
|
||||
// 每提升 5% 或完成时输出
|
||||
if (percent - lastPercent >= 2.0 || processed == total)
|
||||
{
|
||||
lastPercent = percent;
|
||||
Console.WriteLine($"{DateTime.Now} 进度: {processed}/{total} ({percent:F2}%)");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 使用 CopyObject 把历史版本拷贝为最新版本(恢复)
|
||||
//var copyReq = new CopyObjectRequest
|
||||
//{
|
||||
// Bucket = bucketName,
|
||||
// Key = prevVersion.Key,
|
||||
// SourceBucket = bucketName,
|
||||
// SourceKey = prevVersion.Key,
|
||||
// SourceVersionId = prevVersion.VersionId
|
||||
//};
|
||||
|
||||
//try
|
||||
//{
|
||||
// var copyResult = client.CopyObject(copyReq);
|
||||
// Console.WriteLine($"✅ 恢复成功: {prevVersion.Key} -> newVersionId={copyResult.VersionId}");
|
||||
//}
|
||||
//catch (Exception ex)
|
||||
//{
|
||||
// Console.WriteLine($"❌ 恢复失败: {prevVersion.Key}, 错误: {ex.Message}");
|
||||
//}
|
||||
}
|
||||
|
||||
|
||||
|
||||
return ResponseOutput.Ok();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public async Task<IResponseOutput> OSSDeleteReStorre([FromServices] IOSSService _oSSService, [FromServices] IWebHostEnvironment _hostEnvironment)
|
||||
{
|
||||
var aliConfig = _oSSService.ObjectStoreServiceOptions.AliyunOSS;
|
||||
|
||||
var tempToken = _oSSService.GetObjectStoreTempToken();
|
||||
|
||||
var _ossClient = new OssClient(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? aliConfig.EndPoint : aliConfig.InternalEndpoint,
|
||||
tempToken.AliyunOSS.AccessKeyId,
|
||||
tempToken.AliyunOSS.AccessKeySecret,
|
||||
tempToken.AliyunOSS.SecurityToken);
|
||||
|
||||
|
||||
var allVersions = new List<ObjectVersionSummary>();
|
||||
var allDeleteMarkers = new List<DeleteMarkerSummary>();
|
||||
|
||||
var request = new ListObjectVersionsRequest(tempToken.AliyunOSS.BucketName)
|
||||
{
|
||||
Prefix = "test-delete-restore",
|
||||
//Prefix = "01000000-ac13-0242-6397-08dcd2d2a091/Image/08dd9c04-c1b2-c2da-0242-ac1301000000/01000000-ac13-0242-235b-08dd9c04c1b3",
|
||||
MaxKeys = 1000,
|
||||
};
|
||||
|
||||
ObjectVersionList result;
|
||||
do
|
||||
{
|
||||
|
||||
result = _ossClient.ListObjectVersions(request);
|
||||
|
||||
if (result.ObjectVersionSummaries != null)
|
||||
allVersions.AddRange(result.ObjectVersionSummaries);
|
||||
|
||||
if (result.DeleteMarkerSummaries != null)
|
||||
allDeleteMarkers.AddRange(result.DeleteMarkerSummaries);
|
||||
|
||||
request.KeyMarker = result.NextKeyMarker;
|
||||
request.VersionIdMarker = result.NextVersionIdMarker;
|
||||
|
||||
} while (result.IsTruncated);
|
||||
|
||||
Console.WriteLine($"共找到 {allDeleteMarkers.Count} 个删除标记");
|
||||
|
||||
var versionsByKey = allVersions
|
||||
.GroupBy(v => v.Key)
|
||||
.ToDictionary(g => g.Key, g => g.OrderByDescending(x => x.LastModified).ToList());
|
||||
|
||||
|
||||
foreach (var del in allDeleteMarkers)
|
||||
{
|
||||
#region 防止阿里云过期
|
||||
if (tempToken.AliyunOSS.Expiration.AddSeconds(10) <= DateTime.Now)
|
||||
{
|
||||
tempToken = _oSSService.GetObjectStoreTempToken();
|
||||
|
||||
_ossClient = new OssClient(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? aliConfig.EndPoint : aliConfig.InternalEndpoint,
|
||||
tempToken.AliyunOSS.AccessKeyId,
|
||||
tempToken.AliyunOSS.AccessKeySecret,
|
||||
tempToken.AliyunOSS.SecurityToken);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
if (!versionsByKey.TryGetValue(del.Key, out var versions))
|
||||
continue; // 没有历史版本无法恢复
|
||||
|
||||
var prevVersion = versions.FirstOrDefault(v => v.LastModified < del.LastModified);
|
||||
if (prevVersion == null)
|
||||
continue; // 没找到可恢复版本
|
||||
|
||||
|
||||
|
||||
|
||||
// 创建 CopyObject 请求
|
||||
// 先用构造函数指定源和目标
|
||||
var copyReq = new CopyObjectRequest(
|
||||
sourceBucketName: tempToken.AliyunOSS.BucketName,
|
||||
sourceKey: prevVersion.Key,
|
||||
destinationBucketName: tempToken.AliyunOSS.BucketName,
|
||||
destinationKey: prevVersion.Key // 覆盖到同名 Key,达到“恢复”的效果
|
||||
);
|
||||
|
||||
// 再设置版本号
|
||||
copyReq.SourceVersionId = prevVersion.VersionId;
|
||||
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
var copyResult = _ossClient.CopyObject(copyReq);
|
||||
Console.WriteLine($"✅ 恢复成功: {prevVersion.Key}, 新版本ID={copyResult.VersionId}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"❌ 恢复失败: {prevVersion.Key}, 错误: {ex.Message}");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
return ResponseOutput.Ok();
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取excel 恢复oss 数据,读取dicom 恢复序列和Instance
|
||||
/// </summary>
|
||||
/// <param name="_oSSService"></param>
|
||||
/// <param name="_hostEnvironment"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<IResponseOutput> ReadExcelReStorreOSSDeleteDataAndDBData([FromServices] IOSSService _oSSService, [FromServices] IWebHostEnvironment _hostEnvironment)
|
||||
{
|
||||
var folder = FileStoreHelper.GetIRaCISRootDataFolder(_hostEnvironment);
|
||||
|
||||
var trialId = Guid.Parse("01000000-ac13-0242-6397-08dcd2d2a091");
|
||||
|
||||
var rows = await MiniExcel.QueryAsync<RestoreOSSDeleteDataDTO>(@"C:\Users\hang\Desktop\删除恢复.xlsx");
|
||||
|
||||
rows = rows.Where(t => !string.IsNullOrEmpty(t.FileName.ToString())).ToList();
|
||||
|
||||
int total = rows.Count();
|
||||
int processed = 0;
|
||||
double lastPercent = 0;
|
||||
|
||||
var outputErrorFile = Path.Combine(@"D:\dicomWrite", $"{Guid.NewGuid()}_dicom_info_error.txt");
|
||||
|
||||
var aliConfig = _oSSService.ObjectStoreServiceOptions.AliyunOSS;
|
||||
|
||||
var tempToken = _oSSService.GetObjectStoreTempToken();
|
||||
|
||||
var _ossClient = new OssClient(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? aliConfig.EndPoint : aliConfig.InternalEndpoint,
|
||||
tempToken.AliyunOSS.AccessKeyId,
|
||||
tempToken.AliyunOSS.AccessKeySecret,
|
||||
tempToken.AliyunOSS.SecurityToken);
|
||||
|
||||
|
||||
foreach (var item in rows)
|
||||
{
|
||||
#region 防止阿里云过期
|
||||
if (tempToken.AliyunOSS.Expiration.AddSeconds(10) <= DateTime.Now)
|
||||
{
|
||||
tempToken = _oSSService.GetObjectStoreTempToken();
|
||||
|
||||
_ossClient = new OssClient(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? aliConfig.EndPoint : aliConfig.InternalEndpoint,
|
||||
tempToken.AliyunOSS.AccessKeyId,
|
||||
tempToken.AliyunOSS.AccessKeySecret,
|
||||
tempToken.AliyunOSS.SecurityToken);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
// 创建 CopyObject 请求
|
||||
// 先用构造函数指定源和目标
|
||||
var copyReq = new CopyObjectRequest(
|
||||
sourceBucketName: tempToken.AliyunOSS.BucketName,
|
||||
sourceKey: item.Key,
|
||||
destinationBucketName: tempToken.AliyunOSS.BucketName,
|
||||
destinationKey: item.Key // 覆盖到同名 Key,达到“恢复”的效果
|
||||
);
|
||||
|
||||
// 再设置版本号
|
||||
copyReq.SourceVersionId = item.VersionId;
|
||||
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
var copyResult = _ossClient.CopyObject(copyReq);
|
||||
|
||||
|
||||
#region 读取本地dicom ,判断数据库是否存在该序列
|
||||
|
||||
var dicomFilePath = Path.Combine(folder, item.RelativePath);
|
||||
|
||||
|
||||
var dicomFile = await DicomFile.OpenAsync(dicomFilePath);
|
||||
|
||||
await ArchiveDicomFileAsync(dicomFile.Dataset, trialId, item.SubjectId, item.SubjectVisitId);
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
}
|
||||
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
var errorMsg = $"❌ 恢复失败: {item.Key}, 错误: {ex.Message}";
|
||||
Console.WriteLine(errorMsg);
|
||||
|
||||
await File.AppendAllTextAsync(outputErrorFile, errorMsg + Environment.NewLine);
|
||||
}
|
||||
finally
|
||||
{
|
||||
processed++;
|
||||
double percent = processed * 100.0 / total;
|
||||
|
||||
if (percent - lastPercent >= 2.0 || processed == total)
|
||||
{
|
||||
lastPercent = percent;
|
||||
Console.WriteLine($"{DateTime.Now} 进度: {processed}/{total} ({percent:F2}%)");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
await _studyRepository.SaveChangesAsync();
|
||||
|
||||
return ResponseOutput.Ok();
|
||||
}
|
||||
|
||||
public class RestoreOSSDeleteDataDTO
|
||||
{
|
||||
public Guid SubjectId { get; set; }
|
||||
|
||||
public Guid SubjectVisitId { get; set; }
|
||||
|
||||
public string Key { get; set; }
|
||||
|
||||
public string VersionId { get; set; }
|
||||
|
||||
public string RelativePath { get; set; }
|
||||
|
||||
|
||||
public string FileName { get; set; }
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单个文件接收 归档
|
||||
/// </summary>
|
||||
/// <param name="dataset"></param>
|
||||
/// <param name="trialId"></param>
|
||||
/// <param name="subjectId"></param>
|
||||
/// <param name="subjectVisitId"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="NotImplementedException"></exception>
|
||||
public async Task ArchiveDicomFileAsync(DicomDataset dataset, Guid trialId, Guid subjectId, Guid subjectVisitId)
|
||||
{
|
||||
string studyInstanceUid = dataset.GetString(DicomTag.StudyInstanceUID);
|
||||
string seriesInstanceUid = dataset.GetString(DicomTag.SeriesInstanceUID);
|
||||
string sopInstanceUid = dataset.GetString(DicomTag.SOPInstanceUID);
|
||||
|
||||
Guid studyId = IdentifierHelper.CreateGuid(studyInstanceUid, trialId.ToString());
|
||||
Guid seriesId = IdentifierHelper.CreateGuid(studyInstanceUid, seriesInstanceUid, trialId.ToString());
|
||||
Guid instanceId = IdentifierHelper.CreateGuid(studyInstanceUid, seriesInstanceUid, sopInstanceUid, trialId.ToString());
|
||||
|
||||
var isSeriesNeedAdd = false;
|
||||
var isInstanceNeedAdd = false;
|
||||
|
||||
|
||||
var findStudy = await _studyRepository.FirstOrDefaultAsync(t => t.Id == studyId);
|
||||
var findSerice = await _seriesRepository.FirstOrDefaultAsync(t => t.Id == seriesId);
|
||||
var findInstance = await _instanceRepository.FirstOrDefaultAsync(t => t.Id == instanceId);
|
||||
|
||||
|
||||
|
||||
if (findSerice == null)
|
||||
{
|
||||
isSeriesNeedAdd = true;
|
||||
|
||||
findSerice = new DicomSeries
|
||||
{
|
||||
|
||||
|
||||
|
||||
Id = seriesId,
|
||||
StudyId = findStudy.Id,
|
||||
|
||||
StudyInstanceUid = findStudy.StudyInstanceUid,
|
||||
SeriesInstanceUid = seriesInstanceUid,
|
||||
SeriesNumber = dataset.GetSingleValueOrDefault(DicomTag.SeriesNumber, 1),
|
||||
//SeriesTime = dataset.GetSingleValueOrDefault(DicomTag.SeriesDate, DateTime.Now).Add(dataset.GetSingleValueOrDefault(DicomTag.SeriesTime, DateTime.Now).TimeOfDay),
|
||||
//SeriesTime = DateTime.TryParse(dataset.GetSingleValue<string>(DicomTag.SeriesDate) + dataset.GetSingleValue<string>(DicomTag.SeriesTime), out DateTime dt) ? dt : null,
|
||||
SeriesTime = dataset.GetSingleValueOrDefault(DicomTag.SeriesDate, string.Empty) == string.Empty ? null : dataset.GetSingleValue<DateTime>(DicomTag.SeriesDate).Add(dataset.GetSingleValueOrDefault(DicomTag.SeriesTime, string.Empty) == string.Empty ? TimeSpan.Zero : dataset.GetSingleValue<DateTime>(DicomTag.SeriesTime).TimeOfDay),
|
||||
|
||||
DicomSeriesDate = dataset.GetSingleValueOrDefault(DicomTag.SeriesDate, string.Empty),
|
||||
DicomSeriesTime = dataset.GetSingleValueOrDefault(DicomTag.SeriesTime, string.Empty),
|
||||
|
||||
Modality = dataset.GetSingleValueOrDefault(DicomTag.Modality, string.Empty),
|
||||
Description = dataset.GetSingleValueOrDefault(DicomTag.SeriesDescription, string.Empty),
|
||||
SliceThickness = dataset.GetSingleValueOrDefault(DicomTag.SliceThickness, string.Empty),
|
||||
|
||||
ImagePositionPatient = dataset.GetSingleValueOrDefault(DicomTag.ImagePositionPatient, string.Empty),
|
||||
ImageOrientationPatient = dataset.GetSingleValueOrDefault(DicomTag.ImageOrientationPatient, string.Empty),
|
||||
BodyPartExamined = dataset.GetSingleValueOrDefault(DicomTag.BodyPartExamined, string.Empty),
|
||||
SequenceName = dataset.GetSingleValueOrDefault(DicomTag.SequenceName, string.Empty),
|
||||
ProtocolName = dataset.GetSingleValueOrDefault(DicomTag.ProtocolName, string.Empty),
|
||||
ImagerPixelSpacing = dataset.GetSingleValueOrDefault(DicomTag.ImagerPixelSpacing, string.Empty),
|
||||
|
||||
AcquisitionTime = dataset.GetSingleValueOrDefault(DicomTag.AcquisitionTime, string.Empty),
|
||||
AcquisitionNumber = dataset.GetSingleValueOrDefault(DicomTag.AcquisitionNumber, string.Empty),
|
||||
TriggerTime = dataset.GetSingleValueOrDefault(DicomTag.TriggerTime, string.Empty),
|
||||
|
||||
TrialId = trialId,
|
||||
SubjectId = subjectId,
|
||||
SubjectVisitId = subjectVisitId,
|
||||
|
||||
InstanceCount = 0
|
||||
};
|
||||
|
||||
++findStudy.SeriesCount;
|
||||
}
|
||||
|
||||
|
||||
var transferSyntaxUID = dataset.GetSingleValueOrDefault(DicomTag.TransferSyntaxUID, string.Empty);
|
||||
|
||||
var isEncapsulated = false;
|
||||
if (transferSyntaxUID.IsNotNullOrEmpty())
|
||||
{
|
||||
isEncapsulated = DicomTransferSyntax.Lookup(DicomUID.Parse(transferSyntaxUID)).IsEncapsulated;
|
||||
}
|
||||
|
||||
if (findInstance == null)
|
||||
{
|
||||
isInstanceNeedAdd = true;
|
||||
findInstance = new DicomInstance
|
||||
{
|
||||
Id = instanceId,
|
||||
StudyId = findStudy.Id,
|
||||
SeriesId = findSerice.Id,
|
||||
StudyInstanceUid = findStudy.StudyInstanceUid,
|
||||
SeriesInstanceUid = findSerice.SeriesInstanceUid,
|
||||
|
||||
TrialId = trialId,
|
||||
SubjectId = subjectId,
|
||||
SubjectVisitId = subjectVisitId,
|
||||
|
||||
|
||||
|
||||
SopInstanceUid = sopInstanceUid,
|
||||
SOPClassUID = dataset.GetSingleValueOrDefault(DicomTag.SOPClassUID, string.Empty),
|
||||
MediaStorageSOPClassUID = dataset.GetSingleValueOrDefault(DicomTag.MediaStorageSOPClassUID, string.Empty),
|
||||
TransferSytaxUID = transferSyntaxUID,
|
||||
MediaStorageSOPInstanceUID = dataset.GetSingleValueOrDefault(DicomTag.MediaStorageSOPInstanceUID, string.Empty),
|
||||
IsEncapsulated = isEncapsulated,
|
||||
|
||||
InstanceNumber = dataset.GetSingleValueOrDefault(DicomTag.InstanceNumber, 1),
|
||||
InstanceTime = dataset.GetSingleValueOrDefault(DicomTag.ContentDate, string.Empty) == string.Empty ? null : dataset.GetSingleValue<DateTime>(DicomTag.ContentDate).Add(dataset.GetSingleValueOrDefault(DicomTag.ContentTime, string.Empty) == string.Empty ? TimeSpan.Zero : dataset.GetSingleValue<DateTime>(DicomTag.ContentTime).TimeOfDay),
|
||||
|
||||
CPIStatus = false,
|
||||
ImageRows = dataset.GetSingleValueOrDefault(DicomTag.Rows, 0),
|
||||
ImageColumns = dataset.GetSingleValueOrDefault(DicomTag.Columns, 0),
|
||||
SliceLocation = dataset.GetSingleValueOrDefault(DicomTag.SliceLocation, 0),
|
||||
|
||||
SliceThickness = dataset.GetSingleValueOrDefault(DicomTag.SliceThickness, string.Empty),
|
||||
NumberOfFrames = dataset.GetSingleValueOrDefault(DicomTag.NumberOfFrames, 0),
|
||||
PixelSpacing = dataset.GetSingleValueOrDefault(DicomTag.PixelSpacing, string.Empty),
|
||||
ImagerPixelSpacing = dataset.GetSingleValueOrDefault(DicomTag.ImagerPixelSpacing, string.Empty),
|
||||
FrameOfReferenceUID = dataset.GetSingleValueOrDefault(DicomTag.FrameOfReferenceUID, string.Empty),
|
||||
WindowCenter = dataset.GetSingleValueOrDefault(DicomTag.WindowCenter, string.Empty),
|
||||
WindowWidth = dataset.GetSingleValueOrDefault(DicomTag.WindowWidth, string.Empty),
|
||||
};
|
||||
|
||||
++findStudy.InstanceCount;
|
||||
++findSerice.InstanceCount;
|
||||
}
|
||||
|
||||
|
||||
if (isSeriesNeedAdd)
|
||||
{
|
||||
await _seriesRepository.AddAsync(findSerice);
|
||||
}
|
||||
if (isInstanceNeedAdd)
|
||||
{
|
||||
await _instanceRepository.AddAsync(findInstance);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -372,309 +372,7 @@ namespace IRaCIS.Core.Application.Service
|
|||
}
|
||||
|
||||
|
||||
[AllowAnonymous]
|
||||
public async Task<IResponseOutput> RestoreDBOSSDate(
|
||||
[FromServices] IOSSService _oSSService, [FromServices] IWebHostEnvironment _hostEnvironment, [FromServices] IRepository<DicomStudy> _studyRepository)
|
||||
{
|
||||
var folder = FileStoreHelper.GetIRaCISRootDataFolder(_hostEnvironment);
|
||||
|
||||
var outputFile = Path.Combine(folder, $"{Guid.NewGuid()}_deleteKey_info.txt");
|
||||
var outputFile2 = Path.Combine(folder, $"{Guid.NewGuid()}_deleteKeyExport_info.txt");
|
||||
|
||||
var outputErrorFile = Path.Combine(folder, $"{Guid.NewGuid()}_deleteKeyerror.txt");
|
||||
var outputErrorFile2 = Path.Combine(folder, $"{Guid.NewGuid()}_deleteKeyerrorStudy.txt");
|
||||
|
||||
|
||||
var aliConfig = _oSSService.ObjectStoreServiceOptions.AliyunOSS;
|
||||
|
||||
var tempToken = _oSSService.GetObjectStoreTempToken();
|
||||
|
||||
var _ossClient = new OssClient(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? aliConfig.EndPoint : aliConfig.InternalEndpoint,
|
||||
tempToken.AliyunOSS.AccessKeyId,
|
||||
tempToken.AliyunOSS.AccessKeySecret,
|
||||
tempToken.AliyunOSS.SecurityToken);
|
||||
|
||||
|
||||
var allVersions = new List<ObjectVersionSummary>();
|
||||
var allDeleteMarkers = new List<DeleteMarkerSummary>();
|
||||
|
||||
var request = new ListObjectVersionsRequest(tempToken.AliyunOSS.BucketName)
|
||||
{
|
||||
Prefix = "01000000-ac13-0242-6397-08dcd2d2a091/Image",
|
||||
//Prefix = "01000000-ac13-0242-6397-08dcd2d2a091/Image/08dd9c04-c1b2-c2da-0242-ac1301000000/01000000-ac13-0242-235b-08dd9c04c1b3",
|
||||
MaxKeys = 1000,
|
||||
};
|
||||
|
||||
ObjectVersionList result;
|
||||
do
|
||||
{
|
||||
|
||||
result = _ossClient.ListObjectVersions(request);
|
||||
|
||||
if (result.ObjectVersionSummaries != null)
|
||||
allVersions.AddRange(result.ObjectVersionSummaries);
|
||||
|
||||
if (result.DeleteMarkerSummaries != null)
|
||||
allDeleteMarkers.AddRange(result.DeleteMarkerSummaries);
|
||||
|
||||
request.KeyMarker = result.NextKeyMarker;
|
||||
request.VersionIdMarker = result.NextVersionIdMarker;
|
||||
|
||||
} while (result.IsTruncated);
|
||||
|
||||
Console.WriteLine($"共找到 {allDeleteMarkers.Count} 个删除标记");
|
||||
|
||||
|
||||
|
||||
int total = allDeleteMarkers.Count;
|
||||
|
||||
int processed = 0;
|
||||
double lastPercent = 0;
|
||||
|
||||
|
||||
// 按 Key 分组,找每个删除标记前的最近版本
|
||||
var versionsByKey = allVersions
|
||||
.GroupBy(v => v.Key)
|
||||
.ToDictionary(g => g.Key, g => g.OrderByDescending(x => x.LastModified).ToList());
|
||||
|
||||
foreach (var del in allDeleteMarkers)
|
||||
{
|
||||
#region 防止阿里云过期
|
||||
if (tempToken.AliyunOSS.Expiration.AddSeconds(10) <= DateTime.Now)
|
||||
{
|
||||
tempToken = _oSSService.GetObjectStoreTempToken();
|
||||
|
||||
_ossClient = new OssClient(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? aliConfig.EndPoint : aliConfig.InternalEndpoint,
|
||||
tempToken.AliyunOSS.AccessKeyId,
|
||||
tempToken.AliyunOSS.AccessKeySecret,
|
||||
tempToken.AliyunOSS.SecurityToken);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
if (!versionsByKey.TryGetValue(del.Key, out var versions))
|
||||
continue; // 没有历史版本无法恢复
|
||||
|
||||
var prevVersion = versions.FirstOrDefault(v => v.LastModified < del.LastModified);
|
||||
if (prevVersion == null)
|
||||
continue; // 没找到可恢复版本
|
||||
|
||||
if (Path.GetExtension(prevVersion.Key).IsNotNullOrEmpty())
|
||||
{
|
||||
continue;//不是dicom 文件
|
||||
}
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
//await File.AppendAllTextAsync(outputFile, $"{prevVersion.Key},{prevVersion.VersionId}" + Environment.NewLine);
|
||||
|
||||
var getReq = new GetObjectRequest(tempToken.AliyunOSS.BucketName, prevVersion.Key)
|
||||
{
|
||||
VersionId = prevVersion.VersionId
|
||||
};
|
||||
|
||||
using (var getResult = _ossClient.GetObject(getReq))
|
||||
using (var memStream = new MemoryStream())
|
||||
{
|
||||
// 先把 OSS 流复制到内存流
|
||||
getResult.Content.CopyTo(memStream);
|
||||
memStream.Position = 0;
|
||||
|
||||
// 读取 DICOM 信息
|
||||
var dicomFile = DicomFile.Open(memStream);
|
||||
var studyInstanceUID = dicomFile.Dataset.GetString(DicomTag.StudyInstanceUID);
|
||||
|
||||
var findInfo = await _studyRepository.Where(t => t.StudyInstanceUid == studyInstanceUID && t.TrialId == Guid.Parse("01000000-ac13-0242-6397-08dcd2d2a091"))
|
||||
.Select(t => new { t.StudyInstanceUid, t.Subject.Code, t.SubjectVisit.VisitName, t.SubjectId, t.SubjectVisitId }).FirstOrDefaultAsync();
|
||||
|
||||
if (findInfo != null)
|
||||
{
|
||||
|
||||
// 再保存到另一个路径(可以使用 fo-dicom 保存)
|
||||
|
||||
var fileName = Path.GetFileNameWithoutExtension(prevVersion.Key);
|
||||
var anotherPath = Path.Combine(folder, findInfo.Code, findInfo.VisitName, studyInstanceUID, fileName);
|
||||
// 去掉 folder 部分,得到相对路径
|
||||
var relativePath = Path.GetRelativePath(folder, anotherPath);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(anotherPath));
|
||||
dicomFile.Save(anotherPath);
|
||||
|
||||
await File.AppendAllTextAsync(outputFile2, $"{findInfo.SubjectId},{findInfo.SubjectVisitId},{prevVersion.Key},{prevVersion.VersionId},{relativePath},{findInfo.Code},{findInfo.VisitName},{findInfo.StudyInstanceUid},{fileName}" + Environment.NewLine);
|
||||
}
|
||||
else
|
||||
{
|
||||
await File.AppendAllTextAsync(outputErrorFile2, $"{studyInstanceUID},{prevVersion.Key},{prevVersion.VersionId}" + Environment.NewLine);
|
||||
}
|
||||
|
||||
//Console.WriteLine($"读取到 studyInstanceUID: {studyInstanceUID}");
|
||||
|
||||
//var localPath = Path.Combine(folder, prevVersion.Key.Trim('/').Replace('/', Path.DirectorySeparatorChar));
|
||||
//Directory.CreateDirectory(Path.GetDirectoryName(localPath));
|
||||
//// 保存到原本路径
|
||||
//memStream.Position = 0;
|
||||
//using (var fs = File.Create(localPath))
|
||||
//{
|
||||
// memStream.CopyTo(fs);
|
||||
//}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
//Console.WriteLine($"✅ 下载成功: {prevVersion.Key} (version={prevVersion.VersionId})");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"❌ 下载失败: {prevVersion.Key}, 错误: {ex.Message}");
|
||||
|
||||
await File.AppendAllTextAsync(outputErrorFile, $"{prevVersion.Key},{prevVersion.VersionId}" + Environment.NewLine);
|
||||
}
|
||||
finally
|
||||
{
|
||||
processed++;
|
||||
double percent = processed * 100.0 / total;
|
||||
|
||||
// 每提升 5% 或完成时输出
|
||||
if (percent - lastPercent >= 2.0 || processed == total)
|
||||
{
|
||||
lastPercent = percent;
|
||||
Console.WriteLine($"{DateTime.Now} 进度: {processed}/{total} ({percent:F2}%)");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 使用 CopyObject 把历史版本拷贝为最新版本(恢复)
|
||||
//var copyReq = new CopyObjectRequest
|
||||
//{
|
||||
// Bucket = bucketName,
|
||||
// Key = prevVersion.Key,
|
||||
// SourceBucket = bucketName,
|
||||
// SourceKey = prevVersion.Key,
|
||||
// SourceVersionId = prevVersion.VersionId
|
||||
//};
|
||||
|
||||
//try
|
||||
//{
|
||||
// var copyResult = client.CopyObject(copyReq);
|
||||
// Console.WriteLine($"✅ 恢复成功: {prevVersion.Key} -> newVersionId={copyResult.VersionId}");
|
||||
//}
|
||||
//catch (Exception ex)
|
||||
//{
|
||||
// Console.WriteLine($"❌ 恢复失败: {prevVersion.Key}, 错误: {ex.Message}");
|
||||
//}
|
||||
}
|
||||
|
||||
|
||||
|
||||
return ResponseOutput.Ok();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public async Task<IResponseOutput> OSSDeleteReStorre([FromServices] IOSSService _oSSService, [FromServices] IWebHostEnvironment _hostEnvironment)
|
||||
{
|
||||
var aliConfig = _oSSService.ObjectStoreServiceOptions.AliyunOSS;
|
||||
|
||||
var tempToken = _oSSService.GetObjectStoreTempToken();
|
||||
|
||||
var _ossClient = new OssClient(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? aliConfig.EndPoint : aliConfig.InternalEndpoint,
|
||||
tempToken.AliyunOSS.AccessKeyId,
|
||||
tempToken.AliyunOSS.AccessKeySecret,
|
||||
tempToken.AliyunOSS.SecurityToken);
|
||||
|
||||
|
||||
var allVersions = new List<ObjectVersionSummary>();
|
||||
var allDeleteMarkers = new List<DeleteMarkerSummary>();
|
||||
|
||||
var request = new ListObjectVersionsRequest(tempToken.AliyunOSS.BucketName)
|
||||
{
|
||||
Prefix = "test-delete-restore",
|
||||
//Prefix = "01000000-ac13-0242-6397-08dcd2d2a091/Image/08dd9c04-c1b2-c2da-0242-ac1301000000/01000000-ac13-0242-235b-08dd9c04c1b3",
|
||||
MaxKeys = 1000,
|
||||
};
|
||||
|
||||
ObjectVersionList result;
|
||||
do
|
||||
{
|
||||
|
||||
result = _ossClient.ListObjectVersions(request);
|
||||
|
||||
if (result.ObjectVersionSummaries != null)
|
||||
allVersions.AddRange(result.ObjectVersionSummaries);
|
||||
|
||||
if (result.DeleteMarkerSummaries != null)
|
||||
allDeleteMarkers.AddRange(result.DeleteMarkerSummaries);
|
||||
|
||||
request.KeyMarker = result.NextKeyMarker;
|
||||
request.VersionIdMarker = result.NextVersionIdMarker;
|
||||
|
||||
} while (result.IsTruncated);
|
||||
|
||||
Console.WriteLine($"共找到 {allDeleteMarkers.Count} 个删除标记");
|
||||
|
||||
var versionsByKey = allVersions
|
||||
.GroupBy(v => v.Key)
|
||||
.ToDictionary(g => g.Key, g => g.OrderByDescending(x => x.LastModified).ToList());
|
||||
|
||||
|
||||
foreach (var del in allDeleteMarkers)
|
||||
{
|
||||
#region 防止阿里云过期
|
||||
if (tempToken.AliyunOSS.Expiration.AddSeconds(10) <= DateTime.Now)
|
||||
{
|
||||
tempToken = _oSSService.GetObjectStoreTempToken();
|
||||
|
||||
_ossClient = new OssClient(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? aliConfig.EndPoint : aliConfig.InternalEndpoint,
|
||||
tempToken.AliyunOSS.AccessKeyId,
|
||||
tempToken.AliyunOSS.AccessKeySecret,
|
||||
tempToken.AliyunOSS.SecurityToken);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
if (!versionsByKey.TryGetValue(del.Key, out var versions))
|
||||
continue; // 没有历史版本无法恢复
|
||||
|
||||
var prevVersion = versions.FirstOrDefault(v => v.LastModified < del.LastModified);
|
||||
if (prevVersion == null)
|
||||
continue; // 没找到可恢复版本
|
||||
|
||||
|
||||
|
||||
|
||||
// 创建 CopyObject 请求
|
||||
// 先用构造函数指定源和目标
|
||||
var copyReq = new CopyObjectRequest(
|
||||
sourceBucketName: tempToken.AliyunOSS.BucketName,
|
||||
sourceKey: prevVersion.Key,
|
||||
destinationBucketName: tempToken.AliyunOSS.BucketName,
|
||||
destinationKey: prevVersion.Key // 覆盖到同名 Key,达到“恢复”的效果
|
||||
);
|
||||
|
||||
// 再设置版本号
|
||||
copyReq.SourceVersionId = prevVersion.VersionId;
|
||||
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
var copyResult = _ossClient.CopyObject(copyReq);
|
||||
Console.WriteLine($"✅ 恢复成功: {prevVersion.Key}, 新版本ID={copyResult.VersionId}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"❌ 恢复失败: {prevVersion.Key}, 错误: {ex.Message}");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
return ResponseOutput.Ok();
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
[AllowAnonymous]
|
||||
|
|
Loading…
Reference in New Issue