ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

业务系统快速接入阿里云短信API,实现会员到期提醒全攻略

业务系统快速接入阿里云短信API,实现会员到期提醒全攻略 我们系统之前一直用邮件做会员到期提醒效果实在一般打开率低不说还经常被扔进垃圾箱。后来换成短信提醒接口触达效率完全不一样。这个事我在业务系统里从零到一对接过好几轮设计方案、申请签名模板、写发送代码、接定时任务、排查发不出去的问题都经历过。尤其是阿里云短信API平时测试好好的一到批量发就各种返回码报错踩过的坑不比代码少。这篇就把“业务系统快速接入短信API实现到期提醒”的完整链路讲清楚为什么提醒要这么设计、对接前要准备什么、代码怎么写、发不出去怎么查适合正在做会员到期、服务到期、账单提醒这类需求的开发同学参考。1. 先想清楚到期提醒要解决什么问题1.1 为什么不是“发个短信”那么简单很多同学接到需求第一反应就是“调一下短信接口把文案拼好发出去”。但真正做过就会发现到期提醒看起来简单实际上是在一个业务闭环里做决策。到期场景很典型会员要过期了、云服务实例要释放、合同要到期、SSL证书要更新、订阅服务要续费。这类事情的特点是如果不提醒用户会直接流失或者产生资损提醒得不好用户会投诉骚扰。所以短信API只是那个“最后触达”的管道真正决定提醒效果的是上游的业务策略哪些用户该提醒、提前几天提醒、一天发几次、发什么样的内容、失败了怎么补。拿我实际遇到的需求举例用户买了季度会员系统需要在他到期前3天、到期当天各收到一条提醒短信。听起来很简单但一拆开就有很多问题用户可能已经续费了你还发用户可能在免打扰时段用户手机号可能是空号用户已经投诉过一次不想再收短信用户在A库但在B库没有同步到期时间…… 这些都算“提醒任务编排”的范畴不在短信API的职责里。所以第一步永远是把提醒策略理清楚再谈对接。1.2 提醒策略设计提前几天、几点发、发几次我的经验是提醒策略往下拆无非就是三件事时机、频次、内容。时机决定用户什么时候看到频次决定用户会不会烦内容决定用户愿不愿意行动。时机上到期前3天、到期前1天是最常用的两个节点。提前太多用户记不住提前太少用户来不及处理。如果是高客单的续费场景比如年度会员、企业服务合同可以提前7天加一次但前提是用户的续费周期长、决策成本高否则没必要。发送时间建议选在工作日上午10点到11点、下午3点到4点这个时段用户看手机的意愿和回复率都相对理想。周末和晚上8点以后尽量别碰尤其不要晚上10点后发很容易被投诉。频次上一次性任务里同一个用户同一个模板最多发1条整个到期周期内同一个用户最多发2到3条。宁可少发不要多发。很多短信服务商对同一号码的频控非常严格连续高频发送轻则接口报BUSINESS_LIMIT_CONTROL重则号码被列入发送黑名单。内容上要注意用“服务提醒”的口吻而不是“营销推广”提供明确的下一步动作比如“登录App查看续费”短信里带上退订引导既合规又减少投诉。我建议把策略维护成一张可配置的表方便运营调整。我直接放一份我当时用的参数参数推荐值说明首轮提醒提前量到期前3天让用户有时间决策二轮提醒提前量到期前1天紧迫感强促使用户行动发送时间窗口10:00-11:30、14:00-16:30高触达低打扰单模板单用户最多条数1条避免重复发送整个周期最多打扰次数3次含到期当天静默时段20:00至次日09:00不发送任何通知短信紧急安全类除外1.3 技术选型三个方案对比为什么选了阿里云短信API当时摆在我面前的有三块技术路线。第一条是自建短信网关自己对接运营商这条路基本不建议要做大量资质审查、通道对接、状态报告处理人力成本高小团队根本扛不住。第二条是直接采购第三方短信平台的服务这类平台各有各的接口文档优势是价格灵活劣势是通道质量和送达率参差不齐出了问题排查链路很长。第三条是用云厂商的短信API我当时选的是阿里云短信API原因有几个国内送达率高SDK齐全文档清晰返回码和错误信息能做在线排查而且日常用的服务器本来就在阿里云上内网链路也更顺。不是说只有阿里云可选腾讯云、其他正规云厂商也都可以核心是盯住三个硬指标送达率、审核时效、接口文档质量。送达率是最难在事前评估的只能小流量实测审核时效影响你的上线节奏一般签名和模板审核1到2个工作日预留时间别压太紧。技术方案定了之后整体架构就清晰了业务数据库提供到期的用户数据定时任务扫表过滤出需要提醒的用户通过短信服务封装层调用阿里云短信API发送结果同步记录到日志表同时用回调接收最终送达状态。链路里最关键的是“短信服务封装层”它要把发送逻辑、参数校验、日志记录、异常处理都收口避免业务代码里直接到处new Client。我后面第三部分会给出完整代码。2. 对接之前必须弄懂的三个概念2.1 签名和模板短信内容的“门禁”对接短信API之前很多人在申请环节就卡住了因为没搞懂“签名”和“模板”这两个东西。可以这样理解签名就是发件人名称展示在短信最前面比如【会员提醒】用户一眼知道是谁发的模板就是短信正文里的固定内容部分中间用变量代替动态内容比如“亲爱的${name}您的${product}将于${date}到期”。阿里云短信API要求所有正式发送的短信签名和模板都必须事先提交审核。签名审核看的是资质个人用户和企业用户的材料不一样所以在准备阶段要把营业执照、应用名称、官网地址这些提前备好。模板审核看的是内容不允许纯营销、不允许诱导、不允许出现未报备的链接涉及金融、房产等敏感行业还需要额外资质。这两块我没少被卡最疼的一个教训是开发刚写好代码临时改了模板文案结果忘了重新提交审核旧模板被停用线上短信一夜之间全发不出去后来才排查到是模板状态问题。所以建议在项目启动前就把签名和模板申请这件事排进计划不要等代码写完了再补否则上线日期只能往后拖。2.2 号码、变量、时间数据从哪儿来对接代码本身没什么魔法真正需要想清楚的是数据来源。手机号哪里来必须是用户在服务协议里明确授权过的号码注册手机号、绑定手机号都可以但注意不能从第三方买号码来发这是服务商红线也是合规红线。到期时间从哪里来一般在业务表里已经有比如会员表的 expire_time、实例表的 release_time。扫描任务的SQL就是把 expire_time 落在某个时间窗口内的记录捞出来。这里有一个大坑很多表里的时间字段存的不是用户时区的到期时间而是服务器UTC时间直接拿去和业务时间比较差8小时提前量就算错了。建议在SQL里统一转换或者用带时区的DateTime类型避免出现“提前3天提醒”实际上变成“提前2天多”。变量怎么拼短信模板里的变量是用${key}表示的调用接口时传一个JSON字符串里面key必须和模板里完全一致。比如模板是“您的${product}将于${date}到期”那传参就应该是{product:会员,date:2025-08-30}。key拼错了或模板里根本没这个变量接口直接返回参数不合法。我后来干脆写了个工具方法把传参和模板变量做一次比对不匹配直接抛异常避免把错误模板参数发到线上。2.3 发送频率与内容合规决定你能不能活过试运行发送频率和内容合规这两件事直接决定一个短信任务的存活时间。服务商侧对每个签名、每个模板都有频控规则同一个号码、同一个签名、同一个模板一分钟内、一小时内、一天内都有发送上限。一旦触发返回码就是 isv.BUSINESS_LIMIT_CONTROL这种报错不是你改代码能解决的必须等限制窗口过去或者去服务商后台申请解除限制。内容合规更值得认真对待。到期提醒属于通知短信不是营销短信两个类别的收费和审核要求还不一样。通知短信里也不能出现“快来”“限量”“秒杀”这些营销字眼否则可能被重新归类甚至直接拒绝发送。另外手动在短信里拼链接是很不明智的尤其是短链接很多服务商对未备案链接零容忍轻则这条发不出去重则模板被毙。如果业务必须放链接我只建议放已经备案过的域名并且提前在模板审核时备注清楚。这里也回答一个很常见的误解不是接口返回成功短信就送到了。SendSms接口只代表“服务商接收了这条短信”后面还有运营商通道、用户手机侧拦截、号码状态等环节。所以对接的时候就要有“看回执”的意识发送接口同步拿到的是受理结果最终是否送达要以回执为准。我第四部分会详细讲这套链路怎么排查。3. 实操落地Spring Boot接入阿里云短信API3.1 准备工作账号、AccessKey、签名模板实操部分我用Java Spring Boot为例这套代码我在生产环境跑过的依赖、配置、发送、调度都能直接抄。第一步是准备工作阿里云账号开通短信服务SMS在控制台完成实名认证。在RAM控制台创建子用户只授予短信服务相关的访问权限 AliyunDysmsFullAccess生成AccessKey ID和AccessKey Secret。千万别用主账号的AccessKey权限太大会出安全事故。在短信服务控制台申请签名类型选择“通知短信”提交对应的资质材料等审核通过。在短信服务控制台申请模板比如“到期提醒通知”正文示例“亲爱的${name}您购买的${product}将于${date}到期为避免影响使用请及时处理。如有疑问请登录应用查看。回复TD退订。” 模板提交后等审核。这里的审核时间我试过最快2小时最慢一个工作日。所以别把审核放在上线当天尽量提前两三天搞定。短信服务本质上是一种按量付费的资源先充值一点金额测试即可正式上线再按预算充值。3.2 引入依赖与基础配置第一步在pom.xml加入阿里云短信SDK依赖。版本号建议不用最新也不用最旧看maven仓库里的稳定版本我这里用2.0.x系列dependency groupIdcom.aliyun/groupId artifactIddysmsapi20170525/artifactId version2.0.24/version /dependency第二步在application.yml里放配置。AccessKey这类敏感信息我只建议放到环境变量、KMS或者配置中心里不要直接写进代码仓库aliyun: sms: access-key-id: ${SMS_ACCESS_KEY_ID} access-key-secret: ${SMS_ACCESS_KEY_SECRET} endpoint: dysmsapi.aliyuncs.com sign-name: 会员提醒 template-code: SMS_123456789第三步写一个配置类初始化阿里云客户端。注意把配置类做成Bean其他地方注入使用而不是每次发送都new一个客户端后者会有连接资源浪费Configuration public class SmsConfig { Value(${aliyun.sms.access-key-id}) private String accessKeyId; Value(${aliyun.sms.access-key-secret}) private String accessKeySecret; Value(${aliyun.sms.endpoint}) private String endpoint; Bean public com.aliyun.dysmsapi20170525.Client smsClient() throws Exception { com.aliyun.teaopenapi.models.Config config new com.aliyun.teaopenapi.models.Config() .setAccessKeyId(accessKeyId) .setAccessKeySecret(accessKeySecret) .setEndpoint(endpoint); return new com.aliyun.dysmsapi20170525.Client(config); } }3.3 编写发送核心代码与上下文组装发送核心逻辑必须封装好不然后面每个调用方都写一遍异常处理代码会失控。我这里做了一个SmsService对外只暴露sendRemindSms方法内部做参数校验、调用接口、记录日志Service Slf4j public class SmsService { Resource private com.aliyun.dysmsapi20170525.Client client; Value(${aliyun.sms.sign-name}) private String signName; Value(${aliyun.sms.template-code}) private String templateCode; public boolean sendRemindSms(String phone, String templateParam) { if (!isValidPhone(phone)) { log.warn(非法手机号: {}, phone); return false; } com.aliyun.dysmsapi20170525.models.SendSmsRequest request new com.aliyun.dysmsapi20170525.models.SendSmsRequest() .setPhoneNumbers(phone) .setSignName(signName) .setTemplateCode(templateCode) .setTemplateParam(templateParam); try { com.aliyun.dysmsapi20170525.models.SendSmsResponse response client.sendSms(request); String code response.getBody().getCode(); String message response.getBody().getMessage(); if (OK.equals(code)) { log.info(短信发送受理成功, phone{}, requestId{}, phone, response.getBody().getRequestId()); return true; } log.error(短信发送受理失败, phone{}, code{}, message{}, phone, code, message); return false; } catch (Exception e) { log.error(短信发送异常, phone{}, phone, e); return false; } } private boolean isValidPhone(String phone) { return phone ! null phone.matches(^1\\d{10}$); } }调用方的职责是组装参数。比如会员到期提醒的场景我们从业务表里查出用户昵称、到期时间组装成模板参数public void sendMemberExpireRemind(Member member, LocalDate expireDate) { JSONObject param new JSONObject(); param.put(name, member.getNickname()); param.put(date, expireDate.toString()); boolean sent smsService.sendRemindSms(member.getPhone(), param.toJSONString()); if (!sent) { // 进入失败补偿队列后续由定时任务重试 sendRecordService.saveFailRecord(member.getId(), MEM_EXPIRE_REMIND, param); } }这里有两个细节要提醒。第一templateParam 必须转成字符串传JSON对象的序列化结果直接传对象会导致参数类型不匹配。第二模板变量里的日期格式要和模板文案一致比如模板写的是“2025-08-30”你传“08/30/2025”用户读起来就很奇怪运营也会来找你。3.4 定时任务调度到期扫描与发送到期提醒最重要的是“准点”定时任务建议每天跑一次扫描未来N天内到期的用户。我这里用Spring自带的Scheduled演示生产环境如果任务多、节点多建议换成分布式任务调度避免多个节点重复发送Component Slf4j public class ExpireRemindTask { Resource private MemberMapper memberMapper; Resource private SmsService smsService; Resource private SendRecordService sendRecordService; Scheduled(cron 0 0 10 * * ?) public void scanAndSend() { LocalDate today LocalDate.now(); ListMember needRemind memberMapper.findExpiringBetween(today.plusDays(2), today.plusDays(3)); log.info(到期提醒扫描开始, 待提醒用户数: {}, needRemind.size()); for (Member member : needRemind) { // 幂等判断同一用户同一模板同一天已经发过跳过 if (sendRecordService.hasSent(member.getId(), MEM_EXPIRE_REMIND, today)) { continue; } // 夜间不发这里再兜底判断一次 if (LocalTime.now().isAfter(LocalTime.of(20, 0))) { continue; } try { sendMemberExpireRemind(member, member.getExpireDate()); } catch (Exception e) { log.error(用户提醒发送异常, memberId{}, member.getId(), e); } } } }扫描任务要关注三个问题一是SQL的查询条件要用到索引expire_time字段必须加索引否则数据量一大每天扫一次就能把数据库拖垮。二是发送时要控制并发大批量用户不建议在for循环里同步一条条发会受接口QPS限制可以按每100条一批小批量并发同时记录好任务批次号方便追踪。三是任务本身要有幂等标记比如用一个batch_id记录本次扫描发送记录表里也带上batch_id任务重跑的时候不会重复发。4. 阿里云短信API发不出去这是排错全流程4.1 先从返回码入手这个标题对应的热词是“阿里云短信api发不出去”我相信很多人都有同感。其实发不出去的时候服务商已经把原因写在返回码里了问题是很多人不看返回码就乱猜。我先把常见返回码整理成表这是排查的基础返回码含义处理方式OK受理成功继续等待回执isv.BUSINESS_LIMIT_CONTROL触发频控同一号码/签名/模板发送过于频繁停止发送等待限制窗口或分时段降频isv.SMS_SIGNATURE_ILLEGAL签名不合法或未通过审核检查签名状态、是否与账号匹配isv.SMS_TEMPLATE_ILLEGAL模板不合法或未通过审核检查模板状态、模板ID是否配置正确isv.MOBILE_NUMBER_ILLEGAL手机号格式错误检查手机号是否规范isv.SMS_BLACK_POINT号码在服务商黑名单中联系服务商核实用户可能曾投诉isp.RAM_PERMISSION_DENYRAM子账号权限不足给AccessKey添加短信服务权限isv.AMOUNT_NOT_ENOUGH账户余额不足充值isv.INVALID_JSON_PARAM模板参数不是合法JSON检查templateParam序列化格式isv.TEMPLATE_PARAMS_ILLEGAL模板变量与参数不匹配核对变量名、参数数量这个表不是拿来背的是拿来对照的。报错出现后第一件事先看返回码很多问题一眼就能定位方向不用从零开始猜。4.2 排查两步离线和在线如果看完返回码还是没头绪我建议按“离线和在线”两步走。离线排查指的是查自己的代码和配置。我遇到最多的情况包括AccessKey配错了环境变量线上配置的是旧Key子账号没有开通短信服务权限模板ID在测试环境和生产环境里不是同一个SignName参数前后多了空格模板参数用了中文引号而不是英文引号导致JSON解析失败。这种问题多发生在“本地可以用线上发不出去”的场景建议先对比本地和线上的配置差异再看日志。在线排查指的是用阿里云控制台和OpenAPI工具。控制台里能看到签名和模板的真实审核状态以及每条发送记录的状态与备注。OpenAPI Explorer可以输入参数后直接调试用来验证是不是代码拼接参数的问题比翻日志快很多。另外阿里云短信控制台的“发送记录查询”功能里能看到每条短信的发送时间、状态、错误原因这个是排查回执问题的关键入口。我排障时经常先用OpenAPI Explorer模拟一次发送排除代码问题再回到自己系统里查日志。4.3 常见陷阱与重试策略有几个坑是特别容易踩的我一个个说。第一个坑模板变量参数名不一致。模板里写的是${name}代码传的是{username:xx}接口直接返回TEMPLATE_PARAMS_ILLEGAL。这种问题在模板审核通过那一刻其实就能发现发送前先拿一条测试号码发一下比写1000行逻辑都管用。第二个坑短信内容高度相似触发频控。如果你给一万个用户发同一句话服务商侧的频控会非常敏感。对策是合理分批一批500条每次间隔几秒在高峰期更要放慢节奏。还要避免同一个号码在很短时间内收到多条内容不同的短信比如续费提醒和订单通知撞车了用户一投诉可能整个签名都会被限制。第三个坑测试脚本把发送记录打到正式模板上。开发环境如果用了生产签名和生产模板测试发出的短信会真实到达用户手机还可能造成退款或者投诉。我的做法是申请两套签名和模板一套带“测试”字样只允许发到自己手机一套正式生产用。重试策略也不能省。发送接口返回失败要区分“可重试”和“不可重试”余额不足、频控、黑名单这类是不可重试或需要人工介入的网络超时、服务端5xx这类是可重试的。我设计了一个失败补偿表发送失败时写一条记录标记可重试次数退避间隔按2分钟、10分钟、1小时递增最多重试3次。注意不要同一时间把所有失败的一起重试那样容易把频控问题重新触发一遍。5. 上线之后的那些事5.1 监控与埋点发送量、成功率、到达量短信任务上线不是终点真正的运维才刚刚开始。我给系统加了几块最基本的监控第一块发送状态表。每一次发送请求都落库字段包括用户ID、手机号、签名、模板、模板参数、发送时间、受理返回码、requestId、回执状态、回执时间。这个表既是审计依据也是后面数据分析的数据源。第二块指标统计。每天定时汇总发送请求数、受理成功数、回执成功数、回执失败数、平均送达耗时算出发送成功率和回执到达率。回执到达率比受理成功率重要得多因为受理成功只是服务商收下了真正到达用户手机才算有效触达。第三块告警规则。回执失败率连续超过10%或者受理失败次数异常飙升就触发告警。告警通道用钉钉或者电话都行关键是别等用户自己打电话来投诉才发现发不出去。我遇到过最典型的线上事故运营商通道在某个时段回执大量延迟用户早上10点的到期提醒下午3点才收到。这时候看发送接口返回全是OK只有看回执耗时统计才发现异常。所以回执监控一定要做还要把“回执延迟超过X分钟”也纳入告警。5.2 用户拒绝与免打扰短信是一个强触达工具同时也是一把双刃剑。用户如果不想要骚扰服务平台必须有退订通道。我的建议是在每个通知短信末尾加上“回复TD退订”退订的号码要记录到一个退订表里之后任何发送逻辑都要先查这个表命中就不发。这个机制看起来简单但很多人不做等到投诉率上来被服务商限制才发现晚了。免打扰时段也是上线时必须落实的逻辑。我直接用定时任务里那个兜底判断晚上8点到早上9点不发送任何非紧急通知。节假日、周末要尤其小心很多用户投诉就是因为在休息时间被短信吵醒。还有一个容易被忽略的点接收方手机号授权。到期提醒属于业务通知前提是用户注册或购买服务时已经同意接收这类短信。如果连这个基础都没有系统上线之日可能就是被投诉之日。我的原则是在用户注册协议里明确写入“我们会通过短信发送服务状态提醒”把合规工作做在前面。5.3 我的几点个人体会做了这么多次短信对接有几句掏心窝的话想分享。第一不要在对接当天才去申请签名和模板。我见过太多人当天中午申请下午就想上线最后只能干等着审核这种时间浪费完全可以用提前准备来避免。第二代码里的发送逻辑一定要可以开关。在配置中心配一个“短信发送总开关”测试环境、故障应急时直接一键关闭比临时改代码靠谱得多。我还加过“指定手机号白名单”功能关闭后只允许发给自己测试。第三批量发送一定要预留失败出口。系统再好也会有单条失败的时候失败补偿表就是最后一道兜底。没有失败补偿的短信系统长期看一定会在某个凌晨爆出大问题。第四短信费用其实很低但积少成多也要心里有数。我建议每天统计发送条数和费用月度预算和实际开销对得上。不要等到月底对账才发现超出预算一大截。最后再分享一个小技巧对接完成后一定要写一份“短信问题自查手册”把常见错误码、处理步骤、联系人员整理成文档发给运维和客服。线上出问题时运维能靠这份文档快速定位客服能靠它安抚用户而你自己可以少接很多半夜电话。这个文档虽然不产生业务价值但在我这几年的实践中它比写代码有用得多。
返回列表