Преглед изворни кода

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

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

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

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

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

@@ -78,6 +78,7 @@ namespace TeamAAS_VP.Core
         ICalibrationService _calibrationService;
         IRemoteCommandService _remoteCommandService;
         ISystemDatabaseService _systemDatabaseService;
+        IMesService _mesService;
 
         Timer yieldtimer;
         private DateTime StartTime = DateTime.Now;
@@ -212,7 +213,8 @@ namespace TeamAAS_VP.Core
 
         public Management(IRegionManager regionManager, IEventAggregator ea, IContainerProvider container, IConfigService configService,
             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;
             _eventAggregator = ea;
@@ -229,6 +231,7 @@ namespace TeamAAS_VP.Core
             _remoteCommandService = remoteCommandService;
             _systemDatabaseService = systemDatabaseService;
             _lightManagerService = lightManagerService;
+            _mesService = mesService;
         }
 
         #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))
                 {
@@ -1790,27 +1793,47 @@ namespace TeamAAS_VP.Core
                                 {
                                     return;
                                 }
-
+                                var code = plc.ReadNode<string>(string.Format(addressConfig.In_Code.Address, i));
                                 if (state == 1)
                                 {
-                                    if(i == 0)
+                                    //这里缺少从MES获取产品配方的内容,需要补充
+                                    
+
+                                    
+                                    (bool isSuccess, string response) = await _mesService.StationGetAsync(code, 2);
+                                    if (i == 0)
                                     {
                                         SendTaskMessage($"收到当前过站检查请求(当站是否生产)...", MessageLevel.Debug);
                                     }
                                     else
                                     {
                                         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)
                                 {
                                     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);
                                 }
                                 else

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

@@ -6,20 +6,82 @@ using System.Threading.Tasks;
 namespace TeamAAS_VP.Interfaces
 {
     /// <summary>
-    /// MES 通信服务接口,提供通过配置的 URL 执行过站及提交信息的 GET 请求。
+    /// MES 通信服务接口。
+    /// 提供通过配置的 URL 发起 GET 请求的抽象,用于执行过站(Station)与提交结果(Submit)操作。
+    /// 实现者应负责管理内部 HTTP 客户端或通信资源,并在 Dispose 中释放这些资源。
     /// </summary>
     public interface IMesService : IDisposable
     {
         /// <summary>
-        /// 使用配置的过站 URL 发起 GET 请求。可传入额外的查询参数。
-        /// 返回 (IsSuccess, ResponseString)。
+        /// 使用配置的过站(Station)URL 发起 GET 请求,携带序列号(sn)作为查询参数进行过站。
         /// </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>
-        /// 使用配置的提交 URL 发起 GET 请求。可传入额外的查询参数。
-        /// 返回 (IsSuccess, ResponseString)。
+        /// 使用配置的过站(Station)URL 发起 GET 请求,携带序列号(sn)作为查询参数进行过站,并使用指定的超时时间(秒)。
         /// </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>
     /// 基于 <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>
     public class MesService : IMesService
     {
         /// <summary>
-        /// 用于执行 HTTP 请求的客户端实例。建议在服务生命周期内重用。
+        /// 用于执行 HTTP 请求的客户端实例。建议在服务生命周期内重用,避免频繁创建导致套接字/端口耗尽
         /// </summary>
         private readonly HttpClient _httpClient;
 
         /// <summary>
-        /// 配置服务,用于获取设备信息(包含 MES URL)。
+        /// 配置服务,用于获取设备信息(包含 MES URL、是否启用 MES 等)。
         /// </summary>
         private readonly IConfigService _configService;
 
         /// <summary>
-        /// 标记实例是否已释放,防止重复释放。
+        /// 标记实例是否已释放,防止重复释放和在释放后继续使用导致未定义行为
         /// </summary>
         private bool _disposed;
 
@@ -43,112 +50,176 @@ namespace TeamAAS_VP.Services
             _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>
         /// 向配置中指定的 MES 站点 URL 发起 GET 请求(用于过站查询)。
-        /// 返回一个元组,<c>IsSuccess</c> 表示 HTTP 状态码是否为成功,<c>Response</c> 包含响应文本或错误信息。
         /// </summary>
-        /// <param name="queryParameters">可选的查询参数字典,将附加到 MES 站点 URL 上。</param>
-        /// <param name="cancellationToken">用于取消请求的令牌。</param>
+        /// <param name="sn">要查询的序列号(SN)。</param>
+        /// <param name="cancellationToken">可选的取消令牌,用于请求超时或取消。</param>
         /// <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>
-        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();
-            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");
 
-            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>
-        /// 向配置中指定的 MES 提交 URL 发起 GET 请求(用于提交数据/记录)。
-        /// 返回一个元组,<c>IsSuccess</c> 表示 HTTP 状态码是否为成功,<c>Response</c> 包含响应文本或错误信息。
+        /// 向配置中指定的 MES 提交 URL 发起 GET 请求(用于提交测试结果/记录)。
         /// </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>
-        /// 异步任务,结果为 (<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>
-        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();
-            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");
 
-            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>
         /// 释放服务占用的托管资源(当前仅 <see cref="_httpClient"/>)。
-        /// 本方法为幂等操作,重复调用不会产生异常。
+        /// <para>本方法为幂等操作,重复调用不会产生异常。调用后实例不可再用于发起请求,尝试使用将抛出 <see cref="ObjectDisposedException"/>。</para>
         /// </summary>
         public void Dispose()
         {