xiazhengtongxue
3 天以前 0f710285d9f02e3d4cea19557e17945e9ef9532b
Merge branch 'dev' of http://115.159.85.185:8098/r/SuZhouGuanHong/ShanMeiXinNengYuan into dev
已添加6个文件
已修改29个文件
1040 ■■■■ 文件已修改
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Common/HttpEnum/BaseAPI.cs 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Communicator/Siemens/SiemensDBDataType.cs 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Communicator/Siemens/SiemensS7Communicator.cs 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_ITaskInfoService/IRobotTaskService.cs 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Model/Models/RobotState/RobotSocketState.cs 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_QuartzJob/QuartzNet/SchedulerCenterServer.cs 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_RedisService/Connection/RedisConnectionManager.cs 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Server/Controllers/Task/RobotTaskController.cs 21 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Server/appsettings.json 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_TaskInfoService/Flows/OutboundTaskFlowService.cs 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_TaskInfoService/RobotTaskService.cs 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ConveyorLineNewJob/CommonConveyorLineNewJob.cs 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ConveyorLineNewJob/ConveyorLineDispatchHandler.cs 22 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ConveyorLineNewJob/ConveyorLineTargetAddressSelector.cs 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotJob.cs 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotTaskProcessor.cs 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Workflow/RobotSimpleCommandHandler.cs 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Workflow/RobotWorkflowOrchestrator.cs 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/SocketServer/TcpSocketServer.Messaging.cs 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/StackerCraneJob/CommonStackerCraneJob.cs 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/StackerCraneJob/StackerCraneTaskSelector.cs 22 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSClient/src/router/viewGird.js 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSClient/src/views/system/outboundTimeConfig.vue 109 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_BasicService/LocationInfoService.cs 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_Common/Constants/OutboundTimeConstants.cs 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_Common/Constants/TaskAddressConstants.cs 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_Core/Core/OutboundTimeConfigOptions.cs 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService.cs 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/WCS/TaskService_Inbound.cs 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/System/OutboundTimeConfigController.cs 94 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Program.cs 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/WIDESEA_WMSServer.csproj 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/outbound_time_config.json 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/docs/superpowers/plans/2026-04-29-outbound-task-flow-todo-implementation.md 170 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/docs/superpowers/specs/2026-04-29-outbound-task-flow-todo-design.md 148 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Common/HttpEnum/BaseAPI.cs
@@ -11,7 +11,7 @@
        /// <summary>
        /// WMS接口基础URL
        /// </summary>
        public const string WMSBaseUrl = "http://localhost:9291/api/";
        public const string WMSBaseUrl = "http://192.168.60.30:9291/api/";
        /// <summary>
        /// WCS接口基础URL
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Communicator/Siemens/SiemensDBDataType.cs
@@ -112,6 +112,7 @@
                DataType_Char => TypeCode.Char,
                DataType_UInt => TypeCode.UInt16,
                DataType_UDInt => TypeCode.UInt32,
                DataType_ByteArray => TypeCode.SByte,
                _ => throw new CommunicationException($"数据类型错误:【{dataType}】", CommunicationErrorType.TypeError),
            };
        }
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Communicator/Siemens/SiemensS7Communicator.cs
@@ -358,6 +358,9 @@
                            return Encoding.Default.GetString((byte[])GetContent(plc.Read(address, length), address)).ToArray();
                        return (char)GetContent(plc.ReadByte(address), address);
                    case TypeCode.SByte:
                        return Encoding.Default.GetString((byte[])GetContent(plc.Read(address, 20), address)).ToArray();
                    default:
                        throw new CommunicationException(string.Format(CommunicationExceptionMessage.DataTypeErrorException, typeCode.ToString(), address), CommunicationErrorType.TypeError);
                }
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_ITaskInfoService/IRobotTaskService.cs
@@ -83,12 +83,28 @@
        WebResponseContent GetWMSRobotTask(Dt_Task task);
        /// <summary>
        ///
        /// </summary>
        /// <param name="taskType"></param>
        /// <param name="palletCode"></param>
        /// <returns></returns>
        int GetRobotTaskTotalNum(int taskType, string? palletCode);
        /// <summary>
        /// åœ¨æœ¬åœ°ç›´æŽ¥åˆ›å»ºæœºæ¢°æ‰‹ä»»åŠ¡ï¼Œä¸è°ƒç”¨WMS接口
        /// </summary>
        /// <param name="task">出库任务</param>
        /// <returns>操作结果</returns>
        WebResponseContent CreateLocalRobotTask(Dt_Task task);
        /// <summary>
        /// æ£€æŸ¥æºçº¿ä½“是否有托盘号,并根据结果创建机械手任务。
        /// æœ‰æ‰˜ç›˜å·æ—¶è°ƒç”¨ CreateLocalRobotTask,无托盘号时从 WMS èŽ·å–ä»»åŠ¡ã€‚
        /// </summary>
        /// <param name="task">出库任务实体</param>
        /// <returns>操作结果</returns>
        WebResponseContent CheckSourceLineAndCreateRobotTask(Dt_Task task);
        int MapWarehouseIdConfigKey(string? targetAddress);
        string ResolveRobotRuleValue(string? targetAddress, string addressSectionName, string? fallback);
        WebResponseContent CreateRobotTaskManually(ManualRobotTaskDto request);
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Model/Models/RobotState/RobotSocketState.cs
@@ -127,13 +127,23 @@
        public int[]? LastPutPositions { get; set; }
        /// <summary>
        /// ç”µæ± /货位条码列表
        /// ç”µæ± /货位条码列表(累积所有批次,不重复)
        /// </summary>
        /// <remarks>
        /// åœ¨ç»„盘操作时用于记录生成的托盘条码。
        /// åœ¨ç»„盘操作时用于累积记录所有电芯条码,每次读取新条码时追加(去重)。
        /// ä»…在收到 allputfinished(全部放货完成)时清空。
        /// æ¯ä¸ªæ¡ç æ ¼å¼ä¸º "TRAY" + æ—¥æœŸ + æ—¶é—´ + éšæœºæ•°ã€‚
        /// </remarks>
        public List<string> CellBarcode { get; set; } = new List<string>();
        /// <summary>
        /// å½“前批次的电芯条码列表
        /// </summary>
        /// <remarks>
        /// æ¯æ¬¡è¯»å–新条码时设置为本批次的条码,仅用于 WMS æäº¤æ—¶æŒ‰æ‰¹æ¬¡æäº¤ã€‚
        /// æ¯æ¬¡æ–°æ‰¹æ¬¡è¯»å–时覆盖,在 allputfinished æ—¶æ¸…空。
        /// </remarks>
        public List<string> CurrentBatchBarcodes { get; set; } = new List<string>();
        /// <summary>
        /// æœºæ¢°æ‰‹å½“前正在执行的任务
