钉钉互动卡片设计方案
1 概述
1.1 互动卡片是什么
钉钉互动卡片是一种 数据驱动、即时交互、多人协同 的轻量卡片,它能够将原本复杂的应用解构成一个个轻量级的卡片在钉钉的 各个场域 上运行。用户可以在卡片上完成互动协同,提高用户的沟通效率,同时帮助业务更好地触达用户。
1.2 互动卡片的特性
1.2.1 数据驱动
钉钉的互动卡片由模板和数据组成,数据决定了卡片的内容。在正常情况下,开发者只需要搭建一次卡片模板,后续只需要通过数据来驱动不同的卡片样式和内容即可。
1.2.2 即时交互、多人协同
传统的消息都是静态的,更多的是作为内容的载体。而互动卡片除了可以展示丰富的内容之外,还支持让用户在卡片上进行轻量级的交互,交互的结果实时在卡片上同步显示。这意味着卡片不仅作为内容的载体,还是一个可操作可互动的轻量级应用,用户从此不再需要打开额外的页面去完成操作,直接在钉钉聊天窗口即可完成所有操作。

同时互动卡片还具备多人协同的能力,同一个卡片状态变更会实时同步。
1.2.3 多场域运行
钉钉互动卡片最常见于聊天窗口中,通常以卡片消息的方式与大家见面。但其实互动卡片不仅仅可以在聊天中运行,它还可以运行在其他场景。 如下图,消息互动卡片和群吊顶互动卡片均是由同一个互动卡片模板所创建的。吊顶上的互动卡片拥有消息互动卡片消息的所有能力,但吊顶上的互动卡片尺寸是固定的。

下图是互动卡片实现的整体流程

2 卡片模板搭建
互动卡片是由卡片模板和卡片数据构成的,卡片模板决定了卡片的结构,卡片数据则决定了卡片展示的具体内容,现在创建卡片实例的方法分为 卡片平台创建卡片实例 和 开放平台创建卡片实例。 使用互动卡片之前需要先准备一个卡片模板,具体需要经过 创建卡片模板、搭建卡片模板、卡片模板发布 。 实际上卡片模板分为 普通卡片模板 和 Al卡片模板 ,现在使用的主要是普通卡片模板,Al卡片模板这里不展开论述,如有兴趣可以点击Al卡片模板学习。
2.1 创建卡片模板
2.进入新建模板页面,并填写模板名称、卡片类型

3.关联钉钉应用列表中的应用(可选)
说明:关联应用后在应用详情页 > 酷应用 > 扩展到群会话 > 功能设计中对应类型的卡片模板列表可见,如图

4.点击「创建」按钮即可创建模板,创建模板后即可查看该卡片模板的模板 ID说明:后续发送卡片消息流程中接口将会使用模板ID

2.2 搭建卡片模板
1.进入模板列表页面,即可看到创建的卡片模板。单击卡片模板上的「编辑」按钮,即可进入卡片模板搭建器进行编辑

2.钉钉卡片模板搭建器是一个在线可视化搭建卡片的平台,开发者通过简单的拖拽、配置即可完成卡片模板的搭建。同时,搭建器提供了丰富的组件和预置卡片模板,帮助开发者更加方便、快速地接入互动卡片

如上图,卡片模板搭建器的界面共分为以下 5 个部分:
1.功能菜单
2.功能面板
3.模拟器:在模拟器中你可以在配置模拟的客户端环境、暗黑模式、版本号以及语言,同时也可以对组件进行编辑和布局的调整;结合模拟数据和预览模式,可以让你在模拟器提前感知卡片的实际渲染效果
4.组件属性设置面板:在属性设置面板中,开发者可以通过设置组件的属性,来调整组件的样式及内容
5.操作栏
2.3 卡片模板发布
当卡片模板搭建完成时,即可点击「发布」按钮对模板进行发布操作,发布完成后即可进行后续卡片实例的创建以及投放等流程,真正将卡片发送出来

3 卡片实例创建
互动卡片是由卡片模板和卡片数据构成的,创建卡片实例是将卡片模版和卡片数据关联起来进行实例化的过程,完成创建后即可针对卡片实例进行更多的操作(如:投放)。
3.1 卡片平台投放卡片实例
卡片示例的创建总共分以下三个步骤:进入卡片实例管理页面、创建卡片实例、查看卡片实例列表 进入卡片实例管理页面 1.登录开发者后台 > 卡片平台 2.选择卡片,单击「查看」进入卡片模版搭建页面

3.单击右上角的「卡片实例管理」,进入卡片实例管理页面

创建卡片实例进入卡片实例管理页面,单击页面左上角的「创建卡片实例」按钮,进入表单填写界面

在此步骤中,你需要进行以下操作:1.完成数据配置:为卡片的变量配置数据来源2.填写场域信息:配置卡片所要投放的场域完成数据配置卡片的数据分为静态和动态两种类型的数据。这两种类型的数据主要有数据的获取时机和更新机制两方面的不同

1.绑定静态数据对于卡片的每一个变量都可以绑定静态数据。 以变量 content 为例,选择数据类型为静态数据即可在数据一览中输入变量的实际内容。

更新完静态数据后,右侧的模拟器会根据绑定的数据实时地展示预览效果,可以参考预览效果来检查和调整卡片的数据。

2.绑定动态数据在绑定动态数据之前需要先创建动态数据源, 目前卡片平台支持以下两种动态数据源:
数据资产平台数据源
开放平台连接器数据源

以数据资产平台数据源为例,选中后会出现如下图所示的数据源配置表单。

表单中各字段的含义如下表所示:

完成表单项的填写并点击「新增」即可创建一个动态数据源配置项。 如下图所示,新增的动态数据源配置,可以从上面了解到数据源的名称、ID 以及数据类型等基本信息。

接着可以进行数据源的绑定。 因为我们创建的是 chart 类型的数据源, 所以我们需要选择一个图表类型的变量来绑定这个动态数据源。如下图所示,找到图表变量 chart ,为其配置数据类型为动态数据源,在数据项选择创建的折线测试服务。

