SpringBoot+Vue+MyBatis+MySQL招生宣传管理系统实战开发
2026/9/9 19:38:41 网站建设 项目流程

这段时间我在做一个高校招生宣传管理系统,技术栈就是标题里那套:SpringBoot + Vue + MyBatis + MySQL。做之前我以为这只是个普通的CRUD项目,真正动手才发现,招生宣传这个业务场景远比想象中复杂——它既要管内容发布,又要管生源数据,还得兼顾前后端分离下的权限控制。这套系统做完之后,我把整个设计思路、表结构、踩坑记录都整理了一遍,今天一次性分享出来。适合正在做毕业设计、课设,或者刚入门Java全栈想找个完整项目练手的朋友参考。

整个系统我的定位是“内容管理 + 线索管理 + 数据统计”三合一。前端用Vue做单页应用,后端SpringBoot提供接口,MyBatis负责数据库操作,MySQL存数据。下面我按项目开发的实际顺序,把每个环节的思考和技术细节拆开讲。

1. 项目整体设计与技术选型思路

1.1 招生宣传系统到底要管哪些东西

刚开始搭项目的时候,我先把业务场景理清楚了。所谓的招生宣传管理系统,本质上解决的是三个问题:第一,招生信息怎么高效发出去;第二,意向生源怎么收集和跟进;第三,宣传效果怎么量化评估。

围绕这三个问题,系统分成了几个核心模块。信息发布模块负责招生简章、专业介绍、校园新闻、常见问答这些内容的维护,后台编辑发布,前台考生和家长能看到。报名咨询模块给访客提供在线报名和留言咨询的入口,提交后数据进入管理后台。生源管理模块是招生老师的日常工作台,可以查看、筛选、跟进意向学生,记录电话沟通情况。统计报表模块按专业、地区、时间维度统计报名数据,给招生策略做参考。最后还有系统管理模块,管理用户账号、角色权限和操作日志。

我的建议是不要一上来就埋头写代码,先把这些模块在纸上画出来,确定好角色边界。这个系统的用户角色我设计了三种:超级管理员、招生老师、访客。访客只在前台留资、看内容,招生老师管生源和自己的跟进记录,超级管理员拥有全部权限。这个权限模型贯穿了后续所有接口设计,早定早省事。

1.2 为什么选择SpringBoot + Vue + MyBatis + MySQL这个组合

这套技术栈可以说是国内中小型管理系统最主流的搭配了,选它不是因为追新,而是综合考虑了开发效率、学习资料、部署成本和招聘市场需求。

后端用SpringBoot,理由很直接:它把Spring繁琐的XML配置全部干掉,内嵌Tomcat,一个jar包就能跑起来,非常适合快速交付业务系统。自动配置机制让你只需要关注自己的业务代码,而不是花大量时间在环境搭建上。

MyBatis和Spring Boot的配合很紧密。相比JPA那种全自动ORM,MyBatis的半自动特性在复杂查询场景下特别灵活。招生管理里有大量多表关联、条件动态查询的诉求,比如按地区、按专业、按分数段筛选生源,用MyBatis的动态SQL写起来非常直观。另外,MyBatis对SQL优化的把控力更强,系统上线后如果发现慢查询,直接改XML里的SQL就行,不需要动Java代码。

前端选Vue,是因为它上手曲线平缓,组件化开发思路对中小型项目特别友好。管理员后台和前台展示页可以拆成两个Vue应用,也可以用一套代码通过路由区分。我这里用的是后者,一套代码里通过路由和权限控制区分管理端和前台。

MySQL就不用多说了,开源免费、性能稳定、运维资料多。招生宣传系统的数据量级远没有到需要上Oracle或者PostgreSQL的程度,MySQL 8.0的窗口函数、JSON类型等功能也足够应对统计需求。存储引擎选InnoDB,支持事务,线上数据安全有保障。

整套组合下来,无论你以后是继续往Java后端深入,还是转前端,这套经验都能复用。

2. 核心功能拆解与数据库设计

2.1 数据库表结构设计实战

数据库设计是整个系统的地基,这块我花的时间最多。招生宣传管理系统我一共设计了九张核心表。

