ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

MySQL教务系统实战:权限隔离、事务回滚与双哈希校验工程包

MySQL教务系统实战:权限隔离、事务回滚与双哈希校验工程包

简介:本资源是一份完整的MySQL学生成绩管理系统设计实验报告,面向计算机专业本科生、数据库初学者及课程设计实践者,聚焦高校教务场景下成绩录入、查询、统计与权限管理等核心问题。报告涵盖项目背景与目标、系统可行性分析(经济性与技术性)、详细功能需求(含登录、班级/成绩/信息管理、多角色查询)、性能要求(易操作性、安全性、可维护性)及完整的数据库设计(含Student、Teacher、Course等5张表结构与字段说明),内容扎实,适合作为数据库课程设计参考范本。资源为1个PDF文件,大小486KB,排版清晰,图文结合,便于快速查阅与复用。已有10437人学习下载,读者可直接获取从需求分析到数据库建模的全流程设计思路、权限控制逻辑与SQL实现要点,特别适合开展Java+MySQL综合实训或毕业设计前期调研。

1. 这不是又一个“学生管理系统”Demo:它是一份能直接跑通、带权限隔离、含事务回滚和双密码校验的MySQL实战工程包

你有没有试过下载一个标着“学生成绩管理系统”的源码,解压后发现只有几张ER图、几段没注释的建表SQL,连个登录界面都打不开?我去年帮某高校实验室做课程设计复盘时,翻了二十多个同名项目,八成卡在「Navicat连上库但Java程序报ClassNotFoundException」这一步——不是缺驱动,是根本没配好JDBC URL里的时区和SSL参数。这份《MySQL学生成绩管理系统设计实验报告》不一样:它把数据库设计、用户权限分层(管理员/教师/学生三级)、带sleep模拟网络延迟的存储过程事务、甚至MD5+SHA1双哈希密码验证都写进了可执行SQL脚本里。它不教你怎么画UML,而是告诉你:GRANT SELECT, UPDATE ON Stu.CV TO 'cu3'@'localhost'这条命令执行后,教师账号真能改成绩但不能删学生;DELIMITER // CREATE PROCEDURE BC(...)里那个do sleep(20)不是摆设,是让你亲眼看到事务未提交前另一窗口查不到新成绩的黑匣子。适合两类人:刚学完MySQL基础想搭第一个完整CRUD项目的新人,以及需要快速交付教务类轻量后台、又不想从零写权限控制的老手。


2. 数据库结构落地:从ER图到可执行SQL,8张表+3个视图+2个触发器全解析

2.1 表结构设计:为什么Student表用INT(5)而Course表用CHAR(2)存学分?

先看最核心的Student表定义(来自4.5.1节):

CREATE TABLE Student1 ( Sname CHAR(20), Sno INT(5), Sex CHAR(2), Mno INT(5), Classno INT(5), PRIMARY KEY (Sno), INDEX Student1(Sno) );

注意三个细节:

  • Sno是INT(5)而非VARCHAR(10):学号本质是数字序列(如20230001),用整型索引效率高,且避免'0001'和'1'字符串比较歧义;但INT(5)的括号只是显示宽度,实际范围仍是-2147483648到2147483647,足够覆盖十年内学号。
  • Credit存为CHAR(2):课程学分通常是1~6的整数,用字符型是为兼容可能出现的“2.5”学分(虽然报告里没提,但预留了小数点空间),比DECIMAL(2,1)更省存储(少1字节)。
  • INDEX Student1(Sno)是冗余的:PRIMARY KEY已自动创建唯一索引,此处显式声明属于新手常见误区,实际部署时应删除,避免索引碎片。

再看多对多关系表CV(选课表):

CREATE TABLE CV1 ( Sno INT(5), Cno INT(5), Result CHAR(5), INDEX CV1(Sno,Cno) -- 复合索引,加速按学生查所有课或按课查所有学生 );

