# 用户端短信登录增加图形验证码

日期：2026-07-09

## 需求背景

用户端当前发送短信验证码前只校验手机号格式和学员名单。为了减少短信接口被刷、验证码被滥用，需要在发送短信验证码前增加图形验证码。当前业务规则仍然保持：只有后台学员名单中存在且启用的手机号才允许发送短信验证码和登录。

## 本次目标

1. 用户端登录页增加图形验证码输入。
2. 发送短信验证码前必须先通过图形验证码校验。
3. 发送短信验证码前继续校验手机号是否存在于后台学员名单。
4. 手机号不存在时，前端弹出/展示明确错误，用户能直接看到。
5. 图形验证码部署后显示效果要和开发环境尽量一致。

## 字体与部署决策

- 后端生成 PNG 图形验证码，不依赖浏览器字体渲染。
- 使用 Pillow 生成图片。
- Docker 镜像安装 `fonts-dejavu-core`，避免容器环境缺少字体后退回默认小字体。
- 后端字体加载采用多路径 fallback，优先使用 `DejaVuSans-Bold.ttf`。
- 文本居中使用 `textbbox` 计算，避免不同字体下位置偏移。
- 如果字体确实无法加载，会回退到 Pillow 默认字体，并记录日志；但生产镜像应通过 Dockerfile 保证 DejaVu 字体可用。

## 接口设计

### 获取图形验证码

`GET /api/auth/captcha`

返回：

```json
{
  "captchaId": "uuid",
  "imageBase64": "data:image/png;base64,...",
  "expiresInSeconds": 300
}
```

### 发送短信验证码

`POST /api/auth/sms/send`

请求体新增：

```json
{
  "phone": "13800000000",
  "captchaId": "uuid",
  "captchaCode": "1234"
}
```

后端校验顺序：

1. 校验图形验证码。
2. 校验手机号是否存在于后台学员名单，且账号状态允许登录。
3. 发送短信验证码。

## 验收标准

- 登录页打开时自动加载图形验证码。
- 点击图形验证码可刷新。
- 图形验证码为空或错误时，不能发送短信验证码。
- 手机号不存在时，发送短信验证码接口返回“手机号不在学员名单中，请联系管理员”，前端可见。
- 图形验证码通过且手机号存在时，mock 短信仍可正常发送。
- Docker 环境中图形验证码字体清晰，不能退化成过小或错位的默认字体。

## 2026-07-09 补充：验证码状态迁移到 Redis

### 变更原因

图形验证码和短信验证码最初沿用进程内字典保存状态。单 worker 本地环境可以正常使用，但生产环境如果开启多个 backend worker 或多实例部署，就可能出现验证码由 A worker 生成、校验请求打到 B worker 后找不到验证码的问题。

### 处理决策

1. 图形验证码状态优先写入 Redis，key 为 `auth:captcha:{captcha_id}`，TTL 为 300 秒。
2. 短信验证码状态优先写入 Redis，key 为 `auth:sms_code:{phone}`，TTL 使用系统配置中的短信验证码有效期。
3. Redis 可用时，所有 worker 共享同一份验证码状态。
4. Redis 未配置或临时不可用时，保留原进程内字典作为降级路径，避免本地开发环境完全不可用。
5. 验证成功后删除对应 Redis key，保持验证码一次性使用。
6. mock 短信开关开启时，仍保留“未发送但输入固定 mock 验证码可登录”的开发便利能力。

### 新增验收标准

- Docker dev 环境下，图形验证码和短信验证码应写入 Redis，并可通过 Redis key 观察到 TTL。
- 发送短信验证码成功后，Redis 中应存在 `auth:sms_code:{phone}`。
- 图形验证码生成后，Redis 中应存在 `auth:captcha:{captcha_id}`。
- 验证码校验成功后，对应 Redis key 应被删除。
- Redis 不可用时，单 worker 本地开发仍可使用内存降级路径。
