From 36f6ec6fb245610a167256764be4954c9e00b85b Mon Sep 17 00:00:00 2001 From: root Date: Fri, 5 Sep 2025 18:45:12 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E5=9C=BA=E6=99=AF=E4=BB=BB=E5=8A=A1=E6=89=A7=E8=A1=8C=E8=BF=87?= =?UTF-8?q?=E7=A8=8B=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 主要功能: - 新增 TestScenarioTaskTree Feature,提供任务执行过程的树形结构查询 - 实现 TestScenarioTaskExecutionController 控制器,支持任务执行过程管理 - 新增步骤详情查询功能,支持通过 CaseDetailId 获取步骤执行日志 - 为 GetTestScenarioTasksQuery 添加 ExecutionStatus 查询条件过滤 技术实现: - 遵循 CQRS 模式,使用 MediatR 进行查询处理 - 实现三层树形结构:任务 → 执行详情 → 用例详情 - 支持按场景、设备、执行状态等条件过滤 - 前端实现响应式布局的任务执行过程管理界面 文件变更: - 新增:TestScenarioTaskTree Feature 相关文件 - 新增:TestScenarioTaskExecutionController 控制器 - 新增:前端任务执行过程管理页面和组件 - 修改:GetTestScenarioTasksQuery 添加执行状态过滤 - 更新:导航菜单和路由配置 优化内容: - 任务列表显示优化,移除冗余字段 - 步骤日志显示优化,简化信息展示 - 性能优化,支持按需加载执行和用例详情 --- .../GetTestScenarioTaskStepDetailsQuery.cs | 35 ++ ...TestScenarioTaskStepDetailsQueryHandler.cs | 171 +++++++ .../GetTestScenarioTaskStepDetailsResponse.cs | 124 ++++++ .../GetTestScenarioTaskTreeQuery.cs | 33 ++ .../GetTestScenarioTaskTreeQueryHandler.cs | 204 +++++++++ .../GetTestScenarioTaskTreeResponse.cs | 245 ++++++++++ .../GetTestScenarioTasksQuery.cs | 5 + .../GetTestScenarioTasksQueryHandler.cs | 1 + .../TestTask/ITestScenarioTaskRepository.cs | 2 + .../TestTask/TestScenarioTaskRepository.cs | 31 +- .../TestScenarioTaskExecutionController.cs | 118 +++++ .../src/components/ui/collapsible.tsx | 105 +++++ src/X1.WebUI/src/constants/api.ts | 3 + .../src/constants/navigationMenuPresets.ts | 9 + .../NavigationMenuForm_Examples.md | 18 +- .../StepDetailsView.tsx | 338 ++++++++++++++ .../TaskExecutionProcessView.tsx | 199 +++++++++ .../TaskExecutionTable.tsx | 354 +++++++++++++++ .../TaskExecutionTree.tsx | 279 ++++++++++++ src/X1.WebUI/src/routes/AppRouter.tsx | 8 + .../testScenarioTaskExecutionService.ts | 185 ++++++++ src/modify.md | 5 +- ...modify_20250121_execution_status_filter.md | 86 ++++ ...50121_testscenariotaskexecution_feature.md | 420 ++++++++++++++++++ ...y_20250121_testscenariotasktree_feature.md | 228 ++++++++++ 25 files changed, 3202 insertions(+), 4 deletions(-) create mode 100644 src/X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskStepDetails/GetTestScenarioTaskStepDetailsQuery.cs create mode 100644 src/X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskStepDetails/GetTestScenarioTaskStepDetailsQueryHandler.cs create mode 100644 src/X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskStepDetails/GetTestScenarioTaskStepDetailsResponse.cs create mode 100644 src/X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskTree/GetTestScenarioTaskTreeQuery.cs create mode 100644 src/X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskTree/GetTestScenarioTaskTreeQueryHandler.cs create mode 100644 src/X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskTree/GetTestScenarioTaskTreeResponse.cs create mode 100644 src/X1.Presentation/Controllers/TestScenarioTaskExecutionController.cs create mode 100644 src/X1.WebUI/src/components/ui/collapsible.tsx create mode 100644 src/X1.WebUI/src/pages/task-execution-process/StepDetailsView.tsx create mode 100644 src/X1.WebUI/src/pages/task-execution-process/TaskExecutionProcessView.tsx create mode 100644 src/X1.WebUI/src/pages/task-execution-process/TaskExecutionTable.tsx create mode 100644 src/X1.WebUI/src/pages/task-execution-process/TaskExecutionTree.tsx create mode 100644 src/X1.WebUI/src/services/testScenarioTaskExecutionService.ts create mode 100644 src/modify_20250121_execution_status_filter.md create mode 100644 src/modify_20250121_testscenariotaskexecution_feature.md create mode 100644 src/modify_20250121_testscenariotasktree_feature.md diff --git a/src/X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskStepDetails/GetTestScenarioTaskStepDetailsQuery.cs b/src/X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskStepDetails/GetTestScenarioTaskStepDetailsQuery.cs new file mode 100644 index 0000000..c445793 --- /dev/null +++ b/src/X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskStepDetails/GetTestScenarioTaskStepDetailsQuery.cs @@ -0,0 +1,35 @@ +using MediatR; +using X1.Application.Features.TestScenarioTaskTree.Queries.GetTestScenarioTaskStepDetails; + +namespace X1.Application.Features.TestScenarioTaskTree.Queries.GetTestScenarioTaskStepDetails; + +/// +/// 获取测试场景任务步骤详情查询 +/// +public class GetTestScenarioTaskStepDetailsQuery : IRequest> +{ + /// + /// 用例执行明细ID + /// + public string CaseDetailId { get; set; } = null!; + + /// + /// 是否包含步骤日志 + /// + public bool IncludeStepLogs { get; set; } = true; + + /// + /// 日志类型过滤(可选) + /// + public string? LogTypeFilter { get; set; } + + /// + /// 开始时间过滤(可选) + /// + public DateTime? StartTimeFilter { get; set; } + + /// + /// 结束时间过滤(可选) + /// + public DateTime? EndTimeFilter { get; set; } +} diff --git a/src/X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskStepDetails/GetTestScenarioTaskStepDetailsQueryHandler.cs b/src/X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskStepDetails/GetTestScenarioTaskStepDetailsQueryHandler.cs new file mode 100644 index 0000000..5a8866d --- /dev/null +++ b/src/X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskStepDetails/GetTestScenarioTaskStepDetailsQueryHandler.cs @@ -0,0 +1,171 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using Microsoft.Extensions.Logging; +using X1.Domain.Common; +using X1.Domain.Common.Enums; +using X1.Domain.Entities.TestTask; +using X1.Domain.Repositories.TestTask; + +namespace X1.Application.Features.TestScenarioTaskTree.Queries.GetTestScenarioTaskStepDetails; + +/// +/// 获取测试场景任务步骤详情查询处理器 +/// +public class GetTestScenarioTaskStepDetailsQueryHandler : IRequestHandler> +{ + private readonly ITestScenarioTaskExecutionCaseStepDetailRepository _stepDetailRepository; + private readonly ITestScenarioTaskExecutionStepLogRepository _stepLogRepository; + private readonly ILogger _logger; + + /// + /// 初始化处理器 + /// + public GetTestScenarioTaskStepDetailsQueryHandler( + ITestScenarioTaskExecutionCaseStepDetailRepository stepDetailRepository, + ITestScenarioTaskExecutionStepLogRepository stepLogRepository, + ILogger logger) + { + _stepDetailRepository = stepDetailRepository; + _stepLogRepository = stepLogRepository; + _logger = logger; + } + + /// + /// 处理获取测试场景任务步骤详情查询 + /// + public async Task> Handle( + GetTestScenarioTaskStepDetailsQuery request, + CancellationToken cancellationToken) + { + try + { + _logger.LogInformation("开始获取测试场景任务步骤详情,用例明细ID: {CaseDetailId}", request.CaseDetailId); + + // 1. 根据CaseDetailId获取StepId列表 + var stepDetails = await _stepDetailRepository.GetByCaseDetailIdAsync(request.CaseDetailId, cancellationToken); + var stepDetailList = stepDetails.ToList(); + + if (!stepDetailList.Any()) + { + _logger.LogWarning("未找到用例明细ID为 {CaseDetailId} 的步骤详情", request.CaseDetailId); + return OperationResult.CreateSuccess( + new GetTestScenarioTaskStepDetailsResponse()); + } + + // 2. 构建步骤详情树 + var stepDetailDtos = new List(); + var totalLogCount = 0; + + foreach (var stepDetail in stepDetailList) + { + var stepDetailDto = await BuildStepDetailDtoAsync(stepDetail, request, cancellationToken); + stepDetailDtos.Add(stepDetailDto); + totalLogCount += stepDetailDto.LogCount; + } + + // 3. 构建响应 + var response = new GetTestScenarioTaskStepDetailsResponse + { + StepDetails = stepDetailDtos, + TotalStepCount = stepDetailList.Count, + TotalLogCount = totalLogCount + }; + + _logger.LogInformation("获取测试场景任务步骤详情成功,步骤数量: {StepCount}, 日志数量: {LogCount}", + response.TotalStepCount, response.TotalLogCount); + + return OperationResult.CreateSuccess(response); + } + catch (Exception ex) + { + _logger.LogError(ex, "获取测试场景任务步骤详情时发生错误"); + return OperationResult.CreateFailure( + new List { "获取测试场景任务步骤详情失败" }); + } + } + + /// + /// 构建步骤详情DTO + /// + private async Task BuildStepDetailDtoAsync( + TestScenarioTaskExecutionCaseStepDetail stepDetail, + GetTestScenarioTaskStepDetailsQuery request, + CancellationToken cancellationToken) + { + var stepDetailDto = new TestScenarioTaskStepDetailDto + { + StepDetailId = stepDetail.Id, + CaseDetailId = stepDetail.CaseDetailId, + StepId = stepDetail.StepId, + StepName = stepDetail.StepName, + ExecutorId = stepDetail.ExecutorId, + Status = stepDetail.Status.ToString(), + StartTime = stepDetail.StartTime, + EndTime = stepDetail.EndTime, + Duration = stepDetail.Duration, + Loop = stepDetail.Loop, + StepLogs = new List() + }; + + // 如果不需要包含步骤日志,直接返回 + if (!request.IncludeStepLogs) + { + return stepDetailDto; + } + + // 获取步骤日志 + var stepLogs = await GetStepLogsAsync(stepDetail.Id, request, cancellationToken); + var stepLogList = stepLogs.ToList(); + + foreach (var stepLog in stepLogList) + { + var stepLogDto = new TestScenarioTaskStepLogDto + { + LogId = stepLog.Id, + StepDetailId = stepLog.StepDetailId, + LogType = stepLog.LogType.ToString(), + LogTypeDescription = stepLog.GetLogTypeDescription(), + Content = stepLog.Content, + CreatedTime = stepLog.CreatedTime + }; + + stepDetailDto.StepLogs.Add(stepLogDto); + } + + stepDetailDto.LogCount = stepLogList.Count; + return stepDetailDto; + } + + /// + /// 获取步骤日志 + /// + private async Task> GetStepLogsAsync( + string stepDetailId, + GetTestScenarioTaskStepDetailsQuery request, + CancellationToken cancellationToken) + { + // 解析日志类型过滤 + StepLogType? logTypeFilter = null; + if (!string.IsNullOrEmpty(request.LogTypeFilter)) + { + if (Enum.TryParse(request.LogTypeFilter, out var parsedLogType)) + { + logTypeFilter = parsedLogType; + } + } + + // 使用条件查询获取日志 + var logs = await _stepLogRepository.GetByConditionsAsync( + stepDetailId: stepDetailId, + logType: logTypeFilter, + startTime: request.StartTimeFilter, + endTime: request.EndTimeFilter, + cancellationToken: cancellationToken); + + return logs.OrderBy(x => x.CreatedTime); + } +} diff --git a/src/X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskStepDetails/GetTestScenarioTaskStepDetailsResponse.cs b/src/X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskStepDetails/GetTestScenarioTaskStepDetailsResponse.cs new file mode 100644 index 0000000..3aa3b64 --- /dev/null +++ b/src/X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskStepDetails/GetTestScenarioTaskStepDetailsResponse.cs @@ -0,0 +1,124 @@ +namespace X1.Application.Features.TestScenarioTaskTree.Queries.GetTestScenarioTaskStepDetails; + +/// +/// 获取测试场景任务步骤详情响应 +/// +public class GetTestScenarioTaskStepDetailsResponse +{ + /// + /// 步骤详情列表 + /// + public List StepDetails { get; set; } = new(); + + /// + /// 总步骤数量 + /// + public int TotalStepCount { get; set; } + + /// + /// 总日志数量 + /// + public int TotalLogCount { get; set; } +} + +/// +/// 测试场景任务步骤详情数据传输对象 +/// +public class TestScenarioTaskStepDetailDto +{ + /// + /// 步骤执行明细ID + /// + public string StepDetailId { get; set; } = null!; + + /// + /// 关联用例执行明细ID + /// + public string CaseDetailId { get; set; } = null!; + + /// + /// 对应步骤ID + /// + public string StepId { get; set; } = null!; + + /// + /// 步骤名称 + /// + public string? StepName { get; set; } + + /// + /// 实际执行人/终端ID + /// + public string ExecutorId { get; set; } = null!; + + /// + /// 执行状态 + /// + public string Status { get; set; } = null!; + + /// + /// 步骤开始时间 + /// + public DateTime? StartTime { get; set; } + + /// + /// 步骤结束时间 + /// + public DateTime? EndTime { get; set; } + + /// + /// 执行耗时(秒,保留3位小数) + /// + public decimal Duration { get; set; } + + /// + /// 循环次数 + /// + public int Loop { get; set; } + + /// + /// 步骤执行日志列表 + /// + public List StepLogs { get; set; } = new(); + + /// + /// 日志数量 + /// + public int LogCount { get; set; } +} + +/// +/// 测试场景任务步骤日志数据传输对象 +/// +public class TestScenarioTaskStepLogDto +{ + /// + /// 日志ID + /// + public string LogId { get; set; } = null!; + + /// + /// 关联步骤执行明细ID + /// + public string StepDetailId { get; set; } = null!; + + /// + /// 日志类型 + /// + public string LogType { get; set; } = null!; + + /// + /// 日志类型描述 + /// + public string LogTypeDescription { get; set; } = null!; + + /// + /// 日志内容或输出信息 + /// + public string? Content { get; set; } + + /// + /// 日志生成时间 + /// + public DateTime CreatedTime { get; set; } +} diff --git a/src/X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskTree/GetTestScenarioTaskTreeQuery.cs b/src/X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskTree/GetTestScenarioTaskTreeQuery.cs new file mode 100644 index 0000000..3a0032d --- /dev/null +++ b/src/X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskTree/GetTestScenarioTaskTreeQuery.cs @@ -0,0 +1,33 @@ +using X1.Domain.Common; +using MediatR; +using System.ComponentModel.DataAnnotations; + +namespace X1.Application.Features.TestScenarioTaskTree.Queries.GetTestScenarioTaskTree; + +/// +/// 获取测试场景任务树查询 +/// +public class GetTestScenarioTaskTreeQuery : IRequest> +{ + /// + /// 场景编码过滤 + /// + [MaxLength(50)] + public string? ScenarioCode { get; set; } + + /// + /// 设备编码过滤 + /// + [MaxLength(50)] + public string? DeviceCode { get; set; } + + /// + /// 是否包含执行详情 + /// + public bool IncludeExecutionDetails { get; set; } = true; + + /// + /// 是否包含用例详情 + /// + public bool IncludeCaseDetails { get; set; } = true; +} diff --git a/src/X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskTree/GetTestScenarioTaskTreeQueryHandler.cs b/src/X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskTree/GetTestScenarioTaskTreeQueryHandler.cs new file mode 100644 index 0000000..213c15d --- /dev/null +++ b/src/X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskTree/GetTestScenarioTaskTreeQueryHandler.cs @@ -0,0 +1,204 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using Microsoft.Extensions.Logging; +using X1.Domain.Common; +using X1.Domain.Entities.TestTask; +using X1.Domain.Repositories.TestTask; + +namespace X1.Application.Features.TestScenarioTaskTree.Queries.GetTestScenarioTaskTree; + +/// +/// 获取测试场景任务树查询处理器 +/// +public class GetTestScenarioTaskTreeQueryHandler : IRequestHandler> +{ + private readonly ITestScenarioTaskRepository _testScenarioTaskRepository; + private readonly ITestScenarioTaskExecutionDetailRepository _executionDetailRepository; + private readonly ITestScenarioTaskExecutionCaseDetailRepository _caseDetailRepository; + private readonly ILogger _logger; + + /// + /// 初始化处理器 + /// + public GetTestScenarioTaskTreeQueryHandler( + ITestScenarioTaskRepository testScenarioTaskRepository, + ITestScenarioTaskExecutionDetailRepository executionDetailRepository, + ITestScenarioTaskExecutionCaseDetailRepository caseDetailRepository, + ILogger logger) + { + _testScenarioTaskRepository = testScenarioTaskRepository; + _executionDetailRepository = executionDetailRepository; + _caseDetailRepository = caseDetailRepository; + _logger = logger; + } + + /// + /// 处理获取测试场景任务树查询 + /// + public async Task> Handle( + GetTestScenarioTaskTreeQuery request, + CancellationToken cancellationToken) + { + try + { + _logger.LogInformation("开始获取测试场景任务树,场景编码: {ScenarioCode}, 设备编码: {DeviceCode}", + request.ScenarioCode, request.DeviceCode); + + // 1. 获取任务列表 + var tasks = await _testScenarioTaskRepository.GetByConditionsAsync( + scenarioCode: request.ScenarioCode, + deviceCode: request.DeviceCode, + cancellationToken: cancellationToken); + + var taskList = tasks.ToList(); + if (!taskList.Any()) + { + _logger.LogWarning("未找到符合条件的测试场景任务"); + return OperationResult.CreateSuccess( + new GetTestScenarioTaskTreeResponse()); + } + + // 2. 构建任务树 + var taskTrees = new List(); + var totalExecutionCount = 0; + var totalCaseCount = 0; + + foreach (var task in taskList) + { + var taskTree = await BuildTaskTreeAsync(task, request, cancellationToken); + taskTrees.Add(taskTree); + totalExecutionCount += taskTree.ExecutionCount; + totalCaseCount += taskTree.Executions.Sum(e => e.CaseCount); + } + + // 3. 构建响应 + var response = new GetTestScenarioTaskTreeResponse + { + TaskTrees = taskTrees, + TotalTaskCount = taskList.Count, + TotalExecutionCount = totalExecutionCount, + TotalCaseCount = totalCaseCount + }; + + _logger.LogInformation("获取测试场景任务树成功,任务数量: {TaskCount}, 执行数量: {ExecutionCount}, 用例数量: {CaseCount}", + response.TotalTaskCount, response.TotalExecutionCount, response.TotalCaseCount); + + return OperationResult.CreateSuccess(response); + } + catch (Exception ex) + { + _logger.LogError(ex, "获取测试场景任务树时发生错误"); + return OperationResult.CreateFailure( + new List { "获取测试场景任务树失败" }); + } + } + + /// + /// 构建单个任务树 + /// + private async Task BuildTaskTreeAsync( + TestScenarioTask task, + GetTestScenarioTaskTreeQuery request, + CancellationToken cancellationToken) + { + var taskTree = new TestScenarioTaskTreeDto + { + TaskId = task.Id, + TaskCode = task.TaskCode, + TaskName = task.TaskName, + ScenarioCode = task.ScenarioCode, + DeviceCode = task.DeviceCode, + Description = task.Description, + Priority = task.Priority, + IsDisabled = task.IsDisabled, + Tags = task.Tags, + Lifecycle = task.Lifecycle.ToString(), + TemplateId = task.TemplateId, + CreatedAt = task.CreatedAt, + CreatedBy = task.CreatedBy, + UpdatedAt = task.UpdatedAt, + UpdatedBy = task.UpdatedBy, + Executions = new List() + }; + + // 如果不需要包含执行详情,直接返回 + if (!request.IncludeExecutionDetails) + { + return taskTree; + } + + // 获取执行详情 + var executions = await _executionDetailRepository.GetByTaskIdAsync(task.Id, cancellationToken); + var executionList = executions.ToList(); + + foreach (var execution in executionList) + { + var executionTree = await BuildExecutionTreeAsync(execution, request, cancellationToken); + taskTree.Executions.Add(executionTree); + } + + taskTree.ExecutionCount = executionList.Count; + return taskTree; + } + + /// + /// 构建执行树 + /// + private async Task BuildExecutionTreeAsync( + TestScenarioTaskExecutionDetail execution, + GetTestScenarioTaskTreeQuery request, + CancellationToken cancellationToken) + { + var executionTree = new TaskExecutionTreeDto + { + ExecutionId = execution.Id, + TaskId = execution.TaskId, + ScenarioCode = execution.ScenarioCode, + ExecutorId = execution.ExecutorId, + Status = execution.Status.ToString(), + StartTime = execution.StartTime, + EndTime = execution.EndTime, + Duration = execution.Duration, + Loop = execution.Loop, + Progress = execution.Progress, + RuntimeCode = execution.RuntimeCode, + CaseDetails = new List() + }; + + // 如果不需要包含用例详情,直接返回 + if (!request.IncludeCaseDetails) + { + return executionTree; + } + + // 获取用例执行详情 + var caseDetails = await _caseDetailRepository.GetByExecutionIdAsync(execution.Id, cancellationToken); + var caseDetailList = caseDetails.ToList(); + + foreach (var caseDetail in caseDetailList) + { + var caseTree = new TaskExecutionCaseTreeDto + { + CaseDetailId = caseDetail.Id, + ExecutionId = caseDetail.ExecutionId, + ScenarioCode = caseDetail.ScenarioCode, + TestCaseFlowId = caseDetail.TestCaseFlowId, + ExecutorId = caseDetail.ExecutorId, + Status = caseDetail.Status.ToString(), + StartTime = caseDetail.StartTime, + EndTime = caseDetail.EndTime, + Duration = caseDetail.Duration, + Loop = caseDetail.Loop + }; + + executionTree.CaseDetails.Add(caseTree); + } + + executionTree.CaseCount = caseDetailList.Count; + return executionTree; + } +} diff --git a/src/X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskTree/GetTestScenarioTaskTreeResponse.cs b/src/X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskTree/GetTestScenarioTaskTreeResponse.cs new file mode 100644 index 0000000..911d610 --- /dev/null +++ b/src/X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskTree/GetTestScenarioTaskTreeResponse.cs @@ -0,0 +1,245 @@ +namespace X1.Application.Features.TestScenarioTaskTree.Queries.GetTestScenarioTaskTree; + +/// +/// 获取测试场景任务树响应 +/// +public class GetTestScenarioTaskTreeResponse +{ + /// + /// 任务树列表 + /// + public List TaskTrees { get; set; } = new(); + + /// + /// 总任务数量 + /// + public int TotalTaskCount { get; set; } + + /// + /// 总执行数量 + /// + public int TotalExecutionCount { get; set; } + + /// + /// 总用例数量 + /// + public int TotalCaseCount { get; set; } +} + +/// +/// 测试场景任务树数据传输对象 +/// +public class TestScenarioTaskTreeDto +{ + /// + /// 任务ID + /// + public string TaskId { get; set; } = null!; + + /// + /// 任务编码 + /// + public string TaskCode { get; set; } = null!; + + /// + /// 任务名称 + /// + public string TaskName { get; set; } = null!; + + /// + /// 绑定场景编码 + /// + public string ScenarioCode { get; set; } = null!; + + /// + /// 蜂窝设备编码 + /// + public string DeviceCode { get; set; } = null!; + + /// + /// 任务描述或备注 + /// + public string? Description { get; set; } + + /// + /// 任务优先级 + /// + public int Priority { get; set; } + + /// + /// 是否禁用 + /// + public bool IsDisabled { get; set; } + + /// + /// 分类标签 + /// + public string? Tags { get; set; } + + /// + /// 任务生命周期 + /// + public string Lifecycle { get; set; } = null!; + + /// + /// 引用的任务模板ID + /// + public string? TemplateId { get; set; } + + /// + /// 创建时间 + /// + public DateTime CreatedAt { get; set; } + + /// + /// 创建人 + /// + public string CreatedBy { get; set; } = null!; + + /// + /// 更新时间 + /// + public DateTime? UpdatedAt { get; set; } + + /// + /// 更新人 + /// + public string? UpdatedBy { get; set; } + + /// + /// 执行详情列表 + /// + public List Executions { get; set; } = new(); + + /// + /// 执行数量 + /// + public int ExecutionCount { get; set; } +} + +/// +/// 任务执行树数据传输对象 +/// +public class TaskExecutionTreeDto +{ + /// + /// 执行ID + /// + public string ExecutionId { get; set; } = null!; + + /// + /// 对应测试场景任务ID + /// + public string TaskId { get; set; } = null!; + + /// + /// 绑定场景编码 + /// + public string ScenarioCode { get; set; } = null!; + + /// + /// 实际执行人/执行终端ID + /// + public string ExecutorId { get; set; } = null!; + + /// + /// 执行状态 + /// + public string Status { get; set; } = null!; + + /// + /// 执行开始时间 + /// + public DateTime? StartTime { get; set; } + + /// + /// 执行结束时间 + /// + public DateTime? EndTime { get; set; } + + /// + /// 执行时长(秒,保留3位小数) + /// + public decimal Duration { get; set; } + + /// + /// 执行轮次/循环次数 + /// + public int Loop { get; set; } + + /// + /// 执行进度百分比 (0-100) + /// + public int Progress { get; set; } + + /// + /// 蜂窝设备运行时编码 + /// + public string? RuntimeCode { get; set; } + + /// + /// 用例执行详情列表 + /// + public List CaseDetails { get; set; } = new(); + + /// + /// 用例数量 + /// + public int CaseCount { get; set; } +} + +/// +/// 任务执行用例树数据传输对象 +/// +public class TaskExecutionCaseTreeDto +{ + /// + /// 用例执行ID + /// + public string CaseDetailId { get; set; } = null!; + + /// + /// 对应任务执行记录ID + /// + public string ExecutionId { get; set; } = null!; + + /// + /// 绑定场景编码 + /// + public string ScenarioCode { get; set; } = null!; + + /// + /// 测试用例流程ID + /// + public string TestCaseFlowId { get; set; } = null!; + + /// + /// 实际执行人/终端ID + /// + public string ExecutorId { get; set; } = null!; + + /// + /// 执行状态 + /// + public string Status { get; set; } = null!; + + /// + /// 开始时间 + /// + public DateTime? StartTime { get; set; } + + /// + /// 结束时间 + /// + public DateTime? EndTime { get; set; } + + /// + /// 执行耗时(秒,保留3位小数) + /// + public decimal Duration { get; set; } + + /// + /// 执行轮次 + /// + public int Loop { get; set; } +} diff --git a/src/X1.Application/Features/TestScenarioTasks/Queries/GetTestScenarioTasks/GetTestScenarioTasksQuery.cs b/src/X1.Application/Features/TestScenarioTasks/Queries/GetTestScenarioTasks/GetTestScenarioTasksQuery.cs index e967aa0..22b58c8 100644 --- a/src/X1.Application/Features/TestScenarioTasks/Queries/GetTestScenarioTasks/GetTestScenarioTasksQuery.cs +++ b/src/X1.Application/Features/TestScenarioTasks/Queries/GetTestScenarioTasks/GetTestScenarioTasksQuery.cs @@ -54,4 +54,9 @@ public class GetTestScenarioTasksQuery : IRequest public bool? IsDisabled { get; set; } + + /// + /// 执行状态过滤 + /// + public TaskExecutionStatus? ExecutionStatus { get; set; } } diff --git a/src/X1.Application/Features/TestScenarioTasks/Queries/GetTestScenarioTasks/GetTestScenarioTasksQueryHandler.cs b/src/X1.Application/Features/TestScenarioTasks/Queries/GetTestScenarioTasks/GetTestScenarioTasksQueryHandler.cs index b7854f0..b1c1f79 100644 --- a/src/X1.Application/Features/TestScenarioTasks/Queries/GetTestScenarioTasks/GetTestScenarioTasksQueryHandler.cs +++ b/src/X1.Application/Features/TestScenarioTasks/Queries/GetTestScenarioTasks/GetTestScenarioTasksQueryHandler.cs @@ -42,6 +42,7 @@ public class GetTestScenarioTasksQueryHandler : IRequestHandler生命周期状态过滤 /// 优先级过滤 /// 禁用状态过滤 + /// 执行状态过滤 /// 搜索关键词 /// 页码(从1开始,为null时不分页) /// 每页数量(为null时不分页) @@ -125,6 +126,7 @@ namespace X1.Domain.Repositories.TestTask TaskLifecycle? lifecycle = null, int? priority = null, bool? isDisabled = null, + TaskExecutionStatus? executionStatus = null, string? searchTerm = null, int? pageNumber = null, int? pageSize = null, diff --git a/src/X1.Infrastructure/Repositories/TestTask/TestScenarioTaskRepository.cs b/src/X1.Infrastructure/Repositories/TestTask/TestScenarioTaskRepository.cs index 4fb6345..427074c 100644 --- a/src/X1.Infrastructure/Repositories/TestTask/TestScenarioTaskRepository.cs +++ b/src/X1.Infrastructure/Repositories/TestTask/TestScenarioTaskRepository.cs @@ -153,6 +153,7 @@ namespace X1.Infrastructure.Repositories.TestTask TaskLifecycle? lifecycle = null, int? priority = null, bool? isDisabled = null, + TaskExecutionStatus? executionStatus = null, string? searchTerm = null, int? pageNumber = null, int? pageSize = null, @@ -254,6 +255,14 @@ namespace X1.Infrastructure.Repositories.TestTask paramIndex++; } + // 按执行状态过滤 + if (executionStatus.HasValue) + { + baseSql += $" AND ted.\"Status\" = {{{paramIndex}}}"; + parameters.Add(executionStatus.Value.ToString()); + paramIndex++; + } + // 按搜索关键词过滤 if (!string.IsNullOrWhiteSpace(searchTerm)) { @@ -268,8 +277,18 @@ namespace X1.Infrastructure.Repositories.TestTask // 如果启用了分页,先获取总数量 if (pageNumber.HasValue && pageSize.HasValue) { - // 构建简化的 COUNT 查询,只统计主表记录数量 - var countSql = @" + // 构建 COUNT 查询,如果需要按执行状态过滤则需要 JOIN 执行状态表 + var countSql = executionStatus.HasValue ? @" + SELECT COUNT(DISTINCT tst.""Id"") + FROM ""tb_testscenario_task"" tst + LEFT JOIN ( + SELECT DISTINCT ON (""TaskId"") + ""TaskId"", + ""Status"" + FROM ""tb_testscenario_task_execution_detail"" + ORDER BY ""TaskId"", ""StartTime"" DESC NULLS LAST + ) ted ON tst.""Id"" = ted.""TaskId"" + WHERE 1=1" : @" SELECT COUNT(*) FROM ""tb_testscenario_task"" tst WHERE 1=1"; @@ -317,6 +336,14 @@ namespace X1.Infrastructure.Repositories.TestTask countParamIndex++; } + // 按执行状态过滤 + if (executionStatus.HasValue) + { + countSql += $" AND ted.\"Status\" = {{{countParamIndex}}}"; + countParameters.Add(executionStatus.Value.ToString()); + countParamIndex++; + } + // 按搜索关键词过滤 if (!string.IsNullOrWhiteSpace(searchTerm)) { diff --git a/src/X1.Presentation/Controllers/TestScenarioTaskExecutionController.cs b/src/X1.Presentation/Controllers/TestScenarioTaskExecutionController.cs new file mode 100644 index 0000000..1432f5c --- /dev/null +++ b/src/X1.Presentation/Controllers/TestScenarioTaskExecutionController.cs @@ -0,0 +1,118 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using X1.Application.Features.TestScenarioTaskTree.Queries.GetTestScenarioTaskTree; +using X1.Application.Features.TestScenarioTaskTree.Queries.GetTestScenarioTaskStepDetails; +using X1.Presentation.Abstractions; +using X1.Domain.Common; +using MediatR; +using Microsoft.Extensions.Logging; + +namespace X1.Presentation.Controllers; + +/// +/// 测试场景任务执行过程控制器 +/// +[ApiController] +[Route("api/testscenariotaskexecution")] +[Authorize] +public class TestScenarioTaskExecutionController : ApiController +{ + private readonly ILogger _logger; + + /// + /// 初始化测试场景任务执行过程控制器 + /// + public TestScenarioTaskExecutionController(IMediator mediator, ILogger logger) + : base(mediator) + { + _logger = logger; + } + + /// + /// 获取测试场景任务执行过程 + /// + /// 场景编码过滤 + /// 设备编码过滤 + /// 是否包含执行详情 + /// 是否包含用例详情 + /// 测试场景任务执行过程 + [HttpGet] + public async Task> GetTestScenarioTaskExecution( + [FromQuery] string? scenarioCode = null, + [FromQuery] string? deviceCode = null, + [FromQuery] bool includeExecutionDetails = true, + [FromQuery] bool includeCaseDetails = true) + { + _logger.LogInformation("获取测试场景任务执行过程,场景编码: {ScenarioCode}, 设备编码: {DeviceCode}, 包含执行详情: {IncludeExecutionDetails}, 包含用例详情: {IncludeCaseDetails}", + scenarioCode, deviceCode, includeExecutionDetails, includeCaseDetails); + + var query = new GetTestScenarioTaskTreeQuery + { + ScenarioCode = scenarioCode, + DeviceCode = deviceCode, + IncludeExecutionDetails = includeExecutionDetails, + IncludeCaseDetails = includeCaseDetails + }; + + var result = await mediator.Send(query); + if (!result.IsSuccess) + { + _logger.LogWarning("获取测试场景任务执行过程失败: {ErrorMessages}", + string.Join(", ", result.ErrorMessages ?? new List())); + return result; + } + + _logger.LogInformation("成功获取测试场景任务执行过程,任务数量: {TaskCount}, 执行数量: {ExecutionCount}, 用例数量: {CaseCount}", + result.Data?.TotalTaskCount, result.Data?.TotalExecutionCount, result.Data?.TotalCaseCount); + return result; + } + + /// + /// 获取测试场景任务步骤详情 + /// + /// 用例执行明细ID + /// 是否包含步骤日志 + /// 日志类型过滤 + /// 开始时间过滤 + /// 结束时间过滤 + /// 测试场景任务步骤详情 + [HttpGet("step-details")] + public async Task> GetTestScenarioTaskStepDetails( + [FromQuery] string caseDetailId, + [FromQuery] bool includeStepLogs = true, + [FromQuery] string? logTypeFilter = null, + [FromQuery] DateTime? startTimeFilter = null, + [FromQuery] DateTime? endTimeFilter = null) + { + _logger.LogInformation("获取测试场景任务步骤详情,用例明细ID: {CaseDetailId}, 包含步骤日志: {IncludeStepLogs}, 日志类型过滤: {LogTypeFilter}", + caseDetailId, includeStepLogs, logTypeFilter); + + if (string.IsNullOrEmpty(caseDetailId)) + { + _logger.LogWarning("用例明细ID不能为空"); + return OperationResult.CreateFailure( + new List { "用例明细ID不能为空" }); + } + + var query = new GetTestScenarioTaskStepDetailsQuery + { + CaseDetailId = caseDetailId, + IncludeStepLogs = includeStepLogs, + LogTypeFilter = logTypeFilter, + StartTimeFilter = startTimeFilter, + EndTimeFilter = endTimeFilter + }; + + var result = await mediator.Send(query); + if (!result.IsSuccess) + { + _logger.LogWarning("获取测试场景任务步骤详情失败: {ErrorMessages}", + string.Join(", ", result.ErrorMessages ?? new List())); + return result; + } + + _logger.LogInformation("成功获取测试场景任务步骤详情,步骤数量: {StepCount}, 日志数量: {LogCount}", + result.Data?.TotalStepCount, result.Data?.TotalLogCount); + return result; + } +} diff --git a/src/X1.WebUI/src/components/ui/collapsible.tsx b/src/X1.WebUI/src/components/ui/collapsible.tsx new file mode 100644 index 0000000..31620d0 --- /dev/null +++ b/src/X1.WebUI/src/components/ui/collapsible.tsx @@ -0,0 +1,105 @@ +import React, { createContext, useContext, useState } from 'react'; +import { cn } from '@/lib/utils'; + +interface CollapsibleContextValue { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +const CollapsibleContext = createContext(undefined); + +interface CollapsibleProps { + open?: boolean; + onOpenChange?: (open: boolean) => void; + children: React.ReactNode; + className?: string; +} + +export const Collapsible: React.FC = ({ + open, + onOpenChange, + children, + className +}) => { + const [internalOpen, setInternalOpen] = useState(false); + + const isOpen = open !== undefined ? open : internalOpen; + const handleOpenChange = onOpenChange || setInternalOpen; + + return ( + +
+ {children} +
+
+ ); +}; + +interface CollapsibleTriggerProps { + asChild?: boolean; + children: React.ReactNode; + className?: string; +} + +export const CollapsibleTrigger: React.FC = ({ + asChild, + children, + className +}) => { + const context = useContext(CollapsibleContext); + + if (!context) { + throw new Error('CollapsibleTrigger must be used within a Collapsible'); + } + + const { open, onOpenChange } = context; + + const handleClick = () => { + onOpenChange(!open); + }; + + if (asChild && React.isValidElement(children)) { + return React.cloneElement(children, { + onClick: handleClick, + className: cn(children.props.className, className), + }); + } + + return ( + + ); +}; + +interface CollapsibleContentProps { + children: React.ReactNode; + className?: string; +} + +export const CollapsibleContent: React.FC = ({ + children, + className +}) => { + const context = useContext(CollapsibleContext); + + if (!context) { + throw new Error('CollapsibleContent must be used within a Collapsible'); + } + + const { open } = context; + + if (!open) { + return null; + } + + return ( +
+ {children} +
+ ); +}; diff --git a/src/X1.WebUI/src/constants/api.ts b/src/X1.WebUI/src/constants/api.ts index aabe49f..04b3462 100644 --- a/src/X1.WebUI/src/constants/api.ts +++ b/src/X1.WebUI/src/constants/api.ts @@ -62,6 +62,9 @@ export const API_PATHS = { // 测试场景任务相关 TEST_SCENARIO_TASKS: '/testscenariotasks', + // 测试场景任务执行过程相关 + TEST_SCENARIO_TASK_EXECUTION: '/testscenariotaskexecution', + // 用例步骤配置相关 CASE_STEP_CONFIGS: '/casestepconfigs', diff --git a/src/X1.WebUI/src/constants/navigationMenuPresets.ts b/src/X1.WebUI/src/constants/navigationMenuPresets.ts index f0be360..fbf0684 100644 --- a/src/X1.WebUI/src/constants/navigationMenuPresets.ts +++ b/src/X1.WebUI/src/constants/navigationMenuPresets.ts @@ -198,6 +198,15 @@ export const subMenuPresets: NavigationMenuPreset[] = [ description: '执行和管理测试任务', parentTitle: '任务管理' }, + { + title: '查看任务', + path: '/dashboard/tasks/execution-process', + icon: 'ClipboardList', + permissionCode: 'testscenariotaskexecution.view', + sortOrder: 6, + description: '查看任务执行过程和结果', + parentTitle: '任务管理' + }, // 结果分析下的子菜单 { diff --git a/src/X1.WebUI/src/pages/navigation-menus/NavigationMenuForm_Examples.md b/src/X1.WebUI/src/pages/navigation-menus/NavigationMenuForm_Examples.md index 04af5e0..875496f 100644 --- a/src/X1.WebUI/src/pages/navigation-menus/NavigationMenuForm_Examples.md +++ b/src/X1.WebUI/src/pages/navigation-menus/NavigationMenuForm_Examples.md @@ -355,7 +355,7 @@ 父级菜单: [选择"任务管理"菜单的ID] 图标: ClipboardList 权限代码: taskexecutions.view -排序: 4 +排序: 5 启用状态: ✅ 系统菜单: ✅ 描述: 执行和管理测试任务 @@ -363,6 +363,21 @@ 自动识别结果: SubMenuItem(子菜单项) ``` +**查看任务** +``` +菜单标题: 查看任务 +菜单路径: /dashboard/tasks/execution-process +父级菜单: [选择"任务管理"菜单的ID] +图标: ClipboardList +权限代码: testscenariotaskexecution.view +排序: 6 +启用状态: ✅ +系统菜单: ✅ +描述: 查看任务执行过程和结果 + +自动识别结果: SubMenuItem(子菜单项) +``` + #### 3.4 结果分析下的子菜单 **功能分析** @@ -728,6 +743,7 @@ - `taskexecution`: 任务执行明细 - `taskreviews`: 任务审核 - `taskexecutions`: 任务执行 +- `testscenariotaskexecution`: 测试场景任务执行过程 - `functionalanalysis`: 功能分析 - `performanceanalysis`: 性能分析 - `issueanalysis`: 问题分析 diff --git a/src/X1.WebUI/src/pages/task-execution-process/StepDetailsView.tsx b/src/X1.WebUI/src/pages/task-execution-process/StepDetailsView.tsx new file mode 100644 index 0000000..9b0224a --- /dev/null +++ b/src/X1.WebUI/src/pages/task-execution-process/StepDetailsView.tsx @@ -0,0 +1,338 @@ +import React, { useEffect, useState } from 'react'; +import { + testScenarioTaskExecutionService, + TestScenarioTaskStepDetailDto, + TestScenarioTaskStepLogDto, + GetTestScenarioTaskStepDetailsRequest, + StepLogType +} from '@/services/testScenarioTaskExecutionService'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { useToast } from '@/components/ui/use-toast'; +import { + Play, + CheckCircle, + XCircle, + Clock, + Search, + RefreshCw, + FileText, + AlertTriangle, + Info, + Bug, + Eye, + Settings +} from 'lucide-react'; + +// 状态颜色映射 +const getStatusColor = (status: string) => { + switch (status) { + case 'Success': + return 'bg-green-100 text-green-800 border-green-200'; + case 'Failed': + return 'bg-red-100 text-red-800 border-red-200'; + case 'Running': + return 'bg-blue-100 text-blue-800 border-blue-200'; + case 'Pending': + return 'bg-yellow-100 text-yellow-800 border-yellow-200'; + default: + return 'bg-gray-100 text-gray-800 border-gray-200'; + } +}; + +// 状态图标映射 +const getStatusIcon = (status: string) => { + switch (status) { + case 'Success': + return ; + case 'Failed': + return ; + case 'Running': + return ; + case 'Pending': + return ; + default: + return ; + } +}; + +// 日志类型颜色映射 +const getLogTypeColor = (logType: StepLogType) => { + switch (logType) { + case 'Error': + return 'bg-red-100 text-red-800 border-red-200'; + case 'Success': + return 'bg-green-100 text-green-800 border-green-200'; + case 'Warn': + return 'bg-yellow-100 text-yellow-800 border-yellow-200'; + case 'Info': + return 'bg-blue-100 text-blue-800 border-blue-200'; + case 'Debug': + return 'bg-purple-100 text-purple-800 border-purple-200'; + case 'Output': + return 'bg-gray-100 text-gray-800 border-gray-200'; + case 'Parameter': + return 'bg-indigo-100 text-indigo-800 border-indigo-200'; + default: + return 'bg-gray-100 text-gray-800 border-gray-200'; + } +}; + +// 日志类型图标映射 +const getLogTypeIcon = (logType: StepLogType) => { + switch (logType) { + case 'Error': + return ; + case 'Success': + return ; + case 'Warn': + return ; + case 'Info': + return ; + case 'Debug': + return ; + case 'Output': + return ; + case 'Parameter': + return ; + default: + return ; + } +}; + +// 格式化时间 +const formatTime = (time?: string) => { + if (!time) return '-'; + return new Date(time).toLocaleString('zh-CN'); +}; + +// 格式化时长 +const formatDuration = (duration: number) => { + if (duration < 60) { + return `${duration.toFixed(2)}秒`; + } else if (duration < 3600) { + return `${(duration / 60).toFixed(2)}分钟`; + } else { + return `${(duration / 3600).toFixed(2)}小时`; + } +}; + + +// 步骤日志组件(带步骤信息) +const StepLogItemWithStepInfo: React.FC<{ + log: TestScenarioTaskStepLogDto; + stepDetail: TestScenarioTaskStepDetailDto; +}> = ({ log, stepDetail }) => { + return ( +
+
+ + {getLogTypeIcon(log.logType)} + {log.logTypeDescription} + + + {formatTime(log.createdTime)} + +
+ + {/* 步骤信息 */} +
+
+ 步骤: {stepDetail.stepName || stepDetail.stepId} + + {getStatusIcon(stepDetail.status)} + {stepDetail.status} + + 耗时: {formatDuration(stepDetail.duration)} +
+
+ + {/* 日志内容 */} + {log.content && ( +
+ {log.content} +
+ )} +
+ ); +}; + +// 主组件接口 +interface StepDetailsViewProps { + caseDetailId: string; + onClose?: () => void; +} + +// 主组件 +const StepDetailsView: React.FC = ({ caseDetailId, onClose }) => { + const [stepDetails, setStepDetails] = useState([]); + const [loading, setLoading] = useState(false); + const [searchTerm, setSearchTerm] = useState(''); + const [logTypeFilter, setLogTypeFilter] = useState('all'); + const [includeStepLogs, setIncludeStepLogs] = useState(true); + + const { toast } = useToast(); + + const fetchStepDetails = async () => { + setLoading(true); + try { + const params: GetTestScenarioTaskStepDetailsRequest = { + caseDetailId, + includeStepLogs, + logTypeFilter: logTypeFilter === 'all' ? undefined : logTypeFilter + }; + + const result = await testScenarioTaskExecutionService.getTestScenarioTaskStepDetails(params); + + if (result.isSuccess && result.data) { + setStepDetails(result.data.stepDetails); + toast({ + title: "获取成功", + description: `获取到 ${result.data.totalStepCount} 个步骤,${result.data.totalLogCount} 条日志`, + }); + } else { + const errorMessage = result.errorMessages?.join(', ') || "获取步骤详情时发生错误"; + console.error('获取步骤详情失败:', errorMessage); + toast({ + title: "获取失败", + description: errorMessage, + variant: "destructive", + }); + } + } catch (error) { + console.error('获取步骤详情异常:', error); + toast({ + title: "获取失败", + description: "网络错误,请稍后重试", + variant: "destructive", + }); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + if (caseDetailId) { + fetchStepDetails(); + } + }, [caseDetailId, includeStepLogs, logTypeFilter]); + + // 获取所有日志并按时间排序 + const allLogs = stepDetails.flatMap(stepDetail => + stepDetail.stepLogs.map(log => ({ log, stepDetail })) + ).sort((a, b) => new Date(a.log.createdTime).getTime() - new Date(b.log.createdTime).getTime()); + + // 过滤日志 + const filteredLogs = allLogs.filter(({ log, stepDetail }) => { + if (!searchTerm) return true; + + const searchLower = searchTerm.toLowerCase(); + return ( + log.content?.toLowerCase().includes(searchLower) || + log.logTypeDescription.toLowerCase().includes(searchLower) || + stepDetail.stepName?.toLowerCase().includes(searchLower) || + stepDetail.stepId.toLowerCase().includes(searchLower) || + stepDetail.executorId.toLowerCase().includes(searchLower) || + stepDetail.status.toLowerCase().includes(searchLower) + ); + }); + + return ( +
+
+
+
+

