
简介一套基于Java技术栈的外卖系统APP完整项目面向Java/Android方向的课程设计、毕业设计及自主提升。项目仿照主流外卖应用覆盖用户下单、商家接单、配送员配送等核心业务闭环后端基于Spring Boot构建数据存储采用MySQL前后端通过RESTful API与JSON格式交互并引入JWT身份验证。压缩包共270个文件、约150.02MB工程结构清晰Kotlin源码负责Android端界面Java代码实现后端服务XML与SQL脚本分别承载布局配置和数据库初始化另有so库、APK安装包、Gradle构建脚本及docx课程设计报告其中png图片和gif动图可直观预览界面效果。已有168人学习下载。内含可直接运行安装的APK与完整工程目录便于对照源码理解依赖注入、数据访问、多线程与并发控制等机制配套文档还能支撑论文撰写与答辩准备适合边读源码边调试快速跑通用户下单到商家接单的完整流程。1. 外卖系统 Java 课程设计一份能跑通下单选餐全流程的“饥了么”源码很多同学拿到“java 外卖系统”课程设计需求时第一反应是去搜 Spring Boot Vue 全家桶觉得框架越新越有面子。可真到答辩现场老师问一句“ Servlet 生命周期是什么、 Session 什么时候失效、数据库连接为什么断了”反而卡壳。这份“饥了么”外卖系统 APP 源码包走的是最经典也最好讲的 Java Web 路线Servlet JSP MySQL浏览器里访问就是手机端布局的 H5 页面模拟 APP 的下单体验。它适合三类人课程设计需要直接交源码的在校生、准备 Java 面试想找个能说清业务闭环的求职者、刚学完 Servlet 想看看真实业务怎么落地的自学者。压缩包解压后代码结构、SQL 脚本、部署说明都在可以直接改、直接跑、直接讲。2. 前后端技术选型与分层落地Servlet、JSP、MySQL 的角色分工2.1 为什么选 Servlet JSP 而不是 Spring Boot这套源码没有引入 Spring、MyBatis 这些框架数据访问层是 JDBC 封装加 DBUtils 的思路页面层用 JSP。听起来有点“复古”但对课程设计场景非常合适。原因有三一是代码可解释性强每次请求从浏览器到 Servlet 再到 JSP 页面路径非常直观面试官问“一个请求是怎么走完的”你不需要背框架源码直接指文件讲就行二是依赖少部署只需要 Tomcat 和 MySQL不用配 Maven 私服只要 Java 环境变量配置没问题JDK 装好就能跑三是老师大概率会现场让你改功能比如加一个“满减活动”Servlet 里直接加判断逻辑比在 MyBatis 映射文件里找地方下手快得多。这套项目里“面向对象编程 Java”的体现也很典型买家、商家、管理员都抽象成 User 对象菜品、订单、订单明细各是一个 POJO。资源包里的代码没有过度封装每个类都能一眼看到属性和行为这对答辩时应对“你这个类为什么这么设计”很有利。2.2 数据库表设计与角色权限模型外卖系统最少需要四张核心表用户表、菜品表、订单主表、订单明细表。用户表里用 role 字段区分三种角色而不是拆成三张表这样登录接口只需要写一套逻辑后面按角色跳转页面即可。菜品表归属于商家用 status 字段控制上下架。订单主表存总金额、状态、收货地址订单明细表存每个菜品当时的名称、价格和数量。表名核心字段说明sys_userid, username, password, role, shop_idrole1 买家2 商家3 管理员dishid, shop_id, name, price, image, statusstatus1 上架0 下架ordersid, order_no, user_id, shop_id, total_amount, status, addressstatus 表示流程节点order_itemid, order_id, dish_id, dish_name, price, count冗余菜品快照防改名影响历史订单注意 order_item 表故意冗余了 dish_name 和 price。课程设计里很多人不懂这一点等到商家后台改了菜品价格发现历史订单里的金额也跟着变了才意识到明细表要存快照。这里单独拿出来讲是因为它在后期查“订单金额对不上”的问题时非常关键。2.3 项目目录结构与启动方式源码包解压后是标准的动态 Web 工程结构没有用 Maven直接导入 Eclipse 或 IDEA 的 Web 工程即可。需要重点看三个地方src 下 JDBC 工具类和 DAO、WebContent 下的 JSP 页面、WEB-INF/web.xml 里的 Servlet 映射。food/ ├── src/com/food/ │ ├── servlet/ # 登录、菜品、订单等 Servlet │ ├── dao/ # 数据访问层 │ ├── entity/ # User、Dish、Order 等 POJO │ └── util/ # DBUtil 连接工具 ├── WebContent/ │ ├── index.jsp # 入口按角色跳转 │ ├── buyer/ # 买家端页面 │ ├── seller/ # 商家端页面 │ ├── admin/ # 管理员页面 │ ├── upload/ # 菜品图片上传目录 │ └── WEB-INF/web.xml └── db_food.sql # 建库建表脚本第一次启动时顺序是固定的先导库再改数据库连接配置最后启动 Tomcat。mysql -uroot -p db_food.sql # 导入后检查表是否完整 mysql -uroot -p -e use food; show tables; # 启动 Tomcat默认端口 8080 cd /path/to/tomcat/bin ./startup.sh # 看到 Tomcat started 后访问 curl http://localhost:8080/food/先导库再启动是为了避免启动后连不上表来回排查。这里的上下文路径是 /food/如果改成 ROOT 部署访问路径就不带项目名JSP 里写绝对路径时要注意。DBUtil 里的连接串、用户名、密码是分开配置的换机器只需要改这一个类。注意Tomcat 启动脚本在 Windows 下是 startup.batmacOS/Linux 下是 startup.sh不要照抄 Linux 命令在 Windows 上运行。3. 登录鉴权与菜品管理Session 方案和 CRUD 的完整写法3.1 登录接口一次请求从表单到 Session登录是最能体现 Java Web 基础的部分。浏览器把表单 POST 到 LoginServletServlet 拿到参数后查询用户表校验通过就把 User 对象放进 Session然后按角色重定向到不同首页。核心代码如下WebServlet(/login) public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String username req.getParameter(username); String password req.getParameter(password); UserDao dao new UserDao(); User user dao.findByUsernameAndPassword(username, password); if (user null) { resp.sendRedirect(login.jsp?error1); return; } HttpSession session req.getSession(); session.setAttribute(loginUser, user); session.setMaxInactiveInterval(30 * 60); // 30 分钟无操作自动过期 if (seller.equals(user.getRole())) { resp.sendRedirect(seller/index.jsp); } else if (admin.equals(user.getRole())) { resp.sendRedirect(admin/index.jsp); } else { resp.sendRedirect(buyer/index.jsp); } } }代码里有几个细节值得在答辩时主动讲。req.setCharacterEncoding(UTF-8) 必须先于 getParameter 调用否则 POST 中文参数会乱码。session.setMaxInactiveInterval(30 * 60) 控制的是 Session 闲置过期时间单位是秒如果项目里要“记住我”功能就不能只靠 Session要额外做 Cookie 持久化。DAO 层查询用户时并没有把密码取出来在 Java 里 equals而是直接拼进 SQL 用 WHERE 条件过滤。常见做法是用 PreparedStatement 的占位符避免拼接注入风险这一点在面试里被问到的概率很高。资源包里的 UserDao 写的就是占位符方式public User findByUsernameAndPassword(String username, String password) { String sql SELECT * FROM sys_user WHERE username? AND password?; // 通过 DBUtil 获取连接PreparedStatement 设置两个参数 // 执行查询将 ResultSet 封装为 User 对象返回 }password 字段在这里是明文存储这是课程设计的常见简化。但你要知道真实项目要做 MD5 加盐或 BCrypt 哈希答辩老师如果追问能答出来就是加分项。3.2 菜品管理的 CRUD图片到底存哪里商家端最核心的功能是菜品管理涉及增删改查和图片上传。菜品图片的存储方案在课程设计里最容易翻车有些人把图片转成 Base64 直接存数据库结果数据库体积暴涨页面加载变慢有些人把图片存到服务器磁盘但没有建立 upload 目录上传时直接报错。这套源码的方案是图片存到 WebContent/upload 目录数据库 dish 表只存相对访问路径比如 /upload/1630000000000.jpg。文件名用时间戳加随机数生成避免用户上传同名文件互相覆盖。参考逻辑如下// 文件上传后再处理菜品信息 String fileName System.currentTimeMillis() _ new Random().nextInt(1000) extName; // extName 是 .jpg/.png 后缀 String savePath getServletContext().getRealPath(/upload); // 把上传文件写入 savePath / fileName dish.setImage(/upload/ fileName); Dish dish new Dish(); dish.setShopId(shopId); dish.setName(req.getParameter(name)); dish.setPrice(Double.parseDouble(req.getParameter(price))); dish.setStatus(Integer.parseInt(req.getParameter(status))); dish.setImage(/upload/ fileName); dishDao.insert(dish); resp.sendRedirect(seller/dishList.jsp);菜品表里 shop_id 从当前登录用户的 Session 里拿不能让商家随便提交一个 shopId 改别人的菜。这一点很多课设源码都不注意属于典型的越权漏洞。你在答辩时主动提到“我通过 Session 获取商家身份避免水平越权”比背十个设计模式都有用。3.3 JSP 与 Ajax 联动不刷新页面的上下架菜品列表页通常会做一个快速上下架按钮点击后不刷新页面只改变按钮文案。这就是一个典型的异步请求场景。前端在 JSP 里用 JavaScript 发请求到 UpdateDishStatusServlet后端返回 JSON前端根据结果更新 DOM。function toggleStatus(dishId, btn) { fetch(dish/status?id dishId) .then(r r.json()) .then(data { if (data.code 1) { // data.status 是更新后的状态 1 或 0 btn.textContent data.status 1 ? 下架 : 上架; } else { alert(操作失败 data.msg); } }); }这里用的是 fetch比 XMLHttpRequest 写起来短浏览器兼容性也足够。Servlet 端需要做三件事从 Session 里取 shopId校验这道菜确实属于该商家执行 update 后构造 JSON 返回。注意响应头的 ContentType 要设置成 application/json;charsetUTF-8否则前端取到的一直是字符串而不是 JSON 对象。4. 购物车到订单状态机核心流程的代码实现与数据一致性4.1 购物车为什么要放在 Session外卖系统的购物车比电商购物车简单因为用户只会在一家点餐。设计上直接把购物车放在 Session 里不落库。很多课设会把购物车建一张表反而把流程搞复杂用户没登录也能加购物车那购物车属于哪个用户临时用户和登录用户怎么合并放在 Session 里就绕开了这个问题。购物车的推荐结构是 MapdishId, CartItem而不是 ArrayList。因为加购同一个菜品时要合并数量用 Map 的 key 做唯一性判断最方便。public class CartItem { private Integer dishId; private String dishName; private Double price; private Integer count; // getter/setter 略 } // 购物车本体 // MapInteger, CartItem cart new HashMap(); // session.setAttribute(cart, cart);加购逻辑的关键点第一次加购时创建对象已经存在则累加 count同时判断菜品库存和上架状态。如果菜品已经下架不能只加数量要弹提示让用户确认。这属于业务规则不是简单 CRUD。你把这个逻辑讲清楚项目深度立刻不一样。4.2 下单事务与订单号生成下单是整个系统里唯一必须走数据库事务的地方。一单包含两条写入orders 主表和 order_item 明细表。如果主表插入成功、明细表插入失败就会出现“订单有总额但看不到买了什么”的脏数据。资源包里已经用 Connection 手工事务解决Connection conn DBUtil.getConnection(); try { conn.setAutoCommit(false); // 关闭自动提交 // 第一步插入订单主表拿到自增订单 id // INSERT INTO orders(order_no, user_id, shop_id, total_amount, status, address) // VALUES(?,?,?,?,1,?) // 第二步遍历购物车批量插入 order_item // 每条明细用订单 id 关联 // 第三步清空 Session 购物车 conn.commit(); // 全部成功才提交 } catch (SQLException e) { conn.rollback(); // 任何一步失败全部回滚 throw new RuntimeException(下单失败); } finally { DBUtil.close(conn); }事务代码的严格顺序是拿到数据库连接后立刻 setAutoCommit(false)整个业务块里不允许出现 DAO 内部自动获取新连接否则会因为连接不一致导致事务失效。这是最常见的“事务没起作用”原因后面避坑章会详细说。订单号生成不用 UUID因为 UUID 太长且无序支付场景不好查。常见做法是时间戳加用户 id 加随机数保证同一个用户在同一秒不会生成重复单号。String orderNo System.currentTimeMillis() String.format(%04d, userId) String.format(%03d, new Random().nextInt(1000));4.3 商家接单与状态流转订单状态不能随便改每一步都要有限制。这套系统定义的订单状态流程很清晰状态值含义下一个合法状态1待接单2 已接单 / 0 已取消2已接单3 配送中 / 0 已取消3配送中4 已完成4已完成无0已取消无商家接单的 Servlet 拿到订单 id 后先查当前状态只有 status1 才能改成 2。如果用户在下单后立刻取消商家接单时订单已经是 0就不能再被接走。这种前置校验往往被课设忽略导致用户取消的订单还能被商家看到并接单。状态更新建议在 SQL 里带上条件判断例如 UPDATE orders SET status2 WHERE id? AND status1这样并发场景下也不会出现重复接单或状态覆盖。判断影响行数如果返回 0 说明状态已经被改过要回提示“订单状态已变化请刷新”。5. 常见问题与避坑清单乱码、连不上库、订单状态错乱怎么排查5.1 中文乱码页面编码、请求编码、数据库编码不一致现象新增菜品名称保存后显示成“系统”登录用户名里的中文变成问号浏览器控制台各种编码警告。原因三层编码没对齐。JSP 页面 pageEncoding 是 UTF-8但表单 POST 请求里没有设置请求编码Tomcat 默认按 ISO-8859-1 解码MySQL 表结构又是 latin1数据一进一出就完全错乱。Git 上很多旧项目还保留着 GBK 编码导入 IDEA 后被自动转成 UTF-8 显示但实际文件字节没变运行起来又是一套乱码。解决第一统一所有 JSP 头部声明 pageEncodingUTF-8第二给所有 POST 请求入口加 CharacterEncodingFilter 过滤器在 Servlet 执行前把请求和响应编码都设为 UTF-8第三建库时明确指定 utf8mb4或者导入脚本后执行 ALTER TABLE dish CONVERT TO CHARACTER SET utf8mb4。这一步做完90% 的乱码问题都能消失。提示如果数据库连接串是 MySQL 8 及以上记得加 serverTimezoneAsia/Shanghai否则会报时区错误这个错误表面上看和乱码无关实际是驱动连接初始化失败。5.2 数据库连不上驱动包、时区、防火墙三座大山现象Tomcat 启动正常但一访问登录接口就报 CommunicationsException或者 Access denied for user rootlocalhost还有 ClassNotFoundException com.mysql.jdbc.Driver。原因第一mysql-connector-java.jar 没放进 WEB-INF/lib只放在 IDE 外部库列表里部署到 Tomcat 时不会打包进去第二JDBC URL 用了旧的 com.mysql.jdbc.Driver 驱动类MySQL 8 之后必须换成 com.mysql.cj.jdbc.Driver第三root 密码或用户名配置不对连接串写死在 DBUtil 里换机器没同步改。解决顺序检查四件事。确认 WEB-INF/lib 下有没有驱动 jar确认 DBUtil 里的驱动类名和 MySQL 大版本匹配确认数据库密码不是空密码但连接串里却没写最后测试本机命令行能不能连上。命令行能连而 Java 连不上基本就是 jar 缺失或 URL 写错。这里要特别提醒不要在代码里打印账号密码后传到 GitHub课设源码打包前检查一遍 DBUtil换成你自己本机的测试账号。5.3 订单状态错乱重复提交带来的脏读现象买家在订单确认页快速点了两次“提交订单”后台生成两条一模一样但单号不同的订单或者订单主表有记录明细表是空的。原因下单 Servlet 没有做重复提交拦截两次请求都成功执行了事务。第一层防护本该在前端按钮点击后立刻 disabled但前端防不住懂技术的用户直接重放请求所以后端必须做幂等。明细表为空则是事务边界走错常见于 DAO 里每次调用 getConnection()下单主表插入一个连接明细表插入又换了一个连接setAutoCommit(false) 只对第一个连接生效主表提交了明细表却没提交。解决前端在表单提交后把提交按钮置灰这是最基础的一层。后端在进入下单逻辑前检查 Session 里是否已有“下单中”标记没有则设置标记完成事务后移除。更稳的做法是依赖数据库事务和条件更新把接单状态的 UPDATE 和明细 INSERT 放到同一个 Connection 里。5.4 页面样式丢失请求转发与重定向的路径差异现象从登录页跳转到买家首页后 CSS 全部丢失图片也不显示浏览器 F12 看到一堆 404。原因很多人写跳转时用了请求转发 request.getRequestDispatcher(buyer/index.jsp)转发不会改变浏览器地址栏 URL页面里写的相对路径 css/style.css 是基于当前地址解析的地址还是 /food/login 时浏览器去请求 /food/css/style.css而这个目录根本不存在。如果用的是重定向 sendRedirect浏览器地址变成了 /food/buyer/index.jsp相对路径才正常。解决JSP 页面所有静态资源引用不要写相对路径统一写成 ${pageContext.request.contextPath}/css/style.css。link relstylesheet href${pageContext.request.contextPath}/css/style.csslogin.jsp 在根目录、buyer/index.jsp 在子目录用相对路径天然就会出问题。这个写法不仅解决登录跳转也避免以后把页面从 buyer 目录挪到别的目录时再次翻车。排查此类问题时最快的定位方式是看浏览器地址栏 URL 和资源请求 URL 是否同一个层级不是就用 contextPath。6. 部署验证与进阶改造从本机跑通到可演示的完整闭环6.1 用 curl 写一条冒烟脚本项目做完以后最怕演示现场翻车。我一般会在答辩前用 curl 把登录、加购、下单这条链路跑一遍比用浏览器点还靠谱因为脚本能明确看到每次请求的状态码和跳转地址。# 第一步登录并保存 Cookie curl -c cookies.txt -d usernamebuyerpassword123456 \ http://localhost:8080/food/login # 第二步带着 Cookie 访问买家首页确认重定向成功 curl -b cookies.txt -L http://localhost:8080/food/buyer/index.jsp # 第三步直接发起加购请求这里路径按实际 Servlet 映射调整 curl -b cookies.txt -d dishId1count1 \ http://localhost:8080/food/cart/add如果登录失败cookies.txt 文件里就不会有 JSESSIONID后面的加购请求都会因为没有 Session 而跳回登录页。这套脚本在 Windows 下用 Git Bash 也能跑建议拷贝到项目根目录。6.2 把 H5 页面包成真正 APP 壳这套源码前端是 H5 页面模拟 APP 的交互但课程设计答辩时如果你想多讲一层可以把它包进 Android WebView。Android Studio 里用 WebView 加载 Tomcat 地址再通过 JS Bridge 调原生方法比如调用系统相机拍菜品图。这样项目标题里“APP”的定位就更加成立同时你能讲清楚 Web 层与原生层的边界。不过这把火不要烧太大先保证 Web 端链路稳定再谈壳不然核心业务都没跑通套壳反而暴露更多问题。我有一次演示前嫌数据库连接串太长顺手删掉了 serverTimezone 参数结果现场所有查询直接抛异常。从那以后我每次去演示机房都强制先跑一遍 curl 冒烟脚本再插投影仪。项目代码本身好不好是一回事能不能当场跑起来才是另一回事。这份“饥了么”源码我建议你拿到后第一件事不是看功能而是按第 2 章的启动顺序完整跑一遍确认链路通畅后再改需求、写答辩稿。希望帮到你。本文还有配套的精品资源点击获取