wanshenmean
3 天以前 b690250002ee04f4309e6a90fd16fbfd9bd959e2
feat(router): 添加托盘操作页面路由

feat(taskinfo): 实现组盘/拆盘操作页面

feat(robot): 添加机械手状态互斥锁防止并发冲突

feat(logging): 为分容柜接口添加独立日志文件

fix(redis): 修复Redis连接字符串密码配置

fix(robot): 修复机械手状态更新时的版本控制问题

refactor(robot): 重构机械手任务处理流程

perf(robot): 优化机械手消息处理性能

docs(model): 添加机械手状态模型字段注释

test: 更新相关测试用例

chore: 更新项目依赖
已添加1个文件
已修改16个文件
867 ■■■■ 文件已修改
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Model/Models/RobotState/Dt_RobotState.cs 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Server/appsettings.json 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_TaskInfoRepository/RobotStateRepository.cs 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotJob.cs 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotMessageHandler.cs 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotStateManager.cs 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Workflow/RobotSimpleCommandHandler.cs 192 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Workflow/RobotWorkflowOrchestrator.cs 28 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/StackerCraneJob/CommonStackerCraneJob.cs 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSClient/src/api/http.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSClient/src/router/viewGird.js 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSClient/src/views/taskinfo/palletOperation.vue 185 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_Core/Middlewares/ApiLogMiddleware.cs 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService.cs 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService_GradingMachine.cs 69 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/WIDESEA_TaskInfoService.csproj 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Program.cs 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Model/Models/RobotState/Dt_RobotState.cs
@@ -96,8 +96,18 @@
        /// <summary>
        /// æœ€è¿‘一次放货完成的位置数组(JSON)
        /// </summary>
        [SugarColumn(Length = 500, ColumnDescription = "放货位置数组JSON", IsNullable = true)]
        [SugarColumn(Length = 5000, ColumnDescription = "放货位置数组JSON", IsNullable = true)]
        public string? LastPutPositionsJson { get; set; }
        /// <summary>
        /// å½“前批次的电芯条码列表
        /// </summary>
        /// <remarks>
        /// æ¯æ¬¡è¯»å–新条码时设置为本批次的条码,仅用于 WMS æäº¤æ—¶æŒ‰æ‰¹æ¬¡æäº¤ã€‚
        /// æ¯æ¬¡æ–°æ‰¹æ¬¡è¯»å–时覆盖,在 allputfinished æ—¶æ¸…空。
        /// </remarks>
        [SugarColumn(Length = 5000, ColumnDescription = "当前批次的电芯条码列表", IsNullable = true)]
        public string? CurrentBatchBarcodes { get; set; }
        /// <summary>
        /// ç”µæ± /货位条码列表(JSON)
@@ -158,5 +168,15 @@
        /// </summary>
        [SugarColumn(ColumnDescription = "电芯是否到位")]
        public bool BatteryArrived { get; set; }
        /// <summary>
        /// å½“前执行中的机器人任务编号
        /// </summary>
        /// <remarks>
        /// ä¸‹å‘任务时缓存任务编号,用于 RobotJob å¿«é€ŸæŸ¥æ‰¾æ‰§è¡Œä¸­çš„任务,
        /// é¿å…æ¯æ¬¡è½®è¯¢å…¨è¡¨æ‰«æã€‚任务完成时清空为 null。
        /// </remarks>
        [SugarColumn(ColumnDescription = "当前执行中的机器人任务编号")]
        public int? CurrentTaskNum { get; set; }
    }
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Server/appsettings.json
@@ -112,7 +112,7 @@
  },
  "RedisConfig": {
    "Enabled": true, //是否启用Redis,false时仅使用内存缓存
    "ConnectionString": "127.0.0.1:6379,password=,defaultDatabase=0,connectTimeout=5000,abortConnect=false", //Redis连接字符串
    "ConnectionString": "127.0.0.1:6379,password=P@ssw0rd,defaultDatabase=0,connectTimeout=5000,abortConnect=false", //Redis连接字符串
    "InstanceName": "WIDESEAWCS:", //实例名称,用于区分不同应用
    "DefaultDatabase": 0, //默认数据库索引(0-15)
    "EnableSentinel": false, //是否启用哨兵模式
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_TaskInfoRepository/RobotStateRepository.cs
@@ -48,10 +48,24 @@
        {
            newState.ModifyDate = DateTime.Now;
            // ä¹è§‚锁:WHERE IPAddress = @ip AND Version = @expectedVersion,版本匹配才更新
            var affectedRows = Db.Updateable<Dt_RobotState>(newState)
            // SqlSugar çš„ Updateable(entity).Where(x => x.Version == param) å­˜åœ¨å‚数混淆问题:
            // å®žä½“çš„ Version å·²è¢«è®¾ä¸º expectedVersion+1,.Where() ä¸­ SqlSugar å¯èƒ½ä½¿ç”¨å®žä½“çš„
            // Version å€¼ï¼ˆexpectedVersion+1)而非参数值(expectedVersion),导致 WHERE æ°¸è¿œåŒ¹é…ä¸ä¸Šã€‚
            // ä¿®å¤ï¼šå°†ç‰ˆæœ¬æ ¡éªŒæ‹†ä¸ºç‹¬ç«‹æŸ¥è¯¢ï¼Œæ›´æ–°ä»…通过主键执行。
            // æ­¥éª¤1:校验版本号是否与期望一致
            var currentVersion = Db.Queryable<Dt_RobotState>()
                .Where(x => x.IpAddress == ipAddress)
                .ExecuteCommand();
                .Select(x => x.Version)
                .First();
            if (currentVersion != expectedVersion)
            {
                return false;
            }
            // æ­¥éª¤2:版本匹配,通过主键直接更新
            var affectedRows = Db.Updateable(newState).ExecuteCommand();
            return affectedRows > 0;
        }
@@ -76,13 +90,19 @@
                CurrentBatchIndex = entity.CurrentBatchIndex,
                ChangePalletPhase = entity.ChangePalletPhase,
                IsScanNG = entity.IsScanNG,
                BatteryArrived = entity.BatteryArrived
                BatteryArrived = entity.BatteryArrived,
                CurrentTaskNum = entity.CurrentTaskNum,
            };
            // ååºåˆ—化复杂 JSON å­—段
            if (!string.IsNullOrEmpty(entity.RobotCraneJson))
            {
                state.RobotCrane = JsonConvert.DeserializeObject<RobotCraneDevice>(entity.RobotCraneJson);
            }
            if (!string.IsNullOrEmpty(entity.CurrentBatchBarcodes))
            {
                state.CurrentBatchBarcodes = JsonConvert.DeserializeObject<List<string>>(entity.CurrentBatchBarcodes) ?? new List<string>();
            }
            if (!string.IsNullOrEmpty(entity.CurrentTaskJson))
@@ -132,6 +152,8 @@
                LastPickPositionsJson = state.LastPickPositions.ToJson(),
                CurrentTaskJson = state.CurrentTask.ToJson(),
                LastPutPositionsJson = state.LastPutPositions.ToJson(),
                CurrentBatchBarcodes = state.CurrentBatchBarcodes.ToJson(),
                CurrentTaskNum = state.CurrentTaskNum,
            };
            // åºåˆ—化复杂对象为 JSON
