news 2026/9/29 2:03:03

短信发送流程详解:从验证码提交到回执处理的全链路指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
短信发送流程详解:从验证码提交到回执处理的全链路指南

简介:这是一份聚焦短信发送流程的PPT教案,面向通信工程专业学生、核心网运维人员及网络优化工程师,用图解方式讲解短信从发送方到接收方所经过的完整信令链路。内容按场景拆解为漫游用户MO流程、省内互通短信MO流程、省内用户MT流程、漫游用户MT流程、省外用户MT流程,并清晰标注了BSC、MSC、LSTP、HSTP、SMSC、ISMG、HLR等网元在每一步中的职责与应答关系,覆盖号段鉴权、位置查询、短信转发、计费话单生成等关键环节,便于读者快速建立端到端流程的全局视图。压缩包共1个pptx文件,大小158KB,内容结构紧凑,可直接用于课堂教学、自学入门或团队内部分享。目前已有67人学习下载,适合作为理解短信网络架构的基础材料,也能为后续排查短信发送失败、延迟等问题提供流程参考。

1. 短信发送流程到底在送什么:一次验证码的完整旅程

运营群里有人喊“验证码 10 分钟没到”,你查数据库看到状态是“提交成功,等待回执”,找通道方对线,对方甩来一句“已提交运营商”。这条短信到底卡在哪一跳?短信发送流程学习教案.pptx 这个标题,讲的就是这条从业务系统到用户手机的完整链路:验证码怎么被组装、怎么提交给网关、网关怎么转协议、运营商怎么下发、回执怎么回来。适合刚接手短信服务的后端开发、测试和运维,也适合要跟通道方掰扯的运营。学完不是为了背字段,而是能立刻回答三个问题:消息停在哪、该不该重发、找谁要回执。

2. 把短信发送流程拆成三段:业务侧、网关侧、运营商侧

2.1 短信发送流程里的三个角色与责任边界

我见过太多人一上来就啃 SMPP 报文格式,结果线上出故障时连“该找谁”都分不清。带这类教案,我习惯先借用企业数据架构设计方法里的思路:先画数据在哪产生、在哪流转、在哪消费,再填细节。短信发送流程也一样,三段边界必须在一开始就立住。

业务侧是你的服务,负责生成短信内容、校验签名模板、分配唯一消息 ID、记录发送状态。网关侧是通道方提供的接入服务,负责协议转换、按号段路由、重发控制、回执回调。运营商侧是移动、联通、电信的短信中心,负责真正的下行、计费、生成最终状态报告。故障发生时,第一条判断准则就是:这个状态是业务侧生成的,还是网关回传的,还是运营商回执里的。

环节产生状态谁负责写库谁负责解释
业务侧提交SUBMITTED业务系统本服务
网关确认ACK / NACK网关通道方
运营商下发SENT网关回传通道方
手机侧结果DELIVRD / UNDELIV / EXPIRED运营商回执通道方

这张表是整份教案的地基。确认和回执是两个不同的信号,确认只代表“网关收了”,不代表“手机收到了”。很多线上误判,就是把 ACK 当成了成功。

2.2 SMPP、CMPP 与 HTTP API:三种接入方式怎么选

教案里绕不开协议选型。实际从业方案里,短信发送流程对接方式就三种:SMPP、CMPP、HTTP API。不是越底层越好,要看你的业务发往哪里。

SMPP 是国际通用协议,长连接、面向报文,适合跨国业务和自建网关,能承载大批量提交,但接入成本高,回执关联要自己处理。CMPP 是国内三大运营商行业网关常用的协议,很多老系统还在跑,字段里有 PK_TOTAL、PK_NUMBER 这类分包参数,调试比 SMPP 绕。HTTP API 是聚合服务商最常见的接入方式,提交一条 POST 请求,同步返回消息 ID,异步回调状态报告,接入成本最低,但排查问题时依赖对方日志。

接入方式适用场景接入成本回执方式常见坑
SMPP国际业务、批量高并发高长连接 deliver_sm需自己维护 session 和窗口
CMPP国内运营商行业网关中网关主动推送分包、mt/ms 消息类型易混
HTTP API验证码、通知、营销低回调或拉取回调丢失、签名校验

如果只发国内验证码,我建议先走 HTTP API;如果业务线多、量级大,再考虑 SMPP 自建接入。教案里不要让学生陷入“哪个协议更高级”的争论,要看业务边界。

