增加部署日志
This commit is contained in:
parent
4b554a8a2a
commit
c49f345a58
416
backend/docs/deploy-flow-graph-frontend-guide.md
Normal file
416
backend/docs/deploy-flow-graph-frontend-guide.md
Normal file
@ -0,0 +1,416 @@
|
|||||||
|
# 部署记录流程图前端绘制指南
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
本文档说明前端如何调用API获取部署记录的工作流流程图数据,以及如何使用这些数据绘制流程图并标记节点状态。
|
||||||
|
|
||||||
|
## API接口
|
||||||
|
|
||||||
|
### 获取部署流程图数据
|
||||||
|
|
||||||
|
**接口地址:** `GET /api/v1/deploy/records/{deployRecordId}/flow-graph`
|
||||||
|
|
||||||
|
**路径参数:**
|
||||||
|
- `deployRecordId` (Long): 部署记录ID
|
||||||
|
|
||||||
|
**响应示例:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "操作成功",
|
||||||
|
"data": {
|
||||||
|
"deployRecordId": 16,
|
||||||
|
"workflowInstanceId": 16,
|
||||||
|
"processInstanceId": "b24d97ec-b97f-11f0-be70-de5a4815c9ef",
|
||||||
|
"deployStatus": "REJECTED",
|
||||||
|
"graph": {
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "startNode",
|
||||||
|
"nodeCode": "start",
|
||||||
|
"nodeType": "START",
|
||||||
|
"nodeName": "开始",
|
||||||
|
"position": {
|
||||||
|
"x": 100,
|
||||||
|
"y": 200
|
||||||
|
},
|
||||||
|
"configs": {},
|
||||||
|
"inputMapping": {},
|
||||||
|
"outputs": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "sid_4ee9fab9_d626_4691_816c_9435902fc3d0",
|
||||||
|
"nodeCode": "approval",
|
||||||
|
"nodeType": "APPROVAL",
|
||||||
|
"nodeName": "审批节点",
|
||||||
|
"position": {
|
||||||
|
"x": 300,
|
||||||
|
"y": 200
|
||||||
|
},
|
||||||
|
"configs": {
|
||||||
|
"userIds": ["admin"]
|
||||||
|
},
|
||||||
|
"inputMapping": {},
|
||||||
|
"outputs": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "endNode",
|
||||||
|
"nodeCode": "end",
|
||||||
|
"nodeType": "END",
|
||||||
|
"nodeName": "结束",
|
||||||
|
"position": {
|
||||||
|
"x": 500,
|
||||||
|
"y": 200
|
||||||
|
},
|
||||||
|
"configs": {},
|
||||||
|
"inputMapping": {},
|
||||||
|
"outputs": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"edges": [
|
||||||
|
{
|
||||||
|
"source": "startNode",
|
||||||
|
"target": "sid_4ee9fab9_d626_4691_816c_816c_9435902fc3d0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "sid_4ee9fab9_d626_4691_816c_9435902fc3d0",
|
||||||
|
"target": "endNode"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nodeInstances": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"processInstanceId": "b24d97ec-b97f-11f0-be70-de5a4815c9ef",
|
||||||
|
"nodeId": "startNode",
|
||||||
|
"nodeName": "开始",
|
||||||
|
"nodeType": "START",
|
||||||
|
"status": "COMPLETED",
|
||||||
|
"startTime": "2025-11-04T21:10:00",
|
||||||
|
"endTime": "2025-11-04T21:10:01"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"processInstanceId": "b24d97ec-b97f-11f0-be70-de5a4815c9ef",
|
||||||
|
"nodeId": "sid_4ee9fab9_d626_4691_816c_9435902fc3d0",
|
||||||
|
"nodeName": "审批节点",
|
||||||
|
"nodeType": "APPROVAL",
|
||||||
|
"status": "REJECTED",
|
||||||
|
"startTime": "2025-11-04T21:10:01",
|
||||||
|
"endTime": "2025-11-04T21:14:34"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": null,
|
||||||
|
"processInstanceId": "b24d97ec-b97f-11f0-be70-de5a4815c9ef",
|
||||||
|
"nodeId": "endNode",
|
||||||
|
"nodeName": "结束",
|
||||||
|
"nodeType": "END",
|
||||||
|
"status": "NOT_STARTED",
|
||||||
|
"startTime": null,
|
||||||
|
"endTime": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 数据结构说明
|
||||||
|
|
||||||
|
### DeployRecordFlowGraphDTO
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `deployRecordId` | Long | 部署记录ID |
|
||||||
|
| `workflowInstanceId` | Long | 工作流实例ID |
|
||||||
|
| `processInstanceId` | String | Flowable流程实例ID |
|
||||||
|
| `deployStatus` | String | 部署状态(CREATED/PENDING_APPROVAL/RUNNING/SUCCESS/FAILED/REJECTED/CANCELLED/TERMINATED/PARTIAL_SUCCESS) |
|
||||||
|
| `graph` | WorkflowDefinitionGraph | 流程图结构数据(包含节点和边的位置信息) |
|
||||||
|
| `nodeInstances` | List<WorkflowNodeInstanceDTO> | 节点执行状态列表 |
|
||||||
|
|
||||||
|
### WorkflowDefinitionGraph
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `nodes` | List<WorkflowDefinitionGraphNode> | 节点列表(包含位置信息) |
|
||||||
|
| `edges` | List<WorkflowDefinitionGraphEdge> | 边列表(连接关系) |
|
||||||
|
|
||||||
|
### WorkflowDefinitionGraphNode
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `id` | String | 节点ID(用于匹配nodeInstances) |
|
||||||
|
| `nodeCode` | String | 节点代码 |
|
||||||
|
| `nodeType` | String | 节点类型(START/END/APPROVAL/SHELL等) |
|
||||||
|
| `nodeName` | String | 节点名称 |
|
||||||
|
| `position` | Map<String, Object> | 节点位置信息(x, y坐标) |
|
||||||
|
| `configs` | Map<String, Object> | 节点配置信息 |
|
||||||
|
| `inputMapping` | Map<String, Object> | 输入映射 |
|
||||||
|
| `outputs` | List | 输出字段列表 |
|
||||||
|
|
||||||
|
### WorkflowNodeInstanceDTO
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `id` | Long | 节点实例ID(可能为null,表示未开始) |
|
||||||
|
| `nodeId` | String | 节点ID(与graph.nodes中的id匹配) |
|
||||||
|
| `nodeName` | String | 节点名称 |
|
||||||
|
| `nodeType` | String | 节点类型 |
|
||||||
|
| `status` | String | 节点执行状态(NOT_STARTED/RUNNING/COMPLETED/FAILED/REJECTED等) |
|
||||||
|
| `startTime` | LocalDateTime | 开始时间 |
|
||||||
|
| `endTime` | LocalDateTime | 结束时间 |
|
||||||
|
|
||||||
|
## 前端绘制流程
|
||||||
|
|
||||||
|
### 1. 调用API获取数据
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 使用React示例
|
||||||
|
const fetchDeployFlowGraph = async (deployRecordId: number) => {
|
||||||
|
const response = await fetch(`/api/v1/deploy/records/${deployRecordId}/flow-graph`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
return result.data;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 绘制流程图
|
||||||
|
|
||||||
|
使用流程图库(如 `react-flow`、`antv x6`、`mxGraph` 等)绘制流程图:
|
||||||
|
|
||||||
|
#### 步骤1:创建节点映射
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 将nodeInstances转换为Map,方便查找
|
||||||
|
const nodeStatusMap = new Map(
|
||||||
|
data.nodeInstances.map(node => [node.nodeId, node])
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 步骤2:根据graph.nodes创建节点
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const flowNodes = data.graph.nodes.map(node => {
|
||||||
|
const nodeInstance = nodeStatusMap.get(node.id);
|
||||||
|
const status = nodeInstance?.status || 'NOT_STARTED';
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: node.id,
|
||||||
|
type: mapNodeType(node.nodeType), // 根据节点类型选择不同的节点组件
|
||||||
|
position: { x: node.position.x, y: node.position.y },
|
||||||
|
data: {
|
||||||
|
label: node.nodeName,
|
||||||
|
nodeType: node.nodeType,
|
||||||
|
status: status, // 节点状态,用于样式标记
|
||||||
|
nodeInstance: nodeInstance, // 完整的节点实例信息
|
||||||
|
configs: node.configs
|
||||||
|
},
|
||||||
|
style: getNodeStyle(status) // 根据状态设置样式
|
||||||
|
};
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 步骤3:根据graph.edges创建边
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const flowEdges = data.graph.edges.map(edge => ({
|
||||||
|
id: `${edge.source}-${edge.target}`,
|
||||||
|
source: edge.source,
|
||||||
|
target: edge.target,
|
||||||
|
style: getEdgeStyle(edge, nodeStatusMap) // 根据节点状态设置边的样式
|
||||||
|
}));
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 节点状态样式映射
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 节点状态颜色映射
|
||||||
|
const statusColorMap = {
|
||||||
|
'NOT_STARTED': '#d9d9d9', // 灰色 - 未开始
|
||||||
|
'RUNNING': '#1890ff', // 蓝色 - 运行中
|
||||||
|
'COMPLETED': '#52c41a', // 绿色 - 已完成
|
||||||
|
'FAILED': '#ff4d4f', // 红色 - 失败
|
||||||
|
'REJECTED': '#ff4d4f', // 红色 - 审批被拒绝
|
||||||
|
'CANCELLED': '#d9d9d9', // 灰色 - 已取消
|
||||||
|
'TERMINATED': '#ff4d4f' // 红色 - 已终止
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNodeStyle = (status: string) => {
|
||||||
|
return {
|
||||||
|
background: statusColorMap[status] || '#d9d9d9',
|
||||||
|
border: `2px solid ${statusColorMap[status] || '#d9d9d9'}`,
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '10px',
|
||||||
|
color: '#fff',
|
||||||
|
fontWeight: 'bold'
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 边的状态样式
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const getEdgeStyle = (edge: any, nodeStatusMap: Map<string, any>) => {
|
||||||
|
const sourceStatus = nodeStatusMap.get(edge.source)?.status || 'NOT_STARTED';
|
||||||
|
const targetStatus = nodeStatusMap.get(edge.target)?.status || 'NOT_STARTED';
|
||||||
|
|
||||||
|
// 如果源节点已完成,边显示为已完成
|
||||||
|
if (sourceStatus === 'COMPLETED') {
|
||||||
|
return {
|
||||||
|
stroke: '#52c41a',
|
||||||
|
strokeWidth: 2
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果源节点失败或被拒绝,边显示为失败
|
||||||
|
if (sourceStatus === 'FAILED' || sourceStatus === 'REJECTED') {
|
||||||
|
return {
|
||||||
|
stroke: '#ff4d4f',
|
||||||
|
strokeWidth: 2,
|
||||||
|
strokeDasharray: '5,5' // 虚线表示流程中断
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认样式
|
||||||
|
return {
|
||||||
|
stroke: '#d9d9d9',
|
||||||
|
strokeWidth: 1
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 整体部署状态标记
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 根据deployStatus显示整体状态
|
||||||
|
const deployStatusMap = {
|
||||||
|
'CREATED': { color: '#1890ff', text: '已创建' },
|
||||||
|
'PENDING_APPROVAL': { color: '#faad14', text: '待审批' },
|
||||||
|
'RUNNING': { color: '#1890ff', text: '运行中' },
|
||||||
|
'SUCCESS': { color: '#52c41a', text: '部署成功' },
|
||||||
|
'FAILED': { color: '#ff4d4f', text: '部署失败' },
|
||||||
|
'REJECTED': { color: '#ff4d4f', text: '审批被拒绝' },
|
||||||
|
'CANCELLED': { color: '#d9d9d9', text: '已取消' },
|
||||||
|
'TERMINATED': { color: '#ff4d4f', text: '已终止' },
|
||||||
|
'PARTIAL_SUCCESS': { color: '#faad14', text: '部分成功' }
|
||||||
|
};
|
||||||
|
|
||||||
|
// 在流程图上方显示整体状态
|
||||||
|
const statusInfo = deployStatusMap[data.deployStatus];
|
||||||
|
```
|
||||||
|
|
||||||
|
## 完整示例(React + react-flow)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import ReactFlow, { Node, Edge } from 'react-flow-renderer';
|
||||||
|
|
||||||
|
interface DeployFlowGraphProps {
|
||||||
|
deployRecordId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeployFlowGraph: React.FC<DeployFlowGraphProps> = ({ deployRecordId }) => {
|
||||||
|
const [nodes, setNodes] = useState<Node[]>([]);
|
||||||
|
const [edges, setEdges] = useState<Edge[]>([]);
|
||||||
|
const [deployStatus, setDeployStatus] = useState<string>('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchDeployFlowGraph(deployRecordId).then(data => {
|
||||||
|
// 创建节点状态映射
|
||||||
|
const nodeStatusMap = new Map(
|
||||||
|
data.nodeInstances.map(node => [node.nodeId, node])
|
||||||
|
);
|
||||||
|
|
||||||
|
// 创建节点
|
||||||
|
const flowNodes = data.graph.nodes.map(node => {
|
||||||
|
const nodeInstance = nodeStatusMap.get(node.id);
|
||||||
|
const status = nodeInstance?.status || 'NOT_STARTED';
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: node.id,
|
||||||
|
type: 'default',
|
||||||
|
position: { x: node.position.x, y: node.position.y },
|
||||||
|
data: {
|
||||||
|
label: (
|
||||||
|
<div>
|
||||||
|
<div>{node.nodeName}</div>
|
||||||
|
<div style={{ fontSize: '12px', color: getStatusColor(status) }}>
|
||||||
|
{getStatusText(status)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
status: status
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
background: getStatusColor(status),
|
||||||
|
color: '#fff',
|
||||||
|
border: `2px solid ${getStatusColor(status)}`,
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '10px',
|
||||||
|
width: 150
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建边
|
||||||
|
const flowEdges = data.graph.edges.map(edge => ({
|
||||||
|
id: `${edge.source}-${edge.target}`,
|
||||||
|
source: edge.source,
|
||||||
|
target: edge.target,
|
||||||
|
style: {
|
||||||
|
stroke: getEdgeColor(edge, nodeStatusMap),
|
||||||
|
strokeWidth: 2
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
setNodes(flowNodes);
|
||||||
|
setEdges(flowEdges);
|
||||||
|
setDeployStatus(data.deployStatus);
|
||||||
|
});
|
||||||
|
}, [deployRecordId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div style={{ marginBottom: '20px' }}>
|
||||||
|
<h3>部署状态:{getDeployStatusText(deployStatus)}</h3>
|
||||||
|
</div>
|
||||||
|
<ReactFlow nodes={nodes} edges={edges} fitView />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **节点ID匹配**:`graph.nodes` 中的 `id` 与 `nodeInstances` 中的 `nodeId` 需要匹配,用于确定每个节点的执行状态。
|
||||||
|
|
||||||
|
2. **未开始的节点**:如果某个节点还没有开始执行,`nodeInstances` 中可能没有对应的记录,或者 `status` 为 `NOT_STARTED`,`id` 可能为 `null`。
|
||||||
|
|
||||||
|
3. **状态优先级**:
|
||||||
|
- 如果部署状态为 `REJECTED`,说明审批被拒绝,相关审批节点的状态应该也是 `REJECTED`。
|
||||||
|
- 如果部署状态为 `PARTIAL_SUCCESS`,说明部分节点失败,需要检查哪些节点的状态是 `FAILED`。
|
||||||
|
|
||||||
|
4. **节点位置**:`graph.nodes` 中的 `position` 字段包含了节点在画布上的位置信息(x, y坐标),直接使用即可。
|
||||||
|
|
||||||
|
5. **边的绘制顺序**:`graph.edges` 中的边定义了节点之间的连接关系,按照 `source` 和 `target` 绘制即可。
|
||||||
|
|
||||||
|
## 状态流转示意
|
||||||
|
|
||||||
|
```
|
||||||
|
CREATED → PENDING_APPROVAL → RUNNING → SUCCESS
|
||||||
|
↓
|
||||||
|
FAILED/REJECTED
|
||||||
|
```
|
||||||
|
|
||||||
|
- **CREATED**:部署记录已创建,工作流已启动
|
||||||
|
- **PENDING_APPROVAL**:等待审批中
|
||||||
|
- **RUNNING**:审批通过,正在执行部署
|
||||||
|
- **SUCCESS**:部署成功
|
||||||
|
- **FAILED**:部署失败
|
||||||
|
- **REJECTED**:审批被拒绝(终态)
|
||||||
|
- **CANCELLED**:已取消
|
||||||
|
- **TERMINATED**:已终止
|
||||||
|
- **PARTIAL_SUCCESS**:部分成功(工作流完成但存在失败的节点)
|
||||||
|
|
||||||
82
backend/docs/deploy-record-status-analysis.md
Normal file
82
backend/docs/deploy-record-status-analysis.md
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
# 部署记录状态枚举分析
|
||||||
|
|
||||||
|
## 状态使用情况统计
|
||||||
|
|
||||||
|
### ✅ 需要保留的状态
|
||||||
|
|
||||||
|
| 状态 | 使用场景 | 来源 | 是否终态 | 备注 |
|
||||||
|
|------|---------|------|---------|------|
|
||||||
|
| **CREATED** | 1. 创建记录时初始状态<br>2. 工作流状态转换<br>3. 统计查询(30分钟内视为运行中)<br>4. isDeploying 判断 | 工作流状态 CREATED | ❌ | 初始状态,30分钟内视为正在部署 |
|
||||||
|
| **PENDING_APPROVAL** | 1. 审批任务创建时设置 | 审批事件 | ❌ | 审批阶段专用状态 |
|
||||||
|
| **RUNNING** | 1. 审批通过时设置<br>2. 工作流状态转换<br>3. 统计查询<br>4. isDeploying 判断 | 工作流状态 RUNNING<br>审批通过事件 | ❌ | 运行中状态 |
|
||||||
|
| **SUCCESS** | 1. 工作流状态转换(COMPLETED)<br>2. 统计查询(成功计数) | 工作流状态 COMPLETED | ✅ | 部署成功 |
|
||||||
|
| **FAILED** | 1. 工作流状态转换<br>2. 统计查询(失败计数) | 工作流状态 FAILED | ✅ | 部署失败 |
|
||||||
|
| **PARTIAL_SUCCESS** | 1. 工作流状态转换(COMPLETED_WITH_ERRORS)<br>2. 统计查询(失败计数) | 工作流状态 COMPLETED_WITH_ERRORS | ✅ | 部分成功(存在失败节点) |
|
||||||
|
| **CANCELLED** | 1. 审批被拒绝时设置<br>2. 统计查询(失败计数) | 审批拒绝事件 | ✅ | 已取消(审批被拒) |
|
||||||
|
| **TERMINATED** | 1. 工作流状态转换<br>2. 统计查询(失败计数) | 工作流状态 TERMINATED | ✅ | 已终止(手动终止) |
|
||||||
|
|
||||||
|
### ❓ 可能不需要的状态
|
||||||
|
|
||||||
|
| 状态 | 使用场景 | 来源 | 是否终态 | 问题分析 |
|
||||||
|
|------|---------|------|---------|---------|
|
||||||
|
| **SUSPENDED** | 1. 工作流状态转换(但实际未使用) | 工作流状态 SUSPENDED | ❌ | **问题**:<br>1. ❌ 未在 `isFinalState()` 中(不是终态)<br>2. ❌ 未在统计查询中使用<br>3. ❌ 未在 `isDeploying` 判断中使用<br>4. ❌ 部署场景中可能不需要暂停功能<br>5. ✅ 工作流层面支持,但部署业务中无用 |
|
||||||
|
|
||||||
|
## 详细分析
|
||||||
|
|
||||||
|
### SUSPENDED 状态分析
|
||||||
|
|
||||||
|
#### 当前实现
|
||||||
|
- ✅ 在 `fromWorkflowStatus()` 中支持转换:`SUSPENDED -> SUSPENDED`
|
||||||
|
- ✅ 工作流层面支持:`ProcessEventHandler` 会处理 `PROCESS_SUSPENDED` 事件
|
||||||
|
|
||||||
|
#### 未使用的地方
|
||||||
|
1. **未在 `isFinalState()` 中**:说明它不是终态,可以恢复
|
||||||
|
2. **未在统计查询中使用**:部署统计不关心暂停状态
|
||||||
|
3. **未在 `isDeploying` 判断中使用**:暂停状态不视为正在部署
|
||||||
|
4. **实际业务场景**:部署流程一般不需要暂停功能
|
||||||
|
|
||||||
|
#### 建议
|
||||||
|
**可以考虑移除 SUSPENDED 状态**,原因:
|
||||||
|
1. 部署是一次性操作,不需要暂停/恢复功能
|
||||||
|
2. 如果需要停止部署,应该使用 `TERMINATED` 或 `CANCELLED`
|
||||||
|
3. 暂停功能更适合长流程(如审批流程),不适合部署
|
||||||
|
|
||||||
|
### 保留 SUSPENDED 的理由(如果保留)
|
||||||
|
1. 未来可能支持部署暂停功能
|
||||||
|
2. 保持与工作流状态枚举的一致性
|
||||||
|
3. 如果移除,需要在 `fromWorkflowStatus()` 中处理 `SUSPENDED` 状态(可能转换为 `RUNNING` 或其他状态)
|
||||||
|
|
||||||
|
## 推荐方案
|
||||||
|
|
||||||
|
### 方案A:移除 SUSPENDED(推荐)
|
||||||
|
|
||||||
|
**优点**:
|
||||||
|
- 简化状态模型
|
||||||
|
- 符合实际业务需求
|
||||||
|
- 减少不必要的状态转换
|
||||||
|
|
||||||
|
**需要修改**:
|
||||||
|
1. 从枚举中移除 `SUSPENDED`
|
||||||
|
2. 修改 `fromWorkflowStatus()`,将 `SUSPENDED` 转换为其他状态(如 `RUNNING` 或 `FAILED`)
|
||||||
|
3. 更新文档
|
||||||
|
|
||||||
|
### 方案B:保留 SUSPENDED(保守)
|
||||||
|
|
||||||
|
**优点**:
|
||||||
|
- 保持与工作流状态一致
|
||||||
|
- 为未来扩展预留空间
|
||||||
|
|
||||||
|
**需要完善**:
|
||||||
|
1. 在 `isFinalState()` 中明确处理(虽然不是终态,但需要明确逻辑)
|
||||||
|
2. 在统计查询中明确如何处理(计入哪个分类)
|
||||||
|
3. 在 `isDeploying` 判断中明确处理
|
||||||
|
|
||||||
|
## 结论
|
||||||
|
|
||||||
|
**推荐移除 `SUSPENDED` 状态**,因为:
|
||||||
|
1. 部署业务不需要暂停功能
|
||||||
|
2. 当前完全未使用
|
||||||
|
3. 简化状态模型,减少维护成本
|
||||||
|
|
||||||
|
如果未来需要暂停功能,可以再添加回来。
|
||||||
|
|
||||||
202
backend/docs/deploy-record-status-rejected-analysis.md
Normal file
202
backend/docs/deploy-record-status-rejected-analysis.md
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
# 部署记录状态:CANCELLED vs REJECTED 分析
|
||||||
|
|
||||||
|
## 当前状态
|
||||||
|
|
||||||
|
### CANCELLED(已取消)的使用场景
|
||||||
|
|
||||||
|
**唯一使用场景**:审批被拒绝
|
||||||
|
- 位置:`DeployRecordServiceImpl.updateStatusFromApproval()`
|
||||||
|
- 代码:`record.setStatus(DeployRecordStatusEnums.CANCELLED);`
|
||||||
|
- 日志:`"部署记录状态已更新为已取消(审批被拒)"`
|
||||||
|
|
||||||
|
**结论**:当前 `CANCELLED` 只用于审批被拒绝的场景。
|
||||||
|
|
||||||
|
### 其他状态的使用场景
|
||||||
|
|
||||||
|
| 状态 | 使用场景 | 来源 |
|
||||||
|
|------|---------|------|
|
||||||
|
| **TERMINATED** | 工作流被手动终止 | 工作流状态 `TERMINATED` |
|
||||||
|
| **FAILED** | 工作流执行失败 | 工作流状态 `FAILED` |
|
||||||
|
| **CANCELLED** | 审批被拒绝 | 审批事件 `ApprovalResultEnum.REJECTED` |
|
||||||
|
|
||||||
|
## 问题分析
|
||||||
|
|
||||||
|
### 问题1:语义不清晰
|
||||||
|
|
||||||
|
**当前问题**:
|
||||||
|
- `CANCELLED`(已取消)语义模糊,不能明确表示是"审批被拒绝"
|
||||||
|
- 未来如果有手动取消部署功能,会产生歧义
|
||||||
|
|
||||||
|
**对比**:
|
||||||
|
- 工作流层面:`ApprovalResultEnum.REJECTED`(拒绝)
|
||||||
|
- 部署记录层面:`CANCELLED`(已取消)
|
||||||
|
- **不一致**:两个层面使用不同的语义
|
||||||
|
|
||||||
|
### 问题2:未来扩展性
|
||||||
|
|
||||||
|
**未来可能的场景**:
|
||||||
|
1. **手动取消部署**:用户主动取消正在运行的部署
|
||||||
|
- 应该使用什么状态?如果用 `CANCELLED`,会与"审批被拒绝"混淆
|
||||||
|
2. **超时自动取消**:部署超时自动取消
|
||||||
|
- 应该使用什么状态?
|
||||||
|
|
||||||
|
## 方案对比
|
||||||
|
|
||||||
|
### 方案A:保留 CANCELLED,新增 REJECTED(推荐)
|
||||||
|
|
||||||
|
**状态定义**:
|
||||||
|
```java
|
||||||
|
public enum DeployRecordStatusEnums {
|
||||||
|
// ... 其他状态 ...
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审批被拒绝(终态)
|
||||||
|
*/
|
||||||
|
REJECTED("REJECTED", "审批被拒绝"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已取消(终态)
|
||||||
|
* 用于:手动取消部署、超时自动取消等场景
|
||||||
|
*/
|
||||||
|
CANCELLED("CANCELLED", "已取消"),
|
||||||
|
|
||||||
|
// ... 其他状态 ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**优点**:
|
||||||
|
1. ✅ 语义清晰:`REJECTED` 明确表示"审批被拒绝"
|
||||||
|
2. ✅ 与工作流层面保持一致:`ApprovalResultEnum.REJECTED` → `DeployRecordStatusEnums.REJECTED`
|
||||||
|
3. ✅ 保留扩展性:`CANCELLED` 可用于未来手动取消场景
|
||||||
|
4. ✅ 状态语义明确,便于理解和维护
|
||||||
|
|
||||||
|
**缺点**:
|
||||||
|
- 需要新增枚举值
|
||||||
|
- 需要修改相关代码
|
||||||
|
- 需要数据库迁移(如果已有数据)
|
||||||
|
|
||||||
|
**需要修改的地方**:
|
||||||
|
1. 枚举类:新增 `REJECTED`
|
||||||
|
2. `DeployRecordServiceImpl.updateStatusFromApproval()`:改为 `REJECTED`
|
||||||
|
3. `isFinalState()`:添加 `REJECTED`
|
||||||
|
4. 统计查询:`REJECTED` 计入失败计数
|
||||||
|
5. `fromWorkflowStatus()`:不需要修改(因为 `REJECTED` 不是从工作流状态转换来的)
|
||||||
|
|
||||||
|
### 方案B:直接用 REJECTED 替代 CANCELLED
|
||||||
|
|
||||||
|
**状态定义**:
|
||||||
|
```java
|
||||||
|
public enum DeployRecordStatusEnums {
|
||||||
|
// ... 其他状态 ...
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审批被拒绝(终态)
|
||||||
|
*/
|
||||||
|
REJECTED("REJECTED", "审批被拒绝"),
|
||||||
|
|
||||||
|
// 删除 CANCELLED
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**优点**:
|
||||||
|
1. ✅ 语义清晰
|
||||||
|
2. ✅ 与工作流层面保持一致
|
||||||
|
3. ✅ 简化状态模型
|
||||||
|
|
||||||
|
**缺点**:
|
||||||
|
1. ❌ 失去扩展性:如果未来需要手动取消功能,需要再添加状态
|
||||||
|
2. ❌ 如果未来需要区分"审批被拒绝"和"手动取消",需要再次修改
|
||||||
|
|
||||||
|
### 方案C:保持现状(不推荐)
|
||||||
|
|
||||||
|
**优点**:
|
||||||
|
- 无需修改代码
|
||||||
|
|
||||||
|
**缺点**:
|
||||||
|
1. ❌ 语义不清晰
|
||||||
|
2. ❌ 与工作流层面不一致
|
||||||
|
3. ❌ 未来扩展困难
|
||||||
|
|
||||||
|
## 推荐方案
|
||||||
|
|
||||||
|
**推荐采用方案A:保留 CANCELLED,新增 REJECTED**
|
||||||
|
|
||||||
|
### 理由
|
||||||
|
|
||||||
|
1. **语义清晰**:
|
||||||
|
- `REJECTED`:明确表示"审批被拒绝"
|
||||||
|
- `CANCELLED`:用于"手动取消"等场景
|
||||||
|
|
||||||
|
2. **状态映射清晰**:
|
||||||
|
```
|
||||||
|
审批层面:ApprovalResultEnum.REJECTED
|
||||||
|
↓
|
||||||
|
部署记录:DeployRecordStatusEnums.REJECTED
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **未来扩展性**:
|
||||||
|
- 如果未来需要手动取消部署功能,可以直接使用 `CANCELLED`
|
||||||
|
- 如果不需要手动取消,`CANCELLED` 可以保留但不用
|
||||||
|
|
||||||
|
4. **状态完整性**:
|
||||||
|
- 终态包括:`SUCCESS`、`FAILED`、`REJECTED`、`CANCELLED`、`TERMINATED`、`PARTIAL_SUCCESS`
|
||||||
|
- 语义清晰,便于理解和维护
|
||||||
|
|
||||||
|
## 实施建议
|
||||||
|
|
||||||
|
### 步骤1:新增 REJECTED 枚举
|
||||||
|
|
||||||
|
```java
|
||||||
|
/**
|
||||||
|
* 审批被拒绝(终态)
|
||||||
|
*/
|
||||||
|
REJECTED("REJECTED", "审批被拒绝"),
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤2:修改审批状态更新逻辑
|
||||||
|
|
||||||
|
```java
|
||||||
|
} else {
|
||||||
|
// 审批被拒绝,更新为审批被拒绝
|
||||||
|
record.setStatus(DeployRecordStatusEnums.REJECTED);
|
||||||
|
log.info("部署记录状态已更新为审批被拒绝: id={}, workflowInstanceId={}",
|
||||||
|
record.getId(), workflowInstanceId);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤3:更新终态判断
|
||||||
|
|
||||||
|
```java
|
||||||
|
private boolean isFinalState(DeployRecordStatusEnums status) {
|
||||||
|
return status == DeployRecordStatusEnums.SUCCESS
|
||||||
|
|| status == DeployRecordStatusEnums.FAILED
|
||||||
|
|| status == DeployRecordStatusEnums.REJECTED // 新增
|
||||||
|
|| status == DeployRecordStatusEnums.CANCELLED
|
||||||
|
|| status == DeployRecordStatusEnums.TERMINATED
|
||||||
|
|| status == DeployRecordStatusEnums.PARTIAL_SUCCESS;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤4:更新统计查询
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 在统计查询中,REJECTED 计入失败
|
||||||
|
SUM(CASE WHEN dr.status IN ('FAILED', 'REJECTED', 'CANCELLED', 'TERMINATED', 'PARTIAL_SUCCESS')
|
||||||
|
OR (dr.status = 'CREATED' AND TIMESTAMPDIFF(MINUTE, dr.create_time, NOW()) > 30)
|
||||||
|
THEN 1 ELSE 0 END) as failedCount
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤5:数据库迁移(可选)
|
||||||
|
|
||||||
|
如果已有审批被拒绝的记录,状态为 `CANCELLED`,可以考虑:
|
||||||
|
- 保留现有数据(`CANCELLED` 继续表示审批被拒绝)
|
||||||
|
- 或者迁移数据(将 `CANCELLED` 迁移为 `REJECTED`)
|
||||||
|
|
||||||
|
## 结论
|
||||||
|
|
||||||
|
**推荐新增 `REJECTED` 状态**,用于明确表示"审批被拒绝",与工作流层面的 `ApprovalResultEnum.REJECTED` 保持一致。
|
||||||
|
|
||||||
|
**保留 `CANCELLED` 状态**,用于未来可能的手动取消部署功能。
|
||||||
|
|
||||||
|
这样既保证了语义清晰,又保留了扩展性。
|
||||||
|
|
||||||
805
backend/docs/workflow-listener-executor-lifecycle.md
Normal file
805
backend/docs/workflow-listener-executor-lifecycle.md
Normal file
@ -0,0 +1,805 @@
|
|||||||
|
# 工作流执行器和监听器生命周期文档
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
- [概述](#概述)
|
||||||
|
- [监听器分类与配置方式](#监听器分类与配置方式)
|
||||||
|
- [架构分层](#架构分层)
|
||||||
|
- [完整生命周期流程](#完整生命周期流程)
|
||||||
|
- [事件流转图](#事件流转图)
|
||||||
|
- [核心组件详解](#核心组件详解)
|
||||||
|
- [扩展指南](#扩展指南)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
本文档描述了工作流系统中所有执行器(Delegate)和监听器(Listener)的完整生命周期,以及事件驱动的状态同步机制。系统采用分层架构,通过事件解耦,实现业务模块的独立扩展。
|
||||||
|
|
||||||
|
### 核心设计理念
|
||||||
|
|
||||||
|
1. **事件驱动架构**:Flowable 引擎事件 → 业务事件 → 业务状态同步
|
||||||
|
2. **职责分离**:工作流模块负责流程,业务模块负责状态
|
||||||
|
3. **可扩展性**:通过监听业务事件,各业务模块可独立扩展状态同步逻辑
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 监听器分类与配置方式
|
||||||
|
|
||||||
|
### 为什么要在 XML 中配置监听器?
|
||||||
|
|
||||||
|
**Flowable 引擎的监听机制**:
|
||||||
|
- Flowable 引擎在解析 BPMN XML 时,会识别 XML 中配置的监听器
|
||||||
|
- 在流程执行到对应节点时,Flowable 会自动调用这些监听器
|
||||||
|
- 这是 Flowable 的标准机制,必须通过 XML 配置才能被引擎识别和调用
|
||||||
|
|
||||||
|
**配置方式分类**:
|
||||||
|
|
||||||
|
#### 1. XML 配置的监听器(Flowable 引擎自动调用)
|
||||||
|
|
||||||
|
这些监听器必须在 BPMN XML 中配置,Flowable 引擎在解析 XML 时会注册它们,并在流程执行时自动调用。
|
||||||
|
|
||||||
|
**ExecutionListener(执行监听器)**:
|
||||||
|
- 监听节点执行的开始和结束
|
||||||
|
- 配置方式:`<flowable:executionListener event="start|end" delegateExpression="${beanName}"/>`
|
||||||
|
- 触发时机:节点执行时自动触发
|
||||||
|
|
||||||
|
**TaskListener(任务监听器)**:
|
||||||
|
- 监听任务的生命周期(create、assignment、complete、delete)
|
||||||
|
- 配置方式:`<flowable:taskListener event="create" delegateExpression="${beanName}"/>`
|
||||||
|
- 触发时机:任务创建/分配/完成时自动触发
|
||||||
|
|
||||||
|
**JavaDelegate(任务委派)**:
|
||||||
|
- 执行具体的业务逻辑
|
||||||
|
- 配置方式:`<serviceTask flowable:delegateExpression="${beanName}"/>`
|
||||||
|
- 触发时机:ServiceTask 执行时自动调用
|
||||||
|
|
||||||
|
#### 2. 代码注册的监听器(Flowable 引擎事件监听)
|
||||||
|
|
||||||
|
这些监听器通过代码注册到 Flowable 引擎,监听引擎级别的事件。
|
||||||
|
|
||||||
|
**FlowableEventListener**:
|
||||||
|
- 监听 Flowable 引擎的所有事件(PROCESS_*、ACTIVITY_*、TASK_*、JOB_* 等)
|
||||||
|
- 注册方式:通过 `ProcessEngineConfiguration.addEventDispatcher()` 注册
|
||||||
|
- 触发时机:Flowable 引擎事件发生时自动触发
|
||||||
|
|
||||||
|
#### 3. Spring 事件监听器(业务事件监听)
|
||||||
|
|
||||||
|
这些监听器监听 Spring 应用事件,由业务代码发布的事件触发。
|
||||||
|
|
||||||
|
**@EventListener**:
|
||||||
|
- 监听 Spring 应用事件
|
||||||
|
- 注册方式:使用 `@EventListener` 注解,Spring 自动注册
|
||||||
|
- 触发时机:业务代码发布事件时自动触发
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 架构分层
|
||||||
|
|
||||||
|
### 第一层:Flowable 引擎层(代码注册)
|
||||||
|
|
||||||
|
Flowable 引擎在流程执行过程中会触发各种事件,这些事件由 `FlowableEventDispatcher` 统一分发。
|
||||||
|
|
||||||
|
#### FlowableEventDispatcher
|
||||||
|
- **注册方式**:通过 `ProcessEngineConfiguration.addEventListener()` 注册到 Flowable 引擎
|
||||||
|
- **职责**:接收 Flowable 引擎的所有事件,分发给对应的处理器
|
||||||
|
- **位置**:`workflow.event.FlowableEventDispatcher`
|
||||||
|
- **事件类型**:
|
||||||
|
- `PROCESS_*`:流程级别事件(PROCESS_CREATED, PROCESS_STARTED, PROCESS_COMPLETED 等)
|
||||||
|
- `ACTIVITY_*`:活动级别事件(ACTIVITY_STARTED, ACTIVITY_COMPLETED, ACTIVITY_CANCELLED 等)
|
||||||
|
- `TASK_*`:任务级别事件(TASK_CREATED, TASK_ASSIGNED, TASK_COMPLETED 等)
|
||||||
|
- `JOB_*`:作业级别事件(JOB_EXECUTION_SUCCESS, JOB_EXECUTION_FAILURE 等)
|
||||||
|
- **为什么代码注册**:这是 Flowable 引擎的事件分发机制,需要在引擎初始化时注册
|
||||||
|
|
||||||
|
### 第二层:Flowable 事件处理器层(代码注册)
|
||||||
|
|
||||||
|
将 Flowable 引擎事件转换为业务事件。这些处理器通过 Spring 自动注册。
|
||||||
|
|
||||||
|
#### ProcessEventHandler
|
||||||
|
- **注册方式**:通过 Spring `@Component` 自动注册到 `FlowableEventDispatcher`
|
||||||
|
- **职责**:处理流程级别事件(PROCESS_CREATED, PROCESS_STARTED, PROCESS_COMPLETED 等)
|
||||||
|
- **位置**:`workflow.event.handler.ProcessEventHandler`
|
||||||
|
- **监听事件**:`PROCESS_*` 开头的所有事件
|
||||||
|
- **状态流转**:
|
||||||
|
- `PROCESS_CREATED` → `CREATED`
|
||||||
|
- `PROCESS_STARTED` → `RUNNING`
|
||||||
|
- `PROCESS_COMPLETED` → 检查节点状态 → `COMPLETED` 或 `COMPLETED_WITH_ERRORS`
|
||||||
|
- `PROCESS_CANCELLED` → `FAILED`
|
||||||
|
- `PROCESS_SUSPENDED` → `SUSPENDED`
|
||||||
|
- **输出事件**:`WorkflowInstanceStatusChangeEvent`
|
||||||
|
- **为什么代码注册**:这是业务事件处理器,通过 Spring 自动注册到 FlowableEventDispatcher
|
||||||
|
|
||||||
|
#### ActivityEventHandler
|
||||||
|
- **注册方式**:通过 Spring `@Component` 自动注册到 `FlowableEventDispatcher`
|
||||||
|
- **职责**:处理活动级别事件(ACTIVITY_CANCELLED, ACTIVITY_ERROR_RECEIVED 等)
|
||||||
|
- **位置**:`workflow.event.handler.ActivityEventHandler`
|
||||||
|
- **监听事件**:`ACTIVITY_*` 开头的所有事件
|
||||||
|
- **状态流转**:
|
||||||
|
- `ACTIVITY_CANCELLED` → `TERMINATED`
|
||||||
|
- `ACTIVITY_ERROR_RECEIVED` → `FAILED`
|
||||||
|
- **输出事件**:`WorkflowNodeInstanceStatusChangeEvent`
|
||||||
|
- **为什么代码注册**:这是业务事件处理器,通过 Spring 自动注册
|
||||||
|
|
||||||
|
#### JobEventHandler
|
||||||
|
- **注册方式**:通过 Spring `@Component` 自动注册到 `FlowableEventDispatcher`
|
||||||
|
- **职责**:处理作业级别事件(JOB_EXECUTION_SUCCESS, JOB_EXECUTION_FAILURE 等)
|
||||||
|
- **位置**:`workflow.event.handler.JobEventHandler`
|
||||||
|
- **监听事件**:`JOB_EXECUTION_*` 开头的所有事件
|
||||||
|
- **状态流转**:
|
||||||
|
- `JOB_EXECUTION_SUCCESS` → `RUNNING`
|
||||||
|
- `JOB_EXECUTION_FAILURE` → `FAILED` → 发布 `TerminationProcessInstanceListenerEvent`
|
||||||
|
- **为什么代码注册**:这是业务事件处理器,通过 Spring 自动注册
|
||||||
|
|
||||||
|
### 第三层:节点执行监听器层(XML 配置)
|
||||||
|
|
||||||
|
监听节点执行生命周期,管理节点实例状态。这些监听器在 BPMN XML 中配置,Flowable 引擎自动调用。
|
||||||
|
|
||||||
|
#### GlobalNodeStartEndExecutionListener
|
||||||
|
- **配置方式**:XML 中配置 `<flowable:executionListener event="start|end" delegateExpression="${globalNodeStartEndExecutionListener}"/>`
|
||||||
|
- **配置位置**:所有非网关节点(ServiceTask、UserTask、StartEvent、EndEvent 等)
|
||||||
|
- **职责**:监听所有节点的执行生命周期(start/end),发布节点状态变化事件
|
||||||
|
- **位置**:`workflow.listener.GlobalNodeStartEndExecutionListener`
|
||||||
|
- **触发时机**:
|
||||||
|
- 节点开始执行:`EVENTNAME_START` → 发布 `WorkflowNodeInstanceStatusChangeEvent(status=RUNNING)`
|
||||||
|
- 节点执行结束:`EVENTNAME_END` → 读取 `outputs.status` → 发布 `WorkflowNodeInstanceStatusChangeEvent(status=COMPLETED/FAILED)`
|
||||||
|
- **状态流转**:
|
||||||
|
- `EVENTNAME_START` → `RUNNING`
|
||||||
|
- `EVENTNAME_END` + `outputs.status=SUCCESS` → `COMPLETED`
|
||||||
|
- `EVENTNAME_END` + `outputs.status=FAILURE` → `FAILED`
|
||||||
|
- **输出事件**:`WorkflowNodeInstanceStatusChangeEvent`
|
||||||
|
- **为什么在 XML 中配置**:Flowable 引擎需要在节点执行时自动调用,必须通过 XML 配置才能被引擎识别
|
||||||
|
|
||||||
|
#### ApprovalEndExecutionListener
|
||||||
|
- **配置方式**:XML 中配置 `<flowable:executionListener event="end" delegateExpression="${approvalEndExecutionListener}"/>`(仅审批节点)
|
||||||
|
- **配置位置**:审批节点(UserTask)的 end 事件,在 `globalNodeStartEndExecutionListener` 之前执行
|
||||||
|
- **职责**:专门处理审批节点的执行结束事件,丰富审批输出(审批用时、节点执行状态等)
|
||||||
|
- **位置**:`workflow.listener.ApprovalEndExecutionListener`
|
||||||
|
- **触发时机**:审批节点执行结束(`EVENTNAME_END`)
|
||||||
|
- **状态流转**:
|
||||||
|
- 读取已有的 `NodeContext`(包含临时 outputs)
|
||||||
|
- 丰富 `outputs`:设置 `approvalDuration`、`status=SUCCESS`(无论审批结果如何)
|
||||||
|
- 保存 `NodeContext` 回流程变量
|
||||||
|
- **输出事件**:`ApprovalCompletedEvent`(包含审批结果、审批人等信息)
|
||||||
|
- **为什么在 XML 中配置**:需要在审批节点结束时自动执行,必须在 XML 中配置才能被 Flowable 引擎调用
|
||||||
|
|
||||||
|
#### GatewayStartExecutionListener
|
||||||
|
- **配置方式**:XML 中配置 `<flowable:executionListener event="start" delegateExpression="${gatewayStartExecutionListener}"/>`(仅网关节点)
|
||||||
|
- **配置位置**:网关节点(ExclusiveGateway、ParallelGateway、InclusiveGateway)
|
||||||
|
- **职责**:监听网关节点执行(start 事件),发布网关节点状态变化事件
|
||||||
|
- **位置**:`workflow.listener.GatewayStartExecutionListener`
|
||||||
|
- **触发时机**:网关节点开始执行(`EVENTNAME_START`)
|
||||||
|
- **状态流转**:发布 `WorkflowNodeInstanceStatusChangeEvent(status=COMPLETED)`
|
||||||
|
- **为什么在 XML 中配置**:网关节点需要单独处理,必须在 XML 中配置
|
||||||
|
|
||||||
|
### 第四层:任务监听器层(XML 配置)
|
||||||
|
|
||||||
|
监听任务(Task)的生命周期,配置任务属性。这些监听器在 BPMN XML 中配置,Flowable 引擎在任务创建时自动调用。
|
||||||
|
|
||||||
|
#### ApprovalCreateTaskListener
|
||||||
|
- **配置方式**:XML 中配置 `<flowable:taskListener event="create" delegateExpression="${approvalCreateTaskListener}"/>`
|
||||||
|
- **配置位置**:审批节点(UserTask)的 create 事件
|
||||||
|
- **职责**:在审批任务创建时配置审批人、任务信息等,初始化 NodeContext
|
||||||
|
- **位置**:`workflow.listener.ApprovalCreateTaskListener`
|
||||||
|
- **触发时机**:UserTask 创建时(`event="create"`)
|
||||||
|
- **状态流转**:
|
||||||
|
- 解析 `inputMapping`(审批配置)
|
||||||
|
- 配置审批人(从 `inputMapping.approverVariable` 解析)
|
||||||
|
- 设置任务信息(name、description、dueDate 等)
|
||||||
|
- 初始化 `NodeContext`(configs + inputMapping),保存到流程变量
|
||||||
|
- **输出事件**:`ApprovalTaskCreatedEvent`(包含审批人、任务信息等)
|
||||||
|
- **为什么在 XML 中配置**:需要在任务创建时自动配置审批人,必须在 XML 中配置才能被 Flowable 引擎调用
|
||||||
|
|
||||||
|
#### BaseTaskListener
|
||||||
|
- **职责**:任务监听器基类,提供通用的任务配置逻辑
|
||||||
|
- **位置**:`workflow.listener.BaseTaskListener`
|
||||||
|
- **功能**:
|
||||||
|
- 解析 `configs` 和 `inputMapping`
|
||||||
|
- 初始化 `NodeContext` 并保存到流程变量
|
||||||
|
- 子类实现 `configureTask()` 方法进行具体配置
|
||||||
|
|
||||||
|
### 第五层:节点执行器层(Delegate,XML 配置)
|
||||||
|
|
||||||
|
执行具体业务逻辑的节点。这些执行器在 BPMN XML 中配置,Flowable 引擎在 ServiceTask 执行时自动调用。
|
||||||
|
|
||||||
|
#### BaseNodeDelegate
|
||||||
|
- **职责**:节点执行器基类,提供通用的节点执行逻辑
|
||||||
|
- **位置**:`workflow.delegate.BaseNodeDelegate`
|
||||||
|
- **功能**:
|
||||||
|
- 解析 `configs` 和 `inputMapping`
|
||||||
|
- 调用子类的 `executeInternal()` 执行具体业务逻辑
|
||||||
|
- 创建 `NodeContext` 并保存 outputs
|
||||||
|
- 异常处理:创建失败状态的 outputs
|
||||||
|
|
||||||
|
#### JenkinsBuildDelegate
|
||||||
|
- **配置方式**:XML 中配置 `<serviceTask flowable:delegateExpression="${jenkinsBuildDelegate}"/>`
|
||||||
|
- **配置位置**:Jenkins 构建节点(ServiceTask)
|
||||||
|
- **职责**:执行 Jenkins 构建任务
|
||||||
|
- **位置**:`workflow.delegate.JenkinsBuildDelegate`
|
||||||
|
- **继承**:`BaseNodeDelegate`
|
||||||
|
- **触发时机**:ServiceTask 执行时
|
||||||
|
- **状态流转**:
|
||||||
|
- 调用 Jenkins API 构建任务
|
||||||
|
- 根据构建结果设置 `outputs.status`(SUCCESS/FAILURE)
|
||||||
|
- 保存 `NodeContext` 到流程变量
|
||||||
|
- **为什么在 XML 中配置**:需要在 ServiceTask 执行时自动调用,必须在 XML 中配置
|
||||||
|
|
||||||
|
#### ShellNodeDelegate
|
||||||
|
- **配置方式**:XML 中配置 `<serviceTask flowable:delegateExpression="${shellDelegate}"/>`
|
||||||
|
- **配置位置**:Shell 脚本节点(ServiceTask)
|
||||||
|
- **职责**:执行 Shell 脚本
|
||||||
|
- **位置**:`workflow.delegate.ShellNodeDelegate`
|
||||||
|
- **继承**:`BaseNodeDelegate`
|
||||||
|
- **为什么在 XML 中配置**:需要在 ServiceTask 执行时自动调用,必须在 XML 中配置
|
||||||
|
|
||||||
|
#### NotificationNodeDelegate
|
||||||
|
- **配置方式**:XML 中配置 `<serviceTask flowable:delegateExpression="${notificationDelegate}"/>`
|
||||||
|
- **配置位置**:通知节点(ServiceTask)
|
||||||
|
- **职责**:发送通知消息
|
||||||
|
- **位置**:`workflow.delegate.NotificationNodeDelegate`
|
||||||
|
- **继承**:`BaseNodeDelegate`
|
||||||
|
- **为什么在 XML 中配置**:需要在 ServiceTask 执行时自动调用,必须在 XML 中配置
|
||||||
|
|
||||||
|
### 第六层:业务事件监听器层(Spring 事件监听)
|
||||||
|
|
||||||
|
监听业务事件,同步业务模块状态。这些监听器通过 `@EventListener` 注解注册,监听 Spring 应用事件。
|
||||||
|
|
||||||
|
#### WorkflowInstanceStatusChangeListener
|
||||||
|
- **注册方式**:使用 `@EventListener` 注解,Spring 自动注册
|
||||||
|
- **职责**:监听工作流实例状态变化事件,更新数据库中的工作流实例状态
|
||||||
|
- **位置**:`workflow.listener.WorkflowInstanceStatusChangeListener`
|
||||||
|
- **监听事件**:`WorkflowInstanceStatusChangeEvent`
|
||||||
|
- **状态流转**:
|
||||||
|
- 接收 `WorkflowInstanceStatusChangeEvent`
|
||||||
|
- 调用 `workflowInstanceService.updateInstanceStatus()`
|
||||||
|
- 更新 `WorkflowInstance.status` 和 `endTime`
|
||||||
|
- **为什么使用 Spring 事件**:解耦工作流模块和数据库操作,通过事件异步处理
|
||||||
|
|
||||||
|
#### WorkflowNodeInstanceStatusChangeListener
|
||||||
|
- **注册方式**:使用 `@EventListener` 注解,Spring 自动注册
|
||||||
|
- **职责**:监听节点实例状态变化事件,更新数据库中的节点实例状态
|
||||||
|
- **位置**:`workflow.listener.WorkflowNodeInstanceStatusChangeListener`
|
||||||
|
- **监听事件**:`WorkflowNodeInstanceStatusChangeEvent`
|
||||||
|
- **状态流转**:
|
||||||
|
- 接收 `WorkflowNodeInstanceStatusChangeEvent`
|
||||||
|
- 调用 `workflowNodeInstanceService.saveOrUpdateWorkflowNodeInstance()`
|
||||||
|
- 创建或更新 `WorkflowNodeInstance`(状态、时间、变量等)
|
||||||
|
- **为什么使用 Spring 事件**:解耦节点执行监听器和数据库操作
|
||||||
|
|
||||||
|
#### DeployRecordApprovalStatusSyncListener
|
||||||
|
- **注册方式**:使用 `@EventListener` 注解,Spring 自动注册
|
||||||
|
- **职责**:监听审批相关事件,同步部署记录状态(审批阶段)
|
||||||
|
- **位置**:`deploy.listener.DeployRecordApprovalStatusSyncListener`
|
||||||
|
- **监听事件**:
|
||||||
|
- `ApprovalTaskCreatedEvent` → 更新状态为 `PENDING_APPROVAL`
|
||||||
|
- `ApprovalCompletedEvent` → 根据审批结果更新状态(`RUNNING` 或 `REJECTED`)
|
||||||
|
- **状态流转**:
|
||||||
|
- `ApprovalTaskCreatedEvent` → `deployRecordService.updateStatusToPendingApproval()` → `PENDING_APPROVAL`
|
||||||
|
- `ApprovalCompletedEvent` + `APPROVED` → `deployRecordService.updateStatusFromApproval(true)` → `RUNNING`
|
||||||
|
- `ApprovalCompletedEvent` + `REJECTED` → `deployRecordService.updateStatusFromApproval(false)` → `REJECTED`
|
||||||
|
- **为什么使用 Spring 事件**:解耦工作流模块和部署模块,部署模块独立监听审批事件
|
||||||
|
|
||||||
|
#### DeployRecordWorkflowStatusSyncListener
|
||||||
|
- **注册方式**:使用 `@EventListener` 注解,Spring 自动注册
|
||||||
|
- **职责**:监听工作流实例状态变化事件,同步部署记录状态(工作流层面)
|
||||||
|
- **位置**:`deploy.listener.DeployRecordWorkflowStatusSyncListener`
|
||||||
|
- **监听事件**:`WorkflowInstanceStatusChangeEvent`
|
||||||
|
- **状态流转**:
|
||||||
|
- 判断是否为部署类型工作流(通过分类 code)
|
||||||
|
- 检查当前状态是否为终态(状态优先级保护)
|
||||||
|
- 如果不是终态 → 调用 `deployRecordService.syncStatusFromWorkflowInstance()` → 状态映射:
|
||||||
|
- `CREATED` → `CREATED`
|
||||||
|
- `RUNNING` → `RUNNING`
|
||||||
|
- `COMPLETED` → `SUCCESS`
|
||||||
|
- `COMPLETED_WITH_ERRORS` → `PARTIAL_SUCCESS`
|
||||||
|
- `FAILED` → `FAILED`
|
||||||
|
- `TERMINATED` → `TERMINATED`
|
||||||
|
- 如果是终态(如 `REJECTED`)→ 只更新 `endTime`,不更新状态
|
||||||
|
- **为什么使用 Spring 事件**:解耦工作流模块和部署模块,部署模块独立监听工作流事件
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 完整生命周期流程
|
||||||
|
|
||||||
|
### 场景:部署流程(包含审批节点)
|
||||||
|
|
||||||
|
#### 阶段1:工作流启动
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Flowable 引擎启动流程
|
||||||
|
↓
|
||||||
|
2. FlowableEventDispatcher.onEvent()(代码注册,接收 Flowable 引擎事件)
|
||||||
|
- 接收 PROCESS_STARTED 事件
|
||||||
|
- 分发给 ProcessEventHandler
|
||||||
|
↓
|
||||||
|
3. ProcessEventHandler.handle()(代码注册,处理流程事件)
|
||||||
|
- 发布 WorkflowInstanceStatusChangeEvent(status=RUNNING)
|
||||||
|
↓
|
||||||
|
4. WorkflowInstanceStatusChangeListener.handleWorkflowStatusChange()(Spring 事件监听)
|
||||||
|
- 更新 WorkflowInstance.status = RUNNING
|
||||||
|
↓
|
||||||
|
5. DeployRecordWorkflowStatusSyncListener.handleWorkflowStatusChange()(Spring 事件监听)
|
||||||
|
- 判断是否为部署类型工作流(通过分类 code)
|
||||||
|
- 调用 deployRecordService.syncStatusFromWorkflowInstance()
|
||||||
|
- 更新 DeployRecord.status = RUNNING
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 阶段2:审批任务创建(已合并到阶段3)
|
||||||
|
|
||||||
|
审批任务创建在阶段3中详细说明。
|
||||||
|
|
||||||
|
#### 阶段3:审批节点执行
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Flowable 引擎执行审批节点(UserTask)
|
||||||
|
↓
|
||||||
|
2. ApprovalCreateTaskListener.notify()(XML 配置,任务创建时)
|
||||||
|
- 解析 inputMapping(审批配置)
|
||||||
|
- 配置审批人(从 inputMapping.approverVariable 解析)
|
||||||
|
- 初始化 NodeContext(configs + inputMapping),保存到流程变量
|
||||||
|
- 发布 ApprovalTaskCreatedEvent
|
||||||
|
↓
|
||||||
|
3. DeployRecordApprovalStatusSyncListener.handleApprovalTaskCreated()
|
||||||
|
- 判断是否为部署类型工作流(通过 workflowCategoryCode)
|
||||||
|
- 调用 deployRecordService.updateStatusToPendingApproval()
|
||||||
|
- 更新 DeployRecord.status = PENDING_APPROVAL
|
||||||
|
↓
|
||||||
|
4. GlobalNodeStartEndExecutionListener.notify(EVENTNAME_START)(XML 配置,节点开始执行)
|
||||||
|
- 发布 WorkflowNodeInstanceStatusChangeEvent(status=RUNNING)
|
||||||
|
↓
|
||||||
|
5. WorkflowNodeInstanceStatusChangeListener.handleNodeStatusChange()
|
||||||
|
- 创建/更新 WorkflowNodeInstance.status = RUNNING
|
||||||
|
↓
|
||||||
|
6. 用户完成审批任务(通过/拒绝)
|
||||||
|
- ApprovalTaskServiceImpl.completeTask()
|
||||||
|
- 创建临时 ApprovalOutputs(包含审批结果)
|
||||||
|
- 保存到流程变量
|
||||||
|
↓
|
||||||
|
7. ApprovalEndExecutionListener.notify(EVENTNAME_END)(XML 配置,节点结束执行,在 globalNodeStartEndExecutionListener 之前)
|
||||||
|
- 读取 NodeContext(包含临时 outputs)
|
||||||
|
- 丰富 outputs:设置 approvalDuration、status=SUCCESS
|
||||||
|
- 保存 NodeContext 回流程变量
|
||||||
|
- 发布 ApprovalCompletedEvent(result=APPROVED/REJECTED, ...)
|
||||||
|
↓
|
||||||
|
8. GlobalNodeStartEndExecutionListener.notify(EVENTNAME_END)(XML 配置,节点结束执行)
|
||||||
|
- 读取 outputs.status(此时已经是 SUCCESS,由 ApprovalEndExecutionListener 设置)
|
||||||
|
- 发布 WorkflowNodeInstanceStatusChangeEvent(status=COMPLETED)
|
||||||
|
↓
|
||||||
|
9. WorkflowNodeInstanceStatusChangeListener.handleNodeStatusChange()
|
||||||
|
- 更新 WorkflowNodeInstance.status = COMPLETED
|
||||||
|
↓
|
||||||
|
10. DeployRecordApprovalStatusSyncListener.handleApprovalCompleted()
|
||||||
|
- 判断是否为部署类型工作流(通过 workflowCategoryCode)
|
||||||
|
- 根据审批结果:
|
||||||
|
* APPROVED → deployRecordService.updateStatusFromApproval(true) → RUNNING
|
||||||
|
* REJECTED → deployRecordService.updateStatusFromApproval(false) → REJECTED(终态)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 阶段4:工作流完成
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Flowable 引擎检测到流程完成
|
||||||
|
↓
|
||||||
|
2. FlowableEventDispatcher.onEvent()(代码注册,接收 Flowable 引擎事件)
|
||||||
|
- 接收 PROCESS_COMPLETED 事件
|
||||||
|
- 分发给 ProcessEventHandler
|
||||||
|
↓
|
||||||
|
3. ProcessEventHandler.handle()(代码注册,处理流程事件)
|
||||||
|
- 查询所有节点实例,检查是否有失败节点
|
||||||
|
- 如果有失败节点 → status = COMPLETED_WITH_ERRORS
|
||||||
|
- 如果没有失败节点 → status = COMPLETED
|
||||||
|
- 发布 WorkflowInstanceStatusChangeEvent(status=COMPLETED/COMPLETED_WITH_ERRORS)
|
||||||
|
↓
|
||||||
|
4. WorkflowInstanceStatusChangeListener.handleWorkflowStatusChange()(Spring 事件监听)
|
||||||
|
- 更新 WorkflowInstance.status = COMPLETED/COMPLETED_WITH_ERRORS
|
||||||
|
- 更新 WorkflowInstance.endTime
|
||||||
|
↓
|
||||||
|
5. DeployRecordWorkflowStatusSyncListener.handleWorkflowStatusChange()(Spring 事件监听)
|
||||||
|
- 判断是否为部署类型工作流(通过分类 code)
|
||||||
|
- 查询部署记录
|
||||||
|
- 检查当前状态是否为终态(状态优先级保护)
|
||||||
|
* 如果是终态(如 REJECTED)→ 只更新 endTime,不更新状态
|
||||||
|
* 如果不是终态 → 调用 deployRecordService.syncStatusFromWorkflowInstance()
|
||||||
|
- 状态映射:
|
||||||
|
* COMPLETED → SUCCESS
|
||||||
|
* COMPLETED_WITH_ERRORS → PARTIAL_SUCCESS
|
||||||
|
* FAILED → FAILED
|
||||||
|
* TERMINATED → TERMINATED
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 事件流转图
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Flowable 引擎层 │
|
||||||
|
│ PROCESS_STARTED │ PROCESS_COMPLETED │ TASK_CREATED │ ... │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ FlowableEventDispatcher (统一分发) │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌───────────────────┴───────────────────┐
|
||||||
|
↓ ↓
|
||||||
|
┌───────────────────────┐ ┌──────────────────────────┐
|
||||||
|
│ ProcessEventHandler │ │ ApprovalCreateTaskListener │
|
||||||
|
│ (流程事件处理器) │ │ (审批任务监听器) │
|
||||||
|
└───────────────────────┘ └──────────────────────────┘
|
||||||
|
↓ ↓
|
||||||
|
┌───────────────────────┐ ┌──────────────────────────┐
|
||||||
|
│ WorkflowInstance │ │ ApprovalTaskCreatedEvent │
|
||||||
|
│ StatusChangeEvent │ └──────────────────────────┘
|
||||||
|
└───────────────────────┘ ↓
|
||||||
|
↓ ┌──────────────────────────────┐
|
||||||
|
│ │ DeployRecordApprovalStatus │
|
||||||
|
│ │ SyncListener │
|
||||||
|
│ │ (更新为 PENDING_APPROVAL) │
|
||||||
|
│ └──────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ WorkflowInstanceStatusChangeListener │
|
||||||
|
│ (更新 WorkflowInstance 状态) │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ DeployRecordWorkflowStatusSyncListener │
|
||||||
|
│ (更新 DeployRecord 状态 - 工作流层面) │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ GlobalNodeStartEndExecutionListener │
|
||||||
|
│ (节点执行监听器) │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ WorkflowNodeInstanceStatusChangeListener │
|
||||||
|
│ (更新 WorkflowNodeInstance 状态) │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
↓ (审批节点结束时)
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ ApprovalEndExecutionListener │
|
||||||
|
│ (构建审批输出,发布 ApprovalCompletedEvent) │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ DeployRecordApprovalStatusSyncListener │
|
||||||
|
│ (根据审批结果更新 DeployRecord 状态) │
|
||||||
|
│ APPROVED → RUNNING │
|
||||||
|
│ REJECTED → CANCELLED │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 核心组件详解
|
||||||
|
|
||||||
|
### 1. 状态同步优先级机制
|
||||||
|
|
||||||
|
#### 设计目的
|
||||||
|
确保审批拒绝后的状态不会被后续工作流完成事件覆盖。
|
||||||
|
|
||||||
|
#### 实现位置
|
||||||
|
`DeployRecordServiceImpl.syncStatusFromWorkflowInstance()`
|
||||||
|
|
||||||
|
#### 逻辑
|
||||||
|
```java
|
||||||
|
// 如果当前状态已经是终态(特别是审批被拒绝的CANCELLED),不应该被覆盖
|
||||||
|
if (isFinalState(record.getStatus())) {
|
||||||
|
// 只更新时间,不更新状态
|
||||||
|
if (instance.getEndTime() != null && record.getEndTime() == null) {
|
||||||
|
record.setEndTime(instance.getEndTime());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 终态定义
|
||||||
|
- `SUCCESS`
|
||||||
|
- `FAILED`
|
||||||
|
- `CANCELLED`
|
||||||
|
- `TERMINATED`
|
||||||
|
- `PARTIAL_SUCCESS`
|
||||||
|
|
||||||
|
### 2. 事件过滤机制
|
||||||
|
|
||||||
|
#### DeployRecordApprovalStatusSyncListener
|
||||||
|
```java
|
||||||
|
// 在方法开始就判断,避免不必要的数据库操作
|
||||||
|
if (!isDeploymentWorkflow(event.getWorkflowCategoryCode())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### DeployRecordWorkflowStatusSyncListener
|
||||||
|
```java
|
||||||
|
// 查询 WorkflowInstance 后立即判断
|
||||||
|
WorkflowInstance instance = workflowInstanceRepository.findByProcessInstanceId(...);
|
||||||
|
if (!isDeploymentWorkflow(instance)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 事务管理
|
||||||
|
|
||||||
|
所有业务监听器使用 `@Transactional(propagation = Propagation.REQUIRES_NEW)`,确保:
|
||||||
|
- 独立事务,不影响主流程
|
||||||
|
- 异常不影响工作流执行
|
||||||
|
- 状态同步的原子性
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 扩展指南
|
||||||
|
|
||||||
|
### 扩展其他领域的审批同步
|
||||||
|
|
||||||
|
#### 场景示例:订单审批流程
|
||||||
|
|
||||||
|
假设需要为订单系统添加审批状态同步功能。
|
||||||
|
|
||||||
|
#### 步骤1:创建订单审批状态同步监听器
|
||||||
|
|
||||||
|
创建文件:`order/listener/OrderApprovalStatusSyncListener.java`
|
||||||
|
|
||||||
|
```java
|
||||||
|
package com.qqchen.deploy.backend.order.listener;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.order.service.IOrderService;
|
||||||
|
import com.qqchen.deploy.backend.workflow.dto.event.ApprovalTaskCreatedEvent;
|
||||||
|
import com.qqchen.deploy.backend.workflow.dto.event.ApprovalCompletedEvent;
|
||||||
|
import com.qqchen.deploy.backend.workflow.enums.ApprovalResultEnum;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.event.EventListener;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.transaction.annotation.Propagation;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class OrderApprovalStatusSyncListener {
|
||||||
|
|
||||||
|
private static final String ORDER_CATEGORY_CODE = "ORDER";
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IOrderService orderService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监听审批任务创建事件
|
||||||
|
*/
|
||||||
|
@EventListener
|
||||||
|
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||||
|
public void handleApprovalTaskCreated(ApprovalTaskCreatedEvent event) {
|
||||||
|
// 判断是否为订单类型的工作流
|
||||||
|
if (!ORDER_CATEGORY_CODE.equals(event.getWorkflowCategoryCode())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
log.debug("处理订单审批任务创建事件: workflowInstanceId={}",
|
||||||
|
event.getWorkflowInstanceId());
|
||||||
|
|
||||||
|
// 更新订单状态为待审批
|
||||||
|
orderService.updateStatusToPendingApproval(event.getWorkflowInstanceId());
|
||||||
|
|
||||||
|
log.info("订单状态已更新为待审批: workflowInstanceId={}",
|
||||||
|
event.getWorkflowInstanceId());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("同步订单状态失败(审批任务创建): workflowInstanceId={}",
|
||||||
|
event.getWorkflowInstanceId(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监听审批完成事件
|
||||||
|
*/
|
||||||
|
@EventListener
|
||||||
|
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||||
|
public void handleApprovalCompleted(ApprovalCompletedEvent event) {
|
||||||
|
// 判断是否为订单类型的工作流
|
||||||
|
if (!ORDER_CATEGORY_CODE.equals(event.getWorkflowCategoryCode())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
log.debug("处理订单审批完成事件: workflowInstanceId={}, result={}",
|
||||||
|
event.getWorkflowInstanceId(), event.getResult());
|
||||||
|
|
||||||
|
// 根据审批结果更新订单状态
|
||||||
|
if (event.getResult() == ApprovalResultEnum.APPROVED) {
|
||||||
|
orderService.updateStatusFromApproval(event.getWorkflowInstanceId(), true);
|
||||||
|
} else if (event.getResult() == ApprovalResultEnum.REJECTED) {
|
||||||
|
orderService.updateStatusFromApproval(event.getWorkflowInstanceId(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("订单状态已同步(审批完成): workflowInstanceId={}, result={}",
|
||||||
|
event.getWorkflowInstanceId(), event.getResult());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("同步订单状态失败(审批完成): workflowInstanceId={}",
|
||||||
|
event.getWorkflowInstanceId(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 步骤2:创建订单工作流状态同步监听器
|
||||||
|
|
||||||
|
创建文件:`order/listener/OrderWorkflowStatusSyncListener.java`
|
||||||
|
|
||||||
|
```java
|
||||||
|
package com.qqchen.deploy.backend.order.listener;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.order.service.IOrderService;
|
||||||
|
import com.qqchen.deploy.backend.workflow.dto.event.WorkflowInstanceStatusChangeEvent;
|
||||||
|
import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance;
|
||||||
|
import com.qqchen.deploy.backend.workflow.repository.IWorkflowInstanceRepository;
|
||||||
|
import com.qqchen.deploy.backend.workflow.repository.IWorkflowDefinitionRepository;
|
||||||
|
import com.qqchen.deploy.backend.workflow.repository.IWorkflowCategoryRepository;
|
||||||
|
import com.qqchen.deploy.backend.workflow.entity.WorkflowDefinition;
|
||||||
|
import com.qqchen.deploy.backend.workflow.entity.WorkflowCategory;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.event.EventListener;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.transaction.annotation.Propagation;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class OrderWorkflowStatusSyncListener {
|
||||||
|
|
||||||
|
private static final String ORDER_CATEGORY_CODE = "ORDER";
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IOrderService orderService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IWorkflowInstanceRepository workflowInstanceRepository;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IWorkflowDefinitionRepository workflowDefinitionRepository;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IWorkflowCategoryRepository workflowCategoryRepository;
|
||||||
|
|
||||||
|
@EventListener
|
||||||
|
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||||
|
public void handleWorkflowStatusChange(WorkflowInstanceStatusChangeEvent event) {
|
||||||
|
try {
|
||||||
|
// 1. 查询工作流实例
|
||||||
|
WorkflowInstance instance = workflowInstanceRepository
|
||||||
|
.findByProcessInstanceId(event.getProcessInstanceId())
|
||||||
|
.orElse(null);
|
||||||
|
|
||||||
|
if (instance == null) {
|
||||||
|
log.warn("工作流实例不存在: processInstanceId={}", event.getProcessInstanceId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 判断是否为订单类型的工作流(提前校验)
|
||||||
|
if (!isOrderWorkflow(instance)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 更新工作流实例的结束时间
|
||||||
|
if (event.getEndTime() != null && instance.getEndTime() == null) {
|
||||||
|
instance.setEndTime(event.getEndTime());
|
||||||
|
workflowInstanceRepository.save(instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 同步订单状态
|
||||||
|
orderService.syncStatusFromWorkflowInstance(instance, event.getStatus());
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("同步订单状态失败(工作流状态变化): processInstanceId={}",
|
||||||
|
event.getProcessInstanceId(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否为订单类型的工作流
|
||||||
|
*/
|
||||||
|
private boolean isOrderWorkflow(WorkflowInstance instance) {
|
||||||
|
if (instance.getWorkflowDefinitionId() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkflowDefinition definition = workflowDefinitionRepository
|
||||||
|
.findById(instance.getWorkflowDefinitionId())
|
||||||
|
.orElse(null);
|
||||||
|
|
||||||
|
if (definition == null || definition.getCategoryId() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkflowCategory category = workflowCategoryRepository
|
||||||
|
.findById(definition.getCategoryId())
|
||||||
|
.orElse(null);
|
||||||
|
|
||||||
|
return category != null && ORDER_CATEGORY_CODE.equals(category.getCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 步骤3:在 OrderService 中添加状态同步方法
|
||||||
|
|
||||||
|
```java
|
||||||
|
public interface IOrderService {
|
||||||
|
/**
|
||||||
|
* 更新订单状态为待审批
|
||||||
|
*/
|
||||||
|
void updateStatusToPendingApproval(Long workflowInstanceId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据审批结果更新订单状态
|
||||||
|
*/
|
||||||
|
void updateStatusFromApproval(Long workflowInstanceId, boolean approved);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据工作流实例同步订单状态
|
||||||
|
*/
|
||||||
|
void syncStatusFromWorkflowInstance(WorkflowInstance instance, WorkflowInstanceStatusEnums status);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 步骤4:配置工作流分类
|
||||||
|
|
||||||
|
在数据库中添加订单类型的工作流分类:
|
||||||
|
```sql
|
||||||
|
INSERT INTO workflow_category (code, name, ...) VALUES ('ORDER', '订单流程', ...);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 扩展要点总结
|
||||||
|
|
||||||
|
1. **遵循命名规范**:
|
||||||
|
- 监听器:`{业务模块}ApprovalStatusSyncListener`
|
||||||
|
- 监听器:`{业务模块}WorkflowStatusSyncListener`
|
||||||
|
|
||||||
|
2. **事件过滤**:
|
||||||
|
- 必须通过 `workflowCategoryCode` 或 `isXXXWorkflow()` 判断
|
||||||
|
- 避免处理不相关的工作流事件
|
||||||
|
|
||||||
|
3. **事务管理**:
|
||||||
|
- 使用 `@Transactional(propagation = Propagation.REQUIRES_NEW)`
|
||||||
|
- 确保异常不影响主流程
|
||||||
|
|
||||||
|
4. **状态优先级**:
|
||||||
|
- 在 `syncStatusFromWorkflowInstance()` 中实现终态保护
|
||||||
|
- 审批拒绝的状态优先级高于工作流完成状态
|
||||||
|
|
||||||
|
5. **日志记录**:
|
||||||
|
- 记录关键状态转换
|
||||||
|
- 异常时记录详细错误信息
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
### 架构优势
|
||||||
|
|
||||||
|
1. **完全解耦**:工作流模块不依赖业务模块,只发布事件
|
||||||
|
2. **易于扩展**:新增业务模块只需添加监听器,无需修改工作流代码
|
||||||
|
3. **状态准确**:通过状态优先级机制,确保最终状态正确
|
||||||
|
4. **性能优化**:通过事件过滤,避免不必要的数据库操作
|
||||||
|
|
||||||
|
### 关键设计模式
|
||||||
|
|
||||||
|
1. **事件驱动架构**:通过事件解耦各模块
|
||||||
|
2. **监听器模式**:业务模块监听工作流事件
|
||||||
|
3. **策略模式**:不同业务模块实现不同的状态同步策略
|
||||||
|
4. **责任链模式**:事件在监听器链中传递处理
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
1. **事件顺序**:审批事件可能在工作流事件之前或之后触发,需要状态优先级保护
|
||||||
|
2. **并发安全**:使用乐观锁(@Version)防止并发更新冲突
|
||||||
|
3. **异常处理**:监听器异常不应影响工作流执行
|
||||||
|
4. **性能考虑**:避免在监听器中执行耗时操作
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**文档版本**:v1.0
|
||||||
|
**最后更新**:2025-11-04
|
||||||
|
**维护者**:开发团队
|
||||||
|
|
||||||
@ -1,11 +1,14 @@
|
|||||||
package com.qqchen.deploy.backend.deploy.api;
|
package com.qqchen.deploy.backend.deploy.api;
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.deploy.dto.DeployRequestDTO;
|
import com.qqchen.deploy.backend.deploy.dto.DeployRequestDTO;
|
||||||
|
import com.qqchen.deploy.backend.deploy.dto.DeployRecordFlowGraphDTO;
|
||||||
import com.qqchen.deploy.backend.deploy.dto.DeployResultDTO;
|
import com.qqchen.deploy.backend.deploy.dto.DeployResultDTO;
|
||||||
import com.qqchen.deploy.backend.deploy.dto.UserDeployableDTO;
|
import com.qqchen.deploy.backend.deploy.dto.UserDeployableDTO;
|
||||||
|
import com.qqchen.deploy.backend.deploy.service.IDeployRecordService;
|
||||||
import com.qqchen.deploy.backend.deploy.service.IDeployService;
|
import com.qqchen.deploy.backend.deploy.service.IDeployService;
|
||||||
import com.qqchen.deploy.backend.framework.api.Response;
|
import com.qqchen.deploy.backend.framework.api.Response;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@ -29,6 +32,9 @@ public class DeployApiController {
|
|||||||
@Resource
|
@Resource
|
||||||
private IDeployService deployService;
|
private IDeployService deployService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IDeployRecordService deployRecordService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前用户可部署的环境和应用
|
* 获取当前用户可部署的环境和应用
|
||||||
*/
|
*/
|
||||||
@ -48,5 +54,18 @@ public class DeployApiController {
|
|||||||
public Response<DeployResultDTO> executeDeploy(@Validated @RequestBody DeployRequestDTO request) {
|
public Response<DeployResultDTO> executeDeploy(@Validated @RequestBody DeployRequestDTO request) {
|
||||||
return Response.success(deployService.executeDeploy(request));
|
return Response.success(deployService.executeDeploy(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取部署记录的流程图
|
||||||
|
* 用于前端展示工作流的流程图和节点执行状态
|
||||||
|
*/
|
||||||
|
@Operation(summary = "获取部署流程图", description = "获取指定部署记录的工作流流程图数据,包含流程图结构和节点执行状态")
|
||||||
|
@GetMapping("/records/{deployRecordId}/flow-graph")
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public Response<DeployRecordFlowGraphDTO> getDeployFlowGraph(
|
||||||
|
@Parameter(description = "部署记录ID", required = true) @PathVariable Long deployRecordId
|
||||||
|
) {
|
||||||
|
return Response.success(deployRecordService.getDeployFlowGraph(deployRecordId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,41 @@
|
|||||||
|
package com.qqchen.deploy.backend.deploy.dto;
|
||||||
|
|
||||||
|
import com.qqchen.deploy.backend.deploy.enums.DeployRecordStatusEnums;
|
||||||
|
import com.qqchen.deploy.backend.workflow.dto.WorkflowNodeInstanceDTO;
|
||||||
|
import com.qqchen.deploy.backend.workflow.dto.definition.workflow.WorkflowDefinitionGraph;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 部署记录流程图DTO
|
||||||
|
* 用于前端展示部署工作流的流程图和节点状态
|
||||||
|
*
|
||||||
|
* @author qqchen
|
||||||
|
* @since 2025-11-04
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "部署记录流程图信息")
|
||||||
|
public class DeployRecordFlowGraphDTO {
|
||||||
|
|
||||||
|
@Schema(description = "部署记录ID")
|
||||||
|
private Long deployRecordId;
|
||||||
|
|
||||||
|
@Schema(description = "工作流实例ID")
|
||||||
|
private Long workflowInstanceId;
|
||||||
|
|
||||||
|
@Schema(description = "流程实例ID(Flowable)")
|
||||||
|
private String processInstanceId;
|
||||||
|
|
||||||
|
@Schema(description = "部署状态")
|
||||||
|
private DeployRecordStatusEnums deployStatus;
|
||||||
|
|
||||||
|
@Schema(description = "流程图数据(画布快照,包含节点和边的位置信息)")
|
||||||
|
private WorkflowDefinitionGraph graph;
|
||||||
|
|
||||||
|
@Schema(description = "节点执行状态列表(用于标记每个节点的执行状态)")
|
||||||
|
private List<WorkflowNodeInstanceDTO> nodeInstances;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,6 +1,7 @@
|
|||||||
package com.qqchen.deploy.backend.deploy.service;
|
package com.qqchen.deploy.backend.deploy.service;
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.deploy.dto.DeployRecordDTO;
|
import com.qqchen.deploy.backend.deploy.dto.DeployRecordDTO;
|
||||||
|
import com.qqchen.deploy.backend.deploy.dto.DeployRecordFlowGraphDTO;
|
||||||
import com.qqchen.deploy.backend.deploy.entity.DeployRecord;
|
import com.qqchen.deploy.backend.deploy.entity.DeployRecord;
|
||||||
import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance;
|
import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance;
|
||||||
import com.qqchen.deploy.backend.workflow.enums.WorkflowInstanceStatusEnums;
|
import com.qqchen.deploy.backend.workflow.enums.WorkflowInstanceStatusEnums;
|
||||||
@ -67,5 +68,14 @@ public interface IDeployRecordService {
|
|||||||
* @param approved 是否通过审批
|
* @param approved 是否通过审批
|
||||||
*/
|
*/
|
||||||
void updateStatusFromApproval(Long workflowInstanceId, boolean approved);
|
void updateStatusFromApproval(Long workflowInstanceId, boolean approved);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取部署记录的流程图数据
|
||||||
|
* 包含流程图结构(graph)和节点执行状态(nodeInstances)
|
||||||
|
*
|
||||||
|
* @param deployRecordId 部署记录ID
|
||||||
|
* @return 部署记录流程图DTO
|
||||||
|
*/
|
||||||
|
DeployRecordFlowGraphDTO getDeployFlowGraph(Long deployRecordId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,20 +2,40 @@ package com.qqchen.deploy.backend.deploy.service.impl;
|
|||||||
|
|
||||||
import com.qqchen.deploy.backend.deploy.converter.DeployRecordConverter;
|
import com.qqchen.deploy.backend.deploy.converter.DeployRecordConverter;
|
||||||
import com.qqchen.deploy.backend.deploy.dto.DeployRecordDTO;
|
import com.qqchen.deploy.backend.deploy.dto.DeployRecordDTO;
|
||||||
|
import com.qqchen.deploy.backend.deploy.dto.DeployRecordFlowGraphDTO;
|
||||||
import com.qqchen.deploy.backend.deploy.entity.DeployRecord;
|
import com.qqchen.deploy.backend.deploy.entity.DeployRecord;
|
||||||
import com.qqchen.deploy.backend.deploy.enums.DeployRecordStatusEnums;
|
import com.qqchen.deploy.backend.deploy.enums.DeployRecordStatusEnums;
|
||||||
import com.qqchen.deploy.backend.deploy.query.DeployRecordQuery;
|
import com.qqchen.deploy.backend.deploy.query.DeployRecordQuery;
|
||||||
import com.qqchen.deploy.backend.deploy.repository.IDeployRecordRepository;
|
import com.qqchen.deploy.backend.deploy.repository.IDeployRecordRepository;
|
||||||
import com.qqchen.deploy.backend.deploy.service.IDeployRecordService;
|
import com.qqchen.deploy.backend.deploy.service.IDeployRecordService;
|
||||||
|
import com.qqchen.deploy.backend.framework.exception.BusinessException;
|
||||||
import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl;
|
import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl;
|
||||||
|
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
|
||||||
|
import com.qqchen.deploy.backend.workflow.converter.WorkflowNodeInstanceConverter;
|
||||||
|
import com.qqchen.deploy.backend.workflow.dto.WorkflowNodeInstanceDTO;
|
||||||
import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance;
|
import com.qqchen.deploy.backend.workflow.entity.WorkflowInstance;
|
||||||
|
import com.qqchen.deploy.backend.workflow.entity.WorkflowNodeInstance;
|
||||||
import com.qqchen.deploy.backend.workflow.enums.WorkflowInstanceStatusEnums;
|
import com.qqchen.deploy.backend.workflow.enums.WorkflowInstanceStatusEnums;
|
||||||
|
import com.qqchen.deploy.backend.workflow.enums.WorkflowNodeInstanceStatusEnums;
|
||||||
|
import com.qqchen.deploy.backend.workflow.repository.IWorkflowInstanceRepository;
|
||||||
|
import com.qqchen.deploy.backend.workflow.repository.IWorkflowNodeInstanceRepository;
|
||||||
|
import com.qqchen.deploy.backend.workflow.util.FlowableUtils;
|
||||||
|
import org.flowable.bpmn.model.BpmnModel;
|
||||||
|
import org.flowable.bpmn.model.FlowElement;
|
||||||
|
import org.flowable.bpmn.model.Process;
|
||||||
|
import org.flowable.engine.HistoryService;
|
||||||
|
import org.flowable.engine.RepositoryService;
|
||||||
|
import org.flowable.variable.api.history.HistoricVariableInstance;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 部署记录服务实现
|
* 部署记录服务实现
|
||||||
@ -34,6 +54,21 @@ public class DeployRecordServiceImpl extends BaseServiceImpl<DeployRecord, Deplo
|
|||||||
@Resource
|
@Resource
|
||||||
private DeployRecordConverter deployRecordConverter;
|
private DeployRecordConverter deployRecordConverter;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IWorkflowInstanceRepository workflowInstanceRepository;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IWorkflowNodeInstanceRepository workflowNodeInstanceRepository;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private WorkflowNodeInstanceConverter workflowNodeInstanceConverter;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private RepositoryService repositoryService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private HistoryService historyService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public DeployRecordDTO createDeployRecord(
|
public DeployRecordDTO createDeployRecord(
|
||||||
@ -181,6 +216,113 @@ public class DeployRecordServiceImpl extends BaseServiceImpl<DeployRecord, Deplo
|
|||||||
deployRecordRepository.save(record);
|
deployRecordRepository.save(record);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DeployRecordFlowGraphDTO getDeployFlowGraph(Long deployRecordId) {
|
||||||
|
// 1. 查询部署记录
|
||||||
|
DeployRecord deployRecord = deployRecordRepository.findById(deployRecordId)
|
||||||
|
.orElseThrow(() -> new BusinessException(ResponseCode.NOT_FOUND, new Object[]{"部署记录"}));
|
||||||
|
|
||||||
|
// 2. 查询工作流实例(包含流程图快照)
|
||||||
|
WorkflowInstance workflowInstance = workflowInstanceRepository.findById(deployRecord.getWorkflowInstanceId())
|
||||||
|
.orElseThrow(() -> new BusinessException(ResponseCode.NOT_FOUND, new Object[]{"工作流实例"}));
|
||||||
|
|
||||||
|
// 3. 查询节点实例列表(只查询实际执行过的节点)
|
||||||
|
List<WorkflowNodeInstance> nodeInstances = workflowNodeInstanceRepository.findByWorkflowInstanceId(workflowInstance.getId());
|
||||||
|
|
||||||
|
// 4. 从BPMN模型中获取流程元素顺序(用于排序)
|
||||||
|
BpmnModel bpmnModel = repositoryService.getBpmnModel(workflowInstance.getProcessDefinitionId());
|
||||||
|
Process process = bpmnModel.getMainProcess();
|
||||||
|
List<FlowElement> flowElements = FlowableUtils.sortFlowElements(process);
|
||||||
|
|
||||||
|
// 5. 构建节点ID到流程顺序的映射(用于排序)
|
||||||
|
Map<String, Integer> nodeOrderMap = new HashMap<>();
|
||||||
|
for (int i = 0; i < flowElements.size(); i++) {
|
||||||
|
nodeOrderMap.put(flowElements.get(i).getId(), i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 按流程顺序排序节点实例(只包含实际执行过的节点)
|
||||||
|
List<WorkflowNodeInstance> orderedNodeInstances = nodeInstances.stream()
|
||||||
|
.sorted((a, b) -> {
|
||||||
|
Integer orderA = nodeOrderMap.getOrDefault(a.getNodeId(), Integer.MAX_VALUE);
|
||||||
|
Integer orderB = nodeOrderMap.getOrDefault(b.getNodeId(), Integer.MAX_VALUE);
|
||||||
|
return orderA.compareTo(orderB);
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
// 7. 从历史流程变量中获取每个节点的 outputs 数据
|
||||||
|
Map<String, Map<String, Object>> nodeOutputsMap = getNodeOutputsFromHistory(workflowInstance.getProcessInstanceId());
|
||||||
|
|
||||||
|
// 8. 转换为 DTO 并填充 outputs 数据
|
||||||
|
List<WorkflowNodeInstanceDTO> nodeInstanceDTOs = workflowNodeInstanceConverter.toDtoList(orderedNodeInstances);
|
||||||
|
nodeInstanceDTOs.forEach(dto -> {
|
||||||
|
// 从流程变量中获取该节点的 outputs 数据
|
||||||
|
Map<String, Object> nodeOutputs = nodeOutputsMap.get(dto.getNodeId());
|
||||||
|
if (nodeOutputs != null) {
|
||||||
|
// 提取 outputs 部分(格式:{nodeId: {outputs: {...}}}
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<String, Object> nodeData = (Map<String, Object>) nodeOutputs;
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<String, Object> outputs = (Map<String, Object>) nodeData.get("outputs");
|
||||||
|
if (outputs != null) {
|
||||||
|
dto.setOutputs(outputs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 9. 组装DTO
|
||||||
|
DeployRecordFlowGraphDTO dto = new DeployRecordFlowGraphDTO();
|
||||||
|
dto.setDeployRecordId(deployRecord.getId());
|
||||||
|
dto.setWorkflowInstanceId(workflowInstance.getId());
|
||||||
|
dto.setProcessInstanceId(workflowInstance.getProcessInstanceId());
|
||||||
|
dto.setDeployStatus(deployRecord.getStatus());
|
||||||
|
dto.setGraph(workflowInstance.getGraphSnapshot()); // 流程图结构数据
|
||||||
|
dto.setNodeInstances(nodeInstanceDTOs); // 节点执行状态(包含 outputs)
|
||||||
|
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从历史流程变量中获取所有节点的 outputs 数据
|
||||||
|
*
|
||||||
|
* @param processInstanceId 流程实例ID
|
||||||
|
* @return 节点ID到节点数据的映射(格式:{nodeId: {outputs: {...}}})
|
||||||
|
*/
|
||||||
|
private Map<String, Map<String, Object>> getNodeOutputsFromHistory(String processInstanceId) {
|
||||||
|
Map<String, Map<String, Object>> nodeOutputsMap = new HashMap<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 查询历史流程变量
|
||||||
|
List<HistoricVariableInstance> variables = historyService
|
||||||
|
.createHistoricVariableInstanceQuery()
|
||||||
|
.processInstanceId(processInstanceId)
|
||||||
|
.list();
|
||||||
|
|
||||||
|
// 遍历变量,查找节点相关的变量(格式:{nodeId: {outputs: {...}}}
|
||||||
|
for (HistoricVariableInstance variable : variables) {
|
||||||
|
String variableName = variable.getVariableName();
|
||||||
|
Object variableValue = variable.getValue();
|
||||||
|
|
||||||
|
// 检查是否是节点数据(节点ID通常是 sid_ 开头)
|
||||||
|
if (variableName != null && variableValue instanceof Map) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<String, Object> nodeData = (Map<String, Object>) variableValue;
|
||||||
|
|
||||||
|
// 检查是否包含 outputs 字段(这是节点数据的标识)
|
||||||
|
if (nodeData.containsKey("outputs")) {
|
||||||
|
nodeOutputsMap.put(variableName, nodeData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("从历史流程变量中获取节点 outputs: processInstanceId={}, nodeCount={}",
|
||||||
|
processInstanceId, nodeOutputsMap.size());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("获取节点 outputs 失败: processInstanceId={}", processInstanceId, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodeOutputsMap;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 判断是否为终态
|
* 判断是否为终态
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import com.qqchen.deploy.backend.workflow.enums.WorkflowNodeInstanceStatusEnums;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public class WorkflowNodeInstanceDTO extends BaseDTO {
|
public class WorkflowNodeInstanceDTO extends BaseDTO {
|
||||||
@ -30,4 +31,11 @@ public class WorkflowNodeInstanceDTO extends BaseDTO {
|
|||||||
private LocalDateTime createTime;
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
private LocalDateTime updateTime;
|
private LocalDateTime updateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 节点执行结果(outputs)
|
||||||
|
* 从流程变量中获取,格式:{nodeId: {outputs: {...}}}
|
||||||
|
* 例如:审批节点的 outputs 包含 approvalResult、approver、approvalTime 等
|
||||||
|
*/
|
||||||
|
private Map<String, Object> outputs;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +0,0 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.exception;
|
|
||||||
|
|
||||||
public class WorkflowValidationException extends RuntimeException {
|
|
||||||
|
|
||||||
public WorkflowValidationException(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public WorkflowValidationException(String message, Throwable cause) {
|
|
||||||
super(message, cause);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.delegate;
|
package com.qqchen.deploy.backend.workflow.listener;
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.framework.utils.SpelExpressionResolver;
|
import com.qqchen.deploy.backend.framework.utils.SpelExpressionResolver;
|
||||||
import com.qqchen.deploy.backend.workflow.dto.event.ApprovalTaskCreatedEvent;
|
import com.qqchen.deploy.backend.workflow.dto.event.ApprovalTaskCreatedEvent;
|
||||||
@ -9,8 +9,6 @@ import com.qqchen.deploy.backend.workflow.repository.IWorkflowDefinitionReposito
|
|||||||
import com.qqchen.deploy.backend.workflow.repository.IWorkflowInstanceRepository;
|
import com.qqchen.deploy.backend.workflow.repository.IWorkflowInstanceRepository;
|
||||||
import com.qqchen.deploy.backend.workflow.repository.IWorkflowCategoryRepository;
|
import com.qqchen.deploy.backend.workflow.repository.IWorkflowCategoryRepository;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.flowable.engine.ProcessEngineConfiguration;
|
|
||||||
import org.flowable.engine.impl.cfg.ProcessEngineConfigurationImpl;
|
|
||||||
import org.flowable.task.service.delegate.DelegateTask;
|
import org.flowable.task.service.delegate.DelegateTask;
|
||||||
import org.springframework.context.ApplicationEventPublisher;
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
@ -21,15 +19,15 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 审批任务监听器
|
* 审批任务创建监听器
|
||||||
* 在 UserTask 创建时被调用,负责配置审批人和任务基本信息
|
* 在 UserTask 创建时被调用,负责配置审批人和任务基本信息
|
||||||
*
|
*
|
||||||
* @author qqchen
|
* @author qqchen
|
||||||
* @since 2025-10-23
|
* @since 2025-10-23
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component("approvalTaskListener")
|
@Component("approvalCreateTaskListener")
|
||||||
public class ApprovalTaskListener extends BaseTaskListener<ApprovalInputMapping, ApprovalOutputs> {
|
public class ApprovalCreateTaskListener extends BaseTaskListener<ApprovalInputMapping, ApprovalOutputs> {
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private ApplicationEventPublisher eventPublisher;
|
private ApplicationEventPublisher eventPublisher;
|
||||||
@ -1,6 +1,5 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.listener.flowable.execution;
|
package com.qqchen.deploy.backend.workflow.listener;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.qqchen.deploy.backend.workflow.dto.event.ApprovalCompletedEvent;
|
import com.qqchen.deploy.backend.workflow.dto.event.ApprovalCompletedEvent;
|
||||||
import com.qqchen.deploy.backend.workflow.dto.inputmapping.ApprovalInputMapping;
|
import com.qqchen.deploy.backend.workflow.dto.inputmapping.ApprovalInputMapping;
|
||||||
@ -29,15 +28,15 @@ import java.util.Map;
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 审批任务结束监听器
|
* 审批节点结束监听器
|
||||||
* 在 UserTask 结束时自动装配上下文信息并构建 ApprovalOutputs
|
* 在 UserTask 结束时自动装配上下文信息并构建 ApprovalOutputs
|
||||||
*
|
*
|
||||||
* @author qqchen
|
* @author qqchen
|
||||||
* @since 2025-10-23
|
* @since 2025-10-23
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component("approvalExecutionListener")
|
@Component("approvalEndExecutionListener")
|
||||||
public class ApprovalExecutionListener implements ExecutionListener {
|
public class ApprovalEndExecutionListener implements ExecutionListener {
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private ObjectMapper objectMapper;
|
private ObjectMapper objectMapper;
|
||||||
@ -63,45 +62,120 @@ public class ApprovalExecutionListener implements ExecutionListener {
|
|||||||
try {
|
try {
|
||||||
log.info("ApprovalExecutionListener: Building outputs for node: {}", nodeId);
|
log.info("ApprovalExecutionListener: Building outputs for node: {}", nodeId);
|
||||||
|
|
||||||
// 1. 读取现有的 NodeContext
|
// 1. 读取 NodeContext(统一使用 NodeContext,与 BaseNodeDelegate 保持一致)
|
||||||
Object nodeDataObj = execution.getVariable(nodeId);
|
NodeContext<ApprovalInputMapping, ApprovalOutputs> nodeContext =
|
||||||
if (!(nodeDataObj instanceof Map)) {
|
readNodeContext(execution, nodeId);
|
||||||
log.warn("NodeContext not found for node: {}, skipping ApprovalExecutionListener", nodeId);
|
if (nodeContext == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
// 2. 检查并获取 outputs
|
||||||
Map<String, Object> nodeDataMap = (Map<String, Object>) nodeDataObj;
|
|
||||||
NodeContext<ApprovalInputMapping, ApprovalOutputs> nodeContext =
|
|
||||||
NodeContext.fromMap(nodeDataMap, ApprovalInputMapping.class, ApprovalOutputs.class, objectMapper);
|
|
||||||
|
|
||||||
// 2. 检查是否已经有临时的 outputs(由 ApprovalTaskServiceImpl 设置)
|
|
||||||
ApprovalOutputs outputs = nodeContext.getOutputs();
|
ApprovalOutputs outputs = nodeContext.getOutputs();
|
||||||
if (outputs == null) {
|
if (outputs == null) {
|
||||||
log.warn("Outputs not found in NodeContext for node: {}, skipping", nodeId);
|
log.warn("Outputs not found in NodeContext for node: {}, skipping", nodeId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 自动装配上下文信息(计算审批用时、历史任务ID等)
|
// 3. 自动装配上下文信息(丰富 outputs)
|
||||||
enrichApprovalOutputs(execution, nodeId, outputs);
|
enrichApprovalOutputs(execution, nodeId, outputs);
|
||||||
|
|
||||||
// 4. 更新 NodeContext 的 outputs
|
// 4. 更新并保存 NodeContext(与 BaseNodeDelegate 保持一致)
|
||||||
nodeContext.setOutputs(outputs);
|
nodeContext.setOutputs(outputs);
|
||||||
|
saveNodeContext(execution, nodeId, nodeContext);
|
||||||
// 5. 保存回流程变量
|
|
||||||
execution.setVariable(nodeId, nodeContext.toMap(objectMapper));
|
|
||||||
|
|
||||||
log.info("Stored approval outputs for node: {}, result: {}", nodeId, outputs.getApprovalResult());
|
log.info("Stored approval outputs for node: {}, result: {}", nodeId, outputs.getApprovalResult());
|
||||||
|
|
||||||
// 6. 发布审批完成事件
|
// 5. 发布审批完成事件
|
||||||
publishApprovalCompletedEvent(execution, nodeId, outputs);
|
publishApprovalCompletedEvent(execution, nodeId, outputs);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Failed to build approval outputs for node: {}", nodeId, e);
|
log.error("Failed to build approval outputs for node: {}", nodeId, e);
|
||||||
|
|
||||||
|
// 异常处理:统一使用 NodeContext 设置失败状态(与 BaseNodeDelegate 保持一致)
|
||||||
|
handleFailure(execution, nodeId, e);
|
||||||
|
|
||||||
throw new RuntimeException("Failed to build approval outputs: " + nodeId, e);
|
throw new RuntimeException("Failed to build approval outputs: " + nodeId, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取 NodeContext(与 BaseNodeDelegate 的模式保持一致)
|
||||||
|
*/
|
||||||
|
private NodeContext<ApprovalInputMapping, ApprovalOutputs> readNodeContext(
|
||||||
|
DelegateExecution execution, String nodeId) {
|
||||||
|
try {
|
||||||
|
Object nodeDataObj = execution.getVariable(nodeId);
|
||||||
|
if (!(nodeDataObj instanceof Map)) {
|
||||||
|
log.warn("NodeContext not found for node: {}, skipping ApprovalExecutionListener", nodeId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<String, Object> nodeDataMap = (Map<String, Object>) nodeDataObj;
|
||||||
|
return NodeContext.fromMap(nodeDataMap, ApprovalInputMapping.class, ApprovalOutputs.class, objectMapper);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to read NodeContext for node: {}", nodeId, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存 NodeContext(与 BaseNodeDelegate 的模式保持一致)
|
||||||
|
*/
|
||||||
|
private void saveNodeContext(DelegateExecution execution, String nodeId,
|
||||||
|
NodeContext<ApprovalInputMapping, ApprovalOutputs> nodeContext) {
|
||||||
|
try {
|
||||||
|
execution.setVariable(nodeId, nodeContext.toMap(objectMapper));
|
||||||
|
log.debug("Saved NodeContext for node: {}", nodeId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to save NodeContext for node: {}", nodeId, e);
|
||||||
|
throw new RuntimeException("Failed to save NodeContext: " + nodeId, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动装配 ApprovalOutputs(丰富 outputs 的上下文信息)
|
||||||
|
*/
|
||||||
|
private void enrichApprovalOutputs(DelegateExecution execution, String nodeId, ApprovalOutputs outputs) {
|
||||||
|
// 1. 审批用时(从任务历史计算)
|
||||||
|
Long duration = calculateApprovalDuration(execution, nodeId);
|
||||||
|
if (duration != null) {
|
||||||
|
outputs.setApprovalDuration(duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 节点执行状态(审批节点无论通过还是拒绝,都是成功完成)
|
||||||
|
// 只有当审批过程中发生系统异常时,才应该标记为失败(FAILED)
|
||||||
|
if (outputs.getStatus() == null) {
|
||||||
|
outputs.setStatus(NodeExecutionStatusEnum.SUCCESS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理异常情况(统一使用 NodeContext,与 BaseNodeDelegate 保持一致)
|
||||||
|
*/
|
||||||
|
private void handleFailure(DelegateExecution execution, String nodeId, Exception e) {
|
||||||
|
try {
|
||||||
|
NodeContext<ApprovalInputMapping, ApprovalOutputs> nodeContext = readNodeContext(execution, nodeId);
|
||||||
|
if (nodeContext == null) {
|
||||||
|
// 如果无法读取 NodeContext,创建新的
|
||||||
|
nodeContext = new NodeContext<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建失败状态的 outputs(与 BaseNodeDelegate 的模式一致)
|
||||||
|
ApprovalOutputs failureOutputs = nodeContext.getOutputs();
|
||||||
|
if (failureOutputs == null) {
|
||||||
|
failureOutputs = new ApprovalOutputs();
|
||||||
|
}
|
||||||
|
failureOutputs.setStatus(NodeExecutionStatusEnum.FAILURE);
|
||||||
|
failureOutputs.setMessage("审批节点执行异常: " + e.getMessage());
|
||||||
|
|
||||||
|
nodeContext.setOutputs(failureOutputs);
|
||||||
|
saveNodeContext(execution, nodeId, nodeContext);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.error("Failed to set error status for node: {}", nodeId, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发布审批完成事件
|
* 发布审批完成事件
|
||||||
*/
|
*/
|
||||||
@ -150,17 +224,6 @@ public class ApprovalExecutionListener implements ExecutionListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 丰富 ApprovalOutputs
|
|
||||||
* 自动装配上下文信息(审批用时等)
|
|
||||||
*/
|
|
||||||
private void enrichApprovalOutputs(DelegateExecution execution, String nodeId, ApprovalOutputs outputs) {
|
|
||||||
// 1. ⚙️ 自动装配:审批用时(从任务历史计算)
|
|
||||||
Long duration = calculateApprovalDuration(execution, nodeId);
|
|
||||||
outputs.setApprovalDuration(duration);
|
|
||||||
|
|
||||||
// TODO: 未来可以添加更多自动装配逻辑(如历史任务ID、审批状态等)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建 ApprovalOutputs(弃用,保留以防万一)
|
* 构建 ApprovalOutputs(弃用,保留以防万一)
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.delegate;
|
package com.qqchen.deploy.backend.workflow.listener;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
@ -20,7 +20,6 @@ import java.util.Map;
|
|||||||
*
|
*
|
||||||
* @param <I> 输入映射类型 (InputMapping)
|
* @param <I> 输入映射类型 (InputMapping)
|
||||||
* @param <O> 输出类型 (Outputs) - 用于类型标识
|
* @param <O> 输出类型 (Outputs) - 用于类型标识
|
||||||
*
|
|
||||||
* @author qqchen
|
* @author qqchen
|
||||||
* @since 2025-10-23
|
* @since 2025-10-23
|
||||||
*/
|
*/
|
||||||
@ -32,7 +31,9 @@ public abstract class BaseTaskListener<I, O> implements TaskListener {
|
|||||||
|
|
||||||
// Flowable 自动注入的字段
|
// Flowable 自动注入的字段
|
||||||
protected Expression nodeId;
|
protected Expression nodeId;
|
||||||
|
|
||||||
protected Expression configs;
|
protected Expression configs;
|
||||||
|
|
||||||
protected Expression inputMapping;
|
protected Expression inputMapping;
|
||||||
|
|
||||||
private Class<I> inputMappingClass;
|
private Class<I> inputMappingClass;
|
||||||
@ -73,9 +74,9 @@ public abstract class BaseTaskListener<I, O> implements TaskListener {
|
|||||||
/**
|
/**
|
||||||
* 配置任务(子类实现)
|
* 配置任务(子类实现)
|
||||||
*
|
*
|
||||||
* @param delegateTask Flowable 任务对象
|
* @param delegateTask Flowable 任务对象
|
||||||
* @param configs 节点配置
|
* @param configs 节点配置
|
||||||
* @param inputMapping 输入映射(强类型)
|
* @param inputMapping 输入映射(强类型)
|
||||||
*/
|
*/
|
||||||
protected abstract void configureTask(
|
protected abstract void configureTask(
|
||||||
DelegateTask delegateTask,
|
DelegateTask delegateTask,
|
||||||
@ -131,7 +132,8 @@ public abstract class BaseTaskListener<I, O> implements TaskListener {
|
|||||||
return new HashMap<>();
|
return new HashMap<>();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return objectMapper.readValue(jsonStr, new TypeReference<Map<String, Object>>() {});
|
return objectMapper.readValue(jsonStr, new TypeReference<>() {
|
||||||
|
});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Failed to parse JSON field: {}", jsonStr, e);
|
log.error("Failed to parse JSON field: {}", jsonStr, e);
|
||||||
return new HashMap<>();
|
return new HashMap<>();
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.listener.flowable.execution;
|
package com.qqchen.deploy.backend.workflow.listener;
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.workflow.enums.WorkflowNodeInstanceStatusEnums;
|
import com.qqchen.deploy.backend.workflow.enums.WorkflowNodeInstanceStatusEnums;
|
||||||
import com.qqchen.deploy.backend.workflow.dto.event.WorkflowNodeInstanceStatusChangeEvent;
|
import com.qqchen.deploy.backend.workflow.dto.event.WorkflowNodeInstanceStatusChangeEvent;
|
||||||
@ -13,8 +13,8 @@ import org.springframework.stereotype.Component;
|
|||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component("gatewayExecutionListener")
|
@Component("gatewayStartExecutionListener")
|
||||||
public class GatewayExecutionListener implements ExecutionListener {
|
public class GatewayStartExecutionListener implements ExecutionListener {
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private ApplicationEventPublisher eventPublisher;
|
private ApplicationEventPublisher eventPublisher;
|
||||||
@ -1,14 +1,11 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.listener.flowable.execution;
|
package com.qqchen.deploy.backend.workflow.listener;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.qqchen.deploy.backend.framework.utils.SpelExpressionResolver;
|
import com.qqchen.deploy.backend.framework.utils.SpelExpressionResolver;
|
||||||
import com.qqchen.deploy.backend.workflow.constants.WorkFlowConstants;
|
|
||||||
import com.qqchen.deploy.backend.workflow.enums.NodeExecutionStatusEnum;
|
import com.qqchen.deploy.backend.workflow.enums.NodeExecutionStatusEnum;
|
||||||
import com.qqchen.deploy.backend.workflow.enums.WorkflowNodeInstanceStatusEnums;
|
import com.qqchen.deploy.backend.workflow.enums.WorkflowNodeInstanceStatusEnums;
|
||||||
import com.qqchen.deploy.backend.workflow.dto.event.WorkflowNodeInstanceStatusChangeEvent;
|
import com.qqchen.deploy.backend.workflow.dto.event.WorkflowNodeInstanceStatusChangeEvent;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
import org.flowable.bpmn.model.FlowElement;
|
import org.flowable.bpmn.model.FlowElement;
|
||||||
import org.flowable.engine.delegate.DelegateExecution;
|
import org.flowable.engine.delegate.DelegateExecution;
|
||||||
import org.flowable.engine.delegate.ExecutionListener;
|
import org.flowable.engine.delegate.ExecutionListener;
|
||||||
@ -22,8 +19,8 @@ import java.time.LocalDateTime;
|
|||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component("globalNodeExecutionListener")
|
@Component("globalNodeStartEndExecutionListener")
|
||||||
public class GlobalNodeExecutionListener implements ExecutionListener {
|
public class GlobalNodeStartEndExecutionListener implements ExecutionListener {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 不产生 outputs 的节点类型(事件/网关节点)
|
* 不产生 outputs 的节点类型(事件/网关节点)
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.listener.business;
|
package com.qqchen.deploy.backend.workflow.listener;
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.workflow.dto.event.WorkflowInstanceStatusChangeEvent;
|
import com.qqchen.deploy.backend.workflow.dto.event.WorkflowInstanceStatusChangeEvent;
|
||||||
import com.qqchen.deploy.backend.workflow.service.IWorkflowInstanceService;
|
import com.qqchen.deploy.backend.workflow.service.IWorkflowInstanceService;
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package com.qqchen.deploy.backend.workflow.listener.business;
|
package com.qqchen.deploy.backend.workflow.listener;
|
||||||
|
|
||||||
import com.qqchen.deploy.backend.workflow.dto.event.WorkflowNodeInstanceStatusChangeEvent;
|
import com.qqchen.deploy.backend.workflow.dto.event.WorkflowNodeInstanceStatusChangeEvent;
|
||||||
import com.qqchen.deploy.backend.workflow.service.IWorkflowNodeInstanceService;
|
import com.qqchen.deploy.backend.workflow.service.IWorkflowNodeInstanceService;
|
||||||
@ -16,15 +16,10 @@ public class WorkflowNodeInstanceStatusChangeListener {
|
|||||||
@Resource
|
@Resource
|
||||||
private IWorkflowNodeInstanceService workflowNodeInstanceService;
|
private IWorkflowNodeInstanceService workflowNodeInstanceService;
|
||||||
|
|
||||||
/**
|
|
||||||
* 修改事务传播级别为 REQUIRED,加入到 Flowable 的事务中
|
|
||||||
* 这样节点实例会在同一个事务内保存,JavaDelegate 执行时就能立即查询到
|
|
||||||
*/
|
|
||||||
@EventListener
|
@EventListener
|
||||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||||
public void handleWorkflowStatusChange(WorkflowNodeInstanceStatusChangeEvent event) {
|
public void handleWorkflowStatusChange(WorkflowNodeInstanceStatusChangeEvent event) {
|
||||||
log.debug("Handling workflow node instance status change event: nodeId={}, status={}",
|
log.debug("Handling workflow node instance status change event: nodeId={}, status={}", event.getNodeId(), event.getStatus());
|
||||||
event.getNodeId(), event.getStatus());
|
|
||||||
workflowNodeInstanceService.saveOrUpdateWorkflowNodeInstance(event);
|
workflowNodeInstanceService.saveOrUpdateWorkflowNodeInstance(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -114,14 +114,14 @@ public class ApprovalTaskServiceImpl implements IApprovalTaskService {
|
|||||||
nodeContext = new NodeContext<>();
|
nodeContext = new NodeContext<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建临时审批数据(稍后由 ApprovalExecutionListener 完善)
|
// 创建临时审批数据(稍后由 ApprovalEndExecutionListener 完善)
|
||||||
ApprovalOutputs tempOutputs = new ApprovalOutputs();
|
ApprovalOutputs tempOutputs = new ApprovalOutputs();
|
||||||
tempOutputs.setApprovalResult(request.getResult());
|
tempOutputs.setApprovalResult(request.getResult());
|
||||||
tempOutputs.setApprover(task.getAssignee());
|
tempOutputs.setApprover(task.getAssignee());
|
||||||
tempOutputs.setApprovalTime(LocalDateTime.now());
|
tempOutputs.setApprovalTime(LocalDateTime.now());
|
||||||
tempOutputs.setApprovalComment(request.getComment());
|
tempOutputs.setApprovalComment(request.getComment());
|
||||||
|
|
||||||
// 暂时设置为 outputs(ApprovalExecutionListener 会完善)
|
// 暂时设置为 outputs(ApprovalEndExecutionListener 会完善)
|
||||||
nodeContext.setOutputs(tempOutputs);
|
nodeContext.setOutputs(tempOutputs);
|
||||||
|
|
||||||
// 保存回流程变量
|
// 保存回流程变量
|
||||||
@ -132,8 +132,8 @@ public class ApprovalTaskServiceImpl implements IApprovalTaskService {
|
|||||||
taskService.addComment(request.getTaskId(), task.getProcessInstanceId(), request.getComment());
|
taskService.addComment(request.getTaskId(), task.getProcessInstanceId(), request.getComment());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. 完成任务(触发 ApprovalExecutionListener)
|
// 6. 完成任务(触发 ApprovalEndExecutionListener)
|
||||||
// ApprovalExecutionListener 会:
|
// ApprovalEndExecutionListener 会:
|
||||||
// - 读取上面设置的变量
|
// - 读取上面设置的变量
|
||||||
// - 自动装配其他上下文信息(approvalDuration、allApprovers 等)
|
// - 自动装配其他上下文信息(approvalDuration、allApprovers 等)
|
||||||
// - 构建完整的 ApprovalOutputs 对象
|
// - 构建完整的 ApprovalOutputs 对象
|
||||||
|
|||||||
@ -224,11 +224,11 @@ public class BpmnConverter {
|
|||||||
|
|
||||||
if (element instanceof Gateway) {
|
if (element instanceof Gateway) {
|
||||||
// 网关节点只添加 start 监听器
|
// 网关节点只添加 start 监听器
|
||||||
executionListeners.add(createExecutionListener("start", "${gatewayExecutionListener}"));
|
executionListeners.add(createExecutionListener("start", "${gatewayStartExecutionListener}"));
|
||||||
} else {
|
} else {
|
||||||
// 其他节点添加 start 和 end 监听器
|
// 其他节点添加 start 和 end 监听器
|
||||||
executionListeners.add(createExecutionListener("start", "${globalNodeExecutionListener}"));
|
executionListeners.add(createExecutionListener("start", "${globalNodeStartEndExecutionListener}"));
|
||||||
executionListeners.add(createExecutionListener("end", "${globalNodeExecutionListener}"));
|
executionListeners.add(createExecutionListener("end", "${globalNodeStartEndExecutionListener}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
extensionElements.put("executionListener", executionListeners);
|
extensionElements.put("executionListener", executionListeners);
|
||||||
@ -245,11 +245,11 @@ public class BpmnConverter {
|
|||||||
List<ExtensionElement> executionListeners = new ArrayList<>();
|
List<ExtensionElement> executionListeners = new ArrayList<>();
|
||||||
|
|
||||||
// 添加开始事件监听器
|
// 添加开始事件监听器
|
||||||
ExtensionElement startListener = createExecutionListener("start", "${globalNodeExecutionListener}");
|
ExtensionElement startListener = createExecutionListener("start", "${globalNodeStartEndExecutionListener}");
|
||||||
executionListeners.add(startListener);
|
executionListeners.add(startListener);
|
||||||
|
|
||||||
// 添加结束事件监听器
|
// 添加结束事件监听器
|
||||||
ExtensionElement endListener = createExecutionListener("end", "${globalNodeExecutionListener}");
|
ExtensionElement endListener = createExecutionListener("end", "${globalNodeStartEndExecutionListener}");
|
||||||
executionListeners.add(endListener);
|
executionListeners.add(endListener);
|
||||||
|
|
||||||
extensionElements.put("executionListener", executionListeners);
|
extensionElements.put("executionListener", executionListeners);
|
||||||
@ -284,23 +284,23 @@ public class BpmnConverter {
|
|||||||
*/
|
*/
|
||||||
private void configureUserTask(UserTask userTask, WorkflowDefinitionGraphNode node, Map<String, List<ExtensionElement>> extensionElements, String validId) {
|
private void configureUserTask(UserTask userTask, WorkflowDefinitionGraphNode node, Map<String, List<ExtensionElement>> extensionElements, String validId) {
|
||||||
try {
|
try {
|
||||||
// ✅ 1. 创建 TaskListener(在任务创建时调用 ApprovalTaskListener)
|
// ✅ 1. 创建 TaskListener(在任务创建时调用 ApprovalCreateTaskListener)
|
||||||
ExtensionElement taskListener = new ExtensionElement();
|
ExtensionElement taskListener = new ExtensionElement();
|
||||||
taskListener.setName("taskListener");
|
taskListener.setName("taskListener");
|
||||||
taskListener.setNamespace("http://flowable.org/bpmn");
|
taskListener.setNamespace("http://flowable.org/bpmn");
|
||||||
taskListener.setNamespacePrefix("flowable");
|
taskListener.setNamespacePrefix("flowable");
|
||||||
taskListener.addAttribute(createAttribute("event", "create"));
|
taskListener.addAttribute(createAttribute("event", "create"));
|
||||||
taskListener.addAttribute(createAttribute("delegateExpression", "${approvalTaskListener}"));
|
taskListener.addAttribute(createAttribute("delegateExpression", "${approvalCreateTaskListener}"));
|
||||||
|
|
||||||
// ✅ 2. 将 field 字段作为 TaskListener 的子元素添加(而不是 UserTask 的子元素)
|
// ✅ 2. 将 field 字段作为 TaskListener 的子元素添加(而不是 UserTask 的子元素)
|
||||||
// 这样 ApprovalTaskListener 才能通过 @field 注解注入这些字段
|
// 这样 ApprovalCreateTaskListener 才能通过 @field 注解注入这些字段
|
||||||
addFieldsToTaskListener(taskListener, node, validId);
|
addFieldsToTaskListener(taskListener, node, validId);
|
||||||
|
|
||||||
// ✅ 3. 将 TaskListener 添加到扩展元素
|
// ✅ 3. 将 TaskListener 添加到扩展元素
|
||||||
extensionElements.computeIfAbsent("taskListener", k -> new ArrayList<>()).add(taskListener);
|
extensionElements.computeIfAbsent("taskListener", k -> new ArrayList<>()).add(taskListener);
|
||||||
|
|
||||||
// ✅ 4. 添加 ApprovalExecutionListener(在任务结束时构建 ApprovalOutputs)
|
// ✅ 4. 添加 ApprovalEndExecutionListener(在任务结束时构建 ApprovalOutputs)
|
||||||
// 确保它在 globalNodeExecutionListener 之前执行,这样状态变量才能正确设置
|
// 确保它在 globalNodeStartEndExecutionListener 之前执行,这样状态变量才能正确设置
|
||||||
addApprovalExecutionListener(extensionElements);
|
addApprovalExecutionListener(extensionElements);
|
||||||
|
|
||||||
// ✅ 5. 设置扩展元素
|
// ✅ 5. 设置扩展元素
|
||||||
@ -343,8 +343,8 @@ public class BpmnConverter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 添加 ApprovalExecutionListener(审批任务结束监听器)
|
* 添加 ApprovalEndExecutionListener(审批节点结束监听器)
|
||||||
* 确保它在 globalNodeExecutionListener 之前执行
|
* 确保它在 globalNodeStartEndExecutionListener 之前执行
|
||||||
*
|
*
|
||||||
* @param extensionElements 扩展元素
|
* @param extensionElements 扩展元素
|
||||||
*/
|
*/
|
||||||
@ -352,31 +352,31 @@ public class BpmnConverter {
|
|||||||
List<ExtensionElement> executionListeners =
|
List<ExtensionElement> executionListeners =
|
||||||
extensionElements.computeIfAbsent("executionListener", k -> new ArrayList<>());
|
extensionElements.computeIfAbsent("executionListener", k -> new ArrayList<>());
|
||||||
|
|
||||||
// 找到 end 事件的 globalNodeExecutionListener 的位置
|
// 找到 end 事件的 globalNodeStartEndExecutionListener 的位置
|
||||||
int endListenerIndex = -1;
|
int endListenerIndex = -1;
|
||||||
for (int i = 0; i < executionListeners.size(); i++) {
|
for (int i = 0; i < executionListeners.size(); i++) {
|
||||||
ExtensionElement listener = executionListeners.get(i);
|
ExtensionElement listener = executionListeners.get(i);
|
||||||
String event = listener.getAttributeValue(null, "event");
|
String event = listener.getAttributeValue(null, "event");
|
||||||
String delegateExpr = listener.getAttributeValue(null, "delegateExpression");
|
String delegateExpr = listener.getAttributeValue(null, "delegateExpression");
|
||||||
|
|
||||||
if ("end".equals(event) && "${globalNodeExecutionListener}".equals(delegateExpr)) {
|
if ("end".equals(event) && "${globalNodeStartEndExecutionListener}".equals(delegateExpr)) {
|
||||||
endListenerIndex = i;
|
endListenerIndex = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 在 globalNodeExecutionListener 之前插入 approvalExecutionListener
|
// 在 globalNodeStartEndExecutionListener 之前插入 approvalEndExecutionListener
|
||||||
// 这样审批结果会先被构建,状态变量也会被设置
|
// 这样审批结果会先被构建,状态变量也会被设置
|
||||||
ExtensionElement approvalListener = createExecutionListener("end", "${approvalExecutionListener}");
|
ExtensionElement approvalListener = createExecutionListener("end", "${approvalEndExecutionListener}");
|
||||||
|
|
||||||
if (endListenerIndex >= 0) {
|
if (endListenerIndex >= 0) {
|
||||||
executionListeners.add(endListenerIndex, approvalListener);
|
executionListeners.add(endListenerIndex, approvalListener);
|
||||||
} else {
|
} else {
|
||||||
// 如果没有找到 globalNodeExecutionListener,直接添加到列表末尾
|
// 如果没有找到 globalNodeStartEndExecutionListener,直接添加到列表末尾
|
||||||
executionListeners.add(approvalListener);
|
executionListeners.add(approvalListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug("Added ApprovalExecutionListener before GlobalNodeExecutionListener");
|
log.debug("Added ApprovalEndExecutionListener before GlobalNodeStartEndExecutionListener");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
333
frontend/src/pages/Dashboard/components/DeployFlowGraphModal.tsx
Normal file
333
frontend/src/pages/Dashboard/components/DeployFlowGraphModal.tsx
Normal file
@ -0,0 +1,333 @@
|
|||||||
|
import React, { useEffect, useState, useMemo } from 'react';
|
||||||
|
import { ReactFlowProvider, ReactFlow, Background, Controls, MiniMap, Node, Edge, Handle, Position, BackgroundVariant } from '@xyflow/react';
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Loader2 } from 'lucide-react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import '@xyflow/react/dist/style.css';
|
||||||
|
import { getDeployRecordFlowGraph } from '../service';
|
||||||
|
import type { DeployRecordFlowGraph, WorkflowNodeInstance } from '../types';
|
||||||
|
import { getStatusIcon, getStatusText } from '../utils/dashboardUtils';
|
||||||
|
|
||||||
|
interface DeployFlowGraphModalProps {
|
||||||
|
open: boolean;
|
||||||
|
deployRecordId: number | null;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 节点状态颜色映射
|
||||||
|
const nodeStatusColorMap: Record<string, { bg: string; border: string; text: string }> = {
|
||||||
|
NOT_STARTED: {
|
||||||
|
bg: '#fafafa',
|
||||||
|
border: '#d9d9d9',
|
||||||
|
text: '#666'
|
||||||
|
},
|
||||||
|
RUNNING: {
|
||||||
|
bg: 'linear-gradient(135deg, #1890ff 0%, #40a9ff 100%)',
|
||||||
|
border: '#1890ff',
|
||||||
|
text: '#fff'
|
||||||
|
},
|
||||||
|
COMPLETED: {
|
||||||
|
bg: 'linear-gradient(135deg, #52c41a 0%, #73d13d 100%)',
|
||||||
|
border: '#52c41a',
|
||||||
|
text: '#fff'
|
||||||
|
},
|
||||||
|
FAILED: {
|
||||||
|
bg: 'linear-gradient(135deg, #ff4d4f 0%, #ff7875 100%)',
|
||||||
|
border: '#ff4d4f',
|
||||||
|
text: '#fff'
|
||||||
|
},
|
||||||
|
REJECTED: {
|
||||||
|
bg: 'linear-gradient(135deg, #ff4d4f 0%, #ff7875 100%)',
|
||||||
|
border: '#ff4d4f',
|
||||||
|
text: '#fff'
|
||||||
|
},
|
||||||
|
CANCELLED: {
|
||||||
|
bg: '#d9d9d9',
|
||||||
|
border: '#bfbfbf',
|
||||||
|
text: '#666'
|
||||||
|
},
|
||||||
|
TERMINATED: {
|
||||||
|
bg: 'linear-gradient(135deg, #faad14 0%, #ffc53d 100%)',
|
||||||
|
border: '#faad14',
|
||||||
|
text: '#fff'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 简版自定义节点组件(只显示节点名和状态)
|
||||||
|
const CustomFlowNode: React.FC<any> = ({ data, selected }) => {
|
||||||
|
const status = data.status || 'NOT_STARTED';
|
||||||
|
const colors = nodeStatusColorMap[status] || nodeStatusColorMap.NOT_STARTED;
|
||||||
|
const isNotStarted = status === 'NOT_STARTED';
|
||||||
|
const isRunning = status === 'RUNNING';
|
||||||
|
const nodeType = data.nodeType;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative group">
|
||||||
|
{/* 输入连接点 */}
|
||||||
|
{nodeType !== 'START_EVENT' && (
|
||||||
|
<Handle
|
||||||
|
type="target"
|
||||||
|
position={Position.Left}
|
||||||
|
className="!w-2 !h-2 !rounded-full !border-2 !border-white"
|
||||||
|
style={{ background: colors.border }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 节点内容 - 简化版 */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'min-w-[80px] max-w-[120px] transition-all duration-200 rounded-md p-2',
|
||||||
|
selected ? 'ring-1 ring-primary' : '',
|
||||||
|
isNotStarted ? 'border-dashed border' : 'shadow-sm',
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
background: colors.bg,
|
||||||
|
color: colors.text,
|
||||||
|
borderColor: colors.border,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="font-medium text-xs mb-0.5 leading-tight">{data.nodeName}</div>
|
||||||
|
<div className="text-[10px] opacity-80 leading-tight">{getStatusText(status)}</div>
|
||||||
|
{isRunning && <Loader2 className="h-3 w-3 animate-spin mx-auto mt-0.5" />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 输出连接点 */}
|
||||||
|
{nodeType !== 'END_EVENT' && (
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
className="!w-2 !h-2 !rounded-full !border-2 !border-white"
|
||||||
|
style={{ background: colors.border }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const nodeTypes = {
|
||||||
|
default: CustomFlowNode,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 部署流程图模态框
|
||||||
|
*/
|
||||||
|
export const DeployFlowGraphModal: React.FC<DeployFlowGraphModalProps> = ({
|
||||||
|
open,
|
||||||
|
deployRecordId,
|
||||||
|
onOpenChange,
|
||||||
|
}) => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [flowData, setFlowData] = useState<DeployRecordFlowGraph | null>(null);
|
||||||
|
|
||||||
|
// 加载流程图数据
|
||||||
|
useEffect(() => {
|
||||||
|
if (open && deployRecordId) {
|
||||||
|
setLoading(true);
|
||||||
|
getDeployRecordFlowGraph(deployRecordId)
|
||||||
|
.then((data) => {
|
||||||
|
setFlowData(data);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('加载部署流程图失败:', error);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setFlowData(null);
|
||||||
|
}
|
||||||
|
}, [open, deployRecordId]);
|
||||||
|
|
||||||
|
// 创建节点状态映射(用于快速查找)
|
||||||
|
const nodeInstanceMap = useMemo(() => {
|
||||||
|
if (!flowData?.nodeInstances) return new Map<string, WorkflowNodeInstance>();
|
||||||
|
const map = new Map<string, WorkflowNodeInstance>();
|
||||||
|
flowData.nodeInstances.forEach((instance) => {
|
||||||
|
map.set(instance.nodeId, instance);
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
}, [flowData]);
|
||||||
|
|
||||||
|
// 获取节点状态(通过匹配 nodeInstances)
|
||||||
|
const getNodeStatus = (nodeId: string): string => {
|
||||||
|
const instance = nodeInstanceMap.get(nodeId);
|
||||||
|
return instance ? instance.status : 'NOT_STARTED';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 转换为 React Flow 节点(使用后端返回的 position)
|
||||||
|
const flowNodes: Node[] = useMemo(() => {
|
||||||
|
if (!flowData?.graph?.nodes) return [];
|
||||||
|
|
||||||
|
return flowData.graph.nodes.map((node) => {
|
||||||
|
// 1. 匹配执行状态
|
||||||
|
const instance = nodeInstanceMap.get(node.id);
|
||||||
|
const status = instance ? instance.status : 'NOT_STARTED';
|
||||||
|
|
||||||
|
const colors = nodeStatusColorMap[status] || nodeStatusColorMap.NOT_STARTED;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: node.id,
|
||||||
|
type: 'default',
|
||||||
|
// 2. 使用后端返回的 position(不要重新计算)
|
||||||
|
position: node.position,
|
||||||
|
data: {
|
||||||
|
nodeName: node.nodeName,
|
||||||
|
nodeType: node.nodeType,
|
||||||
|
status: status,
|
||||||
|
startTime: instance?.startTime || null,
|
||||||
|
endTime: instance?.endTime || null,
|
||||||
|
// 3. 显示执行结果(如果有)
|
||||||
|
outputs: instance?.outputs || null,
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
background: colors.bg,
|
||||||
|
borderColor: colors.border,
|
||||||
|
color: colors.text,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [flowData, nodeInstanceMap]);
|
||||||
|
|
||||||
|
// 判断边是否已执行
|
||||||
|
const isEdgeExecuted = (sourceNodeId: string): boolean => {
|
||||||
|
const sourceStatus = getNodeStatus(sourceNodeId);
|
||||||
|
return sourceStatus === 'COMPLETED' || sourceStatus === 'RUNNING';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 判断边是否中断
|
||||||
|
const isEdgeInterrupted = (sourceNodeId: string): boolean => {
|
||||||
|
const sourceStatus = getNodeStatus(sourceNodeId);
|
||||||
|
return sourceStatus === 'FAILED' || sourceStatus === 'REJECTED';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 渲染连线(使用 edge.from 和 edge.to)
|
||||||
|
const flowEdges: Edge[] = useMemo(() => {
|
||||||
|
if (!flowData?.graph?.edges) return [];
|
||||||
|
|
||||||
|
const edges: Edge[] = [];
|
||||||
|
flowData.graph.edges.forEach((edge, index) => {
|
||||||
|
// 使用 edge.from 和 edge.to
|
||||||
|
const source = edge.from;
|
||||||
|
const target = edge.to;
|
||||||
|
|
||||||
|
if (!source || !target) {
|
||||||
|
console.warn('边数据不完整:', edge);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const executed = isEdgeExecuted(source);
|
||||||
|
const interrupted = isEdgeInterrupted(source);
|
||||||
|
const targetStatus = getNodeStatus(target);
|
||||||
|
const isDashed = !executed || targetStatus === 'NOT_STARTED';
|
||||||
|
|
||||||
|
let strokeColor = '#d9d9d9';
|
||||||
|
if (interrupted) {
|
||||||
|
strokeColor = '#ff4d4f';
|
||||||
|
} else if (executed) {
|
||||||
|
strokeColor = '#52c41a';
|
||||||
|
}
|
||||||
|
|
||||||
|
edges.push({
|
||||||
|
id: `edge-${source}-${target}-${index}`,
|
||||||
|
source,
|
||||||
|
target,
|
||||||
|
type: 'smoothstep' as const,
|
||||||
|
animated: executed && targetStatus === 'RUNNING',
|
||||||
|
style: {
|
||||||
|
stroke: strokeColor,
|
||||||
|
strokeWidth: executed ? 3 : 2,
|
||||||
|
strokeDasharray: isDashed ? '5,5' : undefined,
|
||||||
|
},
|
||||||
|
markerEnd: {
|
||||||
|
type: 'arrowclosed' as const,
|
||||||
|
color: strokeColor,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return edges;
|
||||||
|
}, [flowData, nodeInstanceMap]);
|
||||||
|
|
||||||
|
// 获取部署状态信息
|
||||||
|
const deployStatusInfo = flowData
|
||||||
|
? (() => {
|
||||||
|
const { icon: StatusIcon, color } = getStatusIcon(flowData.deployStatus);
|
||||||
|
return {
|
||||||
|
icon: StatusIcon,
|
||||||
|
color,
|
||||||
|
text: getStatusText(flowData.deployStatus),
|
||||||
|
};
|
||||||
|
})()
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="!max-w-6xl w-[90vw] h-[90vh] flex flex-col p-0 overflow-hidden">
|
||||||
|
<DialogHeader className="px-6 pt-6 pb-4 border-b flex-shrink-0">
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<span>部署流程图</span>
|
||||||
|
{deployStatusInfo && (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={cn('flex items-center gap-1', deployStatusInfo.color)}
|
||||||
|
>
|
||||||
|
<deployStatusInfo.icon
|
||||||
|
className={cn(
|
||||||
|
'h-3 w-3',
|
||||||
|
flowData?.deployStatus === 'RUNNING' && 'animate-spin'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{deployStatusInfo.text}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{flowData && (
|
||||||
|
<span className="text-sm text-muted-foreground font-normal ml-auto">
|
||||||
|
记录ID: #{flowData.deployRecordId}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-hidden relative min-h-0">
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center justify-center h-full">
|
||||||
|
<div className="text-center space-y-4">
|
||||||
|
<Loader2 className="h-8 w-8 animate-spin mx-auto text-primary" />
|
||||||
|
<p className="text-sm text-muted-foreground">加载流程图数据...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : flowData ? (
|
||||||
|
<ReactFlowProvider>
|
||||||
|
<ReactFlow
|
||||||
|
nodes={flowNodes}
|
||||||
|
edges={flowEdges}
|
||||||
|
nodeTypes={nodeTypes}
|
||||||
|
fitView
|
||||||
|
className="bg-background"
|
||||||
|
>
|
||||||
|
<Background variant={BackgroundVariant.Dots} gap={12} size={1} />
|
||||||
|
<Controls />
|
||||||
|
<MiniMap
|
||||||
|
nodeColor={(node: any) => {
|
||||||
|
const status = node.data?.status || 'NOT_STARTED';
|
||||||
|
const colors = nodeStatusColorMap[status] || nodeStatusColorMap.NOT_STARTED;
|
||||||
|
return colors.border;
|
||||||
|
}}
|
||||||
|
className="bg-background border"
|
||||||
|
/>
|
||||||
|
</ReactFlow>
|
||||||
|
</ReactFlowProvider>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center h-full">
|
||||||
|
<p className="text-sm text-muted-foreground">暂无流程图数据</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user