开发目的:

  • 实现通用流程自动化处理(即实现不需要hardcode代码的bpm统一处理后台,仅需要写少量前端html form代码和拖拽设计BPM定义)
  • 既可独立运行或可依托于Liferay或依托其它门户系统(使用portlet规范技术实现)运行;

先实现一个JSP + Servlet版的通用流程处理,将来迁移到Portlet

迁移工作将保留大量的前后端代码,仅需要改动少量的注解。

考虑到Liferay的客户端体系是bootstrap+jQuery(对移动端的支持非常好),JSP的实现也用了这两者。

第1步,前端原型实现

首先先实现一个客户端的原型,简单实现一些逻辑,

jsp相关的:

  1. 登陆index.jsp:用于模拟获取user session;
  2. 启动流程列表页flowList.jsp: 用于启动流程;
  3. 待办页flowToDo.jsp
  4. 请假流程模拟页formLeave.jsp : 用于模拟请假流程;
  5. 借款流程模拟页formLoan.jsp : 用于模拟借款流程;

java控制器相关的:

  1. Login.java : 用于登陆逻辑;
  2. BpmForm.java : 流程表单统一控制;
  3. BpmDate.java: 数据控制;
  4. BpmInst.java: 流程实例控制;
  5. ......

第2步:登陆逻辑模拟

index.jsp

注意:不支持IE8 。在这个阶段仅仅是先实现模拟前端展示,更细节的代码我们后面再逐步补充

<%@ 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>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>登陆</title>
<!-- Bootstrap core CSS --><link href="css/bootstrap.min.css" rel="stylesheet"><!-- IE10 viewport hack for Surface/desktop Windows 8 bug --><link href="css/ie10-viewport-bug-workaround.css" rel="stylesheet"><!-- Custom styles for this template --><link href="css/signin.css" rel="stylesheet"><!-- Just for debugging purposes. Don't actually copy these 2 lines! --><!--[if lt IE 9]><script src="../../assets/js/ie8-responsive-file-warning.js"></script><![endif]--><script src="js/ie-emulation-modes-warning.js"></script><!-- HTML5 shim and Respond for IE8 support of HTML5 elements and media queries --><!--[if lt IE 9]><script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script><script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script><![endif]--></head><body><div class="container"><form class="form-signin" action="login" method="post"><h2 class="form-signin-heading">用户登陆</h2><label for="inputEmail" class="sr-only">账户</label><input type="text" id="inputUsername" name="inputUsername" class="form-control" placeholder="账户" required autofocus><label for="inputPassword" class="sr-only">密码</label><input type="password" id="inputPassword" name="inputPassword" class="form-control" placeholder="密码" required><button class="btn btn-lg btn-primary btn-block" type="submit">登陆</button></form></div> <!-- /container --><script src="js/ie10-viewport-bug-workaround.js"></script>
</body>
</html>

View Code

不用输入密码,仅仅用于模拟获取用户名

对应的登陆处理逻辑类: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("<H1>用户名不为空</H1>");out.close();        }    }public void doPost(HttpServletRequest request, HttpServletResponse response)throws IOException, ServletException {this.doGet(request, response);}
}

View Code

第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>

View Code

在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">
<!--在移动设备浏览器上,通过为视口(viewport)设置 meta 属性为 user-scalable=no 可以禁用其缩放(zooming)功能。-->
<!--这样禁用缩放功能后,用户只能滚动屏幕,就能让你的网站看上去更像原生应用的感觉。-->
<!--<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">--><!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
<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>
<!-- 以上为所有JSP固定头部 -->
<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-30</td><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-25</td><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-30</td><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  >请假流程:P201610001389</td><td  >2016-10-30 12:20:20</td><td  > <input type="submit" value="查看" class="btn btn-lg btn-primary btn-block"/> </td></tr><tr class="active"><td  >报销流程:P201609000962</td><td  >2016-10-30 12:20:20</td><td  > <input type="submit" value="查看" class="btn btn-lg btn-primary btn-block"/> </td></tr></table></div>      </div>  </div>
</div>  </form>
</body>
</html>

View Code

在移动端的展示:

