3.1 短信接入:腾讯云 SMS
场景:提醒这条命,以前靠嗓子
系统上线第一个月,林姐照老习惯让前台打电话提醒,结果有一天前台请假,八十个预约只提醒了三十个,当天来了六个错时段的客人在店里干等。那天晚上我们就决定:提醒这件事必须交给系统,而且第一条腿必须是短信。
为什么是短信而不是 App 推送?美甲店的客人大多是路过顺手扫码关注的中年女性,手机里未必装得下门店 App,但人人都会看短信。腾讯云 SMS 的到达率在我们后来三个月的统计里稳定在 95% 以上,一条短信四分钱,一天八十条也就三块多,比前台一小时的工资都便宜。
选定方向后还要说服自己不选别的。企微群发和公众号模板消息都试过:群发被折叠在服务通知里,打开率不到 15%;公众号模板消息对未关注客人直接失效,而我们的客人里有四成根本没关注公众号。短信是唯一无视所有前置条件的通道,这也是它成为兜底通道而不是主通道的原因——贵,但一定到。
最先接入的版本只做了一件事:预约成功发一条通知。上线第一天发了三十二条,全店员工盯着各自的手机等短信,那种气氛像是等开奖。第二天客人回门店提反馈说收到了,林姐这才彻底把电话本收起来。
方案:一个 SDK 加一张配置表
接入的总体思路是后台不直接拼短信内容,只填模板编号和参数数组,内容审核交给腾讯云控制台提前过审。发送动作统一封装在一个函数里,预约成功、取消、爽约告警都调它。
腾讯云 SMS 的正文模板分三种类型,审核严格程度和适用场景各不相同,动手申请前先选对类型能少走一半弯路:
| 类型 | 审核严格度 | 单条计费 | 我们的用途 |
|---|---|---|---|
| 验证码 | 最宽松 | 每条约 4 分 | 未用 |
| 通知类 | 中等 | 每条约 4 分 | 预约提醒 |
| 营销类 | 最严格 | 略高且需资质 | 未用 |
我们全部走通知类,营销类需要额外的资质审核和退订机制,门店活动改为在小程序首页 banner 推送,不碰短信。
| 步骤 | 动作 | 耗时 |
|---|---|---|
| 1 | 控制台申请签名 | 审核约 2 小时 |
| 2 | 控制台申请正文模板 | 审核约 2 小时 |
| 3 | 后端封装 SendSms | 代码约 50 行 |
| 4 | 回调确认发送状态 | 配 Webhook 即可 |
SDK 走的是 Go 版本,密钥放在环境变量里,绝不进代码仓库。发送函数的参数只有三个:手机号、模板编号、参数切片:
func SendSms(phone string, tplID string, params []string) error {
cli := sms.NewClient(cred, region)
req := sms.NewSendSmsRequest()
req.SmsSdkAppid = common.StringPtr(appID)
req.Sign = common.StringPtr(signName)
req.PhoneNumberSet = common.StringPtrs([]*string{&phone})
req.TemplateID = common.StringPtr(tplID)
req.TemplateParamSet = common.StringPtrs(toIfaces(params))
_, err := cli.SendSms(req)
return err
}
调用入口在预约成功的异步队列里,主流程不阻塞。这一点很重要:短信接口偶尔会抖动,如果同步等它返回,下单接口的耗时会被拉高三百毫秒以上。
异步队列的实现很轻,没有上消息队列,只是数据库里一张待发表,发送协程每秒扫一次,发完就删:
for _, m := range pullPendingSms(100) {
if err := SendSms(m.phone, m.tpl, m.params); err != nil {
retryOrFail(m) // 最多重试两次,再失败标红等人工
}
}
这么做的好处是宕机重启不丢任务,坏处是延迟大约一到三秒,对提醒场景完全够用。上个月QUEUE里积压最久的一条是四秒,客人无感知。
发送结果不看出参,看回调。腾讯云会把每条短信的最终状态 POST 到我们配好的回调地址,成功、失败、拒收都在 body 里带着。我们把回调落库,后面算触达率全靠这张表:
func handleSmsCallback(c *gin.Context) {
var cb SmsCallback
c.ShouldBindJSON(&cb)
saveSmsReport(cb.SerialNo, cb.ReportStatus, cb.UserReceiveTime)
c.String(200, "SUCCESS") // 必须回包,否则平台会重试
}
回调接口有个细节:返回体必须是 SUCCESS 这几个字符,否则腾讯云会按失败重试,同一条状态会入库多次。我们见过一张表里同一条流水出现八条记录,全部来自重试。
回调落库之后还有一步是核对。每天凌晨跑一次对账任务,把发送成功的条数和控制台账单对一遍,差超过十条就告警。这项对账抓出过一次套餐包余量扣减异常的 bug,不核对根本发现不了。
配置:控制台与后台两侧
控制台一侧要做的事不多,但每一件都有讲究。先在短信控制台创建应用拿到 SdkAppid,再申请签名,最后申请模板。签名是短信开头【】里的内容,我们用的是门店品牌的简称,审核要营业执照;模板正文里所有可变内容必须写成 {1}、{2} 这种占位符,不允许直接写死变量。整个流程走完大概半个工作日,其中审核等待约两小时,其余时间在改文案。
后台一侧的配置统一收敛在一个结构体里,从环境变量读取,切换签名或模板只改配置重启即可:
| 配置项 | 值 | 说明 |
|---|---|---|
| SMS_APPID | 1400xxxxxx | 控制 SdkAppid |
| SMS_SIGN | 某某美甲 | 已审过签名 |
| SMS_TPL_BOOK | 1234567 | 预约成功模板 |
| SMS_TPL_REMIND | 1234568 | 到店提醒模板 |
| SMS_REGION | ap-guangzhou | 服务地域 |
频率限制也顺手配上。同一个手机号同模板一分钟内只发一条、一天最多十条,这是控制台自带的规则,不用写代码,但它救过我们一次:有一年春节活动模板编号配错串线了,差点给一个客人连发六十条拜年短信。频率规则像保险丝,平时嫌它碍事,出事时才知道真香。
计费方式建议提前想清楚。SMS 支持按条计费和套餐包两种,我们算过一笔账:每月两千五百条左右的量,买五千条的套餐包比按条付便宜三成,用不完的不过期。账单可以在控制台的用量统计里按模板拆开看,哪条模板最烧钱一清二楚。
发送量的季节波动也值得摸一遍。我们的短信量随预约量走,周末是工作日的两倍,国庆当月比平时翻番。套餐包按月买、不够再补的策略比一次性囤半年的灵活,补购这件事后来交给运营每周看一次用量报表决定。
踩坑:接入现场的四个坑
第一个坑是手机号格式。小程序拿到的手机号是带国家码的 86 前缀还是裸十一位,不同授权接口返回不一致。我们统一在入库时剥掉前缀,发送前再补 +86,SMS 接口要求 E.164 格式,格式不对直接报错,而且报错信息藏在一长串英文里,好在有 errorCode 可以查表。
第二个坑是发送频率与业务节奏冲突。预约成功的通知和到店前的提醒如果在同一分钟内触发,会被频率规则挡掉第二条。后来我们把提醒任务安排在服务前两小时,与成功通知天然错开。挡掉的时候接口不会报错,只是回调里告诉你失败,所以这种静默失败最坑——要在回调表里按失败原因聚合统计才看得见。
第三个坑是短信内容超过签名模板的长度上限。一条短信 70 个字按一条计费,超过按两条收。提醒模板我们改了三版,从 92 字压到 66 字,每条省一分钱,一年下来省出一顿部门聚餐。压字数是有套路的:先砍客套话,再合并门店名和地址,时间精确到分钟就够了,最后连标点都数着用。
第四个坑是欠费不停服但不报警。有一次账户余额不足,短信悄悄停发了一整个下午,前台不得不重操旧业打电话。从那以后告警里加了一条:连续十分钟无短信回调即触发值班通知。
小结
短信接入本身不难,难的是把它当成业务的一部分来运营:模板要审、回调要收、频率要配、余额要盯。一个 SendSms 调用背后是四章控制台配置和四条踩坑记录,把这些都归档到代码仓库的 docs 目录里,后来接手的同事半天就能复现整套链路。
回头看整套接入,最值得做的一件事是把发送、回调、对账三件事拆成三张表:待发表管重试、回调表管状态、账单表管钱。三张表对上账之后,每个月短信费用从一笔糊涂账变成了可以按门店拆开的明细,林姐第一次能说清三家店各自的提醒成本。下一篇细讲模板和签名怎么过审——我们被驳回三次才摸清审核规则。
延伸阅读
- 模板与签名的审核细节,翻 模板与签名:审核实操
- 免费通道的实现,见 订阅消息:小程序内通知
- 触达率的数据复盘,见 触达策略:多通道兜底
- 通用的腾讯云产品实战,翻 腾讯云实战合集
