Files
ai-paas-comparison/docs/smsbao-integration.md
T
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

8.4 KiB
Raw Blame History

短信宝 (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)

# 短信宝配置
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 核心服务代码

# 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 验证码服务

# 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 路由

# 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. 白名单: 测试阶段在短信宝后台添加测试手机号白名单

文档结束