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

自动出库任务后台服务实现计划

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

目标: 实现一个后台服务,自动检测到期库存并创建出库任务通知 WCS 系统

架构: 使用 BackgroundService 模式实现定时任务,通过 ITaskService 查询到期库存、创建任务并通知 WCS

技术栈: .NET 6, BackgroundService, SqlSugar, Autofac, appsettings.json 配置


前置检查

在开始实现前,请确认:
- 项目位于: d:\Git\ShanMeiXinNengYuan\Code\WMS\WIDESEA_WMSServer
- 主项目文件: WIDESEA_WMSServer\WIDESEA_WMSServer.csproj
- 已阅读设计文档: docs/plans/2026-03-06-auto-outbound-task-design.md


Task 1: 创建配置模型类

Files:
- Create: WIDESEA_Core/Core/AutoOutboundTaskOptions.cs

Step 1: 创建配置模型类

创建文件 WIDESEA_Core/Core/AutoOutboundTaskOptions.cs:

using System.Collections.Generic;

namespace WIDESEA_Core.Core
{
    /// <summary>
    /// 自动出库任务配置选项
    /// </summary>
    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" }
            };
    }
}

Step 2: 提交配置模型类

git add WIDESEA_Core/Core/AutoOutboundTaskOptions.cs
git commit -m "feat: 添加自动出库任务配置模型类"

Task 2: 更新 appsettings.json 配置

Files:
- Modify: WIDESEA_WMSServer/appsettings.json

Step 1: 添加配置节

appsettings.json 中添加 AutoOutboundTask 配置节(在 WebSocketPort 后面):

{
  ...,
  "WebSocketPort": 9296,
  "AutoOutboundTask": {
    "Enable": true,
    "CheckIntervalSeconds": 300,
    "TargetAddresses": {
      "GW": "10081",
      "CW": "10080"
    }
  }
}

Step 2: 提交配置更新

git add WIDESEA_WMSServer/appsettings.json
git commit -m "config: 添加自动出库任务配置"

Task 3: 在 ITaskService 接口中添加方法定义

Files:
- Modify: WIDESEA_ITaskInfoService/ITaskService.cs

Step 1: 添加接口方法

ITaskService.cs 中添加方法定义(在文件末尾,} 之前):

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

Step 2: 提交接口更新

git add WIDESEA_ITaskInfoService/ITaskService.cs
git commit -m "feat: 添加自动出库任务创建接口方法"

Task 4: 在 TaskService 中实现核心逻辑

Files:
- Modify: WIDESEA_TaskInfoService/TaskService.cs

Step 1: 添加依赖注入字段

TaskService 类的私有字段区域(第 20-25 行附近)添加:

private readonly IConfiguration _configuration;

修改构造函数签名(第 37-48 行),添加 IConfiguration 参数:

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;
}

Step 2: 添加地址映射辅助方法

TaskService 类中添加私有方法(在 GetTasksByPalletCodeAsync 方法后面,#endregion 之前):

/// <summary>
/// 根据巷道确定目标地址
/// </summary>
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"; // 默认地址
}

Step 3: 实现自动出库任务创建方法

TaskService 类的 #endregion WCS逻辑处理 之前添加完整方法:

/// <summary>
/// 自动创建出库任务 - 查询到期库存并创建任务
/// </summary>
public async Task<WebResponseContent> CreateAutoOutboundTasksAsync()
{
    try
    {
        // 1. 查询到期库存
        var expiredStocks = await _stockInfoService.Repository
            .QueryAsync(s => s.OutboundDate <= DateTime.Now
                && s.StockStatus == StockStatusEmun.入库完成.GetHashCode());

        if (expiredStocks == null || !expiredStocks.Any())
        {
            return WebResponseContent.Instance.OK("无到期库存需要处理");
        }

        // 加载位置详情
        foreach (var stock in expiredStocks)
        {
            if (stock.LocationId > 0)
            {
                stock.LocationDetails = await _locationInfoService.Repository
                    .GetFirstAsync(s => s.Id == stock.LocationId);
            }
        }

        // 过滤有位置且位置有库存的记录
        expiredStocks = expiredStocks
            .Where(s => s.LocationDetails != null
                && s.LocationDetails.LocationStatus == LocationStatusEnum.InStock.GetHashCode())
            .ToList();

        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.TaskStatus == TaskStatusEnum.New.GetHashCode()
                || t.TaskStatus == TaskStatusEnum.Executing.GetHashCode()
                || t.TaskStatus == TaskInStatusEnum.InNew.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 = _configuration.GetSection("AutoOutboundTask:TargetAddresses")
            .Get<Dictionary<string, string>>()
            ?? 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(异步,不影响主流程)
        _ = Task.Run(async () =>
        {
            foreach (var task in taskList)
            {
                try
                {
                    var wmstaskDto = _mapper.Map<WMSTaskDTO>(task);
                    await _httpClientHelper.Post<WebResponseContent>(
                        "http://logistics-service/api/logistics/notifyoutbound",
                        JsonSerializer.Serialize(wmstaskDto));
                }
                catch (Exception ex)
                {
                    // WCS 通知失败不影响任务创建,记录日志即可
                    Console.WriteLine($"WCS 通知失败,任务编号: {task.TaskNum}, 错误: {ex.Message}");
                }
            }
        });

        return WebResponseContent.Instance.OK($"成功创建 {taskList.Count} 个出库任务", taskList.Count);
    }
    catch (Exception ex)
    {
        return WebResponseContent.Instance.Error($"自动创建出库任务失败: {ex.Message}");
    }
}

