zoukankan      html  css  js  c++  java
  • Liferay7 BPM门户开发之10: 通用流程实现从Servlet到Portlet(Part1)

    开发目的:

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

  • 相关阅读:
    【C语言】C语言static和extern区别
    【C语言】C语言外部变量和内部变量
    【C语言】C语言局部变量和全局变量
    【C语言】C语言常量和变量
    【C语言】C语言数据类型
    【C语言】C语言标识符
    【C语言】C语言关键字
    【C语言】外部函数和内部函数
    【C语言】C语言函数
    Android 测试 Appium、Robotium、monkey等框架或者工具对比
  • 原文地址:https://www.cnblogs.com/starcrm/p/5985290.html
Copyright © 2011-2022 走看看