SpringBoot+Vue学生综合成绩测评系统:从数据库设计到全栈部署
简介一套基于SpringBootVue的学生综合成绩测评系统面向计算机专业毕业设计、课程设计及期末大作业人群也适合项目实战练习的Java学习者。系统覆盖成绩录入、综合测评、统计分析等功能采用前后端分离架构业务模块划分清晰已获导师指导且高分通过可帮助解决从零搭建可运行毕设项目的难题。压缩包共946个文件约18.66MB含Java后端源码、Vue前端组件、JS逻辑脚本、SQL数据库脚本及开发说明文档附论文文档、答辩PPT、演示视频、启动脚本与配置文件目录分层明确便于按模块检索与对照学习。项目已严格调试可正常启动运行开发环境为JDK1.8、MySQL5.7及Maven环境可作为毕业设计底座二次开发。已有168人学习下载适合需要快速拿到可运行源码并理解前后端整合流程的读者。1. 从课设题目到可运行系统基于SpringBootVue的学生综合成绩测评系统到底要做什么基于SpringBootVue的学生综合成绩测评系统是Java课程设计和毕业设计里出镜率最高的题目之一。它不复杂但业务闭环完整教师登录后录入学生多维度成绩系统按配置好的权重自动合成综合成绩并评定等级学生端能查看自己的成绩单和班级排名管理员还能看到各课程成绩分布和统计数据。这篇笔记就顺着这个标题把数据库设计、SpringBoot后端、Vue前端、联调部署到答辩准备的整条线讲清楚。适合正在赶课设或毕设的同学也适合想把这个题目从“能跑”做成“能讲”的从业者直接照着复现。2. 成绩测评的数据基石三张核心表与综合成绩权重规则设计2.1 需求拆解这套系统不是简单成绩CRUD标题里最容易被误解的词是“测评”。很多同学拿到题目的第一反应是“不就是一张成绩表增删改查吗”真做起来才发现成绩测评系统和管理系统的分界线在于它多了一层“合成与判定”逻辑。教师录入的往往不是单一总分而是平时成绩、期中成绩、期末成绩三个维度系统需要按权重合成综合成绩再根据分数区间映射到“优秀、良好、中等、及格、不及格”五个等级。这还没完教师可能需要按课程查排名、按班级看分布、按学期对比平均分学生端要按学期查询自己的成绩单。这些需求全部压到一张表上后面会非常痛苦。我的习惯是任何系统都先画数据流不急着写代码。成绩测评系统的核心数据流其实只有一条原始成绩录入 → 权重合成 → 等级评定 → 排名统计 → 前端展示。设计数据库时只需要问一个问题哪些数据是需要“存下来”的哪些数据是“算出来”的。综合成绩和等级属于“算出来”的但通常会冗余存储因为榜单、图表查询频率远高于录入频率每次现算在数据量上来之后会拖垮接口。排名则不建议存库而是查询时用窗口函数实时算避免频繁更新时排名字段大面积失效。2.2 建表SQLstudent、course、score三张表的结构与字段说明常见的表结构是学生表、课程表、成绩表三张核心表外加一张权重配置表。学生表和课程表是标准字典表字段按需求补充即可不必过度设计。成绩表是重点它需要同时记录原始分、合成分、等级和学期。CREATE TABLE student ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT 学号, name VARCHAR(50) NOT NULL COMMENT 姓名, class_name VARCHAR(50) NOT NULL COMMENT 班级, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE course ( id BIGINT PRIMARY KEY AUTO_INCREMENT, course_code VARCHAR(20) NOT NULL UNIQUE COMMENT 课程代码, course_name VARCHAR(100) NOT NULL COMMENT 课程名称, credit DECIMAL(3,1) DEFAULT 0.0 COMMENT 学分 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE score ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL, course_id BIGINT NOT NULL, daily_score DECIMAL(5,1) DEFAULT 0 COMMENT 平时成绩, midterm_score DECIMAL(5,1) DEFAULT 0 COMMENT 期中成绩, final_score DECIMAL(5,1) DEFAULT 0 COMMENT 期末成绩, total_score DECIMAL(5,1) DEFAULT 0 COMMENT 综合成绩, grade VARCHAR(10) DEFAULT COMMENT 等级, semester VARCHAR(20) NOT NULL COMMENT 学期如2025-01, UNIQUE KEY uk_student_course_semester (student_id, course_id, semester), KEY idx_course_semester (course_id, semester) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这套表结构有几个关键点。第一score表加了uk_student_course_semester唯一约束防止同一个学生同一门课同一学期录入两次成绩这在后期导出和统计时能省掉大量去重麻烦。第二成绩字段全部用DECIMAL(5,1)五位有效数字、一位小数这是成绩系统的硬性精度要求数据库层面就拦住“74.55”这类原始值。第三综合成绩虽然能算出来但这里落库了因为前端榜单、统计图表、导出Excel都要高频读取它冗余一个字段比每次聚合计算划算得多。另外我一般会加一张权重配置表而不是把权重写死在代码里。题目如果要求“可配置”这张表就是评分的核心卖点即使不要求它也能让你在答辩时多讲一个“系统可维护性”的亮点。CREATE TABLE weight_config ( id BIGINT PRIMARY KEY AUTO_INCREMENT, config_name VARCHAR(50) NOT NULL COMMENT 配置名称, semester VARCHAR(20) NOT NULL COMMENT 适用学期, daily_weight DECIMAL(4,2) NOT NULL DEFAULT 0.20, midterm_weight DECIMAL(4,2) NOT NULL DEFAULT 0.30, final_weight DECIMAL(4,2) NOT NULL DEFAULT 0.50, last_updated DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_semester (semester) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;权重表的默认值我按“平时20%、期中30%、期末50%”来设这是国内多数高校课程考核的常见比例。注意权重字段用DECIMAL(4,2)能存0.00到99.99比例合计是否等于1.00由后端在保存时校验MySQL约束写这种业务规则不划算。把semester设为唯一键保证每个学期只有一套生效权重避免前端下拉框里出现一堆历史版本。2.3 综合成绩与等级评定的计算规则权重怎么配、等级怎么划综合成绩的计算公式本身很简单就是三个原始分按权重相加。但落代码时有两个坑一个是浮点误差另一个是“先乘再加”还是“全部算完再四舍五入”。常见做法是全部算完后再保留一位小数而不是每一步都四舍五入否则89.9和90之间可能因为分步舍入产生边界误判。等级映射我建议在后端用纯函数写保持无状态方便前端展示和后续调整。public static String gradeOf(double totalScore) { if (totalScore 90) return 优秀; if (totalScore 80) return 良好; if (totalScore 70) return 中等; if (totalScore 60) return 及格; return 不及格; }这里有个容易被忽略的边界89.9到底算“良好”还是“优秀”。如果按四舍五入到一位小数89.9就是89.9归“良好”但如果系统里明确“90分及以上为优秀”那么89.9就确实不该进优秀档。这种边界不需要做特殊处理但要清楚自己使用的比较运算符是大于等于还是大于并在答辩时能解释清楚。最稳妥的做法是前端提示和后端判定保持一致统一用“”。排名逻辑则不建议在综合成绩落库时同步写一个rank字段。因为任何一条成绩被修改全表排名都会变动更新成本极高。更优雅的做法是利用MySQL 8.0的窗口函数实时计算排名SELECT s.name, s.student_no, sc.total_score, RANK() OVER (PARTITION BY sc.course_id, sc.semester ORDER BY sc.total_score DESC) AS rank_no FROM score sc JOIN student s ON sc.student_id s.id WHERE sc.course_id #{courseId} ORDER BY sc.semester, rank_no;这段SQL的PARTITION BY按课程和学期分组同一门课同一学期的学生单独排名RANK()处理同分并列的方式也符合学校排名的直觉——两个学生同为90分名次并列第1下一个是第3而不是第2。如果要求同分按学号先后来区分名次可以把ORDER BY sc.total_score DESC后面追加s.student_no ASC这个细节同样能在答辩时当加分项讲出来。3. SpringBoot后端把测评逻辑写成能复用的Service3.1 实体映射与数据访问层选型JPA还是MyBatisSpringBoot接入数据库有两条主流路线Spring Data JPA和MyBatis/MyBatis-Plus。课程设计项目里两条路线都有人用但我的建议是如果你熟悉SQL能自己控制查询语句选MyBatis-Plus如果想让代码量更少、仓库层零SQL选Spring Data JPA。成绩测评系统里存在多表联查和窗口函数排名这类需求用JPA反而绕MyBatis的XML或注解SQL写起来更直白排查问题时黑匣子也小一些。选型还需要考虑题目给的时间。一周内要交的课设MyBatis-Plus的代码生成器能帮你把单表CRUD瞬间生成完把精力留给测评逻辑本身。下面的代码示例我都按MyBatis-Plus来写毕竟它就是日常开发里最常见的那条路。3.2 实体映射与数据访问层注解别漏查询别裸奔先写出对应score表的实体类字段名和表字段通过驼峰映射自动对齐不需要额外写繁琐的TableField。Data TableName(score) public class Score { TableId(type IdType.AUTO) private Long id; private Long studentId; private Long courseId; private BigDecimal dailyScore; private BigDecimal midtermScore; private BigDecimal finalScore; private BigDecimal totalScore; private String grade; private String semester; }这里有几个容易踩的坑。第一score表名在MySQL里不算保留字但个别数据库方言可能有冲突最稳妥的是在TableName(score)里明确写上表名别省略。第二字段类型用BigDecimal别用Double。数据库里的DECIMAL(5,1)映射到Double会因为二进制浮点的精度问题在计算时产生“玄学”误差BigDecimal虽然运算时麻烦一点但这是成绩系统的底线。第三TableId(type IdType.AUTO)对应数据库自增主键这行不写的话MyBatis-Plus默认按雪花ID生成策略处理插入数据后主键值和你预期不一致。Mapper接口不需要写方法继承BaseMapper即获得单表CRUD能力。Mapper public interface ScoreMapper extends BaseMapperScore { // 排名和分组统计用自定义SQL ListScoreRankVO selectScoreRank(Param(courseId) Long courseId, Param(semester) String semester); }复杂查询在接口里声明方法SQL写在对应的Mapper.xml里或者直接用Select注解写在接口方法上。我习惯用XML因为排名SQL带了窗口函数后续可能加过滤条件XML里维护比注解里拼字符串舒服得多。注意Param(semester)的参数名要和XML里#{semester}完全一致踩过一次“参数绑定失败”的报错后你就不会再写错这个了。3.3 测评计算Service不要在Controller里写业务计算逻辑写在Controller是课设项目最容易出现的翻车现场。成绩测评不是单纯的增删改查它包含权重校验、综合成绩计算、等级映射、事务控制四步每步都需要被多个接口复用。录入成绩时要用修改某条成绩时也要用批量导入时还要用。把这套逻辑封装到ScoreService里Controller只负责接收请求参数和返回统一结构这是“设计”二字在代码层面最直接的体现。Service RequiredArgsConstructor public class ScoreService { private final ScoreMapper scoreMapper; private final WeightConfigMapper weightConfigMapper; Transactional public void saveScore(ScoreDTO dto) { WeightConfig weight weightConfigMapper.selectOne( new LambdaQueryWrapperWeightConfig() .eq(WeightConfig::getSemester, dto.getSemester())); if (weight null) { throw new BizException(当前学期未配置权重请联系管理员); } BigDecimal total calcTotal(dto, weight); Score score new Score(); // 按 dto 逐字段拷贝 score.setStudentId(dto.getStudentId()); score.setCourseId(dto.getCourseId()); // ... 其余字段省略 score.setTotalScore(total); score.setGrade(ScoreGradeUtil.gradeOf(total.doubleValue())); scoreMapper.insert(score); } private BigDecimal calcTotal(ScoreDTO dto, WeightConfig weight) { BigDecimal daily dto.getDailyScore() .multiply(weight.getDailyWeight()); BigDecimal midterm dto.getMidtermScore() .multiply(weight.getMidtermWeight()); BigDecimal finalScore dto.getFinalScore() .multiply(weight.getFinalWeight()); return daily.add(midterm).add(finalScore) .setScale(1, RoundingMode.HALF_UP); } }这段代码里有三个关键设计。第一Transactional保证同一条成绩的所有字段更新在一个事务里中间任何一步抛错数据库回滚到操作前状态不会出现总分更新了但等级还是旧值的半截数据。第二权重从数据库读而不是从配置文件读是因为成绩测评系统有强烈的分学期诉求权重属于业务数据而非系统配置放数据库里才能支持“管理员在界面上改比例成绩立刻按新比例重算”。第三setScale(1, RoundingMode.HALF_UP)做的是“四舍五入保留一位小数”RoundingMode.HALF_UP就是学校里说的“四舍五入”HALF_DOWN则是“五舍六入”别选错。权重比例之和等于1的校验也要写在这个Service里在保存权重配置时执行。public void saveWeight(WeightConfig weight) { BigDecimal sum weight.getDailyWeight() .add(weight.getMidtermWeight()) .add(weight.getFinalWeight()); if (sum.compareTo(BigDecimal.ONE) ! 0) { throw new BizException(权重比例之和必须等于1); } weightConfigMapper.insert(weight); }注意这里用compareTo而不是equalsBigDecimal(0.2).add(BigDecimal(0.3)).add(BigDecimal(0.5))在数值上等于1但equals比较的是精度描述会认定1.0和1.00不同而compareTo只比较数值大小。这个坑不写测试用例很难发现属于典型的“看起来对但实际有隐患”的代码。3.4 统一返回结果与分页查询接口长什么样才不算野路子前后端分离项目里接口统一返回结构是基础工程。我常用的结构是code、message、data三个字段成功时code为200业务异常时code为业务码HTTP状态码仍然返回200。这样做的好处是前端拦截器只需要处理一种格式网络层和业务层错误分开判断。Data public class ResultT { private int code; private String message; private T data; public static T ResultT ok(T data) { ResultT r new Result(); r.code 200; r.message success; r.data data; return r; } }分页查询我直接使用MyBatis-Plus的Page对象它会把总记录数、总页数一并查出来前端表格组件所需要的字段基本都齐了。传入页码和每页条数即可不需要自己拼LIMIT语句再单独写一条COUNT。GetMapping(/score/page) public ResultIPageScoreVO page(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, RequestParam(required false) String semester) { PageScoreVO page new Page(pageNum, pageSize); LambdaQueryWrapperScore wrapper Wrappers.lambdaQuery(); if (StringUtils.hasText(semester)) { wrapper.eq(Score::getSemester, semester); } return Result.ok(scoreMapper.selectScorePage(page, wrapper)); }selectScorePage这个方法是自定义的因为在成绩列表里用户要看到的不只是score表本身还需要学生的姓名、学号、班级和课程名。这需要多表联查MyBatis-Plus的selectPage只能单表分页所以我把分页查询的SQL写到XML里表连接学生表和课程表用where标签动态拼接过滤条件。Page对象作为第一个参数传给selectScorePage时MyBatis-Plus会自动拦截并拼接LIMIT这个机制是插件底层封装好的不需要你显式做任何分页运算。3.5 成绩分布统计给前端图表准备的一份数据前端要画柱状图或饼图后端就要提供一个按等级分组的统计接口。这个统计逻辑用SQL的GROUP BY做掉比在Java里遍历分类要干净一次查询把所有等级的计数组装好。select idselectGradeDistribution resultTypemap SELECT grade AS name, COUNT(*) AS value FROM score WHERE course_id #{courseId} AND semester #{semester} GROUP BY grade /select返回的ListMapString, Object结构直接对应ECharts饼图的{name: 优秀, value: 12}数据格式前端拿到后几乎不用加工就能塞进图表。后端做统计时要注意把成绩为空的学期也考虑进去比如某学期还没录入成绩返回空数组前端显示“暂无数据”而不是一个报错的白屏页面。这类边界处理在答辩演示时特别容易出彩讲师问一句“空数据怎么办”你接一句“后端返回空列表前端有empty状态展示”这题就答完了。SpringBoot的完整配置里我还会在application.yml中把spring.datasource.hikari.maximum-pool-size设为10到20之间。很大一部分课设项目是几个人共用一台开发机测试环境数据库连接池默认10就够用调得太大反而容易把测试库的连接数拖爆。4. Vue前端成绩录入、明细查看与可视化图表4.1 环境准备与路由设计Vue2还是Vue3依赖怎么装前端部分先解决技术栈选择。Vue2与Element UI是课设项目里的老搭档教程多、组件行为稳定Vue3与Element Plus则在新技术栈上更体面且Vite启动速度快到“秒开”。我的建议是如果你的SpringBoot版本选的是2.x系列前端用Vue2不用思考太多如果后端选的是SpringBoot 3.x那前端与其硬配老组件不如直接用Vue3加Vite生态更配套。以Vue3为例从零初始化项目并安装依赖的命令如下npm create vitelatest score-web -- --template vue cd score-web npm install npm install vue-router4 axios element-plus echarts npm run dev这里最容易翻车的是依赖安装阶段。npm install执行到一半报各种ERR! code ERESOLVE和peer依赖冲突多半是Node版本问题。Vue3加Vite要求Node 16及以上但Element Plus新增版本对Node 18更友好我一般是保证本地Node版本为18或20的LTS版本再动手。如果你电脑上有多个Node版本频繁切换建议用nvm管理不要裸着装最新版Node跑老模板。依赖装完Vue路由需要拆模块不能全堆在main.js里。import { createRouter, createWebHistory } from vue-router const routes [ { path: /, name: Dashboard, component: () import(/views/Dashboard.vue) }, { path: /score/input, name: ScoreInput, component: () import(/views/ScoreInput.vue) }, { path: /score/detail, name: ScoreDetail, component: () import(/views/ScoreDetail.vue) }, { path: /score/detail/:studentId, name: ScoreDetailWithId, props: true, component: () import(/views/ScoreDetail.vue) } ]路由拆到独立文件是必要的因为项目一旦加入“教师端、学生端、管理员端”的角色路由守卫所有逻辑都集中在一个router/index.js里显然比撒在入口文件里可维护得多。这里用() import()做路由懒加载首屏只加载默认页面访问到成绩录入页时才拉对应组件代码性能优化的细节在答辩时提一嘴讲师会认为你有工程化意识。4.2 成绩录入表单动态校验与数值边界成绩录入页是整个前端最重要的交互场景。教师填写学生、课程、平时、期中、期末五类信息最容易出错的是“成绩范围越界”和“录了半截还提交”。表单校验用Element Plus的rules即可自定义validator把0到100的边界堵死同时用v-model.number让输入框值自动转成数字而不是字符串否则你在提交后还得做一次类型转换。el-form refscoreFormRef :modelscoreForm :rulesscoreRules label-width100px el-form-item label平时成绩 propdailyScore el-input-number v-modelscoreForm.dailyScore :min0 :max100 :step1 / /el-form-item /el-formconst scoreRules { dailyScore: [ { required: true, message: 请输入平时成绩, trigger: blur }, { validator: (rule, value, cb) { if (value null || value undefined) { cb(new Error(请输入成绩)) } else if (value 0 || value 100) { cb(new Error(成绩必须在0-100之间)) } else { cb() } }, trigger: blur } ] }el-input-number看起来很省事它自带增减按钮和最小值最大值约束但从键盘输入时校验依然要走rules里的validator两套机制各管一层。注意:step1是步长它不会阻止用户输入带小数的值需要在validator里放行一位小数并拒绝更多小数否则后端保存时DECIMAL字段会直接被MySQL截断用户看到的结果和提交的不一致。这份规则对平时、期中、期末三个字段分别配置一份不要图省事共用一个校验函数却在报错信息里不区分阶段。提交后的处理逻辑也值得注意。保存成功后不要立刻router.push跳走先刷新当前列表再跳转给用户一个“我录进去的数据已经生效了”的确定感。如果后端返回业务异常把message字段的内容直接放到ElMessage上提示比如“当前学期未配置权重”这类可读性极强的提示是后端Result统一返回结构带来的直接好处。4.3 axios封装与跨域联调配置一次少吵一星期前后端分离开发最折磨人的不是写业务而是接口联调时一堆CORS报错。前端在8080端口后端在8080端口浏览器直接发请求会被跨域拦截。常见做法是前端把代理配置好让联调期间所有请求都走相对路径正式部署时再由Nginx转发前端代码里不写死任何一个后端地址。import axios from axios const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.response.use( res res.data, err { ElMessage.error(err.response?.data?.message || 网络异常请稍后重试) return Promise.reject(err) } ) export default serviceaxios实例的baseURL统一为/api配合开发服务器代理请求会自动转发到SpringBoot。Vite的配置文件里这样处理export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })注意changeOrigin: true这行必须写。它会把请求头里的Host改成目标地址的域名不加这个配置后端如果做了域名校验就会拒绝请求。联调阶段只要前端代理多了这行市面上大部分“为什么401”“为什么跨域报错”的问题都能消停一大半。真正上线时Nginx里配一层location /api { proxy_pass http://xxx:8080; }即可前端代码完全不用改。这个“开发代理生产反向代理”的思路讲出来答辩老师会觉得你对部署链路有整体认识。4.4 ECharts成绩分布图让测评结果看得见学生综合成绩测评系统如果只有表格没有图表做得再完整也显不出“测评”的价值。前端用ECharts画班级成绩分布直方图和课程平均分对比图属于投入产出比最高的展示层功能代码量不大视觉冲击力强。import * as echarts from echarts const chart echarts.init(document.getElementById(gradeChart)) chart.setOption({ tooltip: { trigger: axis }, grid: { left: 3%, right: 4%, bottom: 3%, containLabel: true }, xAxis: { type: category, data: [优秀, 良好, 中等, 及格, 不及格] }, yAxis: { type: value, name: 人数 }, series: [{ data: [12, 34, 20, 8, 3], type: bar, barWidth: 40%, itemStyle: { borderRadius: [4, 4, 0, 0] } }] })图表数据通过接口从后端拿把/api/score/distribution返回的数组替换掉示例数据即可。这里的barWidth建议设成固定百分比而不是自适应因为五个等级类别数量固定比例宽度会让柱子忽胖忽瘦观感不稳定。ECharts在Vue3里的常规用法是先创建一个div容器在组件onMounted阶段初始化实例组件卸载时调用chart.dispose()释放内存否则多次切换路由后会看到“图表空白”或“实例已存在”的告警这是ECharts在单页应用里最典型的反面教材。图表下方再配一张明细表展示每个等级对应的人数和占比这个表用Element Plus的el-table即可数据和ECharts共用同一个接口返回不需要二次请求。新增一个学生成绩后重新拉取分布接口图表自动刷新整个闭环就完整了。5. 跑通全栈的避坑清单从数据库连接到打包部署的五个翻车现场5.1 连接MySQL报错时区、SSL与字符集三连首次启动SpringBoot项目连接本地MySQL时最常见的报错是The server time zone value йʱ后面还跟着Unable to load authentication plugin caching_sha2_password这类信息。理解这个现象的关键在于MySQL 8.0默认使用caching_sha2_password认证插件而项目里用的MySQL Connector版本如果过旧就无法完成握手验证报错信息还往往指向时区而不是认证很误导人。解决办法是在application.yml里把连接串写完整。我的固定写法是spring: datasource: url: jdbc:mysql://localhost:3306/score_db?useUnicodetruecharacterEncodingutf8mb4useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.DriveruseSSLfalse是本地开发必加的MySQL 8默认开启SSL本地没配证书会报一堆SSL握手日志。serverTimezoneAsia/Shanghai解决时区问题allowPublicKeyRetrievaltrue解决caching_sha2_password插件模式下“Public Key Retrieval is not allowed”的报错。字符集要写明utf8mb4而不是utf8数据库层面也要在建库时用CREATE DATABASE score_db DEFAULT CHARACTER SET utf8mb4;两边统一才能真正存下中文。5.2 跨域请求后Session失效credentials与allowedOrigins的冲突联调时遇到过一个诡异场景登录接口能通登录后的查询接口却报未授权更诡异的是后端日志里根本查不到请求。排查后发现这是跨域配置里allowCredentials(true)和allowedOrigins(*)同时存在导致的。浏览器规范规定Access-Control-Allow-Origin不能是通配符的同时又允许携带凭证因此预检请求直接失败。正确配置是明确指定前端地址Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOrigins(http://localhost:5173) .allowedMethods(GET, POST, PUT, DELETE) .allowCredentials(true) .maxAge(3600); } }如果前端用了代理转发请求本身已经同源了这个CORS配置甚至可以不加。我后来的习惯是开发环境一律靠Vite代理不启用后端CORS配置这样Session、Cookie、登录态都能按同源请求处理少掉一整类联调问题。5.3 Vue打包后刷新404history模式忘了后端配合Vue项目开发时一切正常npm run build之后把静态文件扔到Nginx或者后端static目录里访问首页没问题一刷新子路由就成了404。这个问题的根因是前端用了createWebHistory路由模式刷新时浏览器向服务器请求/score/detail这个真实路径但服务器在对应路径下并没有这个文件就回了404。两个方向解决后端能用Nginx的话配置try_files把不存在的路径全部指回index.html这是生产环境最正统的做法。嫌Nginx麻烦只打算把dist目录塞进SpringBoot的话要么把路由改成createWebHashHistory模式URL带#号丑但稳定要么添加一个转发控制器在Java里捕获所有非静态资源路径并转发到首页。我建议直接认领hash模式毕竟课设追求的是省心和可演示URL好不好看并没有那么重要。5.4 综合成绩出现长尾小数浮点精度与四舍五入的处理后端计算综合成绩时如果用Double类型存权重并参与运算0.2 * 89.5 0.3 * 76 0.5 * 92这类表达式会产生类似85.79999999999998的结果。这个数存进数据库后再查出来展示前端表格里出现一长串小数和录入的原始成绩风格完全不搭还会造成等级判定不稳定的假象。这是典型的二进制浮点精度问题。解决方案在上文Service里已经体现所有金额、成绩、权重字段全链路用BigDecimal运算完成后用setScale(1, RoundingMode.HALF_UP)统一舍入。MySQL侧字段类型是DECIMAL(5,1)实体字段是BigDecimalJSON序列化时只要不额外配置全局类型转换前端拿到的就是一个数字显示正常。如果项目里还有百分比展示比如“优秀率23.5%”同样建议后端算完保留一位小数再返回把精度问题彻底挡在接口内部。5.5 npm install反复失败Node版本与依赖锁文件的玄学前端依赖装不上是另一个高频翻车现场。现象五花八门ERESOLVE unable to resolve dependency tree、ELIFECYCLE command failed、canvas2.9.1 install script failed。多数情况下是Node版本和项目依赖要求的版本不匹配Vue2生态需要旧一点的NodeVue3加Vite需要新Node直接用系统默认版本很难两头兼顾。我一般让同组的同学固定安装Node 16或18的LTS版本删除项目里的node_modules和package-lock.json后重新执行npm install。如果还是报peer依赖冲突检查一下是不是element-plus和vue的大版本对不上Element Plus 2.x要求Vue 3.x和Vue2完全不兼容。开发环境建议统一npm install而不是npm ci后者严格要求锁文件与package.json一致成员各自安装过不同版本的包后npm ci会直接失败虽然它能保证环境一致但课设场景下反而容易卡住进度。6. 答辩前的最后一公里让这套系统从“能跑”变成“能讲”系统跑通只是及格线答辩时想拿高分还要在演示环节突出两个东西一个是评测逻辑的灵活性展示另一个是数据可视化带来的人均信息密度。我建议在成绩列表页加一个“导出成绩单Excel”按钮由后端生成文件这招几乎百试百灵。GetMapping(/score/export) public void export(RequestParam Long courseId, RequestParam String semester, HttpServletResponse response) throws IOException { ListScoreExcelVO list scoreMapper.selectScoreForExport(courseId, semester); response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setHeader(Content-Disposition, attachment;filenamescore_ semester .xlsx); EasyExcel.write(response.getOutputStream(), ScoreExcelVO.class) .sheet(成绩单) .doWrite(list); }导出功能的价值在于它把“测评结果”真正落到了教学管理场景里评委能直观看到系统能替教师省下手工算分和排版的时间。实现时用EasyExcel比用Apache POI手写工作簿代码量少很多字段顺序和表头名靠ExcelProperty(学号)注解控制导出后Excel里的列顺序和页面表格展示顺序保持一致这个小细节都会让演示观感更专业。答辩时一边导出一边说“教师可以把这张单子直接交给学校教务处”这类业务闭环的完整度比任何技术选型都能打动评委。另外一个高频追问是“你们怎么保证成绩不被学生篡改”。哪怕题目没有要求权限控制我也建议在项目里加上JWT登录和拦截器至少覆盖管理员和教师两种角色。实现要用过滤器拦下除登录和静态资源外的所有接口校验Token后再放行这比在Controller方法里手写判断优雅得多能和成绩分页接口共存而不破坏现有结构。最后是演示数据的准备这是我用多次翻车换来的教训导入Excel生成测试数据时不要只造一个班五个人那样柱状图只有孤零零一两根柱子毫无说服力。至少造三个教学班共六十到八十人的成绩覆盖优秀到不及格的完整区间再准备一条“某教师修改权重后综合成绩整体变化”的演示路径。让分布图呈现出明显的分数段落差让排名榜上有真实的前后差异评委才会相信这套系统经得起真实教学数据的考验。我自己早期就是只拿一张五行数据的表去答辩被问“成绩分布是否符合正态分布”时直接哑口无言从那以后演示数据的真实度就被我列进了项目清单里。希望这套从设计到落地的路径能帮你在同样的题目上少走一段弯路。本文还有配套的精品资源点击获取