ARTICLE DETAIL

资讯详情

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

Spring Boot+微信小程序构建游戏陪玩撮合系统:从订单状态机到微信支付实战

Spring Boot+微信小程序构建游戏陪玩撮合系统:从订单状态机到微信支付实战 游戏陪玩、互动约玩平台、电竞陪游撮合系统这三个名字放在同一个毕设题目下面本质说的是同一件事搭建一个基于 Java 后端的微信小程序让玩家能发布求陪需求陪玩师能展示自己的游戏段位、服务项目和收费标准平台负责撮合双方并把订单、支付、沟通、评价这条链路跑通。我去年完整做过这个方向的毕业设计项目从需求分析到数据库设计、核心功能实现、线上部署再到答辩演示踩了不少坑也积累了一些能直接复用的经验。这篇博文就把整个项目的拆解过程、技术选型逻辑和实操细节一次说清楚不管你是准备拿它当毕设选题还是想练手做一个完整的微信小程序全栈项目都能少走弯路。1. 陪玩系统解决什么问题业务逻辑先于技术选型1.1 三个叫法指向同一类撮合系统很多同学拿到这个题目会有点懵因为游戏陪玩系统、互动约玩平台、电竞陪游撮合系统听起来像三个方向。其实不用被名字带偏它们都聚焦同一个业务内核双边撮合。一端是希望有人陪着打游戏、聊天解闷、上分带飞的玩家另一端是具备一定游戏水平、可以提供陪伴服务的陪玩师。系统要做的事情就是让两端的信息透明化让匹配、下单、支付、履约、评价形成闭环。这类系统天然适合作为毕设原因是它的功能边界非常清晰但又足够复杂到能体现工程能力。用户模块、陪玩师认证模块、游戏分类模块、订单模块、支付回调、IM聊天、评价体系、钱包流水随便挑两个都能写出几千行代码。更关键的是它的交互逻辑可以完全照搬真实业务场景答辩时你不需要解释为什么做这个系统因为需求本身就是成立的。1.2 核心角色与业务流转我实现这个项目时把业务模型拆成了三种角色、六条核心流程。三种角色分别是普通玩家下单方、陪玩师接单方、平台管理员审核与运营。部分系统还会再拆出一个经纪人角色但从毕设体量来说三类已经够用。六条核心流程如下玩家下单流程浏览陪玩师列表 → 查看陪玩师详情游戏、价格、段位、评价 → 选择服务时长和服务类型语音陪玩、视频陪玩等 → 提交订单 → 微信支付。陪玩师接单流程陪玩师端收到新的可接订单通知订阅消息或站内提醒 → 抢单/接单 → 开始服务 → 服务完成后确认完结。订单履约与结算流程订单从待支付流转到待接单、服务中、待确认、已完成平台在订单完成后进行分账陪玩师账户入账玩家可申请退款。沟通聊天流程下单前玩家和陪玩师可以通过 IM 私聊确认细节系统记录聊天消息并做安全校验。评价流程订单完成后双方互评评分实时影响陪玩师的星级和列表排序。管理员审核流程陪玩师提交认证资料游戏段位截图、自我介绍等管理员审核通过后才能接单。为什么强调业务流转要先理清因为后面的数据库表设计、状态机定义、接口文档全都要从这里推导出来。我见过不少同学拿到题目就急着写代码结果写到订单状态时反复改表就是因为流程没定清楚。1.3 这个项目对你能力图谱的补充价值做这个系统本质上是在练一套通用的交易IM架构能力。即便你以后不从事游戏行业订单状态机、并发扣减、支付回调幂等、WebSocket 连接管理这些经验放到电商、本地生活、社交平台上全部通用。所以答辩时千万不要把它局限在游戏陪玩这个场景里要往抽象业务能力上靠这也是评分老师最看重的东西。2. 技术选型Java 后端与微信小程序的配合逻辑2.1 后端框架与核心组件选型后端我采用的是Spring Boot 2.7.x MyBatis-Plus MySQL 8 Redis WebSocket 微信支付 v3这套组合前端使用微信小程序原生开发。先解释几个关键选择背后的理由Spring Boot 而非 SSM/Spring MVC 手写配置毕设周期有限Spring Boot 的自动配置能把环境搭建时间压缩到一个小时内同时内嵌 Tomcat部署时一个 jar 包搞定。这不是偷懒Spring Boot 本身也是目前中小型项目的行业主流答辩时不会有任何减分。MyBatis-Plus 而非 JPA/HibernateJPA 上手快但动态 SQL、复杂联查写起来很别扭MyBatis 原生 XML 写起来繁琐。MyBatis-Plus 在自动 CRUD 上比 JPA 还省代码又保留了 MyBatis 的 SQL 掌控力对订单、统计这类需要自定义 SQL 的场景非常友好。Redis 承担三类职责陪玩师热门排行的实时排序基于 Score 的 Sort Set、支付回调与下单场景的分布式锁、在线 IM 用户的 Session 管理。Redis 在这里不是堆技术而是真的每个场景都有硬需求。WebSocket 处理实时聊天小程序的聊天场景不适合用 HTTP 轮询服务端要能主动推送消息给在线用户。Spring Boot 内置的 WebSocket 支持足够应付毕设规模不需要引入 Netty避免过度设计。技术选型对比可以参考这张表维度我的方案备选方案取舍结论后端框架Spring Boot 2.7SSM、Spring Boot 3.x2.7 成熟稳定微信支付 SDK 兼容性最好ORMMyBatis-PlusJPA、MyBatis兼顾开发效率与 SQL 可控性缓存与锁Redis 6.x无/本地锁排序、锁、Session 三合一复用实时消息Spring WebSocketNetty、第三方 IM SDK毕设规模用 WebSocket 足够第三方 SDK 反而难体现工作量前端微信小程序原生uni-app原生调试最直接分享、支付、订阅消息 API 完整部署2核4G云服务器 Nginx本地演示线上部署能证明工程能力答辩加分2.2 项目管理与数据库初始化准备项目结构上我推荐直接拆成两个目录backendSpring Boot 工程和miniprogram微信小程序前端。后端按常见的分层结构走controller、service、mapper、entity、common、config 各司其职。数据库初始化建议大家直接用 SQL 脚本管理不要用 Navicat 手动建表。把建表语句、初始数据比如游戏分类、管理员账号写进init.sql后端配置里用spring.sql.init.modealways在开发环境自动执行。这样换电脑、答辩演示、部署服务器时数据库重建只需要一条命令而且老师问数据库设计文档在哪的时候你直接把 SQL 文件拿出来就行。前端部分注意一点微信开发者工具里创建项目时选择不使用云开发因为我们后端是独立的 Java 服务接口走 HTTP 请求不需要云开发那套数据库和云函数避免架构混淆。3. 数据库与订单状态机撮合系统的基石设计3.1 核心表结构设计与关联关系我设计数据库时一共落地了 8 张核心表这里挑最重要的几张开列一下。用户表 user主键 idunionid/openidnicknameavatarphonegenderrole0 玩家1 陪玩师2 管理员balance 余额real_name_status是否实名认证等字段。注意 openid 要建唯一索引微信支付的退款和消息推送都依赖它。陪玩师信息表 caster_infouser_id 关联用户表game_id 关联游戏表intro自我介绍skill_tags擅长英雄/玩法标签price_per_hour时薪单位用分存储service_type语音/视频等mining_status接单状态空闲/忙碌rating 评分order_count 接单数。这张表本质上是用户表的扩展信息表为什么要单独拆因为陪玩师认证是后置动作不是所有注册用户都会成为陪玩师拆开后普通用户查询时不需要 JOIN 这张表。游戏分类表 gameidnameiconcategoryhot 热度值。热度值在陪玩师列表排序时会用到后台管理员可以调整。订单表 game_orderidorder_no 订单号业务上展示用user_id 下单玩家caster_id 接单陪玩师game_idservice_typehours 下单时长amount 订单金额分status 订单状态后面单独讲pay_time 支付时间start_time/end_time 服务开始结束时间remark 备注。订单号要保证全局唯一可以用雪花算法生成。评价表 reviewidorder_id 关联订单user_idcaster_idscore1-5 分content 文字评价imgs 图片可选。一个订单只能评价一次所以 order_id 要建唯一索引。钱包流水表 wallet_flowiduser_idtype 流水类型支付、收入、退款、提现amountbalance_before 变动前余额balance_after 变动后余额related_order 关联订单号。这张表很重要它是审计对账的依据也是回答平台的钱怎么流转这个问题最直观的证据。聊天记录表 im_messageidfrom_userto_usermsg_typetext/image/systemcontentsend_timeis_read。消息记录落库是为了历史可追溯上线运行后这条表会越来越大毕设阶段不需要分库分表但可以在设计文档里提一句预留分表方案面试时是加分项。表之间的关联关系一句话概括用户表和陪玩师表是一对一扩展订单表同时关联玩家和陪玩师评价表关联订单和双方钱包流水表关联用户和订单。用图表示会比较直观但这里不展开了核心原则是订单表是中心表几乎所有业务流转都要经过它。3.2 订单状态机的设计思路订单状态是整个系统最容易写砸的地方。一开始我也偷懒直接在代码里用 if else 判断状态结果加上取消、退款、超时之后逻辑彻底乱套。后来老老实实把状态机画出来所有状态流转只允许走合法路径。我采用的订单状态定义状态值状态名称进入条件可流转目标0WAIT_PAY 待支付用户提交订单1 待接单、5 已取消1WAIT_ACCEPT 待接单支付成功2 服务中、4 申请退款、6 已退款2SERVING 服务中陪玩师接单3 待确认3WAIT_CONFIRM 待确认陪玩师确认服务完成4 已完成、4 申请评价4FINISHED 已完成玩家确认/超时自动确认无终点5CANCELED 已取消用户支付前取消/超时未支付无终点6REFUNDED 已退款待接单阶段退款/取消无终点状态流转用枚举 状态机校验表实现而不要在 service 里散落写死状态变化。我贴一段核心代码public enum OrderStatus { WAIT_PAY(0, 待支付), WAIT_ACCEPT(1, 待接单), SERVING(2, 服务中), WAIT_CONFIRM(3, 待确认), FINISHED(4, 已完成), CANCELED(5, 已取消), REFUNDED(6, 已退款); private final int value; private final String desc; OrderStatus(int value, String desc) { this.value value; this.desc desc; } // 状态是否允许从 from 流转到 to public static boolean canTransit(OrderStatus from, OrderStatus to) { MapOrderStatus, SetOrderStatus transitions new HashMap(); transitions.put(WAIT_PAY, new HashSet(Arrays.asList(WAIT_ACCEPT, CANCELED))); transitions.put(WAIT_ACCEPT, new HashSet(Arrays.asList(SERVING, REFUNDED, CANCELED))); transitions.put(SERVING, new HashSet(Collections.singletonList(WAIT_CONFIRM))); transitions.put(WAIT_CONFIRM, new HashSet(Arrays.asList(FINISHED, REFUNDED))); return transitions.getOrDefault(from, Collections.emptySet()).contains(to); } }为什么状态机这么重要因为订单状态涉及钱绝对不能出现已完成的订单还能取消退款、待支付的订单直接跳到服务中这种非法路径。用枚举集中管理状态流转后后续增加新状态比如申诉中只需要改这一处整个系统其他地方不用动。3.3 列表查询与分页加载更多的实现热搜词里出现微信小程序页面列表加载更多这正好是陪玩师列表和订单列表都要用的功能。小程序端我用的常规方案onReachBottom触底时请求下一页。小程序代码示例如下Page({ data: { list: [], page: 1, pageSize: 10, loading: false, finished: false }, onReachBottom() { if (this.data.loading || this.data.finished) return; this.loadMore(); }, loadMore() { this.setData({ loading: true }); wx.request({ url: ${API_BASE}/api/caster/list, data: { page: this.data.page, pageSize: this.data.pageSize }, success: (res) { const rows res.data.data.rows; this.setData({ list: this.data.list.concat(rows), page: this.data.page 1, finished: rows.length this.data.pageSize }); }, complete: () this.setData({ loading: false }) }); } });后端如果用LIMIT offset, size数据量小没问题但订单表到几万条之后深分页性能会明显下降。毕设阶段可以不做优化但你得能在答辩时说出我知道深分页的问题后续可以换成基于游标的分页这种话。我的项目实际上用的就是游标分页WHERE id #{lastId} ORDER BY id DESC LIMIT #{pageSize}接口入参从 page 改成 lastId小程序端每次保存最后一条记录的 id 传给后端。代价是前端逻辑稍复杂一点但响应速度稳定不会有 offset 越大越慢的问题。4. 撮合、聊天与支付三大核心功能的实现要点4.1 撮合与抢单用条件更新保证不超卖陪玩系统的核心是撮合撮合有两种实现模式推荐派单和抢单。我采用的是玩家指定心动陪玩师 陪玩师抢单结合的方式。先说玩家侧陪玩师列表按照评分、接单量和热度排序Redis 里用 Sort Set 保存caster:hotscore 为热度值页面请求时通过ZREVRANGE取前 N 名配合数据库查询详情。不需要把所有陪玩师都放 Redis只放 top 100 的排序缓存就够了热点数据永远只有头部那部分。重点是抢单的并发控制。想象一个场景一位高分陪玩师开放接单后同一秒内来了几十个玩家下单这些订单都进入待接单池陪玩师在客户端疯狂点抢单最后一个订单只应该被一个人抢成功。如果代码写成先查订单状态再更新// 错误示范先查后改存在并发问题 Order order orderMapper.selectById(orderId); if (order.getStatus() 1) { order.setCasterId(casterId); order.setStatus(2); orderMapper.updateById(order); }两个请求同时查到 status1会同时执行更新最后订单被两个人同时接单直接出事故。正确做法是使用原子条件更新把判断和修改合并成一条 SQL// 正确示范条件更新利用数据库行锁保证原子性 int rows orderMapper.acceptOrder(orderId, casterId, expectedStatus, newStatus, now); if (rows 1) { // 抢单成功继续后续逻辑 } else { // 抢单失败说明状态已被其他陪玩师改掉 }对应 Mapper 里的 SQL 大致是UPDATE game_order SET caster_id #{casterId}, status 2, accept_time NOW() WHERE id #{orderId} AND status 1 AND caster_id IS NULL受影响行数为 1 才代表抢单成功。这里我的经验是优先用数据库的条件更新实现业务锁而不是 Redis 分布式锁。原因很简单这条 SQL 本身就具备原子性数据库行锁天然保证了并发安全引入 Redis 锁反而多了一层不一致的风险。只有在跨表事务、或者需要锁很久的场景比如陪玩师批量设置价格再用 Redisson 分布式锁。4.2 WebSocket 实时聊天与离线消息聊天模块我使用的是 Spring Boot 内置 WebSocket握手时通过 JWT Token 鉴权连接建立后服务端用 ConcurrentHashMap 维护userId - WebSocketSession的映射。基本流程玩家点击联系陪玩师前端先请求后端拉取最近聊天记录。建立 WebSocket 连接发送消息时前端把消息同时显示在界面并提交服务端。服务端收到消息先做内容安全校验然后查目标用户是否在线。在线则直接推送不在线则把消息写入 Redis List等用户上线时拉取并落库到 im_message 表。这里有一个非常容易忽略的点微信小程序使用 WebSocket 时的合法域名要求。小程序的wx.connectSocket必须连接 wss 协议而且域名需要在小程序后台配置为 socket 合法域名。如果后端只配置了 http 的 HTTPS 证书没有为 WebSocket 单独配置 wss 转发真机上根本连不上。解决方案一是让后端服务同时暴露 443 端口并支持升级请求二是通过 Nginx 配置反向代理 WebSocketmap $http_upgrade $connection_upgrade { default upgrade; close; } server { listen 443 ssl; location /wss { proxy_pass http://127.0.0.1:8080/ws; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; } }这样小程序端连接wss://yourdomain.com/wss就能正常通信。这个坑我调了整整一个晚上后来用代理配置解决记忆非常深刻。4.3 微信支付统一下单、回调幂等与退款支付是这类系统的重头戏。小程序的支付链路是这样的玩家在小程序端点立即支付后端收到请求后去微信支付服务端调用 JSAPI 下单接口拿到prepay_id。后端返回给小程序端一堆参数时间戳、nonceStr、package、signType、paySign。小程序调用wx.requestPayment拉起支付面板。用户完成支付后微信支付服务器异步回调后端接口/api/pay/callback通知结果。这里最大的坑是回调必须幂等。微信官方明确说了回调可能会多次发送而且时序不保证。如果不做幂等处理极端情况下一笔订单会被回调成功两次给陪玩师入账两笔钱。回调处理核心逻辑public String payCallback(PayCallbackVO callback) { // 1. 验签确保请求来自微信服务器 boolean signatureValid wechatPayService.verifySignature(callback); if (!signatureValid) { return failResponse(); } // 2. 根据 out_trade_no商户订单号查本地订单 Order order orderMapper.findByOrderNo(callback.getOutTradeNo()); if (order null) { return failResponse(); } // 3. 关键如果订单已经是“待接单”或后续状态说明之前已经处理过该回调 if (order.getStatus() OrderStatus.WAIT_ACCEPT.getValue()) { return successResponse(); // 直接返回成功不再重复处理 } // 4. 用条件更新把订单从“待支付”改为“待接单” int rows orderMapper.paySuccess(order.getId()); if (rows 1) { // 5. 生成钱包流水入账金额按平台分成比例处理 walletService.income(order); // 6. 通过订阅消息通知陪玩师有新的可接订单 } return successResponse(); }再强调一遍不要让回调里的业务逻辑自己判断是不是重复而是用订单当前状态来判断。状态是幂等的天然护栏状态一旦往前走了后面的重复回调直接忽略。支付之外还要考虑退款。玩家在待接单状态可以申请取消并退款退款接口要双向证书毕设阶段如果无法申请微信商户号一般用沙箱模拟或后台手动处理。我当时写了一套模拟支付开关开发阶段所有支付逻辑跑通后再切到真实的微信支付配置这个设计可以写进项目说明里能体现工程分层意识。5. 毕业设计阶段最容易踩的五个坑5.1 微信获取用户信息接口的调整如果你现在打开同类型的旧教程还会看到很多项目用wx.getUserInfo拿头像昵称。但微信在 2021 年之后调整了接口能力wx.getUserInfo返回的昵称是微信用户头像也是灰色默认图。正确做法是使用头像昵称填写能力在小程序页面上让用户手动选择头像、填写昵称或者使用button的open-typechooseAvatar和nicknameinput 组件。我的实现是注册页面放一个头像选择组件和昵称输入框用户填写后提交给后端后端保存到 user 表。这样既不依赖微信授权也符合最新规范答辩时讲到这一点会很加分。5.2 手机号获取个人主体开发者的现实困境wx.getPhoneNumber组件可以快速获取用户手机号但前提是小程序已完成企业主体认证。个人开发者的小程序根本没有这个权限。毕设阶段的处理方式开发环境使用内置测试手机号或让用户手动输入手机号并做短信验证码校验短信服务需要购买。我当时是用户表保留 phone 字段开发时前端传 mock 手机号线上演示时用测试号设计文档里注明正式上线接入 getPhoneNumber 组件。这个点要写清楚因为很多同学卡在这里大半夜以为是自己代码写错了其实是权限不够。5.3 真机调试与 Charles 抓包排查开发时可以关闭开发者工具里的不校验合法域名选项来调试但一旦到真机预览所有请求域名必须是配置过的合法域名并且是 HTTPS。这个环节最常见的报错是request:fail原因百分之八十是域名没配置或证书有问题。排查这类问题时我习惯用 Charles 抓包看具体请求和响应。给手机配置好代理并安装信任证书后小程序发起的 HTTPS 请求可以在 Charles 中直观看到状态码、返回体、参数是否异常。注意这个做法仅限你自己开发调试环境的自测使用正式环境千万别用代理方式处理真实用户数据。大部分前后端联调问题其实先看微信开发者工具的 Network 面板就能定位Charles 更多是排查真机环境下 HTTPS 证书和网络链路问题。5.4 并发抢单之外的超时关闭抢单场景除了并发冲突还有超时问题。用户下单支付成功后如果一直没人接单订单不能永远挂在待接单状态。我做了个定时任务每 30 秒扫描一次超过 15 分钟仍未被接单的订单自动进入退款流程。注意扫描订单要用WHERE status 1 AND create_time NOW() - INTERVAL 15 MINUTE利用索引查出来再批量处理不要一次性全表扫描所有状态。5.5 数据一致性支付回调、钱包流水与真实入账支付回调一旦成功订单更新、钱包流水插入、陪玩师账户余额增加这三件事必须保证一致性。毕设可以不加分布式事务用本地事务加上按订单号去重就足够了。我在wallet_flow表上对 related_order 建了唯一索引重复调用时插入会直接报错事务回滚从而兜底整个流程不产生重复入账。这个设计成本很低但能体面地回答老师关于系统如何保证数据一致性的提问。6. 测试、部署与答辩让项目真正落地的最后一公里6.1 接口测试用 Postman 建立请求集项目开发期间接口测试不是可选项。我的习惯是用 Postman 建立一套完整的请求集覆盖每个模块的 CRUD 和核心流程。特别推荐 Postman 的环境变量功能把baseURL、token设为环境变量登录接口通过脚本自动把 token 写进变量后续接口直接引用调试效率能提高一半以上。测试的重心放在订单状态机的流转上下单 → 模拟支付回调 → 抢单 → 确认服务 → 评价每一步都验证订单状态是否符合预期。把状态机的非法流转路径也测一遍比如对已完成订单发起退款预期是接口返回失败如果返回成功说明状态机校验有漏洞。6.2 部署到云服务器的完整操作部署这部分我强烈建议做完线上能跑通和本地能跑通完全是两个概念。服务器配置建议 2 核 4G 起步操作系统选 Linux。部署步骤大致如下安装 JDK 17、MySQL 8、Redis 6、Nginx。把后端代码打成 jar 包mvn clean package -DskipTests。上传 jar 包到服务器用nohup java -jar app.jar --spring.profiles.activeprod后台启动。配置 Nginx 反向代理/api前缀的请求转发到 8080 端口同时为/wss配置 WebSocket 升级。到域名服务商解析一个二级域名申请免费的 SSL 证书并配置 HTTPS。在微信小程序后台配置服务器域名request 合法域名、socket 合法域名把开发环境的不校验合法域名关掉用真实域名测试。注意数据库的init.sql要在服务器上执行一遍而且生产环境不要开启spring.sql.init.modealways避免每次启动都重复执行脚本。6.3 答辩与项目演示的实战技巧答辩演示最好按照真实用户路径走注册登录 → 浏览陪玩师列表 → 查看详情 → 发起聊天 → 下单支付 → 模拟接单 → 服务中 → 确认完成 → 评价 → 钱包余额变化。一个流程走下来系统的所有模块基本都覆盖了比零散演示功能更有说服力。老师大概率会问的问题提前准备好答案为什么用 Redis Sort Set 做排行答热更新、支持范围查询、内存计算快数据库写压力小。如何防止并发抢单超卖答数据库条件更新原子操作状态字段作为天然乐观锁。支付回调重复怎么办答订单状态机作为幂等屏障钱包流水唯一索引兜底。聊天消息如何保证必达答在线走 WebSocket 直推离线写入 Redis List上线后拉取并落库。长时间没人接单怎么办答定时任务扫描并自动退款状态机保证流程闭环。6.4 演示环境准备清单最后提醒一下答辩前的环境检查。演示那天最大的杀手是网络和真机适配准备一部测试手机确保能联网提前真机预览过一次完整流程。后端服务和 Redis 提前启动好脚本写好一张纸遇到数据库挂了能快速重启。微信开发者工具打开时把模拟器和真机都跑一遍因为模拟器支付有时调不起来真机上反而顺畅。如果现场没有外网条件准备一个轻量级的本地演示方案所有接口请求打到本地后端避免域名过期或服务器出问题导致全场尴尬。这个项目做下来我最大的体会是所谓陪玩撮合系统表面上是游戏领域的小众需求内核其实是标准的订单、支付、聊天、评价交易系统。把状态机设计清楚把竞态条件控制住把支付回调做成幂等的你就已经具备了独立搭建一个中小型业务系统的能力。答辩的时候别把这个项目当成游戏陪玩的外包 demo而是把它定位成一个可复用的撮合交易平台原型这分量完全不一样。最后分享一个我踩过几次坑之后才养成的习惯每次改动订单状态相关的代码先问自己一句状态机允许这条流转路径吗。这个小习惯帮我在开发后期避免了好几次线上事故。希望你做完这个系统后也能带着这套思维去面对下一个项目。
返回列表