了解项目

芋道源码是基于ruoyi这个开源项目做了很多增强,里面加了很多功能,比如接下来要学习的工作流,还有sass多租户系统,商城系统,支付系统,微信小程序,流量监控等等很多功能模块;

学习链接:

ruoyi-vue-pro: 🔥 官方推荐 🔥 RuoYi-Vue 全新 Pro 版本,优化重构所有功能。基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 微信小程序,支持 RBAC 动态权限、数据权限、SaaS 多租户、Flowable 工作流、三方登录、支付、短信、商城等功能。你的 ⭐️ Star ⭐️,是作者生发的动力! (gitee.com)

项目大体结构 

这是一个基于SpringBoot的项目,但是因为项目的功能很多,就拆分成了不同的模块,从目录结构看起来比较像一个cloud项目

这里面的其他模块我们先不做研究,主要针对工作流这个模块进行研究;

项目启动和环境配置这里就不多做介绍了,有官方文档,可以参考看看,并且如何解开相对应的模块也有资源可以搜索;

我们研究的主要是工作流这个模块,先看项目构成

主要分为api和biz两个模块,biz主要就是写业务代码的模块,也是我们重点要关注的模块,而api模块就是一个接口,便于其他模块调用我们这个模块的功能,类似于cloud里面的远程调用,但是我们模块之间不需要发送请求,毕竟都是同一个端口,其实说白了,就跟我们之前boot项目controller调用service一样,只是又封装了一下,也是一种新的方式,可以学习;

项目模块简单说明

工作流这个模块主要的包结构:

我们熟悉的controller 和service就不多说了。
convert:其实就是转换,我们再写项目的时候有很多实体类需要互相转换,比如UserDO,UserDTO,UserVO,其中里面的属性名基本都一样,主要是一些需要的属性,我们就封装在一个类里面,所以我们需要经常转换,这换个包里面就有很全的转换方法;

dal:这个包其实就是我们entity,实体类的包,只是换了一个名字,就是跟数据库字段映射的类那个包;

framework:这个包就是对flowable这个框架在封装的一个包,后续用到回说;

了解工作流

简单定义

工作流(Workflow)指“业务过程的部分或整体在计算机应用环境下的自动化”,是对工作流程及其各操作步骤之间业务规则的抽象、概括描述。它主要解决的是为了实现某个业务目标,利用计算机在多个参与者之间按某种预定规则自动传递文档、信息或者任务的问题。工作流概念起源于生产组织和办公自动化领域,是针对日常工作中具有固定程序活动而提出的一个概念,目的是通过将工作分解成定义良好的任务或角色,按照一定的规则和过程来执行这些任务并对其进行监控,达到提高工作效率、更好的控制过程、增强对客户的服务、有效管理业务流程等目的。

百度搜的,看不懂没关系,通俗的说就是,我们有一个流程,现在我就想通过画一个流程图,然后系统自动帮我实现,我指定一些简单的规则,然后系统就自动帮我做了,举一个很简单的例子:请假,比如我要请假,我就需要给我的领导汇报,然后领导在给上一级汇报,一级一级的汇报,最终返回结果,这就是一个流程,当我们在系统里面指定了这么一个流程,系统就自动知道该怎么走流程了,话不多说,直接上图:

这就是一个简单的请假流程图,从上往下走,每一步,都对应这一个用户或者一组用户,然后通过就走到下一步,那么这个功能是如何实现的呢,我们是如何将这个流程图转换成代码的呢,让代码理解然后到下一步的呢?

工作流基本流程

因为我们是基于flowable完成的这个功能,其实上面的流程图就是flowable的一部分,我们先看看我们的系统是如何操作的,接下来在研究具体的代码实现:

我们先选择工作流程这个模块,点击流程管理,第一步,我们需要确定流程表单,那么,什么是流程表单呢?其实就是我这个流程需要传递信息的一个表单,也就是真正的用户需要填的东西,我们需要先设计好,比如我要请假,肯定要填请假表,这个请假表就是我们通过流程表单设计的;

大概就是这个样子:

创建完流程表单,我们需要做一个流程模型,也就是流程图,开始节点-》流程->结束节点,刚刚上面也展示过了,之后就是关键的步骤,我们只设计了模型,但是每一步都需要对应的规则,我们就需要分配规则了

分配规则的时候,系统会生成一个任务标识,其实就是后面我们可能要用到的任务定义的key,也相当于一个任务标识;

这样,一个简单的流程就完成了,流程模型中还有网关这一这种组件,相当于流程的分支,这里大家可以自行去了解;

当我们定义好了我们流程,点击发布流程,这个流程实例相当于就被应用了,然后我们用户就可以通过发起流程,选择对应的流程模型他,然后执行对应的流程了;

代码解析

表结构