3. 用本地仿真脚本跑通最小短信发送流程:生产者、路由、回执

3.1 零依赖的 Python 仿真:从业务侧提交到手机侧回执

只看协议不跑代码,短信发送流程永远是黑匣子。我建议教案里加一个最小仿真脚本,不依赖任何第三方库,装完 Python 3.10 就能跑。它的意义不是模拟真实网关,而是把状态变化顺序印在脑子里:SUBMITTED → ACK → DELIVRD。

# mock_sms_flow.py # 最小短信发送流程仿真:业务侧提交、网关路由、运营商回执 import time import uuid from datetime import datetime def submit_sms(phone: str, content: str): """业务侧提交消息,分配 msgid 并写入初始状态""" msgid = uuid.uuid4().hex[:16] print(f"[SUBMIT] msgid={msgid} phone={phone} time={datetime.now()}") return { "msgid": msgid, "phone": phone, "content": content, "status": "SUBMITTED", "channel": "", } def route_sms(msg): """网关侧按号段路由,这里用 mock 通道代替真实 CMPP/SMPP 连接""" if msg["phone"].startswith("138"): msg["channel"] = "cmpp-mock" else: msg["channel"] = "smpp-mock" msg["status"] = "ACK" print(f"[ROUTE] msgid={msg['msgid']} channel={msg['channel']} status={msg['status']}") return msg def deliver_report(msg, delay: float = 1): """模拟运营商回执,默认投递成功 DELIVRD""" time.sleep(delay) msg["status"] = "DELIVRD" print(f"[REPORT] msgid={msg['msgid']} stat=DELIVRD done_time={datetime.now()}") return msg if __name__ == "__main__": msg = submit_sms("13800009999", "验证码:123456,5分钟内有效。") msg = route_sms(msg) msg = deliver_report(msg) print(f"[FINAL] phone={msg['phone']} status={msg['status']}")

保存为 mock_sms_flow.py,在终端执行python3 mock_sms_flow.py。你会看到三条带时间戳的日志,顺序与真实发送流程一致。逻辑上,submit_sms 负责创造消息并分配 msgid,这一步对应真实系统里写发送记录;route_sms 根据号段把消息扔给不同通道,对应网关侧的路由表;deliver_report 延迟一秒后把状态改成 DELIVRD,对应运营商回执。

参数说明里最值得关注的是 delay。它模拟的是“运营商下发给手机”这段网络时间,真实环境里通常 3 到 30 秒。如果 delay 超过你的超时阈值,教案里就该引出一个问题:这条消息算失败吗?答案是不算,只能继续等回执,超时重发要按幂等规则来。

3.2 状态机表格与异步化改造建议

仿真脚本把发送流程压缩成了同步函数,真实系统里每一步都是异步的。教案里对应放一张状态机表,比堆协议字段有用得多。

状态产生方含义下一步动作
SUBMITTED业务系统已生成消息,尚未确认等待网关 ACK
ACK网关网关已接收,不代表送达等待状态报告
SENT网关已送运营商短信中心继续等待最终回执
DELIVRD运营商手机已收到流程结束
UNDELIV运营商投递失败查原因,决定是否重发
EXPIRED运营商超过有效期未下发终止,不重发
REJECTD网关被拒绝,如签名或模板不合法修正后重发

真实系统里,SUBMITTED 之后的消息通常要进消息队列,用 Kafka 或 Redis 做削峰。网关侧提交后,回调线程按 msgid 找到对应记录更新状态;回执线程再按 msgid 更新最终结果。教案里抛一个问题给学员:如果 ACK 还没回来,业务服务重启了,这条消息应该怎么处理?从业方案是落一张发送流水表,以 msgid 为唯一键,重启后从未确认状态捞起来重新查询网关,而不是直接重发。

4. 状态报告与重试:把回执字段读对,短信发送流程才算闭环

4.1 回执字段到底怎么解析:DELIVRD 不是唯一答案

短信发送流程中最容易翻车的环节是回执解析。很多新手看到 stat 字段等于 DELIVRD 就认为成功,看到 UNDELIV 就重发,完全忽略 done_time、err_code 和 submit_date。回执要和原始提交记录按 msgid 关联,先查流水再改状态,否则回执晚到几分钟就会把成功记录覆盖成失败。

