feat: 引擎多出口路由/退回/移交/或签补齐 + 定义 layout 坐标
build / build-push (push) Successful in 5m11s

- 多出口:FlwTask 增 edge_name 列(sql/20260916),FlwTaskRepository.claimAndFinish
  同步落列;FlowTaskService.handle 出度>1 时 edgeName 必填;FlowEngine.continueAdvance
  按 edge name 选路(单出口原路径逐字节兼容,未命中不推进);FlowAdvanceService/
  FlowNodeAdvanceListener 透传;BranchScope 从 FlowEngine 提取
- DefValidator:多出口出口命名必填且同节点唯一;跨分支连线发布期明确拒绝
- 退回 POST /task/{id}/returnBack:默认退主管线最近已批节点,精确失效当前节点待办,
  split 幂等 upsert 使退回重走并行段不撞 flw_branch uk
- 移交 POST /task/{id}/transfer:TRANSFERRED(5)+新建待办,AssigneeResolver.requireUserName
- 或签 ANY 补齐:首个同意失效同节点兄弟待办
- 已办口径/撤回守卫:status in (1,2,4,5);新事件 flow.returned/flow.transferred
- FlowDiagramService:EdgeVo.name/NodeVo.state/DiagramVo.layout,办件图状态计算
- 新 IT:多出口×3、ANY 或签、退回重走并行段、退回/移交/撤回

mvn test 84/84 绿
This commit is contained in:
chenxing
2026-09-16 22:23:02 +08:00
parent 55e3a3f800
commit b8441065d0
31 changed files with 981 additions and 126 deletions
@@ -0,0 +1,57 @@
package com.yunhai.flowcenter.engine;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashSet;
import java.util.Set;
/**
* 分支归属判定:节点属于哪个并行分支(branchKey=PARALLEL_SPLIT 出边目标节点 id),
* 主线节点与汇聚点之后属 null。运行时用于推导分支上下文({@link FlowEngine}),
* 发布期用于拒绝跨分支连线({@link DefValidator})——两处必须同口径,故提取为静态工具。
*/
public final class BranchScope {
private BranchScope() {
}
/**
* 沿入边反向 BFS 走到最近的上游 PARALLEL_SPLIT,返回该 split 指向本链的 outEdge.to(即分支键);
* 遇 START/PARALLEL_JOIN 边界即停(join 之后属汇聚链,分支键已消耗,返回 null)。
*/
public static String keyOf(FlowDefModel model, String nodeId) {
if (model == null || nodeId == null) {
return null;
}
Set<String> visited = new HashSet<>();
Deque<String> queue = new ArrayDeque<>();
queue.add(nodeId);
visited.add(nodeId);
while (!queue.isEmpty()) {
String cur = queue.poll();
for (FlowDefModel.Edge e : model.inEdges(cur)) {
FlowDefModel.Node from = model.node(e.from);
if (from == null) {
continue;
}
if (FlowDefModel.TYPE_PARALLEL_SPLIT.equals(from.type)) {
return e.to;
}
if (FlowDefModel.TYPE_START.equals(from.type)
|| FlowDefModel.TYPE_PARALLEL_JOIN.equals(from.type)
|| !visited.add(e.from)) {
continue;
}
queue.add(e.from);
}
}
return null;
}
/** 两节点是否同属一个分支上下文(含"都属主线"=都属于 null) */
public static boolean sameScope(FlowDefModel model, String a, String b) {
String ka = keyOf(model, a);
String kb = keyOf(model, b);
return ka == null ? kb == null : ka.equals(kb);
}
}