Session 记录
2026-01-23 15:42:30 - 修复地图资源同步中的库区和货位重复插入问题
问题描述
用户反馈在 SyncMapPointsCommandHandler.cs 中循环 locations 时,存在库区重复插入和货位重复插入的问题。
问题分析
原始代码问题点
-
库区重复插入问题(第 107-131 行):
- 原代码:
var currentArea = existingNode?.StorageAreas.FirstOrDefault(a => a.AreaCode == location.area); - 问题:只从 existingNode 中查找库区,当 existingNode 为 null 时,currentArea 也为 null
- 根本原因:多个 location 可能共享相同的 area 代码,在循环中遍历到第二个相同 area 的 location 时,由于没有从数据库或已处理记录中查询,会导致重复插入同一个库区
- 原代码:
-
货位重复插入问题(第 132-160 行):
- 原代码:
var currLocation = currentArea?.StorageLocations.FirstOrDefault(a => a.LocationCode == location.id); - 问题:只从 currentArea 中查找货位,当 currentArea 为 null 时,会重复插入
- 根本原因:缺少对已处理货位的跟踪机制
- 原代码:
解决方案
核心思路
- 在循环外部,预先查询所有已存在的库区和货位
- 在循环内部,使用字典和 HashSet 跟踪本次循环中已处理的库区和货位
- 处理每个 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 行
修改内容:
- 新增预查询逻辑(第 69-70 行)
- 新增跟踪集合(第 72-74 行)
- 重构库区处理逻辑(第 109-147 行)
- 重构货位处理逻辑(第 149-179 行)
- 性能优化:使用
TryGetValue替代ContainsKey+ 索引器访问(第 112 行)
修改人: @author zzy
修改时间: 2026-01-23 15:42:30
技术要点
-
去重策略:
- 库区去重:基于 AreaCode
- 货位去重:基于 LocationCode
-
性能优化:
- 使用
TryGetValue避免字典双重查找 - 预查询减少循环内的数据库查询次数
- 使用
-
数据一致性:
- 确保同一个库区在一次同步中只插入一次
- 确保同一个货位在一次同步中只插入一次
- 保持库区与货位的关联关系正确
测试建议
-
场景一:多个货位共享同一库区
- 准备测试数据:3 个 location,area 都为 "A01"
- 预期结果:只插入 1 个库区,3 个货位
-
场景二:重复的货位代码
- 准备测试数据:2 个 location,id 都为 "L001"
- 预期结果:只插入 1 个货位
-
场景三:更新已存在的库区和货位
- 准备测试数据:数据库中已存在库区 "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
原有逻辑:
-
PushRobotStatusAsync: 按地图分组后,向map_{mapId}组推送该地图上所有机器人的状态 -
PushStorageLocationsAsync: 按地图分组后,向map_{mapId}组推送该地图所有库位的状态
问题: 客户端订阅地图后,会收到该地图的所有内容,无法按需订阅单个机器人或单个库位。
解决方案
核心思路
- 在 Hub 中添加更细粒度的订阅方法(订阅单个机器人、订阅单个库位)
- 推送服务同时支持两种推送模式:
- 单个资源推送:只向订阅了该资源的客户端推送
- 地图级别推送:向订阅了整个地图的客户端推送所有内容
具体实现
第一步:扩展 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
修改后的推送策略:
-
机器人状态推送 (
PushRobotStatusAsync):-
单个机器人推送: 遍历每个机器人,向
robot_{robotId}组推送(订阅了该机器人的客户端) -
地图级别推送: 按地图分组后,向
map_{mapId}组推送(订阅了该地图所有机器人的客户端)
-
单个机器人推送: 遍历每个机器人,向
-
库位状态推送 (
PushStorageLocationsAsync):-
单个库位推送: 遍历每个库位,向
location_{locationId}组推送(订阅了该库位的客户端) -
地图级别推送: 按地图分组后,向
map_{mapId}组推送(订阅了该地图所有库位的客户端)
-
单个库位推送: 遍历每个库位,向
代码修改详情
1. HHRCSHub.cs 修改
修改位置: 第 50-82 行
修改内容:
- 新增
SubscribeRobot和UnsubscribeRobot方法 - 新增
SubscribeStorageLocation和UnsubscribeStorageLocation方法 - 所有方法都添加了完整的 XML 注释
2. RobotStatusPushService.cs 修改
修改位置: 第 57-189 行
修改内容:
-
PushRobotStatusAsync 方法 (第 64-131 行):
- 方式1:单个机器人精准推送(第 78-102 行)
- 方式2:地图级别推送(第 105-130 行)
-
PushStorageLocationsAsync 方法 (第 141-188 行):
- 方式1:单个库位精准推送(第 158-174 行)
- 方式2:地图级别推送(第 177-187 行)
修正属性名错误:
StorageLocationId→LocationId(第 171 行)
修改人: @author zzy
修改时间: 2026-01-26 15:30:00
技术要点
-
SignalR 组命名规则:
- 机器人组:
robot_{robotId} - 库位组:
location_{locationId} - 地图组:
map_{mapId}
- 机器人组:
-
推送频率控制:
- 机器人状态: 每 200ms 推送一次
- 库位状态: 每 2 秒推送一次(降频推送)
-
数据来源:
- 机器人状态: 从 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