简介:面向赛事评分场景的Java实时评分系统毕业设计项目,针对传统手写评分、人工计分慢且易错的问题,利用大屏展示、手机扫码与实时计算,提供一套从评分到结果展示的完整方案。压缩包内共61个文件,体积仅138KB,包含48个Java源码文件,覆盖实体类、控制器、服务层与Mapper接口,另有6个XML文件提供MyBatis映射与相关配置,1个SQL脚本用于初始化数据库,以及YML配置文件和Markdown项目说明文档,目录分层清晰,便于按模块查阅。目前已有354人学习下载,适合Java方向毕业设计、课程设计,或作为SpringBoot集成MyBatis、Redis的实战练习。项目基于JDK1.8、SpringBoot2.3.7、MyBatis、Redis等技术栈,源码可直接运行,数据库脚本与说明文档齐全,能帮助读者理解实时评分中评委端扫码、服务端计分、大屏同步展示的关键逻辑,并快速复用为其他实时展示类系统框架,是一份结构完整、上手门槛低的毕设项目参考资料。
1. 基于Java的实时评分系统,毕设项目最容易被低估的三件事
如果只把“基于java开发的实时评分系统源码+sql数据库+项目说明文档”当成一个普通的课程作业压缩包,那就低估了这类项目在面试和答辩里的分量。实时评分系统的核心不是CRUD,而是“实时”两个字——评分数据从产生到展示,延迟必须压到秒级甚至毫秒级,这背后牵扯到WebSocket推送、并发写入、数据库事务边界和缓存策略。对于Java方向的毕业生来说,这是少数能同时展示后端基本功和中间件认知的选题。
这类项目的典型场景是课堂互动评分、比赛打分、活动投票,数据特征是小而高频:单次写入量不大,但写入频率高,读多写多且实时性要求强。很多初学者把评分做成了“提交后刷新页面看结果”,这不算实时,只能算“事后查询”。真正能拿得出手的实时评分系统,至少要解决三个问题:评分事件如何低延迟推送到大屏或客户端,高并发下评分数据如何不丢不重,以及数据库结构怎么设计才能既支持实时统计又支持后期回溯分析。
这篇博文就顺着这三个问题展开,从技术选型讲到数据库建表,从WebSocket推送讲到项目说明文档的写作套路,最后给出演示技巧和答辩准备。整条链路打通之后,你会发现这个毕设项目不是“做完就扔”,而是能录入简历、能聊出深度的作品集项目。
2. 技术选型与项目骨架:为什么实时评分系统首选Spring Boot前后端分离
2.1 技术栈的组合逻辑:Spring Boot + MySQL + WebSocket足够覆盖九成需求
实时评分系统的技术选型不需要追新,但要有明确的选择理由。后端框架推荐Spring Boot,原因很直接:自动配置减少繁琐的XML配置,内嵌Tomcat让部署变简单,而且Spring家族的WebSocket支持非常成熟。持久层用MyBatis-Plus或Spring Data JPA都可以,前者写SQL更直观,适合需要手动调优评分统计场景的毕设项目;后者开发速度快,但复杂统计查询要写JPQL或原生SQL,略绕。数据库用MySQL,InnoDB引擎支持行级锁和事务,能应对评分场景的并发写。如果觉得单机MySQL不够,可以引入Redis做评分计数缓存,但这属于加分项,不是必选项。
前端骨架建议用Vue 3 + Element Plus,或者更轻量的原生HTML + WebSocket客户端。毕设答辩时,答辩老师更关心数据怎么流转,而不是前端用了多炫酷的框架。常见做法是前后端分离:Vue项目负责页面展示和WebSocket连接,Spring Boot提供REST API和WebSocket端点。
项目结构: rating-system/ ├── pom.xml ├── src/main/java/com/example/rating/ │ ├── controller/ # REST接口 + 页面跳转 │ ├── service/ # 业务逻辑,评分事务边界 │ ├── mapper/ # MyBatis-Plus的Mapper接口 │ ├── model/ # 实体类,对应数据库表 │ ├── config/ # WebSocket、跨域等配置 │ └── websocket/ # WebSocket端点和处理逻辑 ├── src/main/resources/ │ ├── application.yml # 数据源、端口配置 │ └── mapper/ # XML形式的SQL语句 └── sql/ # 数据库建表脚本和初始化数据这个结构的好处是职责清晰,mapper层和service层分离,答辩时能清楚说明每一层的职责。Controller只做参数接收和响应封装,Service层处理评分业务规则,Mapper层跟数据库打交道。WebSocket端点是独立包,不跟REST接口混在一起,代码可读性更高。
2.2 三个必调参数:数据库连接池、WebSocket缓冲、事务超时
很多毕设项目在本地跑得好好的,一放到演示环境就卡死或报错,多半是默认参数没调。三个地方是必须动手改的:数据库连接池的初始大小和最大大小,WebSocket的sendBufferSize和sessionLimit,以及事务的超时时间。
# application.yml 核心配置 spring: datasource: url: jdbc:mysql://localhost:3306/rating_system?useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true username: root password: 123456 hikari: minimum-idle: 5 maximum-pool-size: 20 connection-timeout: 5000 servlet: multipart: max-file-size: 10MB连接池用HikariCP是默认的,不用换。maximum-pool-size设20,是因为评分并发量通常不大,过大反而浪费数据库连接资源。rewriteBatchedStatements=true是批量写入的优化参数,如果成绩单导入或多条评分批量提交,这个参数能提升明显性能。
WebSocket配置稍微冷门但很关键:
@Configuration public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(new ScoreWebSocketHandler(), "/ws/score") .addInterceptors(new ScoreHandshakeInterceptor()) .setAllowedOrigins("*"); } }setAllowedOrigins("*")在开发阶段方便调试,但上线前必须收紧,否则任意跨域请求都能建立WebSocket连接,这是安全检查项。生产环境改成具体的前端域名即可。
事务超时要用@Transactional(timeout = 3)显式声明。评分写入逻辑是“先更新评分记录,再更新统计表”,两步操作必须在一个事务里,默认的无限超时在数据库死锁时会把线程堵死,加上timeout等于提前释放连接。
3. 数据库设计:评分系统的SQL表结构与实时统计的字段取舍
3.1 三张核心表的设计思路:评分记录表、评分项表、实时统计表
先看一眼数据库表结构会设计成什么样。实时评分系统的库名定成rating_system,下面至少三张表:score_record存每一条评分记录,rating_item存被评分的对象(比如选手、老师、课程),rating_statistics存每个评分项的实时汇总数据。
-- 评分记录表 CREATE TABLE `score_record` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', `rating_item_id` BIGINT NOT NULL COMMENT '评分项ID', `user_id` BIGINT NOT NULL COMMENT '评分人ID', `score` TINYINT NOT NULL COMMENT '评分值,1-10', `remark` VARCHAR(255) DEFAULT NULL COMMENT '评语', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '评分时间', PRIMARY KEY (`id`), KEY `idx_rating_item_id` (`rating_item_id`), KEY `idx_create_time` (`create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评分明细表';score用TINYINT而不是INT,因为评分值基本是1-10的整数,TINYINT只占1字节,索引和存储都更省。create_time加索引很关键,统计“某时间段内的平均分”和“最近10条评分”都需要按时间排序,没有这个索引全表扫描会拖慢接口响应。
-- 实时统计表 CREATE TABLE `rating_statistics` ( `rating_item_id` BIGINT NOT NULL COMMENT '评分项ID,主键', `total_score` DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT '总得分', `score_count` INT NOT NULL DEFAULT 0 COMMENT '评分总次数', `avg_score` DECIMAL(4,2) NOT NULL DEFAULT 0 COMMENT '平均分', `rating_rank` INT DEFAULT NULL COMMENT '排名', `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`rating_item_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评分实时统计表';这张统计表的rating_item_id直接当主键,因为每个评分项只对应一行统计数据。avg_score用DECIMAL(4,2)而不是FLOAT,避免浮点精度问题导致平均分显示成9.999。ON UPDATE CURRENT_TIMESTAMP让更新时间自动刷新,省去一条更新代码。
3.2 统计SQL怎么写才能扛住高频率评分写入
实时评分系统的统计查询如果每次都SELECT AVG(score) FROM score_record WHERE rating_item_id=?,数据量一旦上万就会明显变慢。常见做法是“写时统计”:每次评分写入时,同步更新统计表的total_score和score_count,平均分由这两个字段计算得出。
-- 写入评分并更新统计,事务包裹 BEGIN; INSERT INTO score_record (rating_item_id, user_id, score, remark) VALUES (1, 1001, 9, '表现优秀'); UPDATE rating_statistics SET total_score = total_score + 9, score_count = score_count + 1, avg_score = total_score / score_count WHERE rating_item_id = 1; COMMIT;这个方案的核心是事务保证两条SQL要么同时成功要么同时失败。用一条UPDATE替代每次查询才能算出平均值,性能提升显著。端到端全流程:Java代码里先insert后update,同一个事务内执行,然后用WebSocket把最新的avg_score推送出去。并发写入不存在脏数据问题,因为InnoDB的行锁会串行化对同一评分项的更新。
如果评分量实在太大,一个评分项每秒几百次写入,行锁竞争也会成为瓶颈,可以引入Redis把统计计数做在内存里,批量落库。毕设阶段不推荐这么做,复杂度增加翻倍,但可以在项目说明文档里作为“扩展展望”提到,这是加分写法。
3.3 初始化SQL脚本的编写规范:数据要能落地演示
SQL脚本不能只建表,要有基础数据,否则答辩演示时界面空白什么都没有。至少要准备10到15条评分数据、3到5个评分项,比如“选手A、选手B、选手C”加上每次评分的用户ID。组织这类演示数据的示例:
INSERT INTO rating_item (id, item_name, description) VALUES (1, '创新创意', '评价项目的创新性'), (2, '技术难度', '评价技术实现复杂度'), (3, '现场表现', '评价答辩表现');初始化数据尽量模拟真实场景,评分值分布在7到10之间,别全是10,否则统计图没有梯度,视觉上不真实。评分记录数据的create_time要错开,比如间隔几十秒到几分钟,这样时间轴图表才能画出曲线。项目说明文档里可以放一行命令说明如何导入初始化 SQL:
mysql -u root -p rating_system < sql/init_data.sql参数说明:-u root是用户名的指定方式,-p会提示交互式输入密码,rating_system是目标数据库名,<重定向让命令行工具读取SQL脚本文件。Java端每次启动时还可以用spring.sql.init.mode=always自动执行新增的SQL脚本,在application.yml里配置即可,方便重置演示环境。
4. 实时推送的核心实现:从轮询到WebSocket的评分展示演进
4.1 为什么轮询不是实时评分系统的可靠方案
很多人一上来写前端定时器,每2秒调用一次REST接口拿最新评分数据。这叫轮询,不叫实时推送。轮询的问题在于:延迟不可控,2秒轮询意味着评分后最多要等2秒才显示;资源浪费,没有新数据时请求照样打满服务端;高并发下数据库压力大,每条轮询请求都要查一次统计表。
WebSocket的模型完全不一样:客户端建立一次TCP连接后保持打开,服务端有数据变化时主动推给客户端。评分发生时延迟在几十毫秒级别,浏览器页面感知不到等待。这背后的原理是HTTP是“一问一答”的请求响应模型,而WebSocket是“长连接 + 双工通信”的传输协议。建立连接时需要一次HTTP升级握手,之后数据帧直接走TCP通道,省去了大量HTTP头部的重复传输开销。
4.2 Spring Boot后端WebSocket端点的完整代码与逻辑说明
后端实现一个WebSocket端点,接收评分事件后向所有会话广播最新统计结果。
@Component public class ScoreWebSocketHandler extends TextWebSocketHandler { private static final CopyOnWriteArrayList<WebSocketSession> sessions = new CopyOnWriteArrayList<>(); private final RatingStatisticsService statsService; public ScoreWebSocketHandler(RatingStatisticsService statsService) { this.statsService = statsService; } /** * 连接建立后,先把当前评分数据推给新连接的前端 */ @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { sessions.add(session); List<RatingStatisticsVO> allStats = statsService.getAllStatistics(); String json = new ObjectMapper().writeValueAsString(allStats); session.sendMessage(new TextMessage(json)); } /** * 收到客户端消息时,通常是前端发来的评分指令 */ @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { ScoreRequest request = new ObjectMapper().readValue(message.getPayload(), ScoreRequest.class); // 核心:落库 + 更新统计 + 广播,都在service里完成 List<RatingStatisticsVO> allStats = statsService.submitScoreAndGetStats(request); String json = new ObjectMapper().writeValueAsString(allStats); for (WebSocketSession s : sessions) { if (s.isOpen()) { s.sendMessage(new TextMessage(json)); } } } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { sessions.remove(session); } }这段代码用CopyOnWriteArrayList保存所有会话,这个线程安全的集合在广播遍历时不会抛ConcurrentModificationException,代价是写入时复制数组,但WebSocket连接的增删频率不高,性价比合适。submitScoreAndGetStats是Service层方法,封装了上一章的事务逻辑:写入评分记录、更新统计表、返回最新列表,再把列表序列化成JSON广播出去。这样好处是“推出去的数据永远跟库里的一致”,不会出现统计表还没更新完就广播旧数据的错位。
前端在Vue里连接WebSocket并监听消息:
const ws = new WebSocket(`ws://${location.host}/ws/score`); ws.onmessage = (event) => { const stats = JSON.parse(event.data); // 更新页面的评分排行和平均分展示 this.ratingList = stats; this.refreshChart(stats); }; function submitScore(itemId, score) { ws.send(JSON.stringify({ ratingItemId: itemId, score: score, userId: 1001 })); }注意ws://后面接的是后端地址,如果前后端分离部署在同一个域名下直接用location.host,跨域时则需要拼完整的后端IP和端口。前端评分操作通过ws.send发送JSON串,后端接收后走完整的事务流程。这个设计的好处是评分提交和结果推送共用一个长连接,不额外发HTTP请求。这是保证“实时”的关键设计决策。
4.3 前端断线重连的边界问题:别让评分丢在路由切换里
WebSocket最大的坑是“连接会断”,校园网不稳定、服务器重启、浏览器切后台太久,都可能导致连接关闭。前端必须在onclose里重连,不然评分功能悄无声息地失效。
let ws = null; let reconnectTimer = null; function connectWebSocket() { ws = new WebSocket(`ws://${location.host}/ws/score`); ws.onopen = () => console.log('评分通道已连接'); ws.onmessage = (event) => handleRankingUpdate(event); ws.onclose = () => { // 2秒后重连,避免服务端还没恢复时的无效握手风暴 clearTimeout(reconnectTimer); reconnectTimer = setTimeout(connectWebSocket, 2000); }; ws.onerror = () => ws.close(); // 触发onclose,统一走重连逻辑 } connectWebSocket();这里的关键设计是错误发生时主动关闭连接,让onclose统一处理重连,避免onerror和onclose各写一套重连逻辑导致连接被创建两次。重连间隔设2秒,既不会在服务端恢复期疯狂握手,也不会让用户等太久。这个细节写到项目说明文档里,评委老师会觉得你考虑过生产环境的稳定性问题。
5. 项目说明文档的写作套路:从ER图到部署步骤的完整链条
5.1 说明文档应该包含的六个部分(附标题模板)
很多毕设项目源码和数据库都很完整,但说明文档要么是流水账,要么只有开发环境配置,导致答辩时老师追问系统设计讲不清楚。一份能让老师快速理解系统的说明文档,建议按下述结构组织:引言与选题背景描述为什么需要实时评分、需求分析的用例描述、数据库设计的ER图和字段说明、核心接口API文档(包括REST和WebSocket消息格式)、部署运行步骤、系统测试部分(并发评分测试和实时性验证)。
数据库设计部分要放下面的表格:
| 表名 | 用途 | 核心字段 | 与其他表的关系 |
|---|---|---|---|
| score_record | 记录每次评分的明细 | id, rating_item_id, score, create_time | 多对一关联rating_item |
| rating_item | 被评分对象列表 | id, item_name, status | 主表,被score_record引用 |
| rating_statistics | 每个评分项的聚合结果 | rating_item_id, avg_score, score_count | 一对一关联rating_item |
5.2 部署步骤要写成能跟着照做的程度
文档里的部署章节得是“照着操作就能跑”的程度,不能只写“导入项目并运行”。关键命令和配置一处都不能省:
- 安装JDK 17,配置环境变量JAVA_HOME。
- 安装MySQL 8.x,执行项目的
sql/init_schema.sql和sql/init_data.sql创建库和初始化数据。 - 修改
application.yml中的数据库用户名和密码,确保spring.datasource.url里的数据库名与建库名一致。 - 在项目根目录执行
mvn spring-boot:run启动后端服务。 - 进入前端目录执行
npm install安装依赖,随后npm run dev启动开发服务。 - 访问
http://localhost:8080,向评分接口提交一条测试数据,验证页面是否实时更新。
部署部分有一个非常常见的坑要写清楚:后端端口改了,前端Vite的代理配置也要跟着改。比如后端跑在8081,前端vite.config.js里要设server.proxy指向http://localhost:8081,否则联调时页面请求全部404。这些细节写进说明文档,能省下答辩评委自己折腾环境的时间。
5.3 文档的技术深度:不需要贴全部源码,但接口协议要写细
说明文档不是代码注释的堆砌,更不需要贴所有单表的CRUD方法。重点是接口协议和消息结构,因为它们决定了系统能不能被别人复用。REST接口用表格列出路径、方法、参数和响应示例就足够。
WebSocket的消息格式要单独写清楚:前端发送{"ratingItemId":1,"score":9,"userId":1001},后端返回[{"ratingItemId":1,"avgScore":8.7,"scoreCount":42},...]。用JSON Schema的方式描述清楚字段类型和含义,尤其是业务规则的约束,比如“同一用户不能重复给同一个评分项打分”。这种约束用SQL的联合唯一索引实现,文档里要说明索引设计和实际效果,方便老师理解系统在数据一致性上的考虑。
6. 答辩演示的最佳实践:实时评分系统怎么展示才区分度足够
6.1 演示的关键要展示实时性,而不是操作流程
多数人演示这个项目是用浏览器开两个页面,左面提交评分右面看结果更新,证明WebSocket生效了。这个做法太常规,老师看太多遍没有记忆点。更有效的做法是开两个浏览器窗口,一个窗口模拟评委打分,另一个窗口切换到大屏展示模式,两个窗口同步更新排名变化,加上平均分实时变动。这需要前端支持“评分端”和“展示端”两套路由,展示端页面字号要大、信息要少,只显示排行榜和实时分数。
如果提前用JMeter或Postman的WebSocket客户端脚本做了一次50并发评分请求,并在演示时播放后端日志中的推流记录,这个展示效果比口头说明强得多。50个请求同时写评分记录,数据库连接池参数、事务隔离级别、行锁等待时间都会被暴露出来,所以压测要在本地反复跑过,确认稳定后再演示,否则一旦卡顿就是减分项。
存储过程或SQL脚本不是重点,重点是用时序图展示一次评分的完整生命周期:WebSocket发送评分消息、Service层事务写入数据库、统计表更新、最后一秒内广播给所有连接的客户端。画成PPT里的图,答辩时指着图讲比对着代码讲清晰得多。
6.2 评委老师常问的三个问题与应对
导师大概率会问三个问题:一是WebSocket和轮询的区别,为什么不用SSE?二是数据库事务为什么能保证统计表不出现负数或平均数错乱?三是若线上评分量大,如何处理性能瓶颈。
第一个问题的回答要点是:SSE是单向的,客户端能收服务端推送但很难做到评分消息的上行和下行共用同一条通道,而实时评分系统既有提交请求又有结果推送,WebSocket的双工优势是天然匹配。第三个问题是区分度最高的地方,能否说出“Redis计数 + 异步批量落库 + WebSocket广播”的分层架构,决定了18分和25分的差距。
这三个问题不要背标准答案,按自己设计系统时的取舍来讲。自己的系统用了事务保证统计一致,就说事务的边界在哪里;用了CopyOnWriteArrayList管理会话,就分析为什么会话数量少时它比同步容器更合适。这里每讲出一个设计点,都是简历上能写的技术深度。
本文还有配套的精品资源,点击获取