飞书卡片回调(官方推荐的长连接方式)

发布于:2025-06-14 ⋅ 阅读:(15) ⋅ 点赞:(0)

一、配置卡片

1、搭建飞书卡片地址

https://open.feishu.cn/cardkit

2、搭建飞书卡片

通过拖拉控件方式,并添加组件的变量

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

创建事件,传参给服务器,参数类型使用对象

在这里插入图片描述

注意:须记录左上角卡片ID,用于代码中调用

最后,保存并发布

二、搭建卡片交互机器人

1、地址:https://open.feishu.cn/app

2、配置机器人

记下App Id和App Secret用于代码中调用

在这里插入图片描述
事件配置与回调配置均使用【长连接】方式
使用长连接前需要开启长连接SDK中的Client,运行下面的代码模块即可。
[图片]在这里插入图片描述
自定义机器人事件按钮
https://open.feishu.cn/app/cli_a88f51b2463fd00c/bot
[图片]
在这里插入图片描述
通过推送事件,定义事件ID,用于代码中调用

三、机器人添加卡片

1、卡片地址

https://open.feishu.cn/cardkit

2、导入卡片或自己创建

在这里插入图片描述

3、卡片添加给机器人并保存发布

在这里插入图片描述
左上角的ID参数保存好,后继提供给开发人员

四、获取Open ID

1、入口地址

https://open.feishu.cn/document/faq/trouble-shooting/how-to-obtain-openid

2、进入API调试台

在这里插入图片描述

3、【切换应用】到发卡片的机器人,【查询参数】选择open_id,点击【快速复制 open_id】

在这里插入图片描述

五、飞书SDK

1、maven引用

com.larksuite.oapi oapi-sdk 2.4.13

2、工具类创建

import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.google.gson.JsonParser;
import com.lark.oapi.Client;
import com.lark.oapi.core.utils.Jsons;
import com.lark.oapi.event.EventDispatcher;
import com.lark.oapi.event.cardcallback.P2CardActionTriggerHandler;
import com.lark.oapi.event.cardcallback.model.CallBackCard;
import com.lark.oapi.event.cardcallback.model.CallBackToast;
import com.lark.oapi.event.cardcallback.model.P2CardActionTrigger;
import com.lark.oapi.event.cardcallback.model.P2CardActionTriggerResponse;
import com.lark.oapi.service.application.ApplicationService;
import com.lark.oapi.service.application.v6.model.P2BotMenuV6;
import com.lark.oapi.service.im.ImService;
import com.lark.oapi.service.im.v1.model.CreateMessageReq;
import com.lark.oapi.service.im.v1.model.CreateMessageReqBody;
import com.lark.oapi.service.im.v1.model.CreateMessageResp;
import com.lark.oapi.service.im.v1.model.P2ChatAccessEventBotP2pChatEnteredV1;
import com.lark.oapi.service.im.v1.model.P2MessageReceiveV1;
import com.lark.oapi.service.im.v1.model.ext.MessageTemplate;
import com.lark.oapi.service.im.v1.model.ext.MessageTemplateData;
import com.zs.project.feishu.service.FeiShuSendService;
import com.zs.project.resource.domain.CpeConfTryMth;
import com.zs.project.resource.service.IResourceCpeCustSalesFeeService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;

import javax.annotation.PostConstruct;
/**
 * 飞书卡片回调
 */
@Component
public class FeishuUtil {
    private Logger logger = LoggerFactory.getLogger(FeishuUtil.class);

    @Autowired
    private IResourceCpeCustSalesFeeService resourceCpeCustSalesFeeService;

    @Autowired
    private FeiShuSendService feiShuSendService;

    // 机器人ID
    private final String appId ;
    private final String appSecret ;
    // 卡片 ID
    private final String alertCardId;
    // 接收人ID
    private final String receiveIds;

    private String WELCOME_CARD_ID = System.getenv("WELCOME_CARD_ID");
    private String ALERT_RESOLVED_CARD_ID = System.getenv("ALERT_RESOLVED_CARD_ID");
    private ZoneId SHANGHAI_ZONE_ID = ZoneId.of("Asia/Shanghai");