flowable中的表结构都是以act_开头的,这是flowable自带的表结构,用来存储不同状态的,不同时期的流程,包含很多信息,通过这些信息,我们就可以知道我们的流程在哪一步,下一步是谁,怎么传过去;

这些表主要分为以下几大类:

1、 act_re_:re代表repository,带有这个前缀的表包含“静态”信息,例如流程定义与流程资源(图片、规则等)。

2、act_ru_:ru代表runtime,代表运行时的信息,例如流程实例(process instance)、用户任务(user task)、变量(variable)等。Flowable只在流程实例运行中保存运行时数据,并在流程实例结束时删除记录。这样保证运行时表小和快;

3、act_hi_: hi代表history。这些表存储历史数据,例如已完成的流程实例、变量、任务等。

4、act_ge_: ge代表“General”(通用),通用数据。在多处使用。

5、act_id_:id代表Identity,代表跟用户有关的表

这些表都是flowable自带的表,那么操作这些表,就有自带service来操作这些对应的表

1、通用数据表
  • act_ge_bytearray:二进制数据表,如流程定义、流程模板、流程图的字节流文件;
  • act_ge_property:属性数据表(不常用);
2、HistoryService接口
  • act_hi_actinst:历史节点表,存放流程实例运转的各个节点信息(包含开始、结束等非任务节点);
  • act_hi_attachment:历史附件表,存放历史节点上传的附件信息(不常用);
  • act_hi_comment:历史意见表;
  • act_hi_detail:历史详情表,存储节点运转的一些信息(不常用);
  • act_hi_identitylink:历史流程人员表,存储流程各节点候选、办理人员信息,常用于查询某人或部门的已办任务;
  • act_hi_procinst:历史流程实例表,存储流程实例历史数据(包含正在运行的流程实例);
  • act_hi_taskinst:历史流程任务表,存储历史任务节点;
  • act_hi_varinst:流程历史变量表,存储流程历史节点的变量信息;

3、IdentityService接口
  •  act_id_group:用户组信息表,对应节点选定候选组信息;
  • act_id_info:用户扩展信息表,存储用户扩展信息;
  • act_id_membership:用户与用户组关系表;
  • act_id_user:用户信息表,对应节点选定办理人或候选人信息;
4、RepositoryService接口
  • act_re_deployment:部署信息表,存储流程定义、模板部署信息;
  • act_re_procdef:流程定义信息表,存储流程定义相关描述信息,流程定义的bpmn文件放在act_ge_bytearray表中,以字节形式存储;
  • act_re_model:流程模板信息表,存储流程模板相关描述信息,流程定义的bpmn文件放在act_ge_bytearray表中,以字节形式存储;

5、RuntimeService,taskService接口
  • act_ru_task:运行时流程任务节点表,存储运行中流程的任务节点信息,重要,常用于查询人员或部门的待办任务时使用;
  • act_ru_event_subscr:监听信息表,不常用;
  • act_ru_execution:运行时流程执行实例表,记录运行中流程运行的各个分支信息(当没有子流程时,其数据与act_ru_task表数据是一一对应的);
  • act_ru_identitylink:运行时流程人员表,重要,常用于查询人员或部门的待办任务时使用;
  • act_ru_job:运行时定时任务数据表,存储流程的定时任务信息;
  • act_ru_variable:运行时流程变量数据表,存储运行中的流程各节点的变量信息;

自定义的表,除了这些表,这个项目还自定义了一些表,用来更详细的记录流程,最终实现更强大的功能:

这些bpm_开头的表就是对应的表,根据名称我们也可以猜到他们的用途,第一个form用来存储表单的表,第二个没什么用,做示例用的表,第三个是关于流程定义的表,第四个是流程实例的表,第五个是任务和规则的表,第六个是任务详情表,最后一个就是用户组的信息表;

有了这些表,我们可以更加详细清楚的知道我们的流程,对流程的操作也更加方便了。

代码实现

流程模型的定义

我们根据表单设计 - >流程定义 - > 分配规则 -> 发布流程 这个顺序来看我们的代码实现

首先是表单的设计:

先找到这个类,从controller开始看:

@Tag(name = "管理后台 - 动态表单")
@RestController
@RequestMapping("/bpm/form")
@Validated
public class BpmFormController {

    @Resource
    private BpmFormService formService;

    @PostMapping("/create")
    @Operation(summary = "创建动态表单")
    @PreAuthorize("@ss.hasPermission('bpm:form:create')")
    public CommonResult<Long> createForm(@Valid @RequestBody BpmFormCreateReqVO createReqVO) {
        return success(formService.createForm(createReqVO));
    }

    @PutMapping("/update")
    @Operation(summary = "更新动态表单")
    @PreAuthorize("@ss.hasPermission('bpm:form:update')")
    public CommonResult<Boolean> updateForm(@Valid @RequestBody BpmFormUpdateReqVO updateReqVO) {
        formService.updateForm(updateReqVO);
        return success(true);
    }

