SQL Server通过存储过程调用DLL程序集发送飞书卡片消息

1. 引言

在现代企业应用开发中,数据库系统与即时通讯工具的集成变得越来越重要。SQL Server作为一款功能强大的关系型数据库管理系统,可以通过存储过程实现各种自动化任务,包括发送通知消息。本文将详细介绍如何在SQL Server中创建存储过程,通过HTTP请求与飞书开放平台API交互,实现发送富文本卡片消息的功能。

1.1 应用场景

SQL Server发送飞书卡片消息的应用场景非常广泛,包括但不限于:

  1. 数据库监控告警:当数据库出现性能问题、空间不足或关键作业失败时自动通知DBA团队
  2. 业务流程通知:在订单处理、库存变更等业务操作完成后通知相关人员
  3. 定时报表推送:定期将关键业务数据以卡片形式发送给管理层
  4. 审批流程触发:当数据库中的审批状态变更时通知审批人
  5. 系统集成:作为企业应用集成的一部分,连接数据库系统与协作平台

1.2 技术概览

实现这一功能主要涉及以下技术组件:

  1. SQL Server CLR集成:允许在SQL Server中执行.NET代码
  2. HTTP客户端:用于向飞书API发送请求
  3. JSON处理:构建和解析飞书API所需的JSON格式消息
  4. OAuth 2.0认证:获取访问飞书API所需的令牌
  5. 存储过程封装:提供简洁的数据库接口供其他应用调用

2. 准备工作

2.1 环境要求

在开始实现之前,请确保满足以下环境要求:

  1. SQL Server 2012或更高版本(支持CLR集成)
  2. .NET Framework 4.5或更高版本
  3. 飞书开发者账号及应用权限
  4. 数据库服务器能够访问互联网(与飞书API通信)

