钉钉OA自定义审批流的创建和使用
前言
大家好!我是sum墨,一个一线的底层码农,平时喜欢研究和思考一些技术相关的问题并整理成文,限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。
钉钉作为一款办公软件,审批功能是它的核心功能之一,最常见的审批场景就是请假和报销了。虽然钉钉也内置了一些审批流,但是审批场景层出不穷,光靠钉钉内置的那些是不够用的。尤其一些公司自己也有技术团队,则更希望可以二次开发一下,做一套更适合自己公司的审批流。那么本文我们就钉钉的审批能力来讲一下:钉钉OA自定义审批流的创建和使用。
tips:钉钉OA审批在哪里
这个还是要说下,否则很多人都找不到!
1. 扫码登录钉钉OA
登录链接如下:https://oa.dingtalk.com/
2. 工作台-应用管理-OA审批-进入

进去之后是这样的,我们也可以在这里创建新表单,不过这里创建的表单是不支持代码调用的。

3. 如果实在找不到,去搜索框中搜索审批

那么,接下来正文开始!
一、创建小程序
如果你的组织的类型是认证服务商,那么可以选择创建第三方企业应用,否则就创建企业内部应用。
这两种应用的主要区别就是获取AccessToken的方式不同,如何不同可以看我的这篇文章:钉钉小程序生态1—区分企业内部应用、第三方企业应用、第三方个人应用
那么如何判断自己是不是服务商组织呢?登录开放平台—>首页—>有认证服务商标签的就是啦

这里我为了方便文章撰写,我就创建一个企业内部应用来说明接下来的流程。如果大家使用的是第三方企业应用,那么还需要配置一下钉钉事件回调,详细可见我这篇文章:
钉钉小程序生态4—钉钉应用事件与回调
1. 应用开发-企业内部应用-创建应用-H5微应用

这里H5微应用、小程序两种类型都可以,我们主要是为了获取创建钉钉OA自定义审批流的权限。
2. 基础信息—权限管理—搜索审批

权限一共5个全都点申请,将对应权限权限申请好之后,我们就可以调用接口创建OA审批模板和发起审批实例了。
3. 应用功能—事件与回调—事件订阅—开启审批事件回调

如何接入可以看钉钉的官方文档:配置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依赖代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<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.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.17</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>DingTalkOA</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>DingTalkOA</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.dingtalk.open</groupId>
<artifactId>app-stream-client</artifactId>
<version>1.1.0</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dingtalk</artifactId>
<version>2.0.14</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.26</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
创建好之后可以在OA里面找到刚才创建的审批流模板

审批表单模板创建结束后,钉钉会返回一个processCode给我们,这个processCode很重要需要保存下来。整体来说,审批表单模板的创建不难理解,毕竟在这里不需要设置各个环节的审批人,真正复杂的是发起审批实例这个接口,下面我们来讲一下如何发起审批实例。
三、发起审批实例
1. 参数说明
- processCode
审批模板code,好理解。 - originatorUserId
审批发起人的userId,也理解。 - deptId或approvers
选择审批的部门或者审批的人,这是二选一,传一个就行了,理解上还行。 - ccList
抄送人 userId,就是只会通知到他,而不用他点审批的人,好理解。 - ccPosition
抄送时间点,取值:START:开始时抄送;FINISH:结束时抄送;START_FINISH:开始和结束时都抄送,理解上还行。 - formComponentValues
表单数据内容,控件列表,也就是我们创建的那些控件的具体的值,理解上还行。 - microappAgentId
不理解的来了,这个东西如果你是企业内部应用,你可以很快的在应用信息中找到,如下图

但是!!!谁能告诉我第三方企业应用的agentId在哪???下面是第三方企业应用的应用信息,根本就没有!!!

这是钉钉官方教我们查看官方应用和第三方应用的AgentId的方法

但是!!!钉钉OA升级了了,不是,你们特么升级版本不考虑一下这个的吗???新版本没了,找不到了,打开是这个东西

而且开放平台里面也根本没有可以直接获取AgentId的接口,最后找来找去,终于给找到一个接口:获取企业授权信息,这个接口的返回值里面有一个auth_info,里面有授权应用的agentId,唉,,,