    @DeleteMapping("/delete")
    @Operation(summary = "删除动态表单")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('bpm:form:delete')")
    public CommonResult<Boolean> deleteForm(@RequestParam("id") Long id) {
        formService.deleteForm(id);
        return success(true);
    }

    @GetMapping("/get")
    @Operation(summary = "获得动态表单")
    @Parameter(name = "id", description = "编号", required = true, example = "1024")
    @PreAuthorize("@ss.hasPermission('bpm:form:query')")
    public CommonResult<BpmFormRespVO> getForm(@RequestParam("id") Long id) {
        BpmFormDO form = formService.getForm(id);
        return success(BpmFormConvert.INSTANCE.convert(form));
    }

    @GetMapping("/list-all-simple")
    @Operation(summary = "获得动态表单的精简列表", description = "用于表单下拉框")
    public CommonResult<List<BpmFormSimpleRespVO>> getSimpleForms() {
        List<BpmFormDO> list = formService.getFormList();
        return success(BpmFormConvert.INSTANCE.convertList2(list));
    }

    @GetMapping("/page")
    @Operation(summary = "获得动态表单分页")
    @PreAuthorize("@ss.hasPermission('bpm:form:query')")
    public CommonResult<PageResult<BpmFormRespVO>> getFormPage(@Valid BpmFormPageReqVO pageVO) {
        PageResult<BpmFormDO> pageResult = formService.getFormPage(pageVO);
        return success(BpmFormConvert.INSTANCE.convertPage(pageResult));
    }

}

这个类很简单,就是对表单的增删改查,我们随机看一个新增表单的方法:

 @Override
    public Long createForm(BpmFormCreateReqVO createReqVO) {
        this.checkFields(createReqVO.getFields());
        // 插入
        BpmFormDO form = BpmFormConvert.INSTANCE.convert(createReqVO);
        formMapper.insert(form);
        // 返回
        return form.getId();
    }

 基本上就是新增,给我的bpm_form新增表单数据,看一下数据库

将表单的名字,状态,还有定义表单的配置,还有表单的数组都详细的记录了下来,这样,我们每次使用这个模板表单的时候,从数据库中取出来在解析到前端就可以实现功能啦;

这个模块就是表单的增删改查,没什么可以看的,我们看下一部分

流程定义:

@Tag(name = "管理后台 - 流程定义")
@RestController
@RequestMapping("/bpm/process-definition")
@Validated
public class BpmProcessDefinitionController {

    @Resource
    private BpmProcessDefinitionService bpmDefinitionService;

    @GetMapping("/page")
    @Operation(summary = "获得流程定义分页")
    @PreAuthorize("@ss.hasPermission('bpm:process-definition:query')")
    public CommonResult<PageResult<BpmProcessDefinitionPageItemRespVO>> getProcessDefinitionPage(
            BpmProcessDefinitionPageReqVO pageReqVO) {
        return success(bpmDefinitionService.getProcessDefinitionPage(pageReqVO));
    }

    @GetMapping ("/list")
    @Operation(summary = "获得流程定义列表")
    @PreAuthorize("@ss.hasPermission('bpm:process-definition:query')")
    public CommonResult<List<BpmProcessDefinitionRespVO>> getProcessDefinitionList(
            BpmProcessDefinitionListReqVO listReqVO) {
        return success(bpmDefinitionService.getProcessDefinitionList(listReqVO));
    }

    @GetMapping ("/get-bpmn-xml")
    @Operation(summary = "获得流程定义的 BPMN XML")
    @Parameter(name = "id", description = "编号", required = true, example = "1024")
    @PreAuthorize("@ss.hasPermission('bpm:process-definition:query')")
    public CommonResult<String> getProcessDefinitionBpmnXML(@RequestParam("id") String id) {
        String bpmnXML = bpmDefinitionService.getProcessDefinitionBpmnXML(id);
        return success(bpmnXML);
    }
}

这个就是通过流程定义那个表,获取流程定义的分页,列表,最后一个方法,获得bpmn XML,这个很重要,这个就是我们当时画的那个流程图,flowable可以将这个流程图传换成xml,xml里面详细记录了开始节点,每个流程的名字,信息,还有指向,相当于把流程图抽象成了代码;

 <?xml version="1.0" encoding="UTF-8"?>