2.2 飞书应用配置

  1. 创建飞书应用

    • 登录飞书开放平台(https://open.feishu.cn/)
    • 进入"开发者后台",点击"创建应用"
    • 填写应用名称、描述等基本信息
  2. 获取凭证信息

    • 应用凭证:App ID和App Secret
    • 权限配置:确保已添加"发送消息"权限
    • 启用机器人能力
  3. 获取webhook地址(可选):

    • 如果使用webhook方式发送消息,需在机器人配置中获取webhook地址
    • 本文主要介绍通过API方式发送

2.3 SQL Server配置

  1. 启用CLR集成

    sql 复制代码
    sp_configure 'clr enabled', 1
    RECONFIGURE
  2. 设置TRUSTWORTHY ON(仅开发环境建议):

    sql 复制代码
    ALTER DATABASE YourDatabaseName SET TRUSTWORTHY ON
  3. 创建非对称密钥和登录(生产环境推荐):

    sql 复制代码
    CREATE ASYMMETRIC KEY CLRFeishuKey FROM EXECUTABLE FILE = 'C:\path\to\your\assembly.dll'
    CREATE LOGIN CLRFeishuLogin FROM ASYMMETRIC KEY CLRFeishuKey
    GRANT EXTERNAL ACCESS ASSEMBLY TO CLRFeishuLogin

3. 实现方案设计

3.1 架构设计

整个解决方案的架构分为以下几个层次:

  1. 数据库层:存储过程作为入口点,处理业务逻辑
  2. CLR集成层:.NET程序集处理HTTP通信和JSON序列化
  3. API通信层:与飞书服务器交互,发送消息
  4. 安全层:处理认证和授权

3.2 消息流程

  1. 应用程序或SQL作业调用存储过程
  2. 存储过程调用CLR函数/方法
  3. CLR代码构建请求,发送到飞书API
  4. 飞书API处理请求并返回结果
  5. 结果返回给调用方

3.3 错误处理设计

完善的错误处理机制应包括:

  1. API请求失败的重试逻辑
  2. 详细的错误日志记录
  3. 返回有意义的错误信息给调用方
  4. 敏感信息的安全处理

4. CLR程序集实现

4.1 创建C#类库项目

使用Visual Studio创建新的类库项目,命名为"SQLFeishuIntegration"。

4.2 核心代码实现

以下是完整的C#实现代码:

csharp 复制代码
using System;
using System.IO;
using System.Net;
using System.Text;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Web.Script.Serialization;

[Serializable]
[DataContract]
public class FeishuAccessTokenResponse
{
    [DataMember(Name = "code")]
    public int Code { get; set; }
    
    [DataMember(Name = "msg")]
    public string Message { get; set; }
    
    [DataMember(Name = "tenant_access_token")]
    public string TenantAccessToken { get; set; }
    
    [DataMember(Name = "expire")]
    public int Expire { get; set; }
}

[Serializable]
[DataContract]
public class FeishuMessageResponse
{
    [DataMember(Name = "code")]
    public int Code { get; set; }
    
    [DataMember(Name = "msg")]
    public string Message { get; set; }
    
    [DataMember(Name = "data")]
    public MessageResponseData Data { get; set; }
}

[Serializable]
[DataContract]
public class MessageResponseData
{
    [DataMember(Name = "message_id")]
    public string MessageId { get; set; }
}

[Serializable]
[DataContract]
public class CardContent
{
    [DataMember(Name = "config")]
    public CardConfig Config { get; set; }
    
    [DataMember(Name = "header")]
    public CardHeader Header { get; set; }
    
    [DataMember(Name = "elements")]
    public List<CardElement> Elements { get; set; }
}

[Serializable]
[DataContract]
public class CardConfig
{
    [DataMember(Name = "wide_screen_mode")]
    public bool WideScreenMode { get; set; } = true;
    
    [DataMember(Name = "enable_forward")]
    public bool EnableForward { get; set; } = true;
}

[Serializable]
[DataContract]
public class CardHeader
{
    [DataMember(Name = "title")]
    public CardTitle Title { get; set; }
    
    [DataMember(Name = "template")]
    public string Template { get; set; }
}

[Serializable]
[DataContract]
public class CardTitle
{
    [DataMember(Name = "tag")]
    public string Tag { get; set; } = "plain_text";
    
    [DataMember(Name = "content")]
    public string Content { get; set; }
}

[Serializable]
[DataContract]
public class CardElement
{
    [DataMember(Name = "tag")]
    public string Tag { get; set; }
    
    [DataMember(Name = "text")]
    public CardText Text { get; set; }
    
    [DataMember(Name = "fields")]
    public List<CardField> Fields { get; set; }
    
    [DataMember(Name = "actions")]
    public List<CardAction> Actions { get; set; }
}

[Serializable]
[DataContract]
public class CardText
{
    [DataMember(Name = "tag")]
    public string Tag { get; set; } = "lark_md";
    
    [DataMember(Name = "content")]
    public string Content { get; set; }
}

[Serializable]
[DataContract]
public class CardField
{
    [DataMember(Name = "is_short")]
    public bool IsShort { get; set; }
    
    [DataMember(Name = "text")]
    public CardText Text { get; set; }
}

[Serializable]
[DataContract]
public class CardAction
{
    [DataMember(Name = "tag")]
    public string Tag { get; set; } = "button";
    
    [DataMember(Name = "text")]
    public CardText Text { get; set; }
    
    [DataMember(Name = "type")]
    public string Type { get; set; }
    
    [DataMember(Name = "url")]
    public string Url { get; set; }
}

public class FeishuMessageSender
{
    private const string TokenUrl = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal";
    private const string MessageUrl = "https://open.feishu.cn/open-apis/im/v1/messages";
    
    private static Dictionary<string, Tuple<string, DateTime>> _tokenCache = new Dictionary<string, Tuple<string, DateTime>>();
    private static readonly object _lock = new object();
    
    [SqlProcedure]
    public static void SendFeishuCardMessage(
        SqlString appId, 
        SqlString appSecret, 
        SqlString receiveId, 
        SqlString receiveIdType, 
        SqlString title, 
        SqlString content, 
        SqlString buttonText, 
        SqlString buttonUrl,
        out SqlString resultMessage,
        out SqlInt32 resultCode)
    {
        resultMessage = "";
        resultCode = -1;
        
        try
        {
            // 获取访问令牌
            string accessToken = GetAccessToken(appId.Value, appSecret.Value);
            
            if (string.IsNullOrEmpty(accessToken))
            {
                resultMessage = "Failed to get access token";
                return;
            }
            
            // 构建卡片消息
            var cardContent = new CardContent
            {
                Config = new CardConfig(),
                Header = new CardHeader
                {
                    Title = new CardTitle { Content = title.Value },
                    Template = "blue"
                },
                Elements = new List<CardElement>
                {
                    new CardElement
                    {
                        Tag = "div",
                        Text = new CardText { Content = content.Value }
                    }
                }
            };
            
            if (!string.IsNullOrEmpty(buttonText.Value) && !string.IsNullOrEmpty(buttonUrl.Value))
            {
                cardContent.Elements.Add(new CardElement
                {
                    Tag = "action",
                    Actions = new List<CardAction>
                    {
                        new CardAction
                        {
                            Text = new CardText { Content = buttonText.Value },
                            Type = "default",
                            Url = buttonUrl.Value
                        }
                    }
                });
            }
            
            var messageData = new
            {
                receive_id = receiveId.Value,
                content = new JavaScriptSerializer().Serialize(cardContent),
                msg_type = "interactive"
            };
            
            string jsonData = new JavaScriptSerializer().Serialize(messageData);
            
            // 发送消息
            var response = SendHttpRequest(
                $"{MessageUrl}?receive_id_type={receiveIdType.Value}",
                "POST",
                jsonData,
                new Dictionary<string, string>
                {
                    { "Authorization", $"Bearer {accessToken}" },
                    { "Content-Type", "application/json" }
                });
            
            var serializer = new DataContractJsonSerializer(typeof(FeishuMessageResponse));
            using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(response)))
            {
                var messageResponse = (FeishuMessageResponse)serializer.ReadObject(ms);
                
                if (messageResponse.Code == 0)
                {
                    resultCode = 0;
                    resultMessage = $"Message sent successfully. Message ID: {messageResponse.Data.MessageId}";
                }
                else
                {
                    resultMessage = $"Failed to send message: {messageResponse.Message}";
                }
            }
        }
        catch (Exception ex)
        {
            resultMessage = $"Error: {ex.Message}";
        }
    }
    
    private static string GetAccessToken(string appId, string appSecret)
    {
        string cacheKey = $"{appId}_{appSecret}";
        
        lock (_lock)
        {
            if (_tokenCache.ContainsKey(cacheKey))
            {
                var cachedToken = _tokenCache[cacheKey];
                if (DateTime.Now < cachedToken.Item2.AddMinutes(-5)) // 提前5分钟过期
                {
                    return cachedToken.Item1;
                }
                _tokenCache.Remove(cacheKey);
            }
            
            var requestData = new
            {
                app_id = appId,
                app_secret = appSecret
            };
            
            string jsonData = new JavaScriptSerializer().Serialize(requestData);
            string response = SendHttpRequest(TokenUrl, "POST", jsonData, null);
            
            var serializer = new DataContractJsonSerializer(typeof(FeishuAccessTokenResponse));
            using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(response)))
            {
                var tokenResponse = (FeishuAccessTokenResponse)serializer.ReadObject(ms);
                
                if (tokenResponse.Code == 0)
                {
                    _tokenCache[cacheKey] = new Tuple<string, DateTime>(
                        tokenResponse.TenantAccessToken,
                        DateTime.Now.AddSeconds(tokenResponse.Expire));
                    
                    return tokenResponse.TenantAccessToken;
                }
                else
                {
                    throw new Exception($"Failed to get access token: {tokenResponse.Message}");
                }
            }
        }
    }
    
    private static string SendHttpRequest(string url, string method, string data, Dictionary<string, string> headers)
    {
        HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
        request.Method = method;
        request.Timeout = 30000; // 30秒超时
        
        if (headers != null)
        {
            foreach (var header in headers)
            {
                request.Headers.Add(header.Key, header.Value);
            }
        }
        
        if (method == "POST" || method == "PUT")
        {
            byte[] dataBytes = Encoding.UTF8.GetBytes(data);
            request.ContentType = "application/json";
            request.ContentLength = dataBytes.Length;
            
            using (Stream requestStream = request.GetRequestStream())
            {
                requestStream.Write(dataBytes, 0, dataBytes.Length);
            }
        }
        
        try
        {
            using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
            using (Stream responseStream = response.GetResponseStream())
            using (StreamReader reader = new StreamReader(responseStream, Encoding.UTF8))
            {
                return reader.ReadToEnd();
            }
        }
        catch (WebException ex)
        {
            if (ex.Response != null)
            {
                using (Stream responseStream = ex.Response.GetResponseStream())
                using (StreamReader reader = new StreamReader(responseStream, Encoding.UTF8))
                {
                    string errorResponse = reader.ReadToEnd();
                    throw new Exception($"HTTP Error: {ex.Status}, Response: {errorResponse}");
                }
            }
            throw;
        }
    }
}

