cuicui20210817 3 роки тому
батько
коміт
85426568bf
36 змінених файлів з 1939 додано та 0 видалено
  1. 13 0
      yudao-module-bpm/yudao-module-bpm-biz/pom.xml
  2. 78 0
      yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/AbstractOATest.java
  3. 226 0
      yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/ConditionTest.java
  4. 177 0
      yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/LeaveFormKeyTest.java
  5. 208 0
      yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/MultiInstancesTest.java
  6. BIN
      yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/chapter6/dynamic-form/leave-dynamic-form-with-javascript.zip
  7. BIN
      yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/chapter6/dynamic-form/leave-dynamic-form.zip
  8. 198 0
      yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/chapter6/dynamic-form/leave.bpmn
  9. BIN
      yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/chapter6/dynamic-form/leave.png
  10. 31 0
      yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/chapter6/leave-formkey/approve-deptLeader.form
  11. 31 0
      yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/chapter6/leave-formkey/approve-hr.form
  12. 151 0
      yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/chapter6/leave-formkey/leave-formkey.bpmn
  13. BIN
      yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/chapter6/leave-formkey/leave-formkey.png
  14. BIN
      yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/chapter6/leave-formkey/leave-formkey.zip
  15. 18 0
      yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/chapter6/leave-formkey/leave-start.form
  16. 27 0
      yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/chapter6/leave-formkey/modify-apply.form
  17. 28 0
      yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/chapter6/leave-formkey/report-back.form
  18. 31 0
      yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/chapter6/leave-timeLimit-money/approve-deptLeader.form
  19. 31 0
      yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/chapter6/leave-timeLimit-money/approve-hr.form
  20. 188 0
      yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/chapter6/leave-timeLimit-money/leave-formkey-ext.bpmn
  21. BIN
      yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/chapter6/leave-timeLimit-money/leave-formkey-ext.png
  22. 18 0
      yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/chapter6/leave-timeLimit-money/leave-start.form
  23. 27 0
      yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/chapter6/leave-timeLimit-money/modify-apply.form
  24. 28 0
      yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/chapter6/leave-timeLimit-money/report-back.form
  25. 152 0
      yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/diagrams/chapter9/leave-countersign.bpmn
  26. BIN
      yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/diagrams/chapter9/leave-countersign.png
  27. 44 0
      yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/diagrams/chapter9/testMultiInstanceFixedNumbers.bpmn
  28. 42 0
      yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/diagrams/chapter9/testMultiInstanceForUserTask.exception.bpmn
  29. BIN
      yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/diagrams/chapter9/testMultiInstanceForUserTask.exception.png
  30. 35 0
      yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/diagrams/chapter9/testMultiInstanceForUserTask.nosequential.bpmn
  31. 35 0
      yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/diagrams/chapter9/testMultiInstanceForUserTask.sequential.bpmn
  32. 33 0
      yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/diagrams/chapter9/testMultiInstanceForUserTask.users.nosequential.bpmn
  33. 33 0
      yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/diagrams/chapter9/testMultiInstanceForUserTask.users.sequential.bpmn
  34. 35 0
      yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/diagrams/chapter9/testMultiInstanceForUserTask.users.sequential.with.complete.conditon.bpmn
  35. BIN
      yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/diagrams/chapter9/testMultiInstanceForUserTask.users.sequential.with.complete.conditon.png
  36. 21 0
      yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/flowable.cfg.xml

+ 13 - 0
yudao-module-bpm/yudao-module-bpm-biz/pom.xml

@@ -64,5 +64,18 @@
             <groupId>cn.iocoder.boot</groupId>
             <artifactId>yudao-spring-boot-starter-flowable</artifactId>
         </dependency>
+        <!--junit4-->
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>4.11</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.h2database</groupId>
+            <artifactId>h2</artifactId>
+            <version>1.4.196</version>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 </project>

+ 78 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/AbstractOATest.java

@@ -0,0 +1,78 @@
+package cn.iocoder.yudao.module.bpm;
+
+import org.flowable.engine.*;
+import org.flowable.engine.impl.ProcessEngineImpl;
+import org.flowable.engine.impl.cfg.ProcessEngineConfigurationImpl;
+import org.flowable.engine.impl.test.TestHelper;
+import org.flowable.engine.test.FlowableRule;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+
+/**
+ * 抽象测试基类
+ *
+ * @author henryyan cuicui
+ */
+public abstract class AbstractOATest {
+
+    protected String configurationResource = "flowable.cfg.xml";
+
+    /**
+     * 专门用于测试套件
+     */
+    @Rule
+    public FlowableRule activitiRule = new FlowableRule("flowable.cfg.xml");
+
+    protected ProcessEngineConfigurationImpl processEngineConfiguration;
+
+    protected ProcessEngine processEngine;
+    protected RepositoryService repositoryService;
+    protected RuntimeService runtimeService;
+    protected TaskService taskService;
+    protected HistoryService historyService;
+    protected IdentityService identityService;
+    protected ManagementService managementService;
+    protected FormService formService;
+
+    /**
+     * 开始测试
+     */
+    @BeforeClass
+    public static void setUpForClass() throws Exception {
+        System.out.println("++++++++ 开始测试 ++++++++");
+    }
+
+    /**
+     * 结束测试
+     */
+    @AfterClass
+    public static void testOverForClass() throws Exception {
+        System.out.println("-------- 结束测试 --------");
+    }
+
+    protected void initializeProcessEngine() {
+        processEngine = TestHelper.getProcessEngine(configurationResource);
+    }
+
+    /**
+     * 初始化变量
+     */
+    @Before
+    public void setUp() throws Exception {
+        if (processEngine == null) {
+            initializeProcessEngine();
+        }
+
+        processEngineConfiguration = ((ProcessEngineImpl) processEngine).getProcessEngineConfiguration();
+        repositoryService = processEngine.getRepositoryService();
+        runtimeService = processEngine.getRuntimeService();
+        taskService = processEngine.getTaskService();
+        historyService = processEngine.getHistoryService();
+        identityService = processEngine.getIdentityService();
+        managementService = processEngine.getManagementService();
+        formService = processEngine.getFormService();
+    }
+
+}

+ 226 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/ConditionTest.java

