拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java毕设实战:SpringBoot+SSM网上订餐系统从零搭建全解析

Java毕设实战:SpringBoot+SSM网上订餐系统从零搭建全解析

摩尔街这套“基于Java+SpringBoot+SSM的网上订餐系统”,我在帮学生改项目、带课程设计的时候见过很多次,也亲手从零搭过类似结构的外卖平台。说实话,这题目在Java方向的毕设和课设里属于高频典型,SpringBoot负责后端快速开发,SSM(Spring+SpringMVC+MyBatis)作为底层整合框架解决业务逻辑与数据访问问题,前端配合Bootstrap这类轻量框架做页面展示,整体难度对中等基础的同学来说,刚刚好卡在“能做完”和“有东西可写”之间。

这篇博客我不打算只贴目录和截图,而是把这套系统从需求梳理、数据库设计、核心流程实现,到本地复现、调试排查、论文整理的完整链路讲透。不管你是拿它当毕业设计,还是想自己练手做一个小型电商点餐平台,照着这套思路都能少踩很多坑。

1. 项目整体设计与技术选型

1.1 为什么是SpringBoot+SSM,而不是纯SSM

很多人一看到标题“SpringBoot+SSM”会觉得奇怪,SpringBoot本身是简化Spring配置的框架,SSM是Spring+SpringMVC+MyBatis的组合,这两者听起来有重叠,但实际上搭配非常合理。

纯SSM项目最痛的点是配置地狱。web.xml、spring-mvc.xml、spring-mybatis.xml、数据库连接池配置、事务管理器配置,一套下来少说几十行XML,动不动就报Bean创建异常。SpringBoot用自动配置和约定大于配置的思想,把大部分重复工作收进了starter依赖里,application.yml一行配置就能替代原来五六行的XML。

但SpringBoot并没有替代SpringMVC和MyBatis,它的底层仍然是Spring IoC容器和SpringMVC的DispatcherServlet,MyBatis也照样通过Mapper接口操作数据库。所以在这个项目里,SSM是骨,SpringBoot是皮,皮让骨架更好看、更好用。

1.2 三层架构与模块划分

网上订餐系统的业务并不复杂,但是涉及的角色多:顾客、商家、管理员。每类角色操作的后台逻辑差异很大,所以在设计初期一定要分模块,否则代码写到后面就是一团乱麻。

我建议这样划分:

  • 顾客端(前台):用户注册登录、浏览菜品、加入购物车、下单、支付、查看订单状态
  • 商家端(后台):菜品管理(增删改查)、分类管理、订单接单/出餐/完成操作、销量统计
  • 管理端(平台后台):商家审核、用户管理、订单管理、数据报表

对应的Controller层也需要分开,比如CustomerController、SellerController、AdminController,不要全塞在一个Controller里。Service层负责业务逻辑,Mapper层只做数据存取,严格的单一职责能让后期调试轻松很多。

1.3 技术栈清单与版本建议

这里列一个我自己实测过的版本组合,稳定性比较好,不容易出现依赖冲突:

组件版本/说明
JDK1.8(8)
SpringBoot2.3.x 或 2.5.x
MyBatis3.5.x
MyBatis-Spring-Boot-Starter2.2.x 或 2.3.x
MySQL5.7 或 8.0
Maven3.6以上
前端Thymeleaf 或 JSP + Bootstrap + jQuery
开发工具IDEA 2020+ 或 Eclipse

注意:SpringBoot 2.x的官方维护虽然已经进入尾声,但作为毕设/课设,2.5.x版本足够稳定,网上资料也多,出了问题容易搜到解决方案。不建议一上来就用SpringBoot 3.x,因为3.x基于JDK17,很多老教程里的配置会失效,排查成本高。

2. 数据库设计:网上订餐系统的表结构拆解

2.1 核心数据表梳理

数据库设计是这类项目的灵魂。外卖订餐和普通电商不一样,它天然带有“位置、时效、接单”的业务语义,所以表的设计要重点考虑订单状态流转。

