MapCacheService.cs 9.64 KB
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Rcs.Application.Services;
using Rcs.Domain.Repositories;
using Rcs.Domain.Settings;
using StackExchange.Redis;

namespace Rcs.Infrastructure.Services
{
    /// <summary>
    /// 地图缓存服务实现 - 负责地图数据的Redis缓存操作
    /// @author zzy
    /// </summary>
    public class MapCacheService : IMapCacheService
    {
        private readonly IConnectionMultiplexer _redis;
        private readonly IServiceProvider _serviceProvider;
        private readonly ILogger<MapCacheService> _logger;
        private readonly AppSettings _settings;

        public MapCacheService(
            IConnectionMultiplexer redis,
            IServiceProvider serviceProvider,
            ILogger<MapCacheService> logger,
            IOptions<AppSettings> settings)
        {
            _redis = redis;
            _serviceProvider = serviceProvider;
            _logger = logger;
            _settings = settings.Value;
        }

        /// <summary>
        /// 从Redis获取地图完整数据
        /// @author zzy 99
        /// </summary>
        public async Task<MapCacheData?> GetMapAsync(Guid mapId)
        {
            var db = _redis.GetDatabase();
            var key = $"{_settings.Redis.KeyPrefixes.Map}:{mapId}";
            var json = await db.StringGetAsync(key);

            if (json.IsNullOrEmpty)
            {
                // 缓存未命中,从数据库加载
                await UpdateMapCacheAsync(mapId);
                json = await db.StringGetAsync(key);
                if (json.IsNullOrEmpty) return null;
            }

            return JsonSerializer.Deserialize<MapCacheData>(json!);
        }

        /// <summary>
        /// 获取所有地图ID列表
        /// @author zzy
        /// </summary>
        public async Task<IEnumerable<Guid>> GetAllMapIdsAsync()
        {
            var db = _redis.GetDatabase();
            var json = await db.StringGetAsync(_settings.Redis.KeyPrefixes.MapList);

            if (json.IsNullOrEmpty) return Enumerable.Empty<Guid>();

            var ids = JsonSerializer.Deserialize<List<string>>(json!);
            return ids?.Select(Guid.Parse) ?? Enumerable.Empty<Guid>();
        }

        /// <summary>
        /// 获取所有地图数据
        /// @author zzy
        /// </summary>
        public async Task<IEnumerable<MapCacheData>> GetAllMapsAsync()
        {
            var mapIds = await GetAllMapIdsAsync();
            var maps = new List<MapCacheData>();

            foreach (var mapId in mapIds)
            {
                var map = await GetMapAsync(mapId);
                if (map != null)
                {
                    maps.Add(map);
                }
            }

            return maps;
        }

        /// <summary>
        /// 按MapCode分组获取地图(同一物理环境的不同厂商地图)
        /// @author zzy
        /// </summary>
        public async Task<Dictionary<string, List<MapCacheData>>> GetMapsGroupedByCodeAsync()
        {
            var allMaps = await GetAllMapsAsync();

            return allMaps
                .Where(m => !string.IsNullOrEmpty(m.MapCode))
                .GroupBy(m => m.MapCode)
                .ToDictionary(
                    g => g.Key,
                    g => g.ToList()
                );
        }

        /// <summary>
        /// 通过节点编码快速查找节点ID
        /// @author zzy
        /// </summary>
        public async Task<Guid?> GetNodeIdByCodeAsync(Guid mapId, string nodeCode)
        {
            var db = _redis.GetDatabase();
            var key = $"{_settings.Redis.KeyPrefixes.Map}:{mapId}{_settings.Redis.KeyPrefixes.MapNodeIndexSuffix}:";
            var nodeIdStr = await db.HashGetAsync(key, nodeCode);

            if (nodeIdStr.IsNullOrEmpty) return null;
            return Guid.TryParse(nodeIdStr, out var nodeId) ? nodeId : null;
        }