4.3 代码说明

  1. 数据模型类:定义了飞书API返回的数据结构
  2. FeishuMessageSender类:核心功能类,包含发送消息的主要逻辑
  3. GetAccessToken方法:获取飞书API访问令牌,带缓存机制
  4. SendHttpRequest方法:通用的HTTP请求发送方法
  5. SendFeishuCardMessage方法:SQL Server调用的入口点,构建卡片消息并发送

4.4 编译和部署

  1. 编译项目生成DLL文件

  2. 在SQL Server中注册程序集:

    sql 复制代码
    CREATE ASSEMBLY SQLFeishuIntegration
    FROM 'C:\path\to\SQLFeishuIntegration.dll'
    WITH PERMISSION_SET = EXTERNAL_ACCESS;

5. 存储过程实现

5.1 创建存储过程

基于CLR程序集创建存储过程:

sql 复制代码
CREATE PROCEDURE dbo.sp_SendFeishuCardMessage
    @AppId NVARCHAR(100),
    @AppSecret NVARCHAR(100),
    @ReceiveId NVARCHAR(100),
    @ReceiveIdType NVARCHAR(20) = 'open_id', -- 可以是 open_id, user_id, email, chat_id
    @Title NVARCHAR(200),
    @Content NVARCHAR(MAX),
    @ButtonText NVARCHAR(100) = NULL,
    @ButtonUrl NVARCHAR(500) = NULL,
    @ResultMessage NVARCHAR(MAX) OUTPUT,
    @ResultCode INT OUTPUT
AS
EXTERNAL NAME SQLFeishuIntegration.[SQLFeishuIntegration.FeishuMessageSender].SendFeishuCardMessage
GO

5.2 简化版存储过程

为了方便日常使用,可以创建一个简化版的存储过程:

sql 复制代码
CREATE PROCEDURE dbo.sp_SendSimpleFeishuAlert
    @Title NVARCHAR(200),
    @Content NVARCHAR(MAX),
    @IsSuccess BIT = 1,
    @ButtonText NVARCHAR(100) = NULL,
    @ButtonUrl NVARCHAR(500) = NULL
AS
BEGIN
    DECLARE @AppId NVARCHAR(100) = 'your_app_id';
    DECLARE @AppSecret NVARCHAR(100) = 'your_app_secret';
    DECLARE @ReceiveId NVARCHAR(100) = 'default_receive_id';
    DECLARE @ResultMessage NVARCHAR(MAX);
    DECLARE @ResultCode INT;
    
    -- 根据成功/失败设置不同的标题样式
    IF @IsSuccess = 1
        SET @Title = '✅ ' + @Title;
    ELSE
        SET @Title = '❌ ' + @Title;
    
    EXEC dbo.sp_SendFeishuCardMessage
        @AppId = @AppId,
        @AppSecret = @AppSecret,
        @ReceiveId = @ReceiveId,
        @ReceiveIdType = 'user_id',
        @Title = @Title,
        @Content = @Content,
        @ButtonText = @ButtonText,
        @ButtonUrl = @ButtonUrl,
        @ResultMessage = @ResultMessage OUTPUT,
        @ResultCode = @ResultCode OUTPUT;
    
    IF @ResultCode <> 0
        RAISERROR('Failed to send Feishu message: %s', 16, 1, @ResultMessage);
END
GO

5.3 使用示例

sql 复制代码
-- 示例1:发送简单通知
DECLARE @ResultMsg NVARCHAR(MAX);
DECLARE @ResultCode INT;

EXEC dbo.sp_SendFeishuCardMessage
    @AppId = 'your_app_id',
    @AppSecret = 'your_app_secret',
    @ReceiveId = 'user_id_or_open_id',
    @Title = '数据库备份通知',
    @Content = '数据库备份已完成,耗时2小时15分钟。备份大小: 45GB',
    @ButtonText = '查看详情',
    @ButtonUrl = 'https://your-domain.com/backup-reports',
    @ResultMessage = @ResultMsg OUTPUT,
    @ResultCode = @ResultCode OUTPUT;

SELECT @ResultCode AS ResultCode, @ResultMsg AS ResultMessage;

-- 示例2:使用简化版存储过程
EXEC dbo.sp_SendSimpleFeishuAlert
    @Title = '每日销售报表',
    @Content = '今日销售额: ¥1,250,000\n订单数: 1,245\n平均客单价: ¥1,004',
    @IsSuccess = 1,
    @ButtonText = '下载完整报表',
    @ButtonUrl = 'https://your-domain.com/reports/daily-sales';

6. 高级功能实现

6.1 支持Markdown格式

飞书卡片消息支持Markdown格式,可以增强消息的可读性:

sql 复制代码
ALTER PROCEDURE dbo.sp_SendFeishuCardMessageWithMarkdown
    @AppId NVARCHAR(100),
    @AppSecret NVARCHAR(100),
    @ReceiveId NVARCHAR(100),
    @ReceiveIdType NVARCHAR(20) = 'open_id',
    @Title NVARCHAR(200),
    @MarkdownContent NVARCHAR(MAX),
    @ButtonText NVARCHAR(100) = NULL,
    @ButtonUrl NVARCHAR(500) = NULL,
    @ResultMessage NVARCHAR(MAX) OUTPUT,
    @ResultCode INT OUTPUT
AS
BEGIN
    -- 构建完整的卡片JSON
    DECLARE @CardJson NVARCHAR(MAX);
    SET @CardJson = N'{
        "config": {
            "wide_screen_mode": true,
            "enable_forward": true
        },
        "header": {
            "title": {
                "tag": "plain_text",
                "content": "' + REPLACE(@Title, '"', '\"') + '"
            },
            "template": "blue"
        },
        "elements": [
            {
                "tag": "div",
                "text": {
                    "tag": "lark_md",
                    "content": "' + REPLACE(REPLACE(@MarkdownContent, '\', '\\'), '"', '\"') + '"
                }
            }';
    
    IF @ButtonText IS NOT NULL AND @ButtonUrl IS NOT NULL
    BEGIN
        SET @CardJson = @CardJson + N',
            {
                "tag": "action",
                "actions": [
                    {
                        "tag": "button",
                        "text": {
                            "tag": "plain_text",
                            "content": "' + REPLACE(@ButtonText, '"', '\"') + '"
                        },
                        "type": "default",
                        "url": "' + REPLACE(@ButtonUrl, '"', '\"') + '"
                    }
                ]
            }';
    END
    
    SET @CardJson = @CardJson + N']
    }';
    
    -- 调用CLR方法发送消息
    EXEC dbo.sp_SendFeishuCardMessage
        @AppId = @AppId,
        @AppSecret = @AppSecret,
        @ReceiveId = @ReceiveId,
        @ReceiveIdType = @ReceiveIdType,
        @Title = @Title,
        @Content = @CardJson,
        @ButtonText = NULL, -- 已经在JSON中包含
        @ButtonUrl = NULL, -- 已经在JSON中包含
        @ResultMessage = @ResultMessage OUTPUT,
        @ResultCode = @ResultCode OUTPUT;
