基于若依开发管理项目中引入工作流引擎activiti7,包含前后端(原创)
原项目中用到了工作流引擎,使用若依框架开发,
原二开使用项目:https://gitee.com/y_project/RuoYi-Vue
基于activiti7地址:https://gitee.com/smell2/ruoyi-vue-activiti
导入模块到原二开项目中
使用步骤
- admin导入bpmn文件,或者绘制工作流,并激活。
- post为普通员工进入我的审批可以查看所有审批(经销商角色)
- 数据会存入历史表和任务表
- 历史表展现全部任务数据,task会展示对应岗位的相应条数据
- 登录商管账号,进入代办任务
- 进行审批,通过则状态变为“待财务审核”,进入下一节点;不通过则状态变为“审核失败”,流转结束
- 登录财务账号,进入代办任务
- 进行审批,通过则状态变为“审核成功”,流转结束;不通过则状态变为“审核失败”,流转结束
- 经销商可以查看审批,流转完成
注意事项
工作流程菜单在开发工具查看(没有的话需要事先插入db和相关代码),在线绘制进行bpmn格式的流程图,部署流程导入已经存在的bpmn文件,根据流程key,在java中进行声明和调用该key即可正式使用该工作流
后端相关
activiti7相关依赖
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-spring-boot-starter</artifactId>
<version>7.1.0.M4</version>
</dependency>
<dependency>
<groupId>org.activiti.dependencies</groupId>
<artifactId>activiti-dependencies</artifactId>
<version>7.1.0.M4</version>
<type>pom</type>
</dependency>
解决依赖冲突
activiti7新版本会和mybatis和spring security起冲突,如果项目使用shiro等安全框架会有更大的适配问题
原项目有spring security的情况下需加上配置文件属性
main:
allow-bean-definition-overriding: true
activity配置文件属性如下,包括检测流程定义,自动更新和生成db系统表,历史数据级别和使用等
activiti:
check-process-definitions: false
database-schema-update: true
history-level: full
db-history-used: true
db依赖问题或mybatis版本冲突解决方法如下
<exclusions>
<exclusion>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
</exclusion>
</exclusions>
数据源配置,给url加上如下后缀属性
%2B8&nullCatalogMeansCurrent=true
完整url格式如下:
url: jdbc:mysql://localhost:99999/test11111111?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&nullCatalogMeansCurrent=true
在使用mysql-connect 8.+以上版本的时候需要添加***nullCatalogMeansCurrent=true***参数,否则在使用mybatis-generator生成表对应的xml等时会扫描整个服务器里面的全部数据库中的表,而不是扫描对应数据库的表。因此mysql会扫描所有的库来找表,如果其他库中有相同名称的表,activiti就以为找到了,本质上这个表在当前数据库中并不存在。
接口
流程部署
可以将bpmn文件放在resource下的processes目录下,activiti启动的时候会自动加载该目录下的bpmn文件,或者通过调用接口方式部署:
- 上传文件部署
@PostMapping("/uploadFileAndDeployment")
public BaseResponse uploadFileAndDeployment(
@RequestParam("processFile")MultipartFile processFile,
@RequestParam(value = "processName",required = false) String processName){
String originalFilename = processFile.getOriginalFilename();
String extension = FilenameUtils.getExtension(originalFilename);
if (processName != null){
processName = originalFilename;
}
try {
InputStream inputStream = processFile.getInputStream();
Deployment deployment = null;
if ("zip".equals(extension)){
// 压缩包部署方式
ZipInputStream zipInputStream = new ZipInputStream(inputStream);
deployment = repositoryService.createDeployment().addZipInputStream(zipInputStream).name(processName).deploy();
}else if ("bpmn".equals(extension)){
// bpmn文件部署方式
deployment = repositoryService.createDeployment().addInputStream(originalFilename,inputStream).name(processName).deploy();
}
return BaseResponse.success(deployment);
} catch (IOException e) {
e.printStackTrace();
}
return BaseResponse.success();
}
- 上传BPMN内容字符串部署
@PostMapping("/postBPMNAndDeployment")
public BaseResponse postBPMNAndDeployment(@RequestBody AddXMLRequest addXMLRequest){
Deployment deploy = repositoryService.createDeployment()
// .addString 第一次参数的名字如果没有添加.bpmn的话,不会插入到 ACT_RE_DEPLOYMENT 表中
.addString(addXMLRequest.getProcessName()+".bpmn", addXMLRequest.getBpmnContent())
.name(addXMLRequest.getProcessName())
.deploy();
return BaseResponse.success(deploy);
}
- 获取流程资源文件
@GetMapping("/getProcessDefineXML")
public void getProcessDefineXML(String deploymentId, String resourceName, HttpServletResponse response){
try {
InputStream inputStream = repositoryService.getResourceAsStream(deploymentId,resourceName);
int count = inputStream.available();
byte[] bytes = new byte[count];
response.setContentType("text/xml");
OutputStream outputStream = response.getOutputStream();
while (inputStream.read(bytes) != -1) {
outputStream.write(bytes);
}
inputStream.close();
} catch (Exception e) {
e.toString();
}
}
流程实例
- 启动
@PostMapping("/startProcess")
public BaseResponse startProcess(
String processDefinitionKey,
String instanceName,
@AuthenticationPrincipal LocalUserDetail userDetail){
ProcessInstance processInstance = null;
try{
StartProcessPayload startProcessPayload = ProcessPayloadBuilder.start().withProcessDefinitionKey(processDefinitionKey)
.withBusinessKey("businessKey")
.withVariable("sponsor",userDetail.getUsername())
.withName(instanceName).build();
processInstance = processRuntime.start(startProcessPayload);
}catch (Exception e){
System.out.println(e);
return BaseResponse.error("开启失败:"+e.getLocalizedMessage());
}
return BaseResponse.success(processInstance);
}
- 挂起
@PostMapping("/suspendInstance/{instanceId}")
public BaseResponse suspendInstance(@PathVariable String instanceId){
ProcessInstance processInstance = processRuntime.suspend(ProcessPayloadBuilder.suspend().withProcessInstanceId(instanceId).build());
return BaseResponse.success(processInstance);
}
- 激活
@PostMapping("/resumeInstance/{instanceId}")
public BaseResponse resumeInstance(@PathVariable String instanceId){
ProcessInstance processInstance = processRuntime
.resume(ProcessPayloadBuilder.resume().withProcessInstanceId(instanceId).build());
return BaseResponse.success(processInstance);
}
任务数据
- 通过taskid完成任务
@PostMapping("/completeTask/{taskId}")
public BaseResponse completeTask(@PathVariable String taskId){
Task task = taskRuntime.task(taskId);
if (task.getAssignee()==null){
// 说明任务需要拾取
taskRuntime.claim(TaskPayloadBuilder.claim().withTaskId(taskId).build());
}
taskRuntime.complete(TaskPayloadBuilder.complete().withTaskId(taskId).build());
return BaseResponse.success();
}
- 获取自己的任务(与鉴权机制挂钩)
@GetMapping("/getTasks")
public BaseResponse getTasks(){
Page<Task> taskPage = taskRuntime.tasks(Pageable.of(0, 100));
List<Task> tasks = taskPage.getContent();
List<TaskVO> taskVOS = new ArrayList<>();
for (Task task : tasks) {
TaskVO taskVO = TaskVO.of(task);
ProcessInstance instance = processRuntime.processInstance(task.getProcessInstanceId());
taskVO.setInstanceName(instance.getName());
taskVOS.add(taskVO);
}
return BaseResponse.success(taskVOS);
}
历史数据
- 查询
public List<HistoricActivityInstanceVO> getProcessHistoryByBusinessKey(String businessKey) {
ProcessInstance instance = runtimeService.createProcessInstanceQuery().processInstanceBusinessKey(businessKey).singleResult();
List<HistoricActivityInstance> historicActivityInstanceList = historyService.createHistoricActivityInstanceQuery().processInstanceId(instance.getId())
.orderByHistoricActivityInstanceStartTime().asc().list();
List<HistoricActivityInstanceVO> historicActivityInstanceVOList = new ArrayList<>();
historicActivityInstanceList.forEach(historicActivityInstance -> historicActivityInstanceVOList.add(VOConverter.getHistoricActivityInstanceVO(historicActivityInstance)));
return historicActivityInstanceVOList;
}
- 详情查询
HistoricDetailQuery historicDetailQuery = historyService.createHistoricDetailQuery();
List<HistoricDetail> historicDetails = historicDetailQuery.processInstanceId(instanceId).orderByTime().list();
for (HistoricDetail hd: historicDetails) {
System.out.println("流程实例ID:"+hd.getProcessInstanceId());
System.out.println("活动实例ID:"+hd.getActivityInstanceId());
System.out.println("执行ID:"+hd.getTaskId());
System.out.println("记录时间:"+hd.getTime());
}
- 历史流程实例查询
HistoricProcessInstanceQuery historicProcessInstanceQuery = historyService.createHistoricProcessInstanceQuery();
List<HistoricProcessInstance> processInstances = historicProcessInstanceQuery.processDefinitionId(processDefinitionId).list();
for (HistoricProcessInstance hpi : processInstances) {
System.out.println("业务ID:"+hpi.getBusinessKey());
System.out.println("流程定义ID:"+hpi.getProcessDefinitionId());
System.out.println("流程定义Key:"+hpi.getProcessDefinitionKey());
System.out.println("流程定义名称:"+hpi.getProcessDefinitionName());
System.out.println("流程定义版本:"+hpi.getProcessDefinitionVersion());
System.out.println("流程部署ID:"+hpi.getDeploymentId());
System.out.println("开始时间:"+hpi.getStartTime());
System.out.println("结束时间:"+hpi.getEndTime());
}
- 任务历史查询(某一次流程的执行经历的多少任务)
HistoricTaskInstanceQuery historicTaskInstanceQuery = historyService.createHistoricTaskInstanceQuery();
List<HistoricTaskInstance> taskInstances = historicTaskInstanceQuery.taskId(taskId).list();
for (HistoricTaskInstance hti : taskInstances) {
System.out.println("开始时间:"+hti.getStartTime());
System.out.println("结束时间:"+hti.getEndTime());
System.out.println("任务拾取时间:"+hti.getClaimTime());
System.out.println("删除原因:"+hti.getDeleteReason());
}
github例子:https://github.com/Activiti/activiti-examples
鉴权机制
官方security轮子
@Component
public class SecurityUtil {
// 模拟调用了SpringSecurity 登录鉴权
private Logger logger = LoggerFactory.getLogger(SecurityUtil.class);
@Autowired
private UserDetailsService userDetailsService;
public void logInAs(String username) {
UserDetails user = userDetailsService.loadUserByUsername(username);
if (user == null) {
throw new IllegalStateException("User " + username + " doesn't exist, please provide a valid user");
}
logger.info("> Logged in as: " + username);
SecurityContextHolder.setContext(new SecurityContextImpl(new Authentication() {
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return user.getAuthorities();
}
@Override
public Object getCredentials() {
return user.getPassword();
}
@Override
public Object getDetails() {
return user;
}
@Override
public Object getPrincipal() {
return user;
}
@Override
public boolean isAuthenticated() {
return true;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
}
@Override
public String getName() {
return user.getUsername();
}
}));
org.activiti.engine.impl.identity.Authentication.setAuthenticatedUserId(username);
}
}
与原项目spring security整合
新api包括taskRuntime和processRuntime都会强制使用security,源码使用了如下注解:
@PreAuthorize(“hasRole(‘ACTIVITI_USER’)”)
直接使用接口会导致无权限不允许访问
activiti7中对原有的一些接口做了二次封装,从而进一步简化了用户的使用流程。
通过查看这个两个API的实现类源码来看,调用的话需要调用的用户含有ACTIVITI_USER角色权限。所以,如果没有使用SpringSecurity的话,这两个API便不能直接调用。
- 先在用户验证处理中插入GROUP_的岗位post和加入ACTIVITI_USER的role
public UserDetails createLoginUser(SysUser user)
{
Set<String> postCode = sysPostService.selectPostCodeByUserId(user.getUserId());
postCode = postCode.parallelStream().map( s -> "GROUP_" + s).collect(Collectors.toSet());
postCode.add("ROLE_ACTIVITI_USER");
List<SimpleGrantedAuthority> collect = postCode.stream().map(s -> new SimpleGrantedAuthority(s)).collect(Collectors.toList());
return new LoginUser(user, permissionService.getMenuPermission(user), collect);
//return new LoginUser(user, permissionService.getMenuPermission(user));
}
- 在login controller中,每次登录的时候鉴权
// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
authentication = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(username, password));
最后将信息放入token中处理
if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication()))
{
tokenService.verifyToken(loginUser);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
- SecurityConfig也别忘记配
httpSecurity
// CSRF禁用,因为不使用session
.csrf().disable()
// 认证失败处理类
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 过滤请求
.authorizeRequests()
// 对于登录login 验证码captchaImage 允许匿名访问
.antMatchers("/ssologin/token", "/ssologin","/login", "/captchaImage","/getssourl","/ssologin").anonymous()
.antMatchers(
HttpMethod.GET,
"/*.html",
"/**/*.html",
"/**/*.css",
"/**/*.js"
).permitAll()
.antMatchers("/processDefinition/**").permitAll()
.antMatchers("/activitiHistory/**").permitAll()
.antMatchers("/profile/**").anonymous()
.antMatchers("/common/download**").anonymous()
.antMatchers("/common/download/resource**").anonymous()
.antMatchers("/swagger-ui.html").anonymous()
.antMatchers("/swagger-resources/**").anonymous()
.antMatchers("/webjars/**").anonymous()
.antMatchers("/*/api-docs").anonymous()
.antMatchers("/druid/**").anonymous()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated()
.and()
.headers().frameOptions().disable();
httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
// 添加JWT filter
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
查询当前用户任务
- taskservice方法
//1.得到ProcessEngine对象
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
//2.得到TaskService对象
TaskService taskService = processEngine.getTaskService();
//3.根据流程定义的key,负责人assignee来实现当前用户的任务列表查询
Task task = taskService.createTaskQuery()
.processDefinitionKey("holiday")
.taskAssignee(SecurityUtils.getUsername() //通过鉴权拿到当前角色)
.singleResult();
//4.任务列表的展示
System.out.println("流程实例ID:"+task.getProcessInstanceId());
System.out.println("任务ID:"+task.getId()); //5002
System.out.println("任务负责人:"+task.getAssignee());
System.out.println("任务名称:"+task.getName());
- taskruntime方法(新版本api)
通过taskRuntime.tasks获取任务列表并分页,在实例化一条工作流后,activiti会将数据存到ACT_RU_TASK和ACT_RU_IDENTITYLINK表中,IDENTITYLINK通过task_id作为外键关联TASK表,IDENTITYLINK根据用户身份鉴别相应的角色,通过GROUP_ID筛选出对应数据,TYPE_字段则显示该角色是参与者还是贡献者,db如下:
若衣源码:
@Override
public Page<ActTaskDTO> selectProcessDefinitionList(PageDomain pageDomain) {
Page<ActTaskDTO> list = new Page<ActTaskDTO>();
org.activiti.api.runtime.shared.query.Page<Task> pageTasks = taskRuntime.tasks(Pageable.of((pageDomain.getPageNum() - 1) * pageDomain.getPageSize(), pageDomain.getPageSize()));
List<Task> tasks = pageTasks.getContent();
int totalItems = pageTasks.getTotalItems();
list.setTotal(totalItems);
if (totalItems != 0) {
Set<String> processInstanceIdIds = tasks.parallelStream().map(t -> t.getProcessInstanceId()).collect(Collectors.toSet());
List<ProcessInstance> processInstanceList = runtimeService.createProcessInstanceQuery().processInstanceIds(processInstanceIdIds).list();
List<ActTaskDTO> actTaskDTOS = tasks.stream()
.map(t -> new ActTaskDTO(t, processInstanceList.parallelStream().filter(pi -> t.getProcessInstanceId().equals(pi.getId())).findAny().get()))
.collect(Collectors.toList());
list.addAll(actTaskDTOS);
}
return list;
}
实际项目遇到的问题及解决方法
Error updating database. Cause: java.sql.SQLSyntaxErrorException: Unknown column ‘VERSION_’ in ‘field list’
Error updating database. Cause: java.sql.SQLSyntaxErrorException: Unknown column ‘PROJECT_RELEASE_VERSION_’ in ‘field list’
原因:
创建表缺少VERSION_字段
添加两个字段。
新版bug问题解决
alter table ACT_RE_DEPLOYMENT add column PROJECT_RELEASE_VERSION_ varchar(255) DEFAULT NULL;
alter table ACT_RE_DEPLOYMENT add column VERSION_ varchar(255) DEFAULT NULL;
前端相关
基于BPMN2.0的工作流
demo实例https://demo.bpmn.io/s/start
节点如下,教程https://www.jianshu.com/p/a8a21870986a
审批流程图:
bpmnjs引入
导入相关代码,包括我的审批,代办任务,历史流程等的表单设计和模块划分,其他的调整等
更多推荐
所有评论(0)