SyncMapPointsCommandHandler.cs 13.6 KB
using MassTransit;
using MassTransit.Internals;
using MassTransit.Mediator;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Rcs.Application.Common;
using Rcs.Application.MessageBus.Commands;
using Rcs.Application.Shared;
using Rcs.Cyaninetech.Services;
using Rcs.Domain.Entities;
using Rcs.Domain.Extensions;
using Rcs.Domain.Repositories;
using Rcs.Domain.Settings;

namespace Rcs.Infrastructure.MessageBus.Handlers.Commands;

/// <summary>
/// 同步地图资源命令处理器
/// @author zzy
/// </summary>
public class SyncMapPointsCommandHandler : IConsumer<SyncMapPointsCommand>
{
    private readonly ILogger<SyncMapPointsCommandHandler> _logger;
    private readonly IMapRepository _mapRepository;
    private readonly IMapNodeRepository _mapNodeRepository;
    private readonly IStorageAreaRepository _storageAreaRepository;
    private readonly IStorageLocationRepository _storageLocationRepository;
    private readonly IStorageLocationTypeRepository _locationTypeRepository;
    private readonly ILanYinService _lanYinService;

    public SyncMapPointsCommandHandler(
        ILogger<SyncMapPointsCommandHandler> logger,
        IMapRepository mapRepository,
        IMapNodeRepository mapNodeRepository,
        IStorageAreaRepository storageAreaRepository,
        IStorageLocationRepository storageLocationRepository,
        IStorageLocationTypeRepository locationTypeRepository,
        ILanYinService lanYinService)
    {
        _logger = logger;
        _mapRepository = mapRepository;
        _mapNodeRepository = mapNodeRepository;
        _storageAreaRepository = storageAreaRepository;
        _storageLocationRepository = storageLocationRepository;
        _locationTypeRepository = locationTypeRepository;
        _lanYinService = lanYinService;
    }

