Browse Source

fix: 修复任务执行系统关键问题并优化架构

- 修复 NodeExecutionEventRouter 死循环问题
- 重构 InitialNodeInfo 支持多流程场景,优化数据库查询性能
- 统一 TaskExecutionController 路由设计
- 提升安全性:移除 ExecutorId 参数,使用用户上下文
- 性能优化:查询次数从 O(n) 降低到 O(1)
refactor/permission-config
root 3 months ago
parent
commit
4eb0800db4
  1. 147
      src/X1.Application/ApplicationServices/TaskExecutionService.cs
  2. 5
      src/X1.Application/Features/TaskExecution/Commands/StartTaskExecution/StartTaskExecutionCommand.cs
  3. 70
      src/X1.Application/Features/TaskExecution/Commands/StartTaskExecution/StartTaskExecutionCommandHandler.cs
  4. 6
      src/X1.Application/Features/TaskExecution/Commands/StartTaskExecution/StartTaskExecutionCommandValidator.cs
  5. 6
      src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/DisableFlightModeControllerHandler.cs
  6. 6
      src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/EnableFlightModeControllerHandler.cs
  7. 6
      src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/EndFlowControllerHandler.cs
  8. 6
      src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/ImsiRegistrationControllerHandler.cs
  9. 6
      src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/StartFlowControllerHandler.cs
  10. 22
      src/X1.Application/Features/TaskExecution/Events/EventHandlers/NodeExecutionEventRouter.cs
  11. 66
      src/X1.Application/Features/TaskExecution/Events/NodeExecutionEvents/ControllerExecutionEvent.cs
  12. 8
      src/X1.Domain/Repositories/TestCase/ITestCaseFlowRepository.cs
  13. 8
      src/X1.Domain/Repositories/TestCase/ITestCaseNodeRepository.cs
  14. 40
      src/X1.Domain/Services/ITaskExecutionService.cs
  15. 15
      src/X1.Infrastructure/Repositories/TestCase/TestCaseFlowRepository.cs
  16. 24
      src/X1.Infrastructure/Repositories/TestCase/TestCaseNodeRepository.cs
  17. 9
      src/X1.Presentation/Controllers/TaskExecutionController.cs
  18. 3
      src/X1.WebUI/src/constants/api.ts
  19. 4
      src/X1.WebUI/src/lib/http-client.ts
  20. 59
      src/X1.WebUI/src/pages/taskExecution/TaskExecutionView.tsx
  21. 88
      src/X1.WebUI/src/services/taskExecutionService.ts
  22. 316
      src/modify_20250121_initialnodeinfo_refactor.md
  23. 90
      src/modify_20250121_router_deadloop_fix.md
  24. 152
      src/modify_20250121_starttaskexecution_optimization.md
  25. 88
      src/modify_20250121_taskexecution_completion.md
  26. 150
      src/modify_20250121_taskexecution_controller_route_fix.md
  27. 186
      src/modify_20250121_taskexecution_service_architecture_fix.md

147
src/X1.Application/ApplicationServices/TaskExecutionService.cs

@ -4,6 +4,7 @@ using X1.Domain.Entities.TestCase;
using X1.Domain.Repositories.TestTask;
using X1.Domain.Repositories.TestCase;
using X1.Domain.Services;
using X1.Domain.Repositories.Base;
namespace X1.Application.ApplicationServices;
@ -20,7 +21,7 @@ public class TaskExecutionService : ITaskExecutionService
private readonly IScenarioTestCaseRepository _scenarioTestCaseRepository;
private readonly ITestCaseFlowRepository _testCaseFlowRepository;
private readonly ITestCaseNodeRepository _testCaseNodeRepository;
private readonly IUnitOfWork _unitOfWork;
public TaskExecutionService(
ILogger<TaskExecutionService> logger,
@ -29,7 +30,8 @@ public class TaskExecutionService : ITaskExecutionService
ITestScenarioRepository testScenarioRepository,
IScenarioTestCaseRepository scenarioTestCaseRepository,
ITestCaseFlowRepository testCaseFlowRepository,
ITestCaseNodeRepository testCaseNodeRepository)
ITestCaseNodeRepository testCaseNodeRepository,
IUnitOfWork unitOfWork)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_taskExecutionRepository = taskExecutionRepository ?? throw new ArgumentNullException(nameof(taskExecutionRepository));
@ -38,6 +40,7 @@ public class TaskExecutionService : ITaskExecutionService
_scenarioTestCaseRepository = scenarioTestCaseRepository ?? throw new ArgumentNullException(nameof(scenarioTestCaseRepository));
_testCaseFlowRepository = testCaseFlowRepository ?? throw new ArgumentNullException(nameof(testCaseFlowRepository));
_testCaseNodeRepository = testCaseNodeRepository ?? throw new ArgumentNullException(nameof(testCaseNodeRepository));
_unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork));
}
/// <summary>
@ -74,8 +77,8 @@ public class TaskExecutionService : ITaskExecutionService
);
await _taskExecutionRepository.AddAsync(taskExecution, cancellationToken);
_logger.LogInformation("任务执行记录创建成功,执行ID: {TaskExecutionId}", taskExecution.Id);
_logger.LogInformation("任务执行记录创建成功,执行ID: {TaskExecutionId}", taskExecution.Id);
return taskExecution;
}
@ -148,52 +151,69 @@ public class TaskExecutionService : ITaskExecutionService
return null;
}
// 3. 根据场景ID获取场景测试用例(按执行顺序排序)
var scenarioTestCases = await _scenarioTestCaseRepository.GetByScenarioIdAsync(scenario.Id, cancellationToken);
var firstTestCase = scenarioTestCases
.Where(stc => stc.IsEnabled)
.OrderBy(stc => stc.ExecutionOrder)
.FirstOrDefault();
// 3. 根据场景ID获取启用的场景测试用例(按执行顺序排序)
var scenarioTestCases = await _scenarioTestCaseRepository.GetEnabledByScenarioIdAsync(scenario.Id, cancellationToken);
if (firstTestCase == null)
if (!scenarioTestCases.Any())
{
_logger.LogWarning("场景中没有启用的测试用例,场景编码: {ScenarioCode}", task.ScenarioCode);
return null;
}
// 4. 创建初始节点信息
var initialNodeInfo = new InitialNodeInfo
{
TaskId = taskId,
ScenarioId = scenario.Id,
ScenarioCode = task.ScenarioCode,
InitialNodes = new List<InitialNodeItem>()
};
// 4. 获取测试用例流程
var testCaseFlow = await _testCaseFlowRepository.GetTestCaseFlowByIdAsync(firstTestCase.TestCaseFlowId, cancellationToken);
if (testCaseFlow == null)
// 5. 批量获取测试用例流程和第一个节点
var testCaseFlowIds = scenarioTestCases.Select(stc => stc.TestCaseFlowId).ToList();
// 批量获取测试用例流程
var testCaseFlows = await _testCaseFlowRepository.GetByIdsAsync(testCaseFlowIds, cancellationToken);
var testCaseFlowDict = testCaseFlows.ToDictionary(tcf => tcf.Id, tcf => tcf);
// 批量获取每个流程的起始节点(StepType = Start)
var startNodes = await _testCaseNodeRepository.GetStartNodesByTestCaseIdsAsync(testCaseFlowIds, cancellationToken);
var startNodeDict = startNodes.ToDictionary(sn => sn.TestCaseId, sn => sn);
// 6. 构建初始节点信息
foreach (var scenarioTestCase in scenarioTestCases)
{
_logger.LogWarning("测试用例流程不存在,流程ID: {TestCaseFlowId}", firstTestCase.TestCaseFlowId);
return null;
// 获取测试用例流程
if (!testCaseFlowDict.TryGetValue(scenarioTestCase.TestCaseFlowId, out var testCaseFlow))
{
_logger.LogWarning("测试用例流程不存在,流程ID: {TestCaseFlowId}", scenarioTestCase.TestCaseFlowId);
continue;
}
// 获取流程中的起始节点(StepType = Start)
if (!startNodeDict.TryGetValue(scenarioTestCase.TestCaseFlowId, out var startNode))
{
_logger.LogWarning("测试用例流程中没有起始节点(StepType = Start),流程ID: {TestCaseFlowId}", scenarioTestCase.TestCaseFlowId);
continue;
}
// 添加到初始节点集合
initialNodeInfo.InitialNodes.Add(new InitialNodeItem
{
NodeId = startNode.NodeId,
StepMapping = startNode.StepConfig.Mapping,
FlowId = testCaseFlow.Id,
FlowName = testCaseFlow.Name
});
}
// 5. 获取流程中的第一个节点(按序号排序)
var nodes = await _testCaseNodeRepository.GetByTestCaseIdOrderedAsync(firstTestCase.TestCaseFlowId, cancellationToken);
var firstNode = nodes.FirstOrDefault();
if (firstNode == null)
if (!initialNodeInfo.InitialNodes.Any())
{
_logger.LogWarning("测试用例流程中没有节点,流程ID: {TestCaseFlowId}", firstTestCase.TestCaseFlowId);
_logger.LogWarning("没有找到任何有效的初始节点,任务ID: {TaskId}", taskId);
return null;
}
// 6. 获取步骤映射类型
var stepMapping = GetStepMappingFromStepId(firstNode.StepId);
var initialNodeInfo = new InitialNodeInfo
{
NodeId = firstNode.NodeId,
StepMapping = stepMapping,
ScenarioId = scenario.Id,
ScenarioCode = task.ScenarioCode,
FlowId = testCaseFlow.Id,
FlowName = testCaseFlow.Name
};
_logger.LogInformation("成功获取初始节点,节点ID: {NodeId}, 步骤映射: {StepMapping}",
initialNodeInfo.NodeId, initialNodeInfo.StepMapping);
_logger.LogInformation("成功获取初始节点信息,任务ID: {TaskId}, 节点数量: {NodeCount}",
taskId, initialNodeInfo.InitialNodes.Count);
return initialNodeInfo;
}
@ -203,57 +223,4 @@ public class TaskExecutionService : ITaskExecutionService
return null;
}
}
/// <summary>
/// 根据步骤ID获取步骤映射类型
/// 这里需要根据实际的步骤配置表来实现
/// </summary>
/// <param name="stepId">步骤ID</param>
/// <returns>步骤映射类型</returns>
private StepMapping GetStepMappingFromStepId(string stepId)
{
// TODO: 这里需要根据实际的步骤配置表来实现
// 暂时返回 StartFlow 作为默认值
return StepMapping.StartFlow;
}
/// <summary>
/// 更新任务执行的运行时编码
/// 在 StartFlowControllerHandler 执行成功后调用,设置正式的 RuntimeCode
/// </summary>
/// <param name="taskExecutionId">任务执行ID</param>
/// <param name="newRuntimeCode">新的运行时编码</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>更新是否成功</returns>
public async Task<bool> UpdateRuntimeCodeAsync(string taskExecutionId, string newRuntimeCode, CancellationToken cancellationToken = default)
{
_logger.LogInformation("开始更新任务执行的运行时编码,执行ID: {TaskExecutionId}, 新编码: {NewRuntimeCode}",
taskExecutionId, newRuntimeCode);
try
{
var taskExecution = await _taskExecutionRepository.GetByIdAsync(taskExecutionId, null, cancellationToken);
if (taskExecution == null)
{
_logger.LogWarning("任务执行记录不存在,执行ID: {TaskExecutionId}", taskExecutionId);
return false;
}
// 更新运行时编码
taskExecution.RuntimeCode = newRuntimeCode;
_taskExecutionRepository.Update(taskExecution);
_logger.LogInformation("任务执行运行时编码更新成功,执行ID: {TaskExecutionId}, 新编码: {NewRuntimeCode}",
taskExecutionId, newRuntimeCode);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "更新任务执行运行时编码失败,执行ID: {TaskExecutionId}, 新编码: {NewRuntimeCode}",
taskExecutionId, newRuntimeCode);
return false;
}
}
}