# parse_report.py # 回执解析:按 msgid 关联发送记录,区分最终状态和中间状态 REPORT_FINAL_STATES = {"DELIVRD", "UNDELIV", "EXPIRED", "REJECTD"} def handle_report(report: dict, send_records: dict): """report 包含 msgid/stat/done_time,send_records 是发送流水表""" record = send_records.get(report["msgid"]) if record is None: print(f"[ORPHAN] msgid={report['msgid']} 找不到对应提交记录") return stat = report.get("stat", "") print(f"[REPORT] msgid={report['msgid']} old={record['status']} new={stat} done={report.get('done_time')}") # 只做最终状态覆盖,避免 ACK/SENT 中间状态把结果改脏 if stat in REPORT_FINAL_STATES: record["status"] = stat if stat != "DELIVRD": print(f"[ALERT] msgid={report['msgid']} 失败原因 err_code={report.get('err_code')}")

这里的关键是按 msgid 找到原始记录这一行。真实通道回执有两个最常见的坑:一是回执里的 msgid 是你提交时返回的,不是你自己生成的 UUID,需要建立映射;二是回执可能乱序,DELIVRD 先到、SENT 后到,所以只允许最终状态覆盖最终状态。代码里的 REPORT_FINAL_STATES 就是用来拦截中间状态的过滤器,没有它,晚到的 SENT 会把 DELIVRD 覆盖成“已发送”,线上就会出“明明成功却显示发送中”的幻觉。

4.2 重试退避与幂等:三个必调参数

短信重发是最容易引发客诉的操作。用户收不到验证码,业务侧第一反应是重发,结果网关回执只是延迟,用户最后收到两三条相同验证码。教案里要明确:重试只针对“明确知道网关没收到的场景”,例如 ACK 超时或 NACK,收到 UNDELIV、EXPIRED 这种运营商回执后不要自动重发,转人工或者走语音验证码兜底。

# retry_policy.py # 短信重试策略:指数退避 + 幂等保护 RETRY_CONFIG = { "max_retry": 3, "base_interval_sec": 5, "timeout_sec": 30, "power": 2, } def should_retry(status: str, retry_count: int) -> bool: """只有 ACK 超时才重试,明确的运营商失败状态不重试""" if status in ("UNDELIV", "EXPIRED", "REJECTD"): return False if retry_count >= RETRY_CONFIG["max_retry"]: return False return True def next_interval(retry_count: int) -> int: """退避间隔:5s、10s、20s,指数递增""" return RETRY_CONFIG["base_interval_sec"] * (RETRY_CONFIG["power"] ** retry_count)

参数上最值得调整的是 timeout_sec。国内验证码通道业内常见阈值是 30 到 60 秒,如果设成 5 秒,基本每次都会误判超时然后重发。base_interval_sec 建议从 5 秒起,不要用固定 1 秒去轰炸网关。max_retry 超过 3 次意义不大,因为第 4 次重发时用户已经去点“获取语音验证码”了。

幂等部分必须落到数据库唯一索引上。从业方案里,发送流水表以 request_id 为唯一键,同一业务请求重复提交时直接返回已有 msgid,网关侧再配合业务侧的流水号去重,才能扛住双重重试。只靠 Redis 分布式锁不够,锁过期后第二次请求还是会进来。

5. 短信发送流程的避坑清单:从乱码到到达率玄学的 5 个真实场景

5.1 短信内容乱码,签名被截断

现象:用户收到的短信出现火星文,或者签名“【XX银行】”消失。原因:网关侧按 GSM-7 编码解析你传的 UTF-8 字符串,中文字符被拆错;另一种情况是短信超过 70 个字符被按长短信拆分,签名拼接位置不对被截断。解决:统一在提交参数里指定编码为 UCS-2,中文内容按 70 字一计费单位;长短信用网关的拼接能力,签名放在内容开头而不是结尾,并且不要把签名算进变量长度里。

5.2 状态显示 DELIVRD,用户硬说没收到

现象:发送记录里 stat=DELIVRD,用户投诉没收到。原因:DELIVRD 只代表运营商短信中心下发给手机成功,不代表用户看到了。手机关机后开机补发、短信被手机系统拦截、伪基站干扰,都会造成这种“假成功”。解决:别急着反驳用户,先按手机号找通道方要运营商侧的下发详情,确认短信中心下发时间和终端状态;同时让用户检查拦截列表。教案里要强调,回执是最终参考,不是唯一真相。