@@ -140,27 +162,32 @@
                entity.RobotCraneJson = JsonConvert.SerializeObject(state.RobotCrane);
            }
            if (state.CurrentTask != null)
            {
                entity.CurrentTaskJson = JsonConvert.SerializeObject(state.CurrentTask);
            }
            //if (state.CurrentTask != null)
            //{
            //    entity.CurrentTaskJson = JsonConvert.SerializeObject(state.CurrentTask);
            //}
            if (state.LastPickPositions != null)
            {
                entity.LastPickPositionsJson = JsonConvert.SerializeObject(state.LastPickPositions);
            }
            //if (state.LastPickPositions != null)
            //{
            //    entity.LastPickPositionsJson = JsonConvert.SerializeObject(state.LastPickPositions);
            //}
            if (state.LastPutPositions != null)
            {
                entity.LastPutPositionsJson = JsonConvert.SerializeObject(state.LastPutPositions);
            }
            //if (state.LastPutPositions != null)
            //{
            //    entity.LastPutPositionsJson = JsonConvert.SerializeObject(state.LastPutPositions);
            //}
            if (state.CellBarcode != null && state.CellBarcode.Count > 0)
            {
                entity.CellBarcodeJson = JsonConvert.SerializeObject(state.CellBarcode);
            }
            //if (state.CellBarcode != null && state.CellBarcode.Count > 0)
            //{
            //    entity.CellBarcodeJson = JsonConvert.SerializeObject(state.CellBarcode);
            //}
            //if (state.CurrentBatchBarcodes != null && state.CurrentBatchBarcodes.Count > 0)
            //{
            //    entity.CurrentBatchBarcodes = JsonConvert.SerializeObject(state.CurrentBatchBarcodes);
            //}
            return entity;
        }
    }
}
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotJob.cs
@@ -203,46 +203,65 @@
                    return;
                }
                if (state.CurrentAction == "Picking" || state.CurrentAction == "Puting")
                {
                    return;
                }
                // è½®è¯¢èŽ·å–è¯¥è®¾å¤‡çš„å¾…å¤„ç†ä»»åŠ¡
                // ä¼˜å…ˆé€šè¿‡çŠ¶æ€ä¸­ç¼“å­˜çš„ä»»åŠ¡ç¼–å·æŸ¥æ‰¾æ‰§è¡Œä¸­çš„ä»»åŠ¡
                Dt_RobotTask? task = null;
                if (state.CurrentTaskNum.HasValue)
                {
                    task = _taskProcessor.GetTaskByNum(state.CurrentTaskNum.Value);
                }
                // èŽ·å–è¯¥è®¾å¤‡çš„äº’æ–¥é”ï¼Œç¡®ä¿ Job æ‰§è¡Œä¸Žæ¶ˆæ¯å¤„理互斥
                var robotLock = _stateManager.GetOrCreateLock(ipAddress);
                // ç¼“存的任务号未找到对应任务时,按设备编码获取新任务
                task ??= _taskProcessor.GetTask(robotCrane);
                // å¦‚果没有获取到待处理任务,且RobotArmObject为1(有物料),则获取该设备执行中的任务
                //if (task == null && state.RobotArmObject == 1)
                // é”è¢«æ¶ˆæ¯å¤„理器占用时直接跳过本次 tick,等下次调度再执行
                //if (!await robotLock.WaitAsync(TimeSpan.Zero))
                //{
                //    task = _taskProcessor.GetExecutingTask(robotCrane);
                //    return;
                //}
                // å¦‚果有待处理任务
                if (task != null)
                // èŽ·å–è¯¥è®¾å¤‡çš„äº’æ–¥é”ï¼Œç¡®ä¿ Job æ‰§è¡Œä¸Žæ¶ˆæ¯å¤„理互斥
                await robotLock.WaitAsync();
                try
                {
                    // èŽ·å–æœ€æ–°çš„è®¾å¤‡çŠ¶æ€
                    var latestState = _stateManager.GetState(ipAddress);
                    if (latestState == null)
                    if (state.CurrentAction == "Picking" || state.CurrentAction == "Puting")
                    {
                        // çŠ¶æ€ä¸å­˜åœ¨ï¼Œå¯èƒ½è®¾å¤‡æœªåˆå§‹åŒ–
                        return;
                    }
                    // æ£€æŸ¥ä»»åŠ¡æ€»æ•°æ˜¯å¦æœªè¾¾åˆ°ä¸Šé™
                    if (latestState.RobotTaskTotalNum < RobotConst.MaxTaskTotalNum)
                    // è½®è¯¢èŽ·å–è¯¥è®¾å¤‡çš„å¾…å¤„ç†ä»»åŠ¡
                    // ä¼˜å…ˆé€šè¿‡çŠ¶æ€ä¸­ç¼“å­˜çš„ä»»åŠ¡ç¼–å·æŸ¥æ‰¾æ‰§è¡Œä¸­çš„ä»»åŠ¡
                    Dt_RobotTask? task = null;
                    if (state.CurrentTaskNum.HasValue)
                    {
                        // è°ƒç”¨å·¥ä½œæµç¼–排器执行任务
                        // ç¼–排器会根据当前状态决定下一步动作
                        await _workflowOrchestrator.ExecuteAsync(latestState, task, ipAddress);
                        task = _taskProcessor.GetTaskByNum(state.CurrentTaskNum.Value);
                    }
                    // ç¼“存的任务号未找到对应任务时,按设备编码获取新任务
                    task ??= _taskProcessor.GetTask(robotCrane);
                    // å¦‚果没有获取到待处理任务,且RobotArmObject为1(有物料),则获取该设备执行中的任务
                    //if (task == null && state.RobotArmObject == 1)
                    //{
                    //    task = _taskProcessor.GetExecutingTask(robotCrane);
                    //}
                    // å¦‚果有待处理任务
                    if (task != null)
                    {
                        // èŽ·å–æœ€æ–°çš„è®¾å¤‡çŠ¶æ€
                        var latestState = _stateManager.GetState(ipAddress);
                        if (latestState == null)
                        {
                            // çŠ¶æ€ä¸å­˜åœ¨ï¼Œå¯èƒ½è®¾å¤‡æœªåˆå§‹åŒ–
                            return;
                        }
                        // æ£€æŸ¥ä»»åŠ¡æ€»æ•°æ˜¯å¦æœªè¾¾åˆ°ä¸Šé™
                        if (latestState.RobotTaskTotalNum < RobotConst.MaxTaskTotalNum)
                        {
                            // è°ƒç”¨å·¥ä½œæµç¼–排器执行任务
                            // ç¼–排器会根据当前状态决定下一步动作
                            await _workflowOrchestrator.ExecuteAsync(latestState, task, ipAddress);
                        }
                    }
                }
                finally
                {
                    robotLock.Release();
                }
            }
            catch (Exception ex)
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotMessageHandler.cs
@@ -89,47 +89,60 @@
        public async Task<string?> HandleMessageReceivedAsync(string message, bool isJson, TcpClient client)
        {
            var state = _stateManager.GetState(client.Client.RemoteEndPoint.ToString());
            if (state.OperStatus == message)
            {
                // å¤„理成功后,将原消息回写到客户端(保持原有行为)
                //await _socketClientGateway.SendMessageAsync(client, message);
            }
            // è®°å½•接收到的消息日志
            QuartzLogHelper.LogInfo(_logger, $"接收到客户端【{state.RobotCrane?.DeviceName}】发送消息【{message}】", state.RobotCrane?.DeviceName);
            // æ£€æŸ¥ä»»åŠ¡æ€»æ•°æ˜¯å¦æœªè¾¾åˆ°ä¸Šé™
            if (state.RobotTaskTotalNum > RobotConst.MaxTaskTotalNum)
            // è®¾å¤‡çŠ¶æ€ä¸å­˜åœ¨æ—¶ç›´æŽ¥è¿”å›žï¼Œé¿å…åŽç»­ç©ºå¼•ç”¨
            if (state == null)
            {
                return null;
            }
            // èŽ·å–è¯¥è®¾å¤‡çš„äº’æ–¥é”ï¼Œç¡®ä¿æ¶ˆæ¯å¤„ç†ä¸Ž Job æ‰§è¡Œäº’æ–¥
            // Job åœ¨å¤„理任务时会等待此锁释放后才继续执行
            var robotLock = _stateManager.GetOrCreateLock(state.IPAddress);
            await robotLock.WaitAsync();
            try
            {
                // è®°å½•接收到的消息日志
                QuartzLogHelper.LogInfo(_logger, $"接收到客户端【{state.RobotCrane?.DeviceName}】发送消息【{message}】", state.RobotCrane?.DeviceName);
                await _socketClientGateway.SendMessageAsync(client, message);
                return null;
                // æ£€æŸ¥ä»»åŠ¡æ€»æ•°æ˜¯å¦æœªè¾¾åˆ°ä¸Šé™
                if (state.RobotTaskTotalNum > RobotConst.MaxTaskTotalNum)
                {
                    QuartzLogHelper.LogInfo(_logger, $"接收到客户端【{state.RobotCrane?.DeviceName}】发送消息【{message}】", state.RobotCrane?.DeviceName);
                    await _socketClientGateway.SendMessageAsync(client, message);
                    return null;
                }
                // å°†æ¶ˆæ¯è½¬æ¢ä¸ºå°å†™ï¼ˆç”¨äºŽç®€å•命令匹配)
                string messageLower = message.ToLowerInvariant();
                // å°è¯•用简单命令处理器处理
                // ç®€å•命令包括:homing、homed、running、pausing、runmode、controlmode ç­‰
                if (await _simpleCommandHandler.HandleAsync(messageLower, state))
                {
                    //if (messageLower != "batteryarrived")
                    //{
                    // å¤„理成功后,将原消息回写到客户端(保持原有行为)
                    //await _socketClientGateway.SendMessageAsync(client, message);
                    QuartzLogHelper.LogInfo(_logger, $"接收到消息消息【{message}】,约定不返回发送消息:【{message}】", state.RobotCrane?.DeviceName);
                    //}
                    //// å®‰å…¨æ›´æ–°çŠ¶æ€åˆ°æ•°æ®åº“
                    //_stateManager.TryUpdateStateSafely(state.IPAddress, state);
                    return null;
                }
                // å¦‚果不是简单命令,检查是否是前缀命令
                // å‰ç¼€å‘½ä»¤åŒ…括:pickfinished、putfinished(后面跟逗号分隔的位置参数)
                if (_prefixCommandHandler.IsPrefixCommand(messageLower))
                {
                    // è°ƒç”¨å‰ç¼€å‘½ä»¤å¤„理器
                    await _prefixCommandHandler.HandleAsync(message, state, client);
                }
            }
            // å°†æ¶ˆæ¯è½¬æ¢ä¸ºå°å†™ï¼ˆç”¨äºŽç®€å•命令匹配)
            string messageLower = message.ToLowerInvariant();
            // å°è¯•用简单命令处理器处理
            // ç®€å•命令包括:homing、homed、running、pausing、runmode、controlmode ç­‰
            if (await _simpleCommandHandler.HandleAsync(messageLower, state))
            finally
            {
                //if (messageLower != "batteryarrived")
                //{
                // å¤„理成功后,将原消息回写到客户端(保持原有行为)
                //await _socketClientGateway.SendMessageAsync(client, message);
                QuartzLogHelper.LogInfo(_logger, $"接收到消息消息【{message}】,约定不返回发送消息:【{message}】", state.RobotCrane?.DeviceName);
                //}
                // å®‰å…¨æ›´æ–°çŠ¶æ€åˆ°æ•°æ®åº“
                _stateManager.TryUpdateStateSafely(state.IPAddress, state);
                return null;
            }
            // å¦‚果不是简单命令,检查是否是前缀命令
            // å‰ç¼€å‘½ä»¤åŒ…括:pickfinished、putfinished(后面跟逗号分隔的位置参数)
            if (_prefixCommandHandler.IsPrefixCommand(messageLower))
            {
                // è°ƒç”¨å‰ç¼€å‘½ä»¤å¤„理器
                await _prefixCommandHandler.HandleAsync(message, state, client);
                robotLock.Release();
            }
            return null;
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotStateManager.cs
@@ -1,3 +1,4 @@
using System.Collections.Concurrent;
using Newtonsoft.Json;
using Serilog;
using WIDESEAWCS_ITaskInfoRepository;
@@ -11,6 +12,7 @@
    /// <remarks>
    /// æ ¸å¿ƒåŠŸèƒ½æ˜¯é€šè¿‡ IRobotStateRepository ç®¡ç†æ•°æ®åº“中的机械手状态。
    /// æä¾›ä¹è§‚并发控制,通过 Version å­—段防止并发更新时的数据覆盖问题。
    /// åŒæ—¶æä¾›åŸºäºŽ SemaphoreSlim çš„互斥锁,确保消息处理与 Job æ‰§è¡Œä¸ä¼šå¹¶å‘操作同一设备状态。
    /// </remarks>
    public class RobotStateManager
    {
@@ -25,6 +27,15 @@
        private readonly ILogger _logger;
        /// <summary>
        /// æ¯ä¸ªè®¾å¤‡çš„异步互斥锁字典,用于 Job æ‰§è¡Œä¸Žæ¶ˆæ¯å¤„理之间的互斥
        /// </summary>
        /// <remarks>
        /// Key ä¸ºè®¾å¤‡ IP åœ°å€ï¼ŒValue ä¸ºè¯¥è®¾å¤‡ä¸“属的 SemaphoreSlim(1,1)。
        /// ç¡®ä¿åŒä¸€è®¾å¤‡çš„ Job è½®è¯¢å’Œ TCP æ¶ˆæ¯å¤„理不会同时操作状态。
        /// </remarks>
        private readonly ConcurrentDictionary<string, SemaphoreSlim> _robotLocks = new();
        /// <summary>
        /// æž„造函数
        /// </summary>
        /// <param name="repository">仓储服务实例</param>
@@ -36,6 +47,20 @@
        }
        /// <summary>
        /// èŽ·å–æˆ–åˆ›å»ºæŒ‡å®šè®¾å¤‡çš„å¼‚æ­¥äº’æ–¥é”
        /// </summary>
        /// <remarks>
        /// ä½¿ç”¨ ConcurrentDictionary ç¡®ä¿æ¯ä¸ªè®¾å¤‡ IP å¯¹åº”唯一的 SemaphoreSlim(1,1)。
        /// è¯¥é”ç”¨äºŽ Job æ‰§è¡Œä¸Ž TCP æ¶ˆæ¯å¤„理之间的互斥,防止并发操作同一设备状态。
        /// </remarks>
        /// <param name="ipAddress">设备 IP åœ°å€</param>
        /// <returns>该设备的信号量实例</returns>
        public SemaphoreSlim GetOrCreateLock(string ipAddress)
        {
            return _robotLocks.GetOrAdd(ipAddress, _ => new SemaphoreSlim(1, 1));
        }
        /// <summary>
        /// å®‰å…¨æ›´æ–° RobotSocketState,防止并发覆盖
        /// </summary>
        /// <remarks>
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Workflow/RobotSimpleCommandHandler.cs
@@ -1,8 +1,5 @@
using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup;
using Serilog;
using System.Net;
using WIDESEAWCS_Common.TaskEnum;
using WIDESEAWCS_Core.LogHelper;
using WIDESEAWCS_Model.Models;
using WIDESEAWCS_Tasks.Workflow.Abstractions;
@@ -108,8 +105,20 @@
                case "homed":
                    state.Homed = "Homed";
                    await _socketClientGateway.SendToClientAsync(state.IPAddress, "Homed");
                    return true;
                    if (_stateManager.TryUpdateStateSafely(state.IPAddress, state))
                    {
                        for (int i = 0; i < 5; i++)
                        {
                            QuartzLogHelper.LogDebug(_logger, $"发送回零完成消息,IP: {state.IPAddress},正在重试...", state.RobotCrane?.DeviceName ?? "Unknown");
                            await _socketClientGateway.SendToClientAsync(state.IPAddress, "Homed");
                        }
                        return true;
                    }
                    else
                    {
                        QuartzLogHelper.LogError(_logger, $"发送回零完成消息失败,IP: {state.IPAddress}", state.RobotCrane?.DeviceName ?? "Unknown");
                        return false;
                    }
                // æœºå™¨äººæ­£åœ¨è¿è¡Œ
                case "running":
@@ -144,14 +153,42 @@
                // æ˜¯å¦ç”µèŠ¯åˆ°ä½
                case "batteryarrived":
                    state.BatteryArrived = true;
                    return true;
                // æ˜¯å¦ç”µèŠ¯åˆ°ä½
                    if (_stateManager.TryUpdateStateSafely(state.IPAddress, state))
                    {
                        for (int i = 0; i < 5; i++)
                        {
                            QuartzLogHelper.LogDebug(_logger, $"发送电芯到位消息,IP: {state.IPAddress},正在重试...", state.RobotCrane?.DeviceName ?? "Unknown");
                            await _socketClientGateway.SendToClientAsync(state.IPAddress, "batteryarrived");
                        }
                        return true;
                    }
                    else
                    {
                        QuartzLogHelper.LogError(_logger, $"发送电芯未到位消息失败,IP: {state.IPAddress}", state.RobotCrane?.DeviceName ?? "Unknown");
                        return false;
                    }
                // æ˜¯å¦ç”µèŠ¯æ²¡åˆ°ä½
                case "batteryarrivedno":
                    state.BatteryArrived = false;
                    await _socketClientGateway.SendToClientAsync(state.IPAddress, "batteryarrivedno");
                    return true;
                    if (_stateManager.TryUpdateStateSafely(state.IPAddress, state))
                    {
                        for (int i = 0; i < 5; i++)
                        {
                            QuartzLogHelper.LogDebug(_logger, $"发送电芯未到位消息,IP: {state.IPAddress},正在重试...", state.RobotCrane?.DeviceName ?? "Unknown");
                            await _socketClientGateway.SendToClientAsync(state.IPAddress, "batteryarrivedno");
                        }
                        return true;
                    }
                    else
                    {
                        QuartzLogHelper.LogError(_logger, $"发送电芯未到位消息失败,IP: {state.IPAddress}", state.RobotCrane?.DeviceName ?? "Unknown");
                        return false;
                    }
                // ==================== æŽ¥æ”¶ä»»åŠ¡åé¦ˆ ====================
                // å–货接收
@@ -160,8 +197,20 @@
                    if (!isResult)
                        return false;
                    await _socketClientGateway.SendToClientAsync(state.IPAddress, "pickbatteryover");
                    return true;
                    if (_stateManager.TryUpdateStateSafely(state.IPAddress, state))
                    {
                        for (int i = 0; i < 5; i++)
                        {
                            QuartzLogHelper.LogDebug(_logger, $"发送取货接收完成,IP: {state.IPAddress},正在重试...", state.RobotCrane?.DeviceName ?? "Unknown");
                            await _socketClientGateway.SendToClientAsync(state.IPAddress, "pickbatteryover");
                        }
                        return true;
                    }
                    else
                    {
                        QuartzLogHelper.LogError(_logger, $"发送取货完成消息失败,IP: {state.IPAddress}", state.RobotCrane?.DeviceName ?? "Unknown");
                        return false;
                    }
                // æ”¾è´§æŽ¥æ”¶
                case "putbatteryover":
@@ -169,8 +218,20 @@
                    if (!isResult)
                        return false;
                    await _socketClientGateway.SendToClientAsync(state.IPAddress, "putbatteryover");
                    return true;
                    if (_stateManager.TryUpdateStateSafely(state.IPAddress, state))
                    {
                        for (int i = 0; i < 5; i++)
                        {
                            QuartzLogHelper.LogDebug(_logger, $"发送放货接收消息,IP: {state.IPAddress},正在重试...", state.RobotCrane?.DeviceName ?? "Unknown");
                            await _socketClientGateway.SendToClientAsync(state.IPAddress, "putbatteryover");
                        }
                        return true;
                    }
                    else
                    {
                        QuartzLogHelper.LogError(_logger, $"发送放货完成消息失败,IP: {state.IPAddress}", state.RobotCrane?.DeviceName ?? "Unknown");
                        return false;
                    }
                // ==================== å…¨éƒ¨å®Œæˆå‘½ä»¤ ====================
@@ -194,15 +255,6 @@
                        // æ¢ç›˜ä»»åŠ¡ï¼šæ ¹æ®é˜¶æ®µåŒºåˆ†å¤„ç†
                        if (robotTaskType == RobotTaskTypeEnum.ChangePallet)
                        {
                            // è°ƒç”¨æ‰¹é‡æ‹†ç›˜ç¡®è®¤æŽ¥å£
                            var sourcePallet = state.CurrentTask.RobotSourceAddressPalletCode;
                            var confirmResult = _taskProcessor.PostSplitPalletConfirmAsync(sourcePallet, state.RobotCrane?.DeviceName);
                            if (!confirmResult.IsSuccess && !confirmResult.Data.Status)
                            {
                                QuartzLogHelper.LogError(_logger, $"批量拆盘确认失败: {confirmResult.ErrorMessage}", state.RobotCrane?.DeviceName ?? "Unknown");
                                return false;
                            }
                            if (state.ChangePalletPhase == 5)
                            {
                                // FlowB æœ€ç»ˆé˜¶æ®µï¼šå‡ç”µèŠ¯å–å®Œï¼Œæºç©ºæ‰˜ç›˜å›žåº“ HCSC1
@@ -217,9 +269,6 @@
                                    return false;
                                }
                                await _socketClientGateway.SendToClientAsync(state.IPAddress, $"Group,diskFinished");
                                QuartzLogHelper.LogInfo(_logger, $"发送消息:【Group,diskFinished】", state.RobotCrane.DeviceName);
                                state.CurrentTask = null;
                                state.CurrentTaskNum = null;
                                state.RobotTaskTotalNum = 0;
@@ -228,7 +277,17 @@
                                state.ChangePalletPhase = 0;
                                state.CurrentBatchIndex = 1;
                                state.IsInFakeBatteryMode = false;
                                return true;
                                if (_stateManager.TryUpdateStateSafely(state.IPAddress, state))
                                {
                                    await _socketClientGateway.SendToClientAsync(state.IPAddress, $"Group,diskFinished");
                                    QuartzLogHelper.LogInfo(_logger, $"发送消息:【Group,diskFinished】", state.RobotCrane.DeviceName);
                                    return true;
                                }
                                else
                                {
                                    return false;
                                }
                            }
                            else if (state.ChangePalletPhase != 0)
                            {
@@ -254,13 +313,19 @@
                            //    return false;
                            //}
                            await _socketClientGateway.SendToClientAsync(state.IPAddress, $"Group,diskFinished");
                            QuartzLogHelper.LogInfo(_logger, $"发送消息:【Group,diskFinished】", state.RobotCrane.DeviceName);
                            state.ChangePalletPhase = 0;
                            state.CurrentBatchIndex = 1;
                            state.IsInFakeBatteryMode = false;
                            return true;
                            if (_stateManager.TryUpdateStateSafely(state.IPAddress, state))
                            {
                                await _socketClientGateway.SendToClientAsync(state.IPAddress, $"Group,diskFinished");
                                QuartzLogHelper.LogInfo(_logger, $"发送消息:【Group,diskFinished】", state.RobotCrane.DeviceName);
                                return true;
                            }
                            else
                            {
                                return false;
                            }
                        }
                        // æ‹†ç›˜ä»»åŠ¡ï¼šç›´æŽ¥å¤„ç†å…¥åº“
@@ -287,9 +352,16 @@
                                return false;
                            }
                            await _socketClientGateway.SendToClientAsync(state.IPAddress, $"Group,diskFinished");
                            QuartzLogHelper.LogInfo(_logger, $"发送消息:【Group,diskFinished】", state.RobotCrane.DeviceName);
                            return true;
                            if (_stateManager.TryUpdateStateSafely(state.IPAddress, state))
                            {
                                await _socketClientGateway.SendToClientAsync(state.IPAddress, $"Group,diskFinished");
                                QuartzLogHelper.LogInfo(_logger, $"发送消息:【Group,diskFinished】", state.RobotCrane.DeviceName);
                                return true;
                            }
                            else
                            {
                                return false;
                            }
                        }
                        return false;
                    }
