애플리케이션 개발 플랫폼 통합 워크플로 - 프로세스 모델링 기능 라우팅 분기 변환 설계 및 구현

배경

프로세스 설정이 불친절한 문제에 대해 국내 딩톡에서는 bpmn 사양과 전혀 상관없는 프로세스 모델링 모드 세트를 별도로 설계하고 구현했는데, 누군가 이를 모방하여 오픈소스로 만들었습니다( https://github.com/StavinLi/ Workflow -Vue3 ), 효과 다이어그램은 다음과 같습니다:

구현의 일반 원칙은 무한히 중첩된 하위 노드를 기반으로 하며 JSON 데이터를 출력하고 이를 백엔드에 전달합니다. 백엔드가 구문 분석한 후 Camunda 엔진의 API를 호출합니다. 이를 프로세스 모델로 변환한 다음 유지합니다.

이전 부분에서는 처리 노드의 변환 설계 및 구현을 소개했습니다. 처리 노드는 프로세스의 주요 부분이지만 실제 비즈니스 프로세스에는 여전히 일부 논리적 분기가 필요합니다. 예를 들어 휴가를 신청하는 과정에서 휴가 일수가 3일 미만이면 부서장의 승인이면 충분하고, 3일을 초과하는 경우에는 부사장의 승인이 필요한데 이때, 조건부 분기가 필요합니다. 예를 들어, 시국의 인프라 계획에는 여러 하위 카운티 국이 참여하고 해당 프로세스는 해당 하위 카운티 국으로 이동해야 하며 이때 병렬 분기가 필요합니다.

기본 사상

Workflow 기반 프로세스 처리에는 일반적인 선형 흐름 외에 분기 처리가 필요합니다.
분기에는 두 가지 경우가 있는데, 하나는 모든 분기가 실행되는 병렬 분기이고, 다른 하나는 설정된 조건이 만족되었을 때만 실행되는 조건 선택 분기입니다.
Camunda는 게이트웨이를 사용하여 주로 다음 세 가지 유형의 분기를 처리합니다.

  1. Exclusive Gateway: 하나의 브랜치만 실행이 허용되며, 조건식이나 규칙에 따라 다음 노드가 선택됩니다.
  2. 병렬 게이트웨이(Parallel Gateway): 여러 분기를 동시에 실행하고, 모든 분기가 완료되면 다음 노드를 계속 실행합니다.
  3. 호환 게이트웨이(Inclusive Gateway): 다중 브랜치 실행이 허용되며, 조건식이나 규칙에 따라 다음 노드가 선택되지만, 어느 브랜치도 조건을 만족하지 않으면 기본 브랜치가 선택된다.

병렬 게이트웨이는 조건이나 규칙을 설정하지 않고 모든 분기가 실행된다는 것입니다.

호환 게이트웨이와 배타적 게이트웨이의 차이점은
실행 횟수가 다릅니다. 호환 게이트웨이는 여러 분기 실행을 허용하고, 배타적 게이트웨이는 하나의 분기만 실행을 허용합니다.
선택 방법은 다양합니다. 호환 게이트웨이는 조건식이나 규칙에 따라 다음 노드를 선택하고, 배타적 게이트웨이는 조건을 충족하는 첫 번째 분기에 따라 기본 분기를 실행합니다.
처리 방법은 다릅니다. 호환 가능한 게이트웨이는 모든 송신 시퀀스 흐름을 계산하고 독점 게이트웨이는 true로 평가되는 송신 시퀀스 흐름만 처리합니다.

설계

전용 게이트웨이를 사용해야 하나요?

기능적으로 호환 게이트웨이에는 배타적 게이트웨이가 포함됩니다. 즉, 배타적 게이트웨이는 호환 게이트웨이의 특별한 경우, 즉 하나의 분기만 조건을 충족하는 것입니다. 성능적인 관점에서 차이를 고려하면, 둘의 가장 큰 차이점은 조건에 맞는 분기를 찾았을 때 계산을 멈추느냐, 아니면 모든 분기를 계산하느냐에 있다. 제한된 프로세스 분기(일반적으로 3개 또는 5개, 최대 한 자리 범위 이하)의 경우 부분 계산은 전체 계산과 크게 다르지 않으며 리소스와 성능도 나쁘지 않습니다.
기능적 관점과 사용자 경험의 관점에서 보면 독점 게이트웨이의 논리가 더 명확하고 사용자는 여러 조건 중 하나만 선택할 수 있다는 것을 분명히 알고 있지만 사용자 경험의 개선도 매우 제한적입니다.

병렬 게이트웨이를 사용해야 합니까?

기능적으로 보면 호환 게이트웨이에는 실제로 병렬 게이트웨이가 포함되어 있는데, 테스트 후 지점에 아무런 조건도 설정되지 않았으며 Camunda는 기본적으로 조건을 충족한다고 간주하므로 모든 지점이 갈 수 있다는 의미입니다.직관적으로는 약간 과용된 느낌이 듭니다. 호환 가능한 게이트웨이를 사용하면 병렬 분기와 조건 분기의 논리가 어느 정도 불명확해질 수 있지만 사용자에게는 조건과 병렬이라는 두 가지 개념을 구별하지 않고 하나만 갖는 것보다 반드시 더 나은 것은 아닙니다.

계획 선택

DingTalk의 프로세스 모델을 구체적으로 살펴보면 전용 전용 게이트웨이가 없고 두 가지 유형만 있는데 하나는 병렬 분기이고 다른 하나는 조건 분기입니다. 이 두 이름은 비즈니스 의미에 더 부합하고 사용자가 이해하기 쉽습니다. .

영업적 이용의 관점에서는 지점에 설정된 조건을 만족하는지 여부에 따라 하나의 지점만 사용할 수 있는지, 여러 지점을 사용할 수 있는지 또는 모든 지점을 사용할 수 있는지에 대한 개념을 유지해야 하며, 명확하게 구분할 필요는 없습니다. 조건부 분기 또는 병렬 분기로 비즈니스 사용자에게 더 친숙합니다.

위의 고려 사항을 바탕으로 전용 게이트웨이와 병렬 게이트웨이를 제거하고 호환 가능한 게이트웨이만 사용하며 프런트 엔드는 모두 경로와 분기로 설명됩니다.

프로그램 구현

플랫폼이 통합을 위해 사용되는 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>

원래 오픈소스 프로젝트는 조건부 분기에 우선순위 설정이 있는데, 이는 우선순위가 높은 Edge를 먼저 계산하고, 일단 조건이 충족되면 다른 Edge의 조건은 계산하지 않기 때문에 실제로는 Exclusive에 해당합니다. 즉, 하나의 분기만 선택됩니다. 실제 비즈니스 시나리오에서는 동시에 여러 조건부 분기를 수행해야 합니다. 따라서 플랫폼은 모든 조건을 측에서 계산하기 위해 호환 가능한 게이트웨이를 구현해야 하며, 이 경우 우선 순위는 실제로 의미가 없으므로 제거해야 합니다.

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)
}

