DingTalk钉钉机器人单聊互动卡片消息的一次实现


dingtalk-module迭代记录

  • 07-08-22: 完善一下一些定义的接口, 方便理解
  • 07-26-22: 看了下文档, 钉钉更新了两种互动卡片, 此文档实现的是“互动卡片高级版”
  • 08-09-22: 对于markdown类型消息的补充
  • 08-19-22: 增加了一个封装卡片按钮回调请求体的对象
  • 11-03-22: 抽象项目中对钉钉机器人消息校验的方法,以支持更多校验方式
  • 11-04-22: 修改此前文档中互动卡片回调地址的接口方法, 描述不准确容易误解, 改为 getCallbackRouteKey()
  • 03-17-23: 优化消息接口定义及实现, 增加一个demo演示效果 dingtalk-module-demo
  • 03-20-23: 调整对微应用发送消息的设计, 机器人通过Webhook发送消息暂时没需求,以后集成
  • 03-24-23: 钉钉企业内应用提供第三方网站的免登,及钉钉工作台的免登支持, 功能对接快的丫批.
  • 03-30-23: 状态机完成机器人单聊消息回调交互的抽象设计

文章目录

  • DingTalk钉钉机器人单聊互动卡片消息的一次实现
    • 前言
      • 需求
      • 问题
    • 一、前置要求
      • 1.1 配置互动卡片
      • 1.2 引入项目依赖
    • 二、代码实现
      • 1.1 钉钉应用抽象
      • 1.2 卡片回调接口抽象
      • 1.3 消息的抽象
      • 1.4 互动卡片消息抽象
      • 2.1 抽象的实现及配置
        • 2.1.1 配置
        • 2.1.2 机器人实现
        • 2.1.3 卡片回调实现
        • 2.1.4 卡片实现
      • 2.2 注册互动卡片按钮的回调URL
      • 2.3 机器人通过接口发送消息处理器抽象
      • 2.4 发送互动卡片消息
      • 2.5 用户点击互动卡片的互动按钮
      • 2.6 更新卡片数据
      • 2.7 业务API
    • 三、其他
      • 1.1 常见问题
      • 1.2 尝试
    • 四、总结

前言

互动卡片支持自定义按钮, 并在按钮上绑定回调事件, 点击按钮时将通过提前注册的回调URL, 携带卡片数据及配置的自定义按钮参数调用回调URL, 开发者拿到请求后完成业务处理, 并通过更新互动卡片接口将此前发送的卡片数据更新. 按钮的状态, 显示与否均可动态配置.

模块最主要就是对调用SDK接口需要的杂乱无序的参数做抽象和做最常用的封装, 尽可能简化调用.

需求

网站需要在用户注册后发送钉钉消息给客户经理, 同时客户经理操作按钮功能为新用户开通体验服务, 钉钉机器人发互动卡片消息即可满足.

问题

网上群发,单聊的普通消息的实现都有,但也没有说的很清晰的, 群发的互动卡片也是, 但单聊的互动卡片在实现的时候没查到有人写, 钉钉文档关于机器人部分也是一句话带过.

一、前置要求

1.1 配置互动卡片

钉钉开放平台-开放能力-卡片平台 旧版的平台已经迁移到上述平台

这里没有太多可说的, 基本上就是自己建模版, 提示的教程也很简单, 回调的请求上可以携带自定义的参数, 其他不重要不在此过多赘述

1.2 引入项目依赖

分别为旧新版SDK都要引入, 新版SDK在持续迭代中

<!--new-->
<dependency><groupId>com.aliyun</groupId><artifactId>dingtalk</artifactId><version>1.5.58</version>
</dependency>
<!--old-->
<dependency><groupId>com.aliyun</groupId><artifactId>alibaba-dingtalk-service-sdk</artifactId><version>2.0.0</version>
</dependency>

二、代码实现

至于抽象设计, 纯用SDK调接口写硬编码当然可以, 但写久了就像有蚂蚁在身上爬, 能抽象还是尽量抽象

1.1 钉钉应用抽象

package com.hp.dingding.component.application;import com.hp.dingding.component.factory.app.DingAppFactory;
import org.springframework.beans.factory.SmartInitializingSingleton;/*** 钉钉应用* 整个项目基于Spring容器能力,所以最好将APP实现注册到IoC容器中* @author hp*/
public interface IDingApp extends SmartInitializingSingleton {String getAppName();String getAppKey();String getAppSecret();Long getAppId();@Overridedefault void afterSingletonsInstantiated() {DingAppFactory.setAppCache(this);}
}