END
GO

6.2 支持多列布局

飞书卡片支持多列布局,可以更高效地展示信息:

sql 复制代码
CREATE PROCEDURE dbo.sp_SendFeishuMultiColumnCard
    @AppId NVARCHAR(100),
    @AppSecret NVARCHAR(100),
    @ReceiveId NVARCHAR(100),
    @ReceiveIdType NVARCHAR(20) = 'open_id',
    @Title NVARCHAR(200),
    @Column1Title NVARCHAR(100),
    @Column1Content NVARCHAR(MAX),
    @Column2Title NVARCHAR(100),
    @Column2Content NVARCHAR(MAX),
    @ButtonText NVARCHAR(100) = NULL,
    @ButtonUrl NVARCHAR(500) = NULL,
    @ResultMessage NVARCHAR(MAX) OUTPUT,
    @ResultCode INT OUTPUT
AS
BEGIN
    DECLARE @CardJson NVARCHAR(MAX);
    SET @CardJson = N'{
        "config": {
            "wide_screen_mode": true,
            "enable_forward": true
        },
        "header": {
            "title": {
                "tag": "plain_text",
                "content": "' + REPLACE(@Title, '"', '\"') + '"
            },
            "template": "wathet"
        },
        "elements": [
            {
                "tag": "div",
                "fields": [
                    {
                        "is_short": true,
                        "text": {
                            "tag": "lark_md",
                            "content": "**' + REPLACE(@Column1Title, '"', '\"') + '**\n' + REPLACE(REPLACE(@Column1Content, '\', '\\'), '"', '\"') + '"
                        }
                    },
                    {
                        "is_short": true,
                        "text": {
                            "tag": "lark_md",
                            "content": "**' + REPLACE(@Column2Title, '"', '\"') + '**\n' + REPLACE(REPLACE(@Column2Content, '\', '\\'), '"', '\"') + '"
                        }
                    }
                ]
            }';
    
    IF @ButtonText IS NOT NULL AND @ButtonUrl IS NOT NULL
    BEGIN
        SET @CardJson = @CardJson + N',
            {
                "tag": "action",
                "actions": [
                    {
                        "tag": "button",
                        "text": {
                            "tag": "plain_text",
                            "content": "' + REPLACE(@ButtonText, '"', '\"') + '"
                        },
                        "type": "default",
                        "url": "' + REPLACE(@ButtonUrl, '"', '\"') + '"
                    }
                ]
            }';
    END
    
    SET @CardJson = @CardJson + N']
    }';
    
    EXEC dbo.sp_SendFeishuCardMessage
        @AppId = @AppId,
        @AppSecret = @AppSecret,
        @ReceiveId = @ReceiveId,
        @ReceiveIdType = @ReceiveIdType,
        @Title = @Title,
        @Content = @CardJson,
        @ButtonText = NULL,
        @ButtonUrl = NULL,
        @ResultMessage = @ResultMessage OUTPUT,
        @ResultCode = @ResultCode OUTPUT;
END
GO

6.3 定时消息发送

结合SQL Server Agent作业,可以实现定时消息发送:

sql 复制代码
-- 创建存储过程用于定时发送日报
CREATE PROCEDURE dbo.sp_SendDailyDatabaseReport
AS
BEGIN
    DECLARE @ReportDate NVARCHAR(20) = CONVERT(NVARCHAR(10), GETDATE(), 120);
    DECLARE @Title NVARCHAR(200) = '数据库日报 - ' + @ReportDate;
    DECLARE @Content NVARCHAR(MAX);
    DECLARE @ResultMessage NVARCHAR(MAX);
    DECLARE @ResultCode INT;
    
    -- 获取数据库状态信息
    DECLARE @DbSize NVARCHAR(50);
    DECLARE @BackupStatus NVARCHAR(100);
    DECLARE @JobStatus NVARCHAR(MAX);
    
    SELECT @DbSize = CONVERT(NVARCHAR(50), SUM(size * 8 / 1024)) + ' MB'
    FROM sys.master_files
    WHERE database_id = DB_ID();
    
    SELECT @BackupStatus = 
        CASE WHEN MAX(backup_finish_date) > DATEADD(DAY, -1, GETDATE()) 
             THEN '✅ 最近24小时内有备份' 
             ELSE '❌ 最近24小时内无备份' END
    FROM msdb.dbo.backupset
    WHERE database_name = DB_NAME();
    
    -- 获取作业状态
    SET @JobStatus = '';
    
    SELECT @JobStatus = @JobStatus + 
        CASE WHEN run_status = 0 THEN '❌ 失败 - ' 
             WHEN run_status = 1 THEN '✅ 成功 - ' 
             WHEN run_status = 2 THEN '🔄 重试 - ' 
             WHEN run_status = 3 THEN '⏸️ 取消 - ' 
             ELSE '❓ 未知 - ' END +
        name + '\n最后运行: ' + 
        CONVERT(NVARCHAR(20), last_run_outcome_date, 120) + '\n\n'
    FROM (
        SELECT j.name, h.run_status, MAX(h.run_date) AS last_run_outcome_date
        FROM msdb.dbo.sysjobs j
        INNER JOIN msdb.dbo.sysjobhistory h ON j.job_id = h.job_id
        WHERE h.run_date >= CONVERT(VARCHAR(8), GETDATE(), 112)
        GROUP BY j.name, h.run_status
    ) AS JobStatus;
    
    -- 构建消息内容
    SET @Content = '**数据库大小**: ' + @DbSize + '\n' +
                  '**备份状态**: ' + @BackupStatus + '\n\n' +
                  '**今日作业执行情况**:\n' + @JobStatus;
    
    -- 发送消息
    EXEC dbo.sp_SendSimpleFeishuAlert
        @Title = @Title,
        @Content = @Content,
        @IsSuccess = 1,
        @ButtonText = '查看详细报表',
        @ButtonUrl = 'https://your-domain.com/db-reports/daily';
END
GO

然后创建SQL Server Agent作业,每天定时执行此存储过程。

