session.md 9.22 KB

Session 记录

2026-01-23 15:42:30 - 修复地图资源同步中的库区和货位重复插入问题

问题描述

用户反馈在 SyncMapPointsCommandHandler.cs 中循环 locations 时,存在库区重复插入和货位重复插入的问题。

问题分析

原始代码问题点

  1. 库区重复插入问题(第 107-131 行):

    • 原代码:var currentArea = existingNode?.StorageAreas.FirstOrDefault(a => a.AreaCode == location.area);
    • 问题:只从 existingNode 中查找库区,当 existingNode 为 null 时,currentArea 也为 null
    • 根本原因:多个 location 可能共享相同的 area 代码,在循环中遍历到第二个相同 area 的 location 时,由于没有从数据库或已处理记录中查询,会导致重复插入同一个库区
  2. 货位重复插入问题(第 132-160 行):

    • 原代码:var currLocation = currentArea?.StorageLocations.FirstOrDefault(a => a.LocationCode == location.id);
    • 问题:只从 currentArea 中查找货位,当 currentArea 为 null 时,会重复插入
    • 根本原因:缺少对已处理货位的跟踪机制

解决方案

核心思路

  1. 在循环外部,预先查询所有已存在的库区和货位
  2. 在循环内部,使用字典和 HashSet 跟踪本次循环中已处理的库区和货位
  3. 处理每个 location 时,先检查是否已在本次循环中处理过,避免重复插入

具体实现

第一步:预查询已存在数据

// 获取所有已存在的库区和货位,用于去重判断
var existingAreas = existingNodes.SelectMany(n => n.StorageAreas).ToList();
var existingLocations = existingAreas.SelectMany(a => a.StorageLocations).ToList();

第二步:创建跟踪集合

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

第三步:优化库区处理逻辑

  • 使用 TryGetValue 检查是否已在本次循环中处理过该库区
  • 如果未处理过,从 existingAreas 中查找(而不是从 existingNode)
  • 处理完成后,记录到 processedAreas 字典中

第四步:优化货位处理逻辑

  • 使用 processedLocations.Contains() 检查是否已处理过
  • 从 existingLocations 中查找(而不是从 currentArea)
  • 处理完成后,记录到 processedLocations 集合中

代码修改详情

文件路径: src/Rcs.Infrastructure/MessageBus/Handlers/Commands/Map/SyncMapPointsCommandHandler.cs

修改位置: 第 66-161 行

修改内容:

  1. 新增预查询逻辑(第 69-70 行)
  2. 新增跟踪集合(第 72-74 行)
  3. 重构库区处理逻辑(第 109-147 行)
  4. 重构货位处理逻辑(第 149-179 行)
  5. 性能优化:使用 TryGetValue 替代 ContainsKey + 索引器访问(第 112 行)

修改人: @author zzy

修改时间: 2026-01-23 15:42:30

技术要点

  1. 去重策略

    • 库区去重:基于 AreaCode
    • 货位去重:基于 LocationCode
  2. 性能优化

    • 使用 TryGetValue 避免字典双重查找
    • 预查询减少循环内的数据库查询次数
  3. 数据一致性

    • 确保同一个库区在一次同步中只插入一次
    • 确保同一个货位在一次同步中只插入一次
    • 保持库区与货位的关联关系正确

测试建议

  1. 场景一:多个货位共享同一库区

    • 准备测试数据:3 个 location,area 都为 "A01"
    • 预期结果:只插入 1 个库区,3 个货位
  2. 场景二:重复的货位代码

    • 准备测试数据:2 个 location,id 都为 "L001"
    • 预期结果:只插入 1 个货位
  3. 场景三:更新已存在的库区和货位

    • 准备测试数据:数据库中已存在库区 "A01" 和货位 "L001"
    • 预期结果:更新而不是插入新记录

相关文件

  • 主文件:SyncMapPointsCommandHandler.cs
  • 涉及实体:MapNode、StorageArea、StorageLocation
  • 涉及仓储:IMapNodeRepository、IStorageAreaRepository、IStorageLocationRepository

备注

  • 本次修改遵循 DDD 架构模式
  • 代码符合 .NET 8.0 规范
  • 已通过 IDE 静态代码分析
  • 环境:Windows 10 系统

2026-01-26 15:30:00 - 优化 SignalR 推送服务实现精准订阅

问题描述

用户反馈当前 SignalR 推送服务的逻辑是:客户端订阅地图后,就收到该地图所有机器人和库位的状态。需要实现更精细的订阅控制:客户端订阅什么就只推送什么

原有实现分析

文件路径: RobotStatusPushService.cs

