如果要实现一个如下图所示的 审批 流程,如果是我们自己手动实现该如何做?
image.png
首先说下支持的功能需求:

  1. 审批人员可以是无限数量的
  2. 只能是一人一级一级的审批,不能多个人在同一级审批

下面来说说核心代码

创建审批单

注意:这个审批单仅仅是一个通用的承载审批流程的数据,并不是业务上的审批单(如:请假单,里面包含请假的数据),关于这种审批的数据,需要自己根据业务需求来关联这里的 审批单

审批流程定义数据结构:

  1. @Data
  2. @ToString
  3. public class ApprovalProcessDefine {
  4. /**
  5. * 数据类型
  6. */
  7. private ApprovalDataType dataType;
  8. /**
  9. * 目前仅支持按系统账户
  10. */
  11. private List<Integer> accounts;
  12. }
  • 数据类型:这个审批单可以支持很多中数据类型,比如请假、报销 之类的
  • accounts:这里定义为一个 list,那么整个审批流程就是按照这个顺序 一级一级的审批

这里实现是从配置文件读取的,你完全可以做成可配置的,或则数据更丰富的。

审批流程节点:

  1. @Data
  2. @ToString
  3. public class ApprovalProcessNode {
  4. // 节点顺序,等同于数组的索引+1
  5. private Integer node;
  6. // 该节点需要这个指定的 账户才能审批
  7. private Integer accountId;
  8. // 账户名称
  9. private String accountName;
  10. }
  • 账户信息:这个好理解,就是展示当前是哪一个人进行审批的信息
  • node:这个简单说,就是当前这个审批人是在第几个 节点 进行审批,他可以用来计算下一个节点该谁审批

审批单实体定义:

  1. @Table(name = "`approval`")
  2. public class Approval {
  3. /**
  4. * 创建人ID
  5. */
  6. @Column(name = "created_by_id")
  7. private Integer createdById;
  8. /**
  9. * 创建人
  10. */
  11. @Column(name = "created_by")
  12. private String createdBy;
  13. /**
  14. * 创建时间
  15. */
  16. @Column(name = "created_time")
  17. private Date createdTime;
  18. /**
  19. * 更新人ID
  20. */
  21. @Column(name = "updated_by_id")
  22. private Integer updatedById;
  23. /**
  24. * 更新人
  25. */
  26. @Column(name = "updated_by")
  27. private String updatedBy;
  28. /**
  29. * 更新时间
  30. */
  31. @Column(name = "updated_time")
  32. private Date updatedTime;
  33. /**
  34. * 数据类型,请自定义
  35. */
  36. @Column(name = "data_type")
  37. private Byte dataType;
  38. /**
  39. * 根据类型来的 id
  40. */
  41. @Column(name = "data_id")
  42. private Integer dataId;
  43. /**
  44. * 审核状态: 0 刚创建,1 审批中,2 拒绝,3通过
  45. */
  46. private Byte status;
  47. /**
  48. * 状态改变时间
  49. */
  50. @Column(name = "status_change_time")
  51. private Date statusChangeTime;
  52. /**
  53. * 节点审批状态
  54. */
  55. @Column(name = "node_status")
  56. private Byte nodeStatus;
  57. /**
  58. * 节点状态改变时间
  59. */
  60. @Column(name = "node_status_change_time")
  61. private Date nodeStatusChangeTime;
  62. /**
  63. * 当前待审批人ID;表示当前需要该账户进行审批
  64. */
  65. @Column(name = "pending_by_id")
  66. private Integer pendingById;
  67. /**
  68. * 审批人
  69. */
  70. @Column(name = "pending_by")
  71. private String pendingBy;
  72. /**
  73. * 审批流程经过的最大节点数量,即需要审批多少次才算最终通过,只支持单节点审批
  74. */
  75. @Column(name = "process_node_max")
  76. private Integer processNodeMax;
  77. /**
  78. * 审批流程已经经过的节点数量,也就是当前节点是多少,满足 max=current 则审批通过
  79. */
  80. @Column(name = "process_node_current")
  81. private Integer processNodeCurrent;
  82. /**
  83. * 是否删除
  84. */
  85. @Column(name = "is_deleted")
  86. private Boolean isDeleted;
  87. /**
  88. * 审批流程定义,当前审批需要哪些账户参与
  89. */
  90. @Column(name = "process_define_json")
  91. private String processDefineJson;
  92. /**
  93. * 审批额外字段
  94. */
  95. private String extend;
  96. }

创建审批单:

  1. public Long create(ApprovalDataType dataType, Integer dataId, String extend, UserInfo userInfo) {
  2. /*
  3. 1. 根据数据类型,获取该审批单的审批人员定义
  4. 2. 根据定义构建审批进度节点
  5. 3. 创建审批单
  6. 4. 发送审批单创建事件
  7. */
  8. // 获得这个审批单的 审批流程定义
  9. final ApprovalProcessDefine processDefine = approvalConfig.getProcessDefine(dataType);
  10. // 转换为人员审批节点
  11. final List<ApprovalProcessNode> processNodes = buildProcessNodes(processDefine.getAccounts());
  12. if (CollectionUtil.isEmpty(processNodes)) {
  13. throwErr(StrUtil.format("提供的审批流程定义为空"));
  14. }
  15. // 创建审批单相关信息
  16. final Approval record = new Approval();
  17. record.setDataType(dataType.getValue());
  18. record.setDataId(dataId);
  19. record.setStatus(ApprovalStatus.APPROVAL_ING.getValue());
  20. final Date now = new Date();
  21. record.setStatusChangeTime(now);
  22. record.setNodeStatus(ApprovalStatus.APPROVAL_ING.getValue());
  23. record.setNodeStatusChangeTime(now);
  24. // 获取第一个审批人信息, 转换的 人员审批节点 第一个就是需要该人员审批
  25. final ApprovalProcessNode processNode = processNodes.get(0);
  26. record.setPendingBy(processNode.getAccountName());
  27. record.setPendingById(processNode.getAccountId());
  28. record.setProcessDefineJson(JSON.toJSONString(processNodes));
  29. record.setProcessNodeCurrent(processNode.getNode()); // 当前审批节点顺序
  30. record.setProcessNodeMax(processNodes.size()); // 计算最大审批节点数量
  31. record.setCreatedBy(userInfo.getName());
  32. record.setCreatedById(userInfo.getId());
  33. record.setCreatedTime(now);
  34. approvalMapper.insertSelective(record);
  35. final Long id = record.getId();
  36. approvalEventPublisher.publishCreateEvent(id, dataType, dataId);
  37. return id;
  38. }

构建审批人员节点:

  1. /**
  2. * 构建审批流程节点,并且按照给定的顺序返回
  3. *
  4. * @param accountIds
  5. * @return
  6. */
  7. private List<ApprovalProcessNode> buildProcessNodes(List<Integer> accountIds) {
  8. if (CollectionUtil.isEmpty(accountIds)) {
  9. return null;
  10. }
  11. // 从账户信息中获取 账户名称
  12. final List<Account> accounts = accountService.listById(accountIds);
  13. // 结果和查询的不一致
  14. if (accountIds.size() != accounts.size()) {
  15. final List<Integer> tempAccountIds = new ArrayList<>(accountIds);
  16. final List<Integer> targetAccountIds = accounts.stream().map(Account::getId).collect(Collectors.toList());
  17. tempAccountIds.removeAll(targetAccountIds);
  18. throwErr("审批流程中的审批人已经不存在:" + tempAccountIds);
  19. }
  20. final Map<Integer, Account> accountMap = accounts.stream().collect(Collectors.toMap(Account::getId, Function.identity()));
  21. List<ApprovalProcessNode> nodes = new ArrayList<>(accountIds.size());
  22. for (int i = 0; i < accountIds.size(); i++) {
  23. ApprovalProcessNode node = new ApprovalProcessNode();
  24. node.setNode(i + 1); // 重要的是这里的 node 是 索引 +1
  25. final Account account = accountMap.get(accountIds.get(i));
  26. node.setAccountId(account.getId());
  27. node.setAccountName(account.getName());
  28. nodes.add(node);
  29. }
  30. return nodes;
  31. }

