
简介一套可直接用于毕业设计或课程项目的餐饮小助手系统后台基于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一旦两个请求同时读到 status2同一单会被推进两次。我习惯用条件更新替代读-改-写核心方法就一段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 3Vue 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-ui2.15.8 --saveElement 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-uploadaction 指向后台的上传接口关键点是 headers 里带上 token否则后台拦截器直接 401el-upload action/api/admin/upload :headers{ Authorization: localStorage.getItem(token) } :on-successhandleUploadSuccess namefile !-- 上传接口必须带 token否则后台拦截器返回 401 -- el-button sizesmall上传图片/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}从订单提交、库存校验到支付回调把餐饮小助手三端共用的主链路端到端验一次比开会评审代码更能暴露问题。本文还有配套的精品资源点击获取