文章

【若依】31、流程表单

表单分类:

  1. 动态表单:我们在每一个流程任务中配置的表单信息,可以设置每一个字段的可读性、可写性以及是否必填等信息,动态表单一般来说是不需要完整的页面的。
  2. 外置表单:我们可以自定义一个 HMTL 片段或者一个 JSON 字符串,然后在我们的项目中去引用这个 HTML 片段或者 JSON 字符串。一般来说,我们在流程中用到表单,基本上都是外置表单。
  3. 内置表单:之前在 flowable-ui 中使用的表单,其实就是内置表单。

无论是哪种表单,只有在开始节点和任务节点是支持表单定义的,其他节点不支持。

❓表单与变量传递数据的区别❓ 变量零存零取,表单整存整取; 表单会校验而变量不会;

1 动态表单

DynamicFormDemo01.bpmn20.xml

1. 绘制流程图

在绘制流程图的过程中,可以为 start 节点或者 UserTask 节点添加动态表单属性: image.png image.png 下载流程图,在 UserTask 节点中,就有流程图对应的元素:

1
2
3
4
5
6
7
8
9
<userTask id="sid-B3D149FE-2230-4934-B5E4-89E53CAA940F" name="请假申请" flowable:assignee="${INITATOR}" flowable:formFieldValidation="true">
  <extensionElements>
    <flowable:formProperty id="days" name="请假天数" type="long" required="true"></flowable:formProperty>
    <flowable:formProperty id="reason" name="请假理由" type="string" required="true"></flowable:formProperty>
    <flowable:formProperty id="startTime" name="开始时间" type="date" datePattern="yyyy-MM-dd" required="true"></flowable:formProperty>
    <flowable:formProperty id="endTime" name="结束时间" type="date" datePattern="yyyy-MM-dd" required="true"></flowable:formProperty>
    <modeler:initiator-can-complete xmlns:modeler="http://flowable.org/modeler"><![CDATA[false]]></modeler:initiator-can-complete>
  </extensionElements>
</userTask>

此时,如果想给其他节点也添加动态表单属性,可以参考上面这个写法去完成:

1
2
3
4
5
6
7
8
9
<startEvent id="startEvent1" flowable:initiator="INITATOR" flowable:formFieldValidation="true">
  <extensionElements>
    <flowable:formProperty id="days" name="请假天数" type="long" required="true"></flowable:formProperty>
    <flowable:formProperty id="reason" name="请假理由" type="string" required="true"></flowable:formProperty>
    <flowable:formProperty id="startTime" name="开始时间" type="date" datePattern="yyyy-MM-dd" required="true"></flowable:formProperty>
    <flowable:formProperty id="endTime" name="结束时间" type="date" datePattern="yyyy-MM-dd" required="true"></flowable:formProperty>
    <modeler:initiator-can-complete xmlns:modeler="http://flowable.org/modeler"><![CDATA[false]]></modeler:initiator-can-complete>
  </extensionElements>
</startEvent>

2. 查询启动节点的动态表单定义信息

当启动一个流程实例的时候,可以先去查询流程的启动节点上有哪些表单需要填写,可以查询到参数的名称、类型、是否必填等重要信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
 * 查询启动节点的动态表单定义信息
 */
@Test
void getStartFormData() {
    // 查询流程定义(DynamicFormDemo01)
    ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().processDefinitionKey("DynamicFormDemo01").latestVersion().singleResult();
    // 获取启动节点上的表单定义信息
    StartFormData startFormData = formService.getStartFormData(processDefinition.getId());
    // 表单的 key,这个属性实际上是给外置表单使用的
    log.info(")_(" + "流程定义的 id:{},表单的 key:{}", processDefinition.getId(), startFormData.getFormKey());
    // 获取 startEvent 全部的动态表单属性
    List<FormProperty> formPropertyList = startFormData.getFormProperties();
    for (FormProperty formProperty : formPropertyList) {
        String id = formProperty.getId();
        String name = formProperty.getName();
        String value = formProperty.getValue();
        FormType type = formProperty.getType();
        // 类型的名字
        String typeName = type.getName();
        Object info = "";
        // 对于枚举和日期类型,可以获取该字段的额外信息
        if (type instanceof EnumFormType) {
            // 如果当前字段是一个枚举类型,获取枚举类型的值
            info = type.getInformation("values");
        } else if (type instanceof DateFormType) {
            // 如果当前字段是一个时间类型,获取时间格式
            info = type.getInformation("datePattern");
        }
        log.info(")_(" + "id:{},name:{},value:{},type:{},info:{}", id, name, value, typeName, info);
    }
}

