news 2026/10/22 17:33:19

基于JSP的CIMS课程设计源码实战:从数据库设计到答辩避坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于JSP的CIMS课程设计源码实战:从数据库设计到答辩避坑

简介:这是一份面向软件工程、计算机科学等相关专业学生的CIMS课程设计源码包。项目以JSP动态页面为核心,由Java处理业务逻辑、JavaScript负责前端交互、CSS完成页面样式,完整呈现了从数据库建表到页面渲染的Web开发链路。压缩包共212个文件,大小约15.2MB,包含100个XML配置文件、29个JAR依赖包、14个JAVA源文件、24个CLASS编译文件、12个JSP页面、4个CSS样式表和4个JavaScript脚本,另含SQL初始化脚本、属性文件以及少量图片资源;XML用于配置参数与依赖装配,JAR支撑运行环境,SQL帮助快速还原数据库结构,目录划分清楚,便于按业务模块查阅,已有103人学习下载。阅读源码可以系统理解控制器、业务实现层、分页Bean等典型分层写法,看清JSP、Java类与数据库操作之间的调用关系,同时还能参考前端样式、脚本交互和表结构设计。对需要完成CIMS课程设计或入门Java Web开发的学生而言,这份源码包提供了可运行、可改造的完整学习样本。

1. “基于Java Server Pages技术的CIMS课程设计源码”到底在做什么

“基于Java Server Pages技术的CIMS课程设计源码”这句话拆开看,就是一套用JSP写的Web管理系统,加上一份可以交付的课程设计源码。CIMS在这类课设里,九成指的是Course Information Management System(课程信息管理系统),解决的是用户、课程、选课、成绩这几件事,跟制造业里的计算机集成制造系统不是一回事。做这个东西的人,通常是计算机相关专业正在赶Java Web课设的学生,需要一套能跑通、能讲明白、能应对答辩的源码,而不是一个花哨的Demo。这篇文章就把这个方向从数据库设计一路讲到答辩验证,包括五个常见的翻车点,照着做基本能把课设交上去。

2. 先把CIMS的模块边界画清楚:数据库设计与MVC分层

2.1 CIMS在课程设计里到底是什么:先确认范围再动手

“CIMS”这个缩写是第一个要确认的事。在制造业里,CIMS是Computer Integrated Manufacturing System,那是一个工业控制方向的大题目,不适合用JSP课程设计来做。课程设计语境下,老师要的基本是一个信息管理系统:学生能登录、浏览课程、选课、查成绩;管理员能维护课程、查看选课情况、录入成绩。按这个范围做,规模正好适中,工作量在两周到一个月之间,答辩时也好讲。

我在接这类课设指导时,一般会先列一张功能矩阵,跟学生逐项确认。学生端:登录、修改个人信息、浏览课程列表(带分页和搜索)、选课/退课、查看已选课程和成绩。管理员端:登录、课程增删改、查看选课名单、录入成绩。这个矩阵决定数据库要建哪些表、Servlet要写多少个,先确认再动手能省掉后面一大半改动。不要一上来就追求功能多,JSP课设的评分重点在于“你能否说清一个完整的请求生命周期”,而不是功能数量。

2.2 四张核心表:用户、课程、选课、成绩的建表SQL

数据库设计是这类系统的地基。我习惯用四张表:用户表、课程表、选课表、成绩表。用户表带一个role字段区分管理员和学生,课程表带一个selected_count字段记录已选人数,选课表和成绩表用联合唯一索引防止重复数据。下面是常用建表脚本,数据库用MySQL 5.7,字符集选utf8mb4。

