
简介面向Java毕业设计场景的SSMVue学生成绩管理系统资源包包含完整源码、说明文档与操作录像适合正在准备毕业设计的计算机专业学生与技术爱好者。系统采用SSM框架与Vue构建B/S架构基于MySQL存储数据完整实现管理员、教师、学生三类角色的业务闭环覆盖学生管理、教师管理、优秀师生展示、教学课件、在线答疑、公告信息、班级成绩、学生成绩、试题试卷及考试管理等模块可支撑多角色分权管理。资源共990个文件压缩后约67.89MB主要含166个Java源码、63个Vue页面、156个JavaScript脚本、116个HTML页面、46个CSS样式、4个SQL脚本及大量图片图标后端逻辑、前端交互和数据库初始化内容齐全。另有操作录像、说明文档、组件备份与启动脚本能帮助快速部署和演示便于毕业设计答辩与二次开发。当前已有222人学习下载适合需要以实际项目掌握SSMVue整合开发的Java学习者。1. 这个毕业设计在做什么SSMVue学生成绩系统的真实拆解拿到一个名为 SSMVue项目实战-学生成绩管理系统的Java毕业设计(源码说明录像).zip 的压缩包时先别急着解压。标题已经把技术栈和业务范围说清楚了后端用 SSMSpring SpringMVC MyBatis前端用 Vue核心业务是学生成绩管理。SSM 在 Java 毕业设计里出现频率极高不是因为性能比 Spring Boot 强而是因为它的配置过程足够复杂能让答辩老师看到你对 IoC、AOP、ORM 这些底层概念有没有基本认识再叠一个 Vue 做前后端分离整套东西既能演示接口设计又能展示页面交互。你拿到这套代码后真正要做的事有两件一是让它在你本机跑起来二是搞清楚每个配置和表结构为什么要这样写。接下来按这个顺序把骨架、数据库、前端接口、部署和演示这条线走一遍。2. SSM 整合与项目骨架从 Maven 依赖到三层结构落地2.1 为什么是 SSM 而不是 Spring Boot毕设场景下的选择逻辑SSM总体来说不是新框架而是三个老框架的整合。Spring 管理对象生命周期SpringMVC 负责 Web 层请求分发MyBatis 处理 JDBC 和 SQL 映射。Spring Boot 把这些东西全部自动配置项目会清爽很多但很多学校的课程设计和毕业设计题目仍然写着 SSM原因是显式配置容易体现“整合边界”ApplicationContext 根容器、DispatcherServlet 子容器、SqlSessionFactory 这些关键对象每个都要手动声明老师一问就能看出你是否真的理解。如果你是冲着性能去选 SSM那是误解。SSM 的应用性能并不比 Spring Boot 更高前者在后期的配置维护成本上还要更高一点。但从答辩和教学角度看SSM 项目反而更适合做成毕设框架选型有传统依据配置过程有可讲的细节出问题时也更容易从配置层面定位。一般我会把 Spring 配置拆成几个文件applicationContext.xml管业务层和持久层spring-mvc.xml管 Web 层mybatis-config.xml管 MyBatis 行为最后在 web.xml 里把它们串起来。2.2 用 Maven 搭出 SSM 项目的基础结构传统 SSM 项目大多采用 war 包部署到 TomcatMaven 就是唯一的构建入口。拿到源码后先打开pom.xml确认几个关键依赖spring-context、spring-webmvc、spring-jdbc、mybatis、mybatis-spring、mysql-connector-java以及 servlet-api、jackson-databind。下面是一个可用的依赖清单骨架properties spring.version5.2.22.RELEASE/spring.version mybatis.version3.5.6/mybatis.version mysql.version8.0.27/mysql.version /properties dependencies dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version${spring.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version${mybatis.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.6/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version${mysql.version}/version /dependency /dependencies这里要特别注意Spring 5.2.x 是 SSM 时代常用的版本配合 Tomcat 8.5 或 9.0 都能正常跑MySQL 驱动版本必须和 MySQL 服务端版匹配比如 MySQL 8 需要 8.x 驱动。很多项目启动时报ClassNotFoundException: com.mysql.jdbc.Driver就是因为驱动类名在新版本里改成了com.mysql.cj.jdbc.Driver。此外如果pom.xml里没有显式声明packagingwar/packagingIDEA 可能按 jar 方式处理导致部署到 Tomcat 时找不到页面。至于版本怎么选我的建议是不要盲目跟最新。SSM 项目本身是旧技术栈Spring 5.2.x、MyBatis 3.5.x、JSTL 1.2 这套组合经过大量项目验证比随便升到 Spring 6 后遇到 javax 到 jakarta 命名空间迁移要省心得多。Maven 编译插件也要固定source和target为 1.8否则高版本 JDK 默认编译目标可能与 Tomcat 不兼容。2.3 Spring 与 SpringMVC 双容器初始化2.3.1 web.xml 里区分 ContextLoaderListener 和 DispatcherServletSSM 项目里web.xml是起点它负责启动两个容器一个由ContextLoaderListener创建加载applicationContext.xml另一个由DispatcherServlet创建加载spring-mvc.xml。典型配置如下context-param param-namecontextConfigLocation/param-name param-valueclasspath:spring/applicationContext.xml/param-value /context-param listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener servlet servlet-namedispatcher/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring/spring-mvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namedispatcher/servlet-name url-pattern//url-pattern /servlet-mapping这里最容易出错的是 bean 扫描范围没有隔离。Root 容器如果扫描到了 ControllerSpringMVC 容器又扫描一遍会造成同一 Controller 出现两个实例事务注解有时也会失效。常见做法是 Root 容器只扫描com.example.grade.service和com.example.grade.mapperSpringMVC 容器只扫描com.example.grade.controller。启动后如果发现 Controller 接口返回 404先打开启动日志看是否出现No mapping found for HTTP request再检查两个容器的扫描包。2.3.2 spring-mvc.xml 与 applicationContext.xml 的分工spring-mvc.xml里需要开启注解驱动和组件扫描并配置静态资源放行context:component-scan base-packagecom.example.grade.controller/ mvc:annotation-driven/ mvc:default-servlet-handler/applicationContext.xml里则放数据源、SqlSessionFactoryBean、事务管理器等。这两个文件分开不是形式主义是因为 SpringMVC 子容器会先看自己容器里的 Bean如果 Controller 依赖的 Service 在 Root 容器需要保证子容器能访问父容器而数据源如果配在子容器事务管理器又配在 Root就会出现事务无法拦截 Service 方法的情况。2.4 MyBatis 和 Spring 整合的 3 个必调参数使用 mybatis-spring 整合时org.mybatis.spring.SqlSessionFactoryBean有三个参数决定项目能否跑通configLocation、mapperLocations、typeAliasesPackage。分别说明configLocation指向mybatis-config.xml用来配置驼峰映射、日志实现和插件。mapperLocations指向 Mapper XML 文件的路径比如classpath:mapper/*.xml。typeAliasesPackage指向实体类包配置后 Mapper XML 里写com.example.grade.entity.Score可以直接简写为Score。bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property nameconfigLocation valueclasspath:mybatis/mybatis-config.xml/ property namemapperLocations valueclasspath:mapper/*.xml/ property nametypeAliasesPackage valuecom.example.grade.entity/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.example.grade.mapper/ /bean这段配置里MapperScannerConfigurer会把com.example.grade.mapper下所有接口扫描成 Spring Bean不需要手写实现类。mapperLocations如果漏配项目能正常启动但第一次调用 Mapper 方法就会报Invalid bound statement (not found)这类问题排查时先看 namespace 和接口全限定名是否一致再看 XML 是否真的在 target/classes 目录下。另外mybatis-config.xml建议打开驼峰映射settings setting namemapUnderscoreToCamelCase valuetrue/ /settings这样数据库的student_no才能自动映射到实体的studentNo。如果不打开你只能在 resultMap 里手动配对字段一多就麻烦。3. 成绩管理系统的数据库设计与 MyBatis 映射3.1 学生、课程、成绩三张核心表怎么建学生成绩管理系统的核心不是“增删改查”而是三张表之间的多对多关系。学生与课程是多对多成绩就是中间表。只有一张成绩表而没有学生表和课程表的项目演示起来会显得单薄答辩也容易被追问表结构。建议最少建四张表sys_user存登录账号student存学生course存课程score存成绩。下面是成绩表常用的建表 SQLCREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT 学号, name VARCHAR(50) NOT NULL COMMENT 姓名, gender CHAR(1) DEFAULT 男 COMMENT 性别, class_name VARCHAR(50) COMMENT 班级, created_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, course_no VARCHAR(20) NOT NULL UNIQUE COMMENT 课程编号, course_name VARCHAR(100) NOT NULL COMMENT 课程名称, credit DECIMAL(3,1) DEFAULT 2.0 COMMENT 学分 ); CREATE TABLE score ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, score DECIMAL(5,2) COMMENT 成绩, exam_time VARCHAR(20) COMMENT 考试时间, UNIQUE KEY uk_student_course (student_id, course_id), CONSTRAINT fk_score_student FOREIGN KEY (student_id) REFERENCES student(id), CONSTRAINT fk_score_course FOREIGN KEY (course_id) REFERENCES course(id) );注意score表中的UNIQUE KEY uk_student_course (student_id, course_id)这个联合唯一约束很有意义同一个学生同一门课程只能存在一条成绩重复录入时数据库会直接报错后端在排序时也多了一个可用的索引。外键在真实业务里可以把ON DELETE CASCADE放到student和course上这样删除学生时成绩会被连带删除但要注意级联删除在数据量大的表上会拖慢性能。毕业设计建议保留外键因为数据库设计评分需要它。各表字段类型也值得在说明文档里写清楚score用DECIMAL(5,2)而不是FLOAT是因为浮点类型在计算平均分时可能出现 88.3499999 这类误差gender用CHAR(1)而不是VARCHAR(10)是为了让枚举值尽量短exam_time用VARCHAR(20)而不是DATETIME则是为了规避 MySQL 不同版本对日期零值和时区的处理差异。下面这个表格是成绩表字段的速查写实体类和导出 Excel 时可以直接对照字段名类型是否必填说明idINT是主键自增student_idINT是外键学生IDcourse_idINT是外键课程IDscoreDECIMAL(5,2)否成绩可空表示缺考exam_timeVARCHAR(20)否考试时间3.2 查询成绩列表的 resultMap 与多表关联列表页一般要显示学号、姓名、班级、课程名、成绩字段分布在三张表里所以需要写关联查询。MyBatis 的做法是定义一个 VO 类把所有展示字段放在里面再用resultMap做手动映射。resultMap idScoreVOMap typecom.example.grade.entity.ScoreVO id propertyid columnid/ result propertystudentNo columnstudent_no/ result propertystudentName columnstudent_name/ result propertycourseName columncourse_name/ result propertyscore columnscore/ /resultMap select idselectScoreList resultMapScoreVOMap SELECT s.id, st.student_no, st.name AS student_name, c.course_name, sc.score FROM score sc LEFT JOIN student st ON sc.student_id st.id LEFT JOIN course c ON sc.course_id c.id where if teststudentName ! null and studentName ! AND st.name LIKE CONCAT(%, #{studentName}, %) /if if testcourseId ! null AND c.id #{courseId} /if /where ORDER BY sc.id DESC /select这段 SQL 里的LEFT JOIN不能随便换成INNER JOIN。如果一张成绩表里有历史数据删除了学生INNER JOIN会直接丢失这行记录导致列表总数和成绩统计对不上。where标签会自动判断条件去掉第一个多余的AND比直接写WHERE 11要干净。参数说明#{studentName}是预编译参数最终会以?占位符传给 JDBC可防止 SQL 注入${studentName}是字符串拼接除非排序字段否则不建议用。courseId是 Integer判断条件只需要! null不要写成! null and ! 因为 Integer 与空字符串比较在 MyBatis OGNL 表达式里会走类型转换行为不容易预期。3.3 动态 SQL 的 where、if 和 foreach 使用边界除了多条件筛选成绩模块还会遇到批量导入和批量删除。MyBatis 的foreach是处理IN条件的常用方式delete iddeleteByIds DELETE FROM score WHERE id IN foreach collectionids itemid open( separator, close) #{id} /foreach /deletecollection为ids的取值规则需要和 Mapper 接口保持一致如果接口参数是Param(ids) ListInteger ids这里就写ids如果直接传 List 而不加注解需要在代码里确认 Mapper 接口的引用名。foreach适合几十到几百个 ID 的场景如果一次传几万个 IDIN列表过长会导致 SQL 超过数据库限制建议切片分批执行。3.4 常见坑Mapper 接口与 XML 绑定失败成绩相关的报错里出现频率最高的是Invalid bound statement (not found)。这类问题通常有四个原因Mapper 接口的包路径和 XML 的 namespace 不一致。XML 文件没有放在mapperLocations扫描路径下。mapper标签里声明的 SQL id 与接口方法名不一致。方法参数没有加Param注解XML 里又用了多参数名称。排查顺序是先看target/classes里有没有编译进去的 XML 文件再看 namespace 是否与接口全限定名完全一致最后看方法返回值类型和 SQL 的resultType是否匹配。比如接口返回ListScoreVOXML 的resultMap指向ScoreVO两者不匹配时通常会在启动时或者调用时报Mapper method ... has an unsupported return type。4. Vue 前端与后端接口对接学生成绩管理页面实战4.1 初始化 Vue 项目并安装依赖前端部分现在基本都用 Vue CLI 或 Vite 生成项目。以 vue-cli 为例创建和启动项目依赖的命令如下npm install -g vue/cli vue create grade-ui cd grade-ui npm install axios vue-router npm run servenpm install耗时较长如果网络状况不理想会卡在sill idealTree阶段常见的调整方案是把 npm 源切到国内镜像源npm config set registry https://registry.npmmirror.com配置完成后重新执行npm install。另外vue create时如果选择了Eslint Prettier项目中会多出一层代码风格检查有时候明明代码没问题但启动报错多半是分号或单双引号的问题。如果不打算花时间改风格创建项目时可以直接选Default ([Vue 3] babel, eslint)或者手动取消 Linter。4.2 axios 统一请求封装前后端通过接口交互时不应该在每个组件里直接axios.get然后重复处理错误。常见做法是在src/utils/request.js里创建 axios 实例import axios from axios const request axios.create({ baseURL: /api, timeout: 15000 }) request.interceptors.response.use( response response.data, error { if (error.response) { switch (error.response.status) { case 401: window.location.href /login break case 403: alert(没有权限执行该操作) break default: alert(error.response.data.message || 请求失败) } } else { alert(网络异常请检查后端服务) } return Promise.reject(error) } ) export default request这段封装解决三个问题一是所有请求自动带/api前缀方便转发二是统一把响应体从response.data中解包组件里拿到的直接是后端返回的数据对象三是把 401、403 这些状态码集中处理。如果后端返回的业务数据里还有自定义 code比如{ code: 200, data: [...] }可以在响应拦截器里加一个判断对 code 为 500 的情况直接提示。4.3 本地开发跨域devServer.proxy 转发前端开发服务器默认端口 8080后端 SSM 项目也在 Tomcat 里跑两个服务端口不一样浏览器直接发请求会被同源策略拦下来。Vue CLI 项目里最省事的方案是在vue.config.js中配置转发module.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/api: } } } } }这里的port: 8081是给前端开发服务器换个端口避免和后端 Tomcat 的 8080 撞车。/api开头的请求会被转发到http://localhost:8080同时^/api被替换成空字符串也就是说前端请求/api/score/list后端实际收到的路径是/score/list。如果你后端 Controller 的 RequestMapping 本身带有/api前缀就要把pathRewrite去掉这里很容易重复加前缀导致 404。4.4 路由参数传递从列表页跳到新增成绩页学生成绩管理里最常见的交互是在学生列表页点击“录入成绩”跳转到新增成绩页并携带学生 ID。Vue Router 有两种传参方式this.$router.push({ path: /score/add, query: { studentId: row.id } })在新增页读取参数时用this.$route.query.studentId。query 方式会把参数暴露在 URL 上刷新页面后不会丢失。如果用params方式传参this.$router.push({ name: ScoreAdd, params: { studentId: row.id } })params 参数不会出现在 URL 里但刷新页面后会变成 undefined除非路由表里配置了动态路径参数/score/add/:studentId。所以做新增、编辑这类需要刷新后仍能定位数据的场景我一般用 query 或动态 path。4.5 后端日期格式和数字精度问题SSM 后端返回成绩列表时实体里的Date字段默认会被 Jackson 序列化成时间戳数字比如1759472000000。前端如果不做处理页面上会显示一长串数字。你可以在 spring-mvc.xml 里声明MappingJackson2HttpMessageConverter把日期格式设置进去mvc:annotation-driven mvc:message-converters bean classorg.springframework.http.converter.json.MappingJackson2HttpMessageConverter property nameobjectMapper refjacksonObjectMapper/ /bean /mvc:message-converters /mvc:annotation-driven如果不想动后端前端可以在展示时做一个格式化函数function formatTime(value) { if (!value) return - return String(value).replace(T, ).substring(0, 19) }用new Date(value)配合toLocaleString也可以但要注意 iOS 下对2025-06-01 10:00:00这种格式解析不兼容直接 new Date 会得到 Invalid Date先替换掉中间的横线和空格会更安全。4.6 调试页面时的 Vue DevTools 和 Network 面板页面联调阶段不要只盯着页面看。浏览器按 F12打开 Vue DevTools 组件面板可以确认当前组件接收的 props 和数据状态Network 面板里筛 XHR 请求可以看到接口返回的 JSON以及请求究竟是不是发给了正确地址。如果看到一个请求处于 pending 状态很久多半是后端逻辑没返回或者是timeout设得太短。Vue DevTools 插件装好后如果 Vue 项目里没有出现插件图标第一反应是查项目是否有多个 Vue 实例比如页面里手动引入了vue.min.js又加载了 npm 包里的 vue插件只能识别其中一个。这个问题在 SSM Vue 的非工程化项目里比较常见但在 vue-cli 创建的项目里一般不会出现。5. 拿到源码包后本地跑通、打包部署与毕业设计录像5.1 环境变量与版本对齐JDK、Maven、Tomcat、MySQL从 zip 包把项目跑起来环境准备是第一道坎。很多同学把代码导入 IDEA 后发现全是红叉往往不是代码问题而是 JDK 版本和项目不匹配。SSM 老项目通常面向 Java 8 开发如果你机器上默认的 JDK 是 17建议在 IDEA 里把 Project SDK 和 Maven runner 的 JRE 都切到 1.8。检查命令和推荐版本可以看下这张表工具推荐版本验证命令JDK1.8或 8u202java -versionMaven3.6.xmvn -vTomcat8.5 或 9.0启动后访问http://localhost:8080MySQL5.7 或 8.0mysql -uroot -p环境变量配置是最常见的失败点。JAVA_HOME和M2_HOME必须指向安装目录而不是 bin 目录PATH 里也不要混着多个 JDK否则命令行里显示的版本和 IDEA 使用的版本可能不一致。如果mvn -v能执行但总是编译报错先看一眼它打印的 Java version 是不是 1.8。5.2 数据库初始化导入 SQL 脚本常踩的 3 个问题解压后一般会有.sql脚本操作时注意三点第一脚本里的COMMENT是中文如果工具连接字符集不是 UTF-8导入后说明文字会变成乱码第二MySQL 8 的连接 URL 要加时区和 SSL 参数否则报The server time zone value错误第三先执行学生表、课程表再执行成绩表否则外键关联的表还没创建脚本会停在CONSTRAINT那一步。连接 URL 参考这个格式jdbc:mysql://localhost:3306/grade_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiuseSSLfalse是关闭 MySQL 8 默认的 SSL 连接检查本地开发没有必要开启serverTimezoneAsia/Shanghai是让 JDBC 知道当前时区不然 Date 字段比实际时间少 8 小时。驱动类名如果还在用com.mysql.jdbc.DriverMySQL 8 下会启动失败改成com.mysql.cj.jdbc.Driver。5.3 后端打包 war 并部署到 TomcatSSM 后端是 war 包不是普通 Java 应用直接java -jar跑不起来。在模块根目录执行mvn clean package执行成功后target目录下会出现xxx.war。把 war 复制到 Tomcat 的webapps目录下启动 Tomcat 后会自动解压如果只想在 IDEA 里跑需要在 Run Configuration 里添加 Tomcat Server再在 Deployment 里把 war exploded 加到server.port。这里要注意 context pathTomcat 默认会用 war 包名作为访问路径比如grade.war对应http://localhost:8080/grade/前端转发时只有 server 端口不会自动知道这个路径需要在 proxy 的 target 里带上。5.4 Vue 打包后布局异常publicPath 和路由模式前端开发模式正常npm run build后打开dist/index.html却一片空白控制台里一堆 js/css 404这是 Vue 打包最常见的坑。原因是默认publicPath是/打包后的资源路径写成了/js/app.js如果静态文件被放到 Tomcat 的二级目录下浏览器就会去域名根目录找找不到自然布局异常。解决办法是在vue.config.js里改配置module.exports { publicPath: ./, outputDir: dist, assetsDir: static }publicPath: ./会把资源路径改成相对路径之后重新打包index.html里的 script 路径就是./static/js/xxx.js。注意 Vue CLI 3.3 以前的版本里这个配置项叫baseUrl新版才叫publicPath。如果用了vue-router的 history 模式刷新二级路径页面会 404这是因为 Tomcat 没有把请求转发到 index.html。对毕业设计而言最简单的方法是改用 hash 模式路由地址会多一个#但部署时不需要额外配置后端 rewrite。这两种模式的差异在录像讲解时最好能提一句会显得对整个链路有把握。5.5 毕业设计演示录像怎么录才有效果压缩包里的说明和录像质量参差不齐。如果你要重新录制建议按 8 到 12 分钟来规划开场直接展示项目目录把 pom.xml 里的 SSM 依赖、前端 package.json 里的 Vue 依赖扫一眼然后启动 MySQL 和 Tomcat把启动日志里没有异常的部分露出来接着打开系统完成登录、学生管理、成绩录入、成绩修改和删除操作最后进入到数据库手动执行一条 SQL展示刚才操作的落库结果。录像里要尽量保持鼠标动作干净不要把 IDEA 的报错弹窗长时间放在屏幕上如果录到一半出错宁可重新录这一段也不要保留删除重来的过程。6. 加一个答辩加分项成绩 Excel 批量导入导出6.1 用 EasyExcel 而不是直接用 POI成绩模块做到增删改查已经够及格但如果想在答辩时展示“批量导入导出”常见做法是用 EasyExcel 而不是直接写 Apache POI。EasyExcel 是对 POI 的封装写入 Excel 时采用流式模型内存消耗低处理几千行成绩不会卡顿。Maven 坐标如下dependency groupIdcom.alibaba/groupId artifactIdeasyexcel/artifactId version3.3.2/version /dependency6.2 导出成绩表的 Controller 写法给成绩列表加一个“导出 Excel”按钮后端只需要一个方法。实体类上先加注解public class ScoreVO { ExcelProperty(学号) private String studentNo; ExcelProperty(姓名) private String studentName; ExcelProperty(课程) private String courseName; ExcelProperty(成绩) private BigDecimal score; }Controller 里设置响应头并写出文件GetMapping(/score/export) public void export(HttpServletResponse response) throws IOException { ListScoreVO list scoreService.listAllScore(); String fileName URLEncoder.encode(成绩表, UTF-8); response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setHeader(Content-Disposition, attachment; filename*UTF-8 fileName .xlsx); EasyExcel.write(response.getOutputStream(), ScoreVO.class) .sheet(成绩) .doWrite(list); }这里filename*UTF-8是 HTTP 响应头的标准写法直接写attachment; filename成绩表.xlsx在部分浏览器和 Tomcat 版本里会乱码。Content-Type设置成 Excel 的 MIME 类型响应流不要手动关闭EasyExcel 的doWrite会处理流资源释放关太早反而写入不完整。导入功能可以借EasyExcel.read实现比对学号和课程编号是否存在成绩范围校验后再逐行插入。处理过程中建议一次性收集错误行号全部校验完再提示用户避免提示一个错误就中断用户来回改文件。最后如果你在导出时遇到NoSuchMethodError优先检查 pom 里 POI 和 EasyExcel 的版本是否冲突尤其不要同时手动引入poi-ooxml的旧版本。这个技巧能让你的编码流程少卡半个小时。本文还有配套的精品资源点击获取