# 多出库口轮询功能实现计划 > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **目标:** 改进自动出库任务功能,支持一个巷道配置多个出库口,通过轮询算法实现负载均衡 **架构:** 使用独立的 RoundRobinService 类管理轮询计数器,基于 ConcurrentDictionary 实现线程安全的轮询选择,配置从 Dictionary 改为 Dictionary> **技术栈:** .NET 6, ConcurrentDictionary, IOptions 模式, Autofac 依赖注入 --- ## 前置检查 在开始实现前,请确认: - 项目位于: `d:\Git\ShanMeiXinNengYuan\Code\WMS\WIDESEA_WMSServer` - 已阅读设计文档: `docs/plans/2026-03-09-multi-outbound-address-roundrobin-design.md` - 已完成自动出库任务的基础实现 (Tasks 1-7 已完成) --- ## Task 1: 创建 RoundRobinService 轮询服务类 **Files:** - Create: `WIDESEA_Core/Core/RoundRobinService.cs` **Step 1: 创建 RoundRobinService 类** 创建文件 `WIDESEA_Core/Core/RoundRobinService.cs`: ```csharp using System.Collections.Concurrent; namespace WIDESEA_Core.Core { /// /// 轮询服务 - 线程安全的地址轮询选择 /// public class RoundRobinService { /// /// 轮询计数器 - key: 巷道前缀, value: 当前索引 /// 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, oldValue) => (oldValue + 1) % addresses.Count // 轮询:递增后取模 ); return addresses[index]; } } } ``` **Step 2: 提交 RoundRobinService 类** ```bash git add WIDESEA_Core/Core/RoundRobinService.cs git commit -m "feat: 添加轮询服务类支持多出库口负载均衡" ``` --- ## Task 2: 修改 AutoOutboundTaskOptions 配置模型类 **Files:** - Modify: `WIDESEA_Core/Core/AutoOutboundTaskOptions.cs` **Step 1: 修改 TargetAddresses 属性类型** 读取文件 `WIDESEA_Core/Core/AutoOutboundTaskOptions.cs`,找到 `TargetAddresses` 属性,修改如下: **原代码:** ```csharp public Dictionary TargetAddresses { get; set; } = new() { { "GW", "10081" }, { "CW", "10080" } }; ``` **修改为:** ```csharp public Dictionary> TargetAddresses { get; set; } = new() { { "GW", new List { "10081" } }, { "CW", new List { "10080" } } }; ``` **Step 2: 提交配置模型类修改** ```bash git add WIDESEA_Core/Core/AutoOutboundTaskOptions.cs git commit -m "refactor: TargetAddresses 支持多出库口配置" ``` --- ## Task 3: 更新 appsettings.json 配置示例 **Files:** - Modify: `WIDESEA_WMSServer/appsettings.json` **Step 1: 更新 TargetAddresses 配置** 在 `appsettings.json` 中,将 `AutoOutboundTask.TargetAddresses` 的值从字符串改为数组: **原配置:** ```json { "AutoOutboundTask": { "Enable": true, "CheckIntervalSeconds": 300, "TargetAddresses": { "GW": "10081", "CW": "10080" } } } ``` **新配置:** ```json { "AutoOutboundTask": { "Enable": true, "CheckIntervalSeconds": 300, "TargetAddresses": { "GW": ["10081"], "CW": ["10080"] } } } ``` **注意:** 如果需要配置多个出库口,可以这样配置: ```json "TargetAddresses": { "GW": ["10081", "10082", "10083"], "CW": ["10080"] } ``` **Step 2: 提交配置更新** ```bash git add WIDESEA_WMSServer/appsettings.json git commit -m "config: 更新 TargetAddresses 为数组格式" ``` --- ## Task 4: 在 TaskService 中注入 RoundRobinService **Files:** - Modify: `WIDESEA_TaskInfoService/TaskService.cs` **Step 1: 添加私有字段** 在 `TaskService` 类的私有字段区域(大约第 20-30 行),添加: ```csharp private readonly RoundRobinService _roundRobinService; ``` **Step 2: 修改构造函数** 在构造函数参数中添加 `RoundRobinService roundRobinService`,并赋值: **找到构造函数:** ```csharp public TaskService( IRepository BaseDal, IMapper mapper, IStockInfoService stockInfoService, ILocationInfoService locationInfoService, HttpClientHelper httpClientHelper, IConfiguration configuration) : base(BaseDal) { _mapper = mapper; _stockInfoService = stockInfoService; _locationInfoService = locationInfoService; _httpClientHelper = httpClientHelper; _configuration = configuration; } ``` **修改为:** ```csharp public TaskService( IRepository BaseDal, IMapper mapper, IStockInfoService stockInfoService, ILocationInfoService locationInfoService, HttpClientHelper httpClientHelper, IConfiguration configuration, RoundRobinService roundRobinService) : base(BaseDal) { _mapper = mapper; _stockInfoService = stockInfoService; _locationInfoService = locationInfoService; _httpClientHelper = httpClientHelper; _configuration = configuration; _roundRobinService = roundRobinService; } ``` **Step 3: 提交构造函数修改** ```bash git add WIDESEA_TaskInfoService/TaskService.cs git commit -m "refactor: 注入 RoundRobinService" ``` --- ## Task 5: 修改 DetermineTargetAddress 方法 **Files:** - Modify: `WIDESEA_TaskInfoService/TaskService.cs` **Step 1: 找到并修改 DetermineTargetAddress 方法** 找到 `DetermineTargetAddress` 方法(大约在第 385-397 行),将其完整替换为: ```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); } ``` **Step 2: 提交方法修改** ```bash git add WIDESEA_TaskInfoService/TaskService.cs git commit -m "feat: 支持多出库口轮询选择" ``` --- ## Task 6: 在 Program.cs 中注册 RoundRobinService **Files:** - Modify: `WIDESEA_WMSServer/Program.cs` **Step 1: 添加 RoundRobinService 注册** 在 `Program.cs` 中,找到配置注册区域(大约第 43-45 行),在 `AddAllOptionRegister()` 之后添加: ```csharp builder.Services.AddSingleton(); ``` **完整上下文:** ```csharp builder.Services.AddAllOptionRegister(); builder.Services.AddSingleton(); // 新增 builder.Services.AddMemoryCacheSetup(); ``` **Step 2: 提交服务注册** ```bash git add WIDESEA_WMSServer/Program.cs git commit -m "config: 注册 RoundRobinService 为单例服务" ``` --- ## Task 7: 编译验证 **Step 1: 编译项目** ```bash cd WIDESEA_WMSServer dotnet build --configuration Release ``` **预期输出:** 编译成功,0 个错误 **Step 2: 检查编译结果** 确认输出包含: ``` Build succeeded. 0 Warning(s) 0 Error(s) ``` **Step 3: 如有错误,根据错误信息修复** 常见问题: - 缺少 `using System.Collections.Generic;` → 添加命名空间 - `RoundRobinService` 找不到 → 检查命名空间和注册 --- ## Task 8: 手动测试 - 单出库口 **Step 1: 确认配置为单出库口格式** 检查 `appsettings.json` 中的配置: ```json { "AutoOutboundTask": { "TargetAddresses": { "GW": ["10081"], "CW": ["10080"] } } } ``` **Step 2: 准备测试数据** 在数据库中执行: ```sql -- 设置测试库存的出库日期为过去时间 UPDATE Dt_StockInfo SET OutboundDate = DATEADD(MINUTE, -5, GETDATE()) WHERE PalletCode = 'TEST001' AND StockStatus = 1; ``` **Step 3: 启动应用** ```bash cd WIDESEA_WMSServer dotnet run ``` **Step 4: 观察日志输出** 预期看到: ``` info: 自动出库任务后台服务已启动 info: 到期库存检查完成: 成功创建 1 个出库任务 ``` **Step 5: 验证数据库** ```sql SELECT TaskNum, PalletCode, TargetAddress FROM Dt_Task WHERE PalletCode = 'TEST001' AND Creater = 'system_auto' ``` 预期 `TargetAddress` 为配置的地址(如 "10081") **Step 6: 清理测试数据** ```sql DELETE FROM Dt_Task WHERE PalletCode = 'TEST001' UPDATE Dt_StockInfo SET OutboundDate = NULL WHERE PalletCode = 'TEST001' ``` --- ## Task 9: 手动测试 - 多出库口轮询 **Step 1: 修改配置为多出库口** 更新 `appsettings.json`: ```json { "AutoOutboundTask": { "TargetAddresses": { "GW": ["10081", "10082", "10083"], "CW": ["10080"] } } } ``` **Step 2: 准备多个测试库存** ```sql UPDATE Dt_StockInfo SET OutboundDate = DATEADD(MINUTE, -5, GETDATE()) WHERE PalletCode IN ('TEST001', 'TEST002', 'TEST003', 'TEST004', 'TEST005') AND StockStatus = 1 AND LocationCode LIKE '%GW%'; -- 确保使用 GW 巷道 ``` **Step 3: 启动应用并观察日志** ```bash dotnet run ``` **Step 4: 验证轮询分配** ```sql SELECT TaskNum, PalletCode, TargetAddress FROM Dt_Task WHERE PalletCode IN ('TEST001', 'TEST002', 'TEST003', 'TEST004', 'TEST005') AND Creater = 'system_auto' ORDER BY CreateDate ``` 预期 `TargetAddress` 按轮询顺序分配:10081, 10082, 10083, 10081, 10082... **Step 5: 清理测试数据** ```sql DELETE FROM Dt_Task WHERE PalletCode IN ('TEST001', 'TEST002', 'TEST003', 'TEST004', 'TEST005') UPDATE Dt_StockInfo SET OutboundDate = NULL WHERE PalletCode IN ('TEST001', 'TEST002', 'TEST003', 'TEST004', 'TEST005') ``` --- ## Task 10: 并发安全测试 **Step 1: 创建测试脚本** 创建一个简单的 PowerShell 脚本 `test-concurrent.ps1`: ```powershell # 模拟并发创建任务 $tasks = 1..10 | ForEach-Object { Start-ThreadJob -ScriptBlock { # 调用创建任务的 API 或直接操作数据库 # 这里简化为模拟 Start-Sleep -Milliseconds (Get-Random -Minimum 10 -Maximum 100) } } Wait-Job -Job $tasks Receive-Job -Job $tasks ``` **Step 2: 观察轮询计数器** 在后台服务中添加临时日志(测试后删除): 在 `RoundRobinService.GetNextAddress` 中添加: ```csharp Console.WriteLine($"[RoundRobin] Key={key}, Index={index}, Address={addresses[index]}"); ``` **Step 3: 验证线程安全** - 多个任务同时创建 - 计数器递增不出现重复或跳跃 - 地址分配均匀 **Step 4: 移除调试日志** 测试完成后,移除添加的日志语句。 **Step 5: 提交并发测试代码(如果创建了独立测试项目)** ```bash git add -A git commit -m "test: 添加并发安全测试" ``` --- ## Task 11: 更新设计文档 **Files:** - Modify: `docs/plans/2026-03-09-multi-outbound-address-roundrobin-design.md` **Step 1: 更新实施状态** 在设计文档末尾的"实施清单"部分,更新状态: ```markdown ## 实施清单 - [x] 修改 `AutoOutboundTaskOptions.TargetAddresses` 类型 - [x] 创建 `RoundRobinService` 类 - [x] 修改 `TaskService.DetermineTargetAddress` 方法 - [x] 在 `Program.cs` 注册 `RoundRobinService` - [x] 更新 `appsettings.json` 配置示例 - [x] 编译验证 - [x] 手动测试 - [x] 并发安全测试 - [x] 更新设计文档 **实施日期**: 2026-03-09 **实施人**: Claude Code **状态**: 已完成 ``` **Step 2: 提交文档更新** ```bash git add docs/plans/2026-03-09-multi-outbound-address-roundrobin-design.md git commit -m "docs: 更新设计文档实施状态" ``` --- ## Task 12: 最终验证 **Step 1: 运行完整编译** ```bash cd .. dotnet build WIDESEA_WMSServer/WIDESEA_WMSServer.csproj --configuration Release ``` **Step 2: 确认所有文件已提交** ```bash git status ``` 预期输出:`nothing to commit, working tree clean` **Step 3: 查看提交历史** ```bash git log --oneline -10 ``` **Step 4: 验证功能完整性** 检查清单: - [ ] `RoundRobinService` 类已创建并注册 - [ ] `AutoOutboundTaskOptions` 类型已修改 - [ ] `TaskService.DetermineTargetAddress` 已更新 - [ ] `appsettings.json` 配置格式已更新 - [ ] 编译成功,0 错误 - [ ] 单出库口测试通过 - [ ] 多出库口轮询测试通过 - [ ] 并发安全验证通过 --- ## 完成检查清单 - [ ] 所有代码文件已创建/修改 - [ ] 所有代码已编译通过 - [ ] 配置文件已更新 - [ ] 单出库口测试已完成 - [ ] 多出库口轮询测试已完成 - [ ] 并发安全测试已完成 - [ ] 文档已更新 - [ ] 所有更改已提交到 git --- ## 故障排查 ### 编译错误 **问题**: 找不到 `RoundRobinService` 类型 **解决**: 1. 确认 `WIDESEA_Core/Core/RoundRobinService.cs` 文件存在 2. 检查命名空间是否正确:`WIDESEA_Core.Core` 3. 确认 `TaskService.cs` 中有正确的 using 引用 **问题**: `Dictionary>` 绑定失败 **解决**: 1. 检查 `appsettings.json` 格式是否正确 2. 确认 JSON 数组格式:`"GW": ["10081", "10082"]` 3. 检查是否有逗号分隔符错误 ### 运行时错误 **问题**: 轮询不生效,总是返回第一个地址 **解决**: 1. 检查配置中地址数组是否真的有多个元素 2. 确认 `RoundRobinService` 已注册为单例 3. 验证 `GetNextAddress` 方法被正确调用 **问题**: 应用启动失败,提示依赖注入错误 **解决**: 1. 确认 `RoundRobinService` 在 `Program.cs` 中已注册 2. 检查 `TaskService` 构造函数是否正确接收该参数 3. 验证 Autofac 配置 ### 测试问题 **问题**: 测试数据没有创建任务 **解决**: 1. 确认库存的 `OutboundDate` 已过期 2. 检查库存状态为"入库完成" 3. 验证后台服务已启动(查看日志) **问题**: 轮询顺序不对 **解决**: 1. 检查 `GetNextAddress` 中的模运算逻辑 2. 验证 `AddOrUpdate` 的更新函数是否正确 3. 确认没有多个 `RoundRobinService` 实例 --- ## 性能指标 预期性能: - **单次地址选择**: < 1 微秒(内存操作) - **并发 100 线程**: 无锁竞争,线性扩展 - **内存开销**: 每个巷道前缀约 50 字节 --- ## 回滚方案 如需回滚到原版本: 1. **恢复配置**: ```json "TargetAddresses": { "GW": "10081", "CW": "10080" } ``` 2. **恢复代码**: ```bash git revert ``` 3. **或使用 git reset**(谨慎使用): ```bash git reset --hard ```