5
src/X1.Application/Features/TaskExecution/Commands/StartTaskExecution/StartTaskExecutionCommand.cs

@ -12,9 +12,4 @@ public class StartTaskExecutionCommand : IRequest<OperationResult<StartTaskExecu
/// 任务ID
/// </summary>
public string TaskId { get; set; } = null!;
/// <summary>
/// 执行人ID
/// </summary>
public string ExecutorId { get; set; } = null!;
}

70
src/X1.Application/Features/TaskExecution/Commands/StartTaskExecution/StartTaskExecutionCommandHandler.cs

@ -16,17 +16,20 @@ public class StartTaskExecutionCommandHandler : IRequestHandler<StartTaskExecuti
private readonly ITaskExecutionService _taskExecutionService;
private readonly IUnitOfWork _unitOfWork;
private readonly IMediator _mediator;
private readonly ICurrentUserService _currentUserService;
public StartTaskExecutionCommandHandler(
ILogger<StartTaskExecutionCommandHandler> logger,
ITaskExecutionService taskExecutionService,
IUnitOfWork unitOfWork,
IMediator mediator)
IMediator mediator,
ICurrentUserService currentUserService)
{
_logger = logger;
_taskExecutionService = taskExecutionService;
_unitOfWork = unitOfWork;
_mediator = mediator;
_currentUserService = currentUserService;
}
/// <summary>
@ -39,42 +42,57 @@ public class StartTaskExecutionCommandHandler : IRequestHandler<StartTaskExecuti
{
try
{
// 获取当前用户ID
var currentUserId = _currentUserService.GetCurrentUserId();
if (string.IsNullOrEmpty(currentUserId))
{
_logger.LogWarning("无法获取当前用户ID,任务ID: {TaskId}", request.TaskId);
return OperationResult<StartTaskExecutionResponse>.CreateFailure("无法获取当前用户信息,请重新登录");
}
_logger.LogInformation("开始处理启动任务执行命令,任务ID: {TaskId}, 执行人ID: {ExecutorId}",
request.TaskId, request.ExecutorId);
request.TaskId, currentUserId);
// 调用任务执行服务启动任务
var taskExecution = await _taskExecutionService.StartTaskExecutionAsync(
request.TaskId,
request.ExecutorId,
currentUserId,
cancellationToken);
_logger.LogInformation("任务执行启动成功,执行ID: {TaskExecutionId}, 任务ID: {TaskId}",
taskExecution.Id, request.TaskId);
// 发布 NodeExecutionStartedEvent 来启动流程
var initialNode = await _taskExecutionService.GetInitialNodeAsync(request.TaskId, cancellationToken);
if (initialNode != null)
var initialNodeInfo = await _taskExecutionService.GetInitialNodeAsync(request.TaskId, cancellationToken);
if (initialNodeInfo != null && initialNodeInfo.InitialNodes.Any())
{
var nodeExecutionStartedEvent = new NodeExecutionStartedEvent
// 为每个初始节点发布事件
foreach (var initialNode in initialNodeInfo.InitialNodes)
{
EventId = Guid.NewGuid().ToString(),
TaskExecutionId = taskExecution.Id,
NodeId = initialNode.NodeId,
StepMapping = initialNode.StepMapping,
ExecutorId = request.ExecutorId,
RuntimeCode = taskExecution.RuntimeCode ?? string.Empty, // 可能为 null,在 StartFlowControllerHandler 中生成
ScenarioCode = initialNode.ScenarioCode,
ScenarioId = initialNode.ScenarioId,
FlowName = initialNode.FlowName,
FlowId = initialNode.FlowId,
Timestamp = DateTime.UtcNow
};
var nodeExecutionStartedEvent = new NodeExecutionStartedEvent
{
EventId = Guid.NewGuid().ToString(),
TaskExecutionId = taskExecution.Id,
NodeId = initialNode.NodeId,
StepMapping = initialNode.StepMapping,
ExecutorId = currentUserId,
RuntimeCode = taskExecution.RuntimeCode ?? string.Empty, // 可能为 null,在 StartFlowControllerHandler 中生成
ScenarioCode = initialNodeInfo.ScenarioCode,
ScenarioId = initialNodeInfo.ScenarioId,
FlowName = initialNode.FlowName,
FlowId = initialNode.FlowId,
Timestamp = DateTime.UtcNow
};
// 异步发布事件,不等待执行完成(fire and forget)
_ = _mediator.Publish(nodeExecutionStartedEvent, cancellationToken);
// 异步发布事件,不等待执行完成(fire and forget)
await _mediator.Publish(nodeExecutionStartedEvent, cancellationToken);
_logger.LogInformation("已发布 NodeExecutionStartedEvent,事件ID: {EventId}, 节点ID: {NodeId}, 流程: {FlowName}",
nodeExecutionStartedEvent.EventId, nodeExecutionStartedEvent.NodeId, initialNode.FlowName);
}
_logger.LogInformation("已发布 NodeExecutionStartedEvent,事件ID: {EventId}, 节点ID: {NodeId}",
nodeExecutionStartedEvent.EventId, nodeExecutionStartedEvent.NodeId);
_logger.LogInformation("已为 {NodeCount} 个初始节点发布事件,任务ID: {TaskId}",
initialNodeInfo.InitialNodes.Count, request.TaskId);
}
else
{
@ -85,19 +103,17 @@ public class StartTaskExecutionCommandHandler : IRequestHandler<StartTaskExecuti
{
TaskExecution = taskExecution
};
await _unitOfWork.SaveChangesAsync();
return OperationResult<StartTaskExecutionResponse>.CreateSuccess(response);
}
catch (InvalidOperationException ex)
{
_logger.LogWarning(ex, "启动任务执行失败,任务ID: {TaskId}, 执行人ID: {ExecutorId}",
request.TaskId, request.ExecutorId);
_logger.LogWarning(ex, "启动任务执行失败,任务ID: {TaskId}", request.TaskId);
return OperationResult<StartTaskExecutionResponse>.CreateFailure(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "启动任务执行时发生未预期的错误,任务ID: {TaskId}, 执行人ID: {ExecutorId}",
request.TaskId, request.ExecutorId);
_logger.LogError(ex, "启动任务执行时发生未预期的错误,任务ID: {TaskId}", request.TaskId);
return OperationResult<StartTaskExecutionResponse>.CreateFailure("启动任务执行失败,请稍后重试");
}
}

6
src/X1.Application/Features/TaskExecution/Commands/StartTaskExecution/StartTaskExecutionCommandValidator.cs

@ -14,11 +14,5 @@ public class StartTaskExecutionCommandValidator : AbstractValidator<StartTaskExe
.WithMessage("任务ID不能为空")
.MaximumLength(50)
.WithMessage("任务ID长度不能超过50个字符");
RuleFor(x => x.ExecutorId)
.NotEmpty()
.WithMessage("执行人ID不能为空")
.MaximumLength(50)
.WithMessage("执行人ID长度不能超过50个字符");
}
}

6
src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/DisableFlightModeControllerHandler.cs

