流程规则与约束:构建智能验证系统

通过Rules模块定义流程约束,可以防止用户创建不合法的流程结构,提升流程质量。本文详细讲解如何实现连接规则、放置规则、以及自动验证机制。

Rules模块的作用

Rules(规则引擎)控制:

  • 哪些元素可以连接
  • 元素可以放置在哪里
  • 哪些操作被允许或禁止
  • 流程的合法性检查

通过定义规则,可以实现:

  • ✓ 禁止非法连接(如结束事件不能有出边)
  • ✓ 限制元素放置位置(如边界事件只能附着任务)
  • ✓ 强制流程规范(如必须有启动和结束事件)
  • ✓ 业务逻辑验证(如审批节点必须配置审批人)

基础规则定义

禁止非法连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class CustomRules {
constructor(eventBus) {
eventBus.on('commandStack.connection.create.canExecute', (context) => {
const source = context.source;
const target = context.target;

// 规则1:结束事件不能作为源
if (source.type === 'bpmn:EndEvent') {
return false;
}

// 规则2:启动事件不能作为目标
if (target.type === 'bpmn:StartEvent') {
return false;
}

// 规则3:网关必须至少有两条出边
if (this.isGateway(source)) {
const outgoing = source.outgoing || [];
if (outgoing.length >= 4) {
// 限制最多4条分支
return false;
}
}

return true;
});
}

isGateway(element) {
return element.type === 'bpmn:ExclusiveGateway' ||
element.type === 'bpmn:ParallelGateway' ||
element.type === 'bpmn:InclusiveGateway';
}
}

CustomRules.$inject = ['eventBus'];

const modeler = new BpmnJS({
container: '#canvas',
additionalModules: [
{
__init__: ['customRules'],
customRules: ['type', CustomRules]
}
]
});

使用diagram-js的Rules API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
class BetterCustomRules extends RuleProvider {
constructor(eventBus) {
super(eventBus);
}

init() {
// 连接规则
this.addRule('connection.create', (context) => {
const source = context.source;
const target = context.target;

return this.canConnect(source, target);
});

// 形状创建规则
this.addRule('shape.create', (context) => {
const shape = context.shape;
const target = context.target;

return this.canCreate(shape, target);
});

// 元素移动规则
this.addRule('elements.move', (context) => {
const shapes = context.shapes;
const target = context.target;

return this.canMove(shapes, target);
});

// 元素删除规则
this.addRule('elements.delete', (context) => {
const elements = context.elements;

return this.canDelete(elements);
});
}

canConnect(source, target) {
// 结束事件不能有出边
if (source.type === 'bpmn:EndEvent') {
return false;
}

// 启动事件不能有入边
if (target.type === 'bpmn:StartEvent') {
return false;
}

// 同一个元素不能连接自己
if (source === target) {
return false;
}

// 已经存在连接则不允许重复
if (this.hasConnection(source, target)) {
return false;
}

return true;
}

canCreate(shape, target) {
// 边界事件只能附着在任务上
if (shape.type === 'bpmn:BoundaryEvent') {
return target.type === 'bpmn:Task' ||
target.type === 'bpmn:UserTask' ||
target.type === 'bpmn:ServiceTask';
}

// 顺序流只能在流程中
if (shape.type === 'bpmn:SequenceFlow') {
return target.type === 'bpmn:Process' ||
target.type === 'bpmn:SubProcess';
}

return true;
}

canMove(shapes, target) {
// 启动事件不允许移动到子流程外
const hasStartEvent = shapes.some(s => s.type === 'bpmn:StartEvent');
if (hasStartEvent && target.type !== 'bpmn:Process') {
return false;
}

return true;
}

canDelete(elements) {
// 流程必须至少有一个启动事件
const hasStartEvent = elements.some(e => e.type === 'bpmn:StartEvent');
if (hasStartEvent) {
const registry = this.elementRegistry;
const allStartEvents = registry.filter(e => e.type === 'bpmn:StartEvent');

// 如果删除后没有启动事件,则禁止
if (allStartEvents.length === 1) {
return false;
}
}

return true;
}

hasConnection(source, target) {
const outgoing = source.outgoing || [];
return outgoing.some(conn => conn.target === target);
}
}

BetterCustomRules.$inject = ['eventBus'];
inherits(BetterCustomRules, RuleProvider);

高级业务规则

