Spring Boot外卖点餐系统实战:从技术选型到部署上线全解析
2026/9/10 17:55:41 网站建设 项目流程

做Java后端这些年,我见过太多同学拿着Spring Boot项目却不知道从哪下手。“小熊外卖点餐系统”算是一个很典型的Spring Boot实战项目:业务场景贴近真实生活、技术栈主流、代码量适中,不管是拿来写毕业设计,还是作为简历上的项目经验,都非常合适。这套系统我前后梳理了一段时间,今天干脆把整个项目的设计思路、核心逻辑和踩坑记录整理出来,给准备做毕设、或者刚入行想把Spring Boot吃透的朋友一份能直接“抄作业”的参考。

整个项目基于Spring Boot框架,源码结构清晰,前端可以配Vue做前后端分离,也可以直接用模板渲染。我会从技术选型讲起,再拆解数据库设计、核心业务代码、常见报错排查,最后补一些面试答辩时容易被追问的点。内容尽量按我实际开发时的思考顺序来写,不是教科书式的罗列,你看完能直接对着源码改。

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

1.1 “小熊外卖”是什么:一个能满足三类角色的点餐闭环

先把这个项目到底做了什么说清楚。小熊外卖不是一个只写了几个CRUD接口的玩具项目,它覆盖了外卖业务里最常见的三个端:用户端、商家端、管理端。

用户端做的事情很直观:注册登录、浏览菜品、按分类筛选、把菜加入购物车、提交订单、填写收货地址、在线支付(项目里一般是模拟支付)、查看订单状态、对已完成的订单评价。商家端主要维护菜品信息、上下架商品、处理用户订单(接单、出餐、完成)。管理端则是超级管理员视角,管理用户状态、审核商家信息、查看全平台订单数据、做简单的销量统计。

这三个端如果只靠硬编码写死,代码会乱成一团。所以项目在设计上采用了统一的账号体系,用角色字段区分用户类型,再配合拦截器做权限控制。我见过很多毕设项目把登录逻辑复制三份,一份给用户、一份给商家、一份给管理员,这种做法能跑但非常难维护,后来小熊外卖改成统一鉴权之后,代码量直接砍了三分之一。

1.2 技术栈:为什么偏偏是Spring Boot加MyBatis Plus

技术选型这块,项目用了Spring Boot 2.7.x搭配MyBatis Plus,Redis做缓存,MySQL存主数据,JWT做无状态登录。这套组合是当前中小型管理系统里最常见的主流搭配,稳定性和学习资料的丰富程度都经得住考验。

Spring Boot的好处不用多说,内置Tomcat、自动配置、starter机制让项目从零搭建到跑起来只需要几分钟。选择2.7.x而不是3.x,是因为很多学校机房和老项目依赖还在javax命名空间下,3.x切到jakarta之后兼容性容易出问题,而且2.7.x的社区资料最齐全,遇到问题一搜就有答案。

MyBatis Plus则是在原生MyBatis基础上做的增强工具。单表CRUD不用写XML,继承BaseMapper就有现成的增删改查方法。项目里查询菜品列表、按条件分页、逻辑删除这些高频操作,全是通过QueryWrapper和LambdaQueryWrapper完成的,代码非常精简。选择它而不是JPA,一是国内企业用MyBatis系列的比例更高,二是SQL可控性强,复杂统计查询写XML更灵活,也更贴合面试官对Java后端技术栈的预期。

Redis在项目里承担了三件事:存储登录令牌的会话信息、缓存热门菜品列表、记录购物车临时数据。有人会问,购物车直接存数据库不行吗?可以,但Redis的好处是读写快、自动过期,用户长时间不操作购物车数据也不会堆积成垃圾。这一点在答辩时很加分,能体现出你对“什么时候用缓存”有实际判断。

前端部分,如果源码里配的是Vue3加Element Plus,那就是典型的前后端分离结构,通过Axios调用后端接口。如果配的是Thymeleaf模板,那就是后端渲染模式。我更推荐前者,因为现在企业开发几乎都是前后端分离,Vue项目的加入能让整个系统在展示时更有说服力。

注意:如果你的电脑还没装Redis,一定要先启动Redis服务再运行项目,否则项目启动时会因为无法连接Redis直接报错。这是新手最容易踩的第一个坑。

2. 功能模块拆解与数据库核心表设计

2.1 功能边界:用户、商家、管理员各做什么

项目功能拆得是否清晰,直接决定了代码好不好写。我先画一条业务主线:用户注册登录 → 浏览菜品 → 加购物车 → 生成订单 → 模拟支付 → 商家接单 → 完成配送 → 用户评价。整条链路下来,每一个节点都能对应到后端一个独立的接口,这种“一节点一接口”的设计方式,让二次开发变得非常轻松。

用户端接口大致有:注册、登录、获取用户信息、修改密码、上传头像、按分类查菜品、搜索菜品、加购、查购物车、修改购物车数量、下单、取消订单、确认收货、添加收货地址、获取地址列表、提交评价、查看全部订单、按状态查订单。

商家端接口围绕“管菜”和“管单”展开:新增菜品、编辑菜品、删除菜品、上下架菜品、查看本店订单、接单、标记出餐、标记完成、查看本店销量统计。

管理端接口偏宏观:用户列表、封禁/解封用户、商家列表、审核商家入驻、全平台订单列表、分类管理、销量排行、营业数据概览。

看接口清单就能发现,很多接口逻辑是相似的,都是“查列表+改状态”。真正有技术含量的只有三个部分:登录鉴权、购物车和订单事务、以及并发场景下的库存扣减。后面我会一个个拆开讲。

2.2 表结构设计:订单表为什么冗余了那么多字段

数据库是这套系统的地基,表设计不合理,后面写代码就是给自己挖坑。小熊外卖的核心表我建议按下面这张清单来设计:

表名作用说明关键字段
sys_user统一账号表id、username、password、role、status、avatar、phone、create_time
shop商家信息表id、user_id、shop_name、address、notice、status、rating、delivery_fee
category菜品分类表id、shop_id、name、sort、status
dish菜品表id、shop_id、category_id、name、price、image、description、sales、status
cart购物车表id、user_id、dish_id、dish_name、price、number、create_time
address收货地址表id、user_id、receiver、phone、detail、is_default
orders订单表id、order_no、user_id、shop_id、amount、status、pay_method、address_id、receiver、phone、remark、created_at
order_detail订单明细表id、order_id、dish_id、dish_name、dish_image、price、number、subtotal
comment评价表id、order_id、user_id、dish_id、content、rating、create_time

这里有一个非常关键的设计细节:订单明细表里的dish_name、dish_image、price都是冗余字段。为什么要冗余?因为菜品价格和名称是会变的,商家改价之后,历史订单里的价格不能被跟着改掉,否则对账就会对不上。这是外卖系统里最常见的业务陷阱,也是面试官很喜欢追问的点。

再看订单表,amount字段存的是“最终应付金额”,用BigDecimal类型,绝对不能用double或float。很多初学者在这里踩坑:0.1加0.2在二进制浮点数里会变成0.30000000000000004,算到金额上就是事故。BigDecimal的精度可控,配合字符串构造入参,才能保证钱不出错。

购物车表也做了考究,把dish_name和price冗余进去了,因为用户加购那一刻看到的价格就是下单时的参考价,后面菜品调价不影响购物车展示。但真正下单时,系统会重新从dish表取最新价格计算总价,购物车价格只做展示用,防止前端改价格作弊。

3. 开发环境搭建与Spring Boot项目初始化

3.1 环境准备:这些版本搭配最省心

我实际调试过这套项目,把最稳定的环境版本列在这里,你照着配能少踩很多坑。

JDK用1.8,不要一上来就装JDK 17或21,虽然更高版本能编译运行,但源码里的很多依赖和插件在旧版配置下会报“不支持发行版本”的错误,排查起来非常费时间。Maven用3.6.3或3.8.x都行,注意配置阿里云镜像,否则第一次加载Spring Boot依赖能等十分钟以上。

MySQL用5.7或者8.0都可以,我推荐8.0,因为8.0的驱动类名是com.mysql.cj.jdbc.Driver,性能更好,对UTF-8的支持也更完善。Redis建议用Windows版或者Docker方式安装,只要能启动默认端口6379就行,项目里对Redis的依赖不深,不需要额外配置密码。

IDEA方面,确保安装了Lombok插件。项目源码里大量使用了@Data、@Slf4j这类注解,没有插件的话实体类会疯狂报红色错误,但这并不是代码问题,是开发工具没装全。

3.2 初始化配置:application.yml里的那些门道

拿到源码后,第一件事就是改配置文件。核心配置集中在src/main/resources/application.yml,下面是简化后的关键配置:

server: port: 8888 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/bear_order?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai username: root password: 你的数据库密码 redis: host: localhost port: 6379 database: 0 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 jwt: secret: bear-order-secret-key-please-change-in-production expire: 604800

这里每一行都值得说明。serverTimezone=Asia/Shanghai是MySQL 8.0连接必须加的时区参数,不加会报The server time zone value错误;characterEncoding=UTF-8专门解决中文乱码;map-underscore-to-camel-case把数据库的create_time自动映射成Java实体里的createTime,省去大量起别名操作。

logic-delete-field配置是MyBatis Plus的逻辑删除功能,删除菜品时实际执行UPDATE操作把deleted字段改成1,而不是真删。这样设计的好处是历史订单里的菜品引用不会断掉。

JWT密钥和过期时间放在配置文件里,是项目规范的第一步。源码里默认的密钥只适合本地开发,上线前必须换成随机生成的长字符串,否则别人拿到密钥就能伪造登录令牌,这是非常严重的安全隐患。

启动前记得先执行源码sql目录下的bear_order.sql脚本,把数据库表结构和初始化数据导入。初始化数据里包括了默认的管理员账号、几个测试商家和一些测试菜品,方便你登录后立刻看到效果。

4. 核心业务逻辑实现与源码解读

4.1 登录鉴权:JWT无状态方案是怎么落地的

登录模块是这套系统里最值得讲的部分。它解决了“三个端共用一套账号体系”的权限问题,核心思路是:用户输入账号密码,后端校验通过后生成一个签名字符串(Token)返回给前端;前端之后每次请求都在请求头里带上这个Token;后端拦截器对Token做解析,确认身份和角色。

代码结构上分三层。第一层是登录接口,接收用户名和密码,调用UserService查询用户,再用BCrypt算法比对密码,比对通过后生成JWT。第二层是拦截器,继承HandlerInterceptorAdapter,在preHandle方法里读取请求头中的Authorization,解析Token。第三层是WebMvcConfig,注册拦截器并配置放行路径。

拦截器放行路径需要注意,Swagger文档、静态资源、登录注册接口全部要放行,否则前端页面都打不开:

registry.addInterceptor(jwtInterceptor) .addPathPatterns("/**") .excludePathPatterns( "/user/login", "/user/register", "/doc.html", "/webjars/**", "/images/**", "/error" );

解析Token过后,用户信息不能丢,所以项目里用ThreadLocal存储当前登录用户对象。同一线程内所有代码都能通过UserContext.get()拿到当前用户,不仅方便,还能避免在接口里层层传参。但要注意线程复用问题,所以必须提供remove()方法,在afterCompletion里清理ThreadLocal,否则高并发下会出现用户数据串号。

4.2 购物车逻辑:为什么下单前要重新算一遍价格

购物车模块看着简单,实际容易埋坑。它的核心流程是:加购接口先判断这个用户是否已经加过同样的菜品,如果已存在就把数量加一,否则新增一条记录。

下单时,后端不能直接信任前端传过来的总金额。正确做法是拿到购物车里的明细,逐条回到dish表查出最新价格,再重新计算总价。为什么?因为用户从打开购物车页面到点击“下单”按钮之间,商家完全可能修改了菜品价格。如果直接用前端传来的金额,用户可能花旧价格买到新价格的菜,商家亏损,系统也会被人刷单。

订单状态这里建议使用状态机思想,用一个int字段表示:

订单状态数值含义
待支付0订单已创建,未支付
已支付/待接单1支付成功,等待商家处理
已接单/配送中2商家已接单,骑手配送中
已完成3用户确认收货
已取消4超时未支付或用户主动取消

每次状态流转都做校验,比如只有“待支付”状态才能执行支付操作,“已完成”的订单不能又被改成“配送中”。这种状态机写法虽然多几行代码,但能有效防止脏数据。

4.3 并发扣库存:事务加锁怎么保证不超卖

如果外卖项目只是单机演示,库存扣减确实没人较真。但既然要做成项目经验,并发问题一定要处理好。我拿“下单减库存”这个操作展开讲。

最简单也最推荐的做法是乐观锁。dish表加一个version字段,更新库存时带上版本号条件:

UPDATE dish SET stock = stock - #{number}, version = version + 1 WHERE id = #{id} AND stock >= #{number} AND version = #{version}

这条SQL是原子操作,数据库行锁保证了同一时刻只有一个事务能成功更新。如果影响行数为0,说明库存不足或版本冲突,重新查库存提示用户。配合@Transactional注解,下单、扣库存、生成订单明细三步全部在同一个事务里,任何一步失败都会整体回滚,不会出现“订单生成了但库存没扣”的问题。

我在做代码审查时发现一个常见错误:只在Java代码里判断库存够不够,然后直接UPDATE,这样在高并发下会出现超卖。正确姿势是把“判断库存”和“扣减库存”合并到同一条UPDATE语句里完成,数据库的行锁天然帮我们处理了并发竞争,这比用synchronized或者分布式锁都更简单可靠。如果单选题出一个面试题问“高并发扣库存怎么实现”,这套思路拿满分没问题。

下单接口的整体伪代码如下:

@Transactional(rollbackFor = Exception.class) public Result createOrder(OrderDTO dto) { // 1. 校验购物车不为空 // 2. 循环明细,查最新菜品价格,计算总金额 // 3. 插入orders订单主表,状态为待支付 // 4. 循环插入order_detail订单明细表 // 5. 乐观锁扣减每个菜品的库存 // 6. 清空该用户的购物车 // 7. 返回订单号 }

注意@Transactional必须指定rollbackFor = Exception.class,否则默认只在RuntimeException时回滚。业务里常见的受检异常如果直接向上抛出,事务不会回滚,数据就会处于半完成状态,这个细节能拦住一大批新手。

5. 源码目录结构与二次开发指引

5.1 源码结构:每个包是干什么的

拿到源码之后先别急着跑,先把目录结构看明白。这套项目的包路径是com.bear.order,模块划分非常规范:

bear-order/ ├── sql/ │ └── bear_order.sql # 建表脚本和初始化数据 ├── src/main/java/com/bear/order/ │ ├── config/ # 配置类:WebMvc、Redis、MyBatisPlus分页 │ ├── common/ # 通用类:统一返回结果、全局异常处理 │ ├── controller/ # 接口层:UserController、DishController... │ ├── service/ │ │ ├── impl/ # 业务实现类 │ │ └── *.java # 业务接口定义 │ ├── mapper/ # MyBatis Plus的Mapper接口 │ ├── entity/ # 和数据库表对应的实体类 │ ├── interceptor/ # JWT拦截器 │ ├── utils/ # 工具类:JwtUtils、UserContext │ └── BearOrderApplication.java # 启动类 ├── src/main/resources/ │ ├── application.yml # 核心配置文件 │ └── mapper/ # 复杂SQL的XML文件 └── pom.xml

这个分包方式看着普通,但每一层职责非常清晰。Controller只做参数接收和结果返回,不写业务逻辑;Service层处理业务规则;Mapper层和数据库打交道。想加一个新功能,照着现有代码复制一份改一改,五分钟就能搞定。

5.2 带你二次开发:加一个“今日推荐”功能

用一个实际需求演示怎么在现有代码上做扩展。假设商家想让部分菜品出现在首页“今日推荐”位置,我们需要做三件事:

第一步,数据库dish表加一个recommend字段,类型是tinyint,默认0,1表示推荐。第二步,实体类Dish加一个recommend属性。第三步,在DishController里加一个查询接口:

@GetMapping("/recommend") public Result<List<Dish>> getRecommendDishes() { LambdaQueryWrapper<Dish> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Dish::getStatus, 1) .eq(Dish::getRecommend, 1) .last("LIMIT 8"); return Result.success(dishService.list(wrapper)); }

三条字段加一个接口,推荐功能就上线了。这就是分层清晰带来的好处,代码的扩展性完全取决于前期的结构设计。你在简历项目里写“支持快速扩展新功能”,指的就是这种场景下的操作效率。

6. 常见问题排查与答辩经验

6.1 新手最容易遇到的五个报错

我按出现频率从高到低整理了一份避坑清单,都是这个项目里真实会踩的问题。

项目启动直接报连接超时。排查思路是:先看MySQL服务有没有启动,再看账号密码是否正确,再看数据库bear_order是否存在。本地开发建议直接用一个数据库可视化工具(如Navicat)测试连接,能连上再启动项目,隔离问题。

启动时Redis连接失败。项目用了Redis就要有服务在跑,本地开发可以在命令行输入redis-server启动。如果你没安装Redis,又不想装,可以暂时把项目中所有RedisTemplate相关代码注释掉,但我不推荐这么做,缓存模块也是项目的亮点,面试官问起来你没有实际运行过会很尴尬。

接口返回中文全是问号。这是URL连接参数少了characterEncoding=UTF-8,按前面配置文件补上即可。另一个可能原因是数据库表本身的编码集不是utf8mb4,建表时注意默认字符集。

菜品删除报外键约束失败。order_detail表里引用了菜品ID,直接删除菜品会破坏引用完整性。项目用逻辑删除规避这个问题,所以业务上不要调用物理删除SQL,而是设置deleted字段。

页面能打开但接口返回401。说明JWT拦截器生效了,但请求头里没带Token。用Swagger调试时要先调用登录接口拿到Token,然后在全局参数里配好Authorization: Bearer前缀,再访问业务接口。

6.2 简历和面试:这些追问点提前准备

项目写进简历,就要做好被问的准备。我挑几个高频追问点帮大家梳理一下。

Spring Boot自动配置的原理是必问题。一句话概括:启动类上的@SpringBootApplication组合了@EnableAutoConfiguration,后者通过META-INF/spring.factories或者AutoConfiguration.imports文件加载所有候选自动配置类,再配合@ConditionalOnClass、@ConditionalOnMissingBean等条件注解按需装配。能把这个链路讲清楚,面试官就知道你是真的看过源码。

JWT和Session的区别也是必问。Session存在服务端,多台机器需要共享Session存储;JWT把用户信息签名后存在客户端,服务端无状态,天然适合前后端分离和分布式部署。但JWT也有缺点,没法主动失效,所以过期时间不能设置太长。

项目里最大的难点是什么。别回答“没有难点”,要挑真问题讲。可以讲并发扣库存用乐观锁解决,也可以讲金额精度用BigDecimal处理,或者讲订单状态机的流转设计。重点不是技术多高深,而是你有完整的分析过程。

还有一个容易被追问的点:MyBatis Plus分页原理。它内部通过分页插件拦截器,在Executor执行前拦截SQL,改写为带LIMIT的语句,同时执行一条COUNT查询获取总数。这个知识点虽然不难,但能答上来会显得你不仅仅停留在API调用层面。

7. 部署上线与真实开发体会

本地跑通只是第一步,项目要完整展示给别人看,最好部署到服务器上。最轻量级的部署方案是:服务器装JDK 8、MySQL、Redis,把项目打成jar包,用java -jar bear-order.jar启动。如果想关闭命令行窗口后服务不中断,用nohup命令放到后台运行:

nohup java -jar bear-order.jar --spring.profiles.active=prod > bear-order.log 2>&1 &

上线前必须改两处配置:数据库密码不要用弱密码,JWT密钥换成随机长字符串。另外生产环境建议把MyBatis Plus的SQL日志关掉,否则每个请求的SQL都打到日志文件里,磁盘很快会被撑满。

说到这里,我想起一个真实翻车经历。有一次我在本地开发时,图方便用double类型存金额,结果一个订单下了三份同样的菜,明细里每条单价都对,但合计金额比正确值少了0.01元。排查了大半天,最后发现是浮点数二进制转换的精度问题。自那以后,凡是涉及金额、库存、数量这类精确计算的字段,一律用BigDecimal或者整数(单位换算成分)。这套小熊外卖的源码里全部用的是BigDecimal,希望大家以后写代码也能避免这个坑。

项目里还可以扩展的地方非常多,比如接入微信支付真实支付、引入RabbitMQ做订单超时自动取消、用WebSocket实时推送订单状态给商家,这些都是已经想好但没有在基础版本里实现的亮点。

最后再分享一个小技巧:源码项目最忌讳“一下全看”。建议你按照“启动跑通 → 走一遍用户下单流程 → 看订单表数据变化 → 再看对应代码”的顺序去读。从数据倒推代码逻辑,比对着源码一行行读效率高一倍,而且能真正理解每个接口存在的意义。这套系统我调试过多次,整体代码质量在同类项目中属于上乘,跑通一遍之后,你对Spring Boot项目的理解会上一个台阶。

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

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

立即咨询