增加访视下载接口
continuous-integration/drone/push Build is passing Details

Test_HIR_Net8
hang 2025-12-17 16:57:33 +08:00
parent 6dcdfe4d23
commit 345189995e
6 changed files with 235 additions and 27 deletions

View File

@ -944,12 +944,12 @@ namespace IRaCIS.Core.API.Controllers
var progress = new
{
CountPercent = totalCount > 0
? Math.Round(receivedCount * 100m / totalCount, 2).ToString() + "%"
: "0%",
? Math.Round(receivedCount * 100m / totalCount, 2).ToString()
: "0",
SizePercent = totalSize > 0
? Math.Round(receivedSize * 100m / totalSize, 2).ToString() + "%"
: "0%",
? Math.Round(receivedSize * 100m / totalSize, 2).ToString()
: "0",
Speed = (speedBps / 1024 >= 1024
? (speedBps / 1024 / 1024).ToString("0.00") + " MB/s"
@ -1077,8 +1077,191 @@ namespace IRaCIS.Core.API.Controllers
[HttpPost("download/VisitImageDownload")]
public async Task<IActionResult> VisitImageDownload([FromServices] IPatientService _patientService, [FromServices] IOSSService _oSSService,
[FromServices] IHubContext<DownloadHub, IDownloadClient> _downLoadHub,
[FromServices] IRepository<Trial> _trialRepository,
VisitImageDownloadCommand inCommand)
{
var rusult = await _patientService.GetDownloadSubjectVisitStudyInfo(inCommand);
var visitList = rusult.Data;
var downloadInfo = (SubejctVisitDownload)rusult.OtherData;
var trialInfo = _trialRepository.Where(t => t.Id == inCommand.TrialId).Select(t => new { t.TrialCode, t.ResearchProgramNo }).FirstOrDefault();
var trialZipName = $"{trialInfo.TrialCode}_{trialInfo.ResearchProgramNo}_Image_";
long receivedSize = 0;
long receivedCount = 0;
long totalSize = downloadInfo.ImageSize;
long totalCount = downloadInfo.ImageCount;
var abortToken = HttpContext.RequestAborted;
// -------- SignalR 节流参数 --------
var notifyInterval = TimeSpan.FromSeconds(1);
var lastNotify = DateTime.UtcNow;
// 用于计算下载速度
long lastReceivedSize = 0;
DateTime lastSpeedCheck = DateTime.UtcNow;
async Task NotifyProgressAsync(bool force = false)
{
var now = DateTime.UtcNow;
var elapsedSeconds = (now - lastSpeedCheck).TotalSeconds;
// 如果没有强制推送,并且未到推送间隔,则返回
if (!force && elapsedSeconds < notifyInterval.TotalSeconds)
return;
// 计算下载速度(字节/秒)
double speedBps = 0;
if (elapsedSeconds > 0)
{
speedBps = (receivedSize - lastReceivedSize) / elapsedSeconds;
}
lastSpeedCheck = now;
lastReceivedSize = receivedSize;
lastNotify = DateTime.UtcNow;
var progress = new
{
CountPercent = totalCount > 0
? Math.Round(receivedCount * 100m / totalCount, 2).ToString()
: "0",
SizePercent = totalSize > 0
? Math.Round(receivedSize * 100m / totalSize, 2).ToString()
: "0",
Speed = (speedBps / 1024 >= 1024
? (speedBps / 1024 / 1024).ToString("0.00") + " MB/s"
: (speedBps / 1024).ToString("0.00") + " KB/s")
};
// 不阻塞下载流程
_ = _downLoadHub.Clients
.User(_userInfo.IdentityUserId.ToString())
.ReceivProgressAsync(
inCommand.CurrentNoticeId,
progress
);
}
Response.ContentType = "application/zip";
Response.Headers["Content-Disposition"] = $"attachment; filename={trialZipName}{ExportExcelConverterDate.DateTimeInternationalToString(DateTime.Now, _userInfo.TimeZoneId)}.zip";
Response.Headers["Cache-Control"] = "no-store";
try
{
await using var responseStream = Response.BodyWriter.AsStream();
using var zip = new ZipArchive(responseStream, ZipArchiveMode.Create, leaveOpen: true);
foreach (var visit in visitList)
{
foreach (var study in visit.StudyList)
{
abortToken.ThrowIfCancellationRequested();
var studyTime = study.StudyTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? "UnknownTime";
var modalitysStr = string.Join('_', study.SeriesList.Select(t => t.Modality).Distinct());
// ---------- DICOMDIR ----------
var dicomDirPath = $"{visit.SubjectCode}_{visit.VisitName}/{studyTime}_{modalitysStr}/DICOMDIR";
var dicomDirEntry = zip.CreateEntry(dicomDirPath, CompressionLevel.Fastest);
await using (var entryStream = dicomDirEntry.Open())
await using (var dirStream = await _oSSService.GetStreamFromOSSAsync(study.StudyDIRPath))
{
await dirStream.CopyToAsync(entryStream, 32 * 1024, abortToken);
}
// ---------- IMAGE FILES ----------
foreach (var series in study.SeriesList)
{
foreach (var instance in series.InstanceList)
{
//当前完成大小
receivedSize = receivedSize + instance.FileSize ?? 0;
receivedCount++;
abortToken.ThrowIfCancellationRequested();
var entryPath =
$"{visit.SubjectCode}_{visit.VisitName}/{studyTime}_{modalitysStr}/IMAGE/{instance.FileName}";
var entry = zip.CreateEntry(entryPath, CompressionLevel.Fastest);
await using var entryStream = entry.Open();
await using var source = await _oSSService.GetStreamFromOSSAsync(instance.Path);
#region 将多帧合并为一帧
// 如果你是从 stream 打开
var dicomFile = await DicomFile.OpenAsync(source);
// 获取 Pixel Data 标签
var pixelData = DicomPixelData.Create(dicomFile.Dataset);
//获取像素是否为封装形式
var syntax = dicomFile.Dataset.InternalTransferSyntax;
//对于封装像素的文件做转换
if (syntax.IsEncapsulated)
{
// 创建一个新的片段序列
var newFragments = new DicomOtherByteFragment(DicomTag.PixelData);
// 获取每帧数据并封装为单独的片段
for (int n = 0; n < pixelData.NumberOfFrames; n++)
{
var frameData = pixelData.GetFrame(n);
newFragments.Fragments.Add(new MemoryByteBuffer(frameData.Data));
}
// 替换原有的片段序列
dicomFile.Dataset.AddOrUpdate(newFragments);
}
#endregion
await dicomFile.SaveAsync(entryStream);
//await source.CopyToAsync(entryStream, 32 * 1024, abortToken);
await NotifyProgressAsync();
}
}
}
}
// 正常完成
await NotifyProgressAsync(true);
await _patientService.DownloadImageSuccess(downloadInfo.Id);
}
catch (OperationCanceledException)
{
// ✅ 客户端取消 / 断开 —— 正常情况
Log.Logger.Warning("Download canceled by client");
}
catch (IOException ex) when (abortToken.IsCancellationRequested)
{
// ✅ HttpClient 流在中断时的常见异常
Log.Logger.Warning($"Client disconnected: {ex.Message}");
}
catch (NullReferenceException ex) when (abortToken.IsCancellationRequested)
{
// ✅ HttpConnection.ContentLengthReadStream 已知问题
Log.Logger.Warning($"Stream aborted: {ex.Message}");
}
return new EmptyResult();
}
}
}

