TaskDispatchBackgroundService.cs 29.5 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Rcs.Application.Services;
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;
        private const double EtaPrioritySlackMeters = 5d;
        private const double EtaWeight = 0.55d;
        private const double DetourWeight = 0.20d;
        private const double LoadWeight = 0.20d;
        private const double TaskCountWeight = 0.05d;

        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 robotCache = scope.ServiceProvider.GetRequiredService<IRobotCacheService>();
            var templateRepo = scope.ServiceProvider.GetRequiredService<ITaskTemplateRepository>();
            var locationTypeRepo = scope.ServiceProvider.GetRequiredService<IStorageLocationTypeRepository>();

            // 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 robotDispatchInfo = await FindIdleRobotForTaskAsync(taskWithDetails, taskRepo, robotCache, robotRepo, locationTypeRepo, cancellationToken);
                if (!robotDispatchInfo.HasValue) continue;

                var (idleRobot, availableCacheLocation) = robotDispatchInfo.Value;

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

                // 分配任务
                task.RobotId = idleRobot.RobotId;
                task.TaskTemplateId = template?.TemplateId;
                task.ShelfCode = availableCacheLocation.LocationCode;
                task.Status = TaskStatus.Assigned;
                task.UpdatedAt = DateTime.Now;
                availableCacheLocation.ContainerId = task.ContainerID;
                availableCacheLocation.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 Robot, RobotCacheLocation CacheLocation)?> FindIdleRobotForTaskAsync(
            RobotTask taskWithDetails,
            IRobotTaskRepository taskRepo,
            IRobotCacheService robotCache,
            IRobotRepository robotRepo,
            IStorageLocationTypeRepository locationTypeRepo,
            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 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 robotCacheData = await robotCache.GetAllActiveRobotCacheAsync();
            var idleRobots = robotCacheData.Select(r => new Robot
            {
                RobotId = Guid.Parse(r.Basic.RobotId),
                RobotCode = r.Basic.RobotCode,
                RobotName = r.Basic.RobotName,
                RobotVersion = r.Basic.RobotVersion,
                ProtocolName = r.Basic.ProtocolName,
                ProtocolVersion = r.Basic.ProtocolVersion,
                ProtocolType = (ProtocolType)r.Basic.ProtocolType,
                RobotManufacturer = r.Basic.RobotManufacturer,
                RobotSerialNumber = r.Basic.RobotSerialNumber,
                RobotModel = r.Basic.RobotModel,
                RobotType = (RobotType)r.Basic.RobotType,
                IpAddress = r.Basic.IpAddress,
                CoordinateScale = r.Basic.CoordinateScale,
                Active = r.Basic.Active,
                Status = r.Status?.Status ?? RobotStatus.Idle,
                Online = r.Status?.Online ?? OnlineStatus.Offline,
                BatteryLevel = r.Status?.BatteryLevel,
                Driving = r.Status?.Driving ?? false,
                Paused = r.Status?.Paused ?? false,
                Charging = r.Status?.Charging ?? false,
                OperatingMode = r.Status?.OperatingMode ?? OperatingMode.Automatic,
                CurrentMapCodeId = r.Location?.MapId,
                CurrentNodeId = r.Location?.NodeId,
                CurrentX = r.Location?.X,
                CurrentY = r.Location?.Y,
                CurrentTheta = r.Location?.Theta
            }).ToList();

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

            if (!candidateRobots.Any())
            {
                return null;
            }

            var dispatchTargetPoint = GetDispatchTargetPoint(taskWithDetails);
            if (dispatchTargetPoint == null)
            {
                _logger.LogWarning("[任务调度] 任务 {TaskCode} 缺少可用于调度评分的目标点信息", taskWithDetails.TaskCode);
                return null;
            }

            var dispatchCandidates = new List<DispatchRobotCandidate>();
            foreach (var liveRobot in candidateRobots)
            {
                var robotWithCacheLocations = await robotRepo.GetByIdFullDataAsync(liveRobot.RobotId, cancellationToken);
                if (robotWithCacheLocations == null)
                {
                    continue;
                }

                // 多缓存储位场景:按“由低到高、由左到右、由前到后”分配空缓存位
                var availableCacheLocation = robotWithCacheLocations.CacheLocations
                    .Where(c => string.IsNullOrWhiteSpace(c.ContainerId))
                    .OrderBy(c => c.Level)
                    .ThenBy(c => c.Column)
                    .ThenBy(c => c.Row)
                    .ThenBy(c => c.LocationCode)
                    .FirstOrDefault();

                if (availableCacheLocation != null)
                {
                    var currentX = liveRobot.CurrentX ?? robotWithCacheLocations.CurrentX ?? robotWithCacheLocations.MapNode?.X;
                    var currentY = liveRobot.CurrentY ?? robotWithCacheLocations.CurrentY ?? robotWithCacheLocations.MapNode?.Y;

                    dispatchCandidates.Add(new DispatchRobotCandidate(
                        robotWithCacheLocations,
                        availableCacheLocation,
                        currentX,
                        currentY));
                }
            }

            if (!dispatchCandidates.Any())
            {
                return null;
            }

            var candidateRobotIds = dispatchCandidates
                .Select(c => c.Robot.RobotId)
                .ToHashSet();

            var assignedTasks = (await taskRepo.GetByStatusAsync(TaskStatus.Assigned, cancellationToken))
                .Where(t => t.RobotId.HasValue && candidateRobotIds.Contains(t.RobotId.Value));

            var inProgressTasks = (await taskRepo.GetByStatusAsync(TaskStatus.InProgress, cancellationToken))
                .Where(t => t.RobotId.HasValue && candidateRobotIds.Contains(t.RobotId.Value));

            var activeTaskIds = assignedTasks
                .Concat(inProgressTasks)
                .Select(t => t.TaskId)
                .Distinct()
                .ToList();

            var activeTasksWithDetails = new List<RobotTask>(activeTaskIds.Count);
            foreach (var activeTaskId in activeTaskIds)
            {
                var activeTaskWithDetails = await taskRepo.GetByIdWithDetailsAsync(activeTaskId, cancellationToken);
                if (activeTaskWithDetails?.RobotId.HasValue == true
                    && candidateRobotIds.Contains(activeTaskWithDetails.RobotId.Value))
                {
                    activeTasksWithDetails.Add(activeTaskWithDetails);
                }
            }

            var activeTasksByRobot = activeTasksWithDetails
                .Where(t => t.RobotId.HasValue)
                .GroupBy(t => t.RobotId!.Value)
                .ToDictionary(g => g.Key, g => (IReadOnlyCollection<RobotTask>)g.ToList());

            var scoreCards = new List<DispatchRobotScore>(dispatchCandidates.Count);
            foreach (var candidate in dispatchCandidates)
            {
                activeTasksByRobot.TryGetValue(candidate.Robot.RobotId, out var robotTasks);
                scoreCards.Add(BuildDispatchScore(candidate, robotTasks ?? Array.Empty<RobotTask>(), dispatchTargetPoint.Value));
            }

            var selectedScore = SelectBestScore(scoreCards);
            if (selectedScore == null)
            {
                return null;
            }

            _logger.LogInformation(
                "[任务调度] 任务 {TaskCode} 分配评分结果: 机器人={RobotCode}, ETA={EtaDistance:F2}, 增量={DetourDistance:F2}, 负载={LoadDistance:F2}, 任务数={TaskCount}, 综合分={CompositeScore:F4}",
                taskWithDetails.TaskCode,
                selectedScore.Robot.RobotCode,
                selectedScore.EtaDistance,
                selectedScore.DetourDistance,
                selectedScore.LoadDistance,
                selectedScore.ActiveTaskCount,
                selectedScore.CompositeScore);

            return (selectedScore.Robot, selectedScore.CacheLocation);
        }

        private static DispatchPoint? GetDispatchTargetPoint(RobotTask task)
        {
            var nextSubTask = task.GetNextExecutableSubTask();
            var nextTargetNode = nextSubTask?.EndNode ?? task.BeginLocation?.MapNode;
            if (nextTargetNode != null)
            {
                return new DispatchPoint(nextTargetNode.X, nextTargetNode.Y);
            }

            return null;
        }

        private DispatchRobotScore BuildDispatchScore(
            DispatchRobotCandidate candidate,
            IReadOnlyCollection<RobotTask> activeTasks,
            DispatchPoint dispatchTargetPoint)
        {
            if (!candidate.CurrentX.HasValue || !candidate.CurrentY.HasValue)
            {
                return new DispatchRobotScore(
                    candidate,
                    double.MaxValue,
                    double.MaxValue,
                    double.MaxValue,
                    activeTasks.Count,
                    double.MaxValue);
            }

            var robotStartPoint = new DispatchPoint(candidate.CurrentX.Value, candidate.CurrentY.Value);

            var pendingTaskPoints = activeTasks
                .Select(GetDispatchTargetPoint)
                .Where(point => point.HasValue)
                .Select(point => point!.Value)
                .ToList();

            var executionChain = BuildNearestNeighborChain(robotStartPoint, pendingTaskPoints);
            var loadDistance = CalculatePathDistance(robotStartPoint, executionChain);
            var insertion = EvaluateBestInsertion(robotStartPoint, executionChain, dispatchTargetPoint);

            return new DispatchRobotScore(
                candidate,
                insertion.EtaDistance,
                insertion.DetourDistance,
                loadDistance,
                pendingTaskPoints.Count,
                0d);
        }

        private static DispatchInsertionResult EvaluateBestInsertion(
            DispatchPoint start,
            IReadOnlyList<DispatchPoint> chain,
            DispatchPoint target)
        {
            if (chain.Count == 0)
            {
                var directDistance = CalculateEuclideanDistance(start, target);
                return new DispatchInsertionResult(directDistance, directDistance);
            }

            var arrivalsAtChainPoint = new double[chain.Count];
            arrivalsAtChainPoint[0] = CalculateEuclideanDistance(start, chain[0]);
            for (int i = 1; i < chain.Count; i++)
            {
                arrivalsAtChainPoint[i] = arrivalsAtChainPoint[i - 1] + CalculateEuclideanDistance(chain[i - 1], chain[i]);
            }

            double bestEta = double.MaxValue;
            double bestDetour = double.MaxValue;

            for (int insertIndex = 0; insertIndex <= chain.Count; insertIndex++)
            {
                var previousPoint = insertIndex == 0 ? start : chain[insertIndex - 1];
                var previousArrival = insertIndex == 0 ? 0d : arrivalsAtChainPoint[insertIndex - 1];
                var etaDistance = previousArrival + CalculateEuclideanDistance(previousPoint, target);

                double detourDistance;
                if (insertIndex == chain.Count)
                {
                    detourDistance = CalculateEuclideanDistance(previousPoint, target);
                }
                else
                {
                    var nextPoint = chain[insertIndex];
                    detourDistance = CalculateEuclideanDistance(previousPoint, target)
                                     + CalculateEuclideanDistance(target, nextPoint)
                                     - CalculateEuclideanDistance(previousPoint, nextPoint);
                }

                if (etaDistance < bestEta || (Math.Abs(etaDistance - bestEta) < 0.0001d && detourDistance < bestDetour))
                {
                    bestEta = etaDistance;
                    bestDetour = detourDistance;
                }
            }

            return new DispatchInsertionResult(bestEta, bestDetour);
        }

        private static List<DispatchPoint> BuildNearestNeighborChain(
            DispatchPoint start,
            IReadOnlyCollection<DispatchPoint> points)
        {
            var remaining = points.ToList();
            var chain = new List<DispatchPoint>(remaining.Count);
            var current = start;

            while (remaining.Count > 0)
            {
                int nearestIndex = 0;
                double nearestDistance = double.MaxValue;

                for (int i = 0; i < remaining.Count; i++)
                {
                    var distance = CalculateEuclideanDistance(current, remaining[i]);
                    if (distance < nearestDistance)
                    {
                        nearestDistance = distance;
                        nearestIndex = i;
                    }
                }

                current = remaining[nearestIndex];
                chain.Add(current);
                remaining.RemoveAt(nearestIndex);
            }

            return chain;
        }

        private static DispatchRobotScore? SelectBestScore(IReadOnlyCollection<DispatchRobotScore> scoreCards)
        {
            if (!scoreCards.Any())
            {
                return null;
            }

            var bestEta = scoreCards.Min(s => s.EtaDistance);
            var etaPreferred = scoreCards
                .Where(s => s.EtaDistance <= bestEta + EtaPrioritySlackMeters)
                .ToList();

            var etaMin = etaPreferred.Min(s => s.EtaDistance);
            var etaMax = etaPreferred.Max(s => s.EtaDistance);
            var detourMin = etaPreferred.Min(s => s.DetourDistance);
            var detourMax = etaPreferred.Max(s => s.DetourDistance);
            var loadMin = etaPreferred.Min(s => s.LoadDistance);
            var loadMax = etaPreferred.Max(s => s.LoadDistance);
            var taskCountMin = etaPreferred.Min(s => s.ActiveTaskCount);
            var taskCountMax = etaPreferred.Max(s => s.ActiveTaskCount);

            var rescored = etaPreferred
                .Select(s =>
                {
                    var score = EtaWeight * Normalize(s.EtaDistance, etaMin, etaMax)
                                + DetourWeight * Normalize(s.DetourDistance, detourMin, detourMax)
                                + LoadWeight * Normalize(s.LoadDistance, loadMin, loadMax)
                                + TaskCountWeight * Normalize(s.ActiveTaskCount, taskCountMin, taskCountMax);

                    return s with { CompositeScore = score };
                })
                .ToList();

            return rescored
                .OrderBy(s => s.CompositeScore)
                .ThenBy(s => s.EtaDistance)
                .ThenBy(s => s.LoadDistance)
                .ThenBy(s => s.ActiveTaskCount)
                .ThenBy(s => s.Robot.RobotCode)
                .FirstOrDefault();
        }

        /// <summary>
        /// 计算两点直线距离。
        /// </summary>
        /// <param name="sourceX">起点 X</param>
        /// <param name="sourceY">起点 Y</param>
        /// <param name="targetX">终点 X</param>
        /// <param name="targetY">终点 Y</param>
        /// <returns>直线距离;若起点坐标缺失则返回 double.MaxValue</returns>
        private static double CalculateEuclideanDistance(double? sourceX, double? sourceY, double targetX, double targetY)
        {
            if (!sourceX.HasValue || !sourceY.HasValue)
            {
                return double.MaxValue;
            }

            var dx = sourceX.Value - targetX;
            var dy = sourceY.Value - targetY;
            return Math.Sqrt(dx * dx + dy * dy);
        }

        private static double CalculateEuclideanDistance(DispatchPoint source, DispatchPoint target)
        {
            return CalculateEuclideanDistance(source.X, source.Y, target.X, target.Y);
        }

        private static double CalculatePathDistance(DispatchPoint start, IReadOnlyList<DispatchPoint> chain)
        {
            if (chain.Count == 0)
            {
                return 0d;
            }

            var totalDistance = CalculateEuclideanDistance(start, chain[0]);
            for (int i = 1; i < chain.Count; i++)
            {
                totalDistance += CalculateEuclideanDistance(chain[i - 1], chain[i]);
            }

            return totalDistance;
        }

        private static double Normalize(double value, double min, double max)
        {
            if (double.IsNaN(value) || value == double.MaxValue)
            {
                return 1d;
            }

            var range = max - min;
            if (Math.Abs(range) < 0.0001d)
            {
                return 0d;
            }

            return (value - min) / range;
        }

        private readonly record struct DispatchPoint(double X, double Y);

        private readonly record struct DispatchInsertionResult(double EtaDistance, double DetourDistance);

        private sealed record DispatchRobotCandidate(
            Robot Robot,
            RobotCacheLocation CacheLocation,
            double? CurrentX,
            double? CurrentY);

        private sealed record DispatchRobotScore(
            DispatchRobotCandidate Candidate,
            double EtaDistance,
            double DetourDistance,
            double LoadDistance,
            int ActiveTaskCount,
            double CompositeScore)
        {
            public Robot Robot => Candidate.Robot;
            public RobotCacheLocation CacheLocation => Candidate.CacheLocation;
        }

        /// <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);
        }
    }
}