站内信点击跳转:从三条硬编码映射,到"字段始终携带"的配置驱动
需求一句话:站内信要能点——实时监控申请、回放申请、入侵告警工单这三类消息,申请人、审批人、处理人收到后点击,要跳到对应的列表页。一期用前端硬编码映射快速落地,二期按上级要求改成流程配置驱动。真正值得记录的是二期定下来的那条核心语义——**jumpRoute 字段始终携带,空串等于明确不跳转、绝不回落旧映射**——以及它背后被评审揪出来的盲区。
一期的形态:铃铛面板与三条映射
最终交互定在顶栏铃铛上:面板分”未查看/已查看”两个 tab;行点击 = 先标已读、再跳转,标已读走服务端闭环成功后才跳;行尾常驻 × 软删按钮;hover 浮现”标已读”文字按钮用于批量清未读;铃铛徽标 = 服务端全量未读真值 + WebSocket 告警推送增量。
跳转本身是一张前端常量表 NOTICE_JUMP_ROUTES:
| 消息特征 | 跳转目标 |
|---|---|
| bizType=4(实时监控申请) | 实时监控申请列表 |
| bizType=5(回放申请) | 回放申请列表 |
| bizType=0 且 alarmBizType=VIDEO_SECURITY(入侵告警) | 越界监控页(实测后从告警记录页改过来的) |
| bizType=0 无 alarmBizType(消防等) | 不跳转(设计行为) |
表里那个 alarmBizType 是本次的关键新增。告警工单的 bizType 全是 0,前端无法区分入侵还是消防,于是约定在消息的 bizJson 里补一个 alarmBizType 字段。补字段的位置有三处,第三处是手测才发现漏了的:
- bpm 侧
TaskServiceImpl.makePush(审批任务消息)——从bpm_business.formdata解析; - warning 侧
notifyAlarmManagers(告警管理员通知)——直接取告警记录的 bizType; - 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_ext加jump_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 | ① bpm 库执行 jump_route 迁移 |
顺序不能反:迁移后、配置完成前产生的新消息会带 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 是二期;站内信已读列表页未做。一个功能从”能跳”到”敢上线”,中间隔着的从来不是那三条映射,而是语义、顺序和验证这三件笨功夫。