基于Java的教务系统开发:Spring Boot与MyBatis Plus实战
发布时间:2026/10/5 1:07:42 锦皓数字建站

简介这是一个基于Java开发的教务查询系统练手项目面向正在学习SSM整合与Java后端开发的初学者。系统包含学生、教师、课程、选课等核心教务模块使用Spring、SpringMVC、Mybatis搭建业务与持久层Shiro负责登录鉴权并配合C3P0数据源、log4j日志及Bootstrap前端框架技术栈完整且贴近实际项目结构。压缩包共271个文件内含55个Java源码、42个XML配置、28个JSP页面、31个Jar依赖库以及2个SQL脚本包体大小35.32MB工程目录按代码、配置、页面、依赖分层组织导入IDE后可结合SQL脚本快速初始化数据库。已有412人学习浏览适合作为课程设计或SSM练手参考可帮助读者理解权限拦截、多表关联查询、前端页面整合等关键开发环节同时体会日志与数据源配置在实际项目中的落地方式。1. 基于Java的教务系统为什么说选型定生死接手教务系统改造的时候团队里争论最多的一句话是“这东西不就是 CRUD 吗。”真正跑起来才发现选课并发、教师数据权限、学期切换这三个问题任何一个都能让系统上线后翻车。基于 Java 开发的教务系统在今天的业界基本等于 Spring Boot MySQL MyBatis 这套组合它解决的是学校教务管理里最现实的需求学生选课、成绩录入、课表管理、班级学籍。适合两类人一类是要自己动手写教务系统的 Java 工程师另一类是正在做技术选型、想知道 Java 方案值不值得投的负责人。这篇把技术栈怎么定、表怎么建、权限怎么控以及最容易踩的坑一次性讲透。2. 技术选型与项目骨架Spring Boot MyBatis Plus 怎么把教务系统撑起来教务系统这类业务有一个特点生命周期长、角色多、报表多但并发量远达不到互联网电商的量级。选型的核心不是“谁更先进”而是“三年后还有没有人能维护、能不能跑在学校的旧服务器上”。我见过的早期 Java 教务系统大多是 Struts2 Spring Hibernate 的 SSH 组合配置繁琐不说招新人来接手基本要重新学一遍。现在再做Spring Boot MyBatis Plus 是下限最高的选择。2.1 为什么是 Spring Boot 而不是 SSH/Play FrameworkSpring Boot 把内嵌 Tomcat、自动配置、事务管理这些事都收进了启动器里一个main方法就能起服务这对教务系统这类需要长期维护、人员流动大的项目特别友好。MyBatis Plus 的 BaseMapper 能直接提供单表 CRUD复杂的多表查询仍然可以写 XML不会像 Hibernate 那样为了一个关联查询绕半天。有人会提 Play Framework 或者 Vert.x它们在特定场景有优势但在校园信息化的生态里资料、招人、运维经验都远不如 Spring Boot 成熟。教务系统不追求极致的吞吐量追求的是稳、可维护、出了问题有人会修。所以常见做法就是 Spring Boot 2.7 MyBatis Plus MySQL 8这组合能覆盖 90% 以上学校的需求。2.2 用一个最小工程把骨架跑起来新建一个 Maven 工程pom.xml里加这三个启动器就够了。Spring Boot 版本我一般用 2.7.18它是 2.x 的最终维护版本支持 JDK 8很多学校的服务器还停留在 JDK 8升到 17 需要把javax全部改成jakarta没必要为了新而新。?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent groupIdcom.school/groupId artifactIdedu-admin/artifactId version1.0.0/version properties java.version1.8/java.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId /plugin /plugins /build /projectspring-boot-starter-web自带内嵌 Tomcat 和 Jackson 序列化接口开发和 JSON 解析不用另配。MyBatis Plus 的版本要注意和 Spring Boot 2.x 匹配3.5.5 是稳定版本。MySQL 驱动在 Spring Boot 2.7 里用的是mysql-connector-java如果你用 Spring Boot 3.x 就得改成com.mysql:mysql-connector-j这是最容易踩的第一个版本坑。然后是核心配置application.yml。这里有两行参数是上线前必须检查的不然后面乱码和时间差会让你返工两天。server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/edu_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: root jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0characterEncodingutf8保证 Java 和 MySQL 之间传输中文不乱码serverTimezoneAsia/Shanghai保证 LocalDateTime 和数据库 TIMESTAMP 对得上。MyBatis Plus 的全局逻辑删除配置让deleted字段自动生效单表查询会自动追加WHERE deleted 0比在每个 XML 里手工写条件省事得多。2.3 四张核心表学生、课程、选课、成绩教务系统的数据模型说复杂也复杂说简单也简单核心就四张表学生表、课程表、选课表、成绩表。其他班级、教师、学期都是围绕它们扩展。我的建表习惯是所有业务表都带id、create_time、update_time、deleted四个公共字段后续接逻辑删除和审计都方便。CREATE DATABASE IF NOT EXISTS edu_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE edu_system; CREATE TABLE student ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 学生ID, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT 学号, name VARCHAR(50) NOT NULL, class_name VARCHAR(50) COMMENT 行政班级, deleted TINYINT NOT NULL DEFAULT 0, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENT 学生表; CREATE TABLE course ( id BIGINT PRIMARY KEY AUTO_INCREMENT, course_no VARCHAR(20) NOT NULL UNIQUE COMMENT 课程编号, course_name VARCHAR(100) NOT NULL, teacher_id BIGINT NOT NULL COMMENT 授课教师ID, capacity INT NOT NULL COMMENT 选课容量, selected_count INT NOT NULL DEFAULT 0 COMMENT 已选人数, semester VARCHAR(20) NOT NULL COMMENT 学期如2025-2026-1, credit DECIMAL(3,1) NOT NULL DEFAULT 0.0, deleted TINYINT NOT NULL DEFAULT 0, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_teacher (teacher_id), KEY idx_semester (semester) ) ENGINEInnoDB COMMENT 课程表; CREATE TABLE selection ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL, course_id BIGINT NOT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT 1已选 2退选 3已结课, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_student_course (student_id, course_id) ) ENGINEInnoDB COMMENT 选课表; CREATE TABLE score ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL, course_id BIGINT NOT NULL, score DECIMAL(5,2) COMMENT 成绩100分制, grade_point DECIMAL(3,1) COMMENT 绩点, operator_id BIGINT NOT NULL COMMENT 操作人ID, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_student_course (student_id, course_id) ) ENGINEInnoDB COMMENT 成绩表;选课表的uk_student_course联合唯一索引是最重要的一道防线它能在数据库层面挡住同一个学生重复选同一门课哪怕应用层代码漏了判断数据库也会抛DuplicateKeyException。课程表里放selected_count字段是刻意设计的配合后面要讲的乐观锁可以解决选课抢课时的超选问题。成绩表单独拆出来而不是挂在选课表上是因为成绩有录入、修改、补录这些独立流程而且必须留操作人operator_id便于追溯。实体类和 Mapper 接口直接用 MyBatis Plus 的BaseMapper生成基本不用手写单表 CRUD。到这一步系统骨架已经能跑起来下一步就是往里面填权限和业务逻辑。3. 权限模型先于业务三类角色与数据范围控制教务系统的权限和普通后台管理系统不一样普通后台只管“谁能进哪个菜单”教务系统还要管“进去以后能看哪些数据”。一个教师登录系统能看到课程列表但只能给“自己教的课”录入成绩一个学生能看到成绩列表但只能看自己的。这类行级数据权限如果等到业务写完了再补几乎等于重构。所以权限模型必须摆在业务前面。3.1 学生、教师、教务员的操作边界教务系统里最少有三类角色我把它们的操作边界先列出来后续所有接口的权限校验都按这张表来。角色能进的功能数据范围学生选课、退课、查成绩、查课表只能看到自己的选课和成绩教师录成绩、查课表、查选课名单只能看到自己授课课程的数据教务员建课、排课、调课、学期管理、账号管理全部数据菜单级权限靠 Spring Security 或者拦截器过滤 URL 就能实现真正麻烦的是数据范围。学生调用查询成绩接口时后端不能信任前端传的studentId必须从登录态里取当前用户 ID 去查教师查询选课名单时SQL 里必须带teacher_id 当前登录教师ID这个条件。这些规则看起来简单但要保证每个接口都做对就得在框架层面统一约束。3.2 RBAC 落地的数据库设计与行级权限常见的做法是标准 RBAC 五张表用户表、角色表、权限表、用户角色关联表、角色权限关联表。教务系统可以在用户表上做一个简化设计加一个user_type字段和ref_id字段用来关联学生表或教师表的主键。CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, user_type VARCHAR(20) NOT NULL COMMENT student/teacher/admin, ref_id BIGINT COMMENT 关联student或teacher表的ID ) ENGINEInnoDB; CREATE TABLE sys_role ( id BIGINT PRIMARY KEY AUTO_INCREMENT, role_code VARCHAR(50) NOT NULL UNIQUE, role_name VARCHAR(50) NOT NULL ) ENGINEInnoDB; CREATE TABLE sys_user_role ( user_id BIGINT NOT NULL, role_id BIGINT NOT NULL, PRIMARY KEY (user_id, role_id) ) ENGINEInnoDB;user_type ref_id这个设计的用意是学生登录后根据ref_id能直接找到student表的主键教师在查询自己课程时可以直接用这个 ID 作为teacher_id条件不用每张业务表都存一份用户名。行级数据权限落地有两种方案。第一种是在 Service 层根据user_type手动拼接查询条件小项目推荐逻辑直白、出问题好排查第二种是用 MyBatis 拦截器自动给 SQL 追加数据权限条件适合表和接口非常多的大项目但拦截器生效顺序和 SQL 解析都很容易出玄学问题。我一般用方案一代码量多一点但维护的人不用翻拦截器源码。public ListCourse listMyCourses(String userType, Long userId) { LambdaQueryWrapperCourse wrapper new LambdaQueryWrapper(); if (teacher.equals(userType)) { wrapper.eq(Course::getTeacherId, userId); } if (student.equals(userType)) { wrapper.inSql(Course::getId, select course_id from selection where student_id userId); } return courseMapper.selectList(wrapper); }这段代码里教师只能查teacher_id等于自己的课程学生通过子查询从选课表反查自己选过的课程 ID。注意inSql里的参数如果能被外部输入控制就会有 SQL 注入风险这里userId来自登录态而不是前端参数所以是安全的。如果你实在不放心就把子查询改成 JOIN。3.3 一个登录拦截与权限校验的完整实现权限控制的入口是登录拦截器。我用 Spring MVC 的HandlerInterceptor在请求进入 Controller 之前解析 token把当前用户信息放到ThreadLocal里后面所有 Service 层代码都能直接从UserContext取当前操作人。Component public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 前端预检请求直接放行否则跨域会失败 if (request.getMethod().equalsIgnoreCase(OPTIONS)) { return true; } String token request.getHeader(Authorization); if (token null || token.isEmpty()) { response.setStatus(401); return false; } // 从 Redis 或内存中拿到 userId 和 userType LoginUser user getLoginUser(token); UserContext.set(user); return true; } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { // 必须清理否则线程池复用时用户信息会串号 UserContext.clear(); } }注册拦截器时登录接口本身要放行其余api路径全部走拦截。Configuration public class WebConfig implements WebMvcConfigurer { Autowired private LoginInterceptor loginInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns(/api/**) .excludePathPatterns(/api/auth/login, /api/auth/captcha); } }UserContext.clear()这行是我踩坑踩出来的。Tomcat 的工作线程是复用的如果请求结束后不清理 ThreadLocal下一个请求可能读到上一个用户的数据轻则数据错乱重则是严重的越权漏洞。所有用过 ThreadLocal 存用户信息的项目都必须在afterCompletion里清掉这是铁律。角色级别的校验可以再做一层注解像RequireRole(teacher)配合拦截器判断但核心思路已经够了。教务系统的权限不是功能堆出来的是每一行 SQL 都带上了正确的用户维度条件。4. 核心业务落地选课、成绩录入、排课这三个模块的代码与并发教务系统看起来模块多实际上真正有技术含量的就三个选课、成绩录入、排课。选课是并发问题成绩录入是事务和留痕问题排课是冲突检测和算法问题。把这三个吃透其他功能都是围绕它们的外围 CRUD。4.1 选课用数据库锁防止超选不是用 if选课的核心痛点就一句话热门课 100 个名额300 个人同时抢怎么保证不会选进去 120 人。新手最容易写的代码是先查selected_count再判断是否小于capacity然后插入记录。这个写法在并发下肯定超选因为两个请求可能同时读到 99然后同时通过判断最后变成 101 人。正确做法是让数据库来保证原子性用一条带条件的 UPDATE 来完成“检查并增加”。Transactional(rollbackFor Exception.class) public boolean selectCourse(Long studentId, Long courseId) { // 先检查课程是否在选课窗口内省略学期时间判断 Course course courseMapper.selectById(courseId); if (course.getSelectedCount() course.getCapacity()) { return false; } // 乐观锁只有名额未满时才把已选人数加 1 int rows courseMapper.increaseSelectedCount(courseId); if (rows 0) { return false; } Selection selection new Selection(); selection.setStudentId(studentId); selection.setCourseId(courseId); try { selectionMapper.insert(selection); } catch (DuplicateKeyException e) { // 唯一索引挡住重复选课抛出异常回滚人数 1 throw new BusinessException(你已经选过这门课); } return true; }对应的 Mapper XML 是这条 UPDATEupdate idincreaseSelectedCount UPDATE course SET selected_count selected_count 1 WHERE id #{courseId} AND selected_count capacity /update这条 SQL 就是整个选课模块的并发核心。MySQL 执行 UPDATE 时会锁住这行记录同一时刻两个请求都读到selected_count 99第一个请求执行成功后变成 100第二个请求执行时WHERE selected_count capacity不成立影响行数为 0直接返回失败。这就是“Java 怎么保证数据一致性”在选课场景的标准答案不依赖应用层的 if 判断把业务约束放到数据库的原子操作里。DuplicateKeyException是另一道保险即使两个请求同时插入同一对student_id course_id唯一索引也会让其中一个插入失败。注意整个方法必须加Transactional否则人数加 1 成功但插入选课记录失败时人数就永远多算了一个。4.2 成绩录入批量更新与操作留痕成绩录入的场景是教师打开选课名单一次录一个班几十人的成绩。逐条 UPDATE 没问题但要注意两点事务要包住整批操作成绩修改必须留痕。教务系统里成绩被改错又查不到谁改的是最容易引发纠纷的事。Transactional(rollbackFor Exception.class) public void saveScores(ListScoreDTO scores, Long operatorId) { for (ScoreDTO dto : scores) { Score score scoreMapper.selectOne(new LambdaQueryWrapperScore() .eq(Score::getStudentId, dto.getStudentId()) .eq(Score::getCourseId, dto.getCourseId())); if (score null) { // 补录场景直接插入 Score s new Score(); s.setStudentId(dto.getStudentId()); s.setCourseId(dto.getCourseId()); s.setScore(dto.getScore()); s.setOperatorId(operatorId); scoreMapper.insert(s); } else { // 修改场景更新成绩并写一条日志 score.setScore(dto.getScore()); score.setOperatorId(operatorId); scoreMapper.updateById(score); scoreLogMapper.insert(buildLog(score, operatorId)); } } }这里没有用CASE WHEN拼批量 UPDATE因为教务系统一次录入的量级是几十条循环逐条更新已经很快而且代码可读性好。真正要上CASE WHEN的是上千条数据的批量导入那个放到后面再说。留痕日志表的结构很简单id、score_id、old_score、new_score、operator_id、create_time。注意成绩更新和日志插入必须在一个事务里否则会出现成绩改了但日志没记下来的情况。operatorId不要从接口参数取要从登录态的UserContext里拿防止有人伪造操作人。4.3 排课先贪心后冲突检测排课是教务系统里算法味道最重的一块。常见做法是贪心算法把课程按优先级排序依次往教室时间片里塞塞的过程中检查冲突。具体规则是同一个教室在同一个星期几、同一个节次不能安排两门课同一门课也不能同时出现在两个教室。// 时间片星期几(1-7) 节次(1-12) public boolean arrange(ArrangeRequest req) { ListTimeSlot used roomMapper.findUsedSlots(req.getRoomId()); for (int day 1; day 7; day) { for (int slot 1; slot 12; slot) { if (!isConflict(used, day, slot) roomCapacityOk(req.getRoomId(), req.getStudentCount(), day, slot)) { insertArrangement(req, day, slot); return true; } } } return false; }冲突检测的 SQL 要特别注意带上semester条件不然上一学期的课表会干扰这学期的排课。SELECT COUNT(*) FROM arrangement WHERE room_id #{roomId} AND weekday #{day} AND slot #{slot} AND semester #{semester}这种按时间片枚举的做法容易理解缺点是教室的空余时段会被切得很碎利用率不高。更复杂的排课可以用约束求解或遗传算法但教务系统里“先保证不冲突再谈优化”是底线。算法再差只要冲突检测是对的排出来的课表就能用算法再漂亮冲突检测漏了一周的课表就废了。5. 避坑与排查教务系统开发里的 5 个高频坑做教务系统这类 Java web 项目技术上的难点不是新框架而是那些看着不起眼、一上线就翻车的细节。我整理了五个踩过最多的高频坑每一条都是“现象、原因、解决”三段式。5.1 中文乱码Tomcat 和 MySQL 的编码不一致现象页面上输入的中文正常显示入库后变成???或者乱码。原因最常见的是 JDBC URL 少了characterEncodingutf8或者建库时没指定utf8mb4字符集。解决JDBC URL 加useUnicodetruecharacterEncodingutf8建库语句用utf8mb4。如果表已经建好了用一条 ALTER 语句补救ALTER TABLE student CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;另外Spring Boot 内嵌 Tomcat 对 URL 参数的编码默认就是 UTF-8一般不用配。乱码问题 90% 出在数据库连接参数上排查顺序是先看 JDBC URL再看建库语句最后看页面响应头编码。5.2 时间边界学期、考勤、成绩的日期处理现象学期已经结束了学生还能在系统里选课。原因前端只隐藏了选课按钮后端没有校验当前时间是否在选课窗口内。解决学期表存select_start和select_end在selectCourse方法开头加校验LocalDateTime now LocalDateTime.now(); if (now.isBefore(term.getSelectStart()) || now.isAfter(term.getSelectEnd())) { throw new BusinessException(当前不在选课时间范围内); }这里要强调一个细节时间比较不要用字符串字符串比较在跨年、跨学期时一定会出问题。实验楼里serverTimezoneAsia/Shanghai必须配置否则LocalDateTime.now()和数据库的 TIMESTAMP 会存在时区差时间差八个小时够你排查半天。5.3 Excel 导入“最后一列总是丢”现象用模板导入学生名单每行最后一项数据总是读不到。原因EasyExcel 读取时遇到末尾的空单元格会直接跳过如果模板里最后几列在末尾几行没有填满数据读取器就不会为这些单元格生成字段。解决导入模板不要使用合并单元格同时固定表头读完后检查末尾列。如果数据确实有空值用ExcelProperty指定列下标并手动设置lastRowIndex不要依赖 EasyExcel 的自动判断。这个问题的隐性成本很高因为导入的学生名单一旦缺列后面批量建账号、分班全都会跟着错位。5.4 事务失效同一个类里调用自己的方法现象saveScores方法加了Transactional但执行过程中抛了异常数据没有回滚。原因Spring 的事务代理是基于 AOP 的同类内部自调用不会经过代理对象Transactional直接失效。解决把需要事务隔离的方法放到另一个 Service 里或者注入ApplicationContext获取代理对象调用((ScoreService) AopContext.currentProxy()).saveScores(scores, operatorId);排查方法很简单看日志里有没有Creating new transaction这行。如果是自调用日志里根本不会出现事务创建的记录。这个问题在“一个 Service 里写完了所有业务逻辑”的项目里特别常见属于典型八股和工程实践的差距八股题里知道 AOP 原理代码里还是照踩不误。5.5 定时任务重复执行分布式部署下的幂等现象每天凌晨的“课程状态结转”任务执行了两遍学生收到两次提醒短信。原因项目部署了两个实例Spring 的Scheduled在每个实例上都会注册并执行默认不做分布式协调。解决引入数据库分布式锁。加一张任务锁表CREATE TABLE sys_task_lock ( task_name VARCHAR(50) PRIMARY KEY, lock_until DATETIME NOT NULL, owner VARCHAR(100) );拿锁的判断用一条 UPDATE 语句只有lock_until now()时才允许将锁的持有者更新为当前实例并延长lock_until。更新影响行数为 1 表示拿锁成功为 0 说明其他实例还在执行。这个方案不需要额外引入 Redis和教务系统的技术栈完全匹配。如果一直是单机部署这个坑不会出现但只要上了负载均衡就要提前考虑。6. 批量导入导出与新学期初始化一个能直接抄的 Excel 工具类教务系统里 Excel 导入导出是使用频率最高的功能开学导入学生名单、期末导出成绩单。这块我用 EasyExcel它比 Apache POI 封装得更彻底读写的 API 简单内存占用也小。6.1 EasyExcel 写入与读取导出成绩单的核心代码就几行public void exportScores(HttpServletResponse response, Long courseId) { ListScoreVO list scoreMapper.selectScoreVOByCourse(courseId); response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setCharacterEncoding(utf-8); String fileName URLEncoder.encode(成绩单, UTF-8).replaceAll(\\, %20); response.setHeader(Content-Disposition, attachment;filename*utf-8 fileName .xlsx); EasyExcel.write(response.getOutputStream(), ScoreVO.class) .sheet(成绩单) .doWrite(list); }注意响应头里filename*utf-8这段如果漏了Chrome 和 Firefox 下载中文文件名会变成乱码。ScoreVO类上需要加ExcelProperty注解来指定列名和顺序字段顺序就是导出后的列顺序。6.2 一个带校验与错误回显的导入实现导入比导出复杂在校验。我的做法是用 EasyExcel 的监听器逐行读取并校验把错误行号和错误原因收集到列表里全部读完后如果没错误才入库这样不会出现导入一半留下一堆脏数据的情况。public class StudentImportListener extends AnalysisEventListenerStudentRow { private final ListString errors new ArrayList(); private final ListStudentRow validRows new ArrayList(); Override public void invoke(StudentRow row, AnalysisContext context) { int rowIndex context.readRowHolder().getRowIndex(); if (StringUtils.isBlank(row.getStudentNo())) { errors.add(第 (rowIndex 1) 行学号为空); return; } validRows.add(row); } Override public void doAfterAllAnalysed(AnalysisContext context) { if (errors.isEmpty()) { // 全部通过后再入库 studentService.batchInsert(validRows); } } }核心原则是invoke里只收集和校验不写数据库。这样即使数据源有问题也只是错误列表变长不会留下半截数据。导入完成后把errors列表返回给前端用户看一眼就知道哪一行错在哪。6.3 我养成的习惯做了几个教学项目之后我养成的最重要的习惯是每次新学期开始前先跑一遍“学期切换脚本”再开新功能。脚本做三件事把上学期的选课记录归档到selection_history表重置course表的selected_count为 0把毕业生和新生的班级状态刷一遍。这个脚本不放进管理系统的页面上就放在数据库运维目录里跑之前先备份跑完核对一下数据总量。这个习惯帮我避开了太多“这学期数据被上学期残留污染”的血泪。如果你的教务系统也要做学期切换建议你也先从这个脚本开始而不是先加新功能。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。