@@ -217,5 +227,14 @@
        /// æ‹‰å¸¦çº¿ä¸Šç”µèŠ¯æ˜¯å¦åˆ°ä½ã€‚
        /// </remarks>
        public bool BatteryArrived { get; set; } = false;
        /// <summary>
        /// å½“前执行中的机器人任务编号
        /// </summary>
        /// <remarks>
        /// ä¸‹å‘任务时缓存任务编号,用于 RobotJob å¿«é€ŸæŸ¥æ‰¾æ‰§è¡Œä¸­çš„任务,
        /// é¿å…æ¯æ¬¡è½®è¯¢å…¨è¡¨æ‰«æã€‚任务完成时清空为 null。
        /// </remarks>
        public int? CurrentTaskNum { get; set; }
    }
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_QuartzJob/QuartzNet/SchedulerCenterServer.cs
@@ -459,11 +459,14 @@
        /// <returns></returns>
        private ITrigger CreateSimpleTrigger(DispatchInfoDTO sysSchedule)
        {
            // Quartz要求间隔至少1秒,防止数据库中IntervalSecond为0或负值导致ArgumentOutOfRangeException
            var intervalSeconds = sysSchedule.IntervalSecond <= 0 ? 1 : sysSchedule.IntervalSecond;
            ITrigger trigger = TriggerBuilder.Create()
            .WithIdentity(sysSchedule.Id.ToString(), sysSchedule.JobGroup)
            .StartAt(sysSchedule.BeginTime.GetValueOrDefault())
            .WithSimpleSchedule(x => x
                .WithIntervalInSeconds(sysSchedule.IntervalSecond)
                .WithIntervalInSeconds(intervalSeconds)
                .RepeatForever()
            )
            .EndAt(sysSchedule.EndTime.GetValueOrDefault())
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_RedisService/Connection/RedisConnectionManager.cs
@@ -82,6 +82,8 @@
        public IServer GetServer()
        {
            var endpoints = _connection.Value.GetEndPoints();
            if (endpoints == null || endpoints.Length == 0)
                throw new InvalidOperationException("Redis没有可用的终结点,请检查连接配置");
            return _connection.Value.GetServer(endpoints[0]);
        }
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Server/Controllers/Task/RobotTaskController.cs
@@ -1,14 +1,8 @@
using Autofac.Core;
using Masuit.Tools;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
using WIDESEAWCS_Common.TaskEnum;
using WIDESEAWCS_Core;
using WIDESEAWCS_Core.BaseController;
using WIDESEAWCS_Core.Enums;
using WIDESEAWCS_DTO.TaskInfo;
using WIDESEAWCS_ISystemServices;
using WIDESEAWCS_ITaskInfoService;
using WIDESEAWCS_Model.Models;
@@ -20,22 +14,29 @@
    {
        public RobotTaskController(IRobotTaskService service) : base(service)
        {
        }
        [HttpGet, HttpPost, Route("DeleteRobotTask"), AllowAnonymous]
        public WebResponseContent DeleteRobotTask(int id)
        {
            if (Service.DeleteRobotTask(id)){
            if (Service.DeleteRobotTask(id))
            {
                return WebResponseContent.Instance.OK();
            }
            return WebResponseContent.Instance.Error();
        }
        [HttpGet, HttpPost, Route("GetRobotTaskTotalNum"), AllowAnonymous]
        public int GetRobotTaskTotalNum(int taskType, string? palletCode)
        {
            return Service.GetRobotTaskTotalNum(taskType, palletCode);
        }
        // æ‰‹åŠ¨æœºæ¢°æ‰‹ä»»åŠ¡
        [HttpGet, HttpPost, Route("CreateRobotTaskManually"), AllowAnonymous]
        public WebResponseContent CreateRobotTaskManually([FromBody] ManualRobotTaskDto request)
        public WebResponseContent CreateRobotTaskManually([FromBody] ManualRobotTaskDto request)
        {
            return Service.CreateRobotTaskManually(request);
        }
    }
}
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Server/appsettings.json
@@ -34,7 +34,7 @@
  //5.PostgreSQL
  "DBType": "SqlServer",
  //连接字符串
  "ConnectionString": "Data Source=.;Initial Catalog=WIDESEAWCS_ShanMei;User ID=sa;Password=P@ssw0rd;Integrated Security=False;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False",
  "ConnectionString": "Data Source=192.168.60.30;Initial Catalog=WIDESEAWCS_ShanMei;User ID=sa;Password=P@ssw0rd;Integrated Security=False;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False",
  //"ConnectionString": "Data Source=.;Initial Catalog=WIDESEAWCS_ShanMei;User ID=sa;Password=123456;Integrated Security=False;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False",
  //跨域
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_TaskInfoService/Flows/OutboundTaskFlowService.cs
@@ -107,13 +107,17 @@
            if (task.TaskStatus == (int)TaskOutStatusEnum.Line_OutFinish && task.TaskType == (int)TaskOutboundTypeEnum.Outbound)
            {
                return WebResponseContent.Instance.OK();
                //return WebResponseContent.Instance.OK();
                //return _robotTaskService.CheckSourceLineAndCreateRobotTask(task);
                // Todo:获取对向线体是否有托盘号,如果有托盘号直接生成机械手任务
                return GetWMSOutboundTrayTask(task);
            }
            if (task.TaskStatus == (int)TaskOutStatusEnum.Line_OutFinish && task.TaskType == (int)TaskOutboundTypeEnum.OutEmpty)
            {
               return _robotTaskService.CreateLocalRobotTask(task);
                return _robotTaskService.CreateLocalRobotTask(task);
                //if (!content.Status)
                //{
                //    return content;
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_TaskInfoService/RobotTaskService.cs
@@ -22,10 +22,12 @@
using Masuit.Tools;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Serilog;
using SqlSugar;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using WIDESEA_Core;
using WIDESEAWCS_Common;
using WIDESEAWCS_Common.HttpEnum;
@@ -75,6 +77,7 @@
            _taskExecuteDetailService = taskExecuteDetailService;
            _logger = logger;
        }
        public override WebResponseContent DeleteData(object[] keys)
        {
            List<int> taskKeys = new List<int>();
@@ -85,8 +88,8 @@
            List<Dt_RobotTask> tasks = BaseDal.QueryData(x => taskKeys.Contains(x.RobotTaskId));
            BaseDal.DeleteAndMoveIntoHty(tasks, OperateTypeEnum.人工删除);
            return WebResponseContent.Instance.OK($"成功删除{tasks.Count}条数据");
        }
        public bool DeleteRobotTask(int id)
        {
            Dt_RobotTask task = BaseDal.QueryFirst(x => x.RobotTaskId == id);
@@ -135,7 +138,7 @@
        public Dt_RobotTask? QueryRobotCraneTask(string deviceCode)
        {
            return BaseDal.QueryFirst(x => x.RobotRoadway == deviceCode && x.RobotTaskState != (int)TaskRobotStatusEnum.RobotExecuting, TaskOrderBy);
            return BaseDal.QueryFirst(x => x.RobotRoadway == deviceCode, TaskOrderBy);
        }
        public Dt_RobotTask? QueryRobotCraneExecutingTask(string deviceCode)
@@ -242,10 +245,108 @@
        }
        /// <summary>
        /// æ£€æŸ¥æºçº¿ä½“是否有托盘号,并根据结果创建机械手任务。
        /// </summary>
        /// <param name="task">出库任务实体</param>
        /// <returns>
        /// æœ‰æ‰˜ç›˜å·æ—¶è¿”回 CreateLocalRobotTask ç»“果;
        /// æ— æ‰˜ç›˜å·æ—¶è¿”回 GetWMSOutboundTrayTask ç»“果。
        /// </returns>
        public WebResponseContent CheckSourceLineAndCreateRobotTask(Dt_Task task)
        {
            // 1. èŽ·å–æºçº¿ä½“ç¼–å·ï¼ˆå¤ç”¨å·²æœ‰é€»è¾‘ï¼‰
            string configKey = ResolveRobotTaskConfigKey(task.TargetAddress);
            StockDTO stock = BuildRobotTaskStock(task, configKey);
            string sourceLineNo = stock.SourceLineNo;
            if (string.IsNullOrWhiteSpace(sourceLineNo))
            {
                return GetWMSOutboundTrayTaskFromWMS(task);
            }
            // 2. é€šè¿‡è®¾å¤‡é€šä¿¡è¯»å–线体托盘号
            string? palletCode = ReadLineBarcode(sourceLineNo);
            if (!string.IsNullOrWhiteSpace(palletCode))
            {
                // æœ‰æ‰˜ç›˜å·ï¼Œæœ¬åœ°åˆ›å»ºæœºæ¢°æ‰‹ä»»åŠ¡
                return CreateLocalRobotTask(task);
            }
            // æ— æ‰˜ç›˜å·ï¼Œä»Ž WMS èŽ·å–ä»»åŠ¡
            return GetWMSOutboundTrayTaskFromWMS(task);
        }
        /// <summary>
        /// ä»ŽWMS获取空托盘出库任务。
        /// </summary>
        /// <param name="task">任务实体。</param>
        /// <returns>调用结果。</returns>
        private WebResponseContent GetWMSOutboundTrayTaskFromWMS(Dt_Task task)
        {
            int warehouseId = MapWarehouseIdConfigKey(task.TargetAddress);
            string sourceLineNo = ResolveRobotRuleValue(task.TargetAddress, "AddressSourceLineNoMap", task.TargetAddress);
            string configKey = nameof(ConfigKey.GetOutBoundTrayTaskAsync);
            string requestParam = new CreateTaskDto { WarehouseId = warehouseId, TargetAddress = sourceLineNo }.ToJson();
            DateTime startTime = DateTime.Now;
            var result = _httpClientHelper.Post<WebResponseContent>(configKey, requestParam);
            if (!result.IsSuccess || !result.Data.Status)
            {
                QuartzLogHelper.LogError(_logger, $"调用WMS接口失败,接口:【{configKey}】,请求参数:【{requestParam}】,错误信息:【{result.Data?.Message}】", "RobotTaskService");
                return WebResponseContent.Instance.Error($"获取WMS系统空托盘出库任务失败,任务号:【{task.TaskNum}】,托盘号:【{task.PalletCode}】,错误信息:【{result.Data?.Message}】");
            }
            QuartzLogHelper.LogInfo(_logger, $"调用WMS接口成功,接口:【{configKey}】,响应数据:【{result.Data?.Data}】,耗时:{(DateTime.Now - startTime).TotalMilliseconds}ms", "RobotTaskService");
            WMSTaskDTO? wMSTask = JsonConvert.DeserializeObject<WMSTaskDTO>(result.Data.Data?.ToString() ?? string.Empty);
            if (wMSTask == null)
                return WebResponseContent.Instance.Error($"获取WMS系统空托盘出库任务失败,任务号:【{task.TaskNum}】,托盘号:【{task.PalletCode}】,错误信息:【WMS未返回有效任务数据】");
            // æž„建StockDTO并调用ReceiveWMSTask创建本地入库任务
            var stockDto = new StockDTO
            {
                Roadway = task.Roadway,
                SourceLineNo = sourceLineNo,
                TargetLineNo = task.TargetAddress,
                SourcePalletNo = string.Empty,
                TargetPalletNo = string.Empty
            };
            return ReceiveWMSTask(wMSTask, stockDto);
        }
        /// <summary>
        /// è¯»å–指定线体的托盘号。
        /// </summary>
        /// <param name="sourceLineNo">源线体编号</param>
        /// <returns>托盘号,如有异常返回 null</returns>
        private string? ReadLineBarcode(string sourceLineNo)
        {
            try
            {
                IDevice? device = Storage.Devices.FirstOrDefault(x =>
                    x.DeviceProDTOs.Any(d => d.DeviceChildCode == sourceLineNo));
                if (device == null)
                    return null;
                CommonConveyorLine conveyorLine = (CommonConveyorLine)device;
                return conveyorLine.GetValue<ConveyorLineDBNameNew, string>(
                    ConveyorLineDBNameNew.Barcode, sourceLineNo);
            }
            catch (Exception ex)
            {
                _logger.Error(ex, $"读取线体[{sourceLineNo}]托盘号异常");
                return null;
            }
        }
        /// <summary>
        /// èŽ·å–æœºæ¢°æ‰‹ä»»åŠ¡æ€»æ•°é‡ã€‚
        /// ç»„盘任务固定48,换盘和拆盘任务通过托盘号查询WMS库存明细数量。
        /// </summary>
        private int GetRobotTaskTotalNum(int taskType, string? palletCode)
        public int GetRobotTaskTotalNum(int taskType, string? palletCode)
        {
            if (taskType == (int)RobotTaskTypeEnum.GroupPallet)
                return 48;
@@ -255,8 +356,10 @@
            try
            {
                QuartzLogHelper.LogInfo(_logger, $"开始调用WMS接口获取库存明细数量,托盘号:【{palletCode}】", "RobotTaskService");
                string url = $"{BaseAPI.WMSBaseUrl}Stock/GetStockDetailCount?palletCode={Uri.EscapeDataString(palletCode)}";
                var result = _httpClientHelper.Get(url);
                QuartzLogHelper.LogInfo(_logger, $"调用WMS获取库存明细数量接口,请求URL:【{url}】,响应数据:【{result.Content}】,耗时:{result.Duration}ms", "RobotTaskService");
                if (!result.IsSuccess || string.IsNullOrEmpty(result.Content))
                    return 1;
@@ -264,8 +367,8 @@
                if (response == null || !response.Status)
                    return 1;
                var detailCount = response.Data?.GetType().GetProperty("DetailCount")?.GetValue(response.Data);
                return detailCount is int count and > 0 ? count : 1;
                var detailCount = (response.Data as JObject)?["detailCount"]?.Value<int>();
                return detailCount.HasValue && detailCount.Value > 0 ? detailCount.Value : 1;
            }
            catch
            {
@@ -370,8 +473,10 @@
                    CommonConveyorLine conveyorLine = (CommonConveyorLine)device;
                    DeviceProDTO? devicePro = conveyorLine.DeviceProDTOs.FirstOrDefault(x => x.DeviceProParamName == nameof(ConveyorLineDBNameNew.Barcode) && x.DeviceChildCode == sourceLineNo);
                    //conveyorLine.Communicator.Read(devicePro.DeviceProAddress, 20);
                    //ConveyorLineTaskCommandNew command = conveyorLine.ReadCustomer<ConveyorLineTaskCommandNew>(sourceLineNo);  // æµ‹è¯•用
                    var barcode = conveyorLine.GetValue<ConveyorLineDBNameNew, string>(ConveyorLineDBNameNew.Barcode, sourceLineNo);
                    var bytes = conveyorLine.Communicator.Read(devicePro.DeviceProAddress, 20);
                    var barcode = Encoding.Default.GetString(bytes).Trim();
                    stock.SourcePalletNo = string.IsNullOrEmpty(barcode) ? string.Empty : barcode;
                }
            }
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ConveyorLineNewJob/CommonConveyorLineNewJob.cs
@@ -1,16 +1,11 @@
using MapsterMapper;
using Masuit.Tools;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using Quartz;
using Serilog;
using SqlSugar;
using WIDESEA_Core;
using WIDESEAWCS_Common.HttpEnum;
using WIDESEAWCS_Common.TaskEnum;
using WIDESEAWCS_Core;
using WIDESEAWCS_Core.Helper;
using WIDESEAWCS_DTO.TaskInfo;
using WIDESEAWCS_ITaskInfoService;
using WIDESEAWCS_Model.Models;
using WIDESEAWCS_QuartzJob;
@@ -228,7 +223,11 @@
                            {
                                // å¦‚æžœ WCS_ACK ä¸º 1,先清除(表示处理过上一次请求)
                                if (command.WCS_ACK == 1)
                                    conveyorLine.SetValue(ConveyorLineDBNameNew.WCS_ACK, (short)0, childDeviceCode);
                                {
                                    conveyorLine.SetValue(ConveyorLineDBNameNew.WCS_ACK, (byte)0, childDeviceCode);
                                    //Thread.Sleep(300);
                                    //conveyorLine.SetValue(ConveyorLineDBNameNew.Target, (short)0, childDeviceCode);
                                }
                                // å¤„理手动入库任务(起点为线体点位的任务)
                                try
@@ -247,17 +246,36 @@
                                }
                                continue;
                            }
                            else
                            {
                                if (childDeviceCode == "2103" || childDeviceCode == "2101")
                                {
                                    try
                                    {
                                        var task = _taskService.QueryManualInboundTask(childDeviceCode);
                                        if (task != null)
                                        {
                                            QuartzLogHelper.LogInfo(_logger, $"获取到输送线开始任务,任务号:{task.TaskNum},状态: {task.TaskStatus},当前地址:{conveyorLine.DeviceCode}", conveyorLine.DeviceCode);
                                            var handler = new ManualInboundTaskHandler(_taskService, _logger);
                                            handler.WriteTaskToPlc(conveyorLine, childDeviceCode, task);
                                        }
                                    }
                                    catch (Exception ex)
                                    {
                                        QuartzLogHelper.LogError(_logger, ex, "处理手动入库任务异常", $"处理手动入库任务异常: {ex.Message}", "CommonConveyorLineNewJob");
                                    }
                                }
                            }
                            // ========== å¤„理无托盘条码的情况 ==========
                            // æ— æ‰˜ç›˜æ¡ç æ—¶ï¼Œè¯·æ±‚出库任务
                            if (command.Barcode.IsNullOrEmpty() || command.Barcode.Replace("\0", "") == "")
                            // å¦‚æžœ PLC_STB ä¸º 1,但没有任务号,可能是新任务的开始,先请求出库任务(适用于无条码的情况)
                            if (command.TaskNo == 1000)
                            {
                                _conveyorLineDispatch.RequestOutbound(conveyorLine, command, childDeviceCode);
                                continue;
                            }
                            // ========== å¤„理已有任务号的情况 ==========
                            if (command.TaskNo > 0 && !command.Barcode.IsNullOrEmpty())
                            if (command.TaskNo > 0)
                            {
                                // æŸ¥è¯¢æ­£åœ¨æ‰§è¡Œçš„任务
                                Dt_Task task = _taskService.QueryExecutingConveyorLineTask(command.TaskNo, childDeviceCode);
@@ -299,48 +317,6 @@
                                        conveyorLine.SetValue(ConveyorLineDBNameNew.WCS_ACK, (short)1, childDeviceCode);
                                    }
                                }
                                //else if (!command.Barcode.IsNullOrEmpty() && (childDeviceCode == "11001" || childDeviceCode == "11010"))
                                //{
                                //    var isWcsTask = _taskService.Db.Queryable<Dt_Task>().Any(x => x.PalletCode == command.Barcode && (x.TaskStatus == (int)TaskOutStatusEnum.OutNew || x.TaskStatus == (int)TaskInStatusEnum.InNew));
                                //    var isRobotTask = _robotTaskService.Db.Queryable<Dt_RobotTask>().Any(x => x.RobotTargetAddressPalletCode == command.Barcode);
                                //    if (isWcsTask || isRobotTask)
                                //    {
                                //        continue;
                                //    }
                                //    // è°ƒç”¨ WMS åˆ›å»ºç©ºæ‰˜ç›˜å…¥åº“任务
                                //    string configKey = nameof(ConfigKey.CreateTaskInboundAsync);
                                //    string requestParam = new CreateTaskDto()
                                //    {
                                //        PalletCode = command.Barcode,
                                //        SourceAddress = childDeviceCode,
                                //        TargetAddress = "GWSC1",  // ç›®æ ‡åœ°å€
                                //        Roadway = "GWSC1",             // å··é“
                                //        WarehouseId = 1,                   // ä»“库 ID
                                //        PalletType = 1,                             // æ‰˜ç›˜ç±»åž‹ï¼ˆé»˜è®¤ä¸º1)
                                //        TaskType = TaskTypeEnum.InEmpty.GetHashCode()                         // ä»»åŠ¡ç±»åž‹ï¼ˆå…¥åº“/空托盘入库)
                                //    }.Serialize();
                                //    DateTime startTime = DateTime.Now;
                                //    var responseResult = _httpClientHelper.Post<WebResponseContent>(configKey, requestParam);
                                //    if (responseResult.IsSuccess && responseResult.Data.Status)
                                //    {
                                //        QuartzLogHelper.LogInfo(_logger, $"调用WMS接口成功,接口:【{configKey}】,请求参数:【{requestParam}】,响应数据:【{responseResult.Data?.Data}】,耗时:{(DateTime.Now - startTime).TotalMilliseconds}ms", conveyorLine.DeviceCode);
                                //        var wmsTask = JsonConvert.DeserializeObject<WMSTaskDTO>(responseResult?.Data?.Data?.ToString());
                                //        List<WMSTaskDTO> taskDTOs = new List<WMSTaskDTO> { wmsTask };
                                //        if (wmsTask == null) continue;
                                //        if (_taskService.ReceiveWMSTask(taskDTOs).Status)
                                //        {
                                //            conveyorLine.SetValue(ConveyorLineDBNameNew.WCS_ACK, (short)1, childDeviceCode);
                                //        }
                                //    }
                                //    else
                                //    {
                                //        QuartzLogHelper.LogError(_logger, $"调用WMS接口失败,接口:【{configKey}】,请求参数:【{requestParam}】,错误信息:【{responseResult.Data?.Message}】", conveyorLine.DeviceCode);
                                //    }
                                //}
                            }
                        }
                        catch (Exception innerEx)
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ConveyorLineNewJob/ConveyorLineDispatchHandler.cs
@@ -257,21 +257,21 @@
            // ç¡®å®šç›®æ ‡åœ°å€ï¼šç©ºæ‰˜ç›˜ä»»åŠ¡ä½¿ç”¨ "2201",其他任务使用 NextAddress
            var isEmptyTask = task.TaskType == (int)TaskOutboundTypeEnum.OutEmpty;
            var targetAddress = task.CurrentAddress == "2217" ? (isEmptyTask ? "2201" : task.NextAddress) : task.NextAddress;
            var targetAddress = task.CurrentAddress == "2217" ? (isEmptyTask ? task.TargetAddress : task.NextAddress) : task.NextAddress;
            // å¤„理特殊地址 2217,需要调用目标地址选择器
            if (task.CurrentAddress == "2217" && !_targetAddressSelector.HandleOutboundNextAddress(conveyorLine, targetAddress, childDeviceCode))
            {
                return Task.CompletedTask; ;
            }
            //if (task.CurrentAddress == "2217" && !_targetAddressSelector.HandleOutboundNextAddress(conveyorLine, targetAddress, childDeviceCode))
            //{
            //    return Task.CompletedTask; ;
            //}
            // è®¾ç½®ä»»åŠ¡å·ã€æ‰˜ç›˜æ¡ç ã€ç›®æ ‡åœ°å€ã€WCS_ACK
            var isTaskNoSet = conveyorLine.SetValue(ConveyorLineDBNameNew.TaskNo, task.TaskNum, childDeviceCode);
            Thread.Sleep(100); // ç¡®ä¿ PLC èƒ½æ­£ç¡®è¯»å–任务号后再写入条码
            Thread.Sleep(300); // ç¡®ä¿ PLC èƒ½æ­£ç¡®è¯»å–任务号后再写入条码
            var isPalletSet = conveyorLine.SetValue(ConveyorLineDBNameNew.Barcode, task.PalletCode, childDeviceCode);
            Thread.Sleep(100); // ç¡®ä¿ PLC èƒ½æ­£ç¡®è¯»å–任务号后再写入条码
            Thread.Sleep(300); // ç¡®ä¿ PLC èƒ½æ­£ç¡®è¯»å–任务号后再写入条码
            bool isTargetSet = conveyorLine.SetValue(ConveyorLineDBNameNew.Target, targetAddress, childDeviceCode);
            //if (targetAddress == "2217" && !isEmptyTask)
            //{
