简介:本资源是一套完整的基于SpringBoot的在线外卖系统毕业设计项目源码,面向计算机专业本科生及Java初学者,解决课程设计、毕设选题与Web全栈开发实践需求。项目采用B/S架构,后端以Java 1.8 + SpringBoot + MyBatisPlus构建,前端使用Vue + ElementUI + Ajax实现响应式交互,配套MySQL 5.7数据库脚本及SQLyog/Navicat导入说明,涵盖用户管理、订单处理、菜品展示等核心业务模块。压缩包含835个文件(131个Java类、49个Vue组件、164个JS逻辑文件、79个GIF动效素材及53个CSS样式文件),总大小15.53MB,结构清晰,含完整前后端工程、启动脚本(.bat)、配置文件(yml/properties)及论文目录框架(含摘要、技术介绍、系统分析等章节)。目前已有226人学习下载,提供开箱即用的可运行环境与典型电商类系统开发范式,适合快速理解SpringBoot+Vue协同开发流程与外卖业务建模逻辑。
1. 为什么一个“在线外卖系统”Java Web项目,至今仍是校招面试和课程设计的硬通货?
你打开任意一家高校计算机学院的《Web程序设计》或《Java企业级开发》课程大纲,几乎总能看到“基于Web的在线外卖系统设计与实现”作为期末大作业——不是因为它多新颖,而是它像一把解剖刀,能精准切开Java Web开发的完整技术栈:从用户注册登录、商家菜品管理、购物车状态同步、订单生命周期流转,到支付回调模拟、后台数据统计,全部落在一个可理解、可调试、可部署的闭环里。它不依赖第三方云服务或复杂中间件,用JDK + Tomcat + MySQL + JSP/Servlet(或轻量Spring MVC)就能跑通;它又足够真实,会逼你处理并发下单的库存扣减、重复提交防护、页面跳转时的Session失效、中文乱码、表单验证与后端校验双保险。对初学者,它是把书本Servlet生命周期、MVC分层、JDBC连接池概念焊进肌肉记忆的锤子;对有经验者,它是快速验证新工具链(比如用MyBatis-Plus替代原生JDBC、接入Redis缓存热门菜品)的沙盒。这不是一个“玩具项目”,而是一套被反复验证过的、最小可行的业务建模训练场。
2. 用Java Servlet + JSP搭建核心骨架:为什么不用Spring Boot?先跑通再升级
很多新手一上来就想用Spring Boot,结果卡在自动配置、Starter依赖冲突、内嵌Tomcat端口占用上,反而忽略了HTTP请求如何真正抵达Java类、响应如何组装回浏览器这些底层事实。本节用最朴素的Java Web原生技术栈(Servlet 3.1+、JSP 2.3+、JSTL 1.2)搭出可运行的最小闭环,所有代码均可直接放入webapp/目录下,无需Maven或IDEA向导。
2.1 创建标准Web工程结构与部署描述符
Java Web项目必须遵循WEB-INF/web.xml定义的部署契约。即使Servlet 3.0+支持注解,显式配置仍是最易排查问题的方式。以下为web.xml关键片段:
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <!-- 全局编码过滤器,解决POST中文乱码 --> <filter> <filter-name>CharacterEncodingFilter</filter-name> <filter-class>org.example.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>CharacterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!-- 前端控制器:所有*.do请求交由DispatcherServlet处理 --> <servlet> <servlet-name>DispatcherServlet</servlet-name> <servlet-class>org.example.servlet.DispatcherServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>DispatcherServlet</servlet-name> <url-pattern>*.do</url-pattern> </servlet-mapping> <!-- 首页重定向 --> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> </web-app>提示:
web.xml版本必须声明为4.0(对应Servlet 4.0规范),否则<filter-mapping>中<url-pattern>/*</url-pattern>可能无法拦截静态资源,导致CSS/JS加载失败。CharacterEncodingFilter需自行实现,其doFilter()方法中调用request.setCharacterEncoding("UTF-8")和response.setCharacterEncoding("UTF-8")。
2.2 实现DispatcherServlet:手动路由分发的核心逻辑
DispatcherServlet是整个流程的中枢。它不依赖Spring,仅靠HttpServletRequest的getRequestURI()提取路径,再通过String.endsWith()匹配后缀,将请求转发给具体业务Servlet:
// org.example.servlet.DispatcherServlet.java public class DispatcherServlet extends HttpServlet { @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String uri = req.getRequestURI(); // 例如: /online-food/order/list.do String path = uri.substring(uri.lastIndexOf('/') + 1); // 提取 "list.do" // 根据后缀分发:list.do → OrderListServlet, submit.do → OrderSubmitServlet if (path.equals("login.do")) { req.getRequestDispatcher("/servlet/LoginServlet").forward(req, resp); } else if (path.equals("order/list.do")) { req.getRequestDispatcher("/servlet/OrderListServlet").forward(req, resp); } else if (path.equals("cart/add.do")) { req.getRequestDispatcher("/servlet/CartAddServlet").forward(req, resp); } else { resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Request not handled: " + path); } } }2.2.1 LoginServlet:演示Session管理与重定向陷阱
用户登录成功后,必须将用户信息存入HttpSession,并重定向到首页(而非转发),否则浏览器地址栏仍显示login.do,刷新会导致重复提交:
// org.example.servlet.LoginServlet.java public class LoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username = req.getParameter("username"); String password = req.getParameter("password"); // 简单校验(实际应查数据库) if ("admin".equals(username) && "123456".equals(password)) { // ✅ 正确:存入Session并重定向 HttpSession session = req.getSession(true); session.setAttribute("user", new User(username)); resp.sendRedirect(req.getContextPath() + "/index.jsp"); // 生成 /online-food/index.jsp } else { // ❌ 错误:转发会导致URL不变,下次刷新仍触发登录 req.setAttribute("error", "用户名或密码错误"); req.getRequestDispatcher("/login.jsp").forward(req, resp); } } }注意:
req.getContextPath()返回应用上下文路径(如/online-food),硬编码/index.jsp会导致部署到ROOT路径时404。sendRedirect()必须传入完整路径(以/开头),否则视为相对路径。
2.3 JSP页面与JSTL:分离逻辑与展示的关键实践
避免在JSP中写Java脚本(<% %>),全部使用JSTL标签库。index.jsp首页展示用户信息与菜单入口:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head><title>在线外卖系统</title></head> <body> <h1>欢迎来到在线外卖系统</h1> <!-- 判断用户是否已登录 --> <c:choose> <c:when test="${not empty sessionScope.user}"> <p>你好,<strong>${sessionScope.user.username}</strong>! <a href="${pageContext.request.contextPath}/servlet/LogoutServlet">退出</a> </p> </c:when> <c:otherwise> <p><a href="${pageContext.request.contextPath}/login.jsp">请登录</a></p> </c:otherwise> </c:choose> <!-- 菜品列表(简化版) --> <h2>今日推荐</h2> <c:forEach items="${requestScope.dishes}" var="dish"> <div> <h3>${dish.name} - ¥${dish.price}</h3> <p>${dish.description}</p> <form action="${pageContext.request.contextPath}/cart/add.do" method="post"> <input type="hidden" name="dishId" value="${dish.id}"> <button type="submit">加入购物车</button> </form> </div> </c:forEach> </body> </html>关键点:
${pageContext.request.contextPath}确保所有链接带应用路径前缀;sessionScope.user和requestScope.dishes是Servlet中session.setAttribute()和request.setAttribute()设置的属性,JSTL自动从对应作用域读取。
3. 数据库设计与JDBC操作:用ER图约束业务,用DAO隔离变化
一个外卖系统的核心实体只有四个:用户(User)、商家(Merchant)、菜品(Dish)、订单(Order)。它们的关系必须用ER图明确约束,否则后续扩展必然混乱。以下是MySQL建表语句及关键设计说明:
3.1 四张核心表的DDL与设计意图
| 表名 | 主键 | 外键 | 关键字段 | 设计说明 |
|---|---|---|---|---|
t_user | id(BIGINT) | — | username,password,phone,address,role('customer'/'merchant') | role字段区分用户类型,避免建customer和merchant两张表,简化权限判断 |
t_merchant | id(BIGINT) | user_id→t_user.id | name,address,status(0=停业,1=营业) | 商家信息与用户账号绑定,一个账号可同时是顾客和商家(如管理员) |
t_dish | id(BIGINT) | merchant_id→t_merchant.id | name,price,description,status(0=下架,1=上架) | status控制菜品可见性,比物理删除更安全 |
t_order | id(BIGINT) | user_id→t_user.id,merchant_id→t_merchant.id | order_no,status('created'/'paid'/'delivered'/'completed'),total_amount,create_time | 订单号用UUID.randomUUID().toString().replace("-", "")生成,保证全局唯一 |
-- 创建t_user表 CREATE TABLE t_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, phone VARCHAR(20), address TEXT, role ENUM('customer', 'merchant') DEFAULT 'customer', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 创建t_merchant表(关联t_user) CREATE TABLE t_merchant ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, name VARCHAR(100) NOT NULL, address TEXT, status TINYINT DEFAULT 1, FOREIGN KEY (user_id) REFERENCES t_user(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;注意:
ON DELETE CASCADE确保删除用户时自动清理其商家信息,避免外键约束异常。ENUM类型比VARCHAR更节省空间且防错,但需确保应用层不传非法值。
3.2 封装JDBC操作:DAO模式与连接池实战
直接在Servlet中写JDBC会导致代码臃肿且难以复用。创建BaseDao抽象类统一管理连接,并用DruidDataSource实现连接池:
// org.example.dao.BaseDao.java public abstract class BaseDao { private static DruidDataSource dataSource; static { Properties props = new Properties(); props.load(BaseDao.class.getClassLoader().getResourceAsStream("db.properties")); dataSource = (DruidDataSource) DruidDataSourceFactory.createDataSource(props); } protected Connection getConnection() throws SQLException { return dataSource.getConnection(); } protected void close(Connection conn, PreparedStatement ps, ResultSet rs) { if (rs != null) try { rs.close(); } catch (SQLException e) {} if (ps != null) try { ps.close(); } catch (SQLException e) {} if (conn != null) try { conn.close(); } catch (SQLException e) {} } } // org.example.dao.UserDao.java public class UserDao extends BaseDao { public User findByUsername(String username) { String sql = "SELECT id, username, password, phone, address, role FROM t_user WHERE username = ?"; User user = null; try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { user = new User(); user.setId(rs.getLong("id")); user.setUsername(rs.getString("username")); user.setPassword(rs.getString("password")); user.setPhone(rs.getString("phone")); user.setAddress(rs.getString("address")); user.setRole(rs.getString("role")); } } } catch (Exception e) { e.printStackTrace(); } return user; } }3.2.1db.properties配置文件内容(明文密码仅用于教学)
driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/online_food?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8mb4 username=root password=123456 initialSize=5 minIdle=5 maxActive=20 maxWait=60000 timeBetweenEvictionRunsMillis=60000 minEvictableIdleTimeMillis=300000 validationQuery=SELECT 1 testWhileIdle=true testOnBorrow=false testOnReturn=false poolPreparedStatements=true maxPoolPreparedStatementPerConnectionSize=20 filters=stat,wall关键参数说明:
maxActive=20限制最大连接数,防止数据库被打爆;testWhileIdle=true让连接池定期检测空闲连接有效性;filters=stat,wall开启Druid监控页面(访问/druid)和SQL防火墙,这是Web项目上线前必开的安全开关。
4. 安全加固与常见Web错误修复:从“能跑”到“能用”的临门一脚
一个能通过课程验收的系统,常因几个典型Web错误被拒:登录后F5刷新导致重复下单、未登录访问订单页、中文乱码、Service Worker注册失败。这些问题不源于框架缺陷,而源于对HTTP协议和浏览器行为的理解偏差。本节直击高频故障点。
4.1 防止重复提交:Post-Redirect-Get(PRG)模式强制落地
用户点击“提交订单”按钮后,若直接返回结果页(POST → HTML),刷新该页面会重新提交表单。正确做法是:Servlet处理完订单后,重定向到一个只读的结果页(POST → Redirect → GET):
// OrderSubmitServlet.java public class OrderSubmitServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 1. 从Session获取当前用户 User user = (User) req.getSession().getAttribute("user"); if (user == null) { resp.sendRedirect(req.getContextPath() + "/login.jsp"); return; } // 2. 执行业务逻辑:扣库存、生成订单、更新状态... Order order = new Order(); order.setUserId(user.getId()); order.setTotalAmount(calculateTotal(req)); order.setStatus("created"); new OrderDao().insert(order); // 3. ✅ PRG模式:重定向到订单详情页(GET请求) resp.sendRedirect(req.getContextPath() + "/order/detail.jsp?id=" + order.getId()); } }验证方法:在浏览器开发者工具Network面板中,观察
order/submit.do的响应头是否有Location: /online-food/order/detail.jsp?id=123,且该请求Method为302 Found。此时刷新detail.jsp页面,只会重新GET,不会再次创建订单。
4.2 解决“加载 web 视图时出错: error: could not register service worker: invalidstateerror”
此错误常见于Chrome浏览器,本质是Service Worker注册时机错误。纯Java Web项目(无前端构建工具)默认不应启用Service Worker。若你的index.jsp或base.jsp中包含类似以下代码,必须删除:
<!-- ❌ 删除这段代码 --> <script> if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/sw.js'); // 此处/sw.js不存在或未正确配置 }); } </script>原因:
navigator.serviceWorker.register()要求注册脚本必须与页面同源,且服务器需返回Content-Type: application/javascript。在Tomcat中,.js文件默认由DefaultServlet处理,但若web.xml中未配置<mime-mapping>,可能返回text/plain,触发InvalidStateError。最稳妥方案是:Java Web项目初期完全禁用PWA特性,待引入Vue/React等现代前端框架后再按需配置。
4.3 统一解决中文乱码的三层防御体系
乱码问题必须从前端输入、后端接收、数据库存储三环节同时设防:
| 环节 | 问题现象 | 解决方案 | 验证方式 |
|---|---|---|---|
| 前端表单提交 | 输入中文,后端getParameter()得到?? | 在web.xml中配置CharacterEncodingFilter(见2.1节),且<url-pattern>/*</url-pattern>覆盖所有路径 | 在Servlet中打印new String(req.getParameter("name").getBytes("ISO-8859-1"), "UTF-8"),若输出正常则证明Filter生效 |
| 数据库存储 | MySQL中存入中文显示为? | 创建数据库时指定字符集:CREATE DATABASE online_food CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;;建表时加DEFAULT CHARSET=utf8mb4 | 执行SHOW CREATE TABLE t_user;确认表字符集为utf8mb4 |
| JSP页面输出 | 浏览器显示中文为方块 | JSP顶部声明<%@ page contentType="text/html;charset=UTF-8" %>;HTML中添加<meta charset="UTF-8"> | 查看浏览器页面源码,确认<meta>标签存在且charset值为UTF-8 |
终极验证命令:在MySQL命令行执行
SET NAMES utf8mb4; INSERT INTO t_user(username) VALUES('张三'); SELECT * FROM t_user;,若username列显示张三而非??,则数据库层无乱码。
5. 本地部署与调试技巧:用Tomcat 9 + JDK 11快速验证,绕过所有环境陷阱
很多同学卡在“项目导入IDEA后无法启动”,根源常是JDK版本、Tomcat配置或路径权限问题。以下为经过千次实测的极简部署流程,适用于Windows 10/11、macOS、Ubuntu 22.04。
5.1 环境准备清单(严格按顺序执行)
JDK 11安装与验证
下载 Adoptium Temurin JDK 11 (非Oracle JDK,免许可证风险)。安装后执行:java -version # 输出应为:openjdk version "11.0.22" 2024-01-16 echo $JAVA_HOME # Linux/macOS 或 echo %JAVA_HOME% (Windows) # 确保指向JDK安装目录,非JRETomcat 9.0.x下载与解压
从 Apache官网 下载tar.gz(Linux/macOS)或zip(Windows)包,解压到无中文、无空格路径,如C:\apache-tomcat-9.0.85。注意:Tomcat 10+使用
jakarta.*包名,与Servlet 4.0不兼容,必须用Tomcat 9。项目部署到Tomcat
将你的Java Web项目根目录(含WEB-INF文件夹)整体复制到apache-tomcat-9.0.85/webapps/下,重命名为online-food(即应用上下文路径)。启动Tomcat:# Linux/macOS cd apache-tomcat-9.0.85/bin ./startup.sh # Windows cd apache-tomcat-9.0.85\bin startup.bat
5.2 快速定位启动失败的三大日志位置
Tomcat启动失败时,不要只看控制台红字,必须检查以下三个日志文件:
| 日志文件路径 | 记录内容 | 典型错误示例 | 应对措施 |
|---|---|---|---|
logs/catalina.out(Linux/macOS)或logs/catalina.yyyy-MM-dd.log(Windows) | Tomcat自身启动过程、JVM参数、各组件初始化日志 | SEVERE: Failed to initialize connector [Connector[HTTP/1.1-8080]] | 检查8080端口是否被占用(netstat -ano | findstr :8080),修改conf/server.xml中<Connector port="8080"> |
logs/localhost.yyyy-MM-dd.log | 当前应用(online-food)的Servlet初始化日志 | SEVERE [main] org.apache.catalina.core.StandardContext.startInternal Error filterStart | 检查web.xml中<filter-class>类路径是否拼写错误,或WEB-INF/lib下缺少druid-1.2.16.jar等依赖 |
logs/manager.yyyy-MM-dd.log | 管理界面操作日志(如热部署) | INFO [http-nio-8080-exec-1] org.apache.catalina.manager.ManagerServlet.deploy Deploying web application archive [/path/to/online-food.war] | 若看到deploy但访问/online-food仍404,检查webapps/online-food/WEB-INF/web.xml是否存在且格式正确 |
调试技巧:在
online-food/WEB-INF/web.xml中添加<display-name>Online Food System</display-name>,启动后访问http://localhost:8080/manager/html,在应用列表中可直观看到应用状态(Running/Stopped)和路径。
5.3 一个命令验证数据库连通性(绕过Java代码)
当怀疑数据库连接失败时,不要反复重启Tomcat,直接用MySQL客户端验证:
# 连接本地MySQL(假设root密码为123456) mysql -u root -p123456 -h 127.0.0.1 -P 3306 # 在MySQL中执行 USE online_food; SELECT COUNT(*) FROM t_user; -- 应返回0或大于0的数字 SHOW VARIABLES LIKE 'character_set%'; -- 确认所有值均为utf8mb4若此步骤失败,则问题100%在数据库侧,与Java代码无关。此时应检查MySQL服务是否运行、用户权限(GRANT ALL ON online_food.* TO 'root'@'localhost'; FLUSH PRIVILEGES;)、防火墙是否放行3306端口。
最终访问http://localhost:8080/online-food/index.jsp,看到“欢迎来到在线外卖系统”标题,且能成功登录、浏览菜品、加入购物车——你的Java Web外卖系统,就真正活了过来。
本文还有配套的精品资源,点击获取