wanshenmean
23 小时以前 5f1294d6dea53d286f5e7029839d37bf490e32bb
feat(任务管理): 新增组盘拆盘操作及任务创建功能

实现组盘/拆盘操作并创建入库任务的完整流程:
1. 添加 PalletOperationTaskDto 数据传输对象
2. 在 ITaskService 接口和 TaskController 中添加组盘拆盘操作方法
3. 实现组盘操作逻辑:添加库存、MES绑定、创建入库任务
4. 实现拆盘操作逻辑:MES解绑、清除货位、创建空托盘任务
5. 修复任务类型判断错误,将 OutEmpty 改为 InEmpty
6. 优化堆垛机任务查询逻辑,按目标地址分组去重
7. 注入 SqlSugarClient 以支持数据库操作
已添加1个文件
已修改6个文件
352 ■■■■■ 文件已修改
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ConveyorLineNewJob/ConveyorLineDispatchHandler.cs 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/StackerCraneJob/StackerCraneTaskSelector.cs 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_DTO/Task/PalletOperationTaskDto.cs 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_ITaskInfoService/ITaskService.cs 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService.cs 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/WCS/TaskService_Manual.cs 252 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/TaskInfo/TaskController.cs 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ConveyorLineNewJob/ConveyorLineDispatchHandler.cs
@@ -172,7 +172,7 @@
            QuartzLogHelper.LogInfo(_logger, $"RequestInNextAddress:入库下一地址,任务号: {task.TaskNum},子设备: {childDeviceCode}", conveyorLine.DeviceCode);
            bool isEmptyTask = task.TaskType == (int)TaskOutboundTypeEnum.OutEmpty;
            bool isEmptyTask = task.TaskType == (int)TaskInboundTypeEnum.InEmpty;
            // ç¡®å®šç›®æ ‡åœ°å€
            string targetAddress;
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/StackerCraneJob/StackerCraneTaskSelector.cs
@@ -170,7 +170,10 @@
            // ===== åŒ NextAddress æ— å¯ç”¨ä»»åŠ¡ï¼Œå°è¯•ä¸åŒ NextAddress çš„任务 =====
            // æŸ¥æ‰¾å…¶ä»–可用的出库站台
            // æŸ¥è¯¢å…¶ä»–站台的出库任务
            var tasks = _taskService.QueryStackerCraneOutTasks(deviceCode, new List<string> { candidateTask.NextAddress }, false);
            var tasks = _taskService.QueryStackerCraneOutTasks(deviceCode, new List<string> { candidateTask.NextAddress }, false)
                .GroupBy(x => x.TargetAddress)
                .Select(g => g.FirstOrDefault())
                .ToList();
            foreach (var alternativeTask in tasks)
            {
                selectedTask = TrySelectOutboundTask(alternativeTask);
Code/WMS/WIDESEA_WMSServer/WIDESEA_DTO/Task/PalletOperationTaskDto.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,70 @@
using System.Text.Json.Serialization;
namespace WIDESEA_DTO.Task
{
    /// <summary>
    /// ç»„盘/拆盘操作任务Dto
    /// </summary>
    public class PalletOperationTaskDto
    {
        /// <summary>
        /// ç”µèŠ¯åˆ—è¡¨ï¼ˆç»„ç›˜æ—¶å¿…ä¼ ï¼Œæ‹†ç›˜æ—¶ä¸ä¼ ï¼‰
        /// </summary>
        [JsonPropertyName("cells")]
        public List<PalletCellItem> Cells { get; set; }
        /// <summary>
        /// æ‰˜ç›˜å·
        /// </summary>
        [JsonPropertyName("palletCode")]
        public string PalletCode { get; set; }
        /// <summary>
        /// æ‰§è¡ŒåŠ¨ä½œï¼šç»„ç›˜ æˆ– æ‹†ç›˜
        /// </summary>
        [JsonPropertyName("action")]
        public string Action { get; set; }
        /// <summary>
        /// çº¿ä½“编号(作为任务起点地址 sourceAddress)
        /// </summary>
        [JsonPropertyName("lineId")]
        public string LineId { get; set; }
        /// <summary>
        /// æœºæ¢°æ‰‹åç§°ï¼ˆç”¨äºŽMES设备配置查询)
        /// </summary>
        [JsonPropertyName("robotName")]
        public string RobotName { get; set; }
        /// <summary>
        /// ä»“库编号(用于查询 targetAddress å’Œå··é“)
        /// </summary>
        [JsonPropertyName("warehouseCode")]
        public string WarehouseCode { get; set; }
        /// <summary>
        /// ä¼˜å…ˆçº§ï¼Œé»˜è®¤1
        /// </summary>
        [JsonPropertyName("grade")]
        public int Grade { get; set; } = 1;
    }
    /// <summary>
    /// ç”µèŠ¯é¡¹ï¼ˆç”µèŠ¯ç +通道号一一对应)
    /// </summary>
    public class PalletCellItem
    {
        /// <summary>
        /// ç”µèŠ¯ç 
        /// </summary>
        [JsonPropertyName("sfcCode")]
        public string SfcCode { get; set; }
        /// <summary>
        /// é€šé“号
        /// </summary>
        [JsonPropertyName("channel")]
        public string Channel { get; set; }
    }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_ITaskInfoService/ITaskService.cs
@@ -174,6 +174,13 @@
        /// <returns>批量下发结果</returns>
        Task<WebResponseContent> DispatchTasksToWCSAsync(List<DispatchTaskDto> dtos);
        /// <summary>
        /// ç»„盘/拆盘操作并创建入库任务
        /// </summary>
        /// <param name="dto">组盘/拆盘操作参数</param>
        /// <returns>操作结果</returns>
        Task<WebResponseContent> PalletOperationAndCreateTaskAsync(PalletOperationTaskDto dto);
        #region æžå·åº“任务模块
        /// <summary>
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService.cs
@@ -42,6 +42,7 @@
        private readonly IMESDeviceConfigService _mesDeviceConfigService;
        private readonly IMesLogService _mesLogService;
        private readonly IMesUploadHelper _mesUploadHelper;
        private readonly ISqlSugarClient _sqlSugarClient;
        public IRepository<Dt_Task> Repository => BaseDal;
@@ -69,7 +70,8 @@
            IRecordService recordService,
            IMESDeviceConfigService mesDeviceConfigService,
            IMesLogService mesLogService,
            IMesUploadHelper mesUploadHelper) : base(BaseDal)
            IMesUploadHelper mesUploadHelper,
            ISqlSugarClient sqlSugarClient) : base(BaseDal)
        {
            _mapper = mapper;
            _stockInfoService = stockInfoService;
@@ -85,6 +87,7 @@
            _mesDeviceConfigService = mesDeviceConfigService;
            _mesLogService = mesLogService;
            _mesUploadHelper = mesUploadHelper;
            _sqlSugarClient = sqlSugarClient;
        }
        /// <summary>
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/WCS/TaskService_Manual.cs
@@ -1,9 +1,13 @@
using System.ComponentModel;
using System.Reflection;
using Newtonsoft.Json;
using SqlSugar;
using WIDESEA_Common.Constants;
using WIDESEA_Common.StockEnum;
using WIDESEA_Common.TaskEnum;
using WIDESEA_Core;
using WIDESEA_Core.Helper;
using WIDESEA_DTO.MES;
using WIDESEA_DTO.Task;
using WIDESEA_Model.Models;
@@ -328,6 +332,254 @@
            return descAttr?.Description ?? taskStatus.ToString();
        }
        /// <summary>
        /// ç»„盘/拆盘操作并创建入库任务,完成后下发WCS
        /// </summary>
        /// <param name="dto">组盘/拆盘操作参数</param>
        /// <returns>操作结果</returns>
        public async Task<WebResponseContent> PalletOperationAndCreateTaskAsync(PalletOperationTaskDto dto)
        {
            try
            {
                // 1. å‚数校验
                if (string.IsNullOrWhiteSpace(dto.PalletCode))
                    return WebResponseContent.Instance.Error("托盘号不能为空");
                if (string.IsNullOrWhiteSpace(dto.Action)
                    || (dto.Action != "组盘" && dto.Action != "拆盘"))
                    return WebResponseContent.Instance.Error("执行动作必须是'组盘'或'拆盘'");
                if (string.IsNullOrWhiteSpace(dto.LineId))
                    return WebResponseContent.Instance.Error("线体编号不能为空");
                if (string.IsNullOrWhiteSpace(dto.WarehouseCode))
                    return WebResponseContent.Instance.Error("仓库编号不能为空");
                if (string.IsNullOrWhiteSpace(dto.RobotName))
                    return WebResponseContent.Instance.Error("机械手名称不能为空");
                // ç»„盘时电芯列表必传
                if (dto.Action == "组盘" && (dto.Cells == null || !dto.Cells.Any()))
                    return WebResponseContent.Instance.Error("组盘时电芯列表不能为空");
                // 2. æ ¹æ®ä»“库编号查询仓库信息,获取 WarehouseId、targetAddress å’Œ roadway
                var warehouse = _sqlSugarClient.Queryable<Dt_Warehouse>()
                    .First(w => w.WarehouseCode == dto.WarehouseCode);
                if (warehouse == null)
                    return WebResponseContent.Instance.Error($"未找到仓库编号为[{dto.WarehouseCode}]的仓库");
                int warehouseId = warehouse.WarehouseId;
                string targetAddress = warehouse.WarehouseCode;
                string roadway = warehouse.WarehouseCode;
                // 3. æ ¹æ®åŠ¨ä½œç±»åž‹æ‰§è¡Œä¸åŒæµç¨‹
                if (dto.Action == "组盘")
                    return await ExecuteGroupPalletAsync(dto, warehouseId, targetAddress, roadway);
                return await ExecuteSplitPalletAsync(dto, warehouseId, targetAddress, roadway);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"组盘/拆盘操作异常: {ex.Message}");
            }
        }
        /// <summary>
        /// æ‰§è¡Œç»„盘操作:添加库存 â†’ ç›´æŽ¥è°ƒç”¨MES绑定 â†’ åˆ›å»ºå…¥åº“任务 â†’ ä¸‹å‘WCS
        /// </summary>
        private async Task<WebResponseContent> ExecuteGroupPalletAsync(
            PalletOperationTaskDto dto, int warehouseId, string targetAddress, string roadway)
        {
            return await _unitOfWorkManage.BeginTranAsync(async () =>
            {
                // 1. åˆ›å»ºåº“存主记录(不绑定货位)
                var stockInfo = new Dt_StockInfo
                {
                    PalletCode = dto.PalletCode,
                    PalletType = 0,
                    WarehouseId = warehouseId,
                    LocationId = 0,
                    StockStatus = 0,
                    MesUploadStatus = 0
                };
                var stockId = await _stockInfoService.Repository.AddDataAsync(stockInfo);
                if (stockId <= 0)
                    return WebResponseContent.Instance.Error("创建库存记录失败");
                // 2. æ‰¹é‡åˆ›å»ºåº“存明细
                var details = dto.Cells.Select((cell, i) => new Dt_StockInfoDetail
                {
                    StockId = (int)stockId,
                    MaterielCode = cell.SfcCode,
                    MaterielName = "",
                    OrderNo = "",
                    ProductionDate = DateTime.Now.ToString("yyyy-MM-dd"),
                    EffectiveDate = "",
                    SerialNumber = cell.SfcCode,
                    StockQuantity = 1,
                    OutboundQuantity = 0,
                    Status = 0,
                    InboundOrderRowNo = Convert.ToInt32(cell.Channel),
                    Remark = cell.Channel
                }).ToList();
                if (await _sqlSugarClient.Insertable(details).ExecuteCommandAsync() <= 0)
                    return WebResponseContent.Instance.Error("创建库存明细失败");
                // 3. è°ƒç”¨MES绑定
                var (equipmentCode, resourceCode, token) = ResolveMesConfig(dto.RobotName);
                var bindRequest = new BindContainerRequest
                {
                    ContainerCode = dto.PalletCode,
                    EquipmentCode = equipmentCode,
                    ResourceCode = resourceCode,
                    LocalTime = DateTime.Now,
                    OperationType = StockConstants.MES_BIND_OPERATION_TYPE,
                    ContainerSfcList = dto.Cells.Select((cell, i) => new ContainerSfcItem
                    {
                        Sfc = cell.SfcCode,
                        Location = cell.Channel
                    }).ToList()
                };
                var mesResult = string.IsNullOrWhiteSpace(token)
                    ? _mesService.BindContainer(bindRequest)
                    : _mesService.BindContainer(bindRequest, token);
                if (!CheckMesResult(mesResult, out var mesError))
                    return WebResponseContent.Instance.Error($"MES组盘绑定失败: {mesError}");
                // 4. åˆ›å»ºå…¥åº“任务并下发WCS
                return await CreateTaskAndDispatchAsync(dto, warehouseId, targetAddress, roadway,
                    TaskInboundTypeEnum.Inbound, "组盘成功并创建入库任务");
            });
        }
        /// <summary>
        /// æ‰§è¡Œæ‹†ç›˜æ“ä½œï¼šæŸ¥è¯¢åº“å­˜ â†’ ç›´æŽ¥è°ƒç”¨MES解绑 â†’ æ¸…除库存货位 â†’ åˆ›å»ºç©ºæ‰˜ç›˜å…¥åº“任务 â†’ ä¸‹å‘WCS
        /// </summary>
        private async Task<WebResponseContent> ExecuteSplitPalletAsync(
            PalletOperationTaskDto dto, int warehouseId, string targetAddress, string roadway)
        {
            return await _unitOfWorkManage.BeginTranAsync(async () =>
            {
                // 1. å¯¼èˆªæŸ¥è¯¢åº“存及明细
                var stockInfo = await _stockInfoService.Repository
                    .QueryDataNavFirstAsync(s => s.PalletCode == dto.PalletCode);
                if (stockInfo == null)
                    return WebResponseContent.Instance.Error($"未找到托盘[{dto.PalletCode}]的库存记录");
                var sfcList = stockInfo.Details?.Select(d => d.SerialNumber).ToList()
                    ?? new List<string>();
                if (!sfcList.Any())
                    return WebResponseContent.Instance.Error($"托盘[{dto.PalletCode}]下无电芯数据");
                // 2. è°ƒç”¨MES解绑
                var (equipmentCode, resourceCode, token) = ResolveMesConfig(dto.RobotName);
                var unbindRequest = new UnBindContainerRequest
                {
                    EquipmentCode = equipmentCode,
                    ResourceCode = resourceCode,
                    LocalTime = DateTime.Now,
                    ContainCode = dto.PalletCode,
                    SfcList = sfcList
                };
                var mesResult = string.IsNullOrWhiteSpace(token)
                    ? _mesService.UnBindContainer(unbindRequest)
                    : _mesService.UnBindContainer(unbindRequest, token);
                if (!CheckMesResult(mesResult, out var mesError))
                    return WebResponseContent.Instance.Error($"MES拆盘解绑失败: {mesError}");
                // 3. æ¸…除库存绑定的货位
                stockInfo.LocationId = 0;
                stockInfo.LocationCode = null;
                await _sqlSugarClient.Updateable(stockInfo)
                    .UpdateColumns(s => new { s.LocationId, s.LocationCode })
                    .ExecuteCommandAsync();
                // 4. åˆ›å»ºç©ºæ‰˜ç›˜å…¥åº“任务并下发WCS
                return await CreateTaskAndDispatchAsync(dto, warehouseId, targetAddress, roadway,
                    TaskInboundTypeEnum.InEmpty, "拆盘成功并创建空托盘入库任务");
            });
        }
        /// <summary>
        /// è§£æžMES设备配置,返回 (equipmentCode, resourceCode, token)
        /// </summary>
        private (string equipmentCode, string resourceCode, string token) ResolveMesConfig(string deviceName)
        {
            var config = _mesDeviceConfigService.GetByDeviceName(deviceName);
            return (
                config?.EquipmentCode ?? StockConstants.MES_EQUIPMENT_CODE,
                config?.ResourceCode ?? StockConstants.MES_RESOURCE_CODE,
                config?.Token
            );
        }
        /// <summary>
        /// æ£€æŸ¥MES返回结果,失败时通过 error è¾“出错误信息
        /// </summary>
        private bool CheckMesResult(HttpResponseResult<MesResponse> result, out string error)
        {
            if (result?.Data != null && result.Data.IsSuccess)
            {
                error = null;
                return true;
            }
            error = result?.Data?.Msg ?? result?.ErrorMessage ?? "未知错误";
            return false;
        }
        /// <summary>
        /// åˆ›å»ºå…¥åº“任务并下发WCS
        /// </summary>
        private async Task<WebResponseContent> CreateTaskAndDispatchAsync(
            PalletOperationTaskDto dto, int warehouseId, string targetAddress, string roadway,
            TaskInboundTypeEnum taskType, string successMessage)
        {
            int taskNum = await BaseDal.GetTaskNo();
            var task = new Dt_Task
            {
                TaskNum = taskNum,
                PalletCode = dto.PalletCode,
                SourceAddress = dto.LineId,
                TargetAddress = targetAddress,
                TaskType = taskType.GetHashCode(),
                TaskStatus = TaskInStatusEnum.InNew.GetHashCode(),
                Grade = dto.Grade,
                Roadway = roadway,
                WarehouseId = warehouseId,
                CurrentAddress = dto.LineId,
                NextAddress = targetAddress,
                Creater = "manual",
                CreateDate = DateTime.Now,
                ModifyDate = DateTime.Now
            };
            if (await BaseDal.AddDataAsync(task) <= 0)
                return WebResponseContent.Instance.Error("创建任务失败");
            var wmsTaskDto = new WMSTaskDTO
            {
                TaskNum = task.TaskNum,
                PalletCode = task.PalletCode,
                SourceAddress = task.SourceAddress,
                TargetAddress = task.TargetAddress,
                TaskType = task.TaskType,
                Roadway = task.Roadway,
                TaskStatus = task.TaskStatus,
                WarehouseId = task.WarehouseId
            };
            var wcsResult = _httpClientHelper.Post<WebResponseContent>(
                "http://localhost:9292/api/Task/ReceiveManualTask",
                new List<WMSTaskDTO> { wmsTaskDto }.ToJson());
            if (!wcsResult.IsSuccess || !wcsResult.Data.Status)
                return WebResponseContent.Instance.Error(
                    $"任务已创建但发送给WCS失败: {wcsResult.ErrorMessage}\r\n {wcsResult.Data?.Message}");
            return WebResponseContent.Instance.OK($"{successMessage},任务号: {taskNum}");
        }
        #endregion æ‰‹åŠ¨ä»»åŠ¡
    }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/TaskInfo/TaskController.cs
@@ -69,6 +69,17 @@
        }
        /// <summary>
        /// ç»„盘/拆盘操作并创建入库任务
        /// </summary>
        /// <param name="dto">组盘/拆盘操作参数</param>
        /// <returns>操作结果</returns>
        [HttpGet, HttpPost, Route("PalletOperation"), AllowAnonymous]
        public async Task<WebResponseContent?> PalletOperationAsync([FromBody] PalletOperationTaskDto dto)
        {
            return await Service.PalletOperationAndCreateTaskAsync(dto);
        }
        /// <summary>
        /// èŽ·å–å¯å…¥åº“è´§ä½
        /// </summary>
        /// <param name="taskDto"></param>