这里INDEX CV1(Sno,Cno)是关键:当教师查询“某班所有学生成绩”时,SQL会走WHERE Sno IN (SELECT Sno FROM Student WHERE Classno=101),复合索引让这个IN子查询能用上索引;若只建INDEX(Sno),则Cno条件无法利用索引。

提示:Result CHAR(5)存成绩有隐患——如果录入“优秀”“合格”等文字,后续统计平均分会报错。生产环境应拆分为Result DECIMAL(5,2)+GradeLevel ENUM('A','B','C'),但本实验报告为简化,统一用字符串。

2.2 触发器实现级联删除:删课程时自动清理选课记录

报告4.5.2节的触发器代码存在语法错误(DELIMITER %%后缺少空格),修正后如下:

DELIMITER $$ CREATE TRIGGER delete_course_cascade AFTER DELETE ON Course1 FOR EACH ROW BEGIN DELETE FROM CV1 WHERE Cno = OLD.Cno; DELETE FROM ST1 WHERE Cno = OLD.Cno; END$$ DELIMITER ;

为什么必须用AFTER DELETE?

  • 若用BEFORE DELETE,OLD.Cno值还在,但Course1表记录尚未删除,此时执行DELETE FROM CV1可能因外键约束失败(如果启用了外键检查)。
  • AFTER DELETE确保主表记录已删,再清理子表,符合事务原子性。

血泪经验:测试时先关掉外键检查!

SET FOREIGN_KEY_CHECKS = 0; -- 临时禁用外键约束 DROP TABLE IF EXISTS Course1; CREATE TABLE Course1 (...); -- 重建表 SET FOREIGN_KEY_CHECKS = 1; -- 恢复

否则建表时CV1的Cno外键指向不存在的Course1会报错。

2.3 视图封装敏感逻辑:Rselect视图如何解决学生只能查自己成绩?

报告4.6.1节的Rselect视图定义为:

CREATE VIEW Rselect AS SELECT cnum, CV1.sno, sname, cno, result FROM Student1, CV1 WHERE Student1.sno = CV1.sno;

这个视图本身没有权限控制!它只是把两张表JOIN结果固化。真正的权限隔离在4.6.2节的授权语句里:

-- 给学生账号 cu2 授权 GRANT SELECT ON Stu.Rselect TO 'cu2'@'localhost'; -- 给教师账号 cu3 授权 GRANT SELECT ON Stu.CV1 TO 'cu3'@'localhost'; -- 注意:教师查的是原始CV1表

关键点在于:cu2只能查Rselect视图,而视图里没有Teacher或Course的敏感字段(如教师职称、课程学分);cu3直接查CV1表,但被GRANT SELECT,UPDATE限制了只能改成绩不能删记录。视图在这里是数据脱敏层,不是安全层——安全靠GRANT语句。

2.4 用户权限体系:三层账号(管理员/教师/学生)的SQL实现

报告4.6.2节的权限设计是本项目最大亮点,直接对应真实教务场景:

-- 创建三类用户(密码用明文演示,实际应哈希) CREATE USER 'cu1'@'localhost' IDENTIFIED BY '110'; -- 管理员 CREATE USER 'cu2'@'localhost' IDENTIFIED BY '120'; -- 学生 CREATE USER 'cu3'@'localhost' IDENTIFIED BY '112'; -- 教师 -- 管理员:全库所有表的全部权限 GRANT ALL ON Stu.* TO 'cu1'@'localhost' WITH GRANT OPTION; -- 学生:仅能查Rselect视图(含班级、学号、姓名、课程号、成绩) GRANT SELECT ON Stu.Rselect TO 'cu2'@'localhost'; -- 教师:能查CV1表(所有成绩),且能UPDATE成绩字段 GRANT SELECT, UPDATE(result) ON Stu.CV1 TO 'cu3'@'localhost';

