news 2026/10/10 5:11:38

通知别再写死 if-else:一个 scene 码,四条通道(站内信/短信/邮件/公众号订阅)编排下发

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
通知别再写死 if-else:一个 scene 码,四条通道(站内信/短信/邮件/公众号订阅)编排下发

业务方提了个需求:"下单成功要发短信,还要推个公众号订阅消息,站内信也得有。"以前的通知代码长什么样?每个业务里塞一段smsService.send(...)、pushService.push(...),通道一多就失控——发消息的姿势五花八门,想统计哪个场景发了哪些通道,查日志查到崩溃。

qkl-boot 的 qkl-notify 模块把这事收敛成了"业务只认 scene"。后端业务代码一行notifyFacade.send(userId, "PAY_SUCCESS", params),至于这个场景走站内信还是短信,配置表说了算。这篇讲讲这套场景化触达怎么设计的,以及为什么单通道失败不阻断整体。

表结构:一张场景配置表扛起所有通道

核心就一张qkl_notify_scene表。业务侧从不直接写死通道,而是通过场景编码把"要发通知"这件事声明出来,通道和模板全在配置里:

CREATETABLEqkl_notify_scene(idBIGINTNOTNULLAUTO_INCREMENTCOMMENT'主键',tenant_idVARCHAR(32)NOTNULLDEFAULT'0'COMMENT'租户ID',sceneVARCHAR(64)NOTNULLCOMMENT'场景编码,如 PAY_SUCCESS',scene_nameVARCHAR(128)NOTNULLCOMMENT'场景名称',channelsVARCHAR(128)NOTNULLDEFAULT'INSITE'COMMENT'通道列表,逗号分隔:INSITE,SMS,EMAIL,MP_SUBSCRIBE',insite_template_codeVARCHAR(64)NULLCOMMENT'站内信模板编码',sms_sceneVARCHAR(64)NULLCOMMENT'短信场景编码',email_template_codeVARCHAR(64)NULLCOMMENT'邮件模板场景',mp_subscribe_template_codeVARCHAR(64)NULLCOMMENT'公众号订阅消息模板编码',msg_typeVARCHAR(16)NULLCOMMENT'站内信消息类型:SYSTEM/ORDER/APPROVE/NOTICE',statusTINYINTNOTNULLDEFAULT1COMMENT'1启用 0停用',remarkVARCHAR(255)NULL,PRIMARYKEY(id),UNIQUEKEYuk_tenant_scene(tenant_id,scene))ENGINE=InnoDBDEFAULTCHARSET=utf8mb4COMMENT='通知触达场景配置';

tenant_id + scene 是唯一键,租户可以覆盖默认配置——平台默认配一套tenant_id=0的,某个企业客户要单独关掉短信,插一条自己的记录就行。

多通道编排:switch 分发 + 通道级兜底

核心实现在NotifyFacadeImpl.send():查场景 → 拆通道列表 → 逐个通道分发 → 每通道结果单独记日志。通道分发用 Java 21 的 switch 表达式,一目了然:

privateNotifySendResult.ChannelResultdispatchChannel(Stringchannel,SysNotifyScenesceneCfg,UserNotifyProfileuser,StringtenantId,Mapparams){try{returnswitch(channel.toUpperCase()){caseNotifyChannel.INSITE->sendInsite(sceneCfg,user,tenantId,params);caseNotifyChannel.SMS->sendSms(sceneCfg,user,params);caseNotifyChannel.EMAIL->sendEmail(sceneCfg,user,tenantId,params);caseNotifyChannel.MP_SUBSCRIBE->sendMpSubscribe(sceneCfg,user,tenantId,params);default->NotifySendResult.ChannelResult.builder().channel(channel).status("SKIP").message("不支持的通道: "+channel).build();};}catch(Exceptione){log.warn("[NOTIFY] 通道失败 scene={} channel={} userId={}: {}",sceneCfg.getScene(),channel,user.getId(),e.getMessage());returnNotifySendResult.ChannelResult.builder().channel(channel).status("FAIL").message(truncate(e.getMessage())).build();}}

try-catch 包在 switch 外层,单通道挂了只记 FAIL,不影响其他通道继续发。这是刻意设计的:短信供应商偶发 5xx,不能把站内信也拖下水。每个通道内部还有自己的 SKIP 逻辑,比如用户没绑手机号就直接跳过短信,不白费一次调用:

privateNotifySendResult.ChannelResultsendSms(SysNotifyScenesceneCfg,UserNotifyProfileuser,Mapparams){if(!StringUtils.hasText(user.getPhone())){returnNotifySendResult.ChannelResult.builder().channel(NotifyChannel.SMS).status("SKIP").message("用户未绑定手机号").build();}if(!smsSender.isEnabled()){returnNotifySendResult.ChannelResult.builder().channel(NotifyChannel.SMS).status("SKIP").message("短信通道未开启").build();}StringsmsScene=StringUtils.hasText(sceneCfg.getSmsScene())?sceneCfg.getSmsScene():sceneCfg.getScene();smsSender.sendByScene(smsScene,user.getPhone(),params);returnNotifySendResult.ChannelResult.builder().channel(NotifyChannel.SMS).status("SUCCESS").build();}

统一发送日志:每个通道一条记录

每次 send 都会往qkl_notify_send_log写记录,字段包括 scene、channel、target、status(SUCCESS/SKIP/FAIL)、requestId、failReason、contentSummary。有了这张表,运营同学直接查"最近 3 天 PAY_SUCCESS 场景短信成功率多少",不再需要业务方翻业务日志。

订阅消息的 target 存的是 openid 而不是 userId——排查"这条消息到底发给谁了"时,openid 能直接对到微信侧,比一串自增 ID 有用得多:

if(NotifyChannel.MP_SUBSCRIBE.equals(channel)&&"SUCCESS".equals(cr.getStatus())&&StringUtils.hasText(cr.getMessage())){logEntity.setTarget(cr.getMessage());// 订阅消息场景:message 里放 openid}else{logEntity.setTarget(String.valueOf(userId));}

踩坑记录:MP_SUBSCRIBE 通道的"假成功"

现象:配置好公众号订阅消息后,线上日志显示通道 SUCCESS,但用户微信里一条消息都没收到,运营来投诉。

排查过程:先查qkl_notify_send_log,状态确实是 SUCCESS,requestId 也有值。接着把日志里的 openid 拿出来,对照公众号后台的模板消息记录——根本没有这条下发记录。于是怀疑是模板 ID 有问题。

定位思路:翻SysMpSubscribeTemplate,发现种子数据里模板 ID 是占位符REPLACE_ME_TMPL_ID,运营在后台没改就直接启用了场景。发送前没做"模板 ID 是否真实"的校验,微信接口对不存在的模板 ID 会返回失败——但我们的发送代码没检查返回值里的 errcode,当成成功处理了。

最终解决:发送前加一道硬校验,占位符模板直接 SKIP 并提示:

if("REPLACE_ME_TMPL_ID".equals(tpl.getTmplId())){returnNotifySendResult.ChannelResult.builder().channel(NotifyChannel.MP_SUBSCRIBE).status("SKIP").message("请先配置真实微信模板 ID").build();}

另外把 sender 返回值解析改成强校验 errcode,非 0 一律按 FAIL 落日志。从此"假成功"绝迹,运营没配好模板,场景配置一目了然。

可直接复用的清单

  • 业务代码只声明scene,通道编排全部交给配置表,新增通道不改业务调用方。
  • 通道级 try-catch + SKIP/FAIL 语义:单通道异常不阻断整体触达,短信挂了站内信照发。
  • 租户场景可覆盖:tenant_id=0兜底 + 租户专属记录,靠唯一键(tenant_id, scene)实现。
  • 发送日志按通道粒度落库,target 字段针对订阅消息存 openid,便于对账排查。
  • 外部通道(微信模板消息等)必须强校验返回码,占位符配置直接 SKIP,别让"假成功"骗过自己。

项目源码:https://gitee.com/gzqkl/qkl-boot

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/10 5:10:23

Lit-LLaMA TPU 支持实战指南:在 Google Cloud TPU v4 上运行 LLaMA 推理

人工智能大模型预训练微调LoRA模型量化 【免费下载链接】lit-llama Implementation of the LLaMA language model based on nanoGPT. Supports flash attention, Int8 and GPTQ 4bit quantization, LoRA and LLaMA-Adapter fine-tuning, pre-training. Apache 2.0-licensed. 项…

作者头像 李华
网站建设 2026/10/10 5:09:27

Spring Boot中JSONPath实战:优雅解析嵌套JSON与第三方接口数据

接手一个跨境商城项目时,最让我头疼的不是业务逻辑,而是第三方接口返回的那一大坨嵌套 JSON —— 订单信息、商品快照、支付流水、物流轨迹全揉在一起,层级深得离谱。为了从里面抠出一个状态码或者金额,我写过一堆JSONObject.getJ…

作者头像 李华
网站建设 2026/10/10 5:08:30

stm32cubemx 固件 FW-F4 V1.28.0离线安装教程

由于现在stm32cubemx 下载需要myST登录,但是注册myST又经常无反应,所以我就找了STM32Cube FW-F4 V1.28.0固件版本,进行本地安装,以下是本地安装教程及固件下载路径安装流程1,以管理员身份打开CUBE,单击INSTALL/REMOVE2…

作者头像 李华