RobotStatusPushService.cs 11.2 KB
using MassTransit.Internals;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Rcs.Api.Hubs;
using Rcs.Application.DTOs;
using Rcs.Application.Services;
using Rcs.Domain.Repositories;
using Rcs.Shared.Utils;

namespace Rcs.Api.BackgroundServices
{
    /// <summary>
    /// 机器人状态推送后台服务 - 定时推送机器人实时状态到前端
    /// 推送策略:
    /// 1. 订阅机器人状态的客户端只接收机器人状态
    /// 2. 订阅库位信息的客户端只接收库位信息
    /// @author zzy
    /// </summary>
    public class RobotStatusPushService : BackgroundService
    {
        private readonly ILogger<RobotStatusPushService> _logger;
        private readonly IHubContext<HHRCSHub> _hubContext;
        private readonly IServiceScopeFactory _scopeFactory;
        private readonly TimeSpan _pushInterval = TimeSpan.FromMilliseconds(200);
        private readonly TimeSpan _locationPushInterval = TimeSpan.FromSeconds(2);

        public RobotStatusPushService(
            ILogger<RobotStatusPushService> logger,
            IHubContext<HHRCSHub> hubContext,
            IServiceScopeFactory scopeFactory)
        {
            _logger = logger;
            _hubContext = hubContext;
            _scopeFactory = scopeFactory;
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            _logger.LogInformation("[RobotStatusPush] 服务启动");

            var robotStatusTask = RunRobotStatusLoopAsync(stoppingToken);
            var storageLocationTask = RunStorageLocationLoopAsync(stoppingToken);

            await Task.WhenAll(robotStatusTask, storageLocationTask);
        }

        private async Task RunRobotStatusLoopAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                try
                {
                    await PushRobotStatusAsync(stoppingToken);
                }
                catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
                {
                    break;
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "[RobotStatusPush] 机器人状态推送异常");
                }

