wanshenmean
3 天以前 7278264f027d62664a0209699d0f66a22fd06a8e
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotTaskProcessor.cs
@@ -1,3 +1,4 @@
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using WIDESEA_Core;
using WIDESEAWCS_Common;
@@ -5,183 +6,397 @@
using WIDESEAWCS_Common.TaskEnum;
using WIDESEAWCS_Core;
using WIDESEAWCS_Core.Helper;
using WIDESEAWCS_Core.LogHelper;
using WIDESEAWCS_DTO.Stock;
using WIDESEAWCS_DTO.TaskInfo;
using WIDESEAWCS_ITaskInfoService;
using WIDESEAWCS_Model.Models;
using WIDESEAWCS_QuartzJob;
using WIDESEAWCS_Tasks.SocketServer;
using WIDESEAWCS_Tasks.Workflow.Abstractions;
namespace WIDESEAWCS_Tasks
{
    /// <summary>
    /// 机械手任务处理器 - 负责机械手任务执行和处理
    /// 机器人任务处理器 - 负责任务获取、下发、入库任务回传及库存 DTO 构建
    /// </summary>
    /// <remarks>
    /// 核心职责:
    /// 1. 从数据库轮询待处理的机器人任务
    /// 2. 向机器人客户端下发取货指令(Pickbattery)
    /// 3. 处理入库任务的回传(拆盘/组盘/换盘场景)
    /// 4. 构建库存回传 DTO 并调用 WMS 接口
    ///
    /// 通过网关访问 Socket,避免业务层直接依赖 TcpSocketServer。
    /// </remarks>
    public class RobotTaskProcessor
    {
        private readonly TcpSocketServer _tcpSocket;
        /// <summary>
        /// Socket 客户端网关接口
        /// </summary>
        /// <remarks>
        /// 通过网关访问 Socket,避免业务层直接依赖 TcpSocketServer。
        /// 提供统一的客户端通信接口。
        /// </remarks>
        private readonly ISocketClientGateway _socketClientGateway;
        /// <summary>
        /// 机械手状态管理器
        /// </summary>
        private readonly RobotStateManager _stateManager;
        /// <summary>
        /// 机器人任务服务
        /// </summary>
        /// <remarks>
        /// 用于查询、更新、删除机器人任务记录。
        /// </remarks>
        private readonly IRobotTaskService _robotTaskService;
        /// <summary>
        /// 通用任务服务
        /// </summary>
        /// <remarks>
        /// 用于与 WMS 系统交互,接收任务、处理任务状态等。
        /// </remarks>
        private readonly ITaskService _taskService;
        /// <summary>
        /// HTTP 客户端帮助类
        /// </summary>
        /// <remarks>
        /// 用于调用 WMS 系统的 HTTP 接口。
        /// </remarks>
        private readonly HttpClientHelper _httpClientHelper;
        /// <summary>
        /// 日志记录器
        /// </summary>
        private readonly ILogger _logger;
        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="socketClientGateway">Socket 网关</param>
        /// <param name="stateManager">状态管理器</param>
        /// <param name="robotTaskService">机器人任务服务</param>
        /// <param name="taskService">通用任务服务</param>
        /// <param name="httpClientHelper">HTTP 客户端帮助类</param>
        /// <param name="logger">日志记录器</param>
        public RobotTaskProcessor(
            TcpSocketServer tcpSocket,
            ISocketClientGateway socketClientGateway,
            RobotStateManager stateManager,
            IRobotTaskService robotTaskService,
            ITaskService taskService,
            HttpClientHelper httpClientHelper)
            HttpClientHelper httpClientHelper,
            ILogger logger)
        {
            _tcpSocket = tcpSocket;
            _socketClientGateway = socketClientGateway;
            _stateManager = stateManager;
            _robotTaskService = robotTaskService;
            _taskService = taskService;
            _httpClientHelper = httpClientHelper;
            _logger = logger;
        }
        /// <summary>
        /// 获取机械手任务
        /// 按设备编码获取当前机器人的待处理任务
        /// </summary>
        /// <remarks>
        /// 从数据库中查询指定设备编码的待处理机器人任务。
        /// 只返回状态为"待处理"的任务。
        /// </remarks>
        /// <param name="robotCrane">机器人设备信息,包含设备编码</param>
        /// <returns>待处理的任务对象,如果没有则返回 null</returns>
        public Dt_RobotTask? GetTask(RobotCraneDevice robotCrane)
        {
            return _robotTaskService.QueryRobotCraneTask(robotCrane.DeviceCode);
        }
        /// <summary>
        /// 获取机械手任务
        /// 按设备编码获取当前机器人的执行中任务
        /// </summary>
        /// <remarks>
        /// 从数据库中查询指定设备编码的执行中机器人任务。
        /// 当RobotArmObject为1(有物料)且没有待处理任务时调用。
        /// </remarks>
        /// <param name="robotCrane">机器人设备信息,包含设备编码</param>
        /// <returns>执行中的任务对象,如果没有则返回 null</returns>
        public Dt_RobotTask? GetExecutingTask(RobotCraneDevice robotCrane)
        {
            return _robotTaskService.QueryRobotCraneExecutingTask(robotCrane.DeviceCode);
        }
        /// <summary>
        /// 删除机器人任务
        /// </summary>
        /// <remarks>
        /// 当任务完成(无论是成功还是失败)时调用,删除数据库中的任务记录。
        /// </remarks>
        /// <param name="ID">要删除的任务 ID</param>
        /// <returns>删除是否成功</returns>
        public bool? DeleteTask(int ID)
        {
            return _robotTaskService.Repository.DeleteDataById(ID);
        }
        /// <summary>
        /// 发送机械手取货命令
        /// 下发取货指令(Pickbattery)到机器人客户端
        /// </summary>
        /// <remarks>
        /// 发送格式:Pickbattery,{源地址}
        /// 例如:Pickbattery,A01 表示从 A01 位置取货
        ///
        /// 下发成功后:
        /// 1. 更新任务状态为"机器人执行中"
        /// 2. 将任务关联到状态对象
        /// 3. 安全更新状态到 Redis
        /// 4. 更新任务记录到数据库
        /// </remarks>
        /// <param name="task">要下发的任务对象</param>
        /// <param name="state">机器人当前状态</param>
        public async Task SendSocketRobotPickAsync(Dt_RobotTask task, RobotSocketState state)
        {
            // 构建取货指令,格式:Pickbattery,{源地址}
            string taskString = $"Pickbattery,{task.RobotSourceAddress}";
            // 发送任务指令
            bool result = await _tcpSocket.SendToClientAsync(state.IPAddress, taskString);
            // 通过 Socket 网关发送指令到机器人客户端
            bool result = await _socketClientGateway.SendToClientAsync(state.IPAddress, taskString);
            if (result)
            {
                // 发送成功,记录 Info 日志
                _logger.LogInformation("下发取货指令成功,指令: {TaskString},设备: {DeviceName}", taskString, state.RobotCrane?.DeviceName);
                QuartzLogger.Info($"下发取货指令成功,指令: {taskString}", state.RobotCrane?.DeviceName);
                // 更新任务状态为"机器人执行中"
                task.RobotTaskState = TaskRobotStatusEnum.RobotExecuting.GetHashCode();
                // 将任务关联到状态对象
                state.CurrentTask = task;
                // 更新缓存中的状态(使用安全更新防止并发覆盖)
                // 保持原语义:仅在状态安全写入成功后再更新任务状态
                // 这样可以确保状态和任务记录的一致性
                if (_stateManager.TryUpdateStateSafely(state.IPAddress, state))
                {
                    await _robotTaskService.UpdateRobotTaskAsync(task);
                }
            }
            else
            {
                // 发送失败,记录 Error 日志
                _logger.LogError("下发取货指令失败,指令: {TaskString},设备: {DeviceName}", taskString, state.RobotCrane?.DeviceName);
                QuartzLogger.Error($"下发取货指令失败,指令: {taskString}", state.RobotCrane?.DeviceName);
            }
        }
        /// <summary>
        /// 处理入库任务
        /// 处理入库任务回传(拆盘/组盘/换盘场景)
        /// </summary>
        /// <remarks>
        /// 当取货完成(AllPickFinished)或放货完成(AllPutFinished)时调用此方法。
        /// 根据任务类型和地址来源决定如何回传给 WMS。
        ///
        /// 处理逻辑:
        /// 1. 根据 useSourceAddress 决定使用源地址还是目标地址
        /// 2. 根据任务类型(组盘/换盘/拆盘)决定任务类型(入库/空托盘入库)
        /// 3. 构建 CreateTaskDto 并调用 WMS 接口创建任务
        /// 4. 接收 WMS 返回的任务信息
        /// 5. 更新输送线的目标地址、任务号等
        /// </remarks>
        /// <param name="state">机器人当前状态</param>
        /// <param name="useSourceAddress">是否使用源地址(true 表示拆盘/换盘场景,false 表示组盘/换盘场景)</param>
        /// <returns>处理是否成功</returns>
        public async Task<bool> HandleInboundTaskAsync(RobotSocketState state, bool useSourceAddress)
        {
            // 获取当前关联的任务
            var currentTask = state.CurrentTask;
            if (currentTask == null)
            {
                _logger.LogDebug("HandleInboundTaskAsync:当前任务为空");
                QuartzLogger.Debug($"HandleInboundTaskAsync:当前任务为空", state.RobotCrane?.DeviceName ?? "Unknown");
                return false;
            }
            // 获取巷道代码
            string roadway = currentTask.RobotSourceAddressLineCode;
            int warehouseId = currentTask.RobotRoadway == "ZYRB1" ? 1 : currentTask.RobotRoadway == "HPRB001" ? 2 : 3;
            // 根据巷道名称判断仓库 ID
            // ZYRB1 -> 1, HPRB001 -> 2, 其他 -> 3
            int warehouseId = currentTask.RobotRoadway == "注液组盘机械手" ? 1 : currentTask.RobotRoadway == "HPRB001" ? 2 : 3;
            // 任务类型(0 表示未定义,稍后根据任务类型设置)
            int taskType = 0;
            // 源地址和目标地址(初始化)
            string SourceAddress = currentTask.RobotTargetAddressLineCode;
            string TargetAddress = currentTask.RobotSourceAddressLineCode;
            // 托盘代码(初始化为空)
            string PalletCode = string.Empty;
            // 直接转换为枚举类型进行比较
            // 获取任务类型的枚举值
            var robotTaskType = (RobotTaskTypeEnum)currentTask.RobotTaskType;
            // 根据 useSourceAddress 决定处理逻辑
            if (useSourceAddress)
            {
                // 使用源地址的场景:拆盘、换盘(放空托盘)
                switch (robotTaskType)
                {
                    case RobotTaskTypeEnum.GroupPallet:
                        // 组盘任务不使用源地址,直接返回 false
                        _logger.LogDebug("HandleInboundTaskAsync:组盘任务不使用源地址");
                        QuartzLogger.Debug($"HandleInboundTaskAsync:组盘任务不使用源地址", state.RobotCrane?.DeviceName ?? "Unknown");
                        return false;
                    case RobotTaskTypeEnum.ChangePallet:
                    case RobotTaskTypeEnum.SplitPallet:
                        taskType = TaskTypeEnum.InEmpty.GetHashCode();
                        PalletCode = currentTask.RobotSourceAddressPalletCode;
                        // 换盘/拆盘场景:托盘需要入库
                        taskType = TaskTypeEnum.InEmpty.GetHashCode();  // 空托盘入库
                        PalletCode = currentTask.RobotSourceAddressPalletCode;  // 使用源地址的托盘码
                        break;
                }
            }
            else
            {
                // 使用目标地址的场景:组盘、换盘(成品入库)
                switch (robotTaskType)
                {
                    case RobotTaskTypeEnum.ChangePallet:
                    case RobotTaskTypeEnum.GroupPallet:
                        taskType = TaskTypeEnum.Inbound.GetHashCode();
                        PalletCode = currentTask.RobotTargetAddressPalletCode;
                        // 换盘/组盘场景:货物需要入库
                        taskType = TaskTypeEnum.Inbound.GetHashCode();  // 成品入库
                        PalletCode = currentTask.RobotTargetAddressPalletCode;  // 使用目标地址的托盘码
                        break;
                    case RobotTaskTypeEnum.SplitPallet:
                        // 拆盘任务不使用目标地址
                        _logger.LogDebug("HandleInboundTaskAsync:拆盘任务不使用目标地址");
                        QuartzLogger.Debug($"HandleInboundTaskAsync:拆盘任务不使用目标地址", state.RobotCrane?.DeviceName ?? "Unknown");
                        return true;
                }
            }
            // 构建创建任务的 DTO
            CreateTaskDto taskDto = new CreateTaskDto
            {
                PalletCode = PalletCode,
                SourceAddress = SourceAddress ?? string.Empty,
                TargetAddress = TargetAddress ?? string.Empty,
                Roadway = roadway,
                WarehouseId = warehouseId,
                PalletType = 1,
                TaskType = taskType
                PalletCode = PalletCode,                    // 托盘条码
                SourceAddress = SourceAddress ?? string.Empty,  // 源地址
                TargetAddress = TargetAddress ?? string.Empty,  // 目标地址
                Roadway = roadway,                          // 巷道
                WarehouseId = warehouseId,                   // 仓库 ID
                PalletType = 1,                             // 托盘类型(默认为1)
                TaskType = taskType                         // 任务类型(入库/空托盘入库)
            };
            // 记录日志:开始调用 WMS 创建入库任务
            _logger.LogInformation("HandleInboundTaskAsync:调用WMS创建入库任务,托盘码: {PalletCode},任务类型: {TaskType}", PalletCode, taskType);
            QuartzLogger.Info($"调用WMS创建入库任务,托盘码: {PalletCode},任务类型: {taskType}", state.RobotCrane?.DeviceName ?? "Unknown");
            // 调用 WMS 接口创建入库任务
            var result = _httpClientHelper.Post<WebResponseContent>(nameof(ConfigKey.CreateTaskInboundAsync), taskDto.ToJson());
            // 如果调用失败或返回错误状态
            if (!result.Data.Status && result.IsSuccess)
            {
                _logger.LogError("HandleInboundTaskAsync:WMS返回错误状态,Status: {Status}", result.Data.Status);
                QuartzLogger.Error($"HandleInboundTaskAsync:WMS返回错误状态", state.RobotCrane?.DeviceName ?? "Unknown");
                return false;
            }
            // 解析 WMS 返回的任务信息
            WMSTaskDTO taskDTO = JsonConvert.DeserializeObject<WMSTaskDTO>(result.Data.Data.ToJson() ?? string.Empty) ?? new WMSTaskDTO();
            // 调用任务服务接收 WMS 任务
            var content = _taskService.ReceiveWMSTask(new List<WMSTaskDTO> { taskDTO });
            if (!content.Status) return false;
            if (!content.Status)
            {
                _logger.LogError("HandleInboundTaskAsync:接收WMS任务失败");
                QuartzLogger.Error($"HandleInboundTaskAsync:接收WMS任务失败", state.RobotCrane?.DeviceName ?? "Unknown");
                return false;
            }
            var taskInfo = JsonConvert.DeserializeObject<Dt_Task>(content.Data.ToJson() ?? string.Empty) ?? new Dt_Task();
            // 解析返回的任务信息
            var taskInfos = JsonConvert.DeserializeObject<List<Dt_Task>>(content.Data.ToJson() ?? string.Empty) ?? new List<Dt_Task>();
            var taskInfo = taskInfos.FirstOrDefault();
            // 获取源地址
            string sourceAddress = taskDTO.SourceAddress;
            // 查找源地址对应的输送线设备
            IDevice? device = Storage.Devices.FirstOrDefault(x => x.DeviceProDTOs.Any(d => d.DeviceChildCode == sourceAddress));
            if (device != null)
            {
                // 将设备转换为输送线类型
                CommonConveyorLine conveyorLine = (CommonConveyorLine)device;
                conveyorLine.SetValue(ConveyorLineDBNameNew.Target, taskInfo.NextAddress, sourceAddress);
                conveyorLine.SetValue(ConveyorLineDBNameNew.TaskNo, taskInfo.TaskNum, sourceAddress);
                conveyorLine.SetValue(ConveyorLineDBNameNew.WCS_STB, 1, sourceAddress);
                // 设置输送线的目标地址
                conveyorLine.SetValue(ConveyorLineDBNameNew.Target, taskInfo.NextAddress, sourceAddress);
                // 设置输送线的任务号
                conveyorLine.SetValue(ConveyorLineDBNameNew.TaskNo, taskInfo.TaskNum, sourceAddress);
                // 触发输送线开始执行(写入 WCS_ACK = 1)
                conveyorLine.SetValue(ConveyorLineDBNameNew.WCS_ACK, (short)1, sourceAddress);
                // 更新任务状态到下一阶段
                if (_taskService.UpdateTaskStatusToNext(taskInfo).Status)
                {
                    _logger.LogInformation("HandleInboundTaskAsync:入库任务处理成功,任务号: {TaskNum}", taskInfo.TaskNum);
                    QuartzLogger.Info($"HandleInboundTaskAsync:入库任务处理成功,任务号: {taskInfo.TaskNum}", state.RobotCrane?.DeviceName ?? "Unknown");
                    return true;
                }
            }
            return false;
        }
        /// <summary>
        /// 构建库存DTO
        /// 构建库存回传 DTO
        /// </summary>
        /// <remarks>
        /// 用于拆盘和组盘操作时,向 WMS 回传库存信息。
        /// DTO 包含源货位、目标货位、托盘码以及每个位置的电池条码。
        /// </remarks>
        /// <param name="state">机器人当前状态</param>
        /// <param name="positions">电池位置数组</param>
        /// <returns>构建好的库存 DTO</returns>
        public static StockDTO BuildStockDTO(RobotSocketState state, int[] positions)
        {
            return new StockDTO
            {
                // 源输送线编号
                SourceLineNo = state.CurrentTask.RobotSourceAddressLineCode,
                // 源托盘号
                SourcePalletNo = state.CurrentTask.RobotSourceAddressPalletCode,
                // 目标托盘号
                TargetPalletNo = state.CurrentTask.RobotTargetAddressPalletCode,
                // 目标输送线编号
                TargetLineNo = state.CurrentTask.RobotTargetAddressLineCode,
                // 巷道编号(机器人名称)
                Roadway = state.CurrentTask.RobotRoadway,
                // 电池位置详情列表
                // 过滤掉位置为 0 或负数的无效数据
                // 按位置编号排序
                // 为每个位置生成对应的库存详情
                Details = positions
                    .Where(x => x > 0)
                    .OrderBy(x => x)
                    .Where(x => x > 0)  // 过滤无效位置
                    .OrderBy(x => x)   // 按位置排序
                    .Select((x, idx) => new StockDetailDTO
                    {
                        Quantity = state.RobotTaskTotalNum > 0 ? state.RobotTaskTotalNum + positions.Length : positions.Length,
                        // 数量:如果已有任务总数,使用任务总数+当前位置数;否则只使用当前位置数
                        Quantity = 1,
                        // 通道/位置编号
                        Channel = x,
                        // 电池条码:如果状态中有条码列表,取对应位置的条码;否则为空
                        CellBarcode = state.CellBarcode?.Count > 0 ? state.CellBarcode[x - 1] : ""
                    })
                    .ToList()
@@ -189,16 +404,33 @@
        }
        /// <summary>
        /// 调用拆盘API
        /// 调用拆盘 API
        /// </summary>
        /// <remarks>
        /// 当取货完成且需要拆盘时调用。
        /// 将电池从托盘上取下,逐个放置到目标位置。
        /// </remarks>
        /// <param name="stockDTO">库存 DTO,包含要拆盘的电芯信息</param>
        /// <returns>HTTP 响应结果</returns>
        public HttpResponseResult<WebResponseContent> PostSplitPalletAsync(StockDTO stockDTO)
        {
            return _httpClientHelper.Post<WebResponseContent>(nameof(ConfigKey.SplitPalletAsync), stockDTO.ToJson());
        }
        /// <summary>
        /// 调用组盘或换盘API
        /// 调用组盘/换盘 API
        /// </summary>
        /// <remarks>
        /// 当放货完成且需要组盘或换盘时调用。
        /// 将多个电池组合到同一个托盘上。
        ///
        /// configKey 参数决定调用哪个 API:
        /// - GroupPalletAsync: 组盘接口
        /// - ChangePalletAsync: 换盘接口
        /// </remarks>
        /// <param name="configKey">配置键名,决定调用哪个 API</param>
        /// <param name="stockDTO">库存 DTO,包含要组盘的电芯信息</param>
        /// <returns>HTTP 响应结果</returns>
        public HttpResponseResult<WebResponseContent> PostGroupPalletAsync(string configKey, StockDTO stockDTO)
        {
            return _httpClientHelper.Post<WebResponseContent>(configKey, stockDTO.ToJson());