☰
微信小程序外卖点餐系统开发实战:从架构到部署
2026/10/10 18:39:37 网站建设 项目流程

小程序外卖项目听起来是个常见的教学案例,但真正把它从空文件做到线上可点餐,中间隔着的东西比大多数人想象的多得多。这篇文章不打算复述一遍需求文档,而是把我自己把“苍穹外卖-2”从开发到部署完整跑通的路径、选型逻辑和踩坑记录整理出来,给准备上手同类前后端分离项目的朋友一条更省力的参考路线。

先说清楚这个项目是什么定位。它本质是一套微信小程序外卖点餐系统,小程序端给用户用,点餐、下单、支付;Web管理端给商家用,管理菜品、分类、订单和营业数据。后端采用Spring Boot提供RESTful接口,前端两个端都走HTTP请求拿数据,是典型的、也是目前企业里最主流的前后端分离形态。适合谁看?正在学Java但没接触过完整项目链路的人、准备做毕设或求职项目的人,以及想搞清楚“开发完的代码到底怎么放到服务器上跑起来”的人。你不需要一开始就全懂,跟着走一遍,收获会很大。

1. 项目全景:这套外卖系统的业务模型与工程结构

1.1 业务角色与核心流程

要理解一个项目,先看它服务谁。这套系统里有三类角色:普通用户、平台管理员(商家侧)、系统后台本身。用户在小程序端完成“浏览菜品—加购物车—提交订单—支付—查看订单状态”这一条完整链路;管理员在Web端维护分类和菜品信息、接收新订单、处理订单状态(接单、派送、完成),同时能查看营业额和订单统计。

把这条主线理清楚后,你会发现所有功能模块都不是孤立存在的。购物车要有用户维度来隔离,订单要有状态机来流转,菜品要有上下架状态来决定用户端是否可见。做设计时如果只看单个接口,很容易把数据模型割裂开;只有先把业务闭环在脑子里完整跑一遍,表结构设计才不会返工。这一点是很多初学者最容易忽略的地方,拿到需求就写代码,结果写到订单模块才发现菜品表缺了状态字段。

1.2 前后端分离究竟分离了什么

“前后端分离”这四个字说出来简单,真正落地是物理层面的分离。小程序前端和后端是两个独立工程,Web管理端前端又是一个独立工程,三个工程通过HTTP接口通信,谁都不直接依赖谁的代码。好处有三个:团队可以并行开发,前端不用等后端写完才能开工;后端只需要对外暴露稳定的接口文档,前端用Mock数据就能先跑起来;部署时各端可以独立扩容,小程序访问量大了只升后端实例就行。

但这套模式也带来了额外的工作量:跨域问题、接口鉴权、联调成本、部署时的静态资源与API地址配置。所以选型时不要只盯着框架本身好不好用,要看整个工程链路里谁帮你把这些问题解决了。这也是为什么我用Spring Boot做后端而不是SSH(Spring MVC + Spring + Hibernate)老组合——生态里现成的解决方案多,踩坑成本低。

1.3 工程目录与模块划分

后端工程我按功能模块分包,而不是按技术层分包。也就是说,不是建一个controller包把所有Controller都塞进去,而是按业务域来分,比如user、shop、order、dish、category、addressBook,每个包内部自己包含controller、service、mapper和entity。这种划分方式在单体应用里后期维护成本最低,你改订单相关代码时,所有相关文件都在同一个小包内,不需要跨目录到处找。工程目录大致是这个样子:

sky-server ├── sky-common // 公共模块:常量、异常处理、工具类 ├── sky-pojo // 实体类、DTO、VO ├── sky-server // Spring Boot主模块:controller、service、mapper

分开三个模块的好处是依赖方向清晰,pojo被server和common同时引用,但server不会反向依赖到pojo以外的东西。如果你自己搭工程,我建议一开始就按这个结构建,后面扩展功能时不会乱。

2. 技术栈选型的幕后考量

2.1 后端核心:Spring Boot 2.7 + MyBatis-Plus

