Flowable入门
1) flowable整合springboot
2) 移除flowable权限校验,加入微服务项目本身权限校验
3) 常用的API接口(查看流程图、部署流程、我的待办、我的已办、完结任务、审批历史等)
1. pom文件引入Flowable相关架包
目前这里引入的是6.4.0版本的flowable
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-spring-boot-starter</artifactId>
<version>${flowable.version}</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 配置文件处理器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<!-- app 依赖 包含 rest,logic,conf -->
<!-- flowable 集成依赖 rest,logic,conf -->
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-ui-modeler-rest</artifactId>
<version>${flowable.version}</version>
</dependency>
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-ui-modeler-logic</artifactId>
<version>${flowable.version}</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-ui-modeler-conf</artifactId>
<version>${flowable.version}</version>
</dependency>
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-ldap-configurator</artifactId>
<version>${flowable.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.10</version>
</dependency>
2.用户角色权限整合
2.1 AppDispatcherServletConfiguration
import org.flowable.ui.modeler.rest.app.EditorGroupsResource;
import org.flowable.ui.modeler.rest.app.EditorUsersResource;
import org.flowable.ui.modeler.rest.app.StencilSetResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
@Configuration
@ComponentScan(value = { "org.flowable.ui.modeler.rest.app",
// 不加载 rest,因为 getAccount 接口需要我们自己实现
// "org.flowable.ui.common.rest"
},excludeFilters = {
// 移除 EditorUsersResource 与 EditorGroupsResource,因为不使用 IDM 部分
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = EditorUsersResource.class),
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = EditorGroupsResource.class),
// 配置文件用自己的
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = StencilSetResource.class),
}
)
@EnableAsync
public class AppDispatcherServletConfiguration implements WebMvcRegistrations {
private static final Logger LOGGER = LoggerFactory.getLogger(AppDispatcherServletConfiguration.class);
@Bean
public SessionLocaleResolver localeResolver() {
return new SessionLocaleResolver();
}
@Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
LOGGER.debug("Configuring localeChangeInterceptor");
LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
localeChangeInterceptor.setParamName("language");
return localeChangeInterceptor;
}
@Override
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
LOGGER.debug("Creating requestMappingHandlerMapping");
RequestMappingHandlerMapping requestMappingHandlerMapping = new RequestMappingHandlerMapping();
requestMappingHandlerMapping.setUseSuffixPatternMatch(false);
requestMappingHandlerMapping.setRemoveSemicolonContent(false);
Object[] interceptors = { localeChangeInterceptor() };
requestMappingHandlerMapping.setInterceptors(interceptors);
return requestMappingHandlerMapping;
}
}
2.2 ApplicationConfiguration
import org.flowable.ui.common.service.idm.RemoteIdmService;
import org.flowable.ui.modeler.properties.FlowableModelerAppProperties;
import org.flowable.ui.modeler.servlet.ApiDispatcherServletConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
@Configuration
@EnableConfigurationProperties(FlowableModelerAppProperties.class)
@ComponentScan(basePackages = {
// "org.flowable.ui.modeler.conf",
"org.flowable.ui.modeler.repository",
"org.flowable.ui.modeler.service",
// "org.flowable.ui.modeler.security", //授权方面的都不需要
// "org.flowable.ui.common.conf", // flowable 开发环境内置的数据库连接
// "org.flowable.ui.common.filter", // IDM 方面的过滤器
"org.flowable.ui.common.service",
"org.flowable.ui.common.repository",
//
// "org.flowable.ui.common.security",//授权方面的都不需要
"org.flowable.ui.common.tenant" },excludeFilters = {
// 移除 RemoteIdmService
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = RemoteIdmService.class),
}
)
public class ApplicationConfiguration {
@Bean
public ServletRegistrationBean modelerApiServlet(ApplicationContext applicationContext) {
AnnotationConfigWebApplicationContext dispatcherServletConfiguration = new AnnotationConfigWebApplicationContext();
dispatcherServletConfiguration.setParent(applicationContext);
dispatcherServletConfiguration.register(ApiDispatcherServletConfiguration.class);
DispatcherServlet servlet = new DispatcherServlet(dispatcherServletConfiguration);
ServletRegistrationBean registrationBean = new ServletRegistrationBean(servlet, "/api/*");
registrationBean.setName("Flowable Modeler App API Servlet");
registrationBean.setLoadOnStartup(1);
registrationBean.setAsyncSupported(true);
return registrationBean;
}
}
2.3 FlowableBeanConfig
@Configuration
public class FlowableBeanConfig {
protected static final String LIQUIBASE_CHANGELOG_PREFIX = "ACT_DE_";
@Bean
public Liquibase liquibase(DataSource dataSource) {
try {
DatabaseConnection connection = new JdbcConnection(dataSource.getConnection());
Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(connection);
database.setDatabaseChangeLogTableName(LIQUIBASE_CHANGELOG_PREFIX + database.getDatabaseChangeLogTableName());
database.setDatabaseChangeLogLockTableName(LIQUIBASE_CHANGELOG_PREFIX + database.getDatabaseChangeLogLockTableName());
Liquibase liquibase = new Liquibase("META-INF/liquibase/flowable-modeler-app-db-changelog.xml", new ClassLoaderResourceAccessor(), database);
liquibase.update("flowable");
return liquibase;
} catch (Exception e) {
throw new RuntimeException("Error creating liquibase database", e);
}
}
}
2.4 SecurityUtils
import com.bho.model.LoginUser;
import com.bho.utils.SysUserUtils;
import org.flowable.idm.api.User;
import org.flowable.ui.common.model.RemoteUser;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import java.util.ArrayList;
import java.util.List;
/**
* Utility class for Spring Security.
*/
public class SecurityUtils {
private static User assumeUser;
private SecurityUtils() {
}
/**
* Get the login of the current user.
*/
public static String getCurrentUserId() {
User user = getCurrentUserObject();
if (user != null) {
return user.getId();
}
return null;
}
/**
* @return the {@link User} object associated with the current logged in user.
*/
public static User getCurrentUserObject() {
LoginUser currentLoginUser = SysUserUtils.getCurrentLoginUser();
RemoteUser user = new RemoteUser();
user.setId(String.valueOf(currentLoginUser.getId()));
user.setFirstName(currentLoginUser.getAccount());
List<String> pris = new ArrayList<>();
pris.add(DefaultPrivileges.ACCESS_MODELER);
pris.add(DefaultPrivileges.ACCESS_IDM);
pris.add(DefaultPrivileges.ACCESS_ADMIN);
pris.add(DefaultPrivileges.ACCESS_TASK);
pris.add(DefaultPrivileges.ACCESS_REST_API);
user.setPrivileges(pris);
return user;
// if (assumeUser != null) {
// return assumeUser;
// }
//
// RemoteUser user = new RemoteUser();
// user.setId("admin");
// user.setDisplayName("Administrator");
// user.setFirstName("Administrator");
// user.setLastName("Administrator");
// user.setEmail("admin@flowable.com");
// user.setPassword("123456");
// List<String> pris = new ArrayList<>();
// pris.add(DefaultPrivileges.ACCESS_MODELER);
// pris.add(DefaultPrivileges.ACCESS_IDM);
// pris.add(DefaultPrivileges.ACCESS_ADMIN);
// pris.add(DefaultPrivileges.ACCESS_TASK);
// pris.add(DefaultPrivileges.ACCESS_REST_API);
// user.setPrivileges(pris);
// return user;
}
public static FlowableAppUser getCurrentFlowableAppUser() {
FlowableAppUser user = null;
SecurityContext securityContext = SecurityContextHolder.getContext();
if (securityContext != null && securityContext.getAuthentication() != null) {
Object principal = securityContext.getAuthentication().getPrincipal();
if (principal instanceof FlowableAppUser) {
user = (FlowableAppUser) principal;
}
}
return user;
}
public static boolean currentUserHasCapability(String capability) {
FlowableAppUser user = getCurrentFlowableAppUser();
for (GrantedAuthority grantedAuthority : user.getAuthorities()) {
if (capability.equals(grantedAuthority.getAuthority())) {
return true;
}
}
return false;
}
public static void assumeUser(User user) {
assumeUser = user;
}
public static void clearAssumeUser() {
assumeUser = null;
}
}
3.modeler编辑器汉化
3.1 引入汉化文件
3.2 重写相关接口
import com.bho.model.BaseException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/app")
public class FlowableStencilSetResourceController {
private static final Logger LOGGER = LoggerFactory.getLogger(FlowableStencilSetResourceController.class);
@Autowired
protected ObjectMapper objectMapper;
@RequestMapping(value = "/rest/stencil-sets/editor", method = RequestMethod.GET, produces = "application/json")
public JsonNode getStencilSetForEditor() {
try {
JsonNode stencilNode = objectMapper.readTree(this.getClass().getClassLoader().getResourceAsStream("stencilset/stencilset_bpmn.json"));
return stencilNode;
} catch (Exception e) {
LOGGER.error("Error reading bpmn stencil set json", e);
throw new BaseException("Error reading bpmn stencil set json");
}
}
@RequestMapping(value = "/rest/stencil-sets/cmmneditor", method = RequestMethod.GET, produces = "application/json")
public JsonNode getCmmnStencilSetForEditor() {
try {
JsonNode stencilNode = objectMapper.readTree(this.getClass().getClassLoader().getResourceAsStream("stencilset/stencilset_cmmn.json"));
return stencilNode;
} catch (Exception e) {
LOGGER.error("Error reading bpmn stencil set json", e);
throw new BaseException("Error reading bpmn stencil set json");
}
}
}
4.引入静态资源文件
4.1 将静态资源文件整个目录拷贝到自己项目中
4.2 页面接口添加请求头和权限判断
修改augular.js文件
修改index.html
4.3 修改接口请求路径
修改app-cfg.js文件
4.4 增加表单属性类型(可选操作)
因为flowable自带的表单属性类型有限,不够支撑业务需求,所以增加了几种类型,方便后面动态展示表单。
4.5 修改节点用户和角色
重写用户角色接口
import com.bho.model.LoginUser;
import com.bho.model.RestResponse;
import com.bho.user.feign.SysRoleApi;
import com.bho.user.feign.SysUserApi;
import com.bho.user.form.SysRoleDto;
import com.bho.user.form.SysUserDto;
import com.bho.utils.SysUserUtils;
import org.flowable.ui.common.model.GroupRepresentation;
import org.flowable.ui.common.model.ResultListDataRepresentation;
import org.flowable.ui.common.model.UserRepresentation;
import org.flowable.ui.common.security.DefaultPrivileges;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
@RestController
@RequestMapping("app")
public class UserAndGroupResourceController {
@Autowired
private SysUserApi sysUserApi;
@Autowired
private SysRoleApi sysRoleApi;
@RequestMapping(value = "/rest/editor-groups", method = RequestMethod.GET)
public ResultListDataRepresentation getGroups(@RequestParam(required = false, value = "filter") String filter) {
RestResponse<List<SysRoleDto>> restResponse = sysRoleApi.findRoleInfoByName(filter);
if(restResponse.getCode() == RestResponse.SUCCESS_CODE){
List<SysRoleDto> sysRoleDtoList = restResponse.getData();
List<GroupRepresentation> representationList = new ArrayList<>(sysRoleDtoList.size());
for(SysRoleDto sysRoleDto : sysRoleDtoList) {
GroupRepresentation representation = new GroupRepresentation();
representation.setName(sysRoleDto.getName());
representation.setId(String.valueOf(sysRoleDto.getId()));
representationList.add(representation);
}
return new ResultListDataRepresentation(representationList);
}
return new ResultListDataRepresentation();
}
@RequestMapping(value = "/rest/editor-users", method = RequestMethod.GET)
public ResultListDataRepresentation getUsers(@RequestParam(value = "filter", required = false) String filter) {
RestResponse<List<SysUserDto>> restResponse = sysUserApi.findUserInfoByName(filter);
if(restResponse.getCode() == RestResponse.SUCCESS_CODE){
List<SysUserDto> sysUserDtoList = restResponse.getData();
List<UserRepresentation> userRepresentationList = new ArrayList<>(sysUserDtoList.size());
for(SysUserDto sysUserDto : sysUserDtoList) {
UserRepresentation userRepresentation = new UserRepresentation();
userRepresentation.setEmail(sysUserDto.getEmail());
userRepresentation.setFirstName(sysUserDto.getUsername());
userRepresentation.setId(String.valueOf(sysUserDto.getId()));
userRepresentationList.add(userRepresentation);
}
return new ResultListDataRepresentation(userRepresentationList);
}
return new ResultListDataRepresentation();
}
/**
* 获取默认的管理员信息
* @return
*/
@RequestMapping(value = "/rest/account", method = RequestMethod.GET, produces = "application/json")
public UserRepresentation getAccount() {
LoginUser currentLoginUser = SysUserUtils.getCurrentLoginUser();
UserRepresentation user = new UserRepresentation();
user.setId(String.valueOf(currentLoginUser.getId()));
user.setFirstName(currentLoginUser.getAccount());
List<String> pris = new ArrayList<>();
pris.add(DefaultPrivileges.ACCESS_MODELER);
pris.add(DefaultPrivileges.ACCESS_IDM);
pris.add(DefaultPrivileges.ACCESS_ADMIN);
pris.add(DefaultPrivileges.ACCESS_TASK);
pris.add(DefaultPrivileges.ACCESS_REST_API);
user.setPrivileges(pris);
return user;
}
}
效果图
4.6 其它一些样式调整
5.用例Demo
这里拿维修工单来举例说明
5.1 流程模型
5.1.1 后端代码
import com.bho.flowable.dto.ModelResp;
import com.bho.flowable.form.ReqFormParam;
import com.bho.flowable.mapper.ActCommonMapper;
import com.bho.flowable.service.ActDeModelService;
import com.bho.utils.SysUserUtils;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.repository.Deployment;
import org.flowable.ui.modeler.domain.Model;
import org.flowable.ui.modeler.model.ModelRepresentation;
import org.flowable.ui.modeler.serviceapi.ModelService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.List;
/**
* @description:
* @author:wanzh
* @date:2021/7/2
*/
@Service
public class ActDeModelServiceImpl implements ActDeModelService {
private static final String BPMN_SUFFIX = ".bpmn";
@Autowired
private ModelService modelService;
@Autowired
private RepositoryService repositoryService;
@Autowired
private ActCommonMapper actCommonMapper;
@Override
@Transactional
public String deployment(String modelId) {
Model model = modelService.getModel(modelId);
BpmnModel bpmnModel = modelService.getBpmnModel(model);
Deployment deployment = repositoryService.createDeployment()
.name(model.getName())
.addBpmnModel(model.getKey() + BPMN_SUFFIX, bpmnModel).deploy();
String deploymentId = deployment.getId();
return deploymentId; //部署ID
}
@Override
public int delete(String key) {
modelService.deleteModel(key);
return 0;
}
@Override
public void add(Model model) {
String userId = String.valueOf(SysUserUtils.getCurrentLoginUser().getId());
model.setCreatedBy(userId);
model.setLastUpdatedBy(userId);
Date date = new Date();
model.setCreated(date);
model.setLastUpdated(date);
ModelRepresentation modelRepresentation = new ModelRepresentation();
modelRepresentation.setKey(model.getKey());
modelRepresentation.setName(model.getName());
modelRepresentation.setModelType(model.getModelType());
String json = modelService.createModelJson(modelRepresentation);
model.setModelEditorJson(json);
model.setVersion(1);
modelService.createModel(model,null);
}
/**
提供的SQL查询语句
<select id="findModel" resultType="com.bho.flowable.dto.ModelResp">
select id,name,model_key as "key",created,version from act_de_model where model_type = 0
<if test="name != null and name != ''">
and name like CONCAT('%', #{name}, '%')
</if>
<if test="key != null and key != ''">
and model_key like CONCAT('%', #{key}, '%')
</if>
</select>
*/
@Override
public PageInfo findObjectByPage(ReqFormParam reqFormParam) {
PageHelper.startPage(reqFormParam.getPageNum(), reqFormParam.getPageSize()); // , true
List<ModelResp> modelResps = actCommonMapper.findModel(reqFormParam.getTitle(),reqFormParam.getName(),reqFormParam.getKey());
return new PageInfo(modelResps);
}
}
5.1.2 前端页面
添加
编辑(画流程图、配置用户/组、设置节点表单)
设置故障处置节点表单属性选择的维修人员
接单节点的用户配置跟表单属性保持一致
需要跳过的节点设置${skip}即可
这里不一一介绍每个节点是如何配置的,配置完毕点击部署按钮即可生成流程定义。
5.2 流程定义
5.2.1 后端代码
import com.bho.flowable.dto.ProcessDefinitionResp;
import com.bho.flowable.form.ReqFormParam;
import com.bho.flowable.service.ActReProcdefService;
import com.bho.model.BaseException;
import com.bho.utils.BeanTransferUtils;
import com.github.pagehelper.PageInfo;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.repository.ProcessDefinition;
import org.flowable.engine.repository.ProcessDefinitionQuery;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
/**
* @description:
* @author:wanzh
* @date:2021/7/2
*/
@Slf4j
@Service
public class ActReProcdefServiceImpl implements ActReProcdefService {
@Autowired
private RepositoryService repositoryService;
@Override
public void activate(String id) {
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
.processDefinitionId(id).singleResult();
if(processDefinition == null){
throw new BaseException("当前流程不存在");
}
if(processDefinition.isSuspended()){
repositoryService.activateProcessDefinitionById(id, true, null);
}else {
throw new BaseException("当前流程已被激活,请不要重复操作");
}
}
@Override
public void suspend(String id) {
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
.processDefinitionId(id).singleResult();
if(processDefinition == null){
throw new BaseException("当前流程不存在");
}
if(!processDefinition.isSuspended()){
//processDefinitionKey流程定义的id(key)
// suspendProcessInstances:是否级联挂起该流程定义下的流程实例
// suspensionDate:设置挂起这个流程定义的时间,如果不填写,则立即挂起
repositoryService.suspendProcessDefinitionById(id, true, null);
}else{
throw new BaseException("当前流程已被挂起,请不要重复操作");
}
}
@Override
public void viewImage(OutputStream out, String id) {
try {
InputStream in = repositoryService.getProcessDiagram(id);
IOUtils.copy(in, out);
}catch (Exception e){
log.error("获取流程图出错:{}",e);
throw new BaseException("获取流程图出错");
}
}
@Override
public int delete(String key) {
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().processDefinitionId(key).singleResult();
if(processDefinition == null){
throw new BaseException("当前流程不存在");
}
repositoryService.deleteDeployment(processDefinition.getDeploymentId(), true);
return 0;
}
@Override
public PageInfo findObjectByPage(ReqFormParam reqFormParam) {
PageInfo pageInfo = new PageInfo();
pageInfo.setPageSize(reqFormParam.getPageSize());
pageInfo.setPageNum(reqFormParam.getPageNum());
ProcessDefinitionQuery processDefinitionQuery = repositoryService.createProcessDefinitionQuery();
if(!StringUtils.isEmpty(reqFormParam.getName())){
processDefinitionQuery = processDefinitionQuery.processDefinitionNameLike(reqFormParam.getName());
}
if(!StringUtils.isEmpty(reqFormParam.getKey())){
processDefinitionQuery = processDefinitionQuery.processDefinitionKeyLike(reqFormParam.getKey());
}
long count = processDefinitionQuery.count();
pageInfo.setTotal(count);
if(count > 0){
List<ProcessDefinition> processDefinitions = processDefinitionQuery.orderByProcessDefinitionVersion().desc()
.listPage((reqFormParam.getPageNum() - 1) * reqFormParam.getPageSize(), reqFormParam.getPageSize());
List<ProcessDefinitionResp> processDefinitionResps = BeanTransferUtils.doToDtoList(processDefinitions, ProcessDefinitionResp.class);
pageInfo.setList(processDefinitionResps);
}
return pageInfo;
}
}
防止流程图中文乱码
import org.flowable.spring.SpringProcessEngineConfiguration;
import org.flowable.spring.boot.EngineConfigurationConfigurer;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FlowableConfig implements EngineConfigurationConfigurer<SpringProcessEngineConfiguration> {
@Override
public void configure(SpringProcessEngineConfiguration engineConfiguration) {
engineConfiguration.setActivityFontName("宋体");
engineConfiguration.setLabelFontName("宋体");
engineConfiguration.setAnnotationFontName("宋体");
engineConfiguration.setDrawSequenceFlowNameWithNoLabelDI(true);
}
}
5.2.2 前端页面
5.3 流程实例
5.3.1 后端代码
提供一个发起申请接口供其它微服务调用
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotBlank;
/**
* @description:
* @author:wanzh
* @date:2021/7/14
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ProcessInstanceDto {
private String variable;//变量 json字符串
private String callbackUrl;//回调地址
private String method;//方法
@NotBlank(message = "流程标识不能为空")
private String processKey;//流程标识
@NotBlank(message = "业务标识不能为空")
private String businessKey;//业务标识
private String additionTitle;//附加标题
private String serialNo;//流水号
}
import com.bho.flowable.dto.ProcessInstanceDto;
import com.bho.model.RestResponse;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import javax.validation.Valid;
/**
* @description:
* @author:wanzh
* @date:2021/7/9
*/
@FeignClient(name = "iot-flowable")
public interface ProcessInstanceApi {
/**
* 发起流程实例
* @param processInstanceDto
* @return
*/
@PostMapping("/rest/processInstance/start")
RestResponse<String> start(@Valid @RequestBody ProcessInstanceDto processInstanceDto);
}
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import com.bho.flowable.dto.ProcessInstanceDto;
import com.bho.flowable.feign.ProcessInstanceApi;
import com.bho.model.BaseException;
import com.bho.model.LoginUser;
import com.bho.model.RestResponse;
import com.bho.utils.JacksonUtil;
import com.bho.utils.SysUserUtils;
import lombok.extern.slf4j.Slf4j;
import org.flowable.common.engine.impl.identity.Authentication;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.repository.ProcessDefinition;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.engine.runtime.ProcessInstanceBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @description:
* @author:wanzh
* @date:2021/7/14
*/
@Slf4j
@RestController
public class ProcessInstanceApiImpl implements ProcessInstanceApi{
@Autowired
private RuntimeService runtimeService;
@Autowired
private RepositoryService repositoryService;
@Override
public RestResponse<String> start(ProcessInstanceDto processInstanceDto) {
LoginUser loginUser = SysUserUtils.getCurrentLoginUser();
Authentication.setAuthenticatedUserId(String.valueOf(loginUser.getId()));
String json = processInstanceDto.getVariable();
Map<String,Object> variable = new HashMap<>();
if(!StringUtils.isEmpty(json)){
variable = JacksonUtil.jsonToMap(json,String.class,Object.class);
}
variable.put("_ACTIVITI_SKIP_EXPRESSION_ENABLED", true);
variable.put("skip",true);
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().processDefinitionKey(processInstanceDto.getProcessKey())
.active().latestVersion().singleResult();
if(processDefinition == null){
throw new BaseException("未知的流程标识");
}
String title = loginUser.getUserName() + "在" + DateUtil.format(new Date(),DatePattern.NORM_DATE_PATTERN) +"发起"+processDefinition.getName()
+ (StringUtils.isEmpty(processInstanceDto.getSerialNo()) ? "" : processInstanceDto.getSerialNo())
+ (StringUtils.isEmpty(processInstanceDto.getAdditionTitle()) ? "" : "【"+processInstanceDto.getAdditionTitle()+"】");
ProcessInstanceBuilder processInstanceBuilder = runtimeService.createProcessInstanceBuilder()
.name(title)//流程标题
.businessKey(String.valueOf(processInstanceDto.getBusinessKey()))//业务主键
.processDefinitionKey(processInstanceDto.getProcessKey())//流程标识
.callbackId(processInstanceDto.getCallbackUrl())
.callbackType(StringUtils.isEmpty(processInstanceDto.getMethod()) ? "POST" : processInstanceDto.getMethod())
.variables(variable);//流程变量
// 启动(即创建)流程实例
ProcessInstance processInstance = processInstanceBuilder.start();
Authentication.setAuthenticatedUserId(null);
log.info("instanceId:{}",processInstance.getProcessInstanceId());
return RestResponse.buildSuccessData(processInstance.getProcessInstanceId());
}
}
常用接口
import com.bho.flowable.dto.ApprovalHistoryResp;
import com.bho.flowable.dto.OpTypeEnum;
import com.bho.flowable.dto.ProcessInstanceResp;
import com.bho.flowable.form.ReqFormParam;
import com.bho.flowable.mapper.ActCommonMapper;
import com.bho.flowable.service.ActInstanceService;
import com.bho.flowable.utils.FlowableConstants;
import com.bho.model.BaseException;
import com.bho.utils.CommonConst;
import com.bho.utils.CommonUtils;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.bpmn.model.FlowNode;
import org.flowable.bpmn.model.SequenceFlow;
import org.flowable.engine.HistoryService;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.history.HistoricActivityInstance;
import org.flowable.engine.history.HistoricProcessInstance;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.spring.SpringProcessEngineConfiguration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.awt.*;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
/**
* @description:
* @author:wanzh
* @date:2022/1/6
*/
@Slf4j
@Service
public class ActInstanceServiceImpl implements ActInstanceService {
@Autowired
private RuntimeService runtimeService;
@Autowired
private RepositoryService repositoryService;
@Autowired
private HistoryService historyService;
@Autowired
private SpringProcessEngineConfiguration processEngineConfiguration;
@Autowired
private ActCommonMapper actCommonMapper;
private static final String ACT_TYPE_GATEWAY = "Gateway";
private static final String ACT_TYPE_START = "startEvent";
private static final String ACT_TYPE_END = "endEvent";
private static final String ACTION_ADD_COMMENT = "AddComment";
/**
<select id="findInstance" resultType="com.bho.flowable.dto.ProcessInstanceResp">
select hp.id_ as id,hp.name_ as name,p.key_ as processDefinitionKey,p.name_ as processDefinitionName,t.suspension_state_ as suspensionState,
hp.duration_ as duration,hp.start_time_ as startTime,hp.rev_ as revision,hp.business_key_ as businessKey,t.name_ as nodeName
from act_hi_procinst hp left join act_re_procdef p on hp.proc_def_id_ = p.id_
left join act_ru_task t on t.proc_inst_id_ = hp.id_
</select>
*/
@Override
public PageInfo findObjectByPage(ReqFormParam reqFormParam) {
PageHelper.startPage(reqFormParam.getPageNum(), reqFormParam.getPageSize()," startTime desc"); // , true
List<ProcessInstanceResp> instances = actCommonMapper.findInstance(reqFormParam.getTitle(),reqFormParam.getName(),reqFormParam.getKey());
for(ProcessInstanceResp resp : instances){
Long duration = resp.getDuration();
resp.setDurationStr(CommonUtils.getDuration(duration));
}
return new PageInfo(instances);
}
@Override
public void delete(String key) {
runtimeService.deleteProcessInstance(key,"删除流程实例");
}
@Override
public void deleteHistory(String id) {
historyService.deleteHistoricProcessInstance(id);
}
@Override
public void activate(String id) {
ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(id).singleResult();
if(processInstance == null){
throw new BaseException("当前流程实例不存在");
}
if(processInstance.isSuspended()){
runtimeService.activateProcessInstanceById(id);
}else {
throw new BaseException("当前流程实例已激活,请不要重复操作");
}
}
@Override
public void suspend(String id) {
ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(id).singleResult();
if(processInstance == null){
throw new BaseException("当前流程实例不存在");
}
if(!processInstance.isSuspended()){
runtimeService.suspendProcessInstanceById(id);
}else {
throw new BaseException("当前流程实例已挂起,请不要重复操作");
}
}
//当前接口会签或者并行网关未验证可能存在问题,这里未涉及到这些所以只是简单的写了一下。
@Override
public void viewImage(OutputStream out, String id) {
try {
HistoricProcessInstance processInstance = historyService.createHistoricProcessInstanceQuery().processInstanceId(id).singleResult();
if(processInstance == null){
throw new BaseException("当前流程实例不存在");
}
//根据流程定义ID获取BpmnModel
String processDefinitionId = processInstance.getProcessDefinitionId();
BpmnModel bpmnModel = repositoryService.getBpmnModel(processDefinitionId);
//得到已经走过的节点的id集合
List<String> highLightedRunActivitis = new ArrayList<>();
if(processInstance.getEndTime() == null){
String executionId=processInstance.getId();
highLightedRunActivitis = runtimeService.getActiveActivityIds(executionId);
}
List<HistoricActivityInstance> historicActivityInstanceList = historyService.createHistoricActivityInstanceQuery()
.processInstanceId(id).orderByHistoricActivityInstanceStartTime().asc().list();
List<String> highLightedActivitis = new ArrayList<>();
for(HistoricActivityInstance hi:historicActivityInstanceList) {
highLightedActivitis.add(hi.getActivityId());
}
//得到走过的流的id集合
List<String> highLightedFlows = new ArrayList<>();
for (int i = 0; i < historicActivityInstanceList.size(); i++) {
HistoricActivityInstance currentActivityInstance = historicActivityInstanceList.get(i);
FlowNode currentFlowNode = (FlowNode) bpmnModel.getMainProcess().getFlowElement(currentActivityInstance.getActivityId(), true);
List<SequenceFlow> sequenceFlows = currentFlowNode.getOutgoingFlows();
if(i < historicActivityInstanceList.size() - 1){
HistoricActivityInstance nextActivityInstance = historicActivityInstanceList.get(i + 1);
for (SequenceFlow sequenceFlow : sequenceFlows) {
if (nextActivityInstance.getActivityId().equals(sequenceFlow.getTargetRef())) {
highLightedFlows.add(sequenceFlow.getId());
}
}
}
}
//获取输入流以及高亮显示
CustomProcessDiagramGenerator customProcessDiagramGenerator = new CustomProcessDiagramGenerator();
InputStream in = customProcessDiagramGenerator.generateDiagram(bpmnModel, "png", highLightedActivitis,highLightedFlows,
processEngineConfiguration.getActivityFontName(),processEngineConfiguration.getLabelFontName(),//防止中文乱码
processEngineConfiguration.getAnnotationFontName(),processEngineConfiguration.getClassLoader(),
1.0 , new Color[] { FlowableConstants.COLOR_NORMAL, FlowableConstants.COLOR_CURRENT },
new HashSet<>(highLightedRunActivitis));
//输出图片到指定路径
IOUtils.copy(in, out);
}catch (Exception e){
log.error("获取流程图出错:{}",e);
throw new BaseException("获取流程图出错");
}
}
/**
<select id="findApprovalHistory" resultType="com.bho.flowable.dto.ApprovalHistoryResp">
select ha.act_name_ as actName,ha.act_type_ as actType,ha.start_time_ as startTime,ha.duration_ as duration,
hc.user_id_ as userId,hc.message_ as message,hc.action_ as action
from act_hi_actinst ha left join act_hi_comment hc on ha.task_id_ = hc.task_id_
where ha.proc_inst_id_ = #{id}
order by ha.start_time_,ha.act_type_
</select>
*/
@Override
public List findApprovalHistory(String id) {
List<ApprovalHistoryResp> approvalHistoryResps = actCommonMapper.findApprovalHistory(id);
List<ApprovalHistoryResp> returnList = new ArrayList<>();
String userId = actCommonMapper.findStartUser(id);
for(int i = 0; i < approvalHistoryResps.size(); i++){
ApprovalHistoryResp resp = approvalHistoryResps.get(i);
if(resp.getActType().contains(ACT_TYPE_GATEWAY)){
continue;
}
if(!StringUtils.isEmpty(resp.getAction()) && !resp.getAction().equals(ACTION_ADD_COMMENT)){
continue;
}
if(resp.getActType().equals(ACT_TYPE_START)){
resp.setOpTypeName(OpTypeEnum.COMMIT.getName());
resp.setUserId(userId);
resp.setActName("开始节点");
resp.setMessage("开始节点跳过");
}else if(resp.getActType().equals(ACT_TYPE_END)){
resp.setOpTypeName(OpTypeEnum.COMMIT.getName());
resp.setActName("结束节点");
resp.setMessage("结束节点跳过");
resp.setUserId(userId);
}else{
if(StringUtils.isEmpty(resp.getMessage())){
if(i < approvalHistoryResps.size() - 1){
resp.setOpTypeName(OpTypeEnum.COMMIT.getName());
resp.setMessage("用户任务跳过");
resp.setUserId(userId);
}
}else{
String message = resp.getMessage();
int index = message.indexOf(CommonConst.UNDER_LINE_STR);
String opTypeKey = message.substring(0, index);
OpTypeEnum opType = OpTypeEnum.getOpType(opTypeKey);
resp.setOpTypeName(opType == null ? "未知" : opType.getName());
resp.setMessage(message.substring(index + 1));
}
}
Long duration = resp.getDuration();
resp.setDurationStr(CommonUtils.getDuration(duration));
returnList.add(resp);
userId = resp.getUserId();
}
return returnList;
}
}
为了区分流程当前审批节点以及已审批过的节点,调整流程图样式,后面截图会看到效果
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.image.ProcessDiagramGenerator;
import java.awt.*;
import java.io.InputStream;
import java.util.List;
import java.util.Set;
public interface ICustomProcessDiagramGenerator extends ProcessDiagramGenerator {
InputStream generateDiagram(BpmnModel bpmnModel, String imageType, List<String> highLightedActivities,
List<String> highLightedFlows, String activityFontName, String labelFontName, String annotationFontName,
ClassLoader customClassLoader, double scaleFactor, Color[] colors, Set<String> currIds);
}
import com.bho.flowable.utils.FlowableConstants;
import org.flowable.bpmn.model.AssociationDirection;
import org.flowable.bpmn.model.GraphicInfo;
import org.flowable.image.exception.FlowableImageException;
import org.flowable.image.impl.DefaultProcessDiagramCanvas;
import org.flowable.image.util.ReflectUtil;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.font.FontRenderContext;
import java.awt.font.LineBreakMeasurer;
import java.awt.font.TextAttribute;
import java.awt.font.TextLayout;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.text.AttributedCharacterIterator;
import java.text.AttributedString;
public class CustomProcessDiagramCanvas extends DefaultProcessDiagramCanvas {
protected static Color LABEL_COLOR = new Color(0, 0, 0);
//font
protected String activityFontName = "宋体";
protected String labelFontName = "宋体";
protected String annotationFontName = "宋体";
private static volatile boolean flag = false;
public CustomProcessDiagramCanvas(int width, int height, int minX, int minY, String imageType) {
super(width, height, minX, minY, imageType);
}
public CustomProcessDiagramCanvas(int width, int height, int minX, int minY, String imageType,
String activityFontName, String labelFontName, String annotationFontName, ClassLoader customClassLoader) {
super(width, height, minX, minY, imageType, activityFontName, labelFontName, annotationFontName,
customClassLoader);
}
public void drawHighLight(boolean isStartOrEnd,boolean isGateway, int x, int y, int width, int height, Color color) {
Paint originalPaint = g.getPaint();
Stroke originalStroke = g.getStroke();
g.setPaint(color);
g.setStroke(MULTI_INSTANCE_STROKE);
if (isStartOrEnd) {// 开始、结束节点画圆
g.drawOval(x, y, width, height);
}else if(isGateway){
int px[] = { x, x + width / 2, x + width, x + width / 2};
int py[] = { y + height / 2, y + height, y + height / 2, y};
g.drawPolygon(px, py, px.length);
}else {// 非开始、结束节点画圆角矩形
RoundRectangle2D rect = new RoundRectangle2D.Double(x, y, width, height, 5, 5);
g.draw(rect);
}
g.setPaint(originalPaint);
g.setStroke(originalStroke);
}
public void drawSequenceflow(int[] xPoints, int[] yPoints, boolean conditional, boolean isDefault,
boolean highLighted, double scaleFactor, Color color) {
drawConnection(xPoints, yPoints, conditional, isDefault, "sequenceFlow", AssociationDirection.ONE, highLighted,
scaleFactor, color);
}
public void drawConnection(int[] xPoints, int[] yPoints, boolean conditional, boolean isDefault,
String connectionType, AssociationDirection associationDirection, boolean highLighted, double scaleFactor,
Color color) {
Paint originalPaint = g.getPaint();
Stroke originalStroke = g.getStroke();
g.setPaint(CONNECTION_COLOR);
if (connectionType.equals("association")) {
g.setStroke(ASSOCIATION_STROKE);
} else if (highLighted) {
g.setPaint(color);
g.setStroke(HIGHLIGHT_FLOW_STROKE);
}
for (int i = 1; i < xPoints.length; i++) {
Integer sourceX = xPoints[i - 1];
Integer sourceY = yPoints[i - 1];
Integer targetX = xPoints[i];
Integer targetY = yPoints[i];
Line2D.Double line = new Line2D.Double(sourceX, sourceY, targetX, targetY);
g.draw(line);
}
if (isDefault) {
Line2D.Double line = new Line2D.Double(xPoints[0], yPoints[0], xPoints[1], yPoints[1]);
drawDefaultSequenceFlowIndicator(line, scaleFactor);
}
if (conditional) {
Line2D.Double line = new Line2D.Double(xPoints[0], yPoints[0], xPoints[1], yPoints[1]);
drawConditionalSequenceFlowIndicator(line, scaleFactor);
}
if (associationDirection.equals(AssociationDirection.ONE)
|| associationDirection.equals(AssociationDirection.BOTH)) {
Line2D.Double line = new Line2D.Double(xPoints[xPoints.length - 2], yPoints[xPoints.length - 2],
xPoints[xPoints.length - 1], yPoints[xPoints.length - 1]);
drawArrowHead(line, scaleFactor);
}
if (associationDirection.equals(AssociationDirection.BOTH)) {
Line2D.Double line = new Line2D.Double(xPoints[1], yPoints[1], xPoints[0], yPoints[0]);
drawArrowHead(line, scaleFactor);
}
g.setPaint(originalPaint);
g.setStroke(originalStroke);
}
public void drawLabel(boolean highLighted, String text, GraphicInfo graphicInfo, boolean centered) {
float interline = 1.0f;
// text
if (text != null && text.length() > 0) {
Paint originalPaint = g.getPaint();
Font originalFont = g.getFont();
if (highLighted) {
g.setPaint(FlowableConstants.COLOR_NORMAL);
} else {
g.setPaint(LABEL_COLOR);
}
g.setFont(new Font(labelFontName, Font.BOLD, 10));
int wrapWidth = 100;
int textY = (int) graphicInfo.getY();
// TODO: use drawMultilineText()
AttributedString as = new AttributedString(text);
as.addAttribute(TextAttribute.FOREGROUND, g.getPaint());
as.addAttribute(TextAttribute.FONT, g.getFont());
AttributedCharacterIterator aci = as.getIterator();
FontRenderContext frc = new FontRenderContext(null, true, false);
LineBreakMeasurer lbm = new LineBreakMeasurer(aci, frc);
while (lbm.getPosition() < text.length()) {
TextLayout tl = lbm.nextLayout(wrapWidth);
textY += tl.getAscent();
Rectangle2D bb = tl.getBounds();
double tX = graphicInfo.getX();
if (centered) {
tX += (int) (graphicInfo.getWidth() / 2 - bb.getWidth() / 2);
}
tl.draw(g, (float) tX, textY);
textY += tl.getDescent() + tl.getLeading() + (interline - 1.0f) * tl.getAscent();
}
// restore originals
g.setFont(originalFont);
g.setPaint(originalPaint);
}
}
@Override
public BufferedImage generateBufferedImage(String imageType) {
if (closed) {
throw new FlowableImageException("ProcessDiagramGenerator already closed");
}
// Try to remove white space
minX = (minX <= FlowableConstants.PROCESS_PADDING) ? FlowableConstants.PROCESS_PADDING : minX;
minY = (minY <= FlowableConstants.PROCESS_PADDING) ? FlowableConstants.PROCESS_PADDING : minY;
BufferedImage imageToSerialize = processDiagram;
if (minX >= 0 && minY >= 0) {
imageToSerialize = processDiagram.getSubimage(
minX - FlowableConstants.PROCESS_PADDING,
minY - FlowableConstants.PROCESS_PADDING,
canvasWidth - minX + FlowableConstants.PROCESS_PADDING,
canvasHeight - minY + FlowableConstants.PROCESS_PADDING);
}
return imageToSerialize;
}
@Override
public void initialize(String imageType) {
activityFontName = "宋体";
labelFontName = "宋体";
this.processDiagram = new BufferedImage(canvasWidth, canvasHeight, BufferedImage.TYPE_INT_ARGB);
this.g = processDiagram.createGraphics();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setPaint(Color.black);
Font font = new Font(activityFontName, Font.BOLD, FONT_SIZE);
g.setFont(font);
this.fontMetrics = g.getFontMetrics();
LABEL_FONT = new Font(labelFontName, Font.ITALIC, 10);
ANNOTATION_FONT = new Font(annotationFontName, Font.PLAIN, FONT_SIZE);
//优化加载速度
if(flag) {
return;
}
try {
USERTASK_IMAGE = ImageIO.read(ReflectUtil.getResource("org/flowable/icons/userTask.png", customClassLoader));
SCRIPTTASK_IMAGE = ImageIO.read(ReflectUtil.getResource("org/flowable/icons/scriptTask.png", customClassLoader));
SERVICETASK_IMAGE = ImageIO.read(ReflectUtil.getResource("org/flowable/icons/serviceTask.png", customClassLoader));
RECEIVETASK_IMAGE = ImageIO.read(ReflectUtil.getResource("org/flowable/icons/receiveTask.png", customClassLoader));
SENDTASK_IMAGE = ImageIO.read(ReflectUtil.getResource("org/flowable/icons/sendTask.png", customClassLoader));
MANUALTASK_IMAGE = ImageIO.read(ReflectUtil.getResource("org/flowable/icons/manualTask.png", customClassLoader));
BUSINESS_RULE_TASK_IMAGE = ImageIO.read(ReflectUtil.getResource("org/flowable/icons/businessRuleTask.png", customClassLoader));
SHELL_TASK_IMAGE = ImageIO.read(ReflectUtil.getResource("org/flowable/icons/shellTask.png", customClassLoader));
CAMEL_TASK_IMAGE = ImageIO.read(ReflectUtil.getResource("org/flowable/icons/camelTask.png", customClassLoader));
MULE_TASK_IMAGE = ImageIO.read(ReflectUtil.getResource("org/flowable/icons/muleTask.png", customClassLoader));
TIMER_IMAGE = ImageIO.read(ReflectUtil.getResource("org/flowable/icons/timer.png", customClassLoader));
COMPENSATE_THROW_IMAGE = ImageIO.read(ReflectUtil.getResource("org/flowable/icons/compensate-throw.png", customClassLoader));
COMPENSATE_CATCH_IMAGE = ImageIO.read(ReflectUtil.getResource("org/flowable/icons/compensate.png", customClassLoader));
ERROR_THROW_IMAGE = ImageIO.read(ReflectUtil.getResource("org/flowable/icons/error-throw.png", customClassLoader));
ERROR_CATCH_IMAGE = ImageIO.read(ReflectUtil.getResource("org/flowable/icons/error.png", customClassLoader));
MESSAGE_THROW_IMAGE = ImageIO.read(ReflectUtil.getResource("org/flowable/icons/message-throw.png", customClassLoader));
MESSAGE_CATCH_IMAGE = ImageIO.read(ReflectUtil.getResource("org/flowable/icons/message.png", customClassLoader));
SIGNAL_THROW_IMAGE = ImageIO.read(ReflectUtil.getResource("org/flowable/icons/signal-throw.png", customClassLoader));
SIGNAL_CATCH_IMAGE = ImageIO.read(ReflectUtil.getResource("org/flowable/icons/signal.png", customClassLoader));
/* String baseUrl = Thread.currentThread().getContextClassLoader().getResource("static/img/flowable/").getPath();
SCRIPTTASK_IMAGE = ImageIO.read(new FileInputStream(baseUrl+"scriptTask.png"));
USERTASK_IMAGE = ImageIO.read(new FileInputStream(baseUrl+"userTask.png"));
SERVICETASK_IMAGE = ImageIO.read(new FileInputStream(baseUrl+"serviceTask.png"));
RECEIVETASK_IMAGE = ImageIO.read(new FileInputStream(baseUrl+"receiveTask.png"));
SENDTASK_IMAGE = ImageIO.read(new FileInputStream(baseUrl+"sendTask.png"));
MANUALTASK_IMAGE = ImageIO.read(new FileInputStream(baseUrl+"manualTask.png"));
BUSINESS_RULE_TASK_IMAGE = ImageIO.read(new FileInputStream(baseUrl+"businessRuleTask.png"));
SHELL_TASK_IMAGE = ImageIO.read(new FileInputStream(baseUrl+"shellTask.png"));
CAMEL_TASK_IMAGE = ImageIO.read(new FileInputStream(baseUrl+"camelTask.png"));
MULE_TASK_IMAGE = ImageIO.read(new FileInputStream(baseUrl+"muleTask.png"));
TIMER_IMAGE = ImageIO.read(new FileInputStream(baseUrl+"timer.png"));
COMPENSATE_THROW_IMAGE = ImageIO.read(new FileInputStream(baseUrl+"compensate-throw.png"));
COMPENSATE_CATCH_IMAGE = ImageIO.read(new FileInputStream(baseUrl+"compensate.png"));
ERROR_THROW_IMAGE = ImageIO.read(new FileInputStream(baseUrl+"error-throw.png"));
ERROR_CATCH_IMAGE = ImageIO.read(new FileInputStream(baseUrl+"error.png"));
MESSAGE_THROW_IMAGE = ImageIO.read(new FileInputStream(baseUrl+"message-throw.png"));
MESSAGE_CATCH_IMAGE = ImageIO.read(new FileInputStream(baseUrl+"message.png"));
SIGNAL_THROW_IMAGE = ImageIO.read(new FileInputStream(baseUrl+"signal-throw.png"));
SIGNAL_CATCH_IMAGE = ImageIO.read(new FileInputStream(baseUrl+"signal.png"));*/
flag = true;
} catch (IOException e) {
flag = false;
LOGGER.warn("Could not load image for process diagram creation: {}", e.getMessage());
}
}
}
5.3.2 前端页面
发起流程
调用flowable提供的接口可查看审批历史和流程图,这个页面只提供查询操作,审批流程在待办里面处理,处理完毕回调接口更新状态。
5.4 流程审批
我的待办 我的已办 办理
5.4.1 后端代码
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.bho.flowable.dto.*;
import com.bho.flowable.form.ReqFormParam;
import com.bho.flowable.mapper.ActCommonMapper;
import com.bho.flowable.service.ActTaskService;
import com.bho.model.BaseException;
import com.bho.model.RestResponse;
import com.bho.user.feign.SysUserApi;
import com.bho.utils.*;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import lombok.extern.slf4j.Slf4j;
import org.flowable.common.engine.impl.identity.Authentication;
import org.flowable.engine.HistoryService;
import org.flowable.engine.TaskService;
import org.flowable.engine.history.HistoricProcessInstance;
import org.flowable.identitylink.api.IdentityLink;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @description:
* @author:wanzh
* @date:2022/1/7
*/
@Slf4j
@Service
public class ActTaskServiceImpl implements ActTaskService {
@Autowired
private ActCommonMapper actCommonMapper;
@Autowired
private SysUserApi sysUserApi;
@Autowired
private TaskService taskService;
@Autowired
private HistoryService historyService;
@Override
public PageInfo findToDo(ReqFormParam reqFormParam) {
Integer userId = SysUserUtils.getCurrentLoginUser().getId();
RestResponse<List<Integer>> restResponse = sysUserApi.findRolesByUserId(userId);
if(restResponse.getCode() != RestResponse.SUCCESS_CODE){
throw new BaseException("查询角色接口出错");
}
List<Integer> roleIdList = restResponse.getData();
PageHelper.startPage(reqFormParam.getPageNum(), reqFormParam.getPageSize()," createTime desc"); // , true
List<MyTaskResp> tasks = actCommonMapper.findToDoTask(reqFormParam.getTitle(),reqFormParam.getName(),reqFormParam.getKey(),userId,roleIdList);
return new PageInfo(tasks);
}
@Override
public PageInfo findDone(ReqFormParam reqFormParam) {
Integer userId = SysUserUtils.getCurrentLoginUser().getId();
RestResponse<List<Integer>> restResponse = sysUserApi.findRolesByUserId(userId);
if(restResponse.getCode() != RestResponse.SUCCESS_CODE){
throw new BaseException("查询角色接口出错");
}
List<Integer> roleIdList = restResponse.getData();
PageHelper.startPage(reqFormParam.getPageNum(), reqFormParam.getPageSize()," createTime desc"); // , true
List<MyTaskResp> tasks = actCommonMapper.findDone(reqFormParam.getTitle(),reqFormParam.getName(),reqFormParam.getKey(),userId,roleIdList);
for(MyTaskResp resp : tasks){
Long duration = resp.getDuration();
resp.setDurationStr(CommonUtils.getDuration(duration));
}
return new PageInfo(tasks);
}
@Override
public void complete(CompleteTaskReq completeTaskReq) {
OpTypeEnum opType = OpTypeEnum.getOpType(completeTaskReq.getApprovalOpType());
if(opType == null){
throw new BaseException("未知的审批操作类型");
}
Integer userId = SysUserUtils.getCurrentLoginUser().getId();
Authentication.setAuthenticatedUserId(String.valueOf(userId));
completeTaskReq.setMessage(completeTaskReq.getApprovalOpType() + CommonConst.UNDER_LINE_STR + completeTaskReq.getMessage());
taskService.addComment(completeTaskReq.getTaskId(), completeTaskReq.getInstanceId(), completeTaskReq.getMessage());//comment为批注内容
// 添加批注信息
if(opType == OpTypeEnum.TRANSFER){
if(StringUtils.isEmpty(completeTaskReq.getUserId())){
Authentication.setAuthenticatedUserId(null);
throw new BaseException("转办用户不能为空");
}
List<IdentityLink> identityLinksForTask = taskService.getIdentityLinksForTask(completeTaskReq.getTaskId());
for(IdentityLink identityLink : identityLinksForTask){
if(!StringUtils.isEmpty(identityLink.getUserId())){
taskService.deleteUserIdentityLink(completeTaskReq.getTaskId(), identityLink.getUserId(), identityLink.getType());
}
if(!StringUtils.isEmpty(identityLink.getGroupId())){
taskService.deleteUserIdentityLink(completeTaskReq.getTaskId(),identityLink.getGroupId(), identityLink.getType());
}
}
taskService.setAssignee(completeTaskReq.getTaskId(), completeTaskReq.getUserId());
if(!StringUtils.isEmpty(completeTaskReq.getTransferUserFieldName())){
taskService.setVariable(completeTaskReq.getTaskId(),completeTaskReq.getTransferUserFieldName(),completeTaskReq.getUserId());
Map<String, Object> variables = new HashMap<>();
variables.put("approvalOpType",completeTaskReq.getApprovalOpType());
variables.put(completeTaskReq.getTransferUserFieldName(),completeTaskReq.getUserId());
HistoricProcessInstance hpi = historyService.createHistoricProcessInstanceQuery().processInstanceId(completeTaskReq.getInstanceId()).singleResult();
if(hpi != null && !StringUtils.isEmpty(hpi.getCallbackId()) && !StringUtils.isEmpty(hpi.getCallbackType())){
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
variables.put("id",hpi.getBusinessKey());
if(hpi.getCallbackType().equals(CommonConst.METHOD_TYPE_POST)){
String result = HttpClientUtils.httpPostJsonAndHeader(hpi.getCallbackId(), JacksonUtil.beanToJson(variables), CommonConst.REQ_HEADER_AUTH, request.getHeader(CommonConst.REQ_HEADER_AUTH));
log.debug("result:{}",result);
}else{
String result = HttpClientUtils.httpPutJsonAndHeader(hpi.getCallbackId(),JacksonUtil.beanToJson(variables),CommonConst.REQ_HEADER_AUTH,request.getHeader(CommonConst.REQ_HEADER_AUTH));
log.debug("result:{}",result);
}
}
}
}else{
Map<String, Object> variables = JacksonUtil.jsonToMap(completeTaskReq.getFormData(), String.class, Object.class);
variables.put("approvalOpType",completeTaskReq.getApprovalOpType());
taskService.complete(completeTaskReq.getTaskId(),variables);
HistoricProcessInstance hpi = historyService.createHistoricProcessInstanceQuery().processInstanceId(completeTaskReq.getInstanceId()).singleResult();
if(hpi != null && !StringUtils.isEmpty(hpi.getCallbackId()) && !StringUtils.isEmpty(hpi.getCallbackType())){
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
variables.put("id",hpi.getBusinessKey());
if(hpi.getCallbackType().equals(CommonConst.METHOD_TYPE_POST)){
String result = HttpClientUtils.httpPostJsonAndHeader(hpi.getCallbackId(), JacksonUtil.beanToJson(variables), CommonConst.REQ_HEADER_AUTH, request.getHeader(CommonConst.REQ_HEADER_AUTH));
log.info("result:{}",result);
}else{
String result = HttpClientUtils.httpPutJsonAndHeader(hpi.getCallbackId(),JacksonUtil.beanToJson(variables),CommonConst.REQ_HEADER_AUTH,request.getHeader(CommonConst.REQ_HEADER_AUTH));
log.info("result:{}",result);
}
}
}
Authentication.setAuthenticatedUserId(null);
}
@Override
public TaskFormResp findForm(String id) {
TaskFormResp taskFormResp = actCommonMapper.findFormByTaskId(id);
if(taskFormResp == null){
throw new BaseException("任务不存在");
}
String modelEditorJson = taskFormResp.getModelEditorJson();
JSONObject jsonObject = JSONObject.parseObject(modelEditorJson);
JSONArray childShapes = jsonObject.getJSONArray("childShapes");
for(int i = 0; i < childShapes.size(); i++){
JSONObject childShape = childShapes.getJSONObject(i);
String resourceId = childShape.getString("resourceId");
JSONObject properties = childShape.getJSONObject("properties");
String overrideid = properties.getString("overrideid");
if(!StringUtils.isEmpty(overrideid)){
resourceId = overrideid;
}
if(resourceId.equals(taskFormResp.getTaskDefKey())){
JSONArray formProperties = new JSONArray();
JSONObject formproperties = properties.getJSONObject("formproperties");
if(formproperties != null){
JSONArray tempFormProperties = formproperties.getJSONArray("formProperties");
if(tempFormProperties != null){
formProperties = tempFormProperties;
}
}
taskFormResp.setModelEditorJson(null);
taskFormResp.setFormJson(formProperties.toJSONString());
}
}
List<OpTypeDto> opTypeList = new ArrayList<>();
String[] split = taskFormResp.getTaskDefKey().split(CommonConst.UNDER_LINE_STR);
for(int i = 1; i < split.length; i++){
String key = split[i];
OpTypeEnum opType = OpTypeEnum.getOpType(key);
if(opType != null){
opTypeList.add(OpTypeDto.builder().key(opType.getKey()).name(opType.getName()).build());
}
}
taskFormResp.setOpTypeList(opTypeList);
return taskFormResp;
}
}
5.4.2 前端页面
待办页面点击办理按钮
为了方便派单还是选择自己
办理页面动态显示表单
6.完结
对于flowable整合到项目里,我这里只是简单整合一下,能满足当前业务场景就行,没有做深入研究,基本都是百度搜索一点点凑起来的,若需要源码可私信,只发送flowable服务代码(会用到其它服务一些共用文件配置,其它服务代码不提供)。若遇到问题也可以共用探讨,一起进步。