7. 安全考虑

7.1 敏感信息保护

  1. 加密存储凭证

    sql 复制代码
    -- 创建数据库主密钥
    CREATE MASTER KEY ENCRYPTION BY PASSWORD = 'YourStrongPassword123!';
    
    -- 创建证书
    CREATE CERTIFICATE FeishuCert WITH SUBJECT = 'Feishu API Credentials';
    
    -- 创建对称密钥
    CREATE SYMMETRIC KEY FeishuSymmetricKey
    WITH ALGORITHM = AES_256
    ENCRYPTION BY CERTIFICATE FeishuCert;
    
    -- 加密存储App Secret
    OPEN SYMMETRIC KEY FeishuSymmetricKey DECRYPTION BY CERTIFICATE FeishuCert;
    
    INSERT INTO dbo.FeishuCredentials (AppId, EncryptedAppSecret)
    VALUES (
        'your_app_id',
        ENCRYPTBYKEY(KEY_GUID('FeishuSymmetricKey'), 'your_app_secret')
    );
    
    CLOSE SYMMETRIC KEY FeishuSymmetricKey;
  2. 安全获取凭证

    sql 复制代码
    CREATE PROCEDURE dbo.sp_GetFeishuCredentials
        @AppId NVARCHAR(100) OUTPUT,
        @AppSecret NVARCHAR(100) OUTPUT
    AS
    BEGIN
        OPEN SYMMETRIC KEY FeishuSymmetricKey DECRYPTION BY CERTIFICATE FeishuCert;
        
        SELECT 
            @AppId = AppId,
            @AppSecret = CONVERT(NVARCHAR(100), DECRYPTBYKEY(EncryptedAppSecret))
        FROM dbo.FeishuCredentials
        WHERE IsActive = 1;
        
        CLOSE SYMMETRIC KEY FeishuSymmetricKey;
    END
    GO

7.2 权限控制

  1. 最小权限原则

    sql 复制代码
    -- 创建专门的角色
    CREATE ROLE FeishuMessageSender;
    
    -- 只授予必要的执行权限
    GRANT EXECUTE ON dbo.sp_SendSimpleFeishuAlert TO FeishuMessageSender;
    
    -- 不直接授予基础存储过程的权限
    DENY EXECUTE ON dbo.sp_SendFeishuCardMessage TO PUBLIC;
  2. 审计日志

    sql 复制代码
    CREATE TABLE dbo.FeishuMessageLog (
        LogId INT IDENTITY(1,1) PRIMARY KEY,
        AppId NVARCHAR(100),
        ReceiveId NVARCHAR(100),
        ReceiveIdType NVARCHAR(20),
        Title NVARCHAR(200),
        Content NVARCHAR(MAX),
        ButtonText NVARCHAR(100) NULL,
        ButtonUrl NVARCHAR(500) NULL,
        ResultCode INT,
        ResultMessage NVARCHAR(MAX),
        SentBy NVARCHAR(128) DEFAULT SUSER_SNAME(),
        SentTime DATETIME DEFAULT GETDATE(),
        ClientHost NVARCHAR(128) DEFAULT HOST_NAME(),
        ClientApp NVARCHAR(128) DEFAULT APP_NAME()
    );
    
    -- 修改存储过程添加日志记录
    ALTER PROCEDURE dbo.sp_SendFeishuCardMessage
        -- 原有参数...
    AS
    BEGIN
        -- 原有逻辑...
        
        -- 记录日志
        INSERT INTO dbo.FeishuMessageLog (
            AppId, ReceiveId, ReceiveIdType, Title, Content, 
            ButtonText, ButtonUrl, ResultCode, ResultMessage
        )
        VALUES (
            @AppId, @ReceiveId, @ReceiveIdType, @Title, @Content,
            @ButtonText, @ButtonUrl, @ResultCode, @ResultMessage
        );
    END
    GO

8. 性能优化

8.1 令牌缓存优化

在CLR代码中已经实现了内存缓存,还可以添加数据库层面的缓存:

sql 复制代码
CREATE TABLE dbo.FeishuTokenCache (
    AppId NVARCHAR(100) NOT NULL,
    AccessToken NVARCHAR(500) NOT NULL,
    ExpireTime DATETIME NOT NULL,
    LastUpdated DATETIME NOT NULL DEFAULT GETDATE(),
    PRIMARY KEY (AppId)
);

-- 修改获取令牌的存储过程
ALTER PROCEDURE dbo.sp_GetFeishuAccessToken
    @AppId NVARCHAR(100),
    @AppSecret NVARCHAR(100),
    @AccessToken NVARCHAR(500) OUTPUT,
    @IsNewToken BIT OUTPUT
AS
BEGIN
    SET @IsNewToken = 0;
    
    -- 检查缓存中是否有未过期的令牌
    SELECT @AccessToken = AccessToken
    FROM dbo.FeishuTokenCache
    WHERE AppId = @AppId AND ExpireTime > DATEADD(MINUTE, 5, GETDATE());
    
    IF @AccessToken IS NULL
    BEGIN
        -- 调用CLR方法获取新令牌
        DECLARE @ResultMessage NVARCHAR(MAX);
        DECLARE @ResultCode INT;
        DECLARE @ExpireIn INT;
        
        -- 这里需要扩展CLR方法以返回过期时间
        -- 假设有一个新的CLR方法可以返回完整响应
        
        -- 更新缓存
        DELETE FROM dbo.FeishuTokenCache WHERE AppId = @AppId;
        
        INSERT INTO dbo.FeishuTokenCache (AppId, AccessToken, ExpireTime)
        VALUES (@AppId, @AccessToken, DATEADD(SECOND, @ExpireIn, GETDATE()));
        
        SET @IsNewToken = 1;
    END
END
GO

8.2 批量消息发送

对于需要发送多条消息的场景,可以实现批量发送:

sql 复制代码
CREATE PROCEDURE dbo.sp_SendFeishuCardsBatch
    @AppId NVARCHAR(100),
    @AppSecret NVARCHAR(100),
    @MessageList NVARCHAR(MAX), -- JSON格式的消息列表
    @ResultSummary NVARCHAR(MAX) OUTPUT
