
餐饮行业的点餐系统我前后帮人搭过好几套了。早年间用的是那种传统前后端不分离的项目JSP 页面套后端一套代码既要管后台管理又要管前台展示后来又要适配手机浏览器折腾到后面自己都嫌烦。所以当这次要做“SpringBoot 加微信小程序”的餐厅点餐订餐管理系统时我其实挺有底的这套组合算得上目前中小餐饮商家做线上化的主流方案了。市面上大量现成模板和教学项目都长这样但真正靠谱的、能跑得顺的并不多。这篇就把我实际开发这套系统的完整过程写出来从技术选型、数据库设计、后端接口实现到小程序端联调以及那些不跑一遍绝对发现不了的坑全部交代清楚。如果你正打算做毕业设计或者给亲戚朋友的餐厅搞一套点餐小程序这篇文章可以直接照着抄作业。1. 项目定位与技术选型解析1.1 为什么选择 SpringBoot 加微信小程序这个组合先说 SpringBoot。现在 Java 后端开发基本已经默认用它了原因不复杂它把 Spring 那一套繁琐的 XML 配置几乎全干掉了内嵌 Tomcat打成一个 jar 包就能跑。接手一个老项目你可能还得折腾各种配置但 SpringBoot 项目拿来就能启动这对开发效率和后期维护来说都是实打实的优势。再说微信小程序。对餐厅场景来说小程序最大的好处就是“轻”用户扫个码或者搜一下就能用不用装 App。而且微信自带支付体系用户下单后用微信支付非常顺滑省去了自己做支付网关的麻烦。对商家来说小程序后台还能沉淀用户数据配合微信的推送能力做会员营销也比较方便。技术上这套组合的成熟度也高。SpringBoot 负责提供 RESTful API小程序端通过wx.request调用后端接口。数据格式统一用 JSON前后端完全分离分工清晰。调试的时候小程序开发者工具配好后端地址就能直接联调体验比传统 Web 项目顺畅得多。提示如果你是做毕业设计这套组合也几乎是最稳妥的选择。SpringBoot 体现工程化能力微信小程序体现移动端开发能力两者结合的业务场景完整度又足够答辩时能讲的东西非常多。1.2 功能模块全景拆解这套点餐订餐管理系统我从需求层面给它拆成了三类角色用户端小程序、商家端后台管理、系统端运营维护。用户端的功能一般包含微信授权登录自动获取用户基本信息浏览菜品分类和菜品列表查看菜品详情包括图片、描述、价格、口味标签加入购物车、修改数量、删除提交订单、选择就餐方式堂食/自提微信支付或者模拟支付查看历史订单和订单状态个人信息管理、地址维护商家端的功能相对重一些我用的是 Web 管理后台菜品管理新增、上下架、改价格、维护库存分类管理菜品分类的增删改订单管理查看所有订单、处理订单状态数据看板今日订单数、营业额、热销菜品排名会员管理查看注册用户、订单统计系统端在单体项目里一般不做独立拆分直接把权限挂在管理员用户上就行。实际上开发的时候我更建议先把用户端的核心链路浏览、加购、下单、支付回调跑通再回头补管理后台的功能。因为小程序端的体验直接决定项目能不能用而后台管理功能再复杂也只是 CRUD。1.3 角色体系与权限设计权限设计这里我用的是最简单也最实用的方案——基于角色的访问控制。普通用户默认角色登录后自动创建能访问小程序端所有接口管理员固定账号在后台登录后进入管理界面能访问后台所有管理接口超级管理员配置在配置文件中理论上拥有最高权限但在实际项目中我经常把超级管理员和管理员合并处理后端实现上用拦截器HandlerInterceptor做接口层面的鉴权。普通用户请求时校验 token管理员接口校验管理员身份。项目规模不大时完全够用如果硬要上 Spring Security 加 JWT 那套反而增加复杂度。注意小程序端的用户登录状态我用的是自签发 token就是把用户 id 加个盐做签名存 Redis 设置过期时间。这个方案比 JWT 的优点是方便随时踢人下线缺点是每次请求都要查 Redis。小项目无所谓两种都可以。2. 数据库设计与核心业务模型2.1 业务表结构规划数据库我用的是 MySQL 5.7字符集选 utf8mb4因为菜品描述里经常有 emoji 表情不选 utf8mb4 会直接报错。核心表一共规划了 8 张我列一下典型的建表结构方便参考。用户表useridopenid微信用户唯一标识nickname昵称avatar头像URLphone手机号gender性别create_time创建时间菜品分类表categoryidname分类名称sort排序字段status是否启用菜品表dishidcategory_id关联分类name菜品名image图片description描述price价格stock库存sales销量status上下架状态create_time购物车表cartiduser_iddish_idquantity数量is_selected是否选中结算时有用订单表ordersidorder_no订单编号唯一user_idtotal_amount总金额status订单状态pay_status支付状态remark备注address收货/自提信息create_timepay_timefinish_time订单明细表order_detailidorder_iddish_iddish_name冗余防止菜品删除后订单没记录dish_image冗余price下单时的单价quantity管理员表adminidusernamepasswordBCrypt 加密存储create_time我特意在订单明细里冗余了菜品名称和图片这一点非常重要。因为菜品下架或者删除后如果订单去联查菜品表历史订单就会显示异常。做点餐类项目历史订单数据一定要能独立展示。2.2 订单状态机设计订单状态是点餐系统最核心的领域逻辑之一我把它设计成如下流转待支付0用户提交订单但未支付待接单1支付成功等待商家确认进行中2商家已接单开始制作待取餐/待送达3制作完成等待取餐已完成4用户确认收货或取餐已取消5用户取消或超时未支付自动取消实际开发中状态字段我用的是 int 类型并在代码里定义一个常量类来管理避免魔法数字散落在各处。处理订单状态时只允许按照状态机的方向转移非法状态操作直接抛异常。支付状态和订单状态我拆成了两个字段。为什么要拆因为订单状态是业务层面的支付状态是资金层面的。举个例子一个订单支付成功后退款了订单状态还可能是已完成但支付状态变成已退款。如果混在一个字段里后面做对账和统计会让你怀疑人生。2.3 购物车与库存联动设计购物车表设计得比较简单核心就是user_id加dish_id联合唯一。用户往购物车里加同一个菜品时直接走ON DUPLICATE KEY UPDATE把数量加一避免重复记录。库存的联动我踩过坑。最初版本是下单时不检查库存审核订单时人工看结果节日爆单的时候后台差点乱套。后来改成下单时预扣库存而且必须在数据库层面做原子扣减操作用UPDATE dish SET stock stock - #{count} WHERE id #{dishId} AND stock #{count}这样的 SQL 保证不超卖。如果扣减失败说明库存不足直接抛出异常让事务回滚并返回给小程序端“某某菜品库存不足”的提示。注意订单创建时预扣库存用户超时未支付取消订单时要回补库存否则会出现库存显示有货但实际买不了的情况。这一点一定要在取消订单的逻辑里写清楚。3. 后端核心接口实现3.1 微信登录与用户体系打通小程序端调用wx.login()拿到一个临时 code然后把 code 传给后端。后端拿着 code 调用微信官方的code2session接口换取 openid 和 session_key。我建议这一层封装成独立的 Service方便统一处理。用户第一次登录时根据 openid 查不到记录就自动创建查到了就更新昵称头像最后签发 token 返回给小程序端。关键代码大概是这个结构Override public String login(String code, String nickname, String avatar) { // 使用 code 调用微信接口获取 openid WxLoginResult result getOpenId(code); String openid result.getOpenid(); User user userMapper.selectByOpenid(openid); if (user null) { user new User(); user.setOpenid(openid); user.setNickname(nickname); user.setAvatar(avatar); userMapper.insert(user); } else { userMapper.updateProfile(user.getId(), nickname, avatar); } String token UUID.randomUUID().toString().replace(-, ); redisTemplate.opsForValue().set(TOKEN_PREFIX token, String.valueOf(user.getId()), 2, TimeUnit.DAYS); return token; }这里有两个细节值得注意。第一code2session接口是有调用频率限制的不能让小程序端每次打开页面就登录一次。我是在小程序端把 token 存到了wx.setStorageSync每次启动时先检查本地 token 是否存在存在就直接用不存在才走登录流程。第二session_key绝对不能返回给前端。这个值是微信用来解密敏感数据的密钥泄露出去等于把用户手机号等信息的安全交给别人。后端拿到后只保存在服务端即可如果业务上用不到甚至可以不管。3.2 点餐下单与订单提交下单接口是这个系统里事务最长、最容易出问题的地方。用户点击“提交订单”按钮后端要做的事情按顺序排列校验用户登录状态查询用户的购物车记录校验菜品状态和库存计算订单总金额预扣库存创建订单主表记录批量创建订单明细清空购物车下单成功后生成支付参数调用微信支付统一下单接口这里必须在方法上加Transactional注解保证任何一个步骤失败整个操作都能回滚。一开始我图省事没有对库存扣减做行锁或原子操作测试时用两个账号同时下单买同一个菜品发现库存直接变成负数了。后来改成UPDATE ... WHERE stock #{count}这种原子 SQL 才解决并发问题。生成订单编号的规则我是这样定的日期时间加随机数格式类似20250607123045001。规则是日期到秒再加 3 位随机数。如果订单量特别大的话建议换成雪花算法但小餐馆的并发量用这个规则加唯一索引就足够了。3.3 订单状态流转控制订单状态的每一次变更我都要求必须在后端完成前端只负责展示和发起请求。这样做的好处是业务规则都收敛在后端前端只是被动展示结果。举个例子用户点击“取消订单”按钮前端把orderId和actioncancel传给后端后端判断订单当前状态是否为“待支付”或“待接单”如果是就更新状态同时把预扣的库存回补。如果订单已经进入了制作中状态则后端返回“当前订单状态不允许取消”。前端拿到这个提示后弹窗告知用户。为了跟踪每一步操作我加了一个order_log表记录订单状态变更历史。这样做的好处有两个一是排查问题时能还原现场二是解决顾客投诉时能明确责任。4. 小程序端开发实操4.1 页面结构与跳转逻辑微信小程序的页面结构是典型的单页应用模式。我的页面规划如下首页index轮播图、分类导航、推荐菜品菜品分类页category分类列表、菜品列表菜品详情页detail菜品图片、价格、口味选择、加入购物车购物车页cart购物车列表、数量加减、结算按钮确认订单页confirm选择就餐方式、备注、显示总金额支付结果页payResult支付成功/失败状态展示订单列表页orderList全部订单、按状态筛选订单详情页orderDetail订单详情、状态操作按钮个人中心页mine用户信息、订单入口、设置页面跳转我全部用wx.navigateTo因为它支持返回。唯一例外是首页 tabBar 页面之间的切换用的是wx.switchTab。购物车数据的存储这里推荐两种方案方案一每次加购都调用后端接口购物车数据存在服务端方案二先存在本地 Storage结算时一次性提交后端我刚做的时候图省事选了方案二结果发现用户换了手机或者清理缓存后购物车就丢了体验明显不行。后来改成纯服务端方案页面每次onShow时从后端拉最新购物车数据。虽然会多一点网络请求但数据稳了。4.2 购物车与结算流程购物车页的核心是数量加减和选中状态。数量加减调用后端接口更新数据库并重算总价选中状态同时上报后端。结算按钮触发时前端读取当前选中的购物车项生成订单。这里有一个交互细节值得说用户在购物车页面连续快速点加减时前端容易把旧的响应覆盖新的请求结果。我做了个简单的防抖处理loading状态为 true 时不允许再次点击等当前请求返回后才恢复。这个小细节一开始没做测试时发现网络慢的时候数量会被覆盖回旧值体验很怪。确认订单页要做的事情很清晰展示购物车已选菜品列表和总金额、选择就餐方式、填备注。页面加载时需要一次性拿齐这些数据我用了一个getOrderConfirmData接口合并返回减少了前端多次请求导致的数据不一致问题。4.3 网络层封装与鉴权拦截小程序端的网络请求我封装了一个request.js工具函数核心逻辑是const request (url, method, data) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL url, method: method, data: data, header: { Content-Type: application/json, Authorization: wx.getStorageSync(token) }, success: (res) { if (res.data.code 200) { resolve(res.data.data) } else if (res.data.code 401) { // token 过期重新登录 wx.removeStorageSync(token) wx.navigateTo({ url: /pages/login/login }) reject(res.data) } else { wx.showToast({ title: res.data.msg, icon: none }) reject(res.data) } }, fail: (err) { reject(err) } }) }) }所有请求都走这个封装统一处理 token 注入、错误提示和 401 跳转省去每个页面重复写异常分支的工作。后端也有一层登录拦截器对未带 token 或 token 过期的请求统一返回 401 状态码。前后端约定同一套响应格式联调效率会高很多。注意小程序真机调试时必须在小程序管理后台把请求域名配置到“合法域名”列表里否则请求会被直接拦截。开发阶段可以勾选“不校验合法域名”但发布前必须配置正确这个验证环节经常有同学忽略导致上线后接口全部失败。5. 常见问题与排查技巧实录5.1 并发下单与库存超卖这个问题我在测试阶段撞到过一次比较严重的。当时用 JMeter 模拟 100 个并发请求同时下单同一个菜品结果库存从 30 直接变成了负数。原因就是代码里先查询库存再更新库存中间有间隙并发请求读到的都是同一个旧值。解决办法是使用原子更新的 SQL 语句UPDATE dish SET stock stock - #{count}, sales sales #{count} WHERE id #{dishId} AND stock #{count}数据库层面保证扣减的原子性。如果更新的影响行数为 0说明库存不足抛异常触发事务回滚。这个方案在小型餐饮系统的并发量下完全够用。5.2 小程序真机联调常见坑开发阶段用模拟器跑得好好的但一上真机就各种问题。我汇总了几次真机联调遇到的坑第一wx.request超时设置。默认超时时间是 60 秒但如果后端接口处理时间比较长比如首次启动时初始化数据库连接池真机上容易出现请求超时。我给后端配置了合适的连接池初始化策略并把前端的超时时间设置为 10 秒实测体验稳定。第二真机上对 HTTPS 的要求。开发工具勾了不校验域名后模拟器能跑但真机不行。真机上必须用 HTTPS 域名并且 SSL 证书要有效。我当时图省事把后端接口地址写成局域网 IP真机联调直接全部失败最后在内网环境搭了反向代理才解决。第三不同微信版本的 API 兼容性。比如getUserProfile接口在某个版本之后获取用户信息时需要先授权按钮直接调用会失败。我在个人中心页把获取头像昵称的入口改成了用户主动点击按钮触发授权兼容性和体验都好了许多。5.3 数据一致性与事务管理订单模块涉及多张表的修改最容易出现数据一致性问题。我的原则是一个业务操作涉及的数据库写操作全部放在一个方法里并用事务包裹。最初我在提交订单时只对订单主表和明细加了事务漏了库存扣减操作导致订单创建成功但库存没有扣减。后来把整个下单链路都收敛到一个createOrder方法中加上事务注解问题才彻底解决。事务中还要注意自调用失效的问题。Spring 的Transactional是基于代理机制的同一个类内部方法调用不会走代理所以事务注解要放在对外暴露的入口方法上而且要确保这个入口是被外部调用进来的。另外我建议给事务里涉及的数据库操作加上合理的超时时间避免某个死锁导致事务长时间不释放连接池。排查的时候从日志堆栈和数据库连接池状态入手基本能快速定位是死锁还是慢 SQL。5.4 管理后台和菜品图片存储方案关于图片存储一开始我直接把菜品图片存到了服务器本地目录用 Nginx 映射静态资源访问。小规模内部使用没问题但考虑到数据库迁移和服务器扩容会比较麻烦后期改成了对象存储方案。图片上传后返回一个 URL 存到数据库。如果只是做开发学习和毕业设计本地存储完全够用。给个建议避免把图片二进制直接存到 MySQL 的 BLOB 字段里会让数据库文件飞速膨胀备份恢复都很痛苦。用文件路径或者 URL 存储展示时前端拼接完整地址就行运维也方便。6. 从代码到交付项目整理与演示复盘6.1 为什么“源码加文档加视频”这种组合特别适合做项目交付做这个系统的时候甲方是一家做餐饮数字化服务的公司要求交付的不仅是一套能跑的代码还包括详细的开发文档、部署说明和演示视频。接触多了你会发现真正完整的项目交付不只是“代码能跑”那么简单。我给这套系统整理交付时会包含开箱即用的 SQL 脚本包括建库、建表和初始化数据README 部署文档环境要求、启动步骤、账号密码接口文档用 Markdown 或者在线文档维护字段含义写清楚演示视频录制小程序端操作流程和管理后台功能源码层级也会刻意划分清楚避免 controller、service、mapper 全堆在一起。对于一个交接项目来说别人拿到后能不能快速看懂比代码本身多“高级”更重要。6.2 部署环境与启动步骤配套环境清单大概是这些JDK 1.8 或 11Maven 3.6MySQL 5.7 或 8.0Redis 6.x做 token 存储微信小程序开发者工具启动顺序是先初始化数据库修改配置文件里的数据库连接和 Redis 地址然后运行主启动类最后用小程序开发者工具导入前端工程并修改BASE_URL。部署到云服务器时后端打成 jar 包用nohup后台运行即可。nginx 反代配置一下指向 SpringBoot 的 8080 端口再把本地图片目录映射出去。6.3 实机演示讲解的技巧录制演示视频和讲解视频时我的经验是按照业务链路来讲不要按照代码文件来讲。推荐顺序先用真实账号从小程序端走一遍完整下单流程切到管理后台处理订单状态再回到小程序端看订单状态变化最后演示基础数据维护这样讲的好处是逻辑清晰对方能直观地看到系统的每个功能是干什么用的。讲解代码时再按模块逐层展开先看核心表设计再看接口实现最后看前端页面。写在最后这套系统还能怎么扩展做到后期我又帮对方落地了两个实际生产可用的扩展点虽然不是必须但能明显提升系统的实用程度。第一个是“桌码点餐”顾客进店扫桌上的二维码自动带入桌号参数下单时把桌号存进订单备注里后厨出餐就能知道是哪一桌的。这个改动并不复杂只需在二维码链接里带一个参数订单表中加一列桌号字段前端在确认订单页把桌号回显出来就好。第二个是“菜品推荐位”在后端增加一个推荐菜品查询接口按照销量排序返回 top N首页展示热销榜。数据量大了之后再配合定时任务统计昨天的销量把排名存缓存接口性能就比较稳定了。做这套系统最大的体会是技术选型不重要业务链路通不通最重要。下单、支付、接单、出餐、完成这一整条链路任何一个环节断了都会让餐厅老板觉得“不好用”。哪怕 SpringBoot 用得再熟练小程序写得再花哨核心链路不扎实项目交付也谈不上真正落地。反过来只要把这条链路跑得顺畅扎实再往上加营销、会员、数据统计这些功能都只是时间问题。