书接上回,本文主要分享 企业内部系统集成钉钉官方OA审批流程的步骤 的第二部分。
后端代码中集成钉钉官方OA审批API:
表结构设计、钉钉API访问工具类、发起流程实例接口、查询流程审核日志接口、流程审核同意回调接口、流程审核拒绝回调接口、钉钉免登录接口。
1. 表结构设计
在已经业务数据表结构的基础上添加钉钉审核流程相关字段:
|-------------------|-------------------------------------------------------------------------------------|
| 字段名称 | 功能描述 |
| dingtalk_union_id | 钉钉unionId |
| dingtalk_user_id | 钉钉userId |
| dingtalk_dept_id | 钉钉用户所属部门id 多个逗号分隔 |
| process_id | 钉钉流程实例id |
| auditing_result | 审核结果 同意agree 拒绝refuse |
| auditing_status | 审核状态:NEW("新创建"), RUNNING("审批中"), TERMINATED("被终止"), COMPLETED("完成"), CANCELED("取消") |
2. 钉钉API访问工具类
获取 钉钉API访问 access_token
java
/**
* 获取 钉钉API访问 access_token
* @return
* @throws ApiException
*/
public static String getAccessToken() throws ApiException {
DefaultDingTalkClient client = new DefaultDingTalkClient(DingConstantsUtil.GET_ACCESS_TOKEN_URL);
OapiGettokenRequest request = new OapiGettokenRequest();
request.setAppkey(DingConstantsUtil.APP_KEY);
request.setAppsecret(DingConstantsUtil.APP_SECRET);
request.setHttpMethod("GET");
OapiGettokenResponse response = client.execute(request);
return response.getAccessToken();
}
获取钉钉API访问客户端
java
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);
}
3. 发起流程实例接口
java
/**
* 发起 钉钉审核流程
* @param dingtalkUserId
* @param dingtalkDeptId
* @param processCode
* @param performance
* @return
* @throws Exception
*/
private Map<String, Object> startProjectProfitFinalizationProcess(
String dingtalkUserId, String dingtalkDeptId, String processCode, Performance performance,
List<Attachment> attachments) throws Exception {
// 重新获取 钉钉的 accessToken
String accessToken = DingtalkUtil.getAccessToken();
com.aliyun.dingtalkworkflow_1_0.Client client = DingtalkUtil.createClient();
StartProcessInstanceHeaders startProcessInstanceHeaders = new StartProcessInstanceHeaders();
startProcessInstanceHeaders.xAcsDingtalkAccessToken = accessToken;
List<StartProcessInstanceRequest.StartProcessInstanceRequestFormComponentValues> formComponentValues = new ArrayList<>();
StartProcessInstanceRequest.StartProcessInstanceRequestFormComponentValues formComponentValues00 = new StartProcessInstanceRequest.StartProcessInstanceRequestFormComponentValues()
.setName("隐藏字段-更新流程进度使用")
.setValue(performance.getId().toString());
formComponentValues.add(formComponentValues00);
StartProcessInstanceRequest.StartProcessInstanceRequestFormComponentValues formComponentValues18 = new StartProcessInstanceRequest.StartProcessInstanceRequestFormComponentValues()
.setName("标题")
.setValue(performance.getTitle());
formComponentValues.add(formComponentValues18);
StartProcessInstanceRequest.StartProcessInstanceRequestFormComponentValues formComponentValues0 = new StartProcessInstanceRequest.StartProcessInstanceRequestFormComponentValues()
.setName("被考核人")
.setValue(performance.getAppraisee());
formComponentValues.add(formComponentValues0);
StartProcessInstanceRequest.StartProcessInstanceRequestFormComponentValues formComponentValues9 = new StartProcessInstanceRequest.StartProcessInstanceRequestFormComponentValues()
.setName("部门名称")
.setValue(performance.getDeptName());
formComponentValues.add(formComponentValues9);
StartProcessInstanceRequest.StartProcessInstanceRequestFormComponentValues formComponentValues1 = new StartProcessInstanceRequest.StartProcessInstanceRequestFormComponentValues()
.setName("岗位名称")
.setValue(performance.getPosName());
formComponentValues.add(formComponentValues1);
// 这里需要将 绩效指标 明细数据 转换成json字符数组的形式,以满足钉钉的方法入参格式要求
StartProcessInstanceRequest.StartProcessInstanceRequestFormComponentValues formComponentValues17 = new StartProcessInstanceRequest.StartProcessInstanceRequestFormComponentValues()
.setName("绩效指标")
.setValue(performance.getAppraiseeIndex());
formComponentValues.add(formComponentValues17);
StartProcessInstanceRequest.StartProcessInstanceRequestFormComponentValues formComponentValues16 = new StartProcessInstanceRequest.StartProcessInstanceRequestFormComponentValues()
.setName("绩效等级")
.setValue(performance.getGrade());
formComponentValues.add(formComponentValues16);
StartProcessInstanceRequest.StartProcessInstanceRequestFormComponentValues formComponentValues15 = new StartProcessInstanceRequest.StartProcessInstanceRequestFormComponentValues()
.setName("评语")
.setValue(performance.getComment());
formComponentValues.add(formComponentValues15);
// 解析出钉钉用户所属部门
List<String> deptIds = new ArrayList<>(Arrays.asList(dingtalkDeptId.split(",")));
long deptId = 0l;
if (!CollectionUtils.isEmpty(deptIds)) {
deptId = Long.valueOf(deptIds.get(0));
}
// 发起流程
String processInstanceId = "";
String errMsg = "";
Map<String, Object> returnMap = new HashMap<>();
StartProcessInstanceRequest startProcessInstanceRequest = new StartProcessInstanceRequest()
.setOriginatorUserId(dingtalkUserId)
.setDeptId(deptId)
.setProcessCode(processCode)
.setFormComponentValues(formComponentValues);
Gson gson = new Gson();
logger.info("发起流程实例的请求对象:" + gson.toJson(formComponentValues)+"-"+dingtalkUserId+"-"+deptId+"-"+processCode);
try {
StartProcessInstanceResponse response = client.startProcessInstanceWithOptions(startProcessInstanceRequest,
startProcessInstanceHeaders, new RuntimeOptions());
if (!ObjectUtils.isEmpty(response)) {
processInstanceId = response.getBody().getInstanceId();
}
} catch (TeaException err) {
logger.info("发起流程调用钉钉接口异常信息对象:" + gson.toJson(err));
if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
logger.info("发起流程调用钉钉接口返回错误信息:" + ExceptionUtils.getStackTrace(err));
returnMap.put("errMsg", err.message);
return returnMap;
}
} catch (Exception _err) {
logger.info("发起流程调用钉钉接口异常信息对象:" + gson.toJson(_err));
TeaException err = new TeaException(_err.getMessage(), _err);
if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
returnMap.put("errMsg", err.message);
return returnMap;
}
}
// 如果启动流程实例成功,返回流程实例id
if (StringUtils.isNotBlank(processInstanceId)) {
returnMap.put("processInstanceId", processInstanceId);
}
return returnMap;
}
4. 查询流程审核日志
预测审批流程节点信息 (需要用到前文提到的钉钉流程模板 code)
java
/**
* 审批流程预测
* @param processCode
* @param userId
* @param deptId
* @param accessToken
* @return
* @throws Exception
*/
public static Map<String, Object> forecastProcesses(String processCode, String userId, Integer deptId, String accessToken, List<ProcessForecastRequest.ProcessForecastRequestFormComponentValues> formComponentValues) throws Exception {
Map<String, Object> returnMap = new HashMap<>();
com.aliyun.dingtalkworkflow_1_0.Client client = createClient();
com.aliyun.dingtalkworkflow_1_0.models.ProcessForecastHeaders processForecastHeaders
= new com.aliyun.dingtalkworkflow_1_0.models.ProcessForecastHeaders();
processForecastHeaders.xAcsDingtalkAccessToken = accessToken;
com.aliyun.dingtalkworkflow_1_0.models.ProcessForecastRequest processForecastRequest
= new com.aliyun.dingtalkworkflow_1_0.models.ProcessForecastRequest()
.setDeptId(deptId)
.setUserId(userId)
.setProcessCode(processCode)
.setFormComponentValues(formComponentValues);
try {
ProcessForecastResponse response = client.processForecastWithOptions(processForecastRequest, processForecastHeaders, new com.aliyun.teautil.models.RuntimeOptions());
if (!ObjectUtils.isEmpty(response) && response.getBody().getResult().isForecastSuccess) {
// 审批节点
List<ProcessForecastResponseBody.ProcessForecastResponseBodyResultWorkflowActivityRules> activityRules = response.getBody().getResult().getWorkflowActivityRules();
if (!CollectionUtils.isEmpty(activityRules)) {
activityRules.forEach(rule -> {
returnMap.put(rule.getActivityId(), rule.getActivityName());
});
}
}
} catch (TeaException err) {
if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
// err 中含有 code 和 message 属性,可帮助开发定位问题
logger.info("调用钉钉流程审批预测接口-错误信息:" + err.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 属性,可帮助开发定位问题
logger.info("调用钉钉流程审批预测接口-错误信息:" + err.message);
}
}
return returnMap;
}
查询审核流程的各个节点的审核结果
java
/**
* 查询单个钉钉流程实例的 审核状态 审核结果
* @param instanceId
* @param accessToken
* @return
* @throws Exception
*/
public static Map<String, Object> processInstanceStatusAndResult(String instanceId, String accessToken) throws Exception {
Map<String, Object> returnMap = new HashMap<>();
com.aliyun.dingtalkworkflow_1_0.Client client = createClient();
com.aliyun.dingtalkworkflow_1_0.models.GetProcessInstanceHeaders getProcessInstanceHeaders
= new com.aliyun.dingtalkworkflow_1_0.models.GetProcessInstanceHeaders();
getProcessInstanceHeaders.xAcsDingtalkAccessToken = accessToken;
com.aliyun.dingtalkworkflow_1_0.models.GetProcessInstanceRequest getProcessInstanceRequest
= new com.aliyun.dingtalkworkflow_1_0.models.GetProcessInstanceRequest()
.setProcessInstanceId(instanceId);
try {
GetProcessInstanceResponse response = client.getProcessInstanceWithOptions(getProcessInstanceRequest, getProcessInstanceHeaders, new com.aliyun.teautil.models.RuntimeOptions());
if (!ObjectUtils.isEmpty(response) && "true".equals(response.getBody().getSuccess())) {
// 审批状态
String status = response.getBody().getResult().getStatus();
// 审批结果
String approvalResult = response.getBody().getResult().getResult();
if (StringUtils.isNotBlank(approvalResult)) {
returnMap.put("approvalResult", approvalResult);
}
if (StringUtils.isNotBlank(status)) {
returnMap.put("status", status);
}
// 查询历史审核节点和当前的审核节点
List<GetProcessInstanceResponseBody.GetProcessInstanceResponseBodyResultTasks> tasks = response.getBody().getResult().getTasks();
if (!CollectionUtils.isEmpty(tasks)) {
returnMap.put("tasks", tasks);
}
// 审核日志 评论
List<GetProcessInstanceResponseBody.GetProcessInstanceResponseBodyResultOperationRecords> records = response.getBody().getResult().getOperationRecords();
if (!CollectionUtils.isEmpty(records)) {
returnMap.put("operationRecords", records);
}
}
} catch (TeaException err) {
if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
// err 中含有 code 和 message 属性,可帮助开发定位问题
logger.info("调用钉钉查询单个流程的审批实例详情接口-错误信息:" + err.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 属性,可帮助开发定位问题
logger.info("调用钉钉查询单个流程的审批实例详情接口-错误信息:" + err.message);
}
}
return returnMap;
}
5. 流程审核同意回调接口
java
/**
* 流程 审批完成后 回调更新 审核进度、审核结果
* 正常审核完成 同意
* @return
*/
@RequestMapping(value = "dingCallback", method = RequestMethod.POST, produces = {"application/json;charset=utf-8"})
public Result<Object> dingCallback(@RequestBody DingCallbackUpdateReqVO updateReqVO) {
// 校验业务主键对应的记录是否存在
if (!ObjectUtils.isEmpty(updateReqVO.getId())) {
// 更新业务单据的审核状态
......
}
return new Result<>(ResultStatusEnum.SUCCESS);
}
6. 流程审核拒绝回调接口
java
/**
* 流程 审批完成后 回调更新记录的审核进度、审核结果
* 流程被拒绝
* * @return
*/
@RequestMapping(value = "dingRefuseCallback", method = RequestMethod.POST, produces = {"application/json;charset=utf-8"})
public Result<Object> dingRefuseCallback(@RequestBody DingCallbackUpdateReqVO updateReqVO) {
// 校验业务主键对应的记录是否存在
if (!ObjectUtils.isEmpty(updateReqVO.getId())) {
// 更新业务单据的审核状态
......
}
return new Result<>(ResultStatusEnum.SUCCESS);
}
7. 钉钉免登录接口
https://sso.abc.com/dd/login?code=81168e62b8a53a1ab2d6c4dff0d2e26f
code 前端通过钉钉JSAPI获得的临时访问码
后台结合业务系统的token校验机制,匹配用户的钉钉信息,如果匹配成功,则视为合法用户,返回token
返回参数示例
javascript
{
"code": 200,
"data": {
"password_status": "N",
"user_id": "12345",
"user_info": "mary",
"dingtalkUnionId": "lkgPd1UY5c6dN7EZ9CTdEgiEiE",
"dingtalkUserId": "123456",
"token": "1d9862bcdb7d345f55ed08673388bb84",
"user_no": "456789",
"dingtalkDeptIds": [
1289839283
]
},
"message": "success"
}
8. 检验token合法性接口
https://sso.abc.com/api/checkToken?token=d344a66aad475f473933ac7edcfce589
token 当前用户存储在 localStorage 中的token
后台结合业务系统的token校验机制判断token是否合法,如果合法,正常访问系统API,如果不合法,引导用户重新登录。
接口返回参数示例
javascript
{
"code": 200,
"userno": "123456",
"name": "mary",
"id": "456123",
"tenant": "abc",
"email": "abc@qq.com",
"token": "7ee22693d72a0f4aa0d1ad041c0dccac"
}