조건 설정에는 실제로 두 가지 모드가 있습니다. 하나는 비즈니스 사용자를 위한 것이며 시각적 구성을 제공해야 합니다. 예를 들어 계약 승인 프로세스에서 계약 금액 속성을 선택하고 조건을 100만보다 크게 설정합니다. 동시에 플랫폼에서 처리할 수 있는 표현식으로 변환된 다른 OR 연산을 추가할 수도 있습니다. 다른 하나는 개발자를 위한 것으로, 결국 프로세스 링크의 처리 로직은 여전히 ​​개발자가 작성하는 것에 달려 있으며, 특히 조건식에 사용된 변수는 여전히 프로세스 링크의 로직에서 처리됩니다. 현재 플랫폼의 포지셔닝은 실제로 개발자를 지향합니다. 즉, 로우 코드 구성이 주요 초점이며 개발 효율성을 높이고 개발 비용을 절감하며 소스 코드 개발로 보완되어 비즈니스 로직, 특히 복잡한 로직의 실현을 보장합니다. 좋은 확장성. 플랫폼의 포지셔닝에 따라 여기에 조건을 설정하고 텍스트 상자만 넣으면 개발자는 ${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가 있어야 할 것으로 추측됩니다. Edge 객체 SequenceFlow에는 ConditionExpression 속성을 설정하는 속성이 있지만 이 속성은 문자열이 아니라 인터페이스입니다.

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>

두 가지 테스트 데이터를 가져오고 휴가 일수를 각각 3일과 5일로 설정하고 순환이 정상이며 테스트에 합격했습니다.
3일보다 큰 조건식 설정을 삭제하고, 휴가 일수가 3일이면 두 분기 모두 해당 경로로 흘러 테스트가 통과됩니다.

개발 플랫폼 정보

플랫폼 이름: 하나 둘 셋 개발 플랫폼
소개: 엔터프라이즈급 일반 개발 플랫폼
설계 정보: csdn
열 오픈 소스 주소: Gitee
오픈 소스 프로토콜: MIT
오픈 소스는 쉽지 않습니다. 즐겨찾기, 좋아요, 댓글 환영합니다.

Supongo que te gusta

Origin blog.csdn.net/seawaving/article/details/132099555
Recomendado
Clasificación