绑定完成后即可在右侧看到如下图所示的预览效果。

填写场域信息 除了卡片本身的数据之外, 还需要填写场域所需要的数据才能让卡片在对应场域中流通。 目前卡片支持 IM 、工作台等场域。 特定类型的卡片只能在对应场域中使用,而标准卡片可以在所有场域中使用。 本文示例使用的卡片是消息卡片,所以我们需要创建 IM 场域(群聊/单聊)的配置。单击「场域配置」一栏中的「新增场域」按钮选择场域并填写场域配置信息:


下表所示为 IM 场域投放的参数说明

填写完 IM 场域的配置信息后就可点击「创建实例」按钮完成一个卡片实例的创建 可以在实例列表一栏中查看历史创建的卡片实例列表。通过实例列表你可以了解到卡片的 bizId、创建时间、修改时间和场域信息,并可以点击右侧的投放按钮完成卡片的投放

3.2 开放接口创建卡片实例
开放接口创建卡片实例的流程分为以下两步:创建卡片,设置卡片的高级属性
3.2.1 创建卡片
一张基本的卡片可以包含这几个基本信息:
userId: 创建卡片的用户的 ID,非必填,不超过100个字符
userIdType:卡片使用的用户 ID 的类型,1 为 userId 模式,2 为 unionId 模式,默认为 1
cardTemplateId:卡片模板的 ID,在卡片模板搭建及发布后获取
outTrackId:卡片 ID。这是开发者自定义的,后续对卡片的投放和互动操作,均是通过 outTrackId 来完成,不超过100个字符
cardData:卡片的公共数据
privateData:卡片的私有数据 调用服务端API-创建卡片接口实现卡片实例的创建
package com.aliyun.sample;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import com.aliyun.dingtalkcard_1_0.Client;
import com.aliyun.dingtalkcard_1_0.models.CreateCardHeaders;
import com.aliyun.dingtalkcard_1_0.models.CreateCardRequest;
import com.aliyun.dingtalkcard_1_0.models.PrivateDataValue;
import com.aliyun.tea.TeaConverter;
import com.aliyun.tea.TeaException;
import com.aliyun.tea.TeaPair;
import com.aliyun.teaopenapi.models.Config;
import com.aliyun.teautil.Common;
import com.aliyun.teautil.models.RuntimeOptions;
public class Sample {
/**
* 使用 Token 初始化账号Client
* @return Client
* @throws Exception
*/
public static Client createClient() throws Exception {
Config config = new Config();
config.protocol = "https";
config.regionId = "central";
return new Client(config);
}
public static void main(String[] args_) throws Exception {
List<String> args = Arrays.asList(args_);
Client client = Sample.createClient();
CreateCardHeaders createCardHeaders
= new CreateCardHeaders();
createCardHeaders.xAcsDingtalkAccessToken = "<your access token>";
PrivateDataValue privateDataValueKey
= new PrivateDataValue();
Map<String, PrivateDataValue> privateData = TeaConverter.buildMap(
new TeaPair("privateDataValueKey", privateDataValueKey)
);
CreateCardRequest.CreateCardRequestCardData cardData
= new CreateCardRequest.CreateCardRequestCardData();
CreateCardRequest createCardRequest
= new CreateCardRequest()
.setUserId("example--user-id")
.setUserIdType(1)
.setOutTrackId("example-out-track-id")
.setCardTemplateId("example-template-id")
.setCardData(cardData)
.setPrivateData(privateData);
try {
client.createCardWithOptions(createCardRequest, createCardHeaders,
new RuntimeOptions());
} catch (TeaException err) {
if (!com.aliyun.teautil.Common.empty(err.code) && !Common.empty(err.message)) {
// err 中含有 code 和 message 属性,可帮助开发定位问题
}
} catch (Exception _err) {
TeaException err = new TeaException(_err.getMessage(), _err);
if (!Common.empty(err.code) && !Common.empty(err.message)) {
// err 中含有 code 和 message 属性,可帮助开发定位问题
}
}
}
}
3.2.2 设置卡片的高级属性
如果想使用卡片的高级功能,比如多场域或者动态数据源,要在上述创建卡片步骤的基础上,设置卡片的多场域属性或者动态数据源属性。 卡片在投放到某个场域之前,需要在卡片上配置该场域的属性。卡片的场域属性可以在创建的时候配置,也可以在创建后补加。下面介绍在创建卡片的时候配置场域属性,如何在创建好的卡片上添加场域属性以及将卡片投放到场域,详情参见开放接口投放卡片实例。
package com.aliyun.sample;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import com.aliyun.dingtalkcard_1_0.Client;
import com.aliyun.dingtalkcard_1_0.models.CreateCardHeaders;
import com.aliyun.dingtalkcard_1_0.models.CreateCardRequest;
import com.aliyun.dingtalkcard_1_0.models.PrivateDataValue;
import com.aliyun.tea.TeaConverter;
import com.aliyun.tea.TeaException;
import com.aliyun.tea.TeaPair;
import com.aliyun.teaopenapi.models.Config;
import com.aliyun.teautil.Common;
import com.aliyun.teautil.models.RuntimeOptions;
public class Sample {
/**
* 使用 Token 初始化账号Client
* @return Client
* @throws Exception
*/
public static Client createClient() throws Exception {
Config config = new Config();
config.protocol = "https";
config.regionId = "central";
return new Client(config);
}
public static void main(String[] args_) throws Exception {
List<String> args = Arrays.asList(args_);
Client client = Sample.createClient();
CreateCardHeaders createCardHeaders
= new CreateCardHeaders();
createCardHeaders.xAcsDingtalkAccessToken = "<your access token>";
PrivateDataValue privateDataValueKey
= new PrivateDataValue();
Map<String, PrivateDataValue> privateData = TeaConverter.buildMap(
new TeaPair("privateDataValueKey", privateDataValueKey)
);
CreateCardRequest.CreateCardRequestCardData cardData
= new CreateCardRequest.CreateCardRequestCardData();
// 吊顶场域属性
CreateCardRequest.CreateCardRequestTopOpenSpaceModel topOpenSpaceModel = new CreateCardRequest.CreateCardRequestTopOpenSpaceModel()
.setSpaceType("ONE_BOX");
// 人与人单聊场域属性
// 通知属性
CreateCardRequest.CreateCardRequestImSingleOpenSpaceModelNotification imSingleOpenSpaceModelNotification = new CreateCardRequest.CreateCardRequestImSingleOpenSpaceModelNotification()
.setAlertContent("你收到了一个卡片消息")
.setNotificationOff(false);
// 搜索属性
CreateCardRequest.CreateCardRequestImSingleOpenSpaceModelSearchSupport imSingleOpenSpaceModelSearchSupport = new CreateCardRequest.CreateCardRequestImSingleOpenSpaceModelSearchSupport()
.setSearchIcon("@lALPDgQ9q8hFhlHNAXzNAqI")
.setSearchTypeName("{\"zh_CN\":\"示例\",\"zh_TW\":\"示例\",\"en_US\":\"Example\"}")
.setSearchDesc("卡片的具体描述");
// lastMessage属性
Map<String, String> imSingleOpenSpaceModelLastMessageI18n = TeaConverter.buildMap(
new TeaPair("ZH_CN", "卡片"),
new TeaPair("EN_US", "card"}")
);
CreateCardRequest.CreateCardRequestImSingleOpenSpaceModel imSingleOpenSpaceModel = new CreateCardRequest.CreateCardRequestImSingleOpenSpaceModel()
.setSupportForward(false)
.setLastMessageI18n(imSingleOpenSpaceModelLastMessageI18n)
.setSearchSupport(imSingleOpenSpaceModelSearchSupport)
.setNotification(imSingleOpenSpaceModelNotification);
// 群聊场域属性
// 通知属性
CreateCardRequest.CreateCardRequestImGroupOpenSpaceModelNotification imGroupOpenSpaceModelNotification = new CreateCardRequest.CreateCardRequestImGroupOpenSpaceModelNotification()
.setAlertContent("你收到了一个卡片消息")
.setNotificationOff(false);
// 搜索属性
CreateCardRequest.CreateCardRequestImGroupOpenSpaceModelSearchSupport imGroupOpenSpaceModelSearchSupport = new CreateCardRequest.CreateCardRequestImGroupOpenSpaceModelSearchSupport()
.setSearchIcon("@lALPDgQ9q8hFhlHNAXzNAqI")
.setSearchTypeName("{\"zh_CN\":\"示例\",\"zh_TW\":\"示例\",\"en_US\":\"Example\"}")
.setSearchDesc("卡片的具体描述");
// lastMessage属性
Map<String, String> imGroupOpenSpaceModelLastMessageI18n = TeaConverter.buildMap(
new TeaPair("ZH_CN", "卡片"),
new TeaPair("EN_US", "card"}")
);
CreateCardRequest.CreateCardRequestImGroupOpenSpaceModel imGroupOpenSpaceModel = new CreateCardRequest.CreateCardRequestImGroupOpenSpaceModel()
.setSupportForward(false)
.setLastMessageI18n(imGroupOpenSpaceModelLastMessageI18n)
.setSearchSupport(imGroupOpenSpaceModelSearchSupport)
.setNotification(imGroupOpenSpaceModelNotification);
// 人与机器人单聊场域属性
// 通知属性
CreateCardRequest.CreateCardRequestImRobotOpenSpaceModelNotification imRobotOpenSpaceModelNotification = new CreateCardRequest.CreateCardRequestImRobotOpenSpaceModelNotification()
.setAlertContent("你收到了一个卡片消息")
.setNotificationOff(false);
// 搜索属性
CreateCardRequest.CreateCardRequestImRobotOpenSpaceModelSearchSupport imRobotOpenSpaceModelSearchSupport = new CreateCardRequest.CreateCardRequestImRobotOpenSpaceModelSearchSupport()
.setSearchIcon("@lALPDgQ9q8hFhlHNAXzNAqI")
.setSearchTypeName("{\"zh_CN\":\"示例\",\"zh_TW\":\"示例\",\"en_US\":\"Example\"}")
.setSearchDesc("卡片的具体描述");
// lastMessage属性
Map<String, String> imRobotOpenSpaceModelLastMessageI18n = TeaConverter.buildMap(
new TeaPair("ZH_CN", "卡片"),
new TeaPair("EN_US", "card"}")
);
CreateCardRequest.CreateCardRequestImRobotOpenSpaceModel imRobotOpenSpaceModel = new CreateCardRequest.CreateCardRequestImRobotOpenSpaceModel()
.setSupportForward(false)
.setLastMessageI18n(imRobotOpenSpaceModelLastMessageI18n)
.setSearchSupport(imRobotOpenSpaceModelSearchSupport)
.setNotification(imRobotOpenSpaceModelNotification);
CreateCardRequest createCardRequest
= new CreateCardRequest()
.setUserId("example-user-id")
.setUserIdType(1)
.setOutTrackId("example-out-track-id")
.setCardTemplateId("example-template-id")
.setCardData(cardData)
.setPrivateData(privateData)
.setImSingleOpenSpaceModel(imSingleOpenSpaceModel)
.setImGroupOpenSpaceModel(imGroupOpenSpaceModel)
.setImRobotOpenSpaceModel(imRobotOpenSpaceModel)
.setTopOpenSpaceModel(topOpenSpaceModel);
try {
client.createCardWithOptions(createCardRequest, createCardHeaders,
new RuntimeOptions());
} catch (TeaException err) {
if (!com.aliyun.teautil.Common.empty(err.code) && !Common.empty(err.message)) {
// err 中含有 code 和 message 属性,可帮助开发定位问题
}
} catch (Exception _err) {
TeaException err = new TeaException(_err.getMessage(), _err);
if (!Common.empty(err.code) && !Common.empty(err.message)) {
// err 中含有 code 和 message 属性,可帮助开发定位问题
}
}
}
}在创建卡片的时候,可以配置卡片的动态数据源属性。下例创建了一个简单的具有动态数据源属性的卡片,动态数据源相关配置的参数为openDynamicDataConfig,详情参见动态数据源
package com.aliyun.sample;
import com.aliyun.tea.*;
public class Sample {
/**
* 使用 Token 初始化账号Client
* @return Client
* @throws Exception
*/
public static com.aliyun.dingtalkcard_1_0.Client createClient() throws Exception {
com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config();
config.protocol = "https";
config.regionId = "central";
return new com.aliyun.dingtalkcard_1_0.Client(config);
}
public static void main(String[] args_) throws Exception {
java.util.List<String> args = java.util.Arrays.asList(args_);
com.aliyun.dingtalkcard_1_0.Client client = GetTokenTest.createClient();
com.aliyun.dingtalkcard_1_0.models.CreateCardHeaders createCardHeaders
= new com.aliyun.dingtalkcard_1_0.models.CreateCardHeaders();
createCardHeaders.xAcsDingtalkAccessToken = "<your access token>";
com.aliyun.dingtalkcard_1_0.models.PrivateDataValue privateDataValueKey
= new com.aliyun.dingtalkcard_1_0.models.PrivateDataValue();
java.util.Map<String, com.aliyun.dingtalkcard_1_0.models.PrivateDataValue> privateData = TeaConverter.buildMap(
new TeaPair("privateDataValueKey", privateDataValueKey)
);
com.aliyun.dingtalkcard_1_0.models.CreateCardRequest.CreateCardRequestCardData cardData
= new com.aliyun.dingtalkcard_1_0.models.CreateCardRequest.CreateCardRequestCardData();
com.aliyun.dingtalkcard_1_0.models.CreateCardRequest createCardRequest
= new com.aliyun.dingtalkcard_1_0.models.CreateCardRequest()
.setUserId("example-user-id")
.setUserIdType(1)
.setOutTrackId("example-out-track-id")
.setCardTemplateId("example-template-id")
.setCardData(cardData)
.setPrivateData(privateData);
try {
client.createCardWithOptions(createCardRequest, createCardHeaders,
new com.aliyun.teautil.models.RuntimeOptions());
} catch (TeaException err) {
if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
// err 中含有 code 和 message 属性,可帮助开发定位问题
}
} catch (Exception _err) {
TeaException err = new TeaException(_err.getMessage(), _err);
if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
// err 中含有 code 和 message 属性,可帮助开发定位问题
}
}
}
}4 卡片投放
卡片投放赋予了卡片在各场域中流通的能力,可以将你创建的卡片和钉钉结合起来。
4.1 卡片平台投放卡片实例
卡片平台投放卡片实例的步骤分为:进入卡片实例管理页面,选择卡片实例,投放到特定场域
4.1.1 进入卡片实例管理页面
1、登录开发者后台 > 卡片平台。
2、选择一张已发布的卡片, 点击「查看」进入卡片模版搭建页面。