2. 调用示例
官方文档:发起审批实例
这里我自己发起实例的代码如下:
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... 但是钉钉文档的东西实在是太多,作为一个开发者,我们不可能去从头到尾看一遍的,一般都是用到了就去找。但是这样一来又会很混乱,所以我这篇文章主要是从开发者角度来梳理一下这个流程,不仅利己也能帮助其他人。
钉钉OA自定义审批流的创建和使用的更多相关文章
- Dynamic CRM 2013学习笔记(十九)自定义审批流1 - 效果演示
CRM的项目,审批流是一个必须品.为了更方便灵活地使用.配置审批流,我们自定义了一整套审批流.首先来看下它的效果: 1. 审批模板 这是一个最简单的审批流,首先指定审批实体,及相关字段,再配置流程节点 ...
- Dynamic CRM 2013学习笔记(二十一)自定义审批流2 - 配置按钮
上次介绍了 Dynamic CRM 2013学习笔记(十九)自定义审批流1 - 效果演示 现在开始介绍如何配置审批流,首先在form上添加三个按钮,Submit, Agree, Reject: 1. ...
- Dynamic CRM 2013学习笔记(三十二)自定义审批流3 - 节点及实体配置
上次介绍了<Dynamic CRM 2013学习笔记(十九)自定义审批流1 - 效果演示> 以及如何配置自定义审批流的按钮:<Dynamic CRM 2013学习笔记(二十一)自定义 ...
- Dynamic CRM 2013学习笔记(三十三)自定义审批流4 - 规则节点 -有分支的流程处理
上次介绍过节点的基本配置<Dynamic CRM 2013学习笔记(三十二)自定义审批流3 - 节点及实体配置>,这次介绍下规则节点,因为有时流程里会有一些分支.合并,这时就要用到规则节点 ...
- Dynamic CRM 2013学习笔记(三十七)自定义审批流7 - 初始化(整套审批流下载、安装)
前面介绍了自定义审批流的配置.使用,这篇介绍下如何进行初始化. 一. 下载 从下面的地址下载整个审批流: http://yunpan.cn/cZ5Rdx5HCt3VF 下载完后,一共有三块内容: 二. ...
- SpringBoot+Activiti+bpmn.js+Vue.js+Elementui(OA系统审批流)
引言:OA系统用到请假.加班.调休.离职,需要使用工作流进行流程审批 一:activiti流程设计器的选择(通过学习activiti工作流过程中,发现一款好的流程设计器将会更好的方便的设计好流程(主要 ...
- Dynamic CRM 2013学习笔记(三十四)自定义审批流5 - 自动邮件通知
审批过程中,经常要求自动发邮件:审批中要通知下一个审批人进行审批:审批完通知申请人已审批完:被拒绝后,要通知已批准的人和申请人.下面详细介绍如何实现一个自动发邮件的插件: 1. 根据审批状态来确定 ...
- Dynamic CRM 2013学习笔记(三十五)自定义审批流6 - 审批通过后,再审批 - 二次审批
最近有个特殊的需求,客户想做二次审批,就是审批通过后,再走一次审批流程.最开始一想,这还不简单,审批通过后,直接把状态改成draft就完了,后来一试,发现一堆问题,比如第一次审批完后,界面是不允许修改 ...
- Dynamic CRM 2013学习笔记(四十六)简单审批流的实现
前面介绍过自定义审批流: Dynamic CRM 2013学习笔记(十九)自定义审批流1 - 效果演示 Dynamic CRM 2013学习笔记(二十一)自定义审批流2 - 配置按钮 Dynamic ...
- 141_Power Query之获取钉钉审批流自动刷新Power BI报告
博客:www.jiaopengzi.com 焦棚子的文章目录 请点击下载附件 一.背景 钉钉办公给很多企业带来了很多方便,比如审批流线上化,通用化.线上化填写后,数据自动获取又是一个硬伤了,虽然数据可 ...
随机推荐
- Code Generate 代码生成器 V1.0
Code Generate V1.0 代码生成器 根据配置的模板,根据建表语句,生成Code. 例如java代码.vue代码.jsp代码以及html代码等等,均可根据自己的代码写作习惯进行配置. 缺点 ...
- Hexo博客Next6.0版本主题配置(站内搜索、新建404界面、静态资源压缩、底部信息隐藏、各版块透明度修改、字数统计、推荐阅读、博文置顶、阅读进度、在线评论、运行时间)
新建404界面 在站点根目录下,输入hexo new page 404,在默认Hexo站点下/source/404/index.md 打开新建的404界面,编辑属于自己的404界面,可以显示腾讯公益4 ...
- Centos7中Oracle占用CPU过高(解决方案)
Centos7中Oracle占用CPU过高(解决方案) 前言: 99%的问题几乎都是SQL的问题,一般SQL可能会出现以下几种情况: 相关SQL搜索条件没有加索引 索引失效 联合查询过多 数据量过大 ...
- 论文解读(BERT-DAAT)《Adversarial and Domain-Aware BERT for Cross-Domain Sentiment Analysis》
论文信息 论文标题:Adversarial and Domain-Aware BERT for Cross-Domain Sentiment Analysis论文作者:论文来源:2020 ACL论文地 ...
- 记一次公司内部技术分享—DDD
前言 笔者于2021年入职了杭州一家做水务系统的公司,按照部门经理要求,新人需要做一次个人分享(主题随意). 当时笔者对DDD充满了浓厚的兴趣,之前也牛刀小试过,于是就决定班门弄斧Show一下.后来在 ...
- 用 Python 自动创建 Markdown 表格 - 每天5分钟玩转 GPT 编程系列(4)
目录 1. 他们居然问我要 Prompts 2. 让 GPT-4 来写代码 2.1 我对 DevChat 说 2.2 DevChat 回答 2.3 我又对 DevChat 说 2.4 DevChat ...
- cesium中限制地图浏览范围
https://blog.csdn.net/qq_42740164/article/details/119375782?ops_request_misc=%257B%2522request%255Fi ...
- 「学习笔记」gdb 调试的简单操作
gdb是一个命令行下的.功能强大的调试器. 在学习 gdb 前,我们要知道几个最基本的 cmd 命令. cmd 首先,对于 win10 系统,我们按 Windows + R 键,打开运行窗口,在里面输 ...
- 三维模型OSGB格式轻量化的纹理压缩和质量保持分析
三维模型OSGB格式轻量化的纹理压缩和质量保持分析 在三维模型应用中,纹理数据是一个重要的部分,可以为模型增加更多的真实感和细节.但是,由于纹理数据通常会占用大量的存储空间和传输带宽,因此,在OSGB ...
- uniapp 地图如何添加?你要的教程来喽!
地图在 app 中使用还是很广泛的,常见的应用常见有: 1.获取自己的位置,规划路线. 2.使用标记点进行标记多个位置. 3.绘制多边形,使用围墙标记位置等等. 此篇文章就以高德地图为例,以上述三个常 ...