淘车湾项目实战八_我的代码及流程图高亮节点分析与实现
淘车湾项目实战(八):我的代码及流程图高亮节点分析与实现
本章位置:第三阶段 Java 企业项目 + AI 助手
对应课程:淘车湾项目实战——我的代码及流程图高亮节点分析及实现
前置知识:淘车湾项目实战(一)~(七)、Activiti7、BPMN、HistoryService、TaskService、Vue3、Element Plus
后续衔接:我的申请/我的已办完善、项目总结、Bug 调试、项目优化
学习目标:完成流程图查看和节点高亮功能,掌握 BPMN XML 获取、HistoricActivityInstance、当前运行节点、已完成节点、已走 SequenceFlow、运行中与已结束流程兼容、bpmn-js Viewer、CSS Marker、审批详情流程图以及流程可视化常见问题排查。
一、本章最终要做什么
上一章已经实现:
我的待办
我的已办
claim
unclaim
审批通过
审批拒绝
审批记录
审批时间线
但是用户看到的仍然主要是:
表格 + 文字
这一章要进一步做到:
把 BPMN 流程图显示出来
并且让用户一眼看懂:
哪些节点已经走过
当前走到哪个节点
哪些连线已经经过
流程是否已经结束
最终是通过还是拒绝
二、最终效果
例如结算审批流程:
开始
↓
经理审批
↓
经理网关
↓
财务审批
↓
财务网关
↓
审批通过
如果现在刚到财务审批:
开始 已完成
经理审批 已完成
经理网关 已完成
财务审批 当前节点
财务网关 未执行
审批通过 未执行
同时:
开始 → 经理审批
经理审批 → 经理网关
经理网关 → 财务审批
这些连线:
也应该高亮
三、为什么流程图高亮不是一张后端 PNG
很多旧教程会使用:
Activiti ProcessDiagramGenerator
后端:
生成一张带颜色的流程 PNG
然后前端:
<img>
这种方案可以工作。
但是当前 Vue 项目更推荐:
后端返回 BPMN XML
+
后端返回执行状态
+
前端 bpmn-js Viewer 渲染
+
CSS Marker 高亮
四、为什么前端 bpmn-js 更适合
优点:
清晰
可缩放
矢量图
浏览器直接渲染
可以点击节点
可以加 Overlay
可以高亮
可以复用 BPMN XML
后续扩展方便
五、Viewer 和 Modeler 区别
bpmn-js 有两种非常重要的使用方式:
Viewer
Modeler
六、Viewer
Viewer:
查看流程图
适合本章:
审批详情流程图
用户:
不能拖动修改流程定义
七、Modeler
Modeler:
在线设计和编辑 BPMN
例如:
创建 UserTask
拖动节点
画 SequenceFlow
修改流程
本章:
不需要
八、为什么审批详情必须使用 Viewer
审批详情:
只是查看当前进度
如果使用 Modeler:
用户可能误操作流程图
而且:
加载更多不必要模块
所以:
Viewer 更合适
九、安装 bpmn-js
Vue 项目:
npm install bpmn-js
当前 bpmn-js 官方仍支持:
npm 安装
具体版本:
以项目 package-lock.json
和当前前端兼容情况为准
十、最基本的使用方式
import BpmnViewer
from 'bpmn-js/lib/NavigatedViewer'
创建:
const viewer =
new BpmnViewer({
container:
containerRef.value
})
十一、为什么使用 NavigatedViewer
普通 Viewer:
可以查看
NavigatedViewer:
增加缩放/拖动浏览能力
审批流程图:
体验更好
十二、bpmn-js 核心调用
await viewer.importXML(
bpmnXml
)
然后:
const canvas =
viewer.get(
'canvas'
)
十三、自动适配视口
canvas.zoom(
'fit-viewport'
)
作用:
自动把整张流程图缩放到当前容器
十四、官方 bpmn-js 的核心思想
流程图 XML:
importXML
导入成功后:
通过 viewer.get(...)
访问 Canvas 等服务
十五、一个非常常见的错误
错误:
viewer.importXML(xml)
const canvas =
viewer.get('canvas')
canvas.addMarker(
'managerApprove',
'highlight'
)
如果:
importXML 还没完成
就开始操作节点,
可能报:
element not found
invalid element specified
十六、正确
必须:
await viewer.importXML(
xml
)
const canvas =
viewer.get('canvas')
十七、为什么
importXML:
是异步操作
只有导入完成后:
BPMN 元素才进入 elementRegistry / canvas
十八、本章需要后端返回什么
最少:
bpmnXml
completedActivityIds
currentActivityIds
completedFlowIds
processEnded
十九、推荐返回 VO
public class ProcessDiagramVO {
private String processInstanceId;
private String processDefinitionId;
private String bpmnXml;
private List<String>
completedActivityIds;
private List<String>
currentActivityIds;
private List<String>
completedFlowIds;
private Boolean processEnded;
private String result;
}
二十、为什么不是只返回 completedActivityIds
因为:
节点高亮
和
连线高亮
是两件事。
二十一、Activity
Activity 可以理解:
流程节点
例如:
start
managerApprove
managerGateway
financeApprove
financeGateway
approveEnd
二十二、SequenceFlow
是:
节点之间的连线
例如:
flow1
flow2
managerPass
managerReject
二十三、流程图为什么要给 SequenceFlow 自己写 id
推荐:
<sequenceFlow
id="managerPass"
sourceRef="managerGateway"
targetRef="financeApprove"
/>
不要:
完全依赖设计器生成的随机 ID
二十四、稳定 ID 的好处
后端:
可以识别历史走过哪条线
前端:
可以 canvas.addMarker(flowId)
后续排错:
也更方便
二十五、上一章 BPMN 节点规范
我们已经建议:
start
managerApprove
managerGateway
financeApprove
financeGateway
approveEnd
rejectEnd
这一章:
这些 ID 直接成为高亮依据
二十六、HistoricActivityInstance
Activiti History:
HistoricActivityInstance
代表:
流程中某一个 Activity 的一次执行记录
常见可获取:
activityId
activityName
activityType
startTime
endTime
assignee
二十七、为什么它是高亮核心
因为它告诉我们:
哪些节点已经出现过
二十八、查询历史活动
经典思路:
List<HistoricActivityInstance>
activities =
historyService
.createHistoricActivityInstanceQuery()
.processInstanceId(
processInstanceId
)
.orderByHistoricActivityInstanceStartTime()
.asc()
.list();
具体排序 API:
以当前 Activiti 版本为准
二十九、finished() 和不 finished 的区别
如果只:
.finished()
会拿到:
已经结束的 Activity
三十、当前正在执行的 UserTask
可能:
没有 endTime
所以:
不能只靠 finished()
判断当前节点
三十一、已完成节点
定义:
endTime != null
三十二、当前运行节点
可以从:
TaskService
查询当前 ProcessInstance 的任务。
三十三、为什么当前节点更适合 TaskService
对于人工审批流:
当前真正需要用户处理的节点
就是:
Runtime Task
三十四、查询当前任务
List<Task> tasks =
taskService
.createTaskQuery()
.processInstanceId(
processInstanceId
)
.active()
.list();
三十五、currentActivityIds
List<String> currentActivityIds =
tasks.stream()
.map(
Task::getTaskDefinitionKey
)
.distinct()
.toList();
三十六、为什么可能有多个 currentActivityIds
并行流程:
ParallelGateway
后:
可能同时有多个 UserTask
所以返回:
List
而不是:
String
三十七、即使当前淘车湾是串行审批
也建议:
List<String>
这样接口:
扩展性更好
三十八、completedActivityIds
List<String> completedActivityIds =
activities.stream()
.filter(
a ->
a.getEndTime()
!= null
)
.map(
HistoricActivityInstance
::getActivityId
)
.distinct()
.toList();
三十九、是不是所有 Activity 都要前端高亮
不一定。
History 中可能包含:
startEvent
userTask
exclusiveGateway
endEvent
sequenceFlow
具体:
取决于历史记录和版本
四十、推荐节点高亮类型
startEvent
userTask
exclusiveGateway
parallelGateway
endEvent
四十一、SequenceFlow 怎么高亮
这是本章较难的部分。
方案一:
如果 HistoricActivityInstance
能记录 sequenceFlow
直接:
activityType == sequenceFlow
取:
activityId
四十二、但是不同引擎/History 级别表现可能不同
所以:
不要把代码写死成
历史一定有所有 sequenceFlow
四十三、方案二
读取:
BpmnModel
结合:
已完成节点顺序
推导:
真正走过的 SequenceFlow
四十四、哪种更适合课程
第一版:
优先从 HistoricActivityInstance
取得历史 Flow ID
如果当前版本:
拿不到 SequenceFlow 历史
再:
用 BPMN Model 推导
四十五、不要一开始写很复杂的图算法
当前流程:
经理
→ 网关
→ 财务
→ 网关
→ End
结构简单。
四十六、History Level
如果历史配置太低:
可能拿不到完整 HistoricActivityInstance
所以流程高亮异常时:
先检查 History 配置
四十七、不要直接认为前端 bpmn-js 有 Bug
后端如果:
根本没有返回历史节点
前端当然:
无法高亮
四十八、流程实例可能有两种状态
运行中
已结束
四十九、运行中
可以:
runtimeService
.createProcessInstanceQuery()
查到。
五十、已结束
Runtime:
查不到
必须:
HistoryService
五十一、这是流程图接口必须解决的问题
不能:
ProcessInstance instance =
runtimeService...
然后:
instance == null
直接报流程不存在
五十二、为什么
已办详情中的流程:
很可能已经结束
五十三、正确逻辑
先查 Runtime
↓
有
→ 运行中
没有
↓
查 HistoricProcessInstance
↓
有
→ 已结束
还没有
→ 真不存在
五十四、统一流程上下文 VO
public class ProcessContext {
private String processInstanceId;
private String processDefinitionId;
private String businessKey;
private boolean ended;
}
五十五、resolveProcessContext
private ProcessContext
resolveProcessContext(
String processInstanceId
) {
ProcessInstance running =
runtimeService
.createProcessInstanceQuery()
.processInstanceId(
processInstanceId
)
.singleResult();
if (
running != null
) {
return new ProcessContext(
running.getId(),
running.getProcessDefinitionId(),
running.getBusinessKey(),
false
);
}
HistoricProcessInstance historic =
historyService
.createHistoricProcessInstanceQuery()
.processInstanceId(
processInstanceId
)
.singleResult();
if (
historic == null
) {
throw new ServiceException(
"流程实例不存在"
);
}
return new ProcessContext(
historic.getId(),
historic.getProcessDefinitionId(),
historic.getBusinessKey(),
true
);
}
五十六、为什么这段非常重要
以后:
待办流程图
已办流程图
审批结束流程图
全部:
可以复用
五十七、流程图接口的安全
前端传:
processInstanceId
后端不能:
直接返回 BPMN
五十八、正确
processInstanceId
↓
resolve context
↓
businessKey
↓
settlementId
↓
requireAccessibleSettlement
↓
才返回流程图
五十九、为什么
否则用户可能猜:
其他门店 processInstanceId
查看:
别人的审批进度
六十、完整安全链
@PreAuthorize
↓
processInstanceId
↓
businessKey
↓
Settlement
↓
DataScope
↓
ProcessDiagramVO
六十一、流程图接口
建议:
GET
/car/workflow/process/{processInstanceId}/diagram-data
六十二、权限
可以:
car:workflow:task:query
或单独:
car:workflow:diagram:query
课程推荐:
复用 query
六十三、为什么不要让前端直接查 ACT_RE_BYTEARRAY
还是同一个原则:
不直接让业务层依赖 ACT_* SQL
六十四、BPMN XML 从哪里获取
流程定义:
processDefinitionId
通过:
RepositoryService
读取流程资源。
六十五、先查 ProcessDefinition
ProcessDefinition definition =
repositoryService
.createProcessDefinitionQuery()
.processDefinitionId(
processDefinitionId
)
.singleResult();
六十六、资源名称
definition.getResourceName()
例如:
settlement-approval.bpmn20.xml
六十七、获取资源流
经典思路:
InputStream inputStream =
repositoryService
.getResourceAsStream(
definition
.getDeploymentId(),
definition
.getResourceName()
);
六十八、转 String
private String readText(
InputStream inputStream
)
throws IOException {
return new String(
inputStream.readAllBytes(),
StandardCharsets.UTF_8
);
}
JDK17:
支持 readAllBytes()
六十九、为什么返回 XML 而不是后端文件地址
流程资源:
在 Activiti Repository 中
不一定:
对应服务器某个固定文件
后端:
直接读取并返回 XML
最清晰。
七十、BPMN 必须带 DI 信息
如果 BPMN XML 只有:
节点和连线逻辑
但没有:
BPMNDiagram
BPMNPlane
BPMNShape
BPMNEdge
前端:
可能无法正常显示图形布局
七十一、什么是 BPMN DI
DI:
Diagram Interchange
描述:
节点画在哪
宽高是多少
连线怎么走
七十二、所以课程 BPMN 文件最好由设计器导出
哪怕流程逻辑简单:
也确保带图形坐标
七十三、没有 DI 会发生什么
可能:
bpmn-js importXML 报错
或无法显示完整图形
七十四、流程图后端 Service
@Transactional(
readOnly = true
)
public ProcessDiagramVO
getProcessDiagram(
String processInstanceId
) {
ProcessContext context =
resolveProcessContext(
processInstanceId
);
Long settlementId =
parseSettlementId(
context.getBusinessKey()
);
requireAccessibleSettlement(
settlementId
);
String bpmnXml =
loadBpmnXml(
context
.getProcessDefinitionId()
);
List<HistoricActivityInstance>
activities =
historyService
.createHistoricActivityInstanceQuery()
.processInstanceId(
processInstanceId
)
.orderByHistoricActivityInstanceStartTime()
.asc()
.list();
List<String> completedActivityIds =
resolveCompletedActivityIds(
activities
);
List<String> completedFlowIds =
resolveCompletedFlowIds(
activities
);
List<String> currentActivityIds =
context.isEnded()
? Collections.emptyList()
: resolveCurrentActivityIds(
processInstanceId
);
ProcessDiagramVO vo =
new ProcessDiagramVO();
vo.setProcessInstanceId(
processInstanceId
);
vo.setProcessDefinitionId(
context
.getProcessDefinitionId()
);
vo.setBpmnXml(
bpmnXml
);
vo.setCompletedActivityIds(
completedActivityIds
);
vo.setCurrentActivityIds(
currentActivityIds
);
vo.setCompletedFlowIds(
completedFlowIds
);
vo.setProcessEnded(
context.isEnded()
);
vo.setResult(
resolveBusinessResult(
settlementId
)
);
return vo;
}
七十五、resolveCompletedActivityIds
private List<String>
resolveCompletedActivityIds(
List<HistoricActivityInstance>
activities
) {
return activities.stream()
.filter(
activity ->
activity.getEndTime()
!= null
)
.filter(
activity ->
isNodeType(
activity
.getActivityType()
)
)
.map(
HistoricActivityInstance
::getActivityId
)
.distinct()
.toList();
}
七十六、isNodeType
private boolean isNodeType(
String type
) {
return Set.of(
"startEvent",
"endEvent",
"userTask",
"exclusiveGateway",
"parallelGateway"
).contains(
type
);
}
七十七、为什么不高亮所有历史 Activity
ServiceTask:
可以根据需求
BoundaryEvent:
也可以
当前流程:
只处理常用节点
更容易理解。
七十八、当前节点
private List<String>
resolveCurrentActivityIds(
String processInstanceId
) {
return taskService
.createTaskQuery()
.processInstanceId(
processInstanceId
)
.active()
.list()
.stream()
.map(
Task::getTaskDefinitionKey
)
.distinct()
.toList();
}
七十九、如果当前节点不是 UserTask
例如:
Timer
ServiceTask
ReceiveTask
TaskService:
可能查不到
八十、当前项目为什么问题不大
我们的审批流程:
主要人工 UserTask
八十一、以后复杂流程
可以通过:
RuntimeService.getActiveActivityIds(executionId)
或对应版本能力:
查当前活动
具体 API:
以引擎版本为准
八十二、课程第一版
UserTask 用 TaskService
足够。
八十三、completedFlowIds
方案:
private List<String>
resolveCompletedFlowIds(
List<HistoricActivityInstance>
activities
) {
return activities.stream()
.filter(
activity ->
"sequenceFlow"
.equals(
activity
.getActivityType()
)
)
.map(
HistoricActivityInstance
::getActivityId
)
.distinct()
.toList();
}
八十四、如果拿不到 SequenceFlow
不要立即:
在数据库写死 managerPass
应该:
分析当前 Activiti History 行为
八十五、可以读取 BpmnModel
经典 Activiti API:
BpmnModel model =
repositoryService
.getBpmnModel(
processDefinitionId
);
八十六、BpmnModel 是什么
Activiti:
把 BPMN XML 解析成 Java 对象模型
可以得到:
FlowElement
FlowNode
SequenceFlow
八十七、通过 BpmnModel 找连线
例如:
managerGateway
它有:
outgoingFlows
八十八、但怎么知道到底走哪一条
需要结合:
历史 Activity 顺序
网关后的实际节点
八十九、例如
历史:
managerGateway
↓
financeApprove
BpmnModel:
managerGateway
有两条 outgoing
找到:
targetRef = financeApprove
的 SequenceFlow:
managerPass
九十、简单推导算法
遍历历史有效 Activity
找到相邻两个历史节点
在 source FlowNode.outgoingFlows 中
寻找 targetRef == nextActivityId
把该 flowId 记为 completed
九十一、为什么“相邻历史 Activity”不是永远可靠
并行网关:
可能同时分支
子流程:
可能嵌套
循环:
同一节点执行多次
九十二、当前流程可以使用吗
可以。
当前:
串行 + 排他网关
简单可靠。
九十三、复杂工作流怎么办
需要:
按 executionId
历史顺序
BPMN 图结构
进一步推导。
当前课程:
不展开复杂图算法
九十四、前端组件
推荐:
src/views/car/workflow/components/
ProcessDiagram.vue
九十五、为什么做独立组件
后面:
待办详情
已办详情
结算详情
我的申请
都可能需要:
流程图
九十六、Props
const props =
defineProps({
processInstanceId: {
type: String,
required: true
}
})
九十七、组件结构
<template>
<div
class="process-diagram"
>
<div
ref="canvasRef"
class="bpmn-canvas"
/>
</div>
</template>
九十八、Script
<script setup>
import {
nextTick,
onBeforeUnmount,
ref,
watch
} from 'vue'
import BpmnViewer
from 'bpmn-js/lib/NavigatedViewer'
import {
getProcessDiagramData
} from '@/api/car/workflow'
const props =
defineProps({
processInstanceId: {
type: String,
required: true
}
})
const canvasRef =
ref(null)
let viewer = null
</script>
九十九、为什么 viewer 不一定放 ref
它:
不是需要 Vue 响应式追踪的数据
普通变量:
够用
一百、初始化
function initViewer() {
if (
viewer
) {
return
}
viewer =
new BpmnViewer({
container:
canvasRef.value
})
}
一百零一、加载流程图
async function loadDiagram() {
if (
!props.processInstanceId
) {
return
}
await nextTick()
initViewer()
const res =
await getProcessDiagramData(
props.processInstanceId
)
const data =
res.data
await viewer.importXML(
data.bpmnXml
)
const canvas =
viewer.get(
'canvas'
)
canvas.zoom(
'fit-viewport'
)
applyMarkers(
canvas,
data
)
}
一百零二、为什么 Marker 要在 importXML 之后
因为:
节点还没导入
之前调用:
canvas.addMarker
会失败。
一百零三、applyMarkers
function applyMarkers(
canvas,
data
) {
data.completedActivityIds
?.forEach(
id => {
safeAddMarker(
canvas,
id,
'node-completed'
)
}
)
data.completedFlowIds
?.forEach(
id => {
safeAddMarker(
canvas,
id,
'flow-completed'
)
}
)
data.currentActivityIds
?.forEach(
id => {
safeAddMarker(
canvas,
id,
'node-current'
)
}
)
}
一百零四、为什么 Current 最后加
如果某个节点:
历史里已出现
同时:
当前仍在运行
我们希望:
当前样式优先
一百零五、safeAddMarker
function safeAddMarker(
canvas,
id,
marker
) {
try {
canvas.addMarker(
id,
marker
)
} catch (
error
) {
console.warn(
'流程图元素不存在',
id,
error
)
}
}
一百零六、为什么需要安全处理
历史记录可能:
来自旧版本 ProcessDefinition
如果错误地加载:
最新 BPMN
节点 ID:
可能对不上
一百零七、最重要的版本规则
流程实例:
必须加载它自己的 processDefinitionId
不能:
根据 processDefinitionKey
拿 latestVersion()
一百零八、这是流程图高亮最常见大坑之一
错误:
历史流程是 version 1
现在最新是 version 3
页面加载 version 3 BPMN
然后:
HistoricActivityId
对不上
一百零九、正确
从流程实例/历史实例拿:
processDefinitionId
然后:
加载这个版本 BPMN
一百一十、为什么旧流程仍能正确显示
因为每个实例:
绑定自己的 Definition ID
一百一十一、CSS Marker
bpmn-js 的 Marker 本质上:
给 SVG 元素增加 CSS class
然后:
用 CSS 控制显示
一百一十二、节点已完成样式
示意:
.node-completed
.djs-visual
> :nth-child(1) {
stroke: #67c23a !important;
fill: #f0f9eb !important;
}
一百一十三、当前节点
.node-current
.djs-visual
> :nth-child(1) {
stroke: #409eff !important;
fill: #ecf5ff !important;
}
一百一十四、已走连线
.flow-completed
.djs-visual
> path {
stroke: #67c23a !important;
}
一百一十五、为什么 CSS 选择器要实际检查 DOM
不同 BPMN 元素:
rect
circle
polygon
path
SVG 结构:
并不完全相同
所以:
不能假设所有节点都是 rect
一百一十六、更稳的节点样式
可以:
.node-completed
.djs-visual
> * {
stroke: #67c23a !important;
}
但要注意:
可能影响图标内部元素
一百一十七、课程建议
先针对:
UserTask
Gateway
Event
在浏览器开发者工具:
检查实际 SVG
再微调 CSS。
一百一十八、为什么不能全局改 .djs-element
会把:
所有流程图
一起影响。
推荐:
组件容器作用域
一百一十九、例如
.process-diagram
.node-completed
.djs-visual
> :first-child {
stroke: #67c23a !important;
}
一百二十、Scoped CSS 问题
Vue:
<style scoped>
可能无法直接命中:
bpmn-js 动态生成 DOM
一百二十一、为什么
scoped 会给:
Vue 模板 DOM
加属性选择器。
bpmn-js:
运行时动态插入 SVG
可能没有该属性。
一百二十二、解决方式
使用:
:deep(...)
一百二十三、例如
:deep(
.node-completed
.djs-visual
> :first-child
) {
stroke: #67c23a !important;
}
一百二十四、或者
流程高亮样式:
放全局 CSS
一百二十五、哪个更推荐
组件内:
:deep
更容易:
跟组件一起维护
一百二十六、连线箭头颜色问题
SequenceFlow:
线本身变色
箭头:
可能仍然黑色
一百二十七、为什么
箭头可能使用:
SVG marker-end
样式继承:
和 path 不完全一样
一百二十八、课程第一版
先保证:
线体明显高亮
箭头颜色:
作为 UI 微调
一百二十九、已完成 EndEvent
如果流程结束:
approveEnd
或:
rejectEnd
应该:
高亮为 completed
一百三十、结果色是否区分通过/拒绝
可以。
例如:
通过 End
→ success
拒绝 End
→ danger
一百三十一、后端 result
ProcessDiagramVO:
APPROVED
REJECTED
PROCESSING
一百三十二、为什么 result 由业务表给
最终审批结果:
car_settlement.approval_status
已经明确。
前端:
不必自己猜
一百三十三、是否根据 EndEvent ID 判断结果
可以辅助。
但业务状态:
更直接
一百三十四、流程图顶部状态
可以显示:
当前审批中
审批通过
审批拒绝
一百三十五、不要把流程图做成唯一状态来源
业务状态:
仍以业务后端返回为准
一百三十六、ElementRegistry
bpmn-js:
ElementRegistry
用于:
根据 ID 查图上元素
一百三十七、获取
const elementRegistry =
viewer.get(
'elementRegistry'
)
一百三十八、查询元素
const element =
elementRegistry.get(
'managerApprove'
)
一百三十九、为什么有用
高亮前可以:
判断节点是否真的存在
一百四十、改进 safeAddMarker
function safeAddMarker(
canvas,
elementRegistry,
id,
marker
) {
const element =
elementRegistry.get(
id
)
if (
!element
) {
console.warn(
'BPMN 元素不存在:',
id
)
return
}
canvas.addMarker(
id,
marker
)
}
一百四十一、比 try/catch 更清晰
优先:
显式检查
一百四十二、Label 是否也变色
有些 BPMN:
外部 Label
可能是:
独立图形元素
只给主节点加 Marker:
文字不一定同步
一百四十三、第一版是否必须处理
不用。
重点:
节点和线高亮
一百四十四、后续 UI 优化
可以:
同时给 label 添加 Marker
但具体 API:
按 bpmn-js 当前 ElementRegistry 结构处理
一百四十五、流程图销毁
组件关闭时:
必须 destroy
一百四十六、为什么
否则反复打开 Drawer:
可能堆积监听器
内存泄漏
重复 Canvas
一百四十七、onBeforeUnmount
onBeforeUnmount(
() => {
if (
viewer
) {
viewer.destroy()
viewer = null
}
}
)
一百四十八、如果 Drawer 组件不销毁怎么办
Element Plus Drawer:
默认可能只是隐藏
一百四十九、每次切换 processInstanceId
需要:
重新加载 XML
一百五十、watch
watch(
() =>
props.processInstanceId,
() => {
loadDiagram()
},
{
immediate: true
}
)
一百五十一、重复加载前要不要 clear
importXML:
会替换当前图
具体行为:
以 bpmn-js 当前版本为准
为了稳定:
可以复用同一个 Viewer
一百五十二、容器尺寸问题
如果 Drawer:
还没完全展开
就:
zoom fit-viewport
可能:
尺寸计算不准
一百五十三、解决
在 Drawer:
opened
事件后:
加载 / resize
一百五十四、例如
<el-drawer
v-model="visible"
@opened="
handleDrawerOpened
"
>
一百五十五、handleDrawerOpened
function handleDrawerOpened() {
loadDiagram()
}
一百五十六、为什么不是只 watch visible
DOM:
虽然已经开始显示
但动画:
可能还没完成
opened:
更稳定
一百五十七、流程图容器高度
必须有:
明确高度
一百五十八、错误
.bpmn-canvas {
height: 100%;
}
但父级:
没有高度
结果:
0px
一百五十九、正确
.bpmn-canvas {
width: 100%;
height: 520px;
}
一百六十、为什么图“空白”时先检查容器高度
这是 bpmn-js:
非常常见问题
一百六十一、流程图 API
export function getProcessDiagramData(
processInstanceId
) {
return request({
url:
`/car/workflow/process/${processInstanceId}/diagram-data`,
method:
'get'
})
}
一百六十二、Controller
@PreAuthorize(
"@ss.hasPermi('car:workflow:task:query')"
)
@GetMapping(
"/process/{processInstanceId}/diagram-data"
)
public AjaxResult diagramData(
@PathVariable
String processInstanceId
) {
return success(
workflowProcessService
.getProcessDiagram(
processInstanceId
)
);
}
一百六十三、为什么 Controller 不负责查 History
Controller:
只处理 HTTP
Service:
处理流程逻辑
一百六十四、流程图数据是否缓存
第一版:
不需要
一百六十五、为什么
审批详情访问量:
不高
流程状态:
会变化
加缓存:
反而增加失效复杂度
一百六十六、已结束流程可以缓存吗
技术上:
可以
但当前:
没必要
一百六十七、流程图高亮与 Timeline 的区别
Timeline:
适合读审批记录
流程图:
适合看整体路径
一百六十八、审批详情建议两个 Tab
业务详情
流程进度
一百六十九、或者三个 Tab
结算详情
审批记录
流程图
一百七十、当前推荐
结算详情
流程审批
在流程审批 Tab:
上:流程图
下:审批时间线
一百七十一、为什么
用户:
先看整体走到哪里
再:
看每次审批意见
一百七十二、ApprovalDrawer 结构升级
<el-tabs
v-model="activeTab"
>
<el-tab-pane
label="结算详情"
name="business"
>
...
</el-tab-pane>
<el-tab-pane
label="流程审批"
name="workflow"
>
<ProcessDiagram
:process-instance-id="
detail.task.processInstanceId
"
/>
<ApprovalTimeline
:timeline="
detail.timeline
"
/>
</el-tab-pane>
</el-tabs>
一百七十三、已办也能复用 ProcessDiagram
因为:
组件后端兼容 Runtime + History
一百七十四、这是为什么前面统一 resolveProcessContext 很重要
不需要前端判断:
流程是不是已经结束
一百七十五、前端只传 processInstanceId
后端:
自己判断运行/历史
一百七十六、运行中流程 Current Node
前端:
node-current
一百七十七、已结束流程
currentActivityIds = []
所有真正走过节点:
completed
一百七十八、审批拒绝流程图
历史:
start
managerApprove
managerGateway
rejectEnd
应该只高亮:
真实走过这条路径
一百七十九、不能把 financeApprove 高亮
因为:
根本没执行
一百八十、这就是为什么只高亮 Historic Activity
不能:
看到 BPMN 图里有节点
就全部着色
一百八十一、SequenceFlow 拒绝路径
应该高亮:
managerReject
不是:
managerPass
一百八十二、网关是流程图高亮难点
因为一个网关:
有多个 outgoing flow
需要知道:
实际走的是哪一条
一百八十三、错误做法
只根据:
source 节点 completed
把它的:
所有 outgoing flow
都高亮。
一百八十四、为什么错
经理网关:
通过线
拒绝线
只能:
走一条
一百八十五、正确
结合:
历史 Flow
或
历史下一个节点
确定实际 SequenceFlow。
一百八十六、简单流程连线推导
private List<String>
inferCompletedFlows(
BpmnModel bpmnModel,
List<String> orderedNodeIds
) {
List<String> result =
new ArrayList<>();
for (
int i = 0;
i < orderedNodeIds.size() - 1;
i++
) {
String sourceId =
orderedNodeIds.get(i);
String targetId =
orderedNodeIds.get(i + 1);
FlowElement element =
bpmnModel
.getMainProcess()
.getFlowElement(
sourceId
);
if (
!(element instanceof FlowNode source)
) {
continue;
}
for (
SequenceFlow flow
:
source.getOutgoingFlows()
) {
if (
targetId.equals(
flow.getTargetRef()
)
) {
result.add(
flow.getId()
);
break;
}
}
}
return result;
}
一百八十七、注意 targetRef API
Activiti 不同版本模型对象:
方法可能略不同
所以:
以实际 BpmnModel API 为准
一百八十八、为什么这只是课程简化算法
如果流程有:
并行
循环
多实例
子流程
简单相邻列表:
不够
一百八十九、当前淘车湾结算审批
串行 + 排他
因此:
可以
一百九十、已完成节点顺序如何得到
不能只:
distinct 后丢掉顺序
用于推导连线时:
要保留历史时间顺序
一百九十一、推荐先保留完整 List
orderedActivities
然后分别:
提取 Node IDs
提取 Flow IDs
一百九十二、不要一开始转 Set
Set:
可能破坏顺序
一百九十三、循环流程的重复节点
同一个 activityId:
可能出现多次
显示节点高亮:
distinct
可以。
但是流程路径分析:
不能过早 distinct
一百九十四、这是非常重要的区别
显示集合
可以去重
路径计算
要保留执行顺序
一百九十五、当前流程没有循环
但这个思想:
必须知道
一百九十六、当前节点 Overlay
除了颜色:
还可以加“当前”标签
一百九十七、bpmn-js Overlays
可以:
const overlays =
viewer.get(
'overlays'
)
一百九十八、添加
overlays.add(
activityId,
{
position: {
top: -12,
right: -10
},
html:
'<div class="current-badge">当前</div>'
}
)
一百九十九、为什么 Overlay 是可选增强
Marker:
已经能看出状态
Overlay:
让当前节点更明确
二百、课程第一版是否推荐
可以加:
“当前”小标签
但不要:
给每个节点加很多气泡
二百零一、为什么
流程图会:
非常乱
二百零二、审批人名字要不要直接 Overlay
不建议第一版。
审批人:
放时间线
更清楚。
二百零三、Legend 图例
流程图顶部:
已完成
当前节点
未执行
二百零四、为什么需要图例
用户不一定知道:
颜色是什么意思
二百零五、图例组件
<div class="diagram-legend">
<span>
已完成
</span>
<span>
当前节点
</span>
<span>
未执行
</span>
</div>
二百零六、颜色不要写进业务数据
后端只返回:
completed/current
前端:
决定颜色
二百零七、为什么
颜色:
属于 UI
不属于:
业务协议
二百零八、流程图点击节点
可以:
显示节点信息
但当前:
不必
二百零九、以后可监听
const eventBus =
viewer.get(
'eventBus'
)
例如:
element.click
二百一十、当前为什么不加
毕业/课程项目:
先完成核心高亮
二百一十一、流程图缩放按钮
NavigatedViewer:
支持鼠标导航
也可以自定义:
放大
缩小
重置
二百一十二、重置
canvas.zoom(
'fit-viewport'
)
二百一十三、流程图宽度响应式
父容器:
width: 100%
二百一十四、窗口变化
可以:
ResizeObserver
重新:
fit-viewport
当前课程:
可选
二百一十五、不要每次 resize 都疯狂 zoom
会:
影响用户手动缩放体验
二百一十六、流程图错误状态
后端可能:
流程 XML 不存在
流程实例不存在
无权限
前端要:
显示明确错误
二百一十七、不要只 console.error
用户应该看到:
流程图加载失败
二百一十八、例如
try {
...
} catch (
error
) {
loadError.value =
true
}
二百一十九、模板
<el-empty
v-if="loadError"
description="流程图加载失败"
/>
二百二十、Loading
importXML:
可能有短暂延迟
可以:
v-loading
二百二十一、ProcessDiagram.vue 完整骨架
<template>
<div
class="process-diagram"
v-loading="loading"
>
<el-empty
v-if="loadError"
description="流程图加载失败"
/>
<div
v-show="!loadError"
ref="canvasRef"
class="bpmn-canvas"
/>
</div>
</template>
<script setup>
import {
nextTick,
onBeforeUnmount,
ref
} from 'vue'
import BpmnViewer
from 'bpmn-js/lib/NavigatedViewer'
import {
getProcessDiagramData
} from '@/api/car/workflow'
const props =
defineProps({
processInstanceId: {
type: String,
required: true
}
})
const canvasRef =
ref(null)
const loading =
ref(false)
const loadError =
ref(false)
let viewer = null
async function loadDiagram() {
loading.value =
true
loadError.value =
false
try {
await nextTick()
if (
!viewer
) {
viewer =
new BpmnViewer({
container:
canvasRef.value
})
}
const res =
await getProcessDiagramData(
props.processInstanceId
)
const data =
res.data
await viewer.importXML(
data.bpmnXml
)
const canvas =
viewer.get(
'canvas'
)
const registry =
viewer.get(
'elementRegistry'
)
canvas.zoom(
'fit-viewport'
)
data.completedActivityIds
?.forEach(
id =>
addMarker(
canvas,
registry,
id,
'node-completed'
)
)
data.completedFlowIds
?.forEach(
id =>
addMarker(
canvas,
registry,
id,
'flow-completed'
)
)
data.currentActivityIds
?.forEach(
id =>
addMarker(
canvas,
registry,
id,
'node-current'
)
)
} catch (
error
) {
console.error(
'加载流程图失败',
error
)
loadError.value =
true
} finally {
loading.value =
false
}
}
function addMarker(
canvas,
registry,
id,
marker
) {
if (
!registry.get(id)
) {
console.warn(
'未找到 BPMN 元素',
id
)
return
}
canvas.addMarker(
id,
marker
)
}
defineExpose({
loadDiagram
})
onBeforeUnmount(
() => {
if (
viewer
) {
viewer.destroy()
viewer = null
}
}
)
</script>
二百二十二、为什么 defineExpose
父 Drawer 在:
opened
时可以:
主动调用 loadDiagram()
二百二十三、父组件
<ProcessDiagram
ref="diagramRef"
:process-instance-id="
detail.processInstanceId
"
/>
二百二十四、Drawer opened
function handleOpened() {
diagramRef.value
?.loadDiagram()
}
二百二十五、为什么这种方式比 immediate watch 更稳定
Drawer:
真正打开完成后
容器:
尺寸已经确定
二百二十六、CSS 高亮示意
:deep(
.node-completed
.djs-visual
> :first-child
) {
stroke: #67c23a !important;
}
:deep(
.node-current
.djs-visual
> :first-child
) {
stroke: #409eff !important;
stroke-width: 3px !important;
}
:deep(
.flow-completed
.djs-visual
> path
) {
stroke: #67c23a !important;
stroke-width: 3px !important;
}
二百二十七、注意
这里的样式:
只是示意
实际要根据:
当前 bpmn-js SVG DOM
微调。
二百二十八、为什么笔记不把 CSS 写死成万能答案
bpmn-js:
不同元素 SVG 结构不同
前端版本升级:
也可能影响 DOM
二百二十九、所以最可靠排查
Chrome:
F12
Elements
找到 data-element-id
检查 .djs-visual
二百三十、data-element-id
bpmn-js 渲染后:
一般可以在 DOM 中看到 BPMN 元素 ID
例如:
managerApprove
方便:
排查 Marker 是否加上
二百三十一、如何验证 Marker 是否成功
开发者工具:
找到 managerApprove 元素
看 class:
是否出现 node-current
二百三十二、如果 class 有但颜色没变
说明:
CSS 选择器问题
二百三十三、如果 class 都没有
说明:
addMarker 没执行
ID 不存在
importXML 未完成
二百三十四、分层排错非常重要
数据问题
BPMN ID 问题
Marker 问题
CSS 问题
不要混在一起。
二百三十五、流程图没有高亮排查顺序
1. 后端返回 completed/current 是否有值
2. activityId 是否和 BPMN id 一致
3. 加载的是否正确 ProcessDefinition 版本
4. importXML 是否成功
5. elementRegistry.get(id) 是否存在
6. canvas.addMarker 是否执行
7. DOM class 是否出现
8. CSS 是否命中
二百三十六、错误 1:加载最新流程定义
这是最常见严重错误。
应该:
processInstance.processDefinitionId
二百三十七、错误 2:使用 activityName 高亮
错误:
经理审批
正确:
managerApprove
二百三十八、为什么
bpmn-js 元素 key:
BPMN id
不是:
name
二百三十九、错误 3:只查 finished
当前节点:
不会出现 current
二百四十、错误 4:只查 ACT_RU_TASK
这样只能知道:
当前节点
不知道:
过去走过什么
二百四十一、正确
HistoryService
+
TaskService
组合。
二百四十二、错误 5:流程结束后 RuntimeService 报不存在
应该:
fallback HistoricProcessInstance
二百四十三、错误 6:Drawer 中流程图只有一小块
检查:
容器高度
Drawer opened 时机
zoom fit-viewport
二百四十四、错误 7:流程图完全空白
检查:
BPMN XML
BPMN DI
容器宽高
importXML error
二百四十五、错误 8:流程节点全部高亮
可能:
把 ProcessDefinition 所有节点
当作已执行
二百四十六、错误 9:网关两条线都高亮
说明:
根据 source outgoing 全部标记
二百四十七、正确
必须:
确定实际走过的 SequenceFlow
二百四十八、错误 10:经理拒绝后财务也高亮
检查:
历史节点提取逻辑
真正 History:
不应出现 financeApprove
二百四十九、错误 11:旧审批高亮找不到元素
99% 先检查:
是不是加载 latestVersion BPMN
二百五十、错误 12:Scoped CSS 不生效
使用:
:deep
二百五十一、错误 13:importXML 后立即加 Marker 仍失败
如果代码没有:
await
可能仍是:
异步时序问题
二百五十二、错误 14:反复打开 Drawer 越来越卡
检查:
Viewer 是否重复 new
是否 destroy
是否重复绑定事件
二百五十三、错误 15:processInstanceId 能查看别人流程
说明:
后端 Diagram API 缺 DataScope
二百五十四、为什么流程图也是敏感数据
它可能暴露:
审批结构
业务编号
审批进度
内部角色
所以:
必须鉴权
二百五十五、审批详情整合
上一章:
WorkflowApprovalDetailVO
可以增加:
processInstanceId
然后前端:
独立请求流程图
二百五十六、为什么不把 bpmnXml 放审批详情接口
也可以。
但流程图数据:
相对较大
用户如果只看业务详情:
没必要加载 XML
二百五十七、推荐
审批详情:
业务信息 + Timeline
流程 Tab 打开:
再请求 diagram-data
二百五十八、这叫按需加载
减少:
不必要网络数据
二百五十九、是否每次切换 Tab 都重新请求
不需要。
组件:
第一次加载后
可以保留。
二百六十、如果审批刚完成
流程状态变化:
需要刷新
二百六十一、审批成功后
可以:
关闭 Drawer
刷新待办
下次打开:
加载最新流程
二百六十二、无需实时 WebSocket 更新
当前课程:
不需要
二百六十三、为什么
审批是:
低频业务
用户操作后:
主动刷新
足够。
二百六十四、流程进度百分比要不要做
不推荐简单写:
已完成节点 / 总节点
二百六十五、为什么
流程可能:
分支
跳过节点
并行
例如拒绝:
本来就不会走财务
如果按总节点算:
百分比不合理
二百六十六、所以当前只显示
流程图 + 当前状态
不要造:
假的进度百分比
二百六十七、流程图和审批时间线哪个更适合移动端
时间线:
更适合窄屏
流程图:
更适合桌面
当前若依后台:
桌面端
很适合 BPMN Viewer。
二百六十八、我的代码模块
课程名称里有:
“我的代码及流程图高亮节点”
实际理解:
前面自己完成的业务代码
+
Activiti 历史数据
+
前端流程图
进行整合。
二百六十九、这一章不是重新写 Activiti
重点是:
把已有模块串起来
二百七十、后端模块依赖关系
WorkflowProcessService
↓
RuntimeService
HistoryService
TaskService
RepositoryService
↓
SettlementService / Mapper
↓
DataScope
二百七十一、不要让 SettlementService 反过来依赖整个 Workflow UI Service
避免:
循环依赖
二百七十二、推荐职责
SettlementService:
结算业务
WorkflowTaskService:
任务审批
WorkflowProcessService:
流程定义/实例/图数据
二百七十三、前端职责
ApprovalDrawer.vue:
审批业务容器
ApprovalTimeline.vue:
历史文字记录
ProcessDiagram.vue:
BPMN 渲染和高亮
二百七十四、组件职责分离好处
如果流程图有 Bug:
只改 ProcessDiagram
不会:
影响审批表单
二百七十五、流程图 API 返回示例
{
"processInstanceId": "2501",
"processDefinitionId": "settlementApproval:2:2004",
"bpmnXml": "<?xml version=\"1.0\" ...",
"completedActivityIds": [
"start",
"managerApprove",
"managerGateway"
],
"currentActivityIds": [
"financeApprove"
],
"completedFlowIds": [
"flow1",
"flow2",
"managerPass"
],
"processEnded": false,
"result": "PROCESSING"
}
二百七十六、前端为什么不需要知道 Activiti 表
只关心:
BPMN XML
节点 ID
连线 ID
状态
二百七十七、这是良好的 API 边界
流程引擎细节:
封装在后端
二百七十八、Git 提交建议
后端图数据:
git commit -m "feat: add workflow diagram data API"
二百七十九、流程历史节点:
git commit -m "feat: resolve workflow activity history"
二百八十、bpmn-js:
git commit -m "feat: add bpmn process viewer"
二百八十一、节点高亮:
git commit -m "feat: highlight workflow nodes and flows"
二百八十二、审批详情整合:
git commit -m "feat: integrate process diagram into approval detail"
二百八十三、Cursor 提示词:分析当前代码
当前项目是 RuoYi-Vue SpringBoot3 + 淘车湾 + Activiti 工作流。
请先只分析,不修改。
我现在要实现“流程图高亮”。
请读取当前:
1. BPMN 文件
2. Settlement 表和实体
3. WorkflowTaskService
4. WorkflowApprovalDetailVO
5. Activiti RuntimeService/HistoryService 使用代码
6. Vue 审批 Drawer
请输出:
1. 当前 processDefinitionId 从哪里获得
2. processInstanceId 从哪里获得
3. businessKey 是否正确
4. 当前节点如何查询
5. HistoricActivityInstance 如何查询
6. 当前 BPMN 中节点 ID 和 SequenceFlow ID 列表
7. 运行中流程和已结束流程分别如何处理
8. 是否存在加载 latestVersion 导致旧流程高亮错误的风险
二百八十四、Cursor 提示词:Diagram API
请实现 WorkflowProcessService.getProcessDiagram(processInstanceId)。
要求:
1. 先查 Runtime ProcessInstance
2. Runtime 不存在再查 HistoricProcessInstance
3. 两者都不存在才报流程不存在
4. 从 businessKey 推导 settlementId
5. 对 Settlement 做 DataScope 权限校验
6. 使用该流程实例真实 processDefinitionId
7. 绝对不要使用 processDefinitionKey.latestVersion() 获取 BPMN
8. RepositoryService 获取对应版本 BPMN XML
9. HistoryService 查询 HistoricActivityInstance
10. 返回 completedActivityIds
11. TaskService 查询 currentActivityIds
12. 尝试返回 completedFlowIds
13. 流程结束时 currentActivityIds 返回空数组
14. 不直接查询/修改 ACT_* 表
二百八十五、Cursor 提示词:bpmn-js Viewer
请在 RuoYi-Vue3 中创建:
src/views/car/workflow/components/ProcessDiagram.vue
要求:
1. 使用 bpmn-js Viewer/NavigatedViewer,不使用 Modeler
2. npm 依赖使用项目当前兼容版本
3. props 接收 processInstanceId
4. 调后端 diagram-data
5. await viewer.importXML(bpmnXml)
6. import 完成后再 get('canvas')
7. canvas.zoom('fit-viewport')
8. elementRegistry 检查元素 ID
9. completedActivityIds 添加 node-completed marker
10. currentActivityIds 添加 node-current marker
11. completedFlowIds 添加 flow-completed marker
12. onBeforeUnmount destroy viewer
13. 处理 loading 和 import error
14. 容器设置明确高度
15. Vue scoped 样式用 :deep
二百八十六、Cursor 提示词:连线高亮
请审查当前 Activiti History 是否记录 sequenceFlow。
如果 HistoricActivityInstance 能获取:
activityType = sequenceFlow
则直接提取 activityId。
如果当前版本/History 配置无法获得 SequenceFlow 历史:
1. RepositoryService.getBpmnModel(processDefinitionId)
2. 保留 HistoricActivity 的执行顺序
3. 对当前串行 + ExclusiveGateway 流程,
根据相邻已执行节点,
在 source outgoingFlows 中找 targetRef 对应的 SequenceFlow
4. 不要把网关所有 outgoingFlow 全部高亮
5. 不要为并行/循环流程编造不可靠算法
6. 说明当前简化算法只适合淘车湾现有串行审批流程
二百八十七、Cursor 提示词:旧版本流程
请检查流程图版本问题。
要求:
1. 已启动 ProcessInstance 必须使用自己的 processDefinitionId
2. HistoricProcessInstance 同样使用自己的 processDefinitionId
3. 不允许根据 process key 获取 latestVersion BPMN 来展示历史实例
4. 对比 BPMN 节点 ID 与 HistoricActivity.activityId
5. 如果 elementRegistry.get(activityId) 不存在,优先检查版本错配
二百八十八、Cursor 提示词:流程图安全
请审查流程图接口安全。
检查:
1. Controller 是否 @PreAuthorize
2. processInstanceId 是否只是前端参数
3. 后端是否通过 ProcessInstance.businessKey 找 settlementId
4. 是否对 settlement 做 DataScope
5. 是否能查看其他门店流程
6. 是否直接暴露 ACT_* 查询接口
7. 是否允许通过 processDefinitionId 任意读取其他 BPMN
8. 是否只返回当前业务实例对应的流程定义资源
二百八十九、IDEA 调试重点
断点:
getProcessDiagram
resolveProcessContext
loadBpmnXml
resolveCompletedActivityIds
resolveCurrentActivityIds
resolveCompletedFlowIds
inferCompletedFlows
二百九十、前端调试重点
Chrome:
Network
diagram-data Response
Console
Elements
SVG .djs-element
elementRegistry
二百九十一、第一次验证
流程:
刚提交
当前:
managerApprove
后端应该:
currentActivityIds
=
managerApprove
二百九十二、经理通过后
后端:
completed:
start
managerApprove
managerGateway
current:
financeApprove
二百九十三、财务通过后
processEnded = true
current = []
completed:
approveEnd
也应该:
出现
二百九十四、经理拒绝后
completed:
start
managerApprove
managerGateway
rejectEnd
不能出现:
financeApprove
二百九十五、测试用例 1
运行中:
经理审批当前
预期:
经理节点 current
二百九十六、测试用例 2
经理通过:
经理 completed
财务 current
二百九十七、测试用例 3
经理拒绝:
拒绝 End 高亮
财务不高亮
二百九十八、测试用例 4
财务通过:
通过 End 高亮
current 为空
二百九十九、测试用例 5
财务拒绝:
rejectEnd 高亮
三百、测试用例 6
流程结束后:
RuntimeService 查不到
Diagram API:
仍能成功
三百零一、测试用例 7
历史流程 version 1,
系统现在 version 3。
预期:
仍加载 version 1 BPMN
三百零二、测试用例 8
前端传别的门店 processInstanceId:
拒绝
三百零三、测试用例 9
BPMN 元素 ID 不存在:
前端警告
页面不崩溃
三百零四、测试用例 10
BPMN XML 错误:
显示流程图加载失败
三百零五、测试用例 11
容器 0 高度:
复现空白
加:
height: 520px
恢复。
三百零六、测试用例 12
Drawer 第一次打开:
fit-viewport 正常
三百零七、测试用例 13
反复打开 20 次:
无明显重复 Viewer / 监听器问题
三百零八、测试用例 14
Marker class 有,
颜色不变:
检查 CSS / :deep
三百零九、测试用例 15
网关拒绝:
只高亮 reject SequenceFlow
三百一十、测试用例 16
网关通过:
只高亮 pass SequenceFlow
三百一十一、测试用例 17
已办详情:
能显示完整历史流程图
三百一十二、测试用例 18
待办详情:
能显示 current 节点
三百一十三、测试用例 19
审批完成刷新后:
current → completed
三百一十四、测试用例 20
历史 Activity 有重复 activityId:
节点显示高亮去重
但路径推导保留原执行顺序
三百一十五、数据库诊断:历史 Activity
可以学习查看:
SELECT
ACT_ID_,
ACT_NAME_,
ACT_TYPE_,
PROC_INST_ID_,
TASK_ID_,
ASSIGNEE_,
START_TIME_,
END_TIME_
FROM ACT_HI_ACTINST
WHERE PROC_INST_ID_ = ?
ORDER BY
START_TIME_ ASC;
字段:
以当前 Activiti 实际版本为准
三百一十六、当前运行任务
SELECT
ID_,
NAME_,
TASK_DEF_KEY_,
ASSIGNEE_,
PROC_INST_ID_
FROM ACT_RU_TASK
WHERE PROC_INST_ID_ = ?;
三百一十七、流程定义版本
SELECT
ID_,
KEY_,
VERSION_,
DEPLOYMENT_ID_,
RESOURCE_NAME_
FROM ACT_RE_PROCDEF
WHERE KEY_ =
'settlementApproval'
ORDER BY VERSION_;
三百一十八、为什么这些 SQL 只用于学习
业务代码:
仍然使用官方 Service API
三百一十九、面试题 1:流程图高亮需要哪些数据
答:
至少需要流程实例对应的 BPMN XML、
已经完成的 Activity ID、
当前正在执行的 Activity ID。
如果要高亮实际走过的连线,
还需要 SequenceFlow ID 或通过 BPMN 模型和历史路径推导。
前端使用 bpmn-js 根据 BPMN 元素 ID 加 Marker。
三百二十、面试题 2:为什么已结束流程不能只用 RuntimeService
答:
流程结束后,运行时 ProcessInstance 会从 Runtime 数据中结束。
因此 RuntimeService 可能查不到已经完成的流程。
历史流程应该通过 HistoryService 查询 HistoricProcessInstance,
再取得 processDefinitionId 和 businessKey。
三百二十一、面试题 3:为什么不能加载最新 BPMN 来展示历史流程
答:
流程定义会有版本。
一个历史流程实例是在某个确定的 ProcessDefinition 版本上启动的。
如果展示时加载最新版本,
节点 ID、网关和路径可能已经变化,
HistoricActivityId 就无法正确对应。
因此必须使用流程实例自身的 processDefinitionId。
三百二十二、面试题 4:HistoricActivityInstance 有什么作用
答:
它记录流程中某个 Activity 的历史执行信息,
包括 Activity ID、类型、开始结束时间以及可能的处理人。
流程图高亮可以利用这些历史记录判断哪些节点已经执行过,
流程审计和路径分析也会使用它。
三百二十三、面试题 5:当前节点为什么常用 TaskService 查询
答:
当前淘车湾审批流程主要由 UserTask 组成。
正在等待经理或财务处理的节点会对应一个运行中的 Task,
Task.taskDefinitionKey 就是 BPMN UserTask ID。
因此 TaskService 很适合获得当前人工审批节点。
三百二十四、面试题 6:为什么 currentActivityIds 用 List
答:
当前串行流程通常只有一个人工任务,
但 BPMN 支持并行网关。
并行流程可能同时产生多个当前任务,
因此接口设计成 List 更通用,
避免未来修改接口结构。
三百二十五、面试题 7:为什么 SequenceFlow 高亮比节点难
答:
一个网关可能有多个 outgoing SequenceFlow。
仅知道网关执行过,
并不能说明所有分支都执行过。
必须知道实际经过哪条 Flow,
可以使用历史中的 SequenceFlow 记录,
或者结合 BPMN 模型和实际下一个历史节点推导。
三百二十六、面试题 8:为什么不能把网关所有 outgoingFlow 都高亮
答:
ExclusiveGateway 只选择符合条件的一条分支。
如果因为网关已完成就把所有 outgoingFlow 高亮,
通过路径和拒绝路径会同时显示,
与真实流程不一致。
三百二十七、面试题 9:为什么 bpmn-js Marker 必须在 importXML 后添加
答:
importXML 是异步过程。
只有 XML 导入完成后,
流程元素才被解析并注册到 Canvas/ElementRegistry。
如果提前 addMarker,
对应 BPMN 元素尚不存在,
就可能出现 invalid element 或 element not found。
三百二十八、面试题 10:Viewer 与 Modeler 有什么区别
答:
Viewer 用于渲染和查看 BPMN,
适合流程详情和审批进度。
Modeler 额外提供节点创建、拖动、连线、修改属性等建模能力。
审批详情只需要查看,
因此使用 Viewer 更轻量、更安全。
三百二十九、面试题 11:为什么 Vue scoped CSS 可能无法修改 BPMN 节点
答:
bpmn-js 的 SVG 是运行时动态创建的,
它不一定带 Vue scoped CSS 自动生成的属性标记。
因此普通 scoped 选择器可能匹配不到。
可以使用 :deep(),
或者把流程图 Marker 样式放到合适的全局样式中。
三百三十、面试题 12:为什么流程图接口也需要 DataScope
答:
processInstanceId 本身并不是授权凭证。
用户可能构造其他流程实例 ID。
后端必须通过流程实例 businessKey 找到真实结算单,
再检查该结算单是否属于当前用户数据范围,
之后才能返回 BPMN 和审批进度。
三百三十一、面试题 13:为什么流程结束后 currentActivityIds 是空
答:
流程已经没有正在等待执行的人工任务。
此时所有实际走过的节点都属于历史,
因此 Current 节点集合应为空,
页面通过 Completed 路径和最终业务状态表示结果。
三百三十二、面试题 14:为什么不要用节点名称做高亮 ID
答:
节点 name 是展示文本,
可能重复,也可能后续修改。
BPMN id 才是流程元素的稳定技术标识。
HistoricActivityInstance.activityId 和
Task.taskDefinitionKey 都应该对应 BPMN id,
前端 Marker 也使用该 id。
三百三十三、面试题 15:为什么流程图数据不建议缓存
答:
运行中审批状态会随着 Task 完成不断变化。
如果缓存 completed/current 节点,
需要额外处理复杂的缓存失效。
审批流程图访问频率通常不高,
课程单体项目直接实时查询 Runtime/History 更简单可靠。
三百三十四、流程图知识树
Process Diagram
│
├─ Definition
│ ├─ processDefinitionId
│ ├─ RepositoryService
│ ├─ BPMN XML
│ └─ BPMN DI
│
├─ Runtime
│ ├─ ProcessInstance
│ ├─ TaskService
│ └─ currentActivityIds
│
├─ History
│ ├─ HistoricProcessInstance
│ ├─ HistoricActivityInstance
│ ├─ completedActivityIds
│ └─ completedFlowIds
│
├─ BPMN Model
│ ├─ FlowNode
│ ├─ SequenceFlow
│ └─ Gateway Path
│
├─ Frontend
│ ├─ bpmn-js Viewer
│ ├─ importXML
│ ├─ Canvas
│ ├─ ElementRegistry
│ ├─ addMarker
│ ├─ Overlay
│ └─ fit-viewport
│
├─ Vue
│ ├─ ProcessDiagram.vue
│ ├─ Drawer opened
│ ├─ :deep CSS
│ └─ destroy viewer
│
└─ Security
├─ @PreAuthorize
├─ businessKey
├─ Settlement
└─ DataScope
三百三十五、完整后端流程图数据链
processInstanceId
↓
Runtime ProcessInstance?
├─ Yes
│ → Running Context
│
└─ No
↓
HistoricProcessInstance?
├─ No
│ → Not Found
│
└─ Yes
→ Historic Context
↓
businessKey
↓
Settlement
↓
DataScope
↓
processDefinitionId
↓
RepositoryService
↓
BPMN XML
↓
HistoryService
↓
completed activity
↓
TaskService
↓
current activity
↓
ProcessDiagramVO
三百三十六、完整前端高亮链
ProcessDiagramVO
↓
viewer.importXML
↓
await import complete
↓
Canvas
+
ElementRegistry
↓
completedActivityIds
→ node-completed
↓
completedFlowIds
→ flow-completed
↓
currentActivityIds
→ node-current
↓
fit-viewport
↓
审批进度流程图
三百三十七、完整版本安全链
ProcessInstance
↓
Own processDefinitionId
↓
Own BPMN XML Version
↓
Historic Activity IDs
↓
ElementRegistry Match
↓
Correct Highlight
不要:
Process Key
↓
Latest Version
↓
Historical Instance
三百三十八、本章最终验收
你应该能够独立完成:
1. 获取运行流程上下文
2. 获取历史流程上下文
3. 统一处理运行中和已结束实例
4. 从 businessKey 找 settlementId
5. 流程图接口 DataScope
6. 获取真实 processDefinitionId
7. RepositoryService 读取 BPMN XML
8. 理解 BPMN DI
9. 查询 HistoricActivityInstance
10. 提取 completedActivityIds
11. TaskService 提取 currentActivityIds
12. 提取或推导 completedFlowIds
13. 正确处理 ExclusiveGateway 分支
14. 不高亮未执行节点
15. 历史实例加载自己的 BPMN 版本
16. Vue 安装 bpmn-js
17. 使用 NavigatedViewer
18. await importXML
19. Canvas
20. ElementRegistry
21. canvas.addMarker
22. 完成节点样式
23. 当前节点样式
24. 已走连线样式
25. :deep 处理 scoped CSS
26. fit-viewport
27. Drawer opened 后加载
28. Viewer destroy
29. ApprovalDrawer 集成流程图
30. 已办流程图
31. 待办流程图
32. 错误/空白排查
33. 节点 ID 与 name 区分
34. 流程版本问题排查
35. 流程图权限保护
三百三十九、本章最重要的工程原则
1. 流程图必须加载实例自己的 ProcessDefinition 版本
2. Activity 高亮使用 BPMN id,不使用 name
3. 已完成节点来自 History
4. 当前人工节点来自 Runtime Task
5. 流程结束后 Runtime 查不到是正常的
6. 已结束流程必须回退 HistoricProcessInstance
7. SequenceFlow 不能因为 source 节点完成就全部高亮
8. ExclusiveGateway 只高亮真实执行分支
9. 路径计算时保留历史执行顺序
10. 显示 Marker 集合可以去重
11. bpmn-js importXML 是异步的
12. 必须 import 完成后再 addMarker
13. ElementRegistry 可以提前检查元素是否存在
14. Vue scoped 样式可能需要 :deep
15. BPMN Viewer 容器必须有明确高度
16. Drawer 最好 opened 后再 fit-viewport
17. 组件销毁时 destroy Viewer
18. 流程图接口仍然需要 @PreAuthorize + DataScope
19. 不直接操作 ACT_* 表来生成流程图
20. 流程图是业务展示,业务最终状态仍以后端业务表为准
三百四十、下一篇
按照课程表,下一篇进入:
《淘车湾项目实战(九):我的一般/我的申请分析及代码实现》
课程表原文字段存在“我的一般”表述,
结合前后工作流课程内容,
这一阶段通常对应:
我的申请
我发起的流程
我的业务单据
申请状态
审批进度
业务详情
流程详情
重新提交
我的已办/待办整合
下一篇会继续把:
申请人视角
补完整,形成:
申请人
↓
提交审批
↓
经理
↓
财务
↓
申请人查看结果
的完整闭环。
官方资料提醒
bpmn-js 官方目前仍以:
BPMN 2.0 Viewer / Modeler
方式提供浏览器端流程图能力。
核心使用模式:
new Viewer(...)
↓
await importXML(...)
↓
viewer.get('canvas')
↓
zoom('fit-viewport')
流程图高亮通常通过:
Canvas Marker + CSS
扩展。
Activiti History 中:
HistoricActivityInstance
用于记录流程 Activity 的执行历史,
可以作为已执行节点分析的重要数据来源。
本章所有 Activiti Query/API 名称:
仍应以你项目实际 Activiti 依赖版本为准
尤其你的项目如果是:
Spring Boot 3 + JDK17
不要机械照搬:
旧 Activiti 5/6/7 教程中的依赖配置
但:
ProcessDefinition
ProcessInstance
HistoricProcessInstance
HistoricActivityInstance
Task
BPMN XML
bpmn-js
Marker
这些核心设计思想是完全值得掌握的。