校园篮球场地管理系统:SpringBoot+Vue前后端分离项目实战拆解
发布时间:2026/9/12 5:29:11来源:尧图网络
这个校园篮球场地管理系统我前后做过两版。第一版用的还是老套的JSPServlet后端前端糊在一起改一个按钮都要把整个项目翻一遍。后来痛定思痛全改成JavaVueSpringBoot的前后端分离架构开发效率、代码维护、上线部署包括拿去应付毕业设计答辩和项目面试体验完全不一样。这篇文章就把这个项目的完整拆解写出来从技术选型、数据库设计、前后端核心实现到部署踩坑和面试考点一次性讲清楚想拿这个项目练手或者写进简历的可以直接照着走。1. 项目整体设计与技术选型拆解1.1 这个系统到底要解决什么问题校园篮球场地的预约在很多学校里其实还是靠线下登记或者微信群接龙。场地有限想打球的老师和学生又多撞场、插队、占着茅坑不拉屎的情况特别普遍。管理员拿着一本纸质登记表也没法判断哪个时段已经被预约只能靠肉眼翻效率特别低。这个项目的核心价值就是把“场地信息展示、在线预约、订单管理、管理员审核”这套流程线上化。具体来说系统里主要有两类角色普通用户学生/老师注册登录、浏览场地信息、查看场地空闲时段、提交预约申请、查看自己的预约记录、取消预约。管理员管理场地信息新增、编辑、上下架、处理预约订单审核或标记完成、发布公告、查看用户列表等。如果你要拿这个项目做毕业设计或者个人项目功能边界就控制在这个范围不用再加一些花里胡哨的会员等级、积分商城那些内容既偏离“场地管理”的核心又会给自己挖一堆实现上的坑。1.2 为什么是SpringBootVue这套组合先说结论这套组合已经是当前中小型Web项目里最主流的搭配之一也是最容易找到现成资料、遇到问题最容易搜到答案的搭配。SpringBoot解决的问题是后端开发效率。它对Spring生态做了一层自动配置以前需要在XML里写一大堆Bean定义现在一个SpringBootApplication注解加几行配置就能跑起来。内嵌Tomcat打成一个jar包就能直接执行部署也不用再装外置容器。对于校园场地预约这种业务逻辑不算复杂的系统SpringBoot的快速开发优势非常明显。Vue解决的是前端交互体验。传统JSP页面每个操作都要刷新整个页面而Vue的组件化和响应式设计让场地列表、预约时间选择、订单状态更新这些交互可以像桌面应用一样丝滑。Vue的学习曲线也相对平缓会HTML、CSS、JavaScript基础的同学看几天官方文档就能上手。至于为什么不用更重的微服务架构或者更复杂的权限框架道理很简单杀鸡别用牛刀。一个校园场景的场地管理系统单机部署、单体应用完全能扛住几千人的并发预约。架构越复杂学习成本和部署成本越高对课设、找工作的项目展示反而不利。1.3 整体架构与开发流程这个项目走的是标准的前后端分离架构数据流向可以概括为浏览器Vue页面 - 发送HTTP请求 - SpringBoot后端Controller接收参数 - Service处理业务逻辑 - Mapper操作MySQL数据库 - 数据原路返回 - 前端渲染页面从开发部署的角度系统分成三个独立的部分各自可以单独开发和升级部分技术组件职责前端Vue 3 Vue Router Pinia Element Plus Axios页面展示、用户交互、路由控制后端SpringBoot 2.7 / 3.x MyBatis-Plus Spring Security或JWT Hutool业务逻辑、数据校验、权限认证、接口提供数据层MySQL 8.x Redis可选持久化存储、缓存热点数据这里有个经验如果只是做课设SpringBoot用2.7版本就行稳定、教程多JDK8就能跑。如果是新项目或者想体现自己关注新版本可以用SpringBoot 3.x但注意JDK必须17以上而且很多老版本的第三方库不兼容SpringBoot 3遇到问题要有心理准备。2. 数据库设计从ER图到表结构落地数据库设计是很多同学容易忽略但面试官一定会问的环节。我建议在写任何业务代码之前先画一张ER图把实体和关系理清楚。这个项目核心实体就这几个用户、场地、预约订单、公告。我个人画ER图不喜欢用太复杂的工具直接用ProcessOn画实体框标清楚主外键关系就行。最简单也最实用的规则是一张表对应一个业务实体表与表之间通过外键字段建立关联核心业务表之间不要直接做多对多用中间表拆开。2.1 用户与角色设计细节用户表是所有业务的基础。先看建表SQLCREATE TABLE user ( id bigint NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录账号, password varchar(100) NOT NULL COMMENT 密码BCrypt加密, nickname varchar(50) DEFAULT NULL COMMENT 昵称, phone varchar(20) DEFAULT NULL COMMENT 手机号, role tinyint NOT NULL DEFAULT 1 COMMENT 角色0-管理员 1-普通用户, avatar varchar(255) DEFAULT NULL COMMENT 头像地址, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;几个细节说明一下role用tinyint而不是字符串。数据库里存整数性能好、占空间小业务展示层再转换成“管理员/普通用户”就行。密码绝对不存明文。用Spring Security自带的BCryptPasswordEncoder加密同一个密码每次加密结果都不同安全性高很多。username加唯一索引。防止注册时重复账号这属于最基础的防御性设计。时间字段统一用datetime并且让数据库自动维护create_time和update_time代码里不用手动去set减少低级错误。2.2 场地与预约核心表场地表相对独立存的是场地的静态信息CREATE TABLE court ( id bigint NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL COMMENT 场地名称如一号篮球场, location varchar(255) DEFAULT NULL COMMENT 位置描述, type tinyint DEFAULT 1 COMMENT 场地类型1-室外 2-室内, price decimal(10,2) DEFAULT 0.00 COMMENT 每小时价格, image varchar(255) DEFAULT NULL COMMENT 场地图片URL, description text COMMENT 场地介绍, status tinyint DEFAULT 1 COMMENT 状态1-开放预约 0-停用, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT篮球场地表;预约订单表是整个系统的核心也是数据关系最复杂的一张表CREATE TABLE booking_order ( id bigint NOT NULL AUTO_INCREMENT, order_no varchar(32) DEFAULT NULL COMMENT 订单编号, user_id bigint NOT NULL COMMENT 预约用户ID, court_id bigint NOT NULL COMMENT 场地ID, book_date date NOT NULL COMMENT 预约日期, time_slot tinyint NOT NULL COMMENT 时段编号1-8对应8:00-22:00每两小时一段, status tinyint NOT NULL DEFAULT 0 COMMENT 状态0-待确认 1-已确认 2-已取消 3-已完成, remark varchar(255) DEFAULT NULL COMMENT 备注, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_court_date_slot (court_id, book_date, time_slot) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT预约订单表;这里最关键的是一行联合唯一索引uk_court_date_slot。它的作用后面讲并发时会详细说先记住一个原则凡是“同一个资源同一个时间只能被一个人占用”的业务必须用唯一索引作为兜底防线。预约状态我用status字段维护取值范围提前定好代码里用常量或者枚举表示不要魔法数字满天飞。这个状态机比较简单用户提交预约 - 0待确认管理员确认 - 1已确认用户或管理员取消 - 2已取消场地使用完毕 - 3已完成2.3 数据一致性与并发方案这是我面试时最常问候选人的点如果两个用户同时预约同一个场地同一个时段你怎么办很多人的第一反应是“先用select查一下有没有记录没有就插入”。这个方案在低并发下确实能用但在真正并发场景下会出事。两个请求同时查都发现没有记录同时执行插入最终就会产生两条重复订单。正确做法是分三层防护第一层是数据库唯一索引兜底。上面那张预约表已经加了court_id, book_date, time_slot联合唯一索引就算代码逻辑全是漏洞数据库层面最多只能插入一条另一条会直接报DuplicateKey异常。第二层是应用层用条件更新做原子操作。如果业务需要比如要给场地库存扣减可以写成UPDATE court SET stock stock - 1 WHERE id #{courtId} AND stock 0然后判断受影响行数如果为0说明没抢到。这种把“检查更新”放在一条SQL里的方式天然是原子操作不需要额外加锁。第三层是事务Spring传播行为兜底。预约订单、可能的支付记录、场地状态变更如果跨了多张表就在Service方法上加Transactional(rollbackFor Exception.class)保证任何一个环节失败全部回滚。3. 后端核心模块实现SpringBoot3.1 项目脚手架与目录组织我习惯用IDEA的Spring Initializr创建项目选好Spring Web、MySQL驱动、MyBatis-Plus、Lombok这几个基础依赖。JDK版本如果不是刚需用JDK8SpringBoot 2.7的组合最稳依赖版本不容易打架。创建完项目后我通常会把后端目录整理成这样的结构com.example.court ├── common # 通用类统一返回结果、异常处理、常量 ├── config # 配置类跨域、拦截器、MyBatis-Plus分页插件 ├── controller # 控制层接收请求 ├── entity # 实体类 ├── mapper # MyBatis-Plus的Mapper接口 ├── service # 业务逻辑层 ├── util # 工具类JWT工具、日期工具 └── CourtApplication.java很多同学习惯把Controller写得特别胖逻辑全往里面塞。我建议遵守一个原则**Controller只负责参数接收和结果返回真正的业务判断全部放Service层。**这样写出来的代码既好测又好看面试官看项目代码时也会有好印象。application.yml里最常用的配置直接给你一份能跑的版本server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/court_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true global-config: db-config: id-type: automap-underscore-to-camel-case一定要开这样数据库的create_time才能自动映射成实体的createTime少写一堆配置。MyBatis-Plus的SQL日志建议开发阶段打开排查问题特别有用上线前再关掉。3.2 JWT登录认证与拦截器登录认证这块我建议用JWT拦截器的方式比直接上Spring Security更直观也更容易给面试官讲清楚原理。JWT的结构用一句话概括Header.Payload.Signature通过签名保证token内容在传输过程中没被篡改。用户登录成功后后端生成一个包含用户ID和角色的token返回给前端前端每次请求在请求头里带上Authorization: Bearer token后端拦截器统一校验。工具类核心方法就两个public class JwtUtil { private static final String SECRET your-secret-key; private static final long EXPIRE 1000 * 60 * 60 * 24 * 7; // 7天 public static String generateToken(Long userId, Integer role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(role, role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() EXPIRE)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); } }拦截器做的事有三个从请求头取token、解析token、把用户信息放到ThreadLocal里供后续使用。注册拦截器时注意设置白名单登录、注册、场地查询这些接口不拦截其余接口一律要校验。注意SECRET不要硬编码在代码里更不要推到Git仓库。实际开发中放到环境变量或者配置中心项目演示阶段也要养成好习惯。权限控制上因为角色只有管理员和普通用户两种可以在拦截器里解析出role后简单判断也可以在Controller方法上用自定义RequireAdmin注解。我个人更倾向写一个简单的注解AOP体现对项目设计的思考。3.3 场地预约与防冲突下单实现预约模块是业务逻辑最密集的地方我直接给你看核心Service的写法Override Transactional(rollbackFor Exception.class) public Result createOrder(BookingOrderDto dto, Long userId) { // 1. 校验场地是否存在且开放 Court court courtMapper.selectById(dto.getCourtId()); if (court null || court.getStatus() 0) { return Result.error(场地不存在或未开放); } // 2. 校验预约时间是否合法不早于今天、时段在1-8范围内 if (dto.getBookDate().isBefore(LocalDate.now()) || dto.getTimeSlot() 1 || dto.getTimeSlot() 8) { return Result.error(预约时间不合法); } // 3. 插入订单唯一索引兜底防冲突 BookingOrder order new BookingOrder(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setCourtId(dto.getCourtId()); order.setBookDate(dto.getBookDate()); order.setTimeSlot(dto.getTimeSlot()); order.setStatus(0); try { bookingOrderMapper.insert(order); } catch (DuplicateKeyException e) { return Result.error(该时段已被预约请选择其他时间); } return Result.success(预约成功等待管理员确认); }这段代码的关键就一个点**把并发冲突的检测交给数据库唯一索引代码通过捕获DuplicateKeyException转换成友好的业务提示。**如果不加这个兜底你就要写select count(*)insert两步操作中间隔一个时间窗口就可能产生脏数据。加Transactional是为了后续业务扩展比如扣减场地库存、发送通知万一后面加逻辑也不至于忘记补事务。订单编号生成我一般用时间戳随机数用户ID后四位格式如20250621143000xxxx唯一且不用查库。3.4 其他业务模块实现要点场地的增删改查直接用MyBatis-Plus的IService接口就够了不需要额外写SQL。但有一个点需要注意管理员删除场地时如果这个场地已经存在未完成的预约订单会出现外键逻辑矛盾。我的做法是字段标记上下架不做物理删除简单又安全。公告管理模块非常简单一张公告表字段就标题、内容、发布时间前端首页展示最新一条即可适合当作练手模块。还有一个容易被忽略的是全局异常处理和统一返回结果。我一般会定义一个Result类统一返回code, message, data三个字段再写一个RestControllerAdvice全局异常处理器把SQL异常、参数校验异常等统一包装前端只认这一种JSON格式联调效率高很多。安全提醒SpringBoot如果开启了Actuator监控/actuator/heapdump等端点在某些版本下可能暴露堆内存信息里面有密码等敏感数据。要么不引入Actuator依赖要么在配置里只暴露health端点并且加上访问控制这类漏洞在真实场景里可是被漏洞扫描工具重点盯着的。4. 前端核心模块实现Vue4.1 Vue环境搭建与项目创建前端环境准备这步问的人最多。Node.js版本别用太老的建议16或18以上然后用npm换一个国内镜像源不然装依赖能装到你怀疑人生。项目创建我建议用Vite命令很简单npm create vitelatest court-web -- --template vue cd court-web npm install npm run devVite启动速度快热更新体验好已经是当前Vue生态的默认选择。Vue CLI那套vue create的方式虽然还能用但官方和新项目基本都转向Vite了别再学老教程了。装依赖的时候可以顺手把项目里要用的库一起装上npm install vue-router4 pinia element-plus axios如果你用Element Plus记得全量引入还是按需引入这两个选择要提前定好。课设和面试项目直接用全量引入最简单省事在main.js里加两行就行import ElementPlus from element-plus import element-plus/dist/index.css app.use(ElementPlus)4.2 路由配置与登录态控制Vue Router在项目里负责页面跳转我这里给一个简化的路由配置思路const routes [ { path: /login, component: () import(/views/Login.vue), meta: { public: true } }, { path: /, component: () import(/layout/Layout.vue), redirect: /home, children: [ { path: /home, component: () import(/views/Home.vue), meta: { title: 首页 } }, { path: /courts, component: () import(/views/CourtList.vue), meta: { title: 场地预约 } }, { path: /orders, component: () import(/views/MyOrders.vue), meta: { title: 我的预约 } }, { path: /admin/courts, component: () import(/views/admin/CourtManage.vue), meta: { title: 场地管理, role: admin } }, ] } ]路由用() import()这种懒加载写法打包后会自动拆成多个js文件首屏加载速度会好很多。登录态控制靠全局前置守卫逻辑很直白router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.public) { next() } else if (!token) { next(/login) } else if (to.meta.role admin localStorage.getItem(role) ! 0) { next(/) } else { next() } })没有token就去登录页有token但不是管理员就不能进管理页面。这套逻辑基本能覆盖所有权限跳转场景。4.3 页面开发场地预约与订单管理场地列表页是这个项目最核心的页面我一般用卡片布局每个场地一张卡片展示场地名称、位置、类型、价格和图片加上一个“立即预约”按钮。预约交互设计要特别注意用户点击预约后弹出一个Dialog里面有两个关键选择——日期和时段。时段这里我是做成了el-radio-group1到8一共八个时段每个时段标签上显示“可预约”或者“已满”。这个“可预约”状态怎么来最简单的方式是后端提供一个接口传入场地ID和日期返回该日期已经被预约的时段集合前端过滤一下就能展示。这种设计比一次性加载所有时段的可用状态更实用数据量小、接口简单、逻辑清晰。订单管理页面就是一张表格用el-table展示订单编号、场地名称、预约日期、时段、状态等字段状态用el-tag展示不同颜色已确认是绿色、待确认是橙色、已取消是灰色、已完成是蓝色。管理员在后台订单页还能看到“确认订单”和“取消订单”的操作按钮。整个前端逻辑总结下来就是表单收集数据接口提交数据表格展示数据状态标签区分数据。4.4 axios封装与接口对接前后端联调时最烦的问题是代码里到处写重复的axios请求token要手动加错误要挨个处理。我习惯在src/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 }) // 响应拦截器统一处理错误码 request.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res }, error { if (error.response?.status 401) { localStorage.removeItem(token) router.push(/login) } ElMessage.error(网络异常) return Promise.reject(error) } ) export default request统一封好之后业务代码里一行request.get(/courts)就能发请求token、错误处理、状态码判断全都自动完成。开发阶段的跨域问题不用后端配置CORS更不要用浏览器插件最省心的方式是走Vite的devServer代理。在vite.config.js里加server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样前端代码里的/api/courts请求开发环境下会被自动转发到后端8080端口完全绕开跨域限制前后端各做各的互不干扰。5. 部署、常见问题排查与面试考点总结5.1 本地打包与服务器部署项目做完肯定要部署上线哪怕是部署到自己电脑上访问也是完整流程。后端打包部署很简单mvn clean package -DskipTests java -jar target/court-system.jar前端打包先执行npm run build默认会在dist目录生成一堆静态文件。如果你想本地预览打包后的效果可以npm run preview先看看。生产环境的部署我推荐用Nginx做反向代理把前端静态文件和后端接口统一成一个域名入口。Nginx配置核心就两块server { listen 80; server_name your-domain.com; # 前端静态文件 location / { root /var/www/court-web/dist; 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; } }try_files那一行特别关键它能解决Vue Router的history模式下刷新页面404的问题。如果没有这一行用户访问/orders页面时刷新Nginx会找不到对应文件直接返回404。5.2 高频问题速查表我整理了这个项目里最容易踩的坑里面有不少是我自己实操时踩过的直接做成表格方便你检索现象原因解决办法前端请求后端接口报跨域前后端端口不同开发环境用Vite proxy生产环境用Nginx反向代理npm install报各种依赖错误Node版本太高或太低使用Node 16/18 LTS版本删除node_modules和package-lock.json重装页面点击没有反应控制台报“Cannot read properties of undefined”后端返回数据结构不对data字段层级取错先用浏览器Network看接口真实返回再调整取值登录接口报401token未携带或过期检查请求拦截器是否设置Authorization头Vue项目打包后打开一片空白base路径配置错误资源变成绝对路径vite.config.js里设置base: ./history模式刷新404Nginx没配置try_files按上面Nginx配置补上MyBatis-Plus查询时createTime为null驼峰映射没开启application.yml配置map-underscore-to-camel-case: true端口8080被占用其他进程占用了端口换端口或kill -9占用的进程连续快速点击预约出现两条相同订单并发冲突数据库加联合唯一索引代码从Bug层面解决上传场地图片后无法显示静态资源路径没映射配置WebMvcConfigurer把上传目录映射成访问路径5.3 面试考点梳理从项目到八股文做完这个项目面试官几乎必问的几个点提前准备绝对不吃亏SpringBoot方面自动配置原理是什么为什么加一个starter依赖就能自动创建Bean你在项目里用了哪些starter底层做了什么这个项目启动流程是什么这些属于SpringBoot面试题里最高频的内容建议把SpringBootApplication背后EnableAutoConfiguration的加载逻辑背熟。Java基础方面项目里哪里用到Java8特性为什么用LocalDate不用DateTransactional底层是动态代理实现的你能解释吗这里顺带会把Java动态代理、反射、异常体系一起问了属于java面试八股文里绕不开的部分。Vue方面Vue的响应式原理是什么computed和watch什么区别路由守卫有哪些组件通信方式有哪些Vue面试题的高频内容基本就这些把这个项目的代码过一遍回答起来就有例子可以讲。项目深挖方面预约冲突你怎么解决用户量大了怎么办JWT和Session有什么区别数据库为什么用B树这些问题没有标准答案核心考察你是不是真的做过以及有没有思考过项目的边界和瓶颈。我建议用STAR法则来介绍项目场景校园场地管理混乱、任务实现线上预约和后台管理、行动选了什么技术方案、数据库怎么设计、前后端怎么分工、结果提效多少、解决了什么痛点。尽量说真实数据比如“原来人工登记需要3分钟现在线上预约10秒完成”这种表述比空洞的“提高了效率”有说服力得多。5.4 项目二次扩展方向如果时间和精力允许这几个方向可以给项目加分对接在线支付微信/支付宝模拟把预约支付整理成闭环推荐单里加一个“待支付”状态。WebSocket实时通知管理员确认订单后用户页面能实时收到通知不用刷新。Redis缓存把场地列表、热门时段的预约情况缓存到Redis减轻数据库压力顺带能讲缓存穿透、击穿、雪崩这些高频面试点。小程序端Vue3的语法迁移到uni-app开发小程序成本很低能体现跨端能力。运营数据大屏用ECharts展示每天预约量、热门场地Top5、高峰时段分析这个对进“数据分析”相关岗位的同学很有用。最后再分享一点个人体会。这个校园篮球场地管理系统虽然业务不复杂但它覆盖了一个完整Web项目的全流程需求分析、数据库设计、后端接口、前端交互、权限认证、部署上线。我当时就是用这个项目把SpringBoot和Vue的知识点真正串起来的之前看教程总觉得看懂了真正从零写一遍代码才发现很多细节完全没学到比如并发冲突、跨域、代理、打包路径每一个坑都是实打实踩出来的。做项目的过程中比代码更重要的是建立起的工程思维先设计后开发、先数据库后接口、先接口后页面。这套顺序一旦养成后面你再接触再复杂的系统都不会慌。所以如果你正卡在“只会增删改查、不知道做什么项目”的阶段直接拿这个系统下手就行功能清晰、技术栈主流、扩展空间够大。
网站建设高端定制企业官网