@ -11,7 +11,7 @@ namespace X1.Application.Features.TaskExecution.Events.ControllerHandlers;
/// 关闭飞行模式控制器处理器
/// 处理关闭飞行模式相关的节点执行事件
/// </summary>
public class DisableFlightModeControllerHandler : INodeExecutionHandlerBase<NodeExecutionStartedEvent>
public class DisableFlightModeControllerHandler : INodeExecutionHandlerBase<ControllerExecutionEvent>
{
/// <summary>
/// 初始化关闭飞行模式控制器处理器
@ -29,7 +29,7 @@ public class DisableFlightModeControllerHandler : INodeExecutionHandlerBase<Node
/// <param name="notification">节点执行事件</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>处理任务</returns>
public override async Task HandleAsync(NodeExecutionStartedEvent notification, CancellationToken cancellationToken)
public override async Task HandleAsync(ControllerExecutionEvent notification, CancellationToken cancellationToken)
{
_logger.LogInformation("开始执行关闭飞行模式,任务执行ID: {TaskExecutionId}, 节点ID: {NodeId}, 运行时编码: {RuntimeCode}",
notification.TaskExecutionId, notification.NodeId, notification.RuntimeCode);
@ -97,7 +97,7 @@ public class DisableFlightModeControllerHandler : INodeExecutionHandlerBase<Node
/// <param name="notification">事件通知</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>执行结果</returns>
private async Task<string> ExecuteDisableFlightModeAsync(NodeExecutionStartedEvent notification, CancellationToken cancellationToken)
private async Task<string> ExecuteDisableFlightModeAsync(ControllerExecutionEvent notification, CancellationToken cancellationToken)
{
// TODO: 实现具体的关闭飞行模式逻辑
// 这里应该调用实际的设备控制服务

6
src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/EnableFlightModeControllerHandler.cs

@ -11,7 +11,7 @@ namespace X1.Application.Features.TaskExecution.Events.ControllerHandlers;
/// 开启飞行模式控制器处理器
/// 处理开启飞行模式相关的节点执行事件
/// </summary>
public class EnableFlightModeControllerHandler : INodeExecutionHandlerBase<NodeExecutionStartedEvent>
public class EnableFlightModeControllerHandler : INodeExecutionHandlerBase<ControllerExecutionEvent>
{
/// <summary>
/// 初始化开启飞行模式控制器处理器
@ -29,7 +29,7 @@ public class EnableFlightModeControllerHandler : INodeExecutionHandlerBase<NodeE
/// <param name="notification">节点执行事件</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>处理任务</returns>
public override async Task HandleAsync(NodeExecutionStartedEvent notification, CancellationToken cancellationToken)
public override async Task HandleAsync(ControllerExecutionEvent notification, CancellationToken cancellationToken)
{
_logger.LogInformation("开始执行开启飞行模式,任务执行ID: {TaskExecutionId}, 节点ID: {NodeId}, 运行时编码: {RuntimeCode}",
notification.TaskExecutionId, notification.NodeId, notification.RuntimeCode);
@ -97,7 +97,7 @@ public class EnableFlightModeControllerHandler : INodeExecutionHandlerBase<NodeE
/// <param name="notification">事件通知</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>执行结果</returns>
private async Task<string> ExecuteEnableFlightModeAsync(NodeExecutionStartedEvent notification, CancellationToken cancellationToken)
private async Task<string> ExecuteEnableFlightModeAsync(ControllerExecutionEvent notification, CancellationToken cancellationToken)
{
// TODO: 实现具体的开启飞行模式逻辑
// 这里应该调用实际的设备控制服务

6
src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/EndFlowControllerHandler.cs

@ -11,7 +11,7 @@ namespace X1.Application.Features.TaskExecution.Events.ControllerHandlers;
/// 结束流程控制器处理器
/// 处理测试流程结束相关的节点执行事件
/// </summary>
public class EndFlowControllerHandler : INodeExecutionHandlerBase<NodeExecutionStartedEvent>
public class EndFlowControllerHandler : INodeExecutionHandlerBase<ControllerExecutionEvent>
{
/// <summary>
/// 初始化结束流程控制器处理器
@ -29,7 +29,7 @@ public class EndFlowControllerHandler : INodeExecutionHandlerBase<NodeExecutionS
/// <param name="notification">节点执行事件</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>处理任务</returns>
public override async Task HandleAsync(NodeExecutionStartedEvent notification, CancellationToken cancellationToken)
public override async Task HandleAsync(ControllerExecutionEvent notification, CancellationToken cancellationToken)
{
_logger.LogInformation("开始执行结束流程,任务执行ID: {TaskExecutionId}, 节点ID: {NodeId}, 运行时编码: {RuntimeCode}",
notification.TaskExecutionId, notification.NodeId, notification.RuntimeCode);
@ -97,7 +97,7 @@ public class EndFlowControllerHandler : INodeExecutionHandlerBase<NodeExecutionS
/// <param name="notification">事件通知</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>执行结果</returns>
private async Task<string> ExecuteEndFlowAsync(NodeExecutionStartedEvent notification, CancellationToken cancellationToken)
private async Task<string> ExecuteEndFlowAsync(ControllerExecutionEvent notification, CancellationToken cancellationToken)
{
// TODO: 实现具体的结束流程逻辑
// 这里应该调用实际的结束流程服务

6
src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/ImsiRegistrationControllerHandler.cs

@ -11,7 +11,7 @@ namespace X1.Application.Features.TaskExecution.Events.ControllerHandlers;
/// IMSI注册控制器处理器
/// 处理IMSI注册相关的节点执行事件
/// </summary>
public class ImsiRegistrationControllerHandler : INodeExecutionHandlerBase<NodeExecutionStartedEvent>
public class ImsiRegistrationControllerHandler : INodeExecutionHandlerBase<ControllerExecutionEvent>
{
/// <summary>
/// 初始化IMSI注册控制器处理器
@ -29,7 +29,7 @@ public class ImsiRegistrationControllerHandler : INodeExecutionHandlerBase<NodeE
/// <param name="notification">节点执行事件</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>处理任务</returns>
public override async Task HandleAsync(NodeExecutionStartedEvent notification, CancellationToken cancellationToken)
public override async Task HandleAsync(ControllerExecutionEvent notification, CancellationToken cancellationToken)
{
_logger.LogInformation("开始执行IMSI注册,任务执行ID: {TaskExecutionId}, 节点ID: {NodeId}, 运行时编码: {RuntimeCode}",
notification.TaskExecutionId, notification.NodeId, notification.RuntimeCode);
@ -97,7 +97,7 @@ public class ImsiRegistrationControllerHandler : INodeExecutionHandlerBase<NodeE
/// <param name="notification">事件通知</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>执行结果</returns>
private async Task<string> ExecuteImsiRegistrationAsync(NodeExecutionStartedEvent notification, CancellationToken cancellationToken)
private async Task<string> ExecuteImsiRegistrationAsync(ControllerExecutionEvent notification, CancellationToken cancellationToken)
{
// TODO: 实现具体的IMSI注册逻辑
// 这里应该调用实际的IMSI注册服务

6
src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/StartFlowControllerHandler.cs

@ -11,7 +11,7 @@ namespace X1.Application.Features.TaskExecution.Events.ControllerHandlers;
/// 启动流程控制器处理器
/// 处理测试流程启动相关的节点执行事件
/// </summary>
public class StartFlowControllerHandler : INodeExecutionHandlerBase<NodeExecutionStartedEvent>
public class StartFlowControllerHandler : INodeExecutionHandlerBase<ControllerExecutionEvent>
{
/// <summary>
/// 初始化启动流程控制器处理器
@ -29,7 +29,7 @@ public class StartFlowControllerHandler : INodeExecutionHandlerBase<NodeExecutio
/// <param name="notification">节点执行事件</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>处理任务</returns>
public override async Task HandleAsync(NodeExecutionStartedEvent notification, CancellationToken cancellationToken)
public override async Task HandleAsync(ControllerExecutionEvent notification, CancellationToken cancellationToken)
{
_logger.LogInformation("开始执行启动流程,任务执行ID: {TaskExecutionId}, 节点ID: {NodeId}, 运行时编码: {RuntimeCode}",
notification.TaskExecutionId, notification.NodeId, notification.RuntimeCode);
@ -97,7 +97,7 @@ public class StartFlowControllerHandler : INodeExecutionHandlerBase<NodeExecutio
/// <param name="notification">事件通知</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>执行结果</returns>
private async Task<string> ExecuteStartFlowAsync(NodeExecutionStartedEvent notification, CancellationToken cancellationToken)
private async Task<string> ExecuteStartFlowAsync(ControllerExecutionEvent notification, CancellationToken cancellationToken)
{
// TODO: 实现具体的启动流程逻辑
// 这里应该调用实际的启动流程服务

22
src/X1.Application/Features/TaskExecution/Events/EventHandlers/NodeExecutionEventRouter.cs

@ -50,10 +50,24 @@ public class NodeExecutionEventRouter : INotificationHandler<NodeExecutionStarte
_logger.LogInformation("找到对应的处理器,步骤映射: {StepMapping}, 处理器类型: {HandlerType}",
notification.StepMapping, handlerType.Name);
// 直接重新发布事件,让 MediatR 自动路由到对应的处理器
// 由于所有 ControllerHandler 都实现了 INotificationHandler<NodeExecutionStartedEvent>
// MediatR 会自动调用所有匹配的处理器
await _mediator.Publish(notification, cancellationToken);
// 创建专门的控制器执行事件,避免死循环
var controllerEvent = new ControllerExecutionEvent
{
EventId = Guid.NewGuid().ToString(),
TaskExecutionId = notification.TaskExecutionId,
NodeId = notification.NodeId,
StepMapping = notification.StepMapping,
ExecutorId = notification.ExecutorId,
RuntimeCode = notification.RuntimeCode,
ScenarioCode = notification.ScenarioCode,
ScenarioId = notification.ScenarioId,
FlowName = notification.FlowName,
FlowId = notification.FlowId,
Timestamp = DateTime.UtcNow
};
// 发布控制器执行事件,让对应的 ControllerHandler 处理
await _mediator.Publish(controllerEvent, cancellationToken);
}
else
{

66
src/X1.Application/Features/TaskExecution/Events/NodeExecutionEvents/ControllerExecutionEvent.cs

@ -0,0 +1,66 @@
using X1.Domain.Events;
using X1.Domain.Entities.TestCase;
namespace X1.Application.Features.TaskExecution.Events.NodeExecutionEvents;
/// <summary>
/// 控制器执行事件
/// 专门用于 ControllerHandlers 处理的事件,避免与 NodeExecutionEventRouter 产生死循环
/// </summary>
public class ControllerExecutionEvent : INodeExecutionEvent
{
/// <summary>
/// 事件ID
/// </summary>
public string EventId { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// 任务执行ID
/// </summary>
public string TaskExecutionId { get; set; } = null!;
/// <summary>
/// 节点ID
/// </summary>
public string NodeId { get; set; } = null!;
/// <summary>
/// 步骤映射类型
/// </summary>
public StepMapping StepMapping { get; set; }
/// <summary>
/// 执行人/执行终端ID
/// </summary>
public string ExecutorId { get; set; } = null!;
/// <summary>
/// 蜂窝设备运行时编码
/// </summary>
public string RuntimeCode { get; set; } = null!;
/// <summary>
/// 测试场景编码
/// </summary>
public string ScenarioCode { get; set; } = null!;
/// <summary>
/// 测试场景ID
/// </summary>
public string ScenarioId { get; set; } = null!;
/// <summary>
/// 测试用例流程名称
/// </summary>
public string FlowName { get; set; } = null!;
/// <summary>
/// 测试用例流程ID
/// </summary>
public string FlowId { get; set; } = null!;
/// <summary>
/// 事件时间戳
/// </summary>
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
}

8
src/X1.Domain/Repositories/TestCase/ITestCaseFlowRepository.cs

@ -83,6 +83,14 @@ public interface ITestCaseFlowRepository : IBaseRepository<TestCaseFlow>
/// <returns>是否存在</returns>
Task<bool> NameExistsAsync(string name, CancellationToken cancellationToken = default);
/// <summary>
/// 根据ID列表批量获取测试用例流程
/// </summary>
/// <param name="ids">流程ID列表</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>测试用例流程列表</returns>
Task<IEnumerable<TestCaseFlow>> GetByIdsAsync(IEnumerable<string> ids, CancellationToken cancellationToken = default);
/// <summary>
/// 根据条件分页查询测试用例流程
/// </summary>

8
src/X1.Domain/Repositories/TestCase/ITestCaseNodeRepository.cs

@ -83,6 +83,14 @@ public interface ITestCaseNodeRepository : IBaseRepository<TestCaseNode>
/// <returns>测试用例节点列表</returns>
Task<IEnumerable<TestCaseNode>> GetByStepIdAsync(string stepId, CancellationToken cancellationToken = default);
/// <summary>
/// 根据测试用例ID列表批量获取起始节点(StepType = Start)
/// </summary>
/// <param name="testCaseIds">测试用例ID列表</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>每个测试用例的起始节点</returns>
Task<IEnumerable<TestCaseNode>> GetStartNodesByTestCaseIdsAsync(IEnumerable<string> testCaseIds, CancellationToken cancellationToken = default);
/// <summary>
/// 根据测试用例ID和序号获取节点
/// </summary>

40
src/X1.Domain/Services/ITaskExecutionService.cs

@ -33,16 +33,6 @@ public interface ITaskExecutionService
/// <param name="cancellationToken">取消令牌</param>
/// <returns>初始节点信息</returns>
Task<InitialNodeInfo?> GetInitialNodeAsync(string taskId, CancellationToken cancellationToken = default);
/// <summary>
/// 更新任务执行的运行时编码
/// 在 StartFlowControllerHandler 执行成功后调用,将临时 RuntimeCode 更新为正式的 RuntimeCode
/// </summary>
/// <param name="taskExecutionId">任务执行ID</param>
/// <param name="newRuntimeCode">新的运行时编码</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>更新是否成功</returns>
Task<bool> UpdateRuntimeCodeAsync(string taskExecutionId, string newRuntimeCode, CancellationToken cancellationToken = default);
}
/// <summary>
@ -51,14 +41,9 @@ public interface ITaskExecutionService
public class InitialNodeInfo
{
/// <summary>
/// 节点ID
/// 任务ID
/// </summary>
public string NodeId { get; set; } = null!;
/// <summary>
/// 步骤映射类型
/// </summary>
public StepMapping StepMapping { get; set; }
public string TaskId { get; set; } = null!;
/// <summary>
/// 测试场景ID
@ -70,6 +55,27 @@ public class InitialNodeInfo
/// </summary>
public string ScenarioCode { get; set; } = null!;
/// <summary>
/// 初始节点集合
/// </summary>
public List<InitialNodeItem> InitialNodes { get; set; } = new();
}
/// <summary>
/// 初始节点项
/// </summary>
public class InitialNodeItem
{
/// <summary>
/// 节点ID
/// </summary>
public string NodeId { get; set; } = null!;
/// <summary>
/// 步骤映射类型
/// </summary>
public StepMapping StepMapping { get; set; }
/// <summary>
/// 测试用例流程ID
/// </summary>

15
src/X1.Infrastructure/Repositories/TestCase/TestCaseFlowRepository.cs

@ -120,6 +120,21 @@ public class TestCaseFlowRepository : BaseRepository<TestCaseFlow>, ITestCaseFlo
return await QueryRepository.AnyAsync(x => x.Name == name, cancellationToken: cancellationToken);
}
/// <summary>
/// 根据ID列表批量获取测试用例流程
/// </summary>
public async Task<IEnumerable<TestCaseFlow>> GetByIdsAsync(IEnumerable<string> ids, CancellationToken cancellationToken = default)
{
var idList = ids.ToList();
if (!idList.Any())
{
return Enumerable.Empty<TestCaseFlow>();
}
var flows = await QueryRepository.FindAsync(x => idList.Contains(x.Id), cancellationToken: cancellationToken);
return flows.OrderBy(x => x.Name);
}
/// <summary>
/// 根据条件分页查询测试用例流程
/// </summary>

24
src/X1.Infrastructure/Repositories/TestCase/TestCaseNodeRepository.cs

@ -4,6 +4,7 @@ using System.Threading;
using System.Threading.Tasks;
using System.Linq;
using Microsoft.Extensions.Logging;
using Microsoft.EntityFrameworkCore;
using X1.Infrastructure.Repositories.Base;
using X1.Domain.Repositories.Base;
using X1.Domain.Entities.TestCase;
@ -119,6 +120,29 @@ public class TestCaseNodeRepository : BaseRepository<TestCaseNode>, ITestCaseNod
return nodes.OrderBy(x => x.SequenceNumber);
}
/// <summary>
/// 根据测试用例ID列表批量获取起始节点(StepType = Start)
/// </summary>
public async Task<IEnumerable<TestCaseNode>> GetStartNodesByTestCaseIdsAsync(IEnumerable<string> testCaseIds, CancellationToken cancellationToken = default)
{
var testCaseIdList = testCaseIds.ToList();
if (!testCaseIdList.Any())
{
return Enumerable.Empty<TestCaseNode>();
}
// 使用 EF Core 查询获取每个测试用例的起始节点(StepType = Start)
// 包含 StepConfig 导航属性以获取 mapping 信息
var nodes = await QueryRepository.FindAsync(
x => testCaseIdList.Contains(x.TestCaseId) &&
x.StepConfig != null &&
x.StepConfig.StepType == CaseStepType.Start,
query => query.Include(x => x.StepConfig),
cancellationToken);
return nodes.OrderBy(x => x.TestCaseId);
}
/// <summary>
/// 根据测试用例ID和序号获取节点
/// </summary>

9
src/X1.Presentation/Controllers/TaskExecutionController.cs

@ -13,7 +13,7 @@ namespace X1.Presentation.Controllers;
/// 任务执行控制器
/// </summary>
[ApiController]
[Route("api/[controller]/[action]")]
[Route("api/taskexecution")]
[Authorize]
public class TaskExecutionController : ApiController
{
@ -33,11 +33,10 @@ public class TaskExecutionController : ApiController
/// </summary>
/// <param name="request">启动任务执行请求</param>
/// <returns>启动结果</returns>
[HttpPost]
[HttpPost("start")]
public async Task<OperationResult<StartTaskExecutionResponse>> StartTaskExecution([FromBody] StartTaskExecutionCommand request)
{
_logger.LogInformation("接收到启动任务执行请求,任务ID: {TaskId}, 执行人ID: {ExecutorId}",
request.TaskId, request.ExecutorId);
_logger.LogInformation("接收到启动任务执行请求,任务ID: {TaskId}", request.TaskId);
var result = await mediator.Send(request);
if (!result.IsSuccess)
@ -53,7 +52,7 @@ public class TaskExecutionController : ApiController
/// </summary>
/// <param name="request">停止任务执行请求</param>
/// <returns>停止结果</returns>
[HttpPost]
[HttpPost("stop")]
public async Task<OperationResult<StopTaskExecutionResponse>> StopTaskExecution([FromBody] StopTaskExecutionCommand request)
{
_logger.LogInformation("接收到停止任务执行请求,任务执行ID: {TaskExecutionId}", request.TaskExecutionId);

3
src/X1.WebUI/src/constants/api.ts

@ -49,6 +49,9 @@ export const API_PATHS = {
TASK_EXECUTIONS: '/task-executions',
TASK_REVIEWS: '/task-reviews',
// 任务执行相关
TASK_EXECUTION: '/taskexecution',
// 场景相关
SCENARIOS: '/scenarios',
TEST_SCENARIOS: '/testscenarios',

4
src/X1.WebUI/src/lib/http-client.ts

@ -48,8 +48,8 @@ export class HttpClient {
},
async (error: AxiosError) => {
const config = error.config as AxiosRequestConfig & { _retryCount: number };
const maxRetries = this.configManager.getConfig().maxRetries || 3;
const maxRetries = this.configManager.getConfig().maxRetries || 0;
//console.log('maxRetries', maxRetries,config._retryCount);
// 处理重试逻辑
if (config && typeof config._retryCount === 'undefined') {
config._retryCount = 0;

59
src/X1.WebUI/src/pages/taskExecution/TaskExecutionView.tsx

@ -1,5 +1,6 @@
import React, { useEffect, useState } from 'react';
import { testScenarioTaskService, TestScenarioTask, GetTestScenarioTasksRequest } from '@/services/testScenarioTaskService';
import { taskExecutionService, StartTaskExecutionRequest, StopTaskExecutionRequest } from '@/services/taskExecutionService';
import TaskExecutionTable from './TaskExecutionTable';
import { Input } from '@/components/ui/input';
import PaginationBar from '@/components/ui/PaginationBar';
@ -61,13 +62,26 @@ export default function TaskExecutionView() {
const handleStart = async (task: TestScenarioTask) => {
try {
// TODO: 实现开始任务的逻辑
toast({
title: "开始任务",
description: `任务 "${task.taskName}" 已开始执行`,
});
// 这里可以调用相应的API来开始任务
// await taskExecutionService.startTask(task.taskId);
const request: StartTaskExecutionRequest = {
taskId: task.taskId
};
const result = await taskExecutionService.startTaskExecution(request);
if (result.isSuccess && result.data) {
toast({
title: "开始任务",
description: `任务 "${task.taskName}" 已开始执行,执行ID: ${result.data.taskExecution.id}`,
});
// 刷新任务列表以更新状态
fetchTasks();
} else {
toast({
title: "开始失败",
description: result.errorMessages?.join(', ') || "启动任务执行失败",
variant: "destructive",
});
}
} catch (error) {
console.error('开始任务失败:', error);
toast({
@ -80,13 +94,30 @@ export default function TaskExecutionView() {
const handleStop = async (task: TestScenarioTask) => {
try {
// TODO: 实现停止任务的逻辑
toast({
title: "停止任务",
description: `任务 "${task.taskName}" 已停止执行`,
});
// 这里可以调用相应的API来停止任务
// await taskExecutionService.stopTask(task.taskId);
// TODO: 需要从任务状态中获取当前执行的任务执行ID
// 这里暂时使用一个占位符,实际实现时需要根据任务状态获取正确的执行ID
const taskExecutionId = 'current-execution-id'; // TODO: 从任务状态获取当前执行ID
const request: StopTaskExecutionRequest = {
taskExecutionId: taskExecutionId
};
const result = await taskExecutionService.stopTaskExecution(request);
if (result.isSuccess && result.data) {
toast({
title: "停止任务",
description: `任务 "${task.taskName}" 已停止执行`,
});
// 刷新任务列表以更新状态
fetchTasks();
} else {
toast({
title: "停止失败",
description: result.errorMessages?.join(', ') || "停止任务执行失败",
variant: "destructive",
});
}
} catch (error) {
console.error('停止任务失败:', error);
toast({

88
src/X1.WebUI/src/services/taskExecutionService.ts

@ -0,0 +1,88 @@
import { httpClient } from '@/lib/http-client';
import { OperationResult } from '@/types/auth';
import { API_PATHS } from '@/constants/api';
// ============================================================================
// 类型定义 - 与后端 TaskExecution 实体保持一致
// ============================================================================
// 任务执行状态枚举 - 与后端 TaskExecutionStatus 保持一致
export type TaskExecutionStatus = 'Pending' | 'Running' | 'Success' | 'Failed' | 'Cancelled';
// 任务执行详情接口 - 与后端 TestScenarioTaskExecutionDetail 实体保持一致
export interface TaskExecutionDetail {
id: string;
taskId: string;
scenarioCode: string;
executorId: string;
status: TaskExecutionStatus;
startTime?: string;
endTime?: string;
duration: number; // 执行时长(秒)
loop: number; // 执行轮次
progress: number; // 执行进度百分比 (0-100)
runtimeCode: string;
}
// ============================================================================
// 请求/响应接口定义 - 与后端 Controller 完全对应
// ============================================================================
// 启动任务执行请求接口 - 对应 StartTaskExecutionCommand
export interface StartTaskExecutionRequest {
taskId: string;
}
// 启动任务执行响应接口 - 对应 StartTaskExecutionResponse
export interface StartTaskExecutionResponse {
taskExecution: TaskExecutionDetail;
}
// 停止任务执行请求接口 - 对应 StopTaskExecutionCommand
export interface StopTaskExecutionRequest {
taskExecutionId: string;
}
// 停止任务执行响应接口 - 对应 StopTaskExecutionResponse
export interface StopTaskExecutionResponse {
success: boolean;
message?: string;
}
// ============================================================================
// 任务执行服务类 - 完全对应 TaskExecutionController 的 API 端点
// ============================================================================
class TaskExecutionService {
private readonly baseUrl = API_PATHS.TASK_EXECUTION;
// ============================================================================
// 任务执行操作 - 完全对应 TaskExecutionController 的方法
// ============================================================================
/**
*
* 对应: POST /api/taskexecution/start
* 对应控制器方法: StartTaskExecution(StartTaskExecutionCommand request)
*/
async startTaskExecution(request: StartTaskExecutionRequest): Promise<OperationResult<StartTaskExecutionResponse>> {
return httpClient.post<StartTaskExecutionResponse>(`${this.baseUrl}/start`, request);
}
/**
*
* 对应: POST /api/taskexecution/stop
* 对应控制器方法: StopTaskExecution(StopTaskExecutionCommand request)
*/
async stopTaskExecution(request: StopTaskExecutionRequest): Promise<OperationResult<StopTaskExecutionResponse>> {
return httpClient.post<StopTaskExecutionResponse>(`${this.baseUrl}/stop`, request);
}
}
// ============================================================================
// 导出服务实例
// ============================================================================
export const taskExecutionService = new TaskExecutionService();

316
src/modify_20250121_initialnodeinfo_refactor.md

@ -0,0 +1,316 @@
# 2025-01-21 重构 InitialNodeInfo 和 GetInitialNodeAsync 方法
## 概述
根据用户需求,重构了 `InitialNodeInfo` 类的结构和 `GetInitialNodeAsync` 方法的逻辑,使其能够处理一个场景中包含多个测试用例流程的情况。
## 主要变更
### 1. 重构 InitialNodeInfo 类结构
**文件**: `X1.Domain/Services/ITaskExecutionService.cs`
**变更前**:
```csharp
public class InitialNodeInfo
{
public string NodeId { get; set; } = null!;
public StepMapping StepMapping { get; set; }
public string ScenarioId { get; set; } = null!;
public string ScenarioCode { get; set; } = null!;
public string FlowId { get; set; } = null!;
public string FlowName { get; set; } = null!;
}
```
**变更后**:
```csharp
public class InitialNodeInfo
{
public string TaskId { get; set; } = null!;
public string ScenarioId { get; set; } = null!;
public string ScenarioCode { get; set; } = null!;
public List<InitialNodeItem> InitialNodes { get; set; } = new();
}
public class InitialNodeItem
{
public string NodeId { get; set; } = null!;
public StepMapping StepMapping { get; set; }
public string FlowId { get; set; } = null!;
public string FlowName { get; set; } = null!;
}
```
### 2. 重构 GetInitialNodeAsync 方法
**文件**: `X1.Application/ApplicationServices/TaskExecutionService.cs`
**主要变更**:
- 使用 `GetEnabledByScenarioIdAsync` 方法在数据库层面进行过滤和排序
- 遍历所有启用的测试用例流程,获取每个流程的第一个节点
- 创建包含多个初始节点的 `InitialNodeInfo` 对象
- 改进了错误处理和日志记录
**核心逻辑**:
```csharp
// 3. 根据场景ID获取启用的场景测试用例(按执行顺序排序)
var scenarioTestCases = await _scenarioTestCaseRepository.GetEnabledByScenarioIdAsync(scenario.Id, cancellationToken);
// 4. 创建初始节点信息
var initialNodeInfo = new InitialNodeInfo
{
TaskId = taskId,
ScenarioId = scenario.Id,
ScenarioCode = task.ScenarioCode,
InitialNodes = new List<InitialNodeItem>()
};
// 5. 遍历所有启用的测试用例,获取每个流程的第一个节点
foreach (var scenarioTestCase in scenarioTestCases)
{
// 获取测试用例流程和第一个节点
// 添加到初始节点集合
}
```
### 3. 更新 StartTaskExecutionCommandHandler
**文件**: `X1.Application/Features/TaskExecution/Commands/StartTaskExecution/StartTaskExecutionCommandHandler.cs`
**变更**:
- 适配新的 `InitialNodeInfo` 结构
- 为每个初始节点发布 `NodeExecutionStartedEvent` 事件
- 改进了日志记录,显示节点数量和流程信息
**核心逻辑**:
```csharp
var initialNodeInfo = await _taskExecutionService.GetInitialNodeAsync(request.TaskId, cancellationToken);
if (initialNodeInfo != null && initialNodeInfo.InitialNodes.Any())
{
// 为每个初始节点发布事件
foreach (var initialNode in initialNodeInfo.InitialNodes)
{
var nodeExecutionStartedEvent = new NodeExecutionStartedEvent
{
// ... 设置事件属性
};
_ = _mediator.Publish(nodeExecutionStartedEvent, cancellationToken);
}
}
```
## 技术改进
### 1. 数据库查询优化
- 使用 `GetEnabledByScenarioIdAsync` 替代内存中的 `Where``OrderBy` 操作
- 在数据库层面进行过滤和排序,提高性能
### 2. 错误处理改进
- 使用 `continue` 语句跳过无效的测试用例流程,而不是直接返回 null
- 只有在没有任何有效节点时才返回 null
- 改进了日志记录的详细程度
### 3. 代码结构优化
- 将节点信息分离为独立的 `InitialNodeItem`
- 使 `InitialNodeInfo` 更符合实际业务需求(一个任务对应多个初始节点)
- 提高了代码的可读性和可维护性
## 业务逻辑说明
### 场景与测试用例流程的关系
- 一个场景(TestScenario)可以包含多个测试用例流程(TestCaseFlow)
- 每个场景测试用例(ScenarioTestCase)记录表示场景中的一个测试用例流程
- 包含执行顺序、循环次数、是否启用等配置信息
### 初始节点获取逻辑
1. 根据任务ID获取任务信息
2. 根据场景编码获取场景信息
3. 获取场景中所有启用的测试用例(按执行顺序排序)
4. 遍历每个测试用例流程,获取其第一个节点
5. 将所有初始节点信息组织成 `InitialNodeInfo` 对象返回
## 影响范围
- **向后兼容性**: 需要更新所有使用 `InitialNodeInfo` 的代码
- **事件发布**: 现在会为每个初始节点发布独立的事件
- **日志记录**: 改进了日志信息的详细程度
## 性能优化(2025-01-21 更新)
### 问题
原始的 `GetInitialNodeAsync` 方法存在 N+1 查询问题:
- 在循环中逐个查询测试用例流程
- 在循环中逐个查询每个流程的第一个节点
- 导致数据库查询次数过多,性能低下
### 解决方案
#### 1. 添加批量查询方法
**ITestCaseFlowRepository**:
```csharp
Task<IEnumerable<TestCaseFlow>> GetByIdsAsync(IEnumerable<string> ids, CancellationToken cancellationToken = default);
```
**ITestCaseNodeRepository**:
```csharp
Task<IEnumerable<TestCaseNode>> GetStartNodesByTestCaseIdsAsync(IEnumerable<string> testCaseIds, CancellationToken cancellationToken = default);
```
#### 2. 优化查询逻辑
**变更前**:
```csharp
foreach (var scenarioTestCase in scenarioTestCases)
{
// 每次循环都查询数据库
var testCaseFlow = await _testCaseFlowRepository.GetTestCaseFlowByIdAsync(...);
var nodes = await _testCaseNodeRepository.GetByTestCaseIdOrderedAsync(...);
var firstNode = nodes.FirstOrDefault();
}
```
**变更后**:
```csharp
// 批量获取所有数据
var testCaseFlowIds = scenarioTestCases.Select(stc => stc.TestCaseFlowId).ToList();
var testCaseFlows = await _testCaseFlowRepository.GetByIdsAsync(testCaseFlowIds, cancellationToken);
var firstNodes = await _testCaseNodeRepository.GetFirstNodesByTestCaseIdsAsync(testCaseFlowIds, cancellationToken);
// 转换为字典提高查找效率
var testCaseFlowDict = testCaseFlows.ToDictionary(tcf => tcf.Id, tcf => tcf);
var firstNodeDict = firstNodes.ToDictionary(fn => fn.TestCaseId, fn => fn);
// 在内存中构建结果
foreach (var scenarioTestCase in scenarioTestCases)
{
if (testCaseFlowDict.TryGetValue(scenarioTestCase.TestCaseFlowId, out var testCaseFlow) &&
firstNodeDict.TryGetValue(scenarioTestCase.TestCaseFlowId, out var firstNode))
{
// 构建初始节点信息
}
}
```
#### 3. 使用高效SQL查询
对于获取每个测试用例的起始节点,使用了关联查询:
```sql
SELECT n.*
FROM tb_testcasenode n
INNER JOIN tb_casestepconfig c ON n.stepid = c.id
WHERE n.testcaseid IN (...)
AND c.steptype = 1 -- CaseStepType.Start = 1
ORDER BY n.testcaseid
```
**重要修正**: 根据业务逻辑,应该查找 `CaseStepConfig.StepType = CaseStepType.Start` 的节点,而不是简单地取序号最小的节点。
### 业务逻辑修正(2025-01-21 更新)
#### 问题发现
用户指出原始实现有误:应该根据 `TestCaseNode` 中的 `CaseStepConfig``StepType` 来找到 `CaseStepType.Start` 类型的节点,而不是简单地取序号最小的节点。
#### 修正内容
1. **方法名称更新**:
- `GetFirstNodesByTestCaseIdsAsync``GetStartNodesByTestCaseIdsAsync`
- 更准确地反映业务逻辑
2. **SQL查询修正**:
```sql
-- 修正前:取序号最小的节点
WITH FirstNodes AS (
SELECT *, ROW_NUMBER() OVER (PARTITION BY TestCaseId ORDER BY SequenceNumber ASC) as rn
FROM tb_testcasenodes WHERE TestCaseId IN (...)
)
SELECT * FROM FirstNodes WHERE rn = 1
-- 修正后:根据StepType查找起始节点
SELECT n.*
FROM tb_testcasenode n
INNER JOIN tb_casestepconfig c ON n.stepid = c.id
WHERE n.testcaseid IN (...)
AND c.steptype = 1 -- CaseStepType.Start = 1
ORDER BY n.testcaseid
```
3. **业务逻辑正确性**:
- 确保获取的是真正的起始节点(`CaseStepType.Start`)
- 而不是流程中序号最小的节点
- 符合测试用例流程的设计意图
4. **PostgreSQL 语法修正**:
```sql
-- 修正前:SQL Server 语法
SELECT n.*
FROM tb_testcasenode n
INNER JOIN tb_casestepconfig c ON n.stepid = c.id
WHERE n.testcaseid IN (@p0, @p1, @p2)
AND c.steptype = 1
-- 修正后:PostgreSQL 语法
SELECT n.*
FROM "tb_testcasenode" n
INNER JOIN "tb_casestepconfig" c ON n."stepid" = c."id"
WHERE n."testcaseid" = ANY({0})
AND c."steptype" = 1
```
**关键修正**:
- 表名和字段名使用双引号包围
- 使用 `ANY({0})` 替代 `IN (@p0, @p1, ...)`
- 参数传递方式改为 `new object[] { testCaseIdList.ToArray() }`
5. **优化数据获取方式**:
```csharp
// 修正前:使用原生SQL查询,无法获取导航属性
var sql = "SELECT n.* FROM tb_testcasenode n INNER JOIN tb_casestepconfig c ON n.stepid = c.id WHERE ...";
var nodes = await QueryRepository.ExecuteSqlQueryAsync(sql, parameters, cancellationToken);
// 修正后:使用 EF Core 查询,包含 StepConfig 导航属性
var nodes = await QueryRepository.FindAsync(
x => testCaseIdList.Contains(x.TestCaseId) &&
x.StepConfig != null &&
x.StepConfig.StepType == CaseStepType.Start,
query => query.Include(x => x.StepConfig),
cancellationToken);
```
**优势**:
- 直接获取 `StepConfig.Mapping` 信息,无需额外查询
- 删除了 `GetStepMappingFromStepId` 方法
- 简化了 `TaskExecutionService` 中的逻辑
- 使用 `startNode.StepConfig?.Mapping ?? StepMapping.None` 直接获取映射类型
### 性能提升
- **查询次数**: 从 O(n) 降低到 O(1)(固定3次查询)
- **数据库负载**: 大幅减少数据库连接和查询开销
- **内存效率**: 使用字典提高数据查找效率
- **可扩展性**: 支持大量测试用例流程的场景
### 查询次数对比
**优化前**:
- 1次查询场景测试用例
- N次查询测试用例流程(N = 测试用例数量)
- N次查询每个流程的节点
- **总计**: 1 + 2N 次查询
**优化后**:
- 1次查询场景测试用例
- 1次批量查询测试用例流程
- 1次批量查询第一个节点
- **总计**: 3次查询
## 测试建议
1. 测试包含单个测试用例流程的场景
2. 测试包含多个测试用例流程的场景
3. 测试没有启用测试用例的场景
4. 测试测试用例流程中没有节点的情况
5. 验证事件发布的正确性
6. **性能测试**: 测试包含大量测试用例流程的场景,验证查询性能
7. **并发测试**: 测试高并发场景下的性能表现

90
src/modify_20250121_router_deadloop_fix.md

@ -0,0 +1,90 @@
# 2025-01-21 修复 NodeExecutionEventRouter 死循环问题
## 问题描述
`NodeExecutionEventRouter` 中存在死循环问题:
- `NodeExecutionEventRouter` 实现了 `INotificationHandler<NodeExecutionStartedEvent>`
- 当它重新发布 `NodeExecutionStartedEvent` 时,MediatR 会调用所有实现了该接口的处理器
- 这导致 `NodeExecutionEventRouter` 再次被调用,形成死循环
- 日志显示:`找到对应的处理器,步骤映射: StartFlow, 处理器类型: StartFlowControllerHandler` 但实际没有路由到 `StartFlowControllerHandler`
## 解决方案
### 1. 创建专门的 ControllerExecutionEvent 事件
- **文件**: `X1.Application/Features/TaskExecution/Events/NodeExecutionEvents/ControllerExecutionEvent.cs`
- **功能**: 专门用于 ControllerHandlers 处理的事件,避免与 NodeExecutionEventRouter 产生死循环
- **特点**:
- 实现 `INodeExecutionEvent` 接口
- 包含完整的执行上下文信息
- 与 `NodeExecutionStartedEvent` 结构相同,但类型不同
### 2. 更新 NodeExecutionEventRouter 逻辑
- **文件**: `X1.Application/Features/TaskExecution/Events/EventHandlers/NodeExecutionEventRouter.cs`
- **修改内容**:
- 不再重新发布 `NodeExecutionStartedEvent`
- 创建并发布 `ControllerExecutionEvent`
- 避免死循环问题
### 3. 更新所有 ControllerHandlers
更新以下处理器,让它们处理 `ControllerExecutionEvent` 而不是 `NodeExecutionStartedEvent`
#### 3.1 StartFlowControllerHandler
- **文件**: `X1.Application/Features/TaskExecution/Events/ControllerHandlers/StartFlowControllerHandler.cs`
- **修改**: 继承 `INodeExecutionHandlerBase<ControllerExecutionEvent>`
#### 3.2 EndFlowControllerHandler
- **文件**: `X1.Application/Features/TaskExecution/Events/ControllerHandlers/EndFlowControllerHandler.cs`
- **修改**: 继承 `INodeExecutionHandlerBase<ControllerExecutionEvent>`
#### 3.3 EnableFlightModeControllerHandler
- **文件**: `X1.Application/Features/TaskExecution/Events/ControllerHandlers/EnableFlightModeControllerHandler.cs`
- **修改**: 继承 `INodeExecutionHandlerBase<ControllerExecutionEvent>`
#### 3.4 DisableFlightModeControllerHandler
- **文件**: `X1.Application/Features/TaskExecution/Events/ControllerHandlers/DisableFlightModeControllerHandler.cs`
- **修改**: 继承 `INodeExecutionHandlerBase<ControllerExecutionEvent>`
#### 3.5 ImsiRegistrationControllerHandler
- **文件**: `X1.Application/Features/TaskExecution/Events/ControllerHandlers/ImsiRegistrationControllerHandler.cs`
- **修改**: 继承 `INodeExecutionHandlerBase<ControllerExecutionEvent>`
## 事件流程
修复后的事件流程:
1. **任务启动**: `StartTaskExecutionCommandHandler` 发布 `NodeExecutionStartedEvent`
2. **事件路由**: `NodeExecutionEventRouter` 接收 `NodeExecutionStartedEvent`,创建并发布 `ControllerExecutionEvent`
3. **控制器处理**: 对应的 `ControllerHandler` 接收并处理 `ControllerExecutionEvent`
4. **完成处理**: `ControllerHandler` 发布 `NodeExecutionCompletedEvent`
5. **流程控制**: `NodeExecutionCompletedEventHandler` 接收完成事件,发布下一个节点的 `NodeExecutionStartedEvent`
## 技术特点
### 1. 避免死循环
- `NodeExecutionEventRouter` 只处理 `NodeExecutionStartedEvent`
- `ControllerHandlers` 只处理 `ControllerExecutionEvent`
- 两者不会相互调用,避免死循环
### 2. 职责分离
- `NodeExecutionEventRouter`: 负责事件路由和分发
- `ControllerHandlers`: 负责具体的业务逻辑执行
- `NodeExecutionCompletedEventHandler`: 负责流程控制和下一个节点的启动
### 3. 类型安全
- 基于不同的事件类型进行路由
- 编译时类型检查
- 避免运行时类型错误
## 验证
修复后应该能够看到:
1. `NodeExecutionEventRouter` 正确路由到对应的 `ControllerHandler`
2. `StartFlowControllerHandler` 能够正常接收和处理事件
3. 不再出现死循环问题
4. 日志显示正确的处理器调用路径
## 注意事项
- `NodeExecutionCompletedEventHandler` 不需要修改,它应该继续发布 `NodeExecutionStartedEvent`
- 所有 `ControllerHandlers` 的方法签名都已更新为使用 `ControllerExecutionEvent`
- 保持了原有的业务逻辑不变,只是改变了事件类型

152
src/modify_20250121_starttaskexecution_optimization.md

@ -0,0 +1,152 @@
## 2025-01-21 优化 StartTaskExecutionCommand 移除 ExecutorId 参数
### 概述
优化 StartTaskExecutionCommand,移除 ExecutorId 参数,改为在 Handler 中通过 ICurrentUserService 获取当前用户ID,提高安全性和代码简洁性。
### 主要变更
#### 1. 优化 StartTaskExecutionCommand
- **文件**: `X1.Application/Features/TaskExecution/Commands/StartTaskExecution/StartTaskExecutionCommand.cs`
- **变更**: 移除 `ExecutorId` 属性
- **原因**: 执行人ID应该从当前用户上下文获取,而不是通过参数传递
#### 2. 更新 StartTaskExecutionCommandHandler
- **文件**: `X1.Application/Features/TaskExecution/Commands/StartTaskExecution/StartTaskExecutionCommandHandler.cs`
- **新增依赖**: 注入 `ICurrentUserService`
- **新增逻辑**: 在 Handle 方法中获取当前用户ID
- **安全检查**: 验证用户ID是否为空,为空时返回友好的错误信息
#### 3. 更新前端 TaskExecutionService
- **文件**: `X1.WebUI/src/services/taskExecutionService.ts`
- **变更**: 移除 `StartTaskExecutionRequest` 接口中的 `executorId` 字段
- **简化**: 请求参数只需要 `taskId`
#### 4. 更新前端 TaskExecutionView
- **文件**: `X1.WebUI/src/pages/taskExecution/TaskExecutionView.tsx`
- **变更**: 移除 `handleStart` 方法中的 `executorId` 参数设置
- **简化**: 请求构建更加简洁
### 技术实现
#### 1. 后端优化
```csharp
// 优化前
public class StartTaskExecutionCommand : IRequest<OperationResult<StartTaskExecutionResponse>>
{
public string TaskId { get; set; } = null!;
public string ExecutorId { get; set; } = null!; // 需要移除
}
// 优化后
public class StartTaskExecutionCommand : IRequest<OperationResult<StartTaskExecutionResponse>>
{
public string TaskId { get; set; } = null!;
}
```
#### 2. Handler 中的用户ID获取
```csharp
public async Task<OperationResult<StartTaskExecutionResponse>> Handle(StartTaskExecutionCommand request, CancellationToken cancellationToken)
{
try
{
// 获取当前用户ID
var currentUserId = _currentUserService.GetCurrentUserId();
if (string.IsNullOrEmpty(currentUserId))
{
_logger.LogWarning("无法获取当前用户ID,任务ID: {TaskId}", request.TaskId);
return OperationResult<StartTaskExecutionResponse>.CreateFailure("无法获取当前用户信息,请重新登录");
}
// 使用获取到的用户ID
var taskExecution = await _taskExecutionService.StartTaskExecutionAsync(
request.TaskId,
currentUserId,
cancellationToken);
// ... 其他逻辑
}
catch (Exception ex)
{
// ... 错误处理
}
}
```
#### 3. 前端接口简化
```typescript
// 优化前
export interface StartTaskExecutionRequest {
taskId: string;
executorId: string; // 需要移除
}
// 优化后
export interface StartTaskExecutionRequest {
taskId: string;
}
```
#### 4. 前端调用简化
```typescript
// 优化前
const request: StartTaskExecutionRequest = {
taskId: task.taskId,
executorId: 'current-user-id' // TODO: 从用户上下文获取当前用户ID
};
// 优化后
const request: StartTaskExecutionRequest = {
taskId: task.taskId
};
```
### 优化优势
#### 1. 安全性提升
- **防止伪造**: 用户无法通过前端参数伪造执行人ID
- **权限控制**: 确保只有当前登录用户才能执行任务
- **审计追踪**: 执行人ID始终来自可信的用户上下文
#### 2. 代码简洁性
- **参数减少**: 前端调用更加简洁,只需要传递任务ID
- **逻辑集中**: 用户ID获取逻辑集中在后端Handler中
- **维护性**: 减少了前后端之间的参数传递复杂度
#### 3. 一致性
- **架构统一**: 与其他需要用户上下文的操作保持一致
- **错误处理**: 统一的用户认证失败处理逻辑
- **日志记录**: 更准确的日志记录,不依赖前端传递的参数
### 依赖注入更新
需要在依赖注入容器中确保 `ICurrentUserService` 已正确注册:
```csharp
// 在 DependencyInjection.cs 中
services.AddScoped<ICurrentUserService, CurrentUserService>();
```
### 测试建议
#### 1. 单元测试
- 测试用户ID为空时的错误处理
- 测试正常用户ID获取和任务执行流程
- 测试异常情况下的错误处理
#### 2. 集成测试
- 测试完整的任务启动流程
- 测试用户认证失败的情况
- 测试前后端接口的兼容性
### 总结
本次优化通过移除 ExecutorId 参数,改为在 Handler 中获取当前用户ID,实现了:
1. ✅ **安全性提升**: 防止用户伪造执行人ID
2. ✅ **代码简化**: 减少前后端参数传递
3. ✅ **架构一致**: 与其他需要用户上下文的功能保持一致
4. ✅ **维护性**: 集中用户ID获取逻辑,便于维护
该优化完全符合安全最佳实践,提高了系统的安全性和可维护性。

88
src/modify_20250121_taskexecution_completion.md

@ -0,0 +1,88 @@
## 2025-01-21 完善 TaskExecutionTable.tsx 实现和创建 TaskExecutionService
### 概述
完善 TaskExecutionTable.tsx 组件的实现,创建 TaskExecutionService 服务类,实现与后端任务执行 API 的完整集成。
### 主要变更
#### 1. 创建 TaskExecutionService 服务类
- **文件**: `X1.WebUI/src/services/taskExecutionService.ts`
- **功能**: 提供任务执行相关的 API 调用服务
- **接口**: 与后端 TaskExecutionController 完全对应
#### 2. 实现的功能接口
- **启动任务执行**: `POST /api/taskexecution/start`
- **停止任务执行**: `POST /api/taskexecution/stop`
- **获取任务执行状态**: `GET /api/taskexecution/{executionId}/status`
- **获取任务执行进度**: `GET /api/taskexecution/{executionId}/progress`
#### 3. 接口定义
- **StartTaskExecutionRequest/Response**: 启动任务执行请求和响应
- **StopTaskExecutionRequest/Response**: 停止任务执行请求和响应
- **GetTaskExecutionStatusRequest/Response**: 获取执行状态请求和响应
- **GetTaskExecutionProgressRequest/Response**: 获取执行进度请求和响应
#### 4. 更新 TaskExecutionView.tsx
- **集成服务**: 导入并使用 TaskExecutionService
- **完善逻辑**: 实现真正的 API 调用逻辑
- **错误处理**: 完善的错误处理和用户提示
- **状态刷新**: 操作成功后自动刷新任务列表
#### 5. 技术特点
- **类型安全**: 完整的 TypeScript 类型定义
- **错误处理**: 统一的错误处理和用户友好的错误消息
- **响应格式**: 使用统一的响应格式 `{ isSuccess, data, errorMessages }`
- **日志记录**: 完整的操作日志记录
### 实现细节
#### 1. TaskExecutionService 服务类结构
```typescript
class TaskExecutionService {
private readonly baseUrl = '/api/taskexecution';
async startTaskExecution(request: StartTaskExecutionRequest): Promise<...>
async stopTaskExecution(request: StopTaskExecutionRequest): Promise<...>
async getTaskExecutionStatus(request: GetTaskExecutionStatusRequest): Promise<...>
async getTaskExecutionProgress(request: GetTaskExecutionProgressRequest): Promise<...>
}
```
#### 2. 与后端 API 的对应关系
- 前端服务方法名与后端 Controller 方法名保持一致
- 请求/响应接口与后端 Command/Response 完全对应
- 错误处理与后端 OperationResult 格式保持一致
#### 3. 用户体验优化
- 操作成功后显示详细的成功信息(包括执行ID)
- 操作失败时显示具体的错误原因
- 自动刷新任务列表以反映最新状态
- 保持原有的加载状态和交互反馈
### 待完善项目
#### 1. 用户上下文集成
- 需要从用户上下文获取当前用户ID,替换硬编码的 `'current-user-id'`
- 实现用户权限验证和任务执行权限检查
#### 2. 任务执行状态管理
- 需要从任务状态中获取当前执行的任务执行ID
- 实现任务执行状态的实时更新和显示
- 添加任务执行进度的实时监控
#### 3. 功能扩展
- 实现任务执行状态的实时查询
- 添加任务执行进度的可视化显示
- 支持任务执行日志的查看和下载
### 总结
TaskExecutionTable.tsx 组件现在已经基本完成,包括:
1. ✅ 完整的表格组件实现
2. ✅ 完整的页面布局和交互
3. ✅ 完整的服务类实现
4. ✅ 与后端 API 的集成
5. ✅ 错误处理和用户反馈
该实现为后续的任务执行功能扩展提供了良好的基础,完全符合 Clean Architecture 的设计原则。

150
src/modify_20250121_taskexecution_controller_route_fix.md

@ -0,0 +1,150 @@
## 2025-01-21 修复 TaskExecutionController 路由设计和简化服务
### 概述
修复 TaskExecutionController 的路由设计,使其符合项目架构标准,并简化 taskExecutionService.ts 只保留与 Controller 对应的方法。
### 问题分析
#### 1. 路由设计问题
- **错误路由**: 使用了 `[Route("api/[controller]/[action]")]`
- **正确路由**: 应该使用 `[Route("api/taskexecution")]``[Route("api/[controller]")]`
- **参考标准**: 其他控制器如 TestScenarioTasksController、TestScenariosController 都使用具体路径
#### 2. 服务冗余问题
- **冗余方法**: taskExecutionService.ts 包含了 Controller 中不存在的方法
- **不一致性**: 前端服务与后端 Controller 不匹配
- **维护困难**: 多余的代码增加了维护成本
### 主要变更
#### 1. 修复 TaskExecutionController 路由
- **文件**: `X1.Presentation/Controllers/TaskExecutionController.cs`
- **路由修复**:
```csharp
// 修复前
[Route("api/[controller]/[action]")]
// 修复后
[Route("api/taskexecution")]
```
#### 2. 更新方法路由
- **启动任务**: `[HttpPost("start")]``/api/taskexecution/start`
- **停止任务**: `[HttpPost("stop")]``/api/taskexecution/stop`
#### 3. 移除冗余的 Query 方法
- **移除**: `GetTaskExecutionStatus``GetTaskExecutionProgress` 方法
- **原因**: Controller 中没有对应的端点
- **清理**: 移除相关的 using 语句和导入
#### 4. 简化 taskExecutionService.ts
- **移除接口**: `GetTaskExecutionStatusRequest/Response``GetTaskExecutionProgressRequest/Response`
- **移除方法**: `getTaskExecutionStatus()``getTaskExecutionProgress()`
- **保留方法**: 只保留 `startTaskExecution()``stopTaskExecution()`
### 技术实现
#### 1. Controller 路由设计
```csharp
[ApiController]
[Route("api/taskexecution")] // 使用具体路径,与其他控制器保持一致
[Authorize]
public class TaskExecutionController : ApiController
{
[HttpPost("start")] // POST /api/taskexecution/start
public async Task<OperationResult<StartTaskExecutionResponse>> StartTaskExecution(...)
[HttpPost("stop")] // POST /api/taskexecution/stop
public async Task<OperationResult<StopTaskExecutionResponse>> StopTaskExecution(...)
}
```
#### 2. 前端服务简化
```typescript
class TaskExecutionService {
private readonly baseUrl = API_PATHS.TASK_EXECUTION; // '/taskexecution'
// 只保留与 Controller 对应的方法
async startTaskExecution(request: StartTaskExecutionRequest): Promise<OperationResult<StartTaskExecutionResponse>> {
return httpClient.post<StartTaskExecutionResponse>(`${this.baseUrl}/start`, request);
}
async stopTaskExecution(request: StopTaskExecutionRequest): Promise<OperationResult<StopTaskExecutionResponse>> {
return httpClient.post<StopTaskExecutionResponse>(`${this.baseUrl}/stop`, request);
}
}
```
#### 3. API 路径对应关系
```
前端服务方法 → 后端 Controller 方法
startTaskExecution() → POST /api/taskexecution/start
stopTaskExecution() → POST /api/taskexecution/stop
```
### 架构优势
#### 1. 一致性
- **路由标准**: 与其他控制器保持完全一致的路由设计
- **命名规范**: 使用具体的路径而不是动态路由
- **方法对应**: 前端服务与后端 Controller 完全对应
#### 2. 简洁性
- **代码精简**: 移除冗余的接口和方法
- **维护简单**: 减少不必要的代码维护成本
- **逻辑清晰**: 只保留实际需要的功能
#### 3. 可维护性
- **单一职责**: 每个方法都有明确的对应关系
- **易于测试**: 简化的结构便于单元测试
- **易于扩展**: 需要时可以轻松添加新的端点
### 与其他控制器的对比
#### 1. TestScenarioTasksController
```csharp
[Route("api/testscenariotasks")] // 具体路径
[HttpGet] // GET /api/testscenariotasks
[HttpGet("{id}")] // GET /api/testscenariotasks/{id}
[HttpPost] // POST /api/testscenariotasks
```
#### 2. TestScenariosController
```csharp
[Route("api/testscenarios")] // 具体路径
[HttpGet] // GET /api/testscenarios
[HttpGet("{id}")] // GET /api/testscenarios/{id}
[HttpPost] // POST /api/testscenarios
```
#### 3. TaskExecutionController (修复后)
```csharp
[Route("api/taskexecution")] // 具体路径,保持一致
[HttpPost("start")] // POST /api/taskexecution/start
[HttpPost("stop")] // POST /api/taskexecution/stop
```
### 测试建议
#### 1. API 端点测试
- 测试 `POST /api/taskexecution/start` 端点
- 测试 `POST /api/taskexecution/stop` 端点
- 验证路由是否正确解析
#### 2. 前端集成测试
- 测试 `startTaskExecution()` 方法调用
- 测试 `stopTaskExecution()` 方法调用
- 验证请求和响应格式
### 总结
本次修复解决了以下问题:
1. ✅ **路由设计**: 修复了不符合项目标准的路由设计
2. ✅ **架构一致性**: 与其他控制器保持完全一致的设计模式
3. ✅ **代码精简**: 移除了冗余的接口和方法
4. ✅ **维护性**: 简化了代码结构,提高了可维护性
5. ✅ **对应关系**: 前端服务与后端 Controller 完全对应
现在 TaskExecutionController 和 taskExecutionService.ts 都符合项目的架构标准,具有良好的一致性和可维护性。

186
src/modify_20250121_taskexecution_service_architecture_fix.md

@ -0,0 +1,186 @@
## 2025-01-21 修复 TaskExecutionService 架构设计
### 概述
修复 TaskExecutionService 的架构设计,使其符合当前项目的设计模式,参考 testScenarioTaskService.ts 的实现架构。
### 问题分析
#### 原始问题
- 使用了不存在的 `apiClient` 导入
- 没有遵循当前项目的架构模式
- 响应格式与后端不一致
- 缺少统一的错误处理机制
#### 架构不一致性
- 应该使用 `httpClient` 而不是 `apiClient`
- 应该使用 `OperationResult<T>` 统一响应格式
- 应该使用 `API_PATHS` 常量管理 API 路径
- 应该与后端 Controller 完全对应
### 主要变更
#### 1. 修复导入和依赖
- **文件**: `X1.WebUI/src/services/taskExecutionService.ts`
- **变更**: 使用正确的导入
```typescript
// 修复前
import { apiClient } from './apiClient';
// 修复后
import { httpClient } from '@/lib/http-client';
import { OperationResult } from '@/types/auth';
import { API_PATHS } from '@/constants/api';
```
#### 2. 添加 API 路径常量
- **文件**: `X1.WebUI/src/constants/api.ts`
- **新增**: 任务执行相关 API 路径
```typescript
// 任务执行相关
TASK_EXECUTION: '/api/taskexecution',
```
#### 3. 统一响应格式
- **变更**: 使用 `OperationResult<T>` 统一响应格式
- **优势**: 与后端 Controller 返回格式完全一致
- **包含**: `isSuccess`, `data`, `errorMessages`, `successMessage`
#### 4. 完善类型定义
- **新增**: `TaskExecutionStatus` 枚举
- **新增**: `TaskExecutionDetail` 接口
- **完善**: 请求/响应接口定义,与后端完全对应
#### 5. 更新响应处理
- **文件**: `X1.WebUI/src/pages/taskExecution/TaskExecutionView.tsx`
- **变更**: 适配新的响应格式
```typescript
// 修复前
result.data.taskExecutionId
// 修复后
result.data.taskExecution.id
```
### 技术实现
#### 1. 服务类架构
```typescript
class TaskExecutionService {
private readonly baseUrl = API_PATHS.TASK_EXECUTION;
async startTaskExecution(request: StartTaskExecutionRequest): Promise<OperationResult<StartTaskExecutionResponse>> {
return httpClient.post<StartTaskExecutionResponse>(`${this.baseUrl}/start`, request);
}
async stopTaskExecution(request: StopTaskExecutionRequest): Promise<OperationResult<StopTaskExecutionResponse>> {
return httpClient.post<StopTaskExecutionResponse>(`${this.baseUrl}/stop`, request);
}
// ... 其他方法
}
```
#### 2. 类型定义
```typescript
// 任务执行状态枚举
export type TaskExecutionStatus = 'Pending' | 'Running' | 'Success' | 'Failed' | 'Cancelled';
// 任务执行详情接口
export interface TaskExecutionDetail {
id: string;
taskId: string;
scenarioCode: string;
executorId: string;
status: TaskExecutionStatus;
startTime?: string;
endTime?: string;
duration: number;
loop: number;
progress: number;
runtimeCode: string;
}
// 启动任务执行响应接口
export interface StartTaskExecutionResponse {
taskExecution: TaskExecutionDetail;
}
```
#### 3. API 路径管理
```typescript
// 在 API_PATHS 中统一管理
export const API_PATHS = {
// ... 其他路径
TASK_EXECUTION: '/api/taskexecution',
} as const;
```
### 架构优势
#### 1. 一致性
- **统一导入**: 使用项目标准的 `httpClient`
- **统一响应**: 使用 `OperationResult<T>` 格式
- **统一路径**: 使用 `API_PATHS` 常量管理
#### 2. 可维护性
- **类型安全**: 完整的 TypeScript 类型定义
- **错误处理**: 统一的错误处理机制
- **代码复用**: 遵循项目既定的架构模式
#### 3. 扩展性
- **模块化**: 清晰的服务类结构
- **可测试**: 易于单元测试和集成测试
- **可扩展**: 便于添加新的 API 端点
### 与 testScenarioTaskService.ts 的一致性
#### 1. 导入模式
```typescript
// 两个服务都使用相同的导入模式
import { httpClient } from '@/lib/http-client';
import { OperationResult } from '@/types/auth';
import { API_PATHS } from '@/constants/api';
```
#### 2. 服务类结构
```typescript
// 相同的服务类结构
class ServiceName {
private readonly baseUrl = API_PATHS.SERVICE_PATH;
async methodName(request: RequestType): Promise<OperationResult<ResponseType>> {
return httpClient.post<ResponseType>(`${this.baseUrl}/endpoint`, request);
}
}
```
#### 3. 导出模式
```typescript
// 相同的导出模式
export const serviceName = new ServiceName();
```
### 测试建议
#### 1. 单元测试
- 测试服务方法的正确调用
- 测试响应格式的正确解析
- 测试错误情况的处理
#### 2. 集成测试
- 测试与后端 API 的完整交互
- 测试前端组件的正确集成
- 测试错误处理和用户反馈
### 总结
本次修复通过以下方式解决了架构不一致问题:
1. ✅ **修复导入**: 使用正确的 `httpClient` 和类型定义
2. ✅ **统一响应**: 使用 `OperationResult<T>` 格式
3. ✅ **路径管理**: 使用 `API_PATHS` 常量
4. ✅ **类型安全**: 完整的 TypeScript 类型定义
5. ✅ **架构一致**: 与 `testScenarioTaskService.ts` 保持完全一致
现在 TaskExecutionService 完全符合当前项目的架构设计,具有良好的一致性、可维护性和扩展性。
Loading…
Cancel
Save