3、点击右上角「卡片实例管理」按钮,即可进入卡片实例管理页面。

4.1.2 选择卡片实例
进入卡片实例管理页面后,在「卡片实例」栏目中可以查看历史创建的卡片实例列表。选择你需要投放的卡片实例并点击如图的「投放」按钮及可进入「卡片投放」界面。

4.1.3 投放到特定场域
在卡片投放界面中点击「新增投放配置」,并选择需要投放的场域,完成场域配置表单的填写即可将卡片投放到对应的场域中。

以 IM 场域为例。对于 IM 场域卡片平台默认会将卡片实例投放到「钉钉卡片助手」会话中。以下图所示卡片实例为例:

只需在「卡片投放」界面中新增投放配置并选择 IM 场域。卡片平台将自动完成参数的配置,无需额外填写其他参数,之后点击投放按钮即可完成在 IM 场域的投放

下图所示为投放的效果:

4.2 开放接口投放卡片实例
使用卡片投放接口,可以通过一次调用,将同一个卡片实例,进行跨场域投放,目前已支持的场域包括群聊、机器人单聊、人与人单聊、吊顶。
4.2.1 一次投放的主要属性
对于一次投放,主要包括4个元素:卡片实例id(outTrackId)、统一投放id(openSpaceId)、卡片场域属性(openSpaceModel)、卡片投放属性(openDeliverModel),本文主要介绍这4个要素的含义及使用。
卡片实例 id(outTrackId):在创建卡片时,由开发者生成并作为入参,即为outTrackId,用于唯一标识一张卡片。在投放卡片时,使用卡片实例id唯一标识一张卡片,不超过100个字符。
统一投放id(openSpaceId):在投放接口中,使用 openSpaceId 作为统一投放id,一个 openSpaceId 包含多个开放场域 id,并且可以在多次投放中复用。openSpaceId采用固定协议且支持版本升级,主要由版本、场域code、场域id三部分内容组成,其具体协议内容可参见服务端API-投放卡片接口,总长度不超过1000个字符。 目前支持的场域以及场域id的含义如下:

