1. 这个项目到底解决什么问题
马拉松报名及成绩管理系统,是典型的赛事数字化基础工程。国内每年大大小小的马拉松赛事数量庞大,从城市级的大型赛事到企业、学校组织的迷你跑,每一场都逃不开三个核心痛点:报名信息混乱、名额管控失控、成绩统计耗时耗力。
很多人觉得这不就是一个CRUD吗?选手填表报名、管理员录入成绩、前台展示排名,听起来确实简单。但真正接触过赛事运营的人都知道,这里面的坑远比想象的多。报名阶段要处理名额超卖、重复报名、身份信息核验,比赛当天要应对成绩实时上传、枪声成绩和净成绩的换算、分组排名、证书生成,赛后还要做数据统计和成绩公示。这套流程如果用Excel加手工处理,几百人的小赛事还能勉强撑住,一旦达到几千人的规模,基本就是灾难现场。
这个项目的价值在于,它把赛前、赛中、赛后三个阶段的业务动作统一到一个系统里,用SpringBoot负责后端业务逻辑和数据持久化,用Vue负责前端交互和界面呈现,形成一套完整可落地的前后端分离方案。基于这个诉求衍生出的设计和实现思路,既能当作课程设计的完整选题,也能作为赛事运营团队内部工具的原型参考,实际开发过程中的并发控制、状态机设计、权限管理等知识点,更是能直接复用到其他业务系统里。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 系统整体架构与开发环境选型思路
2.1 前后端分离架构的取舍
这个项目采用SpringBoot + Vue的前后端分离方案,不是赶时髦,而是由实际业务场景决定的。
马拉松管理系统的使用角色至少有三种:选手、赛事管理员、系统运维人员。选手端的操作集中在报名、查成绩、下载证书;管理员端则要处理赛事配置、报名审核、成绩导入、名单导出。如果使用传统的服务端渲染方案,所有页面逻辑都堆在服务端,每次页面跳转都要刷新,在赛事当天成绩实时刷新这种场景下体验会非常差。前后端分离之后,Vue负责构建交互界面,通过接口与后端交换JSON数据,页面局部刷新、状态即时回显,体验顺畅很多。
后端只暴露RESTful API,业务逻辑和数据权限都在服务端控制。这样还有一个实际好处:如果未来要扩展小程序端或者App端,前端可以完全重写,后端接口基本不用动。
2.2 核心技术栈的确定
后端选的SpringBoot版本建议用2.7.x或3.x,搭配MyBatis-Plus做数据访问层。MyBatis-Plus的代码生成器和条件构造器能大幅减少单表操作的样板代码,典型的如选手管理、报名记录查询这些场景,直接用LambdaQueryWrapper就能搞定复杂查询条件。安全框架用Spring Security + JWT,因为系统天然有角色区分,选手、管理员、超级管理员的操作权限必须隔离,JWT无状态认证方式也符合前后端分离的架构需求。
数据库用MySQL 8.x,原因无他,稳定、成熟、团队熟悉度高。报名和成绩两个核心表的数据量在单场赛事级别也就是几万条的规模,MySQL完全能扛住。如果考虑到多场赛事叠加的长期运营,适当做分表即可,不需要一上来就引分布式数据库。
前端Vue选择Vue 3 + Element Plus,状态管理用Pinia,HTTP请求封装axios。Element Plus的表格组件和表单组件非常契合管理后台的界面风格,跑马灯、倒计时这类赛事氛围组件再自己封装几个,界面就足够专业了。
2.3 模块划分的核心思路
系统按业务域拆成四个大模块边界要清晰,这一步直接决定后面开发的复杂度:
- 赛事管理模块:赛事的增删改查,包含赛事名称、举办日期、起终点、组别设置(全马、半马、迷你跑)、报名时间窗口、名额总数、报名费。
- 报名管理模块:选手注册、报名提交、名额扣减、报名审核与取消、名单导出、号码簿分配。
- 成绩管理模块:成绩批量导入、选手成绩查询、枪声成绩与净成绩换算、排名计算、证书生成。
- 系统管理模块:用户管理、角色权限、操作日志。
每个模块之间的依赖关系也要提前约定好。赛事管理是基础,报名和成绩都依赖赛事ID作为外键约束。模块边界清晰,前后端联调的时候才不会被接口设计来回拉扯。
3. 报名模块设计:难点远比想象的多
3.1 名额控制的并发方案
报名模块是整个系统里最考验技术功底的部分,核心难点就一句话:名额有限,同一时刻大量请求同时涌入怎么保证不多卖?
最简单的做法是准备语句查出剩余名额,大于零就插入报名记录,然后更新名额减一。但这种方案在高并发下必然出问题,两个请求同时读到剩余名额是1,各自判断可以报名,结果都插入了记录,名额超卖就发生了。
解决思路通常有两个方向。一是数据库层面加锁,在事务里对赛事记录执行SELECT ... FOR UPDATE,串行化名额扣减操作。这种方式实现简单,但在单场热门赛事报名瞬间会有大量请求等待行锁,数据库连接容易被占满。二是Redis分布式锁配合原子操作,用DECR命令执行名额扣减,返回值大于等于零说明扣减成功,否则说明名额已满,报名请求直接返回“名额已满”。这种方式效率高,能支撑的并发量远超直接操作数据库,但需要额外维护Redis的数据一致性。
我当时实际采用的是双保险策略:Redis原子扣减处理瞬时流量,同时数据库记录报名明细时做唯一约束(如同一赛事同一身份证号只允许一条有效报名记录),组合起来既挡住超卖,又能让数据库最终数据完全准确。Redis意外宕机时,数据库唯一约束也能兜底,不产生脏数据。
3.2 报名状态机的设计
报名不是一个一次性动作,它存在完整的生命周期。新增报名记录后初始状态是“待支付”,支付回调成功后进入“已报名”,赛事管理员可以将其调整为“已审核”;如果选手在报名截止前申请退款,状态变为“已取消”,此时要释放名额回名额池;赛事结束后,报名状态与成绩关联,选手可以查询成绩。
这里做一个简单的状态机流转图,把所有允许的转换路径固定下来,能避免很多逻辑漏洞。比如已取消的报名记录不能直接变成已支付,已审核的记录不能再重复申请改资料。状态机不需要引入专门的框架,在Service层写一个状态转换校验方法就够了,状态不合法直接抛业务异常。
3.3 号码簿分配
号码簿分配看起来简单,实际要处理不少边界情况。全马、半马、迷你跑三个组别如果共用一个号码段,会出现号码重复的混乱情况。正确做法是每个组别分配独立的号码段,全马从A001开始、半马从B001开始、迷你跑从C001开始,数据库存号码段起始值,报名成功后自动生成当前最大号码加1。
还有一类特殊情况是赞助商名额和精英选手名额,通常需要指定专属号码。在号码分配逻辑里预留一个“手工指定号码”的入口,管理员可以绕开自动分配直接给指定选手赋予号码,但号码唯一性校验必须保留。
3.4 报名表单的校验策略
前端校验关注格式,后端校验关注数据和业务的真实性。身份证号码要做长度和校验位校验,邮箱要做格式校验,紧急联系人电话要做手机号格式校验。后端除重复校验外,还要限制同一个手机号或身份证号在同一赛事中只能报名一次。
这里犯过的低级错误是只做了前端校验,Postman直接调接口照样能提交非法数据。前后端分离模式下,前端的校验只是用户体验,后端的校验才是安全边界,这个意识不建立起来,系统上线必然被刷。
4. 成绩管理模块:从数据录入到证书生成
4.1 成绩数据的来源与接入
成绩数据的产生有两种典型场景。一是芯片计时公司提供成绩文件,最通用的是Excel或者CSV格式,包含参赛号、枪声成绩、净成绩、分段计时点数据。二是某些计时系统提供实时接口,每跑过一个计时毯就推送一条记录。
考虑系统的通用性,成绩导入模块设计成基于模板解析的方式。管理员下载系统提供的标准Excel模板,填充成绩数据,上传后后台用EasyExcel解析,逐行校验后写入数据库。校验规则包括:参赛号码是否存在、枪声成绩是否晚于净成绩、是否有重复记录等。解析完成后给管理员返回导入结果汇总,成功多少条、失败多少条、失败原因是什么,支持下载错误明细文件。
4.2 枪声成绩与净成绩的换算逻辑
枪声成绩是从发令枪响到选手冲线的时间,净成绩是选手通过起点计时毯到冲线的时间。对于站在后排起跑的选手,两者可能相差几分钟。排名时全马和半马的官方排名通常按净成绩算,但获奖名次又看枪声成绩,这个规则在不同赛事中可能不同。
系统的做法是成绩数据结构里同时存储枪声成绩和净成绩,排名的时候通过赛事配置决定以哪个字段为排序依据。时间字段不推荐用Datetime类型存储,排名排序和差值计算不方便,直接用整数存毫秒数,展示的时候格式化输出。比如10496400毫秒格式化后是02:54:56.4,界面展示、成绩排序、净成绩和枪声成绩的差值计算都非常高效。
4.3 分组成绩排名
一场马拉松会有多种排名维度。按组别分,全马男子组、全马女子组、半马男子组、半马女子组各自独立排名;按年龄段分,18-34岁组、35-49岁组、50岁以上组还要再排一遍;按团队分,部分企业赛事还会设置团队总成绩排名。
排名逻辑用Java Stream分组后按成绩字段排序就能实现,关键在于排序字段的归一化处理。如果直接比较字符串格式的时间,会出现“10:30”排在“9:45”前面的问题,因为字符串比较是逐位比的。统一转成毫秒数再比较,排序结果就是完全正确的。
4.4 成绩证书的自动生成
证书生成是选手体验的重要一环,也是很多系统容易欠缺的部分。完赛证书需要包含选手姓名、参赛号码、赛事名称、项目组别、枪声成绩、净成绩、组别排名、完赛时间,部分赛事还要生成带有组委会签章的样式。
技术方案推荐用Java后端的图形生成方案,引入Thymeleaf模板引擎,把证书内容渲染成HTML,再用开源组件转成PDF或PNG。好处是模板修改方便,用HTML+CSS就能完成复杂的排版布局,不用像直接操作图片坐标那样反复调像素。渲染性能方面,单张证书生成耗时约200毫秒,用线程池批量处理完全够用,不用上消息队列。
5. 核心接口设计与前后端协作细节
5.1 接口文档先行
前后端分离的项目,接口文档的规范程度直接决定联调效率。实际开发时推荐在动手写代码前,先把所有接口的定义确定下来,包括URL、请求方法、请求参数、返回结构、错误码。
接口返回结构统一用Result<T>封装,包含code、message、data三个字段,成功时code为200,业务异常时code为4xx或5xx特定的业务异常码。如果每个人的接口返回格式都不一样,前端axios拦截器就无法统一处理错误提示,登录过期判断也无从下手。
5.2 认证与权限控制的落地
JWT令牌在登录成功后返回给前端,前端存储在本地,每次请求在axios请求拦截器中自动携带。后端在网关或拦截器层面解析令牌,将用户ID和角色信息放入上下文。角色权限用Spring Security的@PreAuthorize注解控制,比如报名提交接口只需要登录,成绩导入接口要求管理员角色,赛事创建接口要求超级管理员角色。
这里值得注意的一点是,JWT没有状态,令牌签发后不能主动失效。管理员封禁某个选手账号,已经发出去的令牌依然有效,直到过期。业务上如果有封禁或踢人下线的需求,需要额外配合Redis黑名单机制处理。
5.3 大屏展示的数据推送
赛事当天通常需要在起终点或者会场设置成绩展示大屏,实时展示已完赛选手的成绩和排名。如果前端用定时器每隔几秒轮询接口,成绩延迟高且服务器压力大。更好的方案是WebSocket长连接推送。
后端在成绩导入的同时,把新成绩推送到WebSocket通道,前端收到数据自动更新榜单。实测下来,推送延迟在100毫秒以内,体验远好于轮询。这个功能属于加分项,项目答辩和实际演示时效果非常惊艳,值得做进去。
6. 数据库设计与SQL优化细节
6.1 核心表的字段设计
核心业务表建议至少包含这样几张:
报名表:报名ID、赛事ID、选手ID、参赛组别、参赛号码、报名状态、报名时间、支付流水号、支付金额、支付状态。
成绩表:成绩ID、报名ID、赛事ID、参赛号码、枪声成绩(毫秒)、净成绩(毫秒)、组别排名、总排名、分段数据(JSON字段)。
赛事表:赛事ID、赛事名称、举办日期、报名开始时间、报名结束时间、各组别名额JSON字段、状态。
选手表:选手ID、姓名、身份证号、手机号、邮箱、紧急联系人、紧急联系人电话、注册时间。
字段命名统一用下划线风格,与Java实体的驼峰命名通过MyBatis-Plus的驼峰映射机制自动转换。
6.2 索引设计与慢查询排查
报名表的高频查询条件有两个:按赛事ID查报名列表、按身份证号查某一赛事中是否存在重复报名,联合索引必须建。成绩表的高频查询是赛事ID加组别加成绩排序,在赛事ID和组别字段上建联合索引,排名查询用索引覆盖就能完成。
实测中踩过的一个坑是成绩表忘记给参赛号码建索引,导致成绩导入时用参赛号码反查报名信息非常慢。Excel里有几千条成绩记录,每条都要做一次全表扫描匹配参赛号码,导入一次要卡顿好几分钟。加上索引之后,同样的导入流程缩短到几秒内完成。
6.3 赛事配置表的JSON字段设计
不同赛事的组别设置、名额数量、报名费用都不相同,如果每种配置都建单独字段,表结构会变得非常死板。实战中可以把组别配置设计成JSON字段存到赛事表里,例如:[{"groupId":1,"name":"全马","quota":5000,"fee":200},{"groupId":2,"name":"半马","quota":3000,"fee":150}]。
MySQL 8.x对JSON字段的查询支持已经比较完善,解析时用JSON_EXTRACT或者查出后在应用层用Fastjson解析都可以。配置文件化极大提升了赛事的扩展性,新加一个组别只需要改配置,不用改表结构。
7. 避坑实录:实际操作中容易翻车的细节
JSON解析异常。名额扣减成功但报名明细插入失败,Redis计数器没有回滚,造成名额永久少一个。后来在报名事务提交失败时做了一次Redis补偿回加,才彻底解决这个问题。
计时芯片成绩里有DNF(未完赛)和DNS(未出发)状态,导入的时候如果把DNF当作有效成绩存进去,后面的排名计算和完赛率统计全部错乱。导入模板中增加状态列,只导入FIN状态的记录或者专门标识DNF记录,排名查询时统一排除。
Excel导入大文件时同步解析会卡死接口,整合EasyExcel的监听器模式,分批解析分批提交,防止内存溢出和接口超时。
证书模板里出现选手姓名超长导致排版溢出,在服务端做字符串截断,比如姓名超过8个字符自动缩小字号或省略中间字符。
系统上线前一定要模拟一次完整的赛事流程,从创建赛事、配置名额、模拟报名到导入成绩、生成证书、查看排名,全链路走一遍。很多问题只有在完整流程下才会暴露,零散的功能测试测不出数据流转层面的问题。
我个人的建议是,这一类系统最核心的不是界面多华丽、技术多新颖,而是数据准确、流程稳定。名额不能超卖,成绩不能错乱,证书不能出错,这三点做到位,系统的价值就已经兑现了。SpringBoot + Vue这套组合既保证了业务逻辑的严谨性,又提供了良好的交互体验,作为完整项目的实践载体非常合适。
如果你正在做类似的系统,先把报名并发和成绩导入两个核心模块的方案设计清楚,再推进其他功能,会少走很多弯路。