当查询到这些信息之后,就可以去填写一个表单,进而去启动流程实例。

3. 启动带表单的流程实例

在启动的时候,完全可以把表单实例当成是普通变量来对待,直接启动即可:

1
2
3
4
5
6
7
8
9
10
11
@Test
void startProcessInstanceByKey() {
    Authentication.setAuthenticatedUserId("yueyazhui");
    Map<String, Object> vars = new HashMap<>();
    vars.put("days", 3);
    vars.put("reason", "想去西安玩");
    vars.put("startTime", "2023-01-24");
    vars.put("endTime", "2023-01-26");
    vars.put("type", "timeOff");
    ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("DynamicFormDemo01", vars);
}

也可以利用 FormService 来启动流程实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
void submitStartFormData() {
    ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().processDefinitionKey("DynamicFormDemo01").latestVersion().singleResult();
    Authentication.setAuthenticatedUserId("yueya");
    Map<String, String> vars = new HashMap<>();
    vars.put("days", "7");
    vars.put("reason", "休息一周");
    vars.put("startTime", "2023-02-21");
    vars.put("endTime", "2023-02-27");
    vars.put("type", "askForLeave");
    // 用这种方式提交,会检查各个表单属性是否正确
    ProcessInstance processInstance = formService.submitStartFormData(processDefinition.getId(), vars);
}

注:通过 FormService 启动的时候,会去检查动态表单中的各种约束条件是否都满足,如果不满足,表单启动会失败。

4. 查询任务上的表单

前面的案例是 startEvent 上的表单,接下来看任务上的表单,也就是 UserTask 上的表单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
 * 查询某一个任务节点上的表单信息,每一个任务节点表单上的数据,都是通过前面的节点传递过来的
 * 例:startEvent 的表单上有 days、reason、startTime、endTime 四个字段,但是当前的 UserTask 的表单上只有 days 和 type 两个字段,那么 days 的值,就是 startEvent 当时填入的值,type 则没有值
 */
@Test
void getTaskFormData() {
    Task task = taskService.createTaskQuery().singleResult();
    // 查询某一个 Task 的表单数据
    TaskFormData taskFormData = formService.getTaskFormData(task.getId());
    // 获取动态表单的各种属性列表并遍历
    List<FormProperty> formPropertyList = taskFormData.getFormProperties();
    for (FormProperty formProperty : formPropertyList) {
        String id = formProperty.getId();
        String name = formProperty.getName();
        String value = formProperty.getValue();
        FormType type = formProperty.getType();
        // 类型的名字
        String typeName = type.getName();
        Object info = "";
        // 对于枚举和日期类型,可以获取该字段的额外信息
        if (type instanceof EnumFormType) {
            // 如果当前字段是一个枚举类型,获取枚举类型的值
            info = type.getInformation("values");
        } else if (type instanceof DateFormType) {
            // 如果当前字段是一个时间类型,获取时间格式
            info = type.getInformation("datePattern");
        }
        log.info(")_(" + "id:{},name:{},value:{},type:{},info:{}", id, name, value, typeName, info);
    }
}

5. 保存和完成

保存只是保存表单;完成就是完成当前的 UserTask。

5.1 保存表单数据

1
2
3
4
5
6
7
8
9
10
/**
 * 修改某一个 UserTask 中的表单数据
 */
@Test
void saveFormData() {
    Task task = taskService.createTaskQuery().singleResult();
    Map<String, String> vars = new HashMap<>();
    vars.put("days", "10");
    formService.saveFormData(task.getId(), vars);
}

5.2 完成一个表单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
 * taskService.complete();
 * 此方法也可以用来完成表单数据,但在表单完成的时候,不会进行表单数据的校验
 */
@Test
void submitTaskFormData() {
    Task task = taskService.createTaskQuery().singleResult();
    Map<String, String> vars = new HashMap<>();
    vars.put("days", "10");
    vars.put("reason", "休息10天");
    vars.put("startTime", "2023-01-21");
    vars.put("endTime", "2023-01-30");
    vars.put("type", "askForLeave");
    // 这种完成方式,对于传递的表单数据会进行校验,但是日期的格式不会校验
    formService.submitTaskFormData(task.getId(), vars);
}

