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

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



仓库

前言

互动卡片支持自定义按钮, 并在按钮上绑定回调事件, 点击按钮时将通过提前注册的回调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();

    @Override
    default 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
     */
    @Override
    default 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和接口调用
     */
    @Bean
    public 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 you
        return new TestBot("custom bot name", 16757015L, "your bot key", "your bot secret");
    }

    @Bean
    public IDingMiniH5 dummyMiniH5App() {
        return new DummyMiniH5App("custom app name", 16757015L, "your app key", "your app secret");
    }

    /**
     * 如果要做环境隔离,使用此方式配置
     * 否则硬编码+component或者通过configurationProperties自动配置也行
     */
    @Bean
    public 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;

    @Override
    public 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 还没仔细验证过群聊场景,验证后再逐渐增加业务常用参数,近期业务用不到群聊场景,暂时不更新
     */
    @Deprecated
    String 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具体实现

  @Override
    public 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
    @Override
    public 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;
    }
}

三、其他

这里调用的效果就不展示了, 如果感兴趣, 可以尝试配置钉钉开放后台的相关资源后, 可以使用demo项目调用测试下.

1.1 常见问题

  • 2022-07-26
    在这里插入图片描述
    有人问多个按钮回调时如何区分, 这里配置时并没有对按钮区分, 都是同一个回调接口,但是参数可以自定义, 参数一旦自定义,那么如何区分问题也就一目了然了

1.2 尝试

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

四、总结

官方文档东一块西一块的比较容易看乱了, 本文的实现集中于机器人单聊互动卡片的操作, 流程单一, 清晰, 按照本文提供的实现完成发送业务会相对快一些.

钉钉机器人是阿里巴巴钉钉平台上的一种自动化工具,它可以帮助开发者实现钉钉用户之间消息的发送和接收。通过钉钉机器人,可以在不打开钉钉应用的情况下,直接通过编程方式发送消息钉钉群或者个人()。 要在Python中实现钉钉机器人交互,主要包括以下几个步骤: 1. 创建机器人:在钉钉群中添加机器人,获取其Webhook URL,这是一个特殊的URL,用于发送消息到该机器人。 2. 发送消息:使用Python的requests库,通过HTTP POST请求将消息发送到Webhook URL。消息内容需要按照钉钉官方提供的消息格式进行组织。 3. 处理响应:根据发送请求后得到的响应进行相应的处理,比如检查是否发送成功。 下面是一个简的Python示例代码,展示了如何使用钉钉机器人发送消息到个人(): ```python import requests import json # 钉钉机器人的Webhook URL webhook_url = 'https://oapi.dingtalk.com/robot/send?access_token=你的access_token' # 构建消息内容 message = { "msgtype": "text", # 消息类型为文本 "text": { "content": "你好,这是通过Python发送的消息!" # 消息内容 }, "at": { "isAtAll": False # 不@所有人 } } # 发送消息 headers = {'Content-Type': 'application/json'} response = requests.post(webhook_url, headers=headers, data=json.dumps(message)) # 打印响应内容 print(response.text) ``` 注意:上述代码中的`access_token`需要替换为你自己创建的钉钉机器人的实际Token。 在实际使用时,还需要处理网络请求的异常情况以及验证发送消息的安全性。
评论 26
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值