后端我选的是Spring Boot 2.7.x,而不是直接上Spring Boot 3,原因是这个项目需要和中老年JDK环境兼容,很多云服务器上默认装的还是JDK 8,Spring Boot 2.7就是基于JDK 8的,直接就能跑。Spring Boot 3强制要求JDK 17,虽然新,但部署环境不一定跟得上,做教程项目没必要给自己添这个麻烦。

持久层选了MyBatis-Plus,它相对于原生MyBatis最大的价值是把单表CRUD的活全包了。BaseMapper里已经内置了insert、updateById、selectPage这些通用方法,写业务代码时只需要关注多表关联和复杂查询的自定义SQL。比如菜品分页查询要关联分类表查分类名称,这类场景才值得手写XML。如果你的排序、条件查询都是单表级别,用MyBatis-Plus能让代码量至少减掉三分之一。

2.2 小程序端:原生开发而非第三方框架

小程序端我用的是微信原生开发,没有引入uni-app或Taro这类跨端框架。原因很直接:这个项目要教的是小程序本身的运行机制,包括页面路由、组件生命周期、微信API调用,这些用原生方式最透明。如果用了跨端框架,遇到问题你还要先搞清楚框架那层做了什么事,反而增加学习成本。

实际开发里确实会遇到一些原生开发比较麻烦的地方,比如顶部导航栏在不同机型上的高度不一致。iPhone的刘海屏和普通Android机的状态栏高度差别很大,写自定义导航栏时如果高度写死,界面就会被顶出一块空白或重叠。我的处理办法是直接从系统里取数据,通过wx.getWindowInfo获取statusBarHeight,再根据胶囊按钮的位置计算导航栏总高度,实测下来不同机型基本都能对齐。如果等下去做小程序端的页面适配,这是迟早要用到的。

2.3 中间件三件套:Redis、MySQL、Nginx

这个项目的中间件是标准三件套:MySQL存业务数据,Redis扛缓存热点,Nginx做反向代理和静态资源服务。

Redis在这个项目里承担的核心工作是缓存菜品数据。小程序首页每次打开都要查一遍分类和菜品,如果全都打到MySQL上,高峰期会很吃力。我的策略是:分类和菜品数据在修改时主动清理缓存,用户端查询时先查Redis,没命中再查库并回填。这个策略是典型的Cache Aside Pattern,理解它比记住代码更重要:先更新数据库,再删除缓存,下次查询时重建缓存。这里有个细节很多人会踩:先删缓存再更新数据库,会有一个时间窗口让旧数据被重新写回Redis。所以一定要记住顺序,先更新DB,再删缓存。

Nginx的角色是统一入口。部署时后端接口监听8080端口,Nginx监听80端口,把/api/开头的请求转发到后端,把管理端页面和用户端用到的静态资源直接托管。同时Nginx支持配置多个server块,以后如果同一个服务器上还要部署别的项目,直接加配置就行。

3. 核心功能拆解:从登录到下单的完整链路

3.1 微信登录与小程序的静默登录机制

小程序的登录逻辑很多人第一次做会搞混。它不是用户名密码登录,而是调wx.login获取一个临时code,然后由后端拿这个code去微信的接口换openid和session_key。拿到openid之后,去用户表查,查不到就说明是新用户,自动注册一个;查到了就直接走登录流程,颁发一个属于自己的登录凭证。

登录凭证我用了JWT。为什么不用传统Session?因为Session是存储在服务器内存里的,小程序端和后端如果将来要水平扩展成多个实例,Session同步就成了麻烦事。JWT把用户身份信息放进令牌本身,后端只要验签通过就信任身份,天然适合前后端分离和集群部署。签发JWT时我在payload里放userId和用户角色,设置7天有效期,每次请求拦截器里解析令牌并放行。

中间有个网关层要注意的:小程序的wx.request可以自定义header,所以前端请求时在header里放token字段,后端写一个HandlerInterceptor来统一校验,不要在每个Controller里手写判断,否则代码会非常冗余。

3.2 手机号绑定流程

用户在小程序里点登录后,还需要绑定手机号,方便商家联系。微信小程序获取手机号的方式不是让用户手动输入,而是通过

