短信配置
这篇解决什么
业务模块要发短信时,不要自己接各家 SDK。先在后台配好渠道和模板,再调 SmsSendApi / SmsCodeApi。读完能改渠道账号格式、模板占位符、异步发送链路、验证码限流,以及平台回调。
默认端口 48080,管理端前缀 /admin-api。菜单在系统管理 → 消息中心 → 短信管理。PC 开发页 http://127.0.0.1:5800。
示意图:后台维护渠道和模板;业务只调发送 API。真正打短信平台在异步线程里完成。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 短信渠道 | 平台编码、签名、账号密钥、回调 URL | ruoyi-office/yudao-module-system/.../dal/dataobject/sms/SmsChannelDO.java |
| 短信模板 | code、内容 {var}、绑定渠道 | .../dal/dataobject/sms/SmsTemplateDO.java |
| 短信日志 | 每次发送的手机号、渲染结果、收发状态 | .../dal/dataobject/sms/SmsLogDO.java |
| 验证码 | 场景、次数、是否已用 | .../dal/dataobject/sms/SmsCodeDO.java |
| 发送 API | 给管理员 / 会员各一条 | ruoyi-office/yudao-module-system-api/.../api/sms/SmsSendApi.java |
| 验证码 API | 发码、使用、只校验 | .../api/sms/SmsCodeApi.java |
| 后台页 | 渠道、模板、日志三张列表 | ruoyi-office-vben/apps/web-antd/src/views/system/sms/ |
| 前端 API | /system/sms-channel、sms-template、sms-log | ruoyi-office-vben/apps/web-antd/src/api/system/sms/ |
官方菜单 id:1093 短信管理,1094 短信渠道,1100 短信模板,1107 短信日志。路由是 /system/messages/sms/sms-channel、sms-template、sms-log。

本系统截图:系统管理 → 消息中心 → 短信管理 → 短信渠道。

本系统截图:短信模板列表。行上有编辑、测试、删除。编码如 admin-sms-login 要和场景枚举对齐。

本系统截图:短信日志。发送成功后,接收状态常停在「等待结果」,直到平台回调。
生产验证码和短信费用
生产必须打开行为验证码,见 验证码。发短信按条计费,模板页「测试」和登录发码都会打真实平台。调试用「调试(钉钉)」渠道,不要拿正式阿里云 / 腾讯云账号对着自己狂点。
表结构
四张表都带 @TenantIgnore,库里也没有 tenant_id。渠道、模板、日志、验证码全实例共用。
按本仓库生成的 ER:模板必须挂一个渠道;日志同时冗余渠道和模板快照。验证码单独一张表。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
system_sms_channel | @TableName("system_sms_channel")。code 是 SmsChannelEnum | .../dal/dataobject/sms/SmsChannelDO.java |
system_sms_template | @TableName("system_sms_template")。code 全局唯一 | .../dal/dataobject/sms/SmsTemplateDO.java |
system_sms_log | @TableName("system_sms_log")。发送、接收两套状态 | .../dal/dataobject/sms/SmsLogDO.java |
system_sms_code | @TableName("system_sms_code")。按手机号查最近一条 | .../dal/dataobject/sms/SmsCodeDO.java |
SmsTemplateDO.params 不是手填。保存时用正则 \{(.*?)\} 扫 content,写入 JSON 数组。发送时按这个列表校验,缺 key 抛 SMS_SEND_MOBILE_TEMPLATE_PARAM_MISS。
密钥出现在列表里
渠道列表直接展示 apiKey / apiSecret。库备份、导出、越权打开渠道页都能看到。渠道权限只给运维,密钥不要写进文档或提交进仓库。
实现原理
其它模块只依赖 yudao-module-system-api,注入 SmsSendApi。单体里 SmsSendApiImpl 就是本地 Bean;微服务里走 Feign,服务名 system-server。
发送先同步落日志,再异步调平台。接口马上返回日志 id,不堵业务线程。
示意图:SmsProducer 发的是 Spring 事件,不是消息中间件。SmsSendConsumer 上的 @Async 才把 HTTP 调用挪到别的线程。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 管理端测试 | POST /system/sms-template/send-sms | .../controller/admin/sms/SmsTemplateController.java |
| RPC 发送 | /rpc-api/system/sms/send/send-single-admin、send-single-member | .../api/sms/SmsSendApi.java |
| 同步编排 | 校验模板 / 渠道 / 手机号 / 参数 | .../service/sms/SmsSendServiceImpl.java |
| 异步投递 | applicationContext.publishEvent | .../mq/producer/sms/SmsProducer.java |
| 真正发送 | @EventListener + @Async | .../mq/consumer/sms/SmsSendConsumer.java |
| 模板缓存 | Redis sms_template:{code} | RedisKeyConstants.SMS_TEMPLATE |
| 统一消息短信渠 | 场景绑定后仍进 SmsSendService | .../service/msg/channel/SmsChannelAdapter.java |
mobile 为空时,管理员按 userId 读 system_users.mobile,会员走 MemberService#getMemberUserMobile。两边都没有手机号,抛 SMS_SEND_MOBILE_NOT_EXISTS。
腾讯云这类平台按数组下标填参,不认 {name}。buildTemplateParams 按模板 params 的顺序排成 List<KeyValue>,再交给客户端。
模板或渠道关掉也不会报错
任一 status 不是启用时,只写日志,状态 IGNORE = 30,不发事件。平台故障时改模板的渠道编号和 API 模板编号即可,不用改调用方、不用重启。
短信渠道
菜单:系统管理 → 消息中心 → 短信管理 → 短信渠道。接口 @RequestMapping("system/sms-channel")。

