Jelajahi Sumber

MES服务接口重构与集成,增强管理层联动

重构并扩展了IMesService接口,采用更明确的参数签名,支持超时与取消,完善了注释说明。MesService实现类移除通用BuildUrl,改为string.Format拼接URL,增加重试机制和更细致的返回判断。Management类集成MES服务,实现过站检查和数据上传与PLC的联动。优化调用方式和异常处理,提升了代码可读性和健壮性,为后续MES深度集成奠定基础。
孝锋 徐 7 bulan lalu
induk
melakukan
6e4d82b3d0

+ 1 - 0
TeamAAS-VM/App.xaml.cs

@@ -119,6 +119,7 @@ namespace TeamAAS_VP
             containerRegistry.RegisterSingleton<IRemoteCommandService, RemoteCommandService>();
             containerRegistry.RegisterSingleton<IRemoteCommandService, RemoteCommandService>();
             containerRegistry.RegisterSingleton<ILightManagerService, LightManagerService>();
             containerRegistry.RegisterSingleton<ILightManagerService, LightManagerService>();
             containerRegistry.RegisterSingleton<ICameraCalibrationService, CameraCalibrationService>();
             containerRegistry.RegisterSingleton<ICameraCalibrationService, CameraCalibrationService>();
+            containerRegistry.RegisterSingleton<IMesService, MesService>();
             containerRegistry.RegisterSingleton<Management>();
             containerRegistry.RegisterSingleton<Management>();
 
 
             // Register system database service and initializer
             // Register system database service and initializer

+ 31 - 8
TeamAAS-VM/Core/Management.cs

@@ -78,6 +78,7 @@ namespace TeamAAS_VP.Core
         ICalibrationService _calibrationService;
         ICalibrationService _calibrationService;
         IRemoteCommandService _remoteCommandService;
         IRemoteCommandService _remoteCommandService;
         ISystemDatabaseService _systemDatabaseService;
         ISystemDatabaseService _systemDatabaseService;
+        IMesService _mesService;
 
 
         Timer yieldtimer;
         Timer yieldtimer;
         private DateTime StartTime = DateTime.Now;
         private DateTime StartTime = DateTime.Now;
@@ -212,7 +213,8 @@ namespace TeamAAS_VP.Core
 
 
         public Management(IRegionManager regionManager, IEventAggregator ea, IContainerProvider container, IConfigService configService,
         public Management(IRegionManager regionManager, IEventAggregator ea, IContainerProvider container, IConfigService configService,
             IRobotService robotService, ICameraService cameraService, IFeederService feederService, IPlcService plcService, IProductService productService,
             IRobotService robotService, ICameraService cameraService, IFeederService feederService, IPlcService plcService, IProductService productService,
-            ICalibrationService calibrationService, IRemoteCommandService remoteCommandService, ISystemDatabaseService systemDatabaseService, ILightManagerService lightManagerService)
+            ICalibrationService calibrationService, IRemoteCommandService remoteCommandService, ISystemDatabaseService systemDatabaseService, ILightManagerService lightManagerService, 
+            IMesService mesService)
         {
         {
             _regionManager = regionManager;
             _regionManager = regionManager;
             _eventAggregator = ea;
             _eventAggregator = ea;
@@ -229,6 +231,7 @@ namespace TeamAAS_VP.Core
             _remoteCommandService = remoteCommandService;
             _remoteCommandService = remoteCommandService;
             _systemDatabaseService = systemDatabaseService;
             _systemDatabaseService = systemDatabaseService;
             _lightManagerService = lightManagerService;
             _lightManagerService = lightManagerService;
+            _mesService = mesService;
         }
         }
 
 
         #region 读取配置参数
         #region 读取配置参数
@@ -1217,7 +1220,7 @@ namespace TeamAAS_VP.Core
                 }
                 }
 
 
                 //获取产品编码
                 //获取产品编码
-                _productService.CurrentProductCode = plc.ReadNode<string>(addressConfig.In_Code.Address);
+                _productService.CurrentProductCode = plc.ReadNode<string>(string.Format(addressConfig.In_Code.Address,0));
                 //如果产品编码为空,则以当前的时间戳作为编码
                 //如果产品编码为空,则以当前的时间戳作为编码
                 if (string.IsNullOrEmpty(_productService.CurrentProductCode))
                 if (string.IsNullOrEmpty(_productService.CurrentProductCode))
                 {
                 {
@@ -1790,27 +1793,47 @@ namespace TeamAAS_VP.Core
                                 {
                                 {
                                     return;
                                     return;
                                 }
                                 }
-
+                                var code = plc.ReadNode<string>(string.Format(addressConfig.In_Code.Address, i));
                                 if (state == 1)
                                 if (state == 1)
                                 {
                                 {
-                                    if(i == 0)
+                                    //这里缺少从MES获取产品配方的内容,需要补充
+                                    
+
+                                    
+                                    (bool isSuccess, string response) = await _mesService.StationGetAsync(code, 2);
+                                    if (i == 0)
                                     {
                                     {
                                         SendTaskMessage($"收到当前过站检查请求(当站是否生产)...", MessageLevel.Debug);
                                         SendTaskMessage($"收到当前过站检查请求(当站是否生产)...", MessageLevel.Debug);
                                     }
                                     }
                                     else
                                     else
                                     {
                                     {
                                         SendTaskMessage($"收到{i}过站检查请求(当站是否生产)...", MessageLevel.Debug);
                                         SendTaskMessage($"收到{i}过站检查请求(当站是否生产)...", MessageLevel.Debug);
-                                    }                                    
+                                    }
+                                    if (isSuccess)
+                                    {
+                                        //产品发生改变并且供料器发生了变化时,需要通知PLC进行取料更改
+                                        await plc.WriteNodeAsync(addressConfig.Out_ScrewFeederIsChanged.Address, (Int16)1); //不发生改变
 
 
-                                    //产品发生改变并且供料器发生了变化时,需要通知PLC进行取料更改
-                                    await plc.WriteNodeAsync(addressConfig.Out_ScrewFeederIsChanged.Address, (Int16)1); //不发生改变
+                                        await plc.WriteNodeAsync(string.Format(addressConfig.Out_MesStatus.Address, i), (Int16)1);
+                                    }
+                                    else
+                                    {
+                                        //产品发生改变并且供料器发生了变化时,需要通知PLC进行取料更改
+                                        await plc.WriteNodeAsync(addressConfig.Out_ScrewFeederIsChanged.Address, (Int16)1); //不发生改变
 
 
-                                    await plc.WriteNodeAsync(string.Format(addressConfig.Out_MesStatus.Address,i), (Int16)1);
+                                        await plc.WriteNodeAsync(string.Format(addressConfig.Out_MesStatus.Address, i), (Int16)0);
+                                    }
                                 }
                                 }
                                 // 上传本站数据
                                 // 上传本站数据
                                 else if (state == 2)
                                 else if (state == 2)
                                 {
                                 {
                                     SendTaskMessage($"收到{i}上传本站数据请求...", MessageLevel.Debug);
                                     SendTaskMessage($"收到{i}上传本站数据请求...", MessageLevel.Debug);
+
+                                    //这里缺少上传MES的本站数据结果及开始和结束时间,需要补充
+
+                                    (bool isSuccess, string response) = await _mesService.SubmitGetAsync(code, true,DateTime.Now,DateTime.Now);
+                                    //假设上传成功,这里缺少上传失败的处理
+
                                     await plc.WriteNodeAsync(string.Format(addressConfig.Out_MesStatus.Address, i), (Int16)3);
                                     await plc.WriteNodeAsync(string.Format(addressConfig.Out_MesStatus.Address, i), (Int16)3);
                                 }
                                 }
                                 else
                                 else

+ 69 - 7
TeamAAS-VM/Interfaces/IMesService.cs

@@ -6,20 +6,82 @@ using System.Threading.Tasks;
 namespace TeamAAS_VP.Interfaces
 namespace TeamAAS_VP.Interfaces
 {
 {
     /// <summary>
     /// <summary>
-    /// MES 通信服务接口,提供通过配置的 URL 执行过站及提交信息的 GET 请求。
+    /// MES 通信服务接口。
+    /// 提供通过配置的 URL 发起 GET 请求的抽象,用于执行过站(Station)与提交结果(Submit)操作。
+    /// 实现者应负责管理内部 HTTP 客户端或通信资源,并在 Dispose 中释放这些资源。
     /// </summary>
     /// </summary>
     public interface IMesService : IDisposable
     public interface IMesService : IDisposable
     {
     {
         /// <summary>
         /// <summary>
-        /// 使用配置的过站 URL 发起 GET 请求。可传入额外的查询参数。
-        /// 返回 (IsSuccess, ResponseString)。
+        /// 使用配置的过站(Station)URL 发起 GET 请求,携带序列号(sn)作为查询参数进行过站。
         /// </summary>
         /// </summary>
-        Task<(bool IsSuccess, string Response)> StationGetAsync(IDictionary<string, string> queryParameters = null, CancellationToken cancellationToken = default);
+        /// <param name="sn">要过站的产品序列号,不能为空或空白。</param>
+        /// <param name="cancellationToken">用于在外部请求取消时中止异步请求的 <see cref="CancellationToken"/>。默认值为 <see cref="CancellationToken.None"/>。</param>
+        /// <returns>
+        /// 一个包含请求结果的元组:
+        /// IsSuccess 表示请求并解析 MES 响应是否被认为成功(由实现定义,例如 HTTP 200 且业务状态正常)。
+        /// Response 为 MES 返回的原始响应字符串或描述错误的信息(不为 null,可为空字符串)。
+        /// </returns>
+        /// <remarks>
+        /// - 实现应从配置中读取过站 URL 并将 sn 作为查询参数附加到 URL。
+        /// - 实现应尊重 <paramref name="cancellationToken"/>,在取消时尽早抛出 <see cref="OperationCanceledException"/> 或返回失败结果。
+        /// - 若 <paramref name="sn"/> 无效,应抛出 <see cref="ArgumentNullException"/> 或 <see cref="ArgumentException"/>。
+        /// </remarks>
+        Task<(bool IsSuccess, string Response)> StationGetAsync(string sn, CancellationToken cancellationToken = default);
 
 
         /// <summary>
         /// <summary>
-        /// 使用配置的提交 URL 发起 GET 请求。可传入额外的查询参数。
-        /// 返回 (IsSuccess, ResponseString)。
+        /// 使用配置的过站(Station)URL 发起 GET 请求,携带序列号(sn)作为查询参数进行过站,并使用指定的超时时间(秒)。
         /// </summary>
         /// </summary>
-        Task<(bool IsSuccess, string Response)> SubmitGetAsync(IDictionary<string, string> queryParameters = null, CancellationToken cancellationToken = default);
+        /// <param name="sn">要过站的产品序列号,不能为空或空白。</param>
+        /// <param name="timeoutInSeconds">请求超时时间,单位为秒。应为正数;实现可以将其应用到 HTTP 客户端或请求级别超时。</param>
+        /// <returns>
+        /// 一个包含请求结果的元组:
+        /// IsSuccess 表示请求并解析 MES 响应是否被认为成功。
+        /// Response 为 MES 返回的原始响应字符串或描述错误的信息。
+        /// </returns>
+        /// <remarks>
+        /// - 此重载方便在不使用 <see cref="CancellationToken"/> 的场景中指定超时时间。
+        /// - 实现应验证 <paramref name="timeoutInSeconds"/> 为合理值(例如 > 0),否则可抛出 <see cref="ArgumentOutOfRangeException"/>。
+        /// </remarks>
+        Task<(bool IsSuccess, string Response)> StationGetAsync(string sn, double timeoutInSeconds);
+
+        /// <summary>
+        /// 使用配置的提交(Submit)URL 发起 GET 请求,提交过站结果及时间信息。
+        /// </summary>
+        /// <param name="sn">待提交的产品序列号,不能为空或空白。</param>
+        /// <param name="isSuccess">表示该序列号对应的过站是否成功(业务层面的成功标志)。</param>
+        /// <param name="start_time">过程开始时间(本地时间或 UTC,取决于与 MES 的约定)。实现方应按与 MES 约定的时区/格式发送。</param>
+        /// <param name="stop_time">过程结束时间,语义同 <paramref name="start_time"/>。</param>
+        /// <param name="cancellationToken">用于在外部请求取消时中止异步请求的 <see cref="CancellationToken"/>。默认值为 <see cref="CancellationToken.None"/>。</param>
+        /// <returns>
+        /// 一个包含请求结果的元组:
+        /// IsSuccess 表示提交请求及解析 MES 响应是否被认为成功。
+        /// Response 为 MES 返回的原始响应字符串或描述错误的信息。
+        /// </returns>
+        /// <remarks>
+        /// - 实现应将时间参数按 MES 要求格式化为查询字符串或适当的请求参数。
+        /// - 若 <paramref name="start_time"/> 晚于 <paramref name="stop_time"/>,实现应记录并视情况返回失败或抛出异常。
+        /// - 实现应尊重 <paramref name="cancellationToken"/>,并在取消时尽早中止请求。
+        /// </remarks>
+        Task<(bool IsSuccess, string Response)> SubmitGetAsync(string sn, bool isSuccess, DateTime start_time, DateTime stop_time, CancellationToken cancellationToken = default);
+
+        /// <summary>
+        /// 使用配置的提交(Submit)URL 发起 GET 请求,提交过站结果及时间信息,并使用指定的超时时间(秒)。
+        /// </summary>
+        /// <param name="sn">待提交的产品序列号,不能为空或空白。</param>
+        /// <param name="isSuccess">表示该序列号对应的过站是否成功。</param>
+        /// <param name="start_time">过程开始时间。</param>
+        /// <param name="stop_time">过程结束时间。</param>
+        /// <param name="timeoutInSeconds">请求超时时间,单位为秒。应为正数。</param>
+        /// <returns>
+        /// 一个包含请求结果的元组:
+        /// IsSuccess 指示提交是否被认为成功。
+        /// Response 包含 MES 的原始响应或错误信息。
+        /// </returns>
+        /// <remarks>
+        /// - 此重载用于在不传入 <see cref="CancellationToken"/> 的场景中指定超时时间。
+        /// - 实现应确保对时间参数与布尔参数进行适当的 URL 编码或格式化。
+        /// </remarks>
+        Task<(bool IsSuccess, string Response)> SubmitGetAsync(string sn, bool isSuccess, DateTime start_time, DateTime stop_time, double timeoutInSeconds);
     }
     }
 }
 }

+ 144 - 73
TeamAAS-VM/Services/MesService.cs

@@ -11,23 +11,30 @@ namespace TeamAAS_VP.Services
 {
 {
     /// <summary>
     /// <summary>
     /// 基于 <see cref="HttpClient"/> 的 MES 通信服务实现。
     /// 基于 <see cref="HttpClient"/> 的 MES 通信服务实现。
-    /// 该服务通过 <see cref="IConfigService"/> 获取设备配置中的 MES 接口地址,
-    /// 提供对站点查询(StationGetAsync)和提交(SubmitGetAsync)的 GET 请求封装。
+    /// <para>
+    /// 本服务通过注入的 <see cref="IConfigService"/> 获取设备配置(包含 MES 接口地址),
+    /// 提供对站点查询(StationGetAsync)和提交(SubmitGetAsync)的 GET 请求封装并带有重试逻辑。
+    /// </para>
+    /// <para>
+    /// 注意:
+    /// - <see cref="_httpClient"/> 为可复用的单一实例,建议在服务生命周期内重用以避免端口耗尽。
+    /// - 本类实现了显式的 <see cref="Dispose"/> 方法以释放内部 <see cref="HttpClient"/> 资源,调用后实例不可再用。
+    /// </para>
     /// </summary>
     /// </summary>
     public class MesService : IMesService
     public class MesService : IMesService
     {
     {
         /// <summary>
         /// <summary>
-        /// 用于执行 HTTP 请求的客户端实例。建议在服务生命周期内重用。
+        /// 用于执行 HTTP 请求的客户端实例。建议在服务生命周期内重用,避免频繁创建导致套接字/端口耗尽
         /// </summary>
         /// </summary>
         private readonly HttpClient _httpClient;
         private readonly HttpClient _httpClient;
 
 
         /// <summary>
         /// <summary>
-        /// 配置服务,用于获取设备信息(包含 MES URL)。
+        /// 配置服务,用于获取设备信息(包含 MES URL、是否启用 MES 等)。
         /// </summary>
         /// </summary>
         private readonly IConfigService _configService;
         private readonly IConfigService _configService;
 
 
         /// <summary>
         /// <summary>
-        /// 标记实例是否已释放,防止重复释放。
+        /// 标记实例是否已释放,防止重复释放和在释放后继续使用导致未定义行为
         /// </summary>
         /// </summary>
         private bool _disposed;
         private bool _disposed;
 
 
@@ -43,112 +50,176 @@ namespace TeamAAS_VP.Services
             _disposed = false;
             _disposed = false;
         }
         }
 
 
-        /// <summary>
-        /// 根据基地址和查询参数构建完整 URL。
-        /// - 如果 <paramref name="baseUrl"/> 为空或空白,返回 null。
-        /// - 如果 <paramref name="queryParameters"/> 为空或不包含元素,返回原始 <paramref name="baseUrl"/>。
-        /// - 否则对键和值进行 URL 编码并拼接为查询字符串(以 '?' 或 '&amp;' 作为分隔)。
-        /// </summary>
-        /// <param name="baseUrl">基础 URL,例如 "http://example.com/api"。</param>
-        /// <param name="queryParameters">要附加的查询参数字典,可为 null。</param>
-        /// <returns>生成的完整 URL 字符串,或在无效 baseUrl 时返回 null。</returns>
-        private static string BuildUrl(string baseUrl, IDictionary<string, string> queryParameters)
-        {
-            if (string.IsNullOrWhiteSpace(baseUrl)) return null;
-            if (queryParameters == null || queryParameters.Count == 0) return baseUrl;
-            var sb = new StringBuilder();
-            sb.Append(baseUrl);
-            if (!baseUrl.Contains("?")) sb.Append('?');
-            else if (!baseUrl.EndsWith("?") && !baseUrl.EndsWith("&")) sb.Append('&');
-            foreach (var kv in queryParameters)
-            {
-                sb.Append(Uri.EscapeDataString(kv.Key)).Append('=').Append(Uri.EscapeDataString(kv.Value ?? string.Empty)).Append('&');
-            }
-            if (sb.Length > 0 && sb[sb.Length - 1] == '&') sb.Length--;
-            return sb.ToString();
-        }
 
 
         /// <summary>
         /// <summary>
         /// 向配置中指定的 MES 站点 URL 发起 GET 请求(用于过站查询)。
         /// 向配置中指定的 MES 站点 URL 发起 GET 请求(用于过站查询)。
-        /// 返回一个元组,<c>IsSuccess</c> 表示 HTTP 状态码是否为成功,<c>Response</c> 包含响应文本或错误信息。
         /// </summary>
         /// </summary>
-        /// <param name="queryParameters">可选的查询参数字典,将附加到 MES 站点 URL 上。</param>
-        /// <param name="cancellationToken">用于取消请求的令牌。</param>
+        /// <param name="sn">要查询的序列号(SN)。</param>
+        /// <param name="cancellationToken">可选的取消令牌,用于请求超时或取消。</param>
         /// <returns>
         /// <returns>
-        /// 异步任务,结果为 (<c>IsSuccess</c>, <c>Response</c>)。
-        /// - 当缺少配置或 URL 无效时返回 (false, 错误说明)。
-        /// - 当请求被取消时返回 (false, "Canceled")。
-        /// - 其他异常返回 (false, 异常消息)。
+        /// 返回元组 (IsSuccess, Response):
+        /// - IsSuccess: 请求是否视为成功(即 MES 返回了预期的 "SFC_OK" 且包含 "unit_process_check=OK")。
+        /// - Response: 原始响应文本或错误信息说明(如 "MES功能未启用!"、"Missing MES station URL"、"Canceled" 等)。
         /// </returns>
         /// </returns>
-        public async Task<(bool IsSuccess, string Response)> StationGetAsync(IDictionary<string, string> queryParameters = null, CancellationToken cancellationToken = default)
+        /// <remarks>
+        /// 行为说明:
+        /// - 从配置获取设备信息,若 MES 未启用则直接返回 IsSuccess=true 且 Response 为提示文本(不视为错误)。
+        /// - 若未配置 MES URL 则返回 IsSuccess=false 与错误文本。
+        /// - 使用配置的 URL(通过 <see cref="string.Format"/> 填充 sn)发起 GET 请求,最多重试 10 次。
+        /// - 只有当响应包含 "SFC_OK" 且包含 "unit_process_check=OK" 时视为成功并立即返回。
+        /// - 捕获 <see cref="OperationCanceledException"/> 返回 "Canceled",捕获其它异常则返回异常消息。
+        /// - 若实例已被释放则抛出 <see cref="ObjectDisposedException"/>。
+        /// </remarks>
+        public async Task<(bool IsSuccess, string Response)> StationGetAsync(string sn, CancellationToken cancellationToken = default)
         {
         {
+            // 如果已经释放,则抛出异常,防止在已释放资源上继续操作。
+            if (_disposed) throw new ObjectDisposedException(nameof(MesService));
+
             var device = _configService.GetDeviceInfo();
             var device = _configService.GetDeviceInfo();
-            if (device == null || string.IsNullOrWhiteSpace(device.MesStationUrl))
+            if (device == null || !device.EnableMES)
+            {
+                return (true, "MES功能未启用!");
+            }
+            if (string.IsNullOrWhiteSpace(device.MesStationUrl))
                 return (false, "Missing MES station URL");
                 return (false, "Missing MES station URL");
 
 
-            var url = BuildUrl(device.MesStationUrl, queryParameters);
-            if (string.IsNullOrWhiteSpace(url)) return (false, "Invalid station URL");
+            var url = string.Format(device.MesStationUrl, sn);
 
 
-            try
+            (bool IsSuccess, string Response) result = (false, string.Empty);
+            for (int i = 0; i < 5; i++)
             {
             {
-                using (var rsp = await _httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false))
+                try
                 {
                 {
-                    var text = await rsp.Content.ReadAsStringAsync().ConfigureAwait(false);
-                    return (rsp.IsSuccessStatusCode, text);
+                    using (var rsp = await _httpClient.GetAsync(url, cancellationToken))
+                    {
+                        var text = await rsp.Content.ReadAsStringAsync();
+                        // "SFC_OK" 代表和 MES 连接成功
+                        if (text.Contains("SFC_OK"))
+                        {
+                            if (text.Contains("unit_process_check=OK"))
+                            {
+                                return (true, text);
+                            }
+                        }
+                        result = (false, text);
+                    }
+                }
+                catch (OperationCanceledException)
+                {
+                    result = (false, "Canceled");
+                }
+                catch (Exception ex)
+                {
+                    result = (false, ex.Message);
                 }
                 }
             }
             }
-            catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
-            {
-                return (false, "Canceled");
-            }
-            catch (Exception ex)
+            return result;
+        }
+
+        /// <summary>
+        /// 向 MES 发起站点查询请求,使用超时(秒)作为便捷重载。
+        /// </summary>
+        /// <param name="sn">序列号(SN)。</param>
+        /// <param name="timeoutInSeconds">请求超时,单位为秒。</param>
+        /// <returns>与 <see cref="StationGetAsync(string, CancellationToken)"/> 相同的返回语义。</returns>
+        public async Task<(bool IsSuccess, string Response)> StationGetAsync(string sn, double timeoutInSeconds)
+        {
+            using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutInSeconds)))
             {
             {
-                return (false, ex.Message);
+                return await StationGetAsync(sn, cts.Token);
             }
             }
         }
         }
 
 
         /// <summary>
         /// <summary>
