流程追踪图需要把三类信息合在一起:BPMN 模型提供节点和坐标,运行时提供当前活动,历史记录提供已经发生的执行。先把这些数据分清楚,再决定画什么颜色,比直接遍历一张历史表可靠。

不要把最后一条历史记录当成当前节点

按历史活动实例 ID 排序,不等于按执行时间排序。即使按时间排序,最后一条也不一定是唯一当前节点:并行分支可以同时运行;循环可以再次进入已完成过的节点;流程结束后则不再有当前节点。

对仍在运行的实例,优先从运行时读取活动 ID。历史记录用于判断曾发生过什么,不负责单独推导“现在在哪里”。

下面是获取节点高亮数据的局部代码,适用 Activiti 7.1.0.M6 引擎接口,假设三个服务和实例 ID 已准备好:

List<org.activiti.engine.history.HistoricActivityInstance> history =
        historyService.createHistoricActivityInstanceQuery()
                .processInstanceId(processInstanceId)
                .list();

Set<String> completedIds = new HashSet<>();
for (org.activiti.engine.history.HistoricActivityInstance activity : history) {
    if (activity.getEndTime() != null) {
        completedIds.add(activity.getActivityId());
    }
}

org.activiti.engine.runtime.ProcessInstance running =
        runtimeService.createProcessInstanceQuery()
                .processInstanceId(processInstanceId)
                .singleResult();

Set<String> activeIds = new HashSet<>();
if (running != null) {
    activeIds.addAll(runtimeService.getActiveActivityIds(processInstanceId));
}

这里的“completed”表示历史活动已经结束,是否正常完成还要结合结束原因和业务语义;若页面需要区分取消,应增加对应状态。多实例、子流程的展示也应按节点类型选择哪些活动需要画框。

先生成默认高亮图

根据历史流程实例取得定义 ID,再读取同一版本的 BPMN 模型。不能直接取该 key 的最新定义,否则已运行实例可能被画到另一张图上。

org.activiti.engine.history.HistoricProcessInstance instance =
        historyService.createHistoricProcessInstanceQuery()
                .processInstanceId(processInstanceId)
                .singleResult();

if (instance == null) {
    throw new IllegalArgumentException("没有可用的流程历史记录");
}

org.activiti.bpmn.model.BpmnModel model =
        repositoryService.getBpmnModel(instance.getProcessDefinitionId());

Set<String> highlightedIds = new HashSet<>(completedIds);
highlightedIds.addAll(activeIds);
org.activiti.image.ProcessDiagramGenerator generator =
        new org.activiti.image.impl.DefaultProcessDiagramGenerator();

try (InputStream image = generator.generateDiagram(
        model, new ArrayList<>(highlightedIds), Collections.emptyList(),
        "SansSerif", "SansSerif", "SansSerif")) {
    byte[] buffer = new byte[4096];
    int length;
    while ((length = image.read(buffer)) != -1) {
        outputStream.write(buffer, 0, length);
    }
}

代码需要 Java IO、集合类型以及相应 Activiti 依赖;outputStream 由调用方管理,不在这里关闭。7.1.0.M6 的默认生成器是 SVG 生成实现,HTTP 响应类型应与实际输出一致,不能只把扩展名写成 PNG。模型还需要带图形坐标信息,字体也要在运行环境中可用。生成器接口

红色表示当前,绿色表示已结束

默认生成器只接收高亮 ID 列表,不能仅凭一个列表区分两种颜色。需要定制绘图逻辑,将状态集合传到每次绘制使用的生成器或画布,而不是把状态存在静态共享变量中。

节点颜色的判断可以独立为:

Color highlightColor(String nodeId, Set<String> active, Set<String> completed) {
    if (active.contains(nodeId)) {
        return Color.RED;
    }
    if (completed.contains(nodeId)) {
        return Color.GREEN;
    }
    return null; // 无高亮
}

循环中同一节点可能既有完成记录,又正在执行;上述规则让红色优先,页面图例应说明这一点。

定制入口分成两层:在 DefaultProcessDiagramGenerator 的活动绘制逻辑中决定节点状态,在 DefaultProcessDiagramCanvas 中按所选颜色画高亮边框。仅修改一个全局 HIGHLIGHT_COLOR 会把所有高亮改成同一种颜色,无法表达两种状态。扩展时应以所用版本的生成器源码与画布源码为准。

不要给节点 ID 加 # 等后缀来偷偷传状态:那会把真实模型标识和显示协议混在一起。用独立集合或明确的状态对象更容易维护。

连线高亮要有实际路径依据

示例中连线列表故意为空。两条相邻的历史活动,不必然通过同一条 SequenceFlow 连接;并行、网关和重复进入会让简单推断失效。

如果系统记录了可信的连线事件,可以按流程实例汇总实际经过的连线 ID,再交给生成器。只有节点记录、没有路径证据时,显示节点状态比画出一条看似精确却错误的执行路线更合适。