站内信点击跳转:从三条硬编码映射,到"字段始终携带"的配置驱动

需求一句话:站内信要能点——实时监控申请、回放申请、入侵告警工单这三类消息,申请人、审批人、处理人收到后点击,要跳到对应的列表页。一期用前端硬编码映射快速落地,二期按上级要求改成流程配置驱动。真正值得记录的是二期定下来的那条核心语义——**jumpRoute 字段始终携带,空串等于明确不跳转、绝不回落旧映射**——以及它背后被评审揪出来的盲区。

一期的形态:铃铛面板与三条映射

最终交互定在顶栏铃铛上:面板分”未查看/已查看”两个 tab;行点击 = 先标已读、再跳转,标已读走服务端闭环成功后才跳;行尾常驻 × 软删按钮;hover 浮现”标已读”文字按钮用于批量清未读;铃铛徽标 = 服务端全量未读真值 + WebSocket 告警推送增量。

跳转本身是一张前端常量表 NOTICE_JUMP_ROUTES

消息特征 跳转目标
bizType=4(实时监控申请) 实时监控申请列表
bizType=5(回放申请) 回放申请列表
bizType=0 且 alarmBizType=VIDEO_SECURITY(入侵告警) 越界监控页(实测后从告警记录页改过来的)
bizType=0 无 alarmBizType(消防等) 不跳转(设计行为)

