开发目的:
- 实现通用流程自动化处理(即实现不需要hardcode代码的bpm统一处理后台,仅需要写少量前端html form代码和拖拽设计BPM定义)
- 既可独立运行或可依托于Liferay或依托其它门户系统(使用portlet规范技术实现)运行;
先实现一个JSP + Servlet版的通用流程处理,将来迁移到Portlet
迁移工作将保留大量的前后端代码,仅需要改动少量的注解。
考虑到Liferay的客户端体系是bootstrap+jQuery(对移动端的支持非常好),JSP的实现也用了这两者。
第1步,前端原型实现
首先先实现一个客户端的原型,简单实现一些逻辑,
jsp相关的:
- 登陆index.jsp:用于模拟获取user session;
- 启动流程列表页flowList.jsp: 用于启动流程;
- 待办页flowToDo.jsp
- 请假流程模拟页formLeave.jsp : 用于模拟请假流程;
- 借款流程模拟页formLoan.jsp : 用于模拟借款流程;
java控制器相关的:
- Login.java : 用于登陆逻辑;
- BpmForm.java : 流程表单统一控制;
- BpmDate.java: 数据控制;
- BpmInst.java: 流程实例控制;
- ......
第2步:登陆逻辑模拟
index.jsp
注意:不支持IE8 。在这个阶段仅仅是先实现模拟前端展示,更细节的代码我们后面再逐步补充
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> "Content-Type" content="text/html; charset=ISO-8859-1">登陆 "css/bootstrap.min.css" rel="stylesheet"> "css/ie10-viewport-bug-workaround.css" rel="stylesheet"> "css/signin.css" rel="stylesheet">class="container">
不用输入密码,仅仅用于模拟获取用户名
对应的登陆处理逻辑类:Login.java
用于设置用户会话: session.setAttribute("username", username);
package com.lifiti; import java.io.IOException; import java.io.PrintWriter; import javax.servlet.*; import javax.servlet.http.*; public class Login extends HttpServlet{ /** * 用户登陆 * 作者:王昕 */ // public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { response.setContentType("text/html;charset=UTF-8"); String username = request.getParameter("inputUsername"); if (username!=null && username.length()>1){ HttpSession session = request.getSession(true); session.setAttribute("username", username); RequestDispatcher rd = request.getRequestDispatcher("flowList.jsp"); rd.forward(request, response); }else{ PrintWriter out = response.getWriter(); out.println("用户名不为空
"); out.close(); } } public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { this.doGet(request, response); } }
第3步:实现启动页和待办页
flowList.jsp
注意,在这个阶段仅仅是先实现模拟前端展示,具体的代码我们后面再逐步补充
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <link href="css/bootstrap.min.css" rel="stylesheet"> <script src="js/jquery.min.js">script> <script src="js/bootstrap.min.js">script> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="description" content=""> <meta name="author" content=""> <link rel="icon" href="icon/favicon.ico"> <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"> <title>启动-发起流程title> head> <body> <div class="container"> <div class="page-header"> <h2>发起流程 <small>你好,<%=(String)session.getAttribute("username")%> small>h2> div> <div class="page-header"> <h2>人力资源类h2> div> <table class="table"> <tr class="info"> <td width=80%>流程名称td> <td width=20%>启动td> tr> <tr class="active"> <td>请假流程td> <td> <input type="submit" value="启动" class="btn btn-lg btn-primary btn-block"/> td> tr> <tr class="active"> <td>入职培训流程td> <td> <input type="submit" value="启动" class="btn btn-lg btn-primary btn-block"/> td> tr> <tr class="active"> <td>外训申请td> <td> <input type="submit" value="启动" class="btn btn-lg btn-primary btn-block"/> td> tr> table> <div class="page-header"> <h2>财务类h2> div> <table class="table"> <tr class="info"> <td width=80%>流程名称td> <td width=20%>启动td> tr> <tr class="active"> <td>借款流程td> <td> <input type="submit" value="启动" class="btn btn-lg btn-primary btn-block"/> td> tr> table> div> body> html>
在PC的展示:
在移动端的展示:
先通过浏览器自带的模拟器来展示。
待办、待阅、已办:
注意,在这个阶段仅仅是先实现模拟前端展示,具体的代码我们后面再逐步补充
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <link href="css/bootstrap.min.css" rel="stylesheet"> <script src="js/jquery.min.js">script> <script src="js/bootstrap.min.js">script> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="description" content=""> <meta name="author" content=""> <link rel="icon" href="icon/favicon.ico"> <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"> <title>待办事项title> head> <body> <form class="form-horizontal" name="bpmForm" action="bpmForm" method="get" onsubmit="return validate_form(this)"> <div class="tabbable"> <ul class="nav nav-tabs"> <li class="active"><a href="#toDo" data-toggle="tab">待办a>li> <li><a href="#toRead" data-toggle="tab">待阅a>li> <li><a href="#done" data-toggle="tab">已办a>li> ul> <div class="tab-content"> <div class="tab-pane active" id="toDo"> <div class="container"> <div class="page-header"> <h2>待办流程h2> div> <table class="table"> <tr class="success"> <td width=30%>流程名称td> <td width=28%>发起时间td> <td width=22%>发起人td> <td width=20%>处理td> tr> <tr class="active"> <td >请假流程td> <td >2016-10-30td> <td >张三td> <td > <input type="submit" value="审核" class="btn btn-lg btn-primary btn-block"/> td> tr> <tr class="info"> <td >报销申请td> <td >2016-10-25td> <td >李四td> <td > <input type="submit" value="审核" class="btn btn-lg btn-primary btn-block"/> td> tr> table> div> div> <div id="toRead" class="tab-pane"> <div class="container"> <div class="page-header"> <h2>待阅流程h2> div> <table class="table"> <tr class="success"> <td width=30%>流程名称td> <td width=28%>发起时间td> <td width=22%>发起人td> <td width=20%>处理td> tr> <tr class="active"> <td >请假流程td> <td >2016-10-30td> <td >张三td> <td > <input type="submit" value="查看" class="btn btn-lg btn-primary btn-block"/> td> tr> table> div> div> <div id="done" class="tab-pane"> <div class="container"> <div class="page-header"> <h2>已办流程h2> div> <table class="table"> <tr class="success"> <td width=40%>流程名称td> <td width=40%>完成时间td> <td width=20%>查看td> tr> <tr class="active"> <td >请假流程:P201610001389td> <td >2016-10-30 12:20:20td> <td > <input type="submit" value="查看" class="btn btn-lg btn-primary btn-block"/> td> tr> <tr class="active"> <td >报销流程:P201609000962td> <td >2016-10-30 12:20:20td> <td > <input type="submit" value="查看" class="btn btn-lg btn-primary btn-block"/> td> tr> table> div> div> div> div> form> body> html>
在移动端的展示:
第4步,设计流程表单
请假流程前端表单页面 formLeave.jsp
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <link href="css/bootstrap.min.css" rel="stylesheet"> <script src="js/jquery.min.js">script> <script src="js/bootstrap.min.js">script> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="description" content=""> <meta name="author" content=""> <link rel="icon" href="icon/favicon.ico"> <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"> <title>请假流程title> head> <body> <form class="form-horizontal" name="bpmForm" action="bpmForm" method="get" onsubmit="return validate_form(this)"> <input type="hidden" id="taskID" name='taskID' /> <input type="hidden" id="procInstId" name='procInstId' /> <input type="hidden" id="executeId" name='executeId' /> <div id="actionMain" class="container"> <div class="page-header"> <h2>请假信息h2> div> <input name="actionMain_days" type="number" class="form-control" placeholder="请假天数" required autofocus/> <br> <label for="inputEmail">休假开始时间label> <input name="actionMain_beginDate" type="date" class="form-control" required autofocus/> <input name="actionMain_beginTime" type="time" class="form-control" required autofocus/> <br> <label for="inputEmail">休假开始时间label> <input name="actionMain_endDate" type="date" class="form-control" required autofocus/> <input name="actionMain_endTime" type="time" class="form-control" required autofocus/> <br> <select name="actionMain_type" class="form-control" placeholder="休假类型" required> <option value="0">- 选择休假类型-option> <option value="1">年假option> <option value="2">事假option> <option value="3">病假option> <option value="4">探亲假option> select> <br> <textarea name="actionMain_info" rows="3" cols="20" class="form-control" placeholder="备注" required>textarea> <br> div> <div id="actionLeader" class="container" > <div class="page-header"> <h2>上级审批意见h2> div> <label class="radio-inline"> <input type="radio" name="actionLeader_approve" id="inlineRadio1" value="1"> 同意 label> <label class="radio-inline"> <input type="radio" name="actionLeader_approve" id="inlineRadio2" value="0"> 不同意 label> <br> <textarea name="actionLeader_info" rows="3" cols="20" class="form-control" placeholder="备注">textarea> <br> div> <div id="actionHR" class="container"> <div class="page-header"> <h2>HR审批意见h2> div> <label class="radio-inline"> <input type="radio" name="actionHR_approve" id="inlineRadio1" value="1"> 同意 label> <label class="radio-inline"> <input type="radio" name="actionHR_approve" id="inlineRadio2" value="0"> 不同意 label> <br> <textarea name="actionHR_info" rows="3" cols="20" class="form-control" placeholder="备注">textarea> <br> div> <div id="button" class="container"> <button type="submit" value="提交" class="btn btn-primary">提交button> div> <form> body> <script type="text/javascript"> $(document).ready(function() { var enableDivID = '<%=request.getParameter("taskID")%>'; //屏蔽 $("div:not([id='"+enableDivID+"']) input").attr({ disabled: 'true' }); $("div:not([id='"+enableDivID+"']) select").attr({ disabled: 'true' }); $("div:not([id='"+enableDivID+"']) textarea").attr({ disabled: 'true' }); }); script> html>
在PC端的展示:
使用了单列的布局,这是简单的处理,为了写更少的兼容移动端代码。
移动端展示:
我们发现表单的逻辑处理,比如数据验证和日期选择等js代码完美的兼容移动端
如日期选择器:
第5步,开发流程通用处理逻辑Servlet后台
表单数据的提交需要转化为流程变量,这是处理的核心,主要逻辑:
##### 启动流程 ##### String formId = request.getParameter("formId"); String procDefId = request.getParameter("procDefId"); //流程定义ID String objId = new UUID; //业务数据唯一ID String businessKey = ""; // 业务键,提交时组装 //流程变量 MapflowData = new HashMap (); //HTML表单提交数据 --〉 流程变量 flowData = request.getParameterMap(); //启动 //业务键 = 流程ID.实体实例ID; businessKey = procDefId + "." + objId workflowService.startProcess(procDefId,businessKey,formData); //或启动,不存业务键 //ProcessInstance processInstance = formService.submitStartFormData(procDefId, flowData); ##### 提交任务节点 ##### //使用String[]数组是用于处理select类型的多项输入数据 String formId = request.getParameter("formId"); String procInstId = request.getParameter("procInstId"); //流程实例ID Map flowData = new HashMap (); //将表单提交数据注入表单变量 flowData = request.getParameterMap(); //Task task = taskService.createTaskQuery().taskAssignee("user1").singleResult(); Task task = taskHelper.getTask(procInstId)...; //formService.submitTaskFormData(task.getId(), formProperties); formHelper.submitTaskFormData(task.getId(), flowData); ##### 一些帮助方法 ##### //用taskId获取业务对象id public String getBusinessObjId(String taskId) { //1 获取任务对象 Task task = taskService.createTaskQuery().taskId(taskId).singleResult(); //2 通过任务对象获取流程实例 ProcessInstance pi = runtimeService.createProcessInstanceQuery().processInstanceId(task.getProcessInstanceId()).singleResult(); //3 通过流程实例获取“业务键” String businessKey = pi.getBusinessKey(); //4 拆分业务键,拆分成“业务对象名称”和“业务对象ID”的数组 // a=b LeaveBill.1 String objId = null; if(StringUtils.isNotBlank(businessKey)){ objId = businessKey.split("\\.")[1]; } return objId; } //根据业务键获取流程实例 public ProcessInstance getProInstByBusinessKey(String businessKey) { return runtimeService.createProcessInstanceQuery().processInstanceBusinessKey(businessKey).singleResult(); } //根据业务键获取任务 public List getTasksByBusinessKey(String businessKey) { return taskService.createTaskQuery().processInstanceBusinessKey("LeaveBill.1").list(); }
实现流程仓库操作的帮助类:
用于部署\删除\察看流程
package com.lifiti.utils; import java.io.InputStream; import java.util.List; import java.util.zip.ZipInputStream; import org.activiti.engine.RepositoryService; import org.activiti.engine.repository.DeploymentBuilder; import org.activiti.engine.repository.ProcessDefinition; import org.activiti.engine.repository.ProcessDefinitionQuery; /** * 仓库帮助类:用于部署\删除\察看流程 * * @author wx 王昕 * */ public class RepositoryHelper { public static final RepositoryService repositoryService = ActivitiUtils.getProcessEngine().getRepositoryService(); public static void deploy(String xmlFile) { repositoryService.createDeployment().addClasspathResource(xmlFile).deploy(); } /** * * @param bpmn ,比如"diagrams/Leave.bpmn" * @param png, 比如"diagrams/Leave.png" * @throws Exception */ public static void deploy(String flowName,String bpmn,String png) throws Exception { // 创建发布配置对象 DeploymentBuilder builder = repositoryService.createDeployment(); // 设置发布信息 builder.name(flowName)// 添加部署规则的显示别名 .addClasspathResource(bpmn)// 添加规则文件 .addClasspathResource(png);// 添加规则图片 // 不添加会自动产生一个图片,较影响效率 // 完成发布 builder.deploy(); } /** * * @param zipFile ,比如"diagrams/diagrams.bar" * @param flowName,比如"请假流程" * @throws Exception */ public static void deployZIP(String zipFile,String flowName) throws Exception { // 创建发布配置对象 DeploymentBuilder builder = repositoryService.createDeployment(); // 获得上传文件的输入流程 InputStream in = RepositoryHelper.class.getClassLoader().getResourceAsStream(zipFile); ZipInputStream zipInputStream = new ZipInputStream(in); // 设置发布信息 builder.name(flowName)// 添加部署规则的显示别名 .addZipInputStream(zipInputStream); // 完成发布 builder.deploy(); } public static void delDeployment(String deploymentId) throws Exception { // 普通删除,如果当前规则下有正在执行的流程,则抛异常 // repositoryService.deleteDeployment(deploymentId); // 级联删除,会删除和当前规则相关的所有信息,包括历史 repositoryService.deleteDeployment(deploymentId, true); } /** * 查看流程定义 流程定义 ProcessDefinition id : {key}:{version}:{随机值} name : * 对应流程文件process节点的name属性 key : 对应流程文件process节点的id属性 version : * 发布时自动生成的。如果是第一发布的流程,veresion默认从1开始;如果当前流程引擎中已存在相同key的流程,则找到当前key对应的最高版本号,在最高版本号上加1 */ public static void queryProcessDefinition() throws Exception { // 获取流程定义查询对象 ProcessDefinitionQuery processDefinitionQuery = repositoryService.createProcessDefinitionQuery(); // 配置查询对象 processDefinitionQuery // 添加过滤条件 // .processDefinitionName(processDefinitionName) // .processDefinitionId(processDefinitionId) // .processDefinitionKey(processDefinitionKey) // 分页条件 // .listPage(firstResult, maxResults) // 排序条件 .orderByProcessDefinitionId().desc() .orderByProcessDefinitionVersion().desc(); /** * 执行查询 list : 执行后返回一个集合 singelResult * 执行后,首先检测结果长度是否为1,如果为一则返回第一条数据;如果不唯一,抛出异常 count: 统计符合条件的结果数量 */ Listpds = processDefinitionQuery.list(); // 遍历集合,查看内容 for (ProcessDefinition pd : pds) { System.out.print("deploymentId:" + pd.getDeploymentId() + ","); System.out.print("id:" + pd.getId() + ","); System.out.print("name:" + pd.getName() + ","); System.out.print("key:" + pd.getKey() + ","); System.out.println("version:" + pd.getVersion()); } } public static void delAllProcess() throws Exception { // 获取流程定义查询对象 ProcessDefinitionQuery processDefinitionQuery = repositoryService.createProcessDefinitionQuery(); List pds = processDefinitionQuery.list(); // 遍历集合,查看内容 for (ProcessDefinition pd : pds) { repositoryService.deleteDeployment(pd.getDeploymentId(),true); } } }
认证帮助类:用户\组\角色管理, 代码还写得很粗糙,需要完善
还有一个重要的接口和类需要实现:即寻找用户的直属领导
这个应根据每个公司的在用HRM系统或者OA系统进行定制,有的已经有API接口可以调用;
package com.lifiti.utils; import java.util.List; import javax.servlet.http.HttpSession; import org.activiti.engine.IdentityService; import org.activiti.engine.identity.Group; import org.activiti.engine.identity.User; /** * 认证帮助类:用户\组\角色管理 * @author wx 王昕 * */ public class IdentifyHelper { public static final IdentityService identityService = ActivitiUtils.getProcessEngine().getIdentityService(); private static final String USER = "ACTUSER"; public static void saveUser(User user){ identityService.saveUser(user); } public static void saveUser(String userId,String name,String email){ User user = identityService.newUser(userId); user.setFirstName(name); user.setLastName(""); user.setEmail(email); user.setPassword(userId); identityService.saveUser(user); } public static User getUser(String userId){ return identityService.createUserQuery().userId(userId).singleResult(); } public static String getUserInfo(String userId,String key){ return identityService.getUserInfo(userId, key); } public static void delUser(String userId){ identityService.deleteUser(userId); } public static User getUserByMail(String email){ return identityService.createUserQuery().userEmail(email).singleResult(); } public static ListfindUserByName(String firstNameLike){ // 貌似有问题,慎用 return identityService.createUserQuery().userFullNameLike(firstNameLike).list(); } public static List getAllUser(){ return identityService.createUserQuery().list(); } public static void saveGroup(Group group){ // 保存组 identityService.saveGroup(group); } /** * 新建用户组 * @param groupId * @param name * @param type 0:security-role;1:assignment */ public static void saveGroup(String groupId,String name,int type){ Group group = identityService.newGroup(groupId); group.setName(name); if (type==0){ group.setType("security-role"); } else{ group.setType("assignment"); } identityService.saveGroup(group); } public static void createMembership(String userId,String groupId){ try { identityService.createMembership(userId, groupId); } catch (Exception ex ){ } } public static void deleteMembership(String userId,String groupId){ try { identityService.deleteMembership(userId, groupId); } catch (Exception ex ){ } } /** * 用户所在的所有组 * @param userId * @return */ public static List findGroupsByuserId(String userId){ return identityService.createGroupQuery().groupMember(userId).list(); } public static void saveUserToSession(HttpSession session, User user) { session.setAttribute(USER, user); } public static User getUserFromSession(HttpSession session) { Object attribute = session.getAttribute(USER); return attribute == null ? null : (User) attribute; } }
运行时帮助类:合并了运行时服务和任务服务的一些操作,比如启动流程\任务签收\完成任务\传递流程变量...
代码还写得很粗糙,需要完善.
package com.lifiti.utils; import java.util.List; import org.activiti.engine.RuntimeService; import org.activiti.engine.TaskService; import org.activiti.engine.task.Task; /** * 运行时帮助类:合并了运行时服务和任务服务的一些操作,比如启动流程\任务签收\完成任务\ * @author wx 王昕 * */ public class RuntimeHelper { public static final RuntimeService runtimeService = ActivitiUtils.getProcessEngine().getRuntimeService(); public static final TaskService taskService = ActivitiUtils.getProcessEngine().getTaskService(); public static void startProcessByKey(String processDefinitionKey){ runtimeService.startProcessInstanceByKey(processDefinitionKey); } public static void startProcessByKey(String processDefinitionKey,String businessKey){ runtimeService.startProcessInstanceByKey(processDefinitionKey, businessKey); } public static ListfindUserTasks(String userId){ return taskService.createTaskQuery().taskCandidateUser(userId).list(); } public static void setAssignee(String taskId,String userId){ taskService.setAssignee(taskId, userId); } public static void claimAndComplete(String taskId,String userId){ taskService.claim(taskId, userId); completeTask(taskId); } public static void claimTask(String taskId,String userId){ taskService.claim(taskId, userId); } public static void completeTask(String taskId){ taskService.complete(taskId); } public static void addCandidateGroup(String taskId,String groupId){ taskService.addCandidateGroup(taskId, groupId); } public static void addCandidateUser(String taskId,String userId){ taskService.addCandidateUser(taskId, userId); } }
第6步,流程开发的一些统一规则和实现原理
注意:以下规则是为了规范流程的处理过程,不是Activiti公司的官方规定。
1、流程启动需要设置启动者,在Demo程序中,“启动者变量”名统一设置为initUserId
启动时要做的: identityService.setAuthenticatedUserId(initUserId); processInstance = runtimeService.startProcessInstanceByKey(流程ID, 业务Key, 变量map); or startProcessInstanceById(String processDefinitionId, String businessKey, Map variables) 变量map定义的方法: Mapvariables = new HashMap<>(); variables.put("initUserId","wangxin"); variables.put("leaveReason","想休假了");
2、使用el表达式来做流程的动态属性或方法定义
比如完成一个“请假销假”的任务,需要流程发起者销假,销假环节就能找到正确的签收者(activiti:assignee)了:
<startevent id="startevent1" name="Start" activiti:initiator="initUserId">startevent> <usertask id="reportBack" name="销假" activiti:assignee="${initUserId}">usertask>
3、“业务键”定义规则
业务键 = 流程ID + 实体实例ID;
businessKey = procDefId + "." + objId
4、根据“业务键”查询流程实例(反查)
在流程启动的时候,我们已经定义了业务Key,那么只需要反查,即可得到流程实例
//根据业务键获取流程实例 public ProcessInstance getProInstByBusinessKey(String businessKey) { return runtimeService.createProcessInstanceQuery().processInstanceBusinessKey("LeaveBill.1").singleResult(); } //根据业务键获取任务 public ListgetTasksByBusinessKey(String businessKey) { return taskService.createTaskQuery().processInstanceBusinessKey("LeaveBill.1").list(); }
5、通过流程实例ID获取“业务键”
//1、通过任务对象获取流程实例 ProcessInstance pi = runtimeService.createProcessInstanceQuery().processInstanceId(task.getProcessInstanceId()).singleResult(); //2、通过流程实例获取“业务键” String businessKey = pi.getBusinessKey();
6、取得当前活动节点
String processInstanceId="1401"; // 通过流程实例ID查询流程实例 ProcessInstance pi = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId).singleResult(); if(pi!=null){ System.out.println("当前流程节点在:" + pi.getActivityId()); }else{ System.out.println("流程已结束!!"); }
7、查询某人的“候选公共任务”,用于实现“抢签”
“候选公共任务”的认领者即属于一堆候选人其中一个,比如财务审批可以由张三、李四、王五审批,谁批都可以,手快者先认领就是签收者。
这个查询就是把符合条件的候选者的任务查出来,一般可以和“个人任务”合并一起放在“待办任务”菜单里。
也针对于把Task分配给一个角色时,例如部门领导,因为部门领导角色可以指定多个人所以需要先签收再办理,特点:抢占式。
// 创建任务查询对象 TaskQuery taskQuery = taskService.createTaskQuery(); // 配置查询对象 String candidateUser="张三"; taskQuery // 过滤条件 .taskCandidateUser(candidateUser) // 排序条件 .orderByTaskCreateTime().desc(); // 执行查询 Listtasks = taskQuery.list(); System.out.println("======================【"+candidateUser+"】的候选公共任务列表================="); for (Task task : tasks) { System.out.print("id:"+task.getId()+","); System.out.print("name:"+task.getName()+","); System.out.print("createTime:"+task.getCreateTime()+","); System.out.println("assignee:"+task.getAssignee()); }
8、查询某人的“个人任务”,即签收者(assignee)被明确指定。
比如销假人被变量明确指定了:
// 创建任务查询对象 TaskQuery taskQuery = taskService.createTaskQuery(); // 配置查询对象 // String assignee="user"; String assignee="李四"; taskQuery // 过滤条件 .taskAssignee(assignee) // 分页条件 // .listPage(firstResult, maxResults) // 排序条件 .orderByTaskCreateTime().desc(); // 执行查询 Listtasks = taskQuery.list(); System.out.println("======================【"+assignee+"】的代办任务列表================="); for (Task task : tasks) { System.out.print("id:"+task.getId()+","); System.out.print("name:"+task.getName()+","); System.out.print("createTime:"+task.getCreateTime()+","); System.out.println("assignee:"+task.getAssignee()); }
9、任务认领,通过认领,把“候选公共任务”变成指定用户的“个人任务”
// claim 认领 String taskId="1404"; String userId="李四"; // 让指定userId的用户认领指定taskId的任务 taskService.claim(taskId, userId);
10、结合Form表单提交(办理)任务
String formId = request.getParameter("formId"); String procInstId = request.getParameter("procInstId"); //流程实例ID MapflowData = new HashMap (); //将表单提交数据注入表单变量 flowData = request.getParameterMap(); formHelper.submitTaskFormData(request.getParameter("taskId"), flowData); // 完成任务 taskService.complete(taskId );
11、任务动态分配定制处理,比如寻找“某人的直属领导”
Activiti的签收人中只有候选人、候选组、分配人的概念,如果要实现更业务相关的签收逻辑,需要扩展监听器
比如MyLeaderHandler,即扩展实现了TaskListener接口:
<userTask id="task1" name="My task" > <extensionElements> <activiti:taskListener event="create" class="org.activiti.MyLeaderHandler" /> extensionElements> userTask>
//动态实现任务分配 public class MyLeaderHandler implements TaskListener { public void notify(DelegateTask delegateTask) { LeaderService ls =.... String userLeader = ls.findLeaderbyUserId(XXXXXXX); delegateTask.setAssignee(userLeader); delegateTask.addCandidateUser(XXX); delegateTask.addCandidateGroup(XXXX); ... } }
还有一种更方便的方法,即通过el表达式:
可以使用表达式把任务监听器设置为spring代理的bean, 让这个监听器监听任务的创建事件。
下面的例子中,执行者会通过调用ldapService这个spring bean的findManagerOfEmployee方法获得。
流程变量emp会作为参数传递给bean。
也可以用来设置候选人和候选组:
ps:注意方法返回类型只能为String或Collection
public class FakeLdapService { public String findManagerForEmployee(String employee) { return "Kermit"; } public ListfindAllSales() { return Arrays.asList("kermit", "gonzo", "fozzie"); } }
12、会签任务,即多实例
例如,一个任务必须所有领导都通过了才往下走。
activiti其实已经非常优雅的实现了,网上有一些繁琐的实现,其实完全没有必要,比如下面:
http://jee-soft.cn/htsite/html/fzyyj/jsyj/2012/08/08/1344421504026.html
正确的打开方式是通过在Task节点增加multiInstanceCharacteristics节点,设置 collection和 elementVariable属性
例子:
可以指定一个(判断完成)表达式,只有true的情况下全部实例完成,流程继续往下走。
如果表达式返回true,所有其他的实例都会销毁,多实例节点也会结束。 这个表达式必须定义在completionCondition子元素中。
<userTask id="miTasks" name="My Task" activiti:assignee="${assignee}"> <multiInstanceLoopCharacteristics isSequential="false" activiti:collection="assigneeList" activiti:elementVariable="assignee" > <completionCondition>${nrOfCompletedInstances/nrOfInstances >= 0.6 }completionCondition> multiInstanceLoopCharacteristics> userTask>
这里例子中,会为assigneeList集合的每个元素创建一个并行的实例。 当60%的任务完成时,其他任务就会删除,流程继续执行。
会签环节中涉及的几个默认的自带流程变量:
- 1. nrOfInstances 该会签环节中总共有多少个实例
- 2. nrOfActiveInstances 当前活动的实例的数量,即还没有 完成的实例数量。
- 3. nrOfCompletedInstances 已经完成的实例的数量
实现会签人员分配
public class AssgineeMultiInstancePer implements JavaDelegate { @Override public void execute(DelegateExecution execution) throws Exception { System.out.println("设置会签环节的人员."); execution.setVariable("pers", Arrays.asList("张三", "李四", "王五", "赵六")); } }
设置完成会签条件:
public class MulitiInstanceCompleteTask implements Serializable { private static final long serialVersionUID = 1L; public boolean completeTask(DelegateExecution execution) { System.out.println("总的会签任务数量:" + execution.getVariable("nrOfInstances") + "当前获取的会签任务数量:" + execution.getVariable("nrOfActiveInstances") + " - " + "已经完成的会签任务数量:" + execution.getVariable("nrOfCompletedInstances")); System.out.println("I am invoked."); return false; } }
更多可以见这里:
Liferay7 BPM门户开发之11: 工作流程开发的一些统一规则和实现原理(完整版)
一些有用的帮助类代码
//完整帐号信息创建 IdentityHelper.java protected void createUser(String userId, String firstName, String lastName, String password, String email, String imageResource, Listgroups, List userInfo) { if (identityService.createUserQuery().userId(userId).count() == 0) { User user = identityService.newUser(userId); user.setFirstName(firstName); user.setLastName(lastName); user.setPassword(password); user.setEmail(email); identityService.saveUser(user); if (groups != null) { for (String group : groups) { identityService.createMembership(userId, group); } } } if (imageResource != null) { byte[] pictureBytes = IoUtil.readInputStream(this.getClass().getClassLoader().getResourceAsStream(imageResource), null); Picture picture = new Picture(pictureBytes, "image/jpeg"); identityService.setUserPicture(userId, picture); } if (userInfo != null) { for(int i=0; i 2) { identityService.setUserInfo(userId, userInfo.get(i), userInfo.get(i+1)); } } } //解锁操作 BpmnService.java public Set unlockProcess(String processInstanceId, String messageName, Map variables){ Set exIds = new HashSet (); log.debug("Unlocking Process with processInstanceId:'"+processInstanceId+"'"); List executions = runtimeService.createExecutionQuery() .messageEventSubscriptionName(messageName).processInstanceId(processInstanceId) .list(); for (Execution execution2 : executions) { String curExId = execution2.getId(); exIds.add(curExId); runtimeService.setVariables(curExId, variables); runtimeService.messageEventReceived(messageName, curExId); } return exIds; } //监听计数器 TaskCompletionListener.java org.activiti.engine.delegate.DelegateTask public void notify(DelegateTask delegateTask) { Integer counter = (Integer) delegateTask.getVariable("taskListenerCounter"); if (counter == null) { counter = 0; } delegateTask.setVariable("taskListenerCounter", ++counter); } //任务中间变量设置 DelegateTaskTaskListener.java public void notify(DelegateTask delegateTask) { Set candidates = delegateTask.getCandidates(); Set candidateUsers = new HashSet (); Set candidateGroups = new HashSet (); for (IdentityLink candidate : candidates) { if (candidate.getUserId() != null) { candidateUsers.add(candidate.getUserId()); } else if (candidate.getGroupId() != null) { candidateGroups.add(candidate.getGroupId()); } } delegateTask.setVariable(VARNAME_CANDIDATE_USERS, candidateUsers); delegateTask.setVariable(VARNAME_CANDIDATE_GROUPS, candidateGroups); } //自由指派流程测试 TaskServiceTest.java public void testTaskOwner() { Task task = taskService.newTask(); task.setOwner("johndoe"); taskService.saveTask(task); task = taskService.createTaskQuery().taskId(task.getId()).singleResult(); assertEquals("johndoe", task.getOwner()); task.setOwner("joesmoe"); taskService.saveTask(task); task = taskService.createTaskQuery().taskId(task.getId()).singleResult(); assertEquals("joesmoe", task.getOwner()); taskService.deleteTask(task.getId(), true); } //用于实体类型转换 private User getUserInfo(Employee employee) { User user = new UserEntity(employee.getUserCd()); user.setFirstName(employee.getGivenName()); user.setLastName(employee.getFamilyName()); user.setEmail(employee.getEmail()); user.setPassword(employee.getPasswd()); return user; } CommandContext.java @SuppressWarnings({"unchecked"}) public T getSession(Class sessionClass) { Session session = sessions.get(sessionClass); if (session == null) { SessionFactory sessionFactory = sessionFactories.get(sessionClass); if (sessionFactory==null) { throw new ActivitiException("no session factory configured for "+sessionClass.getName()); } session = sessionFactory.openSession(); sessions.put(sessionClass, session); } return (T) session; }
========= 本篇结束 =========
接下来,需要把独立版的流程平台迁移到Liferay委托版的Portlet中去。
第7步,修改润色
&
第8步,最终版,可独立运行的JSP+Servelt+Spring版本流程开发平台
见 第二部分:
Liferay7 BPM门户开发之13: 通用流程实现从Servlet到Portlet (Part2)
第9步,把Servlet工程迁移到Portlet
&
第10步,把Portlet部署到liferay
见 第三部分:
Liferay7 BPM门户开发之14: 通用流程实现从Servlet到Portlet (Part3)