<bpmn2:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmn2="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="diagram_Process_1700643636351" targetNamespace="http://flowable.org/bpmn">
  <bpmn2:process id="a2" name="请假流程v2.0" isExecutable="true">
    <bpmn2:startEvent id="Event_0t5qczr" name="发起人">
      <bpmn2:outgoing>Flow_1im71th</bpmn2:outgoing>
    </bpmn2:startEvent>
    <bpmn2:userTask id="Activity_0glupqi" name="一级经理">
      <bpmn2:incoming>Flow_1im71th</bpmn2:incoming>
      <bpmn2:outgoing>Flow_1o0nrfc</bpmn2:outgoing>
    </bpmn2:userTask>
    <bpmn2:sequenceFlow id="Flow_1im71th" sourceRef="Event_0t5qczr" targetRef="Activity_0glupqi" />
    <bpmn2:userTask id="Activity_0ljg5dg" name="二级经理">
      <bpmn2:incoming>Flow_1o0nrfc</bpmn2:incoming>
      <bpmn2:outgoing>Flow_1x22gj5</bpmn2:outgoing>
    </bpmn2:userTask>
    <bpmn2:sequenceFlow id="Flow_1o0nrfc" sourceRef="Activity_0glupqi" targetRef="Activity_0ljg5dg" />
    <bpmn2:userTask id="Activity_1yn05de" name="HR部门经理">
      <bpmn2:incoming>Flow_1x22gj5</bpmn2:incoming>
      <bpmn2:outgoing>Flow_18cpo3n</bpmn2:outgoing>
    </bpmn2:userTask>
    <bpmn2:sequenceFlow id="Flow_1x22gj5" sourceRef="Activity_0ljg5dg" targetRef="Activity_1yn05de" />
    <bpmn2:userTask id="Activity_1jtam5i" name="副总经理">
      <bpmn2:incoming>Flow_18cpo3n</bpmn2:incoming>
      <bpmn2:outgoing>Flow_0gm567w</bpmn2:outgoing>
    </bpmn2:userTask>
    <bpmn2:sequenceFlow id="Flow_18cpo3n" sourceRef="Activity_1yn05de" targetRef="Activity_1jtam5i" />
    <bpmn2:userTask id="Activity_11bkpww" name="总经理">
      <bpmn2:incoming>Flow_0gm567w</bpmn2:incoming>
      <bpmn2:outgoing>Flow_0aq193e</bpmn2:outgoing>
    </bpmn2:userTask>
    <bpmn2:sequenceFlow id="Flow_0gm567w" sourceRef="Activity_1jtam5i" targetRef="Activity_11bkpww" />
    <bpmn2:endEvent id="Event_1km9hek">
      <bpmn2:incoming>Flow_0aq193e</bpmn2:incoming>
    </bpmn2:endEvent>
    <bpmn2:sequenceFlow id="Flow_0aq193e" sourceRef="Activity_11bkpww" targetRef="Event_1km9hek" />
  </bpmn2:process>
  <bpmndi:BPMNDiagram id="BPMNDiagram_1">
    <bpmndi:BPMNPlane id="a2_di" bpmnElement="a2">
      <bpmndi:BPMNEdge id="Flow_0aq193e_di" bpmnElement="Flow_0aq193e">
        <di:waypoint x="250" y="930" />
        <di:waypoint x="250" y="972" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_0gm567w_di" bpmnElement="Flow_0gm567w">
        <di:waypoint x="250" y="800" />
        <di:waypoint x="250" y="850" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_18cpo3n_di" bpmnElement="Flow_18cpo3n">
        <di:waypoint x="250" y="660" />
        <di:waypoint x="250" y="720" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_1x22gj5_di" bpmnElement="Flow_1x22gj5">
        <di:waypoint x="250" y="510" />
        <di:waypoint x="250" y="580" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_1o0nrfc_di" bpmnElement="Flow_1o0nrfc">
        <di:waypoint x="250" y="390" />
        <di:waypoint x="250" y="430" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge id="Flow_1im71th_di" bpmnElement="Flow_1im71th">
        <di:waypoint x="250" y="258" />
        <di:waypoint x="250" y="310" />
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNShape id="Event_0t5qczr_di" bpmnElement="Event_0t5qczr">
        <dc:Bounds x="232" y="222" width="36" height="36" />
        <bpmndi:BPMNLabel>
          <dc:Bounds x="233" y="192" width="34" height="14" />
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Activity_0glupqi_di" bpmnElement="Activity_0glupqi">
        <dc:Bounds x="200" y="310" width="100" height="80" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Activity_0ljg5dg_di" bpmnElement="Activity_0ljg5dg">
        <dc:Bounds x="200" y="430" width="100" height="80" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Activity_1yn05de_di" bpmnElement="Activity_1yn05de">
        <dc:Bounds x="200" y="580" width="100" height="80" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Activity_1jtam5i_di" bpmnElement="Activity_1jtam5i">
        <dc:Bounds x="200" y="720" width="100" height="80" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Activity_11bkpww_di" bpmnElement="Activity_11bkpww">
        <dc:Bounds x="200" y="850" width="100" height="80" />
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape id="Event_1km9hek_di" bpmnElement="Event_1km9hek">
        <dc:Bounds x="232" y="972" width="36" height="36" />
      </bpmndi:BPMNShape>
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</bpmn2:definitions>

 通过以上详细的xml我们就可以获取到我们流程图的所有信息,接下来该给我们的流程模型分配规则了:

还是以创建任务规则来举例子:

其中实现类的具体方法是:

  @Override
    public Long createTaskAssignRule(@Valid BpmTaskAssignRuleCreateReqVO reqVO) {
        // 校验参数
        validTaskAssignRuleOptions(reqVO.getType(), reqVO.getOptions());
        // 校验是否已经配置
        BpmTaskAssignRuleDO existRule =
                taskRuleMapper.selectListByModelIdAndTaskDefinitionKey(reqVO.getModelId(), reqVO.getTaskDefinitionKey());
        if (existRule != null) {
            throw exception(TASK_ASSIGN_RULE_EXISTS, reqVO.getModelId(), reqVO.getTaskDefinitionKey());
        }

        // 存储
        BpmTaskAssignRuleDO rule = BpmTaskAssignRuleConvert.INSTANCE.convert(reqVO)
                .setProcessDefinitionId(BpmTaskAssignRuleDO.PROCESS_DEFINITION_ID_NULL); // 只有流程模型,才允许新建
        taskRuleMapper.insert(rule);
        return rule.getId();
    }

通过前端传过来的规则,然后校验参数,判断这个流程是否已经设置过,如果没有设置过在存储到数据库中。

校验参数主要是需要校验规则的类型:比如是用户,或者岗位,不能出现我们没有定义的类型

private void validTaskAssignRuleOptions(Integer type, Set<Long> options) {
        if (Objects.equals(type, BpmTaskAssignRuleTypeEnum.ROLE.getType())) {
            roleApi.validRoleList(options);
        } else if (ObjectUtils.equalsAny(type, BpmTaskAssignRuleTypeEnum.DEPT_MEMBER.getType(),
                BpmTaskAssignRuleTypeEnum.DEPT_LEADER.getType())) {
            deptApi.validateDeptList(options);
        } else if (Objects.equals(type, BpmTaskAssignRuleTypeEnum.POST.getType())) {
            postApi.validPostList(options);
        } else if (Objects.equals(type, BpmTaskAssignRuleTypeEnum.USER.getType())) {
            adminUserApi.validateUserList(options);
        } else if (Objects.equals(type, BpmTaskAssignRuleTypeEnum.USER_GROUP.getType())) {
            userGroupService.validUserGroups(options);
        } else if (Objects.equals(type, BpmTaskAssignRuleTypeEnum.SCRIPT.getType())) {
            dictDataApi.validateDictDataList(DictTypeConstants.TASK_ASSIGN_SCRIPT,
                    CollectionUtils.convertSet(options, String::valueOf));
        } else {
            throw new IllegalArgumentException(format("未知的规则类型({})", type));
        }
    }

其中规则定义的类:

public class BpmTaskAssignRuleDO extends BaseDO {

    /**
     * {@link #processDefinitionId} 空串,用于标识属于流程模型,而不属于流程定义
     */
    public static final String PROCESS_DEFINITION_ID_NULL = "";

    /**
     * 编号
     */
    @TableId
    private Long id;

    /**
     * 流程模型编号
     *
     * 关联 Model 的 id 属性
     */
    private String modelId;
    /**
     * 流程定义编号
     *
     * 关联 ProcessDefinition 的 id 属性
     */
    private String processDefinitionId;
    /**
     * 流程任务的定义 Key
     *
     * 关联 Task 的 taskDefinitionKey 属性
     */
    private String taskDefinitionKey;

    /**
     * 规则类型
     *
     * 枚举 {@link BpmTaskAssignRuleTypeEnum}
     */
    @TableField("`type`")
    private Integer type;
    /**
     * 规则值数组,一般关联指定表的编号
     * 根据 type 不同,对应的值是不同的:
     *
     * 1. {@link BpmTaskAssignRuleTypeEnum#ROLE} 时:角色编号
     * 2. {@link BpmTaskAssignRuleTypeEnum#DEPT_MEMBER} 时:部门编号
     * 3. {@link BpmTaskAssignRuleTypeEnum#DEPT_LEADER} 时:部门编号
     * 4. {@link BpmTaskAssignRuleTypeEnum#USER} 时:用户编号
     * 5. {@link BpmTaskAssignRuleTypeEnum#USER_GROUP} 时:用户组编号
     * 6. {@link BpmTaskAssignRuleTypeEnum#SCRIPT} 时:脚本编号,目前通过 {@link BpmTaskRuleScriptEnum#getId()} 标识
     */
    @TableField(typeHandler = JsonLongSetTypeHandler.class)
    private Set<Long> options;

}

这样就能将对应的信息记录的数据库中。

接下来就是部署流程,也就是发布流程了:

