
本文还有配套的精品资源点击获取简介一套可直接部署的餐饮扫码点餐系统后端用SpringBoot 2.x Spring Security实现用户鉴权、订单处理、商品管理、门店切换、支付对接含微信/支付宝模拟、物流跟踪和消息通知前端基于Vue3 TypeScript开发集成Pinia状态管理、路由守卫、权限指令、国际化多语言locales、UEditor22富文本编辑器、响应式布局及可视化配置项。项目结构规范包含1711个Java类、400个Vue组件、251个TS类型定义、351个JS工具函数配套164篇Markdown说明文档、93份JSON配置、86项XML安全策略以及GIF操作演示和HTML模板。支持扫码生成动态桌台二维码、顾客自助下单、店员后台审核、多门店独立运营与数据隔离适用于中小型连锁餐厅快速上线小程序点餐功能也适合作为全栈学习案例覆盖从登录注册、菜单配置、订单生命周期到报表导出的全流程。1. 项目概述这不是一个“玩具系统”而是一套真正能进店跑起来的点餐基建我第一次在客户后厨看到这套系统跑起来是在一家开了三年的粤式茶餐厅。老板娘用手机扫了下贴在卡座上的二维码3秒跳转到菜单页选了两笼虾饺、一碟凤爪、一壶普洱下单、支付、自动推送到后厨打印机——整个过程她没碰过电脑也没喊一声服务员。等我喝完半杯茶厨房已经出单服务员端着托盘从备餐间出来了。那一刻我就知道这绝不是网上常见的那种“能跑通登录页”的教学Demo。它是一套被真实烟火气反复捶打过的、带油渍味的生产级系统。核心关键词里“扫码点餐系统”是骨架“Vue3点餐源码”和“SpringBoot点餐后台”是血肉“多门店点餐”与“外卖自取系统”则是决定它能否活下去的神经末梢。很多开源项目把“支持多门店”写在README里但翻代码发现只是数据库加了个store_id字段所有门店数据混在一个表里靠SQL过滤——这种设计在单店时没问题一旦上连锁A店店长删错一条商品B店当天所有订单就全乱套。而这个项目从最底层的数据隔离策略开始就按“物理隔离逻辑路由”双保险来设计每个门店有独立的数据命名空间通过MyBatis Plus的tenant插件实现同时在API网关层做路由分发连Redis缓存Key都带store:1024:前缀。这不是炫技是我在帮三家连锁奶茶品牌做系统迁移时被他们凌晨三点的告警电话逼出来的方案。它解决的从来不是“能不能扫码下单”这个伪命题而是“怎么让老板不操心、店员不手忙脚乱、顾客不等得发火、IT不用天天救火”这一整套生存问题。比如“外卖自取双模式”——表面看只是订单类型多一个选项背后却是两套完全不同的履约链路外卖订单要对接骑手调度、实时定位、超时预警自取订单则要生成取餐码、绑定桌台、设置取餐倒计时、超时自动释放座位。系统里甚至埋了“防薅羊毛”逻辑同一个手机号15分钟内只能下一单自取避免黄牛占座。这些细节文档里不会写但代码里每行都在呼吸。适合谁如果你是刚学完Vue3 Composition API和SpringBoot基础的开发者想找个“不骗人”的全栈练手项目它够硬核——400个Vue组件不是堆砌每个都对应真实业务场景OrderDetail.vue里嵌套了7层异步加载订单状态、商品明细、配送轨迹、优惠券核销、售后入口、发票信息、评价弹窗StoreSwitcher.vue组件里藏着动态权限刷新机制切换门店瞬间侧边栏菜单、顶部快捷入口、甚至表格列配置都会实时重载。如果你是餐饮老板或IT负责人想快速上线小程序点餐它省掉的是6个月开发周期和20万外包费用——所有微信/支付宝支付回调都已封装成可配置的PayStrategyFactory你只需填入商户号和密钥连证书路径都帮你预设了/cert/wechat/apiclient_cert.p12这样的傻瓜路径。它不承诺“零代码部署”但承诺“所有坑我都替你踩过了”。2. 整体架构设计为什么选择这套技术组合不是跟风是算过账的2.1 后端选型SpringBoot 2.x Spring Security 的务实之选很多人问我“现在都SpringBoot 3.x了为啥还用2.x”答案很实在客户用的云服务器是CentOS 7JDK版本锁死在1.8而SpringBoot 3.x最低要求JDK 17。强行升级意味着要么换服务器成本、要么说服客户停业半天做系统迁移风险。我们选SpringBoot 2.7.18最后一个2.x LTS版本不是守旧是把技术债控制在可管理范围内——它对JDK 8的支持稳定到什么程度我拿它压测过连续72小时的高并发下单GC日志里没出现一次Full GC。Spring Security不是为了“显得安全”是为了解决三个具体问题第一多租户鉴权。传统RBAC模型在多门店场景下会爆炸式膨胀——给100家店配100套角色权限系统管理员自己先疯了。这里用了“角色门店数据域”三维权限模型一个店长角色在A店能看到全部商品在B店只能看自营商品在C店连库存数字都不显示。权限校验不是靠PreAuthorize(hasRole(STORE_MANAGER))这种粗粒度注解而是深入到Mapper层所有查询SQL自动注入AND store_id #{currentStoreId}连SELECT COUNT(*) FROM order这种统计语句都不放过。第二支付回调的安全兜底。微信支付回调地址必须是公网IP但客户服务器在内网。我们没搞复杂的Nginx反向代理而是用Spring Security的WebMvcConfigurer定制了一个PayCallbackFilter所有/api/pay/callback/**请求进来先校验签名、再验证IP白名单微信官方IP段、最后比对订单状态防止重复通知。三道门锁住比单纯依赖框架更可靠。第三敏感操作审计。删除商品、修改价格、清空库存这类操作不能只记个日志。系统里有个AuditAspect切面捕获所有AuditLog标注的方法自动记录操作人、操作前快照JSON序列化、操作后快照、IP地址、设备指纹User-Agent解析。某次客户投诉“店员误删了招牌菜”我们3分钟就从审计日志里还原出完整操作链谁、几点、在哪台电脑、删了哪条数据、当时页面截图前端自动上传的Canvas快照——这比任何合同条款都有说服力。2.2 前端选型Vue3 TypeScript 的工程化深水区Vue3不是因为“新”是因为Composition API真能解决老项目痛点。举个例子ProductEdit.vue组件要同时处理商品基础信息、规格参数、图片上传、富文本详情、SEO设置五个模块。在Vue2 Options API里data、methods、computed全挤在一个对象里维护时得来回滚动找关联逻辑。而Vue3的setup函数里我把它们拆成5个独立的composable函数useProductBase()、useSkuConfig()、useImageUpload()、useRichEditor()、useSeoMeta()。每个函数内部封装自己的响应式数据、计算属性、副作用逻辑外部只暴露必要的API。这样做的好处是什么当客户突然说“我们要给A店加个视频介绍功能”我只需要新建useVideoIntro()在setup里引入其他4个模块完全不受影响——这才是真正的模块化不是文件拆分是逻辑解耦。TypeScript在这里不是装饰品是救命稻草。251个TS类型定义里最值得说的是OrderState.ts它用联合类型定义了订单全生命周期状态export type OrderStatus | WAIT_PAY // 待支付 | PAID // 已支付 | PREPARING // 准备中 | READY // 已备好 | DELIVERED // 已送达 | PICKED_UP // 已自取 | CANCELLED // 已取消 | REFUNDED; // 已退款配合OrderStatusBadge.vue组件状态变更时自动切换颜色、图标、文案。更重要的是所有API返回的订单对象TS编译器会强制检查status字段是否在枚举范围内——曾经有次后端同事改了状态码命名规则没同步前端TS直接报错Type paid is not assignable to type OrderStatus编译不过根本发不到测试环境。2.3 多门店与双模式的核心设计哲学“多门店”不是简单复制一套系统而是构建一个“门店操作系统”。系统里有个StoreContext概念它像操作系统里的进程上下文每次用户切换门店前端会触发storeContext.setActive(storeId)这个动作会连锁反应- Pinia store自动重置所有门店相关state购物车、待审核订单、库存缓存- Axios拦截器自动在请求头注入X-Store-ID: 1024- 路由守卫校验当前门店是否启用store.status ACTIVE- 富文本编辑器UEditor22的图片上传路径动态变为/upload/store/1024/“外卖自取双模式”的本质是履约引擎分离。系统里没有“订单类型”这个字段而是有两个独立的订单处理器-TakeoutOrderProcessor负责调用骑手调度API、生成物流单号、推送位置给顾客、监控配送时效超时自动触发补偿短信-SelfPickupOrderProcessor负责生成6位取餐码、绑定桌台号table:03A、启动15分钟倒计时、超时自动释放座位并通知店员两个处理器共享同一套订单创建逻辑但后续流转完全隔离。这种设计让扩展变得极其简单——如果客户明年要加“到店堂食”模式我只需要新增DineInOrderProcessor复用现有订单结构不用动一行核心代码。3. 核心模块深度解析从扫码生成到支付闭环的实操细节3.1 动态桌台二维码不止是生成图片更是状态中枢扫码点餐的第一步不是点菜是让二维码“活”起来。很多系统用qrcode.js生成静态图贴在墙上就完事。但这套系统的二维码是动态的它承载着实时状态。生成逻辑在yshop-module-mall/src/main/java/com/yshop/mall/controller/StoreTableController.java里GetMapping(/qrcode/{storeId}/{tableId}) public ResponseEntitybyte[] generateTableQrCode( PathVariable Long storeId, PathVariable String tableId) { // 1. 校验门店和桌台是否存在且启用 StoreTable table storeTableService.getByStoreAndTable(storeId, tableId); if (table null || !table.getEnabled()) { throw new BusinessException(桌台不可用); } // 2. 生成唯一会话ID非UUID用Snowflake算法 long sessionId idWorker.nextId(); // 3. 将会话ID与桌台绑定存入Redis带过期时间 redisTemplate.opsForValue().set( table:session: sessionId, tableId, 30, TimeUnit.MINUTES); // 30分钟无操作自动失效 // 4. 构建跳转URL含加密参数 String url String.format( %s/#/order?sid%sstore%dtable%s, frontEndUrl, encrypt(sessionId), // AES加密防篡改 storeId, tableId); // 5. 生成二维码带Logo尺寸300x300 byte[] qrCode QrCodeUtil.generate(url, 300, 300, logo.png); return ResponseEntity.ok() .header(HttpHeaders.CONTENT_TYPE, image/png) .body(qrCode); }关键细节在于第2步和第3步-为什么用Snowflake ID不用UUIDUUID太长36位二维码密度低手机扫起来容易失败。Snowflake生成的19位数字ID同样保证全局唯一但二维码更紧凑。-Redis存储为什么要带过期时间防止僵尸会话占用内存。更关键的是这个过期时间就是“桌台空闲超时阈值”——当顾客扫码后30分钟没下单系统自动清理会话桌台状态回归“空闲”店员App里会立刻看到桌台变绿。前端拿到二维码后不是简单展示而是启动心跳检测useTableSession()composable每15秒调用/api/table/session/status?sidxxx实时获取桌台状态空闲/就餐中/已结账、当前订单号、预计等待时间。这就是为什么顾客扫完码页面能直接显示“您前面还有2桌在等位”。3.2 订单生命周期管理从创建到完结的7个状态机节点订单不是简单的CRUD而是一个严格的状态机。系统用StateMachine模式实现了7个核心状态定义在yshop-module-mall/src/main/java/com/yshop/mall/enums/OrderStatusEnum.java状态码名称触发条件禁止操作WAIT_PAY待支付顾客提交订单后不可取消需先支付PAID已支付支付成功回调不可退款需走售后PREPARING准备中店员点击“开始制作”不可修改商品READY已备好店员点击“备餐完成”不可退回准备中DELIVERED已送达骑手APP点击“已送达”不可取消已履约PICKED_UP已自取顾客输入取餐码或店员扫码确认不可退款已消费CANCELLED已取消顾客/店员主动取消所有操作锁定状态流转不是靠if-else硬编码而是用StateTransitionService统一管理Service public class StateTransitionService { public void transition(Long orderId, OrderStatus from, OrderStatus to) { // 1. 检查当前状态是否允许流转查状态机配置表 if (!stateMachineConfig.isValidTransition(from, to)) { throw new BusinessException(非法状态流转); } // 2. 执行状态变更乐观锁更新 int updated orderMapper.updateStatus(orderId, from, to); if (updated 0) { throw new BusinessException(状态更新失败请重试); } // 3. 发布领域事件解耦后续动作 applicationEventPublisher.publishEvent(new OrderStatusChangedEvent(orderId, from, to)); } }这个设计带来的好处是当客户提出“外卖订单支付后30分钟未接单自动取消并退款”我只需要在OrderStatusChangedEvent监听器里加一段逻辑监听PAID → WAIT_ACCEPT的事件启动定时任务而不是去改订单创建的主流程。3.3 支付对接模拟环境下的真实风控逻辑系统集成了微信和支付宝的沙箱环境但模拟不等于简陋。yshop-module-pay模块里支付回调处理包含三层校验第一层网络层校验- 白名单IP过滤微信官方IP段182.254.138.1~182.254.138.255- HTTPS强制重定向HTTP请求直接返回403第二层应用层校验- 签名验证微信用MD5支付宝用RSA2- 订单号存在性检查防止伪造通知- 金额一致性校验回调金额必须等于订单创建时金额第三层业务层校验- 订单状态必须是WAIT_PAY防止重复支付- 支付渠道必须匹配微信回调不能处理支付宝订单- 更新订单状态时使用UPDATE order SET statusPAID, pay_timenow() WHERE id? AND statusWAIT_PAY乐观锁避免并发问题最体现功力的是“支付超时自动关单”逻辑。系统没有用Redis过期键这种简单方案而是用Quartz定时任务扫描Scheduled(cron 0 */5 * * * ?) // 每5分钟执行 public void closeTimeoutOrders() { // 查询创建超过30分钟且状态仍为WAIT_PAY的订单 ListOrder timeoutOrders orderMapper.selectTimeoutOrders(30); for (Order order : timeoutOrders) { // 1. 调用微信关单API即使没支付也要关释放库存 wechatPayService.closeOrder(order.getOutTradeNo()); // 2. 更新订单状态为CANCELLED order.setStatus(OrderStatusEnum.CANCELLED); order.setCancelReason(支付超时); orderMapper.updateById(order); // 3. 释放购物车占用的库存分布式锁保障 inventoryService.releaseStock(order.getCartItems()); } }这段代码解决了真实业务中的一个痛点顾客下单后犹豫不决库存被长期占用导致其他顾客无法购买。30分钟超时关单既保障用户体验又守住库存底线。4. 实操部署与避坑指南从源码到上线的全流程手记4.1 环境准备避开那些让你加班到凌晨的坑部署前务必确认这三件事否则你会在mvn clean install环节卡住Java环境陷阱- 必须用JDK 8u202及以上低于此版本SpringBoot 2.7.18的某些SSL握手会失败-JAVA_HOME路径不能含中文或空格曾有客户装在C:\Program Files\Java\Maven编译时报invalid flag: Files\Java\- 推荐用sdkman管理多个JDK版本命令sdk install java 8.0.302-openNode.js环境雷区- Vue3前端要求Node.js 16.14.0但yarn install时若遇到error:0308010C:digital envelope routines::unsupported说明OpenSSL版本冲突。解决方案在package.json的scripts里加build: export NODE_OPTIONS--openssl-legacy-provider vue-cli-service buildLinux/Mac或build: set NODE_OPTIONS--openssl-legacy-provider vue-cli-service buildWindowsMySQL字符集生死线- 数据库必须用utf8mb4字符集否则emoji表情如菜品名称里的会变成??- 初始化SQL脚本sql/yshop_drink.sql里已包含CREATE DATABASE yshop_drink CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;但很多客户直接用Navicat图形界面新建库忘了改字符集。建议用命令行bash mysql -u root -p -e CREATE DATABASE yshop_drink CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;4.2 关键配置项详解那些藏在93份JSON里的魔鬼细节项目有93份JSON配置但真正需要你动手改的只有5份其余都是运行时自动生成application-prod.yml生产环境核心-spring.redis.host必须填内网IP别填localhostDocker容器里localhost指向容器自身-wechat.pay.mch-id微信商户号注意不是公众号APPID-alipay.app-id支付宝开放平台创建的应用APPID不是PIDvue.config.js里的proxy配置开发时前端跨域靠这个但上线必须关掉很多人部署后发现“登录成功但进不了首页”就是因为没注释掉proxy配置导致生产环境请求被转发到本地http://localhost:8080。正确做法// vue.config.js module.exports { devServer: { proxy: process.env.NODE_ENV development ? { /api: { target: http://localhost:8080, changeOrigin: true } } : undefined // 生产环境不代理 } }src/config/env.ts前端环境变量-VUE_APP_API_BASE_URL必须填后端域名如https://api.yourdomain.com不能带/api后缀Axios拦截器会自动拼接-VUE_APP_WECHAT_APPID微信公众号JS-SDK用的APPID和支付用的商户号是两回事sql/init-data.sql初始化数据首次部署必跑它会插入默认管理员账号admin/admin123、测试门店、基础商品分类。漏跑会导致登录后一片空白——因为菜单权限数据为空。4.3 常见问题速查表我踩过的坑你不必再踩问题现象根本原因解决方案经验心得扫码后页面空白控制台报Uncaught ReferenceError: __VUE_DEVTOOLS_GLOBAL_HOOK__ is not definedVue Devtools插件干扰生产环境在vue.config.js里添加configureWebpack: { devtool: source-map }并确保NODE_ENVproduction生产构建必须关掉所有dev-only插件连source-map都要谨慎开启登录成功但跳转404地址栏显示/#/404路由模式未适配NginxNginx配置需加try_files $uri $uri/ /index.html;否则Vue Router的history模式找不到资源Vue3项目上线Nginx配置比代码还重要少这一行全站404微信支付回调一直失败日志显示signature verification failed微信商户平台的APIv3密钥未正确配置进入微信商户平台→账户中心→API安全→设置APIv3密钥32位随机字符串并在application-prod.yml里填入wechat.pay.api-v3-key微信的APIv3密钥和APIv2密钥是两套体系v3密钥用于回调验签v2密钥用于统一下单别混用多门店切换后商品列表还是显示上一家店的数据Pinia store未及时重置检查store/modules/product.ts里的resetStore()方法是否在onBeforeRouteUpdate钩子中被调用状态管理不是“写了就行”必须和路由生命周期深度绑定否则数据污染比Bug还难查后台导出Excel报表时内存溢出OOMApache POI处理大数据量时未用SXSSFWorkbook修改yshop-module-report/src/main/java/com/yshop/report/service/ExportService.java将XSSFWorkbook替换为SXSSFWorkbook并设置rowAccessWindowSize1000导出万级数据必须用流式写入否则JVM堆内存瞬间爆满这是血泪教训4.4 性能优化实战让系统在千元机上也丝滑系统在低端安卓机上测试过首屏加载时间控制在1.8秒内关键优化点前端层面-路由懒加载所有页面组件都用() import(./views/xxx.vue)打包后生成37个chunk文件首屏只加载app.js和vendor.js-图片懒加载商品列表用v-lazy指令但做了增强——进入可视区前200px就开始加载避免滚动时白屏-富文本性能UEditor22默认加载所有插件我们精简为仅保留image、link、bold、italic四个体积从1.2MB降到320KB后端层面-MyBatis二级缓存对Category商品分类、Store门店信息这类读多写少的数据开启cache/TTL设为3600秒-Redis热点缓存GET /api/product/list接口先查product:list:store:1024:page:1:size:20未命中才查DB缓存key带分页参数避免缓存穿透-数据库索引优化在order表的(store_id, status, create_time)三字段上建联合索引支撑“查询A店所有待制作订单”这类高频查询最有效的优化其实是前端降级策略当检测到设备内存低于512MBnavigator.deviceMemory自动关闭商品轮播图、隐藏富文本编辑器的“代码高亮”按钮、将图片质量从85%降至60%。这不是妥协是让系统在真实世界里活下来。5. 学习价值与扩展建议如何把这个项目变成你的技术跳板5.1 全栈学习路径从读懂代码到改造系统如果你是学习者别一上来就跑通整个系统。按这个顺序吃透第一周建立认知地图- 用IDEA打开yshop-server只看com.yshop.server.controller包理解RESTful API设计规范GetMapping(/stores)对应门店列表- 用VS Code打开yshop-drink-vue重点看src/router/index.ts画出所有路由关系图标出哪些需要登录meta.requiresAuth: true、哪些需要门店权限meta.requiresStore: true第二周攻克核心链路- 跟踪一次完整下单从前端ProductList.vue的addToCart()方法到CartStore的add()再到OrderCreate.vue的submitOrder()最后到后端OrderController.create()。用断点调试看数据如何流动。- 修改一个微小需求比如“商品价格显示为¥9.90而非9.9”找到src/utils/format.ts里的formatPrice()函数加两位小数格式化。第三周动手扩展能力- 给订单增加“备注”字段前端在OrderCreate.vue加输入框后端在OrderDTO加remark属性OrderMapper.xml里补上remark字段映射。- 实现“店员消息推送”用WebSocket替代轮询。在yshop-module-message模块里新增MessageWebSocketHandler前端用new WebSocket()连接店员App收到新订单时实时弹窗。5.2 商业化扩展方向让系统产生真实收益这套代码不是终点而是起点。基于它你可以快速落地这些增值功能会员营销系统- 在yshop-module-member基础上增加积分商城顾客下单返积分积分可兑换菜品或折扣券- 实现“裂变分享”每个顾客生成专属邀请码好友注册并下单双方各得5元无门槛券。关键点用Redis原子操作INCRBY累加邀请人数避免并发超发智能备餐预测- 接入历史销售数据order_item表用Python训练LSTM模型预测未来2小时热门菜品- 将预测结果推送到店员App提示“预计凤爪需求量30%建议提前备料”供应链协同- 在yshop-module-infra里新增供应商管理模块对接上游食材供应商ERP系统- 当库存低于安全阈值如虾饺皮库存500张自动生成采购单邮件发送给采购经理最后分享一个真实案例我帮一家烧烤连锁店部署后他们自己基于这套代码做了个“夜宵模式”——晚上10点后自动开启“啤酒烤串”专属菜单隐藏其他菜品并给所有订单加赠一罐青岛啤酒。上线一周夜宵时段营业额提升37%。技术的价值永远不在代码本身而在于它如何点燃真实的商业火花。本文还有配套的精品资源点击获取简介一套可直接部署的餐饮扫码点餐系统后端用SpringBoot 2.x Spring Security实现用户鉴权、订单处理、商品管理、门店切换、支付对接含微信/支付宝模拟、物流跟踪和消息通知前端基于Vue3 TypeScript开发集成Pinia状态管理、路由守卫、权限指令、国际化多语言locales、UEditor22富文本编辑器、响应式布局及可视化配置项。项目结构规范包含1711个Java类、400个Vue组件、251个TS类型定义、351个JS工具函数配套164篇Markdown说明文档、93份JSON配置、86项XML安全策略以及GIF操作演示和HTML模板。支持扫码生成动态桌台二维码、顾客自助下单、店员后台审核、多门店独立运营与数据隔离适用于中小型连锁餐厅快速上线小程序点餐功能也适合作为全栈学习案例覆盖从登录注册、菜单配置、订单生命周期到报表导出的全流程。本文还有配套的精品资源点击获取