增加访视下载接口
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 var progress = new
{ {
CountPercent = totalCount > 0 CountPercent = totalCount > 0
? Math.Round(receivedCount * 100m / totalCount, 2).ToString() + "%" ? Math.Round(receivedCount * 100m / totalCount, 2).ToString()
: "0%", : "0",
SizePercent = totalSize > 0 SizePercent = totalSize > 0
? Math.Round(receivedSize * 100m / totalSize, 2).ToString() + "%" ? Math.Round(receivedSize * 100m / totalSize, 2).ToString()
: "0%", : "0",
Speed = (speedBps / 1024 >= 1024 Speed = (speedBps / 1024 >= 1024
? (speedBps / 1024 / 1024).ToString("0.00") + " MB/s" ? (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 class StudyDIRInfo
{ {
public Guid SubjectId { get; set; }
public Guid TrialId { get; set; }
public Guid SubjectVisitId { get; set; } public Guid SubjectVisitId { get; set; }
// Study // Study
public Guid DicomStudyId { get; set; } public Guid DicomStudyId { get; set; }

View File

@ -18834,12 +18834,10 @@
<param name="inCommand"></param> <param name="inCommand"></param>
<returns></returns> <returns></returns>
</member> </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>
获取下载的访视检查信息 获取下载的访视检查信息
</summary> </summary>
<param name="trialId"></param>
<param name="subjectVisitId"></param>
<returns></returns> <returns></returns>
</member> </member>
<member name="M:IRaCIS.Application.Services.PatientService.DownloadImageSuccess(System.Guid)"> <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 class PatientImageDownloadCommand
{ {
public List<Guid> PatientIdList { get; set; } 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 class DownloadPatientDto
{ {
public string PatientName { get; set; } public string PatientName { get; set; }

View File

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