@@ -313,6 +385,15 @@
                        // æ¢ç›˜ä»»åŠ¡ï¼šæ ¹æ®é˜¶æ®µåŒºåˆ†å¤„ç†
                        if (robotTaskType == RobotTaskTypeEnum.ChangePallet)
                        {
                            // è°ƒç”¨æ‰¹é‡æ‹†ç›˜ç¡®è®¤æŽ¥å£
                            var sourcePallet = state.CurrentTask.RobotSourceAddressPalletCode;
                            var splitConfirmResult = _taskProcessor.PostSplitPalletConfirmAsync(sourcePallet, state.RobotCrane?.DeviceName);
                            if (!splitConfirmResult.IsSuccess && !splitConfirmResult.Data.Status)
                            {
                                QuartzLogHelper.LogError(_logger, $"批量拆盘确认失败: {splitConfirmResult.ErrorMessage}", state.RobotCrane?.DeviceName ?? "Unknown");
                                return false;
                            }
                            // è°ƒç”¨æ‰¹é‡ç»„盘确认接口
                            var targetPallet = state.CurrentTask.RobotTargetAddressPalletCode;
                            var confirmResult = _taskProcessor.PostGroupPalletConfirmAsync(targetPallet, state.RobotCrane?.DeviceName);
@@ -341,14 +422,20 @@
                                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);
                                state.ChangePalletPhase = 0;
                                state.CurrentBatchIndex = 1;
                                state.IsInFakeBatteryMode = false;
                                return true;
                                if (_stateManager.TryUpdateStateSafely(state.IPAddress, state))
                                {
                                    await _socketClientGateway.SendToClientAsync(state.IPAddress, $"Group,diskFinished");
                                    QuartzLogHelper.LogInfo(_logger, $"发送消息:【Group,diskFinished】", state.RobotCrane.DeviceName);
                                    return true;
                                }
                                else
                                {
                                    return false;
                                }
                            }
                            else if (state.ChangePalletPhase != 0)
                            {
@@ -379,14 +466,20 @@
                            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);
                            state.ChangePalletPhase = 0;
                            state.CurrentBatchIndex = 1;
                            state.IsInFakeBatteryMode = false;
                            return true;
                            if (_stateManager.TryUpdateStateSafely(state.IPAddress, state))
                            {
                                await _socketClientGateway.SendToClientAsync(state.IPAddress, $"Group,diskFinished");
                                QuartzLogHelper.LogInfo(_logger, $"发送消息:【Group,diskFinished】", state.RobotCrane.DeviceName);
                                return true;
                            }
                            else
                            {
                                return false;
                            }
                        }
                        // ç»„盘任务:直接处理入库
