8e42d0c1b7ae36cff2e7c69999117911a4b6f300..c1aabd3aaa92b072591fc368d81ab2cc37a0aa14
2026-03-27 wanshenmean
路由变更
c1aabd 对比 | 目录
2026-03-27 wanshenmean
feat(MES): 新增陕西顷刻能源科技MES系统对接服务
e8de46 对比 | 目录
2026-03-27 wanshenmean
feat(RouterService): 新增 DeleteRouters 批量删除路由方法
baf1f1 对比 | 目录
2026-03-27 wanshenmean
feat(RouterService): 新增 ExistsRouter 两个重载
a2e2b0 对比 | 目录
2026-03-27 wanshenmean
feat(RouterService): 新增 QueryRoutersByDeviceCode 设备路由查询
b50c79 对比 | 目录
2026-03-27 wanshenmean
feat(RouterService): 新增 ClearRouterCache 清除路由缓存方法
518e2d 对比 | 目录
2026-03-27 wanshenmean
refactor(IRouterService): 去掉多余 public 修饰符并新增7个方法签名
15f1d6 对比 | 目录
2026-03-27 wanshenmean
fix(RouterService): QueryAllPositions 空 catch 块增加错误日志
59bc10 对比 | 目录
2026-03-27 wanshenmean
fix(RouterService): AddRouters 缓存更新用 try-catch 保护
71403d 对比 | 目录
2026-03-27 wanshenmean
docs: 添加 RouterService 逻辑修复与新方法设计文档
4b22bc 对比 | 目录
2026-03-27 wanshenmean
feat(RouterService): 改造 AddRouters 写入后同步更新缓存
db3273 对比 | 目录
2026-03-27 wanshenmean
feat(RouterService): 改造 GetAllWholeRouters 使用缓存
58cf80 对比 | 目录
2026-03-27 wanshenmean
chore(RouterService): 删除 QueryNextRoute 中的遗留死代码
f049a2 对比 | 目录
2026-03-27 wanshenmean
feat(RouterService): 改造 QueryNextRoute 四个重载使用缓存
1aec27 对比 | 目录
2026-03-27 wanshenmean
fix(RouterService): 修复 QueryNextRoutes 缓存列表引用污染问题
69eb90 对比 | 目录
2026-03-27 wanshenmean
feat(RouterService): 改造 QueryNextRoutes 两个重载使用缓存
171e94 对比 | 目录
2026-03-27 wanshenmean
feat(RouterService): 新增 GetAllRoutersFromCache 私有方法
0a6698 对比 | 目录
2026-03-27 wanshenmean
feat(WCS): 完善 WIDESEAWCS_Tasks 模块日志系统
bf2aa9 对比 | 目录
2026-03-27 wanshenmean
feat: 提交WCS与WMS代码调整
dcbd49 对比 | 目录
已添加9个文件
已删除6个文件
已修改29个文件
3500 ■■■■ 文件已修改
.gitignore 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/.claude/settings.local.json 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/.vs/WIDESEAWCS_Server/v18/DocumentLayout.json 270 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Core/LogHelper/QuartzLogger.cs 358 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Core/WIDESEAWCS_Core.csproj 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_QuartzJob/QuartzNet/QuartzNetExtension.cs 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_QuartzJob/QuartzNet/SchedulerCenterServer.cs 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_QuartzJob/Service/IRouterService.cs 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_QuartzJob/Service/RouterService.cs 224 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_RedisService/Connection/RedisConnectionManager.cs 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Server/Program.cs 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Server/WIDESEAWCS_Server.csproj 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ConveyorLineNewJob/CommonConveyorLineNewJob.cs 41 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ConveyorLineNewJob/ConveyorLineDispatchHandler.cs 44 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ConveyorLineNewJob/ConveyorLineTargetAddressSelector.cs 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ConveyorLineNewJob/ConveyorLineTaskFilter.cs 26 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotClientManager.cs 32 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotJob.cs 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotMessageHandler.cs 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotStateManager.cs 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotTaskProcessor.cs 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Workflow/RobotWorkflowOrchestrator.cs 39 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/StackerCraneJob/CommonStackerCraneJob.cs 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/StackerCraneJob/StackerCraneCommandBuilder.cs 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/StackerCraneJob/StackerCraneTaskSelector.cs 61 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tests/StackerCraneTaskSelectorTests.cs 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/docs/superpowers/plans/2026-03-27-router-cache.md 239 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/docs/superpowers/plans/2026-03-27-router-service-audit.md 324 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/WIDESEAWCS_Server/docs/superpowers/specs/2026-03-27-router-service-audit-design.md 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WCS/docs(superpowers)/specs/2026-03-27-task-logging-design.md 167 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/.vs/WIDESEA_WMSServer/v18/DocumentLayout.backup.json 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/.vs/WIDESEA_WMSServer/v18/DocumentLayout.json 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_BasicService/MesService.cs 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_BasicService/WIDESEA_BasicService.csproj 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_DTO/MES/MesRequestDto.cs 243 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_DTO/MES/MesResponseDto.cs 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_IBasicService/IMesService.cs 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_StockService/StockSerivce.cs 223 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService.cs 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/appsettings.json 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/测试工具/WIDESEAWCS_S7Simulator/.vs/WIDESEAWCS_S7Simulator.slnx/v18/DocumentLayout.backup.json 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/测试工具/WIDESEAWCS_S7Simulator/.vs/WIDESEAWCS_S7Simulator.slnx/v18/DocumentLayout.json 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/测试工具/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/src/views/RobotClientsView.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目资料/设备协议/上位系统对接/陕西顷刻能源科技MES系统对接接口.md 329 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.gitignore
@@ -434,3 +434,8 @@
Code/WCS/WIDESEAWCS_Server/.vs/WIDESEAWCS_Server/v18/DocumentLayout.json
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Server/appsettings.json
/Code/WMS/WIDESEA_WMSClient/.omc
# Local IDE and assistant environment files
**/.claude/settings.local.json
**/.vs/**/DocumentLayout.json
**/.vs/**/DocumentLayout.backup.json
Code/WCS/WIDESEAWCS_Server/.claude/settings.local.json
ÎļþÒÑɾ³ý
Code/WCS/WIDESEAWCS_Server/.vs/WIDESEAWCS_Server/v18/DocumentLayout.json
ÎļþÒÑɾ³ý
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Core/LogHelper/QuartzLogger.cs
@@ -1,85 +1,345 @@
using System;
using OfficeOpenXml.FormulaParsing.Excel.Functions.Numeric;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using WIDESEAWCS_Core.Helper;
namespace WIDESEAWCS_Core.LogHelper
{
    public class QuartzLogger
    /// <summary>
    /// æ—¥å¿—等级枚举
    /// </summary>
    public enum LogLevel
    {
        static ReaderWriterLockSlim LogWriteLock = new ReaderWriterLockSlim();
        static string folderPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"Log\\{DateTime.Now.ToString("yyyy-MM-dd")}");
        DEBUG = 0,
        INFO = 1,
        WARN = 2,
        ERROR = 3,
        FATAL = 4
    }
        public static void WriteLogToFile(string fileName, string log)
    /// <summary>
    /// æ—¥å¿—条目:封装单条日志的完整信息
    /// </summary>
    public class LogEntry
    {
        /// <summary>
        /// æ—¥å¿—æ—¶é—´
        /// </summary>
        public DateTime Time { get; set; }
        /// <summary>
        /// æ—¥å¿—等级
        /// </summary>
        public LogLevel Level { get; set; }
        /// <summary>
        /// æ—¥å¿—消息内容
        /// </summary>
        public string Message { get; set; } = string.Empty;
        /// <summary>
        /// æ—¥å¿—来源/文件名
        /// </summary>
        public string? Source { get; set; }
        /// <summary>
        /// å¼‚常信息(可选)
        /// </summary>
        public string? Exception { get; set; }
        /// <summary>
        /// æ ¼å¼åŒ–为标准日志字符串
        /// </summary>
        public string ToFormattedString()
        {
            var sb = new StringBuilder();
            sb.Append($"【{Time:HH:mm:ss}】【{Level}】:【{Message}】");
            if (!string.IsNullOrEmpty(Exception))
            {
                sb.AppendLine();
                sb.Append($"【异常】:{Exception}");
            }
            return sb.ToString();
        }
    }
    /// <summary>
    /// é˜Ÿåˆ—化集中日志写入器
    /// ä½¿ç”¨ç”Ÿäº§è€…-消费者模式,后台线程批量写入文件,提高性能
    /// </summary>
    public class QueuedLogWriter : IDisposable
    {
        private static readonly Lazy<QueuedLogWriter> _instance = new Lazy<QueuedLogWriter>(() => new QueuedLogWriter());
        public static QueuedLogWriter Instance => _instance.Value;
        private readonly BlockingCollection<LogEntry> _logQueue;
        private readonly CancellationTokenSource _cts;
        private readonly Task _writeTask;
        private readonly ReaderWriterLockSlim _fileLock = new ReaderWriterLockSlim();
        private readonly string _logFolder;
        private readonly int _maxFileSize;
        private readonly string _fileExt;
        private bool _disposed;
        /// <summary>
        /// æ—¥å¿—队列写入器单例
        /// </summary>
        private QueuedLogWriter()
        {
            _logFolder = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"Log{Path.DirectorySeparatorChar}{DateTime.Now:yyyy-MM-dd}");
            _maxFileSize = 10 * 1024 * 1024; // 10MB
            _fileExt = ".log";
            _logQueue = new BlockingCollection<LogEntry>(new ConcurrentQueue<LogEntry>());
            _cts = new CancellationTokenSource();
            // ç¡®ä¿æ—¥å¿—目录存在
            if (!Directory.Exists(_logFolder))
            {
                Directory.CreateDirectory(_logFolder);
            }
            // å¯åŠ¨åŽå°å†™å…¥çº¿ç¨‹
            _writeTask = Task.Run(WriteLoop, _cts.Token);
        }
        /// <summary>
        /// åŽå°å†™å…¥å¾ªçޝ
        /// </summary>
        private void WriteLoop()
        {
            var batch = new List<LogEntry>();
            while (!_cts.Token.IsCancellationRequested)
            {
                try
                {
                    // æ‰¹é‡å–出日志,最多100条或等待500ms
                    batch.Clear();
                    if (_logQueue.TryTake(out var entry, 500))
                    {
                        batch.Add(entry);
                        while (batch.Count < 100 && _logQueue.TryTake(out entry, 50))
                        {
                            batch.Add(entry);
                        }
                        WriteBatch(batch);
                    }
                }
                catch (OperationCanceledException)
                {
                    break;
                }
                catch (Exception)
                {
                    // å¿½ç•¥å†™å…¥å¼‚常,防止后台线程崩溃
                }
            }
            // å–消前将剩余日志写出
            while (_logQueue.TryTake(out var remainingEntry, 100))
            {
                batch.Add(remainingEntry);
            }
            if (batch.Count > 0)
            {
                WriteBatch(batch);
            }
        }
        /// <summary>
        /// æ‰¹é‡å†™å…¥æ—¥å¿—到文件
        /// </summary>
        private void WriteBatch(List<LogEntry> entries)
        {
            if (entries.Count == 0) return;
            try
            {
                // è¿›å…¥å†™é”
                LogWriteLock.EnterWriteLock();
                // å¦‚果文件夹不存在,则创建文件夹
                if (!Directory.Exists(folderPath))
                _fileLock.EnterWriteLock();
                foreach (var entry in entries)
                {
                    Directory.CreateDirectory(folderPath);
                    string fileName = GetLogFileName(entry.Source);
                    string filePath = Path.Combine(_logFolder, fileName);
                    string content = entry.ToFormattedString() + Environment.NewLine;
                    ConsoleHelper.WriteInfoLine(content);
                    File.AppendAllText(filePath, content);
                }
                // èŽ·å–æ—¥å¿—æ–‡ä»¶è·¯å¾„
                string logFilePath = Path.Combine(folderPath, GetLastAccessFileName(fileName));
                // èŽ·å–å½“å‰æ—¶é—´
                DateTime now = DateTime.Now;
                // æž„造日志内容
                string logContent = $"【{now}】{Environment.NewLine}{log}";
                // å°†æ—¥å¿—内容追加到日志文件中
                File.AppendAllText(logFilePath, logContent);
            }
            catch { }
            catch (Exception)
            {
                // é™é»˜å¤„理,防止文件IO异常影响主线程
            }
            finally
            {
                // é€€å‡ºå†™é”
                LogWriteLock.ExitWriteLock();
                _fileLock.ExitWriteLock();
            }
        }
        static int size = 10 * 1024 * 1024;
        static string ext = ".log";
        private static string GetLogFilePath(string folderPath, string fileName)
        {
            // èŽ·å–æŒ‡å®šæ–‡ä»¶å¤¹ä¸‹çš„æ‰€æœ‰æ–‡ä»¶
            var allFiles = new DirectoryInfo(folderPath);
            // èŽ·å–ç¬¦åˆæ¡ä»¶çš„æ–‡ä»¶ï¼ŒæŒ‰æ–‡ä»¶åé™åºæŽ’åˆ—
            var selectFiles = allFiles.GetFiles().Where(fi => fi.Name.ToLower().Contains(fileName.ToLower()) && fi.Extension.ToLower() == ext.ToLower() && fi.Length < size).OrderByDescending(d => d.Name).ToList();
            FileInfo? file = selectFiles.FirstOrDefault();
            // å¦‚果有符合条件的文件,返回第一个文件的完整路径
            if (file != null)
        /// <summary>
        /// èŽ·å–æˆ–åˆ›å»ºä¸€ä¸ªæ—¥å¿—æ–‡ä»¶è·¯å¾„ï¼ˆæŒ‰å¤§å°åˆ†æ–‡ä»¶ï¼‰
        /// </summary>
        private string GetLogFileName(string? source)
        {
            string prefix = string.IsNullOrEmpty(source) ? "WCS" : source;
            string searchPattern = $"{prefix}*.log";
            if (!Directory.Exists(_logFolder))
            {
                return file.FullName;
                Directory.CreateDirectory(_logFolder);
            }
            // å¦‚果没有符合条件的文件,返回一个新的文件路径,文件名为原文件名加上当前时间
            return Path.Combine(folderPath, $@"{fileName}_{DateTime.Now.ToString("HH-mm-ss")}.log");
        }
            var files = Directory.GetFiles(_logFolder, searchPattern)
                .Select(f => new FileInfo(f))
                .Where(f => f.Extension.Equals(_fileExt, StringComparison.OrdinalIgnoreCase))
                .OrderByDescending(f => f.Name)
                .ToList();
        private static string GetLastAccessFileName(string fileName)
        {
            foreach (var m in GetExistLogFileNames(fileName))
            // æŸ¥æ‰¾æœ‰ç©ºé—´çš„现有文件
            foreach (var file in files)
            {
                FileInfo fileInfo = new FileInfo(m);
                if (fileInfo.Length < size)
                if (file.Length < _maxFileSize)
                {
                    return m;
                    return file.Name;
                }
            }
            // è¿”回一个新的默认当前时间的日志名称
            return $@"{fileName}_{DateTime.Now.ToString("HH-mm-ss")}.log";
            // åˆ›å»ºæ–°æ–‡ä»¶
            return $"{prefix}_{DateTime.Now:HH-mm-ss}{_fileExt}";
        }
        public static string[] GetExistLogFileNames(string fileName)
        /// <summary>
        /// å†™å…¥æ—¥å¿—(生产端)
        /// </summary>
        public void Enqueue(LogEntry entry)
        {
            string[] fileNames = Directory.GetFiles(folderPath, fileName + "*.log");
            return fileNames;
            if (_disposed) return;
            _logQueue.Add(entry);
        }
        /// <summary>
        /// å†™å…¥æ—¥å¿—的便捷方法
        /// </summary>
        public void Log(LogLevel level, string message, string? source = null, string? exception = null)
        {
            if (_disposed) return;
            var entry = new LogEntry
            {
                Time = DateTime.Now,
                Level = level,
                Message = message,
                Source = source,
                Exception = exception
            };
            _logQueue.Add(entry);
        }
        /// <summary>
        /// å…³é—­æ—¥å¿—写入器
        /// </summary>
        public void Dispose()
        {
            if (_disposed) return;
            _disposed = true;
            _cts.Cancel();
            try
            {
                _writeTask.Wait(3000);
            }
            catch (AggregateException) { }
            _cts.Dispose();
            _logQueue.Dispose();
            _fileLock.Dispose();
        }
    }
    /// <summary>
    /// é™æ€æ—¥å¿—工具类(兼容原有接口)
    /// æä¾›å¤šç§æ ¼å¼åŒ–方法和队列化写入
    /// </summary>
    public class QuartzLogger
    {
        private static readonly QueuedLogWriter _writer = QueuedLogWriter.Instance;
        /// <summary>
        /// å…¼å®¹æ—§æŽ¥å£ï¼šå°†æ—¥å¿—写入文件
        /// </summary>
        /// <param name="fileName">日志文件名(作为Source)</param>
        /// <param name="log">日志内容</param>
        public static void WriteLogToFile(string fileName, string log)
        {
            _writer.Log(LogLevel.INFO, log, fileName);
        }
        /// <summary>
        /// å†™å…¥è°ƒè¯•日志
        /// </summary>
        public static void Debug(string message, string? source = null)
        {
            _writer.Log(LogLevel.DEBUG, message, source);
        }
        /// <summary>
        /// å†™å…¥ä¿¡æ¯æ—¥å¿—
        /// </summary>
        public static void Info(string message, string? source = null)
        {
            _writer.Log(LogLevel.INFO, message, source);
        }
        /// <summary>
        /// å†™å…¥è­¦å‘Šæ—¥å¿—
        /// </summary>
        public static void Warn(string message, string? source = null)
        {
            _writer.Log(LogLevel.WARN, message, source);
        }
        /// <summary>
        /// å†™å…¥é”™è¯¯æ—¥å¿—
        /// </summary>
        public static void Error(string message, string? source = null, Exception? exception = null)
        {
            _writer.Log(LogLevel.ERROR, message, source, exception?.ToString());
        }
        /// <summary>
        /// å†™å…¥è‡´å‘½é”™è¯¯æ—¥å¿—
        /// </summary>
        public static void Fatal(string message, string? source = null, Exception? exception = null)
        {
            _writer.Log(LogLevel.FATAL, message, source, exception?.ToString());
        }
        /// <summary>
        /// ä½¿ç”¨æŒ‡å®šæ ¼å¼å†™å…¥æ—¥å¿—
        /// </summary>
        /// <param name="format">格式化字符串</param>
        /// <param name="args">格式化参数</param>
        public static void WriteFormatted(string format, params object[] args)
        {
            string message = string.Format(format, args);
            _writer.Log(LogLevel.INFO, message);
        }
        /// <summary>
        /// ç›´æŽ¥å†™å…¥ä¸€ä¸ªæ—¥å¿—条目(最灵活)
        /// </summary>
        public static void WriteLogEntry(LogEntry entry)
        {
            _writer.Enqueue(entry);
        }
        /// <summary>
        /// é‡Šæ”¾èµ„源(应用关闭时调用)
        /// </summary>
        public static void Shutdown()
        {
            _writer.Dispose();
        }
    }
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Core/WIDESEAWCS_Core.csproj
@@ -62,6 +62,9 @@
        <PackageReference Include="MiniProfiler.AspNetCore.Mvc" Version="4.3.8" />
        <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
        <PackageReference Include="OfficeOpenXml.Core.ExcelPackage" Version="1.0.0" />
        <PackageReference Include="Serilog" Version="4.3.1" />
        <PackageReference Include="Serilog.AspNetCore" Version="6.0.0" />
        <PackageReference Include="Serilog.Sinks.Seq" Version="9.0.0" />
        <PackageReference Include="SkiaSharp" Version="2.88.8" />
        <PackageReference Include="SqlSugarCore" Version="5.1.4.152" />
        <PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_QuartzJob/QuartzNet/QuartzNetExtension.cs
@@ -3,6 +3,7 @@
using WIDESEAWCS_Core;
using WIDESEAWCS_Core.Caches;
using WIDESEAWCS_Core.Helper;
using WIDESEAWCS_Core.LogHelper;
using WIDESEAWCS_QuartzJob.DTO;
using WIDESEAWCS_QuartzJob.Service;
@@ -118,7 +119,7 @@
                        }
                        catch (Exception ex)
                        {
                            Console.WriteLine("调度服务开启异常" + ex.ToString());
                            QuartzLogger.Error($"调度服务开启异常", "QuartzNetExtension", ex);
                        }
                    }
                    else
