From e4c53f5e55d1b73307b69c7344e9638b9c0515df Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Mon, 26 Oct 2026 21:49:50 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8D=E5=8D=95=E6=94=B9=E4=B8=BA=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E5=90=8D=E3=80=81=E5=AF=86=E7=A0=81=E4=B8=A4=E5=88=97?= =?UTF-8?q?=EF=BC=9B=E5=8E=BB=E6=8E=89=E5=A7=93=E5=90=8D=E5=89=8D=E7=9A=84?= =?UTF-8?q?=E7=8F=AD=E7=BA=A7=E4=BB=A3=E5=8F=B7=EF=BC=8C=E5=AD=A6=E7=94=9F?= =?UTF-8?q?=E7=94=A8=E5=A7=93=E5=90=8D=E7=99=BB=E5=BD=95=EF=BC=9B=E5=88=AB?= =?UTF-8?q?=E7=8F=AD=E5=B7=B2=E6=9C=89=E5=90=8C=E5=90=8D=E5=AD=A6=E7=94=9F?= =?UTF-8?q?=E6=97=B6=E6=8A=A5=E9=94=99=EF=BC=8C=E4=B8=8D=E5=86=8D=E8=BD=AC?= =?UTF-8?q?=E7=8F=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- server/src/app.ts | 2 +- server/src/classes.ts | 23 ++++++++++++++-------- server/test/api.test.ts | 19 +++++++++++++----- web/src/pages/HelpPage.vue | 6 +++--- web/src/pages/LoginPage.vue | 4 ++-- web/src/pages/ReviewPage.vue | 4 +--- web/src/pages/TeacherPage.vue | 17 +++++++--------- web/src/teacher/GradebookPanel.vue | 6 ++---- web/src/teacher/csv.ts | 2 +- web/src/teacher/roster.test.ts | 31 ++++++++++++++++++++++++++---- web/src/teacher/roster.ts | 24 +++++++++++++++++------ 11 files changed, 91 insertions(+), 47 deletions(-) diff --git a/server/src/app.ts b/server/src/app.ts index 0975931..2c8a1da 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -54,7 +54,7 @@ export function createApp(db: Database, gradingQueue: GradingQueue, aiQueue: AiQ const blocked = tooManyFailures(ip, String(username ?? '')) if (blocked) return c.json({ message: blocked }, 429) const tokens = await login(db, String(username ?? ''), String(password ?? ''), ip) - if (!tokens) return c.json({ message: '账号或密码不对' }, 401) + if (!tokens) return c.json({ message: '用户名或密码不对' }, 401) return c.json(await withUser(tokens)) }) diff --git a/server/src/classes.ts b/server/src/classes.ts index 17ea55b..4ae2b19 100644 --- a/server/src/classes.ts +++ b/server/src/classes.ts @@ -72,15 +72,18 @@ export function listStudents(db: Database, classId: number): Student[] { } /** - * 导入名单到一个班:账号不存在就新建,已存在就更新姓名、班级(以及填了的密码)。 + * 导入名单到一个班:用户名不存在就新建,已是本班学生就更新姓名(以及填了的密码)。 + * 用户名全站唯一(学生只用姓名登录),已是别的班的学生时报错,不把他转到本班。 * 密码留空时:新学生生成 6 位数字密码,老学生保持原密码。 */ export function importStudents(db: Database, classId: number, rows: ImportRow[]): ImportResult { const result: ImportResult = { created: 0, updated: 0, errors: [] } const seen = new Set() - const find = db.query<{ id: number; role: string }, [string]>('SELECT id, role FROM users WHERE username = ?') + const find = db.query<{ id: number; role: string; classId: number | null; className: string | null }, [string]>( + 'SELECT u.id, u.role, u.class_id AS classId, c.name AS className FROM users u LEFT JOIN classes c ON c.id = u.class_id WHERE u.username = ?', + ) const insert = db.query(`INSERT INTO users (username, name, password, role, class_id, created_at) VALUES (?, ?, ?, 'student', ?, ?)`) - const update = db.query('UPDATE users SET name = ?, class_id = ?, password = COALESCE(?, password) WHERE id = ?') + const update = db.query('UPDATE users SET name = ?, password = COALESCE(?, password) WHERE id = ?') db.transaction(() => { for (const [index, row] of rows.entries()) { @@ -89,19 +92,19 @@ export function importStudents(db: Database, classId: number, rows: ImportRow[]) const name = row.name.trim() const password = row.password?.trim() || undefined if (!username) { - fail('账号为空') + fail('用户名为空') continue } if (/\s/.test(username)) { - fail(`账号"${username}"里有空格`) + fail(`用户名"${username}"里有空格`) continue } if (!name) { - fail(`账号"${username}"的姓名为空`) + fail(`用户名"${username}"的姓名为空`) continue } if (seen.has(username)) { - fail(`账号"${username}"在名单里重复`) + fail(`用户名"${username}"在名单里重复`) continue } seen.add(username) @@ -111,8 +114,12 @@ export function importStudents(db: Database, classId: number, rows: ImportRow[]) fail(`"${username}"是老师账号`) continue } + if (existing && existing.classId !== classId) { + fail(`"${username}"已经是${existing.className ?? '别的班'}的学生:同名时在用户名后面加数字区分,如"${username}2"`) + continue + } if (existing) { - update.run(name, classId, password ?? null, existing.id) + update.run(name, password ?? null, existing.id) if (password) revokeTokens(db, existing.id) result.updated++ } else { diff --git a/server/test/api.test.ts b/server/test/api.test.ts index a4e528c..214f544 100644 --- a/server/test/api.test.ts +++ b/server/test/api.test.ts @@ -77,11 +77,11 @@ test('没登录时接口返回 401', async () => { expect((await browser.get('/api/works/sample:x')).status).toBe(401) }) -test('账号或密码不对时登录失败,老师登录后能看到自己的信息', async () => { +test('用户名或密码不对时登录失败,老师登录后能看到自己的信息', async () => { const browser = new Browser() const wrong = await browser.login('admin', 'wrong') expect(wrong.status).toBe(401) - expect(wrong.body.message).toBe('账号或密码不对') + expect(wrong.body.message).toBe('用户名或密码不对') const t = await teacher() const me = await t.get('/api/me') @@ -144,9 +144,9 @@ describe('班级和名单', () => { created: 2, updated: 0, errors: [ - { row: 3, message: '账号"2024001"在名单里重复' }, - { row: 4, message: '账号"2024003"的姓名为空' }, - { row: 5, message: '账号"2024 004"里有空格' }, + { row: 3, message: '用户名"2024001"在名单里重复' }, + { row: 4, message: '用户名"2024003"的姓名为空' }, + { row: 5, message: '用户名"2024 004"里有空格' }, { row: 6, message: '"admin"是老师账号' }, ], }) @@ -167,6 +167,15 @@ describe('班级和名单', () => { expect((await t.get(`/api/teacher/classes/${classId}/students`)).body[0]).toMatchObject({ name: '张叁', password: 'abc123' }) }) + test('用户名已是别的班的学生时报错,不转到本班', async () => { + const t = await teacher() + const first = await classWithStudents(t, [{ username: '王超', name: '王超', password: 'abc123' }]) + const { body: other } = await t.post('/api/teacher/classes', { name: '物联网 2402' }) + const result = await t.post(`/api/teacher/classes/${other.id}/import`, { rows: [{ username: '王超', name: '王超', password: '999999' }] }) + expect(result.body).toEqual({ created: 0, updated: 0, errors: [{ row: 1, message: expect.stringContaining('已经是物联网 2401的学生') }] }) + expect((await t.get(`/api/teacher/classes/${first}/students`)).body[0]).toMatchObject({ username: '王超', password: 'abc123' }) + }) + test('学生用老师发的密码登录,不能使用老师接口', async () => { const t = await teacher() await classWithStudents(t, [{ username: '2024001', name: '张三', password: 'abc123' }]) diff --git a/web/src/pages/HelpPage.vue b/web/src/pages/HelpPage.vue index 8993eaf..1712d13 100644 --- a/web/src/pages/HelpPage.vue +++ b/web/src/pages/HelpPage.vue @@ -24,7 +24,7 @@ import { currentUser } from '../auth'

学生

1. 登录

-

用老师发给你的账号(学号)和密码登录。请用新版的 Chrome 或 Edge 浏览器;用 360、QQ 浏览器时,点地址栏右边的闪电图标切换到极速模式。

+

用老师发给你的用户名和密码登录。请用新版的 Chrome 或 Edge 浏览器;用 360、QQ 浏览器时,点地址栏右边的闪电图标切换到极速模式。

2. 自由练习和做作业