1. 为什么我要自己封装一套群发短信方案那个下午我到现在还记得系统刚上线运营说要给全量注册用户发一条版本升级通知当时代码里写着的是for循环逐条调用服务商的单发接口。程序跑了两个多小时跑到三分之一被服务商限频一堆报错堆在日志里用户手机上短信隔了很久才稀稀拉拉收到。事后运营一脸无辜不就是发个短信吗也就是从那天起我决定认认真真把短信发送能力当做一个独立的基础服务来做而不是调个接口而已。1.1 业务需求比发短信复杂得多先别急着写代码。你接到群发短信需求时第一件事不是找服务商而是先把需求拆开。因为发短信这三个字背后至少有四种完全不同的场景验证码短信单条为主单次请求要求快、到达率高通常60秒内必须到达否则用户就点重新获取了。它对接口时延极其敏感属于整个短信系统里优先级最高的一类。通知短信比如订单状态变化、物流更新、系统告警单条批量下发允许一定的排队延迟但不能丢。这类消息量大出问题时用户感知强必须保证稳定送达。营销短信比如活动推广、优惠券提醒单次下发量很大动辄几万到几十万条对到达时间没有秒级要求但对限频、内容审核和退订处理非常敏感发不好就要被投诉。告警短信量小但非常关键必须保证不因批量任务把通道堵死否则系统真出故障时告警短信反而发不出去那就成了连环事故。这四种场景对短信接口的调用方式、重试策略、优先级要求完全不同。如果你只是拿着服务商提供的一个SendSms方法到处调用后面一定会被需求追着打。1.2 直接用服务商API去循环发送的痛点我把最初for循环调接口踩过的坑整理了一下基本就这几个串行发送太慢一条短信即使接口返回只要100毫秒一万条也要1000秒也就是接近17分钟。这还没算网络抖动和限频后的退避等待。失败消息难以追踪循环里一条发送失败直接抛异常你不知道它属于哪一批、用户手机号是多少、服务商返回什么错误码靠翻日志根本没法定位。没有重试机制服务商返回业务限流或触发拦截时如果不想办法重试这批用户就永远收不到短信而且连原因都查不出来。无去重能力批量任务里同一手机号可能出现两次营销短信多发一遍用户会投诉验证码多发一遍问题更大甚至可能被服务商当成异常行为。没有统一模板和签名管理业务人员直接在代码里拼短信内容乱改格式最后被服务商拦截时你根本不知道哪个模板出了问题。所以真正的短信接口集成方案核心不是调通一个接口而是把发送链路做成一个可监控、可重试、可统计的完整流程。标题里说的批量发送短信接口集成方案本质上就是围绕这条链路来设计的。2. 选型阶段短信服务商与接口方案的取舍聊方案之前必须先解决一个问题用哪家的短信接口这个选择直接决定了后面封装层的抽象方式和工作量。2.1 主流服务商的接口风格对比我自己实际用过并维护过三套短信服务商SDK体验差异还是挺明显的维度国内主流云厂商A国内主流云厂商B国际服务商C接口风格RPC风格JSON/XML参数HTTP API参数风格相近REST风格认证简单模板审核必须预审模板与签名必须预审流程较快无需预审但风险自担短信类型验证码/通知/营销验证码/通知/营销通用消息限频控制默认QPS不高可申请提升默认有频率限制可调按账户信誉动态调整回执能力有异步状态报告有异步状态报告有且回执维度更细我的建议是如果你的业务对到达率有硬要求直接选主流云厂商别贪便宜用杂牌通道。你省下的每一分钱都会在用户投诉收不到短信时加倍还回去。国内的短信通道市场很成熟但低价通道往往意味着路由不稳定、回执延迟甚至丢回执而这些恰好都是批量发送最不能接受的。另外还要确认一个容易被忽略的点服务商是否支持企业资质认证。个人开发者想申请签名和模板非常困难很多短信接口权限要求必须有企业主体。所以选型时要把公司资质情况一起纳入判断否则方案设计得再好第一步就卡住了。2.2 为什么最终选择队列服务商API的组合选型时我对比过两条路一条是完全依赖服务商提供的批量发送接口一次提交一个文件或一个大数据包另一条是用本地队列服务商单发接口自己控制节奏。批量发送接口确实省事但短板也很明显你提交之后只能等最终报告中间无法细粒度控制优先级批量接口一旦限频整个批次都得撤回重排而且不同服务商的批量接口格式千差万别做抽象层的成本反而更高。最后我的方案是上层统一走本地队列按业务场景设定优先级和速率底层把单发接口和批量接口都封装成同一套发送器。正常情况下用小批量聚合发送某个业务需要冲量时再切到批量接口。这样既保留了灵活性又让上层业务不用关心底层通道差异。这里有一个关键认知短信本身就是异步的。你调用接口拿到OK只代表服务商接收成功了不代表用户手机收到了。真正的发送结果要通过服务商的回执也就是状态报告异步回调才知道。所以你的集成方案里必须有一个模块专门处理回执否则你永远不知道真实发送成功率也不知道那些提交成功但没送达的短信到底浪费了多少预算。3. 批量发送的核心链路拆分把整条链路拆开看从左到右依次是业务请求层、数据清洗层、调度层、发送器、回执处理层。每一层只做一件事上下层之间通过数据结构和队列解耦。3.1 发送前的数据准备手机号校验与号码段处理批量发送最容易翻车的第一步不是接口而是数据。数据不干净后面的所有优化都白搭。我先是写了一个手机号清洗函数主要做三件事格式校验国内手机号为11位数字以1开头第二位通常是3、5、6、7、8、9。用正则做一次粗筛把明显不合法的号码剔除掉。这一步能挡掉大量看起来像号码的脏数据比如带横杠的、含空格的、位数不对的。号码去重同一批任务里相同手机号只保留一条。用集合或Redis Set在批量前做一次去重成本很低收益很高因为重复发送不只是浪费钱还容易让用户反感。黑名单过滤对已退订用户、投诉过的号码、测试号码单独维护名单发送前过滤掉。这块我在后文还会细说但请记住它是刚需不是可选项。这一步容易被忽略但它在整个链路里性价比最高。几万条数据里哪怕有5%的脏数据清理之后都能帮你在上游省下几百上千条的短信费用同时避免触发服务商的风控。你可以把这个清洗逻辑做成独立服务每次批量任务进来先排队清洗洗完了才允许进入发送队列。3.2 内容模板与签名被拦截最多的环节国内主流服务商基本都要求你提前把短信模板和签名提交审核。审核通过后发送时服务商会校验模板变量和签名是否匹配不匹配直接拒绝。我的经验是模板设计要保守把变化的部分作为变量占位。比如您的验证码是${code}${minute}分钟内有效而不是在代码里拼接一整段话。这样有两个好处一是审核容易通过二是批量内容里变量替换更可控不容易出现某条短信因为特殊字符被服务商当成异常内容拦截。签名方面签名是【】里的那段名称它会直接暴露在用户手机上。签名要和你的企业主体、模板内容匹配不要试图在这里做任何绕过审核的操作那是纯属找麻烦。实际维护中如果你有多个业务线建议每个业务线申请独立的签名这样一旦某一个签名被投诉封禁其他业务线还能正常运行不会全军覆没。3.3 批量调度这一层的设计调度层是整个群发方案的核心。它的职责是把一个大任务拆成可并发执行的小批次同时控制整体速率避免触发服务商限频。我采用的是分批并发节流的组合思路分批每批按分片大小比如500条切分。一批提交完不等待回执继续提交下一批。并发同时提交的批次数量受信号量控制比如最多同时跑8个批次。这样即使底层是单发接口也能实现高吞吐。节流通过令牌桶或简单的sleep控制每秒提交的总条数把QPS压在服务商允许范围内。别小看这一步很多线上事故就是从这里开始的。这样设计后你并不需要依赖服务商提供批量发送接口才能高效群发用单发接口也能打出一万多条/分钟的量关键是节奏控制好。调度层还应该支持暂停和优先级调整否则运营临时要求先把某部分人发掉时你只能干瞪眼。3.4 发送结果与状态回执的处理思路发送器收到服务商响应后只能判断提交成功。真实的到达情况要通过回执回调获知。我的做法是发送记录入库时生成一个msgId服务商回调时带上msgId和status字段回执处理服务再去更新对应记录的状态。注意回执可能延迟几分钟到几小时所以最终的成功率统计要以任务启动后N小时为截止时间而不是任务刚提交完就算。这里我吃过一次亏某次营销任务发完后我看提交成功率99%心里美滋滋结果第二天运营说用户没收到。一查回执那里静默丢了30%的送达失败很多号码是空号或关机而回执处理程序当时有个bug把这一类都当成发送失败但未处理。后来我统一改成回执处理必须落库、必须可查、必须和提交记录关联缺一不可。4. 工程实现从单条到并发群发的代码实战下面是我在实际项目中沉淀出来的精简版实现语言用Python但思路可以平移到任何语言。重点不是抄代码而是理解每一层为什么要这么设计。4.1 核心发送器抽象先把服务商差异挡在门外。我定义一个SendChannel接口里面只暴露两个方法单条发送和批量发送。这样上层业务永远不感知具体服务商。from abc import ABC, abstractmethod from dataclasses import dataclass from typing import List, Optional dataclass class SmsMessage: phone: str template_code: str params: dict sign_name: str msg_id: Optional[str] None class SendChannel(ABC): abstractmethod def send_single(self, msg: SmsMessage) - dict: 返回服务商的原始响应 abstractmethod def send_batch(self, msgs: List[SmsMessage]) - List[dict]: 批量发送返回每条消息的提交结果具体服务商只要继承这个接口实现即可。换供应商时只改工厂类上层代码完全不用动。我给这个抽象层加了两层保障一是超时控制每个HTTP请求必须有timeout否则一个慢接口能拖垮整个线程池二是签名校验服务商返回的数据结构如果不符合预期直接抛错不往下传脏数据。4.2 消费者与线程池并发控制发送任务的消费端我用了ThreadPoolExecutor加信号量把并发批次和批次内条数两个参数独立出来import threading from concurrent.futures import ThreadPoolExecutor, as_completed class SmsDispatcher: def __init__(self, channel: SendChannel, max_workers: int 8, batch_size: int 500): self.channel channel self.batch_size batch_size self.executor ThreadPoolExecutor(max_workersmax_workers) self.semaphore threading.Semaphore(max_workers) def dispatch(self, messages: List[SmsMessage]): chunks [messages[i:i self.batch_size] for i in range(0, len(messages), self.batch_size)] futures [] for chunk in chunks: self.semaphore.acquire() future self.executor.submit(self._send_chunk, chunk) futures.append(future) for future in as_completed(futures): future.result() self.semaphore.release()_send_chunk内部再根据服务商能力决定走批量还是循环单发。原则很简单并发数量足够多时单发接口也能顶住很大的量关键是别在循环里加过重的日志和网络重试否则日志本身就会拖垮性能。一个我踩过的坑是并发数开太大比如20个线程每个线程发送间隙都不控制结果服务商在5秒内收到上千条提交直接触发限频。后来我把并发和节流绑在一起——线程只负责提交全局再挂一个令牌桶每秒只放行固定次数整体反而更稳定。4.3 失败重试与退避策略发送失败的错不能一刀切。我把错误码分成三类每一类的处理方式完全不同可重试错误限流、网络超时、签名解析临时故障、系统繁忙。这类错误等一会儿再发大概率能成。不可重试错误手机号格式错误、模板变量不匹配、签名不存在。这类错误重试多少次都一样直接标记为失败并人工排查。强制失败内容含敏感词、触发运营商拦截。这类要立刻停止该模板继续发送否则越试越糟甚至可能连累整个签名被封。重试用指数退避每次间隔分别为1秒、2秒、4秒最大3次。加一个random抖动防止所有任务同时重试又撞在一起import random import time def retry_send(func, channel, msg, retries3): for attempt in range(retries): try: return func(channel, msg) except RetryableError: if attempt retries - 1: time.sleep(2 ** attempt random.uniform(0, 1)) return {status: failed, msg_id: msg.msg_id}这里有个细节重试的任务要放回队列尾部而不是立刻重新调用。因为发送通道刚被限流时大批重试任务同时进来会让情况更糟。放回队列后让调度层按当前速率自然消费反而平滑很多。4.4 回执回调接口用FastAPI起一个回执接收服务。注意回调地址必须是服务商可以外网访问的URL且要校验来源身份否则容易被人伪造回执刷库。from fastapi import FastAPI, Request app FastAPI() app.post(/sms/receipt) async def receive_receipt(request: Request): data await request.json() msg_id data.get(msg_id) status data.get(status) # 校验签名更新发送记录状态 update_send_status(msg_id, status) return {code: 0, msg: ok}回执服务一定要做好幂等处理。同一个msgId的回执可能因为网络重试被推送多次你的更新逻辑必须是用后到的覆盖先到的这种简单策略或者去重后只处理第一次。否则统计出来的数据一会儿一变排查问题时人都要疯了。5. 高并发群发最容易踩的坑这部分我在生产环境里真的一个个撞过拿出来说比什么理论都管用。5.1 频率限制与并发参数的平衡每家服务商都有单位时间接口调用上限有的是每分钟2000条有的是按QPS算。很多人以为并发开大发得快其实服务商那边的限频才是真瓶颈。你开再多的线程超过限额的请求全被拒绝甚至可能被临时封禁接口权限。我做压测时的做法是先按服务商文档给上限的60%估算并发和每秒条数然后逐步上调观察延迟和失败率。如果失败率开始抬头立刻回退到上一档。频率参数要写在配置中心里不要写在代码里因为服务商随时可能调整通道策略写死在代码里改起来太痛苦。另外一个经验是不同短信类型的限频是分开算的。验证码通道和营销通道通常有独立的QPS配额所以做方案时可以拆成两套配置验证码走验证码的速率营销走营销的速率互不干扰。5.2 手机号格式与海外号码国内手机号校验我上面提过了。但做跨境业务时要注意服务商API的phone字段要么只接受国内11位号要么要求带上86前缀。很多线上事故就是业务把国际号当成国内号传上去被服务商静默丢弃或报格式错误。如果你要支持国际短信最稳妥的做法是在数据清洗层判断号码归属国内号走国内通道国际号走国际通道不要混在一个批次里提交否则有一个号码格式错误整个批次可能都要重发。号码归属判断我用的是手机号前缀匹配规则配合运营商公布号段表每季度更新一次基本够用。5.3 特殊号段与黑名单处理虚拟运营商号码、携号转网过来的号码在运营商侧的到达率会低一些这不一定是你接口的问题。我自己遇到的真实案例是某个批次全量发出后回执显示20条发送失败全是同一虚拟号段。后来在清洗层单独给这类号码标了优先级错误回执处理时走人工确认流程避免误判成通道故障。退订和投诉用户一定要维护黑名单。营销短信被用户回复TD退订后如果下次又给他发了轻则投诉重则服务商封你的模板和签名。这块没有捷径只能老老实实在发送前过滤。我还建议对黑名单做分级硬退订的永久过滤临时投诉的观察一段时间后自动转普通用户避免误伤正常活跃用户。5.4 内容审核与合规红线很多开发者第一次做群发时容易忽视内容不是你想发就能发的。国内通道对营销内容有一套非常严格的审核机制模板里不能有夸大宣传的词汇必须写清楚退订方式推广短信还必须错开休息时段。这些规则不是为了为难人而是为了整个行业生态能长期运转。这里不建议任何人想着绕过审核或者伪造签名去发垃圾短信成本极高。通道一封影响的不是一批任务而是整个业务系统所有依赖短信的场景包括登录验证码、告警通知全都会跟着停摆。技术方案做得再好合规不过关全白搭。营销短信至少要做到提供退订方式、不轰炸、不伪装成通知短信、目标用户是主动订阅过的。把这些做对你的发送成功率也会更高因为投诉少了服务商给你的通道质量也会维持得更好。6. 压测与线上验证方案写完最后一步是验证。我把验证流程分成三档每一档有明确的目的不会直接上生产乱发。6.1 小规模验证跑通第一档100条以内的测试任务用测试签名和测试模板验证发送链路是否通、回执是否正常入库、失败重试是否正确。这个阶段重点看日志不发真实用户。我会特意构造一些异常数据比如空手机号、位数不对的号码、不存在的模板变量看清洗层和发送器是否按预期处理。这一步做得越细线上事故越少。测试手机号我固定用几个指定的号段并且把测试模板和线上模板分开避免混淆。6.2 压测数据与性能指标第二档5000条真实号码的小规模群发压测时记录几个关键指标指标观测值说明提交耗时平均80ms/批网络与服务商接口延迟总耗时5000条约90秒受并发批次与限频共同影响提交成功率99.6%剩余为限频与网络抖动最终到达率24h回执97.8%未到多为空号与关机回执回传延迟中位数10秒内高峰时段会拉长这个数据仅供参考不同服务商和时段差异很大但你可以拿它当基线去对比自己调参前后的效果。注意压测要和线上错峰不然压测短信和真实业务短信抢通道两边都会被限频拖慢。6.3 我的实践体会把成功交给回执把失败交给重试这么多场景做下来我最深的体会就两句提交接口返回成功不代表短信真的到了用户手机真正的成功要等回执来确认而发送失败时也别慌先分清错误类型再决定重不重试比盲目重试一万次有用得多。如果让我给刚做短信集成的朋友一个建议那就是从一开始就按统一抽象、异步化、可观测这三个原则来设计。不要为了省事把代码写死在一个服务商的SDK里也不要迷信某个批量接口能一劳永逸。短信这种场景真正的复杂度不在接口文档里而在海量数据、频率控制、回执处理和那些永远不可能一次考虑全的边界情况中。最后再分享一个实用小技巧把测试手机号固定用几个指定的号段测试模板和线上模板分开在配置中心里加一个测试模式开关。打开后发送器会把所有请求转发到测试通道并打印完整报文。这样每次联调、排查问题都会轻松很多——短信这东西看不见摸不着调试成本本来就高能省一点是一点。
企业数字化 ERP 产品动态
相关推荐
猫狗识别算法源码解析:从CNN训练到迁移学习实战指南 简介:这是一份面向计算机相关专业学生与机器学习初学者的猫狗识别算法项目源码,适合用来完成课程设计、期末大作业或毕业设计中的图像分类任务。项目基于机器学习方法实现猫狗二分类,代码经过调试可直接运行,读者可在现有基础上调… · 2026/9/26 13:29:53
Elasticsearch中文分词利器IK分词器:配置、词典与实战优化 做搜索或者做 Elasticsearch 的工程师,几乎没有人不知道 IK 分词器。这个插件诞生很多年了,但直到今天,中文搜索场景里第一个被考虑的方案依然是它。我做过不少搜索类项目,从站内商品搜索到内容平台的全文检索,IK 基本… · 2026/9/26 13:29:46
RSI、中美模型差距与知识蒸馏:大模型能力增长的动力与工程实践 1. 从一场对谈说起:RSI、模型差距与蒸馏到底在聊什么 Nathan Lambert 和 Epoch AI 的 JS Denain 坐下来聊 RSI、中美模型差距和蒸馏影响,这个组合本身就很有意思。Nathan 是 Allen AI 出身、长期做开源模型和后训练研究的从业者,Epoch AI 则是… · 2026/9/26 13:29:46
FontForge 的起源与演进:从 PfaEdit 到开源字体编辑器的二十年技术编年史 桌面应用图形学 【免费下载链接】fontforge Free (libre) font editor for Windows, Mac OS X and GNULinux 项目地址: https://gitcode.com/gh_mirrors/fo/fontforge 点击查看 免费下载 导读
本文基于 FontForge 官方文档 ff-history.rst(作者 George… · 2026/9/26 14:33:53
嵌入式AI实战:Microduck-HD1910硬件调试与模型部署全流程解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 14:33:53
NHentai-android开源项目:原生Android漫画阅读器架构与性能优化实践 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 14:33:46
会议语音转写准确率真相:为什么98%不等于好用 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 14:33:46
grid还是skeleton?srt-whiteboard-animation笔迹路径选择简单指南 grid还是skeleton?srt-whiteboard-animation笔迹路径选择简单指南 【免费下载链接】srt-whiteboard-animation 将 SRT 字幕做成暖米黄纸张底的流式笔迹白板手绘动画 skill:mask 分区遮罩编排 stream 连续笔迹(ink→color)。 项… · 2026/9/26 14:33:30
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍 简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践 一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46