增加部署日志

This commit is contained in:
dengqichen 2025-11-04 22:37:01 +08:00
parent 4b554a8a2a
commit c49f345a58
20 changed files with 2200 additions and 99 deletions

View 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**:部分成功(工作流完成但存在失败的节点)

View 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. 简化状态模型,减少维护成本
如果未来需要暂停功能,可以再添加回来。

View 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` 状态**,用于未来可能的手动取消部署功能。
这样既保证了语义清晰,又保留了扩展性。

View 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()` 方法进行具体配置
### 第五层节点执行器层DelegateXML 配置)
执行具体业务逻辑的节点。这些执行器在 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 解析)
- 初始化 NodeContextconfigs + 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
**维护者**:开发团队

View File

@ -1,11 +1,14 @@
package com.qqchen.deploy.backend.deploy.api;
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.UserDeployableDTO;
import com.qqchen.deploy.backend.deploy.service.IDeployRecordService;
import com.qqchen.deploy.backend.deploy.service.IDeployService;
import com.qqchen.deploy.backend.framework.api.Response;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
@ -29,6 +32,9 @@ public class DeployApiController {
@Resource
private IDeployService deployService;
@Resource
private IDeployRecordService deployRecordService;
/**
* 获取当前用户可部署的环境和应用
*/
@ -48,5 +54,18 @@ public class DeployApiController {
public Response<DeployResultDTO> executeDeploy(@Validated @RequestBody DeployRequestDTO 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));
}
}

View File

@ -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 = "流程实例IDFlowable")
private String processInstanceId;
@Schema(description = "部署状态")
private DeployRecordStatusEnums deployStatus;
@Schema(description = "流程图数据(画布快照,包含节点和边的位置信息)")
private WorkflowDefinitionGraph graph;
@Schema(description = "节点执行状态列表(用于标记每个节点的执行状态)")
private List<WorkflowNodeInstanceDTO> nodeInstances;
}

View File

@ -1,6 +1,7 @@
package com.qqchen.deploy.backend.deploy.service;
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.workflow.entity.WorkflowInstance;
import com.qqchen.deploy.backend.workflow.enums.WorkflowInstanceStatusEnums;
@ -67,5 +68,14 @@ public interface IDeployRecordService {
* @param approved 是否通过审批
*/
void updateStatusFromApproval(Long workflowInstanceId, boolean approved);
/**
* 获取部署记录的流程图数据
* 包含流程图结构graph和节点执行状态nodeInstances
*
* @param deployRecordId 部署记录ID
* @return 部署记录流程图DTO
*/
DeployRecordFlowGraphDTO getDeployFlowGraph(Long deployRecordId);
}

View File

@ -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.dto.DeployRecordDTO;
import com.qqchen.deploy.backend.deploy.dto.DeployRecordFlowGraphDTO;
import com.qqchen.deploy.backend.deploy.entity.DeployRecord;
import com.qqchen.deploy.backend.deploy.enums.DeployRecordStatusEnums;
import com.qqchen.deploy.backend.deploy.query.DeployRecordQuery;
import com.qqchen.deploy.backend.deploy.repository.IDeployRecordRepository;
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.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.WorkflowNodeInstance;
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 lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
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
private DeployRecordConverter deployRecordConverter;
@Resource
private IWorkflowInstanceRepository workflowInstanceRepository;
@Resource
private IWorkflowNodeInstanceRepository workflowNodeInstanceRepository;
@Resource
private WorkflowNodeInstanceConverter workflowNodeInstanceConverter;
@Resource
private RepositoryService repositoryService;
@Resource
private HistoryService historyService;
@Override
@Transactional
public DeployRecordDTO createDeployRecord(
@ -181,6 +216,113 @@ public class DeployRecordServiceImpl extends BaseServiceImpl<DeployRecord, Deplo
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;
}
/**
* 判断是否为终态
*/

View File

