ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

短信业务流程分析:从下发链路到到达率漏斗的完整拆解

短信业务流程分析:从下发链路到到达率漏斗的完整拆解 简介这份PPT文档面向通信、嵌入式开发及管理信息化方向的从业者与学习者系统梳理短信业务流程中的关键技术环节帮助读者理解SMS从发送到投递的完整链路。资源共1个pptx文件压缩包约1.16MB以图文幻灯片形式组织便于课堂讲解、技术培训或自学查阅。内容围绕存储转发模式展开讲解SMSC在接收方离线时的暂存与补投机制并对比PDU与Text两种发送模式的差异说明PDU模式对中文短信及7-bit、8-bit、UCS2三种编码的支持优势。文档还整理了ATCMGC、ATCMGF、ATCMGS、ATCSCA等常用AT指令的功能与用法并逐项拆解PDU编码的A至M字段构成配合“工作愉快”等实例演示短信中心号码与接收方号码的奇偶位交换、国际化标志添加及长度计算过程。目前已有171人学习适合需要掌握短信协议细节、排查编码问题或优化短信服务系统的读者参考。1. 短信业务流程分析从一条下发短信倒推整条链路一条营销短信从点击“发送”到用户手机震动中间要穿过网关、路由、计费、状态回执四道关卡任何一道卡住运营看到的就只是“发送成功、到达率 60%”这种玄学报表。短信业务流程分析要解决的就是把这条黑匣子链路拆成可观测、可复现、可优化的节点让做运营、做后端、做风控的人都能定位到具体是哪一跳出了问题。它适合三类人需要给业务方解释“为什么这条短信没到”的后端工程师、要优化到达率和成本的运营技术、以及做短信通道对接的集成开发。下面按“链路是什么 → 怎么拆 → 怎么落地分析 → 坑在哪”的顺序讲透每一步都能照着复现。2. 短信下发链路的五个核心节点与状态机2.1 从提交到回执的完整时序一条短信的完整生命周期可以拆成五个节点业务系统提交Submit、短信网关接入Access、路由选路Route、运营商下发Deliver、状态回执Report。每个节点都有独立的状态码业务侧看到的“成功”往往只是 Submit 成功不代表 Deliver 成功。常见做法是用一张状态机表来跟踪每条短信的流转。下面是一个简化的状态流转定义用 Python 字典描述方便直接嵌入分析脚本# 短信状态机定义每个状态允许流转到的下一状态 SMS_STATE_MACHINE { SUBMITTED: [ACCEPTED, REJECTED], # 业务提交后网关接入或拒绝 ACCEPTED: [ROUTING, FAILED], # 网关接收进入选路 ROUTING: [DISPATCHED, NO_ROUTE], # 选路成功或无可达通道 DISPATCHED: [DELIVERED, UNDELIVERED], # 已下发运营商等待回执 DELIVERED: [], # 终态用户已收到 UNDELIVERED: [], # 终态下发失败 REJECTED: [], # 终态网关拒绝 FAILED: [], # 终态系统异常 NO_ROUTE: [], # 终态无可用通道 } def next_states(current): 查询当前状态可流转的下一状态列表 return SMS_STATE_MACHINE.get(current, []) # 示例查询 DISPATCHED 之后可能的状态 print(next_states(DISPATCHED)) # 输出 [DELIVERED, UNDELIVERED]这段代码的逻辑很直白把每个状态当作一个节点用字典的 value 列表表示出边。参数说明——SMS_STATE_MACHINE的 key 是状态名value 是可达状态列表next_states函数用于在分析脚本里快速判断某条短信是否卡在非终态。实际项目中状态机通常存在数据库的sms_record表里字段包括sms_id、current_state、submit_time、deliver_time、report_time、error_code。2.2 各节点的关键参数与观测指标每个节点要采集的字段不同下面这张表是落地分析时最少要落库的字段清单节点关键字段典型耗时异常信号Submit业务ID、手机号、内容长度、提交时间10ms参数校验失败、频控拦截Access网关流水号、接入协议、签名验证结果10-50ms签名不匹配、IP 白名单拒绝Route通道ID、路由策略、号码归属地5-20ms无可用通道、通道权重为0Deliver运营商返回码、下发时间1-30s运营商限流、号码空号Report回执状态、回执时间、错误码异步秒到分钟级回执丢失、状态码映射错误采集这些字段时最容易翻车的是时间戳精度。Submit 用毫秒级Deliver 和 Report 用秒级就够但必须统一时区。我一般会在入库时全部转成 UTC 毫秒时间戳分析时再按业务时区展示。2.3 用 SQL 做链路耗时分解有了上面的字段就可以用一条 SQL 把每条短信的端到端耗时拆开。下面这段 SQL 假设表名为sms_record字段名与上表一致-- 计算各阶段耗时单位毫秒 SELECT sms_id, phone, -- 接入耗时网关接收时间 - 业务提交时间 (access_time - submit_time) AS access_cost_ms, -- 选路耗时下发时间 - 网关接收时间 (deliver_time - access_time) AS route_cost_ms, -- 下发耗时回执时间 - 下发时间 (report_time - deliver_time) AS deliver_cost_ms, -- 端到端耗时 (report_time - submit_time) AS total_cost_ms, current_state, error_code FROM sms_record WHERE submit_time 2025-01-01 00:00:00 AND submit_time 2025-01-02 00:00:00 ORDER BY total_cost_ms DESC LIMIT 100;逻辑说明用相邻节点的时间戳相减得到各阶段耗时total_cost_ms是端到端耗时。参数说明——时间范围按业务需求调整LIMIT 100用于快速查看最慢的短信。如果access_cost_ms普遍超过 100ms说明网关接入层有瓶颈如果deliver_cost_ms波动大通常是运营商侧限流或回执延迟。这条 SQL 可以直接放进 BI 看板按小时聚合就能看出链路健康度的趋势。3. 用 Python 脚本复现一次完整的业务流程分析3.1 构造模拟数据集没有生产数据时可以用脚本生成一批模拟短信记录用来验证分析逻辑。下面这段代码生成 1000 条模拟记录覆盖成功、失败、无路由三种典型场景import random import time from datetime import datetime, timedelta def generate_sms_records(n1000): 生成模拟短信记录用于分析脚本自测 records [] base_time datetime(2025, 1, 1, 0, 0, 0) for i in range(n): submit base_time timedelta(secondsrandom.randint(0, 86400)) # 模拟 85% 成功、10% 下发失败、5% 无路由 r random.random() if r 0.85: state DELIVERED access submit timedelta(millisecondsrandom.randint(10, 80)) deliver access timedelta(millisecondsrandom.randint(5, 30)) report deliver timedelta(secondsrandom.randint(1, 30)) error_code 0 elif r 0.95: state UNDELIVERED access submit timedelta(millisecondsrandom.randint(10, 80)) deliver access timedelta(millisecondsrandom.randint(5, 30)) report deliver timedelta(secondsrandom.randint(1, 60)) error_code random.choice([MK:0011, MK:0024, DB:0141]) else: state NO_ROUTE access submit timedelta(millisecondsrandom.randint(10, 80)) deliver None report None error_code NO_CHANNEL records.append({ sms_id: fSMS{i:06d}, phone: f138{random.randint(10000000, 99999999)}, submit_time: submit, access_time: access, deliver_time: deliver, report_time: report, current_state: state, error_code: error_code, }) return records # 生成并查看前 3 条 data generate_sms_records(1000) for rec in data[:3]: print(rec)逻辑说明generate_sms_records按概率分布生成三种状态的记录时间戳逐级递增。参数说明——n控制生成条数r 0.85控制成功率实际使用时可以调整这些比例来模拟不同通道质量。error_code用了运营商常见错误码前缀方便后续做错误分类统计。3.2 统计各状态占比与错误码分布拿到数据后第一步是看整体健康度。下面这段代码统计状态分布和 Top 错误码from collections import Counter def analyze_status(records): 统计状态分布和错误码 Top5 states Counter(r[current_state] for r in records) total len(records) print( 状态分布 ) for state, count in states.most_common(): print(f{state}: {count} ({count/total*100:.1f}%)) # 只统计失败状态的错误码 fail_codes Counter( r[error_code] for r in records if r[current_state] in (UNDELIVERED, NO_ROUTE, REJECTED, FAILED) ) print(\n 失败错误码 Top5 ) for code, count in fail_codes.most_common(5): print(f{code}: {count}) analyze_status(data)逻辑说明用Counter做频次统计most_common()按降序返回。参数说明——fail_codes只统计非成功状态避免把成功记录的error_code0混进来。实际生产中如果某个错误码突然飙升比如MK:0011通常表示运营商限流就需要立刻检查对应通道的发送速率。3.3 计算各阶段耗时分位数平均值会被极端值拉偏分析链路耗时必须看分位数。下面这段代码计算 P50、P90、P99import statistics def latency_percentiles(records): 计算各阶段耗时的 P50/P90/P99单位毫秒 stages {access: [], route: [], deliver: [], total: []} for r in records: if r[access_time] and r[submit_time]: stages[access].append( (r[access_time] - r[submit_time]).total_seconds() * 1000) if r[deliver_time] and r[access_time]: stages[route].append( (r[deliver_time] - r[access_time]).total_seconds() * 1000) if r[report_time] and r[deliver_time]: stages[deliver].append( (r[report_time] - r[deliver_time]).total_seconds() * 1000) if r[report_time] and r[submit_time]: stages[total].append( (r[report_time] - r[submit_time]).total_seconds() * 1000) print(f{阶段:10} {P50:8} {P90:8} {P99:8}) for name, values in stages.items(): if not values: continue values.sort() p50 values[int(len(values) * 0.5)] p90 values[int(len(values) * 0.9)] p99 values[int(len(values) * 0.99)] print(f{name:10} {p50:8.0f} {p90:8.0f} {p99:8.0f}) latency_percentiles(data)逻辑说明分别收集四个阶段的耗时列表排序后按索引取分位数。参数说明——int(len(values) * 0.5)是简化的分位数取法生产环境建议用numpy.percentile更精确。如果deliver阶段的 P99 超过 30 秒说明回执延迟严重需要检查运营商回执接口是否积压。4. 短信业务流程分析避坑五个血泪教训4.1 把 Submit 成功当成送达成功现象运营报表显示发送成功率 99%但用户投诉没收到。原因业务系统只记录了 Submit 状态没有等回执就标记为成功。解决在sms_record表里增加current_state字段报表只统计DELIVERED状态Submit 成功单独作为“提交成功率”展示。4.2 回执状态码映射错误现象明明用户已收到系统却标记为UNDELIVERED。原因不同运营商的回执码含义不同比如DELIVRD和0都表示成功但代码里只认0。解决维护一张运营商回执码映射表把各家的成功码统一映射为内部状态DELIVERED。下面是一个映射表示例# 运营商回执码映射key 是运营商原始码value 是内部状态 REPORT_CODE_MAP { DELIVRD: DELIVERED, 0: DELIVERED, UNDELIV: UNDELIVERED, MK:0011: UNDELIVERED, # 运营商限流 DB:0141: UNDELIVERED, # 号码空号 NO_CHANNEL: NO_ROUTE, } def map_report_code(raw_code): 将运营商回执码映射为内部状态未知码归为 UNDELIVERED return REPORT_CODE_MAP.get(raw_code, UNDELIVERED)参数说明——REPORT_CODE_MAP需要根据实际对接的运营商文档持续补充未知码默认归为失败避免漏统计。4.3 时间戳时区不统一现象链路耗时出现负数。原因Submit 时间用了本地时区Report 时间用了 UTC。解决入库时全部转成 UTC 毫秒时间戳展示时再转回业务时区。我一般会在数据库连接层强制设置time_zone00:00从源头避免。4.4 忽略长短信的拆分计费现象一条 200 字的短信被拆成 3 条计费但分析时只按 1 条统计。原因长短信在网关侧会拆分成多条提交每条有独立的sms_id。解决在 Submit 节点增加concat_id字段把同一条长短信的多个分片关联起来统计时按concat_id聚合。4.5 回执丢失没有补偿机制现象部分短信永远停留在DISPATCHED状态。原因运营商回执接口偶发丢包没有重试或主动查询。解决增加定时任务对超过 5 分钟仍处于DISPATCHED的记录主动调用运营商的回执查询接口查到结果后更新状态。这个补偿逻辑用 Python 写一个定时脚本即可# 伪代码主动查询超时未回执的记录 # 实际使用时替换为运营商的查询 API def query_pending_reports(db, timeout_seconds300): 查询超过 timeout_seconds 仍未回执的记录并主动拉取 pending db.query( SELECT sms_id, channel_id FROM sms_record WHERE current_state DISPATCHED AND deliver_time NOW() - INTERVAL %s SECOND, (timeout_seconds,) ) for sms_id, channel_id in pending: status call_carrier_query_api(channel_id, sms_id) if status: db.update(UPDATE sms_record SET current_state %s WHERE sms_id %s, (status, sms_id))参数说明——timeout_seconds默认 300 秒可根据运营商回执速度调整call_carrier_query_api需要替换为实际接口。5. 用漏斗模型定位到达率瓶颈一个可复用的分析技巧前面讲的是单条短信的链路拆解但实际运营中更常见的问题是“整体到达率突然下降”。这时候用漏斗模型比逐条排查高效得多。漏斗的思路很简单把 Submit → Access → Route → Deliver → Report 五个节点当作五层漏斗每层的通过率就是该节点的转化率哪一层转化率骤降瓶颈就在哪。具体做法是写一个聚合查询按小时统计各状态的累计数量然后计算相邻节点的转化率。下面这段 SQL 可以直接用-- 按小时统计漏斗各层数量 SELECT DATE_FORMAT(submit_time, %Y-%m-%d %H:00:00) AS hour, COUNT(*) AS submitted, SUM(CASE WHEN access_time IS NOT NULL THEN 1 ELSE 0 END) AS accessed, SUM(CASE WHEN deliver_time IS NOT NULL THEN 1 ELSE 0 END) AS dispatched, SUM(CASE WHEN current_state DELIVERED THEN 1 ELSE 0 END) AS delivered FROM sms_record WHERE submit_time 2025-01-01 00:00:00 GROUP BY hour ORDER BY hour;拿到结果后用 Python 算转化率并画出趋势def funnel_rates(rows): 计算漏斗各层转化率 for row in rows: hour, sub, acc, disp, deliv row if sub 0: continue print(f{hour} | 提交{sub} | 接入率{acc/sub*100:.1f}% f| 下发率{disp/acc*100:.1f}% | 到达率{deliv/disp*100:.1f}%)参数说明——acc/sub是接入率低于 95% 说明网关接入层有问题disp/acc是下发率低于 90% 说明选路或通道有问题deliv/disp是到达率低于 85% 说明运营商侧或号码质量问题。我一般会把这个漏斗做成小时级看板一旦某层转化率比基线低 5 个百分点就触发告警。这个技巧的价值在于它把“到达率下降”这个模糊问题变成了“哪一层转化率异常”的具体定位。比如某天下午 3 点到达率从 88% 掉到 72%看漏斗发现接入率正常、下发率正常但到达率骤降那问题就在运营商侧或号码库质量不用去查网关和路由。反过来如果接入率掉了那就是网关或签名验证的问题跟运营商无关。最后说一个我自己的习惯每次做短信业务流程分析我都会先把状态机和漏斗这两张图贴在工位上状态机用来排查单条问题漏斗用来监控整体健康度。这两个工具配合使用基本能覆盖 90% 的日常分析场景。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进