feat: C 端 1-1 邮箱+密码注册(免码不阻塞,带风控)

为什么:客户端要做的第一件事是账号,但现状下邮箱注册走不通 —— RegisterReqVO.code 是 @NotBlank,
而 OTP 通道现在是 LogOtpSender(只写日志、发不出去),等于注册入口堵死。本轮把"邮箱+密码"打通,
同时按口径补上免码必须带的风控。

- RegisterReqVO.code 放开非必填(PHONE 仍必填,必填性按 targetType 在服务端判断)
- 邮箱注册分支:格式校验(AppIdentifier.requireEmailFormat)+ 密码强度(8-64 位且含字母数字);
  传了 code 就顺手校验一次,邮箱验证能力保留但不阻塞
- 注册频控:同 IP 24h ≤5、同设备 24h ≤3(数 app_user.register_ip / register_device_id,不新建计数表),超出 429
- 顺带修一个真问题:login() 原来**无条件**把 credential 标成 verified=1,
  密码登录并不证明邮箱归用户所有,会把"未验证"状态悄悄抹掉;改成只有验证码登录才置 1
- docs/api-phase1.md 补注册契约表格与残留风险(IP/设备号可伪造、OTP 只写日志、邮箱可达性未验证)

实测:免码注册成功(邮箱大小写与空格被规范化);弱密码/坏邮箱 400;重复 200001;
邮箱+密码登录成功;PHONE 不带码仍报"请先获取验证码";同设备第 4 次、同 IP 第 6 次分别 429;
免码注册后 verified=0 且经一次密码登录仍是 0。
This commit is contained in:
Vicky committed 2026-10-15 17:20:31 +08:00
1 parent 414d9fd482
commit 6833e01e1f
4 files changed
+129 -6

No files matched your search

+24
View File
@@ -74,6 +74,30 @@ POST /app/auth/register
}
```
**邮箱 + 密码注册(C 端第一批口径)**
```json
POST /app/auth/register
{
"targetType": "EMAIL", "target": "User@Example.com ", "password": "nonton2026",
"deviceId": "d-1", "countryCode": "ID", "agreementVersion": "2026-10-01"
}
```
| 规则 | 口径 |
| --- | --- |
| `targetType=EMAIL` | **`code` 可以不传**(邮箱+密码直接注册);传了就走一次验证码校验(邮箱验证能力保留) |
| `targetType=PHONE` | 维持原状:`code` **必填** |
| 邮箱规范化 | 去空格 + 转小写(`batch1@Example.com ` → `batch1@example.com`);格式不合法直接 `400 邮箱格式不正确` |
| 密码强度 | 8–64 位,且**同时包含字母与数字**(不强制特殊字符:低端机输入成本高,拦机器注册靠频控) |
| 注册频控 | 同 IP 24h ≤ **5** 个、同设备 24h ≤ **3** 个,超出返回 `429`(`TOO_MANY_REQUESTS`) |
| 邮箱验证状态 | 免码注册时 `app_user_credential.verified = 0`(保留"待验证"),**不阻塞登录**;只有**验证码登录**会把 `verified` 置 1,密码登录不改变该状态 |
频控口径落库在 `app_user.register_ip` / `register_device_id` 上(按 24h 计数,不新建计数表)。
**残留风险**:IP 取自 `X-Forwarded-For`、设备号由客户端上报,都能伪造,只能挡脚本批量;
真要防住需要接图形/行为验证码或风控服务,本阶段未做。验证码通道目前是 `LogOtpSender`(只写日志不发信),
所以邮箱**可达性**未验证 —— 这也是"不阻塞注册"的前提。
响应(登录、刷新同结构):
```json