@@ -141,13 +142,22 @@
                        _ => targetDevice.Device
                    };
                    WebResponseContent responseContent = await _schedulerCenter.AddScheduleJobAsync(dispatches[i]);
                    if (responseContent.Status) ConsoleHelper.WriteSuccessLine(dispatches[i].Name + "调度服务添加成功"); else ConsoleHelper.WriteErrorLine(dispatches[i].Name + "调度服务添加失败");
                    if (responseContent.Status)
                    {
                        QuartzLogger.Info($"{dispatches[i].Name}调度服务添加成功", "QuartzNetExtension");
                        ConsoleHelper.WriteSuccessLine(dispatches[i].Name + "调度服务添加成功");
                    }
                    else
                    {
                        QuartzLogger.Error($"{dispatches[i].Name}调度服务添加失败", "QuartzNetExtension");
                        ConsoleHelper.WriteErrorLine(dispatches[i].Name + "调度服务添加失败");
                    }
                }
                //await _schedulerCenter.StartScheduleAsync();
            }
            catch (Exception ex)
            {
                Console.WriteLine("调度服务开启异常" + ex.ToString());
                QuartzLogger.Error($"调度服务开启异常", "QuartzNetExtension", ex);
                throw;
            }
        }
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_QuartzJob/QuartzNet/SchedulerCenterServer.cs
@@ -31,6 +31,7 @@
using WIDESEAWCS_QuartzJob.DTO;
using WIDESEAWCS_QuartzJob.CustomException;
using Quartz.Impl.Matchers;
using WIDESEAWCS_Core.LogHelper;
namespace WIDESEAWCS_QuartzJob
{
@@ -103,7 +104,7 @@
                {
                    //等待任务运行完成
                    await this._scheduler.Start();
                    await Console.Out.WriteLineAsync(QuartzJobInfoMessage.StartJobSuccess);
                    QuartzLogger.Info(QuartzJobInfoMessage.StartJobSuccess);
                    result = WebResponseContent.Instance.OK(QuartzJobInfoMessage.StartJobSuccess);
                    return result;
                }
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_QuartzJob/Service/IRouterService.cs
@@ -37,7 +37,7 @@
        /// </summary>
        /// <param name="startPosi">起点/当前位置</param>
        /// <returns>返回下一个路由节点,如果没有则返回null</returns>
        public Dt_Router QueryNextRoute(string startPosi);
        Dt_Router QueryNextRoute(string startPosi);
        /// <summary>
        /// æ ¹æ®èµ·ç‚¹/当前位置和路由类型获取下一个单个子节点路由
@@ -45,7 +45,7 @@
        /// <param name="startPosi">起点/当前位置</param>
        /// <param name="routeType">路由类型</param>
        /// <returns>返回下一个路由节点,如果没有则返回null</returns>
        public Dt_Router QueryNextRoute(string startPosi, int routeType);
        Dt_Router QueryNextRoute(string startPosi, int routeType);
        /// <summary>
        /// æ ¹æ®èµ·ç‚¹/当前位置、终点方向和路由类型获取下一个单个子节点路由(智能选择朝向终点的路由)
@@ -54,7 +54,7 @@
        /// <param name="endPosi">终点位置(用于方向判断)</param>
        /// <param name="routeType">路由类型</param>
        /// <returns>返回下一个路由节点,优先返回朝向终点的路由,如果没有则返回null</returns>
        public Dt_Router QueryNextRoute(string startPosi, string endPosi, int routeType);
        Dt_Router QueryNextRoute(string startPosi, string endPosi, int routeType);
        /// <summary>
        /// æ ¹æ®èµ·ç‚¹/当前位置、终点方向和路由类型获取下一个单个子节点路由(智能选择朝向终点的路由)
@@ -62,7 +62,7 @@
        /// <param name="startPosi">起点/当前位置</param>
        /// <param name="endPosi">终点位置(用于方向判断)</param>
        /// <returns>返回下一个路由节点,优先返回朝向终点的路由,如果没有则返回null</returns>
        public Dt_Router QueryNextRoute(string startPosi, string endPosi);
        Dt_Router QueryNextRoute(string startPosi, string endPosi);
        /// <summary>
        /// èŽ·å–ä»Žèµ·ç‚¹åˆ°ç»ˆç‚¹çš„å®Œæ•´è·¯å¾„ï¼ˆæŒ‰é¡ºåºè¿”å›žæ¯ä¸ªå­èŠ‚ç‚¹è·¯ç”±ï¼‰
@@ -71,7 +71,7 @@
        /// <param name="endPosi">终点位置</param>
        /// <param name="routeType">路由类型</param>
        /// <returns>返回有序的路由列表,如果找不到路径则返回空列表</returns>
        public List<Dt_Router> QueryRoutePath(string startPosi, string endPosi, int routeType);
        List<Dt_Router> QueryRoutePath(string startPosi, string endPosi, int routeType);
        /// <summary>
        /// æ ¹æ®è®¾å¤‡ç¼–号获取对应的路由点位编号(输送线站台编号)信息
@@ -93,5 +93,54 @@
        /// <param name="routerType">路由类型</param>
        /// <returns></returns>
        WebResponseContent AddRouters(List<RoutersAddDTO> routersAddDTOs, int routerType);
        /// <summary>
        /// æ¸…除路由缓存
        /// </summary>
        void ClearRouterCache();
        /// <summary>
        /// æ ¹æ®è®¾å¤‡ç¼–号查询经过该设备的所有路由
        /// </summary>
        /// <param name="deviceCode">设备编号</param>
        /// <returns>返回经过该设备的所有路由列表</returns>
        List<Dt_Router> QueryRoutersByDeviceCode(string deviceCode);
        /// <summary>
        /// åˆ¤æ–­ä¸¤ç‚¹ä¹‹é—´æ˜¯å¦å­˜åœ¨è·¯ç”±ï¼ˆå…¨ç±»åž‹ï¼‰
        /// </summary>
        /// <param name="startPosi">起点位置</param>
        /// <param name="endPosi">终点位置</param>
        /// <returns>存在返回true,不存在返回false</returns>
        bool ExistsRouter(string startPosi, string endPosi);
        /// <summary>
        /// åˆ¤æ–­ä¸¤ç‚¹ä¹‹é—´æ˜¯å¦å­˜åœ¨æŒ‡å®šç±»åž‹çš„路由
        /// </summary>
        /// <param name="startPosi">起点位置</param>
        /// <param name="endPosi">终点位置</param>
        /// <param name="routeType">路由类型</param>
        /// <returns>存在返回true,不存在返回false</returns>
        bool ExistsRouter(string startPosi, string endPosi, int routeType);
        /// <summary>
        /// èŽ·å–å…¨é‡è·¯ç”±æ•°é‡ï¼ˆå…¥å£+出口合计)
        /// </summary>
        /// <returns>返回全量路由数量</returns>
        int GetRouterCount();
        /// <summary>
        /// èŽ·å–æŒ‡å®šç±»åž‹è·¯ç”±æ•°é‡
        /// </summary>
        /// <param name="routeType">路由类型</param>
        /// <returns>返回指定类型的路由数量</returns>
        int GetRouterCount(int routeType);
        /// <summary>
        /// æ‰¹é‡åˆ é™¤è·¯ç”±
        /// </summary>
        /// <param name="routerIds">要删除的路由ID列表</param>
        /// <returns>返回删除操作的结果</returns>
        WebResponseContent DeleteRouters(List<long> routerIds);
    }
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_QuartzJob/Service/RouterService.cs
@@ -1,11 +1,5 @@
using SqlSugar;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WIDESEAWCS_Common;
using WIDESEAWCS_Common.TaskEnum;
using WIDESEAWCS_Core;
using WIDESEAWCS_Core.BaseServices;
using WIDESEAWCS_Core.Enums;
@@ -40,6 +34,137 @@
        }
        /// <summary>
        /// ä»Žç¼“存获取指定类型的全量路由数据,缓存不存在时自动从数据库加载并写入缓存
        /// </summary>
        /// <param name="routeType">路由类型(入口/出口)</param>
        /// <returns>该类型的全部路由列表</returns>
        private List<Dt_Router> GetAllRoutersFromCache(int routeType)
        {
            // æ ¹æ®è·¯ç”±ç±»åž‹æž„建缓存Key,In类型对应"In",Out类型对应"Out"
            string cacheKey = $"Router:AllRouters:{(routeType == RouterInOutType.In.ObjToInt() ? "In" : "Out")}";
            // é€šè¿‡ç¼“存服务获取数据,缓存未命中时调用工厂方法从数据库查询并写入缓存
            return _cacheService.GetOrAdd(
                cacheKey,
                _ => BaseDal.QueryData(x => x.InOutType == routeType)
            );
        }
        /// <summary>
        /// æ¸…除所有路由缓存(入口和出口类型)
        /// </summary>
        public void ClearRouterCache()
        {
            _cacheService.Remove("Router:AllRouters:In");
            _cacheService.Remove("Router:AllRouters:Out");
        }
        /// <summary>
        /// æ ¹æ®è®¾å¤‡ç¼–号查询经过该设备的所有路由(合并入口+出口类型)
        /// </summary>
        /// <param name="deviceCode">设备编号</param>
        /// <returns>经过该设备的路由列表</returns>
        public List<Dt_Router> QueryRoutersByDeviceCode(string deviceCode)
        {
            // ä»Žç¼“存获取入口类型和出口类型的全量路由数据
            List<Dt_Router> inRouters = GetAllRoutersFromCache(RouterInOutType.In.ObjToInt());
            List<Dt_Router> outRouters = GetAllRoutersFromCache(RouterInOutType.Out.ObjToInt());
            // åˆå¹¶åŽç­›é€‰å‡ºç»è¿‡æŒ‡å®šè®¾å¤‡çš„路由(ChildPosiDeviceCode匹配)
            return inRouters.Concat(outRouters)
                .Where(x => x.ChildPosiDeviceCode == deviceCode)
                .ToList();
        }
        /// <summary>
        /// åˆ¤æ–­ä¸¤ç‚¹ä¹‹é—´æ˜¯å¦å­˜åœ¨è·¯ç”±ï¼ˆå…¨ç±»åž‹ï¼‰
        /// </summary>
        public bool ExistsRouter(string startPosi, string endPosi)
        {
            // ä»Žç¼“存获取入口类型和出口类型的全量路由数据并合并
            List<Dt_Router> inRouters = GetAllRoutersFromCache(RouterInOutType.In.ObjToInt());
            List<Dt_Router> outRouters = GetAllRoutersFromCache(RouterInOutType.Out.ObjToInt());
            var allRouters = inRouters.Concat(outRouters).ToList();
            // åœ¨å†…存中查找从起点到终点的路由
            var routes = FindRoutesInMemory(startPosi, endPosi, allRouters, null);
            return routes.Count > 0;
        }
        /// <summary>
        /// åˆ¤æ–­ä¸¤ç‚¹ä¹‹é—´æ˜¯å¦å­˜åœ¨æŒ‡å®šç±»åž‹çš„路由
        /// </summary>
        public bool ExistsRouter(string startPosi, string endPosi, int routeType)
        {
            // ä»Žç¼“存获取指定类型的全量路由数据
            List<Dt_Router> allRouters = GetAllRoutersFromCache(routeType);
            // åœ¨å†…存中查找从起点到终点的路由
            var routes = FindRoutesInMemory(startPosi, endPosi, allRouters, routeType);
            return routes.Count > 0;
        }
        /// <summary>
        /// èŽ·å–å…¨é‡è·¯ç”±æ•°é‡ï¼ˆå…¥å£+出口合计)
        /// </summary>
        public int GetRouterCount()
        {
            // åˆ†åˆ«èŽ·å–å…¥å£ç±»åž‹å’Œå‡ºå£ç±»åž‹çš„è·¯ç”±æ•°é‡å¹¶ç›¸åŠ 
            int inCount = GetAllRoutersFromCache(RouterInOutType.In.ObjToInt()).Count;
            int outCount = GetAllRoutersFromCache(RouterInOutType.Out.ObjToInt()).Count;
            return inCount + outCount;
        }
        /// <summary>
        /// èŽ·å–æŒ‡å®šç±»åž‹è·¯ç”±æ•°é‡
        /// </summary>
        public int GetRouterCount(int routeType)
        {
            // èŽ·å–æŒ‡å®šç±»åž‹çš„å…¨é‡è·¯ç”±æ•°æ®å¹¶è¿”å›žæ•°é‡
            return GetAllRoutersFromCache(routeType).Count;
        }
        /// <summary>
        /// æ‰¹é‡åˆ é™¤æŒ‡å®šID的路由,删除后同步清除对应类型的缓存
        /// </summary>
        /// <param name="routerIds">待删除的路由ID列表</param>
        /// <returns>返回处理结果</returns>
        public WebResponseContent DeleteRouters(List<long> routerIds)
        {
            WebResponseContent content = new WebResponseContent();
            try
            {
                if (routerIds == null || routerIds.Count == 0)
                {
                    return content = WebResponseContent.Instance.Error("待删除的路由ID列表不能为空");
                }
                // æŸ¥è¯¢å¾…删除路由的类型(用于后续清除缓存)
                var routersToDelete = BaseDal.QueryData(x => routerIds.Contains(x.Id));
                if (routersToDelete.Count == 0)
                {
                    return content = WebResponseContent.Instance.Error("未找到待删除的路由");
                }
                // è®°å½•涉及的类型(去重)
                var affectedTypes = routersToDelete.Select(x => x.InOutType).Distinct().ToList();
                // æ‰§è¡Œæ‰¹é‡åˆ é™¤
                BaseDal.DeleteData(routersToDelete);
                // æ¸…除受影响类型的缓存
                foreach (var routeType in affectedTypes)
                {
                    string cacheKey = $"Router:AllRouters:{(routeType == RouterInOutType.In.ObjToInt() ? "In" : "Out")}";
                    _cacheService.Remove(cacheKey);
                }
                content = WebResponseContent.Instance.OK();
            }
            catch (Exception ex)
            {
                content = WebResponseContent.Instance.Error(ex.Message);
            }
            return content;
        }
        /// <summary>
        /// æ ¹æ®èµ·ç‚¹/当前位置、终点获取下一个子节点。
        /// </summary>
        /// <param name="startPosi">起点/当前位置。</param>
@@ -50,8 +175,10 @@
            List<Dt_Router> routers = new List<Dt_Router>();
            try
            {
                // ä¸€æ¬¡æ€§æŸ¥è¯¢æ‰€æœ‰è·¯ç”±æ•°æ®åˆ°å†…å­˜
                List<Dt_Router> allRouters = BaseDal.QueryData(x => true);
                // ä»Žç¼“存加载入口类型和出口类型的路由数据并合并(创建新列表,避免修改缓存引用)
                List<Dt_Router> allRouters = GetAllRoutersFromCache(RouterInOutType.In.ObjToInt())
                    .Concat(GetAllRoutersFromCache(RouterInOutType.Out.ObjToInt()))
                    .ToList();
                // åœ¨å†…存中进行路径搜索
                routers = FindRoutesInMemory(startPosi, endPosi, allRouters, null);
@@ -80,8 +207,8 @@
            List<Dt_Router> routers = new List<Dt_Router>();
            try
            {
                // ä¸€æ¬¡æ€§æŸ¥è¯¢æŒ‡å®šç±»åž‹çš„æ‰€æœ‰è·¯ç”±æ•°æ®åˆ°å†…å­˜
                List<Dt_Router> allRouters = BaseDal.QueryData(x => x.InOutType == routeType);
                // ä»Žç¼“存加载指定类型的所有路由数据
                List<Dt_Router> allRouters = GetAllRoutersFromCache(routeType);
                // åœ¨å†…存中进行路径搜索
                routers = FindRoutesInMemory(startPosi, endPosi, allRouters, routeType);
@@ -190,9 +317,11 @@
        {
            try
            {
                // æŸ¥è¯¢ä»Žèµ·ç‚¹å‡ºå‘的所有路由
                List<Dt_Router> routes = BaseDal.QueryData(x => x.StartPosi == startPosi,
                    new Dictionary<string, OrderByType> { { nameof(Dt_Router.IsEnd), OrderByType.Desc } });
                // ä»Žç¼“存获取入口类型的所有路由数据,基于起点筛选后按IsEnd降序排序
                List<Dt_Router> routes = GetAllRoutersFromCache(RouterInOutType.In.ObjToInt())
                    .Where(x => x.StartPosi == startPosi)
                    .ToList();
                routes = routes.OrderByDescending(x => x.IsEnd).ToList();
                // è¿”回第一个路由
                return routes.FirstOrDefault();
@@ -214,9 +343,11 @@
        {
            try
            {
                // æŸ¥è¯¢ä»Žèµ·ç‚¹å‡ºå‘的指定类型路由
                List<Dt_Router> routes = BaseDal.QueryData(x => x.StartPosi == startPosi && x.InOutType == routeType,
                    new Dictionary<string, OrderByType> { { nameof(Dt_Router.IsEnd), OrderByType.Desc } });
                // ä»Žç¼“存获取指定类型的所有路由数据,筛选起点后按IsEnd降序排列
                List<Dt_Router> routes = GetAllRoutersFromCache(routeType)
                    .Where(x => x.StartPosi == startPosi)
                    .ToList();
                routes = routes.OrderByDescending(x => x.IsEnd).ToList();
                // è¿”回第一个路由
                return routes.FirstOrDefault();
@@ -239,9 +370,11 @@
        {
            try
            {
                // æŸ¥è¯¢ä»Žèµ·ç‚¹å‡ºå‘的指定类型路由
                List<Dt_Router> routes = BaseDal.QueryData(x => x.StartPosi == startPosi && x.InOutType == routeType,
                    new Dictionary<string, OrderByType> { { nameof(Dt_Router.IsEnd), OrderByType.Desc } });
                // ä»Žç¼“存获取指定类型的所有路由数据,筛选起点后按IsEnd降序排列
                List<Dt_Router> routes = GetAllRoutersFromCache(routeType)
                    .Where(x => x.StartPosi == startPosi)
                    .ToList();
                routes = routes.OrderByDescending(x => x.IsEnd).ToList();
                if (routes.Count == 0)
                    return null;
@@ -251,8 +384,8 @@
                if (directRoute != null)
                    return directRoute;
                // å¦‚果没有直接路由,使用查找算法找到朝向终点的路由
                List<Dt_Router> allRouters = BaseDal.QueryData(x => x.InOutType == routeType);
                // å¦‚果没有直接路由,使用缓存中的全量路由数据查找朝向终点的路由
                List<Dt_Router> allRouters = GetAllRoutersFromCache(routeType);
                foreach (var route in routes)
                {
                    // æ£€æŸ¥ä»Žè¿™ä¸ªè·¯ç”±çš„下一个位置是否能到达终点
@@ -281,9 +414,13 @@
        {
            try
            {
                // æŸ¥è¯¢ä»Žèµ·ç‚¹å‡ºå‘的指定类型路由
                List<Dt_Router> routes = BaseDal.QueryData(x => x.StartPosi == startPosi,
                    new Dictionary<string, OrderByType> { { nameof(Dt_Router.IsEnd), OrderByType.Desc } });
                // ä»Žç¼“存获取入口和出口类型的所有路由数据,筛选起点后按IsEnd降序排序
                List<Dt_Router> inRoutes = GetAllRoutersFromCache(RouterInOutType.In.ObjToInt());
                List<Dt_Router> outRoutes = GetAllRoutersFromCache(RouterInOutType.Out.ObjToInt());
                List<Dt_Router> routes = inRoutes.Concat(outRoutes)
                    .Where(x => x.StartPosi == startPosi)
                    .ToList();
                routes = routes.OrderByDescending(x => x.IsEnd).ToList();
                if (routes.Count == 0)
                    return null;
@@ -292,16 +429,6 @@
                Dt_Router directRoute = routes.FirstOrDefault(x => x.NextPosi == endPosi || x.ChildPosi == endPosi);
                if (directRoute != null)
                    return directRoute;
                //// å¦‚果没有直接路由,使用查找算法找到朝向终点的路由
                //List<Dt_Router> allRouters = BaseDal.QueryData(x => x.InOutType == routeType);
                //foreach (var route in routes)
                //{
                //    // æ£€æŸ¥ä»Žè¿™ä¸ªè·¯ç”±çš„下一个位置是否能到达终点
                //    var pathToEnd = FindRoutesInMemory(route.NextPosi, endPosi, allRouters, routeType);
                //    if (pathToEnd.Count > 0)
                //        return route;
                //}
                // å¦‚果都不能到达终点,返回第一个路由
                return routes.FirstOrDefault();
@@ -391,20 +518,21 @@
                    // è¿”回去重后的位置列表
                    return positions.GroupBy(x => x).Select(x => x.Key).ToList();
                }
                catch
                catch (Exception ex)
                {
                    ConsoleHelper.WriteErrorLine($"RouterService.QueryAllPositions æŸ¥è¯¢å¤±è´¥: {ex.Message}");
                }
                finally
                {
                    _cacheService.TryAdd($"{RedisPrefix.System}:{RedisName.DevicePositions}:{deviceCode}", positions);
                }
            }
            else
            else
                positions = device;
            // è¿”回位置列表
            return positions;
        }
        /// <summary>
        /// èŽ·å–è·¯ç”±è¡¨ä¸­æ‰€æœ‰å®Œæ•´çš„è·¯ç”±ä¿¡æ¯(前端调用展示数据)。
        /// </summary>
@@ -412,8 +540,10 @@
        public List<object> GetAllWholeRouters()
        {
            List<object> data = new List<object>();
            // æŸ¥è¯¢æ‰€æœ‰è·¯ç”±
            List<Dt_Router> allRouters = BaseDal.QueryData(x => true);
            // ä»Žç¼“存加载入口类型和出口类型的全量路由数据并合并
            List<Dt_Router> inRouters = GetAllRoutersFromCache(RouterInOutType.In.ObjToInt());
            List<Dt_Router> outRouters = GetAllRoutersFromCache(RouterInOutType.Out.ObjToInt());
            List<Dt_Router> allRouters = inRouters.Concat(outRouters).ToList();
            // æŸ¥è¯¢æ‰€æœ‰ç»“束的路由,并按Id排序
            List<Dt_Router> dt_Routers = allRouters.Where(x => x.IsEnd).OrderBy(x => x.Id).ToList();
@@ -584,6 +714,20 @@
                // æ·»åŠ æ–°çš„è·¯ç”±ä¿¡æ¯
                BaseDal.AddData(routers);
                // é‡æ–°æŸ¥è¯¢å…¨é‡è·¯ç”±ï¼ˆæ­¤æ—¶æ‰åŒ…含新增的路由),再写入缓存
                List<Dt_Router> updatedRouters = BaseDal.QueryData(x => x.InOutType == routerType);
                string cacheKey = $"Router:AllRouters:{(routerType == RouterInOutType.In.ObjToInt() ? "In" : "Out")}";
                try
                {
                    _cacheService.AddOrUpdate(cacheKey, updatedRouters);
                }
                catch
                {
                    // ç¼“存更新失败时静默忽略,下次查询会从DB自动重建缓存
                }
                content = WebResponseContent.Instance.OK();
            }
            catch (Exception ex)
@@ -593,4 +737,4 @@
            return content;
        }
    }
}
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_RedisService/Connection/RedisConnectionManager.cs
@@ -3,6 +3,7 @@
using StackExchange.Redis;
using System.Linq;
using WIDESEAWCS_Core.Helper;
using WIDESEAWCS_Core.LogHelper;
using WIDESEAWCS_RedisService.Options;
namespace WIDESEAWCS_RedisService.Connection
@@ -59,16 +60,16 @@
                var connection = ConnectionMultiplexer.Connect(configOptions);
                connection.ConnectionFailed += (_, e) =>
                    ConsoleHelper.WriteErrorLine($"Redis连接失败: {e.FailureType}");
                    QuartzLogger.Info($"Redis连接失败: {e.FailureType}");
                connection.ConnectionRestored += (_, e) =>
                    ConsoleHelper.WriteSuccessLine($"Redis连接恢复: {e.EndPoint}");
                    QuartzLogger.Info($"Redis连接恢复: {e.EndPoint}");
                ConsoleHelper.WriteSuccessLine($"Redis连接成功: {string.Join(",", configOptions.EndPoints)}");
                QuartzLogger.Info($"Redis连接成功: {string.Join(",", configOptions.EndPoints)}");
                return connection;
            }
            catch (Exception ex)
            {
                ConsoleHelper.WriteErrorLine($"Redis连接创建失败:{ex.Message}");
                QuartzLogger.Error($"Redis连接创建失败:{ex.Message}",null, ex);
                throw;
            }
        }
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Server/Program.cs
@@ -46,7 +46,7 @@
        .ReadFrom.Configuration(context.Configuration) // ä»Žåº”用程序配置中读取Serilog相关设置(如appsettings.json)
        .ReadFrom.Services(services)  // ä»Žä¾èµ–注入容器中读取服务配置,允许在配置中使用已注册的服务
        .Enrich.FromLogContext()  // å¯ç”¨æ—¥å¿—上下文,可以在日志中包含如请求ID、用户ID等动态属性
        .Enrich.WithProperty("Application", "WCS")
        // è®¾ç½®Microsoft命名空间的日志级别为Information
        // è¿™æ ·å¯ä»¥å‡å°‘Microsoft框架本身的详细日志,避免过多的Debug日志
        .MinimumLevel.Override("Microsoft", LogEventLevel.Information)
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Server/WIDESEAWCS_Server.csproj
@@ -65,11 +65,6 @@
    </ItemGroup>
    <ItemGroup>
        <PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
        <PackageReference Include="Serilog.Settings.Configuration" Version="8.0.4" />
        <PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
        <PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
        <PackageReference Include="Serilog.Sinks.Seq" Version="9.0.0" />
        <PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
    </ItemGroup>
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ConveyorLineNewJob/CommonConveyorLineNewJob.cs
@@ -1,5 +1,6 @@
using MapsterMapper;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Quartz;
using SqlSugar;
using System.Text.Json;
@@ -7,6 +8,7 @@
using WIDESEAWCS_Common.TaskEnum;
using WIDESEAWCS_Core;
using WIDESEAWCS_Core.Helper;
using WIDESEAWCS_Core.LogHelper;
using WIDESEAWCS_DTO.TaskInfo;
using WIDESEAWCS_ITaskInfoService;
using WIDESEAWCS_Model.Models;
@@ -70,6 +72,11 @@
        private readonly HttpClientHelper _httpClientHelper;
        /// <summary>
        /// æ—¥å¿—记录器
        /// </summary>
        private readonly ILogger<CommonConveyorLineNewJob> _logger;
        /// <summary>
        /// æž„造函数
        /// </summary>
        /// <param name="taskService">任务服务</param>
@@ -77,16 +84,18 @@
        /// <param name="routerService">路由服务</param>
        /// <param name="mapper">对象映射器</param>
        /// <param name="httpClientHelper">HTTP å®¢æˆ·ç«¯å¸®åŠ©ç±»</param>
        public CommonConveyorLineNewJob(ITaskService taskService, ITaskExecuteDetailService taskExecuteDetailService, IRouterService routerService, IMapper mapper, HttpClientHelper httpClientHelper)
        /// <param name="logger">日志记录器</param>
        public CommonConveyorLineNewJob(ITaskService taskService, ITaskExecuteDetailService taskExecuteDetailService, IRouterService routerService, IMapper mapper, HttpClientHelper httpClientHelper, ILogger<CommonConveyorLineNewJob> logger)
        {
            _taskService = taskService;
            _taskExecuteDetailService = taskExecuteDetailService;
            _routerService = routerService;
            _mapper = mapper;
            _httpClientHelper = httpClientHelper;
            _logger = logger;
            // åˆå§‹åŒ–调度处理器
            _conveyorLineDispatch = new ConveyorLineDispatchHandler(_taskService, _taskExecuteDetailService, _routerService, _mapper);
            _conveyorLineDispatch = new ConveyorLineDispatchHandler(_taskService, _taskExecuteDetailService, _routerService, _mapper, _logger);
        }
        /// <summary>
@@ -116,7 +125,8 @@
                    if (childDeviceCodes == null || childDeviceCodes.Count == 0)
                    {
                        // æ²¡æœ‰å­è®¾å¤‡ï¼Œç›´æŽ¥è¿”回
                        Console.WriteLine($"输送线 {conveyorLine.DeviceCode} æ²¡æœ‰å­è®¾å¤‡");
                        _logger.LogInformation("输送线 {DeviceCode} æ²¡æœ‰å­è®¾å¤‡", conveyorLine.DeviceCode);
                        QuartzLogger.Info($"输送线 {conveyorLine.DeviceCode} æ²¡æœ‰å­è®¾å¤‡", conveyorLine.DeviceCode);
                        return Task.CompletedTask;
                    }
@@ -126,6 +136,9 @@
                        // é™åˆ¶å¹¶å‘数:子设备数量和 CPU æ ¸å¿ƒæ•°*2 çš„较小值
                        MaxDegreeOfParallelism = Math.Min(childDeviceCodes.Count, Environment.ProcessorCount * 2),
                    };
                    _logger.LogDebug("Execute:开始并行处理输送线 {DeviceCode},子设备数量: {Count}", conveyorLine.DeviceCode, childDeviceCodes.Count);
                    QuartzLogger.Debug($"开始并行处理输送线,子设备数量: {childDeviceCodes.Count}", conveyorLine.DeviceCode);
                    // å¹¶è¡Œå¤„理每个子设备
                    Parallel.For(0, childDeviceCodes.Count, parallelOptions, i =>
@@ -140,6 +153,8 @@
                            // å¦‚果命令为空,跳过
                            if (command == null)
                            {
                                _logger.LogDebug("Execute:子设备 {ChildDeviceCode} å‘½ä»¤ä¸ºç©ºï¼Œè·³è¿‡", childDeviceCode);
                                QuartzLogger.Debug($"子设备 {childDeviceCode} å‘½ä»¤ä¸ºç©ºï¼Œè·³è¿‡", conveyorLine.DeviceCode);
                                return;
                            }
@@ -164,6 +179,9 @@
                                    {
                                        // æ²¡æœ‰ä»»åŠ¡ï¼Œå‘ WMS è¯·æ±‚出库托盘任务
                                        var position = checkPalletPositions.FirstOrDefault(x => x.Code == childDeviceCode);
                                        _logger.LogInformation("Execute:检查托盘位置 {ChildDeviceCode},请求WMS出库托盘任务", childDeviceCode);
                                        QuartzLogger.Info($"检查托盘位置 {childDeviceCode},请求WMS出库托盘任务", conveyorLine.DeviceCode);
                                        var responseResult = _httpClientHelper.Post<WebResponseContent>("GetOutBoundTrayTaskAsync", new CreateTaskDto()
                                        {
                                            WarehouseId = position.WarehouseId,
@@ -183,12 +201,19 @@
                            // ========== æ£€æŸ¥ PLC_STB æ ‡å¿— ==========
                            // åªæœ‰å½“ PLC_STB ä¸º 1 æ—¶æ‰å¤„理任务
                            if (command.PLC_STB != 1) return;
                            if (command.PLC_STB != 1)
                            {
                                _logger.LogDebug("Execute:子设备 {ChildDeviceCode} PLC_STB ä¸ä¸º1,跳过", childDeviceCode);
                                QuartzLogger.Debug($"子设备 {childDeviceCode} PLC_STB ä¸ä¸º1,跳过", conveyorLine.DeviceCode);
                                return;
                            }
                            // ========== å¤„理无托盘条码的情况 ==========
                            // æ— æ‰˜ç›˜æ¡ç æ—¶ï¼Œè¯·æ±‚出库任务
                            if (command.Barcode.IsNullOrEmpty() || command.Barcode.Replace("\0", "") == "")
                            {
                                _logger.LogDebug("Execute:子设备 {ChildDeviceCode} æ— æ‰˜ç›˜æ¡ç ï¼Œè¯·æ±‚出库任务", childDeviceCode);
                                QuartzLogger.Debug($"子设备 {childDeviceCode} æ— æ‰˜ç›˜æ¡ç ï¼Œè¯·æ±‚出库任务", conveyorLine.DeviceCode);
                                _conveyorLineDispatch.RequestOutbound(conveyorLine, command, childDeviceCode);
                                return;
                            }
@@ -200,6 +225,8 @@
                                Dt_Task task = _taskService.QueryExecutingConveyorLineTask(command.TaskNo, childDeviceCode);
                                if (!task.IsNullOrEmpty())
                                {
                                    _logger.LogInformation("Execute:子设备 {ChildDeviceCode} å¤„理任务 {TaskNum},状态: {Status}", childDeviceCode, task.TaskNum, task.TaskStatus);
                                    QuartzLogger.Info($"处理任务 {task.TaskNum},状态: {task.TaskStatus}", conveyorLine.DeviceCode);
                                    // å¤„理任务状态(根据状态分发到不同方法)
                                    ProcessTaskState(conveyorLine, command, task, childDeviceCode);
                                    return;
@@ -209,7 +236,8 @@
                        catch (Exception innerEx)
                        {
                            // è®°å½•异常,但不影响其他子设备的处理
                            Console.Error.WriteLine($"{DateTime.UtcNow:O} [{childDeviceCode}] CorrelationId={correlationId} {innerEx}");
                            _logger.LogError(innerEx, "Execute:子设备 {ChildDeviceCode} å¤„理异常,CorrelationId: {CorrelationId}", childDeviceCode, correlationId);
                            QuartzLogger.Error($"子设备处理异常: {innerEx.Message}", conveyorLine.DeviceCode, innerEx);
                        }
                    });
                }
@@ -217,7 +245,8 @@
            catch (Exception ex)
            {
                // è®°å½•整体异常
                Console.Error.WriteLine(ex);
                _logger.LogError(ex, "Execute:输送线 {DeviceCode} æ‰§è¡Œå¼‚常", ex.Message);
                QuartzLogger.Error($"输送线执行异常: {ex.Message}", "CommonConveyorLineNewJob", ex);
            }
            return Task.CompletedTask;
        }
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ConveyorLineNewJob/ConveyorLineDispatchHandler.cs
@@ -1,7 +1,9 @@
using MapsterMapper;
using Microsoft.Extensions.Logging;
using WIDESEAWCS_Common.TaskEnum;
using WIDESEAWCS_Core;
using WIDESEAWCS_Core.Helper;
using WIDESEAWCS_Core.LogHelper;
using WIDESEAWCS_ITaskInfoService;
using WIDESEAWCS_Model.Models;
using WIDESEAWCS_QuartzJob;
@@ -47,6 +49,11 @@
        private readonly IMapper _mapper;
        /// <summary>
        /// æ—¥å¿—记录器
        /// </summary>
        private readonly ILogger _logger;
        /// <summary>
        /// è¾“送线任务过滤器
        /// </summary>
        /// <remarks>
@@ -69,16 +76,18 @@
        /// <param name="taskExecuteDetailService">任务执行明细服务</param>
        /// <param name="routerService">路由服务</param>
        /// <param name="mapper">对象映射器</param>
        public ConveyorLineDispatchHandler(ITaskService taskService, ITaskExecuteDetailService taskExecuteDetailService, IRouterService routerService, IMapper mapper)
        /// <param name="logger">日志记录器</param>
        public ConveyorLineDispatchHandler(ITaskService taskService, ITaskExecuteDetailService taskExecuteDetailService, IRouterService routerService, IMapper mapper, ILogger logger)
        {
            _taskService = taskService;
            _taskExecuteDetailService = taskExecuteDetailService;
            _routerService = routerService;
            _mapper = mapper;
            _logger = logger;
            // åˆå§‹åŒ–任务过滤器和目标地址选择器
            _taskFilter = new ConveyorLineTaskFilter(taskService);
            _targetAddressSelector = new ConveyorLineTargetAddressSelector();
            _taskFilter = new ConveyorLineTaskFilter(taskService, _logger);
            _targetAddressSelector = new ConveyorLineTargetAddressSelector(_logger);
        }
        /// <summary>
@@ -96,6 +105,8 @@
        {
            // æ¸…除任务号,表示当前空闲
            conveyorLine.SetValue(ConveyorLineDBNameNew.TaskNo, 0, childDeviceCode);
            _logger.LogDebug("HeartBeat:子设备 {ChildDeviceCode} å¿ƒè·³", childDeviceCode);
            QuartzLogger.Debug($"HeartBeat:子设备 {childDeviceCode} å¿ƒè·³", conveyorLine.DeviceCode);
        }
        /// <summary>
@@ -114,6 +125,9 @@
        /// <param name="childDeviceCode">子设备编码</param>
        public void RequestInbound(CommonConveyorLine conveyorLine, ConveyorLineTaskCommandNew command, string childDeviceCode)
        {
            _logger.LogInformation("RequestInbound:子设备 {ChildDeviceCode} è¯·æ±‚入库", childDeviceCode);
            QuartzLogger.Info($"请求入库,子设备: {childDeviceCode}", conveyorLine.DeviceCode);
            // å‘ WMS è¯·æ±‚新任务(基于条码)
            if (_taskFilter.RequestWmsTask(command.Barcode, childDeviceCode))
            {
@@ -132,6 +146,9 @@
                    // æ›´æ–°ä»»åŠ¡çŠ¶æ€åˆ°ä¸‹ä¸€é˜¶æ®µ
                    _taskService.UpdateTaskStatusToNext(task);
                    _logger.LogInformation("RequestInbound:入库任务已下发,任务号: {TaskNum},子设备: {ChildDeviceCode}", task.TaskNum, childDeviceCode);
                    QuartzLogger.Info($"入库任务已下发,任务号: {task.TaskNum},子设备: {childDeviceCode}", conveyorLine.DeviceCode);
                }
            }
        }
@@ -152,8 +169,13 @@
            Dt_Task? task = _taskFilter.QueryExecutingTask(command.TaskNo, childDeviceCode);
            if (task == null)
            {
                _logger.LogDebug("RequestInNextAddress:任务 {TaskNo} ä¸å­˜åœ¨", command.TaskNo);
                QuartzLogger.Debug($"RequestInNextAddress:任务 {command.TaskNo} ä¸å­˜åœ¨", conveyorLine.DeviceCode);
                return;
            }
            _logger.LogInformation("RequestInNextAddress:入库下一地址,任务号: {TaskNum},子设备: {ChildDeviceCode}", task.TaskNum, childDeviceCode);
            QuartzLogger.Info($"RequestInNextAddress:入库下一地址,任务号: {task.TaskNum},子设备: {childDeviceCode}", conveyorLine.DeviceCode);
            // å¦‚果不是空托盘任务,处理目标地址(与拘束机/插拔钉机交互)
            if (task.TaskType != (int)TaskOutboundTypeEnum.OutEmpty)
@@ -189,7 +211,9 @@
                // æ›´æ–°ä»»åŠ¡çŠ¶æ€åˆ°ä¸‹ä¸€é˜¶æ®µï¼ˆé€šå¸¸æ˜¯å®Œæˆï¼‰
                WebResponseContent content = _taskService.UpdateTaskStatusToNext(task);
                Console.Out.WriteLine(content.Serialize());
                _logger.LogInformation("ConveyorLineInFinish:入库完成,任务号: {TaskNum},子设备: {ChildDeviceCode}", task.TaskNum, childDeviceCode);
                QuartzLogger.Info($"入库完成,任务号: {task.TaskNum}", conveyorLine.DeviceCode);
            }
        }
