
简介一份基于Spring Boot、Vue.js与MyBatisPlus的前后端分离项目实战教程PDF由作者虾米大王撰写以学生成绩管理系统为例适合具有Java Web基础且希望掌握全栈开发流程的读者。教程先从数据库设计讲起详细说明用户表、学生表、成绩表三张核心表的结构与字段并通过删除标记字段实现数据软删除后端部分使用Spring Boot框架搭建配置Maven依赖利用MyBatisPlus操作数据库自动生成实体类与数据访问接口提供标准的接口服务前端部分使用Vue.js开发登录、学生列表、成绩录入等界面组件通过异步请求与后端交互。读者按照步骤即可完成一个可运行的学生成绩管理系统理解前后端分离模式下的接口设计、数据传递与页面渲染方法。资源共包含一个PDF文件大小约2.8MB内附建表语句、项目配置等关键内容适合课程设计或毕业设计参考目前已有四百余人学习下载内容实用步骤清晰。1. 学生成绩管理系统这份 PDF能跑通前后端分离的最小闭环下载这份 springbootvuemybatisplus 前后端分离项目实战 PDF 之前我一度以为它又是一份堆概念的 PPT。真正拆完才发现它是个能跑通的最小闭环学生成绩管理系统三张表、Spring Boot 2.7.17 MyBatisPlus 3.4.1 做后端前端用 Vue.js 对接 RESTful 接口连 JWT 登录、Swagger 文档和白名单都配好了。作者虾米大王公众号码农新势力建表 SQL、pom.xml、实体类、Mapper XML 全贴了出来几乎没有藏私。适合刚过完 Spring Boot 基础、想完整走一遍前后端分离全流程的人也适合拿来做毕设骨架。结构干净照着敲一遍能收获对数据库 → 后端 → 接口 → 前端整条链路的实感。2. 建表与依赖先行三张表的软删除设计和版本选型写这种管理系统第一件事不是写代码而是把表结构定下来。这份 PDF 给了三张表am_user 用户表、am_student 学生表、am_score 成绩表。三张表全部带 is_deleted 字段这是 MyBatisPlus 逻辑删除的标准姿势也是全文的第一个关键设计点。2.1 三张表为什么这么设计软删除字段才是关键先看用户表的原样建表 SQL后面所有实体类都是围绕它来的drop table if exists am_user; create table am_user( id bigint not null auto_increment primary key comment 主键id, user_name varchar(50) not null comment 用户名, pwd varchar(50) not null comment 密码, is_deleted tinyint default 0 comment 是否删除1删除0正常 ) comment 用户表; insert into am_user values (1,admin,1234,0);逻辑说明am_user 只有用户名和密码两个业务字段没有任何角色字段。原因是这套系统定位就是「管理员 教师」共用一个账户表登录后统一走 JWT 签发 token没有做细粒度的 RBAC。如果你想接学生登录需要自己加 role 字段或者单独建角色表。is_deleted 是逻辑删除标志位0 正常、1 删除MyBatisPlus 会把它自动拼进所有 SQL 的条件里查询时默认只捞 is_deleted0 的数据删除时执行的是 UPDATE 而不是 DELETE。学生表和成绩表的设计更值得看drop table if exists am_student; create table am_student( id bigint not null auto_increment primary key comment 主键id, number varchar(50) not null comment 学号因为使用软删除所以未设置唯一键索引, real_name varchar(50) not null comment 姓名, enroll_time datetime not null default CURRENT_TIMESTAMP comment 入学日期, is_deleted tinyint default 0 comment 是否删除1删除0正常 ) comment 学生表; drop table if exists am_score; create table am_score( id bigint not null auto_increment primary key comment 主键id, student_id bigint not null comment 学生id, exam_name varchar(100) not null comment 考试名称, score_cn decimal(5,2) default 0 comment 语文成绩, score_en decimal(5,2) default 0 comment 英语成绩, score_math decimal(5,2) default 0 comment 数学成绩, is_deleted tinyint default 0 comment 是否删除1删除0正常 ) comment 成绩表;参数说明number 字段在注释里特意写了「因为使用软删除所以未设置唯一键索引」。这个点新手很容易忽略——如果给 number 建了唯一索引那么逻辑删除的学生学号会被索引挡住导致同一个学号无法重新录入。这是软删除方案和唯一约束之间的天然冲突作者的取舍是弃用唯一索引把学号重复校验下沉到业务代码里。成绩表用 decimal(5,2) 而不是 double5 位总长、2 位小数最大能存 999.99对百分制成绩完全够用。值得一提的小尾巴PDF 里实体类用 Double 接收成绩而表结构是 decimal这种不一致在联调阶段偶尔会冒出来后面避坑章会再提。2.2 pom.xml 依赖选型版本号背后是兼容性考虑Spring Boot 选 2.7.17 是聪明的它是 2.x 分支里比较稳的高版本Java 8 完全兼容不会一上来就卡在 Spring Boot 3 的 jakarta 命名空间迁移上。MyBatisPlus 用 3.4.1和 2.7.17 配合不需要额外调 mybatis-spring 版本逻辑删除、分页插件、代码生成器这些核心功能都在这个版本里稳定可用。依赖版本选型理由spring-boot-starter-parent2.7.172.x 分支最稳兼容 Java 8mybatis-plus-boot-starter3.4.1内置分页、逻辑删除、代码生成器mybatis-plus-generator3.4.1配套代码生成器正文后面会用到mysql-connector-java5.1.47适配 MySQL 5.x高版本 MySQL 建议换 8.xjava-jwt3.11.0轻量 JWT 签发与校验springfox-swagger2 / ui2.9.2配合 Spring Boot 2.7 需要 ant_path_matcherfastjson1.2.72JSON 序列化老项目常见搭配freemarker2.3.30代码生成器的模板引擎这里有个现实提醒mysql-connector-java 5.1.47 只适配 MySQL 5.x。如果本机装的是 MySQL 8.0最典型的现象是启动时 SSL 连接警告或者偶尔报时区错误需要把坐标换成 mysql-connector-j 8.0.x。MyBatisPlus 和 mysql 驱动版本追求的不是「最新」而是「能跑且你能解释为什么」作者这套组合明显是踩过坑之后锁定的。2.3 application.properties 里的三处关键配置配置写在 application.properties 上一眼能看完但信息密度很高#服务端口 server.port8089 #mysql链接配置 spring.datasource.driver-class-namecom.mysql.jdbc.Driver spring.datasource.urljdbc:mysql://localhost:3306/test?useUnicodetruecharacterEncodingutf-8useSSLfalseserverTimeZoneUTC spring.datasource.usernameroot spring.datasource.passwordmysql123 #格式化时间 spring.jackson.date-formatyyyy-MM-dd spring.jackson.time-zoneGMT8 #日志 mybatis-plus.configuration.log-implorg.apache.ibatis.logging.stdout.StdOutImpl #配置别名 mybatis-plus.type-aliases-packagecom.shrimpking.pojo #swagger配置 spring.mvc.pathmatch.matching-strategyant_path_matcher #开启逻辑删除标识字段 mybatis-plus.global-config.db-config.logic-delete-fieldis_deleted mybatis-plus.global-config.db-config.logic-delete-value1 mybatis-plus.global-config.db-config.logic-not-delete-value0 #可直接访问的白名单 auth.ignoreUrls/user/login,/swagger-ui.html,/swagger-resources/**,/csrf,/error,/favicon.ico,/user/login2,/webjars/springfox-swagger-ui/**,/student/add有四处必须看懂不然项目跑起来就是一团乱麻。第一处是 MyBatisPlus 的全局逻辑删除配置。logic-delete-field 指定全局逻辑删除字段名配合 pojo 里的 TableLogic 注解以后所有 MP 自动生成的 SQL 都会带上is_deleted0条件。注意 value 的含义logic-delete-value1 表示删除时把 is_deleted 更新成 1logic-not-delete-value0 表示查询时只取 0。这套全局配置加上实体类的 TableLogic 注解是双保险少配一个都可能导致删除变成物理删除。第二处是 spring.mvc.pathmatch.matching-strategyant_path_matcher。Spring Boot 2.6 以后默认 PathPatternParser而 Springfox 2.9.2 这个时代的 swagger 在 Pattern 解析上不兼容不配这一行Swagger UI 会直接报错或者页面空白。配置文件里出现它说明作者已经替你踩过一遍这也是 5.4 节要展开的坑。第三处是 auth.ignoreUrls 白名单它不是 Spring 的标准配置项是项目里自定义的登录过滤器读的配置。/user/login 放行登录/swagger-ui.html 和 /webjars/** 放行文档后面跟着的 /student/add 直接放行新增学生接口——细看会发现这个白名单很宽松生产环境绝对不能照抄为什么后面单独讲。第四处是 spring.jackson.date-formatyyyy-MM-dd。它只对 java.util.Date 生效如果你实体类用了 LocalDateTime这个配置就失效返回 JSON 里会出现 2023-11-04T12:00:00 这种带 T 的格式。正文里 Student 用的恰好是 java.util.Date说明作者在这里是有意避开了 LocalDateTime 的坑但你自己加字段时很容易踩回去。3. 后端分层怎么写pojo、mapper、service 的注解约定与封装这套项目的后端分层特别适合当脚手架看没有花哨的架构就是 pojo → mapper → service → controller 四层。批量写业务之前先把 pojo 层注解吃透后面每一层都会省事。3.1 pojo 层注解背后的约定User 实体是标准模板Data EqualsAndHashCode(callSuper false) TableName(am_user) ApiModel(valueUser对象, description用户表) public class User implements Serializable { TableId(value id, type IdType.AUTO) private Long id; ApiModelProperty(value 用户名) private String userName; ApiModelProperty(value 密码) private String pwd; TableLogic ApiModelProperty(value 是否删除1删除0正常) private Integer isDeleted; }逻辑说明TableName 把实体映射到 am_user 表TableId 标记主键并指定自增策略TableLogic 挂在 isDeleted 上实现逻辑删除。这里有个细节user_name 在 Java 里叫 userName靠 MyBatisPlus 内置的驼峰映射自动把user_name ↔ userName对应起来所以即使没有在字段上加 TableField(user_name) 也能正常工作。EqualsAndHashCode(callSuper false) 是 Lombok 的常规操作实体类不继承父类时写不写都行但留着能避免以后加继承时 equals 方法行为变化。TableLogic 只在 MyBatisPlus 的自动 SQL 里生效。如果你自己手写 XML SQL、自己拼 WHERE 条件逻辑删除条件要自己补这是最常见的「逻辑删除突然不生效」的误报来源。Student 和 Score 在字段级别加了校验注解这是 bean validation 的标准玩法NotBlank ApiModelProperty(value 学号) private String number; NotNull ApiModelProperty(value 入学日期) private Date enrollTime;NotBlank 用于 String 判空还会去掉首尾空格NotNull 用于对象判空。Controller 层入口参数上加 Valid 才会触发校验没加 Valid 的话这些注解形同虚设——这个点常被忽略导致参数校验注解写了跟没写一样。3.2 mapper 接口与 XMLBaseMapper 之外还需要什么Mapper 接口简练到只有一行public interface UserMapper extends BaseMapperUser { }BaseMapper 已经把单表 CRUD、批量插入、分页查询的通用方法全内置了绝大多数业务不需要自己写一条 SQL。同一目录下的 UserMapper.xml 甚至只写了 resultMap 和 Base_Column_List 两段声明性内容没有一条具体 SQLmapper namespacecom.shrimpking.mapper.UserMapper resultMap idBaseResultMap typecom.shrimpking.pojo.User id columnid propertyid / result columnuser_name propertyuserName / result columnpwd propertypwd / result columnis_deleted propertyisDeleted / /resultMap sql idBase_Column_List id, user_name, pwd, is_deleted /sql /mapper逻辑说明resultMap 把数据库列名和 Java 属性名做了显式映射Base_Column_List 是一段通用查询列片段以后写自定义 SQL 时可以直接include refidBase_Column_List/引用。namespace 必须和 Mapper 接口全限定名一致这是 MyBatis 的硬性约定。既然 BaseMapper 够用为什么还要留 XML一是给未来的复杂查询预留位置多表联查、复杂动态 SQL二是 MyBatis 规范里 Mapper 接口和 XML namespace 必须一一对应接口存在但 XML 缺失在启动时一般没事一旦运行时需要加载 XML 里的语句就会报 BindingException让人误以为项目环境坏了。还要提醒一点Mapper 接口所在的包必须被 MapperScan 扫描到或者每个接口上单独加 Mapper 注解。这个项目里肯定配了扫描但很多人从别的项目拷贝代码时容易漏掉启动报「Invalid bound statement」先查这个。3.3 service 与 controller把 CRUD 封装成交互动作Service 接口全部继承 IService它和 BaseMapper 的区别在于 Service 层能叠加事务、业务校验和批量操作逻辑。IService 提供 getOne、saveBatch、page、lambdaQuery 这些链式方法实际写业务时比直接调 Mapper 更顺手。Controller 是资源正文没贴出来的部分但按这套项目的结构最常见写法是这样RestController RequestMapping(/student) public class StudentController { Resource private StudentService studentService; PostMapping(/add) public RBoolean add(RequestBody Valid Student student) { return R.success(studentService.save(student)); } GetMapping(/list) public RPageStudent list(RequestParam(defaultValue 1) int page, RequestParam(defaultValue 10) int size) { PageStudent p new Page(page, size); LambdaQueryWrapperStudent wrapper new LambdaQueryWrapper(); wrapper.eq(Student::getIsDeleted, 0).orderByDesc(Student::getId); return R.success(studentService.page(p, wrapper)); } }参数说明LambdaQueryWrapper 是 MyBatisPlus 3.x 的推荐查询构造器用方法引用替代字符串列名编译期就能发现字段拼写错误。eq(Student::getIsDeleted, 0) 查的是逻辑未删除数据虽然 MyBatisPlus 会自动拼逻辑删除条件但显式写出来语义更清楚。分页用 Page 对象传入 service.page()注意这里的返回数据是 IPage 的子类前端拿到的结构是 records total current size 四件套。这段代码里有几个实战判断一是避免在 Controller 里直接使用 QueryWrapper 做复杂条件拼装业务复杂以后会把 Controller 撑爆二是分页查询必须配合 MybatisPlusInterceptor 里注册的 PaginationInnerInterceptor只 new Page 不注册拦截器拿到的就是全表数据而不是一页数据——这个坑下章排错细说。4. 前后端分离的关键一公里JWT 鉴权、白名单与接口约定很多照着项目敲的人卡在这一步后端接口在 Swagger 里调得好好的前端一接就 401。原因通常是 JWT 拦截器的放行逻辑和前端 axios 的 header 不一致。这套系统的 auth.ignoreUrls 配置正好把问题暴露得特别直白。4.1 auth.ignoreUrls 在保护什么application.properties 里这段白名单是自定义鉴权过滤器在读的auth.ignoreUrls/user/login,/swagger-ui.html,/swagger-resources/**,/csrf,/error,/favicon.ico,/user/login2,/webjars/springfox-swagger-ui/**,/student/add它表达了两层意思登录接口和 Swagger 文档相关路径无条件放行/student/add 也被放行了——这大概率是为了调试方便留下的口子或者作者意在让这个接口配合某种不登录的录入场景。无论哪种原因生产环境把这个路径留在白名单里都是危险的等于任何人都可以不登录往学生表里写数据。正常流程是前端把用户名密码 POST 到 /user/login后端校验通过后用 java-jwt 签发 token 返回前端把 token 存进 localStorage 并在后续请求的 header 里带 Authorization: Bearer xxx。拦截器从 header 取 token 校验不合法就返回 401。JWT 本身的实现不复杂密钥管理、过期时间、黑名单才是生产环境真正要操心的部分但脚手架阶段能跑通全链路就够用了。4.2 Vue 侧怎么接axios 封装与路由守卫PDF 的正文没有贴 Vue 源码但既然叫前后端分离项目前端必然要走 axios。常规做法是在 src/utils/request.js 里封装一个 axios 实例import axios from axios const request axios.create({ baseURL: http://localhost:8089, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer token } return config }) request.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { localStorage.removeItem(token) window.location.href /login } return Promise.reject(error) } ) export default request逻辑说明请求拦截器统一注入 token响应拦截器统一处理后端返回的 401 并跳回登录页。baseURL 写 8089和后端 server.port 对应。这里有个细节后端 R 统一返回体长这样{ code: 200, msg: success, data: ... }所以响应拦截器 return response.data 之后页面里再取一层 data 字段层级别搞混不然经常出现「明明有数据却取不到」的乌龙。路由守卫在 Vue Router 里配置作用是登录态未过期前不允许跳回登录页未登录时不允许进入业务路由router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path ! /login !token) { next(/login) } else { next() } })那行 401 处理值得一提不要一 401 就弹框闪一下先清 token 再跳登录页不然用户会反复看到错误提示体验很差。如果后端返回的是 200 但 code 是 500 这种业务错误应该在全局响应拦截器里先判断 code再决定走错误提示还是放行不能只盯着 HTTP 状态码。4.3 统一返回体 R联调不吵架的底座这个项目规模不大但接口返回体约定同样不能省。常见做法是定义 R 类Data public class RT { private Integer code; private String msg; private T data; public static T RT success(T data) { RT r new R(); r.setCode(200); r.setMsg(success); r.setData(data); return r; } public static T RT error(String msg) { RT r new R(); r.setCode(500); r.setMsg(msg); return r; } }统一返回体的价值在前后端分离项目里被严重低估。状态码、消息、数据三段式一旦定下来前端 axios 拦截器只需要做一件事判断 code 是否 200不是就走全局错误提示页面里完全不用再包一层 try/catch 去猜后端返回了什么形状。等到项目上了全局异常处理器 RestControllerAdvice所有校验失败和各种运行时异常都能统一变成 R 对象返回这才是联调阶段不吵架的技术底座。5. 避坑MyBatisPlus 五个高频翻车点与解决路径这段是从这个项目和 MyBatisPlus 3.4.1 社区高频提问里汇总出来的每一条都有明确的「现象 → 原因 → 解决」路径对着项目跑起来的先后顺序看命中率很高。5.1 现象分页查询返回了全量数据第 4.2 节写完 service 的 page() 调用后如果发现无论 page 传 1 还是 5返回的都是整张表的数据那不是 SQL 写错是分页拦截器压根没注册。MyBatisPlus 3.4.x 的分页能力不是开箱即用的必须显式配置 MybatisPlusInterceptor。原因MyBatisPlus 3.4.x 用拦截器链机制PaginationInnerInterceptor 是负责改写 SQL 的人它不存在Page 参数就会被无视查询退化成普通 select 全表。 解决给配置类注入拦截器Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }注意 DbType 要指定不指定会走自动识别遇到多数据源或者 URL 参数写法不规范时偶尔会识别出错。另外如果项目用了自定义 SQL 分页SQL 里不能自己写 LIMIT要让拦截器去拼否则分页会截断或重复。5.2 现象逻辑删除后重新插入相同学号失败表结构里已经说明了生产环境如果给 number 加了唯一索引那逻辑删除等于白删标记删除的记录仍然占着索引位置insert into 直接 Duplicate entry。原因逻辑删除只是 UPDATE is_deleted1数据行还在表里唯一索引依然生效。 解决要么按作者的方式不建唯一索引把学号查重放到业务层要么把 number 和 is_deleted 做成联合唯一索引——但这样同一条学号只能被逻辑删除一次第二次录入再删除就崩了。更实用的兜底方案是查询时按 number is_deleted0 先查一遍存在就提示「该学号已存在」再用 save 插入。用 LambdaQueryWrapper 几行就能写出来别依赖数据库去防重复。5.3 现象LocalDateTime 字段返回的时间带 Tspring.jackson.date-formatyyyy-MM-dd 配了但有个字段返回的还是 2023-11-04T12:00:00。原因jackson 的 date-format 全局配置只对 java.util.Date 生效LocalDateTime 走的是 JavaTimeModule 的默认序列化根本不看你这个配置。 解决LocalDateTime 字段上加 JsonFormat(pattern yyyy-MM-dd HH:mm:ss)如果要全局生效加一个 Jackson 配置类注册 JavaTimeModule 并自定义 LocalDateTime 的 serializer。这个项目里 Student 用的是 java.util.Date所以没踩到自己后续加字段用了 LocalDateTime马上就会遇到属于典型的「换个类型就翻车」。5.4 现象Swagger 页面打开直接报错或白屏Spring Boot 2.7 springfox 2.9.2 的组合启动时日志里看到 Failed to load remote configuration 或页面 404大概率是路径匹配策略不兼容。原因Spring Boot 2.6 开始默认 PathPatternParserspringfox 2.9.2 只认 AntPathMatcher路径匹配不上Swagger 资源加载不出来。application.properties 里那行 spring.mvc.pathmatch.matching-strategyant_path_matcher 就是正解。 解决把这行配置加进 application.properties 重启如果还不行检查 springfox 和 spring-boot-starter-web 的版本距离2.9.2 是 springfox 的终点版本后续维护基本停了Spring Boot 升到 3.x 之前不要动它。换 swagger 注解依赖时也要注意 io.swagger 和 io.swagger.core.v3 是两套包混用会直接看不到接口列表。5.5 现象saveBatch 批量插入一千条数据执行了十几秒有的项目做成绩批量导入用 MyBatisPlus 内置的 saveBatch数据一多就开始怀疑数据库性能实际是它走了单条 INSERT 循环。原因saveBatch 的批量效果依赖 JDBC 驱动的 rewriteBatchedStatements 参数默认关闭时它只是逐条执行 INSERT不会合并成多值语句。 解决在 JDBC URL 上加rewriteBatchedStatementstrue让驱动真正把批处理合并成一条INSERT INTO ... VALUES (...),(...),(...)。同时也检查一下 batchSizeMyBatisPlus 3.4.1 里默认 1000数据量大的场景可以调小到 500避免单条 SQL 过长。不要指望 saveBatch 一定快压测前看慢日志确认 SQL 是否真的合并了这条属于老生常谈但每次项目联调都有同事踩一遍。6. 交付前的验证习惯Swagger 点一遍接口才算真通项目跑通后我最常做的一件事是不开前端页面直接在 Swagger 里把所有接口过一遍。这比打开 Vue 页面点按钮快因为环境变量、前端代码报错、跨域代理问题全被隔离在外接口通不通一眼就能看出来。6.1 用 Swagger 快速验证 5 个接口启动后端后访问 http://localhost:8089/swagger-ui.html应该能看到 5 个核心操作组。按顺序测登录接口 POST /user/login用户名 admin、密码 1234拿返回值里的 token学生新增 POST /student/addbody 里带 number、realName、enrollTime学生分页 GET /student/list成绩录入 POST /score/add最后是带 token 调一次受保护接口验证拦截器。如果第二步就能过说明逻辑删除和表结构没问题到带 token 这步才 401那就是前端 header 没带对不是后端问题。6.2 用代码生成器把重复劳动交给机器pom.xml 里已经带了 mybatis-plus-generator 3.4.1 和 freemarker这套组合是可以直接跑代码生成器的。常见做法是写一个独立的生成器 main 方法配置好数据库连接和作者名后一键生成 pojo、mapper、service、controller。生成的实体类和 XML 基本和作者贴的模板长得一样。代码生成器我会改两个习惯全局配置里关闭 swagger 注解的自动生成springfox 版本太老自动生成的注解容易和现有依赖冲突mapper XML 的输出路径和实体类分开配不然目录会乱。生成完看一眼 SQL 日志确认逻辑删除字段是否注入再手动调一下 Controller。6.3 部署前要亲手改掉的三个默认值资源里所有配置都是开发环境预设交付或上线前务必过手一遍数据库密码 mysql123 要换掉auth.ignoreUrls 白名单里的 /student/add 必须移除mybatis-plus 的 SQL 日志输出 stdout 要关掉不然生产日志会被刷爆。从那以后我每次接手类似项目都会先检查这三个点再跑起来省得交付后被人追着问「为什么生产环境还能直接加学生」。希望帮到你。本文还有配套的精品资源点击获取