☰
移动云MAS短信接口落地:报备、鉴权、状态报告与频控实践
2026/10/31 3:18:05 网站建设 项目流程

企业短信这活儿,看着就是把一段文字塞给运营商发出去,真要落地到生产环境,麻烦事全在接口之外——资质报备卡两周、签名被驳回三次、模板变量位置填错、长短信被拆成两条计费、状态报告对不上号导致业务侧永远显示"发送中"。移动云MAS平台提供的短信接口,本质上就是给企业开一扇受管控的发送通道,它把鉴权、模板校验、频控、状态回执这些脏活揽过去,但接口怎么调、参数怎么拼、回执怎么处理,还得自己啃。这篇内容适合三类人看:正在给业务系统接短信能力、被报备流程折磨过的后端同学;需要评估用HTTP短连接还是CMPP长连接选型的架构同学;以及已经接通了但线上偶发失败率偏高、想搞清楚根因的运维同学。接下来我把这套接口从申请到上线的完整链路拆开讲,重点放在那些文档里不会写、只有踩过才知道的地方。

1. 先搞清楚MAS接口解决什么问题,别一上来就写代码

1.1 企业短信的三条通路与MAS的定位

企业想给用户发短信,物理上有三条路可走:一是买短信猫或者插SIM卡的设备自己发,二是找第三方短信服务商做代理转发,三是直接对接运营商提供的企业短信平台。第一条路在合规性和到达率上基本走不通,设备被限速、被判定为异常发送是家常便饭;第二条路省事但中间多了一层,出问题时你拿不到运营商的原始回执,只能看到服务商二次封装的结果,"发送失败"这四个字背后到底是号码空号还是通道拥塞,你一无所知。

MAS平台属于第三条路,坐落在运营商侧,企业以独立账号身份接入,短信从平台直接下发到目标号码所属的网关。这个定位带来的最大差别是回执的真实性和通道的可控性。你能拿到运营商网关返回的原始状态码,比如DELIVRD表示对方手机已收到,UNDELIV表示投递失败,EXPIRED表示有效期超时未送达,这些状态码直接对应网关侧的真实结果,而不是某个中间层给你翻译过的模糊描述。

代价也很明显:接入门槛高。账号要在企业主体名下开通,签名要报备,模板要审核,发送内容受严格管控。这套流程对做营销的人来说是束缚,但对做验证码、订单通知、告警提醒这类事务性短信的团队来说反而是好事——通道干净,到达率稳定,不会因为同平台有人在发垃圾短信把你的通道质量一起拖下去。

1.2 接口能力边界:它能发什么、不能发什么

接入前必须先想清楚业务场景落在哪一档,因为不同档位的报备要求和审核难度差异很大。事务性短信(验证码、订单状态、账单提醒、系统告警)审核最快,只要模板里不掺营销话术基本当天能过。通知类短信(会员到期、物流更新、会议提醒)次之,需要提供业务场景说明。营销类短信(促销活动、优惠券发放)审核最严,通常要求用户明确授权证明,发送时段也有限制。

MAS接口本身在技术上是通用的,它不管你发的是什么内容,但平台的策略层会拦。有个很典型的坑:有人把验证码模板写成「您的验证码是{1},关注公众号领取新人礼包」,想着顺手带一句营销,结果模板审核直接驳回,理由是该模板混合了事务性和营销内容。后来拆成两条模板分别报备才过。模板的单一职责原则比代码里的单一职责原则还要严格,一条模板只干一件事。

还有一类需求要提前判断:语音验证码、国际短信、彩信。MAS平台在不同省份部署的模块不完全一样,语音验证码和彩信往往是独立产品线,需要单独开通,不能指望同一个接口一把梭。国际短信的覆盖范围和资费差异极大,得单独确认目标国家在不在支持列表里。这些在写第一行代码之前就要跟对接人问清楚,否则架构设计完了才发现能力不支持,返工成本很高。

2. 账号开通与资质报备:真正决定项目周期的是这一步

2.1 签名与模板报备的材料清单和审核逻辑