Step 4: 提交实现

git add WIDESEA_TaskInfoService/TaskService.cs
git commit -m "feat: 实现自动出库任务创建方法"

Task 5: 创建后台服务类

Files:
- Create: WIDESEA_WMSServer/BackgroundServices/AutoOutboundTaskBackgroundService.cs

Step 1: 创建 BackgroundServices 目录

mkdir -p WIDESEA_WMSServer/BackgroundServices

Step 2: 创建后台服务类

创建文件 WIDESEA_WMSServer/BackgroundServices/AutoOutboundTaskBackgroundService.cs:

using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Threading;
using System.Threading.Tasks;
using WIDESEA_Core.Core;
using WIDESEA_ITaskInfoService;

namespace WIDESEA_WMSServer.BackgroundServices
{
    /// <summary>
    /// 自动出库任务后台服务
    /// 定期检查到期库存并创建出库任务
    /// </summary>
    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;
            }

            _logger.LogInformation("自动出库任务检查间隔: {Seconds} 秒", _options.CheckIntervalSeconds);

            while (!stoppingToken.IsCancellationRequested)
            {
                try
                {
                    _logger.LogDebug("开始检查到期库存...");
                    var result = await _taskService.CreateAutoOutboundTasksAsync();

                    if (result.Status)
                    {
                        _logger.LogInformation("到期库存检查完成: {Message}", result.Message);
                    }
                    else
                    {
                        _logger.LogWarning("到期库存检查失败: {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("自动出库任务后台服务已停止");
        }

        public override async Task StopAsync(CancellationToken cancellationToken)
        {
            _logger.LogInformation("正在停止自动出库任务后台服务...");
            await base.StopAsync(cancellationToken);
        }
    }
}

Step 3: 提交后台服务类

git add WIDESEA_WMSServer/BackgroundServices/AutoOutboundTaskBackgroundService.cs
git commit -m "feat: 添加自动出库任务后台服务"

Task 6: 注册服务和配置

Files:
- Modify: WIDESEA_WMSServer/Program.cs

Step 1: 添加配置注册

Program.cs 中的 builder.Services.AddAllOptionRegister(); 之后添加:

builder.Services.Configure<AutoOutboundTaskOptions>(
    builder.Configuration.GetSection("AutoOutboundTask"));

Step 2: 添加后台服务注册

Program.cs 中的 builder.Services.AddInitializationHostServiceSetup(); 之后添加:

builder.Services.AddHostedService<AutoOutboundTaskBackgroundService>();

Step 3: 添加命名空间引用

Program.cs 文件顶部的 using 区域添加:

using WIDESEA_WMSServer.BackgroundServices;
using WIDESEA_Core.Core;

Step 4: 提交 Program.cs 更新

git add WIDESEA_WMSServer/Program.cs
git commit -m "config: 注册自动出库任务后台服务和配置"

Task 7: 编译验证

Step 1: 编译项目

cd WIDESEA_WMSServer
dotnet build --configuration Release

预期输出: 编译成功,无错误

Step 2: 检查编译输出

确认输出包含:
Build succeeded. 0 Warning(s) 0 Error(s)

Step 3: 如果有错误,根据错误信息修复

常见问题:
- 缺少 using 引用 → 添加相应的 namespace
- 类型不匹配 → 检查属性名称和类型是否正确


Task 8: 手动测试

Step 1: 准备测试数据

在数据库中执行 SQL,创建一条测试库存记录:

-- 确保有一个已入库的库存记录,OutboundDate 设置为过去时间
UPDATE Dt_StockInfo
SET OutboundDate = DATEADD(MINUTE, -5, GETDATE())
WHERE PalletCode = 'TEST001' AND StockStatus = 1; -- 1 = 入库完成

-- 如果没有测试记录,插入一条
INSERT INTO Dt_StockInfo (PalletCode, PalletType, LocationId, LocationCode, WarehouseId, StockStatus, OutboundDate, CreateDate)
VALUES ('TEST001', 1, 1, '01-01-01', 1, 1, DATEADD(MINUTE, -5, GETDATE()), GETDATE())

Step 2: 启动应用程序

cd WIDESEA_WMSServer
dotnet run

Step 3: 观察控制台输出

预期看到类似日志:
info: WIDESEA_WMSServer.BackgroundServices.AutoOutboundTaskBackgroundService[0] 自动出库任务后台服务已启动 info: WIDESEA_WMSServer.BackgroundServices.AutoOutboundTaskBackgroundService[0] 自动出库任务检查间隔: 300 秒 ... info: WIDESEA_WMSServer.BackgroundServices.AutoOutboundTaskBackgroundService[0] 到期库存检查完成: 成功创建 X 个出库任务

Step 4: 验证数据库

查询任务表确认任务已创建:

SELECT * FROM Dt_Task WHERE PalletCode = 'TEST001' AND Creater = 'system_auto'

Step 5: 测试完成,清理测试数据

-- 删除测试任务
DELETE FROM Dt_Task WHERE PalletCode = 'TEST001'
-- 删除测试库存
DELETE FROM Dt_StockInfo WHERE PalletCode = 'TEST001'

Task 9: 配置验证测试

Step 1: 测试禁用功能

修改 appsettings.json:
json "AutoOutboundTask": { "Enable": false, ... }

Step 2: 启动应用验证

dotnet run

预期输出应包含: 自动出库任务功能已禁用,服务退出

Step 3: 恢复配置

"AutoOutboundTask": {
  "Enable": true,
  ...
}

Step 4: 测试不同的检查间隔

修改为 10 秒方便观察:
json "CheckIntervalSeconds": 10

启动应用,观察检查是否按 10 秒间隔执行

Step 5: 提交测试结论

创建测试说明文件 WIDESEA_WMSServer/BackgroundServices/README.md:

# 自动出库任务后台服务

## 功能说明
本服务自动检测到期库存并创建出库任务。

## 配置
在 `appsettings.json` 中配置:

{
"AutoOutboundTask": {
"Enable": true,
"CheckIntervalSeconds": 300,
"TargetAddresses": {
"GW": "10081",
"CW": "10080"
}
}
}
```

测试方法

  1. 设置库存的 OutboundDate 为过去时间
  2. 启动应用,观察日志
  3. 确认任务已创建到数据库
    ```

Step 6: 提交测试文档

git add WIDESEA_WMSServer/BackgroundServices/README.md
git commit -m "docs: 添加后台服务测试说明"

Task 10: 最终验证和文档

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: 创建功能总结

更新设计文档,添加实施状态:

docs/plans/2026-03-06-auto-outbound-task-design.md 末尾添加:

## 实施状态

- [x] 设计完成
- [x] 配置模型类创建
- [x] TaskService 方法实现
- [x] 后台服务类创建
- [x] Program.cs 注册完成
- [x] 编译验证通过
- [x] 手动测试完成

**实施日期**: 2026-03-06
**实施人**: Claude Code

Step 5: 提交文档更新

git add docs/plans/2026-03-06-auto-outbound-task-design.md
git commit -m "docs: 更新设计文档实施状态"

完成检查清单

  • [ ] 所有代码文件已创建
  • [ ] 所有代码已编译通过
  • [ ] 配置文件已更新
  • [ ] 手动测试已完成
  • [ ] 日志输出符合预期
  • [ ] 数据库验证通过
  • [ ] 文档已更新
  • [ ] 所有更改已提交到 git

故障排查

编译错误

问题: 找不到类型或命名空间
解决: 确保已添加正确的 using 引用

问题: 依赖注入失败
解决: 检查 Program.cs 中的服务注册顺序

运行时错误

问题: 后台服务没有启动
解决:
1. 检查 appsettings.jsonEnable 是否为 true
2. 查看启动日志中的错误信息

问题: 没有创建任务
解决:
1. 确认库存记录的 OutboundDate 已过期
2. 确认库存状态为"入库完成"
3. 检查是否已存在相同托盘的任务

问题: WCS 通知失败
解决:
1. 检查 WCS 服务是否运行
2. 验证通知地址是否正确
3. 注意: WCS 通知失败不影响任务创建