diff --git a/src/X1.Application/ApplicationServices/TaskExecutionService.cs b/src/X1.Application/ApplicationServices/TaskExecutionService.cs index 1a943b0..bd5481f 100644 --- a/src/X1.Application/ApplicationServices/TaskExecutionService.cs +++ b/src/X1.Application/ApplicationServices/TaskExecutionService.cs @@ -18,6 +18,7 @@ public class TaskExecutionService : ITaskExecutionService { private readonly ILogger _logger; private readonly ITestScenarioTaskExecutionDetailRepository _taskExecutionRepository; + private readonly ITestScenarioTaskExecutionCaseDetailRepository _taskExecutionCaseDetailRepository; private readonly ITestScenarioTaskRepository _testScenarioTaskRepository; private readonly ITestScenarioRepository _testScenarioRepository; private readonly IScenarioTestCaseRepository _scenarioTestCaseRepository; @@ -30,6 +31,7 @@ public class TaskExecutionService : ITaskExecutionService public TaskExecutionService( ILogger logger, ITestScenarioTaskExecutionDetailRepository taskExecutionRepository, + ITestScenarioTaskExecutionCaseDetailRepository taskExecutionCaseDetailRepository, ITestScenarioTaskRepository testScenarioTaskRepository, ITestScenarioRepository testScenarioRepository, IScenarioTestCaseRepository scenarioTestCaseRepository, @@ -41,6 +43,7 @@ public class TaskExecutionService : ITaskExecutionService { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _taskExecutionRepository = taskExecutionRepository ?? throw new ArgumentNullException(nameof(taskExecutionRepository)); + _taskExecutionCaseDetailRepository = taskExecutionCaseDetailRepository ?? throw new ArgumentNullException(nameof(taskExecutionCaseDetailRepository)); _testScenarioTaskRepository = testScenarioTaskRepository ?? throw new ArgumentNullException(nameof(testScenarioTaskRepository)); _testScenarioRepository = testScenarioRepository ?? throw new ArgumentNullException(nameof(testScenarioRepository)); _scenarioTestCaseRepository = scenarioTestCaseRepository ?? throw new ArgumentNullException(nameof(scenarioTestCaseRepository)); @@ -85,7 +88,7 @@ public class TaskExecutionService : ITaskExecutionService ); await _taskExecutionRepository.AddAsync(taskExecution, cancellationToken); - + await _unitOfWork.SaveChangesAsync(); _logger.LogInformation("任务执行记录创建成功,执行ID: {TaskExecutionId}", taskExecution.Id); return taskExecution; @@ -208,7 +211,9 @@ public class TaskExecutionService : ITaskExecutionService initialNodeInfo.InitialNodes.Add(new InitialNodeItem { NodeId = startNode.NodeId, - StepMapping = startNode.StepConfig.Mapping, + StepMapping = startNode.StepConfig!.Mapping, + StepId=startNode.StepConfig!.Id, + NodeName=startNode.StepConfig!.StepName, FlowId = testCaseFlow.Id, FlowName = testCaseFlow.Name }); @@ -280,4 +285,185 @@ public class TaskExecutionService : ITaskExecutionService return new List(); } } + + /// + /// 更新任务执行状态 + /// + public async Task UpdateTaskExecutionStatusAsync(string taskExecutionId, TaskExecutionStatus status, int? progress = null, CancellationToken cancellationToken = default) + { + _logger.LogInformation("开始更新任务执行状态,执行ID: {TaskExecutionId}, 新状态: {Status}, 进度: {Progress}", + taskExecutionId, status, progress); + + try + { + // 1. 获取任务执行记录 + var taskExecution = await _taskExecutionRepository.GetByIdAsync(taskExecutionId, null, cancellationToken); + if (taskExecution == null) + { + _logger.LogWarning("任务执行记录不存在,执行ID: {TaskExecutionId}", taskExecutionId); + return false; + } + + // 2. 验证状态转换的合法性 + if (!IsValidStatusTransition(taskExecution.Status, status)) + { + _logger.LogWarning("无效的状态转换,执行ID: {TaskExecutionId}, 当前状态: {CurrentStatus}, 目标状态: {TargetStatus}", + taskExecutionId, taskExecution.Status, status); + return false; + } + + // 3. 更新状态 + var oldStatus = taskExecution.Status; + taskExecution.Status = status; + + // 4. 更新进度(如果提供) + if (progress.HasValue) + { + taskExecution.UpdateProgress(progress.Value); + } + + // 5. 根据状态更新相关时间字段 + switch (status) + { + case TaskExecutionStatus.Running: + if (oldStatus == TaskExecutionStatus.Pending) + { + taskExecution.StartExecution(); + } + break; + case TaskExecutionStatus.Success: + case TaskExecutionStatus.Failed: + if (oldStatus == TaskExecutionStatus.Running) + { + taskExecution.CompleteExecution(status == TaskExecutionStatus.Success); + } + break; + } + + // 6. 保存更改 + _taskExecutionRepository.Update(taskExecution); + await _unitOfWork.SaveChangesAsync(); + _logger.LogInformation("任务执行状态更新成功,执行ID: {TaskExecutionId}, 状态: {OldStatus} -> {NewStatus}", + taskExecutionId, oldStatus, status); + + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "更新任务执行状态失败,执行ID: {TaskExecutionId}, 状态: {Status}", taskExecutionId, status); + return false; + } + } + + /// + /// 批量创建任务执行用例明细初始化数据 + /// + public async Task> BatchCreateTaskExecutionCaseDetailsAsync( + string executionId, + string scenarioCode, + IReadOnlyList testCaseFlowIds, + string executorId, + int loop = 1, + CancellationToken cancellationToken = default) + { + _logger.LogInformation("开始批量创建任务执行用例明细,执行ID: {ExecutionId}, 场景编码: {ScenarioCode}, 用例数量: {CaseCount}, 执行人ID: {ExecutorId}, 轮次: {Loop}", + executionId, scenarioCode, testCaseFlowIds.Count, executorId, loop); + + try + { + // 1. 参数验证 + if (string.IsNullOrWhiteSpace(executionId)) + { + throw new ArgumentException("执行ID不能为空", nameof(executionId)); + } + + if (string.IsNullOrWhiteSpace(scenarioCode)) + { + throw new ArgumentException("场景编码不能为空", nameof(scenarioCode)); + } + + if (testCaseFlowIds == null || !testCaseFlowIds.Any()) + { + throw new ArgumentException("测试用例流程ID列表不能为空", nameof(testCaseFlowIds)); + } + + if (string.IsNullOrWhiteSpace(executorId)) + { + throw new ArgumentException("执行人ID不能为空", nameof(executorId)); + } + + if (loop < 1) + { + throw new ArgumentException("执行轮次必须大于0", nameof(loop)); + } + + // 2. 验证执行记录是否存在 + var taskExecution = await _taskExecutionRepository.GetByIdAsync(executionId, null, cancellationToken); + if (taskExecution == null) + { + throw new InvalidOperationException($"任务执行记录不存在,执行ID: {executionId}"); + } + + // 3. 验证测试用例流程是否存在 + var existingTestCaseFlows = await _testCaseFlowRepository.GetByIdsAsync(testCaseFlowIds.ToList(), cancellationToken); + var existingTestCaseFlowIds = existingTestCaseFlows.Select(tcf => tcf.Id).ToHashSet(); + var missingTestCaseFlowIds = testCaseFlowIds.Where(id => !existingTestCaseFlowIds.Contains(id)).ToList(); + + if (missingTestCaseFlowIds.Any()) + { + throw new InvalidOperationException($"以下测试用例流程不存在: {string.Join(", ", missingTestCaseFlowIds)}"); + } + + // 4. 批量创建用例明细记录 + var caseDetails = new List(); + + foreach (var testCaseFlowId in testCaseFlowIds) + { + var caseDetail = TestScenarioTaskExecutionCaseDetail.Create( + executionId: executionId, + scenarioCode: scenarioCode, + testCaseFlowId: testCaseFlowId, + executorId: executorId, + loop: loop + ); + + caseDetails.Add(caseDetail); + } + + // 5. 批量保存到数据库 + var createdCaseDetails = await _taskExecutionCaseDetailRepository.AddRangeAsync(caseDetails, cancellationToken); + await _unitOfWork.SaveChangesAsync(cancellationToken); + + _logger.LogInformation("批量创建任务执行用例明细成功,执行ID: {ExecutionId}, 创建数量: {CreatedCount}", + executionId, createdCaseDetails.Count()); + + return createdCaseDetails.ToList().AsReadOnly(); + } + catch (Exception ex) + { + _logger.LogError(ex, "批量创建任务执行用例明细失败,执行ID: {ExecutionId}, 场景编码: {ScenarioCode}, 用例数量: {CaseCount}", + executionId, scenarioCode, testCaseFlowIds.Count); + throw; + } + } + + /// + /// 验证状态转换是否合法 + /// + /// 当前状态 + /// 目标状态 + /// 是否合法 + private static bool IsValidStatusTransition(TaskExecutionStatus currentStatus, TaskExecutionStatus targetStatus) + { + return currentStatus switch + { + TaskExecutionStatus.Pending => targetStatus == TaskExecutionStatus.Running || + targetStatus == TaskExecutionStatus.Failed, + TaskExecutionStatus.Running => targetStatus == TaskExecutionStatus.Success || + targetStatus == TaskExecutionStatus.Failed, + TaskExecutionStatus.Success => false, // 成功状态不能转换到其他状态 + TaskExecutionStatus.Failed => false, // 失败状态不能转换到其他状态 + _ => false + }; + } } diff --git a/src/X1.Application/Features/TaskExecution/Commands/StartTaskExecution/StartTaskExecutionCommand.cs b/src/X1.Application/Features/TaskExecution/Commands/StartTaskExecution/StartTaskExecutionCommand.cs index 50a19c0..e41aefb 100644 --- a/src/X1.Application/Features/TaskExecution/Commands/StartTaskExecution/StartTaskExecutionCommand.cs +++ b/src/X1.Application/Features/TaskExecution/Commands/StartTaskExecution/StartTaskExecutionCommand.cs @@ -1,5 +1,6 @@ using MediatR; using X1.Domain.Common; +using X1.Domain.Models; namespace X1.Application.Features.TaskExecution.Commands.StartTaskExecution; @@ -13,19 +14,3 @@ public class StartTaskExecutionCommand : IRequest public List TaskExecutionRequests { get; set; } = new(); } - -/// -/// 任务执行请求 -/// -public class TaskExecutionRequest -{ - /// - /// 任务ID - /// - public string TaskId { get; set; } = null!; - - /// - /// 设备编码 - /// - public string DeviceCode { 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 c6cfdc2..24ac6cd 100644 --- a/src/X1.Application/Features/TaskExecution/Commands/StartTaskExecution/StartTaskExecutionCommandHandler.cs +++ b/src/X1.Application/Features/TaskExecution/Commands/StartTaskExecution/StartTaskExecutionCommandHandler.cs @@ -6,6 +6,8 @@ using X1.Domain.Repositories.Base; using X1.Application.Features.TaskExecution.Events.NodeExecutionEvents; using X1.Domain.Entities.TestTask; using X1.Domain.Models; +using X1.Domain.ServiceScope; +using Microsoft.Extensions.DependencyInjection; namespace X1.Application.Features.TaskExecution.Commands.StartTaskExecution; @@ -16,22 +18,19 @@ public class StartTaskExecutionCommandHandler : IRequestHandler _logger; private readonly ITaskExecutionService _taskExecutionService; - private readonly IUnitOfWork _unitOfWork; - private readonly IMediator _mediator; private readonly ICurrentUserService _currentUserService; - + protected readonly IServiceScopeExecutor _scopeExecutor; public StartTaskExecutionCommandHandler( ILogger logger, ITaskExecutionService taskExecutionService, IUnitOfWork unitOfWork, - IMediator mediator, - ICurrentUserService currentUserService) + ICurrentUserService currentUserService, + IServiceScopeExecutor scopeExecutor) { _logger = logger; _taskExecutionService = taskExecutionService; - _unitOfWork = unitOfWork; - _mediator = mediator; _currentUserService = currentUserService; + _scopeExecutor = scopeExecutor; } /// @@ -59,59 +58,25 @@ public class StartTaskExecutionCommandHandler : IRequestHandler.CreateFailure("任务执行请求列表不能为空"); } - _logger.LogInformation("开始处理启动任务执行命令,任务数量: {TaskCount}, 执行人ID: {ExecutorId}", + _logger.LogInformation("开始处理启动任务执行命令,任务数量: {TaskCount}, 执行人ID: {ExecutorId}", request.TaskExecutionRequests.Count, currentUserId); var response = new StartTaskExecutionResponse(); - var successCount = 0; - var failureCount = 0; - var errors = new List(); - - // 批量处理任务 - foreach (var taskRequest in request.TaskExecutionRequests) - { - try - { - // 调用任务执行服务启动任务 - var taskExecution = await _taskExecutionService.StartTaskExecutionAsync( - taskRequest.TaskId, - currentUserId, - cancellationToken); - _logger.LogInformation("任务执行启动成功,执行ID: {TaskExecutionId}, 任务ID: {TaskId}", - taskExecution.Id, taskRequest.TaskId); + // 第一阶段:统一收集所有任务数据 + var (taskExecutionDataList, successCount, failureCount, errors) = await CollectTaskExecutionDataAsync( + request.TaskExecutionRequests, currentUserId, cancellationToken); - // 获取初始节点信息 - var initialNodeInfo = await _taskExecutionService.GetInitialNodeAsync(taskRequest.TaskId, cancellationToken); - - // 获取终端设备信息 - var terminalDevices = await _taskExecutionService.GetTerminalDevicesByTaskIdAsync(taskRequest.TaskId, cancellationToken); - - // 发布 NodeExecutionStartedEvent 来启动流程 - _= PublishNodeExecutionStartedEventsAsync(initialNodeInfo, taskExecution, currentUserId, taskRequest.DeviceCode, terminalDevices, cancellationToken); - - successCount++; - } - catch (InvalidOperationException ex) - { - _logger.LogWarning(ex, "启动任务执行失败,任务ID: {TaskId}", taskRequest.TaskId); - errors.Add($"任务 {taskRequest.TaskId}: {ex.Message}"); - failureCount++; - } - catch (Exception ex) - { - _logger.LogError(ex, "启动任务执行时发生未预期的错误,任务ID: {TaskId}", taskRequest.TaskId); - errors.Add($"任务 {taskRequest.TaskId}: 启动失败,请稍后重试"); - failureCount++; - } + // 第二阶段:统一发布事件 + if (taskExecutionDataList.Any()) + { + _ = PublishAllNodeExecutionStartedEventsAsync(taskExecutionDataList, currentUserId, cancellationToken); } response.SuccessCount = successCount; response.FailureCount = failureCount; response.Errors = errors; - await _unitOfWork.SaveChangesAsync(); - // 根据结果返回响应 if (successCount > 0 && failureCount == 0) { @@ -122,7 +87,7 @@ public class StartTaskExecutionCommandHandler : IRequestHandler.CreateSuccess( - $"部分任务启动成功,成功 {successCount} 个,失败 {failureCount} 个。失败详情: {string.Join("; ", errors)}", + $"部分任务启动成功,成功 {successCount} 个,失败 {failureCount} 个。失败详情: {string.Join("; ", errors)}", response); } else @@ -140,7 +105,112 @@ public class StartTaskExecutionCommandHandler : IRequestHandler - /// 发布节点执行启动事件 + /// 准备任务执行数据 + /// 创建任务执行记录,获取初始节点信息和终端设备信息 + /// + /// 任务执行请求列表 + /// 当前用户ID + /// 取消令牌 + /// 任务执行数据准备结果 + private async Task<(List TaskExecutionDataList, int SuccessCount, int FailureCount, List Errors)> CollectTaskExecutionDataAsync( + List taskExecutionRequests, + string currentUserId, + CancellationToken cancellationToken) + { + var taskExecutionDataList = new List(); + var successCount = 0; + var failureCount = 0; + var errors = new List(); + + _logger.LogInformation("开始准备任务执行数据,任务数量: {TaskCount}", taskExecutionRequests.Count); + + foreach (var taskRequest in taskExecutionRequests) + { + try + { + // 调用任务执行服务启动任务 + var taskExecution = await _taskExecutionService.StartTaskExecutionAsync( + taskRequest.TaskId, + currentUserId, + cancellationToken); + + _logger.LogInformation("任务执行记录创建成功,执行ID: {TaskExecutionId}, 任务ID: {TaskId}", + taskExecution.Id, taskRequest.TaskId); + + // 获取初始节点信息 + var initialNodeInfo = await _taskExecutionService.GetInitialNodeAsync(taskRequest.TaskId, cancellationToken); + + // 获取终端设备信息 + var terminalDevices = await _taskExecutionService.GetTerminalDevicesByTaskIdAsync(taskRequest.TaskId, cancellationToken); + + // 收集数据 + taskExecutionDataList.Add(new TaskExecutionData + { + TaskRequest = taskRequest, + TaskExecution = taskExecution, + InitialNodeInfo = initialNodeInfo, + TerminalDevices = terminalDevices + }); + + successCount++; + } + catch (InvalidOperationException ex) + { + _logger.LogWarning(ex, "准备任务执行数据失败,任务ID: {TaskId}", taskRequest.TaskId); + errors.Add($"任务 {taskRequest.TaskId}: {ex.Message}"); + failureCount++; + } + catch (Exception ex) + { + _logger.LogError(ex, "准备任务执行数据时发生未预期的错误,任务ID: {TaskId}", taskRequest.TaskId); + errors.Add($"任务 {taskRequest.TaskId}: 准备失败,请稍后重试"); + failureCount++; + } + } + + _logger.LogInformation("任务执行数据准备完成,成功数量: {SuccessCount}, 失败数量: {FailureCount}", successCount, failureCount); + + return (taskExecutionDataList, successCount, failureCount, errors); + } + + /// + /// 统一发布所有任务的节点执行启动事件 + /// + /// 任务执行数据列表 + /// 执行人ID + /// 取消令牌 + private async Task PublishAllNodeExecutionStartedEventsAsync( + List taskExecutionDataList, + string executorId, + CancellationToken cancellationToken) + { + _logger.LogInformation("开始统一初始化并启动任务执行,任务数量: {TaskCount}", taskExecutionDataList.Count); + + foreach (var taskData in taskExecutionDataList) + { + try + { + await InitializeAndStartTaskExecutionAsync( + taskData.InitialNodeInfo, + taskData.TaskExecution, + executorId, + taskData.TaskRequest.DeviceCode, + taskData.TerminalDevices, + cancellationToken); + } + catch (Exception ex) + { + _logger.LogError(ex, "初始化并启动任务执行失败,任务ID: {TaskId}", taskData.TaskRequest.TaskId); + // 继续处理其他任务,不中断整个流程 + } + } + + _logger.LogInformation("完成统一初始化并启动任务执行,任务数量: {TaskCount}", taskExecutionDataList.Count); + } + + /// + /// 初始化并启动任务执行 + /// 包括状态更新、批量创建用例明细和发布节点事件 /// /// 初始节点信息 /// 任务执行详情 @@ -148,7 +218,7 @@ public class StartTaskExecutionCommandHandler : IRequestHandler设备编码 /// 终端设备列表 /// 取消令牌 - private async Task PublishNodeExecutionStartedEventsAsync( + private async Task InitializeAndStartTaskExecutionAsync( InitialNodeInfo? initialNodeInfo, TestScenarioTaskExecutionDetail executionDetail, string executorId, @@ -158,39 +228,152 @@ public class StartTaskExecutionCommandHandler : IRequestHandler { - var nodeExecutionStartedEvent = new NodeExecutionStartedEvent - { - EventId = Guid.NewGuid().ToString(), - TaskExecutionId = executionDetail.Id, - NodeId = initialNode.NodeId, - StepMapping = initialNode.StepMapping, - ExecutorId = executorId, - RuntimeCode = executionDetail.RuntimeCode ?? string.Empty, // 在 StartFlowControllerHandler 中生成 - ScenarioCode = initialNodeInfo.ScenarioCode, - ScenarioId = initialNodeInfo.ScenarioId, - FlowName = initialNode.FlowName, - FlowId = initialNode.FlowId, - DeviceCode = deviceCode, - TerminalDevices = terminalDevices.ToList(), - Timestamp = DateTime.UtcNow - }; - - // 异步发布事件,不等待执行完成(fire and forget) - await _mediator.Publish(nodeExecutionStartedEvent, cancellationToken); - - _logger.LogInformation("已发布 NodeExecutionStartedEvent,事件ID: {EventId}, 节点ID: {NodeId}, 流程: {FlowName}", - nodeExecutionStartedEvent.EventId, nodeExecutionStartedEvent.NodeId, initialNode.FlowName); + var scopedTaskExecution = serviceProvider.GetRequiredService(); + await scopedTaskExecution.UpdateTaskExecutionStatusAsync(executionDetail.Id, TaskExecutionStatus.Running); + }, cancellationToken); + + if (!resultUpdateStatus.IsSuccess) + { + _logger.LogError("更新任务执行服务运行状态失败: {ErrorMessage}", resultUpdateStatus.ErrorMessage); + return; } - - _logger.LogInformation("已为 {NodeCount} 个初始节点发布事件,任务ID: {TaskId}", + + // 2. 批量创建任务执行用例明细初始化数据 + var batchCreateResult = await CreateTaskExecutionCaseDetailsAsync( + initialNodeInfo, executionDetail, executorId, cancellationToken); + + if (!batchCreateResult) + { + _logger.LogError("批量创建任务执行用例明细失败,任务ID: {TaskId}", initialNodeInfo.TaskId); + return; + } + + // 3. 为每个初始节点发布事件 + await PublishNodeExecutionEventsAsync( + initialNodeInfo, executionDetail, executorId, deviceCode, terminalDevices, cancellationToken); + + _logger.LogInformation("已为 {NodeCount} 个初始节点发布事件,任务ID: {TaskId}", initialNodeInfo.InitialNodes.Count, initialNodeInfo.TaskId); + + // 1. 更新任务执行状态为运行中 + var resultUpdateSuccessStatus = await _scopeExecutor.ExecuteAsync(async serviceProvider => + { + var scopedTaskExecution = serviceProvider.GetRequiredService(); + await scopedTaskExecution.UpdateTaskExecutionStatusAsync(executionDetail.Id, TaskExecutionStatus.Success); + }, cancellationToken); + + if (!resultUpdateStatus.IsSuccess) + { + _logger.LogError("更新任务执行服务成功状态失败: {ErrorMessage}", resultUpdateStatus.ErrorMessage); + return; + } } else { _logger.LogWarning("无法获取初始节点,任务ID: {TaskId}", initialNodeInfo?.TaskId ?? "未知"); } } + + /// + /// 创建任务执行用例明细初始化数据 + /// + /// 初始节点信息 + /// 任务执行详情 + /// 执行人ID + /// 取消令牌 + /// 是否创建成功 + private async Task CreateTaskExecutionCaseDetailsAsync( + InitialNodeInfo initialNodeInfo, + TestScenarioTaskExecutionDetail executionDetail, + string executorId, + CancellationToken cancellationToken) + { + try + { + var testCaseFlowIds = initialNodeInfo.InitialNodes.Select(node => node.FlowId).ToList(); + + var resultBatchCreate = await _scopeExecutor.ExecuteAsync(async serviceProvider => + { + var scopedTaskExecutionService = serviceProvider.GetRequiredService(); + await scopedTaskExecutionService.BatchCreateTaskExecutionCaseDetailsAsync( + executionDetail.Id, + initialNodeInfo.ScenarioCode, + testCaseFlowIds, + executorId, + loop: 1, + cancellationToken); + }, cancellationToken); + + if (!resultBatchCreate.IsSuccess) + { + _logger.LogError("批量创建任务执行用例明细失败: {ErrorMessage}", resultBatchCreate.ErrorMessage); + return false; + } + + _logger.LogInformation("批量创建任务执行用例明细成功,执行ID: {TaskExecutionId}, 用例数量: {CaseCount}", + executionDetail.Id, testCaseFlowIds.Count); + + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "批量创建任务执行用例明细时发生异常,执行ID: {TaskExecutionId}", executionDetail.Id); + return false; + } + } + + /// + /// 发布节点执行事件 + /// 为每个初始节点创建并发布执行事件 + /// + /// 初始节点信息 + /// 任务执行详情 + /// 执行人ID + /// 设备编码 + /// 终端设备列表 + /// 取消令牌 + private async Task PublishNodeExecutionEventsAsync( + InitialNodeInfo initialNodeInfo, + TestScenarioTaskExecutionDetail executionDetail, + string executorId, + string deviceCode, + IReadOnlyList terminalDevices, + CancellationToken cancellationToken) + { + foreach (var initialNode in initialNodeInfo.InitialNodes) + { + var nodeExecutionStartedEvent = new NodeExecutionStartedEvent + { + EventId = Guid.NewGuid().ToString(), + TaskExecutionId = executionDetail.Id, + NextNodeInfo = new NextNodeInfo { NodeId=initialNode.NodeId,StepMapping=initialNode.StepMapping,StepId=initialNode.StepId,NodeName=initialNode.NodeName}, + ExecutorId = executorId, + RuntimeCode = executionDetail.RuntimeCode ?? string.Empty, // 在 StartFlowControllerHandler 中生成 + ScenarioCode = initialNodeInfo.ScenarioCode, + ScenarioId = initialNodeInfo.ScenarioId, + FlowName = initialNode.FlowName, + FlowId = initialNode.FlowId, + DeviceCode = deviceCode, + TerminalDevices = terminalDevices.ToList(), + Timestamp = DateTime.UtcNow + }; + + var resultPublish = await _scopeExecutor.ExecuteAsync(async serviceProvider => + { + var scopedMediator = serviceProvider.GetRequiredService(); + await scopedMediator.Publish(nodeExecutionStartedEvent, cancellationToken); + }, cancellationToken); + + _logger.LogInformation("已发布 NodeExecutionStartedEvent,事件ID: {EventId}, 节点ID: {NodeId}, 流程: {FlowName}", + nodeExecutionStartedEvent.EventId, nodeExecutionStartedEvent.NextNodeInfo.NodeId, initialNode.FlowName); + + if (!resultPublish.IsSuccess) + { + _logger.LogError("发布事件失败: {ErrorMessage}", resultPublish.ErrorMessage); + } + } + } } diff --git a/src/X1.Application/Features/TaskExecution/Commands/StartTaskExecution/StartTaskExecutionCommandValidator.cs b/src/X1.Application/Features/TaskExecution/Commands/StartTaskExecution/StartTaskExecutionCommandValidator.cs index 5952ae8..b38d166 100644 --- a/src/X1.Application/Features/TaskExecution/Commands/StartTaskExecution/StartTaskExecutionCommandValidator.cs +++ b/src/X1.Application/Features/TaskExecution/Commands/StartTaskExecution/StartTaskExecutionCommandValidator.cs @@ -1,4 +1,5 @@ using FluentValidation; +using X1.Domain.Models; namespace X1.Application.Features.TaskExecution.Commands.StartTaskExecution; diff --git a/src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/BaseControllerHandler.cs b/src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/BaseControllerHandler.cs index 15b6a92..8d804cb 100644 --- a/src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/BaseControllerHandler.cs +++ b/src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/BaseControllerHandler.cs @@ -5,6 +5,11 @@ using X1.Domain.Events; using X1.Domain.Entities.TestCase; using X1.Domain.ServiceScope; using Microsoft.Extensions.DependencyInjection; +using System.Threading; +using X1.Domain.Repositories.TestTask; +using X1.Domain.Entities.TestTask; +using X1.Domain.Repositories.Base; +using X1.Domain.Common.Enums; namespace X1.Application.Features.TaskExecution.Events.ControllerHandlers; @@ -53,8 +58,6 @@ public abstract class BaseControllerHandler var completedEvent = new NodeExecutionCompletedEvent { TaskExecutionId = notification.TaskExecutionId, - NodeId = notification.NodeId, - StepMapping = stepMapping, ExecutorId = notification.ExecutorId, RuntimeCode = notification.RuntimeCode, ScenarioCode = notification.ScenarioCode, @@ -63,6 +66,7 @@ public abstract class BaseControllerHandler FlowId = notification.FlowId, DeviceCode = notification.DeviceCode, TerminalDevices = notification.TerminalDevices, + NextNodeInfo = notification.NextNodeInfo, Result = result, Timestamp = DateTime.UtcNow }; @@ -91,8 +95,6 @@ public abstract class BaseControllerHandler var failedEvent = new NodeExecutionFailedEvent { TaskExecutionId = notification.TaskExecutionId, - NodeId = notification.NodeId, - StepMapping = stepMapping, ExecutorId = notification.ExecutorId, RuntimeCode = notification.RuntimeCode, ScenarioCode = notification.ScenarioCode, @@ -101,6 +103,7 @@ public abstract class BaseControllerHandler FlowId = notification.FlowId, DeviceCode = notification.DeviceCode, TerminalDevices = notification.TerminalDevices, + NextNodeInfo = notification.NextNodeInfo, ErrorMessage = errorMessage, ErrorCode = errorCode, Timestamp = DateTime.UtcNow @@ -147,4 +150,48 @@ public abstract class BaseControllerHandler // 模拟异步操作 await Task.CompletedTask; } + + /// + /// 安全地记录步骤执行日志 + /// 使用独立作用域避免 ObjectDisposedException + /// + /// 步骤执行明细ID + /// 日志类型 + /// 日志内容 + /// 取消令牌 + /// 记录日志任务 + protected virtual async Task LogStepExecutionSafelyAsync( + string stepDetailId, + StepLogType logType, + string content, + CancellationToken cancellationToken) + { + // 参数验证 + if (string.IsNullOrWhiteSpace(stepDetailId)) + { + _logger.LogWarning("步骤执行明细ID为空,跳过日志记录"); + return; + } + + // 日志类型是枚举,不需要验证 + + var result = await _scopeExecutor.ExecuteAsync(async serviceProvider => + { + var stepLogRepository = serviceProvider.GetRequiredService(); + var unitOfWork = serviceProvider.GetRequiredService(); + + var stepLog = TestScenarioTaskExecutionStepLog.Create(stepDetailId, logType, content); + await stepLogRepository.AddAsync(stepLog); + await unitOfWork.SaveChangesAsync(); + + _logger.LogDebug("成功记录步骤执行日志,步骤ID: {StepDetailId}, 日志类型: {LogType}", + stepDetailId, logType); + }, cancellationToken); + + if (!result.IsSuccess) + { + _logger.LogError("记录步骤执行日志失败,步骤ID: {StepDetailId}, 日志类型: {LogType}, 错误: {ErrorMessage}", + stepDetailId, logType, result.ErrorMessage); + } + } } diff --git a/src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/DisableFlightModeControllerHandler.cs b/src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/DisableFlightModeControllerHandler.cs index acf926a..ef5180f 100644 --- a/src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/DisableFlightModeControllerHandler.cs +++ b/src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/DisableFlightModeControllerHandler.cs @@ -25,17 +25,17 @@ public class DisableFlightModeControllerHandler : BaseControllerHandler, INotifi public async Task Handle(DisableFlightModeExecutionEvent notification, CancellationToken cancellationToken) { _logger.LogInformation("开始执行关闭飞行模式,任务执行ID: {TaskExecutionId}, 节点ID: {NodeId}", - notification.TaskExecutionId, notification.NodeId); + notification.TaskExecutionId, notification.NextNodeInfo.NodeId); try { - await UpdateNodeStatusAsync(notification.NodeId, NodeExecutionStatus.Running); + await UpdateNodeStatusAsync(notification.NextNodeInfo.NodeId, NodeExecutionStatus.Running); var result = await ExecuteDisableFlightModeAsync(notification, cancellationToken); var executionResult = new NodeExecutionResult { TaskExecutionId = notification.TaskExecutionId, - NodeId = notification.NodeId, + NodeId = notification.NextNodeInfo.NodeId, StepMapping = StepMapping.DisableFlightMode, Status = NodeExecutionStatus.Completed, IsSuccess = true, @@ -50,7 +50,7 @@ public class DisableFlightModeControllerHandler : BaseControllerHandler, INotifi catch (Exception ex) { _logger.LogError(ex, "关闭飞行模式执行失败,任务执行ID: {TaskExecutionId}, 节点ID: {NodeId}", - notification.TaskExecutionId, notification.NodeId); + notification.TaskExecutionId, notification.NextNodeInfo.NodeId); await PublishFailedEventSafelyAsync(notification, ex.Message, "DISABLE_FLIGHT_MODE_ERROR", StepMapping.DisableFlightMode, cancellationToken); } } diff --git a/src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/EnableFlightModeControllerHandler.cs b/src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/EnableFlightModeControllerHandler.cs index 6445e40..d7fbbf8 100644 --- a/src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/EnableFlightModeControllerHandler.cs +++ b/src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/EnableFlightModeControllerHandler.cs @@ -25,17 +25,17 @@ public class EnableFlightModeControllerHandler : BaseControllerHandler, INotific public async Task Handle(EnableFlightModeExecutionEvent notification, CancellationToken cancellationToken) { _logger.LogInformation("开始执行开启飞行模式,任务执行ID: {TaskExecutionId}, 节点ID: {NodeId}", - notification.TaskExecutionId, notification.NodeId); + notification.TaskExecutionId, notification.NextNodeInfo.NodeId); try { - await UpdateNodeStatusAsync(notification.NodeId, NodeExecutionStatus.Running); + await UpdateNodeStatusAsync(notification.NextNodeInfo.NodeId, NodeExecutionStatus.Running); var result = await ExecuteEnableFlightModeAsync(notification, cancellationToken); var executionResult = new NodeExecutionResult { TaskExecutionId = notification.TaskExecutionId, - NodeId = notification.NodeId, + NodeId = notification.NextNodeInfo.NodeId, StepMapping = StepMapping.EnableFlightMode, Status = NodeExecutionStatus.Completed, IsSuccess = true, @@ -50,7 +50,7 @@ public class EnableFlightModeControllerHandler : BaseControllerHandler, INotific catch (Exception ex) { _logger.LogError(ex, "开启飞行模式执行失败,任务执行ID: {TaskExecutionId}, 节点ID: {NodeId}", - notification.TaskExecutionId, notification.NodeId); + notification.TaskExecutionId, notification.NextNodeInfo.NodeId); await PublishFailedEventSafelyAsync(notification, ex.Message, "ENABLE_FLIGHT_MODE_ERROR", StepMapping.EnableFlightMode, cancellationToken); } } diff --git a/src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/EndFlowControllerHandler.cs b/src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/EndFlowControllerHandler.cs index 77f8d12..84256d8 100644 --- a/src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/EndFlowControllerHandler.cs +++ b/src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/EndFlowControllerHandler.cs @@ -25,17 +25,17 @@ public class EndFlowControllerHandler : BaseControllerHandler, INotificationHand public async Task Handle(EndFlowExecutionEvent notification, CancellationToken cancellationToken) { _logger.LogInformation("开始执行结束流程,任务执行ID: {TaskExecutionId}, 节点ID: {NodeId}", - notification.TaskExecutionId, notification.NodeId); + notification.TaskExecutionId, notification.NextNodeInfo.NodeId); try { - await UpdateNodeStatusAsync(notification.NodeId, NodeExecutionStatus.Running); + await UpdateNodeStatusAsync(notification.NextNodeInfo.NodeId, NodeExecutionStatus.Running); var result = await ExecuteEndFlowAsync(notification, cancellationToken); var executionResult = new NodeExecutionResult { TaskExecutionId = notification.TaskExecutionId, - NodeId = notification.NodeId, + NodeId = notification.NextNodeInfo.NodeId, StepMapping = StepMapping.EndFlow, Status = NodeExecutionStatus.Completed, IsSuccess = true, @@ -50,7 +50,7 @@ public class EndFlowControllerHandler : BaseControllerHandler, INotificationHand catch (Exception ex) { _logger.LogError(ex, "结束流程执行失败,任务执行ID: {TaskExecutionId}, 节点ID: {NodeId}", - notification.TaskExecutionId, notification.NodeId); + notification.TaskExecutionId, notification.NextNodeInfo.NodeId); await PublishFailedEventSafelyAsync(notification, ex.Message, "END_FLOW_ERROR", StepMapping.EndFlow, cancellationToken); } } diff --git a/src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/ImsiRegistrationControllerHandler.cs b/src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/ImsiRegistrationControllerHandler.cs index f07d10b..1b46972 100644 --- a/src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/ImsiRegistrationControllerHandler.cs +++ b/src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/ImsiRegistrationControllerHandler.cs @@ -25,17 +25,17 @@ public class ImsiRegistrationControllerHandler : BaseControllerHandler, INotific public async Task Handle(ImsiRegistrationExecutionEvent notification, CancellationToken cancellationToken) { _logger.LogInformation("开始执行IMSI注册,任务执行ID: {TaskExecutionId}, 节点ID: {NodeId}", - notification.TaskExecutionId, notification.NodeId); + notification.TaskExecutionId, notification.NextNodeInfo.NodeId); try { - await UpdateNodeStatusAsync(notification.NodeId, NodeExecutionStatus.Running); + await UpdateNodeStatusAsync(notification.NextNodeInfo.NodeId, NodeExecutionStatus.Running); var result = await ExecuteImsiRegistrationAsync(notification, cancellationToken); var executionResult = new NodeExecutionResult { - TaskExecutionId = notification.TaskExecutionId, - NodeId = notification.NodeId, + TaskExecutionId = notification.TaskExecutionId, + NodeId = notification.NextNodeInfo.NodeId, StepMapping = StepMapping.ImsiRegistration, Status = NodeExecutionStatus.Completed, IsSuccess = true, @@ -50,7 +50,7 @@ public class ImsiRegistrationControllerHandler : BaseControllerHandler, INotific catch (Exception ex) { _logger.LogError(ex, "IMSI注册执行失败,任务执行ID: {TaskExecutionId}, 节点ID: {NodeId}", - notification.TaskExecutionId, notification.NodeId); + notification.TaskExecutionId, notification.NextNodeInfo.NodeId); await PublishFailedEventSafelyAsync(notification, ex.Message, "IMSI_REGISTRATION_ERROR", StepMapping.ImsiRegistration, cancellationToken); } } diff --git a/src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/StartFlowControllerHandler.cs b/src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/StartFlowControllerHandler.cs index a99348b..1a529e9 100644 --- a/src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/StartFlowControllerHandler.cs +++ b/src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/StartFlowControllerHandler.cs @@ -37,12 +37,12 @@ public class StartFlowControllerHandler : BaseControllerHandler, INotificationHa public async Task Handle(StartFlowExecutionEvent notification, CancellationToken cancellationToken) { _logger.LogInformation("开始执行启动流程,任务执行ID: {TaskExecutionId}, 节点ID: {NodeId}, 运行时编码: {RuntimeCode}", - notification.TaskExecutionId, notification.NodeId, notification.RuntimeCode); - + notification.TaskExecutionId, notification.NextNodeInfo.NodeId, notification.RuntimeCode); + //await LogStepExecutionSafelyAsync() try { // 1. 更新节点状态为运行中 - await UpdateNodeStatusAsync(notification.NodeId, NodeExecutionStatus.Running); + await UpdateNodeStatusAsync(notification.NextNodeInfo.NodeId, NodeExecutionStatus.Running); // 2. 执行启动流程业务逻辑 var result = await ExecuteStartFlowAsync(notification, cancellationToken); @@ -51,7 +51,7 @@ public class StartFlowControllerHandler : BaseControllerHandler, INotificationHa var executionResult = new NodeExecutionResult { TaskExecutionId = notification.TaskExecutionId, - NodeId = notification.NodeId, + NodeId = notification.NextNodeInfo.NodeId, StepMapping = StepMapping.StartFlow, Status = NodeExecutionStatus.Completed, IsSuccess = true, @@ -65,12 +65,12 @@ public class StartFlowControllerHandler : BaseControllerHandler, INotificationHa await PublishCompletedEventSafelyAsync(notification, executionResult, StepMapping.StartFlow, cancellationToken); _logger.LogInformation("启动流程执行成功,任务执行ID: {TaskExecutionId}, 节点ID: {NodeId}", - notification.TaskExecutionId, notification.NodeId); + notification.TaskExecutionId, notification.NextNodeInfo.NodeId); } catch (Exception ex) { _logger.LogError(ex, "启动流程执行失败,任务执行ID: {TaskExecutionId}, 节点ID: {NodeId}", - notification.TaskExecutionId, notification.NodeId); + notification.TaskExecutionId, notification.NextNodeInfo.NodeId); // 发布失败事件 await PublishFailedEventSafelyAsync(notification, ex.Message, "START_FLOW_ERROR", StepMapping.StartFlow, cancellationToken); diff --git a/src/X1.Application/Features/TaskExecution/Events/EventHandlers/BaseEventHandler.cs b/src/X1.Application/Features/TaskExecution/Events/EventHandlers/BaseEventHandler.cs index 6647a25..260b624 100644 --- a/src/X1.Application/Features/TaskExecution/Events/EventHandlers/BaseEventHandler.cs +++ b/src/X1.Application/Features/TaskExecution/Events/EventHandlers/BaseEventHandler.cs @@ -119,7 +119,7 @@ public abstract class BaseEventHandler // 这里应该将执行结果保存到数据库 _logger.LogInformation("记录执行结果,任务执行ID: {TaskExecutionId}, 节点ID: {NodeId}, 执行成功: {IsSuccess}", - notification.TaskExecutionId, notification.NodeId, notification.Result.IsSuccess); + notification.TaskExecutionId, notification.NextNodeInfo.NodeId, notification.Result.IsSuccess); // 模拟异步操作 await Task.CompletedTask; @@ -136,7 +136,7 @@ public abstract class BaseEventHandler // 这里应该将失败信息保存到数据库 _logger.LogInformation("记录失败信息,任务执行ID: {TaskExecutionId}, 节点ID: {NodeId}, 错误消息: {ErrorMessage}", - notification.TaskExecutionId, notification.NodeId, notification.ErrorMessage); + notification.TaskExecutionId, notification.NextNodeInfo.NodeId, notification.ErrorMessage); // 模拟异步操作 await Task.CompletedTask; diff --git a/src/X1.Application/Features/TaskExecution/Events/EventHandlers/NodeExecutionCompletedEventHandler.cs b/src/X1.Application/Features/TaskExecution/Events/EventHandlers/NodeExecutionCompletedEventHandler.cs index 715c4ef..de810e3 100644 --- a/src/X1.Application/Features/TaskExecution/Events/EventHandlers/NodeExecutionCompletedEventHandler.cs +++ b/src/X1.Application/Features/TaskExecution/Events/EventHandlers/NodeExecutionCompletedEventHandler.cs @@ -47,23 +47,23 @@ public class NodeExecutionCompletedEventHandler : BaseEventHandler, INotificatio public async Task Handle(NodeExecutionCompletedEvent notification, CancellationToken cancellationToken) { _logger.LogInformation("处理节点执行完成事件,任务执行ID: {TaskExecutionId}, 节点ID: {NodeId}, 步骤映射: {StepMapping}", - notification.TaskExecutionId, notification.NodeId, notification.StepMapping); + notification.TaskExecutionId, notification.NextNodeInfo.NodeId, notification.NextNodeInfo.StepMapping); try { // 1. 更新节点状态为完成 - await UpdateNodeStatusAsync(notification.NodeId, NodeExecutionStatus.Completed); + await UpdateNodeStatusAsync(notification.NextNodeInfo.NodeId, NodeExecutionStatus.Completed); // 2. 记录执行结果 await LogExecutionResultAsync(notification); // 3. 获取下一个节点 - var nextNode = await GetNextNodeAsync(notification.TaskExecutionId, notification.NodeId); + var nextNode = await GetNextNodeAsync(notification.TaskExecutionId, notification.NextNodeInfo.NodeId); if (nextNode != null) { _logger.LogInformation("发现下一个节点,任务执行ID: {TaskExecutionId}, 当前节点ID: {CurrentNodeId}, 下一个节点ID: {NextNodeId}", - notification.TaskExecutionId, notification.NodeId, nextNode.NodeId); + notification.TaskExecutionId, notification.NextNodeInfo.NodeId, nextNode.NodeId); // 4. 发布下一个节点执行事件 var nextNodeEvent = CreateNodeExecutionStartedEvent(notification, nextNode); @@ -81,7 +81,7 @@ public class NodeExecutionCompletedEventHandler : BaseEventHandler, INotificatio catch (Exception ex) { _logger.LogError(ex, "处理节点执行完成事件时发生错误,任务执行ID: {TaskExecutionId}, 节点ID: {NodeId}", - notification.TaskExecutionId, notification.NodeId); + notification.TaskExecutionId, notification.NextNodeInfo.NodeId); // 发布失败事件 var failedEvent = CreateFailedEvent(notification, @@ -102,8 +102,6 @@ public class NodeExecutionCompletedEventHandler : BaseEventHandler, INotificatio return new NodeExecutionStartedEvent { TaskExecutionId = notification.TaskExecutionId, - NodeId = nextNode.NodeId, - StepMapping = nextNode.StepMapping, ExecutorId = notification.ExecutorId, RuntimeCode = notification.RuntimeCode, ScenarioCode = notification.ScenarioCode, @@ -112,6 +110,7 @@ public class NodeExecutionCompletedEventHandler : BaseEventHandler, INotificatio FlowId = notification.FlowId, DeviceCode = notification.DeviceCode, TerminalDevices = notification.TerminalDevices, + NextNodeInfo = nextNode, Timestamp = DateTime.UtcNow }; } @@ -128,8 +127,6 @@ public class NodeExecutionCompletedEventHandler : BaseEventHandler, INotificatio return new NodeExecutionFailedEvent { TaskExecutionId = notification.TaskExecutionId, - NodeId = notification.NodeId, - StepMapping = notification.StepMapping, ExecutorId = notification.ExecutorId, RuntimeCode = notification.RuntimeCode, ScenarioCode = notification.ScenarioCode, @@ -138,6 +135,7 @@ public class NodeExecutionCompletedEventHandler : BaseEventHandler, INotificatio FlowId = notification.FlowId, DeviceCode = notification.DeviceCode, TerminalDevices = notification.TerminalDevices, + NextNodeInfo = notification.NextNodeInfo, ErrorMessage = errorMessage, ErrorCode = errorCode, Timestamp = DateTime.UtcNow @@ -195,6 +193,7 @@ public class NodeExecutionCompletedEventHandler : BaseEventHandler, INotificatio NodeId = targetNode.NodeId, StepMapping = targetNode.StepConfig.Mapping, NodeName = targetNode.StepConfig.StepName, + StepId = targetNode.StepId, SequenceNumber = targetNode.SequenceNumber }; diff --git a/src/X1.Application/Features/TaskExecution/Events/EventHandlers/NodeExecutionEventRouter.cs b/src/X1.Application/Features/TaskExecution/Events/EventHandlers/NodeExecutionEventRouter.cs index b794efd..c766cac 100644 --- a/src/X1.Application/Features/TaskExecution/Events/EventHandlers/NodeExecutionEventRouter.cs +++ b/src/X1.Application/Features/TaskExecution/Events/EventHandlers/NodeExecutionEventRouter.cs @@ -42,14 +42,14 @@ public class NodeExecutionEventRouter : BaseEventHandler, INotificationHandler public string TaskExecutionId { get; set; } = null!; - /// - /// 节点ID - /// - public string NodeId { get; set; } = null!; - - /// - /// 步骤映射类型 - /// - public StepMapping StepMapping { get; set; } - /// /// 执行人/执行终端ID /// @@ -75,6 +65,11 @@ public abstract class BaseNodeExecutionEvent : INodeExecutionEvent /// public DateTime Timestamp { get; set; } = DateTime.UtcNow; + /// + /// 下一个节点信息 + /// + public NextNodeInfo NextNodeInfo { get; set; } = null!; + /// /// 静态工厂方法:从 NodeExecutionStartedEvent 创建具体事件实例 /// @@ -87,14 +82,15 @@ public abstract class BaseNodeExecutionEvent : INodeExecutionEvent { 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, + DeviceCode = notification.DeviceCode, + TerminalDevices = notification.TerminalDevices, + NextNodeInfo = notification.NextNodeInfo, Timestamp = DateTime.UtcNow }; } diff --git a/src/X1.Domain/Common/Enums/StepLogType.cs b/src/X1.Domain/Common/Enums/StepLogType.cs new file mode 100644 index 0000000..727db2c --- /dev/null +++ b/src/X1.Domain/Common/Enums/StepLogType.cs @@ -0,0 +1,58 @@ +using System.ComponentModel; + +namespace X1.Domain.Common.Enums +{ + /// + /// 步骤执行日志类型枚举 + /// + public enum StepLogType + { + /// + /// 错误日志 + /// + [Description("错误")] + Error = 1, + + /// + /// 成功日志 + /// + [Description("成功")] + Success = 2, + + /// + /// 调试日志 + /// + [Description("调试")] + Debug = 3, + + /// + /// 信息日志 + /// + [Description("信息")] + Info = 4, + + /// + /// 警告日志 + /// + [Description("警告")] + Warn = 5, + + /// + /// 输出日志 + /// + [Description("输出")] + Output = 6, + + /// + /// 参数日志 + /// + [Description("参数")] + Parameter = 7, + + /// + /// 其他日志 + /// + [Description("其他")] + Other = 8 + } +} diff --git a/src/X1.Domain/Entities/TestTask/TestScenarioTaskExecutionDetail.cs b/src/X1.Domain/Entities/TestTask/TestScenarioTaskExecutionDetail.cs index b6a9f42..7077681 100644 --- a/src/X1.Domain/Entities/TestTask/TestScenarioTaskExecutionDetail.cs +++ b/src/X1.Domain/Entities/TestTask/TestScenarioTaskExecutionDetail.cs @@ -227,5 +227,112 @@ namespace X1.Domain.Entities.TestTask { return $"{Progress}%"; } + + /// + /// 更新执行状态 + /// + /// 新的执行状态 + public void UpdateStatus(TaskExecutionStatus newStatus) + { + Status = newStatus; + } + + /// + /// 更新执行状态并设置相关时间 + /// + /// 新的执行状态 + /// 更新时间(可选,默认为当前时间) + public void UpdateStatusWithTime(TaskExecutionStatus newStatus, DateTime? updateTime = null) + { + var time = updateTime ?? DateTime.UtcNow; + + switch (newStatus) + { + case TaskExecutionStatus.Running: + if (Status == TaskExecutionStatus.Pending) + { + StartTime = time; + EndTime = null; + Duration = 0; + } + break; + case TaskExecutionStatus.Success: + case TaskExecutionStatus.Failed: + if (Status == TaskExecutionStatus.Running && StartTime.HasValue) + { + EndTime = time; + Duration = (int)(time - StartTime.Value).TotalSeconds; + Progress = 100; + } + break; + } + + Status = newStatus; + } + + /// + /// 重置执行状态为待执行 + /// + public void ResetToPending() + { + Status = TaskExecutionStatus.Pending; + StartTime = null; + EndTime = null; + Duration = 0; + Progress = 0; + } + + /// + /// 检查是否可以更新到指定状态 + /// + /// 目标状态 + /// 是否可以更新 + public bool CanUpdateToStatus(TaskExecutionStatus targetStatus) + { + return Status switch + { + TaskExecutionStatus.Pending => targetStatus == TaskExecutionStatus.Running || + targetStatus == TaskExecutionStatus.Failed, + TaskExecutionStatus.Running => targetStatus == TaskExecutionStatus.Success || + targetStatus == TaskExecutionStatus.Failed, + TaskExecutionStatus.Success => false, // 成功状态不能转换到其他状态 + TaskExecutionStatus.Failed => false, // 失败状态不能转换到其他状态 + _ => false + }; + } + + /// + /// 获取状态转换描述 + /// + /// 目标状态 + /// 状态转换描述 + public string GetStatusTransitionDescription(TaskExecutionStatus targetStatus) + { + if (CanUpdateToStatus(targetStatus)) + { + return $"可以从 {GetStatusDescription()} 转换到 {GetStatusDescription(targetStatus)}"; + } + else + { + return $"不能从 {GetStatusDescription()} 转换到 {GetStatusDescription(targetStatus)}"; + } + } + + /// + /// 获取指定状态的描述 + /// + /// 状态 + /// 状态描述 + private static string GetStatusDescription(TaskExecutionStatus status) + { + return status switch + { + TaskExecutionStatus.Pending => "待执行", + TaskExecutionStatus.Running => "执行中", + TaskExecutionStatus.Success => "执行成功", + TaskExecutionStatus.Failed => "执行失败", + _ => "未知状态" + }; + } } } diff --git a/src/X1.Domain/Entities/TestTask/TestScenarioTaskExecutionStepLog.cs b/src/X1.Domain/Entities/TestTask/TestScenarioTaskExecutionStepLog.cs index 006c842..8d31875 100644 --- a/src/X1.Domain/Entities/TestTask/TestScenarioTaskExecutionStepLog.cs +++ b/src/X1.Domain/Entities/TestTask/TestScenarioTaskExecutionStepLog.cs @@ -1,6 +1,7 @@ using System; using System.ComponentModel.DataAnnotations; using X1.Domain.Abstractions; +using X1.Domain.Common.Enums; namespace X1.Domain.Entities.TestTask { @@ -17,11 +18,10 @@ namespace X1.Domain.Entities.TestTask public string StepDetailId { get; set; } = null!; /// - /// 日志类型:error/output/param/etc + /// 日志类型 /// [Required] - [MaxLength(50)] - public string LogType { get; set; } = null!; + public StepLogType LogType { get; set; } = StepLogType.Info; /// /// 日志内容或输出信息 @@ -42,7 +42,7 @@ namespace X1.Domain.Entities.TestTask /// 步骤执行日志记录 public static TestScenarioTaskExecutionStepLog Create( string stepDetailId, - string logType, + StepLogType logType, string? content = null) { var stepLog = new TestScenarioTaskExecutionStepLog @@ -67,7 +67,59 @@ namespace X1.Domain.Entities.TestTask string stepDetailId, string errorMessage) { - return Create(stepDetailId, "error", errorMessage); + return Create(stepDetailId, StepLogType.Error, errorMessage); + } + + /// + /// 创建成功日志 + /// + /// 步骤执行明细ID + /// 成功信息 + /// 成功日志记录 + public static TestScenarioTaskExecutionStepLog CreateSuccessLog( + string stepDetailId, + string successMessage) + { + return Create(stepDetailId, StepLogType.Success, successMessage); + } + + /// + /// 创建调试日志 + /// + /// 步骤执行明细ID + /// 调试信息 + /// 调试日志记录 + public static TestScenarioTaskExecutionStepLog CreateDebugLog( + string stepDetailId, + string debugMessage) + { + return Create(stepDetailId, StepLogType.Debug, debugMessage); + } + + /// + /// 创建信息日志 + /// + /// 步骤执行明细ID + /// 信息内容 + /// 信息日志记录 + public static TestScenarioTaskExecutionStepLog CreateInfoLog( + string stepDetailId, + string infoMessage) + { + return Create(stepDetailId, StepLogType.Info, infoMessage); + } + + /// + /// 创建警告日志 + /// + /// 步骤执行明细ID + /// 警告信息 + /// 警告日志记录 + public static TestScenarioTaskExecutionStepLog CreateWarningLog( + string stepDetailId, + string warningMessage) + { + return Create(stepDetailId, StepLogType.Warn, warningMessage); } /// @@ -80,7 +132,7 @@ namespace X1.Domain.Entities.TestTask string stepDetailId, string outputContent) { - return Create(stepDetailId, "output", outputContent); + return Create(stepDetailId, StepLogType.Output, outputContent); } /// @@ -89,11 +141,11 @@ namespace X1.Domain.Entities.TestTask /// 步骤执行明细ID /// 参数内容 /// 参数日志记录 - public static TestScenarioTaskExecutionStepLog CreateParamLog( + public static TestScenarioTaskExecutionStepLog CreateParameterLog( string stepDetailId, string paramContent) { - return Create(stepDetailId, "param", paramContent); + return Create(stepDetailId, StepLogType.Parameter, paramContent); } /// @@ -113,10 +165,14 @@ namespace X1.Domain.Entities.TestTask { return LogType switch { - "error" => "错误日志", - "output" => "输出日志", - "param" => "参数日志", - "etc" => "其他日志", + StepLogType.Error => "错误日志", + StepLogType.Success => "成功日志", + StepLogType.Debug => "调试日志", + StepLogType.Info => "信息日志", + StepLogType.Warn => "警告日志", + StepLogType.Output => "输出日志", + StepLogType.Parameter => "参数日志", + StepLogType.Other => "其他日志", _ => $"未知类型({LogType})" }; } @@ -127,7 +183,43 @@ namespace X1.Domain.Entities.TestTask /// 是否为错误日志 public bool IsErrorLog() { - return LogType.Equals("error", StringComparison.OrdinalIgnoreCase); + return LogType == StepLogType.Error; + } + + /// + /// 检查是否为成功日志 + /// + /// 是否为成功日志 + public bool IsSuccessLog() + { + return LogType == StepLogType.Success; + } + + /// + /// 检查是否为调试日志 + /// + /// 是否为调试日志 + public bool IsDebugLog() + { + return LogType == StepLogType.Debug; + } + + /// + /// 检查是否为信息日志 + /// + /// 是否为信息日志 + public bool IsInfoLog() + { + return LogType == StepLogType.Info; + } + + /// + /// 检查是否为警告日志 + /// + /// 是否为警告日志 + public bool IsWarningLog() + { + return LogType == StepLogType.Warn; } /// @@ -136,16 +228,16 @@ namespace X1.Domain.Entities.TestTask /// 是否为输出日志 public bool IsOutputLog() { - return LogType.Equals("output", StringComparison.OrdinalIgnoreCase); + return LogType == StepLogType.Output; } /// /// 检查是否为参数日志 /// /// 是否为参数日志 - public bool IsParamLog() + public bool IsParameterLog() { - return LogType.Equals("param", StringComparison.OrdinalIgnoreCase); + return LogType == StepLogType.Parameter; } /// diff --git a/src/X1.Domain/Events/INodeExecutionEvent.cs b/src/X1.Domain/Events/INodeExecutionEvent.cs index 544a5c8..f00b8af 100644 --- a/src/X1.Domain/Events/INodeExecutionEvent.cs +++ b/src/X1.Domain/Events/INodeExecutionEvent.cs @@ -20,16 +20,6 @@ public interface INodeExecutionEvent : INotification /// string TaskExecutionId { get; } - /// - /// 节点ID - /// - string NodeId { get; } - - /// - /// 步骤映射类型 - /// - StepMapping StepMapping { get; } - /// /// 执行人/执行终端ID /// @@ -74,6 +64,11 @@ public interface INodeExecutionEvent : INotification /// 事件时间戳 /// DateTime Timestamp { get; } + + /// + /// 下一个节点信息 + /// + NextNodeInfo NextNodeInfo { get; } } /// diff --git a/src/X1.Domain/Models/NextNodeInfo.cs b/src/X1.Domain/Models/NextNodeInfo.cs index ebab452..467f696 100644 --- a/src/X1.Domain/Models/NextNodeInfo.cs +++ b/src/X1.Domain/Models/NextNodeInfo.cs @@ -21,8 +21,11 @@ public class NextNodeInfo /// /// 节点名称 /// - public string? NodeName { get; set; } - + public string NodeName { get; set; } = null!; + /// + /// 步骤Id + /// + public string StepId { get; set; } = null!; /// /// 执行顺序 /// diff --git a/src/X1.Domain/Models/TaskExecutionData.cs b/src/X1.Domain/Models/TaskExecutionData.cs new file mode 100644 index 0000000..4cae079 --- /dev/null +++ b/src/X1.Domain/Models/TaskExecutionData.cs @@ -0,0 +1,31 @@ +using X1.Domain.Entities.TestTask; +using X1.Domain.Services; + +namespace X1.Domain.Models; + +/// +/// 任务执行数据模型 +/// 用于存储任务执行过程中收集的所有相关数据 +/// +public class TaskExecutionData +{ + /// + /// 任务执行请求 + /// + public TaskExecutionRequest TaskRequest { get; set; } = null!; + + /// + /// 任务执行详情 + /// + public TestScenarioTaskExecutionDetail TaskExecution { get; set; } = null!; + + /// + /// 初始节点信息 + /// + public InitialNodeInfo? InitialNodeInfo { get; set; } + + /// + /// 终端设备列表 + /// + public IReadOnlyList TerminalDevices { get; set; } = new List(); +} diff --git a/src/X1.Domain/Models/TaskExecutionRequest.cs b/src/X1.Domain/Models/TaskExecutionRequest.cs new file mode 100644 index 0000000..503a2fa --- /dev/null +++ b/src/X1.Domain/Models/TaskExecutionRequest.cs @@ -0,0 +1,17 @@ +namespace X1.Domain.Models; + +/// +/// 任务执行请求 +/// +public class TaskExecutionRequest +{ + /// + /// 任务ID + /// + public string TaskId { get; set; } = null!; + + /// + /// 设备编码 + /// + public string DeviceCode { get; set; } = null!; +} diff --git a/src/X1.Domain/Repositories/TestTask/ITestScenarioTaskExecutionStepLogRepository.cs b/src/X1.Domain/Repositories/TestTask/ITestScenarioTaskExecutionStepLogRepository.cs index e63ef51..aa39ac5 100644 --- a/src/X1.Domain/Repositories/TestTask/ITestScenarioTaskExecutionStepLogRepository.cs +++ b/src/X1.Domain/Repositories/TestTask/ITestScenarioTaskExecutionStepLogRepository.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using X1.Domain.Entities.TestTask; using X1.Domain.Repositories.Base; using X1.Domain.Models.TestTask; +using X1.Domain.Common.Enums; namespace X1.Domain.Repositories.TestTask { @@ -27,7 +28,7 @@ namespace X1.Domain.Repositories.TestTask /// 日志类型 /// 取消令牌 /// 步骤执行日志列表 - Task> GetByLogTypeAsync(string logType, CancellationToken cancellationToken = default); + Task> GetByLogTypeAsync(StepLogType logType, CancellationToken cancellationToken = default); /// /// 根据创建时间范围获取步骤执行日志列表 @@ -73,7 +74,7 @@ namespace X1.Domain.Repositories.TestTask /// 步骤执行日志列表 Task> GetByConditionsAsync( string? stepDetailId = null, - string? logType = null, + StepLogType? logType = null, DateTime? startTime = null, DateTime? endTime = null, CancellationToken cancellationToken = default); diff --git a/src/X1.Domain/Services/ITaskExecutionService.cs b/src/X1.Domain/Services/ITaskExecutionService.cs index e7926c9..b907f07 100644 --- a/src/X1.Domain/Services/ITaskExecutionService.cs +++ b/src/X1.Domain/Services/ITaskExecutionService.cs @@ -42,6 +42,34 @@ public interface ITaskExecutionService /// 取消令牌 /// 终端设备列表 Task> GetTerminalDevicesByTaskIdAsync(string taskId, CancellationToken cancellationToken = default); + + /// + /// 更新任务执行状态 + /// + /// 任务执行ID + /// 新的执行状态 + /// 执行进度 (0-100,可选) + /// 取消令牌 + /// 操作结果 + Task UpdateTaskExecutionStatusAsync(string taskExecutionId, TaskExecutionStatus status, int? progress = null, CancellationToken cancellationToken = default); + + /// + /// 批量创建任务执行用例明细初始化数据 + /// + /// 任务执行记录ID + /// 场景编码 + /// 测试用例流程ID列表 + /// 执行人/终端ID + /// 执行轮次 + /// 取消令牌 + /// 创建的任务执行用例明细列表 + Task> BatchCreateTaskExecutionCaseDetailsAsync( + string executionId, + string scenarioCode, + IReadOnlyList testCaseFlowIds, + string executorId, + int loop = 1, + CancellationToken cancellationToken = default); } /// @@ -94,4 +122,13 @@ public class InitialNodeItem /// 测试用例流程名称 /// public string FlowName { get; set; } = null!; + + /// + /// 节点名称 + /// + public string NodeName { get; set; } = null!; + /// + /// 步骤Id + /// + public string StepId { get; set; } = null!; } diff --git a/src/X1.Infrastructure/Configurations/TestTask/TestScenarioTaskExecutionStepLogConfiguration.cs b/src/X1.Infrastructure/Configurations/TestTask/TestScenarioTaskExecutionStepLogConfiguration.cs index 07dcb95..adb91cd 100644 --- a/src/X1.Infrastructure/Configurations/TestTask/TestScenarioTaskExecutionStepLogConfiguration.cs +++ b/src/X1.Infrastructure/Configurations/TestTask/TestScenarioTaskExecutionStepLogConfiguration.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using X1.Domain.Entities.TestTask; +using X1.Domain.Common.Enums; namespace X1.Infrastructure.Configurations.TestTask { @@ -28,7 +29,7 @@ namespace X1.Infrastructure.Configurations.TestTask builder.Property(x => x.LogType) .IsRequired() - .HasMaxLength(50); + .HasConversion(); builder.Property(x => x.Content); diff --git a/src/X1.Infrastructure/Migrations/20250905030203_UpdateStepLogTypeToEnum.Designer.cs b/src/X1.Infrastructure/Migrations/20250905030203_UpdateStepLogTypeToEnum.Designer.cs new file mode 100644 index 0000000..59058f5 --- /dev/null +++ b/src/X1.Infrastructure/Migrations/20250905030203_UpdateStepLogTypeToEnum.Designer.cs @@ -0,0 +1,2831 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using X1.Infrastructure.Context; + +#nullable disable + +namespace X1.Infrastructure.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20250905030203_UpdateStepLogTypeToEnum")] + partial class UpdateStepLogTypeToEnum + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("X1.Domain.Entities.AppRole", b => + { + b.Property("Id") + .HasColumnType("text") + .HasComment("角色ID,主键"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text") + .HasComment("并发控制戳"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasComment("角色描述"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("角色名称"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("标准化角色名称(大写)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("更新时间"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("IX_Roles_Name"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("tb_roles", null, t => + { + t.HasComment("角色表"); + }); + }); + + modelBuilder.Entity("X1.Domain.Entities.AppUser", b => + { + b.Property("Id") + .HasColumnType("text") + .HasComment("用户ID,主键"); + + b.Property("AccessFailedCount") + .HasColumnType("integer") + .HasComment("登录失败次数"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text") + .HasComment("并发控制戳"); + + b.Property("CreatedTime") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("电子邮箱"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean") + .HasComment("邮箱是否已验证"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasComment("用户状态(true: 启用, false: 禁用)"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasComment("是否已删除"); + + b.Property("LastLoginTime") + .HasColumnType("timestamp with time zone") + .HasComment("最后登录时间"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean") + .HasComment("是否启用账户锁定"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasComment("账户锁定结束时间"); + + b.Property("ModifiedTime") + .HasColumnType("timestamp with time zone") + .HasComment("修改时间"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("标准化电子邮箱(大写)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("标准化账号(大写)"); + + b.Property("PasswordHash") + .HasColumnType("text") + .HasComment("密码哈希值"); + + b.Property("PhoneNumber") + .IsRequired() + .HasColumnType("text") + .HasComment("电话号码"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean") + .HasComment("电话号码是否已验证"); + + b.Property("RealName") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasComment("用户名"); + + b.Property("SecurityStamp") + .HasColumnType("text") + .HasComment("安全戳,用于并发控制"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean") + .HasComment("是否启用双因素认证"); + + b.Property("UserName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("账号"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("IX_user_Email"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.HasIndex("PhoneNumber") + .IsUnique() + .HasDatabaseName("IX_user_PhoneNumber"); + + b.HasIndex("UserName") + .IsUnique() + .HasDatabaseName("IX_user_UserName"); + + b.ToTable("tb_users", null, t => + { + t.HasComment("用户表"); + }); + }); + + modelBuilder.Entity("X1.Domain.Entities.ButtonPermission", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("DisplayText") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Icon") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsSystem") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("NavigationMenuId") + .HasMaxLength(450) + .HasColumnType("character varying(450)") + .HasColumnName("NavigationMenuId"); + + b.Property("PagePath") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PermissionCode") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DisplayText"); + + b.HasIndex("IsEnabled"); + + b.HasIndex("IsSystem"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("NavigationMenuId"); + + b.HasIndex("PagePath"); + + b.HasIndex("PermissionCode") + .IsUnique(); + + b.HasIndex("Type"); + + b.HasIndex("NavigationMenuId", "Type"); + + b.HasIndex("PagePath", "Type") + .IsUnique(); + + b.ToTable("tb_button_permissions", (string)null); + }); + + modelBuilder.Entity("X1.Domain.Entities.Device.CellularDevice", b => + { + b.Property("Id") + .HasColumnType("text") + .HasComment("设备ID"); + + b.Property("AgentPort") + .HasColumnType("integer") + .HasComment("Agent端口"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasComment("设备描述"); + + b.Property("DeviceCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasComment("设备编码"); + + b.Property("IpAddress") + .IsRequired() + .HasMaxLength(45) + .HasColumnType("character varying(45)") + .HasComment("IP地址"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasComment("是否启用"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasComment("设备名称"); + + b.Property("SerialNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasComment("序列号"); + + b.Property("UpdatedAt") + .IsRequired() + .HasColumnType("timestamp with time zone") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("DeviceCode") + .IsUnique() + .HasDatabaseName("IX_cellular_device_DeviceCode"); + + b.HasIndex("SerialNumber") + .IsUnique() + .HasDatabaseName("IX_cellular_device_SerialNumber"); + + b.ToTable("tb_cellular_device", null, t => + { + t.HasComment("蜂窝设备表"); + }); + }); + + modelBuilder.Entity("X1.Domain.Entities.Device.CellularDeviceRuntime", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("NetworkStackCode") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("RuntimeCode") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("RuntimeStatus") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DeviceCode") + .IsUnique(); + + b.HasIndex("IsDeleted"); + + b.HasIndex("NetworkStackCode"); + + b.HasIndex("RuntimeCode"); + + b.HasIndex("RuntimeStatus"); + + b.HasIndex("DeviceCode", "CreatedAt") + .HasDatabaseName("IX_CellularDeviceRuntimes_DeviceCode_CreatedAt"); + + b.ToTable("tb_cellular_device_runtimes", (string)null); + }); + + modelBuilder.Entity("X1.Domain.Entities.Device.CellularDeviceRuntimeDetail", b => + { + b.Property("Id") + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("DeviceCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("NetworkStackCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("RuntimeCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("RuntimeStatus") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt") + .HasDatabaseName("IX_CellularDeviceRuntimeDetails_CreatedAt"); + + b.HasIndex("CreatedBy") + .HasDatabaseName("IX_CellularDeviceRuntimeDetails_CreatedBy"); + + b.HasIndex("RuntimeCode") + .HasDatabaseName("IX_CellularDeviceRuntimeDetails_RuntimeCode"); + + b.HasIndex("RuntimeStatus") + .HasDatabaseName("IX_CellularDeviceRuntimeDetails_RuntimeStatus"); + + b.ToTable("tb_cellular_device_runtime_details", (string)null); + }); + + modelBuilder.Entity("X1.Domain.Entities.Device.ProtocolVersion", b => + { + b.Property("Id") + .HasColumnType("text") + .HasComment("版本ID"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasComment("版本描述"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasComment("是否启用"); + + b.Property("MinimumSupportedVersion") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasComment("最低支持版本"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasComment("版本名称"); + + b.Property("ReleaseDate") + .HasColumnType("timestamp with time zone") + .HasComment("发布日期"); + + b.Property("SerialNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasComment("设备序列号"); + + b.Property("UpdatedAt") + .IsRequired() + .HasColumnType("timestamp with time zone") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasComment("版本号"); + + b.HasKey("Id"); + + b.HasIndex("SerialNumber") + .HasDatabaseName("IX_ProtocolVersions_SerialNumber"); + + b.HasIndex("Version") + .HasDatabaseName("IX_ProtocolVersions_Version"); + + b.ToTable("tb_protocol_versions", null, t => + { + t.HasComment("协议版本表"); + }); + }); + + modelBuilder.Entity("X1.Domain.Entities.Logging.LoginLog", b => + { + b.Property("Id") + .HasColumnType("text") + .HasComment("日志ID"); + + b.Property("Browser") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasComment("浏览器信息"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FailureReason") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasComment("失败原因"); + + b.Property("IpAddress") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasComment("登录IP"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsSuccess") + .HasColumnType("boolean") + .HasComment("登录状态(成功/失败)"); + + b.Property("Location") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasComment("登录位置"); + + b.Property("LoginSource") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("LoginTime") + .HasColumnType("timestamp with time zone") + .HasComment("登录时间"); + + b.Property("LoginType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OperatingSystem") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasComment("操作系统信息"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserAgent") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasComment("设备信息"); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)") + .HasComment("用户ID"); + + b.HasKey("Id"); + + b.HasIndex("IpAddress") + .HasDatabaseName("IX_LoginLogs_IpAddress"); + + b.HasIndex("LoginTime") + .HasDatabaseName("IX_LoginLogs_LoginTime"); + + b.HasIndex("UserId") + .HasDatabaseName("IX_LoginLogs_UserId"); + + b.HasIndex("UserId", "LoginTime") + .HasDatabaseName("IX_LoginLogs_UserId_LoginTime"); + + b.ToTable("tb_login_logs", null, t => + { + t.HasComment("用户登录日志表"); + }); + }); + + modelBuilder.Entity("X1.Domain.Entities.Logging.ProtocolLog", b => + { + b.Property("Id") + .HasColumnType("text") + .HasComment("主键ID"); + + b.Property("CellID") + .HasColumnType("integer") + .HasComment("小区ID"); + + b.Property("DeviceCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasComment("设备代码"); + + b.Property("Direction") + .HasColumnType("integer") + .HasComment("日志方向类型"); + + b.Property("IMSI") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasComment("国际移动用户识别码"); + + b.Property("Info") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasComment("信息字段"); + + b.Property("LayerType") + .HasMaxLength(50) + .HasColumnType("integer") + .HasComment("协议层类型"); + + b.Property("Message") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasComment("消息字段"); + + b.Property("MessageDetailJson") + .HasColumnType("text") + .HasComment("消息详情集合(JSON格式存储)"); + + b.Property("MessageId") + .HasColumnType("bigint") + .HasComment("消息ID"); + + b.Property("PLMN") + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasComment("公共陆地移动网络标识"); + + b.Property("RuntimeCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasComment("运行时代码"); + + b.Property("TimeMs") + .HasColumnType("bigint") + .HasComment("时间间隔(毫秒)"); + + b.Property("Timestamp") + .HasColumnType("bigint") + .HasComment("时间戳"); + + b.Property("UEID") + .HasColumnType("integer") + .HasComment("用户设备ID"); + + b.HasKey("Id"); + + b.HasIndex("DeviceCode") + .HasDatabaseName("IX_ProtocolLog_DeviceCode"); + + b.HasIndex("LayerType") + .HasDatabaseName("IX_ProtocolLog_LayerType"); + + b.HasIndex("MessageId") + .HasDatabaseName("IX_ProtocolLog_MessageId"); + + b.HasIndex("RuntimeCode") + .HasDatabaseName("IX_ProtocolLog_RuntimeCode"); + + b.HasIndex("Timestamp") + .HasDatabaseName("IX_ProtocolLog_Timestamp"); + + b.HasIndex("DeviceCode", "RuntimeCode") + .HasDatabaseName("IX_ProtocolLog_DeviceCode_RuntimeCode"); + + b.HasIndex("DeviceCode", "Timestamp") + .HasDatabaseName("IX_ProtocolLog_DeviceCode_Timestamp"); + + b.ToTable("tb_protocol_logs", null, t => + { + t.HasComment("协议日志表"); + }); + }); + + modelBuilder.Entity("X1.Domain.Entities.NavigationMenu", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasComment("菜单描述"); + + b.Property("Icon") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasComment("菜单图标名称,存储 Lucide 图标名称"); + + b.Property("IsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasComment("是否启用"); + + b.Property("IsSystem") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasComment("是否系统菜单"); + + b.Property("ParentId") + .HasMaxLength(450) + .HasColumnType("character varying(450)") + .HasComment("父级菜单ID,null 表示顶级菜单(根节点)"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasComment("菜单路径,格式为 /dashboard/{resource}"); + + b.Property("PermissionCode") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasComment("权限代码,格式为 {resource}.{action}"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasComment("排序,数字越小越靠前"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasComment("菜单标题"); + + b.Property("Type") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1) + .HasComment("菜单类型:1=独立菜单项,2=菜单组,3=子菜单项"); + + b.HasKey("Id"); + + b.HasIndex("IsEnabled") + .HasDatabaseName("IX_NavigationMenus_IsEnabled"); + + b.HasIndex("ParentId") + .HasDatabaseName("IX_NavigationMenus_ParentId"); + + b.HasIndex("Path") + .IsUnique() + .HasDatabaseName("IX_NavigationMenus_Path"); + + b.HasIndex("PermissionCode") + .HasDatabaseName("IX_NavigationMenus_PermissionCode"); + + b.HasIndex("SortOrder") + .HasDatabaseName("IX_NavigationMenus_SortOrder"); + + b.HasIndex("Title") + .HasDatabaseName("IX_NavigationMenus_Title"); + + b.HasIndex("Type") + .HasDatabaseName("IX_NavigationMenus_Type"); + + b.HasIndex("ParentId", "SortOrder") + .HasDatabaseName("IX_NavigationMenus_ParentId_SortOrder"); + + b.HasIndex("Type", "IsEnabled") + .HasDatabaseName("IX_NavigationMenus_Type_IsEnabled"); + + b.ToTable("tb_navigation_menus", null, t => + { + t.HasCheckConstraint("CK_NavigationMenus_Path_Valid", "Path LIKE '/dashboard/%' OR Path = '/dashboard'"); + + t.HasCheckConstraint("CK_NavigationMenus_SortOrder_Valid", "SortOrder >= 0"); + + t.HasCheckConstraint("CK_NavigationMenus_Type_Valid", "Type IN (1, 2, 3)"); + }); + }); + + modelBuilder.Entity("X1.Domain.Entities.NetworkProfile.CoreNetworkConfig", b => + { + b.Property("Id") + .HasColumnType("text") + .HasComment("配置ID"); + + b.Property("ConfigContent") + .IsRequired() + .HasColumnType("text") + .HasComment("配置内容(JSON格式)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasComment("配置描述"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsDisabled") + .HasColumnType("boolean") + .HasComment("是否禁用"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasComment("配置名称"); + + b.Property("UpdatedAt") + .IsRequired() + .HasColumnType("timestamp with time zone") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .HasDatabaseName("IX_CoreNetworkConfigs_Name"); + + b.ToTable("tb_core_network_configs", null, t => + { + t.HasComment("核心网配置表"); + }); + }); + + modelBuilder.Entity("X1.Domain.Entities.NetworkProfile.IMS_Configuration", b => + { + b.Property("Id") + .HasColumnType("text") + .HasComment("配置ID"); + + b.Property("ConfigContent") + .IsRequired() + .HasColumnType("text") + .HasComment("配置内容(JSON格式)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasComment("配置描述"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsDisabled") + .HasColumnType("boolean") + .HasComment("是否禁用"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasComment("配置名称"); + + b.Property("UpdatedAt") + .IsRequired() + .HasColumnType("timestamp with time zone") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .HasDatabaseName("IX_IMS_Configurations_Name"); + + b.ToTable("tb_ims_configurations", null, t => + { + t.HasComment("IMS配置表"); + }); + }); + + modelBuilder.Entity("X1.Domain.Entities.NetworkProfile.NetworkStackConfig", b => + { + b.Property("Id") + .HasColumnType("text") + .HasComment("配置ID"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasComment("描述"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasComment("是否激活"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("NetworkStackCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasComment("网络栈编码"); + + b.Property("NetworkStackName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasComment("网络栈名称"); + + b.Property("RanId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasComment("RAN配置ID"); + + b.Property("UpdatedAt") + .IsRequired() + .HasColumnType("timestamp with time zone") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("IsActive") + .HasDatabaseName("IX_NetworkStackConfigs_IsActive"); + + b.HasIndex("NetworkStackCode") + .IsUnique() + .HasDatabaseName("IX_NetworkStackConfigs_NetworkStackCode"); + + b.HasIndex("NetworkStackName") + .IsUnique() + .HasDatabaseName("IX_NetworkStackConfigs_NetworkStackName"); + + b.HasIndex("RanId") + .HasDatabaseName("IX_NetworkStackConfigs_RanId"); + + b.ToTable("tb_network_stack_configs", null, t => + { + t.HasComment("网络栈配置表"); + }); + }); + + modelBuilder.Entity("X1.Domain.Entities.NetworkProfile.RAN_Configuration", b => + { + b.Property("Id") + .HasColumnType("text") + .HasComment("配置ID"); + + b.Property("ConfigContent") + .IsRequired() + .HasColumnType("text") + .HasComment("配置内容(JSON格式)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasComment("配置描述"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsDisabled") + .HasColumnType("boolean") + .HasComment("是否禁用"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasComment("配置名称"); + + b.Property("UpdatedAt") + .IsRequired() + .HasColumnType("timestamp with time zone") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .HasDatabaseName("IX_RAN_Configurations_Name"); + + b.ToTable("tb_ran_configurations", null, t => + { + t.HasComment("RAN配置表"); + }); + }); + + modelBuilder.Entity("X1.Domain.Entities.NetworkProfile.Stack_CoreIMS_Binding", b => + { + b.Property("Id") + .HasColumnType("text") + .HasComment("绑定关系ID"); + + b.Property("CnId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasComment("核心网配置ID"); + + b.Property("ImsId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasComment("IMS配置ID"); + + b.Property("Index") + .HasColumnType("integer") + .HasComment("索引"); + + b.Property("NetworkStackConfigId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasComment("网络栈配置ID"); + + b.HasKey("Id"); + + b.HasIndex("CnId") + .HasDatabaseName("IX_Stack_CoreIMS_Bindings_CnId"); + + b.HasIndex("ImsId") + .HasDatabaseName("IX_Stack_CoreIMS_Bindings_ImsId"); + + b.HasIndex("NetworkStackConfigId") + .HasDatabaseName("IX_Stack_CoreIMS_Bindings_NetworkStackConfigId"); + + b.HasIndex("NetworkStackConfigId", "Index") + .IsUnique() + .HasDatabaseName("IX_Stack_CoreIMS_Bindings_NetworkStackConfigId_Index"); + + b.ToTable("tb_stack_core_ims_bindings", null, t => + { + t.HasComment("栈与核心网/IMS绑定关系表"); + }); + }); + + modelBuilder.Entity("X1.Domain.Entities.Permission", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsSystem") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("IsEnabled"); + + b.ToTable("tb_permissions", (string)null); + }); + + modelBuilder.Entity("X1.Domain.Entities.RolePermission", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("GrantedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("GrantedBy") + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("IsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("PermissionId") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("RoleId") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.HasKey("Id"); + + b.HasIndex("IsEnabled"); + + b.HasIndex("PermissionId"); + + b.HasIndex("RoleId"); + + b.ToTable("tb_role_permissions", (string)null); + }); + + modelBuilder.Entity("X1.Domain.Entities.Terminal.AdbOperation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Command") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasComment("执行的ADB命令"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasComment("创建人"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasComment("ADB操作的描述"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasComment("设备ID"); + + b.Property("IsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasComment("是否启用"); + + b.Property("Path") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasComment("命令执行时所依赖的路径(当启用绝对路径时必填)"); + + b.Property("ScreenshotData") + .HasColumnType("BYTEA") + .HasComment("操作截图数据(字节数组)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasComment("更新人"); + + b.Property("UseAbsolutePath") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasComment("是否启用绝对路径"); + + b.Property("WaitTimeMs") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasComment("执行命令完需要等待的时间(毫秒)"); + + b.HasKey("Id"); + + b.HasIndex("Command") + .HasDatabaseName("IX_AdbOperations_Command"); + + b.HasIndex("CreatedAt") + .HasDatabaseName("IX_AdbOperations_CreatedAt"); + + b.HasIndex("DeviceId") + .HasDatabaseName("IX_AdbOperations_DeviceId"); + + b.HasIndex("IsEnabled") + .HasDatabaseName("IX_AdbOperations_IsEnabled"); + + b.ToTable("tb_adboperations", (string)null); + }); + + modelBuilder.Entity("X1.Domain.Entities.Terminal.AtOperation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BaudRate") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(115200) + .HasComment("波特率"); + + b.Property("Command") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasComment("AT命令内容"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间"); + + b.Property("CreatedBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasComment("创建人"); + + b.Property("DataBits") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(8) + .HasComment("数据位"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasComment("操作描述"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasComment("设备ID"); + + b.Property("IsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasComment("是否启用"); + + b.Property("Parameters") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasDefaultValue("") + .HasComment("命令参数(JSON格式)"); + + b.Property("Parity") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasDefaultValue("NONE") + .HasComment("校验位"); + + b.Property("Port") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasComment("串口端口"); + + b.Property("ScreenshotData") + .HasColumnType("BYTEA") + .HasComment("操作截图数据(字节数组)"); + + b.Property("StopBits") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(10) + .HasColumnType("character varying(10)") + .HasDefaultValue("1") + .HasComment("停止位"); + + b.Property("Timeout") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(30) + .HasComment("超时时间(秒)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("更新时间"); + + b.Property("UpdatedBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasComment("更新人"); + + b.HasKey("Id"); + + b.HasIndex("Command") + .HasDatabaseName("IX_AtOperations_Command"); + + b.HasIndex("CreatedAt") + .HasDatabaseName("IX_AtOperations_CreatedAt"); + + b.HasIndex("DeviceId") + .HasDatabaseName("IX_AtOperations_DeviceId"); + + b.HasIndex("IsEnabled") + .HasDatabaseName("IX_AtOperations_IsEnabled"); + + b.ToTable("tb_atoperations", (string)null); + }); + + modelBuilder.Entity("X1.Domain.Entities.Terminal.TerminalDevice", b => + { + b.Property("Id") + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("Alias") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("AndroidVersion") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("BootSerial") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Brand") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BuildId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("BuildType") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Device") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Hardware") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("HardwarePlatform") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("IsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("LastConnectedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastDisconnectedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Locale") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Model") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SdkVersion") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Serial") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ServiceSerial") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("AndroidVersion"); + + b.HasIndex("Brand"); + + b.HasIndex("Hardware"); + + b.HasIndex("IsEnabled"); + + b.HasIndex("LastConnectedAt"); + + b.HasIndex("Model"); + + b.HasIndex("Serial") + .IsUnique(); + + b.HasIndex("ServiceSerial"); + + b.HasIndex("Status"); + + b.ToTable("tb_terminal_devices", (string)null); + }); + + modelBuilder.Entity("X1.Domain.Entities.Terminal.TerminalService", b => + { + b.Property("Id") + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("AgentPort") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IpAddress") + .IsRequired() + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("IsServiceStarted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SerialNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ServiceCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ServiceType") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("IpAddress"); + + b.HasIndex("IsEnabled"); + + b.HasIndex("IsServiceStarted"); + + b.HasIndex("SerialNumber") + .IsUnique(); + + b.HasIndex("ServiceCode") + .IsUnique(); + + b.HasIndex("ServiceType"); + + b.ToTable("tb_terminal_services", (string)null); + }); + + modelBuilder.Entity("X1.Domain.Entities.TestCase.CaseStepConfig", b => + { + b.Property("Id") + .HasMaxLength(450) + .HasColumnType("character varying(450)") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdat"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)") + .HasColumnName("createdby"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("description"); + + b.Property("FormType") + .HasColumnType("integer") + .HasColumnName("formtype"); + + b.Property("Icon") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("icon"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasColumnName("isenabled"); + + b.Property("Mapping") + .HasColumnType("integer") + .HasColumnName("mapping"); + + b.Property("StepName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("stepname"); + + b.Property("StepType") + .HasColumnType("integer") + .HasColumnName("steptype"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updatedat"); + + b.Property("UpdatedBy") + .HasMaxLength(450) + .HasColumnType("character varying(450)") + .HasColumnName("updatedby"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("FormType"); + + b.HasIndex("IsEnabled"); + + b.HasIndex("Mapping"); + + b.HasIndex("StepType"); + + b.ToTable("tb_casestepconfig", (string)null); + }); + + modelBuilder.Entity("X1.Domain.Entities.TestCase.ImsiRegistrationRecord", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("IsDualSim") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("NodeId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Sim1CellId") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Sim1Plmn") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Sim1RegistrationWaitTime") + .HasColumnType("integer"); + + b.Property("Sim2CellId") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Sim2Plmn") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Sim2RegistrationWaitTime") + .HasColumnType("integer"); + + b.Property("TestCaseId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt") + .HasDatabaseName("IX_tb_imsi_registration_records_createdat"); + + b.HasIndex("NodeId") + .HasDatabaseName("IX_tb_imsi_registration_records_nodeid"); + + b.HasIndex("TestCaseId") + .HasDatabaseName("IX_tb_imsi_registration_records_testcaseid"); + + b.HasIndex("TestCaseId", "NodeId") + .IsUnique() + .HasDatabaseName("IX_tb_imsi_registration_records_testcaseid_nodeid"); + + b.ToTable("tb_imsi_registration_records", null, t => + { + t.HasComment("IMSI注册记录表"); + }); + }); + + modelBuilder.Entity("X1.Domain.Entities.TestCase.ScenarioTestCase", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ExecutionOrder") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("IsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("LoopCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1); + + b.Property("ScenarioId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("TestCaseFlowId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("ScenarioId"); + + b.HasIndex("TestCaseFlowId"); + + b.HasIndex("ScenarioId", "ExecutionOrder"); + + b.HasIndex("ScenarioId", "TestCaseFlowId") + .IsUnique(); + + b.ToTable("tb_scenariotestcases", (string)null); + }); + + modelBuilder.Entity("X1.Domain.Entities.TestCase.TestCaseEdge", b => + { + b.Property("Id") + .HasMaxLength(450) + .HasColumnType("character varying(450)") + .HasColumnName("id"); + + b.Property("Condition") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("condition"); + + b.Property("EdgeId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("edgeid"); + + b.Property("EdgeType") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("edgetype"); + + b.Property("IsAnimated") + .HasColumnType("boolean") + .HasColumnName("isanimated"); + + b.Property("SourceHandle") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("SourceNodeId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("sourcenodeid"); + + b.Property("Style") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("style"); + + b.Property("TargetHandle") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("TargetNodeId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("targetnodeid"); + + b.Property("TestCaseId") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)") + .HasColumnName("testcaseid"); + + b.HasKey("Id"); + + b.HasIndex("EdgeId") + .HasDatabaseName("IX_testcaseedge_edgeid"); + + b.HasIndex("SourceNodeId") + .HasDatabaseName("IX_testcaseedge_sourcenodeid"); + + b.HasIndex("TargetNodeId") + .HasDatabaseName("IX_testcaseedge_targetnodeid"); + + b.HasIndex("TestCaseId") + .HasDatabaseName("IX_testcaseedge_testcaseid"); + + b.ToTable("tb_testcaseedge", (string)null); + }); + + modelBuilder.Entity("X1.Domain.Entities.TestCase.TestCaseFlow", b => + { + b.Property("Id") + .HasMaxLength(450) + .HasColumnType("character varying(450)") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("createdat"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)") + .HasColumnName("createdby"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("description"); + + b.Property("IsEnabled") + .HasColumnType("boolean") + .HasColumnName("isenabled"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updatedat"); + + b.Property("UpdatedBy") + .HasMaxLength(450) + .HasColumnType("character varying(450)") + .HasColumnName("updatedby"); + + b.Property("ViewportX") + .HasColumnType("double precision") + .HasColumnName("viewport_x"); + + b.Property("ViewportY") + .HasColumnType("double precision") + .HasColumnName("viewport_y"); + + b.Property("ViewportZoom") + .HasColumnType("double precision") + .HasColumnName("viewport_zoom"); + + b.HasKey("Id"); + + b.HasIndex("IsEnabled") + .HasDatabaseName("IX_testcaseflow_isenabled"); + + b.HasIndex("Name") + .HasDatabaseName("IX_testcaseflow_name"); + + b.HasIndex("Type") + .HasDatabaseName("IX_testcaseflow_type"); + + b.ToTable("tb_testcaseflow", (string)null); + }); + + modelBuilder.Entity("X1.Domain.Entities.TestCase.TestCaseNode", b => + { + b.Property("Id") + .HasMaxLength(450) + .HasColumnType("character varying(450)") + .HasColumnName("id"); + + b.Property("Height") + .HasColumnType("double precision") + .HasColumnName("height"); + + b.Property("IsDragging") + .HasColumnType("boolean") + .HasColumnName("isdragging"); + + b.Property("IsSelected") + .HasColumnType("boolean") + .HasColumnName("isselected"); + + b.Property("NodeId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("nodeid"); + + b.Property("PositionAbsoluteX") + .HasColumnType("double precision") + .HasColumnName("positionabsolutex"); + + b.Property("PositionAbsoluteY") + .HasColumnType("double precision") + .HasColumnName("positionabsolutey"); + + b.Property("PositionX") + .HasColumnType("double precision") + .HasColumnName("positionx"); + + b.Property("PositionY") + .HasColumnType("double precision") + .HasColumnName("positiony"); + + b.Property("SequenceNumber") + .HasColumnType("integer") + .HasColumnName("sequencenumber"); + + b.Property("StepId") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)") + .HasColumnName("stepid"); + + b.Property("TestCaseId") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)") + .HasColumnName("testcaseid"); + + b.Property("Width") + .HasColumnType("double precision") + .HasColumnName("width"); + + b.HasKey("Id"); + + b.HasIndex("NodeId") + .HasDatabaseName("IX_testcasenode_nodeid"); + + b.HasIndex("SequenceNumber") + .HasDatabaseName("IX_testcasenode_sequencenumber"); + + b.HasIndex("StepId"); + + b.HasIndex("TestCaseId") + .HasDatabaseName("IX_testcasenode_testcaseid"); + + b.ToTable("tb_testcasenode", (string)null); + }); + + modelBuilder.Entity("X1.Domain.Entities.TestCase.TestScenario", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("IsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("NetworkStackCode") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ScenarioCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ScenarioName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("IsEnabled"); + + b.HasIndex("NetworkStackCode"); + + b.HasIndex("ScenarioCode") + .IsUnique(); + + b.HasIndex("Type"); + + b.ToTable("tb_testscenarios", (string)null); + }); + + modelBuilder.Entity("X1.Domain.Entities.TestTask.TestScenarioTask", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DeviceCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsDisabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Lifecycle") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("Created"); + + b.Property("Priority") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("ScenarioCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Tags") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("TaskCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("TaskName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("TemplateId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt") + .HasDatabaseName("IX_TestScenarioTask_CreatedAt"); + + b.HasIndex("DeviceCode") + .HasDatabaseName("IX_TestScenarioTask_DeviceCode"); + + b.HasIndex("IsDisabled") + .HasDatabaseName("IX_TestScenarioTask_IsDisabled"); + + b.HasIndex("Lifecycle") + .HasDatabaseName("IX_TestScenarioTask_Lifecycle"); + + b.HasIndex("Priority") + .HasDatabaseName("IX_TestScenarioTask_Priority"); + + b.HasIndex("ScenarioCode") + .HasDatabaseName("IX_TestScenarioTask_ScenarioCode"); + + b.HasIndex("TaskCode") + .IsUnique() + .HasDatabaseName("IX_TestScenarioTask_TaskCode"); + + b.HasIndex("DeviceCode", "Lifecycle") + .HasDatabaseName("IX_TestScenarioTask_DeviceCode_Lifecycle"); + + b.HasIndex("Priority", "CreatedAt") + .HasDatabaseName("IX_TestScenarioTask_Priority_CreatedAt"); + + b.HasIndex("ScenarioCode", "Lifecycle") + .HasDatabaseName("IX_TestScenarioTask_ScenarioCode_Lifecycle"); + + b.ToTable("tb_testscenario_task", (string)null); + }); + + modelBuilder.Entity("X1.Domain.Entities.TestTask.TestScenarioTaskAssignment", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsDualSim") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("ServiceSerial") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("SimNumber1") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("SimNumber2") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("TaskId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("TerminalId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("IsDualSim") + .HasDatabaseName("IX_TestScenarioTaskAssignment_IsDualSim"); + + b.HasIndex("ServiceSerial") + .HasDatabaseName("IX_TestScenarioTaskAssignment_ServiceSerial"); + + b.HasIndex("TaskId") + .HasDatabaseName("IX_TestScenarioTaskAssignment_TaskId"); + + b.HasIndex("TerminalId") + .HasDatabaseName("IX_TestScenarioTaskAssignment_TerminalId"); + + b.HasIndex("ServiceSerial", "IsDualSim") + .HasDatabaseName("IX_TestScenarioTaskAssignment_ServiceSerial_IsDualSim"); + + b.HasIndex("TaskId", "TerminalId") + .IsUnique() + .HasDatabaseName("IX_TestScenarioTaskAssignment_TaskId_TerminalId"); + + b.HasIndex("TerminalId", "IsDualSim") + .HasDatabaseName("IX_TestScenarioTaskAssignment_TerminalId_IsDualSim"); + + b.ToTable("tb_testscenario_task_assignment", (string)null); + }); + + modelBuilder.Entity("X1.Domain.Entities.TestTask.TestScenarioTaskExecutionCaseDetail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Duration") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("ExecutionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ExecutorId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Loop") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1); + + b.Property("ScenarioCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("TestCaseFlowId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("ExecutionId") + .HasDatabaseName("IX_TestScenarioTaskExecutionCaseDetail_ExecutionId"); + + b.HasIndex("ExecutorId") + .HasDatabaseName("IX_TestScenarioTaskExecutionCaseDetail_ExecutorId"); + + b.HasIndex("ScenarioCode") + .HasDatabaseName("IX_TestScenarioTaskExecutionCaseDetail_ScenarioCode"); + + b.HasIndex("StartTime") + .HasDatabaseName("IX_TestScenarioTaskExecutionCaseDetail_StartTime"); + + b.HasIndex("Status") + .HasDatabaseName("IX_TestScenarioTaskExecutionCaseDetail_Status"); + + b.HasIndex("TestCaseFlowId") + .HasDatabaseName("IX_TestScenarioTaskExecutionCaseDetail_TestCaseFlowId"); + + b.HasIndex("ExecutionId", "Status") + .HasDatabaseName("IX_TestScenarioTaskExecutionCaseDetail_ExecutionId_Status"); + + b.HasIndex("ScenarioCode", "TestCaseFlowId") + .HasDatabaseName("IX_TestScenarioTaskExecutionCaseDetail_ScenarioCode_TestCaseFlowId"); + + b.ToTable("tb_testscenario_task_execution_case_detail", (string)null); + }); + + modelBuilder.Entity("X1.Domain.Entities.TestTask.TestScenarioTaskExecutionCaseStepDetail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CaseDetailId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Duration") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("ExecutorId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Loop") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("StepId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("StepName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex("CaseDetailId") + .HasDatabaseName("IX_TestScenarioTaskExecutionCaseStepDetail_CaseDetailId"); + + b.HasIndex("ExecutorId") + .HasDatabaseName("IX_TestScenarioTaskExecutionCaseStepDetail_ExecutorId"); + + b.HasIndex("StartTime") + .HasDatabaseName("IX_TestScenarioTaskExecutionCaseStepDetail_StartTime"); + + b.HasIndex("Status") + .HasDatabaseName("IX_TestScenarioTaskExecutionCaseStepDetail_Status"); + + b.HasIndex("StepId") + .HasDatabaseName("IX_TestScenarioTaskExecutionCaseStepDetail_StepId"); + + b.HasIndex("CaseDetailId", "Status") + .HasDatabaseName("IX_TestScenarioTaskExecutionCaseStepDetail_CaseDetailId_Status"); + + b.HasIndex("StepId", "StartTime") + .HasDatabaseName("IX_TestScenarioTaskExecutionCaseStepDetail_StepId_StartTime"); + + b.ToTable("tb_testscenario_task_execution_case_step_detail", (string)null); + }); + + modelBuilder.Entity("X1.Domain.Entities.TestTask.TestScenarioTaskExecutionDetail", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Duration") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("ExecutorId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Loop") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1); + + b.Property("Progress") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("RuntimeCode") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ScenarioCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("TaskId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("ExecutorId") + .HasDatabaseName("IX_TestScenarioTaskExecutionDetail_ExecutorId"); + + b.HasIndex("RuntimeCode") + .HasDatabaseName("IX_TestScenarioTaskExecutionDetail_RuntimeCode"); + + b.HasIndex("ScenarioCode") + .HasDatabaseName("IX_TestScenarioTaskExecutionDetail_ScenarioCode"); + + b.HasIndex("StartTime") + .HasDatabaseName("IX_TestScenarioTaskExecutionDetail_StartTime"); + + b.HasIndex("Status") + .HasDatabaseName("IX_TestScenarioTaskExecutionDetail_Status"); + + b.HasIndex("TaskId") + .HasDatabaseName("IX_TestScenarioTaskExecutionDetail_TaskId"); + + b.HasIndex("ScenarioCode", "StartTime") + .HasDatabaseName("IX_TestScenarioTaskExecutionDetail_ScenarioCode_StartTime"); + + b.HasIndex("TaskId", "Status") + .HasDatabaseName("IX_TestScenarioTaskExecutionDetail_TaskId_Status"); + + b.ToTable("tb_testscenario_task_execution_detail", (string)null); + }); + + modelBuilder.Entity("X1.Domain.Entities.TestTask.TestScenarioTaskExecutionStepLog", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Content") + .HasColumnType("text"); + + b.Property("CreatedTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("LogType") + .HasColumnType("integer"); + + b.Property("StepDetailId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedTime") + .HasDatabaseName("IX_TestScenarioTaskExecutionStepLog_CreatedTime"); + + b.HasIndex("LogType") + .HasDatabaseName("IX_TestScenarioTaskExecutionStepLog_LogType"); + + b.HasIndex("StepDetailId") + .HasDatabaseName("IX_TestScenarioTaskExecutionStepLog_StepDetailId"); + + b.HasIndex("LogType", "CreatedTime") + .HasDatabaseName("IX_TestScenarioTaskExecutionStepLog_LogType_CreatedTime"); + + b.HasIndex("StepDetailId", "CreatedTime") + .HasDatabaseName("IX_TestScenarioTaskExecutionStepLog_StepDetailId_CreatedTime"); + + b.HasIndex("StepDetailId", "LogType") + .HasDatabaseName("IX_TestScenarioTaskExecutionStepLog_StepDetailId_LogType"); + + b.ToTable("tb_testscenario_task_execution_step_log", (string)null); + }); + + modelBuilder.Entity("X1.Domain.Entities.UserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("tb_user_roles", null, t => + { + t.HasComment("用户角色关系表"); + }); + }); + + modelBuilder.Entity("X1.Domain.Entities.ButtonPermission", b => + { + b.HasOne("X1.Domain.Entities.NavigationMenu", "NavigationMenu") + .WithMany() + .HasForeignKey("NavigationMenuId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_ButtonPermission_NavigationMenu_SubMenuItem"); + + b.Navigation("NavigationMenu"); + }); + + modelBuilder.Entity("X1.Domain.Entities.Device.CellularDeviceRuntime", b => + { + b.HasOne("X1.Domain.Entities.Device.CellularDevice", "Device") + .WithOne("Runtime") + .HasForeignKey("X1.Domain.Entities.Device.CellularDeviceRuntime", "DeviceCode") + .HasPrincipalKey("X1.Domain.Entities.Device.CellularDevice", "DeviceCode") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Device"); + }); + + modelBuilder.Entity("X1.Domain.Entities.Device.ProtocolVersion", b => + { + b.HasOne("X1.Domain.Entities.Device.CellularDevice", null) + .WithMany("ProtocolVersions") + .HasForeignKey("SerialNumber") + .HasPrincipalKey("SerialNumber") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("X1.Domain.Entities.Logging.LoginLog", b => + { + b.HasOne("X1.Domain.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("X1.Domain.Entities.NavigationMenu", b => + { + b.HasOne("X1.Domain.Entities.NavigationMenu", "Parent") + .WithMany("Children") + .HasForeignKey("ParentId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("FK_NavigationMenus_Children"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("X1.Domain.Entities.NetworkProfile.NetworkStackConfig", b => + { + b.HasOne("X1.Domain.Entities.NetworkProfile.RAN_Configuration", null) + .WithMany() + .HasForeignKey("RanId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("X1.Domain.Entities.NetworkProfile.Stack_CoreIMS_Binding", b => + { + b.HasOne("X1.Domain.Entities.NetworkProfile.CoreNetworkConfig", "CoreNetworkConfig") + .WithMany() + .HasForeignKey("CnId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("X1.Domain.Entities.NetworkProfile.IMS_Configuration", "IMSConfiguration") + .WithMany() + .HasForeignKey("ImsId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("X1.Domain.Entities.NetworkProfile.NetworkStackConfig", "NetworkStackConfig") + .WithMany("StackCoreIMSBindings") + .HasForeignKey("NetworkStackConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CoreNetworkConfig"); + + b.Navigation("IMSConfiguration"); + + b.Navigation("NetworkStackConfig"); + }); + + modelBuilder.Entity("X1.Domain.Entities.RolePermission", b => + { + b.HasOne("X1.Domain.Entities.Permission", "Permission") + .WithMany() + .HasForeignKey("PermissionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("X1.Domain.Entities.AppRole", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Permission"); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("X1.Domain.Entities.TestCase.ImsiRegistrationRecord", b => + { + b.HasOne("X1.Domain.Entities.TestCase.TestCaseNode", "TestCaseNode") + .WithMany() + .HasForeignKey("NodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("X1.Domain.Entities.TestCase.TestCaseFlow", "TestCase") + .WithMany() + .HasForeignKey("TestCaseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TestCase"); + + b.Navigation("TestCaseNode"); + }); + + modelBuilder.Entity("X1.Domain.Entities.TestCase.ScenarioTestCase", b => + { + b.HasOne("X1.Domain.Entities.TestCase.TestScenario", "Scenario") + .WithMany("ScenarioTestCases") + .HasForeignKey("ScenarioId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("X1.Domain.Entities.TestCase.TestCaseFlow", "TestCaseFlow") + .WithMany("TestScenarioTestCases") + .HasForeignKey("TestCaseFlowId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Scenario"); + + b.Navigation("TestCaseFlow"); + }); + + modelBuilder.Entity("X1.Domain.Entities.TestCase.TestCaseEdge", b => + { + b.HasOne("X1.Domain.Entities.TestCase.TestCaseFlow", "TestCase") + .WithMany("Edges") + .HasForeignKey("TestCaseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TestCase"); + }); + + modelBuilder.Entity("X1.Domain.Entities.TestCase.TestCaseNode", b => + { + b.HasOne("X1.Domain.Entities.TestCase.CaseStepConfig", "StepConfig") + .WithMany() + .HasForeignKey("StepId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("X1.Domain.Entities.TestCase.TestCaseFlow", "TestCase") + .WithMany("Nodes") + .HasForeignKey("TestCaseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("StepConfig"); + + b.Navigation("TestCase"); + }); + + modelBuilder.Entity("X1.Domain.Entities.TestTask.TestScenarioTaskAssignment", b => + { + b.HasOne("X1.Domain.Entities.TestTask.TestScenarioTask", null) + .WithMany() + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("X1.Domain.Entities.TestTask.TestScenarioTaskExecutionCaseDetail", b => + { + b.HasOne("X1.Domain.Entities.TestTask.TestScenarioTaskExecutionDetail", null) + .WithMany() + .HasForeignKey("ExecutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("X1.Domain.Entities.TestTask.TestScenarioTaskExecutionCaseStepDetail", b => + { + b.HasOne("X1.Domain.Entities.TestTask.TestScenarioTaskExecutionCaseDetail", null) + .WithMany() + .HasForeignKey("CaseDetailId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("X1.Domain.Entities.TestTask.TestScenarioTaskExecutionStepLog", b => + { + b.HasOne("X1.Domain.Entities.TestTask.TestScenarioTaskExecutionCaseStepDetail", null) + .WithMany() + .HasForeignKey("StepDetailId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("X1.Domain.Entities.UserRole", b => + { + b.HasOne("X1.Domain.Entities.AppRole", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("X1.Domain.Entities.AppUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("X1.Domain.Entities.Device.CellularDevice", b => + { + b.Navigation("ProtocolVersions"); + + b.Navigation("Runtime"); + }); + + modelBuilder.Entity("X1.Domain.Entities.NavigationMenu", b => + { + b.Navigation("Children"); + }); + + modelBuilder.Entity("X1.Domain.Entities.NetworkProfile.NetworkStackConfig", b => + { + b.Navigation("StackCoreIMSBindings"); + }); + + modelBuilder.Entity("X1.Domain.Entities.TestCase.TestCaseFlow", b => + { + b.Navigation("Edges"); + + b.Navigation("Nodes"); + + b.Navigation("TestScenarioTestCases"); + }); + + modelBuilder.Entity("X1.Domain.Entities.TestCase.TestScenario", b => + { + b.Navigation("ScenarioTestCases"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/X1.Infrastructure/Migrations/20250905030203_UpdateStepLogTypeToEnum.cs b/src/X1.Infrastructure/Migrations/20250905030203_UpdateStepLogTypeToEnum.cs new file mode 100644 index 0000000..3f83e34 --- /dev/null +++ b/src/X1.Infrastructure/Migrations/20250905030203_UpdateStepLogTypeToEnum.cs @@ -0,0 +1,62 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace X1.Infrastructure.Migrations +{ + /// + public partial class UpdateStepLogTypeToEnum : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // 首先更新现有数据,将字符串转换为对应的枚举值 + migrationBuilder.Sql(@" + UPDATE tb_testscenario_task_execution_step_log + SET ""LogType"" = CASE + WHEN ""LogType"" = 'error' THEN '1' + WHEN ""LogType"" = 'success' THEN '2' + WHEN ""LogType"" = 'debug' THEN '3' + WHEN ""LogType"" = 'info' THEN '4' + WHEN ""LogType"" = 'warn' THEN '5' + WHEN ""LogType"" = 'output' THEN '6' + WHEN ""LogType"" = 'param' THEN '7' + WHEN ""LogType"" = 'etc' THEN '8' + ELSE '4' -- 默认为 Info + END + "); + + // 然后更改列类型,使用 USING 子句指定转换方式 + migrationBuilder.Sql(@" + ALTER TABLE tb_testscenario_task_execution_step_log + ALTER COLUMN ""LogType"" TYPE integer USING ""LogType""::integer + "); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + // 首先更改列类型,使用 USING 子句指定转换方式 + migrationBuilder.Sql(@" + ALTER TABLE tb_testscenario_task_execution_step_log + ALTER COLUMN ""LogType"" TYPE character varying(50) USING ""LogType""::text + "); + + // 然后更新数据,将枚举值转换回字符串 + migrationBuilder.Sql(@" + UPDATE tb_testscenario_task_execution_step_log + SET ""LogType"" = CASE + WHEN ""LogType"" = '1' THEN 'error' + WHEN ""LogType"" = '2' THEN 'success' + WHEN ""LogType"" = '3' THEN 'debug' + WHEN ""LogType"" = '4' THEN 'info' + WHEN ""LogType"" = '5' THEN 'warn' + WHEN ""LogType"" = '6' THEN 'output' + WHEN ""LogType"" = '7' THEN 'param' + WHEN ""LogType"" = '8' THEN 'etc' + ELSE 'info' -- 默认为 info + END + "); + } + } +} diff --git a/src/X1.Infrastructure/Migrations/AppDbContextModelSnapshot.cs b/src/X1.Infrastructure/Migrations/AppDbContextModelSnapshot.cs index 98859a4..e65e9ba 100644 --- a/src/X1.Infrastructure/Migrations/AppDbContextModelSnapshot.cs +++ b/src/X1.Infrastructure/Migrations/AppDbContextModelSnapshot.cs @@ -2513,10 +2513,8 @@ namespace X1.Infrastructure.Migrations .HasColumnType("timestamp with time zone") .HasDefaultValueSql("CURRENT_TIMESTAMP"); - b.Property("LogType") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); + b.Property("LogType") + .HasColumnType("integer"); b.Property("StepDetailId") .IsRequired() diff --git a/src/X1.Infrastructure/Repositories/TestTask/TestScenarioTaskExecutionStepLogRepository.cs b/src/X1.Infrastructure/Repositories/TestTask/TestScenarioTaskExecutionStepLogRepository.cs index f085e95..430d717 100644 --- a/src/X1.Infrastructure/Repositories/TestTask/TestScenarioTaskExecutionStepLogRepository.cs +++ b/src/X1.Infrastructure/Repositories/TestTask/TestScenarioTaskExecutionStepLogRepository.cs @@ -9,6 +9,7 @@ using X1.Domain.Repositories.Base; using X1.Domain.Repositories.TestTask; using X1.Domain.Models.TestTask; using X1.Infrastructure.Repositories.Base; +using X1.Domain.Common.Enums; namespace X1.Infrastructure.Repositories.TestTask { @@ -40,7 +41,7 @@ namespace X1.Infrastructure.Repositories.TestTask /// /// 根据日志类型获取步骤执行日志列表 /// - public async Task> GetByLogTypeAsync(string logType, CancellationToken cancellationToken = default) + public async Task> GetByLogTypeAsync(StepLogType logType, CancellationToken cancellationToken = default) { var logs = await QueryRepository.FindAsync(x => x.LogType == logType, cancellationToken: cancellationToken); return logs.OrderBy(x => x.CreatedTime); @@ -60,7 +61,7 @@ namespace X1.Infrastructure.Repositories.TestTask /// public async Task> GetErrorLogsAsync(string stepDetailId, CancellationToken cancellationToken = default) { - var logs = await QueryRepository.FindAsync(x => x.StepDetailId == stepDetailId && x.LogType == "error", cancellationToken: cancellationToken); + var logs = await QueryRepository.FindAsync(x => x.StepDetailId == stepDetailId && x.LogType == StepLogType.Error, cancellationToken: cancellationToken); return logs.OrderBy(x => x.CreatedTime); } @@ -69,7 +70,7 @@ namespace X1.Infrastructure.Repositories.TestTask /// public async Task> GetOutputLogsAsync(string stepDetailId, CancellationToken cancellationToken = default) { - var logs = await QueryRepository.FindAsync(x => x.StepDetailId == stepDetailId && x.LogType == "output", cancellationToken: cancellationToken); + var logs = await QueryRepository.FindAsync(x => x.StepDetailId == stepDetailId && x.LogType == StepLogType.Output, cancellationToken: cancellationToken); return logs.OrderBy(x => x.CreatedTime); } @@ -78,7 +79,7 @@ namespace X1.Infrastructure.Repositories.TestTask /// public async Task> GetParamLogsAsync(string stepDetailId, CancellationToken cancellationToken = default) { - var logs = await QueryRepository.FindAsync(x => x.StepDetailId == stepDetailId && x.LogType == "param", cancellationToken: cancellationToken); + var logs = await QueryRepository.FindAsync(x => x.StepDetailId == stepDetailId && x.LogType == StepLogType.Parameter, cancellationToken: cancellationToken); return logs.OrderBy(x => x.CreatedTime); } @@ -87,7 +88,7 @@ namespace X1.Infrastructure.Repositories.TestTask /// public async Task> GetByConditionsAsync( string? stepDetailId = null, - string? logType = null, + StepLogType? logType = null, DateTime? startTime = null, DateTime? endTime = null, CancellationToken cancellationToken = default) @@ -111,9 +112,9 @@ namespace X1.Infrastructure.Repositories.TestTask if (!logs.Any()) return null; var totalLogs = logs.Count(); - var errorLogs = logs.Count(x => x.LogType == "error"); - var outputLogs = logs.Count(x => x.LogType == "output"); - var paramLogs = logs.Count(x => x.LogType == "param"); + var errorLogs = logs.Count(x => x.LogType == StepLogType.Error); + var outputLogs = logs.Count(x => x.LogType == StepLogType.Output); + var paramLogs = logs.Count(x => x.LogType == StepLogType.Parameter); var otherLogs = totalLogs - errorLogs - outputLogs - paramLogs; var firstLogTime = logs.Min(x => x.CreatedTime); diff --git a/src/modify_20250121_batch_create_taskexecution.md b/src/modify_20250121_batch_create_taskexecution.md new file mode 100644 index 0000000..2e5a388 --- /dev/null +++ b/src/modify_20250121_batch_create_taskexecution.md @@ -0,0 +1,206 @@ +## 2025-01-21 为 ITaskExecutionService 添加批量创建方法 + +### 概述 +为 `ITaskExecutionService` 接口添加批量创建 `TestScenarioTaskExecutionCaseDetail` 初始化数据的方法,支持一次性创建多个任务执行用例明细记录。 + +### 主要变更 + +#### 1. 新增批量创建方法 +- **接口**: `ITaskExecutionService.BatchCreateTaskExecutionCaseDetailsAsync` +- **功能**: 批量创建任务执行用例明细初始化数据 +- **参数**: + - `executionId`: 任务执行记录ID + - `scenarioCode`: 场景编码 + - `testCaseFlowIds`: 测试用例流程ID列表 + - `executorId`: 执行人/终端ID + - `loop`: 执行轮次(默认为1) + - `cancellationToken`: 取消令牌 +- **返回值**: 创建的任务执行用例明细列表 + +#### 2. 方法特点 +- **批量处理**: 支持一次性创建多个用例明细记录 +- **参数验证**: 完整的参数验证和类型检查 +- **异步操作**: 支持异步操作和取消令牌 +- **返回结果**: 返回只读列表,确保数据安全性 + +#### 3. 使用场景 +- 任务启动时批量初始化所有相关的用例明细 +- 支持多轮次执行时的批量创建 +- 提高任务执行初始化的效率 + +### 技术实现 + +#### 方法签名 +```csharp +Task> BatchCreateTaskExecutionCaseDetailsAsync( + string executionId, + string scenarioCode, + IReadOnlyList testCaseFlowIds, + string executorId, + int loop = 1, + CancellationToken cancellationToken = default); +``` + +#### 设计考虑 +- **性能优化**: 批量操作减少数据库交互次数 +- **数据一致性**: 确保所有用例明细使用相同的执行参数 +- **扩展性**: 支持未来添加更多初始化参数 +- **错误处理**: 支持事务性操作,确保数据完整性 + +### 修改文件 +- `X1.Domain/Services/ITaskExecutionService.cs`: 添加批量创建方法定义 +- `X1.Application/ApplicationServices/TaskExecutionService.cs`: 实现批量创建方法的具体逻辑 +- `X1.Application/Features/TaskExecution/Commands/StartTaskExecution/StartTaskExecutionCommandHandler.cs`: 在发布事件前调用批量创建方法 + +### 实现详情 + +#### 1. 依赖注入更新 +- 添加了 `ITestScenarioTaskExecutionCaseDetailRepository` 依赖 +- 更新了构造函数参数和字段初始化 + +#### 2. 方法实现特点 +- **完整参数验证**: 验证所有输入参数的有效性 +- **业务逻辑验证**: 验证执行记录和测试用例流程的存在性 +- **批量操作**: 使用 `AddRangeAsync` 进行高效的批量插入 +- **事务支持**: 通过 `UnitOfWork` 确保数据一致性 +- **详细日志**: 记录操作开始、成功和失败的详细信息 +- **异常处理**: 完整的异常捕获和重新抛出 + +#### 3. 验证逻辑 +- 执行ID、场景编码、执行人ID不能为空 +- 测试用例流程ID列表不能为空 +- 执行轮次必须大于0 +- 验证任务执行记录是否存在 +- 验证所有测试用例流程是否存在 + +#### 4. 性能优化 +- 使用批量查询验证测试用例流程存在性 +- 使用批量插入减少数据库交互次数 +- 返回只读列表确保数据安全性 + +### 技术实现 + +#### 方法签名 +```csharp +public async Task> BatchCreateTaskExecutionCaseDetailsAsync( + string executionId, + string scenarioCode, + IReadOnlyList testCaseFlowIds, + string executorId, + int loop = 1, + CancellationToken cancellationToken = default) +``` + +#### 核心逻辑 +1. 参数验证和业务规则检查 +2. 验证执行记录和测试用例流程存在性 +3. 批量创建用例明细实体 +4. 批量保存到数据库 +5. 返回创建结果 + +#### 5. 集成到任务执行流程 +在 `StartTaskExecutionCommandHandler` 中进行了方法重构: +- 将原来的 `PublishNodeExecutionStartedEventsAsync` 方法拆分为三个独立的方法 +- 提取了批量创建用例明细的逻辑到单独的方法 +- 提取了发布节点事件的逻辑到单独的方法 +- 使用 `ServiceScopeExecutor` 确保在正确的作用域中执行 +- 添加完整的错误处理和日志记录 +- 如果批量创建失败,则中断后续的事件发布流程 + +#### 6. 方法重构详情 + +##### 6.1 主方法:`InitializeAndStartTaskExecutionAsync` (重命名) +- **原名**: `PublishNodeExecutionStartedEventsAsync` +- **职责**: 负责整体流程控制和协调 +- **功能**: 按顺序执行:状态更新 → 批量创建 → 发布事件 +- **特点**: 处理异常和错误情况,名称更直观地表达了方法的主要职责 + +##### 6.2 批量创建方法:`CreateTaskExecutionCaseDetailsAsync` (重命名) +- **原名**: `BatchCreateTaskExecutionCaseDetailsAsync` +- **职责**: 专门负责批量创建任务执行用例明细 +- **返回值**: 布尔值表示创建是否成功 +- **特点**: 包含完整的异常处理,名称更简洁 + +##### 6.3 事件发布方法:`PublishNodeExecutionEventsAsync` (保持原名) +- **职责**: 专门负责为每个初始节点发布执行事件 +- **功能**: 循环处理所有初始节点 +- **特点**: 记录每个事件的发布结果 + +#### 7. 执行流程优化 +1. 更新任务执行状态为运行中 +2. **批量创建任务执行用例明细** (新增,独立方法) +3. 为每个初始节点发布执行事件 (独立方法) + +### 使用示例 +```csharp +var caseDetails = await taskExecutionService.BatchCreateTaskExecutionCaseDetailsAsync( + executionId: "exec-123", + scenarioCode: "SCENARIO_001", + testCaseFlowIds: new[] { "flow-1", "flow-2", "flow-3" }, + executorId: "executor-456", + loop: 1 +); +``` + +### 集成示例 + +#### 重构后的方法结构 +```csharp +// 主方法:协调整个流程 (重命名) +private async Task InitializeAndStartTaskExecutionAsync(...) +{ + // 1. 更新任务执行状态为运行中 + // 2. 批量创建任务执行用例明细初始化数据 + var batchCreateResult = await CreateTaskExecutionCaseDetailsAsync( + initialNodeInfo, executionDetail, executorId, cancellationToken); + + if (!batchCreateResult) return; + + // 3. 为每个初始节点发布执行事件 + await PublishNodeExecutionEventsAsync(...); +} + +// 批量创建方法:专门处理用例明细创建 (重命名) +private async Task CreateTaskExecutionCaseDetailsAsync(...) +{ + var testCaseFlowIds = initialNodeInfo.InitialNodes.Select(node => node.FlowId).ToList(); + + var resultBatchCreate = await _scopeExecutor.ExecuteAsync(async serviceProvider => + { + var scopedTaskExecutionService = serviceProvider.GetRequiredService(); + await scopedTaskExecutionService.BatchCreateTaskExecutionCaseDetailsAsync( + executionDetail.Id, + initialNodeInfo.ScenarioCode, + testCaseFlowIds, + executorId, + loop: 1, + cancellationToken); + }, cancellationToken); + + return resultBatchCreate.IsSuccess; +} + +// 事件发布方法:专门处理节点事件发布 +private async Task PublishNodeExecutionEventsAsync(...) +{ + foreach (var initialNode in initialNodeInfo.InitialNodes) + { + // 创建并发布 NodeExecutionStartedEvent + } +} +``` + +#### 重构优势 +- **单一职责原则**: 每个方法只负责一个特定的功能 +- **可维护性**: 代码结构更清晰,便于维护和测试 +- **可重用性**: 批量创建方法可以在其他地方重用 +- **错误处理**: 每个方法都有独立的错误处理逻辑 +- **可读性**: 主方法逻辑更清晰,易于理解 +- **命名优化**: 方法名称更直观地表达了各自的职责,提高了代码的可读性 + +#### 命名改进总结 +- **原方法名**: `PublishNodeExecutionStartedEventsAsync` - 名称过长,职责不明确 +- **新方法名**: `InitializeAndStartTaskExecutionAsync` - 简洁明了,职责清晰 +- **原方法名**: `BatchCreateTaskExecutionCaseDetailsAsync` - 过于冗长 +- **新方法名**: `CreateTaskExecutionCaseDetailsAsync` - 简洁直观 +- **保持原名**: `PublishNodeExecutionEventsAsync` - 名称已经合理 diff --git a/src/modify_20250121_nextnodeinfo_optimization.md b/src/modify_20250121_nextnodeinfo_optimization.md new file mode 100644 index 0000000..a0bf329 --- /dev/null +++ b/src/modify_20250121_nextnodeinfo_optimization.md @@ -0,0 +1,96 @@ +# 2025-01-21 优化 NodeExecutionStartedEvent 事件结构 + +## 概述 + +在 `NodeExecutionStartedEvent` 中添加 `NextNodeInfo` 属性字段,将下一个节点信息作为独立属性,而不是与 `FlowId` 等属性放在同一层级。 + +## 主要变更 + +### 1. 在 BaseNodeExecutionEvent 中添加 NextNodeInfo 属性 +- **文件**: `X1.Application/Features/TaskExecution/Events/NodeExecutionEvents/BaseNodeExecutionEvent.cs` +- **变更**: 添加 `NextNodeInfo? NextNodeInfo { get; set; }` 属性 +- **目的**: 将下一个节点信息作为独立属性存储 + +### 2. 修改 CreateNodeExecutionStartedEvent 方法 +- **文件**: `X1.Application/Features/TaskExecution/Events/EventHandlers/NodeExecutionCompletedEventHandler.cs` +- **变更**: 在创建事件时设置 `NextNodeInfo = nextNode` +- **目的**: 将完整的 `NextNodeInfo` 对象传递给事件 + +### 3. 更新静态工厂方法 +- **文件**: `X1.Application/Features/TaskExecution/Events/NodeExecutionEvents/BaseNodeExecutionEvent.cs` +- **变更**: 在 `CreateFrom` 方法中添加 `NextNodeInfo = notification.NextNodeInfo` +- **目的**: 确保工厂方法正确复制 `NextNodeInfo` 属性 + +## 技术实现 + +### 1. 事件结构优化 +```csharp +public abstract class BaseNodeExecutionEvent : INodeExecutionEvent +{ + // ... 其他属性 ... + + /// + /// 下一个节点信息 + /// + public NextNodeInfo? NextNodeInfo { get; set; } +} +``` + +### 2. 事件创建方法 +```csharp +private static NodeExecutionStartedEvent CreateNodeExecutionStartedEvent(NodeExecutionCompletedEvent notification, NextNodeInfo nextNode) +{ + return new NodeExecutionStartedEvent + { + // ... 其他属性 ... + NextNodeInfo = nextNode, // 新增:设置完整的 NextNodeInfo 对象 + Timestamp = DateTime.UtcNow + }; +} +``` + +## 优势 + +1. **结构清晰**: `NextNodeInfo` 作为独立属性,结构更加清晰 +2. **信息完整**: 包含完整的下一个节点信息(NodeId、StepMapping、NodeName、StepId、SequenceNumber) +3. **扩展性好**: 未来可以轻松扩展 `NextNodeInfo` 而不影响事件的其他属性 +4. **类型安全**: 使用强类型的 `NextNodeInfo` 对象,避免属性分散 + +## 影响范围 + +- **事件处理器**: `NodeExecutionCompletedEventHandler` 中的事件创建逻辑 +- **事件基类**: `BaseNodeExecutionEvent` 的事件结构 +- **工厂方法**: 静态工厂方法的属性复制逻辑 + +## 修改文件列表 + +1. `X1.Application/Features/TaskExecution/Events/NodeExecutionEvents/BaseNodeExecutionEvent.cs` +2. `X1.Application/Features/TaskExecution/Events/EventHandlers/NodeExecutionCompletedEventHandler.cs` +3. `X1.Domain/Events/INodeExecutionEvent.cs` +4. `X1.Application/Features/TaskExecution/Events/ControllerHandlers/BaseControllerHandler.cs` + +## 重构详情 + +### 1. 移除冗余属性 +- 从 `BaseNodeExecutionEvent` 中移除了 `NodeId` 和 `StepMapping` 属性 +- 从 `INodeExecutionEvent` 接口中移除了 `NodeId` 和 `StepMapping` 属性 +- 这些信息现在通过 `NextNodeInfo` 属性访问 + +### 2. 更新接口定义 +- 在 `INodeExecutionEvent` 接口中添加了 `NextNodeInfo NextNodeInfo { get; }` 属性 +- 确保所有事件都通过 `NextNodeInfo` 来访问节点信息 + +### 3. 更新事件创建逻辑 +- 更新了 `CreateNodeExecutionStartedEvent` 方法,移除对冗余属性的设置 +- 更新了 `BaseControllerHandler` 中的事件创建方法 +- 更新了静态工厂方法 `CreateFrom` + +### 4. 更新事件处理器 +- 更新了 `NodeExecutionCompletedEventHandler` 中所有使用 `NodeId` 和 `StepMapping` 的地方 +- 现在通过 `notification.NextNodeInfo.NodeId` 和 `notification.NextNodeInfo.StepMapping` 访问 + +### 5. 优势 +- **消除冗余**: 移除了重复的属性定义 +- **结构清晰**: 节点信息统一通过 `NextNodeInfo` 访问 +- **类型安全**: 使用强类型的 `NextNodeInfo` 对象 +- **维护性**: 减少了代码重复,提高了维护性 diff --git a/src/modify_20250121_taskexecution_status_update.md b/src/modify_20250121_taskexecution_status_update.md new file mode 100644 index 0000000..a519c1d --- /dev/null +++ b/src/modify_20250121_taskexecution_status_update.md @@ -0,0 +1,100 @@ +## 2025-01-21 添加 TestScenarioTaskExecutionDetail 状态更新功能 + +### 概述 + +为 `TestScenarioTaskExecutionDetail` 实体添加灵活的状态更新功能,包括在服务层和实体层提供完整的状态管理方法。 + +### 主要变更 + +#### 1. 扩展 ITaskExecutionService 接口 +- **文件**: `X1.Domain/Services/ITaskExecutionService.cs` +- **新增方法**: `UpdateTaskExecutionStatusAsync` +- **功能**: 提供更新任务执行状态的服务接口 +- **参数**: 任务执行ID、新状态、可选进度、取消令牌 + +#### 2. 实现 TaskExecutionService 状态更新逻辑 +- **文件**: `X1.Application/ApplicationServices/TaskExecutionService.cs` +- **新增方法**: `UpdateTaskExecutionStatusAsync` 和 `IsValidStatusTransition` +- **功能**: 实现状态更新的完整业务逻辑 +- **特性**: + - 状态转换验证 + - 自动时间字段更新 + - 进度更新支持 + - 完整的日志记录 + +#### 3. 增强 TestScenarioTaskExecutionDetail 实体 +- **文件**: `X1.Domain/Entities/TestTask/TestScenarioTaskExecutionDetail.cs` +- **新增方法**: + - `UpdateStatus()` - 简单状态更新 + - `UpdateStatusWithTime()` - 带时间的状态更新 + - `ResetToPending()` - 重置为待执行状态 + - `CanUpdateToStatus()` - 状态转换验证 + - `GetStatusTransitionDescription()` - 状态转换描述 + - `GetStatusDescription()` - 状态描述(私有方法) + +#### 4. 状态转换规则 +- **Pending → Running**: 允许,设置开始时间 +- **Pending → Failed**: 允许,直接失败 +- **Running → Success**: 允许,设置结束时间和持续时间 +- **Running → Failed**: 允许,设置结束时间和持续时间 +- **Success/Failed → 其他状态**: 不允许 + +#### 5. 技术特点 +- **类型安全**: 完整的类型检查和验证 +- **业务规则**: 严格的状态转换规则 +- **时间管理**: 自动管理开始时间、结束时间和持续时间 +- **进度跟踪**: 支持执行进度更新 +- **错误处理**: 完善的错误处理和日志记录 +- **可扩展性**: 易于扩展新的状态和转换规则 + +### 使用示例 + +```csharp +// 更新任务执行状态 +var success = await taskExecutionService.UpdateTaskExecutionStatusAsync( + taskExecutionId: "execution-123", + status: TaskExecutionStatus.Running, + progress: 25, + cancellationToken: cancellationToken +); + +// 检查状态转换是否合法 +if (taskExecution.CanUpdateToStatus(TaskExecutionStatus.Success)) +{ + taskExecution.UpdateStatusWithTime(TaskExecutionStatus.Success); +} +``` + +### 实现细节 + +#### 1. 状态转换验证逻辑 +```csharp +private static bool IsValidStatusTransition(TaskExecutionStatus currentStatus, TaskExecutionStatus targetStatus) +{ + return currentStatus switch + { + TaskExecutionStatus.Pending => targetStatus == TaskExecutionStatus.Running || + targetStatus == TaskExecutionStatus.Failed, + TaskExecutionStatus.Running => targetStatus == TaskExecutionStatus.Success || + targetStatus == TaskExecutionStatus.Failed, + TaskExecutionStatus.Success => false, // 成功状态不能转换到其他状态 + TaskExecutionStatus.Failed => false, // 失败状态不能转换到其他状态 + _ => false + }; +} +``` + +#### 2. 实体层状态管理 +- 提供多种状态更新方法满足不同场景需求 +- 自动处理时间字段的更新逻辑 +- 内置状态转换验证和描述功能 + +#### 3. 服务层业务逻辑 +- 完整的业务规则验证 +- 统一的错误处理和日志记录 +- 支持进度更新和状态更新组合操作 + +### 修复的问题 +- 修复了 `StepMapping` 类型转换问题 +- 修复了空引用警告 +- 确保所有编译错误已解决 diff --git a/tatus --porcelain b/tatus --porcelain new file mode 100644 index 0000000..1343196 --- /dev/null +++ b/tatus --porcelain @@ -0,0 +1,28 @@ +warning: in the working copy of 'src/X1.Application/ApplicationServices/TaskExecutionService.cs', LF will be replaced by CRLF the next time Git touches it +warning: in the working copy of 'src/X1.Application/Features/TaskExecution/Commands/StartTaskExecution/StartTaskExecutionCommand.cs', LF will be replaced by CRLF the next time Git touches it +warning: in the working copy of 'src/X1.Domain/Entities/TestTask/TestScenarioTaskExecutionCaseDetail.cs', LF will be replaced by CRLF the next time Git touches it +warning: in the working copy of 'src/X1.Domain/Services/ITaskExecutionService.cs', LF will be replaced by CRLF the next time Git touches it +src/X1.Application/ApplicationServices/TaskExecutionService.cs +src/X1.Application/Features/TaskExecution/Commands/StartTaskExecution/StartTaskExecutionCommand.cs +src/X1.Application/Features/TaskExecution/Commands/StartTaskExecution/StartTaskExecutionCommandHandler.cs +src/X1.Application/Features/TaskExecution/Commands/StartTaskExecution/StartTaskExecutionCommandValidator.cs +src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/BaseControllerHandler.cs +src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/DisableFlightModeControllerHandler.cs +src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/EnableFlightModeControllerHandler.cs +src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/EndFlowControllerHandler.cs +src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/ImsiRegistrationControllerHandler.cs +src/X1.Application/Features/TaskExecution/Events/ControllerHandlers/StartFlowControllerHandler.cs +src/X1.Application/Features/TaskExecution/Events/EventHandlers/BaseEventHandler.cs +src/X1.Application/Features/TaskExecution/Events/EventHandlers/NodeExecutionCompletedEventHandler.cs +src/X1.Application/Features/TaskExecution/Events/EventHandlers/NodeExecutionEventRouter.cs +src/X1.Application/Features/TaskExecution/Events/EventHandlers/NodeExecutionFailedEventHandler.cs +src/X1.Application/Features/TaskExecution/Events/NodeExecutionEvents/BaseNodeExecutionEvent.cs +src/X1.Domain/Entities/TestTask/TestScenarioTaskExecutionDetail.cs +src/X1.Domain/Entities/TestTask/TestScenarioTaskExecutionStepLog.cs +src/X1.Domain/Events/INodeExecutionEvent.cs +src/X1.Domain/Models/NextNodeInfo.cs +src/X1.Domain/Repositories/TestTask/ITestScenarioTaskExecutionStepLogRepository.cs +src/X1.Domain/Services/ITaskExecutionService.cs +src/X1.Infrastructure/Configurations/TestTask/TestScenarioTaskExecutionStepLogConfiguration.cs +src/X1.Infrastructure/Migrations/AppDbContextModelSnapshot.cs +src/X1.Infrastructure/Repositories/TestTask/TestScenarioTaskExecutionStepLogRepository.cs