编辑 | blame | 历史 | 原始文档

多出库口轮询功能实现计划

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 已完成)


Task 1: 创建 RoundRobinService 轮询服务类

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: 添加轮询服务类支持多出库口负载均衡"

Task 2: 修改 AutoOutboundTaskOptions 配置模型类

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 支持多出库口配置"

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: 提交配置更新

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 行),添加:

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"

Task 5: 修改 DetermineTargetAddress 方法

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: 支持多出库口轮询选择"

Task 6: 在 Program.cs 中注册 RoundRobinService

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 为单例服务"

Task 7: 编译验证

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 找不到 → 检查命名空间和注册


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: 启动应用

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'

Task 9: 手动测试 - 多出库口轮询

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')

Task 10: 并发安全测试

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: 添加并发安全测试"

Task 11: 更新设计文档

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: 更新设计文档实施状态"

Task 12: 最终验证

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 错误
- [ ] 单出库口测试通过
- [ ] 多出库口轮询测试通过
- [ ] 并发安全验证通过


完成检查清单

  • [ ] 所有代码文件已创建/修改
  • [ ] 所有代码已编译通过
  • [ ] 配置文件已更新
  • [ ] 单出库口测试已完成
  • [ ] 多出库口轮询测试已完成
  • [ ] 并发安全测试已完成
  • [ ] 文档已更新
  • [ ] 所有更改已提交到 git

故障排查

编译错误

问题: 找不到 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. 确认 RoundRobinServiceProgram.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 <commit-hash-range>

  3. 或使用 git reset(谨慎使用):
    bash git reset --hard <before-implementation-commit>