@@ -422,9 +515,16 @@
                            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);
                            return true;
                            if (_stateManager.TryUpdateStateSafely(state.IPAddress, state))
                            {
                                await _socketClientGateway.SendToClientAsync(state.IPAddress, $"Group,diskFinished");
                                QuartzLogHelper.LogInfo(_logger, $"发送消息:【Group,diskFinished】", state.RobotCrane.DeviceName);
                                return true;
                            }
                            else
                            {
                                return false;
                            }
                        }
                        return false;
                    }
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Workflow/RobotWorkflowOrchestrator.cs
@@ -1,3 +1,4 @@
using Masuit.Tools;
using Serilog;
using WIDESEAWCS_Common.TaskEnum;
using WIDESEAWCS_Core.Helper;
@@ -241,7 +242,6 @@
                    }
                    return;
                }
            }
            // éžæ¢ç›˜ä»»åŠ¡ï¼šä½¿ç”¨åŽŸæœ‰æ ¼å¼
@@ -337,7 +337,7 @@
                string trayBarcode1 = RobotBarcodeGenerator.GenerateTrayBarcode("DB40.1020");
                // å¦‚果条码生成成功
                if (!string.IsNullOrEmpty(trayBarcode1) && !string.IsNullOrEmpty(trayBarcode2))
                if (!trayBarcode1.IsNullOrEmpty() && !trayBarcode2.IsNullOrEmpty())
                {
                    //if (stateForUpdate.CellBarcode.Contains(trayBarcode1) || stateForUpdate.CellBarcode.Contains(trayBarcode2))
                    //{
@@ -357,21 +357,23 @@
                        QuartzLogHelper.LogInfo(_logger, $"HandlePutFinishedStateAsync:读取的电芯条码唯一,继续执行,任务号: {task.RobotTaskNum}", stateForUpdate?.RobotCrane?.DeviceName ?? ipAddress);
                        // è®°å½•日志:读取托盘条码成功
                        QuartzLogHelper.LogInfo(_logger, $"HandlePutFinishedStateAsync:读取电芯条码成功: ã€{trayBarcode1}】-----【{trayBarcode2}】,任务号: {task.RobotTaskNum}", stateForUpdate?.RobotCrane?.DeviceName ?? ipAddress);
                        // å°†æ¡ç ç´¯ç§¯åˆ° CellBarcode(去重),并设置当前批次条码
                        if (!stateForUpdate.CellBarcode.Contains(trayBarcode1))
                        if (!stateForUpdate.CellBarcode.Contains(trayBarcode1) || !stateForUpdate.CellBarcode.Contains(trayBarcode2))
                        {
                            stateForUpdate.CellBarcode.Add(trayBarcode1);
                        if (!stateForUpdate.CellBarcode.Contains(trayBarcode2))
                            stateForUpdate.CellBarcode.Add(trayBarcode2);
                        // è®¾ç½®å½“前批次条码,用于 WMS æäº¤
                        stateForUpdate.CurrentBatchBarcodes = new List<string>()
                        {
                            trayBarcode1, trayBarcode2
                        };
                            // è®¾ç½®å½“前批次条码,用于 WMS æäº¤
                            stateForUpdate.CurrentBatchBarcodes = new List<string>()
                            {
                                trayBarcode1, trayBarcode2
                            };
                        }
                    }
                    // è®°å½•日志:读取托盘条码成功
                    QuartzLogHelper.LogInfo(_logger, $"HandlePutFinishedStateAsync:读取电芯条码成功: ã€{trayBarcode1}】-----【{trayBarcode2}】,任务号: {task.RobotTaskNum}", stateForUpdate?.RobotCrane?.DeviceName ?? ipAddress);
                    // å‘送取货指令
                    await _taskProcessor.SendSocketRobotPickAsync(task, stateForUpdate);