1.2 卡片回调接口抽象

07-08-22: 钉钉实现了新的注册回调地址的方式, 这里还是历史方式主动调用接口注册, 也能用

package com.hp.dingding.pojo.message.interactive.callback;import com.hp.dingding.component.application.IDingBot;
import org.springframework.beans.factory.SmartInitializingSingleton;import java.util.List;/*** 互动卡片回调地址* @author hp*/
public interface IDingInteractiveCardCallBack extends SmartInitializingSingleton {String getCallbackUrl();String getCallbackRouteKey();List<Class<? extends IDingBot>> getDingBots();
}
package com.hp.dingding.pojo.message.interactive.callback;import com.hp.dingding.component.application.IDingBot;
import lombok.Getter;import java.util.List;/*** @author hp 2023/3/17*/
@Getter
public abstract class AbstractDingInteractiveCardCallback implements IDingInteractiveCardCallBack {private final String callbackRouteKey;private final String callbackUrl;private final List<Class<? extends IDingBot>> dingBots;public AbstractDingInteractiveCardCallback(String callbackRouteKey, String callbackUrl, List<Class<? extends IDingBot>> dingBots) {this.callbackRouteKey = callbackRouteKey;this.callbackUrl = callbackUrl;this.dingBots = dingBots;}
}

1.3 消息的抽象

package com.hp.dingding.pojo.message;import com.google.gson.Gson;/*** @author hp*/
public interface IDingMsg {/*** 获取以类名直接定义的消息类型** @param msg 某个具体类型的消息* @return 消息类型(类名第一个字母小写)*/default String msgType(Object msg) {final char[] chars = msg.getClass().getSimpleName().toCharArray();chars[0] += 32;return String.valueOf(chars);}/*** 消息类型** @return 消息类型*/String getMsgType();/*** 消息内容** @return json消息内容*/default String getMsgParam() {return new Gson().toJson(this);}
}

1.4 互动卡片消息抽象

package com.hp.dingding.pojo.message.interactive;import com.hp.dingding.pojo.message.IDingMsg;
import org.springframework.cglib.beans.BeanMap;
import org.springframework.util.Assert;
import org.springframework.util.DigestUtils;import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;public interface IDingInteractiveMsg extends IDingMsg {/*** 互动卡片全局密码盐,用于对消息对sign值加密*/String GLOBAL_SALT = "dfaen23djf461nJ51FHDowie17hf1";/*** 同一用这个方法转map,或者改hutool也行*/default Map<String, String> getMap() {final BeanMap beanMap = BeanMap.create(this);final Map<String, String> map = new HashMap<>();beanMap.forEach((k, v) -> {if (v != null) {map.put(String.valueOf(k), String.valueOf(v));}});return map;}/*** 主要接口,看作消息唯一id*/String getOutTrackId();/*** 主要参数* 卡片的回调路由key,接口也只能配置一个key,多按钮通过接口参数区分*/String getCallbackRouteKey();/*** 业务历史遗留,简单的通过该加密字段对回调参数做校验*/String getSign();/*** 主要参数* 互动卡片模版Id,卡片后台配置后获取(不需要发布)*/String getTemplateId();/*** 字段加密方法,简单加密*/static String encryptSign(String outTrackId) {Assert.notNull(outTrackId, "参数异常:outTrackId缺失");final String combine = outTrackId + GLOBAL_SALT;return DigestUtils.md5DigestAsHex(combine.getBytes(StandardCharsets.UTF_8));}/*** 空实现* @return*/@Overridedefault String getMsgType(){return null;}
}
package com.hp.dingding.pojo.message.interactive;import com.hp.dingding.pojo.message.interactive.callback.IDingInteractiveCardCallBack;
import lombok.Getter;
import lombok.Setter;/*** @author hp*/
@Setter
@Getter
public abstract class AbstractDingInteractiveMsg implements IDingInteractiveMsg {public String templateId;public String callbackRouteKey;public String outTrackId;public String sign;public AbstractDingInteractiveMsg(IDingInteractiveCardCallBack callBack, String outTrackId, String templateId) {this.callbackRouteKey = callBack.getCallbackRouteKey();this.outTrackId = outTrackId;this.templateId = templateId;this.sign = IDingInteractiveMsg.encryptSign(this.outTrackId);}
}

