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=bizUrl 优先(空回落 * /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 CC_SENT: notifyCc(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() + ")"); // 跳转路径:办件自带 bizUrl(业务方详情页,改造后为裸相对路径)优先——应用内审批免跳 // 流程中心,页面归属=apply 调用方(jumpApp=instance.applyAppCode,apply 时由验签身份 // 按_by-key 换得的 app_code,接收方按它查基址拼绝对地址);applyAppCode 空(老数据/ // UserBase 不可达)或未传 bizUrl 回落流程中心页(/task/{taskId}、/instance/{id}), // 页面属自己(jumpApp 留空按发送方回落)。bizType 是消息分类,不是路由键,不再冒充。 boolean useBizUrl = instance.getBizUrl() != null && !instance.getBizUrl().trim().isEmpty(); String jumpPath = useBizUrl ? instance.getBizUrl() : "/task/" + task.getId(); String jumpApp = useBizUrl ? instance.getApplyAppCode() : null; userMsgClient.sendUserMsg(instance.getSchId().intValue(), Collections.singletonList(task.getAssigneeId()), 2, "flow.task", String.valueOf(instance.getId()), title, content, jumpPath, jumpApp, "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 targets = event.getData().get("targetUserIds") == null ? Collections.emptyList() : (java.util.List) 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(), null, "returned:" + instance.getId() + ":" + instance.getVersion(), 30); } /** * 抄送(2026-09-23 NOTIFY 节点):msgType=1 通知类(待办才是 2),bizType=flow.cc、 * msgNo=cc:{ccId}(逐人逐节点一行一消息,重走重复抄送不互吞);跳转与任务到达同策略 * (bizUrl 优先,空回落办件详情)。★ 不调 todoInc——知会不占待办角标、不进待办列表。 */ private void notifyCc(FlowEvent event) { FlwInstance instance = instanceOf(event.getInstanceId()); Object ccUserId = event.getData() == null ? null : event.getData().get("ccUserId"); if (instance == null || ccUserId == null) { return; } Object nodeName = event.getData().get("nodeName"); Object ccId = event.getData().get("ccId"); String bizTitle = safe(instance.getBizTitle()); String content = "「" + bizTitle + "」抄送给您" + (nodeName == null || String.valueOf(nodeName).isEmpty() ? "" : "(" + nodeName + ")"); // 跳转与任务到达同策略:bizUrl(业务方详情页,页面归属=apply 调用方 applyAppCode)优先, // 空回落办件详情(属自己) boolean useBizUrl = instance.getBizUrl() != null && !instance.getBizUrl().trim().isEmpty(); String jumpPath = useBizUrl ? instance.getBizUrl() : "/instance/" + instance.getId(); String jumpApp = useBizUrl ? instance.getApplyAppCode() : null; userMsgClient.sendUserMsg(instance.getSchId().intValue(), Collections.singletonList((Integer) ccUserId), 1, "flow.cc", String.valueOf(instance.getId()), "您有新的抄送:" + bizTitle, content, jumpPath, jumpApp, "cc:" + ccId, 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(), null, "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(), null, "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; } }