commit 352698044b2df8169633e4be619a47505765430f
Author: 祀梦 <3501646051@qq.com>
Date: Sun Dec 21 21:50:37 2025 +0800
first commit
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e23eab2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,31 @@
+# Node.js
+node_modules/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.env
+
+# Python
+__pycache__/
+*.pyc
+*.pyo
+*.pyd
+.Python
+env/
+venv/
+pip-log.txt
+pip-delete-this-directory.txt
+.venv/
+.env/
+
+# OS
+.DS_Store
+Thumbs.db
+
+# IDE
+.vscode/
+.idea/
+
+# Build
+dist/
+build/
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e889523
--- /dev/null
+++ b/README.md
@@ -0,0 +1,15 @@
+# WebWork
+
+这是一个基于 Node.js 后端和原生 HTML/CSS/JS 前端的 Web 项目。
+
+## 项目结构
+
+- `backend/`: Node.js 服务端代码
+- `frontend/`: 前端静态资源
+- `database/`: 数据库初始化脚本
+
+## 快速开始
+
+1. 进入 `backend` 目录,运行 `npm install` 安装依赖。
+2. 配置 `.env` 文件(参考 `.env.example` 或根据需要创建)。
+3. 启动服务器:`npm start`。
diff --git a/backend/config/database.js b/backend/config/database.js
new file mode 100644
index 0000000..5e39cf1
--- /dev/null
+++ b/backend/config/database.js
@@ -0,0 +1,64 @@
+const mysql = require('mysql2/promise');
+require('dotenv').config();
+
+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
+});
+
+// 测试数据库连接
+async function testConnection() {
+ try {
+ const connection = await pool.getConnection();
+ console.log('数据库连接成功');
+ connection.release();
+ return true;
+ } catch (error) {
+ console.error('数据库连接失败:', error.message);
+ return false;
+ }
+}
+
+// 执行查询
+async function query(sql, params) {
+ try {
+ const [rows] = await pool.execute(sql, params);
+ return rows;
+ } catch (error) {
+ console.error('数据库查询错误:', error.message);
+ throw error;
+ }
+}
+
+// 执行事务
+async function executeTransaction(operations) {
+ const connection = await pool.getConnection();
+ try {
+ await connection.beginTransaction();
+
+ for (const operation of operations) {
+ await connection.execute(operation.sql, operation.params);
+ }
+
+ await connection.commit();
+ console.log('事务执行成功');
+ } catch (error) {
+ await connection.rollback();
+ console.error('事务执行失败:', error.message);
+ throw error;
+ } finally {
+ connection.release();
+ }
+}
+
+module.exports = {
+ pool,
+ query,
+ executeTransaction,
+ testConnection
+};
\ No newline at end of file
diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js
new file mode 100644
index 0000000..e7a440a
--- /dev/null
+++ b/backend/middleware/auth.js
@@ -0,0 +1,170 @@
+const db = require('../config/database');
+
+/**
+ * 认证中间件 - 检查用户是否已登录
+ */
+const requireAuth = (req, res, next) => {
+ if (!req.session.user) {
+ return res.status(401).json({
+ success: false,
+ message: '请先登录'
+ });
+ }
+ next();
+};
+
+/**
+ * 角色权限中间件 - 检查用户是否具有指定角色
+ * @param {Array} allowedRoles - 允许的角色数组
+ */
+const requireRole = (allowedRoles) => {
+ return (req, res, next) => {
+ if (!req.session.user) {
+ return res.status(401).json({
+ success: false,
+ message: '请先登录'
+ });
+ }
+
+ const userRole = req.session.user.role;
+
+ if (!allowedRoles.includes(userRole)) {
+ return res.status(403).json({
+ success: false,
+ message: '权限不足'
+ });
+ }
+
+ next();
+ };
+};
+
+/**
+ * 获取当前用户ID
+ */
+const getCurrentUserId = (req) => {
+ return req.session.user ? req.session.user.id : null;
+};
+
+/**
+ * 获取当前用户角色
+ */
+const getCurrentUserRole = (req) => {
+ return req.session.user ? req.session.user.role : null;
+};
+
+/**
+ * 检查是否是自己的数据(学生只能访问自己的数据)
+ */
+const checkOwnership = async (req, res, next) => {
+ try {
+ const userId = getCurrentUserId(req);
+ const userRole = getCurrentUserRole(req);
+
+ // 管理员和教师可以访问所有数据
+ if (userRole === 'admin' || userRole === 'teacher') {
+ return next();
+ }
+
+ // 学生只能访问自己的数据
+ if (userRole === 'student') {
+ const studentId = req.params.studentId || req.body.studentId;
+
+ // 检查学生ID是否属于当前用户
+ const [students] = await db.execute(
+ 'SELECT id FROM students WHERE user_id = ?',
+ [userId]
+ );
+
+ if (students.length === 0) {
+ return res.status(403).json({
+ success: false,
+ message: '未找到学生信息'
+ });
+ }
+
+ const userStudentId = students[0].id;
+
+ // 如果请求中没有指定学生ID,默认使用当前用户的学生ID
+ if (!studentId) {
+ req.studentId = userStudentId;
+ return next();
+ }
+
+ // 检查请求的学生ID是否与当前用户的学生ID匹配
+ if (parseInt(studentId) !== userStudentId) {
+ return res.status(403).json({
+ success: false,
+ message: '无权访问其他学生的数据'
+ });
+ }
+
+ req.studentId = userStudentId;
+ return next();
+ }
+
+ // 其他角色无权访问
+ return res.status(403).json({
+ success: false,
+ message: '权限不足'
+ });
+
+ } catch (error) {
+ console.error('所有权检查错误:', error);
+ res.status(500).json({
+ success: false,
+ message: '服务器错误'
+ });
+ }
+};
+
+/**
+ * 输入验证中间件
+ */
+const validateInput = (schema) => {
+ return (req, res, next) => {
+ const { error } = schema.validate(req.body);
+ if (error) {
+ return res.status(400).json({
+ success: false,
+ message: error.details[0].message
+ });
+ }
+ next();
+ };
+};
+
+/**
+ * 日志记录中间件
+ */
+const logOperation = (operation) => {
+ return async (req, res, next) => {
+ try {
+ const userId = getCurrentUserId(req);
+ const userRole = getCurrentUserRole(req);
+ const ip = req.ip || req.connection.remoteAddress;
+
+ // 记录操作日志
+ await db.execute(
+ 'INSERT INTO operation_logs (user_id, operation, ip_address, user_role) VALUES (?, ?, ?, ?)',
+ [userId, operation, ip, userRole]
+ );
+
+ next();
+ } catch (error) {
+ console.error('日志记录错误:', error);
+ // 日志记录失败不影响主要操作
+ next();
+ }
+ };
+};
+
+module.exports = {
+ requireAuth,
+ requireRole,
+ getCurrentUserId,
+ getCurrentUserRole,
+ checkOwnership,
+ validateInput,
+ logOperation
+};
\ No newline at end of file
diff --git a/backend/package-lock.json b/backend/package-lock.json
new file mode 100644
index 0000000..8a956b6
--- /dev/null
+++ b/backend/package-lock.json
@@ -0,0 +1,1491 @@
+{
+ "name": "grade-management-system",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "grade-management-system",
+ "version": "1.0.0",
+ "dependencies": {
+ "bcryptjs": "^2.4.3",
+ "body-parser": "^1.20.2",
+ "cors": "^2.8.5",
+ "dotenv": "^16.3.1",
+ "express": "^4.18.2",
+ "express-mysql-session": "^3.0.0",
+ "express-session": "^1.17.3",
+ "mysql2": "^3.6.0"
+ },
+ "devDependencies": {
+ "nodemon": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "license": "MIT"
+ },
+ "node_modules/aws-ssl-profiles": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
+ "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/bcryptjs": {
+ "version": "2.4.3",
+ "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
+ "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==",
+ "license": "MIT"
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.4",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
+ "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "~1.2.0",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "on-finished": "~2.4.1",
+ "qs": "~6.14.0",
+ "raw-body": "~2.5.3",
+ "type-is": "~1.6.18",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
+ "license": "MIT"
+ },
+ "node_modules/cors": {
+ "version": "2.8.5",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/denque": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
+ "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "16.6.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.22.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
+ "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "~1.20.3",
+ "content-disposition": "~0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "~0.7.1",
+ "cookie-signature": "~1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "~1.3.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "~0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "~6.14.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "~0.19.0",
+ "serve-static": "~1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "~2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/express-mysql-session": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/express-mysql-session/-/express-mysql-session-3.0.3.tgz",
+ "integrity": "sha512-sEYrzFrOs3er+Ie/uk1dt93qz4AQ9SU1mpJJ0HPs0MJ4t4hE9AcDRNq0sZQUwy2F/SbXusBt1E5+FY6KzSqXNg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "4.3.4",
+ "mysql2": "3.10.2"
+ }
+ },
+ "node_modules/express-mysql-session/node_modules/debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/express-mysql-session/node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/express-mysql-session/node_modules/ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "license": "MIT"
+ },
+ "node_modules/express-mysql-session/node_modules/mysql2": {
+ "version": "3.10.2",
+ "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.10.2.tgz",
+ "integrity": "sha512-KCXPEvAkO0RcHPr362O5N8tFY2fXvbjfkPvRY/wGumh4EOemo9Hm5FjQZqv/pCmrnuxGu5OxnSENG0gTXqKMgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "denque": "^2.1.0",
+ "generate-function": "^2.3.1",
+ "iconv-lite": "^0.6.3",
+ "long": "^5.2.1",
+ "lru-cache": "^8.0.0",
+ "named-placeholders": "^1.1.3",
+ "seq-queue": "^0.0.5",
+ "sqlstring": "^2.3.2"
+ },
+ "engines": {
+ "node": ">= 8.0"
+ }
+ },
+ "node_modules/express-session": {
+ "version": "1.18.2",
+ "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz",
+ "integrity": "sha512-SZjssGQC7TzTs9rpPDuUrR23GNZ9+2+IkA/+IJWmvQilTr5OSliEHGF+D9scbIpdC6yGtTI0/VhaHoVes2AN/A==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "0.7.2",
+ "cookie-signature": "1.0.7",
+ "debug": "2.6.9",
+ "depd": "~2.0.0",
+ "on-headers": "~1.1.0",
+ "parseurl": "~1.3.3",
+ "safe-buffer": "5.2.1",
+ "uid-safe": "~2.1.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
+ "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "~2.0.2",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/generate-function": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
+ "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-property": "^1.0.2"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ignore-by-default": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
+ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-property": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
+ "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
+ "license": "MIT"
+ },
+ "node_modules/long": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
+ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/lru-cache": {
+ "version": "8.0.5",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz",
+ "integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=16.14"
+ }
+ },
+ "node_modules/lru.min": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.3.tgz",
+ "integrity": "sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q==",
+ "license": "MIT",
+ "engines": {
+ "bun": ">=1.0.0",
+ "deno": ">=1.30.0",
+ "node": ">=8.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wellwelwel"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/mysql2": {
+ "version": "3.16.0",
+ "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.16.0.tgz",
+ "integrity": "sha512-AEGW7QLLSuSnjCS4pk3EIqOmogegmze9h8EyrndavUQnIUcfkVal/sK7QznE+a3bc6rzPbAiui9Jcb+96tPwYA==",
+ "license": "MIT",
+ "dependencies": {
+ "aws-ssl-profiles": "^1.1.1",
+ "denque": "^2.1.0",
+ "generate-function": "^2.3.1",
+ "iconv-lite": "^0.7.0",
+ "long": "^5.2.1",
+ "lru.min": "^1.0.0",
+ "named-placeholders": "^1.1.3",
+ "seq-queue": "^0.0.5",
+ "sqlstring": "^2.3.2"
+ },
+ "engines": {
+ "node": ">= 8.0"
+ }
+ },
+ "node_modules/mysql2/node_modules/iconv-lite": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz",
+ "integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/named-placeholders": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz",
+ "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==",
+ "license": "MIT",
+ "dependencies": {
+ "lru.min": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/nodemon": {
+ "version": "3.1.11",
+ "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz",
+ "integrity": "sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chokidar": "^3.5.2",
+ "debug": "^4",
+ "ignore-by-default": "^1.0.1",
+ "minimatch": "^3.1.2",
+ "pstree.remy": "^1.1.8",
+ "semver": "^7.5.3",
+ "simple-update-notifier": "^2.0.0",
+ "supports-color": "^5.5.0",
+ "touch": "^3.1.0",
+ "undefsafe": "^2.0.5"
+ },
+ "bin": {
+ "nodemon": "bin/nodemon.js"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/nodemon"
+ }
+ },
+ "node_modules/nodemon/node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/nodemon/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/on-headers": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
+ "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
+ "license": "MIT"
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/pstree.remy": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
+ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/qs": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
+ "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/random-bytes": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
+ "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/send": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
+ "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.1",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "~2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "~2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/seq-queue": {
+ "version": "0.0.5",
+ "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz",
+ "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.3",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
+ "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "~0.19.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/simple-update-notifier": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
+ "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/sqlstring": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz",
+ "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/touch": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
+ "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "nodetouch": "bin/nodetouch.js"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/uid-safe": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
+ "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
+ "license": "MIT",
+ "dependencies": {
+ "random-bytes": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/undefsafe": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
+ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ }
+ }
+}
diff --git a/backend/package.json b/backend/package.json
new file mode 100644
index 0000000..a6e01ca
--- /dev/null
+++ b/backend/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "grade-management-system",
+ "version": "1.0.0",
+ "description": "学生成绩管理系统后端",
+ "main": "server.js",
+ "scripts": {
+ "start": "node server.js",
+ "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",
+ "dotenv": "^16.3.1",
+ "body-parser": "^1.20.2"
+ },
+ "devDependencies": {
+ "nodemon": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+}
\ No newline at end of file
diff --git a/backend/routes/admin.js b/backend/routes/admin.js
new file mode 100644
index 0000000..cf50712
--- /dev/null
+++ b/backend/routes/admin.js
@@ -0,0 +1,311 @@
+const express = require('express');
+const router = express.Router();
+const db = require('../config/database');
+const { requireAuth, requireRole } = require('../middleware/auth');
+
+/**
+ * 获取所有用户
+ */
+router.get('/users', requireAuth, requireRole(['admin']), async (req, res) => {
+ try {
+ const { page = 1, limit = 10, search = '', role = '' } = req.query;
+ const offset = (page - 1) * limit;
+
+ let query = 'SELECT id, name, role, class, created_at FROM users WHERE 1=1';
+ let params = [];
+
+ if (search) {
+ query += ' AND (id LIKE ? OR name LIKE ? OR class LIKE ?)';
+ const searchTerm = `%${search}%`;
+ params.push(searchTerm, searchTerm, searchTerm);
+ }
+
+ if (role) {
+ query += ' AND role = ?';
+ params.push(role);
+ }
+
+ // 获取总数
+ const countQuery = query.replace('SELECT id, name, role, class, created_at', 'SELECT COUNT(*) as total');
+ const countResult = await db.pool.execute(countQuery, params);
+ const total = countResult[0][0].total;
+
+ // 获取分页数据
+ query += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
+ params.push(parseInt(limit), parseInt(offset));
+
+ const users = await db.pool.execute(query, params);
+
+ res.json({
+ success: true,
+ data: users,
+ pagination: {
+ page: parseInt(page),
+ limit: parseInt(limit),
+ total,
+ pages: Math.ceil(total / limit)
+ }
+ });
+
+ } catch (error) {
+ console.error('获取用户列表错误:', error);
+ res.status(500).json({
+ success: false,
+ message: '服务器错误'
+ });
+ }
+});
+
+/**
+ * 创建用户
+ */
+router.post('/users', requireAuth, requireRole(['admin']), async (req, res) => {
+ try {
+ const { id, name, password, role, className } = req.body;
+
+ // 输入验证
+ if (!id || !name || !password || !role) {
+ return res.status(400).json({
+ success: false,
+ message: '请填写所有必填字段'
+ });
+ }
+
+ // 检查用户ID是否存在
+ const existingUsers = await db.pool.execute(
+ 'SELECT id FROM users WHERE id = ?',
+ [id]
+ );
+
+ if (existingUsers[0].length > 0) {
+ return res.status(400).json({
+ success: false,
+ message: '用户ID已存在'
+ });
+ }
+
+ // 哈希密码
+ const bcrypt = require('bcrypt');
+ const salt = await bcrypt.genSalt(10);
+ const passwordHash = await bcrypt.hash(password, salt);
+
+ // 创建用户
+ const result = await db.pool.execute(
+ 'INSERT INTO users (id, name, password, role, class) VALUES (?, ?, ?, ?, ?)',
+ [id, name, passwordHash, role, className || null]
+ );
+
+ const userId = result[0].insertId;
+
+ // 根据角色创建相关记录
+ if (role === 'student') {
+ const studentId = 'STU' + Date.now().toString().slice(-6);
+ await db.pool.execute(
+ 'INSERT INTO students (user_id, student_id, full_name, class_name) VALUES (?, ?, ?, ?)',
+ [userId, studentId, fullName, className || '未分配班级']
+ );
+ } else if (role === 'teacher') {
+ await db.pool.execute(
+ 'INSERT INTO teachers (user_id, full_name) VALUES (?, ?)',
+ [userId, fullName]
+ );
+ }
+
+ res.json({
+ success: true,
+ message: '用户创建成功',
+ userId
+ });
+
+ } catch (error) {
+ console.error('创建用户错误:', error);
+ res.status(500).json({
+ success: false,
+ message: '服务器错误'
+ });
+ }
+});
+
+/**
+ * 更新用户
+ */
+router.put('/users/:id', requireAuth, requireRole(['admin']), async (req, res) => {
+ try {
+ const userId = req.params.id;
+ const { name, role, className } = req.body;
+
+ // 检查用户是否存在
+ const users = await db.pool.execute(
+ 'SELECT * FROM users WHERE id = ?',
+ [userId]
+ );
+
+ if (users[0].length === 0) {
+ return res.status(404).json({
+ success: false,
+ message: '用户不存在'
+ });
+ }
+
+ const oldRole = users[0][0].role;
+
+ // 更新用户信息
+ await db.pool.execute(
+ 'UPDATE users SET name = ?, role = ?, class = ? WHERE id = ?',
+ [name, role, className || null, userId]
+ );
+
+ // 如果角色改变,更新相关记录
+ if (oldRole !== role) {
+ // 删除旧角色的记录
+ if (oldRole === 'student') {
+ await db.pool.execute('DELETE FROM students WHERE user_id = ?', [userId]);
+ } else if (oldRole === 'teacher') {
+ await db.pool.execute('DELETE FROM teachers WHERE user_id = ?', [userId]);
+ }
+
+ // 创建新角色的记录
+ if (role === 'student') {
+ await db.pool.execute(
+ 'INSERT INTO students (user_id, class) VALUES (?, ?)',
+ [userId, className || null]
+ );
+ } else if (role === 'teacher') {
+ // 教师不需要额外表
+ }
+ } else if (role === 'student' && className) {
+ // 如果是学生且班级有变化,更新班级
+ await db.pool.execute(
+ 'UPDATE students SET class = ? WHERE user_id = ?',
+ [className, userId]
+ );
+ }
+
+ res.json({
+ success: true,
+ message: '用户更新成功'
+ });
+
+ } catch (error) {
+ console.error('更新用户错误:', error);
+ res.status(500).json({
+ success: false,
+ message: '服务器错误'
+ });
+ }
+});
+
+/**
+ * 删除用户
+ */
+router.delete('/users/:id', requireAuth, requireRole(['admin']), async (req, res) => {
+ try {
+ const userId = req.params.id;
+
+ // 检查用户是否存在
+ const users = await db.pool.execute(
+ 'SELECT role FROM users WHERE id = ?',
+ [userId]
+ );
+
+ if (users[0].length === 0) {
+ return res.status(404).json({
+ success: false,
+ message: '用户不存在'
+ });
+ }
+
+ const userRole = users[0][0].role;
+
+ // 删除相关记录
+ if (userRole === 'student') {
+ await db.pool.execute('DELETE FROM students WHERE user_id = ?', [userId]);
+ } else if (userRole === 'teacher') {
+ await db.pool.execute('DELETE FROM teachers WHERE user_id = ?', [userId]);
+ }
+
+ // 删除用户
+ await db.pool.execute('DELETE FROM users WHERE id = ?', [userId]);
+
+ res.json({
+ success: true,
+ message: '用户删除成功'
+ });
+
+ } catch (error) {
+ console.error('删除用户错误:', error);
+ res.status(500).json({
+ success: false,
+ message: '服务器错误'
+ });
+ }
+});
+
+/**
+ * 获取所有班级
+ */
+router.get('/classes', requireAuth, requireRole(['admin']), async (req, res) => {
+ try {
+ const classes = await db.pool.execute(
+ 'SELECT DISTINCT class_name FROM students ORDER BY class_name'
+ );
+
+ res.json({
+ success: true,
+ data: classes
+ });
+
+ } catch (error) {
+ console.error('获取班级列表错误:', error);
+ res.status(500).json({
+ success: false,
+ message: '服务器错误'
+ });
+ }
+});
+
+/**
+ * 获取统计数据
+ */
+router.get('/stats', requireAuth, requireRole(['admin']), async (req, res) => {
+ try {
+ // 用户统计
+ const userStats = await db.pool.execute(
+ 'SELECT role, COUNT(*) as count FROM users GROUP BY role'
+ );
+
+ // 班级统计
+ const classStats = await db.pool.execute(
+ 'SELECT class_name, COUNT(*) as count FROM students GROUP BY class_name'
+ );
+
+ // 课程统计
+ const courseStats = await db.pool.execute(
+ 'SELECT COUNT(*) as total_courses FROM courses'
+ );
+
+ // 成绩统计
+ const gradeStats = await db.pool.execute(
+ 'SELECT COUNT(*) as total_grades FROM scores'
+ );
+
+ res.json({
+ success: true,
+ data: {
+ users: userStats[0],
+ classes: classStats[0],
+ total_courses: courseStats[0][0].total_courses,
+ total_grades: gradeStats[0][0].total_grades
+ }
+ });
+
+ } catch (error) {
+ console.error('获取统计数据错误:', error);
+ res.status(500).json({
+ success: false,
+ message: '服务器错误'
+ });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/backend/routes/auth.js b/backend/routes/auth.js
new file mode 100644
index 0000000..4f4954c
--- /dev/null
+++ b/backend/routes/auth.js
@@ -0,0 +1,175 @@
+const express = require('express');
+const bcrypt = require('bcryptjs');
+const router = express.Router();
+const db = require('../config/database');
+
+// 登录
+router.post('/login', async (req, res) => {
+ try {
+ const { id, password, role } = req.body;
+
+ // 输入验证
+ if (!id || !password || !role) {
+ return res.status(400).json({
+ success: false,
+ message: '请输入完整的登录信息'
+ });
+ }
+
+ // 查询用户
+ const users = await db.query(
+ 'SELECT * FROM users WHERE id = ? AND role = ?',
+ [id, role]
+ );
+
+ if (users.length === 0) {
+ return res.status(401).json({
+ success: false,
+ message: '用户名或密码错误'
+ });
+ }
+
+ const user = users[0];
+
+ // 验证密码
+ const isValidPassword = await bcrypt.compare(password, user.password);
+ if (!isValidPassword) {
+ return res.status(401).json({
+ success: false,
+ message: '用户名或密码错误'
+ });
+ }
+
+ // 设置会话
+ req.session.user = {
+ id: user.id,
+ name: user.name,
+ role: user.role,
+ class: user.class
+ };
+
+ // 如果是学生,获取学生信息
+ if (user.role === 'student') {
+ const [students] = await db.pool.execute(
+ 'SELECT * FROM students WHERE id = ?',
+ [user.id]
+ );
+ if (students[0].length > 0) {
+ req.session.user.studentInfo = students[0][0];
+ }
+ }
+
+ res.json({
+ success: true,
+ message: '登录成功',
+ user: req.session.user
+ });
+
+ } catch (error) {
+ console.error('登录错误:', error);
+ res.status(500).json({
+ success: false,
+ message: '服务器错误'
+ });
+ }
+});
+
+// 注册
+router.post('/register', async (req, res) => {
+ try {
+ const { id, name, password, role, class: userClass } = req.body;
+
+ // 输入验证
+ if (!id || !name || !password || !role) {
+ return res.status(400).json({
+ success: false,
+ message: '请填写所有必填字段(ID、姓名、密码、角色)'
+ });
+ }
+
+ // 学生和教师需要班级字段,管理员不需要
+ if ((role === 'student' || role === 'teacher') && !userClass) {
+ return res.status(400).json({
+ success: false,
+ message: '学生和教师需要填写班级'
+ });
+ }
+
+ // 检查用户ID是否存在
+ const existingUsers = await db.query(
+ 'SELECT id FROM users WHERE id = ?',
+ [id]
+ );
+
+ if (existingUsers.length > 0) {
+ return res.status(400).json({
+ success: false,
+ message: '用户ID已存在'
+ });
+ }
+
+ // 哈希密码
+ const salt = await bcrypt.genSalt(10);
+ const passwordHash = await bcrypt.hash(password, salt);
+
+ // 创建用户
+ await db.pool.execute(
+ 'INSERT INTO users (id, name, password, role, class) VALUES (?, ?, ?, ?, ?)',
+ [id, name, passwordHash, role, userClass || null]
+ );
+
+ // 如果是学生,创建学生记录
+ if (role === 'student') {
+ await db.pool.execute(
+ 'INSERT INTO students (id, name, class) VALUES (?, ?, ?)',
+ [id, name, userClass]
+ );
+ }
+
+ res.json({
+ success: true,
+ message: '注册成功'
+ });
+
+ } catch (error) {
+ console.error('注册错误:', error);
+ res.status(500).json({
+ success: false,
+ message: '服务器错误'
+ });
+ }
+});
+
+// 注销
+router.post('/logout', (req, res) => {
+ req.session.destroy(err => {
+ if (err) {
+ return res.status(500).json({
+ success: false,
+ message: '注销失败'
+ });
+ }
+ res.clearCookie('session_cookie');
+ res.json({
+ success: true,
+ message: '注销成功'
+ });
+ });
+});
+
+// 获取当前用户信息
+router.get('/me', (req, res) => {
+ if (!req.session.user) {
+ return res.status(401).json({
+ success: false,
+ message: '未登录'
+ });
+ }
+
+ res.json({
+ success: true,
+ user: req.session.user
+ });
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/backend/routes/student.js b/backend/routes/student.js
new file mode 100644
index 0000000..9449ba3
--- /dev/null
+++ b/backend/routes/student.js
@@ -0,0 +1,143 @@
+const express = require('express');
+const router = express.Router();
+const db = require('../config/database');
+const { requireAuth, requireRole } = require('../middleware/auth');
+
+// 获取学生成绩
+router.get('/grades', requireAuth, requireRole(['student']), async (req, res) => {
+ try {
+ const userId = req.session.user.id;
+
+ // 获取学生信息
+ const students = await db.pool.execute(
+ 'SELECT id FROM students WHERE user_id = ?',
+ [userId]
+ );
+
+ if (students[0].length === 0) {
+ return res.status(404).json({
+ success: false,
+ message: '学生信息不存在'
+ });
+ }
+
+ const studentId = students[0][0].id;
+
+ // 获取成绩信息
+ const grades = await db.pool.execute(`
+ SELECT s.*, c.course_code, c.course_name, c.credit,
+ u.name as teacher_name
+ FROM scores s
+ JOIN courses c ON s.course_id = c.id
+ JOIN users u ON s.teacher_id = u.id
+ WHERE s.student_id = ?
+ ORDER BY s.exam_date DESC
+ `, [studentId]);
+
+ // 计算统计信息
+ let totalCredits = 0;
+ let totalGradePoints = 0;
+ let totalCourses = grades.length;
+
+ grades.forEach(grade => {
+ totalCredits += parseFloat(grade.credit);
+ if (grade.grade_point) {
+ totalGradePoints += parseFloat(grade.grade_point) * parseFloat(grade.credit);
+ }
+ });
+
+ const gpa = totalCredits > 0 ? (totalGradePoints / totalCredits).toFixed(2) : 0;
+
+ res.json({
+ success: true,
+ grades,
+ statistics: {
+ totalCourses,
+ totalCredits,
+ gpa,
+ averageScore: totalCourses > 0 ?
+ (grades.reduce((sum, g) => sum + parseFloat(g.score || 0), 0) / totalCourses).toFixed(1) : 0
+ }
+ });
+
+ } catch (error) {
+ console.error('获取成绩错误:', error);
+ res.status(500).json({
+ success: false,
+ message: '服务器错误'
+ });
+ }
+});
+
+// 获取成绩详情
+router.get('/grades/:id', requireAuth, requireRole(['student']), async (req, res) => {
+ try {
+ const gradeId = req.params.id;
+ const userId = req.session.user.id;
+
+ const grades = await db.pool.execute(`
+ SELECT s.*, c.course_code, c.course_name, c.credit, c.semester,
+ u.full_name as teacher_name, u.email as teacher_email,
+ st.student_id as student_number, st.class_name, st.major
+ FROM scores 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
+ WHERE s.id = ? AND st.user_id = ?
+ `, [gradeId, userId]);
+
+ if (grades[0].length === 0) {
+ return res.status(404).json({
+ success: false,
+ message: '成绩不存在'
+ });
+ }
+
+ res.json({
+ success: true,
+ grade: grades[0]
+ });
+
+ } catch (error) {
+ console.error('获取成绩详情错误:', error);
+ res.status(500).json({
+ success: false,
+ message: '服务器错误'
+ });
+ }
+});
+
+// 获取学生个人信息
+router.get('/profile', requireAuth, requireRole(['student']), async (req, res) => {
+ try {
+ const userId = req.session.user.id;
+
+ const students = await db.pool.execute(`
+ SELECT s.*, u.username, u.email, u.created_at as account_created
+ FROM students s
+ JOIN users u ON s.user_id = u.id
+ WHERE u.id = ?
+ `, [userId]);
+
+ if (students[0].length === 0) {
+ return res.status(404).json({
+ success: false,
+ message: '学生信息不存在'
+ });
+ }
+
+ res.json({
+ success: true,
+ profile: students[0]
+ });
+
+ } catch (error) {
+ console.error('获取个人信息错误:', error);
+ res.status(500).json({
+ success: false,
+ message: '服务器错误'
+ });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/backend/routes/teacher.js b/backend/routes/teacher.js
new file mode 100644
index 0000000..c27d3f5
--- /dev/null
+++ b/backend/routes/teacher.js
@@ -0,0 +1,243 @@
+const express = require('express');
+const router = express.Router();
+const db = require('../config/database');
+const { requireAuth, requireRole } = require('../middleware/auth');
+
+// 获取教师教授的课程
+router.get('/courses', requireAuth, requireRole(['teacher']), async (req, res) => {
+ try {
+ const teacherId = req.session.user.id;
+
+ const courses = await db.pool.execute(
+ 'SELECT * FROM courses WHERE teacher_id = ? ORDER BY course_code',
+ [teacherId]
+ );
+
+ res.json({
+ success: true,
+ courses
+ });
+
+ } catch (error) {
+ console.error('获取课程错误:', error);
+ res.status(500).json({
+ success: false,
+ message: '服务器错误'
+ });
+ }
+});
+
+// 录入成绩
+router.post('/grades', requireAuth, requireRole(['teacher']), async (req, res) => {
+ try {
+ const teacherId = req.session.user.id;
+ const { studentId, courseId, score, examDate, remark } = req.body;
+
+ // 验证输入
+ if (!studentId || !courseId || score === undefined) {
+ return res.status(400).json({
+ success: false,
+ message: '请填写必填字段'
+ });
+ }
+
+ // 检查学生和课程是否存在
+ const students = await db.pool.execute(
+ 'SELECT id FROM students WHERE student_id = ?',
+ [studentId]
+ );
+
+ if (students[0].length === 0) {
+ return res.status(404).json({
+ success: false,
+ message: '学生不存在'
+ });
+ }
+
+ // 计算绩点和等级
+ const numericScore = parseFloat(score);
+ 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';
+ }
+
+ // 插入成绩
+ const result = await db.pool.execute(
+ `INSERT INTO scores (student_id, course_id, teacher_id, score,
+ grade_point, grade_level, exam_date, remark)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
+ [students[0][0].id, courseId, teacherId, numericScore,
+ gradePoint, gradeLevel, examDate, remark]
+ );
+
+ res.json({
+ success: true,
+ message: '成绩录入成功',
+ gradeId: result.insertId
+ });
+
+ } catch (error) {
+ console.error('录入成绩错误:', error);
+
+ // 处理唯一约束错误
+ if (error.code === 'ER_DUP_ENTRY') {
+ return res.status(400).json({
+ success: false,
+ message: '该学生此课程成绩已存在'
+ });
+ }
+
+ res.status(500).json({
+ success: false,
+ message: '服务器错误'
+ });
+ }
+});
+
+// 查询成绩(按班级、课程)
+router.get('/grades', requireAuth, requireRole(['teacher']), async (req, res) => {
+ try {
+ const { class_name, course_id } = req.query;
+ const teacherId = req.session.user.id;
+
+ let query = `
+ SELECT sc.*, st.student_id, st.full_name, st.class_name,
+ c.course_code, c.course_name, c.credit
+ FROM scores sc
+ JOIN students st ON sc.student_id = st.id
+ JOIN courses c ON sc.course_id = c.id
+ WHERE sc.teacher_id = ?
+ `;
+
+ const params = [teacherId];
+
+ if (class_name) {
+ query += ' AND st.class_name = ?';
+ params.push(class_name);
+ }
+
+ if (course_id) {
+ query += ' AND sc.course_id = ?';
+ params.push(course_id);
+ }
+
+ query += ' ORDER BY st.class_name, st.student_id, sc.exam_date DESC';
+
+ const grades = await db.pool.execute(query, params);
+
+ res.json({
+ success: true,
+ grades
+ });
+
+ } catch (error) {
+ console.error('查询成绩错误:', error);
+ res.status(500).json({
+ success: false,
+ message: '服务器错误'
+ });
+ }
+});
+
+// 更新成绩
+router.put('/grades/:id', requireAuth, requireRole(['teacher']), async (req, res) => {
+ try {
+ const gradeId = req.params.id;
+ const teacherId = req.session.user.id;
+ const { score, examDate, remark } = req.body;
+
+ // 验证成绩是否存在且属于该教师
+ const existingGrades = await db.pool.execute(
+ 'SELECT id FROM scores WHERE id = ? AND teacher_id = ?',
+ [gradeId, teacherId]
+ );
+
+ if (existingGrades[0].length === 0) {
+ return res.status(404).json({
+ success: false,
+ message: '成绩不存在或无权修改'
+ });
+ }
+
+ // 计算新的绩点和等级
+ const numericScore = parseFloat(score);
+ 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';
+ }
+
+ // 更新成绩
+ await db.pool.execute(
+ `UPDATE scores SET score = ?, grade_point = ?, grade_level = ?,
+ exam_date = ?, remark = ?, updated_at = CURRENT_TIMESTAMP
+ WHERE id = ?`,
+ [numericScore, gradePoint, gradeLevel, examDate, remark, gradeId]
+ );
+
+ res.json({
+ success: true,
+ message: '成绩更新成功'
+ });
+
+ } catch (error) {
+ console.error('更新成绩错误:', error);
+ res.status(500).json({
+ success: false,
+ message: '服务器错误'
+ });
+ }
+});
+
+// 删除成绩
+router.delete('/grades/:id', requireAuth, requireRole(['teacher']), async (req, res) => {
+ try {
+ const gradeId = req.params.id;
+ const teacherId = req.session.user.id;
+
+ // 验证成绩是否存在且属于该教师
+ const existingGrades = await db.pool.execute(
+ 'SELECT id FROM scores WHERE id = ? AND teacher_id = ?',
+ [gradeId, teacherId]
+ );
+
+ if (existingGrades[0].length === 0) {
+ return res.status(404).json({
+ success: false,
+ message: '成绩不存在或无权删除'
+ });
+ }
+
+ // 删除成绩
+ await db.pool.execute('DELETE FROM scores WHERE id = ?', [gradeId]);
+
+ res.json({
+ success: true,
+ message: '成绩删除成功'
+ });
+
+ } catch (error) {
+ console.error('删除成绩错误:', error);
+ res.status(500).json({
+ success: false,
+ message: '服务器错误'
+ });
+ }
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/backend/server.js b/backend/server.js
new file mode 100644
index 0000000..4e9824b
--- /dev/null
+++ b/backend/server.js
@@ -0,0 +1,128 @@
+const express = require('express');
+const cors = require('cors');
+const session = require('express-session');
+const MySQLStore = require('express-mysql-session')(session);
+const path = require('path');
+require('dotenv').config();
+
+// 导入路由
+const authRoutes = require('./routes/auth');
+const studentRoutes = require('./routes/student');
+const teacherRoutes = require('./routes/teacher');
+const adminRoutes = require('./routes/admin');
+
+// 数据库配置
+const db = require('./config/database');
+
+const app = express();
+const PORT = process.env.PORT || 3000;
+
+// 中间件
+app.use(cors({
+ origin: 'http://localhost:3000',
+ credentials: true
+}));
+app.use(express.json());
+app.use(express.urlencoded({ extended: true }));
+
+// 会话配置
+const sessionStore = new MySQLStore({
+ expiration: 86400000, // 1天
+ createDatabaseTable: true,
+ schema: {
+ tableName: 'sessions',
+ columnNames: {
+ session_id: 'session_id',
+ expires: 'expires',
+ data: 'data'
+ }
+ }
+}, db.pool);
+
+app.use(session({
+ key: 'session_cookie',
+ secret: process.env.SESSION_SECRET || 'your-secret-key',
+ store: sessionStore,
+ resave: false,
+ saveUninitialized: false,
+ cookie: {
+ maxAge: 86400000,
+ httpOnly: true,
+ secure: process.env.NODE_ENV === 'production'
+ }
+}));
+
+// 静态文件服务
+app.use(express.static(path.join(__dirname, '../frontend')));
+
+// 重定向旧路径 /frontend/html/* 到 /html/*
+app.get('/frontend/html/*', (req, res) => {
+ const path = req.params[0];
+ res.redirect(`/html/${path}`);
+});
+
+// 路由
+app.use('/api/auth', authRoutes);
+app.use('/api/student', studentRoutes);
+app.use('/api/teacher', teacherRoutes);
+app.use('/api/admin', adminRoutes);
+
+// 认证中间件
+const { requireAuth, requireRole } = require('./middleware/auth');
+
+// 页面路由
+app.get('/', (req, res) => {
+ res.sendFile(path.join(__dirname, '../frontend/html/login.html'));
+});
+
+app.get('/dashboard', requireAuth, (req, res) => {
+ // 根据用户角色重定向到不同的仪表板
+ const role = req.session.user?.role;
+
+ switch (role) {
+ case 'student':
+ res.redirect('/html/student_dashboard.html');
+ break;
+ case 'teacher':
+ res.redirect('/html/teacher_dashboard.html');
+ break;
+ case 'admin':
+ res.redirect('/html/admin_dashboard.html');
+ break;
+ default:
+ // 如果没有角色信息,重定向到登录页面
+ res.redirect('/');
+ break;
+ }
+});
+
+// 学生页面路由
+app.get('/student/*', requireAuth, requireRole(['student', 'admin', 'teacher']), (req, res, next) => {
+ next();
+});
+
+// 教师页面路由
+app.get('/teacher/*', requireAuth, requireRole(['teacher', 'admin']), (req, res, next) => {
+ next();
+});
+
+// 管理员页面路由
+app.get('/admin/*', requireAuth, requireRole(['admin']), (req, res, next) => {
+ next();
+});
+
+// 404处理
+app.use((req, res) => {
+ res.status(404).json({ error: 'Not found' });
+});
+
+// 错误处理
+app.use((err, req, res, next) => {
+ console.error(err.stack);
+ res.status(500).json({ error: 'Internal server error' });
+});
+
+app.listen(PORT, () => {
+ console.log(`Server running on port ${PORT}`);
+ console.log(`访问地址: http://localhost:${PORT}`);
+});
\ No newline at end of file
diff --git a/database/init.sql b/database/init.sql
new file mode 100644
index 0000000..1ed7ce3
--- /dev/null
+++ b/database/init.sql
@@ -0,0 +1,214 @@
+-- 学生成绩管理系统数据库初始化脚本
+-- 创建数据库
+CREATE DATABASE IF NOT EXISTS score_management DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+USE score_management;
+
+-- 用户表(学生、教师、管理员)
+CREATE TABLE IF NOT EXISTS users (
+ id VARCHAR(20) PRIMARY KEY COMMENT '用户ID(学号/工号)',
+ name VARCHAR(50) NOT NULL COMMENT '姓名',
+ password VARCHAR(100) NOT NULL COMMENT '密码(bcrypt加密)',
+ role ENUM('student', 'teacher', 'admin') NOT NULL COMMENT '角色',
+ class VARCHAR(20) DEFAULT NULL COMMENT '班级(学生和教师需要,管理员为NULL)',
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+ INDEX idx_role (role),
+ INDEX idx_class (class)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
+
+-- students 表:学生详细信息(与 users 表 id 关联)
+CREATE TABLE IF NOT EXISTS students (
+ id VARCHAR(20) PRIMARY KEY COMMENT '学生ID(与users表id一致)',
+ name VARCHAR(50) NOT NULL COMMENT '姓名',
+ class VARCHAR(20) COMMENT '班级',
+ FOREIGN KEY (id) REFERENCES users(id) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生详细信息表';
+
+-- scores 表:成绩记录
+CREATE TABLE IF NOT EXISTS scores (
+ id INT AUTO_INCREMENT PRIMARY KEY COMMENT '成绩记录ID',
+ student_id VARCHAR(20) NOT NULL COMMENT '学生ID',
+ course VARCHAR(100) NOT NULL COMMENT '课程名称',
+ score DECIMAL(5,2) NOT NULL CHECK (score >= 0 AND score <= 100) COMMENT '成绩',
+ teacher_id VARCHAR(20) NOT NULL COMMENT '教师ID',
+ class VARCHAR(20) NOT NULL COMMENT '班级',
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ FOREIGN KEY (student_id) REFERENCES users(id) ON DELETE CASCADE,
+ FOREIGN KEY (teacher_id) REFERENCES users(id) ON DELETE CASCADE,
+ INDEX idx_student_id (student_id),
+ INDEX idx_teacher_id (teacher_id),
+ INDEX idx_class (class)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成绩记录表';
+
+-- 班级表
+CREATE TABLE IF NOT EXISTS classes (
+ id INT PRIMARY KEY AUTO_INCREMENT COMMENT '班级ID',
+ class_name VARCHAR(50) NOT NULL COMMENT '班级名称',
+ grade VARCHAR(20) COMMENT '年级',
+ major VARCHAR(100) COMMENT '专业',
+ teacher_id INT COMMENT '班主任ID',
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+ INDEX idx_class_name (class_name),
+ INDEX idx_teacher_id (teacher_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='班级表';
+
+-- 课程表
+CREATE TABLE IF NOT EXISTS courses (
+ id INT PRIMARY KEY AUTO_INCREMENT COMMENT '课程ID',
+ course_code VARCHAR(20) UNIQUE NOT NULL COMMENT '课程代码',
+ course_name VARCHAR(100) 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) COMMENT '学期',
+ academic_year VARCHAR(20) COMMENT '学年',
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+ INDEX idx_course_code (course_code),
+ INDEX idx_teacher_id (teacher_id),
+ INDEX idx_class_id (class_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='课程表';
+
+-- 成绩表
+CREATE TABLE IF NOT EXISTS grades (
+ id INT PRIMARY KEY AUTO_INCREMENT COMMENT '成绩ID',
+ student_id INT NOT NULL COMMENT '学生ID',
+ course_id INT NOT NULL COMMENT '课程ID',
+ usual_score DECIMAL(5,2) COMMENT '平时成绩',
+ midterm_score DECIMAL(5,2) COMMENT '期中成绩',
+ final_score DECIMAL(5,2) COMMENT '期末成绩',
+ total_score DECIMAL(5,2) COMMENT '总评成绩',
+ grade_point DECIMAL(3,2) COMMENT '绩点',
+ grade_level VARCHAR(10) COMMENT '成绩等级(A/B/C/D/F)',
+ teacher_id INT NOT NULL COMMENT '录入教师ID',
+ remark TEXT COMMENT '备注',
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+ UNIQUE KEY uk_student_course (student_id, course_id),
+ INDEX idx_student_id (student_id),
+ INDEX idx_course_id (course_id),
+ INDEX idx_teacher_id (teacher_id),
+ INDEX idx_total_score (total_score)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成绩表';
+
+-- 操作日志表
+CREATE TABLE IF NOT EXISTS operation_logs (
+ id INT PRIMARY KEY AUTO_INCREMENT COMMENT '日志ID',
+ user_id INT NOT NULL COMMENT '操作用户ID',
+ operation_type VARCHAR(50) NOT NULL COMMENT '操作类型',
+ operation_target VARCHAR(100) COMMENT '操作目标',
+ operation_details TEXT COMMENT '操作详情',
+ ip_address VARCHAR(45) COMMENT 'IP地址',
+ user_agent TEXT COMMENT '用户代理',
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
+ INDEX idx_user_id (user_id),
+ INDEX idx_operation_type (operation_type),
+ INDEX idx_created_at (created_at)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='操作日志表';
+
+-- 插入初始数据
+-- 插入管理员用户(密码:admin123)
+INSERT INTO users (username, password, name, role, email, phone) VALUES
+('admin', '$2b$10$N9qo8uLOickgx2ZMRZoMy.MrqK.3.6Z1zXjJX.3Q7JzQ7JzQ7JzQ7', '系统管理员', 'admin', 'admin@school.edu', '13800138000');
+
+-- 插入示例班级
+INSERT INTO classes (class_name, grade, major) VALUES
+('计算机科学与技术2023级1班', '2023', '计算机科学与技术'),
+('软件工程2023级1班', '2023', '软件工程'),
+('人工智能2023级1班', '2023', '人工智能');
+
+-- 插入示例教师(密码:teacher123)
+INSERT INTO users (username, password, name, role, class_id, email, phone) VALUES
+('t1001', '$2b$10$N9qo8uLOickgx2ZMRZoMy.MrqK.3.6Z1zXjJX.3Q7JzQ7JzQ7JzQ7', '张老师', 'teacher', 1, 'zhang@school.edu', '13800138001'),
+('t1002', '$2b$10$N9qo8uLOickgx2ZMRZoMy.MrqK.3.6Z1zXjJX.3Q7JzQ7JzQ7JzQ7', '李老师', 'teacher', 2, 'li@school.edu', '13800138002');
+
+-- 插入示例学生(密码:student123)
+INSERT INTO users (username, password, name, role, class_id, email, phone) VALUES
+('s2023001', '$2b$10$N9qo8uLOickgx2ZMRZoMy.MrqK.3.6Z1zXjJX.3Q7JzQ7JzQ7JzQ7', '张三', 'student', 1, 'zhangsan@school.edu', '13800138111'),
+('s2023002', '$2b$10$N9qo8uLOickgx2ZMRZoMy.MrqK.3.6Z1zXjJX.3Q7JzQ7JzQ7JzQ7', '李四', 'student', 1, 'lisi@school.edu', '13800138112'),
+('s2023003', '$2b$10$N9qo8uLOickgx2ZMRZoMy.MrqK.3.6Z1zXjJX.3Q7JzQ7JzQ7JzQ7', '王五', 'student', 2, 'wangwu@school.edu', '13800138113');
+
+-- 插入示例课程
+INSERT INTO courses (course_code, course_name, credit, teacher_id, class_id, semester, academic_year) VALUES
+('CS101', '计算机基础', 3.0, 2, 1, '2023-2024-1', '2023-2024'),
+('CS201', '数据结构', 4.0, 2, 1, '2023-2024-1', '2023-2024'),
+('SE101', '软件工程导论', 3.0, 3, 2, '2023-2024-1', '2023-2024'),
+('AI101', '人工智能基础', 3.5, 3, 3, '2023-2024-1', '2023-2024');
+
+-- 插入示例成绩
+INSERT INTO grades (student_id, course_id, usual_score, midterm_score, final_score, total_score, grade_point, grade_level, teacher_id) VALUES
+(3, 1, 85.00, 78.00, 82.00, 81.50, 3.2, 'B', 2),
+(3, 2, 90.00, 85.00, 88.00, 87.50, 3.7, 'A', 2),
+(4, 1, 78.00, 82.00, 80.00, 80.50, 3.0, 'B', 2),
+(5, 3, 88.00, 85.00, 90.00, 88.50, 3.8, 'A', 3);
+
+-- 更新班级表的班主任信息
+UPDATE classes SET teacher_id = 2 WHERE id = 1;
+UPDATE classes SET teacher_id = 3 WHERE id = 2;
+
+-- 创建视图:学生成绩详情视图
+CREATE OR REPLACE VIEW student_grade_details AS
+SELECT
+ g.id,
+ g.student_id,
+ u1.name AS student_name,
+ u1.username AS student_no,
+ g.course_id,
+ c.course_code,
+ c.course_name,
+ c.credit,
+ g.usual_score,
+ g.midterm_score,
+ g.final_score,
+ g.total_score,
+ g.grade_point,
+ g.grade_level,
+ g.teacher_id,
+ u2.name AS teacher_name,
+ g.remark,
+ g.created_at,
+ g.updated_at
+FROM grades g
+JOIN users u1 ON g.student_id = u1.id
+JOIN courses c ON g.course_id = c.id
+JOIN users u2 ON g.teacher_id = u2.id;
+
+-- 创建视图:班级成绩统计视图
+CREATE OR REPLACE VIEW class_grade_statistics AS
+SELECT
+ cl.id AS class_id,
+ cl.class_name,
+ c.id AS course_id,
+ c.course_code,
+ c.course_name,
+ COUNT(g.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.grade_level = 'A' THEN 1 ELSE 0 END) AS a_count,
+ SUM(CASE WHEN g.grade_level = 'B' THEN 1 ELSE 0 END) AS b_count,
+ SUM(CASE WHEN g.grade_level = 'C' THEN 1 ELSE 0 END) AS c_count,
+ SUM(CASE WHEN g.grade_level = 'D' THEN 1 ELSE 0 END) AS d_count,
+ SUM(CASE WHEN g.grade_level = 'F' THEN 1 ELSE 0 END) AS f_count
+FROM classes cl
+JOIN courses c ON cl.id = c.class_id
+LEFT JOIN grades g ON c.id = g.course_id
+GROUP BY cl.id, c.id;
+
+-- 显示表结构信息
+SHOW TABLES;
+
+-- 显示各表记录数
+SELECT 'users' AS table_name, COUNT(*) AS record_count FROM users
+UNION ALL
+SELECT 'classes', COUNT(*) FROM classes
+UNION ALL
+SELECT 'courses', COUNT(*) FROM courses
+UNION ALL
+SELECT 'grades', COUNT(*) FROM grades
+UNION ALL
+SELECT 'operation_logs', COUNT(*) FROM operation_logs;
+
+-- 显示视图
+SHOW FULL TABLES WHERE TABLE_TYPE = 'VIEW';
\ No newline at end of file
diff --git a/database/score_management.sql b/database/score_management.sql
new file mode 100644
index 0000000..85ac38a
--- /dev/null
+++ b/database/score_management.sql
@@ -0,0 +1,213 @@
+/*
+ Navicat Premium Dump SQL
+
+ Source Server : test
+ Source Server Type : MySQL
+ Source Server Version : 80042 (8.0.42)
+ Source Host : localhost:3306
+ Source Schema : score_management
+
+ Target Server Type : MySQL
+ Target Server Version : 80042 (8.0.42)
+ File Encoding : 65001
+
+ Date: 21/12/2025 21:37:55
+*/
+
+SET NAMES utf8mb4;
+SET FOREIGN_KEY_CHECKS = 0;
+
+-- ----------------------------
+-- Table structure for 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 NULL DEFAULT NULL COMMENT '年级',
+ `major` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '专业',
+ `teacher_id` int NULL 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,
+ INDEX `idx_class_name`(`class_name` ASC) USING BTREE,
+ INDEX `idx_teacher_id`(`teacher_id` ASC) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '班级表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of classes
+-- ----------------------------
+
+-- ----------------------------
+-- Table structure for 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 NULL DEFAULT NULL COMMENT '学期',
+ `academic_year` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL 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 INDEX `course_code`(`course_code` ASC) USING BTREE,
+ INDEX `idx_course_code`(`course_code` ASC) USING BTREE,
+ INDEX `idx_teacher_id`(`teacher_id` ASC) USING BTREE,
+ INDEX `idx_class_id`(`class_id` ASC) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '课程表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of courses
+-- ----------------------------
+
+-- ----------------------------
+-- Table structure for 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) NULL DEFAULT NULL COMMENT '平时成绩',
+ `midterm_score` decimal(5, 2) NULL DEFAULT NULL COMMENT '期中成绩',
+ `final_score` decimal(5, 2) NULL DEFAULT NULL COMMENT '期末成绩',
+ `total_score` decimal(5, 2) NULL DEFAULT NULL COMMENT '总评成绩',
+ `grade_point` decimal(3, 2) NULL DEFAULT NULL COMMENT '绩点',
+ `grade_level` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL 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 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 INDEX `uk_student_course`(`student_id` ASC, `course_id` ASC) USING BTREE,
+ INDEX `idx_student_id`(`student_id` ASC) USING BTREE,
+ INDEX `idx_course_id`(`course_id` ASC) USING BTREE,
+ INDEX `idx_teacher_id`(`teacher_id` ASC) USING BTREE,
+ INDEX `idx_total_score`(`total_score` ASC) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '成绩表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of grades
+-- ----------------------------
+
+-- ----------------------------
+-- Table structure for 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 NULL DEFAULT NULL COMMENT '操作目标',
+ `operation_details` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '操作详情',
+ `ip_address` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'IP地址',
+ `user_agent` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '用户代理',
+ `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
+ PRIMARY KEY (`id`) USING BTREE,
+ INDEX `idx_user_id`(`user_id` ASC) USING BTREE,
+ INDEX `idx_operation_type`(`operation_type` ASC) USING BTREE,
+ INDEX `idx_created_at`(`created_at` ASC) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '操作日志表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of operation_logs
+-- ----------------------------
+
+-- ----------------------------
+-- Table structure for 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,
+ INDEX `idx_student_id`(`student_id` ASC) USING BTREE,
+ INDEX `idx_teacher_id`(`teacher_id` ASC) USING BTREE,
+ INDEX `idx_class`(`class` ASC) 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 AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '成绩记录表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of scores
+-- ----------------------------
+
+-- ----------------------------
+-- Table structure for 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 NULL,
+ PRIMARY KEY (`session_id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of 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?\"}}');
+INSERT INTO `sessions` VALUES ('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?\"}}');
+INSERT INTO `sessions` VALUES ('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?\"}}');
+INSERT INTO `sessions` VALUES ('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?\"}}');
+INSERT INTO `sessions` VALUES ('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?\"}}');
+INSERT INTO `sessions` VALUES ('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?\"}}');
+INSERT INTO `sessions` VALUES ('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?\"}}');
+INSERT INTO `sessions` VALUES ('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\"}}');
+INSERT INTO `sessions` VALUES ('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 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 NULL 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 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '学生详细信息表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of students
+-- ----------------------------
+INSERT INTO `students` VALUES ('123', '经济局', '123');
+INSERT INTO `students` VALUES ('test123', '????', '????');
+INSERT INTO `students` VALUES ('teststudent', '????', '2023?1?');
+
+-- ----------------------------
+-- Table structure for 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 NULL 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,
+ INDEX `idx_role`(`role` ASC) USING BTREE,
+ INDEX `idx_class`(`class` ASC) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户表' ROW_FORMAT = Dynamic;
+
+-- ----------------------------
+-- Records of users
+-- ----------------------------
+INSERT INTO `users` VALUES ('123', '经济局', '$2a$10$l0NwrM2fNGgPdqDFqXEGx.UfqOIp8womtWN8/omq1fK15zII7b4Nm', 'student', '123', '2025-12-20 22:47:59', '2025-12-20 22:47:59');
+INSERT INTO `users` VALUES ('567', '急急急', '$2a$10$27a0L4fC0rLjK4.Kpq0CceK1cD4O0cW6XTxwvs4eIcYpKnpvEQdVG', 'teacher', '567', '2025-12-21 20:36:17', '2025-12-21 20:36:17');
+INSERT INTO `users` VALUES ('test123', '????', '$2a$10$61WfURr1uI1e71EWwWXOlOUamTR1/AzH2Kb.6bZKVttdmWGk7V366', 'student', '????', '2025-12-20 23:49:32', '2025-12-20 23:49:32');
+INSERT INTO `users` VALUES ('teststudent', '????', '$2a$10$PfNB72GBDBmu8DLcTLptW.4clzmk9qsjjlxfAMZvAJD8B.QUoXXAK', 'student', '2023?1?', '2025-12-21 00:24:16', '2025-12-21 00:24:16');
+
+SET FOREIGN_KEY_CHECKS = 1;
diff --git a/frontend/css/main.css b/frontend/css/main.css
new file mode 100644
index 0000000..bd0656e
--- /dev/null
+++ b/frontend/css/main.css
@@ -0,0 +1,215 @@
+/* 首页通用交互样式 */
+
+/* 滚动时导航栏样式 */
+.navbar-scrolled {
+ background-color: rgba(255, 255, 255, 0.95) !important;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+ backdrop-filter: blur(10px);
+}
+
+.navbar-scrolled .navbar-brand,
+.navbar-scrolled .nav-link {
+ color: #333 !important;
+}
+
+.navbar-scrolled .navbar-toggler-icon {
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba(0, 0, 0, 0.7)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") !important;
+}
+
+/* 滚动动画效果 */
+.feature-card, .hero-content {
+ opacity: 0;
+ transform: translateY(20px);
+ transition: opacity 0.6s ease, transform 0.6s ease;
+}
+
+.feature-card.animate-in,
+.hero-content.animate-in {
+ opacity: 1;
+ transform: translateY(0);
+}
+
+/* 返回顶部按钮 */
+#backToTop {
+ position: fixed;
+ bottom: 30px;
+ right: 30px;
+ width: 50px;
+ height: 50px;
+ background-color: #4e73df;
+ color: white;
+ border: none;
+ border-radius: 50%;
+ cursor: pointer;
+ opacity: 0;
+ visibility: hidden;
+ transition: all 0.3s ease;
+ z-index: 1000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 1.2rem;
+ box-shadow: 0 4px 15px rgba(78, 115, 223, 0.3);
+}
+
+#backToTop:hover {
+ background-color: #2e59d9;
+ transform: translateY(-3px);
+ box-shadow: 0 6px 20px rgba(78, 115, 223, 0.4);
+}
+
+#backToTop.show {
+ opacity: 1;
+ visibility: visible;
+}
+
+/* 通知样式 */
+.notification {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ padding: 15px 20px;
+ border-radius: 8px;
+ background-color: white;
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
+ z-index: 9999;
+ opacity: 0;
+ transform: translateX(100%);
+ transition: opacity 0.3s ease, transform 0.3s ease;
+ max-width: 350px;
+ display: flex;
+ align-items: center;
+}
+
+.notification.show {
+ opacity: 1;
+ transform: translateX(0);
+}
+
+.notification-content {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.notification i {
+ font-size: 1.2rem;
+}
+
+.notification-success {
+ border-left: 4px solid #1cc88a;
+}
+
+.notification-success i {
+ color: #1cc88a;
+}
+
+.notification-error {
+ border-left: 4px solid #e74a3b;
+}
+
+.notification-error i {
+ color: #e74a3b;
+}
+
+.notification-info {
+ border-left: 4px solid #36b9cc;
+}
+
+.notification-info i {
+ color: #36b9cc;
+}
+
+/* 移动端菜单优化 */
+@media (max-width: 991.98px) {
+ .navbar-collapse {
+ background-color: white;
+ padding: 20px;
+ border-radius: 8px;
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
+ margin-top: 10px;
+ }
+
+ .navbar-nav .nav-link {
+ padding: 10px 15px;
+ border-radius: 4px;
+ margin-bottom: 5px;
+ }
+
+ .navbar-nav .nav-link:hover {
+ background-color: #f8f9fc;
+ }
+}
+
+/* 平滑滚动优化 */
+html {
+ scroll-behavior: smooth;
+}
+
+/* 当前页面高亮 */
+.nav-link.active {
+ color: #4e73df !important;
+ font-weight: 600;
+}
+
+.nav-link.active::after {
+ content: '';
+ position: absolute;
+ bottom: -2px;
+ left: 0;
+ width: 100%;
+ height: 2px;
+ background-color: #4e73df;
+ border-radius: 2px;
+}
+
+/* 按钮悬停效果增强 */
+.btn {
+ transition: all 0.3s ease;
+}
+
+.btn:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
+}
+
+/* 卡片悬停效果 */
+.feature-card {
+ transition: all 0.3s ease;
+}
+
+.feature-card:hover {
+ transform: translateY(-5px);
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
+}
+
+/* 加载动画 */
+.loading-spinner {
+ display: inline-block;
+ width: 20px;
+ height: 20px;
+ border: 3px solid rgba(255, 255, 255, 0.3);
+ border-radius: 50%;
+ border-top-color: white;
+ animation: spin 1s ease-in-out infinite;
+}
+
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+/* 响应式调整 */
+@media (max-width: 768px) {
+ #backToTop {
+ bottom: 20px;
+ right: 20px;
+ width: 45px;
+ height: 45px;
+ }
+
+ .notification {
+ left: 20px;
+ right: 20px;
+ max-width: none;
+ }
+}
\ No newline at end of file
diff --git a/frontend/css/style.css b/frontend/css/style.css
new file mode 100644
index 0000000..d44ae76
--- /dev/null
+++ b/frontend/css/style.css
@@ -0,0 +1,1150 @@
+/* 重置和基础样式 */
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
+ background: #f8f9fa;
+ min-height: 100vh;
+ color: #333;
+}
+
+/* 通用容器 */
+.container {
+ max-width: 1000px;
+ margin: 30px auto;
+ padding: 20px;
+ background: white;
+ border-radius: 10px;
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
+}
+
+/* 认证页面容器 */
+.auth-container {
+ max-width: 500px;
+ margin: 50px auto;
+ padding: 40px;
+ background: white;
+ border-radius: 15px;
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
+}
+
+.auth-card {
+ padding: 0;
+}
+
+/* 卡片样式 */
+.card {
+ background: white;
+ border-radius: 15px;
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
+ padding: 30px;
+ margin-bottom: 20px;
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
+}
+
+.card:hover {
+ transform: translateY(-5px);
+ box-shadow: 0 15px 40px rgba(0, 0, 0, 0.15);
+}
+
+/* 按钮样式 */
+.btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 12px 24px;
+ border: none;
+ border-radius: 8px;
+ font-size: 16px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ text-decoration: none;
+ gap: 8px;
+}
+
+.btn-primary {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+}
+
+.btn-primary:hover {
+ background: linear-gradient(135deg, #5a6fd8 0%, #6b4090 100%);
+ transform: translateY(-2px);
+ box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
+}
+
+.btn-secondary {
+ background: #f8f9fa;
+ color: #495057;
+ border: 1px solid #dee2e6;
+}
+
+.btn-secondary:hover {
+ background: #e9ecef;
+ border-color: #adb5bd;
+}
+
+.btn-danger {
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
+ color: white;
+}
+
+.btn-danger:hover {
+ background: linear-gradient(135deg, #e685f0 0%, #e4475c 100%);
+}
+
+.btn-success {
+ background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
+ color: white;
+}
+
+.btn-success:hover {
+ background: linear-gradient(135deg, #3a9bf4 0%, #00d9e4 100%);
+}
+
+/* 表格样式 */
+.table-container {
+ background: white;
+ border-radius: 10px;
+ overflow: hidden;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ margin-bottom: 30px;
+}
+
+table {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+thead {
+ background: #f8f9fa;
+}
+
+th {
+ padding: 15px 20px;
+ text-align: left;
+ font-weight: 600;
+ color: #495057;
+ border-bottom: 2px solid #dee2e6;
+}
+
+td {
+ padding: 15px 20px;
+ border-bottom: 1px solid #dee2e6;
+}
+
+tr:hover {
+ background: #f8f9fa;
+}
+
+/* 表单样式 */
+
+
+/* 导航栏样式 */
+.navbar {
+ background: white;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
+ padding: 15px 30px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ border-radius: 0 0 15px 15px;
+ margin-bottom: 30px;
+}
+
+.navbar-brand {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ font-size: 20px;
+ font-weight: 600;
+ color: #667eea;
+ text-decoration: none;
+}
+
+.navbar-menu {
+ display: flex;
+ gap: 20px;
+ align-items: center;
+}
+
+.navbar-user {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.user-avatar {
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: 600;
+}
+
+/* 响应式设计 */
+@media (max-width: 768px) {
+ .container {
+ padding: 0 15px;
+ }
+
+ .card {
+ padding: 20px;
+ }
+
+ .btn {
+ padding: 8px 16px;
+ font-size: 14px;
+ }
+
+ .table-responsive {
+ overflow-x: auto;
+ }
+}
+
+/* 统计卡片样式 */
+.stats-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ gap: 20px;
+ margin-bottom: 30px;
+}
+
+.stat-card {
+ background: white;
+ border-radius: 10px;
+ padding: 25px;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
+ display: flex;
+ align-items: center;
+}
+
+.stat-card:hover {
+ transform: translateY(-5px);
+ box-shadow: 0 8px 15px rgba(0, 0, 0, 0.15);
+}
+
+.stat-icon {
+ width: 60px;
+ height: 60px;
+ border-radius: 10px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-right: 20px;
+ font-size: 24px;
+ color: white;
+}
+
+.stat-icon.users,
+.stat-icon.gpa {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+}
+
+.stat-icon.students,
+.stat-icon.courses {
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
+}
+
+.stat-icon.teachers,
+.stat-icon.credits {
+ background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
+}
+
+.stat-icon.courses,
+.stat-icon.ranking {
+ background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
+}
+
+.stat-content {
+ flex: 1;
+}
+
+.stat-value {
+ font-size: 32px;
+ font-weight: 700;
+ color: #2d3748;
+ margin-bottom: 5px;
+}
+
+.stat-label {
+ font-size: 14px;
+ color: #718096;
+ margin-bottom: 10px;
+}
+
+.stat-change {
+ font-size: 12px;
+ display: flex;
+ align-items: center;
+}
+
+.stat-change.positive {
+ color: #38a169;
+}
+
+.stat-change i {
+ margin-right: 5px;
+}
+
+/* 功能卡片样式 */
+.function-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+ gap: 25px;
+ margin-bottom: 40px;
+}
+
+.function-card {
+ background: white;
+ border-radius: 12px;
+ padding: 30px;
+ box-shadow: 0 6px 12px rgba(0, 0, 0, 0.08);
+ transition: all 0.3s ease;
+ cursor: pointer;
+ text-align: center;
+ border: 2px solid transparent;
+}
+
+.function-card:hover {
+ transform: translateY(-8px);
+ box-shadow: 0 12px 20px rgba(0, 0, 0, 0.15);
+ border-color: #4a90e2;
+}
+
+.function-icon {
+ width: 80px;
+ height: 80px;
+ border-radius: 50%;
+ background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin: 0 auto 20px;
+ font-size: 32px;
+ color: white;
+}
+
+.function-title {
+ font-size: 20px;
+ font-weight: 600;
+ color: #2d3748;
+ margin-bottom: 15px;
+}
+
+.function-description {
+ font-size: 14px;
+ color: #718096;
+ line-height: 1.6;
+ margin-bottom: 20px;
+}
+
+/* 系统状态样式 */
+.system-status {
+ background: white;
+ border-radius: 12px;
+ padding: 30px;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ margin-bottom: 40px;
+}
+
+.section-title {
+ font-size: 20px;
+ font-weight: 600;
+ color: #2d3748;
+ margin-bottom: 25px;
+ display: flex;
+ align-items: center;
+}
+
+.section-title i {
+ margin-right: 10px;
+ color: #4a90e2;
+}
+
+.status-list {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ gap: 20px;
+}
+
+.status-item {
+ display: flex;
+ align-items: center;
+ padding: 15px;
+ background: #f7fafc;
+ border-radius: 8px;
+ transition: background-color 0.3s ease;
+}
+
+.status-item:hover {
+ background: #edf2f7;
+}
+
+.status-indicator {
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ margin-right: 15px;
+}
+
+.status-indicator.online {
+ background-color: #38a169;
+}
+
+.status-indicator.warning {
+ background-color: #d69e2e;
+}
+
+.status-indicator.offline {
+ background-color: #e53e3e;
+}
+
+.status-label {
+ font-size: 14px;
+ font-weight: 600;
+ color: #2d3748;
+ margin-bottom: 5px;
+}
+
+.status-value {
+ font-size: 12px;
+ color: #718096;
+}
+
+/* 最近活动样式 */
+.recent-activities {
+ background: white;
+ border-radius: 12px;
+ padding: 30px;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+}
+
+.activity-list {
+ list-style: none;
+ padding: 0;
+}
+
+.activity-item {
+ display: flex;
+ align-items: center;
+ padding: 15px 0;
+ border-bottom: 1px solid #e2e8f0;
+}
+
+.activity-item:last-child {
+ border-bottom: none;
+}
+
+.activity-icon {
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ background: #f7fafc;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-right: 15px;
+ color: #4a90e2;
+}
+
+.activity-content {
+ flex: 1;
+}
+
+.activity-title {
+ font-size: 14px;
+ font-weight: 600;
+ color: #2d3748;
+ margin-bottom: 5px;
+}
+
+.activity-time {
+ font-size: 12px;
+ color: #718096;
+}
+
+/* 面包屑导航 */
+.breadcrumb {
+ display: flex;
+ align-items: center;
+ font-size: 14px;
+ color: #718096;
+ margin-top: 5px;
+}
+
+.breadcrumb a {
+ color: #4a90e2;
+ text-decoration: none;
+ transition: color 0.3s ease;
+}
+
+.breadcrumb a:hover {
+ color: #357abd;
+ text-decoration: underline;
+}
+
+.breadcrumb i {
+ margin: 0 10px;
+ font-size: 12px;
+}
+
+/* 当前时间显示 */
+.current-time {
+ font-size: 14px;
+ color: #718096;
+ background: #f7fafc;
+ padding: 8px 15px;
+ border-radius: 20px;
+}
+
+/* 页面标题 */
+.page-title {
+ font-size: 24px;
+ font-weight: 700;
+ color: #2d3748;
+ margin-bottom: 5px;
+}
+
+/* 内容头部 */
+.content-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 30px;
+}
+
+/* ==================== 主页样式 ==================== */
+/* 英雄区域 */
+.hero-section {
+ min-height: 80vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ position: relative;
+ overflow: hidden;
+}
+
+.hero-section::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: url('../images/hero-bg.jpg') center/cover no-repeat;
+ opacity: 0.1;
+}
+
+.hero-content {
+ max-width: 800px;
+ text-align: center;
+ position: relative;
+ z-index: 1;
+ padding: 40px;
+ background: rgba(255, 255, 255, 0.95);
+ border-radius: 20px;
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+ margin: 20px;
+}
+
+.hero-title {
+ font-size: 48px;
+ font-weight: 800;
+ margin-bottom: 20px;
+ color: #2c3e50;
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.hero-subtitle {
+ font-size: 20px;
+ margin-bottom: 30px;
+ color: #7f8c8d;
+ opacity: 0.9;
+ line-height: 1.6;
+}
+
+/* 功能区域 */
+.features-section {
+ padding: 80px 20px;
+ background-color: #f8f9fa;
+ border-radius: 20px;
+ margin: 40px 20px;
+}
+
+.section-title {
+ text-align: center;
+ font-size: 36px;
+ color: #2c3e50;
+ margin-bottom: 50px;
+}
+
+.features-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: 30px;
+ max-width: 1200px;
+ margin: 0 auto;
+}
+
+.feature-card {
+ background: white;
+ border-radius: 15px;
+ padding: 40px 30px;
+ text-align: center;
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
+}
+
+.feature-card:hover {
+ transform: translateY(-10px);
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
+}
+
+.feature-icon {
+ width: 80px;
+ height: 80px;
+ border-radius: 50%;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin: 0 auto 25px;
+ font-size: 32px;
+ color: white;
+}
+
+.feature-title {
+ font-size: 24px;
+ color: #2c3e50;
+ margin-bottom: 15px;
+}
+
+.feature-description {
+ color: #7f8c8d;
+ line-height: 1.6;
+}
+
+/* 行动号召区域 */
+.cta-section {
+ text-align: center;
+ padding: 80px 20px;
+}
+
+.cta-title {
+ font-size: 36px;
+ color: #2c3e50;
+ margin-bottom: 30px;
+}
+
+.cta-buttons {
+ display: flex;
+ justify-content: center;
+ gap: 20px;
+ flex-wrap: wrap;
+}
+
+/* 页脚 */
+.footer {
+ background-color: #2c3e50;
+ color: white;
+ padding: 60px 20px;
+ text-align: center;
+ border-radius: 20px 20px 0 0;
+ margin-top: 80px;
+}
+
+.footer-content {
+ max-width: 1200px;
+ margin: 0 auto;
+}
+
+.footer-links {
+ display: flex;
+ justify-content: center;
+ gap: 30px;
+ margin: 30px 0;
+ flex-wrap: wrap;
+}
+
+.footer-links a {
+ color: #bdc3c7;
+ text-decoration: none;
+ transition: color 0.3s ease;
+}
+
+.footer-links a:hover {
+ color: white;
+}
+
+.copyright {
+ margin-top: 30px;
+ color: #999;
+ font-size: 0.9rem;
+}
+
+/* 主页响应式设计 */
+@media (max-width: 768px) {
+ .hero-title {
+ font-size: 32px;
+ }
+
+ .hero-subtitle {
+ font-size: 16px;
+ }
+
+ .section-title {
+ font-size: 28px;
+ }
+
+ .cta-buttons {
+ flex-direction: column;
+ }
+
+ .cta-buttons .btn {
+ width: 100%;
+ }
+}
+
+/* ==================== 仪表板通用样式 ==================== */
+/* 仪表板布局 */
+.dashboard-container {
+ display: flex;
+ min-height: calc(100vh - 100px);
+}
+
+/* 侧边栏 */
+.sidebar {
+ width: 250px;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ position: sticky;
+ top: 0;
+ height: 100vh;
+ overflow-y: auto;
+}
+
+.sidebar.admin-sidebar {
+ background: linear-gradient(135deg, #9b59b6 0%, #8e44ad 100%);
+}
+
+.sidebar.teacher-sidebar {
+ background: linear-gradient(135deg, #3498db 0%, #2980b9 100%);
+}
+
+.sidebar-header {
+ padding: 30px 20px;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+ margin-bottom: 20px;
+}
+
+.user-info {
+ display: flex;
+ align-items: center;
+ gap: 15px;
+}
+
+.user-avatar {
+ width: 50px;
+ height: 50px;
+ border-radius: 50%;
+ background: rgba(255, 255, 255, 0.2);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 20px;
+ color: white;
+ font-weight: 600;
+}
+
+.user-details h3 {
+ color: white;
+ font-size: 16px;
+ margin-bottom: 5px;
+}
+
+.user-details p {
+ color: rgba(255, 255, 255, 0.7);
+ font-size: 12px;
+}
+
+.sidebar-menu {
+ list-style: none;
+ padding: 0 20px;
+}
+
+.sidebar-menu li {
+ margin-bottom: 10px;
+}
+
+.sidebar-menu a {
+ display: flex;
+ align-items: center;
+ gap: 15px;
+ padding: 15px;
+ color: rgba(255, 255, 255, 0.8);
+ text-decoration: none;
+ border-radius: 8px;
+ transition: all 0.3s ease;
+}
+
+.sidebar-menu a:hover {
+ background: rgba(255, 255, 255, 0.1);
+ color: white;
+}
+
+.sidebar-menu a.active {
+ background: rgba(255, 255, 255, 0.2);
+ color: white;
+}
+
+.sidebar-menu i {
+ width: 20px;
+ text-align: center;
+}
+
+/* 主内容区 */
+.main-content {
+ flex: 1;
+ padding: 30px;
+ background: #f8f9fa;
+ overflow-y: auto;
+}
+
+.content-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 30px;
+}
+
+.content-header h1 {
+ font-size: 28px;
+ color: #2c3e50;
+ margin: 0;
+}
+
+.breadcrumb {
+ color: #7f8c8d;
+ font-size: 14px;
+ margin-top: 5px;
+}
+
+
+
+/* 成绩表格 */
+.grades-table {
+ background: white;
+ border-radius: 15px;
+ overflow: hidden;
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
+}
+
+.table-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 20px 25px;
+ border-bottom: 1px solid #eee;
+}
+
+.table-title {
+ font-size: 1.3rem;
+ color: #333;
+ margin: 0;
+}
+
+.table-actions {
+ display: flex;
+ gap: 10px;
+}
+
+.table-container {
+ overflow-x: auto;
+}
+
+.grades-table table {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+.grades-table th {
+ background: #f8f9ff;
+ padding: 15px 20px;
+ text-align: left;
+ font-weight: 600;
+ color: #333;
+ border-bottom: 1px solid #eee;
+}
+
+.grades-table td {
+ padding: 15px 20px;
+ border-bottom: 1px solid #eee;
+}
+
+.grades-table tr:hover {
+ background: #f8f9ff;
+}
+
+/* 成绩徽章 */
+.grade-badge {
+ display: inline-block;
+ padding: 5px 15px;
+ border-radius: 20px;
+ font-size: 12px;
+ font-weight: 600;
+ color: white;
+}
+
+.grade-badge.A {
+ background: #2ecc71;
+}
+
+.grade-badge.B {
+ background: #3498db;
+}
+
+.grade-badge.C {
+ background: #f39c12;
+}
+
+.grade-badge.D {
+ background: #e74c3c;
+}
+
+/* 查看按钮 */
+.view-btn {
+ background: #f8f9fa;
+ color: #495057;
+ border: 1px solid #dee2e6;
+ padding: 8px 16px;
+ border-radius: 5px;
+ font-size: 14px;
+ cursor: pointer;
+ transition: all 0.3s ease;
+}
+
+.view-btn:hover {
+ background: #e9ecef;
+ border-color: #adb5bd;
+}
+
+/* 仪表板响应式设计 */
+@media (max-width: 992px) {
+ .dashboard-container {
+ flex-direction: column;
+ }
+
+ .sidebar {
+ width: 100%;
+ height: auto;
+ position: static;
+ }
+
+ .sidebar-menu {
+ display: flex;
+ overflow-x: auto;
+ padding: 10px 20px;
+ }
+
+ .sidebar-menu li {
+ margin-bottom: 0;
+ margin-right: 10px;
+ }
+
+ .sidebar-menu a {
+ white-space: nowrap;
+ }
+}
+
+@media (max-width: 768px) {
+ .main-content {
+ padding: 20px;
+ }
+
+ .content-header {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 15px;
+ }
+
+ .stats-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .table-actions {
+ flex-direction: column;
+ }
+
+ .function-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .status-list {
+ grid-template-columns: 1fr;
+ }
+}
+
+/* ==================== 管理页面通用样式 ==================== */
+/* 页面容器 */
+.page-container {
+ padding: 20px;
+}
+
+/* 页面头部 */
+.page-header {
+ margin-bottom: 30px;
+}
+
+.page-header h1 {
+ font-size: 28px;
+ color: #2c3e50;
+ margin-bottom: 10px;
+}
+
+.page-header p {
+ color: #7f8c8d;
+ font-size: 16px;
+ margin: 0;
+}
+
+/* 过滤区域 */
+.filter-section {
+ background: white;
+ border-radius: 10px;
+ padding: 20px;
+ margin-bottom: 30px;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+}
+
+.filter-title {
+ font-size: 18px;
+ font-weight: 600;
+ color: #2c3e50;
+ margin-bottom: 20px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.filter-form {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 15px;
+}
+
+.filter-row {
+ display: flex;
+ gap: 15px;
+ flex-wrap: wrap;
+}
+
+.filter-group {
+ display: flex;
+ flex-direction: column;
+ min-width: 200px;
+}
+
+.filter-group label {
+ font-size: 14px;
+ color: #666;
+ margin-bottom: 5px;
+}
+
+/* 表单样式 */
+.entry-form {
+ background: white;
+ border-radius: 12px;
+ padding: 30px;
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
+}
+
+.form-row {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ gap: 20px;
+ margin-bottom: 20px;
+}
+
+.form-group {
+ margin-bottom: 20px;
+}
+
+.form-group label {
+ display: block;
+ font-size: 14px;
+ color: #666;
+ margin-bottom: 8px;
+}
+
+.form-control {
+ width: 100%;
+ padding: 12px 15px;
+ border: 1px solid #ddd;
+ border-radius: 8px;
+ font-size: 14px;
+ transition: border-color 0.3s ease;
+}
+
+.form-control:focus {
+ outline: none;
+ border-color: #667eea;
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
+}
+
+
+
+/* 操作按钮 */
+.action-buttons {
+ display: flex;
+ gap: 10px;
+}
+
+.btn-add {
+ background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
+ color: white;
+ border: none;
+ padding: 10px 20px;
+ border-radius: 8px;
+ font-size: 14px;
+ cursor: pointer;
+ transition: all 0.3s ease;
+}
+
+.btn-add:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 5px 15px rgba(67, 233, 123, 0.3);
+}
+
+/* 管理页面响应式设计 */
+@media (max-width: 768px) {
+ .filter-form {
+ grid-template-columns: 1fr;
+ }
+
+ .filter-row {
+ flex-direction: column;
+ }
+
+ .filter-group {
+ min-width: 100%;
+ }
+
+ .form-row {
+ grid-template-columns: 1fr;
+ }
+
+ .action-buttons {
+ flex-direction: column;
+ }
+}
\ No newline at end of file
diff --git a/frontend/html/admin_dashboard.html b/frontend/html/admin_dashboard.html
new file mode 100644
index 0000000..a93f4c7
--- /dev/null
+++ b/frontend/html/admin_dashboard.html
@@ -0,0 +1,353 @@
+
+
+
+
+
+ 学生成绩管理系统 - 管理员仪表板
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
1,248
+
总用户数
+
+ 12 本周新增
+
+
+
+
+
+
+
+
+
+
3,567
+
学生总数
+
+ 45 本周新增
+
+
+
+
+
+
+
+
+
+
+
+
89
+
课程总数
+
+ 3 本周新增
+
+
+
+
+
+
+
+
+
+
+
+
用户管理
+
+ 管理所有用户账户,包括添加、编辑、删除用户,设置用户角色和权限。
+
+
+
+
+
+
+
+
+
学生管理
+
+ 管理学生信息,包括学籍管理、班级分配、信息维护和批量导入导出。
+
+
+
+
+
+
+
+
+
教师管理
+
+ 管理教师信息,包括教师分配、课程安排、权限设置和绩效考核。
+
+
+
+
+
+
+
+
+
成绩统计
+
+ 查看全校成绩统计,生成分析报告,支持图表展示和数据导出。
+
+
+
+
+
+
+
+
+
+ 系统状态
+
+
+
+
+
+
数据库服务
+
运行正常 | 响应时间: 12ms
+
+
+
+
+
+
Web服务器
+
运行正常 | 在线用户: 156
+
+
+
+
+
+
文件存储
+
使用率: 65% | 剩余: 35GB
+
+
+
+
+
+
备份服务
+
上次备份: 2天前 | 建议立即备份
+
+
+
+
+
+
+
+
+
+ 最近活动
+
+
+ -
+
+
+
+
+
新增学生用户
+
10分钟前 | 操作人: 管理员
+
+
+ -
+
+
+
+
+
修改教师信息
+
1小时前 | 操作人: 管理员
+
+
+ -
+
+
+
+
+
生成成绩统计报告
+
3小时前 | 操作人: 系统
+
+
+ -
+
+
+
+
+
导出用户数据
+
5小时前 | 操作人: 管理员
+
+
+ -
+
+
+
+
+
系统设置更新
+
1天前 | 操作人: 管理员
+
+
+
+
+
+
+
+
+
+