ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

校园外卖管理系统JavaWeb项目实战:从环境配置到订单事务避坑指南

校园外卖管理系统JavaWeb项目实战:从环境配置到订单事务避坑指南 简介这是一套面向高校计算机相关专业学生的 JavaWeb 校园外卖管理系统完整项目适合作为课程设计、期末大作业或毕业设计参考也便于刚接触 JavaWeb 的小白按部就班实操运行。压缩包共 329 个文件约 28.7MB其中 73 个 java 源文件构成后端业务逻辑119 个 xml 负责配置与映射另有 21 个 html、18 个 css、22 个 js 搭建前端页面与交互并附带 sql 数据库脚本、properties 配置及 Maven 相关文件结构完整、层次清晰。项目围绕校园外卖场景涵盖用户下单、订单管理、菜品展示等典型模块前端引入 vant 等样式资源页面风格统一。目前已有 395 人学习下载代码纯手打且下载即可运行读者可直接获得一套可复用的赛题级方案对照源码理解 MVC 分层与数据库设计快速完成开发环境搭建与功能调试省去从零搭建的繁琐过程。1. 校园外卖管理系统从课程设计到能跑起来的 JavaWeb 项目很多同学做课程设计时拿到一个「基于 JavaWeb 的校园外卖管理系统」压缩包第一反应是解压、找 README、然后卡在环境配置上——Tomcat 版本对不上、JDBC 驱动报 ClassNotFound、数据库连不上折腾两天连登录页都出不来。这个标题背后其实是一套非常典型的 JavaWeb 技术栈组合Servlet JSP MySQL JDBC外加一个能跑起来的前端页面。它解决的核心问题是在校园场景下学生能通过浏览器完成下单、商家能管理菜品、管理员能查看订单数据全部落到 MySQL 里。适合谁适合正在做 JavaWeb 课程设计、大作业、毕业设计初稿的在校生也适合想用一个完整案例把 Servlet 生命周期、Session 管理、JDBC 增删改查串起来的自学者。下面我按「先跑通、再拆解、后避坑」的顺序把这个项目从压缩包到可演示的完整路径讲清楚。2. 跑通之前环境选型与数据库设计先定死2.1 JDK、Tomcat、MySQL 的版本组合怎么选拿到源码包别急着改代码。先看WEB-INF/lib下有没有mysql-connector-java-x.x.x.jar这个 jar 的版本直接决定你 MySQL 驱动类名是com.mysql.jdbc.Driver还是com.mysql.cj.jdbc.Driver。常见做法是JDK 8 Tomcat 8.5 MySQL 5.7 或 8.0这套组合在 IDEA 里配置最省事。如果源码里用的是javax.servlet包那 Tomcat 必须选 9 及以下Tomcat 10 把包名改成了jakarta.servlet直接部署会报 404 或 ClassNotFound。我一般会先确认三件事web.xml的 servlet 版本号、lib下 jar 包列表、数据库连接工具类里的 URL 写法。URL 里如果带useSSLfalseserverTimezoneUTC说明是 MySQL 8 的写法如果只有jdbc:mysql://localhost:3306/xxx那 MySQL 5.7 也能跑。版本对不上时优先改连接 URL 和驱动类名而不是换 Tomcat。提示IDEA 里配置 Tomcat 时Deployment 选项卡下 Application context 建议设成/否则所有页面路径都要加项目名前缀登录后跳转容易 404。2.2 数据库表结构五张核心表撑起整个系统校园外卖管理系统的数据库通常围绕用户、商家、菜品、订单、订单详情展开。下面这张表是我从常见源码包里整理出的核心字段建库时直接照着写CREATE TABLE即可。表名核心字段作用userid, username, password, phone, address, role存学生、商家、管理员role 区分身份shopid, shop_name, owner_id, status商家店铺信息status 控制营业/打烊dishid, shop_id, dish_name, price, stock, image菜品外键关联 shopordersid, user_id, shop_id, total_price, status, create_time订单主表status 表示待接单/配送中/已完成order_itemid, order_id, dish_id, quantity, price订单明细记录每单点了什么建表时注意两点orders的status用 int 或 varchar 都行但代码里通常用 0/1/2 判断改表结构时要同步改 Servlet 里的 if 分支order_item的price要存下单时的单价不能只存 dish_id 去关联查否则菜品调价后历史订单金额会变。2.3 导入 SQL 脚本与修改数据库连接源码包里一般有个sql文件夹或.sql文件。用 Navicat 或命令行导入后找到src下的db.properties或DBUtil.java把用户名密码改成你本地的。// DBUtil.java 常见写法 public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/campus_takeout?useSSLfalseserverTimezoneUTCcharacterEncodingutf8; private static final String USER root; private static final String PASSWORD 你的密码; public static Connection getConnection() throws SQLException { try { Class.forName(com.mysql.cj.jdbc.Driver); // MySQL 8 用 cj5.7 去掉 cj } catch (ClassNotFoundException e) { e.printStackTrace(); } return DriverManager.getConnection(URL, USER, PASSWORD); } }这段代码的逻辑是静态方法每次调用都加载驱动并返回新连接。参数说明characterEncodingutf8防止中文菜品名乱码serverTimezoneUTC在 MySQL 8 下不加会报时区错误。如果导入 SQL 后表里没数据检查脚本里有没有INSERT语句很多课程设计包只建表不插数据登录时自然查不到用户。3. 在 IDEA 里把项目跑起来从导入到登录成功3.1 导入项目与配置 ArtifactsIDEA 打开项目后先看是不是 Maven 项目。如果是等依赖下载完如果不是手动把lib下的 jar 加到 Module 的 Dependencies 里。然后打开 Project Structure在 Artifacts 里新增一个 Web Application: Exploded把web目录设为 Web Resource Directory把classes输出到WEB-INF/classes。这一步翻车最多的地方是out目录下没有生成classes导致 Tomcat 启动后找不到 Servlet。解决方法是 Build 菜单里先 Build Artifacts再看out/artifacts/xxx_war_exploded/WEB-INF/classes下有没有.class文件。没有就检查 Sources Root 有没有标错。3.2 配置 Tomcat Server 与启动参数Run/Debug Configurations 里新增 Tomcat LocalDeployment 添加刚才的 Exploded artifact。Server 选项卡下 On Update action 选 Update classes and resources这样改 JSP 不用重启。VM options 里可以加-Dfile.encodingUTF-8防止控制台中文日志乱码。# 如果不用 IDEA手动部署到 Tomcat 的命令 cp -r campus-takeout /path/to/tomcat/webapps/ /path/to/tomcat/bin/startup.sh # 访问 http://localhost:8080/campus-takeout/login.jsp手动部署时项目文件夹名就是访问路径。如果放到webapps/ROOT下访问时不用加项目名。启动后看logs/catalina.out出现Server startup in xxx ms才算成功。3.3 登录流程走通Servlet 转发与 Session 写入登录页提交到LoginServletServlet 里拿request.getParameter(username)调 DAO 查 user 表查到就session.setAttribute(user, user)然后response.sendRedirect(index.jsp)。常见问题是登录后跳转 404原因是sendRedirect路径写成了绝对路径但没带 context path。// LoginServlet.java 核心片段 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username request.getParameter(username); String password request.getParameter(password); UserDao dao new UserDao(); User user dao.login(username, password); // 内部用 PreparedStatement 防注入 if (user ! null) { request.getSession().setAttribute(user, user); response.sendRedirect(request.getContextPath() /index.jsp); } else { request.setAttribute(msg, 用户名或密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); } }参数说明request.getContextPath()拿到项目名拼出来的路径在任何部署方式下都不会错。DAO 里必须用PreparedStatement不要用字符串拼接 SQL否则输入 or 11就能绕过登录。如果登录后 Session 丢失检查浏览器有没有禁用 Cookie或者 Tomcat 的context.xml里有没有配sessionCookiePath。4. 核心功能拆解下单、购物车与订单状态流转4.1 购物车用 Session 还是数据库校园外卖系统的购物车通常有两种做法Session 存 MapdishId, quantity或者建一张 cart 表。课程设计里用 Session 更简单不用建表但换浏览器就丢。我一般会看源码里有没有CartServlet如果有addToCart方法且操作的是session.getAttribute(cart)那就是 Session 方案。// 加入购物车Session 方案 MapInteger, Integer cart (MapInteger, Integer) session.getAttribute(cart); if (cart null) { cart new HashMap(); } int dishId Integer.parseInt(request.getParameter(dishId)); cart.put(dishId, cart.getOrDefault(dishId, 0) 1); session.setAttribute(cart, cart);逻辑说明getOrDefault保证第一次加购时从 0 开始累加。参数dishId从菜品列表页的链接传过来。如果要做数量增减再写一个updateCart方法把 quantity 改成指定值减到 0 就cart.remove(dishId)。4.2 下单时的事务处理订单主表与明细一起写下单是最容易出数据不一致的地方。正确做法是先往orders插一条拿到自增主键再循环往order_item插明细整个过程放在一个 Connection 里手动提交事务。Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交 // 1. 插入订单主表 String orderSql INSERT INTO orders(user_id, shop_id, total_price, status, create_time) VALUES(?,?,?,?,NOW()); PreparedStatement ps conn.prepareStatement(orderSql, Statement.RETURN_GENERATED_KEYS); ps.setInt(1, userId); ps.setInt(2, shopId); ps.setBigDecimal(3, totalPrice); ps.setInt(4, 0); // 0 表示待接单 ps.executeUpdate(); // 2. 拿到订单 id ResultSet rs ps.getGeneratedKeys(); int orderId 0; if (rs.next()) orderId rs.getInt(1); // 3. 插入订单明细 String itemSql INSERT INTO order_item(order_id, dish_id, quantity, price) VALUES(?,?,?,?); PreparedStatement ps2 conn.prepareStatement(itemSql); for (Map.EntryInteger, Integer e : cart.entrySet()) { ps2.setInt(1, orderId); ps2.setInt(2, e.getKey()); ps2.setInt(3, e.getValue()); ps2.setBigDecimal(4, getDishPrice(e.getKey())); ps2.addBatch(); } ps2.executeBatch(); conn.commit(); // 全部成功才提交 } catch (Exception e) { conn.rollback(); // 任何一步失败都回滚 e.printStackTrace(); } finally { conn.close(); }参数说明Statement.RETURN_GENERATED_KEYS让 JDBC 返回自增主键这是订单和明细关联的关键。addBatch和executeBatch减少数据库往返次数。如果下单后订单列表为空检查conn.commit()有没有执行或者orders表的user_id和 Session 里的 user id 是否一致。4.3 订单状态流转与商家接单订单状态一般用 0 到 4 表示0 待接单、1 已接单、2 配送中、3 已完成、4 已取消。商家端页面查status0的订单点接单后UPDATE orders SET status1 WHERE id?。学生端查自己的订单按create_time DESC排序。-- 商家接单 UPDATE orders SET status 1 WHERE id ? AND shop_id ?; -- 学生查看历史订单 SELECT o.id, o.total_price, o.status, o.create_time, s.shop_name FROM orders o JOIN shop s ON o.shop_id s.id WHERE o.user_id ? ORDER BY o.create_time DESC;注意UPDATE要带shop_id条件防止商家改到别家订单。如果状态改了但页面没变检查 JSP 里是不是用了c:if test${order.status 0}这种 JSTL 判断EL 表达式里 status 是 Integer 还是 String 会影响比较结果。5. 避坑与排查课程设计里最容易翻车的五件事5.1 中文乱码从 JSP 到数据库全链路现象菜品名显示成????或文字。原因JSP 页面pageEncoding、Servlet 的response.setContentType、数据库连接 URL 的characterEncoding、MySQL 表的CHARSET四处有一处没设 UTF-8 就会乱。解决JSP 头部写% page contentTypetext/html;charsetUTF-8 %Servlet 里request.setCharacterEncoding(UTF-8)放在第一行URL 加characterEncodingutf8建表用DEFAULT CHARSETutf8mb4。5.2 404路径对了但 Servlet 没映射上现象访问login报 404但login.jsp能打开。原因web.xml里url-pattern写的是/login但表单 action 写成了login少了斜杠或者用了WebServlet(/login)注解但web.xml的metadata-completetrue导致注解不生效。解决表单 action 统一用${pageContext.request.contextPath}/login检查web.xml的metadata-complete属性。5.3 数据库连接池耗尽Connection 没关现象操作几次后报Too many connections。原因DAO 里getConnection后没有在 finally 里close()每次请求都新建连接。解决每个 DAO 方法用 try-with-resources或者引入 Druid/C3P0 连接池。课程设计里至少保证finally { if (conn ! null) conn.close(); }。5.4 订单金额对不上浮点数用 double 计算现象购物车总价 19.99 元下单后变成 19.989999。原因Java 里double和float做金额计算会有精度丢失。解决金额字段用BigDecimal数据库用DECIMAL(10,2)。new BigDecimal(19.99)而不是new BigDecimal(19.99)。5.5 Tomcat 启动报端口占用现象启动时Address already in use: JVM_Bind。原因8080 端口被其他程序占用或者上次 Tomcat 没关干净。解决netstat -ano | findstr 8080找到 PID任务管理器结束进程或者改conf/server.xml里的 Connector port 为 8081。6. 让项目从「能跑」到「能讲」三个进阶技巧6.1 用 Filter 统一做登录拦截和编码设置课程设计答辩时老师常问「没登录能不能直接访问订单页」。加一个LoginFilter拦截/order/*、/cart/*等路径检查 Session 里有没有 user没有就跳登录页。同时把request.setCharacterEncoding(UTF-8)也放 Filter 里省得每个 Servlet 写一遍。WebFilter(urlPatterns {/order/*, /cart/*, /admin/*}) public class LoginFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; request.setCharacterEncoding(UTF-8); Object user request.getSession().getAttribute(user); if (user null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); } }这段代码的关键是urlPatterns的写法/order/*匹配所有子路径。如果拦截后静态资源也 404把 css/js 路径排除掉或者在 Filter 里判断 URI 后缀。6.2 用 JSTL 和 EL 表达式简化 JSP原始 JSP 里如果全是% %脚本片段答辩时不好看也容易出错。引入jstl.jar和standard.jar后用c:forEach遍历订单列表用fmt:formatDate格式化时间。% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % c:forEach items${orderList} varorder tr td${order.id}/td tdfmt:formatDate value${order.createTime} patternyyyy-MM-dd HH:mm//td td c:choose c:when test${order.status 0}待接单/c:when c:when test${order.status 1}已接单/c:when c:otherwise已完成/c:otherwise /c:choose /td /tr /c:forEachc:choose替代了 Java 里的 if-else页面更干净。注意order.createTime要是java.util.Date类型如果是String则fmt:formatDate会报错。6.3 答辩演示前必做的三项检查第一把数据库脚本重新导入一次确保演示环境数据干净第二用不同角色账号各走一遍完整流程——学生下单、商家接单、管理员查看统计第三把 Tomcat 日志级别调到 INFO演示时如果报错能快速定位。我自己的习惯是提前录一段三分钟的屏幕操作视频万一现场环境出问题直接放视频也能把功能讲清楚。这个项目值不值得做如果你能把订单事务、Session 购物车、Filter 拦截这三块讲明白它就不只是一个「能跑的作业」而是一个能写进简历的 JavaWeb 综合案例。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进