feat: 实现教师资料更新、操作日志和系统设置功能

新增教师资料更新功能,包括个人信息修改和密码更新
添加操作日志记录系统,记录用户关键操作
实现系统设置模块,支持动态配置系统参数
重构数据库模型,新增教师表和系统设置表
优化成绩录入逻辑,支持平时分、期中和期末成绩计算
添加数据导出功能,支持学生、教师和成绩数据导出
完善管理员后台,增加统计图表和操作日志查看
This commit is contained in:
祀梦
2025-12-22 23:30:01 +08:00
parent 16802c85e5
commit b1da021185
43 changed files with 7860 additions and 2835 deletions

View File

@@ -1,36 +1,64 @@
const mysql = require('mysql2/promise');
require('dotenv').config();
const sqlite3 = require('sqlite3').verbose();
const path = require('path');
const fs = require('fs');
const pool = mysql.createPool({
host: process.env.DB_HOST || 'localhost',
user: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || '123456',
database: process.env.DB_NAME || 'score_management',
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
});
const dbPath = path.resolve(__dirname, '../database.sqlite');
const db = new sqlite3.Database(dbPath);
// 封装基本查询方法
const query = async (sql, params) => {
try {
const [rows] = await pool.execute(sql, params);
return rows;
} catch (error) {
console.error('Database query error:', error);
throw error;
}
// Promisify query method
const query = (sql, params = []) => {
return new Promise((resolve, reject) => {
// Handle SELECT vs INSERT/UPDATE/DELETE
const trimmedSql = sql.trim().toUpperCase();
if (trimmedSql.startsWith('SELECT') || trimmedSql.startsWith('PRAGMA')) {
db.all(sql, params, (err, rows) => {
if (err) {
console.error('Database query error:', err);
reject(err);
} else {
resolve(rows);
}
});
} else {
db.run(sql, params, function(err) {
if (err) {
console.error('Database execution error:', err);
reject(err);
} else {
// Normalize result to look like MySQL result
// this.lastID, this.changes
resolve({
insertId: this.lastID,
affectedRows: this.changes,
warningStatus: 0
});
}
});
}
});
};
// Mock Pool object for compatibility
const pool = {
query: query,
execute: (sql, params) => query(sql, params).then(res => [res]), // Wrap in array for mysql2 compatibility
getConnection: () => Promise.resolve({
release: () => {},
query: query,
execute: (sql, params) => query(sql, params).then(res => [res]),
beginTransaction: () => query('BEGIN TRANSACTION'),
commit: () => query('COMMIT'),
rollback: () => query('ROLLBACK')
})
};
// 测试连接
const testConnection = async () => {
try {
const connection = await pool.getConnection();
console.log('数据库连接成功');
connection.release();
await query('SELECT 1');
console.log('SQLite 数据库连接成功');
return true;
} catch (error) {
console.error('数据库连接失败:', error.message);
console.error('SQLite 数据库连接失败:', error.message);
return false;
}
};
@@ -39,4 +67,4 @@ module.exports = {
pool,
query,
testConnection
};
};

View File

@@ -2,28 +2,25 @@ const AdminService = require('../services/adminService');
const { success, error } = require('../utils/response');
class AdminController {
static async getStats(req, res) {
try {
const stats = await AdminService.getStats();
success(res, stats, '获取统计信息成功');
} catch (err) {
console.error('Get Stats Error:', err);
error(res, '服务器错误');
}
}
static async getUsers(req, res) {
try {
const result = await AdminService.getUsers(req.query);
success(res, result.data, '获取成功');
// 注意:原来的响应结构是 { success, data, pagination }
// 现在的 success 工具函数结构是 { success, message, data }
// 我们可以稍微调整 success 调用,或者让前端适应
// 为了兼容性,这里手动返回
/*
res.json({
success: true,
message: '获取成功',
data: result.data,
pagination: result.pagination
});
*/
// 或者修改 response.js 支持 extra 字段,这里简单处理:
res.json({
success: true,
data: result.data,
pagination: result.pagination
});
} catch (err) {
console.error('Get Users Error:', err);
error(res, '服务器错误');
@@ -38,7 +35,10 @@ class AdminController {
return error(res, '请填写所有必填字段', 400);
}
await AdminService.createUser(req.body);
await AdminService.createUser(req.body, {
user_id: req.session.user.id,
ip: req.ip
});
success(res, null, '创建用户成功');
} catch (err) {
if (err.message === '用户ID已存在') {
@@ -48,6 +48,287 @@ class AdminController {
error(res, '服务器错误');
}
}
static async updateUser(req, res) {
try {
const { id } = req.params;
const updateData = req.body;
await AdminService.updateUser(id, updateData, {
user_id: req.session.user.id,
ip: req.ip
});
success(res, null, '更新用户成功');
} catch (err) {
console.error('Update User Error:', err);
error(res, '服务器错误');
}
}
static async deleteUser(req, res) {
try {
const { id } = req.params;
await AdminService.deleteUser(id, {
user_id: req.session.user.id,
ip: req.ip
});
success(res, null, '删除用户成功');
} catch (err) {
console.error('Delete User Error:', err);
error(res, '服务器错误');
}
}
// Student Management
static async getStudents(req, res) {
try {
const result = await AdminService.getStudents(req.query);
res.json({
success: true,
message: '获取学生列表成功',
data: result.data,
pagination: result.pagination
});
} catch (err) {
console.error(err);
error(res, '服务器错误');
}
}
static async createStudent(req, res) {
try {
if (!req.body.id || !req.body.name) return error(res, 'ID和姓名必填', 400);
await AdminService.createStudent(req.body, {
user_id: req.session.user.id,
ip: req.ip
});
success(res, null, '创建学生成功');
} catch (err) {
console.error(err);
error(res, err.message || '创建失败', 400);
}
}
static async updateStudent(req, res) {
try {
await AdminService.updateStudent(req.params.id, req.body, {
user_id: req.session.user.id,
ip: req.ip
});
success(res, null, '更新学生成功');
} catch (err) {
console.error(err);
error(res, '更新失败');
}
}
static async deleteStudent(req, res) {
try {
await AdminService.deleteStudent(req.params.id, {
user_id: req.session.user.id,
ip: req.ip
});
success(res, null, '删除学生成功');
} catch (err) {
console.error(err);
error(res, '删除失败');
}
}
// Teacher Management
static async getTeachers(req, res) {
try {
const result = await AdminService.getTeachers(req.query);
res.json({
success: true,
message: '获取教师列表成功',
data: result.data,
pagination: result.pagination
});
} catch (err) {
console.error(err);
error(res, '服务器错误');
}
}
static async createTeacher(req, res) {
try {
if (!req.body.id || !req.body.name) return error(res, '工号和姓名必填', 400);
await AdminService.createTeacher(req.body, {
user_id: req.session.user.id,
ip: req.ip
});
success(res, null, '创建教师成功');
} catch (err) {
console.error(err);
error(res, err.message || '创建失败', 400);
}
}
static async updateTeacher(req, res) {
try {
await AdminService.updateTeacher(req.params.id, req.body, {
user_id: req.session.user.id,
ip: req.ip
});
success(res, null, '更新教师成功');
} catch (err) {
console.error(err);
error(res, '更新失败');
}
}
static async deleteTeacher(req, res) {
try {
await AdminService.deleteTeacher(req.params.id, {
user_id: req.session.user.id,
ip: req.ip
});
success(res, null, '删除教师成功');
} catch (err) {
console.error(err);
error(res, '删除失败');
}
}
// Grade Statistics
static async getGradeStats(req, res) {
try {
const stats = await AdminService.getGradeStats();
success(res, stats, '获取成绩统计成功');
} catch (err) {
console.error(err);
error(res, '服务器错误');
}
}
// System Settings
static async getSettings(req, res) {
try {
const settings = await AdminService.getSettings();
success(res, settings);
} catch (err) {
console.error(err);
error(res, '服务器错误');
}
}
static async saveSettings(req, res) {
try {
await AdminService.saveSettings(req.body, {
user_id: req.session.user.id,
ip: req.ip
});
success(res, null, '保存设置成功');
} catch (err) {
console.error(err);
error(res, '保存失败');
}
}
// Data Maintenance
static async backupDatabase(req, res) {
try {
const result = await AdminService.backupDatabase({
user_id: req.session.user.id,
ip: req.ip
});
success(res, result, '数据库备份成功');
} catch (err) {
console.error(err);
error(res, '备份失败');
}
}
static async clearCache(req, res) {
try {
await AdminService.clearCache({
user_id: req.session.user.id,
ip: req.ip
});
success(res, null, '缓存已清理');
} catch (err) {
console.error(err);
error(res, '清理失败');
}
}
static async resetStudentPasswords(req, res) {
try {
await AdminService.resetStudentPasswords({
user_id: req.session.user.id,
ip: req.ip
});
success(res, null, '所有学生密码已重置为 123456');
} catch (err) {
console.error(err);
error(res, '重置失败');
}
}
// Operation Logs
static async getOperationLogs(req, res) {
try {
const logs = await AdminService.getLogs(req.query);
success(res, logs);
} catch (err) {
console.error(err);
error(res, '获取日志失败');
}
}
// Data Export
static async exportStudents(req, res) {
try {
const data = await AdminService.exportStudents();
const csv = jsonToCsv(data, ['id', 'name', 'class', 'major', 'grade', 'contact_info']);
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
res.setHeader('Content-Disposition', 'attachment; filename=students.csv');
res.send(csv);
} catch (err) {
console.error(err);
error(res, '导出失败');
}
}
static async exportTeachers(req, res) {
try {
const data = await AdminService.exportTeachers();
const csv = jsonToCsv(data, ['id', 'name', 'department', 'title', 'contact_info', 'created_at']);
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
res.setHeader('Content-Disposition', 'attachment; filename=teachers.csv');
res.send(csv);
} catch (err) {
console.error(err);
error(res, '导出失败');
}
}
static async exportGrades(req, res) {
try {
const data = await AdminService.exportGrades();
const csv = jsonToCsv(data, ['student_id', 'student_name', 'course_code', 'course_name', 'total_score', 'grade_point', 'grade_level', 'teacher_name']);
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
res.setHeader('Content-Disposition', 'attachment; filename=grades.csv');
res.send(csv);
} catch (err) {
console.error(err);
error(res, '导出失败');
}
}
}
// Helper function
function jsonToCsv(data, fields) {
if (!data || data.length === 0) return '';
const header = fields.join(',') + '\n';
const rows = data.map(row => {
return fields.map(field => {
const val = row[field] === null || row[field] === undefined ? '' : row[field];
return `"${String(val).replace(/"/g, '""')}"`;
}).join(',');
}).join('\n');
return '\ufeff' + header + rows; // Add BOM for Excel compatibility
}
module.exports = AdminController;

View File

@@ -85,6 +85,26 @@ class AuthController {
error(res, '服务器错误');
}
}
static async updateProfile(req, res) {
try {
const userId = req.session.user.id;
const updateData = req.body;
const updatedUser = await AuthService.updateProfile(userId, updateData);
// 更新 Session 中的用户信息
req.session.user = {
...req.session.user,
...updatedUser
};
success(res, { user: req.session.user }, '资料更新成功');
} catch (err) {
console.error('Update Profile Error:', err);
error(res, '服务器错误');
}
}
}
module.exports = AuthController;

