简介:面向JavaWeb初学者的苍穹外卖项目完整学习包,内含作者历时一个月开发的在线订餐系统源码与配套笔记。项目实践了Servlet/JSP、MVC分层、Spring依赖注入、MyBatis持久化、数据库表设计与JOIN查询、前端Bootstrap交互、文件上传、Session权限控制、SQL注入防御、JUnit测试及Tomcat部署等关键技术,能帮助学习者串联JavaWeb全栈开发流程。压缩包共2000个文件,以java源码、xml配置、class编译产物和yml配置为主,并有md学习笔记和xlsx数据结构表,整体13.61MB,目录结构符合Maven规范,便于按模块阅读。目前已有16320人浏览学习。通过源码与笔记对照,可快速理解外卖业务从用户点餐到订单管理、菜品图片上传等完整实现细节,适合作为毕业设计或进阶练习。 第一次完整地把苍穹外卖跑通的时候,我盯着控制台里“Started Application”那行日志看了很久。一个月的每天晚上和周末,断断续续地写接口、调联调、改Bug,终于把这套外卖系统完整落地了。刚开始学Java后端的时候,我其实挺迷茫的,SSM学了、Spring Boot也会写个简单CRUD,但真正面对一个有用户端、管理端、订单流转、支付回调的项目时,还是有点手足无措。这也正是我决定把苍穹外卖彻底写一遍的原因——它不是什么“玩具项目”,而是一套接近于真实商业场景的完整系统。
这篇博文就把我这一个月的实战笔记和完整代码分享出来,包括我梳理清楚的技术架构、每一步的实现逻辑,以及那些调试到半夜才解决的坑。如果你正在学Spring Boot + MyBatis + Redis这套组合,或者准备做一个完整项目来充实简历,这套代码和笔记应该能帮你省下不少摸索的时间。全文干货,可以直接照着自己敲一遍。
1. 苍穹外卖这个项目到底练了什么
1.1 从“会写接口”到“会做系统”的跨越
我见过很多人学Spring Boot的方式是照着教程写一个“增删改查”的Demo,然后觉得自己已经会了。但要你独立实现一个带订单状态流转、购物车、地址簿、来单语音提醒的外卖系统时,才会发现真正的复杂度不在某个单一接口,而在不同模块之间的数据一致性和流程衔接。
苍穹外卖的核心业务其实和美团外卖、饿了么很相似:用户在微信小程序上下单,商家在管理后台接单、派单、统计营收。作为学习者,我最看重的就是它完整覆盖了下面这几个场景:
- 用户端:微信登录、浏览菜品、购物车、下单支付、订单查询、历史订单
- 管理端:员工登录、菜品管理、分类管理、套餐管理、订单管理、数据统计
- 共性支撑:JWT身份认证、Redis缓存、定时任务、WebSocket即时推送、微信支付模拟
每个模块单独拎出来都不算难,但要把它们组装成一个能跑通的完整系统,牵扯到的知识面一下子就宽了。这恰恰是苍穹外卖最有价值的地方——它逼着你去思考模块之间的关系,而不是只做一个孤立的功能点。
1.2 一个月的时间节奏我是怎么安排的
写这个项目之前,我的Java基础已经比较扎实,Spring Boot基本用法也没问题,但缓存、消息推送、定时任务这些都是零基础。整个开发周期大致分成了四个阶段:
- 第1周:梳理需求文档和数据库设计,完成员工端登录、JWT拦截器、通用返回结果封装
- 第2周:完成分类、菜品、套餐的增删改查,练习MyBatis动态SQL和文件上传
- 第3周:集中攻坚用户端最核心的交易链路,包括购物车、下单、支付模拟、订单状态流转
- 第4周:做数据统计报表、WebSocket来单提醒、清理Bug和优化代码结构
这个节奏下来,平均每天投入三到四个小时,周末会多一些。如果你白天上班或者课比较多,两周完成核心功能也是可行的,前提是不要把时间浪费在无意义的返工上,比如数据库字段没设计好导致后面疯狂改表。
2. 技术选型的逻辑和整体架构思路
2.1 为什么选了这套技术组合
苍穹外卖的主流技术栈是Spring Boot作为基础框架、MyBatis负责持久层、MySQL存数据、Redis做缓存,前端用微信小程序配合管理后台的Vue页面。这套组合并不是随便选的,它基本上就是当前中小型互联网公司后端团队最常用的标配。
Spring Boot的好处大家都知道,自动配置、内嵌Tomcat、生态成熟,但它真正让你省心的是大量约定优于配置的设计,能让你把注意力集中在业务实现而不是环境搭建上。MyBatis作为持久层框架,轻量灵活,SQL由自己掌控,面对外卖这种查询条件复杂、关联表很多的场景非常好调整。Redis的应用放在这里也很典型,菜品缓存、购物车数据、微信登录Token的临时存储,这些都是高并发场景下绕不开的需求。
WebSocket和定时任务的引入,是我觉得这个项目比较有良心的设计。实际外卖系统里,用户下单后商家需要实时听到语音提醒,这就要靠WebSocket推送;店铺打烊后系统要自动清理过期订单,这就要靠Spring自带的定时任务。这些功能往简历上一写,含金量立刻不一样。
2.2 项目分层与目录结构设计
拿到项目需求后,我没有急着去写代码,而是先把目录结构定下来。我采用的是前后端分离的标准分层架构,后端代码按controller - service - mapper - entity/dto/vo的层次组织。
- controller层:负责接收请求、参数校验、调用service,不关心具体业务逻辑
- service层:核心业务逻辑都在这一层,包括事务控制
- mapper层:只负责数据库交互,SQL写在XML里,便于后期优化
- entity:数据库表对应的实体类
- dto:接收前端传入的参数对象,避免直接拿实体类去接参数
- vo:返回给前端的数据对象,根据页面需要灵活组装
这种分层方式在项目初期看起来很“多此一举”,多写了很多VO和DTO类。但当你做到订单管理模块的时候就会发现,如果没有这些层隔离,一个接口可能牵扯到六七张表的数据,代码会变成一团乱麻。
3. 核心功能模块的实现细节
3.1 员工端身份认证与拦截器设计
管理员登录这里用的是JWT(JSON Web Token)方案。用户输入账号密码后,后端校验通过会生成一个有效期数小时的Token返回给前端,之后前端每次请求都在请求头带上这个Token,后端通过拦截器统一校验。
生成Token的逻辑主要是用jjwt库,把员工ID和用户名放入claims,用密钥签名生成字符串。关键点在于密钥一定要放在配置文件里统一管理,不要硬编码在代码中,否则项目代码一旦泄露攻击者就可以随意伪造身份。
拦截器这里有个细节值得注意:需要放行登录接口本身,不然会出现还没登录就被拦下来要求登录的死循环。我在WebMvc配置类里做了路径白名单配置,比如/admin/employee/login、/user/user/login这些接口需要跳过Token校验,其余接口全部拦截。这里顺便把CORS跨域配置也一起处理了,前后端分离开发时跨域问题避免不了。
同时,我还封装了一个BaseContext工具类,用ThreadLocal存储当前登录用户ID。这样在Service层随时都能获取是谁在操作,做数据审计或者逻辑判断都很方便。用完之后一定要记得调用remove()方法清理,否则Tomcat线程池复用的时候会出现用户数据串号的问题。这个坑我实际遇到过,排查了很久才定位到。
3.2 菜品与分类管理,以及文件上传处理逻辑
分类管理相对简单,就是一个普通的CRUD,但需要注意分类被菜品关联后能不能删除的逻辑判断。我通过查询菜品表来判断当前分类下是否已有菜品,如果存在就抛出业务异常,提示“当前分类下有菜品,不能删除”。这种业务约束必须在后端做,不能只靠前端控制。
菜品管理涉及多表关联,一个菜品对应多个口味,实际表结构是菜品表和口味表分开存储。新增菜品时需要在事务内同时插入菜品主表和口味明细表,删除菜品时也要把对应的口味数据一并删除。事务注解@Transactional在Service方法上一定要加,否则中途失败会留下脏数据。
文件上传这个功能我一开始以为要接阿里云OSS,后来发现项目里用本地存储就够了。前端把图片传到后端,后端用UUID生成新文件名,按照日期分目录存储到磁盘上,再把访问路径返回给前端。真正部署的时候还要做静态资源映射,让外部能够访问到这些图片。如果你以后开发真实项目,建议直接换成对象存储,本地存储方式只适合学习和中小型内网项目。
3.3 用户下单闭环:购物车、订单与缓存联动
用户端最核心的一条链路是:用户加购 -> 提交订单 -> 支付模拟 -> 商家接单 -> 用户查询订单状态。
购物车数据我存在了Redis里,使用当前用户的ID作为Key的一部分,value用Hash结构,包含菜品ID、数量、口味等信息。这么做的好处是购物车天然适合用缓存来做,用户随时可能修改数量,写MySQL反而拖慢响应速度。但要注意,Redis里的购物车数据在用户下单完成后需要清除,否则会出现订单提交成功但购物车还残留数据的情况。
订单表是整个系统里字段最多、逻辑最复杂的表,包含订单号、用户ID、地址ID、订单金额、支付状态、订单状态、下单时间等十几个字段。我的做法是先创建订单主表记录,再批量插入订单明细表,两个操作放在同一个事务里。订单号我采用时间戳加上随机数的方式生成,避免了多用户同时下单时的重复概率。
支付功能在真实项目中要对接微信支付SDK,学习项目里我们不可能真实支付,所以用了一个模拟支付的接口:前端支付时传一个订单号,后端直接更新订单状态为“已支付”。这个设计不影响理解订单状态流转的逻辑,如果你以后对接真实支付,只需要替换掉这个模拟接口,并在支付回调里处理状态更新即可。
3.4 WebSocket来单提醒与定时任务处理
这功能设计上是商家端收到新订单时,WebSocket推送消息提醒商家“您有新的订单待处理”。我的实现思路是:用户支付成功后,在service层通过WebSocket对象向服务端推送一条新订单消息,商家管理后台的前端页面通过WebSocket实时接收,并弹出提示音和消息框。
WebSocket的原理其实不复杂,它建立了一条客户端和服务器之间的长连接通道,服务端可以主动给客户端发消息。我在项目里写了一个WebSocketServer类,用@ServerEndpoint注解声明WebSocket端点,维护一个静态的会话集合。注意这里的线程安全问题,多个用户同时建立连接时,用线程安全的Map来存放Session对象。
定时任务用的是Spring自带的@Scheduled注解,我在配置类上加了@EnableScheduling开启定时任务功能。实际场景是商家长时间不接单,订单要自动取消并回滚库存。我对超过15分钟未支付的订单做超时处理,定时任务每分钟扫描一次订单表,把超时未支付订单取消释放库存。这里使用cron表达式时要注意时区问题,我调试的时候发现时间相差8个小时,后来给服务器指定了正确的时区才解决。
4. 项目中遇到的典型问题与排查过程
4.1 明明配置了跨域为什么还是报错
开发用户端小程序时遇到一个很典型的报错:请求发送后前端提示跨域。我在WebMvcConfigurer中配置了CORS映射,但接口还是访问不通。查了很久才发现问题出在我自定义的JWT拦截器上——拦截器执行顺序在CORS处理之前,请求预检阶段(OPTIONS请求)直接被拦截器拦住了,后端根本没走到跨域处理的环节。
解决办法是在拦截器中加入判断,如果是OPTIONS请求就直接放行,让CORS过滤器来处理跨域。这一步不加的话,你配置再多的跨域规则都没有用。类似的坑在网上很多人踩过,如果你也遇到跨域问题,先从请求方法判断入手排查。
4.2 MyBatis动态SQL一个分号引发的血案
菜品分页查询时,我需要根据分类ID、名称、状态这些条件动态拼接SQL。用<if>标签写动态条件本身没问题,但我在SQL语句结尾多加了一个分号,结果MyBatis直接报语法异常。排查过程其实很简单,把控制台打印的SQL语句复制到Navicat里执行一遍就能发现问题。这里也提醒各位,配置mybatis-plus(或者原生MyBatis)时建议打开SQL日志打印,调试效率能翻好几倍。
分页查询我用的PageHelper插件,使用时有一个潜在的坑:PageHelper只能生效一次,而且必须在查询语句之前设置分页参数。如果代码逻辑里分页设置和查询语句中间还隔着别的SQL操作,分页参数有可能会被下一次查询消费掉,导致数据不对。我的经验是把分页参数设置和紧随其后的查询语句写在一起,别中间插入其他数据库操作。
4.3 微信登录模式下的用户身份识别
用户端的登录我用的是微信登录的模拟实现:前端传入一个code,后端调用微信接口服务换取openid,如果查不到这个用户就自动注册,查询得到就直接登录。学习项目没有真实微信AppID,所以用一个mock接口模拟登录换取openid的过程。
这个看似简单的逻辑有个细节:如果用户第一次登录后查不到记录,需要自动注册,那么注册时要注意唯一约束。我给用户表的openid字段加上了唯一索引,防止重复注册。同时登录成功后也要生成JWT Token返回给前端,和员工端登录的逻辑一样。但用户端和员工端的Token解析密钥要分开配置,防止用户伪造管理员身份访问管理端接口,这个安全细节很容易被忽略。
4.4 MySQL时区导致的时间数据混乱
数据统计模块有一个很隐蔽的Bug:统计营业额时,某一天的数据总是不对,少了几笔订单。后来我把SQL语句单独拿出来执行才发现,between的时间范围条件判断出了问题——我传入的是北京时间,但MySQL连接配置的时区是UTC,时间被自动转换后少了8个小时。
解决办法是在数据库连接URL上显式加上serverTimezone=Asia/Shanghai参数,确保Java时间和数据库时间处在同一时区。另外我建议你统一在Service层处理业务时间的传入,而不是在SQL里用NOW()函数,这样在代码中调试起来更加可控。
5. 关于笔记和代码,我怎么给你做成了可以直接复用的资料
5.1 代码仓库里包含了什么
我把整个项目代码做了完整的梳理和注释,主要包含以下几个部分:
- 完整的后端源码,按照
sky-pojo、sky-server、sky-common拆分成多个模块,其中sky-common放通用工具类、sky-pojo放实体和DTO/VO、sky-server放Controller和Service等业务代码 - 数据库初始化脚本,建表语句加部分测试数据一起给了,拿到就能直接用
- 接口文档说明,标注了每个接口的请求方式、参数说明、返回示例
- 前端管理后台的静态资源文件,配合后端做联调测试
每个核心模块的代码我都加了比较详细的注释,特别是一些容易踩坑的地方,比如拦截器的放行规则、事物方法调用的注意事项、WebSocket的线程安全管理,在代码注释里强调了一下,方便你边看边理解。
5.2 怎么高效地利用这套内容,而不是简单地fork
代码不是拿来跑起来就算学会了,我建议你按照下面这个顺序去用:
- 先花半天时间把数据库的每一张表过一遍,搞明白字段之间的关联关系,尤其是订单表、订单明细表、菜品表之间的关系
- 不看写好的代码,自己先试着把员工登录、菜品分页查询这两个接口写出来,写不出来再看答案,这时候印象最深
- 把项目跑起来之后,配合接口文档逐个测试核心接口,观察Redis缓存的变化、数据库数据的变化,建立起数据的“流动感”
- 拿到代码后着重看三处:拦截器与JWT的处理、下单事务的完整流程、WebSocket推送的整个过程。这三个地方能看懂,这个项目的价值你就吸收了一大半
- 最后选一个模块做重构,比如把订单超时处理改成使用延迟队列,理论上会更好,你可以动手试试
5.3 如果你想进一步挑战,可以把项目扩展成什么样
如果你完成苍穹外卖的基础功能后觉得还有余力,可以试试这几个方向:
- 引入Spring Cloud Alibaba的Nacos注册中心和OpenFeign远程调用,把项目改造成微服务架构
- 使用RabbitMQ延迟队列替换定时任务处理超时订单,更贴近真实的高并发处理模型
- 给数据统计模块接入ECharts图表展示,丰富可视化效果
- 将本地文件存储迁移到MinIO对象存储,提前熟悉私有化部署的对象存储方案
我在这个项目结束之后,坚持把核心模块重构了一遍,才感觉到自己对Spring Boot和MyBatis的理解上了一个台阶。之前那些“学过但不会用”的知识点,因为项目而真正串了起来。记住,项目重要的不是“做完”的那一瞬间,而是你写的过程中踩过多少坑,又爬出来多少次。这些真实的经历,比启动成功那一刻的兴奋有用得多,面试时讲项目也能讲得更有底气。
本文还有配套的精品资源,点击获取