@@ -0,0 +1,226 @@
+package cn.iocoder.yudao.module.bpm;
+
+import org.flowable.engine.history.HistoricDetail;
+import org.flowable.engine.history.HistoricFormProperty;
+import org.flowable.engine.history.HistoricProcessInstance;
+import org.flowable.engine.history.HistoricVariableUpdate;
+import org.flowable.engine.repository.ProcessDefinition;
+import org.flowable.engine.runtime.ProcessInstance;
+import org.flowable.engine.test.Deployment;
+import org.flowable.task.api.Task;
+import org.joda.time.DateTime;
+import org.junit.Test;
+
+import java.text.SimpleDateFormat;
+import java.util.*;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+/**
+ * 条件测试
+ * 1 某个节点-支付额度需要大于70%
+ * 2 支付条件完成后,开始倒计时15天,要完成流程处理
+ * @author cuicui
+ */
+public class ConditionTest extends AbstractOATest {
+
+    @Test
+    @Deployment(resources = {"chapter6/leave-timeLimit-money/leave-formkey-ext.bpmn", "chapter6/leave-timeLimit-money/leave-start.form",
+            "chapter6/leave-timeLimit-money/approve-deptLeader.form", "chapter6/leave-timeLimit-money/approve-hr.form", "chapter6/leave-timeLimit-money/report-back.form",
+            "chapter6/leave-timeLimit-money/modify-apply.form"})
+    public void allPass() throws Exception {
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+        Map<String, String> variables = new HashMap<String, String>();
+        Calendar ca = Calendar.getInstance();
+        String startDate = sdf.format(ca.getTime());
+        ca.add(Calendar.DAY_OF_MONTH, 2); // 当前日期加2天
+        String endDate = sdf.format(ca.getTime());
+
+        // 启动流程
+        variables.put("startDate", startDate);
+        variables.put("endDate", endDate);
+        variables.put("reason", "公休");
+
+        ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().singleResult();
+
+        // 读取启动表单
+        Object renderedStartForm = formService.getRenderedStartForm(processDefinition.getId());
+        assertNotNull(renderedStartForm);
+
+        // 启动流程
+        // 设置当前用户
+        String currentUserId = "henryyan";
+        identityService.setAuthenticatedUserId(currentUserId);
+        ProcessInstance processInstance = formService.submitStartFormData(processDefinition.getId(), variables);
+        assertNotNull(processInstance);
+
+        // 部门领导审批通过
+        Task deptLeaderTask = taskService.createTaskQuery().taskCandidateGroup("deptLeader").singleResult();
+        assertNotNull(formService.getRenderedTaskForm(deptLeaderTask.getId()));
+        variables = new HashMap<String, String>();
+        variables.put("deptLeaderApproved", "true");
+        formService.submitTaskFormData(deptLeaderTask.getId(), variables);
+
+        // 人事审批通过
+        Task hrTask = taskService.createTaskQuery().taskCandidateGroup("hr").singleResult();
+        assertNotNull(formService.getRenderedTaskForm(hrTask.getId()));
+        variables = new HashMap<String, String>();
+        variables.put("hrApproved", "true");
+        //手动设置支付金额
+        variables.put("amountMoney", "19999");
+        formService.submitTaskFormData(hrTask.getId(), variables);
+        //判断支付金额是否>1万元
+        // 财务打款通过
+        Task caiwuTask = taskService.createTaskQuery().taskCandidateGroup("caiwu").singleResult();
+        printTask(caiwuTask);
+        taskService.complete(caiwuTask.getId());
+        //判断倒计时15天
+        Task chuNaTask = taskService.createTaskQuery().taskCandidateGroup("chuNa").singleResult();
+        printTask(chuNaTask);
+        taskService.complete(chuNaTask.getId());
+
+        // 销假(根据申请人的用户ID读取)
+//        Task reportBackTask = taskService.createTaskQuery().taskAssignee(currentUserId).singleResult();
+//        assertNotNull(formService.getRenderedTaskForm(reportBackTask.getId()));
+//        variables = new HashMap<String, String>();
+//        variables.put("reportBackDate", sdf.format(ca.getTime()));
+//        formService.submitTaskFormData(reportBackTask.getId(), variables);
+
+        // 验证流程是否已经结束
+        HistoricProcessInstance historicProcessInstance = historyService.createHistoricProcessInstanceQuery().finished().singleResult();
+        assertNotNull(historicProcessInstance);
+
+        // 读取历史变量
+        Map<String, Object> historyVariables = packageVariables(processInstance);
+
+        // 验证执行结果
+        assertEquals("ok", historyVariables.get("result"));
+
+    }
+
+    /**
+     * 查询过期任务
+     * @throws Exception
+     */
+    @Test
+    @Deployment(resources = {"chapter6/leave-timeLimit-money/leave-formkey-ext.bpmn", "chapter6/leave-timeLimit-money/leave-start.form",
+            "chapter6/leave-timeLimit-money/approve-deptLeader.form", "chapter6/leave-timeLimit-money/approve-hr.form", "chapter6/leave-timeLimit-money/report-back.form",
+            "chapter6/leave-timeLimit-money/modify-apply.form"})
+    public void dueDate() throws Exception {
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+        Map<String, String> variables = new HashMap<String, String>();
+        Calendar ca = Calendar.getInstance();
+        String startDate = sdf.format(ca.getTime());
+        ca.add(Calendar.DAY_OF_MONTH, 2); // 当前日期加2天
+        String endDate = sdf.format(ca.getTime());
+
+        // 启动流程
+        variables.put("startDate", startDate);
+        variables.put("endDate", endDate);
+        variables.put("reason", "公休");
+
+        ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().singleResult();
+
+        // 读取启动表单
+        Object renderedStartForm = formService.getRenderedStartForm(processDefinition.getId());
+        assertNotNull(renderedStartForm);
+
+        // 启动流程
+        // 设置当前用户
+        String currentUserId = "henryyan";
+        identityService.setAuthenticatedUserId(currentUserId);
+        ProcessInstance processInstance = formService.submitStartFormData(processDefinition.getId(), variables);
+        assertNotNull(processInstance);
+
+        // 部门领导审批通过
+        Task deptLeaderTask = taskService.createTaskQuery().taskCandidateGroup("deptLeader").singleResult();
+        assertNotNull(formService.getRenderedTaskForm(deptLeaderTask.getId()));
+        variables = new HashMap<String, String>();
+        variables.put("deptLeaderApproved", "true");
+        formService.submitTaskFormData(deptLeaderTask.getId(), variables);
+
+        // 人事审批通过
+        Task hrTask = taskService.createTaskQuery().taskCandidateGroup("hr").singleResult();
+        assertNotNull(formService.getRenderedTaskForm(hrTask.getId()));
+        variables = new HashMap<String, String>();
+        variables.put("hrApproved", "true");
+        //手动设置支付金额
+        variables.put("amountMoney", "19999");
+        formService.submitTaskFormData(hrTask.getId(), variables);
+        //判断支付金额是否>1万元
+        // 财务打款通过
+        Task caiwuTask = taskService.createTaskQuery().taskCandidateGroup("caiwu").singleResult();
+        printTask(caiwuTask);
+
+        //设置5天前就过期了
+        DateTime dateTime = DateTime.now();
+        DateTime minusDays = dateTime.minusDays(5);
+        taskService.setDueDate(caiwuTask.getId(),minusDays.toDate());
+        //查询今天以前的过期任务
+        List<Task> listTask = taskService.createTaskQuery().taskDueBefore(new Date()).list();
+        for (Task task : listTask) {
+            printTask(task);
+        }
+
+        // 销假(根据申请人的用户ID读取)
+//        Task reportBackTask = taskService.createTaskQuery().taskAssignee(currentUserId).singleResult();
+//        assertNotNull(formService.getRenderedTaskForm(reportBackTask.getId()));
+//        variables = new HashMap<String, String>();
+//        variables.put("reportBackDate", sdf.format(ca.getTime()));
+//        formService.submitTaskFormData(reportBackTask.getId(), variables);
+
+        // 验证流程是否已经结束
+        HistoricProcessInstance historicProcessInstance = historyService.createHistoricProcessInstanceQuery().finished().singleResult();
+        assertNotNull(historicProcessInstance);
+
+        // 读取历史变量
+        Map<String, Object> historyVariables = packageVariables(processInstance);
+
+        // 验证执行结果
+        assertEquals("ok", historyVariables.get("result"));
+
+    }
+
+    /**
+     * 任意流程的跳转
+     */
+    @Test
+    public void taskJump(){
+        // 当前任务
+        String taskId="ddd";
+        Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
+        String assignee = "下一个自由跳转人";
+        taskService.setAssignee(taskId,assignee);
+        // 自由跳转
+        String taskDefKey="目标-任务名称";
+        //moveActivityIdTo的两个参数,源任务key,目标任务key
+        runtimeService.createChangeActivityStateBuilder().processInstanceId(task.getProcessInstanceId()).moveActivityIdTo(task.getTaskDefinitionKey(), taskDefKey).changeState();
+
+    }
+
+
+    /**
+     * 读取历史变量并封装到Map中
+     */
+    private Map<String, Object> packageVariables(ProcessInstance processInstance) {
+        Map<String, Object> historyVariables = new HashMap<String, Object>();
+        List<HistoricDetail> list = historyService.createHistoricDetailQuery().processInstanceId(processInstance.getId()).list();
+        for (HistoricDetail historicDetail : list) {
+            if (historicDetail instanceof HistoricFormProperty) {
+                // 表单中的字段
+                HistoricFormProperty field = (HistoricFormProperty) historicDetail;
+                historyVariables.put(field.getPropertyId(), field.getPropertyValue());
+                System.out.println("form field: taskId=" + field.getTaskId() + ", " + field.getPropertyId() + " = " + field.getPropertyValue());
+            } else if (historicDetail instanceof HistoricVariableUpdate) {
+                HistoricVariableUpdate variable = (HistoricVariableUpdate) historicDetail;
+                historyVariables.put(variable.getVariableName(), variable.getValue());
+                System.out.println("variable: " + variable.getVariableName() + " = " + variable.getValue());
+            }
+        }
+        return historyVariables;
+    }
+    private void printTask(Task task){
+        System.out.println(task.getName()+" : " + task.getDueDate());
+    }
+}

+ 177 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/LeaveFormKeyTest.java

