- 根目录加入 docker-compose、Caddyfile、文档等原本不在仓库里的文件 - 前后端两份 GitHub Actions 合并成根目录的一个 deploy workflow: 构建前端上传到 build/,再在服务器上 git pull 并重建容器 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
154 lines
5.2 KiB
Markdown
154 lines
5.2 KiB
Markdown
# 登录框重构:班级+姓名选择登录
|
||
|
||
**日期:** 2026-03-18
|
||
**状态:** 已批准
|
||
|
||
---
|
||
|
||
## 背景
|
||
|
||
当前登录框只有用户名+密码两个字段。用户(学生)的用户名格式为 `web{classname}{name}`(如 `web301张三`),输入完整用户名对学生不友好。需要重构为班级→姓名下拉选择方式登录,同时为管理员保留用户名+密码入口。
|
||
|
||
---
|
||
|
||
## 需求
|
||
|
||
- 学生通过"选班级 → 选姓名 → 输密码"方式登录
|
||
- 管理员通过原有用户名+密码方式登录
|
||
- 两种模式通过 Tab 切换
|
||
|
||
---
|
||
|
||
## 后端改动
|
||
|
||
### 1. User 模型新增字段
|
||
|
||
```python
|
||
# api/account/models.py
|
||
classname = models.CharField(max_length=50, blank=True, default="", verbose_name="班级")
|
||
```
|
||
|
||
执行数据库迁移。迁移后,**现有数据库中的所有用户(包括历史批量创建的用户)`classname` 均为空字符串**,因此不会出现在新端点的班级列表中。这是已知且可接受的行为——历史用户需重新通过新版批量创建接口创建,或手动更新 `classname`。
|
||
|
||
### 2. batch_create 接口保存 classname
|
||
|
||
在 `User(...)` 构造函数中设置 `classname`,在调用 `set_password()` 之前(`set_password` 内部调用 `self.save()`,必须保证字段在首次持久化时已赋值):
|
||
|
||
```python
|
||
user = User(username=username, classname=payload.classname)
|
||
user.set_password(password) # 触发 save(),此时 classname 已在实例上
|
||
users_to_create.append(user)
|
||
```
|
||
|
||
> 注:现有代码中 `set_password` 会触发 `save()`,之后的 `bulk_create(..., ignore_conflicts=True)` 对已存在行为空操作,无需改动该逻辑。
|
||
|
||
### 3. 新增两个无鉴权 GET 端点
|
||
|
||
> **安全说明:** 两个端点均不需要登录。`/names` 会返回某班级所有学生的用户名,存在账号枚举风险。考虑到平台面向内部学生,此为可接受的设计权衡。
|
||
|
||
**`GET /api/account/classes`**
|
||
|
||
返回所有不重复的非空班级名列表,按字典序升序排列。
|
||
|
||
```json
|
||
["301", "302", "高一1班"]
|
||
```
|
||
|
||
实现:
|
||
```python
|
||
User.objects.filter(classname__gt="").values_list("classname", flat=True).distinct().order_by("classname")
|
||
```
|
||
|
||
**`GET /api/account/names?classname=301`**
|
||
|
||
`classname` 为必填查询参数(缺省时 django-ninja 返回 422)。
|
||
|
||
返回该班级所有用户的姓名和用户名,按 `username` 升序排列(等价于按姓名升序,因 classname 前缀相同)。仅返回 `username` 以 `"web" + classname` 开头的用户,防止异常数据干扰。
|
||
|
||
```json
|
||
[
|
||
{"name": "张三", "username": "web301张三"},
|
||
{"name": "李四", "username": "web301李四"}
|
||
]
|
||
```
|
||
|
||
实现:
|
||
```python
|
||
prefix = "web" + classname
|
||
users = User.objects.filter(classname=classname, username__startswith=prefix).order_by("username")
|
||
return [{"name": u.username[len(prefix):], "username": u.username} for u in users]
|
||
```
|
||
|
||
如果 `classname` 不存在或该班无用户,返回空列表 `[]`。
|
||
|
||
---
|
||
|
||
## 前端改动
|
||
|
||
### Login.vue 重构
|
||
|
||
用 `n-tabs` 分为两个 Tab:
|
||
|
||
**Tab 1 - 学生登录**
|
||
|
||
| 字段 | 组件 | 说明 |
|
||
|------|------|------|
|
||
| 班级 | `n-select` | 挂载时调 `/classes` 加载,结果缓存在组件内,切换 Tab 不重新请求 |
|
||
| 姓名 | `n-select` | 选完班级后调 `/names?classname=X` 加载;班级未选时 disabled |
|
||
| 密码 | `n-input` type=password | 手动输入 |
|
||
|
||
**`n-select` 数据绑定:**
|
||
- 班级下拉:`label = classname, value = classname`
|
||
- 姓名下拉:`label = item.name, value = item.username`(即 `selectedUsername` 存储完整 username)
|
||
|
||
**登录按钮 disabled 条件:**
|
||
学生 Tab:班级、姓名、密码三者均已填写才可点击。
|
||
|
||
**级联状态管理:**
|
||
- 切换到其他 Tab 时,**清空**班级、姓名、密码的已选值(重置为初始状态)
|
||
- 重新选择班级时,**清空**已选姓名(`selectedUsername = ""`)并重新调 `/names` 加载
|
||
|
||
**API 错误处理:**
|
||
- `/classes` 加载失败:班级下拉显示为空,展示通用错误提示
|
||
- `/names` 加载失败:姓名下拉显示为空并保持 disabled,展示通用错误提示
|
||
|
||
登录时发送 `{ username: selectedUsername, password }` 到现有 `/api/account/login` 端点。
|
||
|
||
**Tab 2 - 管理员登录**
|
||
|
||
保持现有用户名+密码表单,无改动。管理员 Tab 登录按钮 disabled 条件:用户名和密码均非空。
|
||
|
||
### api.ts 新增
|
||
|
||
```typescript
|
||
Account.listClasses(): Promise<string[]>
|
||
Account.listNamesByClass(classname: string): Promise<{name: string, username: string}[]>
|
||
```
|
||
|
||
---
|
||
|
||
## 数据流
|
||
|
||
```
|
||
挂载 Login.vue
|
||
→ GET /api/account/classes → 缓存班级列表,填充班级下拉
|
||
|
||
用户选择班级
|
||
→ 清空 selectedUsername
|
||
→ GET /api/account/names?classname=X → 填充姓名下拉
|
||
(每个 option: label=name, value=username)
|
||
|
||
用户选姓名 → selectedUsername = item.username(完整 username)
|
||
用户输密码
|
||
→ POST /api/account/login { username: selectedUsername, password }
|
||
→ 登录成功 → 关闭 Modal,更新 user store
|
||
```
|
||
|
||
---
|
||
|
||
## 不在此次范围内
|
||
|
||
- 历史用户(非 batch 创建)的 classname 回填(现有用户 classname 为空,不出现在班级列表中)
|
||
- `UserListSchema` 和用户管理页不展示 classname 字段(可后续迭代)
|
||
- 学生忘记密码流程
|