我一般会建这几张表:

  • user表(用户表):用户id、昵称、手机号、密码、头像、角色标识(0顾客/1商家/2管理员)、注册时间
  • seller表(商家表):商家id、店铺名称、店铺地址、联系电话、营业状态、评分
  • food表(菜品表):菜品id、所属商家id、分类id、菜品名称、图片、单价、库存、月销量
  • category表(菜品分类表):分类id、分类名称、所属商家id
  • cart表(购物车表):购物车id、用户id、菜品id、数量、单价(快照)
  • orders表(订单表):订单id、订单编号、用户id、商家id、订单金额、订单状态、收货地址、备注、创建时间
  • order_detail表(订单明细表):明细id、订单id、菜品id、菜品名称(快照)、数量、单价

值得一提的是一定要加上address表(收货地址表)。很多毕设懒得做多地址功能,只留一个字段,结果答辩时老师问你“用户换地址怎么处理”,就很难圆回来。哪怕做一个简易的多个地址切换,也是一个亮点。

2.2 订单状态流转与快照机制

订单状态是订餐系统里最容易乱的地方。我在设计时用的是int值,0到5,对应不同阶段:

0-待支付,1-待接单,2-商家已接单(制作中),3-配送中,4-已完成,5-已取消

状态流转不能乱跳。比如待支付状态下只能取消或支付;待接单状态商家可以接单,顾客也能取消;一旦进入制作中,就不能随便取消。

为什么要用状态值而不是直接改字符串?一是数据库存储效率更高,二是Java枚举配合switch更容易维护,三是后期做统计报表时可以直接按数字分组。

另外,订单明细一定要做快照。也就是说,下单那一刻把菜品的名称和单价冗余到order_detail里。因为商家可能会改价、改菜名,甚至下架菜品,如果订单明细里关联的是实时菜品表,那用户查历史订单时看到的信息可能会变,这对系统来说是不可接受的。

2.3 购物车与库存的并发问题

购物车表cart的核心字段是user_id + food_id的组合,前端每次点击加购物车,后端先去查这个组合是否已存在,存在则数量加1,不存在则新增记录。这块逻辑不复杂,但很多人会忽略库存校验。

我的经验是:加入购物车时不校验库存,展示商品时实时显示库存情况,真正扣减库存放在下单接口里。为什么?因为购物车是一个临时状态,用户可能加了菜最后不下单,你频繁锁库存会导致菜品卖不出,不划算。下单成功后一次性扣减,同时用悲观锁或乐观锁避免超卖问题。班级项目里并发量不高,用SELECT ... FOR UPDATE就足够了。

实际上,在JVM层面单机部署时我会在Service方法上加上synchronized或者使用数据库的乐观锁版本号字段来防止并发扣减。虽然有点重,但答辩时你能讲出“我用了乐观锁确保库存不超卖”,这是一个非常加分的点。

3. 核心业务流程与关键代码实现

3.1 登录鉴权:从Session到简易Token

网上订餐系统涉及三类用户,最朴素的实现是Session存用户id,配合拦截器做放行控制。但如果你想让项目显得比同学高一个档次,可以用拦截器+自定义注解做权限校验,或者直接在Controller参数里加一个当前用户的对象。

我的习惯是写一个LoginInterceptor,实现HandlerInterceptor接口,在preHandle里面从请求头里读取token(登录时生成UUID存到数据库或者内存Map),然后解析出用户对象放到ThreadLocal里。这样Controller里到处能拿当前用户,不用在每个方法里传HttpSession。

代码大致是这样:

public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (token == null || "".equals(token)) { response.setStatus(401); return false; } // 根据token查询用户 User user = userService.getUserByToken(token); if (user == null) { response.setStatus(401); return false; } UserThreadLocal.set(user); return true; } }

提示:这种简易Token方案不依赖Redis,适合没有引入中间件的课程设计项目。如果你们要求必须用Redis,也可以把Token存到Redis里,核心逻辑一样,只是存取位置变了。

3.2 下单功能的流程拆解

