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

自动出库任务后台服务设计文档

日期: 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 中添加配置节:

{
  "AutoOutboundTask": {
    "Enable": true,                    // 是否启用自动出库任务
    "CheckIntervalSeconds": 300,       // 检查间隔(秒),默认5分钟
    "TargetAddresses": {               // 按巷道配置目标地址
      "GW": "10081",                   // 高温巷道目标地址
      "CW": "10080"                    // 常温巷道目标地址
    }
  }
}

2. 配置模型类

文件: WIDESEA_Core/Core/AutoOutboundTaskOptions.cs

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

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

/// <summary>
/// 自动创建出库任务 - 查询到期库存并创建任务
/// </summary>
/// <returns>包含创建结果的响应对象</returns>
Task<WebResponseContent> CreateAutoOutboundTasksAsync();

实现: WIDESEA_TaskInfoService/TaskService.cs

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 注册服务

// 配置自动出库任务选项
builder.Services.Configure<AutoOutboundTaskOptions>(
    builder.Configuration.GetSection("AutoOutboundTask"));

// 注册后台服务
builder.Services.AddHostedService<AutoOutboundTaskBackgroundService>();

数据流程

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 地址映射逻辑
  1. 集成测试
  • 后台服务启动和停止
  • 配置项正确加载
  • 数据库查询和任务创建
  • WCS 通知接口调用
  1. 手动测试
  • 修改库存 OutboundDate 为过去时间
  • 观察日志确认任务创建
  • 验证 WCS 是否收到通知

部署注意事项

  1. 确保 appsettings.json 中配置了正确的 AutoOutboundTask
  2. 根据实际需求调整 CheckIntervalSeconds
  3. 验证 WCS 通知接口地址是否正确
  4. 监控应用程序日志,确认后台服务正常运行

未来改进

  1. 支持更复杂的目标地址配置规则
  2. 添加任务创建的统计数据和监控
  3. 支持手动触发任务创建的管理接口
  4. 考虑使用分布式锁支持多实例部署