    public async Task Consume(ConsumeContext<SyncMapPointsCommand> context)
    {
        var command = context.Message;

        try
        {
            var map = await _mapRepository.GetByIdAsync(command.MapId, context.CancellationToken);
            if (map == null)
            {
                await context.RespondAsync(ApiResponse.Failed($"未找到ID为 {command.MapId} 的地图"));
                return;
            }

            if (string.IsNullOrWhiteSpace(map.PointsUrl))
                throw new BusinessException("请先维护节点资源URL");

            // 获取地图资源信息
            var locations = await _lanYinService.GetLocationsAsync(map.PointsUrl);
            var remoteNodeCodes = locations.Select(l => l.id).ToHashSet();
            var remoteAreaCodes = locations.Select(l => l.area).Distinct().ToHashSet();
            var remoteLocationCodes = locations.Select(l => l.id).ToHashSet();

            // 获取当前地图所有节点,用于后续删除判断
            var existingNodes = await _mapNodeRepository.GetByMapIdFullAsync(command.MapId, context.CancellationToken);

            // 获取所有已存在的库区和货位,用于去重判断
            
            var existingLocations = existingNodes.SelectMany(a => a.StorageLocations).ToList();
            var existingAreas = existingLocations.Select(l => l.StorageArea).ToList();
            // 获取默认库位类型
            var defaultLocationType = await _locationTypeRepository.GetDefaultAsync();

            // 用于跟踪本次循环中已处理的库区和货位,避免重复插入
            var processedAreas = new Dictionary<string, Guid>(); // Key: AreaCode, Value: AreaId
            var processedLocations = new HashSet<string>(); // LocationCode

            // 循环处理locations,根据对应关系同步MapNode、StorageArea、StorageLocation
            // 导航关系:MapNode (1) ←→ (N) StorageLocation (N) ←→ (1) StorageArea
            // 对应关系:
            // 1. location.id ↔ MapNode.NodeCode (一一对应)
            // 2. location.id ↔ StorageLocation.LocationCode (一一对应)
            // 3. location.id ↔ StorageLocation.MapNodeId (一一对应,建立MapNode与StorageLocation的关联)
            // 4. location.area去重 ↔ StorageArea.AreaCode (一一对应)
            foreach (var location in locations)
            {
                var useStatus = ParseUseStatus(location.use_status);

                // ============ 1. 处理 MapNode(根据 NodeCode = location.id 判断是否存在)============
                var existingNode = existingNodes.FirstOrDefault( en => en.NodeCode == location.id);
                Guid nodeId;

                if (existingNode != null)
                {
                    // MapNode已存在,更新
                    nodeId = existingNode.NodeId;
                    existingNode.NodeName = location.alias;
                    existingNode.X = (location.position?.x != null ? (double)location.position.x : 0) * 1000;
                    existingNode.Y = (location.position?.y != null ? (double)location.position.y : 0) * 1000;
                    existingNode.StorageLocationTypeId = defaultLocationType?.TypeId;

                    await _mapNodeRepository.UpdateAsync(existingNode, context.CancellationToken);
                }
                else
                {
                    // MapNode不存在,新增
                    nodeId = Guid.NewGuid();
                    var newNode = new MapNode
                    {
                        NodeId = nodeId,
                        MapId = command.MapId,
                        NodeCode = location.id,
                        NodeName = location.alias,
                        X = (location.position?.x != null ? (double)location.position.x : 0) * 1000,
                        Y = (location.position?.y != null ? (double)location.position.y : 0) * 1000,
                        Type = MapNodeTYPE.store,
                        StorageLocationTypeId = defaultLocationType?.TypeId,
                        Active = true,
                        CreatedAt = DateTime.Now
                    };
                    await _mapNodeRepository.AddAsync(newNode, context.CancellationToken);
                }

                // ============ 2. 处理 StorageArea(根据 AreaCode = location.area 判断是否存在)============
                // 注意:StorageArea 作为逻辑分组,一个 area 对应多个 locations(多个 StorageLocation)
                Guid areaId;
                if (processedAreas.TryGetValue(location.area, out var existingAreaId))
                {
                    // 本批次已处理过该库区,直接使用
                    areaId = existingAreaId;
                }
                else
                {
                    // 从数据库查询是否已存在该库区(AreaCode 有全局唯一约束)
                    var currentArea = await _storageAreaRepository.GetByAreaCodeAsync(location.area, context.CancellationToken);
                    if (currentArea != null)
                    {
                        // StorageArea已存在,更新
                        areaId = currentArea.AreaId;
                        currentArea.AreaCode = location.area;
                        currentArea.AreaName = location.area;
                        currentArea.Description = $"自动同步库区(关联{locations.Count(l => l.area == location.area)}个点位)";

                        await _storageAreaRepository.UpdateAsync(currentArea, context.CancellationToken);
                    }
                    else
                    {
                        // StorageArea不存在,新增
                        areaId = Guid.NewGuid();
                        var newArea = new StorageArea()
                        {
                            AreaId = areaId,
                            AreaCode = location.area,
                            AreaName = location.area,
                            Description = $"自动同步库区(关联{locations.Count(l => l.area == location.area)}个点位)",
                            CreatedAt = DateTime.Now
                        };
                        await _storageAreaRepository.AddAsync(newArea, context.CancellationToken);
                    }

                    // 记录已处理的库区
                    processedAreas[location.area] = areaId;
                }

                // ============ 3. 处理 StorageLocation(根据 LocationCode = location.id 判断是否存在)============
                if (!processedLocations.Contains(location.id))
                {
                    // 从数据库查询是否已存在该货位
                    var currLocation = await _storageLocationRepository.GetByLocationCodeAsync(location.id, context.CancellationToken);
                    Guid locationId;

                    if (currLocation != null)
                    {
                        // StorageLocation已存在,更新
                        locationId = currLocation.LocationId;
                        currLocation.AreaId = areaId;
                        currLocation.MapNodeId = nodeId; // 建立与MapNode的关联
                        currLocation.LocationCode = location.id;
                        currLocation.LocationName = location.alias;
                        currLocation.Status = useStatus;
                        currLocation.IsActive = true;
                        currLocation.UpdatedAt = DateTime.Now;

                        await _storageLocationRepository.UpdateAsync(currLocation, context.CancellationToken);
                    }
                    else
                    {
                        // StorageLocation不存在,新增
                        locationId = Guid.NewGuid();
                        var newLocation = new StorageLocation()
                        {
                            LocationId = locationId,
                            AreaId = areaId,
                            MapNodeId = nodeId, // 建立与MapNode的关联
                            LocationCode = location.id,
                            LocationName = location.alias,
                            Status = useStatus,
                            IsActive = true,
                            CreatedAt = DateTime.Now
                        };
                        await _storageLocationRepository.AddAsync(newLocation, context.CancellationToken);
                    }

                    // 记录已处理的货位
                    processedLocations.Add(location.id);
                }
            }

            // ============ 删除远程不存在的数据 ============
            // 删除顺序:先删除子表(StorageLocation),再删除父表(StorageArea、MapNode),避免级联删除问题

            // 1. 删除远程不存在的货位(StorageLocation)
            foreach (var location in existingLocations)
            {
                if (!remoteLocationCodes.Contains(location.LocationCode))
                {
                    _logger.LogInformation("删除远程不存在的货位: LocationCode={LocationCode}, LocationName={LocationName}", location.LocationCode, location.LocationName);
                    await _storageLocationRepository.DeleteAsync(location, context.CancellationToken);
                }
            }

            // 2. 删除远程不存在的库区(StorageArea)
            // 注意:需要重新获取,因为上面删除StorageLocation后,existingAreas可能已过时
            var remainingAreas = await _storageAreaRepository.GetByMapIdAsync(command.MapId, context.CancellationToken);
            foreach (var area in remainingAreas)
            {
                if (!remoteAreaCodes.Contains(area.AreaCode))
                {
                    _logger.LogInformation("删除远程不存在的库区: AreaCode={AreaCode}, AreaName={AreaName}", area.AreaCode, area.AreaName);
                    await _storageAreaRepository.DeleteAsync(area, context.CancellationToken);
                }
            }

            // 3. 删除远程不存在的节点(MapNode)
            foreach (var node in existingNodes)
            {
                if (!remoteNodeCodes.Contains(node.NodeCode))
                {
                    _logger.LogInformation("删除远程不存在的节点: NodeCode={NodeCode}, NodeName={NodeName}", node.NodeCode, node.NodeName);
                    await _mapNodeRepository.DeleteAsync(node, context.CancellationToken);
                }
            }

            if (locations.Count > 0)
            {
                map.MapCode = locations[0].scene_id.ToString();
                await _mapRepository.UpdateAsync(map, context.CancellationToken);
            }

            
            await context.RespondAsync(ApiResponse.Successful("同步地图资源成功"));
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "同步地图资源失败: MapId={MapId}", command.MapId);
            await context.RespondAsync(ApiResponse.Failed($"同步失败: {ex.Message}"));
        }
    }

    /// <summary>
    /// 解析使用状态字符串为枚举
    /// @author zzy
    /// </summary>
    private static StorageLocationStatus ParseUseStatus(string? status)
    {
        return status switch
        {
            "use" => StorageLocationStatus.Occupied,
            "free" => StorageLocationStatus.Empty,
            "pre_use" => StorageLocationStatus.Occupied,
            _ => StorageLocationStatus.Empty
        };
    }

    /// <summary>
    /// 将节点使用状态转换为库位状态
    /// @author zzy
    /// </summary>
    private static StorageLocationStatus ParseStorageLocationStatus(MapNodeUseStatus? useStatus)
    {
        return useStatus switch
        {
            MapNodeUseStatus.use => StorageLocationStatus.Occupied,
            MapNodeUseStatus.pre_use => StorageLocationStatus.Reserved,
            _ => StorageLocationStatus.Empty
        };
    }
}