步骤日志

+

+ 用例明细ID: {caseDetailId} | 总日志数: {allLogs.length} +

+
+
+ + {onClose && ( + + )} +
+
+ + {/* 搜索和过滤 */} +
+
+
+ + setSearchTerm(e.target.value)} + className="pl-10" + /> +
+
+ + +
+
+ +
+ {loading ? ( +
+
加载中...
+
+ ) : filteredLogs.length > 0 ? ( +
+ {filteredLogs.map(({ log, stepDetail }) => ( + + ))} +
+ ) : ( +
+
+ {searchTerm ? '未找到匹配的日志' : '暂无日志数据'} +
+
+ )} +
+
+ ); +}; + +export default StepDetailsView; diff --git a/src/X1.WebUI/src/pages/task-execution-process/TaskExecutionProcessView.tsx b/src/X1.WebUI/src/pages/task-execution-process/TaskExecutionProcessView.tsx new file mode 100644 index 0000000..206421d --- /dev/null +++ b/src/X1.WebUI/src/pages/task-execution-process/TaskExecutionProcessView.tsx @@ -0,0 +1,199 @@ +import React, { useEffect, useState } from 'react'; +import { + testScenarioTaskExecutionService, + TestScenarioTaskTreeDto, + TaskExecutionCaseTreeDto, + GetTestScenarioTaskExecutionRequest +} from '@/services/testScenarioTaskExecutionService'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { useToast } from '@/components/ui/use-toast'; +import { Card, CardContent } from '@/components/ui/card'; +import TaskExecutionTree from './TaskExecutionTree'; +import StepDetailsView from './StepDetailsView'; + + +export default function TaskExecutionProcessView() { + const [taskTrees, setTaskTrees] = useState([]); + const [loading, setLoading] = useState(false); + const [selectedTask, setSelectedTask] = useState(null); + + // 步骤详情相关状态 + const [selectedCaseDetailId, setSelectedCaseDetailId] = useState(''); + + // 搜索参数 + const [scenarioCode, setScenarioCode] = useState(''); + const [deviceCode, setDeviceCode] = useState(''); + const [includeExecutionDetails, setIncludeExecutionDetails] = useState(true); + const [includeCaseDetails, setIncludeCaseDetails] = useState(true); + + // Toast 提示 + const { toast } = useToast(); + + const fetchTaskExecutionProcess = async (params: Partial = {}) => { + setLoading(true); + const queryParams: GetTestScenarioTaskExecutionRequest = { + scenarioCode: scenarioCode || undefined, + deviceCode: deviceCode || undefined, + includeExecutionDetails, + includeCaseDetails, + ...params + }; + + try { + const result = await testScenarioTaskExecutionService.getTestScenarioTaskExecution(queryParams); + if (result.isSuccess && result.data) { + setTaskTrees(result.data.taskTrees || []); + } else { + const errorMessage = result.errorMessages?.join(', ') || "获取任务执行过程时发生错误"; + console.error('获取任务执行过程失败:', errorMessage); + toast({ + title: "获取失败", + description: errorMessage, + variant: "destructive", + }); + } + } catch (error) { + console.error('获取任务执行过程异常:', error); + toast({ + title: "获取失败", + description: "网络错误,请稍后重试", + variant: "destructive", + }); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchTaskExecutionProcess(); + }, []); + + // 查询按钮 + const handleQuery = () => { + fetchTaskExecutionProcess(); + }; + + // 重置按钮 + const handleReset = () => { + setScenarioCode(''); + setDeviceCode(''); + setIncludeExecutionDetails(true); + setIncludeCaseDetails(true); + fetchTaskExecutionProcess({ + scenarioCode: undefined, + deviceCode: undefined, + includeExecutionDetails: true, + includeCaseDetails: true + }); + }; + + // 处理用例点击 + const handleCaseClick = (caseDetail: TaskExecutionCaseTreeDto) => { + setSelectedCaseDetailId(caseDetail.caseDetailId); + }; + + // 关闭步骤详情 + const handleCloseStepDetails = () => { + setSelectedCaseDetailId(''); + }; + + return ( +
+
+ {/* 搜索工具栏 */} + + +
{ + e.preventDefault(); + handleQuery(); + }} + > +
+ + ) => setScenarioCode(e.target.value)} + /> +
+
+ + ) => setDeviceCode(e.target.value)} + /> +
+
+ + +
+
+ + +
+
+
+
+ + {/* 两列布局:任务树 + 步骤详情 */} +
+ {/* 左侧:任务树 */} + + + + + {/* 右侧:步骤详情 */} + + {selectedCaseDetailId ? ( + + ) : ( +
+
+
步骤详细信息
+
请从左侧选择用例查看步骤详情
+
+
+ )} +
+
+
+
+ ); +} diff --git a/src/X1.WebUI/src/pages/task-execution-process/TaskExecutionTable.tsx b/src/X1.WebUI/src/pages/task-execution-process/TaskExecutionTable.tsx new file mode 100644 index 0000000..1bb7a8c --- /dev/null +++ b/src/X1.WebUI/src/pages/task-execution-process/TaskExecutionTable.tsx @@ -0,0 +1,354 @@ +import React, { useState } from 'react'; +import { TestScenarioTaskTreeDto, TaskExecutionTreeDto, TaskExecutionCaseTreeDto } from '@/services/testScenarioTaskExecutionService'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow +} from '@/components/ui/table'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { ChevronDown, ChevronRight, Play, CheckCircle, XCircle, Clock, User, Calendar, Search } from 'lucide-react'; + +// 状态颜色映射 +const getStatusColor = (status: string) => { + switch (status) { + case 'Success': + return 'bg-green-100 text-green-800 border-green-200'; + case 'Failed': + return 'bg-red-100 text-red-800 border-red-200'; + case 'Running': + return 'bg-blue-100 text-blue-800 border-blue-200'; + case 'Pending': + return 'bg-yellow-100 text-yellow-800 border-yellow-200'; + default: + return 'bg-gray-100 text-gray-800 border-gray-200'; + } +}; + +// 状态图标映射 +const getStatusIcon = (status: string) => { + switch (status) { + case 'Success': + return ; + case 'Failed': + return ; + case 'Running': + return ; + case 'Pending': + return ; + default: + return ; + } +}; + +// 格式化时间 +const formatTime = (time?: string) => { + if (!time) return '-'; + return new Date(time).toLocaleString('zh-CN'); +}; + +// 格式化时长 +const formatDuration = (duration: number) => { + if (duration < 60) { + return `${duration.toFixed(2)}秒`; + } else if (duration < 3600) { + return `${(duration / 60).toFixed(2)}分钟`; + } else { + return `${(duration / 3600).toFixed(2)}小时`; + } +}; + +// 表格数据类型 +type TableDataType = 'executions' | 'cases'; + +// 执行记录行组件 +const ExecutionRow: React.FC<{ + execution: TaskExecutionTreeDto; + taskName: string; + taskCode: string; +}> = ({ execution, taskName, taskCode }) => { + return ( + + {execution.executionId} + {taskName} + {taskCode} + + + {getStatusIcon(execution.status)} + {execution.status} + + + {execution.executorId} + {execution.progress}% + {execution.loop} + {execution.caseCount} + {formatTime(execution.startTime)} + {formatTime(execution.endTime)} + {formatDuration(execution.duration)} + {execution.runtimeCode || '-'} + + ); +}; + +// 用例记录行组件 +const CaseRow: React.FC<{ + caseDetail: TaskExecutionCaseTreeDto; + taskName: string; + taskCode: string; + executionId: string; +}> = ({ caseDetail, taskName, taskCode, executionId }) => { + return ( + + {caseDetail.caseDetailId} + {taskName} + {taskCode} + {executionId} + {caseDetail.testCaseFlowId} + + + {getStatusIcon(caseDetail.status)} + {caseDetail.status} + + + {caseDetail.executorId} + {caseDetail.loop} + {formatTime(caseDetail.startTime)} + {formatTime(caseDetail.endTime)} + {formatDuration(caseDetail.duration)} + + ); +}; + +// 表格组件主组件 +interface TaskExecutionTableProps { + selectedTask: TestScenarioTaskTreeDto | null; + loading: boolean; +} + +const TaskExecutionTable: React.FC = ({ + selectedTask, + loading +}) => { + const [tableType, setTableType] = useState('executions'); + const [searchTerm, setSearchTerm] = useState(''); + const [statusFilter, setStatusFilter] = useState('all'); + + // 获取执行记录数据 + const getExecutionData = () => { + if (!selectedTask) return []; + + let executions = selectedTask.executions; + + // 状态过滤 + if (statusFilter !== 'all') { + executions = executions.filter(exec => exec.status === statusFilter); + } + + // 搜索过滤 + if (searchTerm) { + executions = executions.filter(exec => + exec.executionId.toLowerCase().includes(searchTerm.toLowerCase()) || + exec.executorId.toLowerCase().includes(searchTerm.toLowerCase()) || + (exec.runtimeCode && exec.runtimeCode.toLowerCase().includes(searchTerm.toLowerCase())) + ); + } + + return executions; + }; + + // 获取用例记录数据 + const getCaseData = () => { + if (!selectedTask) return []; + + let cases: Array<{ caseDetail: TaskExecutionCaseTreeDto; executionId: string }> = []; + + selectedTask.executions.forEach(execution => { + execution.caseDetails.forEach(caseDetail => { + cases.push({ caseDetail, executionId: execution.executionId }); + }); + }); + + // 状态过滤 + if (statusFilter !== 'all') { + cases = cases.filter(item => item.caseDetail.status === statusFilter); + } + + // 搜索过滤 + if (searchTerm) { + cases = cases.filter(item => + item.caseDetail.caseDetailId.toLowerCase().includes(searchTerm.toLowerCase()) || + item.caseDetail.testCaseFlowId.toLowerCase().includes(searchTerm.toLowerCase()) || + item.caseDetail.executorId.toLowerCase().includes(searchTerm.toLowerCase()) || + item.executionId.toLowerCase().includes(searchTerm.toLowerCase()) + ); + } + + return cases; + }; + + const executionData = getExecutionData(); + const caseData = getCaseData(); + + return ( +
+
+
+
+

