15 KiB
15 KiB
完整示例、Python 脚本与流程模式
1. 完整示例 — 请假审批流程
流程描述: 开始 → 员工提交 → 经理审批 → 排他网关(通过/拒绝) → HR审批 → 结束
1.1 节点定义
<bpmn2:startEvent id="start" name="开始" flowable:initiator="applyUserId" />
<bpmn2:userTask id="task_apply" name="员工提交申请" flowable:assignee="${applyUserId}" />
<bpmn2:userTask id="task_manager" name="部门经理审批" flowable:assignee="manager" />
<bpmn2:exclusiveGateway id="gateway_result" name="审批结果" />
<bpmn2:userTask id="task_hr" name="HR审批" flowable:assignee="hr" />
<bpmn2:endEvent id="end" name="结束" />
1.2 连线定义
<bpmn2:sequenceFlow id="flow_1" name="" sourceRef="start" targetRef="task_apply" />
<bpmn2:sequenceFlow id="flow_2" name="" sourceRef="task_apply" targetRef="task_manager" />
<bpmn2:sequenceFlow id="flow_3" name="" sourceRef="task_manager" targetRef="gateway_result" />
<bpmn2:sequenceFlow id="flow_approve" name="通过" sourceRef="gateway_result" targetRef="task_hr">
<bpmn2:conditionExpression xsi:type="tFormalExpression"><![CDATA[${result == 1}]]></bpmn2:conditionExpression>
</bpmn2:sequenceFlow>
<bpmn2:sequenceFlow id="flow_reject" name="拒绝" sourceRef="gateway_result" targetRef="end">
<bpmn2:conditionExpression xsi:type="tFormalExpression"><![CDATA[${result == 0}]]></bpmn2:conditionExpression>
</bpmn2:sequenceFlow>
<bpmn2:sequenceFlow id="flow_4" name="" sourceRef="task_hr" targetRef="end" />
1.3 布局计算
节点列表(按垂直顺序):
start: type=startEvent, y=30, h=36 → bottom=66
task_apply: type=userTask, y=106, h=60 → bottom=166
task_manager: type=userTask, y=206, h=60 → bottom=266
gateway_result: type=exclusiveGateway, y=306, h=50 → bottom=356
task_hr: type=userTask, y=396, h=60 → bottom=456
end: type=endEvent, y=496, h=36 → bottom=532
1.4 nodes 参数
id=task_apply###nodeName=员工提交申请@@@id=task_manager###nodeName=部门经理审批@@@id=task_hr###nodeName=HR审批@@@
2. Python 调用脚本模板
import urllib.request
import urllib.parse
import json
import time
def create_bpm_process(api_base, token, process_name, process_type, bpmn_xml, nodes_str, tenant_id="1"):
"""
创建 JeecgBoot BPM 流程
Args:
api_base: 后端地址,如 "https://api3.boot.jeecg.com"
token: X-Access-Token
process_name: 流程名称
process_type: 流程类型(如 "oa")
bpmn_xml: 完整 BPMN XML 字符串
nodes_str: nodes 参数字符串
tenant_id: 租户ID,默认 "1"
Returns:
dict: API 返回结果
"""
ts = str(int(time.time() * 1000))
process_key = f"process_{ts}"
# 替换 XML 中的占位符
bpmn_xml = bpmn_xml.replace("${PROCESS_KEY}", process_key)
bpmn_xml = bpmn_xml.replace("${PROCESS_NAME}", process_name)
data = {
"processDefinitionId": "0",
"processName": process_name,
"processkey": process_key,
"typeid": process_type,
"lowAppId": "",
"params": "",
"nodes": nodes_str,
"processDescriptor": bpmn_xml,
"realProcDefId": "",
"startType": "manual"
}
encoded_data = urllib.parse.urlencode(data).encode('utf-8')
req = urllib.request.Request(
f"{api_base}/act/designer/api/saveProcess",
data=encoded_data,
headers={
"X-Access-Token": token,
"X-Sign": "00000000000000000000000000000000",
"X-Tenant-Id": tenant_id,
"X-Timestamp": ts,
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
},
method="POST"
)
resp = urllib.request.urlopen(req)
result = json.loads(resp.read().decode('utf-8'))
return result, process_key
def update_bpm_process(api_base, token, process_id, process_key, process_name, process_type, bpmn_xml, nodes_str, tenant_id="1"):
"""
更新已有 JeecgBoot BPM 流程
Args:
api_base: 后端地址
token: X-Access-Token
process_id: 流程数据表ID(新建时返回的 obj)
process_key: 流程定义ID
process_name: 流程名称
process_type: 流程类型
bpmn_xml: 完整 BPMN XML 字符串
nodes_str: nodes 参数字符串
tenant_id: 租户ID,默认 "1"
Returns:
dict: API 返回结果
"""
ts = str(int(time.time() * 1000))
data = {
"processDefinitionId": process_id,
"processName": process_name,
"processkey": process_key,
"typeid": process_type,
"lowAppId": "",
"params": "",
"nodes": nodes_str,
"processDescriptor": bpmn_xml,
"startType": "manual"
}
encoded_data = urllib.parse.urlencode(data).encode('utf-8')
req = urllib.request.Request(
f"{api_base}/act/designer/api/saveProcess",
data=encoded_data,
headers={
"X-Access-Token": token,
"X-Sign": "00000000000000000000000000000000",
"X-Tenant-Id": tenant_id,
"X-Timestamp": ts,
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
},
method="POST"
)
resp = urllib.request.urlopen(req)
result = json.loads(resp.read().decode('utf-8'))
return result
3. 常见流程模式速查
模式A:简单审批(线性)
开始 → 提交 → 审批 → 结束
模式B:多级审批(线性)
开始 → 提交 → 经理审批 → 总监审批 → HR审批 → 结束
模式C:条件分支(排他网关)
开始 → 提交 → 审批 → 网关
├─ 通过 → 下一步 → 结束
└─ 拒绝 → 结束
模式D:金额条件分支
开始 → 提交 → 网关(金额判断)
├─ ≤1000 → 经理审批 → 结束
├─ ≤10000 → 总监审批 → 结束
└─ >10000 → CEO审批 → 结束
模式E:并行会签
开始 → 提交 → 并行网关(fork)
├─ 部门A审批
└─ 部门B审批
并行网关(join) → 结束
模式F:审批+驳回到发起人(草稿节点模式)
开始 → 草稿(自动跳过) → 审批 → 网关
├─ 通过 → 结束
└─ 驳回 → 草稿(发起人修改后重新提交)
关键节点 XML:
<!-- 草稿节点:首次自动跳过,驳回后发起人手动操作 -->
<bpmn2:userTask id="task_draft" name="草稿" flowable:assignee="${applyUserId}">
<bpmn2:extensionElements>
<flowable:taskExtendJson value="{"sameMode":2,"isSkipAssigneeEmpty":false,"isSkipAssigneeOnePersion":false,"isSkipApproval":false,"isAssignedByPreviousNode":false,"isEmptyAssignedByPreviousNode":false}" />
<flowable:taskListener class="org.jeecg.modules.extbpm.listener.task.TaskSkipApprovalListener" event="create" />
<flowable:taskListener class="org.jeecg.modules.extbpm.listener.task.TaskCreatedAutoSubmitListener" event="create" id="9c3064baa7074eab62e3c5b3b5458691" />
</bpmn2:extensionElements>
</bpmn2:userTask>
<!-- 驳回连线:从网关回到草稿节点 -->
<bpmn2:sequenceFlow id="flow_reject" name="驳回" sourceRef="gateway_result" targetRef="task_draft">
<bpmn2:conditionExpression xsi:type="tFormalExpression"><![CDATA[${result == 0}]]></bpmn2:conditionExpression>
</bpmn2:sequenceFlow>
模式G:多级审批 + 表达式审批人 + 上一节点指派(实战验证)
开始 → 申请人填写(草稿) → 部门负责人审批(表达式) → 分管领导审批(表达式) → 指派确认(上一节点指派) → 结束
已验证成功的完整 Python 脚本(f-string 方式构造 XML):
import urllib.request
import urllib.parse
import json
import time
API_BASE = '{后端地址}'
TOKEN = '{X-Access-Token}'
ts = str(int(time.time() * 1000))
process_key = f'process_{ts}'
process_name = '车辆出差申请流程'
# 注意:f-string 中 ${xxx} 写作 ${{xxx}},JSON 花括号也需 {{}}
bpmn_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<bpmn2:definitions
xmlns:bpmn2="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
xmlns:dc="http://www.omg.org/spec/DD/20100524/DC"
xmlns:di="http://www.omg.org/spec/DD/20100524/DI"
xmlns:flowable="http://flowable.org/bpmn"
id="sample-diagram"
targetNamespace="http://bpmn.io/schema/bpmn"
xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL BPMN20.xsd">
<bpmn2:process id="{process_key}" name="{process_name}">
<bpmn2:extensionElements>
<flowable:executionListener class="org.jeecg.modules.extbpm.listener.execution.ProcessEndListener" event="end" />
<flowable:eventListener class="org.jeecg.modules.listener.tasktip.TaskCreateGlobalListener" />
</bpmn2:extensionElements>
<bpmn2:startEvent id="start" name="开始" flowable:initiator="applyUserId" />
<bpmn2:userTask id="task_apply" name="申请人填写" flowable:assignee="${{applyUserId}}">
<bpmn2:extensionElements>
<flowable:taskExtendJson value='{{"sameMode":2,"isSkipAssigneeEmpty":false,"isSkipAssigneeOnePersion":false,"isSkipApproval":false,"isAssignedByPreviousNode":false,"isEmptyAssignedByPreviousNode":false}}' />
<flowable:taskListener class="org.jeecg.modules.extbpm.listener.task.TaskSkipApprovalListener" event="create" />
<flowable:taskListener class="org.jeecg.modules.extbpm.listener.task.TaskCreatedAutoSubmitListener" event="create" id="auto_submit_1" />
</bpmn2:extensionElements>
</bpmn2:userTask>
<bpmn2:userTask id="task_dept_leader" name="部门负责人审批"
flowable:candidateUsers="${{flowNodeExecution.getDepartLeaders(execution)}}">
<bpmn2:extensionElements>
<flowable:taskExtendJson value='{{"sameMode":0,"isSkipAssigneeEmpty":false,"isSkipAssigneeOnePersion":true,"isSkipApproval":false,"isAssignedByPreviousNode":false,"isEmptyAssignedByPreviousNode":true}}' />
<flowable:taskListener class="org.jeecg.modules.extbpm.listener.task.TaskSkipApprovalListener" event="create" />
</bpmn2:extensionElements>
</bpmn2:userTask>
<bpmn2:userTask id="task_leader" name="分管领导审批"
flowable:candidateUsers="${{flowNodeExecution.getLevel1DepartLeaders(execution)}}">
<bpmn2:extensionElements>
<flowable:taskExtendJson value='{{"sameMode":0,"isSkipAssigneeEmpty":false,"isSkipAssigneeOnePersion":true,"isSkipApproval":false,"isAssignedByPreviousNode":false,"isEmptyAssignedByPreviousNode":true}}' />
<flowable:taskListener class="org.jeecg.modules.extbpm.listener.task.TaskSkipApprovalListener" event="create" />
</bpmn2:extensionElements>
</bpmn2:userTask>
<bpmn2:userTask id="task_dispatch" name="车辆调度确认">
<bpmn2:extensionElements>
<flowable:taskExtendJson value='{{"sameMode":0,"isSkipAssigneeEmpty":false,"isSkipAssigneeOnePersion":false,"isSkipApproval":false,"isAssignedByPreviousNode":true,"isEmptyAssignedByPreviousNode":false}}' />
<flowable:taskListener class="org.jeecg.modules.extbpm.listener.task.TaskSkipApprovalListener" event="create" />
</bpmn2:extensionElements>
</bpmn2:userTask>
<bpmn2:endEvent id="end" name="结束" />
<bpmn2:sequenceFlow id="flow_1" name="" sourceRef="start" targetRef="task_apply" />
<bpmn2:sequenceFlow id="flow_2" name="" sourceRef="task_apply" targetRef="task_dept_leader" />
<bpmn2:sequenceFlow id="flow_3" name="" sourceRef="task_dept_leader" targetRef="task_leader" />
<bpmn2:sequenceFlow id="flow_4" name="" sourceRef="task_leader" targetRef="task_dispatch" />
<bpmn2:sequenceFlow id="flow_5" name="" sourceRef="task_dispatch" targetRef="end" />
</bpmn2:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="{process_key}">
<bpmndi:BPMNShape id="shape_start" bpmnElement="start">
<dc:Bounds x="200" y="30" width="36" height="36" />
<bpmndi:BPMNLabel><dc:Bounds x="207" y="73" width="22" height="14" /></bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_task_apply" bpmnElement="task_apply">
<dc:Bounds x="168" y="106" width="100" height="60" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_task_dept_leader" bpmnElement="task_dept_leader">
<dc:Bounds x="168" y="206" width="100" height="60" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_task_leader" bpmnElement="task_leader">
<dc:Bounds x="168" y="306" width="100" height="60" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_task_dispatch" bpmnElement="task_dispatch">
<dc:Bounds x="168" y="406" width="100" height="60" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="shape_end" bpmnElement="end">
<dc:Bounds x="200" y="506" width="36" height="36" />
<bpmndi:BPMNLabel><dc:Bounds x="207" y="549" width="22" height="14" /></bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="edge_flow_1" bpmnElement="flow_1">
<di:waypoint x="218" y="66" /><di:waypoint x="218" y="106" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_2" bpmnElement="flow_2">
<di:waypoint x="218" y="166" /><di:waypoint x="218" y="206" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_3" bpmnElement="flow_3">
<di:waypoint x="218" y="266" /><di:waypoint x="218" y="306" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_4" bpmnElement="flow_4">
<di:waypoint x="218" y="366" /><di:waypoint x="218" y="406" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="edge_flow_5" bpmnElement="flow_5">
<di:waypoint x="218" y="466" /><di:waypoint x="218" y="506" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn2:definitions>'''
nodes_str = 'id=task_apply###nodeName=申请人填写@@@id=task_dept_leader###nodeName=部门负责人审批@@@id=task_leader###nodeName=分管领导审批@@@id=task_dispatch###nodeName=车辆调度确认@@@'
data = {
'processDefinitionId': '0',
'processName': process_name,
'processkey': process_key,
'typeid': 'oa',
'lowAppId': '',
'params': '',
'nodes': nodes_str,
'processDescriptor': bpmn_xml,
'realProcDefId': '',
'startType': 'manual'
}
encoded_data = urllib.parse.urlencode(data).encode('utf-8')
req = urllib.request.Request(
f'{API_BASE}/act/designer/api/saveProcess',
data=encoded_data,
headers={
'X-Access-Token': TOKEN,
'X-Sign': '00000000000000000000000000000000',
'X-Tenant-Id': '1',
'X-Timestamp': str(int(time.time() * 1000)),
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
},
method='POST'
)
resp = urllib.request.urlopen(req)
result = json.loads(resp.read().decode('utf-8'))
print(json.dumps(result, ensure_ascii=False, indent=2))
print(f'\\nProcess Key: {process_key}')
实战要点:
- 此脚本必须先写入
.py文件再执行(不能python3 -c内联,bash 会展开${}导致报错) - f-string 中所有
${xxx}都写作${{xxx}} - taskExtendJson 的
value用单引号包裹 JSON,JSON 花括号用{{}}转义 - 执行完毕后删除临时
.py文件