diff --git a/src/main/java/com/yunhai/flowcenter/callback/CallbackDispatcher.java b/src/main/java/com/yunhai/flowcenter/callback/CallbackDispatcher.java index d57fe52..bec6659 100644 --- a/src/main/java/com/yunhai/flowcenter/callback/CallbackDispatcher.java +++ b/src/main/java/com/yunhai/flowcenter/callback/CallbackDispatcher.java @@ -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 data = new HashMap<>(); if (event.getData() != null) { data.putAll(event.getData()); diff --git a/src/main/java/com/yunhai/flowcenter/engine/FlowAdvanceService.java b/src/main/java/com/yunhai/flowcenter/engine/FlowAdvanceService.java index 294ecf7..08f14ca 100644 --- a/src/main/java/com/yunhai/flowcenter/engine/FlowAdvanceService.java +++ b/src/main/java/com/yunhai/flowcenter/engine/FlowAdvanceService.java @@ -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; } diff --git a/src/main/java/com/yunhai/flowcenter/engine/FlowEngine.java b/src/main/java/com/yunhai/flowcenter/engine/FlowEngine.java index a7719cb..d89a74f 100644 --- a/src/main/java/com/yunhai/flowcenter/engine/FlowEngine.java +++ b/src/main/java/com/yunhai/flowcenter/engine/FlowEngine.java @@ -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 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 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 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 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) { diff --git a/src/main/java/com/yunhai/flowcenter/engine/FlowEvent.java b/src/main/java/com/yunhai/flowcenter/engine/FlowEvent.java index 0692b69..ab396e6 100644 --- a/src/main/java/com/yunhai/flowcenter/engine/FlowEvent.java +++ b/src/main/java/com/yunhai/flowcenter/engine/FlowEvent.java @@ -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 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 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 data) { + return new FlowEvent(type, instanceId, taskId, operatorId, operatorName, + data == null ? new HashMap<>() : data); } } diff --git a/src/main/java/com/yunhai/flowcenter/engine/FlowNodeAdvanceListener.java b/src/main/java/com/yunhai/flowcenter/engine/FlowNodeAdvanceListener.java index 334de02..1ac00c3 100644 --- a/src/main/java/com/yunhai/flowcenter/engine/FlowNodeAdvanceListener.java +++ b/src/main/java/com/yunhai/flowcenter/engine/FlowNodeAdvanceListener.java @@ -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={}", diff --git a/src/main/java/com/yunhai/flowcenter/engine/FlowOperator.java b/src/main/java/com/yunhai/flowcenter/engine/FlowOperator.java new file mode 100644 index 0000000..d1e4d24 --- /dev/null +++ b/src/main/java/com/yunhai/flowcenter/engine/FlowOperator.java @@ -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); + } +} diff --git a/src/main/java/com/yunhai/flowcenter/service/FlowInstanceService.java b/src/main/java/com/yunhai/flowcenter/service/FlowInstanceService.java index bae6da8..0bf9726 100644 --- a/src/main/java/com/yunhai/flowcenter/service/FlowInstanceService.java +++ b/src/main/java/com/yunhai/flowcenter/service/FlowInstanceService.java @@ -142,7 +142,8 @@ public class FlowInstanceService { FlwEnums.TaskStatus.WITHDRAWN, instance.getSubmitterId(), instance.getSubmitterName()); Map 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 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)); } /** 代发起结果值对象 */ diff --git a/src/main/java/com/yunhai/flowcenter/service/FlowTaskService.java b/src/main/java/com/yunhai/flowcenter/service/FlowTaskService.java index 96fbb9e..e397b50 100644 --- a/src/main/java/com/yunhai/flowcenter/service/FlowTaskService.java +++ b/src/main/java/com/yunhai/flowcenter/service/FlowTaskService.java @@ -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 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)); } /** 待办任务守卫:存在、本校、本人办理、仍待办 */ diff --git a/src/test/java/com/yunhai/flowcenter/callback/CallbackDispatchIT.java b/src/test/java/com/yunhai/flowcenter/callback/CallbackDispatchIT.java index b708078..2e8cfe2 100644 --- a/src/test/java/com/yunhai/flowcenter/callback/CallbackDispatchIT.java +++ b/src/test/java/com/yunhai/flowcenter/callback/CallbackDispatchIT.java @@ -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");