ARTICLE DETAIL

资讯详情

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

台球赛事报名系统实战:微信三端+Java后端架构解析

台球赛事报名系统实战:微信三端+Java后端架构解析 去年帮朋友所在球房搭过一套台球赛事比赛报名系统当时的需求很简单直接办一场中式八球比赛线上报名的链接发到各个微信群里选手自己点开填信息、交报名费。本来以为是个一次性的小工具结果做完了发现真正跑起来之后赛事主办方、球房管理员、参赛选手三方都在同一个系统里干活小程序、公众号、H5三个入口缺一不可。这套东西做到后面慢慢沉淀成了一个相对完整的JAVA后端源码工程前端分别覆盖微信小程序、公众号菜单页和外部H5链接。今天把整个技术方案和踩坑过程拆出来聊聊给准备做同类赛事系统、报名系统、或者微信生态多端项目的同学一个可参考的底子。这套系统的核心价值在于它不只是一个报名表单而是一条完整的办赛链路——赛事发布、分组设置、选手报名、在线支付、报名截止后的赛程编排、选手签到、比分录入全部能在线上跑通。它解决的痛点是传统球房办赛时用Excel收报名表、手动确认缴费、赛前一天还在微信群喊人核对名单的混乱状态。适合的人群也比较宽想自己搞赛事SaaS的技术团队、球房/台球俱乐部运营者以及在学习前后端分离和微信生态开发的同学都能从这套系统里找到对应模块的参考实现。1. 一个真实的业务需求和它背后的三端定位逻辑1.1 台球赛事报名到底涉及哪些参与方和流程台球赛事的办赛流程比很多人想象中要复杂。表面上看起来就是发个公告、收报名费、抽签开打但真正落到系统里至少要拆成这几个参与方赛事主理人负责创建赛事、设置比赛项目和分组、确定报名人数上限和报名费球房管理员要录入场地和球桌信息并在比赛当天核验选手签到选手要完成查看赛事、在线报名、缴纳费用、按时到场签到、查看自己的对阵安排这一连串动作如果比赛有裁判或计分员还需要一个快速录入比分的入口。报名环节本身也有很多变化。台球比赛通常会区分中式八球、斯诺克、九球等项目还会按性别、年龄、水平段位分成不同组别。比如一场比赛里可能有中式八球男子公开组和中式八球女子组每个组别单独限报64人各自设置报名费。选手报名时不能只选一个赛事还要选组别填姓名、手机号、所在城市等基本信息。这些字段的灵活度决定了后端数据模型怎么设计也决定了报名流程里那些校验逻辑放哪里。1.2 为什么要三端都做小程序、公众号、H5各干各的活很多第一次做微信生态项目的开发者会问既然有小程序为什么还要公众号和H5我最初也觉得小程序就够了但实际运营下来发现这三个入口面对的是完全不同的用户行为。小程序是选手的主场。台球选手参赛当天需要反复查看赛程、球桌号、对手信息小程序可以放在微信首页下拉列表里随手打开体验最接近原生App。而且小程序有订阅消息能力报名成功通知、赛前提醒、成绩推送都很方便。公众号承担的是信息阵地功能。赛事品牌、球房日常活动、往期比赛照片和战绩文章都沉淀在公众号里。公众号菜单挂上赛事入口粉丝看到近期有比赛点菜单就能进到赛事报名页。这是把公众号读者转化为参赛选手最短的路径。H5则负责传播裂变。一场比赛要人满才热闹选手会把报名链接转发到球友群、朋友圈。H5链接在微信内打开可以直接授权登录无需下载任何东西传播效率比小程序码高得多。三端共用一套后端API业务逻辑完全一致只是前端容器不同。实际的用户路径经常是这样的在公众号里看到赛事推文点菜单或文末链接进入H5页面授权微信登录后报名缴费比赛当天用小程序打开查看赛程签到赛后公众号推送成绩公告。数据贯穿全流程。下面这个表可以比较清晰地看出三端的定位差异端核心用户主要作用登录方式典型入口微信小程序参赛选手查赛程、签到、消息通知小程序code登录微信搜索、下拉任务栏公众号赛事粉丝/球房会员赛事公告、品牌沉淀OAuth2网页授权公众号菜单、文章链接H5潜在选手/转发对象快速传播、报名引流微信内OAuth2授权微信群、朋友圈链接2. 后端选型与赛事核心数据模型设计2.1 技术栈选型Java Spring Boot MyBatis Plus MySQL Redis 为什么够用这套系统后端选择Java不是因为它高级而是因为它在做这类业务系统时综合成本最低。Spring Boot让工程搭建变得很快MyBatis Plus处理单表CRUD几乎不用写SQLMySQL存业务数据Redis做缓存和分布式锁Hutool解决常见的加密、日期、Http工具需求。整个技术栈都是大众技术后续接手维护的人不会一脸懵。工程结构上分成四个模块后端API服务、管理后台前端、小程序端、H5端公众号内使用的页面。后端API服务是整个系统的核心所有端都调它。管理后台我用的是Vue Element Plus用来创建赛事、审核名单、编排赛程、录入成绩。小程序端和H5端各自独立工程但接口调用逻辑完全一致区别只在于微信登录的触发方式不同。项目分包大致是这样的controller统一收口HTTP请求service层做业务逻辑尤其是报名流程中的事务和锁控制mapper层只管数据库交互domain/entity层放表对应的实体类common/config放全局异常、拦截器、微信SDK的配置。这种分包方式虽然老套但非常稳团队协作时每个人动哪一层一目了然。2.2 核心表结构赛事表、报名表、选手表、赛程表数据模型是整个系统的地基。我第一版设计时把选手信息直接塞进了报名表结果同一个选手报了两场不同比赛手机号、姓名到处冗余后面统计对账非常痛苦。重构之后拆成了四张核心表关系清晰很多。赛事表 t_tournament 保存赛事本身的信息包括赛事名称、报名开始时间、报名截止时间、比赛开始时间、赛事状态、赛事封面图、说明文案、总人数上限。组别的信息我放在独立的 t_tournament_group 表里一个赛事可以对应多个组别每个组别单独记录项目类型、报名人数上限、报名费、已报人数。选手表 t_player 存选手的微信身份和基础资料。注意这里要区分微信身份和报名资料微信身份是openid、unionid、头像、昵称报名资料是姓名、手机号、所在城市。为什么区分因为同一个微信用户可能报两场比赛但每场比赛填的手机号可能不一样选手在不同赛事里的报名资料应该归属于具体的报名记录而不是覆盖到选手全局。报名表 t_registration 是关键中的关键。一个字段都不能省主键id、tournament_id、group_id、player_id、选手填报的姓名、手机号、报名状态、支付订单号、支付金额、支付时间、签到状态、签到时间、创建时间。为了防重复报名我在 tournament_id group_id player_id 上建了唯一索引。赛程表 t_schedule 记录对阵关系。字段包括赛事id、组别id、轮次、台桌号、选手A id、选手B id、A分数、B分数、裁判备注、状态。赛程系统让很多做报名系统的团队放在二期但实际上如果不预留这张表等比赛真要编排的时候数据结构根本撑不住。建表SQL可以挑报名表举例大致结构如下CREATE TABLE t_registration ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, tournament_id bigint(20) NOT NULL COMMENT 赛事ID, group_id bigint(20) NOT NULL COMMENT 组别ID, player_id bigint(20) NOT NULL COMMENT 选手ID, contact_name varchar(50) NOT NULL COMMENT 报名填写的姓名, contact_phone varchar(20) NOT NULL COMMENT 报名填写的手机号, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已取消 3已签到, out_trade_no varchar(64) DEFAULT NULL COMMENT 本地支付订单号, wechat_transaction_id varchar(64) DEFAULT NULL COMMENT 微信支付单号, pay_amount decimal(10,2) NOT NULL DEFAULT 0.00, pay_time datetime DEFAULT NULL, checkin_time datetime DEFAULT NULL, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_tournament_group_player (tournament_id,group_id,player_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT赛事报名表;唯一索引这个细节后面章节会详细展开它是整个报名防重体系的最后一道保险。2.3 状态机报名系统最容易被低估的设计点赛事和报名记录都有状态而且状态之间是严格流转的不能随便跳。我第一版没设计好导致出现赛事已经截止报名了还有选手能通过旧的H5链接进到支付页这种低级事故。赛事状态我定义成了五个草稿、报名中、报名截止、进行中、已结束。草稿状态下选手端查不到赛事只有管理员能编辑。报名中状态才允许选手报名和支付。报名截止后不再接受新的报名但在小范围操作上允许管理员手动补录。比赛开始后进入进行中这时赛程表已经有数据了选手端可以查看对阵。全部比赛打完管理员录入最终成绩后赛事状态变更为已结束。报名记录的状态则围绕支付和签到两个动作展开待支付、已支付、已取消、已签到。签到是比赛当天选手到球房前台管理员在后台输入手机号或扫码核销把待签到变为已签到。状态机在代码里不要散落if-else到处判断我用了一个独立的枚举类管理所有合法流转配合一个更新状态的Service方法。这样后续加需求比如允许选手在报名截止前自助退赛只需要在枚举里增加已退赛状态和对应的流转规则不会改得到处是洞。3. 报名链路里的四个关键细节防重、锁量、幂等、回调3.1 重复提交唯一索引兜底台球赛事的报名窗口期通常只有几天热门比赛开放报名后前几分钟的流量非常集中。选手在报名页面点了一下提交报名前端没反应又点了一下前端转圈其实后端已经收到了两条几乎一样的请求。如果没有防重措施就会生成两条报名记录其中一条是多余的。前端的节流和防抖可以做但永远不能只靠前端。选手机号填写、提交按钮置灰、路由跳转期间禁止返回这些都是体验层的防护。真正的兜底必须有两条后端在事务里先查询是否已经存在有效报名记录存在就直接返回您已报名不再创建数据库层面在 tournament_id group_id player_id 上建唯一索引让并发插入时数据库直接拒绝重复数据。有人会问为什么不只靠唯一索引因为用户看到的反馈不应该是数据库报一个DuplicateKey异常。正确做法是先做一次友好查询命中重复就引导用户去查看自己的报名详情没命中再走插入流程万一高并发下两个请求同时通过查询数据库的唯一索引会拦住后插入的那一条捕获到异常后同样转成友好提示。两层防护配合基本万无一失。3.2 热门赛事名额冲突余量扣减必须用原子操作台球赛事一个组别限报64人报名费可能从几十到几百不等。热门赛事开放报名后64个名额可能在几分钟内被抢完。这时候如果使用先查询剩余名额判断大于0再执行插入的逻辑在并发场景下必然超卖。我初版就踩了这个坑两个选手几乎同时提交各自都查到剩余名额是1都认为自己有名额结果一个64人的组别硬生生报了66个人。排查了一天最后把扣减逻辑改成一条原子SQL才解决问题。核心做法是在组别表里维护一个 current_count 字段每次报名不再先查再插而是直接把报名和名额扣减放进同一个事务扣减语句使用条件更新UPDATE t_tournament_group SET current_count current_count 1, update_time NOW() WHERE id #{groupId} AND current_count max_count AND status 1更新影响行数为1说明名额扣减成功才继续插入报名记录影响行数为0说明名额已经满了直接抛出该组别名额已满的异常回滚整个事务。这样在MySQL的InnoDB引擎下通过行锁天然保证了同一时刻只有一个事务能成功扣减同一个组别的名额。加了这一步之后我再做了个压测模拟100个并发同时报一个仅剩3个名额的组别最终数据库里只有3条报名记录成功其余全部返回名额已满。这才算过了门槛。3.3 微信支付回调必须做幂等处理报名链路里的支付环节最容易出问题的不是支付本身而是支付结果通知的处理。微信支付在用户支付成功后会异步回调你的接口通知支付结果这个回调是有重试机制的如果接口没有返回正确的成功响应微信会隔一段时间再调一次最长可能连续通知好几天。很多人第一次做支付回调代码写成了收到回调就更新订单状态结果同一个订单被回调两次状态被覆盖甚至出现重复发放入场资格的情况。正确的做法是先用订单号查询本地订单如果订单已经是已支付状态直接告诉微信支付我已经处理过了不用再重试不再走任何更新逻辑只有当订单还是待支付状态时才执行状态更新和后续的报名确认。另外回调接口要放在独立的Controller里不要和业务接口耦合在一起。收到回调后第一步验签确认确实是微信支付发的第二步比对金额回调里的金额必须和本地订单金额完全一致防止金额不一致的单子通过第三步做幂等判断和状态更新第四步返回成功应答。这一步的返回值一定不能随意拼微信支付要求特定格式我初期用过一段时间的自定义JSON返回结果微信不认一直重试后来才改回标准格式。3.4 报名时间窗口和组别资格校验状态机只管了整体具体到每一次报名请求还要校验几个业务规则当前时间是否在报名开放窗口内时间未到不能提前报名时间截止不能补报组别是否存在且属于当前赛事组别是否允许当前选手报名比如女子组就应该校验选手性别若有报名费支付金额是否等于组别设定金额。这些校验写在Service层的入口处统一使用一个报名参数对象不要散落在Controller里。为了给选手更清楚的提示我把提示语做得比较具体比如当前组别报名人数已满和该组别报名尚未开始是两条不同的异常信息而不是笼统返回报名失败。这一步对运营方非常重要因为选手看不到后台配置只能靠报错文案理解自己为什么报不了名。4. 微信生态接入三端用户身份怎么统一4.1 小程序登录从code到openid和session_key小程序端登录标准的流程是调 wx.login 拿到一个临时code把它传给后端后端用code向微信接口换取用户在小程序体系下的唯一标识openid和临时会话密钥session_key。openid才是真正用来识别用户的字段code五分钟就失效session_key永远不能下发到前端。后端拿到openid后先去玩家表查这个openid是否已存在。存在就直接登录生成一个自己的token返回给小程序不存在就先创建一条新的玩家记录再登录。token的生成我用了UUID缓存到Redis里设置了7天过期时间小程序每次请求都在请求头带上token后端通过拦截器统一解析。这个设计的核心点在于虽然微信身份是openid但业务系统内部的用户主键必须是自己生成的id所有业务表关联都用这个idopenid只是登录凭证。这样以后如果扩展到抖音小程序、支付宝小程序只需要再维护一套第三方平台openid的映射表业务代码完全不用改。4.2 公众号和H5的网页授权登录公众号里的H5页面因为运行在微信内置浏览器里没有小程序那种wx.login能力走的是另一套OAuth2流程页面跳转到微信的授权地址用户确认授权后微信带你跳转回你的业务页面并带一个code参数后端拿这个code换openid和access_token。这里有一个很多新手会卡住的点网页授权的回调地址必须提前在公众号后台配置到IP白名单和网页授权域名里而且redirect_uri必须做URL编码。实际开发时本地调试就得配一个环境域名去联调我一般用内网穿透把本地服务暴露出去再放到授权回调地址里调试完再切到正式环境。网页授权的scope分两种。静默授权只能拿到openid用户无感知非静默授权可以拿用户头像昵称但会弹出授权确认页。在报名场景里我建议优先走静默授权因为用户点开报名链接的目标很明确多一步弹窗就多流失一个选手。头像昵称可以通过用户后续主动完善资料时再获取。4.3 unionid打通小程序和公众号的同一用户整套系统最容易忽略的技术细节就是把小程序用户和公众号用户识别成同一个人。原因很简单同一个微信用户在小程序里的openid和在公众号里的openid完全不同如果只靠openid做关联就会出现一个用户在小程序报了名管理后台看到的是两个不同的人。解决方案是unionid机制前提是小程序和公众号必须绑定在同一个微信开放平台账号下。绑定好之后同一个微信用户在小程序登录和公众号授权时后端都能拿到同一个unionid。用户表里我专门加了 unionid 字段处理逻辑是登录时先按unionid查询用户查不到再按openid查询再查不到就创建新用户。这样用户不管先从小程序进来还是先从公众号H5进来最终都会落到同一条玩家记录上。这个整合动作务必要在项目刚开始时就做。我吃过这个亏系统上线半个月后老板发现小程序名单里的人和公众号名单里的人是同一拨但后台数据分成两份做数据迁移时花了整整一天。如果项目启动时就设计了unionid字段这个悲剧完全可以避免。没有把小程序和公众号绑定到同一个开放平台账号的情况下后续用户合并非常痛苦。4.4 手机号获取小程序的便利和H5的取舍报名必须要手机号这是赛场联系选手、核对身份的重要字段。小程序端可以引导用户点击一个授权手机号的按钮配合后端接口换取真实手机号用户几乎零操作。需要注意小程序的手机号换取接口在2023年之后改成了动态令牌模式前端拿到的是code后端要用code去微信接口换取手机号而不是直接把手机号传给后端。H5端没有这个能力只能让用户手动输入手机号。这也是为什么H5报名页要把手机号输入框做成必填并且做好格式校验。一个用户通过H5授权登录填好手机号报名成功下次他用小程序进入系统就检测到他unionid下已经有手机号了直接跳过填写步骤省去重复输入。用户信息的补全策略最终是这样的登录以微信身份为基础手机号、姓名、性别等赛事必填信息通过报名时补齐的方式收集。每场比赛的报名表单字段可以灵活配置但模版必须合理太长的表单在手机上会明显降低报名转化率。5. 实测下来最值得注意的五个坑5.1 域名白名单、业务域名和本地联调微信生态的开发最烦人的不是写代码而是各种域名配置。小程序端所有请求的URL都必须在小程序后台配置成request合法域名否则真机上请求直接失败报不在以下合法域名列表中。H5页面如果要在公众号菜单里使用还要配业务域名如果H5里要用微信JS-SDK还要在公众号后台配JS接口安全域名。我在联调阶段吃过最大的亏是本地起了一个后端服务觉得反正就测试一下域名配置随便写写结果小程序真机预览死活连不上接口排查了一下午才发现是request合法域名列表里写的是http地址而微信正式环境强制要求https。后来我学乖了开发和测试阶段统一走内网穿透工具把本地接口暴露成https再配置到一个正式预留的子域名上开发调试体验顺滑很多。5.2 两个端同时登录导致的token冲突H5端和小程序端虽然调的是同一套后端接口但各自的前端状态管理机制完全不同。小程序把token存在storage里H5把token存在localStorage里。如果用户先在小程序登录又去公众号里打开了H5页面是两个独立的token后端需要能够同时接受它们。问题出在我的登录态设计上最开始用openid作为token的valueH5登录后把openid写入localStorage小程序登录后也把同一个openid写入storage。看起来没问题但实际运行中发现两端同时活跃时后登录的一端会把先登录那端的token覆盖掉因为token的过期时间在Redis里是同一个key。排查了两天最后把token设计成每次登录生成一段独立的随机字符串并且用户表里允许多个有效token并存不同端互不影响。这个坑说明一个问题微信生态多端项目用户身份是同一个人但登录态必须是多端独立。身份要统一token要隔离。5.3 服务器时间、时区和赛事状态切换赛事状态机的切换不能依赖选手端请求时去判断更不能依赖前端去定时刷新。我在初版用过一个简单的思路每次请求赛事详情时比较服务器当前时间和报名截止时间然后动态判断状态。这个思路有个隐患如果一整天没有选手访问赛事状态的变更实际没有发生后台管理员的列表里也不会看到状态变化。后期改成了两个手段结合查询时动态判断同时加一个定时任务每分钟扫描一次所有赛事把到达报名截止时间的赛事状态落地到数据库。这里还要注意服务器时区问题数据库存的datetime如果和服务器当前时间处于不同时区定时任务就会出现明明已经到截止时间还没触发的诡异现象。统一把JVM时区设置成Asia/ShanghaiMySQL连接串加上serverTimezoneAsia/Shanghai才能保证时间判断一致。5.4 H5页面在微信内置浏览器里的表现差异H5端虽然开发起来和小程序比更自由但在微信内置浏览器里还是要小心几个问题比如安卓微信浏览器的返回行为会和普通浏览器不太一样用户会习惯点左上角的返回你最好在页面里自己管理好路由跳转栈再比如原生input在部分Android机型上会弹出位置错乱的键盘导致页面排版抖动报名表单里的手机号输入框要对这种情况做滚动处理。还有一个小技巧H5页面里的微信支付用的是JS-SDK必须先注入配置注册wx.ready回调再发起支付。而且支付签名用的参数和普通接口签名不一样容易多一个bug。踩过一次点击支付无反应的坑后来把支付前后的日志都打全了才发现是后端生成签名时漏了一个字段拉取配置和发起支付走的还是两个不同接口前后端排查了好久。5.5 赛程编排的数据结构没有提前预留原本我以为赛程编排可以在报名截止后手动在后台添加结果发现64人的组别小组循环赛加淘汰赛需要生成的对阵记录有上百条手工根本排不过来。后来补做了一个简单的编排模块小组循环赛按每轮轮空一位选手的方式生成对阵淘汰赛按1号对64号、2号对63号的蛇形排列方式生成首轮对阵。这里不绕开算法细节但核心是赛程表的结构一定在设计初期就预留好不要在办赛前一天才临时加表。由于选手可能弃权实际到场人数小于报名人数编排模块还要支持赛前自动剔除未签到选手并重新生成对阵。开始觉得这个逻辑简单落地时发现涉及到轮次重排、对手映射复杂度不低。这套系统的1.0版本里实际是允许管理员手动调整对阵的自动重排放到2.0再做MVP阶段先把最核心的报名链路跑通。做完整套系统再回头看你会发现台球赛事比赛报名系统的难点不在某一个功能有多花哨而在于各个环节之间如何自然衔接。报名状态、支付状态、签到状态、赛程状态每一条都像是一条时间线上的刻度错一步后面全乱。如果重新做一遍我会把报名截止后的自动编排和消息通知设计得更早而不是等选手在群里问我的对手是谁才想起来补。小程序、公众号、H5三端表面上是三个入口底层其实是一套代码、一套数据、一套规则只要后端边界清楚加端只是工作量问题不是技术问题。
返回列表