using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using WIDESEA_Core;
using WIDESEAWCS_Common;
using WIDESEAWCS_Common.HttpEnum;
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.Workflow.Abstractions;
namespace WIDESEAWCS_Tasks
{
///
/// 机器人任务处理器 - 负责任务获取、下发、入库任务回传及库存 DTO 构建
///
///
/// 核心职责:
/// 1. 从数据库轮询待处理的机器人任务
/// 2. 向机器人客户端下发取货指令(Pickbattery)
/// 3. 处理入库任务的回传(拆盘/组盘/换盘场景)
/// 4. 构建库存回传 DTO 并调用 WMS 接口
///
/// 通过网关访问 Socket,避免业务层直接依赖 TcpSocketServer。
///
public class RobotTaskProcessor
{
///
/// Socket 客户端网关接口
///
///
/// 通过网关访问 Socket,避免业务层直接依赖 TcpSocketServer。
/// 提供统一的客户端通信接口。
///
private readonly ISocketClientGateway _socketClientGateway;
///
/// 机械手状态管理器
///
private readonly RobotStateManager _stateManager;
///
/// 机器人任务服务
///
///
/// 用于查询、更新、删除机器人任务记录。
///
private readonly IRobotTaskService _robotTaskService;
///
/// 通用任务服务
///
///
/// 用于与 WMS 系统交互,接收任务、处理任务状态等。
///
private readonly ITaskService _taskService;
///
/// HTTP 客户端帮助类
///
///
/// 用于调用 WMS 系统的 HTTP 接口。
///
private readonly HttpClientHelper _httpClientHelper;
///
/// 日志记录器
///
private readonly ILogger _logger;
///
/// 构造函数
///
/// Socket 网关
/// 状态管理器
/// 机器人任务服务
/// 通用任务服务
/// HTTP 客户端帮助类
/// 日志记录器
public RobotTaskProcessor(
ISocketClientGateway socketClientGateway,
RobotStateManager stateManager,
IRobotTaskService robotTaskService,
ITaskService taskService,
HttpClientHelper httpClientHelper,
ILogger logger)
{
_socketClientGateway = socketClientGateway;
_stateManager = stateManager;
_robotTaskService = robotTaskService;
_taskService = taskService;
_httpClientHelper = httpClientHelper;
_logger = logger;
}
///
/// 按设备编码获取当前机器人的待处理任务
///
///
/// 从数据库中查询指定设备编码的待处理机器人任务。
/// 只返回状态为"待处理"的任务。
///
/// 机器人设备信息,包含设备编码
/// 待处理的任务对象,如果没有则返回 null
public Dt_RobotTask? GetTask(RobotCraneDevice robotCrane)
{
return _robotTaskService.QueryRobotCraneTask(robotCrane.DeviceCode);
}
///
/// 按设备编码获取当前机器人的执行中任务
///
///
/// 从数据库中查询指定设备编码的执行中机器人任务。
/// 当RobotArmObject为1(有物料)且没有待处理任务时调用。
///
/// 机器人设备信息,包含设备编码
/// 执行中的任务对象,如果没有则返回 null
public Dt_RobotTask? GetExecutingTask(RobotCraneDevice robotCrane)
{
return _robotTaskService.QueryRobotCraneExecutingTask(robotCrane.DeviceCode);
}
///
/// 删除机器人任务
///
///
/// 当任务完成(无论是成功还是失败)时调用,删除数据库中的任务记录。
///
/// 要删除的任务 ID
/// 删除是否成功
public bool? DeleteTask(int ID)
{
return _robotTaskService.Repository.DeleteDataById(ID);
}
///
/// 下发取货指令(Pickbattery)到机器人客户端
///
///
/// 发送格式:Pickbattery,{源地址}
/// 例如:Pickbattery,A01 表示从 A01 位置取货
///
/// 下发成功后:
/// 1. 更新任务状态为"机器人执行中"
/// 2. 将任务关联到状态对象
/// 3. 安全更新状态到 Redis
/// 4. 更新任务记录到数据库
///
/// 要下发的任务对象
/// 机器人当前状态
public async Task SendSocketRobotPickAsync(Dt_RobotTask task, RobotSocketState state)
{
// 构建取货指令,格式:Pickbattery,{源地址}
string taskString = $"Pickbattery,{task.RobotSourceAddress}";
// 通过 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);
}
}
///
/// 处理入库任务回传(拆盘/组盘/换盘场景)
///
///
/// 当取货完成(AllPickFinished)或放货完成(AllPutFinished)时调用此方法。
/// 根据任务类型和地址来源决定如何回传给 WMS。
///
/// 处理逻辑:
/// 1. 根据 useSourceAddress 决定使用源地址还是目标地址
/// 2. 根据任务类型(组盘/换盘/拆盘)决定任务类型(入库/空托盘入库)
/// 3. 构建 CreateTaskDto 并调用 WMS 接口创建任务
/// 4. 接收 WMS 返回的任务信息
/// 5. 更新输送线的目标地址、任务号等
///
/// 机器人当前状态
/// 是否使用源地址(true 表示拆盘/换盘场景,false 表示组盘/换盘场景)
/// 处理是否成功
public async Task 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;
// 根据巷道名称判断仓库 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; // 使用源地址的托盘码
break;
}
}
else
{
// 使用目标地址的场景:组盘、换盘(成品入库)
switch (robotTaskType)
{
case RobotTaskTypeEnum.ChangePallet:
case RobotTaskTypeEnum.GroupPallet:
// 换盘/组盘场景:货物需要入库
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, // 仓库 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(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(result.Data.Data.ToJson() ?? string.Empty) ?? new WMSTaskDTO();
// 调用任务服务接收 WMS 任务
var content = _taskService.ReceiveWMSTask(new List { taskDTO });
if (!content.Status)
{
_logger.LogError("HandleInboundTaskAsync:接收WMS任务失败");
QuartzLogger.Error($"HandleInboundTaskAsync:接收WMS任务失败", state.RobotCrane?.DeviceName ?? "Unknown");
return false;
}
// 解析返回的任务信息
var taskInfos = JsonConvert.DeserializeObject>(content.Data.ToJson() ?? string.Empty) ?? new List();
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);
// 触发输送线开始执行(写入 WCS_STB = 1)
conveyorLine.SetValue(ConveyorLineDBNameNew.WCS_STB, 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;
}
///
/// 构建库存回传 DTO
///
///
/// 用于拆盘和组盘操作时,向 WMS 回传库存信息。
/// DTO 包含源货位、目标货位、托盘码以及每个位置的电池条码。
///
/// 机器人当前状态
/// 电池位置数组
/// 构建好的库存 DTO
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) // 按位置排序
.Select((x, idx) => new StockDetailDTO
{
// 数量:如果已有任务总数,使用任务总数+当前位置数;否则只使用当前位置数
Quantity = 1,
// 通道/位置编号
Channel = x,
// 电池条码:如果状态中有条码列表,取对应位置的条码;否则为空
CellBarcode = state.CellBarcode?.Count > 0 ? state.CellBarcode[x - 1] : ""
})
.ToList()
};
}
///
/// 调用拆盘 API
///
///
/// 当取货完成且需要拆盘时调用。
/// 将电池从托盘上取下,逐个放置到目标位置。
///
/// 库存 DTO,包含要拆盘的电芯信息
/// HTTP 响应结果
public HttpResponseResult PostSplitPalletAsync(StockDTO stockDTO)
{
return _httpClientHelper.Post(nameof(ConfigKey.SplitPalletAsync), stockDTO.ToJson());
}
///
/// 调用组盘/换盘 API
///
///
/// 当放货完成且需要组盘或换盘时调用。
/// 将多个电池组合到同一个托盘上。
///
/// configKey 参数决定调用哪个 API:
/// - GroupPalletAsync: 组盘接口
/// - ChangePalletAsync: 换盘接口
///
/// 配置键名,决定调用哪个 API
/// 库存 DTO,包含要组盘的电芯信息
/// HTTP 响应结果
public HttpResponseResult PostGroupPalletAsync(string configKey, StockDTO stockDTO)
{
return _httpClientHelper.Post(configKey, stockDTO.ToJson());
}
}
}