审批单审核操作

审批单审核核心需要做的事情就是:判定当次审批结果,根据审批结果决定是 审批结束 还是 下一个审批人继续审批
审批人意见:

  1. @Data
  2. @ToString
  3. @NoArgsConstructor
  4. public class ApprovalOpinion {
  5. /**
  6. * 审批人
  7. */
  8. private Integer pendingById;
  9. /**
  10. * 审批人
  11. */
  12. private String pendingBy;
  13. /**
  14. * 审批是否通过
  15. */
  16. private Boolean pass;
  17. /**
  18. * 审批意见
  19. */
  20. private String opinion;
  21. public ApprovalOpinion(Integer pendingById, String pendingBy, Boolean pass, String opinion) {
  22. this.pendingById = pendingById;
  23. this.pendingBy = pendingBy;
  24. this.pass = pass;
  25. this.opinion = opinion;
  26. }
  27. }

审批核心操作:

  1. /**
  2. * @param approval 当前审批单
  3. * @param approvalOpinion 审批人与审批意见
  4. * @param finish 当前的审批是否已经完成(中间节点的审批不会回调),如果有值则根据状态判定 整个审批流程 是通过还是拒绝
  5. * java.util.function.Consumer
  6. */
  7. @Override
  8. @Transactional(propagation = Propagation.REQUIRED)
  9. public void doApproval(Approval approval, ApprovalOpinion approvalOpinion, Consumer<ApprovalStatus> finish) {
  10. ApprovalStatus approvalStatus = approvalOpinion.getPass() ? ApprovalStatus.APPROVAL_PASS : ApprovalStatus.APPROVAL_REJECT;
  11. final Long approvalId = approval.getId();
  12. // 当次审核通过
  13. if (ApprovalStatus.APPROVAL_PASS == approvalStatus) {
  14. final Approval record = new Approval();
  15. record.setId(approvalId);
  16. record.setNodeStatus(approvalStatus.getValue());
  17. record.setNodeStatusChangeTime(new Date());
  18. // 需要判断当前审批是否已经完成,如果已经完成,则整个审批流程已经完成
  19. // 如果整个审批流程还未结束,则计算下一个审批节点的相关信息
  20. final Integer processNodeMax = approval.getProcessNodeMax();
  21. final Integer processNodeCurrent = approval.getProcessNodeCurrent();
  22. // 审批单当前的审批顺序 + 1 如果 小于等于 流程的最大节点,则说明还有下一个审批人
  23. if (processNodeCurrent + 1 <= processNodeMax) {
  24. // 还未结束,需要下一个审批节点
  25. // 解析流程定义,流程定义从创建的时候,就需要存储在审批单上,因为一般 流程定义都是可以修改的
  26. // 但是修改流程定义,不能影响已经正在使用的审批
  27. final List<ApprovalProcessNode> approvalProcessNodes = parseProcessDefineJson(approval.getProcessDefineJson());
  28. // 由于这个 node 记录的是索引 +1,所以这里直接获取,就是下一个审批人节点信息
  29. final ApprovalProcessNode nextApprovalProcessNode = approvalProcessNodes.get(processNodeCurrent);
  30. // 换下一个审批人
  31. record.setProcessNodeCurrent(nextApprovalProcessNode.getNode());
  32. record.setPendingById(nextApprovalProcessNode.getAccountId());
  33. record.setPendingBy(nextApprovalProcessNode.getAccountName());
  34. // 换下一个人,要重置审批结果为待审批
  35. record.setNodeStatus(ApprovalStatus.APPROVAL_ING.getValue());
  36. approvalMapper.updateByPrimaryKeySelective(record);
  37. // 添加审批记录操作,每个审批人 操作一次,就会有一条审批记录
  38. approvalRecordService.add(approvalId, processNodeCurrent, approvalOpinion);
  39. // 发送审批事件:业务方,可以通过监听此事件,完成 发送邮件 或则 订单消息之类的通知到当前审批人
  40. /**
  41. * @param approvalId 审批单 ID
  42. * @param dataType 数据类型
  43. * @param dataId 数据类型原始 ID
  44. * @param isFinall 是否是最终结果
  45. * @param approvalStatus 最终结果才会有值
  46. */
  47. approvalEventPublisher.publishApprovalEvent(approvalId, ApprovalDataType.valueOf(approval.getDataType()), approval.getDataId(), false, null);
  48. } else {
  49. // 如果已经结束,则整个审批流程通过
  50. record.setStatus(approvalStatus.getValue());
  51. record.setStatusChangeTime(new Date());
  52. approvalMapper.updateByPrimaryKeySelective(record);
  53. // 整个审批流程结束,则回调业务方,业务方可以更改自己的数据
  54. finish.accept(approvalStatus);
  55. approvalEventPublisher.publishApprovalEvent(approvalId, ApprovalDataType.valueOf(approval.getDataType()), approval.getDataId(), true, approvalStatus);
  56. approvalRecordService.add(approvalId, processNodeCurrent, approvalOpinion);
  57. }
  58. } else {
  59. // 被拒绝:则整个流程结束
  60. final Approval record = new Approval();
  61. record.setId(approvalId);
  62. record.setNodeStatusChangeTime(new Date());
  63. record.setNodeStatus(approvalStatus.getValue());
  64. record.setStatus(approvalStatus.getValue());
  65. record.setStatusChangeTime(new Date());
  66. approvalMapper.updateByPrimaryKeySelective(record);
  67. finish.accept(approvalStatus);
  68. approvalRecordService.add(approvalId, approval.getProcessNodeCurrent(), approvalOpinion);
  69. approvalEventPublisher.publishApprovalEvent(approvalId, ApprovalDataType.valueOf(approval.getDataType()), approval.getDataId(), true, approvalStatus);
  70. }
  71. }

