SpringBoot + Activiti7 工作流之第 1 篇 —— 【基础篇】认识 Activiti,创建 Activiti 25 张表,流程设计器使用
SpringBoot + Activiti7 工作流之第 2 篇 —— 【基础篇】Activiti 流程定义的相关操作
SpringBoot + Activiti7 工作流之第 3 篇 —— 【进阶篇】流程实例、个人任务
SpringBoot + Activiti7 工作流之第 4 篇 —— 【进阶篇】流程变量
SpringBoot + Activiti7 工作流之第 5 篇 —— 【进阶篇】组任务
SpringBoot + Activiti7 工作流之第 6 篇 —— 【进阶篇】网关
SpringBoot + Activiti7 工作流之第 7 篇 —— 整合 SpringSecurity 项目实战(完结)
从微服务迁移到工作流的经验之谈
Activiti7 工作流之第 7 篇 —— 整合 SpringSecurity 项目实战(完结)
技术选型:SpringBoot2.0+、Activiti7、SpringSecurity、Mybatis、MySQL。
本篇博客代码地址(含全部数据库表):
代码地址:https://pan.baidu.com/s/1RNgxtVgKkv1pnIfBjyZWMA 提取码:pyuy
自动生产activiti7表
1、新建数据库
2、配置activiti.cfg.xml,执行testCreateDbTable 方法
在 test 包下找到 test01 运行 testCreateDbTable 测试方法即可自动生成 Activiti 的 25 张表,需要配置数据库的连接信息:activiti.cfg.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/contex
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
<!-- 默认id对应的值 为processEngineConfiguration -->
<!-- processEngine Activiti的流程引擎 -->
<bean id="processEngineConfiguration"
class="org.activiti.engine.impl.cfg.StandaloneProcessEngineConfiguration">
<property name="jdbcDriver" value="com.mysql.jdbc.Driver"/>
<property name="jdbcUrl" value="jdbc:mysql://127.0.0.1:3306/study_activiti?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC"/>
<property name="jdbcUsername" value="root"/>
<property name="jdbcPassword" value="root"/>
<!-- activiti数据库表处理策略:自动更新 -->
<property name="databaseSchemaUpdate" value="true"/>
</bean>
</beans>
3、初始化用户表
这里罗列出 4 张业务表:并且有了 user 的测试数据。zhangsan -> lisi -> wangwu -> zhuliu (这是审批流程,密码都是 123)
DROP TABLE IF EXISTS `t_apply_leave`;
CREATE TABLE `t_apply_leave` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`apply_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '请假标题',
`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户',
`day` double(4, 1) NULL DEFAULT NULL COMMENT '请假天数,保留1位小数',
`begin_date` date NULL DEFAULT NULL COMMENT '开始日期',
`end_date` date NULL DEFAULT NULL COMMENT '结束日期',
`reason` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '请假事由',
`state` int(11) NULL DEFAULT NULL COMMENT '状态:0=开始录入,1=开始审批,2=审批完成',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for t_flow
-- ----------------------------
DROP TABLE IF EXISTS `t_flow`;
CREATE TABLE `t_flow` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`flow_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '部署的名称',
`flow_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '部署的key',
`file_path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '文件路径',
`state` int(11) NULL DEFAULT NULL COMMENT '部署状态:1=已部署,0=未部署',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '部署时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of t_flow
-- ----------------------------
INSERT INTO `t_flow` VALUES (1, '请假申请流程', 'applyLeave', 'bpmn/applyLeave.bpmn', 0, '2021-06-15 14:28:49');
-- ----------------------------
-- Table structure for t_site_message
-- ----------------------------
DROP TABLE IF EXISTS `t_site_message`;
CREATE TABLE `t_site_message` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户',
`type` int(11) NULL DEFAULT NULL COMMENT '类型:1=待办任务',
`content` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '消息内容',
`read_flag` int(11) NULL DEFAULT NULL COMMENT '是否已读:1=已读,0=未读',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
`update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for t_user
-- ----------------------------
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID,也是用户ID',
`username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户名',
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '密码',
`enabled` bit(1) NULL DEFAULT b'1' COMMENT '是否生效,1=true,0=false',
`account_non_expired` bit(1) NULL DEFAULT b'1' COMMENT '是否未过期,1=true,0=false',
`account_non_locked` bit(1) NULL DEFAULT b'1' COMMENT '是否未锁定,1=true,0=false',
`credentials_non_expired` bit(1) NULL DEFAULT b'1' COMMENT '证书是否未过期,1=true,0=false',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of t_user
-- ----------------------------
INSERT INTO `t_user` VALUES (1, 'admin', '202cb962ac59075b964b07152d234b70', b'1', b'1', b'1', b'1');
INSERT INTO `t_user` VALUES (2, 'zhangsan', '202cb962ac59075b964b07152d234b70', b'1', b'1', b'1', b'1');
INSERT INTO `t_user` VALUES (3, 'lisi', '202cb962ac59075b964b07152d234b70', b'1', b'1', b'1', b'1');
INSERT INTO `t_user` VALUES (4, 'wangwu', '202cb962ac59075b964b07152d234b70', b'1', b'1', b'1', b'1');
INSERT INTO `t_user` VALUES (5, 'zhuliu', '202cb962ac59075b964b07152d234b70', b'1', b'1', b'1', b'1');
Activiti7 发布正式版之后,它与SpringBoot2.x已经完全支持整合开发。注意,Activiti 默认使用 SpringSecurity 整合开发的。
查看 Activiti 的版本:https://mvnrepository.com/artifact/org.activiti/activiti-engine
在工程的pom.xml文件中引入相关的依赖,其中activiti的依赖是:activiti-spring-boot-starter
完整代码:
1、pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.study</groupId>
<artifactId>study-activiti</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.8.RELEASE</version>
</parent>
<dependencies>
<!--Spring boot 集成包-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- SpringBoot整合Web组件 -->
<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>
<!-- 阿里巴巴的druid数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.20</version>
</dependency>
<!-- 阿里巴巴 fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.75</version>
</dependency>
<!-- 该 starter 会扫描配置文件中的 DataSource,然后自动创建使用该 DataSource 的 SqlSessionFactoryBean,并注册到 Spring 上下文中 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
<!-- 数据库MySQL 依赖包 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.48</version>
</dependency>
<!-- mybatis 依赖包 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>
<!-- lombok 依赖 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--添加 Activiti工作流的支持-->
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-spring-boot-starter</artifactId>
<version>7.0.0.Beta2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
2、bootstrap.yml 配置:
server:
port: 80
# 将SpringBoot项目作为单实例部署调试时,不需要注册到注册中心
eureka:
client:
fetch-registry: false
register-with-eureka: false
spring:
application:
name: activiti-server
datasource:
# 使用阿里巴巴的 druid 数据源
druid:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/study_activiti?characterEncoding=utf-8
username: root
password: root
# Activiti 配置
activiti:
#1.flase:默认值。activiti在启动时,对比数据库表中保存的版本,如果没有表或者版本不匹配,将抛出异常
#2.true: activiti会对数据库中所有表进行更新操作。如果表不存在,则自动创建
#3.create_drop: 在activiti启动时创建表,在关闭时删除表(必须手动关闭引擎,才能删除表)
#4.drop-create: 在activiti启动时删除原来的旧表,然后在创建新表(不需要手动关闭引擎)
database-schema-update: true
#检测历史表是否存在 activiti7 默认没有开启数据库历史记录 启动数据库历史记录
db-history-used: true
#记录历史等级 可配置的历史级别有none, activity, audit, full
#none:不保存任何的历史数据,因此,在流程执行过程中,这是最高效的。
#activity:级别高于none,保存流程实例与流程行为,其他数据不保存。
#audit:除activity级别会保存的数据外,还会保存全部的流程任务及其属性。audit为history的默认值。
#full:保存历史数据的最高级别,除了会保存audit级别的数据外,还会保存其他全部流程相关的细节数据,包括一些流程参数等。
history-level: full
#校验流程文件,默认校验resources下的processes文件夹里的流程文件
check-process-definitions: false
3 mybatis 配置
mybatis:
# Mybatis扫描的mapper文件
mapper-locations: classpath:mapper/*.xml
# 流程定义的配置,实际业务应该是让公司的管理员进行管理流程,然后通过上传文件来处理,目前写固定
process:
flowName: 请假申请流程
key: applyLeave
filePath: bpmn/applyLeave.bpmn
说明:实际项目中,bpmn 文件应该是通过文件服务器上传、读取。这里为了方便,直接读取 resources 的目录。(其实默认是在数据库已经)
4、集成了 SpringSecurity 安全框架
因为 Activiti7 与 SpringBoot整合后,默认情况下,集成了 SpringSecurity 安全框架,这样我们就要去准备 SpringSecurity 整合进来的相关用户权限配置信息。
SpringBoot的依赖包已经将SpringSecurity的依赖包也添加进项目中。
SecurityConfig 配置类:注意要有 @EnableWebSecurity 注解
package com.study.config;
import com.study.handler.MyAuthenticationFailureHandler;
import com.study.handler.MyAuthenticationSuccessHandler;
import com.study.service.MyUserDetailService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.util.DigestUtils;
/**
* @author biandan
* @description
* @signature 让天下没有难写的代码
* @create 2021-05-30 下午 9:38
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyAuthenticationSuccessHandler successHandler;
@Autowired
private MyAuthenticationFailureHandler failureHandler;
@Autowired
private MyUserDetailService myUserDetailService;
// 配置认证用户信息和权限
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//从数据库中获取数据,并校验密码是否一致
auth.userDetailsService(myUserDetailService).passwordEncoder(myPasswordEncoder());
}
/**
* 配置放行的请求
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web)throws Exception{
web.ignoring().antMatchers("/css/**");
web.ignoring().antMatchers("/js/**");
web.ignoring().antMatchers("/img/**");
web.ignoring().antMatchers("/plugins/**");
web.ignoring().antMatchers("/login.html");
}
// 配置拦截请求资源
@Override
protected void configure(HttpSecurity http) throws Exception {
//其他任何路径都需要管理员登录
http.authorizeRequests().
antMatchers("/**").
access("hasRole('ADMIN')");
//登录相关配置
http.formLogin()
.loginPage("/login.html") //指定登录地址
.loginProcessingUrl("/login") //指定处理登录的请求地址
.successHandler(successHandler) //登录成功的回调
.failureHandler(failureHandler); //登录失败的回调
//登出配置
http.logout().
logoutUrl("/logout"). //登出地址为/logout
invalidateHttpSession(true); //并且登出后销毁session
//设置用户只允许在一处登录,在其他地方登录则挤掉已登录用户,被挤掉的已登录用户则需要返回/login.html重新登录
http.sessionManagement().maximumSessions(1).expiredUrl("/login.html");
//关闭CSRF安全策略
http.csrf().disable();
//允许跳转显示iframe
http.headers().frameOptions().disable();
//异常处理页面,例如没有权限访问等
http.exceptionHandling().accessDeniedPage("/error.html");
}
//自定义加密方式
@Bean
PasswordEncoder myPasswordEncoder() {
PasswordEncoder passwordEncoder = new PasswordEncoder() {
//重写加密方法
@Override
public String encode(CharSequence charSequence) {
String md5Pwd = DigestUtils.md5DigestAsHex(((String) charSequence).getBytes());
return md5Pwd;
}
/**
* 判断密码是否匹配
*
* @param charSequence 请求的密码
* @param sqlPwd 数据库密码
* @return
*/
@Override
public boolean matches(CharSequence charSequence, String sqlPwd) {
String reqPwd = encode(charSequence);
boolean result = reqPwd.equals(sqlPwd);
System.out.println("matches reqPwd=" + reqPwd + ";sqlPwd=" + sqlPwd + ",匹配结果:result=" + result);
return result;//只有返回 true 的情况才会验证成功
}
};
return passwordEncoder;
}
}
我们创建一个工具类:SpringContextUtil,用于获取 Spring 容器的 Bean:
package com.study.util;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
/**
* @author biandan
* @description Spring 上下文工具,可用于获取Spring容器的Bean
* @signature 让天下没有难写的代码
* @create 2021-06-14 下午 9:55
*/
@Component
public class SpringContextUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
/**
* 获取 Spring 容器中的 Bean,通过 bean 名称获取
* @param beanName
* @return 返回 Object,需要做强制类型转换
*/
public static Object getBean(String beanName){
return applicationContext.getBean(beanName);
}
/**
* 获取 Spring 容器中的 Bean,通过 bean 类型获取
* @param beanClass
* @param <T> 返回指定类型的 bean 实例
* @return
*/
public static <T> T getBean(Class<T> beanClass){
return applicationContext.getBean(beanClass);
}
/**
* 获取 Spring 容器中的 Bean,通过 bean 类型获取
* @param beanName 名称
* @param beanClass bean 类型
* @param <T> 返回泛型T,需要强转
* @return
*/
public static <T> T getBean(String beanName,Class<T> beanClass){
return applicationContext.getBean(beanName,beanClass);
}
}
5、applyLeave.xml 代码:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:activiti="http://activiti.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" xmlns:tns="http://www.activiti.org/test" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" expressionLanguage="http://www.w3.org/1999/XPath" id="m1623737360299" name="" targetNamespace="http://www.activiti.org/test" typeLanguage="http://www.w3.org/2001/XMLSchema">
<process id="applyLeave" isClosed="false" isExecutable="true" name="请假申请流程" processType="None">
<startEvent id="_2" name="流程开始"/>
<endEvent id="_3" name="流程结束">
<extensionElements>
<activiti:executionListener class="com.study.listener.MyExecutionListener" event="end"/>
</extensionElements>
</endEvent>
<userTask activiti:assignee="${assignee0}" activiti:exclusive="true" id="_4" name="创建请假申请">
<extensionElements>
<activiti:taskListener class="com.study.listener.MyTaskListener" event="create"/>
</extensionElements>
</userTask>
<userTask activiti:assignee="${assignee1}" activiti:exclusive="true" id="_5" name="主管审批">
<extensionElements>
<activiti:taskListener class="com.study.listener.MyTaskListener" event="create"/>
</extensionElements>
</userTask>
<sequenceFlow id="_6" sourceRef="_2" targetRef="_4"/>
<sequenceFlow id="_7" sourceRef="_4" targetRef="_5"/>
<userTask activiti:assignee="${assignee2}" activiti:exclusive="true" id="_8" name="经理审批">
<extensionElements>
<activiti:taskListener class="com.study.listener.MyTaskListener" event="create"/>
</extensionElements>
</userTask>
<sequenceFlow id="_9" sourceRef="_5" targetRef="_8"/>
<exclusiveGateway gatewayDirection="Unspecified" id="_10" name="排他网关"/>
<sequenceFlow id="_11" sourceRef="_8" targetRef="_10"/>
<sequenceFlow id="_12" sourceRef="_10" targetRef="_3">
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${applyLeave.day<=3}]]></conditionExpression>
</sequenceFlow>
<userTask activiti:assignee="${assignee3}" activiti:exclusive="true" id="_13" name="人资审批">
<extensionElements>
<activiti:taskListener class="com.study.listener.MyTaskListener" event="create"/>
</extensionElements>
</userTask>
<sequenceFlow id="_14" sourceRef="_10" targetRef="_13">
<conditionExpression xsi:type="tFormalExpression"><![CDATA[${applyLeave.day>3}]]></conditionExpression>
</sequenceFlow>
<sequenceFlow id="_15" sourceRef="_13" targetRef="_3"/>
</process>
<bpmndi:BPMNDiagram documentation="background=#FFFFFF;count=1;horizontalcount=1;orientation=0;width=842.4;height=1195.2;imageableWidth=832.4;imageableHeight=1185.2;imageableX=5.0;imageableY=5.0" id="Diagram-_1" name="New Diagram">
<bpmndi:BPMNPlane bpmnElement="applyLeave">
<bpmndi:BPMNShape bpmnElement="_2" id="Shape-_2">
<omgdc:Bounds height="32.0" width="32.0" x="285.0" y="20.0"/>
<bpmndi:BPMNLabel>
<omgdc:Bounds height="32.0" width="32.0" x="0.0" y="0.0"/>
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="_3" id="Shape-_3">
<omgdc:Bounds height="32.0" width="32.0" x="285.0" y="500.0"/>
<bpmndi:BPMNLabel>
<omgdc:Bounds height="32.0" width="32.0" x="0.0" y="0.0"/>
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="_4" id="Shape-_4">
<omgdc:Bounds height="55.0" width="85.0" x="260.0" y="90.0"/>
<bpmndi:BPMNLabel>
<omgdc:Bounds height="55.0" width="85.0" x="0.0" y="0.0"/>
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="_5" id="Shape-_5">
<omgdc:Bounds height="55.0" width="85.0" x="260.0" y="175.0"/>
<bpmndi:BPMNLabel>
<omgdc:Bounds height="55.0" width="85.0" x="0.0" y="0.0"/>
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="_8" id="Shape-_8">
<omgdc:Bounds height="55.0" width="85.0" x="260.0" y="265.0"/>
<bpmndi:BPMNLabel>
<omgdc:Bounds height="55.0" width="85.0" x="0.0" y="0.0"/>
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="_10" id="Shape-_10" isMarkerVisible="false">
<omgdc:Bounds height="32.0" width="32.0" x="285.0" y="355.0"/>
<bpmndi:BPMNLabel>
<omgdc:Bounds height="32.0" width="32.0" x="0.0" y="0.0"/>
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="_13" id="Shape-_13">
<omgdc:Bounds height="55.0" width="85.0" x="385.0" y="415.0"/>
<bpmndi:BPMNLabel>
<omgdc:Bounds height="55.0" width="85.0" x="0.0" y="0.0"/>
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge bpmnElement="_12" id="BPMNEdge__12" sourceElement="_10" targetElement="_3">
<omgdi:waypoint x="301.0" y="387.0"/>
<omgdi:waypoint x="301.0" y="500.0"/>
<bpmndi:BPMNLabel>
<omgdc:Bounds height="-6.0" width="0.0" x="0.0" y="-56.0"/>
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="_15" id="BPMNEdge__15" sourceElement="_13" targetElement="_3">
<omgdi:waypoint x="426.0" y="470.0"/>
<omgdi:waypoint x="426.0" y="515.0"/>
<omgdi:waypoint x="316.9687194226713" y="515.0"/>
<bpmndi:BPMNLabel>
<omgdc:Bounds height="0.0" width="0.0" x="0.0" y="0.0"/>
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="_14" id="BPMNEdge__14" sourceElement="_10" targetElement="_13">
<omgdi:waypoint x="317.0" y="371.0"/>
<omgdi:waypoint x="425.0" y="415.0"/>
<omgdi:waypoint x="425.0" y="415.0"/>
<bpmndi:BPMNLabel>
<omgdc:Bounds height="0.0" width="10.0" x="0.0" y="11.0"/>
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="_6" id="BPMNEdge__6" sourceElement="_2" targetElement="_4">
<omgdi:waypoint x="301.0" y="52.0"/>
<omgdi:waypoint x="301.0" y="90.0"/>
<bpmndi:BPMNLabel>
<omgdc:Bounds height="0.0" width="0.0" x="0.0" y="0.0"/>
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="_7" id="BPMNEdge__7" sourceElement="_4" targetElement="_5">
<omgdi:waypoint x="302.5" y="145.0"/>
<omgdi:waypoint x="302.5" y="175.0"/>
<bpmndi:BPMNLabel>
<omgdc:Bounds height="0.0" width="0.0" x="0.0" y="0.0"/>
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="_9" id="BPMNEdge__9" sourceElement="_5" targetElement="_8">
<omgdi:waypoint x="302.5" y="230.0"/>
<omgdi:waypoint x="302.5" y="265.0"/>
<bpmndi:BPMNLabel>
<omgdc:Bounds height="0.0" width="0.0" x="0.0" y="0.0"/>
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="_11" id="BPMNEdge__11" sourceElement="_8" targetElement="_10">
<omgdi:waypoint x="301.0" y="320.0"/>
<omgdi:waypoint x="301.0" y="355.0"/>
<bpmndi:BPMNLabel>
<omgdc:Bounds height="0.0" width="0.0" x="0.0" y="0.0"/>
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</definitions>
流程图:
项目演示效果:http://127.0.0.1/
1、使用其中一个账号(目前都是管理员权限)在【流程管理】菜单里,部署【请假申请流程】。
2、然后切换到 lisi 的账号进行审批
3、然后切换到 wangwu 的账号审批。因为请假天数小于 3 天,所以直接审批结束。
4、最后切换回 zhangsan 的账号查看。
OK,Activiti 学习到这。
[
](https://blog.csdn.net/BiandanLoveyou/article/details/117909703)