feat(notify): 待办/抄送消息补 jumpApp 页面归属(B0 直批配套补提交)——bizUrl 落地页场景 jumpApp=instance.bizType(接收方按它查基址),未传 bizUrl 回落办理页留空按 bizType;UserMsgClient.sendUserMsg 增 jumpApp 形参;CallbackPushService/ConditionRouteService 跟随 B 系列回调前缀改造;测试补 jumpApp 断言
build / build-push (push) Successful in 49s

This commit is contained in:
chenxing
2026-09-28 19:23:29 +08:00
parent 31476ef707
commit 2f8daa12a5
6 changed files with 68 additions and 28 deletions
@@ -152,7 +152,7 @@ public class CallbackPushService {
+ " 推送连续失败 " + callbackLog.getRetryCount() + " 次,已转入持续重试(封顶 " + " 推送连续失败 " + callbackLog.getRetryCount() + " 次,已转入持续重试(封顶 "
+ capMinutes + " 分钟/次)。消费方恢复后将自动追平,无需人工干预;" + capMinutes + " 分钟/次)。消费方恢复后将自动追平,无需人工干预;"
+ "如确认该事件已无意义,请在管理端标记死信。最近错误:" + truncate(error), + "如确认该事件已无意义,请在管理端标记死信。最近错误:" + truncate(error),
null, "hang:" + callbackLog.getEventId(), null); null, null, "hang:" + callbackLog.getEventId(), null);
} catch (Exception e) { } catch (Exception e) {
log.warn("[flow-center] 挂起告警发送失败:eventId={} {}", callbackLog.getEventId(), e.toString()); log.warn("[flow-center] 挂起告警发送失败:eventId={} {}", callbackLog.getEventId(), e.toString());
} }
@@ -197,7 +197,7 @@ public class CallbackPushService {
"回调推送死信告警", "回调推送死信告警",
"办件 " + callbackLog.getInstanceId() + " 事件 " + callbackLog.getEvent() "办件 " + callbackLog.getInstanceId() + " 事件 " + callbackLog.getEvent()
+ " 推送失败进入死信:" + error, + " 推送失败进入死信:" + error,
null, "dead:" + callbackLog.getEventId(), null); null, null, "dead:" + callbackLog.getEventId(), null);
} catch (Exception e) { } catch (Exception e) {
log.warn("[flow-center] 死信告警发送失败:eventId={} {}", callbackLog.getEventId(), e.toString()); log.warn("[flow-center] 死信告警发送失败:eventId={} {}", callbackLog.getEventId(), e.toString());
} }
@@ -130,7 +130,7 @@ public class ConditionRouteService {
+ (node.name == null || node.name.isEmpty() ? node.id : node.name) + (node.name == null || node.name.isEmpty() ? node.id : node.name)
+ "」路由判断失败已挂起,请在流程管理端重跑路由。出口候选:" + "」路由判断失败已挂起,请在流程管理端重跑路由。出口候选:"
+ outlets + "。最近错误:" + truncate(error), + outlets + "。最近错误:" + truncate(error),
null, "route-hang:" + instance.getId() + ":" + node.id + ":" + attempt, null); null, null, "route-hang:" + instance.getId() + ":" + node.id + ":" + attempt, null);
} catch (Exception e) { } catch (Exception e) {
log.warn("[flow-center] 路由挂起告警发送失败:instance={} {}", instance.getId(), e.toString()); log.warn("[flow-center] 路由挂起告警发送失败:instance={} {}", instance.getId(), e.toString());
} }
@@ -64,10 +64,16 @@ public class UserMsgClient {
return directoryResolved; return directoryResolved;
} }
/** 发送站内信(content 必填;msgNo 幂等,重发安全) */ /**
* 发送站内信(content 必填;msgNo 幂等,重发安全)。
*
* @param jumpApp 跳转页面归属应用(可空):页面属业务方(bizUrl 落地页)时填业务方应用名
* (=instance.bizType),接收方优先按它查基址;页面属流程中心自己(/task、/instance)
* 传 null,由接收方按 bizType 回落
*/
public void sendUserMsg(Integer schId, List<Integer> userIds, Integer msgType, String bizType, public void sendUserMsg(Integer schId, List<Integer> userIds, Integer msgType, String bizType,
String bizId, String title, String content, String jumpPath, String msgNo, String bizId, String title, String content, String jumpPath, String jumpApp,
Integer expireDays) { String msgNo, Integer expireDays) {
if (!enabled || userIds == null || userIds.isEmpty()) { if (!enabled || userIds == null || userIds.isEmpty()) {
return; return;
} }
@@ -82,6 +88,7 @@ public class UserMsgClient {
body.setJumpType(1); body.setJumpType(1);
body.setJumpPath(jumpPath); body.setJumpPath(jumpPath);
} }
body.setJumpApp(jumpApp);
body.setMsgNo(msgNo); body.setMsgNo(msgNo);
body.setSchId(schId); body.setSchId(schId);
body.setExpireDays(expireDays); body.setExpireDays(expireDays);
@@ -72,14 +72,16 @@ public class UserMsgNotifier {
String title = "您有新的待办:" + safe(instance.getBizTitle()); String title = "您有新的待办:" + safe(instance.getBizTitle());
String content = "「" + safe(instance.getBizTitle()) + "」等待您审批" String content = "「" + safe(instance.getBizTitle()) + "」等待您审批"
+ (task.getNodeName() == null ? "" : "(" + task.getNodeName() + ")"); + (task.getNodeName() == null ? "" : "(" + task.getNodeName() + ")");
// 跳转路径:办件自带 bizUrl(应用详情页)优先——应用内审批免跳流程中心; // 跳转路径:办件自带 bizUrl(业务方详情页,改造后为裸相对路径)优先——应用内审批免跳
// 未传 bizUrl 回落流程中心办理页 /task/{taskId} // 流程中心,页面归属=业务方(jumpApp=instance.bizType,接收方按它查基址拼绝对地址);
String jumpPath = instance.getBizUrl() == null || instance.getBizUrl().trim().isEmpty() // 未传 bizUrl 回落流程中心办理页 /task/{taskId},页面属自己(jumpApp 留空按 bizType 回落)
? "/task/" + task.getId() : instance.getBizUrl(); boolean useBizUrl = instance.getBizUrl() != null && !instance.getBizUrl().trim().isEmpty();
String jumpPath = useBizUrl ? instance.getBizUrl() : "/task/" + task.getId();
String jumpApp = useBizUrl ? instance.getBizType() : null;
userMsgClient.sendUserMsg(instance.getSchId().intValue(), userMsgClient.sendUserMsg(instance.getSchId().intValue(),
Collections.singletonList(task.getAssigneeId()), 2, Collections.singletonList(task.getAssigneeId()), 2,
"flow.task", String.valueOf(instance.getId()), title, content, "flow.task", String.valueOf(instance.getId()), title, content,
jumpPath, "task:" + task.getId(), 30); jumpPath, jumpApp, "task:" + task.getId(), 30);
userMsgClient.todoInc(instance.getSchId().intValue(), userMsgClient.todoInc(instance.getSchId().intValue(),
Collections.singletonList(task.getAssigneeId()), 1); Collections.singletonList(task.getAssigneeId()), 1);
} }
@@ -120,7 +122,7 @@ public class UserMsgNotifier {
userMsgClient.sendUserMsg(instance.getSchId().intValue(), targets, 1, userMsgClient.sendUserMsg(instance.getSchId().intValue(), targets, 1,
"flow.returned", String.valueOf(instance.getId()), "flow.returned", String.valueOf(instance.getId()),
"办件被退回:" + safe(instance.getBizTitle()), content.toString(), "办件被退回:" + safe(instance.getBizTitle()), content.toString(),
"/instance/" + instance.getId(), "/instance/" + instance.getId(), null,
"returned:" + instance.getId() + ":" + instance.getVersion(), 30); "returned:" + instance.getId() + ":" + instance.getVersion(), 30);
} }
@@ -141,13 +143,15 @@ public class UserMsgNotifier {
String content = "「" + bizTitle + "」抄送给您" String content = "「" + bizTitle + "」抄送给您"
+ (nodeName == null || String.valueOf(nodeName).isEmpty() + (nodeName == null || String.valueOf(nodeName).isEmpty()
? "" : "(" + nodeName + ")"); ? "" : "(" + nodeName + ")");
String jumpPath = instance.getBizUrl() == null || instance.getBizUrl().trim().isEmpty() // 跳转与任务到达同策略:bizUrl(业务方详情页,页面归属=业务方)优先,空回落办件详情(属自己)
? "/instance/" + instance.getId() : instance.getBizUrl(); boolean useBizUrl = instance.getBizUrl() != null && !instance.getBizUrl().trim().isEmpty();
String jumpPath = useBizUrl ? instance.getBizUrl() : "/instance/" + instance.getId();
String jumpApp = useBizUrl ? instance.getBizType() : null;
userMsgClient.sendUserMsg(instance.getSchId().intValue(), userMsgClient.sendUserMsg(instance.getSchId().intValue(),
Collections.singletonList((Integer) ccUserId), 1, Collections.singletonList((Integer) ccUserId), 1,
"flow.cc", String.valueOf(instance.getId()), "flow.cc", String.valueOf(instance.getId()),
"您有新的抄送:" + bizTitle, content, "您有新的抄送:" + bizTitle, content,
jumpPath, "cc:" + ccId, 30); jumpPath, jumpApp, "cc:" + ccId, 30);
} }
private void notifyResult(FlowEvent event) { private void notifyResult(FlowEvent event) {
@@ -181,7 +185,7 @@ public class UserMsgNotifier {
Collections.singletonList(instance.getSubmitterId()), 1, Collections.singletonList(instance.getSubmitterId()), 1,
"flow.result", String.valueOf(instance.getId()), "flow.result", String.valueOf(instance.getId()),
"办件状态更新:" + bizTitle, content.toString(), "办件状态更新:" + bizTitle, content.toString(),
"/instance/" + instance.getId(), "/instance/" + instance.getId(), null,
"result:" + instance.getId() + ":" + instance.getVersion(), 30); "result:" + instance.getId() + ":" + instance.getVersion(), 30);
} }
@@ -199,7 +203,7 @@ public class UserMsgNotifier {
Collections.singletonList(task.getAssigneeId()), 1, Collections.singletonList(task.getAssigneeId()), 1,
"flow.urge", String.valueOf(instance.getId()), "flow.urge", String.valueOf(instance.getId()),
"催办提醒:" + safe(instance.getBizTitle()), content, "催办提醒:" + safe(instance.getBizTitle()), content,
"/task/" + task.getId(), "urge:" + task.getId() + ":" + count, 7); "/task/" + task.getId(), null, "urge:" + task.getId() + ":" + count, 7);
} }
private FlwInstance instanceOf(Long instanceId) { private FlwInstance instanceOf(Long instanceId) {
@@ -103,8 +103,8 @@ class CallbackDispatchIT extends FlowTestSupport {
@Override @Override
public void sendUserMsg(Integer schId, List<Integer> userIds, Integer msgType, String bizType, public void sendUserMsg(Integer schId, List<Integer> userIds, Integer msgType, String bizType,
String bizId, String title, String content, String jumpPath, String msgNo, String bizId, String title, String content, String jumpPath, String jumpApp,
Integer expireDays) { String msgNo, Integer expireDays) {
if (msgNo != null && msgNo.startsWith("hang:")) { if (msgNo != null && msgNo.startsWith("hang:")) {
hangAlerts.incrementAndGet(); hangAlerts.incrementAndGet();
} }
@@ -13,14 +13,16 @@ import java.util.List;
import java.util.Optional; import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
/** /**
* 待办通知 jumpPath 两态(第三步 T-C3):办件 bizUrl 非空=直达应用详情页(应用内审批免跳 * 待办通知 jumpPath/jumpApp 语义(自指地址优化 Phase 2):办件 bizUrl 非空=直达业务方详情页,
* 流程中心);空/空白=回落流程中心办理页 /task/{taskId}。 * 页面归属 jumpApp=instance.bizType(接收方按它查基址);空/空白=回落流程中心办理页
* /task/{taskId},页面属自己(jumpApp=null,接收方按 bizType 回落)。
*/ */
class UserMsgNotifierBizUrlTest { class UserMsgNotifierBizUrlTest {
@@ -29,9 +31,10 @@ class UserMsgNotifierBizUrlTest {
private CapturingUserMsgClient userMsg; private CapturingUserMsgClient userMsg;
private UserMsgNotifier notifier; private UserMsgNotifier notifier;
/** 捕获 jumpPath 的哑客户端 */ /** 捕获 jumpPath/jumpApp 的哑客户端 */
static class CapturingUserMsgClient extends UserMsgClient { static class CapturingUserMsgClient extends UserMsgClient {
String lastJumpPath; String lastJumpPath;
String lastJumpApp;
CapturingUserMsgClient() { CapturingUserMsgClient() {
super(null, null, false, ""); super(null, null, false, "");
@@ -39,9 +42,10 @@ class UserMsgNotifierBizUrlTest {
@Override @Override
public void sendUserMsg(Integer schId, List<Integer> userIds, Integer msgType, String bizType, public void sendUserMsg(Integer schId, List<Integer> userIds, Integer msgType, String bizType,
String bizId, String title, String content, String jumpPath, String msgNo, String bizId, String title, String content, String jumpPath, String jumpApp,
Integer expireDays) { String msgNo, Integer expireDays) {
this.lastJumpPath = jumpPath; this.lastJumpPath = jumpPath;
this.lastJumpApp = jumpApp;
} }
@Override @Override
@@ -59,7 +63,7 @@ class UserMsgNotifierBizUrlTest {
} }
@Test @Test
void bizUrlPresent_jumpsToAppDetail() { void bizUrlPresent_relativePath_declaresBizOwner() {
FlwTask task = new FlwTask(); FlwTask task = new FlwTask();
task.setId(77L); task.setId(77L);
task.setAssigneeId(101); task.setAssigneeId(101);
@@ -68,17 +72,41 @@ class UserMsgNotifierBizUrlTest {
instance.setId(5L); instance.setId(5L);
instance.setSchId(1L); instance.setSchId(1L);
instance.setBizTitle("张三的事项"); instance.setBizTitle("张三的事项");
instance.setBizUrl("http://base.yunhaixingtong.com/notice/front/detail/27"); instance.setBizType("notice");
instance.setBizUrl("/list/detail?serid=27");
when(taskRepository.findById(77L)).thenReturn(Optional.of(task)); when(taskRepository.findById(77L)).thenReturn(Optional.of(task));
when(instanceRepository.findById(5L)).thenReturn(Optional.of(instance)); when(instanceRepository.findById(5L)).thenReturn(Optional.of(instance));
notifier.onFlowEvent(FlowEvent.of(FlowEvent.Type.TASK_CREATED, 5L, 77L, 101, "甲", null)); notifier.onFlowEvent(FlowEvent.of(FlowEvent.Type.TASK_CREATED, 5L, 77L, 101, "甲", null));
assertEquals("http://base.yunhaixingtong.com/notice/front/detail/27", userMsg.lastJumpPath); assertEquals("/list/detail?serid=27", userMsg.lastJumpPath);
assertEquals("notice", userMsg.lastJumpApp);
}
/** 存量兼容:业务方尚未升级仍传绝对 URL 时原样透传,jumpApp 照常按归属声明 */
@Test
void bizUrlLegacyAbsolute_passedThroughWithOwner() {
FlwTask task = new FlwTask();
task.setId(79L);
task.setAssigneeId(101);
task.setInstanceId(7L);
FlwInstance instance = new FlwInstance();
instance.setId(7L);
instance.setSchId(1L);
instance.setBizTitle("王五的事项");
instance.setBizType("notice");
instance.setBizUrl("http://base.yunhaixingtong.com/notice/#/list/detail?serid=27");
when(taskRepository.findById(79L)).thenReturn(Optional.of(task));
when(instanceRepository.findById(7L)).thenReturn(Optional.of(instance));
notifier.onFlowEvent(FlowEvent.of(FlowEvent.Type.TASK_CREATED, 7L, 79L, 101, "丙", null));
assertEquals("http://base.yunhaixingtong.com/notice/#/list/detail?serid=27", userMsg.lastJumpPath);
assertEquals("notice", userMsg.lastJumpApp);
} }
@Test @Test
void bizUrlBlank_fallsBackToTaskPage() { void bizUrlBlank_fallsBackToTaskPage_withoutOwner() {
FlwTask task = new FlwTask(); FlwTask task = new FlwTask();
task.setId(78L); task.setId(78L);
task.setAssigneeId(101); task.setAssigneeId(101);
@@ -94,5 +122,6 @@ class UserMsgNotifierBizUrlTest {
notifier.onFlowEvent(FlowEvent.of(FlowEvent.Type.TASK_CREATED, 6L, 78L, 101, "乙", null)); notifier.onFlowEvent(FlowEvent.of(FlowEvent.Type.TASK_CREATED, 6L, 78L, 101, "乙", null));
assertEquals("/task/78", userMsg.lastJumpPath); assertEquals("/task/78", userMsg.lastJumpPath);
assertNull(userMsg.lastJumpApp);
} }
} }