业务方调用审核操作示例

  1. @ConditionalOnBean
  2. @Override
  3. @Transactional(propagation = Propagation.REQUIRED)
  4. public void approval(Integer id, PerformanceApprovalRequest params, UserInfo userInfo) {
  5. /*
  6. 审批要做的事情
  7. 1. 检查状态
  8. 2. 检查当前操作人是否是 审批者
  9. 3. 处理
  10. */
  11. final Performance performance = getId(id);
  12. final Long approvalId = performance.getApprovalId();
  13. final Approval approval = approvalService.getById(approvalId);
  14. if (approval == null) {
  15. throwErr("该审批已失效");
  16. }
  17. final ApprovalStatus status = ApprovalStatus.valueOf(approval.getStatus());
  18. if (ApprovalStatus.APPROVAL_ING != status) {
  19. throwErr("状态异常:当前审批状态不在审批中");
  20. }
  21. final Integer pendingById = approval.getPendingById();
  22. if (pendingById != userInfo.getId()) {
  23. throwErr(StrUtil.format("审批人异常:您不是当前审批人,当前审批人为 {} [id={}]", approval.getPendingBy(), approval.getPendingById()));
  24. }
  25. // 处理审批
  26. final ApprovalOpinion approvalOpinion = new ApprovalOpinion(userInfo.getId(), userInfo.getName(), params.getIsPass(), params.getOpinion());
  27. approvalService.doApproval(approval,
  28. approvalOpinion,
  29. finishStatus -> {
  30. // 审批有最终结果,就改变业务审批单的状态
  31. if (ApprovalStatus.APPROVAL_PASS == finishStatus) {
  32. this.changeApprovalStatus(id, finishStatus, userInfo);
  33. } else {
  34. this.changeApprovalStatus(id, finishStatus, userInfo);
  35. }
  36. });
  37. }

