- 根目录加入 docker-compose、Caddyfile、文档等原本不在仓库里的文件 - 前后端两份 GitHub Actions 合并成根目录的一个 deploy workflow: 构建前端上传到 build/,再在服务器上 git pull 并重建容器 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
5.2 KiB
登录框重构:班级+姓名选择登录
日期: 2026-03-18 状态: 已批准
背景
当前登录框只有用户名+密码两个字段。用户(学生)的用户名格式为 web{classname}{name}(如 web301张三),输入完整用户名对学生不友好。需要重构为班级→姓名下拉选择方式登录,同时为管理员保留用户名+密码入口。
需求
- 学生通过"选班级 → 选姓名 → 输密码"方式登录
- 管理员通过原有用户名+密码方式登录
- 两种模式通过 Tab 切换
后端改动
1. User 模型新增字段
# 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(),必须保证字段在首次持久化时已赋值):
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
返回所有不重复的非空班级名列表,按字典序升序排列。
["301", "302", "高一1班"]
实现:
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 开头的用户,防止异常数据干扰。
[
{"name": "张三", "username": "web301张三"},
{"name": "李四", "username": "web301李四"}
]
实现:
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 新增
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 字段(可后续迭代)- 学生忘记密码流程