卡片场域属性(openSpaceModel):对于同一个卡片实例,在同一个场域下,有一些相同的属性(如IM群聊场域下的通知属性),在投放接口中将这些属性统一定义为卡片的场域属性,一个卡片实例在一个场域下拥有唯一的场域属性。同时,卡片实例只有设置了某个场域属性,才能被投放至该场域。为卡片设置场域属性有两种方式:1、在创建卡片时设置场域属性;2、有些卡片在创建时没有设置某些场域的属性,开放接口为此提供了为卡片设置场域属性的接口,调用此接口即可为已经创建的卡片设置某个场域的属性,使其可被投放至该场域中卡片投放属性(openDeliverModel):相对于卡片场域属性,在将卡片投放至某个场域时,有一些属性在每次投放中不相同(如IM群聊场域下的@人信息),在投放接口中将这些属性定义为卡片投放属性。在每次投放时,如果想要将卡片投放至某个场域,需要传入对应场域的投放属性。
4.2.2 投放流程
在了解了上述概念后,本文将以IM群聊、吊顶2个场域的多场域投放为例,介绍投放卡片的流程及示例。其主要流程如下图:

1、调用服务端API-创建卡片接口,实现创建卡片实例并获取outTrackId。2、若卡片未增加场域属性,则需要调用服务端API-新增或者更新卡片的场域信息接口为卡片配置场域属性。
package com.aliyun.sample;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import com.aliyun.dingtalkcard_1_0.Client;
import com.aliyun.dingtalkcard_1_0.models.*;
import com.aliyun.dingtalkcard_1_0.models.AppendSpaceRequest.AppendSpaceRequestCoFeedOpenSpaceModel;
import com.aliyun.dingtalkcard_1_0.models.AppendSpaceRequest.AppendSpaceRequestTopOpenSpaceModel;
import com.aliyun.dingtalkcard_1_0.models.AppendSpaceRequest.AppendSpaceRequestImGroupOpenSpaceModelNotification;
import com.aliyun.teautil.Common.empty;
import com.aliyun.teautil.models.RuntimeOptions;
import com.aliyun.tea.*;
public void appendSpacExample throws Exception {
List<String> args = Arrays.asList(args_);
Client client = Sample.createClient();
AppendSpaceHeaders appendSpaceHeaders = new AppendSpaceHeaders();
appendSpaceHeaders.xAcsDingtalkAccessToken = "<your access token>";
// 吊顶场域属性
AppendSpaceRequestTopOpenSpaceModel topOpenSpaceModel = new AppendSpaceRequestTopOpenSpaceModel()
.setSpaceType("ONE_BOX");
// 群聊场域属性
// 通知属性
AppendSpaceRequestImGroupOpenSpaceModelNotification imGroupOpenSpaceModelNotification = new AppendSpaceRequestImGroupOpenSpaceModelNotification()
.setAlertContent("你收到了一个卡片消息")
.setNotificationOff(false);
// 搜索属性
AppendSpaceRequestImGroupOpenSpaceModelSearchSupport imGroupOpenSpaceModelSearchSupport = new AppendSpaceRequestImGroupOpenSpaceModelSearchSupport()
.setSearchIcon("@lALPDgQ9q8hFhlHNAXzNAqI")
.setSearchTypeName("{\"zh_CN\":\"示例\",\"zh_TW\":\"示例\",\"en_US\":\"Example\"}")
.setSearchDesc("搜索描述示例");
// lastMessage属性
Map<String, String> imGroupOpenSpaceModelLastMessageI18n = TeaConverter.buildMap(
new TeaPair("ZH_CN", "卡片"),
new TeaPair("EN_US", "card"}")
);
AppendSpaceRequestImGroupOpenSpaceModel imGroupOpenSpaceModel = new AppendSpaceRequestImGroupOpenSpaceModel()
.setSupportForward(false)
.setLastMessageI18n(imGroupOpenSpaceModelLastMessageI18n)
.setSearchSupport(imGroupOpenSpaceModelSearchSupport)
.setNotification(imGroupOpenSpaceModelNotification);
String outTrackId = "example_out_track_id";
AppendSpaceRequest appendSpaceRequest = new AppendSpaceRequest()
.setOutTrackId("example_out_track_id")
.setImGroupOpenSpaceModel(imGroupOpenSpaceModel)
.setTopOpenSpaceModel(topOpenSpaceModel);
try {
client.appendSpaceWithOptions(appendSpaceRequest, appendSpaceHeaders, new RuntimeOptions());
} catch (TeaException err) {
if (!empty(err.code) && !empty(err.message)) {
// err 中含有 code 和 message 属性,可帮助开发定位问题
}
} catch (Exception _err) {
TeaException err = new TeaException(_err.getMessage(), _err);
if (!empty(err.code) && !empty(err.message)) {
// err 中含有 code 和 message 属性,可帮助开发定位问题
}
}
}3、获取要被投放的场域id,构造openSpaceId。 openSpaceId由三部分组成:协议版本、场域类型、场域id,当前版本的格式为: dtv1.card://spaceType1.spaceId1;spaceType2.spaceId2 场域类型及场域id见开放场域 id 章节,格式为: dtv1.card//im_group.example_open_conversation_id;one_box.example_open_conversation_id; 具体openSpaceId示 dtv1.card//im_group.cidp4Gh*******VCQ==;one_box.cidp4Gh*******VCQ==;
4、构造目标场域的投放模型,并统一调用服务端API-投放卡片接口,实现卡片投放。
package com.aliyun.sample;
import java.util.ArrayList;
import java.util.List;
import com.aliyun.dingtalkcard_1_0.models.*;
import com.aliyun.dingtalkcard_1_0.models.DeliverCardRequest.DeliverCardRequestImGroupOpenDeliverModel;
import com.aliyun.dingtalkcard_1_0.models.DeliverCardRequest.DeliverCardRequestCoFeedOpenDeliverModel;
import com.aliyun.dingtalkcard_1_0.models.DeliverCardRequest.DeliverCardRequestTopOpenDeliverModel;
import com.aliyun.teautil.Common.empty;
import com.aliyun.teautil.models.RuntimeOptions;
import com.aliyun.tea.*;
public void buildOpenDeliverModelAndDeliverCard() {
// 群聊投放属性
DeliverCardRequestImGroupOpenDeliverModel imGroupOpenDeliverModel = new DeliverCardRequestImGroupOpenDeliverModel()
.setRobotCode("example_robot_code") // 机器人code
.setRecipients(Arrays.asList("example_user_id_1", "example_user_id_2")) // 消息接收者
.setSupportForward(true); // 是否支持转发
// 吊顶投放属性
DeliverCardRequestTopOpenDeliverModel topOpenDeliverModel = new DeliverCardRequestTopOpenDeliverModel()
.setExpiredTimeMills(1665473229000L) // 吊顶过期时间
.setUserIds(Arrays.asList("example_user_id_3", "example_user_id_4")) // 可以看到吊顶的用户
.setPlatforms(Arrays.asList("android", "ios", "win", "mac")); // 可以看到吊顶的设备
String outTrackId = "example_out_track_id";
String openSpaceId = "dtv1.card//im_group.example_open_conversation_id;one_box.example_open_conversation_id";
// 构造投放Request
DeliverCardRequest deliverCardRequest = new DeliverCardRequest()
.setOutTrackId(outTrackId)
.setUserIdType(1)
.setOpenSpaceId(openSpaceId)
.setImGroupOpenDeliverModel(imGroupOpenDeliverModel)
.setTopOpenDeliverModel(topOpenDeliverModel);
try {
client.deliverCardWithOptions(deliverCardRequest, deliverCardHeaders, new RuntimeOptions());
} catch (TeaException err) {
if (!empty(err.code) && !empty(err.message)) {
// err 中含有 code 和 message 属性,可帮助开发定位问题
}
} catch (Exception _err) {
TeaException err = new TeaException(_err.getMessage(), _err);
if (!empty(err.code) && !empty(err.message)) {
// err 中含有 code 和 message 属性,可帮助开发定位问题
}
}
}投放效果展示 如图展示了上述示例中,将同一张卡片投放到IM群聊、吊顶中的效果。

