RobotCacheService.cs 13.8 KB
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Rcs.Application.Services;
using Rcs.Domain.Entities;
using Rcs.Shared.Utils;
using StackExchange.Redis;

namespace Rcs.Infrastructure.Services
{
    /// <summary>
    /// 机器人缓存服务实现 - 使用Redis Hash结构存储机器人状态和位置
    /// @author zzy
    /// </summary>
    public class RobotCacheService : IRobotCacheService
    {
        private readonly IConnectionMultiplexer _redis;
        private readonly ILogger<RobotCacheService> _logger;

        private const string StatusKeyPrefix = "rcs:robot:";
        private const string StatusSuffix = ":status";
        private const string LocationSuffix = ":location";
        private const string BasicSuffix = ":basic";
        private const string RobotsSetKey = "rcs:robots";
        private const string OnlineSetKey = "rcs:robots:online";
        private const string IdleSetKey = "rcs:robots:idle";

        public RobotCacheService(IConnectionMultiplexer redis, ILogger<RobotCacheService> logger)
        {
            _redis = redis;
            _logger = logger;
        }

        /// <summary>
        /// 更新机器人状态到Redis Hash(使用制造商+序列号作为唯一标识)
        /// @author zzy
        /// </summary>
        public async Task UpdateStatusAsync(string manufacturer, string serialNumber, RobotStatus? status, OnlineStatus? online,
            int? batteryLevel, bool? driving, bool? paused, bool? charging, OperatingMode? operatingMode, string? errors)
        {
            var db = _redis.GetDatabase();
            var key = $"{StatusKeyPrefix}{manufacturer}:{serialNumber}{StatusSuffix}";
            var robotKey = $"{manufacturer}:{serialNumber}";
            var entries = new List<HashEntry>();

            if (status.HasValue) entries.Add(new HashEntry("Status", (int)status.Value));
            if (online.HasValue) entries.Add(new HashEntry("Online", (int)online.Value));
            if (batteryLevel.HasValue) entries.Add(new HashEntry("BatteryLevel", batteryLevel.Value));
            if (driving.HasValue) entries.Add(new HashEntry("Driving", driving.Value ? "1" : "0"));
            if (paused.HasValue) entries.Add(new HashEntry("Paused", paused.Value ? "1" : "0"));
            if (charging.HasValue) entries.Add(new HashEntry("Charging", charging.Value ? "1" : "0"));
            if (operatingMode.HasValue) entries.Add(new HashEntry("OperatingMode", (int)operatingMode.Value));
            if (errors != null) entries.Add(new HashEntry("Errors", errors));
            entries.Add(new HashEntry("UpdatedAt", DateTime.Now.ToString("O")));

            if (entries.Count > 0)
            {
                await db.HashSetAsync(key, entries.ToArray());
            }

            // 更新在线/空闲集合
            if (online.HasValue)
            {
                if (online.Value == OnlineStatus.Online)
                    await db.SetAddAsync(OnlineSetKey, robotKey);
                else
                    await db.SetRemoveAsync(OnlineSetKey, robotKey);
            }

            if (status.HasValue)
            {
                if (status.Value == RobotStatus.Idle)
                    await db.SetAddAsync(IdleSetKey, robotKey);
                else
                    await db.SetRemoveAsync(IdleSetKey, robotKey);
            }
        }

        /// <summary>
        /// 更新机器人位置到Redis Hash(使用制造商+序列号作为唯一标识)
        /// @author zzy
        /// </summary>
        public async Task UpdateLocationAsync(string manufacturer, string serialNumber, Guid? mapId, Guid? nodeId,
            double? x, double? y, double? theta)
        {
            var db = _redis.GetDatabase();
            var key = $"{StatusKeyPrefix}{manufacturer}:{serialNumber}{LocationSuffix}";
            var entries = new List<HashEntry>();

            if (mapId.HasValue) entries.Add(new HashEntry("MapId", mapId.Value.ToString()));
            if (nodeId.HasValue) entries.Add(new HashEntry("NodeId", nodeId.Value.ToString()));
            if (x.HasValue) entries.Add(new HashEntry("X", x.Value.ToString(CultureInfo.InvariantCulture)));
            if (y.HasValue) entries.Add(new HashEntry("Y", y.Value.ToString(CultureInfo.InvariantCulture)));
            if (theta.HasValue) entries.Add(new HashEntry("Theta", theta.Value.ToString(CultureInfo.InvariantCulture)));
            entries.Add(new HashEntry("UpdatedAt", DateTime.Now.ToString("O")));

            if (entries.Count > 0)
            {
                await db.HashSetAsync(key, entries.ToArray());
            }
        }

