# feat(user): SMS 登录限定制师 + active role 自动切 CUSTOMIZER

> **仓库**: HL (后端 hl-user-service)
> **关联 PR/Issue**: PR #2920, Closes #2918
> **日期**: 2026-05-22
> **影响范围**: 管理端 `POST /admin/auth/sms/login` SMS 验证码登录入口
> **接收方**: mmg (前端)
> **前端**: **登录页前端无 API 改动**, 但 SMS 登录成功后 `data.role` 一定是 CUSTOMIZER (即使该 admin 还有其他角色)

---

## 🚨 关键变化

SMS 验证码登录入口 (`POST /admin/auth/sms/login`) **现在只允许有 `CUSTOMIZER` 角色的 admin 登录**, 且登录成功后强制以**定制师视角**颁发 token (即使 admin 还有 ADMIN/OPERATOR 等其他角色)。

业务原因: SMS 登录场景**仅定制师小程序分享**需要 (#2907), 其他角色不应该用 SMS 绕过 2FA 登入后台。

---

## 一、API 影响

### `POST /admin/auth/sms/login` 行为变化

| 场景 | 之前 | **现在** |
|---|---|---|
| 仅 CUSTOMIZER 角色 admin 用对码登录 | 颁发 token, `role` = `admin.currentRoleId` 对应的 roleKey | 颁发 token, **`role` 强制 = `CUSTOMIZER`** |
| 多角色 admin (如 CUSTOMIZER+ADMIN) 用对码登录 | 颁发 token, `role` = `admin.currentRoleId` 对应的 roleKey | 颁发 token, **`role` 强制 = `CUSTOMIZER`** (无视 currentRoleId) |
| **无 CUSTOMIZER 角色 admin** 用对码登录 | 颁发 token, `role` = 当前 active role | **返 200102 INVALID `验证码错误,剩余 N 次尝试`** + fail+1 (**防枚举**) |
| mobile 不绑 admin 用对码登录 | 200102 INVALID (现有防枚举路径) | 200102 INVALID (无变化) |

### 防枚举设计

"无 CUSTOMIZER 角色 admin" 路径返回的 code/message 跟 "mobile 不绑 admin" 路径**完全一致** (200102 INVALID + remainingAttempts),攻击者无法区分这两种情况, 也无法用 SMS 入口枚举哪些 mobile 是非定制师 admin。

fail count 也会累加, 错 5 次仍触发 `ADMIN_SMS_CODE_VERIFY_LOCKED` 15 分钟锁定 (复用 #2889 锁定机制)。

### active role 强制 CUSTOMIZER 后

- 颁发的 access token 内嵌的 `role` 字段 = `CUSTOMIZER`
- 后续基于 token 的 `@PreAuthorize("hasRole('CUSTOMIZER')")` 等权限校验都按定制师视角
- **不污染 `admin.currentRoleId` 持久化**: SMS 是临时切角色, 不影响下次密码登录时的默认角色

---

## 🚨 前端需要做的事

### 1. 登录后角色显示

SMS 登录成功后 `data.role` 一定是 `CUSTOMIZER`, 顶部用户信息组件 / 角色 badge 应该按定制师展示。

不需要前端额外区分 SMS 登录 vs 账密登录: 后端已经把 role 强制设为 CUSTOMIZER, 前端按响应字段渲染即可。

### 2. 多角色切换

如果未来前端有"切换角色"功能 (`PUT /admin/auth/role`), SMS 登录拿到的 CUSTOMIZER token, 切到其他角色行为等同于密码登录路径 (后端按现有逻辑处理)。

### 3. 错码无新增

`200102` 错码已经存在, 前端无需新增映射。**`200107` 是后端审计码不会返前端**, 前端忽略。

### 4. 自查

```bash
grep -rE "/admin/auth/sms/login|role.*CUSTOMIZER" hl-ui/src
```

确认登录后 role 字段处理逻辑能正确显示 CUSTOMIZER。

---

## 二、新错码 (后端审计用, 不返前端)

| code | 含义 |
|---|---|
| 200107 | `ADMIN_SMS_LOGIN_NOT_CUSTOMIZER` 短信验证码登录仅限定制师 (仅服务端 log.warn 审计, 防枚举不返前端) |

---

## 三、测试服验证记录

部署到测试服 `api.test.1814.love:9443` 后真测 (2026-05-22 19:05):

| # | 场景 | 实测 |
|---|------|------|
| T1 | 仅 CUSTOMIZER admin 登录 | ✅ `role=CUSTOMIZER` + `roleName=定制师` + token + `requireTwoFa=False` |
| T2 | 仅 ADMIN admin (无 CUSTOMIZER) 登录 | ✅ 200102 INVALID + `remainingAttempts=4` (防枚举) |
| T3 | 多角色 admin (ADMIN+CUSTOMIZER, currentRoleId=ADMIN) | ✅ 强制返 `role=CUSTOMIZER`, DB `current_role_id=2` 保持不污染 |

---

## 四、不在本期范围

- 账号密码登录路径 `/admin/auth/login` 行为完全不动 (仍按 `admin.currentRoleId`)
- 让非定制师能用 SMS 登录 (明确仅定制师)
- 持久化改 admin.currentRoleId (SMS 是临时切角色)

---

## 五、联系人

后端: wx (呼籁旅行)
前端: mmg

如有疑问可在 [#2918](https://git.1814.love:8443/wx/HL/issues/2918) 评论区留言。
