
每年三月份总有准备毕业设计的学弟学妹跑来问我老师要求做一个前后端分离的高校迎新系统技术栈限定SpringBootVueMyBatisMySQL还得有完整源码和部署教程我该从哪下手。这个问题我太熟悉了。我当年就是把这套系统完完整整做下来从数据库建表、后端接口封装到前端页面联调和最后的服务器部署一路踩坑一路填最后在学院迎新现场真正跑起来的时候那个成就感确实难忘。这篇就把整个项目的思路和落地过程一次性写清楚适合正在选题的学生、想练手前后端分离项目的新手以及准备接手校园信息化系统的运维同学。无论你是零基础还是有点底子照着这套流程基本都能把系统搭起来。1. 这个迎新系统到底要做什么需求与架构拆解1.1 系统定位把迎新从排队变成扫码大多数高校的迎新场景是这样的新生拖着行李到报到处先找学院的桌子排队辅导员拿着纸质花名册一个个打钩旁边有人手工填住宿分配表晚上统计报到率的时候还得几个人对着Excel来回对。你要是经历过一次就会明白这个流程有多需要信息化。迎新系统就是把“排队一上午”变成“扫码两分钟”。新生到校后凭学号或者身份证号就能完成报到登记、缴费确认、宿舍分配、材料核验这些步骤。管理员在后台录入新生名单、开设基础数据就能实时看到报到进度哪个学院来了多少人、哪个专业还没到齐、宿舍还剩多少床位这些以前要等一天的统计数据系统里随时查得到。说白了它不是一个炫技的项目而是一个典型的“信息管理系统”。但正因为业务场景接地气需求明确、流程清晰它特别适合拿来练手前后端分离项目。你不需要纠结复杂的权限模型也不需要考虑并发到底有多高只要把“新生报到”这条主线上的数据管明白系统就成功了大半。1.2 功能模块与角色划分整个系统可以按角色拆成两大块学生端和管理员端。学生端是给新生报到时用的重点关注报到流程、个人信息、宿舍查询管理员端是给辅导员、学院管理员、宿管用的重点关注名单管理、登记审核、宿舍分配和数据统计。我一般会把需求拆成一张功能表开发的时候对着表做不会漏功能角色功能模块核心说明学生报到登记输入学号后展示待办流程逐项完成学生信息查询查看分班、分宿舍、缴费结果学生迎新须知查看报到流程和注意事项管理员新生导入Excel批量导入新生名单校验学号唯一管理员报到审核到校确认标记报到状态管理员宿舍分配按学院、班级自动分配或手动调整管理员数据看板实时报到人数、各专业报到率、每日趋势这个需求规模刚好是一个SpringBoot后端加一个Vue前端能扛住的量。再多就比较吃力再少又学不到东西所以拿来做毕设或者练手项目非常合适。1.3 为什么必须用前后端分离如果你看过老校园系统会发现很多还是JSP那一套页面和Java代码耦合在一起改个前端样式要重启整个服务后端接口也没法复用。前后端分离之后后端只负责提供JSON格式的接口前端独立开发、独立部署两边通过约定好的接口格式通信。这套结构的好处是实实在在的。前后端可以并行开发联调的时候各自本地起服务就行后期维护也方便改页面不用重启后端接口可以单独压测更重要的是它贴近企业里真实的开发模式。现在的公司招人不问你会不会JSP但一定会问你前后端分离项目的开发经验。代价就是部署的时候比单体系统多一个步骤但这个步骤本身就是很好的学习机会后面第5章我会把部署的两种方式都讲清楚。2. 技术选型为什么是这四件套2.1 SpringBoot和MyBatis为什么能搭到一起先说SpringBoot。它解决的是配置地狱的问题以前用Spring搭一个Web项目要写一堆XML配置文件现在SpringBoot把这些都自动化了内嵌Tomcat一个java -jar就能把服务跑起来。对毕设项目来说它把“启动一个后端服务”这件事的复杂度降到了最低让你把精力花在业务代码上。MyBatis则把SQL控制权完全还给开发者。迎新系统的业务集中在对学生、宿舍、报到记录这几张表的条件查询和联表统计上用原生SQL写起来直观、好优化比全自动ORM更容易定位慢查询。比如你想查“某学院还没报到的男生有哪些”一句话就能写清楚想在SQL里做GROUP BY统计报到率也能自由发挥。MySQL则是同类关系型数据库里最省心、资料最多、学校机房愿意装的那个。5.7和8.0都可以用但如果机房是老版本你本地开发也要尽量一致避免驱动和语法差异带来的麻烦。这几个选型都不是什么高深理论就是“够用、好上手、有大量参考资料”这三个朴素标准筛出来的。2.2 Vue与Element组件库怎么配合前端选Vue核心原因是组件化开发太适合管理后台了。管理员的界面拆开看无非就是表格、表单、弹窗、菜单、统计图表这几种组件来回组合。Vue把每个页面当成组件的组合数据驱动视图写起来比直接操作DOM爽得多。配合Element UI这套现成的组件库开发速度肉眼可见地快。你不需要自己画一个漂亮的表格el-table拉进来把列配置好数据绑定上就完事。当然这里有个很现实的问题Vue 2已经进入官方维护的尾声Vue 3生态已经成熟但网上大量老教程还在用Vue 2和Element UI。我的建议是如果你参考的教程和源码是老一套那你就跟着用Vue 2千万别Vue 3配Element UI或者Vue 2配Element Plus版本一混报错会让人崩溃。另外说个习惯前端开发的时候把Vue Devtools插件装上查组件里的数据、看路由跳转比在代码里到处console.log舒服太多了。2.3 开发环境准备清单环境配置这部分看着基础但每年都有人卡在这里好几天。我列一份亲测顺畅的版本组合JDK 8或11入门项目JDK 8完全够用Maven 3.6以上别用太老的MySQL 5.7或8.0注意驱动版本和连接串的差异Node.js 14或16Vue 3项目可以上18IDEA装上Lombok、MyBatisX插件XML里的SQL标签有高亮Mapper接口和XML之间能互相跳转省下大量时间MySQL在Windows上直接下载MSI安装包一路Next就行Linux上用apt或yum装完记得启动服务并设置root密码。密码别设得太简单项目配置里虽然是明文但被人扫到数据库就麻烦了。Node.js去官网下载LTS版本npm -v能输出版本号就代表装好了。3. 数据库与后端手把手把核心功能实现掉3.1 核心表结构六张表跑通迎新流程后端开发第一步不是写Controller而是把数据库表设计好。我见过太多人一上来就写代码写到一半发现表缺字段、字段类型不对回头改表改代码折腾好几轮。表设计定了后面基本就是体力活。迎新系统核心表其实就六张左右我拆给你看department院系表存院系名称、编号major专业表关联院系clazz班级表关联专业student新生信息表这是全系统的中心表dormitory宿舍表存楼栋、房间、床位数dorm_assignment宿舍分配记录表register_record报到记录表存报到时间、操作人payment缴费记录表如果学校要求缴费确认可以单独拆学生表是最关键的一张我给你看一个比较标准的建表SQLCREATE TABLE student ( id bigint NOT NULL AUTO_INCREMENT, student_no varchar(20) NOT NULL COMMENT 学号, name varchar(50) NOT NULL COMMENT 姓名, id_card varchar(18) DEFAULT NULL COMMENT 身份证号, gender tinyint DEFAULT 0 COMMENT 0未知 1男 2女, dept_id bigint DEFAULT NULL COMMENT 院系ID, major_id bigint DEFAULT NULL COMMENT 专业ID, class_id bigint DEFAULT NULL COMMENT 班级ID, status tinyint NOT NULL DEFAULT 0 COMMENT 0待报到 1已报到, 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT新生信息表;几个设计细节说一下。学号加唯一索引这是Excel批量导入时防止重复数据的第一道防线。status默认0表示入学名单里都是“待报到”到校登记后改成1。字符集用utf8mb4不用我说你也应该知道为什么中文乱码的坑十个项目八个踩。每张业务表都带create_time和update_time排查问题的时候它们能救命。宿舍分配表和报到记录表的设计也差不多核心是记录“哪个学生住进了哪个床位”“谁在什么时间完成了报到”有了这两张明细表后面统计看板才有数据来源。3.2 后端分层与统一返回体后端代码我习惯按这个目录结构拆com.example.welcome ├── controller ├── service ├── mapper ├── pojo │ ├── entity 数据库实体 │ ├── dto 接收前端参数 │ └── vo 返回给前端的视图对象 └── configController层只负责接收参数和调用ServiceService层处理业务逻辑Mapper层写SQL。分层的意义在于出问题的时候你能快速定位该看哪一层而不是在一大坨代码里乱找。前后端交互最容易被忽视的是统一返回体。如果没有统一格式每个接口返回的结构都不一样前端处理起来就是灾难。我强烈建议你自己写一个ResultT类所有接口都走它Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.code 200; result.message 操作成功; result.data data; return result; } public static T ResultT error(String message) { ResultT result new Result(); result.code 500; result.message message; return result; } }前端拿到响应后只需要判断code 200就继续否则弹错误提示不用为每个接口单独写一套判断逻辑。这个习惯在以后做任何项目都适用。3.3 核心接口实现分页条件查询和分页插件用法迎新系统里最高频的接口就是“按条件分页查询新生列表”这个接口几乎能体现MyBatis的所有基本功动态SQL、联表查询、分页插件。先说分页插件PageHelper的用法这个属于必考点。引入依赖后在Service方法里先调用PageHelper.startPage(pageNum, pageSize)然后紧接着调用Mapper查询最后用PageInfo封装结果public PageInfoStudentVO queryStudentPage(int pageNum, int pageSize, StudentQueryDTO dto) { PageHelper.startPage(pageNum, pageSize); ListStudentVO list studentMapper.selectByCondition(dto); return new PageInfo(list); }注意startPage只对接下来紧跟着的那一次SQL查询生效中间不能穿插其他查询这是PageHelper最容易踩的坑。分页插件的原理是用ThreadLocal记录分页参数在SQL真正执行的时候自动拼接LIMIT所以startPage和Mapper调用必须在同一个线程栈里挨着写。条件查询的动态SQL用MyBatis的where和if标签select idselectByCondition resultTypecom.example.welcome.vo.StudentVO SELECT s.id, s.student_no, s.name, d.dept_name, m.major_name, c.class_name, s.status FROM student s LEFT JOIN department d ON s.dept_id d.id LEFT JOIN major m ON s.major_id m.id LEFT JOIN clazz c ON s.class_id c.id where if testkeyword ! null and keyword ! AND (s.name LIKE CONCAT(%, #{keyword}, %) OR s.student_no LIKE CONCAT(%, #{keyword}, %)) /if if teststatus ! null AND s.status #{status} /if /where ORDER BY s.id DESC /select动态SQL里if标签的test条件不要写错尤其是字符串判空要记得同时判断null和。LIKE拼接用CONCAT而不是直接%${keyword}%前者是预编译参数不会引入SQL注入风险后者是字符串拼接直接踩坑。再说一个MyBatis缓存的真实教训。一级缓存默认开启基于SqlSession项目里一般没什么问题。二级缓存如果开了多表联查的SQL返回结果会被缓存但其中一张表更新后缓存不会自动失效很容易读到脏数据。我的建议是这种小项目直接关掉二级缓存让查询每次都走SQL牺牲一点性能换正确性非常值得。3.4 跨域、异常与登录校验联调第一天遇到最多的就两件事接口返回404、浏览器报跨域错误。404多数是路径写错了跨域则是前后端分离开发环境下的第一道坎。跨域的本质是浏览器的同源策略。你前端跑在http://localhost:5173后端跑在http://localhost:8080端口不一样就是“跨域”了。浏览器发现这个请求不是同源的时候会先发一个OPTIONS预检请求如果服务端响应里没有允许跨域的响应头浏览器就直接拦截请求根本到不了前端代码里。后端解决跨域最省事的办法是写一个全局配置Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowCredentials(true) .maxAge(3600); } }这段配置的本质是给所有接口的响应头加上Access-Control-Allow-Origin告诉浏览器“这个跨域请求我允许”。注意allowCredentials(true)和allowedOriginPatterns(*)要搭配使用如果用了allowedOrigins(*)和allowCredentials(true)有些版本会直接报错。异常处理也要统一。Service里抛出业务异常Controller不可能一个个try-catch所以我用RestControllerAdvice加一个全局异常处理器把异常转成Result.error()返回避免堆栈信息直接甩到前端。登录认证的简单做法是做一套JWT拦截器用户登录成功后后端返回一个带过期时间的token前端每次请求都放在Header里后端拦截器校验token是否有效。放行登录接口和报到查询接口其他接口都要过拦截器。拦截器里注意放行OPTIONS预检请求否则跨域配置做了也会被拦死。上传功能也要在这个阶段处理文件限制大小、限制扩展名上传PDF、图片这类都要做校验前端传的字符串参数如果涉及富文本内容后端要统一做过滤别把脚本内容直接存库再回显。这些看似边缘的安全细节在答辩和面试的时候反而是加分项。4. 前端页面与联调让系统真正能跑起来4.1 路由与菜单设计前端项目结构我习惯用Vue Router做集中路由管理。管理后台的布局是固定的左边侧边栏顶部导航中间内容区。路由配置可以这样写const routes [ { path: /login, name: Login, component: () import(../views/Login.vue), meta: { title: 登录 } }, { path: /register, name: Register, component: () import(../views/Register.vue), meta: { title: 报到登记, roles: [ADMIN] } }, { path: /dashboard, name: Dashboard, component: () import(../views/Dashboard.vue), meta: { title: 统计看板, roles: [ADMIN] } } ]组件用component: () import(...)这种方式加载也就是路由懒加载打包的时候会自动拆包首屏加载速度会快不少。meta.roles用来做权限控制在全局路由守卫里判断一下用户角色能不能访问当前页面。这里有个新手必踩的坑路由的history模式是用HTML5的History API模拟路径路径好看但部署到Tomcat上之后刷新页面会404因为服务器在这个路径下找不到对应的文件。解决的办法无非两个改成hash模式或者让服务器把不存在的路径都重写到index.html。新手先直接用hash模式省心不用折腾服务器配置。4.2 Axios封装拦截器里统一处理登录失效前端请求不能每次都在组件里写一遍axios.get然后手动处理错误。我习惯在src/utils/request.js里封装一个统一的axios实例import axios from axios import { Message } from element-ui import router from ../router const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] token } return config }) service.interceptors.response.use( response { const res response.data if (res.code 200) { return res } if (res.code 401) { localStorage.removeItem(token) router.push(/login) } Message.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) }, error { Message.error(error.message || 网络异常) return Promise.reject(error) } ) export default servicebaseURL设成/api开发环境里让构建工具把/api转给后端的8080端口生产环境再把相同路径交给部署层转发。这样前后端联调最顺畅因为开发和生产环境不需要改一行业务代码。封装完之后页面里调用就变成import request from /utils/request const res await request.get(/student/page, { params: { pageNum: 1, pageSize: 10 } }) this.studentList res.data.list所有错误提示统一在拦截器里弹组件里关心成功的逻辑就行了。这里我特别想提醒一句异步请求的loading状态一定用finally来关不然接口报错的时候按钮还卡在加载状态用户会以为系统死了。就这一个细节实测很影响体验。4.3 报到登记与数据统计页面开发报到登记是整个系统的核心交互页面。新生的操作流程是输入学号回车系统从后端查出名字和院系班级信息展示出来管理员确认后选宿舍、点提交。这个页面涉及多次前后端交互逻辑不复杂但状态管理要细心。页面上的核心逻辑大致是queryStudent接口根据学号查学生详情返回后回填表单submitRegister接口提交报到和宿舍分配。中间加一个registering的布尔值控制按钮禁用和loading展示防止重复提交。统计看板用ECharts做。后端提供一个聚合接口用一句带GROUP BY的SQL查出“各院系报到人数”SELECT d.dept_name, COUNT(*) AS total, SUM(CASE WHEN s.status 1 THEN 1 ELSE 0 END) AS registered FROM student s LEFT JOIN department d ON s.dept_id d.id GROUP BY d.dept_name ORDER BY total DESC这就是前面说MySQL的ORDER BY和GROUP BY真正派上用场的地方。前端拿到数据后渲染柱状图、折线图展示实时报到率。如果感觉这种SQL写起来有点吃力建议回去把聚合函数和分组查询好好补补。开发的时候记得在DEV环境里看Network面板重点确认请求的URL、参数名、返回格式跟后端接口文档完全一致。很多联调问题不是代码写错了而是字段名大小写不一致、日期格式对不上这种小问题。5. 部署上线一份能直接抄的教程5.1 后端与前端分别打包开发环境下前后端各跑各的端口部署的时候就要分别打包成可以直接运行的东西。后端SpringBoot用Maven打包mvn clean package -DskipTests打包完成后target目录下会生成一个welcome-system-0.0.1.jar文件。这就是一个可独立运行的Java服务用Java命令就能启动nohup java -jar welcome-system-0.0.1.jar --server.port8080 app.log 21 nohup让它不随终端关闭而退出 app.log 21把日志输出到文件里这样服务就会一直在后台跑。查看服务有没有起来用jps -l看Java进程或者netstat -tlnp看端口。前端打包更简单npm run build生成一个dist目录里面全是静态文件。打包的时候脚本会读取当前环境配置如果你把后端地址写在了.env.development而不是代码里生产环境就不会带上开发地址。这一段很多人忽略打包出来之后首页能开但接口全部404然后开始怀疑人生。5.2 部署方式一用Tomcat托管前端静态页面如果你手头的服务器是Windows或者学校机房统一装的是Tomcat最省事的部署方式是把前端dist目录里的所有文件直接拷贝到tomcat/webapps/ROOT目录下。Tomcat启动后访问服务器的80或8080端口看到的就是前端登录页。后端jar单独跑在8080端口这时前端页面和后端接口的端口不一样浏览器又会跨域所以第3.4节配置的那个CorsConfig在这里就派上用场了。生产环境的跨域处理思路是要么让前后端处在同一个域名和端口下要么后端明确开启允许跨域。你两种方式都掌握面试官问起来可以对答如流。Tomcat部署的方案本质是“Tomcat管前端静态页面java -jar管后端接口”两个进程互不干扰。这种方式不需要额外安装其他软件对老环境最友好。5.3 部署方式二用Nginx托管静态资源并转发接口如果你用的是Linux服务器更常见的是用Nginx托管前端页面。思路也很直白Nginx的80端口服务前端dist目录所有以/api开头的请求统一转发给本机的8080后端端口。这样配置完之后浏览器看到的服务地址只有一个80端口不存在跨域问题所以后端那套CorsConfig在生产环境反而可以被关掉。Nginx的处理性能和并发能力也比Tomcat托管静态文件强不少。核心就是配置一个location块把路径匹配规则和转发目标写清楚。现在的云服务器文档里都有这类配置模板照着填端口和目录就行。我不打算把Nginx配置逐行贴出来因为不同系统、不同目录结构差异不小我这里只把这个部署思路讲明白等你真正在Linux上操作的时候你会感谢这个“先理解思路再动手”的过程。5.4 MySQL初始化与上线配置上线前先在服务器上创建数据库并导入表结构。把项目里的sql/init.sql传到服务器上执行mysql -u root -p welcome /路径/sql/init.sql注意welcome数据库要先创建字符集设为utf8mb4不然建表的时候中文字段注释就是乱码乱码。然后修改后端配置里的数据库连接串上线用的application.yml要专门检查这几个点数据库地址换成服务器真实IP或localhost、用户名密码改成服务器上配置的、连接串加上时区参数。MySQL 8.0的连接串和5.7不太一样一个经典报错是Public Key Retrieval is not allowed解决办法是在连接串里加allowPublicKeyRetrievaltrue。时区不对也会报错加上serverTimezoneAsia/Shanghai基本就都稳了。如果你在本地跑得好好的一部署就报时间相关的错十有八九就是没加时区参数。6. 常见问题速查我踩过的坑都在这里6.1 环境与部署常见问题我把这几年做这类项目遇到的高频问题整理成一张速查表建议收藏起来问题现象可能原因解决方案前端请求后端报跨域后端没开CORS或前后端端口不一致配置CorsConfig或让前后端走同一端口启动报Communications link failureMySQL服务没启动或端口、账号不对启动MySQL服务检查url的端口和账号MySQL8连接报Public Key Retrieval is not allowed驱动默认禁止公钥检索连接串加allowPublicKeyRetrievaltrue端口被占用8080被别的进程占了netstat -ano | findstr :8080然后结束对应进程npm install报错Node版本和依赖不兼容切换Node 14/16/18删node_modules重装部署后页面刷新404history路由在服务器上没有重写规则改hash模式或服务器配置路径重写到index.html修改页面后首页没变化浏览器缓存了旧资源前端构建时开启文件指纹或CtrlF5强刷6.2 MyBatis与数据库的典型坑MyBatis的坑集中在分页、Mapper绑定和缓存这三个地方。PageHelper不生效先检查startPage之后是不是紧跟着Mapper查询。有人喜欢在中间打印一条日志或者先查了别的表分页参数被消耗掉分页就失效了。还有版本问题PageHelper的Spring Boot Starter版本要和你用的SpringBoot版本兼容不然插件的自动配置根本不生效。Mapper接口报BindingException说找不到实现第一反应检查两件事启动类有没有加MapperScanXML文件里的namespace是不是和接口的全限定名一致。这两个地方对不上运行时就绑定不了。SQL执行慢是个值得花时间的问题。迎新系统里按学号、身份证号查是最高频的路径这两个字段一定要建索引没有索引全表扫描数据量到几万条就会明显变慢。遇到UPDATE异常慢的时候先用EXPLAIN看SQL执行计划检查有没有走索引大多数慢SQL都是索引问题。MyBatis缓存的坑我在3.3节已经提过小项目直接关掉二级缓存让查询走真实SQL。这不是说缓存不好而是缓存失效策略在联表场景下很容易写错性价比太低。6.3 前端联调的隐藏坑前端联调阶段我至少见过这三种让人崩溃的情况第一种是开发环境正常打包上线后接口全部404。原因是开发环境用的转发在打包后失效而后端地址写在了开发配置里。解决方案就是前面说的区分环境变量生产环境的API地址单独配。第二种是ECharts图表组件出来了但没有内容。排查思路先看容器高度ECharts必须挂在有明确高度的DOM上很多组件默认高度是0图当然不显示。再看初始化时机必须在数据返回之后再init不然拿到空数据渲染一个空图。第三种是表单怎么填都提交失败。在浏览器控制台看看提交的JSON数据如果id字段是字符串但后端是Long类型批量转换的地方就会报错。这种问题用console.log在提交前打印一下数据格式比盲改后端快得多。记住一句话前端联调排查永远先看浏览器Network和控制台再做猜测。7. 做完这个项目我的几点体会先说一句大实话这个项目能不能跑顺七成取决于表结构设计不是写代码的速度。我第一次做的时候急着抄别人现成的库表结果报到率统计要跨三张表反复JOIN后来实在受不了重新设计才顺畅。拿到任何源码都一样先把表关系图看明白再动手改别上来就删接口加字段后面有你哭的。再说一个我一直坚持的习惯每张业务表都带create_time和update_time数据出问题的时候能精准定位到“哪条记录在什么时候被改过”。别看它不起眼排查线上问题的时候能省下半天时间。另外数据库密码、Redis密码这类配置不要写死在代码里用配置文件隔离开部署的时候单独改一个文件就行。最后建议你把这个项目往深挖一层。做完基本功能之后可以试着给它加一个Spring Security的权限控制把管理员分成超级管理员和普通辅导员数据看板可以用WebSocket做成实时刷新新生报到可以加一个二维码扫码确认的流程。每次扩展都能逼你学一套新东西而这些东西在面试时候讲出来比背十道八股文有用得多。这套系统的技术栈不算复杂但能把这套流程完整走一遍你自己对“一个项目从零到上线”的理解绝对会上一个台阶。