2 外置表单

ExternalFormDemo01.bpmn20.xml 外置表单就是提前准备好的 HTML 文件。

1. 准备流程图

image.png 在用户启动流程的时候,需要提交请假的表单数据,然后在组长审批和经理审批的节点上,可以查看提交的表单数据。 表单文件: askForLeave.html:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<form action="">
    <table>
        <tr>
            <td>请假天数:</td>
            <td><input type="text" name="days"></td>
        </tr>
        <tr>
            <td>请假理由:</td>
            <td><input type="text" name="reason"></td>
        </tr>
        <tr>
            <td>起始时间:</td>
            <td><input type="date" name="startTime"></td>
        </tr>
        <tr>
            <td>结束时间:</td>
            <td><input type="date" name="endTime"></td>
        </tr>
    </table>
</form>

leaderApproval.html:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<form action="">
    <table>
        <tr>
            <td>请假天数:</td>
            <td><input type="text" name="days" value="${days}"></td>
        </tr>
        <tr>
            <td>请假理由:</td>
            <td><input type="text" name="reason" value="${reason}"></td>
        </tr>
        <tr>
            <td>起始时间:</td>
            <td><input type="date" name="startTime" value="${startTime}"></td>
        </tr>
        <tr>
            <td>结束时间:</td>
            <td><input type="date" name="endTime" value="${endTime}"></td>
        </tr>
    </table>
</form>

2. 部署流程

提供流程部署的接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
 * 部署带表单的流程
 * HTML 表单,部署的时候需要跟流程一起部署,需要跟流程具备相同的部署 ID,否则,在流程运行的时候,查询不到这个流程所使用的外置表单
 * 部署流程时,同时上传该流程的流程部署文件和该流程所使用的外置表单
 *
 * @param files
 * @return
 */
@PostMapping("/deployWithForm")
public Response deployWithForm(MultipartFile[] files) throws IOException {
    DeploymentBuilder deploymentBuilder = repositoryService
            // 开始流程部署的构建
            .createDeployment()
            .name("部署工作流的名称")
            .category("部署工作流的分类")
            .key("部署工作流的 KEY");
    for (MultipartFile file : files) {
        deploymentBuilder.addInputStream(file.getOriginalFilename(), file.getInputStream());
    }
    // 完成项目的部署
    Deployment deployment = deploymentBuilder.deploy();
    return Response.success("部署成功", deployment.getId());
}

部署过程: image.png image.png 部署成功之后,DEPLOYMENT_ID 的值是一模一样的。

3. 查看流程启动节点上的表单

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
 * 查询启动节点上定义的外置表单(属性、内容)
 */
@Test
void getStartFormKeyAndRenderedStartForm() {
    ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().processDefinitionKey("ExternalFormDemo01").latestVersion().singleResult();
    // 查询启动节点上,外置表单的 key
    String startFormKey = formService.getStartFormKey(processDefinition.getId());
    log.info(")_(" + "startFormKey:{}", startFormKey);
    // 查询启动节点上,渲染之后的流程表单(此方法只针对外置表单,动态表单不支持此方法)
    String renderedStartForm = (String) formService.getRenderedStartForm(processDefinition.getId());
    log.info(")_(" + "renderedStartForm:{}", renderedStartForm);
}

如果是一个 Web 工程,那么就可以通过 Ajax 请求去获取这个结果集,这个结果集就是一个已经渲染过的 HTML,拿到这个 HTML 之后,就可以将之放到一个 div 中,然后在前端页面中渲染出来。

查询外置表单的 SQL:

1
select * from ACT_GE_BYTEARRAY where DEPLOYMENT_ID_ = ? AND NAME_ = ?

可以看到,查询外置表单需要根据部署的 ID 去查询,而获取流程的部署 ID 是比较容易的,因此建议将流程和表单一起部署,这样两者就具备相同的部署 ID。

4. 启动流程

启动流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
 * 启动带外置表单的流程
 */