基于属性的规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class AttributeBasedRules extends RuleProvider {
constructor(eventBus) {
super(eventBus);
}

init() {
this.addRule('connection.create', (context) => {
const source = context.source;
const target = context.target;
const bo = source.businessObject;

// 规则:已配置为"同步任务"的不能连接到网关
if (bo.get('custom:executionMode') === 'sync') {
if (this.isGateway(target)) {
return {
allowed: false,
reason: '同步任务不能直接连接网关,请使用中间事件'
};
}
}

// 规则:VIP流程的任务只能连接到VIP任务
if (bo.get('custom:processLevel') === 'vip') {
const targetBo = target.businessObject;
if (targetBo.get('custom:processLevel') !== 'vip') {
return {
allowed: false,
reason: 'VIP流程只能连接VIP级别的节点'
};
}
}

return true;
});
}

isGateway(element) {
return element.type.includes('Gateway');
}
}

AttributeBasedRules.$inject = ['eventBus'];
inherits(AttributeBasedRules, RuleProvider);

流程完整性验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
class ProcessIntegrityRules {
constructor(eventBus, elementRegistry) {
this.elementRegistry = elementRegistry;

// 监听保存前的验证
eventBus.on('saveXML.start', () => {
this.validateProcess();
});
}

validateProcess() {
const errors = [];

// 检查1:必须有启动事件
const startEvents = this.elementRegistry.filter(e => e.type === 'bpmn:StartEvent');
if (startEvents.length === 0) {
errors.push({
type: 'error',
message: '流程必须包含至少一个启动事件'
});
}

// 检查2:必须有结束事件
const endEvents = this.elementRegistry.filter(e => e.type === 'bpmn:EndEvent');
if (endEvents.length === 0) {
errors.push({
type: 'error',
message: '流程必须包含至少一个结束事件'
});
}

// 检查3:所有任务必须有名称
const tasks = this.elementRegistry.filter(e =>
e.type.includes('Task') && e.type !== 'bpmn:SendTask'
);
tasks.forEach(task => {
if (!task.businessObject.name) {
errors.push({
type: 'warning',
element: task,
message: `任务 ${task.id} 缺少名称`
});
}
});

// 检查4:网关必须有分支条件
const gateways = this.elementRegistry.filter(e =>
e.type === 'bpmn:ExclusiveGateway'
);
gateways.forEach(gateway => {
const outgoing = gateway.outgoing || [];
const hasConditions = outgoing.some(flow =>
flow.businessObject.conditionExpression
);

if (outgoing.length > 1 && !hasConditions) {
errors.push({
type: 'warning',
element: gateway,
message: `排他网关 ${gateway.id} 的分支缺少条件`
});
}
});

// 检查5:没有孤立的节点
const allElements = this.elementRegistry.filter(e =>
e.type.startsWith('bpmn:') &&
e.type !== 'bpmn:Process' &&
e.type !== 'bpmn:SequenceFlow'
);
allElements.forEach(element => {
const incoming = element.incoming || [];
const outgoing = element.outgoing || [];

if (element.type !== 'bpmn:StartEvent' && incoming.length === 0) {
errors.push({
type: 'error',
element: element,
message: `元素 ${element.id} 没有输入连接`
});
}

if (element.type !== 'bpmn:EndEvent' && outgoing.length === 0) {
errors.push({
type: 'error',
element: element,
message: `元素 ${element.id} 没有输出连接`
});
}
});

// 显示错误
if (errors.length > 0) {
this.showValidationErrors(errors);
throw new Error('流程验证失败');
}
}

showValidationErrors(errors) {
console.group('流程验证错误');
errors.forEach(error => {
const level = error.type === 'error' ? '❌' : '⚠️';
console.log(`${level} ${error.message}`);
if (error.element) {
console.log(' 元素:', error.element.id);
}
});
console.groupEnd();

// 在UI中显示
const errorPanel = document.getElementById('validation-errors');
if (errorPanel) {
errorPanel.innerHTML = errors.map(e =>
`<div class="validation-${e.type}">${e.message}</div>`
).join('');
}
}
}

ProcessIntegrityRules.$inject = ['eventBus', 'elementRegistry'];

