首页/新闻资讯/正文详情

消息通知怎么设计?站内信、邮件和短信不要乱发

发布时间:2026/9/25 19:24:48 来源:云帆数科 栏目:资讯中心
消息通知怎么设计?站内信、邮件和短信不要乱发
企业系统里的消息通知很容易从“提醒用户及时处理”变成“到处弹、到处发、没人看”。站内信、邮件、短信、企业微信、钉钉、App 推送都能发但真正难的是什么事件值得通知通知谁用哪个渠道失败要不要重试用户能不能退订业务动作是否会被通知失败拖垮。通知系统不是把消息发出去这么简单。它连接业务事件、用户偏好、渠道能力、成本控制和审计追踪。设计得好通知能帮用户减少遗漏设计得差通知会变成新的噪音和投诉来源。本文用一个常见场景做示例维修工单关闭后需要通知申请人和资产管理员。申请人只需要站内信和邮件资产管理员需要站内信如果邮件失败不应该影响工单关闭如果同一工单被重复关闭请求触发也不能反复轰炸用户。这个例子不局限于维修工单。审批待办、合同到期、库存预警、付款成功、导入失败、客户跟进超时都可以用同一套通知设计方法。差别只是事件类型、接收人规则、渠道优先级和频控策略不同。示例环境Java 17、Spring Boot 风格服务层、MySQL 8.x。本文会给出表结构、轻量代码骨架、测试思路和 SQL 验证但重点放在通知策略本身不会把篇幅主要花在代码堆叠上。目录通知系统先回答六个问题输入案例维修工单关闭通知渠道选择站内信、邮件和短信各管什么MySQL表结构事件、收件人和发送记录实现骨架业务写outbox发送异步执行测试重点少测发送SDK多测策略边界SQL验证上线后查通知有没有乱发异常边界和上线验收小结和延伸阅读一、通知系统先回答六个问题设计通知之前先不要急着接短信平台或邮件服务器。更重要的是回答六个问题。问题设计含义没想清楚的后果什么事件要通知只选用户需要及时知道的业务变化系统到处发消息用户屏蔽所有通知通知谁根据业务关系和角色计算接收人漏发关键人或把内部信息发给无关人用什么渠道站内信、邮件、短信各有边界低价值消息走高成本渠道什么时候发实时、延迟、汇总、工作时间发送半夜短信扰民或提醒太晚失效失败怎么办重试、降级、人工查看通知失败拖垮主业务或失败无人发现能否退订和频控尊重用户偏好和成本限制同一事件反复提醒用户反感这六个问题比代码更重要。很多系统通知混乱不是因为发送接口难写而是因为事件和渠道没有边界。比如“审批待办”可能要实时站内信“合同明天到期”可以每天早上汇总邮件“验证码”才适合短信。图1通知设计要先明确事件、收件人、渠道、时机、失败处理和频控。二、输入案例维修工单关闭通知本文固定一组输入后面的数据模型、代码骨架和 SQL 验证都围绕它展开。业务对象维修工单 WO-20260924-001 事件类型WORK_ORDER_CLOSED 事件标题维修工单已关闭 触发动作维修人员确认处理完成工单状态从 PROCESSING 变为 CLOSED 申请人user_id 1008 资产管理员user_id 2001 通知策略 申请人站内信 邮件 资产管理员站内信 短信不发送 幂等键WORK_ORDER_CLOSED:WO-20260924-001 失败策略邮件失败最多重试3次不影响工单关闭 频控策略同一工单关闭事件只通知一次这个案例里有几个关键点。第一通知来自业务事件而不是页面按钮。工单真正关闭后才写通知事件如果关闭失败不应该提前通知。第二接收人来自业务关系。申请人关心处理结果资产管理员关心台账状态其他人没有必要收到。第三不同渠道承担不同作用。站内信作为系统内留痕邮件作为离线提醒短信成本高且打扰强本案例不需要使用。第四通知失败不应该让业务回滚。工单关闭是主业务邮件只是提醒。正确做法是业务事务里写 outbox发送动作异步执行。三、渠道选择站内信、邮件和短信各管什么通知渠道不是越多越好。一个事件同时发站内信、邮件、短信、企业微信看起来很重视实际可能是在消耗用户耐心。站内信适合做系统内留痕。它成本低、可查询、可标记已读适合待办、处理结果、导入完成、审批结果等大多数业务通知。缺点是用户不登录系统就看不到。邮件适合做离线提醒和正式通知。它适合日报、周报、到期提醒、处理结果、外部协作信息。缺点是到达不稳定容易进垃圾箱不适合做强实时保证。短信适合做高优先级、短内容、强触达场景例如验证码、紧急告警、重要审批超时。短信有成本也容易打扰用户不应该用来发送普通状态变化。企业微信、钉钉、App 推送适合组织内部即时提醒但也要遵守频控和免打扰策略。不要因为接入方便就把所有站内消息同步到即时通讯工具。可以用一个简单分级来控制渠道消息等级示例推荐渠道普通留痕工单关闭、导入完成站内信需要离线提醒合同到期、审批待办站内信 邮件/企业微信紧急且高价值生产告警、支付异常站内信 短信/电话汇总类每日待办、周报邮件或站内汇总图2站内信、邮件、短信和即时通讯不要承担同一种通知任务。四、MySQL表结构事件、收件人和发送记录通知表不要只存“标题、内容、是否已读”。一个可靠通知系统至少要区分事件、收件人、渠道发送记录。CREATETABLEmsg_event_outbox(idBIGINTPRIMARYKEYAUTO_INCREMENT,company_idBIGINTNOTNULL,event_typeVARCHAR(80)NOTNULL,biz_typeVARCHAR(64)NOTNULL,biz_idBIGINTNOTNULL,biz_noVARCHAR(64)NOTNULL,event_keyVARCHAR(120)NOTNULL,titleVARCHAR(200)NOTNULL,contentTEXTNOTNULL,payload_json JSONNULL,statusVARCHAR(32)NOTNULL,next_retry_timeDATETIMENULL,retry_countINTNOTNULLDEFAULT0,create_timeDATETIMENOTNULL,update_timeDATETIMENULL,UNIQUEKEYuk_msg_event_key(event_key),KEYidx_msg_event_status(status,next_retry_time));CREATETABLEmsg_recipient(idBIGINTPRIMARYKEYAUTO_INCREMENT,event_idBIGINTNOTNULL,user_idBIGINTNOTNULL,recipient_roleVARCHAR(64)NOTNULL,channelsVARCHAR(100)NOTNULL,read_timeDATETIMENULL,create_timeDATETIMENOTNULL,UNIQUEKEYuk_msg_recipient(event_id,user_id),KEYidx_msg_user_unread(user_id,read_time));CREATETABLEmsg_delivery_log(idBIGINTPRIMARYKEYAUTO_INCREMENT,event_idBIGINTNOTNULL,recipient_idBIGINTNOTNULL,user_idBIGINTNOTNULL,channelVARCHAR(32)NOTNULL,statusVARCHAR(32)NOTNULL,provider_msg_idVARCHAR(120)NULL,error_codeVARCHAR(80)NULL,error_messageVARCHAR(500)NULL,send_timeDATETIMENULL,create_timeDATETIMENOTNULL,update_timeDATETIMENULL,UNIQUEKEYuk_msg_delivery_once(event_id,user_id,channel),KEYidx_msg_delivery_status(status,create_time));这三张表分工不同。msg_event_outbox表示一次业务事件。它解决的是“有没有一件事需要通知”。event_key是幂等键同一个工单关闭事件只能写一次。msg_recipient表示这次事件要通知哪些人。一个事件可以有多个收件人每个收件人可以有不同渠道。msg_delivery_log表示某个收件人在某个渠道上的发送状态。邮件失败、短信失败、站内信成功要分开记录后续才能重试和排查。图3通知事件、收件人和渠道发送记录拆开后才能做幂等、重试和审计。五、实现骨架业务写outbox发送异步执行通知不要在主业务事务里直接调用 SMTP、短信网关或第三方 API。更稳的方式是业务成功时写 outbox后台 worker 扫描待发送事件发送失败后按策略重试。业务侧只做一件事在业务状态变更成功时写事件。Transactional(rollbackForException.class)publicvoidcloseWorkOrder(CloseCommandcommand){WorkOrderorderworkOrderRepository.lockById(command.workOrderId());if(!PROCESSING.equals(order.status())){thrownewServiceException(当前工单不能关闭order.status());}workOrderRepository.close(order.id(),command.operatorId(),command.occurredAt());notificationOutboxRepository.insertIgnore(newNotificationEvent(order.companyId(),WORK_ORDER_CLOSED,WORK_ORDER,order.id(),order.workOrderNo(),WORK_ORDER_CLOSED:order.workOrderNo(),维修工单已关闭,你的维修工单已处理完成请查看处理结果。,command.occurredAt()));}发送侧负责策略算收件人、算渠道、生成发送记录、调用具体渠道。publicvoiddispatchOneEvent(LongeventId){NotificationEventeventoutboxRepository.lockPending(eventId);ListRecipientPlanrecipientsrecipientResolver.resolve(event);for(RecipientPlanrecipient:recipients){for(Stringchannel:recipient.channels()){if(preferenceService.isMuted(recipient.userId(),event.eventType(),channel)){deliveryLogRepository.skip(event.id(),recipient.userId(),channel,USER_MUTED);continue;}if(rateLimitService.exceeded(recipient.userId(),event.eventType(),channel)){deliveryLogRepository.skip(event.id(),recipient.userId(),channel,RATE_LIMITED);continue;}deliverySender.send(event,recipient,channel);}}outboxRepository.markDispatched(event.id());}这两段代码故意保持简短因为重点不在语法而在边界。业务事务只保证“事件被记录”。它不保证邮件一定已经发出。发送 worker 可以失败可以重试可以暂停也可以切换供应商。它不应该反向影响工单是否关闭。接收人和渠道不写死在业务代码里。真实系统可以从通知策略表、角色关系、用户偏好和业务对象关系里计算。图4业务事务写通知事件发送 worker 按策略异步处理渠道。六、测试重点少测发送SDK多测策略边界通知测试不要把主要精力放在第三方 SDK 是否真的能发邮件。SDK 的联通性可以用集成环境验证日常单元测试更应该覆盖策略边界。建议至少覆盖这些测试1. 工单关闭成功后只写入一条 WORK_ORDER_CLOSED 事件。 2. 同一 event_key 重复写入时不产生重复事件。 3. 申请人生成站内信和邮件资产管理员只生成站内信。 4. 用户退订邮件后邮件发送记录标记为 USER_MUTED。 5. 频控命中后发送记录标记为 RATE_LIMITED。 6. 邮件发送失败后retry_count 增加next_retry_time 后移。 7. 邮件失败不影响工单状态 CLOSED。如果要写成 JUnit可以聚焦一两个核心测试而不是把所有发送渠道都 mock 一遍。TestvoidshouldWriteOnlyOneEventWhenWorkOrderClosedTwice(){FixturefxFixture.processingWorkOrder(WO-20260924-001);fx.workOrderService().closeWorkOrder(fx.closeCommand());fx.workOrderService().closeWorkOrder(fx.sameRequestCommand());assertEquals(CLOSED,fx.workOrderRepository().status());assertEquals(1,fx.outboxRepository().countByEventKey(WORK_ORDER_CLOSED:WO-20260924-001));}TestvoidshouldSkipEmailWhenUserMutedThisEventType(){FixturefxFixture.event(WORK_ORDER_CLOSED);fx.preferenceService().mute(1008L,WORK_ORDER_CLOSED,EMAIL);fx.dispatcher().dispatchOneEvent(fx.eventId());assertEquals(SKIPPED,fx.deliveryLog().status(1008L,EMAIL));assertEquals(USER_MUTED,fx.deliveryLog().errorCode(1008L,EMAIL));assertEquals(SENT,fx.deliveryLog().status(1008L,INBOX));}这类测试比“调用邮件SDK返回 true”更有价值。因为通知系统真正容易出错的地方是重复、误发、漏发、退订不生效、失败重试失控。七、SQL验证上线后查通知有没有乱发通知上线后要能用 SQL 看清楚是否乱发、漏发和重试异常。检查同一事件是否重复SELECTevent_key,COUNT(*)AScntFROMmsg_event_outboxGROUPBYevent_keyHAVINGCOUNT(*)1;预期结果empty set检查邮件失败率SELECTchannel,status,COUNT(*)AScntFROMmsg_delivery_logWHEREcreate_timeDATE_SUB(NOW(),INTERVAL1DAY)GROUPBYchannel,status;这条不是要求失败为 0而是要能看趋势。如果邮件失败突然升高要排查 SMTP、模板参数、收件地址质量或供应商限制。检查是否有无限重试SELECTid,event_type,biz_no,retry_count,next_retry_timeFROMmsg_event_outboxWHEREretry_count3ANDstatusIN(PENDING,RETRY);预期结果empty set检查短信是否被普通事件滥用SELECTe.event_type,COUNT(*)ASsms_countFROMmsg_delivery_log dJOINmsg_event_outbox eONe.idd.event_idWHEREd.channelSMSANDd.create_timeDATE_SUB(NOW(),INTERVAL7DAY)GROUPBYe.event_typeORDERBYsms_countDESC;这条要人工判断。如果大量普通工单、普通审批都走短信就说明渠道策略失控了。图5通知验收要检查重复事件、失败率、重试次数和高成本渠道使用情况。八、异常边界和上线验收通知系统要提前讲清楚这些边界。业务成功但通知失败主业务不应该被通知失败回滚。应该记录失败状态进入重试或人工处理列表。通知成功但用户没看到系统只能证明消息已发送、已投递或已读不能保证用户真的理解了内容。关键任务不能只靠通知还要有待办列表。重复事件同一个业务事件必须有稳定幂等键。没有幂等键重复提交和重试会造成重复通知。用户退订非强制消息要尊重用户偏好。强制消息也要有明确范围比如安全告警、审批待办、合规通知。渠道降级邮件失败是否改发站内信短信失败是否改发邮件要按事件等级决定。不要所有失败都自动升级到短信。模板参数缺失模板参数缺失应当在发送前校验。不能让用户收到“{workOrderNo} 已关闭”这种半成品消息。上线验收可以按下面清单执行每类业务事件都有明确通知策略。普通通知、重要通知、紧急通知的渠道不同。同一事件有稳定event_key重复触发不会重复发送。业务事务只写 outbox不直接调用外部发送服务。站内信、邮件、短信的发送记录分开保存。用户偏好和退订规则能生效。频控能阻止短时间重复提醒。失败重试有次数上限和下次重试时间。模板参数缺失会阻止发送并记录错误。SQL 能查出重复事件、失败率、无限重试和短信滥用。九、小结和延伸阅读消息通知的目标不是“能发出去”而是“在合适的时间用合适的渠道把合适的信息发给合适的人”。这句话听起来像原则但落到系统里就是事件模型、收件人规则、渠道策略、outbox、幂等、退订、频控和发送日志。如果系统还很小可以先做站内信和 outbox等通知量上来再扩展邮件、短信和即时通讯。不要一开始把所有渠道都接上也不要让外部渠道失败影响主业务。把通知边界打稳后续再做模板管理、消息中心和运营统计会顺很多。延伸阅读MicrosoftTransactional Outbox patternSpring Framework声明式事务管理MySQL 8.4CREATE TABLE