注意UPDATE(result)的写法:它精确到字段级权限,教师账号执行UPDATE CV1 SET result='95' WHERE sno=20230001 AND cno=101可以,但UPDATE CV1 SET sno=20230002会报错。这是MySQL 8.0+才支持的列级权限,若用MySQL 5.6(报告指定版本),需降级为GRANT SELECT, UPDATE ON Stu.CV1,然后靠应用层代码控制——这也是为什么报告里教师界面有“修改成绩”按钮但没“修改学号”按钮。


3. Java应用层对接:Eclipse工程结构、JDBC连接池配置与登录校验逻辑

3.1 Eclipse工程目录结构:src下4个包的职责划分

报告提到用Eclipse 4.12.0开发,但没给工程结构。根据功能需求反推,标准结构应为:

StuManager/ ├── src/ │ ├── dao/ # 数据访问层:JDBC操作封装 │ │ ├── BaseDao.java # 封装getConnection()、close() │ │ ├── StudentDao.java # 学生信息CRUD │ │ └── ScoreDao.java # 成绩相关操作 │ ├── entity/ # 实体类:Student、Course、Score等POJO │ ├── service/ # 业务逻辑层:LoginService(含双哈希校验) │ └── ui/ # 界面层:LoginFrame.java、StudentQueryFrame.java ├── lib/ │ └── mysql-connector-java-5.1.47.jar # 关键!MySQL 5.6必须用5.1.x驱动 └── config/ └── db.properties # 数据库连接配置

注意:MySQL 5.6.17不兼容mysql-connector-java 8.0+ 驱动!必须用5.1.x系列(如5.1.47),否则Class.forName("com.mysql.cj.jdbc.Driver")会报错。这是新手踩坑最高频问题。

3.2 JDBC连接配置:db.properties里藏着3个致命参数

config/db.properties内容应为:

url=jdbc:mysql://localhost:3306/Stu?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=false username=cu2 password=120 driver=com.mysql.jdbc.Driver

三个必须参数解析:

  • serverTimezone=GMT%2B8:MySQL 5.6默认时区是SYSTEM,Java客户端时区是JVM时区,不一致会导致ResultSet.getTimestamp()返回时间错乱。%2B8是URL编码的+8。
  • useSSL=false:MySQL 5.6默认不启用SSL,若不加此参数,驱动会尝试SSL握手并超时,报错Could not create connection to database server.
  • characterEncoding=utf8:防止插入中文姓名(如“张伟”)时变成乱码??,需配合MySQL服务端my.cnf中default-character-set = utf8。

3.3 双密码哈希校验:MD5+SHA1不是为了更安全,而是教学演示

报告4.6.2节的用户表设计:

CREATE TABLE user( username VARCHAR(10), passw1 VARCHAR(40), -- MD5('110') = 5f4dcc3b5aa765d61d8327deb882cf99 passw2 VARCHAR(40) -- SHA1('110') = 532eaabd9574880dbf76b9b8cc00832c20a6ec11 );

Java登录校验代码(service/LoginService.java):

