餐饮小助手全栈实战:Spring Boot+Vue+微信小程序三端搭建
2026/9/16 7:51:46 网站建设 项目流程

简介:一套可直接用于毕业设计或课程项目的餐饮小助手系统,后台基于Java与Maven构建,管理端使用Vue,客户端为微信小程序,完整覆盖菜品、商品、订单、用户等核心业务模块,适合想快速上手前后端分离开发流程的学习者。压缩包共684个文件,约11.85MB,主要包含Java源码、XML映射与配置、SQL数据库脚本、Vue组件及样式,以及小程序端WXML/WXSS/JS/JSON页面文件,并附带JAR依赖与Maven工程文件,整体目录清晰,导入IDE后便于理解分层结构与运行调试。目前已有817人学习下载,包内提供了订单、菜品、商品、用户等模块的Controller与实体类设计,以及可直接导入的SQL初始化脚本,方便读者复用或在此基础上改造,节省从零搭建的时间,也能更好地支撑毕业设计答辩。

1. 餐饮小助手系统:后台 + Vue 管理端 + 微信小程序三端怎么搭

餐饮小助手系统(后台 + Vue + 微信小程序)这类项目名在社区里出现频率很高,它本质上是一套前后端分离的餐饮点餐管理方案:后台负责数据与业务规则,Vue 管理端给老板和店员操作,微信小程序给顾客扫码点餐。做这套东西的多是两类人,一类拿它当微信小程序毕业设计的模板,一类是给中小餐饮店做私有化系统的开发者。真正拉开差距的不是 CRUD 本身,而是三端对订单状态的认知是否一致——后台推进、小程序展示、Vue 端审核,任何一端动了状态码,另外两端都要重新验证。下面按后台、Vue、小程序端的顺序,把表结构、鉴权、状态机和联调细节一次讲清楚。

2. 后台接口层:从 MySQL 表结构到 JWT 登录与订单状态机

2.1 餐饮业务的最小表结构:菜品、桌台、订单、订单明细

后台是整个系统的数据中枢,Vue 管理端和微信小程序消费的都是后台暴露出来的接口,所以这是典型的 springboot vue 前后端分离结构。我一般会把后台拆成 system(员工、角色)和 biz(分类、菜品、桌台、订单)两组模块,业务表最少五张:dish_category 分类、dish 菜品、table_info 桌台、orders 订单主表、order_item 订单明细。先定表再写接口,因为三端的页面全是围绕这几张表转的,后期改表结构会牵连两端联调。

CREATE TABLE dish_category ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, sort_order INT DEFAULT 0, status TINYINT DEFAULT 1 COMMENT '1启用 0停用' ); CREATE TABLE dish ( id BIGINT PRIMARY KEY AUTO_INCREMENT, category_id BIGINT NOT NULL, name VARCHAR(100) NOT NULL, price INT NOT NULL COMMENT '价格按分存储', image VARCHAR(255), stock INT DEFAULT 9999, status TINYINT DEFAULT 1 ); CREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, table_id BIGINT, status TINYINT NOT NULL COMMENT '1待支付 2已支付 3制作中 4已完成 5已取消', total_amount INT NOT NULL COMMENT '单位分', create_time DATETIME NOT NULL ); CREATE TABLE order_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL, dish_id BIGINT NOT NULL, dish_name VARCHAR(100), price INT COMMENT '下单时价格快照', quantity INT NOT NULL );

这套建表语句里有三个约定值得在项目开始时定死。第一,金额用 INT 存"分"而不是 DECIMAL,避免浮点误差,前端展示时再除以 100。第二,订单明细冗余了 dish_name 和 price,因为菜品可能改名或调价,历史订单必须保留下单瞬间的快照,不能 join 菜品表取当前值。第三,order_no 用「yyyyMMddHHmmss + 4 位随机数」拼出来,比自增 ID 更适合当订单号展示,也方便后续按单号对账。

订单状态用数字常量比字符串好维护,状态码含义三端必须共用一份定义:

status含义触发方
1待支付小程序提交订单后
2已支付支付回调或前端轮询确认
3制作中Vue 管理端接单
4已完成Vue 管理端出餐
5已取消超时或用户主动取消

2.2 JWT 登录鉴权与接口拦截器配置

管理端和小程序共用同一套登录鉴权,差异只在登录接口的参数:管理端用账号密码,小程序用 wx.login 的 code 去后台换 openid。有效的做法是把校验逻辑收拢到一个拦截器里,拦截 /api/** 下除登录、支付回调之外的所有请求,Controller 里不再重复写鉴权判断。我常用的 JwtInterceptor 长这样:

@Component public class JwtInterceptor implements HandlerInterceptor { @Value("${jwt.secret}") private String secret; @Override public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) { // 请求头里取 token,校验失败直接返回 401 String token = req.getHeader("Authorization"); if (token == null || !JwtUtil.verify(token, secret)) { res.setStatus(401); return false; } req.setAttribute("userId", JwtUtil.getUserId(token)); return true; } }

登录接口签发 token 时,把 userId 和 role 放进 payload,但绝不放密码。jwt.secret 在生产环境必须通过环境变量注入,application.yml 里只留占位符,别把密钥提交进代码仓库;token 过期时间建议管理端 12 小时、小程序 7 天,顾客不可能每天重新登录一次小程序。被拦截器返回的 401,小程序端和 Vue 端的请求封装里都要做统一跳登录处理,否则会出现接口报错但用户毫无感知的情况。

提示:微信小程序的 openid 属于敏感信息,code2Session 的调用要放在后台做,小程序的请求体里只传 code,不要在前端做任何解密逻辑。

2.3 订单状态机的推进方式与并发保护

订单状态只能沿 1→2→3→4 单向推进,5 是从待支付分支出来的取消态。很多新手直接在 Service 里先查询订单、改状态、再 updateById,一旦两个请求同时读到 status=2,同一单会被推进两次。我习惯用条件更新替代"读-改-写",核心方法就一段:

@Transactional public void changeOrderStatus(Long orderId, Integer prevStatus, Integer newStatus, Long operatorId) { // 只有影响行数为 1 才说明状态被本次请求抢先更新 int rows = orderMapper.compareAndSetStatus(orderId, prevStatus, newStatus); if (rows != 1) { throw new BizException("订单状态已变化,请刷新后重试"); } // 状态推进成功后,再执行库存扣减、打印标签等后续动作 }

对应的 SQL 是整条状态流里最核心的一行。在 UPDATE 的 WHERE 条件里带上前置状态,等于把并发控制交给数据库的行锁:两个请求同时进来,只有一个 affected rows 会返回 1,比在代码里 synchronized 或引入分布式锁都轻量,也最容易在三端联调时把行为讲清楚。

UPDATE orders SET status = #{newStatus}, update_time = NOW() WHERE id = #{orderId} AND status = #{prevStatus}

affected rows 为 1 才说明操作者成功把状态从 prevStatus 抢过来,为 0 就说明状态已经被别人推进,直接抛业务异常让前端刷新。桌台状态也可以照搬这个套路:用【空闲/就餐中】字段标记,点餐时条件更新抢占桌台,避免两桌顾客同时下单落到同一张台。

3. Vue 后台管理端:路由守卫、axios 封装与菜品管理页

3.1 创建 Vue 项目与依赖安装的版本选择

管理端承担菜品维护、订单审核、桌台管理三类页面。第一步不是写代码,而是确认 vue 的大版本:Vue 2 配 element-ui、vue-router 3、vuex 3,Vue 3 配 element-plus、vue-router 4、pinia。vue 安装依赖时最常见的翻车现场是把 element-ui 装进 Vue 3 项目,页面白屏报 unknown custom element,查半天才发现是版本错配。按 Vue 2 的路线,装依赖我固定用下面这条:

vue create restaurant-admin cd restaurant-admin # element-ui 锁 2.x,避免和 Vue 3 的 element-plus 混淆 npm install axios element-ui@2.15.8 --save

Element UI 2.x 已经停止大版本迭代,但搭配 Vue 2 的生态最省心;如果团队用 Vue 3,把 element-ui 换成 element-plus 即可,router 和状态管理的 API 变化不影响菜品页这类业务组件。装完依赖后先改 vue.config.js 的 devServer.proxy,把 /api 前缀的请求转发到后台的 8080 端口,开发期就不用纠结跨域头了。

3.2 基于路由元信息的登录与角色校验

管理端通常分管理员和收银员两种角色,我把权限判断放在路由守卫里,配合侧边栏只渲染有权限的菜单。vue 路由参数的 meta 字段就是干这个用的,在路由表里声明权限要求,组件里不各自判断。守卫代码很固定,直接抄:

const routes = [ { path: '/login', component: Login }, { path: '/', component: Layout, children: [ { path: 'dish', component: DishList, meta: { title: '菜品管理', requiresAuth: true, roles: ['ADMIN'] } }, { path: 'order', component: OrderList, meta: { title: '订单审核', requiresAuth: true, roles: ['ADMIN', 'CASHIER'] } } ] } ]; router.beforeEach((to, from, next) => { const token = localStorage.getItem('token'); const role = localStorage.getItem('role'); // 需要登录但没 token,先去登录页 if (to.meta.requiresAuth && !token) { next('/login'); } else if (to.meta.roles && to.meta.roles.indexOf(role) === -1) { // 角色不在允许列表里,回首页 next('/dashboard'); } else { next(); } });

meta 字段控制在三个,够用且好维护:

meta 字段类型作用
titleString侧边栏菜单文字和浏览器标题
requiresAuthBoolean是否需要登录态
rolesArray允许访问的角色列表,缺省表示登录即可

需要强调的是,前端路由守卫只解决"看不见、进不去"的体验问题,接口权限必须在后台拦截器里再校验一次。只靠隐藏菜单防不住别人直接请求后台接口,之前有项目只做前端控制,结果运营后台被人用 Postman 改掉了所有菜品价格。

3.3 菜品管理页:表格、分页与图片上传

菜品页是后台最典型的 CRUD 场景,我用 el-table 加 el-dialog 的组合:列表展示缩略图、名称、分类、价格、状态,新增和编辑共用一个弹窗。图片上传单独走 el-upload,action 指向后台的上传接口,关键点是 headers 里带上 token,否则后台拦截器直接 401:

<el-upload action="/api/admin/upload" :headers="{ Authorization: localStorage.getItem('token') }" :on-success="handleUploadSuccess" name="file"> <!-- 上传接口必须带 token,否则后台拦截器返回 401 --> <el-button size="small">上传图片</el-button> </el-upload>

图片上传成功后,后台返回的是相对路径 /uploads/xxx.jpg,不是完整域名。前端展示时用环境变量里的 baseURL 拼一下,开发环境和生产环境各有一套域名,存绝对地址会在服务器迁移后全部裂图。分页参数固定为 page(从 1 开始)和 pageSize(默认 10),后台统一返回 { total, list },这个结构管理端表格和小程序订单列表都能复用。

3.4 axios 实例封装与统一错误处理

管理端所有请求走同一个 axios 实例,拦截器里集中处理三件事:附加 token、解析业务 code、401 跳登录。业务代码里只关心 data 部分,不用每个页面重复写错误提示。一个能直接粘贴的封装:

import axios from 'axios'; const service = axios.create({ baseURL: '/api', timeout: 10000 }); // 请求拦截:从 localStorage 取 token 拼到 header service.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) config.headers.Authorization = token; return config; }); // 响应拦截:code 401 清理登录态并跳转登录页 service.interceptors.response.use(res => { if (res.data.code === 401) { localStorage.clear(); location.href = '/login'; } return res.data; }); export default service;

后台接口统一返回 { code, message, data },code 为 0 表示成功,非 0 的 message 直接拿来弹 ElMessage。这个约定看似简单,却是三端联调时定位问题最快的抓手:看到 code 就能判断是业务错误、参数错误还是网络问题,不用一层层翻 Status Code 和响应体。你如果做的是 vue 项目实战练习,把这一层提前封装好,后面所有页面都能省掉重复的 loading 和错误处理代码。

4. 微信小程序端:登录态、点餐购物车与订单提交

4.1 小程序页面结构与 wx.request 封装

小程序端面向顾客,页面比管理端少,但状态同步更讲究。目录按 pages、utils、components 组织,utils 里放 request.js 和 auth.js。如果团队用 uniapp 打包多端,把 wx.request 换成 uni.request、wx.setStorageSync 换成 uni.setStorageSync,页面逻辑完全不用动,所以这套设计同样适用于 uniapp 微信小程序项目。request.js 是所有页面的唯一网络入口:

// utils/request.js const BASE_URL = 'http://192.168.1.100:8080/api'; function request(path, method, data) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + path, method: method || 'GET', data: data || {}, header: { 'Content-Type': 'application/json', // 从本地缓存取 token,登录态失效统一跳登录页 'Authorization': wx.getStorageSync('token') || '' }, success: (res) => { if (res.statusCode === 401) { wx.removeStorageSync('token'); wx.navigateTo({ url: '/pages/login/login' }); reject(res); return; } if (res.data.code !== 0) { wx.showToast({ title: res.data.message, icon: 'none' }); reject(res.data); return; } resolve(res.data.data); }, fail: () => { wx.showToast({ title: '网络连接失败', icon: 'none' }); reject(new Error('network error')); } }); }); } module.exports = { request, BASE_URL };

BASE_URL 在开发阶段填电脑的局域网 IP 加后台端口,真机预览时手机和电脑必须连同一个网段,否则请求超时。上线前换成正式 HTTPS 域名,并在微信公众平台配置 request 合法域名。调试阶段可以在开发者工具里勾选不校验合法域名,也可以配合抓包工具看请求参数和响应体,但这两个都是开发期手段,上线配置以公众平台后台为准。

4.2 点餐页购物车:setData 与总价联动

点餐页是高频交互,加菜、改份数、删菜都必须即时响应。小程序没有 Vue 那套响应式依赖,所有界面变更都要通过 setData 触发,购物车设计成纯前端数据结构,提交订单时才传给后台。注意每次 setData 前先拷贝数组,避免直接修改 this.data 造成渲染不同步:

Page({ data: { cart: [], totalCount: 0, totalAmount: 0 }, addToCart(e) { const dish = e.currentTarget.dataset.dish; const cart = this.data.cart.slice(); // 已存在则数量加一,不存在则插入新项 const index = cart.findIndex(item => item.dishId === dish.id); if (index > -1) { cart[index].quantity += 1; } else { cart.push({ dishId: dish.id, name: dish.name, price: dish.price, quantity: 1 }); } this.setData({ cart, totalCount: cart.reduce((sum, i) => sum + i.quantity, 0), totalAmount: cart.reduce((sum, i) => sum + i.price * i.quantity, 0) }); }, clearCart() { this.setData({ cart: [], totalCount: 0, totalAmount: 0 }); } });

cart 里存菜品快照而不是完整对象,防止 setData 传大对象导致页面卡顿。金额基准是 dish.price,单位分;小程序端先按分累加,下单时把 totalAmount 传给后台,后台再按数据库价格重新算一遍,前端总额只是顾客看到的预估,最终金额以后台校验为准。这里如果数据库价格和展示价格不一致,问题一定出在后台下单接口,别在前端查。

4.3 提交订单、拉起支付与状态轮询

顾客点完餐点结算,小程序依次做三件事:请求后台预创建订单、调起 wx.requestPayment、主动轮询订单状态。预创建接口把购物车明细发给后台,后台校验库存并返回 orderNo;支付成功后,微信支付的回调会更新后台订单状态,但回调有延迟,所以小程序端在支付回调成功里再轮询几次订单状态兜底:

async function submitOrder(tableId, cart) { // 第一步:后台校验并生成订单,返回订单号 const orderNo = await request('/order/submit', 'POST', { tableId, cart }); // 第二步:后台调微信支付统一下单,返回支付参数 const pay = await request('/pay/create', 'POST', { orderNo }); wx.requestPayment({ timeStamp: pay.timeStamp, nonceStr: pay.nonceStr, package: pay.package, signType: 'RSA', // 与后台签名算法保持一致:v3 用 RSA paySign: pay.paySign, success: () => pollOrderStatus(orderNo), fail: () => wx.showToast({ title: '支付取消', icon: 'none' }) }); } function pollOrderStatus(orderNo) { let times = 0; const timer = setInterval(async () => { times += 1; const order = await request('/order/detail', 'GET', { orderNo }); // 状态落到已支付或制作中,说明后台已确认,跳详情页 if (order.status === 2 || order.status === 3) { clearInterval(timer); wx.redirectTo({ url: '/pages/order/detail?orderNo=' + orderNo }); } else if (times > 5) { clearInterval(timer); } }, 1000); }

小程序端和后台对接时,这五个字段必须一字不差:

字段类型说明
orderNoString订单号,提交和查询统一用它
tableIdNumber桌台 ID,扫码进页面时从二维码参数带入
statusNumber订单状态码,与后台订单状态机保持一致
totalAmountNumber单位分,后端重新校验
signTypeString支付签名类型,RSA 还是 MD5 要跟后台确认

wx.requestPayment 弹不出支付框时,先查后台返回的 prepay_id 是否有效、timeStamp 是否是秒级时间戳,这两个坑占支付联调失败的一大半。轮询频率 1 秒一次、最多 5 次就够,不要把定时器做成无限循环,后台回调通常在几百毫秒内就能到达。

5. 三端联调的验证清单与 Vue 打包部署细节

5.1 Vue 打包后的路由模式与 Nginx 配置

管理端上线时最容易踩的坑是 vue 打包后布局异常和刷新 404。用 history 模式打包后,直接访问 /dish 再刷新,Nginx 找不到对应文件返回 404;用 hash 模式没这问题但 URL 带 #。我保留 history 模式,用 try_files 兜底,同时把 /api 请求转到后台服务:

server { listen 80; server_name admin.example.com; root /usr/share/nginx/html; index index.html; location / { # 前端 history 路由刷新兜底 try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; } }

publicPath 也要对:绑定根路径用 /,部署在子路径就写子路径名,否则打包后的 js 和 css 资源 404,页面只剩 html 骨架,这就是最常见的 vue 打包后布局异常。

5.2 小程序真机预览的合法域名配置

小程序联调分三步走:先在开发者工具里不校验合法域名调通业务逻辑,再用体验版二维码在真机上验证 wx.requestPayment,最后把正式域名配到公众平台的 request 合法域名。第二步最容易漏,支付接口在开发者工具里是模拟环境,签名错误、回调失败这些只能在真机上暴露出来。另外真机调试时如果手机连不上开发机,先检查 BASE_URL 是不是局域网地址,别拿 localhost 在真机上跑。

5.3 金额、时间、分页的三端统一约定

最后把三件容易扯皮的约定写进接口文档:金额以分为单位的整数传输,前端展示时除以 100;时间统一用 yyyy-MM-dd HH:mm:ss 字符串,避免时区解析差异;分页固定 page 和 pageSize,返回 { total, list }。后台接口统一返回 { code, message, data },联调时打开 Network 面板,code 非 0 直接按 message 排错。上线前我用一条 curl 把主链路跑一遍:

# 模拟小程序提交订单,验证库存与金额校验 curl -X POST http://admin.example.com/api/order/submit \ -H "Authorization: $(cat /tmp/token)" \ -d '{"tableId":1,"items":[{"dishId":3,"quantity":2}],"totalAmount":5800}'

从订单提交、库存校验到支付回调,把餐饮小助手三端共用的主链路端到端验一次,比开会评审代码更能暴露问题。

本文还有配套的精品资源,点击获取

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

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

立即咨询