AS
BEGIN
    DECLARE @AccessToken NVARCHAR(500);
    DECLARE @IsNewToken BIT;
    
    EXEC dbo.sp_GetFeishuAccessToken @AppId, @AppSecret, @AccessToken OUTPUT, @IsNewToken OUTPUT;
    
    IF @AccessToken IS NULL
    BEGIN
        SET @ResultSummary = 'Failed to get access token';
        RETURN;
    END
    
    -- 解析JSON消息列表
    -- 这里需要SQL Server 2016+支持JSON功能
    DECLARE @Results TABLE (
        MessageId INT IDENTITY(1,1),
        ReceiveId NVARCHAR(100),
        Title NVARCHAR(200),
        ResultCode INT,
        ResultMessage NVARCHAR(MAX)
    );
    
    INSERT INTO @Results (ReceiveId, Title)
    SELECT 
        receive_id, title
    FROM OPENJSON(@MessageList)
    WITH (
        receive_id NVARCHAR(100) '$.receive_id',
        receive_id_type NVARCHAR(20) '$.receive_id_type',
        title NVARCHAR(200) '$.title',
        content NVARCHAR(MAX) '$.content',
        button_text NVARCHAR(100) '$.button_text',
        button_url NVARCHAR(500) '$.button_url'
    );
    
    -- 遍历发送每条消息
    DECLARE @CurrentId INT = 1;
    DECLARE @MaxId INT = (SELECT MAX(MessageId) FROM @Results);
    DECLARE @CurrentReceiveId NVARCHAR(100);
    DECLARE @CurrentReceiveIdType NVARCHAR(20);
    DECLARE @CurrentTitle NVARCHAR(200);
    DECLARE @CurrentContent NVARCHAR(MAX);
    DECLARE @CurrentButtonText NVARCHAR(100);
    DECLARE @CurrentButtonUrl NVARCHAR(500);
    DECLARE @CurrentResultMessage NVARCHAR(MAX);
    DECLARE @CurrentResultCode INT;
    
    WHILE @CurrentId <= @MaxId
    BEGIN
        SELECT 
            @CurrentReceiveId = r.ReceiveId,
            @CurrentTitle = r.Title,
            @CurrentReceiveIdType = m.receive_id_type,
            @CurrentContent = m.content,
            @CurrentButtonText = m.button_text,
            @CurrentButtonUrl = m.button_url
        FROM @Results r
        CROSS APPLY OPENJSON(@MessageList)
        WITH (
            receive_id NVARCHAR(100) '$.receive_id',
            receive_id_type NVARCHAR(20) '$.receive_id_type',
            title NVARCHAR(200) '$.title',
            content NVARCHAR(MAX) '$.content',
            button_text NVARCHAR(100) '$.button_text',
            button_url NVARCHAR(500) '$.button_url'
        ) m
        WHERE r.MessageId = @CurrentId AND m.receive_id = r.ReceiveId;
        
        EXEC dbo.sp_SendFeishuCardMessage
            @AppId = @AppId,
            @AppSecret = @AppSecret,
            @ReceiveId = @CurrentReceiveId,
            @ReceiveIdType = @CurrentReceiveIdType,
            @Title = @CurrentTitle,
            @Content = @CurrentContent,
            @ButtonText = @CurrentButtonText,
            @ButtonUrl = @CurrentButtonUrl,
            @ResultMessage = @CurrentResultMessage OUTPUT,
            @ResultCode = @CurrentResultCode OUTPUT;
        
        UPDATE @Results
        SET 
            ResultCode = @CurrentResultCode,
            ResultMessage = @CurrentResultMessage
        WHERE MessageId = @CurrentId;
        
        SET @CurrentId = @CurrentId + 1;
    END
    
    -- 生成汇总结果
    SELECT @ResultSummary = (
        SELECT 
            Title AS '消息标题',
            CASE WHEN ResultCode = 0 THEN '成功' ELSE '失败' END AS '发送状态',
            ResultMessage AS '结果详情'
        FROM @Results
        FOR JSON PATH
    );
END
GO

9. 错误处理与监控

9.1 完善的错误处理

sql 复制代码
ALTER PROCEDURE dbo.sp_SendFeishuCardMessage
    -- 原有参数...
AS
BEGIN
    BEGIN TRY
        BEGIN TRANSACTION;
        
        -- 参数验证
        IF @AppId IS NULL OR @AppSecret IS NULL OR @ReceiveId IS NULL OR @Title IS NULL OR @Content IS NULL
        BEGIN
            RAISERROR('Required parameters are missing', 16, 1);
            RETURN;
        END
        
        -- 验证receive_id_type
        IF @ReceiveIdType NOT IN ('open_id', 'user_id', 'email', 'chat_id')
        BEGIN
            SET @ReceiveIdType = 'open_id';
        END
        
        -- 调用CLR方法
        DECLARE @CLRResultMessage NVARCHAR(MAX);
        DECLARE @CLRResultCode INT;
        
        EXEC dbo.CLR_SendFeishuCardMessage
            @AppId = @AppId,
            @AppSecret = @AppSecret,
            @ReceiveId = @ReceiveId,
            @ReceiveIdType = @ReceiveIdType,
            @Title = @Title,
            @Content = @Content,
            @ButtonText = @ButtonText,
            @ButtonUrl = @ButtonUrl,
            @ResultMessage = @CLRResultMessage OUTPUT,
            @ResultCode = @CLRResultCode OUTPUT;
        
        -- 处理结果
        IF @CLRResultCode <> 0
        BEGIN
            -- 记录详细错误
            INSERT INTO dbo.ErrorLog (ProcedureName, ErrorMessage, ErrorDetails)
            VALUES ('sp_SendFeishuCardMessage', @CLRResultMessage, 
                   'AppId: ' + ISNULL(@AppId, 'NULL') + 
                   ', ReceiveId: ' + ISNULL(@ReceiveId, 'NULL'));
            
            -- 根据错误类型决定是否重试
            IF @CLRResultMessage LIKE '%token expired%' OR @CLRResultMessage LIKE '%invalid token%'
            BEGIN
                -- 令牌过期,清除缓存并重试一次
                DELETE FROM dbo.FeishuTokenCache WHERE AppId = @AppId;
                
                EXEC dbo.CLR_SendFeishuCardMessage
                    @AppId = @AppId,
                    @AppSecret = @AppSecret,
                    @ReceiveId = @ReceiveId,
                    @ReceiveIdType = @ReceiveIdType,
                    @Title = @Title,
                    @Content = @Content,
                    @ButtonText = @ButtonText,
                    @ButtonUrl = @ButtonUrl,
                    @ResultMessage = @CLRResultMessage OUTPUT,
                    @ResultCode = @CLRResultCode OUTPUT;
                
                IF @CLRResultCode <> 0
                    RAISERROR('Retry failed: %s', 16, 1, @CLRResultMessage);
            END
            ELSE
            BEGIN
                RAISERROR('%s', 16, 1, @CLRResultMessage);
            END
        END
        
        SET @ResultMessage = @CLRResultMessage;
        SET @ResultCode = @CLRResultCode;
        
        COMMIT TRANSACTION;
    END TRY
    BEGIN CATCH
        IF @@TRANCOUNT > 0
            ROLLBACK TRANSACTION;
        
        SET @ResultMessage = ERROR_MESSAGE();
        SET @ResultCode = -1;
        
        -- 记录未处理异常
        INSERT INTO dbo.ErrorLog (ProcedureName, ErrorMessage, ErrorDetails)
        VALUES ('sp_SendFeishuCardMessage', ERROR_MESSAGE(), 
               'Line: ' + CAST(ERROR_LINE() AS NVARCHAR(10)) + 
               ', State: ' + CAST(ERROR_STATE() AS NVARCHAR(10)));
    END CATCH