5 卡片互动
互动卡片并不是一个静态的资源,允许用户与卡片进行交互,比如在日程卡片上点击“接受”,即可发送事件回调请求到开发者服务端进行业务逻辑处理。 在可交互的组件上设置点击事件类型为“回传请求”即可完成设置,同时您也可以配置回传到服务端的参数

业务流程

提交审批的视觉效果:

审批完成的视觉效果:

更新的两种形式:更新公有数据,更新私有数据当进行公有数据变更的时候,我们进行的变更会推送给所有该卡片的接收者。所有接收到该卡片的用户,在看到该卡片的时候,相应字段都会发生变更。当某个数据只针对部分用户进行更新的时候,我们可以进行私有数据变更,该变更会推送给对应变更数据的用户。当对应用户看到该卡片的时候,相应字段会发生变更。结合需求分析可知,本次审批单审批结果的变更属于私有数据低频变更的场景,且需要变更立即生效。
5.1 前置说明
首先我们搭建了一个模板如下及模板对应的变量如下所示:


其中审批处理按钮只对审批人显示,审批过程中展示正常的审批按钮,审批结束后,展示禁用的审批按钮。当我们创建卡片的时候整个数据如下:
{
"cardData": {
"cardParamMap": {
"title": "**的差旅报销",
"type": "差旅费",
"reason": "出差费用",
"amount": "100",
"status": "未审批"
}
},
"privateData": {
"userId1": {
"cardParamMap": {
"isApprover": "0"
}
},
"userId2": {
"cardParamMap": {
"isApprover": "1",
"isFinished": "0"
}
}
}
}卡片如下图所示

