# 多出库口轮询功能设计文档 **日期**: 2026-03-09 **作者**: Claude Code **状态**: 待实施 ## 概述 本设计旨在改进自动出库任务的目标地址配置功能,支持一个巷道配置多个出库口,并通过轮询算法实现负载均衡。 ## 需求背景 当前系统中,`TargetAddresses` 配置采用一对一映射(巷道前缀 → 出库口地址)。但实际业务中,一个巷道可能有多个出库口,需要支持: 1. **一对多关系**:一个巷道可以配置多个出库口 2. **负载均衡**:通过轮询算法选择出库口,避免单点压力 3. **向后兼容**:保持对单出口配置的支持 ## 技术方案 ### 架构选择 采用 **内存轮询计数器 + 独立服务类** 的模式: 1. **RoundRobinService**:独立的轮询服务类,管理轮询计数器 2. **配置模型变更**:`Dictionary` → `Dictionary>` 3. **线程安全**:使用 `ConcurrentDictionary` 保证多线程安全 ### 核心设计 #### 1. 配置格式变更 **appsettings.json:** ```json { "AutoOutboundTask": { "TargetAddresses": { "GW": ["10081", "10082", "10083"], "CW": ["10080"] } } } ``` **说明:** - 值类型从 `string` 改为 `string[]`(JSON 数组) - 支持一个巷道配置多个出库口 - 单个出库口也可用数组格式(只有一个元素) #### 2. 配置模型类 **AutoOutboundTaskOptions.cs:** ```csharp public class AutoOutboundTaskOptions { public bool Enable { get; set; } = true; public int CheckIntervalSeconds { get; set; } = 300; public Dictionary> TargetAddresses { get; set; } = new() { { "GW", new List { "10081" } }, { "CW", new List { "10080" } } }; } ``` #### 3. 轮询服务类 **RoundRobinService.cs** (新建): ```csharp using System.Collections.Concurrent; namespace WIDESEA_Core.Core { /// /// 轮询服务 - 线程安全的地址轮询选择 /// public class RoundRobinService { private readonly ConcurrentDictionary _counters = new(); /// /// 获取下一个地址(轮询) /// /// 巷道前缀 /// 地址列表 /// 选中的地址 public string GetNextAddress(string key, List addresses) { if (addresses == null || addresses.Count == 0) return "10080"; if (addresses.Count == 1) return addresses[0]; // AddOrUpdate 是原子操作,线程安全 int index = _counters.AddOrUpdate( key, 0, // 首次使用,从 0 开始 (k, old) => (old + 1) % addresses.Count // 轮询:递增后取模 ); return addresses[index]; } } } ``` **关键特性:** - 使用 `ConcurrentDictionary` 存储每个巷道的当前索引 - `AddOrUpdate` 方法保证原子性,无需额外锁 - 模运算实现循环轮询 - 线程安全,支持后台服务的并发调用 #### 4. TaskService 方法修改 **DetermineTargetAddress 方法:** ```csharp private string DetermineTargetAddress( string roadway, Dictionary> addressMap) { if (string.IsNullOrWhiteSpace(roadway)) return "10080"; // 查找匹配的巷道前缀 string matchedPrefix = null; foreach (var kvp in addressMap) { if (roadway.Contains(kvp.Key)) { matchedPrefix = kvp.Key; break; } } if (matchedPrefix == null) return "10080"; var addresses = addressMap[matchedPrefix]; if (addresses == null || addresses.Count == 0) return "10080"; // 单个地址,直接返回 if (addresses.Count == 1) return addresses[0]; // 多个地址,使用轮询 return _roundRobinService.GetNextAddress(matchedPrefix, addresses); } ``` **依赖注入:** ```csharp public class TaskService { private readonly RoundRobinService _roundRobinService; public TaskService( // ... 其他依赖 RoundRobinService roundRobinService) { // ... 其他赋值 _roundRobinService = roundRobinService; } } ``` #### 5. 服务注册 **Program.cs:** ```csharp // 注册为单例,保证全局共享计数器 builder.Services.AddSingleton(); ``` ## 数据流程 ```mermaid graph TD A[创建出库任务] --> B[获取库存巷道信息] B --> C[查找匹配的巷道前缀] C --> D{地址列表数量} D -->|0个| E[返回默认地址 10080] D -->|1个| F[直接返回该地址] D -->|多个| G[调用 RoundRobinService] G --> H[查询/更新计数器] H --> I[返回 addresses counter % count] I --> J[创建任务] F --> J E --> J ``` ## 轮询示例 **配置:** ```json { "TargetAddresses": { "GW": ["10081", "10082", "10083"] } } ``` **调用序列:** | 次数 | 计数器值 | 返回地址 | |------|---------|---------| | 1 | 0 | 10081 | | 2 | 1 | 10082 | | 3 | 2 | 10083 | | 4 | 0 | 10081 | | 5 | 1 | 10082 | | ... | ... | ... | ## 错误处理 1. **配置错误**: - 地址列表为空 → 返回默认地址 "10080" - 巷道不匹配任何前缀 → 返回默认地址 "10080" 2. **并发安全**: - 使用 `ConcurrentDictionary` 保证线程安全 - `AddOrUpdate` 是原子操作 3. **应用重启**: - 轮询位置重置为 0 - 可接受的权衡(简化实现) ## 向后兼容性 ### 旧配置格式 ```json { "TargetAddresses": { "GW": "10081", "CW": "10080" } } ``` ### 新配置格式 ```json { "TargetAddresses": { "GW": ["10081"], "CW": ["10080"] } } ``` **兼容性处理:** - JSON 配置绑定会自动处理两种格式 - 单个字符串会被解析为单元素数组 - 现有配置无需修改即可工作 ## 性能影响 1. **内存开销**:每个巷道前缀一个整数计数器(约 50 字节) 2. **CPU 开销**:模运算和字典查找,O(1) 复杂度 3. **线程安全**:无锁设计,`ConcurrentDictionary` 使用优化的同步机制 ## 测试计划 ### 单元测试 1. `RoundRobinService.GetNextAddress` 轮询逻辑 2. 单个地址直接返回 3. 多个地址轮询返回 4. 并发调用测试 ### 集成测试 1. 修改 appsettings.json 为多出口配置 2. 创建多个出库任务 3. 验证目标地址轮询分配 4. 验证日志记录 ### 手动测试 ```sql -- 准备测试数据 UPDATE Dt_StockInfo SET OutboundDate = DATEADD(MINUTE, -5, GETDATE()) WHERE PalletCode IN ('TEST001', 'TEST002', 'TEST003') AND StockStatus = 1; ``` 启动应用,观察日志: ``` info: 创建任务 TEST001,目标地址: 10081 info: 创建任务 TEST002,目标地址: 10082 info: 创建任务 TEST003,目标地址: 10083 ``` ## 部署注意事项 1. **配置更新**: - 将 `TargetAddresses` 的值从字符串改为数组格式 - 确保每个数组至少有一个元素 2. **依赖注入**: - 确保 `RoundRobinService` 已注册为单例 - `TaskService` 构造函数需要注入该服务 3. **回滚方案**: - 保留 `appsettings.json` 备份 - 如遇问题可恢复为单出口配置 ## 未来改进 1. **权重轮询**:支持为每个地址配置权重 2. **健康检查**:排除不可用的出库口 3. **持久化**:将轮询位置保存到 Redis/数据库 4. **监控统计**:记录每个地址的任务分配数量 ## 实施清单 - [x] 修改 `AutoOutboundTaskOptions.TargetAddresses` 类型 - [x] 创建 `RoundRobinService` 类 - [x] 修改 `TaskService.DetermineTargetAddress` 方法 - [x] 在 `Program.cs` 注册 `RoundRobinService` - [x] 更新 `appsettings.json` 配置示例 - [x] 编译验证 - [ ] 手动测试(需要运行环境和数据库) - [x] 更新设计文档 **实施日期**: 2026-03-09 **实施人**: Claude Code **状态**: 代码实现完成,待手动测试