Files
shanxia2.0/docs/superpowers/specs/2026-07-12-web-admin-change-password-design.md
T
2026-07-12 18:23:02 +08:00

234 lines
7.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# web-admin 顶栏用户菜单 — 修改密码设计
> **日期**: 2026-07-12
> **状态**: 待实施
> **范围**: `web-admin` 顶栏头像下拉 + `backend` 当前用户改密接口
> **UI 规范**: `docs/web-admin-ui-spec-fresh-mint.md`
> **参考实现**: 并列仓库 `zkxw`(`ArtUserMenu.vue` + `UserPasswordDialog.vue` + `/web/auth/change-password`)
## 背景与目标
运营人员在 `web-admin` 顶栏点击头像后,下拉菜单目前仅有电话/区域展示与「退出登录」,无法自助修改密码,只能联系管理员在用户管理页重置为默认密码 `111111`。
**目标**:在头像下拉菜单中,于「退出登录」按钮**上方**增加「修改密码」入口,用户填写原密码与新密码后完成自助改密。
## 已确认决策
| # | 决策 | 说明 |
|---|------|------|
| D1 | **交互形态** | 方案 A:菜单项 + 独立弹窗 `UserPasswordDialog`(对齐 zkxw) |
| D2 | **改密后会话** | **不强制重新登录**;改密成功后提示并关闭弹窗,当前 Token 会话继续有效 |
| D3 | **权限** | 仅需登录态(Bearer Token),不新增菜单权限码 |
| D4 | **密码规则** | 新密码最少 6 位;校验原密码 bcrypt;确认密码前端一致性校验 |
| D5 | **传输安全** | 走 HTTPS + 现有 API 加密中间件(生产环境);**不**单独对改密请求做 RSA(与登录区分,同 zkxw) |
| D6 | **范围** | 仅「修改密码」;本期不做「个人信息」编辑 |
## 非目标(本期不做)
- 忘记密码 / 邮件找回
- 密码复杂度策略(大小写+数字+特殊字符)
- 密码历史表 / 防重复使用旧密码
- 首次登录强制改密
- 改密后吊销当前会话 / 强制退出
- 小程序端改密
---
## 一、现状与缺口
### 前端
| 项 | 状态 |
|----|------|
| `ArtUserMenu.vue` 菜单入口 | ❌ 无「修改密码」 |
| `UserPasswordDialog.vue` | ❌ 不存在 |
| `fetchChangePassword()` | ❌ `api/auth.ts` 未定义 |
入口文件:`web-admin/src/components/core/layouts/art-header-bar/widget/ArtUserMenu.vue`
由 `art-header-bar/index.vue` 引用。
### 后端
| 项 | 状态 |
|----|------|
| `ChangePasswordRequest` Schema | ✅ `app/schemas/auth.py`(已有,未接线) |
| `change_password()` CRUD | ❌ 仅有 `reset_password()`(管理员重置) |
| `POST /web/auth/change-password` | ❌ 未实现 |
已有管理员重置:`POST /web/admin/users/{id}/reset-password` → 固定重置为 `111111`。
---
## 二、目标交互
### 菜单布局
```
┌─────────────────────┐
│ [头像] 张三 │
│ 运营管理员 │
├─────────────────────┤
│ 👤 138xxxx │
│ 📍 华东区(若有) │
├─────────────────────┤
│ 🔒 修改密码 ← 新增 │
├─────────────────────┤
│ [ 退出登录 ] │
└─────────────────────┘
```
### 流程
1. 用户悬停头像 → 点击「修改密码」
2. `ArtUserMenu` 关闭 Popover(沿用 `closeUserMenu()` + `setTimeout` 模式)
3. 打开 `UserPasswordDialog`(`v-model:visible`)
4. 用户填写:原密码、新密码、确认新密码
5. 前端校验通过后 `POST /web/auth/change-password`
6. 成功 → 全局成功提示、关闭弹窗、**不**调用 `logOut()`
7. 失败 → 展示后端错误(如「原密码不正确」)
---
## 三、前端设计
### 改动文件
| 文件 | 操作 |
|------|------|
| `web-admin/src/components/core/layouts/art-header-bar/widget/ArtUserMenu.vue` | 修改:增加菜单项 + 引入弹窗 |
| `web-admin/src/components/core/layouts/art-header-bar/widget/UserPasswordDialog.vue` | **新建**:改密弹窗 |
| `web-admin/src/api/auth.ts` | 修改:新增 `fetchChangePassword()` |
| `web-admin/src/locales/langs/zh.json` | 可选:`topBar.user.changePassword` |
| `web-admin/src/locales/langs/en.json` | 可选:同上英文 |
### `ArtUserMenu.vue` 变更要点
- 在分隔线与「退出登录」之间新增 `<li class="btn-item" @click="openPasswordDialog">`
- 图标:`ArtSvgIcon icon="ri:lock-password-line"`
- 文案:`修改密码`(或 `$t('topBar.user.changePassword')`)
- 根节点包裹 `<UserPasswordDialog v-model:visible="passwordDialogVisible" />`
- `openPasswordDialog()` 模式对齐 zkxw:`closeUserMenu()` → `setTimeout 150ms` → `passwordDialogVisible = true`
### `UserPasswordDialog.vue` 规范
对齐 Fresh Mint 弹窗约定(`web-admin-crud-dialog.mdc`):
- `el-dialog`:`class="dialog-form-layout"`、`align-center`、`append-to-body`、`modal-class="protocol-dialog-mask"`、`destroy-on-close`、`:close-on-click-modal="false"`
- `el-form`:`label-position="top"`、`scroll-to-error`
- 字段(均 `form-col-span-2`):
- 原密码:`type="password"`、`show-password`、`autocomplete="current-password"`
- 新密码:`min 6` 校验、`autocomplete="new-password"`
- 确认新密码:自定义 validator 与 `newPassword` 一致
- Footer:取消(`Close` 图标)、确定(`Check` 图标 + `:loading="submitting"`)
- 打开时 `resetForm()` 清空字段与校验态
### API
```typescript
/** 当前用户修改密码 */
export function fetchChangePassword(data: { oldPassword: string; newPassword: string }) {
return request.post({
url: '/web/auth/change-password',
data,
showSuccessMessage: true
})
}
```
---
## 四、后端设计
### 改动文件
| 文件 | 操作 |
|------|------|
| `backend/app/crud/user.py` | 新增 `change_password()` |
| `backend/app/api/public.py` | 新增 `POST /web/auth/change-password` |
### `change_password()` 逻辑
```python
def change_password(db: Session, user_id: str, old_password: str, new_password: str) -> None:
obj = get_user(db, user_id)
if not obj:
raise ValueError("用户不存在")
if not verify_password(old_password, obj.password or ""):
raise ValueError("原密码不正确")
new_pw = (new_password or "").strip()
if len(new_pw) < 6:
raise ValueError("新密码长度不能少于6位")
if old_password == new_pw:
raise ValueError("新密码不能与原密码相同")
obj.password = _hash_password(new_pw)
obj.update_time = datetime.now()
db.add(obj)
db.commit()
```
### 路由
```
POST /web/auth/change-password
Authorization: Bearer <access_token>
Content-Type: application/json
Request body(驼峰,与前端一致):
{
"oldPassword": "string",
"newPassword": "string"
}
Response 200:
{ "code": 200, "data": { "message": "密码修改成功" } }
Response 400:
- "请输入原密码"
- "新密码长度不能少于6位"
- "原密码不正确"
- "新密码不能与原密码相同"
Response 401:
- "Token无效或已过期" / "未授权"
```
鉴权:从 `Authorization` Bearer Token 解析当前用户 `user_id`(复用 `get_current_user_by_token` 或 `public.py` 现有模式)。
---
## 五、校验规则汇总
| 规则 | 前端 | 后端 |
|------|:----:|:----:|
| 原密码必填 | ✅ | ✅ |
| 新密码 ≥ 6 位 | ✅ | ✅ |
| 确认密码一致 | ✅ | — |
| 原密码 bcrypt 校验 | — | ✅ |
| 新密码 ≠ 原密码 | 建议 | ✅ |
---
## 六、验收清单
- [ ] 头像下拉在「退出登录」上方显示「修改密码」
- [ ] 点击后 Popover 关闭,弹窗居中打开
- [ ] 原密码错误 → 「原密码不正确」
- [ ] 新密码 < 6 位 → 前端拦截
- [ ] 两次新密码不一致 → 前端拦截
- [ ] 改密成功 → 成功提示,弹窗关闭,**不**退出登录
- [ ] 登出后用新密码可重新登录
- [ ] 弹窗符合 Fresh Mint(`dialog-form-layout`、无 backdrop blur、按钮带图标)
---
## 七、工作量
| 模块 | 预估 |
|------|------|
| 前端 | ~1h |
| 后端 | ~0.5h |
| 联调 | ~0.5h |
| **合计** | **~2h** |
无数据库迁移;约 6 个文件改动。