From c49f345a58066ee0f24db51ac2d7c1709f80ac6c Mon Sep 17 00:00:00 2001 From: dengqichen Date: Tue, 4 Nov 2025 22:37:01 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E9=83=A8=E7=BD=B2=E6=97=A5?= =?UTF-8?q?=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../docs/deploy-flow-graph-frontend-guide.md | 416 +++++++++ backend/docs/deploy-record-status-analysis.md | 82 ++ .../deploy-record-status-rejected-analysis.md | 202 +++++ .../workflow-listener-executor-lifecycle.md | 805 ++++++++++++++++++ .../deploy/api/DeployApiController.java | 19 + .../deploy/dto/DeployRecordFlowGraphDTO.java | 41 + .../deploy/service/IDeployRecordService.java | 10 + .../service/impl/DeployRecordServiceImpl.java | 142 +++ .../workflow/dto/WorkflowNodeInstanceDTO.java | 8 + .../WorkflowValidationException.java | 12 - .../ApprovalCreateTaskListener.java} | 10 +- ...java => ApprovalEndExecutionListener.java} | 127 ++- .../BaseTaskListener.java | 22 +- ...ava => GatewayStartExecutionListener.java} | 6 +- ... GlobalNodeStartEndExecutionListener.java} | 9 +- .../WorkflowInstanceStatusChangeListener.java | 2 +- ...kflowNodeInstanceStatusChangeListener.java | 9 +- .../service/impl/ApprovalTaskServiceImpl.java | 8 +- .../backend/workflow/util/BpmnConverter.java | 36 +- .../components/DeployFlowGraphModal.tsx | 333 ++++++++ 20 files changed, 2200 insertions(+), 99 deletions(-) create mode 100644 backend/docs/deploy-flow-graph-frontend-guide.md create mode 100644 backend/docs/deploy-record-status-analysis.md create mode 100644 backend/docs/deploy-record-status-rejected-analysis.md create mode 100644 backend/docs/workflow-listener-executor-lifecycle.md create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/DeployRecordFlowGraphDTO.java delete mode 100644 backend/src/main/java/com/qqchen/deploy/backend/workflow/exception/WorkflowValidationException.java rename backend/src/main/java/com/qqchen/deploy/backend/workflow/{delegate/ApprovalTaskListener.java => listener/ApprovalCreateTaskListener.java} (97%) rename backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/{flowable/execution/ApprovalExecutionListener.java => ApprovalEndExecutionListener.java} (75%) rename backend/src/main/java/com/qqchen/deploy/backend/workflow/{delegate => listener}/BaseTaskListener.java (94%) rename backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/{flowable/execution/GatewayExecutionListener.java => GatewayStartExecutionListener.java} (90%) rename backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/{flowable/execution/GlobalNodeExecutionListener.java => GlobalNodeStartEndExecutionListener.java} (95%) rename backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/{business => }/WorkflowInstanceStatusChangeListener.java (94%) rename backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/{business => }/WorkflowNodeInstanceStatusChangeListener.java (73%) create mode 100644 frontend/src/pages/Dashboard/components/DeployFlowGraphModal.tsx diff --git a/backend/docs/deploy-flow-graph-frontend-guide.md b/backend/docs/deploy-flow-graph-frontend-guide.md new file mode 100644 index 00000000..9c529e03 --- /dev/null +++ b/backend/docs/deploy-flow-graph-frontend-guide.md @@ -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 | 节点执行状态列表 | + +### WorkflowDefinitionGraph + +| 字段 | 类型 | 说明 | +|------|------|------| +| `nodes` | List | 节点列表(包含位置信息) | +| `edges` | List | 边列表(连接关系) | + +### WorkflowDefinitionGraphNode + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | String | 节点ID(用于匹配nodeInstances) | +| `nodeCode` | String | 节点代码 | +| `nodeType` | String | 节点类型(START/END/APPROVAL/SHELL等) | +| `nodeName` | String | 节点名称 | +| `position` | Map | 节点位置信息(x, y坐标) | +| `configs` | Map | 节点配置信息 | +| `inputMapping` | Map | 输入映射 | +| `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) => { + 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 = ({ deployRecordId }) => { + const [nodes, setNodes] = useState([]); + const [edges, setEdges] = useState([]); + const [deployStatus, setDeployStatus] = useState(''); + + 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: ( +
+
{node.nodeName}
+
+ {getStatusText(status)} +
+
+ ), + 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 ( +
+
+

部署状态:{getDeployStatusText(deployStatus)}

+
+ +
+ ); +}; +``` + +## 注意事项 + +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**:部分成功(工作流完成但存在失败的节点) + diff --git a/backend/docs/deploy-record-status-analysis.md b/backend/docs/deploy-record-status-analysis.md new file mode 100644 index 00000000..06a196c8 --- /dev/null +++ b/backend/docs/deploy-record-status-analysis.md @@ -0,0 +1,82 @@ +# 部署记录状态枚举分析 + +## 状态使用情况统计 + +### ✅ 需要保留的状态 + +| 状态 | 使用场景 | 来源 | 是否终态 | 备注 | +|------|---------|------|---------|------| +| **CREATED** | 1. 创建记录时初始状态
2. 工作流状态转换
3. 统计查询(30分钟内视为运行中)
4. isDeploying 判断 | 工作流状态 CREATED | ❌ | 初始状态,30分钟内视为正在部署 | +| **PENDING_APPROVAL** | 1. 审批任务创建时设置 | 审批事件 | ❌ | 审批阶段专用状态 | +| **RUNNING** | 1. 审批通过时设置
2. 工作流状态转换
3. 统计查询
4. isDeploying 判断 | 工作流状态 RUNNING
审批通过事件 | ❌ | 运行中状态 | +| **SUCCESS** | 1. 工作流状态转换(COMPLETED)
2. 统计查询(成功计数) | 工作流状态 COMPLETED | ✅ | 部署成功 | +| **FAILED** | 1. 工作流状态转换
2. 统计查询(失败计数) | 工作流状态 FAILED | ✅ | 部署失败 | +| **PARTIAL_SUCCESS** | 1. 工作流状态转换(COMPLETED_WITH_ERRORS)
2. 统计查询(失败计数) | 工作流状态 COMPLETED_WITH_ERRORS | ✅ | 部分成功(存在失败节点) | +| **CANCELLED** | 1. 审批被拒绝时设置
2. 统计查询(失败计数) | 审批拒绝事件 | ✅ | 已取消(审批被拒) | +| **TERMINATED** | 1. 工作流状态转换
2. 统计查询(失败计数) | 工作流状态 TERMINATED | ✅ | 已终止(手动终止) | + +### ❓ 可能不需要的状态 + +| 状态 | 使用场景 | 来源 | 是否终态 | 问题分析 | +|------|---------|------|---------|---------| +| **SUSPENDED** | 1. 工作流状态转换(但实际未使用) | 工作流状态 SUSPENDED | ❌ | **问题**:
1. ❌ 未在 `isFinalState()` 中(不是终态)
2. ❌ 未在统计查询中使用
3. ❌ 未在 `isDeploying` 判断中使用
4. ❌ 部署场景中可能不需要暂停功能
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. 简化状态模型,减少维护成本 + +如果未来需要暂停功能,可以再添加回来。 + diff --git a/backend/docs/deploy-record-status-rejected-analysis.md b/backend/docs/deploy-record-status-rejected-analysis.md new file mode 100644 index 00000000..06a68668 --- /dev/null +++ b/backend/docs/deploy-record-status-rejected-analysis.md @@ -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` 状态**,用于未来可能的手动取消部署功能。 + +这样既保证了语义清晰,又保留了扩展性。 + diff --git a/backend/docs/workflow-listener-executor-lifecycle.md b/backend/docs/workflow-listener-executor-lifecycle.md new file mode 100644 index 00000000..fc5a4d72 --- /dev/null +++ b/backend/docs/workflow-listener-executor-lifecycle.md @@ -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(执行监听器)**: +- 监听节点执行的开始和结束 +- 配置方式:`` +- 触发时机:节点执行时自动触发 + +**TaskListener(任务监听器)**: +- 监听任务的生命周期(create、assignment、complete、delete) +- 配置方式:`` +- 触发时机:任务创建/分配/完成时自动触发 + +**JavaDelegate(任务委派)**: +- 执行具体的业务逻辑 +- 配置方式:`` +- 触发时机: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 中配置 `` +- **配置位置**:所有非网关节点(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 中配置 ``(仅审批节点) +- **配置位置**:审批节点(UserTask)的 end 事件,在 `globalNodeStartEndExecutionListener` 之前执行 +- **职责**:专门处理审批节点的执行结束事件,丰富审批输出(审批用时、节点执行状态等) +- **位置**:`workflow.listener.ApprovalEndExecutionListener` +- **触发时机**:审批节点执行结束(`EVENTNAME_END`) +- **状态流转**: + - 读取已有的 `NodeContext`(包含临时 outputs) + - 丰富 `outputs`:设置 `approvalDuration`、`status=SUCCESS`(无论审批结果如何) + - 保存 `NodeContext` 回流程变量 +- **输出事件**:`ApprovalCompletedEvent`(包含审批结果、审批人等信息) +- **为什么在 XML 中配置**:需要在审批节点结束时自动执行,必须在 XML 中配置才能被 Flowable 引擎调用 + +#### GatewayStartExecutionListener +- **配置方式**:XML 中配置 ``(仅网关节点) +- **配置位置**:网关节点(ExclusiveGateway、ParallelGateway、InclusiveGateway) +- **职责**:监听网关节点执行(start 事件),发布网关节点状态变化事件 +- **位置**:`workflow.listener.GatewayStartExecutionListener` +- **触发时机**:网关节点开始执行(`EVENTNAME_START`) +- **状态流转**:发布 `WorkflowNodeInstanceStatusChangeEvent(status=COMPLETED)` +- **为什么在 XML 中配置**:网关节点需要单独处理,必须在 XML 中配置 + +### 第四层:任务监听器层(XML 配置) + +监听任务(Task)的生命周期,配置任务属性。这些监听器在 BPMN XML 中配置,Flowable 引擎在任务创建时自动调用。 + +#### ApprovalCreateTaskListener +- **配置方式**:XML 中配置 `` +- **配置位置**:审批节点(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 中配置 `` +- **配置位置**:Jenkins 构建节点(ServiceTask) +- **职责**:执行 Jenkins 构建任务 +- **位置**:`workflow.delegate.JenkinsBuildDelegate` +- **继承**:`BaseNodeDelegate` +- **触发时机**:ServiceTask 执行时 +- **状态流转**: + - 调用 Jenkins API 构建任务 + - 根据构建结果设置 `outputs.status`(SUCCESS/FAILURE) + - 保存 `NodeContext` 到流程变量 +- **为什么在 XML 中配置**:需要在 ServiceTask 执行时自动调用,必须在 XML 中配置 + +#### ShellNodeDelegate +- **配置方式**:XML 中配置 `` +- **配置位置**:Shell 脚本节点(ServiceTask) +- **职责**:执行 Shell 脚本 +- **位置**:`workflow.delegate.ShellNodeDelegate` +- **继承**:`BaseNodeDelegate` +- **为什么在 XML 中配置**:需要在 ServiceTask 执行时自动调用,必须在 XML 中配置 + +#### NotificationNodeDelegate +- **配置方式**:XML 中配置 `` +- **配置位置**:通知节点(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 +**维护者**:开发团队 + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/api/DeployApiController.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/api/DeployApiController.java index 53cc8439..b699ef3c 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/api/DeployApiController.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/api/DeployApiController.java @@ -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 executeDeploy(@Validated @RequestBody DeployRequestDTO request) { return Response.success(deployService.executeDeploy(request)); } + + /** + * 获取部署记录的流程图 + * 用于前端展示工作流的流程图和节点执行状态 + */ + @Operation(summary = "获取部署流程图", description = "获取指定部署记录的工作流流程图数据,包含流程图结构和节点执行状态") + @GetMapping("/records/{deployRecordId}/flow-graph") + @PreAuthorize("isAuthenticated()") + public Response getDeployFlowGraph( + @Parameter(description = "部署记录ID", required = true) @PathVariable Long deployRecordId + ) { + return Response.success(deployRecordService.getDeployFlowGraph(deployRecordId)); + } } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/DeployRecordFlowGraphDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/DeployRecordFlowGraphDTO.java new file mode 100644 index 00000000..8d59a273 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/dto/DeployRecordFlowGraphDTO.java @@ -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 nodeInstances; + +} + diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IDeployRecordService.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IDeployRecordService.java index 21c7e6de..9c638c79 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IDeployRecordService.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IDeployRecordService.java @@ -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); } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/DeployRecordServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/DeployRecordServiceImpl.java index 1870d091..d4825963 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/DeployRecordServiceImpl.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/impl/DeployRecordServiceImpl.java @@ -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 new BusinessException(ResponseCode.NOT_FOUND, new Object[]{"部署记录"})); + + // 2. 查询工作流实例(包含流程图快照) + WorkflowInstance workflowInstance = workflowInstanceRepository.findById(deployRecord.getWorkflowInstanceId()) + .orElseThrow(() -> new BusinessException(ResponseCode.NOT_FOUND, new Object[]{"工作流实例"})); + + // 3. 查询节点实例列表(只查询实际执行过的节点) + List nodeInstances = workflowNodeInstanceRepository.findByWorkflowInstanceId(workflowInstance.getId()); + + // 4. 从BPMN模型中获取流程元素顺序(用于排序) + BpmnModel bpmnModel = repositoryService.getBpmnModel(workflowInstance.getProcessDefinitionId()); + Process process = bpmnModel.getMainProcess(); + List flowElements = FlowableUtils.sortFlowElements(process); + + // 5. 构建节点ID到流程顺序的映射(用于排序) + Map nodeOrderMap = new HashMap<>(); + for (int i = 0; i < flowElements.size(); i++) { + nodeOrderMap.put(flowElements.get(i).getId(), i); + } + + // 6. 按流程顺序排序节点实例(只包含实际执行过的节点) + List 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> nodeOutputsMap = getNodeOutputsFromHistory(workflowInstance.getProcessInstanceId()); + + // 8. 转换为 DTO 并填充 outputs 数据 + List nodeInstanceDTOs = workflowNodeInstanceConverter.toDtoList(orderedNodeInstances); + nodeInstanceDTOs.forEach(dto -> { + // 从流程变量中获取该节点的 outputs 数据 + Map nodeOutputs = nodeOutputsMap.get(dto.getNodeId()); + if (nodeOutputs != null) { + // 提取 outputs 部分(格式:{nodeId: {outputs: {...}}} + @SuppressWarnings("unchecked") + Map nodeData = (Map) nodeOutputs; + @SuppressWarnings("unchecked") + Map outputs = (Map) 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> getNodeOutputsFromHistory(String processInstanceId) { + Map> nodeOutputsMap = new HashMap<>(); + + try { + // 查询历史流程变量 + List 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 nodeData = (Map) 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; + } + /** * 判断是否为终态 */ diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/WorkflowNodeInstanceDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/WorkflowNodeInstanceDTO.java index 65ab1af4..4485926c 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/WorkflowNodeInstanceDTO.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/dto/WorkflowNodeInstanceDTO.java @@ -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 包含 approvalResult、approver、approvalTime 等 + */ + private Map outputs; } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/exception/WorkflowValidationException.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/exception/WorkflowValidationException.java deleted file mode 100644 index 3bd31c51..00000000 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/exception/WorkflowValidationException.java +++ /dev/null @@ -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); - } -} diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/delegate/ApprovalTaskListener.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/ApprovalCreateTaskListener.java similarity index 97% rename from backend/src/main/java/com/qqchen/deploy/backend/workflow/delegate/ApprovalTaskListener.java rename to backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/ApprovalCreateTaskListener.java index a1aeee9e..edf77409 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/delegate/ApprovalTaskListener.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/ApprovalCreateTaskListener.java @@ -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 { +@Component("approvalCreateTaskListener") +public class ApprovalCreateTaskListener extends BaseTaskListener { @Resource private ApplicationEventPublisher eventPublisher; diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/flowable/execution/ApprovalExecutionListener.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/ApprovalEndExecutionListener.java similarity index 75% rename from backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/flowable/execution/ApprovalExecutionListener.java rename to backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/ApprovalEndExecutionListener.java index 738efd0e..0b000e97 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/flowable/execution/ApprovalExecutionListener.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/ApprovalEndExecutionListener.java @@ -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 nodeContext = + readNodeContext(execution, nodeId); + if (nodeContext == null) { return; } - @SuppressWarnings("unchecked") - Map nodeDataMap = (Map) nodeDataObj; - NodeContext 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 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 nodeDataMap = (Map) 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 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 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(弃用,保留以防万一) diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/delegate/BaseTaskListener.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/BaseTaskListener.java similarity index 94% rename from backend/src/main/java/com/qqchen/deploy/backend/workflow/delegate/BaseTaskListener.java rename to backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/BaseTaskListener.java index a0455e4c..de4f4808 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/delegate/BaseTaskListener.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/BaseTaskListener.java @@ -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 输入映射类型 (InputMapping) * @param 输出类型 (Outputs) - 用于类型标识 - * * @author qqchen * @since 2025-10-23 */ @@ -32,7 +31,9 @@ public abstract class BaseTaskListener implements TaskListener { // Flowable 自动注入的字段 protected Expression nodeId; + protected Expression configs; + protected Expression inputMapping; private Class inputMappingClass; @@ -72,10 +73,10 @@ public abstract class BaseTaskListener 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 implements TaskListener { try { String inputMappingJson = getFieldValue(inputMapping, task); Class 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 implements TaskListener { return new HashMap<>(); } try { - return objectMapper.readValue(jsonStr, new TypeReference>() {}); + return objectMapper.readValue(jsonStr, new TypeReference<>() { + }); } catch (Exception e) { log.error("Failed to parse JSON field: {}", jsonStr, e); return new HashMap<>(); diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/flowable/execution/GatewayExecutionListener.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/GatewayStartExecutionListener.java similarity index 90% rename from backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/flowable/execution/GatewayExecutionListener.java rename to backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/GatewayStartExecutionListener.java index 0b641958..31571fab 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/flowable/execution/GatewayExecutionListener.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/GatewayStartExecutionListener.java @@ -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; diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/flowable/execution/GlobalNodeExecutionListener.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/GlobalNodeStartEndExecutionListener.java similarity index 95% rename from backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/flowable/execution/GlobalNodeExecutionListener.java rename to backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/GlobalNodeStartEndExecutionListener.java index 732f7e5d..272a93e9 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/flowable/execution/GlobalNodeExecutionListener.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/GlobalNodeStartEndExecutionListener.java @@ -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 的节点类型(事件/网关节点) diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/business/WorkflowInstanceStatusChangeListener.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/WorkflowInstanceStatusChangeListener.java similarity index 94% rename from backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/business/WorkflowInstanceStatusChangeListener.java rename to backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/WorkflowInstanceStatusChangeListener.java index 9aeca8e6..7c273719 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/business/WorkflowInstanceStatusChangeListener.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/WorkflowInstanceStatusChangeListener.java @@ -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; diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/business/WorkflowNodeInstanceStatusChangeListener.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/WorkflowNodeInstanceStatusChangeListener.java similarity index 73% rename from backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/business/WorkflowNodeInstanceStatusChangeListener.java rename to backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/WorkflowNodeInstanceStatusChangeListener.java index 7b7c4672..bf64d3f6 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/business/WorkflowNodeInstanceStatusChangeListener.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/listener/WorkflowNodeInstanceStatusChangeListener.java @@ -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); } } diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/ApprovalTaskServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/ApprovalTaskServiceImpl.java index 498cdb36..76752d98 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/ApprovalTaskServiceImpl.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/service/impl/ApprovalTaskServiceImpl.java @@ -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()); - // 暂时设置为 outputs(ApprovalExecutionListener 会完善) + // 暂时设置为 outputs(ApprovalEndExecutionListener 会完善) 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 会: // - 读取上面设置的变量 // - 自动装配其他上下文信息(approvalDuration、allApprovers 等) // - 构建完整的 ApprovalOutputs 对象 diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/util/BpmnConverter.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/util/BpmnConverter.java index 7d965636..085e56ef 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/util/BpmnConverter.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/util/BpmnConverter.java @@ -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 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> 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 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"); } /** diff --git a/frontend/src/pages/Dashboard/components/DeployFlowGraphModal.tsx b/frontend/src/pages/Dashboard/components/DeployFlowGraphModal.tsx new file mode 100644 index 00000000..94e53f7b --- /dev/null +++ b/frontend/src/pages/Dashboard/components/DeployFlowGraphModal.tsx @@ -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 = { + 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 = ({ 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 ( +
+ {/* 输入连接点 */} + {nodeType !== 'START_EVENT' && ( + + )} + + {/* 节点内容 - 简化版 */} +
+
+
{data.nodeName}
+
{getStatusText(status)}
+ {isRunning && } +
+
+ + {/* 输出连接点 */} + {nodeType !== 'END_EVENT' && ( + + )} +
+ ); +}; + +const nodeTypes = { + default: CustomFlowNode, +}; + +/** + * 部署流程图模态框 + */ +export const DeployFlowGraphModal: React.FC = ({ + open, + deployRecordId, + onOpenChange, +}) => { + const [loading, setLoading] = useState(false); + const [flowData, setFlowData] = useState(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(); + const map = new Map(); + 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 ( + + + + + 部署流程图 + {deployStatusInfo && ( + + + {deployStatusInfo.text} + + )} + {flowData && ( + + 记录ID: #{flowData.deployRecordId} + + )} + + + +
+ {loading ? ( +
+
+ +

加载流程图数据...

+
+
+ ) : flowData ? ( + + + + + { + const status = node.data?.status || 'NOT_STARTED'; + const colors = nodeStatusColorMap[status] || nodeStatusColorMap.NOT_STARTED; + return colors.border; + }} + className="bg-background border" + /> + + + ) : ( +
+

暂无流程图数据

+
+ )} +
+
+
+ ); +}; +