本系统截图:新增短信渠道。渠道编码来自字典 system_sms_channel_code。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 列表 / 新增 | GET /page,POST /create | SmsChannelController |
| 精简列表 | 模板下拉用 GET /simple-list | 同上 |
| 权限 | system:sms-channel:create 等 | 同上 |
| 请求体 | signature / code / apiKey / apiSecret / callbackUrl | .../vo/channel/SmsChannelSaveReqVO.java |
| 枚举 | DEBUG_DING_TALK / ALIYUN / TENCENT / HUAWEI / QINIU | .../framework/sms/core/enums/SmsChannelEnum.java |
本地没有短信账号时,选「调试(钉钉)」:apiKey 填机器人 Webhook 的 access_token,apiSecret 填加签密钥。机器人接入见 钉钉开放平台。这个客户端不会真发短信,只往群里推一条模拟文本。
有模板挂着的渠道删不掉,错误码 SMS_CHANNEL_HAS_CHILDREN。
保存渠道后,SmsChannelServiceImpl#getSmsClient 会 createOrUpdateSmsClient。改密钥立刻生效,不用重启。
短信模板
菜单:短信模板。接口 @RequestMapping("/system/sms-template")。

本系统截图:新增短信模板。模板编码是 API 识别号,短信 API 的模板编号是平台侧审核通过的 ID。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 短信类型 | 1 验证码,2 通知,3 营销 | SmsTemplateTypeEnum |
| 模板编码 | API 只认这个 code,不是主键 | SmsTemplateDO.code |
| 短信渠道 | 下拉来自渠道精简列表 | channelId → SmsChannelDO.id |
| 模板内容 | 占位符写成 {code}、{name} | content |
| API 模板编号 | 平台审核通过后的模板 ID | apiTemplateId |
| 开启状态 | 停用后只记日志 | status |
保存时会调 SmsClient#getSmsTemplate,确认平台侧模板存在且审核通过。调试(钉钉)客户端直接返回审核成功,方便联调。正式渠道审核中或被拒,分别抛 SMS_TEMPLATE_API_AUDIT_CHECKING、SMS_TEMPLATE_API_AUDIT_FAIL。
改渠道只要改 channelId 和 apiTemplateId。channelCode 是冗余字段,保存时从渠道带过来。
测试发送
行上「测试」走 POST /system/sms-template/send-sms,权限 system:sms-template:send-sms。内部调 sendSingleSmsToAdmin。