View File

@@ -18,6 +18,68 @@ class TeacherController {
}
}
static async getClasses(req, res) {
try {
const classes = await TeacherService.getClasses();
success(res, { classes });
} catch (err) {
console.error('Get Classes Error:', err);
error(res, '服务器错误');
}
}
static async getMyClasses(req, res) {
try {
const teacherId = req.session.user.id;
const classes = await TeacherService.getTeacherClasses(teacherId);
success(res, { classes });
} catch (err) {
console.error('Get My Classes Error:', err);
error(res, '服务器错误');
}
}
static async createCourse(req, res) {
try {
const teacherId = req.session.user.id;
const courseId = await TeacherService.createCourse(teacherId, req.body);
success(res, { courseId }, '课程创建成功');
} catch (err) {
console.error('Create Course Error:', err);
error(res, '服务器错误');
}
}
static async updateCourse(req, res) {
try {
const teacherId = req.session.user.id;
const courseId = req.params.id;
await TeacherService.updateCourse(teacherId, courseId, req.body);
success(res, null, '课程更新成功');
} catch (err) {
if (err.message === '无权修改该课程或课程不存在') {
return error(res, err.message, 403);
}
console.error('Update Course Error:', err);
error(res, '服务器错误');
}
}
static async getGrades(req, res) {
try {
const teacherId = req.session.user.id;
const filters = {
courseId: req.query.courseId,
studentName: req.query.studentName
};
const grades = await TeacherService.getGrades(teacherId, filters);
success(res, { grades });
} catch (err) {
console.error('Get Grades Error:', err);
error(res, '服务器错误');
}
}
static async addScore(req, res) {
try {
const teacherId = req.session.user.id;

BIN
backend/database.sqlite Normal file

Binary file not shown.

343
backend/init_db.js Normal file
View File

@@ -0,0 +1,343 @@
const sqlite3 = require('sqlite3').verbose();
const path = require('path');
const bcrypt = require('bcryptjs');
const dbPath = path.resolve(__dirname, 'database.sqlite');
const db = new sqlite3.Database(dbPath);
const run = (sql, params = []) => {
return new Promise((resolve, reject) => {
db.run(sql, params, function(err) {
if (err) reject(err);
else resolve(this);
});
});
};
const insertUser = async (user) => {
await run(
'INSERT INTO users (id, name, password, role, class) VALUES (?, ?, ?, ?, ?)',
[user.id, user.name, user.password, user.role, user.class]
);
};
const insertStudent = async (student) => {
await run(
'INSERT INTO students (id, name, class, major, grade, contact_info) VALUES (?, ?, ?, ?, ?, ?)',
[student.id, student.name, student.class, student.major, student.grade, student.contact_info]
);
};
const insertClass = async (cls) => {
return await run(
'INSERT INTO classes (class_name, grade, major, teacher_id) VALUES (?, ?, ?, ?)',
[cls.class_name, cls.grade, cls.major, cls.teacher_id]
);
};
const insertCourse = async (course) => {
return await run(
'INSERT INTO courses (course_code, course_name, credit, teacher_id, class_id, semester, academic_year, category) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
[course.course_code, course.course_name, course.credit, course.teacher_id, course.class_id, course.semester, course.academic_year, course.category]
);
};
const insertGrade = async (grade) => {
await run(
`INSERT INTO grades (
student_id, course_id, teacher_id,
usual_score, midterm_score, final_score, total_score,
grade_point, grade_level, remark
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
grade.student_id, grade.course_id, grade.teacher_id,
grade.usual_score, grade.midterm_score, grade.final_score, grade.total_score,
grade.grade_point, grade.grade_level, grade.remark
]
);
};
// Helper to calculate grade point
const calculateGradePoint = (score) => {
if (score >= 90) return 4.0;
if (score >= 85) return 3.7;
if (score >= 82) return 3.3;
if (score >= 78) return 3.0;
if (score >= 75) return 2.7;
if (score >= 72) return 2.3;
if (score >= 68) return 2.0;
if (score >= 64) return 1.5;
if (score >= 60) return 1.0;
return 0.0;
};
const calculateGradeLevel = (score) => {
if (score >= 90) return 'A';
if (score >= 80) return 'B';
if (score >= 70) return 'C';
if (score >= 60) return 'D';
return 'F';
};
const init = async () => {
console.log('开始初始化 SQLite 数据库...');
const hashedPassword = await bcrypt.hash('123456', 10);
try {
// Drop tables
await run('DROP TABLE IF EXISTS grades');
await run('DROP TABLE IF EXISTS courses');
await run('DROP TABLE IF EXISTS classes');
await run('DROP TABLE IF EXISTS students');
await run('DROP TABLE IF EXISTS users');
await run('DROP TABLE IF EXISTS operation_logs');
// Create tables
console.log('创建表结构...');
await run(`
CREATE TABLE users (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
password TEXT NOT NULL,
role TEXT NOT NULL,
class TEXT,
created_at TEXT DEFAULT (datetime('now', 'localtime'))
)
`);
await run(`
CREATE TABLE students (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
class TEXT,
major TEXT,
grade TEXT,
contact_info TEXT,
FOREIGN KEY (id) REFERENCES users(id) ON DELETE CASCADE
)
`);
await run(`
CREATE TABLE classes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
class_name TEXT NOT NULL,
grade TEXT,
major TEXT,
teacher_id TEXT,
created_at TEXT DEFAULT (datetime('now', 'localtime'))
)
`);
await run(`
CREATE TABLE courses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
course_code TEXT UNIQUE NOT NULL,
course_name TEXT NOT NULL,
credit REAL DEFAULT 2.0,
teacher_id TEXT NOT NULL,
class_id INTEGER NOT NULL,
semester TEXT,
academic_year TEXT,
category TEXT,
created_at TEXT DEFAULT (datetime('now', 'localtime'))
)
`);
await run(`
CREATE TABLE grades (
id INTEGER PRIMARY KEY AUTOINCREMENT,
student_id TEXT NOT NULL,
course_id INTEGER NOT NULL,
teacher_id TEXT NOT NULL,
usual_score REAL,
midterm_score REAL,
final_score REAL,
total_score REAL,
grade_point REAL,
grade_level TEXT,
remark TEXT,
created_at TEXT DEFAULT (datetime('now', 'localtime')),
UNIQUE(student_id, course_id)
)
`);
await run(`
CREATE TABLE operation_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT,
operation_type TEXT,
operation_target TEXT,
description TEXT,
ip_address TEXT,
created_at TEXT DEFAULT (datetime('now', 'localtime'))
)
`);
console.log('生成基础数据...');
// 1. Admin
await insertUser({
id: 'admin',
name: '系统管理员',
password: hashedPassword,
role: 'admin',
class: null
});
// 2. Teachers (20 teachers)
const teachers = [];
for (let i = 1; i <= 20; i++) {
const id = `T${1000 + i}`;
const name = `教师${String.fromCharCode(65 + (i % 26))}${i}`;
await insertUser({
id,
name,
password: hashedPassword,
role: 'teacher',
class: null
});
teachers.push(id);
}
// 3. Classes (10 classes)
const majors = ['软件工程', '计算机科学', '信息管理'];
const classIds = [];
const classes = [];
for (let i = 1; i <= 10; i++) {
const major = majors[i % 3];
const gradeYear = 2021 + Math.floor((i-1)/5); // 2021, 2022
const className = `${major.substr(0, 2)}${gradeYear}${String(i).padStart(2, '0')}`;
const teacherId = teachers[i % teachers.length];
const result = await insertClass({
class_name: className,
grade: String(gradeYear),
major: major,
teacher_id: teacherId
});
classIds.push(result.lastID);
classes.push({ id: result.lastID, name: className, year: gradeYear, major });
}
// 4. Students (50 per class -> 500 students)
console.log('生成学生数据...');
const students = [];
for (let clsIdx = 0; clsIdx < classes.length; clsIdx++) {
const cls = classes[clsIdx];
for (let i = 1; i <= 50; i++) {
const id = `${cls.year}${String(clsIdx + 1).padStart(2, '0')}${String(i).padStart(3, '0')}`;
const name = `学生${cls.name.substr(0,1)}${i}`;
await insertUser({
id,
name,
password: hashedPassword,
role: 'student',
class: cls.name
});
await insertStudent({
id,
name,
class: cls.name,
major: cls.major,
grade: String(cls.year),
contact_info: `13800${id.substr(0, 6)}`
});
students.push({ id, classId: cls.id });
}
}
// 5. Courses and Grades
console.log('生成课程和成绩数据...');
const courseNames = [
{ name: '高等数学', credit: 4, category: '必修' },
{ name: '大学英语', credit: 3, category: '必修' },
{ name: '程序设计基础', credit: 4, category: '必修' },
{ name: '数据结构', credit: 4, category: '必修' },
{ name: '操作系统', credit: 3, category: '必修' },
{ name: '计算机网络', credit: 3, category: '必修' },
{ name: '数据库原理', credit: 3, category: '必修' },
{ name: '软件工程导论', credit: 2, category: '必修' },
{ name: 'Web开发技术', credit: 3, category: '选修' },
{ name: '人工智能基础', credit: 2, category: '选修' },
{ name: '大数据分析', credit: 2, category: '选修' },
{ name: '音乐鉴赏', credit: 1, category: '通识' },
{ name: '心理健康', credit: 1, category: '通识' },
{ name: '职业规划', credit: 1, category: '通识' }
];
const semesters = ['2021-2022-1', '2021-2022-2', '2022-2023-1', '2022-2023-2', '2023-2024-1'];
for (const cls of classes) {
// For each class, assign some courses
for (let semIdx = 0; semIdx < semesters.length; semIdx++) {
const semester = semesters[semIdx];
// Select random 5-8 courses for this semester
const semCourses = courseNames.sort(() => 0.5 - Math.random()).slice(0, 6);
for (const cTemplate of semCourses) {
const teacherId = teachers[Math.floor(Math.random() * teachers.length)];
const courseCode = `C${cls.id}${semIdx}${Math.floor(Math.random() * 1000)}`;
const result = await insertCourse({
course_code: courseCode,
course_name: cTemplate.name,
credit: cTemplate.credit,
teacher_id: teacherId,
class_id: cls.id,
semester: semester,
academic_year: semester.substring(0, 9),
category: cTemplate.category
});
const courseId = result.lastID;
// Generate grades for students in this class
const classStudents = students.filter(s => s.classId === cls.id);
// Batch insert could be faster, but let's keep it simple for now
const stmt = db.prepare(`INSERT INTO grades (
student_id, course_id, teacher_id,
usual_score, midterm_score, final_score, total_score,
grade_point, grade_level, remark
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
for (const stu of classStudents) {
// 90% chance to have a grade
if (Math.random() > 0.1) {
const totalScore = Math.floor(Math.random() * 40) + 60; // 60-100 mostly
// 10% chance to fail
const finalTotal = Math.random() > 0.1 ? totalScore : Math.floor(Math.random() * 59);
stmt.run([
stu.id,
courseId,
teacherId,
finalTotal, // usual
finalTotal, // midterm
finalTotal, // final
finalTotal, // total
calculateGradePoint(finalTotal),
calculateGradeLevel(finalTotal),
''
]);
}
}
stmt.finalize();
}
}
}
console.log('数据库初始化完成!');
db.close();
} catch (err) {
console.error('Initialization failed:', err);
process.exit(1);
}
};
init();

