Files
flow-center-service/src/main/java/com/yunhai/flowcenter/notify/UserMsgNotifier.java
T
chenxing b4856a480d
build / build-push (push) Successful in 49s
fix(msg): 消息 jump_app 换源 apply 调用方 app_code 快照,替代 bizType 冒充
P1/P2 联合修复(消息归属 + jump_app 值域污染):
- FlwInstance 新增 apply_app_key/apply_app_code 双列快照(自动 DDL)
- apply 从验签上下文(yh.app.auth)取调用方身份,by-key 一次换得 app_code
  (域间边界只在 apply 转换一次;查无/不可达降级 null,不阻断 apply)
- UserMsgNotifier 两处 jumpApp 改源 applyAppCode——bizType 是消息分类
  不是路由键;降级空时接收方按发送方回落(现状行为)
- FlowDefAdminService.save appKey 必填化(2026-10-12 拍板):新建必须
  显式传;升版本/直改可继承源行,源行为空必须补填(存量 19 条 NULL 随编辑收敛)
- 单测:SaveTest 新增必填拦截 3 用例、BizUrlTest 换源断言+降级用例(19 绿)
- E2E(8085+UserBase):100617->apply_app_code=notice 正向落库、
  100018(app_code NULL)->降级成功、错签名 401,三断言全过
2026-10-12 10:44:07 +08:00

220 lines
11 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=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<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(), 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;
}
}