feat: 短信接入极光真实发送 + 新增运营 admin 后台子应用

短信(SMS_MOCK 切 mock/real):
- integrations/sms.py 重写: real 模式走极光短信 REST /v1/messages 自定义验证码(本服务 secrets
  生成 6 位码 + 进程内存 + 本地校验一次性/防爆破), 鉴权复用极光一键登录 JG_APP_KEY/MASTER_SECRET
  (同一极光应用, 上线只需 SMS_MOCK=false); mock 仍"任意6位通过"不动其余测试
- 防刷四层: 单号冷却 + 单号每日上限 + 单IP rate_limit(/sms/send 10/min、/sms/login 20/min)
  + 单码失败次数作废; SmsError 带 status_code 映射 429/503/400
- config 增 SMS_SEND_ENDPOINT/SIGN_ID/TEMPLATE_ID/CODE_LENGTH/DAILY_LIMIT/MAX_VERIFY_ATTEMPTS;
  test_auth 加 real 模式单测; sms.md/后端技术实现/待办账本同步

admin 后台(app/admin/ 独立子应用, uvicorn app.admin.main:admin_app :8771):
- 复用主仓 models/repositories/integrations + 同库, 鉴权完全隔离(ADMIN_JWT_SECRET≠JWT_SECRET_KEY
  + payload typ=admin + bcrypt 密码 + 可选 IP 白名单); 主 app 不 import 本包, admin 崩不影响主进程
- 路由: 登录 / 账号管理(RBAC: super_admin·finance·operator) / 用户列表+360详情+封禁+手动调币 /
  钱包流水 / 提现重试对账 / 反馈工单 / 数据大盘; 全写操作落 admin_audit_log(涉钱与业务写同事务)