@@ -0,0 +1,177 @@
+package cn.iocoder.yudao.module.bpm;
+
+import org.flowable.engine.history.HistoricDetail;
+import org.flowable.engine.history.HistoricFormProperty;
+import org.flowable.engine.history.HistoricProcessInstance;
+import org.flowable.engine.history.HistoricVariableUpdate;
+import org.flowable.engine.repository.ProcessDefinition;
+import org.flowable.engine.runtime.ProcessInstance;
+import org.flowable.engine.test.Deployment;
+import org.flowable.task.api.Task;
+import org.junit.Test;
+
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+/**
+ * @author henryyan
+ */
+public class LeaveFormKeyTest extends AbstractOATest {
+
+    @Test
+    @Deployment(resources = {"chapter6/leave-formkey/leave-formkey.bpmn", "chapter6/leave-formkey/leave-start.form",
+            "chapter6/leave-formkey/approve-deptLeader.form", "chapter6/leave-formkey/approve-hr.form", "chapter6/leave-formkey/report-back.form",
+            "chapter6/leave-formkey/modify-apply.form"})
+    public void allPass() throws Exception {
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+        Map<String, String> variables = new HashMap<String, String>();
+        Calendar ca = Calendar.getInstance();
+        String startDate = sdf.format(ca.getTime());
+        ca.add(Calendar.DAY_OF_MONTH, 2); // 当前日期加2天
+        String endDate = sdf.format(ca.getTime());
+
+        // 启动流程
+        variables.put("startDate", startDate);
+        variables.put("endDate", endDate);
+        variables.put("reason", "公休");
+
+        ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().singleResult();
+
+        // 读取启动表单
+        Object renderedStartForm = formService.getRenderedStartForm(processDefinition.getId());
+        assertNotNull(renderedStartForm);
+
+        // 启动流程
+        // 设置当前用户
+        String currentUserId = "henryyan";
+        identityService.setAuthenticatedUserId(currentUserId);
+        ProcessInstance processInstance = formService.submitStartFormData(processDefinition.getId(), variables);
+        assertNotNull(processInstance);
+
+        // 部门领导审批通过
+        Task deptLeaderTask = taskService.createTaskQuery().taskCandidateGroup("deptLeader").singleResult();
+        assertNotNull(formService.getRenderedTaskForm(deptLeaderTask.getId()));
+        variables = new HashMap<String, String>();
+        variables.put("deptLeaderApproved", "true");
+        formService.submitTaskFormData(deptLeaderTask.getId(), variables);
+
+        // 人事审批通过
+        Task hrTask = taskService.createTaskQuery().taskCandidateGroup("hr").singleResult();
+        assertNotNull(formService.getRenderedTaskForm(hrTask.getId()));
+        variables = new HashMap<String, String>();
+        variables.put("hrApproved", "true");
+        formService.submitTaskFormData(hrTask.getId(), variables);
+
+        // 销假(根据申请人的用户ID读取)
+        Task reportBackTask = taskService.createTaskQuery().taskAssignee(currentUserId).singleResult();
+        assertNotNull(formService.getRenderedTaskForm(reportBackTask.getId()));
+        variables = new HashMap<String, String>();
+        variables.put("reportBackDate", sdf.format(ca.getTime()));
+        formService.submitTaskFormData(reportBackTask.getId(), variables);
+
+        // 验证流程是否已经结束
+        HistoricProcessInstance historicProcessInstance = historyService.createHistoricProcessInstanceQuery().finished().singleResult();
+        assertNotNull(historicProcessInstance);
+
+        // 读取历史变量
+        Map<String, Object> historyVariables = packageVariables(processInstance);
+
+        // 验证执行结果
+        assertEquals("ok", historyVariables.get("result"));
+
+    }
+
+    /**
+     * 任意流程的跳转
+     */
+    @Test
+    public void taskJump(){
+        // 当前任务
+        String taskId="ddd";
+        Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
+        String assignee = "下一个自由跳转人";
+        taskService.setAssignee(taskId,assignee);
+        // 自由跳转
+        String taskDefKey="目标-任务名称";
+        //moveActivityIdTo的两个参数,源任务key,目标任务key
+        runtimeService.createChangeActivityStateBuilder().processInstanceId(task.getProcessInstanceId()).moveActivityIdTo(task.getTaskDefinitionKey(), taskDefKey).changeState();
+
+    }
+
+    /**
+     * 领导驳回后申请人取消申请
+     */
+    @Test
+    @Deployment(resources = "chapter6/dynamic-form/leave.bpmn")
+    public void cancelApply() throws Exception {
+
+        // 设置当前用户
+        String currentUserId = "henryyan";
+        identityService.setAuthenticatedUserId(currentUserId);
+
+        ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().processDefinitionKey("leave").singleResult();
+
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+        Map<String, String> variables = new HashMap<String, String>();
+        Calendar ca = Calendar.getInstance();
+        String startDate = sdf.format(ca.getTime());
+        ca.add(Calendar.DAY_OF_MONTH, 2);
+        String endDate = sdf.format(ca.getTime());
+
+        // 启动流程
+        variables.put("startDate", startDate);
+        variables.put("endDate", endDate);
+        variables.put("reason", "公休");
+        ProcessInstance processInstance = formService.submitStartFormData(processDefinition.getId(), variables);
+        assertNotNull(processInstance);
+
+        // 部门领导审批通过
+        Task deptLeaderTask = taskService.createTaskQuery().taskCandidateGroup("deptLeader").singleResult();
+        variables = new HashMap<String, String>();
+        variables.put("deptLeaderApproved", "false");
+        formService.submitTaskFormData(deptLeaderTask.getId(), variables);
+
+        // 调整申请
+        Task modifyApply = taskService.createTaskQuery().taskAssignee(currentUserId).singleResult();
+        variables = new HashMap<String, String>();
+        variables.put("reApply", "false");
+        variables.put("startDate", startDate);
+        variables.put("endDate", endDate);
+        variables.put("reason", "公休");
+        formService.submitTaskFormData(modifyApply.getId(), variables);
+
+        // 读取历史变量
+        Map<String, Object> historyVariables = packageVariables(processInstance);
+
+        // 验证执行结果
+        assertEquals("canceled", historyVariables.get("result"));
+
+    }
+
+    /**
+     * 读取历史变量并封装到Map中
+     */
+    private Map<String, Object> packageVariables(ProcessInstance processInstance) {
+        Map<String, Object> historyVariables = new HashMap<String, Object>();
+        List<HistoricDetail> list = historyService.createHistoricDetailQuery().processInstanceId(processInstance.getId()).list();
+        for (HistoricDetail historicDetail : list) {
+            if (historicDetail instanceof HistoricFormProperty) {
+                // 表单中的字段
+                HistoricFormProperty field = (HistoricFormProperty) historicDetail;
+                historyVariables.put(field.getPropertyId(), field.getPropertyValue());
+                System.out.println("form field: taskId=" + field.getTaskId() + ", " + field.getPropertyId() + " = " + field.getPropertyValue());
+            } else if (historicDetail instanceof HistoricVariableUpdate) {
+                HistoricVariableUpdate variable = (HistoricVariableUpdate) historicDetail;
+                historyVariables.put(variable.getVariableName(), variable.getValue());
+                System.out.println("variable: " + variable.getVariableName() + " = " + variable.getValue());
+            }
+        }
+        return historyVariables;
+    }
+}

+ 208 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/MultiInstancesTest.java

@@ -0,0 +1,208 @@
+package cn.iocoder.yudao.module.bpm;
+
+import org.flowable.engine.history.HistoricDetail;
+import org.flowable.engine.history.HistoricFormProperty;
+import org.flowable.engine.history.HistoricProcessInstance;
+import org.flowable.engine.history.HistoricVariableUpdate;
+import org.flowable.engine.repository.ProcessDefinition;
+import org.flowable.engine.runtime.ProcessInstance;
+import org.flowable.engine.test.Deployment;
+import org.flowable.task.api.Task;
+import org.junit.Test;
+
+import java.text.SimpleDateFormat;
+import java.util.*;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+/**
+ * @author henryyan
+ * testMultiInstanceForUserTask 会签
+ * cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior.BpmUserTaskActivityBehavior#handleAssignments(org.flowable.task.service.TaskService, java.lang.String, java.lang.String, java.util.List, java.util.List, org.flowable.task.service.impl.persistence.entity.TaskEntity, org.flowable.common.engine.impl.el.ExpressionManager, org.flowable.engine.delegate.DelegateExecution, org.flowable.engine.impl.cfg.ProcessEngineConfigurationImpl)
+ *
+ * 发生了死锁
+ * cn.iocoder.yudao.module.bpm.controller.admin.task.BpmProcessInstanceController#createProcessInstance(cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCreateReqVO)
+ * 执行了两次,任务分配到了同一个人
+ */
+public class MultiInstancesTest extends AbstractOATest {
+
+    /**
+     * Java Service多实例(是否顺序结果一样)
+     */
+    @Test
+    @Deployment(resources = {"diagrams/chapter9/testMultiInstanceFixedNumbers.bpmn"})
+    public void testParallel() throws Exception {
+        Map<String, Object> variables = new HashMap<String, Object>();
+        long loop = 3;
+        variables.put("loop", loop);
+        variables.put("counter", 0); // 计数器
+        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("testMultiInstanceFixedNumbers", variables);
+        Object variable = runtimeService.getVariable(processInstance.getId(), "counter");
+        assertEquals(loop, variable);
+    }
+
+    /**
+     * 用户任务多实例--顺序
+     */
+    @Test
+    @Deployment(resources = {"diagrams/chapter9/testMultiInstanceForUserTask.sequential.bpmn"})
+    public void testForUserSequence() throws Exception {
+        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("testMultiInstanceForUserTask");
+        long count = taskService.createTaskQuery().processInstanceId(processInstance.getId()).count();
+        assertEquals(1, count);
+
+        Task task = taskService.createTaskQuery().singleResult();
+        taskService.complete(task.getId());
+        count = taskService.createTaskQuery().processInstanceId(processInstance.getId()).count();
+        assertEquals(1, count);
+
+        task = taskService.createTaskQuery().singleResult();
+        taskService.complete(task.getId());
+        count = taskService.createTaskQuery().processInstanceId(processInstance.getId()).count();
+        assertEquals(1, count);
+
+        task = taskService.createTaskQuery().singleResult();
+        taskService.complete(task.getId());
+        count = taskService.createTaskQuery().processInstanceId(processInstance.getId()).count();
+        assertEquals(0, count);
+    }
+
+    /**
+     * 用户任务多实例--并行
+     */
+    @Test
+    @Deployment(resources = {"diagrams/chapter9/testMultiInstanceForUserTask.nosequential.bpmn"})
+    public void testForUserNoSequential() throws Exception {
+        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("testMultiInstanceForUserTask");
+        long count = taskService.createTaskQuery().processInstanceId(processInstance.getId()).count();
+        assertEquals(3, count);
+    }
+
+    /**
+     * 用户任务多实例,通过用户数量决定实例个数--并行
+     */
+    @Test
+    @Deployment(resources = {"diagrams/chapter9/testMultiInstanceForUserTask.users.nosequential.bpmn"})
+    public void testForUserCreateByUsersNoSequential() throws Exception {
+        Map<String, Object> variables = new HashMap<String, Object>();
+        List<String> users = Arrays.asList("user1", "user2", "user3");
+        variables.put("users", users);
+        runtimeService.startProcessInstanceByKey("testMultiInstanceForUserTask", variables);
+        for (String userId : users) {
+            assertEquals(1, taskService.createTaskQuery().taskAssignee(userId).count());
+        }
+    }
+
+    /**
+     * 用户任务多实例,通过用户数量决定实例个数--顺序
+     */
+    @Test
+    @Deployment(resources = {"diagrams/chapter9/testMultiInstanceForUserTask.users.sequential.bpmn"})
+    public void testForUserCreateByUsersSequential() throws Exception {
+        Map<String, Object> variables = new HashMap<String, Object>();
+        List<String> users = Arrays.asList("user1", "user2", "user3");
+        variables.put("users", users);
+        runtimeService.startProcessInstanceByKey("testMultiInstanceForUserTask", variables);
+        for (String userId : users) {
+            Task task = taskService.createTaskQuery().taskAssignee(userId).singleResult();
+            taskService.complete(task.getId());
+        }
+    }
+
+    /**
+     * 用户任务多实例,按照任务完成的百分比比率决定是否提前结束流程
+     */
+    @Test
+    @Deployment(resources = {"diagrams/chapter9/testMultiInstanceForUserTask.users.sequential.with.complete.conditon.bpmn"})
+    public void testForUserCreateByUsersSequentialWithCompleteCondition() throws Exception {
+        Map<String, Object> variables = new HashMap<String, Object>();
+        List<String> users = Arrays.asList("user1", "user2", "user3");
+        variables.put("users", users);
+        variables.put("rate", 0.6d);
+        runtimeService.startProcessInstanceByKey("testMultiInstanceForUserTask", variables);
+
+        Task task = taskService.createTaskQuery().taskAssignee("user1").singleResult();
+        taskService.complete(task.getId());
+
+        task = taskService.createTaskQuery().taskAssignee("user2").singleResult();
+        taskService.complete(task.getId());
+
+        long count = historyService.createHistoricProcessInstanceQuery().finished().count();
+        assertEquals(1, count);
+
+    }
+
+    /**
+     * 用户任务多实例,按照任务完成的百分比比率决定是否提前结束流程
+     */
+    @Test
+    @Deployment(resources = {"diagrams/chapter9/testMultiInstanceForUserTask.exception.bpmn"})
+    public void testForUserCreateByUsersException() throws Exception {
+        Map<String, Object> variables = new HashMap<String, Object>();
+        List<String> users = Arrays.asList("user1", "user2", "user3");
+        variables.put("users", users);
+        runtimeService.startProcessInstanceByKey("testMultiInstanceForUserTask", variables);
+
+        Task task = taskService.createTaskQuery().taskAssignee("user1").singleResult();
+        taskService.complete(task.getId());
+
+        task = taskService.createTaskQuery().taskAssignee("user2").singleResult();
+        taskService.complete(task.getId());
+
+        task = taskService.createTaskQuery().taskAssignee("user3").singleResult();
+        taskService.complete(task.getId());
+
+        List<Task> list = taskService.createTaskQuery().list();
+        for (Task task2 : list) {
+            System.out.println("============" + task2.getName());
+        }
+
+    }
+    /////////////////////////////////////////////////
+    /**
+     * 全部通过
+     */
+    @Test
+    @Deployment(resources = {"diagrams/chapter9/leave-countersign.bpmn"})
+    public void testAllApproved() throws Exception {
+        Map<String, Object> variables = new HashMap<String, Object>();
+        List<String> users = Arrays.asList("groupLeader", "deptLeader", "hr");
+        variables.put("users", users);
+        identityService.setAuthenticatedUserId("henryyan");
+        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("leave-countersign", variables);
+        for (String user : users) {
+            Task task = taskService.createTaskQuery().processInstanceId(processInstance.getId()).taskAssignee(user).singleResult();
+            Map<String, Object> taskVariables = new HashMap<String, Object>();
+            taskVariables.put("approved", "true");
+            taskService.complete(task.getId(), taskVariables);
+        }
+
+        Task task = taskService.createTaskQuery().taskAssignee("henryyan").singleResult();
+        assertNotNull(task);
+        assertEquals("销假", task.getName());
+    }
+
+    /**
+     * 部分通过
+     */
+    @Test
+    @Deployment(resources = {"diagrams/chapter9/leave-countersign.bpmn"})
+    public void testNotAllApproved() throws Exception {
+        Map<String, Object> variables = new HashMap<String, Object>();
+        List<String> users = Arrays.asList("groupLeader", "deptLeader", "hr");
+        variables.put("users", users);
+        identityService.setAuthenticatedUserId("henryyan");
+        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("leave-countersign", variables);
+        for (String user : users) {
+            Task task = taskService.createTaskQuery().processInstanceId(processInstance.getId()).taskAssignee(user).singleResult();
+            Map<String, Object> taskVariables = new HashMap<String, Object>();
+            taskVariables.put("approved", "false");
+            taskService.complete(task.getId(), taskVariables);
+        }
+
+        Task task = taskService.createTaskQuery().taskAssignee("henryyan").singleResult();
+        assertNotNull(task);
+        assertEquals("调整申请", task.getName());
+    }
+}

