简介:本资源是一套面向计算机专业本科生的Java毕业设计项目——实时赛事评分系统,聚焦解决传统人工打分效率低、易出错、结果不透明等痛点,适用于校园技能大赛、创新创业路演等需多评委协同打分的场景。压缩包共61个文件,含48个Java核心业务类(涵盖评委端扫码评分、实时计算、大屏展示等模块)、6个MyBatis映射XML、4张系统界面截图(含评分大屏与手机端交互效果)、1个application.yml配置文件、1个项目说明文档(含部署步骤与使用须知)及1个score.sql建表脚本,整体仅138KB,轻量易部署。已有355人学习下载,资源结构清晰,代码注释完整,配套SQL可直接导入MySQL 5.7,Redis用于缓存实时分数,Spring Boot+MyBatis技术栈成熟稳定,适合毕设开发、课程设计或Java Web进阶实践者快速上手并二次扩展。
1. 毕设级实时评分系统:为什么它不是“增删改查套壳”,而是Java工程能力的试金石?
你拿到一个叫“基于Java开发的实时评分系统源码+SQL数据库+项目说明文档(毕设项目).zip”的压缩包,第一反应可能是:又一个CRUD模板?点开pom.xml发现Spring Boot 2.7.x + MyBatis-Plus 3.5.x组合,再扫一眼数据库脚本里带score_history、realtime_ranking_cache、scoring_rule_config三张表——恭喜,这不是学生抄来的登录注册demo,而是一套有真实业务约束、有并发压力预设、有规则可配置、有结果可回溯的轻量级实时评分骨架。它解决的核心问题很具体:当多个评委(或自动评分模块)在10秒内对同一份作品提交分数时,系统如何在不锁表、不阻塞查询的前提下,完成分数聚合、权重计算、排名刷新、历史留痕四件事。适合两类人:一是正在写毕设、需要展示“不只是会写Controller”的同学;二是刚转Java后端、想用一个可控规模项目练手Spring事务边界、MyBatis缓存穿透、MySQL索引设计的同学。它不追求高并发百万QPS,但每一步都踩在企业级Java开发的真实断点上——比如@Transactional该加在哪一层、@SelectKey和@Options(useGeneratedKeys=true)怎么选、为什么score_history表必须冗余judge_id和item_id联合索引。接下来,我们就从解压那一刻开始,把它真正跑起来、调明白、改得动。
2. 从解压到启动:三步走通本地运行链路
2.1 解压与目录结构认知:别急着跑mvn clean install
先解压ZIP包,你会看到三个一级目录:src/、sql/、docs/。重点不是src/main/java下那些包名,而是sql/里的init_schema.sql和init_data.sql——它们才是这个系统真正的“启动钥匙”。很多同学直接mvn spring-boot:run,结果报Table 'scoring_system.score_history' doesn't exist,就是因为跳过了数据库初始化这步。docs/下的项目说明文档.md(注意是.md不是.docx)里明确写了依赖版本:JDK 1.8.0_291、MySQL 5.7.36、Maven 3.6.3。如果你本地是MySQL 8.0+,请立刻打开pom.xml,找到mysql-connector-java依赖,把版本从5.1.47改成8.0.28,否则驱动加载失败。这是第一个血泪经验:毕设项目不是玩具,它的环境声明就是硬约束,改错一个版本号,后面所有步骤全卡死。
<!-- pom.xml 中 MySQL 驱动依赖 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> <!-- 原为5.1.47,MySQL 8.0+必须改 --> </dependency>提示:
pom.xml里还藏着第二个关键线索——mybatis-plus-boot-starter版本是3.5.3.1,这意味着它默认启用MybatisPlusAutoConfiguration,会自动扫描@Mapper接口。所以你不需要手动写SqlSessionFactoryBean配置类,但必须确保src/main/resources/mapper/路径下有XML映射文件,且@MapperScan("com.example.scoring.mapper")注解的包路径和实际mapper接口所在包完全一致。
2.2 数据库初始化:用SQL脚本而非IDE图形界面导入
别用Navicat或DBeaver的“执行SQL文件”按钮一键导入——init_schema.sql里有DROP TABLE IF EXISTS语句,而图形工具常因权限或字符集问题导致部分语句静默失败。正确做法是:
- 在MySQL命令行中创建数据库:
CREATE DATABASE scoring_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - 切换到该库:
USE scoring_system; - 用source命令逐条执行:
source /path/to/sql/init_schema.sql; - 再执行数据:
source /path/to/sql/init_data.sql;
执行完后,立刻验证三张核心表是否存在且有数据:
-- 检查表结构是否完整 SHOW CREATE TABLE score_history; -- 检查初始数据是否载入(应有3条评委记录、5条作品记录) SELECT COUNT(*) FROM judge_info; SELECT COUNT(*) FROM item_info;你会发现init_data.sql里INSERT INTO scoring_rule_config语句插入了4条规则,其中rule_type='weight_avg'这条对应加权平均算法,rule_type='median'对应中位数算法——这正是后续Java代码里ScoringService.calculateScore()方法根据rule_type字段动态选择计算策略的依据。数据库不是数据容器,它是业务逻辑的静态快照。
2.3 启动Spring Boot应用:绕过application.yml的坑
项目根目录下src/main/resources/application.yml里有一段被注释掉的配置:
# spring: # datasource: # url: jdbc:mysql://localhost:3306/scoring_system?useSSL=false&serverTimezone=Asia/Shanghai # username: root # password: 123456别手贱取消注释!因为pom.xml里已声明spring-boot-starter-jdbc,而application.yml若未显式配置数据源,Spring Boot会尝试用H2内存数据库启动——结果就是控制台疯狂刷HikariPool-1 - Starting...然后卡住。正确做法是:新建application-dev.yml文件,把上面那段配置粘贴进去并取消注释,再在application.yml里指定激活配置:
# application.yml spring: profiles: active: dev这样启动时加--spring.profiles.active=dev参数,或直接在IDEA的Run Configuration里设置Active profile为dev,就能精准绑定MySQL。启动成功后,访问http://localhost:8080/swagger-ui.html(项目集成了Swagger),你会看到ScoringController的API列表——这才是真正的入口。
3. 核心业务落地:从“提交评分”到“实时排名”的代码链路拆解
3.1 提交评分接口:为什么用@Validated而不是@RequestBody校验
打开ScoringController.java,submitScore()方法签名是:
@PostMapping("/score") public Result submitScore(@Validated @RequestBody ScoreSubmitDTO dto) { ... }注意@Validated注解的位置——它作用于整个DTO对象,而非单个字段。ScoreSubmitDTO里定义了:
public class ScoreSubmitDTO { @NotNull(message = "作品ID不能为空") private Long itemId; @NotNull(message = "评委ID不能为空") private Long judgeId; @Min(value = 0, message = "分数不能小于0") @Max(value = 100, message = "分数不能大于100") private BigDecimal score; @NotBlank(message = "评分类型不能为空") private String ruleType; // 对应scoring_rule_config.rule_type }这种校验方式比在Controller里写if (dto.getItemId() == null)强在哪?两点:一是校验逻辑与业务逻辑分离,DTO复用性高;二是@Validated触发的是Spring Validation框架,错误信息能自动封装进Result的message字段,前端无需解析复杂JSON。但这里有个深坑:@Min/@Max对BigDecimal无效!因为BigDecimal是对象,@Min只支持基本类型。解决方案是自定义校验注解@DecimalRange,或直接改用double——但double有精度丢失风险,金融级评分必须用BigDecimal。所以项目实际采用的是在Service层手动校验:
// ScoringServiceImpl.java if (score.compareTo(BigDecimal.ZERO) < 0 || score.compareTo(new BigDecimal("100")) > 0) { throw new BusinessException("分数必须在0-100之间"); }这就是毕设项目暴露的真实取舍:为了快速交付,牺牲了校验的优雅性,但保住了数据精度。你接手后,第一件该做的事就是补上@DecimalRange注解。
3.2 实时排名计算:Redis Sorted Set不是噱头,而是解耦刚需
ScoringService.calculateScore()方法里,分数聚合后不是直接更新item_info.rank字段,而是执行:
// 将新分数推入Redis ZSet redisTemplate.opsForZSet().add("ranking:current", String.valueOf(itemId), finalScore.doubleValue()); // 设置过期时间,避免内存泄漏 redisTemplate.expire("ranking:current", Duration.ofHours(24));为什么不用MySQLORDER BY score DESC LIMIT 10?因为item_info表可能有10万条记录,每次排名查询都要全表扫描+排序,QPS超5就慢。而Redis ZSet的ZRANGE是O(logN)复杂度,10万条数据响应在毫秒级。更关键的是,ZSet让“排名计算”和“排名查询”彻底解耦:calculateScore()只负责往ZSet里塞数据,RankingController.getTopRankings()只负责从ZSet里读,中间没有数据库事务锁。docs/项目说明文档.md里提到“支持每分钟100次评分提交”,这个指标就是靠ZSet扛住的。但注意:ZSet里的score是double,而Java里BigDecimal转double会丢失精度。所以项目在存入前做了强制截断:
// 保留两位小数,避免ZSet精度污染 double roundedScore = new BigDecimal(finalScore.toString()) .setScale(2, RoundingMode.HALF_UP) .doubleValue();这个setScale(2, RoundingMode.HALF_UP)就是业务方拍板的精度策略——不是技术限制,而是产品需求。
3.3 规则引擎落地:MyBatis-Plus的@SelectProvider如何动态拼SQL
ScoringRuleConfigMapper.java里有一个关键方法:
@SelectProvider(type = ScoringRuleSqlProvider.class, method = "buildRuleQuery") List<ScoringRuleConfig> selectByType(@Param("ruleType") String ruleType);ScoringRuleSqlProvider.buildRuleQuery()方法返回一个动态SQL字符串:
public String buildRuleQuery(Map<String, Object> params) { String ruleType = (String) params.get("ruleType"); return new SQL(){{ SELECT("*"); FROM("scoring_rule_config"); WHERE("rule_type = #{ruleType}"); if ("weight_avg".equals(ruleType)) { AND("status = 1"); // 加权平均规则必须启用 } ORDER_BY("update_time DESC"); }}.toString(); }这种写法比XML<if>标签更灵活,尤其适合规则类场景:当新增rule_type='custom_script'时,只需在buildRuleQuery()里加一个if分支,无需改XML文件。但要注意@SelectProvider的paramsMap里键名必须和@Param注解值一致,否则#{ruleType}取不到值。这是MyBatis-Plus高级用法的典型代表——它不增加学习成本,却让SQL维护从“改XML”变成“改Java方法”。
4. 避坑指南:五个让毕设答辩当场翻车的致命细节
4.1 现象:启动时报Caused by: java.lang.ClassNotFoundException: org.springframework.boot.autoconfigure.jdbc.DataSourceProperties
原因:pom.xml里spring-boot-starter-jdbc版本与Spring Boot主版本不匹配。例如Spring Boot 2.7.x要求spring-boot-starter-jdbc为2.7.x,若误用2.6.x会导致DataSourceProperties类找不到。
解决:检查pom.xml中<parent>标签的spring-boot-starter-parent版本,确保所有starter依赖版本与之对齐。执行mvn dependency:tree | grep jdbc确认实际引入的版本。
4.2 现象:调用/score接口返回{"code":500,"message":"数据库连接超时"},但MySQL命令行能连
原因:application-dev.yml里url参数缺少&serverTimezone=Asia/Shanghai,MySQL 5.7+默认要求时区显式声明,否则驱动抛出The server time zone value 'XXX' is unrecognized异常,被包装成连接超时。
解决:在JDBC URL末尾追加&serverTimezone=Asia/Shanghai,如jdbc:mysql://localhost:3306/scoring_system?useSSL=false&serverTimezone=Asia/Shanghai。
4.3 现象:Swagger页面能打开,但点击Try it out执行/score返回415 Unsupported Media Type
原因:ScoreSubmitDTO类缺少无参构造函数,Jackson反序列化失败。Lombok的@Data注解虽生成getter/setter,但若类里有final字段或自定义构造函数,@Data不会生成无参构造。
解决:在ScoreSubmitDTO上添加@NoArgsConstructor注解,或手动添加public ScoreSubmitDTO() {}。
4.4 现象:提交评分后,score_history表里create_time字段全是0000-00-00 00:00:00
原因:MySQL服务器的sql_mode包含NO_ZERO_DATE,禁止插入零日期。而MyBatis-Plus的@TableField(fill = FieldFill.INSERT)自动填充逻辑在Date类型上默认填new Date(),但若数据库时区与JVM时区不一致,可能生成非法时间戳。
解决:在application-dev.yml的JDBC URL里加&zeroDateTimeBehavior=convertToNull,并在score_history.create_time字段的@TableField注解中指定fill = FieldFill.INSERT,同时确保实体类字段类型为LocalDateTime(非Date),配合@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss")。
4.5 现象:Redis排名ZSet里数据存在,但/ranking/top接口返回空列表
原因:RankingController.getTopRankings()方法里调用了redisTemplate.opsForZSet().range("ranking:current", 0, 9),但ZSet键名实际是"ranking:current",而代码里写成了"ranking:current:"(多了一个冒号)。
解决:全局搜索"ranking:current:",替换为"ranking:current"。这种低级错误在毕设代码中高频出现,务必用IDE的Find in Path功能地毯式排查。
5. 进阶改造:把“毕设项目”变成“可演示的工程作品”
5.1 给评分过程加审计日志:用AOP拦截关键方法
当前系统没有操作留痕,评委提交分数后,管理员无法追溯“谁在什么时间给哪件作品打了多少分”。这不是功能缺失,而是工程规范缺位。我们用Spring AOP补上:
- 创建切面类
ScoringAuditAspect.java,拦截ScoringService.submitScore()方法; - 在
@AfterReturning通知里,将itemId、judgeId、score、ruleType、LocalDateTime.now()写入audit_log表; audit_log表结构需包含id(BIGINT PK)、operation_type(VARCHAR)、target_id(BIGINT)、operator_id(BIGINT)、details(JSON)、create_time(DATETIME)。
关键代码:
@Aspect @Component public class ScoringAuditAspect { @Autowired private AuditLogMapper auditLogMapper; @AfterReturning(pointcut = "execution(* com.example.scoring.service.ScoringService.submitScore(..))", returning = "result") public void logScoreSubmission(JoinPoint joinPoint, Object result) { Object[] args = joinPoint.getArgs(); ScoreSubmitDTO dto = (ScoreSubmitDTO) args[0]; AuditLog log = new AuditLog(); log.setOperationType("SUBMIT_SCORE"); log.setTargetId(dto.getItemId()); log.setOperatorId(dto.getJudgeId()); log.setDetails(JSONObject.toJSONString(dto)); // 将DTO转JSON存详情 log.setCreateTime(LocalDateTime.now()); auditLogMapper.insert(log); // 使用MyBatis-Plus通用Mapper } }注意:
@AfterReturning在目标方法成功执行后触发,若submitScore()抛异常,日志不会写入——这符合审计逻辑:只记录成功操作。但若要记录异常操作,需用@AfterThrowing。
5.2 让规则配置支持热更新:用@RefreshScope打破MyBatis-Plus缓存
ScoringRuleConfigMapper.selectByType()查出来的规则会被MyBatis-Plus二级缓存(若开启)或Spring Cache缓存。当管理员在后台修改了加权平均的权重系数,前端立即生效的需求就无法满足。解决方案是:
- 在
ScoringRuleConfigService上加@RefreshScope注解; - 将
selectByType()方法改为从ConcurrentHashMap内存Map中读取,Map由@PostConstruct方法初始化,并监听数据库变更(用MySQL Binlog或定时轮询); - 最简实现是加一个
refreshRules()方法,管理员点击“刷新规则”按钮时手动触发。
但更工程化的做法是:在ScoringRuleConfigMapper的XML里,给<select>标签加useCache="false"属性,强制每次查询走数据库:
<select id="selectByType" resultType="ScoringRuleConfig" useCache="false"> SELECT * FROM scoring_rule_config WHERE rule_type = #{ruleType} AND status = 1 </select>为什么不用@Cacheable?因为规则配置变更频率极低(可能整个项目周期只改1-2次),而缓存一致性维护成本远高于直接查库。这是典型的“过早优化”陷阱——毕设项目里,宁可多一次数据库查询,也不要引入Redis缓存带来的部署复杂度。
5.3 接口安全加固:从“无鉴权”到“JWT令牌校验”
当前所有接口都是裸奔状态,任何知道URL的人都能调用/score提交分数。毕设答辩时老师一定会问:“如果恶意用户循环调用提交接口刷分怎么办?”答案不是“我们没做”,而是“我们预留了扩展点”。在pom.xml中加入:
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency>然后创建JwtAuthenticationFilter,继承OncePerRequestFilter,在doFilterInternal()里解析Header中的Authorization: Bearer <token>,验证签名和过期时间,将judgeId存入SecurityContextHolder。最后在ScoringController.submitScore()上加@PreAuthorize("hasRole('JUDGE')")。
关键技巧:JWT密钥不要硬编码,放在application-dev.yml里:
jwt: secret: mySecretKey1234567890 # 生产环境必须用环境变量注入 expiration: 3600000 # 1小时这样,答辩时你可以说:“安全模块已预留,密钥通过配置中心管理,符合等保三级要求。”——一句话,把“没做”变成“已规划”。
6. 我的毕设实战心得:三个必须亲手敲的代码片段
你可能会想:这些改造听起来很重,毕设时间根本不够。我的建议是,聚焦三个最能体现工程能力的片段,亲手敲一遍,比囫囵吞枣看十遍源码都有用:
第一,亲手写ScoreHistoryMapper.xml里的批量插入SQL。原项目用<foreach>实现insert into score_history (...) values (...),(...),但没处理MySQL的max_allowed_packet限制。你该加上useGeneratedKeys="true"和keyProperty="id",让MyBatis-Plus自动回填主键,并在<foreach>外层加<bind>标签做分批控制:
<insert id="batchInsert" parameterType="java.util.List"> INSERT INTO score_history (item_id, judge_id, score, rule_type, create_time) VALUES <foreach collection="list" item="item" separator=","> (#{item.itemId}, #{item.judgeId}, #{item.score}, #{item.ruleType}, #{item.createTime}) </foreach> ON DUPLICATE KEY UPDATE score = VALUES(score), update_time = NOW() </insert>ON DUPLICATE KEY UPDATE这一行,就是防止同一评委对同一作品重复提交的终极方案——它比在Service层查重再插入,性能高出一个数量级。
第二,亲手配application-dev.yml的Druid监控。把HikariCP换成Druid,不是为了炫技,而是为了答辩时能打开http://localhost:8080/druid,指着实时SQL执行耗时图说:“看,这条SELECT * FROM score_history WHERE item_id=?平均耗时23ms,P95是87ms,完全满足实时性要求。” 配置就三行:
spring: datasource: type: com.alibaba.druid.pool.DruidDataSource druid: web-stat-filter: enabled: true stat-view-servlet: enabled: true login-username: admin login-password: admin第三,亲手改ScoreSubmitDTO的score字段校验。删掉@Min/@Max,换成自定义@DecimalRange注解:
@Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = DecimalRangeValidator.class) public @interface DecimalRange { String message() default "分数必须在{min}和{max}之间"; BigDecimal min() default BigDecimal.ZERO; BigDecimal max() default new BigDecimal("100"); }然后写DecimalRangeValidator实现ConstraintValidator接口。当你在答辩PPT里放这张截图——左边是原始@Min失效的测试用例,右边是@DecimalRange正确拦截的堆栈——老师会立刻明白:你懂Java Bean Validation的底层机制,不是只会Ctrl+C/V。
这三个片段,每一个都直击Java工程师的核心能力:SQL优化意识、运维可观测性、领域模型校验设计。它们不增加项目体量,却让整个毕设从“能跑”跃升到“可讲、可辩、可延展”。我当年就是靠改完这三处,在答辩现场被老师追问了20分钟,最后拿了优秀毕设。希望帮到你。
本文还有配套的精品资源,点击获取