@Test
void submitStartFormDataWithExternalForm() {
    ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().processDefinitionKey("ExternalFormDemo01").latestVersion().singleResult();
    Map<String, String> vars = new HashMap<>();
    vars.put("days", "3");
    vars.put("reason", "去西安看看大唐不夜城");
    vars.put("startTime", "2023-01-24");
    vars.put("endTime", "2023-01-26");
    ProcessInstance processInstance = formService.submitStartFormData(processDefinition.getId(), vars);
}

在 UserTask 处理之前,先查看一下这个 UserTask 所对应的流程信息:

1
2
3
4
5
6
7
8
9
10
/**
 * 在组长审批之前,查看一下这个 UserTask 外置表单的数据
 */
@Test
void getRenderedTaskForm() {
    Task task = taskService.createTaskQuery().singleResult();
    // 获取渲染之后的外置表单,这里会自动的读取流程变量,并将表单中的值给渲染出来
    String renderedTaskForm = (String) formService.getRenderedTaskForm(task.getId());
    log.info(")_(" + "renderedTaskForm:{}", renderedTaskForm);
}

流程审批:

1
2
3
4
5
6
7
8
9
10
/**
 * 流程审批
 */
@Test
void test10() {
    Task task = taskService.createTaskQuery().singleResult();
    Map<String, String> vars = new HashMap<>();
    vars.put("days", "100");
    formService.submitTaskFormData(task.getId(),vars);
}

5. JSON 配置外置表单

JsonFormatExternalFormDemo01.bpmn20.xml 可以利用 Spring Boot 的自动部署来配置 JSON 外置表单。 在 application.properties 中配置外置表单的位置和后缀:

1
2
3
4
5
# JSON 外置表单的默认位置
flowable.form.resource-location=classpath*:/forms/
# JSON 外置表单的默认后缀
# .form 也是 Java 中 Swing 配置的默认后缀,所以在 IDEA 中,.form 后缀的文件,会被当成 Swing 的配置打开,此时,就需要先在一个外部编辑器里边编辑好这个 .form 文件
flowable.form.resource-suffixes=**.form

提供一个 JSON 表单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
{
    "key":"application_form.form",
    "name":"请假审批表单",
    "fields":[
        {
            "id":"days",
            "name":"请假天数",
            "type":"string",
            "required":true,
            "placeholder":"empty"
        },{
            "id":"reason",
            "name":"请假理由",
            "type":"string",
            "required":true,
            "placeholder":"empty"
        },{
            "id":"startTime",
            "name":"开始时间",
            "type":"date",
            "required":true,
            "placeholder":"empty"
        },{
            "id":"endTime",
            "name":"结束时间",
            "type":"date",
            "required":true,
            "placeholder":"empty"
        }
    ]
}

在流程中,使用 JSON 表单: image.png image.png 将流程图下载下来之后,放到 classpath:/processes/ 目录下,让流程图自动部署。 执行任务之前,查看任务需要填写的表单信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
 * 查看 UserTask 需要填写的表单内容
 */
@Test
void getTaskFormModel() {
    Task task = taskService.createTaskQuery().singleResult();
    // 获取 UserTask 上的表单信息
    FormInfo formInfo = taskService.getTaskFormModel(task.getId());
    String formInfoKey = formInfo.getKey();
    String formInfoId = formInfo.getId();
    String formInfoName = formInfo.getName();
    String formInfoDescription = formInfo.getDescription();
    int formInfoVersion = formInfo.getVersion();
    log.info(")_(" + "key:{},id:{},name:{},description:{},version:{}", formInfoKey, formInfoId, formInfoName, formInfoDescription, formInfoVersion);
    SimpleFormModel simpleFormModel = (SimpleFormModel) formInfo.getFormModel();
    // 获取表单中的各个字段
    List<FormField> formFieldList = simpleFormModel.getFields();
    for (FormField formField : formFieldList) {
        String formFieldId = formField.getId();
        String formFieldType = formField.getType();
        Object formFieldValue = formField.getValue();
        String formFieldName = formField.getName();
        log.info(")_(" + "表单上的字段:id:{},type:{},value:{},name:{}", formFieldId, formFieldType, formFieldValue, formFieldName);
    }
}

任务提交之前,这里能够看到的表单字段的 value 都是 null,任务提交之后,表单字段的 value 就是提交具体的值了。

本文由作者按照 CC BY 4.0 进行授权