技术同学最容易低估的就是这一步的耗时。代码两天能写完,报备可能要两周。签名报备的核心是证明"这个签名代表的主体是真实存在且有权发送短信的"。常规材料包括营业执照、法人身份证明、经办人授权书,签名内容一般是企业简称或产品名,长度通常限制在3到8个汉字,不能含"中国""国家""官方"这类容易引起误解的词,也不能用纯英文或特殊符号。

签名驳回的高频原因有几个:一是签名和营业执照主体名称对不上,比如执照是「某某网络科技有限公司」,签名报「某某生活」,这时候需要补充说明两者关系,或者提供商标注册证明;二是签名里带了行业敏感词,比如「金融」「贷款」「医疗」这类,会被要求额外提交行业资质;三是签名重复,同一个签名已经被其他账号占用且未提供授权关系。

报备的时间线我的建议是这样安排:第一天提交签名,同时并行准备模板;签名通过后再提交模板(部分省份允许并行,但模板绑定签名,签名没过模板审核也没法继续);模板通过后申请测试账号,用测试账号跑通链路;测试通过再切换到正式账号。整个过程按七到十个工作日预留比较稳妥,如果是营销类模板或者需要补充材料,再往后延一周。

2.2 模板变量占位符的写法陷阱

模板里用变量占位,写法各家平台略有差异,MAS常见的是用{1}、{2}这种数字占位,也有用${code}形式的,具体看你拿到的文档。这里面的坑集中在三处。

第一处是变量的语义边界。模板「您的订单{1}已发货」里,{1}只能填订单号,不能填一段带标点的话。有人图省事把整句话塞进变量里,审核会驳回,因为变量内容不可控,平台无法预判你实际会发什么。变量应该是短小的、格式固定的值,比如数字、日期、十几位以内的编码。

第二处是变量数量和实际传参的对齐。模板报备了三个变量,代码里只传了两个,接口不一定报错,但下发出去的内容会缺一块或者变量原样显示成{3}。这个必须在代码层做校验,发送前统计模板变量个数,和参数数组长度比对,不一致直接抛异常拦在本地,别指望平台帮你兜。

第三处是变量内容里的特殊字符。变量值如果包含&、=、+、%这类字符,走表单编码的接口会被转义,走JSON的相对安全但也要注意。更麻烦的是变量值里如果出现了和占位符同形的字符串,比如用户昵称里带了{1},虽然实际渲染时通常不会二次解析,但为了保险,变量内容里出现花括号时建议直接过滤或替换。

2.3 测试账号与正式账号的隔离

测试账号通常只能发给白名单里的号码,而且内容受限,一般不允许发真实营销内容。这个阶段要做的事情不是简单验证"能发出去",而是把后面要讲的异常分支全部跑一遍:号码格式非法、内容超长、模板变量缺失、并发超限、重复提交。测试账号的频控阈值往往比正式账号低,正好方便你压出限流行为。

有个细节值得注意:测试账号和正式账号的签名可能不通用。有的省份测试账号用固定测试签名,正式签名审核通过后才能在正式账号上使用。切换的时候记得把配置里的签名值同步改掉,我见过上线当天因为签名还是测试签名导致全量失败的案例。

3. 协议选型:HTTP短连接、WebService与CMPP长连接怎么挑

3.1 三种接入方式的能力对照

MAS平台一般提供三种接入形态,能力差异比想象中大,选错了后期改造代价不小。

维度HTTP短连接WebService(SOAP)CMPP长连接
协议基础HTTP/HTTPS + JSON或表单HTTP + XML/SOAPTCP私有二进制协议
连接模型每次请求新建或复用连接每次请求一个SOAP信封长连接,需登录鉴权与心跳保活
单次发送一次请求一条或一批号码同左,报文体积大一条Submit包对应一条短信
状态报告平台回调客户配置的地址平台回调通过Deliver包在同一连接上回传
上行短信平台回调平台回调Deliver包回传
吞吐能力中等,受HTTP开销限制偏低,XML序列化成本高高,适合大批量
开发成本低中高,需要处理粘包、心跳、重连
适用场景中小流量、事务性短信为主已有SOAP体系的老系统日均十万条以上、需要高吞吐

