diff --git a/backend/controllers/authController.js b/backend/controllers/authController.js index b811c2f..5aa9d07 100644 --- a/backend/controllers/authController.js +++ b/backend/controllers/authController.js @@ -14,7 +14,7 @@ class AuthController { // 设置 Session req.session.user = user; - success(res, user, '登录成功'); + success(res, { user }, '登录成功'); } catch (err) { if (err.message === '用户名或密码错误') { return error(res, err.message, 401); diff --git a/backend/models/Score.js b/backend/models/Score.js index 378eeb5..3f4fbd3 100644 --- a/backend/models/Score.js +++ b/backend/models/Score.js @@ -3,9 +3,11 @@ const db = require('../config/database'); class Score { static async findByStudentId(studentId) { const sql = ` - SELECT s.*, c.course_code, c.course_name, c.credit, + SELECT s.id, s.student_id, s.course_id, s.teacher_id, + s.total_score as score, s.grade_point, s.grade_level, s.created_at, + c.course_code, c.course_name, c.credit, u.name as teacher_name - FROM scores s + FROM grades s JOIN courses c ON s.course_id = c.id JOIN users u ON s.teacher_id = u.id WHERE s.student_id = ? @@ -16,10 +18,13 @@ class Score { static async findDetailsById(scoreId, studentId) { const sql = ` - SELECT s.*, c.course_code, c.course_name, c.credit, c.semester, + SELECT s.id, s.student_id, s.course_id, s.teacher_id, + s.usual_score, s.midterm_score, s.final_score, s.total_score as score, + s.grade_point, s.grade_level, s.created_at, s.remark, + c.course_code, c.course_name, c.credit, c.semester, u.name as teacher_name, u.email as teacher_email, st.id as student_number, st.class as class_name - FROM scores s + FROM grades s JOIN courses c ON s.course_id = c.id JOIN users u ON s.teacher_id = u.id JOIN students st ON s.student_id = st.id @@ -30,22 +35,23 @@ class Score { } static async create(scoreData) { - const { studentId, courseId, teacherId, score, gradePoint, gradeLevel, examDate, remark } = scoreData; + const { studentId, courseId, teacherId, score, gradePoint, gradeLevel, remark } = scoreData; + // 既然TeacherService只传了score,我们默认把它作为 final_score 和 total_score + // 如果需要支持平时分/期中分,需要修改前端和Controller const sql = ` - INSERT INTO scores (student_id, course_id, teacher_id, score, + INSERT INTO grades (student_id, course_id, teacher_id, final_score, total_score, grade_point, grade_level, created_at, remark) - VALUES (?, ?, ?, ?, ?, ?, NOW(), ?) + VALUES (?, ?, ?, ?, ?, ?, ?, NOW(), ?) `; - // 注意:这里用 created_at 替代了 examDate,如果数据库有 examDate 列可以改回去 const result = await db.pool.execute(sql, [ - studentId, courseId, teacherId, score, gradePoint, gradeLevel, remark + studentId, courseId, teacherId, score, score, gradePoint, gradeLevel, remark ]); return result[0].insertId; } static async findByStudentAndCourse(studentId, courseId) { const rows = await db.query( - 'SELECT * FROM scores WHERE student_id = ? AND course_id = ?', + 'SELECT * FROM grades WHERE student_id = ? AND course_id = ?', [studentId, courseId] ); return rows[0]; diff --git a/backend/models/Student.js b/backend/models/Student.js index 3322357..2a48811 100644 --- a/backend/models/Student.js +++ b/backend/models/Student.js @@ -1,21 +1,16 @@ const db = require('../config/database'); class Student { - static async findByUserId(userId) { - const students = await db.query('SELECT * FROM students WHERE user_id = ?', [userId]); - return students[0]; - } - - static async findById(studentId) { - const students = await db.query('SELECT * FROM students WHERE student_id = ?', [studentId]); + static async findById(id) { + const students = await db.query('SELECT * FROM students WHERE id = ?', [id]); return students[0]; } static async create(studentData) { - const { id, name, className, userId } = studentData; + const { id, name, className } = studentData; await db.query( - 'INSERT INTO students (id, name, class, user_id) VALUES (?, ?, ?, ?)', - [id, name, className, userId] + 'INSERT INTO students (id, name, class) VALUES (?, ?, ?)', + [id, name, className] ); } } diff --git a/backend/scripts/backup.js b/backend/scripts/backup.js new file mode 100644 index 0000000..4a700c8 --- /dev/null +++ b/backend/scripts/backup.js @@ -0,0 +1,62 @@ +const fs = require('fs'); +const path = require('path'); +const db = require('../config/database'); + +async function backup() { + console.log('开始备份数据库...'); + const backupDir = path.join(__dirname, '../../database'); + const filename = `backup_${new Date().toISOString().replace(/[:.]/g, '-')}.sql`; + const filepath = path.join(backupDir, filename); + + if (!fs.existsSync(backupDir)) { + fs.mkdirSync(backupDir, { recursive: true }); + } + + try { + const fileStream = fs.createWriteStream(filepath, { flags: 'a' }); + + // 获取所有表 + const tables = await db.query('SHOW TABLES'); + const tableNames = tables.map(t => Object.values(t)[0]); + + for (const tableName of tableNames) { + console.log(`正在备份表: ${tableName}`); + + // 1. 获取 CREATE TABLE 语句 + const createRows = await db.query(`SHOW CREATE TABLE ${tableName}`); + const createTableSql = createRows[0]['Create Table']; + + fileStream.write(`\n\n-- Table structure for table \`${tableName}\`\n`); + fileStream.write(`DROP TABLE IF EXISTS \`${tableName}\`;\n`); + fileStream.write(`${createTableSql};\n`); + + // 2. 获取数据 + const rows = await db.query(`SELECT * FROM ${tableName}`); + if (rows.length > 0) { + fileStream.write(`\n-- Dumping data for table \`${tableName}\`\n`); + fileStream.write(`INSERT INTO \`${tableName}\` VALUES\n`); + + const values = rows.map(row => { + const rowValues = Object.values(row).map(val => { + if (val === null) return 'NULL'; + if (typeof val === 'number') return val; + // 转义单引号和反斜杠 + return `'${String(val).replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`; + }); + return `(${rowValues.join(', ')})`; + }); + + fileStream.write(values.join(',\n') + ';\n'); + } + } + + fileStream.end(); + console.log(`备份完成! 文件保存在: ${filepath}`); + process.exit(0); + } catch (error) { + console.error('备份失败:', error); + process.exit(1); + } +} + +backup(); \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index 804f690..a8b9c89 100644 --- a/backend/server.js +++ b/backend/server.js @@ -96,6 +96,9 @@ app.get('/dashboard', requirePageAuth, (req, res) => { app.get('/student/dashboard', requirePageAuth, requirePageRole(['student']), (req, res) => { res.sendFile(path.join(__dirname, '../frontend/views/student/dashboard.html')); }); +app.get('/student/profile', requirePageAuth, requirePageRole(['student']), (req, res) => { + res.sendFile(path.join(__dirname, '../frontend/views/student/profile.html')); +}); // Teacher Pages const teacherPageRouter = express.Router(); diff --git a/database/backup_2025-12-21T14-30-15-873Z.sql b/database/backup_2025-12-21T14-30-15-873Z.sql new file mode 100644 index 0000000..e69de29 diff --git a/database/backup_2025-12-21T14-30-37-825Z.sql b/database/backup_2025-12-21T14-30-37-825Z.sql new file mode 100644 index 0000000..4dd7142 --- /dev/null +++ b/database/backup_2025-12-21T14-30-37-825Z.sql @@ -0,0 +1,177 @@ + + +-- Table structure for table `classes` +DROP TABLE IF EXISTS `classes`; +CREATE TABLE `classes` ( + `id` int NOT NULL AUTO_INCREMENT COMMENT '班级ID', + `class_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '班级名称', + `grade` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '年级', + `major` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '专业', + `teacher_id` int DEFAULT NULL COMMENT '班主任ID', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_class_name` (`class_name`) USING BTREE, + KEY `idx_teacher_id` (`teacher_id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='班级表'; + +-- Dumping data for table `classes` +INSERT INTO `classes` VALUES +(1, '计算机2301', '2023', '计算机科学与技术', 2001, 'Sun Dec 21 2025 14:23:14 GMT+0800 (中国标准时间)', 'Sun Dec 21 2025 14:23:14 GMT+0800 (中国标准时间)'), +(2, '软件工程2302', '2023', '软件工程', 2002, 'Sun Dec 21 2025 14:23:14 GMT+0800 (中国标准时间)', 'Sun Dec 21 2025 14:23:14 GMT+0800 (中国标准时间)'); + + +-- Table structure for table `courses` +DROP TABLE IF EXISTS `courses`; +CREATE TABLE `courses` ( + `id` int NOT NULL AUTO_INCREMENT COMMENT '课程ID', + `course_code` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '课程代码', + `course_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '课程名称', + `credit` decimal(3,1) NOT NULL DEFAULT '2.0' COMMENT '学分', + `teacher_id` int NOT NULL COMMENT '授课教师ID', + `class_id` int NOT NULL COMMENT '授课班级ID', + `semester` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '学期', + `academic_year` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '学年', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `course_code` (`course_code`) USING BTREE, + KEY `idx_course_code` (`course_code`) USING BTREE, + KEY `idx_teacher_id` (`teacher_id`) USING BTREE, + KEY `idx_class_id` (`class_id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='课程表'; + +-- Dumping data for table `courses` +INSERT INTO `courses` VALUES +(1, 'CS101', '高级程序设计', '4.0', 2001, 1, NULL, NULL, 'Sun Dec 21 2025 14:23:14 GMT+0800 (中国标准时间)', 'Sun Dec 21 2025 14:23:14 GMT+0800 (中国标准时间)'), +(2, 'SE201', '软件工程导论', '3.0', 2002, 2, NULL, NULL, 'Sun Dec 21 2025 14:23:14 GMT+0800 (中国标准时间)', 'Sun Dec 21 2025 14:23:14 GMT+0800 (中国标准时间)'), +(3, 'MAT101', '高等数学', '5.0', 2003, 1, NULL, NULL, 'Sun Dec 21 2025 14:23:14 GMT+0800 (中国标准时间)', 'Sun Dec 21 2025 14:23:14 GMT+0800 (中国标准时间)'); + + +-- Table structure for table `grades` +DROP TABLE IF EXISTS `grades`; +CREATE TABLE `grades` ( + `id` int NOT NULL AUTO_INCREMENT COMMENT '成绩ID', + `student_id` int NOT NULL COMMENT '学生ID', + `course_id` int NOT NULL COMMENT '课程ID', + `usual_score` decimal(5,2) DEFAULT NULL COMMENT '平时成绩', + `midterm_score` decimal(5,2) DEFAULT NULL COMMENT '期中成绩', + `final_score` decimal(5,2) DEFAULT NULL COMMENT '期末成绩', + `total_score` decimal(5,2) DEFAULT NULL COMMENT '总评成绩', + `grade_point` decimal(3,2) DEFAULT NULL COMMENT '绩点', + `grade_level` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '成绩等级(A/B/C/D/F)', + `teacher_id` int NOT NULL COMMENT '录入教师ID', + `remark` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT '备注', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uk_student_course` (`student_id`,`course_id`) USING BTREE, + KEY `idx_student_id` (`student_id`) USING BTREE, + KEY `idx_course_id` (`course_id`) USING BTREE, + KEY `idx_teacher_id` (`teacher_id`) USING BTREE, + KEY `idx_total_score` (`total_score`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='成绩表'; + +-- Dumping data for table `grades` +INSERT INTO `grades` VALUES +(1, 3001, 1, '90.00', '85.00', '88.00', '87.70', '3.00', NULL, 2001, NULL, 'Sun Dec 21 2025 14:23:14 GMT+0800 (中国标准时间)', 'Sun Dec 21 2025 14:23:14 GMT+0800 (中国标准时间)'), +(2, 3001, 3, '80.00', '75.00', '82.00', '79.30', '2.00', NULL, 2003, NULL, 'Sun Dec 21 2025 14:23:14 GMT+0800 (中国标准时间)', 'Sun Dec 21 2025 14:23:14 GMT+0800 (中国标准时间)'), +(3, 3002, 1, '95.00', '92.00', '94.00', '93.70', '4.00', NULL, 2001, NULL, 'Sun Dec 21 2025 14:23:14 GMT+0800 (中国标准时间)', 'Sun Dec 21 2025 14:23:14 GMT+0800 (中国标准时间)'), +(4, 3003, 2, '88.00', '80.00', '85.00', '84.40', '3.00', NULL, 2002, NULL, 'Sun Dec 21 2025 14:23:14 GMT+0800 (中国标准时间)', 'Sun Dec 21 2025 14:23:14 GMT+0800 (中国标准时间)'); + + +-- Table structure for table `operation_logs` +DROP TABLE IF EXISTS `operation_logs`; +CREATE TABLE `operation_logs` ( + `id` int NOT NULL AUTO_INCREMENT COMMENT '日志ID', + `user_id` int NOT NULL COMMENT '操作用户ID', + `operation_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '操作类型', + `operation_target` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '操作目标', + `operation_details` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT '操作详情', + `ip_address` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT 'IP地址', + `user_agent` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT '用户代理', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_user_id` (`user_id`) USING BTREE, + KEY `idx_operation_type` (`operation_type`) USING BTREE, + KEY `idx_created_at` (`created_at`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='操作日志表'; + + +-- Table structure for table `scores` +DROP TABLE IF EXISTS `scores`; +CREATE TABLE `scores` ( + `id` int NOT NULL AUTO_INCREMENT COMMENT '成绩记录ID', + `student_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '学生ID', + `course` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '课程名称', + `score` decimal(5,2) NOT NULL COMMENT '成绩', + `teacher_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '教师ID', + `class` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '班级', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_student_id` (`student_id`) USING BTREE, + KEY `idx_teacher_id` (`teacher_id`) USING BTREE, + KEY `idx_class` (`class`) USING BTREE, + CONSTRAINT `scores_ibfk_1` FOREIGN KEY (`student_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `scores_ibfk_2` FOREIGN KEY (`teacher_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `scores_chk_1` CHECK (((`score` >= 0) and (`score` <= 100))) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='成绩记录表'; + + +-- Table structure for table `sessions` +DROP TABLE IF EXISTS `sessions`; +CREATE TABLE `sessions` ( + `session_id` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `expires` int unsigned NOT NULL, + `data` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin, + PRIMARY KEY (`session_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC; + +-- Dumping data for table `sessions` +INSERT INTO `sessions` VALUES +('KY6QaavAiws7rkdEBFIFDoHefl2bxzlI', 1766334525, '{"cookie":{"originalMaxAge":86400000,"expires":"2025-12-21T16:28:44.650Z","secure":false,"httpOnly":true,"path":"/"},"user":{"id":"teststudent","name":"????","role":"student","class":"2023?1?"}}'), +('KaDFs4HogLmkjS0HAs6qki6g2FmE3sTL', 1766334465, '{"cookie":{"originalMaxAge":86400000,"expires":"2025-12-21T16:27:45.314Z","secure":false,"httpOnly":true,"path":"/"},"user":{"id":"teststudent","name":"????","role":"student","class":"2023?1?"}}'), +('P-_AKfysnsUwA6PypoYdyXCxa-_8TiCE', 1766413502, '{"cookie":{"originalMaxAge":86400000,"expires":"2025-12-22T14:25:01.323Z","secure":false,"httpOnly":true,"path":"/"},"user":{"id":"3001","name":"陈同学","role":"student","class":"计算机2301","studentInfo":{"id":"3001","name":"陈同学","class":"计算机2301"}}}'), +('QAhXDQ1FOlhU6RhaFOm3ghRtLOW4hBTd', 1766334812, '{"cookie":{"originalMaxAge":86400000,"expires":"2025-12-21T16:33:31.987Z","secure":false,"httpOnly":true,"path":"/"},"user":{"id":"teststudent","name":"????","role":"student","class":"2023?1?"}}'), +('SAuQyktAI9gAHpXbjARpe-9BL42pDRiV', 1766334764, '{"cookie":{"originalMaxAge":86400000,"expires":"2025-12-21T16:32:43.695Z","secure":false,"httpOnly":true,"path":"/"},"user":{"id":"teststudent","name":"????","role":"student","class":"2023?1?"}}'), +('XduN1lYhGPeIaLTHbLTNVnTCBtKUCkJR', 1766334689, '{"cookie":{"originalMaxAge":86400000,"expires":"2025-12-21T16:31:28.994Z","secure":false,"httpOnly":true,"path":"/"},"user":{"id":"teststudent","name":"????","role":"student","class":"2023?1?"}}'), +('Y59PFvvqK7M0DKZshc6ONTmFQjzGyMmV', 1766334426, '{"cookie":{"originalMaxAge":86400000,"expires":"2025-12-21T16:27:05.673Z","secure":false,"httpOnly":true,"path":"/"},"user":{"id":"teststudent","name":"????","role":"student","class":"2023?1?"}}'), +('rlscT2Pi2EAyLXHs1CNXyQmNSiW8vEo4', 1766334271, '{"cookie":{"originalMaxAge":86400000,"expires":"2025-12-21T16:24:30.682Z","secure":false,"httpOnly":true,"path":"/"},"user":{"id":"teststudent","name":"????","role":"student","class":"2023?1?"}}'), +('rsaOCJRjYQLPtUWlDmUFJgWcCYZbOCgJ', 1766410574, '{"cookie":{"originalMaxAge":86400000,"expires":"2025-12-21T15:54:39.935Z","secure":false,"httpOnly":true,"path":"/"},"user":{"id":"123","name":"经济局","role":"student","class":"123"}}'), +('wXxRpNTGY0wqLaHsebSAsw1I6Pb7Ed6w', 1766410584, '{"cookie":{"originalMaxAge":86400000,"expires":"2025-12-22T13:35:43.191Z","secure":false,"httpOnly":true,"path":"/"},"user":{"id":"567","name":"急急急","role":"teacher","class":"567"}}'); + + +-- Table structure for table `students` +DROP TABLE IF EXISTS `students`; +CREATE TABLE `students` ( + `id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '学生ID(与users表id一致)', + `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '姓名', + `class` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '班级', + PRIMARY KEY (`id`) USING BTREE, + CONSTRAINT `students_ibfk_1` FOREIGN KEY (`id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='学生详细信息表'; + +-- Dumping data for table `students` +INSERT INTO `students` VALUES +('3001', '陈同学', '计算机2301'), +('3002', '林同学', '计算机2301'), +('3003', '黄同学', '软件工程2302'), +('3004', '吴同学', '软件工程2302'); + + +-- Table structure for table `users` +DROP TABLE IF EXISTS `users`; +CREATE TABLE `users` ( + `id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户ID(学号/工号)', + `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '姓名', + `password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '密码(bcrypt加密)', + `role` enum('student','teacher','admin') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '角色', + `class` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '班级(学生和教师需要,管理员为NULL)', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_role` (`role`) USING BTREE, + KEY `idx_class` (`class`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='用户表'; + +-- Dumping data for table `users` diff --git a/frontend/public/js/auth.js b/frontend/public/js/auth.js index ef33b8c..41c3d2c 100644 --- a/frontend/public/js/auth.js +++ b/frontend/public/js/auth.js @@ -1,6 +1,7 @@ /** * 认证模块管理器 * 处理登录、注册、注销及权限检查 + * 适配 Bootstrap 5 样式 */ class AuthManager { constructor() { @@ -15,21 +16,14 @@ class AuthManager { } /** - * 创建通知容器 + * 创建通知容器 (适配 Bootstrap Toast) */ createNotificationContainer() { if (!document.getElementById('notification-container')) { const container = document.createElement('div'); container.id = 'notification-container'; - container.style.cssText = ` - position: fixed; - top: 20px; - right: 20px; - z-index: 9999; - display: flex; - flex-direction: column; - gap: 10px; - `; + container.className = 'toast-container position-fixed top-0 end-0 p-3'; + container.style.zIndex = '9999'; document.body.appendChild(container); } } @@ -38,7 +32,6 @@ class AuthManager { * 检查用户认证状态 */ async checkAuthStatus() { - // 如果当前是公共页面,可以选择不检查,或者检查后更新UI const currentPath = window.location.pathname; const isAuthPage = currentPath.includes('/login') || currentPath.includes('/register'); @@ -46,20 +39,11 @@ class AuthManager { const response = await fetch(`${this.apiBase}/auth/me`); const data = await response.json(); - if (data.success && data.user) { - // 用户已登录 - const redirectUrl = this.getDashboardUrl(data.user.role); - - // 如果在登录/注册页,跳转到仪表板 + if (data.success && data.data && data.data.user) { + const redirectUrl = this.getDashboardUrl(data.data.user.role); if (isAuthPage || currentPath === '/') { window.location.href = redirectUrl; } - } else { - // 用户未登录,如果在受保护页面,跳转到登录页 - // 注意:后端通常已经处理了重定向,这里是前端的额外保障 - if (!isAuthPage && currentPath !== '/') { - // 可以在这里添加逻辑,但通常交给后端控制 - } } } catch (error) { console.error('Auth check failed:', error); @@ -94,7 +78,7 @@ class AuthManager { } } - // 注销按钮 (可能有多个,例如在导航栏) + // 注销按钮 document.querySelectorAll('.btn-logout, #logoutBtn').forEach(btn => { btn.addEventListener('click', (e) => this.handleLogout(e)); }); @@ -107,12 +91,12 @@ class AuthManager { if (classField && classInput) { if (role === 'student' || role === 'teacher') { - classField.style.display = 'block'; + classField.style.display = 'flex'; // 配合 Bootstrap input-group classInput.required = true; } else { classField.style.display = 'none'; classInput.required = false; - classInput.value = ''; // 清空值 + classInput.value = ''; } } } @@ -120,9 +104,9 @@ class AuthManager { async handleLogin(e) { e.preventDefault(); const form = e.target; - const submitBtn = form.querySelector('button[type="submit"]'); + const submitBtn = document.getElementById('loginBtn') || form.querySelector('button[type="submit"]'); - if (this.setLoading(submitBtn, true, '登录中...')) { + if (this.setLoading(submitBtn, true)) { try { const formData = new FormData(form); const data = Object.fromEntries(formData.entries()); @@ -137,16 +121,27 @@ class AuthManager { if (result.success) { this.showNotification('登录成功,正在跳转...', 'success'); - setTimeout(() => { - window.location.href = this.getDashboardUrl(result.user.role); - }, 1000); + + // 获取用户信息,优先从 result.data.user 获取,兼容旧格式 result.data 或 result.user + const user = (result.data && result.data.user) || result.data || result.user; + const role = user ? user.role : null; + + if (role) { + setTimeout(() => { + window.location.href = this.getDashboardUrl(role); + }, 1000); + } else { + console.error('Login success but role not found:', result); + this.showNotification('登录状态异常,请重试', 'error'); + this.setLoading(submitBtn, false); + } } else { this.showNotification(result.message || '登录失败', 'error'); this.setLoading(submitBtn, false); } } catch (error) { console.error('Login error:', error); - this.showNotification('网络错误,请稍后重试', 'error'); + this.showNotification('服务器连接失败,请稍后重试', 'error'); this.setLoading(submitBtn, false); } } @@ -155,19 +150,17 @@ class AuthManager { async handleRegister(e) { e.preventDefault(); const form = e.target; - const submitBtn = form.querySelector('button[type="submit"]'); + const submitBtn = document.getElementById('registerBtn') || form.querySelector('button[type="submit"]'); - // 获取数据 const formData = new FormData(form); const data = Object.fromEntries(formData.entries()); - // 简单验证 if (data.password !== data.confirmPassword) { this.showNotification('两次输入的密码不一致', 'error'); return; } - if (this.setLoading(submitBtn, true, '注册中...')) { + if (this.setLoading(submitBtn, true)) { try { const response = await fetch(`${this.apiBase}/auth/register`, { method: 'POST', @@ -188,7 +181,7 @@ class AuthManager { } } catch (error) { console.error('Register error:', error); - this.showNotification('网络错误,请稍后重试', 'error'); + this.showNotification('服务器连接失败,请稍后重试', 'error'); this.setLoading(submitBtn, false); } } @@ -196,24 +189,17 @@ class AuthManager { async handleLogout(e) { e.preventDefault(); - if (confirm('确定要退出登录吗?')) { try { const response = await fetch(`${this.apiBase}/auth/logout`, { method: 'POST' }); - const result = await response.json(); - if (result.success) { - this.showNotification('已退出登录', 'success'); - setTimeout(() => { - window.location.href = '/login'; - }, 1000); + window.location.href = '/login'; } } catch (error) { console.error('Logout error:', error); - // 即使出错也强制跳转到登录页 window.location.href = '/login'; } } @@ -221,67 +207,68 @@ class AuthManager { /** * 设置按钮加载状态 - * @param {HTMLElement} btn 按钮元素 - * @param {boolean} isLoading 是否正在加载 - * @param {string} text 加载时的文本 - * @returns {boolean} true表示状态设置成功 */ - setLoading(btn, isLoading, text = '') { + setLoading(btn, isLoading) { if (!btn) return false; + const spinner = btn.querySelector('.spinner-border'); if (isLoading) { - if (btn.dataset.loading) return false; // 防止重复提交 - btn.dataset.loading = 'true'; - btn.dataset.originalText = btn.innerHTML; - btn.innerHTML = ` ${text}`; + if (btn.disabled) return false; btn.disabled = true; + if (spinner) spinner.classList.remove('d-none'); } else { - btn.innerHTML = btn.dataset.originalText || btn.innerHTML; - delete btn.dataset.loading; btn.disabled = false; + if (spinner) spinner.classList.add('d-none'); } return true; } /** - * 显示通知 - * @param {string} message 消息内容 - * @param {string} type 消息类型 'success' | 'error' | 'info' + * 显示通知 (Bootstrap 5 Toast) */ showNotification(message, type = 'info') { const container = document.getElementById('notification-container'); if (!container) return; - const notification = document.createElement('div'); - notification.className = `notification ${type}`; + const iconMap = { + success: 'fa-check-circle', + error: 'fa-exclamation-circle', + warning: 'fa-exclamation-triangle', + info: 'fa-info-circle' + }; - let icon = 'info-circle'; - if (type === 'success') icon = 'check-circle'; - if (type === 'error') icon = 'exclamation-circle'; - - notification.innerHTML = ` - - ${message} + const bgMap = { + success: 'bg-success', + error: 'bg-danger', + warning: 'bg-warning', + info: 'bg-info' + }; + + const toastId = 'toast-' + Date.now(); + const toastHtml = ` +
你还没有任何成绩记录
-- 学生 -
-${profile.student_id}
-${profile.class_name}
-${profile.major || '未设?}
-${profile.enrollment_year || '未设?}
-${grade.remark}
-暂无成绩数据
+ courseList.innerHTML = courses.map(course => ` +请登录您的账户
-