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

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

image-20250107133451315

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

image-20250107133643171

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

image-20250107133932522

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\": \"![图片]({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

// 要发送的图片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就好了,也不知道为啥。

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

posted @   子言sugar  阅读(177)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 上周热点回顾(2.17-2.23)
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
点击右上角即可分享
微信分享提示