Spring Boot+MyBatis-Plus学生档案管理系统从0到1完整实践
简介这是一份基于Java的学生档案管理系统毕业设计文档面向计算机、软件工程等专业学生可作为毕业设计参考或课程设计模板。文档系统梳理了从需求分析到系统实现的完整流程包括性能需求、运行环境、B/S架构下的模型结构、Java与JSP技术选型、Tomcat部署、MySQL数据库设计以及E-R图、主要数据表和登录、院系专业、学生信息、教师信息、课程、成绩、奖惩等管理模块的功能与核心代码分析兼顾设计思路与实现细节。同时文档包含摘要、目录、正文、结束语、参考文献、致谢等完整毕业设计结构并提供了JSP编程技术和MySQL数据库开发的关键代码示例可直接作为论文框架和系统开发的双重参考。资源共1个docx文件压缩包约410KB内容以文字和图表形式呈现便于直接编辑修改。已有458人学习下载是一份结构清晰、内容详实的学生档案管理系统设计与实现方案。1. 学生档案管理系统到底在管什么给没写过 Java Web 毕设的人一个全景图以 Java 技术栈来做一个学生档案管理系统是高校毕业设计和低年级课设里最不高调却一直高频的方向。很多同学以为“管理系统”就是对着数据库写增删改查真正开始做才发现老师手里的学生档案不只是名字和学号还有班级、成绩、奖惩、宿舍、联系方式变动甚至还有纸质材料的扫描件。这套系统要解决的是把散落在 Excel、纸质表格和聊天记录里的学生信息收拢到一个 Web 网站里让管理员录得进、查得着让授课教师能按班级筛选让学生本人只能看自己的档案。这个标题背后其实是一款面向高校教务/学工场景的典型 Java Web 应用适合正在选毕业设计题目的在校生也适合刚转行、想用一套完整项目证明自己能做前后端联通的初级 Java 工程师。反直觉的结论是这个系统 80% 的工作量不在“增删改查”本身而在角色权限、表关系、字段边界和部署细节这些容易被忽视的地方。下面从表结构开始把设计与实现按一条可落地的路径拆开。2. 从需求到表结构先想清楚再敲代码学生档案管理系统的核心设计2.1 角色与功能边界别把系统做成一个无限收纳箱做设计前先问自己三个问题谁在用管什么管到什么粒度我见过最常翻车的做法是打开建表工具一口气设计二十几张表把军训报名、社团活动都塞进来结果写到一半连自己都理不清外键关系。学生档案管理系统最常见的角色有三种管理员、教师、学生。管理员负责维护班级和学生基础信息批量导入数据管理账号和操作日志教师可以按班级或课程范围查询学生档案导出成绩单学生只允许查看自己的档案和部分成绩不能改任何核心字段。功能边界一旦划清楚权限控制就落在“角色 - 菜单 - 接口”三张关联表上不需要在业务代码里到处写 if 判断。管的粒度也要收敛。一个最小可用系统建议从五类数据起步学生基础信息学号、姓名、性别、出生日期、身份证号、民族、政治面貌、班级信息班级名称、辅导员、入学年份、登录账号用户表与学生的关联、成绩记录学期、课程、分数、操作日志谁在什么时间改了什么。至于宿舍床位、贫困认定、实习单位这些可以先留扩展字段但不作为第一版的主表否则项目会陷入“无底洞式需求”。2.2 数据库选型与表关系MySQL 5.7 以上 InnoDB五张表怎么串起来数据库选型没有悬念MySQL 是这个方向最常见的标配原因很简单开源免费、部署资料多、对 Spring Boot 的支持成熟而且大部分人本地装的就是 MySQL。版本上建议 5.7 或 8.0别再用 5.5因为 5.5 对 utf8mb4 的支持不完整存 emoji 和生僻字容易出问题。存储引擎固定为 InnoDB这样才能在外键和事务上获得保障。表关系按“档案为中心”来设计student 表是核心通过 class_id 关联 class_info 表user 表通过 student_id 一对一关联 student用于登录鉴权score_record 表通过 student_id 关联学生实现“一个学生多条成绩记录”的一对多archive_log 表独立记录操作行为不参与业务主链路。外键我建议只在设计文档里体现代码里不要真的建立物理外键原因是后续做分页、批量导入和逻辑删除时物理外键会带来解锁和级联麻烦用程序逻辑控制一致性更顺手。2.3 可直接抄的建表 SQL字符集、索引和注释一次到位建表 SQL 是核心交付物建议直接在 Navicat 或命令行里执行下面这版是我在实际项目和毕设指导中常用的最小完整结构可以直接抄CREATE DATABASE IF NOT EXISTS student_archive DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE student_archive; CREATE TABLE class_info ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 班级ID, class_name VARCHAR(50) NOT NULL COMMENT 班级名称, major_name VARCHAR(50) NOT NULL COMMENT 专业名称, enrollment_year INT NOT NULL COMMENT 入学年份, counselor VARCHAR(20) NULL COMMENT 辅导员姓名, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_class_name (class_name, enrollment_year) ) ENGINEInnoDB COMMENT班级信息表; CREATE TABLE student ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 学生ID, student_no VARCHAR(20) NOT NULL COMMENT 学号, name VARCHAR(50) NOT NULL COMMENT 姓名, gender TINYINT DEFAULT 1 COMMENT 性别: 1男 2女, birth_date DATE NULL COMMENT 出生日期, id_card VARCHAR(18) NULL COMMENT 身份证号, phone VARCHAR(20) NULL COMMENT 手机号, class_id BIGINT NULL COMMENT 班级ID, status TINYINT DEFAULT 1 COMMENT 状态: 1在读 2休学 3毕业, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_student_no (student_no), KEY idx_class_id (class_id) ) ENGINEInnoDB COMMENT学生基础档案表; CREATE TABLE user ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 用户ID, student_id BIGINT NULL COMMENT 关联学生ID教师/管理员为空, username VARCHAR(30) NOT NULL COMMENT 登录账号, password VARCHAR(100) NOT NULL COMMENT 加密后的密码, role VARCHAR(20) NOT NULL DEFAULT student COMMENT 角色, status TINYINT DEFAULT 1 COMMENT 账号状态, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username), KEY idx_student_id (student_id) ) ENGINEInnoDB COMMENT登录账号表; CREATE TABLE score_record ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 成绩ID, student_id BIGINT NOT NULL COMMENT 学生ID, course_name VARCHAR(100) NOT NULL COMMENT 课程名称, semester VARCHAR(20) NOT NULL COMMENT 学期如2024-2025-1, score DECIMAL(5,2) NOT NULL COMMENT 分数, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_student_semester (student_id, semester) ) ENGINEInnoDB COMMENT学生成绩表; CREATE TABLE archive_log ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 日志ID, user_id BIGINT NOT NULL COMMENT 操作人ID, action VARCHAR(50) NOT NULL COMMENT 操作类型, content VARCHAR(255) NULL COMMENT 操作内容描述, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_create (user_id, create_time) ) ENGINEInnoDB COMMENT操作日志表;几个容易被忽略的参数说明id 统一用 BIGINT避免将来数据量上来后 int 溢出字符集用 utf8mb4 而不是 utf8否则学生姓名里出现特殊字符和 emoji 会直接报错student_no 和 username 都建唯一索引这是登录和导入时查重的基础score 用 DECIMAL(5,2) 而不是 FLOAT因为浮点数存储成绩会有 0.1 加 0.2 不等于 0.3 的经典精度问题面试如果聊到这个点会加分status 字段用 TINYINT 做状态位比字符串更省空间也方便后续扩展枚举。五张表建好后业务代码只需要围绕 student 和 score_record 展开登录鉴权围绕 user 表展开日志可以作为切面在 Service 层统一埋点。3. Spring Boot MyBatis-Plus 后端落地从建工程到跑通学生档案的增删改查3.1 环境准备与工程骨架JDK 8/17、Maven、IDEA 的常见组合动手写代码之前先把环境固定下来。我一般用 JDK 1.8 或 11配合 Maven 3.6IDE 用 IDEA如果选 Spring Boot 2.7.xJDK 8 完全够用如果选 Spring Boot 3.x则必须上 JDK 17这一点在 java 环境配置阶段最容易踩版本坑。很多人在第一步就翻车就是因为在 IDEA 里同时开着几个 JDK 版本项目必须用 8但默认环境变量指向了 17。工程骨架直接在 IDEA 里用 Spring Initializr 创建也可以去官网生成压缩包再导入。依赖选择不必贪多Web、MySQL Driver、Lombok 三个就够MyBatis-Plus 需要手动引入。pom.xml 里最关键的部分如下parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency这里的版本组合是我在多个项目里验证过不会打架的组合Spring Boot 2.7.18 是 2.x 系列的收尾版本修复了大量已知问题MyBatis-Plus 3.5.5 对 Boot 2.x 支持最稳。如果你一上来就追新用 Boot 3.x会发现 mybatis-plus-boot-starter 的包名和自动配置方式都变了排错成本一下子拉高。3.2 实体类设计与生成建表 SQLMyBatis-Plus 注解怎么映射字段MyBatis-Plus 没有像 JPA 那样根据实体类自动建表的能力但它的注解本身就是建表元数据。我常用的思路是先写实体类再用注解把字段名、主键策略、逻辑删除标出来然后对照注解去写 CREATE TABLE这个流程虽然不能一键生成但能保证实体和表结构完全对齐避免运行时下划线转驼峰映射错乱。以 student 表为例实体类这样写Data TableName(student) public class Student { TableId(type IdType.AUTO) private Long id; TableField(student_no) private String studentNo; private String name; private Integer gender; TableField(birth_date) private LocalDate birthDate; TableField(id_card) private String idCard; private String phone; TableField(class_id) private Long classId; private Integer status; }关键参数解释TableName 指定实体类映射到哪张数据表表名与类名大小写不同时必须显式声明TableId 主键注解type IdType.AUTO 表示使用数据库自增主键如果你的表主键是手动赋值或雪花 ID这里要改成 ASSIGN_IDTableField 用来处理 Java 驼峰属性与数据库下划线字段的映射比如 birthDate 对应 birth_date。属性上不加注解的字段MyBatis-Plus 会默认按 map-underscore-to-camel-case 规则转换所以 name、gender、phone、status 这种单单词字段可以省略注解。需要注意的是MyBatis-Plus 本身不根据实体类生成创建表的 SQL 语句如果需要自动维护表结构我更建议引入 Flyway把建表 SQL 放到 db/migration 目录里随应用启动执行。这样既保留了 SQL 的可审查性又比手动在 Navicat 里执行更规范。3.3 Service 与 Controller分页、条件查询、新增编辑删除的完整写法后端核心不复杂关键是“一个 Service 接口 实现类 Controller 统一返回体”。先定义统一返回类 R 节省后面每个接口重复包装数据的时间Data public class RT { private Integer code; private String message; private T data; public static T RT ok(T data) { RT r new R(); r.setCode(200); r.setMessage(success); r.setData(data); return r; } }然后写学生档案的业务接口这里选择直接继承 MyBatis-Plus 的 IService让框架把通用 Mapper 方法暴露出来public interface StudentService extends IServiceStudent { PageStudent getStudentPage(int current, int size, String keyword, Long classId); } Service public class StudentServiceImpl extends ServiceImplStudentMapper, Student implements StudentService { Override public PageStudent getStudentPage(int current, int size, String keyword, Long classId) { LambdaQueryWrapperStudent wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(keyword), Student::getName, keyword) .or(StringUtils.hasText(keyword), w - w.like(Student::getStudentNo, keyword)) .eq(classId ! null, Student::getClassId, classId) .orderByAsc(Student::getStudentNo); PageStudent page new Page(current, size); return this.page(page, wrapper); } }这段代码里的逻辑说明LambdaQueryWrapper 是 MyBatis-Plus 的条件构造器用 Lambda 表达式引用实体属性编译期就能发现写错的字段名比字符串拼接安全得多like 方法前面带上 StringUtils.hasText(keyword) 作为 condition 参数keyword 为 null 或空串时该条件自动不生效注意 or 的写法我这里先用一个 like 查姓名再用 or 包一个 like 查学号如果不套 or 子分组条件会变成“姓名 like 或 学号 like 且 classId 等于”逻辑就错了。分页需要配置分页插件在启动类或配置类里注册 MybatisPlusInterceptorBean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }Controller 层把 HTTP 语义映射清楚RestController RequestMapping(/api/student) public class StudentController { Autowired private StudentService studentService; GetMapping(/page) public RPageStudent page(RequestParam int current, RequestParam int size, RequestParam(required false) String keyword, RequestParam(required false) Long classId) { return R.ok(studentService.getStudentPage(current, size, keyword, classId)); } PostMapping public RVoid save(RequestBody Student student) { studentService.save(student); return R.ok(null); } PutMapping public RVoid update(RequestBody Student student) { studentService.updateById(student); return R.ok(null); } DeleteMapping(/{id}) public RVoid delete(PathVariable Long id) { studentService.removeById(id); return R.ok(null); } }参数说明RequestParam 绑定的 current 和 size 是页码和每页条数前端必传keyword 和 classId 可选不传时后端条件自动放行。save 方法直接用 MyBatis-Plus 的 save、updateById、removeById单表 CRUD 不需要写一行 SQL。如果你在 java 面试题里被问到“MyBatis-Plus 有什么缺点”可以答复杂多表联查时还是得写自定义 SQL简单 CRUD 才适合直接用它扛。4. Vue Element UI 前端页面把档案管理做成能过验收的后台界面4.1 前端工程初始化与跨浏览器设计用 Vue CLI 搭一个不挑浏览器的后台后端就绪后前端我用 Vue 2 Element UI 这套组合理由是在跨浏览器支持的设计与实现上最省心Element UI 的组件已经处理掉大部分 IE 之外的兼容问题Vue CLI 内置 Babel 会自动转译 ES6 语法Chrome、Firefox、Edge、Safari 下渲染结果基本一致。如果你用 Vue 3 Element Plus 也可以但对刚起步的人来说 Vue 2 的教程和踩坑资料更多遇到问题更容易搜到答案。初始化命令vue create student-ui npm install element-ui axiosmain.js 里这样引入import Vue from vue import ElementUI from element-ui import element-ui/lib/theme-chalk/index.css import App from ./App.vue Vue.use(ElementUI) Vue.config.productionTip false new Vue({ render: h h(App) }).$mount(#app)这里的跨浏览器支持主要在 Babel 配置和 CSS 前缀上。Vue CLI 创建项目时 Babel 配置已经是预设的不需要额外改Element UI 的样式自带 -webkit、-moz 等前缀文本溢出和表格宽度在不同浏览器下表现比较稳定。实际开发中我还会在 axios 请求里加一个超时时间和统一的错误提示避免某个浏览器下请求长时间挂起导致页面看起来“死掉”。4.2 学生列表页分页表格 搜索表单 弹窗编辑的实现列表页是这套系统的门面也是答辩时最容易吸引眼球的地方。拆分好组件后主体工作只有三块搜索表单、el-table、el-pagination。下面这个组件片段删减了样式代码保留了核心逻辑template div el-form :inlinetrue el-form-item label关键字 el-input v-modelquery.keyword placeholder姓名/学号 clearable / /el-form-item el-form-item el-button typeprimary clickloadData查询/el-button /el-form-item /el-form el-table :datalist border stripe el-table-column propstudentNo label学号 width120 / el-table-column propname label姓名 width100 / el-table-column propgender label性别 width80 template slot-scopescope {{ scope.row.gender 1 ? 男 : 女 }} /template /el-table-column el-table-column propphone label手机号 / el-table-column label操作 width180 template slot-scopescope el-button sizemini clickopenEdit(scope.row)编辑/el-button el-button sizemini typedanger clickremoveRow(scope.row)删除/el-button /template /el-table-column /el-table el-pagination background layouttotal, prev, pager, next :totaltotal :current-page.syncquery.current :page-sizequery.size current-changeloadData / /div /template script import axios from axios export default { data() { return { list: [], total: 0, query: { current: 1, size: 10, keyword: } } }, methods: { async loadData() { const res await axios.get(/api/student/page, { params: this.query }) this.list res.data.data.records this.total res.data.data.total }, openEdit(row) { // 这里打开弹窗把 row 拷贝到表单对象避免直接改表格数据 console.log(row) }, async removeRow(row) { await axios.delete(/api/student/${row.id}) this.loadData() } }, created() { this.loadData() } } /script这段代码的关键点el-table-column 使用 prop 绑定字段显示 0/1 这类数字状态时用 slot-scope 做格式化比在数据源里预处理更直观el-pagination 用 current-page.sync 做双向绑定页码变化时自动触发 loadDataaxios 的 params 会序列化对象后端再用 RequestParam 接字段名没有歧义。开发环境下需要解决跨域在 vue.config.js 里配置代理module.exports { devServer: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }代理配置的原因很简单前端跑在 8081后端跑在 8080浏览器认为端口不同就是跨域。有了代理后前端代码里写 /api 开头的路径开发服务器会代理到后端地址规避了生产环境还要配 Nginx 的麻烦。4.3 照片上传与回显FormData 提交和浏览器兼容细节学生档案里“照片上传”几乎必做这里隐藏着不少浏览器兼容细节。前端上传组件我用 el-upload配合 FormData 提交不直接传 base64 字符串因为大照片转 base64 会让请求体膨胀到几兆后端解析也慢。核心代码如下el-upload action# :show-file-listfalse :before-uploadonBeforeUpload acceptimage/jpeg,image/png el-button sizesmall上传照片/el-button /el-uploadasync onBeforeUpload(file) { const formData new FormData() formData.append(file, file) const res await axios.post(/api/student/upload, formData, { headers: { Content-Type: multipart/form-data } }) return false }accept 控制在 Chrome 和 Edge 下会过滤文件类型但在 Firefox 下只是提示不是强制所以后端还需要校验文件扩展名和 MIME 类型。照片回显时我一般让后端把文件保存到本地磁盘然后返回一个带时间戳的图片访问路径前端在 el-image 或 img 标签里拼接后端地址。如果照片文件名不变浏览器缓存会让替换后的照片不刷新这就是我强调“带时间戳”的原因这也是很多毕设项目演示照片老是旧图的血泪经验。5. 避坑清单MyBatis-Plus、Tomcat、MySQL 这些地方最容易翻车5.1 查询结果全是 null下划线转驼峰没生效的真相现象前端表格能返回记录数但 studentNo、birthDate 这类字段全部为 nullname、phone 却正常。第一次遇到时我以为是 JSON 序列化问题折腾了半天。原因MyBatis-Plus 的 mapUnderscoreToCamelCase 默认是 true但如果你在 application.yml 里自定义了 configuration 配置没有把这一项显式打开或者直接用了原生 MyBatis 的 ResultMap驼峰映射就会失效。另外实体类里没加 TableField 的长字段其实依赖的就是这个全局配置。解决在 application.yml 中显式声明mybatis-plus: configuration: map-underscore-to-camel-case: true还有另一个细节如果你用 Lombok 的 Data字段命名是 studentNo那么 JSON 序列化后前端拿到的是 studentNo但有的同事会把字段命名为 student_no前者才是 Java 标准驼峰命名后者的风格会导致前端也需要跟着用下划线统一比什么都重要。5.2 启动时数据库连接报错serverTimezone 这个参数不能省现象Spring Boot 启动时报错类似于 stating up 时遇到 Cannot create PoolableConnectionFactory或者控制台里出现 The server time zone value 提示。原因MySQL 8.0 之后驱动要求客户端显式指定时区而很多人数据库连接串还是照着 MySQL 5.6 时代抄的没有加 serverTimezone 参数。即使能连上日期字段也会出现和本地时间相差 8 小时的情况。解决连接串写成这样两个参数一个都不能少url: jdbc:mysql://localhost:3306/student_archive?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai这里 serverTimezone 用 Asia/Shanghai不要用中国标准时间这种中文值因为某些驱动版本解析不了。没了 useSSLfalse你还会时不时看到 SSL 握手警告虽然不影响运行但日志刷得让人心慌。5.3 前端调后端接口被 403跨域不是玄学是没配置 CORS现象前端页面能打开但 axios 请求直接失败控制台提示 Access to XMLHttpRequest has been blocked by CORS policyNetwork 标签里显示状态码 403。原因浏览器同源策略拦截了跨端口请求。开发时前端 8081、后端 8080这就是两个源有时前端用了代理但请求路径没有走代理前缀也会白白踩跨域。解决在后端写一个全局跨域配置类不用在每个 Controller 上加注解Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }注意 allowedOriginPatterns() 和 allowCredentials(true) 要搭配使用原来用 allowedOrigins() 在某些版本下会报“origin pattern cannot be null”的错。这个配置一旦加上前后端联调会顺畅很多属于典型的“当初觉得玄学实际就是规范”的坑。5.4 上传档案照片报错Spring Boot 和 Tomcat 的限制是双层的现象小文件上传正常超过 1MB 的照片一传就报 FileSizeLimitExceededException或者提示请求大小超出限制。原因Spring Boot 内置的 multipart 配置默认单文件最大 1MB总请求最大 10MB同时 Tomcat 自身还有一个 maxSwallowSize默认 2MB。如果只调 Spring 的配置不调 Tomcat 的报错依然存在这就是双门槛。解决在 application.yml 中同时配置spring: servlet: multipart: enabled: true max-file-size: 10MB max-request-size: 20MB server: tomcat: max-swallow-size: 20MBmax-swallow-size 这个参数的含义是 Tomcat 允许丢弃的未读取请求体大小表单提交时文件超出会被这里拦截。我见过很多人只改了 multipart 配置问题依旧最后把 Tomcat 配置加上才消停。这个坑在答辩现场翻车率极高建议提前用超过 2MB 的照片实测一遍。5.5 插入数据主键冲突自增 ID 和雪花算法你要选一个现象新增学生时偶尔成功偶尔立刻抛出 Duplicate entry xxx for key PRIMARY但数据库里明明没有这条记录。原因MyBatis-Plus 的 ID 默认策略是 ASSIGN_ID会生成一个雪花 ID也就是 19 位左右的数字。如果表的主键是 AUTO_INCREMENT实体类里又没有声明 TableId(type IdType.AUTO)MyBatis-Plus 会把雪花 ID 当作主键塞进 SQL但数据库的自增序列并没有跳到这个值下次插入时自增 ID 和雪花 ID 就撞车了。解决实体类主键上显式声明TableId(type IdType.AUTO) private Long id;如果你的表确实不想用自增那就把数据库字段改成 BIGINT并让 MyBatis-Plus 用 ASSIGN_ID 生成雪花主键。不要一会儿用自增、一会儿依赖默认策略这属于设计不一致不是框架 bug。我建议学生档案这类低并发管理系统直接走数据库自增简单直观写 SQL 排查时也更容易理解和调试。6. 进阶玩法用代码生成器把 CRUD 变成模板再给学生档案加上 Excel 导出6.1 MyBatis-Plus CodeGenerator 一键生成整套 CRUD 骨架表结构调整后手写实体、Mapper、Service、Controller 非常浪费时间。MyBatis-Plus 的代码生成器可以把这张表的骨架一次性生成常见做法是在启动类里临时加一段生成逻辑跑完就删。你需要先引入 mybatis-plus-generator 和 velocity-engine-core 依赖然后配置数据源和表名执行后代码会自动落到指定包下。生成的结果可以直接省掉手写 CRUD 的时间但要注意生成器默认生成的 Controller 方法粒度很粗比如会把 save 和 update 合并成一个方法你仍然要按业务边界拆开不能指望它一步到位。这个技巧能让整个项目后期维护效率提升不少。6.2 学生档案导出 Excel用 EasyExcel 写一个带模板的下载接口导出 Excel 是答辩时很加分但又容易临时加班的功能。我一般用 Alibaba EasyExcel 而不是 Apache POI 直接操作因为 EasyExcel 内存占得少API 更贴近业务。先引入依赖dependency groupIdcom.alibaba/groupId artifactIdeasyexcel/artifactId version3.3.4/version /dependency在 Controller 里加一个导出接口直接让浏览器下载GetMapping(/export) public void export(HttpServletResponse response) throws IOException { response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setCharacterEncoding(utf-8); response.setHeader(Content-disposition, attachment;filenamestudent.xlsx); ListStudent list studentService.list(); EasyExcel.write(response.getOutputStream(), Student.class) .sheet(学生档案) .doWrite(list); }这段代码的关键是 setContentType 必须用 Excel 的标准 MIME 类型否则部分浏览器会把它当成 HTML 下载打开后乱码Student.class 里可以用 ExcelProperty 注解指定列名和顺序比如学号、姓名、性别、手机号。导出大列表时一般还要加 async 或限制导出条数学生档案规模不会太大所以这里直接 list() 是最简单可靠的写法。最后说一点私人教训我当年做这款系统时最浪费时间的不是编码而是反复改表结构。今天加个字段明天加张表前后端跟着一起动漏掉一个地方就要调试半天。后来我养成了一个习惯任何改动先落 SQL 脚本、再动实体类、最后才写接口和页面顺序一旦乱了数据模型和业务逻辑就开始互相打架。你能看到这里说明你也在认真对待这个方向希望帮到你早点把系统跑起来再把细节打磨到位。本文还有配套的精品资源点击获取