@@ -226,6 +250,9 @@
                // æ›´æ–°ä»»åŠ¡çŠ¶æ€
                _taskService.UpdateTaskStatusToNext(task);
                _logger.LogInformation("RequestOutbound:出库任务已下发,任务号: {TaskNum},子设备: {ChildDeviceCode}", task.TaskNum, childDeviceCode);
                QuartzLogger.Info($"出库任务已下发,任务号: {task.TaskNum}", conveyorLine.DeviceCode);
            }
        }
@@ -245,8 +272,13 @@
            Dt_Task? task = _taskFilter.QueryExecutingTask(command.TaskNo, childDeviceCode);
            if (task == null)
            {
                _logger.LogDebug("RequestOutNextAddress:任务 {TaskNo} ä¸å­˜åœ¨", command.TaskNo);
                QuartzLogger.Debug($"RequestOutNextAddress:任务 {command.TaskNo} ä¸å­˜åœ¨", conveyorLine.DeviceCode);
                return;
            }
            _logger.LogInformation("RequestOutNextAddress:出库下一地址,任务号: {TaskNum},子设备: {ChildDeviceCode}", task.TaskNum, childDeviceCode);
            QuartzLogger.Info($"RequestOutNextAddress:出库下一地址,任务号: {task.TaskNum},子设备: {childDeviceCode}", conveyorLine.DeviceCode);
            // å¦‚果不是空托盘任务,处理目标地址
            if (task.TaskType != (int)TaskOutboundTypeEnum.OutEmpty)
@@ -282,7 +314,9 @@
                // æ›´æ–°ä»»åŠ¡çŠ¶æ€åˆ°ä¸‹ä¸€é˜¶æ®µï¼ˆé€šå¸¸æ˜¯å®Œæˆï¼‰
                WebResponseContent content = _taskService.UpdateTaskStatusToNext(task);
                Console.Out.WriteLine(content.Serialize());
                _logger.LogInformation("ConveyorLineOutFinish:出库完成,任务号: {TaskNum},子设备: {ChildDeviceCode}", task.TaskNum, childDeviceCode);
                QuartzLogger.Info($"出库完成,任务号: {task.TaskNum}", conveyorLine.DeviceCode);
            }
        }
    }
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ConveyorLineNewJob/ConveyorLineTargetAddressSelector.cs
@@ -1,3 +1,5 @@
using Microsoft.Extensions.Logging;
using WIDESEAWCS_Core.LogHelper;
using WIDESEAWCS_QuartzJob;
namespace WIDESEAWCS_Tasks
@@ -44,6 +46,20 @@
        private static readonly List<string> PinMachineCodes = new List<string> { "10190", "20100" };
        /// <summary>
        /// æ—¥å¿—记录器
        /// </summary>
        private readonly ILogger _logger;
        /// <summary>
        /// æž„造函数
        /// </summary>
        /// <param name="logger">日志记录器</param>
        public ConveyorLineTargetAddressSelector(ILogger logger)
        {
            _logger = logger;
        }
        /// <summary>
        /// å¤„理入库场景的下一地址请求
        /// </summary>
        /// <remarks>
@@ -55,6 +71,8 @@
        /// <param name="childDeviceCode">当前子设备编码</param>
        public void HandleInboundNextAddress(CommonConveyorLine conveyorLine, string nextAddress, string childDeviceCode)
        {
            _logger.LogDebug("HandleInboundNextAddress:入库下一地址,子设备: {ChildDeviceCode},目标地址: {NextAddress}", childDeviceCode, nextAddress);
            QuartzLogger.Debug($"HandleInboundNextAddress:入库下一地址,子设备: {childDeviceCode},目标地址: {nextAddress}", conveyorLine.DeviceCode);
            // è°ƒç”¨é€šç”¨å¤„理方法,isUpper = true è¡¨ç¤ºå¤„理上层
            HandleDeviceRequest(conveyorLine, nextAddress, childDeviceCode, isUpper: true);
        }
@@ -71,6 +89,8 @@
        /// <param name="childDeviceCode">当前子设备编码</param>
        public void HandleOutboundNextAddress(CommonConveyorLine conveyorLine, string nextAddress, string childDeviceCode)
        {
            _logger.LogDebug("HandleOutboundNextAddress:出库下一地址,子设备: {ChildDeviceCode},目标地址: {NextAddress}", childDeviceCode, nextAddress);
            QuartzLogger.Debug($"HandleOutboundNextAddress:出库下一地址,子设备: {childDeviceCode},目标地址: {nextAddress}", conveyorLine.DeviceCode);
            // è°ƒç”¨é€šç”¨å¤„理方法,isUpper = false è¡¨ç¤ºå¤„理下层
            HandleDeviceRequest(conveyorLine, nextAddress, childDeviceCode, isUpper: false);
        }
@@ -99,6 +119,8 @@
                ConstraintMachine? constraint = devices.OfType<ConstraintMachine>().FirstOrDefault(d => d.DeviceName == ConstraintMachineName);
                if (constraint == null)
                {
                    _logger.LogDebug("HandleDeviceRequest:未找到拘束机设备");
                    QuartzLogger.Debug("HandleDeviceRequest:未找到拘束机设备", conveyorLine.DeviceCode);
                    // æœªæ‰¾åˆ°æ‹˜æŸæœºè®¾å¤‡ï¼Œç›´æŽ¥è¿”回
                    return;
                }
@@ -126,7 +148,8 @@
                        {
                            constraint.SetValue(ConstraintMachineDBName.ConstraintTrayOutputReadyLower, outputReq ? 1 : 0);
                        }
                    });
                    },
                    "拘束机");
            }
            else if (PinMachineCodes.Contains(nextAddress))
            {
@@ -135,6 +158,8 @@
                PinMachine? pinMachine = devices.OfType<PinMachine>().FirstOrDefault(d => d.DeviceName == PinMachineName);
                if (pinMachine == null)
                {
                    _logger.LogDebug("HandleDeviceRequest:未找到插拔钉机设备");
                    QuartzLogger.Debug("HandleDeviceRequest:未找到插拔钉机设备", conveyorLine.DeviceCode);
                    return;
                }
@@ -161,7 +186,8 @@
                        {
                            pinMachine.SetValue(PinMachineDBName.PlugPinTrayOutputReadyLower, outputReq ? 1 : 0);
                        }
                    });
                    },
                    "插拔钉机");
            }
        }
@@ -178,18 +204,24 @@
        /// <param name="getMaterialRequest">获取物料请求状态的委托</param>
        /// <param name="getOutputRequest">获取出料请求状态的委托</param>
        /// <param name="setOutputReady">设置输出就绪标志的委托</param>
        private static void ProcessDeviceRequest(
        /// <param name="deviceType">设备类型描述</param>
        private void ProcessDeviceRequest(
            CommonConveyorLine conveyorLine,
            string childDeviceCode,
            Func<bool> getMaterialRequest,
            Func<bool> getOutputRequest,
            Action<bool> setOutputReady)
            Action<bool> setOutputReady,
            string deviceType)
        {
            // èŽ·å–ç‰©æ–™è¯·æ±‚çŠ¶æ€
            bool materialReq = getMaterialRequest();
            // èŽ·å–å‡ºæ–™è¯·æ±‚çŠ¶æ€
            bool outputReq = getOutputRequest();
            _logger.LogDebug("ProcessDeviceRequest:{DeviceType},子设备: {ChildDeviceCode},物料请求: {MaterialReq},出料请求: {OutputReq}",
                deviceType, childDeviceCode, materialReq, outputReq);
            QuartzLogger.Debug($"ProcessDeviceRequest:{deviceType},子设备: {childDeviceCode},物料请求: {materialReq},出料请求: {outputReq}", conveyorLine.DeviceCode);
            // å¦‚果设备需要物料
            if (materialReq)
@@ -199,6 +231,9 @@
                // å›žå¤ ACK ç¡®è®¤ä¿¡å·
                conveyorLine.SetValue(ConveyorLineDBNameNew.WCS_ACK, 1, childDeviceCode);
                _logger.LogInformation("ProcessDeviceRequest:{DeviceType} éœ€è¦ç‰©æ–™ï¼Œå·²è®¾ç½®ç›®æ ‡åœ°å€å’ŒACK", deviceType);
                QuartzLogger.Info($"ProcessDeviceRequest:{deviceType} éœ€è¦ç‰©æ–™ï¼Œå·²è®¾ç½®ç›®æ ‡åœ°å€å’ŒACK", conveyorLine.DeviceCode);
            }
            else
            {
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/ConveyorLineNewJob/ConveyorLineTaskFilter.cs
@@ -1,3 +1,5 @@
using Microsoft.Extensions.Logging;
using WIDESEAWCS_Core.LogHelper;
using WIDESEAWCS_ITaskInfoService;
using WIDESEAWCS_Model.Models;
@@ -26,12 +28,19 @@
        private readonly ITaskService _taskService;
        /// <summary>
        /// æ—¥å¿—记录器
        /// </summary>
        private readonly ILogger _logger;
        /// <summary>
        /// æž„造函数
        /// </summary>
        /// <param name="taskService">任务服务实例</param>
        public ConveyorLineTaskFilter(ITaskService taskService)
        /// <param name="logger">日志记录器</param>
        public ConveyorLineTaskFilter(ITaskService taskService, ILogger logger)
        {
            _taskService = taskService;
            _logger = logger;
        }
        /// <summary>
@@ -46,7 +55,10 @@
        /// <returns>待处理的任务对象,如果没有则返回 null</returns>
        public Dt_Task? QueryPendingTask(string deviceCode, string childDeviceCode)
        {
            return _taskService.QueryConveyorLineTask(deviceCode, childDeviceCode);
            var task = _taskService.QueryConveyorLineTask(deviceCode, childDeviceCode);
            _logger.LogDebug("QueryPendingTask:设备 {DeviceCode},子设备 {ChildDeviceCode},查询结果: {TaskNum}", deviceCode, childDeviceCode, task?.TaskNum);
            QuartzLogger.Debug($"QueryPendingTask:设备 {deviceCode},子设备 {childDeviceCode},查询结果: {task?.TaskNum}", deviceCode);
            return task;
        }
        /// <summary>
@@ -61,7 +73,10 @@
        /// <returns>执行中的任务对象,如果没有则返回 null</returns>
        public Dt_Task? QueryExecutingTask(int taskNo, string childDeviceCode)
        {
            return _taskService.QueryExecutingConveyorLineTask(taskNo, childDeviceCode);
            var task = _taskService.QueryExecutingConveyorLineTask(taskNo, childDeviceCode);
            _logger.LogDebug("QueryExecutingTask:任务号 {TaskNo},子设备 {ChildDeviceCode},查询结果: {Found}", taskNo, childDeviceCode, task != null);
            QuartzLogger.Debug($"QueryExecutingTask:任务号 {taskNo},子设备 {childDeviceCode},查询结果: {(task != null)}", childDeviceCode);
            return task;
        }
        /// <summary>
@@ -76,7 +91,10 @@
        /// <returns>请求是否成功</returns>
        public bool RequestWmsTask(string barcode, string childDeviceCode)
        {
            return _taskService.RequestWMSTask(barcode, childDeviceCode).Status;
            _logger.LogInformation("RequestWmsTask:向WMS请求任务,条码: {Barcode},子设备: {ChildDeviceCode}", barcode, childDeviceCode);
            QuartzLogger.Info($"向WMS请求任务,条码: {barcode}", childDeviceCode);
            var result = _taskService.RequestWMSTask(barcode, childDeviceCode);
            return result.Status;
        }
    }
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotClientManager.cs
@@ -1,5 +1,6 @@
using System.Collections.Concurrent;
using System.Net.Sockets;
using Microsoft.Extensions.Logging;
using WIDESEAWCS_Core.LogHelper;
using WIDESEAWCS_QuartzJob;
using WIDESEAWCS_Tasks.SocketServer;
@@ -27,6 +28,11 @@
        /// æœºæ¢°æ‰‹çŠ¶æ€ç®¡ç†å™¨ï¼Œç”¨äºŽè¯»å†™è®¾å¤‡çŠ¶æ€
        /// </summary>
        private readonly RobotStateManager _stateManager;
        /// <summary>
        /// æ—¥å¿—记录器
        /// </summary>
        private readonly ILogger _logger;
        /// <summary>
        /// è·Ÿè¸ªå·²å¯åŠ¨æ¶ˆæ¯å¤„ç†çš„å®¢æˆ·ç«¯ï¼Œé¿å…é‡å¤å¯åŠ¨
@@ -59,10 +65,12 @@
        /// </summary>
        /// <param name="tcpSocket">TCP Socket æœåŠ¡å™¨å®žä¾‹</param>
        /// <param name="stateManager">状态管理器实例</param>
        public RobotClientManager(TcpSocketServer tcpSocket, RobotStateManager stateManager)
        /// <param name="logger">日志记录器</param>
        public RobotClientManager(TcpSocketServer tcpSocket, RobotStateManager stateManager, ILogger logger)
        {
            _tcpSocket = tcpSocket;
            _stateManager = stateManager;
            _logger = logger;
        }
        /// <summary>
@@ -92,6 +100,8 @@
                // æ¸…理该客户端的 HandleClientAsync å¯åŠ¨æ ‡å¿—
                // ä»¥ä¾¿ä¸‹æ¬¡é‡è¿žæ—¶å¯ä»¥é‡æ–°å¯åŠ¨å¤„ç†
                _handleClientStarted.TryRemove(ipAddress, out _);
                _logger.LogDebug("客户端未连接,IP: {IpAddress}", ipAddress);
                QuartzLogger.Debug($"客户端未连接,IP: {ipAddress}", robotCrane.DeviceName);
                return false;
            }
@@ -101,8 +111,9 @@
            {
                // ç»‘定客户端断开连接的事件处理
                _tcpSocket.RobotReceived += OnRobotReceived;
                // è®°å½•日志(注意:日志内容为"客户端已断开连接",可能是遗留的占位文本)
                QuartzLogger.Error($"客户端已断开连接", robotCrane.DeviceName);
                // è®°å½•日志:事件订阅成功
                _logger.LogInformation("机械手TCP消息事件已订阅,设备: {DeviceName}", robotCrane.DeviceName);
                QuartzLogger.Info($"机械手TCP消息事件已订阅", robotCrane.DeviceName);
            }
            // ä»Ž TCP æœåŠ¡å™¨çš„å®¢æˆ·ç«¯å­—å…¸ä¸­èŽ·å– TcpClient å¯¹è±¡
@@ -114,6 +125,8 @@
            {
                // ç§»é™¤å¯åŠ¨æ ‡å¿—ï¼Œè¿”å›ž false è¡¨ç¤ºå®¢æˆ·ç«¯ä¸å¯ç”¨
                _handleClientStarted.TryRemove(ipAddress, out _);
                _logger.LogWarning("获取TcpClient失败,IP: {IpAddress}", ipAddress);
                QuartzLogger.Warn($"获取TcpClient失败,IP: {ipAddress}", robotCrane.DeviceName);
                return false;
            }
@@ -123,8 +136,9 @@
            // å¦‚果尚未启动,则启动消息处理循环
            if (!alreadyStarted)
            {
                // è®°å½•日志
                QuartzLogger.Error($"启动客户端消息处理", robotCrane.DeviceName);
                // è®°å½•日志:启动消息处理
                _logger.LogInformation("启动客户端消息处理,IP: {IpAddress}", ipAddress);
                QuartzLogger.Info($"启动客户端消息处理", robotCrane.DeviceName);
                // èŽ·å–æœ€æ–°çš„çŠ¶æ€å¯¹è±¡
                var latestStateForSubscribe = _stateManager.GetState(ipAddress);
@@ -142,8 +156,8 @@
                            if (t.IsFaulted)
                            {
                                // è®°å½•错误日志
                                QuartzLogger.Error($"监听客户端消息事件异常", robotCrane.DeviceName);
                                Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] HandleClientAsync error: {t.Exception?.GetBaseException().Message}");
                                _logger.LogError(t.Exception, "监听客户端消息事件异常,IP: {IpAddress}", ipAddress);
                                QuartzLogger.Error($"监听客户端消息事件异常", robotCrane.DeviceName, t.Exception);
                                // å‘生错误时,移除启动标志,允许下次重试
                                _handleClientStarted.TryRemove(ipAddress, out _);
                            }
