using System; using System.Collections.Generic; using System.IO; using System.Net.Http; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; using AlibabaCloud.SDK.Dingtalkoauth2_1_0.Models; using MQTT_WinformV1; using MySql.Data.MySqlClient; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Tea; using YiDa_WinForm.Config; using YiDa_WinForm.Model; using YiDa_WinForm.Utils; namespace YiDa_WinForm.Service { // 连杆上传状态事件参数(原有,保留不变) public class RodUploadStatusEventArgs : EventArgs { /// /// 是否上传成功(用于更新 MainForm 的 _isImageUploadedInCycle 标记) /// public bool IsUploadSuccess { get; set; } /// /// 附加消息(可选,如上传失败原因) /// public string Message { get; set; } = string.Empty; } public class YiDaUploadService { private readonly ButtonOperationService _buttonService; // 数据库连接配置 private readonly string _connectionString = AppConfigHelper.MySqlConnectionString; // 通知UI层 public event Action MessageReceived; // 钉钉token static readonly string _tokenAppKey = AppConfigHelper.YiDa_TokenAppKey; static readonly string _tokenAppSecret = AppConfigHelper.YiDa_TokenAppSecret; // 宜搭上传配置 static readonly string _appType = AppConfigHelper.YiDa_AppType; static readonly string _systemToken = AppConfigHelper.YiDa_SystemToken; static readonly string _userId = AppConfigHelper.YiDa_UserId; static readonly string _formUuid = AppConfigHelper.YiDa_FormUuid; static readonly string _processCode = AppConfigHelper.YiDa_ProcessCode; // 报废图片 public string _reformationPicture { get; set; } = string.Empty; // 钉钉配置 static readonly string _baseAddress = AppConfigHelper.DingDing_BaseAddress; static readonly string _dingDingWebHook = AppConfigHelper.DingDing_RobotWebHook; static readonly string _dingDingSecret = AppConfigHelper.DingDing_RobotSecret; // 定义公共事件(供 MainForm 订阅,连杆+报废) public event EventHandler RodUploadStatusChanged; public event EventHandler ScrapUploadStatusChanged; // 构造函数(初始化 _buttonService) public YiDaUploadService() { _buttonService = new ButtonOperationService(); } // ==================================== 宜搭上传(原有代码,保留不变) ==================================== private static AlibabaCloud.SDK.Dingtalkoauth2_1_0.Client CreateClient() { AlibabaCloud.OpenApiClient.Models.Config config = new AlibabaCloud.OpenApiClient.Models.Config(); config.Protocol = "https"; config.RegionId = "central"; return new AlibabaCloud.SDK.Dingtalkoauth2_1_0.Client(config); } public string GetDingDingToken() { string strToken = string.Empty; GetAccessTokenResponse token = null; var client = CreateClient(); var getAccessTokenRequest = new GetAccessTokenRequest { AppKey = _tokenAppKey, AppSecret = _tokenAppSecret, }; try { token = client.GetAccessToken(getAccessTokenRequest); } catch (TeaException err) { string errorMsg = $"获取钉钉Token异常【TeaException】:错误码={err.Code},错误信息={err.Message}"; Console.WriteLine(errorMsg); LogHelper.AppendLog(errorMsg); } catch (Exception ex) { string errorMsg = $"获取 token 异常:{ex.Message}"; Console.WriteLine(errorMsg); LogHelper.AppendLog(errorMsg); } return token != null ? token.Body.AccessToken : string.Empty; } private static async Task RecordSuccessLog(List mqttLists) { if (mqttLists != null && mqttLists.Count > 0) { var buttonService = new ButtonOperationService(); await buttonService.CreateSuccessLog(mqttLists); } } private static AlibabaCloud.SDK.Dingtalkyida_2_0.Client CreateYiDaClient() { AlibabaCloud.OpenApiClient.Models.Config config = new AlibabaCloud.OpenApiClient.Models.Config(); config.Protocol = "https"; config.RegionId = "central"; return new AlibabaCloud.SDK.Dingtalkyida_2_0.Client(config); } public async Task UploadDatabaseDataToYiDaWithLogging(string accessToken, List yidaLists, List mqttLists, MainForm form) { var client = CreateYiDaClient(); AlibabaCloud.SDK.Dingtalkyida_2_0.Models.StartInstanceHeaders startInstanceHeaders = new AlibabaCloud.SDK.Dingtalkyida_2_0.Models.StartInstanceHeaders(); startInstanceHeaders.XAcsDingtalkAccessToken = accessToken; for (int i = 0; i < yidaLists.Count; i++) { var item = yidaLists[i]; var mqttItem = mqttLists[i]; var dataDict = new Dictionary { { "textField_mha98neu", item.textField_mha98neu }, //供应商代码 { "textField_mha98nev", item.textField_mha98nev }, //供应商名称 { "textField_mha98new", item.textField_mha98new }, //车型 { "textField_mha98nex", item.textField_mha98nex }, //零件号 { "textField_mha98ney", item.textField_mha98ney }, //零件名称 { "textField_mha98nf1", item.textField_mha98nf1 }, //参数名 { "textField_mhx44i2i", item.textField_mhx44i2i }, //参数值 { "textField_mhx44i2j", item.textField_mhx44i2j }, //下公差 { "textField_mhx44i2k", item.textField_mhx44i2k }, //上公差 { "textField_mha98nf7", item.textField_mha98nf7 }, //零件责任人 { "textField_mhlvt8ht", DateTime.Now.ToString("yyyy-MM-dd") }, //写入时间 { "textField_mha98nf5", item.textField_mha98nf5 }, //合格判断 { "textField_mha98nf0", item.textField_mha98nf0 }, //工位 { "textField_mha98nfh", item.textField_mha98nfh } //外保负责人 }; string jsonData = JsonConvert.SerializeObject(dataDict); AlibabaCloud.SDK.Dingtalkyida_2_0.Models.StartInstanceRequest startInstanceRequest = new AlibabaCloud.SDK.Dingtalkyida_2_0.Models.StartInstanceRequest { AppType = _appType, SystemToken = _systemToken, UserId = _userId, Language = "zh_CN", FormUuid = _formUuid, FormDataJson = jsonData, ProcessCode = _processCode, }; try { var response = client.StartInstanceWithOptions(startInstanceRequest, startInstanceHeaders, new AlibabaCloud.TeaUtil.Models.RuntimeOptions()); if (response != null && response.Body != null) { await RecordSuccessLog(mqttLists); string logData = JsonConvert.SerializeObject(dataDict, Formatting.Indented); LogHelper.AppendLog($"上传数据成功:\r\n{logData}"); Console.WriteLine($"上传数据成功:\r\n{logData}"); } else { Console.WriteLine($"上传返回空结果,数据未存入 MySQL。"); string logData = JsonConvert.SerializeObject(dataDict, Formatting.Indented); LogHelper.AppendLog($"上传返回空结果,数据未存入 MySQL:\r\n{logData}"); } } catch (TeaException err) { Console.WriteLine($"上传异常:{err.Code} --- {err.Message}"); string logData = JsonConvert.SerializeObject(dataDict, Formatting.Indented); LogHelper.AppendLog($"上传异常:{err.Code} --- {err.Message}\r\n数据:\r\n{logData}"); } catch (Exception ex) { Console.WriteLine($"上传异常:{ex.Message}"); string logData = JsonConvert.SerializeObject(dataDict, Formatting.Indented); LogHelper.AppendLog($"上传异常:{ex.Message}\r\n数据:\r\n{logData}"); } } } // ==================================== 核心修复:签名生成方法(补充URL编码) ==================================== static string CalcSign(long timestamp, string secret) { if (string.IsNullOrWhiteSpace(secret)) { throw new ArgumentNullException(nameof(secret), "钉钉机器人密钥不能为空"); } string stringToSign = $"{timestamp}\n{secret}"; var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret)); byte[] hashBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign)); string base64Sign = Convert.ToBase64String(hashBytes); return base64Sign; } // ==================================== 报废证据上传(稳定成功版 + 事件触发) ==================================== public async Task UploadScrapCertificate(string token, string filePath) { try { if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath)) { string errorMsg = "报废图片路径为空或文件不存在"; OnScrapUploadStatusChanged(false, errorMsg); return; } // 1. 初始化 HttpClient client = new HttpClient { BaseAddress = new Uri(_baseAddress) }; var form = new MultipartFormDataContent(); var fileStream = File.OpenRead(filePath); var streamContent = new StreamContent(fileStream); streamContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("image/jpeg"); // 或 image/png form.Add(streamContent, "media", Path.GetFileName(filePath)); form.Add(new StringContent("image"), "type"); // 接口要求字段 type=image // 2. 发送请求上传文件(access_token 放查询串) var url = $"/media/upload?access_token={token}"; var resp = await client.PostAsync(url, form); resp.EnsureSuccessStatusCode(); string json = await resp.Content.ReadAsStringAsync(); JObject jo = JObject.Parse(json); string mediaId = jo["media_id"].Value(); LogHelper.AppendLog($"报废图片上传完成,mediaId = {mediaId}"); // 3. 发送群消息 long ts = DateTimeOffset.Now.ToUnixTimeMilliseconds(); string ymdhms = DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss"); string sign = CalcSign(ts, _dingDingSecret); string webhook_url = $"{_dingDingWebHook}×tamp={ts}&sign={sign}"; var msg = new { msgtype = "markdown", markdown = new { title = "报废证据上传", text = $"## 报废凭证照片\n\n\n> 时间:{ymdhms}" } }; string transJson = JsonConvert.SerializeObject(msg); var resp2 = await client.PostAsync(webhook_url, new StringContent(transJson, Encoding.UTF8, "application/json")); resp2.EnsureSuccessStatusCode(); string json2 = await resp2.Content.ReadAsStringAsync(); JObject res2 = JObject.Parse(json2); string errmsg = res2["errmsg"].Value(); LogHelper.AppendLog($"报废图片发送群消息完成,msg = {errmsg}"); } catch (Exception ex) { string errorMsg = $"报废凭证图片上传异常:{ex.Message}"; LogHelper.AppendLog(errorMsg); throw; } } // ==================================== 连杆测试上传(和报废上传完全对齐,稳定成功 + 事件触发) ==================================== public async Task ConnectingRodTestUpload(string token, string filePath) { try { if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath)) { string errorMsg = "连杆图片路径为空或文件不存在"; OnRodUploadStatusChanged(false, errorMsg); return; } // 1. 初始化 HttpClient client = new HttpClient { BaseAddress = new Uri(_baseAddress) }; var form = new MultipartFormDataContent(); var fileStream = File.OpenRead(filePath); var streamContent = new StreamContent(fileStream); streamContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("image/jpeg"); // 或 image/png form.Add(streamContent, "media", Path.GetFileName(filePath)); form.Add(new StringContent("image"), "type"); // 接口要求字段 type=image // 2. 发送请求上传文件(access_token 放查询串) var url = $"/media/upload?access_token={token}"; var resp = await client.PostAsync(url, form); resp.EnsureSuccessStatusCode(); string json = await resp.Content.ReadAsStringAsync(); JObject jo = JObject.Parse(json); string mediaId = jo["media_id"].Value(); LogHelper.AppendLog($"连杆图片上传完成,mediaId = {mediaId}"); // 3. 发送群消息 long ts = DateTimeOffset.Now.ToUnixTimeMilliseconds(); string ymdhms = DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss"); string sign = CalcSign(ts, _dingDingSecret); // HMACSHA256→Base64 string webhook_url = $"{_dingDingWebHook}×tamp={ts}&sign={sign}"; var msg = new { msgtype = "markdown", markdown = new { title = "连杆测试上传", text = $"## 连杆测试照片\n\n\n> 时间:{ymdhms}" } }; string transJson = JsonConvert.SerializeObject(msg); var resp2 = await client.PostAsync(webhook_url, new StringContent(transJson, Encoding.UTF8, "application/json")); resp2.EnsureSuccessStatusCode(); string json2 = await resp2.Content.ReadAsStringAsync(); JObject res2 = JObject.Parse(json2); string errmsg = res2["errmsg"].Value(); LogHelper.AppendLog($"连杆图片发送群消息完成,msg = {errmsg}"); } catch (Exception ex) { string errorMsg = $"连杆测试图片上传异常:{ex.Message}"; LogHelper.AppendLog(errorMsg); throw; } } // ==================================== 钉钉报警(支持加签,稳定发送,适配连杆+报废) ==================================== public async Task SendDingDingTextAlarmAsync(string alarmContent, bool isRodAlarm = true) { // 前置校验 if (string.IsNullOrEmpty(alarmContent)) { LogHelper.AppendLog("钉钉报警失败:报警内容不能为空"); return false; } // 初始化 HttpClient HttpClient client = new HttpClient { BaseAddress = new Uri(_baseAddress) }; try { // 4. 生成加签参数 long ts = DateTimeOffset.Now.ToUnixTimeMilliseconds(); string ymdhms = DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss"); string sign = CalcSign(ts, _dingDingSecret); string webhook_url = $"{_dingDingWebHook}×tamp={ts}&sign={sign}"; // 5. 构造报警消息(区分连杆/报废) string alarmTitle = isRodAlarm ? "连杆测试报警" : "报废上传报警"; string alarmDesc = isRodAlarm ? "未及时提供强度测试图片" : "未及时提供报废图片"; var msg = new { msgtype = "markdown", markdown = new { title = alarmTitle, text = $"## **{alarmTitle}**
报警内容:{alarmContent}" } }; // 6. 序列化消息 string transJson = JsonConvert.SerializeObject(msg); LogHelper.AppendLog($"钉钉报警:构造的 JSON 消息:\r\n{transJson}"); // 7. 发送请求 var resp2 = await client.PostAsync(webhook_url, new StringContent(transJson, Encoding.UTF8, "application/json")); resp2.EnsureSuccessStatusCode(); // 8. 解析响应结果 string json2 = await resp2.Content.ReadAsStringAsync(); JObject res2 = JObject.Parse(json2); string errmsg = res2["errmsg"].Value(); LogHelper.AppendLog($"钉钉报警发送完成,msg = {errmsg}"); // 9. 判断是否真正成功 return errmsg.Equals("ok", StringComparison.OrdinalIgnoreCase); } catch (Exception ex) { LogHelper.AppendLog($"钉钉报警异常:{ex.Message},堆栈信息:{ex.StackTrace}"); return false; } finally { // 10. 释放资源 client.Dispose(); } } // ==================================== 连杆上传内部封装 + 事件触发(优化稳定性) ==================================== public async Task UploadRodTestImageInternalAsync(string filePath) { try { if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath)) { string errorMsg = "图片路径为空或文件不存在"; OnRodUploadStatusChanged(false, errorMsg); return false; } string token = GetDingDingToken(); if (string.IsNullOrEmpty(token)) { string errorMsg = "获取 DingDing Token 失败,无法执行上传"; OnRodUploadStatusChanged(false, errorMsg); return false; } await ConnectingRodTestUpload(token, filePath); string successMsg = "连杆测试图片上传成功,已发送至钉钉群"; OnRodUploadStatusChanged(true, successMsg); return true; } catch (Exception ex) { string errorMsg = $"连杆测试图片上传异常:{ex.Message}"; LogHelper.AppendLog(errorMsg); OnRodUploadStatusChanged(false, errorMsg); return false; } } // ==================================== 报废上传内部封装 + 事件触发(和连杆对齐) ==================================== public async Task UploadScrapImageInternalAsync(string filePath) { try { if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath)) { string errorMsg = "图片路径为空或文件不存在"; OnScrapUploadStatusChanged(false, errorMsg); return false; } string token = GetDingDingToken(); if (string.IsNullOrEmpty(token)) { string errorMsg = "获取 DingDing Token 失败,无法执行上传"; OnScrapUploadStatusChanged(false, errorMsg); return false; } await UploadScrapCertificate(token, filePath); string successMsg = "报废凭证图片上传成功,已发送至钉钉群"; OnScrapUploadStatusChanged(true, successMsg); return true; } catch (Exception ex) { string errorMsg = $"报废凭证图片上传异常:{ex.Message}"; LogHelper.AppendLog(errorMsg); OnScrapUploadStatusChanged(false, errorMsg); return false; } } // ==================================== 事件触发辅助方法(连杆+报废) ==================================== protected virtual void OnRodUploadStatusChanged(bool isUploadSuccess, string message) { RodUploadStatusChanged?.Invoke(this, new RodUploadStatusEventArgs { IsUploadSuccess = isUploadSuccess, Message = message }); } protected virtual void OnScrapUploadStatusChanged(bool isUploadSuccess, string message) { ScrapUploadStatusChanged?.Invoke(this, new ScrapUploadStatusEventArgs { IsUploadSuccess = isUploadSuccess, Message = message }); } } }