
做了好几年的会员系统和合同系统我越来越觉得“到期提醒”这四个字听着是小事做起来全是细节。会员到期、课程有效期到期、资质证照到期几乎每个业务模块最后都得回到同一个问题怎么让用户在被切断服务之前就知道自己快到期了邮件会沉底、公众号推送要看用户心情、APP推送先得用户装客户端并开权限真正可靠又强触达的方式到头来还是短信。可短信接口对接这件事真不是往接口里塞一个手机号那么简单。我自己第一次做短信API接入时以为跟调一个普通HTTP接口差不多寻思着申请个账号、复制一段示例代码、跑通一次发送就算完事。结果从签名审核、模板申报、AccessKey权限配置到后来遇到的“发送返回OK但对方死活收不到”一路踩了不少坑。最近在技术群和社区里看到“阿里云短信api发不出去”这类话题讨论得很热说明这不是个别人的问题。这篇就把我在这类项目里的完整实施过程、故障排查思路和上线稳定性经验都梳理一遍给正在做到期提醒功能或者已经接入短信API但时不时出问题的同行做个参考。1. 到期提醒的业务形态为什么我最终选了短信接口1.1 到期提醒不是“发条消息”那么简单很多系统的到期提醒第一版都是提前一天发一封邮件或者登录以后在站内信里看。我自己维护过一套会员管理系统当时的提醒方式是每天早上8点跑一个定时任务把所有三天内到期的会员捞出来往管理员邮箱发一张Excel表让客服人工联系。结果就是客服打电话漏人、管理员忘记看邮件、用户到期才发现登录不了然后打电话过来投诉。后来痛定思痛把提醒直接发给用户。可选通道无非几个短信、APP推送、微信公众号服务号、邮件。真实经验是短信对于“到期”这种强时效场景仍然是最稳的一条。短信不需要用户安装任何东西只要手机号是对的运营商链路正常基本能做到1分钟内触达。用户就算当时不理通知栏里的那条短信也一直挂在那儿比邮件的“未读小红点”压力大多了。但短信也有代价每一条都要花钱签名模板需要审核发送频率有限制而且文案一旦措辞不合规轻则发不出去重则模板直接下掉。更麻烦的是短信发送链路分为“业务系统-短信服务商-运营商网关-用户手机”四段前面两段你自己能控制后面两段几乎是个黑盒。这也是为什么“api明明返回OK用户却没收到”这种诡异问题会频繁出现。1.2 动手之前先列六条硬指标在选择短信服务商、写代码之前我建议所有做到期提醒的人先把需求指标写死。很多项目做崩都是因为“先跑通再补需求”结果发送频率、重试策略、回执确认这些关键环节全是事后补救。我在项目里是这样定的触达时效从定时任务触发到用户收到短信链路延迟控制在30秒以内提醒节奏到期前T-7、T-3、T-1三天各发一次到期当天不再打扰可变内容每条短信里至少包含用户名、业务名称、到期日期三个变量发送确认发送接口返回成功不等于用户收到必须拉取状态回执失败重试发送失败按5分钟/30分钟/6小时三个档位重试最终失败进入人工处理表通道稳定性单个签名或接口故障时能切换到备用通道这几条看起来不复杂但写代码前划定清楚后面能省掉大量模糊决策。比如“发送成功不等于到达”这句话很多人第一次听觉得是废话实际对接时你会遇到“CodeOK但用户没收到”的情况没有回执机制你根本不知道问题出在运营商还是内容被过滤。1.3 短信API供应商怎么选市面上的主流供应商我基本都用过一轮这里不拉踩只把选型判断标准写出来大家按自己的业务体量对照即可。对比维度我的判断标准为什么重要签名和模板审核速度1个工作日内到期提醒文案固定模板审核慢了会卡住上线回执消息获取方式同时支持HTTP推送和拉取拉取适合调试推送适合生产频率限制单号单模板有明确QPS批量到期时避免被流控打死价格单条0.035-0.055元区间一年几万条价格影响不大别贪便宜选小厂文档和SDK维护官方有最新版多语言SDK自己封装HTTP最痛苦的是签名算法故障自愈有多个可用region真遇到区域故障时能切换很多团队选型时盯着价格和营销优惠但在到期提醒这类事务性短信场景里最重要的其实是“回执与推送通道”以及“流控策略”。因为发送短信本质上是一个异步过程短信服务商把请求接过去以后还要经过运营商审核和下发这是不可控的一段链路。谁能把这段链路的可观测性做好谁就值得优先选。2. 接口能调通前必须把账号、签名、模板这三件事做对2.1 账号认证个人开发者和企业开发者的差别以我主要使用的阿里云短信服务为例账号类型卡得比较严。个人实名认证账号在签名环节限制很多尤其到了申请签名的阶段很多行业类别不给通过。如果你所在的公司没有企业认证建议先让管理员把账号升级成企业认证否则后面全是卡点。具体操作路径阿里云控制台 → 账号实名认证 → 企业认证。认证材料一般是营业执照法人信息和经办人信息按页面填就行快的几小时就过。顺带提醒一句如果主体信息里有字号的“括号”是全角还是半角这类细节也照着营业执照原件来别自己发挥。2.2 签名申请一步到位比反复修改省时间签名是发给用户短信前面的【】比如【XX会员】。申请签名时有两种类型最常用公司全称/简称以及App名称或商标名。我建议直接选“公司全称或者简称”理由很简单审核通过率最高而且到期提醒短信里出现公司主体名用户分辨起来也方便。申请签名还需要提供使用场景说明。我当时是这样写的本签名用于会员到期提醒短信内容包括会员有效期剩余天数、到期日期、续费引导。发送对象为本系统已注册会员发送频率为到期前3次提醒。审核人员看的其实是两点内容是否存在诱导点击文案是否可能引发投诉。短信内容诱导点击、含钱款转账信息是审核最敏感的。后面模板申请同理。签名一旦被驳回修改再提交的周期是重新排队的所以第一次就把场景写清楚比反复提交省时得多。2.3 模板申报变量不是你想怎么写就怎么写模板是短信正文的骨架。我的到期提醒模板长这样尊敬的${username}您名下的合同将于${expireDate}到期为避免影响后续使用请及时续费。如有疑问请回复TD退订。注意变量用${}包起来变量名只能包含字母、数字、点、下划线推荐用英文。中文变量名某些平台虽然支持但为了兼容性我统一用英文。模板审核最容易踩的坑是文案里出现“最”“第一”“绝对”等极限词会被打回出现“银行账户”“支付宝/微信转账”等字眼直接要求修改缺少退订提示部分企业审核会退回变量之间隔得太近比如连续两个${a}${b}容易触发拼接违规另外有一条隐性规则——模板里不要放URL。需要用户点击链接的情况尽量用备案过的短链不然审核期会拉长。到期提醒本身不依赖点击我直接就毙掉了所有带链接的方案。模板内容里“到期”“续费”这种词本身没问题但如果你加一句“限时折扣”“再不续费将涨价”大概率会从通知类被划到营销类审核标准和发送通道都不一样务必注意。2.4 RAM子账号与最小权限别把主账号Key交给代码这是很多程序员容易忽视但在生产环境会出问题的细节。用主账号的AccessKey去调短信接口虽然能跑通但风险集中一旦泄露整个账号都能被操作。我更推荐在RAM里建一个子账号只授予短信发送和查询权限。RAM权限策略怎么配路径RAM访问控制 → 用户 → 创建用户 → 授权。可以直接用系统策略AliyunDysmsFullAccess如果想更精确可以自定义策略{ Version: 1, Statement: [ { Action: [ dysms:SendSms, dysms:QuerySendDetails ], Effect: Allow, Resource: * } ] }子账号创建后只展示一次AccessKeySecret要立即复制保存好。我一般会把Key配置到配置中心或环境变量里而不是写死在代码中方便后续无感轮换。这个习惯看起来很基础但我接手过的项目里至少两次因为Key硬编码到Git仓库被泄露导致短信被刷一夜之间费用出去几百上千。3. 发送接口对接从依赖到跑通的完整步骤3.1 客户端初始化阿里云从2022年起主推v2版SDK对应的是dysmsapi20170525这个包。相比老版aliyun-java-sdk-core新SDK用起来清爽很多。Maven依赖如下dependency groupIdcom.aliyun/groupId artifactIddysmsapi20170525/artifactId version2.0.31/version /dependency然后是客户端初始化。短信服务在国内所有区域的Endpoint基本一致标准Endpoint固定为dysmsapi.aliyuncs.com。我见过有人在这个环节折腾半天以为区域选错了其实并不是com.aliyun.teaopenapi.models.Config config new com.aliyun.teaopenapi.models.Config() .setAccessKeyId(accessKeyId) .setAccessKeySecret(accessKeySecret); config.endpoint dysmsapi.aliyuncs.com; com.aliyun.dysmsapi20170525.Client client new com.aliyun.dysmsapi20170525.Client(config);再说一遍AccessKey的存储问题千万别硬编码在代码里。哪怕是内部系统一旦代码流转到外包、第三方或者到了Git仓库Key就是裸奔状态。环境变量或者配置中心是底线。3.2 构造一条到期提醒短信SDK的发送方法叫sendSms参数说起来就四个手机号、签名名称、模板编码、模板变量。模板变量是JSON字符串这是最容易写错的地方import com.aliyun.dysmsapi20170525.models.*; SendSmsRequest sendSmsRequest new SendSmsRequest() .setPhoneNumbers(13800138000) .setSignName(XX会员) .setTemplateCode(SMS_123456789) .setTemplateParam({\username\:\张三\,\expireDate\:\2024-06-01\}); SendSmsResponse response client.sendSms(sendSmsRequest); String code response.getBody().getCode(); String message response.getBody().getMessage();应答里的code字段判断很关键。code为OK表示阿里云受理成功但注意这里的用词是“受理”——后面运营商能不能发出去还需要通过回执确认。code不是OK时返回的错误码就可以直接用来排查具体我放在第4节展开。服务端在组装模板变量时还有一个容易踩的坑变量值里如果包含中文逗号、特殊符号或者含有客户填写的自由文本必须在写入短信前过滤或者做长度截断。我遇到过客户把备注里一大段话拼进变量结果触发运营商风控整批短信全被拦截。短信变量越干净越好宁可截断也不要让内容失控。3.3 批量到期提醒循环发送要克制业务系统里的到期提醒通常不是一条条手发而是每天定时任务扫库把一批即将到期的用户捞出来群发。最朴素的做法是for循环一条一条sendSms但这样容易撞上流控。我当时的做法是给发送任务加一个简单的限速器。比如每分钟最多发送60条每次循环后sleep对应的间隔// 简化示例每分钟限速60条 long intervalMs 1000L; int maxPerSecond 1; for (RemindUser u : list) { client.sendSms(buildRequest(u)); Thread.sleep(intervalMs / maxPerSecond); }更工程化的方案是把用户ID、手机号、计划发送时间、模板变量都先放入一张发送任务表由定时任务分批执行。这样既能控制节奏又能在失败后手动重跑某条任务而不是重新扫全库。如果一次性发送量特别大还应该把发送计划错峰。比如合同系统里月底集中到期这种场景千万别说夜里12点整一个定时任务把三万条短信全部扔出去——短信通道、运营商网关、用户投诉三方面都会出问题。把起止时间分布在一个时间段里分期分批发送效果会好很多。3.4 回执查询这条用户到底收到没有发送接口只能告诉你“请求已被受理”用户真正收到短信的凭据是短信回执。阿里云提供两种方式控制台查看发送记录适合人工排查通过API查询或订阅消息服务获取回执查询API是QuerySendDetails可以把指定手机号在指定时间段的发送状态拉回来QuerySendDetailsRequest queryRequest new QuerySendDetailsRequest() .setPhoneNumber(13800138000) .setSendDate(20240601) .setPageSize(10L) .setCurrentPage(1L); QuerySendDetailsResponse queryResponse client.querySendDetails(queryRequest); // 返回中的SendStatusList里能看到每条的状态回执状态常见值有两个DELIVRD表示成功到达手机UNDELIV或UNDELIVERED表示未送达原因可能是停机、空号、关机、用户主动退订等。查询接口属于“同步拉取”在到期提醒这种低频场景里完全够用——每天跑一次拉一下昨天没送达的记录打上标记再做人工跟进。生产环境我推荐把状态回执异步推送到自己的接口做到逐条状态入库。推送格式是JSON验签方式是用阿里云给出的密钥算HMAC。没有专门回执系统的小团队用QuerySendDetails轮询就足够了。4. “阿里云短信API发不出去”到底卡在哪一条排查链路走给你看先说为什么会流行“阿里云短信api发不出去”这类讨论。我在技术群里看到很多人明明按照文档调了接口返回也是正常但用户就是收不到或者在某个阶段开始大面积报错。这不是阿里云一家的问题任何短信通道都有类似情况但用户基数大讨论声音也大。下面这套排查顺序是我从多次失败案例里总结出来的按顺序走效率最高。4.1 第一层区分“接口报错”和“发送未到达”接到“发不出去”的反馈先别急着改代码做一件事把发送后拿到的返回Code和Message完完整整记下来。这是最基础的一层区分。如果调用sendSms时直接抛异常或者返回非OK那是“接口层面失败”如果sendSms返回OK但下游回执显示未送达那是“运营商层面失败”二者处理方式完全不同。前者是账号、签名、模板、参数、鉴权的问题后者多半涉及内容过滤、号码状态、运营商网关策略。这一层不搞清楚后面排查就容易病急乱投医。4.2 第二层鉴权和权限相关错误这类错误提示非常明显常见的几个错误码含义处理方式InvalidAccessKeyId.NotFoundAccessKey不存在检查Key是否复制正确是否留有多余空格Forbidden子账号无权限或IP白名单限制检查RAM授权和Key所在账号SignatureDoesNotMatch签名算法不一致大概率是Secret配错InvalidTimeStamp.Expired请求时间戳过期检查系统时间和服务器时间是否偏差太大isv.INVALID_PARAMETERS参数格式错误逐个检查手机号、模板变量JSON我遇到最离奇的一次是测试环境AccessKey正常但生产环境一直Forbidden。排查半天发现生产环境的Key是另一个账号下的子账号Key而那个账号根本没开通短信服务。权限这东西配好以后一般不出声但等到发送量上来了才暴露是最坑的。4.3 第三层签名和模板状态导致的暗坑签名和模板在申请通过后不代表永久可用。平台会不定期复审一旦发现使用场景异常或者文案变化会把签名或模板置于“待完善”或“禁用”状态。这类问题很隐蔽因为发送请求时依然返回OK但短信就是不投递。或者返回isv.SMS_SIGNATURE_ILLEGAL字面意思是签名不合法实际是签名没通过审核或已被封禁。遇到isv.SMS_TEMPLATE_ILLEGAL、isv.SMS_TEMPLATE_NOT_EXIST这类错误去控制台看模板状态模板审核中没通过不能用审核不通过点击查看原因改完重新提交停用可能因为内容命中违规词或者用户投诉率过高另外签名和模板之间还有关联关系。如果签名类型没有覆盖模板的使用场景也会被拒。比如签名是“XX会员”应用名称模板内容却是“XX银行提醒您”一看就不匹配审核不通过的原因里会写明“请在签名部分填写对应的App名称”。4.4 第四层手机号与内容触发运营商拦截这是“api返回OK用户却收不到”的最主要源头。sendSms受理成功不代表运营商网关一定放行。移动、联通、电信在具体下发环节会有二次内容过滤和号码状态检查。常见原因包括号码本身有问题空号、停机、关机、携号转网后通道未及时同步、号码被标记为营销号码。这类只能用回执状态判断不会有接口报错。内容命中运营商关键词即使模板审核通过了实际下发时如果内容里包含“发票”“贷款”“积分兑换”“余额变更”等高风险词部分省网会做二次过滤。“到期”“续费”这些词本身没问题但它们出现在“尽快续费否则自动扣款”的语境里容易触发运营商误判。同一模板内容被高频群发如果某个模板在短时间内被大量使用即使发送方请求合规运营商也会启动批量内容核查。一旦被标记为营销或骚扰后面同一批次就会大面积失败。退订号码的影响用户之前对某个签名回复过TD退订如果运营商的黑名单列表已生效你继续发就会被拦。所以模板里的退订提示不是摆设要用就得真的处理退订请求。针对这种情况我的建议是发现回执大面积未送达时先抽几条手机号在控制台查发送记录看运营商返回的描述再把模板内容通过审核人员会审尽量把文案往“纯通知”方向靠——通知内容里不要有任何“现在不办就亏了”的诱导性表达促销类话术是运营商过滤的重灾区。4.5 第五层频率控制和流控限制阿里云短信的流控分为两类账号维度流控单个账号在单位时间内的最大发送量有限制号码维度流控同一个手机号在24小时内对同一个签名和模板组合的发送次数有限制常见错误码是isv.BUSINESS_LIMIT_CONTROL和isvm.DAY_LIMIT_CONTROL。前者的意思是触发账号级流控通常出现在突然大批量发送时后者是单号码日发送次数超限。我在到期提醒场景里对这个限制的处理是把T-7、T-3、T-1三次提醒放到不同的模板里。比如模板A发“即将到期”、模板B发“最后3天提醒”、模板C发“今天是最后一天”。三个模板分别对应不同的发送组合可以减小单个模板被重复推送同文案的概率。同时业务上也避免了同一个模板内容反复发给用户导致投诉。还有一点要注意24小时内同一个用户收到同类短信达到一定条数运营商也会拦截。所以T-7、T-3、T-1的节奏如果间隔太短比如当天连发两条大概率第二条就卡在运营商那儿。4.6 第六层回执状态到底怎么解读当你把查询回执的代码写好以后会看到各种状态字段。别只盯DELIVRD和UNDELIV两个词运营商返回的Description才是关键。常见描述包括用户已关机空号/不存在停机黑名单用户手机容量满手机上安装了拦截软件我在做回执统计时会按描述文本做聚合而不是只看成功失败。“空号”“停机”可以走线下客服跟进或直接清洗号码“黑名单用户”可能代表该用户已经对这个签名退订过继续发反而会引发投诉。所以回执信息不只是技术数据它直接指导运营策略。4.7 第七层余额、套餐和服务的“隐藏”原因最后一种情况问题根本不在API代码或推送网关而是账号层面余额不足、套餐到期、签名对应的产品没有续费。这些情况发送接口一般会直接返回特定错误码比如isv.ACCOUNT_NOT_EXIST或者AMOUNT_NOT_ENOUGH。但也有部分场景下请求受理了后续下发才失败账上欠费越多失败越明显。所以每次到期高峰之前我都有一个固定动作检查账号余额设置余额提醒阈值。这不算技术活但属于“最容易忽略却最致命”的生产事故。账上没钱的当天正好赶上合同到期高峰期那画面不要太美。5. 把到期提醒做成靠谱功能重试、削峰、数据留痕5.1 发送链路状态机在实际业务系统中我不会把“sendSms一发完任务就结束”作为实现。发一条短信完整流程应该是一个状态机初始状态PENDING → 定时任务捞取 → 调用发送接口 → 得到受理结果 → 进入SENT状态 → 通过回执确认为DELIVERED或者FAILED → 失败则进入RETRY队列 → 重试多次仍失败 → 进入MANUAL队列。对应的表结构大概是字段说明id主键user_id用户IDphone手机号template_code模板编码template_paramJSON变量plan_send_time计划发送时间status枚举PENDING/SENT/DELIVERED/FAILED/MANUALretry_count已重试次数api_code发送接口返回Codereceipt_status回执状态create_time / update_time时间戳定时任务每5分钟扫一次PENDING任务到期且满足发送时间窗口的才真正调用。这相当于把发送动作变成一个异步任务池。好处非常大批量任务不会一次性打爆接口单条失败可以精准重试运营人员想插队或取消某条任务直接在数据库改状态就行。5.2 重试策略重试不等于无脑循环。我这边实践下来的重试时间线是第一次失败接口报错非OK5分钟后重试第二次失败30分钟后重试第三次失败6小时后重试超过三次状态置为MANUAL推送给运营人员人工处理还有一条很重要的原则对“接口报错”和“回执未送达”要区别对待。接口报错说明请求都没进通道重试有价值回执未送达可能意味着号码本身失效重试10次也一样。所以我会把FAILED任务按失败原因分类只有错误码属于可恢复类型比如频率限制、流控、临时性错误才自动重试。空号、停机这种直接进人工表。5.3 削峰到期日批量发送的调度设计到期提醒天然具有“月底扎堆”的特征。比如合同系统底数5000个用户其中一半月底到期。如果在28、29、30号三天里各发一次每天的发送量可能有两三千条。这时候除了限速还要做提醒日历的调度设计。我的做法是把用户按到期日构建一张“提醒日历”比如到期日为T的用户在T-7、T-3、T-1分别做标记每天的定时任务不重扫全部用户而是只扫当天有标记的用户发送时间窗口分散比如T-7的发送时间安排在上午9点到11点T-1的发送时间安排在下午14点到16点这样既是流控友好的也让用户的接收时间不那么“半夜惊魂”。顺便说一句晚上22点到次日8点之间运营商对营销内容管控很严虽然通知类短信能发但用户投诉率更高所以尽量把发送时间安排在白天工作时间段。5.4 数据留痕给运营留一条“看得见的退路”代码跑通只是第一步。运营团队每天都会问今天到底给多少人发了成功短信多少人没收到为什么没收到所以从上线的第一天我就把短信发送记录同步到了业务系统的运营看板。每一天的到期提醒任务都要能查出来任务执行时间、发送条数、成功条数、失败条数、失败原因分布。这部分数据也是后面和短信供应商对账、甚至换供应商的依据。另外短信内容本身也要留痕。用户投诉时客服需要知道“当时到底发了什么”所以发送历史表里除了状态字段还建议存一条完整短信内容正文快照。拿着这个去跟用户解释比空口说要靠谱得多。5.5 备份通道与紧急联系人最后是一条兜底经验。短信API的故障不会提前跟你打招呼它可能在某天上午突然延迟很高或者某条消息大面积不回执。如果业务真的要求“到期提醒必须到达”那就要准备备用通道。同样的一条到期提醒我在生产上就配了双通道主通道阿里云短信备通道腾讯云短信。日常全部走主通道如果主通道回执延迟超过10分钟或者失败率超过阈值发送任务自动切到备通道。双通道实现成本不高只要在发送服务层做一个接口抽象public interface SmsSender { SendResult send(RemindTask task); } public class AliyunSmsSender implements SmsSender { // 阿里云实现 } public class TencentSmsSender implements SmsSender { // 腾讯云实现 }然后给发送任务配置一个默认Sender和一个Fallback Senderfailover逻辑非常简单。当然备用通道的签名、模板也要提前审核好别等切过去才发现备胎根本没通过审核。做完这些到期提醒这件事才算真正落地而不只是“调通了个接口”。我个人在实际操作中最深的体会是短信API对接的难点从来不在那几行发送代码而在于你愿不愿意花时间把签名模板的合规性、回执状态的追踪、发送任务的调度和失败降级方案都补齐。这活儿看着琐碎但在生产环境里每一环都能在关键时刻救你一命。