接下来点击按钮需要触发事件回调,目前钉钉提供了如下 2 回调接入的模式:基于 HTTP 服务的事件回调和基于 Stream 模式的事件回调,其中基于 HTTP 服务的事件回调是本篇的主要部分,本文以一个互动卡片的更新的需求为例子。
5.2 基于HTTP服务的事件回调
HTTP 模式,需要开发者提供一个公网可访问的域名,钉钉会通过 http 请求将回调信息发送到开发者应用程序,需要完成的准备工作: 1. 在卡片平台上完成卡片模板搭建及发布,该步骤已经前置完成。 2. 调用服务端 API-注册卡片回调地址接口完成回调地址的注册。 3. 并配置交互组件 4. 实现完成开放接口创建卡片实例,并在创建卡片时指定参数 callbackType 为 HTTP 和注册的 callbackRouteKey 绑定回调地址。 5. 实现完成开放接口投放卡片实例,完成卡片投放。 用户在进行事件回调前,需要先调用注册卡片回调地址接口完成回调地址的注册,而为了完成回调地址的注册,需要先通过 accessToken 来鉴权调用者身份进行授权。
5.2.1 鉴权:获取 accessToken
在使用 accessToken 时,请注意: 1.accessToken 的有效期为 7200 秒(2小时),有效期内重复获取会返回相同结果并自动续期,过期后获取会返回新的 accessToken 。 2.开发者需要缓存 accessToken ,用于后续接口的调用。因为每个应用的 accessToken 是彼此独立的,所以进行缓存时需要区分应用来进行存储。 3.不能频繁调用 gettoken 接口,否则会受到频率拦截。 在获取 accessToken 前,需要在开发者后台查看应用的AppKey和AppSecret:

请求方式:
POST /v1.0/oauth2/accessToken HTTP/1.1
Host:api.dingtalk.com
Content-Type:application/json
{
"appKey" : "String",
"appSecret" : "String"
}请求示例(Java):
// This file is auto-generated, don't edit it. Thanks.
package com.aliyun.sample;
import com.aliyun.tea.*;
public class Sample {
/**
* 使用 Token 初始化账号Client
* @return Client
* @throws Exception
*/
public static com.aliyun.dingtalkoauth2_1_0.Client createClient() throws Exception {
com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config();
config.protocol = "https";
config.regionId = "central";
return new com.aliyun.dingtalkoauth2_1_0.Client(config);
}
public static void main(String[] args_) throws Exception {
java.util.List<String> args = java.util.Arrays.asList(args_);
com.aliyun.dingtalkoauth2_1_0.Client client = Sample.createClient();
com.aliyun.dingtalkoauth2_1_0.models.GetAccessTokenRequest getAccessTokenRequest = new com.aliyun.dingtalkoauth2_1_0.models.GetAccessTokenRequest()
.setAppKey("dingeqqpkv3xxxxxx")
.setAppSecret("GT-lsu-taDAxxxsTsxxxx");
try {
client.getAccessToken(getAccessTokenRequest);
} catch (TeaException err) {
if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
// err 中含有 code 和 message 属性,可帮助开发定位问题
}
} catch (Exception _err) {
TeaException err = new TeaException(_err.getMessage(), _err);
if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
// err 中含有 code 和 message 属性,可帮助开发定位问题
}
}
}
}返回参数:
HTTP/1.1 200 OK
Content-Type:application/json
{
"accessToken" : "fw8ef8we8f76e6f7s8dxxxx",
"expireIn" : 7200
}5.2.2 注册回调地址
注册回调地址,就是用户将自己服务的 URL 注册到一个 callbackRouteKey 上,用户在创建卡片时,需要将这个 callbackRouteKey 填写到卡片的创建参数中。之后,卡片发生交互请求时,卡片服务端会将这个交互请求发送给卡片绑定的 callbackRouteKey 所对应的 URL 处。下面简单介绍注册回调地址的 API 。 请求方法:
POST /v1.0/card/callbacks/register HTTP/1.1
Host:api.dingtalk.com
x-acs-dingtalk-access-token:String
Content-Type:application/json
{
"callbackRouteKey" : "String",
"callbackUrl" : "String",
"apiSecret" : "String",
"forceUpdate" : Boolean
}Header 参数中的 x-acs-dingtalk-access-token 就是上面获得的 accessToken Body 参数说明:

Java请求示例:
// This file is auto-generated, don't edit it. Thanks.
package com.aliyun.sample;
import com.aliyun.tea.*;
public class Sample {
/**
* 使用 Token 初始化账号Client
* @return Client
* @throws Exception
*/
public static com.aliyun.dingtalkcard_1_0.Client createClient() throws Exception {
com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config();
config.protocol = "https";
config.regionId = "central";
return new com.aliyun.dingtalkcard_1_0.Client(config);
}
public static void main(String[] args_) throws Exception {
java.util.List<String> args = java.util.Arrays.asList(args_);
com.aliyun.dingtalkcard_1_0.Client client = Sample.createClient();
com.aliyun.dingtalkcard_1_0.models.RegisterCallbackHeaders registerCallbackHeaders = new com.aliyun.dingtalkcard_1_0.models.RegisterCallbackHeaders();
registerCallbackHeaders.xAcsDingtalkAccessToken = "<your access token>";
com.aliyun.dingtalkcard_1_0.models.RegisterCallbackRequest registerCallbackRequest = new com.aliyun.dingtalkcard_1_0.models.RegisterCallbackRequest()
.setCallbackRouteKey("example-route-key")
.setCallbackUrl("https://example/callback")
.setApiSecret("example-secret")
.setForceUpdate(false);
try {
client.registerCallbackWithOptions(registerCallbackRequest, registerCallbackHeaders, new com.aliyun.teautil.models.RuntimeOptions());
} catch (TeaException err) {
if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
// err 中含有 code 和 message 属性,可帮助开发定位问题
}
} catch (Exception _err) {
TeaException err = new TeaException(_err.getMessage(), _err);
if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
// err 中含有 code 和 message 属性,可帮助开发定位问题
}
}
}
}返回示例:
HTTP/1.1 200 OK
Content-Type:application/json
{
"success" : true,
"result" : {
"callbackUrl" : "https://example/callback",
"apiSecret" : "example-secret"
}
}错误码:

5.2.3 接收事件回调
当在前置环节中配置好按钮绑定回传请求地址之后,用户在钉钉上点击该按钮,卡片会向您注册好的互动卡片回调地址发送一个 POST 请求,请求内容为:
{
"type": "actionCallback",
"outTrackId": "XXXXXX",
"corpId": "dingXXXXXX",
"userId": "XXXXXX",
"content": "{\"cardPrivateData\":{\"actionIds\":[\"1\"],\"params\":{\"action\":\"accept\"}}}"
}content 字段是一个 JSON String,解析后的格式如下:
{
"cardPrivateData": {
"actionIds": [
"1"
],
"params": {
"action": "accept"
}
}
}参数说明:

5.2.4 回调签名验证
为了提升回调接口的安全性,从钉钉侧发起的 HTTP 回调请求,支持开发者进行来源校验。 如注册卡片回调地址时提供了“卡片数据回调 apiSecret”,则收到的 HTTP 请求 Header 中包含签名相关 Header: • x-ddpaas-signature-timestamp:签名时间戳 • x-ddpaas-signature:签名串 其中 <签名串> = calcSignature(apiSecret, <签名时间戳>),apiSecret 是配置时指定的“卡片数据回调 Secret” 接口提供方应使用如下方法计算签名并验证签名串是否正确以防未授权的调用:
public static String calcSignature(String apiSecret, long ts) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec key = new SecretKeySpec(apiSecret.getBytes(), "HmacSHA256");
mac.init(key);
return Base64.getEncoder()
.encodeToString(mac.doFinal(Long.toString(ts).getBytes()));
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new GatewayException(ErrorCodeConstant.SYSTEM_ERROR,
"sign api secret failed", e);
}
}5.3 基于Stream的事件回调
Stream 模式是钉钉开放平台提供的一种集成方式,它可以监听机器人回调、事件订阅回调和注册卡片回调。使用 Stream 模式接入,钉钉开放平台将通过 Websocket 连接与应用程序通讯,Stream 模式将极大降低接入门槛和资源依赖,不需要公网服务器、IP、域名等资源,只需集成钉钉开放平台 SDK 即可。
在 Stream 模式下,开发者的应用程序通过集成 SDK 的方式与钉钉开放平台建立一条 WebSocket 连接,建立连接过程中开放平台将对连接进行鉴权。当有卡片回调发生时,开放平台将通过 WebSocket 连接将数据通知到开发者的应用程序。开发者的应用程序可以接收到这些数据并进行相应处理,从而实现与钉钉开放平台的实时通信,参考下图所示

STREAM 需要完成的准备工作:
1.在卡片平台上完成卡片模板搭建及发布,并配置交互组件
2.通过 Stream SDK 和钉钉建立长连接
3.实现完成开放接口创建卡片实例,并在创建卡片时指定参数 callbackType 为 STREAM
4.实现完成开放接口投放卡片实例,完成卡片投放
5.3.1 接入方式
接入限制: 1.应用程序所部署环境具备访问公网的能力。 2.仅适用于企业内部开发和第三方企业应用。 3.每个客户端实例默认启用一条 WebSocket 连接,一个应用默认最多建立50条连接。 Java -运行环境:JDK1.8及以上 -安装Java SDK 添加依赖项到工程的pom.xml文件或下载对应的jar包,最新的 SDK 版本可以在这里查看和下载。
<dependency>
<groupId>com.dingtalk.open</groupId>
<artifactId>dingtalk-stream</artifactId>
<version>{sdk-version}</version>
</dependency>5.3.2 长连接注册
用户在进行事件回调前,开发者需要先通过SDK和钉钉建立长连接。下面简单介绍长连接注册的相关代码。
import com.dingtalk.open.app.api.OpenDingTalkStreamClientBuilder;
import com.dingtalk.open.app.api.callback.OpenDingTalkCallbackListener;
import com.dingtalk.open.app.api.security.AuthClientCredential;
/**
* 长连接holder, 维护和钉钉开放平台外联网关的Stream长连接
*/
public class PersistenceConnectionHolder {
public static void main(String[] args) throws Exception{
OpenDingTalkStreamClientBuilder
.custom()
.credential(new AuthClientCredential("<your appKey>", "<you app secret>"))
.registerCallbackListener("<card call back topic>", yourListener)
.build().start();
//other code
}
}参数说明: 回调处理 长连接注册完成后,可以接收到卡片的回调请求。当配置好按钮之后,用户在钉钉上点击该按钮,卡片会向长连接推送回调请求
import com.dingtalk.open.app.api.OpenDingTalkStreamClientBuilder;
import com.dingtalk.open.app.api.callback.OpenDingTalkCallbackListener;
import com.dingtalk.open.app.api.security.AuthClientCredential;
/**
* 长连接holder, 维护和钉钉开放平台外联网关的Stream长连接
* @author dingtalk
*/
public class PersistenceConnectionHolder {
private static OpenDingTalkCallbackListener<CardCallbackRequest, CardCallbackResponse> yourListener
= new OpenDingTalkCallbackListener<CardCallbackRequest, CardCallbackResponse>() {
@Override
public CardCallbackResponse execute(CardCallbackRequest request) {
log.info("receive call back request, {}", request);
//your code is here
//开发者根据自身业务需求,变更卡片内容,返回response
CardCallbackResponse response = new CardCallbackResponse();
return response;
}
};
//main
/**
* 卡片回调请求
*/
public static class CardCallbackRequest{
/**
* 回调类型,actionCallback
*/
private String type;
/**
* 发起事件回调卡片的ID
*/
private String outTrackId;
/**
* 回调内容,ActionCallbackContent的jsonString格式
*/
private String content;
/**
* 卡片归属的企业id
*/
private String corpId;
/**
* 用户userId
*/
private String userId;
/**
* 回调按钮的内容信息
*/
public static class ActionCallbackContent {
private PrivateCardActionData cardPrivateData;
public static class PrivateCardActionData {
//点击按钮的id
private List<String> actionIds;
//给按钮配置的额外参数
private Map<String, Object> params;
}
}
}
/**
* 卡片回调响应
*/
public static class CardCallbackResponse {
//卡片公有数据
private CardDataDTO cardData;
//触发回调用户的私有数据
private CardDataDTO userPrivateData;
public static class CardDataDTO{
//卡片参数
private Map<String, String> cardParamMap;
}
}
}request参数说明:

5.3.3 事件回调响应
不论是 HTTP 模式还是 Stream 模式,都可以通过响应事件回调来更新卡片数据,并且在回传请求事件或者包含回传请求的事件链中,需要以事件返回值作为判断条件进行弹窗、打开链接等场景时都需要通过事件回调响应 response 的方式来更新数据才能生效。 事件回调的响应 response 参数说明: cardData,卡片的公共数据,参考示例:
{
"cardData": {
"cardParamMap": {
"key" : "value"
}
}
}userPrivateData,触发回调用户的私有数据,不需要以用户的 userId 为 key,参考示例:
{
"userPrivateData": {
"cardParamMap": {
"key" : "value"
}
}
}cardUpdateOptions,卡片更新选项,是否按 key 更新 cardData 和 userPrivateData,参考示例:
{
"cardUpdateOptions": {
"updateCardDataByKey": true,
"updatePrivateDataByKey": true
}
}5.3.4 注意事项
1.事件回调有超时(TIMEOUT)限制,请在 2 秒内完成业务处理并响应。如果有比较耗时的业务逻辑处理(比如调用大模型),考虑异步调用更新卡片的方式来更新卡片。
2.请勿在回调过程中,调用更新接口。
名词速查
互动卡片:一种即时交互、多人协同,数据驱动的轻量卡片
搭建平台:卡片提供的低代码搭建平台,通过该平台可进行卡片的模板搭建、管理,卡片实例创建以及投放操作
卡片模板:通过搭建平台创建的卡片模板样式,一个模板包含了一组卡片的布局、组件、变量的集合
卡片实例化:通过模板和模板中每个变量对应的数据做映射绑定,生成的产物即为卡片实例,基于卡片实例可以做后续的行为,例如绑定动态数据源,卡片投放,卡片数据更新,多端多人互动交互,事件回调等操作
卡片投放:通过搭建平台或者卡片提供的开放接口将卡片实例面向钉钉的开放场域进行投递发送行为,例如在钉钉群聊场域中发送一个卡片消息,面向协作场域中投递一个待办的卡片
卡片更新:通过卡片提供的开放接口更新已经投放的卡片上的数据。 事件回调:通过卡片提供的开放接口注册回调地址,当卡片发生互动行为或者需要回源到业务拉取数据时,通过注册的回调地址请求到业务服务,进行相应的动作
动态数据源:卡片提供的一种渲染时业务数据同步策略,当业务卡片在用户端上上屏时,通过卡片绑定的动态数据源配置,实时发起动态数据请求到业务侧做数据拉取,将业务返回的数据同步渲染到卡片对应的动态数据源字段,增强业务数据到卡片渲染的及时性,实现千人千面的卡片数据展示效果
卡片鉴权:基于酷应用创建的卡片支持酷应用鉴权,当卡片实例化过程中开启鉴权,在用户端侧卡片数据获取渲染之前可以做酷应用的可用性有效性的鉴权,如果卡片对应的酷应用已下架或已退订,卡片的实例将无法获取以及后续操作
场域:基于钉钉产品功能的人和事的协同场,例如个人 Profile、群聊、单聊等