    // 构造函数注入,Spring 会自动注入 @Value 的值
    @Autowired
    public FeishuUtil(
            @Value("${feishu.appId}") String appId,
            @Value("${feishu.appSecret}") String appSecret,
            @Value("${feishu.alertCardId}") String alertCardId,
            @Value("${feishu.receiveIds}") String receiveIds
    ) {
        this.appId = appId;
        this.appSecret = appSecret;
        this.alertCardId = alertCardId;
        this.receiveIds = receiveIds;
        this.client = new Client.Builder(appId, appSecret).build();
    }

    /**
     * 创建 LarkClient 对象,用于请求OpenAPI。
     * Create LarkClient object for requesting OpenAPI
     */
    private final Client client;

    /*
     *
     * 发送欢迎卡片
     * Send welcome card
     *
     */
    private void sendWelcomeCard(String openID) throws Exception {
        /*
         * 构造欢迎卡片
         * Construct a welcome card
         * https://open.feishu.cn/document/uAjLw4CM/ukzMukzMukzM/feishu-cards/send-feishu-card#718fe26b
         */
        String replyContent = new MessageTemplate.Builder()
                .data(new MessageTemplateData.Builder().templateId(WELCOME_CARD_ID)
                        .templateVariable(new HashMap<String, Object>() {
                            {
                                put("open_id", openID);
                            }
                        })
                        .build())
                .build();

        /**
         * 使用发送OpenAPI发送通知卡片,你可以在API接口中打开 API 调试台,快速复制调用示例代码
         * Use send OpenAPI to send notice card. You can open the API debugging console in the API interface and quickly copy the sample code for API calls.
         * https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/im-v1/message/create
         */
        CreateMessageReq req = CreateMessageReq.newBuilder()
                .receiveIdType("open_id")
                .createMessageReqBody(CreateMessageReqBody.newBuilder()
                        .receiveId(openID)
                        .msgType("interactive")
                        .content(replyContent)
                        .build())
                .build();

        // 发起请求
        CreateMessageResp resp = client.im().v1().message().create(req);

        // 处理服务端错误
        if(!resp.success()) {
            logger.info(String.format("code:%s,msg:%s,reqId:%s, resp:%s",
                    resp.getCode(), resp.getMsg(), resp.getRequestId(), Jsons.createGSON(true, false).toJson(JsonParser.parseString(new String(resp.getRawResponse().getBody(), StandardCharsets.UTF_8)))));
            return;
        }

        // 调用成功,打印返回结果
        logger.info(Jsons.DEFAULT.toJson(resp.getData()));

    }

    /**
     * 发送告警卡片
     * Send alarm card
     */
    public void sendAlarmCard(String receiveIdType, String receiveId) throws Exception {
        /*
         * 构造告警卡片
         * Construct an alarm card
         * https://open.feishu.cn/document/uAjLw4CM/ukzMukzMukzM/feishu-cards/send-feishu-card#718fe26b
         */
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMM");
        LocalDateTime today = LocalDateTime.now();
        // 上个月
        LocalDateTime lastMonth = today.minusMonths(1);

        String date = lastMonth.format(formatter);

        Map<String, String> cardData = getCardData(date);

        Map<String, Object> data = new HashMap<String, Object>();
        data.put("userNum", "<font color='orange'>**"+cardData.get("userNum")+"**</font>");
        data.put("amount", "<font color='orange'>**"+cardData.get("amount")+"**</font>");
        data.put("pushTime", "<font color='grey'>推送时间:"+cardData.get("now")+"</font>");
        data.put("queryDate", date);

        String replyContent = new MessageTemplate.Builder()
                .data(new MessageTemplateData.Builder().templateId(alertCardId)
                        .templateVariable(data)
                        .build())
                .build();

        /**
         * 使用发送OpenAPI发送告警卡片,根据传入的receiveIdType不同,可发送到用户单聊或群聊中。你可以在API接口中打开 API 调试台,快速复制调用示例代码
         * Use the Send OpenAPI to send an alarm card. Depending on the value of the incoming receiveIdType, it can be sent to an individual user chat or a group chat. You can open the API debugging console in the API interface and quickly copy the sample code for API calls.
         * https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/im-v1/message/create
         */
        CreateMessageReq req = CreateMessageReq.newBuilder()
                .receiveIdType(receiveIdType)
                .createMessageReqBody(CreateMessageReqBody.newBuilder()
                        .receiveId(receiveId)
                        .msgType("interactive")
                        .content(replyContent)
                        .build())
                .build();

        // 发起请求
        CreateMessageResp resp = client.im().v1().message().create(req);

        // 处理服务端错误
        if(!resp.success()) {
            logger.info(String.format("code:%s,msg:%s,reqId:%s, resp:%s",
                    resp.getCode(), resp.getMsg(), resp.getRequestId(), Jsons.createGSON(true, false).toJson(JsonParser.parseString(new String(resp.getRawResponse().getBody(), StandardCharsets.UTF_8)))));
            return;
        }

        // 调用成功,打印返回结果
        logger.info(Jsons.DEFAULT.toJson(resp.getData()));

    }

