news 2026/9/9 15:38:05

基于Spring Boot与MyBatis-Plus的舞萌比赛积分管理系统开发实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于Spring Boot与MyBatis-Plus的舞萌比赛积分管理系统开发实战

很多音游店赛组织者都经历过这样的场景:一场舞萌比赛三四十位选手,每一轮歌曲不同、难度不同,达成率和结算分数全靠在纸质表格上手记,最后算总积分、排名次的时候要反复核对,一个数抄错影响一整条分数链。

本文就从“纱露朵娃娃”这个有点玩梗的项目代号说起,完整拆解一套舞萌比赛积分管理系统的开发过程。项目后端采用 Spring Boot + MyBatis-Plus,数据库用 MySQL,前端使用 Vue CDN + 原生 HTML,整体结构适合作为中等规模实战项目参考。读完你可以掌握一套完整的成绩录入、积分换算、排行榜展示实现方案,也能直接把它改造为门店赛、积分联赛或社团赛的管理工具。

需要先说明一点:本文内容只做赛后成绩录入和管理,不涉及机台数据破解、外挂辅助或任何违规抓取。所有比赛数据都来自人工登记或主办方结算表,这一点在开发中也要格外注意边界。

1. 背景与项目定位

1.1 舞萌比赛管理中的真实痛点

舞萌(maimai)是世嘉推出的街机音乐游戏,玩家根据屏幕上的箭头提示,通过在机台两侧的按键和触摸屏区域完成击打。国内许多机厅会定期举办门店赛或区域积分赛,赛事流程通常是:选手报名、分组、多轮歌曲比拼、按达成率折算积分,最后累积积分排名。

比赛过程中最大的压力其实在裁判和记分员身上。每名选手打完一轮后,需要记录歌曲名、难度、达成率、结算分数,还要判断是否 Clear。如果赛事采用“积分制”,主办方还要按达成率区间给选手换算积分,再根据歌曲难度进行加权。一旦选手较多,手工表格就很容易出现漏记、错记、上传缓慢的问题。

这个问题用一套 Web 系统就能解决。选手信息维护在系统里,成绩只需填达成率和分数,积分计算交给后端服务,排名自动生成。这样主办方在比赛间隙就能看到当前排名,也方便赛后在玩家群里分享结果。

1.2 “纱露朵娃娃”项目定位

看到“纱露朵娃娃”这个名字,可能有读者会好奇它到底是什么。它不是真实玩家,也不是什么官方吉祥物,只是机厅里一只常年摆在机台上的布偶娃娃。玩家开玩笑说,它是全场唯一一个不用动手、不需要搓键、却每天看着所有人打舞萌比赛的选手。

本文借用这个梗作为项目代号,原因也很简单:一个赛事管理工具本来就不需要“亲自上场”,它只需要安静地记录每一场成绩,像那只娃娃一样,坐在角落里看着比赛发生。项目代号不需要严肃,但功能设计一定要认真。

这套系统的定位不是大型赛事平台,而是一个轻量、可自托管的小工具。它不需要用户注册,不需要 OAuth,不需要复杂的权限体系,满足单机厅、单管理员的小规模赛事场景即可。如果后续要接入多门店、多赛事、多角色权限,可以在现有结构上继续扩展。

1.3 功能清单

第一版系统包含以下核心模块:

模块功能说明
选手管理新增选手、列表查询、按昵称搜索
比赛管理创建比赛、查询比赛列表
成绩录入录入选手某场比赛的成绩,包括歌曲、难度、达成率、分数
积分计算根据达成率区间与歌曲难度,自动计算本场积分
排行榜按比赛查看当前选手累计积分排名
数据导出按比赛导出积分表,方便赛后存档

从业务规则来看,本系统使用一种简化的评分方式:先按达成率区间得到基础积分,再乘上歌曲难度系数。实际赛事规则可能更复杂,但核心计算逻辑可以替换,不影响整体架构。

2. 环境准备与数据库设计

2.1 开发环境与版本约定

下面以 Spring Boot 2.7.x + JDK 8/11 + MySQL 8.x + MyBatis-Plus 3.5.x 为例进行讲解。如果你使用的是 Spring Boot 3.x,需要注意javax包名已经迁移到jakarta,部分配置项也发生了变化。