@@ -408,7 +410,7 @@
                    return;
                }
                QuartzLogHelper.LogDebug(_logger,$"HandlePutFinishedStateAsync:换盘任务目标数量: {targetNormalCount},当前已完成数量: {currentCompletedCount},流向: {(isFlowA ? "A" : "B")},任务号: {task.RobotTaskNum}", stateForUpdate?.RobotCrane?.DeviceName ?? ipAddress);
                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/StackerCraneJob/CommonStackerCraneJob.cs
@@ -81,7 +81,7 @@
        /// <summary>
        /// æ—¥å¿—记录器
        /// </summary>
        private readonly ILogger  _logger;
        private readonly ILogger _logger;
        /// <summary>
        /// å †åž›æœºè®¾å¤‡ç¼–码
@@ -159,11 +159,8 @@
                    QuartzLogHelper.LogInfo(_logger, "Execute:订阅任务完成事件,设备: {DeviceCode}", "订阅任务完成事件", _deviceCode, _deviceCode);
                }
                QuartzLogHelper.LogInfo(_logger, $"开始检查堆垛机完成状态,【{_deviceCode}】", _deviceCode);
                // ========== æ£€æŸ¥å †åž›æœºä»»åŠ¡å®ŒæˆçŠ¶æ€ ==========
                commonStackerCrane.CheckStackerCraneTaskCompleted();
                QuartzLogHelper.LogInfo(_logger, $"检查完成,正在监听堆垛机任务完成,【{_deviceCode}】", _deviceCode);
                // ========== æ£€æŸ¥æ˜¯å¦å¯ä»¥å‘送新任务 ==========
                //if (!commonStackerCrane.IsCanSendTask(commonStackerCrane.Communicator, commonStackerCrane.DeviceProDTOs, commonStackerCrane.DeviceProtocolDetailDTOs))
@@ -172,8 +169,6 @@
                    // å †åž›æœºä¸å¯ç”¨ï¼ˆå¦‚正在执行上一任务),直接返回
                    return Task.CompletedTask;
                }
                QuartzLogHelper.LogInfo(_logger, $"堆垛机可下发任务,【{_deviceCode}】", _deviceCode);
                // ========== é€‰æ‹©ä»»åŠ¡ ==========
                // ä»»åŠ¡é€‰æ‹©ä¸‹æ²‰åˆ°ä¸“ç”¨é€‰æ‹©å™¨