BIN
yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/chapter6/dynamic-form/leave-dynamic-form-with-javascript.zip


BIN
yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/chapter6/dynamic-form/leave-dynamic-form.zip


Різницю між файлами не показано, бо вона завелика
+ 198 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/chapter6/dynamic-form/leave.bpmn


BIN
yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/chapter6/dynamic-form/leave.png


+ 31 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/chapter6/leave-formkey/approve-deptLeader.form

@@ -0,0 +1,31 @@
+<div class="control-group">
+	<label class="control-label" for="startDate">申请人:</label>
+	<div class="controls">${applyUserId}</div>
+</div>
+<div class="control-group">
+	<label class="control-label" for="startDate">开始时间:</label>
+	<div class="controls">
+		<input type="text" id="startDate" name="startDate" value="${startDate}" readonly />
+	</div>
+</div>
+<div class="control-group">
+	<label class="control-label" for="endDate">结束时间:</label>
+	<div class="controls">
+		<input type="text" id="endDate" name="endDate" value="${endDate}" readonly />
+	</div>
+</div>
+<div class="control-group">
+	<label class="control-label" for="reason">请假原因:</label>
+	<div class="controls">
+		<textarea id="reason" name="reason" readonly>${reason}</textarea>
+	</div>
+</div>
+<div class="control-group">
+	<label class="control-label" for="deptLeaderApproved">审批意见:</label>
+	<div class="controls">
+		<select name="deptLeaderApproved" id="deptLeaderApproved">
+			<option value="true">同意</option>
+			<option value="false">拒绝</option>
+		</select>
+	</div>
+</div>

+ 31 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/chapter6/leave-formkey/approve-hr.form

@@ -0,0 +1,31 @@
+<div class="control-group">
+	<label class="control-label" for="startDate">申请人:</label>
+	<div class="controls">${applyUserId}</div>
+</div>
+<div class="control-group">
+	<label class="control-label" for="startDate">开始时间:</label>
+	<div class="controls">
+		<input type="text" id="startDate" name="startDate" value="${startDate}" readonly />
+	</div>
+</div>
+<div class="control-group">
+	<label class="control-label" for="endDate">结束时间:</label>
+	<div class="controls">
+		<input type="text" id="endDate" name="endDate" value="${endDate}" readonly />
+	</div>
+</div>
+<div class="control-group">
+	<label class="control-label" for="reason">请假原因:</label>
+	<div class="controls">
+		<textarea id="reason" name="reason" readonly>${reason}</textarea>
+	</div>
+</div>
+<div class="control-group">
+	<label class="control-label" for="hrApproved">审批意见:</label>
+	<div class="controls">
+		<select name="hrApproved" id="hrApproved">
+			<option value="true">同意</option>
+			<option value="false">拒绝</option>
+		</select>
+	</div>
+</div>

Різницю між файлами не показано, бо вона завелика
+ 151 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/chapter6/leave-formkey/leave-formkey.bpmn


BIN
yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/chapter6/leave-formkey/leave-formkey.png


BIN
yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/chapter6/leave-formkey/leave-formkey.zip


+ 18 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/chapter6/leave-formkey/leave-start.form

@@ -0,0 +1,18 @@
+<div class="control-group">
+	<label class="control-label" for="startDate">开始时间:</label>
+	<div class="controls">
+		<input type="text" id="startDate" name="startDate" class="datepicker" data-date-format="yyyy-mm-dd" required />
+	</div>
+</div>
+<div class="control-group">
+	<label class="control-label" for="endDate">结束时间:</label>
+	<div class="controls">
+		<input type="text" id="endDate" name="endDate" class="datepicker" data-date-format="yyyy-mm-dd" required />
+	</div>
+</div>
+<div class="control-group">
+	<label class="control-label" for="reason">请假原因:</label>
+	<div class="controls">
+		<textarea id="reason" name="reason" required></textarea>
+	</div>
+</div>

+ 27 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/chapter6/leave-formkey/modify-apply.form

@@ -0,0 +1,27 @@
+<div class="control-group">
+	<label class="control-label" for="startDate">开始时间:</label>
+	<div class="controls">
+		<input type="text" id="startDate" name="startDate" value="${startDate}" class="datepicker" data-date-format="yyyy-mm-dd" required />
+	</div>
+</div>
+<div class="control-group">
+	<label class="control-label" for="endDate">结束时间:</label>
+	<div class="controls">
+		<input type="text" id="endDate" name="endDate" value="${endDate}" class="datepicker" data-date-format="yyyy-mm-dd" required />
+	</div>
+</div>
+<div class="control-group">
+	<label class="control-label" for="reason">请假原因:</label>
+	<div class="controls">
+		<textarea id="reason" name="reason" required>${reason}</textarea>
+	</div>
+</div>
+<div class="control-group">
+	<label class="control-label" for="reason">是否继续申请:</label>
+	<div class="controls">
+		<select id="reApply" name="reApply">
+			<option value='true'>重新申请</option>
+			<option value='false'>结束流程</option>
+		</select>
+	</div>
+</div>

+ 28 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/chapter6/leave-formkey/report-back.form

@@ -0,0 +1,28 @@
+<div class="control-group">
+	<label class="control-label" for="startDate">申请人:</label>
+	<div class="controls">${applyUserId}</div>
+</div>
+<div class="control-group">
+	<label class="control-label" for="startDate">开始时间:</label>
+	<div class="controls">
+		<input type="text" id="startDate" name="startDate" value="${startDate}" readonly />
+	</div>
+</div>
+<div class="control-group">
+	<label class="control-label" for="endDate">结束时间:</label>
+	<div class="controls">
+		<input type="text" id="endDate" name="endDate" value="${endDate}" readonly />
+	</div>
+</div>
+<div class="control-group">
+	<label class="control-label" for="reason">请假原因:</label>
+	<div class="controls">
+		<textarea id="reason" name="reason" readonly>${reason}</textarea>
+	</div>
+</div>
+<div class="control-group">
+	<label class="control-label" for="reportBackDate">销假日期:</label>
+	<div class="controls">
+		<input type="text" id="reportBackDate" name="reportBackDate" class="datepicker" data-date-format="yyyy-mm-dd" required />
+	</div>
+</div>

