mirror of
https://gitee.com/fightroad/DicomSCP.git
synced 2025-12-06 10:38:57 +08:00
增加type区分不同的功能
This commit is contained in:
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
// 监听节点选择变化
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user