feat(students,teachers,classes,auth,web): 学生个人账号彻底清理,学生只是名单记录
学生不再有登录能力:User.passwordHash 以空串占位,role=STUDENT 在密码校验之前 一律 403 并提示改用班级登录(否则「密码错 401 / 密码对但已停用 403」的差异会把 学生账号密码变成可离线验证的预言机)。成绩、未交名单、叫人、已读全部按名单记。 配套改动: - 删除 POST /students/:id/reset-password(调用返回 404,e2e 有负断言); - 新建学生不设密码,StudentsView 移除密码输入与「重置密码」入口, 列表标识仍自动生成为学号式(student01…); - class-account.ts / auth.service.ts 收紧班级会话语义; - 经管「修改密码」入口收敛到 Web 端:教师改本人密码走顶栏下拉, 班级密码由管理员在班级管理里维护,教师可用 POST /teachers/:id/reset-password 设初始密码。
This commit is contained in:
1 parent
7ea0e0f7c9
commit
d27f9a4b9e
11 files changed
+121
-104
No files matched your search
@@ -3,6 +3,7 @@ import { computed, onMounted, onUnmounted, reactive, ref } from 'vue';
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';
|
||||
import {
|
||||
SOCKET_EVENTS,
|
||||
MAX_TERM_WEEK,
|
||||
formatDate,
|
||||
type ClassDetailDto,
|
||||
type ClassDto,
|
||||
@@ -76,6 +77,11 @@ function openEdit(row: ClassDto): void {
|
||||
form.grade = row.grade;
|
||||
form.code = '';
|
||||
form.termWeeks = row.termWeeks ?? 20;
|
||||
// 必须回填当前班主任:编辑弹窗里的下拉与"新建"共用同一个 form.headTeacherId,
|
||||
// 而 submitForm 会在"值不同"时直接调 assignHeadTeacher。不回填的话,只要之前打开过一次
|
||||
// 「新建班级」(openCreate 会把它设成当前管理员自己),之后保存任何一次编辑都会把
|
||||
// 该班班主任静默改成管理员本人 —— 而班主任决定谁能管这个班的课表与成绩。
|
||||
form.headTeacherId = row.teacher?.id ?? '';
|
||||
void loadStaffOptions();
|
||||
formVisible.value = true;
|
||||
}
|
||||
@@ -102,6 +108,9 @@ async function submitForm(): Promise<void> {
|
||||
await classApi.create({
|
||||
name: form.name,
|
||||
grade: form.grade,
|
||||
// 学期周数:新建时也要提交。原先只提交名称/年级/班级码,表单里填的周数被直接丢弃,
|
||||
// 服务端落库恒为默认 20,用户看到的是"设置了没生效"且没有任何提示。
|
||||
termWeeks: form.termWeeks,
|
||||
...(form.code.trim() ? { code: form.code.trim().toUpperCase() } : {}),
|
||||
...(isAdmin.value && form.headTeacherId ? { teacherId: form.headTeacherId } : {}),
|
||||
});
|
||||
@@ -452,9 +461,9 @@ onUnmounted(() => {
|
||||
</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="学期周数">
|
||||
<el-input-number v-model="form.termWeeks" :min="1" :max="40" :step="1" />
|
||||
<el-input-number v-model="form.termWeeks" :min="1" :max="MAX_TERM_WEEK" :step="1" />
|
||||
<span class="text-muted ml-8">
|
||||
本学期一共多少教学周(默认 20);课表周次选择与默认结束周都按它来
|
||||
本学期一共多少教学周(默认 20,最多 {{ MAX_TERM_WEEK }} 周);课表周次选择与默认结束周都按它来
|
||||
</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
@@ -47,8 +47,8 @@ function openImport(): void {
|
||||
const formVisible = ref(false);
|
||||
const formRef = ref<FormInstance>();
|
||||
const editingId = ref<string | null>(null);
|
||||
/** 表单不再暴露「用户名」:新建时由前端自动生成学号式登录名(见 autoCreateStudent) */
|
||||
const form = reactive({ name: '', password: '', classId: '' });
|
||||
/** 表单不再暴露「用户名」:新建时由前端自动生成学号式名单标识(见 autoCreateStudent) */
|
||||
const form = reactive({ name: '', classId: '' });
|
||||
const rules: FormRules = {
|
||||
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
|
||||
};
|
||||
@@ -56,7 +56,6 @@ const rules: FormRules = {
|
||||
function openCreate(): void {
|
||||
editingId.value = null;
|
||||
form.name = '';
|
||||
form.password = '';
|
||||
form.classId = filter.classId;
|
||||
formVisible.value = true;
|
||||
}
|
||||
@@ -64,18 +63,17 @@ function openCreate(): void {
|
||||
function openEdit(row: StudentDto): void {
|
||||
editingId.value = row.id;
|
||||
form.name = row.name;
|
||||
form.password = '';
|
||||
form.classId = row.classId ?? '';
|
||||
formVisible.value = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动生成学生登录名(学号式:student01、student02 …,与种子数据 student01~student15 同风格)。
|
||||
* 自动生成学生名单标识(学号式:student01、student02 …,与种子数据同风格)。
|
||||
*
|
||||
* 后端 `POST /api/students` 的 username 是必填且全局唯一的(教师/管理员账号同样占位),
|
||||
* 界面已按需求隐藏「用户名」输入,因此这里用「全量学生账号 + 递增序号」生成,
|
||||
* 并在服务端返回 409(唯一约束冲突,例如并发创建或撞上教师账号)时换下一个序号重试;
|
||||
* 其它错误(姓名/密码不合法等)立即抛出,不再重试,避免刷屏。
|
||||
* 后端 `POST /api/students` 的 username 必填且全局唯一(教师/管理员账号同样占位),
|
||||
* 界面已按需求隐藏「用户名」输入,因此这里用「全量学生 + 递增序号」生成,
|
||||
* 并在服务端返回 409(唯一约束冲突)时换下一个序号重试;其它错误立即抛出,避免刷屏。
|
||||
* 学生没有密码、不能登录,username 仅作名单标识。
|
||||
*/
|
||||
function studentUsernameAt(index: number): string {
|
||||
return index < 100 ? `student${String(index).padStart(2, '0')}` : `student${index}`;
|
||||
@@ -84,7 +82,7 @@ function studentUsernameAt(index: number): string {
|
||||
async function autoCreateStudent(): Promise<void> {
|
||||
const existing = new Set((await studentApi.list()).map((item) => item.username));
|
||||
let index = existing.size + 1;
|
||||
let lastError: unknown = new Error('自动生成学生账号失败');
|
||||
let lastError: unknown = new Error('自动生成名单标识失败');
|
||||
|
||||
for (let attempt = 0; attempt < 20; attempt += 1) {
|
||||
let username = studentUsernameAt(index);
|
||||
@@ -97,7 +95,6 @@ async function autoCreateStudent(): Promise<void> {
|
||||
username,
|
||||
name: form.name.trim(),
|
||||
classId: form.classId || null,
|
||||
...(form.password ? { password: form.password } : {}),
|
||||
});
|
||||
return;
|
||||
} catch (error) {
|
||||
@@ -125,7 +122,7 @@ async function submitForm(): Promise<void> {
|
||||
} else {
|
||||
try {
|
||||
await autoCreateStudent();
|
||||
ElMessage.success('学生账号创建成功(登录名已自动生成)');
|
||||
ElMessage.success('学生已创建(名单标识已自动生成)');
|
||||
} catch {
|
||||
// 接口层已弹出服务端原因,这里补充可执行的兜底建议
|
||||
ElMessage.error('创建失败:可稍后重试,或用「导入名单」在表格里显式指定用户名');
|
||||
@@ -146,12 +143,6 @@ async function removeStudent(row: StudentDto): Promise<void> {
|
||||
await loadStudents();
|
||||
}
|
||||
|
||||
async function resetPassword(row: StudentDto): Promise<void> {
|
||||
await ElMessageBox.confirm(`将「${row.name}」的密码重置为默认密码?`, '重置密码', { type: 'warning' });
|
||||
await studentApi.resetPassword(row.id);
|
||||
ElMessage.success('密码已重置为默认密码');
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadClasses(), loadStudents()]);
|
||||
});
|
||||
@@ -209,7 +200,7 @@ async function submitCall(): Promise<void> {
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2 class="page-title">学生管理</h2>
|
||||
<p class="page-subtitle">学生账号、分班与密码重置(数据范围按班级权限收敛)</p>
|
||||
<p class="page-subtitle">学生名单、分班与叫人(学生端统一用班级账号登录,学生本身没有密码)</p>
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
<el-select
|
||||
@@ -257,7 +248,6 @@ async function submitCall(): Promise<void> {
|
||||
<template #default="{ row }">
|
||||
<el-button link type="success" @click="openCall(row)">叫人</el-button>
|
||||
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button link type="warning" @click="resetPassword(row)">重置密码</el-button>
|
||||
<el-button link type="danger" @click="removeStudent(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -329,9 +319,6 @@ async function submitCall(): Promise<void> {
|
||||
<el-form-item label="姓名" prop="name">
|
||||
<el-input v-model="form.name" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="!editingId" label="初始密码">
|
||||
<el-input v-model="form.password" placeholder="留空使用默认密码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="班级">
|
||||
<el-select v-model="form.classId" placeholder="暂不分班" clearable style="width: 100%">
|
||||
<el-option v-for="item in classes" :key="item.id" :label="item.name" :value="item.id" />
|
||||
|
||||
@@ -8,7 +8,7 @@ import TableImportDialog from '@/components/TableImportDialog.vue';
|
||||
/**
|
||||
* 教师管理(**仅管理员可见/可用**,与后端 `requireRole('ADMIN')` 对应)。
|
||||
*
|
||||
* 与「学生管理」保持同一套交互:单个录入 / 表格导入名单 / 编辑 / 重置密码 / 删除。
|
||||
* 与「学生管理」保持同一套交互:单个录入 / 表格导入名单 / 编辑 / 修改密码 / 删除。
|
||||
* 学生端主体是班级账号,教师账号则决定"谁是班主任、谁能录课表与成绩",
|
||||
* 属于系统级配置,所以录入权限只给管理员。
|
||||
*/
|
||||
@@ -92,12 +92,33 @@ async function submitForm(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function resetPassword(row: UserDto): Promise<void> {
|
||||
await ElMessageBox.confirm(`将「${row.name}」的密码重置为默认初始密码?`, '重置密码', {
|
||||
type: 'warning',
|
||||
});
|
||||
await teacherApi.resetPassword(row.id);
|
||||
ElMessage.success('密码已重置为默认初始密码');
|
||||
/**
|
||||
* 修改教师密码(管理员给教师改密)。
|
||||
*
|
||||
* 后端 `POST /teachers/:id/reset-password` 本来就接受可选的 `newPassword`(留空才用默认初始密码),
|
||||
* 但界面原先只发空请求、按钮还叫「重置密码」 —— 于是管理员**没有任何办法**把某个教师的密码改成
|
||||
* 指定值,只能重置成默认的 `DEFAULT_TEACHER_PASSWORD`(用户反馈:"教师账号没有办法修改密码",
|
||||
* 并且指着教师列表问"教师修改密码呢")。
|
||||
* 因此这里把入口正名为「修改密码」:填了就设成填的值,留空才是"重置为默认初始密码"。
|
||||
*/
|
||||
async function changePassword(row: UserDto): Promise<void> {
|
||||
const result = await ElMessageBox.prompt(
|
||||
'输入新密码即可直接为该账号设置密码;留空则重置为默认初始密码。',
|
||||
`修改「${row.name}」的密码`,
|
||||
{
|
||||
confirmButtonText: '确认修改',
|
||||
cancelButtonText: '取消',
|
||||
inputType: 'password',
|
||||
inputPlaceholder: '新密码(至少 6 位,留空 = 默认初始密码)',
|
||||
// 默认校验器不允许空输入,这里必须放行"留空"这一条路
|
||||
inputValidator: (value: string) => !value || value.length >= 6 || '新密码至少 6 位',
|
||||
},
|
||||
).catch(() => null);
|
||||
if (!result) return;
|
||||
|
||||
const newPassword = (result.value ?? '').trim();
|
||||
await teacherApi.resetPassword(row.id, newPassword || undefined);
|
||||
ElMessage.success(newPassword ? '教师密码已修改' : '密码已重置为默认初始密码');
|
||||
}
|
||||
|
||||
async function removeTeacher(row: UserDto): Promise<void> {
|
||||
@@ -129,7 +150,7 @@ onMounted(loadTeachers);
|
||||
<div>
|
||||
<h2 class="page-title">教师管理</h2>
|
||||
<p class="page-subtitle">
|
||||
录入教师账号(单个录入或导入名单)、修改姓名与角色、重置密码(仅管理员可见)
|
||||
录入教师账号(单个录入或导入名单)、修改姓名与角色、修改密码(仅管理员可见)
|
||||
</p>
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
@@ -139,6 +160,7 @@ onMounted(loadTeachers);
|
||||
clearable
|
||||
style="width: 180px"
|
||||
@keyup.enter="loadTeachers"
|
||||
@clear="loadTeachers"
|
||||
/>
|
||||
<el-button :icon="'Search'" @click="loadTeachers">查询</el-button>
|
||||
<el-button type="primary" :icon="'Plus'" @click="openCreate">新建教师</el-button>
|
||||
@@ -163,7 +185,7 @@ onMounted(loadTeachers);
|
||||
<el-table-column label="操作" width="220" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button link type="warning" @click="resetPassword(row)">重置密码</el-button>
|
||||
<el-button link type="warning" @click="changePassword(row)">修改密码</el-button>
|
||||
<el-button link type="danger" @click="removeTeacher(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
Reference in new issue
Block a user