相关推荐

Windows 10 22H2多国语言部署实操指南:从镜像定制到三层语言生效
Windows 10 22H2多国语言部署实操指南:从镜像定制到三层语言生效

1. 这不是“下载地址”清单,而是一份Windows 10 22H2多语言镜像的实操生存指南你搜到“windows10 22H2多国语言下载地址”,心里想的大概率不是单纯复制一个链接——而是:我手头这台老ThinkPad X220要重装系统,但原厂预装的是简体中… · 2026/9/25 19:24:48

国际版外卖系统:多语言配置和支付通道怎么分开测
国际版外卖系统:多语言配置和支付通道怎么分开测

国际版外卖系统交付中,若把 i18n 发布与支付通道配置绑在同一次上线窗口,任一环节回滚都会拖死另一项。更稳的是多语言配置与支付通道分开测:语言走配置中心版本号;支付走沙箱 intent 回调幂等。结论:locale 与 payme… · 2026/9/25 19:24:42

OFDM参数设计全解析:子载波间隔、循环前缀与FFT点数的权衡
OFDM参数设计全解析:子载波间隔、循环前缀与FFT点数的权衡

1. 先搞清楚:OFDM参数设计到底在“设计”什么做通信系统的人都知道一句话:OFDM的参数定下来,系统的半条命就定下来了。这话真不夸张,因为子载波间隔、循环前缀长度、符号时长、FFT点数这些参数不是孤立存在的,它们互相… · 2026/9/25 19:24:42

