一、前言
销售同学在日常作业中,会有一些客户归属问题、帮客户申请汇率、费率、优惠券等。
这些都需要经过审批,同时会根据这个销售提供资料以及客户过往入账情况走不同的审批流、审批人。
既然会有那么多工单,且会在不同应用中发起和处理结果,那么我们可以统一封装:
- 封装发起审批工单模版:抽象模版类,可以封装到SDK提供出去
- 封装处理回调数据模版:采用公共服务接收数据
- 接收了回调数据,再发送到 MQ中
- 需要用的服务均监听这个MQ的TAG,根据 processCode 再进行处理
那些年踩过的坑
如果找不到问题,得多看看文档:(求生之道)
OA工单一些名称解释:
| 名词 | 说明 |
|---|---|
| processCode | 工单对应的标识比如:PROC-0DB6F096-1442-42C6-925F-A9995A41701D |
| instanceId | 工单实例唯一标识:比如:8LOGI3g-S2W5xZrRea6OIQ04211729327498 |
(1)发起审批实例:字段问题
发起审批失败:大部分都是字段问题,会有如下问题:
- 字段数据为必填,但没填
- 钉钉会报错误:TextField_1DW9UVRW9RHC0 值错误
- 字段名对不上:
- 因为字段名是HR(管理员)来配置,你不知道她咋配置的。
- 名字前面多个空格,或者后面多个空格
- 字段数据类型对不上:
- 不同的数据类型,需要转化不同的值:JSON格式、列表格式等
- 数据格式有问题:字符型、数字型
当然线上出现问题怎么办?不要慌,解决三步骤 :
- 当然根据钉钉返回的报错信息:是哪种类别错误
- 通过 prorcessCode 和 接口查询:对应表单数据长啥样
- 模拟接口调用
- 根据钉钉返回的数据,匹配接口查询
针对以上四种问题,汇总如下解决方法:
| 问题 | 解决 |
|---|---|
| 字段数据为必填项 | 可以先去钉钉找工单,必填项前面是带 ***** 。 |
| 字段名对不上 | 空格问题,很痛苦。通过接口查字段名,然后复制出来。接口:api.dingtalk.com/v1.0/workfl… |
| 字段数据类型对不上 | 看这个文档:open.dingtalk.com/document/is…,多选框/图片/日期 |
| 数据格式有问题 | 同样可以看表单如何定义的。钉钉报错中也能提现出来。 |
POST /v1.0/workflow/processInstances HTTP/1.1
Host:api.dingtalk.com
x-acs-dingtalk-access-token:String
Content-Type:application/json
{
"originatorUserId" : "String",
"processCode" : "String",
"deptId" : Long,
"microappAgentId" : Long,
"approvers" : [ {
"actionType" : "String",
"userIds" : [ "String" ]
} ],
"ccList" : [ "String" ],
"ccPosition" : "String",
"targetSelectActioners" : [ {
"actionerKey" : "String",
"actionerUserIds" : [ "String" ]
} ],
"formComponentValues" : [ {
"name" : "String",
"value" : "String"
} ],
"RequestId" : "String"
}
POST /v1.0/workflow/processInstances HTTP/1.1
Host:api.dingtalk.com
Content-Type:application/json
{
/*
该接口当前尚未支持审批应用中的所有控件,以以下列出示例的控件为准。
基本的控件数据在传递时只需要填写 name 和 value 属性即可,两者都是字符串格式。
如果数据是 json 格式,也需要先转义为字符串格式。
*/
"processCode": "PROC-17428B8C-6C60-xxxx-924C-64F1037AE067",
"originatorUserId": "26652461xxxx5992",
"deptId": 1,
"microappAgentId": 1234,
"formComponentValues":[
{
/*
value 需要将实际的 url 组成的数组转义为字符串,即使只有一个
选项也需要是数组形式
*/
"name": "图片",
"value": "["http://url1","http://url2","http://url3"]"
}
],
"targetSelectActioners":[
{
"actionerKey": "manual_f953_8c70_xxxx_7ffa",
"actionerUserIds": ["26652461xxxx5992", "011220460xxxx8765"]
}
],
"approvers": [
{
"actionType": "AND",
"userIds": ["26652461xxxx5992", "011220460xxxx8765"]
}
],
"ccList": ["25054456xxxx0123"],
"ccPosition": "START",
"RequestId" : "4F73A5B6-81E5-1556-BC35-C2912C84993D"
}
(2)撤回工单
之前以为工单状态只有:同意、拒绝。
没想到发起后还能撤回。
发起完工单后,可以撤回:本人可以操作
钉钉审批流回调的状态有:
| 状态 | 说明 |
|---|---|
| 同意 | 审批通过 |
| 拒绝 | 审批拒绝 |
| 终止 | 撤回 |
(3)上传附件的坑
首当其冲就是:应用的各种文件读写权限得开启。
- 钉钉空间磁盘权限
- 文件读写权限
- 上传指定人:得有权限那个人,目前都是直接选择管理员(HR)
开发过程可以按照这个文档:open.dingtalk.com/document/or…
- 吐槽下:写得真烂,绕太多了。
二、封装通用工具
主要流程有:
- 发起审批实例
- 接受钉钉回调
- 上传附件到钉钉
public interface OAWorkService {
AttachmentDto uploadAttachment(MultipartFile file);
String createApproveFlow(ProcessData processData);
}
@Slf4j
@Service
@RequiredArgsConstructor
public class OAWorkServiceImpl implements OAWorkService {
private final DingClient dingClient;
private final AuthClient authClient;
@Value("${upload.user.email:xxx}")
private String uploadUserEmail;
/**
* 上传附件
* @param file 文件
* @return 内容
*/
@Override
public AttachmentDto uploadAttachment(MultipartFile file) {
User user = this.authClient.getDingUser("xxx");
Assert.warnIsTrue(!Objects.isNull(user), "用户不存在");
CommitFileResponse fileUploadInfo = dingClient.uploadFile(ppUserV1.getDingdingUserId(), file);
Assert.warnIsTrue(Objects.nonNull(fileUploadInfo), "上传失败,请重试");
Assert.warnIsTrue(Objects.nonNull(fileUploadInfo.getBody()), "上传失败,请重试");
Assert.warnIsTrue(Objects.nonNull(fileUploadInfo.getBody().getDentry()), "上传失败,请重试");
CommitFileResponseBody.CommitFileResponseBodyDentry dentry = fileUploadInfo.getBody().getDentry();
return AttachmentDto.builder().fileId(dentry.getId()).fileName(dentry.getName())
.fileSize(dentry.getSize()).spaceId(dentry.getSpaceId()).build();
}
@Override
public String createApproveFlow(ProcessData processData) {
Assert.warnIsTrue(Objects.nonNull(processData), "流程数据不能为空");
Assert.warnIsTrue(StringUtils.isNotBlank(processData.getUserId()), "userId不能为空");
Assert.warnIsTrue(StringUtils.isNotBlank(processData.getProcessCode()), "processCode不能为空");
Assert.warnIsTrue(CollectionUtils.isNotEmpty(processData.getProcessFormDataList()), "流程表单不能为空");
OrgEntity userDept = this.authClient.getUserDept(processData.getUserId());
Assert.warnIsTrue(!Objects.isNull(userDept), "用户部门不存在");
PPUserV1 ppUserV1 = this.authClient.getDingUser(processData.getUserId());
Assert.warnIsTrue(!Objects.isNull(ppUserV1), "用户不存在");
List<OapiProcessinstanceCreateRequest.FormComponentValueVo> formList = processData.getProcessFormDataList()
.stream().map(this::buildFormList).collect(Collectors.toList());
String processInstanceId = dingClient.createProcessInstance(processData.getProcessCode(), formList,
ppUserV1.getDingdingUserId(), userDept.getOrgId());
Assert.warnIsTrue(StringUtils.isNotBlank(processInstanceId), "创建流程失败,请重试");
return processInstanceId;
}
private OapiProcessinstanceCreateRequest.FormComponentValueVo buildFormList(ProcessFormData processFormData) {
OapiProcessinstanceCreateRequest.FormComponentValueVo formComponentValueVo
= new OapiProcessinstanceCreateRequest.FormComponentValueVo();
formComponentValueVo.setName(processFormData.getName());
formComponentValueVo.setValue(processFormData.getValue());
formComponentValueVo.setExtValue(processFormData.getExtValue());
return formComponentValueVo;
}
}
@Slf4j
public abstract class ApproveFlowTemplate<T> {
@Resource
private OAWorkService oaWorkService;
protected abstract String getProcessCode();
protected abstract ProcessData buildApproveFlowInfo(String userId, T content);
/**
* 创建审批流
*
* @param userId 用户Id,操作人
* @param content 内容
* @return 实例Id
*/
public String createApproveFlow(String userId, T content) {
ProcessData processData = buildApproveFlowInfo(userId, content);
return doCreateApproveFlow(userId, processData);
}
protected String doCreateApproveFlow(String userId, ProcessData processData) {
String processCode = getProcessCode();
processData.setProcessCode(processCode);
processData.setUserId(userId);
return oaWorkService.createApproveFlow(processData);
}
protected ProcessFormData buildAttachmentFormData(List<AttachmentDto> attachments) {
if (CollectionUtils.isEmpty(attachments)) {
return new ProcessFormData("附件", "");
}
List<AttachmentData> attachmentDataList = attachments.stream()
.map(this::buildAttachmentData).collect(Collectors.toList());
String value = JSON.toJSONString(attachmentDataList);
return new ProcessFormData("附件", value);
}
protected AttachmentData buildAttachmentData(AttachmentDto attachment) {
AttachmentData attachmentData = new AttachmentData();
attachmentData.setFileId(attachment.getFileId());
attachmentData.setFileName(attachment.getFileName());
attachmentData.setFileSize(attachment.getFileSize());
attachmentData.setFileType("FILE");
attachmentData.setSpaceId(attachment.getSpaceId());
return attachmentData;
}
}