+ 31 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/chapter6/leave-timeLimit-money/approve-deptLeader.form

@@ -0,0 +1,31 @@
+<div class="control-group">
+	<label class="control-label" for="startDate">申请人:</label>
+	<div class="controls">${applyUserId}</div>
+</div>
+<div class="control-group">
+	<label class="control-label" for="startDate">开始时间:</label>
+	<div class="controls">
+		<input type="text" id="startDate" name="startDate" value="${startDate}" readonly />
+	</div>
+</div>
+<div class="control-group">
+	<label class="control-label" for="endDate">结束时间:</label>
+	<div class="controls">
+		<input type="text" id="endDate" name="endDate" value="${endDate}" readonly />
+	</div>
+</div>
+<div class="control-group">
+	<label class="control-label" for="reason">请假原因:</label>
+	<div class="controls">
+		<textarea id="reason" name="reason" readonly>${reason}</textarea>
+	</div>
+</div>
+<div class="control-group">
+	<label class="control-label" for="deptLeaderApproved">审批意见:</label>
+	<div class="controls">
+		<select name="deptLeaderApproved" id="deptLeaderApproved">
+			<option value="true">同意</option>
+			<option value="false">拒绝</option>
+		</select>
+	</div>
+</div>

+ 31 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/chapter6/leave-timeLimit-money/approve-hr.form

@@ -0,0 +1,31 @@
+<div class="control-group">
+	<label class="control-label" for="startDate">申请人:</label>
+	<div class="controls">${applyUserId}</div>
+</div>
+<div class="control-group">
+	<label class="control-label" for="startDate">开始时间:</label>
+	<div class="controls">
+		<input type="text" id="startDate" name="startDate" value="${startDate}" readonly />
+	</div>
+</div>
+<div class="control-group">
+	<label class="control-label" for="endDate">结束时间:</label>
+	<div class="controls">
+		<input type="text" id="endDate" name="endDate" value="${endDate}" readonly />
+	</div>
+</div>
+<div class="control-group">
+	<label class="control-label" for="reason">请假原因:</label>
+	<div class="controls">
+		<textarea id="reason" name="reason" readonly>${reason}</textarea>
+	</div>
+</div>
+<div class="control-group">
+	<label class="control-label" for="hrApproved">审批意见:</label>
+	<div class="controls">
+		<select name="hrApproved" id="hrApproved">
+			<option value="true">同意</option>
+			<option value="false">拒绝</option>
+		</select>
+	</div>
+</div>

Різницю між файлами не показано, бо вона завелика
+ 188 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/chapter6/leave-timeLimit-money/leave-formkey-ext.bpmn


BIN
yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/chapter6/leave-timeLimit-money/leave-formkey-ext.png


+ 18 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/chapter6/leave-timeLimit-money/leave-start.form

@@ -0,0 +1,18 @@
+<div class="control-group">
+	<label class="control-label" for="startDate">开始时间:</label>
+	<div class="controls">
+		<input type="text" id="startDate" name="startDate" class="datepicker" data-date-format="yyyy-mm-dd" required />
+	</div>
+</div>
+<div class="control-group">
+	<label class="control-label" for="endDate">结束时间:</label>
+	<div class="controls">
+		<input type="text" id="endDate" name="endDate" class="datepicker" data-date-format="yyyy-mm-dd" required />
+	</div>
+</div>
+<div class="control-group">
+	<label class="control-label" for="reason">请假原因:</label>
+	<div class="controls">
+		<textarea id="reason" name="reason" required></textarea>
+	</div>
+</div>

+ 27 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/chapter6/leave-timeLimit-money/modify-apply.form

@@ -0,0 +1,27 @@
+<div class="control-group">
+	<label class="control-label" for="startDate">开始时间:</label>
+	<div class="controls">
+		<input type="text" id="startDate" name="startDate" value="${startDate}" class="datepicker" data-date-format="yyyy-mm-dd" required />
+	</div>
+</div>
+<div class="control-group">
+	<label class="control-label" for="endDate">结束时间:</label>
+	<div class="controls">
+		<input type="text" id="endDate" name="endDate" value="${endDate}" class="datepicker" data-date-format="yyyy-mm-dd" required />
+	</div>
+</div>
+<div class="control-group">
+	<label class="control-label" for="reason">请假原因:</label>
+	<div class="controls">
+		<textarea id="reason" name="reason" required>${reason}</textarea>
+	</div>
+</div>
+<div class="control-group">
+	<label class="control-label" for="reason">是否继续申请:</label>
+	<div class="controls">
+		<select id="reApply" name="reApply">
+			<option value='true'>重新申请</option>
+			<option value='false'>结束流程</option>
+		</select>
+	</div>
+</div>

+ 28 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/chapter6/leave-timeLimit-money/report-back.form

@@ -0,0 +1,28 @@
+<div class="control-group">
+	<label class="control-label" for="startDate">申请人:</label>
+	<div class="controls">${applyUserId}</div>
+</div>
+<div class="control-group">
+	<label class="control-label" for="startDate">开始时间:</label>
+	<div class="controls">
+		<input type="text" id="startDate" name="startDate" value="${startDate}" readonly />
+	</div>
+</div>
+<div class="control-group">
+	<label class="control-label" for="endDate">结束时间:</label>
+	<div class="controls">
+		<input type="text" id="endDate" name="endDate" value="${endDate}" readonly />
+	</div>
+</div>
+<div class="control-group">
+	<label class="control-label" for="reason">请假原因:</label>
+	<div class="controls">
+		<textarea id="reason" name="reason" readonly>${reason}</textarea>
+	</div>
+</div>
+<div class="control-group">
+	<label class="control-label" for="reportBackDate">销假日期:</label>
+	<div class="controls">
+		<input type="text" id="reportBackDate" name="reportBackDate" class="datepicker" data-date-format="yyyy-mm-dd" required />
+	</div>
+</div>

+ 152 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/diagrams/chapter9/leave-countersign.bpmn

