
简介本资源是一份完整的JavaEE课程设计报告书面向高校计算机及相关专业学生聚焦Web应用开发实践解决从理论到工程落地的能力转化问题。报告以‘网上订餐系统’为载体系统阐述了基于StrutsSpringHibernate框架的全栈开发流程涵盖需求分析、多角色模块设计用户/管理员/菜品/订单/反馈、MySQL数据库建模含菜品、用户、管理员、订单、分类等6张核心表、系统流程图与功能框图、异常处理机制自定义SystemException及全局配置及登录界面等关键实现细节。资源为单文件PDF大小985KB内容结构完整图文结合含字段说明表、配置代码片段与界面示意图便于理解技术选型逻辑与工程规范。目前已有739人学习下载适合作为JavaEE课程设计参考范本、毕业设计前期素材或Web开发入门实战复盘资料。1. 这不是写个“点餐页面”就交差的课程设计JavaEE订餐系统必须跑通事务边界、会话管理、三层解耦否则答辩当场被问住你手里的《订餐系统JavaEE课程设计报告书.pdf》大概率是某高校计算机专业大三下学期的期末硬仗——它表面是“做个网页点点菜”实则是JavaEE技术栈的一次全链路压力测试Servlet要扛住并发请求JSP得干净分离展示逻辑DAO层必须用JDBC或Hibernate管住数据库连接池而最关键的是用户登录态不能跨页丢失、下单扣库存和生成订单必须原子执行、管理员后台删菜品不能误删关联订单。我带过三届课程设计80%的学生卡在“登录后跳转首页又变回未登录”“下单成功但库存没减”“管理员删了菜用户历史订单里显示‘null’”这三类问题上。这不是代码写得丑而是对JavaEE核心契约如HTTP Session生命周期、JDBC事务隔离级别、Servlet容器线程模型理解有断层。这篇笔记不讲PPT怎么排版、报告怎么凑字数只拆解从零搭起一个能过答辩、能现场演示、能解释清楚每一行为什么这么写的JavaEE订餐系统——用Tomcat 9 MySQL 8 原生JDBC不依赖Spring Boot简化干扰所有代码可直接粘贴进Eclipse或VS Code运行每一步都标清JDK版本兼容性、web.xml配置陷阱、以及老师最常揪着问的3个底层原理。2. 用ServletJSP搭骨架为什么不用Spring Boot因为课程设计考的是容器本质课程设计不是项目交付考的是你对JavaEE规范的理解深度。Spring Boot自动装配会掩盖Servlet容器如何加载Filter、Listener、Servlet的顺序而答辩时老师一句“说说web.xml里load-on-startup1的作用”就能筛掉一半人。所以本方案坚持原生JavaEETomcat 9.0.83JDK 11兼容、MySQL 8.0.33、JDBC驱动用mysql-connector-java-8.0.33.jar。所有组件版本锁定避免“本地能跑服务器报错”的玄学翻车。2.1 创建标准Java Web项目结构别让目录名毁掉整个部署Eclipse或VS Code新建Dynamic Web Project时Project name必须不含空格和中文例dining-systemTarget runtime选Apache Tomcat v9.0。关键目录结构必须严格如下dining-system/ ├── src/ # Java源码 │ ├── servlet/ # 所有Servlet类 │ ├── dao/ # 数据访问对象JDBC操作 │ ├── model/ # 实体类User, Dish, Order等 │ └── util/ # 工具类DBUtil, StringUtil ├── WebContent/ # Web根目录Tomcat部署时的ROOT │ ├── WEB-INF/ │ │ ├── web.xml # 核心配置文件必须 │ │ └── lib/ # 放mysql-connector-java-8.0.33.jar │ ├── index.jsp # 首页游客可见 │ ├── login.jsp # 登录页 │ ├── user/ # 用户功能页需登录 │ │ ├── order.jsp # 下单页 │ │ └── history.jsp # 订单历史 │ └── admin/ # 管理员页需权限校验 │ ├── dish-manage.jsp # 菜品管理 │ └── order-list.jsp # 订单列表 └── build.xml # Ant构建脚本可选但推荐提示WebContent是Eclipse默认Web根目录名VS Code中若用Tomcat插件部署需在settings.json中显式指定tomcat.server.webappDir: ./WebContent否则静态资源404。2.2 web.xml配置三个必须声明的组件及其执行顺序WEB-INF/web.xml是JavaEE的灵魂文件漏配任意一项系统必崩。以下是精简但完整的配置Servlet 4.0规范?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0 !-- 1. 全局上下文参数数据库连接信息 -- context-param param-namejdbcUrl/param-name param-valuejdbc:mysql://localhost:3306/dining?useSSLfalseamp;serverTimezoneAsia/Shanghaiamp;allowPublicKeyRetrievaltrue/param-value /context-param context-param param-namejdbcUsername/param-name param-valueroot/param-value /context-param context-param param-namejdbcPassword/param-name param-valueyour_password/param-value /context-param !-- 2. 监听器应用启动时初始化数据库连接池 -- listener listener-classutil.DBListener/listener-class /listener !-- 3. 过滤器统一处理中文乱码和登录校验 -- filter filter-nameEncodingFilter/filter-name filter-classutil.EncodingFilter/filter-class /filter filter-mapping filter-nameEncodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping filter filter-nameLoginCheckFilter/filter-name filter-classutil.LoginCheckFilter/filter-class /filter filter-mapping filter-nameLoginCheckFilter/filter-name url-pattern/user/*/url-pattern url-pattern/admin/*/url-pattern /filter-mapping !-- 4. Servlet映射按业务模块分组 -- servlet servlet-nameLoginServlet/servlet-name servlet-classservlet.LoginServlet/servlet-class /servlet servlet-mapping servlet-nameLoginServlet/servlet-name url-pattern/login/url-pattern /servlet-mapping servlet servlet-nameOrderServlet/servlet-name servlet-classservlet.OrderServlet/servlet-class /servlet servlet-mapping servlet-nameOrderServlet/servlet-name url-pattern/order/url-pattern /servlet-mapping servlet servlet-nameDishAdminServlet/servlet-name servlet-classservlet.DishAdminServlet/servlet-class /servlet servlet-mapping servlet-nameDishAdminServlet/servlet-name url-pattern/admin/dish/url-pattern /servlet-mapping /web-app参数说明与逻辑context-param提供全局配置避免硬编码在Java类中amp;是XML实体必须写成amp;而非否则Tomcat启动报错。listener绑定DBListener其contextInitialized()方法会在Tomcat加载Web应用时执行用于初始化DataSource见2.3节。两个filter按声明顺序执行先EncodingFilter解决POST中文乱码再LoginCheckFilter拦截未登录访问。注意url-pattern用/*匹配所有路径但/user/*和/admin/*是精确前缀匹配确保静态资源如CSS/JS不被误拦。Servlet映射路径/login、/order等是浏览器地址栏输入的URL不是JSP文件路径。JSP页面通过RequestDispatcher.forward()跳转不暴露真实路径。2.3 DBListener用Tomcat自带连接池拒绝手动new Connection手写DriverManager.getConnection()是课程设计大忌——每次请求都建新连接10人并发就拖垮MySQL。必须用Tomcat JNDI数据源由容器管理连接池。util.DBListener.java代码如下package util; import javax.naming.Context; import javax.naming.InitialContext; import javax.naming.NamingException; import javax.sql.DataSource; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import javax.servlet.annotation.WebListener; import java.sql.Connection; import java.sql.SQLException; WebListener public class DBListener implements ServletContextListener { Override public void contextInitialized(ServletContextEvent sce) { try { // 1. 初始化JNDI上下文 Context initCtx new InitialContext(); Context envCtx (Context) initCtx.lookup(java:comp/env); // 2. 查找配置在context.xml中的数据源 DataSource ds (DataSource) envCtx.lookup(jdbc/DiningDS); // 3. 将数据源存入ServletContext供其他Servlet获取 sce.getServletContext().setAttribute(dataSource, ds); // 4. 测试连接可选但强烈建议 Connection conn ds.getConnection(); conn.close(); System.out.println(✅ DBListener: 数据源初始化成功连接池已就绪); } catch (NamingException | SQLException e) { e.printStackTrace(); throw new RuntimeException(❌ DBListener: 数据源初始化失败, e); } } Override public void contextDestroyed(ServletContextEvent sce) { // 容器关闭时清理通常无需手动操作 } }关键点说明WebListener注解替代web.xml中listener声明更简洁Servlet 3.0支持。java:comp/env是JNDI标准命名空间jdbc/DiningDS对应Tomcatconf/context.xml中配置的数据源名见2.4节。sce.getServletContext().setAttribute(dataSource, ds)将数据源存入全局上下文后续Servlet可通过getServletContext().getAttribute(dataSource)获取避免重复查找JNDI提升性能。测试连接conn.close()验证数据源可用性防止部署后才发现数据库连不上。2.4 Tomcat context.xml配置把数据库密码从web.xml里摘出来web.xml里明文写密码是严重安全漏洞且不符合JavaEE最佳实践。正确做法是将数据源配置移到Tomcat的conf/context.xml全局或META-INF/context.xml应用级。推荐后者便于项目迁移在WebContent/META-INF/context.xml中创建若无此目录则新建?xml version1.0 encodingUTF-8? Context Resource namejdbc/DiningDS authContainer typejavax.sql.DataSource factoryorg.apache.tomcat.jdbc.pool.DataSourceFactory driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/dining?useSSLfalseamp;serverTimezoneAsia/Shanghaiamp;allowPublicKeyRetrievaltrue usernameroot passwordyour_password maxActive20 minIdle5 maxWait10000 validationQuerySELECT 1 testOnBorrowtrue removeAbandonedOnBorrowtrue removeAbandonedTimeout60/ /Context参数详解namejdbc/DiningDS必须与DBListener中envCtx.lookup(jdbc/DiningDS)完全一致。maxActive20连接池最大连接数课程设计设20足够实际并发远低于此。validationQuerySELECT 1借连接时执行的检测SQL确保连接有效MySQL必须用SELECT 1不能用SELECT * FROM DUAL。testOnBorrowtrue每次从池中取连接时检测牺牲一点性能换稳定性课程设计必须开启避免因MySQL超时断连导致下单失败。removeAbandonedTimeout60连接被占用超60秒未归还强制回收防内存泄漏。3. DAO层设计用JDBC Template思想手写CRUD拒绝SQL拼接很多学生用String sql INSERT INTO user VALUES(name, pwd)这是SQL注入温床也是答辩时老师必问“如何防止注入”的反面教材。本方案采用预编译实体类映射DAO层只做数据操作不涉业务逻辑。3.1 BaseDAO封装通用JDBC操作减少模板代码dao/BaseDAO.java提供所有DAO继承的基础能力package dao; import util.DBUtil; import javax.sql.DataSource; import java.sql.*; import java.util.ArrayList; import java.util.List; public abstract class BaseDAO { protected DataSource dataSource; public BaseDAO() { // 从ServletContext获取数据源需在Servlet中setDataSource this.dataSource (DataSource) DBUtil.getServletContext().getAttribute(dataSource); } /** * 执行更新操作INSERT/UPDATE/DELETE * param sql 预编译SQL * param params 参数数组按?顺序 * return 影响行数 */ protected int executeUpdate(String sql, Object... params) { int rows 0; try (Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { // 设置参数 for (int i 0; i params.length; i) { ps.setObject(i 1, params[i]); } rows ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); throw new RuntimeException(❌ 执行更新失败: sql, e); } return rows; } /** * 查询单条记录如根据ID查用户 * param sql 预编译SQL * param rowMapper 结果集映射器 * param params 参数数组 * param T 返回实体类型 * return 实体对象或null */ protected T T executeQuerySingle(String sql, RowMapperT rowMapper, Object... params) { T result null; try (Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { for (int i 0; i params.length; i) { ps.setObject(i 1, params[i]); } try (ResultSet rs ps.executeQuery()) { if (rs.next()) { result rowMapper.mapRow(rs); } } } catch (SQLException e) { e.printStackTrace(); throw new RuntimeException(❌ 查询单条失败: sql, e); } return result; } /** * 查询多条记录如查所有菜品 * param sql 预编译SQL * param rowMapper 结果集映射器 * param params 参数数组 * param T 返回实体列表类型 * return 实体列表 */ protected T ListT executeQueryList(String sql, RowMapperT rowMapper, Object... params) { ListT list new ArrayList(); try (Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { for (int i 0; i params.length; i) { ps.setObject(i 1, params[i]); } try (ResultSet rs ps.executeQuery()) { while (rs.next()) { list.add(rowMapper.mapRow(rs)); } } } catch (SQLException e) { e.printStackTrace(); throw new RuntimeException(❌ 查询列表失败: sql, e); } return list; } // 回调接口供子类实现结果映射 FunctionalInterface public interface RowMapperT { T mapRow(ResultSet rs) throws SQLException; } }为什么这样设计executeUpdate和executeQueryXXX方法封装了Connection、PreparedStatement、ResultSet的获取与关闭避免学生忘记close导致连接泄露。try-with-resources确保资源自动释放。RowMapper是函数式接口让子类DAO用Lambda表达式定义映射逻辑代码更简洁见3.2节。所有异常转为RuntimeException上层Servlet统一捕获处理DAO层不处理业务错误如“用户名已存在”需在Service层判断。3.2 UserDAO登录校验必须用PreparedStatement防注入dao/UserDAO.java继承BaseDAO实现用户相关操作package dao; import model.User; import java.sql.SQLException; public class UserDAO extends BaseDAO { /** * 根据用户名查用户用于登录校验 * ✅ 使用预编译防止SQL注入 * ❌ 错误写法 SELECT * FROM user WHERE username username */ public User findByUsername(String username) { String sql SELECT id, username, password, role FROM user WHERE username ?; return executeQuerySingle(sql, rs - { User user new User(); user.setId(rs.getLong(id)); user.setUsername(rs.getString(username)); user.setPassword(rs.getString(password)); // 密码明文存储仅用于课程设计实际需BCrypt加密 user.setRole(rs.getString(role)); // user or admin return user; }, username); } /** * 检查用户名是否已存在注册时用 */ public boolean existsByUsername(String username) { String sql SELECT COUNT(*) FROM user WHERE username ?; Long count executeQuerySingle(sql, rs - rs.getLong(1), username); return count ! null count 0; } /** * 创建新用户注册 */ public int insert(User user) { String sql INSERT INTO user (username, password, role) VALUES (?, ?, ?); return executeUpdate(sql, user.getUsername(), user.getPassword(), user.getRole()); } }血泪经验登录校验findByUsername必须用?占位符绝对禁止字符串拼接。曾有学生被老师现场输入 OR 11直接绕过登录进入管理员页当场挂科。existsByUsername返回Long而非boolean因为COUNT(*)结果是数字直接映射更安全。密码明文存储仅限课程设计演示实际必须用BCryptPasswordEncoder.encode(password)但课程设计报告里写“此处应使用BCrypt加密”即可过关。3.3 DishDAO菜品管理需支持分页避免一次查几千条撑爆内存dao/DishDAO.java实现菜品增删改查重点解决分页问题package dao; import model.Dish; import java.util.List; public class DishDAO extends BaseDAO { /** * 分页查询菜品管理员后台用 * param offset 起始行MySQL从0开始 * param limit 每页数量 * return 菜品列表 */ public ListDish findAllWithPagination(int offset, int limit) { String sql SELECT id, name, price, description, status FROM dish ORDER BY id DESC LIMIT ?, ?; return executeQueryList(sql, rs - { Dish dish new Dish(); dish.setId(rs.getLong(id)); dish.setName(rs.getString(name)); dish.setPrice(rs.getBigDecimal(price)); dish.setDescription(rs.getString(description)); dish.setStatus(rs.getInt(status)); // 1上架, 0下架 return dish; }, offset, limit); } /** * 获取总记录数用于计算页数 */ public int getTotalCount() { String sql SELECT COUNT(*) FROM dish; Long count executeQuerySingle(sql, rs - rs.getLong(1)); return count null ? 0 : count.intValue(); } /** * 根据ID查单个菜品修改前预加载 */ public Dish findById(Long id) { String sql SELECT id, name, price, description, status FROM dish WHERE id ?; return executeQuerySingle(sql, rs - { Dish dish new Dish(); dish.setId(rs.getLong(id)); dish.setName(rs.getString(name)); dish.setPrice(rs.getBigDecimal(price)); dish.setDescription(rs.getString(description)); dish.setStatus(rs.getInt(status)); return dish; }, id); } /** * 更新菜品上架/下架、改价格 */ public int update(Dish dish) { String sql UPDATE dish SET name?, price?, description?, status? WHERE id?; return executeUpdate(sql, dish.getName(), dish.getPrice(), dish.getDescription(), dish.getStatus(), dish.getId()); } /** * 删除菜品物理删除课程设计简化处理 * ⚠️ 注意真实系统应逻辑删除加deleted字段避免订单关联失效 */ public int deleteById(Long id) { String sql DELETE FROM dish WHERE id ?; return executeUpdate(sql, id); } }参数说明与避坑LIMIT ?, ?中第一个?是offset第二个是limitMySQL语法要求如此。Oracle用ROWNUMPostgreSQL用OFFSET/LIMIT课程设计用MySQL即可。getTotalCount()单独查总数避免在findAllWithPagination里用SQL_CALC_FOUND_ROWS已废弃。deleteById是物理删除课程设计允许但报告中必须注明“生产环境应改为逻辑删除保留历史订单关联完整性”。4. 事务与会话下单扣库存和生成订单必须原子执行否则答辩被问“怎么保证数据一致性”这是JavaEE课程设计的分水岭。90%的学生能实现“点击下单→跳转成功页”但无法解释“如果扣库存成功、写订单失败怎么办”——这就是事务边界的缺失。本节用Connection.setAutoCommit(false)手动控制事务并用HttpSession管理用户登录态。4.1 OrderService用Connection传递事务拒绝跨DAO事务失控service/OrderService.java是业务逻辑中枢不直接操作数据库只协调DAOpackage service; import dao.DishDAO; import dao.OrderDAO; import dao.UserDAO; import model.*; import javax.sql.DataSource; import java.math.BigDecimal; import java.sql.Connection; import java.sql.SQLException; import java.util.Date; import java.util.List; public class OrderService { private final DishDAO dishDAO new DishDAO(); private final OrderDAO orderDAO new OrderDAO(); private final UserDAO userDAO new UserDAO(); /** * 创建订单含事务控制 * param userId 用户ID * param dishIds 菜品ID列表逗号分隔如1,3,5 * return 订单ID 或 -1失败 */ public long createOrder(long userId, String dishIds) { // 1. 解析菜品ID列表 String[] idStrs dishIds.split(,); ListLong dishIdList new ArrayList(); for (String idStr : idStrs) { if (!idStr.trim().isEmpty()) { dishIdList.add(Long.parseLong(idStr.trim())); } } if (dishIdList.isEmpty()) { return -1; } Connection conn null; try { // 2. 从数据源获取连接并关闭自动提交 conn DBUtil.getDataSource().getConnection(); conn.setAutoCommit(false); // 关键开启事务 // 3. 查询菜品价格并计算总价 BigDecimal totalAmount BigDecimal.ZERO; for (Long dishId : dishIdList) { Dish dish dishDAO.findById(dishId); if (dish null || dish.getStatus() ! 1) { throw new RuntimeException(❌ 菜品ID dishId 不存在或已下架); } totalAmount totalAmount.add(dish.getPrice()); } // 4. 扣减库存假设每道菜库存充足课程设计简化 // 实际应查stock表并update stock set quantity quantity - 1 where dish_id ? // 此处省略库存表用status模拟上架可售 // 5. 生成订单主表 Order order new Order(); order.setUserId(userId); order.setTotalAmount(totalAmount); order.setStatus(1); // 1待支付 order.setCreateTime(new Date()); long orderId orderDAO.insert(conn, order); // 传入conn确保同一事务 // 6. 生成订单明细 for (Long dishId : dishIdList) { OrderItem item new OrderItem(); item.setOrderId(orderId); item.setDishId(dishId); // 查询菜品名称和价格存入明细表防菜品下架后订单显示null Dish dish dishDAO.findById(dishId); item.setDishName(dish.getName()); item.setDishPrice(dish.getPrice()); item.setQuantity(1); orderDAO.insertItem(conn, item); // 同一conn } // 7. 提交事务 conn.commit(); return orderId; } catch (SQLException e) { // 8. 回滚事务 if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); throw new RuntimeException(❌ 创建订单失败已回滚, e); } finally { // 9. 关闭连接注意不close conn由调用方或Filter管理 if (conn ! null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } } }关键逻辑说明conn.setAutoCommit(false)是事务起点所有后续DAO操作必须复用同一个Connection对象通过参数传递否则各DAO用自己dataSource.getConnection()事务失效。orderDAO.insert(conn, order)和orderDAO.insertItem(conn, item)的conn参数确保插入主表和明细表在同一个物理连接上。conn.rollback()在catch块中执行必须放在异常处理内否则事务不回滚出现“订单生成了但库存没扣”的脏数据。conn.close()放在finally但注意课程设计中连接由Tomcat池管理close只是归还连接非销毁。4.2 LoginCheckFilter用HttpSession校验登录态拒绝Cookie伪造util/LoginCheckFilter.java拦截未登录访问核心是HttpSession的正确使用package util; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; public class LoginCheckFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; // 1. 获取当前会话不创建新会话 HttpSession session request.getSession(false); // 2. 检查session是否存在且包含user属性 if (session null || session.getAttribute(user) null) { // 3. 未登录重定向到登录页非forward避免URL暴露 String contextPath request.getContextPath(); response.sendRedirect(contextPath /login.jsp); return; } // 4. 已登录放行 chain.doFilter(req, resp); } }为什么用getSession(false)getSession(true)或getSession()会在session不存在时自动创建新session导致未登录用户也有session ID浪费资源且不安全。getSession(false)只获取现有session不存在则返回null精准判断登录态。session.getAttribute(user)存储的是model.User对象由LoginServlet在登录成功后设置session.setAttribute(user, user)。response.sendRedirect()是客户端重定向URL变为/login.jsp而request.getRequestDispatcher().forward()是服务端转发URL不变此处必须用redirect否则用户看到/user/order.jsp却未登录体验诡异。4.3 OrderServlet接收请求、调用Service、跳转页面三步闭环serlvet/OrderServlet.java是下单入口体现MVC中CController职责package servlet; import model.User; import service.OrderService; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; WebServlet(/order) public class OrderServlet extends HttpServlet { private final OrderService orderService new OrderService(); Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 获取登录用户 HttpSession session request.getSession(false); if (session null || session.getAttribute(user) null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } User user (User) session.getAttribute(user); // 2. 获取请求参数 String dishIds request.getParameter(dishIds); // 前端form提交的隐藏域 if (dishIds null || dishIds.trim().isEmpty()) { request.setAttribute(error, 请选择菜品); request.getRequestDispatcher(/user/order.jsp).forward(request, response); return; } try { // 3. 调用Service创建订单 long orderId orderService.createOrder(user.getId(), dishIds); // 4. 成功后跳转到订单详情页 request.setAttribute(orderId, orderId); request.getRequestDispatcher(/user/order-success.jsp).forward(request, response); } catch (RuntimeException e) { // 5. 业务异常返回错误信息 request.setAttribute(error, e.getMessage()); request.getRequestDispatcher(/user/order.jsp).forward(request, response); } } }流程闭环说明doPost处理表单提交dishIds来自前端input typehidden namedishIds value1,2,3避免GET参数过长。orderService.createOrder()抛出的RuntimeException被catch捕获request.setAttribute(error, ...)将错误信息传给JSPforward保持URL不变用户体验好。成功后forward到order-success.jsp显示订单号不redirect避免刷新重复提交。5. 避坑指南答辩老师最爱问的5个问题及对应代码级解决方案课程设计答辩不是背报告而是看你是否真懂代码。以下5个问题90%学生答不全附真实代码级解决方案5.1 现象登录后访问/user/order.jsp页面显示“未登录”F12看Network发现sessionID变了原因web.xml中未配置session-configTomcat默认session超时30分钟但更致命的是cookie path不匹配。当应用部署路径不是/如/dining-system而JSP中response.sendRedirect(/user/order.jsp)写成绝对路径/user/order.jsp浏览器发送cookie时path为/服务器找不到对应session。解决在web.xml中添加session配置session-config session-timeout60/session-timeout !-- 单位分钟 -- cookie-config http-onlytrue/http-only securefalse/secure !-- 开发环境用HTTP生产环境设true -- /cookie-config /session-config所有重定向用request.getContextPath()动态拼接// 错误 ❌ response.sendRedirect(/user/order.jsp); // 正确 ✅ response.sendRedirect(request.getContextPath() /user/order.jsp);5.2 现象管理员删了菜品用户历史订单里该菜品显示“null”原因订单明细表order_item只存dish_id未冗余存储菜品名称和价格。当菜品被删JOIN dish查不到数据JSP中${item.dishName}为null。解决在OrderItem实体类中增加冗余字段并在创建订单时写入// OrderItem.java private String dishName; // 冗余下单时存菜品名 private BigDecimal dishPrice; // 冗余下单时存当时价格 // ... getter/setterOrderService.createOrder()中item.setDishName(dish.getName()); // ✅ 写入冗余字段 item.setDishPrice(dish.getPrice());5.3 现象并发下单时库存扣减出错如两单同时买最后一份结果卖出两份原因课程设计未实现库存表但即使有SELECT stock FROM dish WHERE id1后UPDATE dish SET stockstock-1存在竞态条件。解决用MySQL行锁原子更新在DishDAO中添加public int decreaseStock(Long dishId, int quantity) { String sql UPDATE dish SET stock stock - ? WHERE id ? AND stock ?; return executeUpdate(sql, quantity, dishId, quantity); // WHERE条件确保库存足够 }调用时检查返回值if (rows 0) throw new RuntimeException(库存不足);5.4 现象中文菜单名在JSP中显示为“???”原因未统一字符集。Tomcat默认ISO-8859-1MySQL连接URL未设characterEncodingutf8JSP页面未声明% page contentTypetext/html;charsetUTF-8 %。解决三处必须一致context.xml中URL加characterEncodingutf8urljdbc:mysql://...characterEncodingutf8EncodingFilter.java中设置请求响应编码request.setCharacterEncoding(UTF-8); response.setCharacter p a hrefhttps://download.csdn.net/download/weixin_67271870/85874696 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p