这里有个需要特别注意的坑:手机号快速验证组件要求小程序必须完成微信认证,否则调用不成功。个人主体的小程序是没法开通这个能力的,所以做毕设或演示项目时,常见做法是保留这个入口,同时加一个手动输入手机号的兜底方案,不然审核老师或者面试官拿到的真机演示会卡在登录这步。这个细节我在上线前就被卡过一次,后来补了兜底逻辑才顺利过审。

3.3 菜品浏览与缓存策略

用户端首页需要展示分类和对应菜品,接口设计上我拆成了两个接口:一个查分类列表,一个根据分类ID查菜品列表。这样用户切换分类时不需要重新拉全部分类数据,只需要按分类查询菜品,请求量更小。

缓存设计上,key的粒度是按分类ID区分的。菜品更新是低频操作,但查询是高频操作,所以缓存策略是“查询时缓存缺失才回填”,而不是启动时就把全量菜品预加载进Redis。另外商家端在新增、修改、起售停售菜品时,都要记得清理该分类下的菜品缓存,这是整个缓存方案里最容易遗漏的一环。我给这套方案做了个简单的约定:所有涉及菜品写操作的接口,统一调用一个清理缓存的方法,避免散落在各业务代码里。

3.4 购物车与订单状态机

购物车数据我存的是MySQL而不是Redis,原因很朴素:它是强业务数据,用户可能中途退出,下次还要继续看到;放Redis里还要解决持久化和恢复问题,直接用表存反而简单。购物车表以userId和dishId做唯一约束,添加时如果已存在就直接加数量。

订单模块是整个系统里最复杂的,核心是状态机。我设计了以下几个状态:待支付、已支付待接单、已接单配送中、已完成、已取消、退款中。每个状态之间的流转都通过后端接口控制,不允许前端直接乱跳。比如待支付订单在超时后需要自动关闭,我用的是定时任务轮询未支付订单,超过15分钟就将状态改为已取消并回滚库存。如果你不想引入消息队列,这种基于定时任务的方案完全够用。

3.5 支付流程与回调处理

支付环节对接的是微信支付的能力。流程是这样:用户提交订单后,后端调用微信下单接口拿到支付参数(主要是prepay_id),前端wx.requestPayment拉起收银台,用户输入密码完成支付。微信服务器随后会异步回调后端配置的通知地址,后端收到回调后校验签名、核对订单金额,再把订单状态改成已支付。

这里最容易被忽略的是幂等处理。微信支付回调可能不止一次到达,如果后端不判断状态就直接改订单,会出现重复入账的问题。所以我在回调方法里第一件事就是先查订单当前状态,如果已经不是待支付,直接返回成功,不再重复处理。支付金额校验也不能跳过,回调里带过来的金额要和数据库订单金额一致才能更新状态,防止支付结果被篡改。

4. 数据库设计:从表结构到MyBatis-Plus实践

4.1 核心表结构设计与字段规范

这个项目的数据模型不算复杂,但很典型。梳理下来核心表有这些:用户表、分类表、菜品表、套餐表、购物车表、订单表、订单明细表、地址簿表、管理员表。每张表的关键字段都遵循一套统一约定:主键id使用bigint自增;create_time和update_time用datetime记录创建和更新时间;逻辑删除用status字段或deleted字段,而不是物理删行。统一字段规范的好处是后续写通用的公共字段自动填充功能时,可以一次性处理,不需要每张表单独适配。

设计时有个容易忽略的地方:订单表和订单明细表必须分离。一个订单对应多个菜品,明细表里存下单时的菜品快照,包括菜品名称、价格、数量。为什么是快照?因为菜品表里的价格和名称随时可能被商家修改,但用户订单产生后的历史数据不能被影响。很多表结构设计上的问题,都是从业务约束倒推出来的,而不是靠凭空想象。

4.2 利用MyBatis-Plus实体类快速生成建表SQL

很多朋友问,用MyBatis-Plus时,表结构和实体类的关系是手写SQL还是生成好的?我的经验是,直接用实体类反向生成SQL更高效,尤其是项目原型阶段。

