如果要实现一个如下图所示的 审批 流程,如果是我们自己手动实现该如何做?
首先说下支持的功能需求:
- 审批人员可以是无限数量的
- 只能是一人一级一级的审批,不能多个人在同一级审批
创建审批单
注意:这个审批单仅仅是一个通用的承载审批流程的数据,并不是业务上的审批单(如:请假单,里面包含请假的数据),关于这种审批的数据,需要自己根据业务需求来关联这里的 审批单
审批流程定义数据结构:
@Data
@ToString
public class ApprovalProcessDefine {
/**
* 数据类型
*/
private ApprovalDataType dataType;
/**
* 目前仅支持按系统账户
*/
private List<Integer> accounts;
}
- 数据类型:这个审批单可以支持很多中数据类型,比如请假、报销 之类的
- accounts:这里定义为一个 list,那么整个审批流程就是按照这个顺序 一级一级的审批
这里实现是从配置文件读取的,你完全可以做成可配置的,或则数据更丰富的。
审批流程节点:
@Data
@ToString
public class ApprovalProcessNode {
// 节点顺序,等同于数组的索引+1
private Integer node;
// 该节点需要这个指定的 账户才能审批
private Integer accountId;
// 账户名称
private String accountName;
}
- 账户信息:这个好理解,就是展示当前是哪一个人进行审批的信息
- node:这个简单说,就是当前这个审批人是在第几个 节点 进行审批,他可以用来计算下一个节点该谁审批
审批单实体定义:
@Table(name = "`approval`")
public class Approval {
/**
* 创建人ID
*/
@Column(name = "created_by_id")
private Integer createdById;
/**
* 创建人
*/
@Column(name = "created_by")
private String createdBy;
/**
* 创建时间
*/
@Column(name = "created_time")
private Date createdTime;
/**
* 更新人ID
*/
@Column(name = "updated_by_id")
private Integer updatedById;
/**
* 更新人
*/
@Column(name = "updated_by")
private String updatedBy;
/**
* 更新时间
*/
@Column(name = "updated_time")
private Date updatedTime;
/**
* 数据类型,请自定义
*/
@Column(name = "data_type")
private Byte dataType;
/**
* 根据类型来的 id
*/
@Column(name = "data_id")
private Integer dataId;
/**
* 审核状态: 0 刚创建,1 审批中,2 拒绝,3通过
*/
private Byte status;
/**
* 状态改变时间
*/
@Column(name = "status_change_time")
private Date statusChangeTime;
/**
* 节点审批状态
*/
@Column(name = "node_status")
private Byte nodeStatus;
/**
* 节点状态改变时间
*/
@Column(name = "node_status_change_time")
private Date nodeStatusChangeTime;
/**
* 当前待审批人ID;表示当前需要该账户进行审批
*/
@Column(name = "pending_by_id")
private Integer pendingById;
/**
* 审批人
*/
@Column(name = "pending_by")
private String pendingBy;
/**
* 审批流程经过的最大节点数量,即需要审批多少次才算最终通过,只支持单节点审批
*/
@Column(name = "process_node_max")
private Integer processNodeMax;
/**
* 审批流程已经经过的节点数量,也就是当前节点是多少,满足 max=current 则审批通过
*/
@Column(name = "process_node_current")
private Integer processNodeCurrent;
/**
* 是否删除
*/
@Column(name = "is_deleted")
private Boolean isDeleted;
/**
* 审批流程定义,当前审批需要哪些账户参与
*/
@Column(name = "process_define_json")
private String processDefineJson;
/**
* 审批额外字段
*/
private String extend;
}
创建审批单:
public Long create(ApprovalDataType dataType, Integer dataId, String extend, UserInfo userInfo) {
/*
1. 根据数据类型,获取该审批单的审批人员定义
2. 根据定义构建审批进度节点
3. 创建审批单
4. 发送审批单创建事件
*/
// 获得这个审批单的 审批流程定义
final ApprovalProcessDefine processDefine = approvalConfig.getProcessDefine(dataType);
// 转换为人员审批节点
final List<ApprovalProcessNode> processNodes = buildProcessNodes(processDefine.getAccounts());
if (CollectionUtil.isEmpty(processNodes)) {
throwErr(StrUtil.format("提供的审批流程定义为空"));
}
// 创建审批单相关信息
final Approval record = new Approval();
record.setDataType(dataType.getValue());
record.setDataId(dataId);
record.setStatus(ApprovalStatus.APPROVAL_ING.getValue());
final Date now = new Date();
record.setStatusChangeTime(now);
record.setNodeStatus(ApprovalStatus.APPROVAL_ING.getValue());
record.setNodeStatusChangeTime(now);
// 获取第一个审批人信息, 转换的 人员审批节点 第一个就是需要该人员审批
final ApprovalProcessNode processNode = processNodes.get(0);
record.setPendingBy(processNode.getAccountName());
record.setPendingById(processNode.getAccountId());
record.setProcessDefineJson(JSON.toJSONString(processNodes));
record.setProcessNodeCurrent(processNode.getNode()); // 当前审批节点顺序
record.setProcessNodeMax(processNodes.size()); // 计算最大审批节点数量
record.setCreatedBy(userInfo.getName());
record.setCreatedById(userInfo.getId());
record.setCreatedTime(now);
approvalMapper.insertSelective(record);
final Long id = record.getId();
approvalEventPublisher.publishCreateEvent(id, dataType, dataId);
return id;
}
构建审批人员节点:
/**
* 构建审批流程节点,并且按照给定的顺序返回
*
* @param accountIds
* @return
*/
private List<ApprovalProcessNode> buildProcessNodes(List<Integer> accountIds) {
if (CollectionUtil.isEmpty(accountIds)) {
return null;
}
// 从账户信息中获取 账户名称
final List<Account> accounts = accountService.listById(accountIds);
// 结果和查询的不一致
if (accountIds.size() != accounts.size()) {
final List<Integer> tempAccountIds = new ArrayList<>(accountIds);
final List<Integer> targetAccountIds = accounts.stream().map(Account::getId).collect(Collectors.toList());
tempAccountIds.removeAll(targetAccountIds);
throwErr("审批流程中的审批人已经不存在:" + tempAccountIds);
}
final Map<Integer, Account> accountMap = accounts.stream().collect(Collectors.toMap(Account::getId, Function.identity()));
List<ApprovalProcessNode> nodes = new ArrayList<>(accountIds.size());
for (int i = 0; i < accountIds.size(); i++) {
ApprovalProcessNode node = new ApprovalProcessNode();
node.setNode(i + 1); // 重要的是这里的 node 是 索引 +1
final Account account = accountMap.get(accountIds.get(i));
node.setAccountId(account.getId());
node.setAccountName(account.getName());
nodes.add(node);
}
return nodes;
}
审批单审核操作
审批单审核核心需要做的事情就是:判定当次审批结果,根据审批结果决定是 审批结束 还是 下一个审批人继续审批?
审批人意见:
@Data
@ToString
@NoArgsConstructor
public class ApprovalOpinion {
/**
* 审批人
*/
private Integer pendingById;
/**
* 审批人
*/
private String pendingBy;
/**
* 审批是否通过
*/
private Boolean pass;
/**
* 审批意见
*/
private String opinion;
public ApprovalOpinion(Integer pendingById, String pendingBy, Boolean pass, String opinion) {
this.pendingById = pendingById;
this.pendingBy = pendingBy;
this.pass = pass;
this.opinion = opinion;
}
}
审批核心操作:
/**
* @param approval 当前审批单
* @param approvalOpinion 审批人与审批意见
* @param finish 当前的审批是否已经完成(中间节点的审批不会回调),如果有值则根据状态判定 整个审批流程 是通过还是拒绝
* java.util.function.Consumer
*/
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void doApproval(Approval approval, ApprovalOpinion approvalOpinion, Consumer<ApprovalStatus> finish) {
ApprovalStatus approvalStatus = approvalOpinion.getPass() ? ApprovalStatus.APPROVAL_PASS : ApprovalStatus.APPROVAL_REJECT;
final Long approvalId = approval.getId();
// 当次审核通过
if (ApprovalStatus.APPROVAL_PASS == approvalStatus) {
final Approval record = new Approval();
record.setId(approvalId);
record.setNodeStatus(approvalStatus.getValue());
record.setNodeStatusChangeTime(new Date());
// 需要判断当前审批是否已经完成,如果已经完成,则整个审批流程已经完成
// 如果整个审批流程还未结束,则计算下一个审批节点的相关信息
final Integer processNodeMax = approval.getProcessNodeMax();
final Integer processNodeCurrent = approval.getProcessNodeCurrent();
// 审批单当前的审批顺序 + 1 如果 小于等于 流程的最大节点,则说明还有下一个审批人
if (processNodeCurrent + 1 <= processNodeMax) {
// 还未结束,需要下一个审批节点
// 解析流程定义,流程定义从创建的时候,就需要存储在审批单上,因为一般 流程定义都是可以修改的
// 但是修改流程定义,不能影响已经正在使用的审批
final List<ApprovalProcessNode> approvalProcessNodes = parseProcessDefineJson(approval.getProcessDefineJson());
// 由于这个 node 记录的是索引 +1,所以这里直接获取,就是下一个审批人节点信息
final ApprovalProcessNode nextApprovalProcessNode = approvalProcessNodes.get(processNodeCurrent);
// 换下一个审批人
record.setProcessNodeCurrent(nextApprovalProcessNode.getNode());
record.setPendingById(nextApprovalProcessNode.getAccountId());
record.setPendingBy(nextApprovalProcessNode.getAccountName());
// 换下一个人,要重置审批结果为待审批
record.setNodeStatus(ApprovalStatus.APPROVAL_ING.getValue());
approvalMapper.updateByPrimaryKeySelective(record);
// 添加审批记录操作,每个审批人 操作一次,就会有一条审批记录
approvalRecordService.add(approvalId, processNodeCurrent, approvalOpinion);
// 发送审批事件:业务方,可以通过监听此事件,完成 发送邮件 或则 订单消息之类的通知到当前审批人
/**
* @param approvalId 审批单 ID
* @param dataType 数据类型
* @param dataId 数据类型原始 ID
* @param isFinall 是否是最终结果
* @param approvalStatus 最终结果才会有值
*/
approvalEventPublisher.publishApprovalEvent(approvalId, ApprovalDataType.valueOf(approval.getDataType()), approval.getDataId(), false, null);
} else {
// 如果已经结束,则整个审批流程通过
record.setStatus(approvalStatus.getValue());
record.setStatusChangeTime(new Date());
approvalMapper.updateByPrimaryKeySelective(record);
// 整个审批流程结束,则回调业务方,业务方可以更改自己的数据
finish.accept(approvalStatus);
approvalEventPublisher.publishApprovalEvent(approvalId, ApprovalDataType.valueOf(approval.getDataType()), approval.getDataId(), true, approvalStatus);
approvalRecordService.add(approvalId, processNodeCurrent, approvalOpinion);
}
} else {
// 被拒绝:则整个流程结束
final Approval record = new Approval();
record.setId(approvalId);
record.setNodeStatusChangeTime(new Date());
record.setNodeStatus(approvalStatus.getValue());
record.setStatus(approvalStatus.getValue());
record.setStatusChangeTime(new Date());
approvalMapper.updateByPrimaryKeySelective(record);
finish.accept(approvalStatus);
approvalRecordService.add(approvalId, approval.getProcessNodeCurrent(), approvalOpinion);
approvalEventPublisher.publishApprovalEvent(approvalId, ApprovalDataType.valueOf(approval.getDataType()), approval.getDataId(), true, approvalStatus);
}
}
业务方调用审核操作示例
@ConditionalOnBean
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void approval(Integer id, PerformanceApprovalRequest params, UserInfo userInfo) {
/*
审批要做的事情
1. 检查状态
2. 检查当前操作人是否是 审批者
3. 处理
*/
final Performance performance = getId(id);
final Long approvalId = performance.getApprovalId();
final Approval approval = approvalService.getById(approvalId);
if (approval == null) {
throwErr("该审批已失效");
}
final ApprovalStatus status = ApprovalStatus.valueOf(approval.getStatus());
if (ApprovalStatus.APPROVAL_ING != status) {
throwErr("状态异常:当前审批状态不在审批中");
}
final Integer pendingById = approval.getPendingById();
if (pendingById != userInfo.getId()) {
throwErr(StrUtil.format("审批人异常:您不是当前审批人,当前审批人为 {} [id={}]", approval.getPendingBy(), approval.getPendingById()));
}
// 处理审批
final ApprovalOpinion approvalOpinion = new ApprovalOpinion(userInfo.getId(), userInfo.getName(), params.getIsPass(), params.getOpinion());
approvalService.doApproval(approval,
approvalOpinion,
finishStatus -> {
// 审批有最终结果,就改变业务审批单的状态
if (ApprovalStatus.APPROVAL_PASS == finishStatus) {
this.changeApprovalStatus(id, finishStatus, userInfo);
} else {
this.changeApprovalStatus(id, finishStatus, userInfo);
}
});
}
如何获取 UI 上的审批流程数据?
上面有了 审批单信息、审批操作处理,那么如何构建 UI 展示的数据信息呢?这里其实也是一个小难点
/**
* 转换审批单流程节点信息
*
* @param approval
* @return
*/
private List<ApprovalProcessNodeDetail> convertProcessNodeDetail(Approval approval) {
// 从审批单获取流程定义
final String processDefineJson = approval.getProcessDefineJson();
final List<ApprovalProcessNode> approvalProcessNodes = parseProcessDefineJson(processDefineJson);
final int processNodeCurrent = approval.getProcessNodeCurrent();
// 获取到审批记录,填充审批记录信息
List<ApprovalRecord> records = approvalRecordService.listByApprovalId(approval.getId());
final Map<Integer, ApprovalRecord> recordMap = records.stream().collect(Collectors.toMap(ApprovalRecord::getProcessNodeCurrent, Function.identity()));
List<ApprovalProcessNodeDetail> list = new ArrayList<>(approvalProcessNodes.size());
// 循环审批流程定义,然后填充 审批记录信息:比如审批意见
for (ApprovalProcessNode approvalProcessNode : approvalProcessNodes) {
final Integer node = approvalProcessNode.getNode();
final ApprovalProcessNodeDetail nodeDetail = new ApprovalProcessNodeDetail();
nodeDetail.setNode(node);
nodeDetail.setAccountId(approvalProcessNode.getAccountId());
nodeDetail.setAccountName(approvalProcessNode.getAccountName());
final ApprovalRecord record = recordMap.get(node);
// 如果已经有对应的审批记录,则使用审批记录中的相关信息
// 如果没有,留空即可
if (record != null) {
nodeDetail.setStatus(record.getNodeStatus());
nodeDetail.setStatusChangeTime(record.getNodeStatusChangeTime());
nodeDetail.setAccountName(record.getPendingBy());
nodeDetail.setOpinion(record.getOpinion());
list.add(nodeDetail);
continue;
}
// 已通过,因为没有通过的话,就不会继续到下一审批人了
if (node < processNodeCurrent) {
nodeDetail.setStatus(ApprovalStatus.APPROVAL_PASS.getValue());
} else if (node == processNodeCurrent) {
// 如果是当前节点,就直接复制当前状态上的
nodeDetail.setStatus(approval.getNodeStatus());
}
list.add(nodeDetail);
}
return list;
}
审批流程节点详细信息:
/**
* 审批流程节点详细:可以包含谁审核的,审核时间,原因等
*
* @author zhuqiang
* @date 2021/7/23 10:23
*/
@Data
@ToString
public class ApprovalProcessNodeDetail {
// 节点顺序, 数组索引 +1
private Integer node;
// 该节点需要这个指定的 账户才能审批
private Integer accountId;
// 账户名称
private String accountName;
/**
* 审批状态
*/
private Byte status;
/**
* 状态时间
*/
private Date statusChangeTime;
/**
* 审批意见
*/
private String opinion;
}
上面的 list 审批流程节点数据,就能实现下面这个 UI 了