feat(flow): 回调信封升级——事件携带操作人身份(operatorUserId/operatorName)

在飞工作合入:FlowEvent 增操作人字段(可空),引擎/推进监听/回调派发全程透传,
对齐 SDK FlowCallbackEvent 的 taskId/operator 字段(规范 §6.4)。
This commit is contained in:
chenxing
2026-09-22 16:54:02 +08:00
parent 695e20ab52
commit d2395c20ce
9 changed files with 102 additions and 32 deletions
@@ -111,6 +111,11 @@ public class CallbackDispatcher {
payload.put("bizId", instance.getBizId());
payload.put("bizTitle", instance.getBizTitle());
payload.put("occurredTime", LocalDateTime.now().format(TIME));
// 2026-09-20 信封升级:任务定位与操作人(均可空,办件级事件/历史重放体为 null——
// 契约见《子应用接入流程中心规范.md》§6.3/§6.4)
payload.put("taskId", event.getTaskId());
payload.put("operatorUserId", event.getOperatorId());
payload.put("operatorName", event.getOperatorName());
Map<String, Object> data = new HashMap<>();
if (event.getData() != null) {
data.putAll(event.getData());
@@ -33,10 +33,12 @@ public class FlowAdvanceService {
* @param edgeName 多出口节点所选出口(办理事件携带);可空——兜底从该节点
* 最近一条已同意任务的 edge_name 列重放(推进在 AFTER_COMMIT 新事务重查,
* 事件传参只是快路径,落库的值才是权威)
* @param operator 本节点办理人(TASK_HANDLED 事件 data 穿透;推进产生的
* TASK_CREATED/COMPLETED 操作人信封字段来源),可空
* @return 是否实际推进了
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public boolean tryAdvance(Long instanceId, String nodeId, String edgeName) {
public boolean tryAdvance(Long instanceId, String nodeId, String edgeName, FlowOperator operator) {
FlwInstance instance = instanceRepository.findById(instanceId).orElse(null);
if (instance == null
|| instance.getStatus() != FlwEnums.InstanceStatus.APPROVING.getCode()) {
@@ -72,7 +74,7 @@ public class FlowAdvanceService {
.map(FlwTask::getEdgeName)
.orElse(null);
}
flowEngine.continueAdvance(instance, def, nodeId, chosen);
flowEngine.continueAdvance(instance, def, nodeId, chosen, operator);
instanceRepository.save(instance);
return true;
}
@@ -38,11 +38,11 @@ public class FlowEngine {
private final AssigneeResolver assigneeResolver;
private final ApplicationEventPublisher eventPublisher;
/** 发起后首推:从 START 激活(instance 已建好、status=10) */
/** 发起后首推:从 START 激活(instance 已建好、status=10)。operator=null:TASK_CREATED/COMPLETED 的操作人回退发起人 */
public void startAdvance(FlwInstance instance, FlwDef def) {
FlowDefModel model = parseModel(def);
setCurrentNodes(instance, null);
advance(instance, model, model.start, null);
advance(instance, model, model.start, null, null);
}
/**
@@ -50,8 +50,10 @@ public class FlowEngine {
* 单出边走唯一出边(旧定义逐字节兼容);多出边按办理人选的出口名(edgeName,即办理按钮文案)
* 选路——未命中时记录错误并保持节点在途,绝不静默走第一条边。
* 分支上下文由 {@link BranchScope#keyOf} 从图上回溯推导(分支内串行链的中间节点无任务级分支标记)。
* operator=本节点办理人(TASK_CREATED/COMPLETED 操作人信封字段来源),由推进监听器从事件 data 穿透传入。
*/
public void continueAdvance(FlwInstance instance, FlwDef def, String fromNodeId, String edgeName) {
public void continueAdvance(FlwInstance instance, FlwDef def, String fromNodeId, String edgeName,
FlowOperator operator) {
FlowDefModel model = parseModel(def);
FlowDefModel.Node node = model.nodeById(fromNodeId);
List<FlowDefModel.Edge> outs = model.outEdges(fromNodeId);
@@ -73,26 +75,29 @@ public class FlowEngine {
FlowDefModel.Node nextNode = model.nodeById(next);
if (FlowDefModel.TYPE_PARALLEL_JOIN.equals(nextNode.type)) {
// 会签合并守门:标记本分支到达、全部到达才放行(直通无分支键时 JOIN 直通放行)
advance(instance, model, next, branchKey);
advance(instance, model, next, branchKey, operator);
} else {
activate(instance, model, nextNode, branchKey);
activate(instance, model, nextNode, branchKey, operator);
}
}
/**
* 退回目标激活(FlowTaskService.returnBack 调用):从 currentNodes 摘除来源节点,
* 重新激活目标审批节点建新任务。沿用统一激活入口,split 幂等化后重入并行段不撞分支唯一约束。
* operator=退回人(重件 TASK_CREATED 操作人信封字段来源)。
*/
public void returnTo(FlwInstance instance, FlwDef def, String fromNodeId, String targetNodeId) {
public void returnTo(FlwInstance instance, FlwDef def, String fromNodeId, String targetNodeId,
FlowOperator operator) {
FlowDefModel model = parseModel(def);
removeCurrentNode(instance, fromNodeId);
FlowDefModel.Node target = model.nodeById(targetNodeId);
String branchKey = BranchScope.keyOf(model, targetNodeId);
advance(instance, model, target.id, branchKey);
advance(instance, model, target.id, branchKey, operator);
}
/** 统一激活入口:含 JOIN 的到达标记与放行判断 */
private void advance(FlwInstance instance, FlowDefModel model, String nodeId, String branchKey) {
private void advance(FlwInstance instance, FlowDefModel model, String nodeId, String branchKey,
FlowOperator operator) {
FlowDefModel.Node node = model.nodeById(nodeId);
if (FlowDefModel.TYPE_PARALLEL_JOIN.equals(node.type)) {
if (branchKey != null) {
@@ -114,24 +119,25 @@ public class FlowEngine {
if (outs.isEmpty()) {
return;
}
activate(instance, model, model.nodeById(outs.get(0).to), null);
advance(instance, model, outs.get(0).to, null, operator);
return;
}
activate(instance, model, node, branchKey);
activate(instance, model, node, branchKey, operator);
}
/** 非 JOIN 节点激活 */
private void activate(FlwInstance instance, FlowDefModel model, FlowDefModel.Node node, String branchKey) {
private void activate(FlwInstance instance, FlowDefModel model, FlowDefModel.Node node, String branchKey,
FlowOperator operator) {
switch (node.type) {
case FlowDefModel.TYPE_START: {
List<FlowDefModel.Edge> outs = model.outEdges(node.id);
if (!outs.isEmpty()) {
advance(instance, model, outs.get(0).to, branchKey);
advance(instance, model, outs.get(0).to, branchKey, operator);
}
break;
}
case FlowDefModel.TYPE_APPROVAL:
createTasks(instance, model, node);
createTasks(instance, model, node, operator);
addCurrentNode(instance, node.id);
break;
case FlowDefModel.TYPE_PARALLEL_SPLIT: {
@@ -148,23 +154,26 @@ public class FlowEngine {
branch.setStatus(FlwEnums.BranchStatus.RUNNING.getCode());
}
branchRepository.save(branch);
advance(instance, model, edge.to, edge.to);
advance(instance, model, edge.to, edge.to, operator);
}
break;
}
case FlowDefModel.TYPE_END:
complete(instance);
complete(instance, operator);
break;
default:
log.warn("[flow-center] 未知节点类型 {}(节点 {}),推进中止", node.type, node.id);
}
}
/** 建任务(ALL=每人一条;ANY=每人一条、任一人办理即放行)+ 到达事件 */
private void createTasks(FlwInstance instance, FlowDefModel model, FlowDefModel.Node node) {
/** 建任务(ALL=每人一条;ANY=每人一条、任一人办理即放行)+ 到达事件。operator=null 回退发起人 */
private void createTasks(FlwInstance instance, FlowDefModel model, FlowDefModel.Node node,
FlowOperator operator) {
List<AssigneeRef> assignees = assigneeResolver.resolve(instance.getSchId(), node);
LocalDateTime now = LocalDateTime.now();
LocalDateTime deadline = node.deadlineHours == null ? null : now.plusHours(node.deadlineHours);
FlowOperator op = operator != null ? operator
: FlowOperator.of(instance.getSubmitterId(), instance.getSubmitterName());
for (AssigneeRef ref : assignees) {
FlwTask task = new FlwTask();
task.setInstanceId(instance.getId());
@@ -185,20 +194,23 @@ public class FlowEngine {
data.put("totalTasks", assignees.size());
data.put("doneTasks", 0);
eventPublisher.publishEvent(FlowEvent.of(FlowEvent.Type.TASK_CREATED,
instance.getId(), task.getId(), data));
instance.getId(), task.getId(), op.getUserId(), op.getUserName(), data));
}
}
/** END 激活:办件完成、残留任务/分支失效、完成事件 */
private void complete(FlwInstance instance) {
/** END 激活:办件完成、残留任务/分支失效、完成事件。operator=null 回退发起人 */
private void complete(FlwInstance instance, FlowOperator operator) {
instance.setStatus(FlwEnums.InstanceStatus.COMPLETED.getCode());
instance.setFinishTime(LocalDateTime.now());
setCurrentNodes(instance, null);
taskRepository.invalidatePendingByInstance(instance.getId());
branchRepository.invalidateRunningByInstance(instance.getId());
FlowOperator op = operator != null ? operator
: FlowOperator.of(instance.getSubmitterId(), instance.getSubmitterName());
Map<String, Object> data = new HashMap<>();
data.put("result", "APPROVED");
eventPublisher.publishEvent(FlowEvent.of(FlowEvent.Type.COMPLETED, instance.getId(), null, data));
eventPublisher.publishEvent(FlowEvent.of(FlowEvent.Type.COMPLETED, instance.getId(), null,
op.getUserId(), op.getUserName(), data));
}
public FlowDefModel parseModel(FlwDef def) {
@@ -59,14 +59,24 @@ public class FlowEvent {
private final Long instanceId;
/** 任务事件带 taskId(SUBMITTED 等办件级事件为 null) */
private final Long taskId;
/** 操作人 id(谁导致本事件;回调信封提升为 operatorUserId)。可空:语义见《子应用接入流程中心规范.md》§6.4 */
private final Integer operatorId;
/** 操作人姓名快照。可空 */
private final String operatorName;
/** 事件明细(result/opinion/rejectNode/task.created 的 n/m 等),可为空 */
private final Map<String, Object> data;
public static FlowEvent of(Type type, Long instanceId) {
return new FlowEvent(type, instanceId, null, new HashMap<>());
return new FlowEvent(type, instanceId, null, null, null, new HashMap<>());
}
public static FlowEvent of(Type type, Long instanceId, Long taskId, Map<String, Object> data) {
return new FlowEvent(type, instanceId, taskId, data == null ? new HashMap<>() : data);
return new FlowEvent(type, instanceId, taskId, null, null, data == null ? new HashMap<>() : data);
}
public static FlowEvent of(Type type, Long instanceId, Long taskId,
Integer operatorId, String operatorName, Map<String, Object> data) {
return new FlowEvent(type, instanceId, taskId, operatorId, operatorName,
data == null ? new HashMap<>() : data);
}
}
@@ -31,9 +31,13 @@ public class FlowNodeAdvanceListener {
return;
}
String edgeName = (String) event.getData().get("edgeName");
// 办理人随事件 data 穿透进推进链:新 TASK_CREATED/COMPLETED 的操作人信封字段(2026-09-20)
FlowOperator operator = FlowOperator.of(
(Integer) event.getData().get("assigneeId"),
(String) event.getData().get("assigneeName"));
for (int attempt = 1; attempt <= 3; attempt++) {
try {
advanceService.tryAdvance(event.getInstanceId(), nodeId, edgeName);
advanceService.tryAdvance(event.getInstanceId(), nodeId, edgeName, operator);
return;
} catch (ObjectOptimisticLockingFailureException e) {
log.warn("[flow-center] 推进冲突重试 {}/3:instance={} node={}",
@@ -0,0 +1,23 @@
package com.yunhai.flowcenter.engine;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* 推进链携带的操作人上下文(2026-09-20 信封升级):任务办理后的放行推进在
* AFTER_COMMIT 新事务里由 FlowNodeAdvanceListener 重查执行,"是谁办的"只存在于
* TASK_HANDLED 事件 data 里,必须显式穿透 listener → tryAdvance → continueAdvance
* → activate → createTasks/complete,否则 TASK_CREATED/COMPLETED 事件拿不到操作人。
* null 语义=未穿透(发起直达路径),事件构建点回退办件发起人。
*/
@Getter
@RequiredArgsConstructor
public class FlowOperator {
private final Integer userId;
private final String userName;
public static FlowOperator of(Integer userId, String userName) {
return new FlowOperator(userId, userName);
}
}
@@ -142,7 +142,8 @@ public class FlowInstanceService {
FlwEnums.TaskStatus.WITHDRAWN, instance.getSubmitterId(), instance.getSubmitterName());
Map<String, Object> data = new HashMap<>();
data.put("result", "WITHDRAWN");
eventPublisher.publishEvent(FlowEvent.of(FlowEvent.Type.WITHDRAWN, instanceId, null, data));
eventPublisher.publishEvent(FlowEvent.of(FlowEvent.Type.WITHDRAWN, instanceId, null,
instance.getSubmitterId(), instance.getSubmitterName(), data));
}
/**
@@ -230,7 +231,8 @@ public class FlowInstanceService {
FlowDefModel model = flowEngine.parseModel(requireDef(instance.getDefId()));
Map<String, Object> data = new HashMap<>();
data.put("nodeSummary", FlowEngine.nodeSummary(model));
eventPublisher.publishEvent(FlowEvent.of(FlowEvent.Type.SUBMITTED, instance.getId(), null, data));
eventPublisher.publishEvent(FlowEvent.of(FlowEvent.Type.SUBMITTED, instance.getId(), null,
instance.getSubmitterId(), instance.getSubmitterName(), data));
}
/** 代发起结果值对象 */
@@ -6,6 +6,7 @@ import com.yunhai.flowcenter.engine.BranchScope;
import com.yunhai.flowcenter.engine.FlowDefModel;
import com.yunhai.flowcenter.engine.FlowEvent;
import com.yunhai.flowcenter.engine.FlowEngine;
import com.yunhai.flowcenter.engine.FlowOperator;
import com.yunhai.flowcenter.entity.FlwDef;
import com.yunhai.flowcenter.entity.FlwInstance;
import com.yunhai.flowcenter.entity.FlwTask;
@@ -97,7 +98,7 @@ public class FlowTaskService {
data.put("edgeName", chosenEdge);
}
eventPublisher.publishEvent(FlowEvent.of(FlowEvent.Type.TASK_HANDLED,
instance.getId(), taskId, data));
instance.getId(), taskId, task.getAssigneeId(), task.getAssigneeName(), data));
if (!approved) {
rejectFlow(instance, task, opinion);
} else if (FlowDefModel.COUNTERSIGN_ANY.equals(countersignOf(task))) {
@@ -163,7 +164,7 @@ public class FlowTaskService {
data.put("rejectTaskId", task.getId());
data.put("opinion", opinion);
eventPublisher.publishEvent(FlowEvent.of(FlowEvent.Type.REJECTED,
instance.getId(), task.getId(), data));
instance.getId(), task.getId(), task.getAssigneeId(), task.getAssigneeName(), data));
}
/**
@@ -199,7 +200,8 @@ public class FlowTaskService {
throw new BizException(ErrorCode.PARAM_ERROR, "任务已被办理或已失效,请刷新");
}
taskRepository.invalidatePendingByNode(instance.getId(), task.getNodeId());
flowEngine.returnTo(instance, def, task.getNodeId(), target);
flowEngine.returnTo(instance, def, task.getNodeId(), target,
FlowOperator.of(task.getAssigneeId(), task.getAssigneeName()));
instanceRepository.save(instance);
Map<String, Object> data = new HashMap<>();
@@ -291,7 +293,7 @@ public class FlowTaskService {
data.put("opinion", reason);
}
eventPublisher.publishEvent(FlowEvent.of(FlowEvent.Type.TRANSFERRED,
instance.getId(), newTask.getId(), data));
instance.getId(), newTask.getId(), task.getAssigneeId(), task.getAssigneeName(), data));
}
/** 待办任务守卫:存在、本校、本人办理、仍待办 */
@@ -197,6 +197,16 @@ class CallbackDispatchIT extends FlowTestSupport {
assertTrue(payload.get("instanceVersion").asInt() >= 1, "instanceVersion 应为办件乐观锁版本");
assertNotNull(payload.get("eventId"));
assertNotNull(payload.get("occurredTime"));
// 2026-09-20 信封升级:taskId + 操作人(发起直达链路 → 操作人=发起人快照)
assertEquals("201", payload.get("operatorUserId").asText(), "operatorUserId 应为发起人");
assertEquals("张三", payload.get("operatorName").asText(), "operatorName 应为发起人");
if ("flow.task.created".equals(event)) {
assertTrue(payload.get("taskId") != null && payload.get("taskId").isNumber(),
"task.created 必带顶层 taskId");
} else if ("flow.submitted".equals(event)) {
assertTrue(payload.get("taskId") == null || payload.get("taskId").isNull(),
"办件级事件 taskId 应为 null");
}
// 签名断言:Base64(HmacSHA256(secret, ts.nonce.body))
String expected = hmac(reg.getSecret(), r.timestamp, r.nonce, r.body);
assertEquals(expected, r.signature, "推送必须带合法 X-Flw-Signature");