短信带链接怎么发:链接报备、签名重算与动态链接边界的工程做法
短信带链接的三个真问题:链接应写进 code 还是 title(两者一个参与签名一个不参与)、改链接要不要重新报备(要,人工终审)、签名与表单编码的先后顺序。另含 1002/1004/1005/1008/1009 排查表与 11 条上线检查单。
短信带链接,最常见的误解是"这是个短链生成问题"——找一家短链服务、生成一个链接、塞进短信就完事。
真正卡住接入的是另外三件事,而且没有一件跟短链服务有关:
大部分人在第 1 步就把链接塞进了正文,然后发现线上开始报签名错误;或者链接改了、签名重算了,发出去却没有短信。这篇只讲这三件事,以及它们背后的同一个判断:链接不是一个字段,是一个会同时触发签名链和报备链的改动。
一、先看接口:没有 link 字段,只有两个落点
短信通道的请求体一共 5 个参数,其中能装下链接文本的只有两个:
| 参数 | 含义 | 必填 | 参与签名 | 能放链接 |
|---|
mall_id | 商户 ID | 是 | 是 | 否 |
|---|---|---|---|---|
phone | 接收手机号 | 是 | 是 | 否 |
code | 短信内容(正文) | 是 | 是 | 能 |
title | 模板固定内容 | 见下方说明 | 否 | 能 |
sign | 请求签名 | 是 | — | 否 |
签名算式与核验类接口不是一套体系:
sms 通道: sign = md5(mall_id + phone + code + appkey) ← 没有 tm
核验类通道: sign = md5(mall_id + realname + idcard + tm + appkey)
请注意 title 不在这条算式里。 这是本篇所有结论的起点。
另外两个必须先知道的接口事实:
http://api.nztapi.com/sms.php(注意是 http),核验类走 https://api2.nztapi.com。短信密钥和核验密钥不要在同一个客户端配置里复用——地址、签名算式、返回结构三样都不一样。data / status 包装:{ "code": 1000, "message": "发送成功" }
一个文档层面的矛盾
title 在参数表里标注为"必填",同一行的说明却写着"模板固定内容,可为空";而 OpenAPI 规格(nztapi.yaml)的 required 列表是 [mall_id, phone, code, sign]——里面没有 title。
两处对不上,所以联调时别猜:传空 title 试一次,看通道是否接受。如果接受,说明它确实可空;如果报参数错,那就是必填,只是 OpenAPI 规格漏写了。两种情况都不影响你把链接放进 code,但会影响你怎么组织模板。
二、链接写 code 还是写 title:三条判据
| 判据 | 写进 code(正文) | 写进 title(模板固定内容) |
|---|
| 签名 | 参与签名 → 链接一变,签名必须重算 | 不参与签名 → 链接变,签名不动 |
|---|---|---|
| 报备 | 模板内容变更 → 通常要重新报备 | 模板固定内容变更 → 通常要重新报备 |
| 适用 | 每个用户/每批活动用不同落地页 | 链接固定、同一落地页批量发送 |
两条都逃不掉报备,但只有一条逃不掉签名。这就是"两条独立成本线"的由来。
再往下一层,有个实操上的细节经常被忽略:code 这个字段会同时承担"固定内容"和"变量内容"两种角色。
比如验证码短信,正文是"点击 https://短域名/a 领取验证码 123456"——链接是固定的,验证码是变量。链接一旦写在 code 里,报备的模板正文里就必须出现这条具体链接;变量部分还要另外满足占位符规则。也就是说,你没法靠"这字段是变量位"来回避链接报备。
反过来,如果把链接放进 title,title 本身就是模板的固定部分,链接在报备模板里的位置是明确的,同样要逐条报备。区别只在于签名。
三、坑 1:链接进了签名,code 的字符一个都不能差
签名算式里的 code 参与哈希,也就是说最终发送出去的每一个字符都会改变签名结果。
最容易踩的顺序错误是"先编码、再算签名":
# ❌ 错:先 URL 编码,再拿编码后的串算签名
code_encoded = urllib.parse.quote(code) sign = md5(f"{mall_id}{phone}{code_encoded}{appkey}") requests.post(url, data={"code": code_encoded, "sign": sign})
问题:表单提交时 requests 会对 code_encoded 再编码一次,
服务端解出来的是 code(未编码版),与签名时的串不一致 → 1002
✅ 对:先定稿最终正文,用同一份原文算签名,编码交给表单层
code = f"【诺正通】点击 {link} 完成验证,5 分钟内有效。" sign = md5(f"{mall_id}{phone}{code}{appkey}".encode("utf-8")).hexdigest() requests.post(url, data={"mall_id": mall_id, "phone": phone, "title": "验证码", "code": code, "sign": sign})
铁律:签名用的串和发送出去的串,必须是同一个字符串对象的内容。 只要中间做过任何一次转义、拼接、模板渲染后的二次处理,就可能出现签名对不上。
还有一个更隐蔽的变体:链接里带了 &、=、中文或空格。表单编码时这些字符的处理方式和签名时用的原文对不上,同样表现为签名错误。
建议:短链路径只用 [A-Za-z0-9-] 这类字符。域名可以自定义、也可以用短域名,但路径部分越简单,签名与编码越不容易打架。这不是为了好看,是为了让签名可复现。
四、坑 2:报备要落到具体链接 + 不支持动态,两条规则叠在一起才是难点
把两条业务规则放在一起看:
单看每一条都不算复杂,叠在一起推出的结论才是关键:
短信正文里能出现的链接,是一个需要逐个报备的有限集合。
这意味着规模是可以推演的:一次活动一个新落地页 = 一条链接 = 一次报备 = 运营商终审 1-3 个工作日(部分省份还有额外审核环节)。如果链接按用户维度变化,报备条数就发散,等于永远发不出去。
所以正确的架构是:把"每条消息都不同的东西"从链接里拿出去。
| 做法 | 链接是否逐条不同 | 报备条数 | 评价 |
|---|
链接里带 user_id | 每个用户一条 | 发散 | 走不通 |
|---|---|---|---|
| 链接固定(已报备的那一条),个性化在落地页侧识别 | 一条 | 1 | 可行 |
| 短域名做固定入口,短链后台映射到真实落地页 | 一条(短信里那一条) | 1 | 可行,且换落地页不用重新报备 |
第三行值得多说一句:改短链后台的映射关系,短信里的链接没变,报备就不用动。 这正是短域名在带链接短信里的真正价值——它把"要报备的东西"和"真正要去的地方"解耦了。
顺带划清一条能力边界:短链服务本身不在接口范围内——我们的接口文档里没有短链 API,短链与跳转需要你自己搭或用第三方。我们提供的是"短信正文可以带链接"这一层能力,以及带链接短信的通道、签名与报备支持。
五、坑 3:改链接要付两笔钱
把成本拆开看,决策就清楚了:
| 你改了什么 | 签名要不要重算 | 报备要不要重做 | 生效时间 |
|---|
| 链接路径(换落地页) | 写在 code 里 → 要 | 要 | 运营商终审后 |
|---|---|---|---|
| 链接域名 | 写在 code 里 → 要 | 要 | 运营商终审后 |
| 只改短链后台的跳转映射 | 不用 | 不用 | 立即 |
| 只改正文里的固定文案 | 要 | 要 | 运营商终审后 |
链接写在 title 里,改链接 | 不用 | 要 | 运营商终审后 |
结论:一次报备能覆盖的,是"同一条链接 + 同一套正文"的全部发送。 要发不同的链接,就得再报备一次。
很多团队只算了签名这一笔(改完代码、重算签名、以为完事),结果线上静默不发——通道侧对未报备内容直接拦,用户收不到,接口还可能返回成功。这一条是带链接短信最典型的"以为做完了其实没做"的场景。
六、失败排查:按错误码定位
| 现象 | 原因 | 怎么查 |
|---|
返回 1002 签名错误,核验类接口却正常 | 签名函数复用了核验类的拼接顺序(多了 tm,或漏拼 code) | 把签名函数按接口分开实现,不要写死字段列表 |
|---|---|---|
返回 1002,正文一个字没改却偶发失败 | 签名用的串和实际发送的串不是同一份(编码时机、模板二次渲染) | 打印签名时的 code 与发出去的 code,逐字符比对 |
返回 1004 短信内容为空 | code 传空(把内容全放进 title 也不行,code 本身不能空) | 检查正文组装逻辑 |
返回 1005 敏感词 | 短链路径或正文文案命中审核 | 短链路径改成无意义的短码,正文措辞复查 |
返回 1003 可发送条数为 0 | 通道内可发余额或配额为 0 | 不是参数问题,查额度 |
返回 1008 频率过快 | 同一号码短时间多次请求 | 业务侧加频控与倒计时 |
返回 1009 短信内容缺少签名信息 | 正文没带【签名】 | 正文结构必须是 【签名】+ 内容 |
| 报备已过,用户仍收不到 | 运营商侧拦截;或通道自动重发与业务层重发叠加造成对账错位 | 见短信验证码被刷怎么防一文的排查顺序 |
返回 1000,用户说链接点不动 | 链接所在域名未解析、HTTPS 证书异常,或短链后台映射未配 | 先在浏览器里打开那条链接本身 |
七、合规:带链接的短信更容易踩线的地方
八、上线检查单
□ 本批次一共要发几条不同的链接?(这就是报备条数,先数清楚)
□ 每条链接都已完成报备,并确认运营商终审通过 □ 链接域名能正常解析、HTTPS 正常、落地页与文案一致 □ 短链路径只用 [A-Za-z0-9-],不含中文/空格/&/= □ 签名用最终 code 原文计算,URL 编码交给表单层,不提前编码 □ title 的必填性已实测确认(文档表与 OpenAPI 规格不一致) □ 业务层不叠加自动重发,避免与通道自动重发重复计费 □ 短信接口只从服务端调用(地址是 http,别在客户端直连) □ 个性化参数放在落地页/短链映射侧,不拼进短信里的链接 □ 正文里明示退订方式 □ 密钥分环境隔离,日志脱敏(短信正文与短链不落日志明文)
相关阅读
短信通道按量计费、签名与模板需报备,具体与商务确认;新用户有免费试用额度可用于联调。