概述

因为业务需求原因,业务单据的审批功能需要接入到钉钉OA审批上。

因为未接触过钉钉OA审批这方面的东西,所以第一步肯定是打开钉钉开放平台

image-20230825155240047

进了开放平台,肯定就得找文档,不过我看到了搜索框,既然明确自己需要查询OA审批,那就直接搜索了。

image-20230825155422768

根据搜索结果,有两百多条,可见找对地方了。看推荐提示,钉钉的OA审批有多种(官方、自有...),点进“官方OA审批”。

image-20230825155626324

结果跑进“常见问题”来了,切换目录到“概述”,先了解下整个OA审批。

image-20230825155802447

文档内容“一目了然”(接口参数文档和错误码解释给我整麻了),也确定了OA审批分为了“官方OA审批”和“自有OA审批”两种。

把目录菜单都大致点完一遍,总结如下:

  • 官方OA审批是走的钉钉审批流;自有OA审批只是做了层跳转和展示,可用于多系统的统一审批入口。
  • OA审批的元素有:审批表单、审批实例、审批任务。
  • 审批单据里面的附件是必须要上传到钉钉的钉盘,才能展示到审批详情里面的。
  • 审批回调需要单独配置,属于钉钉事件订阅中的一种,事件推送方式有HTTP推送、Stream推送两种(个人推荐首选Stream方式),配置完推送方式后还需要配置需要订阅的事件。
  • HTTP推送就是将公网地址接口提供给钉钉,让钉钉回调;Stream推送是业务系统与钉钉开放平台通过Websocket连接。Stream模式介绍:服务端Stream模式
  • 与钉钉开放平台的API交互,全程都要有token,因此在应用开发中需要配置钉钉应用,获取对应的AppKey、AppSecret,通过AK和AS调用接口获取token。
  • 使用钉钉开放平台的OA审批功能,是需要钉钉用户具有开发者权限的(偷懒方式是直接使用拥有主管理员权限的账号),也需要开启应用的接口权限,在应用开发-权限管理下申请如下权限:
    • 成员信息读权限
    • 工作流实例写权限
    • OA审批和存储的所有权限 (工作流模板写权限、工作流模板读权限)
    • 根据手机号姓名获取成员信息的接口访问权限

官方OA审批示例

了解文档大致后,与需求方确定使用“官方OA审批”,因此后续示例中都默认表示官方OA审批的demo。

接下来,将通过发起一个OA审批单来展示调用API的全过程。

添加maven依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<dependency>
<!-- 钉钉新版SDK -->
<groupId>com.aliyun</groupId>
<artifactId>dingtalk</artifactId>
<!-- 截止20230825的最新版 -->
<version>2.0.30</version>
</dependency>
<dependency>
<!-- 钉钉旧版SDK -->
<groupId>com.aliyun</groupId>
<artifactId>alibaba-dingtalk-service-sdk</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<!-- 钉钉事件回调Stream推送SDK -->
<groupId>com.dingtalk.open</groupId>
<artifactId>app-stream-client</artifactId>
<version>1.0.5</version>
</dependency>

获取token

在概述里也说明了,钉钉开放平台的所有接口基本都是需要使用token的,一是确保数据安全,二是根据token识别钉钉企业。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class GetTokenSample {
public static void main(String[] args) {
try {
DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/gettoken");
OapiGettokenRequest req = new OapiGettokenRequest();
req.setAppkey("AppKey");
req.setAppsecret("AppSecret");
req.setHttpMethod("GET");
OapiGettokenResponse rsp = client.execute(req);
System.out.println(rsp.getBody());
} catch (ApiException e) {
e.printStackTrace();
}
}
}

打印响应数据如下:

1
2
3
4
5
6
{
"errcode":0,
"access_token":"7011eb7b5a5e37c694b18c6c79406111",
"errmsg":"ok",
"expires_in":7200
}

创建审批表单模板

