SpringBoot+Vue前后端分离招生宣传管理系统开发与部署

SpringBoot+Vue前后端分离招生宣传管理系统开发与部署 先说结论这套前后端分离招生宣传管理系统本质就是一个典型的校园招生业务闭环——专业展示、计划发布、在线报名、后台审核、数据统计。技术栈是SpringBoot Vue MyBatis MySQL前端用Vue全家桶后端提供RESTful接口数据库用MySQL存业务数据。适合正在做毕业设计、刚入职想练手前后端分离项目、或者学校/培训机构想快速搭一套招生系统的朋友参考。源码结构和部署流程都比较规整照着跑一遍能把前后端分离开发的完整链路摸清楚。这套项目我实际部署跑过代码层面的东西不算复杂真正的价值在于它的业务完整性和部署可复现性。很多新手学完框架基础后拿着手里的项目不知道怎么串联起来要么卡在跨域要么卡在Token要么卡在数据库连不上。这篇文章我就从设计思路、数据库、后端、前端、部署、踩坑这几个维度完整拆一遍尽量讲清楚每一步为什么要这么做而不是只给你甩几个命令。1. 项目概述与整体设计思路1.1 招生宣传系统到底在解决什么问题招生宣传管理系统这个名字听起来很宽泛但落到实际业务里核心其实是三个字信息流。考生和家长需要查学校有哪些专业、每个专业招多少人、学费多少、学制几年招生老师需要发布招生计划、更新宣传文章、维护专业信息管理员需要审核报名记录、统计报名数据。这三类角色之间的信息流转就是整个系统要解决的核心问题。从系统功能模块来看通常包含这么几个部分专业信息管理、招生计划管理、宣传文章发布、在线报名登记、报名审核与统计、用户权限管理。前台面向考生和家长提供查询和报名功能后台面向管理员和招生老师提供服务端数据管理和审核功能。业务闭环很清晰非常适合用来学习和二次开发。1.2 为什么选择前后端分离 SpringBoot Vue这套组合先说前后端分离。传统开发模式里页面和后端逻辑混在一起JSP或者Thymeleaf模板写完改个前端样式经常要重启后端服务而且前端开发和后端开发没法并行推进。前后端分离之后前端只负责页面渲染和交互后端只提供JSON数据的接口两边通过HTTP协议通信开发效率高不说后期前端可以单独部署到Nginx后端单独部署到服务器扩展性和维护性都强很多。再说技术栈选型。SpringBoot解决了Spring框架繁琐的XML配置问题内置Tomcat一个jar包就能跑起来是目前Java后端开发的事实标准。Vue作为渐进式JavaScript框架上手曲线平滑组件化开发让页面复用变得非常方便配合Vue Router和Vuex/Pinia可以很好地管理路由和状态。MyBatis是持久层框架SQL由开发者自己控制在复杂查询和SQL调优场景下比JPA更灵活面试中也经常被问到。MySQL不用多说开源、稳定、生态成熟中小型系统的首选数据库。这套组合的优势在于每一层都有清晰边界任何一个环节出问题都能快速定位而且这套技术栈在招聘市场上的需求量大学完直接能用在工作中。用这套技术栈来做招生管理系统属于杀鸡用牛刀但正因为如此才能把各个技术点的最佳实践都覆盖到。2. 数据库设计与SQL脚本准备2.1 核心表结构设计思路数据库设计是整个系统的地基地基打不好后面写接口、写页面全都会别扭。招生宣传系统的数据库表设计核心围绕用户-专业-计划-报名-文章这几个业务实体展开。我建的时候分了这几张表用户表sys_user、专业信息表major_info、招生计划表recruit_plan、宣传文章表article_info、报名记录表enroll_record、公告表notice_info。建议再加一张角色表或者直接通过用户表的role字段来区分权限小规模系统用字段就够了省得表关联太复杂。核心表关系是专业信息和招生计划是多对一的关系一个专业在一段时间内可能对应多条不同年份的招生计划报名记录关联专业同时记录学生的基本信息用户表和报名记录之间通过操作人字段关联但不建强外键约束保持表间的松耦合。所有表都建议加上create_time、update_time两个字段方便排查问题和做排序。下面给出最核心的两张表SQL其他表结构在完整源码里都有这里先看设计思路-- 专业信息表 CREATE TABLE major_info ( id int NOT NULL AUTO_INCREMENT, major_name varchar(100) NOT NULL COMMENT 专业名称, department varchar(100) DEFAULT NULL COMMENT 所属院系, education_level varchar(20) DEFAULT 本科 COMMENT 学历层次, duration varchar(20) DEFAULT 四年 COMMENT 学制, tuition decimal(10,2) DEFAULT NULL COMMENT 学费, description text COMMENT 专业介绍, cover_image varchar(255) DEFAULT NULL COMMENT 封面图, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT专业信息表; -- 报名记录表 CREATE TABLE enroll_record ( id int NOT NULL AUTO_INCREMENT, student_name varchar(50) NOT NULL COMMENT 学生姓名, id_card varchar(18) DEFAULT NULL COMMENT 身份证号, phone varchar(20) DEFAULT NULL COMMENT 联系电话, major_id int NOT NULL COMMENT 意向专业ID, score decimal(5,2) DEFAULT NULL COMMENT 高考分数, status tinyint DEFAULT 0 COMMENT 审核状态 0待审核 1已通过 2已拒绝, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_major (major_id), KEY idx_phone (phone) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT报名记录表;2.2 SQL脚本的导入与初始化源码包里一般会带一个sql文件夹里面有两类脚本一类是建表脚本schema.sql一类是初始化数据脚本data.sql。建议分两个文件不要混在一起方便后期排查。初始化数据里至少要包含一个管理员账号用户名admin密码经过BCrypt加密后存储这里注意密码一定不能明文存在数据库里。导入数据库用命令行或者Navicat都行。命令行操作是mysql -u root -p create database recruit_system default character set utf8mb4; use recruit_system; source /path/to/schema.sql; source /path/to/data.sql;utf8mb4这个字符集一定不能省不然存emoji表情或者生僻字会出现乱码。导入完成后可以随机抽几张表看下数据确认没问题再改后端配置。3. 后端SpringBoot核心实现与业务逻辑3.1 后端工程目录结构后端工程用的是标准Maven结构包名一般叫com.xxx.recruit。初始搭建的时候我建议按功能模块分包而不是按技术类型分包这样业务边界更清晰。推荐这样的结构com.xxx.recruit ├── common # 通用工具类、统一返回结果、全局异常处理 │ ├── Result.java │ ├── ResultCode.java │ └── GlobalExceptionHandler.java ├── config # 配置类如跨域配置、拦截器注册 ├── controller # 控制层接收请求并返回结果 ├── service # 业务层接口实现类 ├── mapper # MyBatis的Mapper接口 ├── entity # 实体类 ├── interceptor # 登录拦截器或JWT拦截器 ├── utils # JwtUtil、MD5等工具类 └── RecruitApplication.javaController层只负责参数接收和结果封装具体逻辑全部下沉到Service层Mapper层只做数据库交互这种分层方式刚开始写会感觉繁琐但项目一大了就能体会到好处——每个层次职责单一出问题不用翻遍所有代码。3.2 SpringBoot整合MyBatis时的配置要点SpringBoot整合MyBatis的配置集中体现在application.yml文件里。很多新手在这里容易踩坑我直接把关键配置列出来server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/recruit_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.xxx.recruit.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl驱动类这里要特别强调MySQL 8.x必须用com.mysql.cj.jdbc.Driver老版本的com.mysql.jdbc.Driver已经废弃了。URL里serverTimezoneAsia/Shanghai解决时区问题useSSLfalse解决连接警告。map-underscore-to-camel-case这个配置非常实用它能自动把数据库的下划线字段映射为Java的驼峰属性比如数据库里的major_name会自动匹配实体类里的majorName免去在XML里写大量resultMap。日志配置里log-impl用StdOutImpl开发阶段能在控制台直接看到SQL语句排查问题非常方便上线前再关掉。3.3 登录认证与Token机制设计前后端分离项目里最常问到的就是Token怎么处理。这套系统我用的方案是登录成功后后端生成一个JWTJSON Web Token返回给前端前端保存在本地存储中之后每次请求都在请求头Authorization字段携带这个Token后端通过拦截器统一校验。JWT的生成代码核心逻辑如下public String generateToken(Integer userId, String username, String role) { return Jwts.builder() .setSubject(username) .claim(userId, userId) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() 1000 * 60 * 60 * 24)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); }这个Token设置了一天过期时间实际项目中可以根据需求调整管理后台建议短一点前台用户长一点。登录接口在用户校验通过后返回这个Token同时返回用户基本信息供前端展示。后端拦截器的作用是拦截需要登录才能访问的接口校验请求头里的Token是否有效无效或者过期直接返回401状态码。拦截后把解析出来的用户信息放入ThreadLocal或请求域中方便业务层获取当前登录用户。我这里强烈建议写一个自定义注解比如LoginRequired在Controller方法上声明需要的权限级别比在拦截器里硬编码URL列表要灵活得多。3.4 统一返回结果与全局异常处理前后端分离开发中前端最怕的就是后端返回的数据格式不统一。有的接口直接返回一个对象有的返回List有的返回Map前端就要写一堆奇怪的判断逻辑。我通常是定义一个通用返回类Resultpublic class ResultT { private Integer code; // 状态码200成功401未登录500服务器错误 private String message; // 提示信息 private T data; // 返回数据 // 静态方法 success(data), error(message), unauthorized() ... }所有接口的返回值都包裹在Result里前端只需要根据code判断请求是否成功然后从data里取数据即可。全局异常处理器用RestControllerAdvice统一捕获业务异常和系统异常避免把堆栈信息直接抛给前端让接口返回的错误信息既安全又友好。这套代码写起来很机械但价值极高能省掉前后端联调时大量扯皮。团队里如果有规范这个类通常也是最早定下来不动的。4. 前端Vue项目实现与Token请求链路4.1 前端工程初始化与环境准备前端工程用Vue CLI或者Vite创建脚手架推荐Vite配合Vue 3启动速度快。但在很多学校的教学环境和企业现有项目中Vue 2的生态存量依然很大所以源码里无论用哪种原理都一样。初始化命令npm create vitelatest recruit-web -- --template vue cd recruit-web npm installnpm install这个步骤经常有人栽跟头。如果网络条件不好直接设置淘宝镜像源npm config set registry https://registry.npmmirror.com装完依赖后我会顺手安装项目需要的核心依赖npm install vue-router4 axios element-plus piniaElement Plus是Vue 3的UI组件库用它可以快速搭出表格、表单、弹窗等后台管理界面。前端项目的目录结构我会在src下分成views、components、router、store、api、utils几个目录。views按页面拆分components放复用组件api目录按业务模块封装请求方法utils放axios实例和工具函数。4.2 Axios请求封装与Token注入的实现这一步是整个前后端分离项目的灵魂所在。Axios如果每次请求都手写代码会非常冗余而且Token注入逻辑很难统一。我习惯单独建一个utils/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 }, error { return Promise.reject(error) }) // 响应拦截器统一处理返回结果 request.interceptors.response.use(response { const res response.data if (res.code 200) { return res.data } else if (res.code 401) { localStorage.removeItem(token) router.push(/login) ElMessage.error(登录状态已过期请重新登录) return Promise.reject(new Error(未授权)) } else { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } }, error { ElMessage.error(网络异常请稍后重试) return Promise.reject(error) }) export default request请求拦截器里做的事情就是给每次请求加上Authorization头响应拦截器里根据状态码做统一处理。这里最关键的是401处理逻辑——Token过期后自动清理本地存储并跳转登录页这个体验比用户点半天才发现接口全报错要友好得多。注意baseURL直接写成/api开发环境通过Vite代理转发到后端生产环境通过Nginx反向代理转发这样前端代码不用区分环境部署的时候非常省心。4.3 路由与页面访问权限控制Vue Router的配置比较套路化但路由守卫是实现权限控制的关键所在。我的做法是路由表里给每个需要登录才能访问的页面加上meta属性标记requiredAuth为true然后在全局前置守卫里做校验router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiredAuth !token) { next(/login) } else if (to.path /login token) { next(/) } else { next() } })如果系统里有面向普通用户和管理员的不同页面那就需要更进一步从后端返回的用户信息里解析角色在路由守卫里判断当前用户的角色是否有权限访问目标路由这样就能做到不同角色看到不同菜单、访问不同页面。4.4 核心页面与表单联调的实现细节前端页面里最核心的几个页面是专业列表、招生计划展示、在线报名表单和后台管理表格。专业列表页就是调用后端接口获取专业数据用Element Plus的表格或者卡片组件渲染在线报名表单用el-form做好字段校验电话号码格式、身份证号格式都需要在前端先拦一道减轻后端压力。后台管理表格用el-table展示报名记录每一行提供审核通过和拒绝按钮操作成功后刷新列表数据。这里有个细节可以分享表格分页我用的后端分页方案前端传pageNum和pageSize参数后端返回总条数和当前页数据而不是一次性把几万条数据全查出来丢给前端数据量一大前端浏览器就会卡死。页面交互层面的核心逻辑都不难难的是接口字段名对不上、数据类型对不上这类问题。所以我会在组件里先看清楚后端返回的数据结构再决定怎么渲染这就是联调的日常。5. 从本地到服务器完整部署教程5.1 本地部署十分钟跑通前后端本地部署前确认电脑上已经装好JDK 1.8、Maven 3.6、Node.js 14、MySQL 8.0。环境检查命令java -version mvn -version node -v npm -v后端启动步骤是先把数据库脚本导入MySQL然后修改application.yml中的数据配置最后在项目根目录执行mvn clean package -DskipTests java -jar target/recruit-system-1.0.0.jar看到Spring Boot的启动日志显示Tomcat started on port(s): 8080就说明后端已经跑起来了。前端启动cd recruit-web npm install npm run devVite默认端口是5173但我们在axios请求里写了baseURL是/api所以需要在vite.config.js里加服务代理server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }配置完成后浏览器访问http://localhost:5173登录页面能正常跳转、首页能拉取到接口数据说明本地链路已经全部打通。5.2 服务器部署Nginx前后端分离部署服务器部署我推荐用云服务器系统选Linux的CentOS也可以用别的发行版命令略有差异。整体流程分四步第一步安装JDK、MySQL、Nginx。JDK用yum安装OpenJDKMySQL如果yum源版本不够就直接下载官方仓库安装Nginx用yum install nginx。第二步导入数据库脚本创建同名的数据库和用户修改后端配置文件里的数据库地址为服务器地址重新执行mvn clean package打包把jar包上传到服务器用nohup后台启动nohup java -jar recruit-system-1.0.0.jar --spring.profiles.activeprod run.log 21 这里建议分环境配置application-dev.yml和application-prod.yml避免本地和服务器配置来回改。第三步前端构建产物生成上传dist目录到服务器指定目录。前端打包命令npm run build生成的dist文件夹里就是纯静态文件上传到/srv/www/recruit-web下。第四步配置Nginx。Nginx一方面托管前端静态文件另一方面把/api开头的请求反向代理到后端端口。核心配置如下server { listen 80; server_name your_domain_or_ip; # 前端静态资源 location / { root /srv/www/recruit-web; index index.html; 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; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }try_files配置非常关键它的作用是当用户访问某个路由路径时如果静态目录里找不到对应文件就返回index.html由前端路由接管页面渲染这样Vue Router的history模式才能正常工作。配置完成后重启Nginxnginx -t systemctl restart nginx浏览器直接访问服务器IP能看到系统页面并且接口数据正常加载整个部署就算完成了。5.3 部署过程中的参数选择说明部署中有几个参数值得说清楚。后端启动时加了--spring.profiles.activeprod这个参数作用是让SpringBoot加载application-prod.yml文件中的配置隔离各环境配置。Nginx里的proxy_pass我写的http://127.0.0.1:8080注意结尾有没有/的区别很细微建议以实际请求路径为准做测试。数据库连接池里的max-active参数建议保持默认招生系统的并发量不会太高不需要过度调优。6. 常见问题与排查技巧实录6.1 问题速查表我在部署这套系统的过程中以及在帮别人排查问题时遇到的所有高频问题基本都能归到下面这张表里现象可能原因解决方案后端启动报端口被占用8080被其他程序占用lsof -i:8080找到进程并kill或修改server.port前端请求接口报404代理配置错误或后端没启动检查vite.config.js代理配置确认后端能访问登录成功后调用业务接口仍401Token没传或拦截器校验失败打开浏览器开发者工具看请求头确认Authorization有值数据库中文乱码字符集不是utf8mb4建库时指定default character set utf8mb4MyBatis提示Invalid bound statementMapper接口和XML映射文件未对应检查mapper-locations路径和XML的namespacenpm install非常慢默认源访问慢切换到npmmirror镜像源前端打包后刷新404路由模式使用了historyNginx配置try_files回退到index.html接口返回时间字段格式不对时区问题或Jackson配置不对URL加serverTimezoneAsia/Shanghai配置Jackson日期格式6.2 两个典型的坑详细说第一个坑是跨域问题。本地开发时通过Vite代理请求后端所以看不到跨域报错但服务器上如果前端静态文件由Nginx管理接口请求方向是前端页面所在域名或IP发起的如果没有走代理直接请求后端地址就会触发浏览器的跨域限制。解决方案是后端在Config类里配置CORS允许跨域或者在Nginx层完成反向代理推荐后者生产环境不暴露后端端口更安全。第二个坑是MyBatis的mapper-locations配置。很多系统报错Invalid bound statement的原因是application.yml里没有配mapper-locations或者XML文件没放在classpath对应目录。这个问题的排查思路是启动日志里看MyBatis的Mapper扫描日志确认每个接口是否都找到了对应的XML声明没找到就重点检查文件名、namespace、方法id这三处是否完全一致。6.3 用这套源码做二次开发怎么最快上手源码拿到手先不要急着跑先花半小时把目录结构和表结构过一遍。我推荐的阅读顺序是先跑通项目然后用一个最简单的功能比如专业列表从前端页面点击开始沿着路由到api请求再到controller、service、mapper、SQL完整走一遍请求链路你就知道每个文件是干什么的了。之后再想改哪里直接对应调整就行。如果要做二次开发优先考虑这几个扩展点一是增加图片上传功能把专业封面和宣传文章的图片上传到服务器二是增加数据导出功能把报名信息导出为Excel三是加一个前端首页的轮播图管理。这三个功能都比较独立不破坏现有代码结构适合练手。写在最后的一些经验体会这套系统跑通之后我最大的感受是前后端分离项目的难点不在写代码而在理解数据和请求是怎么流动的。从数据库的一张表到后端接口的一次查询再到前端页面的一个表格这条链路理清楚了整个项目的骨架就掌握了。招生宣传管理系统算是一个很好的练手项目——业务不复杂但覆盖了登录鉴权、CRUD、分页、部署上线这些实际工作中天天要用的东西。如果你正在学SpringBoot和Vue别纠结于看多少教程把源码跑起来改两个功能比看十个小时的视频都管用。最后说一个小建议源码里的配置文件、注释、命名规范都是可以学的地方但更值得学习的是包结构和请求链路的组织方式这些才是真正能迁移到其他项目里的东西。