钉钉小程序生态1—区分企业内部应用、第三方企业应用、第三方个人应用
钉钉小程序生态2—区分小程序和H5微应用
钉钉小程序生态3—钉钉扫码登录PC端网站
钉钉小程序生态4—钉钉小程序三方企业应用事件与回调
钉钉小程序生态5—钉钉群机器人消息通知和钉钉工作通知
钉钉小程序生态6—钉钉OA自定义审批流的创建和使用
大家好!我是sum墨,一个一线的底层码农,平时喜欢研究和思考一些技术相关的问题并整理成文,限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。
钉钉作为一款办公软件,审批功能是它的核心功能之一,最常见的审批场景就是请假和报销了。虽然钉钉也内置了一些审批流,但是审批场景层出不穷,光靠钉钉内置的那些是不够用的。尤其一些公司自己也有技术团队,则更希望可以二次开发一下,做一套更适合自己公司的审批流。那么本文我们就钉钉的审批能力来讲一下:钉钉OA自定义审批流的创建和使用。
这个还是要说下,否则很多人都找不到!
登录链接如下:https://oa.dingtalk.com/
进去之后是这样的,我们也可以在这里创建新表单,不过这里创建的表单是不支持代码调用的。
那么,接下来正文开始!
如果你的组织的类型是认证服务商
,那么可以选择创建第三方企业应用,否则就创建企业内部应用。
这两种应用的主要区别就是获取AccessToken的方式不同,如何不同可以看我的这篇文章:钉钉小程序生态1—区分企业内部应用、第三方企业应用、第三方个人应用
那么如何判断自己是不是服务商组织
呢?登录开放平台—>首页—>有认证服务商
标签的就是啦
这里我为了方便文章撰写,我就创建一个企业内部应用来说明接下来的流程。如果大家使用的是第三方企业应用,那么还需要配置一下钉钉事件回调,详细可见我这篇文章:
钉钉小程序生态4—钉钉应用事件与回调
这里H5微应用、小程序两种类型都可以,我们主要是为了获取创建钉钉OA自定义审批流的权限。
权限一共5个全都点申请,将对应权限权限申请好之后,我们就可以调用接口创建OA审批模板和发起审批实例了。
如何接入可以看钉钉的官方文档:配置Stream推送,非常的简单,这里我就不贴代码了。
配置回调的作用是为了后续审批状态发生变化的时候可以及时通知到我们。
到目前为止,创建和配置相关的工作我们已经完成了,接下来就是开发了。
模板的创建是一次性的,也就是说只需要调用一下创建接口就行,这里复杂的东西是它的控件很多,比如:文本框、数字框、日期选择器等等,如下图:
用可视化界面创建固然是容易,但是要用代码来创建就有点麻烦了,我开始也错了好几次,从简单的控件开始尝试就好了,多试几次就行。
官方链接如下:创建或更新审批表单模板
这里我自己创建的代码如下:
package com.example.dingtalkoa.demo;
import com.aliyun.dingtalkworkflow_1_0.models.FormComponent;
import com.aliyun.dingtalkworkflow_1_0.models.FormComponentProps;
import com.aliyun.dingtalkworkflow_1_0.models.FormCreateHeaders;
import com.aliyun.dingtalkworkflow_1_0.models.FormCreateRequest;
import com.aliyun.dingtalkworkflow_1_0.models.FormCreateResponse;
import com.aliyun.tea.TeaException;
import com.aliyun.teaopenapi.models.Config;
import com.aliyun.teautil.Common;
import com.aliyun.teautil.models.RuntimeOptions;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class Sample3 {
/**
* 获取AccessToken
*
* @return
*/
public static String getAccessToken() throws Exception {
com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config();
config.protocol = "https";
config.regionId = "central";
com.aliyun.dingtalkoauth2_1_0.Client client = new com.aliyun.dingtalkoauth2_1_0.Client(config);
com.aliyun.dingtalkoauth2_1_0.models.GetAccessTokenRequest getAccessTokenRequest
= new com.aliyun.dingtalkoauth2_1_0.models.GetAccessTokenRequest()
.setAppKey("xxx")
.setAppSecret("xxxx");
try {
return client.getAccessToken(getAccessTokenRequest).getBody().getAccessToken();
} 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 属性,可帮助开发定位问题
}
}
return null;
}
public static void main(String[] args) throws Exception {
Config config = new Config();
config.protocol = "https";
config.regionId = "central";
com.aliyun.dingtalkworkflow_1_0.Client client = new com.aliyun.dingtalkworkflow_1_0.Client(config);
FormCreateHeaders formCreateHeaders = new FormCreateHeaders();
formCreateHeaders.xAcsDingtalkAccessToken = getAccessToken();
// 1. 单行输入控件
FormComponentProps formComponentProps1 = new FormComponentProps()
.setComponentId("TextField-title")
.setPlaceholder("文章标题")
.setLabel("文章标题")
.setRequired(true);
FormComponent formComponent1 = new FormComponent()
.setComponentType("TextField")
.setProps(formComponentProps1);
FormComponentProps formComponentProps2 = new FormComponentProps()
.setComponentId("TextField-url")
.setPlaceholder("文章内容链接")
.setLabel("文章内容链接")
.setRequired(true);
FormComponent formComponent2 = new FormComponent()
.setComponentType("TextField")
.setProps(formComponentProps2);
FormCreateRequest formCreateRequest = new FormCreateRequest()
.setName("文章发布申请")
.setDescription("文章发布申请")
.setFormComponents(java.util.Arrays.asList(formComponent1, formComponent2));
try {
FormCreateResponse formCreateResponse = client.formCreateWithOptions(formCreateRequest, formCreateHeaders,
new RuntimeOptions());
System.out.println("创建的processCode:" + formCreateResponse.getBody().getResult().getProcessCode());
} catch (TeaException err) {
log.error("--->", err);
if (!Common.empty(err.code) && !Common.empty(err.message)) {
// err 中含有 code 和 message 属性,可帮助开发定位问题
}
} catch (Exception _err) {
log.error("--->", _err);
TeaException err = new TeaException(_err.getMessage(), _err);
if (!Common.empty(err.code) && !Common.empty(err.message)) {
// err 中含有 code 和 message 属性,可帮助开发定位问题
}
}
}
}
maven依赖代码如下:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.7.17version>
<relativePath/>
parent>
<groupId>com.examplegroupId>
<artifactId>DingTalkOAartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>DingTalkOAname>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>com.dingtalk.opengroupId>
<artifactId>app-stream-clientartifactId>
<version>1.1.0version>
dependency>
<dependency>
<groupId>com.aliyungroupId>
<artifactId>dingtalkartifactId>
<version>2.0.14version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.26version>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
创建好之后可以在OA里面找到刚才创建的审批流模板
审批表单模板创建结束后,钉钉会返回一个processCode给我们,这个processCode很重要需要保存下来。整体来说,审批表单模板的创建不难理解,毕竟在这里不需要设置各个环节的审批人,真正复杂的是发起审批实例这个接口,下面我们来讲一下如何发起审批实例。
官方文档:发起审批实例
这里我自己发起实例的代码如下:
package com.example.dingtalkoa.demo;
import java.util.ArrayList;
import java.util.List;
import com.alibaba.fastjson.JSONObject;
import com.aliyun.dingtalkworkflow_1_0.models.StartProcessInstanceRequest.StartProcessInstanceRequestApprovers;
import com.aliyun.dingtalkworkflow_1_0.models.StartProcessInstanceResponse;
import com.aliyun.tea.TeaException;
import com.aliyun.teaopenapi.models.Config;
import com.aliyun.teautil.models.RuntimeOptions;
public class Sample4 {
/**
* 获取AccessToken
*
* @return
*/
public static String getAccessToken() throws Exception {
com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config();
config.protocol = "https";
config.regionId = "central";
com.aliyun.dingtalkoauth2_1_0.Client client = new com.aliyun.dingtalkoauth2_1_0.Client(config);
com.aliyun.dingtalkoauth2_1_0.models.GetAccessTokenRequest getAccessTokenRequest
= new com.aliyun.dingtalkoauth2_1_0.models.GetAccessTokenRequest()
.setAppKey("xxx")
.setAppSecret("xxx");
try {
return client.getAccessToken(getAccessTokenRequest).getBody().getAccessToken();
} 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 属性,可帮助开发定位问题
}
}
return null;
}
/**
* 使用 Token 初始化账号Client
*
* @return Client
* @throws Exception
*/
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.models.StartProcessInstanceRequest.StartProcessInstanceRequestFormComponentValues
formComponentValues0
=
new com.aliyun.dingtalkworkflow_1_0.models.StartProcessInstanceRequest.StartProcessInstanceRequestFormComponentValues()
.setName("TextField-title")
.setValue("测试文章标题");
com.aliyun.dingtalkworkflow_1_0.models.StartProcessInstanceRequest.StartProcessInstanceRequestFormComponentValues
formComponentValues1
=
new com.aliyun.dingtalkworkflow_1_0.models.StartProcessInstanceRequest.StartProcessInstanceRequestFormComponentValues()
.setName("TextField-url")
.setValue("https://baidu.com");
//获取审批人
List<StartProcessInstanceRequestApprovers> approvers = new ArrayList<>();
approvers.add(
new com.aliyun.dingtalkworkflow_1_0.models.StartProcessInstanceRequest.StartProcessInstanceRequestApprovers()
.setActionType("NONE")
.setUserIds(java.util.Arrays.asList(
"xxx"
)));
com.aliyun.dingtalkworkflow_1_0.models.StartProcessInstanceRequest startProcessInstanceRequest
= new com.aliyun.dingtalkworkflow_1_0.models.StartProcessInstanceRequest()
//.setDeptId(1L)
.setApprovers(approvers)
.setMicroappAgentId(xxx)
.setOriginatorUserId("xxx")
.setProcessCode("xxx")
.setFormComponentValues(java.util.Arrays.asList(
formComponentValues0,
formComponentValues1
));
com.aliyun.dingtalkworkflow_1_0.Client client = createClient();
com.aliyun.dingtalkworkflow_1_0.models.StartProcessInstanceHeaders startProcessInstanceHeaders
= new com.aliyun.dingtalkworkflow_1_0.models.StartProcessInstanceHeaders();
startProcessInstanceHeaders.xAcsDingtalkAccessToken = getAccessToken();
JSONObject.toJSONString(startProcessInstanceRequest);
StartProcessInstanceResponse startProcessInstanceResponse = client.startProcessInstanceWithOptions(
startProcessInstanceRequest, startProcessInstanceHeaders,
new RuntimeOptions());
}
}
把参数都准备好之后,实现起来还是比较简单的,调用代码创建的审批实例,钉钉会返回一个实例ID:instanceId,这个instanceId和processCode一样也需要保存下来,发送成功后钉钉APP上就会自动出现一条OA审批啦。
所谓审批实例状态监控,就是当前审批流程是被同意啦还是被拒绝了。这里有两种方案:
而作为一个成年人,这两个肯定是全都要啦,一个用来实时更新,一个用来做兜底。
这里查询的审批实例的接口文档链接如下:获取单个审批实例详情。
如果前面创建审批模板、发起审批实例都能跑通,那么这个接口也肯定不在话下,所以这里我就不贴代码了。
最后我把事件订阅推送的数据格式贴一下:
[
{
"result": "refuse",
"processInstanceId": "xxx",
"eventId": "xxx",
"finishTime": 1698231807000,
"createTime": 1698227806000,
"processCode": "PROC-xxx",
"businessId": "xxx",
"title": "xxx提交的文章发布申请",
"type": "finish",
"staffId": "xxx",
"taskId": "xxx"
}
]
写在最后:其实这些东西大部分都是钉钉官方文档上面的,除了那个agentId… 但是钉钉文档的东西实在是太多,作为一个开发者,我们不可能去从头到尾看一遍的,一般都是用到了就去找。但是这样一来又会很混乱,所以我这篇文章主要是从开发者角度来梳理一下这个流程,不仅利己也能帮助其他人。