恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
SpringBoot+微信小程序点餐系统实战:从架构到支付回调避坑指南
首页
资讯中心
/
SpringBoot+微信小程序点餐系统实战:从架构到支付回调避坑指南
SpringBoot+微信小程序点餐系统实战:从架构到支付回调避坑指南
发布时间:2026/10/11 17:38:09
如果你最近在调研“小程序点餐系统”这类题目大概率会看到一堆千篇一律的项目骨架用户登录、商品列表、下单、支付没了。但真正到了答辩或者上线阶段才会发现购物车并发、库存扣减、微信支付回调、小程序体验版配置这些才是拉开差距的地方。这篇博文我就以一套完整的 SpringBoot 微信小程序咖啡店点餐系统为例把从架构设计、表结构、核心接口到小程序端实现、联调部署的过程完整过一遍重点讲清楚每一步为什么这么做以及实测中容易踩的坑。这个项目适合三类人第一准备做毕业设计或课程设计的在校同学可以直接参考这套思路搭出自己的系统第二刚入门全栈开发、想搞懂前后端如何闭环的开发者我会把登录态、接口鉴权、订单状态机这些基础但关键的点都拆开讲第三有真实运营需求的小店老板或独立开发者可以照着这套设计快速出原型再根据实际业务做裁剪。先说结论技术上没有高难度内容但完整跑通需要耐心处理几个隐藏细节。当年我花了一周把主流程写完结果又花了两周跟微信支付和会话登录“搏斗”希望这篇能帮你把这两周省下来。1. 项目整体设计与技术选型1.1 为什么是 SpringBoot 微信小程序而不是别的组合市面上做点餐系统的方案其实很多纯 H5 做移动端、原生 App、公众号网页、甚至扫码点单的 SaaS 平台。但 SpringBoot 微信小程序这套组合在“校园级项目”和“中小商家轻量需求”这两个场景下是性价比最高的原因很直接。微信小程序对用户几乎没有使用成本扫一扫或者搜一下就能打开不需要下载安装这对咖啡店这种高频、低客单价的场景特别合适。顾客可能只是在等咖啡的间隙顺手点一杯让他专门去下载一个 App 显然不现实。小程序天然具备微信生态的传播便利用户点完单可以顺手转发给同事这个裂变路径比 H5 短得多。后端选 SpringBoot 则是因为它足够“标准”。Java 生态里 SpringBoot 的起步依赖、自动配置、Starter 机制让开发效率很高而且网上资料极其丰富遇到问题几乎都能搜到解决方案。对于学生项目或者小团队来说“能快速找到人问”本身就是很大的优势。相比 Node.js 或 Python 后端SpringBoot 在事务管理、接口规范、部署生态上更成熟订单这类涉及金额和库存的业务强一致性的保障会更让人放心。当然这套组合也有代价小程序端只能用微信那一套语法不能直接复用 Web 组件库后端要处理微信登录解密的逻辑。但这些成本是可控的而且都是成熟的既定流程按官方文档走就行。1.2 系统核心角色与业务闭环这套系统我拆成了三个端顾客使用的小程序端、商家使用的管理后台Web端、以及统一提供接口的 SpringBoot 后端服务。小程序的角色明确为普通用户管理后台可以是 Web 页面用 Vue 或 Thymeleaf 都行也可以是另一个小程序取决于你的实际场景。我做的这套里管理后台采用了一个简单的 Web 页面前后端不分离直接用 Thymeleaf 渲染理由是减少部署复杂度把核心精力放在接口和业务逻辑上。如果你用前后端分离的 Vue 后台思路完全一致只是多一层跨域和鉴权处理。核心角色有三类顾客浏览商品、加购物车、下单、支付、查看订单状态。商家管理员管理商品分类与商品信息、上下架、处理订单接单、制作完成、查看营业数据。系统后端服务负责鉴权、业务规则校验、库存扣减、订单状态流转、支付回调处理。业务闭环可以描述成一条链路顾客打开小程序 → 微信登录换取 openid → 浏览菜单、加购物车 → 提交订单 → 微信支付 → 支付回调更新订单状态 → 商家后台看见新订单 → 商家接单/制作/出餐 → 顾客查看订单进度 → 完成评价可选。这条链路里最值得花心思设计的不是“下单”这个动作本身而是“订单状态怎么安全地流转”。点餐不像普通电商有退款、售后的复杂流程但也至少包含待支付、已支付、制作中、已完成、已取消。有些商家还需要“待自提/待配送”这类状态根据实际业务扩展即可。订单状态机设计我放在后面详细说这里先记住一个原则状态变更必须由后端统一控制小程序端只能提交动作请求绝不能自己改状态。1.3 数据库表结构设计要点数据库设计直接决定后续写代码是丝滑还是痛苦。点餐系统的表结构不算复杂核心围绕“商品—订单—用户”三条线展开。我的核心表设计如下用户表user用户ID、openid微信唯一标识必加唯一索引、昵称、头像、手机号、注册时间。商品分类表category分类ID、分类名称、排序值、状态启用/停用。商品表product商品ID、分类ID、商品名称、描述、图片URL、原价、现价、月销量、库存、状态上架/下架、创建时间。购物车表cart购物车ID、用户ID、商品ID、商品数量、选中状态、加入时间。这里有个关键点——购物车是存后端还是只存小程序本地我当时采用“后端存储 本地缓存”双轨方案未登录时可以操作本地购物车登录后以后端数据为准。如果只做本地存储换设备购物车就丢了如果只做后端存储每次加购都要请求接口体验会卡。折中方案是登录后强制同步一次。订单表orders订单ID、订单编号业务上展示用用时间戳随机数生成、用户ID、总金额、实际支付金额、优惠金额、订单状态、收货/自提信息、备注、创建时间、支付时间、完成时间。订单明细表order_item明细ID、订单ID、商品ID、商品名称快照这点非常重要后面讲、商品图片快照、单价快照、数量、小计。这里有个新手容易犯的错误订单明细里只存商品ID不存商品名称和价格。结果商家改价或者删除商品后历史订单显示的名称和金额全变了查账对不上非常尴尬。正确的做法是做“快照”下单那一刻把商品名称、图片、单价都冗余进明细表。订单是交易凭证必须能还原当时的交易现场。还有一张表容易忽略微信支付回调日志表pay_log记录回调的原始报文、处理状态、错误信息排查支付问题全靠它。表与表之间的关系上订单表和订单明细是一对多用户和订单是一对多商品和分类是多对一。订单表一定要加 user_id 和 status 两个索引否则用户查历史订单、商家按状态筛选时数据量稍微上来就会很慢。2. 核心功能拆解与实现路径2.1 用户端小程序端的核心页面与交互小程序端我划分了五个核心页面首页/菜单页展示分类和商品列表支持按分类切换、搜索、按销量或价格排序。商品详情页展示商品大图、描述、价格、规格选择大杯/中杯、冷/热、加糖/少糖这类咖啡店特色属性、加入购物车按钮。购物车页已选商品列表、数量增减、合计金额、结算按钮。确认订单页选择自提或外送、填写备注、选择优惠券可以不做、提交订单并拉起支付。订单列表页/订单详情页查看待支付、制作中、已完成状态的订单支持取消订单。页面结构不复杂重点在几个交互细节上。“加购”这个动作我一开始犯了个错误每次都直接请求后端导致用户快速点击时加载动画频繁体验很差。后来改成加购时先更新本地购物车数据界面立刻响应然后异步同步到后端。如果同步失败比如网络断了下次进入购物车页时再提示重新同步。体验和可靠性兼顾。菜单页的加载策略也有讲究。商品列表如果每次进入页面都重新请求接口会有白屏等待。我采用的是首次加载全部商品数据缓存到本地设置10分钟过期时间管理员在后台改价、上下架时前端下拉刷新即可拉取最新。同时在后端接口里加了版本号机制商品更新时间戳小程序端启动时对比版本号不一致才全量刷新节省流量也提升速度。规格属性的实现咖啡店场景下比较常见的做法是定义一个商品规格模板。比如“温度冰/热”“杯型中杯/大杯”“糖度标准/半糖/无糖”。对于毕设或轻量系统我建议直接在商品表里加一个 extra_properties 字段存 JSON 字符串类似[{name:温度,options:[冰,热]},{name:杯型,options:[中杯,大杯]}]这样后端不需要建复杂的规格表前端拿到后动态渲染选项下单时再把用户的选择记录到订单明细的备注字段里。真要搞 SKU库存单位级别的规格管理系统复杂度会指数上升除非业务强需求否则不建议在第一版做。2.2 商家端后台管理功能商家后台我做了五个模块仪表盘数据统计、商品管理、分类管理、订单管理、基础设置。仪表盘展示今日营业额、今日订单数、待处理订单数、热销商品 Top 榜。批量查询这些数据时最忌讳在 Java 里循环查数据库然后内存求和——对小型项目来说数据量不大其实无所谓但既然写出来了还是建议用一条 SQL 做 GROUP BY 聚合性能和代码简洁度都会好很多。订单管理是后台最核心的部分。订单列表默认展示“待接单”状态的订单新订单要能实时提醒。我第一版用的是前端轮询每5秒查询一次新订单简单但不够优雅。后来引入了 WebSocket 推送商家端页面建立长连接后端在订单支付成功回调里推送一条新订单通知商家端收到后自动刷新列表。轮询作为兜底保留双保险。实际业务中这个推送很重要。咖啡店高峰期商家不会一直盯着后台刷新按钮必须有主动提醒。WebSocket 的实现不难SpringBoot 里加一个 WebSocketConfig 和 Handler用 ConcurrentHashMap 维护会话。真正麻烦的是会话管理——商家端多标签页打开时会建立多条连接要按用户维度做分组推送。这个后面在常见问题里会讲。商品管理就是常规的增删改查加上下架注意图片上传的处理。图片我选择存储到本地服务器目录数据库里存访问路径这样开发环境简单。上线后建议换对象存储比如阿里云 OSS否则服务器磁盘会一直增长。上传时还要做文件类型和大小校验防止有人上传脚本文件。2.3 关键业务逻辑购物车、订单与状态机设计购物车的核心逻辑有两个合并和数量校验。合并场景用户在小程序里未登录时加了几件商品到本地购物车然后点击结算触发登录登录成功后将本地购物车和后端购物车合并同一商品数量相加。合并操作必须是原子的不能先删后插否则并发下会丢数据。我用的事务方法是先查后端已有购物车项逐条对比存在就更新数量不存在就新增。整体包在事务里。数量校验每次加购、下单前后端必须校验商品状态是否为上架、库存是否足够。很多项目只在界面上判断“库存0”下单接口里不校验结果超卖。库存扣减我推荐使用乐观锁或条件更新最简单的做法是 SQL 里加条件UPDATE product SET stock stock - #{count} WHERE id #{productId} AND stock #{count}执行后返回受影响行数如果为0说明库存不足下单失败。这个小技巧能避免并发场景下的超卖问题。另一个点是库存扣减的时机。我建议在“提交订单时扣减库存”如果用户超时未支付订单取消时回补库存。这样能锁定库存避免用户下单后没货。缺点是需要处理取消订单的补偿逻辑。还有一种做法是支付成功后再扣减库存但这样会出现用户支付成功了商家却没货的尴尬处理起来更麻烦。两相对比我选了前者。订单状态机的设计是整套系统的骨架。我定义的状态如下待支付0刚提交支付超时自动取消。已支付/待接单1支付成功等待商家接单。制作中2商家已接单开始制作。已完成3制作完成顾客取餐/送达订单终结。已取消4用户主动取消或超时未支付自动取消。状态变更的规则是0 → 1支付成功1 → 2商家接单2 → 3出餐0 → 4超时/用户取消1 → 4商家拒单/用户退款。这些流转必须由后端统一校验非法跳转一律拒绝。实现上我封装了一个订单状态变更方法入参是订单ID、当前状态、目标状态和操作者信息方法内部先查订单判断当前状态是否合法然后更新并记录变更日志。这个日志表order_status_log在后期排查问题和做数据统计时非常有用。超时未支付自动取消我用了两种策略配合下单时创建定时任务延迟队列在15分钟后检查订单状态同时提供一个主动查询接口用户重新进入订单页时如果发现订单超时前端调接口取消。定时任务用延时队列或者 Spring 的 Scheduled 轮询都可以小项目没必要引入消息中间件。2.4 微信登录与鉴权机制微信小程序登录的原理很多文章讲过但真正落地时有一个容易卡住的点code2Session 接口的调用时机。标准流程是这样的小程序端 wx.login() 获取临时 code。把 code 发给后端。后端调用微信接口 code2Session换取 openid 和 session_key。后端用 openid 查用户表存在则返回登录成功不存在则自动注册一个新用户。后端生成自定义登录态 token比如 JWT返回给小程序。小程序后续请求都在 header 里带上 token。这里有个细节新手经常忽略session_key 是微信加密用户数据的密钥需要配合用户授权手机号等能力时才用得上。普通的静默登录其实不需要 session_key只要 openid。所以如果项目不涉及解密用户信息session_key 拿到后可以不存避免敏感信息泄露风险。登录接口的并发问题值得注意如果两个请求同时用同一个 code 换取 openid微信会报错因为 code 是一次性的。更关键的是同一用户快速调用两次 wx.login()会生成两个不同的 code后端的处理方式应该是“一个 code 只能换一次”代码里要处理 code 无效的异常提示前端重新登录。token 我用的是 JWT好处是无状态、可以携带用户ID和角色信息。坏处是没法主动失效比如用户被封禁后 token 仍然有效。对于点餐系统用户封禁场景很少所以 JWT 够用。如果要做严格的权限控制推荐换成服务端存储的 session token可以随时踢人下线。接口鉴权我用了一个拦截器除登录接口、商品浏览接口外所有 /api/user/** 请求都要校验 token。管理员后台用独立的拦截器校验管理员身份并且要求管理员账号是独立的表不能跟普通用户混在一起权限边界要清晰。3. 实操过程核心模块从零到可用3.1 工程结构与环境准备后端工程我用的标准 Maven 结构Java 8如果你用更高版本也行但 8 的兼容性最稳SpringBoot 2.7.xMyBatis-Plus 作为 ORM 框架MySQL 8.0Redis 用来存验证码和部分缓存小型项目可以用 HashMap 替代但最好还是用 Redis后续扩展方便。工程包结构按业务模块划分com.example.coffee ├── config // 配置类拦截器、WebSocket、Redis等 ├── controller // 接口层 ├── service // 业务逻辑层 ├── mapper // 数据访问层 ├── entity // 实体类 ├── dto // 入参出参对象 ├── common // 统一返回结果、异常处理、工具类这种包结构虽然网上到处都有但真的建议遵守尤其是你的项目要给别人看或者答辩的情况下。清晰的业务边界比你用多高深的技术都重要。小程序端我是用原生微信开发者工具写的没有用 uni-app 或 Taro。原因很简单这套系统场景固定不需要跨端原生语法最直接调试也最方便。如果你未来想同时发布支付宝小程序再考虑 uni-app 不迟。小程序端目录结构我做了简单分层miniprogram/ ├── pages/ // 页面文件 ├── components/ // 自定义组件商品卡片、数量选择器等 ├── utils/ // 封装工具请求、登录态、时间格式化 ├── api/ // 接口调用封装 └── app.js // 全局逻辑登录态管理3.2 后端核心实现订单接口与支付回调以提交订单接口为例完整实现包含以下步骤Override Transactional(rollbackFor Exception.class) public SubmitOrderResult submitOrder(SubmitOrderDTO dto, Long userId) { // 1. 校验用户购物车 ListCartItem cartItems cartMapper.selectByUserId(userId); if (CollectionUtils.isEmpty(cartItems)) { throw new BusinessException(购物车不能为空); } // 2. 根据购物车生成订单明细同时校验商品状态和库存 BigDecimal totalAmount BigDecimal.ZERO; ListOrderItem orderItems new ArrayList(); for (CartItem item : cartItems) { Product product productMapper.selectById(item.getProductId()); if (product null || product.getStatus() ! 1) { throw new BusinessException(商品已下架 item.getProductName()); } // 3. 乐观锁扣减库存 int rows productMapper.deductStock(item.getProductId(), item.getQuantity()); if (rows 0) { throw new BusinessException(库存不足 product.getName()); } // 4. 生成订单明细快照 OrderItem orderItem new OrderItem(); orderItem.setProductId(product.getId()); orderItem.setProductName(product.getName()); orderItem.setProductImage(product.getImage()); orderItem.setPrice(product.getPrice()); orderItem.setQuantity(item.getQuantity()); orderItem.setSubtotal(product.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); totalAmount totalAmount.add(orderItem.getSubtotal()); orderItems.add(orderItem); } // 5. 生成订单主记录 Order order new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setTotalAmount(totalAmount); order.setStatus(OrderStatusEnum.PENDING_PAY.getCode()); order.setRemark(dto.getRemark()); // 6. 保存订单和明细清空购物车 orderMapper.insert(order); orderItemMapper.batchInsert(order.getId(), orderItems); cartMapper.deleteByUserId(userId); // 7. 调用统一支付入口生成支付参数返回给前端 String payParams paymentService.createPayParams(order.getId(), totalAmount, 咖啡店点餐); return new SubmitOrderResult(order.getId(), order.getOrderNo(), totalAmount, payParams); }这里最重要的就是 Transactional 和库存扣减那一步。整个下单操作必须当作一个原子事务处理要么全部成功要么全部回滚。有时候新手发现订单生成了但库存没扣通常是因为扣库存操作在事务之外执行或者抛了异常但没触发回滚。支付部分我用的是微信支付 Native/小程序支付后端只需要做两步第一步创建预支付订单调用微信支付的下单接口拿到 payment 参数timeStamp、nonceStr、package、signType、paySign返回给小程序端第二步处理支付结果回调。小程序的 wx.requestPayment 拿到这些参数就能拉起收银台。支付回调是很多人头疼的地方核心是“验签 幂等 处理业务”。微信支付成功后微信服务器会 POST 一个 XML/JSON 通知到你的回调地址。你必须在回调里做以下几件事验证签名确认请求确实来自微信支付防止伪造回调。根据订单号查询订单判断是否已经处理过处理过就直接返回成功不再重复处理幂等性。更新订单状态为已支付记录支付流水号给商家端推送新订单提醒。返回成功应答给微信否则微信会认为通知失败会多次重试。这个回调地址必须是公网可访问的 HTTPS 地址。如果只是在本地开发可以用内网穿透工具把本机端口暴露到公网但仅限测试上线必须用正式域名和 HTTPS 证书。回调日志一定要记全包括原始报文、解密后的内容、处理结果不然排错时两眼一抹黑。3.3 小程序端核心实现登录、请求封装与页面交互小程序端的核心是统一请求封装。我在 utils/request.js 里封装了 wx.requestconst request (url, method, data, header {}) { return new Promise((resolve, reject) { wx.request({ url: baseUrl url, method, data, header: { Content-Type: application/json, Authorization: wx.getStorageSync(token) || , ...header }, success: (res) { // 统一处理 401 未登录重新登录后再重试一次 if (res.statusCode 401) { // 重新登录逻辑 reLogin().then(() { request(url, method, data, header).then(resolve).catch(reject); }); return; } if (res.data.code 0) { resolve(res.data.data); } else { wx.showToast({ title: res.data.msg, icon: none }); reject(res.data); } }, fail: (err) { wx.showToast({ title: 网络异常请稍后重试, icon: none }); reject(err); } }); }); };这个封装有几个关键点处理 401 时不能无脑重新登录必须判断请求次数否则登录接口本身报 401 会导致无限递归。我加了一个 retry 标记只重试一次。后端统一返回结构是 { code: 0, msg: success, data: ... }非 0 码统一弹 Toast前端不需要每个页面重复写错误提示。登录态 token 存储在本地但注意真正比较安全的做法是 token 由后端生成的短期 token2小时过期配合小程序端的静默刷新机制而不是一个 token 用一个月。微信小程序有 session 过期机制后端 token 有效期设得太长会有安全风险。登录流程的代码大致是function login() { return new Promise((resolve, reject) { wx.login({ success: async (res) { if (res.code) { const data await request(/api/user/login, POST, { code: res.code }); wx.setStorageSync(token, data.token); wx.setStorageSync(userInfo, data.userInfo); resolve(data); } else { reject(登录失败); } }, fail: reject }); }); }购物车页面需要在 onShow 生命周期里重新拉取购物车数据因为用户可能在商品详情页加了购物车再切回来如果不刷新数据就会错乱。这个小细节很多人不注意。确认订单页的流程是从购物车页携带选中的购物车项 ID 列表跳转页面内拉取后端汇总的订单预览信息金额、商品列表、优惠用户确认后点击“提交订单”调用下单接口拿到支付参数后调 wx.requestPayment。wx.requestPayment 的失败处理很关键。用户取消支付返回 errMsg 包含 cancel不要当作异常处理要提示用户“订单未支付可在订单列表中继续支付”然后跳转到订单列表页。用户支付成功success后不要立刻跳转首页建议跳转到订单详情页显示“商家正在制作中”。这里前端还需要处理一个中间态支付成功但后端订单状态还没更新回调延迟此时页面要有个轮询逻辑过几秒再查订单状态。3.4 小程序点餐的 UI 与交互优化不少同学把精力全花在后端小程序端做得很粗糙结果评委或老板一看界面就给项目判了死刑。点餐系统这种面向 C 端的项目UI 的权重比想象中高得多。我当时的设计思路是整体采用暖色调咖啡色 米白卡片式布局商品图片统一方形裁剪价格用亮色突出。底部固定 tabBar 三个入口菜单、订单、我的。菜单页左侧分类栏、右侧商品列表这种布局是外卖类小程序的标配用户不需要学习成本。商品卡片的“加入购物车”按钮要足够大点击区域不能太小否则用户要精确点击体验很差。加购后购物车 tab 上要有一个角标显示数量这个通过全局状态同步每页 onShow 时刷新。购物车页我做了“左滑删除”的手势操作以及“全选/取消全选”功能这两个交互在点餐场景里很实用。结算按钮固定在底部显示“去结算¥xx.x”点击后进入确认订单页。数量加减时要有简单的动画反馈这些用微信小程序的动画 API 或者简单的 CSS transition 就能实现但很影响手感。确认订单页里自提/外送切换要醒目。自提模式下需要填写取餐人姓名和手机号外送模式需要填写配送地址并显示配送费可以在商品总价上按距离或固定金额加。第一版可以只做自提把外送作为扩展项。一旦做外送就涉及配送范围判断、配送费计算、配送员接单等逻辑精力会急速消耗。订单列表页按状态分类使用 tabs 切换。待支付订单要有醒目的“去支付”按钮且支持再次拉起支付制作中的订单用动画比如饮品制作中的动态图标表示进度已完成的订单有“再来一单”按钮一键把历史订单商品加入购物车这是个高性价比的留存功能。3.5 商家后台与 WebSocket 实时提醒商家后台如果用 Thymeleaf 渲染最简单的方式是集成一个 Vue 的 CDN 模式后端返回的 HTML 页面里嵌 Vue 语法接口走 /api/admin/**。这样做的好处是不需要构建前端工程一个 SpringBoot 应用就能全部搞定。缺点是代码比较难维护但单管理员的场景完全够用。订单列表我用了一个表格重点是操作列接单、完成、查看详情。接单操作触发状态从“已支付”变成“制作中”同时可选填预计完成时间。完成后触发状态变成“已完成”。实时提醒模块我用 WebSocket 把新订单推送到商家端。关键代码Component public class OrderWebSocketHandler extends TextWebSocketHandler { private static final MapString, WebSocketSession SESSIONS new ConcurrentHashMap(); Override public void afterConnectionEstablished(WebSocketSession session) { // 从握手参数中获取商家ID String merchantId session.getAttributes().get(merchantId).toString(); SESSIONS.put(merchantId _ session.getId(), session); } Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { String key session.getAttributes().get(merchantId) _ session.getId(); SESSIONS.remove(key); } public void sendMessage(String merchantId, String message) { SESSIONS.forEach((key, session) - { if (key.startsWith(merchantId _)) { try { session.sendMessage(new TextMessage(message)); } catch (IOException e) { // 发送失败移除失效连接 SESSIONS.remove(key); } } }); } }这里有个坑WebSocketSession 默认是会被 Spring 的拦截器拦截的所以要在 WebSocket 握手阶段把商家ID传入 session 属性。不然你都不知道连接是谁的。在订单支付成功回调里调用 sendMessage 推送给商家同时前端保留5秒轮询兜底。另外要注意 WebSocket 连接没有鉴权隐患握手阶段一定要校验 token不能裸连就接受否则攻击者可以模拟商家接收订单数据。我在拦截器里手动解析了查询参数中的 token校验通过后才能建立连接。4. 常见问题与排查技巧实录4.1 微信登录总是失败code2Session 返回 40029 或 40163这是所有新手都会遇到的问题。40029 的意思是 code 无效最常见原因是同一个 code 被使用了两次。为什么会用两次因为前端在某些情况下会重复调用 wx.login()或者后端并发处理了同一个 code。我排查时发现登录成功后前端还没跳转页面用户又触发了一次登录两个请求几乎同时到达后端其中一个必然失败。解决办法后端在分布式锁或数据库唯一约束层面保证同一个 code 只处理一次我用的最简单方式是代码里对 code 加 synchronized或者用 Redis 的 setnx。前端在登录流程中加标志位登录中不允许再次触发。code2Session 接口调用失败时一定不要直接报错要提示用户重新点击登录按钮或者自动重试一次。40163 表示 code 已被使用过处理思路同上。还有个坑小程序端 app.js 的 globalData 里保存登录状态但如果小程序被杀掉进程重新启动token 还在本地存储里但微信 session 可能已经失效此时登录接口返回的是有效 token但后续接口可能会因为 session 过期出现奇怪的错误。建议每次启动小程序时主动调用一次 wx.checkSession()过期就重新静默登录。4.2 微信支付回调没有触发或重复触发支付回调不触发的常见原因回调地址配置错误必须在小程序后台配置 request 合法域名并且回调地址必须是 HTTPS。你在公众平台配置的域名要和你后端实际使用的回调路径完全一致包括路径前缀。本地联调时回调地址无法访问这个前面说了可以用内网穿透但要注意穿透工具的免费域名可能被微信判定为不安全。回调地址路径写错了微信支付通知路径是你在统一下单接口里传入的 notify_url 参数必须和实际部署路径一致。回调重复触发是正常的。微信支付为了保证通知到达会按照一定间隔重试多次最长几十分钟。所以幂等处理是必须的我使用订单状态判断来实现幂等如果订单已经是“已支付”状态直接返回成功不再走业务逻辑。同时把回调报文记录到 pay_log 表里每个订单多条回调记录方便追溯。还有个常见问题是“支付成功了但订单状态没更新”。排查顺序先看 pay_log 表有没有记录没有就说明回调根本没到有记录就看处理逻辑里有没有抛异常常见的是订单状态更新的 SQL 语句写错或者 redis 分布式锁没释放导致后续请求阻塞。4.3 并发下单导致库存超卖这个问题我前面提过解决方案用条件更新扣库存。但实际测试时发现一个微妙的地方MyBatis-Plus 的 updateById 不会自动加库存条件你需要写自定义 SQL。update iddeductStock UPDATE product SET stock stock - #{count} WHERE id #{productId} AND stock #{count} /update联调时我用 JMeter 模拟 100 个并发请求同时下单同一商品库存设置 10 个最终订单表里只生成了 10 条有效订单其余全部报“库存不足”没有一条是超卖的。如果发现超卖检查两点第一扣库存 SQL 是否真的带了 stock #{count} 条件第二下单接口是否真的加了事务如果没加事务异常回滚只能回滚部分操作库存可能已经扣了但订单没生成就会出现“幽灵库存”。另外一个隐蔽问题订单取消/支付超时后回补库存。回补操作必须放在一个独立的事务方法里并且也要用条件更新UPDATE product SET stock stock #{count} WHERE id #{productId}。不存在“加库存也会失败”的场景但要注意别重复回补否则库存会越补越多。我的办法是取消订单方法里判断状态只有从“待支付”变更为“已取消”时才算一次有效的取消回补库存只执行一次。4.4 小程序体验版环境配置这个环节很多人卡了一天。体验版是给非开发者预览的需要在小程序后台把请求域名配置为 HTTPS 且通过 ICP 备案。如果你的后端只是本地局域网 IP或者一个测试域名没备案小程序端请求会被拦截报“不在以下 request 合法域名列表中”。解决办法开发阶段在开发者工具里勾选“不校验合法域名”这样本地可以跑通。但体验版和正式版是没法绕过这个限制的必须申请正式域名并备案国内服务器要求。域名配置 HTTPS 证书。在小程序后台“开发管理-服务器域名”里配置 request 合法域名和 socket 合法域名。记得把本地的 baseUrl 从 http://localhost 改成正式域名。还有一个坑如果用了 WebSocket还需要在小程序后台配置 socket 合法域名。我当时就是漏了这个导致商家端后台能打开但 WebSocket 一直连接不上排查了半天才发现是域名配置问题。4.5 商家后台 WebSocket 推送不了或连接闪断WebSocket 连接闪断有两种原因一是服务器防火墙没放开 443 端口如果你用的不是标准端口微信小程序强制要求 wss 且使用 443 端口二是会话超时。小程序端 WebSocket 的默认超时时间比较短长时间空闲会被断开需要心跳机制。我在小程序端加了每 30 秒发送一个 ping 心跳后端收到后回一个 pong。如果连续三次没有收到 pong前端主动重连。后端在 WebSocket Handler 里通过定期检查最后通信时间来剔除无效连接。WebSocket 连接数超限问题也遇到过同一个商家开了多个管理页面每个页面建立一条连接这是合理的但如果连接建立后没有正确关闭服务端会积累很多僵尸连接导致新连接无法建立。处理办法是服务端维护 session 时每次新连接到来就把同一用户之前的连接关闭或者定期清理。5. 部署与上线经验补充5.1 后端部署要点后端我部署在云服务器上用的方式是mvn clean package -DskipTests java -jar coffee-shop.jar --spring.profiles.activeprod这里有两个细节。第一配置文件区分开发和生产环境生产环境里的数据库密码、微信 AppSecret 一定要用环境变量注入不能写死在代码里。第二JVM 参数建议加上内存限制防止小内存服务器 OOMjava -Xms256m -Xmx512m -jar coffee-shop.jarNginx 反向代理配置也很重要把 80/443 端口的请求转发到后端 8080 端口。后端接口路径如果是 /api/user/**Nginx 可以加一个 location 规则。HTTPS 证书用免费的就够了申请后配置在 Nginx 里。5.2 数据库备份与安全上线后最怕数据丢。数据库每天凌晨自动备份用 cron 执行 mysqldump备份文件保留7天再同步到对象存储。这个操作用一个 shell 脚本就能完成。数据库连接配置里生产环境不建议使用 root 账号要单独建一个业务账号只授权当前数据库的增删改查权限。用户密码字段要做加盐哈希存储不能明文。接口层面后端统一处理跨域开发时需要真机上小程序不存在跨域问题、参数校验Validated、防 SQL 注入MyBatis 用 #{} 预编译尽量别用 ${} 拼 SQL。小程序端请求频率也要做限制尤其是扫码点餐场景用户多重进入时可能瞬间发起大量请求用拦截器加简单的令牌桶限流就可以。5.3 压测与容量预估小店场景通常同时在线人数不会超过一两百但关键时刻比如早高峰、活动日请求会集中。我用 Apache JMeter 对核心接口做了简单压测模拟 200 个并发用户同时浏览菜单和下单接口平均响应时间 200ms 以内数据库没出现死锁和连接池打满。如果后端接口响应变慢优先排查的是数据库慢查询商品列表的 SQL 没有索引订单列表的联表查询字段没建索引这两个是最常见的瓶颈点。小程序端的图片资源一定要压缩否则首屏加载时间会非常长。商品图建议统一压到 200KB 以下后台可以先压缩再上传。菜单接口如果一次返回全量商品数据量大了之后要做分页或懒加载我同时做了首页只加载前 20 个商品的策略滚动到底部再加载下一页。6. 这套系统还能怎么扩展做完基础版后想加东西的话有几个性价比很高的方向。会员与积分点餐系统非常适合做会员体系。消费 1 元积 1 分积分可以兑换饮品或优惠券。表结构只需要加积分明细表和优惠券表下单时增加积分抵扣逻辑整体改动不算大但对留存率的提升很明显。多店支持如果要把系统卖给多个咖啡店就需要引入“店铺维度”的概念所有表加 shop_id后端增加多租户隔离逻辑。这个改动涉及面很广前端页面、数据查询、权限体系都要改建议在项目初期就预留这个字段哪怕暂时用不到。打印机对接商家端订单实时打印是小店的刚需。通过云打印服务商提供的接口在支付回调中调用打印接口即可实现自动出单。这个扩展能极大提升系统的实际可用性很多小店愿意为这个功能买单。用户评价与复购订单完成后用户可以评价包括评分和文字/图片对商家的选品和品控非常有价值也是社区运营的基础。数据分析商家后台的仪表盘可以深入做用户复购率、时段销量分析、商品关联推荐。数据量小的时候直接用 SQL 就能实现不需要上大数据组件。小程序端的订阅消息用户下单后可以通过微信订阅消息推送“订单已接单”“订单已完成”通知。这比用户反复刷新订单列表更接近真实体验且实现成本很低——在下单时请求用户授权订阅一次后端在状态变更时调接口推送即可。以上扩展建议的优先级我的排序是打印机对接 订阅消息 会员积分 多店支持 数据分析。打印机是真实经营刚需订阅消息是体验刚需这两项练手价值也都很高。7. 写在最后的实操体会做完这套系统后我最深的感受是点餐系统看起来简单但“简单”恰恰是陷阱。订单状态机的严谨性、库存扣减的并发安全、支付回调的幂等处理、微信生态的各类限制这些才是真正决定项目能不能活下来的骨架。如果你只觉得 CRUD 写完了就万事大吉那大概率会在联调阶段被各种隐蔽问题折磨到崩溃。一个很具体的建议从第一天开始就给所有接口设计统一的返回结构、统一的异常处理机制、统一的日志记录。这三个统一看似浪费时间但在联调和排错阶段能节省你三倍以上的时间。尤其是日志我见过太多人排查支付问题时两眼一抹黑根本不知道回调有没有进来、报了什么错。最后分享一个团队协作或者个人多设备开发的技巧把环境配置文件按 dev/prod 分离小程序端把 baseUrl 的配置抽到单独文件里并在代码里加注释标记“上线前必须修改的地方”。我当时就是在这里栽过跟头——用了错误的 baseUrl 在真机上调试白折腾了大半天。这套系统的完整代码量不算大前后端加起来大概几千行但每一个模块都值得认真对待。希望这篇内容能帮你避开我踩过的坑把更多精力放在打磨业务细节上。