MyBatis-Plus提供了代码生成器,但我这里想说的是另一个口径:通过实体类上的注解来辅助建表。将字段约束用注解标记清楚,TableId和@TableField标注主键和字段映射关系,字段名统一驼峰转下划线。有了清晰的实体类,再借助数据库的建表语句去生成DDL,能极大降低遗漏字段的概率。实际动作上,可以在工程里写一段JUnit测试或独立Main方法,遍历实体类字段,动态拼接CREATE TABLE语句,这是一个很实用的小技巧。

拼接时要注意字段类型映射:Integer对应int,Long对应bigint,String对应varchar并指定长度,BigDecimal对应decimal,LocalDateTime对应datetime。列名默认是驼峰转下划线,所以Java里的createTime在表里是create_time,这个映射关系MyBatis-Plus会在SQL执行时自动处理,但你建表时得建对,否则后续查出来全是null。

4.3 订单查询的复杂SQL实践

订单模块里有个很典型的多表关联场景:分页查询订单列表,同时需要展示订单里的菜品名称。菜品在明细表里是多行,所以不能直接在订单表上联查,而要在查出订单列表后,再用订单ID集合去批量查明细。这种N+1问题的解法是“先查主表分页,再根据主表ID批量查明细,最后在内存里组装”,避免在循环里逐条查数据库。

MyBatis-Plus的Page对象配合自定义XML写联表SQL也是常用组合。列表查询通常带关键字、状态、时间区间这几个条件,XML里用 标签动态拼接,参数不为空才追加条件,这样一套模板基本覆盖所有列表页的查询需求。注意排序字段统一用order_time DESC,因为运营经常要看最新订单,索引也要建在order_time上,否则数据量上来后分页查询会越来越慢。

5. 开发起步:从空工程到第一个接口跑通

5.1 后端工程初始化步骤

搭建后端工程我用的是Spring Initializr的方式,直接在IDEA里新建项目,选择Spring Web、MySQL驱动、Validation依赖,然后把MyBatis-Plus和JWT的依赖手动加进pom.xml。版本上我固定用Spring Boot 2.7.18,MyBatis-Plus用3.5.3,这两个版本组合是经过大量项目验证过的稳定搭配,不要随便升大版本,切记。

配置文件的组织方式是application.yml里放通用配置,部署时用application-prod.yml覆盖生产环境的数据库地址和密码。这里有个实用技巧:不要在yml文件里写死密码,用环境变量占位符${DB_PASSWORD}去引用,部署时在服务器环境变量里配置真实密码,这样代码仓库即使泄露也不会直接把数据库密码一起暴露。

启动类上要加@MapperScan注解,指定mapper接口所在的包路径,不然MyBatis-Plus扫描不到你写的Mapper接口,启动时不会报错,但一调接口就会告诉你NoSuchBeanDefinition。这一步是新手最常见的启动“假正常”问题。

5.2 用Swagger/OpenAPI快速生成接口文档

前后端分离开发时,接口文档是协作的契约。我在这套项目里集成了Knife4j,它是Swagger的增强版,直接通过注解在Controller上描述参数和返回结构,自动生成可调试的API页面。开发阶段的价值非常大:小程序前端只需要对着API页面看参数名和类型,就能自己联调,不需要后端随时口头解释。

Controller的注解规范建议统一:每个接口写明@ApiOperation描述用途,每个参数用@ApiParam标注含义,返回的统一结果对象Result 里包含code、msg、data三部分。这套约定贯穿整个项目好处很大,接口页面一目了然,前端同学看文档效率能翻倍。

5.3 小程序端骨架与导航栏适配

小程序工程初始化后,我第一时间做的是封装请求工具。所有wx.request调用统一封装成request方法,自动拼接baseURL,自动从storage里取出token放到header里,响应统一拦截处理code,非0状态直接跳到错误提示。这样业务页面里不需要重复写这些公共逻辑,代码干净很多。

页面结构上,tabBar配置了首页、购物车、订单、我的四个主页面,对应外卖App里最常见的四个主场景。首页涉及分类左侧栏、菜品列表右侧栏这种双列联动布局,其实是两个scroll-view配合联动滚动,分类点击时右侧滚动到对应区块,右侧滚动时左侧自动高亮当前分类。代码逻辑并不复杂,但滚动计算要对齐,否则会出现滑动时左侧高亮乱跳的情况。

