Spring Boot短信服务系统设计与实现:从验证码到前后端分离全解析

Spring Boot短信服务系统设计与实现:从验证码到前后端分离全解析
做“基于Spring Boot的短信服务系统”这个题目我一开始其实有点不以为然。短信服务嘛无非是调一个短信平台的API把验证码或者通知消息发出去就完事了能有多复杂直到真正动手把用户登录、权限校验、签名模板管理、发送限流、异步重试、回调对账、数据统计、前后端分离联调全部串起来才意识到这居然是一个麻雀虽小、五脏俱全的完整系统。这也是为什么每年都有大量毕业生和转行者选这个题目当作毕设或实训项目——它覆盖面广、可展示性强每一块单独拆出来都有实实在在的技术含量。这篇文章就围绕“基于Spring Boot的短信服务系统的设计与实现”这个课题结合我在真实项目里积累的经验把整套系统的设计思路、数据库建模、后端核心实现、前端Vue页面开发、部署上线以及各种坑一次性讲透。不管你是正在准备毕业设计的学生还是想通过一个完整项目来巩固Spring Boot和Vue技能的开发者这篇文章都值得你从头到尾看一遍。1. 开工前先把需求聊透短信服务系统到底要做什么1.1 真实业务中的短信场景很多时候学生做项目容易犯一个毛病——拿到题目就急着建工程写代码结果写到一半发现需求根本没想清楚。短信服务系统的核心业务场景其实非常明确主要就是三大类第一类是验证码短信最常见的就是用户注册、登录、找回密码时发送的六位数字验证码。这类短信的特点是频率高、时效性强、安全要求高必须考虑防刷、防暴力破解、验证码过期等细节。第二类是通知短信比如订单支付成功提醒、发货通知、系统告警。这类短信的发送对象通常是老用户内容相对固定一般通过短信模板来管理。第三类是营销短信像电商平台的活动推广、优惠券提醒。这类短信在真实商业场景里受到严格管控学生项目里一般只是预留接口不会真正大规模发送。一个合格的短信服务系统往小了做就是一个“调用第三方短信API的工具”但往完整了做它应该是一套可运营的短信平台——有用户管理、有签名审核、有模板管理、有发送记录、有统计报表。我们做设计的时候目标应当定在后者。1.2 技术栈选型为什么是Spring Boot加Vue技术选型往往不是选“最好的”而是选“最合适的”。Spring Boot在这类管理系统里的统治力不需要我多说它最大的优势在于快速集成和生态成熟。你想加Redis做缓存加MyBatis-Plus做数据持久化加RabbitMQ做异步消息加Spring Security做权限控制都是一套起步依赖加几行配置的事非常适合课程设计和中小型项目落地。前端选Vue也很合理。Vue的学习曲线相对平缓中文资料多社区活跃度极高。结合Element Plus组件库可以快速搭建出看着还不错的后台管理界面。更重要的是Vue本身就是当下国内企业使用率最高的前端框架之一用这个技术栈做出来的项目在面试和答辩时能讲的东西也更多。有一点我想单独强调如果只是做一个“能跑”的演示项目Spring Boot加Thymeleaf模板引擎可能更快。但我们在设计阶段选择前后端分离是因为短信服务系统天然带有“平台管理”属性——前端页面负责交互和展示后端只提供RESTful API两者通过JSON通信。这种架构在真实项目中是主流也方便团队并行开发。既然要学就按业界标准来。2. 系统架构与数据库设计2.1 前后端分离的整体架构整个系统的架构可以拆成几个层次来看。最顶层是Vue前端应用负责用户登录、短信发送页面、签名模板管理页面、发送记录查询页面、数据统计大屏这些界面。前端通过Axios调用后端接口请求携带JWT Token做身份认证。后端整体采用经典的分层架构Controller层接收HTTP请求Service层处理业务逻辑Mapper层负责数据库操作。这种分层方式在Spring Boot项目里已经是标准答案好处是职责清晰、便于测试、易于扩展。再往下是技术支撑组件。MySQL存储业务数据Redis用来存验证码和做频率限制RabbitMQ或者线程池负责处理异步发送任务短信服务商SDK负责真正把短信发出去。这里用的是市面上主流的短信服务商比如阿里云短信或腾讯云短信对接的是它们提供的HTTP API或SDK不涉及任何违规内容。从模块划分角度来看后端可以分为认证模块、短信签名管理模块、短信模板管理模块、发送模块、回调模块、统计模块。每个模块独立成包包名和类名都遵循Spring Boot目录规范这里我放一个简化版的工程结构供参考com.example.sms ├── controller │ ├── AuthController.java │ ├── SmsSendController.java │ ├── SmsSignController.java │ ├── SmsTemplateController.java │ └── SmsRecordController.java ├── service │ ├── SmsSendService.java │ ├── SmsVerifyCodeService.java │ └── SmsCallbackService.java ├── mapper │ └── SmsRecordMapper.java ├── entity ├── dto ├── config │ ├── RedisConfig.java │ ├── SecurityConfig.java │ └── SmsClientConfig.java └── common ├── Result.java └── exception2.2 核心数据表的设计思路数据库设计是整个系统最不能马虎的环节。很多初学者喜欢“用到什么建什么”结果表之间字段对不上改起来痛苦无比。我建议先想清楚核心业务对象再动手建表。首先是用户表sys_user字段包括id、username、password、phone、status、role、create_time。密码必须用BCrypt加密存储绝对不允许明文入库。role字段区分管理员和普通操作员虽然学生项目里可能用不到太细的权限但预留这个字段是合理的。然后是短信签名表sms_sign和模板表sms_template。签名是短信内容开头的【xx公司】这部分模板是经过审核的内容骨架例如“您的验证码为${code}5分钟内有效”。这两张表都必须有audit_status字段因为服务商对签名和模板有审核机制审核通过之后才能正式发送。最核心的是短信发送记录表sms_record。这张表直接决定了系统能不能做数据分析和问题排查。字段包括id、batch_id、phone、sign_name、template_code、template_param、content、scene、status、error_code、error_msg、retry_count、send_time、create_time。batch_id用于幂等控制同一批发送请求共享一个批次号scene用于区分验证码、通知、营销等不同业务场景retry_count记录重试次数避免无限重试。最后建议加一张sms_callback表保存服务商回调过来的短信状态报告。短信发出之后服务商会异步回调通知我们这条短信是“发送成功”还是“发送失败”把这些回调数据存下来才能做对账和统计分析。2.3 API接口设计接口设计遵循RESTful风格统一返回结构Result对象。Result里包含code、message、data三个字段前端根据code判断业务成功还是失败这样Axios拦截器处理起来非常统一。核心接口大致有这几个POST /api/auth/login登录获取JWTGET /api/auth/info获取当前用户信息POST /api/sms/verify-code发送验证码短信POST /api/sms/direct-send发送通知或营销短信POST /api/sms/callback接收服务商回调需要服务商签名校验GET /api/sms/records分页查询发送记录GET /api/sms/stats获取发送统计图表数据POST /api/sms/sign新增签名GET /api/sms/sign签名列表POST /api/sms/template新增模板GET /api/sms/template模板列表这些接口基本覆盖了一个短信平台的所有核心操作也在答辩的时候足够支撑你讲清楚系统的业务闭环。3. 后端核心模块的落地实现3.1 接入短信服务商SDK的正确姿势对接短信服务商是整个系统最核心的环节。以阿里云短信为例你需要先注册账号并开通短信服务创建AccessKey、签名和模板。服务商审核通过之后在Spring Boot的配置文件里配置AccessKey ID、AccessKey Secret、签名名称、模板CODE。这里特别提醒一个安全习惯千万不要把AccessKey硬编码在代码里更不要提交到Git仓库。即使是课程设计项目也建议通过环境变量或者配置文件去引用密钥因为答辩现场或者面试官翻你仓库代码的概率比你想象中要高。后端发送短信的核心代码大致是这样// 初始化客户端 DefaultProfile profile DefaultProfile.getProfile( cn-hangzhou, accessKeyId, accessKeySecret ); DefaultAcsClient client new DefaultAcsClient(profile); // 构建请求 CommonRequest request new CommonRequest(); request.setSysMethod(MethodType.POST); request.setSysDomain(dysmsapi.aliyuncs.com); request.setSysVersion(2017-05-25); request.setSysAction(SendSms); request.putQueryParameter(PhoneNumbers, phone); request.putQueryParameter(SignName, signName); request.putQueryParameter(TemplateCode, templateCode); request.putQueryParameter(TemplateParam, {\code\:\123456\}); // 发送并判断返回结果 CommonResponse response client.getCommonResponse(request); String code JSON.parseObject(response.getData()).getString(Code); if (!OK.equals(code)) { throw new SmsSendException(发送失败 code); }这里有一个非常重要的细节服务商返回Code字段只有Code为OK才代表请求受理成功。很多人对接接口时只判断了HTTP状态码200结果短信没发出去还查不到原因。常见的错误码比如isv.SMS_SIGNATURE_ILLEGAL表示签名不合法isv.MOBILE_NUMBER_ILLEGAL表示手机号格式不对isv.BUSINESS_LIMIT_CONTROL表示触发业务流控这些错误信息一定要存到sms_record表的error_code和error_msg字段里方便后续排查。3.2 验证码的生成、存储与校验验证码短信是短信服务系统里最常用的功能也是安全要求最高的环节。验证码本身是六位随机数字生成逻辑很简单但“存储、校验、过期”这一整条链路需要注意的细节很多。我采用的是Redis存储方案。发送验证码时把手机号、业务场景和验证码作为一个Key存进Redis并设置5分钟过期时间。校验时取出Redis中的验证码与用户输入做比对匹配成功后立即删除这个Key保证一次性使用。如果校验失败还应该记录失败次数超过一定次数直接让验证码失效防暴力破解。这里有一个初学者经常踩的坑验证码的Key撞车问题。如果同一个手机号在登录和找回密码两个场景都发送验证码只用手机号做Key就会互相覆盖。所以我建议Key设计成sms:code:{scene}:{phone}的格式场景不同互不干扰。对应的清理逻辑也不要只依赖Redis过期机制校验成功之后要手动delete这样更保险。3.3 接口防刷与发送频率限制短信接口天然容易被恶意刷——有人拿脚本批量调你的发短信接口消耗你的短信配额同时也骚扰手机用户。所以防刷是短信系统必须具备的能力这也是答辩时一个很好的加分点。防刷策略我做了两级。第一级是对同一手机号的发送频率限制一分钟内同一手机号最多发送1条一小时最多5条一天最多10条。实现方式是用Redis的INCR命令配合过期时间比如sms:limit:{phone}:minute这个Key设置60秒过期每次发送前检查计数是否超限。第二级是验证码校验失败次数限制比如一个手机号的验证码校验连续失败5次就锁定该手机号15分钟不允许校验。这样可以防止攻击者用脚本暴力枚举验证码。一条发送前的完整校验链路大致是这样校验用户是否登录 → 校验手机号格式 → 校验场景参数 → 校验签名模板状态 → 检查Redis频率限制 → 生成验证码或准备模板参数 → 发送短信 → 写入发送记录 → 更新Redis计数 → 返回结果这套链路在真实项目中已经非常接近生产标准。当你把这个过程讲清楚面试官基本能确认你对接口防刷有实操认识而不是只会“调用API发短信”。3.4 为什么发送要做异步化和重试在短信服务系统里“发送”这一步的耗时往往是整个接口最大的瓶颈。同步发送模式下用户请求会一直阻塞到服务商返回结果如果服务商响应慢接口就可能超时。更严重的是如果服务商临时故障同步模式直接返回失败用户只能再次点击发送体验很差。解决方案是把发送操作扔到异步线程池里执行。接口收到请求后先把记录写入数据库状态标记为“待发送”立即返回“已受理”。后台线程池从任务队列里取出记录调用服务商API完成发送再更新状态。这样接口响应时间从一秒以上降低到几十毫秒用户体验明显提升。使用线程池的时候要注意不要用JDK默认的newFixedThreadPool里的无界队列否则消息堆积时内存容易出问题。我用的是ThreadPoolExecutor配合有界队列并设置拒绝策略为交由调用方线程执行保证不丢任务。同时还要注意线程池核心线程数不要设置太大短信服务的瓶颈通常在服务商侧本地并发太高反而容易触发服务商的流控策略。异步发送还带来了重试的可能性。服务商返回isv.BUSINESS_LIMIT_CONTROL之类的临时性错误时简单返回失败对业务方不够友好。我的实现是发送失败后如果重试次数小于3次退避重试第一次等10秒第二次等1分钟第三次等5分钟。重试逻辑用Spring的Retryable注解或者自己写一个循环都可以关键是重试之后一定要更新retry_count字段方便统计重试成功率和排查问题。3.5 回调接收与对账机制短信发出去了不代表就送达了。服务商只能保证“短信平台受理成功”至于用户手机有没有收到需要通过服务商的状态回调来确认。阿里云短信支持设置状态报告回调URL短信有最终状态后会向这个URL发起POST请求内容包含手机号、业务ID、状态码等。后端需要提供一个回调接口去接收这些数据。这里有一个生产环境非常重要的原则回调接口必须做签名校验防止别人伪造回调请求污染你的数据。服务商一般会用请求头和特定密钥做MD5签名校验不通过直接拒绝。回调带来的另一个问题是数据对账。回调是异步的、可能乱序的可能一条短信回调两次。所以写入sms_callback表时要做幂等判断最好给biz_id加唯一索引重复回调时直接忽略。回调数据落库后再根据biz_id去更新sms_record表里的状态字段把“已发送”更新为“已送达”或“发送失败”。这一整套回调机制做完之后系统在业务上就形成了完整的闭环前端发起请求 → 后端异步发送 → 服务商受理 → 服务商回调状态 → 后端更新记录 → 前端在列表页看到最新状态。这也是我在答辩时最喜欢展示的一条完整业务链路。4. 前端Vue实现与关键技术细节4.1 工程搭建和Element Plus自动导入的问题短信服务系统的前端是典型的Vue后台管理应用。实操下来我推荐直接用Vite创建Vue3工程比较干净构建速度也比Webpack快一个量级。至于Vue2还是Vue3我建议直接学Vue3。不是喜新厌旧而是Vue3的Composition API在写复杂业务组件时逻辑复用和代码组织的优势确实太大了而且现在新项目基本都在拥抱Vue3花时间学老版本是浪费。创建好工程后我优先配置的是Element Plus组件库。这里重点说一个很多初学者必踩的坑自动导入的问题。按官方文档配置unplugin-auto-import和unplugin-vue-components之后大家会发现HTML模板里直接用el-button没有问题但在JavaScript代码里用ElMessage时会报“ElMessage is not defined”。原因在于自动导入插件默认只处理组件模板中的标签不会处理JavaScript变量。解决办法是在vite.config.js里额外声明ElMessage、ElMessageBox这些函数的自动导入import { defineConfig } from vite import vue from vitejs/plugin-vue import AutoImport from unplugin-auto-import/vite import Components from unplugin-vue-components/vite import { ElementPlusResolver } from unplugin-vue-components/resolvers export default defineConfig({ plugins: [ vue(), AutoImport({ resolvers: [ElementPlusResolver()], imports: [vue, vue-router] }), Components({ resolvers: [ElementPlusResolver()] }) ] })这里还有一个细节自动导入样式。如果是按需引入组件别忘记在组件配置里开启importStyle: css否则组件样式会丢失页面看着惨不忍睹。这个坑我调试了一下午才找到原因。4.2 登录鉴权与路由守卫短信服务系统是一个管理平台没有登录鉴权等于把短信接口裸奔在公网上任何人都能恶意调用。所以前端必须配合后端实现登录流程。登录页提交用户名密码后后端验证通过会返回一个JWT Token前端把它存到localStorage里。Axios请求拦截器在每个请求的请求头上带上Authorization: Bearer ${token}后端再从JWT中解析出用户信息。Vue Router的全局前置守卫则负责路由拦截没有拿到Token就跳转登录页已经在登录页却带着Token直接跳转首页。手动设置Axios拦截器时有一个细节我要多嘴一句浏览器回传的Token在请求头里的名称前后端一定要对得上。后端读取请求头字段名写的是Authorization前端Axios拦截器设置的位置就不要用token之类自定义名称统一标准最好是Authorization避免联调时互相甩锅。4.3 验证码倒计时和短信发送页面短信发送页面是整个前端交互的核心。我设计的是一个“立即发送”工作台左侧选择业务场景中间填手机号右侧是发送结果显示。验证码发送的话需要实现一个倒计时按钮。用户点击“获取验证码”之后按钮要变成“重新发送(60s)”倒计时结束才能再次点击。这里有个常见的实现错误定时器泄漏。如果用户倒计时过程中直接跳转了路由而组件销毁时没有清除setInterval定时器就会一直运行可能出现反复触发更新甚至内存泄漏的问题。正确做法是在onUnmounted钩子里clearInterval。另外一个细节是按钮要在发送成功后再进入倒计时防止用户重复点击发出多条短信。发送记录列表页我使用了Element Plus的Table组件加分页每一行显示手机号、签名、模板、状态、发送时间。状态那一列用Tag标签渲染成功显示绿色失败显示红色待发送显示黄色一眼就能看出问题。列表页还要支持按手机号、按状态、按时间范围筛选筛选条件直接拼到后端分页查询的Query参数里。4.4 Vue3组合式API的合理运用Vue3项目里我倾向于用组合式API来组织代码。选项式API的data、methods、computed、watch在组件逻辑简单时确实直观但组件复杂之后同一个功能点的代码会被拆到不同选项块里维护起来很碎。组合式API则可以把“发送验证码”相关的所有响应式状态和函数放到一起形成一个独立的逻辑块。更进一步还可以用composables目录抽离自定义Hook比如useSmsSend这个函数可以把手机号、倒计时、发送状态、发送方法全部封装起来多个组件共用代码整洁度直接上一个台阶。前端在工程项目里的目录规范也很重要。我建议的目录结构是src/api放接口请求、src/router放路由配置、src/store放Pinia状态、src/views放页面组件、src/components放通用组件、src/utils放工具函数。这套结构是Vue后台项目中非常成熟的范式能直接沿用到大项目里。5. 联调部署与安全加固5.1 前后端联调和跨域问题前后端分离开发联调是绕不开的话题。开发环境里前端跑在5173端口后端跑在8080端口浏览器的同源策略会拦截跨域请求。最简单的解决方案是在Vite配置文件里启用开发代理把所有/api开头的请求转发到后端服务地址server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样前端代码里请求路径写成/api/sms/records浏览器发起的是同源请求Vite开发服务器再转发给后端完美规避跨域问题。很多人喜欢在后端加CrossOrigin注解或者配置全局CORS过滤器这种方式在生产环境不推荐因为前端部署到Nginx之后同域名下根本不需要CORS加了反而多一道风险。后端如果确实要支持跨域应该用配置类统一设置而不是在Controller类上零散地打注解。允许的域名要精确配置别直接用*否则生产环境任何第三方网站都可以往你的接口发请求安全风险极大。5.2 生产环境部署的完整过程部署部分我踩过不少坑写出来给后来人省点时间。后端用Maven的mvn clean package打成Jar包在服务器上用nohup java -jar sms-server.jar 启动。前端先执行npm run build生成静态文件到dist目录然后通过Nginx把dist目录挂起来对外提供服务。Nginx配置是前后端分离部署的关键。核心是两段前端静态资源直接指向dist目录接口请求通过location /api代理到后端。如果Vue路由用了history模式还要额外配置try_files $uri $uri/ /index.html;否则刷新页面会404这个坑几乎每个做前后端分离的人都会踩。一个实用的小技巧接口代理出问题时可以用curl http://localhost:8080/api/sms/records先直接测试后端接口确认后端通不通再看Nginx转发配置能帮你快速缩小问题范围。5.3 Actuator监控暴露风险与安全配置Spring Boot Actuator是项目自带的一套监控组件提供健康检查、指标信息、运行环境等端点。对开发和运维来说非常有用但如果不做安全控制它就是一颗定时炸弹。近两年相关的安全通告不少说到底都是因为端点无意中暴露了敏感信息比如环境变量、配置参数、堆内存快照等。在实际项目中我强烈建议做两件事。第一只暴露必要的端点比如health和info绝对不要把env、beans、heapdump这类端点开放到公网。配置如下management: endpoints: web: exposure: include: health,info第二Actuator端点要加访问控制。最简单的方式是配合Spring Security给这些端点设置独立的认证规则要求只有管理员角色才能访问。如果项目本身没有接入安全框架至少在Nginx层将/actuator路径设置黑白名单限制或者干脆只允许内网IP访问。这不仅是答辩时一个很大的加分项也是真实生产环境里必须具备的运维意识。毕竟一个短信平台如果被通过监控端点扒出了AccessKey、数据库密码那基本上是灾难级别的事故。6. 常见问题与排查技巧实录6.1 短信发送失败的问题怎么快速定位短信发不出去是这类系统最高频的问题。我遇到的失败原因大致可以分成几类第一类是账号配置问题比如AccessKey没配、签名审核没通过、模板审核没通过、账号余额不足第二类是参数问题手机号格式不对、模板变量参数格式不对第三类是频率限制服务商侧限制了同一号码的发送量。排查的时候我习惯先看发送记录表里的error_code和error_msg字段大部分问题在服务商返回的错误信息里都能找到明确线索。如果记录里根本没有数据那说明发送动作压根没触发要去看异步线程池是否正常工作如果记录状态是“待发送”一直没变多半是异步任务没有执行检查线程池配置和日志。这里分享一个我在项目中沉淀的排查口诀先看配置再查记录先看报文再查网络。配置决定能不能连通记录反映有没有执行报文告诉我们为什么失败网络问题永远是最后才怀疑的。6.2 前端联调时的典型问题前端的问题常见的有这么几个。一个是页面白屏命令行报路由找不到多半是路由配置路径和views目录下的文件名不匹配或者是history模式部署后刷新404前者本地就能定位后者需要按前面说的Nginx配置处理。另一个是登录之后Token丢失刷新页面就回到登录页通常是把Token存在了内存变量里而不是localStorage。还有用户经常反馈的“验证码明明对却提示验证码错误”大多数情况是校验成功了但Redis里的Key没有被删除再次校验拿到的还是旧验证码。或者是前端把用户输入的验证码做了Trim处理而后端存的时候没做多了空格导致匹配不上。这些都是细节问题但严重影响体验轮到自己调试的时候建议把请求参数、Redis里的值、校验逻辑一步步打日志对比。6.3 数据库和统计报表相关的常见坑数据统计是短信系统里容易被低估的功能。当发送记录表数据量大了之后按手机号和时间范围做模糊查询会越来越慢。实际项目里可以在phone、create_time、status这几个字段上加联合索引同时统计类SQL尽量走覆盖索引避免回表查询。另外提醒一个非常容易忽略的问题时区。MySQL连接串里一定要写上serverTimezoneAsia/Shanghai否则时间字段在不同环境下可能出现8小时偏差数据分析时经常莫名其妙。这个问题出现过太多次几乎每个项目里都有人栽过。短信验证码、统计报表这类数据还要考虑定时清理策略。生产环境不会让你无限期保留所有验证码记录设计MySQL事件或者写个定时任务定期清理过期无用数据让数据库保持轻量也是系统设计里值得写进文档的一点。写在最后的一点项目体会整套系统做下来我最大的收获不是学会了调用某个短信SDK而是理清了一个“看起来很简单”的业务的完整技术链路从需求拆分、数据库建模到异步化改造、幂等控制、防刷限流再到前后端联调、部署上线和监控安全。每一环单独拿出来都不算复杂但串在一起就会发现真正的难度永远在细节里。短信服务系统这个题目之所以经典正是因为它能让你在有限的时间里走完一个工程师从开发到上线的完整思考过程。如果你也在做类似的课程设计或项目实战建议别满足于“接口通了就完事”试着把限流、重试、回调对账这些生产特性一项项加上去做完之后你对Spring Boot和Vue的理解会完全不一样。

最新新闻

日新闻

周新闻

月新闻