アプリケーション開発プラットフォーム統合ワークフロー - プロセスモデリング機能のルーティングブランチ変換の設計と実装

背景

不親切なプロセス設定の問題については、国内のDingTalkがbpmnの仕様とは関係なく、プロセスモデリングモード一式を別途設計・実装しており、それを誰かが模倣してオープンソース化しました(https://github.com/StavinLi/ワークフロー -Vue3 ) の効果図は次のとおりです:

実装の一般原則は、無限にネストされた子ノードに基づいており、json データを出力し、それをバックエンドに渡します。バックエンドが解析した後、Camunda エンジンの API を呼び出します。それをプロセス モデルに変換し、永続化します。

前のパートでは、処理ノードの変換設計と実装について紹介しました。処理ノードはプロセスの主要部分ですが、実際のビジネス プロセスには依然としていくつかの論理分岐が必要です。例えば休暇申請の際、休暇日数が3日未満の場合は部長の承認があれば十分ですが、3日を超える場合は副社長の承認が必要になります。条件分岐が必要です。たとえば、地方自治体のインフラ計画には複数の下位郡局が関与するため、プロセスは関連する下位郡局に行く必要があり、この時点で並行する支店が必要になります。

基本的な考え方

ワークフローベースのプロセス処理では、一般的な直線的なフローに加えて分岐処理が必要になります。
分岐には、すべての分岐を実行する並列分岐と、設定した条件を満たした場合のみ実行する条件選択分岐があります。
Camunda は主に次の 3 種類のブランチを処理するためにゲートウェイを使用します。

  1. 排他的ゲートウェイ: 1 つのブランチのみが実行を許可され、次のノードは条件式またはルールに従って選択されます。
  2. パラレル ゲートウェイ (Parallel Gateway): 複数の分岐を同時に実行し、すべての分岐が完了すると次のノードの実行を継続します。
  3. 互換ゲートウェイ (包含ゲートウェイ): 複数のブランチの実行が許可され、条件式またはルールに従って次のノードが選択されますが、どのブランチも条件を満たさない場合は、デフォルト ブランチが選択されます。

並列ゲートウェイとは、条件やルールを設定せずにすべての分岐が実行されることです。

互換ゲートウェイと専用ゲートウェイの違いは、
実行回数が異なります。互換ゲートウェイでは複数のブランチを実行できますが、専用ゲートウェイでは 1 つのブランチのみを実行できます。
選択方法は異なります。互換ゲートウェイは条件式またはルールに従って次のノードを選択しますが、排他ゲートウェイは条件を満たす最初のブランチに従ってデフォルト ブランチを実行します。
処理方法は異なります。互換ゲートウェイはすべての出力シーケンス フローを計算しますが、排他的ゲートウェイは true と評価された出力シーケンス フローのみを処理します。

デザイン

専用のゲートウェイを使用する必要はありますか?

機能的には、互換ゲートウェイには専用ゲートウェイが含まれます。つまり、専用ゲートウェイは互換ゲートウェイの特殊なケースです。つまり、条件を満たすブランチが 1 つだけです。性能面での違いを考えると、両者の主な違いは、条件を満たす分岐が見つかった時点で計算を終了するか、すべての分岐を計算するかにあります。プロセス分岐が限られている場合 (通常は 3 つまたは 5 つ、最大でも 1 桁の範囲)、部分的な計算は完全な計算と大差なく、リソースとパフォーマンスは悪くありません。
機能とユーザー エクスペリエンスの観点から見ると、専用ゲートウェイのロジックはより明確であり、ユーザーは複数の条件から 1 つしか選択できないことを明確に認識していますが、ユーザー エクスペリエンスの向上も非常に限られています。

パラレルゲートウェイを使用する必要がありますか?

機能的には、互換ゲートウェイには並列ゲートウェイも含まれます。テスト後、ブランチに条件は設定されておらず、Camunda はデフォルトで条件を満たしているとみなします。つまり、すべてのブランチが実行できます。直感的には、少し使いすぎているように感じます。互換性のあるゲートウェイを使用することにより、並列分岐と条件分岐のロジックがある程度不明確になりますが、ユーザーにとっては、条件と並列の 2 つの概念を区別せずに 1 つの概念のみを持つよりも区別する方が必ずしも良いとは限りません。

プラン選択

DingTalk のプロセスモデルを具体的に見ると、専用の排他的なゲートウェイはなく、並列分岐と条件分岐の 2 種類のみであり、この 2 つの名前はビジネス上の意味に沿っており、ユーザーにとって理解しやすいです。 。

ビジネス利用の観点からは、分岐に設定された条件を満たしているかどうかによって、1つの分岐だけが分岐できるのか、複数の分岐が分岐できるのか、あるいはすべての分岐が分岐できるのかという概念は保持する必要があり、明確に区別する必要はありません。これは条件付き分岐または並列分岐であり、ビジネス ユーザーにとってはより使いやすいものです。

上記の考慮事項に基づいて、専用ゲートウェイと並列ゲートウェイを削除し、互換性のあるゲートウェイのみを使用し、フロントエンドはすべてルートとブランチで記述されます。

プログラムの実現

統合にプラットフォームが使用されている Workflow-vue3 オープン ソース プロジェクトには、条件付き分岐が組み込まれており、独自のニーズを満たすように変更されています。比較的単純な休暇承認プロセスを実装してみましょう。フローチャートは次のとおりです。
画像.png

フロントエンドの実装

nodeWrap.vueを変更し、ルーティングブランチを個別に処理し、タイプエンコーディングをINCLUSIVE_GATEWAYに設定します。

  <!-- 路由分支 -->
  <div class="branch-wrap" v-else-if="modelValue.type == 'INCLUSIVE_GATEWAY'">
    <div class="branch-box-wrap">
      <div class="branch-box">
        <button class="add-branch" @click="addCondition">添加条件</button>
        <div class="col-box" v-for="(item, index) in modelValue.branchList" :key="index">
          <div class="condition-node">
            <div class="condition-node-box">
              <div class="auto-judge" :class="isTried && item.error ? 'error active' : ''">
                <div class="sort-left" v-if="index != 0" @click="arrTransfer(index, -1)">&lt;</div>
                <div class="title-wrapper">
                  <input
                    v-if="isInputList[index]"
                    type="text"
                    class="ant-input editable-title-input"
                    @blur="blurEvent(index)"
                    @focus="$event.currentTarget.select()"
                    v-focus
                    v-model="item.name"
                  />
                  <span v-else class="editable-title" @click="clickEvent(index)">{
   
   {
                    item.name
                  }}</span>
                  <i class="anticon anticon-close close" @click="removeCondition(index)"></i>
                </div>
                <div
                  class="sort-right"
                  v-if="index != modelValue.branchList.length - 1"
                  @click="arrTransfer(index)"
                  >&gt;</div
                >
                <div class="content" @click="setConditionNode(item)">{
   
   {
                  $func.conditionStr(modelValue, index)
                }}</div>
                <div class="error_tip" v-if="isTried && item.error">
                  <i class="anticon anticon-exclamation-circle"></i>
                </div>
              </div>
              <addNode v-model:childNodeP="item.child" />
            </div>
          </div>
          <nodeWrap v-if="item.child" v-model:modelValue="item.child" />
          <template v-if="index == 0">
            <div class="top-left-cover-line"></div>
            <div class="bottom-left-cover-line"></div>
          </template>
          <template v-if="index == modelValue.branchList.length - 1">
            <div class="top-right-cover-line"></div>
            <div class="bottom-right-cover-line"></div>
          </template>
        </div>
      </div>
      <addNode v-model:childNodeP="modelValue.child" />
    </div>
  </div>

オリジナルのオープンソースプロジェクトでは、条件分岐に優先度が設定されており、優先度の高いエッジが先に計算され、条件が満たされると他のエッジの条件は計算されなくなるため、実質的には排他的なものに相当します。つまり、1 つのブランチのみが選択されます。実際のビジネスシナリオでは、複数の条件分岐を同時に行う必要があります。したがって、プラットフォーム側ですべての条件を計算するには、互換性のあるゲートウェイを実装する必要がありますが、この場合、優先度は実際には無意味であるため、削除する必要があります。

addNode.vue を変更し、デフォルトのルーティング データを設定し、条件付きノード タイプ コードを CONDITION に設定します。

const addConditionBranch = () => {
  visible.value = false
  const data = {
    name: '路由',
    id: 'node' + uuid(),
    type: 'INCLUSIVE_GATEWAY',
    config: {},
    child: null,
    branchList: [
      {
        name: '条件1',
        id: 'condition' + uuid(),
        type: 'CONDITION',
        config: {},
        branchList: [],
        child: props.childNodeP
      },
      {
        name: '条件2',
        id: 'condition' + uuid(),
        type: 'CONDITION',
        config: {},
        branchList: []
      }
    ]
  }
  emits('update:childNodeP', data)
}

条件の設定に関しては、実際には2つのモードがあり、1つはビジネスユーザー向けで、たとえば契約承認プロセスで契約金額属性を選択し、条件を100万以上に設定するなど、視覚的に設定する必要があります。同時に、他の and or OR 演算を追加することもできます。この演算は、プラットフォームによって処理可能な式に変換されます。もう 1 つは開発者向けで、結局のところ、プロセス リンクの処理ロジックは依然として開発者の記述に依存しており、特に条件式で使用される変数は引き続きプロセス リンクのロジックで処理されます。現在のプラットフォームの位置付けは、実際には開発者向けです。つまり、ローコード構成が主な焦点であり、開発効率の向上と開発コストの削減が、ソースコード開発によって補完され、ビジネスロジック、特に複雑なロジックの実現を保証します。優れた拡張性。プラットフォームの配置に基づいて、ここで条件を設定し、テキスト ボックスを配置するだけで、開発者は ${contractMoney>1000000} などの最終条件式を設定でき、シンプルで実用的です。

条件式設定コンポーネントを追加

<template>
  <el-drawer
    :append-to-body="true"
    title="条件设置"
    v-model="visible"
    :show-close="false"
    :size="550"
    :before-close="close"
    destroy-on-close
  >
    <el-form
      ref="form"
      :model="entityData"
      :rules="rules"
      label-width="120px"
      label-position="right"
      style="width: 90%; margin: 0px auto"
    >
      <!--表单区域 -->

      <el-form-item label="表达式" prop="expression">
        <el-input v-model="entityData.expression" type="textarea" rows="4" />
      </el-form-item>
      <el-form-item style="float: right; margin-top: 20px">
        <el-button type="primary" @click="save">确 定</el-button>
        <el-button @click="close">取 消</el-button>
      </el-form-item>
    </el-form>
  </el-drawer>
</template>
<script>
import { useStore } from '../../stores/index'
let store = useStore()

export default {
  data() {
    return {
      entityData: {},
      rules: {
        //前端验证规则
       
      }
    }
  },
  computed: {
    visible() {
      return store.conditionNodeConfigVisible
    },
    conditionNodeConfig() {
      return store.conditionNodeConfig
    }
  },
  watch: {
    conditionNodeConfig(value) {
      this.entityData = value
    }
  },
  methods: {
    close() {
      store.setConditionNodeConfigVisible(false)
    },
    save() {
      const nodeConfig = Object.assign(
        store.conditionNodeConfig,
        { ...this.entityData },
        { flag: true }
      )
      store.setConditionNodeConfig(nodeConfig)
      this.close()
    }
  }
}
</script>
<style scoped></style>

バックエンドの実装

変換ロジック

Camunda モデルには、実際には特定のブランチ ノードやシンク ノードはなく、すべてゲートウェイ ノードです。
したがって、収束の役割だけでなく、分岐の役割も果たす特定のゲートウェイ ノードが存在します。
フロントエンド モデルとバックエンド モデルが独立して実装されている場合、フロントエンド ルーティング ノードは Camunda ゲートウェイに対応し、条件ノードは Camunda 条件エッジに対応します。
DingTalk プロセス モデルでは、条件分岐には明示的な収束ノードが存在せず、バックエンドが自ら判断して補完する必要があります。

条件分岐の場合、フロントエンドの自己構築プロセス モデルはバックエンドの Camunda モデルとは大きく異なります。
フロントエンドのみの場合、条件付きブランチ ノードは、ブランチに含まれるルーティング ノード、条件付きノード、処理ノード、さらにはブランチ内のネストされた条件付きブランチ ノードを含む複数のノードの組み合わせです。フロントエンドのデータ構造はネストされたオブジェクトであり、条件分岐の例は次のとおりです。

{
    
    
	"name": "路由",
	"id": "node3278_00b0_e238_a105",
	"type": "INCLUSIVE_GATEWAY",
	"config": {
    
    },
	"child": null,
	"branchList": [{
    
    
			"name": "3天以内",
			"id": "condition5914_12fb_e783_f171",
			"type": "CONDITION",
			"config": {
    
    
				"expression": "${total<=3}"
			},
			"branchList": []
		},
		{
    
    
			"name": "超过3天",
			"id": "condition10081_fd56_1fb6_f8ed",
			"type": "CONDITION",
			"config": {
    
    
				"expression": "${total>3}"
			},
			"branchList": []
		}
	]
}

その分岐データは属性branchListに配置され、型はノード配列です。バックエンドの場合、タイプが INCLUSIVE_GATEWAY であるノードを互換性のあるゲートウェイに変換し、ノードの BranchList プロパティを読み取り、対応するノード配列の各要素を短いプロセスに変換して、最初のノードを互換性のあるゲートウェイ。ノードは、自動的に追加されたシンク ノードとドッキングします。

コア変換ロジックの実装

コア モデルの変換については、以下のメソッドのケース INCLUSIVE_GATEWAY ブランチを参照してください。完全なコードについては、オープン ソース プロジェクトを参照してください。

  /**
     * 将json转换为模型
     * 流程节点转换
     *
     * @param process       流程
     * @param parentElement 父元素
     * @param flowNode      流程节点
     * @param tempVersion   临时版本
     * @param expression    表达式
     * @return {@link FlowNode}
     */
    private  FlowNode convertJsonToModel(Process process, FlowNode parentElement,
                                     MyFlowNode flowNode,String tempVersion,String expression) {
    
    
        // 获取模型实例
        ModelInstance modelInstance = process.getModelInstance();
        // 构建节点
        FlowNode element=null;
        FlowCodeTypeEnum type = EnumUtils.getEnum(FlowCodeTypeEnum.class, flowNode.getType());
        switch (type){
    
    
            case ROOT:
                UserTask firstNode = modelInstance.newInstance(UserTask.class);
                firstNode.setName(flowNode.getName());
                firstNode.setCamundaAssignee("${firstNodeAssignee}");
                firstNode.setId("node"+ UUID.randomUUID().toString());
                process.addChildElement(firstNode);
                element=firstNode;
                // 构建边
                createSequenceFlow(process, parentElement, element);
                break;
            case HANDLE:
                UserTask userTask = modelInstance.newInstance(UserTask.class);
                // 基本属性设置
                userTask.setName(flowNode.getName());
                userTask.setId("node"+UUID.randomUUID().toString());
                // 环节配置
                String config=flowNode.getConfig();
                WorkflowNodeConfig userTaskNodeConfig =JSON.parseObject(config, WorkflowNodeConfig.class) ;
                userTask.setCamundaCandidateGroups(userTaskNodeConfig.getUserGroup());
                if (userTaskNodeConfig.getMode().equals(NodeModeEnum.COUNTERSIGN.name())) {
    
    
                    //会签模式
                    //设置处理人为变量
                    userTask.setCamundaAssignee("${assignee}");
                    //设置多实例
                    MultiInstanceLoopCharacteristics loopCharacteristics =
                            modelInstance.newInstance(MultiInstanceLoopCharacteristics.class);

                    loopCharacteristics.setSequential(false);
                    loopCharacteristics.setCamundaCollection("${assigneeList}");
                    loopCharacteristics.setCamundaElementVariable("assignee");
                    userTask.addChildElement(loopCharacteristics);
                } else {
    
    
                    //普通模式
                    //设置处理人为变量
                    userTask.setCamundaAssignee("${singleHandler}");
                }

                // 附加固化的人员指派监听器
                ExtensionElements extensionElements=modelInstance.newInstance(ExtensionElements.class);

                CamundaTaskListener listener=modelInstance.newInstance(CamundaTaskListener.class);
                listener.setCamundaEvent("create");
                listener.setCamundaClass("tech.abc.platform.workflow.listener.ApproverTaskListener");
                extensionElements.addChildElement(listener);
                userTask.setExtensionElements(extensionElements);

                process.addChildElement(userTask);
                element=userTask;
                // 构建边
                SequenceFlow sequenceFlow = createSequenceFlow(process, parentElement, element);
                // 如表达式不为空,则意味着需要设置条件边
                if(StringUtils.isNotBlank(expression)){
    
    
                    ConditionExpression conditionExpression= modelInstance.newInstance(ConditionExpression.class);
                    conditionExpression.setTextContent(expression);
                    sequenceFlow.setConditionExpression(conditionExpression);
                    // 使用一次后置空
                    expression=null;
                }

                // 生成环节配置
                userTaskNodeConfig.setProcessDefinitionId(tempVersion);
                userTaskNodeConfig.setName(userTask.getName());
                userTaskNodeConfig.setNodeId(userTask.getId());
                flowNodeConfigService.add(userTaskNodeConfig);

                break;
            case INCLUSIVE_GATEWAY:
                InclusiveGateway node = modelInstance.newInstance(InclusiveGateway.class);
                process.addChildElement(node);
                // 基本属性设置
                node.setName(flowNode.getName());
                node.setId(flowNode.getId());
                // 构建入边
                SequenceFlow inflow = createSequenceFlow(process, parentElement, node);
                // 如表达式不为空,则意味着需要设置条件边
                if(StringUtils.isNotBlank(expression)){
    
    
                    ConditionExpression conditionExpression= modelInstance.newInstance(ConditionExpression.class);
                    conditionExpression.setTextContent(expression);
                    inflow.setConditionExpression(conditionExpression);
                    // 使用一次后置空
                    expression=null;
                }
                // 生成虚拟的汇聚节点
                InclusiveGateway convergeNode = modelInstance.newInstance(InclusiveGateway.class);
                process.addChildElement(convergeNode);
                convergeNode.setName("汇聚节点");
                convergeNode.setId("convergeNode"+UUID.randomUUID().toString());
                element=convergeNode;

                // 分支处理
                List<MyFlowNode> branchList = flowNode.getBranchList();
                // 转换分支
                branchList.stream().forEach(item->{
    
    
                    // 分支首节点涉及到在边上设置条件表达式
                    MyConditionNode myConditionNode = JSON.parseObject(item.getConfig(), MyConditionNode.class);
                    String branchExpression=myConditionNode.getExpression();
                    log.info("expression:{}",branchExpression);

                    if(item.getChild()!=null && StringUtils.isNotBlank(item.getChild().getName())) {
    
    


                        FlowNode brachEndNode = convertJsonToModel(process, node,
                                item.getChild(), tempVersion,branchExpression);
                        // 附加汇聚节点
                        createSequenceFlow(process, brachEndNode, convergeNode);
                    }else{
    
    
                        // 附加汇聚节点
                        SequenceFlow endFlow = createSequenceFlow(process, node, convergeNode);
                        ConditionExpression conditionExpression= modelInstance.newInstance(ConditionExpression.class);                      
                        conditionExpression.setTextContent(branchExpression);
                        inflow.setConditionExpression(conditionExpression);

                    }

                });

                break;
            case SERVICE_TASK:
                // TODO
                // element = modelInstance.newInstance(ServiceTask.class);
                break;
            default:
                log.error("未找到合适的类型");

        }

        //递归处理子节点
        if(flowNode.getChild()!=null && StringUtils.isNotBlank(flowNode.getChild().getName())){
    
    
            return convertJsonToModel(process,element,flowNode.getChild(),tempVersion,expression);
        }else{
    
    
            return element;
        }

    }


条件付きエッジを設定するにはどうすればよいですか?

条件面の設定が難しいところですが、この作業を完結させるためのAPIが必要だと推測されます。エッジ オブジェクト SequenceFlow には、ConditionExpression プロパティを設定するプロパティがありますが、このプロパティは String ではなく、インターフェイスです。

public interface ConditionExpression extends FormalExpression {
    
    
    String getType();

    void setType(String var1);

    String getCamundaResource();

    void setCamundaResource(String var1);
}

これはクラス構築メソッドを実装しており、渡す必要があるパラメータは別の奇妙なクラス オブジェクト ModelTypeInstanceContext...

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.camunda.bpm.model.bpmn.impl.instance;

import org.camunda.bpm.model.bpmn.instance.ConditionExpression;
import org.camunda.bpm.model.bpmn.instance.FormalExpression;
import org.camunda.bpm.model.xml.ModelBuilder;
import org.camunda.bpm.model.xml.impl.instance.ModelTypeInstanceContext;
import org.camunda.bpm.model.xml.type.ModelElementTypeBuilder;
import org.camunda.bpm.model.xml.type.attribute.Attribute;

public class ConditionExpressionImpl extends FormalExpressionImpl implements ConditionExpression {
    
    
    protected static Attribute<String> typeAttribute;
    protected static Attribute<String> camundaResourceAttribute;

    public static void registerType(ModelBuilder modelBuilder) {
    
    
        ModelElementTypeBuilder typeBuilder = modelBuilder.defineType(ConditionExpression.class, "conditionExpression").namespaceUri("http://www.omg.org/spec/BPMN/20100524/MODEL").extendsType(FormalExpression.class).instanceProvider(new ModelElementTypeBuilder.ModelTypeInstanceProvider<ConditionExpression>() {
    
    
            public ConditionExpression newInstance(ModelTypeInstanceContext instanceContext) {
    
    
                return new ConditionExpressionImpl(instanceContext);
            }
        });
        typeAttribute = typeBuilder.stringAttribute("type").namespace("http://www.w3.org/2001/XMLSchema-instance").defaultValue("tFormalExpression").build();
        camundaResourceAttribute = typeBuilder.stringAttribute("resource").namespace("http://camunda.org/schema/1.0/bpmn").build();
        typeBuilder.build();
    }

    public ConditionExpressionImpl(ModelTypeInstanceContext instanceContext) {
    
    
        super(instanceContext);
    }

    public String getType() {
    
    
        return (String)typeAttribute.getValue(this);
    }

    public void setType(String type) {
    
    
        typeAttribute.setValue(this, type);
    }

    public String getCamundaResource() {
    
    
        return (String)camundaResourceAttribute.getValue(this);
    }

    public void setCamundaResource(String camundaResource) {
    
    
        camundaResourceAttribute.setValue(this, camundaResource);
    }
}

長い間検索し、試してみましたが、API でこのオブジェクトを構築する方法が見つかりませんでした。コードを見つめながら考えていると、突然ひらめき、modelInstance.newInstance(ConditionExpression.class) を使って構築してみたところ、うまく動作し、問題は解決しました。
さらに、再帰を使用するため、無関係なエッジに条件が設定されるのを避けるために、一度使用した後は式パラメータを空にする必要があります。

変換されたXMLデータ

上記の操作に従って、数回のデバッグの後、最終的に複雑な変換が完了し、モデル検証に合格し、次のように Camunda の XML モデルが出力されます。

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<definitions xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" id="definitions_159e6873-e4fc-4ae3-83e5-0b4978edb636" targetNamespace="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL">
  <process id="Leave" isExecutable="true" name="请假申请">
    <startEvent id="startEvent_339a89ba-eb8f-453e-a874-4d141233719f" name="流程开始">
      <outgoing>SequenceFlowff1bca6e-fe64-46ec-86d0-8382bec8d3dc</outgoing>
    </startEvent>
    <userTask camunda:assignee="${firstNodeAssignee}" id="node72a9790c-a534-4dee-879b-32ea648d6a34" name="填报">
      <incoming>SequenceFlowff1bca6e-fe64-46ec-86d0-8382bec8d3dc</incoming>
      <outgoing>SequenceFlowc6d1d96a-24b4-4dcd-b587-da5d8631c317</outgoing>
    </userTask>
    <sequenceFlow id="SequenceFlowff1bca6e-fe64-46ec-86d0-8382bec8d3dc" sourceRef="startEvent_339a89ba-eb8f-453e-a874-4d141233719f" targetRef="node72a9790c-a534-4dee-879b-32ea648d6a34"/>
    <userTask camunda:assignee="${singleHandler}" camunda:candidateGroups="99" id="nodefde7fee7-6cd2-4adc-a773-79716bd008e2" name="部门领导审批">
      <extensionElements>
        <camunda:taskListener class="tech.abc.platform.workflow.listener.ApproverTaskListener" event="create"/>
      </extensionElements>
      <incoming>SequenceFlowc6d1d96a-24b4-4dcd-b587-da5d8631c317</incoming>
      <outgoing>SequenceFlow6493c88b-381f-438d-974b-cc3480acee5c</outgoing>
    </userTask>
    <sequenceFlow id="SequenceFlowc6d1d96a-24b4-4dcd-b587-da5d8631c317" sourceRef="node72a9790c-a534-4dee-879b-32ea648d6a34" targetRef="nodefde7fee7-6cd2-4adc-a773-79716bd008e2"/>
    <inclusiveGateway id="node3278_00b0_e238_a105" name="条件路由">
      <incoming>SequenceFlow6493c88b-381f-438d-974b-cc3480acee5c</incoming>
      <outgoing>SequenceFlow34963022-a892-44ee-9d47-bd4cf157c77d</outgoing>
      <outgoing>SequenceFlowf5da3c9f-849e-4d37-9768-465f65755a82</outgoing>
    </inclusiveGateway>
    <sequenceFlow id="SequenceFlow6493c88b-381f-438d-974b-cc3480acee5c" sourceRef="nodefde7fee7-6cd2-4adc-a773-79716bd008e2" targetRef="node3278_00b0_e238_a105"/>
    <inclusiveGateway id="convergeNode58d061bf-4fc6-45ff-9199-ec2ec5870b6c" name="汇聚节点">
      <incoming>SequenceFlowa876a2e0-d9e4-4253-bc42-fdb7ee07036c</incoming>
      <incoming>SequenceFlow60d208fc-b01f-4253-a0df-a39cb0f9860f</incoming>
      <outgoing>SequenceFlowf12d18b3-96fe-4b22-b872-b96c07e1e5bb</outgoing>
    </inclusiveGateway>
    <userTask camunda:assignee="${singleHandler}" camunda:candidateGroups="99" id="node8ebc9ee6-bf0d-4542-b6b5-2509a9ea440d" name="HR审批">
      <extensionElements>
        <camunda:taskListener class="tech.abc.platform.workflow.listener.ApproverTaskListener" event="create"/>
      </extensionElements>
      <incoming>SequenceFlow34963022-a892-44ee-9d47-bd4cf157c77d</incoming>
      <outgoing>SequenceFlowa876a2e0-d9e4-4253-bc42-fdb7ee07036c</outgoing>
    </userTask>
    <sequenceFlow id="SequenceFlow34963022-a892-44ee-9d47-bd4cf157c77d" sourceRef="node3278_00b0_e238_a105" targetRef="node8ebc9ee6-bf0d-4542-b6b5-2509a9ea440d">
      <conditionExpression id="conditionExpression_42181437-38e5-48fb-8788-94c7af7b8790">${total&lt;=3}</conditionExpression>
    </sequenceFlow>
    <sequenceFlow id="SequenceFlowa876a2e0-d9e4-4253-bc42-fdb7ee07036c" sourceRef="node8ebc9ee6-bf0d-4542-b6b5-2509a9ea440d" targetRef="convergeNode58d061bf-4fc6-45ff-9199-ec2ec5870b6c"/>
    <userTask camunda:assignee="${singleHandler}" camunda:candidateGroups="99" id="node26d04870-685c-4ae9-9b83-971983d3016b" name="副总审批">
      <extensionElements>
        <camunda:taskListener class="tech.abc.platform.workflow.listener.ApproverTaskListener" event="create"/>
      </extensionElements>
      <incoming>SequenceFlowf5da3c9f-849e-4d37-9768-465f65755a82</incoming>
      <outgoing>SequenceFlow60d208fc-b01f-4253-a0df-a39cb0f9860f</outgoing>
    </userTask>
    <sequenceFlow id="SequenceFlowf5da3c9f-849e-4d37-9768-465f65755a82" sourceRef="node3278_00b0_e238_a105" targetRef="node26d04870-685c-4ae9-9b83-971983d3016b">
      <conditionExpression id="conditionExpression_f9565f4c-ef57-4c9c-a394-dffb2f299089">${total&gt;3}</conditionExpression>
    </sequenceFlow>
    <sequenceFlow id="SequenceFlow60d208fc-b01f-4253-a0df-a39cb0f9860f" sourceRef="node26d04870-685c-4ae9-9b83-971983d3016b" targetRef="convergeNode58d061bf-4fc6-45ff-9199-ec2ec5870b6c"/>
    <endEvent id="endEvent_ddd6bac7-1400-44c9-b54f-a53de7256c34">
      <incoming>SequenceFlowf12d18b3-96fe-4b22-b872-b96c07e1e5bb</incoming>
    </endEvent>
    <sequenceFlow id="SequenceFlowf12d18b3-96fe-4b22-b872-b96c07e1e5bb" sourceRef="convergeNode58d061bf-4fc6-45ff-9199-ec2ec5870b6c" targetRef="endEvent_ddd6bac7-1400-44c9-b54f-a53de7256c34"/>
  </process>
  <bpmndi:BPMNDiagram id="BPMNDiagram_3d886d70-b6b3-4ad2-8311-9e1eaa6a4fca">
    <bpmndi:BPMNPlane bpmnElement="Leave" id="BPMNPlane_6e44812c-0b24-4f06-8d7f-c8e5425638d6">
      <bpmndi:BPMNShape bpmnElement="startEvent_339a89ba-eb8f-453e-a874-4d141233719f" id="BPMNShape_c1f6af6c-c5e3-4525-b39b-6c12e68e959c">
        <dc:Bounds height="36.0" width="36.0" x="100.0" y="100.0"/>
      </bpmndi:BPMNShape>
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</definitions>

2 つのテスト データを取得し、休暇日数をそれぞれ 3 日と 5 日に設定し、循環が正常でテストに合格しました。
3 日を超える条件式の設定を削除し、休暇日数が 3 日であれば、両方の分岐がそこに流れ、テストは合格します。

開発プラットフォーム情報

プラットフォーム名: One Two Three 開発プラットフォーム
紹介: エンタープライズ レベルの総合開発プラットフォーム
設計情報: csdn コラム
オープン ソース アドレス: Gitee
オープン ソース プロトコル: MIT
オープン ソースは簡単ではありません。お気に入り、いいね、コメントへようこそ。

おすすめ

転載: blog.csdn.net/seawaving/article/details/132099555
おすすめ