1. 健美操评分的业务复杂度,决定了这套系统不是普通的CRUD
先聊一个实际问题:健美操比赛到底怎么打分?
我在学校体育学院帮忙做信息化建设的时候,第一次被拉到比赛现场,发现裁判手里拿的还是纸质评分表,工作人员在旁边用计算器加总,念成绩还要靠人工跑腿。一场比赛几十支队伍,每支队伍要评编排分(艺术性、动作设计、音乐配合)和完成分(动作质量、一致性、场地使用),多名裁判打分后还要去掉最高分和最低分,再取平均。脑子稍微一走神,分数就容易算错。
这也是为什么要做这套系统的根本原因。健美操评分不是简单的"几个评委点个分存进数据库",它有几个非常折磨人的业务特征:
- 评分维度是复合的。单个裁判对一支队伍要同时给出编排分和完成分,甚至在某些规则下还有难度分。每个维度的权重还不一样,汇总时必须按公式计算。
- 裁判人数有讲究。基层比赛用5名裁判,正式比赛可能用7名或更多。去掉一个最高分、去掉一个最低分,逻辑一样,但人数不同,代码必须参数化,不能写死。
- 实时性要求高。比赛现场,下一个队伍已经候场了,前面队伍的成绩必须尽快出来,大屏要显示,记录台要核对。拖个几分钟节奏就乱了。
- 流程有严格顺序。抽签排序、检录、比赛、评分、公布、排名,每一步都有状态流转。不能出现"还没比赛就有成绩"这种荒唐事。
- 裁判会轮换。健美操比赛经常分预赛和决赛,预赛评委和决赛评委可能不是同一批人,评分明细必须能区分场次。
所以,这套"基于SpringBoot+Vue的健美操评分系统"本质上要解决的,是赛事组织者从赛前登记、赛中评分、赛后排名的一整套数字化流程问题。它适合谁用?学校院系办比赛、体育俱乐部内部赛、以及需要做课程设计或毕业设计但没有业务场景的学生开发者——后者尤其需要理解真实赛事的规则,才能在代码里写出像样的评分逻辑。
说实话,网上有很多"管理系统"项目,但大部分都是用户CRUD加两张表的套路,评委打分往往只是最简单的"输入一个分数"存起来。而健美操评分系统的核心难点恰恰在于:如何在多人同时评分的情况下,保证每个评委的独立性、分数计算的准确性、以及比赛流程的完整性。这篇博文我把整个项目的设计思路、表结构、核心算法、踩坑经历全部摊开来说,照着做,你不仅能跑起来,还能真正理解为什么这么设计。
2. 技术选型背后的取舍:为什么是SpringBoot+Vue+MySQL+MyBatis,而不是别的组合
很多新手拿到一个项目标题,第一反应是"用SpringBoot就行""用Vue就行",但对"为什么"完全没概念。这个项目选型其实经过了非常现实的考虑。
2.1 前后端分离的动因:比赛现场的特殊环境
我第一次做这类系统时,想过用JSP或Thymeleaf做服务端渲染,一次性搞定。后来打消了这个念头,原因是比赛现场的硬件和网络环境太复杂了:
- 评委席上可能放的是不同配置的笔记本或平板,浏览器版本老旧,服务端渲染的页面一旦要局部刷新,对网络往返的依赖太重。
- 大屏展示、成绩复核、后台管理是三个完全不同的界面,同时打开的页面很多,前端逻辑复杂度远高于后端模板能承载的。
- 比赛现场容易出现瞬时网络波动,如果页面一刷新整个数据就丢,那就是事故。前后端分离后,前端页面和数据接口完全独立,网络抖动只要不刷新页面,数据还在内存里,重试机制也好做。
Vue 3(或Vue 2,根据你手头版本)的响应式机制和组件化开发,在这种多角色、多界面的场景下优势非常明显。评委打分手、成绩展示页、管理员后台,本质上就是几套相对独立的界面,但共享同一套API。
2.2 SpringBoot:省去配置地狱,自带内嵌容器
后端选SpringBoot几乎没什么可犹豫的。这个项目涉及登录鉴权、赛事管理、评分提交、成绩计算、排名查询,需要一套完整的Web框架来组织接口。SpringBoot带来的直接好处是:
- 内嵌Tomcat,打成一个Jar包就能跑,不用单独装服务器。比赛现场很可能是临时搭建的电脑环境,能用一条
java -jar命令启动最好。 - 自动配置简化了数据库连接,只需在
application.yml里写上数据源信息,SpringBoot自动完成连接池管理、事务管理等基础设施。 - 生态成熟,资料多。遇到问题Google一搜一大把,对做课设或毕设的同学尤其友好。
2.3 MyBatis:复杂查询自己写SQL,比分计算才能心里有底
这里很多人会问:为什么用MyBatis不用MyBatis-Plus,甚至不用JPA?我的原话是:MyBatis的SQL可控性在这个项目中非常重要。
评分系统的核心逻辑是"去掉最高分最低分再取平均",以及"多表联查排名"这类统计操作。这些需要用SQL直接表达,而且查询条件经常变化(不同赛事裁判人数不同、是否需要加权、预赛和决赛取分方式是否一致)。MyBatis让你把SQL完全掌握在手里,想怎么优化就怎么优化。
MyBatis-Plus当然也能做,但它的查询构造器在复杂统计SQL面前,可读性和排错成本反而不如原生SQL直观。而且这个项目涉及的动态SQL(比如按赛事状态筛选、按裁判ID批量查询)用MyBatis的<if>标签组合非常自然。
2.4 MySQL:单机部署、轻量可靠,完全够用
比赛现场的数据量有多大?顶多几百个队伍、几十个裁判、几千条评分明细,MySQL完全能扛住,而且安装方便,大家也最熟悉。更关键的是,MySQL对中文支持好、线程安全、MyBatis的兼容性也最成熟。如果用Oracle或者PostgreSQL,反而会给部署和开发增加不必要的复杂度。
下表是我最终确定的各层选型和理由,方便你核对:
| 层次 | 选型 | 选型理由 |
|---|---|---|
| 前端框架 | Vue 3 + Vite / Vue 2 + Webpack | 响应式快速开发,组件化便于拆分打分台与展示屏 |
| 后端框架 | Spring Boot 2.x / 3.x | 快速集成、内嵌容器、生态完善 |
| 持久层 | MyBatis | SQL可控,适合复杂评分统计 |
| 数据库 | MySQL 5.7 / 8.0 | 轻量稳定,部署简单,中文支持好 |
| 前端构建 | npm + Maven | 前后端独立构建,最终合并或分离部署 |
| 权限控制 | JWT + 拦截器/Spring Security | 轻量、无状态,满足多角色鉴权 |
如果你是非互联网大厂内部的百万级并发场景,这套选型肯定不是最优解。但作为一个比赛现场评分系统,它已经可以稳定支撑几百人同时使用,性价比极高。
3. 数据库设计:把编排分、完成分、裁判轮换和赛事流程落到表结构里
数据库设计是整个项目中最容易"先写代码后补表"的环节,但一旦表设计烂了,后面所有查询都会变成灾难。我这套系统前后改了三轮表结构,最终定下来这几张核心表,直接截取最关键的几张讲清楚。
3.1 用户与角色:不要把所有身份塞进一张表
用户表(sys_user)保存基础的账号密码和角色字段,这是最简单的设计。但要注意,裁判角色在这个项目里不是普通用户,他需要在某个具体的赛事中、为某场具体比赛打分。所以我没有把"裁判绑定了哪场比赛"写进用户表,而是单独设计了一张比赛的裁判分配表。
我当时的设计是:
- sys_user:用户ID、用户名、密码(BCrypt加密)、真实姓名、角色标识(admin、referee、recorder、audience)
- competition:赛事ID、赛事名称、比赛时间、比赛地点、状态(未开始/进行中/已结束)、评分规则(单人/集体、裁判人数)
- team:队伍ID、所属赛事ID、队伍名称、参赛项目、领队姓名、联系方式
- competition_referee:评委ID、赛事ID、用户ID —— 表示某个裁判参与哪场比赛
- performance:表演场次ID、赛事ID、队伍ID、出场顺序、比赛轮次(预赛/决赛)、状态
这里有一个非常容易踩的坑:裁判和队伍之间是多对多的关系,直接加一个参赛表字段管理裁判容易乱。所以我把"谁评谁"完全交给competition_referee和performance两张表组合,评分明细表再去引用它们。
3.2 评分明细表:核心中的核心
评分表(score_detail)是这个系统最重要的表,直接决定成绩计算的难度。我最终的设计如下:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键自增 |
| competition_id | bigint | 赛事ID |
| performance_id | bigint | 表演场次ID(哪支队伍第几个出场) |
| referee_id | bigint | 裁判用户ID |
| score_type | tinyint | 评分类型:1=编排分,2=完成分,3=难度分(可扩展) |
| score_value | decimal(5,2) | 实际打出的分数,保留两位小数 |
| create_time | datetime | 评分时间 |
| status | tinyint | 0=有效,1=已作废(裁判误操作时) |
同时,我给(performance_id, referee_id, score_type)建了联合唯一索引,确保同一个裁判对同一支队伍的同一类分数只能录一次。这个索引在防重复提交上非常重要——前端就算连点两次提交,数据库这一层也会挡住,不至于出现两个分数。
很多人做这类系统会忽略status字段,觉得"分数提交了还能改?"但实际情况是裁判经常手滑填错。允许作废重录,比直接删记录更安全,因为你还能留痕。这个字段在后面的成绩计算中必须用到:只统计status=0的有效记录。
3.3 为什么不直接存平均分:明细与汇总分离的设计思路
还有一个关键决策:要不要单独建一张成绩汇总表(score_summary)?
我的答案是必须建,但不能用它来替代明细表。原因有三:
- 观众/大屏要展示总成绩,如果每次展示都现场计算平均分,虽然逻辑不复杂,但面对大屏频繁刷新,没必要。
- 比赛排名可能会因为某裁判的分数作废而重新计算,如果只有汇总表,改起来会非常麻烦。有了明细表,重新算一遍就行。
- 竞赛主办方需要留档每一份原始评分,这是赛事仲裁的依据。
所以我的做法是:评委提交分数时,只写score_detail表;当评委确认整场评分结束,后台通过一个"成绩计算"接口,聚合score_detail的数据,算出各队伍总成绩并写入score_summary表。这样既保留了明细,又有快速查询的汇总表。
score_summary表大致是:赛事ID、队伍ID、编排分平均、完成分平均、总分、排名、计算时间。其中"平均分"这个字段名容易误导——实际上它是去掉最高最低后的平均,保留两位小数。
如果你还要支持"团队赛中裁判分别打分、最后汇总"的规则,可以在score_summary里再加一个weight字段,用于不同评分类型的加权计算。不过一般健美操比赛用不到那么复杂,先保持简洁。
4. 后端核心逻辑:从评分提交接口到"去掉最高最低分"的实现
技术选型和表结构都定了,接下来是后端逻辑实现。这里我挑几个我认为最能体现系统质量的核心点,详细展开。
4.1 登录鉴权:JWT+拦截器,轻量但不是没有
该项目有四种角色,访问权限必须区分。我用的是Spring Security或简单的JWT拦截器方案。如果不想引入复杂的Spring Security配置,JWT+拦截器组合完全够用。
流程如下:
- 用户登录成功后,后端生成一个包含用户ID和角色标识的JWT令牌返回。
- 前端将令牌放在请求头
Authorization: Bearer <token>中。 - 后端定义一个拦截器(HandlerInterceptor),在preHandle方法中解析JWT,校验有效性和过期时间,并把用户信息放入ThreadLocal或Request Attribute,供后续Controller使用。
需要特别注意的是评委的权限判断。管理员能管理所有赛事,但评委只能查看自己参与的比赛、给对应的队伍打分。这种"数据级权限"不能只靠拦截器里的角色判断,还得在service层再做一次校验:当前评委是否在本场赛事的competition_referee表里有记录。我当时就犯过错——评委能查到他没参与的比赛的队伍名单,还好在评审阶段被指出来了。
4.2 评分提交接口:事务和防重,缺一不可
评分提交的核心逻辑很简单,但有几个细节处理不好很容易翻车。
接口设计大致是这样:
@PostMapping("/api/score/submit") public Result submitScore(@RequestBody ScoreSubmitDTO dto) { // dto包含:competitionId, performanceId, refereeId, scoreType, scoreValue // 1. 校验赛事状态是否为进行中 // 2. 校验裁判是否有权限评这场表演 // 3. 校验scoreValue在合理区间(比如0.0-10.0) // 4. 保存评分明细(通过唯一索引防重) }特别说明第3点。评分区间判断很重要,比如编排分范围通常0~10分、完成分也是0~10分,但有些赛事规则不同(比如部分比赛总分是20分)。我建议把评分区间在赛事配置表里维护,而不是写死在代码里。这样以后办不同的比赛,只需要在后台配置规则,不用改代码重新部署。
第4点保存评分明细时,我用MyBatis的insert,配合唯一索引做防重。数据库抛出DuplicateKeyException时,捕获后返回给前端"您已提交过该分数,请勿重复提交"。不要假装没问题继续走流程,否则成绩会乱。
4.3 去掉最高最低分的计算:别被"平均"两个字骗了
这里就是整篇博客我认为最有含金量的部分之一。很多人做评分系统时会写一个方法:
Double avg = scoreList.stream() .mapToDouble(Score::getScoreValue) .average() .orElse(0.0);然后交付了。但健美操评分规则是去掉最高分和最低分,再取平均。这个逻辑完全不同。
一家5名裁判打分为例:某队伍的编排分分别是9.5、9.2、9.8、9.0、9.4,去掉最高9.8和最低9.0后,剩下三个数的平均是(9.5+9.2+9.4)/3≈9.37。如果直接平均是9.38,数字差不了多少,但比赛规则就是规则,就这么0.1分的差距可能决定冠亚军。
实现时我建议别在SQL里做这种计算,除非你的SQL功底真的很强。原因是:这个逻辑涉及排序、去掉端点、再求平均,用Java代码实现更直观、更好测试、更易于扩展(比如7名裁判时也是去掉一个最高最低,但如果规则改成去掉两个最高两个最低,只要改参数即可)。
我用Java实现的基本方法如下:
public BigDecimal calcAverage(List<BigDecimal> scores) { // 排除null List<BigDecimal> validScores = scores.stream() .filter(Objects::nonNull) .sorted() .collect(Collectors.toList()); // 去掉最高最低 if (validScores.size() > 2) { validScores = validScores.subList(1, validScores.size() - 1); } return validScores.stream() .reduce(BigDecimal.ZERO, BigDecimal::add) .divide(BigDecimal.valueOf(validScores.size()), 2, RoundingMode.HALF_UP); }注意几个细节:
- BigDecimal而不是double。裁判分数保留两位小数,浮点数直接做加减和除法会产生精度问题。比如9.2+9.4+9.5在double里很可能会出现9.199999999。用BigDecimal配合RoundingMode.HALF_UP,分数才精确。
- 成绩计算时机。我之前是在评委每次提交分数时实时触发一次汇总计算。后来发现高并发下性能会下降,而且频繁计算容易造成数据库锁竞争。改成在"本场评分结束后,由记录员点击'计算成绩'"触发一次,效果更好。也就是说,比分计算是异步的、按场次批量执行的。
- 某些赛事规则要求去掉两个最高两个最低,这个参数建议也放到赛事配置里:
dropHighestCount、dropLowestCount,默认都是1。
4.4 排名和并列处理:不要只order by完事
队伍总成绩出来之后,排名看似就是ORDER BY total_score DESC,但在真实比赛中会遇到"并列"。如果两个队伍总分都是9.50,谁前谁后?这就需要引入"并列排名"逻辑——通常健美操比赛采用并列排名,即相同分数排名一致,后续排名跳过。比如第一名有2支队伍,那下一支队伍就是第三名,没有第二名了。
这在代码实现上是一个小小的细节,但直接影响榜单展示。我在生成排名时不是单纯靠SQL的row_number,而是用Java代码处理:
if (index > 0 && list.get(index).getTotalScore() .compareTo(list.get(index - 1).getTotalScore()) == 0) { currentRank = list.get(index - 1).getRank(); } else { currentRank = index + 1; }这个逻辑不难,但如果你完全依赖数据库排名函数,在并列情况下的处理就会比较别扭。
5. 前端不将就:Vue里的评委打分台与大屏展示的交互设计
前端部分是这个系统最容易被低估的地方。很多项目后端写得不错,但前端页面要么是"管理后台表格流水线",要么是完全忽略赛事现场操作体验。而评委打分台的用户体验直接影响比赛效率,不得不认真对待。
5.1 打分台组件:想清楚用键盘还是鼠标
评委在比赛现场打分时往往很紧张,一支队伍只有一两分钟时间,必须快速找到队伍、打两个分、提交成功、继续下一支。我最终把打分台设计成三块区域:
- 左侧是"当前候场队伍"列表,按出场顺序排列,当前表演的队伍高亮。
- 中间是评分操作区,可以同时看到编排分和完成分的输入框(我用的是两个滑块加数字输入框的组合,滑块粗调、数字框精调)。
- 右侧是"最近评分记录",展示当前评委已提交的分数,每提交成功一条就刷新一次。
使用体验上有三个细节值得注意:
- 防误触提交。我加了一个"确认提交"的二次确认弹窗,且评分操作后3秒内按钮不可重复点击。比赛现场评委容易手滑,这个成本很低但很管用。
- 支持键盘快速操作。Tab键切换编排分/完成分,按回车提交。有的评委习惯用键盘盲打,如果不做这个,现场效率会低很多。
- 网络异常时的处理。评委提交分数如果网络失败,前端不要直接丢掉输入值,要弹出提示并提供"重试"按钮。我用的方案是axios拦截器,检测到提交失败时,把数据保存在本地一个重试队列,配合手动重试按钮,避免现场手忙脚乱重新输入。
5.2 Vue响应式数据流:打分记录如何实时联动
评分提交后,如果想大屏实时看到最新分数,需要前端轮询或者后端推送。很多人一上来就想着WebSocket,但实际比赛场景中,"当前全场最高分"这类数据并不需要毫秒级更新,轮询足够。我用的是定时器每5秒拉取一次成绩汇总接口,更新排名榜单。这里不建议把轮询做得太频繁,比赛现场网络并不一定稳定,太频繁反而给自己找麻烦。
组件划分上,我大概拆成这些:
ScoreBoard.vue—— 评分台主组件,负责队伍列表和评分操作ResultDisplay.vue—— 大屏成绩展示,排名、分数、队伍名CompetitionConfig.vue—— 管理员配置赛事和裁判RefereeAssign.vue—— 分配裁判到具体场次Login.vue、Layout.vue等基础组件
每个组件通过Vue Router来切换页面,状态管理用Pinia(Vue3的话)或Vuex(Vue2的话),主要保存当前用户、当前赛事ID、当前评分状态这类全局信息。
5.3 大屏展示页:成绩上屏的边界情况
大屏展示页常常被忽略的几个场景:
- 下一轮比赛开始前,大屏应该显示的是宣传页/候场提示,而不是上一轮最终成绩。
- 当前轮次所有队伍都比完、成绩未公布时,大屏要显示"成绩计算中",不能出现0分或空数据。
- 单项总分和排名在榜单上必须有明确区分,字体要大、信息要少,观众不会盯着小字看。
我在设计这个页面时,用了一个status字段控制展示状态:0=候场,1=比赛中,2=等待公布,3=已公布。接口返回的成绩数据根据这个状态切换,前端就不用做复杂的判断了。
6. 部署、打包与避坑:从Maven构建到比赛现场能跑起来的经验清单
这部分是新人最容易卡住的地方。代码写完了,但"怎么把它部署到一台临时电脑上"却很少有人讲透。我把自己反复踩过的坑整理成一份清单。
6.1 Maven构建与SpringBoot配置
后端部分我用Maven管理依赖,整个项目的构建非常顺滑。但有几个配置文件细节值得说:
application.yml里的数据库连接:
spring: datasource: url: jdbc:mysql://localhost:3306/aerobics?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: xxxxxx driver-class-name: com.mysql.cj.jdbc.Driver这里的serverTimezone=Asia/Shanghai必须加,不然系统时间和数据库时间对不上,成绩的create_time会差8个小时,查出来的评分时间怎么看怎么别扭。useSSL=false在MySQL 5.7以上通常是必要的,不然会报SSL连接错误。
端口设置:
server: port: 8080比赛现场如果有多台机器访问,要注意防火墙放行这个端口,同时确认数据库允许远程连接(修改MySQL的bind-address和账号host配置),否则前端页面能打开,但接口全部超时。
6.2 前后端分离打包的两种方式
Vue项目开发时用npm run dev访问8080(或其它端口),但正式比赛不可能让评委用开发服务器。有两种部署方案:
方案一:前后端合并打包
Vue项目构建后,把dist目录里的静态资源复制到SpringBoot项目的src/main/resources/static目录,再重新mvn package。这样后端Tomcat同时托管前端页面和API,一个Jar包全搞定。优点是部署最简单——就一条java -jar命令。
方案二:Nginx托管前端,反向代理后端
用Nginx托管Vue的构建产物,并通过proxy_pass把/api请求转发到SpringBoot。这种方式性能更好、前端更新也不用重新打包后端,适合并发较高的场景。配置大概是:
location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://localhost:8080/api/; }我最终在比赛现场用的方案一,图省事。但如果你做的是毕业设计,建议在文档里把两种方案都写清楚,答辩老师很吃这一套。
6.3 部署路上最容易踩的坑清单
按我实际经验排名,这几个坑出现频率最高:
| 坑 | 现象 | 解决办法 |
|---|---|---|
| Vue路由history模式刷新404 | 页面一刷新就报404 | Nginx配置try_files 或 后端加forward请求到index.html |
| 前端访问后端接口跨域 | 浏览器报CORS错误 | SpringBoot配置CorsFilter或 @CrossOrigin |
| MySQL驱动版本不匹配 | 连接数据库报ClassNotFoundException | SpringBoot 2.x用mysql-connector-java,3.x用com.mysql.cj.jdbc.Driver |
| 数据库脚本编码错误 | 中文乱码 | 建库时指定utf8mb4,连接串指定characterEncoding=utf8 |
| 评委重复提交导致成绩错误 | 分数出现多个相同记录 | 联合唯一索引+前端防重 |
| 评分时间差8小时 | 创建时间和本地时间对不上 | 连接串加serverTimezone=Asia/Shanghai |
| Vue构建失败 | 报内存溢出 | 在package.json里加"build": "vue-cli-service build --max-old-space-size=4096" |
第3个坑我在做Spring Boot 3.x时吃过亏,直接用Spring Initializr生成的项目默认是com.mysql.cj.jdbc.Driver,而网上教程大多还写着com.mysql.jdbc.Driver,不仔细就会抄错。注意SpringBoot版本和MySQL驱动的对应关系,别看一个是旧的就直接复制。
6.4 MyBatis使用中的实用细节:打印SQL和配置驼峰映射
开发调试阶段,MyBatis的SQL日志非常重要。我的建议是开发环境开SQL打印,正式部署时关掉,避免性能损耗和敏感信息泄露。配置方式如下:
mybatis: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case这个配置尤其值得记住。MySQL字段命名常用create_time这种下划线风格,Java实体类用createTime的驼峰风格,不开启这个映射的话,查询结果全是null,排查起来能让人崩溃。但只要一行配置就能解决,新手特别容易漏掉。
还有一个细节:使用MyBatis的<resultMap>做评分明细多表联查时,如果字段多,建议别偷懒用resultType="map",虽然方便,但返回的字段是String类型,日期和时间处理会特别麻烦。我后来全部改成明确的实体类映射,代码量多一点,但类型安全、后期维护方便。
6.5 比赛现场的应急方案:数据备份和手动纠错
最后聊一个"正规文档里不会写"的实际经验:比赛现场一定要准备一键备份方案。
我在现场就遇到过一台评委电脑突然死机,而评分记录因为网络原因没来得及提交的情况。因为我的score_detail表有作废机制,我可以马上把死机电脑上的口头分数录入到备用管理账号中,并且在备注里标记"代录"。这个功能虽然没有出现在初始需求里,但救了大命。
具体做法是:管理员后台增加一个"代录分数"入口,选择赛事、队伍、裁判,手动填分。这个操作会自动写入一条评分记录,并将status字段标记为0(有效),同时记录操作人和操作时间。分数一旦代录,原裁判回来后无法再提交同类的分数(因为联合唯一索引挡住了)。
所以,"作废重录"和"管理员代录"这两个功能,我认为是这套评分系统区别于普通CRUD项目的真正亮点。如果你照着本文复现,一定要把这两个功能加进去,实际使用价值非常大。
部署时我也建议准备一个backup.sql的定时备份脚本,比赛每结束一轮,手动导出一份当前数据,成本极低,心理踏实很多。MySQL里mysqldump这条命令就够,别把简单的事搞复杂。
这些经验总结下来就是一句话:评分系统的好坏,不在于功能列表里写了多少,而在于现场能不能稳定跑完一整天。如果你准备自己复刻这套系统,先把"去掉最高最低分"的算法跑对,再把异常场景(重复提交、误操作、代录)想清楚,这场实战就算真正入门了。