C#笔记(1、钉钉机器人推送帆软报表图片)

news/2025/1/8 16:41:03/文章来源:https://www.cnblogs.com/ziyansugar/p/18657519

C#笔记——钉钉机器人推送帆软报表图片(一)

1、前言

​ 2024年最后一个月,家里多了个小公主,在家休息了一段时间。2025年,休完假上班第一天,领导就说:哎,我看总部那边做了个每日产出的报表推送到钉钉群,来看计划达成率。我们基地这边能不能做啊。我心里默默一想,然后大声一喊能做(内心os:上家公司做过类似的功能,不过他们是自己的通讯工具,不是钉钉)。既然有了活,那就开干吧。最终忙活了几天,终于算是实现了这个功能吧,记录下整个过程,以及踩到的坑吧,最终实现效果如图。

image-20250107130753085

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.dllMetadataReference.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这个方法,但是推送到钉钉,确实表情包,无法放大查看图片。

image-20250107133451315

然后,我换了个格式使用SendMessageWithMarkDownAsync方法来推送markdown,这次图片可以正常放大来查看了。

image-20250107133643171

可是,当我将url换成我本地刚下载的帆软报表图片时,却发现推送到钉钉,显示如下。问了下百度发现,钉钉群机器人目前不支持直接推送本地图片,解决方法可以将图片放到钉钉服务器可以http访问的存储服务器上。这下难办了,因为总部安全的策略,这部分只能找总部寻求帮助了。

image-20250107133932522

using System.Text;namespace DingDingTest
{public class DingTalkClient{private readonly HttpClient _httpClient;// 钉钉机器人的Webhook URLprivate 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\": \"![图片]({imageUrl})\" }} }}", Encoding.UTF8, "application/json");// 发送POST请求var response = await _httpClient.PostAsync(webhookUrl, content);// 输出结果Console.WriteLine(await response.Content.ReadAsStringAsync());}}
}

4、上传本地图片到gitee

在总部的帮助下,最后也是成功实现了功能,但是我一想,我们平时可以把本地图片上传到gitee,然后钉钉机器人推送的地址改成gitee的图片访问路径不也行吗,说干就干。查看gitee帮助文档。上传成功。

image-20250107134758636

// 要发送的图片URLstring 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";// 设置请求头中的ContentTyperequest.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. 创建SchedulerIScheduler scheduler = await StdSchedulerFactory.GetDefaultScheduler();// 2. 开启Schedulerawait 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. 关闭Schedulerawait 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就好了,也不知道为啥。

最好,前后搞了两三天还是实现了该功能,特此记录分享下。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/865325.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Javascript实现asp.net mvc的checkbox基本功能

Html的checkbox使用很广的,它的状态,勾选与非选。初始状态,默认为非选。你可以设置它是勾选, 直在checkbox 标签中,添加checked属性。另外,在javascript可以这样,getById(Checkbox_IsPublished).checked = true; 或者,getById(Checkbox_IsPublished).setAttribute(che…

“非旺玖原装的PL2303,请联系您的供应商”232串口转换器解决办法

偶尔会用到PL2303这种十几年前的旧芯片做的232串口转换器,如果手头没有别的工具必须使用的话,需要配置一下才能使用。该串口调试器在不同的USB口插入可能出现COM口无效或者端口号变动(COM 4/7/8等),需要通过设备管理器进行修改 当提示题目上的错误时,需要重装旧版本驱动1…

记录一个Github推送的问题(ssh推送出现:Connection closed by 198.18.0.9 port 22)

一、前言: 在公司需要将一个文档推送到GitHub仓库,由于Github对Https的推送已经移除了对密码认证的支持(2021.8.13),所以想要进行身份验证就只能使用两种方式:使用Persional Access Token(PAT) 使用SSH认证 PAT认证太麻烦了,每次都要输入,这里就主要介绍使用SSH认证的方…

安川机器人的独特优势

安川机器人,这一源自日本安川电机(Yaskawa Electric Corporation)的杰出产物,是工业自动化领域中的佼佼者,其核心价值在于生产线上那精准无误的自动化作业能力。这款机器人集成了电机、减速器、传感器、控制器等一系列精密部件,如同一套高效协同的作战系统,为企业带来了生…

2020-2024 IDEA安装+激活

一、下载 1. IDEA各版本官方下载入口 IDEA官网下载地址 2. 选择左边,然后点击【20xx.x.x-Windows(exe)】 PS: 如需下载特定版本,可以往下拉,都是选择【202x.x-Windows(exe)】下载二、安装 1. 点击运行 ps: 安全警告是部分电脑有,没有跳过就可以了~2. 点击下一步3. 选择安…

CDS标准视图:维护包描述 I_MaintPackageTextData

视图名称:维护包描述 I_MaintPackageTextData 视图类型:基础 视图代码:点击查看代码 @EndUserText.label: Maintenance Package - Text @ObjectModel.dataCategory: #TEXT @VDM.viewType: #COMPOSITE @AbapCatalog.sqlViewName: IMNTPCKGTXTDATA @AbapCatalog.compiler.comp…

CDS标准视图:维护包数据 I_MaintenancePackageData

视图名称:维护包数据 I_MaintenancePackageData 视图类型:基础 视图代码:点击查看代码 @AbapCatalog.sqlViewName: IMAINTPCKGDATA @AbapCatalog.compiler.compareFilter: true @AccessControl.authorizationCheck: #PRIVILEGED_ONLY @EndUserText.label: Maintenance Packa…

让跨 project 联查更轻松,SLS StoreView 查询和分析实践

在业务场景中,日志数据可能存储在日志服务 Project 的不同 Logstore/MetricStore 中或不同地域的 Project 中。日志服务的数据集(StoreView)功能支持跨地域、跨 Store 联合查询和分析,让用户基于数据集就能高效便捷地查询分析全地域的数据,真正做到数据分析不受地域边界的…

流程配置中心同步后流程版本ID不一致

不同的账套进行流程发布时会产生不同的版本ID , BOS同步后的流程设计的ID是相同的.

大语言模型提示技巧(六)-文本转换

大语言模型是基于自然语言的人工智能,所以它在语言上的表现相当出色,使用大语言模型进行可以进行诸如翻译、语气转换、润色、语言评价、扩写、润色等语言处理,对于日常文字工作,它是一名合格甚至优秀的私人秘书。 (一)翻译 在不同语言之间进行翻译是众多大语言模型都支持…

Unreal Engine 5 课程记录 蓝图部分(非教程)

学习课程:Unreal Engine 5 – Full Course for Beginners 非教程,仅学习记录及碎碎念,学完感觉UE就非常的像预制菜,简简单单就能有非常好的效果(非常に新鲜で、非常に美味しい!),但装料太足了要想拆分明白用料和流程又会比较复杂!Creating Levels 创建基本关卡,几个组…

Unreal Engine 5 课程笔记 蓝图部分

学习课程:Unreal Engine 5 – Full Course for Beginners 非教程,仅学习记录及碎碎念,学完感觉UE就非常的像预制菜,简简单单就能有非常好的效果(非常に新鲜で、非常に美味しい!),但装料太足了要想拆分明白用料和流程又会比较复杂!Creating Levels 创建基本关卡,几个组…