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
118 lines
5.3 KiB
Markdown
118 lines
5.3 KiB
Markdown
# 创蓝云智(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`(二选一) |
|
||
|
||
## 鉴权:两种方式(二选一,不可并用)
|
||
|
||
1. **HMAC 签名头(推荐,密码不上行)**:请求头带 `X-QA-Hmac-Signature`,body **不放** `password`。
|
||
2. **明文密码**:body 放 `password`,不带签名头。
|
||
|
||
### HMAC-SHA256 签名算法
|
||
|
||
1. `md5Password = MD5(password)` —— 32 位**小写十六进制**。
|
||
2. 取三个值 `[md5Password, timestamp, nonce]`,**按字典序升序排序**,**无分隔符拼接**,再 `replaceAll("\\s+", "")` 去除所有空白。
|
||
3. `signature = HmacSHA256(key = md5Password, message = 上一步拼接串)` —— 输出**小写十六进制**。
|
||
4. 放入请求头:`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\"}]"`
|
||
|
||
## 响应格式
|
||
|
||
```json
|
||
{
|
||
"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 用签名头)
|
||
|
||
```json
|
||
{
|
||
"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)= 请求鉴权。二者含义完全不同,勿混。
|