TaskDispatchBackgroundService.cs 13.6 KB
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Rcs.Application.Shared;
using Rcs.Domain.Entities;
using Rcs.Domain.Enums;
using Rcs.Domain.Repositories;
using TaskStatus = Rcs.Domain.Entities.TaskStatus;

namespace Rcs.Infrastructure.Services
{
    /// <summary>
    /// 后台任务调度服务 - 循环调度等待中的任务分配给空闲机器人
    /// @author zzy
    /// </summary>
    public class TaskDispatchBackgroundService : BackgroundService, ITaskDispatchService
    {
        private readonly ILogger<TaskDispatchBackgroundService> _logger;
        private readonly IServiceProvider _serviceProvider;
        private readonly TimeSpan _dispatchInterval = TimeSpan.FromSeconds(5);
        private const int MaxPendingTasksPerCycle = 10;

        public TaskDispatchBackgroundService(
            ILogger<TaskDispatchBackgroundService> logger,
            IServiceProvider serviceProvider)
        {
            _logger = logger;
            _serviceProvider = serviceProvider;
        }

        /// <summary>
        /// 后台服务执行入口
        /// @author zzy
        /// </summary>
        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            _logger.LogInformation("[任务调度] 后台任务调度服务已启动");

            while (!stoppingToken.IsCancellationRequested)
            {
                try
                {
                    await DispatchAsync(stoppingToken);
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "[任务调度] 调度过程发生异常");
                }

                await Task.Delay(_dispatchInterval, stoppingToken);
            }

