ARTICLE DETAIL

资讯详情

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

Java+MySQL实时评分系统设计与实现:从建表到部署避坑

Java+MySQL实时评分系统设计与实现:从建表到部署避坑 简介这是一个基于Java开发的实时评分系统毕业设计项目面向需要完成毕设或学习SpringBoot与MyBatis整合开发的在校生。系统解决传统赛事评分中手写打分、人工计算耗时且易出错的问题支持评委手机扫码实时评分并通过大屏即时展示得分契合现代比赛场景。压缩包共61个文件其中48个Java源文件承载核心业务逻辑6个XML与1个YML文件用于框架配置SQL脚本提供数据库初始数据说明文档帮助快速理解项目结构整体仅138KB便于部署和二次开发。开发环境采用JDK1.8、SpringBoot 2.3.7、MyBatis、MySQL 5.7及Redis 3.0等主流技术栈适合学习企业级分层架构和业务集成。目前已有355人下载学习可作为毕业设计或课程实训的完整参考方案直接运行并在此基础上扩展功能。1. 实时评分系统用什么技术做为什么 Java SQL 是毕设的稳妥选择答辩前夜最常听到的需求就是“评分得实时”评委一打分大屏这边马上能看到总分和排名变化。这个基于 Java 开发的实时评分系统源码加 SQL 数据库加项目说明文档的毕设项目就是把这条链路完整落地的东西。它解决的不是“能打分”的问题而是“同一秒内几十个人提交评分系统还能快速写入、校验、汇总并刷新结果”的问题。适合做 Java 课程设计或毕业设计的同学也适合想在公司内部搭一套活动评分小工具的人。后面我会从架构、表结构、核心代码、部署到踩坑完整拆一遍这套方案。2. 先定架构再写代码单机 Tomcat MySQL 如何支撑实时评分2.1 实时评分系统的两条链路写入链路和汇总链路实时评分系统的本质可以拆成两条链路。第一是写入链路评委在前端点击提交HTTP 请求到达控制器层控制器解析参数并做基础校验然后调服务层把评分记录写入数据库。第二是汇总链路前端页面定时向服务端要最新榜单服务端执行聚合计分算出每个评分项的总分或平均分再按分数排序返回给页面。很多人一听到“实时”就以为必须上 WebSocket 或者消息队列这是典型的过度设计。拿实际场景算一笔账课堂互评最多五六十人比赛打分通常就十个评委就算每秒同时提交二十条评分MySQL 单机写二十行记录完全没压力。真正的实时瓶颈不在网络推送而在写入链路是否抢锁、汇总查询是否走了全表扫描。把这两条链路想清楚后面写代码就不会乱。2.2 为什么单机 Tomcat MySQL 就够了这套毕设源码最常见的默认架构就是 Tomcat MySQL我不建议你为了看起来高级去改造成分布式。评分场景的峰值并发很低单机 Tomcat 配一个连接池就能顶住MySQL 的 InnoDB 引擎在几十 QPS 下根本不存在性能问题。面试或答辩被问到“为什么不用 Redis / 消息队列”时你可以正面回答实时评分需要的是低写入延迟和数据一致性不是高吞吐削峰消息队列反而会让“评委提交到榜单变化”这条链路多一跳延迟更高。数据库连接池建议用 HikariCP配置核心就三个参数maximumPoolSize 设 10 到 20minimumIdle 设 5connectionTimeout 设 30000。这个配置足够支撑几十个评委同时提交。连接池不是越大越好最大连接数设成 100 反而会让 MySQL 频繁切换线程体现不出实时性。2.3 解压 ZIP 后先看什么源码包的目录结构拿到 ZIP 包解压后第一步不是急着启动而是先看目录结构搞清楚里面装了什么。常见做法是分成四个部分SQL 数据库脚本、Java 后端源码、前端页面、项目说明文档。先打开说明文档里的“运行环境”和“部署步骤”章节再对照源码找对应文件。score-system/ ├── src/ │ ├── main/ │ │ ├── java/com/example/score/ │ │ │ ├── controller/ScoreController.java │ │ │ ├── service/ScoreService.java │ │ │ └── dao/ScoreDao.java │ │ └── resources/ │ │ ├── db.properties │ │ └── mybatis-config.xml │ └── webapp/ │ ├── WEB-INF/web.xml │ └── index.jsp ├── sql/score_db.sql ├── 项目说明文档.docx └── README.md这个结构对应的是经典 SSM 或 Spring Boot 分层controller 负责接收请求service 放业务逻辑dao 管数据库增删改查。sql 目录下只有一个建表脚本说明所有表都在一个文件里建完。如果源码根目录有 pom.xml这是 Maven 项目依赖会自动下载如果只有 lib 目录就手动导入 jar 包。判断项目类型的这个细节直接决定你能不能启动成功。3. 数据库设计是评分系统的地基建表 SQL、索引与事务边界3.1 三张核心表用户表、评分项表、评分记录表实时评分系统的数据模型不复杂核心就三张表。用户表存评委和选手评分项表存“仪表形象”“回答内容”“创新性”这类打分维度评分记录表存每一次具体打分。评分记录表是整张表设计的重心它同时关联用户和评分项一条记录代表“某个评委给某个评分项打了多少分”。表名作用关键字段score_user存放评委和选手账号id, username, rolescore_item存放评分项定义id, name, max_scorescore_record存放每一条评分id, item_id, user_id, score, create_time分数建议用 DECIMAL(5,2)不要用 INT。很多评分规则允许 8.5 分这种小数用整数类型就得整套改逻辑。role 字段用 TINYINT 区分评委和选手比用字符串更省空间也方便 Java 里用枚举判断。score_record 表必须保留 item_id 和 user_id 两个外键字段否则无法回答“某个评委是否已经评过分”以及“某个评分项一共多少分”这两个核心问题。3.2 建表 SQL 与关键索引直接看最常用的建表脚本这是一套能直接跑通的最小闭环。放在 SQL 文件最前面的是建库语句后面依次是三张表。CREATE DATABASE IF NOT EXISTS score_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE score_db; CREATE TABLE score_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, role TINYINT NOT NULL DEFAULT 1 COMMENT 1评委 2选手, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE score_item ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, max_score DECIMAL(5,2) NOT NULL DEFAULT 10.00, status TINYINT NOT NULL DEFAULT 1 COMMENT 1启用 0停用 ) ENGINEInnoDB; CREATE TABLE score_record ( id INT PRIMARY KEY AUTO_INCREMENT, item_id INT NOT NULL, user_id INT NOT NULL, score DECIMAL(5,2) NOT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_item_user (item_id, user_id), KEY idx_item (item_id) ) ENGINEInnoDB;这段 SQL 里最关键的是两处索引UNIQUE KEY uk_item_user 和 KEY idx_item。前者是数据库层的防重复评分保障同一个评委对同一个评分项只能插入一条记录重复插入会被 MySQL 拒绝后者是给汇总查询准备的按评分项聚合算总分时能走索引扫对应记录不然表数据一多就会出现慢 SQL。你可以在项目里测试一下不加 idx_item 时 SELECT SUM 扫全表加完之后执行计划里能看到走了索引效果非常明显。3.3 事务边界与 SQL 注入防护评分提交不总是一条 INSERT 那么简单。如果项目设计里有“评分项总分”这种冗余字段每次打分要同时做两件事插入评分记录更新评分项的总分。这两个动作必须放同一个事务里要么都成功要么都回滚。Spring Boot 里在 Service 方法上加 Transactional 注解JDBC 原生写法就手动 setAutoCommit(false)全部执行完再 commit。SQL 注入是这类项目最容易翻车的地方。评分项的 ID 是用户传入的如果把参数直接拼进查询语句别人构造一个 itemId1 OR 11 就能把整个表的数据拖出来。正确的写法是 MyBatis 里用 #{}JDBC 里用 PreparedStatement 的参数占位。// 错误拼接字符串存在 sql 注入风险 String sql SELECT * FROM score_item WHERE id request.getParameter(id); // 正确PreparedStatement 占位符 String sql SELECT * FROM score_item WHERE id ?; PreparedStatement ps conn.prepareStatement(sql); ps.setInt(1, Integer.parseInt(request.getParameter(id)));PreparedStatement 不只是防注入同一个 SQL 语句可以复用执行计划批量插入评分记录时性能也比 Statement 好。setInt 和 setString 要对应数据库字段类型ID 字段是 INT 就用 setIntusername 是 VARCHAR 就用 setString乱用类型会报数据类型转换异常。4. 实时评分核心逻辑的 Java 实现提交、防重、汇总与推送4.1 实时评分的两种实现SQL 聚合与内存累加后端算实时分数有两种路径。第一种是 SQL 聚合每次查榜单都用 SUM、AVG 现算代码简单数据永远和数据库一致第二种是内存累加用 ConcurrentHashMap 缓存每个评分项的总分打分请求进来直接改内存值再异步写库响应更快但宕机可能丢数据。毕设选 SQL 聚合更稳妥。理由很直接评分记录最多几千条到几万条聚合查询在索引帮助下毫秒级返回你根本感觉不到性能问题。内存累加要处理缓存和数据库的一致性一旦进程崩溃总分和明细对不上答辩时很难自圆其说。实时性用查询本身来保证比引入缓存黑匣子更可靠。4.2 评分提交接口校验、防重、入库核心接口就是提交评分这一段把校验、防重、写库串在一起。以 Spring Boot 的写法为例方法接收一个 JSON 对象包含评分项 ID、评委用户 ID 和分数三个字段。PostMapping(/api/score/submit) ResponseBody public MapString, Object submit(RequestBody ScoreForm form) { // 1. 参数校验分数不能为负也不能超过该评分项的上限 ScoreItem item scoreItemMapper.selectById(form.getItemId()); if (item null || form.getScore() null || form.getScore() 0 || form.getScore() item.getMaxScore()) { return errorResult(分数超出评分项上限); } // 2. 防重校验同一个评委对同一个评分项只能打分一次 int exists scoreRecordMapper.countByItemAndUser(form.getItemId(), form.getUserId()); if (exists 0) { return errorResult(该评委已对此项评分); } // 3. 插入评分记录 scoreRecordMapper.insert(new ScoreRecord(form.getItemId(), form.getUserId(), form.getScore())); // 4. 返回最新排名 return okResult(scoreRecordMapper.selectRank()); }第一步先查评分项拿到 max_score这是校验分数合法性的依据第二步先查后插配合表设计里的唯一索引双保险防止前端绕过按钮直接发请求第三步插入记录第四步立刻返回最新排行。这里有个容易忽略的参数问题RequestBody 接收 JSON 时前端必须把请求头的 Content-Type 设置成 application/json否则框架会报 HttpMediaTypeNotSupportedException表单提交和 JSON 提交在 Spring Boot 里是两套不同的参数解析逻辑。4.3 实时汇总与推送轮询还是长连接榜单刷新有两种方案WebSocket 长连接服务和前端定时轮询。实时评分系统用轮询就够了前端页面加载后每隔 3 秒请求一次榜单接口后端返回最新排名。WebSocket 的实时性确实更好但要在 Java 端维护连接状态、处理断线重连代码量直接翻倍只有在弹幕、行情这类高并发持续推送场景才值得。后端汇总查询是整条实时链路的关键SQL 写得好不好直接决定榜单刷得快不快。SELECT si.id, si.name, COUNT(sr.id) AS score_count, IFNULL(SUM(sr.score), 0) AS total_score FROM score_item si LEFT JOIN score_record sr ON si.id sr.item_id GROUP BY si.id, si.name ORDER BY total_score DESC;LEFT JOIN 在这里有讲究一个评分项如果还没有任何人打分RIGHT JOIN 或 INNER JOIN 会让它从榜单上消失而 LEFT JOIN 能保证它显示出来分数是 0。IFNULL 是 MySQL 的兜底函数SUM 没有数据时返回 NULL页面展示就会变成 0。ORDER BY total_score DESC 让最高分排最前面。这个查询配合 score_record 表上的 idx_item 索引评分记录在百万行以内都很快真正让它变慢的场景是缺索引导致的全表扫描而不是查询本身。5. 把项目跑起来从 ZIP 解压到部署 MySQL 和 Tomcat 的完整命令5.1 环境版本搭配JDK 8 MySQL 5.7/8.0 Tomcat 8.5/9很多毕设源码跑不起来版本不一致占一半原因。这套项目最适合的环境组合是 JDK 1.8、MySQL 5.7 或 8.0、Tomcat 8.5 或 9。JDK 版本尤其敏感老项目用 openjdk 17 编译经常报 javac 错误因为旧版代码里调用的某些内部 API 在高版本被移除了。软件推荐版本原因JDK1.8大部分毕设源码基于 JDK 8 语法和 API 编写MySQL5.7 或 8.0兼容性最好8.0 使用新驱动名Tomcat8.5 或 9对应 Servlet 3.1 / 4.0足够支持 Spring BootMaven3.6.x稳定版本依赖解析正常先检查本机环境命令窗口里依次执行三条命令确认版本在推荐范围内。java -version mysql --version mvn -version如果 java -version 显示的是 17 或 21需要额外安装 JDK 8 并配置 JAVA_HOME 指向它不然后面编译时会碰到一长串报错。mysql 版本显示 5.7 以上的数字就正常。mvn 没有安装的话可以直接用项目里自带的 mvnw 脚本Maven Wrapper替代。5.2 导入 SQL 数据库字符集和常见报错数据库导入是启动项目前的关键步骤。直接在命令行里把建表脚本灌进 MySQL注意先建库再导表库名要和后端配置保持一致。mysql -u root -p -e CREATE DATABASE IF NOT EXISTS score_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; mysql -u root -p score_db sql/score_db.sql第一条命令创建数据库同时指定 utf8mb4 字符集这是防止中文乱码的第一道闸门。第二条命令把 SQL 文件里的建表语句按顺序执行 是 shell 重定向相当于把文件内容逐行喂给 mysql 客户端。如果 SQL 文件开头已经包含了 CREATE DATABASE 语句导入时可以直接省略第一条命令但要注意库名是否一致否则项目启动后连接的是空库。报 ERROR 1064 语法错误时先检查 SQL 文件里有没有 MySQL 8 不支持的旧语法报 ERROR 1366 字符集错误时重新用 utf8mb4 建库再导。5.3 改数据库连接配置并启动项目数据库导入成功后打开源码里的数据库连接配置文件。Spring Boot 项目通常是 application.properties 或 application.yml老项目是 db.properties。jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/score_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8mb4 jdbc.usernameroot jdbc.password你自己的密码driver 这一行是重灾区。MySQL 5.7 时代驱动类名是 com.mysql.jdbc.DriverMySQL 8 之后改成 com.mysql.cj.jdbc.Driver用旧名字会报 ClassNotFoundException。url 里的 serverTimezoneAsia/Shanghai 不加的话MySQL 8 会报 CST 时区异常characterEncodingutf8mb4 配合建库语句里的字符集能保证中文评分项名称不再乱码。username 和 password 必须改成你自己 MySQL 的账号密码。改完配置后启动项目。mvn spring-boot:run看到 Tomcat started on port(s): 8080 这行日志就说明启动成功浏览器访问 http://localhost:8080 进入系统。如果日志里提示端口被占用到配置文件里把 server.port 改成 8081 之类的空端口再启动。6. 避坑与验证答辩演示前必须处理的 5 个坑6.1 五个必踩的坑现象同一个评委对同一个评分项可以重复打分总分越点越高。原因前端按钮没有在提交后置灰后端也没有校验是否已评分。解决后端先查 score_record 表判断记录是否存在同时依赖表设计里的 uk_item_user 唯一索引兜底做到双保险。现象页面上中文名字和评分项名称变成问号。原因数据库默认字符集是 latin1或者连接串没指定 characterEncoding。解决建库时用 utf8mb4连接串加上 characterEncodingutf8mb4 并重启项目。现象启动时报 ClassNotFoundException com.mysql.jdbc.Driver。原因项目用的是 MySQL 8 驱动类名已经改成 com.mysql.cj.jdbc.Driver。解决改配置文件里的 driver 类名同时确认 pom.xml 里引用的 mysql-connector-java 版本是 8.0.x。现象Tomcat 或 Spring Boot 启动失败提示端口被占用。解决Windows 下用 netstat -ano | findstr 8080 查占用进程macOS / Linux 用 lsof -i:8080找到对应进程结束掉或者直接换端口。现象打完分榜单没反应手动刷新页面才看到新排名。原因前端没有做定时刷新或者后端聚合查询走了缓存。解决前端在页面加载后启动 setInterval每 3 秒请求一次榜单接口后端如果用缓存存排名缓存过期时间设 1 秒或不缓存。6.2 演示前的验证技巧演示之前用一个最小场景完整走一遍准备 3 个评委账号和 5 个评分项让 3 个评委依次给 5 个评分项打分总共提交 15 条记录每提交一条就观察榜单总分和排名有没有变化。实时评分系统的判断标准就一条提交后 1 秒内榜单更新。拿两台设备同时打开页面提交检查会不会出现重复评分和界面报错。答辩前还可以临时打开 MySQL 的慢查询日志观察以 score_record 表为核心的 SQL 有没有超过 1 秒的如果有就检查是不是少了 idx_item 索引。我自己第一次部署这类项目在端口和 SQL 驱动名上耗了半小时后来养成一个习惯拿到源码先看说明文档里的环境步骤再导数据再启动。希望帮到你。本文还有配套的精品资源点击获取
返回列表