@@ -0,0 +1,152 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xmlns:activiti="http://activiti.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC"
+	xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath"
+	targetNamespace="http://www.activiti.org/test">
+	<process id="leave-countersign" name="请假流程-会签" isExecutable="true">
+		<documentation>请假流程演示-会签</documentation>
+		<startEvent id="startevent1" name="Start" activiti:initiator="applyUserId">
+			<extensionElements>
+				<activiti:formProperty id="startDate" name="请假开始日期" type="date" datePattern="yyyy-MM-dd" required="true"></activiti:formProperty>
+				<activiti:formProperty id="endDate" name="请假结束日期" type="date" datePattern="yyyy-MM-dd" required="true"></activiti:formProperty>
+				<activiti:formProperty id="reason" name="请假原因" type="string" required="true"></activiti:formProperty>
+				<activiti:formProperty id="users" name="审批参与人" type="users" required="true"></activiti:formProperty>
+				<activiti:formProperty id="validScript" type="javascript" default="alert('表单已经加载完毕');"></activiti:formProperty>
+			</extensionElements>
+		</startEvent>
+		<userTask id="countersign" name="[部门/人事]联合会签" activiti:assignee="${user}">
+			<extensionElements>
+				<activiti:formProperty id="startDate" name="请假开始日期" type="date" datePattern="yyyy-MM-dd" writable="false"></activiti:formProperty>
+				<activiti:formProperty id="endDate" name="请假结束日期" type="date" datePattern="yyyy-MM-dd" writable="false"></activiti:formProperty>
+				<activiti:formProperty id="reason" name="请假原因" type="string" writable="false"></activiti:formProperty>
+				<activiti:formProperty id="approved" name="审批意见" type="enum" required="true">
+					<activiti:value id="true" name="同意"></activiti:value>
+					<activiti:value id="false" name="拒绝"></activiti:value>
+				</activiti:formProperty>
+				<activiti:taskListener event="complete" delegateExpression="${leaveCounterSignCompleteListener}"></activiti:taskListener>
+			</extensionElements>
+			<multiInstanceLoopCharacteristics isSequential="false" activiti:collection="users"
+				activiti:elementVariable="user"></multiInstanceLoopCharacteristics>
+		</userTask>
+		<userTask id="reportBack" name="销假" activiti:assignee="${applyUserId}">
+			<extensionElements>
+				<activiti:formProperty id="startDate" name="请假开始日期" type="date" datePattern="yyyy-MM-dd" writable="false"></activiti:formProperty>
+				<activiti:formProperty id="endDate" name="请假结束日期" type="date" datePattern="yyyy-MM-dd" writable="false"></activiti:formProperty>
+				<activiti:formProperty id="reason" name="请假原因" type="string" writable="false"></activiti:formProperty>
+				<activiti:formProperty id="reportBackDate" name="销假日期" type="date" default="${endDate}" datePattern="yyyy-MM-dd"
+					required="true"></activiti:formProperty>
+			</extensionElements>
+		</userTask>
+		<endEvent id="endevent1" name="End"></endEvent>
+		<sequenceFlow id="flow2" sourceRef="startevent1" targetRef="countersign">
+			<extensionElements>
+				<activiti:executionListener event="take" expression="${execution.setVariable('approvedCounter', 0)}"></activiti:executionListener>
+			</extensionElements>
+		</sequenceFlow>
+		<sequenceFlow id="flow8" name="销假" sourceRef="reportBack" targetRef="endevent1">
+			<extensionElements>
+				<activiti:executionListener event="take" expression="${execution.setVariable('result', 'ok')}"></activiti:executionListener>
+			</extensionElements>
+		</sequenceFlow>
+		<sequenceFlow id="flow13" name="全部通过" sourceRef="exclusivegateway1" targetRef="reportBack">
+			<conditionExpression xsi:type="tFormalExpression"><![CDATA[${approvedCounter == users.size()}]]></conditionExpression>
+		</sequenceFlow>
+		<exclusiveGateway id="exclusivegateway1" name="Exclusive Gateway"></exclusiveGateway>
+		<sequenceFlow id="flow14" sourceRef="countersign" targetRef="exclusivegateway1"></sequenceFlow>
+		<userTask id="modifyApply" name="调整申请" activiti:assignee="${applyUserId}">
+			<extensionElements>
+				<activiti:formProperty id="startDate" name="请假开始日期" type="date" datePattern="yyyy-MM-dd" required="true"></activiti:formProperty>
+				<activiti:formProperty id="endDate" name="请假结束日期" type="date" datePattern="yyyy-MM-dd" required="true"></activiti:formProperty>
+				<activiti:formProperty id="reason" name="请假原因" type="string" required="true"></activiti:formProperty>
+				<activiti:formProperty id="reApply" name="重新申请" type="enum" required="true">
+					<activiti:value id="true" name="重新申请"></activiti:value>
+					<activiti:value id="false" name="取消申请"></activiti:value>
+				</activiti:formProperty>
+			</extensionElements>
+		</userTask>
+		<sequenceFlow id="flow15" name="部分通过" sourceRef="exclusivegateway1" targetRef="modifyApply">
+			<conditionExpression xsi:type="tFormalExpression"><![CDATA[${approvedCounter < users.size()}]]></conditionExpression>
+		</sequenceFlow>
+		<exclusiveGateway id="exclusivegateway2" name="Exclusive Gateway"></exclusiveGateway>
+		<sequenceFlow id="flow16" sourceRef="modifyApply" targetRef="exclusivegateway2"></sequenceFlow>
+		<sequenceFlow id="flow17" sourceRef="exclusivegateway2" targetRef="endevent1">
+			<conditionExpression xsi:type="tFormalExpression"><![CDATA[${reApply == 'false'}]]></conditionExpression>
+		</sequenceFlow>
+		<sequenceFlow id="flow18" name="重新申请" sourceRef="exclusivegateway2" targetRef="countersign">
+			<extensionElements>
+				<activiti:executionListener event="take" expression="${execution.setVariable('approvedCounter', 0)}"></activiti:executionListener>
+			</extensionElements>
+			<conditionExpression xsi:type="tFormalExpression"><![CDATA[${reApply == 'true'}]]></conditionExpression>
+		</sequenceFlow>
+	</process>
+	<bpmndi:BPMNDiagram id="BPMNDiagram_leave-countersign">
+		<bpmndi:BPMNPlane bpmnElement="leave-countersign" id="BPMNPlane_leave-countersign">
+			<bpmndi:BPMNShape bpmnElement="startevent1" id="BPMNShape_startevent1">
+				<omgdc:Bounds height="35.0" width="35.0" x="10.0" y="30.0"></omgdc:Bounds>
+			</bpmndi:BPMNShape>
+			<bpmndi:BPMNShape bpmnElement="reportBack" id="BPMNShape_reportBack">
+				<omgdc:Bounds height="55.0" width="105.0" x="340.0" y="20.0"></omgdc:Bounds>
+			</bpmndi:BPMNShape>
+			<bpmndi:BPMNShape bpmnElement="endevent1" id="BPMNShape_endevent1">
+				<omgdc:Bounds height="35.0" width="35.0" x="375.0" y="203.0"></omgdc:Bounds>
+			</bpmndi:BPMNShape>
+			<bpmndi:BPMNShape bpmnElement="countersign" id="BPMNShape_countersign">
+				<omgdc:Bounds height="55.0" width="105.0" x="90.0" y="20.0"></omgdc:Bounds>
+			</bpmndi:BPMNShape>
+			<bpmndi:BPMNShape bpmnElement="exclusivegateway1" id="BPMNShape_exclusivegateway1">
+				<omgdc:Bounds height="40.0" width="40.0" x="220.0" y="27.0"></omgdc:Bounds>
+			</bpmndi:BPMNShape>
+			<bpmndi:BPMNShape bpmnElement="modifyApply" id="BPMNShape_modifyApply">
+				<omgdc:Bounds height="55.0" width="105.0" x="188.0" y="110.0"></omgdc:Bounds>
+			</bpmndi:BPMNShape>
+			<bpmndi:BPMNShape bpmnElement="exclusivegateway2" id="BPMNShape_exclusivegateway2">
+				<omgdc:Bounds height="40.0" width="40.0" x="220.0" y="200.0"></omgdc:Bounds>
+			</bpmndi:BPMNShape>
+			<bpmndi:BPMNEdge bpmnElement="flow2" id="BPMNEdge_flow2">
+				<omgdi:waypoint x="45.0" y="47.0"></omgdi:waypoint>
+				<omgdi:waypoint x="90.0" y="47.0"></omgdi:waypoint>
+			</bpmndi:BPMNEdge>
+			<bpmndi:BPMNEdge bpmnElement="flow8" id="BPMNEdge_flow8">
+				<omgdi:waypoint x="392.0" y="75.0"></omgdi:waypoint>
+				<omgdi:waypoint x="392.0" y="203.0"></omgdi:waypoint>
+				<bpmndi:BPMNLabel>
+					<omgdc:Bounds height="11.0" width="100.0" x="-22.0" y="-17.0"></omgdc:Bounds>
+				</bpmndi:BPMNLabel>
+			</bpmndi:BPMNEdge>
+			<bpmndi:BPMNEdge bpmnElement="flow13" id="BPMNEdge_flow13">
+				<omgdi:waypoint x="260.0" y="47.0"></omgdi:waypoint>
+				<omgdi:waypoint x="340.0" y="47.0"></omgdi:waypoint>
+				<bpmndi:BPMNLabel>
+					<omgdc:Bounds height="11.0" width="100.0" x="-29.0" y="-17.0"></omgdc:Bounds>
+				</bpmndi:BPMNLabel>
+			</bpmndi:BPMNEdge>
+			<bpmndi:BPMNEdge bpmnElement="flow14" id="BPMNEdge_flow14">
+				<omgdi:waypoint x="195.0" y="47.0"></omgdi:waypoint>
+				<omgdi:waypoint x="220.0" y="47.0"></omgdi:waypoint>
+			</bpmndi:BPMNEdge>
+			<bpmndi:BPMNEdge bpmnElement="flow15" id="BPMNEdge_flow15">
+				<omgdi:waypoint x="240.0" y="67.0"></omgdi:waypoint>
+				<omgdi:waypoint x="240.0" y="110.0"></omgdi:waypoint>
+				<bpmndi:BPMNLabel>
+					<omgdc:Bounds height="11.0" width="100.0" x="10.0" y="0.0"></omgdc:Bounds>
+				</bpmndi:BPMNLabel>
+			</bpmndi:BPMNEdge>
+			<bpmndi:BPMNEdge bpmnElement="flow16" id="BPMNEdge_flow16">
+				<omgdi:waypoint x="240.0" y="165.0"></omgdi:waypoint>
+				<omgdi:waypoint x="240.0" y="200.0"></omgdi:waypoint>
+			</bpmndi:BPMNEdge>
+			<bpmndi:BPMNEdge bpmnElement="flow17" id="BPMNEdge_flow17">
+				<omgdi:waypoint x="260.0" y="220.0"></omgdi:waypoint>
+				<omgdi:waypoint x="375.0" y="220.0"></omgdi:waypoint>
+			</bpmndi:BPMNEdge>
+			<bpmndi:BPMNEdge bpmnElement="flow18" id="BPMNEdge_flow18">
+				<omgdi:waypoint x="220.0" y="220.0"></omgdi:waypoint>
+				<omgdi:waypoint x="142.0" y="220.0"></omgdi:waypoint>
+				<omgdi:waypoint x="142.0" y="75.0"></omgdi:waypoint>
+				<bpmndi:BPMNLabel>
+					<omgdc:Bounds height="11.0" width="100.0" x="9.0" y="14.0"></omgdc:Bounds>
+				</bpmndi:BPMNLabel>
+			</bpmndi:BPMNEdge>
+		</bpmndi:BPMNPlane>
+	</bpmndi:BPMNDiagram>
+</definitions>

BIN
yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/diagrams/chapter9/leave-countersign.png


+ 44 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/diagrams/chapter9/testMultiInstanceFixedNumbers.bpmn

