ARTICLE DETAIL

建站实战干货

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

SpringBoot+Vue招生管理系统开发实战:从设计到部署

2026/9/9 19:39:47 拓冰建站 浏览量
SpringBoot+Vue招生管理系统开发实战:从设计到部署 招生旺季又要来了各大高校、职业院校的招生办每年这个时候都在同一件事上头疼咨询电话一个接一个微信群里的宣传资料转来转去考生报名信息还靠Excel来回传汇总起来既费劲又容易出错。我去年帮一所职业院校搭过一套基于SpringBoot Vue的招生宣传管理系统核心就干三件事把招生信息统一发布、把咨询和报名流程管起来、把各专业的宣传数据统计清楚。前后端分离SpringBoot做接口服务Vue做管理后台MyBatis操作MySQL数据库一套下来招生办的工作效率提升非常明显。这篇文章我把整个项目的思路、数据库设计、后端实现、前端页面还有部署上线时容易踩的坑全部拆开讲一遍。适合正在做毕业设计的学生、想系统学习SpringBoot Vue全栈开发的初级工程师以及学校信息中心想快速搭一套招生系统的技术老师。整套项目的源码结构和关键代码我都会贴出来可以直接照着改不需要从零开始。1. 项目整体设计与技术选型思路1.1 为什么是SpringBoot Vue而不是其他方案招生宣传管理系统本质上是一个典型的信息管理类Web应用核心是数据的增删改查加上文件上传下载。这类项目最大的特点是什么业务逻辑不复杂但对开发效率、维护成本、二次开发友好度要求很高。招生办的老师每年都会提新的需求比如今年要加一个“直播回放”栏目明年要加一个“各专业热度排行”如果技术栈选得太重改一次需求要折腾一周那这系统基本就废了。我选择SpringBoot Vue这套组合核心原因有三个第一SpringBoot让后端开发变得极其轻量。不需要像传统SSM项目那样写一堆XML配置一个Application类启动整个服务内嵌Tomcat打jar包扔服务器上就能跑。对于招生系统这种业务密集型的项目SpringBoot的自动配置机制能省掉大量重复工作。第二Vue Element Plus做后台管理界面效率极高。招生系统基本都是表单、表格、弹窗、报表这类页面Element Plus组件库直接把这些东西封装好了你今天要加一个专业管理页面半小时就能搞定。第三前后端分离的架构让后续扩展空间更大。现在很多学校要对接微信公众号、小程序前后端分离意味着后端接口可以直接被小程序复用不用重新开发一套接口。1.2 技术栈版本选择与兼容性说明版本选择这块是很多人容易忽略的坑。一套代码在本地跑得好好的换台电脑报各种错多半是版本问题。我用的这套组合经过实际项目验证兼容性最稳JDK 17 Spring Boot 2.7.18。为什么不用Spring Boot 3.x因为3.x要求JDK 17起步MyBatis相关的很多第三方插件兼容性还没完全跟上虽然现在好多了但对于一个要快速上线、稳定运行的项目2.7.x是最成熟的选择。JDK 17本身可以稳定跑Spring Boot 2.7.x我用这个组合已经上过两个生产项目了没有遇到任何问题。Vue 3.2 Vite 4 Element Plus 2.x。Vue 3的Composition API配合Vite的开发调试体验比Vue 2 Webpack舒服太多热更新秒级响应。Element Plus是Element UI的Vue 3版本表格、表单、分页这些组件颜值和功能都在线。MyBatis 2.3.x MySQL 8.0。MyBatis的SQL控制力是JPA没法比的招生系统的统计报表需要写相对复杂的多表关联查询用MyBatis可以精确控制每一条SQL。MySQL 8.0的窗口函数在做数据统计时非常有用比如各专业近三年的报名人数趋势。有的同学问为什么不直接用MyBatis-Plus我的回答是可以但我建议先老老实实把原生MyBatis搞明白。MyBatis-Plus确实能省很多事自动CRUD、分页插件、代码生成器但如果你连resultMap映射、动态SQL这些原生机制都不了解出了问题根本不知道去哪里排查。这个项目我教学校老师的时候是让他们用原生MyBatis的学完再上MyBatis-Plus就是降维打击。2. 核心功能模块与数据库表结构设计2.1 招生业务模块拆解在写代码之前最需要花时间的是把业务模块理清楚。招生办的实际工作流程大概是这样的各院系上报招生计划招办审核后统一发布到官网和公众号考生和家长通过浏览招生信息、在线咨询、提交报名意向等渠道与学校互动招办老师需要实时掌握每个专业的咨询量、报名量、意向转化率等数据据此调整宣传策略。根据这个流程我把系统拆成五个核心模块系统管理管理员账号、角色权限、操作日志。招生系统虽然业务上不算特别敏感但报名数据涉及考生姓名、电话、身份证号权限控制必须做。招生信息发布通知公告、招生简章、专业介绍、宣传视频和图片的管理。这是系统的“内容中心”。在线咨询与留言考生和家长提交咨询问题招办老师后台回复。相当于一个轻量工单系统。考生报名管理考生在线填写报名意向招办老师审核、导出、批量处理。数据统计报表按专业、按地区、按时间维度统计咨询量和报名量用图表展示支持导出Excel。2.2 数据库表设计详解数据库设计是整个系统的地基。地基打不好后面写SQL的时候全是泪。我把核心表的结构贴出来每张表都标注了关键字段的设计理由。专业表t_major——承载各院系的招生专业信息CREATE TABLE t_major ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键, major_name varchar(100) NOT NULL COMMENT 专业名称, college_name varchar(100) DEFAULT NULL COMMENT 所属院系, education_type varchar(20) DEFAULT NULL COMMENT 学历层次专科/本科, study_years int(11) DEFAULT NULL COMMENT 学制, tuition_fee decimal(10,2) DEFAULT NULL COMMENT 学费/年, major_intro text COMMENT 专业介绍, recruit_plan int(11) DEFAULT 0 COMMENT 招生计划人数, enrolled_count int(11) DEFAULT 0 COMMENT 已报名人数, status tinyint(4) DEFAULT 1 COMMENT 状态1启用 0停用, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT招生专业表;这个表有两个字段很容易被忽略一个是status用来控制专业是否在前台展示招生名额招满之后直接改状态下线比删除数据安全得多。另一个是enrolled_count这个字段是冗余的本来可以通过报名表count出来但招生季高频查询时每次都去count是性能灾难直接在专业表里维护一个计数报名成功就1减少一次多表统计。考生报名表t_enrollment——核心业务表字段设计遵循“快照”原则CREATE TABLE t_enrollment ( id bigint(20) NOT NULL AUTO_INCREMENT, student_name varchar(50) NOT NULL COMMENT 考生姓名, id_card varchar(18) DEFAULT NULL COMMENT 身份证号, phone varchar(20) NOT NULL COMMENT 联系电话, province varchar(50) DEFAULT NULL COMMENT 生源地省份, graduate_school varchar(100) DEFAULT NULL COMMENT 毕业学校, score decimal(5,2) DEFAULT NULL COMMENT 高考分数, major_id bigint(20) DEFAULT NULL COMMENT 意向专业ID关联t_major, major_name varchar(100) DEFAULT NULL COMMENT 意向专业名称冗余, status tinyint(4) DEFAULT 0 COMMENT 审核状态0待审核 1通过 2拒绝, remark varchar(500) DEFAULT NULL COMMENT 招生办备注, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_major_id (major_id), KEY idx_phone (phone) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT考生报名表;major_name这个冗余字段是我特别加上的。为什么不直接关联t_major查因为专业名称可能会调整但报名记录是历史事实考生当年报的就是那个专业名字必须冻结下来。这是典型的“数据快照”思想业务数据表里冗余存储名称字段避免后续字典表变更影响历史数据。咨询留言表t_consultation和公告信息表t_notice相对简单咨询表主要包含咨询人姓名、联系方式、咨询内容、回复内容、状态字段公告表就是标题、内容、发布时间、是否置顶、点击量。还需要一张用户表t_user管理招办和院系老师的登录账号密码用BCrypt加密存储有role字段区分角色。还有一个容易忽略的点所有表的主键我都用bigint自增除非有明确的分布式需求否则不要用UUID这种随机字符串做主键因为MySQL的InnoDB引擎主键是聚簇索引UUID会频繁导致页分裂高并发下性能下降得厉害。3. 后端代码实现与MyBatis实战3.1 SpringBoot项目基础搭建项目结构按功能分包不是按技术分包。很多初学者喜欢建controller、service、mapper这样的包名结果每个包下挤了几十个类找起来非常痛苦。我习惯按业务模块分包com.recruit ├── config # 配置类CORS、拦截器、文件上传配置 ├── controller # 接口层 ├── service # 业务逻辑层 │ └── impl # 业务实现 ├── mapper # MyBatis数据访问层 ├── entity # 实体类 ├── dto # 接收前端参数的DTO对象 ├── vo # 返回前端的视图对象 ├── common # 统一返回结果、异常处理 └── utils # 工具类JWT工具、Excel导出工具这里要特别提一下DTO和VO这两个概念很多同学搞混。DTO是接收前端传过来的参数比如分页查询的页码、每页条数、筛选条件你在DTO里加参数校验注解NotNull、Size这类保证非法数据不进入业务层。VO是返回给前端的数据结构比如专业列表要附带“报名人数/计划名额”的百分比直接用MajorVO封装不在entity里塞一堆无关字段。把请求参数和响应数据分开接口的扩展性会好很多。pom.xml里关键的依赖只有这几个spring-boot-starter-web、mybatis-spring-boot-starter、mysql-connector-j、lombok、jwt相关依赖、poi用于Excel导出。不要盲目加依赖每加一个就多一份维护负担。3.2 MyBatis关键SQL与动态标签使用招生系统最复杂的SQL集中在统计报表模块。以“各专业咨询量统计”为例需求是按专业分组统计近30天的咨询量并按量排序。如果用JPA写这种SQL要么写JPQL要么用原生SQL侵入性很强。MyBatis就直接清爽得多。我先说一个很多初学者在MyBatis注解和XML之间的抉择问题。我这个项目里的简单CRUD用注解Select、Insert直接写在Mapper接口上一行搞定。但稍微复杂的统计查询、动态条件查询全部放到XML里。原因很简单调试SQL方便而且多表关联的动态SQLXML里的sql标签可以抽公共片段复用注解做不到。举一个动态查询的例子报名记录列表要支持按姓名、专业、审核状态、时间段筛选select idselectEnrollmentPage resultTypecom.recruit.vo.EnrollmentVO SELECT e.*, m.major_name FROM t_enrollment e LEFT JOIN t_major m ON e.major_id m.id where if teststudentName ! null and studentName ! AND e.student_name LIKE CONCAT(%, #{studentName}, %) /if if testmajorId ! null AND e.major_id #{majorId} /if if teststatus ! null AND e.status #{status} /if if teststartTime ! null AND e.create_time gt; #{startTime} /if if testendTime ! null AND e.create_time lt; #{endTime} /if /where ORDER BY e.create_time DESC /selectwhere标签和if标签是MyBatis动态SQL里最常用的两个where会自动处理第一个条件前面的AND避免拼接出WHERE AND e.name xxx这种语法错误。注意时间比较要用gt;和lt;XML特殊字符需要转义这个坑我见人踩过无数次。还有MyBatis缓存的问题。这个项目里我直接把二级缓存关掉了而且建议你也不要开。为什么因为MySQL本身有自己的查询缓存和索引机制MyBatis的二级缓存默认是单机本地缓存在集群环境下会出现数据不一致的严重问题。招生季的系统访问量其实是有限的数据库查询优化到正确走索引性能完全够用。缓存的正确使用场景是热点数据比如首页公告列表可以用Redis或者Caffeine做应用层缓存而不是依赖MyBatis的二级缓存。3.3 登录鉴权与JWT方案招生系统虽然不像银行系统那么严格但考生报名数据涉及个人隐私接口不能裸奔。我用的方案是JWT 拦截器。登录成功之后后端签发一个JWT令牌有效时长设置为12小时包含用户ID、用户名、角色三个关键信息用HMAC256算法加上密钥签名。前端每次请求在Header里带上Authorization: Bearer token后端写一个拦截器统一校验。Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口 if (request.getRequestURI().contains(/auth/login)) { return true; } String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { token token.substring(7); try { Claims claims JwtUtil.parseToken(token); request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); return true; } catch (Exception e) { // token无效或过期 } } response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\msg\:\登录状态已失效\}); return false; } }这里有一个设计细节值得注意拦截器只做鉴权不做权限控制。鉴权是确认“你是谁”权限控制是确认“你能否做这件事”。比如/api/enrollment/export导出Excel接口只有管理员能调院系老师不行。这种情况我建议在Controller层或者Service层用自定义注解RequireRole(admin)做判断而不是在拦截器里硬编码URL因为URL和角色很容易变化硬编码后期就是噩梦。权限控制还有一个简单原则按钮级别的权限不需要每个按钮都做。很多系统把权限粒度细化到页面上每个按钮能不能点这在招生系统里完全没有必要做到“菜单权限”和“操作权限”两个层级就够了。菜单权限决定你能看到哪些页面操作权限决定你能不能用增删改查和导出这些核心功能再多就是过度设计。3.4 文件上传与图片访问招生简章和专业介绍肯定要放图片宣传视频要上传这就涉及文件存储的问题。生产环境我推荐两种方案一是服务器本地磁盘存储简单直接二是对象存储便宜大碗。如果学校有云服务器用阿里云OSS或者腾讯云COS一年几百G的存储成本基本可以忽略。本地存储的方案更通用适合在校内服务器部署的场景。本地存储的核心配置是上传的文件保存到服务器磁盘上的某个目录然后通过虚拟路径映射把这些静态资源暴露给前端访问。SpringBoot里面加一个配置类Configuration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { String uploadPath System.getProperty(user.dir) /upload/; registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadPath); } }这样用户上传的头像、专业图片、宣传视频保存到项目的upload目录前端直接通过/upload/xxx.jpg就能访问。文件上传有个非常容易被忽略的细节修改spring.servlet.multipart.max-file-size这个配置。SpringBoot默认的上传文件大小限制是1MB招生宣传视频动辄几十MB不改这个配置上传必报错。另外还要注意如果部署环境用了Nginx反向代理client_max_body_size也要相应调大这个坑我帮人排查过整整一个下午。3.5 Excel导出与大数据量处理招生办老师最离不开的功能就是“导出Excel”。每天要统计当天新增了多少报名、各专业分布如何不能每次都在网页上数。我用Apache POI做Excel导出核心思路是先查出数据再生成Workbook最后写到响应流。不废话贴核心代码public void exportEnrollment(HttpServletResponse response, String status) throws IOException { ListEnrollmentVO list enrollmentService.selectAllForExport(status); // 创建Excel工作簿 XSSFWorkbook workbook new XSSFWorkbook(); Sheet sheet workbook.createSheet(报名信息); // 创建表头 String[] headers {序号, 考生姓名, 联系电话, 意向专业, 生源地, 报名时间, 审核状态}; Row headerRow sheet.createRow(0); for (int i 0; i headers.length; i) { headerRow.createCell(i).setCellValue(headers[i]); } // 填充数据 for (int i 0; i list.size(); i) { Row row sheet.createRow(i 1); EnrollmentVO vo list.get(i); row.createCell(0).setCellValue(i 1); row.createCell(1).setCellValue(vo.getStudentName()); row.createCell(2).setCellValue(vo.getPhone()); row.createCell(3).setCellValue(vo.getMajorName()); row.createCell(4).setCellValue(vo.getProvince()); row.createCell(5).setCellValue(vo.getCreateTime().toString()); row.createCell(6).setCellValue(待审核.equals(vo.getStatus()) ? 待审核 : 已通过); } // 设置响应头触发浏览器下载 response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setHeader(Content-Disposition, attachment; filenameenrollment.xlsx); workbook.write(response.getOutputStream()); workbook.close(); }这段代码有个问题需要处理如果数据量很大比如导出两万条报名记录直接全量加载到内存里生成Excel内存会不会爆会而且大概率会。我的处理思路是分页流式查询每查5000条就往Workbook里写一批用完就清掉引用20000条数据内存占用也在可控范围内。另外导出的时候要在Service层加一个时间限制比如限制只能导出最近一年的数据防止有人一次性把自己这个年份之前的所有历史数据全导出去。4. 前端Vue3页面实现与核心交互4.1 Vue3项目创建与目录结构前端我用Vite搭建Vue3项目执行npm create vitelatest recruit-web -- --template vue一行命令就能创建。然后安装Element Plus、Vue Router、Axios、ECharts这些核心依赖。npm install的过程里有一个很多新手都会遇到的问题依赖安装特别慢或者装到一半卡死。原因就是npm默认源在国外你只需要把npm源切换到国内镜像就行。一条命令解决npm config set registry https://registry.npmmirror.com切换到镜像源之后Element Plus这种大组件库几十秒就能装完。前端项目目录结构按模块划分src ├── api # 接口请求封装 ├── router # 路由配置 ├── store # Pinia状态管理 ├── views # 页面组件 │ ├── dashboard # 数据统计面板 │ ├── enrollment # 报名管理 │ ├── consult # 咨询管理 │ ├── major # 专业管理 │ └── system # 系统管理 ├── components # 公共组件 └── utils # 工具函数、axios实例封装4.2 axios封装与登录状态持久化前后端分离项目axios必须做一层统一封装。为什么要封装因为拦截器需要在请求发出时自动带上JWT令牌在响应回来时统一处理错误码比如401跳转到登录页500弹出错误提示。如果不封装每个接口都要手动处理这些逻辑代码重复率会高到你怀疑人生。// axios实例 import axios from axios import { ElMessage } from element-plus import router from /router const service axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器自动携带token service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) // 响应拦截器统一处理错误码 service.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.msg || 请求失败) return Promise.reject(new Error(res.msg)) } return res }, error { if (error.response?.status 401) { localStorage.removeItem(token) router.push(/login) ElMessage.error(登录已过期请重新登录) } else { ElMessage.error(网络异常请稍后重试) } return Promise.reject(error) } ) export default service要重点强调一个细节不要用location.href /login做登录跳转。因为整个Vue应用是单页应用SPA你直接用location.href跳转会触发整个页面刷新所有状态全部丢失相当于浏览器重新加载了一次页面性能差且体验割裂。正确的做法是拿到router实例调用router.push(/login)由Vue Router在前端内部完成路由切换页面不刷新、状态不丢失。4.3 Vue Router路由配置与权限守卫Vue Router在Vue3里的用法和Vue2略有不同路由守卫的写法变化比较大。核心代码import { createRouter, createWebHistory } from vue-router const routes [ { path: /login, component: () import(/views/login.vue), meta: { public: true } }, { path: /, component: () import(/layout/Index.vue), redirect: /dashboard, children: [ { path: dashboard, component: () import(/views/dashboard/index.vue), meta: { title: 数据看板, roles: [admin, staff] } }, { path: enrollment, component: () import(/views/enrollment/index.vue), meta: { title: 报名管理, roles: [admin] } }, // ...更多路由 ] } ] router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.public) { next() return } if (!token) { next(/login) return } const role localStorage.getItem(role) if (to.meta.roles !to.meta.roles.includes(role)) { next(/dashboard) return } next() })这里的关键是meta.roles这个自定义字段。我在路由配置里给每个页面声明了允许访问的角色列表守卫函数里判断当前登录用户的角色是否在这个列表里不在就直接踢回首页。这种“配置式权限”比在页面里写一堆if/else判断要优雅得多维护的时候只需要改路由配置。Vue Router还有一个非常重要的Api是路由params和query传参的区别很多面试也会问。params传参的意思是路径参数比如/major/123用来定位一个具体的资源。query传参是查询参数比如/enrollment?status1用来描述过滤条件。在这个项目里修改专业信息时跳转编辑页就适合用paramsrouter.push(/major/edit/${id})而报名列表筛选状态刷新后还想保持当前筛选条件就适合用query因为query会同步到URL上刷新不丢失。4.4 Element Plus表格与表单实践Element Plus的表格组件el-table封装得非常好配合分页组件el-pagination能快速实现一个带筛选条件的列表页。我在这个项目里把表格和分页的交互提炼成了一种固定套路页面加载时调用fetchList(1, pageSize)拉取第一页数据点击分页或者切换每页条数时调用handlePageChange筛选条件变化时重置到第一页用监听器或者显式调用核心代码逻辑如下const queryParams reactive({ studentName: , majorId: null, status: null, pageNum: 1, pageSize: 10 }) const tableData ref([]) const total ref(0) async function fetchList() { const res await api.getEnrollmentList(queryParams) tableData.value res.data.rows total.value res.data.total } function handleSearch() { queryParams.pageNum 1 fetchList() } function handleSizeChange(size) { queryParams.pageSize size queryParams.pageNum 1 fetchList() } function handlePageChange(page) { queryParams.pageNum page fetchList() }reactive和ref这两个Vue3的核心响应式API使用场景是有明确区分的ref适合包装单个基本类型的响应式值reactive适合包装对象。像queryParams这种包含多个字段的查询参数对象用reactive最合适。你如果非要用ref包一个对象也可以正常用但访问时都要.value模板里你会觉得有点啰嗦。4.5 ECharts数据可视化看板招生数据看板是系统的门面招生办领导打开电脑第一眼看的就是这个页面。我用ECharts实现两个核心图表一个是“各专业咨询量TOP10”的横向柱状图另一个是“近一个月的每日咨询和报名趋势”折线图。ECharts在Vue3里的使用有几个经验很实用第一按需引入不要全量引入。ECharts全量包有几百KB而且首屏渲染时还要解析所有图表组件。按需引入的方式是先引入核心模块再手动注册用到的图表类型import * as echarts from echarts/core import { BarChart, LineChart } from echarts/charts import { GridComponent, TooltipComponent, LegendComponent } from echarts/components import { CanvasRenderer } from echarts/renderers echarts.use([BarChart, LineChart, GridComponent, TooltipComponent, LegendComponent, CanvasRenderer])第二图表容器必须有固定的高度。ECharts初始化的前提是容器有确定的宽高很多新手把图表塞进一个没有设置height的div里结果页面一片空白就是因为容器高度是0。我的做法是统一用styleheight: 350px固定高度。第三组件卸载时要销毁图表实例。在Vue3的onBeforeUnmount钩子函数里调用myChart.dispose()否则页面切换后ECharts实例还在占用内存时间长了页面会越来越卡。如果用了Vue3的KeepAlive缓存页面那销毁的时机要特别注意不能销毁缓存中的实例而是用setOption更新数据。5. 环境搭建、部署上线与常见问题排查5.1 MySQL安装配置与数据库初始化MySQL 8.0的安装配置是很多新手的第一道坎。如果你是在Windows上开发用官方安装包安装最省心下载mysql-installer-community一路Next选择Developer Default组件设置root密码搞定。如果你在云服务器上部署Linux环境下用包管理器安装更快。MySQL 8.0的默认认证插件是caching_sha2_password和旧版本驱动有兼容性问题。如果你连接时报Public Key Retrieval is not allowed这个错在MySQL连接URL上加上allowPublicKeyRetrievaltrue参数即可解决。另外数据库初始化阶段一定要设置好时区和字符集。MySQL 8.0默认的时区是系统时区有时候驱动连接的时区不对时间就会差8个小时。强烈建议在连接URL上显式指定jdbc:mysql://localhost:3306/recruit_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue还有字符集要用utf8mb4而不是utf8。utf8在MySQL里实际上不是真正的UTF-8它只能存1~3个字节的字符遇到生僻字或者Emoji直接报错乱码。utf8mb4才是完整的UTF-8编码。建库语句我习惯写成这样CREATE DATABASE recruit_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;5.2 前后端联调中的跨域问题前后端分离项目联调阶段第一个遇到的大概率就是跨域问题。前端跑在localhost:5173后端跑在localhost:8080端口不一样浏览器的同源策略就会拦截请求。跨域问题有三层解法从简单到复杂排列第一层开发环境用Vite的代理。前端在vite.config.js里配置server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样前端请求/api/enrollment/list时Vite开发服务器会把请求转发到后端8080端口浏览器视角下是同源请求跨域问题消弭于无形。这层方案只在开发环境有效生产环境用Nginx一样可以配置。第二层后端开启CORS。在后端写一个CorsConfig类或者用CrossOrigin注解。全局限流式的CORS配置注意要指定allowedOriginPatterns而不是allowedOrigins因为后者不支持*通配符且Cookie跨域行为不同registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600);第三层生产环境用Nginx反向代理。这是最推荐的生产环境方案把前端的静态文件交给Nginx托管/api开头的请求反向代理到后端的8080端口location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }这套方案既解决了跨域又把前后端部署在同一个端口下省去了配置HTTPS证书时还要处理跨域证书的麻烦。5.3 生产环境打包与部署前端打包一条命令npm run build打包产物在dist目录里面是纯静态文件扔给Nginx托管就行。有一个很关键的坑Vue Router使用createWebHistory模式时刷新页面会404。因为/enrollment这种路径是前端路由服务器上并不存在这个物理文件直接刷新时Nginx找不到对应文件就返回404了。解决方案是在Nginx配置里加一个兜底规则location / { try_files $uri $uri/ /index.html; }这句话的意思是如果请求的路径找不到对应文件就返回index.html让前端路由接管。后端打包用Maven的package命令打成jar包mvn clean package -DskipTests拿到target/recruit-system.jar文件上传到服务器一条命令启动nohup java -jar recruit-system.jar --spring.profiles.activeprod app.log 21 nohup和是为了让进程在后台运行日志输出到app.log。注意SpringBoot项目的配置文件里生产环境的数据库密码绝不能以明文形式写在配置文件中。我把数据库密码配置在环境变量里运行jar包时从环境变量读取或者用Jasypt对密码加密存储到配置文件。这个细节虽然麻烦一点但涉及到考生个人隐私数据必要的安全加固还是要做的。5.4 常见问题速查表把我在开发维护这套系统过程中遇到的典型问题整理成一个速查表方便排查问题现象原因分析解决方案前端请求接口报401JWT令牌过期或未携带检查axios拦截器是否正确配置Authorization头确认token有效期设置上传图片报错“文件大小超过限制”SpringBoot默认上传限制1MBapplication.yml设置spring.servlet.multipart.max-file-size和max-request-size数据库中文乱码连接URL未指定字符集或数据库字符集错误URL加characterEncodingutf8建库用utf8mb4前端刷新页面404Vue Router history模式未配置兜底Nginx加try_files $uri $uri/ /index.html;查询数据特别慢where条件字段未建索引给major_id、phone、create_time等高频查询字段加索引跨域请求被拦截开发环境端口不同Vite配置proxy代理或后端开启CORS部署后图片无法显示上传目录映射未配置确认WebConfig里的虚拟路径映射是否正确表格数据排序异常排序字段是varchar类型排序字段应为数值类型或SQL里用CAST()转换5.5 源码学习与二次开发建议最后说一点源码学习和基于这套系统做二次开发的建议。很多同学拿到源码第一件事就是点运行这其实是最低效的学习方式。我的建议是三步走第一步看懂数据库。把所有表的字段和关系搞明白在MySQL里执行一遍建表SQL用Navicat或者MySQL Workbench画出实体关系图。数据库是系统的骨架你只要把表结构搞透了系统里大概有什么功能、数据怎么流动心里就有底了。第二步跑通一条完整链路。从登录开始到新增一条专业信息再到提交一条报名记录最后在数据看板上看到统计数据更新。走通这条链路你就理解了整个系统的核心业务闭环。第三步针对一个模块做二次开发。比如给“报名管理”增加一个批量导入Excel的功能或者给“数据看板”增加一个新图表。带着需求改源码学到的东西比单纯读代码多得多。二次开发的方向上这套系统可以扩展的点其实很多。比如对接微信公众号让考生在微信里直接填报名信息增加短信验证码登录提高考生账号的安全性接入对象存储服务把宣传视频和大文件迁移到云端用定时任务实现每天自动汇总招生数据发送邮件报表。SpringBoot生态里这些都有现成的组件扩展起来并不困难。我平时带新人时经常说一句话不怕代码报错就怕你没有自己的思路。这套SpringBoot Vue MyBatis MySQL的组合是目前Java全栈开发的主流技术栈把招生宣传管理系统这个项目吃透你以后遇到其他管理类系统办公OA、教务管理、库存管理都能很快上手因为核心的CRUD、权限、文件上传、报表导出这些套路是通用的。希望这篇文章能帮你少踩一些坑项目顺利跑起来。