@@ -281,16 +281,16 @@
            //}
            //if (!isTargetSet || !isTaskNoSet || !isPalletSet)
            //{
            //    QuartzLogHelper.LogError(_logger, $"RequestOutbound:下发出库任务失败,任务号: {task.TaskNum},子设备: {childDeviceCode}", conveyorLine.DeviceCode);
            //    QuartzLogHelper.LogError(_logger, $"RequestOutbound:下发出库任务失败,任务号: {task.TaskNum},子设备: {childDeviceCode}", conveyorLine.DeviceCode);
            //    return Task.CompletedTask;
            //}
            bool isWmsResult = false;
            // æ›´æ–°ä»»åŠ¡çŠ¶æ€æˆ–ä½ç½®
            if (isEmptyTask && task.NextAddress == "2217")
            if (isEmptyTask && (task.TargetAddress == "2103" || task.TargetAddress == "2101"))
            {
                task.TaskStatus = task.TaskStatus.GetNextNotCompletedStatus<TaskOutStatusEnum>();
                task.NextAddress = "2201";
                task.NextAddress = "2103";
                isWmsResult = _taskService.Repository.UpdateData(task);
            }
            else
@@ -298,7 +298,7 @@
                isWmsResult = _taskService.UpdateTaskStatusToNext(task).Status;
            }
            if(!isWmsResult)
            if (!isWmsResult)
            {
                QuartzLogHelper.LogError(_logger, $"RequestOutbound:更新任务状态失败,任务号: {task.TaskNum},子设备: {childDeviceCode}", conveyorLine.DeviceCode);
                return Task.CompletedTask;
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ConveyorLineNewJob/ConveyorLineTargetAddressSelector.cs
@@ -68,8 +68,11 @@
            var cvState = conveyorLine.GetValue<ConveyorLineDBNameNew, byte>(ConveyorLineDBNameNew.CV_State, nextAddress);
            bool isAvailable = cvState == 2;
            WriteDebug(conveyorLine, "出库下一地址状态", childDeviceCode, $"CV_State={cvState},可用={isAvailable}");
            if (isAvailable)
            {
                WriteDebug(conveyorLine, "出库下一地址可用,写入目标地址", childDeviceCode, nextAddress);
                Thread.Sleep(300); // çŸ­æš‚等待,确保设备状态稳定后再写入目标地址
                return conveyorLine.SetValue(ConveyorLineDBNameNew.Target, Convert.ToInt16(nextAddress), childDeviceCode);
            }
            return false;
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotJob.cs
@@ -209,7 +209,15 @@
                }
                // è½®è¯¢èŽ·å–è¯¥è®¾å¤‡çš„å¾…å¤„ç†ä»»åŠ¡
                var task = _taskProcessor.GetTask(robotCrane);
                // ä¼˜å…ˆé€šè¿‡çŠ¶æ€ä¸­ç¼“å­˜çš„ä»»åŠ¡ç¼–å·æŸ¥æ‰¾æ‰§è¡Œä¸­çš„ä»»åŠ¡
                Dt_RobotTask? task = null;
                if (state.CurrentTaskNum.HasValue)
                {
                    task = _taskProcessor.GetTaskByNum(state.CurrentTaskNum.Value);
                }
                // ç¼“存的任务号未找到对应任务时,按设备编码获取新任务
                task ??= _taskProcessor.GetTask(robotCrane);
                // å¦‚果没有获取到待处理任务,且RobotArmObject为1(有物料),则获取该设备执行中的任务
                //if (task == null && state.RobotArmObject == 1)
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotTaskProcessor.cs
@@ -124,6 +124,20 @@
        }
        /// <summary>
        /// æŒ‰ä»»åŠ¡ç¼–å·èŽ·å–æœºå™¨äººä»»åŠ¡
        /// </summary>
        /// <remarks>
        /// ç”¨äºŽ RobotJob å¿«é€ŸæŸ¥æ‰¾æ‰§è¡Œä¸­çš„任务,避免全表扫描。
        /// ä¼˜å…ˆé€šè¿‡çŠ¶æ€ä¸­ç¼“å­˜çš„ CurrentTaskNum å®šä½ä»»åŠ¡ã€‚
        /// </remarks>
        /// <param name="taskNum">机器人任务编号</param>
        /// <returns>匹配的任务对象,如果没有则返回 null</returns>
        public Dt_RobotTask? GetTaskByNum(int taskNum)
        {
            return _robotTaskService.Repository.QueryFirst(x => x.RobotTaskNum == taskNum);
        }
        /// <summary>
        /// æŒ‰è®¾å¤‡ç¼–码获取当前机器人的执行中任务
        /// </summary>
        /// <remarks>