完整环境清单如下:

  • JDK:1.8 或 11
  • 构建工具:Maven 3.6+
  • 开发框架:Spring Boot 2.7.x
  • 持久层:MyBatis-Plus 3.5.x
  • 数据库:MySQL 5.7 或 8.x
  • 数据库连接池:Spring Boot 默认的 HikariCP
  • 前端:Vue 2 CDN + Axios CDN,单页 HTML
  • IDE:IntelliJ IDEA 或 VS Code

版本号不一定使用最新版,关键是 Spring Boot、MyBatis-Plus、MySQL 驱动三者之间的兼容性。建议先建立一个最小工程,确认依赖能正常下载后再往下开发。

2.2 数据库设计

数据库名建议为maimai_score,字符集使用utf8mb4,可以用来存储玩家昵称中的特殊字符和 emoji。下面先看三张核心表的字段设计。

第一张表是选手表,记录参赛选手基本信息。

字段类型说明
idBIGINT主键,自增
nicknameVARCHAR(64)玩家昵称
real_nameVARCHAR(64)真实姓名或备注,可空
phoneVARCHAR(20)联系电话,可空
player_levelVARCHAR(20)玩家评级,如 SS、S 等,可空
create_timeDATETIME创建时间

第二张表是比赛表,记录一场比赛的基本信息和状态。

字段类型说明
idBIGINT主键,自增
event_nameVARCHAR(128)比赛名称
event_dateDATE比赛日期
event_typeVARCHAR(20)比赛类型,如单赛、积分赛
statusTINYINT0 未开始,1 进行中,2 已结束
create_timeDATETIME创建时间

第三张表是成绩记录表,记录选手每一轮的具体成绩。

字段类型说明
idBIGINT主键,自增
match_idBIGINT比赛 ID
player_idBIGINT选手 ID
song_nameVARCHAR(128)歌曲名
song_levelINT歌曲难度等级
achievement_rateDECIMAL(5,2)达成率,例如 98.50
song_scoreINT结算分数
basic_pointsINT基础积分
total_pointsDECIMAL(8,2)加权后总积分
create_timeDATETIME录入时间

字段basic_pointstotal_points可以冗余存储,这样排行榜查询时不需要每次都重新计算积分。如果赛事规则调整,重新跑一遍历史成绩即可。

2.3 建表 SQL

下面是完整的建表脚本,可以直接在 MySQL 中执行。

