Java实时评分系统:Spring Boot+MyBatis-Plus工程实践指南 简介本资源是一套面向计算机专业本科生的Java毕业设计项目——实时赛事评分系统聚焦解决传统人工打分效率低、易出错、结果不透明等痛点适用于校园技能大赛、创新创业路演等需多评委协同打分的场景。压缩包共61个文件含48个Java核心业务类涵盖评委端扫码评分、实时计算、大屏展示等模块、6个MyBatis映射XML、4张系统界面截图含评分大屏与手机端交互效果、1个application.yml配置文件、1个项目说明文档含部署步骤与使用须知及1个score.sql建表脚本整体仅138KB轻量易部署。已有355人学习下载资源结构清晰代码注释完整配套SQL可直接导入MySQL 5.7Redis用于缓存实时分数Spring BootMyBatis技术栈成熟稳定适合毕设开发、课程设计或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(useGeneratedKeystrue)怎么选、为什么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 doesnt 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 groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.28/version !-- 原为5.1.47MySQL 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_typeweight_avg这条对应加权平均算法rule_typemedian对应中位数算法——这正是后续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?useSSLfalseserverTimezoneAsia/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.activedev参数或直接在IDEA的Run Configuration里设置Active profile为dev就能精准绑定MySQL。启动成功后访问http://localhost:8080/swagger-ui.html项目集成了Swagger你会看到ScoringController的API列表——这才是真正的入口。3. 核心业务落地从“提交评分”到“实时排名”的代码链路拆解3.1 提交评分接口为什么用Validated而不是RequestBody校验打开ScoringController.javasubmitScore()方法签名是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如何动态拼SQLScoringRuleConfigMapper.java里有一个关键方法SelectProvider(type ScoringRuleSqlProvider.class, method buildRuleQuery) ListScoringRuleConfig selectByType(Param(ruleType) String ruleType);ScoringRuleSqlProvider.buildRuleQuery()方法返回一个动态SQL字符串public String buildRuleQuery(MapString, 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(); }这种写法比XMLif标签更灵活尤其适合规则类场景当新增rule_typecustom_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参数缺少serverTimezoneAsia/ShanghaiMySQL 5.7默认要求时区显式声明否则驱动抛出The server time zone value XXX is unrecognized异常被包装成连接超时。解决在JDBC URL末尾追加serverTimezoneAsia/Shanghai如jdbc:mysql://localhost:3306/scoring_system?useSSLfalseserverTimezoneAsia/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里加zeroDateTimeBehaviorconvertToNull并在score_history.create_time字段的TableField注解中指定fill FieldFill.INSERT同时确保实体类字段类型为LocalDateTime非Date配合JsonFormat(patternyyyy-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标签加useCachefalse属性强制每次查询走数据库select idselectByType resultTypeScoringRuleConfig useCachefalse 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 groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.11.5/version scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-jackson/artifactId version0.11.5/version scoperuntime/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限制。你该加上useGeneratedKeystrue和keyPropertyid让MyBatis-Plus自动回填主键并在foreach外层加bind标签做分批控制insert idbatchInsert parameterTypejava.util.List INSERT INTO score_history (item_id, judge_id, score, rule_type, create_time) VALUES foreach collectionlist itemitem separator, (#{item.itemId}, #{item.judgeId}, #{item.score}, #{item.ruleType}, #{item.createTime}) /foreach ON DUPLICATE KEY UPDATE score VALUES(score), update_time NOW() /insertON DUPLICATE KEY UPDATE这一行就是防止同一评委对同一作品重复提交的终极方案——它比在Service层查重再插入性能高出一个数量级。第二亲手配application-dev.yml的Druid监控。把HikariCP换成Druid不是为了炫技而是为了答辩时能打开http://localhost:8080/druid指着实时SQL执行耗时图说“看这条SELECT * FROM score_history WHERE item_id?平均耗时23msP95是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的底层机制不是只会CtrlC/V。这三个片段每一个都直击Java工程师的核心能力SQL优化意识、运维可观测性、领域模型校验设计。它们不增加项目体量却让整个毕设从“能跑”跃升到“可讲、可辩、可延展”。我当年就是靠改完这三处在答辩现场被老师追问了20分钟最后拿了优秀毕设。希望帮到你。本文还有配套的精品资源点击获取