@ -5,6 +5,7 @@ import com.qqchen.deploy.backend.workflow.enums.WorkflowNodeInstanceStatusEnums;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.Map;
@Data
public class WorkflowNodeInstanceDTO extends BaseDTO {
@ -30,4 +31,11 @@ public class WorkflowNodeInstanceDTO extends BaseDTO {
private LocalDateTime createTime;
private LocalDateTime updateTime;
/**
* 节点执行结果outputs
* 从流程变量中获取格式{nodeId: {outputs: {...}}}
* 例如审批节点的 outputs 包含 approvalResultapproverapprovalTime
*/
private Map<String, Object> outputs;
}

View File

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

View File

@ -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.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.IWorkflowCategoryRepository;
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.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
@ -21,15 +19,15 @@ import java.util.List;
import java.util.Map;
/**
* 审批任务监听器
* 审批任务创建监听器
* UserTask 创建时被调用负责配置审批人和任务基本信息
*
* @author qqchen
* @since 2025-10-23
*/
@Slf4j
@Component("approvalTaskListener")
public class ApprovalTaskListener extends BaseTaskListener<ApprovalInputMapping, ApprovalOutputs> {
@Component("approvalCreateTaskListener")
public class ApprovalCreateTaskListener extends BaseTaskListener<ApprovalInputMapping, ApprovalOutputs> {
@Resource
private ApplicationEventPublisher eventPublisher;

View File

@ -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.qqchen.deploy.backend.workflow.dto.event.ApprovalCompletedEvent;
import com.qqchen.deploy.backend.workflow.dto.inputmapping.ApprovalInputMapping;
@ -29,15 +28,15 @@ import java.util.Map;
/**
* 审批任务结束监听器
* 审批节点结束监听器
* UserTask 结束时自动装配上下文信息并构建 ApprovalOutputs
*
* @author qqchen
* @since 2025-10-23
*/
@Slf4j
@Component("approvalExecutionListener")
public class ApprovalExecutionListener implements ExecutionListener {
@Component("approvalEndExecutionListener")
public class ApprovalEndExecutionListener implements ExecutionListener {
@Resource
private ObjectMapper objectMapper;
@ -63,45 +62,120 @@ public class ApprovalExecutionListener implements ExecutionListener {
try {
log.info("ApprovalExecutionListener: Building outputs for node: {}", nodeId);
// 1. 读取现有的 NodeContext
Object nodeDataObj = execution.getVariable(nodeId);
if (!(nodeDataObj instanceof Map)) {
log.warn("NodeContext not found for node: {}, skipping ApprovalExecutionListener", nodeId);
// 1. 读取 NodeContext统一使用 NodeContext BaseNodeDelegate 保持一致
NodeContext<ApprovalInputMapping, ApprovalOutputs> nodeContext =
readNodeContext(execution, nodeId);
if (nodeContext == null) {
return;
}
@SuppressWarnings("unchecked")
Map<String, Object> nodeDataMap = (Map<String, Object>) nodeDataObj;
NodeContext<ApprovalInputMapping, ApprovalOutputs> nodeContext =
NodeContext.fromMap(nodeDataMap, ApprovalInputMapping.class, ApprovalOutputs.class, objectMapper);
// 2. 检查是否已经有临时的 outputs ApprovalTaskServiceImpl 设置
// 2. 检查并获取 outputs
ApprovalOutputs outputs = nodeContext.getOutputs();
if (outputs == null) {
log.warn("Outputs not found in NodeContext for node: {}, skipping", nodeId);
return;
}
// 3. 自动装配上下文信息计算审批用时历史任务ID等
// 3. 自动装配上下文信息丰富 outputs
enrichApprovalOutputs(execution, nodeId, outputs);
// 4. 更新 NodeContext outputs
// 4. 更新并保存 NodeContext BaseNodeDelegate 保持一致
nodeContext.setOutputs(outputs);
// 5. 保存回流程变量
execution.setVariable(nodeId, nodeContext.toMap(objectMapper));
saveNodeContext(execution, nodeId, nodeContext);
log.info("Stored approval outputs for node: {}, result: {}", nodeId, outputs.getApprovalResult());
// 6. 发布审批完成事件
// 5. 发布审批完成事件
publishApprovalCompletedEvent(execution, nodeId, outputs);
} catch (Exception 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);
}
}
/**
* 读取 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弃用保留以防万一

View File

@ -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.databind.ObjectMapper;
@ -17,10 +17,9 @@ import java.util.Map;
/**
* TaskListener 基类
* 用于 UserTask 的任务创建阶段统一处理输入映射解析和任务配置
*
*
* @param <I> 输入映射类型 (InputMapping)
* @param <O> 输出类型 (Outputs) - 用于类型标识
*
* @author qqchen
* @since 2025-10-23
*/
@ -32,7 +31,9 @@ public abstract class BaseTaskListener<I, O> implements TaskListener {
// Flowable 自动注入的字段
protected Expression nodeId;
protected Expression configs;
protected Expression inputMapping;
private Class<I> inputMappingClass;
@ -72,10 +73,10 @@ public abstract class BaseTaskListener<I, O> implements TaskListener {
/**
* 配置任务子类实现
*
* @param delegateTask Flowable 任务对象
* @param configs 节点配置
* @param inputMapping 输入映射强类型
*
* @param delegateTask Flowable 任务对象
* @param configs 节点配置
* @param inputMapping 输入映射强类型
*/
protected abstract void configureTask(
DelegateTask delegateTask,
@ -91,13 +92,13 @@ public abstract class BaseTaskListener<I, O> implements TaskListener {
try {
String inputMappingJson = getFieldValue(inputMapping, task);
Class<I> inputClass = getInputMappingClass();
if (inputMappingJson == null || inputMappingJson.isEmpty()) {
return inputClass.getDeclaredConstructor().newInstance();
}
return objectMapper.readValue(inputMappingJson, inputClass);
} catch (Exception e) {
log.error("Failed to parse input mapping", e);
throw new RuntimeException("Failed to parse input mapping", e);
@ -131,7 +132,8 @@ public abstract class BaseTaskListener<I, O> implements TaskListener {
return new HashMap<>();
}
try {
return objectMapper.readValue(jsonStr, new TypeReference<Map<String, Object>>() {});
return objectMapper.readValue(jsonStr, new TypeReference<>() {
});
} catch (Exception e) {
log.error("Failed to parse JSON field: {}", jsonStr, e);
return new HashMap<>();

View File

@ -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.dto.event.WorkflowNodeInstanceStatusChangeEvent;
@ -13,8 +13,8 @@ import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Slf4j
@Component("gatewayExecutionListener")
public class GatewayExecutionListener implements ExecutionListener {
@Component("gatewayStartExecutionListener")
public class GatewayStartExecutionListener implements ExecutionListener {
@Resource
private ApplicationEventPublisher eventPublisher;

View File

@ -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.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.WorkflowNodeInstanceStatusEnums;
import com.qqchen.deploy.backend.workflow.dto.event.WorkflowNodeInstanceStatusChangeEvent;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.flowable.bpmn.model.FlowElement;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.delegate.ExecutionListener;
@ -22,8 +19,8 @@ import java.time.LocalDateTime;
import java.util.*;
@Slf4j
@Component("globalNodeExecutionListener")
public class GlobalNodeExecutionListener implements ExecutionListener {
@Component("globalNodeStartEndExecutionListener")
public class GlobalNodeStartEndExecutionListener implements ExecutionListener {
/**
* 不产生 outputs 的节点类型事件/网关节点

View File

@ -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.service.IWorkflowInstanceService;

View File

@ -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.service.IWorkflowNodeInstanceService;
@ -16,15 +16,10 @@ public class WorkflowNodeInstanceStatusChangeListener {
@Resource
private IWorkflowNodeInstanceService workflowNodeInstanceService;
/**
* 修改事务传播级别为 REQUIRED加入到 Flowable 的事务中
* 这样节点实例会在同一个事务内保存JavaDelegate 执行时就能立即查询到
*/
@EventListener
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void handleWorkflowStatusChange(WorkflowNodeInstanceStatusChangeEvent event) {
log.debug("Handling workflow node instance status change event: nodeId={}, status={}",
event.getNodeId(), event.getStatus());
log.debug("Handling workflow node instance status change event: nodeId={}, status={}", event.getNodeId(), event.getStatus());
workflowNodeInstanceService.saveOrUpdateWorkflowNodeInstance(event);
}
}

View File

@ -114,14 +114,14 @@ public class ApprovalTaskServiceImpl implements IApprovalTaskService {
nodeContext = new NodeContext<>();
}
// 创建临时审批数据稍后由 ApprovalExecutionListener 完善
// 创建临时审批数据稍后由 ApprovalEndExecutionListener 完善
ApprovalOutputs tempOutputs = new ApprovalOutputs();
tempOutputs.setApprovalResult(request.getResult());
tempOutputs.setApprover(task.getAssignee());
tempOutputs.setApprovalTime(LocalDateTime.now());
tempOutputs.setApprovalComment(request.getComment());
// 暂时设置为 outputsApprovalExecutionListener 会完善
// 暂时设置为 outputsApprovalEndExecutionListener 会完善
nodeContext.setOutputs(tempOutputs);
// 保存回流程变量
@ -132,8 +132,8 @@ public class ApprovalTaskServiceImpl implements IApprovalTaskService {
taskService.addComment(request.getTaskId(), task.getProcessInstanceId(), request.getComment());
}
// 6. 完成任务触发 ApprovalExecutionListener
// ApprovalExecutionListener
// 6. 完成任务触发 ApprovalEndExecutionListener
// ApprovalEndExecutionListener
// - 读取上面设置的变量
// - 自动装配其他上下文信息approvalDurationallApprovers
// - 构建完整的 ApprovalOutputs 对象

View File

@ -224,11 +224,11 @@ public class BpmnConverter {
if (element instanceof Gateway) {
// 网关节点只添加 start 监听器
executionListeners.add(createExecutionListener("start", "${gatewayExecutionListener}"));
executionListeners.add(createExecutionListener("start", "${gatewayStartExecutionListener}"));
} else {
// 其他节点添加 start end 监听器
executionListeners.add(createExecutionListener("start", "${globalNodeExecutionListener}"));
executionListeners.add(createExecutionListener("end", "${globalNodeExecutionListener}"));
executionListeners.add(createExecutionListener("start", "${globalNodeStartEndExecutionListener}"));
executionListeners.add(createExecutionListener("end", "${globalNodeStartEndExecutionListener}"));
}
extensionElements.put("executionListener", executionListeners);
@ -245,11 +245,11 @@ public class BpmnConverter {
List<ExtensionElement> executionListeners = new ArrayList<>();
// 添加开始事件监听器
ExtensionElement startListener = createExecutionListener("start", "${globalNodeExecutionListener}");
ExtensionElement startListener = createExecutionListener("start", "${globalNodeStartEndExecutionListener}");
executionListeners.add(startListener);
// 添加结束事件监听器
ExtensionElement endListener = createExecutionListener("end", "${globalNodeExecutionListener}");
ExtensionElement endListener = createExecutionListener("end", "${globalNodeStartEndExecutionListener}");
executionListeners.add(endListener);
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) {
try {
// 1. 创建 TaskListener在任务创建时调用 ApprovalTaskListener
// 1. 创建 TaskListener在任务创建时调用 ApprovalCreateTaskListener
ExtensionElement taskListener = new ExtensionElement();
taskListener.setName("taskListener");
taskListener.setNamespace("http://flowable.org/bpmn");
taskListener.setNamespacePrefix("flowable");
taskListener.addAttribute(createAttribute("event", "create"));
taskListener.addAttribute(createAttribute("delegateExpression", "${approvalTaskListener}"));
taskListener.addAttribute(createAttribute("delegateExpression", "${approvalCreateTaskListener}"));
// 2. field 字段作为 TaskListener 的子元素添加而不是 UserTask 的子元素
// 这样 ApprovalTaskListener 才能通过 @field 注解注入这些字段
// 这样 ApprovalCreateTaskListener 才能通过 @field 注解注入这些字段
addFieldsToTaskListener(taskListener, node, validId);
// 3. TaskListener 添加到扩展元素
extensionElements.computeIfAbsent("taskListener", k -> new ArrayList<>()).add(taskListener);
// 4. 添加 ApprovalExecutionListener在任务结束时构建 ApprovalOutputs
// 确保它在 globalNodeExecutionListener 之前执行这样状态变量才能正确设置
// 4. 添加 ApprovalEndExecutionListener在任务结束时构建 ApprovalOutputs
// 确保它在 globalNodeStartEndExecutionListener 之前执行这样状态变量才能正确设置
addApprovalExecutionListener(extensionElements);
// 5. 设置扩展元素
@ -343,8 +343,8 @@ public class BpmnConverter {
}
/**
* 添加 ApprovalExecutionListener审批任务结束监听器
* 确保它在 globalNodeExecutionListener 之前执行
* 添加 ApprovalEndExecutionListener审批节点结束监听器
* 确保它在 globalNodeStartEndExecutionListener 之前执行
*
* @param extensionElements 扩展元素
*/
@ -352,31 +352,31 @@ public class BpmnConverter {
List<ExtensionElement> executionListeners =
extensionElements.computeIfAbsent("executionListener", k -> new ArrayList<>());
// 找到 end 事件的 globalNodeExecutionListener 的位置
// 找到 end 事件的 globalNodeStartEndExecutionListener 的位置
int endListenerIndex = -1;
for (int i = 0; i < executionListeners.size(); i++) {
ExtensionElement listener = executionListeners.get(i);
String event = listener.getAttributeValue(null, "event");
String delegateExpr = listener.getAttributeValue(null, "delegateExpression");
if ("end".equals(event) && "${globalNodeExecutionListener}".equals(delegateExpr)) {
if ("end".equals(event) && "${globalNodeStartEndExecutionListener}".equals(delegateExpr)) {
endListenerIndex = i;
break;
}
}
// globalNodeExecutionListener 之前插入 approvalExecutionListener
// globalNodeStartEndExecutionListener 之前插入 approvalEndExecutionListener
// 这样审批结果会先被构建状态变量也会被设置
ExtensionElement approvalListener = createExecutionListener("end", "${approvalExecutionListener}");
ExtensionElement approvalListener = createExecutionListener("end", "${approvalEndExecutionListener}");
if (endListenerIndex >= 0) {
executionListeners.add(endListenerIndex, approvalListener);
} else {
// 如果没有找到 globalNodeExecutionListener直接添加到列表末尾
// 如果没有找到 globalNodeStartEndExecutionListener直接添加到列表末尾
executionListeners.add(approvalListener);
}
log.debug("Added ApprovalExecutionListener before GlobalNodeExecutionListener");
log.debug("Added ApprovalEndExecutionListener before GlobalNodeStartEndExecutionListener");
}
/**

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