毕设学校体育场管理系统的设计与实现:新手入门实战指南
摘要:许多计算机专业学生在毕业设计中面临业务逻辑不清、技术栈选型混乱、系统耦合度高等问题。本文以“毕设学校体育场管理系统的设计与实现”为案例,从零开始讲解如何基于 Spring Boot + MyBatis + Vue 构建一个高内聚低耦合的 Web 应用。涵盖场地预约、冲突检测、用户权限控制等核心功能,并提供可运行的代码结构与数据库设计。读者将掌握 MVC 分层实践、RESTful API 设计规范及前后端联调技巧,显著提升毕设开发效率与代码质量。
一、先吐槽:毕设里那些绕不过去的坑
做毕设最怕什么?不是写不出代码,而是“写了一大堆,最后连自己都不想看”。我总结了三条高频痛点,看看你有没有中招:
- 需求拍脑袋:老师一句“做个预约系统”,结果连“允不允许重复预约”都没说清楚,写到一半才发现逻辑全崩。
- 技术大拼盘:听说微服务火,就把 Spring Cloud 全家桶怼进去;听说 React 酷,又把前端脚手架换成 Next.js。最后服务器 2G 内存,连 IDEA 都跑不动。
- 代码能跑就行:一个
Controller里写 500 行 SQL,一个jsp里嵌 7 层if,答辩时老师问“如果两个同学同时预约最后一块场地,会咋样?”——当场社死。
如果你也踩过类似的坑,下面的实战路线或许能救你一把。
二、技术选型:别被“网红”框架带节奏
毕设不是双十一,稳定能跑才是硬道理。我对比了四组常见方案,结论直接给:
| 技术方向 | 候选 | 推荐指数 | 理由 |
|---|---|---|---|
| 后端 | Spring Boot 2.7 | ★★★★☆ | 生态成熟,IDE 支持好,出活快;注解驱动,新手易读。 |
| 后端 | Django 4.x | ★★★☆☆ | 开发快,但 Python 部署对本科生略陌生,服务器装环境易翻车。 |
| 前端 | Vue3 + Vite | ★★★★☆ | 模板语法贴近 HTML,单文件组件易拆分,导师一眼能看懂。 |
| 前端 | React 18 | ★★★☆☆ | 函数式+Hooks 学习曲线陡,状态管理一乱就炸。 |
结论:Spring Boot + MyBatis-Plus + Vue3 + MySQL8 是最低成本“能跑+能写+能讲”组合,下文所有示例均基于此。
三、总体架构:一张图先建立共识
- 前端:Vue-Router 负责页面跳转,Axios 统一拦截器封装 REST 请求。
- 网关:Spring Security 做 JWT 登录鉴权,全局异常统一返回 JSON。
- 服务层:MyBatis-Plus 提供通用 Mapper,业务代码只写扩展 SQL。
- 数据层:MySQL 8.0,事务交给 Spring 声明式,一行注解搞定。
四、数据库设计:提前消灭“并发冲突”
别急着敲代码,先把 ERD 画明白。核心就三张表,其余字典表按需扩展。
- 用户表
userid/student_no/password/role(admin/user)
- 场地表
fieldid/name/type(篮球/足球…) /open_at/close_at
- 预约表
bookingid/user_id/field_id/start_time/end_time/status(预约成功/已取消)
关键约束:
- 联合唯一索引
(field_id, start_time)保证同一时段只能有一条“成功”预约。 - 取消记录逻辑删除,不物理删,方便后续对账。
五、后端核心模块拆解
下面给出 4 个最常见“必考”功能点的实现思路 + 关键代码,每段都带注释,CV 即可用。
1. 用户认证:JWT + RefreshToken 双缓存
Spring Security 配置太长,这里只贴核心过滤器,重点看注释:
public class JwtFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String token = resolveToken(request); if (token != null && jwtUtil.validate(token)) { // 从 Redis 拿用户信息,避免每次查库 LoginUser loginUser = redisCache.get( RedisKey.access(token), LoginUser.class); if (loginUser != null) { UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken( loginUser, null, loginUser.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(auth); } } chain.doFilter(request, response); } }幂等性:登录接口/api/login对同一学号并发请求,JWT 签发前用SETNX锁 3 秒,防止重复生成 Token。
2. 场地列表:分页 + 多条件查询
MyBatis-Plus 的LambdaQueryChainWrapper是真香:
public IPage<FieldVo> pageField(FieldPageDTO dto) { return fieldMapper.selectPage( new Page<>(dto.getCurrent(), dto.getSize()), new LambdaQueryWrapper<Field>() .like(StringUtils.isNotBlank(dto.getName()), Field::getName, dto.getName()) .eq(dto.getType() != null, Field::getType, dto.getType()) .orderByAsc(Field::getId) ).convert(FieldVo::fromPo); }避坑:不要把Page对象直接返回给前端,字段太多会暴露总记录数,统一用 VO 再包一层。
3. 预约调度:并发竞争 + 幂等性
最容易翻车的就是“最后一块场地”。采用“先插后判”策略:
- 前端点击预约 → 后端收到
fieldId + startTime - 直接执行
INSERT … ON DUPLICATE KEY UPDATE - 若影响行数 = 1,说明抢占成功;= 2 则唯一索引冲突,返回“已被预约”
@Transactional public R<Void> book(BookDTO dto)现 { Booking po = new Booking(); po.setUserId(UserContext.getUserId()); po.setFieldId(dto.getFieldId()); po.setStartTime(dto.getStartTime()); po.setEndTime(dto.getEndTime()); po.setStatus(BookingStatus.SUCCESS); // 利用唯一索引兜底 int row = bookingMapper.insert(po); return row == 1 ? R.ok() : R.fail("该时段已被预约"); }并发测试:用 JMeter 200 线程同时请求,数据库层唯一索引保证最终一致性,应用层无需分布式锁。
4. 取消预约:软删除 + 乐观锁
取消不是DELETE,而是UPDATE status = CANCELLED。同时加版本号防止重复提交:
@Transactional public R<Void> cancel(Long bookingId) { Booking b = bookingMapper.selectById(bookingId); if (b == null || !b.getUserId().equals(UserContext.getUserId())) { return R.fail("无权取消"); } LambdaUpdateWrapper<Booking> wrapper = new LambdaUpdateWrapper<>(); wrapper.eq(Booking::getId, bookingId) .eq(Booking::getStatus, BookingStatus.SUCCESS) .set(Booking::getStatus, BookingStatus.CANCELLED); return bookingMapper.update(null, wrapper) == 1 ? R.ok() : R.fail("取消失败"); }六、前端必会:Vue3 组合式 API 示例
下面给出“预约时间选择”组件的核心片段,展示如何一次性把日期 + 时段传给后端,避免多次请求。
<template> <el-form :model="form"> <el-date-picker v-model="form.date" value-format="YYYY-MM-DD"/> <el-time-select v-model="form.timeRange" is-range format="HH:mm"/> </el-form> <el-button @click="submit">提交预约</el-button> </template> <script setup> import { reactive } fromvue'; import axios from '@/utils/request'; const form = reactive({ date: '', timeRange: [] }); async function submit() { const [start, end] = form.timeRange; await axios.post('/api/booking', { fieldId: props.fieldId, startTime: `${form.date} ${start}:00`, endTime: `${form.date} ${end}:00` }); ElMessage.success('预约成功'); } </script>跨域联调:本地用 Vite 代理/api到localhost:8080,一行配置搞定,无需 CORS 注解:
// vite.config.js server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }七、安全加固:别让“小系统”成“大漏洞”
- SQL 注入:MyBatis-Plus 的
${}与#{}千万别混用,凡是外部参数一律#{}预编译。 - 会话安全:JWT 只存学号与角色,不存密码;密钥长度 256 bit,定期轮换。
- 水平越权:所有“用户级”接口必须带
userId = @CurrentUser,并在 SQL 里再判一次,防止接口串改。 - 敏感操作日志:预约、取消、结算全部写
operate_log表,保留 180 天,老师最爱看审计功能。
八、生产环境避坑指南
- 冷启动优化
Spring Boot 2.7 默认开启devtools,打包时一定exclude,否则服务器 1C2G 启动 60s+。 - 事务边界误用
在select上标@Transactional会加长锁时间,读多写少的业务只在insert/update方法加事务。 - 时间戳时区陷阱
服务器 CST、MySQL system_time_zone 不一致会导致“预约成功却查不到”。统一用UTC+0存,前端按浏览器本地时区展示。 - 日志级别
生产环境root: INFO即可,MyBatis SQL 打印记得关,否则 1k 并发能把磁盘打满。
九、可继续扩展的“加分项”
- 短信通知:接入阿里云/腾讯云,预约成功即发送“时间+场地号”,老师直呼专业。
- 日历视图:用 FullCalendar 组件,把预约数据渲染成色块,一眼看出空档。
- 微信小程序:把 Vue 页面直接塞进 UniApp,打包发微信,秒变“互联网+”项目。
十、写在最后
整个系统从 0 到答辩版,我大概用了 4 周,每天 3 小时。最费时的不是写代码,而是“想清楚到底要解决什么问题”。一旦把需求、边界、异常都想明白,代码只是翻译工作。希望这份“踩坑笔记”能让你少走一点弯路,把省下来的时间拿去刷剧、打球、谈恋爱,毕业快乐!