接住这道送上门的Java全栈毕业题:苍穹外卖,从零部署到上线的实战复盘
如果你正在准备Java后端面试,或者想在简历里写一个有分量的全栈项目,那《苍穹外卖》这个名字你大概率刷到过。我去年带团队招人时,十份简历里至少有四份写了这个项目,但真正能讲清楚“微信小程序是怎么跟Spring Boot后端通信的”“Redis到底缓存在哪里”的人,不超过两个。这也说明一个事实:项目本身是好的,但大多数人都停留在“跟着视频敲一遍”的阶段,没有真正吃透它。
这篇文章我想换个角度,不跟你从头到尾复述代码,而是把这个教学项目当作一个真正的“产品”来拆解:为什么它适合做前后端分离的练手项目、用户端和管理端各自的设计逻辑是什么、微信登录那条链路到底怎么走通、以及最后是怎么一步步部署到云服务器上的。无论你是准备秋招找工作的学生,还是想补全全栈能力地图的初级开发,这篇文章都会给你一些“文档之外”的判断和实操细节。
1. 项目整体设计与思路拆解
1.1 为什么说“苍穹外卖”是一个被低估的业务模型
很多初学者看这个项目,第一反应是“这不就是个点餐系统吗,有什么稀罕的”。但如果你把它放到“用来理解真实企业级开发”的坐标系里看,它其实是一个非常典型的全链路业务系统:有面向C端用户的微信小程序,有面向B端商家的管理后台,有完整的上架、下单、支付、接单、配送状态流转,还有营业数据统计。
做后端开发的人最怕什么?最怕只写CRUD,没有业务逻辑。而苍穹外卖最妙的地方就在于,它的核心业务线——“用户浏览菜品 -> 加购物车 -> 下单 -> 支付 -> 商家接单 -> 配送完成”——天然需要你在代码里贯穿“状态机”的思想。订单状态不是随便改的,待支付不能直接跳到已完成,每个状态的流转都有业务约束。这种约束一旦在面试时被问起来,你就能讲出“为什么要在Service层做状态校验,而不是单纯靠前端按钮控制”,这就是面试官想听的深度。
另一个被低估的点是它的角色划分。同一个系统里至少要区分三类人:微信小程序里的普通用户、管理后台里的商家员工、以及系统管理员。不同角色看到的菜单不同、能调用的接口不同、操作权限也不同。这逼着你从一开始就要考虑JWT Token里该放什么信息、拦截器怎么区分用户端和管理端、接口的权限注解怎么设计。这些思考,是你在做那种“单角色管理系统”时完全体会不到的。
1.2 前后端分离的核心思路:为什么非要把两端拆开
项目标题里专门强调了“前后端分离”,这个点很多人只是听着觉得高级,但并没有真正理解它解决了什么问题。我举个最直白的例子:在没有前后端分离的年代,页面是服务端渲染的,用户点一个按钮,浏览器要向服务器请求一整个新的HTML页面,服务器端得同时懂Java和前端JSP,项目一大了前后端代码揉在一起,每人敢动。
而前后端分离之后,后端只提供JSON格式的API接口,页面渲染、交互逻辑全部交给前端(管理端Vue或用户端微信小程序)自己处理。这样做最大的收益不是“技术新潮”,而是前后端可以并行开发、独立部署。前端团队和后端团队只需要约定好接口文档(OpenAPI/Swagger),就可以各干各的,后端改接口逻辑不影响前端页面,前端改样式也不要求后端重新打包。
放到苍穹外卖这个具体项目里,效果更直观。同一套后端服务,要同时服务两个前端:一个是Vue写的中后台页面,跑在PC浏览器上;一个是微信小程序原生代码,跑在微信里。如果不用前后端分离,你要么写两套后端渲染逻辑,要么把页面逻辑硬塞给小程序的WXML,怎么想都是灾难。而采用前后端分离后,后端只负责输出数据,两个前端各自消费同一份API,工作量直接从加法变成乘法级的缩减。
1.3 技术选型的逻辑:为什么Spring Boot + Redis + MyBatis是黄金组合
苍穹外卖的技术栈非常“标准”,标准到你可以把它当成Java开发岗位的“最小公约数”:Spring Boot做基础框架、MyBatis操作MySQL数据库、Redis做缓存、Knife4j生成接口文档、阿里云OSS存图片,用户端通过微信登录授权。这套组合在真实中小型公司里极其普遍,所以学好这个项目,你进去不会觉得“学校教的和公司用的完全是两回事”。
选Spring Boot不用多说,它把繁琐的XML配置全部简化成了自动配置和注解,让开发者把精力聚焦在业务逻辑上。但我要特别说一句:Spring Boot简单是简单,你千万别只停留在“会用注解”的层面。面试时被问到Spring Boot底层原理,你要是答不上Spring容器的启动流程、自动装配到底是靠哪个注解触发的,项目做得再完整也白搭。
选Redis做缓存,最主要原因是外卖业务的“读多写少”特性。用户打开小程序,第一件事就是看菜品列表,这个列表在短时间内是几乎不变的。如果每次都去查数据库,数据库的压力会很大。把菜品数据缓存到Redis里,第一次查询时查库并写入缓存,后续请求直接命中Redis,响应时间能从几十毫秒降到几毫秒。为了应对“商家改了菜品信息但缓存还没更新”的问题,项目里用了Spring Cache的@CacheEvict注解,在增删改菜品时同步删除缓存,保证下次查询能拿到最新数据。
至于MyBatis,它的核心优势是让SQL对你的代码“可见”。相比JPA那种“你都不知道框架帮你生成了什么SQL”的黑盒感觉,MyBatis让你自己控制SQL,出现问题可以直接把SQL复制到Navicat里执行排查,对于刚接触企业开发的人来说,这种“看得见摸得着”的掌控感特别重要。
1.4 项目的目录结构与模块划分:如何做到“不迷路”
我在带新人时经常强调一句话:看一个项目,先看目录结构,再看数据库表设计,最后才看代码逻辑。苍穹外卖在目录设计上有一个很清晰的逻辑——按业务功能分包,再按技术职责细分。
- controller:接收前端请求,做参数解析,不写业务逻辑
- service:业务逻辑的核心层,处理订单流转、购物车计算等关键操作
- mapper:数据访问层,定义接口,SQL写在XML文件里
- entity:实体类,对应数据库表结构
- dto:数据传输对象,承载前端传来的参数,例如UserLoginDTO
- vo:视图对象,用于返回给前端展示的数据,例如OrderVO
你可能会疑惑,entity和dto、vo长得几乎一样,为什么还要分三个?这里的道理是解耦。数据库的实体类是最稳定的,不能因为前端想多传一个参数就改它;DTO是针对每个接口的“入参模型”,不同接口对同一实体的字段需求不同;VO是针对每个接口的“出参模型”,比如返回订单详情时你不想把数据库里的createTime原始时间戳直接抛给前端,你可以格式化成字符串再放到VO里。想清楚这一层,你在面试时讲“为什么设计DTO”就不会只说“规范”两个字,而是能讲出实实在在的好处。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心功能拆解与实操要点
2.1 微信登录:那段最容易被复制粘贴但没走通的“code换openid”逻辑
微信小程序登录,是每一个做小程序开发的人必须跨过的第一道坎。很多教程会引导你创建一个“wx.login”按钮,前端拿到一个code(临时凭证)后传给后端,后端拿着code去微信的接口换openid和session_key。逻辑听起来很简单,但实操时问题特别多。
我在用这个项目教学时,最多被问到的报错是“code无效”和“获取用户信息失败”。前者多半是因为前端拿到code之后没有及时传给后端,因为code的有效期只有5分钟,而且只能使用一次。后者则是因为微信在2021年后调整了用户隐私政策,原来那种直接调wx.getUserInfo弹窗授权的方式已经被收紧了,现在必须在页面上通过button的open-type="chooseAvatar"来引导用户主动授权头像和昵称。
回到后端设计,苍穹外卖的登录实现思路值得你长期复用:后端接收前端传过来的code,调用微信官方接口 https://api.weixin.qq.com/sns/jscode2session(传appid和secret),拿到用户唯一标识openid。注意,这里有个很多初学者不知道的坑:后端绝不能把secret暴露给前端,必须放在后端配置文件里。拿到openid后,业务逻辑只有一句话:“查一下这个openid在不在user表里,不在就注册一个新用户,在就说明是老用户”,紧接着生成JWT Token返回给前端。
这个流程你写代码可能只需要二三十行,但面试官能问出花来:openid为什么不能当Token用?(因为它是固定不变的,一旦泄露等于别人永远能冒充你)Token的有效期设多长合适?(外卖场景建议7天,因为用户不想频繁登录)登录成功后Redis里存了什么?(通常存一份用户信息缓存,减少下次查询数据库的开销)你把这些想明白,才算真正把登录模块吃透了。
2.2 购物车与下单流程的幂等设计:最考验后端逻辑的一环
很多项目里购物车就是简单的“增删改查”,但在苍穹外卖里,购物车涉及一个很重要的概念叫“幂等性”。简单说就是:一个操作无论执行多少次,结果都应该是一样的。放在购物车场景里,用户疯狂点了三次“添加到购物车”,最终购物车里应该只有一条对应数量的记录,而不是三条雷同记录。
要实现这个效果,代码里不能只做简单的insert,而是要先查一下:这个用户id + 菜品id 的组合是否已经存在于购物车表中。如果存在,就在原有数量上加1;如果不存在,才新增一条记录。你看,逻辑不复杂,但如果你没意识到要这么做,就会出现用户体验极差的重复数据。
下单流程比购物车更考验设计。用户从购物车点击结算,前端把选中的菜品列表和一个“备注”传给后端,后端要做的动作是:验库存、计算总金额、创建主订单、创建订单明细(一个订单里可能有多个菜品)、清空购物车。这五个动作不能有任何一步失败后留下“半截数据”,所以必须用 @Transactional事务注解把它们包在同一个事务里,保证要么全部成功,要么全部回滚。
我在实际项目里见过有人把“清空购物车”这个操作放在前端做,想着用户支付成功后再调接口清空,结果订单明明创建了,购物车却没清掉,用户第二次点结算又生成了重复订单。所以强调一句:所有状态变更的请求,都应该是后端控制的,前端只管发起请求和接收响应。
2.3 缓存设计:Redis在什么时机缓存什么,什么时候又必须失效
苍穹外卖最典型的缓存场景是“菜品信息”。用户端浏览分类时,需要查询当前分类下所有启售菜品,这个数据变化频率不高,非常适合缓存。项目里用了Spring Cache的@Cacheable注解,第一次查询时命中数据库,然后框架自动把返回值存入Redis;后续请求直接返回缓存,不再穿透到数据库。
但是缓存最怕的另一个问题叫“数据一致性”。商家在管理后台把菜品的价格从30块改成了25块,如果Redis里还存着旧价格,用户端看到的就还是30块,这就闹大笑话了。所以项目在管理端的菜品修改、删除、上下架操作上,加了@CacheEvict注解,执行成功后会按指定的缓存key把对应数据清掉。下次用户再查时,缓存里没有数据,就会重新查库并回填。
这里我想额外提醒你一个实践中的细节:缓存key的设计要有规范。苍穹外卖里的缓存key通常是“dish_分类id”这种格式,方便你制定失效策略。在真实项目中,如果缓存key过于随意,排查线上问题时你根本不知道Redis里存的是哪份数据,只能一个个翻。建议从一开始就养成习惯,key里带上业务含义和维度id。
2.4 管理端与用户端双入口:一套后端如何同时服务两套前端
苍穹外卖项目里有两个前端:管理端(Web后台)和用户端(微信小程序)。它们访问的是同一套后端服务,但接口的路径前缀不同。管理端一般在/api/admin下,用户端在/api/user下。这个“前缀区分”看似不起眼,实际上是后面所有权限控制的基础。
如果没有这个划分,你要怎么拦截“用户不能操作管理端接口”这类请求?有了前缀之后,你可以部署两层拦截器:第一层校验请求是否携带合法Token,第二层根据请求路径前缀判断这个Token对应的角色有没有权限访问。苍穹外卖里用的方法是定义两类拦截器,分别处理管理端和用户端请求,校验逻辑分别注入WebMvcConfigurer的拦截器注册里。
还要注意管理端接口的鉴权细节。管理员登录后拿到的JWT Token里,payload里存的可能是employeeId;用户登录后,payload里存的是userId。两种Token的签名密钥可以相同,但校验逻辑要考虑进去——用户端Token绝对不能调管理端接口。所以拦截器在解析出用户身份后,还得再校验一下当前请求的路径前缀是否和身份匹配。这个设计思路,是你在简历上写“熟悉Spring Security或拦截器鉴权方案”时,最值得展开讲的细节。
3. 实操过程与核心环节实现
3.1 环境准备清单:从JDK到MySQL再到Redis,一个都不能少
开始部署前,先把环境梳理清楚。我用的是Linux服务器(也可以用本机Windows/Mac),但核心依赖版本一定要精确,否则后面会有一堆莫名其妙的问题。
- JDK:推荐1.8或11。苍穹外卖基于Spring Boot 2.x,JDK 8完全够用,如果你用JDK 17,有些老版本依赖可能不兼容
- MySQL:5.7或8.0都可以,注意字符集要设置为utf8mb4,因为微信用户的昵称可能包含表情符号
- Redis:6.x或7.x,开箱即用,默认端口6379
- Maven:3.6以上,用于构建项目
- Node.js(可选):如果你要本地跑管理端Vue项目,需要14以上版本
安装各环境后,第一步永远是检查版本:java -version、mysql --version、redis-cli ping(返回PONG才算正常)。我在部署时踩过一个坑,Redis装好了,Java代码连不上,排查半天发现是Redis配置文件里bind 127.0.0.1只允许本机访问,云服务器上的后端代码自然连不上。所以如果后端和Redis在同一台机器上,可以默认;但如果你把服务分开部署,一定要修改bind配置和防火墙规则。
3.2 数据库初始化与核心表结构设计解析
苍穹外卖的数据库表大概有20张左右,核心表包括employee(员工)、user(微信用户)、category(分类)、dish(菜品)、setmeal(套餐)、orders(订单)、order_detail(订单明细)、shopping_cart(购物车)。这些表还涉及一个常见的设计模式:逻辑删除(用status字段标记删除,而不是真正DELETE)。
在初始化数据库时,我建议你不要直接一股脑把SQL全执行了,而是对照表结构理解一遍每张表是干嘛的。比如dish表里有个字段叫status,1表示起售,0表示停售。你在做前端“上下架”功能时,其实改的就是这个字段,并不是真的把数据库记录删掉。明白了这张表背后的业务,你在写代码时才能清楚“状态变更”和“数据新增”分别要对应哪些SQL操作。
创建数据库时,务必给MySQL设置合适的时区。数据库默认时区是UTC的话,你的订单创建时间会比北京时间少8个小时,前端展示时很容易错乱。最简单的方式是在MySQL连接字符串里加上serverTimezone=Asia/Shanghai,同时在应用配置文件里设置spring.jackson.time-zone=GMT+8。
3.3 本地联调:启动后端、管理端、用户端的正确姿势
本地联调阶段,我推荐按“后端 -> 管理端 -> 用户端”的顺序启动,这样你能逐步排查问题,不至于三个项目一起报错时手忙脚乱。
第一步,启动后端。先把application.yml里的数据库地址、Redis地址、微信appid和secret、OSS密钥等配置修改成你自己申请到的。然后运行主启动类,看到类似“Started Application in 8.312 seconds”的日志就说明启动成功。这时候建议先访问Knife4j接口文档(默认路径通常在/doc.html),这个页面能直接显示所有接口定义。你可以在文档里直接测试接口,不需要前端参与就能验证后端是否正常。
第二步,启动管理端前端。在manage-web目录下执行npm install安装依赖,再执行npm run serve启动开发服务器。启动后浏览器访问localhost:8080,输入默认账号密码(一般是admin和123456),如果能看到管理后台界面,说明管理端和后端已经连通。
第三步,启动用户端微信小程序。用微信开发者工具打开用户端项目目录,注意需要先注册一个小程序测试号或使用你账号下的测试AppID。这里有个常见问题:微信开发者工具默认不允许访问本地的后端接口,你需要在“详情 -> 本地设置”里勾选“不校验合法域名”。否则小程序请求localhost会直接被拦截报错,很多新手在这里卡很久。
3.4 部署到云服务器:手动部署到自动部署,一次走通全流程
本地跑通后,最后一步就是把项目部署到真正的云服务器上。最直觉的方式是:打一个后端jar包,上传到服务器,然后用java -jar命令跑起来。前端项目构造成静态资源,放到nginx目录下,把服务器80端口代理到nginx上,nginx再根据路径把/api开头的请求转发给后端jar包监听的8080端口。
这套“手动部署”的流程,我建议你至少完整走一遍,因为只有手工操作过,你才能理解后面的Docker和自动化部署到底解决了什么痛点。手动部署会遇到几个经典问题:后端jar包启动后,你在本地浏览器访问服务器的IP怎么都打不开接口?这时候十有八九是云服务器的安全组没放行端口。你需要在网络安全组规则里允许TCP入方向的8080端口。还有一个问题是JVM默认堆内存太小,线上数据一多直接OOM,启动时建议加上-Xms512m -Xmx1024m参数。
如果你的服务器配置还不错,再用Docker Compose把MySQL、Redis、后端、前端nginx统一编排管理,会让整个部署过程清爽很多。我习惯把每个服务写成一个docker-compose里的service,数据卷挂载在宿主机目录,日志输出到宿主机文件。这样重启服务或版本升级时,只需要docker-compose up -d一条命令。Docker的好处不是“用起来很酷”,而是它把环境依赖全部隔离在了镜像内部,你换一台新服务器,只要装上Docker,一条命令就能恢复所有服务,这才是企业部署的标准姿势。
4. 常见问题与排查技巧实录
4.1 微信小程序真机预览时“request合法域名”校验报错
这个报错几乎是每个人都会遇到的。本地开发者工具调试时可以勾选“不校验合法域名”,但是真机预览时这个选项无效,微信会强制校验接口域名必须是HTTPS且在公众平台配置过的合法域名。
解决办法有两个方向。第一个是开发阶段的临时方案:在微信开发者工具右上角“详情”里勾选“不校验合法域名”(仅本地调试);第二个是上线前的正式方案:注册一个域名,配置SSL证书,用nginx反向代理后端接口,然后在微信公众平台把HTTPS域名添加到“request合法域名”列表里。这里有个容易被忽略的细节:域名不仅要备案,SSL证书也不能用自签名证书,微信要求的是正规CA签发的证书。所以如果你打算正儿八经上线一个小程序,域名和证书的准备工作要提前一周就开始。
4.2 Redis缓存和数据库数据不一致:改了数据库但前端还是旧数据
这个问题的核心在于缓存失效时机。最常见的错误是:管理端修改菜品时执行了更新SQL,但忘记调用@CacheEvict,或者注解里的key跟查询时的key写得不一样,导致Redis里还是旧数据。
排查思路很简单:先去Redis命令行执行keys *看看有哪些缓存key,再对照查询菜品的缓存key前缀,看管理端失效缓存时写的key是否完全一致。很多新手写缓存key时容易把对象toString的格式带进去,比如用"dish_" + categoryId查,但删除时用的却是"dish_" + dish.getId(),一个存的是分类id,一个删的是菜品id,当然删不到同一个key上。我建议你在设计缓存key时,保持“查询维度和失效维度一致”的原则,按分类缓存就只按分类id失效,按菜品id缓存就只按菜品id失效,不要混用。
4.3 订单支付回调没有触发,订单一直卡在“待支付”
外卖项目的支付一般对接微信支付,但这个正经支付流程需要商户号,个人开发很难申请下来。所以教学项目里通常会提供一个“模拟支付”的接口,或者直接在前端用一个“模拟支付成功”的按钮跳过真实支付。
如果你在真实场景中对接微信支付,回调通知不触发一般是这几个原因:一个是回调地址需要是公网可访问的HTTPS地址,不能用内网IP或localhost;另一个是微信支付回调会连续发送多次通知,你的接口必须实现“幂等处理”,即同一笔订单收到多次回调不能重复更新状态或重复加余额。处理办法:在回调处理逻辑里先查一下订单当前状态,只有待支付状态才执行更新,否则直接返回成功应答。
4.4 部署后CSS/JS文件404,页面白屏
这个问题几乎100%出在nginx静态资源路径配置上。你构建完Vue项目后,生成的是一个dist目录,里面有index.html和一堆带哈希值的js/css文件。nginx配置中root要指向dist目录,并配置try_files规则:“如果请求的路径不存在,就回退到index.html”。否则前端路由切换时刷新页面,nginx会去磁盘上找对应的文件路径,找不到就返回404。
还有一个隐蔽的坑:Vue项目的路由模式默认是hash模式(路径里有#),这在nginx下不用特殊处理;但如果你把路由改成history模式,就必须加上try_files的fallback。我见过不少人部署时页面白屏,打开浏览器控制台才发现js文件是text/html类型,就是因为没有配置try_files,nginx把js请求返回成了index.html的内容。
4.5 常见问题速查表
| 问题现象 | 可能原因 | 排查顺序 |
|---|---|---|
| 后端启动报“Access denied for user” | 数据库用户名或密码错误 | 检查application.yml配置和MySQL授权 |
| 小程序请求接口报“request:fail” | 域名未加入合法域名或未开HTTPS | 先本地勾选不校验域名,再查线上域名 |
| 菜品列表一直空白 | 后端接口报错或Redis缓存异常 | 先直接访问后端接口文档看返回,再查Redis |
| 上传图片失败 | OSS密钥错误或Bucket跨域未配置 | 检查accessKey、endpoint、bucket名称 |
| 接口返回401 | Token过期或未携带Token | 查看请求头是否包含Authorization字段 |
5. 项目复盘:这个项目带给我的三个深度体会
写到这里,技术细节都讲得差不多了。最后分享一点我个人的体会。
第一,苍穹外卖的价值不在于“它会点餐”,而在于它覆盖了Java后端开发从接口设计、数据库设计、缓存设计、鉴权设计到部署上线的完整闭环。你做完这个项目,最大的收获不应该是“我能把一个外卖系统跑起来”,而是你弄清楚了一个请求从微信小程序到nginx到Spring Boot到MySQL再到Redis的完整路径。
第二,部署环节千万别跳过。我知道很多同学喜欢把代码跑在本地就觉得自己会了,但其实“本地能跑”和“线上能跑”之间的距离,恰恰是公司里最看重的能力。你因为域名HTTPS、安全组、Linux权限、nginx配置、Docker容器编排所踩过的每一个坑,都会在未来某一天变成你解决线上问题的经验直觉。
第三,一定要把项目里藏着的“为什么”想清楚。面试官不会因为你用过@Cacheable就给你加分,但会因为你讲得清楚“我用Redis缓存菜品列表是因为这个接口并发高、数据变更少,缓存的成本远低于数据库查询”而对你刮目相看。多追问一层为什么,你的技术认知就会跟别人不一样。
