`
pdd7531
  • 浏览: 4303 次
  • 性别: Icon_minigender_1
  • 来自: 无锡
社区版块
存档分类
最新评论

JBPM流程复制功能实现

    博客分类:
  • JBPM
阅读更多
      公司产品一直用JBPM作为流程驱动核心,之前的主要应用场景为数据审批等变动较小的业务范围,今年新开发了产品模块,用JBPM来驱动协同项目,实现多人协同工作的需求,在WBS分解项目时无法做到确保项目运行过程中不会变动,所以出现了项目运行过程中需要能动态改变流程定义并保证之前已经运行过的项目节点数据不会丢失的需求。
       在网上搜索了很多,没有发现类似的流程实例拷贝的功能,自己试着实现了一个版本,当然有诸多限制,譬如已完成节点不可编辑,不可删除,否则流程无法正确流至上个流程实例的当前节点。
        数据库中增加了流程过程描述表,主要字段为主流程实例ID,项目ID,流程任务ID,项目任务ID,以及任务的outCome。流程运行过程中,完成任务操作时往这张表中增加记录,记录流程运行过程。
        实例拷贝主要分为六步,一、根据项目ID获取最新流程定义。二、根据最新流程定义获取上个版本流程定义。三、根据上个版本流程定义获取上个版本的流程实例。四、根据上个版本流程实例抽取出上个版本中已经运行的任务的描述,包括任务关联数据,任务走向等。五、开始最新流程实例,递归寻找当前任务,从上个版本的任务描述中拷贝关联数据以及确定任务走向。六、清理上个版本流程数据。
        实现的重点是要考虑分支,子流程等复杂元素,特变是嵌套分支,嵌套子流程等也要能完美运行通过,主要的业务是在抽取历史任务数据以及递归推动最新流程,绑定历史任务数据的地方。代码的编写过程中,也运用了一些JBMP的源码知识,譬如控制执行先后顺序参开了ExecutionImpl的performAtomicOperation的模式,匹配历史任务时采用了责任链模式,将历史任务按照执行顺序组织成责任链模式,匹配成功后跳过已匹配的任务。
  附上关键代码:
public abstract class EDMAtomicOperation implements Serializable {

	//serialVersionUID is
	
	private static final long serialVersionUID = 1L;
	
	public static final EDMAtomicOperation GET_LATEST_PDID = new GetLatestPdIdOperation();
	public static final EDMAtomicOperation GET_LAST_VERSION_PDID = new GetLastVersionPdIdOperation();
	public static final EDMAtomicOperation GET_LAST_VERSION_PIID = new GetLastVersionPiIdOperation();
	public static final EDMAtomicOperation GET_LAST_VERSION_TASKDESC = new GetLastVersionTaskDescOperation();
	public static final EDMAtomicOperation COPY_PROCESS = new CopyProcessDataOperation();
	public static final EDMAtomicOperation CLEAN_OLD_PROCESS = new CleanOldProcessOperation();
	
	
	public abstract void perform(ProcessCourserImpl processCourse);
}


public interface ProcessCourser {

	public void sign();
	
	public String getErrorMessage();
}


public class ProcessCourserImpl implements ProcessCourser,Serializable{

	//serialVersionUID is
	
	private static final long serialVersionUID = -3239018905632135241L;
	
	public ProcessCourserImpl(String projectId, ProcessCourseTool processCourseTool, String userShowName){
		this.projectId = projectId;
		this.processCourseTool = processCourseTool;
		this.userShowName = userShowName;
	}

	/**
	 *@Function Name:  sign
	 *@Description:   开始工作
	 *@Date Created:  2013-12-24 下午04:46:35
	 *@Author:  Pan Duan Duan
	 *@Last Modified:     ,  Date Modified: 
	 */
	@Override
	public void sign() {
		//如果还未获取最新流程定义
		if("".equals(latestPdId)){
			performAtomicOperation(EDMAtomicOperation.GET_LATEST_PDID);
		}
	}
	
	/**
	 *@Function Name:  performAtomicOperation
	 *@Description: @param operation 
	 *@Date Created:  2013-12-25 上午08:34:54
	 *@Author:  Pan Duan Duan 执行操作
	 *@Last Modified:     ,  Date Modified: 
	 */
	public synchronized void performAtomicOperation(EDMAtomicOperation operation) {
		performAtomicOperationSync(operation);
	}

	/**
	 *@Function Name:  performAtomicOperationSync
	 *@Description: @param operation 
	 *@Date Created:  2013-12-25 上午08:35:02
	 *@Author:  Pan Duan Duan 以同步的方式执行操作
	 *@Last Modified:     ,  Date Modified: 
	 */
	private void performAtomicOperationSync(EDMAtomicOperation operation) {
		//初始化
		if (atomicOperations==null) {
			atomicOperations = new LinkedList<EDMAtomicOperation>();
			atomicOperations.offer(operation);
			while (! atomicOperations.isEmpty()) {
				EDMAtomicOperation atomicOperation = atomicOperations.poll();
				atomicOperation.perform(this);
			}
		}else {
		      atomicOperations.offer(operation);
	    }
	}


public class CopyProcessDataOperation extends EDMAtomicOperation {

	Logger	log	= Logger.getLogger(this.getClass().getName());
	//serialVersionUID is
	
	private static final long serialVersionUID = -6126577335188267494L;

	@Override
	public void perform(ProcessCourserImpl processCourse) {
		log.warn("Copy Process Step 5: copy old process info to the new one");
		//最新流程定义
		String latestPdId = processCourse.getLatestPdId();
		//得到当前用户显示名称
		String userShowName = processCourse.getUserShowName();
		String latestPiId = processCourse.getProcessCourseTool().getProcessInstanceTool().startProcessInstance(latestPdId,userShowName);
		//上个版本流程任务描述集合
		List<EDMTaskDesc> lastTaskDescs = processCourse.getTaskDescs();
		if(null != lastTaskDescs && lastTaskDescs.size() > 0){
			processFlow(latestPiId,processCourse);
		}
		processCourse.performAtomicOperation(EDMAtomicOperation.CLEAN_OLD_PROCESS);
	}

	/**
	 *@Function Name:  processFlow
	 *@Description: @param latestProcessInstance
	 *@Description: @param processCourse 
	 *@Date Created:  2013-12-26 上午09:26:54
	 *@Author:  Pan Duan Duan 驱动流程 复制数据
	 *@Last Modified:     ,  Date Modified: 
	 */
	private void processFlow(String latestPiId,
			ProcessCourserImpl processCourse) {
		
		//当前流程主流程ID
		String mainPiId = latestPiId;
		//
		ProcessInstanceTool processInstanceTool = processCourse.getProcessCourseTool().getProcessInstanceTool();
		//当前任务集合
		List<Task> currentTasks = new ArrayList<Task>();
		while(currentTasks.isEmpty()){
			//获取最新流程的当前任务集合
			getNextTaskByPiId(mainPiId, currentTasks, processInstanceTool);
			//已完成任务集合
			List<Task> completedTask = new ArrayList<Task>();
			for(Task currentTask : currentTasks){
				//完成任务 拷贝任务数据
				boolean completed = complateCurrentTask(currentTask,processInstanceTool,processCourse,mainPiId);
				if(completed){
					completedTask.add(currentTask);
				}
			}
			//移除已经完成的任务 推动流程
			for(Task tempTask : completedTask){
				currentTasks.remove(tempTask);
			}
		}
		//
	}
	
	/**
	 *@param processCourse 
	 * @Function Name:  complateCurrentTask
	 *@Description: @param currentTask
	 *@Description: @param processInstanceTool 
	 *@Date Created:  2013-12-26 上午11:05:06
	 *@Author:  Pan Duan Duan 完成当前任务
	 *@Last Modified:     ,  Date Modified: 
	 */
	private boolean complateCurrentTask(Task currentTask,
			ProcessInstanceTool processInstanceTool, ProcessCourserImpl processCourse, String mainPiId) {
		//流程引擎
		ProcessEngine processEngine = processInstanceTool.getProcessEngine();
		//得到流程实例Service
		ExecutionService executionService = processEngine.getExecutionService();
		//得到taskService
		TaskService taskService = processEngine.getTaskService();
		//返回结果
		boolean flag = false;
		//
		try{
			//获取任务ID
			String taskId = currentTask.getId();
			//得到executionId
			String executionId = currentTask.getExecutionId();
			//得到流程实例
			Execution execution = executionService.findExecutionById(executionId);
			//得到主线流程
			execution = processInstanceTool.findMainExecution(execution);
			//获取项目任务ID
			String prjTaskId = processInstanceTool.getTaskDataIdByProcessTaskId(taskId);
			//复制任务数据
			EDMTaskDesc copyedTaskDesc = processCourse.getTaskDescs().get(0).doCopy(prjTaskId, processInstanceTool);
			//获取新的变量ID
			if(null != copyedTaskDesc){
				String newVariableId = copyedTaskDesc.getNewVariableId();
				//获取新变量实例
				EdmTaskVariable edmTaskVariable = processInstanceTool.getEdmTaskVariableById(newVariableId);
				//获取任务执行人
				String taskOperator = edmTaskVariable.getOperator();
				//转化为登录名称
				taskOperator = processInstanceTool.getUserLoginNameByShowName(taskOperator);
				//注入流程任务中
				processEngine.execute(new TaskDelegateCmd(taskId, taskOperator));
				//保存任务变量
				executionService.createVariable(execution.getId(), taskId, edmTaskVariable, true);
				//得到任务流向
				String outCome = copyedTaskDesc.getOutCome();
				//完成任务
				taskService.completeTask(taskId,outCome);
				//保存执行过程
				processCourse.getProcessCourseTool().saveTaskCourse(mainPiId, processCourse.getProjectId(), taskId, copyedTaskDesc.getPrjTaskId(), outCome);
				//修改任务状态
				processCourse.getProcessCourseTool().changePrjTaskState(copyedTaskDesc.getPrjTaskId(), ProjectState.STATE_COMPLETED);
				flag = true;
			}
		}catch (Exception e) {
			e.printStackTrace();
		}
		return flag;
	}

	/**
	 *@Function Name:  getNextTaskByPiId
	 *@Description: @param hisPiId
	 *@Description: @param processEngine
	 *@Description: @return 得到下一个任务
	 *@Date Created:  2013-7-24 下午02:48:15
	 *@Author:  Pan Duan Duan
	 *@Last Modified:     ,  Date Modified: 
	 */
	public void getNextTaskByPiId(String mainPiId,  List<Task> tasks, ProcessInstanceTool processInstanceTool) {
		
		ProcessEngine processEngine = processInstanceTool.getProcessEngine();
		//任务Serveice
		TaskService taskService = processEngine.getTaskService();
		//流程实例Service
		ExecutionService executionService = processEngine.getExecutionService();
		//HistoryService
		HistoryService historyService = processEngine.getHistoryService();
		RepositoryService repositoryService = processEngine.getRepositoryService();
		//得到流程实例
		Execution excution = executionService.findExecutionById(mainPiId);
		//寻找主流程当前任务
		List<Task> taskList = taskService.createTaskQuery().processInstanceId(mainPiId).list();
		if(null != taskList && taskList.size() > 0)
		{
			tasks.addAll(taskList);
		}
		//尋找 分支 当前任务
		Collection<? extends Execution> executions = excution.getExecutions();
		if(executions.size() > 0)
		{
			for(Execution branchExecution : executions){
				getNextTaskByPiId(branchExecution.getId(), tasks, processInstanceTool);
			}
		}
		//寻找子流程
		String subExecutionId = null;
		ExecutionImpl executionImpl = (ExecutionImpl) excution;
		//得到activityName
		String activityName = executionImpl.getActivityName();
		if(null != activityName){
			//获取主线流程
			String mainBranchExecutionId = processInstanceTool.getProcessInstanceService().getMainPiIdByBranchPiId(excution.getId());
			//根据当前流程 得到所有父子流程集合
			List<Map<String,Object>> processInfos = processInstanceTool.getProcessInstanceIds(mainBranchExecutionId);
			for(Map<String,Object> processInfo : processInfos){
				String piId = CommonTools.Obj2String(processInfo.get("PIID"));
				//得到流程实例
				HistoryProcessInstance hisPi = historyService.createHistoryProcessInstanceQuery().processInstanceId(piId).uniqueResult();
				if(null != hisPi){
					//得到流程定义
					ProcessDefinition pd = repositoryService.createProcessDefinitionQuery().processDefinitionId(hisPi.getProcessDefinitionId()).uniqueResult();
					//得到流程定义名称
					String pdName = pd.getName();
					//得到流程定义
					if(activityName.equals(pdName)){
						subExecutionId = piId;
						break;
					}
				}
			}
			if(null != subExecutionId){
				getNextTaskByPiId(subExecutionId, tasks, processInstanceTool);
			}
		}
	}

}


public class GetLastVersionTaskDescOperation extends EDMAtomicOperation{

	Logger	log	= Logger.getLogger(this.getClass().getName());
	//serialVersionUID is
	
	private static final long serialVersionUID = 1665471796593388193L;

	@Override
	public void perform(ProcessCourserImpl processCourse) {
		log.warn("Copy Process Step 4: get the last version flow task description");
		//获取上个版本的 流程实例ID
		String lastVersionPiId = processCourse.getLastVersionPiId();
		if(!"".equals(lastVersionPiId)){
			List<EDMTaskDesc> taskDescs = getLastVersionTaskDesc(lastVersionPiId,processCourse);
			//组织责任链
			organizeTaskChain(taskDescs);
			processCourse.setTaskDescs(taskDescs);
		}
		processCourse.performAtomicOperation(EDMAtomicOperation.COPY_PROCESS);
	}

	/**
	 *@Function Name:  organizeTaskChain
	 *@Description: @param taskDesc 
	 *@Date Created:  2013-12-26 上午10:23:30
	 *@Author:  Pan Duan Duan 组织责任链
	 *@Last Modified:     ,  Date Modified: 
	 */
	private void organizeTaskChain(List<EDMTaskDesc> taskDescs) {
		int taskDescSize = taskDescs.size();
		if(taskDescSize > 0){
			//临时变量
			EDMTaskDesc tempTaskDesc = taskDescs.get(taskDescSize - 1);
			//遍历循环
			for(int index = taskDescSize - 2; index >= 0; index--){
				EDMTaskDesc taskDesc = taskDescs.get(index);
				taskDesc.setNextEDMTaskDesc(tempTaskDesc);
				tempTaskDesc = taskDesc;
			}
			
		}
		
	}

	/**
	 *@Function Name:  getLastVersionTaskDesc
	 *@Description: @param lastVersionPiId
	 *@Description: @param processCourse
	 *@Description: @return  获取上个版本的任务描述
	 *@Date Created:  2013-12-25 上午11:27:22
	 *@Author:  Pan Duan Duan
	 *@Last Modified:     ,  Date Modified: 
	 */
	private List<EDMTaskDesc> getLastVersionTaskDesc(
			String lastVersionPiId, ProcessCourserImpl processCourse) {
		
		List<EDMTaskDesc> retVal = new ArrayList<EDMTaskDesc>();
		ProcessInstanceTool processInstanceTool = processCourse.getProcessCourseTool().getProcessInstanceTool();
		ProcessEngine processEngine = processInstanceTool.getProcessEngine();
		//获取流程历史服务
		HistoryService historyService = processEngine.getHistoryService();
		//获取父子流程集合
		List<Map<String, Object>> processIdDescs = processCourse.getProcessCourseTool().getProcessInstanceTool().getProcessInstanceService().getProcessInstanceIds(lastVersionPiId);
		//遍历获取已完成任务集合
		Set<String> processInstanceIds = new HashSet<String>();
		//加入主流程
		processInstanceIds.add(lastVersionPiId);
		for(Map<String, Object> processDescMap : processIdDescs){
			//得到流程ID
			String piId = CommonTools.Obj2String(processDescMap.get("PIID"));
			processInstanceIds.add(piId);
		}
		for(String piId : processInstanceIds){
			//得到历史流程
			HistoryProcessInstance historyProcessInstance = historyService.createHistoryProcessInstanceQuery().processInstanceId(piId).uniqueResult();
			//得到历史流程定义
			String hisPdId = historyProcessInstance.getProcessDefinitionId();
			//获取任务表单描述
			XmlFormReader formReader = processEngine.execute(new GetFormReaderCommand(hisPdId));
			List<HistoryActivityInstance> historyActivityInstances = historyService.createHistoryActivityInstanceQuery().processInstanceId(piId).list();;
			//遍历任务集合
			for(HistoryActivityInstance hisAci : historyActivityInstances){
				//过滤掉正在进行的 以及非正常任务节点
				if(hisAci.getEndTime() == null || ((HistoryActivityInstanceImpl)hisAci).getType().equals("sub-process")){
					continue;
				}
				//强转为实现类
				HistoryActivityInstanceImpl hisAcImpl = (HistoryActivityInstanceImpl) hisAci;
				XmlForm xmlForm = formReader.getFormByName(hisAcImpl.getActivityName());
				//获取历史任务ID
				String taskId = processInstanceTool.getTaskIdByHaiImplId(String.valueOf(hisAcImpl.getDbid()));
				//获取历史变量值
				String hisVariId = CommonTools.Obj2String(historyService.getVariable(piId, taskId));
				//存储
				EDMTaskDesc edmTaskDesc = new EDMTaskDesc();
				edmTaskDesc.setHisTaskId(taskId);
				edmTaskDesc.setVariableId(hisVariId);
				edmTaskDesc.setPrjTaskId(xmlForm == null ? "" : xmlForm.getId());
				retVal.add(edmTaskDesc);
			}
		}
		//增加outCome并排序
		retVal = appendOutCome(retVal,processCourse);
		return retVal;
	}

	/**
	 *@Function Name:  appendOutCome
	 *@Description: @param retVal
	 *@Description: @return 增加任务流向 以及 排序
	 *@Date Created:  2013-12-26 上午08:23:06
	 *@Author:  Pan Duan Duan
	 *@Last Modified:     ,  Date Modified: 
	 */
	/**
	 *@param processCourse 
	 * @Function Name:  appendOutCome
	 *@Description: @param source
	 *@Description: @return 
	 *@Date Created:  2013-12-26 上午08:27:16
	 *@Author:  Pan Duan Duan
	 *@Last Modified:     ,  Date Modified: 
	 */
	private List<EDMTaskDesc> appendOutCome(List<EDMTaskDesc> source, ProcessCourserImpl processCourse) {
		//返回结果
		List<EDMTaskDesc> desc = new ArrayList<EDMTaskDesc>();
		//获取流程执行过程
		StringBuffer querySql = new StringBuffer();
		querySql.append("SELECT ID,MAIN_PIID,PROJECT_ID,FLOW_TASK_ID,PRJ_TASK_ID,OUTCOME FROM ");
		querySql.append("JBPM4_EDM_COURSE WHERE MAIN_PIID = ? AND PROJECT_ID = ? ORDER BY ID");
		//获取查询结果
		List<Map<String,Object>> queryList = processCourse.getProcessCourseTool().getProcessInstanceTool().getMetaDaoFactory().getJdbcTemplate().
													queryForList(querySql.toString(),new Object[]{processCourse.getLastVersionPiId(),processCourse.getProjectId()});
		//遍历组织
		for(Map<String,Object> dataMap : queryList){
			//流程任务ID
			String flowTaskId = CommonTools.Obj2String(dataMap.get("FLOW_TASK_ID"));
			//任务流向
			String outCome = CommonTools.Obj2String(dataMap.get("OUTCOME"));
			//从sources中寻找
			for(EDMTaskDesc tempTaskDesc : source){
				if(flowTaskId.equals(tempTaskDesc.getHisTaskId())){
					tempTaskDesc.setOutCome(outCome);
					desc.add(tempTaskDesc);
					break;
				}
			}
		}
		return desc;
	}

  
分享到:
评论

相关推荐

    MyEclipse6.0下Jbpm流程设计器

    【MyEclipse6.0下Jbpm流程设计器】是一个专为MyEclipse6.0集成的业务流程管理(Business Process Management,BPM)工具,主要用于设计和管理Jbpm流程。Jbpm是一个开源的工作流和业务流程管理平台,它提供了一套完整...

    经典的配置好的jbpm实例

    jbpm,全称为Java Business Process Management,是一款开源的工作流管理系统,用于实现业务流程自动化和管理。这个经典的jbpm实例提供了一个已经配置好的环境,可以直接在Tomcat应用服务器上部署,并能在MyEclipse...

    jbpm开发实例详解

    在jbpm的开发中,你会遇到流程定义、流程实例的启动和执行、任务的分配和完成、流程变量的管理和查询等功能。例如,你可以使用jPDL定义一个请假流程,其中包含申请、审批和销假等步骤。流程的执行可以通过jbpm提供的...

    jboss jbpm 5 developer guide

    5. 编程与实现:详细讨论如何通过Java代码与jBPM5交互,包括流程的启动、控制、监控和持久化等。 6. 集成与扩展:探索jBPM5与企业服务总线(Enterprise Service Bus,简称ESB)、数据持久层和第三方系统的集成方案...

    自己写的jbpm学习资料

    - 将jbpm-designer/jbpm-gpd-feature/eclipse下的plugins和features目录内容复制到Eclipse相应位置,实现插件集成。 - 在Web工程的src目录下新建process definition,启动图形用户界面设计器,进行流程设计。 #### ...

    ssh+jbpm整合.doc

    通过以上步骤,可以有效地将JBPM工作流框架与SSH框架进行整合,不仅解决了数据库Session不一致的问题,还实现了大字段的有效处理,并最终实现了流程的Web部署。这种整合方式不仅可以提高系统的灵活性和可维护性,还...

    JBPM5 插件安装

    在JBPM5中,插件的安装是扩展系统功能的重要手段,能够帮助用户实现特定的需求,如监控、报表、集成等。本文将详细讲解JBPM5插件的安装过程。 首先,我们需要理解JBPM5的核心组件。JBPM5主要包括工作流引擎、工作流...

    jbpm会签.zip

    通过以上知识点的应用,jbpm能够有效地实现会签功能,满足企业对于多角色协作审批的需求,提高业务流程的效率。在实际开发中,开发者需要深入理解这些概念,并结合具体业务场景进行适配和优化。

    jbpm4入门例子

    此外,jbPM4还提供了强大的监控和报表功能,帮助管理员和开发者跟踪流程的执行情况,找出潜在的问题,优化流程效率。你可以查看流程实例的统计信息、任务状态、事件日志等,从而更好地理解和控制业务流程。 总结来...

    jbpm-4.3-src.rar_bdf-jbpm4 src _jboss 4.3_jboss jbpm4.3_jbpm-4.3

    5. **源码分析**:通过查看jbpm-4.3-src中的源码,开发者可以深入了解jBPM的工作原理,学习如何实现复杂的流程控制逻辑,以及如何自定义任务服务、事件处理和规则引擎等核心组件。 6. **学习路径**:对于初学者,...

    JBPM4.3使用说明.doc

    JBPM4.3 使用说明 JBPM4.3 是一个基于工作流程管理的系统,旨在帮助用户更好地管理和自动化...JBPM4.3 是一个功能强大且灵活的工作流程管理系统,提供了许多实用的功能和命令来帮助用户更好地管理和自动化业务流程。

    Spring-Jbpm-JSF-Example.pdf

    此项目通过一个简单的应用实例,演示了如何运行基本的工作流程,并在MySql数据库上实现数据存储。 ### 2. **技术栈** #### Spring Spring框架是Java平台上的一个开源应用程序框架,提供了一种全面的编程和配置...

    JBPM整合mysql小例子

    此外,还可以利用JBPM的监控功能,实时查看流程状态、任务列表和历史记录,以便进行问题排查和性能优化。 总结起来,"JBPM整合MySQL小例子"是一个演示如何将JBPM的工作流系统与MySQL数据库相结合的实际操作。通过这...

    jbpm4.3简单实例

    通过这个简单的实例,你可以对jbpm4.3有一个基本的认识,了解如何在实际项目中应用它来实现业务流程的自动化。同时,这也将帮助你理解工作流管理系统的基本原理和操作方式,为进一步学习和使用更复杂的jbpm功能打下...

    eclipse JBPM插件的安装图解 myeclipse JBPM插件的安装图解

    这些功能极大地提高了开发效率,让开发者能更专注于业务逻辑的实现,而不是繁琐的流程管理工作。 注意,JBPM插件的安装可能需要Eclipse或MyEclipse特定版本的兼容性,所以在安装前,请确保你的开发环境与JBPM版本相...

    jbpm ppt资料

    JBPM作为一种成熟的工作流管理系统,不仅可以帮助企业实现业务流程的自动化,还可以提供强大的监控和审计功能,从而为企业带来更高的运营效率和更好的业务控制能力。通过本文档的学习,开发者可以更好地理解和应用...

    Business Process Management with JBoss jBPM.pdf

    《Business Process Management with JBoss jBPM》是一本专为业务分析师撰写的实用指南,旨在帮助读者掌握如何利用JBoss jBPM这一强大的开源框架来设计、实现和优化业务流程。本书由Matt Cumberlidge撰写,并于2007...

Global site tag (gtag.js) - Google Analytics