C#笔记(1、钉钉机器人推送帆软报表图片)
C#笔记——钉钉机器人推送帆软报表图片(一)
1、前言
2024年最后一个月,家里多了个小公主,在家休息了一段时间。2025年,休完假上班第一天,领导就说:哎,我看总部那边做了个每日产出的报表推送到钉钉群,来看计划达成率。我们基地这边能不能做啊。我心里默默一想,然后大声一喊能做(内心os:上家公司做过类似的功能,不过他们是自己的通讯工具,不是钉钉)。既然有了活,那就开干吧。最终忙活了几天,终于算是实现了这个功能吧,记录下整个过程,以及踩到的坑吧,最终实现效果如图。
2、下载帆软报表图片到本地
因为上家做过类似的功能,那就直接借鉴下之前的代码,先把帆软报表以图片格式下载到本地。大概过程如下
1、在帆软的报表访问页面的url后面添加
&format=image&extype=PNG
2、使用http请求访问,下载到本地,添加动态反射以用来传帆软报表的查询条件参数
3、裁剪图片留白区域,添加水印等
public async Task SendImageOutput() { try { //string fineReportUrl = "http://localhost:8075/webroot/decision/view/report?viewlet=tzreport/E04.cpt&format=image&extype=PNG"; string image_Path = "D:\\1"; string retMsg = string.Empty; DateTime dateTime = DateTime.Now; string time = dateTime.ToString("yyyyMMdd");//当前时间格式 string downPath = Path.Combine(image_Path, time);//下载到当前的路径 //方便删除历史的记录文件 if (!Directory.Exists(downPath)) { Directory.CreateDirectory(downPath); } string startdate = dateTime.AddDays(-7).ToString("yyyy-MM-dd"); string enddate = dateTime.ToString("yyyy-MM-dd"); string cSharpScript = $"string startdate= \"{startdate}\";string enddate= \"{enddate}\"; var paramlist =\"&startdate=\"+startdate+\"&\"+\"enddate=\"+enddate; return paramlist;"; string urlParam = FineReportUrlParam(cSharpScript); string url = fineReportUrl; if (!string.IsNullOrEmpty(urlParam)) { url += urlParam; } //下载图片 bool downResult = HttpDownloadImage(url, "C02产出", downPath, out string imageFullpath); if (downResult) { //发送数据到dingding ....... } else { retMsg = "执行失败!"; } } catch (Exception ex) { throw ex; } } /// <summary> /// http下载文件 /// </summary> /// <param name="url">下载文件地址</param> /// <param name="path">文件存放地址</param> /// <param name="isAddSecurity">是否标密</param> /// <param name="imageNamePrefix">保存的文件的前缀</param> /// <param name="imageFullPath">图片保存到的本地路径</param> /// <returns></returns> public bool HttpDownloadImage(string url, string imageNamePrefix, string path, out string imageFullPath) { try { HttpWebRequest? request = WebRequest.Create(url) as HttpWebRequest; //发送请求并获取相应回应数据 HttpWebResponse? response = request.GetResponse() as HttpWebResponse; string fileName = imageNamePrefix + "_" + DateTime.Now.ToString("yyyyMMddHHmmssFFF") + ".png"; string fileFullPath = Path.Combine(path, fileName); Stream responseStream = response.GetResponseStream(); ImageEditDomain imageEdit = new ImageEditDomain(); Bitmap imageBitmap = imageEdit.CutImageWhitePart(responseStream, 80, fileFullPath); Bitmap imageBitmapSecret = imageBitmap; Bitmap imageBitmapWord = imageEdit.AddWatermarkWord(imageBitmapSecret);//加有“MES系统自动发送”的图片 imageFullPath = string.Empty; if (imageBitmapWord == null) { return false; } else { imageBitmapWord.Save(fileFullPath, ImageFormat.Png); imageFullPath = fileFullPath; return true; } } catch (Exception ex) { imageFullPath = string.Empty; return false; } } /// <summary> /// 生成动态代码,方便调用 /// </summary> /// <param name="argMethodCode"></param> /// <returns></returns> public string GenerateCode(string argMethodCode) { StringBuilder sb = new StringBuilder(); sb.Append("using System;"); sb.Append(Environment.NewLine); sb.Append("namespace FineReport"); sb.Append(Environment.NewLine); sb.Append("{"); sb.Append(Environment.NewLine); sb.Append(" public class UrlParam"); sb.Append(Environment.NewLine); sb.Append(" {"); sb.Append(Environment.NewLine); sb.Append(" public string GetParam()"); sb.Append(Environment.NewLine); sb.Append(" {"); sb.Append(Environment.NewLine); sb.Append(argMethodCode); sb.Append(Environment.NewLine); sb.Append(" }"); sb.Append(Environment.NewLine); sb.Append(" }"); sb.Append(Environment.NewLine); sb.Append("}"); return sb.ToString(); } /// <summary> /// 获取执行动态C#代码的值 /// </summary> /// <param name="argCodeStr"></param> /// <returns></returns> public string FineReportUrlParam(string argCodeStr) { if (string.IsNullOrEmpty(argCodeStr)) { return string.Empty; } string code = GenerateCode(argCodeStr); // CSharpCodeProvider objCSharpCodePrivoder = new CSharpCodeProvider(); //// CSharpCompilation objCSharpCodePrivoder = new CSharpCompilation(); // CompilerParameters objCompilerParameters = new CompilerParameters(); // objCompilerParameters.ReferencedAssemblies.Add("System.dll"); // //objCompilerParameters.ReferencedAssemblies.Add("Newtonsoft.Json.dll"); // objCompilerParameters.GenerateExecutable = false; // objCompilerParameters.GenerateInMemory = true; // CompilerResults cresult = objCSharpCodePrivoder.CompileAssemblyFromSource(objCompilerParameters, code); // // 通过反射,执行代码 // Assembly objAssembly = cresult.CompiledAssembly; // object obj = objAssembly.CreateInstance("FineReport.UrlParam"); // MethodInfo objMI = obj.GetType().GetMethod("GetParam"); // return objMI.Invoke(obj, null)?.ToString(); // Parse the source code into a syntax tree. var syntaxTree = CSharpSyntaxTree.ParseText(code); // Define the references that your compiled code will need. var references = new[] { MetadataReference.CreateFromFile(typeof(object).Assembly.Location), // mscorlib/System.Runtime.dll MetadataReference.CreateFromFile(typeof(Console).Assembly.Location), // For example, if you use Console.WriteLine // Add other necessary references here, for example: // MetadataReference.CreateFromFile(typeof(Newtonsoft.Json.JsonConvert).Assembly.Location) // Newtonsoft.Json.dll }; // Create a compilation object with the parsed code and references. var compilation = CSharpCompilation.Create( assemblyName: Path.GetRandomFileName(), syntaxTrees: new[] { syntaxTree }, references: references, options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); // Emit the compiled code into a memory stream. using (var ms = new MemoryStream()) { var result = compilation.Emit(ms); if (!result.Success) { var failures = result.Diagnostics.Where(diagnostic => diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error); throw new Exception("Compilation failed: " + string.Join(Environment.NewLine, failures.Select(d => d.ToString()))); } else { ms.Seek(0, SeekOrigin.Begin); // Load the compiled assembly in a separate context to avoid locking issues. var loadContext = new AssemblyLoadContext(null, isCollectible: true); var assembly = loadContext.LoadFromStream(ms); // Use reflection to create an instance of the type and invoke its method. Type? type = assembly.GetType("FineReport.UrlParam"); if (type == null) throw new InvalidOperationException("Type 'FineReport.UrlParam' not found."); object obj = Activator.CreateInstance(type)!; MethodInfo? methodInfo = type.GetMethod("GetParam"); if (methodInfo == null) throw new InvalidOperationException("Method 'GetParam' not found."); return methodInfo.Invoke(obj, null)?.ToString(); } } }
using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Imaging; using System.Linq; using System.Text; using System.Threading.Tasks; namespace DingDingTest { internal class ImageEditDomain { /// <summary> /// 剪去图片空余白边 /// </summary> /// <param name="stream">源文件</param> /// <param name="WhiteBar">保留的白边,单位为像素</param> /// <param name="fullPath">保存到本地的地址</param> public Bitmap CutImageWhitePart(Stream stream, int WhiteBar, string fullPath) { Bitmap bmp = new Bitmap(stream); int top = 0, left = 0; int right = bmp.Width, bottom = bmp.Height; Color white = Color.White; //寻找最上面的标线,从左(0)到右,从上(0)到下 for (int i = 0; i < bmp.Height; i++)//行 { bool find = false; for (int j = 0; j < bmp.Width; j++)//列 { Color c = bmp.GetPixel(j, i); if (IsWhite(c)) { top = i; find = true; break; } } if (find) break; } //寻找最左边的标线,从上(top位)到下,从左到右 for (int i = 0; i < bmp.Width; i++)//列 { bool find = false; for (int j = top; j < bmp.Height; j++)//行 { Color c = bmp.GetPixel(i, j); if (IsWhite(c)) { left = i; find = true; break; } } if (find) break; ; } // 寻找最下边标线,从下到上,从左到右 for (int i = bmp.Height - 1; i >= 0; i--)//行 { bool find = false; for (int j = left; j < bmp.Width; j++)//列 { Color c = bmp.GetPixel(j, i); if (IsWhite(c)) { bottom = i; find = true; break; } } if (find) break; } //寻找最右边的标线,从上到下,从右往左 for (int i = bmp.Width - 1; i >= 0; i--)//列 { bool find = false; for (int j = 0; j <= bottom; j++)//行 { Color c = bmp.GetPixel(i, j); if (IsWhite(c)) { right = i; find = true; break; } } if (find) break; } int iWidth = right - left + 2; int iHeight = bottom - top + 2; bmp = Cut(bmp, left, top, iWidth, iHeight, WhiteBar); return bmp; } /// <summary> /// 添加文字:MES系统自动发送 /// </summary> /// <param name="bitmap"></param> /// <returns></returns> public Bitmap AddWatermarkWord(Bitmap bitmap) { //添加MES系统标识 string text = "MES系统自动发送"; int fontSize = 10; Graphics g = Graphics.FromImage(bitmap); int rectWidth = text.Length * (fontSize + 10); int rectHeight = fontSize + 15; //声明矩形域 Rectangle rectangle = new Rectangle(bitmap.Width - 200, bitmap.Height - 25, rectWidth, rectHeight); Font font = new Font("微软雅黑", fontSize, FontStyle.Bold); //定义字体 SolidBrush backbrush = new SolidBrush(Color.Transparent); SolidBrush sbrushRed = new SolidBrush(Color.Red); var stringFormat = new StringFormat(); stringFormat.Alignment = StringAlignment.Center; g.FillRectangle(backbrush, rectangle); g.DrawString(text, font, sbrushRed, rectangle, stringFormat); return bitmap; } /// <summary> /// 对图片进行裁剪 /// </summary> /// <param name="b">要裁剪的图片</param> /// <param name="StartX">裁剪的X轴</param> /// <param name="StartY">裁剪的Y轴</param> /// <param name="iWidth">要裁剪的宽度</param> /// <param name="iHeight">要裁剪的高度</param> /// <param name="WhiteBar">保留的白边</param> /// <returns></returns> public Bitmap Cut(Bitmap b, int StartX, int StartY, int iWidth, int iHeight, int WhiteBar) { Bitmap bmpOut = new Bitmap(iWidth + 2 * WhiteBar, iHeight + 2 * WhiteBar, PixelFormat.Format24bppRgb); Graphics g = Graphics.FromImage(bmpOut); g.FillRectangle(Brushes.White, new Rectangle(0, 0, iWidth + 2 * WhiteBar, iHeight + 2 * WhiteBar)); g.DrawImage(b, new Rectangle(WhiteBar, WhiteBar, iWidth, iHeight), new Rectangle(StartX, StartY, iWidth, iHeight), GraphicsUnit.Pixel); g.Dispose(); return bmpOut; } /// <summary> /// 判断白色与否,非纯白色 /// </summary> /// <param name="c"></param> /// <returns></returns> public bool IsWhite(Color c) { if (c.R < 245 || c.G < 245 || c.B < 245) return true; else return false; } } }
3、使用钉钉机器人发送图片
上一步,我们将帆软图片已经下载到本地了,那么就可以用钉钉机器人推送图片了。这边过程还是比较曲折的,查阅资料,我先使用了SendMessageWithImageAsync这个方法,但是推送到钉钉,确实表情包,无法放大查看图片。
然后,我换了个格式使用SendMessageWithMarkDownAsync方法来推送markdown,这次图片可以正常放大来查看了。
可是,当我将url换成我本地刚下载的帆软报表图片时,却发现推送到钉钉,显示如下。问了下百度发现,钉钉群机器人目前不支持直接推送本地图片,解决方法可以将图片放到钉钉服务器可以http访问的存储服务器上。这下难办了,因为总部安全的策略,这部分只能找总部寻求帮助了。
using System.Text; namespace DingDingTest { public class DingTalkClient { private readonly HttpClient _httpClient; // 钉钉机器人的Webhook URL private readonly string webhookUrl = "https://oapi.dingtalk.com/robot/send?access_token={token}"; private readonly string title = "MES报障"; public DingTalkClient(HttpClient httpClient) { _httpClient = httpClient; } /// <summary> /// 发送图片 /// </summary> /// <param name="webhookUrl"></param> /// <param name="imageUrl"></param> /// <returns></returns> public async Task SendMessageWithImageAsync(string webhookUrl, string imageUrl) { var content = new StringContent($"{{ \"msgtype\": \"image\", \"image\": {{ \"picURL\": \"{imageUrl}\" }} }}", Encoding.UTF8, "application/json"); // 发送POST请求 var response = await _httpClient.PostAsync(webhookUrl, content); // 输出结果 Console.WriteLine(await response.Content.ReadAsStringAsync()); } /// <summary> /// 发送markdown /// </summary> /// <param name="webhookUrl"></param> /// <param name="imageUrl"></param> /// <param name="message"></param> /// <returns></returns> public async Task SendMessageWithMarkDownAsync(string webhookUrl, string imageUrl, string message) { var content = new StringContent($"{{ \"msgtype\": \"markdown\", \"markdown\": {{ \"title\": \"{message}\", \"text\": \"\" }} }}", Encoding.UTF8, "application/json"); // 发送POST请求 var response = await _httpClient.PostAsync(webhookUrl, content); // 输出结果 Console.WriteLine(await response.Content.ReadAsStringAsync()); } } }
4、上传本地图片到gitee
在总部的帮助下,最后也是成功实现了功能,但是我一想,我们平时可以把本地图片上传到gitee,然后钉钉机器人推送的地址改成gitee的图片访问路径不也行吗,说干就干。查看gitee帮助文档。上传成功。
// 要发送的图片URL string imageUrl = $"https://gitee.com/{user}/{repos}/raw/{branch}/{path}";
using System.Text; namespace DingDingTest { internal class UploadImage { private const string AccessToken = {你的令牌}; private const string Repository = {你的仓库}; private const string Branch = {你的分支}; private const string BasePath = "gitee.com"; private const string ApiPath = "/api/v5/repos/{user}/{repos}/contents/{path}"; public async Task UploadImageAsync(string imagePath, string commitMessage) { var imageFileName = Path.GetFileName(imagePath); try { var url = {上传地址}; using (var request = new HttpRequestMessage(HttpMethod.Post, url)) { string contentType = "application/json"; // 设置请求头中的ContentType request.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue(contentType)); var data = new { access_token = AccessToken, content = Convert.ToBase64String(System.IO.File.ReadAllBytes(imagePath)), // 这里应该放置你的Base64编码后的图片内容 message = commitMessage }; string jsonBody = Newtonsoft.Json.JsonConvert.SerializeObject(data); request.Content = new StringContent(jsonBody, Encoding.UTF8, contentType); var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("token", AccessToken); HttpResponseMessage response = client.Send(request); response.EnsureSuccessStatusCode(); // 抛出异常如果响应不是成功的 string responseBody = await response.Content.ReadAsStringAsync().ConfigureAwait(false); Console.WriteLine(responseBody); } } catch (HttpRequestException e) { Console.WriteLine("\nException Caught!"); Console.WriteLine("Message :{0} ", e.Message); } } } }
5、使用Quartz
添加Quartz,每三分钟执行测试一下,测试OK。
using Quartz; using Quartz.Impl; using System; using System.Threading.Tasks; namespace DingDingTest { internal class Program { static async Task Main(string[] args) { // 1. 创建Scheduler IScheduler scheduler = await StdSchedulerFactory.GetDefaultScheduler(); // 2. 开启Scheduler await scheduler.Start(); // 3. 创建作业 IJobDetail job = JobBuilder.Create<TestJob>() .WithIdentity("myJob", "group1") .Build(); // 4. 创建触发器 var trigger = TriggerBuilder.Create() .WithIdentity("myTrigger", "group1") .StartNow() // 立即启动 .WithSimpleSchedule(x => x .RepeatForever() // 重复执行 .WithIntervalInMinutes(3)) // 每3min执行一次 .Build(); // 5. 将作业和触发器添加到调度器 await scheduler.ScheduleJob(job, trigger); Console.WriteLine("Press any key to close the application"); Console.ReadKey(); // 6. 关闭Scheduler await scheduler.Shutdown(); } } }
6、总结
整体思路最后如下:
1、下载帆软报表图片到本地
2、上传图片到公网可ping通的服务器
3、钉钉机器人以markdown方式推送
踩坑如下:
1、因为之前下载帆软报表图片到本地,其中帆软的参数使用的外部脚本,所以使用了using Microsoft.CSharp包的CSharpCodeProvider,来动态执行外部C#脚本代码,来拼接查询的参数,但是因为版本的问题.Net 8 中CSharpCodeProvider被废弃了,所以换成了Microsoft.CodeAnalysis.CSharp。简单使用的话,上诉代码FineReportUrlParam方法可以直接拼接下传参
2、钉钉机器人无法上传本地图片(也不知道百度的对不对,或许有其他方式,哈哈)
3、 上传到gitee上时,HttpResponseMessage response = client.Send(request);可以看到我这边没有使用SendAsync,😭这是因为我使用了异步,vs执行到这里就会崩溃,更改成Send就好了,也不知道为啥。
最好,前后搞了两三天还是实现了该功能,特此记录分享下。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 上周热点回顾(2.17-2.23)
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章