@@ -179,6 +193,10 @@
            // ç§»é™¤è¯¥å®¢æˆ·ç«¯çš„ HandleClientAsync å¯åŠ¨æ ‡å¿—
            _handleClientStarted.TryRemove(clientId, out _);
            // è®°å½•日志:客户端断开连接
            _logger.LogInformation("客户端断开连接,IP: {ClientId}", clientId);
            QuartzLogger.Info($"客户端断开连接", clientId);
            // é‡ç½®è¯¥å®¢æˆ·ç«¯çš„状态信息
            _stateManager.TryUpdateStateSafely(clientId, state =>
            {
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotJob.cs
@@ -116,7 +116,7 @@
            ILogger<RobotJob> logger)
        {
            // åˆå§‹åŒ–状态管理器,传入缓存服务
            _stateManager = new RobotStateManager(cache);
            _stateManager = new RobotStateManager(cache, _logger);
            _logger = logger;
            // åˆ›å»º Socket ç½‘关,封装 TcpSocketServer çš„访问
@@ -124,10 +124,10 @@
            ISocketClientGateway socketGateway = new SocketClientGateway(tcpSocket);
            // åˆå§‹åŒ–任务处理器
            _taskProcessor = new RobotTaskProcessor(socketGateway, _stateManager, robotTaskService, taskService, httpClientHelper);
            _taskProcessor = new RobotTaskProcessor(socketGateway, _stateManager, robotTaskService, taskService, httpClientHelper, _logger);
            // åˆå§‹åŒ–客户端管理器
            _clientManager = new RobotClientManager(tcpSocket, _stateManager);
            _clientManager = new RobotClientManager(tcpSocket, _stateManager, _logger);
            // åˆå§‹åŒ–命令处理器
            // ç®€å•命令处理器:处理状态更新等简单命令
@@ -139,7 +139,7 @@
            _messageRouter = new RobotMessageHandler(socketGateway, _stateManager, cache, simpleCommandHandler, prefixCommandHandler, logger);
            // åˆå§‹åŒ–工作流编排器
            _workflowOrchestrator = new RobotWorkflowOrchestrator(_stateManager, _clientManager, _taskProcessor, robotTaskService);
            _workflowOrchestrator = new RobotWorkflowOrchestrator(_stateManager, _clientManager, _taskProcessor, robotTaskService, _logger);
            // è®¢é˜…客户端断开连接事件
            _clientManager.OnClientDisconnected += OnClientDisconnected;
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotMessageHandler.cs
@@ -117,7 +117,7 @@
        {
            // è®°å½•接收到的消息日志
            _logger.LogInformation($"接收到客户端【{state.RobotCrane.DeviceName}】发送消息【{message}】");
            QuartzLogger.Error($"接收到客户端消息【{message}】", state.RobotCrane.DeviceName);
            QuartzLogger.Info($"接收到客户端消息【{message}】", state.RobotCrane.DeviceName);
            // æž„建缓存键,检查 Redis ä¸­æ˜¯å¦å­˜åœ¨è¯¥è®¾å¤‡çš„状态
            var cacheKey = $"{RedisPrefix.Code}:{RedisName.SocketDevices}:{client.Client.RemoteEndPoint}";
@@ -141,7 +141,7 @@
                // å¤„理成功后,将原消息回写到客户端(保持原有行为)
                await _socketClientGateway.SendMessageAsync(client, message);
                _logger.LogInformation($"发送消息【{message}】");
                QuartzLogger.Error($"发送消息:【{message}】", state.RobotCrane.DeviceName);
                QuartzLogger.Info($"发送消息:【{message}】", state.RobotCrane.DeviceName);
                // å®‰å…¨æ›´æ–°çŠ¶æ€åˆ° Redis
                _stateManager.TryUpdateStateSafely(activeState.IPAddress, activeState);
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotStateManager.cs
@@ -1,6 +1,8 @@
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using WIDESEAWCS_Common;
using WIDESEAWCS_Core.Caches;
using WIDESEAWCS_Core.LogHelper;
using WIDESEAWCS_QuartzJob;
namespace WIDESEAWCS_Tasks
@@ -20,12 +22,19 @@
        private readonly ICacheService _cache;
        /// <summary>
        /// æ—¥å¿—记录器
        /// </summary>
        private readonly ILogger _logger;
        /// <summary>
        /// æž„造函数
        /// </summary>
        /// <param name="cache">缓存服务实例(通常为 HybridCacheService)</param>
        public RobotStateManager(ICacheService cache)
        /// <param name="logger">日志记录器</param>
        public RobotStateManager(ICacheService cache, ILogger logger)
        {
            _cache = cache;
            _logger = logger;
        }
        /// <summary>
@@ -100,6 +109,8 @@
                newState.Version = DateTime.UtcNow.Ticks;
                // ç›´æŽ¥æ·»åŠ åˆ°ç¼“å­˜
                _cache.AddObject(cacheKey, newState);
                _logger.LogDebug("TryUpdateStateSafely:创建新状态,IP: {IpAddress}", ipAddress);
                QuartzLogger.Debug($"创建新状态,IP: {ipAddress}", ipAddress);
                return true;
            }
@@ -110,12 +121,20 @@
            newState.Version = DateTime.UtcNow.Ticks;
            // å°è¯•安全更新,如果版本冲突则返回 false
            return _cache.TrySafeUpdate(
            bool success = _cache.TrySafeUpdate(
                cacheKey,
                newState,
                expectedVersion,
                s => s.Version
            );
            if (!success)
            {
                _logger.LogWarning("TryUpdateStateSafely:版本冲突,更新失败,IP: {IpAddress},期望版本: {ExpectedVersion}", ipAddress, expectedVersion);
                QuartzLogger.Warn($"版本冲突,更新失败,IP: {ipAddress}", ipAddress);
            }
            return success;
        }
        /// <summary>
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/RobotTaskProcessor.cs
@@ -1,3 +1,4 @@
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using WIDESEA_Core;
using WIDESEAWCS_Common;
@@ -68,6 +69,11 @@
        private readonly HttpClientHelper _httpClientHelper;
        /// <summary>
        /// æ—¥å¿—记录器
        /// </summary>
        private readonly ILogger _logger;
        /// <summary>
        /// æž„造函数
        /// </summary>
        /// <param name="socketClientGateway">Socket ç½‘å…³</param>
@@ -75,18 +81,21 @@
        /// <param name="robotTaskService">机器人任务服务</param>
        /// <param name="taskService">通用任务服务</param>
        /// <param name="httpClientHelper">HTTP å®¢æˆ·ç«¯å¸®åŠ©ç±»</param>
        /// <param name="logger">日志记录器</param>
        public RobotTaskProcessor(
            ISocketClientGateway socketClientGateway,
            RobotStateManager stateManager,
            IRobotTaskService robotTaskService,
            ITaskService taskService,
            HttpClientHelper httpClientHelper)
            HttpClientHelper httpClientHelper,
            ILogger logger)
        {
            _socketClientGateway = socketClientGateway;
            _stateManager = stateManager;
            _robotTaskService = robotTaskService;
            _taskService = taskService;
            _httpClientHelper = httpClientHelper;
            _logger = logger;
        }
        /// <summary>
@@ -141,8 +150,9 @@
            if (result)
            {
                // å‘送成功,记录日志
                QuartzLogger.Error($"下发取货指令,指令: {taskString}", state.RobotCrane.DeviceName);
                // å‘送成功,记录 Info æ—¥å¿—
                _logger.LogInformation("下发取货指令成功,指令: {TaskString},设备: {DeviceName}", taskString, state.RobotCrane?.DeviceName);
                QuartzLogger.Info($"下发取货指令成功,指令: {taskString}", state.RobotCrane?.DeviceName);
                // æ›´æ–°ä»»åŠ¡çŠ¶æ€ä¸º"机器人执行中"
                task.RobotTaskState = TaskRobotStatusEnum.RobotExecuting.GetHashCode();
@@ -156,6 +166,12 @@
                {
                    await _robotTaskService.UpdateRobotTaskAsync(task);
                }
            }
            else
            {
                // å‘送失败,记录 Error æ—¥å¿—
                _logger.LogError("下发取货指令失败,指令: {TaskString},设备: {DeviceName}", taskString, state.RobotCrane?.DeviceName);
                QuartzLogger.Error($"下发取货指令失败,指令: {taskString}", state.RobotCrane?.DeviceName);
            }
        }
@@ -182,6 +198,8 @@
            var currentTask = state.CurrentTask;
            if (currentTask == null)
            {
                _logger.LogDebug("HandleInboundTaskAsync:当前任务为空");
                QuartzLogger.Debug($"HandleInboundTaskAsync:当前任务为空", state.RobotCrane?.DeviceName ?? "Unknown");
                return false;
            }
@@ -213,6 +231,8 @@
                {
                    case RobotTaskTypeEnum.GroupPallet:
                        // ç»„盘任务不使用源地址,直接返回 false
                        _logger.LogDebug("HandleInboundTaskAsync:组盘任务不使用源地址");
                        QuartzLogger.Debug($"HandleInboundTaskAsync:组盘任务不使用源地址", state.RobotCrane?.DeviceName ?? "Unknown");
                        return false;
                    case RobotTaskTypeEnum.ChangePallet:
@@ -237,6 +257,8 @@
                    case RobotTaskTypeEnum.SplitPallet:
                        // æ‹†ç›˜ä»»åŠ¡ä¸ä½¿ç”¨ç›®æ ‡åœ°å€
                        _logger.LogDebug("HandleInboundTaskAsync:拆盘任务不使用目标地址");
                        QuartzLogger.Debug($"HandleInboundTaskAsync:拆盘任务不使用目标地址", state.RobotCrane?.DeviceName ?? "Unknown");
                        return true;
                }
            }
@@ -253,12 +275,18 @@
                TaskType = taskType                         // ä»»åŠ¡ç±»åž‹ï¼ˆå…¥åº“/空托盘入库)
            };
            // è®°å½•日志:开始调用 WMS åˆ›å»ºå…¥åº“任务
            _logger.LogInformation("HandleInboundTaskAsync:调用WMS创建入库任务,托盘码: {PalletCode},任务类型: {TaskType}", PalletCode, taskType);
            QuartzLogger.Info($"调用WMS创建入库任务,托盘码: {PalletCode},任务类型: {taskType}", state.RobotCrane?.DeviceName ?? "Unknown");
            // è°ƒç”¨ WMS æŽ¥å£åˆ›å»ºå…¥åº“任务
            var result = _httpClientHelper.Post<WebResponseContent>(nameof(ConfigKey.CreateTaskInboundAsync), taskDto.ToJson());
            // å¦‚果调用失败或返回错误状态
            if (!result.Data.Status && result.IsSuccess)
            {
                _logger.LogError("HandleInboundTaskAsync:WMS返回错误状态,Status: {Status}", result.Data.Status);
                QuartzLogger.Error($"HandleInboundTaskAsync:WMS返回错误状态", state.RobotCrane?.DeviceName ?? "Unknown");
                return false;
            }
@@ -269,6 +297,8 @@
            var content = _taskService.ReceiveWMSTask(new List<WMSTaskDTO> { taskDTO });
            if (!content.Status)
            {
                _logger.LogError("HandleInboundTaskAsync:接收WMS任务失败");
                QuartzLogger.Error($"HandleInboundTaskAsync:接收WMS任务失败", state.RobotCrane?.DeviceName ?? "Unknown");
                return false;
            }
@@ -298,6 +328,8 @@
                // æ›´æ–°ä»»åŠ¡çŠ¶æ€åˆ°ä¸‹ä¸€é˜¶æ®µ
                if (_taskService.UpdateTaskStatusToNext(taskInfo).Status)
                {
                    _logger.LogInformation("HandleInboundTaskAsync:入库任务处理成功,任务号: {TaskNum}", taskInfo.TaskNum);
                    QuartzLogger.Info($"HandleInboundTaskAsync:入库任务处理成功,任务号: {taskInfo.TaskNum}", state.RobotCrane?.DeviceName ?? "Unknown");
                    return true;
                }
            }
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/RobotJob/Workflow/RobotWorkflowOrchestrator.cs
@@ -1,3 +1,4 @@
using Microsoft.Extensions.Logging;
using WIDESEA_Core;
using WIDESEAWCS_Common.TaskEnum;
using WIDESEAWCS_Core.LogHelper;
@@ -58,22 +59,30 @@
        private readonly IRobotTaskService _robotTaskService;
        /// <summary>
        /// æ—¥å¿—记录器
        /// </summary>
        private readonly ILogger _logger;
        /// <summary>
        /// æž„造函数
        /// </summary>
        /// <param name="stateManager">状态管理器</param>
        /// <param name="clientManager">客户端管理器</param>
        /// <param name="taskProcessor">任务处理器</param>
        /// <param name="robotTaskService">任务服务</param>
        /// <param name="logger">日志记录器</param>
        public RobotWorkflowOrchestrator(
            RobotStateManager stateManager,
            RobotClientManager clientManager,
            RobotTaskProcessor taskProcessor,
            IRobotTaskService robotTaskService)
            IRobotTaskService robotTaskService,
            ILogger logger)
        {
            _stateManager = stateManager;
            _clientManager = clientManager;
            _taskProcessor = taskProcessor;
            _robotTaskService = robotTaskService;
            _logger = logger;
        }
        /// <summary>
@@ -112,6 +121,8 @@
                    && latestState.RobotArmObject == 1
                    && task.RobotTaskState == TaskRobotStatusEnum.RobotPickFinish.GetHashCode())
                {
                    _logger.LogInformation("ExecuteAsync:满足放货条件,开始处理取货完成,任务号: {TaskNum}", task.RobotTaskNum);
                    QuartzLogger.Info($"ExecuteAsync:满足放货条件,开始处理取货完成", latestState.RobotCrane?.DeviceName ?? ipAddress);
                    // å‘送放货指令
                    await HandlePickFinishedStateAsync(task, ipAddress);
                }
@@ -127,6 +138,8 @@
                    && (task.RobotTaskState == TaskRobotStatusEnum.RobotPutFinish.GetHashCode()
                    || task.RobotTaskState != TaskRobotStatusEnum.RobotExecuting.GetHashCode()))
                {
                    _logger.LogInformation("ExecuteAsync:满足取货条件,开始处理放货完成,任务号: {TaskNum}", task.RobotTaskNum);
                    QuartzLogger.Info($"ExecuteAsync:满足取货条件,开始处理放货完成", latestState.RobotCrane?.DeviceName ?? ipAddress);
                    // å‘送取货指令
                    await HandlePutFinishedStateAsync(task, ipAddress);
                }
@@ -155,8 +168,9 @@
            if (result)
            {
                // å‘送成功,记录日志
                QuartzLogger.Error($"下发放货指令,指�?: {taskString}", task.RobotRoadway);
                // å‘送成功,记录 Info æ—¥å¿—
                _logger.LogInformation("HandlePickFinishedStateAsync:下发放货指令成功,指令: {TaskString},任务号: {TaskNum}", taskString, task.RobotTaskNum);
                QuartzLogger.Info($"下发放货指令成功,指令: {taskString}", task.RobotRoadway);
                // æ›´æ–°ä»»åŠ¡çŠ¶æ€ä¸º"机器人执行中"
                task.RobotTaskState = TaskRobotStatusEnum.RobotExecuting.GetHashCode();
@@ -174,6 +188,12 @@
                        await _robotTaskService.UpdateRobotTaskAsync(task);
                    }
                }
            }
            else
            {
                // å‘送失败,记录 Error æ—¥å¿—
                _logger.LogError("HandlePickFinishedStateAsync:下发放货指令失败,指令: {TaskString},任务号: {TaskNum}", taskString, task.RobotTaskNum);
                QuartzLogger.Error($"下发放货指令失败,指令: {taskString}", task.RobotRoadway);
            }
        }
@@ -202,6 +222,8 @@
            var stateForUpdate = _stateManager.GetState(ipAddress);
            if (stateForUpdate == null)
            {
                _logger.LogWarning("HandlePutFinishedStateAsync:获取状态失败,IP: {IpAddress}", ipAddress);
                QuartzLogger.Warn($"HandlePutFinishedStateAsync:获取状态失败,IP: {ipAddress}", ipAddress);
                return;
            }
@@ -231,12 +253,19 @@
                    stateForUpdate.CellBarcode.Add(trayBarcode1);
                    stateForUpdate.CellBarcode.Add(trayBarcode2);
                    // è®°å½•日志
                    QuartzLogger.Error($"ȡ�������о�ţ���о: {trayBarcode1}+{trayBarcode2}", stateForUpdate.RobotCrane.DeviceName);
                    // è®°å½•日志:生成托盘条码成功
                    _logger.LogInformation("HandlePutFinishedStateAsync:生成托盘条码成功: {Barcode1}+{Barcode2},任务号: {TaskNum}", trayBarcode1, trayBarcode2, task.RobotTaskNum);
                    QuartzLogger.Info($"生成托盘条码成功: {trayBarcode1}+{trayBarcode2}", stateForUpdate.RobotCrane.DeviceName);
                    // å‘送取货指令
                    await _taskProcessor.SendSocketRobotPickAsync(task, stateForUpdate);
                }
                else
                {
                    // æ¡ç ç”Ÿæˆå¤±è´¥ï¼Œè®°å½•错误日志
                    _logger.LogError("HandlePutFinishedStateAsync:生成托盘条码失败,任务号: {TaskNum}", task.RobotTaskNum);
                    QuartzLogger.Error($"生成托盘条码失败", stateForUpdate.RobotCrane.DeviceName);
                }
            }
            else
            {
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/StackerCraneJob/CommonStackerCraneJob.cs
@@ -1,3 +1,4 @@
using Microsoft.Extensions.Logging;
using Quartz;
using System;
using System.Diagnostics.CodeAnalysis;
@@ -11,6 +12,7 @@
using WIDESEAWCS_QuartzJob.StackerCrane;
using WIDESEAWCS_Tasks.StackerCraneJob;
using WIDESEA_Core;
using WIDESEAWCS_Core.LogHelper;
using WIDESEAWCS_QuartzJob.Service;
namespace WIDESEAWCS_Tasks
@@ -79,6 +81,16 @@
        private readonly StackerCraneCommandBuilder _commandBuilder;
        /// <summary>
        /// æ—¥å¿—记录器
        /// </summary>
        private readonly ILogger<CommonStackerCraneJob> _logger;
        /// <summary>
        /// å †åž›æœºè®¾å¤‡ç¼–码
        /// </summary>
        private string _deviceCode = string.Empty;
        /// <summary>
        /// æž„造函数
        /// </summary>
        /// <param name="taskService">任务服务</param>
@@ -86,25 +98,28 @@
        /// <param name="taskRepository">任务仓储</param>
        /// <param name="routerService">路由服务</param>
        /// <param name="httpClientHelper">HTTP å®¢æˆ·ç«¯å¸®åŠ©ç±»</param>
        /// <param name="logger">日志记录器</param>
        public CommonStackerCraneJob(
            ITaskService taskService,
            ITaskExecuteDetailService taskExecuteDetailService,
            ITaskRepository taskRepository,
            IRouterService routerService,
            HttpClientHelper httpClientHelper)
            HttpClientHelper httpClientHelper,
            ILogger<CommonStackerCraneJob> logger)
        {
            _taskService = taskService;
            _taskExecuteDetailService = taskExecuteDetailService;
            _taskRepository = taskRepository;
            _logger = logger;
            // åŠ è½½é…ç½®æ–‡ä»¶
            _config = LoadConfig();
            // åˆå§‹åŒ–任务选择器
            _taskSelector = new StackerCraneTaskSelector(taskService, routerService, httpClientHelper);
            _taskSelector = new StackerCraneTaskSelector(taskService, routerService, httpClientHelper, _logger);
            // åˆå§‹åŒ–命令构建器
            _commandBuilder = new StackerCraneCommandBuilder(taskService, routerService, _config);
            _commandBuilder = new StackerCraneCommandBuilder(taskService, routerService, _config, _logger);
        }
        /// <summary>
@@ -161,23 +176,33 @@
                if (!flag || value is not IStackerCrane commonStackerCrane)
                {
                    // å‚数无效,直接返回
                    _logger.LogWarning("Execute:参数无效");
                    QuartzLogger.Warn("Execute:参数无效", "CommonStackerCraneJob");
                    return Task.CompletedTask;
                }
                _deviceCode = commonStackerCrane.DeviceCode;
                // ========== è®¢é˜…任务完成事件(全局只订阅一次) ==========
                if (!commonStackerCrane.IsEventSubscribed)
                {
                    // ç»‘定任务完成事件处理方法
                    commonStackerCrane.StackerCraneTaskCompletedEventHandler += CommonStackerCrane_StackerCraneTaskCompletedEventHandler;
                    _logger.LogInformation("Execute:订阅任务完成事件,设备: {DeviceCode}", _deviceCode);
                    QuartzLogger.Info($"订阅任务完成事件", _deviceCode);
                }
                // ========== æ£€æŸ¥å †åž›æœºä»»åŠ¡å®ŒæˆçŠ¶æ€ ==========
                commonStackerCrane.CheckStackerCraneTaskCompleted();
                _logger.LogDebug("Execute:检查任务完成状态,设备: {DeviceCode}", _deviceCode);
                QuartzLogger.Debug($"检查任务完成状态,设备: {_deviceCode}", _deviceCode);
                // ========== æ£€æŸ¥æ˜¯å¦å¯ä»¥å‘送新任务 ==========
                if (!commonStackerCrane.IsCanSendTask(commonStackerCrane.Communicator, commonStackerCrane.DeviceProDTOs, commonStackerCrane.DeviceProtocolDetailDTOs))
                {
                    // å †åž›æœºä¸å¯ç”¨ï¼ˆå¦‚正在执行上一任务),直接返回
                    _logger.LogDebug("Execute:堆垛机不可用,设备: {DeviceCode}", _deviceCode);
                    QuartzLogger.Debug($"堆垛机不可用,设备: {_deviceCode}", _deviceCode);
                    return Task.CompletedTask;
                }
@@ -187,8 +212,13 @@
                if (task == null)
                {
                    // æ²¡æœ‰å¯ç”¨ä»»åŠ¡
                    _logger.LogDebug("Execute:没有可用任务,设备: {DeviceCode}", _deviceCode);
                    QuartzLogger.Debug($"没有可用任务,设备: {_deviceCode}", _deviceCode);
                    return Task.CompletedTask;
                }
                _logger.LogInformation("Execute:选择任务,设备: {DeviceCode},任务号: {TaskNum}", _deviceCode, task.TaskNum);
                QuartzLogger.Info($"选择任务,任务号: {task.TaskNum}", _deviceCode);
                // ========== æž„建命令 ==========
                // å‘½ä»¤æž„建下沉到专用构建器
@@ -196,6 +226,8 @@
                if (stackerCraneTaskCommand == null)
                {
                    // å‘½ä»¤æž„建失败
                    _logger.LogWarning("Execute:命令构建失败,设备: {DeviceCode},任务号: {TaskNum}", _deviceCode, task.TaskNum);
                    QuartzLogger.Warn($"命令构建失败,任务号: {task.TaskNum}", _deviceCode);
                    return Task.CompletedTask;
                }
@@ -206,12 +238,21 @@
                    // å‘送成功,更新状态
                    commonStackerCrane.LastTaskType = task.TaskType;
                    _taskService.UpdateTaskStatusToNext(task.TaskNum);
                    _logger.LogInformation("Execute:命令发送成功,设备: {DeviceCode},任务号: {TaskNum}", _deviceCode, task.TaskNum);
                    QuartzLogger.Info($"命令发送成功,任务号: {task.TaskNum}", _deviceCode);
                }
                else
                {
                    _logger.LogError("Execute:命令发送失败,设备: {DeviceCode},任务号: {TaskNum}", _deviceCode, task.TaskNum);
                    QuartzLogger.Error($"命令发送失败", _deviceCode);
                }
            }
            catch (Exception ex)
            {
                // è®°å½•异常
                Console.WriteLine($"CommonStackerCraneJob Error: {ex.Message}");
                _logger.LogError(ex, "Execute:执行异常,设备: {DeviceCode}", _deviceCode);
                QuartzLogger.Error($"执行异常: {ex.Message}", _deviceCode, ex);
            }
            return Task.CompletedTask;
@@ -235,7 +276,8 @@
            if (stackerCrane != null)
            {
                // è®°å½•日志
                Console.Out.WriteLine("TaskCompleted" + e.TaskNum);
                _logger.LogInformation("CommonStackerCrane_StackerCraneTaskCompletedEventHandler:任务完成,任务号: {TaskNum}", e.TaskNum);
                QuartzLogger.Info($"任务完成,任务号: {e.TaskNum}", stackerCrane.DeviceCode);
                // æ›´æ–°ä»»åŠ¡çŠ¶æ€ä¸ºå®Œæˆ
                _taskService.StackCraneTaskCompleted(e.TaskNum);
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/StackerCraneJob/StackerCraneCommandBuilder.cs
@@ -1,6 +1,8 @@
using Microsoft.Extensions.Logging;
using System;
using System.Diagnostics.CodeAnalysis;
using WIDESEAWCS_Common.TaskEnum;
using WIDESEAWCS_Core.LogHelper;
using WIDESEAWCS_ITaskInfoService;
using WIDESEAWCS_Model.Models;
using WIDESEAWCS_QuartzJob.Models;
@@ -39,19 +41,27 @@
        private readonly StackerCraneCommandConfig _config;
        /// <summary>
        /// æ—¥å¿—记录器
        /// </summary>
        private readonly ILogger _logger;
        /// <summary>
        /// æž„造函数
        /// </summary>
        /// <param name="taskService">任务服务</param>
        /// <param name="routerService">路由服务</param>
        /// <param name="config">命令配置</param>
        /// <param name="logger">日志记录器</param>
        public StackerCraneCommandBuilder(
            ITaskService taskService,
            IRouterService routerService,
            StackerCraneCommandConfig config)
            StackerCraneCommandConfig config,
            ILogger logger)
        {
            _taskService = taskService;
            _routerService = routerService;
            _config = config;
            _logger = logger;
        }
        /// <summary>