2.1 抽象的实现及配置

2.1.1 配置

package com.hp.dingtalk.demo.infrastructure.config;import com.hp.dingding.component.application.IDingBot;
import com.hp.dingding.component.application.IDingMiniH5;
import com.hp.dingding.pojo.message.interactive.callback.IDingInteractiveCardCallBack;
import com.hp.dingtalk.demo.domain.app.DummyMiniH5App;
import com.hp.dingtalk.demo.domain.message.interactive_card.DummyInteractiveCardCallback;
import com.hp.dingtalk.demo.domain.robot.TestBot;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.util.Collections;/*** 配置类* 或者更简化使用匿名内部类来注册bean,这里为了方便理解,还是通过完整实现类注册** @author hp 2023/3/17*/
@Configuration
public class DingConfig {/*** 如果要做环境隔离,使用此方式配置* 否则硬编码+component或者通过configurationProperties自动配置也行* 最终目的是将应用注册到工厂中,后续accessToken和接口调用*/@Beanpublic IDingBot testBot() {// TODO change the constructor parameters to your own DingTalk application info. app name is for log.// Note that the app name is only for logging readability and is fully customizable by youreturn new TestBot("custom bot name", 16757015L, "your bot key", "your bot secret");}@Beanpublic IDingMiniH5 dummyMiniH5App() {return new DummyMiniH5App("custom app name", 16757015L, "your app key", "your app secret");}/*** 如果要做环境隔离,使用此方式配置* 否则硬编码+component或者通过configurationProperties自动配置也行*/@Beanpublic IDingInteractiveCardCallBack dummyInteractiveCardCallback() {// TODO change the constructor parameters to your own custom configuration.// Note that the third parameter, robots on your DingTalk, indicates that those robots will be used to register callback url when the system is ready.return new DummyInteractiveCardCallback("your callback route key(no space)", "http://sdavw8.natappfree.cc/dummy/test/callback", Collections.singletonList(TestBot.class));}
}

2.1.2 机器人实现

package com.hp.dingtalk.demo.domain.robot;import com.hp.dingding.component.application.IDingBot;
import lombok.Getter;/*** @author hp 2023/3/17*/
@Getter
public class TestBot implements IDingBot {private final String appName;private final Long appId;private final String appKey;private final String appSecret;public TestBot(String appName, Long appId, String appKey, String appSecret) {this.appName = appName;this.appId = appId;this.appKey = appKey;this.appSecret = appSecret;this.afterSingletonsInstantiated();}
}

2.1.3 卡片回调实现

package com.hp.dingtalk.demo.domain.message.interactive_card;import com.hp.dingding.component.application.IDingBot;
import com.hp.dingding.pojo.message.interactive.callback.AbstractDingInteractiveCardCallback;import java.util.List;/*** @author hp 2023/3/17*/
public class DummyInteractiveCardCallback extends AbstractDingInteractiveCardCallback {public DummyInteractiveCardCallback(String callbackRouteKey, String callbackUrl, List<Class<? extends IDingBot>> dingBots) {super(callbackRouteKey, callbackUrl, dingBots);}
}

2.1.4 卡片实现

package com.hp.dingtalk.demo.domain.message.interactive_card;import com.hp.dingding.pojo.message.interactive.AbstractDingInteractiveMsg;
import com.hp.dingding.pojo.message.interactive.callback.IDingInteractiveCardCallBack;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;/*** @author hp 2023/3/17*/
@Accessors(chain = true)
@Getter
@Setter
public class DummyInteractiveCard extends AbstractDingInteractiveMsg {/*** 模版属性*/private String dummyName;private String otherInformation;private int button;/*** 模版id可以写死或者暂时自己实现动态配置*/public DummyInteractiveCard(IDingInteractiveCardCallBack callBack, String outTrackId) {// TODO change the templateId parameter to your own template id that you previously created on the DingTalk interactive card platform.super(callBack, outTrackId, "your interactive card template id");}
}

2.2 注册互动卡片按钮的回调URL

因为只要注册一次, 我在SpringBoot启动后通过事件机制调用注册流程

package com.hp.dingding.component;import com.hp.dingding.component.application.IDingBot;
import com.hp.dingding.component.factory.app.DingAppFactory;
import com.hp.dingding.pojo.message.interactive.callback.IDingInteractiveCardCallBack;
import com.hp.dingding.service.IDingInteractiveMessageHandler;
import com.taobao.api.ApiException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;import java.util.List;/*** springboot ready后 调用注册** @author hp*/
@Slf4j
@Component
@RequiredArgsConstructor
public class DingBooter implements ApplicationListener<ApplicationReadyEvent> {private final List<IDingInteractiveCardCallBack> callBacks;@Overridepublic void onApplicationEvent(@NotNull ApplicationReadyEvent applicationReadyEvent) {if (CollectionUtils.isEmpty(callBacks)) {return;}callBacks.forEach(callBack ->callBack.getDingBots().forEach(clazz -> {final IDingBot app = DingAppFactory.app(clazz);try {IDingInteractiveMessageHandler.registerCallBackUrl(app, callBack, true);} catch (ApiException e) {log.error("注册回调地址失败:应用:{}, 回调地址:{}, 路由键:{}", app.getAppName(), callBack.getCallbackUrl(), callBack.getCallbackRouteKey());log.error("注册回调地址失败:异常:{}", e.getCause(), e);}}));}
}

2.3 机器人通过接口发送消息处理器抽象

对于机器人应用, 钉钉文档中提及两种发送消息方式