View File

@@ -2,10 +2,15 @@ const db = require('../config/database');
class Course {
static async findByTeacherId(teacherId) {
return await db.query(
'SELECT * FROM courses WHERE teacher_id = ? ORDER BY course_code',
[teacherId]
);
const sql = `
SELECT c.*, cl.class_name,
(SELECT COUNT(*) FROM students s WHERE s.class = cl.class_name) as student_count
FROM courses c
LEFT JOIN classes cl ON c.class_id = cl.id
WHERE c.teacher_id = ?
ORDER BY c.course_code
`;
return await db.query(sql, [teacherId]);
}
static async findById(id) {
@@ -37,6 +42,28 @@ class Course {
`;
return await db.query(sql, [studentId]);
}
static async getClasses() {
return await db.query('SELECT * FROM classes');
}
static async create(data) {
const { course_name, course_code, credit, teacher_id, semester, class_id, category } = data;
const sql = `INSERT INTO courses (course_name, course_code, credit, teacher_id, semester, class_id, category) VALUES (?, ?, ?, ?, ?, ?, ?)`;
const result = await db.query(sql, [course_name, course_code, credit, teacher_id, semester, class_id, category]);
return result.insertId;
}
static async update(id, data) {
const { course_name, course_code, credit, semester, class_id } = data;
const sql = `
UPDATE courses
SET course_name = ?, course_code = ?, credit = ?, semester = ?, class_id = ?
WHERE id = ?
`;
const result = await db.query(sql, [course_name, course_code, credit, semester, class_id, id]);
return result.affectedRows > 0;
}
}
module.exports = Course;

View File

@@ -0,0 +1,28 @@
const db = require('../config/database');
class OperationLog {
static async add(logData) {
const { user_id, type, target, description, ip } = logData;
const sql = `
INSERT INTO operation_logs (user_id, operation_type, operation_target, description, ip_address)
VALUES (?, ?, ?, ?, ?)
`;
return await db.query(sql, [user_id, type, target, description, ip]);
}
static async findAll(params = {}) {
const limit = parseInt(params.limit) || 50;
const offset = parseInt(params.offset) || 0;
const sql = `
SELECT l.*, u.name as user_name
FROM operation_logs l
LEFT JOIN users u ON l.user_id = u.id
ORDER BY l.created_at DESC
LIMIT ? OFFSET ?
`;
return await db.query(sql, [limit, offset]);
}
}
module.exports = OperationLog;

View File

@@ -41,7 +41,7 @@ class Score {
const sql = `
INSERT INTO grades (student_id, course_id, teacher_id, final_score, total_score,
grade_point, grade_level, created_at, remark)
VALUES (?, ?, ?, ?, ?, ?, ?, NOW(), ?)
VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now', 'localtime'), ?)
`;
const result = await db.pool.execute(sql, [
studentId, courseId, teacherId, score, score, gradePoint, gradeLevel, remark
@@ -56,6 +56,78 @@ class Score {
);
return rows[0];
}
static async findByTeacher(teacherId, filters) {
let sql = `
SELECT g.*, s.name as student_name, c.course_name
FROM grades g
JOIN students s ON g.student_id = s.id
JOIN courses c ON g.course_id = c.id
WHERE c.teacher_id = ?
`;
const params = [teacherId];
if (filters.courseId) {
sql += ' AND g.course_id = ?';
params.push(filters.courseId);
}
if (filters.studentName) {
sql += ' AND (s.name LIKE ? OR s.id LIKE ?)';
params.push(`%${filters.studentName}%`, `%${filters.studentName}%`);
}
return await db.query(sql, params);
}
static async findCourseStudentsWithGrades(courseId, teacherId) {
const sql = `
SELECT s.id as student_id, s.name as student_name,
g.usual_score, g.midterm_score, g.final_score, g.total_score, g.grade_point, g.grade_level,
c.id as course_id, c.course_name
FROM students s
JOIN classes cl ON s.class = cl.class_name
JOIN courses c ON c.class_id = cl.id
LEFT JOIN grades g ON s.id = g.student_id AND g.course_id = c.id
WHERE c.id = ? AND c.teacher_id = ?
`;
return await db.query(sql, [courseId, teacherId]);
}
static async upsert(scoreData) {
const {
studentId, courseId, teacherId,
usual_score, midterm_score, final_score, score,
gradePoint, gradeLevel, remark
} = scoreData;
const existing = await this.findByStudentAndCourse(studentId, courseId);
// 处理参数:如果是 undefined 或空字符串,则设为 null
const sanitize = (val) => (val === undefined || val === '' || val === null) ? null : val;
const params = [
sanitize(usual_score),
sanitize(midterm_score),
sanitize(final_score),
sanitize(score),
sanitize(gradePoint),
sanitize(gradeLevel),
sanitize(remark)
];
if (existing) {
const sql = `UPDATE grades SET usual_score=?, midterm_score=?, final_score=?, total_score=?, grade_point=?, grade_level=?, remark=? WHERE id=?`;
await db.query(sql, [...params, existing.id]);
return existing.id;
} else {
const sql = `INSERT INTO grades (student_id, course_id, teacher_id, usual_score, midterm_score, final_score, total_score, grade_point, grade_level, created_at, remark) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now', 'localtime'), ?)`;
const insertParams = [
studentId, courseId, teacherId,
...params
];
const result = await db.query(sql, insertParams);
return result.insertId;
}
}
}
module.exports = Score;