自定义导航栏是这个项目前端最需要花时间适配的点。最好是调wx.getWindowInfo获取状态栏高度,再根据胶囊按钮位置算出导航栏高度,按照这套动态值去占位渲染,不要写死。不同机型的适配效果直接影响用户对小程序的体验观感,做一次一劳永逸。

6. 部署实操:从开发机到云服务器的完整路径

6.1 服务器环境与基础软件清单

部署前先列清楚服务器需要安装的软件清单。我用的是一台2核4G内存的云服务器,操作系统是CentOS 7,部署需要的基础软件包括:JDK 1.8、MySQL 8.0、Redis 6.x、Nginx。软件装齐之后,第一件事是修改MySQL的root密码并创建项目专用的数据库和账号,不要用root账号连业务库,这是安全底线。Redis默认只绑定127.0.0.1,如果项目和服务在同一台机器上,保持默认绑定就好,千万不要开着外网访问直接裸奔。

如果你的服务器性能更宽裕,也可以考虑用Docker来部署这些组件。Docker的好处是环境隔离,MySQL、Redis、Nginx各跑一个容器互不干扰,出问题直接删容器重建即可,配置也通过挂载目录放在宿主机上。但对刚接触部署的朋友来说,直接用yum和tar包安装排错更直观——容器化之后问题会多一层排查维度,反而不利于理解底层。

6.2 后端项目打包与JAR包运行

后端打包直接用Maven命令执行clean package,跳过测试用-DskipTests,打出JAR包后上传到服务器的指定目录,比如/opt/sky。启动命令我推荐用nohup方式后台运行,同时指定JVM内存参数,避免默认堆内存过大导致2G内存的服务器直接顶不住。2C4G机器上我用的参数是-Xms512m -Xmx1024m,对这套系统完全够用。

启动后第一件事不是立刻测接口,而是去看日志。日志文件用nohup.out就能先顶一阵子,但生产环境建议用logback配置按天滚动归档,保留最近30天。遇到启动失败先看是端口被占、数据库连不上还是Redis连不上,这三个顺序排下来基本能解决九成问题。

6.3 Nginx配置与前端资源发布

后端起来之后,Nginx的配置核心是两块:反向代理和静态资源托管。反向代理部分只需要把/api前缀的请求转发到本地8080端口,同时设置proxy_set_header头传递真实IP;静态资源部分把Web管理端构建后的dist目录作为站点根目录,直接提供服务。

这里要给新手提个醒:前端调用后端接口时,不能把后端地址写成localhost,也不能写内网IP。上线后必须通过域名的/api/路径去访问,所有请求都在同一个域名下,跨域问题天然消失。所以开发阶段前端的baseURL要设计成环境变量,在部署阶段替换成线上域名。

6.4 微信小程序发布流程

小程序端开发完成后,在微信开发者工具里点击上传,填好版本号和备注,就会提交到微信公众平台的后台。然后在后台“版本管理”里将上传的版本选为体验版,先用体验版在真机上完整走一遍流程,确认登录、支付、下单都没问题后,再提交审核。审核通过后,点“全量发布”即可上线。

审核期间有个点容易被卡:如果你的项目里用了“测试”或“demo”字样,或者页面里有明显的开发调试信息,都会被拒绝。正式提交前把页面里所有测试数据清干净,登录入口要真实可用,不要留“敬请期待”之类的占位页。另外微信小程序要求所有涉及用户信息的接口必须走合法合规的授权组件,不能私自拿用户openid去当账号,这个也是审核红线。

7. 试过才知道的坑:问题排查与避坑技巧

7.1 启动期的疑难报错与排查顺序