CREATE DATABASE IF NOT EXISTS maimai_score DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE maimai_score; CREATE TABLE player ( id BIGINT AUTO_INCREMENT PRIMARY KEY, nickname VARCHAR(64) NOT NULL COMMENT '玩家昵称', real_name VARCHAR(64) DEFAULT NULL COMMENT '真实姓名或备注', phone VARCHAR(20) DEFAULT NULL COMMENT '联系电话', player_level VARCHAR(20) DEFAULT NULL COMMENT '玩家评级', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', UNIQUE KEY uk_nickname (nickname) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='选手表'; CREATE TABLE match_event ( id BIGINT AUTO_INCREMENT PRIMARY KEY, event_name VARCHAR(128) NOT NULL COMMENT '比赛名称', event_date DATE NOT NULL COMMENT '比赛日期', event_type VARCHAR(20) DEFAULT 'SINGLE' COMMENT '比赛类型', status TINYINT NOT NULL DEFAULT 1 COMMENT '0未开始 1进行中 2已结束', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='比赛表'; CREATE TABLE match_record ( id BIGINT AUTO_INCREMENT PRIMARY KEY, match_id BIGINT NOT NULL COMMENT '比赛ID', player_id BIGINT NOT NULL COMMENT '选手ID', song_name VARCHAR(128) NOT NULL COMMENT '歌曲名', song_level INT NOT NULL COMMENT '歌曲难度等级', achievement_rate DECIMAL(5,2) NOT NULL COMMENT '达成率', song_score INT NOT NULL COMMENT '结算分数', basic_points INT NOT NULL DEFAULT 0 COMMENT '基础积分', total_points DECIMAL(8,2) NOT NULL DEFAULT 0 COMMENT '加权后总积分', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '录入时间', KEY idx_match_id (match_id), KEY idx_player_id (player_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='比赛成绩记录表';

player表的昵称字段建议加唯一索引,防止同一个人被重复录入。真实赛事中同名情况比较常见,如果担心误判,也可以去掉唯一索引,改为录入时前端搜索确认。

3. 后端接口开发

3.1 创建 Spring Boot 工程与依赖

使用 IDEA 的 Spring Initializr 创建工程,Group 可以填com.example,Artifact 填maimai-score。生成后修改pom.xml,加入 MyBatis-Plus 和 MySQL 驱动依赖。

<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <groupId>com.example</groupId> <artifactId>maimai-score</artifactId> <version>1.0.0-SNAPSHOT</version> <name>maimai-score</name> <description>舞萌比赛积分管理系统</description> <properties> <java.version>1.8</java.version> <mybatis-plus.version>3.5.3</mybatis-plus.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>${mybatis-plus.version}</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>

注意事项:Spring Boot 2.7.x 默认使用mysql-connector-java,如果升级到 Spring Boot 3.x,驱动坐标会变化,使用com.mysql:mysql-connector-j。MyBatis-Plus 3.5.x 对较高版本 Spring Boot 的兼容性需要确认,遇到报错优先调整版本号。

3.2 配置文件

src/main/resources/application.yml中配置数据源和 MyBatis-Plus。

server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/maimai_score?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: root hikari: maximum-pool-size: 10 minimum-idle: 2 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto

map-underscore-to-camel-case会自动把数据库字段achievement_rate映射为 Java 属性achievementRate,减少手写 ResultMap 的麻烦。log-impl配置会在控制台输出 SQL,便于调试,生产环境可以移除。

3.3 实体类与 Mapper

com.example.maimai.entity包下创建三个实体类。这里以MatchRecord为例,其他两个类结构类似。

package com.example.maimai.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import java.math.BigDecimal; import java.time.LocalDateTime; @TableName("match_record") public class MatchRecord { @TableId(type = IdType.AUTO) private Long id; private Long matchId; private Long playerId; private String songName; private Integer songLevel; private BigDecimal achievementRate; private Integer songScore; private Integer basicPoints; private BigDecimal totalPoints; private LocalDateTime createTime; // getter / setter 省略 }

在 Java 8 环境下,LocalDateTime与 MySQL 的DATETIME可以正常映射。注意实体类属性命名要遵循驼峰规则,否则需要额外配置。

Mapper 接口非常简单,继承BaseMapper即可获得基础的增删改查能力。代码如下:

package com.example.maimai.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.maimai.entity.MatchRecord; import org.apache.ibatis.annotations.Mapper; @Mapper public interface MatchRecordMapper extends BaseMapper<MatchRecord> { }

PlayerMapperMatchEventMapper写法相同,只是泛型参数不同。MyBatis-Plus 的BaseMapper已经包含insertselectByIdselectListupdateById等常用方法,小规模项目几乎不需要写 XML SQL。

3.4 积分计算服务

积分计算是这套系统的业务核心。这里使用一个简化规则:

  • 达成率低于 70%:0 分
  • 达成率 70%(含)到 80%:1 分
  • 达成率 80%(含)到 90%:2 分
  • 达成率 90%(含)到 94%:3 分
  • 达成率 94%(含)到 97%:4 分
  • 达成率 97%(含)以上:5 分

基础积分算完后,再乘上歌曲难度等级系数。这里以歌曲等级 10 级作为基准,等级每高 1 级,系数增加 0.05,上限为 1.3。也就是说,一首 13 级歌曲拿到 5 分基础积分后,最终积分是 5 × 1.15。

package com.example.maimai.service; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.math.RoundingMode; @Service public class ScoreCalculator { public int calculateBasicPoints(BigDecimal achievementRate) { if (achievementRate == null) { return 0; } double rate = achievementRate.doubleValue(); if (rate < 70) { return 0; } else if (rate < 80) { return 1; } else if (rate < 90) { return 2; } else if (rate < 94) { return 3; } else if (rate < 97) { return 4; } else { return 5; } } public BigDecimal calculateTotalPoints(int basicPoints, int songLevel) { double coefficient = 1.0 + Math.max(0, songLevel - 10) * 0.05; if (coefficient > 1.3) { coefficient = 1.3; } BigDecimal ratePoint = BigDecimal.valueOf(basicPoints * coefficient); return ratePoint.setScale(2, RoundingMode.HALF_UP); } }

这种设计的好处是业务规则和控制器分离。如果赛事主办方调整了积分规则,只需要修改ScoreCalculator,接口层不用动。

3.5 业务层与控制器

设计一个成绩录入的 DTO,接收前端提交的数据。

package com.example.maimai.dto; import javax.validation.constraints.DecimalMax; import javax.validation.constraints.DecimalMin; import javax.validation.constraints.NotNull; import java.math.BigDecimal; public class RecordSaveDTO { @NotNull(message = "比赛ID不能为空") private Long matchId; @NotNull(message = "选手ID不能为空") private Long playerId; @NotNull(message = "歌曲名不能为空") private String songName; @NotNull(message = "歌曲等级不能为空") private Integer songLevel; @NotNull(message = "达成率不能为空") @DecimalMin(value = "0.00", message = "达成率不能小于0") @DecimalMax(value = "101.00", message = "达成率不能大于101") private BigDecimal achievementRate; @NotNull(message = "结算分数不能为空") private Integer songScore; // getter / setter 省略 }

业务层MatchRecordService负责保存成绩,并在保存前调用积分计算服务:

package com.example.maimai.service; import com.example.maimai.dto.RecordSaveDTO; import com.example.maimai.entity.MatchRecord; import com.example.maimai.mapper.MatchRecordMapper; import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.math.BigDecimal; import java.time.LocalDateTime; @Service public class MatchRecordService { @Resource private MatchRecordMapper matchRecordMapper; @Resource private ScoreCalculator scoreCalculator; public MatchRecord saveRecord(RecordSaveDTO dto) { MatchRecord record = new MatchRecord(); BeanUtils.copyProperties(dto, record); int basicPoints = scoreCalculator.calculateBasicPoints(dto.getAchievementRate()); BigDecimal totalPoints = scoreCalculator.calculateTotalPoints(basicPoints, dto.getSongLevel()); record.setBasicPoints(basicPoints); record.setTotalPoints(totalPoints); record.setCreateTime(LocalDateTime.now()); matchRecordMapper.insert(record); return record; } }

控制器层提供三个核心接口:新增选手、创建比赛、录入成绩并查询排行榜。

package com.example.maimai.controller; import com.example.maimai.dto.RecordSaveDTO; import com.example.maimai.entity.MatchEvent; import com.example.maimai.entity.MatchRecord; import com.example.maimai.entity.Player; import com.example.maimai.mapper.MatchEventMapper; import com.example.maimai.mapper.MatchRecordMapper; import com.example.maimai.mapper.PlayerMapper; import com.example.maimai.service.MatchRecordService; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import java.util.List; import java.util.Map; @RestController @RequestMapping("/api") public class MatchController { @Resource private PlayerMapper playerMapper; @Resource private MatchEventMapper matchEventMapper; @Resource private MatchRecordMapper matchRecordMapper; @Resource private MatchRecordService matchRecordService; @PostMapping("/player") public Player createPlayer(@RequestBody Player player) { playerMapper.insert(player); return player; } @GetMapping("/player") public List<Player> listPlayer(@RequestParam(required = false) String keyword) { if (keyword == null || keyword.trim().isEmpty()) { return playerMapper.selectList(null); } return playerMapper.selectList(new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<Player>() .like("nickname", keyword)); } @PostMapping("/match") public MatchEvent createMatch(@RequestBody MatchEvent matchEvent) { matchEventMapper.insert(matchEvent); return matchEvent; } @PostMapping("/record") public MatchRecord addRecord(@RequestBody RecordSaveDTO dto) { return matchRecordService.saveRecord(dto); } }

上面的接口没有对参数做复杂校验,也不包含统一异常处理。为了节省篇幅,示例刻意保持精简。实际项目中建议使用@Valid注解统一校验,并用@RestControllerAdvice捕获异常。

搭配一个排行榜查询接口。这里不直接写 SQL,而是先把某场比赛的所有记录查出来,在内存中聚合。因为单场比赛的数据量通常只有几百条,性能没有问题。

@GetMapping("/record/ranking") public List<Map<String, Object>> ranking(@RequestParam Long matchId) { List<MatchRecord> records = matchRecordMapper.selectList( new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<MatchRecord>() .eq("match_id", matchId) .orderByDesc("total_points") ); return records.stream().map(record -> { Map<String, Object> row = new java.util.HashMap<>(); row.put("recordId", record.getId()); row.put("songName", record.getSongName()); row.put("songLevel", record.getSongLevel()); row.put("achievementRate", record.getAchievementRate()); row.put("songScore", record.getSongScore()); row.put("totalPoints", record.getTotalPoints()); return row; }).collect(java.util.stream.Collectors.toList()); }

严格来说,这个接口返回的是“单轮成绩按分数排序”,不是跨歌曲累计成绩。如果要按选手聚合累计积分,需要按playerId分组求和。可以在 Service 中增加一个aggregateByPlayer方法:

public List<Map<String, Object>> aggregateRanking(Long matchId) { List<MatchRecord> records = matchRecordMapper.selectList( new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<MatchRecord>() .eq("match_id", matchId) ); Map<Long, BigDecimal> playerTotalMap = new java.util.HashMap<>(); Map<Long, Integer> playerCountMap = new java.util.HashMap<>(); for (MatchRecord record : records) { playerTotalMap.merge(record.getPlayerId(), record.getTotalPoints(), BigDecimal::add); playerCountMap.merge(record.getPlayerId(), 1, Integer::sum); } return playerTotalMap.entrySet().stream() .sorted(Map.Entry.<Long, BigDecimal>comparingByValue().reversed()) .map(entry -> { Long playerId = entry.getKey(); Map<String, Object> row = new java.util.HashMap<>(); row.put("playerId", playerId); row.put("totalPoints", entry.getValue()); row.put("roundCount", playerCountMap.getOrDefault(playerId, 0)); return row; }) .collect(java.util.stream.Collectors.toList()); }

这种方式不适合海量数据,但作为几十人规模的比赛积分榜完全够用。如果后续要扩展到上千人,可以改成 SQLGROUP BY聚合,或者在积分表里增加累计字段。

4. 前端页面开发

4.1 页面整体架构

前端不搞复杂工程,直接把页面放在src/main/resources/static/index.html,后端启动后访问http://localhost:8080/即可打开。页面使用 Vue 2 CDN 和 Axios CDN,省去 npm 构建步骤。

页面整体分为三个区域:

  • 选手管理区:新增选手、查询选手列表。
  • 比赛管理区:创建比赛、选择当前比赛。
  • 成绩录入区:选择选手、填写歌曲和达成率,提交后展示排行榜。

考虑到示例页面较长,这里只展示关键代码结构:引入 CDN 资源、创建 Vue 实例、定义初始数据和方法。

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>舞萌比赛积分管理</title> <style> body { font-family: "Microsoft YaHei", Arial, sans-serif; margin: 30px auto; max-width: 1000px; background: #f7f9fc; } .card { background: #fff; border-radius: 8px; padding: 16px; margin-bottom: 16px; box-shadow: 0 2px 8px rgba(0,0,0,0.06); } .input { margin: 6px 8px 6px 0; padding: 8px 10px; border: 1px solid #d9d9d9; border-radius: 4px; } .btn { padding: 8px 14px; margin-right: 6px; border-radius: 4px; border: none; cursor: pointer; } .btn-primary { background: #3b82f6; color: #fff; } table { width: 100%; border-collapse: collapse; margin-top: 12px; } th, td { border: 1px solid #e5e7eb; padding: 8px; text-align: center; font-size: 14px; } th { background: #f3f4f6; } </style> </head> <body> <div id="app"> <!-- 页面内容通过 Vue 组件渲染 --> </div> <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/axios@1.6.8/dist/axios.min.js"></script> <script> new Vue({ el: '#app', data: { player: { nickname: '', realName: '', phone: '' }, playerList: [], match: { eventName: '', eventDate: '', eventType: 'SINGLE' }, matchList: [], currentMatchId: null, record: { playerId: null, songName: '', songLevel: 10, achievementRate: 0, songScore: 0 }, rankingList: [] }, methods: { loadPlayers() { axios.get('/api/player').then(res => { this.playerList = res.data; }); }, loadMatches() { axios.get('/api/match').then(res => { this.matchList = res.data; if (this.matchList.length > 0 && !this.currentMatchId) { this.currentMatchId = this.matchList[0].id; this.loadRanking(); } }); }, createPlayer() { axios.post('/api/player', this.player).then(() => { alert('选手添加成功'); this.player.nickname = ''; this.player.realName = ''; this.player.phone = ''; this.loadPlayers(); }); }, createMatch() { axios.post('/api/match', this.match).then(() => { alert('比赛创建成功'); this.loadMatches(); }); }, addRecord() { if (!this.currentMatchId || !this.record.playerId) { alert('请先选择比赛和选手'); return; } const data = { matchId: this.currentMatchId, playerId: this.record.playerId, songName: this.record.songName, songLevel: this.record.songLevel, achievementRate: this.record.achievementRate, songScore: this.record.songScore }; axios.post('/api/record', data).then(() => { alert('成绩录入成功'); this.record.songName = ''; this.record.songLevel = 10; this.record.achievementRate = 0; this.record.songScore = 0; this.loadRanking(); }); }, loadRanking() { axios.get('/api/record/ranking', { params: { matchId: this.currentMatchId } }).then(res => { this.rankingList = res.data; }); } }, mounted() { this.loadPlayers(); this.loadMatches(); } }); </script> </body> </html>

这里需要补一个/api/match的 GET 接口,直接返回matchEventMapper.selectList(null)即可。前端启动后,选手列表、比赛列表都会自动加载。

4.2 选手信息管理页面

#app内部渲染一个选手管理卡片。新增选手表单包含昵称、姓名、手机号三个输入框,下面辅助一个简单的搜索框。

<div class="card"> <h3>选手管理</h3> <input class="input" placeholder="选手昵称" v-model="player.nickname" /> <input class="input" placeholder="真实姓名(可选)" v-model="player.realName" /> <input class="input" placeholder="手机号(可选)" v-model="player.phone" /> <button class="btn btn-primary" @click="createPlayer">添加选手</button> <input class="input" placeholder="搜索选手昵称" v-model="searchNickname" @input="searchPlayer" /> <table> <tr> <th>ID</th> <th>昵称</th> <th>姓名</th> <th>手机号</th> </tr> <tr v-for="item in playerList" :key="item.id"> <td>{{ item.id }}</td> <td>{{ item.nickname }}</td> <td>{{ item.realName || '-' }}</td> <td>{{ item.phone || '-' }}</td> </tr> </table> </div>

搜索功能不需要请求后端,直接在前端用filter过滤即可。如果选手量很大,再改成后端分页查询。

4.3 比赛成绩录入与排行榜

在录入成绩前,先加载比赛列表并提供一个下拉选择框。

<div class="card"> <h3>比赛成绩录入</h3> <select v-model="currentMatchId" @change="loadRanking"> <option v-for="m in matchList" :value="m.id">{{ m.eventName }}({{ m.eventDate }})</option> </select> <div style="margin-top:10px;"> <select v-model="record.playerId"> <option v-for="p in playerList" :value="p.id">{{ p.nickname }}</option> </select> <input class="input" placeholder="歌曲名" v-model="record.songName" /> <input class="input" type="number" placeholder="歌曲等级" v-model.number="record.songLevel" /> <input class="input" type="number" step="0.01" placeholder="达成率" v-model.number="record.achievementRate" /> <input class="input" type="number" placeholder="结算分数" v-model.number="record.songScore" /> <button class="btn btn-primary" @click="addRecord">录入成绩</button> </div> <table> <tr> <th>歌曲</th> <th>等级</th> <th>达成率</th> <th>结算分</th> <th>加权积分</th> </tr> <tr v-for="row in rankingList" :key="row.recordId"> <td>{{ row.songName }}</td> <td>{{ row.songLevel }}</td> <td>{{ row.achievementRate }}</td> <td>{{ row.songScore }}</td> <td>{{ row.totalPoints }}</td> </tr> </table> </div>

如果使用前面的aggregateRanking接口,表格展示的字段要改成playerIdtotalPointsroundCount。实际开发中可以同时提供两个接口:一个是单轮成绩明细,一个是选手累计排名。

5. 运行与验证

5.1 启动后端服务

application.yml中确认数据库账号密码无误后,直接在 IDEA 中运行MaimaiScoreApplication。启动成功后控制台会打印 Spring Boot 的 Banner 和端口信息。

如果使用命令行启动,可以执行:

mvn spring-boot:run

启动后访问http://localhost:8080/,如果能看到前端页面,说明工程基本没问题。

5.2 使用 curl 验证接口

不依赖页面,先用 curl 验证后端接口是否正常。新增一名选手:

curl -X POST http://localhost:8080/api/player \ -H "Content-Type: application/json" \ -d '{"nickname":"纱露朵娃娃","realName":"","phone":""}'

预期返回 JSON 中包含选手 ID 和昵称:

{ "id": 1, "nickname": "纱露朵娃娃", "realName": "", "phone": "", "createTime": "2025-01-01T12:00:00" }

然后创建一场比赛,再录入成绩:

curl -X POST http://localhost:8080/api/match \ -H "Content-Type: application/json" \ -d '{"eventName":"福州门店积分赛","eventDate":"2025-01-05","eventType":"SINGLE","status":1}'
curl -X POST http://localhost:8080/api/record \ -H "Content-Type: application/json" \ -d '{"matchId":1,"playerId":1,"songName":"Grievous Lady","songLevel":14,"achievementRate":98.50,"songScore":1010000}'

录入成功后,totalPoints应该由后端计算出来。按照前面的规则,达成率 98.5% 对应基础积分 5 分,歌曲等级 14 的系数是 1.2,最终积分是 6.00。

5.3 页面联调验证

页面加载后,先添加一名选手,再创建一场比赛。接着选择比赛,在成绩录入区选择刚创建的选手,填写歌曲名、等级、达成率、结算分,点击“录入成绩”。如果一切正常,下方表格会立即出现该条成绩,并显示加权积分。

如果点击按钮后没有反应,打开浏览器开发者工具中的 Network 面板,重点检查请求状态码。如果是 404,检查接口路径是否写错;如果是 500,查看后端控制台异常栈,最常见的是数据库表没建或字段映射错误。

6. 常见问题与排查思路

在开发这套系统的过程中,有几个高频问题值得单独拿出来讲。

问题现象常见原因解决思路
启动时报数据库连接失败数据库未创建、账号密码错误、驱动配置不对先执行建表 SQL,再检查application.yml的连接配置
接口返回 404请求路径与@RequestMapping不一致,或前端路径拼错检查后端控制类路径,对比浏览器 Network 中的实际请求地址
前端页面样式错乱CDN 资源加载失败,或离线环境将 Vue 和 Axios 文件下载到本地 static 目录引入
中文乱码数据库字符集不是 utf8mb4,或连接 URL 缺少编码参数建库时指定 utf8mb4,连接 URL 增加characterEncoding=utf8
达成率字段插入失败前端传入字符串,映射到 BigDecimal 时类型异常前端使用v-model.number,后端 DTO 使用BigDecimal
排行榜顺序不对前端直接使用单轮成绩明细接口,没有按选手聚合改用聚合接口,或按total_points倒序排列
MyBatis-Plus 方法报空指针实体类没有 setter,或未添加@Mapper注解检查实体类 getter/setter,检查 Mapper 接口注解

如果出现“Could not create connection to database server”这一类错误,不要急着改代码。先确认 MySQL 服务是否启动,再用数据库客户端尝试连接,最后才检查 Spring Boot 配置。

7. 最佳实践与工程建议

7.1 数据一致性与事务控制

成绩录入涉及两个动作:写入match_record表,同时根据积分规则计算积分。如果后续接入“选手累计积分”字段,还需要更新选手总积分。这时必须用事务保证一致性。

MatchRecordService的保存方法上增加@Transactional注解,是一个简单有效的做法。

@Transactional(rollbackFor = Exception.class) public MatchRecord saveRecord(RecordSaveDTO dto) { // 保存成绩并计算积分 }

实际赛事中,成绩录入员可能会误操作,把选手的达成率填错。系统层面至少要支持“修改”和“删除”接口,或者提供“成绩作废”逻辑。不能只允许新增,否则后期更正成本很高。

7.2 权限与安全边界

这套系统默认是赛事管理员使用,所以没有引入登录认证。但如果部署到公网,任何人都可能访问新增、删除接口。最小可落地的方案是加一层简单的管理员密码校验,例如接口请求头要求携带X-Admin-Token

在 Spring Boot 中可以使用拦截器统一校验,避免在每个 Controller 里重复写判断逻辑。更完整的方案是引入 Spring Security,但小规模工具项目不推荐一开始就上重型安全框架。

另外一个容易被忽略的点是 SQL 注入。MyBatis-Plus 的QueryWrapper在传值时会使用占位符,普通查询不会产生注入风险。但如果自行拼接 SQL,必须使用#{},不要使用${}

7.3 可维护性与后续扩展

项目规模不大,但代码结构上仍然应该分层。目前示例只展示了 Controller、Service、Mapper 三层,如果继续扩展,建议增加 DTO/VO 层和统一的异常响应结构。

后续可以从下面几个方向继续完善:

  • 增加多轮赛制支持,把每轮成绩作为单独记录,并提供轮次字段。
  • 增加歌曲谱面定数管理,把歌曲等级和定数维护为独立表。
  • 支持从 CSV 批量导入成绩。
  • 增加选手排名变化曲线,用图表展示。
  • 将前端工程化,使用 Vue 3 + Vite + Element Plus 重构。
  • 引入登录鉴权和操作日志,记录谁在什么时间修改了成绩。
  • 增加数据备份脚本,比赛结束后自动导出 SQL。

这套系统最值得保留的设计是把积分计算从 Controller 中剥离出来,后续赛事规则调整时,只需要维护ScoreCalculator和配置表,而不需要改动接口和页面。类似的小型赛事工具,往往就是从一张成绩记录表开始的。先把数据结构和录入流程跑通,再逐步加权限、加推送、加可视化,项目的复杂度就会始终处于可控范围内。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 15:37:58

接手陌生仓库,先看交互式架构图再读码

1. 引言 接手一个陌生的代码仓库&#xff0c;往往是一场噩梦&#xff1a;模块关系不清、调用链混乱、注释过时&#xff0c;只能硬着头皮从入口函数一步步追踪。tt-a1i/archify 正是为解决这个痛点而生——它不要求你先读懂源码&#xff0c;而是先为你生成一张可交互的架构图&am…

作者头像 李华
网站建设 2026/9/9 15:38:02

2026机器学习学习路线:从基础到大模型的七个核心知识点实践方法

很多人在 2026 年重新打开收藏夹&#xff0c;原因往往不是“突然想学习了”&#xff0c;而是大模型时代带来的焦虑&#xff1a;以前学的机器学习还有用吗&#xff1f;深度学习是不是已经过时&#xff1f;神经网络算法还要不要从头看&#xff1f;强化学习是不是只属于 AlphaGo&a…

作者头像 李华
网站建设 2026/9/7 9:20:36

Hadoop MapReduce实现电影用户性别预测:从数据清洗到模型应用

简介&#xff1a;本资源是一套基于Hadoop生态实现的电影网站用户性别预测项目源码&#xff0c;面向大数据初学者与高校课程实践者&#xff0c;聚焦生活娱乐场景下的用户画像建模问题&#xff0c;适用于MapReduce编程、数据清洗与分类算法&#xff08;如KNN&#xff09;的综合训…

作者头像 李华
网站建设 2026/9/7 4:58:47

STM32 ADC注入组直接寄存器访问:原理、代码与实战优化

用着好好的HAL库&#xff0c;为什么我还要回头去翻寄存器&#xff1f;这问题我最近被问过好几次了&#xff0c;尤其是一些做电机控制和电源管理的朋友。他们用STM32的ADC注入组&#xff08;Injection Group&#xff09;做波形的精准采样&#xff0c;但发现HAL库提供的接口在高频…

作者头像 李华
网站建设 2026/9/8 4:36:28

STM32水质检测系统开发:从ADC采集到滤波标定的完整实战

简介&#xff1a;本资源是一套基于STM32F103系列单片机开发的C语言水质监测系统完整工程&#xff0c;面向嵌入式初学者与课程设计、毕业设计及物联网实践项目开发者&#xff0c;解决水体PH值、TDS&#xff08;总溶解固体&#xff09;及温度三项核心参数的实时采集、本地显示与远…

作者头像 李华
网站建设 2026/9/8 5:13:30

基于理想电流源的Multistage Doherty功放ADS仿真方法

简介&#xff1a;本资源是面向射频工程师与微波电路设计学习者的ADS仿真工程包&#xff0c;聚焦多级高回退Doherty功率放大器&#xff08;Multistage Doherty&#xff09;的理论建模与理想电流源实现方案&#xff0c;重点解决传统Doherty在宽功率回退区效率塌陷问题。资源包含9…

作者头像 李华