@@ -186,6 +200,7 @@
            // å°†ä»»åŠ¡å…³è”åˆ°çŠ¶æ€å¯¹è±¡
            state.CurrentTask = task;
            state.CurrentTaskNum = task.RobotTaskNum;
            if (isScanNG)
            {
@@ -276,6 +291,7 @@
            // å°†ä»»åŠ¡å…³è”åˆ°çŠ¶æ€å¯¹è±¡
            state.CurrentTask = task;
            state.CurrentTaskNum = task.RobotTaskNum;
            if (_stateManager.TryUpdateStateSafely(state.IPAddress, state))
            {
@@ -346,6 +362,7 @@
            task.RobotTaskState = TaskRobotStatusEnum.RobotExecuting.GetHashCode();
            state.CurrentTask = task;
            state.CurrentTaskNum = task.RobotTaskNum;
            if (_stateManager.TryUpdateStateSafely(state.IPAddress, state))
            {
@@ -389,6 +406,7 @@
        {
            task.RobotTaskState = TaskRobotStatusEnum.RobotExecuting.GetHashCode();
            state.CurrentTask = task;
            state.CurrentTaskNum = task.RobotTaskNum;
            if (_stateManager.TryUpdateStateSafely(state.IPAddress, state))
            {
@@ -434,6 +452,7 @@
        {
            task.RobotTaskState = TaskRobotStatusEnum.RobotExecuting.GetHashCode();
            state.CurrentTask = task;
            state.CurrentTaskNum = task.RobotTaskNum;
            if (_stateManager.TryUpdateStateSafely(state.IPAddress, state))
            {
@@ -627,6 +646,12 @@
            // è§£æž WMS è¿”回的任务信息
            WMSTaskDTO taskDTO = JsonConvert.DeserializeObject<WMSTaskDTO>(result.Data.Data.ToJson() ?? string.Empty) ?? new WMSTaskDTO();
            var task = await _taskService.Repository.QueryFirstAsync(x => x.PalletCode == taskDTO.PalletCode);
            if(task != null)
            {
                await _taskService.Repository.DeleteDataAsync(task);
            }
            // è°ƒç”¨ä»»åŠ¡æœåŠ¡æŽ¥æ”¶ WMS ä»»åŠ¡
            var content = _taskService.ReceiveWMSTask(new List<WMSTaskDTO> { taskDTO });
            if (!content.Status)
@@ -682,9 +707,9 @@
                        // é€šé“/位置编号
                        Channel = x,
                        // ç”µæ± æ¡ç ï¼šå¦‚果状态中有条码列表,取对应位置的条码;否则为空
                        // ç”µæ± æ¡ç ï¼šä½¿ç”¨å½“前批次条码列表,取对应位置的条码;否则为空
                        //CellBarcode = state.CellBarcode?.Count > 0 ? state.CellBarcode[x - 1] : ""
                        CellBarcode = !state.CellBarcode.IsNullOrEmpty() ? state.CellBarcode[idx].ToString() ?? string.Empty : string.Empty
                        CellBarcode = !state.CurrentBatchBarcodes.IsNullOrEmpty() ? state.CurrentBatchBarcodes[idx].ToString() ?? string.Empty : string.Empty
                    })
                    .ToList()
            };
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Workflow/RobotSimpleCommandHandler.cs
@@ -197,7 +197,7 @@
                            // è°ƒç”¨æ‰¹é‡æ‹†ç›˜ç¡®è®¤æŽ¥å£
                            var sourcePallet = state.CurrentTask.RobotSourceAddressPalletCode;
                            var confirmResult = _taskProcessor.PostSplitPalletConfirmAsync(sourcePallet, state.RobotCrane?.DeviceName);
                            if (!confirmResult.IsSuccess)
                            if (!confirmResult.IsSuccess && !confirmResult.Data.Status)
                            {
                                QuartzLogHelper.LogError(_logger, $"批量拆盘确认失败: {confirmResult.ErrorMessage}", state.RobotCrane?.DeviceName ?? "Unknown");
                                return false;
@@ -221,8 +221,10 @@
                                QuartzLogHelper.LogInfo(_logger, $"发送消息:【Group,diskFinished】", state.RobotCrane.DeviceName);
                                state.CurrentTask = null;
                                state.CurrentTaskNum = null;
                                state.RobotTaskTotalNum = 0;
                                state.CellBarcode = new List<string>();
                                state.CurrentBatchBarcodes = new List<string>();
                                state.ChangePalletPhase = 0;
                                state.CurrentBatchIndex = 1;
                                state.IsInFakeBatteryMode = false;
@@ -314,7 +316,7 @@
                            // è°ƒç”¨æ‰¹é‡ç»„盘确认接口
                            var targetPallet = state.CurrentTask.RobotTargetAddressPalletCode;
                            var confirmResult = _taskProcessor.PostGroupPalletConfirmAsync(targetPallet, state.RobotCrane?.DeviceName);
                            if (!confirmResult.IsSuccess)
                            if (!confirmResult.IsSuccess && !confirmResult.Data.Status)
                            {
                                QuartzLogHelper.LogError(_logger, $"批量组盘确认失败: {confirmResult.ErrorMessage}", state.RobotCrane?.DeviceName ?? "Unknown");
                                return false;
@@ -335,8 +337,10 @@
                                }
                                state.CurrentTask = null;
                                state.CurrentTaskNum = null;
                                state.RobotTaskTotalNum = 0;
                                state.CellBarcode = new List<string>();
                                state.CurrentBatchBarcodes = new List<string>();
                                await _socketClientGateway.SendToClientAsync(state.IPAddress, $"Group,diskFinished");
                                QuartzLogHelper.LogInfo(_logger, $"发送消息:【Group,diskFinished】", state.RobotCrane.DeviceName);
@@ -371,8 +375,10 @@
                            }
                            state.CurrentTask = null;
                            state.CurrentTaskNum = null;
                            state.RobotTaskTotalNum = 0;
                            state.CellBarcode = new List<string>();
                            state.CurrentBatchBarcodes = new List<string>();
                            await _socketClientGateway.SendToClientAsync(state.IPAddress, $"Group,diskFinished");
                            QuartzLogHelper.LogInfo(_logger, $"发送消息:【Group,diskFinished】", state.RobotCrane.DeviceName);
@@ -411,8 +417,10 @@
                            // æ¸…理状态,为下一个任务做准备
                            state.CurrentTask = null;           // æ¸…除当前任务
                            state.CurrentTaskNum = null;        // æ¸…除当前任务编号
                            state.RobotTaskTotalNum = 0;        // é‡ç½®ä»»åŠ¡è®¡æ•°
                            state.CellBarcode = new List<string>();  // æ¸…空条码列表
                            state.CurrentBatchBarcodes = new List<string>();  // æ¸…空当前批次条码
                            await _socketClientGateway.SendToClientAsync(state.IPAddress, $"Group,diskFinished");
                            QuartzLogHelper.LogInfo(_logger, $"发送消息:【Group,diskFinished】", state.RobotCrane.DeviceName);
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Workflow/RobotWorkflowOrchestrator.cs
@@ -271,6 +271,7 @@
                if (stateToUpdate != null)
                {
                    stateToUpdate.CurrentTask = task;
                    stateToUpdate.CurrentTaskNum = task.RobotTaskNum;
                    if (_stateManager.TryUpdateStateSafely(ipAddress, stateToUpdate))
                    {
@@ -356,10 +357,16 @@
                        QuartzLogHelper.LogInfo(_logger, $"HandlePutFinishedStateAsync:读取的电芯条码唯一,继续执行,任务号: {task.RobotTaskNum}", stateForUpdate?.RobotCrane?.DeviceName ?? ipAddress);
                        // å°†æ¡ç æ·»åŠ åˆ°çŠ¶æ€ä¸­ï¼Œä¾›åŽç»­æ”¾è´§æ—¶ä½¿ç”¨
                        stateForUpdate.CellBarcode = new List<string>()
                        // å°†æ¡ç ç´¯ç§¯åˆ° CellBarcode(去重),并设置当前批次条码
                        if (!stateForUpdate.CellBarcode.Contains(trayBarcode1))
                            stateForUpdate.CellBarcode.Add(trayBarcode1);
                        if (!stateForUpdate.CellBarcode.Contains(trayBarcode2))
                            stateForUpdate.CellBarcode.Add(trayBarcode2);
                        // è®¾ç½®å½“前批次条码,用于 WMS æäº¤
                        stateForUpdate.CurrentBatchBarcodes = new List<string>()
                        {
                            trayBarcode1,trayBarcode2
                            trayBarcode1, trayBarcode2
                        };
                    }
@@ -396,10 +403,13 @@
                // ç›®æ ‡æ•°é‡ä¸º48:直接走原有逻辑,不进入批次模式
                if (targetNormalCount + currentCompletedCount == targetTotal)
                {
                    QuartzLogHelper.LogInfo(_logger, $"HandlePutFinishedStateAsync:目标数量已达48,直接下发取货指令,任务号: {task.RobotTaskNum}", stateForUpdate?.RobotCrane?.DeviceName ?? ipAddress);
                    await _taskProcessor.SendSocketRobotPickAsync(task, stateForUpdate);
                    return;
                }
                QuartzLogHelper.LogDebug(_logger,$"HandlePutFinishedStateAsync:换盘任务目标数量: {targetNormalCount},当前已完成数量: {currentCompletedCount},流向: {(isFlowA ? "A" : "B")},任务号: {task.RobotTaskNum}", stateForUpdate?.RobotCrane?.DeviceName ?? ipAddress);
                // åˆå§‹åŒ–批次模式
                if (stateForUpdate.ChangePalletPhase == 0)
                {
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/SocketServer/TcpSocketServer.Messaging.cs
@@ -3,6 +3,7 @@
using System.Text.Json;
using System.IO;
using WIDESEAWCS_Model.Models;
using Serilog.Core;
namespace WIDESEAWCS_Tasks.SocketServer
{
@@ -64,6 +65,7 @@
                        //{
                        //    if (_clientLastMessage.TryGetValue(clientId, out var prev) && message == prev)
                        //    {
                        //        QuartzLogHelper.LogInfo(Logger.None, $"来自客户端 {clientId} çš„重复消息,内容: {message}", clientId);
                        //        continue;
                        //    }
                        //    _clientLastMessage[clientId] = message;
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/StackerCraneJob/CommonStackerCraneJob.cs
@@ -121,36 +121,6 @@
        }
        /// <summary>
        /// åŠ è½½é…ç½®æ–‡ä»¶ï¼ˆä¼˜å…ˆçº§ï¼šé…ç½®æ–‡ä»¶ > é»˜è®¤é…ç½®ï¼‰
        /// </summary>
        /// <remarks>
        /// ä»Žåº”用程序目录下的 StackerCraneJob/stackercrane-command-config.json è¯»å–配置。
        /// å¦‚果文件不存在或解析失败,使用默认配置。
        /// </remarks>
        /// <returns>堆垛机命令配置</returns>
        private static StackerCraneCommandConfig LoadConfig()
        {
            try
            {
                // æž„造配置文件路径
                string configPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "StackerCraneJob", "stackercrane-command-config.json");
                if (File.Exists(configPath))
                {
                    // è¯»å–并解析 JSON é…ç½®
                    string json = File.ReadAllText(configPath);
                    return System.Text.Json.JsonSerializer.Deserialize<StackerCraneCommandConfig>(json) ?? new StackerCraneCommandConfig();
                }
            }
            catch (Exception ex)
            {
                // é…ç½®åŠ è½½å¤±è´¥ï¼Œä½¿ç”¨é»˜è®¤é…ç½®
                Console.WriteLine($"配置加载失败: {ex.Message},使用默认配置");
            }
            return new StackerCraneCommandConfig();
        }
        /// <summary>
        /// Quartz Job çš„æ‰§è¡Œå…¥å£
        /// </summary>
        /// <remarks>
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/StackerCraneJob/StackerCraneTaskSelector.cs
@@ -1,7 +1,6 @@
using Newtonsoft.Json;
using Serilog;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using WIDESEA_Core;
using WIDESEAWCS_Common.Constants;
using WIDESEAWCS_Common.HttpEnum;
@@ -10,7 +9,6 @@
using WIDESEAWCS_ITaskInfoService;
using WIDESEAWCS_Model.Models;
using WIDESEAWCS_QuartzJob;
using WIDESEAWCS_QuartzJob.ConveyorLine.Enum;
using WIDESEAWCS_QuartzJob.Models;
using WIDESEAWCS_QuartzJob.Service;
@@ -208,7 +206,7 @@
        {
            // å…ˆè¿›è¡Œæœ¬åœ°ç«™å°æ£€æŸ¥ï¼ˆPLC è¯»å–,快速),避免不必要的 WMS HTTP è°ƒç”¨
            if (outboundTask.TaskType != (int)TaskOutboundTypeEnum.OutEmpty && outboundTask.Roadway != "GWSC1" && outboundTask.TargetAddress != "CWSC1")
            if (outboundTask.TaskType != (int)TaskOutboundTypeEnum.OutEmpty && outboundTask.TargetAddress != "CWSC1")
            {
                // åˆ¤æ–­ TargetAddress è¾“送线站台是否空闲
                if (!IsTargetAddressConveyorStationAvailable(outboundTask))
@@ -223,18 +221,18 @@
                return null;
            }
            if (outboundTask.TargetAddress != "CWSC1")
            //if (outboundTask.TargetAddress != "CWSC1")
            //{
            // æ£€æŸ¥æ˜¯å¦æœ‰æ­£åœ¨æ‰§è¡Œçš„输送线任务去往同一 TargetAddress
            if (_taskService.HasExecutingTaskToTarget(outboundTask.Roadway, outboundTask.TargetAddress))
            {
                // æ£€æŸ¥æ˜¯å¦æœ‰æ­£åœ¨æ‰§è¡Œçš„输送线任务去往同一 TargetAddress
                if (_taskService.HasExecutingTaskToTarget(outboundTask.Roadway, outboundTask.TargetAddress))
                {
                    QuartzLogHelper.LogInfo(_logger, "TrySelectOutboundTask:TargetAddress: {TargetAddress} å·²æœ‰æ­£åœ¨æ‰§è¡Œçš„输送线任务,任务号: {TaskNum}",
                        $"TrySelectOutboundTask:TargetAddress: {outboundTask.TargetAddress} å·²æœ‰æ­£åœ¨æ‰§è¡Œçš„输送线任务", outboundTask.Roadway, outboundTask.TargetAddress, outboundTask.TaskNum);
                    return null;
                }
                QuartzLogHelper.LogInfo(_logger, "TrySelectOutboundTask:TargetAddress: {TargetAddress} å·²æœ‰æ­£åœ¨æ‰§è¡Œçš„输送线任务,任务号: {TaskNum}",
                    $"TrySelectOutboundTask:TargetAddress: {outboundTask.TargetAddress} å·²æœ‰æ­£åœ¨æ‰§è¡Œçš„输送线任务", outboundTask.Roadway, outboundTask.TargetAddress, outboundTask.TaskNum);
                return null;
            }
            //}
            if(outboundTask.Roadway != "GWSC1")
            if (outboundTask.Roadway != "GWSC1")
            {
                return outboundTask;
            }
Code/WMS/WIDESEA_WMSClient/src/router/viewGird.js
@@ -243,6 +243,11 @@
    name: 'PDA',
    component: () => import('@/views/system/PDA.vue')
  }
  , {
    path: '/outboundTimeConfig',
    name: 'outboundTimeConfig',
    component: () => import('@/views/system/outboundTimeConfig.vue')
  }
]
export default viewgird
Code/WMS/WIDESEA_WMSClient/src/views/system/outboundTimeConfig.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,109 @@
<template>
  <div class="config-container">
    <el-card class="config-card">
      <template #header>
        <span>出库时效配置</span>
      </template>
      <el-form :model="form" label-width="180px" v-loading="loading">
        <el-form-item label="GW首放入库时效(小时)">
          <el-input-number
            v-model="form.gw1FirstHours"
            :min="0.01"
            :max="999"
            :precision="2"
            :step="1"
          />
        </el-form-item>
        <el-form-item label="GW二放入库时效(小时)">
          <el-input-number
            v-model="form.gw1SecondHours"
            :min="0.01"
            :max="999"
            :precision="2"
            :step="0.01"
          />
        </el-form-item>
        <el-form-item label="CW出库时效(小时)">
          <el-input-number
            v-model="form.cw1Hours"
            :min="0.01"
            :max="999"
            :precision="2"
            :step="1"
          />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleSave" :loading="saving">
            ä¿å­˜
          </el-button>
        </el-form-item>
      </el-form>
    </el-card>
  </div>
</template>
<script>
import { ref, onMounted } from "vue";
import { ElMessage } from "element-plus";
import http from "@/api/http";
export default {
  name: "outboundTimeConfig",
  setup() {
    const loading = ref(false);
    const saving = ref(false);
    const form = ref({
      gw1FirstHours: 24,
      gw1SecondHours: 0.05,
      cw1Hours: 3,
    });
    /// åŠ è½½å½“å‰é…ç½®
    const loadConfig = async () => {
      loading.value = true;
      try {
        const res = await http.post("/api/OutboundTimeConfig/get", {}, false);
        if (res.status) {
          form.value = {
            gw1FirstHours: res.data.gw1FirstHours,
            gw1SecondHours: res.data.gw1SecondHours,
            cw1Hours: res.data.cw1Hours,
          };
        }
      } finally {
        loading.value = false;
      }
    };
    /// ä¿å­˜é…ç½®
    const handleSave = async () => {
      saving.value = true;
      try {
        const res = await http.post("/api/OutboundTimeConfig/update", form.value, false);
        if (res.status) {
          ElMessage.success("保存成功");
        } else {
          ElMessage.error(res.message || "保存失败");
        }
      } finally {
        saving.value = false;
      }
    };
    onMounted(() => {
      loadConfig();
    });
    return { form, loading, saving, handleSave };
  },
};
</script>
<style scoped>
.config-container {
  padding: 20px;
}
.config-card {
  max-width: 600px;
}
</style>
Code/WMS/WIDESEA_WMSServer/WIDESEA_BasicService/LocationInfoService.cs
@@ -159,9 +159,12 @@
                    && x.LocationType == locationType)
                .CountAsync();
            // ç©ºé—²è´§ä½ä¸è¶³æœ€ä½Žä¿ç•™æ•°é‡æ—¶è¿”回null,避免将巷道分配耗尽
            const int minFreeLocationThreshold = 5;
            if (freeCount < minFreeLocationThreshold) return null;
            if(roadwayNo != "CWSC1")
            {
                // ç©ºé—²è´§ä½ä¸è¶³æœ€ä½Žä¿ç•™æ•°é‡æ—¶è¿”回null,避免将巷道分配耗尽
                const int minFreeLocationThreshold = 1;
                if (freeCount < minFreeLocationThreshold) return null;
            }
            // æ•°æ®åº“端排序取第一条(只传输单行数据)
            return await BaseDal.Db.Queryable<Dt_LocationInfo>()
Code/WMS/WIDESEA_WMSServer/WIDESEA_Common/Constants/OutboundTimeConstants.cs
@@ -29,16 +29,16 @@
        /// <summary>
        /// GW_1首放入库时效(24小时)
        /// </summary>
        public const int OUTBOUND_HOURS_GW1_FIRST = 24;
        public const double OUTBOUND_HOURS_GW1_FIRST = 24;
        /// <summary>
        /// GW_1二放入库时效(24小时)
        /// GW_1二放入库时效(5分钟)
        /// </summary>
        public const int OUTBOUND_HOURS_GW1_SECOND = 24;
        public const double OUTBOUND_HOURS_GW1_SECOND = 0.05;
        /// <summary>
        /// CW_1出库时效(12小时)
        /// CW_1出库时效(3小时)
        /// </summary>
        public const int OUTBOUND_HOURS_CW1 = 12;
        public const double OUTBOUND_HOURS_CW1 = 3;
    }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_Common/Constants/TaskAddressConstants.cs
@@ -13,7 +13,7 @@
        /// <summary>
        /// é«˜æ¸©1号出库地址列表(轮询)
        /// </summary>
        public static readonly string[] GW1_ADDRESSES = { "11001" };
        public static readonly string[] GW1_ADDRESSES = { "11010" };
        /// <summary>
        /// é«˜æ¸©2号出库地址
@@ -28,7 +28,7 @@
        /// <summary>
        /// åˆ†å®¹åº“出库地址
        /// </summary>
        public const string GRADING_OUTBOUND_ADDRESS = "2103";
        public const string GRADING_OUTBOUND_ADDRESS = "2101";
        /// <summary>
        /// åˆ†å®¹åº“出库地址
Code/WMS/WIDESEA_WMSServer/WIDESEA_Core/Core/OutboundTimeConfigOptions.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
namespace WIDESEA_Core.Core
{
    /// <summary>
    /// å‡ºåº“时效配置选项,绑定 appsettings.json ä¸­çš„ OutboundTimeConfig èŠ‚
    /// </summary>
    public class OutboundTimeConfigOptions
    {
        /// <summary>
        /// é…ç½®èŠ‚åç§°
        /// </summary>
        public const string SectionName = "OutboundTimeConfig";
        /// <summary>
        /// GW首放入库时效(小时),默认24小时
        /// </summary>
        public double Gw1FirstHours { get; set; } = 24;
        /// <summary>
        /// GW二放入库时效(小时),默认0.05小时(约3分钟)
        /// </summary>
        public double Gw1SecondHours { get; set; } = 0.05;
        /// <summary>
        /// CW出库时效(小时),默认3小时
        /// </summary>
        public double Cw1Hours { get; set; } = 3;
    }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService.cs
@@ -9,6 +9,7 @@
using WIDESEA_Common.StockEnum;
using WIDESEA_Common.TaskEnum;
using WIDESEA_Common.WareHouseEnum;
using Microsoft.Extensions.Options;
using WIDESEA_Core;
using WIDESEA_Core.BaseRepository;
using WIDESEA_Core.BaseServices;
@@ -43,6 +44,7 @@
        private readonly IMesLogService _mesLogService;
        private readonly IMesUploadHelper _mesUploadHelper;
        private readonly ISqlSugarClient _sqlSugarClient;
        private readonly IOptionsMonitor<OutboundTimeConfigOptions> _outboundTimeOptions;
        public IRepository<Dt_Task> Repository => BaseDal;
@@ -71,7 +73,8 @@
            IMESDeviceConfigService mesDeviceConfigService,
            IMesLogService mesLogService,
            IMesUploadHelper mesUploadHelper,
            ISqlSugarClient sqlSugarClient) : base(BaseDal)
            ISqlSugarClient sqlSugarClient,
            IOptionsMonitor<OutboundTimeConfigOptions> outboundTimeOptions) : base(BaseDal)
        {
            _mapper = mapper;
            _stockInfoService = stockInfoService;
@@ -88,6 +91,7 @@
            _mesLogService = mesLogService;
            _mesUploadHelper = mesUploadHelper;
            _sqlSugarClient = sqlSugarClient;
            _outboundTimeOptions = outboundTimeOptions;
        }
        /// <summary>
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/WCS/TaskService_Inbound.cs
@@ -141,8 +141,10 @@
                            Creater = StockConstants.SYSTEM_USER,
                            Details = null,
                            LocationCode = location.LocationCode,
                            LocationId = location.Id
                            LocationId = location.Id,
                            OutboundDate = DateTime.Now
                        };
                        location.LocationStatus = LocationStatusEnum.InStock.GetHashCode();
                        var updateLocationResult = await _locationInfoService.UpdateLocationInfoAsync(location);
                        var updateStockResult = await _stockInfoService.Repository.AddDataAsync(stockInfo);
                        return await CompleteTaskAsync(task, "入库完成");