这个项目的新手期报错,我总结下来有三个高频点。第一个是Mapper接口扫描不到,报Invalid bound statement (not found),原因是@MapperScan没加或者包名不对,检查启动类和Mapper包路径是否一致就好。第二个是数据库连接失败,报Communications link failure,多半是连接串的地址、端口、时区配置有误,在jdbc的url里把serverTimezone设为Asia/Shanghai能顺带解决时间差8小时的问题。第三个是Redis连接被拒,先确认Redis进程是否存活,用的是云Redis还要检查白名单和密码配置。

排查问题的顺序建议是:先看异常栈最下面的Caused by,这是真正的根因,不要盯着最上面那一大段框架调用栈浪费时间。前端联调时接口报401或403,先看我方token解析逻辑输出了什么,再去怀疑前端。这条规则帮我省了大量的排查时间。

7.2 订单超时未支付的处理细节

订单超时关单这个需求,我最初用的是Spring的@Scheduled定时任务,每30秒扫一次待支付订单,超15分钟就关单。这样做简单,但要注意三个细节:定时任务默认是单线程串行执行的,如果扫描SQL写得慢,后面任务会堆积;订单表要建立status和create_time的联合索引,否则扫描会越来越慢;关单时要把库存回滚,并且做好重复关单的幂等判断。

实际运行中还有一个边界情况要考虑:用户在第14分59秒完成了支付,但定时任务在此之前已经把订单关了。这种情况虽然没有在代码里刻意去避免,但我在支付回调里加了一个判断,如果订单状态是已取消但支付成功,走退款逻辑;如果订单是待支付,就正常转已支付。业务逻辑不闭环的地方,要通过兜底逻辑去接受现实世界的不完美。

7.3 性能优化与体验打磨

项目上线后面临的第一个性能瓶颈几乎都在数据库。菜品缓存能扛住用户端的查询,但商家的订单管理列表页会经常按时间、状态做复杂条件查询,如果慢查询出现,优先看执行计划是否走了索引。我给订单表加了(status, create_time)联合索引之后,列表页查询时间从700毫秒降到50毫秒左右,这个优化性价比是最高的。

小程序端的体验优化,重点是首屏加载。首页的各分类菜品接口请求数量多,如果全部并行发出,弱网环境下会有大量请求阻塞。我做了两个优化:首页接口合并,一次请求返回聚合数据;静态图片资源上传到CDN加速,而不是直接放小程序服务器。这两个动作做完后,小程序的冷启动和首屏渲染速度提升非常明显。

7.4 安全加固不能省

部署上线后,安全这块不能省。我做了三个最基本的加固:Nginx层封禁非业务端口的外网访问,只放通80和443;MySQL创建专用账号,权限只给项目数据库的所有表;所有对外接口统一做登录拦截和权限校验,管理端接口额外校验管理员角色。哪怕只是一个学习项目,把这些习惯养成,对你以后进企业写代码也是加分项。

关于密码和密钥管理,再强调一次:任何密钥都不能提交到Git仓库里。微信小程序的AppSecret、数据库密码、支付商户证书,这些要么用环境变量,要么用配置中心管理。我之前见过不止一个项目因为把密钥提交到代码仓库导致数据泄露,这是完全可以避免的低级事故。

8. 一些心里话

这个项目做完,我自己最大的感受是:教程项目最大的价值不在于某个技术点有多深,而在于它逼着你把链条上的每一环都走了一遍,从需求拆解、表设计、接口实现、联调到上线。很多东西看文档觉得懂了,实际操作时才会发现缺了哪一环。如果你正在学Java,我建议你按这个顺序踏踏实实走:先把这张项目涉及的表结构和接口关系梳理清楚,再动手写代码,最后部署上线。过程中产生的所有报错和调试记录,都是你面试时可以讲的实战素材。

另外如果你打算把这个项目作为求职项目,有一点建议:不要只讲你做了什么功能,要多讲你在做某件事时遇到了什么问题、为什么这么解、有没有对比过其他方案。能讲清楚这些,比简单地展示代码量更能体现你的工程能力。

部署一次之后,这个项目后续还有很多扩展空间,比如把定时关单改成延迟队列实现、点击统计接入数据分析、复制一套给不同商户多租户化,这些都是可以继续深挖的方向。先把第一版稳定跑起来,你会对Java全栈开发有一个完整而踏实的认知。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询