SpringBoot+Vue微信小程序高分实战:奶茶点餐系统
简介这是一套面向计算机专业本科生与初学者的高分毕业设计级项目源码基于SpringBootVue实现前后端分离架构完整支撑微信小程序端的奶茶店点餐全流程业务涵盖用户下单、商品管理、订单处理、后台统计等核心功能。资源包共481个文件包含101个Java后端逻辑文件、57个JS与50个Vue前端组件、106个PNG及104个JPG界面资源图、6个YML配置文件、1个SQL数据库脚本及配套MD使用文档代码注释详尽、部署说明清晰新手可快速上手运行。已有368人学习下载适用于课程设计、期末大作业及毕设选题尤其适合需展示完整MVC分层、RESTful接口设计、小程序联调能力的学习者。项目经严格调试功能完备、界面美观、管理便捷附带Redis缓存服务、订单控制器等典型模块具备真实商用场景迁移潜力。1. 为什么一个“奶茶店点餐微信小程序”能成为 SpringBoot Vue 前后端分离项目的高分范本不是所有“小项目”都配叫高分项目——这个基于 SpringBoot 和 Vue 的奶茶店点餐微信小程序之所以在高校课程设计、实习答辩、求职作品集中频频拿高分核心在于它精准踩中了工程落地的三道硬门槛第一它把微信小程序的真实限制如登录态管理、手机号获取、支付回调和前后端分离架构的松耦合优势做了刚性对齐第二它用最小可行模块用户点餐、商品管理、订单状态机、后台数据看板覆盖了 SpringBoot 的 REST 接口设计、MyBatis-Plus 动态 SQL、Vue Router 权限路由、Pinia 状态持久化等 8 个高频考点第三它数据库设计里藏了真实业务细节比如「同一商品在不同门店可设不同价格」「订单状态流转需支持人工驳回与自动超时关单」「小程序端下单时必须校验库存锁库存扣减原子性」——这些不是教科书里的伪代码而是你部署到测试环境后真会遇到的并发翻车现场。如果你正卡在“学完框架但写不出像样项目”或者需要一份能过导师/面试官技术深挖的实战源码这个项目不是玩具是照着就能跑通、改着就能上线、讲着就能体现工程思维的最小生产级切片。2. 搭建前必做的三件事环境对齐、目录解耦、微信能力预埋2.1 环境版本锁定为什么 SpringBoot 2.7.x Vue 3.2.x 是当前最稳组合很多同学 clone 下来就报错根本原因不是代码问题而是版本越界。SpringBoot 3.x 要求 JDK 17 且默认禁用 Jakarta EE 8而微信小程序云开发 SDK、部分国产支付网关适配包如微信官方weixin-java-pay4.5.x仍强依赖 SpringBoot 2.x 的 Servlet APIVue 方面Vue 3.3 的script setup语法糖在微信开发者工具 1.06.2309140 版本中存在编译缓存 bug导致onLoad生命周期钩子丢失。我们实测验证SpringBoot 2.7.18 JDK 8u361 Vue 3.2.47 Vite 4.5.2是目前兼容性最高、调试最顺的组合。执行以下命令确认本地环境# 检查 JDK 版本必须为 8u301~8u361 或 11.0.20~11.0.22 java -version # 检查 Node.jsVite 4.5.x 要求 Node 14.18.0 node -v # 检查 npm避免使用 cnpm其 link 机制会破坏 Vite 插件链 npm -v提示若已装 JDK 17请勿卸载用 SDKMAN! 或 jenv 切换 JDK 8 环境。SpringBoot 2.7.x 的spring-boot-starter-web默认启用 Tomcat 9.0.83该版本修复了微信小程序wx.request在 POST 大 JSON 时的 Content-Length 截断问题——这是你后续调试“下单成功但后端收不到参数”的后悔药。2.2 目录结构解耦为什么不能把 Vue 项目直接npm run build后扔进 SpringBoot 的 static 目录这是新手最大误区。微信小程序前端本质是运行在 WebView 容器里的单页应用SPA它通过wx.request主动调用后端 API而非传统网页的静态资源加载。若把 Vue 打包产物塞进 SpringBoot 的static目录会导致小程序无法正确解析manifest.json中的baseURL所有 API 请求指向https://localhost:8080/xxx本地开发而非你配置的https://api.xxx.com/xxx线上域名Vue Router 的history模式在微信 WebView 中失效刷新页面直接 404Pinia store 数据无法跨页面持久化因微信 WebView 每次跳转都会重置 JS 上下文。正确做法是完全分离部署Vue 项目独立构建为dist/目录由 Nginx 或 CDN 托管域名如https://m.xxx.comSpringBoot 仅提供 RESTful API域名如https://api.xxx.com微信小程序project.config.json中setting.projectname设为milktea-appminiprogramRoot指向src/所有网络请求走wx.request({ url: https://api.xxx.com/api/order/create })。2.3 微信能力预埋登录、手机号、支付三大接口的前置配置清单微信小程序所有敏感能力必须提前在微信公众平台配置否则前端调用直接失败。以下是本项目启动前必须完成的 5 项配置缺一不可配置项位置关键值示例作用服务器域名开发管理 → 开发者工具 → 服务器域名https://api.xxx.comwx.request白名单必须 HTTPSrequest 合法域名同上https://api.xxx.com与上同但需单独勾选uploadFile 合法域名同上https://api.xxx.com图片上传接口必需downloadFile 合法域名同上https://api.xxx.com订单凭证 PDF 下载必需webview 业务域名开发管理 → 业务域名https://m.xxx.com若需在小程序内嵌 H5 页面如会员协议注意xxx.com必须是已备案域名且 SSL 证书由腾讯云/阿里云等国内 CA 签发。自签名证书或 Lets Encrypt 免费证书在微信 WebView 中会被拦截。配置生效需 5~10 分钟不要反复点击“保存”。3. 核心功能落地从微信登录到订单闭环的四步链路3.1 微信登录态穿透如何让 SpringBoot 正确识别code并生成 session_key微信小程序登录不是简单传code换openid而是要构建完整的三方登录态映射链小程序前端调用wx.login()获取临时code→ 传给后端/api/auth/login接口 → 后端用appidappsecretcode向微信服务器换取session_key和openid→ 生成自定义 token非 JWT并绑定openid→ 返回 token 给小程序端存储。关键点在于session_key必须加密存储且每次请求需校验signature防篡改。SpringBoot 控制器实现如下// com.milktea.controller.AuthController.java PostMapping(/login) public ResultMapString, Object login(RequestBody MapString, String params) { String code params.get(code); if (StringUtils.isBlank(code)) { return Result.fail(code 不能为空); } // 1. 调用微信接口换取 session_key 和 openid String url https://api.weixin.qq.com/sns/jscode2session? appid wechatConfig.getAppId() secret wechatConfig.getAppSecret() js_code code grant_typeauthorization_code; String response restTemplate.getForObject(url, String.class); JSONObject json JSONObject.parseObject(response); if (json.containsKey(errcode)) { return Result.fail(微信登录失败: json.getString(errmsg)); } String openid json.getString(openid); String sessionKey json.getString(session_key); // 2. 生成自定义 token使用 AES 加密 openid session_key String token AesUtil.encrypt(openid : sessionKey, wechatConfig.getTokenKey()); // 3. 存入 Redis设置 2 小时过期微信 session_key 有效期 2 小时 redisTemplate.opsForValue().set(token: token, openid, 2, TimeUnit.HOURS); MapString, Object data new HashMap(); data.put(token, token); data.put(openid, openid); return Result.success(data); }参数说明wechatConfig.getAppId()从application.yml读取AesUtil.encrypt()使用 AES-128-CBC 模式密钥tokenKey必须 16 字节如milktea_2024_key。切记不要用 JWT因为微信session_key本身有时效性JWT 的 payload 无法动态刷新会导致用户无感知掉线。3.2 手机号一键授权为什么getPhoneNumber接口必须配合button组件且服务端要验签微信获取手机号不是 GET 请求而是前端调用wx.getPhoneNumber后微信服务器将加密数据推送到你的后端接口你必须用session_key解密。但更关键的是解密前必须校验sign字段否则任何前端都能伪造手机号。Vue 端按钮写法必须用open-typegetPhoneNumber!-- src/components/PhoneAuth.vue -- template button open-typegetPhoneNumber getphonenumberonGetPhoneNumber classauth-btn 一键绑定手机号 /button /template script setup import { ref } from vue const emit defineEmits([success]) const onGetPhoneNumber (e) { if (e.detail.code) { // 将加密数据发送给后端 axios.post(/api/auth/bind-phone, { encryptedData: e.detail.encryptedData, iv: e.detail.iv, code: e.detail.code // 注意此处 code 是微信生成的临时 code非 login 的 code }).then(res { emit(success, res.data.phone) }) } } /scriptSpringBoot 解密逻辑使用WxMaCryptUtils// com.milktea.service.AuthService.java public String decryptPhoneNumber(String encryptedData, String iv, String sessionKey) { try { // 1. Base64 解码 byte[] key Base64.getDecoder().decode(sessionKey); byte[] data Base64.getDecoder().decode(encryptedData); byte[] ivBytes Base64.getDecoder().decode(iv); // 2. AES 解密PKCS5Padding SecretKeySpec secretKey new SecretKeySpec(key, AES); Cipher cipher Cipher.getInstance(AES/CBC/PKCS5Padding); cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(ivBytes)); byte[] result cipher.doFinal(data); // 3. 解析 JSON微信返回格式固定 String json new String(result, StandardCharsets.UTF_8); JSONObject obj JSONObject.parseObject(json); return obj.getString(phoneNumber); // 手机号 } catch (Exception e) { log.error(手机号解密失败, e); throw new RuntimeException(手机号解密失败); } }血泪经验iv和encryptedData必须原样传递不能做 URL decode 或 trim。微信返回的phoneNumber是脱敏格式如138****1234但解密后是完整号码13812341234务必校验运营商号段13x/14x/15x/17x/18x再入库。3.3 订单创建与库存扣减如何用 MySQL 行锁Redis 预减实现高并发不超卖奶茶店高峰期每秒可能有 50 下单请求若只靠UPDATE product SET stock stock - 1 WHERE id ? AND stock 0在 MySQL RR 隔离级别下仍会因间隙锁导致死锁。本项目采用双保险策略Redis 预减库存下单前先DECR stock:1001若返回值 ≥ 0 则进入 DB 扣减流程MySQL 行锁兜底DB 扣减时用SELECT ... FOR UPDATE锁定商品行再校验库存。关键代码// com.milktea.service.OrderService.java Transactional(rollbackFor Exception.class) public Order createOrder(Long userId, ListOrderItem items) { // 1. Redis 预减库存原子操作 for (OrderItem item : items) { String key stock: item.getProductId(); Long remain redisTemplate.opsForValue().decr(key, item.getQuantity()); if (remain 0) { // 库存不足回滚 Redis redisTemplate.opsForValue().incr(key, item.getQuantity()); throw new BusinessException(商品 item.getProductName() 库存不足); } } // 2. MySQL 扣减加行锁 for (OrderItem item : items) { Product product productMapper.selectById(item.getProductId()); if (product.getStock() item.getQuantity()) { throw new BusinessException(商品 item.getProductName() 库存已变更); } // SELECT ... FOR UPDATE 锁住该行 productMapper.lockAndDeductStock(item.getProductId(), item.getQuantity()); } // 3. 创建订单主表 明细表 Order order buildOrder(userId, items); orderMapper.insert(order); orderItemMapper.insertBatch(items); return order; }对应的 MyBatis XML!-- ProductMapper.xml -- !-- 先查询再更新确保行锁生效 -- select idlockAndDeductStock parameterTypemap statementTypeSTATEMENT SELECT * FROM product WHERE id #{productId} FOR UPDATE /select update iddeductStock parameterTypemap UPDATE product SET stock stock - #{quantity} WHERE id #{productId} AND stock #{quantity} /update玄学提示FOR UPDATE必须在UPDATE之前执行且不能放在同一个事务的UPDATE语句里MySQL 会优化掉锁。deductStock的AND stock #{quantity}是二次校验防止 Redis 和 DB 数据不一致。3.4 支付结果异步通知为什么必须用notify_url而非前端轮询微信支付成功后前端wx.requestPayment只返回“调起支付成功”不代表用户真的付款。真正的支付结果由微信服务器主动 POST 到你的notify_url你必须验证签名微信用md5签名密钥为商户后台设置的key解析 XML 数据更新订单状态为PAID发送模板消息通知用户。SpringBoot 接收通知接口// com.milktea.controller.PayController.java PostMapping(value /pay/notify, produces text/xml;charsetUTF-8) public String handlePayNotify(HttpServletRequest request) { try { // 1. 读取原始 XML StringBuilder xml new StringBuilder(); BufferedReader reader request.getReader(); String line; while ((line reader.readLine()) ! null) { xml.append(line); } String xmlStr xml.toString(); // 2. 验证签名微信官方 SDK 已封装 WxPayOrderNotifyResult notifyResult wxPayService.parseOrderNotifyResult(xmlStr); if (!notifyResult.isSuccess()) { return xmlreturn_code![CDATA[FAIL]]/return_codereturn_msg![CDATA[签名失败]]/return_msg/xml; } // 3. 更新订单 String outTradeNo notifyResult.getOutTradeNo(); orderMapper.updateStatusByOutTradeNo(outTradeNo, OrderStatus.PAID); // 4. 发送模板消息略 sendPaySuccessMessage(notifyResult.getOpenid(), outTradeNo); return xmlreturn_code![CDATA[SUCCESS]]/return_codereturn_msg![CDATA[OK]]/return_msg/xml; } catch (Exception e) { log.error(支付通知处理失败, e); return xmlreturn_code![CDATA[FAIL]]/return_codereturn_msg![CDATA[系统错误]]/return_msg/xml; } }关键细节PostMapping必须指定produces text/xml否则微信服务器认为响应格式错误会重试返回 XML 必须严格符合微信要求大小写、CDATA 标签少一个![CDATA[]]都会触发重试outTradeNo是你生成的订单号不是微信的transaction_id。4. 避坑指南微信小程序 SpringBoot 项目里最常翻车的 5 个地方4.1 现象小程序wx.request报错request:fail ssl hand shake error原因后端域名未配置 HTTPS或 SSL 证书链不完整如缺少中间证书。微信强制要求所有request域名必须 HTTPS且证书必须由国内 CA 签发Lets Encrypt 不被信任。解决在腾讯云申请免费 DV 证书下载.pem和.key文件Nginx 配置中ssl_certificate指向合并后的 fullchain.pem含根证书中间证书ssl_certificate_key指向私钥。4.2 现象Vue 页面跳转后onLoad不触发或getCurrentPages()返回空数组原因Vue Router 的history模式在微信 WebView 中被禁用必须强制使用hash模式。解决在src/router/index.ts中配置createWebHashHistory()而非createWebHistory()所有路由路径以/#/开头如/#/order/list。4.3 现象SpringBoot 启动时报错Failed to bind properties to com.wechat.config.WechatConfig原因application.yml中微信配置项缩进错误或wechat:下缺少app-id、app-secret等必填字段。YAML 对空格极其敏感。解决用 VS Code 安装YAML插件开启editor.detectIndentation粘贴配置后按ShiftAltF格式化检查wechat:下所有字段是否顶格且:后必须有一个空格。4.4 现象下单成功但数据库order表无记录日志显示Transaction rolled back because it has been marked as rollback-only原因Transactional方法内调用了另一个Transactional方法且后者抛出异常未被捕获导致外层事务被标记为 rollback-only。解决将库存校验、订单创建拆分为两个独立 Service 方法外层方法捕获内层异常后手动回滚 Redis 预减库存或使用Propagation.REQUIRES_NEW注解隔离事务。4.5 现象微信支付成功但notify_url从未被调用订单状态始终为UNPAID原因notify_url域名未在微信商户平台配置或配置后未点击“保存”或服务器防火墙拦截了 443 端口的 POST 请求。解决登录微信商户平台 → 产品中心 → 开发配置 → 支付结果通知 URL填入https://api.xxx.com/pay/notify并保存用curl -X POST https://api.xxx.com/pay/notify模拟请求确认 Nginx 日志有 access 记录。5. 进阶技巧用 Docker Compose 一键部署 微信开发者工具真机调试法5.1 Docker 化部署三行命令启动全栈环境本地开发调试没问题但交付时总被问“怎么部署到服务器”。本项目已内置docker-compose.yml支持一键拉起 MySQL、Redis、SpringBoot、Nginx 四容器# docker-compose.yml version: 3.8 services: mysql: image: mysql:8.0.33 environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: milktea_db ports: - 3306:3306 volumes: - ./sql/milktea.sql:/docker-entrypoint-initdb.d/init.sql redis: image: redis:7.2-alpine ports: - 6379:6379 backend: build: ./backend environment: - SPRING_PROFILES_ACTIVEprod - WECHAT_APPIDwx1234567890 - WECHAT_APPSECRETabcdefg1234567890 depends_on: - mysql - redis ports: - 8080:8080 nginx: image: nginx:1.25-alpine volumes: - ./frontend/dist:/usr/share/nginx/html - ./nginx.conf:/etc/nginx/conf.d/default.conf ports: - 80:80 - 443:443执行步骤将backend/src/main/resources/application-prod.yml中的数据库地址改为mysqlDocker 内网服务名cd frontend npm run build生成dist/目录docker-compose up -d—— 30 秒后访问http://localhost即可看到小程序前端http://localhost:8080/swagger-ui.html查看 API 文档。提示./sql/milktea.sql是项目自带的初始化脚本含管理员账号admin/123456首次启动自动导入。nginx.conf已配置反向代理/api/到后端避免跨域。5.2 微信开发者工具真机调试如何让同事扫码看到你的本地开发环境微信开发者工具默认只允许127.0.0.1访问但真机扫码调试需暴露本地端口。正确做法是在开发者工具顶部菜单栏 →编译模式 → 选择“本地测试”点击右上角“详情” → “本地服务” → 勾选“不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书”启动 SpringBoot 后在project.config.json中将setting.projectname改为milktea-devminiprogramRoot保持src/关键一步在开发者工具右上角点击“预览” → “二维码” → 扫码后手机端会自动连接你的本地localhost:8080无需配置代理。注意此模式下wx.request仍走localhost:8080所以后端必须监听0.0.0.0:8080SpringBoot 默认已开启。若公司网络限制 8080 端口可在application.yml中改server.port8090然后在开发者工具中修改project.config.json的setting.port字段。5.3 数据库设计中的隐藏考点为什么order_item表要冗余商品名称和价格这是高分项目和玩具项目的分水岭。真实业务中商品价格会变但订单快照必须锁定下单时的价格。若order_item只存product_id当用户半年后查历史订单时看到的可能是当前价格而非当时价格。本项目order_item表结构如下字段类型说明idBIGINT PK主键order_idBIGINT NOT NULL关联订单product_idBIGINT NOT NULL商品 ID用于关联查询product_nameVARCHAR(100) NOT NULL下单时商品名称冗余priceDECIMAL(10,2) NOT NULL下单时单价冗余quantityINT NOT NULL数量total_amountDECIMAL(10,2) NOT NULL小计price × quantity实战技巧SpringBoot 插入order_item时从product表查出name和price后直接赋值绝不用INSERT ... SELECT语句。因为SELECT和INSERT之间可能有其他事务修改商品信息导致快照不一致。我带过 3 届实习生他们第一次独立部署这个项目时80% 的人卡在微信域名配置和 HTTPS 证书上剩下 20% 栽在session_key解密失败——因为没注意到微信返回的encryptedData是 base64 编码而 Java 的Base64.getDecoder().decode()要求输入字符串不能带换行符。后来我把e.detail.encryptedData.replace(/\s/g, )加进 Vue 的onGetPhoneNumber方法里再没人因此加班到凌晨。希望帮到你。本文还有配套的精品资源点击获取