Files
flow-center-service/src/main/java/com/yunhai/flowcenter/notify/UserMsgNotifier.java
T
chenxing b8441065d0
build / build-push (push) Successful in 5m11s
feat: 引擎多出口路由/退回/移交/或签补齐 + 定义 layout 坐标
- 多出口: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 绿
2026-09-16 22:23:02 +08:00

179 lines
7.9 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.yunhai.flowcenter.notify;
import com.yunhai.flowcenter.engine.FlowEvent;
import com.yunhai.flowcenter.entity.FlwInstance;
import com.yunhai.flowcenter.entity.FlwTask;
import com.yunhai.flowcenter.repository.FlwInstanceRepository;
import com.yunhai.flowcenter.repository.FlwTaskRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
import java.util.Collections;
/**
* 站内信/待办角标通知(概览 §七 契约,回调通知"业务系统"、本类通知"人",两通道互不替代):
* - 任务到达:msgType=2、bizType=flow.task、msgNo=task:{taskId}、jumpPath=/task/{taskId}、todo +1
* - 任务被办理:todo -1(办理人)
* - 完成/拒绝/撤回:msgType=1、bizType=flow.result、content 必填(结果+意见)、msgNo 带 version
* (重提重启多轮终态互不幂等吞掉)
* - 催办:bizType=flow.urge、msgNo=urge:{taskId}:{第N次}
* 全部在 AFTER_COMMIT 监听器里发送,失败不阻断(UserMsgClient 内部容错)。
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class UserMsgNotifier {
private final UserMsgClient userMsgClient;
private final FlwTaskRepository taskRepository;
private final FlwInstanceRepository instanceRepository;
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true)
public void onFlowEvent(FlowEvent event) {
switch (event.getType()) {
case TASK_CREATED:
notifyTaskCreated(event);
break;
case TASK_HANDLED:
notifyTaskHandled(event);
break;
case RETURNED:
notifyReturned(event);
break;
case TRANSFERRED:
notifyTaskCreated(event);
break;
case COMPLETED:
case REJECTED:
case WITHDRAWN:
notifyResult(event);
break;
case URGE:
notifyUrge(event);
break;
default:
break;
}
}
private void notifyTaskCreated(FlowEvent event) {
FlwTask task = taskRepository.findById(event.getTaskId()).orElse(null);
FlwInstance instance = instanceOf(event.getInstanceId());
if (task == null || instance == null) {
return;
}
String title = "您有新的待办:" + safe(instance.getBizTitle());
String content = "「" + safe(instance.getBizTitle()) + "」等待您审批"
+ (task.getNodeName() == null ? "" : "(" + task.getNodeName() + ")");
userMsgClient.sendUserMsg(instance.getSchId().intValue(),
Collections.singletonList(task.getAssigneeId()), 2,
"flow.task", String.valueOf(instance.getId()), title, content,
"/task/" + task.getId(), "task:" + task.getId(), 30);
userMsgClient.todoInc(instance.getSchId().intValue(),
Collections.singletonList(task.getAssigneeId()), 1);
}
private void notifyTaskHandled(FlowEvent event) {
FlwTask task = taskRepository.findById(event.getTaskId()).orElse(null);
FlwInstance instance = instanceOf(event.getInstanceId());
if (task == null || instance == null) {
return;
}
userMsgClient.todoInc(instance.getSchId().intValue(),
Collections.singletonList(task.getAssigneeId()), -1);
}
/**
* 退回:通知被退回节点的全部新待办人(RETURNED 事件 data.targetNodeIds 由服务层带上——
* 退回重建任务在事件发布前已提交,这里按节点查待办即可);todo 角标由 notifyTaskCreated
* 通用链路在引擎重建任务时已发过,这里只补一条带退回意见的消息。
*/
private void notifyReturned(FlowEvent event) {
FlwInstance instance = instanceOf(event.getInstanceId());
if (instance == null || event.getData() == null) {
return;
}
Object fromNode = event.getData().get("fromNodeName");
Object opinion = event.getData().get("opinion");
StringBuilder content = new StringBuilder("「").append(safe(instance.getBizTitle())).append("」")
.append("被退回(").append(fromNode == null ? "" : fromNode).append(")");
if (opinion != null && !String.valueOf(opinion).isEmpty()) {
content.append(",意见:").append(opinion);
}
java.util.List<Integer> targets = event.getData().get("targetUserIds") == null
? Collections.emptyList()
: (java.util.List<Integer>) event.getData().get("targetUserIds");
if (targets.isEmpty()) {
return;
}
userMsgClient.sendUserMsg(instance.getSchId().intValue(), targets, 1,
"flow.returned", String.valueOf(instance.getId()),
"办件被退回:" + safe(instance.getBizTitle()), content.toString(),
"/instance/" + instance.getId(),
"returned:" + instance.getId() + ":" + instance.getVersion(), 30);
}
private void notifyResult(FlowEvent event) {
FlwInstance instance = instanceOf(event.getInstanceId());
if (instance == null) {
return;
}
Object result = event.getData() == null ? null : event.getData().get("result");
String outcome;
String bizTitle = safe(instance.getBizTitle());
switch (event.getType()) {
case COMPLETED:
outcome = "已通过审批";
break;
case REJECTED:
outcome = "审批被拒绝";
break;
default:
outcome = "已撤回";
break;
}
StringBuilder content = new StringBuilder("您提交的「" + bizTitle + "」" + outcome);
if (result != null) {
content.append("(结果:").append(result).append(")");
}
Object opinion = event.getData() == null ? null : event.getData().get("opinion");
if (opinion != null && !String.valueOf(opinion).isEmpty()) {
content.append(",意见:").append(opinion);
}
userMsgClient.sendUserMsg(instance.getSchId().intValue(),
Collections.singletonList(instance.getSubmitterId()), 1,
"flow.result", String.valueOf(instance.getId()),
"办件状态更新:" + bizTitle, content.toString(),
"/instance/" + instance.getId(),
"result:" + instance.getId() + ":" + instance.getVersion(), 30);
}
private void notifyUrge(FlowEvent event) {
FlwTask task = taskRepository.findById(event.getTaskId()).orElse(null);
FlwInstance instance = instanceOf(event.getInstanceId());
if (task == null || instance == null) {
return;
}
int count = task.getUrgeCount() == null ? 1 : task.getUrgeCount();
String content = "「" + safe(instance.getBizTitle()) + "」任务("
+ (task.getNodeName() == null ? task.getNodeId() : task.getNodeName())
+ ")被催办(第 " + count + " 次),请尽快办理";
userMsgClient.sendUserMsg(instance.getSchId().intValue(),
Collections.singletonList(task.getAssigneeId()), 1,
"flow.urge", String.valueOf(instance.getId()),
"催办提醒:" + safe(instance.getBizTitle()), content,
"/task/" + task.getId(), "urge:" + task.getId() + ":" + count, 7);
}
private FlwInstance instanceOf(Long instanceId) {
return instanceId == null ? null : instanceRepository.findById(instanceId).orElse(null);
}
private static String safe(String s) {
return s == null ? "" : s;
}
}