@@ -184,20 +179,14 @@
                    return Task.CompletedTask;
                }
                QuartzLogHelper.LogInfo(_logger, $"获取到任务,开始构建任务下发命令,【{_deviceCode}】", _deviceCode);
                // ========== æž„建命令 ==========
                // å‘½ä»¤æž„建下沉到专用构建器
                object? stackerCraneTaskCommand = _commandBuilder.ConvertToStackerCraneTaskCommand(task);
                if (stackerCraneTaskCommand == null)
                {
                    // å‘½ä»¤æž„建失败
                    QuartzLogHelper.LogInfo(_logger, $"Execute:命令构建失败,设备: {_deviceCode},任务号: {task.TaskNum}", _deviceCode);
                    return Task.CompletedTask;
                }
                QuartzLogHelper.LogInfo(_logger, $"命令构建完成,开始下发任务,【{_deviceCode}】", _deviceCode);
                // ========== å‘送命令 ==========
                bool sendFlag = SendStackerCraneCommand(commonStackerCrane, stackerCraneTaskCommand);
Code/WMS/WIDESEA_WMSClient/src/api/http.js
@@ -12,7 +12,7 @@
let loadingInstance;
let loadingStatus = false;
if (process.env.NODE_ENV == 'development') {
    axios.defaults.baseURL = window.webConfig.webApiBaseUrl;
    axios.defaults.baseURL = window.webConfig.webApiProduction;
}
else if (process.env.NODE_ENV == 'debug') {
    axios.defaults.baseURL = window.webConfig.webApiBaseUrl;
Code/WMS/WIDESEA_WMSClient/src/router/viewGird.js
@@ -241,6 +241,11 @@
    name: 'outboundTimeConfig',
    component: () => import('@/views/system/outboundTimeConfig.vue')
  }
  , {
    path: '/palletOperation',
    name: 'palletOperation',
    component: () => import('@/views/taskinfo/palletOperation.vue')
  }
]
export default viewgird
Code/WMS/WIDESEA_WMSClient/src/views/taskinfo/palletOperation.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,185 @@
<template>
  <div class="pallet-operation-page">
    <el-card shadow="hover">
      <template #header>
        <span>组盘/拆盘操作</span>
      </template>
      <el-form :model="formData" ref="formRef" label-width="100px" style="max-width: 600px">
        <el-form-item label="操作类型" prop="action" required>
          <el-select v-model="formData.action" placeholder="请选择操作类型" @change="onActionChange">
            <el-option label="组盘" value="组盘" />
            <el-option label="拆盘" value="拆盘" />
          </el-select>
        </el-form-item>
        <el-form-item label="托盘号" prop="palletCode" required>
          <el-input v-model="formData.palletCode" placeholder="请输入托盘号" />
        </el-form-item>
        <el-form-item label="线体编号" prop="lineId" required>
          <el-input v-model="formData.lineId" placeholder="请输入线体编号" />
        </el-form-item>
        <el-form-item label="机械手名称" prop="robotName" required>
          <el-input v-model="formData.robotName" placeholder="请输入机械手名称" />
        </el-form-item>
        <el-form-item label="仓库编号" prop="warehouseCode" required>
          <el-input v-model="formData.warehouseCode" placeholder="请输入仓库编号" />
        </el-form-item>
        <el-form-item label="优先级" prop="grade">
          <el-input-number v-model="formData.grade" :min="1" :max="99" />
        </el-form-item>
      </el-form>
    </el-card>
    <!-- ç”µèŠ¯åˆ—è¡¨ï¼ˆç»„ç›˜æ—¶æ˜¾ç¤ºï¼‰ -->
    <el-card v-if="formData.action === '组盘'" shadow="hover" style="margin-top: 16px">
      <template #header>
        <div class="cell-header">
          <span>电芯列表</span>
          <el-button type="primary" size="small" @click="addCellRow">添加行</el-button>
        </div>
      </template>
      <el-table :data="formData.cells" border style="width: 100%" max-height="400">
        <el-table-column label="序号" width="60" align="center">
          <template v-slot="{ $index }">{{ $index + 1 }}</template>
        </el-table-column>
        <el-table-column label="电芯码" min-width="200">
          <template v-slot="{ row }">
            <el-input v-model="row.sfcCode" size="small" placeholder="请输入电芯码" />
          </template>
        </el-table-column>
        <el-table-column label="通道号" width="160">
          <template v-slot="{ row }">
            <el-input v-model="row.channel" size="small" placeholder="请输入通道号" />
          </template>
        </el-table-column>
        <el-table-column label="操作" width="80" align="center">
          <template v-slot="{ $index }">
            <el-button type="danger" size="small" link @click="removeCellRow($index)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
    </el-card>
    <div style="margin-top: 16px; text-align: center">
      <el-button type="primary" @click="submitForm" :loading="submitting">提交</el-button>
      <el-button @click="resetForm">重置</el-button>
    </div>
  </div>