-        /// 向配置中指定的 MES 提交 URL 发起 GET 请求(用于提交数据/记录)。
-        /// 返回一个元组,<c>IsSuccess</c> 表示 HTTP 状态码是否为成功,<c>Response</c> 包含响应文本或错误信息。
+        /// 向配置中指定的 MES 提交 URL 发起 GET 请求(用于提交测试结果/记录)。
         /// </summary>
         /// </summary>
-        /// <param name="queryParameters">可选的查询参数字典,将附加到 MES 提交 URL 上。</param>
-        /// <param name="cancellationToken">用于取消请求的令牌。</param>
+        /// <param name="sn">要提交的序列号(SN)。</param>
+        /// <param name="isSuccess">表示此次记录是否通过(true => PASS,false => FAIL)。</param>
+        /// <param name="start_time">开始时间,提交时使用 "yyyy-MM-dd HH:mm:ss" 格式。</param>
+        /// <param name="stop_time">结束时间,提交时使用 "yyyy-MM-dd HH:mm:ss" 格式。</param>
+        /// <param name="cancellationToken">可选的取消令牌,用于请求超时或取消。</param>
         /// <returns>
         /// <returns>
-        /// 异步任务,结果为 (<c>IsSuccess</c>, <c>Response</c>)。
-        /// - 当缺少配置或 URL 无效时返回 (false, 错误说明)。
-        /// - 当请求被取消时返回 (false, "Canceled")。
-        /// - 其他异常返回 (false, 异常消息)。
+        /// 返回元组 (IsSuccess, Response):
+        /// - IsSuccess: 提交是否被 MES 视为成功(收到 "SFC_OK" 且响应中无额外多行信息)。
+        /// - Response: MES 返回文本或错误说明;当 MES 返回 "SFC_OK" 但包含额外多行信息(代表 SN 数据问题)时,IsSuccess 为 false 且 Response 为该文本。
         /// </returns>
         /// </returns>