CREATE DATABASE cims DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE cims; -- 用户表:学生和管理员共用一张表,用role区分 CREATE TABLE tb_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录名', password VARCHAR(64) NOT NULL COMMENT '密码,课设阶段可用明文,进阶用MD5', real_name VARCHAR(50) DEFAULT NULL COMMENT '姓名', role CHAR(1) NOT NULL DEFAULT '1' COMMENT '0=管理员,1=学生', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 课程表:max_students限制容量,selected_count为当前已选人数 CREATE TABLE tb_course ( id INT PRIMARY KEY AUTO_INCREMENT, course_name VARCHAR(100) NOT NULL, credit DECIMAL(3,1) DEFAULT 2.0 COMMENT '学分', teacher_name VARCHAR(50) DEFAULT NULL, classroom VARCHAR(50) DEFAULT NULL, max_students INT DEFAULT 50, selected_count INT DEFAULT 0, course_time VARCHAR(100) DEFAULT NULL COMMENT '上课时间,如 周一3-4节' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 选课表:student_id+course_id加唯一索引,从数据库层面挡住重复选课 CREATE TABLE tb_enroll ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, enroll_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_student_course (student_id, course_id), CONSTRAINT fk_enroll_student FOREIGN KEY (student_id) REFERENCES tb_user(id), CONSTRAINT fk_enroll_course FOREIGN KEY (course_id) REFERENCES tb_course(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 成绩表:score为空表示未录入,学生端显示“未出分” CREATE TABLE tb_score ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, score DECIMAL(5,2) DEFAULT NULL, UNIQUE KEY uk_score_student_course (student_id, course_id), CONSTRAINT fk_score_student FOREIGN KEY (student_id) REFERENCES tb_user(id), CONSTRAINT fk_score_course FOREIGN KEY (course_id) REFERENCES tb_course(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

参数说明里有几个点值得单独提。username设为UNIQUE,业务上登录名不能重复,这是数据库层面的第一道校验。tb_enroll的联合唯一索引是我特别强调的,很多学生只在Servlet里写“先查再插”,两个请求同时到达时就能插出重复记录,加了唯一索引后数据库会直接报错,属于兜底方案。外键在课设阶段建议保留,虽然会影响删除顺序,但能让ER图和数据字典更完整,答辩时是一个加分点。DECIMAL(3,1)和DECIMAL(5,2)分别是学分和成绩的类型,不建议用FLOAT,避免浮点误差。

2.3 分层选择:JSP+Servlet+DAO,而不是一上来就SSM

JSP课设最常见的结构是纯JSP页面加JDBC,或者JSP+Servlet+DAO。我见到的大部分java课程设计案例源码都采用后者,因为它足够体现MVC思想,又不会像SSM那样把原理包在黑匣子里。答辩时老师通常会问“请求是怎么从页面走到数据库的”,用JSP+Servlet能把这个链路讲得很清楚:JSP发请求,Servlet接收参数并调用DAO,DAO执行SQL并返回结果,Servlet把结果放进request或session,最后forward或redirect到JSP渲染。

项目目录建议按下面组织,源码交付时结构清晰也是一项分。

cims-course-design/ ├── src/ │ ├── com/cims/dao/ -- UserDao、CourseDao、EnrollDao、ScoreDao │ ├── com/cims/model/ -- User、Course、Enroll、Score 实体类 │ ├── com/cims/servlet/ -- LoginServlet、CourseServlet、EnrollServlet等 │ └── com/cims/filter/ -- AuthFilter、EncodingFilter ├── web/ │ ├── login.jsp │ ├── student/ -- 学生端页面:courseList.jsp、myCourses.jsp │ ├── admin/ -- 管理端页面:courseManage.jsp、scoreInput.jsp │ └── WEB-INF/ │ ├── web.xml │ └── lib/ -- mysql-connector、jstl 等 jar 包 └── sql/ └── cims.sql

实体类对应四张表,DAO里只写SQL和JDBC操作,Servlet里只写参数接收和流程控制,JSP里尽量用EL表达式和JSTL,少写Java代码块。这个分层的好处是,任何一层出问题都能单独定位,也方便在文档里画架构图。

2.4 环境组合与版本坑:JDK 8 + Tomcat 9 + MySQL 5.7

环境版本是JSP课设里最容易翻车的部分。我常用的组合是JDK 8 + Tomcat 9 + MySQL 5.7,开发工具用Eclipse或IntelliJ IDEA都行。Tomcat 9对应Servlet 4.0规范,包名还是javax.servlet,和绝大多数课设代码兼容。Tomcat 10把包名改成了jakarta.servlet,很多老代码直接跑不起来,这个放到避坑章节细说,这里先记住结论:课设阶段别用Tomcat 10。

