ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

在线点餐小程序源码跑通指南:前后端分离架构与多门店扫码点餐实践

在线点餐小程序源码跑通指南:前后端分离架构与多门店扫码点餐实践 简介面向Java全栈学习者与高校课程设计、毕业设计场景这份在线点餐小程序项目提供了完整可运行的源码与配套说明覆盖扫码点餐、外卖与自取、多门店管理等典型业务。后端采用Java实现前端基于uniapp(vue3)构建整体为前后端分离架构适合用来理解小程序点餐系统从数据库设计到接口交互的完整链路。压缩包共包含2005个文件以Java源码、Vue页面、JavaScript逻辑为主辅以SQL脚本、HTML/CSS样式及YAML配置等便于直接导入工程并对照学习包体仅18.17MB下载与部署成本低。项目中已整理项目说明文档可帮助快速梳理模块结构、权限模型与下单流程适合具备一定Java基础、希望动手扩展功能的读者参考。目前已有97人学习使用作为参考资料具备较好的实践价值。1. 这份在线点餐小程序源码值不值得花一周跑通如果你下载过这类「在线点餐小程序源码项目说明」的压缩包大概率经历过同样的事解压后看到十多个文件夹数据库脚本、后端代码、小程序前端挤在一起双击 README 却只看到一段复制粘贴的环境要求。折腾两小时后端起不来小程序端白屏最后关掉窗口骂一句“又是一堆垃圾资源”。这份以 Java 为后端、微信小程序为前端、前后端分离的扫码点餐项目解决的是餐饮门店最常见的三件事顾客扫桌台码点餐、外卖与自取双渠道接单、多门店统一管理。它不是那种只有一个 hello world 的演示代码而是把门店、桌台、菜品、订单、支付回调串成一条完整链路的课设级项目。适合三类人拿它做 Java 课程设计或毕业设计的学生想给自家小餐饮店做私有化点餐系统的小老板以及想通过完整项目理解前后端分离架构的转行开发者。能不能跑起来、值不值得在上面花时间看完这篇再决定。2. 先看懂骨架多门店数据模型与前后端分离架构2.1 前后端分离怎么分Spring Boot 只出 JSON小程序端只做渲染这类项目最常见的后端框架是 Spring Boot MyBatis Plus MySQL前端小程序部分可能是原生微信小程序也可能是 uni-app 工程。所谓前后端分离在点餐系统里的边界非常清楚小程序端负责扫码、展示菜品、加购物车、下单支付后端负责校验登录态、管理菜品库存、生成订单、处理支付回调、推送订单给门店。后端不返回任何页面只返回 JSON。小程序端拿到 JSON 自己渲染界面。这个分离带来一个实际好处同一套后端接口可以同时服务小程序端、门店管理后台的 Web 端甚至以后接 POS 机。你在压缩包里看到后端工程里有controller、service、mapper三层结构就是标准的 Spring Boot 分层。提示判断这份源码是不是真前后端分离看后端接口返回。如果接口返回的是 HTML 片段或 ModelAndView那是服务端渲染不是分离架构如果全部是RestController返回 JSON才是真分离。2.2 多门店不是多张表门店、桌台、菜品、订单的核心表设计支持多门店模式最容易踩的坑是给每个门店复制一套菜品表这是灾难。正确做法是所有业务表都带store_id字段查询时统一按门店过滤。核心表通常有五张表名关键字段作用storeid, name, address, phone, status门店基础信息dining_tableid, store_id, table_no, qr_code桌台与桌台码dishid, store_id, category_id, name, price, stock, status菜品与库存ordersid, order_no, store_id, table_id, order_type, status, pay_status, total_amount订单主表order_itemid, order_id, dish_id, dish_name, price, quantity订单明细冗余菜品名防止菜品改价后历史订单错乱orders表里的order_type是关键设计用1表示堂食扫码点餐2表示外卖3表示自取。三个业务共用一个订单主表只是状态流转规则不同。order_item冗余dish_name和price是必须的否则菜品改价或者删除后历史订单的金额就对不上了。门店与商品的关系再加一张store_dish关联表会更好维护某个门店卖哪些菜品、门店自己的售价和库存都放在关联表里。很多课设项目省略了这张表直接用dish.store_id也能跑但门店之间想差异化定价就要改表结构。2.3 为什么扫码点餐要单独做「桌台绑定」而不是直接下单扫码点餐和普通外卖点餐最大的区别在桌台。顾客扫桌上二维码后端需要知道两件事这是哪个门店、哪张桌。然后整个订单生命周期里桌台信息要跟着订单走这样门店才能知道「3 号桌点了什么」。实现上桌台码的二维码内容通常是一串参数格式类似storeId1tableId12。小程序端扫码后解析参数带着storeId和tableId去请求菜品接口和创建订单。有些项目会把这串参数编码到二维码里有些项目用短码映射比如桌台码只存一个tableCode后端根据tableCode反查门店和桌台。后一种更安全因为顾客可以随便改 URL 参数但很难猜出一个有效的桌台短码。这里有一个容易忽略的点桌台状态。顾客扫码后发现桌台被占用或者已禁用要能正常提示而不是直接报错。所以dining_table表要有status字段点餐前先查一次桌台状态下单时再校验一次防止并发场景下同一桌台被重复下单。2.4 项目目录怎么读从压缩包结构反推功能模块拿到压缩包先别急着启动。花十分钟把目录结构过一遍判断这份源码的完整度。常见结构是这样在线点餐小程序源码/ ├── 项目说明.docx / README.md ├── sql/ │ └── ordering.sql -- 数据库初始化脚本 ├── server/ -- Java 后端工程 │ ├── pom.xml │ └── src/main/java/com/xxx/ │ ├── controller/ -- 接口层OrderController / DishController / AuthController │ ├── service/ -- 业务层订单状态流转、库存扣减 │ ├── mapper/ -- MyBatis Plus Mapper │ ├── entity/ -- 数据库实体 │ └── config/ -- 跨域、拦截器、WebSocket 配置 └── miniprogram/ -- 小程序前端工程 ├── app.js / app.json ├── pages/ │ ├── index/ -- 首页菜品列表 │ ├── cart/ -- 购物车 │ ├── order/ -- 订单确认与支付 │ └── user/ -- 我的订单列表 └── utils/request.js -- 封装 wx.request看目录时重点确认三件事第一sql目录里有没有完整的建表脚本缺了它后端根本起不来第二server工程是不是 Maven 或 Gradle 项目有没有pom.xml第三miniprogram里有没有app.json以及utils/request.js里的接口地址是不是写死的。这三件事直接决定你后面要花多少时间填坑。3. 本地跑通的最小路径从建库到小程序端出菜单3.1 数据库初始化执行 SQL 脚本前先改三处配置先创建数据库再导入 SQL 脚本。用 MySQL 命令行或者 Navicat 都可以。导入前打开 SQL 脚本检查三点数据库名、字符集、默认数据里的门店 ID 和桌台 ID。-- 创建数据库指定 utf8mb4 避免菜品名称 emoji 乱码 CREATE DATABASE IF NOT EXISTS ordering_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 导入后检查门店和桌台默认数据 SELECT id, name FROM store; SELECT id, store_id, table_no FROM dining_table;这里容易踩坑如果 SQL 脚本里写了CREATE DATABASE注意脚本里的库名和后面后端配置要一致如果脚本里没写只写了USE database_name那你要自己建好数据库再执行。字符集必须用utf8mb4而不是utf8否则餐品名称里如果带特殊字符或后续加 emoji 表情数据库会报错。导入完成后核对默认门店 ID、桌台记录是否存在。很多项目启动失败根源就是 SQL 导入不完整后端查询时拿到空数据。3.2 后端启动Spring Boot 的端口、数据源与文件上传路径数据库就绪后打开server/src/main/resources/application.yml改数据源配置。这是后端能否启动的第一道关卡。server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/ordering_system?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml configuration: map-underscore-to-camel-case: true # 菜品图片上传后的访问路径改成你本机的绝对路径 file: upload-dir: D:/upload/ access-path: /upload/**参数说明serverTimezoneAsia/Shanghai必加MySQL 8.0 以后不写时区会报错map-underscore-to-camel-case控制数据库store_id自动映射成 Java 的storeId设置成true可以省掉大量TableField注解。file.upload-dir是菜品图片的本地存储路径Windows 和 Linux 写法不一样后台上传图片后前端能不能访问到全看这个配置。启动项目本地用开发工具直接运行主类Application.java或者在服务器上用mvn spring-boot:run。看到Started Application in xx seconds才算成功。如果报端口占用把server.port改成 8081 或其他可用端口同时记得小程序端配置也要同步改。3.3 小程序端跑通把接口地址从 localhost 换成局域网 IP后端起来后小程序端还连不上因为代码里utils/request.js的接口地址写的多半是http://localhost:8080。在微信开发者工具里这个地址只能模拟器访问真机预览时手机访问不到电脑的 localhost。打开miniprogram/utils/request.js改成局域网 IP// 小程序端全局接口地址配置 const BASE_URL http://192.168.31.xxx:8080; // 改成你电脑的局域网 IP const request (url, method, data) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL url, method: method || GET, data: data || {}, header: { Content-Type: application/json, // 如果后端做了登录鉴权带上 token Authorization: wx.getStorageSync(token) || }, success: (res) { if (res.data.code 200) { resolve(res.data); } else { wx.showToast({ title: res.data.msg, icon: none }); reject(res.data); } }, fail: (err) reject(err) }); }); };这段代码的逻辑把所有请求统一封装自动带上前端缓存的 token后端返回code 200时视为成功否则弹出错误提示。你拿到源码后大概率只需要改BASE_URL一个常量。此外微信开发者工具里需要勾选「不校验合法域名」否则本地调试时http://192.168.x.x会被拦截。3.4 验证这一步三个接口确认链路通畅后端确认启动小程序端配置也改了先用 Postman 或浏览器打三个接口验证不要急着扫码点餐。打通这三步后面所有调试都有信心。# 1. 获取门店列表确认数据库连上了 curl http://localhost:8080/api/store/list # 2. 获取某门店的菜品列表确认菜品表有数据 curl http://localhost:8080/api/dish/list?storeId1 # 3. 模拟创建订单确认订单表可写 curl -X POST http://localhost:8080/api/order/create \ -H Content-Type: application/json \ -d {storeId:1,tableId:1,orderType:1,items:[{dishId:1,quantity:2}]}第三个接口是试金石。如果返回订单号说明数据源、MyBatis 映射、事务配置基本没毛病。如果报 500看后端控制台日志九成是 SQL 脚本缺字段或者order_item表没建全。这三个接口通了再回小程序端点「编译」首页菜品列表能出来这个项目就正式跑起来了。4. 把业务写完整外卖与自取双链路、状态机与扫码逻辑4.1 外卖和自取本质是同一个订单模型配送方式的三种分支很多初学者改这类源码时总想把外卖和自取拆成两套接口、两张表这是过度设计。项目里的做法通常是用orders.order_type区分下单接口不变只是根据类型补充额外信息外卖要填收货地址和联系方式自取要填预计取餐时间堂食绑定桌台。这三个分支在订单创建时走不同的校验逻辑但最终都落到同一张orders表。用代码表示就是public Order createOrder(CreateOrderDTO dto) { // 1. 统一校验门店状态和菜品库存 Store store storeService.getById(dto.getStoreId()); if (store null || store.getStatus() ! 1) { throw new BizException(门店不存在或已打烊); } // 2. 按订单类型补充校验 if (dto.getOrderType() 1) { // 堂食必须传桌台 ID且桌台状态正常 DiningTable table diningTableService.getById(dto.getTableId()); if (table null || table.getStatus() ! 1) { throw new BizException(桌台不可用); } } else if (dto.getOrderType() 2) { // 外卖必须传收货地址和联系电话 if (StringUtils.isBlank(dto.getAddress()) || StringUtils.isBlank(dto.getPhone())) { throw new BizException(外卖订单必须填写地址和电话); } } // 3. 计算总价、扣库存、生成订单号然后入库 // 订单号规则日期 门店ID 随机数保证全局唯一 String orderNo NO LocalDateTime.now().format(DateTimeFormatter.ofPattern(yyyyMMddHHmmss)) dto.getStoreId() RandomUtil.randomNumbers(4); // ... 省略明细写入和库存扣减 }这段代码说明了几件事统一的订单创建入口用orderType分支校验保证三个业务共用一套写库逻辑。订单号要全局唯一一个常见做法就是时间戳加门店 ID 加随机数。库存扣减要放在事务里点餐系统并发高下单时库存可能超卖。4.2 订单状态机从已支付到已完成谁在改状态点餐系统的订单状态是完整的状态机。自取和堂食是「待接单 → 制作中 → 待取餐 → 已完成」外卖多一步「配送中」。前端展示、门店接单、顾客申请退款全部依赖这个状态字段所以状态流转的代码要收敛在一个地方。public void updateOrderStatus(String orderNo, Integer targetStatus) { Order order orderMapper.selectOne( new LambdaQueryWrapperOrder().eq(Order::getOrderNo, orderNo)); if (order null) { throw new BizException(订单不存在); } // 校验当前状态是否允许流转到目标状态 boolean allowed StatusTransition.canTransit(order.getStatus(), targetStatus); if (!allowed) { throw new BizException(订单状态不允许从 order.getStatus() 变更为 targetStatus); } order.setStatus(targetStatus); if (targetStatus 4) { order.setFinishTime(LocalDateTime.now()); } orderMapper.updateById(order); }StatusTransition.canTransit是一个静态方法内部维护一张允许流转的映射表。比如0待支付只能到1已支付或5已取消1只能到2制作中或53待取餐只能到4已完成。把这个校验抽出来比在 Controller 里到处写 if 强得多后期加退款状态也不会改到散落各处的代码。实际改源码时注意订单支付回调里也要调状态流转方法不要直接在回调里order.setStatus(1)跳过校验。如果你的项目带自动接单功能接单动作是在支付回调后触发要保证这两步在同一个事务里或者用消息队列异步处理。4.3 扫码点餐的完整链路从扫码到桌台绑定再到后厨打印扫码点餐的链路比普通点餐多一个「场景值解析」。微信小程序的wx.scanCode或者「扫普通链接二维码」功能会把二维码里的内容传给小程序。拿到内容后要解析出门店 ID 和桌台 ID这一步很多项目处理得粗糙。// 小程序端解析扫码得到的桌台参数 onLoad(options) { // 通过扫码进入时options.scene 是 URL 编码后的参数 if (options.scene) { const scene decodeURIComponent(options.scene); // scene 格式约定为 storeId1tableId12 const params parseQueryString(scene); this.setData({ storeId: params.storeId, tableId: params.tableId }); // 把桌台信息写入全局下单时带上 getApp().globalData.currentTable { storeId: params.storeId, tableId: params.tableId }; } } function parseQueryString(str) { const result {}; str.split().forEach(item { const [key, value] item.split(); result[key] value; }); return result; }这段代码的坑在于扫码进入小程序时options.scene的值是 URL 编码的很多人忘了decodeURIComponent导致解析出来的storeId带一串%3D之类的乱码后端自然查不到门店。处理完参数后把桌台信息存到全局数据里后续所有下单请求都从全局拿。这样即使顾客中途退出再进来只要小程序没被杀掉桌台信息还在。下单后后厨看单有几种常见做法门店管理端轮询新订单接口WebSocket 实时推送或者简单点后厨页面定时刷新。课设项目多数用轮询或定时刷新因为实现简单。真要上线WebSocket 是更好的选择。4.4 多门店模式下的数据隔离门店 ID 贯穿所有查询多门店模式最容易翻车的地方不是下单接口而是管理端的数据越权。比如门店 A 的管理员登录后能查到门店 B 的订单。这个问题要从三个层面解决登录时绑定门店、查询时强制带门店 ID、操作时校验数据归属。// 门店管理员登录后把 storeId 放进 Token public LoginResponse login(String username, String password) { // ... 校验账号密码查出该管理员所属的 storeId String token JwtUtil.createToken(admin.getId(), admin.getStoreId()); return new LoginResponse(token, admin.getStoreId()); } // 拦截器里从 Token 取出 storeId放入 ThreadLocal public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token request.getHeader(Authorization); Claims claims JwtUtil.parseToken(token); StoreContext.setStoreId(claims.get(storeId, Integer.class)); return true; } // 查询订单时强制带上当前登录管理员的 storeId public PageOrder getOrderPage(PageQuery query) { Integer storeId StoreContext.getStoreId(); LambdaQueryWrapperOrder wrapper new LambdaQueryWrapper(); wrapper.eq(Order::getStoreId, storeId); // 追加其他查询条件 return orderMapper.selectPage(new Page(query.getPageNum(), query.getPageSize()), wrapper); }代码的核心是登录鉴权时就把门店 ID 编码进 Token每次请求通过拦截器解析 Token把门店 ID 放进ThreadLocal查询时从上下文取。这样即使多个门店共用一套接口数据也天然隔离。没有这套机制的多门店项目基本只能算「伪多门店」前后端都靠手动传参。5. 部署与调试避坑这份源码最容易翻车的 5 个地方5.1 真机预览连不上后端不是代码问题是域名和 IP现象微信开发者工具里模拟器一切正常菜品都能显示一切换到真机预览小程序直接白屏或请求超时。原因模拟器里 localhost 指向电脑本机真机上 localhost 指向手机自己当然连不上。另外微信公众平台对请求域名有校验开发阶段没配置合法域名真机上 wx.request 会被拦截。解决把request.js里的地址改成电脑的局域网 IP在微信开发者工具里勾选「不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书」后重新编译。手机和电脑连同一个 Wi-Fi 才能访问局域网 IP。如果公司网络禁了局域网互通开手机热点给电脑实测能通。5.2 菜品图片全部裂开本地文件存储路径的坑现象管理后台上传菜品图片成功小程序端菜品图片加载不出来控制台报 404但图片文件确实存在。原因项目把图片存到了本地磁盘绝对路径比如D:/upload/xxx.jpg但访问 URL 是http://localhost:8080/upload/xxx.jpg。Spring Boot 默认不暴露磁盘路径后端没做静态资源映射时浏览器访问这个 URL 肯定 404。解决加一个静态资源映射配置把本地上传目录映射为外部访问路径。Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 把 /upload/** 映射到本地上传目录 registry.addResourceHandler(/upload/**) .addResourceLocations(file:D:/upload/); } }参数说明addResourceHandler是外部访问路径addResourceLocations是本地磁盘路径末尾的斜杠不能少。改成这个配置后访问http://localhost:8080/upload/dish1.jpg就能映射到D:/upload/dish1.jpg。上线部署时同理把D:/upload/换成服务器上的/var/www/upload/。5.3 多门店数据串了ThreadLocal 里的门店 ID 丢了现象管理员 A 登录门店 1 后查询订单把门店 2 的订单也查出来了过一会儿又发现数据正常。时好时坏非常玄学。原因拦截器从 Token 取出门店 ID 放进 ThreadLocal但请求结束后没有清理 ThreadLocal。Tomcat 的工作线程是复用的上一次请求的数据残留在线程里下一次请求如果没有被拦截器覆盖就会串到别的门店。解决在拦截器的afterCompletion方法里清理上下文这是必须做的。public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { // 清掉 ThreadLocal防止线程复用导致门店数据串 StoreContext.remove(); }另外要检查拦截器是否覆盖了所有需要鉴权的接口。如果管理端部分接口没走拦截器比如菜品列表接口那招标式查询就不会带门店 ID同样导致数据混乱。建议所有接口统一走拦截器注册路径宁可多拦截不可少拦截。5.4 支付回调收不到回调地址不是你想填就能填现象在小程序端点支付微信支付弹出后支付成功但订单状态一直停留在待支付没有变成已支付。原因支付回调地址填的是http://localhost:8080/api/pay/callback。微信支付服务器无法访问你电脑的 localhost。真机调试时微信支付回调走公网必须有一个公网可访问的地址。解决本地开发阶段可以用局域网 IP 配合路由端口映射度过更省事的做法是把回调处理逻辑改为前端轮询——小程序端支付成功后前端每隔 2 秒调一次「查询订单支付状态」接口直到后端确认支付成功。虽然不如回调实时但开发和调试成本最低。上线时再换成真正的公网回调地址。提示用轮询替代回调属于开发阶段的临时手段。正式部署到服务器后应该把回调地址改成https://你的域名/api/pay/callback并配置好微信支付密钥否则高并发下轮询会造成不小压力。5.5 桌台码一直失效扫码参数被转码现象扫描桌台二维码进入小程序提示门店不存在或桌台不存在同一个码扫第二次又好了时好时坏。原因二维码里的参数storeId1tableId12在扫码后进入小程序时微信会对参数做 URL 编码变成%26等于号变成%3D。如果小程序端解析时没做decodeURIComponent后端收到的就是storeId%3D1%26tableId%3D12整个解析结果变成单个 key当然查不到数据。解决按 4.3 节里的方式先decodeURIComponent(options.scene)再解析。另外二维码生成工具的选择也影响结果直接用后端zxing库生成二维码最稳前端插件生成的二维码有时会带隐藏字符。定位这类问题最快的方式是小程序端console.log打印出扫码后拿到的原始options.scene看一眼就知道是不是被转码了。6. 从课设到可交付多门店运营的权限、打印与统计进阶跑通和改完坑之后这个项目能交付给真实门店使用吗还差三块拼图权限隔离、后厨联动、数据统计。权限隔离不只是门店数据隔离还要做角色区分。店长能改菜品、看营业额报表收银员只能操作订单。简单做法是给管理员表加role字段用一个RequiresRole(SHOP_OWNER)注解加拦截器实现不改动现有接口逻辑就能逐步加上。后厨打印联动用 WebSocket 比轮询体验好得多。订单创建成功后调用WebSocketServer.sendMessage(storeId, orderNo)把新订单消息推送到对应门店的后厨大屏页面。核心代码是维护一个ConcurrentHashMapString, Sessionkey 是门店 IDvalue 是后厨页面的 WebSocket 连接。门店后厨页面打开时注册连接收到新订单消息后自动打印小票或弹窗提醒。数据统计方面按门店、按日、按时段的订单聚合查询是店长最关心的。SQL 里用GROUP BY DATE(create_time), store_id就能输出每日营业额报表。更进一步把菜品销量 Top10 单独查出来能直接指导后厨备菜。这份源码到我手里我会先清理掉所有测试数据和写死的门店配置再跑通全流程然后才考虑上线。做这类项目别指望解压即用把它当成一份需要二次开发的半成品心里预期就稳了。希望这篇能帮你少踩几个坑早点跑出自己的点餐系统。本文还有配套的精品资源点击获取
返回列表