  1. 通过消息接口发送, 本例
  2. 通过webhook发送, 例如用户向机器人发送单聊消息, 钉钉调用开发者在应用后台配置的回调接口, 请求体中包含了webhooh地址和webhook Session的信息用于此场景发送消息
package com.hp.dingding.service;import com.dingtalk.api.DefaultDingTalkClient;
import com.dingtalk.api.DingTalkClient;
import com.dingtalk.api.request.OapiImChatScencegroupInteractivecardCallbackRegisterRequest;
import com.hp.dingding.component.IDingApi;
import com.hp.dingding.component.application.IDingBot;
import com.hp.dingding.component.factory.token.DingAccessTokenFactory;
import com.hp.dingding.constant.DingConstant;
import com.hp.dingding.pojo.message.interactive.IDingInteractiveMsg;
import com.hp.dingding.pojo.message.interactive.callback.IDingInteractiveCardCallBack;
import com.taobao.api.ApiException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;import java.util.List;/*** 发送钉钉互动卡片,高级版,普通版暂不实现* 以下接口都是使用钉钉userId模式,不推荐unionId模式* <p>* unionid是员工在当前开发者企业账号范围内的唯一标识,由系统生成:* 同一个企业员工,在不同的开发者企业账号下,unionid是不相同的。* 在同一个开发者企业账号下,unionid是唯一且不变的,例如同一个服务商开发的多个应用,或者是扫码登录等场景的多个App账号。** @author hp*/
public interface IDingInteractiveMessageHandler extends IDingApi {Logger logger = LoggerFactory.getLogger(IDingInteractiveMessageHandler.class);/*** 发送互动卡片至单聊* 机器人对用户单聊* 这里不对是否转发,针对某个用户的私域数据,@用户做处理** @param bot            调用应用* @param userIds        接收用户* @param interactiveMsg 卡片消息:包含必要配置信息:卡片id,callbackUrl等* @return outTrackId for additional usage*/String sendInteractiveMsgToIndividual(IDingBot bot, List<String> userIds, IDingInteractiveMsg interactiveMsg);/*** 发送互动卡片至群聊** @param bot                调用应用* @param userIds            为空则群内所有成员可见,不为空则指定人员可见* @param openConversationId 场景群id* @param interactiveMsg     卡片消息:包含必要配置信息:卡片id,callbackUrl等* @return outTrackId* @deprecated 还没仔细验证过群聊场景,验证后再逐渐增加业务常用参数,近期业务用不到群聊场景,暂时不更新*/@DeprecatedString sendInteractiveMsgToGroup(IDingBot bot, List<String> userIds, String openConversationId, IDingInteractiveMsg interactiveMsg);/*** 更新互动卡片** @param bot                调用应用* @param openConversationId 场景群id* @param interactiveMsg     卡片消息:包含必要配置信息:卡片id,callbackUrl等* @return 互动卡片唯一id(outTrackId)*/String updateInteractiveMsg(IDingBot bot, String openConversationId, IDingInteractiveMsg interactiveMsg);/*** 注册互动卡片回调api** @param bot         调用应用* @param callback    回调url配置* @param forceUpdate 是否强制更新,在不区分环境的情况下,强制更新,容易出现开发或测试更新到上线的问题,项目启动时默认为true,期望客户端能正确将配置通过环境区分* @throws ApiException 调用钉钉API异常*/static void registerCallBackUrl(IDingBot bot, IDingInteractiveCardCallBack callback, boolean forceUpdate) throws ApiException {if (bot == null || callback == null) {return;}logger.info("注册回调地址,应用:{},路由键:{},路由地址:{}", bot.getAppName(), callback.getCallbackRouteKey(), callback.getCallbackUrl());DingTalkClient client = new DefaultDingTalkClient(DingConstant.REGISTER_CALLBACK);OapiImChatScencegroupInteractivecardCallbackRegisterRequest requset = new OapiImChatScencegroupInteractivecardCallbackRegisterRequest();requset.setCallbackUrl(callback.getCallbackUrl());requset.setApiSecret(bot.getAppSecret());requset.setCallbackRouteKey(callback.getCallbackRouteKey());requset.setForceUpdate(forceUpdate);client.execute(requset, DingAccessTokenFactory.accessToken(bot));}
}

2.4 发送互动卡片消息

官方文档一句话: 非场景群机器人单聊发送:chatBotId和robotCode都不填写,直接用支持单聊的机器人应用来发送。

2.3具体实现