HTTP短连接是最容易上手的,请求体就是一段JSON或者表单,返回也是一段JSON,调试的时候curl一把就能看到结果。它的短板在吞吐上,每条短信一次HTTP往返,加上TLS握手(如果走HTTPS),单机QPS上不去,而且平台侧对HTTP接口通常有独立的限流策略。

CMPP是运营商体系里的标准短信网关协议,二进制PDU,需要自己实现Connect登录、Submit提交、Deliver接收、ActiveTest心跳。它的优势是长连接复用、吞吐高、状态报告和上行短信都在同一条连接上回来,不用额外开放回调端口。但开发复杂度高一个量级,粘包拆包、超时重连、序号管理、窗口控制都得自己处理,没有成熟的客户端库就得啃协议文档。

WebService现在用得少了,主要是历史系统兼容。SOAP信封套一层XML,报文体积是JSON的好几倍,解析也重,除非对接方强制要求,一般不建议新项目选它。

3.2 我们最终选HTTP的原因与它的代价

我们当时的场景是验证码加订单通知,日均峰值大概在两万条左右,集中在早晚两个时段。选HTTP的理由很直接:流量没大到需要长连接压榨吞吐,而团队里没人写过CMPP,为一个非核心功能引入一个需要长期维护的私有协议客户端,风险和收益不成比例。

但选HTTP要接受它的代价,并且提前做补偿。第一个代价是没有原生回执通道。CMPP长连接上状态报告是顺着连接回来的,HTTP模式下必须由平台主动POST到你提供的一个公网回调地址。这意味着你要额外维护一个对外的HTTP端点,要考虑它的可用性、鉴权、防重放,还要保证它在公网可访问——内网部署的系统得想办法做映射或者用反向通道。第二个代价是单次请求的号码数量受限。接口一般支持一次传多个号码,逗号分隔,但平台对单次号码数有上限(常见是几十到几百个),超过要分批。

还有一个隐性代价是连接建立的抖动。如果接口地址是HTTPS,每次请求都要走一次完整握手,网络稍微抖一下就超时。我们的做法是在客户端侧维护连接池,给请求设置合理的超时(连接超时3秒、读超时5秒),并且对超时请求做有限次重试——但重试必须配合幂等设计,这个后面细说。

4. 发送链路的核心参数与鉴权机制拆解

4.1 鉴权字段的语义与mac计算逻辑

MAS的HTTP接口鉴权不是常见的Token或者AK/SK签名,而是一套基于字段拼接做摘要的机制。请求里需要带企业名称、账号ID、密钥、手机号串、内容、签名、扩展码这几个字段,然后把它们按固定顺序拼成一个长字符串,做MD5摘要,再Base64编码,结果作为mac字段一起提交。服务端会用同样的规则重算一遍,比对一致才受理。

用Python写出来大概是这样:

import hashlib import base64 def build_mac(ec_name: str, ap_id: str, secret_key: str, mobiles: str, content: str, sign: str, add_serial: str = "") -> str: raw = ec_name + ap_id + secret_key + mobiles + content + sign + add_serial md5_bytes = hashlib.md5(raw.encode("utf-8")).digest() return base64.b64encode(md5_bytes).decode("utf-8")

这段代码有三个必须注意的点。拼接顺序不能错,差一个字段的位置算出来的结果完全不同,而且服务端只会告诉你鉴权失败,不会告诉你是哪个字段错了。编码统一用UTF-8,中文内容和英文内容混在一起时,用GBK或者平台默认编码算出来的字节序列不一样,mac必然不匹配。区分digest和hexdigest,有些文档写的是"MD5后Base64",指的是对16字节的原始摘要做Base64;如果文档写的是"MD5转小写字符串再Base64",那就是对32位十六进制字符串做Base64,两者结果完全不同。这一点最容易搞错,建议拿到文档后先让对方提供一个样例输入和期望输出,自己算一遍对上再往下写。

