做微信小程序点餐系统这件事,我前后完整跟过两个项目,一个是帮朋友的小餐厅做扫码点餐,另一个是做成完整交付给学生用作毕业设计的版本。标题里“源码+文档+调试”三个词,其实是三个完全不同的交付维度,但很多人只盯着源码看,最后都卡在文档和调试上。这篇文章我就拿这个项目当例子,把从需求分析到架构设计、从前端页面到后端接口、从调试部署到避坑记录,全部串起来讲一遍。如果你正准备做类似的项目,或者想拿它练手接单,这篇文章能帮你省下大量自己摸索的时间。
1. 项目全景拆解:需求线、技术线、交付线三线并行
1.1 这个系统到底在解决什么场景问题
先搞清楚一件事:点餐系统不是“把菜单放到网上”这么简单。它要解决的是餐饮门店里三个角色的不同痛点——顾客不想排队等服务员点餐,服务员不想来回跑着记单,老板不想每天手工对账。这三个痛点对应到系统里,就是三条明确的功能主线。
顾客端要的是“看到菜单、加购物车、下单支付、查看订单状态”,全程不需要跟服务员说一句话。服务员或者后厨要的是“接到新订单、确认接单、标记制作、通知出餐”,让整个流程有据可循。老板要的是“管理菜品、上下架商品、查看每天的流水和热销菜品”,用数据代替拍脑袋。所以这个项目表面上看是一个微信小程序,实际上是一个完整的餐饮门店数字化解决方案,只是入口选在了微信生态里。
微信小程序作为入口,最大的优势是免下载、免注册、用完即走,对顾客来说学习成本极低。扫码或者搜一下就能打开,点完餐直接微信支付,这跟线下餐厅的场景匹配度非常高。对比一下H5网页点餐,小程序在调用微信支付、获取用户身份、消息订阅提醒这些环节上要顺畅得多,这也是我建议用小程序而不是H5的核心原因。
1.2 技术选型:为什么是小程序原生加Spring Boot
选型这件事,我见过太多人一上来就纠结。有人想用uni-app一套代码多端复用,有人想上微服务,有人非要前后端分离再搞个独立的Vue管理后台。我个人的态度很明确:做点餐系统这种体量的项目,技术栈越稳越简单越好,先把业务跑通,再谈架构升级。
前端我选微信小程序原生框架,不引入额外的跨端框架。原因有三条:第一,原生框架对微信API的封装最直接,像wx.login、wx.requestPayment这些核心接口,原生环境下调用最省事;第二,原生框架的调试体验最好,开发者工具里能直接看到网络请求、Storage操作、组件渲染状态;第三,这个项目的高频操作是列表滚动、图片加载、购物车数据同步,这些用原生组件就能很好处理,不需要虚拟DOM那一套。
后端选Spring Boot,配MyBatis Plus操作MySQL,用Redis做缓存和会话管理。这套组合在中小型项目中是绝对的“稳定区”,网上资料多、出问题能查、部署也简单。认证方案用JWT——用户在小程序端登录后,后端签发一个token,后续所有接口都带上这个token来识别身份。具体到项目里是knife4j生成接口文档,配合统一的响应结果封装类Result,前端后端对接的时候非常省心。
管理后台这一块,我没有单独再做一个Vue项目,而是让管理功能直接复用后端的接口,用微信小程序的“管理端”入口承载。这样做的好处是部署成本极低——只需要跑一个小程序后端、一个MySQL、一个Redis,不需要再维护一个独立的Web管理端。如果后面真需要PC端后台,接口都是现成的,扩展不费力。单项目单部署,对新手维护最友好。
1.3 数据库设计:6张核心表一次讲清
数据库设计是整个项目的根基,表结构如果设计得别扭,后面写代码能烦死。我最终沉淀下来6张核心表,基本覆盖了点餐系统的所有核心业务,这里直接列出来供参考。
用户表user:字段包括用户ID、openid(微信唯一标识)、昵称、头像、手机号、余额、积分、创建时间。openid要加唯一索引,这是用户体系的根。
分类表category:字段包括分类ID、分类名称、排序号、状态。比如“热菜”“凉菜”“主食”“饮料”,排序号用来控制前端菜单栏的展示顺序。
菜品表dish:字段包括菜品ID、所属分类ID、菜品名称、描述、图片URL、价格(单位用分)、月销量、库存、状态(上架/下架)。价格用分存储,避免浮点数精度问题,这个细节很重要。
购物车表cart:字段包括购物车ID、用户ID、菜品ID、数量、勾选状态。用户加购物车后,数据先存本地Storage,登录后同步到服务端,防止换设备丢数据。
订单表orders:字段包括订单ID、订单号、用户ID、订单金额、实付金额、支付状态、订单状态、备注、创建时间、支付时间。订单状态用数字表示:0待支付、1待接单、2制作中、3待取餐、4已完成、5已取消。
订单明细表order_item:字段包括明细ID、订单ID、菜品ID、菜品名称、菜品图片、单价、数量。设计成“订单+明细”两张表,是为了支持一个订单里包含多个菜品,并且要在订单详情页完整还原用户当时点的东西——所以菜品名称和图片要冗余存储一份,不能只存菜品ID,否则菜品改名或删除了,历史订单就乱了。
除了这6张核心表,还可以根据业务需要加一张地址表address,用于外卖配送场景的收货地址管理。整体的建表SQL、索引设计、字段注释,在完整文档里都有,这里先把核心结构讲清楚,后面对照着看代码就不会一头雾水。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 用户端实操拆解:从微信登录到下单支付
2.1 微信登录与鉴权:一次code换一个token
微信小程序的登录机制,核心就一句话:前端通过wx.login拿到临时code,后端拿这个code去微信服务器换openid,然后用openid作为用户唯一标识,签发自定义token给前端。很多初学者在这里犯迷糊,把code当成了可以重复使用的凭证——实际上code有效期只有5分钟,而且只能用一次,用完就作废。
前端登录的那段代码非常固定,基本每次都是在onLoad里调用:
javascript复制// app.js 或登录页面的核心逻辑
wx.login({
success(res) {
if (res.code) {
wx.request({
url: 'https://api.example.com/user/login',
method: 'POST',
data: { code: res.code },
success(response) {
wx.setStorageSync('token', response.data.data.token);
wx.setStorageSync('userInfo', response.data.data.userInfo);
}
});
}
}
});
后端收到code之后,要调用微信的code2Session接口,把code换成openid和session_key。这里的关键点在于:不要把openid直接返回给前端。openid是用户在小程序生态里的唯一身份证,如果暴露给前端,有被恶意利用的风险。正确做法是把openid作为查询条件,从数据库查出或创建用户记录,然后签发一个JWT token返回给前端。后续接口只需要校验token,不需要再跟微信服务器打交道。
token的过期时间我建议设置成7天,并且在小程序每次启动时检查本地storage里的token是否存在。如果存在就静默请求一次用户信息接口,检测token是否有效;如果接口返回401,就重新走一遍wx.login流程。这样用户感知不到登录过程,体验顺畅,后端也不会频繁地签发新token。
拦截器是后端鉴权的核心。Spring Boot里的实现方式是写一个HandlerInterceptor,在preHandle方法里从请求头取token,解析失败或过期就直接返回401,不进入controller。要注意的是,登录接口、支付回调接口要加白名单,否则回调被拦截器挡在外面,支付状态就永远更新不了。
2.2 菜单浏览与购物车:本地缓存加服务端同步的组合策略
菜单浏览这部分,技术上没有什么高难度,但有个实操细节值得注意:菜品图片的加载性能直接决定用户对系统的第一印象。小程序端图片组件建议开启lazy-load属性,列表页只保留首屏的图片加载优先级,后端返回菜品列表时也要控制返回字段,不要把菜品描述这种大段文本一股脑返回。
购物车的设计是个经典问题——纯前端存储还是纯后端存储?我采用的是“本地优先、服务端同步”的组合策略。用户加购物车的瞬间,数据写入本地Storage,页面立刻更新,不需要等待网络请求,体验上零延迟。同时异步把购物车数据提交给后端,后端存进购物车表。这样即使清缓存或者换设备,登录后购物车数据也能同步回来。
具体实现上,购物车存储在Storage里的结构是数组,每一项包含菜品ID、名称、价格、图片、数量、勾选状态。加购时要做的判断是:如果同一个菜品已经存在,就把数量加一;如果不存在,就push一条新记录。这个逻辑看起来简单,但如果没处理好“本地数据和服务端数据的合并策略”,会出现重复加购的问题——用户在A页面加了一次,到B页面又加了一次,结果同步时数量翻倍。解决办法是同步时按菜品ID去重,以服务端已有的菜品ID为准,把本地数量叠加到服务端对应记录上。
购物车的角标数字是用wx.setTabBarBadge来实现的,在每次购物车数据变化时重新计算总数量。这里有个优化细节:不要在每次加购操作时都调用一次setTabBarBadge,而是用一个防抖函数,300毫秒内连续操作只更新一次,避免频繁调用小程序API造成卡顿。
2.3 下单与微信支付:按分计算的细节
下单接口是所有接口里逻辑最重的,它要做的动作包括:校验用户购物车是否为空、锁定菜品库存、生成订单号和订单明细、计算订单总金额、清空本地购物车、调用微信支付统一下单接口。这些操作一定要放在同一个事务里,任何一个环节失败都要回滚,否则会出现“订单创建成功但库存没扣”或“库存扣了但订单不存在”这种数据不一致的问题。
订单号的生成规则建议做成yyyyMMddHHmmss + 6位随机数 + 用户ID后4位,总共20位左右。为什么不用数据库自增ID?因为订单号直接暴露给用户,自增ID会让竞争对手根据订单号估算出你的日订单量,存在商业风险。时间戳加随机数的方式在业务层面有唯一性保障,如果实在不放心,可以再加一个数据库唯一索引兜底。
金额计算这里我要单独强调,所有价格字段在后端必须用整数分存储和计算。菜品表里的价格unit_price是分,购物车明细里的价格也是分,订单金额的计算全部基于分做加法。前端展示时再除以100转成元,用Number.parseFloat((price / 100).toFixed(2))做一次格式化。千万不要用float或者double直接存价格,你会被精度问题逼疯的。
调用微信支付统一下单接口时,后端需要准备这些参数:小程序appid、商户号mch_id、随机字符串nonce_str、订单号out_trade_no、订单金额total_fee(单位分)、终端IP、回调通知地址notify_url、交易类型trade_type=JSAPI、用户openid。其中最关键的一个参数是sign签名——微信要求把所有参与签名的参数按照字典序排序,拼接成键值对字符串,然后加上商户密钥key,用MD5做加密,生成32位大写字符串。这里容易出的问题是:参数排序用的字段名必须和文档完全一致,一个字母都不能差。
支付回调是整套支付体系里最容易踩坑的环节。微信服务器会异步POST请求你的notify_url,通知支付结果。后端要做的第一件事是验签——用同样的签名算法验证这个回调是不是微信官方发来的。验签通过后,还要做防重处理:查询订单当前状态,如果已经是“已支付”,就直接返回成功,不做重复更新。回调处理完必须给微信返回一个固定格式的XML字符串<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>,否则微信会认为回调失败,隔一段时间继续重试。
支付成功后的前端轮询逻辑也要设计好。用户在wx.requestPayment的回调里拿到支付成功标记后,不要立刻跳转订单页,而是延迟1.5秒左右再跳转,给后端留出处理回调的时间。如果用户支付成功后立刻杀掉小程序,订单状态没有及时更新,也不用担心——后端回调迟早会到,下次打开小程序时查询订单状态会发现已经是已支付。
3. 商家管理端:订单流转与菜品维护的核心逻辑
3.1 菜品管理:分类、上下架与库存
商家管理端如果走小程序入口,核心页面就是菜品管理和订单处理两个大块。菜品管理页面要提供分类维护、菜品列表、新增/编辑菜品、上下架开关、库存调整等功能。这里的交互逻辑跟用户端不一样,管理端更看重的是操作效率——一个菜品下架之后,用户端要能立刻看不到,不需要重启服务或者刷新缓存。
菜品上下架功能的实现,后端就是一个更新状态的接口,把dish表的status从1改成0。但要注意,上下架之后再配合一个缓存刷新策略,否则用户端看到的数据还是旧数据。我在项目里用的方案是:菜品列表接口先查Redis缓存,如果缓存不存在再查MySQL并回写到缓存。上下架接口更新MySQL成功后,删除对应的Redis缓存key。这样下一次查询会重新从MySQL加载最新数据写入缓存,逻辑简单也不容易出错。
库存管理这块,要区分两种场景。一种是有实库的餐厅,比如食材是有限的,点完就没了;另一种是无限库存的虚拟菜品,比如线上门店线下的库存是分离的。我做的系统里给菜品加了一个stock字段,默认为-1表示无限库存,大于等于0表示有限库存。下单时扣库存的SQL是这样的:
sql复制UPDATE dish SET stock = stock - 1 WHERE id = #{dishId} AND stock > 0;
这行SQL的含义是:只有当前库存大于0时才允许扣减,同时把库存减1。对于并发场景(两个人同时点最后一个菜),数据库自身会保证这个更新操作的原子性——其中一个请求会拿到0条受影响行数,说明库存不足,这时候就提示用户“该菜品已售罄”。这是最简单的乐观锁用法,不需要引入分布式锁就能解决绝大部分超卖问题。
3.2 订单处理流程与状态机设计
订单状态流转是整个商家端的灵魂。如果把订单做成一个状态机,它的合法流转路径是这样的:待支付只能变成已取消或待接单,待接单只能变成制作中或已取消,制作中只能变成待取餐,待取餐只能变成已完成。这个约束关系可以在后端用一个枚举类或者状态流转表来维护,非法流转直接拒绝。
商家端的订单列表页面,要支持按状态筛选,分别是待接单、制作中、待取餐、已完成、已取消。入口位置建议默认打开“待接单”页签,因为这是商家每天处理的最核心请求。每个订单卡片上要展示:订单号、下单时间、用户昵称、菜品清单(带数量)、总金额、备注、操作按钮。
操作按钮的逻辑要跟状态机一一对应。待接单的订单显示“接单”按钮,点击后订单状态从1变成2,同时可以给用户推送一条订阅消息(到店取餐小程序可以用wx.requestSubscribeMessage)。制作中的订单显示“出餐”按钮,点击后状态从2变成3,表示餐品已经放到取餐台。这个按钮的设计背后其实是门店动线的模拟——接单代表后厨知道要做菜,出餐代表用户知道可以来取了。
整个流程中还要考虑异常情况。用户下了单又申请退款,商家端要做“同意退款”操作,退款走微信支付的退款API,具体是调用refund接口,参数包括商户订单号、退款金额(要小于等于原订单金额的整数分)、退款原因。这里有个坑要注意:微信支付退款接口需要用到商户证书,证书文件要放在后端的resource目录或者配置中心里,不能在代码里硬编码证书路径。
3.3 简单但够用的日经营统计
老板视角最关心的统计数据就三个:今天卖了多少单、今天收了多少钱、哪些菜卖得最好。这三个数据对应的SQL非常简单,但不建议每次老板打开统计页面都实时全表扫描订单表。我的方案是写一个定时任务,每天晚上12点跑一次,把当天的订单数、营业额、热销菜品TOP10写入一张统计表daily_report。管理端打开统计页面时,查这张表就够用了,查询速度极快。
热销菜品排名的计算逻辑也不复杂,就是从order_item表按菜品ID分组,统计每个菜品的销售数量和销售金额,排序取前10。定时任务要兼容当天没有订单的情况,给营业额的默认值填0,否则报表里出现空行会让老板觉得系统坏了。
如果需要更细致的分析,比如按小时维度统计下单分布,可以在订单表加一个下单小时字段,然后按小时分组。这个数据对餐厅排班很有参考价值——哪几个小时是高峰,哪几个小时完全可以少招人,一清二楚。统计报表不用做得很花哨,纯文本列表加几个数字,老板看得懂就够了。
4. 调试部署与真实避坑记录
4.1 本地调试环境搭建的三个关键点
本地开发调试这个项目,第一步要解决的就是环境依赖——JDK、Maven、MySQL、Redis、微信开发者工具,一个都不能少。我建议先把后端跑起来,再启动小程序前端,顺序不要反过来。后端启动时注意MySQL要提前建好库,配置文件里的数据库连接、Redis地址要改成你自己本地的。
微信开发者工具导入项目时,有三个关键配置容易踩坑。第一,AppID必须用真实的小程序AppID,不能选“测试号”——测试号不支持支付功能,很多接口行为也和真实环境不一致。第二,如果不校验合法域名的话,本地开发时通过勾选“不校验合法域名”临时跳过域名校验,但上线前一定要把这个勾去掉,改成真实的白名单域名。第三,要在开发者工具的详情里确认项目配置的ES6转ES5是打开的,某些低版本安卓机不转ES5会出现白屏。
后端接口的联调过程中,强烈建议先用knife4j或Postman把接口全部测通,再去对接小程序端。这样能快速定位问题到底出在前端还是后端——如果你在小程序里调不通接口,但是同样的请求在Postman里能返回数据,那问题一定在小程序端的请求封装或者参数格式上。
4.2 常见问题速查表与排查思路
这里我整理一份点餐系统开发中最高频的10个问题,对照排查基本能解决掉90%的线上故障。
| 问题现象 | 根本原因 | 解决办法 |
|---|---|---|
| wx.login返回的code无效 | 同一个code被使用两次 | 确保每次登录只用一次code,5分钟内有效 |
| 请求接口返回401 | token过期或未携带 | 检查请求头Authorization字段,过期后重新走登录流程 |
| 前端请求报域名不合法 | 小程序后台未配置request合法域名 | 在小程序管理后台添加HTTPS域名,且域名已备案 |
| 支付调用报签名错误 | 参数排序或字段名拼写错误 | 对照官方文档检查参与签名的字段,注意参数名大小写 |
| 支付成功后订单状态未更新 | 回调通知被防火墙拦截或验签失败 | 检查notify_url公网可达性,检查回调日志 |
| 商品显示已下架但用户仍能看到 | Redis缓存未清除 | 上下架接口同步删除菜品缓存key |
| 两个用户同时下单库存超卖 | 扣库存没有原子操作 | 使用UPDATE dish SET stock=stock-1 WHERE id=? AND stock>0 |
| 订单创建一半失败产生脏数据 | 事务没有加或回滚不完整 | 在下单service方法上加@Transactional注解 |
| 用户端图片加载慢或加载失败 | 图片域名未加入downloadFile白名单 | 在小程序后台配置downloadFile合法域名 |
| 定时任务重复执行生成了多份统计表 | 多个实例同时跑定时任务 | 在定时任务上加分布式锁或单实例部署执行任务 |
这些问题的排查顺序有个通用的方法论:先看前端报错信息,再查后端日志,最后查数据库数据状态。小程序端的报错可以通过vConsole工具打开日志面板,后端的报错直接看Spring Boot的日志输出,数据状态用Navicat连上MySQL查一眼就知道有没有动过。
4.3 上线部署时最容易翻车的5件事
项目本地调试通过只是第一步,真正上线部署的过程才是检验功力的时刻。我列这5个翻车点,每一个都是我亲眼见过或者亲手踩过的。
第一,HTTPS证书没配好。微信小程序要求所有网络请求必须走HTTPS,而且证书必须是有效且受信任的。我遇到过有人用自签名证书部署,本地调试通过,一上真机就全部请求失败。解决办法是别折腾,直接买正规的DV证书或者用云服务商提供的免费证书,配置好Nginx做TLS终结。
第二,回调地址写成了内网IP。微信支付的回调通知服务器必须能通过公网访问到你的notify_url。如果你部署在本地联调,可以用内网穿透工具临时拿到一个公网域名来调支付流程,但生产环境一定要填公网域名,并且这个域名能和Nginx对上。
第三,MySQL和Redis没有配置密码,或者密码太简单。线下开发环境无所谓,但一旦部署到公网,扫描机器人分钟级就能把裸奔的数据库攻破。安全基线要做到MySQL使用强密码、Redis开启认证并且不对外暴露6379端口,通过bind 127.0.0.1或者防火墙隔离公网访问。
第四,图片资源没有走CDN。餐厅的菜品图片都是大图,如果全压在服务器带宽上,用户一多加载就很慢。建议图片上传到对象存储,加一层CDN加速,小程序端通过带签名的URL访问。这个方案成本低,带来的体验提升非常明显。
第五,日志没有落盘和滚动清理。后端日志如果不做分文件和定时清理,几个月后磁盘会被日志占满,服务直接崩溃。Spring Boot里配置logback,按天滚动日志文件,保留30天,超过自动清理,这个习惯要养成。
5. 源码之外的进阶扩展方向
5.1 扫码点餐与桌台绑定
拿到一套基础的点餐系统源码之后,最值得做的第一个扩展就是“扫码点餐+桌台绑定”。这个功能在真实餐厅场景中需求度极高——顾客入座后扫桌上的二维码,小程序直接打开并自动带上桌台ID,点餐下单后无需取餐号,服务员直接按桌台上菜。
桌台绑定的实现思路是在小程序中通过scene参数携带桌台ID。餐厅把每个桌台的小程序码打印出来贴在桌上,顾客扫码进入小程序时,可以在onLoad的options参数里通过scene字段解析出桌台ID。后端接收这个桌台ID并关联到订单表,查询订单时就可以按桌台维度过滤。这里要注意,桌台码最好不要用普通二维码,而是用微信官方的getUnlimited接口生成小程序码,这样可以控制码的样式和有效期。
有了桌台ID之后,订单状态机里还可以增加一个“呼叫服务员”的能力,实际上就是在订单上加一个备注字段:用户点“呼叫服务”按钮后,后端通知商家端的接口,让管理端页面上弹出一个提示条。整个链路不用装任何硬件,纯软件就能实现,成本几乎为零。
5.2 后厨打印、会员卡与经营报表
第二个值得做的扩展是接云打印机。小餐厅后厨不需要大屏幕,有一台云打印机就够了。用户支付成功后,后端调用打印机的云API,把订单内容推给打印机自动出票。出票模板要包含订单号、桌台号、菜品明细、备注、下单时间。这里有个细节要注意:打印机出票是在支付回调里触发的,回调处理要保证幂等性,不能让同一条订单重复打印两次。
第三个扩展方向是会员体系。小程序天然支持绑定用户身份,可以加充值赠送逻辑,比如充100送10、充300送40。充值金额存入user表的balance字段,支付时可以选择余额支付或者微信支付。充值赠送的金额建议做成独立的赠送余额,这样账目比较清楚。积分系统也可以做,比如下单送积分、积分抵现金,核心就是维护一张积分流水表,记录每笔积分的变动原因,方便对账。
第四个方向是把经营报表做得更专业。基础版的日统计只解决了“今天卖了多少”的问题,进阶版要解决“哪个品类贡献利润最多”“复购率是多少”“新客和熟客的消费占比”这类问题。这些分析需要更完整的数据模型,比如在订单表里记录用户的下单次数,在菜品表里记录菜品的成本价,这样毛利率才能算得出来。报表功能可以直接复用现有的管理端入口,只是数据查询的SQL要写得更精细一些。
如果后续业务量涨起来,这套单体架构也可以平滑演进。把订单模块单独拆成一个独立的服务,用消息队列来做订单和支付之间的解耦,商品模块走独立的缓存集群。但那是后话,现阶段把单体架构做到干净、清晰、可维护,就已经是一个足够优秀的项目了。
我个人在实际操作中的体会是,做这类系统最重要的不是炫技,而是把基础流程跑得足够稳。微信登录、购物车、下单支付、订单流转、菜品管理,这五个核心模块任何一个出问题,都会直接影响餐厅的正常营业。所以调试的时候一定要把异常场景一个个过一遍——用户取消订单、支付超时、库存不足、菜品下架、并发下单,这些东西在文档里写清楚,比任何花哨的功能都有价值。希望这篇文章能帮你少踩几个坑,把项目做得更扎实。