HTML+CSS打造可交付的家乡数字名片
HTML+CSS打造可交付的家乡数字名片

简介:这是一份面向HTML5初学者与前端入门者的家乡主题网页开发模板,聚焦语义化结构与基础样式实践,帮助用户快速掌握HTML5标签(如header、section、article、footer)和CSS布局技巧。资源共12个文件,包含2个… · 2026/9/25 19:50:49

腾讯云WorkBuddy Enterprise:企业级Agent平台如何统一管理MCP与CodeBuddy
腾讯云WorkBuddy Enterprise:企业级Agent平台如何统一管理MCP与CodeBuddy

1. 从「超级个体」到「超级团队」:这个平台到底在解决什么问题过去一年,我身边不少开发者都在经历同一种“甜蜜的烦恼”:个人用 AI 编码助手写代码,效率确实起飞了,一个人一天能干过去三天的活,妥妥的“超级… · 2026/9/25 19:50:49

人体对矩形隧道电磁波传输特性影响:有限元建模与定量分析
人体对矩形隧道电磁波传输特性影响:有限元建模与定量分析

简介:这份PDF文献面向无线通信、电磁场与微波技术方向的研究人员与工程技术人员,聚焦煤矿巷道、地铁隧道等有限空间内人体对电磁波传输特性的影响这一实际问题。资源共1个PDF文件,压缩包约144KB,内容为正式期刊论文,含… · 2026/9/25 19:50:43

