Spring Boot短信服务接入实战:签名机制、回调处理与生产环境避坑指南
发布时间:2026/9/30 7:58:08来源:尧图网络
做Java后端这些年几乎每个项目都逃不过“发短信”这个需求登录验证码、订单通知、告警提醒、营销触达……短信接口看着就是个POST请求但真正从参数配置、签名计算、回调接收到生产环境的限流、幂等、监控一整条链路埋了不少坑。这两年我陆续在几个Spring Boot项目里完整对接过短信服务踩过不少坑也攒了不少经验这篇就把整个流程串起来讲清楚从服务商选型、工程配置、核心代码封装一直聊到回调处理和线上问题排查适合刚接到短信需求不知道从哪下手的同学也适合已经接入但偶尔出问题的兄弟对照自查。1. 短信接入前先想清楚这三件事1.1 服务商怎么选三种方案的取舍国内接短信本质上只有三条路云厂商短信服务、运营商行业网关、第三方短信聚合平台。云厂商短信服务是目前最主流的选择。阿里云、腾讯云、华为云这些都提供完整的短信API和封装好的SDK接入文档齐全、控制台有签名和模板审核流程、回执和统计都给你配好了软件开发侧要操心的事情最少。缺点是价格不算最低营销类短信被审核得也很严。直接对接运营商行业网关适合短信量非常大、对单价敏感的团队。比如一天发几十万条验证码的公司走运营商网关能把单价压到很低。但这条路门槛高需要企业资质、需要和运营商谈通道、需要自己处理协议细节CMPP、SGIP这些协议熟悉的人很少、还要自建网关服务。我见过有团队为省成本选了这条路结果一个协议字段问题调了一周。第三方聚合平台比如云片、SendCloud这类的本质上是把多家运营商的通道聚合起来提供统一的API。好处是容灾好一个通道挂了自动切另一个运维省心坏处是底层通道不稳定时排查比较麻烦服务质量取决于平台。我的建议很明确90%的项目直接选云厂商短信服务就行别自己折腾。选的时候看四个指标三网触达率、到达时效、单价、审核速度。测试时一定要拿真实手机号跑一遍联通、移动、电信三网的手机别只看官网宣传数据。1.2 短信类型决定对接策略很多人把短信API理解成一个简单的“发消息”接口但实际业务里短信分三类对接策略完全不同。第一类是验证码短信。特点是时效性要求高、频率受限、必须防刷。验证码一般在5分钟或10分钟内有效同一个手机号一分钟内不能重复发送一天内发送次数要限制而且必须有图形验证码或行为验证来兜底。这类短信对接的关键不只在于“发出去”更在于后续的校验逻辑——要把验证码存到Redis并设置过期时间提交时比对且一次性消费掉。第二类是通知短信。订单状态、物流提醒、账户变动这类量大但对时效要求没那么极致。这类短信可以走异步丢一两条也不至于出大问题适合接MQ做削峰发送失败有补偿机制兜底就行。第三类是营销短信。需要专门的营销签名、营销模板发送时间段有明确限制比如晚上不能发用户在短信里有退订入口。这类短信审核最严而且到达率往往不如验证码因为通道方会对营销内容限流。对接前先分清楚自己的业务属于哪一类因为后续的签名、模板、发送限额、代码设计都会不一样。别用一个签名和模板去硬撑三类场景审核会被卡到了也容易被封。1.3 读接口文档先抓这四点短信接口的API文档动辄几百页别全看先盯住四个核心部分发送接口、查询接口、状态回执回调、上行短信回调。发送接口是核心。注意请求参数有哪些、返回字段怎么解析、错误码表怎么查。一般来说发送接口只要返回了“请求成功”都不代表用户收到了那只是代表网关受理了最终结果要看状态回执。查询接口属于辅助能力。发送后如果不放心可以用MessageId去查一条短信的最终状态。真正生产环境里我的建议是别依赖主动查询主要靠回调查询接口只作为补偿手段。状态回执回调是重中之重。短信从网关下发到用户手机后运营商会上报“已送达”“已失败”这些状态服务商再通过HTTP回调把你的后台地址。这个回调如果处理不当最典型的问题就是用户明明没收到短信你的系统却一直以为发出去了。上行短信就是用户回复的短信比如回复T退订、回复特定指令参与活动。有互动类业务才需要关注纯通知类项目可以先不看。最后还必须搞清楚接口的鉴权方式。几乎所有云服务商都是AK/SK加签名的方式AccessKeyId标识身份AccessKeySecret参与签名计算服务端通过校验签名确认请求没被篡改。签名计算规则通常是参数排序、URL编码、拼接、HMAC加密这一步流程这部分我后面单独讲是大多数人第一次对接时最容易卡住的点。2. 工程准备与参数配置2.1 初始化Spring Boot项目要接短信建议直接新建个模块或者在已有项目里加依赖我比较推荐在已有项目里加而不是单独建服务除非你们公司短信逻辑特别复杂需要独立部门维护。项目初始化时的几个选择先捋清楚。JDK版本用17或21如果你们还在用JDK8那你大概率用的是Spring Boot 2.x下面的示例代码里把jakarta改成javax即可。Spring Boot直接用3.xMaven或者Gradle都行我用的是Maven。依赖方面核心就是spring-boot-starter-web做异步的话再加spring-boot-starter-data-redis。Lombok属于个人偏好了我加了能让配置类和实体类少写很多样板代码但如果你不习惯就算了不强迫。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency注意Spring Boot 3.x里面很多javax包都迁移到jakarta了网上不少老教程复制下来是直接用不了的遇到包名不一致时先从这点排查。2.2 配置参数用ConfigurationProperties短信的配置项说多不多说少不少AccessKeyId、AccessKeySecret、签名名称、模板Code、接口地址、超时时间、连接池大小。散落在代码各处绝对是灾难统一集中管理用ConfigurationProperties绑定是最舒服的。在application.yml里加一段配置sms: access-key-id: ${SMS_AK_ID:your-access-key-id} access-key-secret: ${SMS_AK_SECRET:your-access-key-secret} sign-name: ${SMS_SIGN_NAME:你的短信签名} endpoint: https://sms.aliyuncs.com connect-timeout: 3000 read-timeout: 5000 max-conn-total: 200 max-conn-per-route: 50注意这里的${SMS_AK_ID:...}写法先读环境变量环境变量不存在时才用默认值。生产环境压根不要把真实Key写到application.yml里一定要走环境变量或者配置中心。再建一个配置类Data Component ConfigurationProperties(prefix sms) public class SmsProperties { private String accessKeyId; private String accessKeySecret; private String signName; private String endpoint; private int connectTimeout; private int readTimeout; private int maxConnTotal; private int maxConnPerRoute; }为什么推荐ConfigurationProperties而不是Value注解注入第一是类型安全配置项多的时候不会因为拼错字段名在运行时才炸第二是分组清晰所有短信参数在一个类里维护第三是方便做参数校验。在类上用Validated加NotBlank就能启动时校验关键参数是否缺失比运行时才发现问题强得多。2.3 参数安全与多环境管理短信的AccessKeySecret是最高敏感级别的凭据泄露了别人就能拿你的账号发垃圾短信月初账单能让你怀疑人生。生产环境务必遵守几条铁律第一AccessKeySecret不进application.yml、不进Git仓库、不打日志第二环境隔离测试环境和生产环境用不同的AK/SK别共用第三正规一点的公司建议用KMS或者专门的密钥管理服务托管让应用在启动时动态拉取。多环境管理我是这么做的本地开发和测试环境用.env或者本地环境变量注入生产环境字段全部从环境变量读取配置中心负责下发。图上画的流程不复杂核心就是三个环境三套AK/SK互不交叉。有一个细节很多人会忽略接口的endpoint也可能随环境变化测试环境服务商的沙箱地址和生产地址不一样配置类里要留好这个字段。3. 核心服务封装与发送实现3.1 发送接口抽象与扩展点短信发送代码不要一股脑写在一个工具类里然后到处new建议抽象出一个接口加一个实现方便以后换服务商或者做Mock测试。public interface SmsSender { SmsSendResult send(SmsMessage message); } Data Builder NoArgsConstructor AllArgsConstructor public class SmsMessage { private String phone; private String templateCode; private MapString, String templateParams; private String bizId; // 业务侧唯一ID用于幂等 } Data Builder public class SmsSendResult { private boolean success; private String requestId; private String bizId; private String code; private String message; }接口里塞一个bizId字段容易被忽略但它非常关键。业务调用方每次发验证码时生成一个UUID传进来后面无论排查问题还是做幂等、对账都靠这个ID串联。扩展点方面至少要预留两个。一个是服务商切换的桥接口这个接口本身就是为了让服务商实现可替换。另一个是短信类型枚举SmsType区分验证码和通知方便后续做分模板、分通道、分限制策略。3.2 短信签名计算的原理与代码服务商提供的SDK用起来固然省事但我强烈建议至少手写一遍签名计算不然出了问题只能干瞪眼。下面以最常见的RPC风格API为例走一遍完整链路。签名的目的是保证请求参数没有被篡改。其核心流程是把请求参数按字典序排序、做URL编码、按规则拼接成一个待签名字符串再用AccessKeySecret做HMAC加密最后Base64编码放进请求参数里。public static String sign(MapString, String params, String accessKeySecret) throws Exception { // 1. 参数名按ASCII码从小到大排序 MapString, String sortedParams new TreeMap(params); // 2. 拼接规范化的请求字符串 StringBuilder sb new StringBuilder(); for (Map.EntryString, String entry : sortedParams.entrySet()) { sb.append(percentEncode(entry.getKey())) .append() .append(percentEncode(entry.getValue())) .append(); } sb.deleteCharAt(sb.length() - 1); // 3. 构造待签名字符串 String stringToSign GET%2F percentEncode(sb.toString()); // 4. HMAC-SHA1 加密key是 AccessKeySecret Mac mac Mac.getInstance(HmacSHA1); mac.init(new SecretKeySpec((accessKeySecret ).getBytes(StandardCharsets.UTF_8), HmacSHA1)); byte[] signData mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8)); // 5. Base64编码后返回 return Base64.getEncoder().encodeToString(signData); } private static String percentEncode(String value) throws Exception { return URLEncoder.encode(value, StandardCharsets.UTF_8.name()) .replace(, %20) .replace(*, %2A) .replace(%7E, ~); }这个代码里有几个非常容易踩的坑。URL编码不能直接用URLEncoder.encode的结果因为Java默认会把空格编码成但规范要求的是%20把*保留为%2A把~还原成普通字符这几个替换步骤缺一不可。参数顺序必须是ASCII字典序TreeMap天然帮你排序了但如果你自己拼字符串一定要记得排序。HMAC的key是AccessKeySecret 这个不能丢丢了签名永远比对不上。签名算什么是对了发送时的公共参数也要凑齐。一个完整的接口调用需要这些参数混在一起MapString, String params new HashMap(); params.put(AccessKeyId, smsProperties.getAccessKeyId()); params.put(Action, SendSms); params.put(Version, 2017-05-25); params.put(Format, json); params.put(RegionId, cn-hangzhou); params.put(PhoneNumbers, message.getPhone()); params.put(SignName, smsProperties.getSignName()); params.put(TemplateCode, message.getTemplateCode()); params.put(TemplateParam, JSON.toJSONString(message.getTemplateParams())); params.put(SignatureMethod, HMAC-SHA1); params.put(SignatureNonce, UUID.randomUUID().toString()); params.put(SignatureVersion, 1.0); params.put(Timestamp, formatTime(new Date())); params.put(Signature, sign(params, smsProperties.getAccessKeySecret()));SignatureNonce是随机串每次请求必须不同Timestamp要使用GMT格式的ISO8601时间且要求服务端时间偏差在15分钟以内。生产环境如果服务器时间漂移了签名也会报错这个在排查问题时要能想到。3.3 HTTP调用细节与连接配置签名算完了接下来的HTTP调用环节反而有不少细节坑。很多人图省事直接HttpClient裸调结果线上发慢、超时、连接复用出问题。我这里用的是Spring的RestTemplate但一定要自定义配置因为Spring Boot自动配置的RestTemplate默认没有超时限制一个慢接口能拖死你的线程。Bean public RestTemplate smsRestTemplate(Autowired SmsProperties props) { HttpComponentsClientHttpRequestFactory factory new HttpComponentsClientHttpRequestFactory(); factory.setConnectTimeout(props.getConnectTimeout()); factory.setReadTimeout(props.getReadTimeout()); factory.setConnectionRequestTimeout(1000); // 连接池配置 CloseableHttpClient httpClient HttpClientBuilder.create() .setMaxConnTotal(props.getMaxConnTotal()) .setMaxConnPerRoute(props.getMaxConnPerRoute()) .setRetryHandler(new DefaultHttpRequestRetryHandler(0, false)) .build(); factory.setHttpClient(httpClient); return new RestTemplate(factory); }几个参数解释一下。connectTimeout是建立TCP连接的超时readTimeout是等待响应超时这两个对短信场景建议都别超过5秒因为短信API本身的响应就很快慢势必有网络问题。连接池的大小根据你们系统的并发量来估Tomcat默认200线程的话短信连接池设个50就够没多少场景需要短信这块瞬时扛200并发。重试策略注意置成0短信发送接口不该盲目重试因为首次请求成功但因为超时你以为失败了再发一次就重复扣费了。3.4 异步发送与超时兜底业务里发短信通常只是所有步骤中的一个环节比如用户注册流程校验参数、创建账号、发送验证码、返回结果。如果发短信同步等待一个网络抖动能把整个请求拖到5秒多用户早走了。我推荐的做法是短信发送本身异步化但异步策略分两种场景。验证码场景建议用CompletableFuture做异步加超时兜底。给未来设置一个3秒的超时超时了不阻塞主流程但需要打日志报警追踪。因为验证码用户大概率会重试所以发送失败可以异步补偿。CompletableFutureSmsSendResult future CompletableFuture.supplyAsync(() - smsSender.send(smsMessage), smsExecutor); try { SmsSendResult result future.get(3, TimeUnit.SECONDS); if (!result.isSuccess()) { log.error(短信发送失败: {}, result.getMessage()); } } catch (TimeoutException e) { log.warn(短信发送超时, bizId{}, smsMessage.getBizId()); }通知类短信如果量大建议直接丢进消息队列由消费方异步发送加失败重试。但要注意短信发送本身很快绝大多数项目其实不需要引入MQ加入MQ反而会让链路变得复杂运维成本和故障点都会增加。一个几千上万日发送量的系统用线程池加上定时任务补偿完全够用。线程池一定要单独定义并且用有界队列不然短信服务商抖动时任务堆积能把内存打满。我一般这样配核心线程4最大8队列容量200拒绝策略是CallerRunsPolicy让业务线程自己兜底。4. 回调接收状态报告与上行回复4.1 回调地址的配置要求短信发出去之后最终结果是通过回调通知到你的系统。在服务商控制台里需要配置两个回调地址状态报告回调地址和上行回复回调地址。这两个地址有几条硬性要求。第一必须是公网可访问的HTTPS地址没有公网地址的话就用内网穿透或者网关转发但生产环境一定要有正式域名。第二处理回调的接口响应要快服务商那边对回调响应有时限你返回超时它会认为是失败然后重推。第三接口必须做好验签和防重不能谁都能往你接口上POST数据。回调机制本身很简单服务商把状态数据POST到你的接口你的接口返回HTTP 200表示收到。有个设计上很容易忽略的点接口返回的200只代表“你收到了”不代表“你处理成功了”。如果处理数据时数据库出了问题建议还是返回200然后异步补偿而不是返回500让服务商一直重推否则服务商会把你这个地址当故障地址处理。4.2 用Controller接收状态回执回调接口的实现不难但有几个细节很关键。以下面这个状态报告接收为例RestController RequestMapping(/api/sms/callback) public class SmsCallbackController { PostMapping(/report) public String handleReport(RequestBody SmsReportRequest request) { // 1. 验签验签失败直接拒绝 if (!smsReportService.verifySign(request)) { return sign verify failed; } // 2. 落库存原始报文 smsReportService.saveRawReport(request); // 3. 解析业务状态更新订单/验证码状态 smsReportService.processReport(request); return SUCCESS; } }不同服务商的回调报文格式不一样有的JSON有的Form表单。后端统一做法是先拿到原始报文原样存一份到日志表或日志文件再做字段解析和业务更新。这个原始报文很值钱后面如果出现对账纠纷、查“用户到底收没收到”的问题原始报文就是最硬的证据。处理流程上建议拆成异步。回调接口本身只做验签、存原始报文、投递到内部队列真正的状态更新逻辑放在异步执行。这样回调接口响应快不会因为业务数据更新失败导致超时而且批量回调打进来时不会把你的接口压垮。4.3 幂等处理与消息去重这是我必须重点强调的环节。服务商为了保证消息不被丢失回调是会重推的而且不一定按顺序推同一个MessageId可能推两三次、三四次。如果不做幂等最直接的后果是一条验证码的“发送成功”回执被处理两次把用户状态改成“已消费”的代码执行了两次业务上出现莫名其妙的状态重复变更。更严重的是电商场景里“用户付款成功”这种如果重复处理会影响订单状态判断。幂等的做法无非两种。数据库唯一约束最稳在状态回执表上给biz_id建唯一索引重复的记录插入直接冲突报错第二次来了自然就跳过了。Redis的SETNX也可以但要注意设置过期时间别让Redis里的标记无限堆积。我给个最简单的数据库方案try { smsReportDao.insert(report); } catch (DuplicateKeyException e) { // 重复回调直接忽略 return SUCCESS; }配合唯一索引代码里连判断逻辑都可以省了数据库帮你去重。要在一个事务里处理查一下是否存在再插入不是原子操作并发下会有竞态别图省事写这种逻辑。4.4 回调安全验签与白名单回调地址暴露在公网上除了服务商任何人都有可能往这个地址上POST假数据。如果回调里串了业务状态更新逻辑别人伪造“发送成功”的回执就能让未发送成功的业务状态变成已成功。安全上两手都要硬。第一是验签服务商通常会提供验签方法或签名算法说明在接收回调后第一步就验验不过的直接返回失败。第二是IP白名单把调用回调的IP段固定下来Spring Boot里用一个Filter或者AOP拦截非白名单IP直接拒绝双保险。无论哪个环节回调接口的日志一定要记录完整请求头和请求体排查问题全靠这些。5. 生产环境部署的经验补充5.1 限流防刷同一手机号60秒只能发一次短信是有成本的而且验证码接口天然会被羊毛党盯上。不限制的话一台服务器能被人刷掉一天几千块的短信费用。最基础的维度有三个手机号维度、IP维度、设备维度。全部用Redis实现逻辑简单但很有效。String key sms:limit:phone: phone; Long count redisTemplate.opsForValue().increment(key); if (count ! null count 1) { redisTemplate.expire(key, Duration.ofSeconds(60)); } if (count ! null count 1) { throw new SmsLimitException(发送太频繁请稍后再试); }这个代码的思路是用INCR命令给手机号加计数器第一次时顺手设置60秒过期第二次再来时已经过期重新计数或直接拦住。为了防止竞态在实际生产里这个检查和后面的发送动作之间最好加一个分布式锁或者直接把限流放进发送流程的最前面。除了发送频率限制验证码本身的校验也要设计好验证码存Redis5分钟有效验证成功后立即删除防止同一个验证码被重复使用。还可以加一个“一天内同一手机号最多发10条验证码”的限制防止恶意用户不断触发发送接口。5.2 重试与补偿本地任务表兜底短信发送不是每一次都能成功。网络抖动、服务商接口限流、模板审核不过都可能让发送失败。失败之后要不要重试、重试多少次、间隔多久这需要一套补偿机制。我的做法是建一张本地短信发送任务表字段大致是biz_id、手机号、模板Code、模板参数、发送状态、失败原因、重试次数、下次重试时间。业务侧调发送接口时先往这张表里落一条记录状态是PENDING然后异步去调服务商接口。成功了更新状态失败了根据错误码决定直接放弃还是延迟重试。定时任务每30秒扫一次表把next_retry_time早于当前时间且重试次数小于5的记录捡出来重新发送。重试间隔用指数退避第一次失败等1分钟第二次等5分钟第三次等15分钟最多试5次。注意区分可重试错误码和不可重试错误码手机号非法这种重试一万次也没用直接置为FAILED网络超时、系统繁忙这种才值得重试。5.3 日志与监控指标短信系统的日志和监控比想象中重要得多。用户说“我没收到验证码”时你要能在30秒内定位出是没发出去、网关拒了、运营商拦截了还是手机号本身有问题。日志方面发送接口的日志必须包含 biz_id、手机号脱敏、模板Code、请求ID、返回结果、耗时。回调日志必须保存原始报文。所有日志严禁输出AccessKeySecret和完整的短信验证码明文。手机号建议脱敏成138****8000不然日志文件泄露就是安全隐患。监控方面重点看四个指标。发送成功率(发送成功请求数/发送请求总数)任何异常阈值要能通过Prometheus或Zabbix告警。到达率这东西依赖回调统计判断用户实际收到短信的比例达到率骤降往往意味着通道出问题。回调耗时和回调堆积量回调接口处理不过来会导致状态更新延迟。费用消耗每天跑个定时任务拉一把短信费用账单费用异常上涨往往对应着攻击或业务异常。5.4 密钥管理与审计最后强调一下AccessKeySecret的管理。有些团队喜欢写死在application.yml里然后提交到Git这是最危险的行为。规范的做法开发环境可以用环境变量测试环境放在配置中心里生产环境由专门的密钥管理系统下发。至少要做到每个环境一套独立的AK/SK谁泄露了都能快速定位。服务商后台也会提供子账号能力给短信服务单独创建一个子账号权限只授权短信API别拿公司的全局主账号去调短信接口。再加一道审计短信发送的记录表里必须有请求来源。某个功能模块发的短信、某个接口的调用IP、哪个用户在操作都能查得到。这项审计在出安全问题的时候能救命。6. 对接中高频问题排查实录做短信对接这一年多我把遇到最多的问题列个速查表每个都是真实环境里碰过的现象常见原因处理办法报SignatureDoesNotMatch参数排序不对、URL编码错了、拼接顺序反了按字典序排序检查%20、%2A、~的转义打印待签名字符串与服务商文档比对报InvalidAccessKeyIdAccessKeyId配错或子账号没权限核对ID确认子账号已授权短信服务发送接口返回成功但用户没收到手机号在黑名单、内容含敏感词、通道被拦截查回调状态联系服务商查通道记录报TemplateParamMissing模板里有变量但参数没传全对照模板内容检查参数Map一个变量都不能少同一验证码重复使用消费验证码时没删除Redis里的记录验证成功后delete再看一眼是不是有并发提交的情况回调重复处理服务商重推接口没做幂等建唯一索引或Redis标记重复直接忽略报MobileNumberIllegal手机号里有空格、带了86、位数不对发送前统一做正则校验和格式化服务器时间漂移导致签名失败系统时间与标准时间偏差超过15分钟同步NTP时间调整签名逻辑验证码发送超时但用户收到了readTimeout设置太短服务商响应慢了调大readTimeout加上超时后的状态查询补偿这里重点讲两个印象最深的。签名问题是最多人卡的坎。我见过有同事自己写签名逻辑怎么验都对不上最后打印出来才发现是URL编码时号没有替换成%20。这种问题排查起来就是对照待签名字符串和服务商的文档一步步比对没有捷径。还有一个是“发送成功但用户收不到”的玄学问题。发送成功只代表网关受理成功不代表用户真的收到了。从服务商那拿到状态回执后失败原因十有八九是这几类用户在运营商侧退订或投诉拉黑、内容含敏感词触发拦截、某个通道临时故障、测试号码没在服务商白名单里这个特别容易出现测试阶段不加上白名单发送压根不推。排查先看回执消息再联系服务商客服拿通道原始日志基本都能定位。7. 踩坑后的几点个人体会做短信功能做了这么久最大的体会是这个功能单看技术难度完全不高难的是把整条链路串起来的细节。签名计算、超时配置、异步策略、回调幂等、限流防刷、日志监控、密钥管理每一环都有坑任何一环偷懒上线后都是要还的。先说签名。我很建议每个要做这块的人都手写一遍签名逻辑哪怕最后实际用的是SDK。手写一遍你才能理解SDK到底帮你做了什么出了问题才知道去查哪里不然拿个报错去问客服客服跟你要待签名字符串你都不知道是什么。再说回调。回调的幂等处理一定一定不能省。我见过市面上不少开源项目里的短信对接代码接收回调后直接更新业务状态也不查重。这种代码短期看着没问题一旦服务商重推数据就对不上了。先建唯一索引这是投入最小的保障。然后说测试。短信功能绝不能只在测试环境发几条就算完了。上线前必须准备一批真实手机号覆盖三网、虚拟运营商、停机号、空号实际测一遍完整链路重点看状态回执能不能按时回来、回调有没有延迟、失败时业务有没有正确补偿。这一轮测试能暴露的问题比写代码时多得多。最后说架构。如果是微服务架构推荐把短信能力独立成一个公共服务其他业务通过HTTP或RPC调用别每个服务各自对接一遍服务商。这样限流策略、签名密钥、日志监控都集中出了问题排查路径也短。模块拆不拆取决于团队规模但至少公共代码要抽到一个独立模块别复制粘贴。说句实话等这套流程都跑顺了、线上稳定运营一段时间之后短信功能在整个系统里基本就是个“沉睡”的模块。你在它身上投入的那几天时间会在某次用户投诉、某次对账、某次通道故障时以极高的方式回报给你。
网站建设高端定制企业官网