</template>
<script>
import { ref, reactive, getCurrentInstance } from "vue";
export default {
  name: "PalletOperation",
  setup() {
    const { proxy } = getCurrentInstance();
    const formRef = ref(null);
    const submitting = ref(false);
    const defaultFormData = () => ({
      action: "",
      palletCode: "",
      lineId: "",
      robotName: "",
      warehouseCode: "",
      grade: 1,
      cells: [],
    });
    const formData = reactive(defaultFormData());
    /// æ“ä½œç±»åž‹å˜æ›´æ—¶ï¼Œç»„盘初始化一条空行,拆盘清空列表
    const onActionChange = (val) => {
      if (val === "组盘") {
        formData.cells = [{ sfcCode: "", channel: "" }];
      } else {
        formData.cells = [];
      }
    };
    /// æ·»åŠ ç”µèŠ¯è¡Œ
    const addCellRow = () => {
      formData.cells.push({ sfcCode: "", channel: "" });
    };
    /// ç§»é™¤ç”µèŠ¯è¡Œ
    const removeCellRow = (index) => {
      formData.cells.splice(index, 1);
    };
    /// æäº¤è¡¨å•
    const submitForm = () => {
      if (!formData.action) return proxy.$message.error("请选择操作类型");
      if (!formData.palletCode) return proxy.$message.error("请输入托盘号");
      if (!formData.lineId) return proxy.$message.error("请输入线体编号");
      if (!formData.robotName) return proxy.$message.error("请输入机械手名称");
      if (!formData.warehouseCode) return proxy.$message.error("请输入仓库编号");
      if (formData.action === "组盘") {
        if (formData.cells.length === 0) return proxy.$message.error("组盘时至少需要一个电芯");
        for (let i = 0; i < formData.cells.length; i++) {
          if (!formData.cells[i].sfcCode) return proxy.$message.error(`第${i + 1}行电芯码不能为空`);
          if (!formData.cells[i].channel) return proxy.$message.error(`第${i + 1}行通道号不能为空`);
        }
      }
      const payload = {
        action: formData.action,
        palletCode: formData.palletCode,
        lineId: formData.lineId,
        robotName: formData.robotName,
        warehouseCode: formData.warehouseCode,
        grade: formData.grade,
        cells: formData.action === "组盘" ? formData.cells : null,
      };
      submitting.value = true;
      proxy.http
        .post("/api/Task/PalletOperation", payload, "提交中...")
        .then((res) => {
          submitting.value = false;
          if (!res.status) return proxy.$message.error(res.message);
          proxy.$message.success(res.message || "操作成功");
        })
        .catch(() => {
          submitting.value = false;
        });
    };
    /// é‡ç½®è¡¨å•
    const resetForm = () => {
      Object.assign(formData, defaultFormData());
    };
    return {
      formRef,
      formData,
      submitting,
      onActionChange,
      addCellRow,
      removeCellRow,
      submitForm,
      resetForm,
    };
  },
};
</script>
<style scoped>
.pallet-operation-page {
  padding: 20px;
}
.cell-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
</style>
Code/WMS/WIDESEA_WMSServer/WIDESEA_Core/Middlewares/ApiLogMiddleware.cs
@@ -14,6 +14,18 @@
    public class ApiLogMiddleware
    {
        /// <summary>
        /// ä¸å†™å…¥ Sys_Log è¡¨çš„æŽ¥å£è·¯å¾„(只写文件日志)
        /// </summary>
        private static readonly string[] _apiLogSkipPaths =
        {
            "InOrOutCompleted",
            "SendLocationStatus",
            "RequestOutbound",
            "GetPalletCodeCell",
            "GetOutBoundTrayTask"
        };
        /// <summary>
        ///
        /// </summary>
        private readonly RequestDelegate _next;
@@ -21,6 +33,15 @@
        public ApiLogMiddleware(RequestDelegate next, ILogger<ApiLogMiddleware> logger)
        {
            _next = next;
        }
        /// <summary>
        /// åˆ¤æ–­å½“前请求路径是否在跳过数据库日志列表中
        /// </summary>
        private static bool IsApiLogSkipPath(string? path)
        {
            if (string.IsNullOrEmpty(path)) return false;
            return _apiLogSkipPaths.Any(skip => path.Contains(skip));
        }
        //todo
@@ -87,7 +108,8 @@
                    ms.Position = 0;
                    await ms.CopyToAsync(originalBody);
                    if (!ignoreUrls.Any(x => context.Request.Path.Value?.Contains(x) ?? false))
                    if (!ignoreUrls.Any(x => context.Request.Path.Value?.Contains(x) ?? false)
                        && !IsApiLogSkipPath(context.Request.Path.Value))
                    {
                        Logger.Add(requestParam, responseParam);
                    }
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService.cs
@@ -1,23 +1,14 @@
using Mapster;
using MapsterMapper;
using Microsoft.Extensions.Configuration;
using SqlSugar;
using System.DirectoryServices.Protocols;
using System.Text.Json;
using WIDESEA_Common.Constants;
using WIDESEA_Common.LocationEnum;
using WIDESEA_Common.StockEnum;
using WIDESEA_Common.TaskEnum;
using WIDESEA_Common.WareHouseEnum;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using SqlSugar;
using WIDESEA_Common.Constants;
using WIDESEA_Common.TaskEnum;
using WIDESEA_Core;
using WIDESEA_Core.BaseRepository;
using WIDESEA_Core.BaseServices;
using WIDESEA_Core.Core;
using WIDESEA_Core.Enums;
using WIDESEA_Core.Helper;
using WIDESEA_DTO.GradingMachine;
using WIDESEA_DTO.MES;
using WIDESEA_DTO.Task;
using WIDESEA_IBasicService;
using WIDESEA_IRecordService;
@@ -45,6 +36,7 @@
        private readonly IMesUploadHelper _mesUploadHelper;
        private readonly ISqlSugarClient _sqlSugarClient;
        private readonly IOptionsMonitor<OutboundTimeConfigOptions> _outboundTimeOptions;
        private readonly ILogger<TaskService> _logger;
        public IRepository<Dt_Task> Repository => BaseDal;
@@ -74,7 +66,8 @@
            IMesLogService mesLogService,
            IMesUploadHelper mesUploadHelper,
            ISqlSugarClient sqlSugarClient,
            IOptionsMonitor<OutboundTimeConfigOptions> outboundTimeOptions) : base(BaseDal)
            IOptionsMonitor<OutboundTimeConfigOptions> outboundTimeOptions,
            ILogger<TaskService> logger) : base(BaseDal)
        {
            _mapper = mapper;
            _stockInfoService = stockInfoService;
@@ -92,6 +85,7 @@
            _mesUploadHelper = mesUploadHelper;
            _sqlSugarClient = sqlSugarClient;
            _outboundTimeOptions = outboundTimeOptions;
            _logger = logger;
        }
        /// <summary>
@@ -206,6 +200,5 @@
            // Remark ä¸ºç©ºæ—¶ï¼Œå›žé€€åˆ°å··é“配置
            return DetermineTargetAddress(roadway, addressMap);
        }
    }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService_GradingMachine.cs
@@ -1,4 +1,7 @@
using Serilog;
using System.Text.Json;
using System.Text.Encodings.Web;
using System.Text.Unicode;
using WIDESEA_Common.Constants;
using WIDESEA_Common.StockEnum;
using WIDESEA_Common.TaskEnum;
@@ -11,6 +14,13 @@
{
    public partial class TaskService
    {
        /// <summary>
        /// JSON序列化选项(中文不转义)
        /// </summary>
        private static readonly JsonSerializerOptions _jsonOptions = new()
        {
            Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
        };
        #region åˆ†å®¹æŸœæŽ¥å£
        /// <summary>
@@ -18,10 +28,14 @@
        /// </summary>
        public async Task<WebResponseContent> InOrOutCompletedAsync(GradingMachineInputDto input)
        {
            var log = Log.ForContext("SourceContext", "分容柜完成信号");
            log.Information("[InOrOutCompleted] è¯·æ±‚参数: {Request}", JsonSerializer.Serialize(input, _jsonOptions));
            WebResponseContent content = new WebResponseContent();
            if (string.IsNullOrWhiteSpace(input.LocationCode))
            {
                return content.Error($"货位编号不能为空");
                var errResult = content.Error($"货位编号不能为空");
                log.Warning("[InOrOutCompleted] å“åº”: {Response}", JsonSerializer.Serialize(errResult, _jsonOptions));
                return errResult;
            }
            try
@@ -30,7 +44,11 @@
                int locationStatus;
                if (stockInfo == null)
                    return content.Error("WMS未找到库存信息");
                {
                    var errResult = content.Error("WMS未找到库存信息");
                    log.Warning("[InOrOutCompleted] å“åº”: {Response}", JsonSerializer.Serialize(errResult, _jsonOptions));
                    return errResult;
                }
                locationStatus = MapLocationStatus(stockInfo.StockStatus);
                int MapLocationStatus(int stockStatus) => stockStatus switch
@@ -47,11 +65,14 @@
                    IsNormalProcedure = 1,
                    LocationStatus = locationStatus
                };
                return content.OK(data: outPutDto);
                var result = content.OK(data: outPutDto);
                log.Information("[InOrOutCompleted] å“åº”: {Response}", JsonSerializer.Serialize(result, _jsonOptions));
                return result;
            }
            catch (Exception ex)
            {
                content.Error(ex.Message);
                log.Error(ex, "[InOrOutCompleted] å¼‚常");
            }
            return content;
@@ -64,10 +85,14 @@
        /// <returns></returns>
        public async Task<WebResponseContent> SendLocationStatusAsync(GradingMachineInputDto input)
        {
            var log = Log.ForContext("SourceContext", "分容柜状态更新");
            log.Information("[SendLocationStatus] è¯·æ±‚参数: {Request}", JsonSerializer.Serialize(input, _jsonOptions));
            WebResponseContent content = new WebResponseContent();
            if (string.IsNullOrWhiteSpace(input.LocationCode))
            {
                return content.Error($"货位编号不能为空");
                var errResult = content.Error($"货位编号不能为空");
                log.Warning("[SendLocationStatus] å“åº”: {Response}", JsonSerializer.Serialize(errResult, _jsonOptions));
                return errResult;
            }
            try
@@ -86,10 +111,12 @@
                {
                    content.Error("更新失败");
                }
                log.Information("[SendLocationStatus] å“åº”: {Response}", JsonSerializer.Serialize(content, _jsonOptions));
            }
            catch (Exception ex)
            {
                content.Error(ex.Message);
                log.Error(ex, "[SendLocationStatus] å¼‚常");
            }
            return content;
        }