第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">
<!--在移动设备浏览器上,通过为视口(viewport)设置 meta 属性为 user-scalable=no 可以禁用其缩放(zooming)功能。-->
<!--这样禁用缩放功能后,用户只能滚动屏幕,就能让你的网站看上去更像原生应用的感觉。-->
<!--<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">--><!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
<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>
<!-- 以上为所有JSP固定头部 --><!-- form 开始 -->
<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' /><!-- 1 主任务节点 开始 -->
<!--.container-fluid 类用于 100% 宽度,占据全部视口(viewport)的容器。-->
<!--<div id="actionMain" class="container-fluid">-->
<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>
<!-- 1 主任务节点 结束 --><!-- 2 上级任务节点 开始 -->
<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"> 同意   &nbsp;&nbsp;
</label>
<label class="radio-inline"><input type="radio" name="actionLeader_approve" id="inlineRadio2" value="0"> 不同意   &nbsp;&nbsp;
</label><br><textarea name="actionLeader_info" rows="3" cols="20" class="form-control" placeholder="备注"></textarea>
<br></div>
<!-- 2 上级任务节点 结束 --><!-- 3 HR任务节点 开始 -->
<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"> 同意   &nbsp;&nbsp;
</label>
<label class="radio-inline"><input type="radio" name="actionHR_approve" id="inlineRadio2" value="0"> 不同意   &nbsp;&nbsp;
</label>
<br><textarea name="actionHR_info" rows="3" cols="20" class="form-control" placeholder="备注"></textarea>
<br></div>
<!-- 3 HR任务节点 结束 -->
<div id="button" class="container">
<button type="submit" value="提交" class="btn btn-primary">提交</button>
</div>
<form><!-- form 结束 -->
</body><!--用jquery写的-->
<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><!--用原生Javascript写的
<script>var enableDivID = '<%=request.getParameter("taskID")%>';
document.getElementById('taskID').value = enableDivID ;// inputs lock
inputs = document.getElementsByTagName("input")
for(i=0;i<inputs.length;i++)
{inputs[i].disabled=true;
}
// textareas lock
textareas = document.getElementsByTagName("textarea")
for(i=0;i<textareas.length;i++)
{textareas[i].disabled=true;
}
selects = document.getElementsByTagName("select")
for(i=0;i<selects.length;i++)
{selects[i].disabled=true;
} // inputs open
inputsOpen = document.getElementById(enableDivID).getElementsByTagName("input")
for(i=0;i<inputsOpen.length;i++)
{inputsOpen[i].disabled=false;
}
// textareas open
textareasOpen = document.getElementById(enableDivID).getElementsByTagName("textarea")
for(i=0;i<textareasOpen.length;i++)
{textareasOpen[i].disabled=false;
} selectsOpen = document.getElementById(enableDivID).getElementsByTagName("select")
for(i=0;i<selectsOpen.length;i++)
{selectsOpen[i].disabled=false;
}
//taskID open
document.getElementById('taskID').disabled=false;
document.getElementById('procInstId').disabled=false;
document.getElementById('executeId').disabled=false;document.getElementById('form1').action="";</script>
--></html>

View Code

在PC端的展示:

使用了单列的布局,这是简单的处理,为了写更少的兼容移动端代码。

移动端展示:

我们发现表单的逻辑处理,比如数据验证和日期选择等js代码完美的兼容移动端

如日期选择器:

第5步,开发流程通用处理逻辑Servlet后台

表单数据的提交需要转化为流程变量,这是处理的核心,主要逻辑:

##### 启动流程 #####String formId  = request.getParameter("formId");
String procDefId  = request.getParameter("procDefId"); //流程定义ID
String objId  = new UUID; //业务数据唯一ID
String businessKey = ""; // 业务键,提交时组装//流程变量
Map<String, String[]> flowData = new HashMap<String, String>();//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<String, String[]> flowData = new HashMap<String, String>();//将表单提交数据注入表单变量
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.1String 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<Task> 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: 统计符合条件的结果数量*/List<ProcessDefinition> pds = 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<ProcessDefinition> 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 List<User> findUserByName(String firstNameLike){// 貌似有问题,慎用return identityService.createUserQuery().userFullNameLike(firstNameLike).list();}public static List<User> 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<Group> 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 List<Task> findUserTasks(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定义的方法:
Map<String ,Object > variables = 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 List<Task> getTasksByBusinessKey(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();
// 执行查询
List<Task> tasks = 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)被明确指定。

比如销假人被变量明确指定了:
<usertask id="reportBack" name="销假" activiti:assignee="${initUserId}"></usertask>

// 创建任务查询对象
TaskQuery taskQuery = taskService.createTaskQuery();
// 配置查询对象
// String assignee="user";
String assignee="李四";
taskQuery
// 过滤条件
.taskAssignee(assignee)
// 分页条件
// .listPage(firstResult, maxResults)
// 排序条件
.orderByTaskCreateTime().desc();
// 执行查询
List<Task> tasks = 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

Map<String, String[]> flowData = new HashMap<String, String[]>();//将表单提交数据注入表单变量
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。

<userTask id="task" name="My Task" activiti:assignee="${ldapService.findManagerForEmployee(emp)}"/>

也可以用来设置候选人和候选组:

<userTask id="task" name="My Task" activiti:candidateUsers="${ldapService.findAllSales()}"/>

ps:注意方法返回类型只能为String或Collection<String> (对应候选人和候选组):

public class FakeLdapService {public String findManagerForEmployee(String employee) {
return "Kermit";
}public List<String> findAllSales() {
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 {@Overridepublic 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.javaprotected void createUser(String userId, String firstName, String lastName, String password, String email, String imageResource, List<String> groups, List<String> 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<userInfo.size(); i+=2) {identityService.setUserInfo(userId, userInfo.get(i), userInfo.get(i+1));}}}//解锁操作
BpmnService.java
public Set<String> unlockProcess(String processInstanceId, String messageName, Map<String, ? extends Object> variables){Set<String> exIds = new HashSet<String>();log.debug("Unlocking Process with processInstanceId:'"+processInstanceId+"'");List<Execution> 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.DelegateTaskpublic 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<IdentityLink> candidates = delegateTask.getCandidates();Set<String> candidateUsers = new HashSet<String>();Set<String> candidateGroups = new HashSet<String>();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.javapublic 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> T getSession(Class<T> 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)

Liferay7 BPM门户开发之10: 通用流程实现从Servlet到Portlet(Part1)相关推荐

  1. bpmn 文件 服务器部署,Liferay7 BPM门户开发之45: 集成Activiti文件上传部署流程BPMN模型...

    开发文件上传,部署流程模板. 首先,开发jsp页面,deploy.jsp ${RETURN_MESSAGE} 其中,上传form的action为portlet:actionURL,它的name就是在p ...

  2. Liferay7 BPM门户开发之11: Activiti工作流程开发的一些统一规则和实现原理(完整版)...

    注意:以下规则是我为了规范流程的处理过程,不是Activiti公司的官方规定. 1.流程启动需要设置启动者,在Demo程序中,"启动者变量"名统一设置为initUserId 启动时 ...

  3. Liferay7 BPM门户开发之5: Activiti和Spring集成

    参考文档: https://github.com/jbarrez/spring-boot-with-activiti-example https://github.com/sxyx2008/sprin ...

  4. Liferay7 BPM门户开发之4: Activiti事件处理和监听Event handlers

    事件机制从Activiti 5.15开始引入,这非常棒,他可以让你实现委托. 可以通过配置添加事件监听器,也可以通过Runtime API加入注册事件. 所有的事件参数子类型都来自org.activi ...

  5. Liferay7 BPM门户开发之46: 集成Activiti用户、用户组、成员关系同步

    在实际的BPM集成开发过程中,Liferay和Activiti这两个异构的系统之间,用户.组的同步需求非常重要,用来实现签收组的概念,比如指定签收组.会签.抢签都需要用到. Activiti可以通过自 ...

  6. Liferay7 BPM门户开发之24: Liferay7应用程序安全

    整理中...... Resources, Roles, and Permissions Portal Access Control List (PACL) Custom SSO Providers A ...

  7. liferay7.0 mysql_Liferay7 BPM门户开发之6: Activiti数据库换为mysql

    第一步: 在mysql中创建数据库名字叫 'activiti' 执行D:\activiti-5.21.0\database\create下的脚本 第二步: 打开=> apache-tomcat/ ...

  8. (0040) iOS 开发之10.3新特性:程序内评价

    程序内评价之SKStoreReviewController 在ios 10.3之后,系统提供了一个SKStoreReviewController类,可以帮助在app内部实现评价.App实现评价一般有下 ...

  9. Java EE WEB工程师培训-JDBC+Servlet+JSP整合开发之10.Web_工程结构

    –简介 –Web应用程序的思想 –Web应用程序的目的 –Web工程结构 –web.xml 文件 –实例 • 创建一个简单的web应用程序 • 部署到tomcat中来运行 ############## ...

最新文章

  1. 在线录音机 html5,recorder
  2. 【学习笔记】1、Python的基本介绍
  3. 手动创建DataTable并绑定gridview
  4. 让你省写大量重复代码的方法 使用PropertyInfo类 反射获取类 的类型 .
  5. exec 和 call 用法详解
  6. Go语言之进阶篇响应报文测试方法
  7. Ribbon 均衡策略 与 脱离 Eureka 使用、LoadBalancerClient
  8. 微型计算机上的tab作用,TAB键有什么用处
  9. 涛涛的若依学习笔记——登录
  10. 【基于ARM cortex-A53的音视频】
  11. 计算机模拟光照,建筑太阳光照实时模拟软件
  12. Microsoft Teams 深度使用体验——创建团队
  13. Service Mesh架构下的认证与授权
  14. 计算机组成原理-基本组成
  15. 探究:Adobe Premiere Pro CC 2018 导入SRT字幕显示不全问题
  16. python调用shell命令
  17. 研究音频编解码要看什么书
  18. C语言dialog函数用法,DialogBox用法
  19. java获取root权限_apk如何获取root权限
  20. 用免费OA办公系统打造统一移动办公系统

热门文章

  1. [问题2014S07] 解答
  2. 苹果新漏洞 “Shrootless” 可使攻击者在macOS 系统上安装后门
  3. FBI 连续第三次发布关于国家黑客利用 Kwampirs 发动全球供应链攻击的警告
  4. VMware 修复 Fusion 和 Horizon 中的两个提权漏洞
  5. SpringBoot之RabbitMQ的使用
  6. linux系统文件查找及管理
  7. 找个轻量级的Log库还挺难
  8. 深入继承之抽象类和接口综合分析及完整案列解说(一)
  9. SEO 搜索引擎优化技巧
  10. 在板子上电后自动运行程序