如果要实现一个如下图所示的 审批 流程,如果是我们自己手动实现该如何做?
首先说下支持的功能需求:
- 审批人员可以是无限数量的
- 只能是一人一级一级的审批,不能多个人在同一级审批
创建审批单
注意:这个审批单仅仅是一个通用的承载审批流程的数据,并不是业务上的审批单(如:请假单,里面包含请假的数据),关于这种审批的数据,需要自己根据业务需求来关联这里的 审批单
审批流程定义数据结构:
@Data@ToStringpublic class ApprovalProcessDefine {/*** 数据类型*/private ApprovalDataType dataType;/*** 目前仅支持按系统账户*/private List<Integer> accounts;}
- 数据类型:这个审批单可以支持很多中数据类型,比如请假、报销 之类的
- accounts:这里定义为一个 list,那么整个审批流程就是按照这个顺序 一级一级的审批
这里实现是从配置文件读取的,你完全可以做成可配置的,或则数据更丰富的。
审批流程节点:
@Data@ToStringpublic class ApprovalProcessNode {// 节点顺序,等同于数组的索引+1private 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 是 索引 +1final Account account = accountMap.get(accountIds.get(i));node.setAccountId(account.getId());node.setAccountName(account.getName());nodes.add(node);}return nodes;}
审批单审核操作
审批单审核核心需要做的事情就是:判定当次审批结果,根据审批结果决定是 审批结束 还是 下一个审批人继续审批?
审批人意见:
@Data@ToString@NoArgsConstructorpublic 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@ToStringpublic class ApprovalProcessNodeDetail {// 节点顺序, 数组索引 +1private Integer node;// 该节点需要这个指定的 账户才能审批private Integer accountId;// 账户名称private String accountName;/*** 审批状态*/private Byte status;/*** 状态时间*/private Date statusChangeTime;/*** 审批意见*/private String opinion;}
上面的 list 审批流程节点数据,就能实现下面这个 UI 了
