RobotStatusPushService.cs 6.25 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);
        private DateTime _lastLocationPush = DateTime.MinValue;

        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] 服务启动");

            while (!stoppingToken.IsCancellationRequested)
            {
                try
                {
                    await PushRobotStatusAsync(stoppingToken);
                    await PushStorageLocationsAsync(stoppingToken);
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "[RobotStatusPush] 推送异常");
                }

                await Task.Delay(_pushInterval, stoppingToken);
            }
        }

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

            // 从缓存获取所有启用的机器人
            var activeRobots = await cacheService.GetAllActiveRobotCacheAsync();

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

            foreach (var mapGroup in robotsByMap)
            {
                var mapId = mapGroup.Key.ToString();
                var statusList = mapGroup.Select(r => 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
                }).ToList();

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

        /// <summary>
        /// 推送库位状态到订阅了库位信息的客户端(按地图分组,降频推送)
        /// @author zzy
        /// </summary>
        private async Task PushStorageLocationsAsync(CancellationToken cancellationToken)
        {
            // 降频:每2秒推送一次库位状态
            if (DateTime.Now - _lastLocationPush < _locationPushInterval)
                return;
            _lastLocationPush = DateTime.Now;

            using var scope = _scopeFactory.CreateScope();
            var areaRepo = scope.ServiceProvider.GetRequiredService<IStorageAreaRepository>();
            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);
            }
        }
    }
}