Files
web/docs/superpowers/specs/2026-03-18-login-class-select-design.md
T
xuyueandClaude Opus 5.5 47d4679984 chore: 合并为单仓库,统一部署流程
- 根目录加入 docker-compose、Caddyfile、文档等原本不在仓库里的文件
- 前后端两份 GitHub Actions 合并成根目录的一个 deploy workflow:
  构建前端上传到 build/,再在服务器上 git pull 并重建容器

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 16:02:12 +08:00

5.2 KiB
Raw Blame History

登录框重构:班级+姓名选择登录

日期: 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 字段(可后续迭代)
  • 学生忘记密码流程