之前实现的动态表单的启动功能,现在把审核功能也做个总结。
审核流程界面 最终效果图:
主要需要实现的是一下功能点:
1. 列表页面
1.1.待办任务页面。列表中显示当前用户可以处理的流程。
1.2.运行中的流程。列表中显示当前用户 待办 或者 参与过并且未结束 的流程。
1.3.已结束的流程。列表中显示当前用户 参与过并且已结束 的流程。
2.审核页面
2.1 审核列表。 显示流程审核节点的流程中相关审核情况。根据是是待办人决定是否显示 审核操作里的 【通过】 【退回】等按钮。非待办人不能操作流程。
2.2 具体业务的动态表单。根据节点配置的表单属性配置显示动态表单。类是启动流程功能里的start节点配置。同一流程实例不同审核人可以根据配置的不同显示不同的业务表单。如上面图片中的任务分配节点可以配置一个任务分配的下拉框。这个下拉框控件只在任务分配节点显示,其他节点则不会显示。
待办任务页面点击任务连接进入审核页面具体实现:
ProcInstController.java
/**
* 审核流程页面
*/
@RequestMapping(value = "index")
public String index(Model model,String pid,RedirectAttributes attr) {
String pageUrl = "";
ProcInst pi = procInstService.getEntityById(pid);
//动态表单,外置表单,普通表单(普通表单使用c_前缀,外置表单使用ex_前缀,其他的是动态表单)。参考procDefList.jsp页面
ProcDef pd = procDefService.getEntityById(pi.getProcDefId());
pi.setProcDef(pd);
if(pd.getKey().indexOf("c_") == 0){//普通表单
}else if(pd.getKey().indexOf("ex_") == 0){//外置表单
//外置表单, 与动态表单不同的是根据表单key获取事先定义好的表单,不需要系统自动生成。
}else {
//动态表单
List<FormAttr> formAttrlist = procInstService.getFormAttrList(pi);
model.addAttribute("pi", pi);
model.addAttribute("formAttrlist", formAttrlist);
//审核流程列表
List<TaskInst> tasks = taskInstService.getTaskList(pi.getId());
model.addAttribute("tasks",tasks);
model.addAttribute("tasksSize",tasks.size());
//流程变量
List<HistoricVariableInstance> varList = historyService.createHistoricVariableInstanceQuery().processInstanceId(pi.getId()).list();
Map<String,Object> varMap = new HashMap<String,Object>();
for (HistoricVariableInstance variableInstance : varList) {
varMap.put(variableInstance.getVariableName(), variableInstance.getValue());
}
model.addAttribute("varMap",varMap);
pageUrl = "/system/workflow/hi/reviewed";
}
return pageUrl;
}
reviewed.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
<!DOCTYPE html >
<html>
<head>
<title>启动流程</title>
<%@include file="/common/base.jsp"%>
<script type="text/javascript">
$(function(){
$('input[type="checkbox"]').each(function(index,element){
$(element).click(function(){
element.value = element.checked;
});
})
$('.easyui-datebox').each(function(index,element){
if($(element).attr("defaultVal")){
$(element).datebox('setValue', $(element).attr("defaultVal"));
}
})
$('#dg').datagrid();
$('#dg').remove();
})
function startProc(){
$('#fm').mySubmit({
url : 'backstage/workflow/hi/startProc',
success: function(res){
closeWin();
}
});
}
function completeTask(){
$('#fm').mySubmit({
url : 'workflow/hi/ProcInstController/completeTask',
success: function(res){
closeWin();
}
});
}
function closeWin(){
//关闭easyui弹出窗口
parent.window.$(".panel-tool-close").click();
//关闭layer弹出窗口
var index = parent.layer.getFrameIndex(window.name); //获取窗口索引
parent.layer.close(index);
}
</script>
<style type="text/css">
.fitem {
margin: 5px;
}
.fitem label {
display: inline-block;
width: 120px;
text-align: right;
}
</style>
</head>
<body>
<div class="ftitle"
style="text-align: center; font-size: 26px; padding: 5px;">${pd.name }</div>
<form id="fm" method="post">
<div class="container" class="">
<table id="dg" title="审核列表" toolbar="#tt"
style="width: 100%; max-height: 300px; min-height: 120px; margin: 0 auto;">
<thead>
<tr>
<th width="110" align="center" data-options="field:'name'">任务名称</th>
<th width="90" align="center" data-options="field:'assigneeName'">处理人</th>
<th width="240" align="center" data-options="field:'comment'">批注</th>
<th width="140" align="center" data-options="field:'endTime'">操作时间</th>
</tr>
</thead>
<tbody>
<c:forEach var="task" items="${tasks}">
<c:choose>
<c:when test="${task.isCurAccount == 0}">
<tr>
<td class="center">${task.name }</td>
<td class="center">${task.assigneeName }</td>
<td class="center">${task.comment }</td>
<td class="center"><fmt:formatDate value="${task.endTime}"
pattern="yyyy-MM-dd HH:mm" /></td>
</tr>
<c:forEach var="user" items="${task.candidateUsers}">
<tr>
<td class="center"></td>
<td class="center">${user.name }</td>
<td class="center"></td>
<td class="center"></td>
</tr>
</c:forEach>
</c:when>
<c:otherwise>
<tr>
<td class="center">${task.name }</td>
<td class="center">${task.assigneeName }</td>
<td class="center"><input type="hidden" name="taskId"
value="${task.id}"> <input type="text"
required='true' name="comment"
style="width: 100%; height: 26px;" placeholder="请输入审核意见">
</td>
<td class="center"></td>
</tr>
<c:forEach var="user" items="${task.candidateUsers}">
<tr>
<td class="center"></td>
<td class="center">${user.name }</td>
<td class="center"></td>
<td class="center"></td>
</tr>
</c:forEach>
</c:otherwise>
</c:choose>
</c:forEach>
</tbody>
</table>
<div id="tt">
<div
style='${tasks.get(tasksSize-1).isCurAccount != 1? "display:none;" : ""} '>
<a href="javascript:void(0)" title="通过" class="easyui-linkbutton"
data-options="plain:true,iconCls:'icon-ok'"
οnclick="completeTask()">通过</a> <a href="javascript:void(0)"
title="退回" class="easyui-linkbutton"
data-options="plain:true,iconCls:'icon-back'">退回</a> <a
href="javascript:void(0)" class="easyui-linkbutton"
data-options="plain:true,iconCls:'icon-help'"></a>
</div>
</div>
</div>
<br />
<div id="p" class="easyui-panel" title="流程详细信息">
<c:forEach var="attr" items="${formAttrlist}" varStatus="status">
<div class="fitem" style="${attr.toReadableStr()}">
<label>${attr.name}:</label>
<c:choose>
<c:when test='${"string".equals(attr.type)}'>
<input ${attr.toWritableStr()} class="easyui-validatebox"
${attr.toRequiredStr()} value="${varMap.get(attr.id) }">
</c:when>
<c:when test='${"long".equals(attr.type)}'>
<input ${attr.toWritableStr()} class="easyui-numberspinner"
data-options="increment:1" ${attr.toRequiredStr() }
value="${varMap.get(attr.id) }" />
</c:when>
<c:when test='${"boolean".equals(attr.type)}'>
<input type="checkbox" ${attr.toRequiredStr()}
value="${varMap.get(attr.id)}"
${varMap.get(attr.id)? "checked='checked'" : ""}
class="easyui-checkbox" ${attr.toWritableStr()} />
</c:when>
<c:when test='${"date".equals(attr.type)}'>
<input ${attr.toWritableStr()} class="easyui-datebox"
defaultVal="${varMap.get(attr.id) }">
<%-- <input id="${attr.id}" ${attr.toWritableStr()} class="easyui-datebox" --%>
<%-- data-options="sharedCalendar:'#div${attr.id}'" ${attr.toRequiredStr()}> --%>
<%-- <div id="div${attr.id}" class="easyui-calendar"></div> --%>
</c:when>
<c:when test='${"enum".equals(attr.type)}'>
<!-- <input class="easyui-combobox" name="fundKind.id" id="kindId" -->
<!-- data-options="valueField:'id',textField:'name',url:'FundKind/list/false'"> -->
<select class="easyui-combobox" ${attr.toWritableStr()}
style="width: 173px;">
<c:forEach var="node" items="${attr.selects}">
<option value="${node.attrMap.id}"
${node.attrMap.id.equals(varMap.get(attr.id))? "selected" : "" }>${node.attrMap.name}</option>
</c:forEach>
</select>
</c:when>
<c:otherwise></c:otherwise>
</c:choose>
</div>
</c:forEach>
</div>
</form>
</body>
</html>
1. 当前节点 业务表单
//动态表单
List<FormAttr> formAttrlist = procInstService.getFormAttrList(pi);
这个是打开审核页面时获取流程节点当前表单配置属性列表。类似于启动流程中获取start节点一样,只是多了个判断,即先要判断流程当前运行到哪个节点了,然后再获取该节点的表单属性配置。具体实现:
public List<FormAttr> getFormAttrList(ProcInst pi) {
List<FormAttr> formMap = new ArrayList<FormAttr>();
ByteArray ba = pi.getProcDef().getByteArray();
String xml = null;
try {
xml = new String(ba.getBytes(), "utf-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
Node root = XMLTools.Dom2Map(xml);
Node processNode = root.getSubNodeByName("process");
//获取当前流程待执行的结点
String actId = getLastActId(pi.getId());
List<Node> list = processNode.getChildrenList();
Node lastNode = null;
for (Node node : list) {
if(actId.equals(node.getAttrMap().get("id"))){
lastNode = node;
}
}
Node extensionElements = lastNode.getSubNodeByName("extensionElements");
if(extensionElements != null){
List<Node> formPropertyList = extensionElements.getChildrenList();
for (Node formProperty : formPropertyList) {
FormAttr fa = new FormAttr(formProperty);
formMap.add(fa);
}
}
return formMap;
}
2.获取当前实例的流程变量,然后对 业务表单 进行初始化赋值。
//流程变量
List<HistoricVariableInstance> varList = historyService.createHistoricVariableInstanceQuery().processInstanceId(pi.getId()).list();
Map<String,Object> varMap = new HashMap<String,Object>();
for (HistoricVariableInstance variableInstance : varList) {
varMap.put(variableInstance.getVariableName(), variableInstance.getValue());
}
model.addAttribute("varMap",varMap);
3.审核列表。比较复杂的和关键的一个逻辑功能模块,暂时先只考虑常用的情景。
List<TaskInst> tasks = taskInstService.getTaskList(pi.getId());
public List<TaskInst> getTaskList(String pid) {
List<TaskInst> tasks = taskInstDao.getTaskList(pid);
int size = tasks.size();
TaskInst lastTaskInst = tasks.get(size-1);
Account curAccount = AccountShiroUtil.getCurrentUser();
if(lastTaskInst.getEndTime() == null){
List<Account> candidateUsers = null;
if(lastTaskInst.getAssignee() != null && !lastTaskInst.getAssignee().equals("")){
//任务已签收,不用管配置的候选用户了,可以看做只有签收人一个候选用户
Account account = new Account();
account.setAccountId(lastTaskInst.getAssignee());
candidateUsers = accountDao.find(account);
//这个可能size=0,表示配置的Assignee值可能不在用户表里
if(candidateUsers.size() == 0){
lastTaskInst.setAssignee(null);
}
}
if(lastTaskInst.getAssignee() == null || lastTaskInst.getAssignee().equals("")){
//通过流程配置的候选用户和候选组获取所有候选用户
candidateUsers = taskInstDao.getCandidateUsers(lastTaskInst.getId());
}
lastTaskInst.setCandidateUsers(candidateUsers);
//判断当前用户是否在候选用户里,在则把当前用户移除,并且将最后一个任务指派人改为当前用户(没有考虑已签收,但还没有处理的情况)
for (int i = 0; i < candidateUsers.size(); i++) {
if(curAccount.getAccountId().equals(candidateUsers.get(i).getAccountId())){
Account account = candidateUsers.remove(i);
lastTaskInst.setIsCurAccount(1);
lastTaskInst.setAssignee(account.getAccountId());
lastTaskInst.setAssigneeName(account.getName());
break;
}
}
}
//第一行添加发起流程任务
TaskInst tk = taskInstDao.getStartProcTask(pid);
tasks.add(0, tk);
return tasks;
}
4.相关的一些dao层sql
procInstService.getEntityById(pid)
<!-- 通过id获取对象 -->
<select id="getEntityById" resultMap="pi" parameterType="String">
select pi.id_ as id,pi.proc_inst_id_ as procInstId,pi.business_key_ as businessKey,
pi.proc_def_id_ as procDefId,pi.start_time_ as startTime,pi.end_time_ as endTime,
pi.duration_ as duration, pi.start_user_id_ as startUserId,pi.start_act_id_ as startActId,
pi.end_act_id_ as endActId, pi.super_process_instance_id_ as superPrcessInstanceId,
pi.delete_reason_ as deleteReason, pi.tenant_id_ as tenantId,pi.name_ as name,pi.summary as summary
from ACT_HI_PROCINST pi
where pi.proc_inst_id_=#{id}
</select>
procDefService.getEntityById(pi.getProcDefId())
<!-- 通过id获取对象 -->
<select id="getEntityById" resultMap="procDef" parameterType="String">
select pd.id_ as id ,pd.name_ as name ,pd.key_ as key ,pd.version_ as version ,pd.category_ as category,pd.deployment_id_ as deploymentId,
pd.resource_name_ as resourceName ,pd.dgrm_resource_name_ as dgrmResourceName ,pd.description_ as description
,ba.id_ as ba_id ,ba.rev_ as ba_rev ,ba.name_ as ba_name ,ba.deployment_id_ as ba_deploymentId ,ba.bytes_ as ba_bytes
from ACT_RE_PROCDEF pd
left join ACT_GE_BYTEARRAY ba on pd.deployment_id_=ba.deployment_id_ and pd.resource_name_=ba.name_
where pd.id_ = #{id}
</select>
taskInstDao.getTaskList(pid)
<select id="getTaskList" resultMap="base" parameterType="String">
select t.id_, t.proc_Def_Id_, t.task_Def_Key_, t.proc_Inst_Id_, t.name_, t.description_, t.start_time_, t.end_time_, t.owner_, t.assignee_
, a.name as assigneeName, t.form_Key_ ,c.message_ as lastComment
from ACT_HI_TASKINST t
left join jy_base_account a on t.assignee_=a.id
left join(
select c.*,ROW_NUMBER() OVER(partition by c.task_id_ order by c.time_ desc) as req
from act_hi_comment c
)c on c.task_id_=t.id_ and req=1
where t.proc_inst_id_=#{pid}
order by t.start_time_
</select>
taskInstDao.getCandidateUsers(lastTaskInst.getId())
<select id="getCandidateUsers" resultMap="com.jy.repository.system.account.AccountDao.base" parameterType="String">
select a.id,
a.loginName,
a.roleId,
jbr.name as roleName,
a.name,
a.picUrl,
a.email,
a.isValid,
a.createTime,
a.updateTime,
a.skin,
a.description
from (
select Translate(t.user_id_ USING CHAR_CS) as accountid
from ACT_HI_IDENTITYLINK t
where task_id_=#{taskId} and t.user_id_ is not null
union
select ap.accountid as accountid
from ACT_HI_IDENTITYLINK t
left join jy_base_position p on t.group_id_=p.name
left join jy_base_account_position ap on ap.posid=p.id
where task_id_=#{taskId} and t.group_id_ is not null
)t1
inner join jy_base_account a on t1.accountid=a.id
LEFT JOIN JY_BASE_ROLE jbr ON jbr.id=a.roleId
</select>
taskInstDao.getStartProcTask(pid)
<!-- 获取流程发起人,并封装为发起流程任务 -->
<select id="getStartProcTask" resultMap="base" parameterType="String">
select '发起流程' as id_,'发起流程' as name_,t.start_user_id_ as assignee_, a.name as assigneeName,t.start_time_ end_Time_
from act_hi_procinst t
left join jy_base_account a on t.start_user_id_=a.id
where t.proc_inst_id_=#{pid}
</select>
5.流程相关的实体。
ProcDef.java
public class ProcDef extends BaseEntity{
private static final long serialVersionUID = 1L;
private String id;
private String name;
private String key;
private String version;
private String category;
private String deploymentId;
private String resourceName;
private String dgrmResourceName;
private String description;
private ByteArray byteArray;
}
public class ProcInst extends BaseEntity{
private static final long serialVersionUID = 1L;
private String id;
private String procInstId;
private String businessKey;
private String procDefId;
private Date startTime;
private Date endTime;
private Long duration; //耗时
private String startUserId;
private Account startUser;
private String startActId;
private String endActId;
private String superPrcessInstanceId;
private String deleteReason;
private String tenantId;
private String name;
private String summary;
private ProcDef procDef;
}
public class TaskInst extends BaseEntity{
private String id;
private String processDefinitionId;
private String taskDefinitionKey;
private String processInstanceId;
private ProcInst pi;
private String name;
private String description;
private Date startTime;
private Date endTime;
private String owner;
private String assignee;
private String assigneeName;
private String formKey;
private String comment;
private int isCurAccount;//0表示该任务不是当前用户处理,1表示是该用户处理
private List<Account> candidateUsers;
private List<Comment> comments;
}
ByteArray.java
public class ByteArray extends BaseEntity{
private static final long serialVersionUID = 1L;
String id;
String rev;
String name;
String deploymentId;
byte[] bytes;
}