END
GO

9.2 监控与报警

创建监控存储过程,检查消息发送失败情况:

sql 复制代码
CREATE PROCEDURE dbo.sp_MonitorFeishuMessageFailures
    @HoursToCheck INT = 24,
    @AlertThreshold INT = 5
AS
BEGIN
    DECLARE @FailureCount INT;
    DECLARE @LastError NVARCHAR(MAX);
    
    SELECT 
        @FailureCount = COUNT(*),
        @LastError = MAX(ResultMessage)
    FROM dbo.FeishuMessageLog
    WHERE ResultCode <> 0
    AND SentTime > DATEADD(HOUR, -@HoursToCheck, GETDATE());
    
    IF @FailureCount >= @AlertThreshold
    BEGIN
        DECLARE @AlertTitle NVARCHAR(200) = '飞书消息发送失败警报';
        DECLARE @AlertContent NVARCHAR(MAX) = 
            '过去' + CAST(@HoursToCheck AS NVARCHAR) + '小时内共发生' + 
            CAST(@FailureCount AS NVARCHAR) + '次消息发送失败。\n\n' +
            '最后一次错误信息:\n' + @LastError;
        
        -- 发送给管理员,使用不同的接收ID
        EXEC dbo.sp_SendSimpleFeishuAlert
            @Title = @AlertTitle,
            @Content = @AlertContent,
            @IsSuccess = 0;
    END
END
GO

10. 完整示例与测试

10.1 数据库监控报警示例

sql 复制代码
-- 监控数据库空间并发送警报
CREATE PROCEDURE dbo.sp_CheckDatabaseSpaceAndAlert
    @SpaceThresholdMB INT = 1024 -- 1GB
AS
BEGIN
    DECLARE @FreeSpaceMB DECIMAL(10,2);
    DECLARE @DbName NVARCHAR(128) = DB_NAME();
    DECLARE @Title NVARCHAR(200);
    DECLARE @Content NVARCHAR(MAX);
    DECLARE @IsCritical BIT = 0;
    
    -- 获取数据库文件空间信息
    SELECT @FreeSpaceMB = SUM(CAST(available_bytes AS DECIMAL(10,2)) / 1024 / 1024)
    FROM sys.dm_os_volume_stats(DB_ID(), NULL) v
    CROSS APPLY sys.database_files f
    WHERE f.file_id = 1; -- 主要数据文件
    
    IF @FreeSpaceMB < @SpaceThresholdMB
    BEGIN
        SET @IsCritical = 1;
        SET @Title = '⚠️ 数据库空间不足警报 - ' + @DbName;
        SET @Content = '数据库 **' + @DbName + '** 剩余空间仅剩 ' + 
                      CAST(@FreeSpaceMB AS NVARCHAR(20)) + ' MB,低于阈值 ' + 
                      CAST(@SpaceThresholdMB AS NVARCHAR(20)) + ' MB。\n\n' +
                      '**建议立即处理**,否则可能导致数据库操作失败。';
    END
    ELSE
    BEGIN
        SET @Title = '✅ 数据库空间正常 - ' + @DbName;
        SET @Content = '数据库 **' + @DbName + '** 剩余空间充足: ' + 
                      CAST(@FreeSpaceMB AS NVARCHAR(20)) + ' MB,' +
                      '阈值: ' + CAST(@SpaceThresholdMB AS NVARCHAR(20)) + ' MB。';
    END
    
    -- 添加详细信息
    DECLARE @DetailInfo NVARCHAR(MAX) = '';
    
    SELECT @DetailInfo = @DetailInfo + 
        '文件: ' + name + '\n' +
        '类型: ' + CASE WHEN type = 0 THEN '数据' ELSE '日志' END + '\n' +
        '大小: ' + CAST(CAST(size * 8 / 1024 AS DECIMAL(10,2)) AS NVARCHAR(20)) + ' MB\n' +
        '使用率: ' + CAST(CAST((size - FILEPROPERTY(name, 'SpaceUsed')) * 8 / 1024 AS DECIMAL(10,2)) AS NVARCHAR(20)) + ' MB 空闲\n\n'
    FROM sys.database_files;
    
    SET @Content = @Content + '\n\n**详细文件信息**:\n' + @DetailInfo;
    
    -- 发送警报
    EXEC dbo.sp_SendSimpleFeishuAlert
        @Title = @Title,
        @Content = @Content,
        @IsSuccess = CASE WHEN @IsCritical = 1 THEN 0 ELSE 1 END,
        @ButtonText = CASE WHEN @IsCritical = 1 THEN '立即处理' ELSE NULL END,
        @ButtonUrl = CASE WHEN @IsCritical = 1 THEN 'https://your-domain.com/db-admin?action=cleanup' ELSE NULL END;
END
GO

10.2 业务通知示例

sql 复制代码
-- 订单处理完成通知
CREATE PROCEDURE dbo.sp_SendOrderProcessedNotification
    @OrderId INT,
    @CustomerName NVARCHAR(100),
    @OrderAmount DECIMAL(18,2),
    @ProcessedBy NVARCHAR(100),
    @ProcessTime DATETIME
