UFLO2是一款纯Java流程引擎,它架构于Spring、Hibernate之上,提供诸如并行、动态并行、串行、会签等各种常见及不常见的业务流程流转功能,支持单机或集群部署。它采用全新的基于网页的流程模版设计器,打开网页即可完成流程模版的设计与制作。它是锐道自主研发的一款开源的工作流引擎,采用Apache-2.0开源协议(点击进入gitee源码页面),有兴趣的同学可以download下来进行学习和交流。
笔者是在学习UReport2的过程中发现了该公司的另一个工作流产品UFlo,所以download了源码,并根据个人在学习过程中对部分细节进行了个性化的调整,同时使用springboot+mysql+jfinal+iview+jquery实现了一个简单的请假流程demo,在此做一个简单的学习记录,并同各位同学分享。
先上部分截图展示一下实现结果
在开始项目之前,需要手动创建好uflo工作流需要的数据表,共17张表。
uflo工作流还提供了定时处理流程的功能,该功能需要以下11张表支撑,由于当前demo未涉及定时调用,故此处带过。
为了模拟请假的工作流,还需要创建4张业务支撑表。
其中leave是请假业务表,保存请假业务信息。
sys_dept是部门表,保存公司部门信息。
sys_role是角色表,记录用户的岗位角色。
sys_user是用户表,保存用户信息。
请假流程简述:请假流程模拟公司员工请假审批流程。用户角色分为普通员工,部门主管,部门经理,HR,总经理。通过下图可以清晰看出整个请假流程的设置。
首先创建一个Springboot项目,命名为uflo,并在pom.xml文件中添加需要的maven依赖。由于代码量过多,在此仅对关键部分贴出代码。
<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.0modelVersion>
<groupId>com.crwlgroupId>
<artifactId>ufloartifactId>
<version>1.0-SNAPSHOTversion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.4.0version>
<relativePath/>
parent>
<properties>
<java.version>1.8java.version>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<maven.compiler.source>1.8maven.compiler.source>
<maven.compiler.target>1.8maven.compiler.target>
<spring-cloud.version>2.1.1.RELEASEspring-cloud.version>
<flowable.version>6.5.0flowable.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jdbcartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
<version>1.16.20version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>6.0.2version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>RELEASEversion>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>RELEASEversion>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
<version>4.6.10version>
dependency>
<dependency>
<groupId>com.jfinalgroupId>
<artifactId>activerecordartifactId>
<version>4.8version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.1.6version>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger2artifactId>
<version>2.8.0version>
dependency>
<dependency>
<groupId>org.dom4jgroupId>
<artifactId>dom4jartifactId>
<version>2.1.3version>
dependency>
<dependency>
<groupId>org.aspectjgroupId>
<artifactId>aspectjrtartifactId>
<version>1.8.14version>
dependency>
<dependency>
<groupId>org.aspectjgroupId>
<artifactId>aspectjweaverartifactId>
<version>1.8.14version>
dependency>
<dependency>
<groupId>log4jgroupId>
<artifactId>log4jartifactId>
<version>1.2.17version>
dependency>
<dependency>
<groupId>com.bstek.uflogroupId>
<artifactId>uflo-consoleartifactId>
<version>2.1.6-proversion>
dependency>
dependencies>
project>
此处uflo的jar包依赖为笔者重新修改并编译源码后生成(uflo的源码可以从gitee中下载,搭建好环境后可根据用户自己的需求以及对jar包中存在的bug进行修改),针对本demo有可能无法运行,如有用户需要当前jar包可联系笔者。
applicaiton.yaml
server:
port: 9090
servlet:
context-path: /pro
spring:
profiles:
active: debug
freemarker:
cache: false #页面不加载缓存,修改即时生效
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/uflo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=false&allowMultiQueries=true&pinGlobalTxToPhysicalConnection=true
username: root
password:
initialSize: 1
minIdle: 3
maxActive: 20
# 配置获取连接等待超时的时间
maxWait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 30000
validationQuery: select 'x'
validationQueryTimeout: 3
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
# 打开PSCache,并且指定每个连接上PSCache的大小
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
filters: stat,wall,slf4j
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
type: com.alibaba.druid.pool.DruidDataSource
resources:
static-locations: classpath:/,classpath:/static/
servlet:
multipart:
max-file-size: -1
max-request-size: -1
笔者是放在resources/config目录下面。看过笔者的实现Springboot整合UReport2可能会发现,整合uflo同整合ureport2的配置信息高度相似。因为这两个产品出自同一个公司,其底层实现方式以及实现逻辑基本相似,所以配置方式也相似。
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<import resource="classpath:uflo-console-context.xml" />
<bean id="propertyConfigurer" parent="uflo.props">
<property name="locations">
<list>
<value>classpath:config/config.propertiesvalue>
list>
property>
bean>
<bean id="processTaskDiagramInfoProvider" class="com.crwl.provider.ProcessTaskDiagramInfoProvider">
<property name="disabled" value="${uflo.disableFileDbProvider}">property>
bean>
beans>
config.properties
#流程图物理存放路径
#uflo.defaultFileStoreDir=''
#是否禁用文件配置方式
uflo.disableDefaultFileProcessProvider=true
#是否禁用数据库存放模式
uflo.disableFileDbProvider=false
urflo.contextPath=/pro
Uflo2工作流引擎默认采用文件系统,其中uflo.defaultFileStoreDir是配置生成的流程模板文件的物理路径,uflo.disableDefaultFileProcessProvider是定义是否禁用用当前工作流的文件系统,true为禁用,false为启用。笔者当前的思路是将文件系统同mysql数据库结合起来进行管理,所以配置为禁用。
笔者扩展出来的mysql保存工作流模板文件信息的一种方式,参数uflo.disableFileDbProvider是配置是否禁用。uflo只需要实现uflo的ProcessProvider接口即可扩展工作流引擎模板的保存处理方式。各位同学可以根据自己的需求实现出自己的工作流模板保存处理方式。此demo创建了一张数据表(uflo_model),将模板信息通过实现接口ProcessProvider的ProcessDbStorageProvider类进行持久化处理。对于阅读过实现Springboot整合UReport2的同学可能会发现,Ureport2以及Uflo2都是采用了实现其提供的接口的方式扩展用户自己的数据模板文件的持久方式。
uflo.contextPath参数为笔者添加的参数,此参数可帮助用户实现业务后台系统同前端分离会出现的Uflo工作流访问地址同项目后端访问地址不匹配的情况。当前demo未采用前后端分离,所以此参数可以配置,也可以不配置,不配置默认为application.yaml文件对应的context-path。
package com.crwl.config;
import com.alibaba.druid.filter.logging.Log4jFilter;
import com.alibaba.druid.filter.stat.StatFilter;
import com.alibaba.druid.util.JdbcUtils;
import com.alibaba.druid.wall.WallFilter;
import com.crwl.model._MappingKit;
import com.jfinal.plugin.activerecord.ActiveRecordPlugin;
import com.jfinal.plugin.activerecord.CaseInsensitiveContainerFactory;
import com.jfinal.plugin.activerecord.dialect.MysqlDialect;
import com.jfinal.plugin.druid.DruidPlugin;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy;
import javax.sql.DataSource;
/**
* @author teamo
* @Package com.crwl.config
* @Description:
* @date 2022-8-17
*/
@Configuration
public class DataSourceConfig {
@Value("${spring.datasource.driver-class-name}")
private String driverClassName;
@Value("${spring.datasource.url}")
private String url;
@Value("${spring.datasource.username}")
private String userName;
@Value("${spring.datasource.password}")
private String password;
@Primary
@Bean("dataSource")
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource(DruidPlugin dp) {
//return new DruidDataSource();
return dp.getDataSource();
}
@Bean
public DruidPlugin createDruidPlugin() {
DruidPlugin dp =new DruidPlugin(url, userName, password);
dp.setDriverClass(driverClassName);
dp.set(20,5,50);
Log4jFilter filter = new Log4jFilter();
filter.setConnectionLogEnabled(false);
filter.setStatementLogEnabled(false);
filter.setStatementExecutableSqlLogEnable(true);
filter.setResultSetLogEnabled(false);
dp.addFilter(new StatFilter());
dp.addFilter(filter);
WallFilter wall = new WallFilter();
wall.setDbType(JdbcUtils.MYSQL);
dp.addFilter(wall);
dp.start();
return dp;
}
/**
* 设置数据源代理
*/
@Bean
public TransactionAwareDataSourceProxy transactionAwareDataSourceProxy(DruidPlugin dp) {
TransactionAwareDataSourceProxy transactionAwareDataSourceProxy = new TransactionAwareDataSourceProxy();
transactionAwareDataSourceProxy.setTargetDataSource(dp.getDataSource());
return transactionAwareDataSourceProxy;
}
/**
* 设置ActiveRecord
*/
@Bean
public ActiveRecordPlugin activeRecordPlugin(DruidPlugin dp) {
//ActiveRecordPlugin arp = new ActiveRecordPlugin(transactionDsProxy);
//DruidPlugin dp = createDruidPlugin();
//dp.start();
ActiveRecordPlugin arp = new ActiveRecordPlugin(dp);
arp.setDialect(new MysqlDialect());
arp.setContainerFactory(new CaseInsensitiveContainerFactory(true));//忽略大小写
arp.setShowSql(true);
arp.setDevMode(true);
arp.getEngine().setToClassPathSourceFactory();
//arp.addSqlTemplate("sql/all.sql");
_MappingKit.mapping(arp);
arp.start();
System.out.println("调用Jfinal ActiveRecordPlugin 成功");
return arp;
}
/**
* 设置事务管理
*/
@Bean
public DataSourceTransactionManager dataSourceTransactionManager(TransactionAwareDataSourceProxy tadp) {
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
dataSourceTransactionManager.setDataSource(tadp);
return dataSourceTransactionManager;
}
}
此类主要用于根据配置文件对数据库链接以及Jfinal的进行初始化工作,同时对外暴露DataSource类供uflo接入数据库。此处有个细节配置,如果用DataSource对象直接初始化ActiveRecordPlugin对象的话,在调试代码的时候,控制台只打印sql语句,但是不打印sql语句传入的参数值,这个给我们debug代码带来非常大困难。经过网上查找资料,使用方法createDruidPlugin()创建了DruidPlugin对象,通过DruidPlugin对象添加Log4jFilter,可以通过Log4j的方式实现让Jfinl同数据交互的时候,打印包含参数值的sql语句。
由于uflo持久层采用的是Hibernate,所以需要提供提供DataSource来注册LocalSessionFactoryBean的实现对象供uflo进行持久化操作。
package com.crwl.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.orm.hibernate5.LocalSessionFactoryBean;
import javax.sql.DataSource;
import java.beans.PropertyVetoException;
import java.io.IOException;
import java.util.Properties;
@Configuration
public class UfloConfig {
@Bean("localSessionFactoryBean")
public LocalSessionFactoryBean localSessionFactoryBean(DataSource dataSource) throws
PropertyVetoException, IOException {
LocalSessionFactoryBean sessionFactoryBean = new LocalSessionFactoryBean();
sessionFactoryBean.setDataSource(dataSource);
sessionFactoryBean.setPackagesToScan("com.bstek.uflo.model*");
Properties prop = new Properties();
prop.put("hibernate.dialect","org.hibernate.dialect.MySQL5Dialect");
prop.put("hibernate.show_sql",true);
prop.put("hibernate.format_sql",true);
prop.put("hibernate.hbm2ddl.auto","update");
prop.put("hibernate.jdbc.batch_size",100);
//prop.put("hibernate.current_session_context_class","jta");
prop.put("hibernate.current_session_context_class","thread");
sessionFactoryBean.setHibernateProperties(prop);
return sessionFactoryBean;
}
}
ProcessDbStorageProvider.java实现了ProcessProvider接口,通过实现方法loadProcess,loadAllProcesses,saveProcess,deleteProcess以达到对流程模板进行增删改查的功能。此实现类同UReport2的ReportProvider 接口类似,都是提供给用户实现自定义维护模板(报表/工作流模板)的途径。
当前demo扩展了一块表uflo_model(取代默认文件系统的方式)来实现工作流模板的维护。
package com.crwl.provider;
import com.bstek.uflo.console.provider.ProcessFile;
import com.bstek.uflo.console.provider.ProcessProvider;
import com.crwl.model.UfloModel;
import com.crwl.service.uflo.UfloModelService;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@Component
public class ProcessDbStorageProvider implements ProcessProvider {
public String prefix="file:";
private boolean disabled = false;
@Autowired
private UfloModelService processModelService;
//载入流程模型
@Override
public InputStream loadProcess(String fileName) {
if (fileName.startsWith(this.prefix)) {
fileName = fileName.substring(this.prefix.length(), fileName.length());
}
UfloModel file = processModelService.findByName(fileName);
return new ByteArrayInputStream(file.getContent().getBytes(Charset.forName("utf-8")));
}
//获取全部流程模型
@Override
public List<ProcessFile> loadAllProcesses() {
List<UfloModel> dbFileList = UfloModel.dao.findAll();
List<ProcessFile> fileList = new ArrayList<ProcessFile>();
for(UfloModel df : dbFileList){
ProcessFile f = new ProcessFile(df.getFileName(), df.getCreateTime());
fileList.add(f);
}
return fileList;
}
//保存流程模型
@Override
public void saveProcess(String fileName, String content) throws DocumentException {
if (fileName.startsWith(this.prefix)) {
fileName = fileName.substring(this.prefix.length(), fileName.length());
}
UfloModel file = processModelService.findByName(fileName);
if(file == null) {
file = new UfloModel();
file.setFileName(getRealName(fileName));
file.setContent(content);
String bizCode = getBizCode(content);
//BizConfig bizConfig = BizConfig.dao.findFirst("select * from biz_config t where t.biz_code=?",bizCode);
//file.setBizCode(bizConfig.getBizCode());
//file.setBizName(bizConfig.getBizName());
file.setProcessName(getProcessName(content));
file.setProcessKey(getProcessKey(content));
file.setDescription(getDescription(content));
file.setCreateTime(new Date());
file.save();
} else {
file.setContent(content);
file.setProcessName(getProcessName(content));
file.setProcessKey(getProcessKey(content));
file.setDescription(getDescription(content));
file.setUpdateTime(new Date());
file.update();
}
}
//删除流程模型
@Override
public void deleteProcess(String fileName) {
if (fileName.startsWith(this.prefix)) {
fileName = fileName.substring(this.prefix.length(), fileName.length());
}
UfloModel file = processModelService.findByName(fileName);
if(null != file){
file.delete();
}
}
@Override
public String getName() {
return "dbstore";
}
@Override
public String getPrefix() {
return prefix;
}
@Override
public boolean support(String fileName) {
return fileName.startsWith(prefix);
}
@Override
public boolean isDisabled() {
return disabled;
}
private String getRealName(String name){
if(name.startsWith(getPrefix())){
return name.substring(name.indexOf(getPrefix())+3);
}
return name;
}
private String getProcessName(String xml) throws DocumentException {
Document doc = DocumentHelper.parseText(xml); // 将字符串转为xml
Element root = doc.getRootElement(); // 获取根节点
System.out.println(root.getName());
return root.attributes().get(0).getValue();
}
private String getProcessKey(String xml) throws DocumentException {
Document doc = DocumentHelper.parseText(xml); // 将字符串转为xml
Element root = doc.getRootElement(); // 获取根节点
System.out.println(root.getName());
return root.attributes().get(1).getValue();
}
private String getBizCode(String xml) throws DocumentException {
Document doc = DocumentHelper.parseText(xml); // 将字符串转为xml
Element root = doc.getRootElement(); // 获取根节点
System.out.println(root.getName());
return root.attributes().get(2).getValue();
}
//获取流程描述
private String getDescription(String xml) throws DocumentException {
Document doc = DocumentHelper.parseText(xml); // 将字符串转为xml
Element root = doc.getRootElement(); // 获取根节点
Element descriptionEl = root.element("description");
if(null != descriptionEl){
return descriptionEl.getText();
}
return "";
}
}
WebEnvironmentProvider.java,该配置类实现了EnvironmentProvider接口。其中getSessionFactory方法为uflo提供Hibernate的SessionFactory对象;getPlatformTransactionManager方法为Hibernate提供PlatformTransactionManager对象来处理数据库事务管理;getLoginUser方法为工作流提供当前流程节点的操作人,当前demo采用ThreadLocal(线程内部的局部变量)对象的方式,将当前节点的处理人传递给uflo工作流引擎。
package com.crwl.provider;
import com.bstek.uflo.env.EnvironmentProvider;
import org.hibernate.SessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.persistence.EntityManagerFactory;
@Component
@EnableTransactionManagement
public class WebEnvironmentProvider implements EnvironmentProvider {
private static ThreadLocal<String> localVar = new ThreadLocal<>();
@Autowired
private SessionFactory sessionFactory;
@Autowired
private EntityManagerFactory entityManagerFactory;
@Override
public SessionFactory getSessionFactory() {
return sessionFactory;
}
@Override
public PlatformTransactionManager getPlatformTransactionManager() {
return new JpaTransactionManager(entityManagerFactory);
}
@Override
public String getCategoryId() {
return null;
}
@Override
public String getLoginUser() {
String loginUser = localVar.get();
if(null==loginUser){
loginUser="";
}
return loginUser;
}
public static void setLoginUser(String loginUser){
localVar.set(loginUser);
}
public static String getLoginUserTest() {
return localVar.get();
}
}
工作流模板Service工具类接口,提供工作流模板的查询,保存,删除以及部署等方法。
package com.crwl.service.uflo;
import com.crwl.model.UfloModel;
import com.crwl.model.UfloProcess;
import com.jfinal.plugin.activerecord.Page;
public interface UfloModelService {
/***
*
* @param currentPage
* @param pageSize
* @param processName 模型名称
* @param processKey 模型key
* @return
*/
public Page<UfloModel> getPageList(Integer currentPage, Integer pageSize, String processName, String processKey);
/***
* 保存流程模型
* @param model
* @return
*/
public void saveUfloModel(UfloModel model) throws Exception;
/***
* 删除流程模型
* @param modelId
* @return
*/
public void deleteUfloModel(Long modelId) throws Exception;
/***
* 根据文件名称获取流程模型
* @param fileName
* @return
*/
public UfloModel findByName(String fileName);
/***
* 根据流程Key获取已经部署的流程实例
* @param currentPage
* @param pageSize
* @param key
* @return
*/
Page<UfloProcess> getDeployProcessPage(Integer currentPage, Integer pageSize, String key);
/**
* 根据流程模型部署一个新版本的流程实例
* @param processModelId
* @return
*/
boolean deployProcess(long processModelId) throws Exception;
}
package com.crwl.service.uflo.impl;
import com.bstek.uflo.command.CommandService;
import com.bstek.uflo.model.ProcessDefinition;
import com.bstek.uflo.service.ProcessService;
import com.crwl.annation.UfloTransaction;
import com.crwl.model.UfloModel;
import com.crwl.model.UfloProcess;
import com.crwl.service.uflo.UfloModelService;
import com.jfinal.plugin.activerecord.Db;
import com.jfinal.plugin.activerecord.Page;
import com.jfinal.plugin.activerecord.Record;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.ByteArrayInputStream;
import java.nio.charset.Charset;
@Service
public class UfloModelServiceImpl implements UfloModelService {
@Autowired
private ProcessService processService;
@Autowired
private CommandService commandService;
private final String table = "uflo_model";
@Override
public Page<UfloModel> getPageList(Integer currentPage, Integer pageSize, String processName, String processKey) {
StringBuilder sql = new StringBuilder();
sql.append(" from "+table +" t where 1=1 " );
if(StringUtils.isNotEmpty(processName)){
sql.append(" and instr(t.process_name,'"+ processName +"')>0 ");
}
if(StringUtils.isNotEmpty(processKey)){
sql.append(" and instr(t.process_key,'"+ processKey +"')>0 ");
}
Page<UfloModel> pageList = UfloModel.dao.paginate(currentPage,pageSize,"select t.*", sql.toString());
return pageList;
}
@Override
@Transactional(rollbackFor=Exception.class)
public void saveUfloModel(UfloModel model) throws Exception{
String processName = model.getProcessName();
String processKey = model.getProcessKey();
String description = model.getDescription();
String key = model.getProcessKey();
StringBuilder sql = new StringBuilder();
sql.append(" select count(1) count from uflo_model t ");
sql.append(" where t.process_key=? ");
Record rec = Db.findFirst(sql.toString(),key);
if(null != rec && rec.getInt("count")>0){
throw new Exception("流程Key已经存在,请重新设置流程Key");
}
if(StringUtils.isEmpty(processName)){
throw new Exception("流程名称不能为空");
}
if(StringUtils.isEmpty(processKey)){
throw new Exception("流程Key不能为空");
}
model.setFileName(processKey+".uflo.xml");
StringBuilder content = new StringBuilder();
content.append("");
content.append("+processName +"\" key=\""+ processKey +"\"> ");
if(StringUtils.isNotEmpty(description)){
content.append("" +description+"");
}
content.append(" ");
content.append(" ");
model.setContent(content.toString());
model.save();
}
@Override
@Transactional(rollbackFor=Exception.class)
public void deleteUfloModel(Long modelId) throws Exception{
UfloModel model = UfloModel.dao.findById(modelId);
if(null == model){
throw new Exception("流程模型不存");
}
String key = model.getProcessKey();
StringBuilder sql = new StringBuilder();
sql.append(" select count(1) count from uflo_process t ");
sql.append(" inner join uflo_process_instance t1 on t.id_=t1.process_id_ ");
sql.append(" where t.key_=? ");
Record rec = Db.findFirst(sql.toString(),key);
if(null == rec || rec.getInt("count") == 0){
model.delete();
}else{
throw new Exception("流程模型已经部署流程存在正运行的工作记录,不能删除");
}
}
@Override
public UfloModel findByName(String fileName) {
UfloModel result = UfloModel.dao.findFirst("select * from "+table + " t where t.file_name=?",fileName);
return result;
}
@Override
public Page<UfloProcess> getDeployProcessPage(Integer currentPage, Integer pageSize, String key) {
String sql = " from uflo_process t where t.key_=? order by create_date_ desc";
Page<UfloProcess> pageList = UfloProcess.dao.paginate(currentPage,pageSize,"select t.* ",sql,key);
return pageList;
}
@Override
@Transactional(rollbackFor=Exception.class)
@UfloTransaction
public boolean deployProcess(long processModelId) throws Exception {
UfloModel model = UfloModel.dao.findById(processModelId);
if(null ==model ){
throw new Exception("流程模板不存在");
}
if(StringUtils.isEmpty(model.getProcessKey())) {
throw new Exception("流程Key不能为空");
}
if(StringUtils.isEmpty(model.getProcessName())) {
throw new Exception("流程名称不能为空");
}
if(StringUtils.isEmpty(model.getContent())){
throw new Exception("流程模板定义不能为空");
}
ByteArrayInputStream inputStream = new ByteArrayInputStream(model.getContent().getBytes(Charset.forName("utf-8")));
ProcessDefinition definition = processService.deployProcess(inputStream);
//更新最新部署的流程缓存
processService.updateProcessForMemory(definition.getId());
/*ProcessDefinition process=commandService.executeCommand(new GetProcessByKeyCommand(definition.getKey()));
if(process!=null){
CacheService cache= EnvironmentUtils.getEnvironment().getCache();
cache.putProcessDefinition(process.getId(), process);
}*/
return null != definition ? true : false;
}
}
工作流实例Service工具类接口,提供工作流启动实例,查询实例,删除实例,作废实例,获取实例当前任务,获取实例参数等通用方法。
package com.crwl.service.uflo;
import com.bstek.uflo.model.ProcessInstance;
import com.bstek.uflo.model.task.Task;
import com.bstek.uflo.model.variable.Variable;
import com.bstek.uflo.service.StartProcessInfo;
import com.crwl.dto.CurrUser;
import com.crwl.dto.ProcessIntanceDelDto;
import java.util.List;
public interface UfloProcessService {
/***
* 启动流程
* @param info
* @return
*/
ProcessInstance start(StartProcessInfo info, String processKey) throws Exception;
/***
* 删除流程部署的实例,与该实例有关的所有流程记录都将被删除
* @param processId
*/
void deleteProcess(long processId) throws Exception;
/**
* 删除流程实例中一个具体的流程记录
* @param intanceDelDto
*/
void deleteProcessInstance(ProcessIntanceDelDto intanceDelDto) ;
/***
* 根据processInstanceId获取当前新创建的任务
* @param processInstanceId
* @return
*/
Task getTaskByProcessInstanceId(Long processInstanceId);
/**
* 作废流程
* @param processInstanceId
* @param currUser
*/
void voidProcessInst(Long processInstanceId, CurrUser currUser);
/***
* 找到制定流程实例中所有的参数
* @param processInsanceId
* @return
*/
List<Variable> getProcessVariables(long processInsanceId);
}
package com.crwl.service.uflo.impl;
import com.bstek.uflo.model.ProcessDefinition;
import com.bstek.uflo.model.ProcessInstance;
import com.bstek.uflo.model.task.Task;
import com.bstek.uflo.model.task.TaskState;
import com.bstek.uflo.model.variable.Variable;
import com.bstek.uflo.query.TaskQuery;
import com.bstek.uflo.service.ProcessService;
import com.bstek.uflo.service.StartProcessInfo;
import com.bstek.uflo.service.TaskService;
import com.crwl.annation.UfloTransaction;
import com.crwl.dto.CurrUser;
import com.crwl.dto.ProcessIntanceDelDto;
import com.crwl.service.uflo.UfloProcessService;
import com.jfinal.plugin.activerecord.Db;
import com.jfinal.plugin.activerecord.Record;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class UfloProcessServiceImpl implements UfloProcessService {
@Autowired
private ProcessService processService;
@Autowired
private TaskService taskService;
@Override
@Transactional(rollbackFor=Exception.class)
public ProcessInstance start(StartProcessInfo info, String processKey) throws Exception{
//检查表单重复提交
ProcessDefinition process = processService.getProcessByKey(processKey);
String businessId = info.getBusinessId();
//检查当前业务businessId是否存在开启且未结束的流程
String sql = "select count(1) count from uflo_process_instance t where t.business_id_=? and t.process_id_=? ";
Record rec = Db.findFirst(sql, info.getBusinessId(), process.getId());
if (rec.getLong("count") > 0) {
throw new Exception("业务ID[" + businessId + "]禁止重复提交");
}
sql = "select count(1) count from uflo_his_process_instance t where t.business_id_=? and t.process_id_=? ";
rec = Db.findFirst(sql, info.getBusinessId(), process.getId());
if (rec.getLong("count") > 0) {
throw new Exception("业务ID[" + businessId + "]禁止重复提交");
}
//检查流程发起人是否存在
String promoter = info.getPromoter();
if (StringUtils.isEmpty(promoter)) {
throw new Exception("流程发起人不能为空");
}
//检查流程主题是否为空
String subject = info.getSubject();
if (StringUtils.isEmpty(subject)) {
throw new Exception("流程主题不能为空");
}
/**尝试开启流程start**/
ProcessInstance instance = processService.startProcessByKey(processKey, info);
return instance;
}
@Override
@UfloTransaction
public void deleteProcess(long processId) throws Exception{
Record rec = Db.findFirst("select count(1) count from uflo_process_instance t where t.process_id_=?",processId);
if(null == rec || rec.getInt("count")==0){
processService.deleteProcess(processId);
}else{
throw new Exception("当前流程存在正进行的工作流程记录,不能删除流程");
}
}
@Override
@UfloTransaction
public void deleteProcessInstance(ProcessIntanceDelDto processIntanceDelDto) {
Task task = taskService.getTask(processIntanceDelDto.getTaskId());
ProcessInstance instance = processService.getProcessInstanceById(task.getProcessInstanceId());
processService.deleteProcessInstance(instance);
}
@Override
public Task getTaskByProcessInstanceId(Long processInstanceId) {
ProcessInstance instance = processService.getProcessInstanceById(processInstanceId);
String currentTask = instance.getCurrentTask();
String businessId = instance.getBusinessId();
TaskQuery taskQuery = taskService.createTaskQuery();
Task task = taskQuery.nodeName(currentTask).businessId(businessId).processInstanceId(processInstanceId).addTaskState(TaskState.Created).addOrderDesc("id").list().get(0);
return task;
}
@Override
public void voidProcessInst(Long processInstanceId, CurrUser currUser) {
ProcessInstance instance = processService.getProcessInstanceById(processInstanceId);
String currentTask = instance.getCurrentTask();
String businessId = instance.getBusinessId();
TaskQuery taskQuery = taskService.createTaskQuery();
List<Task> taskList = taskQuery.nodeName(currentTask).businessId(businessId).processInstanceId(processInstanceId).addOrderDesc("id").list();
boolean isVoid = false;
for(int i=0; i<taskList.size();i++){
Task task = taskList.get(i);
//如任意未完成的一个任务,并写任务日志(作废操作只能由流程创建人作废)
if(!TaskState.Completed.toString().equals(task.getState()) && !isVoid){
task.setState(TaskState.Canceled);
task.setOpinion(currUser.getUserName() +"作废了业务");
taskService.saveHisTask(task,instance);
isVoid = true;
}
}
//将流程任务删除
Db.update("delete from uflo_task where BUSINESS_ID_=?", businessId);
//将流程实例删除
Db.update("delete from uflo_process_instance where ID_=?",processInstanceId);
}
@Override
public List<Variable> getProcessVariables(long processInsanceId) {
return processService.getProcessVariables(processInsanceId);
}
}
工作流任务Service工具类接口,提供工作流完成任务,查询任务,删除任务,驳回任务,增删改查任务变量以及获取任务审核日期等通用方法。
package com.crwl.service.uflo;
import com.bstek.uflo.query.TaskQuery;
import com.crwl.dto.ProcessRollBackDto;
import com.crwl.dto.ProcessTaskDto;
import com.crwl.dto.UfloTaskLogDto;
import java.util.List;
import java.util.Map;
public interface UfloTaskService {
/**
* 完成一个任务
* @param taskDto
* @return
*/
void complateTask(ProcessTaskDto taskDto) throws Exception;
/**
* 驳回上一节点
* @param rollBackDto
*/
void rollBack(ProcessRollBackDto rollBackDto) throws Exception;
/**
* 驳回指定节点
* @param rollBackDto
*/
void rollBackTargetNode(ProcessRollBackDto rollBackDto) throws Exception;
/**
* 驳回开始节点
* @param rollBackDto
*/
void rollBackStart(ProcessRollBackDto rollBackDto);
/***
* 获取流程实例中一个具体的流程记录实例对应工作流程记录的审核日志
* @param processInstanceId
* @return
*/
List<UfloTaskLogDto> getTaskLogList(long processInstanceId);
/***
* 获取指定流程记录的指定变量的变量值
* @param paramKey
* @param instancesId
* @return
*/
Object getVaribale(String paramKey, long instancesId);
/***
* 删除指定流程记录的指定变量的变量值
* @param paramKey
* @param instancesId
* @return
*/
void deleteVaribale(String paramKey, long instancesId);
/***
* 保存指定流程记录的指定变量的变量值
* @param instancesId
* @param paramKey
* @return
*/
void saveVaribaleVal(long instancesId, String paramKey, Object paramVal);
/***
* 保存指定流程记录的Map变量对象
* @param instancesId
* @param paramMap
* @return
*/
void saveVaribaleMap(long instancesId, Map<String, Object> paramMap);
/***
* 生成一个TaskQuery对象
* @return
*/
TaskQuery generateTaskQuery();
}
package com.crwl.service.uflo.impl;
import cn.hutool.core.util.ArrayUtil;
import com.bstek.uflo.model.ProcessDefinition;
import com.bstek.uflo.model.task.Task;
import com.bstek.uflo.model.task.TaskType;
import com.bstek.uflo.process.node.Node;
import com.bstek.uflo.process.node.StartNode;
import com.bstek.uflo.query.TaskQuery;
import com.bstek.uflo.service.ProcessService;
import com.bstek.uflo.service.TaskOpinion;
import com.bstek.uflo.service.TaskService;
import com.crwl.dto.ProcessRollBackDto;
import com.crwl.dto.ProcessTaskDto;
import com.crwl.dto.UfloTaskLogDto;
import com.crwl.model.SysUser;
import com.crwl.model.UfloTask;
import com.crwl.service.uflo.UfloTaskService;
import com.jfinal.plugin.activerecord.Db;
import com.jfinal.plugin.activerecord.Record;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
public class UfloTaskServiceImpl implements UfloTaskService {
@Autowired
private ProcessService processService;
@Autowired
private TaskService taskService;
@Override
public Object getVaribale(String paramKey,long instancesId) {
Object res = processService.getProcessVariable(paramKey,instancesId);
return res;
}
@Override
public void deleteVaribale(String paramKey, long instancesId) {
processService.deleteProcessVariable(paramKey,instancesId);
}
@Override
public void saveVaribaleVal(long instancesId,String paramKey,Object paramVal) {
processService.saveProcessVariable(instancesId,paramKey,paramVal);
}
@Override
public void saveVaribaleMap(long instancesId, Map<String,Object> paramMap) {
processService.saveProcessVariables(instancesId,paramMap);
}
@Override
@Transactional(rollbackFor=Exception.class)
public void complateTask(ProcessTaskDto taskDto) throws Exception{
//先查找当前任务节点下可指定任务处理人的任务节点名,如果有,返回指定节点和人员
List<String> nodeNames = taskService.getAvaliableAppointAssigneeTaskNodes(taskDto.getTaskId());
if(nodeNames.size()==1){
taskDto.setNextNodeName(nodeNames.get(0));
}
//先判断当前处理任务的用户是否在任务可处理用户集合中
Task task = taskService.getTask(taskDto.getTaskId());
//竞争类型的任务
if(task.getType().equals(TaskType.Participative)){
String appointor = taskDto.getAppointor();
String owners = task.getOwner();
if(StringUtils.isNotEmpty(owners)){
String[] ownerArr = owners.split(",");
if(!Arrays.asList(ownerArr).contains(appointor)){
throw new Exception("任务待处理人中不包含用户["+appointor+"]");
}
}
//认领任务(任务还未认领)
if(StringUtils.isEmpty(task.getAssignee())){
taskService.claim(taskDto.getTaskId(),appointor);
//如果任务已经认领,且处理人和认领人不是同一个人,则抛出异常
}else if(!appointor.equals(task.getAssignee())){
throw new Exception("任务认领人["+ task.getAssignee()+"]不是当前处理人");
}
}
//开始处理任务
if("Created".equals(task.getState().toString())|| "Reserved".equals(task.getState().toString())){
taskService.start(taskDto.getTaskId());
}
//任务如果有设置下个节点的处理人,指定下个节点处理人,
if (StringUtils.isNotEmpty(taskDto.getNextNodeName()) && taskDto.getAssignee() != null) {
String assignee = taskDto.getAssignee();
List<SysUser> users = SysUser.dao.find("select * from sys_user t where t.id in("+ assignee + ") or t.user_code in("+ assignee+")");
List<String> userIds = users.stream().map(e->e.getId().toString()).collect(Collectors.toList());
String[] assigneeIds = ArrayUtil.toArray(userIds,String.class);
taskService.saveTaskAppointor(taskDto.getTaskId(),assigneeIds,taskDto.getNextNodeName());
}
//设置处理意见
TaskOpinion opinion = new TaskOpinion(taskDto.getOpinion());
taskService.complete(taskDto.getTaskId(),taskDto.getVariables(), opinion);
}
@Override
public void rollBack(ProcessRollBackDto rollBackDto) throws Exception{
Long taskId = rollBackDto.getTaskId();
Task task = taskService.getTask(taskId);
//获取该节点第一次执行的任务
Long processInstanceId = task.getProcessInstanceId();
String nodeName = task.getNodeName();
String sql = "select * from uflo_task t where t.process_instance_id_=? and t.node_name_=? order by t.create_date_";
List<UfloTask> taskList = UfloTask.dao.find(sql,processInstanceId,nodeName);
Map variables = rollBackDto.getVariables();
TaskOpinion option = new TaskOpinion(rollBackDto.getOpinion());
if(null != taskList && taskList.size()>0){
UfloTask ufloTask = taskList.get(0);
String preTask = ufloTask.getPrevTask();
taskService.rollback(taskId,preTask,variables,option);
}else{
// 获取上一步节点 这里不能直接取上一步,否则会造成A-B-A的循环驳回
String preTask = task.getPrevTask();
if(StringUtils.isNotEmpty(preTask)){
taskService.rollback(taskId,preTask,variables,option);
}else{
throw new Exception("上一步节点不存在");
}
}
}
@Override
public void rollBackTargetNode(ProcessRollBackDto rollBackDto) throws Exception {
String targetNodeName = rollBackDto.getTargetNodeName();
//校验目标节点是否存在于流程中
Long taskId= rollBackDto.getTaskId();
Map variables = rollBackDto.getVariables();
Task task = taskService.getTask(taskId);
Long processId = task.getProcessId();
ProcessDefinition processDef = processService.getProcessById(processId);
List<Node> nodeList = processDef.getNodes();
if(null == nodeList || nodeList.size()==0){
throw new Exception("流程节点不能为空");
}
boolean isExist = false;
for(int i=0; i<nodeList.size();i++){
Node node = nodeList.get(i);
if(targetNodeName.equals(node.getName())){
isExist = true;
}
}
if(!isExist){
throw new Exception("流程节点不存在");
}
TaskOpinion option = new TaskOpinion(rollBackDto.getOpinion());
taskService.rollback(taskId,targetNodeName,variables,option);
}
@Override
public void rollBackStart(ProcessRollBackDto rollBackDto) {
Long taskId= rollBackDto.getTaskId();
Map variables = rollBackDto.getVariables();
TaskOpinion option = new TaskOpinion(rollBackDto.getOpinion());
StartNode startNode = processService.getProcessById(taskId).getStartNode();
taskService.rollback(taskId,startNode.getName(),variables,option);
}
@Override
public List<UfloTaskLogDto> getTaskLogList(long processInstanceId) {
List<UfloTaskLogDto> resList = new ArrayList<UfloTaskLogDto>();
StringBuilder sql = new StringBuilder();
sql.append(" select t.task_name_ taskName,t1.user_name auditUser, t.end_date_ auditDate,t.opinion_ opinion,t.state_ ");
sql.append(" from uflo_his_task t ");
sql.append(" left join sys_user t1 on t.assignee_ = t1.user_code ");
sql.append(" where t.process_instance_id_=? ");
sql.append(" order by t.id_ asc,t.create_date_ desc ");
List<Record> logList = Db.find(sql.toString(),processInstanceId);
String strDateFormat = "yyyy-MM-dd HH:mm";
SimpleDateFormat sdf = new SimpleDateFormat(strDateFormat);
if(null != logList && logList.size()>0){
for(int i=0; i<logList.size();i++){
Record hTask = logList.get(i);
UfloTaskLogDto logDto = new UfloTaskLogDto();
logDto.setTaskName(hTask.getStr("taskName"));
logDto.setAuditUser(hTask.getStr("auditUser"));
logDto.setAuditDate(null ==hTask.getDate("auditDate")?null:sdf.format(hTask.getDate("auditDate")));
logDto.setOption(hTask.getStr("opinion"));
resList.add(logDto);
}
}
return resList;
}
@Override
public TaskQuery generateTaskQuery() {
return taskService.createTaskQuery();
}
}
Provider类是实现Uflo开放出来的供业务系统实现的接口类,用户可以同时实现不同的Provider类从代码层面去实现用户的业务需求。
此处以demo中用到的两个Provider接口为例简单介绍
用户可以通过实现AssigneeProvier接口以实现在设计流程的时候,以系统不同业务维度给用户提供节点可审核的用户,例如我们指定HR节点的审核人为行政部门的部门经理,那么就可以实现一个接口(按部门+角色)为节点提供审核人。
方法getName,提供为本实现类起一个代办用户的方案名称
方法queryEntities,提供部门+角色的组合列表
方法getUsers,按照节点选中的部门+角色组合获取用户集合获取用户成为该节点的审核人
package com.crwl.provider;
import com.bstek.uflo.env.Context;
import com.bstek.uflo.model.ProcessInstance;
import com.bstek.uflo.process.assign.AssigneeProvider;
import com.bstek.uflo.process.assign.Entity;
import com.bstek.uflo.process.assign.PageQuery;
import com.crwl.model.SysDept;
import com.crwl.model.SysRole;
import com.crwl.model.SysUser;
import com.crwl.service.sys.SysDeptService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
@Component("DeptAndRoleProvider")
public class AssigneeDeptAndRoleProvider implements AssigneeProvider {
@Autowired
private SysDeptService deptService;
@Override
public boolean isTree() {
return false;
}
@Override
public String getName() {
return "按部门+角色";
}
@Override
public void queryEntities(PageQuery<Entity> pageQuery, String parentId) {
List<SysDept> deptList = deptService.getDeptList("no");
List<SysRole> roleList = SysRole.dao.find(" select * from sys_role t where t.is_deleted=? ", 2);
List<Entity> entities = new ArrayList<>();
deptList.forEach(n->{
roleList.forEach(m->{
Entity en = new Entity(n.getDeptCode()+"-"+m.getRoleCode(),n.getDeptName()+"-"+m.getRoleName());
entities.add(en);
});
});
pageQuery.setResult(entities);
pageQuery.setRecordCount(deptList.size());
}
/**
* 按部门取人员
* @param entityId 处理部门Code
* @param context context 流程上下文对象
* @param processInstance 流程实例对象
* @return
*/
@Override
public Collection<String> getUsers(String entityId, Context context, ProcessInstance processInstance) {
String[] arr = entityId.split("-");
List<SysUser> userList = SysUser.dao.find(" select * from sys_user t where t.is_deleted=? and t.dept_code=? and t.role_code like \"%'"+arr[1]+"'%\" ",
0,arr[0]);
List<String> userCodeList = userList.stream().map(s-> s.getUserCode()).collect(Collectors.toList());
return userCodeList;
}
@Override
public boolean disable() {
return false;
}
}
用户可以通过实现TaskDiagramInfoProvider接口以实现当鼠标移动到流程图节点上,按照接口实现方法显示当前节点的审核日志
package com.crwl.provider;
import com.bstek.uflo.diagram.TaskDiagramInfoProvider;
import com.bstek.uflo.diagram.TaskInfo;
import com.crwl.model.SysUser;
import com.jfinal.plugin.activerecord.Db;
import com.jfinal.plugin.activerecord.Record;
import org.apache.commons.lang.StringUtils;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.stream.Collectors;
//翻译流程图任务节点简述信息
public class ProcessTaskDiagramInfoProvider implements TaskDiagramInfoProvider {
private Boolean disabled;
public void setDisabled(Boolean disabled) {
this.disabled = disabled;
}
@Override
public boolean disable() {
return disabled;
}
@Override
public String getInfo(String nodeName, List<TaskInfo> tasks) {
SimpleDateFormat sd=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
StringBuffer sb=null;
if(tasks!=null && tasks.size()>0){
sb=new StringBuffer();
if(tasks.size()>1){
for(int i=0;i<tasks.size();i++){
TaskInfo task=tasks.get(i);
String owners = task.getOwner();
String ownerNames = "";
if(StringUtils.isNotEmpty(owners)){
owners = owners.replaceAll(",","','");
owners = "'"+owners+"'";
List<Record> resList = Db.find("select t.user_name userName from sys_user t where t.user_code in("+ owners+ ") and t.is_deleted=?", 2);
ownerNames = resList.stream().map(rec -> rec.getStr("userName")).collect(Collectors.joining(","));
}
String assignee = task.getAssignee();
String assigneeName = "";
SysUser assUser = SysUser.dao.findFirst("select t.user_name from sys_user t where t.user_code=? and t.is_deleted=?",assignee,2);
if(null != assUser){
assigneeName = assUser.getUserName();
}
String options = task.getOpinion();
sb.append("任务"+(i+1)+":\r");
sb.append("所有人:"+ownerNames+"\r");
sb.append("处理人:"+assigneeName+"\r");
sb.append("创建时间:"+sd.format(task.getCreateDate())+"\r");
if(task.getEndDate()!=null){
sb.append("完成时间:"+sd.format(task.getEndDate())+"\r");
if(StringUtils.isNotEmpty(options)){
sb.append("处理意见:"+options+"\n");
}
}else{
sb.append("完成时间:处理中\r");
}
}
}else{
TaskInfo task=tasks.get(0);
String owners = task.getOwner();
String ownerNames = "";
if(StringUtils.isNotEmpty(owners)){
owners = owners.replaceAll(",","','");
owners = "'"+owners+"'";
List<Record> resList = Db.find("select t.user_name from sys_user t where t.user_code in("+ owners+ ") and t.is_deleted=?",2);
ownerNames = resList.stream().map(rec -> rec.getStr("user_name")).collect(Collectors.joining(","));
}
String assignee = task.getAssignee();
String assigneeName = "";
SysUser assUser = SysUser.dao.findFirst("select t.user_name from sys_user t where t.user_code=? and t.is_deleted=?",assignee,2);
if(null != assUser){
assigneeName = assUser.getUserName();
}
String options = task.getOpinion();
sb.append("所有人:"+ownerNames+"\r");
sb.append("处理人:"+assigneeName+"\r");
sb.append("创建时间:"+sd.format(task.getCreateDate())+"\r");
if(task.getEndDate()!=null){
sb.append("完成时间:"+sd.format(task.getEndDate())+"\r");
if(StringUtils.isNotEmpty(options)){
sb.append("处理意见:"+options+"\n");
}
}else{
sb.append("完成时间:处理中\r");
}
}
}
if(sb!=null){
return sb.toString();
}else{
return null;
}
}
}
Provider类是实现Uflo开放出来的供业务系统实现的接口类,用户可以同时实现不同的Provider类从代码层面去实现用户的业务需求。
此处以demo中用到的两个Provider接口为例简单介绍
用户在设计流程的时候,可以通过实现AssignmentHandler接口实现为节点指定审核人,此接口可以实现通过代码来处理自定义复杂的指定审核人逻辑。
package com.crwl.handler;
import com.bstek.uflo.env.Context;
import com.bstek.uflo.model.ProcessInstance;
import com.bstek.uflo.process.handler.AssignmentHandler;
import com.bstek.uflo.process.node.TaskNode;
import com.crwl.model.BusiLeave;
import com.crwl.model.SysDept;
import com.crwl.model.SysUser;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
@Component("deptManger")
public class DeptMangerHandler implements AssignmentHandler {
@Override
public Collection<String> handle(TaskNode taskNode, ProcessInstance processInstance, Context context) {
String businessId = processInstance.getBusinessId();
String subject = processInstance.getSubject();
BusiLeave leave = BusiLeave.dao.findById(businessId);
String sbUserCode = leave.getSbUserCode();
SysUser sbUser = null;
if("请假流程".equals(subject)){
sbUser = SysUser.dao.findFirst("select * from sys_user t where t.user_code=? and t.is_deleted=?",
sbUserCode, 2);
}
String deptCode = sbUser.getDeptCode();
List<SysUser> userList = SysUser.dao.find("select * from sys_user t where t.dept_code=? and t.role_name_list like \"%部门经理%\" ",deptCode);
//如果在当前部门找不到部门经理,则从当前部门的上级部门找部门经理(当前仅支持二级部门)
if(null == userList || userList.size()==0){
SysDept dept = SysDept.dao.findFirst("select * from sys_dept t where t.dept_code=? ",deptCode);
String parentDeptCode = dept.getParentCode();
if(null != dept && !"0".equals(parentDeptCode)){
userList = SysUser.dao.find("select * from sys_user t where t.dept_code=? and t.role_name_list like \"%部门经理%\" ",parentDeptCode);
}
}
List<String> userCodeList = null;
if(null !=userList && userList.size()>0){
userCodeList = userList.stream().map(s->s.getUserCode()).collect(Collectors.toList());
}
return userCodeList;
}
@Override
public String desc() {
return "获取部门经理岗";
}
}
用户在设计流程的时候,可以通过实现DecisionHandler为指定的决策节点提供决策流向的特定逻辑。此demo为根据申报用户自身的角色来指定申报应该流向哪个节点。
package com.crwl.handler;
import com.bstek.uflo.env.Context;
import com.bstek.uflo.model.ProcessInstance;
import com.bstek.uflo.process.handler.DecisionHandler;
import com.crwl.enums.FlowStatusEnum;
import com.crwl.model.BusiLeave;
import com.crwl.model.SysUser;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Component;
@Component("leaveSbDecision")
public class LeaveSbDecisionHandler implements DecisionHandler {
@Override
public String handle(Context context, ProcessInstance processInstance) {
String businessId = processInstance.getBusinessId();
BusiLeave leave = BusiLeave.dao.findById(businessId);
leave.setStatus(FlowStatusEnum.YES_DECLARE.getCode());
leave.update();
String sbUserCode = leave.getSbUserCode();
SysUser sbUser = SysUser.dao.findFirst("select * from sys_user t where t.user_code=? and t.is_deleted=?",
sbUserCode, 2);
String roleNameList = sbUser.getRoleNameList();
if(StringUtils.isNotEmpty(roleNameList) ){
if(roleNameList.indexOf("部门主管")>-1){
return "部门主管";
}
if(roleNameList.indexOf("普通员工")>-1){
return "普通员工";
}
}
return "其他";
}
@Override
public String desc() {
return "请假申报决策处理";
}
}
uflo提供了很多的Handler接口,下图展示了提供的可供实现的Handler接口,笔者为全部去尝试,有兴趣的同学可以去实现看看。
UfloApplication.java
@ImportResource(“classpath:config/context.xml”)不能漏了,它是引入uflo的spring配置文件。
buildUfloServlet方法是将uflo注册为一个Servlet,是使用uflo的入口文件,不能缺少。
package com.crwl;
import com.bstek.uflo.console.UfloServlet;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ImportResource;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
* 入口类
*/
@SpringBootApplication()
@EnableTransactionManagement
@ImportResource("classpath:config/context.xml")
@ComponentScan(basePackages = {"com.crwl.*"})
public class UfloApplication {
public static void main(String[] args) {
SpringApplication.run(UfloApplication.class, args);
}
//uflo工作流
@Bean
public ServletRegistrationBean buildUfloServlet(){
return new ServletRegistrationBean(new UfloServlet(),"/uflo/*");
}
}
由于后端代码太多,所以当前只是摘取几个关键的或者是笔者认为应该需要记录的源码进行展示并根据笔者的理解进行一些必要的阅读注释,当然可能存在理解不到位或者理解错误的地方,欢迎有兴趣的各位同学在评论区里进行沟通交流。此处就不再贴出其它的源码了。
前端使用的是传统的在html文件引入的方式js库文件的方式实现。采用的技术是jquery,iview。在此贴出首页以及模板设计的页面代码
首页index.html
DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>首页title>
<link rel="stylesheet" type = "text/css" href="../static/lib/iview/css/iview.css?v=1" />
<script src="../static/js/jquery-1.8.2.min.js">script>
<script src="../static/lib/iview/vue.min.js">script>
<script src="../static/lib/iview/iview.min.js">script>
<script src="../static/js/md5.js">script>
<script src="../static/js/common.js">script>
<style>
.clearfix{clear:both;}
style>
head>
<body scroll="no" style="overflow-y:hidden;scrollbar-width: none;">
<Layout id="app" style="overflow-y:hidden;">
<div style="height: 60px; line-height: 60px; background: #fff;">
<i-menu mode="horizontal" :theme="theme" active-name="1" style="height:50px;" @on-select="seletMenu">
<menu-item name="1">
<icon type="ios-paper" >icon>
请假申报
menu-item>
<menu-item name="2">
<icon type="ios-paper" >icon>
请假审核
menu-item>
<menu-item name="3">
<icon type="ios-paper" >icon>
请假查询
menu-item>
<menu-item name="4">
<icon type="ios-paper" >icon>
模板管理
menu-item>
i-menu>
<div v-show="null != loginUser && null != loginUser.userCode" style="position: absolute; right: 10px; top: -7px; z-index: 900">
登录用户: {{loginUser.userName}}
<i-button :size="styleSize" type="warning" @click="loginout" style="margin: 0 0 0 5px;">退出i-button>
div>
<div v-show="null == loginUser || null == loginUser.userCode" style="position: absolute; right: 10px; top: -7px; z-index: 900">
<i-button :size="styleSize" type="primary" @click="openLoginWin" >登录i-button>
div>
div>
<Modal v-model="loginModal"
title="登录"
width="400px"
>
<i-form ref="editValidate" :model="formData" :rules="editRuleValidate" :label-width="80" style="padding:10px 15px 0 0;">
<i-col span="24">
<form-item label="用户名" prop="userCode">
<i-input :size="styleSize" type="userCode" placeholder="选输入工号" v-model="formData.userCode" style="width:100%">i-input>
form-item>
i-col>
<i-col span="24">
<form-item label="密码" prop="password" >
<i-input :size="styleSize" type="password" placeholder="选输入密码" v-model="formData.password" style="width:100%">i-input>
form-item>
i-col>
i-form>
<div class="clearfix">div>
<div slot="footer">
<i-button :size="styleSize" type="primary" @click="loginFunc">登录i-Button>
<i-button :size="styleSize" type="warning" @click="loginModal=false">关闭i-Button>
div>
Modal>
<iframe :src="url" :height="height" width="100%" style="border: 0">iframe>
Layout>
body>
<script>
$(function() {
var height = ($(window).height()-160)/2;
var vue = new Vue({
el: '#app',
data: {
url:'leave-sb.html',
theme:'light',
height:'90%',
item1:'模板管理',
loginUser: {},
styleSize:constants.styleSize,
formData:{
userCode: '',
password: ''
},
loginModal:false,
editRuleValidate: {
userCode:{type:'string',required:true,message:'请输入用户名',trigger: 'blur'},
password:{type:'string',required:true,message:'请输入密码',trigger: 'blur'},
}
},
created:function(){
this.getLoginUser();
},
mounted :function(){
//this.tableHeight = window.innerHeight - this.$refs.table.$el.offsetTop - 160
var _this =this;
var height = $(window).height() - 60;
this.height = height;
window.onresize = () => {
return (() => {
var height = $(window).height() - 60;
_this.height = height;
})()
}
},
methods: {
getLoginUser(){
commonFunc.submit("/login/getLoginUser","post",{},function(data){
if(data.code == "200"){
var loginUser = data.data;
console.log(loginUser)
if(null !=loginUser){
localStorage.setItem("loginUser",JSON.stringify(data.data));
this.loginUser = data.data;
}
}
}.bind(this),function(data){
this.$Message.error(data.msg);
}.bind(this),"obj");
},
seletMenu(name){
if(name=='1'){
this.url='leave-sb.html';
}else if(name=='2'){
this.url='leave-sh.html';
}else if(name=='3'){
this.url='leave-qry.html';
}else if(name=='4'){
this.url='uflo-modelerlist.html';
}
},
openLoginWin(){
if(null != this.$refs.editValidate) {
this.$refs.editValidate.resetFields();
}
this.loginModal = true;
},
loginFunc(){
var _this = this;
this.$refs['editValidate'].validate((valid) => {
if (valid) {
var params = {
userCode:this.formData.userCode,
password: hex_md5(this.formData.password).toUpperCase(),
};
commonFunc.submit("/login/login","post",params,function(data){
if(data.code == "200"){
_this.$Message.success("登录成功");
localStorage.setItem("loginUser",JSON.stringify(data.data));
_this.loginModal = false;
window.location.reload();
}
},function(data){
_this.$Message.error(data.msg);
},"obj");
}
});
},
loginout(){
commonFunc.submit("/login/loginout","post",{},function(data){
if(data.code == "200"){
this.$Message.success("退出成功");
localStorage.removeItem("loginUser");
this.loginUser = {};
window.location.reload();
}
}.bind(this),function(data){
this.$Message.error(data.msg);
}.bind(this),"obj");
}
}
});
});
script>
html>
流程模型设计列表uflo-modelerlist.html
DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>流程模型列表title>
<link rel="stylesheet" type = "text/css" href="../static/lib/iview/css/iview.css?v=1" />
<script src="../static/js/jquery-1.8.2.min.js">script>
<script src="../static/lib/iview/vue.min.js">script>
<script src="../static/lib/iview/iview.min.js">script>
<script src="../static/js/common.js">script>
<script src="./js/uflo-modelerlist.js">script>
<style>
#searchForm .ivu-col {
padding: 10px 0;
}
#searchForm .ivu-form-item {
margin-bottom: 0px;
}
.ivu-modal-confirm-footer .ivu-btn-text{
float:right!important;
}
#ufloDesignerIframe{border:0;}
#ufloDesingerWin .ivu-modal-footer{display:none;}
#ufloDesingerWin .ivu-modal-body{bottom:0}
style>
head>
<body scroll="no" style="overflow-y:hidden;scrollbar-width: none;">
<Layout id="app" style="overflow-y:hidden;">
<div style="height:54px;">
<i-row id="searchForm">
<i-form :label-width="110">
<i-col span="4">
<form-item label="模型名称" prop="processName">
<i-input :size="styleSize" v-model="searchForm.processName">i-input>
form-item>
i-col>
<i-col span="4">
<form-item label="模型Key" prop="processKey">
<i-input :size="styleSize" v-model="searchForm.processKey">i-input>
form-item>
i-col>
<i-col span="4">
<i-Button :size="styleSize" type="primary" icon="ios-search" style="margin-left:15px;" @click="searchFunc">查询i-Button>
i-col>
i-form>
<div class="clearfix">div>
i-row>
div>
<i-row style="text-align:left;padding:5px;background:#fff;">
<i-Button :size="styleSize" type="primary" icon="ios-add" @click="openCreate()">新增i-Button>
i-row>
<Content>
<i-Table :size="styleSize" row-key="id" border :columns="columns" :data="tableDataList" ref="table" @on-row-click="selectRow" :height="tableHeight" :loading="loading" highlight-row style="overflow-y:auto;">i-Table>
<Page :total="dataCount" :page-size="pageSize" :page-size-opts="pageOptions" show-sizer class="paging" @on-change="changepage" @on-page-size-change="pagesize">Page>
Content>
<content>
<i-Table :size="styleSize" row-key="id" border :columns="deployColumns" :data="deployTableDataList" ref="deployTable" :height="deployTableHeight" :loading="loading" highlight-row style="overflow-y:auto;">i-Table>
content>
<Modal v-model="createModal"
title="新增模型"
width="400px"
>
<i-form ref="createValidate" :model="formData" :rules="createValidate" :label-width="80" style="padding:10px 25px 10px 10px;">
<form-item label="模型名称" prop="processName">
<i-input :size="styleSize" type="text" v-model="formData.processName" maxlength="20" show-word-limit >i-input>
form-item>
<form-item label="模型Key" prop="processKey">
<i-input :size="styleSize" type="text" v-model="formData.processKey" maxlength="20" show-word-limit >i-input>
form-item>
<form-item label="模型描述" prop="description">
<i-input :size="styleSize" type="textarea" v-model="formData.description" maxlength="100" show-word-limit >i-input>
form-item>
<div class="clearfix">div>
<div class="clearfix">div>
i-form>
<div slot="footer">
<i-Button :size="styleSize" type="primary" @click="submitFunc">保存i-Button>
<i-Button :size="styleSize" type="warning" @click="cancelFunc">取消i-Button>
div>
Modal>
<Modal id="ufloDesingerWin" v-model="ufloDesignerModal" title="流程设计" fullscreen="true">
<iframe id="ufloDesignerIframe" style="width:100%;height:100%;">iframe>
Modal>
<Modal id="flowWin" v-model="flowModal" title="流程详细" :mask-closable="false" fullscreen="true"style="padding:5px 50px;margin-bottom:40px;">
<div style="height:95%">
<iframe id="flowIframe" style="width:100%;height:100%">iframe>
div>
<div slot="footer">
<i-button :size="styleSize" type="warning" @click="flowModal=false">关闭i-Button>
div>
Modal>
Layout>
body>
html>
uflo-modelerlist.js
$(function() {
var height = ($(window).height()-160)/2;
var vue = new Vue({
el: '#app',
data: {
styleSize:constants.styleSize,
createModal:false,
editFlag:'add',
ufloDesignerModal:false,
tableHeight:height,
loading:true,
flowModal:false,
tableDataList:[],
columns:[
{title:'序号',type:'index',key: '',width:'80',align:'center'},
{title: '模型名称',key: 'processName'},
{title: '模型key',key: 'processKey'},
{title: '模型描述',key: 'description'},
{title:'操作',key: 'action',render(h,params){
var _this = this;
return h('div', [
h('Button', {
props: {
type: 'primary',
size: 'small'
},
style: {
marginRight: '5px'
},
on: {
click: () => {
vue.editFunc(params.row);
}
}
}, '编辑'),
h('Button', {
props: {
type: 'warning',
size: 'small'
},
style: {
marginRight: '5px'
},
on: {
click: () => {
vue.deleteFunc(params.row);
}
}
}, '删除'),
h('Button', {
props: {
type: 'info',
size: 'small'
},
style: {
marginRight: '5px'
},
on: {
click: () => {
vue.deployFunc(params.row);
}
}
}, '部署')
]);
}}
],
deployColumns:[
{title:'序号',type:'index',key: '',width:'80',align:'center'},
{title: '部署版本',key: 'version'},
{
title: '部署时间', key: 'createDate', render(row, column) {
if (column.row.createDate != null && column.row.createDate != "") {
return commonFunc.formatDate(column.row.createDate,'date')
//return column.row.createDate.replace("T"," ").replace(".000+0000","");
}
}
},
{title: '部署编号',key: 'id'},
{
title: '操作', key: 'action', render(h, params) {
var _this = this;
return h('div', [
h('Button', {
props: {
type: 'primary',
size: 'small'
},
style: {
marginRight: '5px'
},
on: {
click: () => {
vue.viewFlowFunc(params.row);
}
}
}, '查看')]
);
}
}
],
deployTableDataList:[],
//bizCodeList:[],
deployTableHeight:height,
searchForm:{
processName:'',
processKey:''
},
formData:{
processName:'',
processKey:'',
description:''
},
createValidate:{
name:'',
key:'',
},
createRuleValidate: {
processName:{required:true,message:'请输入模型名称',trigger: 'blur'},
processsKey:{required:true,message:'请输入模型key',trigger: 'blur'}
},
dataCount:0,
// 每页显示多少条
pageSize:20,
// 当前页码
currentPage:1,
pageOptions:[20,40,60,80,100]
},
created:function(){
this.initPage();
var _this = this;
},
mounted :function(){
//this.tableHeight = window.innerHeight - this.$refs.table.$el.offsetTop - 160
var _this =this;
window.onresize = () => {
return (() => {
var height = ($(window).height()-160)/2;
_this.tableHeight = height;
})()
}
},
methods: {
queryFunc(params){
var _this = this ;
var searchParam = this.searchForm;
searchParam.currentPage = this.currentPage;
searchParam.pageSize = this.pageSize;
if(null != params){
for(var i in params){
searchParam[i] = params[i];
}
}
//searchParam.tenantId='bsga';
_this.loading = true;
commonFunc.submit("/ufloModel/getTableList","post",searchParam,function(data){
_this.tableDataList = data.rows;
_this.dataCount = data.total;
_this.loading = false;
_this.deployTableDataList=[];
},function(){
_this.loading = false;
});
},
initPage(){
this.currentPage = 1;
this.queryFunc();
},
changepage(index){
this.pageFunc(index);
},
pagesize(index){
this.pageFunc(index);
},
pageFunc(index){
this.currentPage = index;
this.queryFunc();
},
searchFunc(){
this.currentPage = 1;
this.queryFunc();
},
selectRow(row, index) {
//this.$refs.table.toggleSelect(index);
var _this = this;
//刷新部署的模型
var param = {processKey:row.processKey,currentPage:1,pageSize:1000};
commonFunc.submit("/ufloModel/getDeployProcessPage","post",param,function(data){
if(null != data.rows){
_this.deployTableDataList = data.rows;
}
},function(data){});
},
editFunc(row){
//window.open('../index.html#/editor/'+row.id,'blank_');
var src = "/pro/uflo/designer?p=file:"+row.fileName;
var parent = $("#ufloDesignerIframe").parent();
var iframeHtml = ' src +'" style="width:100%;height:100%;">';
parent.html(iframeHtml);
this.ufloDesignerModal = true;
},
openCreate(){
var _this = this;
this.formData.name="";
this.formData.key="";
this.formData.description="";
if(null != this.$refs.editValidate) {
this.$refs.editValidate.resetFields();
}
this.createModal = true;
},
submitFunc(){
var _this = this;
this.$refs['createValidate'].validate((valid) => {
if(valid) {
var submitUrl = "/ufloModel/saveUfloModel";
var params = _this.formData;
commonFunc.submit(submitUrl,"post",params,function(data){
_this.$Message.success("新增流程模板成功");
_this.createModal = false;
var row = {
fileName:params.processKey+".uflo.xml"
}
_this.editFunc(row);
_this.searchFunc();
},function(data){
_this.$Message.error(data.msg);
},'obj');
}
});
},
deleteFunc(row){
var _this = this;
_this.$Modal.confirm({
title: '你确定删除当前的模型记录吗?',
okText: '确定',
cancelText: '取消',
onOk: function () {
commonFunc.submit("/ufloModel/deleteUfloModel","post",{modelId:row.id},function(data){
_this.$Message.success("删除流程模板成功");
_this.searchFunc();
},function(data){
_this.$Message.error(data.msg);
});
}
});
},
deployFunc(row){
var _this = this;
_this.$Modal.confirm({
title: '你确定部署当前流程模板吗?',
okText: '确定',
cancelButtonClass: 'btn-custom-cancel',
cancelText: '取消',
onOk: function () {
commonFunc.submit("/ufloModel/deployProcess","post",{modelId:row.id,tenantId:row.tenantId},function(data){
_this.$Message.success("流程部署成功");
var param = {
processKey:row.processKey
}
//刷新部署的模型
_this.selectRow(param);
},function(data){
_this.$Message.error(data.msg);
});
}
});
},
cancelFunc(){
this.createModal = false;
},
viewFlowFunc(row){
var processId = row.id;
var now = new Date();
var timestamp = now.getTime();
var url= commonFunc.getBasePath("/uflo/diagram")+"/uflo/diagram?processId="+processId+"&timeStamp="+timestamp;
var parent = $("#flowIframe").parent();
$("#flowIframe").remove();
parent.append('url+'" style="width:100%;height:100%;border:0">')
this.flowModal = true;
}
}
});
});
右键点击启动文件(UfloApplication.java),在弹出的菜单中点击Run UfloApplication.java菜单启动系统。
启动成功后,打开浏览器,输入http://localhost:9090/pro/views/index.html即可进入报表管理页面
点击【登录】按钮,弹出登录弹窗,输入用户名密码,点击登录进入系统。
登录成功侯可在请假申报页面里进行请假申报审核人员登录系统侯可以在请假审核页面进行审核操作
系统管理员登录后可以在模板管理页面中增删改查以及部署流程模板
到此,Springboot整合Uflo2工作流分享结束。如需要同笔者进行学习交流。