Camunda实战教程之员工请假流程
本文中使用的Camunda版本为7.17。
本文内容以员工请假流程为例子对Camunda的整合使用进行说明,涉及基础组件,但并不完全,不然篇幅就过大了。
目的是为了让对Camunda工作流感兴趣却又无从下手的同志,花1小时的时间了解其工作方式,快速判断是否适合自己。
环境准备
官网:camunda.com/
中文站点:camunda-cn.shaochenfeng.com/
流程绘制器下载:camunda.com/download/mo…
流程管理平台下载:camunda.com/download/pl…
重点说明一下流程绘制器和流程管理平台:
流程绘制器
是一个可视化的流程图绘制工具,Camunda提供了支持多平台的软件下载链接,按需下载,启动后如下所示:
实战中我们选择的是Camunda Platform 7的BPMN diagram,这表示Camunda版本为7的BPMN绘图。至于Camunda8他有些组件是商业授权,所以先不考虑这个了。
在流程绘制器里面作图,仍然需要一些学习成本,因为绘制器包含的组件太多,所以本文仅对场景设计所涉及的组件进行说明,有需要可以自行查看这方面的教程。
流程管理平台
我们在流程绘制器作好图之后,就可以把他部署到Camunda引擎中,这一步叫:部署流程,部署流程就是将流程推送到流程管理平台上面,这种推送有很多种方式,最简单的我们可以通过流程绘制器下面的按钮部署:
这里要提醒一下,Camunda多数是英文界面,有需要可以打中文补丁。
可以看到上图部署的路径是:http://localhost:8888/engine-rest,这就是流程管理平台,流程管理平台的启动有三种方式:
方式一:下载到本地后执行启动脚本
在上面的流程管理平台下载链接下载后,下载解压后,执行.bat或.sh启动,
默认账密是:demo/demo
方式二:Docker
# 拉取镜像
docker pull camunda/camunda-bpm-platform:7.17.0
# 启动容器
docker run -d --name camunda -p 8080:8080 ccamunda/camunda-bpm-platform:7.17.0
方式三:Spring Boot
pom.xml:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.3.4.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.camunda.bpm</groupId>
<artifactId>camunda-bom</artifactId>
<version>7.17.0</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!--springboot启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.camunda.bpm.springboot</groupId>
<artifactId>camunda-bpm-spring-boot-starter-rest</artifactId>
</dependency>
<dependency>
<groupId>org.camunda.bpm.springboot</groupId>
<artifactId>camunda-bpm-spring-boot-starter-webapp</artifactId>
</dependency>
<dependency>
<groupId>org.camunda.bpm</groupId>
<artifactId>camunda-engine-plugin-spin</artifactId>
</dependency>
<dependency>
<groupId>org.camunda.spin</groupId>
<artifactId>camunda-spin-dataformat-all</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- Spring-data-jpa依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!--MySQL数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
<!--使用阿里云的Maven源-->
<repositories>
<repository>
<id>aliyunmaven</id>
<name>aliyun</name>
<url>https://maven.aliyun.com/repository/public</url>
</repository>
</repositories>
application.yml:
spring.datasource.url: jdbc:h2:file:./camunda-h2-database
camunda.bpm.admin-user:
id: demo
password: demo
server:
port: 8888
改用MySQL:
spring:
datasource:
url: jdbc:mysql://mylocalhost:3306/camunda?createDatabaseIfNotExist=true&character_set_server=utf8mb4&useSSL=false&serverTimezone=Asia/Shanghai&allowMultiQueries=true
username: root
password: root
一般不会使用h2数据库,所以直接配置MySQL了,记得提前创建好数据库:camunda
启动项目后,会自动在数据库中建表,然后同样访问:http://localhost:8888 即可。
结论
不管用什么方式部署,只要流程管理平台起来了,就能访问链接,通过默认账密:demo/demo进入:
上图中Cockpit是流程信息,部署的流程、流程实例等都在这里查看;
Tasklist的任务列表,顾名思义与当前用户有关的流程任务在这里显示;
Admin是用户、组、租户等这些授权相关,具体后面再说。
本文采用方式三的方式部署流程管理平台,更灵活,也比较实际一些,因为项目开发时肯定要和系统业务作关联嘛。
实战场景:员工请假流程
**场景说明:**员工发起请假申请,天数<=1天的由组长审批,天数>1天并且<=3天的由经理审批,大于3天的由总经理审批,并且审批完成后抄送给人事做记录。
现在打开绘图工具,作图。
创建Camunda7的BPMN diagram图:
先给出完整的流程图做参考:
流程图信息
在空白处点击一下,然后右侧的框框中把流程图的名字填好:
- Name:表示该流程显示的名称,可以用中文加强可读性。
- ID:该流程的唯一标识,默认会生成一个随机字符串,我们改成leave_flow即请假流程,这个ID等会要用来部署流程的。
发起人
点击开始圆圈,在下图箭头处填写发起人名称,这里为:initiator,可以随意修改,这个变量会通过流程路径传递,注意这时候的initiator是一个变量名,不是值,我们在后面创建流程实例的时候会把值传进这个变量。
这个输入框是Initiator,表示发起人。
员工发起请假申请
在下图框框中填写${initiator},这是EL表达式,表示获取该变量的值,结合上面发起人可以明白这里是通过表达式获取该变量的值。
这个框是Assignee,表示分配到任务的人,假如我们传进来的值是xiaoming,那么这个任务就是分配给xiaoming这个单一用户的。
任务类型说明
这里需要再说明一下任务这个组件,这算是一些小细节,需要注意,看下图:
两个红色框框,一个包含任务类型:
- User Task:用户任务,最常见的任务类型,用于分配给特定用户或用户组去完成的人工任务。通常要求用户提供输入、执行某些操作或决策,并将结果反馈到流程引擎。
- Service Task:服务任务,是一个自动执行的任务,通常与外部系统或服务进行集成。可以执行与业务逻辑相关的操作,例如调用REST API、发送电子邮件、生成报告等。可以通过编写适当的代码或调用外部服务来实现服务任务。
员工发起请假申请、领导审批这些都是用户任务,要人工操作;最后抄送人事做记录这个是服务任务,由系统自动执行。
一个是多实例类型:
- 三条竖线:并行任务,当这个任务分配给三个人执行时,这三个人可以并行执行任务,互不影响
- 三条横线:串行任务,当这个任务分配给三个人执行时,这三个人需要顺序执行任务,等上一个人执行完才能接着执行
分配给单一用户的任务不需要设置实例类型,但是当任务分配给多人的时候就需要设置。
请假申请表单
发起申请当然要填写请假信息啦,点击发起申请的任务,选择箭头指的这项,这样就会出现一个Form fields的表单配置:
然后填写表单内容,记得ID和Label不要搞混了,一个是变量名,一个是描述文本,然后是变量类型,一个是long,一个是string:
排他网关
我们创建的这两个表单变量会传递到后面的网关,由网关判断流程路径的走向,在这里我们使用排他网关,就是一个大大的X的菱形组件,排他网关的意思是只会选择一条路径。
我们点击网关连接任务的三条路径线,在右侧的条件中选择Type为Expression的选项,表示执行条件为表达式,然后使用EL表达式填写条件内容,顺便为了流程图好看一点,给路径线加上了一些描述文本,如下图所示:
领导审批
领导审批和员工发起请假申请相似,不同之处在于:
- 多实例:这样可以动态分配给多个领导审批,而不是固定某一个用户,为了动态,我们需要写一个类来处理
- 表单内容:是否批准和评论
多实例
对于三个领导审核的任务,我们首先需要处理多实例部分,如下图:
这里介绍一下每一栏的作用:
- Loop cardinality:循环条件,为什么是循环?因为是多实例,给多个用户操作的,意思为循环分配任务
- Completion condition:跳出条件,当需要循环跳出时,配置这里
上面两个我们没有用到,所以重点关注下面的:
- Collection:循环集合,我们用EL表达式写成:${leaders},我们需要传入这个leaders用户数组对象,但是前面的流程没有这个leaders变量,所以我们要新建一个类来处理
- Element variable:循环元素,每次循环出的对象叫什么变量名
- Assignee:分配到任务的人,这个刚刚写过了,很好理解,用的是传进来的:${leader}
上面这样写,是因为我们希望把领导用户做成动态的,其实我们可以不写Collection,直接在Assignee处赋予用户名称,但这样固定并不是一个好的业务流程。
如图所示,我们在员工发起请假申请到网关的这条路径上,添加一个监听器,监听器的代码为:
import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.camunda.bpm.engine.delegate.ExecutionListener;
@Component("AddLeaderListener")
public class AddLeaderListener implements ExecutionListener {
@Override
public void notify(DelegateExecution execution) throws Exception {
long leaveDay = (long) execution.getVariable("leaveDays");
if (leaveDay < 0) {
throw new RuntimeException("请假天数异常");
}
System.out.println("进入增加领导集合类,员工请假天数:" + leaveDay);
List<String> leaders = new ArrayList<>();
if (leaveDay <= 1) {
leaders.add("ZuZhang");
} else if (leaveDay > 3 && leaveDay <= 5) {
leaders.add("JingLi");
} else if (leaveDay > 5) {
leaders.add("JingLi");
leaders.add("ZongJingLi");
}
// 将leaders数组设置到变量中
execution.setVariable("leaders", leaders);
}
}
监听器的内容为:${AddLeaderListener},表示去找出AddLeaderListener这个类,所以要在Bean注解上面配置正确。
通过监听器之后,Camunda引擎就能流程的知道下一步走向,给到哪些用户,我想通过结合代码的情况,更能体现出Camunda工作流引擎是如何与业务系统做关联的。
领导审批表单
在说明下一步用户模块之前,我们最好先把领导审批的表单做完,步骤同样,增加是否批准和评论:
记得三个任务都要加上,别漏了也别写错了。
虽然给三个任务加上重复的表单很蠢,但是绘制器支持引入外部表单,即写一份表单给多个任务使用,不过本文不扯这么复杂了,先这样顶上。
重点:三个领导审批任务都检查一下,别漏填了,不然后面无法部署流程
用户和组
我们虽然有超级管理员demo,但是还缺少几个用户:
- cc:员工/发起人
- ZuZhang:组长
- JingLing:经理
- ZongJingLi:总经理
添加用户有两种方式:
- 在流程管理平台的Admin中添加
- 通过Camunda引擎的API添加
第一种方式很简单,大家摸索一下就能创建出来,为了体现实战,我们当然是用API啦。
除了用户和组之外,还有一个租户的概念,租户不在本文范畴。
API的使用也不难,这里直接贴出接口代码和接口请求示例:
接口代码
import com.cc.model.GroupParam;
import com.cc.model.UserParam;
import org.camunda.bpm.engine.IdentityService;
import org.camunda.bpm.engine.RuntimeService;
import org.camunda.bpm.engine.identity.Group;
import org.camunda.bpm.engine.identity.User;
import org.camunda.bpm.engine.impl.persistence.entity.GroupEntity;
import org.camunda.bpm.engine.impl.persistence.entity.UserEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class CamundaController {
@Autowired
private IdentityService identityService;
@Autowired
private RuntimeService runtimeService;
// 用户列表
@PostMapping("/user/list")
public List<User> userList() {
return identityService.createUserQuery().list();
}
// 查询用户
@PostMapping("/user/detail/{id}")
public User userDetail(@PathVariable("id") String id) {
return identityService.createUserQuery().userId(id).singleResult();
}
// 添加用户
@PostMapping("/user/create")
public String create(@RequestBody UserParam param) {
final User exist = identityService.createUserQuery().userId(param.getId()).singleResult();
if (exist != null) {
return "该用户已存在:" + param.getId();
}
UserEntity entity = new UserEntity();
entity.setId(param.getId());
entity.setFirstName(param.getFirstName());
entity.setLastName(param.getLastName());
entity.setEmail(param.getEmail());
entity.setPassword(param.getPassword());
identityService.saveUser(entity);
return param.getId();
}
// 修改用户信息
@PostMapping("/user/update")
public String update(@RequestBody UserParam param) {
final User user = identityService.createUserQuery().userId(param.getId()).singleResult();
if (user == null) {
return "该用户不存在:" + param.getId();
}
user.setFirstName(param.getFirstName());
user.setLastName(param.getLastName());
user.setEmail(param.getEmail());
identityService.saveUser(user);
return user.getId();
}
// 修改用户密码
@PostMapping("/user/updatePassword")
public String updatePassword(@RequestBody UserParam param) {
final User user = identityService.createUserQuery().userId(param.getId()).singleResult();
if (user == null) {
return "该用户不存在:" + param.getId();
}
user.setPassword(param.getPassword());
identityService.saveUser(user);
return user.getId();
}
// 删除用户
@PostMapping("/user/delete/{id}")
public String delete(@PathVariable("id") String id) {
identityService.deleteUser(id);
return id;
}
// 组列表
@PostMapping("/group/list")
public List<Group> groupList() {
return identityService.createGroupQuery().list();
}
// 查询组
@PostMapping("/group/detail/{id}")
public Group groupDetail(@PathVariable("id") String id) {
return identityService.createGroupQuery().groupId(id).singleResult();
}
// 添加组
@PostMapping("/group/create")
public String groupCreate(@RequestBody GroupParam param) {
final Group exist = identityService.createGroupQuery().groupId(param.getId()).singleResult();
if (exist != null) {
return "该组已存在:" + param.getId();
}
GroupEntity entity = new GroupEntity();
entity.setId(param.getId());
entity.setName(param.getName());
entity.setType(param.getType());
identityService.saveGroup(entity);
return param.getId();
}
// 修改组信息
@PostMapping("/group/update")
public String groupUpdate(@RequestBody GroupParam param) {
final Group group = identityService.createGroupQuery().groupId(param.getId()).singleResult();
if (group == null) {
return "该组不存在:" + param.getId();
}
group.setName(param.getName());
group.setType(param.getType());
identityService.saveGroup(group);
return group.getId();
}
// 删除组
@PostMapping("/group/delete/{id}")
public String groupDelete(@PathVariable("id") String id) {
identityService.deleteGroup(id);
return id;
}
// 将用户添加到组中
@PostMapping("/user/group/relation/{userId}/{groupId}")
public String userGroupRelation(@PathVariable("userId") String userId,
@PathVariable("groupId") String groupId) {
String error = checkUserGroupExist(userId, groupId);
if (error != null) {
return error;
}
final User exist = identityService.createUserQuery().memberOfGroup(groupId).userId(userId).singleResult();
if (exist != null) {
return "该用户与组已关联";
}
identityService.createMembership(userId, groupId);
return "请求成功";
}
// 从组中删除用户
@PostMapping("/user/group/delete/{userId}/{groupId}")
public String userGroupDelete(@PathVariable("userId") String userId,
@PathVariable("groupId") String groupId) {
String error = checkUserGroupExist(userId, groupId);
if (error != null) {
return error;
}
identityService.deleteMembership(userId, groupId);
return "请求成功";
}
private String checkUserGroupExist(String userId, String groupId) {
final User user = identityService.createUserQuery().userId(userId).singleResult();
final Group group = identityService.createGroupQuery().groupId(groupId).singleResult();
if (userId != null && user == null) {
return "该用户不存在:" + userId;
}
if (groupId != null && group == null) {
return "该组不存在:" + groupId;
}
return null;
}
}
用户请求参数类:
public class UserParam {
// 账号,唯一
private String id;
private String firstName;
private String lastName;
private String email;
// 密码
private String password;
}
组请求参数类:
public class GroupParam {
// 唯一
private String id;
// 组名
private String name;
// 类型,用于角色、部门等分类
private String type;
}
接口请求示例
用户列表:http://localhost:8888/user/list
用户详情:http://localhost:8888/user/detail/cc
添加用户:http://localhost:8888/user/create
{
"id": "cc",
"firstName": "c",
"lastName": "c",
"password": "1"
}
更新用户信息:http://localhost:8888/user/update
{
"id": "cc",
"firstName": "c31232",
"lastName": "c"
}
更新用户密码:http://localhost:8888/user/updatePassword
{
"id": "cc",
"password": "1"
}
删除用户:http://localhost:8888/user/delete/cc
组列表:http://localhost:8888/group/list
组详情:http://localhost:8888/group/detail/td
添加组:http://localhost:8888/group/create
{
"id": "td",
"name": "技术部",
"type": "dept"
}
更新组信息:http://localhost:8888/group/update
{
"id": "td",
"name": "技术部",
"type": "dept"
}
删除组:http://localhost:8888/group/delete/td
将用户添加到组中:http://localhost:8888/user/group/relation/cc/td
将用户从组中删除:http://localhost:8888/user/group/delete/cc/td
创建需要的用户和组
有了上面的基础,我们把用户:cc、ZuZhang、JingLi、ZongJingLi创建好,并添加到组TD中。
抄送人事
当领导审批通过后,抄送给人事告知,这一步不需要人事这个用户操作,所以使用Service Task即业务任务:
别忘了审核不通过的条件:
然后是抄送人事的业务任务代理类:
贴出代码:
@Service("NotifyHRService")
public class NotifyHRService implements JavaDelegate {
@Override
public void execute(DelegateExecution execution) throws Exception {
final String initiator = String.valueOf(execution.getVariable("initiator"));
final long leaveDays = (long) execution.getVariable("leaveDays");
final boolean approve = (boolean) execution.getVariable("approve");
final String comment = String.valueOf(execution.getVariable("comment"));
System.out.println("员工发起请假申请,申请人:" + initiator + ",请假天数:" + leaveDays);
System.out.println("申请是否通过:" + (approve ? "是" : "否"));
System.out.println("上级审批意见:" + comment);
}
}
部署流程
现在开始干正事,把绘制好的流程图部署到流程管理平台上面,部署流程有两种方式:
-
在流程绘制器的下面有个部署按钮,点击deploy即可。
-
把流程图的XML内容拷贝出来,放到Spring Boot项目的/resource/BPMN路径下:如:
/resources /BPMN leave_flow.bpmn
然后启动程序就会自动部署了。
下图所示为流程绘制器的底部:
创建流程实例
虽然我们可以用流程绘制器或者流程管理平台创建一个流程实例,但是照旧使用API的方式去进行,为此我们要写一个接口:
@PostMapping("/start/{processKey}")
public void start(@PathVariable(value = "processKey") String processKey) {
identityService.setAuthenticatedUserId("cc");
final VariableMap variables = Variables.createVariables();
variables.putValue("initiator", "cc");
runtimeService.startProcessInstanceByKey(processKey, variables);
}
- processKey:是我们流程图的唯一标识
- setAuthenticatedUserId:表示流程的启动者、发起人,这可以是管理员或其他用户,因为请假申请人和流程发起人相同,所以这里一样传cc这个用户
- VariableMap:表示流程开始就传入的参数,现在我们把流程图中的initiator设置为cc用户,根据我们绘制的流程图,这会将“发起请假申请”任务分配给这个cc用户
- startProcessInstanceByKey:开始这个流程
调用接口:http://localhost:8888/start/Process_leave_flow
由此即可创建一个流程实例。
待办任务列表
登录cc用户,我们可以在流程管理平台看到Tasklist里面有自己的待办任务,并且很贴心的有一个UI表单可以填写:
但是我们不!我们要用API来做。
@PostMapping("/task/list/{userId}")
public List<TaskDto> taskList(@PathVariable("userId") String userId) {
final List<Task> list = taskService.createTaskQuery()
.taskAssignee(userId).list();
List<TaskDto> dtoList = new ArrayList<>();
for (Task task : list) {
TaskDto dto = new TaskDto();
dto.setId(task.getId());
dto.setName(task.getName());
dto.setAssignee(task.getAssignee());
dto.setCreateTime(task.getCreateTime());
dtoList.add(dto);
}
return dtoList;
}
Camunda的Task类不能直接返回,所以我们要做一个转换类:
public class TaskDto {
private String id;
private String name;
private String assignee;
private Date createTime;
}
调用接口:http://localhost:8888/task/list/cc
返回结果:
[
{
"id": "fb83cd42-2c62-11ee-aaa4-3c7c3fd838f3",
"name": "发起请假申请",
"assignee": "cc",
"createTime": "2023-07-27T09:50:13.000+00:00"
}
]
员工填写请假表单
@PostMapping("/task/complete")
public String complete(@RequestBody HashMap<String, Object> params) {
final String id = String.valueOf(params.get("id"));
final Task task = taskService.createTaskQuery().taskId(id).singleResult();
if (task == null) {
return "该任务不存在";
}
VariableMap variables = Variables.createVariables();
for (String key : params.keySet()) {
Object value = params.get(key);
if (value instanceof Integer) {
variables.putValue(key, Long.parseLong(params.get(key).toString()));
} else {
variables.putValue(key, params.get(key));
}
}
taskService.complete(id, variables);
return "请求成功";
}
因为希望一个接口兼容员工表单、领导审批,所以用HashMap来接收,for循环遍历赋值。
请求数据:
{
"id": "fb83cd42-2c62-11ee-aaa4-3c7c3fd838f3",
"leaveDays": 1,
"reason": "我要请假!!!"
}
id是从待办任务列表里面拿来的
填写成功后,这时候再获取待办任务列表就是空的了。
监听器设置审批领导
员工请假表单提交之后,流程路径到达网管前会触发我们之前写的监听类:AddLeaderListener
从监听类的代码逻辑来看可知,因为请求天数是1天,所以审批人是:ZuZhang
网关根据条件进行流转
这里走流程图的条件,不走代码。
领导审批
请求接口和员工一样,请求体不同:
{
"id": "7ff706f9-2d16-11ee-a86a-3c7c3fd838f3",
"approve": true,
"comment": "准了"
}
抄送人事
因为领导审批通过,所以会经过类:NotifyHRService
这个类会输出一些log,就当抄送人事了。
流程结束,查看历史任务
至此流程结束,我们查看一下用户已完成的历史任务:
@Autowired
private HistoryService historyService;
@PostMapping("/history/list/{userId}")
public List<HistoricTaskInstance> taskList(@PathVariable("userId") String userId) {
return historyService.createHistoricTaskInstanceQuery().taskAssignee(userId).finished().list();
}
调用接口:http://localhost:8888/history/list/cc
可以看到一条。
总结
以上是使用Camunda实现一个员工请假流程的过程,实践完后可以看出其使用难度不高,API丰富易懂,提供的组件也很完整,最重要的是流程可视化,所以有工作流需要的同志可以考虑。
转载自:https://juejin.cn/post/7260763321480380453