        /// <summary>
        /// 从Redis获取机器人状态(使用制造商+序列号作为唯一标识)
        /// @author zzy
        /// </summary>
        public async Task<RobotStatusCache?> GetStatusAsync(string manufacturer, string serialNumber)
        {
            var db = _redis.GetDatabase();
            var key = $"{StatusKeyPrefix}{manufacturer}:{serialNumber}{StatusSuffix}";
            var hash = await db.HashGetAllAsync(key);

            if (hash.Length == 0) return null;

            var dict = hash.ToDictionary(h => h.Name.ToString(), h => h.Value.ToString());
            return new RobotStatusCache
            {
                Status = dict.TryGetValue("Status", out var s) && !string.IsNullOrEmpty(s) ? (RobotStatus)int.Parse(s) : RobotStatus.Idle,
                Online = dict.TryGetValue("Online", out var o) && !string.IsNullOrEmpty(o) ? (OnlineStatus)int.Parse(o) : OnlineStatus.Offline,
                BatteryLevel = dict.TryGetValue("BatteryLevel", out var b) && !string.IsNullOrEmpty(b) ? int.Parse(b) : null,
                Driving = dict.TryGetValue("Driving", out var d) && d == "1",
                Paused = dict.TryGetValue("Paused", out var p) && p == "1",
                Charging = dict.TryGetValue("Charging", out var c) && c == "1",
                OperatingMode = dict.TryGetValue("OperatingMode", out var m) && !string.IsNullOrEmpty(m) ? (OperatingMode)int.Parse(m) : OperatingMode.Automatic,
                Errors = dict.TryGetValue("Errors", out var e) && !string.IsNullOrEmpty(e)
                    ? e.FromJson<List<RobotError>>() : null,
                UpdatedAt = dict.TryGetValue("UpdatedAt", out var u) && !string.IsNullOrEmpty(u) ? DateTime.Parse(u) : DateTime.Now
            };
        }

        /// <summary>
        /// 从Redis获取机器人位置(使用制造商+序列号作为唯一标识)
        /// @author zzy
        /// </summary>
        public async Task<RobotLocationCache?> GetLocationAsync(string manufacturer, string serialNumber)
        {
            var db = _redis.GetDatabase();
            var key = $"{StatusKeyPrefix}{manufacturer}:{serialNumber}{LocationSuffix}";
            var hash = await db.HashGetAllAsync(key);

            if (hash.Length == 0) return null;

            var dict = hash.ToDictionary(h => h.Name.ToString(), h => h.Value.ToString());
            return new RobotLocationCache
            {
                MapId = dict.TryGetValue("MapId", out var m) && Guid.TryParse(m, out var mapId) ? mapId : null,
                NodeId = dict.TryGetValue("NodeId", out var n) && Guid.TryParse(n, out var nodeId) ? nodeId : null,
                X = dict.TryGetValue("X", out var x) && !string.IsNullOrEmpty(x) ? double.Parse(x, CultureInfo.InvariantCulture) : null,
                Y = dict.TryGetValue("Y", out var y) && !string.IsNullOrEmpty(y) ? double.Parse(y, CultureInfo.InvariantCulture) : null,
                Theta = dict.TryGetValue("Theta", out var t) && !string.IsNullOrEmpty(t) ? double.Parse(t, CultureInfo.InvariantCulture) : null,
                Path = dict.TryGetValue("Path", out var p) ? p.FromJson<List<List<double>>>() : null,
                UpdatedAt = dict.TryGetValue("UpdatedAt", out var ut) && !string.IsNullOrEmpty(ut) ? DateTime.Parse(ut) : DateTime.Now
            };
        }
        /// <summary>
        /// 从Redis获取机器人基础信息
        /// @author zzy
        /// </summary>
        /// <param name="manufacturer">制造商</param>
        /// <param name="serialNumber">序列号</param>
        /// <returns>机器人基础信息缓存,不存在则返回null</returns>
        public async Task<RobotBasicCache?> GetBasicAsync(string manufacturer, string serialNumber)
        {
            var db = _redis.GetDatabase();
            var key = $"{StatusKeyPrefix}{manufacturer}:{serialNumber}{BasicSuffix}";
            var hash = await db.HashGetAllAsync(key);

            if (hash.Length == 0) return null;

            var dict = hash.ToDictionary(h => h.Name.ToString(), h => h.Value.ToString());
            return new RobotBasicCache
            {
                RobotId = dict.TryGetValue("RobotId", out var rId) ? rId : string.Empty,
                RobotCode = dict.TryGetValue("RobotCode", out var rc) ? rc : string.Empty,
                RobotName = dict.TryGetValue("RobotName", out var rn) ? rn : string.Empty,
                RobotVersion = dict.TryGetValue("RobotVersion", out var rv) ? rv : string.Empty,
                ProtocolName = dict.TryGetValue("ProtocolName", out var pn) ? pn : string.Empty,
                ProtocolVersion = dict.TryGetValue("ProtocolVersion", out var pv) ? pv : string.Empty,
                ProtocolType = dict.TryGetValue("ProtocolType", out var pt) && int.TryParse(pt, out var protocolType) ? protocolType : 0,
                RobotManufacturer = dict.TryGetValue("RobotManufacturer", out var rm) ? rm : string.Empty,
                RobotSerialNumber = dict.TryGetValue("RobotSerialNumber", out var rs) ? rs : string.Empty,
                RobotType = dict.TryGetValue("RobotType", out var rt) && int.TryParse(rt, out var robotType) ? robotType : 0,
                IpAddress = dict.TryGetValue("IpAddress", out var ip) ? ip : null,
                CoordinateScale = dict.TryGetValue("CoordinateScale", out var cs) && double.TryParse(cs, CultureInfo.InvariantCulture, out var scale) ? scale : 1d,
                Active = dict.TryGetValue("Active", out var a) && a == "1"
            };
        }