AS
BEGIN
    DECLARE @Title NVARCHAR(200) = '订单处理完成通知 #' + CAST(@OrderId AS NVARCHAR(10));
    DECLARE @Content NVARCHAR(MAX) = 
        '**订单编号**: #' + CAST(@OrderId AS NVARCHAR(10)) + '\n' +
        '**客户名称**: ' + @CustomerName + '\n' +
        '**订单金额**: ¥' + CAST(@OrderAmount AS NVARCHAR(20)) + '\n' +
        '**处理人员**: ' + @ProcessedBy + '\n' +
        '**处理时间**: ' + CONVERT(NVARCHAR(20), @ProcessTime, 120) + '\n\n' +
        '订单已处理完成,请相关人员进行后续跟进。';
    
    EXEC dbo.sp_SendSimpleFeishuAlert
        @Title = @Title,
        @Content = @Content,
        @IsSuccess = 1,
        @ButtonText = '查看订单详情',
        @ButtonUrl = 'https://your-domain.com/orders/' + CAST(@OrderId AS NVARCHAR(10));
END
GO

10.3 测试脚本

sql 复制代码
-- 测试简单消息
DECLARE @ResultMsg NVARCHAR(MAX);
DECLARE @ResultCode INT;

EXEC dbo.sp_SendFeishuCardMessage
    @AppId = 'your_app_id',
    @AppSecret = 'your_app_secret',
    @ReceiveId = 'user_id_or_open_id',
    @Title = '测试消息',
    @Content = '这是一条测试消息,用于验证SQL Server发送飞书卡片的功能。',
    @ButtonText = '确认收到',
    @ButtonUrl = 'https://your-domain.com/confirm',
    @ResultMessage = @ResultMsg OUTPUT,
    @ResultCode = @ResultCode OUTPUT;

SELECT @ResultCode AS ResultCode, @ResultMsg AS ResultMessage;

-- 测试Markdown格式消息
DECLARE @MarkdownContent NVARCHAR(MAX) = 
'### 数据库性能报告\n' +
'**服务器**: DB-PROD-01\n' +
'**时间**: ' + CONVERT(NVARCHAR(20), GETDATE(), 120) + '\n\n' +
'#### 关键指标\n' +
'- CPU使用率: 78%\n' +
'- 内存压力: 65%\n' +
'- 活动连接数: 142\n\n' +
'```sql\n' +
'SELECT TOP 5 query_text, execution_count\n' +
'FROM sys.query_stats\n' +
'ORDER BY total_worker_time DESC;\n' +
'```';

EXEC dbo.sp_SendFeishuCardMessageWithMarkdown
    @AppId = 'your_app_id',
    @AppSecret = 'your_app_secret',
    @ReceiveId = 'user_id_or_open_id',
    @Title = '数据库性能报告',
    @MarkdownContent = @MarkdownContent,
    @ButtonText = '查看详细报告',
    @ButtonUrl = 'https://your-domain.com/db-reports/performance',
    @ResultMessage = @ResultMsg OUTPUT,
    @ResultCode = @ResultCode OUTPUT;

SELECT @ResultCode AS ResultCode, @ResultMsg AS ResultMessage;

-- 测试多列布局
EXEC dbo.sp_SendFeishuMultiColumnCard
    @AppId = 'your_app_id',
    @AppSecret = 'your_app_secret',
    @ReceiveId = 'user_id_or_open_id',
    @Title = '服务器状态概览',
    @Column1Title = '数据库服务器',
    @Column1Content = '**状态**: 正常运行\n' +
                     '**CPU**: 45%\n' +
                     '**内存**: 62%\n' +
                     '**磁盘**: 78%空闲',
    @Column2Title = '应用服务器',
    @Column2Content = '**状态**: 警告\n' +
                     '**CPU**: 85%\n' +
                     '**内存**: 90%\n' +
                     '**响应时间**: 2.4s',
    @ButtonText = '查看所有服务器',
    @ButtonUrl = 'https://your-domain.com/server-monitor',
    @ResultMessage = @ResultMsg OUTPUT,
    @ResultCode = @ResultCode OUTPUT;

SELECT @ResultCode AS ResultCode, @ResultMsg AS ResultMessage;

11. 总结与最佳实践

11.1 技术总结

本文详细介绍了如何在SQL Server环境中通过存储过程发送飞书卡片消息的完整实现方案,包括:

  1. CLR集成的基本原理和实现方法
  2. 飞书开放平台API的认证和消息发送流程
  3. 多种卡片消息格式的实现(基础、Markdown、多列布局)
  4. 安全存储敏感信息的策略
  5. 错误处理和监控机制
  6. 性能优化和批量处理技术

11.2 最佳实践

  1. 安全实践

    • 永远不要在代码中硬编码敏感信息
    • 使用SQL Server加密功能保护凭证
    • 实现最小权限原则
    • 记录详细的审计日志
  2. 性能实践

    • 缓存访问令牌减少API调用
    • 考虑批量发送减少网络开销
    • 异步处理非关键路径消息
  3. 维护实践

    • 统一管理消息模板
    • 定期检查错误日志
    • 监控消息发送成功率
    • 文档化接口和使用示例
  4. 扩展性考虑

    • 设计可插拔的架构,便于支持其他消息平台
    • 考虑消息队列处理高并发场景
    • 实现模板引擎支持动态内容

11.3 未来扩展方向

  1. 支持更多飞书卡片功能,如图片、视频等富媒体内容
  2. 实现消息回调和交互处理
  3. 开发管理界面用于消息模板管理
  4. 集成到SQL Server的扩展事件和警报系统
  5. 支持多租户和更复杂的权限控制

通过本文提供的完整解决方案,企业可以轻松地将SQL Server数据库系统与飞书协作平台集成,实现自动化的监控告警、业务通知和报表推送功能,大大提高工作效率和系统可靠性。

相关推荐
Favor_Yang12 小时前
SQL Server通过存储过程实现HTML页面生成
前端·信息可视化·sqlserver·存储过程
落笔画忧愁e2 天前
扣子Coze飞书多维表插件添加数据记录
java·服务器·飞书
白仑色3 天前
Oracle 存储过程、函数与触发器
数据库·oracle·数据库开发·存储过程·plsql编程
落笔画忧愁e3 天前
扣子Coze飞书多维表插件参数获取流程
飞书
白仑色3 天前
Oracle PL/SQL 编程基础详解(从块结构到游标操作)
数据库·oracle·数据库开发·存储过程·plsql编程
贾修行4 天前
SQL Server 空间函数从入门到精通:原理、实战与多数据库性能对比
数据库·sqlserver
Clownseven4 天前
Shell 脚本实战指南:内网 ARP 洪泛监控与飞书/邮件自动告警
网络·php·飞书
小爱同学ya4 天前
【效率提升教程】飞书自动化上传图片和文字
自动化·飞书
合作小小程序员小小店4 天前
web网页开发,在线%ctf管理%系统,基于html,css,webform,asp.net mvc, sqlserver, mysql
mysql·sqlserver·性能优化·asp.net·mvc