ed76820e97
读完了两份 spec + 分派器,这分支的全貌清晰了。下面是可直接粘进 Gitea PR 的 MR 说明(标题 + 正文): 建议标题 feat(sms): 短信验证码可切换多 provider(极光/阿里云/创蓝),默认极光零改动 MR 正文(复制以下 markdown): ## 概述 把原单文件 `app/integrations/sms.py` 重构为**可切换 provider 包** `app/integrations/sms/`,在保留极光(默认、行为零改动)的基础上,新增两家验证码短信 provider: - **阿里云号码认证 dypns**(Mode A:阿里云生成/存储/校验验证码,核验免费) - **创蓝云智 253**(Mode B:本服务自管码,httpx 直连 + HMAC 签名) Provider 由 `SMS_PROVIDER` 按调用实时选择,默认 `jiguang`。短信=花钱 + 登录关键路径,故新 provider **opt-in、可灰度、秒级回退**,默认路径零变更。 ## 为什么 现有极光路径本地内存存码(多 worker 不共享,已是技术债),且单一供应商无法灰度/切换。引入 provider 抽象后:阿里云托管码可消除存码债,创蓝作为备选降低单点依赖,三家随配置切换与回退。 ## 改动内容 **架构(`app/integrations/sms/`)** | 文件 | 说明 | |---|---| | `__init__.py` | 对外仍暴露 `send_code/verify_code/SmsError`(auth 导入不变);按 `SMS_PROVIDER` **每次调用**分派;未知值回退 `jiguang` | | `base.py` | `SmsError`(status_code→HTTP) + provider 无关的 `mock_verify` | | `jiguang.py` | 原 `sms.py` 逻辑**原样迁入**,行为零改动(git 识别为 rename) | | `aliyun.py` | 新增,Mode A:`SendSmsVerifyCode` + `CheckSmsVerifyCode`,惰性加载 SDK | | `chuanglan.py` | 新增,Mode B:自管码 + `tpl/send` + HMAC 签名 | **两种验证码模式** - Mode A(阿里云):不本地存码,阿里云 `##code##` 托管生成+校验;本地仅留 per-phone 失败计数防爆破。 - Mode B(极光/创蓝):`secrets` 生成 N 位 → 进程内存 → 供应商只下发;本地一次性校验 + 失败 N 次作废。创蓝**复制**极光存码机器(不重构极光,零回归风险)。 **配置(`config.py` + `.env.example`)** - `SMS_PROVIDER = jiguang | aliyun | chuanglan`(默认 jiguang) - `ALIYUN_SMS_*`(AK/签名/模板/方案名/时长…) + `aliyun_sms_configured` 门控 - `CHUANGLAN_SMS_*`(账号/密码/模板/签名/endpoint…) + `chuanglan_sms_configured` 门控 - 复用现有 `SMS_MOCK / SMS_CODE_LENGTH / SMS_CODE_TTL_SEC / SMS_SEND_INTERVAL_SEC / SMS_MAX_VERIFY_ATTEMPTS` - 切到某 provider 却未配齐 → `send_code` 抛 `SmsError(503)`,不静默 **auth.py(最小改动)** - `verify_code` 现在可能抛 `SmsError`(阿里云降级 503)→ `sms_login`、`wechat_bind_phone_sms` 两处各包 `try/except SmsError → HTTPException`,与 `send_code` 现有写法一致。 **依赖** - `pyproject.toml` 增 `alibabacloud_dypnsapi20170525`(仅阿里云 provider 惰性 import;jiguang/chuanglan 不加载)。创蓝零新依赖(httpx + 标准库)。 **测试** - 新增 `test_sms_aliyun.py` / `test_sms_chuanglan.py`(均 monkeypatch 网络接缝,不发真短信) + `test_sms_dispatch.py`(分派/回退)。 - `test_auth.py` 相应更新。 - 现有测试走 `SMS_MOCK=true` 在分派层短路,不受影响。 **文档** - 设计 spec:`docs/superpowers/specs/2026-07-25-aliyun-sms-verify-design.md`、`2026-07-26-chuanglan-sms-verify-design.md` - 接口调研:`docs/integrations/aliyun/*`、`docs/integrations/chuanglan/tpl-send.md`、`docs/integrations/sms.md` ## 兼容性 & 回退 - **默认 `SMS_PROVIDER=jiguang`,线上行为与现状完全一致**;不改极光逻辑、不动 API 层频控与测试账号短路。 - 切阿里云/创蓝仅改环境变量,出问题秒切回极光;未知 `SMS_PROVIDER` 一律回退极光,防误配打挂登录。 --------- Co-authored-by: guke <guke@autohome.com.cn> Reviewed-on: #188
5.3 KiB
5.3 KiB
创蓝云智(253/蓝创云智)模板短信 v2 发送接口
官方文档:https://doc.chuanglan.com/document/HAQYSZKH9HT5Z50L 用途:手机号 + 验证码登录的验证码短信下发(本服务生成码 → 创蓝只负责发送,属自管码 Mode B,与极光同模式)。 本文件为接口调研摘要,供
app/integrations/sms/chuanglan.py实现对照。以线上文档为准。
接口概览
| 项 | 值 |
|---|---|
| 请求地址 | POST https://smssh.253.com/msg/sms/v2/tpl/send |
| Content-Type | application/json(UTF-8) |
| 协议 | HTTPS |
| 鉴权 | HMAC-SHA256 签名头 X-QA-Hmac-Signature(推荐)或 body 明文 password(二选一) |
鉴权:两种方式(二选一,不可并用)
- HMAC 签名头(推荐,密码不上行):请求头带
X-QA-Hmac-Signature,body 不放password。 - 明文密码:body 放
password,不带签名头。
HMAC-SHA256 签名算法
md5Password = MD5(password)—— 32 位小写十六进制。- 取三个值
[md5Password, timestamp, nonce],按字典序升序排序,无分隔符拼接,再replaceAll("\\s+", "")去除所有空白。 signature = HmacSHA256(key = md5Password, message = 上一步拼接串)—— 输出小写十六进制。- 放入请求头:
X-QA-Hmac-Signature: <signature>。
注意
key就是md5Password本身(32 位 hex 字符串),不是原始 password。timestamp/nonce同时也是 body 字段,必须与签名里用的一致。
请求参数(body,JSON)
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
account |
String | 是 | API 账号;验证码短信用 YZM 前缀账号(如 YZM0000001) |
timestamp |
String | 是 | Unix 秒级时间戳;60 秒内有效,过期报 139 |
nonce |
String | 是 | 32 位随机串(防重放) |
phoneNumbers |
String | 是 | 手机号,逗号分隔最多 1000 个;YZM 验证码账号不支持批量,只能单号 |
templateId |
String | 是 | 模板 ID(控制台创建 / 模板接口查询) |
templateParamJson |
String | 条件 | 模板变量,JSON 字符串;模板有 {s} 占位符时必填(见下) |
password |
String | 条件 | 仅在不使用签名头时放 body |
signature |
String | 条件 | 短信签名文案(如 【创蓝云智】);模板未关联签名时必填。注意与鉴权头 X-QA-Hmac-Signature 是两回事 |
report |
String | 否 | "true" 时接收状态回执 |
callbackUrl |
String | 否 | 回执回调完整 URL |
uid |
String | 否 | 自定义标识(≤256 字符),回执原样返回 |
extend |
String | 否 | 数字扩展码(≤5 位),用于上行匹配 |
templateParamJson 格式与 {s} 占位
- 模板内容用
{s}作占位符,例:您正在申请手机注册,验证码为:{s},5分钟内有效! templateParamJson是 JSON 数组,元素为对象,键按param1、param2…递增;第 1 个{s}←param1,第 2 个 ←param2。- 单条验证码(一个
{s}= 验证码)示例:"[{\"param1\":\"123456\"}]"
响应格式
{
"code": "000000",
"msgId": "25071018345400902898000000000001",
"time": "20250710183454",
"successNum": "1",
"failNum": "0",
"errorMsg": ""
}
| 字段 | 说明 |
|---|---|
code |
状态码,"000000" = 成功 |
msgId |
消息 ID(32 位) |
time |
响应时间戳 |
successNum / failNum |
提交成功 / 失败条数 |
errorMsg |
错误描述(成功为空) |
响应 / 错误码(节选)
| code | 含义 | 归属 |
|---|---|---|
000000 |
成功 | — |
101 |
账号不存在 | 客服 |
102 |
密码错误 | 客服 |
103 |
提交速度过快(超频) | 客服 |
107 |
手机号码错误 | 客服 |
109 |
无发送量(余额/套餐不足) | 销售 |
110 |
不在发送时段 | 销售 |
116 |
签名不合法 / 未带签名 | 服务 |
117 |
IP 未加白名单 | 服务 |
118 |
账号无发送权限 | 服务 |
124 |
模板内容不匹配 | 服务 |
129 |
JSON 格式错误 | 客服 |
135 |
相同手机号内容重复 | 客服 |
139 |
时间戳过期 | 客服 |
152 |
模板不存在 | 服务 |
158 |
退订文案不合规 | 客服 |
完整请求示例(单条验证码,明文密码方式省略 password 用签名头)
{
"account": "YZM0000001",
"timestamp": "1752143733",
"nonce": "x4zfk0y5foqwx6cbnw3bfmimy98abqs1",
"phoneNumbers": "17601337176",
"templateId": "1021143438",
"templateParamJson": "[{\"param1\":\"123456\"}]",
"report": "true"
}
(HMAC 方式:另加请求头 X-QA-Hmac-Signature: <算法输出>,body 不含 password。)
接入要点
- IP 白名单:服务器出网 IP 必须在控制台加白,否则 117。
- 验证码账号(YZM):无发送时段限制;单号发送、不支持批量。
- 时间戳 60s:
timestamp与本地时钟偏差过大会 139,注意服务器时间同步。 - 退订文案:仅支持
拒收请回复R,且必须在短信末尾(验证码短信一般无需)。 - 签名 vs 鉴权头:
signature(body)= 短信开头的【品牌】文案;X-QA-Hmac-Signature(header)= 请求鉴权。二者含义完全不同,勿混。