@@ -66,6 +76,9 @@
        {
            // æ ¹æ®å··é“获取命令类型
            string commandType = GetCommandType(task.Roadway);
            _logger.LogInformation("ConvertToStackerCraneTaskCommand:构建命令,任务号: {TaskNum},巷道: {Roadway},命令类型: {CommandType}", task.TaskNum, task.Roadway, commandType);
            QuartzLogger.Info($"构建命令,任务号: {task.TaskNum},巷道: {task.Roadway},命令类型: {commandType}", task.Roadway);
            // æ ¹æ®å‘½ä»¤ç±»åž‹è°ƒç”¨ç›¸åº”的构建方法
            return commandType switch
@@ -90,10 +103,14 @@
            {
                if (roadway.Contains(mapping.Key))
                {
                    _logger.LogDebug("GetCommandType:匹配巷道 {Roadway},命令类型: {CommandType}", roadway, mapping.Value);
                    QuartzLogger.Debug($"GetCommandType:匹配巷道 {roadway},命令类型: {mapping.Value}", roadway);
                    return mapping.Value;
                }
            }
            _logger.LogDebug("GetCommandType:巷道 {Roadway} æœªåŒ¹é…ï¼Œä½¿ç”¨é»˜è®¤å‘½ä»¤ç±»åž‹: {DefaultType}", roadway, _config.DefaultCommandType);
            QuartzLogger.Debug($"GetCommandType:巷道 {roadway} æœªåŒ¹é…ï¼Œä½¿ç”¨é»˜è®¤å‘½ä»¤ç±»åž‹: {_config.DefaultCommandType}", roadway);
            return _config.DefaultCommandType;
        }
@@ -153,6 +170,9 @@
            // èŽ·å–ä»»åŠ¡ç±»åž‹åˆ†ç»„
            TaskTypeGroup taskTypeGroup = task.TaskType.GetTaskTypeGroup();
            _logger.LogDebug("BuildCommand:任务号: {TaskNum},任务类型分组: {TaskTypeGroup}", task.TaskNum, taskTypeGroup);
            QuartzLogger.Debug($"BuildCommand:任务号: {task.TaskNum},任务类型分组: {taskTypeGroup}", task.Roadway);
            // æ ¹æ®ä»»åŠ¡ç±»åž‹åˆ†å‘æž„å»º
            return taskTypeGroup switch
            {
@@ -178,6 +198,9 @@
        /// <returns>填充好的命令对象</returns>
        private T? BuildInboundCommand<T>(Dt_Task task, T command) where T : class
        {
            _logger.LogInformation("BuildInboundCommand:构建入库命令,任务号: {TaskNum}", task.TaskNum);
            QuartzLogger.Info($"BuildInboundCommand:构建入库命令,任务号: {task.TaskNum}", task.Roadway);
            // ç¡®å®šä»»åŠ¡ç±»åž‹ï¼ˆç©ºæ‰˜ç›˜ç”¨ç‰¹æ®Šç±»åž‹ 100)
            int taskType = 0;
            if (task.TaskType == (int)TaskOutboundTypeEnum.OutEmpty)
@@ -192,6 +215,8 @@
            if (router == null)
            {
                // æœªæ‰¾åˆ°ç«™å°ï¼Œæ›´æ–°å¼‚常信息
                _logger.LogError("BuildInboundCommand:未找到站台【{CurrentAddress}】信息,任务号: {TaskNum}", task.CurrentAddress, task.TaskNum);
                QuartzLogger.Error($"BuildInboundCommand:未找到站台【{task.CurrentAddress}】信息", task.Roadway);
                _taskService.UpdateTaskExceptionMessage(task.TaskNum, $"未找到站台【{task.CurrentAddress}】信息,无法获取对应的堆垛机取货站台信息");
                return null;
            }
@@ -204,6 +229,8 @@
            // è§£æžç›®æ ‡åœ°å€ï¼ˆåº“位地址)
            if (!TryParseAddress(task.NextAddress, out short endRow, out short endColumn, out short endLayer))
            {
                _logger.LogError("BuildInboundCommand:入库任务终点地址解析失败,终点: {NextAddress},任务号: {TaskNum}", task.NextAddress, task.TaskNum);
                QuartzLogger.Error($"BuildInboundCommand:入库任务终点地址解析失败,终点: {task.NextAddress}", task.Roadway);
                _taskService.UpdateTaskExceptionMessage(task.TaskNum, $"入库任务终点错误,终点:【{task.NextAddress}】");
                return null;
            }
@@ -212,6 +239,10 @@
            SetCommandProperty(command, "EndRow", endRow);
            SetCommandProperty(command, "EndColumn", endColumn);
            SetCommandProperty(command, "EndLayer", endLayer);
            _logger.LogInformation("BuildInboundCommand:入库命令构建成功,起点: {StartRow}-{StartColumn}-{StartLayer},终点: {EndRow}-{EndColumn}-{EndLayer},任务号: {TaskNum}",
                router.SrmRow, router.SrmColumn, router.SrmLayer, endRow, endColumn, endLayer, task.TaskNum);
            QuartzLogger.Info($"BuildInboundCommand:入库命令构建成功,起点: {router.SrmRow}-{router.SrmColumn}-{router.SrmLayer},终点: {endRow}-{endColumn}-{endLayer}", task.Roadway);
            return command;
        }
@@ -231,6 +262,9 @@
        /// <returns>填充好的命令对象</returns>
        private T? BuildOutboundCommand<T>(Dt_Task task, T command) where T : class
        {
            _logger.LogInformation("BuildOutboundCommand:构建出库命令,任务号: {TaskNum}", task.TaskNum);
            QuartzLogger.Info($"BuildOutboundCommand:构建出库命令,任务号: {task.TaskNum}", task.Roadway);
            // ç¡®å®šä»»åŠ¡ç±»åž‹
            int taskType = 0;
            if (task.TaskType == (int)TaskOutboundTypeEnum.OutEmpty)
@@ -244,6 +278,8 @@
            Dt_Router? router = _routerService.QueryNextRoute(task.Roadway, task.TargetAddress, taskType);
            if (router == null)
            {
                _logger.LogError("BuildOutboundCommand:未找到站台【{TargetAddress}】信息,任务号: {TaskNum}", task.TargetAddress, task.TaskNum);
                QuartzLogger.Error($"BuildOutboundCommand:未找到站台【{task.TargetAddress}】信息", task.Roadway);
                _taskService.UpdateTaskExceptionMessage(task.TaskNum, $"未找到站台【{task.TargetAddress}】信息,无法获取对应的堆垛机放货站台信息");
                return null;
            }
@@ -256,6 +292,8 @@
            // è§£æžèµ·å§‹åœ°å€ï¼ˆåº“位地址)
            if (!TryParseAddress(task.CurrentAddress, out short startRow, out short startColumn, out short startLayer))
            {
                _logger.LogError("BuildOutboundCommand:出库任务起点地址解析失败,起点: {CurrentAddress},任务号: {TaskNum}", task.CurrentAddress, task.TaskNum);
                QuartzLogger.Error($"BuildOutboundCommand:出库任务起点地址解析失败,起点: {task.CurrentAddress}", task.Roadway);
                _taskService.UpdateTaskExceptionMessage(task.TaskNum, $"出库任务起点错误,起点:【{task.CurrentAddress}】");
                return null;
            }
@@ -264,6 +302,10 @@
            SetCommandProperty(command, "StartRow", startRow);
            SetCommandProperty(command, "StartColumn", startColumn);
            SetCommandProperty(command, "StartLayer", startLayer);
            _logger.LogInformation("BuildOutboundCommand:出库命令构建成功,起点: {StartRow}-{StartColumn}-{StartLayer},终点: {EndRow}-{EndColumn}-{EndLayer},任务号: {TaskNum}",
                startRow, startColumn, startLayer, router.SrmRow, router.SrmColumn, router.SrmLayer, task.TaskNum);
            QuartzLogger.Info($"BuildOutboundCommand:出库命令构建成功,起点: {startRow}-{startColumn}-{startLayer},终点: {router.SrmRow}-{router.SrmColumn}-{router.SrmLayer}", task.Roadway);
            return command;
        }
@@ -283,9 +325,14 @@
        /// <returns>填充好的命令对象</returns>
        private T? BuildRelocationCommand<T>(Dt_Task task, T command) where T : class
        {
            _logger.LogInformation("BuildRelocationCommand:构建移库命令,任务号: {TaskNum}", task.TaskNum);
            QuartzLogger.Info($"BuildRelocationCommand:构建移库命令,任务号: {task.TaskNum}", task.Roadway);
            // è§£æžç›®æ ‡åœ°å€
            if (!TryParseAddress(task.NextAddress, out short endRow, out short endColumn, out short endLayer))
            {
                _logger.LogError("BuildRelocationCommand:移库任务终点地址解析失败,终点: {NextAddress},任务号: {TaskNum}", task.NextAddress, task.TaskNum);
                QuartzLogger.Error($"BuildRelocationCommand:移库任务终点地址解析失败,终点: {task.NextAddress}", task.Roadway);
                _taskService.UpdateTaskExceptionMessage(task.TaskNum, $"移库任务终点错误,终点:【{task.NextAddress}】");
                return null;
            }
@@ -298,6 +345,8 @@
            // è§£æžèµ·å§‹åœ°å€
            if (!TryParseAddress(task.CurrentAddress, out short startRow, out short startColumn, out short startLayer))
            {
                _logger.LogError("BuildRelocationCommand:移库任务起点地址解析失败,起点: {CurrentAddress},任务号: {TaskNum}", task.CurrentAddress, task.TaskNum);
                QuartzLogger.Error($"BuildRelocationCommand:移库任务起点地址解析失败,起点: {task.CurrentAddress}", task.Roadway);
                _taskService.UpdateTaskExceptionMessage(task.TaskNum, $"移库任务起点错误,起点:【{task.CurrentAddress}】");
                return null;
            }
@@ -307,6 +356,10 @@
            SetCommandProperty(command, "StartColumn", startColumn);
            SetCommandProperty(command, "StartLayer", startLayer);
            _logger.LogInformation("BuildRelocationCommand:移库命令构建成功,起点: {StartRow}-{StartColumn}-{StartLayer},终点: {EndRow}-{EndColumn}-{EndLayer},任务号: {TaskNum}",
                startRow, startColumn, startLayer, endRow, endColumn, endLayer, task.TaskNum);
            QuartzLogger.Info($"BuildRelocationCommand:移库命令构建成功,起点: {startRow}-{startColumn}-{startLayer},终点: {endRow}-{endColumn}-{endLayer}", task.Roadway);
            return command;
        }
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tasks/StackerCraneJob/StackerCraneTaskSelector.cs
@@ -1,9 +1,11 @@
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System.Diagnostics.CodeAnalysis;
using WIDESEA_Core;
using WIDESEAWCS_Common.HttpEnum;
using WIDESEAWCS_Common.TaskEnum;
using WIDESEAWCS_Core;
using WIDESEAWCS_Core.LogHelper;
using WIDESEAWCS_ITaskInfoService;
using WIDESEAWCS_Model.Models;
using WIDESEAWCS_QuartzJob;
@@ -40,6 +42,11 @@
        private readonly IRouterService _routerService;
        /// <summary>
        /// æ—¥å¿—记录器
        /// </summary>
        private readonly ILogger _logger;
        /// <summary>
        /// ç§»åº“检查委托函数
        /// </summary>
        /// <remarks>
@@ -53,8 +60,9 @@
        /// <param name="taskService">任务服务</param>
        /// <param name="routerService">路由服务</param>
        /// <param name="httpClientHelper">HTTP å®¢æˆ·ç«¯å¸®åŠ©ç±»</param>
        public StackerCraneTaskSelector(ITaskService taskService, IRouterService routerService, HttpClientHelper httpClientHelper)
            : this(taskService, routerService, taskNum => QueryTransferTask(httpClientHelper, taskNum))
        /// <param name="logger">日志记录器</param>
        public StackerCraneTaskSelector(ITaskService taskService, IRouterService routerService, HttpClientHelper httpClientHelper, ILogger logger)
            : this(taskService, routerService, taskNum => QueryTransferTask(httpClientHelper, taskNum), logger)
        {
        }
@@ -64,11 +72,13 @@
        /// <param name="taskService">任务服务</param>
        /// <param name="routerService">路由服务</param>
        /// <param name="transferCheck">移库检查函数</param>
        public StackerCraneTaskSelector(ITaskService taskService, IRouterService routerService, Func<int, Dt_Task?> transferCheck)
        /// <param name="logger">日志记录器</param>
        public StackerCraneTaskSelector(ITaskService taskService, IRouterService routerService, Func<int, Dt_Task?> transferCheck, ILogger logger)
        {
            _taskService = taskService;
            _routerService = routerService;
            _transferCheck = transferCheck;
            _logger = logger;
        }
        /// <summary>
@@ -88,35 +98,49 @@
        public Dt_Task? SelectTask(IStackerCrane commonStackerCrane)
        {
            Dt_Task? candidateTask;
            var deviceCode = commonStackerCrane.DeviceCode;
            _logger.LogInformation("SelectTask:开始选择任务,设备: {DeviceCode},上一任务类型: {LastTaskType}", deviceCode, commonStackerCrane.LastTaskType);
            QuartzLogger.Info($"开始选择任务,设备: {deviceCode},上一任务类型: {commonStackerCrane.LastTaskType}", deviceCode);
            // æ ¹æ®ä¸Šä¸€ä»»åŠ¡ç±»åž‹å†³å®šæŸ¥è¯¢ç­–ç•¥
            if (commonStackerCrane.LastTaskType == null)
            {
                // æ²¡æœ‰ä¸Šä¸€ä»»åŠ¡ç±»åž‹ï¼ŒæŸ¥è¯¢æ™®é€šä»»åŠ¡
                candidateTask = _taskService.QueryStackerCraneTask(commonStackerCrane.DeviceCode);
                candidateTask = _taskService.QueryStackerCraneTask(deviceCode);
                _logger.LogDebug("SelectTask:查询普通任务,设备: {DeviceCode},结果: {TaskNum}", deviceCode, candidateTask?.TaskNum);
                QuartzLogger.Debug($"查询普通任务,设备: {deviceCode},结果: {candidateTask?.TaskNum}", deviceCode);
            }
            else if (commonStackerCrane.LastTaskType.GetValueOrDefault().GetTaskTypeGroup() == TaskTypeGroup.OutbondGroup)
            {
                // ä¸Šä¸€ä»»åŠ¡æ˜¯å‡ºåº“ï¼Œä¼˜å…ˆæŸ¥å…¥åº“ä»»åŠ¡
                candidateTask = _taskService.QueryStackerCraneInTask(commonStackerCrane.DeviceCode);
                candidateTask = _taskService.QueryStackerCraneInTask(deviceCode);
                // å¦‚果没有入库任务,再查一下出库任务
                candidateTask ??= _taskService.QueryStackerCraneOutTask(commonStackerCrane.DeviceCode);
                candidateTask ??= _taskService.QueryStackerCraneOutTask(deviceCode);
                _logger.LogDebug("SelectTask:出库后优先查入库,设备: {DeviceCode},结果: {TaskNum}", deviceCode, candidateTask?.TaskNum);
                QuartzLogger.Debug($"出库后优先查入库,设备: {deviceCode},结果: {candidateTask?.TaskNum}", deviceCode);
            }
            else
            {
                // ä¸Šä¸€ä»»åŠ¡æ˜¯å…¥åº“ï¼ˆéžå‡ºåº“ï¼‰ï¼Œä¼˜å…ˆæŸ¥å‡ºåº“ä»»åŠ¡
                candidateTask = _taskService.QueryStackerCraneOutTask(commonStackerCrane.DeviceCode);
                candidateTask = _taskService.QueryStackerCraneOutTask(deviceCode);
                _logger.LogDebug("SelectTask:入库后优先查出库,设备: {DeviceCode},结果: {TaskNum}", deviceCode, candidateTask?.TaskNum);
                QuartzLogger.Debug($"入库后优先查出库,设备: {deviceCode},结果: {candidateTask?.TaskNum}", deviceCode);
            }
            // å¦‚果没有候选任务,返回 null
            if (candidateTask == null)
            {
                _logger.LogDebug("SelectTask:没有候选任务,设备: {DeviceCode}", deviceCode);
                QuartzLogger.Debug($"没有候选任务,设备: {deviceCode}", deviceCode);
                return null;
            }
            // å¦‚果不是出库任务,直接返回
            if (candidateTask.TaskType.GetTaskTypeGroup() != TaskTypeGroup.OutbondGroup)
            {
                _logger.LogInformation("SelectTask:选中非出库任务,设备: {DeviceCode},任务号: {TaskNum},任务类型: {TaskType}", deviceCode, candidateTask.TaskNum, candidateTask.TaskType);
                QuartzLogger.Info($"选中非出库任务,任务号: {candidateTask.TaskNum},任务类型: {candidateTask.TaskType}", deviceCode);
                return candidateTask;
            }
@@ -124,28 +148,35 @@
            Dt_Task? selectedTask = TrySelectOutboundTask(candidateTask);
            if (selectedTask != null)
            {
                _logger.LogInformation("SelectTask:选中出库任务,设备: {DeviceCode},任务号: {TaskNum}", deviceCode, selectedTask.TaskNum);
                QuartzLogger.Info($"选中出库任务,任务号: {selectedTask.TaskNum}", deviceCode);
                return selectedTask;
            }
            // æŸ¥æ‰¾å…¶ä»–可用的出库站台
            var otherOutStationCodes = _routerService
                .QueryNextRoutes(commonStackerCrane.DeviceCode, candidateTask.NextAddress, candidateTask.TaskType)
                .QueryNextRoutes(deviceCode, candidateTask.NextAddress, candidateTask.TaskType)
                .Select(x => x.ChildPosi)
                .ToList();
            // æŸ¥è¯¢å…¶ä»–站台的出库任务
            var tasks = _taskService.QueryStackerCraneOutTasks(commonStackerCrane.DeviceCode, otherOutStationCodes);
            var tasks = _taskService.QueryStackerCraneOutTasks(deviceCode, otherOutStationCodes);
            foreach (var alternativeTask in tasks)
            {
                selectedTask = TrySelectOutboundTask(alternativeTask);
                if (selectedTask != null)
                {
                    _logger.LogInformation("SelectTask:选中备选出库任务,设备: {DeviceCode},任务号: {TaskNum}", deviceCode, selectedTask.TaskNum);
                    QuartzLogger.Info($"选中备选出库任务,任务号: {selectedTask.TaskNum}", deviceCode);
                    return selectedTask;
                }
            }
            // æ²¡æœ‰å¯ç”¨å‡ºåº“任务,尝试返回入库任务
            return _taskService.QueryStackerCraneInTask(commonStackerCrane.DeviceCode);
            var inboundTask = _taskService.QueryStackerCraneInTask(deviceCode);
            _logger.LogInformation("SelectTask:返回入库任务,设备: {DeviceCode},任务号: {TaskNum}", deviceCode, inboundTask?.TaskNum);
            QuartzLogger.Info($"返回入库任务,任务号: {inboundTask?.TaskNum}", deviceCode);
            return inboundTask;
        }
        /// <summary>
@@ -267,6 +298,8 @@
            if (router == null)
            {
                // æœªæ‰¾åˆ°ç«™å°è·¯ç”±ä¿¡æ¯
                _logger.LogWarning("IsOutTaskStationAvailable:未找到站台路由信息,站台: {NextAddress},任务号: {TaskNum}", task.NextAddress, task.TaskNum);
                QuartzLogger.Warn($"IsOutTaskStationAvailable:未找到站台路由信息,站台: {task.NextAddress}", task.Roadway);
                _taskService.UpdateTaskExceptionMessage(task.TaskNum, $"未找到站台【{task.NextAddress}】信息,无法校验站台");
                return false;
            }
@@ -276,6 +309,8 @@
            if (device == null)
            {
                // æœªæ‰¾åˆ°è®¾å¤‡
                _logger.LogWarning("IsOutTaskStationAvailable:未找到出库站台对应的通讯对象,站台: {ChildPosiDeviceCode},任务号: {TaskNum}", router.ChildPosiDeviceCode, task.TaskNum);
                QuartzLogger.Warn($"IsOutTaskStationAvailable:未找到出库站台对应的通讯对象,站台: {router.ChildPosiDeviceCode}", task.Roadway);
                _taskService.UpdateTaskExceptionMessage(task.TaskNum, $"未找到出库站台【{router.ChildPosiDeviceCode}】对应的通讯对象,无法判断出库站台是否被占用");
                return false;
            }
@@ -284,7 +319,11 @@
            CommonConveyorLine conveyorLine = (CommonConveyorLine)device;
            // æ£€æŸ¥ç«™å°æ˜¯å¦è¢«å ç”¨
            return conveyorLine.IsOccupied(router.ChildPosi);
            bool isOccupied = conveyorLine.IsOccupied(router.ChildPosi);
            _logger.LogInformation("IsOutTaskStationAvailable:站台 {ChildPosi},是否被占用: {IsOccupied},任务号: {TaskNum}", router.ChildPosi, isOccupied, task.TaskNum);
            QuartzLogger.Info($"IsOutTaskStationAvailable:站台 {router.ChildPosi},是否被占用: {isOccupied}", task.Roadway);
            return !isOccupied;
        }
    }
}
Code/WCS/WIDESEAWCS_Server/WIDESEAWCS_Tests/StackerCraneTaskSelectorTests.cs
@@ -1,3 +1,4 @@
using Microsoft.Extensions.Logging;
using Moq;
using WIDESEAWCS_Common.TaskEnum;
using WIDESEAWCS_Core;
@@ -44,6 +45,7 @@
        taskService.Setup(x => x.AddData(It.IsAny<Dt_Task>())).Returns(WebResponseContent.Instance.OK());
        var transferCheckCalledCount = 0;
        var mockLogger = new Mock<ILogger>();
        var selector = new StackerCraneTaskSelector(
            taskService.Object,
            routerService.Object,
@@ -52,7 +54,8 @@
                transferCheckCalledCount++;
                Assert.Equal(1001, taskNum);
                return relocationTask;
            });
            },
            mockLogger.Object);
        var selectedTask = selector.SelectTask(stackerCrane.Object);
@@ -104,10 +107,12 @@
            .Setup(x => x.QueryNextRoutes("SC01", "OUT-01", (int)TaskOutboundTypeEnum.Outbound))
            .Returns(new List<WIDESEAWCS_QuartzJob.Models.Dt_Router>());
        var mockLogger = new Mock<ILogger>();
        var selector = new StackerCraneTaskSelector(
            taskService.Object,
            routerService.Object,
            _ => newOutboundTask);
            _ => newOutboundTask,
            mockLogger.Object);
        _ = selector.SelectTask(stackerCrane.Object);