    /**
     * 注册事件处理器。
     * Register event handler.
     */
    private EventDispatcher EVENT_HANDLER = EventDispatcher.newBuilder("", "") // 长连接不需要这两个参数,请保持空字符串
            /**
             * 处理用户进入机器人单聊事件
             * handle user enter bot single chat event
             * https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/im-v1/chat-access_event/events/bot_p2p_chat_entered
             */
            .onP2ChatAccessEventBotP2pChatEnteredV1(new ImService.P2ChatAccessEventBotP2pChatEnteredV1Handler() {
                @Override
                public void handle(P2ChatAccessEventBotP2pChatEnteredV1 event) throws Exception {
                    System.out.printf("[ onP2ChatAccessEventBotP2pChatEnteredV1 access ], data: %s\n",
                            Jsons.DEFAULT.toJson(event.getEvent()));
                    String openID = event.getEvent().getOperatorId().getOpenId();
                    //sendWelcomeCard(openID);
                }

            })

            /**
             * 处理用户点击机器人菜单事件
             * handle user click bot menu event
             * https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/application-v6/bot/events/menu
             */
            .onP2BotMenuV6(new ApplicationService.P2BotMenuV6Handler() {
                @Override
                public void handle(P2BotMenuV6 event) throws Exception {
                    System.out.printf("[ onP2BotMenuV6 access ], data: %s\n", Jsons.DEFAULT.toJson(event));

                    /**
                     * 通过菜单 event_key 区分不同菜单。 你可以在开发者后台配置菜单的event_key
                     * Use event_key to distinguish different menus. You can configure the event_key
                     * of the menu in the developer console.
                     */
                    if ("CPE_SALES".equals(event.getEvent().getEventKey())) {
//                        String openID = event.getEvent().getOperator().getOperatorId().getOpenId();
                        for(String openID : receiveIds.split(",")) {
                            logger.info("onP2BotMenuV6 openID:" + openID);
                            sendAlarmCard("open_id", openID);
                        }
                    }

                }
            })

            /**
             * 接收用户发送的消息(包括单聊和群聊),接受到消息后发送告警卡片
             * Register event handler to handle received messages, including individual chats and group chats.
             * https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/im-v1/message/events/receive
             */
            .onP2MessageReceiveV1(new ImService.P2MessageReceiveV1Handler() {
                @Override
                public void handle(P2MessageReceiveV1 event) throws Exception {
                    System.out.printf("[ onP2MessageReceiveV1 access ], data: %s\n", Jsons.DEFAULT.toJson(event.getEvent()));
                    String type = event.getEvent().getMessage().getChatType();
                    String openID = event.getEvent().getSender().getSenderId().getOpenId();
                    String chatID = event.getEvent().getMessage().getChatId();
                    if (type.equals("group")) {
                        logger.info("onP2MessageReceiveV1 group");
                        //sendAlarmCard("chat_id", chatID);
                    } else if (type.equals("p2p")) {
                        logger.info("onP2MessageReceiveV1 p2p");
                        // sendAlarmCard("open_id", openID);
                    }

                }
            })