用户表存管理员和招生老师的账号信息,包含用户名、密码(BCrypt加密存储)、真实姓名、手机号、角色ID等字段。角色表存角色名称和角色标识。菜单权限表管理前端路由和后端接口权限。

招生资讯表是内容发布的核心,字段有标题、摘要、正文、封面图URL、资讯类型(简章/新闻/问答)、发布状态、发布时间、浏览量。报名信息表记录前台提交的报名数据,包含考生姓名、性别、电话、意向专业、预估分数、生源地区、备注、创建时间。咨询留言表记录访客的留言内容和回复状态。

生源跟进表是招生老师的日常工作记录,关联报名信息ID,记录跟进方式、沟通内容、下次跟进时间、跟进状态。专业表维护学校开设的专业信息,包括专业名称、所属院系、招生人数、学制、学费。统计报表这块我直接用的SQL动态统计,没有单独建报表表。操作日志表记录管理系统内的关键操作。

建表时有个很重要的细节:统一使用utf8mb4字符集。因为前台报名和留言可能包含表情符号,utf8mb4才能完整存储,用utf8会出现乱码。我在初期就遇到过一次这种问题,后面会详细讲。

2.2 自动建表:SpringBoot + MyBatis项目启动时自动初始化表结构

这里说一下很多新手会问的问题:SpringBoot项目在MyBatis环境下,怎么实现表不存在时自动建表?我用的是秒杀项目或CMS项目里比较常见的方案:利用SpringBoot的事件监听机制,在ApplicationRunner里检查表是否存在,不存在就执行建表SQL。

思路是这样的:项目启动成功后,触发一个自定义的Runner,它先查询information_schema数据库里是否存在目标表,如果返回结果为空,就读取classpath下的schema.sql建表脚本并执行。核心代码大概长这样:

@Component public class TableInitRunner implements ApplicationRunner { @Autowired private JdbcTemplate jdbcTemplate; @Override public void run(ApplicationArguments args) { String checkSql = "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 't_student_apply'"; Integer count = jdbcTemplate.queryForObject(checkSql, Integer.class); if (count == null || count == 0) { Resource resource = new ClassPathResource("sql/schema.sql"); // 读取SQL脚本并按分号拆分执行 } } }

这个方案的关键点是检测和初始化要放在同一个事务里,否则建表执行一半失败了会产生脏数据。另外拆分SQL语句时要注意,存储过程、函数这类脚本里可能包含分号,简单按分号切割会出问题。咱们这个项目都是普通建表语句,按行读取、过滤注释和空行就可以。

如果你用的是Spring Boot 2.5以上的版本,在application.yml里配置spring.sql.init.mode=always也能实现类似效果,但那个方案对脚本格式要求严格,而且表已存在时启动会报错,不如自定义Runner灵活。还是推荐用Runner这种方式,写一次能一直用。

2.3 数据库设计时的几个关键原则

设计完表之后,有几个地方我特别想强调一下。

第一个是时间字段的处理。创建时间、更新时间这类字段,我统一用DATETIME类型,并且在应用层通过MyBatis的自动填充功能写入,而不是依赖数据库的CURRENT_TIMESTAMP。原因是应用层控制时间可以保证所有环境的时间逻辑一致,后续如果做分库分表或者迁移,也不会因为数据库时区设置不同而出问题。

第二个是逻辑删除和物理删除的取舍。报名信息、跟进记录属于核心业务数据,我用的逻辑删除,也就是加一个deleted字段,查询时统一过滤。资讯类内容也是逻辑删除。只有日志表的数据可以物理删除,因为日志量增长快,需要定期清理。

第三个是索引设计。报名信息表的phone字段、intention_major字段、create_time字段都要建索引。招生资讯表的typepublish_status字段建联合索引。这些索引在数据量到达十万级别之后效果非常明显。我实测过,没有索引的模糊查询耗时800多毫秒,加上索引后降到几十毫秒,差别巨大。

3. 后端实现:SpringBoot + MyBatis操作MySQL的关键细节

3.1 项目骨架搭建与依赖配置

创建SpringBoot项目的时候,我建议直接去Spring Initializr生成,勾选Spring Web、MyBatis Framework、MySQL Driver这几个依赖。生成后pom文件里的核心依赖大概是这样的:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.2</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency>

这里要特别提醒:MySQL 8.0的驱动类名和5.x不一样,8.0用的是com.mysql.cj.jdbc.Driver,5.x用的是com.mysql.jdbc.Driver。如果你用的MySQL 8.0但还复制老项目的驱动配置,启动时会直接报ClassNotFoundException。application.yml里数据源这样配置:

spring: datasource: url: jdbc:mysql://localhost:3306/enroll_system?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.enroll.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

注意MyBatis配置里map-underscore-to-camel-case这个选项,它能把数据库字段的下划线命名自动映射为Java实体的驼峰命名。比如数据库里apply_status字段会自动映射到实体的applyStatus属性,省掉一大堆ResultMap手工映射。log-impl配置成StdOutImpl后,控制台会直接打印SQL语句和参数,排查问题非常方便,这就是大家常说的MyBatis打印SQL配置。

3.2 MyBatis动态SQL的实战使用

招生管理系统的查询条件多变,这是MyBatis最能发挥价值的地方。比如生源列表页,招生老师可能组合筛选地区、意向专业、分数区间、报名时间段,用动态SQL来实现是最舒服的方式。

举个例子,查询报名信息列表,过滤条件不固定,XML里的核心SQL是这样写的:

<select id="selectApplyList" resultType="com.enroll.entity.StudentApply"> SELECT * FROM t_student_apply <where> <if test="name != null and name != ''"> AND name LIKE CONCAT('%', #{name}, '%') </if> <if test="intentionMajor != null and intentionMajor != ''"> AND intention_major = #{intentionMajor} </if> <if test="minScore != null"> AND estimated_score &gt;= #{minScore} </if> <if test="region != null and region != ''"> AND region = #{region} </if> <if test="startTime != null and startTime != ''"> AND create_time &gt;= #{startTime} </if> </where> ORDER BY create_time DESC </select>

注意XML里的小于号<必须转义成&lt;,否则XML解析直接报错。大于号最好也转义,保持一致性。这个坑我见过不少新手踩,特地说一下。

<where>标签会自动处理首行AND的问题——如果第一个条件不成立,第二个条件成立,<where>会智能去掉SQL语句前面的AND,避免语法错误。这套动态SQL写完,多条件组合查询就不用再改SQL了,只用封装查询对象传给Mapper接口就行。

3.3 关于MyBatis缓存,我说点实际的

热搜词里看到有人搜MyBatis缓存,这块确实值得说道。MyBatis的一级缓存是SqlSession级别的,默认开启,同一个SqlSession内执行两次相同的查询,第二次直接走缓存。但SpringBoot集成MyBatis后,每次Mapper操作默认都会创建一个新的SqlSession,用完就关闭,所以一级缓存基本起不到跨请求的作用,不用对它抱太多期望。

二级缓存是Mapper级别的,需要在XML里配置<cache/>标签开启,多个SqlSession可以共享。但我要提醒一句:在真正的业务系统里,二级缓存反而容易出问题。因为只要表数据一更新,所有关联这个Mapper的缓存都要手动清理,如果涉及多表联查,缓存的数据可能变得不可控。我在这个项目里干脆没有开启二级缓存,因为招生数据的实时性要求很高,报名进来了立刻就要能被查到,宁可多查一次数据库也不能看到旧数据。系统的QPS不高,数据库压力完全扛得住。

3.4 登录鉴权与接口安全实现

这个系统有登录功能,用户管理和数据查看必须做权限控制,不能让人随便访问后台接口。我采用的是JWT + 拦截器的经典组合,没有引入Spring Security这么重的东西,因为系统的角色模型很简单,面向的也是内部使用场景。

用户登录成功后,后端用JWT生成一个带用户ID和角色标识的token返回给前端。前端每次请求接口时在请求头里带上Authorization: Bearer token。后端写一个拦截器,在请求进入Controller之前校验token是否合法、是否过期,然后从token里取出用户信息放进ThreadLocal,Controller里就能直接拿到当前登录人。

拦截器里做权限控制时,我维护了一个@RequireRole注解,标注在Controller方法上,拦截器校验角色是否匹配。比如生源跟进接口只允许招生老师和管理员访问,就在方法上标注@RequireRole({"ADMIN", "TEACHER"})。这样即使有人绕过前端直接调接口,没有对应角色也进不来。

密码存储这块必须用BCrypt加密,不要用MD5。MD5已经能被彩虹表轻易碰撞出来,BCrypt加盐哈希要安全得多。Spring Security里自带BCryptPasswordEncoder,即使不引入整个Spring Security,单纯引入spring-security-crypto包也能直接用这个类。

4. 前端Vue构建投放页与管理后台

4.1 Vue环境配置与项目脚手架

Vue部分我用的是Vue 3 + Vite的组合。相比Vue 2的Webpack,Vite的冷启动速度和热更新体验好太多了,开发期间保存代码几乎是秒级刷新。前端环境的配置建议按这个顺序来:先安装Node.js 16以上版本,然后npm配置国内镜像,再安装Vite脚手架创建项目。

# 检查node版本 node -v # 配置淘宝镜像(国内开发必备,速度差好几倍) npm config set registry https://registry.npmmirror.com # 创建Vue3项目 npm create vite@latest enroll-front -- --template vue # 安装依赖 cd enroll-front npm install npm install vue-router@4 axios pinia element-plus

Element Plus是Vue 3对应的UI组件库,后台管理页面包括表格、表单、弹窗、分页这些组件它都有现成的,直接引入就能用。前台展示页我用的普通CSS加Flex布局,没有引入多余的UI框架,避免包体过大。

4.2 Vue路由配置与路由参数传递实战

前端路由是Vue项目的骨架,这个系统我采用了嵌套路由的结构。管理后台整体是一个布局组件,里面再嵌子路由,比如/admin下面是/admin/applyList/admin/enrollNews/admin/studentFollow这些。前台页面则是独立的/home/newsDetail/apply路由。

路由参数的传递这里说细一点,因为在列表页跳转到详情页这个场景太常见了。比如招生资讯列表页点一条资讯,要跳转到详情页,最方便的方式就是通过路径参数传ID,路由配置这样写:

{ path: '/newsDetail/:id', name: 'NewsDetail', component: NewsDetail }

跳转的时候用编程式导航:

router.push({ name: 'NewsDetail', params: { id: row.id } })

在详情页读取参数:

import { useRoute } from 'vue-router' const route = useRoute() const newsId = route.params.id

这里有个坑:用params方式传参时,如果直接在地址栏刷新页面,参数不会丢,因为参数在URL路径里。但如果用query方式传参,比如?id=123,刷新也不会丢,不过URL会带问号参数。两种方式我建议:重要的、需要被收藏或分享的页面用params路径参数,临时筛选条件用query参数。另外还要注意,当从详情页返回列表页时,列表页的滚动位置和筛选条件可能会丢失,这个场景我用keep-alive缓存了列表组件,数据量的系统内存占用可以接受。

4.3 axios封装与前后端联调

前端和后台交互,我用axios做统一的请求封装。封装时主要解决三件事:基础URL统一配置、token自动携带、统一错误处理。

// request.js import axios from 'axios' import { ElMessage } from 'element-plus' import router from '../router' const request = axios.create({ baseURL: '/api', timeout: 10000 }) // 请求拦截器:自动带token request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) // 响应拦截器:统一处理错误码 request.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) } return res.data }, error => { if (error.response && error.response.status === 401) { ElMessage.error('登录已过期,请重新登录') router.push('/login') } else { ElMessage.error('网络异常,请稍后重试') } return Promise.reject(error) } ) export default request

前后端联调时最常见的报错就是跨域。跨域的本质是浏览器同源策略的限制——前端跑在5173端口,后端跑在8080端口,端口不同就算跨域。解决方案有两种,开发环境用Vite代理,生产环境用Nginx反向代理,千万不要在后端直接开放跨域,那会有安全问题。Vite的代理配置在vite.config.js里:

export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })

这样前端请求/api/xxx时,Vite开发服务器会把请求转发到http://localhost:8080/api/xxx,浏览器看到的只是同源请求,跨域问题就消失了。

5. 部署上线与常见问题排查实录

5.1 本地开发环境搭建傻瓜式指南

开发这个项目时,我花了不少时间在环境搭建上,尤其是MySQL。MySQL 8.0的安装和5.x差别挺大,我整理了一套比较省心的流程。

先装MySQL 8.0。Windows直接去MySQL官网下载MSI安装包,安装过程中会要求设置root密码,选择Use Legacy Authentication还是Use Strong Password Encryption时,我建议选第一个。因为很多老项目的连接驱动和工具不支持新的caching_sha2_password认证插件,选了后面会连接报错。如果你安装时已经选了第二个,可以在命令行里执行ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码';改回旧认证方式。

Java环境装JDK 8或者JDK 17都可以。Spring Boot 2.7和3.x对JDK版本要求不同,你如果创建的是Spring Boot 2.x项目,用JDK 8就行;如果用了Spring Boot 3.x,必须JDK 17以上。热词里那个“springboot版本太高”的问题,多半就是JDK版本没跟上。前端Node.js装最新的LTS版本,用nvm管理多个Node版本最方便。

5.2 项目打包部署的两种方式

开发完成后要上线,我习惯用前后端分离的部署方式。

后端打包:

mvn clean package -DskipTests

打包后会生成target/enroll-system.jar,用java -jar enroll-system.jar就能跑起来。如果要后台运行,Linux服务器上可以用systemd创建一个service文件,或者用nohup java -jar enroll-system.jar > app.log 2>&1 &这种方式。

前端构建:

npm run build

构建产物在dist目录。用Nginx托管dist目录,同时配置反向代理把/api请求转发到SpringBoot应用:

server { listen 80; server_name yourdomain.com; root /var/www/enroll/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }

注意location /里的try_files配置,这是Vue Router的history模式必须的,否则刷新子路由页面时会404。

5.3 常见问题速查表

开发过程中我记录了一堆问题和对应的解决办法,整理成了一张速查表,遇到问题直接对号入座。

问题现象可能原因解决方案
启动报ClassNotFoundException: com.mysql.jdbc.Driver驱动类名错误改成com.mysql.cj.jdbc.Driver
连接数据库报Access denied for user密码错误或认证插件不兼容检查密码,改MySQL认证插件为mysql_native_password
控制台不打印SQL没有配置MyBatis日志在application.yml配置mybatis.configuration.log-impl
前端请求接口报跨域后端没代理或没配置代理开发环境用Vite代理,生产用Nginx反代
刷新页面404Nginx没配置try_files加上try_files $uri $uri/ /index.html
中文乱码数据库字符集不是utf8mb4建库时指定CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
日期字段差8小时时区配置问题连接URL加serverTimezone=Asia/Shanghai
前端页面白屏JS报错或构建失败打开浏览器控制台看报错,先本地跑dev模式排错
上传图片后无法访问静态资源路径映射问题SpringBoot配置WebMvcConfigurer映射本地磁盘路径到URL

5.4 几个不会写在文档里的经验

最后分享几个我调试过程中得来的经验。

第一,MyBatis的Mapper接口和XML文件必须在同一个包路径下,否则扫描不到。SpringBoot启动类上如果加了@MapperScan,记得确认扫描路径覆盖到了所有Mapper接口。XML文件放在resources/mapper目录,配置里mapper-locations要对应上。

第二,分页查询不要自己写LIMIT,直接用PageHelper,但要注意PageHelper的线程安全问题。PageHelper通过ThreadLocal传递分页参数,使用后必须消费掉,否则线程池复用线程时会出现分页串数据。我的做法是在service层调用PageHelper.startPage后立刻执行查询,中间不要穿插其他数据库操作。

第三,大数据量导出Excel时,不要一次性查全量到内存。招生数据几万条的时候,POI导出很容易内存溢出。我的方案是分批查询,每次查5000条,写完一批再查下一批,这样内存占用一直是平稳的。

第四,日志一定要打到位。我在每个Controller的入口和出口都打了请求参数和响应结果日志,生产环境排查问题的时候全靠这些日志定位。别嫌麻烦,上线后你就会感谢当初的自己。

这个系统从设计到落地,前前后后大概两周多时间。做完之后我对SpringBoot + Vue这套组合的理解比之前看书深入太多了。整个流程走下来,从数据库设计、后端接口开发、前端页面联调,再到打包部署,每个环节都踩过坑也填了坑。如果你也想拿类似项目练手,我的建议是别光盯着源码看,自己动手从建表开始把整个流程走一遍,遇到问题比看十遍代码都管用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询