做线上触达开发的团队,基本都会碰到同一个问题:需要发邮件,又需要发短信,但这两个通道的能力边界、成本模型和技术链路完全不是一回事。我见过太多项目把“邮件和短信发送”做成两个孤立的工具类,各自接各自的渠道,等到用户被重复打扰、账单日送达率暴跌、短信模板因内容不合规被驳回来的时候,才发现问题并不是“接个接口”那么简单。这篇就围绕我自己在几个项目里沉淀下来的经验,把邮件和短信发送这件事从头到尾拆开讲一遍,包括通道定位、投递链路、服务商接入、工程化设计以及上线前后真正会踩的坑。
这篇内容适合两类读者:一类是刚接手通知模块、准备把发送能力做完整的后端开发;另一类是已经在用某个服务商SDK、但发现送达率、回执、重试、限流这些环节始终靠手动维护的团队。全文不会依赖某个具体品牌,所有方案都能平移到你自己的架构里。
1. 邮件与短信的定位之争:两类通道究竟该给谁用
先说结论:邮件和短信不是互相替代的关系,它们是两种定位完全不同的触达通道。邮件适合“内容多、时效要求宽松、成本敏感”的场景,短信适合“必须马上让用户知道、内容短、愿意按条付费”的场景。如果一开始没把这个边界划清楚,后面所有的技术选型都会被带着走偏。
我习惯把系统消息先按这三个等级归类:
- 通知提醒类:例如账号有异动、订单状态变化、发货提醒。这类消息“用户必须尽快知道”,以短信为主,邮件做兜底内容补充。
- 内容推送类:例如周报、账单明细、产品使用建议。这类消息“用户可晚点看、可忽略”,以邮件为主,短信只发送摘要加提醒。
- 系统告警类:例如服务宕机、数据异常、监控触发。这类消息必须短信,因为值夜班的人不会随时盯着邮箱,但手机在身边。
这样划分之后,通道选择和业务价值直接挂钩,而不是拍脑袋觉得“邮件便宜就全走邮件”“短信送达率高就全走短信”。
1.1 不同通道的核心成本模型
短信的成本是硬成本。每一条都要按条计费,量大了之后,一个月几万甚至几十万的短信费非常常见。我在一个日活中等规模的项目里见过,光是验证码短信一个月就烧掉不少预算,后来不得不引入“行为风控”来减少恶意请求。邮件则几乎是边际成本趋近于零的通道,但它的隐性成本在运营侧:退订管理、垃圾箱过滤、发送域名信誉维护,这些都需要专人关注,并不比对接一个短信API轻松。
所以成本上真正合理的做法是:能用邮件说明白的,绝不用短信打扰用户;必须立刻触达的,才动用短信。这个原则听上去简单,但很多团队是在月度账单出来之后才想起做这件事。
1.2 “验证码该走哪条通道”就是一张试金石
验证码是一个特别典型的场景。它可以走邮件,但用户体验非常糟糕:用户注册时收不到邮件验证码,第一反应就是骂产品;客服介入后一查,邮件要么进了垃圾箱,要么延迟了两分钟。验证码这类强时效消息,优先级高于一切,必须短信优先,邮件只能作为备用通道。
反过来说,一封周报邮件没必要做成短信。用户每周收到一条“您的周报已生成,请点击查看”,意思不大,反而消耗预算、增加投诉风险。消息到达的“刚好感”才是通道选择里最重要的指标。
2. 邮件投递不是把信扔进邮筒就行:域名认证与送达率实战
很多开发第一次接邮件发送时,思路是“找一个SMTP服务器,配置好账号密码,调用发信函数”。走通一两次确实没问题,但等真正开始给真实用户发批量邮件,就会发现送达率一下子掉到惨不忍睹的水平。问题基本出在两个地方:没有做发信域名的身份认证,以及没有控制发信节奏。
2.1 自建SMTP还是用邮件服务商API
自建SMTP服务器的核心优势是量足够大之后单封成本非常低。只要硬件资源到位,发信量可以做到很高。劣势也很明显:IP信誉需要养。新的IP一开始发信,邮箱服务商不认识你,很容易把大量邮件扔进垃圾箱,甚至直接拒收。IP信誉一旦掉下去,恢复周期是按周甚至按月计算的,这个过程比写代码痛苦多了。
中小型业务我推荐直接用邮件服务商API。原因不在于花多少钱,而在于服务商已经帮你把退信处理、投诉统计、IP池预热这些脏活都做完了。你只需要做两件事:域名验证和模板适配。如果你所在地区的业务量极大,也可以走“服务商API为主、自建SMTP兜底”的混合方案,但我不建议一上来就自建,运维成本远高于想象。
2.2 SPF、DKIM、DMARC是投递的基本门槛
邮件服务商接入后,第一件事不是写发信代码,而是配置DNS记录。如果不配置或者配置漏掉其中一项,发出的邮件极难进收件箱。这三项的作用:
- SPF:声明“哪些IP允许代发我这个域名的邮件”,防止别人拿你的域名伪造发件人。
- DKIM:给邮件做一段数字签名,收件方可以校验这封邮件确实来自该域名且内容未被篡改。
- DMARC:明确告诉收件方,前面的SPF和DKIM校验如果失败,该怎么处理这封邮件。
配置示例:
SPF记录(TXT): v=spf1 include:spf.examplemail.com ~all DKIM记录(TXT,selector由邮件服务商生成): v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC... DMARC记录(TXT): v=DMARC1; p=quarantine; rua=mailto:dmarc@example.com这里有个细节容易被忽略:SPF里的~all和-all是有区别的。~all表示“如果不是授权IP,邮件可能有问题但别直接拒绝”,-all表示“直接拒绝”。刚开始切流量的阶段建议用~all,等确认没有第三方系统在冒用你的域名发信之后,再考虑切换成-all。DMARC策略同样建议从p=none开始,配合报告观察一段时间的邮件来源,再逐步收紧到quarantine或者reject。
2.3 发信节奏和退信处理是被多数人忽略的变量
即使SPF、DKIM、DMARC配置全对,也不能保证送达率一直稳定。决定送达率的另外两个变量是收件人列表质量和发送节奏。
收件人列表质量指的是邮箱地址是否为有效、活跃的地址。如果列表里残留大量拼写错误、停机、已注销的老地址,每次批量发送都会产生大量退信(尤其是hard bounce)。服务商会把这视为“你在给无效地址发信”,直接降低你的域名信誉。
我在一个新的通知项目里做过一次实测:导入一批历史注册用户时,没有过滤掉无效邮箱,直接跑了批量发送,结果一小时内有大量退信反馈回来,当天下午再发正常用户邮件,几乎全部进了垃圾箱。后来修复动作是:加了一层退信统计逻辑,每个地址连续三次hard bounce就自动标记为无效,从后续批次中剔除。同时把发送节奏从“一次性全部发完”改成“按批次发送,每批之间间隔数秒到几分钟”的平滑模式。两个动作叠加之后,送达率恢复到了正常水平。
2.4 邮件模板的样式克制
写技术方案时很容易把邮件模板当成普通HTML页面来做,但邮件客户端和浏览器是完全不同的渲染环境。大部分邮箱客户端不支持JavaScript,部分CSS属性也支持得很差,比如Flex布局在不少老牌客户端里会直接失效。
稳妥的做法是:表格布局加内联样式。尽量少用外部CSS,少用图片做核心信息承载,因为很多客户端默认不加载远程图片。模板里的变量填充建议用模板引擎而不是字符串拼接,可以避免用户输入的内容里带HTML标签导致整个模板错乱。
3. 短信发送的接入细节:签名、模板、回执与通道切换
短信发送表面上比邮件简单,其实坑一点都不少。最核心的三个环节是资质审核、异步回执和通道容灾。任何一个处理不好,线上都会出现问题。
3.1 签名与模板审核是前置条件
短信不是随手发一串字符就能成功的。国内短信通道要求消息必须带签名,签名通常是【品牌名】或【企业名】,申请签名需要主体资质材料。内容部分走模板审核,模板里允许包含变量,但整个模板的文案结构必须经过服务商和运营商侧审核。
运营上有个常见麻烦:业务方喜欢直接在代码里写死短信内容,换一个运营活动就要改代码重新走审核。建议把短信模板统一收口到配置中心或者线上管理系统里,由运营或者有权限的开发者提交修改,审核通过后才能被发送服务引用。模板变量也要在系统里声明清楚,避免随意拼接导致内容不合规。
3.2 模板变量与内容填充的几个具体坑
审核过了不代表发送就顺利。我见过下面这几类问题反复出现:
- 变量位超长。模板预留了8个字符,业务方传了一个几十位的订单号,结果短信被网关截断或报错。解决方式是在发送前对每个变量做长度校验。
- 变量内容携带换行符或特殊字符。从后台输入框复制出来的文本常带着换行,拼接进短信后内容被截断,部分网关会把这类请求判定为格式异常。
- 双模板变量填错位置。订单号和优惠券码传反,用户收到一条完全看不懂的短信,这个基本属于接口参数映射错误,但只要多一个校验层就能拦住大部分情况。
一个简单实用的方案是:发送服务内部维护一套变量元数据,声明每个变量的最大长度、是否允许特殊字符、是否必须为数字,提交发送请求时先做一次白名单校验,不合格的直接返回业务错误,而不是把脏数据发给通道。
3.3 异步回执决定了“发送成功”的真正含义
短信发送必须理解清楚提交状态和最终状态的区别。客户端调用服务商接口提交成功,只代表服务商已经把消息转给运营商,不代表用户手机已经收到。最终是否送达,依赖短信中心异步返回的状态回执。
实际对接中常见的回执状态:
| 回执状态 | 含义 | 建议处理方式 |
|---|---|---|
| DELIVRD | 用户已收到 | 更新发送记录为终态 |
| UNDELIV | 未送达,多为停机、空号或信号异常 | 可重试一次,仍失败则进人工队列 |
| REJECTD | 被运营商拦截 | 检查内容合规性,不建议反复重发 |
| TIMEOUT | 等待回执超时 | 一般保守处理,以发送方记录为准 |
回执这块最容易出的问题,是开发只写了提交接口,没做回执接收接口,导致数据库里一堆“发送中”状态永远不更新。运营要核对短信账单时,两边数据对不上,就很难查。上线前必须把回执回调打通,并且保证回调处理接口是幂等的,因为服务商在网络异常时可能会重复推送同一回执。
3.4 多通道接入和动态切换
短信服务商不是无状态虚拟化,它也有自己的通道容量和稳定性上限。大促、活动、验证码高峰期,单一通道很容易出现提交成功但回执延迟的情况。我建议至少接入两家短信服务商,做一个轻量级的健康度评估池。
健康度评估可以从三个指标入手:提交成功率、回执到达平均延迟、最近五分钟的失败率。当其中一个通道的失败率超过阈值,发送服务自动把流量切到另一个通道。这个机制在平时是静默的,通常不会触发,但到了关键时刻,它是整个通知系统最后一道防线。
另外要给每条短信请求设置超时时间,不能让发送请求无限阻塞发送线程。某些网关看起来正常的提交,可能在服务商内部排队,等待回执的时间远超过预期,这种场景下超时后进入重试逻辑反而更合适。
4. 把发送能力收敛成一个稳定引擎:队列、重试与幂等设计
邮件和短信这两个通道如果做成散落在各业务模块的工具类,最直接的问题就是重复建设、重复打扰、无法统一治理。更好的选择是做一个独立的通知发送服务,对上暴露统一的提交接口,对下统一管理邮件和短信通道。
4.1 统一消息对象的设计
所有业务方调用的提交接口,都传一个结构化的消息对象。字段设计参考:
{ "message_id": "uuid-20260101-xxxx", "channel": "sms", "template": "order_notify", "to": ["13800000000"], "params": { "order_no": "20260101001", "user_name": "张三" }, "priority": "high", "schedule_time": "2026-01-01T10:00:00+08:00" }每个字段都有它的作用,message_id是幂等关键,channel决定走邮件还是短信通道,template对应双方各自维护的模板标识,params是模板变量填充数据。业务方不需要关心具体通道的API细节,也不需要关心邮件SPF或短信签名这些底层内容,发送服务统一处理。
接口设计上有意识地做成“幽灵提交”风格会更好:就是只要任务被受理,就返回成功,后续真正的发送和重试由后台任务完成。这样做的好处是接口响应快、并发能力高,业务方不会被通道抖动拖垮。
4.2 发送状态机与重试策略
发送记录的状态建议拆成更细的状态机:
- pending:任务已提交,等待调度
- sending:已投递给通道
- sent:提交通道成功,但短信回执或邮件投递结果未确认
- success:用户实际收到(短信有DELIVRD回执,邮件有服务商送达反馈)
- failed:最终失败
- expired:超过有效期限,不再尝试
重试不能盲目重试。标准的做法是指数退避加抖动。比如第一次失败后1分钟重试,第二次5分钟,第三次30分钟,最多三次。要加上随机抖动,避免同一批失败任务在同一个时间点集中重试,把通道打挂。
更重要的是全局熔断:假设服务商通道故障,同一时间如果有十万条消息进入重试队列,系统会自我加压。用一个失败率阈值判断,超过阈值就暂停通道发送,等健康度恢复再放量。很多时候系统的崩溃不是外部通道挂掉造成的,而是内部重试风暴把整个服务拖垮的。
4.3 幂等和防重是短信场景的硬需求
短信按条计费,重复发送等于白白扣钱加消耗用户信任。幂等实现其实不复杂:发送记录表里给message_id加唯一索引,提交任务时先插入记录,重复提交会在数据库唯一约束层面被拦截。另一种常见情况是消息队列的at-least-once投递模式下,同一任务被消费两次,如果发送逻辑没有幂等校验,用户就会收到两条一模一样的验证码短信。这个问题的根子在架构而非代码,落库时做一层去重很有必要。
除了消息级幂等,还需要用户级频控。每个手机号每分钟接收短信的上限、每天接收短信的上限、同一用户验证码发送间隔,都应在发送服务里配置好。这些规则用一张配置表维护,不需要改代码就能调整。
4.4 定时任务和调度语义
日常业务里“账单日给用户发邮件”“活动开始时批量发短信”都是定时触发的。发送服务需要支持schedule_time,把定时任务落入待调度队列,由后台扫描任务到期的消息并下发。这里有个容易忽略的问题:定时任务不能只依赖扫描一次就发出,因为通道可能刚好在抖动,最好在扫描触发后把任务再放进实时队列走一次标准的发送流程,保证能复用重试机制。
5. 上线前后我踩过的坑和一份可以直接抄的检查清单
这部分我尽量把写过悔过的经验写详细一点,希望能帮大家少走弯路。
第一个印象特别深的邮件坑:一个项目更换邮件服务商后,只改了API Key,没有重新做域名验证。结果服务商要求把新的DKIM记录添加到DNS,我漏了这一步,测试邮件看着能发出去,但全部进垃圾箱。当时的排查顺序是怀疑内容模板,怀疑发送频率,最后才想到查DNS记录。后来我做了一个工具脚本,上线前自动检查当前域名在DNS里是否有完整的SPF、DKIM、DMARC记录,并把检查结果作为发布流程的硬性门槛。
第二个邮件坑是模板渲染太“现代”。我刚开始写邮件模板时,用了比较多的CSS选择器和Flex布局,拿主流浏览器预览一切正常。真实的邮箱客户端里渲染出来完全错位,用户反馈“邮件看起来像没做完”。从那之后,我给自己定了一条规矩:邮件模板只用table布局和内联样式,并对排版效果做跨客户端测试。
短信侧的坑更直接。有一次生产环境的短信模板变量长度限制配了个8,但实际传参时没有做校验,导致触发告警消息的订单号被截断,用户收到的内容语义完全变了。排查下来发现是开发阶段只测了模板本身,没测变量长度的边界情况。后来模板系统就按“模板变量元数据优先”的方式重做了,所有发送请求在进入发送前,都会先做一次参数校验。
还有一个短信坑是关于测试号码。当时把某个同事的真实手机号当成线上测试号,一不小心触发了批量发送,直接把该号码送进了运营商的频率限制池,正常验证码都被拦截了半天。从那以后,测试号码统一用一个灰度白名单体系管理,不在白名单里的号码一律禁止触发批量发送。
5.1 上线前的检查清单
我每次发布或者新接入一个发送方案之前,会在本地跑一遍这张清单,照着打勾,缺一项就先不上线:
- DNS记录完整:SPF、DKIM、DMARC均已添加,并用公开检测工具能查到记录;
- 邮件模板已在内联样式方式下验证,主流邮箱客户端的渲染效果均正常;
- 短信签名和模板已通过服务商审核,模板标识在配置中与代码一致;
- 接收回执的接口已上线,且用测试号码实际触发了一条短信并确认回执状态回填正确;
- 对同一message_id重复提交测试,确定不会产生重复短信息费;
- 配置了用户级频控,测试号码在频控规则内的发送行为符合预期;
- 至少接入了两个短信通道,并验证过手动切换开关能正常工作;
- 发送失败率、队列积压长度、通道超时率都已接好监控告警,且告警联系人是实际在线的人。
5.2 最后再分享一个排查习惯
刚开始接触消息发送系统的时候,我处理线上问题习惯一上来就翻业务日志,结果经常查到一半才发现是通道侧的问题,非常浪费时间。后来我慢慢养成了一个固定的排查顺序:先看今天的送达率趋势和回执状态分布,确认到底是邮件送不进收件箱,还是短信回执迟迟不到,再决定继续查代码还是直接联系服务商。
这个习惯帮我省下了很多无效排查。邮件送达率突然下降,我会先检查域名配置是否被动过,再检查列表里是否混入了大量无效地址;短信DELIVRD比例下降,我会先看当前用的通道是否还健康,再去看模板有没有被改过。先把“通道本身”这个问题排除掉,再回到应用代码层面找问题,逻辑会清晰很多。
邮件和短信发送这件事,做到能发并不难,难的是发得准、发得稳、发得可控。希望这篇内容能给你提供一个完整的参考框架。