Code/WCS/WIDESEAWCS_Server/docs/superpowers/plans/2026-03-27-router-cache.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,239 @@
# è·¯ç”±ç¼“存实现计划
> **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:** åœ¨ RouterService ä¸­å¼•å…¥ HybridCacheService æ··åˆç¼“存,第一次查询加载全量路由到缓存,后续查询直接从缓存读取,AddRouters å†™å…¥åŽåŒæ­¥æ›´æ–°ç¼“存。
**Architecture:** æŒ‰ InOutType åˆ†å¼€ç¼“存两份(入口/出口),GetOrAdd æ‡’加载,写穿更新缓存,单实例部署无广播需求。
**Tech Stack:** ASP.NET Core 6.0, HybridCacheService, SqlSugar ORM
---
## æ–‡ä»¶å˜æ›´æ¦‚览
**修改文件:**
- `WIDESEAWCS_QuartzJob/Service/RouterService.cs` â€” å…¨éƒ¨ç¼“存逻辑改动
**不变:**
- `FindRoutesInMemory` â€” åªæ”¹è°ƒç”¨æ–¹ï¼Œç®—法逻辑不变
- `QueryAllPositions` â€” å·²æœ‰ç‹¬ç«‹ç¼“存,不依赖本方案
---
## Task 1: æ–°å¢ž GetAllRoutersFromCache ç§æœ‰æ–¹æ³•
**文件:**
- Modify: `WIDESEAWCS_QuartzJob/Service/RouterService.cs`
在类成员变量声明区之后、现有方法之前,新增以下私有方法:
```csharp
/// <summary>
/// ä»Žç¼“存获取指定类型的全量路由数据,缓存不存在时自动从数据库加载并写入缓存
/// </summary>
/// <param name="routeType">路由类型(入口/出口)</param>
/// <returns>该类型的全部路由列表</returns>
private List<Dt_Router> GetAllRoutersFromCache(int routeType)
{
    string cacheKey = $"Router:AllRouters:{(routeType == RouterInOutType.In.ObjToInt() ? "In" : "Out")}";
    return _cacheService.GetOrAdd(
        cacheKey,
        () => BaseDal.QueryData(x => x.InOutType == routeType)
    );
}
---
## Task 2: æ”¹é€  QueryNextRoutes é‡è½½ï¼ˆ2个方法)
**文件:**
- Modify: `WIDESEAWCS_QuartzJob/Service/RouterService.cs:48-98`
### QueryNextRoutes(string, string) â€” ç¬¬48-69行
原代码:
```csharp
List<Dt_Router> allRouters = BaseDal.QueryData(x => true);
routers = FindRoutesInMemory(startPosi, endPosi, allRouters, null);
```
改为:
```csharp
List<Dt_Router> allRouters = GetAllRoutersFromCache(RouterInOutType.In.ObjToInt());
// å…¥å£+出口都加载,FindRoutesInMemory å†…部按 routeType==null ä¸åšè¿‡æ»¤
List<Dt_Router> outRouters = GetAllRoutersFromCache(RouterInOutType.Out.ObjToInt());
allRouters.AddRange(outRouters);
routers = FindRoutesInMemory(startPosi, endPosi, allRouters, null);
```
### QueryNextRoutes(string, string, int) â€” ç¬¬78-99行
原代码:
```csharp
List<Dt_Router> allRouters = BaseDal.QueryData(x => x.InOutType == routeType);
routers = FindRoutesInMemory(startPosi, endPosi, allRouters, routeType);
```
改为:
```csharp
List<Dt_Router> allRouters = GetAllRoutersFromCache(routeType);
routers = FindRoutesInMemory(startPosi, endPosi, allRouters, routeType);
```
---
## Task 3: æ”¹é€  QueryNextRoute é‡è½½ï¼ˆ4个方法)
**文件:**
- Modify: `WIDESEAWCS_QuartzJob/Service/RouterService.cs:189-314`
### QueryNextRoute(string) â€” ç¬¬189-205行
原代码:
```csharp
List<Dt_Router> routes = BaseDal.QueryData(x => x.StartPosi == startPosi, ...);
return routes.FirstOrDefault();
```
改为:
```csharp
List<Dt_Router> routes = GetAllRoutersFromCache(RouterInOutType.In.ObjToInt())
    .Where(x => x.StartPosi == startPosi)
    .ToList();
routes = routes.OrderByDescending(x => x.IsEnd).ToList();
return routes.FirstOrDefault();
```
### QueryNextRoute(string, int) â€” ç¬¬213-229行
原代码:
```csharp
List<Dt_Router> routes = BaseDal.QueryData(x => x.StartPosi == startPosi && x.InOutType == routeType, ...);
return routes.FirstOrDefault();
```
改为:
```csharp
List<Dt_Router> routes = GetAllRoutersFromCache(routeType)
    .Where(x => x.StartPosi == startPosi)
    .ToList();
routes = routes.OrderByDescending(x => x.IsEnd).ToList();
return routes.FirstOrDefault();
```
### QueryNextRoute(string, string, int) â€” ç¬¬238-272行
原代码(第255行):
```csharp
List<Dt_Router> allRouters = BaseDal.QueryData(x => x.InOutType == routeType);
```
改为:
```csharp
List<Dt_Router> allRouters = GetAllRoutersFromCache(routeType);
```
其余直接路由判断和 `FindRoutesInMemory` è°ƒç”¨é€»è¾‘不变。
### QueryNextRoute(string, string) â€” ç¬¬280-314行
原代码(第285行):
```csharp
List<Dt_Router> routes = BaseDal.QueryData(x => x.StartPosi == startPosi, ...);
```
改为(合并入口+出口两份缓存,与原方法全量查询语义一致):
```csharp
List<Dt_Router> inRoutes = GetAllRoutersFromCache(RouterInOutType.In.ObjToInt());
List<Dt_Router> outRoutes = GetAllRoutersFromCache(RouterInOutType.Out.ObjToInt());
List<Dt_Router> routes = inRoutes.Concat(outRoutes)
    .Where(x => x.StartPosi == startPosi)
    .ToList();