5.3 ACK 超时重发,用户收到两条验证码

现象:同一验证码短时间内收到两条短信,时间间隔 1 分钟左右。原因:业务侧设置的超时时间太短,网关实际已接收但 ACK 回传延迟,业务侧判断超时后重发。解决:把幂等键落到 request_id,重发前先查同一 request_id 的提交记录;超时后先调网关查询接口确认消息状态,不要直接重发。这条在教案里应该单独作为反面案例,比讲十页协议格式都管用。

5.4 测试环境一切正常,生产环境全部拒收

现象:同一套代码,测试通道跑得通,切到生产通道后短信全部被拦截或回执 REJECTD。原因:生产签名或模板没有报备,报备的签名与提交的签名不一致,变量内容与模板不匹配。解决:把签名、模板 ID、变量内容拆成三个字段管理,提交前做本地校验;测试环境也要用生产同款签名做“影子报备”,不要让开发裸奔到上线才暴露。很多公司把这条归为配置问题,实际是流程缺失。

5.5 回执永远不来,全部卡在 ACK

现象:发送后网关 ACK 正常,但状态报告一直不回调,流水全停在 ACK。原因:提交时没有开启回执开关,或者回调地址配错、回调验签失败被网关丢弃。解决:提交参数里把 registered_delivery 置为 1;检查回调接口是否返回 200,验签失败时要能看到日志而不是静默丢弃。这个坑我见过多次,多数是网关文档里叫回执标志,开发漏传了。

现象首选排查位置误判代价
乱码截断编码参数与签名位置用户看不懂短信
DELIVRD 但没收到运营商下发日志客诉升级
重复验证码幂等键与超时配置资金安全风险
生产拒收签名模板报备上线失败
卡在 ACK回执开关与回调日志功能不可用

6. 用教案做验收:从链路图到消息追踪练习

6.1 第一课先画链路图,再谈协议

我带人学短信发送流程时,第一个动作不是看 pptx 里的报文格式,而是让他在白板上画出六个节点:业务系统 → 发送流水 → 网关接口 → 运营商短信中心 → 用户手机 → 状态报告回流。每个节点写清楚状态字段和超时阈值,画完这张图,协议字段才有位置安放。教案里应该把这张链路图作为课前作业,而不是教学内容。

6.2 必考题:三分钟定位一条丢了的短信

最后一个练习,我会出一道必考题:用户在晚上 23:00 说没收到验证码,给你 msgid,三分钟内说出定位步骤。正确顺序是:第一步查流水,确认状态停在 SUBMITTED 还是 ACK;第二步查网关日志,确认 ACK 有没有回来;第三步调通道方查询接口拿到运营商回执;第四步看手机是否正常。每一步都有明确的负责人和后手,而不是一句“帮我查一下”。这条链路走一遍,教案的内容才算真正闭环。我现在的习惯是,任何短信接入项目,先备好语音验证码兜底,再把回执解析和幂等重试写成单元测试,最后才调 UI。希望帮到你。

本文还有配套的精品资源,点击获取

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

花卉识别数据集5类实战:从数据划分到迁移学习分类器

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

作者头像 李华
网站建设 2026/9/29 2:01:55

VC2010Express中文版2025年使用指南:安装配置与C++编译实战

简介:Visual C 2010 Express 简体中文离线独立安装包,面向刚接触 C 编程的初学者、高校学生以及需要搭建本地开发环境的教学人员。它解决的是在线安装受网络波动影响、组件下载不全的问题,一次解压即可在无网或弱网环境下完成部署&#xff0c…

作者头像 李华
网站建设 2026/9/29 2:01:13

机器学习驱动的工业物联网入侵检测:从特征工程到异常识别

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

作者头像 李华
网站建设 2026/9/29 2:01:08

38个AI视频生成网站实测:从新手到批量生产的完整指南

看到“38个AI智能生成视频的网站”这类标题,很多人第一反应是收藏,然后就没有然后了。因为大多数合集只是扔给你一堆链接,看完根本不知道从哪下手。我花了大概两周时间,把市面上主流的AI视频生成平台挨个测了一遍,从生…

作者头像 李华
网站建设 2026/9/29 2:01:00

驱动电路能力四要素:峰值电流、开关速度、抗扰性与负载匹配

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

作者头像 李华
网站建设 2026/9/29 2:00:31

Unity数字孪生机械臂虚实联动实战指南

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

作者头像 李华