@Override
    @Transactional(rollbackFor = Exception.class) // 因为进行多个操作,所以开启事务
    public void deployModel(String id) {
        // 1.1 校验流程模型存在
        Model model = repositoryService.getModel(id);
        if (ObjectUtils.isEmpty(model)) {
            throw exception(MODEL_NOT_EXISTS);
        }
        // 1.2 校验流程图
        // TODO 芋艿:校验流程图的有效性;例如说,是否有开始的元素,是否有结束的元素;
        byte[] bpmnBytes = repositoryService.getModelEditorSource(model.getId());
        if (bpmnBytes == null) {
            throw exception(MODEL_NOT_EXISTS);
        }
        // 1.3 校验表单已配
        BpmFormDO form = checkFormConfig(model.getMetaInfo());
        // 1.4 校验任务分配规则已配置
        taskAssignRuleService.checkTaskAssignRuleAllConfig(id);

        // 1.5 校验模型是否发生修改。如果未修改,则不允许创建
        BpmProcessDefinitionCreateReqDTO definitionCreateReqDTO = BpmModelConvert.INSTANCE.convert2(model, form).setBpmnBytes(bpmnBytes);
        if (processDefinitionService.isProcessDefinitionEquals(definitionCreateReqDTO)) { // 流程定义的信息相等
            ProcessDefinition oldProcessDefinition = processDefinitionService.getProcessDefinitionByDeploymentId(model.getDeploymentId());
            if (oldProcessDefinition != null && taskAssignRuleService.isTaskAssignRulesEquals(model.getId(), oldProcessDefinition.getId())) {
                throw exception(MODEL_DEPLOY_FAIL_TASK_INFO_EQUALS);
            }
        }

        // 2.1 创建流程定义
        String definitionId = processDefinitionService.createProcessDefinition(definitionCreateReqDTO);

        // 2.2 将老的流程定义进行挂起。也就是说,只有最新部署的流程定义,才可以发起任务。
        updateProcessDefinitionSuspended(model.getDeploymentId());

        // 2.3 更新 model 的 deploymentId,进行关联
        ProcessDefinition definition = processDefinitionService.getProcessDefinition(definitionId);
        model.setDeploymentId(definition.getDeploymentId());
        repositoryService.saveModel(model);

        // 2.4 复制任务分配规则
        taskAssignRuleService.copyTaskAssignRules(id, definition.getId());
    }

这里面的注释也很详细,发布流程本质就是新建一条记录并且启用,将原来的记录挂起,那么有问题了,为什么不直接更新原来那条记录呢?其实我觉得是,如果我们之前用的流程模型还有流程没走完,但是我们修改了流程模型的规则,那么这条之前没有做完的流程应该是之前的流程,如果我们直接更新那条流程数据,那么就达不到这个效果了,如果我们新建一条记录那么就没有问题了;

这里的方法,参数校验,还有考虑的方面也比较全面,先校验流程模型是否存在,在校验有没有分配表单,是否有规则分配,这些都是流程能正常工作的很必要的元素,如果没有这些,我们的业务就有问题,发布的流程模型肯定是要能用的,之后在校验跟之前的记录是否发生了改变,减少不必要的业务执行。

还有最后一步,调用了一个方法赋值任务的分配规则:

将流程流程模型的任务分配规则,复制一份给流程定义
 目的:每次流程模型部署时,都会生成一个新的流程定义,此时考虑到每次部署的流程不可变性,所以需要复制一份给该流程定义
    @Override
    public void copyTaskAssignRules(String fromModelId, String toProcessDefinitionId) {
        List<BpmTaskAssignRuleRespVO> rules = getTaskAssignRuleList(fromModelId, null);
        if (CollUtil.isEmpty(rules)) {
            return;
        }
        // 开始复制
        List<BpmTaskAssignRuleDO> newRules = BpmTaskAssignRuleConvert.INSTANCE.convertList2(rules);
        newRules.forEach(rule -> rule.setProcessDefinitionId(toProcessDefinitionId).setId(null).setCreateTime(null)
                .setUpdateTime(null));
        taskRuleMapper.insertBatch(newRules);
    }
 发起流程

接下来就该发起流程了

点击发起流程,然后选择已发布的流程模型,然后页面就会根据我们定义的模型找到对应的表单展示在前端,让用户填写:

下面也会根据我们存在数据库的bpmn XML 显示成流程图到前端:

具体接口方法很简单,就是查询对应表,先查表单数据,在查下拉框数据这种,我们看看获取bpmn 这个接口:

    @Override
    public String getProcessDefinitionBpmnXML(String id) {
        BpmnModel bpmnModel = repositoryService.getBpmnModel(id);
        if (bpmnModel == null) {
            return null;
        }
        BpmnXMLConverter converter = new BpmnXMLConverter();
        return StrUtil.utf8Str(converter.convertToXML(bpmnModel));
    }

也不是很难,就是对数据库的操作,只是,flowable将这个xml封装成了一个BpmnModel 对象,我们的代码就可以对这个对象操作就可以了,这里使用的repositoryService.getBpmnModel(id);这个方法就是flowable自带的service,操作模型定义类的表,太底层我也没有研究,毕竟小白;