@@ -166,6 +168,8 @@
                        SetOutboundDateByRoadway(task, stockInfo);
                        stockInfo.StockStatus = StockStatusEmun.入库完成.GetHashCode();
                        stockInfo.CreateDate = DateTime.Now;
                        location.LocationStatus = LocationStatusEnum.InStock.GetHashCode();
@@ -229,14 +233,15 @@
        /// <param name="stockInfo">库存信息</param>
        private void SetOutboundDateByRoadway(Dt_Task task, Dt_StockInfo stockInfo)
        {
            var config = _outboundTimeOptions.CurrentValue;
            var now = DateTime.Now;
            if (task.Roadway.Contains("GW"))
            {
                stockInfo.OutboundDate = string.IsNullOrEmpty(stockInfo.Remark)
                    ? now.AddHours(OutboundTimeConstants.OUTBOUND_HOURS_GW1_FIRST)
                    ? now.AddHours(config.Gw1FirstHours)
                    : stockInfo.Remark == StockRemarkConstants.GW1
                        ? now.AddHours(OutboundTimeConstants.OUTBOUND_HOURS_GW1_SECOND)
                        : now.AddHours(OutboundTimeConstants.OUTBOUND_HOURS_GW1_FIRST);
                        ? now.AddHours(config.Gw1SecondHours)
                        : now.AddHours(config.Gw1FirstHours);
                stockInfo.Remark = string.IsNullOrEmpty(stockInfo.Remark)
                    ? StockRemarkConstants.GW1
@@ -246,7 +251,7 @@
            }
            else if (task.Roadway.Contains("CW"))
            {
                stockInfo.OutboundDate = now.AddHours(OutboundTimeConstants.OUTBOUND_HOURS_CW1);
                stockInfo.OutboundDate = now.AddHours(config.Cw1Hours);
                if (stockInfo.Remark == StockRemarkConstants.GW2)
                    stockInfo.Remark = StockRemarkConstants.CW1;
            }
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/System/OutboundTimeConfigController.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,94 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using WIDESEA_Core;
using WIDESEA_Core.Core;
namespace WIDESEA_WMSServer.Controllers
{
    /// <summary>
    /// å‡ºåº“时效配置控制器,读写独立的 outbound_time_config.json é…ç½®æ–‡ä»¶
    /// </summary>
    [Route("api/[controller]")]
    [ApiController]
    [Authorize]
    public class OutboundTimeConfigController : ControllerBase
    {
        private readonly IOptionsMonitor<OutboundTimeConfigOptions> _optionsMonitor;
        private readonly IWebHostEnvironment _env;
        /// <summary>
        /// é…ç½®æ–‡ä»¶å
        /// </summary>
        private const string ConfigFileName = "outbound_time_config.json";
        /// <summary>
        /// æž„造函数
        /// </summary>
        /// <param name="optionsMonitor">出库时效配置 Options ç›‘控器</param>
        /// <param name="env">Web å®¿ä¸»çŽ¯å¢ƒï¼Œç”¨äºŽå®šä½é…ç½®æ–‡ä»¶è·¯å¾„</param>
        public OutboundTimeConfigController(IOptionsMonitor<OutboundTimeConfigOptions> optionsMonitor, IWebHostEnvironment env)
        {
            _optionsMonitor = optionsMonitor;
            _env = env;
        }
        /// <summary>
        /// èŽ·å–å½“å‰å‡ºåº“æ—¶æ•ˆé…ç½®
        /// </summary>
        /// <returns>当前配置值</returns>
        [HttpPost("get")]
        public IActionResult Get()
        {
            var config = _optionsMonitor.CurrentValue;
            return Ok(WebResponseContent.Instance.OK("获取成功", new
            {
                config.Gw1FirstHours,
                config.Gw1SecondHours,
                config.Cw1Hours
            }));
        }
        /// <summary>
        /// æ›´æ–°å‡ºåº“时效配置,写入独立的 outbound_time_config.json æ–‡ä»¶
        /// </summary>
        /// <param name="config">新的配置值</param>
        /// <returns>操作结果</returns>
        [HttpPost("update")]
        public IActionResult Update([FromBody] OutboundTimeConfigOptions config)
        {
            if (config.Gw1FirstHours <= 0 || config.Gw1SecondHours <= 0 || config.Cw1Hours <= 0)
            {
                return Ok(WebResponseContent.Instance.Error("配置值必须大于0"));
            }
            try
            {
                var filePath = Path.Combine(_env.ContentRootPath, ConfigFileName);
                // æž„建配置 JSON,包裹在 OutboundTimeConfig èŠ‚ä¸‹ä»¥åŒ¹é… Options ç»‘定
                var configSection = new JObject
                {
                    ["Gw1FirstHours"] = config.Gw1FirstHours,
                    ["Gw1SecondHours"] = config.Gw1SecondHours,
                    ["Cw1Hours"] = config.Cw1Hours
                };
                var jsonObj = new JObject
                {
                    [OutboundTimeConfigOptions.SectionName] = configSection
                };
                // å†™å…¥æ–‡ä»¶ï¼Œæ ¼å¼åŒ–输出
                System.IO.File.WriteAllText(filePath, jsonObj.ToString(Formatting.Indented));
                return Ok(WebResponseContent.Instance.OK("配置更新成功"));
            }
            catch (Exception ex)
            {
                return Ok(WebResponseContent.Instance.Error($"配置更新失败: {ex.Message}"));
            }
        }
    }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Program.cs
@@ -36,6 +36,7 @@
    hostingContext.Configuration.ConfigureApplication();
    config.Sources.Clear();
    config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
    config.AddJsonFile("outbound_time_config.json", optional: true, reloadOnChange: true);
});
builder.Host.UseSerilog((context, services, loggerConfiguration) =>
@@ -75,6 +76,8 @@
builder.Services.AddSingleton<RoundRobinService>();
builder.Services.Configure<AutoOutboundTaskOptions>(
    builder.Configuration.GetSection("AutoOutboundTask"));
builder.Services.Configure<OutboundTimeConfigOptions>(
    builder.Configuration.GetSection(OutboundTimeConfigOptions.SectionName));
builder.Services.AddMemoryCacheSetup(); // ç¼“存服务
builder.Services.AddWebSocketSetup();
builder.Services.AddSqlsugarSetup(); // SqlSugar æ•°æ®åº“配置
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/WIDESEA_WMSServer.csproj
@@ -24,6 +24,12 @@
  </ItemGroup>
  <ItemGroup>
    <Content Update="outbound_time_config.json">
      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </Content>
  </ItemGroup>
  <ItemGroup>
      <Content Update="wwwroot\swg-login.html">
          <CopyToOutputDirectory>Never</CopyToOutputDirectory>
      </Content>
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/outbound_time_config.json
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,7 @@
{
  "OutboundTimeConfig": {
    "Gw1FirstHours": 24.0,
    "Gw1SecondHours": 0.05,
    "Cw1Hours": 3.0
  }
}
Code/docs/superpowers/plans/2026-04-29-outbound-task-flow-todo-implementation.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,170 @@
# OutboundTaskFlowService.MoveToNextStatus TODO å®žçŽ°è®¡åˆ’
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** è¡¥å…¨ `OutboundTaskFlowService.MoveToNextStatus` æ–¹æ³•中的 TODO,实现在出库任务到达 `Line_OutFinish` çŠ¶æ€æ—¶æ£€æŸ¥å¯¹å‘çº¿ä½“æ‰˜ç›˜å·ï¼Œæœ‰æ‰˜ç›˜å·åˆ™æœ¬åœ°åˆ›å»ºæœºæ¢°æ‰‹ä»»åŠ¡ï¼Œæ— æ‰˜ç›˜å·åˆ™ä»Ž WMS èŽ·å–ä»»åŠ¡ã€‚
**Architecture:** åœ¨ `RobotTaskService` ä¸­æ–°å¢ž `CheckSourceLineAndCreateRobotTask` æ–¹æ³•,复用已有的 `BuildRobotTaskStock` èŽ·å–æºçº¿ä½“ç¼–å·ï¼Œé€šè¿‡è®¾å¤‡é€šä¿¡è¯»å–çº¿ä½“æ‰˜ç›˜å·ï¼Œæ ¹æ®ç»“æžœå†³å®šè°ƒç”¨ `CreateLocalRobotTask` æˆ– `GetWMSOutboundTrayTask`。
**Tech Stack:** C# / .NET 6, ASP.NET Core, SqlSugar ORM
---
## æ¶‰åŠæ–‡ä»¶
| æ–‡ä»¶ | æ”¹åЍ |
|------|------|
| `WIDESEAWCS_TaskInfoService/RobotTaskService.cs` | æ–°å¢ž `CheckSourceLineAndCreateRobotTask` æ–¹æ³• |
| `WIDESEAWCS_TaskInfoService/Flows/OutboundTaskFlowService.cs` | æ›¿æ¢ TODO ä»£ç æ®µä¸ºå§”托调用 |
---
## Task 1: åœ¨ RobotTaskService æ–°å¢ž CheckSourceLineAndCreateRobotTask æ–¹æ³•
**Files:**
- Modify: `WIDESEAWCS_Server/WIDESEAWCS_TaskInfoService/RobotTaskService.cs:241`(在 `CreateLocalRobotTask` æ–¹æ³•之后)
- [ ] **Step 1: åœ¨ `CreateLocalRobotTask` æ–¹æ³•后添加新方法**
在 `RobotTaskService.cs` ç¬¬ 240 è¡Œï¼ˆ`CreateLocalRobotTask` æ–¹æ³•结束后的位置)添加以下方法:
```csharp
/// <summary>
/// æ£€æŸ¥æºçº¿ä½“是否有托盘号,并根据结果创建机械手任务。
/// </summary>
/// <param name="task">出库任务实体</param>
/// <returns>
/// æœ‰æ‰˜ç›˜å·æ—¶è¿”回 CreateLocalRobotTask ç»“果;
/// æ— æ‰˜ç›˜å·æ—¶è¿”回 GetWMSOutboundTrayTask ç»“果。
/// </returns>
public WebResponseContent CheckSourceLineAndCreateRobotTask(Dt_Task task)
{
    // 1. èŽ·å–æºçº¿ä½“ç¼–å·ï¼ˆå¤ç”¨å·²æœ‰é€»è¾‘ï¼‰
    string configKey = ResolveRobotTaskConfigKey(task.TargetAddress);
    StockDTO stock = BuildRobotTaskStock(task, configKey);
    string sourceLineNo = stock.SourceLineNo;
    if (string.IsNullOrWhiteSpace(sourceLineNo))
    {
        return GetWMSOutboundTrayTask(task);
    }
    // 2. é€šè¿‡è®¾å¤‡é€šä¿¡è¯»å–线体托盘号
    string? palletCode = ReadLineBarcode(sourceLineNo);
    if (!string.IsNullOrWhiteSpace(palletCode))
    {
        // æœ‰æ‰˜ç›˜å·ï¼Œæœ¬åœ°åˆ›å»ºæœºæ¢°æ‰‹ä»»åŠ¡
        return CreateLocalRobotTask(task);
    }
    // æ— æ‰˜ç›˜å·ï¼Œä»Ž WMS èŽ·å–ä»»åŠ¡
    return GetWMSOutboundTrayTask(task);
}
/// <summary>
/// è¯»å–指定线体的托盘号。
/// </summary>
/// <param name="sourceLineNo">源线体编号</param>
/// <returns>托盘号,如有异常返回 null</returns>
private string? ReadLineBarcode(string sourceLineNo)
{
    try
    {
        IDevice? device = Storage.Devices.FirstOrDefault(x =>
            x.DeviceProDTOs.Any(d => d.DeviceChildCode == sourceLineNo));
        if (device == null)
            return null;
        CommonConveyorLine conveyorLine = (CommonConveyorLine)device;
        return conveyorLine.GetValue<ConveyorLineDBNameNew, string>(
            ConveyorLineDBNameNew.Barcode, sourceLineNo);
    }
    catch (Exception ex)
    {
        _logger.Error(ex, $"读取线体[{sourceLineNo}]托盘号异常");
        return null;
    }
}
```
- [ ] **Step 2: éªŒè¯ç¼–译**
Run: `dotnet build WIDESEAWCS_Server/WIDESEAWCS_Server.sln --no-restore`
Expected: BUILD SUCCEEDED(无编译错误)
- [ ] **Step 3: æäº¤**
```bash
git add WIDESEAWCS_TaskInfoService/RobotTaskService.cs
git commit -m "feat(机械手任务): æ–°å¢žCheckSourceLineAndCreateRobotTask方法支持检查线体托盘号决策
- æ–°å¢žCheckSourceLineAndCreateRobotTask方法,根据源线体托盘号决定创建机械手任务方式
- æ–°å¢žReadLineBarcode私有方法,读取指定线体的托盘号
- æœ‰æ‰˜ç›˜å·æ—¶è°ƒç”¨CreateLocalRobotTask,无托盘号时调用GetWMSOutboundTrayTask
- å¼‚常时降级为从WMS获取任务"
```
---
## Task 2: ä¿®æ”¹ OutboundTaskFlowService.MoveToNextStatus è°ƒç”¨æ–°æ–¹æ³•
**Files:**
- Modify: `WIDESEAWCS_Server/WIDESEAWCS_TaskInfoService/Flows/OutboundTaskFlowService.cs:107-114`
- [ ] **Step 1: æ›¿æ¢ TODO ä»£ç æ®µ**
将 `OutboundTaskFlowService.cs` ç¬¬ 107-114 è¡Œï¼š
```csharp
if (task.TaskStatus == (int)TaskOutStatusEnum.Line_OutFinish && task.TaskType == (int)TaskOutboundTypeEnum.Outbound)
{
    return WebResponseContent.Instance.OK();
    // Todo:获取对向线体是否有托盘号,如果有托盘号直接生成机械手任务
    return GetWMSOutboundTrayTask(task);
}
```
替换为:
```csharp
if (task.TaskStatus == (int)TaskOutStatusEnum.Line_OutFinish && task.TaskType == (int)TaskOutboundTypeEnum.Outbound)
{
    // èŽ·å–å¯¹å‘çº¿ä½“æ˜¯å¦æœ‰æ‰˜ç›˜å·ï¼Œå¦‚æžœæœ‰æ‰˜ç›˜å·ç›´æŽ¥ç”Ÿæˆæœºæ¢°æ‰‹ä»»åŠ¡
    return _robotTaskService.CheckSourceLineAndCreateRobotTask(task);
}
```
- [ ] **Step 2: éªŒè¯ç¼–译**
Run: `dotnet build WIDESEAWCS_Server/WIDESEAWCS_Server.sln --no-restore`
Expected: BUILD SUCCEEDED(无编译错误)
- [ ] **Step 3: æäº¤**
```bash
git add WIDESEAWCS_TaskInfoService/Flows/OutboundTaskFlowService.cs
git commit -m "feat(出库任务): MoveToNextStatus调用CheckSourceLineAndCreateRobotTask决策机械手任务创建方式
- æ›¿æ¢TODO代码段为_robotTaskService.CheckSourceLineAndCreateRobotTask调用
- æœ‰æ‰˜ç›˜å·æ—¶æœ¬åœ°åˆ›å»ºæœºæ¢°æ‰‹ä»»åŠ¡ï¼Œæ— æ‰˜ç›˜å·æ—¶ä»ŽWMS获取"
```
---
## éªŒè¯è¦ç‚¹
1. å½“输送线有料时(对向线体 Barcode æœ‰å€¼ï¼‰ï¼Œæœºæ¢°æ‰‹ä»»åŠ¡ç›´æŽ¥æœ¬åœ°åˆ›å»ºï¼Œä¸è°ƒç”¨ WMS æŽ¥å£
2. å½“输送线无料时(对向线体 Barcode ä¸ºç©ºï¼‰ï¼Œé™çº§è°ƒç”¨ WMS èŽ·å–ç©ºæ‰˜ç›˜ä»»åŠ¡
3. å¼‚常场景不阻塞主流程,降级到 WMS èŽ·å–
---
## æ³¨æ„äº‹é¡¹
- `ReadLineBarcode` æ–¹æ³•复用了 `BuildRobotTaskStock` ä¸­è¯»å– Barcode çš„设备查询逻辑(见 `RobotTaskService.cs:365-374`)
- å¼‚常处理返回 null,触发降级逻辑调用 `GetWMSOutboundTrayTask`
- è®¾è®¡æ–‡æ¡£ä½ç½®ï¼š`docs/superpowers/specs/2026-04-29-outbound-task-flow-todo-design.md`
Code/docs/superpowers/specs/2026-04-29-outbound-task-flow-todo-design.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,148 @@
# OutboundTaskFlowService.MoveToNextStatus TODO è®¾è®¡æ–¹æ¡ˆ
## èƒŒæ™¯
`OutboundTaskFlowService.MoveToNextStatus` æ–¹æ³•中存在一个 TODO:
```csharp
if (task.TaskStatus == (int)TaskOutStatusEnum.Line_OutFinish && task.TaskType == (int)TaskOutboundTypeEnum.Outbound)
{
    return WebResponseContent.Instance.OK();
    // Todo:获取对向线体是否有托盘号,如果有托盘号直接生成机械手任务
    return GetWMSOutboundTrayTask(task);
}
```
该 TODO éœ€è¦åœ¨å‡ºåº“任务到达 `Line_OutFinish` çŠ¶æ€æ—¶ï¼Œæ£€æŸ¥å¯¹å‘çº¿ä½“æ˜¯å¦æœ‰æ‰˜ç›˜å·ï¼Œä»¥å†³å®šåŽç»­æœºæ¢°æ‰‹ä»»åŠ¡çš„åˆ›å»ºæ–¹å¼ã€‚
## ä¸šåŠ¡é€»è¾‘
出库任务流程中,当物料通过输送线到达目标位置后:
- **对向线体有托盘号** â†’ æ‰˜ç›˜ä¸Šæœ‰è´§ï¼Œç›´æŽ¥åœ¨æœ¬åœ°åˆ›å»ºæœºæ¢°æ‰‹ä»»åŠ¡æ‰§è¡Œæ¢ç›˜/组盘/拆盘
- **对向线体无托盘号** â†’ éœ€è¦ä»Ž WMS èŽ·å–ç©ºæ‰˜ç›˜å‡ºåº“ä»»åŠ¡
## æ–¹æ¡ˆé€‰æ‹©
**采用方案 B**:在 `RobotTaskService` ä¸­å°è£…检查逻辑,复用已有的 `BuildRobotTaskStock` åŸºç¡€è®¾æ–½ã€‚
### åŽŸå› 
- `RobotTaskService.BuildRobotTaskStock` å·²å®žçŽ°é€šè¿‡ `AddressSourceLineNoMap` è§£æžæºçº¿ä½“编号,并通过设备通信读取 `Barcode` çš„完整逻辑
- æ–°å¢žæ–¹æ³•职责单一:检查 â†’ å†³ç­–,仅做判断不执行副作用
- `OutboundTaskFlowService` ä¿æŒç®€æ´ï¼Œåªéœ€è°ƒç”¨å³å¯
## å®žçŽ°è®¾è®¡
### 1. RobotTaskService æ–°å¢žæ–¹æ³•
```csharp
/// <summary>
/// æ£€æŸ¥æºçº¿ä½“是否有托盘号,并根据结果创建机械手任务。
/// </summary>
/// <param name="task">出库任务实体</param>
/// <returns>
/// æœ‰æ‰˜ç›˜å·æ—¶è¿”回 CreateLocalRobotTask ç»“果;
/// æ— æ‰˜ç›˜å·æ—¶è¿”回 GetWMSOutboundTrayTask ç»“果。
/// </returns>
public WebResponseContent CheckSourceLineAndCreateRobotTask(Dt_Task task)
{
    // 1. èŽ·å–æºçº¿ä½“ç¼–å·ï¼ˆå¤ç”¨å·²æœ‰é€»è¾‘ï¼‰
    string configKey = ResolveRobotTaskConfigKey(task.TargetAddress);
    StockDTO stock = BuildRobotTaskStock(task, configKey);
    string sourceLineNo = stock.SourceLineNo;
    if (string.IsNullOrWhiteSpace(sourceLineNo))
    {
        return GetWMSOutboundTrayTask(task);
    }
    // 2. é€šè¿‡è®¾å¤‡é€šä¿¡è¯»å–线体托盘号
    string? palletCode = ReadLineBarcode(sourceLineNo);
    if (!string.IsNullOrWhiteSpace(palletCode))
    {
        // æœ‰æ‰˜ç›˜å·ï¼Œæœ¬åœ°åˆ›å»ºæœºæ¢°æ‰‹ä»»åŠ¡
        return CreateLocalRobotTask(task);
    }
    // æ— æ‰˜ç›˜å·ï¼Œä»Ž WMS èŽ·å–ä»»åŠ¡
    return GetWMSOutboundTrayTask(task);
}
/// <summary>
/// è¯»å–指定线体的托盘号。
/// </summary>
private string? ReadLineBarcode(string sourceLineNo)
{
    try
    {
        IDevice? device = Storage.Devices.FirstOrDefault(x =>
            x.DeviceProDTOs.Any(d => d.DeviceChildCode == sourceLineNo));
        if (device == null)
            return null;
        CommonConveyorLine conveyorLine = (CommonConveyorLine)device;
        return conveyorLine.GetValue<ConveyorLineDBNameNew, string>(
            ConveyorLineDBNameNew.Barcode, sourceLineNo);
    }
    catch
    {
        return null;
    }
}
```
### 2. OutboundTaskFlowService ä¿®æ”¹
```csharp
if (task.TaskStatus == (int)TaskOutStatusEnum.Line_OutFinish && task.TaskType == (int)TaskOutboundTypeEnum.Outbound)
{
    // èŽ·å–å¯¹å‘çº¿ä½“æ˜¯å¦æœ‰æ‰˜ç›˜å·ï¼Œå¦‚æžœæœ‰æ‰˜ç›˜å·ç›´æŽ¥ç”Ÿæˆæœºæ¢°æ‰‹ä»»åŠ¡
    return _robotTaskService.CheckSourceLineAndCreateRobotTask(task);
}
```
## æ•°æ®æµ
```
MoveToNextStatus (OutboundTaskFlowService)
    â”‚
    â–¼
CheckSourceLineAndCreateRobotTask (RobotTaskService)
    â”‚
    â”œâ”€â”€â”€â–º BuildRobotTaskStock â”€â”€â–º ResolveRobotTaskConfigKey
    â”‚                              â””──► AddressSourceLineNoMap èŽ·å– sourceLineNo
    â”‚
    â–¼
ReadLineBarcode(sourceLineNo)
    â”‚
    â”œâ”€â”€â”€â–º Storage.Devices æŸ¥æ‰¾è®¾å¤‡
    â””───► CommonConveyorLine.GetValue(Barcode)
                â”‚
                â–¼
        â”Œâ”€â”€â”€â”€â”€â”€â”€â”´â”€â”€â”€â”€â”€â”€â”€â”
        â”‚  æ‰˜ç›˜å·æœ‰å€¼    â”‚     æ‰˜ç›˜å·ä¸ºç©º
        â–¼               â–¼
CreateLocalRobotTask   GetWMSOutboundTrayTask
```
## é”™è¯¯å¤„理
- è®¾å¤‡æŸ¥æ‰¾å¤±è´¥æˆ–读取异常 â†’ é™çº§ä¸ºè°ƒç”¨ `GetWMSOutboundTrayTask`(从 WMS èŽ·å–ä»»åŠ¡ï¼‰
- ä¸é˜»å¡žä¸»æµç¨‹ï¼Œå¼‚常仅记录日志
## æ¶‰åŠæ–‡ä»¶
| æ–‡ä»¶ | æ”¹åЍ |
|------|------|
| `WIDESEAWCS_TaskInfoService/RobotTaskService.cs` | æ–°å¢ž `CheckSourceLineAndCreateRobotTask` å’Œ `ReadLineBarcode` æ–¹æ³• |
| `WIDESEAWCS_TaskInfoService/Flows/OutboundTaskFlowService.cs` | æ›¿æ¢ TODO ä»£ç æ®µä¸ºå§”托调用 |
## éªŒè¯è¦ç‚¹
1. å½“输送线有料时,机械手任务直接本地创建,不调用 WMS æŽ¥å£
2. å½“输送线无料时,降级调用 WMS èŽ·å–ç©ºæ‰˜ç›˜ä»»åŠ¡
3. å¼‚常场景不阻塞主流程,降级到 WMS èŽ·å–