简介面向数据库课程设计的学生信息管理系统基于JSP/Servlet/MySQL技术栈开发适合计算机相关专业学生完成课程项目或系统学习Java Web开发。系统覆盖用户登录注册、学生信息增删改查、成绩统计分析报表、数据可视化展示及权限分级管理后端逻辑与数据库操作完整可直接部署运行。压缩包共268个文件包含java源码、jsp页面、class编译文件、jar依赖库以及css/js等前端资源另有数据库脚本和docx说明文档整体约10.25MB便于按需查阅与二次开发。目前已吸引35人学习项目中附带的说明文件与文档有助于理解设计思路、数据库结构和部署流程是巩固JSPServletMySQL技术栈并快速搭建管理系统的实用案例。1. 技术栈与项目基调JSP、Servlet、MySQL为什么是数据库课程设计的标准答案打开任意一个学生信息管理系统项目包最常见的组合就是 JSP、Servlet 和 MySQL。这个标题描述的项目几乎就是数据库课程设计里的标准模板登录注册、学生信息增删改查、成绩统计分析报表、数据可视化展示再加权限分级。这套技术栈没有 Spring Boot 托底所有对象的创建、请求的转发、数据库连接的开关都是显式写出来的反而更容易看清楚一次 HTTP 请求从浏览器到 Servlet 再到 MySQL 的完整路径。我见过不少同学下载同类项目后跑不起来问题通常出在 MySQL 连接串时区缺失、Tomcat 与 JDK 版本不匹配或者 JSP 编译失败后不知道去哪看栈信息。下文的写法围绕“能不能复现”来组织登录注册怎么接 Session、增删改查的 DAO 怎么落、成绩统计的聚合 SQL 怎么组织、图表数据怎么通过 Servlet 输出 JSON 挂到 ECharts 上权限分级通过 Filter 按路径拦截实现。这套方案适合正在做课程设计、需要跑通并答辩的在校生也适合想补传统 Java Web 底子、想绕过框架看请求-响应-数据库这条线的从业者。2. 登录注册模块从 JSP 表单到 Servlet 的请求流转与会话保持2.1 表单提交与 Servlet 映射的配置细节使用 IDEA 创建 JSP 文件时默认会生成到src/main/webapp目录下浏览器里访问的是http://localhost:8080/项目名/register.jsp。表单的action直接指向一个 Servlet 的 URL 映射method固定为post避免用户名密码出现在 URL 上。老项目用web.xml注册 Servlet新一点的项目直接加WebServlet(/register)注解两种方式等价但web.xml在答辩时更好讲——老师能看到完整的映射关系。form action${pageContext.request.contextPath}/register methodpost input typetext nameusername placeholder用户名 required maxlength20 input typepassword namepassword placeholder密码 required maxlength32 input typepassword nameconfirmPassword placeholder确认密码 required button typesubmit注册/button /form${pageContext.request.contextPath}是 JSP 的 EL 表达式用来拼项目上下文路径。直接写死/student/register也可以跑但当项目名改掉时所有表单和链接都会失灵课程设计项目经常因为改上下文路径后出现大面积 404。JSP 文件在第一次被访问时由 Tomcat 编译成 Java 类编译后的 class 文件保存在Tomcat 安装目录/work/Catalina/localhost/项目名/org/apache/jsp目录下。如果你改了 JSP 却看不到效果排查路径就是先找到work目录下对应的_jsp.java看里面是不是旧代码确认后清掉整个work目录重启 Tomcat。这一步在开发阶段出现的频率远比你想象的高idea 里热部署偶尔失灵时直接走这条路径最快。2.2 注册逻辑密码加密存储与重复账号判断注册模块的 Servlet 里只有一个核心任务把用户提交的用户名密码写入数据库user表。但直接存明文密码在答辩时几乎必被追问所以常见做法是存 MD5 加盐后的密文。盐值可以取用户名的哈希也可以注册时生成一段随机字符串盐和密文一起存到salt字段。WebServlet(/register) public class RegisterServlet extends HttpServlet { private static final String SALT course-design-2025; protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { String username req.getParameter(username); String password req.getParameter(password); String confirm req.getParameter(confirmPassword); if (!password.equals(confirm)) { resp.sendRedirect(register.jsp?errorpassword_not_match); return; } String encrypted md5(username SALT password); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement( INSERT INTO user(username, password, salt, role) VALUES(?,?,?,student))) { ps.setString(1, username); ps.setString(2, encrypted); ps.setString(3, SALT); ps.executeUpdate(); resp.sendRedirect(login.jsp?messageregister_ok); } catch (SQLException e) { if (e.getErrorCode() 1062) { resp.sendRedirect(register.jsp?errorduplicate_user); } else { resp.sendRedirect(register.jsp?errordb_error); } } } }md5方法的实现用MessageDigest.getInstance(MD5)即可注意拿到 byte 数组后要转成十六进制字符串。数据库层面user表的username字段必须加唯一索引代码里先查询再插入存在并发空隙唯一索引兜底能直接让重复注册抛 1062 错误码代码里判断这个码再跳回提示页——这是课设项目里比较拿得出手的防重设计。注册时把salt一起入库登录时用username查出salt再拼一次算 MD5你不需要搞 BCrypt 一类的东西课程设计的时间预算有限老师问起来你只要答清楚“加盐防止彩虹表”就算过关。2.3 登录成功后的 Session 会话保持登录逻辑和注册几乎对称区别只在验证通过后要建立会话。这一块很多初学者会往 Cookie 里塞用户 ID 和角色这是不对的Cookie 会被前端篡改。正确做法是让 Tomcat 维护服务端 Session浏览器只拿到一个随机的JSESSIONID。User user userDao.findByUsername(username); if (user ! null user.getPassword().equals(encrypt(username, user.getSalt(), password))) { HttpSession session req.getSession(); session.setAttribute(loginUser, user); session.setAttribute(role, user.getRole()); session.setMaxInactiveInterval(1800); resp.sendRedirect(index.jsp); }登录成功的跳转建议用sendRedirect而不是forward。forward是服务端内部跳转地址栏保持 login.jsp刷新一次就重新提交一次登录表单redirect会发 302 让浏览器重新请求 index.jsp地址栏变化也更符合用户预期。setMaxInactiveInterval(1800)指定 30 分钟无操作后 Session 失效这个值也可以写在web.xml的session-config节点里不同角色想要不同超时时间时代码里设置更灵活。2.4 权限分级管理Filter 拦截与路径规则权限分级是课程设计里最容易做成摆设的模块很多项目只在页面隐藏了按钮直接访问 URL 照样能进管理页面。正确的做法是用 Filter 统一拦截。标题里的“权限分级管理”对应三个角色管理员、教师、学生。三个角色的可访问页面差异用一个路径前缀来表达例如管理员对应/admin/*教师对应/teacher/*学生对应/student/*。角色可访问路径典型操作admin/admin/*、/teacher/*、/student/*全部功能含删除与授权teacher/teacher/*、/student/*查询学生、录入成绩、查看统计student/student/*查询个人信息、查看成绩Filter 的url-pattern配成/*会拦截掉登录注册页和静态资源所以要放行 login.jsp、register.jsp 以及 css/js 目录。业界通用的做法是定义一个不需要登录的 whitelist在doFilter里先判断请求路径前缀。WebFilter(/*) public class AuthFilter implements Filter { private static final SetString WHITE_LIST Set.of(/login, /register, /css, /js, /images); public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; String path request.getRequestURI().substring(request.getContextPath().length()); boolean isWhite WHITE_LIST.stream().anyMatch(path::startsWith); HttpSession session request.getSession(false); if (isWhite || (session ! null session.getAttribute(loginUser) ! null)) { chain.doFilter(req, resp); return; } ((HttpServletResponse) resp).sendRedirect(login.jsp); } }在 Filter 里判断登录态之后角色判断放各自权限前缀的第二个 Filter 里或者直接在受保护 Servlet 的业务代码开头校验session.getAttribute(role)我一般倾向后一种课设项目 Servlet 数量不多写在业务代码里逻辑更直观。需要注意的顺序问题是 Filter 的dispatcher类型初次请求和forward进入的请求默认都走REQUEST分发如果你在 Servlet 里forward到 JSPJSP 会再次过一遍 Filter繁琐的校验写在 Filter 里容易在跳转时出现重定向循环出现 302 循环时优先检查 Filter 里是否放行了目标 JSP 路径。3. 学生信息增删改查JDBC 连接 MySQL 的工程化写法与分页3.1 数据库设计字段类型选型与连接参数学生信息表的字段需要覆盖课设最常见的展示项学号、姓名、性别、班级、出生日期、手机号。MySQL 安装配置完成后先用 Navicat 或命令行验证 3306 端口能连通再执行建表语句避免把表建到了默认数据库里。CREATE TABLE student ( id INT AUTO_INCREMENT PRIMARY KEY, student_no VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(50) NOT NULL, gender CHAR(1) DEFAULT 男, class_name VARCHAR(50), birthday DATE, phone VARCHAR(20), is_deleted TINYINT DEFAULT 0, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, KEY idx_class (class_name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;学号用VARCHAR(20)而不是INT因为学号不参与加减运算前面有 0 的学号用 INT 存会被截断。gender用CHAR(1)比用TINYINT更直观查询出来的结果直接就是“男/女”不用在前端再转一遍。create_time用TIMESTAMP有 2038 年问题但课设场景不会碰到这个边界。JDBC 连接串是初学者踩坑最多的地方。MySQL 8.x 的 JDBC 驱动要求显式指定时区少写serverTimezoneAsia/Shanghai会直接报Communications link failureuseSSLfalse避免本机开发时的 SSL 握手告警characterEncodingutf8对应 MySQL 侧的 utf8mb4注意两边字符集必须一致否则中文姓名插入后变成问号。jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/student_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 jdbc.usernameroot jdbc.password1234563.2 DAO 层写法PreparedStatement 防 SQL 注入的三种体现数据库增删改查的常见做法是单独建一个StudentDao类每个方法都走 JDBC 标准流程加载驱动、获取连接、预编译 SQL、填参数、执行、关闭资源。你可以在DBUtil里做连接管理也可以每个方法开头手动调DriverManager.getConnection前者代码量更少。查询学生信息的时候学生管理页面几乎必然带搜索框按姓名或班级模糊查。SQL 写成WHERE name LIKE % ? %是错的MySQL 的LIKE参数化不支持把百分号写在占位符外面应该用LIKE CONCAT(%, ?, %)。public ListStudent searchStudents(String keyword, int pageNo, int pageSize) { String sql SELECT id, student_no, name, gender, class_name, birthday, phone FROM student WHERE is_deleted 0 AND (name LIKE CONCAT(%, ?, %) OR student_no LIKE CONCAT(%, ?, %)) ORDER BY id DESC LIMIT ?, ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, keyword); ps.setString(2, keyword); ps.setInt(3, (pageNo - 1) * pageSize); ps.setInt(4, pageSize); try (ResultSet rs ps.executeQuery()) { ListStudent list new ArrayList(); while (rs.next()) { Student s new Student(); s.setId(rs.getInt(id)); s.setStudentNo(rs.getString(student_no)); s.setName(rs.getString(name)); s.setGender(rs.getString(gender)); s.setClassName(rs.getString(class_name)); s.setBirthday(rs.getDate(birthday) null ? null : rs.getDate(birthday).toLocalDate()); s.setPhone(rs.getString(phone)); list.add(s); } return list; } } catch (SQLException e) { throw new RuntimeException(查询学生列表失败, e); } }LIMIT ?, ?前面必须没有空格问题但真正要命的是分页偏移量(pageNo - 1) * pageSize的乘法要在 Java 里算完再传给 PreparedStatement不能直接在 SQL 里写LIMIT (pageNo - 1) * pageSize, pageSize——MySQL 的预编译语句允许表达式但 JDBC 驱动对占位符绑定要求是具体值。查总条数的 SQL 建议写成SELECT COUNT(*) FROM student WHERE is_deleted 0 AND (name LIKE ...)和列表查询保持完全一致的 WHERE 条件否则分页总页数会算错。总页数公式是(totalCount pageSize - 1) / pageSize这三个变量在控制层里算好再通过request.setAttribute转发给 JSP。3.3 增删改的完整闭环逻辑删除与前后端参数对齐新增和修改在 Servlet 层的区别只有一个新增是INSERT修改是UPDATE但表单提交过来的字段名完全一致。常见做法是在 JSP 的隐藏域里带id两个功能共用一个表单页面。Servlet 里先取id为空走新增否则走更新这样代码能省掉一个页面。删除这里我强烈建议用is_deleted逻辑删除。理由有两个成绩表的外键可能指向学生 ID物理删除直接报外键约束错误课设答辩时老师几乎必问“删除后数据去哪了”逻辑删除的答案更有价值。把DELETE FROM student换成UPDATE student SET is_deleted 1 WHERE id ?所有列表查询统一加is_deleted 0条件即可。更新操作的 SQL 有一个容易忽略的坑如果允许用户修改学号那UPDATE的 SET 子句里学生表和成绩表会连锁产生孤儿数据。我通常会保证学号在创建之后不可修改或者在界面上把学号字段设为readonly让数据库的唯一索引始终有效。3.4 事务边界批量操作和级联删除的统一提交如果你的课设扩展了“批量导入学生”或者“删除学生时同时删除其成绩”的功能就必须处理事务。没有事务的时候第一条 SQL 执行成功、第二条 SQL 抛异常数据库里就会残留半截数据。JDBC 事务的标准写法是关闭自动提交多个操作之后手动 commit出现异常回滚。关键点是这些操作必须使用同一个 Connection 对象很多同学在 DAO 层每个方法内部都连接数据库两个 DAO 各拿各的连接事务根本控制不住。public void batchImport(ListStudent students) { Connection conn DBUtil.getConnection(); try { conn.setAutoCommit(false); String sql INSERT INTO student(student_no, name, gender, class_name) VALUES(?,?,?,?); for (Student s : students) { try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, s.getStudentNo()); ps.setString(2, s.getName()); ps.setString(3, s.getGender()); ps.setString(4, s.getClassName()); ps.executeUpdate(); } } conn.commit(); } catch (SQLException e) { conn.rollback(); throw new RuntimeException(批量导入失败已回滚, e); } finally { DBUtil.close(conn); } }PreparedStatement在循环里每次创建和关闭没问题但如果你把ps.executeUpdate()放到 for 循环外只复用一次性能反而更差。真正要优化的方向是改用addBatch()和executeBatch()课设数据量通常只有几百条不需要这个优化但批量导入在循环里反复conn.prepareStatement会让 MySQL 预编译缓存快速膨胀能提一句executeBatch()代表你理解了 mysql 原理层面的连接复用。4. 成绩统计分析报表MySQL 聚合查询与报表接口设计4.1 成绩表设计一个评分维度拆出多少指标成绩统计的报表需求可以拆成两条线按课程汇总平均分、最高分、最低分、及格率按分数段统计人数分布。这两条线共用一张score表设计时把course_name而不是course_id直接存进表里虽然冗余了课程名但课设项目不需要维护单独的课程表查询时少一次 JOIN。CREATE TABLE score ( id INT AUTO_INCREMENT PRIMARY KEY, student_id INT NOT NULL, course_name VARCHAR(50) NOT NULL, score DECIMAL(5,1) NOT NULL, exam_date DATE, KEY idx_course (course_name), KEY idx_student (student_id), CONSTRAINT fk_score_student FOREIGN KEY (student_id) REFERENCES student(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;score用DECIMAL(5,1)而不是 FLOATFLOAT 是近似值算平均分后会出现 87.499999 之类的尾巴。外键约束加上之后删除学生会被阻止这和你第 3 章的逻辑删除设计正好形成呼应。4.2 聚合 SQL 的三种写法GROUP BY、CASE WHEN 与多维度统计按课程统计平均分的 SQL 是最基础的一张报表。GROUP BY course_name 之后AVG、MAX、MIN 三个聚合函数同时算出来一条记录就是课程汇总的一行。SELECT course_name, COUNT(*) AS exam_count, AVG(score) AS avg_score, MAX(score) AS max_score, MIN(score) AS min_score, SUM(CASE WHEN score 60 THEN 1 ELSE 0 END) / COUNT(*) * 100 AS pass_rate FROM score GROUP BY course_name ORDER BY avg_score DESC;分数段分布更考验 CASE WHEN 的写法。用一个SUM(CASE WHEN ...)就能把 90 分以上、80 到 90、70 到 80、60 到 70、60 以下的五段人数都算出来不需要写五条查询然后在前端拼表。SELECT course_name, SUM(CASE WHEN score 90 THEN 1 ELSE 0 END) AS excellent, SUM(CASE WHEN score 80 AND score 90 THEN 1 ELSE 0 END) AS good, SUM(CASE WHEN score 70 AND score 80 THEN 1 ELSE 0 END) AS medium, SUM(CASE WHEN score 60 AND score 70 THEN 1 ELSE 0 END) AS pass, SUM(CASE WHEN score 60 THEN 1 ELSE 0 END) AS fail FROM score GROUP BY course_name;这两条 SQL 跑出来的结果集可以直接作为报表接口的 JSON 数据源。分数的边界按 90 为“优秀”、80 为“良好”、70 为“中等”、60 为“及格”划分是大多数院校的通例如果你学校的标准不同改 CASE WHEN 里的边界值就行前端图形不用动。4.3 Servlet 返回 JSON报表接口与前端图表的解耦方式报表接口不应该在 JSP 里用c:forEach渲染表格原因很简单后面的 ECharts 图表需要 JSON 数组JSP 渲染出的 HTML 表格无法直接复用。常见做法是单独建一个ReportServlet根据请求参数typecourse|dist返回 JSON 字符串。课设项目不需要引入 fastjson用StringBuilder手动拼 JSON 完全够用而且拼的过程中你能更清楚 JSON 转义问题出在哪里。WebServlet(/report) public class ReportServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { String type req.getParameter(type); resp.setContentType(application/json;charsetutf-8); ListString categories new ArrayList(); ListDouble data new ArrayList(); String sql SELECT course_name, AVG(score) AS avg FROM score GROUP BY course_name ORDER BY avg DESC; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { categories.add(rs.getString(course_name)); data.add(rs.getDouble(avg)); } } catch (SQLException e) { throw new RuntimeException(e); } StringBuilder json new StringBuilder(); json.append({\categories\:[); for (int i 0; i categories.size(); i) { if (i 0) json.append(,); json.append(\).append(categories.get(i)).append(\); } json.append(],\data\:[); for (int i 0; i data.size(); i) { if (i 0) json.append(,); json.append(data.get(i)); } json.append(]}); resp.getWriter().write(json.toString()); } }字符集设置application/json;charsetutf-8是中文课程名不乱码的关键缺少这个头在浏览器里开起来是乱码但接口通信用浏览器直接访问${pageContext.request.contextPath}/report?typecourse验证两端是否对齐。4.4 报表慢的排查思路索引、EXPLAIN 与存储过程的取舍成绩表的数据量到几万行以后聚合查询会明显变慢。先给score表补一个联合索引覆盖最常见的查询路径。ALTER TABLE score ADD INDEX idx_course_score (course_name, score);这是“mysql 创建索引”里性价比最高的一条course_name精确匹配score用于聚合MySQL 可以不回表直接取出成绩数据。当老师问“你做了哪些优化”时这个联合索引比你在 Filter 里少写一个判断更能撑场面。查询变慢时用EXPLAIN SELECT ...看执行计划重点关注 type 字段的取值ALL是全表扫描ref或range是走索引的正常状态。关于存储过程老师如果问为什么不用你可以答“存储过程把业务逻辑下沉到数据库端调试和版本管理不方便课设项目的报表查询放 DAO 层更直观”——这是合理的取舍而不是你不会写。5. 数据可视化展示ECharts 从 MySQL 到图表的最小链路5.1 接入方式不引入构建工具直接放 echarts.min.js数据可视化在课程设计里的定位是“用图形把第 4 章的统计数据展示出来”。ECharts 是课设项目里最稳妥的方案官方提供的免费数据可视化大屏模板可以直接复用不需要 npm、不需要 webpack。把echarts.min.js下载到webapp/js目录JSP 页面里用script src${pageContext.request.contextPath}/js/echarts.min.js/script引入。5.2 柱状图、饼图、折线图的核心配置项fetch(contextPath /report?typedistcourseJavaWeb) .then(resp resp.json()) .then(res { let chart echarts.init(document.getElementById(pieChart)); chart.setOption({ title: { text: JavaWeb 成绩分布, left: center }, tooltip: { trigger: item }, series: [{ type: pie, radius: [40%, 70%], roseType: radius, data: res.data }] }); });radius: [40%, 70%]把饼图变成环形roseType: radius让每段扇形半径按数值大小变化视觉上比普通饼图更像“大屏”。折线图做“某课程历次考试平均分趋势”时SQL 里必须ORDER BY exam_date ASC否则图表横轴的时间顺序是乱的——这是你在答辩现场最容易修复又最容易忽略的问题。5.3 验证技巧先 curl 接口再调图表凡是图表不出数据我习惯先在浏览器地址栏直接访问/report?typecourse或者用 curl 确认接口返回的 JSON 结构是否和前端解析逻辑一致然后再打开开发者工具看 Network 面板里 fetch 请求的响应体。这一招能快速区分问题在 Servlet 后端、数据库 SQL还是前端 ECharts 的 option 配置——三个环节各自独立验证避免在 JSP 缓存和 Tomcat work 目录里瞎转。调试 ECharts 容器不显示时先检查承载图表的 div 是否有明确的width和heightinit一个宽度为 0 的 DOM 元素控制台不会报错但页面一定白屏。用setTimeout(() chart.resize(), 200)处理 Tab 页切换后的图表宽度塌陷是比较成熟的收尾处理方式。本文还有配套的精品资源点击获取