wanshenmean
2026-03-06 d812bb29149f19feac04d0723ce5795e0fddb658
添加自动出库任务后台服务设计文档

- 详细描述了使用 BackgroundService 实现自动出库任务的方案
- 包含配置设计、组件结构、数据流程和错误处理
- 定义了需要新建和修改的文件列表
已添加1个文件
368 ■■■■■ 文件已修改
Code/WMS/WIDESEA_WMSServer/docs/plans/2026-03-06-auto-outbound-task-design.md 368 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/docs/plans/2026-03-06-auto-outbound-task-design.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,368 @@
# è‡ªåŠ¨å‡ºåº“ä»»åŠ¡åŽå°æœåŠ¡è®¾è®¡æ–‡æ¡£
**日期**: 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
    {
        /// <summary>
        /// æ˜¯å¦å¯ç”¨è‡ªåŠ¨å‡ºåº“ä»»åŠ¡
        /// </summary>
        public bool Enable { get; set; } = true;
        /// <summary>
        /// æ£€æŸ¥é—´éš”(秒)
        /// </summary>
        public int CheckIntervalSeconds { get; set; } = 300;
        /// <summary>
        /// æŒ‰å··é“前缀配置目标地址
        /// </summary>
        public Dictionary<string, string> 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<AutoOutboundTaskBackgroundService> _logger;
        private readonly ITaskService _taskService;
        private readonly AutoOutboundTaskOptions _options;
        public AutoOutboundTaskBackgroundService(
            ILogger<AutoOutboundTaskBackgroundService> logger,
            ITaskService taskService,
            IOptions<AutoOutboundTaskOptions> 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
/// <summary>
/// è‡ªåŠ¨åˆ›å»ºå‡ºåº“ä»»åŠ¡ - æŸ¥è¯¢åˆ°æœŸåº“存并创建任务
/// </summary>
/// <returns>包含创建结果的响应对象</returns>
Task<WebResponseContent> CreateAutoOutboundTasksAsync();
```
**实现**: `WIDESEA_TaskInfoService/TaskService.cs`
```csharp
public async Task<WebResponseContent> 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<Dictionary<string, string>>("AutoOutboundTask:TargetAddresses")
            ?? new Dictionary<string, string>();
        // 5. æ‰¹é‡åˆ›å»ºä»»åŠ¡
        var taskList = new List<Dt_Task>();
        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<WMSTaskDTO>(task);
                await _httpClientHelper.Post<WebResponseContent>(
                    "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<string, string> 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<AutoOutboundTaskOptions>(
    builder.Configuration.GetSection("AutoOutboundTask"));
// æ³¨å†ŒåŽå°æœåŠ¡
builder.Services.AddHostedService<AutoOutboundTaskBackgroundService>();
```
## æ•°æ®æµç¨‹
```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. è€ƒè™‘使用分布式锁支持多实例部署