            _logger.LogInformation("[任务调度] 后台任务调度服务已停止");
        }

        /// <summary>
        /// 执行一次任务调度
        /// @author zzy
        /// </summary>
        public async Task<TaskDispatchResult> DispatchAsync(CancellationToken cancellationToken = default)
        {
            using var scope = _serviceProvider.CreateScope();
            var taskRepo = scope.ServiceProvider.GetRequiredService<IRobotTaskRepository>();
            var robotRepo = scope.ServiceProvider.GetRequiredService<IRobotRepository>();
            var templateRepo = scope.ServiceProvider.GetRequiredService<ITaskTemplateRepository>();

            // 1. 获取等待中的任务,按优先级升序(数值小优先级高)、创建时间升序排序,取前10个
            var pendingTasks = (await taskRepo.GetByStatusAsync(TaskStatus.Pending, cancellationToken))
                .OrderBy(t => t.Priority)
                .ThenBy(t => t.CreatedAt)
                .Take(MaxPendingTasksPerCycle)
                .ToList();

            if (!pendingTasks.Any())
            {
                return new TaskDispatchResult { Success = true, AssignedCount = 0, Message = "无待调度任务" };
            }

            int assignedCount = 0;

            foreach (var task in pendingTasks)
            {
                var taskWithDetails = await taskRepo.GetByIdWithDetailsAsync(task.TaskId, cancellationToken);
                if (taskWithDetails == null)
                    continue;

                // 获取任务起点所在地图的空闲机器人,筛选起点和终点库位类型都支持的机器人
                var idleRobot = await FindIdleRobotForTaskAsync(taskWithDetails, robotRepo,taskRepo, cancellationToken);
                if (idleRobot == null) continue;

                // 获取机器人对应的任务模板(包含步骤)
                var template = await GetTemplateForRobotAsync(idleRobot, templateRepo, cancellationToken);

                // 分配任务
                task.RobotId = idleRobot.RobotId;
                task.TaskTemplateId = template?.TemplateId;
                task.Status = TaskStatus.Assigned;
                task.UpdatedAt = DateTime.Now;

                await taskRepo.UpdateAsync(task, cancellationToken);
                await taskRepo.SaveChangesAsync(cancellationToken);

                // 根据模板创建子任务
                if (template != null && template.TaskSteps.Any())
                {
                    await CreateSubTasksFromTemplateAsync(taskWithDetails, idleRobot, template, cancellationToken);
                }

                _logger.LogInformation("[任务调度] 任务 {TaskCode} 已分配给机器人 {RobotCode},模板: {TemplateCode}",
                    task.TaskCode, idleRobot.RobotCode, template?.TemplateCode ?? "无");

                assignedCount++;
            }

            return new TaskDispatchResult
            {
                Success = true,
                AssignedCount = assignedCount,
                Message = $"本次调度完成,已分配 {assignedCount} 个任务"
            };
        }

        /// <summary>
        /// 根据任务起点所在地图和库位类型查找空闲机器人
        /// 筛选起点和终点库位类型都支持的机器人
        /// @author zzy
        /// </summary>
        private async Task<Robot?> FindIdleRobotForTaskAsync(
            RobotTask taskWithDetails,
            IRobotRepository robotRepo,
            IRobotTaskRepository taskRepo,
            CancellationToken cancellationToken)
        {
            if (taskWithDetails?.BeginLocation?.MapNode == null)
            {
                _logger.LogWarning("[任务调度] 任务 {TaskCode} 无起点库位信息", taskWithDetails.TaskCode);
                return null;
            }

            if (taskWithDetails?.EndLocation?.MapNode == null)
            {
                _logger.LogWarning("[任务调度] 任务 {TaskCode} 无终点库位信息", taskWithDetails.TaskCode);
                return null;
            }

            var mapId = taskWithDetails.BeginLocation?.MapNode.MapId;

            // 获取起点和终点的库位类型
            var beginLocationTypeId = taskWithDetails.BeginLocation?.MapNode.StorageLocationTypeId;
            var endLocationTypeId = taskWithDetails.EndLocation?.MapNode.StorageLocationTypeId;

            if (!beginLocationTypeId.HasValue || !endLocationTypeId.HasValue)
            {
                _logger.LogWarning("[任务调度] 任务 {TaskCode} 库位类型信息不完整", taskWithDetails.TaskCode);
                return null;
            }

            // 获取库位类型详情
            var locationTypeRepo = _serviceProvider.CreateScope().ServiceProvider
                .GetRequiredService<IStorageLocationTypeRepository>();
            var beginLocationType = await locationTypeRepo.GetByIdAsync(beginLocationTypeId.Value, cancellationToken);
            var endLocationType = await locationTypeRepo.GetByIdAsync(endLocationTypeId.Value, cancellationToken);

            if (beginLocationType == null || endLocationType == null)
            {
                _logger.LogWarning("[任务调度] 任务 {TaskCode} 库位类型不存在", taskWithDetails.TaskCode);
                return null;
            }

            // 获取该地图上所有空闲且在线的机器人
            var idleRobots = await robotRepo.GetIdleRobotsAsync(cancellationToken);

            // 先进行同步条件筛选
            var candidateRobots = idleRobots
                .Where(r => r.CurrentMapCodeId == mapId
                            && r.Online == OnlineStatus.Online
                            && r.Active == true
                            && r.Driving == false
                            && r.Paused == false
                            && r.CurrentNodeId != null
                            && r.RobotModel != null
                            && beginLocationType.RobotModels.Contains(r.RobotModel)
                            && endLocationType.RobotModels.Contains(r.RobotModel))
                .ToList();

            // 异步筛选:排除有进行中任务的机器人
            Robot? availableRobot = null;
            foreach (var robot in candidateRobots)
            {
                var robotTasks = await taskRepo.GetByRobotIdAsync(robot.RobotId);
                if (!robotTasks.Any(t => t.IsInProgress()))
                {
                    availableRobot = robot;
                    break;
                }
            }

            return availableRobot;
        }

        /// <summary>
        /// 获取机器人对应的任务模板(包含步骤和属性)
        /// 优先获取默认模板,默认模板优先
        /// @author zzy
        /// </summary>
        private async Task<TaskTemplate?> GetTemplateForRobotAsync(
            Robot robot,
            ITaskTemplateRepository templateRepo,
            CancellationToken cancellationToken)
        {
            // 优先获取该机器人类型和制造商的默认模板
            var template = await templateRepo.GetDefaultTemplateAsync(
                robot.RobotType,
                robot.RobotManufacturer,
                cancellationToken: cancellationToken);

            TaskTemplate? resultTemplate = null;
            if (template != null)
            {
                // 获取包含完整详情的模板(步骤、属性、动作)
                resultTemplate = await templateRepo.GetWithFullDetailsAsync(template.TemplateId, cancellationToken);
            }

            if (resultTemplate != null) return resultTemplate;

            // 如果没有默认模板,获取该机器人类型的任意启用模板
            var templates = await templateRepo.GetByRobotTypeAsync(robot.RobotType, cancellationToken);
            var fallbackTemplate = templates.FirstOrDefault(t => t.IsEnabled);

            if (fallbackTemplate != null)
            {
                // 获取包含完整详情的模板(步骤、属性、动作)
                resultTemplate = await templateRepo.GetWithFullDetailsAsync(fallbackTemplate.TemplateId, cancellationToken);
            }

            return resultTemplate;
        }

        /// <summary>
        /// 根据模板中的步骤创建子任务
        /// 以模板中的order排序创建子任务
        /// 除了第一个子任务,后续的子任务的起点都是上一个子任务的终点
        /// 根据step中的Node属性类型(NodeValueType)来确定终点
        /// @author zzy
        /// </summary>
        private async Task CreateSubTasksFromTemplateAsync(
            RobotTask taskWithDetails,
            Robot robot,
            TaskTemplate template,
            CancellationToken cancellationToken)
        {
            using var scope = _serviceProvider.CreateScope();
            var subTaskRepo = scope.ServiceProvider.GetRequiredService<IRobotSubTaskRepository>();
            var taskRepo = scope.ServiceProvider.GetRequiredService<IRobotTaskRepository>();

            // 按order排序获取模板步骤
            var orderedSteps = template.TaskSteps
                .OrderBy(s => s.Order)
                .ToList();

            if (orderedSteps.Count == 0)
            {
                _logger.LogWarning("[任务调度] 模板 {TemplateCode} 无步骤配置", template.TemplateCode);
                return;
            }

            if (taskWithDetails?.BeginLocation?.MapNode == null
                || taskWithDetails?.EndLocation?.MapNode == null)
            {
                _logger.LogWarning("[任务调度] 任务 {TaskCode} 缺少起点或终点节点信息", taskWithDetails.TaskCode);
                return;
            }

            Guid beginNodeId = taskWithDetails.BeginLocation.MapNode.NodeId;
            Guid endNodeId = taskWithDetails.EndLocation.MapNode.NodeId;

            // 跟踪上一个子任务的终点
            Guid previousEndNodeId = (Guid)robot.CurrentNodeId;
            int sequence = 1;

            foreach (var step in orderedSteps)
            {
                var subTask = new RobotSubTask
                {
                    SubTaskId = Guid.NewGuid(),
                    TaskId = taskWithDetails.TaskId,
                    RobotId = robot.RobotId,
                    Status = TaskStatus.Pending,
                    CreatedAt = DateTime.Now,
                    Sequence = sequence
                };

                // 设置子任务起点:第一个为机器人当前节点,后续为上一个子任务的终点

                subTask.BeginNodeId = previousEndNodeId;
                

                // 根据步骤的Node属性确定终点
                var nodeProperty = step.Properties.FirstOrDefault(p => p.PropertyType == StepPropertyType.Node);

                subTask.EndNodeId = nodeProperty?.NodeValue.HasValue == true
                    ? nodeProperty.NodeValue.Value switch
                    {
                        NodeValueType.Ts => beginNodeId,  // 任务起点
                        NodeValueType.Te => endNodeId,    // 任务终点
                        NodeValueType.Ws => endNodeId,    // 工位集合 - 暂时使用任务终点,后续可根据具体业务逻辑确定工位节点
                        _ => endNodeId                     // 默认使用任务终点
                    }
                    : endNodeId;  // 没有配置Node属性,默认使用任务终点

                await subTaskRepo.AddAsync(subTask, cancellationToken);

                previousEndNodeId = subTask.EndNodeId;
                sequence++;

                _logger.LogInformation("[任务调度] 创建子任务: 任务={TaskCode}, 子任务ID={SubTaskId}, 顺序={Sequence}, 起点={BeginNode}, 终点={EndNode}",
                    taskWithDetails.TaskCode, subTask.SubTaskId, subTask.Sequence, subTask.BeginNodeId, subTask.EndNodeId);
            }

            await subTaskRepo.SaveChangesAsync(cancellationToken);
        }
    }
}