@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:activiti="http://activiti.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.activiti.org/test">
+  <process id="testMultiInstanceFixedNumbers" name="testMultiInstanceFixedNumbers" isExecutable="true">
+    <startEvent id="startevent1" name="Start"></startEvent>
+    <serviceTask id="servicetask1" name="Service Task" activiti:expression="${counter + 1}" activiti:resultVariableName="counter">
+      <multiInstanceLoopCharacteristics isSequential="false">
+        <loopCardinality>${loop}</loopCardinality>
+      </multiInstanceLoopCharacteristics>
+    </serviceTask>
+    <sequenceFlow id="flow1" sourceRef="startevent1" targetRef="servicetask1"></sequenceFlow>
+    <endEvent id="endevent1" name="End"></endEvent>
+    <sequenceFlow id="flow2" sourceRef="receivetask1" targetRef="endevent1"></sequenceFlow>
+    <receiveTask id="receivetask1" name="Receive Task"></receiveTask>
+    <sequenceFlow id="flow3" sourceRef="servicetask1" targetRef="receivetask1"></sequenceFlow>
+  </process>
+  <bpmndi:BPMNDiagram id="BPMNDiagram_testMultiInstanceFixedNumbers">
+    <bpmndi:BPMNPlane bpmnElement="testMultiInstanceFixedNumbers" id="BPMNPlane_testMultiInstanceFixedNumbers">
+      <bpmndi:BPMNShape bpmnElement="startevent1" id="BPMNShape_startevent1">
+        <omgdc:Bounds height="35.0" width="35.0" x="50.0" y="50.0"></omgdc:Bounds>
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNShape bpmnElement="servicetask1" id="BPMNShape_servicetask1">
+        <omgdc:Bounds height="55.0" width="105.0" x="116.0" y="40.0"></omgdc:Bounds>
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNShape bpmnElement="endevent1" id="BPMNShape_endevent1">
+        <omgdc:Bounds height="35.0" width="35.0" x="410.0" y="50.0"></omgdc:Bounds>
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNShape bpmnElement="receivetask1" id="BPMNShape_receivetask1">
+        <omgdc:Bounds height="55.0" width="105.0" x="260.0" y="40.0"></omgdc:Bounds>
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNEdge bpmnElement="flow1" id="BPMNEdge_flow1">
+        <omgdi:waypoint x="85.0" y="67.0"></omgdi:waypoint>
+        <omgdi:waypoint x="116.0" y="67.0"></omgdi:waypoint>
+      </bpmndi:BPMNEdge>
+      <bpmndi:BPMNEdge bpmnElement="flow2" id="BPMNEdge_flow2">
+        <omgdi:waypoint x="365.0" y="67.0"></omgdi:waypoint>
+        <omgdi:waypoint x="410.0" y="67.0"></omgdi:waypoint>
+      </bpmndi:BPMNEdge>
+      <bpmndi:BPMNEdge bpmnElement="flow3" id="BPMNEdge_flow3">
+        <omgdi:waypoint x="221.0" y="67.0"></omgdi:waypoint>
+        <omgdi:waypoint x="260.0" y="67.0"></omgdi:waypoint>
+      </bpmndi:BPMNEdge>
+    </bpmndi:BPMNPlane>
+  </bpmndi:BPMNDiagram>
+</definitions>

+ 42 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/diagrams/chapter9/testMultiInstanceForUserTask.exception.bpmn

@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:activiti="http://activiti.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.activiti.org/test">
+  <process id="testMultiInstanceForUserTask" name="testMultiInstanceForUserTask" isExecutable="true">
+    <startEvent id="startevent1" name="Start"></startEvent>
+    <userTask id="usertask1" name="User Task" activiti:assignee="${user}">
+      <multiInstanceLoopCharacteristics isSequential="false" activiti:collection="${users}" activiti:elementVariable="user"></multiInstanceLoopCharacteristics>
+    </userTask>
+    <sequenceFlow id="flow1" sourceRef="startevent1" targetRef="usertask1"></sequenceFlow>
+    <endEvent id="endevent1" name="End"></endEvent>
+    <sequenceFlow id="flow2" sourceRef="usertask2" targetRef="endevent1"></sequenceFlow>
+    <userTask id="usertask2" name="User Task2"></userTask>
+    <sequenceFlow id="flow3" sourceRef="usertask1" targetRef="usertask2"></sequenceFlow>
+  </process>
+  <bpmndi:BPMNDiagram id="BPMNDiagram_testMultiInstanceForUserTask">
+    <bpmndi:BPMNPlane bpmnElement="testMultiInstanceForUserTask" id="BPMNPlane_testMultiInstanceForUserTask">
+      <bpmndi:BPMNShape bpmnElement="startevent1" id="BPMNShape_startevent1">
+        <omgdc:Bounds height="35.0" width="35.0" x="90.0" y="80.0"></omgdc:Bounds>
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNShape bpmnElement="usertask1" id="BPMNShape_usertask1">
+        <omgdc:Bounds height="55.0" width="105.0" x="170.0" y="70.0"></omgdc:Bounds>
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNShape bpmnElement="endevent1" id="BPMNShape_endevent1">
+        <omgdc:Bounds height="35.0" width="35.0" x="340.0" y="190.0"></omgdc:Bounds>
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNShape bpmnElement="usertask2" id="BPMNShape_usertask2">
+        <omgdc:Bounds height="55.0" width="105.0" x="170.0" y="180.0"></omgdc:Bounds>
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNEdge bpmnElement="flow1" id="BPMNEdge_flow1">
+        <omgdi:waypoint x="125.0" y="97.0"></omgdi:waypoint>
+        <omgdi:waypoint x="170.0" y="97.0"></omgdi:waypoint>
+      </bpmndi:BPMNEdge>
+      <bpmndi:BPMNEdge bpmnElement="flow2" id="BPMNEdge_flow2">
+        <omgdi:waypoint x="275.0" y="207.0"></omgdi:waypoint>
+        <omgdi:waypoint x="340.0" y="207.0"></omgdi:waypoint>
+      </bpmndi:BPMNEdge>
+      <bpmndi:BPMNEdge bpmnElement="flow3" id="BPMNEdge_flow3">
+        <omgdi:waypoint x="222.0" y="125.0"></omgdi:waypoint>
+        <omgdi:waypoint x="222.0" y="180.0"></omgdi:waypoint>
+      </bpmndi:BPMNEdge>
+    </bpmndi:BPMNPlane>
+  </bpmndi:BPMNDiagram>
+</definitions>

BIN
yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/diagrams/chapter9/testMultiInstanceForUserTask.exception.png


+ 35 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/diagrams/chapter9/testMultiInstanceForUserTask.nosequential.bpmn

@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:activiti="http://activiti.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.activiti.org/test">
+  <process id="testMultiInstanceForUserTask" name="testMultiInstanceForUserTask" isExecutable="true">
+    <startEvent id="startevent1" name="Start"></startEvent>
+    <userTask id="usertask1" name="User Task">
+      <multiInstanceLoopCharacteristics isSequential="false">
+        <loopCardinality>3</loopCardinality>
+      </multiInstanceLoopCharacteristics>
+    </userTask>
+    <sequenceFlow id="flow1" sourceRef="startevent1" targetRef="usertask1"></sequenceFlow>
+    <endEvent id="endevent1" name="End"></endEvent>
+    <sequenceFlow id="flow2" sourceRef="usertask1" targetRef="endevent1"></sequenceFlow>
+  </process>
+  <bpmndi:BPMNDiagram id="BPMNDiagram_testMultiInstanceForUserTask">
+    <bpmndi:BPMNPlane bpmnElement="testMultiInstanceForUserTask" id="BPMNPlane_testMultiInstanceForUserTask">
+      <bpmndi:BPMNShape bpmnElement="startevent1" id="BPMNShape_startevent1">
+        <omgdc:Bounds height="35.0" width="35.0" x="90.0" y="80.0"></omgdc:Bounds>
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNShape bpmnElement="usertask1" id="BPMNShape_usertask1">
+        <omgdc:Bounds height="55.0" width="105.0" x="170.0" y="70.0"></omgdc:Bounds>
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNShape bpmnElement="endevent1" id="BPMNShape_endevent1">
+        <omgdc:Bounds height="35.0" width="35.0" x="330.0" y="80.0"></omgdc:Bounds>
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNEdge bpmnElement="flow1" id="BPMNEdge_flow1">
+        <omgdi:waypoint x="125.0" y="97.0"></omgdi:waypoint>
+        <omgdi:waypoint x="170.0" y="97.0"></omgdi:waypoint>
+      </bpmndi:BPMNEdge>
+      <bpmndi:BPMNEdge bpmnElement="flow2" id="BPMNEdge_flow2">
+        <omgdi:waypoint x="275.0" y="97.0"></omgdi:waypoint>
+        <omgdi:waypoint x="330.0" y="97.0"></omgdi:waypoint>
+      </bpmndi:BPMNEdge>
+    </bpmndi:BPMNPlane>
+  </bpmndi:BPMNDiagram>
+</definitions>

+ 35 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/diagrams/chapter9/testMultiInstanceForUserTask.sequential.bpmn

@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:activiti="http://activiti.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.activiti.org/test">
+  <process id="testMultiInstanceForUserTask" name="testMultiInstanceForUserTask" isExecutable="true">
+    <startEvent id="startevent1" name="Start"></startEvent>
+    <userTask id="usertask1" name="User Task">
+      <multiInstanceLoopCharacteristics isSequential="true">
+        <loopCardinality>3</loopCardinality>
+      </multiInstanceLoopCharacteristics>
+    </userTask>
+    <sequenceFlow id="flow1" sourceRef="startevent1" targetRef="usertask1"></sequenceFlow>
+    <endEvent id="endevent1" name="End"></endEvent>
+    <sequenceFlow id="flow2" sourceRef="usertask1" targetRef="endevent1"></sequenceFlow>
+  </process>
+  <bpmndi:BPMNDiagram id="BPMNDiagram_testMultiInstanceForUserTask">
+    <bpmndi:BPMNPlane bpmnElement="testMultiInstanceForUserTask" id="BPMNPlane_testMultiInstanceForUserTask">
+      <bpmndi:BPMNShape bpmnElement="startevent1" id="BPMNShape_startevent1">
+        <omgdc:Bounds height="35.0" width="35.0" x="90.0" y="80.0"></omgdc:Bounds>
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNShape bpmnElement="usertask1" id="BPMNShape_usertask1">
+        <omgdc:Bounds height="55.0" width="105.0" x="170.0" y="70.0"></omgdc:Bounds>
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNShape bpmnElement="endevent1" id="BPMNShape_endevent1">
+        <omgdc:Bounds height="35.0" width="35.0" x="330.0" y="80.0"></omgdc:Bounds>
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNEdge bpmnElement="flow1" id="BPMNEdge_flow1">
+        <omgdi:waypoint x="125.0" y="97.0"></omgdi:waypoint>
+        <omgdi:waypoint x="170.0" y="97.0"></omgdi:waypoint>
+      </bpmndi:BPMNEdge>
+      <bpmndi:BPMNEdge bpmnElement="flow2" id="BPMNEdge_flow2">
+        <omgdi:waypoint x="275.0" y="97.0"></omgdi:waypoint>
+        <omgdi:waypoint x="330.0" y="97.0"></omgdi:waypoint>
+      </bpmndi:BPMNEdge>
+    </bpmndi:BPMNPlane>
+  </bpmndi:BPMNDiagram>
+</definitions>

