1. 项目概述与需求拆解
做毕设或者接外包的时候,"智慧校园系统"这个名字几乎每周都能看到。但说实话,大部分包装成"智慧校园"的项目,实际就是基础的CRUD套壳:一个学生管理、一个课程表、一个公告栏,再加个登录注册就敢说自己是"数字化校园综合服务平台"。我在接手这个基于SpringBoot+Vue的前后端分离智慧校园系统时,第一件事就是先把需求盘清楚,搞清楚"智慧"到底体现在哪,哪些功能是撑门面的,哪些才是真正卡住用户的硬需求。
这个项目定位很明确:面向高校场景的数字化校园综合服务平台,核心价值是打通教学管理和校园生活两个维度。教学管理侧,要解决的是选课、排课、成绩管理、教师授课安排这些行政事务的线上化;校园生活侧,要覆盖的是校园卡消费、宿舍报修、社团活动、失物招领这类日常场景。整套系统采用前后端分离架构,前端Vue负责交互与展示,后端SpringBoot提供RESTful API,通过JSON数据进行通信,最终交付的是一个可独立部署、可扩展、可直接用于毕业设计展示或二期开发的完整系统。
我评估了这个项目的受众:如果是学生拿来当毕业设计,重点在于技术栈完整、功能覆盖全面、演示效果好,评审老师关心的通常是前后端分离的合理性、数据库设计的规范性、关键业务逻辑有没有深度;如果是企业级落地,那更看重的是权限模型够不够细、并发场景撑不撑得住、接口有没有做安全校验。这套设计从两头考虑,选课模块用到了Redis缓存预选课数据,权限模型用RBAC实现菜单和按钮两级粒度控制,既能体现技术深度,又不会因为过度设计导致工作量失控。
2. 技术选型与架构设计
2.1 为什么是SpringBoot+Vue这个组合
技术选型是整个项目的根基,也是很多人容易拍脑袋决定的部分。SpringBoot+Vue这对组合能成为当前前后端分离项目的标准答案,是有它的底层逻辑的。
后端用SpringBoot,核心理由是它把Spring生态从繁琐的XML配置里解放出来。基于Java技术栈的团队,不需要再写一堆bean.xml,通过自动配置和Starter机制,几个依赖就能拉起一个可运行的Web服务。校园系统里涉及的事务管理、ORM映射、安全认证,Spring家族全都有现成的解决方案:MyBatis-Plus处理CRUD连表查询,Spring Security配合JWT做无状态认证,Redis做缓存中间件,Spring Task处理定时任务。每个环节都不是冷门技术,出了问题网上资料一抓一大把,这对教学型项目的可维护性至关重要。
前端选Vue,看中的是它的渐进式框架特性和组件化开发模式。Vue2到Vue3的生态已经非常成熟,配合Element-UI(或Element Plus)组件库,后台管理类页面的开发效率极高。表格、表单、弹窗、分页这些高频组件开箱即用,再加上Vue Router做前端路由控制,Vuex或Pinia做全局状态管理,一个体验不错的管理后台很快就能搭起来。
对比传统的JSP+Servlet方案,前后端分离最大的优势在开发和部署两个维度。开发阶段,前后端人员可以并行工作,只要提前约定好接口文档,后端写接口、前端写页面互不阻塞;部署阶段,静态资源交给Nginx处理,Java应用单独跑在Tomcat或直接用内置容器,还可以把前后端放在不同服务器上做负载均衡。这个项目的部署也是我特意用Nginx做反向代理来演示的,既符合企业生产实践,答辩时还能多讲一个知识点的深度。
2.2 整体架构分层与模块划分
我习惯在动手写代码之前,先把项目的架构图画在纸上(虽然交付文档里我用了正式的架构图,但本质思路都一样)。这个系统我把它划分成三个端加四个层。
三个端分别是:学生端(H5/PC浏览器)、教师端(PC浏览器)、管理端(PC浏览器)。虽然他们共用同一个前端工程,但通过路由守卫和动态权限菜单区分了可见范围,这比拆成三个独立前端工程要节省大量重复代码。
四个层从下往上依次是:
- 数据存储层:MySQL负责结构化业务数据,Redis负责缓存和分布式会话。
- 后端服务层:SpringBoot应用,按业务域拆分成多个模块,每个模块遵循Controller-Service-Mapper三层架构。
- 接口传输层:RESTful API,统一返回Result对象,包含状态码、消息、数据三段结构。
- 前端展示层:Vue应用,组件化开发,通过Axios发起HTTP请求,Vue Router控制页面导航。
这种分层解决的问题很实际:接口层让前后端解耦,后端内部的模块化让多人协作不冲突。我在开发时,把学生选课、成绩管理、宿舍报修、校园卡消费这些业务拆成独立的功能模块,每个模块的代码边界很清楚,后面扩展新功能(比如加一个在线考试)只需要新增一个模块,不会影响已有功能的稳定性。
2.3 关键依赖与版本选择
版本选型是很多新手最先踩的坑。SpringBoot 2.x和3.x差异巨大,Java版本要求完全不同,Vue2和Vue3的语法和生态也不兼容,一旦版本选错,后面全是报错和兼容性问题。
我这次用的是以下这套组合,稳定性和资料丰富度都在线:
| 技术组件 | 版本选择 | 说明 |
|---|---|---|
| JDK | 1.8 | 企业的基石版本,兼容性最好,市面资料最多 |
| SpringBoot | 2.7.x | 2.x线的最终版本系列,支持长时间维护 |
| MyBatis-Plus | 3.5.x | 增强CRUD效率,分页插件好用 |
| MySQL | 8.0 | 生产常用稳定版,字符集选utf8mb4 |
| Redis | 6.x | 选课缓存和验证码存储 |
| Vue | 2.6.x 或 3.2.x | 二选一,建议Vue3+Element Plus |
| Element UI / Plus | 对应版本 | 后台管理UI框架 |
| Maven | 3.8+ | 依赖管理 |
| Nginx | 1.20+ | 部署静态资源和反向代理 |
关于Vue版本,我多说一句:如果你的毕设想要走稳妥路线,选Vue2+Element UI,组件的坑基本都被前人踩平了;如果你想展示自己有学习新技术的意愿,那选Vue3+Vite+Element Plus,启动速度快,组合式API写起来也舒服,但要注意Element Plus的一些组件用法跟Vue2版差异不小,调试需要时间。我项目里最终选了Vue2,原因很实在:回答答辩问题时,我对Vue2的生命周期、响应式原理更有底气,能讲到源码层面的东西。你选型的时候也要记住,技术栈不是越新越好,能讲明白才是自己的。
3. 核心功能模块与实现细节
3.1 用户认证与权限控制
任何一个校园系统,认证和权限都是第一个要设计的模块。学生、教师、管理员三类角色的权限完全不一样,比如学生只能看到自己的成绩和课表,教师能录入成绩和查看授课班级列表,管理员能管理用户和做数据统计,绝对不能让普通学生请求一个接口就去修改其他人的成绩。
我用Spring Security + JWT实现无状态认证,具体流程是这样的:
- 用户提交用户名密码,后端校验通过后生成JWT令牌,令牌内包含用户ID、角色编码、过期时间等信息。
- 前端拿到令牌后存到localStorage,每次请求在Axios拦截器里把令牌塞进Header的Authorization字段。
- 后端写一个JWT过滤器,拦截所有请求,解析令牌并校验有效性,把用户信息放到SecurityContext里。
- 通过Spring Security的注解,比如@PreAuthorize("hasRole('ADMIN')"),控制接口的角色访问范围。
Redis在这里的用武之地是存储令牌的黑名单和刷新令牌。用户退出登录时,不需要等JWT自然过期,直接把token加入黑名单,Redis设置一个与token剩余时间相同的过期时间,就能实现快速失效。密码存储方面坚决不用MD5,明文更是大忌,要用BCrypt算法加盐散列,Spring Security内置的BCryptPasswordEncoder就能干这个活。
这里有一个很容易被忽视的坑:JWT的密钥不能硬编码写在代码里,一定要放到配置文件里,并且在生产环境通过环境变量注入。我见过不少项目把secret直接写在application.yml里还推到Git仓库,这是严重的安全隐患。我们这个是毕业设计,安全习惯从项目一开始就要养成。
3.2 选课系统的并发处理
选课是校园系统里并发压力最大的场景。到了选课时间,几千个学生同时点选课按钮,如果直接用MySQL串行处理,数据库瞬间就会打满连接,页面报错、超时,直接崩掉。
我的方案是Redis缓存 + 异步落库,核心思路是把压力从数据库前移:
- 选课前:维护一个Redis中的课程剩余名额字段,用Hash结构存储courseStock:{courseId} -> stock。
- 选课瞬间:用Redis的RedisTemplate.opsForHash().increment()做原子扣减,这个操作底层是单个命令执行,天生就是原子的,不会出现超卖问题。扣减成功才允许继续执行后续逻辑。
- 选课结果异步落库:用户点击选课后,先把选课请求发送到消息队列(ActiveMQ或RabbitMQ),接口立即返回"选课中"状态,后端消费者慢慢地从队列里取消息写MySQL,削峰填谷,保护数据库。
- 选课状态查询:前端轮询后端接口,或者用WebSocket推送选课结果。
如果你觉得引入消息队列太重,也可以简化成先用Redis记录选课结果,再通过Spring的@Async异步方法批量把Redis中的数据同步到MySQL,但这种方案要自己做失败补偿,确认数据一致性,比用消息队列更考验功力。学生选课成功率为考核指标时,这个方案能明显展示你对并发问题的理解深度,答辩老师一般都会很感兴趣。
3.3 校园生活的业务闭环
除了教学管理,校园生活模块是这个系统区别于普通成绩管理系统的加分项。我做了校园卡消费记录、宿舍报修、社团活动报名、失物招领四个生活场景。
校园卡消费我设计了一个简单的账户系统:学生卡内余额、消费流水、充值记录。每一次消费都记录消费时间、商户名称、消费金额、消费类型(餐饮/购物/洗浴),前端展示月度消费统计图,可以用ECharts画饼图或柱状图。这模块的业务逻辑不复杂,但涉及多表关联查询和聚合统计,正好考察MySQL的分组查询和日期处理。
宿舍报修则是流程性业务的代表:学生提交报修单(包含宿舍号、故障类型、故障描述、图片上传)-> 系统自动通知宿管员 -> 宿管员派单给维修工 -> 维修工填写维修结果 -> 学生确认完成并评价。这个流程涉及三个角色的协作,状态机设计很清晰:待受理、维修中、已完成、已取消。用前端时间线组件展示状态变化,效果非常直观。
这类业务给你写博文和简历带来的价值是一样的:每个模块都能单独拎出来讲一个完整的故事——需求、设计、实现、问题。与其做十个没有深度的功能,不如把三五个业务流的闭环做深了。
4. 数据库设计与接口规范
4.1 核心数据表设计
数据库设计是很多毕业设计项目的重灾区。常见的问题是:表结构设计不合理,没有外键约束和索引,字段类型乱用,数据量一大就全表扫描。我设计这套系统的核心表时,反复考虑了三范式和应用场景的平衡。
主要数据表包括:
- sys_user:用户表,存储学生、教师、管理员公共字段。
- sys_role 和 sys_user_role:角色表与用户角色关联表,RBAC模型的基础。
- sys_menu:菜单权限表,前端动态生成路由的依据。
- course 和 teacher_course:课程表、教师授课表。
- student_course:学生选课表,含选课时间、成绩、状态字段。
- score:成绩表,记录学生每门课的成绩、绩点、补考信息。
- campus_card:校园卡账户表,与用户一对一。
- card_transaction:校园卡消费流水表。
- repair_order:宿舍报修工单表。
- activity 和 activity_signup:社团活动与报名表。
- notice:校园公告表。
在设计时我给了自己几个约束:所有表必须有主键(自增id)、create_time和update_time字段,让数据可追溯;逻辑外键的字段类型必须与关联表主键一致,比如user_id是bigint,关联表里绝对不能写成int;所有状态字段用tinyint,注释标注清楚每个数值代表的含义;金额字段用decimal(10,2),绝对不用double,避免浮点精度问题。
比如学生选课表和课程表之间,通过course_id关联,还要保证同一个学生同一学期不能重复选择同一门课。这个约束我设计的是联合唯一索引:UNIQUE KEY uk_student_course (student_id, course_id, semester),这个索引同时还能加速按学生查课表的查询速度,一举两得。
这里想特别深聊一下"成绩表需不需要冗余快照"这个问题。刚开始我图省事,成绩表直接关联选课表拿学生和课程信息。后来发现,如果管理员调整了课程名称,或者学生转专业改了班级,历史成绩单上的关联信息就会跟着变,这不符合现实需求——成绩单应该是某个时间点的截图。所以我额外冗余了课程名称、课程编号、任课教师名字段,切断了与业务主表的实时关联。这样设计虽然违反了严格的第三范式,但更符合实际场景,数据不会因为主表变动而"悄悄改历史"。
4.2 RESTful API设计与统一返回格式
前后端分离项目,接口约定的好坏直接影响联调效率。项目开始前,我先把接口的返回格式统一了,定下一个规范:
{ "code": 200, "message": "操作成功", "data": {} }code为200表示成功,401代表未认证,403代表无权限,500代表服务器内部错误。前端Axios在响应拦截器里统一处理code,遇到401跳登录页,遇到403弹无权限提示,遇到500统一弹出错误消息。这样页面代码里基本不需要每个接口都写一遍错误处理逻辑,非常省事。
具体接口的设计,我遵循了RESTful的风格。下面拿课程模块举几个例子:
| 接口 | 请求方式 | 功能 |
|---|---|---|
| /api/course/list | GET | 课程分页列表 |
| /api/course/{id} | GET | 课程详情 |
| /api/course | POST | 新增课程 |
| /api/course | PUT | 修改课程 |
| /api/course/{id} | DELETE | 删除课程 |
| /api/course/select | POST | 学生选课 |
| /api/course/{courseId}/students | GET | 查看某课程的选课学生列表 |
对于接口路径的命名和请求方式,我跟团队(或者如果你是一个人做,也要自己约束自己)约定:查询用GET,新增用POST,修改用PUT,删除用DELETE,语义要清晰。不要出现一个接口又做新增又做修改,客户端只能用"有没有ID"来猜,这种设计在维护阶段非常容易踩雷。
另一个重要约定是分页参数。所有列表接口统一定义为page(当前页)、pageSize(每页条数)、可选的关键字key。后端用MyBatis-Plus的分页插件,前端封装一个统一的usePagination组合式函数或mixin,这样无论哪个页面要写表格分页,代码风格完全一致。
4.3 数据库索引与查询性能优化
校园系统的数据量在演示阶段可能只有几百条数据,看不出来性能问题。但如果答辩老师说"你这个系统选课的时候卡不卡",你得能说出性能优化的思路来。我在设计阶段就做了几项预防性的优化:
第一,所有列表查询涉及的WHERE字段和ORDER BY字段建好索引。比如成绩表,教师端要按课程查学生成绩列表,那么course_id就是高频查询条件,必须建索引。学生端查自己的成绩,student_id也要有索引。
第二,关联查询尽量少用多表JOIN,改用单表查询+内存组装。比如查询课程列表并显示每门课的已选人数,我不会去写一个复杂的JOIN加GROUP BY的SQL,而是先查课程列表,再查选课表按课程分组统计人数,最后在Java代码里组装成VO对象输出。这样SQL简单了,也更容易加缓存。虽然多了一次查询,但数据量不大时性能反而更可控。
第三,热门查询加Redis缓存。比如课程列表和校园公告,数据变动不频繁,加载比较重,在Service层做一层缓存:查询时先读Redis,没有再查MySQL,然后回填Redis并设置过期时间。用Spring的@Cacheable注解能少写很多模板代码,不过要特别留意缓存穿透和缓存雪崩的问题,空值也要缓存,过期时间加随机数打散。
5. 前端Vue实现关键点
5.1 项目结构与动态路由
前端工程我采用Vue CLI创建(如果选Vue3可以用Vite),目录结构按照功能划分:
src/ api/ # 接口请求模块,按业务域拆分 assets/ # 静态资源 components/ # 公共组件 layout/ # 主布局组件(侧边栏、顶部栏、面包屑) router/ # 路由配置 store/ # 全局状态管理 utils/ # 工具函数(request封装、token存储、校验等) views/ # 页面组件这个结构看起来很常规,但真正考验水平的是用动态路由控制权限菜单。后端返回当前用户的菜单列表(从sys_menu表查出来),前端遍历这个列表,用router.addRoutes()(Vue2写法)或router.addRoute()(Vue3写法)动态注册路由。这样学生登录后看不到管理端的菜单,就算手动输入URL,路由守卫也会拦截。
工具类里的request模块我做了统一封装,用Axios实例化,设置了baseURL、超时时间、请求拦截器和响应拦截器:
import axios from 'axios' const service = axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 10000 }) // 请求拦截器:自动携带token service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }, error => Promise.reject(error)) // 响应拦截器:统一处理业务状态码 service.interceptors.response.use(response => { const res = response.data if (res.code === 200) { return res } else if (res.code === 401) { // 跳转登录页 localStorage.removeItem('token') location.href = '/login' return Promise.reject(new Error('登录已过期')) } else { // 弹出错误提示 Message.error(res.message) return Promise.reject(new Error(res.message)) } }, error => Promise.reject(error)) export default service5.2 关键页面的实现难点与避坑经验
整个前端里,有两个页面的实现难度明显高于其他增删改查页面:选课页和成绩统计页。
选课页要实现"剩余名额实时可见"和"选课按钮的防重复点击"。实时余量可以用轮询接口实现,每5秒拉一次课程库存数据,也可以用WebSocket做服务端推送,体验更好但部署复杂度高。我当时的方案是首次进入页面拉全量数据,之后如果用户不刷新页面,就每隔10秒只拉一次所有课程的名额变更状态,前端对比展示。防重复点击不仅在按钮上加了loading状态,还在后端做了幂等校验,同一个学生同一门课只能选一次,双重保险。
成绩统计页需要把数据可视化,我用ECharts做了柱状图和雷达图。一个坑是ECharts的图表在切换tab或窗口大小变化时会变形,需要调用resize方法,而且初始化时DOM可能还没渲染完成。解决办法是用this.$nextTick包一层初始化逻辑,并在页面销毁时移除事件监听。
还有一个所有人都会遇到的坑:跨域问题。开发环境下,前端跑在8080端口,后端跑在8081端口,Axios请求直接跨域。我开发时在vue.config.js里配置了devServer的proxy代理,把所有/api开头的请求转发到后端地址,避免每个请求都带全路径。生产环境也不需要在后端写CORS配置,直接把前端打包后的静态文件扔给Nginx,让Nginx把/api反向代理到Java服务,就没有跨域问题了,这是生产环境的标准做法。
5.3 前端数据状态管理与组件复用
项目里用户的登录状态、个人信息、消息数这类数据,在很多页面都要用到,我放到Vuex/Pinia里管理。用户登录成功后,把用户信息和路由权限列表存到全局状态里,页面刷新时再从后端拉取一次,用Vuex的actions重新加载。很多新手会忽略一个问题:刷新页面后store会重置,如果只存在store里的权限路由丢了,刷新后菜单就消失了。我的解决办法是,刷新时在App.vue的created钩子里调用一个初始化方法,重新获取用户信息和权限,再动态挂载路由。
组件复用方面,我封装了分页表格组件、图片上传组件、状态标签组件、时间线组件,这些组件在宿舍报修、活动管理、公告管理等多个页面反复用。封装组件要遵循一个原则:组件只管展示和交互,不管业务逻辑。父子组件通过props和emit通信,遇到复杂的数据处理就放到页面的mixin或hook里。这样写出来的代码,别人review的时候会觉得你非常有章法。
6. 部署方案与常见问题排查
6.1 从开发到生产的部署全流程
很多人在电脑上跑通了项目,一到部署就发懵。这个系统我完整走了一遍生产环境的部署流程,用的是阿里云服务器(CentOS 7 + 2核4G配置)。大体分四步:
后端部署:我先把SpringBoot项目用Maven打成JAR包,命令很固定:
mvn clean package -DskipTests打包完成后会在target目录生成一个jar包。服务器上安装好JDK 1.8,直接用java -jar启动:
nohup java -jar -Xms512m -Xmx1024m /app/school-system.jar > /app/logs/school.log 2>&1 &nohup和&让应用在后台运行,日志写入日志文件方便排查。生产环境我不会用SpringBoot默认的8080端口跟前端冲突,而是在配置文件里指定server.port=8081,用防火墙或安全组只放行需要对外开放的端口。
MySQL部署:安装MySQL 8.0后,先改root密码,再创建业务数据库和执行初始化SQL脚本。要留一个心眼,使用source命令执行SQL时,脚本里不要写USE database;,而是用mysql -uroot -p school_db < init.sql的方式导入。脚本里如果有中文,最好先在客户端里set names utf8mb4再执行,否则字符集容易乱。
Redis部署:直接yum安装redis,修改redis.conf设置密码和绑定地址。SpringBoot连接Redis的密码要写在application-prod.yml里,用环境变量引用,避免密钥泄漏。
前端部署:
npm run build打包后在dist目录生成静态文件,把dist里的内容上传到服务器的/usr/share/nginx/html目录。Nginx配置里做两个转发:root指向静态文件目录,location /api/ 把请求代理到本地的8081端口。
server { listen 80; server_name your-domain.com; root /usr/share/nginx/html; index index.html; location /api/ { proxy_pass http://127.0.0.1:8081; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 前端路由history模式需要 location / { try_files $uri $uri/ /index.html; } }如果你开发时用的是Hash路由(URL带#号),配置会简单一些;但体验好的History模式必须配try_files,否则刷新非首页就404。这也是我自己踩过的坑,第一次部署刷新页面全白,排查半天发现是Nginx没配置try_files。
6.2 项目生命周期里的高频报错与解决方案
我把整个开发过程中最典型的几个报错整理一下,一个个都是我亲手踩过、排查过的:
第一个,SpringBoot版本和JDK版本不匹配。SpringBoot 2.7默认要求JDK 8以上,3.0以后要求JDK 17以上。如果你电脑装了JDK 8,却引入了SpringBoot 3.x,启动直接报UnsupportedClassVersionError。解决办法是严格对照版本表,或者用IDEA的Spring Initializr生成项目时保持默认的版本组合。
第二个,MyBatis的XML映射文件报Invalid bound statement。这个错误基本可以确定是Mapper接口和XML文件在编译后不在同一个包路径下,或者application.yml里没有配置mapper-locations。我用MyBatis-Plus时,在配置里写上:
mybatis-plus: mapper-locations: classpath*:mapper/**/*Mapper.xml type-aliases-package: com.school.system.entity同时在pom.xml里确保resources标签包含mapper目录的xml文件,不然打JAR包的时候xml会被丢掉。
第三个,前端Vue项目npm install时报各种peer依赖冲突。最常见的场景是用node 18以上版本安装vue2版本的依赖。我不建议硬解冲突,最好用nvm装一个Node 14或16版本再install,瞬间清净。Element UI和node版本的兼容性问题是老生常谈,但每届毕设总有人遇到。
第四个,文件上传后通过URL访问404。这个坑出现在你设置静态资源映射的方式不对。SpringBoot默认只映射classpath下的static目录,如果你把上传文件保存在服务器的/opt/upload目录,就需要写一个WebMvcConfigurer的配置类,做自定义资源映射:
@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceHandler("file:/opt/upload/"); }生产环境我更推荐直接用Nginx的alias或root直接托管upload目录,这样静态资源请求根本不经过Java应用,负载都在Nginx层,效率更高:
location /upload/ { alias /opt/upload/; }遇到问题不要急着百度报错原文,先把日志级别调到DEBUG,mysql和redis连接是否正常也看一下,很多时候只是配置文件里某个地址写错了。
6.3 高并发和异常场景的预案
虽然这是个毕设项目,但把应对异常场景的意识培养起来,对后面找工作、做正式项目特别有好处。我重点做了三个预案:
接口幂等性。除了选课接口,报修、报名、充值这类写操作接口全部支持幂等。简单做法是前端生成一个请求唯一编号(UUID),后端用Redis的setnx判断是否处理过该请求编号,处理过就直接返回上次结果。这样用户连续点击按钮,或者前端超时重试,都不会产生重复数据。
降级方案。当Redis不可用时,选课功能不能彻底瘫痪。我在Service层加了开关配置,如果Redis连接失败就降级到数据库事务控制选课,用悲观锁SELECT ... FOR UPDATE保证不超卖。虽然性能差很多,但至少功能可用。这个降级逻辑也是答辩时可以聊的亮点。
统一异常处理。整个项目用@RestControllerAdvice做了全局异常处理,业务异常、参数校验异常、系统异常分别返回不同的提示信息。防止系统抛出的堆栈信息直接暴露到前端,既不好看也有安全隐患。
7. 开发工具链与协同经验
7.1 接口文档与前端对接的协作方式
前后端分离项目,接口就是双方的"契约",契约越清晰,联调越顺利。我这次用了Apifox管理接口,一个工具就把接口设计、Mock数据、自动化测试全包了。先写接口定义,生成Mock数据,前端拿Mock数据开发页面,后端按同一份文档实现真实接口。前后端并行开发,联调阶段基本不会出现"你说我接口返回了,我说我没收到"的扯皮情况。
接口文档里有一个字段定义非常关键:数据字典。比如性别0男1女,报修状态0待受理1维修中2已完成,这些枚举值不仅后端要用,前端也要在页面上展示成对应文本。我在文档里把每个枚举的取值和含义都标清楚,前端拉取数据后自己转成标签显示,不用硬编码在代码里。
7.2 代码版本管理与提交规范
这个系统我全程用Git做管理,虽然是一个人开发,但分支模型也按照小团队的标准来。main分支保持稳定可运行,develop分支做功能集成,每个功能开一个feature分支,开发完合并回develop。提交信息格式统一:
feat: 新增学生选课功能 fix: 修复选课超卖问题 docs: 更新接口文档清晰的提交历史在写项目报告和答辩PPT时特别有用,你只要翻一下git log,就知道每段代码是什么时候写的、配套文档在哪儿。我自己见过太多人毕设写到一半,代码里全是"update"、“111"、"asdf”这样的提交,后面自己都看不懂,更别说跟导师展示了。
7.3 测试与演示数据的准备
别把测试当应付差事。我体验过最尴尬的答辩场景:演示选课功能时,数据库里就两门课程,还全是测试垃圾数据。所以项目写完,我特意造了一套拟真的演示数据:5个院系、20个教师、200个学生、50门课程、每个学生有和历史选课记录、有成绩、有校园卡消费记录。数据用Java的定时模拟程序生成,批量插入,保证数据时间线是连贯的。
前端演示时,我准备了一套完整的演示路径:管理员登录查看系统首页的统计数据 -> 查看用户管理和权限分配 -> 切换教师账号录入成绩 -> 切换学生账号选课和报修 -> 再回到管理端查看报修处理。整个流程走下来大概10分钟,每个环节都有数据支撑,评审老师看到的是一个"活的系统",而不是一堆空表格。这个演示路径的流畅度,很大程度决定了答辩的印象分。
8. 总结与个人实操心得
项目做到后期,我最大的感受是:智慧校园这个方向本身不新,但把它做得完整、做得有深度,靠的不是一个花哨的功能,而是对技术选型、数据设计、前后端协作、部署运维整个链条的把控。回想我做的最值得的几件事:选课模块坚持用Redis处理并发,让我真正理解了原子性操作的威力;动态权限菜单的设计让我把Spring Security和Vue Router的配合玩明白了;部署阶段踩了一遍Nginx反向代理的坑,现在任何前后端分离项目的部署我基本都能不看文档搞定。
如果你准备照着这个思路做一个类似的系统,我给你的建议是:不要急着写代码,先用一周时间把需求文档、表结构设计、接口文档定下来。设计阶段多花的时间,会在编码阶段成倍地省回来。特别是表结构,一旦上线再改,牵一发动全身,代价极高。
最后再分享一个小技巧:项目里专门写一个DataInitializer的类,在系统启动时自动检测是否需要初始化数据,如果需要就自动建表并插入演示数据。这样不管换到哪台电脑、哪个环境,启动项目就能看到一个有内容的系统,不用每次手动导SQL,不管是自己演示还是给老师看,都非常省心。