前言

工作小结-钉钉OA审批(2) 中,介绍了项目搭建钉钉OA审批底层框架的思路。本章则主要是整理项目集成中台内部系统OA审批的设计思路,以及优化oa审批底层框架结构。

前源码地址:https://github.com/wangfarui/java-study/tree/main/third-study/dingtalk/dingtalk-oa

现整合后的源码地址:https://github.com/wangfarui/work-report/tree/main/oa-approval

框架目录结构

image-20240103164812301

优化后的项目结构大体上分为三类:

  1. OA审批共有的对象与接口。例如ApprovalBusinessTypeEnum表示审批业务类型对象,BusinessApprovalCallbackListener表示业务审批回调监听接口。
  2. 业务审批表单目录,即 from 目录。它们通过 XxxApprovalFormEngine 接口实现表单控件的自动构建。
  3. OA审批工作流的实现方式,例如 dingtalksystem 目录,它们分别代表了钉钉OA审批和内部系统OA审批。

示例在 DemoApprovalService 下,包含发起审批和审批回调操作。

设计思路

整体功能流程

首先需要明确一点,做这个OA审批底层框架,是为了业务开发人员可以快速使用OA审批功能,不用关注怎么去对接各种OA审批工作流的使用方法。

OA审批功能在业务上主要分为两个,一是发起审批,二是审批结果回调。然后,OA审批底层框架再以此基础上,扩展新功能,例如审批流程的查询、审批撤销等等。

OA审批底层框架通过自定义了一套使用方法,将发起审批和审批结果回调做了包装处理,业务开发人员只用关注业务数据的交互。具体业务流程如下图:

image-20240104152826675

发起审批

发起审批前需要先定义好审批表单模板对象,这个底层框架封装的第一步,通过自定义注解告知控件的名称、类型、是否必填等信息。例如钉钉表单控件 DingTalkFormComponent

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
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface DingTalkFormComponent {

/**
* 表单控件名称
*/
String value();

/**
* 表单控件id
* <p>表单控件列表中唯一</p>
*/
String id() default "";

/**
* 表单控件类型
*/
ComponentType componentType() default ComponentType.AUTO;

/**
* 是否非空, 默认不能为空
*/
boolean required() default false;

/**
* 字段为{@link java.util.Date}时,日期格式化样式
*/
String pattern() default "yyyy-MM-dd HH:mm:ss";
}

一个普通对象定义完控件后,还需要实现 XxxApprovalFormEngine 接口,它的作用就是通过解析自定义注解构建表单模板对象和表单实例对象。

有了表单模板对象后,就可以开始发起审批了,发起审批需要根据不同的OA审批实现端找到对应的 XxxApprovalService 服务类,并调用 startApprovalFlowInstance 方法。方法接收一个表单实例对象参数,实例对象需要业务唯一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
33
34
35
36
37
38
39
40
41
42
43
public class SystemApprovalFormInstance {

/**
* scm业务id
* <p>非空</p>
*/
private Long businessId;

/**
* 审批业务类型
* <p>非空</p>
*/
private ApprovalBusinessTypeEnum businessTypeEnum;

/**
* 审批流程表单模板对象
* <p>非空</p>
*/
private SystemApprovalFormEngine approvalForm;

/**
* 业务发起日期
*/
private Date businessDate;

/**
* scm租户id
* <p>非空字段,为空时默认从UserUtils获取</p>
*/
private Long tenantId;

/**
* scm操作人id
* <p>非空字段,为空时默认从UserUtils获取</p>
*/
private Long userId;

/**
* 部门id
* <p>存在多部门时,必传</p>
*/
private Long departmentId;
}

在调用服务类的 startApprovalFlowInstance 方法发起审批成功后,底层框架会保存一条业务与OA审批实现端的关联映射关系数据,这条数据是后面审批回调做数据绑定的关键,同时也可以用来判断当前业务的审批状态。

审批回调

在业务方,因为有了底层框架的存在,所以不需要关注OA审批实现端是怎么回调至系统的,它只需要针对不同的审批结果做业务处理即可。因此底层框架提供了审批回调注解 XxxApprovalCallback ,通过标记在方法上实现。例如系统审批回调注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface SystemApprovalCallback {

/**
* 业务审批类型
*/
ApprovalBusinessTypeEnum value();

/**
* 审批事件类型
* <p>系统审批不支持 ApprovalEventType.ALL </p>
*/
ApprovalEventType eventType();
}

业务方在方法上指定其对应的业务审批类型和审批事件类型,底层框架会根据OA审批实现端的审批结果回调对应的方法,并返回一个业务审批回调事件对象(BusinessApprovalCallbackEvent)。

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

/**
* 业务id
*/
private Long businessId;

/**
* 租户id
*/
private Long tenantId;

/**
* 用户id
* <p>钉钉审批回调时: 当钉钉企业用户与SCM企业用户的手机号匹配不到时,可能为空</p>
*/
private Long userId;

/**
* 用户名称
*/
private String userName;
}

设计缺陷

因为工作内容的排期问题,原是只需要钉钉审批一种实现方式的,所以没有考虑抽象对象的层级,底层框架对业务系统提供的接口名称都含有钉钉相关字样。后面接入内部系统审批时,因为涉及到的业务代码范围太广,只好做成增量式扩展,例如表单控件注解分成了 DingTalkFormComponentSystemFormComponent ,审批回调注解分成了 DingTalkApprovalCallbackSystemApprovalCallback ,包括很多内部实现都出现了重合情况。

总结

这次主要是将钉钉审批和内部系统审批做了整合,但因为历史迭代原因,对外接口是分离的,整体的流程思路基本是不变的,基于 “定义表单模板对象 -> 发起审批 -> 审批回调” 封装。

若后期还会新的OA审批实现端接入,希望把对外接口做成统一吧,至于历史模块只能做兼容了。