
做Java这几年几乎每个想转行或者刚毕业的程序员手里都缺一个“拿得出手”的完整项目。如果你最近在学Spring Boot又恰好对微信小程序感兴趣那《苍穹外卖-2》是绕不开的经典实战一个前后端分离的外卖系统管理端做运营后台用户端跑在微信小程序里从数据库设计、接口开发、小程序页面渲染到最终买一台云服务器把它部署上线整个链路完整得不像练手项目更像是一家小公司的真实业务系统。这篇文章我把这个项目从开发到部署的每个关键环节都拆开讲清楚包括技术选型、核心功能实现、联调时最容易踩的坑以及上线部署的完整步骤给准备把它写进简历或者当毕设的同学一份能直接照着落地的实操参考。1. 项目概述与整体架构拆解1.1 苍穹外卖-2到底在做什么苍穹外卖项目的核心业务很简单用户在微信小程序里浏览菜品、加入购物车、提交订单、完成支付管理端在网页上维护菜品分类、菜品信息、套餐组合、门店营业状态还要处理订单列表和销售统计。你可以把它理解成迷你版的美团但业务边界更清晰没有骑手调度没有复杂的商家分账重点放在用户下单和商家管理这两条主线上。项目名称苍穹外卖-2用户端微信小程序 管理端Web技术主干Java 8/11 Spring Boot 2.x MyBatis-Plus MySQL Redis前端形态微信小程序原生框架 Vue管理后台通信方式前端通过HTTP/HTTPS调用后端RESTful接口JSON格式交互关键中间件Redis缓存菜品数据与购物车状态定时任务处理订单超时自动取消从“开发到部署”这条主线看它比单纯写一个单体Web应用多了一层微信生态的接入。你需要处理wx.login登录换token、小程序端请求域名白名单、HTTPS证书、小程序发布审核这些环节学校项目里基本不会讲到但实际企业开发里几乎每天都用得到。1.2 为什么选择前后端分离 小程序方案外卖业务天然适合前后端分离因为用户触点太多了用户在小程序下单运营人员在管理后台处理菜品二者是两套完全独立的前端工程只有后端能共用。如果走传统的模板引擎渲染管理端和用户端就得做两套模板每次发版都要一起联调上线太痛苦。前后端分离的好处体现在三个层面开发节奏解耦小程序端工程师只管调后端接口管理端Web工程师也只管调后端接口两边互不阻塞只要接口文档约定好。部署形态灵活后端可以部署在通用服务器上小程序端存放在微信平台管理端Web则可以扔到Nginx甚至对象存储里任何一端出问题都不会拖累其他端。方便做权限隔离管理端接口走管理员JWT校验小程序端接口走用户JWT校验两套Token体系通过Spring MVC拦截器反向隔离逻辑非常清晰。微信小程序作为用户端的载体还有一个额外优势它天然自带登录体系和支付体系用户不需要注册账号打开微信就能下单这在真实业务中能显著降低获客门槛。而且小程序端的渲染层和逻辑层是分离的页面跳转、组件生命周期都有一套非常成熟的规范比纯H5在移动端的体验顺畅得多。2. 开发前的工程设计与技术选型2.1 后端到底该用哪些依赖苍穹外卖-2的后端围绕Spring Boot搭建选型上有一条非常明确的组合你做完简历上也好看Spring Boot 2.7.x稳定版与微信小程序官方API兼容性最好MyBatis-Plus 3.5.x单表CRUD几乎不用写SQL内置分页插件和代码生成器MySQL 8.x业务数据的主存储Redis 5.x缓存菜品列表、登录Token、购物车状态JWTjjwt登录后的无状态身份认证Knife4j自动生成接口文档前端联调时直接看文档就能调接口Lombok简化实体类代码这里最核心的选型决策是把MyBatis-Plus而不是原生MyBatis作为持久层框架。外卖系统的业务逻辑并不算复杂但表结构多得出奇用户表、分类表、菜品表、套餐表、购物车表、订单表、订单明细表、地址簿表。如果每张表都手写XML映射文件光是增删改查的样板代码就要写上千行。MyBatis-Plus的BaseMapper把单表操作全包了你只需要定义实体类继承一个Mapper接口条件构造器QueryWrapper可以处理90%以上的动态查询场景。对于复杂联表查询也不慌该写SQL的明确写SQL。比如后台管理端要查“每个分类下的菜品数量”直接在Mapper方法上用Select注解写一句join统计SQL既保证可读性又不牺牲性能。2.2 数据库表设计外卖业务的数据底座这是整个项目里我觉得最值得静下心研究的部分。业务表设计得是否合理直接决定了后面写业务逻辑会不会经常返工。苍穹外卖-2的核心表结构可以归成四组表分组表名核心字段说明用户体系userid, openid, nickname, phoneopenid是微信用户的唯一标识菜品体系categoryid, name, type, sorttype区分菜品分类与套餐分类菜品体系dishid, name, category_id, price, image, statusstatus控制是否在售菜品体系setmealid, name, category_id, price, status套餐主表关联多个菜品用户操作shopping_cartid, user_id, dish_id, number购物车可以存Redis也可以落库订单体系ordersid, number, user_id, status, amount, address订单主表status带订单生命周期订单体系order_detailid, order_id, dish_name, dish_number下单时菜品快照防止菜品改价影响历史订单用户操作address_bookid, user_id, consignee, detail用户收货地址很多人第一次设计订单表会忽略order_detail菜品快照我在这里踩过坑。如果订单明细直接关联dish表那用户下单之后你把菜品价格改了之前的订单历史金额在联表查询时就会变财务对账直接乱套。正确做法是下单时把菜品名称、价格、数量原样复制一份到order_detail之后菜品表怎么改都不影响历史订单。2.3 微信小程序端的技术结构小程序端我用的是原生微信小程序语法没有引入uni-app之类的跨端框架。很多人问为什么不用跨端框架我的答案很直接苍穹外卖-2的项目定位是搞懂微信生态原生框架能让你的注意力集中在小程序自己的生命周期、API调用和组件通讯上而不是被框架的抽象层分心。小程序的页面结构大致拆成这样pages/index首页展示分类和推荐菜品pages/order点餐页按分类加载菜品列表加购物车pages/cart购物车页增减数量、清空、结算pages/address地址簿管理新增和选择收货地址pages/pay结算页模拟支付pages/order-list订单列表与订单详情在原生小程序里跨页面状态传递靠的是全局data、本地storage和页面路由参数三件套。购物车数据每次从后端拉避免在小程序本地维护一套容易出错的状态副本。这样虽然每次进购物车页面多一次网络请求但能保证多设备之间的数据一致。3. 核心功能实现与业务闭环3.1 微信登录从wx.login到JWT这是小程序端最核心的一个环节也是新手最容易写歪的地方。正确的流程分四步小程序端调用wx.login()获取一个临时登录凭证code有效期只有五分钟用一次就失效。小程序把code通过后端接口传上来后端用appid、secret和这个code请求微信的服务接口code2Session换来openid和session_key。后端拿openid查user表如果用户不存在就自动注册一个新用户然后生成一个JWT令牌返回给前端。小程序把令牌存到storage里后面所有需要登录态的请求都带着这个令牌。理论上后端逻辑就是这么简单。但我在项目实操中发现一个特别容易错的小细节小程序前端不能拿code直接当身份令牌用因为code是一次性的而且session_key落在前端没有任何意义。整套换openid的逻辑必须放在后端服务端执行这是微信安全规范明确要求的。前端如果直接把code当token传给后端接口做校验接口就完全没有安全可言。JWT令牌建议设置有效期为7天左右密钥放在后端的配置文件里不要硬编码在代码中更不要随代码提交到公开仓库。我见过有人把JWT密钥和数据库密码一起打包传到GitHub然后整个服务器被人当肉鸡这种事不是段子。3.2 菜品列表与购物车实现小程序首页最核心的交互就是两个按分类切换菜品列表、把菜品加入购物车。菜品列表查询接口的SQL很简单但性能可以通过Redis优化把分类和菜品的关系序列化成JSON存进Rediskey设计成category:dish:{categoryId}缓存过期时间2小时左右。点餐高峰时段同一份菜品数据会被几百人反复查询命中缓存能直接把数据库的压力降一个数量级。购物车有两种实现姿势。一种是把购物车数据直接存数据库的shopping_cart表一种是存Redis的Hash结构每条记录以userId为key。我推荐用Redis存购物车理由很简单购物车本身就是临时性的东西用户今天加了一堆菜明天可能就不下单了落库反而会堆出来一大堆垃圾数据。Redis设置24小时过期再配合一个定时清理任务购物车数据自然不会膨胀。不过Redis方案有一个坑购物车里的菜品数据如果已经反序列化成普通对象后端改价之后用户的购物车显示的还是旧价格。解决思路是在提交订单时从数据库重新拉一遍菜品真实价格做校验而不是直接信任前端传过来的金额。所有订单金额一律以后端计算为准这是电商系统的基本底线也是面试官喜欢深挖的考点。3.3 订单提交与状态流转提交订单这一步涉及多个数据表变更创建orders主表记录、插入order_detail明细表、清理购物车、扣减菜品库存如果有、关闭Redis中对应的购物车缓存。这么多操作不能用普通的try-catch串行处理必须放在同一个Spring事务里任何一个环节失败都要整体回滚否则会出现“订单主表存在但明细表为空”的脏数据。我实操时用Transactional(rollbackFor Exception.class)标记在Service方法上确保任务异常、数据访问异常都能回滚。这里提醒一下事务方法内部不能自己catch异常吞掉如果内部捕获后不抛出Spring不会触发回滚逻辑调试起来非常隐蔽。订单状态流转也是面试高频题。我定义的数量如下状态值含义触发方式1待付款用户提交订单后2待接单用户支付成功后3已接单/制作中商家后台点接单4派送中商家点派送5已完成用户确认收货/商家标记完成6已取消超时未支付或用户主动取消超时未支付订单我建议用定时任务批量扫描每2分钟扫一次把创建时间超过30分钟且状态仍是待付款的订单置为已取消。这个逻辑单独抽一个Task类跑不要混在用户请求线程里否则会拖慢主流程。3.4 支付模拟支付与真实支付的差别真实微信支付需要企业资质和小程序商户号个人开发者通常做不了。苍穹外卖-2这种教学项目一般都用“模拟支付”替代前端在支付页面放一个“确认支付”按钮点击后调用后端接口把订单状态置为已支付同时生成一个假的支付流水号记录到订单表。虽然是模拟支付设计上也要为将来接真实支付留好扩展点。建议在订单表里加一个支付方式字段wechat/mock支付回调接口单独写一个Controller接口路径设计成/pay/notify将来接入真实微信支付时只需要在这个接口里做签名校验和回调处理不需要改订单主链路。模拟支付有个隐藏好处它让你能把注意力放在订单状态机的完整闭环上而不需要纠结证书、退款、对账单这些支付领域的问题。4. 前后端联调那些绕不开的坑4.1 开发环境下的跨域与代理配置开发模式下小程序端要请求本机的后端接口最省事的方式是在微信开发者工具里勾选“不校验合法域名”然后用本机IP加后端端口去访问。但有个关键注意点微信开发者工具的模拟器能访问http://localhost:8081真机预览却不能。真机调试时后端接口地址必须改成局域网IP例如http://192.168.1.100:8081且手机和电脑要在同一Wi-Fi下。后端还要处理跨域问题。小程序端的请求domain是https://服务商域名而本地后端是http://ip:8081跨域是必然的。我习惯在Spring Boot里写一个CorsFilter放行所有来源允许携带认证信息放行所有HTTP方法。但注意一个细节如果CORS配置中allowedOrigins用的是就不能同时allowCredentials(true)两者是对立的*否则浏览器会直接拦截响应。4.2 接口文档工具的高效用法前后端分离开发最怕的是接口签名不一致后端字段叫dishName前端写的是name联调时接口日志调到人崩溃。我强烈建议在项目里集成Knife4j接口调试页面启动后自动生成前端同学可以直接在页面上传参、看响应、复制请求示例。我在实际项目中的工作流是先定义好实体类和Controller的空的返回值结构用Knife4j看生成的接口定义是否合理然后才开始写小程序前端。这样能保证前端开发不阻塞。如果你的项目还在设计阶段甚至可以先用Knife4j把接口文档固化下来再让前后端同时开工联调效率至少提升一倍。4.3 高频报错排查清单这一节的内容绝对对得起“实操”两个字。我整理了联调阶段碰到过的几类问题全部是真实遇到过而且很有代表性的。现象根因分析解决办法小程序请求接口报“url not in domain list”request合法域名没有配置小程序后台添加服务器域名开发阶段暂时本地可跳过所有接口都报401Token没有在请求头带上小程序封装request对象在header里统一加Authorization登录接口报invalid codecode已经失效或重复使用确认每次wx.login都重新获取code后端不缓存code真机请求后端超时真机和电脑不在同一局域网或防火墙拦截改为局域网IP访问服务器防火墙放行后端端口数据库查询出的时间比本地少8小时JDBC连接串没设置时区连接串加serverTimezoneAsia/Shanghai指定时区订单提交后明细缺失业务方法内部捕获异常未抛出事务方法内禁止捕获后吞掉回滚条件要生效这里单拎出数据库时间少8小时说一下。这是国内开发者最常见的时区问题根源在于MySQL和Java默认时区不一致。解决办法不只是在JDBC连接串里加serverTimezone还要保证MySQL的global.time_zone也设置为8:00一个是连接层对齐一个是数据库层对齐俩缺一不可。5. 生产环境部署从云服务器到小程序上线5.1 服务器环境规划部署苍穹外卖-2我建议买一台2核4G的云服务器内存太低Redis和Java同时跑会频繁GC太高预算又划不来。操作系统选CentOS或者Ubuntu Server具体无所谓。2核4G配置下MySQL占用1.5G左右内存Redis占用几百兆Spring Boot应用大概占1G内存左右刚好卡在够用的范围。需要安装的运行时组件只有一个思路能用Docker容器化就不要再手动编译安装。MySQL、Redis这两个中间件直接拉官方镜像跑容器Java后端打一个镜像管理端Nginx做静态文件服务加反向代理。这样不仅部署快后面迁移服务器也方便。唯一不建议容器化的是数据库数据本身数据卷必须挂到宿主机目录。5.2 Docker编排整个后端服务我在部署项目时习惯用docker-compose管理整个技术栈在一个文件里定义MySQL、Redis、app三个服务。数据库和Redis的编排很简单核心是数据持久化。MySQL容器必须指定挂载宿主机的一个目录到容器内的/var/lib/mysql同时设置MYSQL_ROOT_PASSWORD环境变量。Redis容器挂载data目录到宿主机防止容器重建后缓存全丢。Java后端镜像的Dockerfile核心就一段FROM openjdk:8-jre-alpine ARG JAR_FILEtarget/sky-take-out.jar COPY ${JAR_FILE} app.jar EXPOSE 8081 ENTRYPOINT [java,-jar,/app.jar]构建顺序是先在本机执行mvn clean package -DskipTests把jar包打出来然后执行docker build -t sky-server .构建镜像最后在docker-compose.yml里引用。提醒一个非常关键的细节数据库地址不要配localhost容器内的localhost指向容器自己连不上宿主机数据库。正确做法是写mysql也就是docker-compose里MySQL服务的名称Docker内部网络会自动解析。5.3 Nginx配置与HTTPS证书部署小程序正式环境要求所有网络请求必须走HTTPS而小程序后台配置的request合法域名又必须是你自己已经备案的域名。所以部署环节最重要的就是搞定域名和HTTPS证书。Nginx在整个架构中承担两个角色一是托管管理端Web的静态资源二是把/api/路径的反向代理到Spring Boot容器。核心配置片段大致长这样server { listen 443 ssl; server_name api.你的域名.com; ssl_certificate /etc/nginx/ssl/你的证书.crt; ssl_certificate_key /etc/nginx/ssl/你的证书.key; location /api/ { proxy_pass http://127.0.0.1:8081; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }证书申请现在很方便用certbot或者云平台的一年期免费证书都可以。申请下来后把证书文件和密钥文件放到Nginx配置目录再加一条80端口的server块把HTTP请求301跳转到HTTPS。小程序上线之前要把这个域名填进微信公众平台后台的服务器域名白名单里配置之后大约几分钟到几小时内生效。部署MySQL初期的建库建表我建议直接导入SQL脚本而不是让程序自动建表。因为线上建表权限、索引优化这些都是DBA要控制的体面活程序自动建表只能当开发环境便利工具用。可以用MyBatis-Plus代码生成器先在本机生成好完整的建表语句整理成一个schema.sql部署时执行导入干净利落。5.4 微信小程序发布客户端发布前需要在微信公众平台上传小程序代码走一遍审核流程。个人认证的小程序一年有一次去除广告的限制企业认证则可以根据资质多做一些。审核一般1-2天如果涉及支付相关类目可能需要额外提交商户资质证明。小程序发布前记得做三件事把request合法域名从开发环境切换成正式HTTPS域名用微信开发者工具做一轮“预览”和“真机调试”确认正式环境下所有接口都能通清掉开发环境残留的模拟登录Token不然手机上的缓存数据会对不上小程序在审核期间可以先用“体验版”内部测试把微信成员加入开发白名单他们通过小程序码进入体验版全流程出餐。等审核通过后发布正式版整个苍穹外卖-2项目就完成了从开发到部署的闭环。6. 项目扩展与面试亮点打造6.1 性能优化空间苍穹外卖做完了如果只是停留在“能跑”的程度就浪费了这个项目起码一半的价值。想让你在面试时有的聊可以从三个方向做性能优化缓存分层菜品列表Redis缓存之外再叠加一层本地缓存Caffeine热点数据连网络开销都省了。数据库索引优化订单表按user_id和create_time建联合索引查询用户的历史订单能快一个量级。异步化取消订单的短信通知、支付结果的异步通知都可以塞进MQ或者Spring的Async线程池里把请求核心路径的耗时降下来。6.2 功能扩展点从产品角度讲外卖系统还可以继续加很多模块优惠券、会员积分、商家店铺管理、路由配送、评价中心随便挑一个都能扩展出一个新的子项目。我自己在苍穹外卖的基础上加过优惠券模块用户领券、下单抵扣、后台发券代码工作量其实不大但面试时聊起来非常加分因为优惠券模块涉及大量状态机和并发扣减逻辑。6.3 简历上的写法建议如果你准备把这个项目写进简历不要只写一句“负责苍穹外卖系统的开发与部署”。诚实的建议是拆成几个有明确业务动词的条目主导外卖平台微信小程序端从0到1的开发完成微信登录、菜品浏览、购物车、订单提交全链路基于Spring Boot MyBatis-Plus搭建后端设计订单与菜品核心表结构实现订单状态流转与超时取消的定时任务通过Redis缓存菜品数据、购物车状态降低数据库压力提升高频接口响应速度负责项目容器化部署通过Docker Compose编排MySQL、Redis、Java后端配置Nginx反向代理与HTTPS证书完成小程序上线简历上写项目核心原则是数据优先、业务闭环优先不要堆名词。能把这个项目的来龙去脉讲清楚比十个只写了一行的项目都有说服力。我自己的体会是做完苍穹外卖-2之后看很多中小型项目的源码都会觉得脉络清晰不少。它不像那些纯理论教程讲完就忘而是一个真正命中业务场景的完整闭环。练项目的意义不在于代码本身而在于你亲手经历过“设计——开发——踩坑——部署”这个完整周期后建立的对业务系统的整体感知。这个感知是面试时你和背八股文的人最大的区别。