diff --git a/src/X1.Application/ApplicationServices/TaskExecutionService.cs b/src/X1.Application/ApplicationServices/TaskExecutionService.cs index 1ec3731..e795846 100644 --- a/src/X1.Application/ApplicationServices/TaskExecutionService.cs +++ b/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 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)); } /// @@ -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() + }; - // 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; } } - - /// - /// 根据步骤ID获取步骤映射类型 - /// 这里需要根据实际的步骤配置表来实现 - /// - /// 步骤ID - /// 步骤映射类型 - private StepMapping GetStepMappingFromStepId(string stepId) - { - // TODO: 这里需要根据实际的步骤配置表来实现 - // 暂时返回 StartFlow 作为默认值 - return StepMapping.StartFlow; - } - - - /// - /// 更新任务执行的运行时编码 - /// 在 StartFlowControllerHandler 执行成功后调用,设置正式的 RuntimeCode - /// - /// 任务执行ID - /// 新的运行时编码 - /// 取消令牌 - /// 更新是否成功 - public async Task 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; - } - } } diff --git a/src/X1.Application/Features/TaskExecution/Commands/StartTaskExecution/StartTaskExecutionCommand.cs b/src/X1.Application/Features/TaskExecution/Commands/StartTaskExecution/StartTaskExecutionCommand.cs index 272deac..1aaf5db 100644 --- a/src/X1.Application/Features/TaskExecution/Commands/StartTaskExecution/StartTaskExecutionCommand.cs +++ b/src/X1.Application/Features/TaskExecution/Commands/StartTaskExecution/StartTaskExecutionCommand.cs @@ -12,9 +12,4 @@ public class StartTaskExecutionCommand : IRequest public string TaskId { get; set; } = null!; - - /// - /// 执行人ID - /// - public string ExecutorId { get; set; } = null!; } diff --git a/src/X1.Application/Features/TaskExecution/Commands/StartTaskExecution/StartTaskExecutionCommandHandler.cs b/src/X1.Application/Features/TaskExecution/Commands/StartTaskExecution/StartTaskExecutionCommandHandler.cs index 68bf648..b2d38a4 100644 --- a/src/X1.Application/Features/TaskExecution/Commands/StartTaskExecution/StartTaskExecutionCommandHandler.cs +++ b/src/X1.Application/Features/TaskExecution/Commands/StartTaskExecution/StartTaskExecutionCommandHandler.cs @@ -16,17 +16,20 @@ public class StartTaskExecutionCommandHandler : IRequestHandler logger, ITaskExecutionService taskExecutionService, IUnitOfWork unitOfWork, - IMediator mediator) + IMediator mediator, + ICurrentUserService currentUserService) { _logger = logger; _taskExecutionService = taskExecutionService; _unitOfWork = unitOfWork; _mediator = mediator; + _currentUserService = currentUserService; } /// @@ -39,42 +42,57 @@ public class StartTaskExecutionCommandHandler : IRequestHandler.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.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.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.CreateFailure("启动任务执行失败,请稍后重试"); } } diff --git a/src/X1.Application/Features/TaskExecution/Commands/StartTaskExecution/StartTaskExecutionCommandValidator.cs b/src/X1.Application/Features/TaskExecution/Commands/StartTaskExecution/StartTaskExecutionCommandValidator.cs index dd3b284..70b6648 100644 --- a/src/X1.Application/Features/TaskExecution/Commands/StartTaskExecution/StartTaskExecutionCommandValidator.cs +++ b/src/X1.Application/Features/TaskExecution/Commands/StartTaskExecution/StartTaskExecutionCommandValidator.cs @@ -14,11 +14,5 @@ public class StartTaskExecutionCommandValidator : AbstractValidator x.ExecutorId) - .NotEmpty() - .WithMessage("执行人ID不能为空") - .MaximumLength(50) - .WithMessage("执行人ID长度不能超过50个字符"); } } diff --git a/src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/DisableFlightModeControllerHandler.cs b/src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/DisableFlightModeControllerHandler.cs index 89a0d88..a5bd31c 100644 --- a/src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/DisableFlightModeControllerHandler.cs +++ b/src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/DisableFlightModeControllerHandler.cs @@ -11,7 +11,7 @@ namespace X1.Application.Features.TaskExecution.Events.ControllerHandlers; /// 关闭飞行模式控制器处理器 /// 处理关闭飞行模式相关的节点执行事件 /// -public class DisableFlightModeControllerHandler : INodeExecutionHandlerBase +public class DisableFlightModeControllerHandler : INodeExecutionHandlerBase { /// /// 初始化关闭飞行模式控制器处理器 @@ -29,7 +29,7 @@ public class DisableFlightModeControllerHandler : INodeExecutionHandlerBase节点执行事件 /// 取消令牌 /// 处理任务 - 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事件通知 /// 取消令牌 /// 执行结果 - private async Task ExecuteDisableFlightModeAsync(NodeExecutionStartedEvent notification, CancellationToken cancellationToken) + private async Task ExecuteDisableFlightModeAsync(ControllerExecutionEvent notification, CancellationToken cancellationToken) { // TODO: 实现具体的关闭飞行模式逻辑 // 这里应该调用实际的设备控制服务 diff --git a/src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/EnableFlightModeControllerHandler.cs b/src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/EnableFlightModeControllerHandler.cs index 698de46..1b7f37b 100644 --- a/src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/EnableFlightModeControllerHandler.cs +++ b/src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/EnableFlightModeControllerHandler.cs @@ -11,7 +11,7 @@ namespace X1.Application.Features.TaskExecution.Events.ControllerHandlers; /// 开启飞行模式控制器处理器 /// 处理开启飞行模式相关的节点执行事件 /// -public class EnableFlightModeControllerHandler : INodeExecutionHandlerBase +public class EnableFlightModeControllerHandler : INodeExecutionHandlerBase { /// /// 初始化开启飞行模式控制器处理器 @@ -29,7 +29,7 @@ public class EnableFlightModeControllerHandler : INodeExecutionHandlerBase节点执行事件 /// 取消令牌 /// 处理任务 - 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事件通知 /// 取消令牌 /// 执行结果 - private async Task ExecuteEnableFlightModeAsync(NodeExecutionStartedEvent notification, CancellationToken cancellationToken) + private async Task ExecuteEnableFlightModeAsync(ControllerExecutionEvent notification, CancellationToken cancellationToken) { // TODO: 实现具体的开启飞行模式逻辑 // 这里应该调用实际的设备控制服务 diff --git a/src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/EndFlowControllerHandler.cs b/src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/EndFlowControllerHandler.cs index 5467e1a..b42e2b0 100644 --- a/src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/EndFlowControllerHandler.cs +++ b/src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/EndFlowControllerHandler.cs @@ -11,7 +11,7 @@ namespace X1.Application.Features.TaskExecution.Events.ControllerHandlers; /// 结束流程控制器处理器 /// 处理测试流程结束相关的节点执行事件 /// -public class EndFlowControllerHandler : INodeExecutionHandlerBase +public class EndFlowControllerHandler : INodeExecutionHandlerBase { /// /// 初始化结束流程控制器处理器 @@ -29,7 +29,7 @@ public class EndFlowControllerHandler : INodeExecutionHandlerBase节点执行事件 /// 取消令牌 /// 处理任务 - 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事件通知 /// 取消令牌 /// 执行结果 - private async Task ExecuteEndFlowAsync(NodeExecutionStartedEvent notification, CancellationToken cancellationToken) + private async Task ExecuteEndFlowAsync(ControllerExecutionEvent notification, CancellationToken cancellationToken) { // TODO: 实现具体的结束流程逻辑 // 这里应该调用实际的结束流程服务 diff --git a/src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/ImsiRegistrationControllerHandler.cs b/src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/ImsiRegistrationControllerHandler.cs index 90899e8..47db879 100644 --- a/src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/ImsiRegistrationControllerHandler.cs +++ b/src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/ImsiRegistrationControllerHandler.cs @@ -11,7 +11,7 @@ namespace X1.Application.Features.TaskExecution.Events.ControllerHandlers; /// IMSI注册控制器处理器 /// 处理IMSI注册相关的节点执行事件 /// -public class ImsiRegistrationControllerHandler : INodeExecutionHandlerBase +public class ImsiRegistrationControllerHandler : INodeExecutionHandlerBase { /// /// 初始化IMSI注册控制器处理器 @@ -29,7 +29,7 @@ public class ImsiRegistrationControllerHandler : INodeExecutionHandlerBase节点执行事件 /// 取消令牌 /// 处理任务 - 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事件通知 /// 取消令牌 /// 执行结果 - private async Task ExecuteImsiRegistrationAsync(NodeExecutionStartedEvent notification, CancellationToken cancellationToken) + private async Task ExecuteImsiRegistrationAsync(ControllerExecutionEvent notification, CancellationToken cancellationToken) { // TODO: 实现具体的IMSI注册逻辑 // 这里应该调用实际的IMSI注册服务 diff --git a/src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/StartFlowControllerHandler.cs b/src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/StartFlowControllerHandler.cs index 86c4395..64e12dd 100644 --- a/src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/StartFlowControllerHandler.cs +++ b/src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/StartFlowControllerHandler.cs @@ -11,7 +11,7 @@ namespace X1.Application.Features.TaskExecution.Events.ControllerHandlers; /// 启动流程控制器处理器 /// 处理测试流程启动相关的节点执行事件 /// -public class StartFlowControllerHandler : INodeExecutionHandlerBase +public class StartFlowControllerHandler : INodeExecutionHandlerBase { /// /// 初始化启动流程控制器处理器 @@ -29,7 +29,7 @@ public class StartFlowControllerHandler : INodeExecutionHandlerBase节点执行事件 /// 取消令牌 /// 处理任务 - 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事件通知 /// 取消令牌 /// 执行结果 - private async Task ExecuteStartFlowAsync(NodeExecutionStartedEvent notification, CancellationToken cancellationToken) + private async Task ExecuteStartFlowAsync(ControllerExecutionEvent notification, CancellationToken cancellationToken) { // TODO: 实现具体的启动流程逻辑 // 这里应该调用实际的启动流程服务 diff --git a/src/X1.Application/Features/TaskExecution/Events/EventHandlers/NodeExecutionEventRouter.cs b/src/X1.Application/Features/TaskExecution/Events/EventHandlers/NodeExecutionEventRouter.cs index 870d267..7d1b00d 100644 --- a/src/X1.Application/Features/TaskExecution/Events/EventHandlers/NodeExecutionEventRouter.cs +++ b/src/X1.Application/Features/TaskExecution/Events/EventHandlers/NodeExecutionEventRouter.cs @@ -50,10 +50,24 @@ public class NodeExecutionEventRouter : INotificationHandler - // 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 { diff --git a/src/X1.Application/Features/TaskExecution/Events/NodeExecutionEvents/ControllerExecutionEvent.cs b/src/X1.Application/Features/TaskExecution/Events/NodeExecutionEvents/ControllerExecutionEvent.cs new file mode 100644 index 0000000..18f6aed --- /dev/null +++ b/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; + +/// +/// 控制器执行事件 +/// 专门用于 ControllerHandlers 处理的事件,避免与 NodeExecutionEventRouter 产生死循环 +/// +public class ControllerExecutionEvent : INodeExecutionEvent +{ + /// + /// 事件ID + /// + public string EventId { get; set; } = Guid.NewGuid().ToString(); + + /// + /// 任务执行ID + /// + public string TaskExecutionId { get; set; } = null!; + + /// + /// 节点ID + /// + public string NodeId { get; set; } = null!; + + /// + /// 步骤映射类型 + /// + public StepMapping StepMapping { get; set; } + + /// + /// 执行人/执行终端ID + /// + public string ExecutorId { get; set; } = null!; + + /// + /// 蜂窝设备运行时编码 + /// + public string RuntimeCode { get; set; } = null!; + + /// + /// 测试场景编码 + /// + public string ScenarioCode { get; set; } = null!; + + /// + /// 测试场景ID + /// + public string ScenarioId { get; set; } = null!; + + /// + /// 测试用例流程名称 + /// + public string FlowName { get; set; } = null!; + + /// + /// 测试用例流程ID + /// + public string FlowId { get; set; } = null!; + + /// + /// 事件时间戳 + /// + public DateTime Timestamp { get; set; } = DateTime.UtcNow; +} diff --git a/src/X1.Domain/Repositories/TestCase/ITestCaseFlowRepository.cs b/src/X1.Domain/Repositories/TestCase/ITestCaseFlowRepository.cs index 56ef3d1..17d582d 100644 --- a/src/X1.Domain/Repositories/TestCase/ITestCaseFlowRepository.cs +++ b/src/X1.Domain/Repositories/TestCase/ITestCaseFlowRepository.cs @@ -83,6 +83,14 @@ public interface ITestCaseFlowRepository : IBaseRepository /// 是否存在 Task NameExistsAsync(string name, CancellationToken cancellationToken = default); + /// + /// 根据ID列表批量获取测试用例流程 + /// + /// 流程ID列表 + /// 取消令牌 + /// 测试用例流程列表 + Task> GetByIdsAsync(IEnumerable ids, CancellationToken cancellationToken = default); + /// /// 根据条件分页查询测试用例流程 /// diff --git a/src/X1.Domain/Repositories/TestCase/ITestCaseNodeRepository.cs b/src/X1.Domain/Repositories/TestCase/ITestCaseNodeRepository.cs index 87c1e6f..4ff329f 100644 --- a/src/X1.Domain/Repositories/TestCase/ITestCaseNodeRepository.cs +++ b/src/X1.Domain/Repositories/TestCase/ITestCaseNodeRepository.cs @@ -83,6 +83,14 @@ public interface ITestCaseNodeRepository : IBaseRepository /// 测试用例节点列表 Task> GetByStepIdAsync(string stepId, CancellationToken cancellationToken = default); + /// + /// 根据测试用例ID列表批量获取起始节点(StepType = Start) + /// + /// 测试用例ID列表 + /// 取消令牌 + /// 每个测试用例的起始节点 + Task> GetStartNodesByTestCaseIdsAsync(IEnumerable testCaseIds, CancellationToken cancellationToken = default); + /// /// 根据测试用例ID和序号获取节点 /// diff --git a/src/X1.Domain/Services/ITaskExecutionService.cs b/src/X1.Domain/Services/ITaskExecutionService.cs index 54b5c26..54713c9 100644 --- a/src/X1.Domain/Services/ITaskExecutionService.cs +++ b/src/X1.Domain/Services/ITaskExecutionService.cs @@ -33,16 +33,6 @@ public interface ITaskExecutionService /// 取消令牌 /// 初始节点信息 Task GetInitialNodeAsync(string taskId, CancellationToken cancellationToken = default); - - /// - /// 更新任务执行的运行时编码 - /// 在 StartFlowControllerHandler 执行成功后调用,将临时 RuntimeCode 更新为正式的 RuntimeCode - /// - /// 任务执行ID - /// 新的运行时编码 - /// 取消令牌 - /// 更新是否成功 - Task UpdateRuntimeCodeAsync(string taskExecutionId, string newRuntimeCode, CancellationToken cancellationToken = default); } /// @@ -51,14 +41,9 @@ public interface ITaskExecutionService public class InitialNodeInfo { /// - /// 节点ID + /// 任务ID /// - public string NodeId { get; set; } = null!; - - /// - /// 步骤映射类型 - /// - public StepMapping StepMapping { get; set; } + public string TaskId { get; set; } = null!; /// /// 测试场景ID @@ -70,6 +55,27 @@ public class InitialNodeInfo /// public string ScenarioCode { get; set; } = null!; + /// + /// 初始节点集合 + /// + public List InitialNodes { get; set; } = new(); +} + +/// +/// 初始节点项 +/// +public class InitialNodeItem +{ + /// + /// 节点ID + /// + public string NodeId { get; set; } = null!; + + /// + /// 步骤映射类型 + /// + public StepMapping StepMapping { get; set; } + /// /// 测试用例流程ID /// diff --git a/src/X1.Infrastructure/Repositories/TestCase/TestCaseFlowRepository.cs b/src/X1.Infrastructure/Repositories/TestCase/TestCaseFlowRepository.cs index 885f85a..f1f02d0 100644 --- a/src/X1.Infrastructure/Repositories/TestCase/TestCaseFlowRepository.cs +++ b/src/X1.Infrastructure/Repositories/TestCase/TestCaseFlowRepository.cs @@ -120,6 +120,21 @@ public class TestCaseFlowRepository : BaseRepository, ITestCaseFlo return await QueryRepository.AnyAsync(x => x.Name == name, cancellationToken: cancellationToken); } + /// + /// 根据ID列表批量获取测试用例流程 + /// + public async Task> GetByIdsAsync(IEnumerable ids, CancellationToken cancellationToken = default) + { + var idList = ids.ToList(); + if (!idList.Any()) + { + return Enumerable.Empty(); + } + + var flows = await QueryRepository.FindAsync(x => idList.Contains(x.Id), cancellationToken: cancellationToken); + return flows.OrderBy(x => x.Name); + } + /// /// 根据条件分页查询测试用例流程 /// diff --git a/src/X1.Infrastructure/Repositories/TestCase/TestCaseNodeRepository.cs b/src/X1.Infrastructure/Repositories/TestCase/TestCaseNodeRepository.cs index a2bf27b..352dc6a 100644 --- a/src/X1.Infrastructure/Repositories/TestCase/TestCaseNodeRepository.cs +++ b/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, ITestCaseNod return nodes.OrderBy(x => x.SequenceNumber); } + /// + /// 根据测试用例ID列表批量获取起始节点(StepType = Start) + /// + public async Task> GetStartNodesByTestCaseIdsAsync(IEnumerable testCaseIds, CancellationToken cancellationToken = default) + { + var testCaseIdList = testCaseIds.ToList(); + if (!testCaseIdList.Any()) + { + return Enumerable.Empty(); + } + + // 使用 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); + } + /// /// 根据测试用例ID和序号获取节点 /// diff --git a/src/X1.Presentation/Controllers/TaskExecutionController.cs b/src/X1.Presentation/Controllers/TaskExecutionController.cs index 244c752..907d4c4 100644 --- a/src/X1.Presentation/Controllers/TaskExecutionController.cs +++ b/src/X1.Presentation/Controllers/TaskExecutionController.cs @@ -13,7 +13,7 @@ namespace X1.Presentation.Controllers; /// 任务执行控制器 /// [ApiController] -[Route("api/[controller]/[action]")] +[Route("api/taskexecution")] [Authorize] public class TaskExecutionController : ApiController { @@ -33,11 +33,10 @@ public class TaskExecutionController : ApiController /// /// 启动任务执行请求 /// 启动结果 - [HttpPost] + [HttpPost("start")] public async Task> 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 /// /// 停止任务执行请求 /// 停止结果 - [HttpPost] + [HttpPost("stop")] public async Task> StopTaskExecution([FromBody] StopTaskExecutionCommand request) { _logger.LogInformation("接收到停止任务执行请求,任务执行ID: {TaskExecutionId}", request.TaskExecutionId); diff --git a/src/X1.WebUI/src/constants/api.ts b/src/X1.WebUI/src/constants/api.ts index 06dbf43..aabe49f 100644 --- a/src/X1.WebUI/src/constants/api.ts +++ b/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', diff --git a/src/X1.WebUI/src/lib/http-client.ts b/src/X1.WebUI/src/lib/http-client.ts index 9d372b1..23a0e5b 100644 --- a/src/X1.WebUI/src/lib/http-client.ts +++ b/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; diff --git a/src/X1.WebUI/src/pages/taskExecution/TaskExecutionView.tsx b/src/X1.WebUI/src/pages/taskExecution/TaskExecutionView.tsx index 1023258..56e5679 100644 --- a/src/X1.WebUI/src/pages/taskExecution/TaskExecutionView.tsx +++ b/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({ diff --git a/src/X1.WebUI/src/services/taskExecutionService.ts b/src/X1.WebUI/src/services/taskExecutionService.ts new file mode 100644 index 0000000..99f4110 --- /dev/null +++ b/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> { + return httpClient.post(`${this.baseUrl}/start`, request); + } + + /** + * 停止任务执行 + * 对应: POST /api/taskexecution/stop + * 对应控制器方法: StopTaskExecution(StopTaskExecutionCommand request) + */ + async stopTaskExecution(request: StopTaskExecutionRequest): Promise> { + return httpClient.post(`${this.baseUrl}/stop`, request); + } + +} + +// ============================================================================ +// 导出服务实例 +// ============================================================================ + +export const taskExecutionService = new TaskExecutionService(); diff --git a/src/modify_20250121_initialnodeinfo_refactor.md b/src/modify_20250121_initialnodeinfo_refactor.md new file mode 100644 index 0000000..d901bb5 --- /dev/null +++ b/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 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() +}; + +// 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> GetByIdsAsync(IEnumerable ids, CancellationToken cancellationToken = default); +``` + +**ITestCaseNodeRepository**: +```csharp +Task> GetStartNodesByTestCaseIdsAsync(IEnumerable 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. **并发测试**: 测试高并发场景下的性能表现 diff --git a/src/modify_20250121_router_deadloop_fix.md b/src/modify_20250121_router_deadloop_fix.md new file mode 100644 index 0000000..cfbfefe --- /dev/null +++ b/src/modify_20250121_router_deadloop_fix.md @@ -0,0 +1,90 @@ +# 2025-01-21 修复 NodeExecutionEventRouter 死循环问题 + +## 问题描述 + +在 `NodeExecutionEventRouter` 中存在死循环问题: +- `NodeExecutionEventRouter` 实现了 `INotificationHandler` +- 当它重新发布 `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` + +#### 3.2 EndFlowControllerHandler +- **文件**: `X1.Application/Features/TaskExecution/Events/ControllerHandlers/EndFlowControllerHandler.cs` +- **修改**: 继承 `INodeExecutionHandlerBase` + +#### 3.3 EnableFlightModeControllerHandler +- **文件**: `X1.Application/Features/TaskExecution/Events/ControllerHandlers/EnableFlightModeControllerHandler.cs` +- **修改**: 继承 `INodeExecutionHandlerBase` + +#### 3.4 DisableFlightModeControllerHandler +- **文件**: `X1.Application/Features/TaskExecution/Events/ControllerHandlers/DisableFlightModeControllerHandler.cs` +- **修改**: 继承 `INodeExecutionHandlerBase` + +#### 3.5 ImsiRegistrationControllerHandler +- **文件**: `X1.Application/Features/TaskExecution/Events/ControllerHandlers/ImsiRegistrationControllerHandler.cs` +- **修改**: 继承 `INodeExecutionHandlerBase` + +## 事件流程 + +修复后的事件流程: + +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` +- 保持了原有的业务逻辑不变,只是改变了事件类型 diff --git a/src/modify_20250121_starttaskexecution_optimization.md b/src/modify_20250121_starttaskexecution_optimization.md new file mode 100644 index 0000000..2b0f2b4 --- /dev/null +++ b/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> +{ + public string TaskId { get; set; } = null!; + public string ExecutorId { get; set; } = null!; // 需要移除 +} + +// 优化后 +public class StartTaskExecutionCommand : IRequest> +{ + public string TaskId { get; set; } = null!; +} +``` + +#### 2. Handler 中的用户ID获取 +```csharp +public async Task> Handle(StartTaskExecutionCommand request, CancellationToken cancellationToken) +{ + try + { + // 获取当前用户ID + var currentUserId = _currentUserService.GetCurrentUserId(); + if (string.IsNullOrEmpty(currentUserId)) + { + _logger.LogWarning("无法获取当前用户ID,任务ID: {TaskId}", request.TaskId); + return OperationResult.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(); +``` + +### 测试建议 + +#### 1. 单元测试 +- 测试用户ID为空时的错误处理 +- 测试正常用户ID获取和任务执行流程 +- 测试异常情况下的错误处理 + +#### 2. 集成测试 +- 测试完整的任务启动流程 +- 测试用户认证失败的情况 +- 测试前后端接口的兼容性 + +### 总结 + +本次优化通过移除 ExecutorId 参数,改为在 Handler 中获取当前用户ID,实现了: + +1. ✅ **安全性提升**: 防止用户伪造执行人ID +2. ✅ **代码简化**: 减少前后端参数传递 +3. ✅ **架构一致**: 与其他需要用户上下文的功能保持一致 +4. ✅ **维护性**: 集中用户ID获取逻辑,便于维护 + +该优化完全符合安全最佳实践,提高了系统的安全性和可维护性。 diff --git a/src/modify_20250121_taskexecution_completion.md b/src/modify_20250121_taskexecution_completion.md new file mode 100644 index 0000000..937b765 --- /dev/null +++ b/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 的设计原则。 diff --git a/src/modify_20250121_taskexecution_controller_route_fix.md b/src/modify_20250121_taskexecution_controller_route_fix.md new file mode 100644 index 0000000..985e183 --- /dev/null +++ b/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> StartTaskExecution(...) + + [HttpPost("stop")] // POST /api/taskexecution/stop + public async Task> StopTaskExecution(...) +} +``` + +#### 2. 前端服务简化 +```typescript +class TaskExecutionService { + private readonly baseUrl = API_PATHS.TASK_EXECUTION; // '/taskexecution' + + // 只保留与 Controller 对应的方法 + async startTaskExecution(request: StartTaskExecutionRequest): Promise> { + return httpClient.post(`${this.baseUrl}/start`, request); + } + + async stopTaskExecution(request: StopTaskExecutionRequest): Promise> { + return httpClient.post(`${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 都符合项目的架构标准,具有良好的一致性和可维护性。 diff --git a/src/modify_20250121_taskexecution_service_architecture_fix.md b/src/modify_20250121_taskexecution_service_architecture_fix.md new file mode 100644 index 0000000..7b40873 --- /dev/null +++ b/src/modify_20250121_taskexecution_service_architecture_fix.md @@ -0,0 +1,186 @@ +## 2025-01-21 修复 TaskExecutionService 架构设计 + +### 概述 + +修复 TaskExecutionService 的架构设计,使其符合当前项目的设计模式,参考 testScenarioTaskService.ts 的实现架构。 + +### 问题分析 + +#### 原始问题 +- 使用了不存在的 `apiClient` 导入 +- 没有遵循当前项目的架构模式 +- 响应格式与后端不一致 +- 缺少统一的错误处理机制 + +#### 架构不一致性 +- 应该使用 `httpClient` 而不是 `apiClient` +- 应该使用 `OperationResult` 统一响应格式 +- 应该使用 `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` 统一响应格式 +- **优势**: 与后端 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> { + return httpClient.post(`${this.baseUrl}/start`, request); + } + + async stopTaskExecution(request: StopTaskExecutionRequest): Promise> { + return httpClient.post(`${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` 格式 +- **统一路径**: 使用 `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> { + return httpClient.post(`${this.baseUrl}/endpoint`, request); + } +} +``` + +#### 3. 导出模式 +```typescript +// 相同的导出模式 +export const serviceName = new ServiceName(); +``` + +### 测试建议 + +#### 1. 单元测试 +- 测试服务方法的正确调用 +- 测试响应格式的正确解析 +- 测试错误情况的处理 + +#### 2. 集成测试 +- 测试与后端 API 的完整交互 +- 测试前端组件的正确集成 +- 测试错误处理和用户反馈 + +### 总结 + +本次修复通过以下方式解决了架构不一致问题: + +1. ✅ **修复导入**: 使用正确的 `httpClient` 和类型定义 +2. ✅ **统一响应**: 使用 `OperationResult` 格式 +3. ✅ **路径管理**: 使用 `API_PATHS` 常量 +4. ✅ **类型安全**: 完整的 TypeScript 类型定义 +5. ✅ **架构一致**: 与 `testScenarioTaskService.ts` 保持完全一致 + +现在 TaskExecutionService 完全符合当前项目的架构设计,具有良好的一致性、可维护性和扩展性。