详细信息

+

+ {selectedTask ? `当前任务: ${selectedTask.taskName}` : '请选择任务查看详细信息'} +

+
+
+ +
+
+ + {/* 搜索和过滤 */} +
+
+
+ + setSearchTerm(e.target.value)} + className="pl-10" + /> +
+
+ + +
+
+ +
+ {loading ? ( +
+
加载中...
+
+ ) : !selectedTask ? ( +
+
请从左侧选择任务查看详细信息
+
+ ) : tableType === 'executions' ? ( + + + + 执行ID + 任务名称 + 任务编码 + 状态 + 执行人 + 进度 + 轮次 + 用例数 + 开始时间 + 结束时间 + 耗时 + 运行时编码 + + + + {executionData.length > 0 ? ( + executionData.map((execution) => ( + + )) + ) : ( + + + 暂无执行记录 + + + )} + +
+ ) : ( + + + + 用例ID + 任务名称 + 任务编码 + 执行ID + 用例流程ID + 状态 + 执行人 + 轮次 + 开始时间 + 结束时间 + 耗时 + + + + {caseData.length > 0 ? ( + caseData.map((item) => ( + + )) + ) : ( + + + 暂无用例记录 + + + )} + +
+ )} +
+
+ ); +}; + +export default TaskExecutionTable; diff --git a/src/X1.WebUI/src/pages/task-execution-process/TaskExecutionTree.tsx b/src/X1.WebUI/src/pages/task-execution-process/TaskExecutionTree.tsx new file mode 100644 index 0000000..325ad80 --- /dev/null +++ b/src/X1.WebUI/src/pages/task-execution-process/TaskExecutionTree.tsx @@ -0,0 +1,279 @@ +import React, { useState } from 'react'; +import { TestScenarioTaskTreeDto, TaskExecutionTreeDto, TaskExecutionCaseTreeDto } from '@/services/testScenarioTaskExecutionService'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; +import { ChevronDown, ChevronRight, Play, CheckCircle, XCircle, Clock, Calendar } from 'lucide-react'; + +// 状态颜色映射 +const getStatusColor = (status: string) => { + switch (status) { + case 'Success': + return 'bg-green-100 text-green-800 border-green-200'; + case 'Failed': + return 'bg-red-100 text-red-800 border-red-200'; + case 'Running': + return 'bg-blue-100 text-blue-800 border-blue-200'; + case 'Pending': + return 'bg-yellow-100 text-yellow-800 border-yellow-200'; + default: + return 'bg-gray-100 text-gray-800 border-gray-200'; + } +}; + +// 状态图标映射 +const getStatusIcon = (status: string) => { + switch (status) { + case 'Success': + return ; + case 'Failed': + return ; + case 'Running': + return ; + case 'Pending': + return ; + default: + return ; + } +}; + +// 格式化时间 +const formatTime = (time?: string) => { + if (!time) return '-'; + return new Date(time).toLocaleString('zh-CN'); +}; + +// 格式化时长 +const formatDuration = (duration: number) => { + if (duration < 60) { + return `${duration.toFixed(2)}秒`; + } else if (duration < 3600) { + return `${(duration / 60).toFixed(2)}分钟`; + } else { + return `${(duration / 3600).toFixed(2)}小时`; + } +}; + +// 用例执行详情组件 +const CaseExecutionDetail: React.FC<{ + caseDetail: TaskExecutionCaseTreeDto; + onCaseClick?: (caseDetail: TaskExecutionCaseTreeDto) => void; +}> = ({ caseDetail, onCaseClick }) => { + return ( +
onCaseClick?.(caseDetail)} + > + {/* 标题行 */} +
+ 用例执行详情 +
+ + {/* 第一行:状态和基本信息 */} +
+
+ + {getStatusIcon(caseDetail.status)} + {caseDetail.status} + + + 轮次: {caseDetail.loop} | 耗时: {formatDuration(caseDetail.duration)} + +
+
+ + {/* 第二行:时间信息和提示 */} +
+
+ + 开始: {formatTime(caseDetail.startTime)} + {caseDetail.endTime && ( + <> + | + 结束: {formatTime(caseDetail.endTime)} + + )} +
+ 点击查看步骤详情 +
+
+ ); +}; + +// 执行详情组件 +const ExecutionDetail: React.FC<{ + execution: TaskExecutionTreeDto; + onCaseClick?: (caseDetail: TaskExecutionCaseTreeDto) => void; +}> = ({ execution, onCaseClick }) => { + const [isOpen, setIsOpen] = useState(false); + + return ( + + +
+
+
+ {isOpen ? : } + + {getStatusIcon(execution.status)} + {execution.status} + +
+
+ 进度: {execution.progress}% | 轮次: {execution.loop} | 用例数: {execution.caseCount} +
+
+
+
+ + 开始: {formatTime(execution.startTime)} + 耗时: {formatDuration(execution.duration)} +
+
+ {execution.runtimeCode && ( +
+ 运行时编码: {execution.runtimeCode} +
+ )} +
+
+ +
+ {execution.caseDetails.map((caseDetail) => ( + + ))} +
+
+
+ ); +}; + +// 任务树组件 +const TaskTree: React.FC<{ + task: TestScenarioTaskTreeDto; + isSelected: boolean; + onSelect: (task: TestScenarioTaskTreeDto) => void; + onCaseClick?: (caseDetail: TaskExecutionCaseTreeDto) => void; +}> = ({ task, isSelected, onSelect, onCaseClick }) => { + const [isOpen, setIsOpen] = useState(false); + + return ( + + + + +
onSelect(task)} + > +
+ {isOpen ? : } + {task.taskName} + 编码: {task.taskCode} +
+
+ 执行: {task.executionCount} + 优先级: {task.priority === 1 ? '高' : '普通'} +
+
+
+ +
+
设备: {task.deviceCode}
+
生命周期: {task.lifecycle}
+
创建人: {task.createdBy}
+
创建时间: {formatTime(task.createdAt)}
+ {task.updatedAt &&
更新时间: {formatTime(task.updatedAt)}
} +
+ {task.description && ( +
+ 描述: {task.description} +
+ )} + {task.tags && ( +
+ 标签: + {task.tags} +
+ )} +
+
+
+ + + +
+

