For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
目标: 改进自动出库任务功能,支持一个巷道配置多个出库口,通过轮询算法实现负载均衡
架构: 使用独立的 RoundRobinService 类管理轮询计数器,基于 ConcurrentDictionary 实现线程安全的轮询选择,配置从 Dictionary<string, string> 改为 Dictionary<string, List>
技术栈: .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 已完成)
Files:
- Create: WIDESEA_Core/Core/RoundRobinService.cs
Step 1: 创建 RoundRobinService 类
创建文件 WIDESEA_Core/Core/RoundRobinService.cs:
using System.Collections.Concurrent;
namespace WIDESEA_Core.Core
{
/// <summary>
/// 轮询服务 - 线程安全的地址轮询选择
/// </summary>
public class RoundRobinService
{
/// <summary>
/// 轮询计数器 - key: 巷道前缀, value: 当前索引
/// </summary>
private readonly ConcurrentDictionary<string, int> _counters = new();
/// <summary>
/// 获取下一个地址(轮询)
/// </summary>
/// <param name="key">巷道前缀标识</param>
/// <param name="addresses">候选地址列表</param>
/// <returns>选中的目标地址</returns>
public string GetNextAddress(string key, List<string> 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 类
git add WIDESEA_Core/Core/RoundRobinService.cs
git commit -m "feat: 添加轮询服务类支持多出库口负载均衡"
Files:
- Modify: WIDESEA_Core/Core/AutoOutboundTaskOptions.cs
Step 1: 修改 TargetAddresses 属性类型
读取文件 WIDESEA_Core/Core/AutoOutboundTaskOptions.cs,找到 TargetAddresses 属性,修改如下:
原代码:csharp public Dictionary<string, string> TargetAddresses { get; set; } = new() { { "GW", "10081" }, { "CW", "10080" } };
修改为:csharp public Dictionary<string, List<string>> TargetAddresses { get; set; } = new() { { "GW", new List<string> { "10081" } }, { "CW", new List<string> { "10080" } } };
Step 2: 提交配置模型类修改
git add WIDESEA_Core/Core/AutoOutboundTaskOptions.cs
git commit -m "refactor: TargetAddresses 支持多出库口配置"
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: 提交配置更新
git add WIDESEA_WMSServer/appsettings.json
git commit -m "config: 更新 TargetAddresses 为数组格式"
Files:
- Modify: WIDESEA_TaskInfoService/TaskService.cs
Step 1: 添加私有字段
在 TaskService 类的私有字段区域(大约第 20-30 行),添加:
private readonly RoundRobinService _roundRobinService;
Step 2: 修改构造函数
在构造函数参数中添加 RoundRobinService roundRobinService,并赋值:
找到构造函数:csharp public TaskService( IRepository<Dt_Task> 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<Dt_Task> 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: 提交构造函数修改
git add WIDESEA_TaskInfoService/TaskService.cs
git commit -m "refactor: 注入 RoundRobinService"
Files:
- Modify: WIDESEA_TaskInfoService/TaskService.cs
Step 1: 找到并修改 DetermineTargetAddress 方法
找到 DetermineTargetAddress 方法(大约在第 385-397 行),将其完整替换为:
/// <summary>
/// 根据巷道确定目标地址(支持多出库口轮询)
/// </summary>
private string DetermineTargetAddress(string roadway, Dictionary<string, List<string>> 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: 提交方法修改
git add WIDESEA_TaskInfoService/TaskService.cs
git commit -m "feat: 支持多出库口轮询选择"
Files:
- Modify: WIDESEA_WMSServer/Program.cs
Step 1: 添加 RoundRobinService 注册
在 Program.cs 中,找到配置注册区域(大约第 43-45 行),在 AddAllOptionRegister() 之后添加:
builder.Services.AddSingleton<RoundRobinService>();
完整上下文:csharp builder.Services.AddAllOptionRegister(); builder.Services.AddSingleton<RoundRobinService>(); // 新增 builder.Services.AddMemoryCacheSetup();
Step 2: 提交服务注册
git add WIDESEA_WMSServer/Program.cs
git commit -m "config: 注册 RoundRobinService 为单例服务"
Step 1: 编译项目
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 找不到 → 检查命名空间和注册
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: 启动应用
cd WIDESEA_WMSServer
dotnet run
Step 4: 观察日志输出
预期看到: info: 自动出库任务后台服务已启动 info: 到期库存检查完成: 成功创建 1 个出库任务
Step 5: 验证数据库
SELECT TaskNum, PalletCode, TargetAddress FROM Dt_Task
WHERE PalletCode = 'TEST001' AND Creater = 'system_auto'
预期 TargetAddress 为配置的地址(如 "10081")
Step 6: 清理测试数据
DELETE FROM Dt_Task WHERE PalletCode = 'TEST001'
UPDATE Dt_StockInfo SET OutboundDate = NULL WHERE PalletCode = 'TEST001'
Step 1: 修改配置为多出库口
更新 appsettings.json:json { "AutoOutboundTask": { "TargetAddresses": { "GW": ["10081", "10082", "10083"], "CW": ["10080"] } } }
Step 2: 准备多个测试库存
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: 启动应用并观察日志
dotnet run
Step 4: 验证轮询分配
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: 清理测试数据
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')
Step 1: 创建测试脚本
创建一个简单的 PowerShell 脚本 test-concurrent.ps1:
# 模拟并发创建任务
$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: 提交并发测试代码(如果创建了独立测试项目)
git add -A
git commit -m "test: 添加并发安全测试"
Files:
- Modify: docs/plans/2026-03-09-multi-outbound-address-roundrobin-design.md
Step 1: 更新实施状态
在设计文档末尾的"实施清单"部分,更新状态:
## 实施清单
- [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: 提交文档更新
git add docs/plans/2026-03-09-multi-outbound-address-roundrobin-design.md
git commit -m "docs: 更新设计文档实施状态"
Step 1: 运行完整编译
cd ..
dotnet build WIDESEA_WMSServer/WIDESEA_WMSServer.csproj --configuration Release
Step 2: 确认所有文件已提交
git status
预期输出:nothing to commit, working tree clean
Step 3: 查看提交历史
git log --oneline -10
Step 4: 验证功能完整性
检查清单:
- [ ] RoundRobinService 类已创建并注册
- [ ] AutoOutboundTaskOptions 类型已修改
- [ ] TaskService.DetermineTargetAddress 已更新
- [ ] appsettings.json 配置格式已更新
- [ ] 编译成功,0 错误
- [ ] 单出库口测试通过
- [ ] 多出库口轮询测试通过
- [ ] 并发安全验证通过
问题: 找不到 RoundRobinService 类型
解决:
1. 确认 WIDESEA_Core/Core/RoundRobinService.cs 文件存在
2. 检查命名空间是否正确:WIDESEA_Core.Core
3. 确认 TaskService.cs 中有正确的 using 引用
问题: Dictionary<string, List<string>> 绑定失败
解决:
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 字节
如需回滚到原版本:
恢复配置:json "TargetAddresses": { "GW": "10081", "CW": "10080" }
恢复代码:bash git revert <commit-hash-range>
或使用 git reset(谨慎使用):bash git reset --hard <before-implementation-commit>