如果用Maven管理依赖,核心依赖如下,注意scope是provided,编译时需要但Tomcat运行时已经自带。

<dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jsp-api</artifactId> <version>2.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.49</version> </dependency> </dependencies>

如果不用Maven,就把mysql-connector-java和jstl的jar包直接拷到web/WEB-INF/lib目录下,这是JSP课设常用的传统做法,IDE里部署时会自动带上。JDBC 5.1.49连接MySQL 5.7没有问题,连MySQL 8.0则建议换8.0.33。版本选定后不要随意升级,很多“昨天还能跑今天就不行”的问题都是版本漂移造成的。

3. 用JSP+Servlet把登录与角色权限跑通

3.1 登录页的JSP写法:POST提交、提示信息与回车键

登录页是整套系统的入口,代码很简单,但有几个细节课设答辩时经常被追问。form的method必须用post,action指向LoginServlet,用户密码通过post提交而不是出现在URL上。提示信息用request域变量回显,登录失败后返回登录页并提示原因。

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>课程信息管理系统 - 登录</title> </head> <body> <h2>课程信息管理系统登录</h2> <%-- 登录失败时由LoginServlet转发回来,message非空则显示 --%> <c:if test="${not empty message}"> <p style="color:red">${message}</p> </c:if> <form action="loginServlet" method="post"> 用户名:<input type="text" name="username" required><br> 密 码:<input type="password" name="password" required><br> <button type="submit">登录</button> </form> </body> </html>

这里有个容易忽略的点:页面顶部如果没有引入JSTL的taglib,<c:if>标签会直接报错。要么在文件头加上<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>,要么改用<%= request.getAttribute("message") %>这种脚本表达式。我建议学一下JSTL,答辩时老师看到EL表达式会比看到满屏Java脚本印象好。required属性是HTML5自带的前端校验,能挡住大部分空提交。

3.2 UserDao与JDBC连接:连接串里的三个必写参数

DAO层负责和数据库打交道。连接串是最容易踩坑的地方,尤其是中文乱码和SSL警告。下面是UserDao的核心代码,重点看getConnection方法里的连接串参数。

package com.cims.dao; import com.cims.model.User; import java.sql.*; public class UserDao { private static final String DRIVER = "com.mysql.jdbc.Driver"; private static final String URL = "jdbc:mysql://localhost:3306/cims" + "?useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai"; private static final String USER = "root"; private static final String PASSWORD = "123456"; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { throw new RuntimeException("JDBC驱动未找到,请检查mysql-connector的jar包"); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public User findByUsernameAndPassword(String username, String password) { String sql = "SELECT * FROM tb_user WHERE username = ? AND password = ?"; try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { User u = new User(); u.setId(rs.getInt("id")); u.setUsername(rs.getString("username")); u.setRealName(rs.getString("real_name")); u.setRole(rs.getString("role")); return u; } } } catch (SQLException e) { e.printStackTrace(); } return null; } }

连接串里的三个参数是血泪经验。第一个useSSL=false,不加的话MySQL 5.7会输出一大段SSL警告,虽然不影响运行,但控制台刷屏会干扰你排查其他问题。第二个characterEncoding=utf8,这决定了中文能不能正确写入数据库,不加它后面必现乱码。第三个serverTimezone=Asia/Shanghai,不写的话连接MySQL 8.0直接报时区错误,5.7可能不报但建议统一加上。另外注意,这里用了PreparedStatement而不是Statement,参数用?占位,从源头避开SQL注入。

3.3 LoginServlet:查询、session保存、角色跳转

Servlet是控制层,负责接收参数、调用DAO、决定跳转方向。登录逻辑里有三个细节值得写清楚:密码校验放在DAO里还是Servlet里、登录成功后用户对象放哪里、不同角色跳转到哪里。

