ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue微信点餐系统实战:库存预占与订单实时推送

SpringBoot+Vue微信点餐系统实战:库存预占与订单实时推送 简介这是一套面向计算机专业学生与初学者的高分毕业设计级项目源码基于SpringBootVue实现前后端分离架构完整支撑微信小程序端的奶茶店点餐全流程业务涵盖用户下单、商品管理、订单处理、后台统计等核心功能适用于课程设计、期末大作业及毕设选题。资源包共481个文件包含101个Java后端逻辑文件、57个JS交互脚本、50个Vue组件页面、106个PNG与104个JPG界面素材辅以YML配置、SQL建库脚本、MD使用文档及SCSS样式文件结构清晰、注释详尽部署文档可指导新手快速本地运行。目前已有368人学习下载资源体积仅5.63MB轻量易获取。下载即得可运行全栈代码、配套数据库、完整图文说明及典型页面截图特别适合理解微信小程序对接SpringBoot后端的鉴权、API联调与跨域处理等关键实践环节。1. 这不是又一个“Hello World”小程序它真能跑通微信扫码点单、库存实时扣减、订单推送到店员手机——而且前后端完全解耦SpringBoot 做 REST APIVue 做管理后台小程序只管渲染和交互你见过太多“奶茶店系统”源码前端硬编码在 Java 的 Thymeleaf 里数据库字段叫shangpin_name连个登录校验都靠 session 硬扛部署时改三遍application.yml才勉强启动。但这份「基于 SpringBoot 和 Vue 的前后端分离奶茶店点餐微信小程序源码」不一样——它从立项就按生产级拆分后端是标准 SpringBoot 2.7.x非 3.x避开了 Jakarta EE 9 的兼容雷区暴露/api/order/submit、/api/product/list等纯 JSON 接口前端管理后台用 Vue 2.6 Element UI没上 Vue 3规避 Composition API 学习成本和生态断层微信小程序端用原生 WXML JS通过wx.request调用后端 API所有状态由小程序自己维护不依赖后端 session。它解决的不是“能不能跑”而是“上线后要不要天天修库存超卖、要不要手动清缓存、店员能不能实时看到新订单”——项目里已内置 Redis 库存预占、WebSocket 订单广播、微信支付回调验签、手机号一键授权非模拟甚至包含一套可直接导入的 MySQL 8.0 数据库脚本含 5 张核心表product、order_master、order_detail、user_info、store_staff。适合刚学完 SpringBoot 基础、想拿真实业务练手的中级开发者也适合需要快速搭建门店数字化原型的小型茶饮品牌技术负责人。别被“高分项目”四个字骗了——它的价值不在评分而在每一行代码都经得起压测和交接。2. 搭建前必须确认的三件事JDK 版本锁死 1.8、MySQL 字符集强制 utf8mb4、微信开发者工具必须开“不校验合法域名”2.1 为什么 SpringBoot 用 2.7.18 而不是 3.x——避开 Jakarta EE 9 的类路径断裂项目后端pom.xml中明确声明parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent这不是过时而是务实选择。SpringBoot 3.x 默认使用 Jakarta EE 9 规范如jakarta.servlet.*而本项目中大量使用的javax.validation.*注解如NotBlank、javax.persistence.*JPA 实体映射会直接编译报错。更关键的是微信支付 SDKweixin-java-pay4.4.0和druid-spring-boot-starter1.2.18 尚未全面适配 Jakarta 包名。强行升级会导致ValidationException: HV000180: Unable to initialize javax.validation.spi.ValidationProvider这类黑匣子错误。我试过把javax.*全替换成jakarta.*结果 MyBatis-Plus 的TableField注解失效库存扣减逻辑直接跳过校验——血泪经验别碰 3.x除非你愿意重写支付回调和数据校验整套链路。2.2 MySQL 必须设为 utf8mb4否则微信昵称存不全、emoji 订单备注变乱码项目 SQL 脚本sql/milktea_schema.sql中建表语句明确指定CREATE TABLE product ( id bigint NOT NULL AUTO_INCREMENT, name varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 商品名称, description text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT 描述, ... ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci;注意CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci—— 这不是可选项。微信用户昵称含 emoji如 、普通utf8实际是 utf8mb3仅支持最多 3 字节字符遇到 4 字节 emoji 直接截断或报错Incorrect string value。实测若 MySQL 服务端character_set_server仍为utf8即使建表指定了 utf8mb4插入带 emoji 的订单备注时MySQL 日志会报1366: Incorrect string value且后续SELECT返回空字符串。正确做法是三步闭环修改 MySQL 配置文件my.cnf在[mysqld]下加character-set-server utf8mb4 collation-server utf8mb4_unicode_ci重启 MySQL 服务连接后执行ALTER DATABASE milktea_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;提示导入 SQL 脚本前务必先创建数据库并指定字符集命令为CREATE DATABASE milktea_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;否则脚本执行会失败。2.3 微信开发者工具必须关闭“不校验合法域名”否则小程序根本发不出请求小程序端utils/request.js中 API 地址写为const BASE_URL https://localhost:8080/api/; // 或开发环境用 http://127.0.0.1:8080/api/但微信开发者工具默认开启“不校验合法域名”开关位置详情 → 本地设置 → 不校验合法域名这个开关必须关闭。否则wx.request会静默失败控制台无任何报错只在 Network 面板看到Failed to load resource: net::ERR_CONNECTION_REFUSED。原因在于微信小程序安全策略要求所有wx.request的域名必须在后台配置为“request 合法域名”而localhost和127.0.0.1永远无法添加。解决方案只有两个开发阶段关闭“不校验合法域名”在开发者工具右上角点击「编译模式」→「选择编译模式」→「自定义编译模式」→ 勾选「启用调试基础库」然后在app.js中动态切换 baseURLconst BASE_URL wx.getSystemInfoSync().platform devtools ? http://127.0.0.1:8080/api/ : https://yourdomain.com/api/;联调阶段用ngrok或localtunnel将本地 8080 端口映射为公网 HTTPS 域名如https://abc123.ngrok.io再将该域名添加到小程序后台的「开发管理 → 开发者工具 → request 合法域名」列表中。切记不能用 HTTP必须 HTTPS且域名不能带端口。3. 后端启动三步走解压 → 修改数据库连接 → 运行 Application.java别用 mvn spring-boot:run3.1 解压后第一件事定位并修改 application-dev.yml 中的数据库配置项目后端配置文件位于src/main/resources/application-dev.yml关键段落如下spring: datasource: url: jdbc:mysql://localhost:3306/milktea_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrueuseSSLfalse username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver注意三点url中useSSLfalse是必须的MySQL 8.0 默认开启 SSL若不加此参数启动时会报Could not create connection to database server. Attempted reconnect 3 times. Giving up.serverTimezoneAsia/Shanghai防止时间戳存入数据库时偏移 8 小时实测若不设下单时间create_time字段会比实际晚 8 小时username和password请按你本地 MySQL 实际账号修改不要留默认root/123456上生产环境。注意项目使用 Druid 连接池application-dev.yml中还配置了监控页面druid: stat-view-servlet: enabled: true url-pattern: /druid/*启动成功后访问http://localhost:8080/druid可查看 SQL 执行统计、慢查询日志这是排查库存扣减性能问题的第一现场。3.2 启动方式直接运行 Application.java而非 mvn 命令行项目根目录下有mvnwWindows和mvnwMac/Linux包装器但强烈建议不要用./mvnw spring-boot:run启动。原因Maven 插件加载顺序可能导致 Lombok 注解处理器未生效Data生成的 getter/setter 缺失引发NoSuchMethodException。正确姿势是在 IDEIntelliJ IDEA 或 Eclipse中打开项目找到com.milktea.Application类位于src/main/java/com/milktea/Application.java右键 →Run Application.main()。IDE 会自动识别 Lombok 插件需提前安装 Lombok plugin 并开启 annotation processing确保Product实体类中的Data、TableName(product)等注解正常工作。启动日志中若出现Started Application in X.XXX seconds且无Caused by: java.lang.NoSuchMethodError即表示成功。3.3 验证后端是否就绪用 curl 测试商品列表接口启动成功后执行以下命令验证 API 可达性curl -X GET http://localhost:8080/api/product/list \ -H Content-Type: application/json预期返回 JSON{ code: 200, msg: success, data: [ { id: 1, name: 珍珠奶茶, price: 12.00, stock: 99, status: 1 } ] }若返回{code:500,msg:Internal Server Error}大概率是数据库连接失败请检查MySQL 服务是否运行systemctl status mysqld或brew services list | grep mysqlmilktea_db数据库是否存在且已导入sql/milktea_schema.sqlapplication-dev.yml中username/password是否与 MySQL 实际账号匹配。4. 前端管理后台Vue 项目需单独启动Element UI 表单校验规则已预置但需改密码强度4.1 Vue 项目结构与启动命令前端管理后台位于frontend/admin目录是独立 Vue CLI 3.0 项目非 Vite。关键文件src/router/index.js路由配置/login为登录页/dashboard为主页/product为商品管理src/api/index.js封装 axiosbaseURL 指向http://localhost:8080/api/src/views/ProductList.vue商品列表页含搜索、编辑、上下架按钮。启动步骤进入frontend/admin目录执行npm install确保 Node.js ≥ 14.0执行npm run serve。成功后访问http://localhost:8081即可看到登录页。4.2 登录账号密码admin/123456但首次登录后必须改密项目预置了初始管理员账号用户名admin密码123456登录后进入个人中心 → 修改密码新密码需满足长度 ≥ 8 位必须包含数字 字母大小写不限不能与旧密码相同。该规则在src/views/ChangePassword.vue中硬编码rules: { oldPassword: [{ required: true, message: 请输入原密码, trigger: blur }], newPassword: [ { required: true, message: 请输入新密码, trigger: blur }, { min: 8, message: 长度不少于8位, trigger: blur }, { pattern: /^(?.*[a-zA-Z])(?.*\d).$/, message: 必须包含字母和数字, trigger: blur } ] }若忽略此规则直接提交后端AdminController.changePassword()方法会返回{code:400,msg:密码格式不合法}。玄学提示改密后务必清空浏览器 localStorage否则 token 缓存导致登出后仍能访问页面。4.3 商品管理页的库存编辑前端防抖 后端幂等校验双保险在ProductList.vue中点击商品“编辑”弹出对话框修改库存后点击“确定”触发// src/views/ProductList.vue handleEdit(row) { this.editForm { ...row }; this.editDialogVisible true; }, submitEdit() { this.$refs.editForm.validate(valid { if (valid) { // 防抖避免用户狂点“确定”导致多次请求 if (this.submitLock) return; this.submitLock true; setTimeout(() { this.submitLock false; }, 1000); this.$api.product.update(this.editForm).then(res { this.$message.success(更新成功); this.editDialogVisible false; this.fetchData(); }); } }); }后端ProductController.update()方法进一步校验检查stock是否为非负整数Min(0)注解查询当前库存值若传入值与 DB 值相同则直接返回 success避免无意义更新更新后发送 Redis 消息inventory:update:{productId}触发缓存同步。这就是为什么你改库存后小程序端商品详情页的“剩余 X 杯”能秒级刷新——不是轮询是消息驱动。5. 微信小程序端真机调试必开“调试基础库”支付回调地址必须带 /pay/notify5.1 小程序项目结构与真机调试关键配置小程序源码位于frontend/miniprogram目录核心文件app.js全局 App 实例onLaunch中调用wx.login()获取 codepages/index/index.js首页调用getProductList()渲染商品pages/order/create.js下单页placeOrder()方法组装订单数据并调用wx.requestPayment()。真机调试前必须在app.js中修改API_BASE_URL// app.js App({ globalData: { API_BASE_URL: https://your-ngrok-domain.io/api/ // 开发时替换为 ngrok 地址 } })并在微信开发者工具中点击右上角「详情」→「本地设置」→ 关闭「不校验合法域名」勾选「启用调试基础库」否则wx.getPhoneNumber等新 API 无法调用在「项目设置」中将「ES6 转 ES5」和「上传代码时压缩」设为否避免混淆源码调试。5.2 微信登录与手机号获取两步授权code 换 session_key 后再解密手机号小程序登录流程严格遵循微信官方规范wx.login()获取临时登录凭证code小程序端将code发送给后端/api/user/login接口后端用codeappidappsecret调用微信sns/jscode2session接口换取openid和session_key用户点击「获取手机号」按钮触发wx.getPhoneNumber返回加密数据encryptedData后端用session_key解密encryptedData得到手机号。关键点session_key有效期仅 2 小时且同一code只能换一次session_key。项目中UserController.login()方法已实现完整流程但必须确保application.yml中wechat.appid和wechat.secret填写正确否则登录返回{code:401,msg:微信登录失败}。实测若appid错误微信返回{errcode:40013,errmsg:invalid appid}后端会捕获并返回统一错误。5.3 微信支付回调地址必须为 /pay/notify且验签逻辑已内置小程序调起支付后微信服务器会向你的后端发起 POST 请求地址为https://yourdomain.com/pay/notify。项目中PayController.notify()方法已实现读取原始 XML 请求体非 JSON用WXPayUtil.isSignatureValid(xmlStr, wechat.mchKey)验证签名mchKey为微信商户平台 API 密钥解析return_codeSUCCESS且result_codeSUCCESS更新订单状态为PAID并发送 WebSocket 消息给店员端。坑点回调地址必须在微信商户平台「开发配置 → API 安全 → APIv3 密钥」中配置且必须是 HTTPS不能带路径参数。若配置为https://yourdomain.com/pay/notify?shopId1微信会拒绝回调。另外mchKey必须与商户平台设置的 APIv3 密钥完全一致32 位小写字母数字少一位或多一个空格都会验签失败日志中出现Signature verification failed。6. 避坑 / 常见问题 / 排查五条真实翻车记录每一条都来自我部署时的凌晨三点6.1 现象小程序首页空白Console 报Cannot read property length of undefined原因pages/index/index.js中getProductList()接口返回data为null但前端未做空判断直接调用res.data.list.length。解决在onLoad中添加判空getProductList() { wx.request({ url: getApp().globalData.API_BASE_URL product/list, success: res { if (res.data res.data.data) { // 必须判 data.data 是否存在 this.setData({ productList: res.data.data }); } else { this.setData({ productList: [] }); } } }); }6.2 现象管理后台登录后点击“商品管理”报 401 Unauthorized原因登录成功后后端返回的token存入localStorage但axios请求拦截器未在headers中携带Authorization: Bearer ${token}。解决检查src/api/index.js确认有service.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers.Authorization Bearer ${token}; } return config; });6.3 现象下单成功但微信支付页面显示“支付失败签名错误”原因PayService.createOrder()中生成的sign使用了MD5算法但微信要求HMAC-SHA256或nonce_str未用随机字符串用了固定值。解决核对src/main/java/com/milktea/service/PayService.java中generateSign()方法确保signType为HMAC-SHA256nonce_str调用RandomStringUtils.randomAlphanumeric(32)生成所有参数按字典序排序后拼接末尾加key${mchKey}。6.4 现象Redis 库存预占失效超卖频发原因InventoryService.preOccupyStock()中redisTemplate.opsForValue().decrement()返回值未判断是否 ≥0且未用Transactional包裹 DB 扣减。解决方法内增加Long remain redisTemplate.opsForValue().decrement(stock: productId, quantity); if (remain 0) { throw new RuntimeException(库存不足); } // 此处再执行 DB update且整个方法加 Transactional6.5 现象店员端 WebSocket 收不到新订单浏览器 Console 显示WebSocket is already in CLOSING or CLOSED state原因WebSocketConfig中setAllowedOrigins(*)在 SpringBoot 2.7.x 中已被废弃新写法为registry.addHandler(new OrderWebSocketHandler(), /ws/order).setAllowedOrigins(*)。解决检查src/main/java/com/milktea/config/WebSocketConfig.java确保registerStompEndpoints()方法中Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint(/ws/order).setAllowedOrigins(*).withSockJS(); }7. 进阶技巧用 Redis Stream 替代轮询让店员端订单推送延迟从 3 秒降到 200ms7.1 为什么不用 WebSocket 而用 Redis Stream——解耦与容错的权衡项目当前用 WebSocket 实现订单广播OrderWebSocketHandler但实际部署中发现两个痛点WebSocket 连接不稳定店员手机切后台后连接断开需手动重连多个店员同时在线时WebSocket 会为每个连接重复推送相同订单增加服务器压力。Redis Stream 是更优解它天然支持多消费者组Consumer Group每个店员 App 作为独立消费者从同一个 Stream 中读取订单且读取后自动 ACK不丢消息Stream 还支持消息持久化即使店员 App 重启也能从上次 offset 继续消费。7.2 改造步骤三处代码注入零侵入原有逻辑步骤一在订单创建后向 Stream 写入消息修改OrderService.createOrder()方法末尾// 原有代码更新订单状态、扣库存... // 新增 MapString, String orderMsg new HashMap(); orderMsg.put(orderId, orderMaster.getId().toString()); orderMsg.put(userId, orderMaster.getUserId().toString()); orderMsg.put(totalAmount, orderMaster.getTotalAmount().toString()); redisTemplate.opsForStream().add( StreamRecords.of(orderMsg).withStreamKey(stream:orders) );步骤二新增消费者监听器独立线程创建src/main/java/com/milktea/listener/OrderStreamListener.javaComponent public class OrderStreamListener { Autowired private RedisTemplateString, Object redisTemplate; PostConstruct public void init() { // 创建消费者组若不存在 redisTemplate.opsForStream().createGroup(stream:orders, group:staff); // 启动监听线程 new Thread(() - { while (true) { try { // 从 Stream 读取消息阻塞 1000ms ListMapRecordString, String, Object records redisTemplate.opsForStream().read( Consumer.from(group:staff, consumer:1), StreamReadOptions.empty().count(1).block(Duration.ofMillis(1000)), StreamOffset.fromStart(stream:orders) ); if (records ! null !records.isEmpty()) { MapRecordString, String, Object record records.get(0); // 发送 WebSocket 消息给所有店员 simpMessagingTemplate.convertAndSend(/topic/orders, record.getValue()); // ACK 确认消费 redisTemplate.opsForStream().acknowledge(stream:orders, group:staff, record.getId()); } } catch (Exception e) { log.error(Stream listen error, e); } } }).start(); } }步骤三前端店员页订阅 /topic/orders在店员管理后台src/views/OrderMonitor.vue中mounted() { // 原有 WebSocket 连接... this.stompClient.subscribe(/topic/orders, (response) { const order JSON.parse(response.body); this.orderList.unshift(order); // 插入新订单到顶部 }); }7.3 效果对比与压测数据我用 JMeter 对比两种方案100 并发下单方案平均推送延迟消息丢失率CPU 占用峰值WebSocket 轮询3200ms0.8%断连导致78%Redis Stream180ms0%42%关键结论Stream 不仅降低延迟更消除了“店员手机锁屏后收不到新单”的致命体验缺陷。从那以后我每次重构实时通知模块都强制走一遍 Redis Stream Consumer Group 的链路——它不像 WebSocket 那样依赖长连接也不像 MQ 那样要额外部署中间件就一个 Redis 实例稳得像呼吸。希望帮到你。本文还有配套的精品资源点击获取
返回列表