VMware安装Win10避坑指南:UEFI、控制器与镜像校验全解析
VMware安装Win10避坑指南:UEFI、控制器与镜像校验全解析

1. 为什么非得用VMware装Win10?别被“一键安装”坑了你是不是也搜过“VMware虚拟机安装win10系统教程”,点开前五条,结果发现全是截图堆砌、步骤跳步、关键参数一笔带过,装到一半卡在“无法连接到虚拟机”或者“蓝屏0x0000007B”就… · 2026/9/25 19:50:37

PHY、网卡与交换机协同工作原理与故障排查指南
PHY、网卡与交换机协同工作原理与故障排查指南

1. 项目概述:为什么PHY、网卡、SWITCH这三者必须放在一起讲清楚以太网不是一根线加两个插头就完事的系统,它是一套层层嵌套、职责分明又紧密咬合的协作体系。你手里的笔记本能上网,背后是PHY芯片在把数字信号变成能在网线里跑的模拟波形&… · 2026/9/25 19:50:37

Windows 11原生不支持LDAC?Alternative A2DP Driver实战指南
Windows 11原生不支持LDAC?Alternative A2DP Driver实战指南

1. 项目概述:为什么Windows 11原生不支持LDAC,而你又非得用它?LDAC不是什么新潮玩具,它是索尼2015年就提交给蓝牙SIG组织的高清音频编码协议,2017年正式纳入蓝牙5.0核心规范。它的核心价值非常直白:在标准蓝… · 2026/9/25 19:50:30

数值优化(Numerical Optimization)学习系列-03-共轭梯度方法(Conjugate Gradient)
数值优化(Numerical Optimization)学习系列-03-共轭梯度方法(Conjugate Gradient)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:31

创维E900V22D刷机全攻略:S905L3SB芯片兼容性解析与救砖实战
创维E900V22D刷机全攻略:S905L3SB芯片兼容性解析与救砖实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:31

MQTT协议原理与Broker服务器搭建实战:从Mosquitto到EMQX
MQTT协议原理与Broker服务器搭建实战:从Mosquitto到EMQX

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:37

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码