View File

@ -13,6 +13,8 @@ namespace IRaCIS.Core.Application.Helper
public class StudyDIRInfo
{
public Guid SubjectId { get; set; }
public Guid TrialId { get; set; }
public Guid SubjectVisitId { get; set; }
// Study
public Guid DicomStudyId { get; set; }

View File

@ -18834,12 +18834,10 @@
<param name="inCommand"></param>
<returns></returns>
</member>
<member name="M:IRaCIS.Application.Services.PatientService.GetDownloadSubjectVisitStudyInfo(System.Guid,System.Guid,IRaCIS.Core.Infra.EFCore.IRepository{IRaCIS.Core.Domain.Models.DicomStudy},IRaCIS.Core.Application.Helper.IOSSService)">
<member name="M:IRaCIS.Application.Services.PatientService.GetDownloadSubjectVisitStudyInfo(IRaCIS.Application.Contracts.VisitImageDownloadCommand)">
<summary>
获取下载的访视检查信息
</summary>
<param name="trialId"></param>
<param name="subjectVisitId"></param>
<returns></returns>
</member>
<member name="M:IRaCIS.Application.Services.PatientService.DownloadImageSuccess(System.Guid)">

View File

@ -965,6 +965,16 @@ namespace IRaCIS.Application.Contracts
}
public class VisitImageDownloadCommand
{
public Guid TrialId { get; set; }
public List<Guid> SubjectVisitIdList { get; set; }
[NotDefault]
public string CurrentNoticeId { get; set; }
}
public class PatientImageDownloadCommand
{
public List<Guid> PatientIdList { get; set; }
@ -1211,6 +1221,19 @@ namespace IRaCIS.Application.Contracts
}
public class DownloadVisitDto
{
public Guid TrialId { get; set; }
public Guid SubjectId { get; set; }
public string SubjectCode { get; set; }
public string VisitName { get; set; }
public List<DownloadDicomStudyDto> StudyList { get; set; }
}
public class DownloadPatientDto
{
public string PatientName { get; set; }

View File

@ -10,5 +10,8 @@ namespace IRaCIS.Application.Interfaces
public Task<IResponseOutput> DownloadImageSuccess(Guid trialImageDownloadId);
public Task<IResponseOutput<List<DownloadVisitDto>>> GetDownloadSubjectVisitStudyInfo(VisitImageDownloadCommand inCommand);
}
}

