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数据库系统与飞书协作平台集成,实现自动化的监控告警、业务通知和报表推送功能,大大提高工作效率和系统可靠性。

相关推荐
吃饭最爱4 天前
JUnit技术的核心和用法
数据库·oracle·sqlserver
tanxinji5 天前
SQLServer死锁监测方案:如何使用XE.Core解析xel文件里包含死锁扩展事件的死锁xml
sqlserver·死锁·扩展事件
代码的余温7 天前
SQL Server全链路安全防护
数据库·安全·sqlserver
张人玉7 天前
SQLSERVER数据备份
数据库·oracle·sqlserver
我想起个名字8 天前
sqlserver2008导入excel表数据遇到的问题
sqlserver·excel
浊尘8 天前
SQL server实现异地增量备份和全量备份
数据库·sqlserver
代码的余温9 天前
SQL Server服务管理
数据库·sqlserver
代码的余温9 天前
解析SQL Server核心服务与功能
数据库·sqlserver
YoungUpUp10 天前
【SQL Server 2022】保姆级SQL Server 详细图文下载安装教程
数据库·sql·sqlserver·sql server·sql server数据库·sql server 2022·sql 数据库
代码的余温10 天前
SQL Server核心架构深度解析
数据库·sqlserver·架构