1. 数据库设计
首先我们来看看审批数据是如何存储的。在办公自动化系统中,审批流程的管理是至关重要的部分,其核心功能通过几张特定的数据库表来实现。这些表包括approval_record
、approval_step
、approval_step_record
和approval_type
:
如何交互的呢?
1. approval_record
(审批记录)
此表用于存储整个审批流程的记录,包括每个审批的基本信息和当前状态。
这张表是审批流程的核心,记录了每一个发起的审批事务。它通过current_step_id字段与approval_step表发生直接关联,指向当前审批事务所处的具体步骤。这个字段的更新反映了审批流程的推进,每完成一个步骤后,current_step_id会更新为下一个步骤的ID,从而驱动整个审批流程向前发展。
其字段包括:
id
:主键ID,唯一标识一个审批记录。approval_type_id
:关联的审批类型ID,指明当前审批属于哪一类审批。initiator_username
:发起人的用户名,记录谁启动了这个审批流程。created_at
:审批发起的时间。update_at
:记录的最后一次更新时间。current_step_id
:当前所处的审批步骤ID。status
:当前审批的状态,可能是waiting
(正在审批)、success
(成功)、failed
(失败)。watch_username_set
:用户查看集合,采用JSON格式存储可查看此审批记录的用户列表。document_id
:关联的文档ID,如果审批过程中涉及文档处理。attachment_id_set
:附件ID集合,以JSON格式存储与审批相关的所有附件。
2. approval_step
(审批步骤)
定义审批流程中的每一步骤。approval_step表定义了审批流程中的各个固定步骤。这些步骤记录是静态的,一旦定义,就不会发生改变,确保审批流程的标准化和一致性。每个步骤通过approval_type_id与approval_type表关联,表明该步骤属于哪一类审批类型。step_order字段用于标识审批流程中的步骤顺序字段说明如下:
id
:主键ID,唯一标识一个步骤。step_order
:步骤顺序,定义步骤在审批流程中的执行顺序。logic
:审核逻辑,0代表“与”逻辑,1代表“或”逻辑,决定这一步的通过条件。description
:步骤描述,详细说明步骤的内容和要求。approval_type_id
:该步骤所属的审批类型ID,表明这个步骤属于哪种类型的审批。
3. approval_step_record
(审批步骤记录)
记录审批流程中每个步骤的具体执行情况。这张表记录了审批流程中每个步骤的具体执行情况。对于同一个approval_record,可能会有多个approval_step_record条目,每个条目记录了一次具体的审批操作。这些记录详细追踪了谁在何时对审批事务做出了哪些处理,包括审批结果、审批意见等。通过approval_username_set字段,以JSON格式存储有权审批当前记录的用户列表,这不仅保证了审批权限的正确性,还帮助前端系统正确显示相关用户的操作权限。字段包括:
id
:主键ID。approval_record_id
:关联的审批记录ID,指明这个步骤记录属于哪个审批流程。step_id
:具体步骤ID,表示这条记录是对哪个步骤的执行情况的记录。username
:执行审批的用户的用户名。update_at
:记录更新时间。logic
:审批逻辑,与approval_step
表中的逻辑相对应。comment
:审批意见,记录审批者的具体意见或反馈。next_step_id
:下一步步骤ID,指向流程中的下一个步骤。status
:步骤的审批状态,如success
(成功)、failed
(失败)、waiting
(等待中)。approval_type_id
:审批类型ID,说明这个步骤属于哪种类型的审批。approval_username_set
:用户审批集合,以JSON格式记录参与此步骤审批的所有用户。attachment_id_set
:附件ID集合,存储相关的所有附件。
4. approval_type
(审批类型)
分类和定义不同的审批类型。approval_type表定义了系统中所有可用的审批类型。字段如下:
id
:主键ID。name
:审批类型的名称。description
:对审批类型的详细描述。
2. 一个请求是怎么发起的呢?
2.1 getHandler的动态匹配
以插入一条OA表单为例子,使用apifox来发起一条请求。
在上面的表内,发现合法的type_id只有1~9号,也就是一共只有9类OA类型。
在此之前,先输入请求一个非法的ID,比如10号:
可以预料到,返回肯定不存在对应的OA类型。
OK,那现在输入一个正确的id数值,比如说id == 5,对应的是校外转专业的类型。
debug进入OfficeAutomationService#getHandler(Long typeId)之内,通过typeId在枚举类内部匹配到当前的操作类型
由于笔者当前分支的枚举类并没有完全合并其余的分支的字段,仅供参考,正确的情况应该刚好是9个字段。
但没有关系,在match函数内开始进行匹配,在上篇文章内,我们已经讨论过,在match内先通过id在数据库中查询到对应的OA审批类型,和枚举类的字段进行匹配。
匹配成功,返回value给上一层的getHandler方法。通过当前的这个handler完成数据插入。又因为OfficeAutomationHandler是一个抽象类,子类重写抽象类的方法,通过动态绑定的方式会调用子类重写的方法,从而实现了策略模式匹配不同的OA审批类型。
那么其余的方法也是完全一样的,都是先传入typeId然后去数据库进行查询,并且之后和枚举的字段进行匹配,如果成功匹配,就调用这个方法查询到具体的handler,调用当前的handler完成特定的数据操作。
2.2 MongoDB表单管理
表单管理是一个核心功能,它支持创建、更新、查询和删除表单的操作。用户填报者在前端输入表单并且提交,这个表单在整个OA审批流程中不断流转。这一过程需要对MongoDB的集合进行操作。
插入表单
当需要在OA系统中创建一个新的审批表单时,首先需要定义一个insertDocument
方法。这个方法接受一个键值对集合Map<String, Object>
作为表单数据,以及一个typeId
表示表单的类型。方法的实现如下:
public String insertDocument(Map<String, Object> map, Long typeId) {
if (Objects.isNull(typeId)) {
throw new BusinessException("类型 id 为空");
}
// 获取对应类型的处理器
String id = getHandler(typeId).insertDocument(map);
if (StrUtil.isBlank(id)) {
throw new BusinessException("插入失败");
}
return id;
}
这里,getHandler(typeId)
方法负责获取处理指定类型表单的处理器,该处理器实现了具体的插入逻辑,并返回生成的文档ID。如果插入失败,方法会抛出一个业务异常。
更新表单
更新表单也是一个常见需求,updateDocument
方法允许对已存在的表单进行修改:
public Object updateDocument(Map<String, Object> map, Long typeId) {
if (Objects.isNull(typeId)) {
throw new BusinessException("类型 id 为空");
}
if (!map.containsKey("id")) {
throw new BusinessException("表单 id 为空");
}
return getHandler(typeId).updateById(map, String.valueOf(map.get("id")));
}
此方法确保表单ID和类型ID均有效,然后调用处理器的updateById
方法,根据表单ID更新数据。
查询表单
查询特定表单的selectDocumentById
方法可以根据表单ID和类型ID获取表单数据:
public Object selectDocumentById(String id, Long typeId) {
if (Objects.isNull(typeId)) {
throw new BusinessException("类型 id 为空");
}
if (StrUtil.isBlank(id)) {
throw new BusinessException("表单 id 为空");
}
return getHandler(typeId).selectDocument(id);
}
这一方法通过调用处理器的selectDocument
方法返回指定ID的文档。
删除表单
最后,deleteDocument
方法实现了根据表单ID删除文档的功能:
public Integer deleteDocument(String id, Long typeId) {
if (Objects.isNull(typeId)) {
throw new BusinessException("类型 id 为空");
}
if (StrUtil.isBlank(id)) {
throw new BusinessException("表单 id 为空");
}
return getHandler(typeId).deleteDocument(id);
}
这一方法调用处理器的deleteDocument
方法来移除文档,并返回操作结果。
2.3 如何使用这些数据库?
approval_step,approval_type这两个表一直存储的都是“死数据”,都是后台管理员手动插入数据。
如果是正常的推进,在OfficeAutomationHandler#createApprovalStepRecord方法中,先使用buildApprovalUsernameSet方式构建构建一条approvalStepRecordPO的类数据,并且插入数据库,状态为等待审批。
新的数据已经被插入了,同时系统消息也已经被插入到数据库内。
3. 流转函数
1. process(ApprovalStepRecordPO approvalStepRecordPO)
该方法负责处理审批步骤的工作流程。包括验证、更新步骤记录、根据步骤结果确定后续行动(成功、失败或转移),以及处理步骤完成后的后续活动。具体流程如下:
- 验证:使用 check() 方法确保输入参数的正确性和用户具有必要的权限。
- 更新步骤记录:调用 approvalStepRecordService.updateApprovalStepRecordById(approvalStepRecordPO) 来更新数据库中对应的审批步骤记录的状态。如果更新失败(即返回的更新计数为0),则抛出异常,指出“更新当前步骤失败”。
- 确定结果并处理:
- SUCCESS:调用 success() 方法,处理步骤成功的后续操作。
- FAILED:调用 failed() 方法,处理步骤失败的后续操作。
- TRANSFER:调用 transfer() 方法,将审批流程转移到下一个指定的步骤。
- 其他情况:返回 false,表示未能识别或处理的状态。
- 查询更新完的审批记录:使用 approvalRecordService.selectById(approvalStepRecordPO.getApprovalId()) 查询此步骤对应的完整审批记录。如果查询失败(记录为空),则抛出异常,指出“获取审核记录失败”。
- 后处理:更新步骤后,获取更新后的审批记录,并触发 afterProcess() 执行步骤完成后的任何附加动作。
- 审批最终处理:如果当前审批记录的状态为 SUCCESS 或 FAILED,则说明审批流程已经结束。此时,调用 afterApproval() 方法来执行审批完成后的任何必要操作,如资源清理、最终通知等。
方法的一个大致的骨架像是这样:
具体的代码像是这样:
/**
* 审核当前步骤
*
* @param approvalStepRecordPO 审核参数
* @return true-成功
*/
public Boolean process(ApprovalStepRecordPO approvalStepRecordPO) {
// 检查参数
if (!check(approvalStepRecordPO)) {
throw new BusinessException("参数非法");
}
// 基本参数
DateTime date = DateUtil.date();
// 更新当前步骤记录状态
int count = approvalStepRecordService.updateApprovalStepRecordById(approvalStepRecordPO);
if (count == 0) {
throw new BusinessException("更新当前步骤失败");
}
switch (Objects.requireNonNull(match(approvalStepRecordPO.getStatus()))) {
case SUCCESS:
success(approvalStepRecordPO.getApprovalRecordId(), approvalStepRecordPO.getStepId(), date);
break;
case FAILED:
failed(approvalStepRecordPO.getApprovalRecordId(), date);
break;
case TRANSFER:
transfer(approvalStepRecordPO.getApprovalRecordId(), date, approvalStepRecordPO.getNextStepId());
break;
default:
return false;
}
// 查询更新完的审批记录
ApprovalRecordPO approvalRecordPO = approvalRecordService.selectById(approvalStepRecordPO.getApprovalRecordId());
if (Objects.isNull(approvalRecordPO)) {
throw new BusinessException("获取审核记录失败");
}
// 执行当前步骤完成后的函数
afterProcess(approvalStepRecordPO, approvalRecordPO);
// 如果当前审批记录为success或者failed状态则说明已经完成,执行完成的步骤
if (SUCCESS.getStatus().equals(approvalRecordPO.getStatus()) || FAILED.getStatus().equals(approvalRecordPO.getStatus())) {
afterApproval(approvalRecordPO, approvalStepRecordPO);
}
return true;
}
- 职责定位:afterProcess 主要处理单个步骤的后续动作,而 afterApproval 处理整个审批过程的终结。
- 调用条件:afterProcess 在每个审批步骤后调用,而 afterApproval 仅在审批流程完全结束后调用。
- 业务逻辑:afterProcess 可能包括一些通用的后处理操作,如更新数据库、发送状态消息等;afterApproval 更多地关注如何根据审批的最终结果进行响应。
2. createApprovalStepRecord(Long approvalId, Date date, Long stepId)
为新记录设置具有审批权限的用户,并以等待状态初始化。
这个方法 createApprovalStepRecord(Long approvalId, Date date, Long stepId) 是用来在审批流程中创建新的审批步骤记录的。其中会调用 buildApprovalUsernameSet(approvalStepPO, approvalRecordPO.getDocumentId()) 方法,根据审批步骤和关联的文档ID构建一个拥有审核权限的用户集合 (Set)。这个集合指定了哪些用户有权进行当前审批步骤的审核。
引用了这个方法,分别构建审核步骤记录的审核人群:
/**
* 构建审核步骤记录的审核人群
*
* @param approvalStepPO 审核步骤
* @param documentId 申请表单编号
* @return username集合
*/
protected abstract Set<String> buildApprovalUsernameSet(ApprovalStepPO approvalStepPO, String documentId);
/**
* 构建审核记录可见人群
*
* @param approvalRecordPO 审核记录
* @return 可见人群的文档编号
*/
protected abstract Set<String> buildWatchUsernameSet(ApprovalRecordPO approvalRecordPO);
3. check(ApprovalStepRecordPO approvalStepRecordPO)
验证处理步骤所需的信息。检查步骤记录数据的完整性和正确性,并确保用户有权对步骤进行操作。
步骤流转函数配置
1. 步骤流转处理函数(transfer)
-
1.1 新建步骤记录:
- 调用 createApprovalStepRecord(approvalId, date, nextStepId) 方法创建一个新的审批步骤记录。
- 如果创建失败(返回值为0),抛出 BusinessException 异常,错误信息为“新增步骤记录失败”。
-
1.2 更新审批记录信息:
- 调用 approvalRecordService.updateApprovalRecordById(approvalId, date, nextStepId, TRANSFER) 更新审批记录的状态为流转(TRANSFER)。
- 如果更新失败(返回值为0),抛出 BusinessException 异常,错误信息为“修改OA信息失败”。
-
1.3 返回操作结果:
- 方法成功执行后返回 true。
2. 步骤失败处理函数(failed)
-
2.1 更新审批记录为失败状态:
- 调用 approvalRecordService.updateApprovalRecordById(approvalId, date, null, FAILED) 更新审批记录的状态为失败(FAILED)。
- 如果更新失败(返回值为0),抛出 BusinessException 异常,错误信息为“修改OA信息失败”。
-
2.2 返回操作结果:
- 方法成功执行后返回 true。
3. 步骤成功处理函数(success)
-
3.1 获取当前步骤信息:
- 调用 approvalStepService.selectById(stepId) 获取当前审批步骤的详细信息。
- 如果步骤信息获取失败(返回值为 null),抛出 BusinessException 异常,错误信息为“获取审核步骤失败”。
-
3.2 分析跳过的步骤:
- 调用 approvalStepRecordService.selectApprovalRecordWithApprovalRecordId(approvalId) 获取与审批ID关联的所有步骤记录信息。
- 如果未获取到任何步骤记录,抛出 BusinessException 异常,错误信息为“获取审核步骤记录失败”。
- 将步骤记录按步骤顺序进行分组。
-
3.3 确定下一步骤ID:
- 遍历分组后的步骤记录,查找未完成的步骤。
- 如果存在未完成的步骤,则设置下一步骤ID为未完成步骤的第一条记录的步骤ID。
- 如果当前步骤是流程中的最后一步,或者没有未完成的步骤,跳转至3.4。
-
3.4 创建或更新步骤记录:
- 如果存在下一步骤ID,创建新的步骤记录,并更新当前审批记录为等待状态(WAITING)。
- 如果不存在下一步骤(当前步骤是最后一步或已跳过所有未完成的步骤),更新当前审批记录为成功状态(SUCCESS)。
-
3.5 返回操作结果:
- 方法成功执行后返回 true。
支持方法和抽象钩子
- 抽象方法:该类包括几个抽象方法,如 createApprovalRecord()、afterProcess()、afterApproval() 和与文档相关的操作(insertDocument()、deleteDocument()、selectDocument()、updateById())。这些方法需要在派生类中实现,以处理特定类型的审批流程的特定行为。
- 实用功能:如 buildApprovalUsernameSet() 方法帮助确定哪些用户被允许参与给定的审批步骤,可以根据审批步骤的具体情况或组织规则进行定制。