☰
校园外卖管理系统JavaWeb项目实战:从环境配置到订单事务避坑指南
2026/10/8 23:24:47 网站建设 项目流程

简介:这是一套面向高校计算机相关专业学生的 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 里如果带useSSL=false&serverTimezone=UTC,说明是 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菜品,外键关联 shop
ordersid, 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?useSSL=false&serverTimezone=UTC&characterEncoding=utf8"; 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 用 cj,5.7 去掉 cj } catch (ClassNotFoundException e) { e.printStackTrace(); } return DriverManager.getConnection(URL, USER, PASSWORD); } }

这段代码的逻辑是:静态方法每次调用都加载驱动并返回新连接。参数说明:characterEncoding=utf8防止中文菜品名乱码;serverTimezone=UTC在 MySQL 8 下不加会报时区错误。如果导入 SQL 后表里没数据,检查脚本里有没有INSERT语句,很多课程设计包只建表不插数据,登录时自然查不到用户。

3. 在 IDEA 里把项目跑起来:从导入到登录成功

3.1 导入项目与配置 Artifacts

IDEA 打开项目后,先看是不是 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 Local,Deployment 添加刚才的 Exploded artifact。Server 选项卡下 On Update action 选 Update classes and resources,这样改 JSP 不用重启。VM options 里可以加-Dfile.encoding=UTF-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 写入

登录页提交到LoginServlet,Servlet 里拿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 '1'='1就能绕过登录。如果登录后 Session 丢失,检查浏览器有没有禁用 Cookie,或者 Tomcat 的context.xml里有没有配sessionCookiePath。

4. 核心功能拆解:下单、购物车与订单状态流转

4.1 购物车用 Session 还是数据库

校园外卖系统的购物车通常有两种做法:Session 存 Map<dishId, quantity>,或者建一张 cart 表。课程设计里用 Session 更简单,不用建表,但换浏览器就丢。我一般会看源码里有没有CartServlet,如果有addToCart方法且操作的是session.getAttribute("cart"),那就是 Session 方案。

// 加入购物车:Session 方案 Map<Integer, Integer> cart = (Map<Integer, 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.Entry<Integer, 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 已取消。商家端页面查status=0的订单,点接单后UPDATE orders SET status=1 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 contentType="text/html;charset=UTF-8" %>,Servlet 里request.setCharacterEncoding("UTF-8")放在第一行,URL 加characterEncoding=utf8,建表用DEFAULT CHARSET=utf8mb4。

5.2 404:路径对了但 Servlet 没映射上

现象:访问login报 404,但login.jsp能打开。原因:web.xml里<url-pattern>写的是/login,但表单 action 写成了login(少了斜杠),或者用了@WebServlet("/login")注解但web.xml的metadata-complete="true"导致注解不生效。解决:表单 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 prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <c:forEach items="${orderList}" var="order"> <tr> <td>${order.id}</td> <td><fmt:formatDate value="${order.createTime}" pattern="yyyy-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:forEach>

<c:choose>替代了 Java 里的 if-else,页面更干净。注意order.createTime要是java.util.Date类型,如果是String则fmt:formatDate会报错。

6.3 答辩演示前必做的三项检查

第一,把数据库脚本重新导入一次,确保演示环境数据干净;第二,用不同角色账号各走一遍完整流程——学生下单、商家接单、管理员查看统计;第三,把 Tomcat 日志级别调到 INFO,演示时如果报错能快速定位。我自己的习惯是提前录一段三分钟的屏幕操作视频,万一现场环境出问题,直接放视频也能把功能讲清楚。这个项目值不值得做?如果你能把订单事务、Session 购物车、Filter 拦截这三块讲明白,它就不只是一个「能跑的作业」,而是一个能写进简历的 JavaWeb 综合案例。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询