View File

@@ -7,12 +7,33 @@ class Student {
}
static async create(studentData) {
const { id, name, className } = studentData;
const { id, name, class: className, major, grade, contact_info } = studentData;
await db.query(
'INSERT INTO students (id, name, class) VALUES (?, ?, ?)',
[id, name, className]
'INSERT INTO students (id, name, class, major, grade, contact_info) VALUES (?, ?, ?, ?, ?, ?)',
[id, name, className, major, grade, contact_info]
);
}
static async update(id, data) {
const fields = [];
const values = [];
if (data.name) { fields.push('name = ?'); values.push(data.name); }
if (data.class) { fields.push('class = ?'); values.push(data.class); }
if (data.major !== undefined) { fields.push('major = ?'); values.push(data.major); }
if (data.grade !== undefined) { fields.push('grade = ?'); values.push(data.grade); }
if (data.contact_info !== undefined) { fields.push('contact_info = ?'); values.push(data.contact_info); }
if (fields.length === 0) return true;
values.push(id);
const sql = `UPDATE students SET ${fields.join(', ')} WHERE id = ?`;
await db.query(sql, values);
return true;
}
static async delete(id) {
await db.query('DELETE FROM students WHERE id = ?', [id]);
}
}
module.exports = Student;
module.exports = Student;

View File

@@ -0,0 +1,32 @@
const db = require('../config/database');
class SystemSetting {
static async get(key) {
const rows = await db.query('SELECT value FROM system_settings WHERE key = ?', [key]);
return rows.length > 0 ? rows[0].value : null;
}
static async getAll() {
const rows = await db.query('SELECT key, value FROM system_settings');
const settings = {};
rows.forEach(row => {
settings[row.key] = row.value;
});
return settings;
}
static async set(key, value) {
return await db.query(
'INSERT INTO system_settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value',
[key, value]
);
}
static async setMany(settings) {
for (const [key, value] of Object.entries(settings)) {
await this.set(key, String(value));
}
}
}
module.exports = SystemSetting;

38
backend/models/Teacher.js Normal file
View File

@@ -0,0 +1,38 @@
const db = require('../config/database');
class Teacher {
static async findById(id) {
const teachers = await db.query('SELECT * FROM teachers WHERE id = ?', [id]);
return teachers[0];
}
static async create(teacherData) {
const { id, name, department, title, contact_info } = teacherData;
await db.query(
'INSERT INTO teachers (id, name, department, title, contact_info) VALUES (?, ?, ?, ?, ?)',
[id, name, department, title, contact_info]
);
}
static async update(id, data) {
const fields = [];
const values = [];
if (data.name) { fields.push('name = ?'); values.push(data.name); }
if (data.department) { fields.push('department = ?'); values.push(data.department); }
if (data.title !== undefined) { fields.push('title = ?'); values.push(data.title); }
if (data.contact_info !== undefined) { fields.push('contact_info = ?'); values.push(data.contact_info); }
if (fields.length === 0) return true;
values.push(id);
const sql = `UPDATE teachers SET ${fields.join(', ')} WHERE id = ?`;
await db.query(sql, values);
return true;
}
static async delete(id) {
await db.query('DELETE FROM teachers WHERE id = ?', [id]);
}
}
module.exports = Teacher;

View File

@@ -35,6 +35,27 @@ class User {
await db.query('UPDATE users SET password = ? WHERE id = ?', [hashedPassword, id]);
return true;
}
static async updateProfile(id, updateData) {
const fields = [];
const params = [];
if (updateData.name) {
fields.push('name = ?');
params.push(updateData.name);
}
if (updateData.class !== undefined) {
fields.push('class = ?');
params.push(updateData.class);
}
if (fields.length === 0) return false;
params.push(id);
const sql = `UPDATE users SET ${fields.join(', ')} WHERE id = ?`;
await db.query(sql, params);
return true;
}
}
module.exports = User;

1445
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,14 +8,15 @@
"dev": "nodemon server.js"
},
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"express-session": "^1.17.3",
"express-mysql-session": "^3.0.0",
"mysql2": "^3.6.0",
"bcryptjs": "^2.4.3",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"body-parser": "^1.20.2"
"express": "^4.18.2",
"express-mysql-session": "^3.0.0",
"express-session": "^1.17.3",
"mysql2": "^3.6.0",
"sqlite3": "^5.1.7"
},
"devDependencies": {
"nodemon": "^3.0.1"
@@ -23,4 +24,4 @@
"engines": {
"node": ">=14.0.0"
}
}
}

View File

@@ -3,7 +3,42 @@ const router = express.Router();
const AdminController = require('../controllers/adminController');
const { requireAuth, requireRole } = require('../middleware/auth');
router.get('/stats', requireAuth, requireRole(['admin']), AdminController.getStats);
router.get('/users', requireAuth, requireRole(['admin']), AdminController.getUsers);
router.post('/users', requireAuth, requireRole(['admin']), AdminController.createUser);
router.put('/users/:id', requireAuth, requireRole(['admin']), AdminController.updateUser);
router.delete('/users/:id', requireAuth, requireRole(['admin']), AdminController.deleteUser);
// Student Management
router.get('/students', requireAuth, requireRole(['admin']), AdminController.getStudents);
router.post('/students', requireAuth, requireRole(['admin']), AdminController.createStudent);
router.put('/students/:id', requireAuth, requireRole(['admin']), AdminController.updateStudent);
router.delete('/students/:id', requireAuth, requireRole(['admin']), AdminController.deleteStudent);
// Teacher Management
router.get('/teachers', requireAuth, requireRole(['admin']), AdminController.getTeachers);
router.post('/teachers', requireAuth, requireRole(['admin']), AdminController.createTeacher);
router.put('/teachers/:id', requireAuth, requireRole(['admin']), AdminController.updateTeacher);
router.delete('/teachers/:id', requireAuth, requireRole(['admin']), AdminController.deleteTeacher);
// Grade Statistics
router.get('/grade-stats', requireAuth, requireRole(['admin']), AdminController.getGradeStats);
// System Settings
router.get('/settings', requireAuth, requireRole(['admin']), AdminController.getSettings);
router.post('/settings', requireAuth, requireRole(['admin']), AdminController.saveSettings);
// Data Maintenance
router.post('/maintenance/backup', requireAuth, requireRole(['admin']), AdminController.backupDatabase);
router.post('/maintenance/clear-cache', requireAuth, requireRole(['admin']), AdminController.clearCache);
router.post('/maintenance/reset-passwords', requireAuth, requireRole(['admin']), AdminController.resetStudentPasswords);
// Data Export
router.get('/export/students', requireAuth, requireRole(['admin']), AdminController.exportStudents);
router.get('/export/teachers', requireAuth, requireRole(['admin']), AdminController.exportTeachers);
router.get('/export/grades', requireAuth, requireRole(['admin']), AdminController.exportGrades);
// Operation Logs
router.get('/logs', requireAuth, requireRole(['admin']), AdminController.getOperationLogs);
module.exports = router;

View File

@@ -8,5 +8,6 @@ router.post('/register', AuthController.register);
router.post('/logout', AuthController.logout);
router.get('/me', AuthController.getCurrentUser);
router.put('/update-password', requireAuth, AuthController.updatePassword);
router.put('/update-profile', requireAuth, AuthController.updateProfile);
module.exports = router;

View File

@@ -4,6 +4,11 @@ const TeacherController = require('../controllers/teacherController');
const { requireAuth, requireRole } = require('../middleware/auth');
router.get('/courses', requireAuth, requireRole(['teacher']), TeacherController.getCourses);
router.get('/classes', requireAuth, requireRole(['teacher']), TeacherController.getClasses);
router.get('/my-classes', requireAuth, requireRole(['teacher']), TeacherController.getMyClasses);
router.post('/courses', requireAuth, requireRole(['teacher']), TeacherController.createCourse);
router.put('/courses/:id', requireAuth, requireRole(['teacher']), TeacherController.updateCourse);
router.get('/grades', requireAuth, requireRole(['teacher']), TeacherController.getGrades);
router.post('/grades', requireAuth, requireRole(['teacher']), TeacherController.addScore);
module.exports = router;

View File

@@ -0,0 +1,31 @@
const db = require('../config/database');
async function fixTeachers() {
const departments = ['计算机学院', '软件学院', '信息工程学院', '理学院', '外国语学院'];
const titles = ['教授', '副教授', '讲师', '助教'];
try {
const teachers = await db.query('SELECT id FROM teachers');
for (let i = 0; i < teachers.length; i++) {
const dept = departments[i % departments.length];
const title = titles[i % titles.length];
const contact = `139${Math.floor(Math.random() * 90000000 + 10000000)}`;
await db.query(
'UPDATE teachers SET department = ?, title = ?, contact_info = ? WHERE id = ?',
[dept, title, contact, teachers[i].id]
);
// Also sync back to users table's class field if needed (though we'll use teachers table now)
await db.query(
'UPDATE users SET class = ? WHERE id = ?',
[dept, teachers[i].id]
);
}
console.log('教师信息修复完成!');
} catch (err) {
console.error('修复失败:', err);
}
}
fixTeachers();

View File

@@ -0,0 +1,48 @@
const db = require('../config/database');
async function migrate() {
try {
console.log('Starting migration v2...');
// 1. System Settings Table
await db.query(`
CREATE TABLE IF NOT EXISTS system_settings (
key TEXT PRIMARY KEY,
value TEXT
)
`);
// Default settings
const defaultSettings = [
['system_name', '学校成绩管理系统'],
['current_semester', '2023-2024-2'],
['allow_course_selection', '1'],
['allow_grade_check', '1']
];
for (const [key, value] of defaultSettings) {
await db.query('INSERT OR IGNORE INTO system_settings (key, value) VALUES (?, ?)', [key, value]);
}
// 2. Operation Logs Table
await db.query(`
CREATE TABLE IF NOT EXISTS operation_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT,
operation_type TEXT,
operation_target TEXT,
description TEXT,
ip_address TEXT,
created_at TEXT DEFAULT (datetime('now', 'localtime'))
)
`);
console.log('Migration v2 completed successfully.');
process.exit(0);
} catch (error) {
console.error('Migration v2 failed:', error);
process.exit(1);
}
}
migrate();

View File

@@ -0,0 +1,33 @@
const db = require('../config/database');
async function migrate() {
console.log('开始执行 v3 迁移: 创建 teachers 表...');
try {
await db.query(`
CREATE TABLE IF NOT EXISTS teachers (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
department TEXT,
title TEXT,
contact_info TEXT,
FOREIGN KEY (id) REFERENCES users(id) ON DELETE CASCADE
)
`);
// 从 users 表中迁移现有的教师数据
const teachers = await db.query('SELECT id, name, class FROM users WHERE role = "teacher"');
for (const t of teachers) {
await db.query(
'INSERT OR IGNORE INTO teachers (id, name, department) VALUES (?, ?, ?)',
[t.id, t.name, t.class]
);
}
console.log('v3 迁移成功!');
} catch (err) {
console.error('v3 迁移失败:', err);
}
}
migrate();

View File

