一到饭点,打开手机扫码点餐已经是很多餐厅的标配。这类小程序背后其实是一套典型的“移动端 + 后端服务”全栈系统,用 Spring Boot 写接口、用微信小程序做用户界面,涉及到的知识点覆盖了前后端开发、数据库设计、接口联调和部署上线。最近我在折腾一套带源码的 Spring Boot 餐厅点餐微信小程序(源码编号 62608),前后把整条链路从鉴权、菜单展示、购物车到订单支付都过了一遍,踩了不少坑也整理了不少经验,这篇就把它掰开揉碎,从一个项目源码出发,聊聊这套系统到底怎么落地、里里外外都有哪些值得关注的实现细节。
这套源码适合谁看?我觉得主要是三类人:一是准备做毕业设计或者课程设计的学生,需要快速拿下“前后端分离全栈项目”这个思路模板;二是刚入行或者转岗做 Java 开发、想补一个完整项目经验的朋友,这套代码短小但五脏俱全,很适合用来拆着学;三是创业或者给餐厅做外包的开发者,很多点餐业务逻辑可以直接抄作业。我会从技术选型、业务拆解、代码实现、问题排查一条线讲下去,不会只停留在“能跑就行”,而是尽量把“为什么这么做”也讲明白。
1. 项目全景与技术选型思路
1.1 这套项目到底实现了什么
先说结论:这套餐厅点餐小程序覆盖了一个点餐业务最核心的链路,用户打开小程序看到菜品列表,加入购物车,提交订单,后台生成订单记录,商家侧可以管理菜品和查看订单。功能上它是完整的“C 端用户点餐闭环”。
从源码目录来看,它分成两大块:一部分是 Spring Boot 后端工程,负责提供所有 REST 接口,处理业务逻辑、读写数据库;另一部分是微信小程序前端工程,负责页面展示、用户交互、调用后端接口。两者通过 HTTP/HTTPS 接口通信,前端拿到 JSON 数据渲染页面。
具体功能清单大概是这样的:
- 用户侧:微信登录授权、菜品分类浏览、菜品详情、搜索、购物车管理、下单、订单列表、订单详情、地址信息维护。
- 商家侧:菜品分类管理、菜品增删改、上下架、订单状态变更(接单、出餐、完成)、订单列表与统计。
- 通用能力:图片上传、通用接口返回封装、鉴权拦截器、全局异常处理。
相比很多“只能看不能点”的 demo 型项目,这套源码至少把“用户点餐→生成订单→商家处理→订单完成”这条主链路走通了,这是它能作为学习样本的核心价值。源码里还带了数据库初始化脚本,表结构设计得比较基础但清晰,属于那种一看就懂、适合拿来做二次开发底子的类型。
1.2 为什么是 Spring Boot + 微信小程序这个组合
微信小程序做餐厅点餐前端是很自然的选择。用户不用下载 App,扫码就能进,用完即走,和餐厅的线下场景完全契合。小程序端开发门槛相对原生 Android/iOS 低很多,背靠微信生态,登录授权、支付、消息通知这些能力都是现成的,对独立开发者和学生项目特别友好。
后端选 Spring Boot,理由也直白:它是当前 Java 领域做 Web 接口最主流的框架,内置 Tomcat,自动配置能力极强,配合 Spring MVC 写 RESTful API 非常顺手。再加上 Spring Boot 生态里 MyBatis-Plus、Spring Data Redis、Spring Security 这些组件都有非常成熟的整合方案,能让开发者把注意力放在业务本身而不是环境配置上。
选这个组合还有一重现实考量:市面上这类“小程序前端 + Java 后端”的教程、开源项目、源码分享非常多,遇到问题很容易搜到解决方案,学习成本低。真放到生产环境,只要做一层 Nginx 反向代理、配好 HTTPS 证书,后端也完全扛得住中小型餐厅的日常并发量。
1.3 前后端分离的项目结构怎么看
拿到源码先别急着跑,先把目录结构过一遍,这样后面调试的时候心里才有数。
后端工程是典型的 Maven 结构,核心包路径大致包括:
controller:接收前端请求,做参数校验,调用 service,返回统一响应体。service:业务逻辑层,比如下单时处理购物车数据、算总价、生成订单号、扣库存。mapper:数据访问层,基于 MyBatis-Plus 的 BaseMapper,简单 CRUD 基本不用写 SQL。entity:数据库实体类,对应一张张表。config:配置类,比如 WebMvcConfig、拦截器注册、跨域配置、微信配置读取。common或utils:工具类、统一返回结果封装、常量定义、全局异常处理。
小程序端目录结构则分成pages、components、utils、images、app.js、app.json这些常见块。需要注意utils/request.js这种公共请求封装,几乎每个页面的接口调用都会走它,改请求头、改 baseURL 都在这里。
有一个细节新手经常忽略:这类前后端分离项目,联调时最容易出问题的不是业务代码,而是“域名 + 端口 + 跨域 + HTTPS 校验”这一套网络边界问题。后面我会专门讲这块的坑。
2. 后端 Spring Boot 核心模块实现细节
2.1 数据库表设计:菜品、分类、订单、购物车
数据库设计是整个项目的地基,也是最值得花时间看的部分。这套项目为了让逻辑简单,表数量不多,但每一张表都对应了一个明确业务场景。大致可以划分为几类:
- 用户相关:用户表,主要存微信 openid、昵称、头像、手机号,
openid是唯一索引,因为后续所有登录态都靠它关联。 - 菜品相关:菜品分类表、菜品表。菜品表一般包含分类 id、名称、图片、价格、描述、销量、状态(上下架)。分类和菜品是一对多关系,用分类 id 关联。
- 购物车相关:购物车表,记录用户 id、菜品 id、数量、是否选中,有的项目干脆直接用小程序本地 Storage 存购物车,但带后端版本会强调服务端保存,方便多端同步。
- 订单相关:订单主表 + 订单明细表。主表存用户 id、订单号、总金额、状态、下单时间、备注;明细表存订单对应的每个菜品、单价、数量、小计。主表和明细表是典型的一对多。
我个人的建议是,看这套源码时优先关注订单和订单明细为什么拆两张表。这不是为了显得专业,是因为一张订单会有多个菜品,如果把菜品列表直接塞在订单表里,后续想统计“某个菜卖了多少份”会非常痛苦。拆成主表和明细表以后,报表统计、退款部分取消、评价某个菜品,都只需要操作明细表。这就是第一张 DB 设计课。
另外注意价格字段,我见过不少项目用double存价格,这是一种很不好的习惯。金额这种东西涉及精度,线上环境一定要用decimal,数据库字段也建议指定精度,比如decimal(10,2)。如果你拿到的旧代码用的是double,二次开发时最好先把它改了,后面算账才不会出现尴尬的0.1 + 0.2 != 0.3。
2.2 登录鉴权:微信登录拿到 openid,JWT 维持会话
微信小程序和传统 Web 登录有个本质上的不同:小程序里没有“用户名密码”输入,用户点一下“微信一键登录”,前端调用wx.login()拿到一个临时code,把这个 code 传给后端,后端拿着 code 去微信的接口换openid和session_key,openid就是用户的唯一身份标识。
这套源码的后端登录流程基本是这样的:
- 小程序前端调用
wx.login()获取code。 - 前端把
code通过 POST 请求发到后端/user/login之类接口。 - 后端拿着
code调用微信官方jscode2session接口,得到openid。 - 后端拿着
openid查数据库,如果用户不存在就自动注册一条新记录。 - 生成一个 token 返回给前端,前端把 token 存到
Storage里,后续所有请求都在 header 中带上。
token 生成方式有几种,这套源码用 JWT 是比较常规的选型。JWT 本质上是一段经过签名、可携带用户信息的字符串,格式是Header.Payload.Signature,后端在 Payload 里塞用户 id、openid 这些信息,再用密钥签名。后续请求进来,后端拦截器解析 JWT、校验签名和有效期,就能识别当前是哪个用户。
实际写拦截器的时候有几个建议:
- 放行名单要明确处理。登录接口、注册接口、静态资源、图片访问这些不需要 token 的请求必须放行,不能一刀切全部拦截。
- token 过期和无效要区分处理。前端收到 401 状态码,应该自动跳转登录页而不是弹出报错。
- 密钥不要写死在代码里。放在
application.yml或者环境变量里,项目上线后定期更换密钥是个好习惯。
我自己调试这类接口时,习惯先在后端日志里打印每个请求的 header 和参数。微信小程序端传 token 要遵守字段名约定,比如Authorization: Bearer xxx,后端取的时候别取错位置。这个坑排查起来困不困难?不算难,但是相当浪费时间,规范统一能省很多事。
2.3 菜品与订单接口:从 RESTful 设计到统一返回体
点餐业务的核心接口其实就那么几个:GET /api/dish/list?categoryId=xx查菜品、POST /api/cart/add加购物车、POST /api/order/submit提交订单、GET /api/order/list查订单列表。真正需要花心思的是接口返回结构设计。
好的做法是定义统一返回体,比如:
{ "code": 200, "message": "success", "data": { "id": 1, "name": "宫保鸡丁", "price": 28.00 } }code表示业务状态码,200是成功,其他数字是各类异常;message给前端提示语;data放真正的业务数据。定义好以后,后端所有接口都包一层这个结构,前端拿到后先判断code,再做后续逻辑,代码会比“运气好拿到了 data,运气不好拿到的是个错误提示”规整得多。
这套源码在接口实现上用了 MyBatis-Plus,这是一个很加分的选型。MyBatis-Plus 提供了通用 CRUD 方法,单表操作基本不用手写 SQL,大大加快了开发速度。比如你想查所有上架菜品,只要你写的 Mapper 继承BaseMapper<Dish>,直接用selectList(new LambdaQueryWrapper<Dish>().eq(Dish::getStatus, 1))就可以,不需要写一个冗长的 if 嵌套 XML。
不过我不建议看到 MyBatis-Plus 好用就完全丢掉 SQL 能力。这个项目里订单统计、销量排行、按分类聚合这些查询,一旦涉及多表 join 或者复杂聚合,还是得手写 SQL。建议是在 Mapper 接口里用@Select注解或者在 XML 里维护,保持复杂 SQL 的可读性和可优化空间。
2.4 订单状态机的设计思考
订单模块是整个项目里业务最重的地方,因为它是状态最多的对象。一套标准的点餐订单状态大概是:待支付 → 已支付/待接单 → 商家已接单 → 已出餐 → 已完成,另外一个方向是:已取消/已退款。这些状态之间不是随便跳转的,必须遵循业务规则,比如待支付订单才能取消,已完成的订单不能直接退款。
代码层面,我喜欢用一个状态字段status加常量或枚举控制:
- 用户端能看到的状态:待支付、进行中、已完成、已取消。
- 商家端能看到的状态:新订单、已接单、已出餐、已完成。
- 支付和退款相关的状态可以单独延伸,但基础版不建议过度设计。
这个设计背后有个实用原因:给前端展示和给商家操作的角色视角不同。用户不需要区分“待接单”和“已出餐”,他只需要知道“我的饭还在做”;商家则需要精细控制每张订单处于哪个环节。如果你把整个状态机弄得太复杂,联调的时候前后端对状态就容易对不上;太简单,又无法支撑商家日常运营。这套源码的整体状态粒度算适中的,学习价值很高。
事务在这个模块里也特别重要。提交订单这个动作,如果订单主表插入了、明细表插入到一半发现库存不足,又或者 Redis 里的销量已经扣减了,但数据库写失败了,那么会出现什么?数据不一致。所以提交订单的 service 方法必须加上@Transactional,让这些数据库操作同生共死——要么全部成功,要么全部回滚。
3. 微信小程序前端实现要点
3.1 从页面路由到组件复用
小程序端的页面结构大致能分成两类:一类是 tabBar 页,比如首页菜单、购物车、我的;另一类是普通页面,比如订单详情、菜品详情、下单确认。
app.json里定义了页面路由和 tabBar 列表,新手容易犯的错误是修改路由后没有重新编译。这不能怪代码,而是微信开发者工具的缓存机制问题——改了app.json不重编译,新页面可能一直提示找不到。不用慌,重新编译一次基本都能解决。
页面的抽取和复用也很关键。菜品卡片(图片、名称、价格、加号按钮)在首页列表、搜索结果页、推荐菜品区域都可能出现,如果每个页面都复制粘贴一套代码,后续改样式就要改好几处。合理做法是把菜品卡片抽成一个components/dish-card组件,通过 properties 把菜品对象传进去,内部维护加购按钮的点击事件,然后每个页面引用同一个组件。
源码项目里一般能看到 components 目录,如果没有,那么这就是你第一次二次开发时可以动手重构的切入点。组件化不只是好看,它直接决定你后续加功能、改界面的时候是改一处还是改十处。
3.2 购物车状态管理:本地存储还是服务端同步
购物车这个小东西,看起来简单,其实要做好不容易。它涉及到两个层面的数据:一个在用户本地(小程序 Storage),一个在后端数据库(购物车表)。两者关系怎么处理?主要有两种实现路线。
第一种路线:购物车纯前端管理,下单时一次性提交。这种方式的好处是交互流畅,加减菜品不需要等网络请求,坏处是用户中途关闭小程序会丢数据,且更换手机后购物车不保留。
第二种路线:购物车后端管理,前端每次操作都调用接口。好处是数据稳定,能实现“手机上的购物车和 iPad 上的购物车是同一份”,坏处是交互稍慢,对接口设计要求高。
这套项目为了兼顾实现难度和体感,我盯源码后判断大概率是“本地为主、下单同步”的混合方案。这其实是很多同类小程序初版都会采用的折中路线,开发速度最快。所以你看前端代码时,重点看storage.js或者购物车页面里有没有对 Storage 的读写操作,以及提交订单时是怎么把本地购物车数据 POST 给后端的。
如果让我做二次开发,购物车这边我一定要做的优化是“防重复提交”。用户手快连点两次“下单”,前端没有防抖的话,后端就会收到两个相同的下单请求,于是生成两笔订单。最理想的做法是后端对同一用户短时间内相同内容的订单做幂等校验,或者前端在提交后置一个 pending 状态,按钮置灰、loading 转圈,直到拿到响应才恢复。
3.3 请求封装与登录过期处理
小程序里发请求有两种方式:wx.request直接发,或者自己封装一层。直接发的问题在于每个页面都要重复写url、method、header,baseURL 一旦更换,几十个文件跟着改,非常痛苦。源码里一般会在utils/request.js做封装,它做的事情大概是这样:
- 设置统一的 baseURL。
- 从 Storage 取 token,附加到请求头。
- 封装 Promise,成功时 resolve 业务数据,失败时 reject 并统一提示。
- 检查接口返回的
code,不是 200 的话根据业务码做全局处理,比如未登录就跳转登录页。
我见过很多项目的登录过期处理做得比较粗暴:每次请求返回“登录已过期”,就弹个窗口,用户看一次弹一次。更顺滑的做法是:封装层发现 401,就先wx.removeStorageSync('token'),再跳转到一个专门的登录页,整个过程不能弹窗轰炸,最好静默处理。
再就是超时时间。点餐场景中,用户下单后往往马上要看结果,如果请求设置成默认 60 秒超时,弱网环境下用户会一直干等着。把这套系统的请求超时时间调到 10 秒左右会比较合适,超时后给一个明确的“网络不给力”提示,比无限转圈强得多。
4. 核心业务流程串讲:从扫码进店到订单完成
4.1 用户视角的完整流程
假设你已经把这个项目部署好了,打开小程序,整个流程是这样的:
- 用户通过微信扫描餐桌二维码,进入小程序首页。二维码上一般可以带一个
tableId或者shopId参数,用于识别哪张桌、哪个店。如果你拿到的源码没有这个参数,也可以用后续手动选桌位的方式补上。 - 首页展示菜品分类和菜品列表,用户浏览并点击加号把菜品加入购物车。
- 进入购物车页,可以选择“去结算”。结算页会让用户填用餐人数、备注口味偏好(要不要辣、少盐),也可能让用户选择座位或取餐方式。
- 点击“提交订单”,前端组装购物车数据、用户信息、备注,后端生成订单,返回订单号。
- 用户跳转到订单详情页或直接唤起支付。如果是纯 demo 版,可能没有真实微信支付,而是模拟“已支付”状态。
- 商家端看到新订单,开始配菜出餐,把状态更新为“已完成”,用户端订单状态同步变化。
整个过程并不复杂,但每一步都牵涉前后端的数据交换和状态同步,其中最容易出 bug 的就是“状态不同步”。比如商家已接单,但是用户端小程序没刷新,页面还停留在“待接单”,用户就会以为商家不理人。很多源码项目不会主动做 WebSocket 或消息推送,这时候前端页面到订单列表时重新拉取接口,或者下拉刷新,是性价比最高的保底手段。
4.2 后端的订单生命周期管理
在提交订单这个接口的内部,至少要做这样几件事:
- 参数校验:用户 id 是否为空、购物车是否为空、菜品是否在售、价格是否正常。
- 创建订单主记录:生成订单号,计算总金额,设置初始状态。
- 写入订单明细:遍历菜品列表,逐条落库。
- 修改销量/库存:如果做了销量统计,这里需要更新菜品表的销量字段。
- 清空购物车:下单成功之后,把对应的购物车记录删除或标记为已提交。
这套流程里有个隐藏难点:金额的计算应该以服务端为准,不能直接信任前端传过来的“总价”。流传的源码项目有的直接信任前端传参,这会导致很严重的安全问题——用户篡改请求里的总金额怎么办?规范的做法是后端根据下单菜品明细重新算一遍总价,前端传来的总价只用来做校验对照。
还有一些细节高手会注意到:订单号生成不要用System.currentTimeMillis()裸拼,因为并发场景下重复概率很高,而且暴露了机器时间。更好一点的做法是“时间戳 + 随机数 + 用户 id 后四位”,或者直接用一个全局递增的业务流水号。订单号这东西不光是给人看的,也是后续对账、客服查单的依据,可读性和唯一性都要兼顾。
4.3 库存和销量的取舍
餐厅点餐的“库存”和电商的“库存”含义不完全一样。电商的库存是实物库存,卖一件扣一件;餐厅的菜品库存更像“今日可售数量”。有的餐厅几个招牌菜每天限量 50 份,有的菜品会临时估清(卖完了)。“限量”和“估清”如果都要做,那就不只是销量字段能解决的,可能需要一张“每日限量表”。
这套基础版项目一般不会做这么细,最常见的实现是菜品表上带一个sales销量字段,下单成功就加 1。这个实现虽然简单,但它支持不了“库存上限”这种运营场景。如果你打算把项目拿去给真实餐厅用,建议加上“今日库存”或“剩余份数”字段,下单事务里用乐观锁或者UPDATE ... SET stock = stock - 1 WHERE stock > 0防止超卖。这也是很多面试官喜欢追问的点,项目经验里写上这一个优化,含金量会高出不少。
5. 本地跑通源码与常见问题排查
5.1 拿到源码后怎么把它跑起来
源码拿到手之后,第一步不是打开 IDE 直接点运行,我建议按这个顺序来做:
- 找数据库脚本。一般源码里都有一个
.sql文件,用 Navicat 或者命令行导入到 MySQL。注意这一步很容易出问题——导入时如果你的 MySQL 版本太新或太旧,字符集和排序规则可能导致导入失败。可以先检查文件头部有没有SET NAMES utf8mb4。 - 修改后端配置。
application.yml或application.properties里改数据源,重点是url、username、password。如果你的 MySQL 装在本地默认端口 3306,基本就只需要改密码。 - 启动后端。用 IDE 直接启动 Spring Boot 主类,看到“Started Application in xx seconds”才算成功。启动失败时先看端口是否被占用,8080 被其他进程占了就改配置里的
server.port。 - 打开微信开发者工具,导入小程序目录,填写自己的 AppID。没有 AppID 可以用测试号,但是测试号对登录和支付能力有限制。
- 修改前端
request.js里的 baseURL。本地调试时后端跑在http://localhost:8080,但手机上预览或者真机调试时必须换成局域网 IP,比如http://192.168.x.x:8080,否则手机访问不到电脑上的服务。 - 编译运行,先看请求通不通。打开调试面板的 Network 一栏,如果所有请求都在报
404或failed,优先检查 baseURL 和后端 CORS 配置。
5.2 本地调试避坑清单
为了让你少走弯路,我把最容易踩的坑按“操作顺序”整理成一张速查表,每个都是实测真实会遇到的:
| 症状 | 大概率原因 | 解决思路 |
|---|---|---|
| 后端启动失败,端口被占用 | 本机 8080 被其他服务占用 | 换端口,比如server.port=8081,前端 baseURL 一起改 |
| 数据库连接失败 | application.yml中密码错误或者数据库未启动 | 确认 MySQL 服务运行中,核对账密,先拿客户端工具测连通性 |
小程序 request 请求报url not in domain list | 开发者工具开启了域名校验 | 开发阶段在“详情-本地设置”勾选“不校验合法域名” |
| 真机预览请求失败 | baseURL 还是 localhost | 把 baseURL 改成电脑局域网 IP,注意同一 Wi-Fi 网络 |
| 接口报 401 | token 没传或者已过期 | 检查请求头有没有带 token,登录接口是否正常返回 token |
| 提交订单报“系统繁忙” | 大概率是后端异常但被全局异常处理吞了 | 看后端控制台完整异常堆栈,先定位是 SQL 问题还是空指针 |
| 菜品图片不显示 | 图片域名不在合法域名列表或拼接路径错误 | 本地用开发者工具“不校验域名”,线上要把图片域名加到合法域名里 |
| 时间字段差 8 小时 | MySQLserverTimezone和本机时区不一致 | 连接参数加serverTimezone=Asia/Shanghai,数据库连接 URL 显式指定时区 |
这张表格背后我多说一句:大概 80% 的“项目跑不起来”都不是代码逻辑写错了,而是环境问题。排查顺序永远是从外到内——先看网络通不通,再看服务起没起,再看数据库连不连得上,最后才去查业务代码,效率最高。
5.3 时间、金额与空指针:那些“经典但是查起来很费劲”的坑
有几种异常,我在调试时遇到频率极高,单独拎出来说一下。
时间问题:后端返回的订单时间到前端显示出来多了或者少了 8 小时,本质上是时区问题的变体。解决方式就是连接 MySQL 的 URL 后面显式加serverTimezone=Asia/Shanghai,同时后端如果用的 Jackson 序列化,要在配置里统一时间格式化格式,比如yyyy-MM-dd HH:mm:ss。前端再用new Date(str)解析,时间就稳了。
金额问题:用double计算金额导致精度丢失。这属于基础但经典的坑。如果底层表字段已经是decimal,Java 实体也要用BigDecimal接收,不要因为数据库是 decimal 而实体用 Double,传输过程中照样丢精度。写购物车合计和小计逻辑时,用BigDecimal的add和multiply,不寫a + b这种直接加法。
空指针问题:后端起服务很容易忽略的一个场景是“用户下单时购物车是空的”。前端发来一个空数组,后端没有判空,直接遍历取值,空指针就来了,最终给前端一个 500。良好习惯是在所有入口层做防御性判断。微信小程序端尤其容易在“拿options取参数”时忽略参数缺失,最好都加个默认值兜底。
6. 从毕设到生产环境:说点二次开发的实在话
6.1 基础设施类优化:缓存、文件存储与部署
如果你要把这套项目从“本地能跑”推进到“线上能用”,第一刀应该砍在基础设施上。最优先考虑的是 Redis 介入。
为什么要上 Redis?最典型的场景是:首页菜单是访问量最大的接口,每次请求都查数据库,几万个用户同时打开店里的小程序,MySQL 压力会很明显。把菜品列表和分类列表缓存到 Redis 里,设置一个合理的过期时间,在后台改菜品时主动更新缓存,接口性能会提升非常多。另外一个适合上 Redis 的场景是购物车,用户购物车的读写频率比下单要高得多,而且结构简单,用 Redis 的 Hash 结构保存“用户 id -> 菜品列表”很自然,就算用户中途退出,购物车也在服务端保住了。
第二个方向是图片文件存储。很多基础项目的菜品图片都是本地路径,这在生产环境不可行,因为应用服务器文件系统不适合存大量图片,而且小程序合法域名配置也会受限。建议切换到对象存储,比如各大云厂商提供的对象存储服务,至少也要通过 Nginx 静态资源服务来托管图片。改起来不复杂,主要是把上传接口的落盘逻辑替换成上传到对象存储并返回 URL 即可。
部署方式上,别上 Docker 就盲目上 Swarm 或 K8s,中小项目一套Nginx + Jar包 + MySQL已经足够。有条件的话可以用 Docker Compose 把 MySQL、Redis、后端 Jar 包编排起来,一台 2 核 4G 的云服务器跑个餐厅活动日的流量没有压力。
6.2 业务增强方向:接真实微信支付与商家管理端
真实运营还有一个绕不开的环节:微信支付。基础源码里的支付流程一般是模拟的,并不会真实扣款。接真实微信支付需要你有一个微信支付商户号,并且在微信公众平台完成小程序和商户号的关联绑定。后端下单成功后,调用微信支付统一下单接口,拿到prepay_id,再用签名算法生成小程序端所需的支付参数,前端wx.requestPayment拉起支付面板。用户支付成功后,微信会通过回调通知你的后端服务器,这时你才能把订单状态从“待支付”改成“已支付”。
这个流程有几个重点:一是支付回调的签名校验必须做好,否则可以伪造通知;二是回调接口返回给微信的内容必须是固定格式的 XML 成功标志,否则微信会一直重试;三是支付结果不能只信前端传参,必须以回调结果为准,否则用户可能支付成功但订单没更新,非常影响体验。
如果再加一个商家管理端,那就更有意思了。你可以把后端接口直接复用到管理端,开发一个基于 Web 的 Vue 或 React 后台,让商家在电脑上处理订单、上下架菜品、查看流水。这样整套系统就从一个“单端 demo”变成了一个具备“用户端 + 管理端”的完整产品形态,简历上写这个项目的分量也会完全不同。管理端的核心接口大多不用重写,订单列表、菜品管理这些后端接口本来就是面向管理场景设计的。
6.3 代码质量:那些我建议你动手改掉的地方
看源码学习,除了跑通,更重要的是“带着批判眼光去改”。我自己过这套源码时,有几个改进点是立刻想动手做的。
统一日志切面。源码里的日志大概率是打印在 service 方法里的,或者根本没有几条,排查问题基本靠肉眼。建议花半天时间加一个基于 AOP 的请求日志切面,拦截所有 Controller 方法,统一打印请求路径、参数、响应耗时和异常信息。这套基础代码一旦加上,对你理解整个项目请求链路都有巨大帮助。
全局异常体系。源码里可能有 GlobalExceptionHandler,也可能只有零散 try-catch。规范的做法是自定义几个业务异常类,比如BizException、ParamException,service 层发现异常就throw出去,由全局异常处理器统一转成 JSON 返回给前端。这样 controller 层会干净很多,也避免了异常堆栈直接暴露给前端。
配置分离。源码里的微信 appId、密钥、数据库账号密码可能都是明文写在配置里的,这种习惯不好。线上环境改用环境变量或配置中心,至少把配置文件和代码仓库分离。这也是面试官经常追问的点,你主动提出来会显得有生产经验。
最后再分享一点我的体会
这套 Spring Boot 餐厅点餐微信小程序源码,说实话它不是一个特别复杂或者高深的项目,但它的价值在于“完整”。从前端页面到后端接口,从数据库表到业务流程,从用户登录到商家接单,一条业务链路全程闭环。我前后也读过不少类似的源码项目,真正能静下心把整条链路读透、改通的人,学到的东西其实不亚于做一个崭新的大项目。
如果你现在正准备拿这套代码做毕业设计或者项目经历,我由衷建议不要停留在“启动成功就交差”的状态。试着加一个 Redis 缓存、把模拟支付换成真实支付、给管理端做个简单的 Web 界面,这每一步都会让你踩到新的坑,但也正是这些坑,才会真正内化成你的经验。项目源码只是起点,你能从里面挖出多少东西来打磨出自己的作品,那才是决定你技术成长速度的事。