本系统截图:测试发送。模板内容只读,占位符会拆成独立输入框。
正式渠道会把短信打到填写的手机号。钉钉渠道只进机器人群。
短信日志
菜单:短信日志。接口 @RequestMapping("/system/sms-log")。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 列表 / 导出 | GET /page,GET /export-excel | SmsLogController |
| 发送状态 | 0 初始化,10 成功,20 失败,30 忽略 | SmsSendStatusEnum |
| 接收状态 | 0 初始化,10 成功,20 失败 | SmsReceiveStatusEnum |
| 平台回执 | apiSendCode / apiRequestId / apiSerialNo | SmsLogDO |
接收状态一直停在初始化,是因为平台回不到本系统。回调入口 @PermitAll、@TenantIgnore:
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 阿里云 | POST /system/sms/callback/aliyun | SmsCallbackController#receiveAliyunSmsStatus |
| 腾讯云 | POST /system/sms/callback/tencent | #receiveTencentSmsStatus |
| 华为云 | POST /system/sms/callback/huawei | #receiveHuaweiSmsStatus |
| 七牛云 | POST /system/sms/callback/qiniu | #receiveQiniuSmsStatus |
公网地址形如 https://你的域名/admin-api/system/sms/callback/aliyun。华为云还可把同一地址填进渠道的「短信发送回调 URL」。
短信发送
业务模块注入 SmsSendApi,不要直接 new 客户端。
@Resource
private SmsSendApi smsSendApi;
smsSendApi.sendSingleSmsToAdmin(new SmsSendSingleToUserReqDTO()
.setUserId(userId)
.setTemplateCode("bpm_task_assigned")
.setTemplateParams(Map.of("name", nickname)));| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 给管理员 | mobile 空则按 userId 查 | SmsSendApi#sendSingleSmsToAdmin |
| 给会员 | 同上,查会员手机号 | #sendSingleSmsToMember |
| 请求体 | userId / mobile / templateCode / templateParams | .../api/sms/dto/send/SmsSendSingleToUserReqDTO.java |
| 返回 | 短信日志编号 | CommonResult<Long> |
工作流待办、审批结果这类通知,模板编码自己起名(如 bpm_task_assigned),和验证码场景枚举无关。消息中心绑定短信渠后,也会进同一套 SmsSendService。
验证码发送
登录、注册、改密不要自己生成数字再调 SmsSendApi。用 SmsCodeApi,它会写 system_sms_code、按场景限流,再以 {code} 发出。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 发码 | POST /rpc-api/system/oauth2/sms/code/send | SmsCodeApi#sendSmsCode |
| 使用 | PUT .../use,校验后把 used 置真 | #useSmsCode |
| 只校验 | GET .../validate,不消耗 | #validateSmsCode |
| 管理端发码 | POST /system/auth/send-sms-code | AuthController |
| 会员发码 | POST /member/auth/send-sms-code | AppAuthController |
场景号在 SmsSceneEnum。templateCode 必须和后台模板编码一致,否则发送阶段找不到模板。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
1 | 会员登录,模板 user-sms-login | SmsSceneEnum.MEMBER_LOGIN |
2 | 会员改手机,user-update-mobile | MEMBER_UPDATE_MOBILE |
3 | 会员改密,user-update-password | MEMBER_UPDATE_PASSWORD |
4 | 会员忘记密码,user-reset-password | MEMBER_RESET_PASSWORD |
21 | 管理员登录,admin-sms-login | ADMIN_MEMBER_LOGIN |
22 | 管理员注册,admin-sms-register | ADMIN_MEMBER_REGISTER |
23 | 管理员忘记密码,admin-reset-password | ADMIN_MEMBER_RESET_PASSWORD |
管理员发码前会先查 system_users 有没有这个手机号,没有直接失败,不会顺手开户。会员短信登录可以 createUserIfAbsent。细节见 用户体系。
限流写在 yudao.sms-code:
yudao:
sms-code:
expire-times: 10m
send-frequency: 1m
send-maximum-quantity-per-day: 10
begin-code: 9999
end-code: 9999| 名称 | 说明 | 仓库路径 |
|---|---|---|
expire-times | 有效期,默认 10 分钟 | SmsCodeProperties |
send-frequency | 同一手机最短间隔,默认 1 分钟 | 同上 |
send-maximum-quantity-per-day | 当天上限,默认 10 条 | 同上 |
begin-code / end-code | 随机区间。两端都是 9999 时验证码恒为 9999 | application.yaml |
超频抛 SMS_CODE_SEND_TOO_FAST,超日限抛 SMS_CODE_EXCEED_SEND_MAXIMUM_QUANTITY_PER_DAY。过期、已用、找不到分别是 SMS_CODE_EXPIRED、SMS_CODE_USED、SMS_CODE_NOT_FOUND。
生产不要留下 9999
begin-code 和 end-code 都配成 9999 是为了本地好测。正式环境改成四位或六位区间,例如 100000~999999。否则任何人猜 9999 就能过短信登录。
新增场景时:在 SmsSceneEnum 加一项,后台建同名模板,调用方传新的 scene。不要复用别人的模板编码。
短信客户端
framework/sms 封装各平台 HTTP。业务层只拿 SmsClient。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 接口 | sendSms / parseSmsReceiveStatus / getSmsTemplate | .../framework/sms/core/client/SmsClient.java |
| 工厂 | 按渠道编码 new 实现类,按渠道 id 缓存 | .../client/impl/SmsClientFactoryImpl.java |
| 阿里云 | dysmsapi.aliyuncs.com,SignName 取渠道签名 | AliyunSmsClient |
| 腾讯云 | sms.tencentcloudapi.com,地域 ap-guangzhou | TencentSmsClient |
| 华为云 | 华北四接入地址,账号要带通道号 | HuaweiSmsClient |
| 七牛云 | sms.qiniuapi.com | QiniuSmsClient |
| 调试钉钉 | Webhook 模拟发送 | DebugDingTalkSmsClient |
接新平台:写一个 AbstractSmsClient 子类,在 SmsClientFactoryImpl#createSmsClient 的 switch 里加分支,并在 SmsChannelEnum、字典 system_sms_channel_code 补编码。
建议至少备两个正式渠道。验证码渠道挂了,把对应模板改到另一条渠道即可。
各平台账号
阿里云:apiKey / apiSecret 用 AccessKey。签名填渠道的「短信签名」,必须和控台已审签名一致。回执在 短信服务 · 通用设置 配到 /system/sms/callback/aliyun。
腾讯云:apiSecret 填 SecretKey。apiKey 必须是 secretId sdkAppId,中间一个空格。例如 SecretId 为 A、SDKAppID 为 1400xxxx,则账号填 A 1400xxxx。密钥在 API 密钥,应用编号在 应用管理。回执在 基础配置 配到 /system/sms/callback/tencent。
华为云:apiKey 必须是 accessKeyId sender,sender 是短信通道号。密钥在 访问密钥。回执可填渠道表单的回调 URL,对应 /system/sms/callback/huawei。
七牛云:账号密钥在 密钥管理。回执在 云短信设置 配到 /system/sms/callback/qiniu。
配置与操作
管理员先在 消息中心 → 短信渠道 选厂商、填密钥,再在模板里写 code(如 bpm_*)并绑渠道。保存后 SmsSendApi 按编码找模板。
回执 URL 填到云厂商控制台,对应 /system/sms/callback/{channel}。调试钉钉走同一套渠道,见 Webhook。开启见 系统功能设计。截图见上文渠道和模板。