调试阶段可以加一个开关,把参与拼接的原始字符串和算出的mac都打到日志里(密钥部分脱敏),出问题时能快速定位是拼接错了还是编码错了。

4.2 手机号格式、内容编码与长短信拆分

手机号参数一般要求是纯数字、不带国际区号前缀、多个号码用英文逗号分隔。这里有几个容易翻车的细节:从Excel或者数据库里读出来的号码可能带空格、带横杠、带+86前缀,甚至有全角字符,提交前必须统一清洗成11位纯数字并做正则校验。批量提交时如果其中一个号码格式非法,有的平台会整批拒绝,有的会跳过非法的只发合法的,行为不一致,所以要自己先过滤。

内容编码方面,接口通常要求UTF-8,但短信实际下发时走的是运营商侧的编码体系。纯英文数字按7位编码,一个字符占7bit,一条短信能装160个字符;含中文的走UCS2编码,一个字符占16bit,一条短信装70个字符。超过单条长度的内容会被拆成多条长短信,拆分时每条要留出头部开销(通常是6到7个字节用于拼接标识),所以实际每条能装67个中文字符左右。

计费按拆分后的条数算,这一点必须在业务侧提前对齐。一条200字的通知短信会被拆成3条,用户收到的是拼在一起的完整内容,但账单上是3条。如果你的业务量大且内容偏长,这个成本差异很可观。我们的做法是在内容生成环节就做长度检查,超过140个字符的通知类内容强制走站内信或者App推送,只有关键信息才用短信。

另外要注意签名是否计入长度。多数情况下平台会把签名拼在内容前面一起下发,签名本身占用的字符数也要算进去。报备模板的时候可以把签名长度和变量长度都算进去,估算出最坏情况下的拆分条数,写进模板说明里,避免上线后被账单吓一跳。

4.3 一次完整调用的代码骨架

把上面这些拼起来,一次发送大概是这个形态:

import json import requests def send_sms(mobiles, content, sign): mobiles_str = ",".join(mobiles) mac = build_mac(EC_NAME, AP_ID, SECRET_KEY, mobiles_str, content, sign) payload = { "ecName": EC_NAME, "apId": AP_ID, "secretKey": SECRET_KEY, "mobiles": mobiles_str, "content": content, "sign": sign, "addSerial": "", "mac": mac, } body = json.dumps(payload, ensure_ascii=False).encode("utf-8") resp = requests.post( SUBMIT_URL, data=body, headers={"Content-Type": "application/json;charset=utf-8"}, timeout=(3, 5), ) return resp.json()

两个细节:ensure_ascii=False必须加,否则中文会被转成\uXXXX,虽然JSON解析后内容一样,但如果你在客户端算mac用的是原文、服务端拿到的是转义后的字符,摘要就对不上。Content-Type里的charset=utf-8也建议显式写上,有些网关对缺失charset的请求按默认编码解析,中文内容会乱码。

返回体一般包含一个响应码字段、一个消息组ID和受理状态。受理成功不等于送达成功,这一点是新手最容易误解的。返回成功只代表平台接下了这条短信并进入了下发队列,真正有没有到用户手机上要看后面的状态报告。所以业务侧的状态机不能设计成"接口返回成功就标记已发送",中间必须有一个"已受理"的中间态。

5. 状态报告与上行短信:异步回执才是短信系统的真面目

5.1 状态报告的推送机制与幂等处理

平台下发完短信后,会向你配置的回调地址POST一条状态报告,内容包含消息组ID、手机号、接收时间、状态码等字段。状态码里DELIVRD表示投递成功,UNDELIV表示投递失败,EXPIRED表示超过有效期未送达,还有一些中间态比如ACCEPTD(已受理)、ENROUTE(在途)。

回调处理有三个必须解决的问题。第一个是幂等。同一条短信的状态报告可能重复推送,尤其是在网络抖动、你的服务响应慢导致平台重试的情况下。处理逻辑必须基于"消息组ID + 手机号"做唯一约束,已经处理过的直接返回成功,不重复更新业务状态。我们的做法是建一张短信发送记录表,用消息组ID和手机号做联合唯一索引,回调进来先insert ignore,插入成功才继续后续逻辑。