原有逻辑:

  1. PushRobotStatusAsync: 按地图分组后,向 map_{mapId} 组推送该地图上所有机器人的状态
  2. PushStorageLocationsAsync: 按地图分组后,向 map_{mapId} 组推送该地图所有库位的状态

问题: 客户端订阅地图后,会收到该地图的所有内容,无法按需订阅单个机器人或单个库位。

解决方案

核心思路

  1. 在 Hub 中添加更细粒度的订阅方法(订阅单个机器人、订阅单个库位)
  2. 推送服务同时支持两种推送模式:
    • 单个资源推送:只向订阅了该资源的客户端推送
    • 地图级别推送:向订阅了整个地图的客户端推送所有内容

具体实现

第一步:扩展 Hub 订阅方法

文件路径: HHRCSHub.cs

新增方法:

// 订阅指定机器人的状态(订阅后只接收该机器人的状态)
public async Task SubscribeRobot(string robotId)

// 取消订阅指定机器人
public async Task UnsubscribeRobot(string robotId)

// 订阅指定库位的状态(订阅后只接收该库位的状态)
public async Task SubscribeStorageLocation(string locationId)

// 取消订阅指定库位
public async Task UnsubscribeStorageLocation(string locationId)

第二步:优化推送服务逻辑

文件路径: RobotStatusPushService.cs

修改后的推送策略:

  1. 机器人状态推送 (PushRobotStatusAsync):

    • 单个机器人推送: 遍历每个机器人,向 robot_{robotId} 组推送(订阅了该机器人的客户端)
    • 地图级别推送: 按地图分组后,向 map_{mapId} 组推送(订阅了该地图所有机器人的客户端)
  2. 库位状态推送 (PushStorageLocationsAsync):

    • 单个库位推送: 遍历每个库位,向 location_{locationId} 组推送(订阅了该库位的客户端)
    • 地图级别推送: 按地图分组后,向 map_{mapId} 组推送(订阅了该地图所有库位的客户端)

代码修改详情

1. HHRCSHub.cs 修改

修改位置: 第 50-82 行

修改内容:

  • 新增 SubscribeRobotUnsubscribeRobot 方法
  • 新增 SubscribeStorageLocationUnsubscribeStorageLocation 方法
  • 所有方法都添加了完整的 XML 注释

2. RobotStatusPushService.cs 修改

修改位置: 第 57-189 行

修改内容:

  1. PushRobotStatusAsync 方法 (第 64-131 行):

    • 方式1:单个机器人精准推送(第 78-102 行)
    • 方式2:地图级别推送(第 105-130 行)
  2. PushStorageLocationsAsync 方法 (第 141-188 行):

    • 方式1:单个库位精准推送(第 158-174 行)
    • 方式2:地图级别推送(第 177-187 行)
  3. 修正属性名错误: StorageLocationIdLocationId (第 171 行)

修改人: @author zzy

修改时间: 2026-01-26 15:30:00

技术要点

  1. SignalR 组命名规则:

    • 机器人组: robot_{robotId}
    • 库位组: location_{locationId}
    • 地图组: map_{mapId}
  2. 推送频率控制:

    • 机器人状态: 每 200ms 推送一次
    • 库位状态: 每 2 秒推送一次(降频推送)
  3. 数据来源:

    • 机器人状态: 从 Redis 缓存读取(纯缓存,不查数据库)
    • 库位状态: 从 PostgreSQL 数据库查询

客户端使用示例

// 连接 Hub
const connection = new signalR.HubConnectionBuilder()
    .withUrl("/hHub")
    .build();

// 订阅单个机器人
await connection.invoke("SubscribeRobot", "robot-id-123");

// 订阅单个库位
await connection.invoke("SubscribeStorageLocation", "location-id-456");

// 订阅整个地图(接收该地图所有内容)
await connection.invoke("SubscribeMap", "map-id-789");

// 接收推送数据
connection.on("RobotStatusUpdate", (data) => {
    console.log("机器人状态更新:", data);
});

connection.on("StorageLocationsUpdate", (data) => {
    console.log("库位状态更新:", data);
});

相关文件

  • 主文件 1: HHRCSHub.cs
  • 主文件 2: RobotStatusPushService.cs
  • 涉及实体: RobotRealtimeStatusDto、StorageLocation
  • 涉及服务: IRobotCacheService、IMapRepository、IStorageAreaRepository

备注

  • 本次修改遵循 DDD 架构模式
  • 代码符合 .NET 8.0 规范
  • 已通过 IDE 静态代码分析
  • 环境:Windows 10 系统
  • 模型版本:glm-4.7