增加type区分不同的功能

This commit is contained in:
ztx
2024-12-20 22:59:24 +08:00
parent c6f6f775ba
commit 1cd079307d
7 changed files with 196 additions and 131 deletions

View File

@@ -20,5 +20,15 @@ public class RemoteNode
public string AeTitle { get; set; } = string.Empty;
public string HostName { get; set; } = "localhost";
public int Port { get; set; } = 104;
public bool IsDefault { get; set; }
public string Type { get; set; } = "all";
public bool SupportsStore()
{
return Type.ToLower() is "store" or "all";
}
public bool SupportsQueryRetrieve()
{
return Type.ToLower() is "qr" or "all";
}
}

View File

@@ -198,7 +198,9 @@ public class QueryRetrieveController : ControllerBase
[HttpGet("nodes")]
public ActionResult<IEnumerable<RemoteNode>> GetNodes()
{
return Ok(_config.RemoteNodes);
// 只返回支持查询检索的节点
var qrNodes = _config.RemoteNodes.Where(n => n.SupportsQueryRetrieve());
return Ok(qrNodes);
}
// 统一的查询接口
@@ -214,6 +216,12 @@ public class QueryRetrieveController : ControllerBase
return NotFound($"未找到节点: {nodeId}");
}
// 验证节点是否支持查询检索
if (!node.SupportsQueryRetrieve())
{
return BadRequest($"节点 {nodeId} 不支持查询检索操作");
}
// 解析查询级别
if (!Enum.TryParse<DicomQueryRetrieveLevel>(level, true, out var queryLevel))
{
@@ -364,127 +372,145 @@ public class QueryRetrieveController : ControllerBase
[FromQuery] string level,
[FromBody] MoveRequest moveRequest)
{
var node = _config.RemoteNodes.FirstOrDefault(n => n.Name == nodeId);
if (node == null)
{
return NotFound($"未找到节点: {nodeId}");
}
// 解析级别
if (!Enum.TryParse<DicomQueryRetrieveLevel>(level, true, out var queryLevel))
{
return BadRequest(new MoveResponse
{
Success = false,
Message = $"无效的获取级别: {level}。有效值为: Patient, Study, Series, Image"
});
}
// 验证请求参数
var (isValid, errorMessage) = ValidateMoveRequest(queryLevel, moveRequest);
if (!isValid)
{
return BadRequest(new MoveResponse
{
Success = false,
Message = errorMessage
});
}
try
{
// 构建数据集
var dataset = new DicomDataset();
dataset.Add(DicomTag.QueryRetrieveLevel, queryLevel.ToString().ToUpper());
// 根据不同级别添加必要的字段
switch (queryLevel)
var node = _config.RemoteNodes.FirstOrDefault(n => n.Name == nodeId);
if (node == null)
{
case DicomQueryRetrieveLevel.Patient:
dataset.Add(DicomTag.PatientID, moveRequest.PatientId);
break;
case DicomQueryRetrieveLevel.Study:
dataset.Add(DicomTag.StudyInstanceUID, moveRequest.StudyInstanceUid);
break;
case DicomQueryRetrieveLevel.Series:
dataset.Add(DicomTag.StudyInstanceUID, moveRequest.StudyInstanceUid);
dataset.Add(DicomTag.SeriesInstanceUID, moveRequest.SeriesInstanceUid);
break;
case DicomQueryRetrieveLevel.Image:
dataset.Add(DicomTag.StudyInstanceUID, moveRequest.StudyInstanceUid);
dataset.Add(DicomTag.SeriesInstanceUID, moveRequest.SeriesInstanceUid);
dataset.Add(DicomTag.SOPInstanceUID, moveRequest.SopInstanceUid);
break;
return NotFound($"未找到节点: {nodeId}");
}
DicomLogger.Debug(LogPrefix, "Move请求数据集: {0}", dataset.ToString());
// 解析传输语法
string? transferSyntax = null;
if (!string.IsNullOrEmpty(moveRequest.TransferSyntax))
// 验证节点是否支持查询检索
if (!node.SupportsQueryRetrieve())
{
try
{
var syntaxType = DicomTransferSyntaxParser.Parse(moveRequest.TransferSyntax);
if (syntaxType.HasValue)
{
transferSyntax = syntaxType.Value.GetUID();
DicomLogger.Debug(LogPrefix,
"使用指定的传输语法: {0} ({1}) [输入: {2}]",
syntaxType.Value.GetDescription(),
transferSyntax,
moveRequest.TransferSyntax);
}
}
catch (ArgumentException ex)
{
return BadRequest(new MoveResponse
{
Success = false,
Message = ex.Message
});
}
return BadRequest($"节点 {nodeId} 不支持查询检索操作");
}
// 直接使用本地 AE Title并传入传输语法参数
var success = await _queryRetrieveScu.MoveAsync(
node,
queryLevel,
dataset,
_settings.AeTitle,
transferSyntax);
if (!success)
// 解析级别
if (!Enum.TryParse<DicomQueryRetrieveLevel>(level, true, out var queryLevel))
{
// 根据不同情况返回不同的错误信息
return StatusCode(500, new MoveResponse
return BadRequest(new MoveResponse
{
Success = false,
Message = queryLevel == DicomQueryRetrieveLevel.Patient ?
"Patient级别获取未返回任何影像该级别可能不被支持请尝试使用Study级别获取" :
"获取请求被拒绝"
Message = $"无效的获取级别: {level}。有效值为: Patient, Study, Series, Image"
});
}
return Ok(new MoveResponse
// 验证请求参数
var (isValid, errorMessage) = ValidateMoveRequest(queryLevel, moveRequest);
if (!isValid)
{
Success = true,
Message = queryLevel == DicomQueryRetrieveLevel.Patient ?
"Patient级别获取请求已发送如果支持此级别操作稍后可在影像管理中查看" :
"获取请求已发送,请稍后在影像管理中查看",
JobId = Guid.NewGuid().ToString()
});
return BadRequest(new MoveResponse
{
Success = false,
Message = errorMessage
});
}
try
{
// 构建数据集
var dataset = new DicomDataset();
dataset.Add(DicomTag.QueryRetrieveLevel, queryLevel.ToString().ToUpper());
// 根据不同级别添加必要的字段
switch (queryLevel)
{
case DicomQueryRetrieveLevel.Patient:
dataset.Add(DicomTag.PatientID, moveRequest.PatientId);
break;
case DicomQueryRetrieveLevel.Study:
dataset.Add(DicomTag.StudyInstanceUID, moveRequest.StudyInstanceUid);
break;
case DicomQueryRetrieveLevel.Series:
dataset.Add(DicomTag.StudyInstanceUID, moveRequest.StudyInstanceUid);
dataset.Add(DicomTag.SeriesInstanceUID, moveRequest.SeriesInstanceUid);
break;
case DicomQueryRetrieveLevel.Image:
dataset.Add(DicomTag.StudyInstanceUID, moveRequest.StudyInstanceUid);
dataset.Add(DicomTag.SeriesInstanceUID, moveRequest.SeriesInstanceUid);
dataset.Add(DicomTag.SOPInstanceUID, moveRequest.SopInstanceUid);
break;
}
DicomLogger.Debug(LogPrefix, "Move请求数据集: {0}", dataset.ToString());
// 解析传输语法
string? transferSyntax = null;
if (!string.IsNullOrEmpty(moveRequest.TransferSyntax))
{
try
{
var syntaxType = DicomTransferSyntaxParser.Parse(moveRequest.TransferSyntax);
if (syntaxType.HasValue)
{
transferSyntax = syntaxType.Value.GetUID();
DicomLogger.Debug(LogPrefix,
"使用指定的传输语法: {0} ({1}) [输入: {2}]",
syntaxType.Value.GetDescription(),
transferSyntax,
moveRequest.TransferSyntax);
}
}
catch (ArgumentException ex)
{
return BadRequest(new MoveResponse
{
Success = false,
Message = ex.Message
});
}
}
// 直接使用本地 AE Title并传入传输语法参数
var success = await _queryRetrieveScu.MoveAsync(
node,
queryLevel,
dataset,
_settings.AeTitle,
transferSyntax);
if (!success)
{
// 根据不同情况返回不同的错误信息
return StatusCode(500, new MoveResponse
{
Success = false,
Message = queryLevel == DicomQueryRetrieveLevel.Patient ?
"Patient级别获取未返回任何影像该级别可能不被支持请尝试使用Study级别获取" :
"获取请求被拒绝"
});
}
return Ok(new MoveResponse
{
Success = true,
Message = queryLevel == DicomQueryRetrieveLevel.Patient ?
"Patient级别获取请求已发送如果支持此级别操作稍后可在影像管理中查看" :
"获取请求已发送,请稍后在影像管理中查看",
JobId = Guid.NewGuid().ToString()
});
}
catch (Exception ex)
{
DicomLogger.Error(LogPrefix, ex, "发送{Level}获取请求失败", level);
return StatusCode(500, new MoveResponse
{
Success = false,
Message = "发送获取请求失败"
});
}
}
catch (Exception ex)
{
DicomLogger.Error(LogPrefix, ex, "发送{Level}获取请求失败", level);
DicomLogger.Error(LogPrefix, ex, "执行{Level}获取请求失败", level);
return StatusCode(500, new MoveResponse
{
Success = false,
Message = "发送获取请求失败"
Message = "执行获取请求失败"
});
}
}
@@ -784,16 +810,22 @@ public class QueryRetrieveController : ControllerBase
}
[HttpPost("{nodeId}/verify")]
public async Task<ActionResult> VerifyConnection(string nodeId)
public async Task<IActionResult> VerifyConnection(string nodeId)
{
var node = _config.RemoteNodes.FirstOrDefault(n => n.Name == nodeId);
if (node == null)
{
return NotFound($"未找到节点: {nodeId}");
}
try
{
var node = _config.RemoteNodes.FirstOrDefault(n => n.Name == nodeId);
if (node == null)
{
return NotFound($"未找到节点: {nodeId}");
}
// 验证节点是否支持查询检索
if (!node.SupportsQueryRetrieve())
{
return BadRequest($"节点 {nodeId} 不支持查询检索操作");
}
var success = await _queryRetrieveScu.VerifyConnectionAsync(node);
if (!success)

View File

@@ -27,7 +27,9 @@ public class StoreSCUController : ControllerBase
[HttpGet("nodes")]
public ActionResult<IEnumerable<RemoteNode>> GetNodes()
{
return Ok(_config.RemoteNodes);
// 只返回支持存储的节点
var storeNodes = _config.RemoteNodes.Where(n => n.SupportsStore());
return Ok(storeNodes);
}
[HttpPost("verify/{remoteName}")]
@@ -54,6 +56,18 @@ public class StoreSCUController : ControllerBase
{
try
{
var node = _config.RemoteNodes.FirstOrDefault(n => n.Name == remoteName);
if (node == null)
{
return NotFound($"未找到节点: {remoteName}");
}
// 验证节点是否支持存储
if (!node.SupportsStore())
{
return BadRequest($"节点 {remoteName} 不支持存储操作");
}
if (!string.IsNullOrEmpty(folderPath))
{
DicomLogger.Information("Api", "开始发送文件夹 - 路径: {FolderPath}, 目标: {RemoteName}",
@@ -121,6 +135,18 @@ public class StoreSCUController : ControllerBase
{
try
{
var node = _config.RemoteNodes.FirstOrDefault(n => n.Name == remoteName);
if (node == null)
{
return NotFound($"未找到节点: {remoteName}");
}
// 验证节点是否支持存储
if (!node.SupportsStore())
{
return BadRequest($"节点 {remoteName} 不支持存储操作");
}
// 获取研究相关的所有文件路径
var repository = HttpContext.RequestServices.GetRequiredService<DicomRepository>();
var instances = repository.GetInstancesByStudyUid(studyInstanceUid);

View File

@@ -1,8 +1,5 @@
using System.Text.Json.Serialization;
using DicomSCP.Configuration;
using FellowOakDicom;
using System.ComponentModel.DataAnnotations;
using FellowOakDicom.Network;
namespace DicomSCP.Models;
@@ -20,8 +17,8 @@ public class DicomNodeConfig
[JsonPropertyName("port")]
public int Port { get; set; }
[JsonPropertyName("isDefault")]
public bool IsDefault { get; set; }
[JsonPropertyName("type")]
public string Type { get; set; } = "all"; // 可选值: "store", "qr", "all"
}
public class QueryRequest

View File

@@ -74,21 +74,21 @@
"AeTitle": "SERVERAE",
"HostName": "192.168.2.2",
"Port": 104,
"IsDefault": false
"Type": "all"
},
{
"Name": "自己STORESCP",
"AeTitle": "STORESCP",
"HostName": "127.0.0.1",
"Port": 11112,
"IsDefault": true
"Type": "store"
},
{
"Name": "自己QRSCP",
"AeTitle": "QRSCP",
"HostName": "127.0.0.1",
"Port": 11114,
"IsDefault": true
"Type": "qr"
}
]
},

View File

@@ -72,18 +72,19 @@ async function loadQRNodes() {
}
select.innerHTML = nodes.map(node => `
<option value="${node.name}">${node.name} (${node.aeTitle}@${node.hostName})</option>
<option value="${node.name}">
${node.name} (${node.aeTitle}@${node.hostName})
</option>
`).join('');
// 恢复之前选择的节点,如果没有则使用默认节点
// 恢复之前选择的节点,如果没有则使用第一个节点
if (selectedQRNode) {
select.value = selectedQRNode;
} else {
const defaultNode = nodes.find(n => n.isDefault);
if (defaultNode) {
select.value = defaultNode.name;
selectedQRNode = nodes[0]?.name;
if (selectedQRNode) {
select.value = selectedQRNode;
}
selectedQRNode = select.value;
}
// 监听节点选择变化

View File

@@ -394,13 +394,13 @@ function clearSelection() {
// 加载节点列表
async function loadStoreNodes() {
const select = document.getElementById('storeNode');
if (!select) return;
try {
const response = await axios.get('/api/StoreScu/nodes');
const nodes = response.data;
const select = document.getElementById('storeNode');
if (!select) return;
if (nodes.length === 0) {
select.innerHTML = '<option value="">未配置DICOM节点</option>';
return;
@@ -408,7 +408,7 @@ async function loadStoreNodes() {
// 生成节点选项
select.innerHTML = nodes.map(node => `
<option value="${node.name}" ${node.isDefault ? 'selected' : ''}>
<option value="${node.name}">
${node.name} (${node.aeTitle}@${node.hostName})
</option>
`).join('');
@@ -417,9 +417,8 @@ async function loadStoreNodes() {
if (selectedStoreNode) {
select.value = selectedStoreNode;
} else {
// 否则使用默认节点或第一个节点
const defaultNode = nodes.find(n => n.isDefault);
selectedStoreNode = defaultNode ? defaultNode.name : nodes[0]?.name;
// 否则使用第一个节点
selectedStoreNode = nodes[0]?.name;
if (selectedStoreNode) {
select.value = selectedStoreNode;
}