From 2f8daa12a5114931d8a1ceefdd07fa5344c01ad0 Mon Sep 17 00:00:00 2001 From: chenxing Date: Mon, 28 Sep 2026 19:23:29 +0800 Subject: [PATCH] =?UTF-8?q?feat(notify):=20=E5=BE=85=E5=8A=9E/=E6=8A=84?= =?UTF-8?q?=E9=80=81=E6=B6=88=E6=81=AF=E8=A1=A5=20jumpApp=20=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E5=BD=92=E5=B1=9E=EF=BC=88B0=20=E7=9B=B4=E6=89=B9?= =?UTF-8?q?=E9=85=8D=E5=A5=97=E8=A1=A5=E6=8F=90=E4=BA=A4=EF=BC=89=E2=80=94?= =?UTF-8?q?=E2=80=94bizUrl=20=E8=90=BD=E5=9C=B0=E9=A1=B5=E5=9C=BA=E6=99=AF?= =?UTF-8?q?=20jumpApp=3Dinstance.bizType=EF=BC=88=E6=8E=A5=E6=94=B6?= =?UTF-8?q?=E6=96=B9=E6=8C=89=E5=AE=83=E6=9F=A5=E5=9F=BA=E5=9D=80=EF=BC=89?= =?UTF-8?q?=EF=BC=8C=E6=9C=AA=E4=BC=A0=20bizUrl=20=E5=9B=9E=E8=90=BD?= =?UTF-8?q?=E5=8A=9E=E7=90=86=E9=A1=B5=E7=95=99=E7=A9=BA=E6=8C=89=20bizTyp?= =?UTF-8?q?e=EF=BC=9BUserMsgClient.sendUserMsg=20=E5=A2=9E=20jumpApp=20?= =?UTF-8?q?=E5=BD=A2=E5=8F=82=EF=BC=9BCallbackPushService/ConditionRouteSe?= =?UTF-8?q?rvice=20=E8=B7=9F=E9=9A=8F=20B=20=E7=B3=BB=E5=88=97=E5=9B=9E?= =?UTF-8?q?=E8=B0=83=E5=89=8D=E7=BC=80=E6=94=B9=E9=80=A0=EF=BC=9B=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E8=A1=A5=20jumpApp=20=E6=96=AD=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../callback/CallbackPushService.java | 4 +- .../engine/ConditionRouteService.java | 2 +- .../flowcenter/notify/UserMsgClient.java | 13 +++-- .../flowcenter/notify/UserMsgNotifier.java | 26 +++++----- .../callback/CallbackDispatchIT.java | 4 +- .../notify/UserMsgNotifierBizUrlTest.java | 47 +++++++++++++++---- 6 files changed, 68 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/yunhai/flowcenter/callback/CallbackPushService.java b/src/main/java/com/yunhai/flowcenter/callback/CallbackPushService.java index f0164be..c386d77 100644 --- a/src/main/java/com/yunhai/flowcenter/callback/CallbackPushService.java +++ b/src/main/java/com/yunhai/flowcenter/callback/CallbackPushService.java @@ -152,7 +152,7 @@ public class CallbackPushService { + " 推送连续失败 " + callbackLog.getRetryCount() + " 次,已转入持续重试(封顶 " + capMinutes + " 分钟/次)。消费方恢复后将自动追平,无需人工干预;" + "如确认该事件已无意义,请在管理端标记死信。最近错误:" + truncate(error), - null, "hang:" + callbackLog.getEventId(), null); + null, null, "hang:" + callbackLog.getEventId(), null); } catch (Exception e) { log.warn("[flow-center] 挂起告警发送失败:eventId={} {}", callbackLog.getEventId(), e.toString()); } @@ -197,7 +197,7 @@ public class CallbackPushService { "回调推送死信告警", "办件 " + callbackLog.getInstanceId() + " 事件 " + callbackLog.getEvent() + " 推送失败进入死信:" + error, - null, "dead:" + callbackLog.getEventId(), null); + null, null, "dead:" + callbackLog.getEventId(), null); } catch (Exception e) { log.warn("[flow-center] 死信告警发送失败:eventId={} {}", callbackLog.getEventId(), e.toString()); } diff --git a/src/main/java/com/yunhai/flowcenter/engine/ConditionRouteService.java b/src/main/java/com/yunhai/flowcenter/engine/ConditionRouteService.java index 289f011..ee4b56f 100644 --- a/src/main/java/com/yunhai/flowcenter/engine/ConditionRouteService.java +++ b/src/main/java/com/yunhai/flowcenter/engine/ConditionRouteService.java @@ -130,7 +130,7 @@ public class ConditionRouteService { + (node.name == null || node.name.isEmpty() ? node.id : node.name) + "」路由判断失败已挂起,请在流程管理端重跑路由。出口候选:" + 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) { log.warn("[flow-center] 路由挂起告警发送失败:instance={} {}", instance.getId(), e.toString()); } diff --git a/src/main/java/com/yunhai/flowcenter/notify/UserMsgClient.java b/src/main/java/com/yunhai/flowcenter/notify/UserMsgClient.java index 6c421aa..facedbe 100644 --- a/src/main/java/com/yunhai/flowcenter/notify/UserMsgClient.java +++ b/src/main/java/com/yunhai/flowcenter/notify/UserMsgClient.java @@ -64,10 +64,16 @@ public class UserMsgClient { return directoryResolved; } - /** 发送站内信(content 必填;msgNo 幂等,重发安全) */ + /** + * 发送站内信(content 必填;msgNo 幂等,重发安全)。 + * + * @param jumpApp 跳转页面归属应用(可空):页面属业务方(bizUrl 落地页)时填业务方应用名 + * (=instance.bizType),接收方优先按它查基址;页面属流程中心自己(/task、/instance) + * 传 null,由接收方按 bizType 回落 + */ public void sendUserMsg(Integer schId, List userIds, Integer msgType, String bizType, - String bizId, String title, String content, String jumpPath, String msgNo, - Integer expireDays) { + String bizId, String title, String content, String jumpPath, String jumpApp, + String msgNo, Integer expireDays) { if (!enabled || userIds == null || userIds.isEmpty()) { return; } @@ -82,6 +88,7 @@ public class UserMsgClient { body.setJumpType(1); body.setJumpPath(jumpPath); } + body.setJumpApp(jumpApp); body.setMsgNo(msgNo); body.setSchId(schId); body.setExpireDays(expireDays); diff --git a/src/main/java/com/yunhai/flowcenter/notify/UserMsgNotifier.java b/src/main/java/com/yunhai/flowcenter/notify/UserMsgNotifier.java index 93d54ca..a7704de 100644 --- a/src/main/java/com/yunhai/flowcenter/notify/UserMsgNotifier.java +++ b/src/main/java/com/yunhai/flowcenter/notify/UserMsgNotifier.java @@ -72,14 +72,16 @@ public class UserMsgNotifier { String title = "您有新的待办:" + safe(instance.getBizTitle()); String content = "「" + safe(instance.getBizTitle()) + "」等待您审批" + (task.getNodeName() == null ? "" : "(" + task.getNodeName() + ")"); - // 跳转路径:办件自带 bizUrl(应用详情页)优先——应用内审批免跳流程中心; - // 未传 bizUrl 回落流程中心办理页 /task/{taskId} - String jumpPath = instance.getBizUrl() == null || instance.getBizUrl().trim().isEmpty() - ? "/task/" + task.getId() : instance.getBizUrl(); + // 跳转路径:办件自带 bizUrl(业务方详情页,改造后为裸相对路径)优先——应用内审批免跳 + // 流程中心,页面归属=业务方(jumpApp=instance.bizType,接收方按它查基址拼绝对地址); + // 未传 bizUrl 回落流程中心办理页 /task/{taskId},页面属自己(jumpApp 留空按 bizType 回落) + 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(), Collections.singletonList(task.getAssigneeId()), 2, "flow.task", String.valueOf(instance.getId()), title, content, - jumpPath, "task:" + task.getId(), 30); + jumpPath, jumpApp, "task:" + task.getId(), 30); userMsgClient.todoInc(instance.getSchId().intValue(), Collections.singletonList(task.getAssigneeId()), 1); } @@ -120,7 +122,7 @@ public class UserMsgNotifier { userMsgClient.sendUserMsg(instance.getSchId().intValue(), targets, 1, "flow.returned", String.valueOf(instance.getId()), "办件被退回:" + safe(instance.getBizTitle()), content.toString(), - "/instance/" + instance.getId(), + "/instance/" + instance.getId(), null, "returned:" + instance.getId() + ":" + instance.getVersion(), 30); } @@ -141,13 +143,15 @@ public class UserMsgNotifier { String content = "「" + bizTitle + "」抄送给您" + (nodeName == null || String.valueOf(nodeName).isEmpty() ? "" : "(" + nodeName + ")"); - String jumpPath = instance.getBizUrl() == null || instance.getBizUrl().trim().isEmpty() - ? "/instance/" + instance.getId() : instance.getBizUrl(); + // 跳转与任务到达同策略:bizUrl(业务方详情页,页面归属=业务方)优先,空回落办件详情(属自己) + 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(), Collections.singletonList((Integer) ccUserId), 1, "flow.cc", String.valueOf(instance.getId()), "您有新的抄送:" + bizTitle, content, - jumpPath, "cc:" + ccId, 30); + jumpPath, jumpApp, "cc:" + ccId, 30); } private void notifyResult(FlowEvent event) { @@ -181,7 +185,7 @@ public class UserMsgNotifier { Collections.singletonList(instance.getSubmitterId()), 1, "flow.result", String.valueOf(instance.getId()), "办件状态更新:" + bizTitle, content.toString(), - "/instance/" + instance.getId(), + "/instance/" + instance.getId(), null, "result:" + instance.getId() + ":" + instance.getVersion(), 30); } @@ -199,7 +203,7 @@ public class UserMsgNotifier { Collections.singletonList(task.getAssigneeId()), 1, "flow.urge", String.valueOf(instance.getId()), "催办提醒:" + 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) { diff --git a/src/test/java/com/yunhai/flowcenter/callback/CallbackDispatchIT.java b/src/test/java/com/yunhai/flowcenter/callback/CallbackDispatchIT.java index 2e8cfe2..5728e74 100644 --- a/src/test/java/com/yunhai/flowcenter/callback/CallbackDispatchIT.java +++ b/src/test/java/com/yunhai/flowcenter/callback/CallbackDispatchIT.java @@ -103,8 +103,8 @@ class CallbackDispatchIT extends FlowTestSupport { @Override public void sendUserMsg(Integer schId, List userIds, Integer msgType, String bizType, - String bizId, String title, String content, String jumpPath, String msgNo, - Integer expireDays) { + String bizId, String title, String content, String jumpPath, String jumpApp, + String msgNo, Integer expireDays) { if (msgNo != null && msgNo.startsWith("hang:")) { hangAlerts.incrementAndGet(); } diff --git a/src/test/java/com/yunhai/flowcenter/notify/UserMsgNotifierBizUrlTest.java b/src/test/java/com/yunhai/flowcenter/notify/UserMsgNotifierBizUrlTest.java index 89ab7aa..20dd583 100644 --- a/src/test/java/com/yunhai/flowcenter/notify/UserMsgNotifierBizUrlTest.java +++ b/src/test/java/com/yunhai/flowcenter/notify/UserMsgNotifierBizUrlTest.java @@ -13,14 +13,16 @@ import java.util.List; import java.util.Optional; 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.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.when; /** - * 待办通知 jumpPath 两态(第三步 T-C3):办件 bizUrl 非空=直达应用详情页(应用内审批免跳 - * 流程中心);空/空白=回落流程中心办理页 /task/{taskId}。 + * 待办通知 jumpPath/jumpApp 语义(自指地址优化 Phase 2):办件 bizUrl 非空=直达业务方详情页, + * 页面归属 jumpApp=instance.bizType(接收方按它查基址);空/空白=回落流程中心办理页 + * /task/{taskId},页面属自己(jumpApp=null,接收方按 bizType 回落)。 */ class UserMsgNotifierBizUrlTest { @@ -29,9 +31,10 @@ class UserMsgNotifierBizUrlTest { private CapturingUserMsgClient userMsg; private UserMsgNotifier notifier; - /** 捕获 jumpPath 的哑客户端 */ + /** 捕获 jumpPath/jumpApp 的哑客户端 */ static class CapturingUserMsgClient extends UserMsgClient { String lastJumpPath; + String lastJumpApp; CapturingUserMsgClient() { super(null, null, false, ""); @@ -39,9 +42,10 @@ class UserMsgNotifierBizUrlTest { @Override public void sendUserMsg(Integer schId, List userIds, Integer msgType, String bizType, - String bizId, String title, String content, String jumpPath, String msgNo, - Integer expireDays) { + String bizId, String title, String content, String jumpPath, String jumpApp, + String msgNo, Integer expireDays) { this.lastJumpPath = jumpPath; + this.lastJumpApp = jumpApp; } @Override @@ -59,7 +63,7 @@ class UserMsgNotifierBizUrlTest { } @Test - void bizUrlPresent_jumpsToAppDetail() { + void bizUrlPresent_relativePath_declaresBizOwner() { FlwTask task = new FlwTask(); task.setId(77L); task.setAssigneeId(101); @@ -68,17 +72,41 @@ class UserMsgNotifierBizUrlTest { instance.setId(5L); instance.setSchId(1L); 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(instanceRepository.findById(5L)).thenReturn(Optional.of(instance)); 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 - void bizUrlBlank_fallsBackToTaskPage() { + void bizUrlBlank_fallsBackToTaskPage_withoutOwner() { FlwTask task = new FlwTask(); task.setId(78L); task.setAssigneeId(101); @@ -94,5 +122,6 @@ class UserMsgNotifierBizUrlTest { notifier.onFlowEvent(FlowEvent.of(FlowEvent.Type.TASK_CREATED, 6L, 78L, 101, "乙", null)); assertEquals("/task/78", userMsg.lastJumpPath); + assertNull(userMsg.lastJumpApp); } }