处理审批业务,相较于传统的if else改状态(处理单一业务),审批流(处理大量复杂任务,或签、会签、条件签)比较省事。因此写一个轻量级、与业务完全解耦的通用工作流引擎,能避免屎山。必须把"业务单据"和"审批规则"分离开来
业务表基础字段(所有业务表必须包含)
为了能让引擎统一操作,无论是请假表还是报销表,都必须包含以下核心流程字段:
bash
ALTER TABLE `业务表`
ADD COLUMN `workflow_id` varchar(32) COMMENT '绑定的流程图ID',
ADD COLUMN `node_id` varchar(32) COMMENT '当前停留的节点ID',
ADD COLUMN `node_code` varchar(50) COMMENT '当前停留的节点编码(如: HR_AUDIT)',
ADD COLUMN `node_name` varchar(50) COMMENT '当前停留的节点名称(如: 人事审批)',
ADD COLUMN `vstatus` char(1) DEFAULT '0' COMMENT '审批状态(0:待审 1:通过 2:退回 3:终止)',
ADD COLUMN `vcondition` varchar(50) COMMENT '路由条件(比如存"请假>3天",供引擎走分支判断)',
ADD COLUMN `opinions` varchar(500) COMMENT '最新一次的审核意见';
流程节点配置表(t_workflow_node)
管理员在后台配置好,引擎在流转时就会来查这张表找下一个目标。
bash
CREATE TABLE `t_workflow_node` (
`id` varchar(32) NOT NULL COMMENT '主键',
`workflow_id` varchar(32) NOT NULL COMMENT '所属流程ID(如: 请假流程ID)',
`node_code` varchar(50) NOT NULL COMMENT '节点编码(如: start, end, manager_audit)',
`node_name` varchar(50) NOT NULL COMMENT '节点名称(如: 经理审批)',
`parent_node_code` varchar(50) COMMENT '上一个节点的编码(用于串联整个流程图)',
`audit_type` char(1) COMMENT '审核人类型(0:指定人员 1:指定角色 4:发起人自己)',
`auditor_id` varchar(32) COMMENT '审核人ID(如果audit_type=0)',
`role_id` varchar(32) COMMENT '审核角色ID(如果audit_type=1)',
`countersignature` char(1) DEFAULT '0' COMMENT '是否会签?(0:或签-一人同意即可, 1:会签-所有人同意)',
`vcondition` varchar(100) COMMENT '进入该节点的条件(如金额大小,用于分支路由)',
PRIMARY KEY (`id`)
) COMMENT='流程节点配置表';
审核流水/待办任务表 (t_workflow_audit)
所有的待办任务、已办记录全在这里。引擎的流转,本质上就是不断往这张表里insert新节点的待办人,并update老节点的状态。
bash
CREATE TABLE `t_workflow_audit` (
`id` varchar(32) NOT NULL COMMENT '主键',
`workflow_id` varchar(32) NOT NULL COMMENT '所属流程ID',
`object_id` varchar(32) NOT NULL COMMENT '具体业务单据的ID(如请假单的ID)',
`object_name` varchar(100) COMMENT '业务单据名称(如: 张三的请假单)',
`node_id` varchar(32) NOT NULL COMMENT '当前节点ID',
`node_code` varchar(50) NOT NULL COMMENT '当前节点编码',
`node_name` varchar(50) NOT NULL COMMENT '当前节点名称',
`auditor_id` varchar(32) NOT NULL COMMENT '该条任务的审核人ID(谁来批)',
`auditor_name` varchar(50) COMMENT '审核人姓名',
`vstatus` char(1) DEFAULT '0' COMMENT '审核状态(0:待审 1:同意 2:退回)',
`opinions` varchar(500) COMMENT '该审核人填写的审核意见',
`finish_flag` char(1) DEFAULT '0' COMMENT '是否流转结束标志',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '任务到达时间',
`audit_time` datetime COMMENT '实际审核时间',
PRIMARY KEY (`id`)
) COMMENT='流程审核流水表';
一、业务表
为了让引擎能统一处理各种奇形怪状的业务单据,我们需要定义一个泛型上限 ProcBean。所有的业务表(如请假表、采购表等)对应的实体类,都必须继承它。
java
import lombok.Data;
import java.io.Serializable;
/**
* 流程业务单据基类
* 任何需要接入工作流的业务实体(如请假单、采购单)都必须继承此类
*/
@Data
public class ProcBean implements Serializable {
// 业务基础字段
private String id; // 业务单据主键ID
private String objectId; // 业务单据ID (与id一致)
private String objectNo; // 业务单据编号
private String objectName; // 业务单据名称(如:张三的请假单)
private String preObjectId; // 前置关联单据ID(如:采购申请关联的报价单)
private String preObjectName; // 前置关联单据名称
// 流程核心控制字段
private String workflowId; // 当前绑定的流程图ID
private String nodeId; // 当前卡在哪个流程节点的ID
private String nodeCode; // 当前节点编码
private String nodeName; // 当前节点名称
private String vstatus; // 状态 (0:待审 1:通过 2:退回)
private String vcondition; // 路由条件分支(如填入 ">5000" 让引擎判断)
private String opinions; // 最新的审批意见
// 基础溯源字段
private String dataOrgId; // 数据所属部门机构ID
private String createdById; // 创建人(发起人)ID
private String createdByName; // 创建人(发起人)姓名
}
二、BaseFlowController.java
既然是通用流转,前端调用的无非就是**"发起审批"、"同意"、"驳回"**。可以利用 Java 8 接口的default方法,写一个统一的BaseFlowController。新业务只需要继承这个接口,零代码就能拥有完整的审批端点。
java
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
/**
* 通用流程控制层接口
* 利用 default 方法实现通用路由挂载,新业务直接继承即可拥有所有审批接口
*/
public interface BaseFlowController<S extends BaseFlowService, T> extends BaseController<S,T>{
/**
* 发起/初始化流程
*/
@PostMapping("/insertFlowList")
default JsonResult insertFlowList(@RequestBody FlowObjBean objBean){
String flowCode = objBean.getFlowCode();
if (PxStringUtils.isEmpty(flowCode)){
return JsonResult.error("流程编码【flowCode】不能为空!");
}
// 直接转交底层的泛型 Service 处理
return getService().insertFlowList(objBean);
}
/**
* 审核通过 (Pass)
*/
@PostMapping("/pass")
default JsonResult pass(@RequestBody FlowObjBean objBean){
return getService().pass(objBean);
}
/**
* 审核驳回/拒绝 (Back)
*/
@PostMapping("/back")
default JsonResult back(@RequestBody FlowObjBean objBean){
if (objBean == null){
return JsonResult.error("参数不能为空!");
}
String opinion = objBean.getOpinions();
if (PxStringUtils.isEmpty(opinion)){
return JsonResult.error("退回原因不能为空!");
}
return getService().back(objBean);
}
}
三、WorkflowEngineService.java
引擎完全不知道当前处理的是请假单还是报销单,修改具体业务表状态使用 Java 泛型边界<T extends ProcBean>+ Spring IOC 容器动态获取 Service 实例。
1、initWorkflowNode方法
当用户点击"提交审批"时,调用此方法为单据装载第一个节点,并通知第一位审核人。
java
/**
* 初始化流程 (发起审批)
* @param list 业务单据列表 (支持批量发起)
* @param flowCode 流程图编码 (如:LEAVE_FLOW)
* @param user 当前登录用户 (发起人)
* @param serviceImplName 重点!具体的业务Service的Bean名称 (如:leaveServiceImpl)
*/
@Transactional
public <T extends ProcBean> void initWorkflowNode(List<T> list, String flowCode, UserBean user, String serviceImplName) {
Assert.notNull(list,"数据列表不能为空!");
Assert.notNull(flowCode,"流程编码不能为空");
Assert.notNull(serviceImplName,"service层文件名不能为空!");
LocalDateTime now = LocalDateTime.now();
// 1. 查询该流程图的【第一个节点】配置 (即起点)
WorkflowNodeBean workflowNodeBean = workflowNodeMapper.selectFirstNodeBeanByCode(flowCode);
if(workflowNodeBean == null){
throw new RuntimeException("流程已停用或没有设置初始节点!");
}
List<WorkflowAuditBean> insertBeans = new ArrayList<>();
// 2. 遍历业务单据,写入节点信息,并生成第一条审核流水记录
for (T addbean : list){
// 生成待办流水记录给第一个审批人
insertBeans.add(this.initFlowAuditBean((ProcBean) addbean, workflowNodeBean, user, now));
// 将流程图ID、节点ID等状态信息回写到业务单据实体中
addbean.setWorkflowId(workflowNodeBean.getWorkflowId());
addbean.setNodeId(workflowNodeBean.getId());
addbean.setNodeCode(workflowNodeBean.getNodeCode());
addbean.setNodeName(workflowNodeBean.getNodeName());
}
// 批量插入待办流水
if (PxListUtils.isNotEmpty(insertBeans)){
workflowAuditMapper.insertList(insertBeans);
}
// 3. 【核心解耦魔法】通过前端传来的 serviceImplName,动态抓取具体业务的 Service!
// 引擎不管你是请假单还是报销单,直接调用你的通用更新方法,将状态刷入对应业务表
BaseService service = PxApplicationContextUtils.getService(serviceImplName);
for(T bean : list){
service.getMapper().updateById(bean);
}
}
2、审批通过(pass)
处理常规的流转,且实现会签/或签策略和基于vcondition的条件分支路由。
java
/**
* 审核通过核心驱动
* @param auditBeanList 前端传来的待处理业务单据集合
* @param objBean 包装了审核意见(opinions)等额外参数的对象
* @param loginUser 当前点击同意的登录用户
* @param serviceImplName 业务Service的Bean名称
*/
@Transactional
public <T extends ProcBean> String pass(List<T> auditBeanList, FlowObjBean objBean, UserBean loginUser , String serviceImplName) {
String nextNodeCode = objBean.getNextNodeCode();
String opinions = objBean.getOpinions();
if(PxListUtils.isEmpty(auditBeanList)){
throw new RuntimeException("流程数据不能为空!");
}
String workFlowId = auditBeanList.get(0).getWorkflowId();
// 1. 获取当前节点信息:比对流程图,确认当前走到哪一步了
List<String> nodeCodes = auditBeanList.stream().map(ProcBean::getNodeCode).distinct().collect(Collectors.toList());
List<WorkflowNodeBean> currBeans = workflowNodeMapper.selectCurrBeansByCodes(workFlowId, nodeCodes);
if(PxListUtils.isEmpty(currBeans)){
throw new RuntimeException("流程节点不存在,或者流程已停用!");
}
// 2. 预查询可能的【下一个节点】集合
List<WorkflowNodeBean> nextNodeBeans;
Map<String,Object> param = new HashMap<>();
if (PxStringUtils.isNotEmpty(nextNodeCode)){
// 如果前端强制指定了下一节点
param.put("workflowId",workFlowId);
param.put("nodeCode", nextNodeCode);
nextNodeBeans = workflowNodeMapper.selectListByParam(param);
} else {
// 根据流程图连线,自动找出所有的子节点
nextNodeBeans = workflowNodeMapper.selectNextBeansByCodes(workFlowId, nodeCodes);
}
List<WorkflowAuditBean> updateBeans = new ArrayList<>();
List<WorkflowAuditBean> insertBeans = new ArrayList<>();
List<WorkflowAuditBean> deleteBeans = new ArrayList<>();
List<T> passBeans = new ArrayList<>();
for (WorkflowNodeBean currBean : currBeans){
List<String> objectIds = auditBeanList.stream().filter(t-> t.getNodeId().equals(currBean.getId())).map(ProcBean::getId).distinct().collect(Collectors.toList());
// 3. 查出当前节点下所有的待办任务
param.put("nodeId", currBean.getId());
param.put("workflowId", currBean.getWorkflowId());
param.put("finishFlag", "0");
param.put("objectIdList", objectIds);
List<WorkflowAuditBean> workflowAuditBeans = workflowAuditMapper.selectListByParam(param);
// 找出属于当前登录人的待办任务
List<WorkflowAuditBean> updates = workflowAuditBeans.stream().filter(t -> t.getAuditorId().equals(loginUser.getId())).collect(Collectors.toList());
// 防越权:判断当前用户是否有审核权限,是否重复审核
this.judgeIsAudited(updates, objectIds);
updateBeans.addAll(updates);
// 4.判断是【会签】还是【或签】
String countersignature = currBean.getCountersignature();
List<T> addBeans;
if (PxStringUtils.isNotEmpty(countersignature) && "1".equals(countersignature)){
// 【会签逻辑】:必须所有人同意。查找是否还有其他人的待办状态是待审(vstatus=0)
List<String> updateIds = updates.stream().map(WorkflowAuditBean::getId).collect(Collectors.toList());
List<String> remainObjectIds = workflowAuditBeans.stream().filter(t -> !updateIds.contains(t.getId()) && "0".equals(t.getVstatus())).map(WorkflowAuditBean::getObjectId).collect(Collectors.toList());
// 只有全部人审完,单据才能进入下一环节 (过滤掉还在等待其他人的单据)
addBeans = auditBeanList.stream().filter(t -> !remainObjectIds.contains(t.getObjectId())).collect(Collectors.toList());
} else {
// 【或签逻辑】:只要当前人同意即可。
addBeans = auditBeanList;
// 立刻物理删除同节点下其他人的待办任务!(比如张三批了,李四桌上的任务就消失了)
deleteBeans.addAll(workflowAuditBeans.stream().filter(t->!t.getAuditorId().equals(loginUser.getId()) && "0".equals(t.getVstatus())).collect(Collectors.toList()));
}
// 5. 将过滤后允许通行的单据,推送到【下一个节点】
if (PxListUtils.isNotEmpty(addBeans)) {
LocalDateTime now = LocalDateTime.now();
List<WorkflowNodeBean> nexts = nextNodeBeans.stream().filter(t -> t.getParentNodeCode().equals(currBean.getNodeCode())).collect(Collectors.toList());
if (nexts.size() == 1) {
// 单线流转:直接去唯一的下一站
for (T addbean : addBeans){
if ("end".equals(nexts.get(0).getNodeCode().toLowerCase())){
// 如果下一站是结束节点,生成归档流水
insertBeans.add(this.initFlowAuditBean(addbean, nexts.get(0), null, now));
} else {
// 动态查询下一站的审核人(可能是按角色查出一批人),并为他们生成待办流水
List<UserBean> users = this.getNextNodeAuditors(nexts.get(0), addbean, addbean.getDataOrgId());
for (UserBean user : users){
insertBeans.add(this.initFlowAuditBean(addbean, nexts.get(0), user, now));
}
}
addbean.setVstatus("1"); // 标记业务状态为审核通过
addbean.setOpinions("");
this.initObjectNode(addbean, nextNodeBeans.get(0));
}
} else {
// 【分支路由流转】:下一站有多个分叉(如:>1万走老板,<1万走主管)
for (T addbean : addBeans){
boolean hasPass = false;
for (WorkflowNodeBean nextNodeBean : nexts){
// 比对业务单据里携带的条件 (addbean.getVcondition()) 和 路线图配置的条件
if (nextNodeBean.getVcondition() != null && nextNodeBean.getVcondition().equals(addbean.getVcondition())){
// 找到匹配的分支路线!
if ("end".equals(nextNodeBean.getNodeCode().toLowerCase())){
insertBeans.add(this.initFlowAuditBean(addbean, nextNodeBean, null, now));
} else {
List<UserBean> users = this.getNextNodeAuditors(nextNodeBean, addbean, addbean.getDataOrgId());
for (UserBean user : users){
insertBeans.add(this.initFlowAuditBean(addbean, nextNodeBean, user, now));
}
}
addbean.setVstatus("1");
this.initObjectNode(addbean, nextNodeBean);
hasPass = true;
break;
}
}
if (!hasPass){
throw new RuntimeException("分支条件不满足,未能找到下一个节点!");
}
}
}
passBeans.addAll(addBeans);
}
}
// 6. 执行最终的数据库持久化操作
if (PxListUtils.isNotEmpty(passBeans)){
// 回写更新具体业务表的状态!
BaseService service = PxApplicationContextUtils.getService(serviceImplName);
for(T updateObject : passBeans){
service.updateByPrimaryKey(updateObject);
}
}
// 更新旧待办任务为已办,填入审核意见
if (PxListUtils.isNotEmpty(updateBeans)){
LocalDateTime now = LocalDateTime.now();
for(WorkflowAuditBean updateBean : updateBeans){
updateBean.setVstatus("1");
updateBean.setOpinions(opinions);
updateBean.setAuditTime(now);
}
this.workflowAuditMapper.updateStatus(updateBeans);
}
// 销毁或签中废弃的其他任务
if (PxListUtils.isNotEmpty(deleteBeans)){
this.workflowAuditMapper.deleteBatchIds(deleteBeans.stream().map(WorkflowAuditBean::getId).collect(Collectors.toList()));
}
// 推送下一步的新待办任务
if (PxListUtils.isNotEmpty(insertBeans)){
this.workflowAuditMapper.insertList(insertBeans);
}
return "SUCCESS";
}
3、审核驳回(back)
直接将单据打回给原发起人,并强制终止当前的流程环节。
java
/**
* 审核退回/驳回
*/
@Transactional
public <T extends ProcBean> void back(List<T> objBeanList, FlowObjBean objBean, String serviceImplName) {
String opinions = objBean.getOpinions();
String workFlowId = objBeanList.get(0).getWorkflowId();
UserBean loginUser = PxLocalContextHelper.getLoginUser();
// 1. 获取当前节点所有审核记录,校验权限
List<String> objectIds = objBeanList.stream().map(ProcBean::getId).distinct().collect(Collectors.toList());
Map<String,Object> param = new HashMap<>();
param.put("workflowId", workFlowId);
param.put("objectIdList", objectIds);
param.put("finishFlag", "0");
param.put("vstatus", "0");
List<WorkflowAuditBean> workflowAuditBeans = workflowAuditMapper.selectListByParam(param);
List<WorkflowAuditBean> updateBeans = workflowAuditBeans.stream().filter(t->t.getAuditorId().equals(loginUser.getId())).collect(Collectors.toList());
this.judgeIsAudited(updateBeans, objectIds);
// 驳回时,必须删除该节点其他还没审核的待办记录
List<WorkflowAuditBean> deleteBeans = workflowAuditBeans.stream().filter(t->!t.getAuditorId().equals(loginUser.getId())).collect(Collectors.toList());
// 2. 找到流程的起始节点 (退回给发起人)
WorkflowNodeBean workflowNodeBean = workflowNodeMapper.selectFirstNodeByFlowId(workFlowId);
List<WorkflowAuditBean> insertBeans = new ArrayList<>();
LocalDateTime now = LocalDateTime.now();
// 3. 生成退回流水通知发起人
for (WorkflowAuditBean auditBean : updateBeans){
T t1 = objBeanList.stream().filter(t -> t.getObjectId().equals(auditBean.getObjectId())).findFirst().orElse(null);
WorkflowAuditBean newAuditBean = new WorkflowAuditBean();
// ... 基础属性赋值省略
newAuditBean.setAuditorId(t1.getCreatedById()); // 审核人设为原发起人
newAuditBean.setNodeId(workflowNodeBean.getId());
newAuditBean.setVstatus("2"); // 状态标记为退回
insertBeans.add(newAuditBean);
}
// 4. 将业务单据的状态强行改为"退回(2)",节点重置为起点,并附上驳回意见
if (PxListUtils.isNotEmpty(objBeanList)){
BaseService service = PxApplicationContextUtils.getService(serviceImplName);
for(T updateObject : objBeanList){
updateObject.setNodeId(workflowNodeBean.getId());
updateObject.setNodeCode(workflowNodeBean.getNodeCode());
updateObject.setOpinions(opinions);
updateObject.setVstatus("2");
service.updateByPrimaryKey(updateObject);
}
}
// 5. 落库持久化操作
// 更新原待办为退回状态,删除其他冗余待办,插入退回通知记录
}