# 自动出库任务后台服务设计文档 **日期**: 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. 考虑使用分布式锁支持多实例部署