做完苍穹外卖这个项目之后,我复盘了很久,今天把这些收获整理成文。不是流水账式的“我做了什么”,而是“我从中真正学到了什么”,以及面试官最可能追问的那些点,我踩过的坑和最终的解法。这篇文章适合正在做苍穹外卖、或者做完了一遍但感觉说不出“收获”在哪里的朋友,建议收藏后对照自查。
1. 为什么很多人做完项目,却总觉得自己“没收获”
先说一个普遍现象。我见过太多人做苍穹外卖,包括我自己第一遍的时候——跟着视频敲代码,跟着文档部署,一顿操作猛如虎,项目也跑起来了,界面也挺好看,但面试官一问“你这个项目里最有技术难度的地方是什么”,立刻卡壳,只能憋出一句“我用了Spring Boot和Redis”。
这不是项目的问题,是学习方式的问题。苍穹外卖这类教学级项目,最典型的特点是逻辑完整、覆盖技术栈广,从后端接口到管理端页面,从微信授权登录到订单支付回调,链条很长。你跟着做一遍很容易,但如果你从头到尾只是在“复制粘贴”,那你收获的只有手指肌肉记忆,没有技术能力沉淀。
我在第二遍复做的时候,才真正开始有收获。那一次我强迫自己做三件事:每写完一个模块,就关掉视频、删掉代码重写一遍;每个技术选型,都要问自己一句“为什么用Redis而不用Map缓存”;每个异常场景,都想一下“如果这里线上出了问题,日志里能看出什么”。做到第二遍后半程,我才感觉这个项目真正长在了自己脑子里。
所以这篇文章与其说是在讲“苍穹外卖项目的收获”,不如说是在讲“怎么把一个培训级项目,变成自己的面试级项目”。核心不是敲了多少代码,而是你在每个模块里,有没有形成自己的判断力和排除问题的思路。
2. 苍穹外卖的技术栈,到底在模拟什么真实业务场景
先看看苍穹外卖整体在干什么。这是一个典型的同城生活服务类平台的后端实现,核心用户分为两类:C端用户在小程序端下单点餐,B端运营方在管理后台处理菜品、订单、分类和营业状态。整体架构其实是在模拟一条“商家后台 + 用户小程序 + 运营管理端”的标准生意链路。
技术选型上,主流的项目实现基本都是这套组合:Spring Boot作为基础框架,Spring Cache加Redis做缓存,MySQL存核心业务数据,MyBatis负责持久层,配合Nginx做静态资源代理和反向代理,用JWT做登录态管理,接入微信小程序登录和微信支付。有升级版本还会引入Spring Cloud的注册中心、网关和OpenFeign远程调用,把单体拆分成了微服务。
这份技术栈放到真实职场里是不过时的,甚至可以说很主流。中小型电商、本地生活类公司的后端,很多还是这套体系。区别在于,培训项目把复杂的分布式事务、多端一致性、高并发削峰这些难题阉割掉了,但业务边界和表结构设计,跟实际项目是很贴近的。
我自己的体会是,不要把苍穹外卖当成“教辅书”,而要把里面的每个模块当成一家真实公司的某个“部门”去看。比如管理端的增删改查只是表面,背后是“运营人员怎么高效维护一套线上菜单”;用户端的购物车和订单流程,背后是“怎么保证用户下单时看到的价格和实际支付价格一致”。你带着这个视角去做,收获会多一个量级。
3. 核心模块的“为什么”才是收获最大的地方
说实话,苍穹外卖的代码量不算少,但是单纯列模块没有意义。我把这几年面试别人和被人面试的经验结合进来,真正能拉开差距的,是你对每个核心模块背后“为什么”的理解。下面挑几个最关键的模块拆开讲。
3.1 缓存设计:不只是加一个Redis键值对
很多照着教程写的人,对缓存的理解就是“把查询数据库的结果放进Redis里,下次直接取。”这在苍穹外卖的菜品模块里是够用的,但放到真实场景里,立刻会踩到两个问题:缓存穿透和缓存一致性。
先看缓存穿透。用户端高频访问菜品列表,如果没有加防穿透处理,一个恶意请求不断拿不存在的菜品ID去查,Redis查不到,放行到MySQL,MySQL也没有,又不会回写缓存,短时间内大量无效查询会把数据库打挂。项目里你可以用缓存空值,也可以用一个简单的布隆过滤器。空值缓存简单实用,代价是空值会占用一小部分内存,设置一个较短的过期时间就能规避。
再看缓存一致性。菜品的增删改发生在管理端,用户端的缓存却不能感知。最简单的方案是:在管理端执行CUD操作的同一个事务里,主动删除对应的Redis缓存键,让用户端下一次查询时回源数据库,重新加载缓存。这比什么延迟双删、消息队列削峰要实在得多,因为在苍穹外卖这种读多写少、并发量可控的场景里,删除缓存已经能解决99%的问题。我见过有人一开始就用“双写”方案,把简单问题复杂化,反而引入了新的脏数据风险。
还有一个小技巧是拆分缓存键,不要让一个Hash键装下所有菜品数据。按分类维度、商户维度拆分缓存Key,一是能精准失效,二是在极端情况下还能做局部降级。这一点我对接真实项目时发现尤其重要——一个用户投诉“改了价格但小程序还是显示旧价格”,排查到最后就是缓存Key粒度太粗,一次全线失效把热点门店的缓存也误杀了。
3.2 订单状态机:你的业务有没有死循环
订单模块是所有类似项目里的重头戏。苍穹外卖里一个订单会经历待支付、已支付、已接单、派送中、已完成、已取消这几个状态。很多人实现的时候,就是在一个Controller里写一堆if-else,比如“如果status是1就改成2”,表面上功能正常,但代码维护起来就是灾难。
我在复做时给自己加了一项训练:把订单状态流转画成一张状态机表,明确哪些状态可以跳到哪些状态,哪些跳转是非法操作,然后为非法跳转统一抛业务异常。比如已经取消的订单,就不允许再标记为已支付;已完成订单不允许再次派单。这套校验逻辑放在Service层的最前面,相当于给订单数据上了一道守卫。
好处在什么时候体现?我在一次模拟演练中故意用脚本并发提交“已支付”和“已取消”两个操作,没有状态机守卫时,由于后执行的覆盖了先执行的,订单最终状态变成了“已支付”,但用户已经收到取消通知了。上了状态机校验之后,后到的非法操作直接被拦截,数据一致性一下就有了保障。这个点我强烈建议你在简历的项目描述里用“状态机驱动订单流转,拒绝非法状态跳转”这个说法,面试官基本都会追问实现细节。
另外订单超时未支付自动取消,也是一道经典题目。实现通常有两种:定时任务批量扫描,或者延迟队列。苍穹外卖教学里基本是前者,但你在项目总结时应该能说出定时任务方案的两个缺点——全表扫描的IO浪费,以及扫描间隔带来的时间差。然后你再提一句“如果是我优化,我会用Redis延时队列,Key为订单号,Value为过期时间戳,启动一个延迟拉取线程处理到期的订单”,立刻就能让对方觉得你对方案的边界是清楚的。
3.3 购物车与库存:超卖问题怎么演变的
购物车接口看起来是简单的读写,但如果你把它放大成“并发抢购”场景,逻辑就变了。苍穹外卖的业务是用户往购物车加菜、改数量、清空,看似人畜无害,但真正的坑在“下单时扣减库存”的环节。
我在测试阶段用压测工具模拟了100个用户同时提交同一道限购菜品的订单,结果出现了库存变成负数的情况。原因很典型:先SELECT库存,再UPDATE库存,两步之间没有做原子性控制。这个问题的解决方案有很多层,最简单的是在UPDATE语句里把库存判断条件并入,例如UPDATE dish SET stock = stock - #{num} WHERE id = #{id} AND stock >= #{num},然后检查受影响行数,如果为0说明库存不足。这是乐观锁思路的常见落地。
如果需要更严格的控制,可以用分布式锁或Redis原子操作,但因为苍穹外卖本身并发量并不高,SQL加条件已经足够,而且面试时你从“简单方案到分布式锁方向”的演进逻辑说出来,比直接背一个“我用Redisson实现分布式锁”要更有说服力。为什么?因为真实场景里过度设计比技术不足更常见。
这里我还要提一个同学们经常忽略的点——库存预扣与支付回滚。用户提交订单时预扣库存,但如果支付超时或用户取消,库存必须回补,否则会有幽灵库存。苍穹外卖基本没有涉及这部分,但你主动考虑并写进文档里,是很大的加分项。这个完整闭环体现的是你对资金和库存联动风险的感知力。
3.4 对接微信小程序登录与支付:签名和异步回调才是重点
项目里接微信登录是一个“看起来繁琐、实际上原理清晰”的模块:前端通过wx.login()获取临时code,后端拿code去微信接口换openid,再以自己的逻辑生成业务token返回给小程序端。如果项目没有真正配置微信商户号,一般也是用模拟接口或本地支付沙箱跑通。
这里我要特别强调两个容易出问题的细节。第一个是JWT的密钥和安全策略。不要把密钥写死在前端或公共配置里,要放到服务端环境变量中,token的过期时间不要设置成永久,建议2小时过期、7天刷新,防止用户登录态被盗用后长期有效。第二个是支付的回调幂等性。真实的微信支付是异步回调,可能出现同一条支付结果通知多次投递,你的回调接口必须支持幂等,第一次处理完把订单状态改为已支付,第二次再来时应该直接返回成功而不是再改一次。
我在项目里遇到过最离谱的一个Bug,是回调接口里没有对sign参数做校验,任何一个知道接口地址的人,伪造一个“支付成功”的POST请求,就能把订单改成已支付。这条安全漏洞在真实项目中是致命级别,我在苍穹外卖复做时专门把这个校验逻辑补上了。收获是什么?不是你写了多少接口,而是你能不能在任何一个带资金属性的接口前,本能地问一句“这个请求可信吗,来源和签名校验在哪里”。
3.5 管理端鉴权与操作日志:权限边界和审计思维
管理端的权限设计,很多人会做得很随意,比如判断“是不是管理员”,是就放行。但真正做深一层,你会发现管理端内部的员工账号也分角色。苍穹外卖里面至少应该有:超级管理员、门店管理员、普通操作员。不同角色能访问的接口和菜单不同。
我在复做时基于Spring的拦截器实现了多角色权限校验:登录时把角色ID写进JWT的Claims,拦截器从Claims中取出角色,再匹配当前请求所需的角色集合,不匹配就返回403。同时我还加了一道“操作审计”逻辑,对菜品上下架、订单改价等敏感操作,专门记录操作人、操作时间和变更前后的值落库。
这套逻辑的好处,不只是技术上多了自定义注解和切面这些写法,而是让你在项目描述时能说一句“我做了一套带审计的操作权限体系”——这个在真实的后台系统里是标配需求,面试官听到会眼前一亮。很多新手只关注接口写没写出来,忽略了权限和合规这种“看不见但必须对”的东西,而后者往往就是区分初级和高级工程师的隐性标准。
4. 线上问题的完整排查链路:一个真实案例
项目做完,在本地跑得好,不代表上线没问题。这里我复盘一个我在测试环境遇到的经典问题,完整讲一下排查过程,这种链路本身就是面试中“项目难点”最好的素材。
4.1 现象:深夜用户批量反馈“下单失败”
某轮测试中,测试同学在晚上集中提交订单,发现部分用户的订单一直停留在“待支付”状态,点击支付也会报“系统繁忙”。我第一反应不是去看后端代码,而是先看监控。虽然没有完整的监控平台,但我至少把日志和数据库状态拉了出来。
关键信息有三条:该时间段内订单创建接口的QPS并不高,数据库连接数却飙升到接近最大连接数;相关SQL日志里大量出现“Waiting for table metadata lock”的锁等待;同时Redis的缓存命中率从正常的90%以上骤降到30%。
这个组合非常典型——不是下单逻辑本身的问题,而是数据库层面的锁把请求堵住了。
4.2 根因定位:一个被忽略的老旧定时任务
继续追查后发现,定时任务里有个“每分钟清空一周前历史结算单”的指令,这个任务本身很常规,问题出在它在一个大事务里执行——先删主表,再删明细表,但明细表数据量极大,导致删除操作执行时间很长。删除期间,任何需要访问这些表的业务SQL都会被元数据锁卡住,于是用户的下单请求全部阻塞在SQL等待上。
再加一个更深层的问题:缓存集中过期。用户端菜品列表缓存设置的过期时间都是午夜整点,这批请求回源数据库,触发了缓存击穿。本来数据库还能扛住,但和上面的锁等待叠加在一起,就成了雪崩。
4.3 修复与反思
那次修复分了三步,我先紧急停掉那个定时任务,把积压的下单请求和水位排空;然后把历史数据清理改成按小时分区删除,错峰执行,删除操作放在凌晨低峰时段;最后把缓存过期时间改造为固定基础时间加一个随机偏移量,避免缓存集体失效。
这套排查链路,如果你能清晰地在面试时讲出来,甚至比写了多少代码更有说服力。因为它体现了你的思维方式:从现象到监控,从监控到日志,从日志到根因,再到修复后的验证和回归。这种“从现象到根因”的路径,是做真实项目最值钱的能力。
5. 做完之后,怎么把“收获”写进简历和面试表达里
这一节可能最实用。很多人项目做完了,但简历上只能写“负责用户端和管理端的功能开发”,这种描述等于没写。我给你的建议是,用“项目描述—个人职责—技术难点—量化结果”的结构,把苍穹外卖重新包装一下。
举个例子。项目描述可以改写成:“基于Spring Boot + Redis + MySQL实现同城餐饮外卖平台,包含用户端小程序与商家运营管理后台;负责订单、菜品、购物车等核心模块的设计与实现;通过对缓存失效策略与库存扣减逻辑的优化,将订单接口异常率从测试期的3%降低至0.1%。”
你没有必要把每个接口都写上去,但要把三个维度的能力体现出来:业务理解(状态机、幂等)、技术深度(缓存穿透、锁、分布式)、工程规范(日志、异常、权限)。技术亮点部分不要堆名词,每个亮点遵循“场景→方案→结果”的格式。比如“针对库存并发扣减问题,通过将库存判断并入更新SQL并结合事务,消除超卖风险,压测单接口TPS提升约40%”。
面试表达还有一个小技巧,准备一个“一页纸清单”,把你在项目里所有用到的技术点列出来,然后挨个问自己三个问题:为什么要用它?不用它会怎样?它的局限性是什么?比如Redis缓存,你不仅要说它快,还要说它不适合做什么——不适合强一致性数据,不适合大数据量冷数据存储。这种“知其短”的坦诚,反而会让面试官更相信你真的用过。
6. 避坑实录:做苍穹外卖中最容易白费功夫的六个地方
最后分享几条我做这个项目时踩过的坑,都是一些看起来很小的决策,却实实在在影响效率和最终质量。
第一,不要一开始就追求“代码和视频完全一致”。完全一致没有任何意义,因为视频里的代码是基于它自己的环境写的,依赖版本、数据库初始化、甚至Nginx配置都可能有微妙的兼容差异。按自己的环境调整并记录差异,才是有效学习。
第二,数据库表结构和字段命名要提前理清。苍穹外卖的表数量不少,如果边做边建,很容易后面改代码改到吐血。先花半天把所有表的ER关系理一遍、字段含义标注出来,后面每个模块的开发效率会直接翻倍。
第三,支付模块不要只跑通就完事。至少要模拟一次“支付成功回调”和“支付超时未回调”两个场景,并确认日志里有完整的关键步骤输出。这些异常路径才是真实项目里最容易出事故的地方。
第四,缓存Key的规范要定好。不要用裸字符串,建议一套“模块:业务:ID”的命名规范,比如dish:category:001。没有规范的话,等你排查问题的时候,会发现Redis里塞满了看不懂的键,只能一个个用SCAN猜。
第五,不要忽略日志规范。Service层每个核心业务接口,至少要记录入参、关键分支、出参或异常摘要。这个习惯我是在排查上面那个锁等待问题时才深刻体会到的,如果日志不完整,靠猜来复盘问题,效率太低了。
第六,也是最重要的一点,尽量在项目做完后,挑一个模块推翻重写一遍。比如把订单模块从“能用”改成“设计上说得通”,你会发现第二次写的代码量可能只有第一次的一半,但逻辑清晰度却翻了一倍。这才是项目带给你的最大收获——你有了自己的审美和判断力。
苍穹外卖不是一个终点,它是一个很好的起点。从照着敲,到理解,到能改、能讲、能应对追问,整个过程下来,你会明显感觉自己的后端功底扎实了一个台阶。如果你也正在做这个项目,或者做完了想再沉淀一遍,希望这篇复盘能帮你把那些散落的代码,真正长成自己的能力。