@@ -101,17 +128,23 @@
        /// <returns></returns>
        public async Task<WebResponseContent> RequestOutboundAsync(GradingMachineInputDto input)
        {
            var log = Log.ForContext("SourceContext", "分容柜出库请求");
            log.Information("[RequestOutbound] è¯·æ±‚参数: {Request}", JsonSerializer.Serialize(input, _jsonOptions));
            WebResponseContent content = new WebResponseContent();
            if (string.IsNullOrWhiteSpace(input.LocationCode) || string.IsNullOrWhiteSpace(input.PalletCode))
            {
                return content.Error($"托盘号或者货位编号不能为空");
                var errResult = content.Error($"托盘号或者货位编号不能为空");
                log.Warning("[RequestOutbound] å“åº”: {Response}", JsonSerializer.Serialize(errResult, _jsonOptions));
                return errResult;
            }
            try
            {
                var stock = await _stockInfoService.GetStockInfoAsync(input.PalletCode, input.LocationCode);
                if (stock == null)
                {
                    return content.Error("未找到对应的托盘");
                    var errResult = content.Error("未找到对应的托盘");
                    log.Warning("[RequestOutbound] å“åº”: {Response}", JsonSerializer.Serialize(errResult, _jsonOptions));
                    return errResult;
                }
                var taskList = new Dt_Task
@@ -139,17 +172,22 @@
                    var httpResponse = _httpClientHelper.Post<WebResponseContent>("http://localhost:9292/api/Task/ReceiveTask", JsonSerializer.Serialize(wmsTaskDtos)).Data;
                    if (result && httpResponse != null)
                    {
                        return content.OK("出库请求成功");
                        var okResult = content.OK("出库请求成功");
                        log.Information("[RequestOutbound] å“åº”: {Response}", JsonSerializer.Serialize(okResult, _jsonOptions));
                        return okResult;
                    }
                    else
                    {
                        return content.Error("出库请求失败");
                        var errResult = content.Error("出库请求失败");
                        log.Warning("[RequestOutbound] å“åº”: {Response}", JsonSerializer.Serialize(errResult, _jsonOptions));
                        return errResult;
                    }
                });
            }
            catch (Exception ex)
            {
                content.Error(ex.Message);
                log.Error(ex, "[RequestOutbound] å¼‚常");
            }
            return content;
        }
@@ -161,17 +199,23 @@
        /// <returns></returns>
        public async Task<WebResponseContent> GetPalletCodeCellAsync(GradingMachineInputDto input)
        {
            var log = Log.ForContext("SourceContext", "分容柜电芯查询");
            log.Information("[GetPalletCodeCell] è¯·æ±‚参数: {Request}", JsonSerializer.Serialize(input, _jsonOptions));
            WebResponseContent content = new WebResponseContent();
            if (string.IsNullOrWhiteSpace(input.PalletCode) || string.IsNullOrWhiteSpace(input.LocationCode))
            {
                return content.Error($"托盘号或者货位编号不能为空");
                var errResult = content.Error($"托盘号或者货位编号不能为空");
                log.Warning("[GetPalletCodeCell] å“åº”: {Response}", JsonSerializer.Serialize(errResult, _jsonOptions));
                return errResult;
            }
            try
            {
                var stockInfo = await _stockInfoService.GetStockInfoAsync(input.PalletCode, input.LocationCode);
                if (stockInfo == null)
                {
                    return content.Error("未找到对应的托盘");
                    var errResult = content.Error("未找到对应的托盘");
                    log.Warning("[GetPalletCodeCell] å“åº”: {Response}", JsonSerializer.Serialize(errResult, _jsonOptions));
                    return errResult;
                }
                var outPutDtos = new
@@ -186,10 +230,13 @@
                        Channel = x.InboundOrderRowNo.ToString()
                    }).ToList()
                };
                return content.OK(data: outPutDtos);
                var result = content.OK(data: outPutDtos);
                log.Information("[GetPalletCodeCell] å“åº”: {Response}", JsonSerializer.Serialize(result, _jsonOptions));
                return result;
            }
            catch (Exception ex)
            {
                log.Error(ex, "[GetPalletCodeCell] å¼‚常");
                return content.Error(ex.Message);
            }
        }
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/WIDESEA_TaskInfoService.csproj
@@ -15,4 +15,8 @@
    <ProjectReference Include="..\WIDESEA_ITaskInfoService\WIDESEA_ITaskInfoService.csproj" />
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="Serilog" Version="4.3.1" />
  </ItemGroup>
</Project>
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Program.cs
@@ -10,6 +10,7 @@
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
using Serilog;
using Serilog.Events;
using Serilog.Formatting.Json;
using System.Reflection;
using System.Text;
@@ -20,6 +21,7 @@
using WIDESEA_Core.Extensions;
using WIDESEA_Core.Filter;
using WIDESEA_Core.Helper;
//using WIDESEA_Core.HostedService;
using WIDESEA_Core.Middlewares;
using WIDESEA_WMSServer.BackgroundServices;
@@ -56,6 +58,43 @@
            fileSizeLimitBytes: 10 * 1024 * 1024,
            shared: true
            )
         // åˆ†å®¹æŸœæŽ¥å£ç‹¬ç«‹æ—¥å¿—文件
         .WriteTo.Logger(lc => lc
            .Filter.ByIncludingOnly(e => e.Properties.TryGetValue("SourceContext", out var sc) && sc is ScalarValue sv && sv.Value?.ToString() == "分容柜完成信号")
            .WriteTo.File(
                "logs/分容柜完成信号-.log",
                outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}",
                rollingInterval: RollingInterval.Day,
                retainedFileCountLimit: 30,
                fileSizeLimitBytes: 10 * 1024 * 1024,
                shared: true))
         .WriteTo.Logger(lc => lc
            .Filter.ByIncludingOnly(e => e.Properties.TryGetValue("SourceContext", out var sc) && sc is ScalarValue sv && sv.Value?.ToString() == "分容柜状态更新")
            .WriteTo.File(
                "logs/分容柜状态更新-.log",
                outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}",
                rollingInterval: RollingInterval.Day,
                retainedFileCountLimit: 30,
                fileSizeLimitBytes: 10 * 1024 * 1024,
                shared: true))
         .WriteTo.Logger(lc => lc
            .Filter.ByIncludingOnly(e => e.Properties.TryGetValue("SourceContext", out var sc) && sc is ScalarValue sv && sv.Value?.ToString() == "分容柜出库请求")
            .WriteTo.File(
                "logs/分容柜出库请求-.log",
                outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}",
                rollingInterval: RollingInterval.Day,
                retainedFileCountLimit: 30,
                fileSizeLimitBytes: 10 * 1024 * 1024,
                shared: true))
         .WriteTo.Logger(lc => lc
            .Filter.ByIncludingOnly(e => e.Properties.TryGetValue("SourceContext", out var sc) && sc is ScalarValue sv && sv.Value?.ToString() == "分容柜电芯查询")
            .WriteTo.File(
                "logs/分容柜电芯查询-.log",
                outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}",
                rollingInterval: RollingInterval.Day,
                retainedFileCountLimit: 30,
                fileSizeLimitBytes: 10 * 1024 * 1024,
                shared: true))
         // 6. å¯é€‰ï¼šè¾“出到Seq日志服务器(结构化日志服务器)
         // éœ€è¦å®‰è£… Serilog.Sinks.Seq NuGet包,并确保Seq服务在 http://localhost:5341 è¿è¡Œ
         // å¦‚不需要Seq日志,注释掉下方代码即可
@@ -95,7 +134,6 @@
builder.Services.AddSwaggerSetup();
builder.Services.AddHttpContextSetup();
builder.Services.AddMvc(options =>
{
@@ -137,7 +175,6 @@
builder.Services.Replace(ServiceDescriptor.Transient<IControllerActivator, ServiceBasedControllerActivator>());
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
var app = builder.Build();
// 3. é…ç½®ä¸­é—´ä»¶
@@ -154,7 +191,6 @@
app.UseApiLogMiddleware();
// todo
// app.UseRecordAccessLogsMiddle();
DefaultFilesOptions defaultFilesOptions = new DefaultFilesOptions();
defaultFilesOptions.DefaultFileNames.Clear();
@@ -182,4 +218,4 @@
app.MapHub<WIDESEA_WMSServer.Hubs.StockHub>("/stockHub");
app.Run();
app.Run();