package com.cims.servlet; import com.cims.dao.UserDao; import com.cims.model.User; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; import java.io.IOException; @WebServlet("/loginServlet") public class LoginServlet extends HttpServlet { private UserDao userDao = new UserDao(); @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String username = req.getParameter("username"); String password = req.getParameter("password"); User user = userDao.findByUsernameAndPassword(username, password); if (user == null) { // 登录失败,把提示放进request,forward回登录页 req.setAttribute("message", "用户名或密码错误"); req.getRequestDispatcher("/login.jsp").forward(req, resp); return; } // 登录成功:用户信息放进session,跨页面保持登录状态 HttpSession session = req.getSession(); session.setAttribute("loginUser", user); session.setMaxInactiveInterval(30 * 60); // 30分钟无操作自动过期 // 按角色分派首页 if ("0".equals(user.getRole())) { resp.sendRedirect(req.getContextPath() + "/admin/courseManage.jsp"); } else { resp.sendRedirect(req.getContextPath() + "/student/courseList.jsp"); } } }

逻辑说明:登录失败用forward而不是redirect,这样request里的message能在登录页显示;登录成功用redirect,这是Post/Redirect/Get模式的最简实现,防止用户刷新浏览器时重复提交登录表单。session里存的是User对象而不是用户名,这样后续页面可以直接取${sessionScope.loginUser.realName}显示姓名。setMaxInactiveInterval设置30分钟会话超时,这是为了防止登录态永远不过期,答辩时老师问到会话管理你可以直接答出这段。

3.4 过滤器做权限:拦截未登录访问与管理员路径

登录能过,权限控制才是系统完整性的体现。常见做法是写一个AuthFilter,拦截所有*.jsp和*.do请求,做两件事:判断session里有没有loginUser,没有就跳回登录页;再根据访问路径判断角色是否匹配。这里给出过滤器的核心逻辑。

package com.cims.filter; import com.cims.model.User; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; @WebFilter("/*") public class AuthFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; // 登录页、登录Servlet、静态资源直接放行 String uri = req.getRequestURI(); if (uri.endsWith("/login.jsp") || uri.endsWith("/loginServlet") || uri.contains("/css/") || uri.contains("/js/")) { chain.doFilter(request, response); return; } HttpSession session = req.getSession(false); User user = (session == null) ? null : (User) session.getAttribute("loginUser"); if (user == null) { // 未登录:重定向到登录页,带上来源路径便于登录后跳回 resp.sendRedirect(req.getContextPath() + "/login.jsp"); return; } // 管理员路径拦截:student目录只允许学生,admin目录只允许管理员 if (uri.contains("/admin/") && !"0".equals(user.getRole())) { resp.sendError(HttpServletResponse.SC_FORBIDDEN, "无权限访问"); return; } if (uri.contains("/student/") && !"1".equals(user.getRole())) { resp.sendError(HttpServletResponse.SC_FORBIDDEN, "无权限访问"); return; } chain.doFilter(request, response); } }

这段代码里有一个容易被忽视的细节:req.getSession(false)。传false的意思是如果当前没有session就返回null,而不是新创建一个。很多翻车案例就是这里写成getSession(),导致每次请求都创建一个新session,未登录用户也能绕过后面的判空逻辑。过滤器的顺序也值得一提,如果有编码过滤器,应该在web.xml里把EncodingFilter放在AuthFilter前面,保证先处理乱码再做权限判断。

4. 课程管理核心页面:列表分页与选课提交的完整链路

4.1 课程列表页:JSTL循环、分页参数与搜索条件拼接

课程列表是学生端最核心的页面,涉及三个点:用JSTL循环渲染表格、分页参数如何传给Servlet、带搜索条件时SQL如何拼接。先看CourseServlet里的分页查询逻辑。

protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); // 分页参数:默认第1页,每页5条 int pageNum = 1; int pageSize = 5; String pageNumStr = req.getParameter("pageNum"); if (pageNumStr != null && !pageNumStr.isEmpty()) { pageNum = Integer.parseInt(pageNumStr); } // 搜索条件:按课程名模糊查询 String keyword = req.getParameter("keyword"); if (keyword == null) { keyword = ""; } CourseDao courseDao = new CourseDao(); List<Course> courseList = courseDao.findByPage(pageNum, pageSize, keyword); int totalCount = courseDao.countByKeyword(keyword); // 计算总页数,Math.ceil向上取整,避免最后一页数据丢失 int totalPages = (int) Math.ceil(totalCount * 1.0 / pageSize); req.setAttribute("courseList", courseList); req.setAttribute("pageNum", pageNum); req.setAttribute("totalPages", totalPages); req.setAttribute("keyword", keyword); req.getRequestDispatcher("/student/courseList.jsp").forward(req, resp); }

对应的DAO查询方法里,SQL用LIMIT分页,MySQL的语法是LIMIT offset, size,其中offset等于(pageNum - 1) * pageSize。搜索条件拼接时要注意,keyword为空时不能拼出WHERE course_name LIKE '%%'这种浪费查询的语句,可以在Java里判断一下再拼接。分页参数从前端页面通过链接传回Servlet,JSP页面里循环显示课程列表核心代码如下。

<table border="1" cellpadding="8"> <tr> <th>课程名</th><th>学分</th><th>教师</th> <th>已选/容量</th><th>操作</th> </tr> <c:forEach items="${courseList}" var="c"> <tr> <td>${c.courseName}</td> <td>${c.credit}</td> <td>${c.teacherName}</td> <td>${c.selectedCount}/${c.maxStudents}</td> <td> <a href="enrollServlet?courseId=${c.id}">选课</a> </td> </tr> </c:forEach> </table> <%-- 分页导航 --%> <c:if test="${pageNum > 1}"> <a href="courseServlet?pageNum=${pageNum-1}&keyword=${keyword}">上一页</a> </c:if> <span>第 ${pageNum} / ${totalPages} 页</span> <c:if test="${pageNum < totalPages}"> <a href="courseServlet?pageNum=${pageNum+1}&keyword=${keyword}">下一页</a> </c:if>

分页这里有个很考细节的地方:翻页链接必须带上keyword参数,否则从第二页开始搜索条件就丢了,结果页和搜索初衷对不上。这是JSP课设里最常见的分页bug,我抽查学生代码时十次能遇到八次。

4.2 选课提交:事务处理、容量检查与防重复

选课是整张表设计里最需要谨慎的操作,因为一个选课动作要同时做三件事:往tb_enroll插入记录、把tb_course的selected_count加1、可能还要初始化一条tb_score记录。这三件事必须在一个事务里完成,否则会出现“选课成功的提示,但数据没插进去”这种半截状态。下面是EnrollServlet的核心逻辑。

protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { HttpSession session = req.getSession(); User user = (User) session.getAttribute("loginUser"); int courseId = Integer.parseInt(req.getParameter("courseId")); Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交,开启事务 // 第一步:统计这门课当前已选人数 String countSql = "SELECT selected_count, max_students FROM tb_course WHERE id = ? FOR UPDATE"; PreparedStatement ps1 = conn.prepareStatement(countSql); ps1.setInt(1, courseId); ResultSet rs = ps1.executeQuery(); int selected = 0, max = 0; if (rs.next()) { selected = rs.getInt("selected_count"); max = rs.getInt("max_students"); } if (selected >= max) { conn.rollback(); req.setAttribute("message", "该课程已满员"); req.getRequestDispatcher("/student/courseList.jsp").forward(req, resp); return; } // 第二步:插入选课记录,依赖联合唯一索引兜底 String insertSql = "INSERT INTO tb_enroll(student_id, course_id) VALUES(?, ?)"; PreparedStatement ps2 = conn.prepareStatement(insertSql); ps2.setInt(1, user.getId()); ps2.setInt(2, courseId); ps2.executeUpdate(); // 第三步:更新已选人数 String updateSql = "UPDATE tb_course SET selected_count = selected_count + 1 WHERE id = ?"; PreparedStatement ps3 = conn.prepareStatement(updateSql); ps3.setInt(1, courseId); ps3.executeUpdate(); conn.commit(); resp.sendRedirect("myCourses.jsp"); } catch (Exception e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } req.setAttribute("message", "选课失败,可能已选过该课程"); req.getRequestDispatcher("/student/courseList.jsp").forward(req, resp); } finally { if (conn != null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }

这段里有两个容易被答辩追问的地方。一个是SELECT ... FOR UPDATE,这是行级锁,目的是防止两个用户同时选同一门最后一门课,出现超卖问题。第一次看到这个写法的学生可能会觉得多余,但并发场景下确实是必要的,但课设阶段如果只做演示,可以简化成不带FOR UPDATE的普通查询。另一个是重复选课问题,虽然数据库有联合唯一索引,但程序里仍然要在插入前用student_id和course_id先查一次,给用户一个友好的“你已选过这门课”提示,而不是让数据库抛一个看不懂的异常。

4.3 成绩与个人信息展示页面:两种典型的JSP写法

成绩查询页做起来不复杂,但从这个页面能看出一个人对JSP语法的熟练度。很多网上流传的java课程设计案例源码里,成绩页直接简化为“所有学生在所有课程的成绩列表”,但这不符合CIMS的定位。正确的是学生登录后只能看到自己已选课程的成绩,管理员能看到选课名单并录入成绩。

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <h3>我的成绩</h3> <table border="1"> <tr><th>课程名</th><th>学分</th><th>成绩</th></tr> <c:forEach items="${scoreList}" var="s"> <tr> <td>${s.courseName}</td> <td>${s.credit}</td> <td> <c:choose> <c:when test="${s.score == null}">未出分</c:when> <c:otherwise>${s.score}</c:otherwise> </c:choose> </td> </tr> </c:forEach> </table>

jsp个人信息展示页面在课设里也是必做的。我最推荐的写法是直接用EL表达式读取session里的loginUser,页面打开就能看到当前登录人的姓名、角色、用户名,不需要再查一次数据库。修改信息时单独做一个updateProfileServlet,接收表单参数后更新数据库,并同步更新session里的user对象,否则页面会一直显示旧数据。

<p>姓名:${sessionScope.loginUser.realName}</p> <p>用户名:${sessionScope.loginUser.username}</p> <p>角色:${sessionScope.loginUser.role == '0' ? '管理员' : '学生'}</p>

EL表达式里的三元运算符是这个页面的一个小亮点,比写一堆<c:if>干净,答辩时如果老师指着角色这一行问怎么写判断,这算一个加分的小细节。

4.4 删除课程时的外键保护:业务上有余量才算完

管理员删除课程这个功能,稍不注意就会翻车。因为tb_enroll和tb_score都通过外键指向tb_course,如果已经有学生选了这门课,直接DELETE会触发外键约束异常,页面报一个看不懂的SQL错误。常见做法是删除前先检查选课人数。

public boolean deleteCourse(int courseId) { String checkSql = "SELECT COUNT(*) FROM tb_enroll WHERE course_id = ?"; // 如果count > 0,返回false,由Servlet提示“该课程已有学生选课,不能删除” String deleteSql = "DELETE FROM tb_course WHERE id = ?"; Connection conn = null; try { conn = getConnection(); conn.setAutoCommit(false); PreparedStatement ps1 = conn.prepareStatement(checkSql); ps1.setInt(1, courseId); ResultSet rs = ps1.executeQuery(); if (rs.next() && rs.getInt(1) > 0) { conn.rollback(); return false; } PreparedStatement ps2 = conn.prepareStatement(deleteSql); ps2.setInt(1, courseId); ps2.executeUpdate(); conn.commit(); return true; } catch (SQLException e) { e.printStackTrace(); return false; } finally { // 关闭连接、还原自动提交模式 } }

逻辑说明:先查后删,选课人数大于0就拒绝删除。有人会问,为什么不靠外键的ON DELETE CASCADE级联删除?效果上确实可以,但课设阶段不建议用,因为级联会静默删掉学生的选课记录和成绩记录,这个行为不好在答辩时解释,业务上也不合理——课程删掉,学生的历史成绩不应随之消失。先查后删虽然多了一条SQL,但每一步行为都可控、可讲。

5. JSP课程设计常见的5个翻车点:从环境到答辩

5.1 Tomcat 10上烧出的ClassNotFoundException

现象:代码完全按教程写,Eclipse或IDEA里部署时提示找不到javax.servlet.Filter,或者运行时抛java.lang.ClassNotFoundException: javax.servlet.Filter。原因:Tomcat 10起,Servlet API的包名从javax.servlet迁移到了jakarta.servlet,你项目里所有import javax.servlet.*的代码在Tomcat 10上无法加载。解决:课设阶段最省事的是把Tomcat版本换回9.0.x;如果坚持用Tomcat 10,就要把所有javax.servlet替换成jakarta.servlet,同时Maven依赖改成jakarta.servlet:jakarta.servlet-api:5.0.0。我的建议是不要在这上面浪费时间,一遍跑通比追求新版重要。

5.2 数据库中文乱码:连接串、页面、过滤器三处要统一

现象:页面上输入中文保存后,数据库里变成???,或者页面显示一堆乱码。原因:字符集链路里某一环断了,常见有三处。JDBC连接串没写characterEncoding=utf8;JSP页面头部的contentType没带charset=UTF-8;Servlet里没执行request.setCharacterEncoding("UTF-8")。解决:三处全部统一。第一处见3.2节的连接串,第二处是JSP第一行<%@ page contentType="text/html;charset=UTF-8" language="java" %>,第三处写一个EncodingFilter,在doFilter里设置request和response的编码,顺序上放在AuthFilter之前。数据库表和字段如果已经建错了字符集,用ALTER TABLE tb_user CONVERT TO CHARACTER SET utf8mb4;补救。

5.3 前端改了不生效:浏览器缓存与target目录

现象:修改了JSP页面的布局,刷新浏览器还是旧页面;或者页面改了,但重启Tomcat后又变回改动前的样子。原因:两种情况要区分。一种是浏览器缓存,旧页面被缓存在本地;另一种是IDE的部署机制,JSP文件被拷贝到了target或webapps目录下的工程副本里,你改的是源文件但部署目录没更新。解决:浏览器层面按Ctrl+F5强制刷新,或者在网址后面加?t=20250601这种时间戳查询参数绕过缓存;IDE层面在Tomcat部署配置里勾选“自动重载”,或每次改完手动重新发布。这个坑看起来小,但在答辩演示现场特别容易造成尴尬。

5.4 8080端口被占用:一条命令找出占位进程

现象:启动Tomcat时报Port 8080 required by Tomcat v9.0 Server at localhost is already in use,或者启动后无法访问。原因:机器上有其他进程占用了8080端口,常见的是之前启动的Tomcat没关闭干净,或者其他开发工具占了端口。解决:Windows下在命令行执行netstat -ano | findstr 8080,找到占用端口的进程PID,再用taskkill /PID 1234 /F结束它;Linux或Mac下用lsof -i:8080查看进程,kill -9 PID结束。也可以把Tomcat的端口改成8081,在conf/server.xml里修改<Connector port="8081">,但改端口治标不治本,还是查清占用的来源更好。

5.5 刷新页面导致重复提交:选课记录重复、表单二次提交

现象:学生选课时点了一次“选课”,页面没反应,又点了一次,结果数据库里出现了两条选课记录。或者录入成绩时按了一次提交,刷新后成绩被重复录入。原因:表单和链接的GET请求被浏览器重复发送,Servlet没有做防重处理。解决:后台在插入前先按student_id和course_id查一下数据库,已有记录就返回“你已选过该课程”,这个逻辑配合tb_enroll表上的唯一索引,双保险;成绩录入同理,先查该组合是否已存在,决定UPDATE还是INSERT。另一个常见的防重手段是Post/Redirect/Get模式,Servlet处理完数据后用resp.sendRedirect(...)重定向到一个GET页面,这样刷新时是刷新新的页面,不会重复提交表单。

6. 答辩前的验证清单与三个低成本改造方向

答辩前的验证清单不需要写进文档,但一定要自己过一遍。按下面表格逐项操作,能排除掉大多数现场翻车。

验证项操作方式预期结果
登录校验输入错误密码页面出现“用户名或密码错误”,停留在登录页
角色跳转分别用管理员和学生账号登录管理员进入admin目录页面,学生进入student目录页面
未登录拦截直接访问courseList.jsp跳转回login.jsp
分页与搜索输入关键词搜索,翻到第二页URL里保留keyword参数,数据正确过滤
选课与重复选课同一学生选同一门课两次第二次提示“已选过该课程”
容量限制把max_students改成1再选课第二人选课时提示“课程已满员”
删除保护删除已有选课记录的课程提示“该课程已有学生选课,不能删除”
中文显示新增含中文的课程数据页面和数据库均为中文,无乱码

三个低成本改造方向,每一个都能在答辩时讲出“我在基础功能之上还做了什么”。第一个是把明文密码换成MD5加盐存储,只需要在注册和登录时调用一段加密方法,改动量极小,但能体现安全意识。第二个是给登录页加一个简单的数字验证码,用Session存验证码、Servlet生成图片,这个功能在答辩时单独拿出来能讲几分钟,属于性价比极高的加分项。第三个是成绩导出Excel,课设阶段不需要引入POI依赖,直接用逗号拼接生成CSV文件,十分钟就能写完。

我见过不少学生把课设功能堆得很满,但到最后连“登录流程走了哪些代码”都说不清。真正的源码交付不是让老师看懂每一行,而是让他看到你的分层思路、关键实现和解决问题的能力。把每个Servlet的输入输出、每个表的关系整理成一份设计说明,比多写两个花哨页面更有价值。这套CIMS做完再回头加功能,你会发现JSP的整个运行链路已经不再是黑匣子。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 8:54:53

基于MCP协议为AI Agent接入实时搜索能力实战

1. 为什么你的 Agent 需要一个"实时搜索"外挂做过 AI Agent 开发的人大概都有过这种体验&#xff1a;模型本身推理能力不差&#xff0c;工具调用逻辑也跑通了&#xff0c;但一旦问它"今天有什么新闻""某家公司最新融资情况""某个开源项目最…

作者头像 李华
网站建设 2026/10/14 1:17:41

case条件语句的跨语言实战:从SQL CASE WHEN到FPGA独热码设计

写case语句的文章其实挺容易写“空”的——要么变成语法手册&#xff0c;要么变成面试八股。但这玩意儿恰恰是日常写代码里绕不开的“小事”&#xff1a;条件一多&#xff0c;if-else拖一屏&#xff0c;case一上&#xff0c;逻辑立刻清爽。更重要的是&#xff0c;它不只是“多个…

作者头像 李华
网站建设 2026/10/22 17:33:18

基于Matlab的深度残差网络多特征多分类实战

我在实际做数据分类项目时发现&#xff0c;很多入门者拿到"深度残差网络"就一头扎进图像分类里&#xff0c;但实际上它在表格数据、传感器时序数据、多特征融合分类场景里同样能打。尤其是手里只有Matlab、又不想切Python生态的人&#xff0c;完全可以用Deep Learnin…

作者头像 李华
网站建设 2026/10/13 14:48:59

系统规划与管理师备考:智能制造章节考点解析与复习策略

备考系统规划与管理师时&#xff0c;大多数人翻开《系统规划与管理师教程》目录&#xff0c;会本能地先奔着ITIL、ITSS、服务生命周期这些"有逻辑可循"的章节去&#xff0c;把智能制造发展规划这类带行业背景的内容留到临考前临阵磨枪。我第一次备考也是这个套路&…

作者头像 李华
网站建设 2026/10/14 8:38:18

MySQL order by 性能优化实战:从 filesort 到索引设计的完整排查链路

上个月排查客服后台慢SQL&#xff0c;我被一条order by查询折磨了一个下午。MySQL order by性能优化&#xff0c;听起来是老生常谈&#xff0c;可真到线上出问题时&#xff0c;你依仗的绝不是“加个索引”这种一句话经验。SQL本身不长&#xff1a; SELECT * FROM order_info W…

作者头像 李华
网站建设 2026/10/17 12:02:50

微网经济调度中的场景生成与削减:从不确定性建模到随机优化落地

做微网动态经济调度的人&#xff0c;基本都会撞上同一个拦路虎&#xff1a;风机出力、光伏波动、负荷预测偏差这些不确定性&#xff0c;到底该怎么塞进优化模型里。如果你只取期望值&#xff0c;调度方案往往过于乐观&#xff0c;遇到极端天气直接抓瞎&#xff1b;如果你把所有…

作者头像 李华