            /**
             * 处理卡片按钮点击回调
             * handle card button click callback
             * https://open.feishu.cn/document/uAjLw4CM/ukzMukzMukzM/feishu-cards/card-callback-communication
             */
            .onP2CardActionTrigger(new P2CardActionTriggerHandler() {
                @Override
                public P2CardActionTriggerResponse handle(P2CardActionTrigger event) throws Exception {
                    System.out.printf("[ P2CardActionTrigger access ], data: %s\n", Jsons.DEFAULT.toJson(event));
                    String openID = event.getEvent().getOperator().getOpenId();
                    String date = event.getEvent().getAction().getInputValue();
                    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMM");
                    LocalDateTime today = LocalDateTime.now();
                    // 当前月的上个月
                    if (event.getEvent().getAction().getValue().get("action") != null &&
                            event.getEvent().getAction().getValue().get("action").equals("lastMonth")) {
                        LocalDateTime lastMonth = today.minusMonths(1);
                        date = lastMonth.format(formatter);
                    } else {

                    }

                    /**
                     * 通过 action 区分不同按钮点击,你可以在卡片搭建工具配置按钮的action。此处处理用户点击了欢迎卡片中的发起告警按钮
                     * Use action to distinguish different buttons. You can configure the action of the button in the card building tool.
                     * Here, handle the situation where the user clicks the "Initiate Alarm" button on the welcome card.
                     *
                     */
//                        if (event.getEvent().getAction().getValue().get("action").equals("CPE_SALES")) {
//
//                            /**
//                             * 响应回调请求,保持卡片原内容不变
//                             * Respond to the callback request and keep the original content of the card unchanged.
//                             */
//                            P2CardActionTriggerResponse resp = new P2CardActionTriggerResponse();
//                            sendAlarmCard("open_id", openID);
//                            return resp;
//                        }

                    /**
                     * 通过 action 区分不同按钮, 你可以在卡片搭建工具配置按钮的action。此处处理用户点击了告警卡片中的已处理按钮
                     * Use action to distinguish different buttons. You can configure the action of the button in the card building tool.
                     * Here, handle the scenario where the user clicks the "Mark as resolved" button on the alarm card.
                     */

                    //  if (event.getEvent().getAction().getValue().get("action").equals("complete_alarm")) {
                    /**
                     * 读取告警卡片中用户填写的备注文本信息
                     * Read the note text information filled in by the user in the alarm card.
                     */
//                            String notes;
//                            if (event.getEvent().getAction().getFormValue() != null) {
//                                notes =String.valueOf(event.getEvent().getAction().getFormValue().get("notes_input"));
//                            } else {
//                                notes= "";
//                            }
//                            System.out.printf("[ notes ], data: %s\n", notes);
//                            /**
//                             * 通过卡片回传交互toast提示操作成功,并返回一个新卡片:已处理的卡片
//                             * Through the card callback interaction, display a toast to indicate successful operation and return a new card: the resolved card.
//                             */
                    P2CardActionTriggerResponse resp = new P2CardActionTriggerResponse();
                    CallBackToast toast = new CallBackToast();
                    //判断dateStr符不符合日期格式
                    if (!date.matches("^\\d{4}\\d{2}$")) {
                        toast.setType("error");
                        toast.setContent("请输入正确的日期格式YYYYMM");
                        resp.setToast(toast);
                        return resp;
                    }

                    toast.setType("info");
                    toast.setContent("已处理完成!");
                    toast.setI18n(new HashMap<String, String>() {
                        {
                            put("zh_cn", "已处理完成!");
                            put("en_us", "Resolved!");
                        }
                    });

                    Map<String, String> cardData = getCardData(date);

                    Map<String, Object> data = new HashMap<String, Object>();
                    data.put("userNum", "<font color='orange'>**"+cardData.get("userNum")+"**</font>");
                    data.put("amount", "<font color='orange'>**"+cardData.get("amount")+"**</font>");
                    data.put("pushTime", event.getEvent().getAction().getValue().get("pushTime"));
                    data.put("queryDate", date);

                    logger.info("userNum:"+cardData.get("userNum")+",amount:"+cardData.get("amount")+",now:"+cardData.get("now"));
                    CallBackCard card = new CallBackCard();
                    card.setType("template");
                    card.setData(new MessageTemplateData.Builder().templateId(alertCardId)
                            .templateVariable(data).build());

//                            CallBackCard card = new CallBackCard();
//                            card.setData(new MessageTemplateData.Builder().templateId(ALERT_RESOLVED_CARD_ID));
//                            card.setType("template");
//                            card.setData(new MessageTemplateData.Builder().templateId(ALERT_RESOLVED_CARD_ID)
//                                    .templateVariable(new HashMap<String, Object>() {
//                                        {
//                                            put("alarm_time", event.getEvent().getAction().getValue().get("time"));
//                                            put("open_id", openID);
//                                            put("complete_time", LocalDateTime.now(SHANGHAI_ZONE_ID).format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
//                                            put("notes", notes);
//                                        }
//                                    }).build());

                    resp.setCard(card);
                    resp.setToast(toast);
                    return resp;
//                        }
//                        return null;
                }
            }).build();


