简介:这是一份基于Java Web的在线报名系统课程设计完整项目,主要面向Java初学者及正在准备课程设计的高校学生。系统实现了考生注册登录、个人资料维护、按身份证号或准考证号查询成绩、在线问答等考生端功能,也提供管理员对考生信息和成绩的查询、修改、删除、录入、报表生成,以及网上缴费模拟等后台模块,覆盖在线报名类系统常见业务流程,有助于理解JSP+Servlet模式下的分层架构、会话管理与数据库操作。压缩包共130个文件,约5.24MB,主要包含31个JSP页面、26个Java源文件及对应class编译文件,还附有Word版设计报告、数据库配置文件和界面图片素材,结构清晰,可导入IDE对照学习。已有617人学习下载。对于正在构思在线报名或考试管理系统的读者,这套源码提供了完整的前后端交互示例、数据库表设计思路和报告撰写参考,能够快速迁移到类似课程设计项目中。
1. 为什么选这个在线报名系统:三件套齐全的 JavaWeb 闭环项目
每年课设季,Java web在线报名系统这类题目总能排进题库热门前三。原因很直白:它把考生注册、登录验证、信息修改、成绩录入、组合查询、在线问答、网上缴费串成了一条完整业务链,一个系统同时覆盖 JSP、Servlet、JDBC、数据库设计、会话管理这些 JavaWeb 核心考点。这份资源是完整打包的课程设计,源码、数据库脚本、课程设计报告三件套都在压缩包里,适合两类人——准备交课设但不想从零搭建的学生,以及想拿一套带报告的完整项目反推 JavaWeb 代码组织方式的初学者。要理解它的价值,先得看清这套代码是怎么把业务流程落到表结构、Servlet 和 JSP 页面里的。
2. 先拆结构再动手:JSP + Servlet + JDBC 的经典三层架构怎么对上号
拿到压缩包别急着导入 IDE 跑,先花五分钟把包里有什么看清楚。这门课设的代码组织方式非常典型:表现层用 JSP 收集和展示数据,控制层用 Servlet 接收请求、调度业务,数据层用 JDBC 直连 MySQL 完成增删改查。没有 Spring、没有 MyBatis,就是最原始的 JavaWeb 三件套。这种结构放在今天看反而有它的好处——每一条请求从浏览器到数据库再返回页面,中间发生了什么一目了然,做课设答辩时老师问到底层原理你也能答得上来。
2.1 从 .class 类名反推六块业务:这个包里的类名就是功能地图
压缩包内层能看到一串编译后的 .class 文件和 .bmp 截图,类名本身已经把功能边界划清楚了。我把它们按模块归了一下:
| 类名 | 对应模块 | 对应摘要里的功能点 |
|---|---|---|
| EnterOnline.class | 考生注册与登录入口 | 考生注册、登录接口,凭数据库信息验证身份 |
| UserMessage.class | 个人信息管理 | 考生添加、查看、修改详细个人信息 |
| Score.class | 考生成绩查询 | 通过身份证号和准考证号查询成绩 |
| AdminEnterScore.class | 管理员成绩录入 | 网上成绩录入,修改成绩 |
| AdminCheckScore.class | 管理员成绩审核与查询 | 按组合方式查询成绩,产生成绩报表 |
| UserFeeOnline.class | 网上缴费模块 | 考生通过网上银行缴费,便于管理和查收 |
| Check.class | 登录状态校验 | 未登录用户访问受保护页面时拦截并跳转登录页 |
这种“类名即功能地图”的命名风格是这类课设源码包的通用规律。你拿到手先做一件事:把类名和题目要求的功能点逐个连线,连完线你就知道这套代码的完整性如何,哪些功能是核心、哪些只是凑数的。我之前拿到过一个报名系统,类名写得天花乱坠,一打开里面全是空方法,这种坑在下载源码时最容易踩。
2.2 标准目录结构对照:src、WebRoot、classes 与编译产物的正确关系
这个项目是 Eclipse 老式工程结构,和现在 Maven 项目的 src/main/java 布局不同。标准目录结构大致是:
OnlineSignup/ ├── src/ # Java 源码根目录 │ ├── com/xxx/... # 包结构,Servlet、DAO、Bean 都在这里 │ └── ... # 各个 .java 源文件 ├── WebRoot/ # Web 根目录(新版 IDE 里常叫 webapp) │ ├── WEB-INF/ │ │ ├── web.xml # Web 部署描述符,Servlet 映射、过滤器配置 │ │ └── lib/ # 依赖 jar 包,比如 mysql-connector-java │ ├── login.jsp # 登录页 │ ├── register.jsp # 注册页 │ ├── ... # 其他 JSP 页面 │ └── images/ # 静态资源 └── ...数据库脚本和报告文档注意包里出现 .class 文件,说明这是编译过之后才打包的。.class 是字节码文件,不是源码,真正能修改的是 src 目录下的 .java 文件。导入 IDE 后先检查两个地方:第一,JDK 版本和编译级别对不对;第二,Tomcat 运行环境有没有加到 Build Path。这两个配置错了,项目在 IDE 里能打开但一运行就报 ClassNotFoundException 或 404。我一般拿到这类老工程,第一步就是 Project → Clean 全量重新编译一次,把旧的 classes 产物清掉,避免 IDE 用缓存编译结果启动导致改代码不生效。
2.3 为什么不建议随手改成 Spring Boot:课设代码的学习边界
有些读者一看到 JSP 和 JDBC 直连就觉得“技术陈旧”,恨不得立刻改造成 Spring Boot + MyBatis Plus。我建议先按住这个冲动。这门课设的评分标准通常明确要求体现 JSP、Servlet、JavaBean、JDBC 这些基础技术点,你改成 Spring Boot 后反而没法展示“原始链路”的能力。更重要的是,这份代码的价值正是它的“不抽象”——你可以在 EnterOnline 和 Score 里清楚看到一次表单提交如何经过 Servlet 的 doPost、调用 DAO、拼 SQL、执行查询、把结果 setAttribute 回 JSP 的完整过程。这个链路理解透了,后面看 Spring MVC 的 DispatcherServlet、MyBatis 的 Mapper 代理,本质上都是在这个链路上包了一层壳。
当然,它的边界也很明显:没有 Maven 依赖管理,jar 包靠 WEB-INF/lib 手动维护;没有连接池,每个 DAO 操作都现开连接;SQL 有注入风险;密码是明文存储。这些都是课设项目的正常水平,也正好是你在报告“不足与改进”章节可以写的内容。先把这套代码跑通、读懂、能答辩,再谈改造。
3. 数据库设计与初始化:五张表串起报名、缴费到成绩查询
这套系统的数据库设计是整个项目的地基。摘要里的功能点看起来很多,但落到表结构上其实只有五张核心表:考生表、管理员表、成绩表、问答表、缴费表。理解这五张表的关系,你就理解了整个报名系统的数据流转。
需要说明的是,原始压缩包里的数据库脚本我没有执行过修改,下面这版表结构设计是基于摘要功能点反推的、在课设场景下最合理的方案。如果你拿到的 sql 文件字段名略有出入,按“职责对齐”的思路核对即可,不要死磕字段名。
3.1 表结构拆解:主键策略、冗余字段与各表职责
先看整体职责划分。考生表负责存储注册信息和报名资料;成绩表记录每个考生的各科分数;问答表存放考生提问和管理员回复;缴费表标记每位考生的缴费状态;管理员表单独存放后台账号。要点在成绩表和缴费表如何与考生表关联,以及为什么主键选择自增 id 而非身份证号。
我习惯把这种关联关系用一个表格说明,课设报告里也建议画同样的表:
| 表名 | 核心字段 | 作用 | 关联方式 |
|---|---|---|---|
| t_student | id、username、password、id_card、exam_no、phone | 考生身份与报名信息 | 主表 |
| t_admin | id、admin_name、password | 管理员登录凭证 | 独立 |
| t_score | id、exam_no、subject、score、record_time | 考生各科成绩 | exam_no 关联 t_student |
| t_question | id、student_id、question、answer、ask_time、reply_time | 在线问答双方内容 | student_id 关联 t_student |
| t_payment | id、exam_no、amount、status、pay_time | 缴费状态记录 | exam_no 关联 t_student |
这里有两个设计决策值得在答辩时主动讲。第一个是成绩表和缴费表都用 exam_no 准考证号而不是 student_id 主键做关联,原因是准考证号在业务上对考生和管理员都是可见的,成绩查询接口只需要准考证号和身份证号两个条件就能完成,不需要回查考生表主键,少一次 join 查询。这是典型的“用空间换时间”的冗余设计。第二个是问答表和缴费表都带时间字段,ask_time 和 pay_time 在生成报表时会成为排序依据,这也是摘要里“产生成绩报表”“有助于管理与查收”这两个功能点的数据基础。
3.2 建表脚本落地:编码、默认值与查询索引一次配齐
下面是这五张表的建表脚本。字符集统一用 utf8mb4,因为 MySQL 的 utf8 实际是 utf8mb3,存不了生僻字和 emoji。身份证号、准考证号、科目名这三个字段是高频查询条件,建议建索引,虽然课设数据量小感觉不到差别,但答辩时老师问“查询慢怎么办”你能接上话。
CREATE DATABASE IF NOT EXISTS online_signup DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE online_signup; -- 考生表:注册、登录、个人信息维护 CREATE TABLE t_student ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键自增', username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录用户名,唯一', password VARCHAR(64) NOT NULL COMMENT '登录密码,课设可用明文,生产必须哈希', student_name VARCHAR(50) NOT NULL COMMENT '考生姓名', id_card CHAR(18) NOT NULL UNIQUE COMMENT '身份证号,18位定长', exam_no VARCHAR(20) NOT NULL UNIQUE COMMENT '准考证号,业务查询键', phone VARCHAR(20) DEFAULT NULL COMMENT '联系电话,允许为空', email VARCHAR(100) DEFAULT NULL COMMENT '邮箱,非必填', address VARCHAR(200) DEFAULT NULL COMMENT '通讯地址', register_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间,默认当前时间' ) ENGINE=InnoDB COMMENT='考生信息表'; -- 管理员表:后台账号独立存储 CREATE TABLE t_admin ( id INT PRIMARY KEY AUTO_INCREMENT, admin_name VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL ) ENGINE=InnoDB COMMENT='管理员表'; -- 成绩表:一个考生多科成绩,按准考证号关联 CREATE TABLE t_score ( id INT PRIMARY KEY AUTO_INCREMENT, exam_no VARCHAR(20) NOT NULL COMMENT '准考证号,关联 t_student', subject VARCHAR(50) NOT NULL COMMENT '考试科目', score DECIMAL(5,2) NOT NULL COMMENT '成绩,保留两位小数', record_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '录入时间', UNIQUE KEY uk_exam_subject (exam_no, subject) COMMENT '同一考生同一科目只能有一条成绩', KEY idx_exam_no (exam_no) ) ENGINE=InnoDB COMMENT='考生成绩表'; -- 问答表:考生提问、管理员解答 CREATE TABLE t_question ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL COMMENT '提问考生主键', question VARCHAR(500) NOT NULL COMMENT '问题内容', answer VARCHAR(500) DEFAULT NULL COMMENT '管理员回答,null表示未回复', ask_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '提问时间', reply_time DATETIME DEFAULT NULL COMMENT '回复时间' ) ENGINE=InnoDB COMMENT='在线问答表'; -- 缴费表:模拟网银缴费记录 CREATE TABLE t_payment ( id INT PRIMARY KEY AUTO_INCREMENT, exam_no VARCHAR(20) NOT NULL COMMENT '准考证号', amount DECIMAL(10,2) NOT NULL COMMENT '缴费金额', status TINYINT DEFAULT 0 COMMENT '0未缴费 1已缴费', pay_time DATETIME DEFAULT NULL COMMENT '实际缴费时间', KEY idx_exam_no (exam_no) ) ENGINE=InnoDB COMMENT='缴费记录表';脚本里有几个参数值得单独说明。id_card 用 CHAR(18) 而不是 VARCHAR(18),因为身份证号定长 18 位,CHAR 按定长存储,检索时不用计算长度,性能略优。score 用 DECIMAL(5,2) 而不用 FLOAT,因为浮点类型在比较相等时存在精度误差,成绩这种需要精确判定的数据必须用定点数。成绩表加了一个联合唯一键 uk_exam_subject,保证同一考生同一科目只有一条成绩,这是让“修改成绩”功能逻辑成立的关键约束——如果允许重复,修改时就不知道改的是哪一条了。
3.3 JDBC 连接配置:驱动类、URL 参数与数据库版本对应关系
数据库建好后,Java 代码要连上它。这个项目的连接配置通常写在一个 DBUtil 工具类里,用静态代码块加载驱动,用 DriverManager.getConnection 获取连接。这是 JDBC 直连的标准写法,也是课设里最容易出问题的位置——问题不在代码,而在你本机的 MySQL 版本。
package com.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String DRIVER = "com.mysql.cj.jdbc.Driver"; private static final String URL = "jdbc:mysql://localhost:3306/online_signup" + "?useUnicode=true&characterEncoding=utf8" + "&useSSL=false" + "&serverTimezone=Asia/Shanghai"; private static final String USER = "root"; private static final String PASSWORD = "123456"; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, java.sql.Statement stmt, java.sql.ResultSet rs) { // 关闭顺序:rs -> stmt -> conn,反向关闭 if (rs != null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } } if (stmt != null) { try { stmt.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn != null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }这段代码有两个版本差异大的参数项。DRIVER 这行,如果你连的是 MySQL 5.x,可以用 com.mysql.jdbc.Driver;如果是 MySQL 8.x,这个类已经被移除,必须用 com.mysql.cj.jdbc.Driver,否则会报 ClassNotFoundException。连接串里的 serverTimezone=Asia/Shanghai 是 MySQL 8.x 强制要求的时区参数,不加会报 CST 时区识别不了的异常,5.x 不强制但建议加上,省得数据库和 JVM 默认时区不一致导致时间字段差 8 小时。useSSL=false 是关掉 SSL 握手,本地开发环境没必要开,能省一次加密握手的时间。
一个容易被忽略的细节:PASSWORD 要和本地 MySQL 实际一致,别直接抄 123456。我之前帮人排查过一次“明明盯着代码看不出错”的启动失败,最后发现是数据库密码改了,代码里还留着旧值。这个类所有模块复用,改密码只改这一处,改完务必重新编译整个项目,别让 IDE 用旧 class 跑。
4. 把三条核心链路跑通:登录拦截、报名维护与成绩录入的代码走读
数据库就位之后,接下来是代码走读。这个项目最值得读的不是某个复杂算法,而是三条业务链路的组织方式:登录校验怎么拦住未授权访问、考生报名和成绩查询怎么操作数据库、成绩录入和组合查询怎么处理动态条件。读懂了这三条链路,这个系统你就掌握了八成。
4.1 登录校验怎么拦:Filter 拦截器搭配 Session 的最小实现
先看登录链路的拦截逻辑。这个系统的受保护资源包括考生个人信息页、成绩查询页、缴费页,以及全部管理员页面。最小可行的做法是写一个 Filter,拦截所有 .jsp 和 Servlet 路径,检查 Session 里有没有登录标记,没有就跳回 login.jsp。
package com.filter; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; public class LoginFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; // 登录页和注册页不需要校验,直接放行 String uri = req.getRequestURI(); if (uri.endsWith("login.jsp") || uri.endsWith("register.jsp") || uri.endsWith("LoginServlet")) { chain.doFilter(req, resp); return; } // 检查 Session 中是否存在登录标记 HttpSession session = req.getSession(false); Object loginUser = (session != null) ? session.getAttribute("loginUser") : null; if (loginUser == null) { // 未登录,重定向到登录页,并携带来源路径便于登录后跳回 resp.sendRedirect(req.getContextPath() + "/login.jsp"); return; } chain.doFilter(req, resp); } @Override public void init(FilterConfig filterConfig) throws ServletException { // 过滤器初始化时可读取 web.xml 中的 init-param } @Override public void destroy() { // 释放资源 } }这段过滤器代码的逻辑是:先放行登录页、注册页和登录 Servlet 本身;然后从请求里拿 Session,注意 getSession(false) 的参数——false 表示如果当前没有 Session 就返回 null 而不是新建一个。这一点是课设里经常写错的地方,很多人用 getSession(),导致未登录用户每次访问都会被创建一个空 Session,白占内存不说,还会干扰判断。最后,如果 Session 里没有 loginUser 属性就重定向到登录页,有就放行继续走后续链路。
对应的 web.xml 里要注册这个过滤器,映射范围写成 /*,同时给登录 Servlet 设置 url-pattern。有个小细节:重定向时要加 req.getContextPath() 前缀。ContextPath 是应用部署的根路径,比如你在 Tomcat 里把项目部署成 /OnlineSignup,那重定向地址就得是 /OnlineSignup/login.jsp。漏掉这个前缀,页面会跳到 localhost:8080/login.jsp 而不是应用下的登录页,直接 404。
4.2 考生报名与信息修改:后端校验兜底,前端校验只做体验
考生报名链路的核心是注册 Servlet 和考生信息 Servlet。注册页收集用户名、密码、身份证号、准考证号等字段,提交给 Servlet,Servlet 先查重再插入。下面这段是注册逻辑的关键片段。
package com.servlet; import com.dao.StudentDao; import com.entity.Student; public class RegisterServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("utf-8"); String username = request.getParameter("username"); String password = request.getParameter("password"); String idCard = request.getParameter("idCard"); String examNo = request.getParameter("examNo"); // 后端必须重新校验,不信任前端传过来的任何数据 if (username == null || username.trim().isEmpty() || password == null || password.trim().isEmpty() || idCard == null || idCard.length() != 18 || examNo == null || examNo.trim().isEmpty()) { request.setAttribute("msg", "必填项缺失或格式不正确"); request.getRequestDispatcher("register.jsp").forward(request, response); return; } StudentDao dao = new StudentDao(); // 查重:用户名和身份证号都要求唯一 if (dao.isUsernameExists(username)) { request.setAttribute("msg", "用户名已被占用"); request.getRequestDispatcher("register.jsp").forward(request, response); return; } Student stu = new Student(); stu.setUsername(username); stu.setPassword(password); // 课设阶段明文入库,生产环境必须 BCrypt 或 MD5+盐 stu.setIdCard(idCard); stu.setExamNo(examNo); boolean ok = dao.insertStudent(stu); if (ok) { response.sendRedirect("login.jsp"); } else { request.setAttribute("msg", "注册失败,请检查资料后重试"); request.getRequestDispatcher("register.jsp").forward(request, response); } } }注意这段代码的校验顺序。先做空值和格式校验,再做业务唯一性校验,最后才写库。顺序很重要——如果把查重放在格式校验之前,用户随便传一个空字符串,SQL 查出来的结果没有意义,还会多一次无谓的数据库交互。idCard.length() != 18 这种硬校验可以挡掉绝大多数手误,但注意身份证号最后一位可能是 X,所以这个字段在数据库里设计成 CHAR(18) 而非 CHAR(17)+校验位就是不为难使用者。
还有一点:页面上用 JavaScript 做的校验只是体验优化,后端 Servlet 必须重新校验。原因很简单——前端校验可以被绕过,任何人用 Postman 直接向 RegisterServlet 发请求就能跳过页面限制。课设答辩时老师十有八九会问“前端已经校验了为什么后端还要写一遍”,这个答案要提前准备好。
4.3 成绩录入与组合查询:多条件动态 SQL 的预编译写法
成绩模块是管理员端最重的功能,包含录入、组合查询、修改、报表四个操作。组合查询是这里的技术难点——查询条件可能是准考证号、身份证号、姓名、科目中的任意几个,SQL 必须根据前端传来的条件动态拼接。下面这段代码处理了动态拼接,同时用预编译防止 SQL 注入。
package com.dao; public class ScoreDao { /** * 组合条件查询成绩 * 条件全部可选,只对非空条件追加 SQL */ public List<Score> queryByCondition(String examNo, String idCard, String subject) { // 基础 SQL 先固定,WHERE 子句动态追加 StringBuilder sql = new StringBuilder( "SELECT s.exam_no, s.subject, s.score, st.student_name, st.id_card " + "FROM t_score s JOIN t_student st ON s.exam_no = st.exam_no " + "WHERE 1=1 "); List<Object> params = new ArrayList<>(); if (examNo != null && !examNo.trim().isEmpty()) { sql.append("AND s.exam_no = ? "); params.add(examNo.trim()); } if (idCard != null && !idCard.trim().isEmpty()) { sql.append("AND st.id_card = ? "); params.add(idCard.trim()); } if (subject != null && !subject.trim().isEmpty()) { sql.append("AND s.subject = ? "); params.add(subject.trim()); } sql.append("ORDER BY s.exam_no, s.subject"); // 执行时用 PreparedStatement,? 占位符防止注入 Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql.toString()); for (int i = 0; i < params.size(); i++) { ps.setObject(i + 1, params.get(i)); } ResultSet rs = ps.executeQuery(); // 封装结果集的代码省略,常规 while(rs.next()) 映射到对象 } }这里的 WHERE 1=1 在不在代码里,老开发看法不一。它的实际作用不是判断条件,而是让后续的 AND 拼接不需要判断“是不是第一个条件”——每个条件只管 append "AND 字段 = ?",不用写额外的标记变量来区分首条件。代价是 WHERE 1=1 在 MySQL 里几乎无性能损耗,但会在日志里显得业余。更优雅的做法是维护一个 List conditions 收集条件片段,最后统一用 " AND " 拼接。课设里两种都可以,关键是 params 列表和 SQL 里的 ? 必须一一对应——顺序错了,成绩就查错人了。
另一个血泪教训是:千万别用字符串拼 SQL 的方式动态拼接,比如"AND s.exam_no = '" + examNo + "'"。如果 examNo 被传成' OR '1'='1,整个查询条件被绕过,返回全部考生的成绩。这就是教科书级别的 SQL 注入。用 PreparedStatement 占位符写,注入这条路就堵死了。答辩时主动提一句“预编译能防注入”,这分是稳拿的。
4.4 网上缴费模块的本地模拟:没有支付接口时怎么把流程闭环
摘要里写的是“考生通过网上银行进行交费”,但课程设计不可能真的对接网银接口。常见做法是把缴费流程模拟成三步:考生在缴费页看到待缴订单,点击确认后进入一个模拟支付确认页,点击“确认支付”按钮后后端更新缴费状态。真正对接网银需要商户号、回调地址、签名验签,这些在课设环境里都不具备,报告里如实写“模拟缴费流程,真实支付需对接第三方接口”就行。
package com.servlet; public class PayServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { HttpSession session = request.getSession(); Student stu = (Student) session.getAttribute("loginUser"); String examNo = stu.getExamNo(); // 实际项目从表单取金额,这里按固定报名费模拟 double amount = 120.00; // 更新缴费状态,记录缴费时间 PaymentDao dao = new PaymentDao(); boolean ok = dao.updatePayStatus(examNo, amount); if (ok) { // 跳转到缴费成功页,展示缴费凭证 request.setAttribute("payAmount", amount); request.setAttribute("payTime", new java.util.Date()); request.getRequestDispatcher("pay_success.jsp").forward(request, response); } else { request.setAttribute("msg", "缴费失败,请稍后重试或联系管理员"); request.getRequestDispatcher("pay.jsp").forward(request, response); } } }模拟缴费的关键在 PaymentDao.updatePayStatus 方法里的 SQL,大概是UPDATE t_payment SET status=1, pay_time=NOW() WHERE exam_no=? AND status=0。注意后面这个 AND status=0 条件——它防止重复提交导致同一笔订单被支付两次,这在并发场景下是个简单的乐观锁思路。本地课设几乎碰不到并发,但写上去会显得你考虑过边界情况。缴费完成后,考生的缴费状态变化会直接反映在管理端的 t_payment 表里,管理员查收时按 status 字段筛一遍就能看到谁没交、谁交了、什么时候交的。
5. 部署避坑指南:从 Tomcat 404 到中文乱码,五个高频故障一次排完
这一章写的是我拆这类老课设项目时反复踩过的坑。每一类都按现象、原因、解决的顺序写,你照着排查即可。这些坑里有一半是环境问题,一半是代码本身的历史遗留问题,但它们有一个共同点:报错信息都不直接,靠看日志猜,很玄学。
5.1 项目在 Tomcat 10 上启动失败:javax.servlet 与 jakarta.servlet 的差别
现象是 Tomcat 点启动后,控制台不报错,但浏览器访问项目路径直接 404,或者在启动日志里看到大量java.lang.NoClassDefFoundError: javax/servlet/http/HttpServlet。
原因很明确:Apache 在 Tomcat 10.0 之后把 JavaEE 的 javax.servlet 命名空间迁移成了 Jakarta EE 的 jakarta.servlet。这份课设源码是基于旧规范的,所有import javax.servlet.*的类在 Tomcat 10 里根本找不到。IDE 不报错的原因是编译时用的 Tomcat 9 运行时库,启动时换成了 Tomcat 10,编译和运行环境不一致。
解决的方法是装一个 Tomcat 8.5 或 9.0(推荐 9.0,兼容性和稳定性都更好),然后重新关联 IDE 的 Server Runtime Environment。再不行就全局替换源码里的import javax.servlet为import jakarta.servlet,但那些 jar 包里的拦截器配置也会受影响,工作量不小。课设场景,换 Tomcat 9 是最省事的后悔药。
5.2 数据库连接报错:驱动类、时区、端口三连问
现象是启动项目后点登录,页面报Communications link failure或Unknown database 'online_signup',有的还会出现The server time zone value 'CST' is unrecognized。
原因有三个高频来源。第一,驱动类写错——MySQL 8 里 com.mysql.jdbc.Driver 已被移除,要用 com.mysql.cj.jdbc.Driver。第二,连接串缺 serverTimezone 参数。第三,数据库端口不是默认的 3306,或者 MySQL 服务压根没启动。
解决时按顺序排查:先services.msc确认 MySQL 服务在运行;再用命令行mysql -uroot -p验证密码;接着确认端口netstat -ano | findstr 3306;最后才改 DBUtil 里的连接串。别一上来就改代码,很多时候是服务没起,浪费半小时盯着一行代码看都看不出毛病。
5.3 中文乱码:JSP、Servlet、数据库三处编码必须同频
现象是注册时输入中文姓名,存进数据库变成???,或者页面上查询出来的中文全是乱码。
原因是一个老生常谈的编码链路问题。JSP 页面本身的 pageEncoding、浏览器提交表单时用的字符集、Servlet 里读请求参数的编码、MySQL 连接串里的 characterEncoding、数据库表默认字符集,这五处只要有一处不是 utf8,汉字就可能出乱码。
解决的措施是五处统一。第一,JSP 页面第一行写<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>,并且<meta charset="UTF-8">。第二,Servlet 里读参数前先执行request.setCharacterEncoding("utf-8"),这一步必须在第一次 getParameter 之前调用。第三,连接串里加characterEncoding=utf8。第四,建库时用DEFAULT CHARACTER SET utf8mb4。第五,如果你用的 MySQL 5.5 以下版本,utf8mb4 可能不支持,但那个时代的东西现在基本碰不到了,统一 UTF-8 即可。
5.4 导入项目后包结构错乱:Build Path 与部署路径修正
现象是源码里能看到 .java 文件,但 IDE 里没有把它们识别成源码,一堆类名旁边显示红叉,或者运行提示找不到主类。
原因是用 Eclipse 的 Open Projects from File System 导入老工程时,.classpath 和 .project 文件可能没被正确加载,导致源码目录、输出目录、依赖库的配置全部丢失。这个项目没有 Maven,所有依赖只能手动配。
解决的步骤是:先确认 src 目录被标记为源码根目录,WebRoot 被标记为 Web 资源目录;再把 WEB-INF/lib 下的 mysql-connector-java.jar 加进 Build Path;最后把 JDK 和 Tomcat 运行时也加到 Build Path,全量重新编译。IDEA 里对应操作是右键目录 → Mark Directory as Sources Root,以及 Project Structure → Libraries 里添加 jar。
5.5 端口被占用:改 Tomcat 端口后别忘了同步项目访问路径
现象是 Tomcat 启动时报Port 8080 required by Tomcat v9.0 Server at localhost is already in use,或者另一个项目占着 8080,你改了 Tomcat 的 HTTP 端口后,页面却跳转到一个完全不相关的服务。
解决时在 Tomcat 安装目录 conf/server.xml 里改三处端口:8080 是 HTTP 主端口,8009 是 AJP 端口,8005 是关闭指令端口。这三个都要改成未被占用的值,比如 8081、8010、8006。但注意,改完端口后,访问地址变成http://localhost:8081/项目名/,凡是代码里硬编码了localhost:8080的跳转链接都要同步改,否则会跳到别人的服务。我看过有人只改 server.xml 没改代码里的跳转路径,结果缴费成功后跳到了一个完全不相干的本地服务,折腾了一个晚上才定位到是端口写死了。
6. 验收与加分改造:从“能跑”到“耐看”的三个低成本动作
系统能跑通、功能都正常,这只是课设的及格线。拿高分还得看两个东西:能不能按文档快速复现、有没有体现出超出课设平均水平的工程意识。这一章讲三个低成本的改进动作,都不用大改代码,但对报告和答辩加分的性价比很高。
6.1 五分钟验收清单
拿到最终部署好的系统,建议按下面这个清单过一遍,确认无遗漏再写报告。这张表同时可以直接作为课程设计报告里“系统测试”章节的素材。
| 测试项 | 操作步骤 | 预期结果 |
|---|---|---|
| 注册与查重 | 用同一用户名注册两次 | 第二次提示用户名已占用 |
| 登录拦截 | 未登录直接访问个人中心 URL | 跳转到登录页 |
| 成绩查询 | 输入正确的准考证号+身份证号 | 显示该考生全部科目成绩 |
| 成绩录入与修改 | 管理员录入一条成绩后再次修改 | 同科目成绩被更新而非新增记录 |
| 组合查询 | 只输科目、只输姓名、两者都输 | 返回结果集随条件变化 |
| 缴费闭环 | 考生完成模拟支付,管理员查看缴费表 | 状态由 0 变 1,pay_time 有值 |
| 问答回复 | 考生提问,管理员后台回复 | 考生端能看到回答内容和时间 |
6.2 连接池替换与密码哈希两个加分项
加分项一是把 DBUtil 从 DriverManager 直连改成 Druid 连接池。改造量不大,核心就三步:把 druid jar 包放进 WEB-INF/lib,新建 druid.properties 配置,DBUtil 里的 getConnection 改为从池子里取。连接池的好处是反复创建连接的性能开销省掉了,而且连接数可控——这个点几乎所有答辩老师都认。
# druid.properties driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/online_signup?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username=root password=123456 initialSize=5 maxActive=20加分项二是密码哈希存储。现在的代码里密码是明文入库的,报告里写清楚“后续改进方向是使用加盐哈希”。如果时间允许,直接改成 MD5 加盐也就加一个工具类的事,注册时存摘要、登录时比对摘要。别直接存,明文密码是课设答辩时最容易暴露工程意识短板的地方。
// MD5加盐示例:盐值可以用用户名,保证同密码不同用户摘要不同 public static String md5WithSalt(String password, String salt) { String toHash = password + "{" + salt + "}"; try { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] digest = md.digest(toHash.getBytes("utf-8")); StringBuilder sb = new StringBuilder(); for (byte b : digest) { sb.append(String.format("%02x", b)); } return sb.toString(); } catch (Exception e) { throw new RuntimeException("密码加密失败", e); } }从那以后,我每次拿到这类课设源码包,都强制自己先走一遍“建库 → 改连接串 → 指定 Tomcat 9 → 全量编译 → 跑验收清单”的流程,五分钟内确认项目能不能跑,省下后面几天排雷的时间。这套系统最值钱的地方不是代码本身,而是让你能在一次完整的业务闭环里看清 JavaWeb 的底层运转方式,看清楚之后再做任何框架都会顺手很多。希望帮到你。
本文还有配套的精品资源,点击获取