Activiti 7快速入门
一直有一个想法,就是写一篇关于工作流入门的文章,希望能对你理解工作流有所帮助。
什么是工作流
代码地址:https://github.com/hardpbl/activiti-learn.git
在学习工作流的时候,我们先来看看工作流的定义。
工作流(Workflow),就是通过计算机对业务流程自动化执行管理。它主要解决的是“使在多个参与者之间按照某种预定义的规则自动进行传递文档、信息或任务的过程,从而实现某个预期的业务目标,或者促使此目标的实现”。
我们为什么使用工作流引擎
在学习工作流的过程中,我们肯定看到过这个模型:填写请假单->部门经理审批->分管领导审批->人事备案。然后就哔哩哔哩一大堆了。不知道大家有没有想过难道我们非要用工作流吗?我们自己也可以通过字段标识来实现这个审批效果,在业务表中加个字段,比如填写请假单用1标识,部门经理用2标识,分管领导用3标识,人事备案用4标识,好像看起来没啥问题,也实现了审批效果。可是一旦我们的流程出现了变化,这个时候我们就需要改动我们的代码了,这显然是不可取的,那么有没有专业的方式来实现工作流的管理呢?并且可以做到业务流程变化之后,我们的程序可以不用改变,如果可以实现这样的效果,那么我们的业务系统的适应能力就得到了极大提升。
你可能会好奇,为什么工作流引擎能实现业务流程改变,不用修改代码,流程还能自动推进?
其实这个实现的原理的也非常简单,我们先来说说为什么流程改变,不用修改代码:其实是这样的,我们的工作流引擎都实现了一个 规范,这个规范要求我们的流程管理与转态字段无关,始终都是读取业务流程图的下一个节点。当业务更新的时候我们只需要更新业务流程图就行了。这就实现了业务流程改变,不用修改代码。再来说说流程自动推进,这个原理就更简单了,就拿上面的请假模型来说,工作流引擎会用一张表来记录当前处在的节点。当填写完清单后肯定是要轮到部门经理来审批了,所以我们一旦我们完成了请假单填写那么这条记录将会被从这张表删除掉,并且会把下一个节点部门经理的信息插入到这张表中,当我们用部门经理的信息去这张表中查询的时候就能查出部门经理相关的审批的信息了,以此类推,这样层层递进,就实现了流程的自动递交了。
BPM
百度百科解释:
BPM,即业务流程管理,是一种以规范化的构造端到端的卓越业务流程为中心,以持续的提高组织业务绩效为目的的系统化方法,常见商业管理教育如EMBA、MBA等均将BPM包含在内。
BPMN
百度百科解释:
BPMN:业务流程建模与标注,包括这些图元如何组合成一个业务流程图(Business Process Diagram);讨论BPMN的各种的用途,包括以何种精度来影响一个流程图中的模型;BPMN作为一个标准的价值,以及BPMN未来发展的远景。可以理解为是对BPM的具体化,通过图形来描述整个业务管理流程。
Activiti使用流程
第一步: 引入相应jar包并初始化数据库
既然activiti是一个框架,那么我们肯定是需要引入对应的jar包坐标的,具体参考代码中的。
第二步: 通过工具绘画流程图
使用 activiti 流程建模工具(activity-designer)定义业务流程(.bpmn 文件) 。.bpmn 文件就是业务流程定义文件,通过 xml 定义业务流程。
第三步:流程定义部署
向 activiti 部署业务流程定义(.bpmn 文件),使用 activiti 提供的 api 向 activiti 中部署.bpmn 文件
第四步: 启动一个流程实例(ProcessInstance)
启动一个流程实例表示开始一次业务流程的运行,比如员工请假流程部署完成,如果张三要请假就可以启动一个流程实例,如果李四要请假也启动一个流程实例,两个流程的执行互相不影响,就好比定义一个 java 类,实例化两个对象一样,部署的流程就好比 java 类,启动一个流程实例就好比 new 一个 java 对象
第五步: 用户查询待办任务(Task)
因为现在系统的业务流程已经交给 activiti 管理,通过 activiti 就可以查询当前流程执行到哪了,当前用户需要办理什么任务了,这些 activiti帮我们管理了。实际上我们学习activiti也只是学习它的API怎么使用,因为很多功能activiti都已经封装好了,我们会调用就行了~
第六步: 用户办理任务
用户查询待办任务后,就可以办理某个任务,如果这个任务办理完成还需要其它用户办理,比如采购单创建后由部门经理审核,这个过程也是由 activiti 帮我们完成了,不需要我们在代码中硬编码指定下一个任务办理人了
第七步: 流程结束
当任务办理完成没有下一个任务/结点了,这个流程实例就完成了。
上面说了一大堆,下面还是实操吧,希望没把你说的更迷惑了O(∩_∩)O哈哈
环境准备:
activiti 7
jdk1.8
开发IDE:IDEA2019.1.1
mysql:5.7
具体jar包依赖,日志配置 参考代码中的
流程设计工具这里只介绍在IDEA中如何安装:
在idea的设置中选择插件然后搜索 actiBPM,点击安装然后重启idea就好了
如何检查是否安装成功
好了,万事具备,我们是时候来介绍下activiti的总体架构了
先上一张图
在上图中7个接口我们无需关注FormService和IdentityService,因为在activiti7中已经被删除了。
ProcessEngine工作流引擎 相当于一个门面接口,通过 ProcessEngineConfiguration 创建 processEngine
ProcessEngine 创建各个 service ,比如RuntimeService,RepositoryService,TaskService,HistoryService等
activiti.cfg.xml activiti 的引擎配置文件,包括:ProcessEngineConfiguration 的定义、数据源定义、事务管理器等。ProcessEngineConfiguration 流程引擎的配置类,通过 ProcessEngineConfiguration 可以创建工作流引擎 ProceccEngine。
还记得上面说个activiti就是一个高度封装的框架嘛?我们学习activiti就是学习它的API,现在我们来介绍下几个它的常用接口
RepositoryService
是 activiti 的资源管理类,提供了管理和控制流程发布包和流程定义的操作。使用工作流建模工具设计的业务流程图需要使用此 service 将流程定义文件的内容部署到计算机。除了部署流程定义以外还可以:查询引擎中的发布包和流程定义。暂停或激活发布包,对应全部和特定流程定义。
RuntimeService
它是 activiti 的流程运行管理类。可以从这个服务类中获取很多关于流程执行相关的信息
TaskService
是 activiti 的任务管理类。可以从这个类中获取任务的信息。
HistoryService
是 activiti 的历史管理类,可以查询历史信息,执行流程时,引擎会保存很多数据(根据配置),比如流程实例启动时间,任务的参与者, 完成任务的时间,每个流程实例的执行路径,等等。 这个服务主要通过查询功能来获得这些数据
ManagementService
是 activiti 的引擎管理类,提供了对 Activiti 流程引擎的管理和维护功能,这些功能不在工作流驱动的应用程序中使用,主要用于 Activiti 系统的日常维护。(主要是了解下,一般用不到)
又说了一大堆,我们还是用代码说话吧
第一步:引入jar包,配置文件,各种巴拉巴拉,最后是这样的:(bpmn用来存放bpmn文件)
第二步: 绘制流程图
resources下的bpmn文件夹鼠标右键 new->BPMN File
](https://upload-images.jianshu.io/upload_images/16433761-7c2778fed26768e1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
点击OK后会出现如下界面
好了,我们来绘制第一个流程图,直接把右边的区域拖到中间这个流程图绘制区域就行,最后得到这样的流程图
指定流程定义key
如下图中标出来的就是指定了流程定义key为holiday
](https://upload-images.jianshu.io/upload_images/16433761-d9b4a0eb25a627e4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
指定流程负责人(手动指定人员中文可能会出现乱码)
这里假如我们想把填写请假单分派给张三,就可以这样设置,具体可以参考代码里面的
这里有两个小技巧,两个图之间连线需要把鼠标移动到正中间,再去连接另一个图片,如果想改变线的形状,可以把鼠标点一下这根连线,拖动下图中标识的位置就可以改变连线的形状了
最后成果图参考代码里面bpmn文件夹下holiday.bpmn文件
第三步:定义流程部署
首先我们需要在我们的mysql数据库中建立一个名为activiti的数据库(名称随意,只是我的配置文件中写的这个)
执行basic包下的InitActiviti类的main方法,再次用Navicat打开activiti数据库,是不是发现多了很多张表,
哈哈,是不是想数一下有多少张表呢?不用数了,一共是25张表。activiti7是25张,其他版本的有23张的也有26张的
现在我们来部署流程,执行basic下的DeployProcess的main方法就可以完成流程部署,执行完后我们来观察下数据库表结构变化
act_re_deployment 流程定义部署表,记录流程部署信息
act_re_procdef 流程定义表,记录流程定义信息
其中的KEY_就是我们在上面指定的唯一流程定义key holiday
第四步:开启流程实例
执行basic下面的StartInstance,即可开启流程实例
public static void main(String[] args) {
//得到processEngine对象
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
//得到RuntimeService方法
RuntimeService runtimeService = processEngine.getRuntimeService();
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("holiday");
System.out.println(processInstance.getId());
System.out.println(processInstance.getDeploymentId());
System.out.println(processInstance.getActivityId());
}
注意哦:holiday就是上面定义的唯一流程key
那执行开启流程又影响了那些表呢
act_ru_execution #流程实例执行表,记录当前流程实例的执行情况
act_ru_task 任务执行表,记录当前执行的任务。可以看见endtime字段是没有值的哦,因为我们还没有执行流程审批完成
请看 ASSIGNEE_字段,你看是不是你自己在流程图定义的时候分配的人,神奇吧
act_ru_identitylink #任务参与者,记录当前参与任务的用户或组
act_ru_execution 流程实例执行表,记录当前流程实例的执行情况
act_hi_actinst 活动历史表,记录所有活动
记录了任务,还记录了流程执行过程的其它活动,比如开始事件、
结束事件
act_hi_identitylink 任务参与者,记录历史参与任务的用户或组
act_hi_procinst 流程实例历史表
流程实例启动,会在此表插入一条记录,流程实例运行完成记录也不会删除。
act_hi_taskinst 任务历史表,记录所有任务
开始一个任务,不仅在 act_ru_task 表插入记录,也会在历史任务表插入一条记录,任务历史表的主键就是任务 id,任务完成此表记录不删除。
第五步:用户查询待办任务
比如我们查询zhangsan的待办任务,basic包下的QueryProcessList
public static void main(String[] args) {
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = processEngine.getTaskService();
List<Task> list = taskService.createTaskQuery().processDefinitionKey("holiday").taskAssignee("zhangsan").list();
for (Task task : list){
System.out.println("流程实例ID:"+task.getProcessDefinitionId());
System.out.println("任务ID:"+task.getId());
System.out.println("任务负责人:"+task.getAssignee());
System.out.println("任务名称:"+task.getName());
}}
第六步:处理待办任务
basic下的CompleteTask类,其中任务 id的值来自于上一步查询用户待办任务的任务ID
将id替换为2505,这样我们就完成了zhangsan的待办任务处理,将第五步的zhangsan替换为lisi,我们就能得到lisi的任务id,完成lisi的任务审批,再然后wangwu的就不用说了吧。
//任务id
String id = "2505";ProcessEngine
processEngine = ProcessEngines.getDefaultProcessEngine();
TaskService taskService = processEngine.getTaskService();taskService.complete(id);System.out.println("完成任务id:" + id);
这里主要的表变化情况是处理人为zhangsan的任务endtime都有了值,并且identitylink这类的表会插入下一个节点的参与者,如我们的部门经理审批节点是lisi,那么就会把审批人lisi的信息插入。这里有一张表需要注意,act_ru_task会删掉已经完成的填写请假单操作,并且插入了部门经理审批的相关信息,也验证了我们开始说的自动递交的原理。
第七步:结束任务
重复执行第五步和第六步,完成部门经理审批和总经理审批,这时流程就会自动结束,所有act_ru_* 表的数据都会被清空掉,在act_hi_actinst 会存储我们的审批节点操作
小小总结下:
问问工作流是啥清楚了吗?
.bpmn图实际上只是xml文件,并且已经存储到了activiti数据库中的act_ge_bytearray
使用工作流的七步是怎样的你清楚了吗?
至此,相信你对工作流有个基础认知了,现在我们来看看工作流的高级部分
流程实例
什么是流程实例
在java类中,我们可以生产无数个对象出来,而基于bpmn流程图为模板张三也可以来执行这个请假流程,李四也可以来执行请求流程,每一个具体个体执行的请假流程就是一个流程实例。
如何启动流程实例
还有印象不,前面我们已经说过了~,不记得的去前面看看吧。启动工作流的过程也可以做很多其他的工作的,如初始化审批人(上面的审批人是写死的,初始化业务流程key 也就是businessKey。这是啥玩意?O(∩_∩)O哈哈~下面介绍)
BusinessKey
这个 翻译为业务标识,通过前面的填写请假单->部门经理审批->总经理审批的请假模型不知道大家意识到没,虽然我们通过zhangsan来填写请假单最后也审批通过了。可是我们的部门经理审批的时候只是知道自己有一个任务需要审批,并不知道审批的具体事项是什么,这显然是与我们的现实需求是不一致。你让别人审批,你起码得让人知道审批的是什么,比如你请假的时间,天数,请假的事由等等。而这些数据是不是需要一张业务表来存储呢,所以我们的工作流需要存储这个业务表的主键,这样我们才能审批的时候知道我们审批的具体事项是啥,也更符合业务需要。
既然我们已经知道了businesskey是啥意思了,那我们如何把工作流和我们的业务标识关联起来呢?
还记得前面说的流程实例概念嘛,我们可以在启动流程实例的时候将我们的业务id主键也就是businesskey传入进来,并且我们在后面流程中都能取到这个businesskey,这样我们就能查看我们的审批事由啦~
我们实际操作一把
demo2包下,注意先把activiti数据库所有的表清除掉哦,这样会清晰些。先执行初始化,InitDemo类的main方法,在执行部署流程的DeployProcess的main方法。然后执行开启流程实例的StartInstance。注意看这行代码。你看,那个"10"的位置就是我们填入businesskey的位置,后面那个参数我们先不管
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("holidayEUL","10",variables);
这是我们再来看我们的act_ru_execution,businesskey已经存进去啦~
如果我们要得到businesskey的姿势请看demo2包下的GetBusinessKey
流程实例挂起与激活
操作流程定义为挂起状态,该流程定义下边所有的流程实例全部暂停:流程定义为挂起状态该流程定义将不允许启动新的流程实例,同时该流程定义下所有的流程实例将全部挂起暂停执行。具体代码参考demo2包下的SuspendOrActivateProcessInstance类
个人任务
分配任务负责人
一共三种方式:
(一)固定分配
在 properties 视图中,填写 Assignee 项为任务负责人,比如我们开始的填写请假单就分配给了zhangsan
注意:由于固定分配方式,任务只管一步一步执行任务,执行到每一个任务将按照 bpmn 的配置去分配任务负责人。
(二)表达式分配
我们先来学习下啥事UEL表达式
UEL 是 java EE6 规范的一部分,UEL(Unified Expression Language)即统一表达式语言,activiti 支持两个 UEL 表达式:UEL-value 和 UEL-method。
UEL-value 一般是这样的:
{bean.属性}。是的,如果你学过el表达式,就会发现这玩意和el表达式用法差不多,所以上手难度为~你觉得呢?
UEL-method 一般是这样的:
${bean.getXX()}
表达式支持解析基础类型、bean、list、array 和 map,也可作为条件判断。
如下:
${order.price > 100 && order.price < 250}
是不是很强大,这样我们就能实现动态的设置流程审批人了,而不是固定的审批人了。
我们来实际体验一波吧~
还是上次的步骤,先删掉activiti下所有的表,然后执行InitDemo的main方法,再执行部署bpmn图的DeployProcess,再执行DeployProcess的main方法,然后在开启流程的类中进行流程变量的赋值。还记得我们上面说variables先不管嘛,那个参数就是设置我们的流程变量用的。
public static void main(String[] args) {
//得到processEngine对象
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
//得到RuntimeService方法
RuntimeService runtimeService = processEngine.getRuntimeService();
//流程启动的时候设置流程变量
//定义流程变量
Map<String, Object> variables = new HashMap<String, Object>();
//设置流程变量 原理是因为startProcessInstanceByKey存在重载方法
variables.put("employee", "张三");
variables.put("deptManager","李四");
variables.put("generalManager","王五");
//第一个参数流程图key,第二个参数businesskey,第三个参数流程变量
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("holidayEUL","10",variables);
System.out.println(processInstance.getId());
System.out.println(processInstance.getDeploymentId());
System.out.println(processInstance.getActivityId());
}
执行完后,再去看看act_ru_task这张表,是不是惊奇的发现assign字段已经分派给张三了,然后可以在FinishPersonTask这个类中体验一把按照我们审批节点参数一一完成自己的审批任务(注意参数的替换,一次是张三,李四,王五,与你设置的值有关,以及定义的流程唯一key),再次感受下工作流的魅力。
(三)监听器分配
任务监听器是发生对应的任务相关事件时执行自定义 java 逻辑 或表达式。
任务相当事件包括
Create:任务创建后触发
Assignment:任务分配后触发
Delete:任务完成后触发
All:所有事件发生都触发
定义任务监听类,且类必须实现 org.activiti.engine.delegate.TaskListener 接口
public class MyTaskListener implements TaskListener {
@Override
public void notify(DelegateTask delegateTask) {
//这里指定任务负责人
delegateTask.setAssignee("张三");
}
}
具体效果可以自行体验一波,listen包下,建议删表后测试~
流程变量
流程变量在 activiti 中是一个非常重要的角色,流程运转有时需要靠流程变量,业务系统和 activiti结合时少不了流程变量,流程变量就是 activiti 在管理工作流时根据管理需要而设置的变量。比如在请假流程流转时如果请假天数大于 3 天则由总经理审核,否则由人事直接审核,请假天数就可以设置为流程变量,在流程流转时使用。
流程变量支持的类型
String,Integer,short,long,double, boolean,date,binary,Serializable
如果将 pojo 存储到流程变量中,必须实现序列化接口 serializable,为了防止由于新增字段无法反序列化,需要生成 serialVersionUID。
流程变量分为两种:global变量和local变量
global变量:在所有流程实例中都可使用
local变量:某一个流程实例中使用
好像这样说有点抽象,比如我们填写请假单的${employee}这个流程变量,当processEngine.getTaskService().setVariablesLocal(task.getId(),map);此时它就是局部变量,当
processEngine.getTaskService().setVariables(task.getId(),map);他就是全局变量
流程变量通常在哪使用呢?
通过 UEL 表达式使用流程变量
可以在 assignee 处设置 UEL 表达式,表达式的值为任务的负责人比如:${assignee},assignee 就是一个流程变量名称Activiti 获取 UEL 表达式的值 ,即流程变量 assignee 的值 ,将 assignee 的值作为任务的负责人进行任务分配
可以在连线上设置 UEL 表达式,决定流程走向
比如:{price<10000}: price 就是一个流程变量名称,uel 表达式结果类型为布尔类型。如果 UEL 表达式是 true,那么就会往表达式为true的线上走。
还有印象吗?在我们启动流程实例的时候设置了审批人。对,我们可以在启动流程实例的时候设置流程变量的值,总结如下:
全局流程变量的设置时机
启动流程的时候设置
// 启动流程时设置流程变量
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(processDefinitionKey, variables);
任务办理是设置
//变量名是holiday,变量值是holiday对象
variables.put("holiday", holiday);
taskService.complete(taskId, variables);
通过当前流程实例设置
//第一个参数流程实例执行id,第二个流程实例定义key,第三个object类型
runtimeService.setVariable(executionId, "holiday", holiday);
通过当前任务设置
/第一个参数任务id,第二个流程实例定义key,第三个object类型
taskService.setVariable(taskId, "holiday", holiday);
局部流程变量时机
与全局流程变量基本一致,注意把setVariables改为setVariablesLocal即可
流程实例与之相关的表
act_ru_variable #当前流程变量表
act_hi_varinst #历史流程变量表
具体参考 variable包下的例子
组任务
什么是组任务呢?通过上面一系列的介绍,不知道你们发现没,我们所有的任务都只有一个人在审批,假如我们这个同事他因为某些事情无法审批了怎么办呢?就是针对这种情况就有了组任务的概念,简单说就是一个任务可以有多个人来执行,这多个任务处理人称为候选人。不过不可能一个任务能被多个人审批,所以有了拾取任务的概念。任务被一个人拾 取后(其他人不能再去拾取),然后就可以去选择完成任务或者是转交任务。具体参考condidateuser包下的代码
如图中的部门经理审批就被设置了xiaowang和xiaozhao都有权利处理,中间注意逗号分开
组任务办理流程:
第一步:查询组任务
指定候选人,查询该候选人当前的待办任务。
此时候选人不能办理任务。
第二步:拾取(claim)任务
该组任务的所有候选人都能拾取。
将候选人的组任务,变成个人任务。原来候选人就变成了该任务的负责人。
如果拾取后不想办理该任务?
需要将已经拾取的个人任务归还到组里边,将个人任务变成了组任务。
第三步:查询个人任务
查询方式同个人任务部分,根据 assignee 查询用户负责的个人任务。
第四步:办理个人任务
可以执行办理,转交,退还等操作,具体查看condidateuser下的包
总结,实际上组任务的处理很简单,当候选人没有拾取任务的时候,此时act_ru_task里面的assign字段是空的,当某一个候选人拾取任务实际上只是对assign字段进行了赋值,退还组任务也就是从新将这个字段设置为null,而转交不过是将assign从一个处理人设置为另一个候选人。
网关:
网关部分代码我删掉了,大家自己试试吧~体验下工作流流程。
排他网关
排他网关(也叫异或(XOR)网关,或叫基于数据的排他网关),用来在流程中实现决策。 当流程执行到这个网关,所有分支都会判断条件是否为 true,如果为 true 则执行该分支,注意,排他网关只会选择一个为 true 的分支执行。(即使有两个分支条件都为 true,排他网关也会只选择一条分支去执行。
排他网关的好处:还记得上面我们说的流程变量作为判断条件吗,这里如果把排他网关去掉,看起来好像也行的样子,流程也能走下去。可是假如${holiday.day<=1},而请假天数是2呢,这时候不用排他网关将会使流程异常结束,而使用排他网关将会抛出异常,可以自行测试哦~
并行网关
并行网关允许将流程分成多条分支,也可以把多条分支汇聚到一起,并行网关的功能是基于进入和外出顺序流的:
fork 分支:
并行后的所有外出顺序流,为每个顺序流都创建一个并发分支。
join 汇聚:
所有到达并行网关,在此等待的进入分支, 直到所有进入顺序流的分支都到达以后, 流程就会通过汇聚网关。注意,如果同一个并行网关有多个进入和多个外出顺序流, 它就同时具有分支和汇聚功能。 这时,网关会先汇聚所有进入的顺序流,然后再切分成多个并行分支。与其他网关的主要区别是,并行网关不会解析条件。 即使顺序流中定义了条件,也会被忽略.
简单来说,就是一个可以通过并行网关流出两个,两个可以通过并行网关汇聚为一个。如下图
包含网关
包含网关可以看做是排他网关和并行网关的结合体。 和排他网关一样,你可以在外出顺序流上定义条件,包含网关会解析它们。 但是主要的区别是包含网关可以选择多于一条顺序流,这和并行网关一样
包含网关的功能是基于进入和外出顺序流的:
分支:
所有外出顺序流的条件都会被解析,结果为 true 的顺序流会以并行方式继续执行, 会为每个顺序流创建一个分支。
汇聚:
所有并行分支到达包含网关,会进入等待状态, 直到其他顺序流的分支都到达。 这是与并行网关的最大不同。换句话说,包含网关只会等待被选中执行了的进入顺序流。 在汇聚之后,流程会穿过包含网关继续执行。
userType假设是员工类型,1.是普通员工 2.是领导
写在最后,实际上这个只是工作流的入门知识,还有比如像其他的驳回,会签,表单也就是formkey等等其他骚操作都没说了,因为实在太多了。不过我觉得理解了基础的东西,知道了工作流25张表的各自用处,其他的无非也就那样。以上参考了一些博客,还有博学谷的视频,以及官网,还买了本书名叫《疯狂工作流讲义》的书籍(没什么用)。如果这篇文章对你有所帮助是我的荣幸,感谢阅读_。