    /**
     * 获取卡片数据
     * @param date
     * @return
     */
    private Map<String, String> getCardData(String date) {
        DateTimeFormatter toDayformatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm");
        DateTimeFormatter dayformatter = DateTimeFormatter.ofPattern("yyyyMM");
        LocalDateTime today = LocalDateTime.now();
        String now = today.format(toDayformatter);
        String month = today.format(dayformatter);
        String dateType = "month";
        // 如果为当前月,则取日数据,否则取月数据
        if(month.equals(date)) {
            dateType = "day";
        }
        CpeConfTryMth cpeConfTryMth = new CpeConfTryMth();
        cpeConfTryMth.setDateType(dateType);
        cpeConfTryMth.setMonth(date);
        List<Map> list = resourceCpeCustSalesFeeService.statisticsCpeConfTry(cpeConfTryMth);
        String userNum = "0", amount = "0";
        if(ObjectUtils.isEmpty(list))  {
            userNum = "暂无数据";
            amount = "暂无数据";
        } else {
            for(int i=0; i<list.size(); i++) {
                Map map =  list.get(i);
                String type_= String.valueOf(map.get("type_"));
                String stats_= map.get("stats_") == null?"暂无数据":String.valueOf(map.get("stats_"));
                if ("count".equals(type_)) {
                    userNum = stats_;
                } else if ("sum".equals(type_)) {
                    amount = stats_;
                }
            }
        }
        Map<String, String> cardData = new HashMap<>();
        cardData.put("userNum", userNum);
        cardData.put("amount", amount);
        cardData.put("now", now);
        return cardData;
    }

    /**
     * 启动长连接
     */
    @PostConstruct
    public void start(){
        /**
         * 启动长连接,并注册事件处理器。
         * Start long connection and register event handler.
         * https://open.feishu.cn/document/server-docs/event-subscription-guide/event-subscription-configure-/request-url-configuration-case#d286cc88
         */
        com.lark.oapi.ws.Client wsClient = new com.lark.oapi.ws.Client.Builder(appId, appSecret)
                .eventHandler(EVENT_HANDLER).build();

        logger.info("Starting bot..."+appId);
        wsClient.start();
        logger.info("监听飞书多维表程序启动...");
    }
}


重点方法
onP2BotMenuV6 机器人菜单事件 CPE_SALES对应按钮事件ID
sendAlarmCard 发送卡片方法
可通过定时任务调用: feishuWangZhe.sendAlarmCard(“open_id”, feishuWangZhe_receiveId);
onP2CardActionTrigger 处理卡片回调方法
event.getEvent().getAction().getValue().get(“action”).equals(“参数名”) 通过此方法获取参数名action的值,判断是哪个按钮执行的操作。
下面代码返回结果给对应卡片中设定的对应变量在这里插入图片描述

3、测试回调

通过点击“CPE销售数据”按钮(或自行定义按钮)触发卡片。 点击卡片中的按钮调用代码中的回调方法

[图片]

四、其他

1、异常码处理

如遇异常,通过异常码在飞书URL中搜索,如: https://open.feishu.cn/search?from=header&page=1&pageSize=10&q=200671&topicFilter=&type=history

[图片]

如果智能助手提示的信息不能解决,可点击上面红框定位详细异常信息排查
[图片]