示例代码展示创建一个“最简单”的审批表单模板,详情的参数说明、示例代码可以参考钉钉开放平台文档-创建或更新审批表单模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class CreateFormTemplateSample {

/**
* 使用 Token 初始化账号Client
*/
public static com.aliyun.dingtalkworkflow_1_0.Client createClient() throws Exception {
Config config = new Config();
config.protocol = "https";
config.regionId = "central";
return new com.aliyun.dingtalkworkflow_1_0.Client(config);
}

public static void main(String[] args_) throws Exception {
com.aliyun.dingtalkworkflow_1_0.Client client = CreateFormTemplateSample.createClient();
FormCreateHeaders formCreateHeaders = new FormCreateHeaders();
formCreateHeaders.xAcsDingtalkAccessToken = "7011eb7b5a5e37c694b18c6c79406111";
// 1. 单行输入控件
FormComponentProps formComponentProps1 = new FormComponentProps()
.setLabel("名称");
FormComponent formComponent1 = new FormComponent()
.setComponentType("TextField")
.setProps(formComponentProps1);

FormCreateRequest formCreateRequest = new FormCreateRequest()
.setName("表单示例")
.setFormComponents(java.util.Arrays.asList(
formComponent1
));
try {
FormCreateResponse response = client.formCreateWithOptions(formCreateRequest, formCreateHeaders, new RuntimeOptions());
System.out.println(JSON.toJSONString(response));
} catch (Exception _err) {
_err.printStackTrace();
}
}
}

打印响应数据如下:

1
{"processCode":"PROC-0FAB48EA-FB39-4981-AD80-E67D552B3355"}

发起审批实例

示例代码展示发起一个“最简单”的审批实例,详情的参数说明、示例代码可以参考钉钉开放平台文档-发起审批实例

💡提示:钉钉开放平台在创建审批实例的文档中,有个“巨大”的坑。在body参数的介绍中,未提及到deptId(用户所属部门id)字段,在API Explorer中才有介绍,根据提示说明,在approvers(审批人对象)未传值时,deptId为必填字段,根部门传-1。

如果没有传approvers,同时也没有传deptId,接口回调就会返回异常信息,异常信息就提示个“审批实例参数错误”,根本无从分析具体是哪里错误,这种情况充斥在钉钉OA审批相关的各个API中。

如果你“恰好”碰到了这种类似的“参数错误”,不要着急,仔细浏览钉钉开放平台的文档、示例代码、API Explorer,再不济可以调成功的接口进行分析。例如这里如果创建审批实例错误,可以用钉钉直接人为发起一个审批,接着调用“获取审批实例ID列表”接口得到审批实例id,再调用“获取单个审批实例详情”接口得到审批实例详情数据,进而与自己的代码进行比较分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class StartInstanceSample {

/**
* 使用 Token 初始化账号Client
*/
public static com.aliyun.dingtalkworkflow_1_0.Client createClient() throws Exception {
Config config = new Config();
config.protocol = "https";
config.regionId = "central";
return new com.aliyun.dingtalkworkflow_1_0.Client(config);
}

public static void main(String[] args_) throws Exception {
com.aliyun.dingtalkworkflow_1_0.Client client = StartInstanceSample.createClient();
StartProcessInstanceHeaders startProcessInstanceHeaders = new StartProcessInstanceHeaders();
startProcessInstanceHeaders.xAcsDingtalkAccessToken = "7011eb7b5a5e37c694b18c6c79406111";
StartProcessInstanceRequest.StartProcessInstanceRequestFormComponentValues formComponentValues0 = new StartProcessInstanceRequest.StartProcessInstanceRequestFormComponentValues()
.setName("名称")
.setValue("wray");
StartProcessInstanceRequest startProcessInstanceRequest = new StartProcessInstanceRequest()
.setOriginatorUserId("306354436429120376")
.setProcessCode("PROC-0FAB48EA-FB39-4981-AD80-E67D552B3355")
.setDeptId(-1L)
.setFormComponentValues(Collections.singletonList(formComponentValues0));
try {
StartProcessInstanceResponse response = client.startProcessInstanceWithOptions(startProcessInstanceRequest, startProcessInstanceHeaders, new RuntimeOptions());
System.out.println(JSON.toJSONString(response));
} catch (Exception _err) {
_err.printStackTrace();
}
}
}

打印数据如下:

1
{"instanceId":"KW2ZY679TRyy3Nupz5MN8A10181692956791"}

查询审批实例详情

审批实例详情大致包含了一个审批实例的实例基础信息(审批状态、发起人信息等)、操作记录、任务列表、组件详情列表。

示例代码展示查询上述发起的审批实例详情,详情的参数说明、示例代码可以参考钉钉开放平台文档-获取单个审批实例详情

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class GetInstanceDetailSample {

/**
* 使用 Token 初始化账号Client
*/
public static com.aliyun.dingtalkworkflow_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.dingtalkworkflow_1_0.Client(config);
}

