简介一份面向Java Web学习者的完整车票预订信息系统设计与实现项目适合毕业设计、课程设计或Servlet/JSP进阶实战。项目采用Java、Servlet、JSP和MySQL技术栈覆盖页面展示、业务逻辑、数据库访问等Web分层可引导读者理解一个预订业务从请求到响应的处理过程。压缩包共1337个文件大小55.82MB主要包含JSP页面、Java类、JS/CSS前端脚本、JAR依赖库和SQL数据库脚本另含一个MP4演示讲解方便对照运行。包内大量图片、字体等静态资源用于界面展示目录结构完整清晰。已有1105人学习下载通过该项目可掌握JSPServletMySQL整合开发并可在MyEclipse中直接导入运行借助数据库脚本和演示视频理解关键逻辑适合需要完整课设源码与学习范式的读者。1. 为什么做车票预订还要用 JSP 这套“老技术”如果你正在做毕业设计或课程设计大概率会遇到一个尴尬技术选型时身边同学都去卷前后端分离、Vue Spring Boot 微服务而你打开一份题目叫“基于WEB的车票预订信息系统设计与实现”的课题心里开始犯嘀咕——这东西用 JSP 做是不是太“老”了先说结论不是。车票预订这类信息管理系统本质是“业务逻辑清晰、数据流转明确、页面交互中等”的典型 MIS 系统用 JSP Servlet JavaBean MySQL 这套经典 Java Web 技术栈来做恰恰是毕设和课设评审老师最熟悉的套路。它不需要分布式事务不需要消息队列不需要 Redis 缓存热点数据更不需要微服务治理——这些当然很酷但用在票务系统上属于杀鸡用牛刀答辩时反而容易被追问“为什么用这个中间件、它的故障转移怎么做”。而 JSP 方案的优势非常直接目录结构一目了然、每一页都能对应到实体文件、运行效果直观评审老师打开浏览器点几个页面整套系统的骨架就清楚了。这套资源的核心价值在于它不是给你一堆零散的 JSP 页面而是一条完整的设计主线——从需求分析、数据库建模、模块划分到关键代码的落地实现、部署运行再到最后怎么准备验收和答辩。适合两类人一类是正在做毕设/课设、需要一份能讲清楚“为什么这样设计”的完整方案的在校生另一类是想快速搭一套 Java Web 课设骨架、后续改成其他管理系统的从业者。接下来我会按实际做项目的顺序把这套系统的设计思路和实现细节拆开讲。2. 总体架构设计MVC 分层与四个核心模块的职责划分2.1 为什么选 JSP Servlet JavaBean而不是纯 JSP 或纯 Servlet很多第一次做 Java Web 的人容易陷入两个极端一个是把所有 Java 代码直接写在 JSP 的% %里页面几十个脚本片段混在一起改一个查询条件要找半天另一个是全部用 Servlet 输出 HTML 字符串out.println(table)写到手酸。这两种做法在答辩时都容易被一眼看穿——“这没分层啊维护性在哪”通常的做法是标准的 MVC 分层把整个系统拆成三层视图层ViewJSP 页面只负责展示数据、收集表单输入不写业务逻辑。控制层ControllerServlet接收请求、调用业务逻辑、跳转页面。模型层ModelJavaBean实体类 DAO数据访问层封装数据和对数据库的增删改查。这套结构对应到资源里的目录大致是src/ ├── com.xxx.bean 实体类User, Train, Order, Ticket ├── com.xxx.dao DAO 接口和实现UserDao, TrainDao, OrderDao ├── com.xxx.servlet 控制层LoginServlet, QueryTrainServlet, OrderServlet... ├── com.xxx.util 工具类DBUtil连接管理, StringUtil参数校验 web/ ├── jsp/ 视图层login.jsp, register.jsp, index.jsp, order.jsp... ├── css/ js/ images/ 静态资源 └── WEB-INF/web.xml Servlet 映射配置这个划分不是拍脑袋想出来的。以登录功能为例如果登录逻辑写在 JSP 里那么以后要增加“验证码校验”或者“账号锁定”功能你得去 JSP 里翻脚本片段而按上面这层分只需要改 Servlet 调用新的 Service 方法JSP 页面一行不动。这就是分层最直接的好处——把变的和不变的隔离开。2.2 数据库设计四张核心表与字段设计思路车票预订系统的数据模型不复杂但设计得好不好直接影响后面所有功能的实现难度。通常的做法是围绕“用户—车次—订单”这三个实体建表我在做课设时一般会这样设计用户表user字段名类型说明idint 自增主键用户IDusernamevarchar(20) 唯一登录名passwordvarchar(32)MD5 加密后的密码real_namevarchar(20)乘客姓名取票用id_cardvarchar(18)身份证号实名制预留phonevarchar(11)联系电话车次表train字段名类型说明idint 自增主键车次IDtrain_novarchar(10) 唯一车次号如 G101start_stationvarchar(30)始发站end_stationvarchar(30)终点站start_timedatetime发车时间end_timedatetime到达时间ticket_pricedecimal(10,2)票价total_seatsint总座位数remain_seatsint余票数订单表orders字段名类型说明idint 自增主键订单IDorder_novarchar(32) 唯一订单号程序生成user_idint 外键下单用户train_idint 外键预订的车次order_timedatetime下单时间statusvarchar(10)订单状态已支付/待支付/已取消passenger_namevarchar(20)乘车人passenger_id_cardvarchar(18)乘车人身份证号这里有一个值得注意的设计细节为什么订单表里要冗余一个passenger_name和passenger_id_card而不是直接关联用户表答案是——车票订单本质上是一个“快照”。用户下单时的乘车人信息和用户注册时的个人信息在业务上并不一定一致比如给家人代购票而且如果用户后续修改了个人资料订单里存的信息不应该跟着变。这个冗余字段的设计在答辩时主动讲出来评审老师会认为你考虑到了业务语义而不是只会照抄表结构。另外remain_seats这个字段是典型的“空间换时间”做法。如果不设计它每次查询余票都要total_seats - 已售张数现场算而预订时还要加锁校验SQL 写起来复杂且容易出并发问题。直接在车次表上维护一个剩余票数的字段每次预订成功后UPDATE train SET remain_seats remain_seats - 1 WHERE id ? AND remain_seats 0一条 UPDATE 语句就同时完成了“扣减”和“校验余票充足”两件事。3. 关键模块实现从注册登录到车次预订的完整流程3.1 用户模块注册校验、MD5 加密与会话管理用户模块是所有系统的大门做得不严谨后面的功能全部白搭。这里说三个最容易在课设里被忽略、但实际写代码时一定会遇到的细节。第一个是密码存储。很多初稿里密码直接明文存数据库查数据时能看到所有人的密码——这在平时自己玩没问题但一旦答辩老师瞟到你的 SQL 文件里INSERT INTO user VALUES(admin,123456)基本就会被问“密码安全性怎么考虑”。通常的做法是在工具类里封装一个 MD5 加密方法public class MD5Util { public static String md5(String input) { try { MessageDigest md MessageDigest.getInstance(MD5); byte[] digest md.digest(input.getBytes(UTF-8)); StringBuilder sb new StringBuilder(); for (byte b : digest) { String hex Integer.toHexString(b 0xff); if (hex.length() 1) sb.append(0); sb.append(hex); } return sb.toString(); } catch (Exception e) { throw new RuntimeException(MD5加密失败, e); } } }注意这段代码里的一个细节b 0xff。因为 Java 的 byte 是有符号类型直接转 int 会出现负数不先与 0xff 做位与运算生成的十六进制字符串会不对。这是一个很隐蔽的坑很多人的 MD5 工具类跑出来是 7 位、8 位长度不齐的字符串原因就在这。第二个是注册时的参数校验。这里说的校验不是前端 JS 校验那种随便就能绕过而是 Servlet 里的后端校验。通常会写一个StringUtil工具类核心逻辑就是“任何参数都可能为 null任何字符串都可能被前后空格污染”public class StringUtil { public static boolean isEmpty(String str) { return str null || str.trim().isEmpty(); } public static boolean isValidPhone(String phone) { return phone ! null phone.matches(^1[3-9]\\d{9}$); } }一个值得留意的点str.trim().isEmpty()和str 或str.equals()完全不同。trim()能把用户误输的空格去掉避免用户输入“ admin ”导致登录时发现用户名对不上。这是实际运行中非常容易出现的问题。第三个是登录后的会话管理。登录成功后把用户信息放进 Session后续页面通过 Session 判断是否已登录这是最常规的做法。但有一个细节容易被漏掉用户登录成功后跳转到哪个页面以及 Session 中存的是整个 User 对象还是只存 userId。我一般会在LoginServlet里这样处理protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String username request.getParameter(username); String password MD5Util.md5(request.getParameter(password)); UserDao userDao new UserDao(); User user userDao.findByUsernameAndPassword(username, password); if (user ! null) { HttpSession session request.getSession(); session.setAttribute(loginUser, user); response.sendRedirect(request.getContextPath() /jsp/index.jsp); } else { request.setAttribute(errorMsg, 用户名或密码错误请重试); request.getRequestDispatcher(/jsp/login.jsp).forward(request, response); } }这里的sendRedirect和forward是有本质区别的。sendRedirect重定向是浏览器重新发起一次请求地址栏会变forward转发是服务器内部跳转地址栏不变。登录成功用重定向是为了避免用户按 F5 刷新时反复提交表单导致重复登录登录失败用转发是为了带errorMsg回到登录页并保留输入框里的用户名。这种细节在答辩时随口说出“为什么这里用 redirect 不用 forward”很加分。3.2 车次查询模块多条件组合查询与分页处理车次查询是票务系统的核心入口。用户输入出发地、目的地、出发日期系统返回符合条件的车次列表。听起来简单但实现上有两个问题必须要解决一是如果某个查询条件为空SQL 要怎么拼才不会报错二是查询结果可能很多所有数据一次性显示在一页上既不美观也不合理必须分页。动态拼接 SQL 在这里是绕不开的。用字符串拼 SQL 最怕的是条件不确定比如用户只输入了出发地没输入目的地或者三个条件全空应该查全部车次。通常的做法是先用一个ArrayListString收集条件再统一拼接public ListTrain findTrains(String start, String end, String date) { StringBuilder sql new StringBuilder(SELECT * FROM train WHERE 11); ListObject params new ArrayList(); if (StringUtil.isNotEmpty(start)) { sql.append( AND start_station LIKE ?); params.add(% start %); } if (StringUtil.isNotEmpty(end)) { sql.append( AND end_station LIKE ?); params.add(% end %); } if (StringUtil.isNotEmpty(date)) { sql.append( AND DATE(start_time) ?); params.add(date); } sql.append( ORDER BY start_time LIMIT ?, ?); // 执行查询将 params 中的参数依次设置到 PreparedStatement // 分页参数 limitOffset 和 limitSize 由 Service 层计算传入 }这里写WHERE 11不是闲得没事它是一个非常实用的小技巧后续每个条件都用AND开头拼接就不需要去判断“当前是不是第一个条件、要不要加 WHERE”。虽然这种写法在很多“代码洁癖”眼里不太优雅但课设项目中胜在直观、不易出错。分页参数通常由 Servlet 层计算接收页面传过来的pageNo当前页码配合固定的pageSize每页条数得到offset (pageNo-1) * pageSize然后传给 DAO 层。前端 JSP 页面底部分页导航条要显示“上一页、下一页、当前第 X / Y 页”Y总页数需要额外查询一次总记录数SELECT COUNT(*) FROM train WHERE ...。注意这里查询总数的条件必须和查询列表的条件完全一致否则分页页码会不对——我自己翻车过列表查询加了start_station LIKE总数查询忘了加结果第二页显示不出数据。3.3 预订模块事务处理与余票扣减的原子操作预订是整套系统里唯一直接涉及“钱”和“库存”的操作也是答辩时最容易暴露问题的地方。如果余票扣减和订单插入这两步操作之间程序突然异常退出会出现什么情况要么扣了票但没生成订单要么生成了订单但没扣票。解决这个问题靠的是数据库事务。通常在OrderDao里把“插入订单 扣减余票”封装成一个非静态方法并且在同一个数据库连接里执行public boolean createOrder(Order order) { Connection conn null; PreparedStatement psOrder null; PreparedStatement psUpdate null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交开启事务 String sqlOrder INSERT INTO orders(order_no, user_id, train_id, order_time, status, passenger_name, passenger_id_card) VALUES(?,?,?,?,?,?,?); psOrder conn.prepareStatement(sqlOrder); // 设置参数... psOrder.executeUpdate(); String sqlUpdate UPDATE train SET remain_seats remain_seats - 1 WHERE id ? AND remain_seats 0; psUpdate conn.prepareStatement(sqlUpdate); psUpdate.setInt(1, order.getTrainId()); int rows psUpdate.executeUpdate(); if (rows 0) { // 说明余票不足回滚 conn.rollback(); return false; } conn.commit(); // 全部成功提交 return true; } catch (Exception e) { try { if (conn ! null) conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } e.printStackTrace(); return false; } finally { // 依次关闭 psUpdate, psOrder, conn注意恢复 autoCommit 为 true } }这段代码里最关键的是conn.setAutoCommit(false)和WHERE id ? AND remain_seats 0这个条件。前者是整个事务的开关后者是防超卖的最后一道保险——即使两个请求同时进来数据库层面的行锁也会保证只有一个 UPDATE 能匹配到remain_seats 0的行并成功执行另一个匹配 0 行返回rows 0触发回滚。顺带说一个问题这段代码的运行结果最终依靠的还是“数据库行锁”MySQL 默认引擎 InnoDB 执行 UPDATE 时会对匹配的行加写锁所以这个逻辑在并发量不大的课设场景下是安全的。如果高并发就要引入悲观锁或者更重的机制但就车票预订课设而言这个方案已经足够答辩时如果能主动说出“这里是靠数据库行锁保证原子性的”评审基本不会再深究。4. 技术难点实战JDBC 连接管理、订单状态机与数据可视化4.1 DBUtil 连接工具类为什么不能每次 new Connection很多初稿里每个 DAO 方法的开头都是DriverManager.getConnection(url, user, password)用完就conn.close()。在课设这种并发量几乎为零的场景里这当然不会出问题但一旦页面稍微多点几次数据库就会报 “Too many connections” 错误。原因在于每 getConnection 一次数据库就要创建一个物理连接这个操作是重操作开销极大。通常的改进方案有两种一是用连接池C3P0、Druid二是写一个简单的 DBUtil 工具类统一管理连接获取和释放。课设阶段用连接池配置略显繁重但用一个工具类把驱动加载、连接获取、资源关闭收拢到一起已经是质变public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/ticket_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { if (rs ! null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } } if (ps ! null) { try { ps.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn ! null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }注意 URL 里的characterEncodingutf8、useSSLfalse、serverTimezoneAsia/Shanghai这三个参数缺一个都可能踩坑编码不对会导致 JSP 页面显示中文乱码SSL 握手在某些 MySQL 版本下会警告时区不设置会报 ”The server time zone value” 的异常。这些属于典型的“环境配置玄学”不是代码逻辑问题但报错起来毫无头绪。一个重要的习惯是conn.close()之后实际上只是把连接还回连接池如果用池的话数据库物理连接还在。所以 DAO 层的 finally 块里释放资源是必须的每漏一次就是一个连接泄漏。血泪经验曾经跑了个通宵作业第二天早上系统直接假死一看show processlist;密密麻麻全是 Sleep 连接——都是前一天测试时忘记关的。4.2 订单状态机待支付、已支付、已取消的三态流转很多课设的订单模块就是把数据存进表里就完了不做状态管理。但如果你在需求分析里写过“用户可以查询我的订单、可以取消订单”那就必须考虑订单状态否则“取消”的逻辑会变成物理删除——这是很多人会踩的坑直接DELETE FROM orders WHERE id ?订单没了数据统计也没了。通常的做法是为订单表设计一个status字段定义三到四个状态待支付0刚下单还没付款。已支付1完成支付车票生效。已取消2用户主动取消。已出行3超过发车时间自动视为已使用可选状态。状态流转规则是单向的待支付可以到已支付或已取消已支付可以到已取消退票已取消和已出行是终态不能再回退。在 JSP 页面上状态字段存的是数字0/1/2/3展示时要做映射比如c:choose c:when test${order.status 0}span classstatus-pending待支付/span/c:when c:when test${order.status 1}span classstatus-paid已支付/span/c:when c:when test${order.status 2}span classstatus-canceled已取消/span/c:when /c:choose这里用span加 CSS 类而不是直接输出汉字是为了后续可以给不同状态配不同颜色比如待支付橙色、已支付绿色、已取消灰色页面一看就清楚。取消订单的逻辑不是简单地改 status它涉及一个数据一致性操作把订单状态改为“已取消”的同时必须把车次的remain_seats加回去。所以取消操作同样要放在事务里执行和创建订单完全对称。很多初稿只改了状态没加票数结果用户取消两次余票反而比初始值还多——这是业务逻辑错误答辩时被问出来会非常难看。4.3 数据可视化使用 ECharts 展示售票统计如果需求分析里写了“系统管理员能查看售票情况统计”那么可视化就是一个很加分的模块。通常的做法是后端查统计数据前端用 ECharts 画图表。相比 JSP 页面里加一个table展示统计数字图表的效果完全是两个量级的观感。管理员统计页面的思路是这样的后端写一个StatServlet查询最近 7 天的订单数量、按车次统计销售额把结果转成 JSON 字符串输出前端用 AJAX 获取数据然后交给 ECharts 渲染。后端输出 JSON 的代码很简单ListMapString, Object stats orderDao.countDailyOrders(7); // 转换成 [{date:2024-05-01, count:12}, {date:2024-05-02, count:18}...]JSP 页面里引入 ECharts 的 CDN 链接然后写一小段 JavaScript 从后端接口拉数据$.ajax({ url: ${pageContext.request.contextPath}/admin/statServlet, dataType: json, success: function (data) { var chart echarts.init(document.getElementById(chart)); var option { xAxis: { type: category, data: data.labels }, yAxis: { type: value }, series: [{ type: bar, data: data.values }] }; chart.setOption(option); } });这段代码的关键在于后端返回的数据格式要和 ECharts 的 option 结构对齐。我常用的方式是后端直接返回一个带labels和values两个数组的 JSON 对象前端拿到之后直接塞进 option 里。这样前端不用做任何二次加工逻辑最简。这里的labels是日期字符串数组values是对应的订单数数组顺序由 SQL 的GROUP BY DATE(order_time)保证。这一模块在答辩时属于“亮点功能”。评审老师看到不是冷冰冰的数字表格而是图表第一印象就是“这个系统有完整的管理闭环”。5. 避坑排查JSP 项目里最容易翻车的 6 个问题做 Java Web 课设代码能不能跑通一半取决于逻辑一半取决于对环境和细节的处理。这一章集中写我在这个过程里真实踩过的坑每一条都是“现象 → 原因 → 解决”的结构希望能给你省下几个通宵。5.1 中文乱码从 URL 到数据库的全链路排查现象JSP 页面显示正常但用户注册时输入中文姓名存进数据库后是问号或者从数据库查询出来在页面上显示乱码。原因中文乱码的本质是“编码不一致”。常见的有三种断点浏览器提交表单时用的是页面编码一般是 UTF-8但 Servlet 默认按 ISO-8859-1 解析请求参数数据库连接 URL 没指定characterEncodingutf8MySQL 表本身的字符集不是 utf8。解决三层同时设置。JSP 页面顶部加% page contentTypetext/html;charsetUTF-8 languagejava %Servlet 的doPost第一行加request.setCharacterEncoding(UTF-8)JDBC 连接 URL 加characterEncodingutf8。三层做完绝大多数乱码都能解决。还有一个小坑是 Tomcat 8 以上 GET 请求的 UTF-8 默认是开着的但你要是用了 Tomcat 7GET 请求乱码得改server.xml里的URIEncodingUTF-8。5.2 数据库连接不释放导致系统假死现象系统跑了一个晚上第二天所有页面打开都转圈重启 Tomcat 才恢复。原因程序里有连接泄漏。可能是某个finally块里漏了rs.close()也可能是某个conn.close()被放到了return语句之后导致异常提前返回时连接没关。MySQL 的默认连接超时是 8 小时期间所有打开的连接都被 MySQL 当作 Sleep 连接保留超过max_connections之后新的连接一律被拒。解决检查每一个 DAO 方法的 finally 块确保 ResultSet、PreparedStatement、Connection 都调用了 close。一个省事的技巧是用 IDE 的代码模板生成 try-catch-finally 骨架不要手写完逻辑再回去补关闭代码那样一定会漏。5.3 Tomcat 端口被占用Address already in use现象启动 Tomcat 时报Port 8080 already in use或 Eclipse / IDEA 里启动到一半失败。原因上一个 Tomcat 实例没有正常关闭比如直接杀了终端窗口之前运行的 Java 进程还占着端口。解决Windows 下打开命令行执行netstat -ano | findstr 8080查到占用端口的 PID再打开任务管理器结束对应进程或者直接改 Tomcat 的server.xml把端口改成 8081、8082。但注意如果改端口所有 JSP 页面的访问地址都要跟着变。5.4 Session 失效消失登录后跳转又回到登录页现象登录成功后点“我的订单”跳转回登录页或者登录状态下刷新页面登录态直接丢了。原因最常见的两个——一是web.xml里配置了session-config的超时时间默认 30 分钟你开了多个页面测试可能超时二是 JSP 页面上用了session.getAttribute但 Servlet 里存的时候 key 写错了比如存的是loginUser取的是user。解决Session 的 key 统一放在常量类里不要一个地方写字符串一个地方写另一个字符串。建议用 Eclipse 或 IDEA 的Refactor功能把loginUser替换成Constants.SESSION_LOGIN_USER从根上杜绝拼写不一致的问题。5.5 SQL 注入风险密码框输入 or 11现象用户输入admin or 11作为密码居然能登录成功。这是最经典的 SQL 注入演示方式。原因代码里用了字符串拼接 SQL比如String sql SELECT * FROM user WHERE username username AND password password ;当password是 or 11时SQL 变成SELECT * FROM user WHERE usernameadmin AND password or 11这个 SQL 恒为真所以不用知道密码就登录成功了。解决所有 SQL 一律改用PreparedStatement的占位符?传参不要用字符串拼接。这个不仅是安全要求也是代码规范问题——答辩时如果代码里出现拼接 SQL基本会被判定为安全意识不足。用预编译语句之后参数会被当作字符串字面量处理注入逻辑自然失效。5.6 余票变成负数点击“预订”后余票越点越多现象并发点击“预订”按钮车次余票从 10 变成 -1或者取消订单后余票变成 11。原因既没有在 UPDATE 时加remain_seats 0条件也没有在取消订单时正确恢复余票。前者是超卖问题后者是业务逻辑不对称。解决参照前面事务章节的写法扣余票的 SQL 必须带WHERE id ? AND remain_seats 0用返回的affected rows判断是否实际扣减成功取消订单时把状态更新和余票恢复放在同一个事务里。6. 系统验收与答辩技巧让方案从“能运行”到“能讲清楚”6.1 功能测试清单演示前把项目完整走一遍很多系统做完了代码能编译能启动但一到演示环节就翻车——问题往往不是功能缺失而是路径没走通。我一向坚持验收阶段要打印一张“功能走查清单”按用户视角逐条操作而不是只点菜单栏里的几个入口。以这套车票预订系统为例演示前至少要把下面的路径完整走一遍操作路径预期结果常见翻车点注册新用户 → 登录提示注册成功登录后跳转首页注册时密码二次校验不一致首页搜索“北京→上海”→ 点击“预订”跳转订单确认页余票数正确显示车次查询条件拼错导致查不到提交订单 → 查看“我的订单”状态为“待支付”余额票数减一Session 用户丢失导致查不到订单取消订单 → 查看“我的订单”状态为“已取消”车次余票加一状态更新了但余票没恢复管理员登录 → 查看统计能看到订单数/销售额的图表管理员和用户用同一张表时权限没区分每一条都要做两遍一遍正常操作一遍故意输入错误数据错格式的手机号、不存在的账号、空条件查询验证系统有错误提示而不是直接 500。评审老师最喜欢干的事情就是输入一些“边界数据”来看你的系统有没有防御意识。6.2 答辩高频问题与结构化回答思路答辩时间一般五分钟到十分钟核心问题翻来覆去就那么几个。答案不要求多深但要求“逻辑闭环”你的设计方案里写的每一句话都要准备好被追问。第一个高频问题“为什么选 JSP Servlet 而不是 Spring Boot”建议回答思路这个系统的核心需求是 MISH——逻辑清晰、页面流程简单、数据量小JSP Servlet 的原生方案可以最直观地展示 MVC 分层思想课程设计的目标是掌握 Web 应用的底层请求—响应模型而不是使用框架的便捷性。如果你用的是 Spring Boot也要能说清楚自己为什么选择了更重的框架而不用原生方案。关键不是选哪个而是能自己论证为什么选这个。第二个高频问题“余票数据怎么保证不超卖”回答思路扣减余票的 SQL 语句带了remain_seats 0条件数据库的行锁机制保证只有一个请求能匹配成功另一个请求匹配 0 行并触发事务回滚。这道题是区分“会写代码”和“懂设计”的分水岭认真讲清楚事务和行锁印象分会明显上一个档次。顺带可以补一句如果未来并发量变大可以引入乐观锁version 字段或分布式锁但当前场景下数据库行锁已经足够——这句话表明你有扩展意识。第三个高频问题“你的项目里哪部分最难”这个问题看似开放实际上希望听到一个你仔细思考过的技术点。通常可以选“订单取消时的数据一致性”或者“多条件动态 SQL 拼接”讲清楚难点是什么、怎么解决的、有没有更优方案。最怕的回答是“好像没有特别难的”——它会让人觉得你没有深入理解自己的项目。6.3 一个能拉开差距的细节准备一份“未知错误处理”演示大部分同学验收时只会演示“正常路径”即输入正确、操作顺利、界面正常。但评审老师看多了千篇一律的演示真正想看到的是你有“防御编程”的意识。我建议做两页“错误处理界面”放在手里一是数据库连接不可用时的统一错误页404/500 error page二是前端表单校验失败后的错误提示回显。前者对应web.xml里的error-page配置后者对应login.jsp上回显errorMsg的写法——这两处代码其实非常简单但在整个班里只要你不说谁都不会主动做而一旦你做了在评审眼里就是“这个学生有工程化思维而不只是会写 CRUD”。一个建议不要只在最后准备阶段才翻这些代码。从我开始做 Web 类课设起每次演示前都强制把“注册→登录→查询→预订→取消订单”这条完整链路从头到尾走三遍第一遍正常操作第二遍故意输错数据第三遍快速连续点击页面按钮测试异常情况。这个习惯帮我避免过很多次现场翻车——有一次演示前发现取消订单后列表状态没刷新当时如果直接上台演示到一半必然卡住。从那以后我把这条流程走查固定成了每个项目交付前的必做动作。希望这套方案能帮你把系统从“能运行”推进到“能讲清楚”祝你的答辩顺利。本文还有配套的精品资源点击获取