-        public async Task<(bool IsSuccess, string Response)> SubmitGetAsync(IDictionary<string, string> queryParameters = null, CancellationToken cancellationToken = default)
+        /// <remarks>
+        /// 行为说明:
+        /// - 从配置获取设备信息,若 MES 未启用则直接返回 IsSuccess=true 且 Response 为提示文本(不视为错误)。
+        /// - 若未配置 MES 提交 URL 则返回 IsSuccess=false 与错误文本。
+        /// - 使用配置的 URL(通过 <see cref="string.Format"/> 按顺序填充 sn、PASS/FAIL、start_time、stop_time)发起 GET 请求,最多重试 10 次。
+        /// - 在收到包含 "SFC_OK" 的响应时,如果返回文本包含多行(有附加信息)则视为提交失败并返回该文本,否则视为提交成功。
+        /// - 捕获 <see cref="OperationCanceledException"/> 返回 "Canceled",捕获其它异常则返回异常消息。
+        /// - 若实例已被释放则抛出 <see cref="ObjectDisposedException"/>。
+        /// </remarks>
+        public async Task<(bool IsSuccess, string Response)> SubmitGetAsync(string sn, bool isSuccess, DateTime start_time, DateTime stop_time, CancellationToken cancellationToken = default)
         {
         {
+            // 如果已经释放,则抛出异常,防止在已释放资源上继续操作。
+            if (_disposed) throw new ObjectDisposedException(nameof(MesService));
+
             var device = _configService.GetDeviceInfo();
             var device = _configService.GetDeviceInfo();
-            if (device == null || string.IsNullOrWhiteSpace(device.MesSubmitUrl))
+            if (device == null || !device.EnableMES)
+            {
+                return (true, "MES功能未启用!");
+            }
+            if (string.IsNullOrWhiteSpace(device.MesStationUrl))
                 return (false, "Missing MES submit URL");
                 return (false, "Missing MES submit URL");
 
 
-            var url = BuildUrl(device.MesSubmitUrl, queryParameters);
-            if (string.IsNullOrWhiteSpace(url)) return (false, "Invalid submit URL");
+            var url = string.Format(device.MesStationUrl, sn, isSuccess ? "PASS" : "FAIL", start_time.ToString("yyyy-MM-dd HH:mm:ss"), stop_time.ToString("yyyy-MM-dd HH:mm:ss"));
 
 
-            try
+            (bool IsSuccess, string Response) result = (false, string.Empty);
+            for (int i = 0; i < 5; i++)
             {
             {
-                using (var rsp = await _httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false))
+                try
                 {
                 {
-                    var text = await rsp.Content.ReadAsStringAsync().ConfigureAwait(false);
-                    return (rsp.IsSuccessStatusCode, text);
+                    using (var rsp = await _httpClient.GetAsync(url, cancellationToken))
+                    {
+                        var text = await rsp.Content.ReadAsStringAsync();
+                        // "SFC_OK" 代表和 MES 连接成功
+                        if (text.Contains("SFC_OK"))
+                        {
+                            // 如果有附加信息(多行),代表此 SN 数据有问题,视为失败并返回该文本
+                            if (text.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries).Length > 1)
+                            {
+                                return (false, text);
+                            }
+                            return (true, text);
+                        }
+                        result = (false, text);
+                    }
+                }
+                catch (OperationCanceledException)
+                {
+                    result = (false, "Canceled");
+                }
+                catch (Exception ex)
+                {
+                    result = (false, ex.Message);
                 }
                 }
             }
             }
