简介:这是一套基于Java后端与uniapp(Vue3)前端实现的在线点餐小程序完整源码,面向计算机、电子信息等专业学生及初级全栈开发者,适用于课程设计、期末大作业或毕业设计参考。系统支持扫码点餐、外卖配送与到店自取双模式,并具备多门店统一管理能力,涵盖用户端、商家端、后台管理端三大模块,体现典型前后端分离架构实践。压缩包共2005个文件,含1323个Java后端业务与接口代码、257个Vue3组件与页面逻辑、161个JS工具与交互脚本、84份Markdown项目说明与接口文档,整体18.17MB,结构清晰、注释较全,便于分层学习与功能拓展。目前已有98人学习下载,读者可直接运行调试,快速掌握Spring Boot+MyBatis后端开发、uniapp跨端构建、多门店数据隔离设计及扫码支付集成等核心技能。
1. 为什么一个「扫码点餐小程序」源码包,比你想象中更难跑通?
这不是一个拿来就能上线的“开箱即用”模板——它是一套真实业务场景倒逼出来的工程切片:顾客扫桌牌二维码进小程序,选餐、选自取/外卖、选门店(比如A店堂食、B店外送),下单后订单实时推送到对应门店的后厨屏;管理员在后台能按门店维度看销量、改菜单、调库存。整个链路横跨微信小程序前端、Spring Boot 后端、MySQL + Redis 数据库、多租户式门店隔离逻辑,还隐含了微信支付回调验签、订单状态机流转、并发下单锁库存等硬核细节。
很多开发者解压 ZIP 后第一反应是“怎么连登录都进不去”,第二反应是“为什么切换门店后菜单还是旧的”,第三反应是“支付成功但订单没变状态”。问题不在代码写得烂,而在于这套源码默认假设你已具备:① 微信开放平台基础配置能力(不是只懂wx.login());② Spring Boot 多数据源或逻辑分库分表意识;③ 小程序wx.request与后端@CrossOrigin/ JWT 鉴权的协同调试经验。它适合两类人:一是正要启动本地餐饮SaaS项目的Java后端工程师,想拿它当最小可行架构参考;二是带毕设任务的学生,需在2周内交付可演示的“多门店扫码点餐”系统——但必须亲手填平那几个关键断层,否则永远卡在“页面白屏”或“401 Unauthorized”。
2. 搭建环境:从解压 ZIP 到首页渲染成功的最小闭环
2.1 解压后目录结构识别与关键文件定位
先别急着mvn clean install。打开 ZIP,你会看到典型前后端分离结构:
├── backend/ ← Spring Boot 项目(Maven) │ ├── pom.xml ← 注意 JDK 版本(常见为 1.8 或 11)、Spring Boot 版本(2.7.x 或 3.0+) │ └── src/main/ │ └── resources/ │ ├── application.yml ← 核心配置入口(数据库、微信参数、多门店开关) │ └── application-prod.yml ← 生产环境配置(通常被忽略,但多门店逻辑常在此激活) ├── frontend/ ← 微信小程序源码(非 H5!是 .wxml/.wxss/.js 结构) │ ├── project.config.json ← 小程序 AppID 配置位置(必须改!) │ ├── app.js ← 全局逻辑(含门店切换拦截器) │ └── pages/index/index.js ← 首页数据拉取逻辑(重点看 wx.request 的 url 域名) └── docs/ ← 项目说明文档(PDF 或 Markdown,含数据库 ER 图、接口列表)提示:
project.config.json中的"appid"字段必须替换成你自己的小程序 AppID,否则真机扫码必报request:fail url not in domain list。这是新手翻车率最高的第一步。
2.2 后端服务启动:绕过微信认证的本地调试法
源码默认依赖微信开放平台鉴权(如wx.getUserProfile获取用户信息),但本地开发时无法触发微信授权弹窗。正确做法是临时注释鉴权逻辑,直连模拟数据:
// backend/src/main/java/com/example/controller/OrderController.java @PostMapping("/order/create") // @PreAuthorize("hasRole('USER')") ← 注释掉这行(Spring Security 配置) public Result createOrder(@RequestBody OrderDTO dto) { // 临时跳过微信用户校验 Long userId = 1001L; // 强制设为测试用户ID return orderService.createOrder(userId, dto); }同时,在application.yml中关闭微信敏感操作:
wechat: enable: false # 关闭微信登录、支付回调等模块,避免启动报错 appid: your_appid_here secret: your_secret_here启动命令:
cd backend mvn spring-boot:run -Dspring.profiles.active=dev验证:访问http://localhost:8080/actuator/health返回{"status":"UP"}即后端就绪。
2.3 小程序前端配置与域名白名单设置
微信要求所有wx.request请求域名必须在「小程序后台 → 开发管理 → 开发者工具 → 服务器域名」中备案。但本地调试时,你无法将localhost:8080加入白名单。解决方案是使用微信开发者工具的「不校验合法域名」开关(仅限开发版):
- 打开微信开发者工具 → 右上角「详情」→「本地设置」→ 勾选「不校验合法域名、https 以及 web-view 业务域名」;
- 在
frontend/app.js中修改请求 baseURL:
// 替换原 config.js 中的 apiHost const API_HOST = 'http://127.0.0.1:8080'; // 不要用 localhost!iOS 真机调试会失败- 点击开发者工具「编译」,若控制台无
net::ERR_CONNECTION_REFUSED报错,且 Network 面板能看到/menu/list返回 JSON,则前端与后端通路打通。
注意:
127.0.0.1是关键。localhost在 iOS 真机调试时会被解析失败,必须用 IP 地址。
3. 多门店模式落地:从数据库设计到前端门店切换逻辑
3.1 数据库表结构中的「门店租户字段」识别
多门店不是靠前端下拉框切换实现的,而是后端在每张核心表(menu,order,store_stock)中强制添加store_id字段,并在 SQL 查询时自动注入WHERE store_id = ?条件。查看backend/src/main/resources/mapper/MenuMapper.xml:
<select id="listByStore" resultType="MenuVO"> SELECT * FROM menu WHERE store_id = #{storeId} AND status = 1 <!-- 这里隐含了「同一菜品在不同门店可独立上下架」 --> </select>关键点:
store_id是外键,关联store表(含门店名称、地址、营业状态);- 所有增删改查接口的 DTO 中,
storeId必须作为必传参数(如MenuDTO.setStoreId(1L)); - 若
application.yml中multi-store.enabled=true,则 MyBatis Plus 的MetaObjectHandler会自动填充store_id字段(防止漏写)。
3.2 前端门店选择器与全局状态同步
小程序首页顶部有「切换门店」按钮,点击后需:① 更新本地缓存wx.setStorageSync('currentStore', store);② 重新拉取该门店菜单;③ 同步购物车中商品所属门店(避免跨店下单)。核心逻辑在pages/index/index.js:
// 切换门店后触发 switchStore(e) { const storeId = e.detail.value; wx.setStorageSync('currentStore', { id: storeId, name: e.detail.label }); // 重点:清空当前购物车(因不同门店菜单ID体系独立) wx.removeStorageSync('cartItems'); // 重新拉取菜单 this.loadMenuList(storeId); }, loadMenuList(storeId) { wx.request({ url: `${API_HOST}/menu/list?storeId=${storeId}`, // URL 中显式传参 success: (res) => { this.setData({ menuList: res.data }); } }); }血泪经验:若忘记清空购物车,用户在A店加购后切到B店,仍显示A店商品,提交时后端因
store_id不匹配直接拒绝——错误日志里只会打Invalid store_id in cart items,很难定位。
3.3 后端门店上下文传递:ThreadLocal + 拦截器方案
为避免每个 Controller 方法都手动接收storeId参数,源码采用StoreContext工具类 +HandlerInterceptor自动提取:
// StoreContext.java public class StoreContext { private static final ThreadLocal<Long> STORE_ID_HOLDER = new ThreadLocal<>(); public static void setStoreId(Long storeId) { STORE_ID_HOLDER.set(storeId); } public static Long getStoreId() { return STORE_ID_HOLDER.get(); } } // StoreInterceptor.java(注册在 WebMvcConfigurer 中) public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String storeId = request.getHeader("X-Store-ID"); // 前端请求头携带 if (storeId != null && !storeId.isEmpty()) { StoreContext.setStoreId(Long.valueOf(storeId)); } return true; }前端请求时必须加 Header:
wx.request({ header: { 'X-Store-ID': wx.getStorageSync('currentStore').id } });这样,后续 Service 层可直接调用StoreContext.getStoreId()获取当前门店,无需层层透传。
4. 外卖与自取双模式:状态机设计与库存扣减差异
4.1 订单类型字段定义与业务分流
在order表中,关键字段是order_type(TINYINT):
| 值 | 含义 | 库存扣减时机 | 配送逻辑 |
|---|---|---|---|
| 1 | 自取 | 下单成功即扣减 | 无需配送员,仅通知门店 |
| 2 | 外卖 | 支付成功后扣减 | 调用第三方配送 API |
后端创建订单时,根据前端传参决定分支:
// OrderService.java public Result createOrder(Long userId, OrderDTO dto) { if (dto.getOrderType() == 1) { // 自取 stockService.deductStock(dto.getItems()); // 立即扣减 } Order order = buildOrder(userId, dto); orderMapper.insert(order); if (dto.getOrderType() == 2) { // 外卖 // 支付成功回调中再扣库存(见 4.3) notifyKitchens(order.getId(), "OUTSIDE_DELIVERY"); } return Result.success(order.getId()); }4.2 自取模式下的「到店核销」流程实现
自取订单生成后,顾客会收到取餐码(6位数字),门店员工在后台输入该码完成核销。核销表order_verification结构:
CREATE TABLE `order_verification` ( `id` BIGINT PRIMARY KEY, `order_id` BIGINT NOT NULL, `verify_code` CHAR(6) NOT NULL, -- 索引字段,加速查询 `verified_at` DATETIME, `verified_by` VARCHAR(32) );核销接口关键逻辑:
@Transactional public Result verifyOrder(Long orderId, String code) { // 1. 校验取餐码是否匹配且未核销 OrderVerification v = verificationMapper.selectOne( new QueryWrapper<OrderVerification>() .eq("order_id", orderId) .eq("verify_code", code) .isNull("verified_at") ); if (v == null) return Result.fail("取餐码错误或已核销"); // 2. 更新核销时间 & 订单状态 v.setVerifiedAt(LocalDateTime.now()); verificationMapper.updateById(v); orderMapper.update(null, new UpdateWrapper<Order>() .eq("id", orderId) .set("status", 5) // 5=已取餐 ); return Result.success(); }玄学坑:若
verify_code字段未建索引,高并发核销时会出现「超时等待锁」,导致核销失败。务必执行ALTER TABLE order_verification ADD INDEX idx_code (verify_code);
4.3 外卖模式的支付回调与库存补偿机制
外卖订单的库存扣减不能放在创建时(防恶意下单占库存),必须等微信支付成功回调后执行。但回调可能丢失或延迟,因此需「最终一致性」设计:
- 支付回调接口
/pay/callback接收微信通知; - 验签通过后,更新订单状态为「已支付」,并发送 MQ 消息触发扣库存;
- 若 MQ 消费失败,启用定时任务扫描
status=2(待支付)且pay_time < NOW()-5min的订单,调用stockService.compensateDeduct(orderId)补偿扣减。
补偿方法核心:
public void compensateDeduct(Long orderId) { Order order = orderMapper.selectById(orderId); if (order.getStatus() == 2) { // 仍是待支付状态,说明回调丢失 stockService.deductStock(order.getItems()); order.setStatus(3); // 已支付 order.setPayTime(LocalDateTime.now()); orderMapper.updateById(order); } }5. 避坑指南:那些让开发者凌晨三点还在抓头发的典型问题
5.1 现象:小程序首页空白,Network 显示401 Unauthorized
原因:后端 JWT 鉴权拦截器未放行/menu/list等免登录接口,或前端未在请求头携带Authorization: Bearer xxx。
解决:
- 检查
SecurityConfig.java中antMatchers("/menu/**").permitAll()是否配置; - 若需登录态,确认
app.js中wx.getStorageSync('token')是否存在,且wx.request的header正确拼接。
5.2 现象:切换门店后菜单不变,Network 显示请求 URL 中storeId仍为旧值
原因:前端未正确读取wx.getStorageSync('currentStore'),或loadMenuList()方法未传参storeId。
解决:
- 在
index.js的onLoad生命周期中打印console.log(wx.getStorageSync('currentStore')); - 检查
loadMenuList调用处是否遗漏参数:this.loadMenuList(this.data.currentStore.id)。
5.3 现象:支付成功后订单状态卡在「待支付」,后台无任何日志
原因:微信支付回调地址未在公众号/小程序后台配置,或application.yml中wechat.notify-url域名未备案(必须是 HTTPS 且在白名单)。
解决:
- 登录微信商户平台 → 「产品中心」→ 「开发配置」→ 「支付回调URL」填写
https://yourdomain.com/pay/callback; - 本地调试时,用
ngrok或localtunnel将localhost:8080映射为 HTTPS 域名,并填入回调地址。
5.4 现象:多门店下,A店修改菜品价格,B店菜单同步变更
原因:menu表设计错误,缺少store_id字段,导致所有门店共用同一份菜品数据。
解决:
- 执行 SQL 添加字段:
ALTER TABLE menu ADD COLUMN store_id BIGINT NOT NULL DEFAULT 0 AFTER id;; - 修改所有
menu相关查询 XML,WHERE 条件增加AND store_id = #{storeId}; - 初始化数据时,为每条菜品记录插入对应
store_id。
5.5 现象:并发下单时出现超卖,库存扣减为负数
原因:stockService.deductStock()使用普通 SQLUPDATE stock SET quantity = quantity - ? WHERE id = ? AND quantity >= ?,但未加SELECT ... FOR UPDATE锁表。
解决:
- 改用悲观锁:在事务中先
SELECT quantity FROM stock WHERE id = ? FOR UPDATE; - 或升级为 Redis 原子操作:
DECRBY stock:1001 2(菜品ID为1001,扣2份),再判断返回值是否 ≥0。
6. 真实可用的验证技巧:三步确认你的多门店点餐系统已就绪
6.1 门店维度数据隔离验证表
用以下 SQL 快速验证多门店数据是否真正隔离(以菜单为例):
| 验证目标 | SQL 示例 | 期望结果 |
|---|---|---|
| A店有菜品「红烧肉」 | SELECT COUNT(*) FROM menu WHERE store_id = 1 AND name = '红烧肉'; | > 0 |
| B店无该菜品 | SELECT COUNT(*) FROM menu WHERE store_id = 2 AND name = '红烧肉'; | = 0 |
| 同一菜品不同价格 | SELECT store_id, price FROM menu WHERE name = '可乐' AND status = 1; | 返回多行,price 值各不相同 |
提示:若第一条 SQL 返回 0,说明初始化脚本未按门店导入数据,需检查
data.sql中INSERT INTO menu语句是否漏写store_id值。
6.2 外卖/自取混合下单压力测试脚本
用 JMeter 模拟 50 并发用户,分别执行两种下单流程:
- 线程组1(自取):HTTP 请求
/order/create,Body 中"orderType":1,"storeId":1; - 线程组2(外卖):同上,但
"orderType":2; - 监听器:聚合报告中关注「90% Line」响应时间(应 < 800ms)、「Errors」率(应为 0);
- 数据库验证:压测后执行
SELECT COUNT(*), order_type FROM order GROUP BY order_type;,确认两类订单数量与线程组比例一致。
6.3 微信支付沙箱环境联调 checklist
生产环境前,必须用微信支付沙箱验证资金流:
- 商户平台开启沙箱环境,获取沙箱
mch_id和key; application.yml中替换:
wechat: sandbox: true mch-id: your_sandbox_mch_id key: your_sandbox_key- 调用
/pay/unifiedorder接口,返回return_code=SUCCESS且result_code=SUCCESS; - 用沙箱
notify_url接收模拟回调(微信提供沙箱回调触发工具); - 检查订单状态是否由
2(待支付)变为3(已支付),且库存准确扣减。
我带过的几个模拟项目X,都是卡在沙箱回调验签失败——根源是沙箱key末尾多了空格,复制时没注意。现在我的习惯是:粘贴完立刻echo "xxx" | md5sum对比官方文档给出的 MD5 值。这种细节不写进文档,但能帮你省下 6 小时排查时间。
希望帮到你。
本文还有配套的精品资源,点击获取