第二个是乱序。一条短信可能先收到ENROUTE再收到DELIVRD,也可能因为重试导致DELIVRD先到、旧的ENROUTE后到。状态更新要做成单向的、不可逆的,终态(成功/失败/过期)一旦写入就不能被中间态覆盖。用状态机来表达就是:只允许已受理 → 在途 → 终态这样的单向流转,反向流转直接丢弃。

第三个是回调地址的可用性。这个端点是公网暴露的,必须做鉴权。MAS的回调一般没有签名机制,或者只有简单的固定参数校验,所以要在URL里带一个足够长的随机token,同时在网关层做IP白名单(把平台侧的出口IP段加进去),再加一层频率限制防止被刷。处理逻辑要轻,收到回调后只做落库和发消息队列,真正的业务处理异步做,别在回调里同步调下游服务——下游一慢,回调超时,平台重试,雪崩就开始了。

5.2 上行短信与会话保持

用户回复短信会触发上行推送,回调参数里包含发送方号码、回复内容、接收时间。做验证码"回复验证"、退订、满意度调查这类场景会用到。

上行处理的核心是会话语义的还原。用户回复"TD"表示退订,回复"1"表示满意,但这些约定必须和你下发时的引导语严格对应。下发「回复TD退订」就必须能正确识别TD的各种变体(大小写、带空格、全角字符),否则用户投诉退订无效是很麻烦的事。建议在匹配前对上行内容做标准化:去空格、转半角、转大写,再做关键词匹配。

退订的处理要落到号码级别的黑名单里,并且在发送前就过滤掉,不能等发了再退。这个黑名单是全局的,不能按业务线分开——用户在A业务退了订,B业务也不能再给他发,否则就是骚扰。有的平台侧也维护退订名单,会自动过滤,但你不能依赖它,自己这边必须有一份。

6. 上线后最常炸的几个点:从错误码到频控

6.1 失败现象的分类排查路径

线上出问题时,最忌讳的是看到失败就盲目重试。先按现象分类,再定动作。

现象可能原因排查动作
接口返回鉴权失败mac算错、拼接顺序错、密钥不对、编码不一致用固定输入重算mac比对样例值,检查密钥是否被配置中心截断
接口返回参数错误号码格式非法、内容超长、模板变量数不匹配打印完整请求体,逐字段比对文档
接口返回频控单号码发送过频、账号QPS超限、内容重复查频控维度是号码级还是账号级,加本地令牌桶
受理成功但状态报告失败号码空号、关机、拦截、通道拥塞看具体状态码,区分永久失败和临时失败
状态报告完全收不到回调地址不可达、白名单没配、端口未开放从平台侧发起一次连通性测试,看接入日志
用户收到乱码编码不一致、内容里混了特殊字符检查请求编码和Content-Type声明

这里面最容易被误判的是"受理成功但状态报告失败"。业务同学看到成功率只有85%就慌了,其实短信的行业平均到达率就在这个量级,空号、停机、拦截都是正常损耗。要区分的是通道问题还是号码问题:如果同一批号码里失败的集中在某几个号段,可能是该号段的通道拥塞;如果失败的是零散的、状态码多为UNDELIV,那基本就是号码本身的问题。做一个按号段和状态码分组的统计报表,能快速定位。

6.2 频控的三个维度和应对策略

频控有三层,很多人只防了最上面一层。平台层是运营商对企业账号的限制,通常按条/秒或者条/天计算,超了直接拒绝,阈值需要跟对接人确认,有的可以申请提额但要提前报备。号码层是同一个手机号的接收频率限制,一分钟内多条相同内容、一天内超过N条,都可能被拦或者被用户投诉。内容层是相同内容群发的限制,短时间内大量相同文本发往不同号码,会被判定为疑似营销或者骚扰。

