公司最近想要使用flowable作为我们工作流引擎,主要用于各类流程审批上。在网上搜索到了很多有参考意义的文章,但有些实现细节还需要自己去摸索。本文的实战包括:
sample源码地址: https://github.com/ChangeChe/flowable
flowable的集成有两部分:flowable-api与flowable-ui。flowable-api主要管创建好流程后怎么使用,flowable-ui就是通过页面去编排我们的流程。
这里的是较新的版本6.7.0。因为自身的框架用的是springboot
,所以需要引入一个starter
。引入的包里有flowable-ui-xxx
是flowable-ui需要的包;其他是我们使用flowable-api需要的包。
6.7.0
org.flowable
flowable-spring-boot-starter-process
${flowable.version}
org.flowable
flowable-idm-spring-configurator
${flowable.version}
org.flowable
flowable-json-converter
${flowable.version}
org.flowable
flowable-ui-modeler-rest
${flowable.version}
org.flowable
flowable-json-converter
org.springframework.boot
spring-boot-starter-log4j2
org.flowable
flowable-ui-modeler-conf
${flowable.version}
org.flowable
flowable-json-converter
org.flowable
flowable-ui-idm-conf
${flowable.version}
org.flowable
flowable-ui-idm-rest
${flowable.version}
org.flowable
flowable-form-engine
${flowable.version}
org.flowable
flowable-form-spring
${flowable.version}
org.flowable
flowable-form-engine-configurator
${flowable.version}
org.flowable
flowable-form-spring-configurator
${flowable.version}
org.flowable
flowable-app-engine
${flowable.version}
org.liquibase
liquibase-core
4.3.5
javax.xml.bind
jaxb-api
# flowable配置
flowable.common.app.idm-url = /idm
flowable.modeler.app.rest-enabled = true
flowable.database-schema-update = true
flowable.process.definition-cache-limit = 1
flowable.xml.encoding = UTF-8
# mybatis配置
mybatis.mapper-locations = classpath:/META-INF/modeler-mybatis-mappings/*.xml
mybatis.config-location = classpath:/META-INF/mybatis-config.xml
mybatis.configuration-properties.blobType = BLOB
mybatis.configuration-properties.boolValue = TRUE
mybatis.configuration-properties.prefix =
@Configuration
@EnableConfigurationProperties({FlowableIdmAppProperties.class, FlowableModelerAppProperties.class})
@ComponentScan(basePackages = {
"org.flowable.ui.idm.conf",
// "org.flowable.ui.idm.security",
"org.flowable.ui.idm.service",
"org.flowable.ui.modeler.repository",
"org.flowable.ui.modeler.service",
// "org.flowable.ui.common.filter",
"org.flowable.ui.common.service",
"org.flowable.ui.common.repository",
// "org.flowable.ui.common.security",
"org.flowable.ui.common.tenant",
"org.flowable.form"
}, excludeFilters = {
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = org.flowable.ui.idm.conf.ApplicationConfiguration.class)
})
public class ApplicationConfiguration implements BeanPostProcessor {
@Bean
public ServletRegistrationBean apiServlet(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 IDM App API Servlet");
registrationBean.setLoadOnStartup(1);
registrationBean.setAsyncSupported(true);
return registrationBean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
// 这里与用户鉴权相关 这里先省略
...
}
}
@Configuration
@ComponentScan(basePackages = {
"org.flowable.ui.idm.rest.app",
"org.flowable.ui.common.rest.exception",
"org.flowable.ui.modeler.rest.app",
"org.flowable.ui.common.rest"
}, excludeFilters = {
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = RemoteAccountResource.class),
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = StencilSetResource.class),
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = EditorUsersResource.class),
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = EditorGroupsResource.class)
})
@Slf4j
public class AppDispatcherServletConfiguration implements WebMvcRegistrations {
@Bean
public SessionLocaleResolver localeResolver() {
return new SessionLocaleResolver();
}
@Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
log.debug("Configuring localeChangeInterceptor");
LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
localeChangeInterceptor.setParamName("language");
return localeChangeInterceptor;
}
@Override
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
log.debug("Creating requestMappingHandlerMapping");
RequestMappingHandlerMapping requestMappingHandlerMapping = new RequestMappingHandlerMapping();
requestMappingHandlerMapping.setUseSuffixPatternMatch(false);
requestMappingHandlerMapping.setRemoveSemicolonContent(false);
Object[] interceptors = {localeChangeInterceptor()};
requestMappingHandlerMapping.setInterceptors(interceptors);
return requestMappingHandlerMapping;
}
}
路由集成后,我们需要将资源文件放到我们的resource
目录下。首先需要将路由跟资源路径绑定:
@Configuration
public class WebMvcConfigurerAdapter implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/modeler/**")
.addResourceLocations("classpath:/static/modeler/");
registry.addResourceHandler("/idm/**")
.addResourceLocations("classpath:/static/idm/");
}
}
然后从flowable-engine
源码中将前端资源拷贝到自己的项目下:
modules/flowable-ui
目录flowable-idm-frontend
、flowable-modeler-frontend
、flowable-task-frontend
到自己的项目下,如下图:@Configuration
@Slf4j
public class DatabaseConfiguration {
protected static final String LIQUIBASE_CHANGELOG_PREFIX = "ACT_DE_";
@Bean
public Liquibase liquibase(DataSource dataSource) {
log.info("Configuring Liquibase");
Liquibase liquibase = null;
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 = new Liquibase("META-INF/liquibase/flowable-modeler-app-db-changelog.xml", new ClassLoaderResourceAccessor(), database);
liquibase.update("flowable");
return liquibase;
} catch (Exception e) {
throw new InternalServerErrorException("Error creating liquibase database", e);
} finally {
closeDatabase(liquibase);
}
}
private void closeDatabase(Liquibase liquibase) {
if (liquibase != null) {
Database database = liquibase.getDatabase();
if (database != null) {
try {
database.close();
} catch (DatabaseException e) {
log.warn("Error closing database", e);
}
}
}
}
}
经过上述步骤,我们已经能在自己的项目里启动flowable-ui了。
使用flowable-idm进行用户授权不满足我们的使用需求,idm也提供了ldap的登录方式;在这里,我们使用自己的用户中心进行登录,我们的用户中心对外提供了登录/用户鉴权接口。参考了LDAP提供的JavaConfig(FlowableLdapAutoConfiguration
)知道我们需要实现自己的IdmIdentityServiceImpl
以及UserDetailsService
IdmIdentityServiceImpl
@Slf4j
public class MyIdentityServiceImpl extends IdmIdentityServiceImpl {
MyUserCenterProperties properties;
ApplicationContext applicationContext;
public MyIdentityServiceImpl(ApplicationContext applicationContext, MyUserCenterProperties properties, IdmEngineConfiguration idmEngineConfiguration) {
super(idmEngineConfiguration);
this.applicationContext = applicationContext;
this.properties = properties;
}
@Override
public boolean checkPassword(String userId, String password) {
return executeCheckPassword(userId, password);
}
protected boolean executeCheckPassword(final String userId, final String password) {
// Extra password check, see http://forums.activiti.org/comment/22312
if (password == null || password.length() == 0) {
throw new FlowableException("Null or empty passwords are not allowed!");
}
try {
// 调用用户中心服务进行登录
UserCenterFeignService userCenterFeignService = applicationContext.getBean(UserCenterFeignService.class);
userCenterFeignService.login(new UcLoginReq(properties.getAppId(), userId, password));
return true;
} catch (DecodeException e) {
log.error("用户中心认证失败:{}", userId, e);
return false;
} catch (FlowableException e) {
log.error("Could not authenticate user : {}", userId, e);
return false;
}
}
// 很多方法直接不支持
// throw new FlowableException("My identity service doesn't support getting groups with privilege");
...
@Override
public UserQuery createUserQuery() {
// 通过调用其他服务来查询
SoaCommonPhpFeignService soaCommonPhpFeignService = applicationContext.getBean(SoaCommonPhpFeignService.class);
SoaCommonJavaFeignService soaCommonJavaFeignService = applicationContext.getBean(SoaCommonJavaFeignService.class);
return new MyUserQueryImpl(soaCommonPhpFeignService, soaCommonJavaFeignService);
}
// 这个在对任务查询时需要针对user_id/group_id查询时需要
@Override
public GroupQuery createGroupQuery() {
return new MyGroupQueryImpl();
}
}
public class MyUserQueryImpl extends UserQueryImpl {
SoaCommonPhpFeignService soaCommonPhpFeignService;
SoaCommonJavaFeignService soaCommonJavaFeignService;
public MyUserQueryImpl(SoaCommonPhpFeignService soaCommonPhpFeignService, SoaCommonJavaFeignService soaCommonJavaFeignService) {
this.soaCommonPhpFeignService = soaCommonPhpFeignService;
this.soaCommonJavaFeignService = soaCommonJavaFeignService;
}
@Override
public long executeCount(CommandContext commandContext) {
return executeQuery().size();
}
@Override
public List executeList(CommandContext commandContext) {
return executeQuery();
}
protected List executeQuery() {
if (getId() != null) {
List result = new ArrayList<>();
UserEntity user = findById(getId());
if (user != null) {
result.add(user);
}
return result;
} else if (getIdIgnoreCase() != null) {
List result = new ArrayList<>();
UserEntity user = findById(getIdIgnoreCase());
if (user != null) {
result.add(user);
}
return result;
} else if (getFullNameLike() != null) {
return executeNameQuery(getFullNameLike());
} else if (getFullNameLikeIgnoreCase() != null) {
return executeNameQuery(getFullNameLikeIgnoreCase());
} else {
return executeAllUserQuery();
}
}
// 我们的使用场景并不需要这些返回值,所以都返回null
protected List executeNameQuery(String name) {
return null;
}
protected List executeAllUserQuery() {
return null;
}
protected UserEntity findById(final String userId) {
return null;
}
}
public class MyGroupQueryImpl extends GroupQueryImpl {
@Override
public List executeList(CommandContext commandContext) {
// 也不需要这里的值,只是为了在执行其他逻辑时不报错
return new ArrayList<>();
}
}
UserDetailsService
@Service
public class MyUserDetailsServiceImpl implements UserDetailsService {
@Autowired
SoaCommonPhpFeignService soaCommonPhpFeignService;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
// 去别的服务查询用户 MySoaEmployeeDetailResp需要实现UserDetails接口
MySoaEmployeeDetailResp employeeByAccount = soaCommonPhpFeignService.getEmployeeByAccount(s);
if (null == employeeByAccount) {
throw new UsernameNotFoundException("用户不存在");
}
return employeeByAccount;
}
}
@Configuration(proxyBeanMethods = false)
@AutoConfigureBefore({
FlowableUiSecurityAutoConfiguration.class,
FlowableSecurityAutoConfiguration.class,
IdmEngineServicesAutoConfiguration.class,
ProcessEngineServicesAutoConfiguration.class,
IdmSecurityConfiguration.class
})
@EnableConfigurationProperties({
MyUserCenterProperties.class
})
public class MyUserCenterAutoConfiguration implements ApplicationContextAware {
protected final MyUserCenterProperties properties;
ApplicationContext applicationContext;
public MyUserCenterAutoConfiguration(MyUserCenterProperties properties) {
this.properties = properties;
}
@Bean
public EngineConfigurationConfigurer myIdmEngineConfigurer() {
return idmEngineConfiguration -> idmEngineConfiguration
.setIdmIdentityService(new MyIdentityServiceImpl(applicationContext, properties, idmEngineConfiguration));
}
@Bean
public FlowableAuthenticationProvider flowableAuthenticationProvider(IdmIdentityService idmIdentitySerivce, @Qualifier("myUserDetailsServiceImpl") UserDetailsService userDetailsService) {
return new FlowableAuthenticationProvider(idmIdentitySerivce, userDetailsService);
}
@Bean
@ConditionalOnMissingBean
public RememberMeServices flowableUiRememberMeService(FlowableCommonAppProperties properties, @Qualifier("myUserDetailsServiceImpl") UserDetailsService userDetailsService, PersistentTokenService persistentTokenService) {
return new CustomPersistentRememberMeServices(properties, userDetailsService, persistentTokenService);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
经过上述步骤,我们可以用我们自己的账号登录flowable-ui
并通过可视化工具编排流程了。但在我们通过flowable-api
开始一个流程时,它会告诉我们这个流程不存在。这是怎么回事呢?
这是因为org.flowable.engine.RuntimeService#startProcessInstanceXXX
是要在act_re_procdef
表里找到流程定义,而我们通过flowable-ui
定义的流程还在act_de_model
里。那么怎么将act_de_model
里的数据同步到act_re_procdef
去呢?
flowable
给我们提供了org.flowable.engine.RepositoryService#createDeployment
这个api,通过这个api我们就能将act_de_model
里的数据同步到act_re_procdef
去了;同理,还有flowable-ui
里的表单模型也可以通过org.flowable.form.api.FormRepositoryService#createDeployment
同步到act_fo_form_definition
里去。
这里用了定时器执行这个规则:
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.repository.Deployment;
import org.flowable.form.api.FormDeployment;
import org.flowable.form.api.FormRepositoryService;
import org.flowable.ui.modeler.domain.AbstractModel;
import org.flowable.ui.modeler.serviceapi.ModelService;
@Configuration
public class ModelActiveJob {
@Autowired
ModelService modelService;
@Autowired
RepositoryService repositoryService;
@Autowired
FormRepositoryService formRepositoryService;
@Autowired
ITaskService taskService;
/**
* 激活流程定义
*/
@Scheduled(cron = "0/5 * * * * ?")
public void activeProcessDefinition() {
// 查找BPMN模型
List modelsByModelType = modelService.getModelsByModelType(ModelTypeConst.BPMN);
for (AbstractModel model: modelsByModelType) {
Date modelLastUpdated = model.getLastUpdated();
// 查询该流程模型最新的发布记录
List list = repositoryService.createDeploymentQuery().deploymentKey(model.getKey()).latest().list();
boolean deploy = true;
if (list.size() > 0) {
Deployment deployment = list.get(0);
Date deploymentTime = deployment.getDeploymentTime();
// 如果模型的更新时间比最新的发布时间小,说明没有更新
if (modelLastUpdated.compareTo(deploymentTime) < 0) {
deploy = false;
}
}
// 发布
if (deploy) {
BpmnModel bpmnModel = modelService.getBpmnModel(model);
repositoryService.createDeployment().name(model.getName()).key(model.getKey()).addBpmnModel(model.getKey() + ".bpmn", bpmnModel).deploy();
}
}
}
/**
* 激活表单
*/
@Scheduled(cron = "0/5 * * * * ?")
public void activeFormDefinition() {
// 查找FORM模型
List modelsByModelType = modelService.getModelsByModelType(ModelTypeConst.FORM);
for (AbstractModel model: modelsByModelType) {
Date modelLastUpdated = model.getLastUpdated();
List list = formRepositoryService.createDeploymentQuery().formDefinitionKey(model.getKey()).list();
boolean deploy = true;
if (list.size() > 0) {
list.sort((o1, o2) -> o2.getDeploymentTime().compareTo(o1.getDeploymentTime()));
// 查询该表单模型最新的发布记录
FormDeployment formDeployment = list.get(0);
Date deploymentTime = formDeployment.getDeploymentTime();
// 如果模型的更新时间比最新的发布时间小,说明没有更新
if (modelLastUpdated.compareTo(deploymentTime) < 0) {
deploy = false;
}
}
// 发布
if (deploy) {
formRepositoryService.createDeployment().name(model.getName()).addFormDefinition(model.getKey() + ".form", model.getModelEditorJson()).deploy();
}
}
}
}
在审批执行过程中,可能需要查询我们自己的业务数据库,对一些操作进行约束或者怎么地;因为flowable
是一个基础服务,我们不可能把flowable
的相关数据表放到业务数据库里,所以就需要在项目里使用多数据源。
我们使用的是mybatis-plus
作为我们的数据库工具,它提供了@DS
让我们在多数据源的情况下可以进行数据源切换,但这有一个问题,就是在一个服务里使用用两个数据源是会出现误读的情况的:
db1.tableA not exists
而像我们实现的一些监听器,都是在flowable
里访问我们的业务数据库,所以这种方式不满足我们的需求,需要重新制定规则。
动态数据源的配置很简单
spring.datasource.type = com.alibaba.druid.pool.DruidDataSource
spring.datasource.dynamic.primary = flowable
spring.datasource.dynamic.datasource.flowable.username =
spring.datasource.dynamic.datasource.flowable.password =
spring.datasource.dynamic.datasource.flowable.driver-class-name = com.mysql.cj.jdbc.Driver
spring.datasource.dynamic.datasource.flowable.url =
spring.datasource.dynamic.datasource.flowable.druid.initial-size = 10
spring.datasource.dynamic.datasource.flowable.druid.max-active = 100
spring.datasource.dynamic.datasource.flowable.druid.min-idle = 10
spring.datasource.dynamic.datasource.flowable.druid.max-wait = 10
spring.datasource.dynamic.datasource.biz.username =
spring.datasource.dynamic.datasource.biz.password =
spring.datasource.dynamic.datasource.biz.driver-class-name = com.mysql.cj.jdbc.Driver
spring.datasource.dynamic.datasource.biz.url =
spring.datasource.dynamic.datasource.biz.druid.initial-size = 10
spring.datasource.dynamic.datasource.biz.druid.max-active = 100
spring.datasource.dynamic.datasource.biz.druid.min-idle = 10
spring.datasource.dynamic.datasource.biz.druid.max-wait = 10
@Configuration
@Slf4j
public class DatabaseConfiguration {
protected static final String LIQUIBASE_CHANGELOG_PREFIX = "ACT_DE_";
/**
* flowable数据源
*/
@Bean
@ConfigurationProperties("spring.datasource.dynamic.datasource.flowable")
@Primary
public DataSource flowableDataSource(){
log.info("加载主数据源flowable DataSource.");
return DruidDataSourceBuilder.create().build();
}
/**
* 业务
*/
@Bean
@ConfigurationProperties("spring.datasource.dynamic.datasource.biz")
public DataSource bizDataSource(){
log.info("加载从数据源biz DataSource.");
return DruidDataSourceBuilder.create().build();
}
/**
* 动态数据源
*/
@Bean
public DataSource myRoutingDataSource(@Qualifier("flowableDataSource") DataSource flowableDataSource,
@Qualifier("bizDataSource") DataSource courseDataSource) {
log.info("加载[flowableDataSource-bizDataSource]设置为动态数据源DynamicDataSource.");
Map
进行到这一步可以感觉到跟Mybatis读写分离很像。后续也差不多,创建一个org.apache.ibatis.plugin.Interceptor
并将其设置到com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean
;不同的就是org.apache.ibatis.plugin.Interceptor
里的规则不同。在读写分离中,我们根据sql
里的关键字(UPDATE
、SELECT
、DELETE
…)来切换,这里我们怎么区分呢?
目前我们这边的实现方式是在Mapper
类上打上一个注解,声明这个Mapper
要用哪个库,再在Interceptor
里拦截进行切换。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DBType {
DBTypeEnum value() default DBTypeEnum.FLOWABLE;
}
@DBType(DBTypeEnum.BIZ)
public interface AuthorityDepartmentMapper extends BaseMapper {
...
}
import liquibase.util.StringUtil;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
@Slf4j
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {
MappedStatement.class, Object.class }),
@Signature(type = Executor.class, method = "query", args = {
MappedStatement.class, Object.class, RowBounds.class,
ResultHandler.class }),
@Signature(type = Executor.class, method = "close", args = {boolean.class})
})
public class DbSelectorInterceptor implements Interceptor {
private static final Map CACHE_MAP = new ConcurrentHashMap<>();
@Override
public Object intercept(Invocation invocation) throws Throwable {
String methodName = invocation.getMethod().getName();
String closeMethodName = "close";
DBTypeEnum databaseType = null;
if(!closeMethodName.equals(methodName)) {
Object[] objects = invocation.getArgs();
MappedStatement ms = (MappedStatement) objects[0];
if((databaseType = CACHE_MAP.get(ms.getId())) == null) {
// statementId的组成为xxx.xxx.xxMapper.funcName
String statementId = ms.getId();
String[] parts = statementId.split("\\.");
String[] strings = Arrays.copyOf(parts, parts.length - 1);
String className = StringUtil.join(strings, ".");
Class> aClass = Class.forName(className);
DBType annotation = aClass.getAnnotation(DBType.class);
// 默认使用FLOWABLE库
if (null == annotation) {
databaseType = DBTypeEnum.FLOWABLE;
} else {
databaseType = annotation.value();
}
log.debug("设置方法[{}] use [{}] Strategy, SqlCommandType [{}]..", ms.getId(), databaseType.name(), ms.getSqlCommandType().name());
CACHE_MAP.put(ms.getId(), databaseType);
}
} else {
log.debug("close方法 重置为 [{}] Strategy", DBTypeEnum.FLOWABLE.name());
databaseType = DBTypeEnum.FLOWABLE;
}
DbContextHolder.set(databaseType);
return invocation.proceed();
}
...
}
至此,数据库切换也搞定了~
任务节点的自动分配是我们对flowable
的重要增强。目标是能根据现有的分配规则进行抽象,通过在flowable-ui
上配置的方式来指定分配规则。
根据我们现有的规则总结了这几类分配类型:
类型 | 说明 |
---|---|
fixed-user | 指定用户 |
user-major | 流程提交者的部门主管 |
fixed-department | 指定部门-部门下的所有人 |
fixed-department-major | 指定部门主管 |
…
这里只是举个例子,分配类型是根据自己公司实际的业务规则抽象的。
有了分配类型后,怎么实现自动分配呢?
这里是在任务的任务监听器
的create
事件监听去进行分配的。
import org.flowable.common.engine.impl.el.FixedValue;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.TaskService;
import org.flowable.engine.delegate.TaskListener;
import org.flowable.engine.repository.ProcessDefinition;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.task.api.Task;
import org.flowable.task.service.delegate.DelegateTask;
public abstract class AbstractAssignDelegate implements AssignDelegate, TaskListener {
/**
* 自动分配类型
*/
FixedValue assignType;
/**
* 自动分配依据
*/
FixedValue assignValue;
@Override
public void notify(DelegateTask delegateTask) {
TaskService taskService = SpringUtil.getBean(TaskService.class);
List list = taskService.createTaskQuery().taskId(delegateTask.getId()).list();
if (list.size() < 1) {
return;
}
Task task = list.get(0);
autoAssign(task.getProcessInstanceId(), task);
}
/**
* 自动分配
* @param processInstanceId
* @param taskId
*/
protected void autoAssign(String processInstanceId, Task task) {
...
String taskId = task.getId();
if (null != userId) {
taskService.addCandidateUser(taskId, userId);
} else if (null != department) {
taskService.addCandidateGroup(taskId, department);
} else if (null != role) {
taskService.addCandidateGroup(taskId, role);
}
...
}
}
import org.flowable.common.engine.impl.el.FixedValue;
import java.util.ArrayList;
import java.util.List;
/**
* 除fixed-user会保存userId外,其他方式都是保存的部门-角色信息,便于数据扩展
* @author Chenjing
*/
public interface AssignDelegate {
/**
* 角色存储前缀
* 部门与角色都存在groupId属性中,通过前缀区分
*/
static final String ROLE_PREFIX = "$$";
/**
* 指定用户
* @param fixedUser
* @return
*/
default String getFixedUser(FixedValue fixedUser) {return fixedUser.getExpressionText();}
/**
* 指定部门
* @param fixedDepartment
* @return
*/
default String getFixedDepartment(FixedValue fixedDepartment) {return fixedDepartment.getExpressionText();}
/**
* 指定角色
* @param fixedRole
* @return
*/
default String getFixedRole(FixedValue fixedRole) {return ROLE_PREFIX + fixedRole.getExpressionText();}
...
}
我们利用了task
的identitylink
(act_hi_identitylink
表)。约定type=candidate
的user/group有审批权限,type=participant
有查看权限。