如何获取 UI 上的审批流程数据?

上面有了 审批单信息、审批操作处理,那么如何构建 UI 展示的数据信息呢?这里其实也是一个小难点

  1. /**
  2. * 转换审批单流程节点信息
  3. *
  4. * @param approval
  5. * @return
  6. */
  7. private List<ApprovalProcessNodeDetail> convertProcessNodeDetail(Approval approval) {
  8. // 从审批单获取流程定义
  9. final String processDefineJson = approval.getProcessDefineJson();
  10. final List<ApprovalProcessNode> approvalProcessNodes = parseProcessDefineJson(processDefineJson);
  11. final int processNodeCurrent = approval.getProcessNodeCurrent();
  12. // 获取到审批记录,填充审批记录信息
  13. List<ApprovalRecord> records = approvalRecordService.listByApprovalId(approval.getId());
  14. final Map<Integer, ApprovalRecord> recordMap = records.stream().collect(Collectors.toMap(ApprovalRecord::getProcessNodeCurrent, Function.identity()));
  15. List<ApprovalProcessNodeDetail> list = new ArrayList<>(approvalProcessNodes.size());
  16. // 循环审批流程定义,然后填充 审批记录信息:比如审批意见
  17. for (ApprovalProcessNode approvalProcessNode : approvalProcessNodes) {
  18. final Integer node = approvalProcessNode.getNode();
  19. final ApprovalProcessNodeDetail nodeDetail = new ApprovalProcessNodeDetail();
  20. nodeDetail.setNode(node);
  21. nodeDetail.setAccountId(approvalProcessNode.getAccountId());
  22. nodeDetail.setAccountName(approvalProcessNode.getAccountName());
  23. final ApprovalRecord record = recordMap.get(node);
  24. // 如果已经有对应的审批记录,则使用审批记录中的相关信息
  25. // 如果没有,留空即可
  26. if (record != null) {
  27. nodeDetail.setStatus(record.getNodeStatus());
  28. nodeDetail.setStatusChangeTime(record.getNodeStatusChangeTime());
  29. nodeDetail.setAccountName(record.getPendingBy());
  30. nodeDetail.setOpinion(record.getOpinion());
  31. list.add(nodeDetail);
  32. continue;
  33. }
  34. // 已通过,因为没有通过的话,就不会继续到下一审批人了
  35. if (node < processNodeCurrent) {
  36. nodeDetail.setStatus(ApprovalStatus.APPROVAL_PASS.getValue());
  37. } else if (node == processNodeCurrent) {
  38. // 如果是当前节点,就直接复制当前状态上的
  39. nodeDetail.setStatus(approval.getNodeStatus());
  40. }
  41. list.add(nodeDetail);
  42. }
  43. return list;
  44. }

审批流程节点详细信息:

  1. /**
  2. * 审批流程节点详细:可以包含谁审核的,审核时间,原因等
  3. *
  4. * @author zhuqiang
  5. * @date 2021/7/23 10:23
  6. */
  7. @Data
  8. @ToString
  9. public class ApprovalProcessNodeDetail {
  10. // 节点顺序, 数组索引 +1
  11. private Integer node;
  12. // 该节点需要这个指定的 账户才能审批
  13. private Integer accountId;
  14. // 账户名称
  15. private String accountName;
  16. /**
  17. * 审批状态
  18. */
  19. private Byte status;
  20. /**
  21. * 状态时间
  22. */
  23. private Date statusChangeTime;
  24. /**
  25. * 审批意见
  26. */
  27. private String opinion;
  28. }

上面的 list 审批流程节点数据,就能实现下面这个 UI 了
image.png