ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Spring Boot+MyBatis+MySQL实战:学生信息管理系统开发全流程

2026/10/7 20:51:04 拓冰建站 浏览量
Spring Boot+MyBatis+MySQL实战:学生信息管理系统开发全流程 简介基于JavaMySQL实现的学生信息管理系统Web项目面向Java Web课程设计、毕业设计及初学入门人群采用JavaBeanServletDAO经典分层架构以IntelliJ IDEA开发并通过过滤器统一处理权限校验防止未授权访问。系统内置三类核心角色学生端支持成绩查询、课程平均成绩、班级开课与教学计划查询教师端可进行成绩录入与查询、查看任课情况管理员端覆盖学生、教师、课程、成绩、学院、专业、班级等信息的完整增删改查管理场景全面贴近真实教务系统需求。压缩包共551个文件主要包含JSP动态页面、Java源码及Servlet类同时配有JS、CSS、HTML前端资源以及XML配置、Jar依赖库和项目图标图片等整体大小约17.88MB目录结构规整便于直接导入IDEA运行学习。已有568人浏览学习适合作为课程设计参考或Java Web入门实战资源内还包含DAO层、登录验证及成绩查询等核心Java类有助于快速理解分层开发和请求流转流程。1. 学生信息管理系统JavaMySQL 这套组合为什么到现在还有人做每年挑课设题目的时候学生信息管理系统总会被翻出来。JavaMySQL 做一个 Web 学生信息管理系统表面看就是一张学生表的增删改查实际做下来你会发现登录、分页、搜索、Excel 导入导出、权限拦截全都要串起来。这篇文章要解决的是刚学完 Java 基础、想独立完成一个 Web 项目的人怎么不靠视频模板也能把它跑通。方案用 Spring Boot MyBatis MySQL 来讲因为这套结构最接近企业级 Web 开发也能最小成本从课设变成能给别人用的内部小系统。2. 先做技术选型Servlet/JSP 和 Spring Boot 两条路线选错才是第一个坑2.1 传统路线为什么容易在第一步翻车早年的课设几乎都是用 JSP Servlet Tomcat 手工搭 Web 工程。不是说不能跑而是对新手来说真正花时间的不是业务代码是环境。JSP 页面里塞 Java 代码Servlet 里拼 HTML改一个字段要重启 Tomcat。你去搜 java 面试题面试官问 Servlet 生命周期、Filter 调用链但那是在考底层原理不是在推荐你现在从零搭一个 web.xml 手写的项目。更麻烦的是版本兼容JDK 8 配 Tomcat 9javax.servlet 包名会变IDEA 里 Artifact 配置不对运行后直接 404。这些坑和业务一点关系都没有却能耗掉一大半时间。所以传统路线只适合一类人你想彻底搞懂 HTTP 请求是怎么被 Servlet 接住、又是怎么转发给 JSP 的那可以拿它当解剖样本。如果你的目标是快速做出一个能演示、能往里加功能的学生信息管理系统选 Servlet/JSP 性价比很低。2.2 Spring Boot 路线的选型理由Spring Boot 把 Tomcat 内嵌进来省掉了外部部署 Web 工程的步骤一个java -jar就起服务。配合 MyBatis 操作 MySQL、Thymeleaf 做页面渲染这已经是目前企业级 Web 开发的最小骨架。在这个项目里你写的实体类、Mapper、Service、Controller 分层换到公司项目一样通用。网上 MySQL 安装配置教程、Spring Boot 教程都很多遇到问题更容易搜到答案。对课设答辩来说你能讲清楚“为什么分层、为什么用预编译 SQL”比背 JSP 标签更有价值。选择 Spring Boot 不是逃避 Servlet 原理而是先把系统跑通再回头补底层。另外Spring Boot 内置的 java 容器概念也是 java 面试题常客你亲手部署一次比背一百次“什么是内嵌容器”都管用。2.3 用 Maven 搭出最小 Web 工程骨架我推荐的运行环境是JDK 8 或 11Maven 3.6MySQL 5.7 或 8.0。IDEA 里新建一个 Maven 项目父 pom 引入 spring-boot-starter-parent然后加入下面依赖。这是整个项目最基础的骨架文件。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent properties java.version1.8/java.version mybatis.starter.version2.3.1/mybatis.starter.version /properties dependencies !-- Web 启动器内嵌 Tomcat -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- Thymeleaf 模板引擎 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependency !-- MyBatis 和 Spring Boot 的桥接包 -- dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version${mybatis.starter.version}/version /dependency !-- MySQL 驱动Spring Boot 2.7 会自动管理版本 -- dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency /dependencies这里几个参数要留意。mybatis-spring-boot-starter的版本不能省略2.3.1 对应 Spring Boot 2.x如果你搜到 3.x 的 mybatis starter那是配合 Spring Boot 3 JDK 17 的别照抄到 JDK 8 上。mysql-connector-j不写版本是因为 Spring Boot 2.7 的依赖管理已经锁定了 8.0.33 左右的版本。java.version设成 1.8编译目标就是 Java 8即便本机装了 JDK 17只要 IDE 里 Project SDK 也选 17照样能跑只是 class 兼容级别低一些。工程目录我一般这样建controller放页面跳转和参数接收service放业务逻辑mapper放 MyBatis 接口entity放实体类config放拦截器等配置。Thymeleaf 模板放在src/main/resources/templates静态资源放src/main/resources/staticMyBatis 的 XML 映射文件放resources/mapper。这个结构后文所有代码都会填进去。3. 先用 SQL 把数据结构定死四张表、索引和连接池参数3.1 从需求反推表结构学生信息管理系统的核心对象是学生但只建一张 student 表走不远。要有登录功能就得有用户表学生要归属班级班级又属于专业和年级。如果后面还要加成绩、考勤student 表的主键会被多处引用。所以第一版至少四张表system_user、class_info、student、operation_log。system_user存登录账号和角色class_info存班级信息student存学生主体operation_log记录谁在什么时间干了什么。字段和索引的设计有几个约定我来回用了很多项目都没改过主键用自增 BIGINT不搞 UUID学生系统没有跨库合并需求自增主键插入也更快。学号student_no一定要加唯一索引这是业务上的天然主键不能出现两条相同学号。创建时间create_time用DEFAULT CURRENT_TIMESTAMP更新时间update_time用ON UPDATE CURRENT_TIMESTAMP这样在 Java 代码里不用手动维护时间。字符集统一用 utf8mb4排序规则用utf8mb4_general_ci中文和 emoji 都能存。如果你的 MySQL 是 8.0默认排序规则是utf8mb4_0900_ai_ci但 5.7 不支持为了兼容初期尽量用 general_ci。MySQL 排序规则越精确越慢学生系统这点数据量根本不需要 unicode_ci 的严格比较general_ci 够用。学生表还要考虑“逻辑删除”。我见过很多项目把删除做成物理 DELETE学生一删关联的成绩记录、照片全部跟着丢。学生信息管理系统里的“删除”通常是退学或毕业状态应该用status字段控制1 在读0 离校。这样页面默认只查status1数据还在库里遇到要补历史数据也能找回来。3.2 建表 SQL 与初始化数据下面这段 SQL 可以直接在 MySQL 里执行。为了演示方便管理员密码先存 MD5 值生产环境必须换 BCrypt后面会再强调。CREATE DATABASE IF NOT EXISTS student_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE student_db; CREATE TABLE system_user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL, password VARCHAR(64) NOT NULL COMMENT 演示项目存MD5生产环境必须换BCrypt, real_name VARCHAR(50), role TINYINT NOT NULL DEFAULT 2 COMMENT 1管理员 2普通用户, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_username (username) ) ENGINEInnoDB; CREATE TABLE class_info ( id BIGINT AUTO_INCREMENT PRIMARY KEY, class_name VARCHAR(100) NOT NULL, grade VARCHAR(20) NOT NULL, major VARCHAR(50), create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_class_name (class_name, grade) ) ENGINEInnoDB; CREATE TABLE student ( id BIGINT AUTO_INCREMENT PRIMARY KEY, student_no VARCHAR(20) NOT NULL, name VARCHAR(50) NOT NULL, gender TINYINT NOT NULL DEFAULT 0 COMMENT 0未知 1男 2女, birth_date DATE, phone VARCHAR(20), class_id BIGINT, status TINYINT NOT NULL DEFAULT 1 COMMENT 1在读 0离校, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_student_no (student_no), KEY idx_class_id (class_id), KEY idx_name (name), CONSTRAINT fk_student_class FOREIGN KEY (class_id) REFERENCES class_info(id) ) ENGINEInnoDB; INSERT INTO system_user (username, password, real_name, role) VALUES (admin, 21232f297a57a5a743894a0e4a801fc3, 管理员, 1); INSERT INTO class_info (class_name, grade, major) VALUES (计科2101, 2021级, 计算机科学与技术); INSERT INTO student (student_no, name, gender, birth_date, phone, class_id, status) VALUES (20210001, 张三, 1, 2003-05-01, 13800138000, 1, 1);建表语句里几个细节说一下。所有表都用 InnoDB因为它支持事务处理和外键约束。修改学生信息时可能要同时写 operation_log如果 student 的 update 成功了、log 插入失败了没有事务就会出现数据不一致。物理外键在课设里建议保留能向老师解释清楚“学生表通过 class_id 关联班级表”但生产环境的大表一般不建物理外键因为删除时要多处加锁容易死锁靠应用层保证一致性。idx_name是为列表页“按姓名模糊搜索”准备的但LIKE %名字%一般情况下不会走这个索引所以它起到的作用有限保留它是为了应付精确搜索和后续可能的LIKE 名字%场景。idx_class_id是真正高频的按班级筛选学生一定会用到。3.3 连接池参数与 MySQL 驱动配置Spring Boot 2.x 默认连接池是 HikariCP性能比 C3P0、DBCP 好配置也更简洁。C3P0 的问题在于连接回收不够主动超时后容易把线程卡死出了问题日志还很难读。HikariCP 只需要在application.yml里写几行参数。server: port: 8080 servlet: context-path: /student spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/student_db?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltruecharacterEncodingutf8 username: root password: root123 hikari: minimum-idle: 5 maximum-pool-size: 15 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000 pool-name: StudentHikariPool mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true连接串是重点。useSSLfalse用来关掉 SSL 握手不然本地开发环境很容易报 mysql ssl 连接错误。serverTimezoneAsia/Shanghai解决 MySQL 8 的时区报错避免驱动把本地时间和数据库时间当成两个 UTC 时间转换。allowPublicKeyRetrievaltrue解决 MySQL 8 使用 caching_sha2_password 认证时出现 “Public Key Retrieval is not allowed” 的问题。characterEncodingutf8保证中文写入不乱码和数据库的 utf8mb4 配套。Hikari 参数里maximum-pool-size是核心学生系统 15 个连接足够别学网上教程随意写到 200连接数越多 MySQL 内部线程切换越浪费。connection-timeout是 30 秒超过就抛异常。max-lifetime设置成 30 分钟必须小于 MySQL 的wait_timeout否则数据库会把空闲连接断开连接池不知道仍然把死连接发给应用跑几天后就会出现接口卡死。这也是后文要讲的经典故障。4. 把增删改查写成三层代码实体、Mapper、Service、Controller4.1 实体类与 MyBatis 映射实体类对应数据库表也是 Java 面向对象基础的直接体现。这里不需要任何框架代码就是纯 pojo。以 Student 为例public class Student { private Long id; private String studentNo; private String name; private Integer gender; private LocalDate birthDate; private String phone; private Long classId; private Integer status; private LocalDateTime createTime; private LocalDateTime updateTime; // 非数据库字段用于页面显示班级名称 private String className; // getter、setter 省略用 IDE 自动生成 }字段类型要和数据库对应student_no映射到studentNo靠 application.yml 里的map-underscore-to-camel-case: true自动转换省掉一堆 resultMap。日期类型用LocalDate/LocalDateTime比java.util.Date安全得多能避开 MySQL 驱动对 java.sql.Date 的转换问题。className不是 student 表字段但列表页需要显示所以放在实体里在 Mapper 查询时用 JOIN 查出来并映射。这种“非数据库字段”在 MyBatis 里很常见不用额外配置只要数据库查询结果里包含classNameMyBatis 就会自动赋值。4.2 Mapper 接口和 XML用 #{} 而不是 ${}Mapper 接口只定义方法SQL 写在 XML 里。分页查询加上模糊搜索是最核心的一个方法。Mapper public interface StudentMapper { ListStudent selectPage(Param(keyword) String keyword, Param(offset) int offset, Param(limit) int limit); long count(Param(keyword) String keyword); int insert(Student student); int update(Student student); int delete(Param(id) Long id); }select idselectPage resultTypecom.example.entity.Student SELECT s.*, c.class_name AS className FROM student s LEFT JOIN class_info c ON s.class_id c.id where if testkeyword ! null and keyword ! AND (s.student_no LIKE CONCAT(%, #{keyword}, %) OR s.name LIKE CONCAT(%, #{keyword}, %)) /if /where ORDER BY s.create_time DESC LIMIT #{offset}, #{limit} /select这里最重要的一个参数就是#{}。它会编成 PreparedStatement 的占位符?用户输入的内容只作为参数值传入不会改变 SQL 结构。如果用${keyword}MyBatis 会先做文本替换再执行用户输入1 OR 11就能改写你的 SQL登录接口直接被人绕过。关于排序字段如果将来要支持前端传入“按姓名排序”或“按学号排序”一定不要在 XML 里用${}拼列名应该在后端做一个白名单比如Map.get(name)存在才允许用否则就拒绝请求。分页这里手写了LIMIT #{offset}, #{limit}没有引入 PageHelper。原因很简单学生系统数据量几千条手写分页性能完全够还能避免 PageHelper 和 Spring Boot、MyBatis 版本兼容的坑。面试时如果被问到分页优化你能说出“数据量大了用游标分页用 id 大于上一页最后一行 id 代替 offset”这句话比只会用插件更让人信服。4.3 Service 层、Controller 层和登录会话Service 层处理业务逻辑Controller 只做参数接收和页面跳转。以新增学生为例Service public class StudentService { private final StudentMapper studentMapper; public StudentService(StudentMapper studentMapper) { this.studentMapper studentMapper; } Transactional public void addStudent(Student student) { if (studentMapper.selectByNo(student.getStudentNo()) ! null) { throw new BusinessException(学号已存在); } studentMapper.insert(student); } }Transactional是事务处理的开关。先查重再插入这两个操作必须在一个事务里。如果只查不插超卖学号的问题如果只插不查重复学号会直接命中唯一索引抛异常体验不好。BusinessException必须继承RuntimeExceptionSpring 才默认会回滚事务。我之前见过有人用Transactional包一个 try-catchcatch 里只打了日志没抛异常最后数据照旧只写了一半这就是“事务处理做过等于没做”的典型翻车现场。Controller 里路由映射是 Web 层的关键。用 GET 打开页面用 POST 提交表单。Controller RequestMapping(/student) public class StudentController { GetMapping(/list) public String list(RequestParam(defaultValue 1) int pageNo, RequestParam(defaultValue 10) int pageSize, RequestParam(required false) String keyword, Model model) { PageResultStudent result studentService.page(pageNo, pageSize, keyword); model.addAttribute(page, result); model.addAttribute(keyword, keyword); return student/list; } }pageNo默认 1pageSize默认 10前端分页条点击时用?pageNo2传参。PageResult是一个包含list和total的简单对象前端 Thymeleaf 里遍历page.list底部显示第几页。这个结构是 Web 工程里最标准的做法也是企业级 Web 开发的最小分层样例。登录和拦截器也要在这里搭好。登录成功后把用户放进 session之后每个请求通过拦截器校验。PostMapping(/login) public String login(String username, String password, HttpSession session) { String md5 DigestUtils.md5DigestAsHex(password.getBytes()); SystemUser user userService.login(username, md5); if (user null) { return redirect:/login?error1; } session.setAttribute(loginUser, user); return redirect:/student/list; }Component public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (request.getSession().getAttribute(loginUser) null) { response.sendRedirect(request.getContextPath() /login); return false; } return true; } } Configuration public class WebConfig implements WebMvcConfigurer { Resource private LoginInterceptor loginInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns(/**) .excludePathPatterns(/login, /css/**, /js/**, /error); } }拦截器路径这里特别容易踩坑。addPathPatterns(/**)表示拦截所有请求如果不排除/css/**和/js/**浏览器加载的样式表会被 302 到登录页页面光秃秃的没有样式F12 能看到 CSS 请求返回 302。另外由于设置了server.servlet.context-path/studentController 里的路径不用带/student但response.sendRedirect一定要用request.getContextPath()拼上否则会跳到/login而不是/student/login直接 404。5. 联调避坑指南乱码、SQL 注入、连接池泄漏和 MySQL 连接报错5.1 乱码问题请求、响应、数据库三处都要 UTF-8乱码最常见的是表单提交“张三”插入数据库后变成“å¼ ä¸”或者“??”。这不是单一原因而是三处编码不一致页面本身没声明 UTF-8服务器解码用默认 ISO-8859-1JDBC 连接串没指定字符集数据库表用的还是 latin1。只改一处没有用。解决办法是一套组合页面meta charsetUTF-8Thymeleaf 模板默认就是 UTF-8不用额外改。连接串加characterEncodingutf8前面已经配过。建库建表统一用 utf8mb4。还是乱码的话在项目里显式注册一个字符编码过滤器。Bean public org.springframework.web.filter.CharacterEncodingFilter characterEncodingFilter() { org.springframework.web.filter.CharacterEncodingFilter filter new org.springframework.web.filter.CharacterEncodingFilter(); filter.setEncoding(UTF-8); filter.setForceEncoding(true); return filter; }setForceEncoding(true)会同时覆盖请求和响应强制用 UTF-8。如果接口返回 JSON 字符串还要在RequestMapping的produces里写application/json;charsetUTF-8否则浏览器只看 header 里的 Content-Type 不带 charset可能继续按 GBK 渲染。这个坑在前后端分离项目里更常见学生系统用 Thymeleaf 一般不会遇到。5.2 SQL 注入和 PreparedStatement 边界登录框输入 or 11密码乱填居然能登录成功这就是 SQL 注入的经典现场。原因很直接代码在拼 SQL而不是用占位符。比如早期 JSP 项目里的写法是WHERE username username AND password md5 输入 or 11后SQL 含义变成“用户名等于空或者 11”后面的密码校验直接失效。MyBatis 里同样有对应的错误写法和正确写法。!-- 错误示例面试时最常被问 -- select idlogin_bad resultTypecom.example.entity.SystemUser SELECT * FROM system_user WHERE username ${username} AND password ${password} /select !-- 正确写法 -- select idlogin_good resultTypecom.example.entity.SystemUser SELECT * FROM system_user WHERE username #{username} AND password #{password} /select${}是文本替换等于把用户输入直接拼进 SQL 再编译#{}是预编译参数MySQL 拿到的是“值”而不是“SQL 片段”。在正确写法里用户输入 or 11会被当成一个完整字符串去匹配 username 字段匹配不到就是登录失败。这个点也是 Java 开发工程师面试题里几乎必问的你能讲清楚“占位符防止的是结构注入”比背结论高很多。5.3 连接池泄漏和 MySQL 的 SSL、时区报错系统跑了两天突然偶发HikariPool-1 - Connection is not available, request timed out。日志再往后翻可能还有Communications link failure。这通常是两个原因叠加。一是业务代码里事务没有正确结束连接没有归还连接池。典型写法是方法上加了Transactional但 catch 住异常后只打印不抛出Spring 认为事务正常提交连接被事务管理器一直拿着。二是 Hikari 的max-lifetime比 MySQL 的wait_timeout大MySQL 把空闲连接断开后连接池不知道应用拿到死连接。排查步骤我一般这么走先到 MySQL 执行SHOW PROCESSLIST;看连接数是不是打满再查看SHOW VARIABLES LIKE wait_timeout;默认一般是 28800 秒8 小时最后回应用里看 Hikari 的max-lifetime必须小于 8 小时。配置里写 1800000 毫秒也就是 30 分钟是安全的。如果事务方法里确实要 catch 异常记得 catch 之后重新抛一个 RuntimeException让事务回滚。MySQL 8 还有一个独立报错ssl connection error: server doesnt support public key retrieval。这是认证插件 caching_sha2_password 和连接器之间的握手问题开发环境直接连接串加useSSLfalseallowPublicKeyRetrievaltrue。如果是照着 MySQL 5.7.44 安装过程详细教程装的老版本5.7 默认不是 caching_sha2_password这两个参数不加也能连但统一加上不会有副作用。5.4 三条高频踩坑记录现象、原因、解决页面访问 /student/list 返回 Whitelabel Error Page但数据接口是通的。原因一般是 Controller 返回的模板路径不对或者templates/student/list.html文件没编译到 target 目录。解决先看 IDEA 控制台有没有 Thymeleaf 模板解析异常再打开 target/classes/templates/student 看有没有 list.html。没有就执行mvn clean package不要只点 Rebuild Project。Excel 导入报java.lang.NoClassDefFoundError: org/apache/poi/xssf/usermodel/XSSFWorkbook。原因通常是 pom 里只引入了poi没有引入poi-ooxml或引入了两份不同版本。解决统一用 Apache POI 5.2.5只保留poi-ooxml依赖它会传递依赖poi千万别再单独引入poi-ooxml-schemas版本冲突会让人崩溃。静态资源被拦截器拦住页面没有样式。现象是页面能打开但 CSS 全部失效控制台看.css请求返回 302。原因是拦截器加addPathPatterns(/**)时没有排除静态资源目录。解决在excludePathPatterns里加/css/**、/js/**、/images/**或者干脆把拦截路径只设为/student/**、/operationlog/**静态资源不在拦截范围内。6. 进阶技巧Excel 批量导入和一键启动脚本让系统真正能交差6.1 用 Apache POI 批量导入学生名单学生信息管理系统做到最后真正能加分的是“不用一条条手工录入”。教务老师手里通常是一张 Excel 名单我一般用 Apache POI 读取并批量插入。PostMapping(/import) public String importExcel(RequestParam(file) MultipartFile file) { try (Workbook workbook WorkbookFactory.create(file.getInputStream())) { Sheet sheet workbook.getSheetAt(0); int rowIndex 0; ListStudent validList new ArrayList(); for (Row row : sheet) { if (rowIndex 0) continue; // 跳过表头 String studentNo row.getCell(0).getStringCellValue(); String name row.getCell(1).getStringCellValue(); Student s new Student(); s.setStudentNo(studentNo); s.setName(name); validList.add(s); } studentService.batchInsert(validList); } catch (Exception e) { return redirect:/student/list?importError1; } return redirect:/student/list?importSuccess1; }WorkbookFactory.create会根据文件后缀自动识别 xls 和 xlsx不用自己写分支。try-with-resources 确保 Workbook 用完即关否则重复导入几次就会把文件句柄耗尽。批量插入不要一条条 insert改成 MyBatis 的foreach一次性插入几百条配合 Service 方法上的Transactional一条失败全部回滚。真正的生产级导入还要逐行校验学号格式、手机号正则、班级是否存在把错误行号收集起来返回给前端。这个加强很现实总共不到二十行代码但会让你的系统明显比“只会单条增删改查”的版本高一个档次。6.2 一键启动脚本与接口验证项目能跑只是第一步能部署到服务器才算交差。在 Linux 上我习惯放一个启动脚本#!/bin/bash mvn clean package -DskipTests nohup java -jar target/student-system.jar --server.port8080 app.log 21 sleep 10 curl -s -o /dev/null -w %{http_code} http://localhost:8080/student/login-DskipTests跳过测试阶段加快打包nohup ... 让 Java 进程在 SSH 断开后继续运行curl -s -o /dev/null -w %{http_code}只输出 HTTP 状态码看到 200 就说明服务起来了。如果服务器内存只有 1G最好是先手动java -jar跑一次确认内存够再考虑后台运行。部署到 Linux MySQL 8.0.44 这类新环境时连接串里的serverTimezoneAsia/Shanghai和allowPublicKeyRetrievaltrue一定保留两个参数都能救你一次。我最早做这类系统时也犯过同样的错把学生删除做成物理 DELETE后来需要补考勤记录只能从备份里捞。希望你在建表阶段就把逻辑删除、日志表、事务边界想清楚这样不管项目编号怎么变答辩和后续改造都不会慌。希望帮到你。本文还有配套的精品资源点击获取