routes = routes.OrderByDescending(x => x.IsEnd).ToList();
```
然后复用已有的直接路由判断逻辑。
---
## Task 4: æ”¹é€  QueryRoutePath
**文件:**
- Modify: `WIDESEAWCS_QuartzJob/Service/RouterService.cs:323-366`
第355行附近,原代码中 `while` å¾ªçŽ¯é‡Œæ¯æ¬¡è°ƒç”¨ `QueryNextRoute` çš„逻辑**不需要改动**(因为 `QueryNextRoute` å·²ç»åœ¨ Task3 ä¸­æ”¹é€ äº†ï¼‰ã€‚本任务的改动仅确认 `QueryRoutePath` æ–¹æ³•本身不再直接调用 `BaseDal.QueryData`,即不需要任何改动 â€”— å®ƒè°ƒç”¨çš„æ˜¯ `QueryNextRoute`,而后者已被改造。
**验证:** ç¡®è®¤ `QueryRoutePath` æ–¹æ³•体内没有直接调用 `BaseDal.QueryData`,只有对 `QueryNextRoute` çš„调用。
---
## Task 5: æ”¹é€  GetAllWholeRouters
**文件:**
- Modify: `WIDESEAWCS_QuartzJob/Service/RouterService.cs:412-446`
原代码:
```csharp
List<Dt_Router> allRouters = BaseDal.QueryData(x => true);
```
改为:
```csharp
List<Dt_Router> inRouters = GetAllRoutersFromCache(RouterInOutType.In.ObjToInt());
List<Dt_Router> outRouters = GetAllRoutersFromCache(RouterInOutType.Out.ObjToInt());
List<Dt_Router> allRouters = inRouters.Concat(outRouters).ToList();
```
后续遍历 `dt_Routers`(取 `IsEnd == true` çš„路由)的逻辑不变。
---
## Task 6: æ”¹é€  AddRouters,写入 DB åŽæ›´æ–°ç¼“å­˜
**文件:**
- Modify: `WIDESEAWCS_QuartzJob/Service/RouterService.cs:511-594`
在第568行附近,找到:
```csharp
// æŸ¥è¯¢æ•°æ®åº“中已有的路由信息
List<Dt_Router> dt_Routers = BaseDal.QueryData(x => x.InOutType == routerType);
```
这个 `dt_Routers` æ˜¯**添加之前**的已有数据,不包含本次新增的路由。在第586行 `BaseDal.AddData(routers)` **之后**、`content = WebResponseContent.Instance.OK()` **之前**,必须重新查询一次全量数据再写入缓存:
```csharp
// æ·»åŠ æ–°çš„è·¯ç”±ä¿¡æ¯
BaseDal.AddData(routers);
// é‡æ–°æŸ¥è¯¢å…¨é‡è·¯ç”±ï¼ˆæ­¤æ—¶æ‰åŒ…含新增的路由),再写入缓存
List<Dt_Router> updatedRouters = BaseDal.QueryData(x => x.InOutType == routerType);
string cacheKey = $"Router:AllRouters:{(routerType == RouterInOutType.In.ObjToInt() ? "In" : "Out")}";
_cacheService.Set(cacheKey, updatedRouters);
content = WebResponseContent.Instance.OK();
```
---
## Task 7: ç¼–译验证
**验证命令:**
```bash
dotnet build WIDESEAWCS_Server/WIDESEAWCS_QuartzJob/WIDESEAWCS_QuartzJob.csproj
```
预期:无编译错误(CS0103/CS0019 ç­‰ç±»åž‹/语法错误全部修复后)
---
## å®žæ–½é¡ºåº
1. Task 1 â€” æ–°å¢ž `GetAllRoutersFromCache` ç§æœ‰æ–¹æ³•
2. Task 2 â€” æ”¹é€  `QueryNextRoutes` ä¸¤ä¸ªé‡è½½
3. Task 3 â€” æ”¹é€  `QueryNextRoute` å››ä¸ªé‡è½½
4. Task 4 â€” ç¡®è®¤ `QueryRoutePath` æ— éœ€æ”¹åŠ¨ï¼ˆå®ƒä¾èµ–å·²æ”¹é€ çš„ `QueryNextRoute`)
5. Task 5 â€” æ”¹é€  `GetAllWholeRouters`
6. Task 6 â€” æ”¹é€  `AddRouters` æ›´æ–°ç¼“å­˜
7. Task 7 â€” ç¼–译验证
Code/WCS/WIDESEAWCS_Server/docs/superpowers/plans/2026-03-27-router-service-audit.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,324 @@
# RouterService é€»è¾‘修复与新方法实现计划
> **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:** ä¿®å¤ RouterService ä¸­çš„ 3 ä¸ªé€»è¾‘问题,新增 7 ä¸ªè·¯ç”±æŸ¥è¯¢/管理方法,并更新接口层。
**Architecture:** åœ¨çŽ°æœ‰ RouterService åŸºç¡€ä¸Šæ–°å¢žç¼“存管理方法和路由查询增强方法,保持现有 HybridCacheService ç¼“存架构不变。
**Tech Stack:** ASP.NET Core 6.0, HybridCacheService, SqlSugar ORM
---
## æ–‡ä»¶å˜æ›´æ¦‚览
**修改文件:**
- `WIDESEAWCS_QuartzJob/Service/RouterService.cs` â€” é€»è¾‘修复 + æ–°å¢žæ–¹æ³•实现
- `WIDESEAWCS_QuartzJob/Service/IRouterService.cs` â€” æŽ¥å£æ–°å¢žæ–¹æ³•签名 + åŽ»æŽ‰å¤šä½™ `public` ä¿®é¥°ç¬¦
---
## Task 1: ä¿®å¤ AddRouters ç¼“存一致性问题
**文件:**
- Modify: `WIDESEAWCS_QuartzJob/Service/RouterService.cs:605-613`
**原代码:**
```csharp
BaseDal.AddData(routers);
List<Dt_Router> updatedRouters = BaseDal.QueryData(x => x.InOutType == routerType);
string cacheKey = $"Router:AllRouters:{(routerType == RouterInOutType.In.ObjToInt() ? "In" : "Out")}";
_cacheService.AddOrUpdate(cacheKey, updatedRouters);
content = WebResponseContent.Instance.OK();
```
**改为:**
```csharp
// æ·»åŠ æ–°çš„è·¯ç”±ä¿¡æ¯
BaseDal.AddData(routers);
// é‡æ–°æŸ¥è¯¢å…¨é‡è·¯ç”±ï¼ˆæ­¤æ—¶æ‰åŒ…含新增的路由),再写入缓存
List<Dt_Router> updatedRouters = BaseDal.QueryData(x => x.InOutType == routerType);
string cacheKey = $"Router:AllRouters:{(routerType == RouterInOutType.In.ObjToInt() ? "In" : "Out")}";
try
{
    _cacheService.AddOrUpdate(cacheKey, updatedRouters);
}
catch
{
    // ç¼“存更新失败时静默忽略,下次查询会从DB自动重建缓存
}
content = WebResponseContent.Instance.OK();
```
**说明**:用 try-catch åŒ…裹缓存更新操作,防止缓存写入失败时异常逃逸到外层被当作错误返回。DB å·²å†™å…¥æˆåŠŸï¼Œç¼“å­˜å¤±è´¥ä¸å½±å“ä¸šåŠ¡æ­£ç¡®æ€§ã€‚
---
## Task 2: ä¿®å¤ QueryAllPositions ç©º catch å—
**文件:**
- Modify: `WIDESEAWCS_QuartzJob/Service/RouterService.cs:412-415`
**原代码:**
```csharp
catch
{
}
```
**改为:**
```csharp
catch (Exception ex)
{
    ConsoleHelper.WriteErrorLine($"RouterService.QueryAllPositions æŸ¥è¯¢å¤±è´¥: {ex.Message}");
}
```
**说明**:增加错误日志记录,便于排查问题。返回值仍为空列表,调用方行为不变。
---
## Task 3: ä¿®å¤ IRouterService æŽ¥å£ public ä¿®é¥°ç¬¦ + æ–°å¢žæ–¹æ³•签名
**文件:**
- Modify: `WIDESEAWCS_QuartzJob/Service/IRouterService.cs`
去掉以下行的 `public` ä¿®é¥°ç¬¦ï¼š
- ç¬¬40行:`public Dt_Router QueryNextRoute(string startPosi);` â†’ `Dt_Router QueryNextRoute(string startPosi);`
- ç¬¬48行
- ç¬¬57行
- ç¬¬65行
- ç¬¬74行
同时在接口末尾新增以下方法签名:
```csharp
/// <summary>
/// æ¸…除路由缓存
/// </summary>
void ClearRouterCache();
/// <summary>
/// æ ¹æ®è®¾å¤‡ç¼–号查询经过该设备的所有路由
/// </summary>
List<Dt_Router> QueryRoutersByDeviceCode(string deviceCode);
/// <summary>
/// åˆ¤æ–­ä¸¤ç‚¹ä¹‹é—´æ˜¯å¦å­˜åœ¨è·¯ç”±ï¼ˆå…¨ç±»åž‹ï¼‰
/// </summary>
bool ExistsRouter(string startPosi, string endPosi);
/// <summary>
/// åˆ¤æ–­ä¸¤ç‚¹ä¹‹é—´æ˜¯å¦å­˜åœ¨æŒ‡å®šç±»åž‹çš„路由
/// </summary>
bool ExistsRouter(string startPosi, string endPosi, int routeType);
/// <summary>
/// èŽ·å–å…¨é‡è·¯ç”±æ•°é‡ï¼ˆåˆå¹¶å…¥å£+出口)
/// </summary>
int GetRouterCount();
/// <summary>
/// èŽ·å–æŒ‡å®šç±»åž‹è·¯ç”±æ•°é‡
/// </summary>
int GetRouterCount(int routeType);
/// <summary>
/// æ‰¹é‡åˆ é™¤è·¯ç”±
/// </summary>
WebResponseContent DeleteRouters(List<long> routerIds);
```
---
## Task 4: å®žçް ClearRouterCache()
**文件:**
- Modify: `WIDESEAWCS_QuartzJob/Service/RouterService.cs`
在 `GetAllRoutersFromCache` æ–¹æ³•之后新增:
```csharp
/// <summary>
/// æ¸…除所有路由缓存(入口和出口类型)
/// </summary>
public void ClearRouterCache()
{
    _cacheService.Remove("Router:AllRouters:In");
    _cacheService.Remove("Router:AllRouters:Out");
}
```
---
## Task 5: å®žçް QueryRoutersByDeviceCode
**文件:**
- Modify: `WIDESEAWCS_QuartzJob/Service/RouterService.cs`
在 `ClearRouterCache` ä¹‹åŽæ–°å¢žï¼š
```csharp
/// <summary>
/// æ ¹æ®è®¾å¤‡ç¼–号查询经过该设备的所有路由(合并入口+出口类型)
/// </summary>
/// <param name="deviceCode">设备编号</param>
/// <returns>经过该设备的路由列表</returns>
public List<Dt_Router> QueryRoutersByDeviceCode(string deviceCode)
{
    List<Dt_Router> inRouters = GetAllRoutersFromCache(RouterInOutType.In.ObjToInt());
    List<Dt_Router> outRouters = GetAllRoutersFromCache(RouterInOutType.Out.ObjToInt());
    return inRouters.Concat(outRouters)
        .Where(x => x.ChildPosiDeviceCode == deviceCode)
        .ToList();
}
```
---
## Task 6: å®žçް ExistsRouter(两个重载)
**文件:**
- Modify: `WIDESEAWCS_QuartzJob/Service/RouterService.cs`
在 `QueryRoutersByDeviceCode` ä¹‹åŽæ–°å¢žï¼š
```csharp
/// <summary>
/// åˆ¤æ–­ä¸¤ç‚¹ä¹‹é—´æ˜¯å¦å­˜åœ¨è·¯ç”±ï¼ˆå…¨ç±»åž‹ï¼‰
/// </summary>
public bool ExistsRouter(string startPosi, string endPosi)
{
    List<Dt_Router> inRouters = GetAllRoutersFromCache(RouterInOutType.In.ObjToInt());
    List<Dt_Router> outRouters = GetAllRoutersFromCache(RouterInOutType.Out.ObjToInt());
    var allRouters = inRouters.Concat(outRouters).ToList();
    var routes = FindRoutesInMemory(startPosi, endPosi, allRouters, null);
    return routes.Count > 0;
}
/// <summary>
/// åˆ¤æ–­ä¸¤ç‚¹ä¹‹é—´æ˜¯å¦å­˜åœ¨æŒ‡å®šç±»åž‹çš„路由
/// </summary>
public bool ExistsRouter(string startPosi, string endPosi, int routeType)
{
    List<Dt_Router> allRouters = GetAllRoutersFromCache(routeType);
    var routes = FindRoutesInMemory(startPosi, endPosi, allRouters, routeType);
    return routes.Count > 0;
}
```
---
## Task 7: å®žçް GetRouterCount(两个重载)
**文件:**
- Modify: `WIDESEAWCS_QuartzJob/Service/RouterService.cs`
在 `ExistsRouter` ä¹‹åŽæ–°å¢žï¼š
```csharp
/// <summary>
/// èŽ·å–å…¨é‡è·¯ç”±æ•°é‡ï¼ˆå…¥å£+出口合计)
/// </summary>
public int GetRouterCount()
{
    int inCount = GetAllRoutersFromCache(RouterInOutType.In.ObjToInt()).Count;
    int outCount = GetAllRoutersFromCache(RouterInOutType.Out.ObjToInt()).Count;
    return inCount + outCount;
}
/// <summary>
/// èŽ·å–æŒ‡å®šç±»åž‹è·¯ç”±æ•°é‡
/// </summary>
public int GetRouterCount(int routeType)
{
    return GetAllRoutersFromCache(routeType).Count;
}
```
---
## Task 8: å®žçް DeleteRouters
**文件:**
- Modify: `WIDESEAWCS_QuartzJob/Service/RouterService.cs`
在 `AddRouters` æ–¹æ³•之后新增:
```csharp
/// <summary>
/// æ‰¹é‡åˆ é™¤æŒ‡å®šID的路由,删除后同步清除对应类型的缓存
/// </summary>
/// <param name="routerIds">待删除的路由ID列表</param>
/// <returns>返回处理结果</returns>
public WebResponseContent DeleteRouters(List<long> routerIds)
{
    WebResponseContent content = new WebResponseContent();
    try
    {
        if (routerIds == null || routerIds.Count == 0)
        {
            return content = WebResponseContent.Instance.Error("待删除的路由ID列表不能为空");
        }
        // æŸ¥è¯¢å¾…删除路由的类型(用于后续清除缓存)
        var routersToDelete = BaseDal.QueryData(x => routerIds.Contains(x.Id));
        if (routersToDelete.Count == 0)
        {
            return content = WebResponseContent.Instance.Error("未找到待删除的路由");
        }
        // è®°å½•涉及的类型(去重)
        var affectedTypes = routersToDelete.Select(x => x.InOutType).Distinct().ToList();
        // æ‰§è¡Œæ‰¹é‡åˆ é™¤
        BaseDal.DeleteData(routersToDelete);
        // æ¸…除受影响类型的缓存
        foreach (var routeType in affectedTypes)
        {
            string cacheKey = $"Router:AllRouters:{(routeType == RouterInOutType.In.ObjToInt() ? "In" : "Out")}";
            _cacheService.Remove(cacheKey);
        }
        content = WebResponseContent.Instance.OK();
    }
    catch (Exception ex)
    {
        content = WebResponseContent.Instance.Error(ex.Message);
    }
    return content;
}
```
---
## Task 9: ç¼–译验证
**验证命令:**
```bash
dotnet build WIDESEAWCS_Server/WIDESEAWCS_QuartzJob/WIDESEAWCS_QuartzJob.csproj
```
预期:0 errors
---
## å®žæ–½é¡ºåº
1. Task 1 â€” ä¿®å¤ AddRouters ç¼“存一致性
2. Task 2 â€” ä¿®å¤ QueryAllPositions ç©º catch å—
3. Task 3 â€” ä¿®å¤ IRouterService æŽ¥å£ï¼ˆåŽ»æŽ‰ public + æ–°å¢žç­¾åï¼‰
4. Task 4 â€” å®žçް ClearRouterCache
5. Task 5 â€” å®žçް QueryRoutersByDeviceCode
6. Task 6 â€” å®žçް ExistsRouter ä¸¤ä¸ªé‡è½½
7. Task 7 â€” å®žçް GetRouterCount ä¸¤ä¸ªé‡è½½
8. Task 8 â€” å®žçް DeleteRouters
9. Task 9 â€” ç¼–译验证
Code/WCS/WIDESEAWCS_Server/docs/superpowers/specs/2026-03-27-router-service-audit-design.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,104 @@
# RouterService é€»è¾‘修复与新方法设计
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:writing-plans to create implementation plan.
**Goal:** ä¿®å¤ `RouterService` ä¸­å·²å‘现的逻辑错误,新增 5 ä¸ªè·¯ç”±æŸ¥è¯¢/管理方法,并更新接口层。
**Architecture:** åœ¨çŽ°æœ‰ `RouterService` åŸºç¡€ä¸Šæ–°å¢žç¼“存管理方法和路由查询增强方法,保持现有缓存架构不变。
**Tech Stack:** ASP.NET Core 6.0, HybridCacheService, SqlSugar ORM
---
## ä¸€ã€çŽ°æœ‰é€»è¾‘é—®é¢˜ä¿®å¤
### 1.1 AddRouters ç¼“存一致性
**问题**:`BaseDal.AddData` å’Œ `_cacheService.AddOrUpdate` ä¹‹é—´æ— ä¿æŠ¤ã€‚如果 `AddOrUpdate` æŠ›å¼‚常,DB å·²å†™å…¥ä½†ç¼“存未更新,下次查询读到旧数据。
**修复**:使用 try-finally é€»è¾‘,保证 DB å†™å…¥åŽå†æ›´æ–°ç¼“存。如果缓存更新失败,下次查询会从 DB è‡ªåŠ¨é‡å»ºç¼“å­˜ï¼ˆ`GetOrAdd` æ‡’加载保证)。
### 1.2 QueryAllPositions ç©º catch å—
**问题**:catch å—仅有注释,无实际日志记录。异常被静默吞掉,调用方无法区分"没有数据"和"查询失败"。
**修复**:增加 `ConsoleHelper.WriteErrorLine` è®°å½•错误。
### 1.3 æŽ¥å£å±‚ public ä¿®é¥°ç¬¦
**问题**:`IRouterService` ä¸­çš„æ–¹æ³•签名有多余的 `public` ä¿®é¥°ç¬¦ã€‚
**修复**:移除接口方法上的 `public` ä¿®é¥°ç¬¦ã€‚
---
## äºŒã€æ–°å¢žæ–¹æ³•设计
### 2.1 ClearRouterCache()
清除 `Router:AllRouters:In` å’Œ `Router:AllRouters:Out` ä¸¤ä¸ªç¼“存键。
**签名**:`void ClearRouterCache()`
**实现**:
```csharp
_cacheService.Remove("Router:AllRouters:In");
_cacheService.Remove("Router:AllRouters:Out");
```
### 2.2 QueryRoutersByDeviceCode(string deviceCode)
查询经过指定设备的所有路由(合并入口/出口类型)。
**签名**:`List<Dt_Router> QueryRoutersByDeviceCode(string deviceCode)`
**逻辑**:从缓存加载 In + Out è·¯ç”±ï¼Œç­›é€‰ `ChildPosiDeviceCode == deviceCode` çš„路由。
**缓存**:不独占缓存,直接使用现有 `GetAllRoutersFromCache` èŽ·å–æ•°æ®ã€‚
### 2.3 ExistsRouter(两个重载)
判断两点之间是否存在路由。
**签名**:
- `bool ExistsRouter(string startPosi, string endPosi)` â€” å…¨ç±»åž‹
- `bool ExistsRouter(string startPosi, string endPosi, int routeType)` â€” æŒ‡å®šç±»åž‹
**逻辑**:调用 `FindRoutesInMemory`,返回 `routers.Count > 0`。
### 2.4 GetRouterCount(两个重载)
返回路由数量。
**签名**:
- `int GetRouterCount()` â€” åˆå¹¶ In + Out æ€»æ•°
- `int GetRouterCount(int routeType)` â€” æŒ‡å®šç±»åž‹æ•°é‡
**实现**:从缓存获取后 `.Count`。
### 2.5 DeleteRouters(List<long> routerIds)
批量删除指定 ID çš„路由,删除后同步清除对应类型缓存。
**签名**:`WebResponseContent DeleteRouters(List<long> routerIds)`
**逻辑**:
1. æŸ¥å‡ºå¾…删除路由各自的 `InOutType`
2. `BaseDal.DeleteData` æ‰¹é‡åˆ é™¤
3. æ ¹æ®æ¶‰åŠçš„ `InOutType` æ¸…除对应缓存键
4. è¿”回 `WebResponseContent`
---
## ä¸‰ã€æ¶‰åŠæ–‡ä»¶
| æ–‡ä»¶ | æ”¹åЍ |
|------|------|
| `WIDESEAWCS_QuartzJob/Service/RouterService.cs` | ä¿®å¤é€»è¾‘ + æ–°å¢žæ–¹æ³•实现 |
| `WIDESEAWCS_QuartzJob/Service/IRouterService.cs` | æŽ¥å£æ–°å¢žæ–¹æ³•签名,去掉多余 `public` |
---
## å››ã€ç¼“存键汇总
| é”® | ç”¨é€” |
|----|------|
| `Router:AllRouters:In` | å…¥å£ç±»åž‹å…¨é‡è·¯ç”± |
| `Router:AllRouters:Out` | å‡ºå£ç±»åž‹å…¨é‡è·¯ç”± |
| `System:DevicePositions:{deviceCode}` | `QueryAllPositions` å·²æœ‰ç¼“存,不在本方案范围内 |
Code/WCS/docs(superpowers)/specs/2026-03-27-task-logging-design.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,167 @@
# ä»»åŠ¡æ—¥å¿—å¢žå¼ºè®¾è®¡æ–‡æ¡£
## æ¦‚è¿°
为 `RobotJob`、`ConveyorLineNewJob`、`StackerCraneJob` ä¸‰ä¸ªæ¨¡å—添加完善的日志记录功能。
## æ—¥å¿—规范
### æ—¥å¿—级别定义(严格按语义)
| çº§åˆ« | ç”¨é€” | ç¤ºä¾‹åœºæ™¯ |
|------|------|----------|
| `Info` | æ­£å¸¸æµç¨‹èŠ‚ç‚¹ | ä»»åŠ¡å¼€å§‹å¤„ç†ã€å‘½ä»¤ä¸‹å‘æˆåŠŸã€çŠ¶æ€è½¬æ¢å®Œæˆ |
| `Warn` | éœ€è¦å…³æ³¨ä½†éžé”™è¯¯ | å®¢æˆ·ç«¯æ–­è¿žé‡è¯•、站台不可用、版本冲突 |
| `Error` | å¼‚常/失败 | WMS è°ƒç”¨å¤±è´¥ã€å‘½ä»¤å‘送失败、任务查询失败 |
| `Debug` | è¯¦ç»†ä¿¡æ¯ | è½®è¯¢æ“ä½œã€ç¼“存读取(可选) |
### æ—¥å¿—输出方式
1. **ILogger<T>** - é€šè¿‡ä¾èµ–注入或父类传递
2. **QuartzLogger** - å¼‚步文件日志,同时记录
```csharp
// æ ‡å‡†æ—¥å¿—写法
_logger.LogInformation("消息内容");
QuartzLogger.Info("消息内容", source);
```
## ä¿®æ”¹æ¸…单
### RobotJob æ¨¡å—
#### RobotJob.cs
- çŠ¶æ€ï¼šå·²æœ‰ ILogger,无需修改
#### RobotClientManager.cs
- æ·»åŠ  `ILogger<RobotClientManager>`
- ä¿®æ­£æ–­è¿žæ—¥å¿—:Warn â†’ Info(客户端断开是正常流程)
- æ·»åŠ  EnsureClientSubscribed ä¸­çš„重试/不可用日志
#### RobotTaskProcessor.cs
- æ·»åŠ  `ILogger<RobotTaskProcessor>`
- ä¿®æ­£ SendSocketRobotPickAsync:Error â†’ Info(成功下发应记录 Info)
- æ·»åŠ  HandleInboundTaskAsync çš„ WMS è°ƒç”¨ç»“果日志
#### RobotStateManager.cs
- æ·»åŠ  `ILogger<RobotStateManager>`
- æž„造函数的 GetOrCreateState æ·»åŠ  Info æ—¥å¿—
- TryUpdateStateSafely æ·»åŠ ç‰ˆæœ¬å†²çª Warn æ—¥å¿—
#### RobotMessageHandler.cs
- å·²æœ‰ ILogger,已有良好日志,保持不变
#### RobotWorkflowOrchestrator.cs
- æ·»åŠ  `ILogger<RobotWorkflowOrchestrator>`
- ExecuteAsync æ·»åŠ çŠ¶æ€æœºå†³ç­–æ—¥å¿—ï¼ˆæ»¡è¶³æ¡ä»¶æ—¶è®°å½• Info)
- HandlePickFinishedStateAsync æ·»åŠ æ”¾è´§æŒ‡ä»¤ä¸‹å‘æ—¥å¿—
- HandlePutFinishedStateAsync æ·»åŠ å–è´§æŒ‡ä»¤ä¸‹å‘æ—¥å¿—
### ConveyorLineNewJob æ¨¡å—
#### CommonConveyorLineNewJob.cs
- æ·»åŠ  `ILogger<CommonConveyorLineNewJob>`
- Execute æ–¹æ³•:
  - å­è®¾å¤‡æ•°é‡ä¸º 0:Info
  - Parallel.For å¼€å§‹ï¼šDebug
  - å‘½ä»¤ä¸ºç©ºè·³è¿‡ï¼šDebug
  - WCS_ACK å¤„理:Debug
  - æ£€æŸ¥æ‰˜ç›˜ä½ç½®ï¼šInfo
  - PLC_STB æ£€æŸ¥ï¼šDebug
  - æ— æ¡ç è¯·æ±‚出库:Info
  - æœ‰ä»»åŠ¡å·å¤„ç†ä»»åŠ¡ï¼šInfo
  - å¼‚常捕获:Error
- ProcessTaskState æ–¹æ³•:添加各状态分支的入口日志
#### ConveyorLineDispatchHandler.cs
- æ·»åŠ  `ILogger<ConveyorLineDispatchHandler>`
- HeartBeat:Debug
- RequestInbound:Info(入库请求开始)
- RequestInNextAddress:Info(入库下一地址)
- ConveyorLineInFinish:Info(入库完成)
- RequestOutbound:Info(出库请求)
- RequestOutNextAddress:Info(出库下一地址)
- ConveyorLineOutFinish:Info(出库完成)
#### ConveyorLineTaskFilter.cs
- æ·»åŠ  `ILogger<ConveyorLineTaskFilter>`
- QueryPendingTask:Debug
- QueryExecutingTask:Debug
- RequestWmsTask:Info(WMS è¯·æ±‚)
#### ConveyorLineTargetAddressSelector.cs
- æ·»åŠ  `ILogger<ConveyorLineTargetAddressSelector>`
- HandleInboundNextAddress:Debug
- HandleOutboundNextAddress:Debug
- HandleDeviceRequest:Debug
- ProcessDeviceRequest:Debug
### StackerCraneJob æ¨¡å—
#### CommonStackerCraneJob.cs
- æ·»åŠ  `ILogger<CommonStackerCraneJob>`
- Execute æ–¹æ³•:
  - å‚数无效:Warn
  - äº‹ä»¶è®¢é˜…:Info
  - ä»»åŠ¡å®Œæˆæ£€æŸ¥ï¼šDebug
  - ä¸å¯å‘送任务:Debug
  - ä»»åŠ¡é€‰æ‹©ç»“æžœï¼šInfo(选中任务号或无任务)
  - å‘½ä»¤æž„建结果:Info
  - å‘½ä»¤å‘送结果:Info
  - å¼‚常捕获:Error
- CommonStackerCrane_StackerCraneTaskCompletedEventHandler:Info
- LoadConfig å¤±è´¥ï¼šWarn
#### StackerCraneTaskSelector.cs
- æ·»åŠ  `ILogger<StackerCraneTaskSelector>`
- SelectTask:Info(任务选择开始、选择结果)
- TrySelectOutboundTask:Debug
- IsOutTaskStationAvailable:Info(站台可用/不可用)
- TryAddTaskFromWms:Info
#### StackerCraneCommandBuilder.cs
- æ·»åŠ  `ILogger<StackerCraneCommandBuilder>`
- ConvertToStackerCraneTaskCommand:Info(命令类型、任务号)
- GetCommandType:Debug
- BuildInboundCommand:Info(入库命令构建)
- BuildOutboundCommand:Info(出库命令构建)
- BuildRelocationCommand:Info(移库命令构建)
- åœ°å€è§£æžå¤±è´¥ï¼šError
## ILogger ä¾èµ–传递方案
对于通过 `new` ç›´æŽ¥å®žä¾‹åŒ–的辅助类,通过父类构造函数传入 ILogger:
```csharp
// è¾…助类接收 ILogger
public class RobotStateManager
{
    private readonly ILogger _logger;
    public RobotStateManager(ICacheService cache, ILogger<RobotStateManager> logger)
    {
        _logger = logger;
    }
}
// Job åœ¨åˆ›å»ºè¾…助类时传入自己的 logger
public RobotJob(..., ILogger<RobotJob> logger)
{
    _stateManager = new RobotStateManager(cache, logger);
}
```
## éœ€ä¿®æ­£çš„æ—¥å¿—语义问题
| ä½ç½® | åŽŸå†™æ³• | ä¿®æ­£åŽ |
|------|--------|--------|
| RobotTaskProcessor.SendSocketRobotPickAsync | QuartzLogger.Error (成功时) | QuartzLogger.Info |
| RobotClientManager.EnsureClientSubscribed | QuartzLogger.Info (异常时) | QuartzLogger.Error |
| RobotClientManager.OnRobotReceived | QuartzLogger.Warn (断连) | QuartzLogger.Info |
## éªŒæ”¶æ ‡å‡†
1. æ‰€æœ‰å…³é”®ä¸šåŠ¡èŠ‚ç‚¹æœ‰ Info æ—¥å¿—
2. å¼‚常情况有 Error æ—¥å¿—并包含异常信息
3. éœ€è¦å…³æ³¨çš„æƒ…况有 Warn æ—¥å¿—
4. æ—¥å¿—同时输出到 ILogger å’Œ QuartzLogger
5. æ—¥å¿—消息清晰,包含关键上下文(如任务号、设备编码)
Code/WMS/WIDESEA_WMSServer/.vs/WIDESEA_WMSServer/v18/DocumentLayout.backup.json
ÎļþÒÑɾ³ý
Code/WMS/WIDESEA_WMSServer/.vs/WIDESEA_WMSServer/v18/DocumentLayout.json
ÎļþÒÑɾ³ý
Code/WMS/WIDESEA_WMSServer/WIDESEA_BasicService/MesService.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,77 @@
using Newtonsoft.Json;
using System.Collections.Generic;
using WIDESEA_Core;
using WIDESEA_Core.Helper;
using WIDESEA_IBasicService;
using WIDESEA_DTO.MES;
namespace WIDESEA_BasicService
{
    /// <summary>
    /// MES服务实现 - é™•西顷刻能源科技MES系统对接
    /// </summary>
    public class MesService : IMesService
    {
        private readonly HttpClientHelper _httpClient;
        private readonly string _baseUrl;
        private readonly string _authorization;
        private const string BindContainerPath = "/EquipmentService/api/v1/BindContainer";
        private const string UnBindContainerPath = "/EquipmentService/api/v1/UnBindContainer";
        private const string ContainerNgReportPath = "/EquipmentService/api/v1/ContainerNgReport";
        private const string InboundInContainerPath = "/EquipmentService/api/v1/InboundInContainer";
        private const string OutboundInContainerPath = "/EquipmentService/api/v1/OutboundInContainer";
        public MesService(HttpClientHelper httpClient)
        {
            _httpClient = httpClient;
            _baseUrl = AppSettings.Get("MES:BaseUrl").TrimEnd('/');
            _authorization = AppSettings.Get("MES:Authorization");
        }
        private HttpRequestConfig BuildConfig()
        {
            return new HttpRequestConfig
            {
                Headers = new Dictionary<string, string>
                {
                    { "Authorization", _authorization }
                },
                TimeoutMs = 30000,
                MaxRetryCount = 0,
                EnableLogging = true
            };
        }
        private HttpResponseResult<MesResponse> Post<T>(string url, T request)
        {
            string json = JsonConvert.SerializeObject(request);
            return _httpClient.Post<MesResponse>(url, json, "application/json", BuildConfig());
        }
        public HttpResponseResult<MesResponse> BindContainer(BindContainerRequest request)
        {
            return Post(_baseUrl + BindContainerPath, request);
        }
        public HttpResponseResult<MesResponse> UnBindContainer(UnBindContainerRequest request)
        {
            return Post(_baseUrl + UnBindContainerPath, request);
        }
        public HttpResponseResult<MesResponse> ContainerNgReport(ContainerNgReportRequest request)
        {
            return Post(_baseUrl + ContainerNgReportPath, request);
        }
        public HttpResponseResult<MesResponse> InboundInContainer(InboundInContainerRequest request)
        {
            return Post(_baseUrl + InboundInContainerPath, request);
        }
        public HttpResponseResult<MesResponse> OutboundInContainer(OutboundInContainerRequest request)
        {
            return Post(_baseUrl + OutboundInContainerPath, request);
        }
    }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_BasicService/WIDESEA_BasicService.csproj
@@ -9,6 +9,7 @@
  <ItemGroup>
    <ProjectReference Include="..\WIDESEA_IBasicService\WIDESEA_IBasicService.csproj" />
    <ProjectReference Include="..\WIDESEA_IRecordService\WIDESEA_IRecordService.csproj" />
    <ProjectReference Include="..\WIDESEA_Core\WIDESEA_Core.csproj" />
  </ItemGroup>
</Project>
Code/WMS/WIDESEA_WMSServer/WIDESEA_DTO/MES/MesRequestDto.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,243 @@
using System;
using System.Collections.Generic;
namespace WIDESEA_DTO.MES
{
    #region æ‰˜ç›˜ç”µèŠ¯ç»‘å®š
    /// <summary>
    /// æ‰˜ç›˜ç”µèŠ¯ç»‘å®š - è¯·æ±‚
    /// </summary>
    public class BindContainerRequest
    {
        /// <summary>
        /// è®¾å¤‡ç¼–码
        /// </summary>
        public string EquipmentCode { get; set; }
        /// <summary>
        /// èµ„源编码
        /// </summary>
        public string ResourceCode { get; set; }
        /// <summary>
        /// è°ƒç”¨æœ¬åœ°æ—¶é—´
        /// </summary>
        public DateTime LocalTime { get; set; }
        /// <summary>
        /// æ‰˜ç›˜ç 
        /// </summary>
        public string ContainerCode { get; set; }
        /// <summary>
        /// ç»‘定的电芯条码列表
        /// </summary>
        public List<ContainerSfcItem> ContainerSfcList { get; set; }
        /// <summary>
        /// æ“ä½œç±»åž‹ï¼š0-默认 1-进站 2-出站 3-进出站
        /// </summary>
        public int OperationType { get; set; }
    }
    /// <summary>
    /// æ‰˜ç›˜ç”µèН项
    /// </summary>
    public class ContainerSfcItem
    {
        /// <summary>
        /// ç”µèŠ¯ç 
        /// </summary>
        public string Sfc { get; set; }
        /// <summary>
        /// ä½ç½®ä¿¡æ¯
        /// </summary>
        public string Location { get; set; }
    }
    #endregion
    #region æ‰˜ç›˜ç”µèŠ¯è§£ç»‘
    /// <summary>
    /// æ‰˜ç›˜ç”µèŠ¯è§£ç»‘ - è¯·æ±‚
    /// </summary>
    public class UnBindContainerRequest
    {
        /// <summary>
        /// è®¾å¤‡ç¼–码
        /// </summary>
        public string EquipmentCode { get; set; }
        /// <summary>
        /// èµ„源编码
        /// </summary>
        public string ResourceCode { get; set; }
        /// <summary>
        /// è°ƒç”¨æœ¬åœ°æ—¶é—´
        /// </summary>
        public DateTime LocalTime { get; set; }
        /// <summary>
        /// æ‰˜ç›˜ç 
        /// </summary>
        public string ContainCode { get; set; }
        /// <summary>
        /// ç”µèŠ¯æ¡ç ç»„
        /// </summary>
        public List<string> SfcList { get; set; }
    }
    #endregion
    #region æ‰˜ç›˜NG电芯上报
    /// <summary>
    /// æ‰˜ç›˜NG电芯上报 - è¯·æ±‚
    /// </summary>
    public class ContainerNgReportRequest
    {
        /// <summary>
        /// è®¾å¤‡ç¼–码
        /// </summary>
        public string EquipmentCode { get; set; }
        /// <summary>
        /// èµ„源编码
        /// </summary>
        public string ResourceCode { get; set; }
        /// <summary>
        /// è°ƒç”¨æœ¬åœ°æ—¶é—´
        /// </summary>
        public DateTime LocalTime { get; set; }
        /// <summary>
        /// æ‰˜ç›˜ç 
        /// </summary>
        public string ContainerCode { get; set; }
        /// <summary>
        /// NG电芯条码列表
        /// </summary>
        public List<NgSfcItem> NgSfcList { get; set; }
    }
    /// <summary>
    /// NG电芯项
    /// </summary>
    public class NgSfcItem
    {
        /// <summary>
        /// äº§å“æ¡ç 
        /// </summary>
        public string Sfc { get; set; }
        /// <summary>
        /// NG代码
        /// </summary>
        public string NgCode { get; set; }
        /// <summary>
        /// NG设备
        /// </summary>
        public string NgEquipmentCode { get; set; }
        /// <summary>
        /// NG资源
        /// </summary>
        public string NgResourceCode { get; set; }
    }
    #endregion
    #region æ‰˜ç›˜è¿›ç«™
    /// <summary>
    /// æ‰˜ç›˜è¿›ç«™ - è¯·æ±‚
    /// </summary>
    public class InboundInContainerRequest
    {
        /// <summary>
        /// è®¾å¤‡ç¼–码
        /// </summary>
        public string EquipmentCode { get; set; }
        /// <summary>
        /// èµ„源编码
        /// </summary>
        public string ResourceCode { get; set; }
        /// <summary>
        /// è°ƒç”¨æœ¬åœ°æ—¶é—´
        /// </summary>
        public DateTime LocalTime { get; set; }
        /// <summary>
        /// æ‰˜ç›˜ç 
        /// </summary>
        public string ContainerCode { get; set; }
    }
    #endregion
    #region æ‰˜ç›˜å‡ºç«™
    /// <summary>
    /// æ‰˜ç›˜å‡ºç«™ - è¯·æ±‚
    /// </summary>
    public class OutboundInContainerRequest
    {
        /// <summary>
        /// è®¾å¤‡ç¼–码
        /// </summary>
        public string EquipmentCode { get; set; }
        /// <summary>
        /// èµ„源编码
        /// </summary>
        public string ResourceCode { get; set; }
        /// <summary>
        /// è°ƒç”¨æœ¬åœ°æ—¶é—´
        /// </summary>
        public DateTime LocalTime { get; set; }
        /// <summary>
        /// æ‰˜ç›˜ç 
        /// </summary>
        public string ContainerCode { get; set; }
        /// <summary>
        /// äº§å“å‚数列表
        /// </summary>
        public List<ParamItem> ParamList { get; set; }
    }
    /// <summary>
    /// å‚数项
    /// </summary>
    public class ParamItem
    {
        /// <summary>
        /// å‚数编码
        /// </summary>
        public string ParamCode { get; set; }
        /// <summary>
        /// å‚数值
        /// </summary>
        public string ParamValue { get; set; }
        /// <summary>
        /// é‡‡é›†å‚数的时间
        /// </summary>
        public DateTime CollectionTime { get; set; }
    }
    #endregion
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_DTO/MES/MesResponseDto.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,23 @@
namespace WIDESEA_DTO.MES
{
    /// <summary>
    /// MES接口统一响应
    /// </summary>
    public class MesResponse
    {
        /// <summary>
        /// æ‰§è¡Œä»£ç ï¼š0-成功,其他-失败
        /// </summary>
        public int Code { get; set; }
        /// <summary>
        /// è¿”回信息
        /// </summary>
        public string Msg { get; set; }
        /// <summary>
        /// æ˜¯å¦æˆåŠŸ
        /// </summary>
        public bool IsSuccess => Code == 0;
    }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_IBasicService/IMesService.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,37 @@
using System.Net.Http;
using WIDESEA_Core;
using WIDESEA_DTO.MES;
namespace WIDESEA_IBasicService
{
    /// <summary>
    /// MES服务接口 - é™•西顷刻能源科技MES系统对接
    /// </summary>
    public interface IMesService : IDependency
    {
        /// <summary>
        /// æ‰˜ç›˜ç”µèŠ¯ç»‘å®š
        /// </summary>
        HttpResponseResult<MesResponse> BindContainer(BindContainerRequest request);
        /// <summary>
        /// æ‰˜ç›˜ç”µèŠ¯è§£ç»‘
        /// </summary>
        HttpResponseResult<MesResponse> UnBindContainer(UnBindContainerRequest request);
        /// <summary>
        /// æ‰˜ç›˜NG电芯上报
        /// </summary>
        HttpResponseResult<MesResponse> ContainerNgReport(ContainerNgReportRequest request);
        /// <summary>
        /// æ‰˜ç›˜è¿›ç«™
        /// </summary>
        HttpResponseResult<MesResponse> InboundInContainer(InboundInContainerRequest request);
        /// <summary>
        /// æ‰˜ç›˜å‡ºç«™
        /// </summary>
        HttpResponseResult<MesResponse> OutboundInContainer(OutboundInContainerRequest request);
    }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_StockService/StockSerivce.cs
@@ -102,46 +102,53 @@
        public async Task<WebResponseContent> GroupPalletAsync(StockDTO stock)
        {
            WebResponseContent content = new WebResponseContent();
            var now = DateTime.Now;
            var details = stock.Details.Select(item => new Dt_StockInfoDetail
            try
            {
                MaterielCode = "电芯",
                MaterielName = "电芯",
                StockQuantity = item.Quantity,
                Unit = "PCS",
                Creater = "system",
                OrderNo = "111",
                ProductionDate = now.ToString(),
                EffectiveDate = now.AddYears(1).ToString(),
                SerialNumber = item.CellBarcode,
                InboundOrderRowNo = item.Channel,
                Status = StockStatusEmun.组盘暂存.GetHashCode(),
            }).ToList();
            return await ExecuteWithinTransactionAsync(async () =>
            {
                var existingStock = StockInfoService.Repository.QueryFirst(s => s.PalletCode == stock.TargetPalletNo);
                var result = false;
                if (existingStock != null)
                var now = DateTime.Now;
                var details = stock.Details.Select(item => new Dt_StockInfoDetail
                {
                    details.ForEach(d => d.StockId = existingStock.Id);
                    result = await StockInfoDetailService.Repository.AddDataAsync(details) > 0;
                    return result ? content.OK("组盘成功") : content.Error("组盘失败");
                }
                var entity = new Dt_StockInfo
                {
                    PalletCode = stock.TargetPalletNo,
                    //WarehouseId = stock.WarehouseId > 0 ? stock.WarehouseId : 1,
                    WarehouseId = 1,
                    StockStatus = StockStatusEmun.组盘暂存.GetHashCode(),
                    MaterielCode = "电芯",
                    MaterielName = "电芯",
                    StockQuantity = item.Quantity,
                    Unit = "PCS",
                    Creater = "system",
                    Details = details
                };
                    OrderNo = "111",
                    ProductionDate = now.ToString(),
                    EffectiveDate = now.AddYears(1).ToString(),
                    SerialNumber = item.CellBarcode,
                    InboundOrderRowNo = item.Channel,
                    Status = StockStatusEmun.组盘暂存.GetHashCode(),
                }).ToList();
                result = StockInfoService.Repository.AddData(entity, x => x.Details);
                return result ? content.OK("组盘成功") : content.Error("组盘失败");
            });
                return await ExecuteWithinTransactionAsync(async () =>
                {
                    var existingStock = StockInfoService.Repository.QueryFirst(s => s.PalletCode == stock.TargetPalletNo);
                    var result = false;
                    if (existingStock != null)
                    {
                        details.ForEach(d => d.StockId = existingStock.Id);
                        result = await StockInfoDetailService.Repository.AddDataAsync(details) > 0;
                        return result ? content.OK("组盘成功") : content.Error("组盘失败");
                    }
                    var entity = new Dt_StockInfo
                    {
                        PalletCode = stock.TargetPalletNo,
                        //WarehouseId = stock.WarehouseId > 0 ? stock.WarehouseId : 1,
                        WarehouseId = 1,
                        StockStatus = StockStatusEmun.组盘暂存.GetHashCode(),
                        Creater = "system",
                        Details = details
                    };
                    result = StockInfoService.Repository.AddData(entity, x => x.Details);
                    return result ? content.OK("组盘成功") : content.Error("组盘失败");
                });
            }
            catch (Exception ex)
            {
                return content.Error($"组盘失败: {ex.Message}");
            }
        }
        /// <summary>
@@ -150,55 +157,62 @@
        public async Task<WebResponseContent> ChangePalletAsync(StockDTO stock)
        {
            WebResponseContent content = new WebResponseContent();
            if (stock == null ||
                string.IsNullOrWhiteSpace(stock.TargetPalletNo) ||
                string.IsNullOrWhiteSpace(stock.SourcePalletNo) ||
                string.Equals(stock.SourcePalletNo, stock.TargetPalletNo, StringComparison.OrdinalIgnoreCase))
            try
            {
                return content.Error("源托盘号与目标托盘号相同");
            }
            return await ExecuteWithinTransactionAsync(async () =>
            {
                var sourceStock = await StockInfoService.Repository.QueryDataNavFirstAsync(s => s.PalletCode == stock.SourcePalletNo);
                if (sourceStock == null) return content.Error("源托盘不存在");
                var targetStock = StockInfoService.Repository.QueryFirst(s => s.PalletCode == stock.TargetPalletNo);
                if (targetStock == null)
                if (stock == null ||
                    string.IsNullOrWhiteSpace(stock.TargetPalletNo) ||
                    string.IsNullOrWhiteSpace(stock.SourcePalletNo) ||
                    string.Equals(stock.SourcePalletNo, stock.TargetPalletNo, StringComparison.OrdinalIgnoreCase))
                {
                    var newStock = new Dt_StockInfo
                    {
                        PalletCode = stock.TargetPalletNo,
                        WarehouseId = sourceStock.WarehouseId,
                        StockStatus = StockStatusEmun.组盘暂存.GetHashCode(),
                        Creater = "system",
                    };
                    var newId = StockInfoService.Repository.AddData(newStock);
                    if (newId <= 0) return content.Error("换盘失败");
                    targetStock = newStock;
                    targetStock.Id = newId;
                    return content.Error("源托盘号与目标托盘号相同");
                }
                var serialNumbers = stock.Details.Select(d => d.Channel).Distinct().ToList();
                if (!serialNumbers.Any()) return content.Error("未找到有效的序列号");
                return await ExecuteWithinTransactionAsync(async () =>
                {
                    var sourceStock = await StockInfoService.Repository.QueryDataNavFirstAsync(s => s.PalletCode == stock.SourcePalletNo);
                    if (sourceStock == null) return content.Error("源托盘不存在");
                var detailEntities = StockInfoDetailService.Repository.QueryData(
                    d => d.StockId == sourceStock.Id && serialNumbers.Contains(d.InboundOrderRowNo));
                if (!detailEntities.Any()) return content.Error("未找到有效的库存明细");
                    var targetStock = StockInfoService.Repository.QueryFirst(s => s.PalletCode == stock.TargetPalletNo);
                    if (targetStock == null)
                    {
                        var newStock = new Dt_StockInfo
                        {
                            PalletCode = stock.TargetPalletNo,
                            WarehouseId = sourceStock.WarehouseId,
                            StockStatus = StockStatusEmun.组盘暂存.GetHashCode(),
                            Creater = "system",
                        };
                if (await StockInfoDetail_HtyService.Repository.AddDataAsync(CreateDetailHistory(detailEntities, "换盘")) <= 0)
                    return content.Error("换盘历史记录保存失败");
                        var newId = StockInfoService.Repository.AddData(newStock);
                        if (newId <= 0) return content.Error("换盘失败");
                if (await StockInfo_HtyService.Repository.AddDataAsync(CreateStockHistory(new[] { sourceStock, targetStock }, "换盘")) <= 0)
                    return content.Error("换盘历史记录保存失败");
                        targetStock = newStock;
                        targetStock.Id = newId;
                    }
                detailEntities.ForEach(d => d.StockId = targetStock.Id);
                var result = await StockInfoDetailService.Repository.UpdateDataAsync(detailEntities);
                if (!result) return content.Error("换盘失败");
                return content.OK("换盘成功");
            });
                    var serialNumbers = stock.Details.Select(d => d.Channel).Distinct().ToList();
                    if (!serialNumbers.Any()) return content.Error("未找到有效的序列号");
                    var detailEntities = StockInfoDetailService.Repository.QueryData(
                        d => d.StockId == sourceStock.Id && serialNumbers.Contains(d.InboundOrderRowNo));
                    if (!detailEntities.Any()) return content.Error("未找到有效的库存明细");
                    if (await StockInfoDetail_HtyService.Repository.AddDataAsync(CreateDetailHistory(detailEntities, "换盘")) <= 0)
                        return content.Error("换盘历史记录保存失败");
                    if (await StockInfo_HtyService.Repository.AddDataAsync(CreateStockHistory(new[] { sourceStock, targetStock }, "换盘")) <= 0)
                        return content.Error("换盘历史记录保存失败");
                    detailEntities.ForEach(d => d.StockId = targetStock.Id);
                    var result = await StockInfoDetailService.Repository.UpdateDataAsync(detailEntities);
                    if (!result) return content.Error("换盘失败");
                    return content.OK("换盘成功");
                });
            }
            catch (Exception ex)
            {
                return content.Error($"换盘失败: {ex.Message}");
            }
        }
        /// <summary>
@@ -207,37 +221,44 @@
        public async Task<WebResponseContent> SplitPalletAsync(StockDTO stock)
        {
            WebResponseContent content = new WebResponseContent();
            if (stock == null || string.IsNullOrWhiteSpace(stock.SourcePalletNo))
                return content.Error("源托盘号不能为空");
            return await ExecuteWithinTransactionAsync(async () =>
            try
            {
                var sourceStock = StockInfoService.Repository.QueryFirst(s => s.PalletCode == stock.SourcePalletNo);
                if (sourceStock == null) return content.Error("源托盘不存在");
                if (stock == null || string.IsNullOrWhiteSpace(stock.SourcePalletNo))
                    return content.Error("源托盘号不能为空");
                var serialNumbers = stock.Details.Select(d => d.CellBarcode).Distinct().ToList();
                if (!serialNumbers.Any())
                return await ExecuteWithinTransactionAsync(async () =>
                {
                    serialNumbers = sourceStock.Details
                                                .Where(x => stock.Details.Any(d => d.Channel == x.InboundOrderRowNo))
                                                .Select(x => x.SerialNumber)
                                                .ToList();
                }
                    var sourceStock = StockInfoService.Repository.QueryFirst(s => s.PalletCode == stock.SourcePalletNo);
                    if (sourceStock == null) return content.Error("源托盘不存在");
                var detailEntities = StockInfoDetailService.Repository.QueryData(
                    d => d.StockId == sourceStock.Id && serialNumbers.Contains(d.SerialNumber));
                if (!detailEntities.Any()) return content.Error("未找到有效的库存明细");
                    var serialNumbers = stock.Details.Select(d => d.CellBarcode).Distinct().ToList();
                    if (!serialNumbers.Any())
                    {
                        serialNumbers = sourceStock.Details
                                                    .Where(x => stock.Details.Any(d => d.Channel == x.InboundOrderRowNo))
                                                    .Select(x => x.SerialNumber)
                                                    .ToList();
                    }
                if (await StockInfoDetail_HtyService.Repository.AddDataAsync(CreateDetailHistory(detailEntities, "拆盘")) <= 0)
                    return content.Error("拆盘历史记录保存失败");
                    var detailEntities = StockInfoDetailService.Repository.QueryData(
                        d => d.StockId == sourceStock.Id && serialNumbers.Contains(d.SerialNumber));
                    if (!detailEntities.Any()) return content.Error("未找到有效的库存明细");
                if (await StockInfo_HtyService.Repository.AddDataAsync(CreateStockHistory(new[] { sourceStock }, "拆盘")) <= 0)
                    return content.Error("拆盘历史记录保存失败");
                    if (await StockInfoDetail_HtyService.Repository.AddDataAsync(CreateDetailHistory(detailEntities, "拆盘")) <= 0)
                        return content.Error("拆盘历史记录保存失败");
                var result = await StockInfoDetailService.Repository.DeleteDataAsync(detailEntities);
                if (!result) return content.Error("拆盘失败");
                return content.OK("拆盘成功");
            });
                    if (await StockInfo_HtyService.Repository.AddDataAsync(CreateStockHistory(new[] { sourceStock }, "拆盘")) <= 0)
                        return content.Error("拆盘历史记录保存失败");
                    var result = await StockInfoDetailService.Repository.DeleteDataAsync(detailEntities);
                    if (!result) return content.Error("拆盘失败");
                    return content.OK("拆盘成功");
                });
            }
            catch (Exception ex)
            {
                return content.Error($"拆盘失败: {ex.Message}");
            }
        }
        /// <summary>
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService.cs
@@ -381,7 +381,7 @@
                };
                var taskDtos = task.Adapt<WMSTaskDTO>();
                var addResult = await BaseDal.AddDataAsync(task);
                var addResult = await BaseDal.AddDataAsync(task) > 0;
                if (!addResult)
                    return WebResponseContent.Instance.Error("任务创建失败");
                return WebResponseContent.Instance.OK("任务创建成功", taskDtos);
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/appsettings.json
@@ -68,5 +68,9 @@
      "GW": [ "11001", "11010", "11068" ],
      "CW": [ "10080" ]
    }
  },
  "MES": {
    "BaseUrl": "http://localhost:5000",
    "Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjMwMTcyNzM5Mzk5NzYxOTIwIiwibmFtZSI6IlBBQ0voo4XphY3lt6XkvY0wMSIsIkZhY3RvcnlJZCI6IjEyMzQ1NiIsIlNpdGVJZCI6IjEyMzQ1NiIsIkNvZGUiOiJYWExQQUNLMDRBRTAzMiIsIm5iZiI6MTcwNDE4NzY5MCwiZXhwIjoyMTQ1NjkxNjkwLCJpc3MiOiJodHRwczovL3d3dy5oeW1zb24uY29tIiwiYXVkIjoiaHR0cHM6Ly93d3cuaHltc29uLmNvbSJ9.An1BE7UgfcSP--LtTOmmmWVE2RQFPDahLkDg1xy5KqY"
  }
}
Code/²âÊÔ¹¤¾ß/WIDESEAWCS_S7Simulator/.vs/WIDESEAWCS_S7Simulator.slnx/v18/DocumentLayout.backup.json
ÎļþÒÑɾ³ý
Code/²âÊÔ¹¤¾ß/WIDESEAWCS_S7Simulator/.vs/WIDESEAWCS_S7Simulator.slnx/v18/DocumentLayout.json
ÎļþÒÑɾ³ý
Code/²âÊÔ¹¤¾ß/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/src/views/RobotClientsView.vue
@@ -203,7 +203,7 @@
  serverId: 'robot-client-1',
  listenIp: '127.0.0.1',
  listenPort: 2000,
  localPort: 2001
  localPort: 62212
})
const status = ref<RobotClientStatusResponse | null>(null)
ÏîÄ¿×ÊÁÏ/É豸ЭÒé/ÉÏλϵͳ¶Ô½Ó/ÉÂÎ÷Çê¿ÌÄÜÔ´¿Æ¼¼MESϵͳ¶Ô½Ó½Ó¿Ú.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,329 @@
好的,我已经将您提供的五个接口文档优化为更清晰、结构更统一的Markdown格式。主要优化点包括:
1.  **统一格式**:将所有接口的说明、请求字段、响应字段等采用一致的标题层级和表格结构。
2.  **修正错误**:修正了“托盘出站”接口名称和“适用工序”中的明显笔误。
3.  **增强可读性**:使用加粗、代码块等Markdown元素,使关键信息和报文示例更突出。
4.  **补充说明**:在报文示例中,将`body`部分用JSON代码块高亮显示,便于阅读。
---
### **1.1. æ‰˜ç›˜ç”µèŠ¯ç»‘å®š**
#### **1.1.1. è§¦å‘条件**
1.  ç”¨äºŽç”µèŠ¯ç ç»‘å®šæ‰˜ç›˜ç ã€‚
#### **1.1.2. æŽ¥å£è¯´æ˜Ž**
| é¡¹ç›®         | å†…容                                     |
| :----------- | :--------------------------------------- |
| **接口名称** | æ‰˜ç›˜ç”µèŠ¯ç»‘å®šï¼ˆåœ¨åˆ¶å“&容器)              |
| **接口方式** | WebApi                                   |
| **请求方式** | POST                                     |
| **发送方**   | EQP                                      |
| **接收方**   | MES                                      |
| **接口地址** | `/EquipmentService/api/v1/BindContainer` |
| **适用工序** | ç”µèŠ¯ç ç»‘å®šæ‰˜ç›˜ç                          |
#### **1.1.3. è¯·æ±‚报文**
**Header å­—段**
| åºå· | å­—段            | å†…容        | æ•°æ®ç±»åž‹ | å¤‡æ³¨        |
| :--- | :-------------- | :---------- | :------- | :---------- |
| 1    | `Authorization` | MES认证信息 | STRING   | å€¼ç”±MES提供 |
**Body å­—段**
| åºå· | å­—段               | å†…容               | æ•°æ®ç±»åž‹                               | å¤‡æ³¨                   |
| :--- | :----------------- | :----------------- | :------------------------------------- | :--------------------- |
| 1    | `EquipmentCode`    | è®¾å¤‡ç¼–码           | STRING                                 |                        |
| 2    | `ResourceCode`     | èµ„源编码           | STRING                                 |                        |
| 3    | `LocalTime`        | è°ƒç”¨æœ¬åœ°æ—¶é—´       | DATETIME                               |                        |
| 4    | `ContainerCode`    | æ‰˜ç›˜ç              | STRING                                 |                        |
| 5    | `ContainerSfcList` | ç»‘定的电芯条码列表 | ARRAY OBJECT                           |                        |
| 6    | `OperationType`    | æ“ä½œç±»åž‹           | 0-默认<br>1-进站<br>2-出站<br>3-进出站 | æ¢æ‹˜æŸæ‰˜ç›˜ä¸Šä¼ 3,其余0 |
**`ContainerSfcList` æ•°æ®é›†å­—段**
| åºå· | å­—段       | å†…容     | æ•°æ®ç±»åž‹ | å¤‡æ³¨ |
| :--- | :--------- | :------- | :------- | :--- |
| 1    | `Sfc`      | ç”µèŠ¯ç    | STRING   |      |
| 2    | `Location` | ä½ç½®ä¿¡æ¯ | STRING   |      |
**报文示例**
```json
{
  "equipmentCode": "string",
  "resourceCode": "string",
  "localTime": "2024-03-01T03:12:29.265Z",
  "containerCode": "string",
  "containerSfcList": [
    {
      "sfc": "string",
      "location": "string"
    }
  ]
}
```
#### **1.1.4. å“åº”字段**
| å­—段   | å†…容     | æ•°æ®ç±»åž‹ | å¤‡æ³¨                  |
| :----- | :------- | :------- | :-------------------- |
| `code` | æ‰§è¡Œä»£ç  | INT      | 0: æˆåŠŸ<br>其他: å¤±è´¥ |
| `msg`  | è¿”回信息 | STRING   | åŒ…含具体的错误信息    |
---
### **1.2. æ‰˜ç›˜ç”µèŠ¯è§£ç»‘**
#### **1.2.1. è§¦å‘条件**
1.  ç”¨äºŽæ‰˜ç›˜ç è§£ç»‘电芯组。
#### **1.2.2. æŽ¥å£è¯´æ˜Ž**
| é¡¹ç›®         | å†…容                                       |
| :----------- | :----------------------------------------- |
| **接口名称** | æ‰˜ç›˜ç”µèŠ¯è§£ç»‘ï¼ˆåœ¨åˆ¶å“&容器)                |
| **接口方式** | WebApi                                     |
| **请求方式** | POST                                       |
| **发送方**   | EQP                                        |
| **接收方**   | MES                                        |
| **接口地址** | `/EquipmentService/api/v1/UnBindContainer` |
| **适用工序** | æ‰˜ç›˜ç è§£ç»‘电芯组                           |
#### **1.2.3. è¯·æ±‚报文**
**Header å­—段**
| åºå· | å­—段            | å†…容        | æ•°æ®ç±»åž‹ | å¤‡æ³¨        |
| :--- | :-------------- | :---------- | :------- | :---------- |
| 1    | `Authorization` | MES认证信息 | STRING   | å€¼ç”±MES提供 |
**Body å­—段**
| åºå· | å­—段            | å†…容         | æ•°æ®ç±»åž‹     | å¤‡æ³¨ |
| :--- | :-------------- | :----------- | :----------- | :--- |
| 1    | `EquipmentCode` | è®¾å¤‡ç¼–码     | STRING       |      |
| 2    | `ResourceCode`  | èµ„源编码     | STRING       |      |
| 3    | `LocalTime`     | è°ƒç”¨æœ¬åœ°æ—¶é—´ | DATETIME     |      |
| 4    | `ContainCode`   | æ‰˜ç›˜ç        | STRING       |      |
| 5    | `SfcList`       | ç”µèŠ¯æ¡ç ç»„   | ARRAY STRING |      |
**报文示例**
```json
{
  "equipmentCode": "string",
  "resourceCode": "string",
  "localTime": "2024-03-01T03:13:12.482Z",
  "containCode": "string",
  "sfcList": [
    "string"
  ]
}
```
#### **1.2.4. å“åº”字段**
| å­—段   | å†…容     | æ•°æ®ç±»åž‹ | å¤‡æ³¨                  |
| :----- | :------- | :------- | :-------------------- |
| `code` | æ‰§è¡Œä»£ç  | INT      | 0: æˆåŠŸ<br>其他: å¤±è´¥ |
| `msg`  | è¿”回信息 | STRING   | åŒ…含具体的错误信息    |
---
### **1.3. æ‰˜ç›˜NG电芯上报**
#### **1.3.1. è§¦å‘条件**
1.  æ‰˜ç›˜å­˜åœ¨NG条码,在拆盘或者OCV2, OCV3时,需要上报NG电芯。
#### **1.3.2. æŽ¥å£è¯´æ˜Ž**
| é¡¹ç›®         | å†…容                                         |
| :----------- | :------------------------------------------- |
| **接口名称** | æ¡ç ç»‘定(在制品&容器)                      |
| **接口方式** | WebApi                                       |
| **请求方式** | POST                                         |
| **发送方**   | EQP                                          |
| **接收方**   | MES                                          |
| **接口地址** | `/EquipmentService/api/v1/ContainerNgReport` |
| **适用工序** | æ‰˜ç›˜NG电芯上报                               |
#### **1.3.3. è¯·æ±‚报文**
**Header å­—段**
| åºå· | å­—段            | å†…容        | æ•°æ®ç±»åž‹ | å¤‡æ³¨        |
| :--- | :-------------- | :---------- | :------- | :---------- |
| 1    | `Authorization` | MES认证信息 | STRING   | å€¼ç”±MES提供 |
**Body å­—段**
| åºå· | å­—段            | å†…容                 | æ•°æ®ç±»åž‹     | å¤‡æ³¨ |
| :--- | :-------------- | :------------------- | :----------- | :--- |
| 1    | `EquipmentCode` | è®¾å¤‡ç¼–码             | STRING       |      |
| 2    | `ResourceCode`  | èµ„源编码             | STRING       |      |
| 3    | `LocalTime`     | è°ƒç”¨æœ¬åœ°æ—¶é—´         | DATETIME     |      |
| 4    | `ContainerCode` | æ‰˜ç›˜ç                | STRING       |      |
| 5    | `NgSfcList`     | ç»‘定NG的电芯条码列表 | ARRAY OBJECT |      |
**`NgSfcList` æ•°æ®é›†å­—段**
| åºå· | å­—段              | å†…容     | æ•°æ®ç±»åž‹ | å¤‡æ³¨ |
| :--- | :---------------- | :------- | :------- | :--- |
| 1    | `sfc`             | äº§å“æ¡ç  | STRING   |      |
| 2    | `ngCode`          | NG代码   | STRING   |      |
| 3    | `ngEquipmentCode` | NG设备   | STRING   |      |
| 4    | `ngResourceCode`  | NG资源   | STRING   |      |
**报文示例**
```json
{
  "equipmentCode": "string",
  "resourceCode": "string",
  "localTime": "2024-03-01T03:42:55.528Z",
  "containerCode": "string",
  "ngSfcList": [
    "string"
  ]
}
```
#### **1.3.4. å“åº”字段**
| å­—段   | å†…容     | æ•°æ®ç±»åž‹ | å¤‡æ³¨                  |
| :----- | :------- | :------- | :-------------------- |
| `code` | æ‰§è¡Œä»£ç  | INT      | 0: æˆåŠŸ<br>其他: å¤±è´¥ |
| `msg`  | è¿”回信息 | STRING   | åŒ…含具体的错误信息    |
---
### **1.4. æ‰˜ç›˜è¿›ç«™ï¼ˆå®¹å™¨è¿›ç«™ï¼‰**
#### **1.4.1. è§¦å‘条件**
1.  æ‰˜ç›˜è¿›ç«™ã€‚
#### **1.4.2. æŽ¥å£è¯´æ˜Ž**
| é¡¹ç›®         | å†…容                                          |
| :----------- | :-------------------------------------------- |
| **接口名称** | æ‰˜ç›˜è¿›ç«™ï¼ˆå®¹å™¨è¿›ç«™ï¼‰                          |
| **接口方式** | WebApi                                        |
| **请求方式** | POST                                          |
| **发送方**   | EQP                                           |
| **接收方**   | MES                                           |
| **接口地址** | `/EquipmentService/api/v1/InboundInContainer` |
| **适用工序** | æ‰˜ç›˜è¿›ç«™                                      |
#### **1.4.3. è¯·æ±‚报文**
**Header å­—段**
| åºå· | å­—段            | å†…容        | æ•°æ®ç±»åž‹ | å¤‡æ³¨        |
| :--- | :-------------- | :---------- | :------- | :---------- |
| 1    | `Authorization` | MES认证信息 | STRING   | å€¼ç”±MES提供 |
**Body å­—段**
| åºå· | å­—段            | å†…容         | æ•°æ®ç±»åž‹ | å¤‡æ³¨ |
| :--- | :-------------- | :----------- | :------- | :--- |
| 1    | `EquipmentCode` | è®¾å¤‡ç¼–码     | STRING   |      |
| 2    | `ResourceCode`  | èµ„源编码     | STRING   |      |
| 3    | `LocalTime`     | è°ƒç”¨æœ¬åœ°æ—¶é—´ | DATETIME |      |
| 4    | `ContainerCode` | æ‰˜ç›˜ç        | STRING   |      |
**报文示例**
```json
{
  "equipmentCode": "string",
  "resourceCode": "string",
  "localTime": "2024-03-01T03:43:42.144Z",
  "containerCode": "string"
}
```
#### **1.4.4. å“åº”字段**
| å­—段   | å†…容     | æ•°æ®ç±»åž‹ | å¤‡æ³¨                  |
| :----- | :------- | :------- | :-------------------- |
| `code` | æ‰§è¡Œä»£ç  | INT      | 0: æˆåŠŸ<br>其他: å¤±è´¥ |
| `msg`  | è¿”回信息 | STRING   | åŒ…含具体的错误信息    |
---
### **1.5. æ‰˜ç›˜å‡ºç«™ï¼ˆå®¹å™¨å‡ºç«™ï¼‰**
#### **1.5.1. è§¦å‘条件**
1.  æ‰˜ç›˜å‡ºç«™ã€‚
#### **1.5.2. æŽ¥å£è¯´æ˜Ž**
| é¡¹ç›®         | å†…容                                           |
| :----------- | :--------------------------------------------- |
| **接口名称** | æ‰˜ç›˜å‡ºç«™ï¼ˆå®¹å™¨å‡ºç«™ï¼‰                           |
| **接口方式** | WebApi                                         |
| **请求方式** | POST                                           |
| **发送方**   | EQP                                            |
| **接收方**   | MES                                            |
| **接口地址** | `/EquipmentService/api/v1/OutboundInContainer` |
| **适用工序** | æ‰˜ç›˜å‡ºç«™                                       |
#### **1.5.3. è¯·æ±‚报文**
**Header å­—段**
| åºå· | å­—段            | å†…容        | æ•°æ®ç±»åž‹ | å¤‡æ³¨        |
| :--- | :-------------- | :---------- | :------- | :---------- |
| 1    | `Authorization` | MES认证信息 | STRING   | å€¼ç”±MES提供 |
**Body å­—段**
| åºå· | å­—段            | å†…容         | æ•°æ®ç±»åž‹     | å¤‡æ³¨ |
| :--- | :-------------- | :----------- | :----------- | :--- |
| 1    | `EquipmentCode` | è®¾å¤‡ç¼–码     | STRING       |      |
| 2    | `ResourceCode`  | èµ„源编码     | STRING       |      |
| 3    | `LocalTime`     | è°ƒç”¨æœ¬åœ°æ—¶é—´ | DATETIME     |      |
| 4    | `ContainerCode` | æ‰˜ç›˜ç        | STRING       |      |
| 5    | `ParamList`     | äº§å“å‚数列表 | ARRAY OBJECT |      |
**`ParamList` æ•°æ®é›†å­—段**
| åºå· | å­—段             | å†…容     | æ•°æ®ç±»åž‹ | å¤‡æ³¨           |
| :--- | :--------------- | :------- | :------- | :------------- |
| 1    | `ParamCode`      | å‚数编码 | STRING   | å·¥è‰ºæä¾›       |
| 2    | `ParamValue`     | å‚数值   | STRING   |                |
| 3    | `CollectionTime` | æ—¶é—´æˆ³   | DATETIME | é‡‡é›†å‚数的时间 |
**报文示例**
```json
{
  "equipmentCode": "string",
  "resourceCode": "string",
  "localTime": "2024-03-01T03:43:42.144Z",
  "containerCode": "string",
  "paramList": [
    {
      "paramCode": "string",
      "paramValue": "string",
      "collectionTime": "2024-03-01T03:43:42.144Z"
    }
  ]
}
```
#### **1.5.4. å“åº”字段**
| å­—段   | å†…容     | æ•°æ®ç±»åž‹ | å¤‡æ³¨                  |
| :----- | :------- | :------- | :-------------------- |
| `code` | æ‰§è¡Œä»£ç  | INT      | 0: æˆåŠŸ<br>其他: å¤±è´¥ |
| `msg`  | è¿”回信息 | STRING   | åŒ…含具体的错误信息    |