资讯动态

Spring Boot外卖点餐系统实战:从技术选型到部署上线全解析

发布时间:2026/9/10 17:55:45 来源:尧图企业网站定制
做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 PlusRedis做缓存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_timeshop商家信息表id、user_id、shop_name、address、notice、status、rating、delivery_feecategory菜品分类表id、shop_id、name、sort、statusdish菜品表id、shop_id、category_id、name、price、image、description、sales、statuscart购物车表id、user_id、dish_id、dish_name、price、number、create_timeaddress收货地址表id、user_id、receiver、phone、detail、is_defaultorders订单表id、order_no、user_id、shop_id、amount、status、pay_method、address_id、receiver、phone、remark、created_atorder_detail订单明细表id、order_id、dish_id、dish_name、dish_image、price、number、subtotalcomment评价表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?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/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这里每一行都值得说明。serverTimezoneAsia/Shanghai是MySQL 8.0连接必须加的时区参数不加会报The server time zone value错误characterEncodingUTF-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默认01表示推荐。第二步实体类Dish加一个recommend属性。第三步在DishController里加一个查询接口GetMapping(/recommend) public ResultListDish getRecommendDishes() { LambdaQueryWrapperDish 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连接参数少了characterEncodingUTF-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.activeprod bear-order.log 21 上线前必须改两处配置数据库密码不要用弱密码JWT密钥换成随机长字符串。另外生产环境建议把MyBatis Plus的SQL日志关掉否则每个请求的SQL都打到日志文件里磁盘很快会被撑满。说到这里我想起一个真实翻车经历。有一次我在本地开发时图方便用double类型存金额结果一个订单下了三份同样的菜明细里每条单价都对但合计金额比正确值少了0.01元。排查了大半天最后发现是浮点数二进制转换的精度问题。自那以后凡是涉及金额、库存、数量这类精确计算的字段一律用BigDecimal或者整数单位换算成分。这套小熊外卖的源码里全部用的是BigDecimal希望大家以后写代码也能避免这个坑。项目里还可以扩展的地方非常多比如接入微信支付真实支付、引入RabbitMQ做订单超时自动取消、用WebSocket实时推送订单状态给商家这些都是已经想好但没有在基础版本里实现的亮点。最后再分享一个小技巧源码项目最忌讳“一下全看”。建议你按照“启动跑通 → 走一遍用户下单流程 → 看订单表数据变化 → 再看对应代码”的顺序去读。从数据倒推代码逻辑比对着源码一行行读效率高一倍而且能真正理解每个接口存在的意义。这套系统我调试过多次整体代码质量在同类项目中属于上乘跑通一遍之后你对Spring Boot项目的理解会上一个台阶。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价