# 自动出库任务后台服务设计文档
**日期**: 2026-03-06
**作者**: Claude Code
**状态**: 待实施
## 概述
本设计旨在实现一个自动出库任务后台服务,该服务在 WMS 应用程序启动后自动运行,定期检查库存中到期需要出库的物料,并自动创建出库任务通知 WCS 系统执行。
## 需求背景
当前系统中,库存记录包含 `OutboundDate` 字段,表示预期的出库日期。需要实现一个后台服务,自动检测已到期(`OutboundDate <= 当前时间`)的库存,并创建相应的出库任务。
## 技术方案
### 架构选择
采用 **BackgroundService** 模式实现后台定时任务。
**理由**:
- .NET 标准模式,简单易懂
- 无需额外依赖
- 生命周期与应用程序绑定,自动管理启动/停止
- 易于配置检查间隔
### 组件结构
```
WIDESEA_WMSServer/
├── BackgroundServices/
│ └── AutoOutboundTaskBackgroundService.cs # 新建:后台服务类
├── WIDESEA_TaskInfoService/
│ └── TaskService.cs # 修改:添加新方法
├── WIDESEA_ITaskInfoService/
│ └── ITaskService.cs # 修改:添加接口定义
├── WIDESEA_Core/Core/
│ └── AutoOutboundTaskOptions.cs # 新建:配置模型类
└── Program.cs # 修改:注册后台服务和配置
```
## 详细设计
### 1. 配置设计
在 `appsettings.json` 中添加配置节:
```json
{
"AutoOutboundTask": {
"Enable": true, // 是否启用自动出库任务
"CheckIntervalSeconds": 300, // 检查间隔(秒),默认5分钟
"TargetAddresses": { // 按巷道配置目标地址
"GW": "10081", // 高温巷道目标地址
"CW": "10080" // 常温巷道目标地址
}
}
}
```
### 2. 配置模型类
**文件**: `WIDESEA_Core/Core/AutoOutboundTaskOptions.cs`
```csharp
namespace WIDESEA_Core.Core
{
public class AutoOutboundTaskOptions
{
///
/// 是否启用自动出库任务
///
public bool Enable { get; set; } = true;
///
/// 检查间隔(秒)
///
public int CheckIntervalSeconds { get; set; } = 300;
///
/// 按巷道前缀配置目标地址
///
public Dictionary TargetAddresses { get; set; }
= new()
{
{ "GW", "10081" },
{ "CW", "10080" }
};
}
}
```
### 3. 后台服务类
**文件**: `WIDESEA_WMSServer/BackgroundServices/AutoOutboundTaskBackgroundService.cs`
```csharp
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace WIDESEA_WMSServer.BackgroundServices
{
public class AutoOutboundTaskBackgroundService : BackgroundService
{
private readonly ILogger _logger;
private readonly ITaskService _taskService;
private readonly AutoOutboundTaskOptions _options;
public AutoOutboundTaskBackgroundService(
ILogger logger,
ITaskService taskService,
IOptions options)
{
_logger = logger;
_taskService = taskService;
_options = options.Value;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("自动出库任务后台服务已启动");
if (!_options.Enable)
{
_logger.LogInformation("自动出库任务功能已禁用");
return;
}
while (!stoppingToken.IsCancellationRequested)
{
try
{
_logger.LogDebug("开始检查到期库存...");
var result = await _taskService.CreateAutoOutboundTasksAsync();
_logger.LogInformation("到期库存检查完成: {Message}", result.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "自动出库任务创建失败");
}
var delay = TimeSpan.FromSeconds(_options.CheckIntervalSeconds);
_logger.LogDebug("等待 {Seconds} 秒后进行下次检查", delay.TotalSeconds);
await Task.Delay(delay, stoppingToken);
}
_logger.LogInformation("自动出库任务后台服务已停止");
}
}
}
```
### 4. TaskService 新增方法
**接口定义**: `WIDESEA_ITaskInfoService/ITaskService.cs`
```csharp
///
/// 自动创建出库任务 - 查询到期库存并创建任务
///
/// 包含创建结果的响应对象
Task CreateAutoOutboundTasksAsync();
```
**实现**: `WIDESEA_TaskInfoService/TaskService.cs`
```csharp
public async Task CreateAutoOutboundTasksAsync()
{
try
{
// 1. 查询到期库存
var expiredStocks = await _stockInfoService.Repository
.QueryAsync(s => s.OutboundDate <= DateTime.Now
&& s.StockStatus == StockStatusEmun.入库完成.GetHashCode()
&& s.LocationDetails.LocationStatus == LocationStatusEnum.InStock.GetHashCode(),
nameof(Dt_StockInfo.LocationDetails));
if (!expiredStocks.Any())
{
return WebResponseContent.Instance.OK("无到期库存需要处理");
}
// 2. 检查已存在的任务
var palletCodes = expiredStocks.Select(s => s.PalletCode).ToList();
var existingTasks = await Repository.QueryAsync(t =>
palletCodes.Contains(t.PalletCode)
&& t.TaskType == TaskTypeEnum.Outbound.GetHashCode()
&& t.TaskStatus != TaskStatusEnum.Completed.GetHashCode());
var processedPallets = existingTasks.Select(t => t.PalletCode).ToHashSet();
// 3. 筛选需要处理的库存
var stocksToProcess = expiredStocks
.Where(s => !processedPallets.Contains(s.PalletCode))
.ToList();
if (!stocksToProcess.Any())
{
return WebResponseContent.Instance.OK("所有到期库存已存在任务");
}
// 4. 获取配置的目标地址映射
var targetAddressMap = _appSettings.Get>("AutoOutboundTask:TargetAddresses")
?? new Dictionary();
// 5. 批量创建任务
var taskList = new List();
foreach (var stock in stocksToProcess)
{
// 根据巷道确定目标地址
var targetAddress = DetermineTargetAddress(stock.LocationDetails.RoadwayNo, targetAddressMap);
var task = new Dt_Task
{
WarehouseId = stock.WarehouseId,
PalletCode = stock.PalletCode,
PalletType = stock.PalletType,
SourceAddress = stock.LocationCode,
CurrentAddress = stock.LocationCode,
NextAddress = targetAddress,
TargetAddress = targetAddress,
Roadway = stock.LocationDetails.RoadwayNo,
TaskType = TaskTypeEnum.Outbound.GetHashCode(),
TaskStatus = TaskStatusEnum.New.GetHashCode(),
Grade = 1,
TaskNum = await Repository.GetTaskNo(),
Creater = "system_auto"
};
taskList.Add(task);
}
var addResult = await BaseDal.AddDataAsync(taskList) > 0;
if (!addResult)
{
return WebResponseContent.Instance.Error($"批量创建任务失败,共 {taskList.Count} 个任务");
}
// 6. 通知 WCS
var notifyTasks = taskList.Select(async task =>
{
try
{
var wmstaskDto = _mapper.Map(task);
await _httpClientHelper.Post(
"http://logistics-service/api/logistics/notifyoutbound",
JsonSerializer.Serialize(wmstaskDto));
}
catch (Exception ex)
{
_logger.LogWarning(ex, "WCS 通知失败,任务编号: {TaskNum}", task.TaskNum);
}
});
await Task.WhenAll(notifyTasks);
return WebResponseContent.Instance.OK($"成功创建 {taskList.Count} 个出库任务", taskList.Count);
}
catch (Exception ex)
{
_logger.LogError(ex, "自动创建出库任务时发生错误");
return WebResponseContent.Instance.Error($"自动创建出库任务失败: {ex.Message}");
}
}
private string DetermineTargetAddress(string roadway, Dictionary addressMap)
{
if (string.IsNullOrWhiteSpace(roadway))
return "10080"; // 默认地址
foreach (var kvp in addressMap)
{
if (roadway.Contains(kvp.Key))
return kvp.Value;
}
return "10080"; // 默认地址
}
```
### 5. Program.cs 注册服务
```csharp
// 配置自动出库任务选项
builder.Services.Configure(
builder.Configuration.GetSection("AutoOutboundTask"));
// 注册后台服务
builder.Services.AddHostedService();
```
## 数据流程
```mermaid
graph TD
A[应用程序启动] --> B[注册 AutoOutboundTaskBackgroundService]
B --> C[后台服务启动]
C --> D{检查 Enable 配置}
D -->|禁用| E[服务退出]
D -->|启用| F[进入定时循环]
F --> G[调用 CreateAutoOutboundTasksAsync]
G --> H[查询到期库存]
H --> I[检查已存在的任务]
I --> J[筛选需要处理的库存]
J --> K[批量创建 Dt_Task]
K --> L[通知 WCS 系统]
L --> M[返回结果]
M --> N[记录日志]
N --> O[等待配置的间隔时间]
O --> F
```
## 错误处理
### 后台服务级别
- 捕获所有异常,记录错误日志
- 不中断服务循环
- 使用 `try-catch` 包裹整个循环体
### 任务创建级别
- 单个任务创建失败不影响其他任务
- 记录失败的托盘码和原因
- 返回详细的成功/失败统计
### WCS 通知失败
- 任务已创建到数据库,但 WCS 通知失败
- 记录警告日志
- 不重试(WCS 会主动轮询获取任务)
## 日志记录
| 级别 | 场景 |
|------|------|
| Information | 服务启动/停止、检查周期、创建任务数量 |
| Warning | WCS 通知失败、无到期库存 |
| Error | 异常情况、数据库操作失败 |
## 测试计划
1. **单元测试**
- `CreateAutoOutboundTasksAsync` 方法逻辑
- `DetermineTargetAddress` 地址映射逻辑
2. **集成测试**
- 后台服务启动和停止
- 配置项正确加载
- 数据库查询和任务创建
- WCS 通知接口调用
3. **手动测试**
- 修改库存 `OutboundDate` 为过去时间
- 观察日志确认任务创建
- 验证 WCS 是否收到通知
## 部署注意事项
1. 确保 `appsettings.json` 中配置了正确的 `AutoOutboundTask` 节
2. 根据实际需求调整 `CheckIntervalSeconds`
3. 验证 WCS 通知接口地址是否正确
4. 监控应用程序日志,确认后台服务正常运行
## 未来改进
1. 支持更复杂的目标地址配置规则
2. 添加任务创建的统计数据和监控
3. 支持手动触发任务创建的管理接口
4. 考虑使用分布式锁支持多实例部署