尧图精选

扫码点餐系统全栈拆解:SpringBoot+OAuth2+uniapp前后端分离实战

🕒 发布时间:2026/10/1 10:38:50 📁 来源:尧图网络
简介这是一套面向高校毕业设计或课程大作业的「意向点餐扫码点餐系统」前后端分离项目。项目采用 SpringBoot 与 Spring Security OAuth2 作为后端认证及接口框架前端使用 uniappVue3开发可编译到 H5 与微信小程序并覆盖外卖与自取点餐、多门店管理等常见业务模块能直接作为 Java 小程序方向的设计参考或二次开发底稿。压缩包内共 2000 个文件以 Java 源码1323 个和 Vue 前端文件257 个为主体辅以 JS、XML、HTML、CSS、SQL 等配置文件与脚本整体体积 15.09MB目录划分清晰便于按模块定位代码与配置。目前已有 403 人学习或下载适合需要从零搭建点餐系统、梳理 OAuth2 认证流程或完成小程序端与后台联调的开发者。通过阅读源码与 SQL 初始化脚本可快速复现扫码点餐、订单流转、门店切换等核心链路再结合前端页面能直观理解 uniapp 多端打包与 SpringBoot 接口对接的完整过程。1. 扫码点餐真的不只是“点个菜”这套前后端分离系统到底装了什么一个“扫码点餐系统.zip”解压之后往往不是点开就能跑的前台 demo而是一整套前后端分离工程后端是 SpringBoot Spring Security OAuth2前端是 uniapp(vue3)同一套代码能编译成微信小程序也能跑 H5。对要做毕业设计或者想完整走一遍全栈流程的人来说它的价值不在于“点餐”这个业务有多新奇而在于它把微信登录、多门店隔离、外卖/自取订单流转这几个高频模块串在了一起每一个都是简历上能写、面试时能聊的实打实的技术点。拿到这种压缩包第一件事不是急着配数据库而是先把工程结构看清哪个是后端、哪个是前端、配置文件里藏着哪些环境依赖。这个系统里的核心链路是用户扫码进店 → 微信一键登录 → 浏览门店菜品 → 加购物车 → 提交外卖或自取订单 → 商家接单出餐。下面我按后端、小程序端、订单流程、避坑、联调验证的顺序把整套资源拆开讲。2. SpringBoot OAuth2 后端底座令牌怎么发、门店数据怎么写2.1 为什么是这个组合无状态令牌与微信生态的配合扫码点餐这类业务前端是微信小程序/H5后端是独立 API 服务天然适合前后端分离。小程序端每次请求 API 时不可能像传统 Web 应用那样靠 Session 维持登录态——小程序没有 Cookie 概念H5 跨域场景下 Cookie 也经常被浏览器拦掉。Spring Security OAuth2 在这里解决的就是“无状态登录”用户登录成功后后端签发一个 access_token前端后续请求只要在 Header 里带上Authorization: Bearer token后端就能通过令牌识别身份。这套模式和小程序的微信登录能顺畅衔接小程序端拿uni.login()得到的 code向后端换 openid后端用 openid 创建或绑定本地用户再走自定义的 token 颁发流程。access_token 短期有效比如 2 小时refresh_token 长期有效比如 7 天小程序端发现 401 后主动用 refresh_token 换新用户就无感知续期。2.2 把认证服务搭起来关键配置与代码这个系统的后端认证核心是 Spring Security OAuth2。老版本项目常用EnableAuthorizationServer直接声明授权服务器新版则拆成了 Authorization Server 和 Resource Server 两套独立配置。无论哪种写法核心逻辑都是三件事定义 client 客户端、定义用户来源、定义令牌存储方式。Configuration EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient(app-client) .secret({noop}app-secret) .authorizedGrantTypes(password, refresh_token) .scopes(all) .accessTokenValiditySeconds(7200) .refreshTokenValiditySeconds(604800); } Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) { endpoints .authenticationManager(authenticationManager) .userDetailsService(userDetailsService) .tokenStore(new InMemoryTokenStore()); } }这段配置里有几个点要说明password模式适合小程序端拿用户名密码换令牌但扫码点餐场景更多是“手机号 验证码”或者“微信登录后换 token”所以这里更常见的做法是自定义一个/oauth/token的扩展端点在内部用 openid 直接生成令牌。refresh_token授权类型一定要保留否则小程序端 token 过期后只能让用户重新登录。InMemoryTokenStore只适合开发和单机部署生产环境建议换成 Redis 存储否则重启后端所有在线用户都要重新登录。用户来源这块后端要提供一个UserDetailsService的实现从数据库里查用户Service public class UserService implements UserDetailsService { Autowired private UserMapper userMapper; Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user userMapper.selectByPhoneOrOpenid(username); if (user null) { throw new UsernameNotFoundException(用户不存在); } return org.springframework.security.core.userdetails.User .withUsername(user.getPhone()) .password(user.getPassword()) .authorities(ROLE_USER) .build(); } }这里有个实际坑微信登录用户的密码字段可能是空的password授权模式下UserDetailsService必然校验密码。所以这个系统里微信登录通常不走 password 模式而是后端直接拿 openid 查用户查到就发 token查不到就先自动注册再发 token绕开密码校验这一步。2.3 多门店数据模型门店 ID 要进每一张业务表后端除了认证模块最核心的就是多门店模型。这个系统支持多门店意味着菜品、订单、购物车都不能做成全局一份。我见过不少人在这个点上翻车订单表里没存门店 ID导致 A 门店的订单流到了 B 门店的待处理列表里。数据模型上门店表先独立存在菜品表和门店是多对多或直接挂store_id订单表必须冗余store_id字段——下单时从门店维度写入查询时用store_id 订单状态联合过滤索引也要按这个组合建。CREATE TABLE store ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL, address VARCHAR(255), status TINYINT DEFAULT 1, business_hours VARCHAR(64), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE product ( id BIGINT PRIMARY KEY AUTO_INCREMENT, store_id BIGINT NOT NULL, name VARCHAR(128) NOT NULL, price DECIMAL(10,2) NOT NULL, category_id BIGINT, image_url VARCHAR(255), stock INT DEFAULT 0, status TINYINT DEFAULT 1, KEY idx_store_category (store_id, category_id) ); CREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, store_id BIGINT NOT NULL, user_id BIGINT NOT NULL, order_type TINYINT COMMENT 1-自取 2-外卖, status TINYINT NOT NULL, total_amount DECIMAL(10,2), address VARCHAR(255), contact_phone VARCHAR(20), remark VARCHAR(255), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_store_status (store_id, status), KEY idx_user (user_id) );价格字段一定要用DECIMAL(10,2)不要用double。金额计算涉及精度用浮点数会出现 0.1 0.2 0.30000000000000004 这种结果订单金额对不上账就是从这里开始的。库存字段这里写的是stock INT但实际扣库存必须配合乐观锁下面讲订单流转时会再展开。3. uniapp(vue3) 小程序端从微信登录到购物车结算的一条链路3.1 微信一键登录从 uni.login 到 openid 的换取小程序端不存密码登录靠的是微信的 code 换 openid。前端第一步调用uni.login()拿到临时 code然后把这个 code 发给后端后端拿着 code 去微信接口换 openid查库后签发自己的 token。这个链路里后端要自己维护一个/api/auth/wx-login接口前端只需要关注 code 的获取和 token 的保存。async function wxLogin() { const { code } await uni.login({ provider: weixin }); const res await request({ url: /api/auth/wx-login, method: POST, data: { code } }); if (res.code 0) { uni.setStorageSync(access_token, res.data.access_token); uni.setStorageSync(refresh_token, res.data.refresh_token); uni.setStorageSync(user_info, res.data.userInfo); } }代码里的request是封装过的请求函数不是uni.request裸调用。为什么要包一层因为业务里除了登录接口其他所有请求都要带 token、都要统一处理 401 和网络错误。如果每个页面都写一遍uni.request光 Header 的拼接就能把人逼疯。这里要提醒一个容易忽略的细节uni.login拿到的 code 一次性有效而且有效期只有几分钟。如果用户手机时间不准、或者后端拿 code 换 openid 时网络抖动会报invalid code。常见做法是前端拿到 code 后立即请求后端不要在中间夹其他异步操作。3.2 请求封装与 token 静默续期避免 401 死循环封装请求层的核心目的有三个统一带 token、统一错误提示、统一处理过期续期。小程序端的 token 续期逻辑不能写成“遇到 401 就跳登录页”那样用户正点着菜突然被踢出去体验极差。正确姿势是请求返回 401 时先检查本地有没有 refresh_token有就静默调一次刷新接口用新 token 重放刚才失败的请求。function request(options) { return new Promise((resolve, reject) { uni.request({ url: BASE_URL options.url, method: options.method || GET, data: options.data || {}, header: { Authorization: Bearer uni.getStorageSync(access_token), Content-Type: application/json }, success: async (res) { if (res.data.code 40101) { const refreshed await refreshToken(); if (refreshed) { retryRequest(options).then(resolve).catch(reject); } else { uni.navigateTo({ url: /pages/login/login }); reject(res.data); } } else { resolve(res.data); } }, fail: (err) { uni.showToast({ title: 网络异常, icon: none }); reject(err); } }); }); }这个封装里有两个关键参数40101是后端约定的“token 过期或无效”的业务码不能拿 HTTP 状态码 401 直接判断——因为如果网关或代理层先拦截了 401前端根本拿不到后端业务响应。refreshToken()内部要防止并发如果购物车有三个请求同时 401三个请求都去刷新 token会造成多次刷新、旧 token 还没失效就被顶掉。常见做法是加一个isRefreshing标志位第一个请求刷新时把后面的请求先挂起等新 token 回来后统一重放。3.3 扫码进店与点餐页的数据绑定扫码点餐的“码”本质上是一个带参数的 URL 或小程序码里面最关键的就是storeId。用户扫码进入小程序后前端从启动参数里解析出门店 ID然后调门店详情接口拿到门店名、营业时间、公告再调菜品列表接口。这个顺序不能反过来没拿到门店 ID 之前就去拉菜品列表后端会直接报“门店不能为空”。购物车在小程序端适合放本地缓存键名可以按门店维度隔离const CART_KEY cart_ storeId; function addToCart(product, quantity) { const cart uni.getStorageSync(CART_KEY) || []; const idx cart.findIndex(item item.productId product.id); if (idx -1) { cart[idx].quantity quantity; } else { cart.push({ productId: product.id, name: product.name, price: product.price, quantity, storeId }); } uni.setStorageSync(CART_KEY, cart); }按门店维度存购物车是为了多门店场景用户在 A 门店加了几道菜又扫了 B 门店的码两个购物车不能串。下单时再按当前进入的门店 ID 去取对应缓存。这里有个体验细节结算页要同步展示“购物车清空时机”——下单成功即清空本地缓存否则会出现“订单已提交但购物车还显示着”的诡异状态。4. 多门店与订单流转外卖/自取的差异不是换个字段那么简单4.1 外卖与自取的业务差异清单外卖和自取在数据库里可能只是一个order_type字段的差别但业务流程上是两套逻辑。自取的履约路径是“用户到店 → 凭取餐码取餐”外卖的履约路径是“骑手接单 → 送达到地址”。两者在订单字段、接单流程、配送费计算上都有明显差异业务维度自取外卖用户地址不需要配送地址必须填收货地址配送费0按距离或门店规则计算预计时间用户自选到店时间系统估算配送时间接单环节门店确认后出餐可能涉及骑手分配订单取消出餐前可退骑手接单后限制取消在实现上自取订单的下单接口可以不接收地址字段但外卖订单必须有完整的地址、联系人、电话校验。后端在做参数校验时就要按类型区分不能一套校验走到底。很多半成品系统翻车就翻在这里订单表给出了address字段但外卖单没做非空校验用户不填地址也能提交成功门店端收到一个没法配送的单子。4.2 订单状态机设计与状态流转订单状态是这个系统的核心业务逻辑设计上要覆盖从下单到完成/取消的全生命周期。这个系统里的状态流转大致是待支付 → 待接单 → 制作中 → 待取餐自取/ 配送中外卖→ 已完成另外还有已取消和退款中两个负向状态。public enum OrderStatus { WAIT_PAY(0, 待支付), WAIT_CONFIRM(1, 待接单), MAKING(2, 制作中), WAIT_PICKUP(3, 待取餐), DELIVERING(4, 配送中), COMPLETED(5, 已完成), CANCELED(6, 已取消), REFUNDING(7, 退款中); private final int code; private final String desc; }状态流转要放到后端统一封装不能由前端随意跳转。前端只能按下单、取消、确认收货这类操作按钮后端根据当前状态判断能不能执行。比如“已完成的订单不能取消”“待支付订单不能直接跳到配送中”。一个容易踩的坑是订单支付成功后通知门店的逻辑要幂等——用户支付成功的回调可能因为网络超时被微信重试多次如果每次回调都执行一遍“改状态 通知门店”门店会收到好几个重复的新订单提醒。常见做法是在回调逻辑入口处先查一次订单状态只有待支付状态的订单才执行支付成功后的流程。4.3 并发下单与库存防超卖不只是在代码里减 1点餐系统的库存和电商不完全一样菜品取消率更高但“限量菜品卖超了”这件事一样会发生。扣库存的代码如果写成先查库存、再判断、再更新两个请求同时进来就可能都通过检查// 错误示范查-判-改分三步中间有间隙 Integer stock productMapper.selectStock(productId); if (stock 0) { productMapper.decreaseStock(productId); }并发场景下这个写法必出超卖。正确做法是用带条件的更新语句把“判断”和“扣减”合成一步UPDATE product SET stock stock - 1 WHERE id #{productId} AND stock 0;这条 SQL 的影响行数如果为 1说明扣减成功为 0 说明库存不足。下单接口里拿到影响行数后再去插入订单记录库存不足直接返回“菜品已售罄”。另外如果菜品允许退款、取消扣掉的库存还要在订单取消时回补回补操作同样要用条件更新避免“取消订单”和“再次下单”同时执行导致库存变成负数。多门店场景下所有菜品操作都必须带上store_id作为查询条件。库存更新语句里如果不带门店条件一个门店的菜品更新可能影响同 ID 的其他门店菜品这就属于数据隔离没做干净。5. 避坑与常见问题部署跑不起来时先查这五个地方5.1 解压后文件不完整项目无法启动现象压缩包解压到一半报错或者解压成功了但目录里找不到 pom.xml、manifest.json 等关键文件IDE 打开后直接不识别。原因网上流传的资源包经常带伪加密标志或者上传者用压缩软件分卷后没有合并完整。winrar、360 压缩、mac 自带归档工具对伪加密的处理逻辑不一样有的解压工具能跳过伪加密直接读有的则拒绝解压。解决压缩包先看文件大小和正常工程比明显小很多就要警惕。用 7-Zip 打开后先点“测试”按钮测试通过再解压。解压路径不要带中文和空格D:\project\meal-system这种路径最稳。如果提示损坏但测试通过先把文件复制到本地再解压不要直接在 U 盘或共享目录里解压。5.2 token 过期后刷新死循环小程序白屏现象小程序运行一段时间后所有请求都失败页面空白控制台一大堆 401。刷新页面后短暂正常过一会儿又不行。原因access_token 过期后前端请求里带的是旧 token后端返回 401。如果 refresh_token 接口本身也要求带 access_token或者刷新接口返回的也是 401整个链路就卡死了。另外一个常见原因是后端是集群部署但 token 存在内存里前一次请求打到 A 实例刷新请求打到 B 实例B 实例不认识这个 refresh_token。解决刷新接口自身必须在 Security 配置里放行不能纳入 token 校验范围。多实例部署时令牌存储切换为 Redis。前端做好并发刷新控制避免多个请求同时触发 refresh。用isRefreshing标志位刷新过程中新进来的请求先排队拿到新 token 后统一重放。5.3 微信支付回调验签失败订单一直待支付现象小程序端调起支付后钱扣了但订单状态一直是待支付门店端看不到新订单。原因最常见的是回调接口接收参数时用了框架的 JSON 自动解析导致验签用的原文和微信服务器发出来的原文不一致。微信支付回调签名是基于原始 body 字符串计算的框架把 body 转成对象再转回字符串后字段顺序或格式变了验签必然失败。解决回调接口用HttpServletRequest直接读原始 body 字符串完成验签后再手动解析业务内容。同一个订单号要按幂等处理避免重复回调导致状态错乱。沙箱环境中回调地址必须是公网可访问的 HTTPS 地址本地联调时可以用内网穿透工具把回调转发到本地调试。5.4 金额计算用了浮点数账单对不上现象用户下单时菜品 9.9 元两份结算显示 19.8 元但数据库里订单金额变成 19.799999 之类的小数。原因后端或者前端数据库字段用了float/double金额经过多次加减乘除后浮点误差累积。点餐系统里还有满减、配送费、打包费叠加误差会越来越明显。解决数据库金额字段统一用DECIMAL(10,2)Java 后端用BigDecimal计算前端展示时保留两位小数。涉及到折扣计算时每一步都调用setScale(2, RoundingMode.HALF_UP)不要等最后一步再统一四舍五入。5.5 H5 模式联调跨域接口全部 404现象在微信开发者工具里跑小程序正常但切到 H5 模式后所有接口都请求失败浏览器控制台报 CORS 错误或请求 URL 变成相对路径打不开。原因H5 页面跑在http://localhost:8081后端接口跑在http://localhost:8080跨域了。微信小程序没有跨域限制所以小程序端一切正常容易被忽略。解决开发环境下用 vite 的 proxy 代理把/api代理到后端地址前端请求统一走相对路径/api/xxx。生产环境把前端构建产物放到 Nginx由 Nginx 统一转发/api到后端服务这样浏览器端就没有跨域问题。6. 本地联调与交付验证一套能复现的“开门三板斧”拿到这套系统后别急着改业务代码先按固定流程验证整套环境能不能跑通。我一般分三步走每步都能独立验证出问题时能快速定位是前端还是后端的问题。第一步启动后端看认证接口是否通。后端启动后先不走数据库直接请求一下/oauth/token或登录接口确认 Spring Security 配置没写错。用 curl 发一次带 client 信息的请求最直接curl -X POST http://localhost:8080/oauth/token \ -H Content-Type: application/x-www-form-urlencoded \ -d grant_typepasswordusernametestpassword123456client_idapp-clientclient_secretapp-secret返回的 JSON 里有access_token和refresh_token字段说明认证链路已经通了。这个步骤能过滤掉一半以上的“为什么小程序起不来”的问题——很多情况下后端根本没启动成功前端白屏只是表象。第二步起前端 H5验证代理和登录跳转。在 uniapp 项目根目录执行npm install后再npm run dev:h5。浏览器打开页面先看网络面板里请求是否走代理、是否带上了 token。如果登录接口通了再扫码或者手动带storeId参数进门店页拉一次菜品列表。这一步能验证前端到后端的数据链路是完整的。第三步模拟一单完整的“下单”流程。不进真实支付本地没有商户号直接调后端的模拟支付接口或者手动把订单状态改成待接单然后确认门店端的订单列表能刷出来。这一步可以先直接把店里的菜品库存改成 1然后用两个并发请求去抢购验证防超卖逻辑是否生效。curl -X POST http://localhost:8080/api/order/create \ -H Authorization: Bearer access_token \ -H Content-Type: application/json \ -d {storeId:1,orderType:2,items:[{productId:1,quantity:1}],address:某市某区某路 1 号,contact:张先生,phone:13800000000}响应返回orderNo和totalAmount去数据库里查对应的记录核对金额、门店 ID、状态是否正确。我带学生跑这个项目时最怕的是他们拿到手就改配置、改代码改到后面连哪步出的问题都说不清。从那以后我每次拿到一个陌生系统都强制先走一遍“后端认证 → H5 联调 → 模拟下单”这个固定流程确认原始状态能跑通再动手改业务。这套流程看着笨但能省掉大量“我昨天还能跑今天就不行”的玄学排查时间。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →