feat: 实现教师资料更新、操作日志和系统设置功能
新增教师资料更新功能,包括个人信息修改和密码更新 添加操作日志记录系统,记录用户关键操作 实现系统设置模块,支持动态配置系统参数 重构数据库模型,新增教师表和系统设置表 优化成绩录入逻辑,支持平时分、期中和期末成绩计算 添加数据导出功能,支持学生、教师和成绩数据导出 完善管理员后台,增加统计图表和操作日志查看
This commit is contained in:
BIN
backend/backups/database-2025-12-22T15-15-32-013Z.sqlite
Normal file
BIN
backend/backups/database-2025-12-22T15-15-32-013Z.sqlite
Normal file
Binary file not shown.
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
BIN
backend/database.sqlite
Normal file
Binary file not shown.
343
backend/init_db.js
Normal file
343
backend/init_db.js
Normal 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();
|
||||
@@ -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;
|
||||
|
||||
28
backend/models/OperationLog.js
Normal file
28
backend/models/OperationLog.js
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
32
backend/models/SystemSetting.js
Normal file
32
backend/models/SystemSetting.js
Normal 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
38
backend/models/Teacher.js
Normal 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;
|
||||
@@ -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
1445
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
31
backend/scripts/fix_teachers.js
Normal file
31
backend/scripts/fix_teachers.js
Normal 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();
|
||||
48
backend/scripts/migrate_v2.js
Normal file
48
backend/scripts/migrate_v2.js
Normal 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();
|
||||
33
backend/scripts/migrate_v3.js
Normal file
33
backend/scripts/migrate_v3.js
Normal 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();
|
||||
@@ -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 ---
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user