using Prism.Events; using Prism.Ioc; using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Media.Media3D; using TeamAAS_VP.Enums; using TeamAAS_VP.Events; using TeamAAS_VP.Interfaces; using TeamAAS_VP.Models; namespace TeamAAS_VP.Services { /// /// 基于 的 MES 通信服务实现。 /// /// 本服务通过注入的 获取设备配置(包含 MES 接口地址), /// 提供对站点查询(StationGetAsync)和提交(SubmitGetAsync)的 GET 请求封装并带有重试逻辑。 /// /// /// 注意: /// - 为可复用的单一实例,建议在服务生命周期内重用以避免端口耗尽。 /// - 本类实现了显式的 方法以释放内部 资源,调用后实例不可再用。 /// /// public class MesService : IMesService { IEventAggregator _eventAggregator; /// /// 用于执行 HTTP 请求的客户端实例。建议在服务生命周期内重用,避免频繁创建导致套接字/端口耗尽。 /// private readonly HttpClient _httpClient; /// /// 配置服务,用于获取设备信息(包含 MES URL、是否启用 MES 等)。 /// private readonly IConfigService _configService; /// /// 标记实例是否已释放,防止重复释放和在释放后继续使用导致未定义行为。 /// private bool _disposed; /// /// 使用指定的配置服务创建 实例。 /// /// 用于获取设备配置信息的实现,不能为空。 /// 为 null 时抛出。 public MesService(IConfigService configService, IContainerProvider container, IEventAggregator ea) { _configService = configService ?? throw new ArgumentNullException(nameof(configService)); _httpClient = new HttpClient(); _disposed = false; _eventAggregator = ea; } /// /// 向配置中指定的 MES 站点 URL 发起 GET 请求(用于过站查询)。 /// /// 要查询的序列号(SN)。 /// 可选的取消令牌,用于请求超时或取消。 /// /// 返回元组 (IsSuccess, Response): /// - IsSuccess: 请求是否视为成功(即 MES 返回了预期的 "SFC_OK" 且包含 "unit_process_check=OK")。 /// - Response: 原始响应文本或错误信息说明(如 "MES功能未启用!"、"Missing MES station URL"、"Canceled" 等)。 /// /// /// 行为说明: /// - 从配置获取设备信息,若 MES 未启用则直接返回 IsSuccess=true 且 Response 为提示文本(不视为错误)。 /// - 若未配置 MES URL 则返回 IsSuccess=false 与错误文本。 /// - 使用配置的 URL(通过 填充 sn)发起 GET 请求,最多重试 10 次。 /// - 只有当响应包含 "SFC_OK" 且包含 "unit_process_check=OK" 时视为成功并立即返回。 /// - 捕获 返回 "Canceled",捕获其它异常则返回异常消息。 /// - 若实例已被释放则抛出 。 /// public async Task<(bool IsSuccess, string Response)> StationGetAsync(string sn, string comp, CancellationToken cancellationToken = default) { // 如果已经释放,则抛出异常,防止在已释放资源上继续操作。 if (_disposed) throw new ObjectDisposedException(nameof(MesService)); var device = _configService.GetDeviceInfo(0); if (device == null || !device.EnableMES) { return (true, "MES功能未启用!"); } if (string.IsNullOrWhiteSpace(device.MesUrl)) return (false, "Missing MES station URL"); if (device.F == "PTL") { if (string.IsNullOrEmpty(device.comp)) { return (false, "PTL模式下comp不能为空"); } } string url = ""; if (device.F == "PTL") { url = $"{device.MesUrl}?c=QUERY_RECORD&line={device.Line}&station={device.Station}&fixtureid={device.FixtureId}&sn={sn}&comp={device.comp}:{comp}&p=unit_process_check,message,model,sn,wo,model_num,color,ppid,stage,KB"; } else { url = $"{device.MesUrl}?c=QUERY_RECORD&line={device.Line}&station={device.Station}&fixtureid={device.FixtureId}&sn={sn}&p=unit_process_check,message,model,sn,wo,model_num,color,ppid,stage,KB"; } LogHelper.WriteLogMes($"【询问URL】{url}"); SendTaskMessage($"【询问URL】{url}", MessageLevel.Info); (bool IsSuccess, string Response) result = (false, string.Empty); for (int i = 0; i < 3; i++) { try { using (var rsp = await _httpClient.GetAsync(url)) { var text = await rsp.Content.ReadAsStringAsync(); LogHelper.WriteLogMes($"【HTTP接收】{text}"); SendTaskMessage($"【HTTP接收】{text}", MessageLevel.Info); // "SFC_OK" 代表和 MES 连接成功 if (text.Contains("SFC_OK")) { string[] item = text.Split('\n'); //获取产品主sn和wo用于后续上传数据使用 string snItem = item.FirstOrDefault(f => f.Contains("sn=")); string woItem = item.FirstOrDefault(f => f.Contains("wo=")); if (snItem != null && woItem != null && text.Contains("message=OK")) { return (true, text); ; } } result = (false, text); } } catch (OperationCanceledException) { result = (false, "Canceled"); } catch (Exception ex) { result = (false, ex.Message); } } return result; } /// /// 向 MES 发起站点查询请求,使用超时(秒)作为便捷重载。 /// /// 序列号(SN)。 /// 请求超时,单位为秒。 /// 相同的返回语义。 public async Task<(bool IsSuccess, string Response)> StationGetAsync(string sn, string comp, double timeoutInSeconds) { using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutInSeconds))) { return await StationGetAsync(sn, comp, cts.Token); } } public async Task<(bool IsSuccess, string Response)> StationGetExAsync(string sn, string comp, double timeoutInSeconds) { using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutInSeconds))) { return await StationGetExAsync(sn, comp, cts.Token); } } /// /// 向配置中指定的 MES 站点 URL 发起 GET 请求(用于过站查询)。 /// /// 要查询的序列号(SN)。 /// 可选的取消令牌,用于请求超时或取消。 /// /// 返回元组 (IsSuccess, Response): /// - IsSuccess: 请求是否视为成功(即 MES 返回了预期的 "SFC_OK" 且包含 "unit_process_check=OK")。 /// - Response: 原始响应文本或错误信息说明(如 "MES功能未启用!"、"Missing MES station URL"、"Canceled" 等)。 /// /// /// 行为说明: /// - 从配置获取设备信息,若 MES 未启用则直接返回 IsSuccess=true 且 Response 为提示文本(不视为错误)。 /// - 若未配置 MES URL 则返回 IsSuccess=false 与错误文本。 /// - 使用配置的 URL(通过 填充 sn)发起 GET 请求,最多重试 10 次。 /// - 只有当响应包含 "SFC_OK" 且包含 "unit_process_check=OK" 时视为成功并立即返回。 /// - 捕获 返回 "Canceled",捕获其它异常则返回异常消息。 /// - 若实例已被释放则抛出 。 /// public async Task<(bool IsSuccess, string Response)> StationGetExAsync(string sn, string comp, CancellationToken cancellationToken = default) { // 如果已经释放,则抛出异常,防止在已释放资源上继续操作。 if (_disposed) throw new ObjectDisposedException(nameof(MesService)); var device = _configService.GetDeviceInfo(0); if (device == null || !device.EnableMES) { return (true, "MES功能未启用!"); } if (string.IsNullOrWhiteSpace(device.MesUrl)) return (false, "Missing MES station URL"); string url = ""; url = $"{device.MesUrl}?c=QUERY_RECORD&line={device.Line}&station={device.Station}&fixtureid={device.FixtureId}&sn={sn}&p=sn,message"; LogHelper.WriteLogMes($"【询问URL】{url}"); SendTaskMessage($"【询问URL】{url}", MessageLevel.Info); (bool IsSuccess, string Response) result = (false, string.Empty); for (int i = 0; i < 3; i++) { try { using (var rsp = await _httpClient.GetAsync(url)) { var text = await rsp.Content.ReadAsStringAsync(); LogHelper.WriteLogMes($"【HTTP接收】{text}"); SendTaskMessage($"【HTTP接收】{text}", MessageLevel.Info); // "SFC_OK" 代表和 MES 连接成功 if (text.Contains("SFC_OK")) { if (text.Contains("message=OK")) { return (true, text); } } result = (false, text); } } catch (OperationCanceledException) { result = (false, "Canceled"); } catch (Exception ex) { result = (false, ex.Message); } } return result; } /// /// 向配置中指定的 MES 提交 URL 发起 GET 请求(用于提交测试结果/记录)。 /// /// 要提交的序列号(SN)。 /// 表示此次记录是否通过(true => PASS,false => FAIL)。 /// 开始时间,提交时使用 "yyyy-MM-dd HH:mm:ss" 格式。 /// 结束时间,提交时使用 "yyyy-MM-dd HH:mm:ss" 格式。 /// 可选的取消令牌,用于请求超时或取消。 /// /// 返回元组 (IsSuccess, Response): /// - IsSuccess: 提交是否被 MES 视为成功(收到 "SFC_OK" 且响应中无额外多行信息)。 /// - Response: MES 返回文本或错误说明;当 MES 返回 "SFC_OK" 但包含额外多行信息(代表 SN 数据问题)时,IsSuccess 为 false 且 Response 为该文本。 /// /// /// 行为说明: /// - 从配置获取设备信息,若 MES 未启用则直接返回 IsSuccess=true 且 Response 为提示文本(不视为错误)。 /// - 若未配置 MES 提交 URL 则返回 IsSuccess=false 与错误文本。 /// - 使用配置的 URL(通过 按顺序填充 sn、PASS/FAIL、start_time、stop_time)发起 GET 请求,最多重试 10 次。 /// - 在收到包含 "SFC_OK" 的响应时,如果返回文本包含多行(有附加信息)则视为提交失败并返回该文本,否则视为提交成功。 /// - 捕获 返回 "Canceled",捕获其它异常则返回异常消息。 /// - 若实例已被释放则抛出 。 /// public async Task<(bool IsSuccess, string Response)> SubmitGetAsync(int deviceNo,string sn, string comp, string mesdata, bool isSuccess,string submitDataError, DateTime start_time, DateTime stop_time, CancellationToken cancellationToken = default) { // 如果已经释放,则抛出异常,防止在已释放资源上继续操作。 if (_disposed) throw new ObjectDisposedException(nameof(MesService)); string ErrorData = submitDataError; var device = _configService.GetDeviceInfo(deviceNo); if (device == null) device = _configService.GetDeviceInfo(); if (device == null || !device.EnableMES) { return (true, "MES功能未启用!"); } if (string.IsNullOrWhiteSpace(device.MesUrl)) return (false, "Missing MES submit URL"); if (device.F == "PTL") { if (string.IsNullOrEmpty(device.comp)) { return (false, "PTL模式下comp不能为空"); } } string res = isSuccess ? "PASS" : "FAIL"; //if (res == "FAIL") //{ // res = ErrorData; // //res = "A1-gap-Ctrl2_4_Fail"; //} string url = ""; if (device.F == "PTL") { url = $"{device.MesUrl}?c=ADD_RECORD&line={device.Line}&station={device.Station}&fixtureid={device.FixtureId}&sn={sn}&f=PTL&comp={device.comp}:{comp}" + $"&Mesdata{mesdata}result={res}&start_time={start_time.ToString("yyyy-MM-dd HH:mm:ss")}&stop_time={stop_time.ToString("yyyy-MM-dd HH:mm:ss")}"; } else { url = $"{device.MesUrl}?c=ADD_RECORD&line={device.Line}&station={device.Station}&fixtureid={device.FixtureId}&sn={sn}&f=PQC" + $"&Mesdata{mesdata}result={res}&start_time={start_time.ToString("yyyy-MM-dd HH:mm:ss")}&stop_time={stop_time.ToString("yyyy-MM-dd HH:mm:ss")}"; } LogHelper.WriteLogMes($"【上传URL】{url}"); SendTaskMessage($"【上传URL】{url}", MessageLevel.Info); (bool IsSuccess, string Response) result = (false, string.Empty); for (int i = 0; i < 3; i++) { try { using (var rsp = await _httpClient.GetAsync(url, cancellationToken)) { var text = await rsp.Content.ReadAsStringAsync(); LogHelper.WriteLogMes($"【HTTP接收】{text}"); SendTaskMessage($"【HTTP接收】{text}", MessageLevel.Info); // "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); } } return result; } /// /// 向配置中指定的 MES 提交 URL 发起 GET 请求(用于提交测试结果/记录)。 /// /// 要提交的序列号(SN)。 /// 表示此次记录是否通过(true => PASS,false => FAIL)。 /// 开始时间,提交时使用 "yyyy-MM-dd HH:mm:ss" 格式。 /// 结束时间,提交时使用 "yyyy-MM-dd HH:mm:ss" 格式。 /// 可选的取消令牌,用于请求超时或取消。 /// /// 返回元组 (IsSuccess, Response): /// - IsSuccess: 提交是否被 MES 视为成功(收到 "SFC_OK" 且响应中无额外多行信息)。 /// - Response: MES 返回文本或错误说明;当 MES 返回 "SFC_OK" 但包含额外多行信息(代表 SN 数据问题)时,IsSuccess 为 false 且 Response 为该文本。 /// /// /// 行为说明: /// - 从配置获取设备信息,若 MES 未启用则直接返回 IsSuccess=true 且 Response 为提示文本(不视为错误)。 /// - 若未配置 MES 提交 URL 则返回 IsSuccess=false 与错误文本。 /// - 使用配置的 URL(通过 按顺序填充 sn、PASS/FAIL、start_time、stop_time)发起 GET 请求,最多重试 10 次。 /// - 在收到包含 "SFC_OK" 的响应时,如果返回文本包含多行(有附加信息)则视为提交失败并返回该文本,否则视为提交成功。 /// - 捕获 返回 "Canceled",捕获其它异常则返回异常消息。 /// - 若实例已被释放则抛出 。 /// public async Task<(bool IsSuccess, string Response)> SubmitGetAsync(string sn, string comp, string assypress, string cc, string height, bool isSuccess, DateTime start_time, DateTime stop_time, CancellationToken cancellationToken = default) { // 如果已经释放,则抛出异常,防止在已释放资源上继续操作。 if (_disposed) throw new ObjectDisposedException(nameof(MesService)); var device = _configService.GetDeviceInfo(0); if (device == null || !device.EnableMES) { return (true, "MES功能未启用!"); } if (string.IsNullOrWhiteSpace(device.MesUrl)) return (false, "Missing MES submit URL"); if (device.F == "PTL") { if (string.IsNullOrEmpty(device.comp)) { return (false, "PTL模式下comp不能为空"); } } //string res = isSuccess ? "PASS" : "FAIL"; string res = "PASS"; string url = ""; if (device.F == "PTL") { url = $"{device.MesUrl}?c=ADD_RECORD&line={device.Line}&station={device.Station}&fixtureid={device.FixtureId}&sn={sn}&f=PTL&comp={device.comp}:{comp}" + $"&cc={cc}&laser_height={height}&result={res}&start_time={start_time.ToString("yyyy-MM-dd HH:mm:ss")}&stop_time={stop_time.ToString("yyyy-MM-dd HH:mm:ss")}"; } else { url = $"{device.MesUrl}?c=ADD_RECORD&line={device.Line}&station={device.Station}&fixtureid={device.FixtureId}&sn={sn}&f=PQC" + $"&cc={cc}&laser_height={height}&result={res}&start_time={start_time.ToString("yyyy-MM-dd HH:mm:ss")}&stop_time={stop_time.ToString("yyyy-MM-dd HH:mm:ss")}"; } LogHelper.WriteLogMes($"【上传URL】{url}"); SendTaskMessage($"【上传URL】{url}", MessageLevel.Info); (bool IsSuccess, string Response) result = (false, string.Empty); for (int i = 0; i < 3; i++) { try { using (var rsp = await _httpClient.GetAsync(url, cancellationToken)) { var text = await rsp.Content.ReadAsStringAsync(); LogHelper.WriteLogMes($"【HTTP接收】{text}"); SendTaskMessage($"【HTTP接收】{text}", MessageLevel.Info); // "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); } } return result; } /// /// 向 MES 提交请求的超时重载,使用超时(秒)作为便捷参数。 /// /// 序列号(SN)。 /// 是否通过(PASS/FAIL)。 /// 开始时间。 /// 结束时间。 /// 请求超时,单位为秒。 /// 相同的返回语义。 public async Task<(bool IsSuccess, string Response)> SubmitGetAsync(string sn, string comp, string assypress, string cc, string height, bool isSuccess, DateTime start_time, DateTime stop_time, double timeoutInSeconds) { using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutInSeconds))) { return await SubmitGetAsync(sn, comp, assypress, cc, height, isSuccess, start_time, stop_time, cts.Token); } } /// /// 获取服务器当前时间 /// /// /// /// public async Task<(bool IsSuccess, DateTime Now)> GetServerTimeAsync(string sn, double timeoutInSeconds) { // 如果已经释放,则抛出异常,防止在已释放资源上继续操作。 if (_disposed) throw new ObjectDisposedException(nameof(MesService)); var device = _configService.GetDeviceInfo(0); if (device == null) device = _configService.GetDeviceInfo(0); if (device == null || !device.EnableMES) { return (true, DateTime.UtcNow); } if (string.IsNullOrWhiteSpace(device.MesUrl)) return (false, DateTime.UtcNow); string url = ""; url = $"{device.MesUrl}?c=QUERY_RECORD&line={device.Line}&station={device.Station}&fixtureid={device.FixtureId}&sn={sn}&p=mes_server_time"; LogHelper.WriteLogMes($"【询问URL】{url}"); SendTaskMessage($"【询问URL】{url}", MessageLevel.Info); (bool IsSuccess, DateTime Now) result = (false, DateTime.UtcNow); for (int i = 0; i < 3; i++) { try { using (var rsp = await _httpClient.GetAsync(url)) { var text = await rsp.Content.ReadAsStringAsync(); LogHelper.WriteLogMes($"【HTTP接收】{text}"); SendTaskMessage($"【HTTP接收】{text}", MessageLevel.Info); // "SFC_OK" 代表和 MES 连接成功 if (text.Contains("SFC_OK")) { //按照分隔符'/n'分割字符串 string[] lines = text.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); //通过linq查找是否包含"mes_server_time"的行 var timeLine = lines.FirstOrDefault(l => l.Contains("mes_server_time")); if (timeLine != null) { // 提取时间字符串 var timeString = timeLine.Split('=')[1].Trim(); if (DateTime.TryParse(timeString, out DateTime serverTime)) { return (true, serverTime); } } } result = (false, DateTime.UtcNow); } } catch (OperationCanceledException) { result = (false, DateTime.UtcNow); } catch (Exception ex) { result = (false, DateTime.UtcNow); } } return result; } /// /// 释放服务占用的托管资源(当前仅 )。 /// 本方法为幂等操作,重复调用不会产生异常。调用后实例不可再用于发起请求,尝试使用将抛出 /// public void Dispose() { if (!_disposed) { _httpClient.Dispose(); _disposed = true; } } public void SendTaskMessage(string msg, MessageLevel level) { App.Current.Dispatcher.Invoke(() => { _eventAggregator.GetEvent().Publish(new Models.MessageStruct() { Message = msg, level = level }); }); } /// /// 上传log服务器 /// /// /// /// /// /// public async Task<(bool IsSuccess, string Response)> SendFtpFileAsync(string sn, string name, CancellationToken cancellationToken = default) { // 如果已经释放,则抛出异常,防止在已释放资源上继续操作。 if (_disposed) throw new ObjectDisposedException(nameof(MesService)); var device = _configService.GetDeviceInfo(0); if (device == null || !device.EnableMES) { return (true, "MES功能未启用!"); } if (string.IsNullOrWhiteSpace(device.LogUrl)) { return (false, "Missing MES station LogUrl"); } string url = $"{device.LogUrl}?db=mysql&ApiKey={device.ApiKey}&jsondata={{\"SN\":\"{sn}\",\"LINE\":\"{device.Line}\",\"STATION\":\"{device.Station}\",\"TESTRESULT\":\"PASS\",\"FIXTUREID\":\"{device.FixtureId}\",\"TESTDATETIME\":\"{DateTime.Now.ToString()}\",\"LOGFILENAME\":\"{name}.zip\",\"STATIONTYPE\":\"AE\"}}"; LogHelper.WriteLogMes($"【上传LogData的URL】{url}"); SendTaskMessage($"【上传LogData的URL】{url}", MessageLevel.Info); (bool IsSuccess, string Response) result = (false, string.Empty); for (int i = 0; i < 3; i++) { try { using (var rsp = await _httpClient.GetAsync(url, cancellationToken)) { var text = await rsp.Content.ReadAsStringAsync(); LogHelper.WriteLogMes($"【上传LogData的HTTP接收】{text}"); SendTaskMessage($"【上传LogData的HTTP接收】{text}", MessageLevel.Info); // "SFC_OK" 代表和 MES 连接成功 if (text.Contains("\"Code\":1")) { return (true, text); } result = (false, text); } } catch (OperationCanceledException) { result = (false, "Canceled"); } catch (Exception ex) { result = (false, ex.Message); } } return result; } } }