  @Overridepublic String sendInteractiveMsgToIndividual(IDingBot bot, List<String> userIds, IDingInteractiveMsg interactiveMsg) {log.debug("机器人发送互动卡片至用户,机器人:{},用户id:{},内容:{}", bot.getAppName(), userIds, interactiveMsg.getMap());try {com.aliyun.dingtalkim_1_0.Client client = new com.aliyun.dingtalkim_1_0.Client(this.config());SendInteractiveCardHeaders sendInteractiveCardHeaders = new SendInteractiveCardHeaders();sendInteractiveCardHeaders.xAcsDingtalkAccessToken = DingAccessTokenFactory.accessToken(bot);SendInteractiveCardRequest.SendInteractiveCardRequestCardData cardData = new SendInteractiveCardRequest.SendInteractiveCardRequestCardData();cardData.setCardParamMap(interactiveMsg.getMap());SendInteractiveCardRequest sendInteractiveCardRequest = new SendInteractiveCardRequest().setCardTemplateId(interactiveMsg.getTemplateId()).setReceiverUserIdList(userIds).setConversationType(0).setCallbackRouteKey(interactiveMsg.getCallbackRouteKey()).setCardData(cardData).setOutTrackId(interactiveMsg.getOutTrackId());client.sendInteractiveCardWithOptions(sendInteractiveCardRequest, sendInteractiveCardHeaders, new RuntimeOptions());return interactiveMsg.getOutTrackId();} catch (TeaException err) {if (StringUtils.hasText(err.code) && StringUtils.hasText(err.message)) {log.error("机器人发送互动卡片至用户异常,机器人:{},异常信息:{},{}", bot.getAppName(), err.code, err.message);}throw new DingApiException("机器人发送互动卡片至用户异常", err);} catch (Exception e) {TeaException err = new TeaException(e.getMessage(), e);if (StringUtils.hasText(err.code) && StringUtils.hasText(err.message)) {log.error("机器人发送互动卡片至用户异常,机器人:{},异常信息:{},{}", bot.getAppName(), err.code, err.message);}throw new DingApiException("机器人发送互动卡片至用户异常", e);}}

2.5 用户点击互动卡片的互动按钮

钉钉调用2.2注册的URLPOST请求

outTrackId(卡片唯一id)回调的时候可以用这个参数更新卡片状态

