1. 项目概述
最近在Jeecgboot 3.9.0框架中整合Flowable 7.2.0工作流引擎,实现了一个OA请假单功能模块。这个案例完整展示了从流程定义到前端表单的全链路开发过程,特别适合需要快速构建审批流程的中小型企业应用场景。
2. 技术栈选型解析
2.1 Jeecgboot框架优势
Jeecgboot 3.9.0作为底层开发框架,提供了完善的代码生成器和权限管理体系。实测发现其Vue3前端架构配合Ant Design组件库,能显著减少基础CRUD页面的开发时间。特别值得一提的是它的Online表单功能,对于简单业务场景可以做到零代码配置。
2.2 Flowable工作流引擎
选择Flowable 7.2.0主要考虑其轻量级特性和Spring Boot的良好集成。相比Activiti,Flowable的异步任务处理性能提升约30%,且提供了更直观的BPMN流程设计器。在实际开发中,其REST API可以直接对接前端审批操作。
3. 数据库设计要点
3.1 请假单主表结构
sql复制CREATE TABLE `oa_leave` (
`id` varchar(32) NOT NULL COMMENT '主键ID',
`process_instance_id` varchar(64) DEFAULT NULL COMMENT '流程实例ID',
`user_id` varchar(32) NOT NULL COMMENT '申请人ID',
`leave_type` tinyint(4) NOT NULL COMMENT '请假类型(1事假 2病假 3年假)',
`start_time` datetime NOT NULL COMMENT '开始时间',
`end_time` datetime NOT NULL COMMENT '结束时间',
`reason` varchar(500) NOT NULL COMMENT '请假事由',
`status` tinyint(4) DEFAULT '0' COMMENT '状态(0草稿 1审批中 2已通过 3已驳回)',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3.2 流程相关表
Flowable引擎会自动创建以下核心表:
- ACT_RE_*:存储流程定义和部署资源
- ACT_RU_*:运行时的流程实例和任务
- ACT_HI_*:历史流程数据
4. 后端实现关键代码
4.1 流程部署服务
java复制@Service
public class ProcessDeployer {
@Autowired
private RepositoryService repositoryService;
public void deployLeaveProcess() {
Deployment deployment = repositoryService.createDeployment()
.addClasspathResource("processes/leave.bpmn20.xml")
.name("请假流程")
.deploy();
logger.info("流程部署成功,ID: {}", deployment.getId());
}
}
4.2 启动流程实例
java复制public String startProcess(LeaveDTO leaveDTO) {
// 1. 保存业务数据
Leave leave = new Leave();
BeanUtils.copyProperties(leaveDTO, leave);
leaveMapper.insert(leave);
// 2. 启动流程
RuntimeService runtimeService = processEngine.getRuntimeService();
Map<String, Object> variables = new HashMap<>();
variables.put("leaveId", leave.getId());
variables.put("starter", leave.getUserId());
ProcessInstance instance = runtimeService.startProcessInstanceByKey(
"leaveProcess",
leave.getId(),
variables
);
// 3. 更新业务数据状态
leave.setProcessInstanceId(instance.getProcessInstanceId());
leave.setStatus(1);
leaveMapper.updateById(leave);
return instance.getId();
}
5. 前端Vue3实现
5.1 请假表单组件
vue复制<template>
<a-form-model :model="form" :rules="rules" ref="formRef">
<a-form-model-item label="请假类型" prop="leaveType">
<a-select v-model="form.leaveType">
<a-select-option :value="1">事假</a-select-option>
<a-select-option :value="2">病假</a-select-option>
<a-select-option :value="3">年假</a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item label="时间范围" required>
<a-range-picker
v-model="dateRange"
:show-time="{ format: 'HH:mm' }"
format="YYYY-MM-DD HH:mm"
/>
</a-form-model-item>
<a-form-model-item label="请假事由" prop="reason">
<a-textarea v-model="form.reason" :rows="4"/>
</a-form-model-item>
</a-form-model>
</template>
5.2 审批操作组件
vue复制methods: {
handleApprove(taskId) {
this.$confirm({
title: '确认通过该申请?',
onOk: () => {
completeTask(taskId, { action: 'approve' }).then(() => {
this.$message.success('审批完成');
this.loadData();
});
}
});
},
handleReject(taskId) {
this.$modal.info({
title: '驳回原因',
content: (
<a-textarea placeholder="请输入驳回理由" v-model="rejectReason"/>
),
onOk: () => {
if (!this.rejectReason) {
this.$message.warning('请填写驳回原因');
return Promise.reject();
}
completeTask(taskId, {
action: 'reject',
comment: this.rejectReason
}).then(() => {
this.$message.success('已驳回申请');
this.loadData();
});
}
});
}
}
6. 流程定义文件(leave.bpmn20.xml)
xml复制<process id="leaveProcess" name="请假流程" isExecutable="true">
<startEvent id="start" initiator="starter"/>
<userTask id="leaderAudit" name="部门领导审批"
candidateGroups="deptLeader">
<extensionElements>
<flowable:formProperty id="comment"
name="审批意见" type="string"/>
</extensionElements>
</userTask>
<sequenceFlow id="flow1" sourceRef="start" targetRef="leaderAudit"/>
<exclusiveGateway id="decision"/>
<sequenceFlow id="flow2" sourceRef="leaderAudit" targetRef="decision"/>
<sequenceFlow id="flow3" sourceRef="decision" targetRef="hrAudit">
<conditionExpression xsi:type="tFormalExpression">
<![CDATA[${action == 'approve'}]]>
</conditionExpression>
</sequenceFlow>
<sequenceFlow id="flow4" sourceRef="decision" targetRef="end">
<conditionExpression xsi:type="tFormalExpression">
<![CDATA[${action == 'reject'}]]>
</conditionExpression>
</sequenceFlow>
<userTask id="hrAudit" name="人事备案"
candidateGroups="hr"/>
<sequenceFlow id="flow5" sourceRef="hrAudit" targetRef="end"/>
<endEvent id="end"/>
</process>
7. 常见问题解决方案
7.1 流程实例无法启动
现象:调用startProcessInstanceByKey返回null
排查步骤:
- 检查bpmn文件是否部署成功
- 确认processDefinitionKey拼写完全匹配
- 查看ACT_RE_PROCDEF表确认流程定义状态
7.2 审批人无法看到任务
可能原因:
- 候选组设置错误,检查bpmn中的candidateGroups
- 用户未加入对应角色组
- 任务已被其他人签收
解决方案:
java复制// 查询用户所属组
List<Group> groups = identityService.createGroupQuery()
.groupMember(userId)
.list();
7.3 历史数据查询缓慢
优化方案:
- 添加ACT_HI_*表的索引
- 定期归档历史数据
- 使用异步日志记录
8. 性能优化建议
-
流程变量管理:
- 避免存储大对象到流程变量
- 对频繁访问的变量启用缓存
-
批量操作:
java复制// 批量完成任务
List<Task> tasks = taskService.createTaskQuery()
.taskCandidateGroup("deptLeader")
.list();
tasks.forEach(task -> {
taskService.complete(task.getId());
});
- 异步处理:
properties复制# application.properties
flowable.async-executor-activate=true
flowable.async-executor-threads=10
9. 扩展功能实现
9.1 加签功能
java复制public void addSigner(String taskId, String userId) {
Task task = taskService.createTaskQuery()
.taskId(taskId)
.singleResult();
// 创建新任务
Task newTask = taskService.newTask();
newTask.setAssignee(userId);
newTask.setParentTaskId(taskId);
newTask.setName(task.getName() + "(加签)");
taskService.saveTask(newTask);
// 原任务挂起
taskService.suspendTask(taskId);
}
9.2 流程版本控制
java复制// 部署新版本
repositoryService.createDeployment()
.addClasspathResource("processes/leave_v2.bpmn")
.name("请假流程V2")
.deploy();
// 自动迁移运行中实例
runtimeService.createProcessInstanceMigrationBuilder()
.migrateToProcessDefinition("leaveProcess:2");
10. 安全注意事项
-
权限校验:
- 所有流程操作接口必须校验当前用户权限
- 防止越权操作其他用户的流程实例
-
数据过滤:
java复制// 查询时添加租户过滤
taskService.createTaskQuery()
.taskCandidateOrAssigned(userId)
.processInstanceTenantId(tenantId)
.list();
- 防注入处理:
- 所有流程变量值需进行XSS过滤
- 审批意见内容需转义特殊字符
在实现过程中发现,Jeecgboot的表单设计器与Flowable的表单属性需要特别注意数据类型匹配问题。实际测试时,日期类型的表单字段在流程变量中会自动转为Timestamp格式,前端显示时需要特别处理格式转换。