                try
                {
                    await Task.Delay(_pushInterval, stoppingToken);
                }
                catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
                {
                    break;
                }
            }
        }

        private async Task RunStorageLocationLoopAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                try
                {
                    await PushStorageLocationsAsync(stoppingToken);
                }
                catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
                {
                    break;
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "[RobotStatusPush] 库位状态推送异常");
                }

                try
                {
                    await Task.Delay(_locationPushInterval, stoppingToken);
                }
                catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
                {
                    break;
                }
            }
        }

        /// <summary>
        /// 推送机器人状态到订阅了机器人状态的客户端(纯缓存读取,不查数据库)
        /// @author zzy
        /// </summary>
        private async Task PushRobotStatusAsync(CancellationToken cancellationToken)
        {
            using var scope = _scopeFactory.CreateScope();
            var cacheService = scope.ServiceProvider.GetRequiredService<IRobotCacheService>();
            var mapCacheService = scope.ServiceProvider.GetRequiredService<IMapCacheService>();
            var trafficControlService = scope.ServiceProvider.GetRequiredService<IUnifiedTrafficControlService>();

            // 从缓存获取所有启用的机器人
            var activeRobots = (await cacheService.GetAllActiveRobotCacheAsync())
                .Where(r => r.Location?.MapId.HasValue == true)
                .ToList();

            if (activeRobots.Count == 0)
            {
                return;
            }

            // 预加载 MapId -> MapCode 映射,用于按当前地图筛选锁资源
            var mapCodeById = new Dictionary<Guid, string>();
            var mapIds = activeRobots
                .Select(r => r.Location!.MapId!.Value)
                .Distinct()
                .ToList();

            foreach (var mapId in mapIds)
            {
                var mapData = await mapCacheService.GetMapAsync(mapId);
                if (!string.IsNullOrWhiteSpace(mapData?.MapCode))
                {
                    mapCodeById[mapId] = mapData.MapCode;
                }
            }

            // 预加载每台机器人在当前地图中的节点锁/边锁
            var lockCodesByRobotId = new Dictionary<Guid, (List<string> NodeCodes, List<string> EdgeCodes)>();
            foreach (var robot in activeRobots)
            {
                if (!Guid.TryParse(robot.Basic.RobotId, out var robotId))
                {
                    continue;
                }

                var mapId = robot.Location!.MapId!.Value;
                if (!mapCodeById.TryGetValue(mapId, out var mapCode))
                {
                    continue;
                }

                var (nodeKeys, edgeKeys) = await trafficControlService.GetRobotLockedResourcesAsync(robotId);
                lockCodesByRobotId[robotId] = (
                    ExtractLockCodesByMap(nodeKeys, mapCode),
                    ExtractLockCodesByMap(edgeKeys, mapCode)
                );
            }

            // 按地图分组
            var robotsByMap = activeRobots
                .GroupBy(r => r.Location!.MapId!.Value);

            foreach (var mapGroup in robotsByMap)
            {
                var mapId = mapGroup.Key.ToString();
                var statusList = mapGroup
                    .OrderBy(r => r.Basic.RobotCode)
                    .Select(r =>
                    {
                        var lockedNodeCodes = new List<string>();
                        var lockedEdgeCodes = new List<string>();
                        if (Guid.TryParse(r.Basic.RobotId, out var robotId) &&
                            lockCodesByRobotId.TryGetValue(robotId, out var lockCodes))
                        {
                            lockedNodeCodes = lockCodes.NodeCodes;
                            lockedEdgeCodes = lockCodes.EdgeCodes;
                        }

                        return new RobotRealtimeStatusDto
                        {
                            RobotId = r.Basic.RobotId,
                            RobotType = r.Basic.RobotType,
                            RobotCode = r.Basic.RobotCode,
                            RobotName = r.Basic.RobotName,
                            X = r.Location?.X,
                            Y = r.Location?.Y,
                            Theta = AngleConverter.ToCycleDegrees(r.Location?.Theta),
                            Status = r.Status != null ? (int)r.Status.Status : 1,
                            Online = r.Status != null ? (int)r.Status.Online : 2,
                            BatteryLevel = r.Status?.BatteryLevel,
                            Driving = r.Status?.Driving ?? false,
                            Paused = r.Status?.Paused ?? false,
                            Charging = r.Status?.Charging ?? false,
                            Errors = r.Status?.Errors,
                            Path = r.Location?.Path,
                            CurNode = r.Location?.NodeId?.ToString(),
                            LockedNodeCodes = lockedNodeCodes,
                            LockedEdgeCodes = lockedEdgeCodes
                        };
                    })
                    .ToList();

                // 向订阅了该地图机器人状态的客户端推送(地图隔离)
                await _hubContext.Clients.Group($"{mapId}_robot_status")
                    .SendAsync("RobotStatusUpdate", statusList, cancellationToken);
            }
        }

        /// <summary>
        /// 推送库位状态到订阅了库位信息的客户端(按地图分组,降频推送)
        /// @author zzy
        /// </summary>
        private async Task PushStorageLocationsAsync(CancellationToken cancellationToken)
        {
            using var scope = _scopeFactory.CreateScope();
            var mapRepo = scope.ServiceProvider.GetRequiredService<IMapRepository>();
            var nodeRepo = scope.ServiceProvider.GetRequiredService<IMapNodeRepository>();

            // 获取所有启用的地图
            var maps = await mapRepo.GetAllAsync(cancellationToken);
            foreach (var map in maps.Where(m => m.Active))
            {
                var nodes = await nodeRepo.GetLocationsByMapIdAsync(map.MapId, cancellationToken);
                // 映射到 DTO 避免循环引用
                var areasDto = nodes.Select(a => new
                {
                    StorageLocations = a.StorageLocations.Select(loc => new
                    {
                        locationId = loc.LocationId.ToString(),
                        locationCode = loc.LocationCode,
                        locationName = loc.LocationName,
                        layerNumber = loc.LayerNumber,
                        status = loc.Status,
                        mapNodeId = loc.MapNodeId,
                        isActive = loc.IsActive
                    }).ToList(),
                    NodeId = a.NodeId,
                    NodeCode = a.NodeCode,
                    NodeName = a.NodeName,
                }).ToList();

                // 向订阅了该地图库位信息的客户端推送(地图隔离)
                await _hubContext.Clients.Group($"{map.MapId}_storage_location")
                    .SendAsync("StorageLocationsUpdate", areasDto, cancellationToken);
            }
        }

        private static List<string> ExtractLockCodesByMap(IEnumerable<string> lockKeys, string mapCode)
        {
            if (string.IsNullOrWhiteSpace(mapCode))
            {
                return new List<string>();
            }

            var marker = $":{mapCode}:";
            return lockKeys
                .Select(key =>
                {
                    var index = key.IndexOf(marker, StringComparison.OrdinalIgnoreCase);
                    if (index < 0)
                    {
                        return null;
                    }

                    var codeStartIndex = index + marker.Length;
                    return codeStartIndex >= key.Length ? null : key[codeStartIndex..];
                })
                .Where(code => !string.IsNullOrWhiteSpace(code))
                .Select(code => code!)
                .Distinct(StringComparer.OrdinalIgnoreCase)
                .OrderBy(code => code, StringComparer.OrdinalIgnoreCase)
                .ToList();
        }
    }
}