名单改为用户名、密码两列;去掉姓名前的班级代号,学生用姓名登录;别班已有同名学生时报错,不再转班
CI/CD / test (push) Canceled after 0s
CI/CD / deploy (push) Canceled after 0s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-26 21:49:50 +08:00
1 parent 61a32e900d
commit e4c53f5e55
11 files changed
+91 -47

No files matched your search

+1 -1
View File
@@ -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))
})
+15 -8
View File
@@ -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<string>()
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 {
+14 -5
View File
@@ -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' }])