Files
qyyfw a9e141b695 docs: 集成短信宝 SMS Bao 验证码服务
- 新增 docs/smsbao-integration.md(完整集成文档+代码示例)
- 更新 PRD:注册/登录使用短信宝发送验证码
- 更新架构设计:技术栈+选型矩阵+配置说明
- 更新 API 规范:新增 /send-code 接口
- 更新 AGENTS.md:短信服务配置说明

短信宝 API:
- 发送: GET https://api.smsbao.com/sms?u=&p=&m=&c=
- 查询: GET http://api.smsbao.com/query?u=&p=
- 签名格式: 【QihuAI】您的验证码是123456
2026-06-27 12:03:28 +00:00

293 lines
8.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 短信宝 (SMS Bao) 接口集成文档
> 版本: 1.0 | 日期: 2026-06-27
> 官网: https://www.smsbao.com | API 文档: https://www.smsbao.com/openapi
---
## 1. 接口概述
短信宝提供简洁的 HTTP GET 接口,用于发送国内短信验证码。
### 1.1 接口地址
| 功能 | 地址 | 方法 |
|------|------|------|
| 发送短信 | `https://api.smsbao.com/sms` | GET |
| 查询余额 | `http://api.smsbao.com/query` | GET |
### 1.2 请求参数(发送短信)
| 参数 | 必填 | 说明 | 示例 |
|------|------|------|------|
| `u` | 是 | 短信宝注册用户名 | `tom` |
| `p` | 是 | 密码 MD5 值(32位)**或** ApiKey(推荐) | `9b11127a9701975c734b8aee81ee3526` |
| `g` | 否 | 产品 ID(专用通道时填,不填用通用短信) | `123456` |
| `m` | 是 | 手机号(群发逗号分隔,≤99个) | `15205201314` |
| `c` | 是 | 短信内容(UTF-8 URL 编码,须含签名) | `%E3%80%90...%E3%80%91` |
### 1.3 返回值
| 返回值 | 含义 |
|--------|------|
| `0` | 发送成功 |
| 其他 | 错误代码(见下方错误码表) |
### 1.4 查询余额参数
| 参数 | 必填 | 说明 |
|------|------|------|
| `u` | 是 | 用户名 |
| `p` | 是 | 密码 MD5 或 ApiKey |
返回: 数字字符串,如 `"523"` 表示剩余 523 条短信。
---
## 2. 错误码
| 错误码 | 含义 |
|--------|------|
| `0` | 成功 |
| `40` | 手机号格式错误 |
| `41` | 手机号为空 |
| `42` | 内容为空 |
| `43` | 用户名或密码错误 |
| `44` | 账户余额不足 |
| `45` | 签名格式错误(必须含【签名】) |
| `46` | 内容包含敏感词 |
| `47` | 频率超限(同号码4条/天) |
| `50` | 账号被封禁 |
---
## 3. 短信内容规范
### 3.1 签名格式
```
【QihuAI】您的验证码是123456,5分钟内有效。若非本人操作请忽略。
```
- 签名在 `【】` 中间,3-10 个字符
- 签名可为公司简称、网站简称、注册商标名称
- 签名需在短信宝后台报备审核
### 3.2 测试阶段注意事项
- 测试时也需发送正规内容(验证码、通知等)
- 不要发送无意义内容(如"测试"、"你好")
- 同号码每天限收 4 条(可在后台白名单取消限制)
- 群发短信会进入人工审核
---
## 4. FastAPI 集成代码
### 4.1 配置 (.env)
```env
# 短信宝配置
SMSBAO_USERNAME=your_username
SMSBAO_API_KEY=your_api_key_here
SMSBAO_PRODUCT_ID= # 可选,专用通道产品ID
SMSBAO_SIGN_NAME=QihuAI # 短信签名
SMSBAO_API_URL=https://api.smsbao.com/sms
SMSBAO_QUERY_URL=http://api.smsbao.com/query
```
### 4.2 核心服务代码
```python
# services/gateway/smsbao.py
import hashlib
from urllib.parse import quote
from pydantic import BaseModel
import httpx
from core.config import settings
class SMSBaoClient:
"""短信宝短信发送客户端"""
def __init__(self):
self.api_url = settings.SMSBAO_API_URL
self.query_url = settings.SMSBAO_QUERY_URL
self.username = settings.SMSBAO_USERNAME
self.api_key = settings.SMSBAO_API_KEY
self.product_id = settings.SMSBAO_PRODUCT_ID
self.sign_name = settings.SMSBAO_SIGN_NAME
def _get_password(self) -> str:
"""获取认证密码(优先使用 ApiKey,否则 MD5 密码)"""
if self.api_key:
return self.api_key
# 如果没有 ApiKey,对密码做 MD5
return hashlib.md5(settings.SMSBAO_PASSWORD.encode()).hexdigest()
async def send_sms(self, phone: str, content: str) -> dict:
"""
发送短信
:param phone: 手机号
:param content: 短信内容(不含签名,会自动添加)
:return: {"success": bool, "message": str}
"""
# 自动添加签名
full_content = f"【{self.sign_name}】{content}"
encoded_content = quote(full_content, encoding="utf-8")
params = {
"u": self.username,
"p": self._get_password(),
"m": phone,
"c": encoded_content,
}
if self.product_id:
params["g"] = self.product_id
async with httpx.AsyncClient(timeout=10.0) as client:
resp = await client.get(self.api_url, params=params)
result = resp.text.strip()
if result == "0":
return {"success": True, "message": "发送成功"}
else:
return {"success": False, "message": f"发送失败,错误码: {result}"}
async def send_verification_code(self, phone: str, code: str) -> dict:
"""发送验证码短信"""
content = f"您的验证码是{code},5分钟内有效。若非本人操作请忽略。"
return await self.send_sms(phone, content)
async def query_balance(self) -> dict:
"""查询余额"""
params = {
"u": self.username,
"p": self._get_password(),
}
async with httpx.AsyncClient(timeout=10.0) as client:
resp = await client.get(self.query_url, params=params)
result = resp.text.strip()
try:
balance = int(result)
return {"success": True, "balance": balance}
except ValueError:
return {"success": False, "message": f"查询失败: {result}"}
# 单例
smsbao_client = SMSBaoClient()
```
### 4.3 验证码服务
```python
# services/gateway/verification.py
import random
import string
from datetime import datetime, timedelta
from redis import asyncio as redis
redis_client = redis.from_url("redis://localhost:6379/0")
VERIFICATION_CODE_TTL = 300 # 5 分钟
VERIFICATION_CODE_LENGTH = 6
RATE_LIMIT_PER_PHONE = 1 # 每分钟最多 1 条
async def generate_and_send_code(phone: str) -> dict:
"""
生成验证码并发送
1. 检查频率限制
2. 生成 6 位随机验证码
3. 存入 Redis(5分钟过期)
4. 调用短信宝发送
"""
# 频率限制:同号码 60 秒内不能重复发送
rate_key = f"sms:rate:{phone}"
if await redis_client.exists(rate_key):
return {"success": False, "message": "发送太频繁,请60秒后重试"}
# 生成验证码
code = "".join(random.choices(string.digits, k=VERIFICATION_CODE_LENGTH))
# 存入 Redis
code_key = f"sms:code:{phone}"
await redis_client.setex(code_key, VERIFICATION_CODE_TTL, code)
await redis_client.setex(rate_key, 60, "1")
# 发送
from smsbao import smsbao_client
result = await smsbao_client.send_verification_code(phone, code)
if result["success"]:
return {"success": True, "message": "验证码已发送"}
else:
return {"success": False, "message": result["message"]}
async def verify_code(phone: str, code: str) -> bool:
"""验证验证码是否正确"""
code_key = f"sms:code:{phone}"
stored_code = await redis_client.get(code_key)
if stored_code and stored_code.decode() == code:
await redis_client.delete(code_key) # 验证通过后删除
return True
return False
```
### 4.4 API 路由
```python
# services/gateway/routes/auth.py
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from verification import generate_and_send_code, verify_code
router = APIRouter(prefix="/api/v1/auth", tags=["认证"])
class SendCodeRequest(BaseModel):
phone: str
class VerifyCodeRequest(BaseModel):
phone: str
code: str
@router.post("/send-code")
async def send_verification_code(req: SendCodeRequest):
"""发送手机验证码"""
result = await generate_and_send_code(req.phone)
if not result["success"]:
raise HTTPException(status_code=400, detail=result["message"])
return {"code": 0, "message": "验证码已发送", "data": None}
@router.post("/verify-code")
async def verify_phone_code(req: VerifyCodeRequest):
"""验证手机验证码"""
valid = await verify_code(req.phone, req.code)
if not valid:
raise HTTPException(status_code=400, detail="验证码错误或已过期")
return {"code": 0, "message": "验证成功", "data": {"verified": True}}
```
---
## 5. 安全注意事项
1. **图形验证码**: 发送短信前必须验证图形验证码(防短信轰炸)
2. **频率限制**: 同号码 60 秒内只能发 1 条
3. **每日限制**: 同号码每天最多 10 条(可在短信宝后台调整)
4. **密码安全**: ApiKey / MD5 密码存储在 .env,不提交到 Git
5. **HTTPS**: 使用安全接口 `https://api.smsbao.com/sms`
6. **白名单**: 测试阶段在短信宝后台添加测试手机号白名单
---
*文档结束*