@@ -1,7 +1,7 @@
const express = require('express');
const cors = require('cors');
const session = require('express-session');
const MySQLStore = require('express-mysql-session')(session);
// const MySQLStore = require('express-mysql-session')(session);
const path = require('path');
require('dotenv').config();
@@ -28,6 +28,7 @@ app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Session
/*
const sessionStore = new MySQLStore({
expiration: 86400000,
createDatabaseTable: true,
@@ -40,11 +41,12 @@ const sessionStore = new MySQLStore({
}
}
}, db.pool);
*/
app.use(session({
key: 'session_cookie',
secret: process.env.SESSION_SECRET || 'your-secret-key',
store: sessionStore,
// store: sessionStore, // Use MemoryStore for SQLite migration simplification
resave: false,
saveUninitialized: false,
cookie: {
@@ -112,14 +114,20 @@ teacherPageRouter.use(requirePageAuth, requirePageRole(['teacher']));
teacherPageRouter.get('/dashboard', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/teacher/dashboard.html')));
teacherPageRouter.get('/grade_entry', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/teacher/grade_entry.html')));
teacherPageRouter.get('/grade_management', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/teacher/grade_management.html')));
teacherPageRouter.get('/profile', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/teacher/profile.html')));
app.use('/teacher', teacherPageRouter);
// Admin Pages
const adminPageRouter = express.Router();
adminPageRouter.use(requirePageAuth, requirePageRole(['admin']));
adminPageRouter.get('/dashboard', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/admin/dashboard.html')));
adminPageRouter.get('/student_management', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/admin/student_management.html')));
adminPageRouter.get('/user_management', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/admin/user_management.html')));
adminPageRouter.get('/student_management', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/admin/student_management.html')));
adminPageRouter.get('/teacher_management', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/admin/teacher_management.html')));
adminPageRouter.get('/grade_statistics', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/admin/grade_statistics.html')));
adminPageRouter.get('/system_settings', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/admin/system_settings.html')));
adminPageRouter.get('/data_export', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/admin/data_export.html')));
adminPageRouter.get('/operation_logs', (req, res) => res.sendFile(path.join(__dirname, '../frontend/views/admin/operation_logs.html')));
app.use('/admin', adminPageRouter);
// --- API Routes ---

View File

@@ -1,10 +1,17 @@
const db = require('../config/database');
const bcrypt = require('bcryptjs');
const User = require('../models/User');
const Student = require('../models/Student');
const Teacher = require('../models/Teacher');
const SystemSetting = require('../models/SystemSetting');
const OperationLog = require('../models/OperationLog');
class AdminService {
static async getUsers(params) {
const { page = 1, limit = 10, search = '', role = '' } = params;
const page = parseInt(params.page) || 1;
const limit = parseInt(params.limit) || 10;
const search = params.search || '';
const role = params.role || '';
const offset = (page - 1) * limit;
let queryStr = 'SELECT id, name, role, class, created_at FROM users WHERE 1=1';
@@ -28,9 +35,9 @@ class AdminService {
// Data
queryStr += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
queryParams.push(parseInt(limit), parseInt(offset));
const dataQueryParams = [...queryParams, limit, offset];
const users = await db.query(queryStr, queryParams);
const users = await db.query(queryStr, dataQueryParams);
return {
data: users,
@@ -43,8 +50,22 @@ class AdminService {
};
}
static async createUser(userData) {
const { id } = userData;
static async getStats() {
const usersCount = await db.query('SELECT COUNT(*) as count FROM users');
const studentsCount = await db.query('SELECT COUNT(*) as count FROM students');
const teachersCount = await db.query('SELECT COUNT(*) as count FROM users WHERE role = "teacher"');
const coursesCount = await db.query('SELECT COUNT(*) as count FROM courses');
return {
users: usersCount[0].count,
students: studentsCount[0].count,
teachers: teachersCount[0].count,
courses: coursesCount[0].count
};
}
static async createUser(userData, operator) {
const { id, role, name } = userData;
// 检查 ID
const existingUser = await User.findById(id);
@@ -53,7 +74,445 @@ class AdminService {
}
// 创建用户
return await User.create(userData);
const userId = await User.create(userData);
// 如果是学生,同时创建学生记录
if (role === 'student') {
await Student.create(userData);
} else if (role === 'teacher') {
await Teacher.create({
id,
name,
department: userData.class || ''
});
}
if (operator) {
await OperationLog.add({
user_id: operator.user_id,
type: '用户管理',
target: `user:${id}`,
description: `创建了${role === 'student' ? '学生' : (role === 'teacher' ? '教师' : '管理员')}用户: ${name}(${id})`,
ip: operator.ip
});
}
return userId;
}
static async updateUser(id, userData, operator) {
// 如果修改密码
if (userData.password) {
const salt = await bcrypt.genSalt(10);
userData.password = await bcrypt.hash(userData.password, salt);
} else {
delete userData.password;
}
const fields = [];
const values = [];
if (userData.name) { fields.push('name = ?'); values.push(userData.name); }
if (userData.role) { fields.push('role = ?'); values.push(userData.role); }
if (userData.class !== undefined) { fields.push('class = ?'); values.push(userData.class); } // class can be empty string
if (userData.password) { fields.push('password = ?'); values.push(userData.password); }
if (fields.length === 0) return true;
values.push(id);
const sql = `UPDATE users SET ${fields.join(', ')} WHERE id = ?`;
const result = await db.query(sql, values);
if (operator) {
await OperationLog.add({
user_id: operator.user_id,
type: '用户管理',
target: `user:${id}`,
description: `修改了用户信息: ${id}`,
ip: operator.ip
});
}
return result.affectedRows > 0;
}
static async deleteUser(id, operator) {
// 删除用户
await Student.delete(id);
await Teacher.delete(id);
const result = await db.query('DELETE FROM users WHERE id = ?', [id]);
if (operator) {
await OperationLog.add({
user_id: operator.user_id,
type: '用户管理',
target: `user:${id}`,
description: `删除了用户: ${id}`,
ip: operator.ip
});
}
return result.affectedRows > 0;
}
// ================= Student Management =================
static async getStudents(params) {
const page = parseInt(params.page) || 1;
const limit = parseInt(params.limit) || 10;
const search = params.search || '';
const offset = (page - 1) * limit;
let queryStr = 'SELECT * FROM students WHERE 1=1';
let queryParams = [];
if (search) {
queryStr += ' AND (id LIKE ? OR name LIKE ? OR class LIKE ?)';
const searchTerm = `%${search}%`;
queryParams.push(searchTerm, searchTerm, searchTerm);
}
// Count
const countSql = queryStr.replace('SELECT *', 'SELECT COUNT(*) as total');
const countRows = await db.query(countSql, queryParams);
const total = countRows[0].total;
// Data
queryStr += ' ORDER BY id ASC LIMIT ? OFFSET ?';
const dataQueryParams = [...queryParams, limit, offset];
const students = await db.query(queryStr, dataQueryParams);
return {
data: students,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / limit)
}
};
}
static async createStudent(studentData, operator) {
const { id, name, class: className } = studentData;
// Check ID
const existing = await User.findById(id);
if (existing) throw new Error('学号已存在');
// Create User first
await User.create({
id,
name,
password: id, // Default password
role: 'student',
class: className
});
// Create Student
await Student.create(studentData);
if (operator) {
await OperationLog.add({
user_id: operator.user_id,
type: '学生管理',
target: `student:${id}`,
description: `创建了学生: ${name}(${id})`,
ip: operator.ip
});
}
return id;
}
static async updateStudent(id, data, operator) {
// Update students table
await Student.update(id, data);
// Sync User table (name, class)
const userFields = [];
const userValues = [];
if (data.name) { userFields.push('name = ?'); userValues.push(data.name); }
if (data.class) { userFields.push('class = ?'); userValues.push(data.class); }
if (userFields.length > 0) {
userValues.push(id);
await db.query(`UPDATE users SET ${userFields.join(', ')} WHERE id = ?`, userValues);
}
if (operator) {
await OperationLog.add({
user_id: operator.user_id,
type: '学生管理',
target: `student:${id}`,
description: `修改了学生信息: ${id}`,
ip: operator.ip
});
}
return true;
}
static async deleteStudent(id, operator) {
await Student.delete(id);
await db.query('DELETE FROM users WHERE id = ?', [id]);
if (operator) {
await OperationLog.add({
user_id: operator.user_id,
type: '学生管理',
target: `student:${id}`,
description: `删除了学生: ${id}`,
ip: operator.ip
});
}
return true;
}
// ================= Teacher Management =================
static async getTeachers(params) {
const page = parseInt(params.page) || 1;
const limit = parseInt(params.limit) || 10;
const search = params.search || '';
const offset = (page - 1) * limit;
let queryStr = 'SELECT t.*, u.created_at FROM teachers t JOIN users u ON t.id = u.id WHERE 1=1';
let queryParams = [];
if (search) {
queryStr += ' AND (t.id LIKE ? OR t.name LIKE ? OR t.department LIKE ?)';
const searchTerm = `%${search}%`;
queryParams.push(searchTerm, searchTerm, searchTerm);
}
// Count
const countSql = `SELECT COUNT(*) as total FROM teachers t WHERE 1=1 ${search ? 'AND (t.id LIKE ? OR t.name LIKE ? OR t.department LIKE ?)' : ''}`;
const countRows = await db.query(countSql, queryParams);
const total = countRows[0].total;
// Data
queryStr += ' ORDER BY t.id ASC LIMIT ? OFFSET ?';
const dataQueryParams = [...queryParams, limit, offset];
const teachers = await db.query(queryStr, dataQueryParams);
return {
data: teachers,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / limit)
}
};
}
static async createTeacher(teacherData, operator) {
const { id, name, department } = teacherData;
// Check ID
const existing = await User.findById(id);
if (existing) throw new Error('工号已存在');
// Create User
await User.create({
id,
name,
password: id, // Default password
role: 'teacher',
class: department // Use class field for department in user table
});
// Create Teacher
await Teacher.create(teacherData);
if (operator) {
await OperationLog.add({
user_id: operator.user_id,
type: '教师管理',
target: `teacher:${id}`,
description: `创建了教师: ${name}(${id})`,
ip: operator.ip
});
}
return id;
}
static async updateTeacher(id, data, operator) {
await Teacher.update(id, data);
// Sync User table (name, department)
const userFields = [];
const userValues = [];
if (data.name) { userFields.push('name = ?'); userValues.push(data.name); }
if (data.department) { userFields.push('class = ?'); userValues.push(data.department); }
if (userFields.length > 0) {
userValues.push(id);
await db.query(`UPDATE users SET ${userFields.join(', ')} WHERE id = ?`, userValues);
}
if (operator) {
await OperationLog.add({
user_id: operator.user_id,
type: '教师管理',
target: `teacher:${id}`,
description: `修改了教师信息: ${id}`,
ip: operator.ip
});
}
return true;
}
static async deleteTeacher(id, operator) {
await Teacher.delete(id);
await db.query('DELETE FROM users WHERE id = ?', [id]);
if (operator) {
await OperationLog.add({
user_id: operator.user_id,
type: '教师管理',
target: `teacher:${id}`,
description: `删除了教师: ${id}`,
ip: operator.ip
});
}
return true;
}
// ================= Grade Statistics =================
static async getGradeStats() {
const sql = `
SELECT
c.course_code,
c.course_name,
u.name as teacher_name,
COUNT(g.student_id) as student_count,
AVG(g.total_score) as avg_score,
MAX(g.total_score) as max_score,
MIN(g.total_score) as min_score,
SUM(CASE WHEN g.total_score >= 60 THEN 1 ELSE 0 END) as pass_count
FROM courses c
LEFT JOIN grades g ON c.id = g.course_id
LEFT JOIN users u ON c.teacher_id = u.id
GROUP BY c.id
ORDER BY avg_score DESC
`;
const rows = await db.query(sql);
return rows.map(row => ({
...row,
pass_rate: row.student_count > 0 ? ((row.pass_count / row.student_count) * 100).toFixed(1) : 0,
avg_score: row.avg_score ? Number(row.avg_score).toFixed(1) : 0
}));
}
// ================= System Settings =================
static async getSettings() {
return await SystemSetting.getAll();
}
static async saveSettings(settings, operator) {
await SystemSetting.setMany(settings);
await OperationLog.add({
user_id: operator.user_id,
type: '系统设置',
target: 'system_settings',
description: '修改了系统基础设置',
ip: operator.ip
});
return true;
}
// ================= Data Maintenance =================
static async backupDatabase(operator) {
const path = require('path');
const fs = require('fs');
const source = path.resolve(__dirname, '../database.sqlite');
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupDir = path.resolve(__dirname, '../backups');
const target = path.resolve(backupDir, `database-${timestamp}.sqlite`);
if (!fs.existsSync(backupDir)) {
fs.mkdirSync(backupDir);
}
fs.copyFileSync(source, target);
await OperationLog.add({
user_id: operator.user_id,
type: '数据维护',
target: 'database',
description: `手动备份数据库: ${path.basename(target)}`,
ip: operator.ip
});
return { filename: path.basename(target) };
}
static async clearCache(operator) {
await OperationLog.add({
user_id: operator.user_id,
type: '数据维护',
target: 'cache',
description: '清理系统缓存',
ip: operator.ip
});
return true;
}
static async resetStudentPasswords(operator) {
const hashedPassword = await bcrypt.hash('123456', 10);
await db.query('UPDATE users SET password = ? WHERE role = "student"', [hashedPassword]);
await OperationLog.add({
user_id: operator.user_id,
type: '数据维护',
target: 'users',
description: '重置了所有学生的密码为 123456',
ip: operator.ip
});
return true;
}
// ================= Operation Logs =================
static async getLogs(params) {
return await OperationLog.findAll(params);
}
// ================= Data Export =================
static async exportStudents() {
return await db.query('SELECT * FROM students');
}
static async exportTeachers() {
return await db.query('SELECT t.*, u.created_at FROM teachers t JOIN users u ON t.id = u.id');
}
static async exportGrades() {
const sql = `
SELECT
g.student_id,
s.name as student_name,
c.course_code,
c.course_name,
g.total_score,
g.grade_point,
g.grade_level,
u.name as teacher_name
FROM grades g
JOIN students s ON g.student_id = s.id
JOIN courses c ON g.course_id = c.id
JOIN users u ON g.teacher_id = u.id
`;
return await db.query(sql);
}
}

View File

@@ -70,6 +70,36 @@ class AuthService {
return await User.updatePassword(userId, newPassword);
}
static async updateProfile(userId, updateData) {
await User.updateProfile(userId, updateData);
const updatedUser = await User.findById(userId);
// 如果是学生,同步更新 students 表
if (updatedUser.role === 'student') {
const studentFields = [];
const studentParams = [];
if (updateData.name) {
studentFields.push('name = ?');
studentParams.push(updateData.name);
}
if (updateData.class) {
studentFields.push('class = ?');
studentParams.push(updateData.class);
}
if (studentFields.length > 0) {
studentParams.push(userId);
await db.query(`UPDATE students SET ${studentFields.join(', ')} WHERE id = ?`, studentParams);
}
}
return {
id: updatedUser.id,
name: updatedUser.name,
role: updatedUser.role,
class: updatedUser.class
};
}
}
module.exports = AuthService;

View File

@@ -7,8 +7,39 @@ class TeacherService {
return await Course.findByTeacherId(teacherId);
}
static async getClasses() {
return await Course.getClasses();
}
static async getTeacherClasses(teacherId) {
const sql = `SELECT * FROM classes WHERE teacher_id = ?`;
const db = require('../config/database');
return await db.query(sql, [teacherId]);
}
static async createCourse(teacherId, courseData) {
return await Course.create({ ...courseData, teacher_id: teacherId });
}
static async updateCourse(teacherId, courseId, courseData) {
// Verify ownership
const course = await Course.findById(courseId);
if (!course || course.teacher_id != teacherId) {
throw new Error('无权修改该课程或课程不存在');
}
return await Course.update(courseId, courseData);
}
static async getGrades(teacherId, filters) {
if (filters.courseId) {
return await Score.findCourseStudentsWithGrades(filters.courseId, teacherId);
} else {
return await Score.findByTeacher(teacherId, filters);
}
}
static async addScore(teacherId, scoreData) {
const { studentId, courseId, score } = scoreData;
let { studentId, courseId, score, usual_score, midterm_score, final_score } = scoreData;
// 验证学生
const student = await Student.findById(studentId);
@@ -16,13 +47,12 @@ class TeacherService {
throw new Error('学生不存在');
}
// 验证课程(可选:验证是否是该教师的课程)
// const course = await Course.findById(courseId);
// 检查重复
const existingScore = await Score.findByStudentAndCourse(studentId, courseId);
if (existingScore) {
throw new Error('该学生此课程成绩已存在');
// 如果没有总分但有平时/期中/期末分,尝试计算总分 (30% + 30% + 40%)
if ((score === undefined || score === '') && (usual_score || midterm_score || final_score)) {
const u = parseFloat(usual_score) || 0;
const m = parseFloat(midterm_score) || 0;
const f = parseFloat(final_score) || 0;
score = (u * 0.3 + m * 0.3 + f * 0.4).toFixed(1);
}
// 计算绩点和等级
@@ -30,19 +60,33 @@ class TeacherService {
let gradePoint = 0;
let gradeLevel = 'F';
if (numericScore >= 90) { gradePoint = 4.0; gradeLevel = 'A'; }
else if (numericScore >= 80) { gradePoint = 3.0; gradeLevel = 'B'; }
else if (numericScore >= 70) { gradePoint = 2.0; gradeLevel = 'C'; }
else if (numericScore >= 60) { gradePoint = 1.0; gradeLevel = 'D'; }
if (!isNaN(numericScore)) {
if (numericScore >= 90) { gradePoint = 4.0; gradeLevel = 'A'; }
else if (numericScore >= 85) { gradePoint = 3.7; gradeLevel = 'A-'; }
else if (numericScore >= 82) { gradePoint = 3.3; gradeLevel = 'B+'; }
else if (numericScore >= 78) { gradePoint = 3.0; gradeLevel = 'B'; }
else if (numericScore >= 75) { gradePoint = 2.7; gradeLevel = 'B-'; }
else if (numericScore >= 72) { gradePoint = 2.3; gradeLevel = 'C+'; }
else if (numericScore >= 68) { gradePoint = 2.0; gradeLevel = 'C'; }
else if (numericScore >= 64) { gradePoint = 1.5; gradeLevel = 'C-'; }
else if (numericScore >= 60) { gradePoint = 1.0; gradeLevel = 'D'; }
} else {
gradePoint = null;
gradeLevel = null;
}
const fullScoreData = {
...scoreData,
score, // 更新后的总分
teacherId,
gradePoint,
gradeLevel
gradeLevel,
usual_score,
midterm_score,
final_score
};
const gradeId = await Score.create(fullScoreData);
const gradeId = await Score.upsert(fullScoreData);
return gradeId;
}
}