下单是订餐系统的核心业务,最少涉及四步操作:校验库存、生成订单主表、生成订单明细表、扣减库存。所以我强烈建议在Service层加上@Transactional注解,保证多张表的写入要么全部成功,要么全部回滚。

下单流程的伪代码:

@Transactional public Long createOrder(CreateOrderRequest request) { // 1. 查询购物车数据(根据用户id) List<CartItem> cartItems = cartMapper.findByUserId(request.getUserId()); // 2. 遍历校验库存 for (CartItem item : cartItems) { Food food = foodMapper.selectByPrimaryKey(item.getFoodId()); if (food.getStock() < item.getQuantity()) { throw new BizException("菜品【" + food.getName() + "】库存不足"); } } // 3. 计算总金额 BigDecimal total = calculateTotal(cartItems); // 4. 创建订单主记录 Orders order = new Orders(); order.setOrderNo(generateOrderNo()); // 时间戳+随机数 order.setUserId(request.getUserId()); order.setTotalAmount(total); order.setStatus(0); // 待支付 ordersMapper.insert(order); // 5. 批量插入订单明细快照 for (CartItem item : cartItems) { OrderDetail detail = new OrderDetail(); detail.setOrderId(order.getId()); detail.setFoodId(item.getFoodId()); detail.setFoodName(item.getFoodName()); // 快照 detail.setPrice(item.getPrice()); // 快照 detail.setQuantity(item.getQuantity()); orderDetailMapper.insert(detail); } // 6. 扣减库存 foodMapper.decreaseStock(item.getFoodId(), item.getQuantity()); // 7. 清空购物车 cartMapper.deleteByUserId(request.getUserId()); return order.getId(); }

这段逻辑每个步骤都有对应的数据库操作,却都在同一个事务里。如果不加事务,用户下单过程中数据库突然断电,很可能出现“订单主表有了、明细没有、库存已经扣了”的脏数据,答辩时这种bug被抓住就很难受。

3.3 订单编号生成策略

订单编号看起来不起眼,但最容易出问题。很多新手直接用SimpleDateFormat生成“yyyyMMddHHmmss”格式,结果同一秒内下单就会重复。我推荐用时间戳+随机数的组合:

String orderNo = System.currentTimeMillis() + String.format("%04d", new Random().nextInt(10000));

这样每秒可以容纳一万个不重复订单,对于课设来说完全足够。如果要求更严格一点,可以用UUID的前8位拼接时间戳,但UUID太长,订单明细表里做索引不太友好,而且展示给用户看也不美观。

3.4 后端分页与模糊搜索的通用写法

菜品搜索是网上订餐系统标配功能。MyBatis分页如果你不想手写LIMIT,可以用PageHelper插件,但是PageHelper有个坑,就是它基于ThreadLocal实现,如果SQL执行时上下文不是同一个线程,分页会失效。

我个人倾向于手写一个简单的分页查询参数对象,包含pageNum和pageSize,Mapper里写:

<select id="searchFoods" resultType="com.moore.pojo.Food"> SELECT * FROM food <where> <if test="name != null and name != ''"> AND name LIKE CONCAT('%', #{name}, '%') </if> <if test="categoryId != null"> AND category_id = #{categoryId} </if> </where> LIMIT #{offset}, #{pageSize} </select>

注意这里的offset是(pageNum-1)*pageSize,最好在Service层算好再传进去,而不是在Mapper里写乘法。原因很简单:业务逻辑尽量留在Service层,Mapper只做参数生效。

前端商品列表部分,我建议用简单的卡片式布局,每个菜品卡片显示图片、名称、价格、月销量、库存状态。加入购物车用Ajax提交,避免整个页面刷新,用户体验会好很多,同时也方便你对接后端的JSON接口。

3.5 支付模块的模拟实现

真正的支付接口需要对接微信支付或支付宝,需要商户号、证书、回调地址,毕设环境一般不具备条件。最省事也最安全的做法是做一个模拟支付:用户点击“去支付”,弹出一个模拟支付页面,确认后直接修改订单状态为“1-待接单”。

模拟支付页面可以做一个倒计时,确认后订单状态置为待接单,同时给商家端推送一个订单提醒(这里用简易的轮询即可,不需要WebSocket,否则复杂度爆炸)。如果你想让项目看起来更有亮点,再在支付页面加一个支付金额展示和“余额支付”,为了这个再去建一张user_account表也未尝不可。

4. 项目复现与调试实录:从0到能跑通的完整过程

4.1 环境准备与基础配置

拿到源码包后的第一件事不是点运行,而是检查自己的环境。我见过太多人卡在启动阶段,其实多半是环境变量有问题。

第一步装好JDK 1.8,确保命令行能输出java -version;第二步装好Maven,并确认settings.xml里配置了阿里云镜像;第三步装MySQL,建库建用户,赋予权限;第四步用IDEA打开项目,等待Maven依赖下载完成。

项目里的核心配置在src/main/resources/application.yml:

server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/moore?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.moore.pojo

注意:MySQL 8.0以上必须指定serverTimezone,否则启动报时区错误。driver-class-name也要用com.mysql.cj.jdbc.Driver,老版本的com.mysql.jdbc.Driver已经不能用了。

4.2 数据库脚本导入的坑与技巧

源码包里一般会有sql文件,通常是moore.sql或者db_moore.sql。先用Navicat或命令行建好空数据库,命名和配置文件保持一致,然后右键导入SQL脚本。

导入后一定要做的事:

  • 检查所有表是否创建成功
  • 检查测试账号的密码有没有被加密存储
  • 检查菜品图片路径是相对路径还是URL

如果账号密码是MD5加密的,你需要知道对应的明文。我常用的是“admin/admin123”,或者“123456”直接MD5‘e10adc3949ba59abbe56e057f20f883e’。如果不知道明文,可以直接在数据库里执行UPDATE user SET password='e10adc3949ba59abbe56e057f20f883e' WHERE username='admin',把密码重置成123456。

4.3 常见启动报错与排查方案

我整理了一份自己调试这个项目时反复踩过的坑,按出现频率排个序:

报错信息/现象根本原因解决方法
Access denied for user 'root'@'localhost'数据库密码错误检查application.yml的密码,注意特殊字符
ClassNotFoundException: com.mysql.cj.jdbc.DriverMaven依赖没下载reimport项目,检查pom.xml是否有mysql依赖
Port 8080 was already in use端口被占杀掉占用进程,或改server.port
Invalid bound statement (not found)Mapper接口和XML没绑定检查MyBatis配置的mapper-locations路径是否匹配
Table 'moore.user' doesn't exist数据库连错或脚本没导入核对数据库名和表名
页面中文乱码URL编码或编辑器编码不一致检查IDEA文件编码为UTF-8,数据库连接加characterEncoding

4.4 源码阅读顺序建议

很多学生拿到源码第一反应是从Controller开始读,结果云里雾里。我建议按这样的顺序阅读:

  1. 先看数据库表结构,理解实体之间的关系
  2. 再看pojo/entity包,对照表字段
  3. 看Mapper接口和XML,理解每个SQL的作用
  4. 看Service接口和实现类,先看业务逻辑流转
  5. 最后看Controller,理解请求入口和参数绑定

这样一层层从下往上读,逻辑是顺的。如果直接看Controller,很容易被各种方法签名绕晕,也不知道某个service方法背后到底执行了什么SQL。

5. 文档撰写与答辩实战经验

5.1 LW(论文)的写作框架与字数分配

这类项目的配套LW,也就是毕业设计论文,通常要求1.2万字左右。我的经验是不要平均分配,重点放在需求分析和系统设计上。

建议章节结构:

  • 绪论:背景意义、国内外研究现状、论文结构(约2000字)
  • 相关技术介绍:SpringBoot、SSM、MySQL、Bootstrap,介绍不要写太多源码,重点讲为什么选它(约1500字)
  • 系统分析:可行性分析(技术、经济、操作)、需求分析(功能性需求、非功能性需求)(约2000字)
  • 系统设计:总体架构设计、功能模块设计、数据库设计(E-R图、表结构)(约3000字)
  • 系统实现:分模块贴核心代码和截图,每段代码配文字说明(约2500字)
  • 系统测试:测试用例表、测试结果、性能测试(简单负载)(约1500字)
  • 总结与展望:完成工作、不足、改进方向(约500字)

论文中最容易扣分的是数据库设计部分,光贴SQL建表语句是不够的,必须画E-R图。E-R图不用太专业,用ProcessOn或者Draw.io画清楚就行,实体、属性、联系缺一不可。

5.2 调试文档的写法

调试文档本质上是运维笔记。源码包里带一份调试文档的话,你应该按这样的结构写:

  • 环境要求:JDK、Maven、MySQL版本
  • 项目导入步骤:IDEA打开项目、配置Maven、修改application.yml
  • 数据库初始化:导入脚本、创建数据库、修改账号密码
  • 启动顺序:先MySQL,再启动SpringBoot,最后浏览器访问
  • 默认账号列表:管理员、商家、顾客的测试账号
  • 常见错误处理:把上面那个表格搬进去

这份文档写完不只是应付交差,对你自己的答辩也有用。老师问“项目怎么部署”,你可以一气呵成地讲出来,印象分会好很多。

5.3 答辩高频问题与加分回答

答辩时老师最喜欢问的几个问题,我提前帮你们预演一遍:

问:你的项目为什么用SpringBoot还要写SSM?

答:SpringBoot解决的是配置复杂的问题,但它的核心IoC容器仍然是Spring,MVC层仍然是SpringMVC,ORM层用MyBatis,所以本质上就是SSM框架的SpringBoot版本。SpringBoot通过自动配置和starter依赖,把以前需要手写的大量XML配置封装起来,开发效率更高,但底层的请求处理流程和数据访问方式没有变。

问:订单超时未支付怎么处理?

如果你用了定时任务或延迟队列就能加分,如果没实现也不能慌,可以回答:当前版本采用用户手动取消和商家拒绝订单的机制规避超时问题。后续可以引入延迟队列或者Spring的@Scheduled定时扫描订单表,把超过30分钟未支付的订单自动置为已取消并释放库存。

问:你项目里如何保证数据库和安全?

密码采用MD5加密存储,SQL全部用PreparedStatement参数化,过滤XSS和SQL注入风险。订单和库存操作使用事务@Transactional,保证数据一致性。

5.4 二次开发方向建议

如果学有余力,我建议在这个项目上做三个小改造,范围小但能显著提升技术含量:

  1. 引入Redis做Token管理和菜品缓存,让登录态和热门菜品不频繁查数据库
  2. 使用WebSocket实现商家实时接单提醒,这样顾客下单后商家页面不需要手动刷新就能看到新订单
  3. 引入RabbitMQ或者简单的阻塞队列处理下单后的短信通知逻辑(模拟),把异步解耦体现出来

这三个方向改完,你的项目在班里基本就是第一梯队了。但要注意一次别全做完,每个改动都够写2000字论文内容,结合你自己时间决定。

6. 关于这套系统的几点个人体会

从带过的学生和对接过的真实需求来看,网上订餐系统的技术难点从来不在“功能多”,而在“状态准、数据稳、流程顺”。SpringBoot+SSM的组合,刚好用最小的配置成本把这三点兜住了——SpringBoot管理依赖和自动装配,Spring管业务对象,SpringMVC管请求分发,MyBatis管SQL和数据映射,各司其职。

我试过把这个系统部署到云服务器上演示,除了把数据库连接地址改成云数据库的IP之外,几乎不用改别的代码。这说明这套架构的迁移成本低,对环境依赖小,也是它能常年在毕设和课设里“霸榜”的原因。

最后再分享一个小技巧:如果你在本地调试时总是觉得启动慢,可以在IDEA里把Maven测试跳过,然后在SpringBoot的VM options加上-Dspring.devtools.restart.enabled=false,开发阶段有热部署工具的话可以开着。真等答辩演示时,可以提前启动好项目,别在台上盯着控制台等加载,这个小细节很多人不注意,结果现场翻车。

返回列表