实战案例:审批流程规则引擎

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
class ApprovalProcessRules extends RuleProvider {
constructor(eventBus, elementRegistry) {
super(eventBus);
this.elementRegistry = elementRegistry;
}

init() {
// 规则1:审批任务必须连接到决策网关
this.addRule('connection.create', (context) => {
const source = context.source;
const target = context.target;

if (this.isApprovalTask(source)) {
if (target.type !== 'bpmn:ExclusiveGateway') {
return {
allowed: false,
reason: '审批任务后必须连接决策网关'
};
}
}

return true;
});

// 规则2:审批网关必须有"同意"和"拒绝"分支
this.addRule('connection.create', (context) => {
const source = context.source;

if (this.isApprovalGateway(source)) {
const outgoing = source.outgoing || [];
const conditions = outgoing.map(f =>
f.businessObject.get('custom:approvalResult')
);

if (conditions.includes('approved') && conditions.includes('rejected')) {
return {
allowed: false,
reason: '该审批网关已有完整的分支'
};
}
}

return true;
});

// 规则3:审批任务必须配置审批人
this.addRule('shape.create', (context) => {
const shape = context.shape;

if (this.isApprovalTask(shape)) {
// 延迟验证:在拖拽完成后检查
setTimeout(() => {
this.validateApprovalTask(shape);
}, 100);
}

return true;
});
}

isApprovalTask(element) {
return element.type === 'bpmn:UserTask' &&
element.businessObject.get('custom:taskType') === 'approval';
}

isApprovalGateway(element) {
return element.type === 'bpmn:ExclusiveGateway' &&
element.businessObject.get('custom:gatewayType') === 'approval-decision';
}

validateApprovalTask(task) {
const bo = task.businessObject;
const errors = [];

if (!bo.get('custom:assignee')) {
errors.push('未配置审批人');
}

if (!bo.get('custom:dueDate')) {
errors.push('未配置审批期限');
}

if (!bo.get('custom:formKey')) {
errors.push('未配置审批表单');
}

if (errors.length > 0) {
this.highlightError(task);
this.showToast(`审批任务配置不完整:${errors.join('、')}`);
}
}

highlightError(element) {
const canvas = this.elementRegistry._canvas;
canvas.addMarker(element, 'validation-error');
}

showToast(message) {
// 显示提示消息
console.warn(message);
}
}

ApprovalProcessRules.$inject = ['eventBus', 'elementRegistry'];
inherits(ApprovalProcessRules, RuleProvider);

// 样式
const style = `
.validation-error .djs-visual > rect {
stroke: #ff4d4f !important;
stroke-width: 3px !important;
}
`;

错误提示与用户体验

友好的错误消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
class UserFriendlyRules extends RuleProvider {
constructor(eventBus, canvas, translate) {
super(eventBus);
this.canvas = canvas;
this.translate = translate;
}

init() {
this.addRule('connection.create', (context) => {
const result = this.validateConnection(context);

if (!result.allowed) {
// 显示提示气泡
this.showTooltip(context.source, result.message);

// 短暂高亮源元素
this.canvas.addMarker(context.source, 'highlight-error');
setTimeout(() => {
this.canvas.removeMarker(context.source, 'highlight-error');
}, 2000);
}

return result.allowed;
});
}

validateConnection(context) {
const source = context.source;
const target = context.target;

if (source.type === 'bpmn:EndEvent') {
return {
allowed: false,
message: this.translate('结束事件不能有出边')
};
}

if (target.type === 'bpmn:StartEvent') {
return {
allowed: false,
message: this.translate('启动事件不能有入边')
};
}

return { allowed: true };
}

showTooltip(element, message) {
const tooltip = document.createElement('div');
tooltip.className = 'validation-tooltip';
tooltip.textContent = message;

const bounds = element.bounds || element;
tooltip.style.left = `${bounds.x + bounds.width / 2}px`;
tooltip.style.top = `${bounds.y - 30}px`;

document.body.appendChild(tooltip);

setTimeout(() => {
tooltip.remove();
}, 3000);
}
}

UserFriendlyRules.$inject = ['eventBus', 'canvas', 'translate'];
inherits(UserFriendlyRules, RuleProvider);

// 样式
const tooltipStyle = `
.validation-tooltip {
position: absolute;
background: #ff4d4f;
color: white;
padding: 8px 12px;
border-radius: 4px;
font-size: 12px;
pointer-events: none;
z-index: 1000;
animation: fadeOut 3s;
}

@keyframes fadeOut {
0%, 80% { opacity: 1; }
100% { opacity: 0; }
}

.highlight-error .djs-visual > * {
stroke: #ff4d4f !important;
animation: pulse 0.5s ease-in-out 2;
}

@keyframes pulse {
0%, 100% { stroke-width: 2px; }
50% { stroke-width: 4px; }
}
`;

小结

掌握流程规则与约束:

  • ✓ 定义连接与放置规则
  • ✓ 实现流程完整性验证
  • ✓ 构建业务规则引擎
  • ✓ 提供友好的错误提示
  • ✓ 保障流程质量与规范

便能构建智能、健壮的流程设计工具

参考资料