简介:基于SpringBoot的学生成绩可视化分析系统是一套面向高校计算机专业毕业设计、课程大作业及项目实战练习的完整源代码与数据库资源,由导师指导并获98分高分。系统针对传统成绩管理缺乏直观统计的问题,围绕学生成绩数据提供课程平均分、学分绩点、不及格预警以及散点分布、树状占比、线性趋势等多维度可视化分析;后端基于SpringBoot搭建,前端使用JavaScript与CSS完成图表展示,数据库脚本已随包附带,导入后可快速运行调试并观察完整交互效果。资源包共144个文件,以XML配置文件、Java源码、编译后的Class文件为主,另有JS脚本、SQL文件、样式表、字体图标等,整体仅约606KB,目录结构简洁,适合逐步研读代码与模块划分。当前已有97人学习使用。项目中Treemap、Scatter、Linear等多个分析Controller覆盖不同成绩分析场景,均可本地编译运行,便于理解可视化生成流程;对需要完整可运行案例、从零学习SpringBoot数据可视化开发的读者尤为适合,也为二次扩展提供可用的工程基础。
1. 为什么SpringBoot成绩可视化系统能成为高分毕设的常青树
期末成绩一出来,教务老师最忙的就是把十几个班的成绩表汇总成图表。基于SpringBoot的学生成绩可视化分析系统,做的就是这件事:后端用SpringBoot接MySQL,前端用ECharts出图,把Excel里的分数变成班级平均分、及格率、分数段分布和排名趋势。
它适合两类人:一是毕设选题还没定、想兼顾系统完整度和演示效果的同学;二是想低成本搭一套内部成绩看板的学校或机构开发者。
做这个题最容易翻车的地方反而不是框架,而是统计口径不一致:及格率按人数算还是按科次算,图上数字就对不上。下面就把口径、代码和坑一次讲清楚。
2. 系统拆解:SpringBoot后端、数据库表与项目结构
写成绩可视化,第一件事不是写图表,而是想清楚后端要提供哪些数据。很多同学把大量时间花在“画一个炫酷大屏”上,结果到了连表阶段发现没有字段支撑,只能回头补。这里按“SpringBoot + MySQL + ECharts”这条最稳妥的路线拆开讲。
2.1 为什么这个毕设选SpringBoot不选SSH或Flask
SSH时代的Struts + Spring + Hibernate做这套系统当然也能跑,但配置全躺在XML里:事务代理、SqlMap、视图解析器,每个都要单独配。单人毕设最怕的就是“答辩前一周还在调配置”,SpringBoot的自动装配把这些全干掉了,一个spring-boot-starter-web就能把Web容器带起来。
Flask或FastAPI写统计接口很爽,但本科毕设的评审习惯看Java技术栈,SpringBoot就业市场认可度也高。如果你后续还想加登录、权限、导出PDF,SpringSecurity、POI这些成熟生态都能接上。
还有一层考虑:这个系统的核心是“分析”,不是“高并发”。数据量撑死几万条,选型更应看重开发速度和可读性。MyBatis-Plus的QueryWrapper能少写大量SQL,配合Lombok让实体类干净很多,非常适合拿来做课程设计或毕业论文。
2.2 数据库设计:一套能讲清楚“成绩从哪来到哪去”的表结构
成绩系统的核心表逃不开这几张:班级表、学生表、课程表、成绩表。不要为了图省事把所有字段塞进一张大宽表,可视化系统里的统计SQL一旦涉及多条件聚合,拆开的表反而好写。我一般会这样建:
| 表名 | 关键字段 | 说明 |
|---|---|---|
| class | id, class_name | 班级维度,一个班一条 |
| student | id, student_no, name, class_id | 学生归属班级,学号唯一 |
| course | id, course_no, course_name | 课程维度 |
| score | id, student_no, course_no, exam_type, score_value, exam_time | 一条成绩记录 |
这里有个关键点:exam_type必须单独成字段,用来区分“期中”“期末”“补考”。如果一张表里同时混着期中、期末成绩,平均分没有任何解释力。还有一个容易踩的小坑:字段名不要叫score,虽然它不是严格的数据库保留字,但在一些SQL方言和ORM映射里容易和“分数/得分”语义混淆,用score_value最稳。
在Java实体类里,字段尽量跟表字段一一对应,成绩用BigDecimal接收,别用double。double做聚合时会有0.1+0.2的精度问题,而成绩分析恰恰要给评委看小数点,精度丢失会直接体现在图表标签上。
2.3 Maven依赖与三层目录:先把架子搭起来
SpringBoot项目用Maven管理依赖最省事,核心依赖只需要这几个:
<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>3.5.x</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>3.3.x</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency>依赖说明:spring-boot-starter-web提供Controller和内置Tomcat;MyBatis-Plus负责数据库映射和分页,比裸MyBatis少写大量XML;EasyExcel用来读成绩Excel,比POI写起来更简洁;MySQL驱动坐标要根据SpringBoot版本调整,Boot 3.x用mysql-connector-j,Boot 2.x用mysql-connector-java。
项目目录按三层结构分好:
src/main/java/com/example/grade ├── controller ├── service ├── mapper ├── entity └── configController只负责接收参数和返回结果,Service负责统计口径和事务,Mapper负责SQL落地。很多同学图省事把SQL写在Controller里,一旦答辩被问“怎么扩展”,很难自圆其说。按这个结构拆,每个类都能对应到论文里的一个功能模块,写文档也顺手。
另外,我会把返回结果统一封装成Result<T>,结构固定为{code, message, data}。前端只用解析一种格式,ECharts那层少写很多if判断,答辩时也方便解释“接口统一返回结构”这个设计点。
3. 让学生成绩“进得来”:建表SQL、Excel批量导入与数据清洗
3.1 建表SQL:给成绩表加唯一索引,防止重复成绩
CREATE DATABASE IF NOT EXISTS grade_db DEFAULT CHARACTER SET utf8mb4; USE grade_db; CREATE TABLE class ( id BIGINT PRIMARY KEY AUTO_INCREMENT, class_name VARCHAR(50) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE student ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(50) NOT NULL, class_id BIGINT NOT NULL, INDEX idx_student_class (class_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE score ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL, course_no VARCHAR(20) NOT NULL, exam_type VARCHAR(20) NOT NULL, score_value DECIMAL(5,2), exam_time DATE, UNIQUE KEY uk_score_student_course_exam (student_no, course_no, exam_type), INDEX idx_score_course (course_no) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;逻辑说明:score_value用DECIMAL(5,2),最大值可以存9999.99,给0到100的成绩留足余量。唯一索引uk_score_student_course_exam是这套表设计的关键,它保证同一学生、同一课程、同一考试类型只能存在一条记录,重复导入Excel时要么报错、要么覆盖,不会产生两条成绩把统计结果拉高。
参数说明:表引擎统一用InnoDB,支持事务和行级锁;字符集用utf8mb4而不是utf8,因为MySQL的utf8存不下四字节emoji,虽然成绩表里一般没有表情,但学生姓名里可能出现冷僻字。exam_type加不加索引看查询频率,如果经常按“期末”过滤,可以建一个联合索引(exam_type, course_no)。
建表之后,别忘了在application.yml里把数据库连接串配上:
spring: datasource: url: jdbc:mysql://localhost:3306/grade_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai driver-class-name: com.mysql.cj.jdbc.Driver连接串里的characterEncoding=UTF-8和serverTimezone=Asia/Shanghai缺一不可,缺了前者中文乱码,缺了后者在高版本驱动下启动直接报时区异常。
提示:MySQL 8.0.20 之后对
VALUES()函数有弃用警告,但功能仍可用;如果不想看到警告,可以换成行别名语法,毕设阶段用VALUES()完全够。
3.2 用EasyExcel批量导入:不要一条条insert
很多早期代码用POI的HSSFWorkbook逐行读Excel,再在循环里insert,几千行数据能跑几分钟。EasyExcel基于SAX模式读文件,内存占用低,配合批量插入,速度完全够用。先定义一个和Excel表头对应的DTO:
public class ScoreExcelDTO { @ExcelProperty("学号") private String studentNo; @ExcelProperty("课程号") private String courseNo; @ExcelProperty("考试类型") private String examType; @ExcelProperty("成绩") private BigDecimal scoreValue; }@ExcelProperty里的字符串必须和Excel表头完全一致,包括空格。如果表头是“成绩 ”带个空格,映射不上,字段会全为null。
然后写一个监听器,EasyExcel每解析到一行就会回调invoke:
@Slf4j public class ScoreExcelListener extends AnalysisEventListener<ScoreExcelDTO> { private static final int BATCH_SIZE = 1000; private final List<ScoreExcelDTO> rows = new ArrayList<>(); private final ScoreService scoreService; public ScoreExcelListener(ScoreService scoreService) { this.scoreService = scoreService; } @Override public void invoke(ScoreExcelDTO data, AnalysisContext context) { if (data.getStudentNo() == null || data.getScoreValue() == null) { log.warn("跳过非法行: {}", data); return; } rows.add(data); if (rows.size() >= BATCH_SIZE) { scoreService.saveBatchScore(rows); rows.clear(); } } @Override public void doAfterAllAnalysed(AnalysisContext context) { if (!rows.isEmpty()) { scoreService.saveBatchScore(rows); rows.clear(); } } }逻辑说明:invoke先做空值校验,学号或成绩为空的行直接跳过,不让脏数据进库;rows是内存缓存,攒够1000条批量写一次,能显著减少数据库连接往返。doAfterAllAnalysed处理最后不足1000条的剩余数据,避免漏写。
Controller里接收前端上传的MultipartFile并启动解析:
@PostMapping("/import") public Result<String> importExcel(@RequestParam("file") MultipartFile file) throws IOException { EasyExcel.read(file.getInputStream(), ScoreExcelDTO.class, new ScoreExcelListener(scoreService)) .sheet() .headRowNumber(1) .doRead(); return Result.ok("导入完成"); }参数说明:.sheet()默认读第一个Sheet,如果你的Excel有多个Sheet,需要逐个.sheet(name)指定;headRowNumber(1)表示第一行是表头,从第二行开始解析。MultipartFile拿到的是输入流,不需要先把文件写到磁盘,直接交给EasyExcel。入库的saveBatchScore里,MyBatis-Plus的saveBatch默认也是分批提交,底层还是单条insert,但至少比循环里一次次连数据库好。想更快,可以在Mapper里写一个foreach批量insert,把1000条组装成一条SQL。
3.3 数据清洗:重复导入、缺失成绩和“0分”的边界
Excel导入最常见的问题就是同一个文件被上传了两次。因为表上已经有唯一索引,最稳妥的做法是用“有则更新,无则插入”的SQL兜底:
INSERT INTO score (student_no, course_no, exam_type, score_value, exam_time) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE score_value = VALUES(score_value);ON DUPLICATE KEY UPDATE会让同一次考试的成绩覆盖成最新值,而不是报“Duplicate entry”。这么设计的好处是用户重复上传时有后悔药,坏处是如果老师改错成绩再上传,旧正确数据也会被覆盖。所以更严格的做法是导入前先按(student_no, course_no, exam_type)查一遍库,把库中已有和文件里相同的行单独列出来让用户确认。
成绩是0分和缺考要分开处理:0分说明学生参加了考试但不得分,统计时要计入平均分;“缺考”在Excel里通常是空单元格,不应该插入成绩记录。很多数据库设计会把缺考也存成0,导致平均分被拉低,这就是写论文时最难解释的“口径”问题。
在可视化页面里,我会加一个“最近导入记录”列表,展示每次导入的文件名、成功条数、跳过条数。这不是加分项,而是排查数据问题时最快的抓手。数据“进得来”只是第一步,下一章要解决的是“怎么算得准、画得出来”。
4. 数据“看得出来”:聚合统计接口与ECharts图表的联动
4.1 查询口径:用一条SQL把平均分、及格率、分数段算出来
后端统计最忌讳在Java内存里for循环算平均分。成绩记录几千条时还能跑,一旦上了万,接口响应就会肉眼可见变慢。正确做法是把聚合交给MySQL,Service只负责组织参数和返回结构。
以“某班级某次考试”为例,Mapper XML里写:
<select id="selectClassScoreStats" resultType="map"> SELECT COUNT(DISTINCT sc.student_no) AS student_count, ROUND(AVG(sc.score_value), 2) AS avg_score, ROUND( SUM(CASE WHEN sc.score_value >= 60 THEN 1 ELSE 0 END) / COUNT(sc.score_value) * 100, 2 ) AS pass_rate FROM score sc JOIN student s ON s.student_no = sc.student_no WHERE s.class_id = #{classId} AND sc.exam_type = #{examType} AND sc.score_value IS NOT NULL </select>逻辑说明:COUNT(DISTINCT sc.student_no)统计实际有成绩的学生数;AVG(sc.score_value)只对有分数的记录求平均;及格率的分母用COUNT(sc.score_value)而不是COUNT(*),这样缺考空行不会混进来。ROUND(..., 2)保留两位小数,避免返回一串浮点尾数。
如果你用的是MySQL,SUM(CASE...)/COUNT(...)在整数之间做除法时,MySQL会自动转成小数,这一步不会丢精度。但Java端如果用Integer接收就错了,建议返回值用BigDecimal或Map<String, Object>接收,前端再统一转成数字。
Service层和组织接口只需要很薄一层:
public Map<String, Object> getClassAnalysis(Long classId, String examType) { return scoreMapper.selectClassScoreStats(classId, examType); }Controller暴露成GET接口:
@GetMapping("/class/{classId}/analysis") public Result<Map<String, Object>> classAnalysis(@PathVariable Long classId, @RequestParam(defaultValue = "期末") String examType) { return Result.ok(gradeService.getClassAnalysis(classId, examType)); }@RequestParam(defaultValue = "期末")是给前端兜底的:如果页面第一次加载没选择考试类型,后端不会因为参数为空而抛NPE,默认展示最后一次考试。这个习惯在可视化系统里特别重要,前端图表组件经常先挂载再发请求,参数不齐是常态。
4.2 分数段分布:后端算好区间,前端接到就能画
分数段分布如果用Java遍历算,代码又长又容易漏边界。直接在SQL里用CASE WHEN分段:
SELECT CASE WHEN sc.score_value < 60 THEN '不及格' WHEN sc.score_value < 70 THEN '60-69' WHEN sc.score_value < 80 THEN '70-79' WHEN sc.score_value < 90 THEN '80-89' ELSE '90-100' END AS segment, COUNT(*) AS cnt FROM score sc JOIN student s ON sc.student_no = s.student_no WHERE s.class_id = #{classId} AND sc.exam_type = #{examType} AND sc.score_value IS NOT NULL GROUP BY segment ORDER BY MIN(sc.score_value)注意这里的区间是左闭右开:60分落入“60-69”,90分落入“90-100”,100分也被ELSE收进“90-100”。如果写score_value <= 100 AND score_value >= 90作为90分段的判断,100分会重复,这是分数段统计最典型的翻车点。ORDER BY MIN(sc.score_value)是让“不及格”按分数从低到高排序,而不是按分段名称的字典序。如果写成ORDER BY cnt DESC,柱状图会按人数多少排列,视觉上不利于看分布趋势。
返回给前端时,我会把一个班级的各项指标合成一个对象:
{ "studentCount": 42, "avgScore": 74.50, "passRate": 83.33, "segments": [ {"segment": "不及格", "cnt": 7}, {"segment": "60-69", "cnt": 10}, {"segment": "70-79", "cnt": 12}, {"segment": "80-89", "cnt": 9}, {"segment": "90-100", "cnt": 4} ] }这个JSON结构就是前后端约定的“数据合同”。前端不关心SQL怎么算,只按字段名拿数;后端不关心图形怎么画,只保证字段名稳定。很多项目做到后面前后端互相甩锅,本质都是没把这份结构提前定下来。
4.3 ECharts联动:接口返回和图表配置要对齐
前端我一般用原生JavaScript + ECharts,不引入大框架,毕设演示完全够用。页面里放一个<div id="classChart" style="height:400px"></div>,然后请求接口:
async function renderClassChart(classId) { const res = await fetch(`/api/grade/class/${classId}/analysis?examType=期末`); const json = await res.json(); const stat = json.data; const chart = echarts.init(document.getElementById('classChart')); chart.setOption({ xAxis: { type: 'category', data: stat.segments.map(s => s.segment) }, yAxis: { type: 'value' }, series: [{ name: '人数', type: 'bar', data: stat.segments.map(s => s.cnt) }] }); }这段代码的坑在于:如果json.data里的字段叫scoreSegments,而这里写成segments,图表会静默空白,且控制台没有报错。我通常会先console.log(json)看一眼结构再写map,省得面对一个“黑匣子”。
图表类型的选型也有讲究,不是所有指标都适合柱状图。我的习惯是:
| 指标 | 推荐图表 | 原因 |
|---|---|---|
| 班级平均分对比 | 柱状图/条形图 | 类别少,直观看高矮 |
| 分数段分布 | 柱状图 | 连续区间,适合统计频次 |
| 个人成绩走势 | 折线图 | 坐标轴是考试时间 |
| 多科成绩对比 | 雷达图 | 单学生多维度对比 |
雷达图做出来最像“可视化作品”,在答辩时很出彩。但雷达图的数据结构比柱状图复杂,要求后端返回一个固定顺序的指标数组,比如语文、数学、英语、专业课,前端做indicator和data对齐。如果指标顺序前后端不一致,图形会变形得厉害,这也是毕业设计里常见的“玄学Bug”。
另外,如果你用的是前后端分离部署,把Vue或React的dist打包产物放进SpringBoot的static目录时,记得处理路由刷新404。常见做法是在WebMvcConfigurer里加一个viewController把非静态路径转发到index.html,否则直接访问/dashboard刷新就是白屏。这里不展开,但遇到时知道是路由问题就行。
5. 避坑与排查:从启动报错到答辩数据对不上的5个问题
成绩可视化系统看起来简单,实际跑起来遇到的问题却不少。这里按“现象→原因→解决”写5条我见过的典型坑,很多是新手必踩。
5.1 数据库中文乱码:Excel里的“张三”入库成“???”
现象:用EasyExcel导入学生名单后,查询页面显示“???”或乱码,控制台SQL日志里也是乱码。
原因:八成是JDBC连接串没指定编码,或建库时字符集不是utf8mb4。Excel读出来的字符本身是对的,问题出在Java往MySQL写的那条路上。
解决:把application.yml里的连接串改成jdbc:mysql://localhost:3306/grade_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai,并确认建库语句用了DEFAULT CHARACTER SET utf8mb4。如果表已经建好,可以执行ALTER TABLE student CONVERT TO CHARACTER SET utf8mb4;,但注意这会重建表,数据量大时先备份。
5.2 Mapper注入失败:SpringBoot版本太高带来的连锁反应
现象:项目启动报Field mapper in ... required a bean of type 'XxxMapper' that could not be found。
原因:一个是Mapper接口没被Spring扫描到;另一个是SpringBoot版本和MyBatis-Plus不兼容。Spring Boot 3.x把很多Java EE规范迁移到jakarta命名空间,旧版mybatis-plus-boot-starter没法正常工作。
解决:先确认启动类上有没有@MapperScan("com.example.grade.mapper"),或者每个Mapper接口上加@Mapper。再检查依赖,Spring Boot 3.x要用mybatis-plus-spring-boot3-starter,不是原来的mybatis-plus-boot-starter:
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <version>3.5.x</version> </dependency>我之前见过有人把Spring Boot直接拉到3.2.x,还在用旧依赖,启动必翻车,换starter后立刻正常。
5.3 Excel导入报“Can not find the .xlsx suffix”
现象:调用导入接口后,EasyExcel抛出异常,提示找不到xlsx后缀,或者POI报OfficeXmlFileException。
原因:文件本身是xlsx格式,但被手动改了后缀成.xls;或者文件是WPS导出的加密文档。EasyExcel对后缀和内容匹配很敏感。
解决:后端做一层校验,只接收.xlsx结尾的文件;如果用户拿到的是.xls,用Excel/WPS另存为xlsx再传。代码里可以加一个简单的后缀判断:
String filename = file.getOriginalFilename(); if (filename == null || !filename.toLowerCase().endsWith(".xlsx")) { return Result.error("请上传 xlsx 格式文件"); }这一段不是多余的,它能让“导入失败”的错误信息在页面上明确显示,而不是让用户盯着一个堆栈截图来问你。
5.4 图表不更新:接口返回正常,页面还是旧数据
现象:数据库里改了成绩,浏览器刷新多次,柱状图还是原来的高度。
原因:GET请求被浏览器或代理缓存;或者ECharts的setOption默认是merge模式,新旧series名称相同但数据不同时,部分旧配置残留在图表里。
解决:请求时加一个时间戳参数,强迫浏览器拿新数据:
fetch(`/api/grade/class/${classId}/analysis?examType=期末&ts=${Date.now()}`)同时给chart.setOption(option, true),第二个参数notMerge为true,表示完全替换配置而不是合并。这样能避免很多“明明接口有数据,页面就是不动”的诡异问题。当然,后端加Cache-Control: no-cache是更彻底的做法,毕设里两种都写上最稳。
5.5 及格率超过100%或显示“-”
现象:某班及格率算出来是120%,或者空班级页面上显示“-”/NaN。
原因:及格率的口径没定好。如果统计的是“某课程某次考试”,一个学生只该有一条成绩;但重复导入或统计范围跨考试类型,同一个学生在分母里出现多次,及格率就可能超过100%。另一个极端是没有成绩记录,SQL聚合返回NULL,前端Number(null)变成0还能接受,直接字符串拼接就显示“-”。
解决:定死口径。单科及格率的分母用COUNT(DISTINCT student_no),分子用COUNT(DISTINCT CASE WHEN score_value >= 60 THEN student_no END);如果统计的是“全科及格率”,则按科次算,“及格科次/总科次”最大值就是100%,不存在超过100%。空数据用IFNULL兜底:
ROUND(IFNULL(SUM(...) / COUNT(...), 0), 2) AS pass_rate统计口径是答辩时最容易被追问的点。你必须在论文里写清楚:及格率的定义是“人数口径”还是“科次口径”,为什么这么定义。写清楚这句话,比多画两张图有用得多。
6. 从“能跑”到“高分”:三个验证技巧让毕设经得起追问
6.1 把统计口径整理成一张“口径表”
写代码时觉得“及格率就是及格人数除以总人数”,到了论文评审现场容易被问住。我一般会做一张口径表,贴在接口文档或README最前面:
| 指标 | 计算公式 | 分母说明 |
|---|---|---|
| 平均分 | SUM(score_value) / COUNT(score_value) | 只有成绩记录的学生 |
| 及格率 | COUNT(DISTINCT CASE WHEN score_value >= 60 THEN student_no END) / COUNT(DISTINCT student_no) | 实际参考学生数 |
| 分数段 | CASE WHEN 分组 | 左闭右开,100分进入90-100 |
这张表的价值在于,答辩时评委问“为什么这个班及格率是83.33%”,你可以把SQL指给他看,而不是说“程序算的应该对吧”。建立口径表还能反过来发现代码里的不一致,比如“平均分包含缺考但及格率不包含”,一眼就能看出来。
6.2 用JUnit给统计方法上个保险
成绩统计是核心,改一次接口就该回归一次。写一个简单的@SpringBootTest,插入固定数据后断言结果:
@SpringBootTest class GradeServiceTest { @Autowired private GradeService gradeService; @Test void passRateShouldBe100WhenAllScoresPass() { Map<String, Object> result = gradeService.getClassAnalysis(1L, "期末"); double passRate = Double.parseDouble(result.get("pass_rate").toString()); assertEquals(100.0, passRate, 0.01); } }逻辑说明:测试前先通过SQL或MyBatis-Plus向库里插入一班5个学生的期末成绩,全部大于60分;然后调用getClassAnalysis,断言及格率等于100。虽然测试里出现了魔法数字1L,但毕设项目里可读性比严格的测试隔离更重要。这段代码放在答辩演示里,比写十页“系统测试”截图都有说服力。
6.3 给可视化页面加一个安全数字函数
图表最怕后端返回null,一个空值能让整张图白屏。我习惯在前端写一个小工具函数,所有字段过一遍再交给ECharts:
function safeNumber(value) { const num = Number(value); return Number.isNaN(num) ? 0 : num; }然后在取数时用safeNumber(stat.avgScore ?? 0)包裹。这不是技术含量多高的东西,但能避免答辩现场“页面打不开”的尴尬。
这套系统我前后做过两版,第一版把所有精力放在堆图表上,结果被评委一句“你的及格率口径是什么”问住;第二版先把数据库设计、统计口径和接口结构定死,图表反而两天就画完了。现在我接手任何可视化项目,第一件事永远是理口径、定字段,而不是开IDE。希望帮到你。
本文还有配套的精品资源,点击获取