Activiti如何实现流程的回退
1.概述
流程回退一直以来是个老旧的难题,也一直没有好的解决方法,本文就来详述流程回退的解决办法。首先我们来分析一下不同的流程审批情况,并在对应的节点上实现流程的回退处理,以及应该提供的回退处理,当然我们说的回退不是指通过在流程节点上画一条线回退到想回的节点上。
回退时,需要解决两种情况:
- 回退到发起人
- 回退到上一步及逐步回退
因为回退至任一节点上,Activiti本身的api是不支持的,我们只能通过扩展activiti的的api,以实现*跳转才达到回退至任一节点上,但有情况是例外的,回退的时候,需要注意,否则activiti在跳转的时候,数据是容易出问题的,主要是在并发的节点分支里跳到外面时(如下图所示,B、D节点回到A节点时),其执行的实例Id会变化,因此,需要注意对这种情况下的流程跳转作一些限制。
那么我们需要在当前审批的任务上,需要进行回退到任何一个节点,实现*跳转时,如何扩展,如下为我们扩展activiti来实现*跳转的实现方式:
/** * 将节点之后的节点删除然后指向新的节点。 * @param actDefId 流程定义ID * @param nodeId 流程节点ID * @param aryDestination 需要跳转的节点 * @return Map<String,Object> 返回节点和需要恢复节点的集合。 */ @SuppressWarnings("unchecked") private Map<String,Object> prepare(String actDefId,String nodeId,String[] aryDestination){ Map<String,Object> map=new HashMap<String, Object>(); //修改流程定义 ProcessDefinitionEntity processDefinition = (ProcessDefinitionEntity)repositoryService.getProcessDefinition(actDefId); ActivityImpl curAct= processDefinition.findActivity(nodeId); List<PvmTransition> outTrans= curAct.getOutgoingTransitions(); try{ List<PvmTransition> cloneOutTrans=(List<PvmTransition>) FileUtil.cloneObject(outTrans); map.put("outTrans", cloneOutTrans); } catch(Exception ex){ } /** * 解决通过选择*跳转指向同步节点导致的流程终止的问题。 * 在目标节点中删除指向自己的流转。 */ for(Iterator<PvmTransition> it=outTrans.iterator();it.hasNext();){ PvmTransition transition=it.next(); PvmActivity activity= transition.getDestination(); List<PvmTransition> inTrans= activity.getIncomingTransitions(); for(Iterator<PvmTransition> itIn=inTrans.iterator();itIn.hasNext();){ PvmTransition inTransition=itIn.next(); if(inTransition.getSource().getId().equals(curAct.getId())){ itIn.remove(); } } } curAct.getOutgoingTransitions().clear(); if(aryDestination!=null && aryDestination.length>0){ for(String dest:aryDestination){ //创建一个连接 ActivityImpl destAct= processDefinition.findActivity(dest); TransitionImpl transitionImpl = curAct.createOutgoingTransition(); transitionImpl.setDestination(destAct); } } map.put("activity", curAct); return map; } /** * 将临时节点清除掉,加回原来的节点。 * @param map * void */ @SuppressWarnings("unchecked") private void restore(Map<String,Object> map){ ActivityImpl curAct=(ActivityImpl) map.get("activity"); List<PvmTransition> outTrans=(List<PvmTransition>) map.get("outTrans"); curAct.getOutgoingTransitions().clear(); curAct.getOutgoingTransitions().addAll(outTrans); } /** * 通过指定目标节点,实现任务的跳转 * @param taskId 任务ID * @param destNodeIds 跳至的目标节点ID * @param vars 流程变量 */ public synchronized void completeTask(String taskId,String[] destNodeIds,Map<String,Object> vars) { TaskEntity task=(TaskEntity)taskService.createTaskQuery().taskId(taskId).singleResult(); String curNodeId=task.getTaskDefinitionKey(); String actDefId=task.getProcessDefinitionId(); Map<String,Object> activityMap= prepare(actDefId, curNodeId, destNodeIds); try{ taskService.complete(taskId); } catch(Exception ex){ throw new RuntimeException(ex); } finally{ //恢复 restore(activityMap); } }
若我们需要进行跳转,就需要知道回退上一步时,其上一步是什么节点。如何仅是通过流程获得其回退的节点,这是达不到业务的需求的,因为有时我们需要回退到某个节点处理后,下一步需要回到原来的节点上,如我们在上图E节点上,回退时,E回退需要回到D或C上,完成后再回到B,这种情况下我们可以要求E必须需要去到G1节点上,往下执行。这种回退就会显得人性化,同时也保证流程实例在后续的执行过程中,其信号及各参数是正常的,这时就要求我们需要有一个完整记录流程实例执行经过的各个节点ID的数据,并且通过以下的数据可以快速找到当前节点回退时,应该回退到哪一个节点上,并且当时这个节点的执行人员是谁。
2.如何记录流程的执行过程
为了更好记录流程经过的树节点,我们采用了一个树结构来存储流程实例执行时,经过的流程节点,如上图所示,其执行的树型图所示所示:
我们需要在各个节点那里可以找到其退回至上一步环节的父节点那里,这需要一个算法,如在B或D那里回退,我们让他退回A,在C回退我们让他回到B,若我们在E位置回退,我们需要让他回到G1那里。这个算法的实现不算复杂,有这个树型的执行树数据后,一切变得很简单。但要注意一点,我们在回退时,需要记录他是从哪个节点回退过来的,若用户处理完成后,可以要求他直接回到原回退的节点去,也可以按流程定义重新走一次审批。假如执行到E,让他回退时并且重新审批,其执行的树图如下所示:
注意G1,那里有指向E,当完成时,可以让他来跳到E上,这就是任务完成后,可以找到它应该跳至哪一个任务节点上。
3.扩展表记录流程的执行的路径
/*==============================================================*/ /* Table: BPM_RU_PATH */ /*==============================================================*/ CREATE TABLE BPM_RU_PATH ( PATH_ID_ VARCHAR(64) NOT NULL, INST_ID_ VARCHAR(64) NOT NULL COMMENT '流程实例ID', ACT_DEF_ID_ VARCHAR(64) NOT NULL COMMENT 'Act定义ID', ACT_INST_ID_ VARCHAR(64) NOT NULL COMMENT 'Act实例ID', SOL_ID_ VARCHAR(64) NOT NULL COMMENT '解决方案ID', NODE_ID_ VARCHAR(255) NOT NULL COMMENT '节点ID', NODE_NAME_ VARCHAR(255) COMMENT '节点名称', NODE_TYPE_ VARCHAR(50) COMMENT '节点类型', START_TIME_ DATETIME NOT NULL COMMENT '开始时间', END_TIME_ DATETIME COMMENT '结束时间', DURATION_ INT COMMENT '持续时长', DURATION_VAL_ INT COMMENT '有效审批时长', ASSIGNEE_ VARCHAR(64) COMMENT '处理人ID', TO_USER_ID_ VARCHAR(64) COMMENT '代理人ID', IS_MULTIPLE_ VARCHAR(20) COMMENT '是否为多实例', EXECUTION_ID_ VARCHAR(64) COMMENT '活动执行ID', USER_IDS_ VARCHAR(300) COMMENT '原执行人IDS', PARENT_ID_ VARCHAR(64) COMMENT '父ID', LEVEL_ INT COMMENT '层次', OUT_TRAN_ID_ VARCHAR(255) COMMENT '跳出路线ID', TOKEN_ VARCHAR(255) COMMENT '路线令牌', JUMP_TYPE_ VARCHAR(50) COMMENT '跳到该节点的方式 正常跳转 *跳转 回退跳转', NEXT_JUMP_TYPE_ VARCHAR(50) COMMENT '下一步跳转方式', OPINION_ VARCHAR(500) COMMENT '审批意见', REF_PATH_ID_ VARCHAR(64) COMMENT '引用路径ID 当回退时,重新生成的结点,需要记录引用的回退节点,方便新生成的路径再次回退。', TENANT_ID_ VARCHAR(64) COMMENT '租用机构ID', CREATE_BY_ VARCHAR(64) COMMENT '创建人ID', CREATE_TIME_ DATETIME COMMENT '创建时间', UPDATE_BY_ VARCHAR(64) COMMENT '更新人ID', UPDATE_TIME_ DATETIME COMMENT '更新时间', PRIMARY KEY (PATH_ID_) ); ALTER TABLE BPM_RU_PATH COMMENT '流程实例运行路线';
4.如何创建执行路径
有了上面的表结构后,如何让activiti在执行的过程中,往上面的表加上我们需要的数据,这时我们就需要利用activiti的全局事件监听器,具体的实现请参考我的 【全局事件监听处理】。
<bean id="globalEventListener" class="com.redxun.bpm.activiti.listener.GlobalEventListener"> <property name="handlers"> <map> <entry key="TASK_CREATED" value="taskCreateListener"/> <entry key="TASK_COMPLETED" value="taskCompleteListener"/> <entry key="TASK_ASSIGNED" value="taskAssignedListener"/> <entry key="PROCESS_COMPLETED" value="processCompleteListener"/> <entry key="ACTIVITY_STARTED" value="activityStartedListener"/> <entry key="ACTIVITY_COMPLETED" value="activityCompletedListener"/> <entry key="ACTIVITY_SIGNALED" value="activitySignaledListener"/> <entry key="PROCESS_STARTED" value="processStartEventListener"/> </map> </property> </bean>
其中Activiti提供了两个不错的事件监听,一个是执行实体创建事件ACTIVITY_STARTED,一个实体完成的事件ACTIVITY_COMPLETED。我们分别在这两个事件上加上bpm_ru_path表的记录创建与更新即可。在其回退的时候,通过算法找到其需要回退的节点,然后通过上文提供的*跳转方法,即可以实现流程的回退。
package com.redxun.bpm.activiti.listener; import java.util.Date; import java.util.Map; import javax.annotation.Resource; import org.activiti.engine.RuntimeService; import org.activiti.engine.delegate.event.ActivitiEvent; import org.activiti.engine.delegate.event.impl.ActivitiActivityEventImpl; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import com.redxun.bpm.activiti.entity.ActNodeDef; import com.redxun.bpm.activiti.service.ActRepService; import com.redxun.bpm.activiti.util.ProcessHandleHelper; import com.redxun.bpm.core.entity.BpmInst; import com.redxun.bpm.core.entity.BpmRuPath; import com.redxun.bpm.core.entity.IExecutionCmd; import com.redxun.bpm.core.entity.ProcessNextCmd; import com.redxun.bpm.core.entity.ProcessStartCmd; import com.redxun.bpm.core.entity.config.ActivityConfig; import com.redxun.bpm.core.entity.config.BpmEventConfig; import com.redxun.bpm.core.manager.BpmInstManager; import com.redxun.bpm.core.manager.BpmNodeSetManager; import com.redxun.bpm.core.manager.BpmRuPathManager; import com.redxun.bpm.enums.TaskEventType; import com.redxun.core.constants.MBoolean; import com.redxun.core.script.GroovyEngine; import com.redxun.saweb.util.IdUtil; /** * 活动节点开始时的监听器 * @author keitch * */ public class ActivityStartedListener implements EventHandler{ private Log logger=LogFactory.getLog(ActivityStartedListener.class); @Resource BpmRuPathManager bpmRuPathManager; @Resource BpmInstManager bpmInstManager; @Resource ActRepService actRepService; @Resource RuntimeService runtimeService; @Resource BpmNodeSetManager bpmNodeSetManager; @Resource GroovyEngine groovyEngine; /** * 执行脚本事件 * @param eventImpl */ public void executeEventScript(ActivitiActivityEventImpl eventImpl){ String solId=(String)runtimeService.getVariable(eventImpl.getExecutionId(), "solId"); //处理事件 ActivityConfig actConfig=bpmNodeSetManager.getActivityConfig(solId, eventImpl.getActivityId()); if(actConfig.getEvents().size()>0){ BpmEventConfig bpmEventConfig=null; for(BpmEventConfig eventConfig:actConfig.getEvents()){ if(TaskEventType.ACTIVITY_STARTED.name().equals(eventConfig.getEventKey())){ bpmEventConfig=eventConfig; break; } } //执行脚本 if(bpmEventConfig!=null && StringUtils.isNotEmpty(bpmEventConfig.getScript())){ logger.debug("==================execute the ActivityStartedListener complete listener:"+bpmEventConfig.getScript()); Map<String,Object> vars=runtimeService.getVariables(eventImpl.getExecutionId()); vars.put("executionId",eventImpl.getExecutionId()); groovyEngine.executeScripts(bpmEventConfig.getScript(),vars); } } } /** * 创建执行路径的数据,用于流程图的追踪,流程回退及执行等 */ @Override public void handle(ActivitiEvent event) { logger.debug("enter the event ActivityStartedListener handler is .....============"); ActivitiActivityEventImpl eventImpl=(ActivitiActivityEventImpl)event; String activityId=eventImpl.getActivityId(); String entityName=eventImpl.getActivityName(); logger.debug("entity:"+activityId + " entityName:"+entityName); IExecutionCmd cmd=ProcessHandleHelper.getProcessCmd(); ActNodeDef actNodeDef=actRepService.getActNodeDef(eventImpl.getProcessDefinitionId(), activityId); //判断一些并行的网关的结束点,防止其生成多条记录 if(eventImpl.getActivityType().indexOf("Gateway")!=-1 || StringUtils.isNotEmpty(actNodeDef.getMultiInstance())){ BpmRuPath ruPath= bpmRuPathManager.getFarestPath(eventImpl.getProcessInstanceId(),eventImpl.getActivityId()); if(ruPath!=null){ cmd.setNodeId(activityId); if("userTask".equals(eventImpl.getActivityType())){ //获得会签人员列表,并且进行会签人员设置 if(StringUtils.isEmpty(ruPath.getUserIds())){ String userIds=(String)runtimeService.getVariable(eventImpl.getExecutionId(), "signUserIds_"+activityId); ruPath.setUserIds(userIds); bpmRuPathManager.update(ruPath); } } return; } } //创建执行路径 BpmRuPath path=new BpmRuPath(); path.setPathId(IdUtil.getId()); path.setActDefId(eventImpl.getProcessDefinitionId()); path.setActInstId(eventImpl.getProcessInstanceId()); path.setExecutionId(eventImpl.getExecutionId()); path.setNodeName(actNodeDef.getNodeName()); path.setNodeId(activityId); path.setNodeType(actNodeDef.getNodeType()); path.setStartTime(new Date()); if(cmd instanceof ProcessStartCmd){//若为启动时,需要从线程中获得 ProcessStartCmd startCmd=(ProcessStartCmd)cmd; path.setInstId(startCmd.getBpmInstId()); path.setSolId(startCmd.getSolId()); }else{ BpmInst bpmInst=bpmInstManager.getByActInstId(eventImpl.getProcessInstanceId()); path.setInstId(bpmInst.getInstId()); path.setSolId(bpmInst.getSolId()); path.setNextJumpType(((ProcessNextCmd)cmd).getNextJumpType()); } //是否为多实例 if(StringUtils.isNotEmpty(actNodeDef.getMultiInstance())){ path.setIsMultiple(MBoolean.YES.name()); }else{ path.setIsMultiple(MBoolean.NO.name()); } //记录跳转的原节点,并且把跳转记录挂至该节点上 BpmRuPath parentPath=null; if(cmd!=null && StringUtils.isNotEmpty(cmd.getNodeId())){ parentPath=bpmRuPathManager.getFarestPath(eventImpl.getProcessInstanceId(),cmd.getNodeId()); } if(parentPath!=null){ path.setParentId(parentPath.getPathId()); path.setLevel(parentPath.getLevel()+1); }else{ path.setLevel(1); path.setParentId("0"); } //是否由回退时产生的,若是需要记录回退时的流程ID BpmRuPath bpmRuPath=ProcessHandleHelper.getBackPath(); if(bpmRuPath!=null){ path.setRefPathId(bpmRuPath.getPathId()); } //当从开始启动时,进入两次,这时需要记录其父ID //或在任务节点后的非任务点时,传递其父Id if(!"userTask".equals(actNodeDef.getNodeType())){ cmd.setNodeId(eventImpl.getActivityId()); } bpmRuPathManager.create(path); } }
完成的事件处理
package com.redxun.bpm.activiti.listener; import java.util.Date; import java.util.Map; import javax.annotation.Resource; import org.activiti.engine.RuntimeService; import org.activiti.engine.delegate.event.ActivitiEvent; import org.activiti.engine.delegate.event.impl.ActivitiActivityEventImpl; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import com.redxun.bpm.activiti.util.ProcessHandleHelper; import com.redxun.bpm.core.entity.BpmRuPath; import com.redxun.bpm.core.entity.IExecutionCmd; import com.redxun.bpm.core.entity.ProcessNextCmd; import com.redxun.bpm.core.entity.config.ActivityConfig; import com.redxun.bpm.core.entity.config.BpmEventConfig; import com.redxun.bpm.core.manager.BpmNodeSetManager; import com.redxun.bpm.core.manager.BpmRuPathManager; import com.redxun.bpm.enums.TaskEventType; import com.redxun.bpm.enums.TaskOptionType; import com.redxun.core.script.GroovyEngine; import com.redxun.saweb.context.ContextUtil; /** * 活动节点结束时的监听器 * @author keitch * */ public class ActivityCompletedListener implements EventHandler{ private Log logger=LogFactory.getLog(ActivityCompletedListener.class); @Resource BpmRuPathManager bpmRuPathManager; @Resource BpmNodeSetManager bpmNodeSetManager; @Resource GroovyEngine groovyEngine; @Resource RuntimeService runtimeService; /** * 执行脚本事件 * @param eventImpl */ public void executeEventScript(ActivitiActivityEventImpl eventImpl){ String solId=(String)runtimeService.getVariable(eventImpl.getExecutionId(), "solId"); //处理事件 ActivityConfig actConfig=bpmNodeSetManager.getActivityConfig(solId, eventImpl.getActivityId()); if(actConfig.getEvents().size()>0){ BpmEventConfig bpmEventConfig=null; for(BpmEventConfig eventConfig:actConfig.getEvents()){ if(TaskEventType.ACTIVITY_COMPLETED.name().equals(eventConfig.getEventKey())){ bpmEventConfig=eventConfig; break; } } //执行脚本 if(bpmEventConfig!=null && StringUtils.isNotEmpty(bpmEventConfig.getScript())){ logger.debug("==================execute the ActivityCompletedListener complete listener:"+bpmEventConfig.getScript()); Map<String,Object> vars=runtimeService.getVariables(eventImpl.getExecutionId()); vars.put("executionId",eventImpl.getExecutionId()); groovyEngine.executeScripts(bpmEventConfig.getScript(),vars); } } } @Override public void handle(ActivitiEvent event) { logger.debug("enter the event ActivityCompletedListener handler is .....============"); ActivitiActivityEventImpl eventImpl=(ActivitiActivityEventImpl)event; //执行配置的事件脚本 executeEventScript(eventImpl); IExecutionCmd cmd=ProcessHandleHelper.getProcessCmd(); BpmRuPath ruPath=bpmRuPathManager.getFarestPath(eventImpl.getProcessInstanceId(),eventImpl.getActivityId() ); if(ruPath!=null){ ruPath.setAssignee(ContextUtil.getCurrentUserId()); //TODO 设置代理人,表示代理谁来执行 //ruPath.setAgnentUserId(aValue); if(cmd instanceof ProcessNextCmd){ ruPath.setToUserId(((ProcessNextCmd)cmd).getAgentToUserId()); } ruPath.setEndTime(new Date()); Long duration=ruPath.getEndTime().getTime()-ruPath.getStartTime().getTime(); ruPath.setDuration(duration.intValue()); //TODO,结合工作日历计算有效时间 ruPath.setDurationVal(duration.intValue()); if(cmd!=null && "userTask".equals(ruPath.getNodeType())){ if(StringUtils.isNotBlank(cmd.getJumpType())){ ruPath.setJumpType(cmd.getJumpType()); ruPath.setOpinion(cmd.getOpinion()); }else{ ruPath.setJumpType(TaskOptionType.AGREE.name()); ruPath.setOpinion("同意"); } } //更新其数据 bpmRuPathManager.update(ruPath); } } }
5.流程回退处理
有了以上的执行数据,流程的回退,就可以通过算法找到其需要回退的流程节点,从而可以实现流程的回退处理,注意以下的获得当前任务的回退节点Id,然后指定这个节点Id为执行完成后,需要跳转至这个节点上。
注意这部分代码 BpmRuPath bpmRuPath = getBackNodeId(task.getProcessInstanceId(), task.getTaskDefinitionKey());
/** * 任务往下跳转 * * @param taskId * @param jsonData * @param vars * @throws Exception */ public void doNext(ProcessNextCmd cmd) throws Exception { boolean isSetBackPath = false; try { TaskEntity task = (TaskEntity) taskService.createTaskQuery().taskId(cmd.getTaskId()).singleResult(); UserTaskConfig userTaskConfig=bpmNodeSetManager.getTaskConfig(task.getSolId(), task.getTaskDefinitionKey()); //String processInstanceId = task.getProcessInstanceId(); // 加上executionId,用来记录执行的路径 cmd.setNodeId(task.getTaskDefinitionKey()); // 加上线程变量 ProcessHandleHelper.setProcessCmd(cmd); BpmInst bpmInst = bpmInstManager.getByActInstId(task.getProcessInstanceId()); BpmFormInst bpmFormInst = bpmFormInstManager.get(bpmInst.getFormInstId()); try { String newJson = JSONUtil.copyJsons(bpmFormInst.getJsonData(), cmd.getJsonData()); bpmFormInst.setJsonData(newJson); bpmFormInstManager.saveOrUpdate(bpmFormInst); } catch (Exception ex) { logger.error(ex.getCause()); } Map<String, Object> vars = handleTaskVars(task, cmd.getJsonData()); // 加上外围传过来的变量 if (cmd.getVars() != null) { vars.putAll(cmd.getVars()); } // 若为回退,则处理回退的操作 if (TaskOptionType.BACK.name().equals(cmd.getJumpType())) { BpmRuPath bpmRuPath = getBackNodeId(task.getProcessInstanceId(), task.getTaskDefinitionKey()); // 没有找到回退的节点,提示用户 if (bpmRuPath == null) { ProcessHandleHelper.getProcessMessage().getErrorMsges().add("本环节不能回退!没有找到上一步的回退审批环节!"); return; } else {// 设置回退的节点 cmd.setDestNodeId(bpmRuPath.getNodeId()); ProcessHandleHelper.setBackPath(bpmRuPath); isSetBackPath = true; } } else if (TaskOptionType.BACK_TO_STARTOR.name().equals(cmd.getJumpType())) {// 回退至发起人 ActNodeDef afterNode = actRepService.getNodeAfterStart(task.getProcessDefinitionId()); if (afterNode == null) { ProcessHandleHelper.getProcessMessage().getErrorMsges().add("没有找到发起人所在的审批环节!"); return; } else { cmd.setDestNodeId(afterNode.getNodeId()); } } else { // 查找是否为原路返回的模式,即当前任务是否由回退处理的 BpmRuPath ruPath = bpmRuPathManager.getFarestPath(task.getProcessInstanceId(), task.getTaskDefinitionKey()); if (ruPath != null && "".equals(ruPath.getNextJumpType())) { BpmRuPath toNodePath = bpmRuPathManager.get(ruPath.getParentId()); if (toNodePath != null) { cmd.setDestNodeId(toNodePath.getNodeId()); } } } //加上前置处理 if(StringUtils.isNotEmpty(userTaskConfig.getPreHandle())){ Object preBean=AppBeanUtil.getBean(userTaskConfig.getPreHandle()); if(preBean instanceof TaskPreHandler){ TaskPreHandler handler=(TaskPreHandler)preBean; handler.taskPreHandle(cmd, task, bpmInst.getBusKey()); } } // 以下为任务的跳转处理 if (StringUtils.isNotEmpty(cmd.getDestNodeId())) {// 进行指定节点的跳转 actTaskService.completeTask(cmd.getTaskId(), new String[] { cmd.getDestNodeId() }, vars); } else {// 正常跳转 taskService.complete(cmd.getTaskId(), vars); } //加上后置处理 if(StringUtils.isNotEmpty(userTaskConfig.getAfterHandle())){ Object preBean=AppBeanUtil.getBean(userTaskConfig.getAfterHandle()); if(preBean instanceof TaskAfterHandler){ TaskAfterHandler handler=(TaskAfterHandler)preBean; handler.taskAfterHandle(cmd, task.getTaskDefinitionKey(), bpmInst.getBusKey()); } } } catch (Exception e) { e.printStackTrace(); logger.error(e.getCause()); throw e; } finally { ProcessHandleHelper.clearProcessCmd(); if (isSetBackPath) { ProcessHandleHelper.clearBackPath(); } } }
具体的实现效果可以参考如下在线示例,
http://www.redxun.cn:8020/saweb/index.do,
user:admin
pwd:1
http://redxun.iteye.com/blog/2406509
需要在流程解决方案的节点配置中,打开回退按钮,如下图所示: