一、配置卡片
1、搭建飞书卡片地址
https://open.feishu.cn/cardkit2、搭建飞书卡片
通过拖拉控件方式,并添加组件的变量
创建事件,传参给服务器,参数类型使用对象
注意:须记录左上角卡片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/cardkit2、导入卡片或自己创建
3、卡片添加给机器人并保存发布
左上角的ID参数保存好,后继提供给开发人员
四、获取Open ID
1、入口地址
https://open.feishu.cn/document/faq/trouble-shooting/how-to-obtain-openid2、进入API调试台
3、【切换应用】到发卡片的机器人,【查询参数】选择open_id,点击【快速复制 open_id】
五、飞书SDK
1、maven引用
com.larksuite.oapi oapi-sdk 2.4.132、工具类创建
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如果智能助手提示的信息不能解决,可点击上面红框定位详细异常信息排查