日期: 2026-03-09
作者: Claude Code
状态: 待实施
本设计旨在改进自动出库任务的目标地址配置功能,支持一个巷道配置多个出库口,并通过轮询算法实现负载均衡。
当前系统中,TargetAddresses 配置采用一对一映射(巷道前缀 → 出库口地址)。但实际业务中,一个巷道可能有多个出库口,需要支持:
采用 内存轮询计数器 + 独立服务类 的模式:
Dictionary<string, string> → Dictionary<string, List<string>>ConcurrentDictionary 保证多线程安全appsettings.json:json { "AutoOutboundTask": { "TargetAddresses": { "GW": ["10081", "10082", "10083"], "CW": ["10080"] } } }
说明:
- 值类型从 string 改为 string[](JSON 数组)
- 支持一个巷道配置多个出库口
- 单个出库口也可用数组格式(只有一个元素)
AutoOutboundTaskOptions.cs:
```csharp
public class AutoOutboundTaskOptions
{
public bool Enable { get; set; } = true;
public int CheckIntervalSeconds { get; set; } = 300;
public Dictionary<string, List<string>> TargetAddresses { get; set; }
= new()
{
{ "GW", new List<string> { "10081" } },
{ "CW", new List<string> { "10080" } }
};
}
```
RoundRobinService.cs (新建):
```csharp
using System.Collections.Concurrent;
namespace WIDESEA_Core.Core
{
///
/// <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, old) => (old + 1) % addresses.Count // 轮询:递增后取模
);
return addresses[index];
}
}
}
```
关键特性:
- 使用 ConcurrentDictionary<string, int> 存储每个巷道的当前索引
- AddOrUpdate 方法保证原子性,无需额外锁
- 模运算实现循环轮询
- 线程安全,支持后台服务的并发调用
DetermineTargetAddress 方法:
```csharp
private string DetermineTargetAddress(
string roadway,
Dictionary<string, List> 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;
}
}
```
Program.cs:csharp // 注册为单例,保证全局共享计数器 builder.Services.AddSingleton<RoundRobinService>();
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 |
| ... | ... | ... |
ConcurrentDictionary 保证线程安全AddOrUpdate 是原子操作{
"TargetAddresses": {
"GW": "10081",
"CW": "10080"
}
}
{
"TargetAddresses": {
"GW": ["10081"],
"CW": ["10080"]
}
}
兼容性处理:
- JSON 配置绑定会自动处理两种格式
- 单个字符串会被解析为单元素数组
- 现有配置无需修改即可工作
ConcurrentDictionary 使用优化的同步机制RoundRobinService.GetNextAddress 轮询逻辑-- 准备测试数据
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
TargetAddresses 的值从字符串改为数组格式RoundRobinService 已注册为单例TaskService 构造函数需要注入该服务appsettings.json 备份AutoOutboundTaskOptions.TargetAddresses 类型RoundRobinService 类TaskService.DetermineTargetAddress 方法Program.cs 注册 RoundRobinServiceappsettings.json 配置示例