post 表单方式传递参数
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
parent
40fd641210
commit
87dcebd00b
|
|
@ -482,9 +482,9 @@ namespace IRaCIS.Core.SCP.Service
|
|||
|
||||
var frag = dicomFile.Dataset.GetDicomItem<DicomOtherByteFragment>(DicomTag.PixelData);
|
||||
|
||||
int fragmentCount = frag.Fragments.Count();
|
||||
int fragmentCount = frag?.Fragments?.Count()??0;
|
||||
|
||||
var originOffsetTable = frag.OffsetTable; //有可能没有表,需要自己重建
|
||||
var originOffsetTable = frag?.OffsetTable; //有可能没有表,需要自己重建
|
||||
|
||||
var bot = new List<uint>();
|
||||
|
||||
|
|
@ -572,7 +572,9 @@ namespace IRaCIS.Core.SCP.Service
|
|||
|
||||
|
||||
// 替换原 PixelData
|
||||
dicomFile.Dataset.AddOrUpdate(newFragments);
|
||||
dicomFile.Dataset.AddOrUpdate(DicomTag.PixelData, newFragments);
|
||||
|
||||
//dicomFile.Dataset.AddOrUpdate(newFragments);
|
||||
|
||||
|
||||
// 重新保存 dicom 到流
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using ExcelDataReader;
|
|||
using FellowOakDicom;
|
||||
using FellowOakDicom.Imaging;
|
||||
using FellowOakDicom.IO.Buffer;
|
||||
using Hangfire.Storage;
|
||||
using IRaCIS.Application.Contracts;
|
||||
using IRaCIS.Application.Interfaces;
|
||||
using IRaCIS.Core.API._ServiceExtensions.NewtonsoftJson;
|
||||
|
|
@ -1157,11 +1158,455 @@ namespace IRaCIS.Core.API.Controllers
|
|||
}
|
||||
|
||||
|
||||
[HttpGet("download/PatientStudyBatchDownload")]
|
||||
[HttpPost("download/PatientStudyBatchDownloadForm")]
|
||||
public async Task<IActionResult> DownloadPatientStudyBatchForm([FromServices] IPatientService _patientService, [FromServices] IOSSService _oSSService,
|
||||
[FromServices] IHubContext<DownloadHub, IDownloadClient> _downLoadHub,
|
||||
[FromForm] PatientImageDownloadCommand inCommand)
|
||||
{
|
||||
inCommand.SCPStudyIdList = inCommand.SCPStudyIdList.Where(t => t != Guid.Empty).ToList();
|
||||
var rusult = await _patientService.GetDownloadPatientStudyInfo(inCommand);
|
||||
|
||||
var patientList = rusult.Data;
|
||||
var downloadInfo = (SubejctVisitDownload)rusult.OtherData;
|
||||
|
||||
|
||||
long receivedSize = 0;
|
||||
long receivedCount = 0;
|
||||
long totalSize = downloadInfo.ImageSize;
|
||||
long totalCount = downloadInfo.ImageCount;
|
||||
long failedCount = 0;
|
||||
|
||||
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
|
||||
{
|
||||
FailedCount = failedCount,
|
||||
TotalCount = totalCount,
|
||||
TotalSize = (totalSize / 1024 / 1024).ToString("0.00") + " MB",
|
||||
|
||||
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=Image_{ExportExcelConverterDate.DateTimeInternationalToString(DateTime.Now, _userInfo.TimeZoneId)}.zip";
|
||||
Response.Headers["Cache-Control"] = "no-store";
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
//开始推送一次
|
||||
await NotifyProgressAsync(true);
|
||||
|
||||
await using var responseStream = Response.BodyWriter.AsStream();
|
||||
using var zip = new ZipArchive(responseStream, ZipArchiveMode.Create, leaveOpen: true);
|
||||
|
||||
foreach (var patient in patientList)
|
||||
{
|
||||
foreach (var study in patient.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 = $"{patient.PatientIdStr}/{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)
|
||||
{
|
||||
abortToken.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
//当前完成大小
|
||||
receivedSize = receivedSize + instance.FileSize ?? 0;
|
||||
receivedCount++;
|
||||
|
||||
|
||||
var entryPath =
|
||||
$"{patient.PatientIdStr}/{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;
|
||||
|
||||
try
|
||||
{
|
||||
//对于封装像素的文件做转换
|
||||
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));
|
||||
}
|
||||
|
||||
var frag = dicomFile.Dataset.GetDicomItem<DicomOtherByteFragment>(DicomTag.PixelData);
|
||||
|
||||
var originOffsetTable = frag?.OffsetTable;
|
||||
|
||||
newFragments.OffsetTable.AddRange(originOffsetTable?.ToArray());
|
||||
// 替换原有的片段序列
|
||||
dicomFile.Dataset.AddOrUpdate(newFragments);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
Log.Logger.Warning($"处理多帧合并{instance.Path}失败: {ex.Message}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
await dicomFile.SaveAsync(entryStream);
|
||||
//await source.CopyToAsync(entryStream, 32 * 1024, abortToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
failedCount++;
|
||||
Log.Logger.Warning($"处理文件{instance.Path}失败: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
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();
|
||||
|
||||
|
||||
}
|
||||
|
||||
[HttpPost("download/PatientStudyBatchDownloadJson")]
|
||||
public async Task<IActionResult> DownloadPatientStudyBatchJson([FromServices] IPatientService _patientService, [FromServices] IOSSService _oSSService,
|
||||
[FromServices] IHubContext<DownloadHub, IDownloadClient> _downLoadHub,
|
||||
[FromForm] string jsonDataStr)
|
||||
{
|
||||
|
||||
var inCommand = JsonConvert.DeserializeObject<PatientImageDownloadCommand>(jsonDataStr);
|
||||
|
||||
inCommand.SCPStudyIdList = inCommand.SCPStudyIdList.Where(t => t != Guid.Empty).ToList();
|
||||
var rusult = await _patientService.GetDownloadPatientStudyInfo(inCommand);
|
||||
|
||||
var patientList = rusult.Data;
|
||||
var downloadInfo = (SubejctVisitDownload)rusult.OtherData;
|
||||
|
||||
|
||||
long receivedSize = 0;
|
||||
long receivedCount = 0;
|
||||
long totalSize = downloadInfo.ImageSize;
|
||||
long totalCount = downloadInfo.ImageCount;
|
||||
long failedCount = 0;
|
||||
|
||||
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
|
||||
{
|
||||
FailedCount = failedCount,
|
||||
TotalCount = totalCount,
|
||||
TotalSize = (totalSize / 1024 / 1024).ToString("0.00") + " MB",
|
||||
|
||||
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=Image_{ExportExcelConverterDate.DateTimeInternationalToString(DateTime.Now, _userInfo.TimeZoneId)}.zip";
|
||||
Response.Headers["Cache-Control"] = "no-store";
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
//开始推送一次
|
||||
await NotifyProgressAsync(true);
|
||||
|
||||
await using var responseStream = Response.BodyWriter.AsStream();
|
||||
using var zip = new ZipArchive(responseStream, ZipArchiveMode.Create, leaveOpen: true);
|
||||
|
||||
foreach (var patient in patientList)
|
||||
{
|
||||
foreach (var study in patient.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 = $"{patient.PatientIdStr}/{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)
|
||||
{
|
||||
abortToken.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
//当前完成大小
|
||||
receivedSize = receivedSize + instance.FileSize ?? 0;
|
||||
receivedCount++;
|
||||
|
||||
|
||||
var entryPath =
|
||||
$"{patient.PatientIdStr}/{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;
|
||||
|
||||
try
|
||||
{
|
||||
//对于封装像素的文件做转换
|
||||
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));
|
||||
}
|
||||
|
||||
var frag = dicomFile.Dataset.GetDicomItem<DicomOtherByteFragment>(DicomTag.PixelData);
|
||||
|
||||
var originOffsetTable = frag?.OffsetTable;
|
||||
|
||||
newFragments.OffsetTable.AddRange(originOffsetTable?.ToArray());
|
||||
// 替换原有的片段序列
|
||||
dicomFile.Dataset.AddOrUpdate(newFragments);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
Log.Logger.Warning($"处理多帧合并{instance.Path}失败: {ex.Message}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
await dicomFile.SaveAsync(entryStream);
|
||||
//await source.CopyToAsync(entryStream, 32 * 1024, abortToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
failedCount++;
|
||||
Log.Logger.Warning($"处理文件{instance.Path}失败: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
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();
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("download/PatientStudyBatchDownload")]
|
||||
public async Task<IActionResult> DownloadPatientStudyBatch([FromServices] IPatientService _patientService, [FromServices] IOSSService _oSSService,
|
||||
[FromServices] IHubContext<DownloadHub, IDownloadClient> _downLoadHub,
|
||||
[FromQuery] PatientImageDownloadCommand inCommand)
|
||||
PatientImageDownloadCommand inCommand)
|
||||
{
|
||||
Console.WriteLine(inCommand.ToJsonStr());
|
||||
|
||||
inCommand.SCPStudyIdList = inCommand.SCPStudyIdList.Where(t => t != Guid.Empty).ToList();
|
||||
var rusult = await _patientService.GetDownloadPatientStudyInfo(inCommand);
|
||||
|
||||
|
|
|
|||
|
|
@ -991,7 +991,7 @@ namespace IRaCIS.Application.Contracts
|
|||
public List<Guid> SCPStudyIdList { get; set; } = new List<Guid>();
|
||||
|
||||
[NotDefault]
|
||||
public string CurrentNoticeId { get; set; }
|
||||
public string CurrentNoticeId { get; set; } = "11";
|
||||
}
|
||||
|
||||
public class VisitImageDownloadQuery : PageInput
|
||||
|
|
|
|||
Loading…
Reference in New Issue