执行详情

+ {task.executions.length > 0 ? ( + task.executions.map((execution) => ( + + )) + ) : ( +
暂无执行记录
+ )} +
+
+
+
+
+ ); +}; + +// 树形组件主组件 +interface TaskExecutionTreeProps { + taskTrees: TestScenarioTaskTreeDto[]; + selectedTask: TestScenarioTaskTreeDto | null; + onTaskSelect: (task: TestScenarioTaskTreeDto) => void; + onCaseClick?: (caseDetail: TaskExecutionCaseTreeDto) => void; + loading: boolean; +} + +const TaskExecutionTree: React.FC = ({ + taskTrees, + selectedTask, + onTaskSelect, + onCaseClick, + loading +}) => { + return ( +
+
+

任务列表

+

点击任务查看详细信息

+
+
+ {loading ? ( +
+
加载中...
+
+ ) : taskTrees.length > 0 ? ( +
+ {taskTrees.map((task) => ( + + ))} +
+ ) : ( +
+
暂无任务数据
+
+ )} +
+
+ ); +}; + +export default TaskExecutionTree; diff --git a/src/X1.WebUI/src/routes/AppRouter.tsx b/src/X1.WebUI/src/routes/AppRouter.tsx index b239d52..6f94bab 100644 --- a/src/X1.WebUI/src/routes/AppRouter.tsx +++ b/src/X1.WebUI/src/routes/AppRouter.tsx @@ -23,6 +23,7 @@ const TestStepsView = lazy(() => import('@/pages/teststeps/TestStepsView')); // 任务管理页面 const TasksView = lazy(() => import('@/pages/tasks/TasksView')); const TaskExecutionView = lazy(() => import('@/pages/taskExecution/TaskExecutionView')); +const TaskExecutionProcessView = lazy(() => import('@/pages/task-execution-process/TaskExecutionProcessView')); // 结果分析页面 const FunctionalAnalysisView = lazy(() => import('@/pages/analysis/FunctionalAnalysisView')); @@ -184,6 +185,13 @@ export function AppRouter() { } /> + + + + + + } /> {/* 结果分析路由 */} diff --git a/src/X1.WebUI/src/services/testScenarioTaskExecutionService.ts b/src/X1.WebUI/src/services/testScenarioTaskExecutionService.ts new file mode 100644 index 0000000..f66b5ac --- /dev/null +++ b/src/X1.WebUI/src/services/testScenarioTaskExecutionService.ts @@ -0,0 +1,185 @@ +import { httpClient } from '@/lib/http-client'; +import { OperationResult } from '@/types/auth'; +import { API_PATHS } from '@/constants/api'; + +// ============================================================================ +// 类型定义 - 与后端 TestScenarioTaskTree 相关实体保持一致 +// ============================================================================ + +// 任务执行状态枚举 - 与后端 TaskExecutionStatus 保持一致 +export type TaskExecutionStatus = 'Pending' | 'Running' | 'Success' | 'Failed'; + +// 步骤日志类型枚举 - 与后端 StepLogType 保持一致 +export type StepLogType = 'Error' | 'Success' | 'Debug' | 'Info' | 'Warn' | 'Output' | 'Parameter' | 'Other'; + +// 任务执行用例树数据传输对象 +export interface TaskExecutionCaseTreeDto { + caseDetailId: string; + executionId: string; + scenarioCode: string; + testCaseFlowId: string; + executorId: string; + status: TaskExecutionStatus; + startTime?: string; + endTime?: string; + duration: number; + loop: number; +} + +// 任务执行树数据传输对象 +export interface TaskExecutionTreeDto { + executionId: string; + taskId: string; + scenarioCode: string; + executorId: string; + status: TaskExecutionStatus; + startTime?: string; + endTime?: string; + duration: number; + loop: number; + progress: number; + runtimeCode?: string; + caseDetails: TaskExecutionCaseTreeDto[]; + caseCount: number; +} + +// 测试场景任务树数据传输对象 +export interface TestScenarioTaskTreeDto { + taskId: string; + taskCode: string; + taskName: string; + scenarioCode: string; + deviceCode: string; + description?: string; + priority: number; + isDisabled: boolean; + tags?: string; + lifecycle: string; + templateId?: string; + createdAt: string; + createdBy: string; + updatedAt?: string; + updatedBy?: string; + executions: TaskExecutionTreeDto[]; + executionCount: number; +} + +// ============================================================================ +// 请求/响应接口定义 - 与后端 Controller 完全对应 +// ============================================================================ + +// 获取任务执行过程请求接口 - 对应 TestScenarioTaskExecutionController.GetTestScenarioTaskExecution 参数 +export interface GetTestScenarioTaskExecutionRequest { + scenarioCode?: string; + deviceCode?: string; + includeExecutionDetails?: boolean; + includeCaseDetails?: boolean; +} + +// 获取任务执行过程响应接口 +export interface GetTestScenarioTaskExecutionResponse { + taskTrees: TestScenarioTaskTreeDto[]; + totalTaskCount: number; + totalExecutionCount: number; + totalCaseCount: number; +} + +// ============================================================================ +// 步骤详情相关类型定义 - 与后端 GetTestScenarioTaskStepDetails 保持一致 +// ============================================================================ + +// 测试场景任务步骤日志数据传输对象 +export interface TestScenarioTaskStepLogDto { + logId: string; + stepDetailId: string; + logType: StepLogType; + logTypeDescription: string; + content?: string; + createdTime: string; +} + +// 测试场景任务步骤详情数据传输对象 +export interface TestScenarioTaskStepDetailDto { + stepDetailId: string; + caseDetailId: string; + stepId: string; + stepName?: string; + executorId: string; + status: TaskExecutionStatus; + startTime?: string; + endTime?: string; + duration: number; + loop: number; + stepLogs: TestScenarioTaskStepLogDto[]; + logCount: number; +} + +// 获取测试场景任务步骤详情请求接口 +export interface GetTestScenarioTaskStepDetailsRequest { + caseDetailId: string; + includeStepLogs?: boolean; + logTypeFilter?: StepLogType; + startTimeFilter?: string; + endTimeFilter?: string; +} + +// 获取测试场景任务步骤详情响应接口 +export interface GetTestScenarioTaskStepDetailsResponse { + stepDetails: TestScenarioTaskStepDetailDto[]; + totalStepCount: number; + totalLogCount: number; +} + +// ============================================================================ +// 测试场景任务执行过程服务类 - 完全对应 TestScenarioTaskExecutionController +// ============================================================================ + +class TestScenarioTaskExecutionService { + private readonly baseUrl = API_PATHS.TEST_SCENARIO_TASK_EXECUTION; + + /** + * 获取测试场景任务执行过程 + * 对应: GET /api/testscenariotaskexecution + * 对应控制器方法: GetTestScenarioTaskExecution() + * 支持场景和设备过滤,可控制是否包含执行和用例详情 + */ + async getTestScenarioTaskExecution(params: GetTestScenarioTaskExecutionRequest = {}): Promise> { + const queryParams = new URLSearchParams(); + + if (params.scenarioCode) queryParams.append('scenarioCode', params.scenarioCode); + if (params.deviceCode) queryParams.append('deviceCode', params.deviceCode); + if (params.includeExecutionDetails !== undefined) queryParams.append('includeExecutionDetails', params.includeExecutionDetails.toString()); + if (params.includeCaseDetails !== undefined) queryParams.append('includeCaseDetails', params.includeCaseDetails.toString()); + + const url = queryParams.toString() ? `${this.baseUrl}?${queryParams.toString()}` : this.baseUrl; + return httpClient.get(url); + } + + /** + * 获取测试场景任务步骤详情 + * 对应: GET /api/testscenariotaskexecution/step-details + * 对应控制器方法: GetTestScenarioTaskStepDetails() + * 通过CaseDetailId获取步骤详情和日志信息 + */ + async getTestScenarioTaskStepDetails(params: GetTestScenarioTaskStepDetailsRequest): Promise> { + const queryParams = new URLSearchParams(); + + // 必需参数 + queryParams.append('caseDetailId', params.caseDetailId); + + // 可选参数 + if (params.includeStepLogs !== undefined) queryParams.append('includeStepLogs', params.includeStepLogs.toString()); + if (params.logTypeFilter) queryParams.append('logTypeFilter', params.logTypeFilter); + if (params.startTimeFilter) queryParams.append('startTimeFilter', params.startTimeFilter); + if (params.endTimeFilter) queryParams.append('endTimeFilter', params.endTimeFilter); + + const url = `${this.baseUrl}/step-details?${queryParams.toString()}`; + return httpClient.get(url); + } +} + +// ============================================================================ +// 导出服务实例 +// ============================================================================ + +export const testScenarioTaskExecutionService = new TestScenarioTaskExecutionService(); diff --git a/src/modify.md b/src/modify.md index 64064a4..10dbb8d 100644 --- a/src/modify.md +++ b/src/modify.md @@ -169841,5 +169841,8 @@ export const getRouteComponent = (routeKey: string): React.LazyExoticComponent +/// 执行状态过滤 +/// +public TaskExecutionStatus? ExecutionStatus { get; set; } +``` + +### 2. SQL 查询优化 +- **主查询**: 添加 `AND ted."Status" = {paramIndex}` 过滤条件 +- **COUNT 查询**: 根据 `executionStatus` 参数动态选择是否 JOIN 执行状态表 +- **参数处理**: 使用枚举的字符串表示进行 SQL 参数绑定 + +### 3. 性能考虑 +- **条件查询**: 只在需要时才 JOIN 执行状态表,避免不必要的性能开销 +- **索引优化**: 利用现有的执行状态表索引进行高效过滤 +- **分页支持**: 保持原有的分页功能,确保大数据量下的查询性能 + +## 使用示例 +```csharp +// 查询所有执行成功的任务 +var query = new GetTestScenarioTasksQuery +{ + ExecutionStatus = TaskExecutionStatus.Success, + PageNumber = 1, + PageSize = 10 +}; + +// 查询所有执行失败的任务 +var failedQuery = new GetTestScenarioTasksQuery +{ + ExecutionStatus = TaskExecutionStatus.Failed +}; +``` + +## 兼容性 +- **向后兼容**: 新增的 `ExecutionStatus` 参数为可空类型,不影响现有调用 +- **默认行为**: 当 `ExecutionStatus` 为 `null` 时,不进行执行状态过滤 +- **API 兼容**: 前端 API 调用保持兼容,新参数为可选参数 + +## 修改的文件列表 +1. `X1.Application/Features/TestScenarioTasks/Queries/GetTestScenarioTasks/GetTestScenarioTasksQuery.cs` +2. `X1.Application/Features/TestScenarioTasks/Queries/GetTestScenarioTasks/GetTestScenarioTasksQueryHandler.cs` +3. `X1.Domain/Repositories/TestTask/ITestScenarioTaskRepository.cs` +4. `X1.Infrastructure/Repositories/TestTask/TestScenarioTaskRepository.cs` + +## 测试建议 +1. 测试不传递 `ExecutionStatus` 参数时的默认行为 +2. 测试传递不同执行状态值时的过滤效果 +3. 测试与其他过滤条件组合使用时的效果 +4. 测试分页功能在执行状态过滤下的正确性 diff --git a/src/modify_20250121_testscenariotaskexecution_feature.md b/src/modify_20250121_testscenariotaskexecution_feature.md new file mode 100644 index 0000000..e5a9281 --- /dev/null +++ b/src/modify_20250121_testscenariotaskexecution_feature.md @@ -0,0 +1,420 @@ +## 2025-01-21 创建测试场景任务执行过程Feature + +### 概述 + +为测试场景任务系统创建树形结构Feature,将三个核心表(tb_testscenario_task、tb_testscenario_task_execution_detail、tb_testscenario_task_execution_case_detail)组合生成层次化的树形结构,便于前端展示和管理。 + +### 主要变更 + +#### 1. 创建 TestScenarioTaskTree Feature +- **目录**: `X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskTree/` +- **功能**: 提供测试场景任务执行过程查询功能 +- **架构**: 遵循CQRS模式,使用MediatR进行查询处理 + +#### 2. 实现的功能组件 +- **查询类**: `GetTestScenarioTaskTreeQuery.cs` - 定义查询参数和条件 +- **响应类**: `GetTestScenarioTaskTreeResponse.cs` - 定义树形结构响应格式 +- **处理器**: `GetTestScenarioTaskTreeQueryHandler.cs` - 实现树形结构构建逻辑 +- **DTO类**: 包含任务树、执行树、用例树的数据传输对象 + +#### 3. 树形结构设计 +- **第一层**: 测试场景任务(TestScenarioTaskTreeDto) +- **第二层**: 任务执行详情(TaskExecutionTreeDto) +- **第三层**: 用例执行详情(TaskExecutionCaseTreeDto) + +#### 4. 查询参数支持 +- **场景过滤**: `ScenarioCode` - 按场景编码过滤 +- **设备过滤**: `DeviceCode` - 按设备编码过滤 +- **执行详情**: `IncludeExecutionDetails` - 是否包含执行详情 +- **用例详情**: `IncludeCaseDetails` - 是否包含用例详情 + +#### 5. 技术特点 +- **性能优化**: 支持按需加载,可选择是否包含执行和用例详情 +- **数据完整性**: 提供完整的统计信息(任务数量、执行数量、用例数量) +- **错误处理**: 完整的异常处理和日志记录 +- **类型安全**: 强类型的DTO设计,确保数据一致性 + +### 技术实现 + +#### 1. 查询类设计 +```csharp +public class GetTestScenarioTaskTreeQuery : IRequest> +{ + public string? ScenarioCode { get; set; } + public string? DeviceCode { get; set; } + public bool IncludeExecutionDetails { get; set; } = true; + public bool IncludeCaseDetails { get; set; } = true; +} +``` + +#### 2. 树形结构DTO +- **TestScenarioTaskTreeDto**: 任务树节点,包含任务基本信息和执行列表 +- **TaskExecutionTreeDto**: 执行树节点,包含执行信息和用例列表 +- **TaskExecutionCaseTreeDto**: 用例树节点,包含用例执行详情 + +#### 3. 处理器逻辑 +- **分层构建**: 先构建任务层,再构建执行层,最后构建用例层 +- **按需加载**: 根据参数决定是否加载执行和用例详情 +- **统计计算**: 自动计算各层级的数量统计 + +#### 4. 数据关系 +- **任务 → 执行**: 一对多关系,通过TaskId关联 +- **执行 → 用例**: 一对多关系,通过ExecutionId关联 +- **完整链路**: 任务 → 执行详情 → 用例详情 + +### 使用场景 + +1. **任务管理界面**: 展示完整的任务执行层次结构 +2. **执行监控**: 实时查看任务执行状态和进度 +3. **用例分析**: 分析用例执行情况和性能数据 +4. **报告生成**: 生成包含完整执行链路的报告 + +### 文件清单 + +#### Application Layer +- `X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskTree/GetTestScenarioTaskTreeQuery.cs` +- `X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskTree/GetTestScenarioTaskTreeResponse.cs` +- `X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskTree/GetTestScenarioTaskTreeQueryHandler.cs` + +#### Presentation Layer +- `X1.Presentation/Controllers/TestScenarioTaskExecutionController.cs` + +#### Frontend Layer +- `X1.WebUI/src/services/testScenarioTaskExecutionService.ts` - 任务执行过程服务 +- `X1.WebUI/src/pages/task-execution-process/TaskExecutionProcessView.tsx` - 任务执行过程主视图组件 +- `X1.WebUI/src/pages/task-execution-process/TaskExecutionTree.tsx` - 左侧树形结构组件 +- `X1.WebUI/src/pages/task-execution-process/TaskExecutionTable.tsx` - 右侧表格组件 +- `X1.WebUI/src/components/ui/collapsible.tsx` - 可折叠组件(新增) +- `X1.WebUI/src/constants/api.ts` - 更新API常量配置 +- `X1.WebUI/src/constants/navigationMenuPresets.ts` - 更新导航菜单预设配置 +- `X1.WebUI/src/pages/navigation-menus/NavigationMenuForm_Examples.md` - 更新导航菜单示例文档 +- `X1.WebUI/src/routes/AppRouter.tsx` - 添加任务执行过程页面路由配置 + +### Controller实现 + +#### 1. 控制器设计 +- **基类**: 继承自 `ApiController` 基类 +- **路由**: `api/testscenariotaskexecution` +- **权限**: 需要授权访问 `[Authorize]` +- **日志**: 完整的操作日志记录 + +#### 2. API端点 +- **获取任务执行过程**: `GET /api/testscenariotaskexecution` - 支持场景和设备过滤,可控制是否包含执行和用例详情 + +**设计说明**: 基于现有Controller的实现模式,采用单一端点设计,通过查询参数控制不同的过滤条件和数据包含选项,这与现有的TestScenarioTasksController和TestScenariosController的设计保持一致。 + +#### 3. 查询参数 +- **scenarioCode**: 场景编码过滤 +- **deviceCode**: 设备编码过滤 +- **includeExecutionDetails**: 是否包含执行详情(默认true) +- **includeCaseDetails**: 是否包含用例详情(默认true) + +#### 4. 技术特点 +- **统一响应**: 使用 `OperationResult` 统一响应格式 +- **错误处理**: 完整的错误处理和用户友好的错误消息 +- **日志记录**: 详细的操作日志,包括开始、成功、失败状态 +- **参数验证**: 自动参数验证和类型转换 + +#### 5. API使用示例 + +```http +# 获取所有任务执行过程 +GET /api/testscenariotaskexecution + +# 获取指定场景的任务执行过程 +GET /api/testscenariotaskexecution?scenarioCode=SCENARIO_001 + +# 获取指定设备的任务执行过程 +GET /api/testscenariotaskexecution?deviceCode=DEVICE_001 + +# 获取任务执行过程概览(不包含执行详情) +GET /api/testscenariotaskexecution?scenarioCode=SCENARIO_001&includeExecutionDetails=false&includeCaseDetails=false + +# 获取完整任务执行过程(包含执行和用例详情) +GET /api/testscenariotaskexecution?scenarioCode=SCENARIO_001&includeExecutionDetails=true&includeCaseDetails=true + +# 获取任务执行过程(仅包含执行详情,不包含用例详情) +GET /api/testscenariotaskexecution?deviceCode=DEVICE_001&includeExecutionDetails=true&includeCaseDetails=false +``` + +#### 6. 响应格式示例 + +```json +{ + "isSuccess": true, + "data": { + "taskTrees": [ + { + "taskId": "TASK_001", + "taskCode": "TASK_001", + "taskName": "测试任务1", + "scenarioCode": "SCENARIO_001", + "deviceCode": "DEVICE_001", + "executionCount": 2, + "executions": [ + { + "executionId": "EXEC_001", + "taskId": "TASK_001", + "status": "Success", + "caseCount": 3, + "caseDetails": [ + { + "caseDetailId": "CASE_001", + "executionId": "EXEC_001", + "testCaseFlowId": "FLOW_001", + "status": "Success" + } + ] + } + ] + } + ], + "totalTaskCount": 1, + "totalExecutionCount": 2, + "totalCaseCount": 3 + }, + "errorMessages": null +} + +### 数据库表关系 + +``` +tb_testscenario_task (任务表) +├── Id (主键) +├── TaskCode (任务编码) +├── TaskName (任务名称) +├── ScenarioCode (场景编码) +├── DeviceCode (设备编码) +└── ... (其他字段) + +tb_testscenario_task_execution_detail (执行详情表) +├── Id (主键) +├── TaskId (外键 → tb_testscenario_task.Id) +├── ScenarioCode (场景编码) +├── ExecutorId (执行人ID) +├── Status (执行状态) +└── ... (其他字段) + +tb_testscenario_task_execution_case_detail (用例详情表) +├── Id (主键) +├── ExecutionId (外键 → tb_testscenario_task_execution_detail.Id) +├── ScenarioCode (场景编码) +├── TestCaseFlowId (测试用例流程ID) +├── ExecutorId (执行人ID) +└── ... (其他字段) +``` + +### 树形结构示例 + +``` +任务1 (TestScenarioTaskTreeDto) +├── 执行1 (TaskExecutionTreeDto) +│ ├── 用例1 (TaskExecutionCaseTreeDto) +│ ├── 用例2 (TaskExecutionCaseTreeDto) +│ └── 用例3 (TaskExecutionCaseTreeDto) +├── 执行2 (TaskExecutionTreeDto) +│ ├── 用例4 (TaskExecutionCaseTreeDto) +│ └── 用例5 (TaskExecutionCaseTreeDto) +└── 执行3 (TaskExecutionTreeDto) + └── 用例6 (TaskExecutionCaseTreeDto) + +任务2 (TestScenarioTaskTreeDto) +└── 执行4 (TaskExecutionTreeDto) + ├── 用例7 (TaskExecutionCaseTreeDto) + └── 用例8 (TaskExecutionCaseTreeDto) +``` + +### 性能优化 + +1. **按需加载**: 通过参数控制是否加载执行和用例详情 +2. **批量查询**: 使用仓储的批量查询方法减少数据库访问次数 +3. **内存优化**: 避免N+1查询问题,使用高效的关联查询 +4. **缓存友好**: 设计支持后续缓存优化的数据结构 + +### 前端实现 + +#### 1. 服务层设计 +- **服务类**: `TestScenarioTaskExecutionService` - 完全对应后端Controller +- **API路径**: `/testscenariotaskexecution` - 与后端路由保持一致 +- **类型定义**: 与后端DTO完全对应的TypeScript接口 +- **错误处理**: 统一的错误处理和用户友好的提示 + +#### 2. 视图组件设计 +- **主组件**: `TaskExecutionProcessView` - 避免与现有组件重名 +- **功能定位**: 专门用于查看任务执行结果和过程 +- **布局设计**: 左右分栏布局,左侧树形结构,右侧详细信息表格 +- **UI特点**: + - 左侧可折叠的树形结构展示任务层次 + - 右侧表格展示选中任务的详细执行信息 + - 状态图标和颜色区分不同执行状态 + - 支持按场景和设备过滤 + - 可控制是否显示执行和用例详情 + +#### 3. 组件拆分架构 +- **TaskExecutionProcessView**: 主视图组件,负责数据获取和状态管理 +- **TaskExecutionTree**: 左侧树形组件,展示任务列表和层次结构 +- **TaskExecutionTable**: 右侧表格组件,展示选中任务的详细信息 +- **组件通信**: 通过props传递数据和回调函数实现组件间通信 + +#### 4. 组件特性 +- **响应式设计**: 适配不同屏幕尺寸,大屏显示左右布局,小屏堆叠显示 +- **交互友好**: 左侧树形结构可折叠展开,右侧表格支持搜索和过滤 +- **状态可视化**: 不同颜色和图标表示执行状态 +- **实时更新**: 支持刷新和重新查询 +- **性能优化**: 按需加载执行和用例详情 +- **数据联动**: 左侧选择任务,右侧自动更新对应详细信息 +- **主题适配**: 使用主题变量,支持明暗主题切换 +- **布局优化**: 任务列表铺满容器,内部滚动条优化用户体验 +- **Card布局**: 为Card组件添加flex布局确保子组件正确继承高度 + +#### 5. 步骤详情功能扩展 + +##### 5.1 新增功能概述 +基于用户需求,在现有的TestScenarioTaskTree功能基础上,新增了通过CaseDetailId获取StepId,然后关联tb_testscenario_task_execution_step_log表的功能。 + +##### 5.2 数据流程 +1. **输入**: CaseDetailId (用例执行明细ID) +2. **查询步骤**: 通过CaseDetailId从tb_testscenario_task_execution_case_step_detail表获取StepId列表 +3. **关联日志**: 使用StepDetailId关联tb_testscenario_task_execution_step_log表获取步骤执行日志 +4. **输出**: 包含步骤详情和日志的完整数据结构 + +##### 5.3 新增文件 + +**X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskStepDetails/GetTestScenarioTaskStepDetailsResponse.cs** +- 定义了步骤详情和日志的DTO结构 +- `GetTestScenarioTaskStepDetailsResponse`: 响应容器 +- `TestScenarioTaskStepDetailDto`: 步骤详情DTO +- `TestScenarioTaskStepLogDto`: 步骤日志DTO + +**X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskStepDetails/GetTestScenarioTaskStepDetailsQuery.cs** +- 定义了查询参数 +- 支持日志类型过滤、时间范围过滤等条件 + +**X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskStepDetails/GetTestScenarioTaskStepDetailsQueryHandler.cs** +- 实现了查询逻辑 +- 通过Repository获取步骤详情和日志数据 +- 支持条件过滤和排序 + +##### 5.4 Controller扩展 + +**X1.Presentation/Controllers/TestScenarioTaskExecutionController.cs** +- 新增 `GetTestScenarioTaskStepDetails` 端点 +- 路由: `GET /api/testscenariotaskexecution/step-details` +- 支持查询参数: caseDetailId, includeStepLogs, logTypeFilter, startTimeFilter, endTimeFilter + +##### 5.5 技术特性 +- **CQRS模式**: 遵循现有的Command Query Responsibility Segregation模式 +- **MediatR**: 使用MediatR进行查询处理 +- **Repository模式**: 利用现有的Repository接口和实现 +- **日志记录**: 完整的日志记录和错误处理 +- **参数验证**: 输入参数验证和错误处理 +- **条件过滤**: 支持多种过滤条件 + +#### 6. 前端服务实现 + +##### 6.1 服务层扩展 + +**X1.WebUI/src/services/testScenarioTaskExecutionService.ts** +- 新增步骤日志类型枚举 `StepLogType` +- 新增步骤详情相关DTO接口: + - `TestScenarioTaskStepLogDto`: 步骤日志DTO + - `TestScenarioTaskStepDetailDto`: 步骤详情DTO + - `GetTestScenarioTaskStepDetailsRequest`: 请求接口 + - `GetTestScenarioTaskStepDetailsResponse`: 响应接口 +- 新增服务方法 `getTestScenarioTaskStepDetails()`: 获取步骤详情和日志 + +##### 6.2 组件层实现 + +**X1.WebUI/src/pages/task-execution-process/StepDetailsView.tsx** +- 步骤日志视图组件 +- 直接显示所有步骤的日志,按时间排序 +- 支持日志类型过滤和搜索功能 +- 每个日志项包含对应的步骤信息 +- 完整的错误处理和加载状态 + +**X1.WebUI/src/pages/task-execution-process/TaskExecutionTree.tsx** +- 修改用例执行详情组件,添加点击事件 +- 支持用例点击查看步骤详情 +- 传递用例点击回调函数 + +**X1.WebUI/src/pages/task-execution-process/TaskExecutionProcessView.tsx** +- 添加步骤详情状态管理 +- 移除TaskExecutionTable组件,直接使用StepDetailsView +- 实现固定两列布局:任务树 + 步骤详情 +- 添加用例点击处理逻辑 +- 集成步骤详情视图 + +##### 6.3 交互流程 +1. **用例点击**: 用户在任务树中点击用例执行详情 +2. **状态更新**: 设置选中的用例明细ID,显示步骤日志视图 +3. **数据获取**: 自动调用步骤详情API获取数据 +4. **日志展示**: 在右侧面板展示所有步骤的日志,按时间排序 +5. **关闭功能**: 用户可点击关闭按钮清空选择 + +##### 6.4 技术特性 +- **响应式布局**: 支持不同屏幕尺寸的布局适配 +- **状态管理**: 完整的组件状态管理 +- **错误处理**: 统一的错误处理和用户提示 +- **加载状态**: 友好的加载状态显示 +- **搜索过滤**: 支持步骤搜索和日志类型过滤 +- **主题适配**: 使用主题变量,支持明暗主题 + +#### 7. 技术实现 +- **React Hooks**: 使用useState和useEffect管理状态 +- **TypeScript**: 强类型定义确保数据安全 +- **Tailwind CSS**: 现代化的样式设计,支持响应式布局 +- **Lucide Icons**: 统一的图标系统 +- **Toast通知**: 用户友好的操作反馈 +- **组件化设计**: 模块化的组件结构,便于维护和扩展 +- **状态管理**: 集中式状态管理,通过props传递数据 + +#### 6. 导航菜单配置 +- **菜单位置**: 任务管理 → 查看任务 +- **菜单路径**: `/dashboard/tasks/execution-process` +- **权限代码**: `testscenariotaskexecution.view` +- **图标**: `ClipboardList` - 表示查看和列表 +- **排序**: 6 - 在任务管理子菜单中排在最后 +- **描述**: 查看任务执行过程和结果 + +#### 7. 路由配置 +- **路由路径**: `/dashboard/tasks/execution-process` +- **组件**: `TaskExecutionProcessView` +- **权限控制**: `testscenariotaskexecution.view` +- **懒加载**: 使用React.lazy进行代码分割 +- **动画容器**: 使用AnimatedContainer包装 +- **保护路由**: 使用ProtectedRoute进行权限验证 + +### 最新更新 (2025-01-21) + +#### 任务列表显示优化 +- **文件**: `X1.WebUI/src/pages/task-execution-process/TaskExecutionTree.tsx` +- **变更**: 移除不必要的字段显示,优化任务列表显示为更紧凑的格式 +- **具体修改**: + - 移除用例流程 (`testCaseFlowId`) 显示 + - 移除场景 (`scenarioCode`) 显示 + - 移除执行人 (`executorId`) 显示 + - 移除执行ID (`executionId`) 显示 + - 优化间距和内边距,使显示更紧凑 + - 减少组件间距:`space-y-2` → `space-y-1` + - 减少内边距:`p-3` → `p-2`,`p-4` → `p-2` + - 移除未使用的 `User` 图标导入 + +#### 步骤日志显示优化 +- **文件**: `X1.WebUI/src/pages/task-execution-process/StepDetailsView.tsx` +- **变更**: 移除步骤日志信息中的执行人和ID显示,优化显示内容 +- **具体修改**: + - 移除 `log.logId` 的显示 + - 移除 `stepDetail.executorId` 的显示 + - 简化步骤信息显示,只保留步骤名称、状态和耗时 + - 优化布局,减少不必要的空白空间 + - 为日志内容添加滚动条:`max-h-32 overflow-y-auto` + - 进一步紧凑化显示:减少间距和内边距 + +### 扩展性 + +1. **层级扩展**: 可以轻松添加更多层级(如步骤详情) +2. **过滤扩展**: 可以添加更多过滤条件(如时间范围、状态等) +3. **排序扩展**: 可以添加排序功能(如按时间、状态等) +4. **分页扩展**: 可以添加分页功能以处理大量数据 +5. **实时更新**: 可以添加WebSocket支持实时状态更新 +6. **导出功能**: 可以添加数据导出功能 diff --git a/src/modify_20250121_testscenariotasktree_feature.md b/src/modify_20250121_testscenariotasktree_feature.md new file mode 100644 index 0000000..8d66beb --- /dev/null +++ b/src/modify_20250121_testscenariotasktree_feature.md @@ -0,0 +1,228 @@ +## 2025-01-21 创建测试场景任务执行过程Feature + +### 概述 + +为测试场景任务系统创建树形结构Feature,将三个核心表(tb_testscenario_task、tb_testscenario_task_execution_detail、tb_testscenario_task_execution_case_detail)组合生成层次化的树形结构,便于前端展示和管理。 + +### 主要变更 + +#### 1. 创建 TestScenarioTaskTree Feature +- **目录**: `X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskTree/` +- **功能**: 提供测试场景任务执行过程查询功能 +- **架构**: 遵循CQRS模式,使用MediatR进行查询处理 + +#### 2. 实现的功能组件 +- **查询类**: `GetTestScenarioTaskTreeQuery.cs` - 定义查询参数和条件 +- **响应类**: `GetTestScenarioTaskTreeResponse.cs` - 定义树形结构响应格式 +- **处理器**: `GetTestScenarioTaskTreeQueryHandler.cs` - 实现树形结构构建逻辑 +- **DTO类**: 包含任务树、执行树、用例树的数据传输对象 + +#### 3. 树形结构设计 +- **第一层**: 测试场景任务(TestScenarioTaskTreeDto) +- **第二层**: 任务执行详情(TaskExecutionTreeDto) +- **第三层**: 用例执行详情(TaskExecutionCaseTreeDto) + +#### 4. 查询参数支持 +- **场景过滤**: `ScenarioCode` - 按场景编码过滤 +- **设备过滤**: `DeviceCode` - 按设备编码过滤 +- **执行详情**: `IncludeExecutionDetails` - 是否包含执行详情 +- **用例详情**: `IncludeCaseDetails` - 是否包含用例详情 + +#### 5. 技术特点 +- **性能优化**: 支持按需加载,可选择是否包含执行和用例详情 +- **数据完整性**: 提供完整的统计信息(任务数量、执行数量、用例数量) +- **错误处理**: 完整的异常处理和日志记录 +- **类型安全**: 强类型的DTO设计,确保数据一致性 + +### 技术实现 + +#### 1. 查询类设计 +```csharp +public class GetTestScenarioTaskTreeQuery : IRequest> +{ + public string? ScenarioCode { get; set; } + public string? DeviceCode { get; set; } + public bool IncludeExecutionDetails { get; set; } = true; + public bool IncludeCaseDetails { get; set; } = true; +} +``` + +#### 2. 树形结构DTO +- **TestScenarioTaskTreeDto**: 任务树节点,包含任务基本信息和执行列表 +- **TaskExecutionTreeDto**: 执行树节点,包含执行信息和用例列表 +- **TaskExecutionCaseTreeDto**: 用例树节点,包含用例执行详情 + +#### 3. 处理器逻辑 +- **分层构建**: 先构建任务层,再构建执行层,最后构建用例层 +- **按需加载**: 根据参数决定是否加载执行和用例详情 +- **统计计算**: 自动计算各层级的数量统计 + +#### 4. 数据关系 +- **任务 → 执行**: 一对多关系,通过TaskId关联 +- **执行 → 用例**: 一对多关系,通过ExecutionId关联 +- **完整链路**: 任务 → 执行详情 → 用例详情 + +### 使用场景 + +1. **任务管理界面**: 展示完整的任务执行层次结构 +2. **执行监控**: 实时查看任务执行状态和进度 +3. **用例分析**: 分析用例执行情况和性能数据 +4. **报告生成**: 生成包含完整执行链路的报告 + +### 文件清单 + +#### Application Layer +- `X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskTree/GetTestScenarioTaskTreeQuery.cs` +- `X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskTree/GetTestScenarioTaskTreeResponse.cs` +- `X1.Application/Features/TestScenarioTaskTree/Queries/GetTestScenarioTaskTree/GetTestScenarioTaskTreeQueryHandler.cs` + +#### Presentation Layer +- `X1.Presentation/Controllers/TestScenarioTaskExecutionController.cs` + +### Controller实现 + +#### 1. 控制器设计 +- **基类**: 继承自 `ApiController` 基类 +- **路由**: `api/testscenariotaskexecution` +- **权限**: 需要授权访问 `[Authorize]` +- **日志**: 完整的操作日志记录 + +#### 2. API端点 +- **获取任务执行过程**: `GET /api/testscenariotaskexecution` - 支持场景和设备过滤,可控制是否包含执行和用例详情 + +**设计说明**: 基于现有Controller的实现模式,采用单一端点设计,通过查询参数控制不同的过滤条件和数据包含选项,这与现有的TestScenarioTasksController和TestScenariosController的设计保持一致。 + +#### 3. 查询参数 +- **scenarioCode**: 场景编码过滤 +- **deviceCode**: 设备编码过滤 +- **includeExecutionDetails**: 是否包含执行详情(默认true) +- **includeCaseDetails**: 是否包含用例详情(默认true) + +#### 4. 技术特点 +- **统一响应**: 使用 `OperationResult` 统一响应格式 +- **错误处理**: 完整的错误处理和用户友好的错误消息 +- **日志记录**: 详细的操作日志,包括开始、成功、失败状态 +- **参数验证**: 自动参数验证和类型转换 + +#### 5. API使用示例 + +```http +# 获取所有任务执行过程 +GET /api/testscenariotaskexecution + +# 获取指定场景的任务执行过程 +GET /api/testscenariotaskexecution?scenarioCode=SCENARIO_001 + +# 获取指定设备的任务执行过程 +GET /api/testscenariotaskexecution?deviceCode=DEVICE_001 + +# 获取任务执行过程概览(不包含执行详情) +GET /api/testscenariotaskexecution?scenarioCode=SCENARIO_001&includeExecutionDetails=false&includeCaseDetails=false + +# 获取完整任务执行过程(包含执行和用例详情) +GET /api/testscenariotaskexecution?scenarioCode=SCENARIO_001&includeExecutionDetails=true&includeCaseDetails=true + +# 获取任务执行过程(仅包含执行详情,不包含用例详情) +GET /api/testscenariotaskexecution?deviceCode=DEVICE_001&includeExecutionDetails=true&includeCaseDetails=false +``` + +#### 6. 响应格式示例 + +```json +{ + "isSuccess": true, + "data": { + "taskTrees": [ + { + "taskId": "TASK_001", + "taskCode": "TASK_001", + "taskName": "测试任务1", + "scenarioCode": "SCENARIO_001", + "deviceCode": "DEVICE_001", + "executionCount": 2, + "executions": [ + { + "executionId": "EXEC_001", + "taskId": "TASK_001", + "status": "Success", + "caseCount": 3, + "caseDetails": [ + { + "caseDetailId": "CASE_001", + "executionId": "EXEC_001", + "testCaseFlowId": "FLOW_001", + "status": "Success" + } + ] + } + ] + } + ], + "totalTaskCount": 1, + "totalExecutionCount": 2, + "totalCaseCount": 3 + }, + "errorMessages": null +} + +### 数据库表关系 + +``` +tb_testscenario_task (任务表) +├── Id (主键) +├── TaskCode (任务编码) +├── TaskName (任务名称) +├── ScenarioCode (场景编码) +├── DeviceCode (设备编码) +└── ... (其他字段) + +tb_testscenario_task_execution_detail (执行详情表) +├── Id (主键) +├── TaskId (外键 → tb_testscenario_task.Id) +├── ScenarioCode (场景编码) +├── ExecutorId (执行人ID) +├── Status (执行状态) +└── ... (其他字段) + +tb_testscenario_task_execution_case_detail (用例详情表) +├── Id (主键) +├── ExecutionId (外键 → tb_testscenario_task_execution_detail.Id) +├── ScenarioCode (场景编码) +├── TestCaseFlowId (测试用例流程ID) +├── ExecutorId (执行人ID) +└── ... (其他字段) +``` + +### 树形结构示例 + +``` +任务1 (TestScenarioTaskTreeDto) +├── 执行1 (TaskExecutionTreeDto) +│ ├── 用例1 (TaskExecutionCaseTreeDto) +│ ├── 用例2 (TaskExecutionCaseTreeDto) +│ └── 用例3 (TaskExecutionCaseTreeDto) +├── 执行2 (TaskExecutionTreeDto) +│ ├── 用例4 (TaskExecutionCaseTreeDto) +│ └── 用例5 (TaskExecutionCaseTreeDto) +└── 执行3 (TaskExecutionTreeDto) + └── 用例6 (TaskExecutionCaseTreeDto) + +任务2 (TestScenarioTaskTreeDto) +└── 执行4 (TaskExecutionTreeDto) + ├── 用例7 (TaskExecutionCaseTreeDto) + └── 用例8 (TaskExecutionCaseTreeDto) +``` + +### 性能优化 + +1. **按需加载**: 通过参数控制是否加载执行和用例详情 +2. **批量查询**: 使用仓储的批量查询方法减少数据库访问次数 +3. **内存优化**: 避免N+1查询问题,使用高效的关联查询 +4. **缓存友好**: 设计支持后续缓存优化的数据结构 + +### 扩展性 + +1. **层级扩展**: 可以轻松添加更多层级(如步骤详情) +2. **过滤扩展**: 可以添加更多过滤条件(如时间范围、状态等) +3. **排序扩展**: 可以添加排序功能(如按时间、状态等) +4. **分页扩展**: 可以添加分页功能以处理大量数据