+ 33 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/diagrams/chapter9/testMultiInstanceForUserTask.users.nosequential.bpmn

@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:activiti="http://activiti.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.activiti.org/test">
+  <process id="testMultiInstanceForUserTask" name="testMultiInstanceForUserTask" isExecutable="true">
+    <startEvent id="startevent1" name="Start"></startEvent>
+    <userTask id="usertask1" name="User Task" activiti:assignee="${user}">
+      <multiInstanceLoopCharacteristics isSequential="false" activiti:collection="${users}" activiti:elementVariable="user"></multiInstanceLoopCharacteristics>
+    </userTask>
+    <sequenceFlow id="flow1" sourceRef="startevent1" targetRef="usertask1"></sequenceFlow>
+    <endEvent id="endevent1" name="End"></endEvent>
+    <sequenceFlow id="flow2" sourceRef="usertask1" targetRef="endevent1"></sequenceFlow>
+  </process>
+  <bpmndi:BPMNDiagram id="BPMNDiagram_testMultiInstanceForUserTask">
+    <bpmndi:BPMNPlane bpmnElement="testMultiInstanceForUserTask" id="BPMNPlane_testMultiInstanceForUserTask">
+      <bpmndi:BPMNShape bpmnElement="startevent1" id="BPMNShape_startevent1">
+        <omgdc:Bounds height="35.0" width="35.0" x="50.0" y="90.0"></omgdc:Bounds>
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNShape bpmnElement="usertask1" id="BPMNShape_usertask1">
+        <omgdc:Bounds height="55.0" width="105.0" x="130.0" y="80.0"></omgdc:Bounds>
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNShape bpmnElement="endevent1" id="BPMNShape_endevent1">
+        <omgdc:Bounds height="35.0" width="35.0" x="290.0" y="90.0"></omgdc:Bounds>
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNEdge bpmnElement="flow1" id="BPMNEdge_flow1">
+        <omgdi:waypoint x="85.0" y="107.0"></omgdi:waypoint>
+        <omgdi:waypoint x="130.0" y="107.0"></omgdi:waypoint>
+      </bpmndi:BPMNEdge>
+      <bpmndi:BPMNEdge bpmnElement="flow2" id="BPMNEdge_flow2">
+        <omgdi:waypoint x="235.0" y="107.0"></omgdi:waypoint>
+        <omgdi:waypoint x="290.0" y="107.0"></omgdi:waypoint>
+      </bpmndi:BPMNEdge>
+    </bpmndi:BPMNPlane>
+  </bpmndi:BPMNDiagram>
+</definitions>

+ 33 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/diagrams/chapter9/testMultiInstanceForUserTask.users.sequential.bpmn

@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:activiti="http://activiti.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.activiti.org/test">
+  <process id="testMultiInstanceForUserTask" name="testMultiInstanceForUserTask" isExecutable="true">
+    <startEvent id="startevent1" name="Start"></startEvent>
+    <userTask id="usertask1" name="User Task" activiti:assignee="${user}">
+      <multiInstanceLoopCharacteristics isSequential="true" activiti:collection="${users}" activiti:elementVariable="user"></multiInstanceLoopCharacteristics>
+    </userTask>
+    <sequenceFlow id="flow1" sourceRef="startevent1" targetRef="usertask1"></sequenceFlow>
+    <endEvent id="endevent1" name="End"></endEvent>
+    <sequenceFlow id="flow2" sourceRef="usertask1" targetRef="endevent1"></sequenceFlow>
+  </process>
+  <bpmndi:BPMNDiagram id="BPMNDiagram_testMultiInstanceForUserTask">
+    <bpmndi:BPMNPlane bpmnElement="testMultiInstanceForUserTask" id="BPMNPlane_testMultiInstanceForUserTask">
+      <bpmndi:BPMNShape bpmnElement="startevent1" id="BPMNShape_startevent1">
+        <omgdc:Bounds height="35.0" width="35.0" x="50.0" y="90.0"></omgdc:Bounds>
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNShape bpmnElement="usertask1" id="BPMNShape_usertask1">
+        <omgdc:Bounds height="55.0" width="105.0" x="130.0" y="80.0"></omgdc:Bounds>
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNShape bpmnElement="endevent1" id="BPMNShape_endevent1">
+        <omgdc:Bounds height="35.0" width="35.0" x="290.0" y="90.0"></omgdc:Bounds>
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNEdge bpmnElement="flow1" id="BPMNEdge_flow1">
+        <omgdi:waypoint x="85.0" y="107.0"></omgdi:waypoint>
+        <omgdi:waypoint x="130.0" y="107.0"></omgdi:waypoint>
+      </bpmndi:BPMNEdge>
+      <bpmndi:BPMNEdge bpmnElement="flow2" id="BPMNEdge_flow2">
+        <omgdi:waypoint x="235.0" y="107.0"></omgdi:waypoint>
+        <omgdi:waypoint x="290.0" y="107.0"></omgdi:waypoint>
+      </bpmndi:BPMNEdge>
+    </bpmndi:BPMNPlane>
+  </bpmndi:BPMNDiagram>
+</definitions>

+ 35 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/diagrams/chapter9/testMultiInstanceForUserTask.users.sequential.with.complete.conditon.bpmn

@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:activiti="http://activiti.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.activiti.org/test">
+  <process id="testMultiInstanceForUserTask" name="testMultiInstanceForUserTask" isExecutable="true">
+    <startEvent id="startevent1" name="Start"></startEvent>
+    <userTask id="usertask1" name="User Task" activiti:assignee="${user}">
+      <multiInstanceLoopCharacteristics isSequential="false" activiti:collection="${users}" activiti:elementVariable="user">
+        <completionCondition>${nrOfCompletedInstances / nrOfInstances &gt;= rate}</completionCondition>
+      </multiInstanceLoopCharacteristics>
+    </userTask>
+    <sequenceFlow id="flow1" sourceRef="startevent1" targetRef="usertask1"></sequenceFlow>
+    <endEvent id="endevent1" name="End"></endEvent>
+    <sequenceFlow id="flow2" sourceRef="usertask1" targetRef="endevent1"></sequenceFlow>
+  </process>
+  <bpmndi:BPMNDiagram id="BPMNDiagram_testMultiInstanceForUserTask">
+    <bpmndi:BPMNPlane bpmnElement="testMultiInstanceForUserTask" id="BPMNPlane_testMultiInstanceForUserTask">
+      <bpmndi:BPMNShape bpmnElement="startevent1" id="BPMNShape_startevent1">
+        <omgdc:Bounds height="35.0" width="35.0" x="50.0" y="90.0"></omgdc:Bounds>
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNShape bpmnElement="usertask1" id="BPMNShape_usertask1">
+        <omgdc:Bounds height="55.0" width="105.0" x="130.0" y="80.0"></omgdc:Bounds>
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNShape bpmnElement="endevent1" id="BPMNShape_endevent1">
+        <omgdc:Bounds height="35.0" width="35.0" x="290.0" y="90.0"></omgdc:Bounds>
+      </bpmndi:BPMNShape>
+      <bpmndi:BPMNEdge bpmnElement="flow1" id="BPMNEdge_flow1">
+        <omgdi:waypoint x="85.0" y="107.0"></omgdi:waypoint>
+        <omgdi:waypoint x="130.0" y="107.0"></omgdi:waypoint>
+      </bpmndi:BPMNEdge>
+      <bpmndi:BPMNEdge bpmnElement="flow2" id="BPMNEdge_flow2">
+        <omgdi:waypoint x="235.0" y="107.0"></omgdi:waypoint>
+        <omgdi:waypoint x="290.0" y="107.0"></omgdi:waypoint>
+      </bpmndi:BPMNEdge>
+    </bpmndi:BPMNPlane>
+  </bpmndi:BPMNDiagram>
+</definitions>

BIN
yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/diagrams/chapter9/testMultiInstanceForUserTask.users.sequential.with.complete.conditon.png


+ 21 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/flowable.cfg.xml

@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://www.springframework.org/schema/beans
+				http://www.springframework.org/schema/beans/spring-beans.xsd">
+
+	<bean id="processEngineConfiguration" class="org.flowable.engine.impl.cfg.StandaloneProcessEngineConfiguration">
+		<!--保存到文件模式-->
+		<!--<property name="jdbcUrl" value="jdbc:h2:file:~/activiti-in-action-chapter6;AUTO_SERVER=TRUE" />-->
+		<!--内存模式-->
+		<property name="jdbcUrl" value="jdbc:h2:mem:activiti-in-action-chapter6;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE" />
+		<property name="jdbcDriver" value="org.h2.Driver" />
+		<property name="jdbcUsername" value="sa" />
+		<property name="jdbcPassword" value="" />
+
+		<!--HistoryLevel-->
+		<property name="history" value="full" />
+		<property name="databaseSchemaUpdate" value="true" />
+		<property name="flowable5CompatibilityEnabled" value="false" />
+<!--		<property name="jobExecutorActivate" value="false" />-->
+	</bean>
+</beans>