    @PostMapping("此前自定义的callbackurl")public void interactiveCardCallback(//此类在仓库中可以找到,根据回调数据写的一个对象@RequestBody DingInteractiveCardCallBackPayload payload
) {try {final DingInteractiveCardCallBackPayload.Value value = payload.getValue();final DingInteractiveCardCallBackPayload.CardPrivateData cardPrivateData = value.getCardPrivateData();//使用自己的业务对象获取编辑卡片按钮回调时自定义携带的参数/* * 问题: 因为返回的json参数层级较多, 自定义的返回参数在最里面一层, gson无法直接将这部分数据序列化为业务对象, 返回的是一个map集合, * 这里的操作就是重写get时再用gson反序列化一下, 能到达效果, 但感觉不太好*/final T params = cardPrivateData.getParams(T.class);//根据卡片实现的卡片模版类中的自定义sign属性值简单校验一下 (不强制)Assert.isTrue(Objects.equals(IDingInteractiveMsg.encryptSign(payload.getOutTrackId()), params.getSign()), "!!! ILLEGAL ACCESS !!!");//完成业务操作......//最后获取此前提交的卡片数据, 我这里是从db查出IDingInteractiveMsg msg = (IDingInteractiveMsg) new Gson().fromJson(message, Class.forName(messageLog.getClazz()));//调用更新卡片接口, 这里的 msg 对于我写的钉钉模块中就是互动卡片消息类new DingBotMessageHandler().updateInteractiveMsg(DingAppFactory.app(Long.valueOf(messageLog.getAppId())), null, msg);//方法结束}

2.6 更新卡片数据

此次处理是在发送消息时保存卡片的json数据, 需要更新时查出来直接反序列化为对应卡片模版实例, 更新值对象后调用接口更新卡片数据

 @SneakyThrows@Overridepublic String updateInteractiveMsg(IDingBot bot, String openConversationId, IDingInteractiveMsg interactiveMsg) {log.debug("机器人更新互动卡片至用户,机器人:{},会话Id:{},内容:{}", bot.getAppName(), openConversationId, interactiveMsg.getMap());com.aliyun.dingtalkim_1_0.Client client = new com.aliyun.dingtalkim_1_0.Client(this.config());UpdateInteractiveCardHeaders updateInteractiveCardHeaders = new UpdateInteractiveCardHeaders();updateInteractiveCardHeaders.xAcsDingtalkAccessToken = DingAccessTokenFactory.accessToken(bot);UpdateInteractiveCardRequest.UpdateInteractiveCardRequestCardOptions cardOptions = new UpdateInteractiveCardRequest.UpdateInteractiveCardRequestCardOptions().setUpdateCardDataByKey(true).setUpdatePrivateDataByKey(true);UpdateInteractiveCardRequest.UpdateInteractiveCardRequestCardData cardData = new UpdateInteractiveCardRequest.UpdateInteractiveCardRequestCardData().setCardParamMap(interactiveMsg.getMap());UpdateInteractiveCardRequest updateInteractiveCardRequest = new UpdateInteractiveCardRequest().setOutTrackId(interactiveMsg.getOutTrackId()).setCardData(cardData).setUserIdType(1).setCardOptions(cardOptions);try {client.updateInteractiveCardWithOptions(updateInteractiveCardRequest, updateInteractiveCardHeaders, new RuntimeOptions());return interactiveMsg.getOutTrackId();} catch (TeaException err) {if (StringUtils.hasText(err.code) && StringUtils.hasText(err.message)) {log.error("机器人更新互动卡片至用户异常,机器人:{},异常信息:{},{}", bot.getAppName(), err.code, err.message);}throw new DingApiException("机器人更新互动卡片至用户异常", err);} catch (Exception e) {TeaException err = new TeaException(e.getMessage(), e);if (StringUtils.hasText(err.code) && StringUtils.hasText(err.message)) {log.error("机器人更新互动卡片至用户异常,机器人:{},异常信息:{},{}", bot.getAppName(), err.code, err.message);}throw new DingApiException("机器人更新互动卡片至用户异常", e);}}

2.7 业务API

package com.hp.dingtalk.demo.controller;import com.dingtalk.api.response.OapiV2UserGetResponse;
import com.google.gson.Gson;
import com.hp.dingding.component.factory.DingAppFactory;
import com.hp.dingding.pojo.callback.DingInteractiveCardCallBackPayload;
import com.hp.dingding.pojo.message.interactive.IDingInteractiveMsg;
import com.hp.dingding.pojo.message.interactive.callback.IDingInteractiveCardCallBack;
import com.hp.dingding.service.message.DingBotMessageHandler;
import com.hp.dingding.service.user.DingUserHandler;
import com.hp.dingtalk.demo.domain.login.request.DingTalkLoginRequest;
import com.hp.dingtalk.demo.domain.login.service.DingTalkLoginService;
import com.hp.dingtalk.demo.domain.message.interactive_card.DummyInteractiveCard;
import com.hp.dingtalk.demo.domain.robot.TestBot;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.time.LocalDateTime;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;/*** @author hp 2023/3/17*/
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/dummy")
public class DummyApiController {private final IDingInteractiveCardCallBack dummyInteractiveCardCallback;private final DingTalkLoginService dingTalkLoginService;private static final Map<String, IDingInteractiveMsg> LOCAL_CACHE = new HashMap<>(16);@PostMapping("/test/interactive-card")public String testSendInteractiveCardMessage() {final DingBotMessageHandler dingBotMessageHandler = new DingBotMessageHandler();final DingUserHandler dingUserHandler = new DingUserHandler();final DummyInteractiveCard card = new DummyInteractiveCard(dummyInteractiveCardCallback, String.valueOf(System.currentTimeMillis())).setDummyName(" Hello Human ").setOtherInformation(" Surprise Mother发卡... ").setButton(0);final TestBot app = DingAppFactory.app(TestBot.class);// TODO change the second parameter to a phone number that exists in your organization.final String dingTalkUserId = dingUserHandler.findUserIdByMobile(app, "The phone number that equals the phone number on the DingTalk profile");final String outTrackId = dingBotMessageHandler.sendInteractiveMsgToIndividual(app, Collections.singletonList(dingTalkUserId), card);LOCAL_CACHE.putIfAbsent(outTrackId, card);return "successfully sent, and the outTrackId is: " + outTrackId;}@PostMapping("/test/callback")public void testUpdateInteractiveCardMessage(@RequestBody DingInteractiveCardCallBackPayload payload) {log.info("callback payload: {}", new Gson().toJson(payload));final String outTrackId = payload.getOutTrackId();final IDingInteractiveMsg msg = LOCAL_CACHE.get(outTrackId);if (!(msg instanceof DummyInteractiveCard)) {return;}((DummyInteractiveCard) msg).setDummyName("Oops! I got updated").setOtherInformation(" Updated Time: " + LocalDateTime.now()).setButton(1);// openConversationId 为null 钉钉根据卡片实例id更新卡片new DingBotMessageHandler().updateInteractiveMsg(DingAppFactory.app(TestBot.class), null, msg);}@PostMapping("/test/login")public String login(@RequestBody DingTalkLoginRequest request) {final OapiV2UserGetResponse.UserGetResponse userGetResponse = dingTalkLoginService.queryDingDing(request);final String userid = userGetResponse.getUserid();/*...后续查询系统用户信息校验等等*/return "token:" + userid;}
}

三、其他

这里调用的效果就不展示了, 如果感兴趣, 可以尝试配置钉钉开放后台的相关资源后, 填写相关配置到dingtalk-module-demo启动调用测试下.

1.1 常见问题

  • 2022-07-26

    有人问多个按钮回调时如何区分, 这里配置时并没有对按钮区分, 都是同一个回调接口,但是参数可以自定义, 参数一旦自定义,那么如何区分问题也就一目了然了

1.2 尝试

在模块中实现了一个简单的状态机, 最近在用其配合钉钉机器人的消息回调功能模拟类似shell交互的方式提供业务功能, 有兴趣的同好可以看下模块里状态机的实现, 提提优化意见或建议. much appreciate

DingTalk钉钉机器人单聊互动卡片消息的一次实现(附仓库)相关推荐

  1. 钉钉机器人单聊实现互动卡片推送

    钉钉开发机器人单聊业务实现 背景 钉钉待办 钉钉卡片 卡片类型的工作通知 创建并投放卡片 机器人发送互动卡片 实现 过程搭建 1. 授权 2. 创建机器人 3. 创建卡片 变量创建 链接问题 4. 卡 ...

  2. 小飞鱼通达二开 PHP使用钉钉接口发送带链接的卡片消息(代码)

    普通的钉钉消息,采用小飞鱼上一篇文章的方法进行发送即可,很多时候我们需要在发送消息的同时想让接收人看到消息后可以直接点击链接进入到应用内进行相关操作,这里我们就采用了卡片消息的方法,当然钉钉还提供有很 ...

  3. 【钉钉-场景化能力包】IoT物联网设备协同

    需求场景 环境预警监控:企业与监控数据实时同步,当设备监测到环境异常时,需要通过机器人发送预警信息到个人或场景群,并呈现预警具体信息. 考勤机数据推送:员工通过考勤机完成一键考勤,考勤成功后通过机器人 ...

  4. 【钉钉-场景化能力包】群机器人数据报表推送

    客户痛点 现代企业中的员工人数随着时代发展逐渐增多,让原本的管理者管理员工也越发困难,特别是特殊行业,在设备消息推送.监控报警.信息公示等多种场景中,因为任务的复杂性比较高,需要多人一起协同时,更是让 ...

  5. 使用钉钉创建企业内部机器人

    目录 玩转钉钉机器人 创建钉钉机器人: 如何创建钉钉机器人 如何调用机器人api 支持的消息格式 text类型 markdown类型 整体跳转actionCard类型 feedCard类型 使用钉钉机 ...

  6. 钉钉企业内部机器人python开发(公网部署版本)

    钉钉企业内部机器人开发(公网部署版本) 钉钉给出的开发文档地址如下: https://open.dingtalk.com/document/group/enterprise-created-chatb ...

  7. python 钉钉消息推送_python3实现钉钉消息推送的方法示例

    背景 偶然发现一个python实现的按照农历/阴历推送消息提醒的程序,钉钉群消息推送.此处总结并对其可推送的消息做. DingtalkNotice 环境:python3.7 安装: pip insta ...

  8. 如何造一个“钉钉”?谈谈消息系统架构的实现

    阿里妹导读:消息类场景是表格存储(Tablestore)主推的方向之一,因其数据存储结构在消息类数据存储上具有天然优势.为了方便用户基于Tablestore为消息类场景建模,Tablestore封装T ...

  9. 集成 Jira 与钉钉和企业微信通知

    如果觉得这篇文章不错,请记得点赞.收藏和分享,也欢迎加我个人微信交流:craigyoung1985,请备注:姓名/城市/工作岗位. 每每看到别人家公司将聊天机器人与项目管理有效结合的实践,我都甚是羡慕 ...

最新文章

  1. python count()计算字符出现的频数
  2. mfc 静态框接收tab焦点_目标检测中焦点损失的入门指南
  3. 不能用 + 拼接字符串? 这次我要吊打面试官!
  4. Rational Rose正逆向工程(类图转Java代码,Java代码转类图)
  5. SAP云平台CloudFoundry上部署了一个应用的技术明细
  6. Ubuntu20.04配置ip地址
  7. oracle连接数一直超出,Oracle超出最大連接數問題及解決(…
  8. python args_Python可变参数*args和**kwargs用法实例小结
  9. 获取Android版本信息和电话信息
  10. django入门项目图书管理
  11. CentOS7.6下安装Ambari
  12. apache、nginx验证
  13. Wings-让单元测试智能全自动生成
  14. 使用bat脚本添加JAVA_HOME和修改PATH
  15. 网贷大数据什么时候会好_为什么大数据待遇那么好 如何入门大数据行业
  16. 五大服务顺序_百度大脑5.0技术干货:详解飞桨五大优势,鸿鹄芯片架构细节
  17. ENVI学习总结(十五)——遥感影像分类与应用
  18. js添加option设置空值_3.11 在散点图中添加标签(2)
  19. 如何将视频的每一帧提取成图片
  20. layui 表格数据源_学习数据分析,这些超好用的数据分析工具你必须知道!|数据挖掘|数据源|oracle...

热门文章

  1. html 翻页效果,JavaScript实现翻页功能(附效果图)
  2. 必备软件——视频播放
  3. VueJS实现模糊查询
  4. 寻找Hadoop启动Namenode失败原因
  5. 直方图匹配(规定化)
  6. WebAPI中className与classList的区别
  7. 如何修改驱动使得NVIDIA Geforce GTX 970, 980, 980 TI and Titan X等显卡可以在Win XP/Win 2003 server x64下驱动?...
  8. Tigo获得Flex MLPE平台专利
  9. linux新建用户命令行无颜色问题
  10. (文献阅读笔记)基于雾计算提出的一种交通路况监测的一种隐私保护协议