        /// <summary>
        /// 获取所有在线机器人标识(制造商:序列号)
        /// @author zzy
        /// </summary>
        public async Task<IEnumerable<string>> GetOnlineRobotKeysAsync()
        {
            var db = _redis.GetDatabase();
            var members = await db.SetMembersAsync(OnlineSetKey);
            return members.Select(m => m.ToString());
        }

        /// <summary>
        /// 获取所有空闲机器人标识(制造商:序列号)
        /// @author zzy
        /// </summary>
        public async Task<IEnumerable<string>> GetIdleRobotKeysAsync()
        {
            var db = _redis.GetDatabase();
            var members = await db.SetMembersAsync(IdleSetKey);
            return members.Select(m => m.ToString());
        }

        /// <summary>
        /// 根据制造商、序列号和字段名修改对应的值
        /// @author zzy
        /// </summary>
        /// <param name="manufacturer">制造商</param>
        /// <param name="serialNumber">序列号</param>
        /// <param name="field">Hash字段名</param>
        /// <param name="value">要设置的值</param>
        /// <returns>是否为新增字段(true=新增,false=更新)</returns>
        public async Task<bool> SetLocationValueAsync(string manufacturer, string serialNumber, string field, string value)
        {
            var db = _redis.GetDatabase();
            var key = $"{StatusKeyPrefix}{manufacturer}:{serialNumber}{LocationSuffix}";
            return await db.HashSetAsync(key, field, value);
        }

        /// <summary>
        /// 获取所有需要持久化的机器人状态数据(使用制造商+序列号作为唯一标识)
        /// @author zzy
        /// </summary>
        public async Task<IEnumerable<(string Manufacturer, string SerialNumber, RobotStatusCache Status, RobotLocationCache Location)>> GetAllRobotCacheDataAsync()
        {
            var db = _redis.GetDatabase();
            var members = await db.SetMembersAsync(RobotsSetKey);
            var result = new List<(string, string, RobotStatusCache, RobotLocationCache)>();

            foreach (var member in members)
            {
                var parts = member.ToString().Split(':');
                if (parts.Length != 2) continue;
                var manufacturer = parts[0];
                var serialNumber = parts[1];

                var status = await GetStatusAsync(manufacturer, serialNumber);
                var location = await GetLocationAsync(manufacturer, serialNumber);

                if (status != null && location != null)
                {
                    result.Add((manufacturer, serialNumber, status, location));
                }
            }

            return result;
        }

        /// <summary>
        /// 获取所有启用机器人的完整缓存数据
        /// @author zzy
        /// </summary>
        public async Task<IEnumerable<(RobotBasicCache Basic, RobotStatusCache? Status, RobotLocationCache? Location)>> GetAllActiveRobotCacheAsync()
        {
            var db = _redis.GetDatabase();
            var members = await db.SetMembersAsync(RobotsSetKey);
            var result = new List<(RobotBasicCache, RobotStatusCache?, RobotLocationCache?)>();

            foreach (var member in members)
            {
                var parts = member.ToString().Split(':');
                if (parts.Length != 2) continue;

                var basic = await GetBasicAsync(parts[0], parts[1]);
                if (basic == null || !basic.Active) continue;

                var status = await GetStatusAsync(parts[0], parts[1]);
                var location = await GetLocationAsync(parts[0], parts[1]);
                result.Add((basic, status, location));
            }

            return result;
        }
    }
}