        /// <summary>
        /// 更新单个地图缓存
        /// @author zzy
        /// </summary>
        public async Task UpdateMapCacheAsync(Guid mapId)
        {
            using var scope = _serviceProvider.CreateScope();
            var mapRepo = scope.ServiceProvider.GetRequiredService<IMapRepository>();
            var db = _redis.GetDatabase();

            var map = await mapRepo.GetWithFullDetailsAsync(mapId);
            if (map == null) return;

            var key = $"{_settings.Redis.KeyPrefixes.Map}:{mapId}";
            var cacheData = new MapCacheData
            {
                MapId = map.MapId,
                MapCode = map.MapCode,
                MapName = map.MapName,
                MapType = (int)map.MapType,
                Version = map.Version,
                Active = map.Active,
                Nodes = map.MapNodes.Select(n => new MapNodeCache
                {
                    NodeId = n.NodeId,
                    NodeCode = n.NodeCode,
                    X = n.X,
                    Y = n.Y,
                    Theta = n.Theta,
                    Type = (int)n.Type,
                    Active = n.Active,
                    IsReverseParking = n.IsReverseParking,
                    AllowRotate = n.AllowRotate,
                    MaxCoordinateOffset = n.MaxCoordinateOffset
                }).ToList(),
                Edges = map.MapEdges.Select(e => new MapEdgeCache
                {
                    EdgeId = e.EdgeId,
                    EdgeCode = e.EdgeCode,
                    FromNode = e.FromNode,
                    ToNode = e.ToNode,
                    Length = e.Length,
                    Cost = e.Cost,
                    Active = e.Active,
                    OrientationRads = e.OrientationRads,
                    MaxSpeed = e.MaxSpeed,
                    MaxRadDeviation = e.MaxRadDeviation,
                    IsCurve = e.IsCurve,
                    ControlPoints = e.ControlPoints?
                        .Select(p => new PointCache { X = p.X, Y = p.Y })
                        .ToList()
                }).ToList(),
                Resources = map.MapResources.Select(r => new MapResourceCache
                {
                    ResourceId = r.ResourceId,
                    ResourceCode = r.ResourceCode,
                    Type = (int)r.Type,
                    Capacity = r.Capacity,
                    LocationCoordinates = r.LocationCoordinates?.Coordinates
                        .Select(c => new PointCache { X = c.X, Y = c.Y })
                        .ToList(),
                    MaxSpeed = r.MaxSpeed,
                    CanRotate = r.CanRotate,
                    PreAction1Type = r.PreAction1Type,
                    PreNetActions1 = r.PreNetActions1,
                    PostAction1Type = r.PostAction1Type,
                    PostNetActions1 = r.PostNetActions1,
                    PreAction2Type = r.PreAction2Type,
                    PreNetActions2 = r.PreNetActions2,
                    PostAction2Type = r.PostAction2Type,
                    PostNetActions2 = r.PostNetActions2
                }).ToList()
            };

            await db.StringSetAsync(key, JsonSerializer.Serialize(cacheData));

            // 更新节点索引
            var nodeIndexKey = $"{_settings.Redis.KeyPrefixes.Map}:{mapId}{_settings.Redis.KeyPrefixes.MapNodeIndexSuffix}:";
            var nodeEntries = map.MapNodes.Select(n => new HashEntry(n.NodeCode, n.NodeId.ToString())).ToArray();
            if (nodeEntries.Length > 0)
            {
                await db.KeyDeleteAsync(nodeIndexKey);
                await db.HashSetAsync(nodeIndexKey, nodeEntries);
            }

            _logger.LogInformation("[地图缓存] 已更新地图 {MapId} 缓存", mapId);
        }

        /// <summary>
        /// 删除地图缓存
        /// @author zzy
        /// </summary>
        public async Task RemoveMapCacheAsync(Guid mapId)
        {
            var db = _redis.GetDatabase();
            var key = $"{_settings.Redis.KeyPrefixes.Map}:{mapId}";
            var nodeIndexKey = $"{_settings.Redis.KeyPrefixes.Map}:{mapId}{_settings.Redis.KeyPrefixes.MapNodeIndexSuffix}:";

            await db.KeyDeleteAsync(key);
            await db.KeyDeleteAsync(nodeIndexKey);

            // 更新地图列表
            var mapIds = (await GetAllMapIdsAsync()).Where(id => id != mapId).Select(id => id.ToString()).ToList();
            await db.StringSetAsync(_settings.Redis.KeyPrefixes.MapList, JsonSerializer.Serialize(mapIds));

            _logger.LogInformation("[地图缓存] 已删除地图 {MapId} 缓存", mapId);
        }

        /// <summary>
        /// 根据坐标查询最近的MapNode,允许误差在EndDeviationPosition范围内
        /// @author zzy
        /// </summary>
        public async Task<MapNodeCache?> GetNearestNodeAsync(Guid mapId, double x, double y)
        {
            var mapData = await GetMapAsync(mapId);
            if (mapData == null || mapData.Nodes == null || mapData.Nodes.Count == 0)
            {
                return null;
            }

            var tolerance = Domain.ValueObjects.PositionConstants.EndDeviationPosition;
            MapNodeCache? nearestNode = null;
            double minDistance = double.MaxValue;

            foreach (var node in mapData.Nodes)
            {
                var distance = Math.Sqrt(Math.Pow(node.X - x, 2) + Math.Pow(node.Y - y, 2));
                if (distance <= tolerance && distance < minDistance)
                {
                    minDistance = distance;
                    nearestNode = node;
                }
            }

            return nearestNode;
        }
    }
}