开发目的:

  • 实现通用流程自动化处理(即实现不需要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 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 ]><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 ]>
<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>

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

对应的登陆处理逻辑类: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()>){ 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);
}
}

第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">
<!--在移动设备浏览器上,通过为视口(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>

在移动端的展示:

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

在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.1
String objId = null;
if(StringUtils.isNotBlank(businessKey)){
objId = businessKey.split("\\.")[];
}
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="";
// 通过流程实例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="";
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 {
@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, List<String> groups, List<String> userInfo) { if (identityService.createUserQuery().userId(userId).count() == ) { 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=; i<userInfo.size(); i+=) {
identityService.setUserInfo(userId, userInfo.get(i), userInfo.get(i+));
}
} } //解锁操作
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.DelegateTask public void notify(DelegateTask delegateTask) {
Integer counter = (Integer) delegateTask.getVariable("taskListenerCounter");
if (counter == null) {
counter = ;
}
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.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> 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. Liferay7 BPM门户开发之30: 通用帮助类Validator、ArrayUtil、StringUtil等使用

    废话不多说,直接上代码. 验证类Validator 主要是空验证.数字.格式验证 调用的例子: protected void validateEmailFrom(ActionRequest actio ...

  2. Liferay7 BPM门户开发之37: Liferay7下的OSGi Hook集成开发

    hook开发是Liferay客制扩展的一种方式,比插件灵活,即可以扩展liferay门户,也能对原有特性进行更改,Liferay有许多内置的服务,比如用hook甚至可以覆盖Liferay服务. 可作为 ...

  3. Liferay7 BPM门户开发之17: Portlet 生命周期

    Portlet 生命周期 init() =〉 render() =〉 processAction() =〉 processEvent() =〉 serveResource() =〉destroy() ...

  4. Liferay7 BPM门户开发之9: 流程表单数据动态映射体系

    设计目的: 每个流程表单涉及不同的表单变量.比如请假流程有3个任务节点,分别是 Task1:开始流程,填写请假人.请假原因.请假开始时间.请假结束时间: Task2:上级审批,填写是否同意,审批意见: ...

  5. Liferay7 BPM门户开发之15: Liferay开发体系简介

    Liferay SDK 开发体系 主要分6种: Portlet Hook Theme Layout Templates Web Modules Ext Portlet :类似于servlet的web组 ...

  6. Liferay7 BPM门户开发之12:acitiviti和liferay用户权限体系集成

    写到第12章才出现Liferay的内容,希望可以厚积薄发. 我们的目标是不使用不维护Activiti的用户组织架构,只维护Liferay的体系,这样的好处是非常明显的,即不用做组织架构的同步工作. 原 ...

  7. Liferay7 BPM门户开发之44: 集成Activiti展示流程列表

    处理依赖关系 集成Activiti之前,必须搞清楚其中的依赖关系,才能在Gradle里进行配置. 依赖关系: 例如,其中activiti-engine依赖于activiti-bpmn-converte ...

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

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

  9. Liferay7 BPM门户开发之47: 集成Activiti待办已办任务清单和流程启动

    首先增加两个Portlet,分别用于待办处理.流程启动.待办是别人发起的流程,流到自己这里的流程:流程启动用于发起新的流程. 程序文件放置于 在ACtivit中待办概念分两种,1是指派给你的,专门的指 ...

随机推荐

  1. php 获取数组深度的值

    匿名函数(闭包) $val = array(); array_walk_recursive($array, function ($x) use (&$val) { $val[] = $x; } ...

  2. 算法练习LeetCode初级算法之字符串

    反转字符串 我的解法比较low,利用集合的工具类Collections.reverse反转,用时过长 class Solution { public void reverseString(char[] ...

  3. RN与webview通讯

     一.RN给webview发送信息 this.webview.postMessage(message) 二.监听从React Native发过来的消息: window.document.addEven ...

  4. JS生成PDF文件

    代码: var pdf = new jsPDF('p','pt','a4'); pdf.internal.scaleFactor = 1; //可以调整缩放比例 var options = { //p ...

  5. CentOS 系统 git clone出错

    CentOS 操作系统 安装npm git clone 项目时出现类似如下错误: fatal: unable to access 'https://github.com/creationix/nvmg ...

  6. linux学习第十二天 (Linux就该这么学)找到一本不错的Linux电子书,附《Linux就该这么学》章节目录

    本书是由全国多名红帽架构师(RHCA)基于最新Linux系统共同编写的高质量Linux技术自学教程,极其适合用于Linux技术入门教程或讲课辅助教材,目前是国内最值得去读的Linux教材,也是最有价值 ...

  7. exchange 2010 数据库管理

    1. 查看数据库中空白空间 Get-MailboxDatabase databasename -Status | FL AvailableNewMailboxSpace 2.卸载数据库 Dismoun ...

  8. docker安装radis

    sudo docker search redis sudo docker pull redis sudo docker run --name redis6379 -p 6379:6379 -v /op ...

  9. Scrum冲刺阶段2

    成员今日完成的任务 人员 任务 何承华 后端设计 陈宇 后端设计 丁培辉 后端设计 温志铭 主页面的设计 杨宇潇 主页面的设计 张主强 服务器构建 成员遇到的问题 人员 问题 何承华 暂无 陈宇 暂无 ...

  10. Unity的几个特殊文件夹

    1.以.开头的文件夹会被unity忽略,资源不会被导入,脚本不会编译. 2.Standard Assets和Pro Standard Assets:在这个文件夹中的脚本最先被编译. 3.Editor: ...