应对方式是在客户端做本地令牌桶,按平台给的阈值往下压一点(比如平台允许100条/秒,本地限到80),留出缓冲。号码层的频控要在业务逻辑里做,同一个号码同一类短信一分钟内只发一次,验证码场景还要加上"60秒后可重发"的前端倒计时,双重保险。内容层的规避方式是把相同内容做轻微差异化,比如订单通知里带上订单号,自然就不同了,但验证码这种天然带随机数的反而不受影响。

还有一个隐蔽的坑:重试会放大频控风险。接口超时了,代码重试一次,如果第一次其实已经成功只是响应丢了,那用户就收到两条一样的短信。所以重试必须满足两个条件:只对明确的失败(连接超时、5xx)重试,不对业务失败重试;重试前先查一次本地记录,确认这条消息没有已经成功的迹象。更稳妥的做法是给每条短信生成一个业务侧的唯一ID,作为扩展码或者请求ID传过去,重试时带同一个ID,让平台侧去重。

6.3 幂等设计要贯穿整条链路

短信系统的幂等不是可选项。用户点一次"获取验证码",前端可能因为网络慢被点了三次,后端如果不做防抖,就是三条短信三个费用,用户还会因为收到三条不同验证码而迷惑。

我们的做法是三层防抖:前端按钮点击后立即置灰60秒;网关层对同一用户同一场景做限流,60秒内只放行一次;服务层用"用户ID + 场景 + 时间窗口"生成幂等键,重复请求直接返回上一次的结果。这个幂等键还要传给短信平台作为请求标识的一部分,虽然平台不一定支持去重,但至少在你自己的日志里能追踪到。

7. 压测、监控与降级预案

7.1 压测要压出限流而不是压出吞吐

短信接口的压测和普通接口不一样,目的不是看能跑多快,而是看限流触发时的行为是否符合预期。平台侧的限流通常是硬拒绝,返回一个明确的错误码,你的代码必须能识别这个码并且不重试、不堆积,直接把消息转入延迟队列稍后重发,而不是傻等或者疯狂重试把账号打进黑名单。

压测方案我建议分两轮。第一轮用小流量(比如阈值的50%)跑十分钟,观察响应时间的分布和错误率,确认基线正常。第二轮直接把并发拉到阈值的150%,看限流是否按预期触发、触发后你的降级逻辑是否生效、恢复后积压的消息能否正常消化。第二轮一定要在测试账号上做,别拿正式账号试,正式账号被限流影响的是真实业务。

还有一个压测时容易忽略的点:状态报告回调的压力。发出去一万条短信,回调也是一万次POST,如果你的回调端点处理能力跟不上,回调会堆积、超时、被平台重试,最终状态全部乱掉。压测时要把回调链路一起压,不能只压发送。

7.2 监控指标与降级开关

必须监控的指标有四个:发送量(按业务线、按小时)、受理成功率(接口返回成功的比例)、到达率(状态报告成功的比例)、回调延迟(从下发到收到状态报告的时间差)。前两个反映接口健康度,第三个反映通道质量,第四个反映你的回调链路是否通畅。

告警阈值建议这样设:受理成功率低于95%持续5分钟告警,到达率低于80%持续30分钟告警,回调延迟的P99超过10分钟告警。到达率告警要谨慎,因为它受号码质量影响大,波动是常态,设得太敏感会天天响。

降级方面,至少要有两个开关。一个是总量开关,按小时统计发送量,超过预设上限自动关闭非核心业务(营销、通知类)的发送,保住验证码通道。另一个是通道开关,如果发现某一类状态码异常集中(比如某个号段全部失败),可以临时屏蔽该号段,避免无效发送浪费配额。这两个开关都要做成配置中心可动态调整的,出事时能立刻生效,不用等发版。

最后分享一个我们踩过的坑:上线初期没做发送量按业务线拆分统计,某天营销活动误配置导致群发了八万条,账单出来才发现。后来强制要求每条短信必须带业务线标识(放在扩展码字段里),既能在平台侧按业务线看报表,也能在自己这边做配额控制。这个字段看着不起眼,真出事的时候是唯一的追责和止损依据。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询