feat(class-account): 学生端主体改为班级(班级码 + 班级密码)+ 数据按班级隔离

服务端:
- Class 新增 code(唯一)/passwordHash 与迁移(老数据回填 C00001 形式, 未设密码禁止登录)
- POST /auth/class-login: 班级码 + 班级密码 -> classSession 会话(sub=classId)
- PATCH /classes/:id/class-account: 管理员改班级码/重置密码(唯一性、格式校验)
- auth 中间件/socket 握手支持班级会话(sub 为 classId, 回查 Class 表, 加入 user:{classId} 房间)
- lib/session.ts: 班级设备代全班操作(通知已读/作业完成/成绩总览按全班读写), 普通学生账号行为不变
- 叫人消息同时广播到 user:{classId}, 班级设备立即上岛
- 班级列表/详情按权限返回班级码与密码状态(哈希不外泄)

客户端:
- 登录页改为班级码 + 班级密码(个人学生入口移除, /auth/login 接口保留兼容)
- 作业按钮/成绩标题按班级会话显示(全班标记完成 / 本班成绩)
- 冒烟脚本自动准备班级账号凭据(兼容全新与升级安装)

Web 管理端:
- 班级管理新增「班级账号」列与设置弹窗(改班级码/重置密码/随机生成)
- 新建班级可选自定义班级码, 默认由服务端生成

验证: verify:e2e 125/125(新增班级账号 18 项) verify:web 18/18 verify:desktop 40/40
      (login=高一(1)班 classSession=true)
文档: README 新增「学生端班级账号」章节、API/迁移/验收/默认账号更新; 安装包 README 同步
This commit is contained in:
LoveChengke committed 2026-09-12 15:51:08 +08:00
1 parent 8b85079d81
commit 1073436ad6
38 files changed
+1289 -157

No files matched your search

@@ -14,7 +14,7 @@ const auth = useAuthStore();
const realtime = useRealtimeStore();
const formRef = ref<FormInstance>();
const form = reactive({ serverUrl: DEFAULT_SERVER, username: '', password: '' });
const form = reactive({ serverUrl: DEFAULT_SERVER, code: '', password: '' });
const testing = ref(false);
const testResult = ref<'ok' | 'fail' | null>(null);
@@ -29,7 +29,7 @@ const rules: FormRules = {
trigger: 'blur',
},
],
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
code: [{ required: true, message: '请输入班级码', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
};
@@ -38,7 +38,7 @@ onMounted(async () => {
if (window.desktop) {
const config = await window.desktop.getConfig();
if (config.serverUrl) form.serverUrl = config.serverUrl;
if (config.username) form.username = config.username;
if (config.username) form.code = config.username;
}
});
@@ -66,10 +66,10 @@ async function submit(): Promise<void> {
if (!valid) return;
try {
await auth.login(form.serverUrl, form.username.trim(), form.password);
await auth.login(form.serverUrl, form.code.trim(), form.password);
await appStore.init(form.serverUrl);
if (auth.token) realtime.connect(appStore.serverUrl, auth.token);
ElMessage.success(`欢迎回来,${auth.displayName}`);
ElMessage.success(`已进入 ${auth.displayName}`);
await router.replace('/schedule');
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '登录失败');
@@ -92,7 +92,7 @@ async function enterOffline(): Promise<void> {
<el-icon :size="30" color="#409eff"><School /></el-icon>
<div>
<h1 class="login-title">班级小助手</h1>
<p class="login-sub">学生客户端 · 登录后实时接收课表、作业、通知与成绩</p>
<p class="login-sub">班级客户端 · 以班级账号登录,实时接收课表、作业、通知与成绩</p>
</div>
</div>
@@ -105,19 +105,24 @@ async function enterOffline(): Promise<void> {
</el-input>
</el-form-item>
<el-form-item label="用户名" prop="username">
<el-input v-model="form.username" placeholder="学号 / 用户名" clearable @keyup.enter="submit">
<el-form-item label="班级码" prop="code">
<el-input
v-model="form.code"
placeholder="例如 G101(由老师在班级管理中提供)"
clearable
@keyup.enter="submit"
>
<template #prefix>
<el-icon><User /></el-icon>
<el-icon><School /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-form-item label="班级密码" prop="password">
<el-input
v-model="form.password"
type="password"
placeholder="密码"
placeholder="班级密码"
show-password
@keyup.enter="submit"
>
@@ -156,9 +161,9 @@ async function enterOffline(): Promise<void> {
</el-button>
<p class="login-tip">
演示账号:student01 / student123(高一(1)班)
班级码与班级密码由管理员在 Web 管理端「班级管理 → 班级账号」中设置或重置。
<br />
数据来源:教师或管理员在 Web 管理端发布的内容。
登录后本机即代表整个班级:作业完成、通知已读都会按全班记录。
</p>
</div>
</div>