表里那个 alarmBizType 是本次的关键新增。告警工单的 bizType 全是 0,前端无法区分入侵还是消防,于是约定在消息的 bizJson 里补一个 alarmBizType 字段。补字段的位置有三处,第三处是手测才发现漏了的:

  1. bpm 侧 TaskServiceImpl.makePush(审批任务消息)——从 bpm_business.formdata 解析;
  2. warning 侧 notifyAlarmManagers(告警管理员通知)——直接取告警记录的 bizType;
  3. bpm 侧 ProcessEndListener(”您的审批已通过/被驳回”)——最初漏了这里,导致完成通知点不动。补的时候还踩了一个同名枚举的坑:ProcessEndListener 导入的是 model.enums.BusinessTypeEnum(取值用 getValue()),和 TaskServiceImpl 里那个 enums.BusinessTypeEnum(用 getType()不是同一个枚举

一期踩掉的三个坑

vue-router 3.0.2 的 push() 不返回 Promise。 现象是点可跳消息弹”标记已读失败”但跳转成功——push(x).catch() 里的 push 返回 undefined.catch 直接抛 TypeError,然后被链尾的空参 catch(()=>{}) 吞掉,伪装成业务错误。3.1.0 才返回 Promise。教训有两层:升级前查版本行为;链尾空参 .catch(()=>{}) 会把 then 回调里的同步异常一起吞掉,排查第一步永远是把它改成 (e)=>console.error(e)

快速双击同一条消息,报错还可能删错相邻条。 第二次点击时 read_status 已是 1,update 影响行数为 0,通用层直接报 FAIL_OPERATE;而前端 splice 用的是过期 index,错位删了相邻的消息。修法:入口处 item.unread=false 占位挡连点、删除按 id findIndex、失败回滚占位。

申请人同一秒收两条”审批通过”。 security 侧手拼发送和 BPM 结束监听器双发。修法是收敛到 BPM 单一来源,顺带修掉了 VideoApplyMapper 漏映射 handler 三列导致处理人信息不落库的问题。

配置驱动:流程配置决定跳转目标
图 1:二期把跳转目标从代码常量搬进 bpm_process_ext.jump_route,加新跳转业务不再改代码。

二期:配置驱动,和”字段始终携带”语义

下午上级拍板:跳转目标不能写死在前端,要按流程配置。落地的形态是:

  • 存储bpm_process_extjump_route 列(Flyway 迁移),哪个流程跳哪个页面,配置决定;
  • 后端JumpRouteHelper.resolveJumpRoute(processExtId) 返回路由字符串,五类消息发送点(审批任务、完成通知、转派、超时、抄送)统一在 bizJson 写 jumpRoute
  • 前端hasOwnProperty('jumpRoute') 优先消费 → 旧映射 LEGACY_NOTICE_JUMP_ROUTES 兜底 → 跳转前 $router.match(route).matched.length > 0 校验路由已注册,收件人无菜单授权时提示”暂无访问权限”而不是白屏(vue-router 3.0.2 没有 getRoutes(),这个坑第二轮评审才发现)。

最核心的是这条写入语义:**jumpRoute 字段始终携带**——读不到配置、配置为空、查询异常,一律写空串 "",用”字段是否存在”区分新旧消息:

消息状态 判定 行为
字段存在(含空串) 新消息 按配置判定;空串 = 明确不跳转,不回落旧映射
字段不存在 历史消息 前端走旧映射兜底

为什么要这么绕?因为评审揪出了一个盲区:如果是”读到配置才写字段”,那么管理员清空某条流程的配置后,新产生的消息会因为没有字段而错误地回退到旧映射——配置”清空”的语义被吞掉了。字段始终携带之后,空串就是”明确配置不跳转”的显式表达,语义闭环。

新旧消息的判定语义
图 2:字段存在即新消息(空串=明确不跳);字段不存在才是历史消息,走旧映射兜底。

评审第二轮还做了性能收敛:批量通知(超时/抄送)的路由查询从收件人循环里挪出来,500 人抄送从 500 次 SQL 变 1 次;审批通知直接传 processExtId,省掉一次 business 反查;resolveJumpRoute 改返回 String,让”查一次复用”成为自然写法。

配置化也有一个明确的例外:告警直接通知(notifyAlarmManagers不做配置化——它产生在流程绑定之前,与 process_ext 没有稳定对应关系,一期靠前端旧映射里的 alarmBizType 判断兜底,二期规划挪到 alarming_processor 配置。

发布顺序是有严格约束的

1
2
3
① bpm 库执行 jump_route 迁移
→ ② 按流程 id 逐条配置 jump_route(先 SELECT 摸底)
→ ③ 放新版 BPM 流量

顺序不能反:迁移后、配置完成前产生的新消息会带 jumpRoute:""——字段存在所以是新消息,空串所以明确不跳转,不回落旧映射。如果先放流量再配,间隙期该跳的消息全都不跳。配置摸底时注意老数据 delete_flag 有 NULL,过滤条件要写 COALESCE(delete_flag,'0')='0'

三条真实链路的本地验证

功能不是”编译过、单测绿”就交付的,而是在本地起了全栈环境(gateway/admin/bpm/security/smart/warning 六个服务连本地库 + 前端 dev server),把三条真实链路各跑了一遍:实时监控申请走 Flowable 三节点流程(发起→部门审批→分配摄像头);回放申请同款;入侵告警链路最真实——监控任务 + 任务级规则(自动模式)+ 流程绑定三件套配齐后,从告警注入接口打一条带 monitoringId 的事件进去,自动转成 BPM 工单,审批任务、告警管理员通知、审批通过通知三种消息依次落位,逐条点击验证跳转。过程中还处理了本地库缺按钮资源行、缺角色导致通知静默跳过等一串环境问题——这些排查记录留在了项目笔记里,线上复测时直接照用。

三条链路的消息全景
图 3:一条告警自动链路会产生三条消息,bizType 都是 0,靠 alarmBizType 和 jumpRoute 区分去向。

收尾

最终交付:前端 5 个提交、后端 8 个提交,覆盖铃铛面板、三处 alarmBizType、双发收敛、配置化、性能收敛。遗留事项也登记在案:历史消息过渡期靠 LEGACY_NOTICE_JUMP_ROUTES 兜底、存量消息过保留周期后删除兜底分支;告警路由配置化放 alarming_processor 是二期;站内信已读列表页未做。一个功能从”能跳”到”敢上线”,中间隔着的从来不是那三条映射,而是语义、顺序和验证这三件笨功夫。