- 涉钱逻辑(调微信/退款/对账)复用 app.repositories.wallet 不重写
- 新增 models/admin.py(AdminUser/AdminAuditLog) + admin_tables 迁移 + create_admin.py +
  deploy/shaguabijia-admin.service; 依赖加 bcrypt

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 03:02:41 +08:00
parent 90c0d65a16
commit b50495bebe
46 changed files with 2621 additions and 68 deletions
+47 -12
View File
@@ -3,22 +3,57 @@
> 文件:`app/integrations/sms.py` | 关联接口:[auth-sms-send](../api/auth-sms-send.md) · [auth-sms-login](../api/auth-sms-login.md) | [← 集成索引](./README.md)
## 作用
手机号 + 验证码登录的验证码发送 / 校验。**当前是 mock 模式**(占坑),未接真实短信供应商
手机号 + 验证码登录的验证码发送 / 校验。**已接极光短信 REST**,由 `SMS_MOCK` 切 mock / real
| | mock(`SMS_MOCK=true`,默认 / 开发测试) | real(`SMS_MOCK=false`,生产) |
|---|---|---|
| 发送 | 不真发,验证码打日志 | 调极光 `/v1/messages` 真发 |
| 校验 | 放行任意 6 位数字(测试便利) | 比对本服务存的码(一次性 / 过期 / 防爆破) |
## 为什么是"自己生成码"模式
极光有两种验证码玩法:① `/v1/codes` 极光生成 + `/codes/{msg_id}/valid` 极光校验;② `/v1/messages` 本服务生成码、极光只负责发。**选 ②**:校验本地完成(不依赖极光二次往返)、有效期/错误次数/作废完全可控、跟原 mock 口子结构一致。
## 函数 / 异常
| 函数 | 行为 |
|---|---|
| `send_code(phone) -> int` | mock 下不真发、只 log;返回距下次可发的秒数。进程内存表 `_last_sent``phone → 发送时间``SMS_SEND_INTERVAL_SEC` 内再发抛 `SmsError`(发送过频)。真供应商未接时抛 `SmsError("sms provider not configured")` |
| `verify_code(phone, code) -> bool` | mock 下**任意 6 位数字**均通过(配合前台 demo);真模式未实现返回 `False` |
| 异常 `SmsError` | 发送过频 / 验证码不对,api 层 catch 后翻成 4xx(过频 `429` |
| `send_code(phone) -> int` | 防刷检查(冷却 + 每日上限)→ `secrets` 生成 N 位码 → **预占**(冷却/计数/存码)→ mock 打日志 / real 调极光 → 失败**回滚预占**。返回距下次可发秒数 |
| `verify_code(phone, code) -> bool` | mock 放行任意 6 位;real 比对存码,匹配即作废,失败累计到上限作废 |
| `SmsError(msg, status_code)` | `status_code` 决定 HTTP 码:过频/每日超限 **429**、供应商失败 **503**、号码无效 **400** |
## 配置
| 配置项 | 说明 |
|---|---|
| `SMS_MOCK` | 是否 mock(当前 `true` |
| `SMS_SEND_INTERVAL_SEC` | 同号两次发送最小间隔(防过频) |
| 配置项 | 默认 | 说明 |
|---|---|---|
| `SMS_MOCK` | `true` | mock / real 切换。**生产置 false** |
| `SMS_CODE_TTL_SEC` | 300 | 验证码有效期(秒),与极光模板文案"5分钟"一致 |
| `SMS_SEND_INTERVAL_SEC` | 60 | 单号两次发送最小间隔(冷却) |
| `SMS_SEND_ENDPOINT` | 极光 `/v1/messages` | 短信发送地址 |
| `SMS_SIGN_ID` | 31729 | 极光签名 ID |
| `SMS_TEMPLATE_ID` | 1 | 极光模板 ID(变量名 `code`) |
| `SMS_CODE_LENGTH` | 6 | 验证码位数(本服务生成;前端 code 4-8 位兼容) |
| `SMS_DAILY_LIMIT_PER_PHONE` | 10 | 单号每日发送上限(防刷 + 控费) |
| `SMS_MAX_VERIFY_ATTEMPTS` | 5 | 单码最多校验失败次数,超过作废(防爆破) |
## 后续(接真实供应商时)
- `send_code()` 改调供应商 API(阿里云 / 腾讯云),把生成的 6 位码存 cacheredis / sqlite)。
- `verify_code()` 比对 cache 里的码,且**验过即作废**。
- 进程内存方案占坑期够用;切真实供应商时一并切 redis(多进程/多机时内存表不共享,频控会失效)。
> **鉴权复用极光一键登录**:`/v1/messages` 用 `base64(JG_APP_KEY:JG_MASTER_SECRET)` 做 HTTP Basic Auth——短信与一键登录是**同一个极光应用**(同 AppKey)。**上线不需要额外凭证,只需 `SMS_MOCK=false`**(`JG_*` 一键登录已配)。
## 防刷(短信花钱 + `/sms/send` 在登录前无法 JWT 鉴权)
1. 单号 `SMS_SEND_INTERVAL_SEC` 冷却
2. 单号每日 `SMS_DAILY_LIMIT_PER_PHONE` 条上限
3. 单 IP 频控:`/sms/send``rate_limit(10,60)``/sms/login``rate_limit(20,60)`
4. 单码校验失败 `SMS_MAX_VERIFY_ATTEMPTS` 次即作废
5. 运维侧建议在极光控制台叠加:**IP 白名单**(只许服务器 IP)+ **防轰炸设置**
## 极光错误码(节选,映射在 `_send_via_jiguang`)
| code | 含义 | 处理 |
|---|---|---|
| 50014 | 余额不足 | `logger.critical` 告警 + 503 |
| 50009 | 极光侧超频 | 429 |
| 50006 | 手机号无效 | 400 |
| 其他 | — | 503 |
## 上线步骤
1. 极光控制台:企业实名 + 短信充值 + 签名审核(`sign_id=31729`)+ 模板审核(`temp_id=1`)——**均已就绪**
2. `.env``SMS_MOCK=false`(`JG_APP_KEY`/`JG_MASTER_SECRET` 一键登录已配)
3. 真机发一条验证:收到短信 + 能登录
## 已知局限
**验证码存进程内存**:单 worker uvicorn 够用;重启丢码(用户重发即可);**多 worker / 多机不共享 → 冷却 / 每日上限 / 校验失效**,扩 worker 前迁移到 DB/Redis。见 [待办与技术债](../待办与技术债.md)。