View File

@ -55,6 +55,7 @@ namespace IRaCIS.Application.Services
public class PatientService(IRepository<SCPStudySubjectVisit> _studySubjectVisitRepository,
IRepository<TrialIdentityUser> _trialIdentityUserRepository,
IRepository<SCPStudy> _SCPStudyRepository, IOSSService _oSSService,
IRepository<DicomStudy> _dicomStudyRepository,
IRepository<IdentityUser> _identityUserRepository,
IRepository<SubjectPatient> _subjectPatientRepository, IRepository<SCPStudyHospitalGroup> _SCPStudyHospitalGroupRepository,
IRepository<Trial> _trialRepository,
@ -3458,11 +3459,8 @@ namespace IRaCIS.Application.Services
/// <summary>
/// 获取下载的访视检查信息
/// </summary>
/// <param name="trialId"></param>
/// <param name="subjectVisitId"></param>
/// <returns></returns>
public async Task<IResponseOutput> GetDownloadSubjectVisitStudyInfo(Guid trialId, Guid subjectVisitId,
[FromServices] IRepository<DicomStudy> _dicomStudyRepository, [FromServices] IOSSService _oSSService)
public async Task<IResponseOutput<List<DownloadVisitDto> >> GetDownloadSubjectVisitStudyInfo(VisitImageDownloadCommand inCommand)
{
var dirDic = new Dictionary<string, string>();
@ -3470,13 +3468,15 @@ namespace IRaCIS.Application.Services
#region DIR处理导出文件名并将对应关系上传到OSS里面存储
//有传输语法值的导出 才生成DIR
if (_subjectVisitRepository.Where(t => t.Id == subjectVisitId).SelectMany(t => t.StudyList.SelectMany(t => t.InstanceList)).All(c => c.TransferSyntaxUID != string.Empty))
if (_subjectVisitRepository.Where(t => inCommand.SubjectVisitIdList.Contains(t.Id) ).SelectMany(t => t.StudyList.SelectMany(t => t.InstanceList)).All(c => c.TransferSyntaxUID != string.Empty))
{
var list = _subjectVisitRepository.Where(t => t.Id == subjectVisitId).SelectMany(t => t.StudyList)
var list = _subjectVisitRepository.Where(t => inCommand.SubjectVisitIdList.Contains(t.Id)).SelectMany(t => t.StudyList)
.SelectMany(t => t.InstanceList)
.Select(t => new StudyDIRInfo()
{
SubjectId=t.SubjectId,
TrialId=t.TrialId,
SubjectVisitId=t.SubjectVisitId,
DicomStudyId = t.DicomStudy.Id,
PatientId = t.DicomStudy.PatientId,
@ -3509,11 +3509,11 @@ namespace IRaCIS.Application.Services
}).ToList();
var pathInfo = await _subjectVisitRepository.Where(t => t.Id == subjectVisitId).Select(t => new { t.TrialId, t.SubjectId, VisitId = t.Id }).FirstNotNullAsync();
//var pathInfoList = await _subjectVisitRepository.Where(t => inCommand.SubjectVisitIdList.Contains(t.Id)).Select(t => new { t.TrialId, t.SubjectId, VisitId = t.Id }).ToListAsync();
foreach (var item in list.GroupBy(t => new { t.StudyInstanceUid, t.DicomStudyId }))
foreach (var item in list.GroupBy(t => new { t.StudyInstanceUid, t.DicomStudyId,t.SubjectVisitId,t.TrialId,t.SubjectId }))
{
var ossFolder = $"{pathInfo.TrialId}/Image/{pathInfo.SubjectId}/{pathInfo.VisitId}/{item.Key.StudyInstanceUid}";
var ossFolder = $"{item.Key.TrialId}/Image/{item.Key.SubjectId}/{item.Key.SubjectVisitId}/{item.Key.StudyInstanceUid}";
var isSucess = await SafeBussinessHelper.RunAsync(async () => await DicomDIRHelper.GenerateStudyDIRAndUploadAsync(item.ToList(), dirDic, ossFolder, _oSSService));
@ -3529,9 +3529,9 @@ namespace IRaCIS.Application.Services
#endregion
var query = from sv in _subjectVisitRepository.Where(t => t.Id == subjectVisitId)
var query = from sv in _subjectVisitRepository.Where(t => inCommand.SubjectVisitIdList.Contains(t.Id))
select new
select new DownloadVisitDto()
{
TrialId = sv.TrialId,
SubjectId = sv.SubjectId,
@ -3567,9 +3567,9 @@ namespace IRaCIS.Application.Services
};
var result = query.FirstOrDefault();
var visitList = await query.ToListAsync();
foreach (var item in result.StudyList.SelectMany(t => t.SeriesList).SelectMany(t => t.InstanceList))
foreach (var item in visitList.SelectMany(t=>t.StudyList).SelectMany(t => t.SeriesList).SelectMany(t => t.InstanceList))
{
var key = item.InstanceId.ToString();
if (dirDic.ContainsKey(key))
@ -3580,7 +3580,7 @@ namespace IRaCIS.Application.Services
var downLoadList = new ImageDownloadLog()
{
DownLoadList = result.StudyList
DownLoadList = visitList.SelectMany(t => t.StudyList)
.Select(t => new ImageDownloadInfo()
{ PatientId = t.PatientId, StudyId = t.StudyId, StudyInstanceUid = t.StudyInstanceUid, ImageCount = t.SeriesList.SelectMany(c => c.InstanceList).Count() }).ToList()
};
@ -3590,16 +3590,15 @@ namespace IRaCIS.Application.Services
DownLoadListStr = downLoadList.ToJsonStr(),
Id = NewId.NextSequentialGuid(),
IP = _userInfo.IP,
SubjectVisitId = subjectVisitId,
DownloadStartTime = DateTime.Now,
StudyCount = result.StudyList.Count(),
ImageCount = result.StudyList.Sum(s => s.SeriesList.Sum(s => s.InstanceList.Count())),
ImageSize = result.StudyList.Sum(t => t.SeriesList.Sum(s => s.InstanceList.Sum(i => i.FileSize))) ?? 0
StudyCount = visitList.SelectMany(t => t.StudyList).Count(),
ImageCount = visitList.SelectMany(t => t.StudyList).Sum(s => s.SeriesList.Sum(s => s.InstanceList.Count())),
ImageSize = visitList.SelectMany(t => t.StudyList).Sum(t => t.SeriesList.Sum(s => s.InstanceList.Sum(i => i.FileSize))) ?? 0
};
await _subejctVisitDownloadRepository.AddAsync(preDownloadInfo, true);
return ResponseOutput.Ok(result, preDownloadInfo.Id);
return ResponseOutput.Ok(visitList, preDownloadInfo.Id);
}
/// <summary>