-            catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
-            {
-                return (false, "Canceled");
-            }
-            catch (Exception ex)
+            return result;
+        }
+
+        /// <summary>
+        /// 向 MES 提交请求的超时重载,使用超时(秒)作为便捷参数。
+        /// </summary>
+        /// <param name="sn">序列号(SN)。</param>
+        /// <param name="isSuccess">是否通过(PASS/FAIL)。</param>
+        /// <param name="start_time">开始时间。</param>
+        /// <param name="stop_time">结束时间。</param>
+        /// <param name="timeoutInSeconds">请求超时,单位为秒。</param>
+        /// <returns>与 <see cref="SubmitGetAsync(string, bool, DateTime, DateTime, CancellationToken)"/> 相同的返回语义。</returns>
+        public async Task<(bool IsSuccess, string Response)> SubmitGetAsync(string sn, bool isSuccess, DateTime start_time, DateTime stop_time, double timeoutInSeconds)
+        {
+            using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutInSeconds)))
             {
             {
-                return (false, ex.Message);
+                return await SubmitGetAsync(sn, isSuccess, start_time, stop_time, cts.Token);
             }
             }
         }
         }
 
 
         /// <summary>
         /// <summary>
         /// 释放服务占用的托管资源(当前仅 <see cref="_httpClient"/>)。
         /// 释放服务占用的托管资源(当前仅 <see cref="_httpClient"/>)。
-        /// 本方法为幂等操作,重复调用不会产生异常。
+        /// <para>本方法为幂等操作,重复调用不会产生异常。调用后实例不可再用于发起请求,尝试使用将抛出 <see cref="ObjectDisposedException"/>。</para>
         /// </summary>
         /// </summary>
         public void Dispose()
         public void Dispose()
         {
         {