public static void main(String[] args_) throws Exception {
com.aliyun.dingtalkworkflow_1_0.Client client = GetInstanceDetailSample.createClient();
com.aliyun.dingtalkworkflow_1_0.models.GetProcessInstanceHeaders getProcessInstanceHeaders = new com.aliyun.dingtalkworkflow_1_0.models.GetProcessInstanceHeaders();
getProcessInstanceHeaders.xAcsDingtalkAccessToken = "d62086b9f7943eedb06ee0c096d9e111";
com.aliyun.dingtalkworkflow_1_0.models.GetProcessInstanceRequest getProcessInstanceRequest = new com.aliyun.dingtalkworkflow_1_0.models.GetProcessInstanceRequest()
.setProcessInstanceId("KW2ZY679TRyy3Nupz5MN8A10181692956791");
try {
GetProcessInstanceResponse response = client.getProcessInstanceWithOptions(getProcessInstanceRequest, getProcessInstanceHeaders, new RuntimeOptions());
System.out.println(JSON.toJSONString(response));
} catch (Exception _err) {
_err.printStackTrace();
}
}
}

打印数据如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
{
"attachedProcessInstanceIds":[

],
"bizAction":"NONE",
"businessId":"202308251746000319111",
"createTime":"2023-08-25T17:46Z",
"finishTime":"2023-08-25T17:46Z",
"formComponentValues":[
{
"componentType":"TextField",
"id":"TextField_HWna4IkQPgtWz",
"name":"名称",
"value":"wray"
}
],
"operationRecords":[
{
"date":"2023-08-25T17:46Z",
"result":"NONE",
"type":"START_PROCESS_INSTANCE",
"userId":"306354436429120111"
}
],
"originatorDeptId":"-1",
"originatorDeptName":"开发部",
"originatorUserId":"306354436429120111",
"result":"agree",
"status":"COMPLETED",
"tasks":[

],
"title":"Wray提交的表单示例"
}

OA审批事件回调

OA审批事件是钉钉事件消息的一种,在「概述-总结」中也提到过,这里主要就展示Stream推送的代码示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class StreamCallbackSample {

public static void main(String[] args) throws Exception {
OpenDingTalkStreamClientBuilder
.custom()
.credential(new AuthClientCredential("AppKey", "AppSecret"))
//注册事件监听
.registerAllEventListener(new GenericEventListener() {
public EventAckStatus onEvent(GenericOpenDingTalkEvent event) {
try {
//事件类型 根据事件类型推断是否为OA审批事件
String eventType = event.getEventType();
//获取事件体
JSONObject bizData = event.getData();
//处理事件
System.out.println(JSON.toJSONString(bizData));
//消费成功
return EventAckStatus.SUCCESS;
} catch (Exception e) {
//消费失败
return EventAckStatus.LATER;
}
}
})
.build().start();
}
}

启动代码后,在钉钉后台验证Stream模式通道。

image-20230827115355744

推送数据分为两个部分,一个部分为事件的基础信息,另一个部分为事件业务数据信息。

格式如下:

1
2
3
4
5
6
7
8
9
10
{
"eventBornTime":1684132707000, //事件生成时间
"eventCorpId":"ding9f50b15b*****41", //事件所属的corpId
"eventId":"c69632e6e3794bfbb07d33fad9fa82d2", //事件的唯一Id
"eventType":"suite_ticket", //事件类型
"eventUnifiedAppId":"unifiedAppId1", //统一应用身份Id
"data":{ //事件的业务信息
"suiteTicket":"1234455" //suiteTicke业务信息
}
}

接入成功之后,对钉钉实例进行操作,例如现在对OA审批任务进行评论,推送的业务信息如下:

1
2
3
4
5
6
7
8
9
10
11
{
"processInstanceId":"FiFx7mFVTX6R9Iw8TOeheQ10181693105111",
"createTime":1693108281607,
"processCode":"PROC-4650E083-0D7F-42BD-BC1A-30043D5BC111",
"businessId":"202308271058000577111",
"title":"wray提交的表单示例",
"type":"comment",
"businessType":"",
"content":"3",
"staffId":"306354436429120376"
}

相关文章

项目搭建钉钉OA审批底层框架的历程:工作小结-钉钉OA审批(2)