当我们填好表单之后,点击发起流程,这个流程就启动了,发起人就是我们的当前用户:

    @PostMapping("/create")
    @Operation(summary = "新建流程实例")
    @PreAuthorize("@ss.hasPermission('bpm:process-instance:query')")
    public CommonResult<String> createProcessInstance(@Valid @RequestBody BpmProcessInstanceCreateReqVO createReqVO) {
        return success(processInstanceService.createProcessInstance(getLoginUserId(), createReqVO));
    }

 这里面用到的service也是flowable自带的service,通过调用创建实例的方法,将我们的实例信息存储到对应的表中;

接下来我们就可以看到我们的流程开始,已经指向了下一个节点

然后,对应的审批人就应该执行属于他们自己的任务,也就是审核这次请求。

可以看到,有6个选项,每个选项都代表不同的接口,需要我们进行不同的处理;

这些任务流程都在我们的task包下:

我们流程的每一个步骤,相当于一个任务,每个任务都有自己对应的负责人,也就是审核人,我们先看最简单的通过和不通过步骤:

通过任务:

/**
     * 通过任务
     *
     * @param userId 用户编号
     * @param reqVO  通过请求
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void approveTask(Long userId, @Valid BpmTaskApproveReqVO reqVO) {
        // 1.1 校验任务存在  如果存在查找出来,如果不存在抛异常
        Task task = validateTask(userId, reqVO.getId());
        // 1.2 校验流程实例存在   //查找出任务id符合的一条实例
        ProcessInstance instance = processInstanceService.getProcessInstance(task.getProcessInstanceId());
        if (instance == null) {
            throw exception(PROCESS_INSTANCE_NOT_EXISTS);
        }

        // 情况一:被委派的任务,不调用 complete 去完成任务
        if (DelegationState.PENDING.equals(task.getDelegationState())) {
            approveDelegateTask(reqVO, task);
            return;
        }

        // 情况二:后加签的任务
        if (BpmTaskAddSignTypeEnum.AFTER.getType().equals(task.getScopeType())) {
            // 后加签处理
            approveAfterSignTask(task, reqVO);
            return;
        }

        // 情况三:自己审批的任务,调用 complete 去完成任务
        // 完成任务,审批通过

        //找到下一个节点的任务
        String nextTaskKey = getNextTaskKey("next", task.getId());
        taskService.complete(task.getId(), instance.getProcessVariables());

        // 更新任务拓展表为通过
        taskExtMapper.updateByTaskId(
                new BpmTaskExtDO().setTaskId(task.getId()).setResult(BpmProcessInstanceResultEnum.APPROVE.getResult())
                        .setReason(reqVO.getReason()));
        // 处理加签任务
        handleParentTask(task);


    }

 我们一般通过任务只需要调用taskService的complete方法,只需要当前任务的id和任务携带的信息,比如审批意见什么的,调用完这个方法之后,flowable内部通过各种监听器和拦截器,对数据库的信息进行操作,比如我们之前介绍的act_ru类型的表,也就是我们的流程在运行的时候,数据库里面都会记录,还有历史表这些,flowable底层都会通过调用不同的方法记录到对应的表中,但是我们这个项目还自定义了一些表,bpm_开头的,这些表也需要实时的记录数据,那么我们也需要和flowable底层一样,通过监听各种我们的流程的各种状态然后给我们对应的表进行修改数据,

找到对应的包:

这个就是我们对应的监听器:

@Component
@Slf4j
public class BpmTaskEventListener extends AbstractFlowableEngineEventListener {

    @Resource
    @Lazy // 解决循环依赖
    private BpmTaskService taskService;

    @Resource
    @Lazy // 解决循环依赖
    private BpmActivityService activityService;

    public static final Set<FlowableEngineEventType> TASK_EVENTS = ImmutableSet.<FlowableEngineEventType>builder()
            .add(FlowableEngineEventType.TASK_CREATED)
            .add(FlowableEngineEventType.TASK_ASSIGNED)
            .add(FlowableEngineEventType.TASK_COMPLETED)
            .add(FlowableEngineEventType.ACTIVITY_CANCELLED)
            .build();

    public BpmTaskEventListener(){
        super(TASK_EVENTS);
    }

    @Override
    protected void taskCreated(FlowableEngineEntityEvent event) {
        taskService.createTaskExt((Task) event.getEntity());


    }

    @Override
    protected void taskCompleted(FlowableEngineEntityEvent event) {

        taskService.updateTaskExtComplete((Task)event.getEntity());

    }

    @Override
    protected void taskAssigned(FlowableEngineEntityEvent event) {
        taskService.updateTaskExtAssign((Task)event.getEntity());
    }

    @Override
    protected void activityCancelled(FlowableActivityCancelledEvent event) {
        List<HistoricActivityInstance> activityList = activityService.getHistoricActivityListByExecutionId(event.getExecutionId());
        if (CollUtil.isEmpty(activityList)) {
            log.error("[activityCancelled][使用 executionId({}) 查找不到对应的活动实例]", event.getExecutionId());
            return;
        }
        // 遍历处理
        activityList.forEach(activity -> {
            if (StrUtil.isEmpty(activity.getTaskId())) {
                return;
            }
            taskService.updateTaskExtCancel(activity.getTaskId());
        });
    }

}

我们也并不是实现flowable监听任务的对应的接口,而是继承了AbstractFlowableEngineEventListener 这个类,这样就可以在之前的基础上,更新我们自定义的表了,但是需要注意的是,每个方法的执行的顺序,对数据库操作的顺序,然后保证事务,有些数据只有在事务提交之后才会查到。我们可以通过对每个方法打断点,然后进行debug调试,得到这些方法的执行顺序,以便于我们对功能更好的理解和维护;

举个例子,我们现在的系统,只能一步一步的往下执行任务,如果遇到一种,连续几个任务的审批人都是同一个的,比如说一个人身兼多职,既是一级经理又是二级经理,现在我们的系统到达已经经理,已经经理审批通过,交给二级经理,还是同一个人,那么这个人还需要重复审批一遍,这就很麻烦,我想要的是,如果是连续的流程审批人是同一个人,那么这一个人只需要审批一次,剩下相同审批人的流程直接跳过。交给下一个人。

问题提出了:那么如何解决呢,首先就是我们点击完通过这个按钮,后端发送通过任务的请求,我们刚才也看了就是后端调用了一个完成的方法就结束了,之后的流程相当于自动执行的,我们并没有显示的去调用任何方法,来到我们自定义的监听器,通过debug发现,当完成任务的时候,我们先要更新task拓展表的记录,将任务的状态变成已完成,然后更新下一个任务的信息,其中有计算候选人,然后如何选出候选人,这些都在代码里有,大家可以仔细看看,也有注释,慢慢理一遍流程就好,更新完下一步任务的信息之后,我们就需要创建新的任务了,在这里,我们发现,这个方法传进来的任务对象就是下一个任务的对象,我们在这里只需要判断当前用户的信息和下一个任务的审核人的id是否一致,就可以判断出下一个任务和当前任务是否是同一个审批人了,如果是的话,那么我们在调用一次完成任务的方法就ok了

下面是我写的简单的实现

    /**
     * 创建 Task 拓展记录
     *
     * @param task 任务实体
     */
    @Override
    public void createTaskExt(Task task) {



        BpmTaskExtDO taskExtDO = BpmTaskConvert.INSTANCE.convert2TaskExt(task)
                .setResult(BpmProcessInstanceResultEnum.PROCESS.getResult());
        // 向后加签生成的任务,状态不能为进行中,需要等前面父任务完成
        if (BpmTaskAddSignTypeEnum.AFTER_CHILDREN_TASK.getType().equals(task.getScopeType())) {
            taskExtDO.setResult(BpmProcessInstanceResultEnum.WAIT_BEFORE_TASK.getResult());
        }
        taskExtMapper.insert(taskExtDO);

        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();//当前用户
        if (loginUserId != null && loginUserId.equals(NumberUtils.parseLong(task.getAssignee()))){
            //如果两个任务的审批人是同一个人
            //完成下个节点的任务
            taskService.complete(task.getId(), null);
            // 更新任务拓展表为通过 下个任务
            taskExtMapper.updateByTaskId(
                    new BpmTaskExtDO().setTaskId(task.getId()).setResult(BpmProcessInstanceResultEnum.APPROVE.getResult())
                            .setReason("pass"));
        }



    }

这样就基本实现了我们的需求了,如果这里还是不明白,建议多debug几遍,然后对应着注释多看看代码,对于刚刚开始工作的我们还是有很大的帮助的,可以学习大佬的思路,学习大佬的想法,也是一种很不错的收获;

而且这里的实现方式有很多种,不只有这一种,我也是刚学习这个技术,有不好的地方也希望有大佬能多多指正啊~~~

这样我们的基本流程就有点思路了,先发起这个流程实例,根据流程模型计算出下一个任务和下一个任务的审批人,这个项目还有发送通知的方法,好像是通过短信发送消息给对应的人,这里我大概看了一下,没有具体看,然后对应的审批人看到审批了,进行操作,操作之后在次调用给下一个任务的审批人....一直到结束;

这里只是一个通过的例子,还有不通过,转办,委派,加签,回退各种操作;

剩下的大家可以通过文档和资料学习,通过代码一步步追踪,里面有很多东西很不错,值得我们深入研究,这篇文章就只是一个入门级别的,要想更深层次的研究,还需要时间,大家一起加油!!!

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