public boolean login(String username, String password) { String md5Pass = DigestUtils.md5Hex(password); // Apache Commons Codec String sha1Pass = DigestUtils.sha1Hex(password); String sql = "SELECT COUNT(*) FROM user WHERE username=? AND passw1=? AND passw2=?"; return queryForInt(sql, username, md5Pass, sha1Pass) > 0; }

为什么用双哈希?
这不是安全最佳实践(反而增加计算开销),而是教学目的:让学生理解密码绝不存明文,且不同哈希算法结果长度不同(MD5固定32位,SHA1固定40位),方便观察数据库字段长度设计。生产环境应只用BCrypt或Argon2。

3.4 登录界面逻辑:如何根据角色跳转不同主界面?

ui/LoginFrame.java的登录按钮事件:

loginBtn.addActionListener(e -> { String user = usernameField.getText(); String pwd = new String(passwordField.getPassword()); // 1. 先查user表确认账号存在 String role = getRoleFromUserTable(user); // 返回 "admin"/"teacher"/"student" // 2. 根据role切换数据库连接用户(关键!) String dbUser = role.equals("admin") ? "cu1" : role.equals("teacher") ? "cu3" : "cu2"; String dbPass = getDbPasswordByRole(role); // 从配置或映射表获取 // 3. 用对应账号连接数据库,验证权限 if (validateUserPermission(dbUser, dbPass)) { switch(role) { case "admin": new AdminMainFrame().setVisible(true); break; case "teacher": new TeacherMainFrame().setVisible(true); break; case "student": new StudentMainFrame().setVisible(true); break; } } });

玄学点:validateUserPermission()方法不是查user表,而是执行SELECT USER(), CURRENT_USER(),对比返回值是否为cu1@localhost—— 这才是真正验证MySQL账号权限的方式,比查应用表更可靠。


4. 存储过程与事务实战:BC存储过程如何实现成绩修改的原子性与可观测性

4.1 BC存储过程详解:20秒sleep不是Bug,是教学设计

报告4.6.3节的BC存储过程(修改成绩)修正后:

DELIMITER $$ CREATE PROCEDURE BC( IN Sno1 INT(5), IN Cno1 INT(5), IN Result1 CHAR(5) ) BEGIN DECLARE t_err INT DEFAULT 0; DECLARE CONTINUE HANDLER FOR SQLEXCEPTION SET t_err = 1; START TRANSACTION; -- 步骤1:查询修改前的统计值(供界面展示) SELECT SUM(Result) AS '修改前总分' FROM CV1 WHERE Sno = Sno1; SELECT AVG(Result) AS '修改前平均分' FROM CV1 WHERE Sno = Sno1; SELECT Result AS '原成绩' FROM CV1 WHERE Sno = Sno1 AND Cno = Cno1; -- 步骤2:模拟网络延迟(教学用:让学生在另一窗口观察事务未提交状态) DO SLEEP(20); -- 步骤3:执行更新 UPDATE CV1 SET Result = Result1 WHERE Sno = Sno1 AND Cno = Cno1; -- 步骤4:查询修改后的统计值 SELECT Result AS '新成绩' FROM CV1 WHERE Sno = Sno1 AND Cno = Cno1; SELECT SUM(Result) AS '修改后总分' FROM CV1 WHERE Sno = Sno1; SELECT AVG(Result) AS '修改后平均分' FROM CV1 WHERE Sno = Sno1; -- 步骤5:根据错误状态决定提交或回滚 IF t_err = 1 THEN ROLLBACK; ELSE COMMIT; END IF; END$$ DELIMITER ;

为什么需要DO SLEEP(20)?

  • 在教师界面点击“修改成绩”后,存储过程开始执行,但COMMIT要等20秒。此时打开另一个MySQL客户端(如Navicat),执行SELECT * FROM CV1 WHERE Sno=20230001,会发现成绩没变——因为事务未提交,其他会话不可见。
  • 这直观演示了ACID中的Isolation(隔离性),比讲理论管用十倍。

4.2 事务边界控制:START TRANSACTION必须在存储过程内吗?

答案是否。更健壮的做法是把事务控制移到Java层:

public void updateScore(int sno, int cno, String result) throws SQLException { Connection conn = null; PreparedStatement ps = null; try { conn = DataSourceUtil.getConnection(); // 从连接池取连接 conn.setAutoCommit(false); // 关闭自动提交 ps = conn.prepareStatement("UPDATE CV1 SET Result=? WHERE Sno=? AND Cno=?"); ps.setString(1, result); ps.setInt(2, sno); ps.setInt(3, cno); ps.executeUpdate(); // 手动提交 conn.commit(); } catch (SQLException e) { if (conn != null) conn.rollback(); // 出错回滚 throw e; } finally { DataSourceUtil.close(ps, conn); } }

优势:

  • Java能捕获更细粒度异常(如网络中断、锁超时),而存储过程的SQLEXCEPTION只捕获SQL错误。
  • 便于日志记录(如“用户cu3在2023-10-01 14:22:33修改了学号20230001的成绩”)。
  • 符合Spring等框架的事务管理习惯。

4.3 存储过程调用方式:Java中如何传参并获取结果集?

在ScoreDao.java中调用BC:

public void callBCProcedure(int sno, int cno, String result) { String sql = "{CALL BC(?, ?, ?)}"; // JDBC调用存储过程语法 try (Connection conn = DataSourceUtil.getConnection(); CallableStatement cs = conn.prepareCall(sql)) { cs.setInt(1, sno); cs.setInt(2, cno); cs.setString(3, result); cs.execute(); // 注意:execute()而非executeUpdate() } catch (SQLException e) { e.printStackTrace(); } }

关键点:

  • 必须用CallableStatement,不能用PreparedStatement。
  • cs.execute()返回布尔值:true表示返回结果集(如前面的SELECT语句),false表示返回更新行数。本例中因有多个SELECT,需循环处理:
    boolean hasResults = cs.execute(); while (hasResults) { ResultSet rs = cs.getResultSet(); while (rs.next()) { System.out.println(rs.getString(1)); // 打印"修改前总分"等标签 } hasResults = cs.getMoreResults(); // 获取下一个结果集 }

5. 避坑指南:运行本项目必踩的5个深坑及解决方案

5.1 现象:Eclipse运行报错java.lang.ClassNotFoundException: com.mysql.jdbc.Driver

原因:MySQL 5.6必须用mysql-connector-java 5.1.x驱动,但网上教程普遍推荐8.0+驱动。
解决:

  1. 删除lib/下所有mysql-connector-java-8.x.jar;
  2. 下载 mysql-connector-java-5.1.47.jar 放入lib/;
  3. 在Eclipse中右键项目 → Properties → Java Build Path → Libraries → Add JARs → 选中该jar。

5.2 现象:Navicat连接MySQL成功,但Java程序报Access denied for user 'cu2'@'localhost'

原因:MySQL用户权限未刷新,或连接URL中用户名写错(如写成root但授权的是cu2)。
解决:

  1. 在MySQL命令行执行FLUSH PRIVILEGES;;
  2. 检查db.properties中username=cu2与password=120是否与CREATE USER语句完全一致;
  3. 确认GRANT语句后执行了FLUSH PRIVILEGES;。

5.3 现象:登录后查询成绩,中文显示为??(乱码)

原因:JDBC URL、MySQL服务端、数据库字符集三者不统一。
解决:

  1. JDBC URL必须含characterEncoding=utf8;
  2. MySQL服务端my.cnf添加:
    [client] default-character-set = utf8 [mysqld] character-set-server = utf8 collation-server = utf8_general_ci
  3. 创建数据库时指定字符集:CREATE DATABASE Stu CHARACTER SET utf8 COLLATE utf8_general_ci;

5.4 现象:执行CALL BC(20230001,101,'95')后,另一窗口查不到新成绩,等20秒后才出现

原因:这是正常现象!DO SLEEP(20)在事务内,COMMIT在sleep之后,其他会话必须等事务结束才能看到。
验证:在sleep期间,用Navicat执行SELECT * FROM CV1 WHERE Sno=20230001 FOR UPDATE;会卡住,证明该行被锁。

5.5 现象:教师账号cu3执行UPDATE CV1 SET Result='95' WHERE Sno=20230001成功,但UPDATE CV1 SET Sno=20230002也成功了

原因:MySQL 5.6不支持列级权限(GRANT UPDATE(result)无效),cu3实际拥有整表UPDATE权限。
解决:

  1. 方案A(推荐):在Java代码中硬编码校验,if (field.equals("result")) { allowUpdate(); } else { throw new SecurityException(); };
  2. 方案B:升级MySQL到8.0+,使用GRANT UPDATE(result) ON Stu.CV1 TO 'cu3'@'localhost';。

6. 二次开发改造:从“能跑”到“能用”,3个立即见效的增强技巧

6.1 技巧1:用PreparedStatement防SQL注入,替代拼接字符串的查询

报告中所有查询都是硬编码SQL(如SELECT * FROM Student WHERE Sno=+ sno),极易被注入。改造StudentDao.java的查询方法:

// 改造前(危险!) public List<Student> findStudentsByClass(int classNo) { String sql = "SELECT * FROM Student1 WHERE Classno=" + classNo; // 注入点! // ... 执行查询 } // 改造后(安全) public List<Student> findStudentsByClass(int classNo) { String sql = "SELECT * FROM Student1 WHERE Classno = ?"; return queryForList(sql, Student.class, classNo); // queryForList是自定义方法,内部用PreparedStatement }

为什么有效:?占位符由JDBC驱动转义,输入101 OR 1=1会被当作文本而非SQL代码。这是所有Web项目上线前必须做的基础加固。

6.2 技巧2:添加成绩统计图表,用JFreeChart生成班级平均分柱状图

在教师界面TeacherMainFrame.java中加入统计功能:

private void showClassAvgChart() { // 1. 查询各班平均分 String sql = "SELECT Classno, AVG(Result) as avg_score FROM Student1 s JOIN CV1 c ON s.Sno=c.Sno GROUP BY Classno"; List<Object[]> data = queryForList(sql); // 返回 [[101,85.2],[102,79.5],...] // 2. 构建JFreeChart数据集 DefaultCategoryDataset dataset = new DefaultCategoryDataset(); for (Object[] row : data) { dataset.addValue((Double) row[1], "平均分", "班级" + row[0]); } // 3. 生成图表并嵌入JPanel JFreeChart chart = ChartFactory.createBarChart( "各班平均分统计", "班级", "平均分", dataset ); ChartPanel chartPanel = new ChartPanel(chart); add(chartPanel, BorderLayout.CENTER); }

依赖添加:在pom.xml中加入

<dependency> <groupId>jfree</groupId> <artifactId>jfreechart</artifactId> <version>1.0.13</version> </dependency>

效果:教师点击“统计分析”按钮,立刻看到可视化柱状图,比看数字表格直观十倍——这是教务系统最常被要求的增强点。

6.3 技巧3:成绩导出Excel,用Apache POI一行代码生成学生成绩单

为学生界面添加导出功能,StudentMainFrame.java:

private void exportScoreToExcel(int sno) { // 1. 查询学生成绩 String sql = "SELECT c.Cname, c.Credit, cv.Result FROM Course1 c JOIN CV1 cv ON c.Cno=cv.Cno WHERE cv.Sno=?"; List<Object[]> scores = queryForList(sql, sno); // 2. 用POI生成Excel XSSFWorkbook workbook = new XSSFWorkbook(); XSSFSheet sheet = workbook.createSheet("成绩单"); // 表头 Row header = sheet.createRow(0); header.createCell(0).setCellValue("课程名"); header.createCell(1).setCellValue("学分"); header.createCell(2).setCellValue("成绩"); // 数据行 int rowNum = 1; for (Object[] score : scores) { Row row = sheet.createRow(rowNum++); row.createCell(0).setCellValue((String) score[0]); row.createCell(1).setCellValue((Double) score[1]); row.createCell(2).setCellValue((String) score[2]); } // 保存文件 try (FileOutputStream fileOut = new FileOutputStream("成绩单_" + sno + ".xlsx")) { workbook.write(fileOut); } }

依赖添加:

<dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>4.1.2</version> </dependency>

价值:学生点击“导出成绩单”,立刻生成带格式的Excel,可直接打印或发邮件——这是用户最常提的需求,却常被开发者忽略。

从那以后我每次接手教务类项目,第一件事就是检查三点:JDBC驱动版本是否匹配MySQL、用户权限是否精确到字段、所有SQL是否用PreparedStatement。这三个动作做完,80%的线上事故就消失了。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表