☰
SpringBoot+Vue3实现公共运动场地预约管理系统实战解析
2026/10/1 11:03:35 网站建设 项目流程

1. 场地预约这件事,痛点比想象中多得多

先聊聊我为什么会对"公共运动场地预约管理系统"这个选题上心。

之前帮单位团委做过一版内部场地预约小工具,当时的情况就是典型的"球场混乱现场":乒乓球室永远有人占着却不知道是谁约的,羽毛球场靠微信群接龙,篮球场全靠谁来得早谁先打,周末热门时段全凭嗓门大。预约靠人肉、改期靠电话、爽约靠脸皮厚,管理基本靠人工统计和现场协调——这还只是个几十人的单位。放到高校、社区或全民健身中心,场地多、时段多、用户规模大,人工管理直接失控。

所以这个项目的核心价值就一句话:用一个前后端分离的系统,把"场地资源-用户预约-使用核销"这条链路规范化、在线化。系统要管的不是"系统本身",而是"时间"和"场地"这两个有限资源怎么分配。

这个项目选型为 SpringBoot + Vue3 很典型,也是目前Java方向毕设和企业内部系统里最常见的组合之一。如果你是准备做毕业设计,或者想在简历上写一个完整的管理系统项目,这个题目可以覆盖的技术点非常扎实:SpringBoot后端、MyBatis-Plus操作数据库、JWT登录鉴权、Vue3组合式API、Element Plus组件库、前后端联调、Nginx部署——整套供应链下来,是能撑住面试提问的。这篇文章,我就按实际做这类系统的常见流程,把从需求拆解到核心实现、从踩坑到部署的完整链路展开讲一遍。

2. 需求拆解:场地预约的单子,本质上是一个"资源调度系统"

2.1 三种角色与核心流程

别看项目名叫"预约管理系统",它跟一般的CRUD管理系统差的不是一点半点。普通的用户管理、公告管理都是辅助,真正的核心是预约流程的状态流转。

按常见的需求做法,系统里会有三类角色参与动作:

  • 普通用户:查看场地列表和可约时段,发起预约申请,查看"我的预约",取消未开始的预约。
  • 管理员:维护场地信息(新增场地、关闭维修、调整开放时段),查看全部预约记录,处理异常预约(比如用户爽约后的标记),以及简单的内容管理(公告、场地类型)。
  • 系统本身:核心职责是两件事——判断某个时段是否可约,以及防止同一时段被不同人重复占用。

业务流程大致如下:用户选定场地与日期,系统加载该场地在当日的"时段格子",用户选择一个完整时段提交预约,后端做冲突校验,通过后生成预约记录,状态为"已确认"(有审核需求时也可以先"待审核"再"已确认")。用户实际到场使用,管理员可以标记为"已使用"或系统按时间自动流转"已完成";用户在开场前可以主动取消,否则迟到太久或爽约会被记录。

这里我需要特别提醒一句:如果这是毕业设计,建议把状态机画清楚。预约状态至少要包含:待确认/已确认/已取消/已完成/爽约。状态流转规则在答辩时几乎必问,属于业务设计功底的直接体现。

2.2 为什么是SpringBoot+Vue3,而不是其他方案

这个选型很多人会有疑问——SpringBoot已经不算新,Vue3也已经稳定了很久,为什么还推荐这个组合?

我的观点是:这个项目想看重的不是新技术,而是工程化成熟度和知识覆盖面。SpringBoot的自动装配机制、starter生态、内嵌Tomcat这些特性,让开发效率极高,而且企业里存量最多的Java项目基本都是这个路线;Vue3现在已经是前端主流,组合式API配合Element Plus做管理端是绝对的主流玩法,招聘市场也认这个技术栈。相对地,如果换成Go+React,路由、鉴权、部署的很多细节就得自己重新趟一遍,对以"完成一个可用系统"为目标的场景来说性价比不高。

技术栈清单如下:

模块选型说明
后端框架Spring Boot 2.7.x稳定、资料多,JDK8/11通用
持久层MyBatis-Plus单表CRUD免写SQL,分页、条件查询都很方便
数据库MySQL 5.7/8.0经典组合
鉴权JWT +拦截器无状态登录方案
前端框架Vue3 + ViteVite启动快,组合式API编码
组件库Element Plus表单、日期选择器、弹窗日历组件一应俱全
状态管理PiniaVue3官方推荐替代Vuex,TS友好
HTTP请求Axios配合拦截器统一处理token和错误码
部署Nginx + jar包前后端分离的标准部署模式

3. 数据结构设计:5张核心表把业务规则焊死在字段上

3.1 表结构如何规划

做管理系统,表结构设计一定要走在代码前面。我把这个项目最核心的表拆成5张,覆盖主体业务:

用户表(user):id、username、password(BCrypt加密存储)、nickname、phone、role(USER/ADMIN)。这算是标准款,需要单独提取的是角色字段,建议用tinyint存数字,比如0-普通用户、1-管理员,方便扩展。

场地表(venue):id、name、type(篮球/羽毛球/乒乓球等)、location、description、cover_url、status(1-开放,0-停用)、open_time(如"08:00")、close_time(如"22:00")。很多初学者会漏了开放时间字段,导致用户在晚上十点后还能预约早八的场地,逻辑上奇葩。场地必须有自己的可用时间段。

预约表(reservation):这是全表里最核心的一张。id、venue_id、user_id、reserve_date(预约日期)、start_time(开始时刻)、end_time(结束时刻)、status、remark、create_time。这里的status建议用int:0-待确认,1-已确认,2-已取消,3-已使用,4-已完成,5-爽约。

场地场次表(schedule)(可选但推荐):id、venue_id、date、start_time、end_time、status。为什么不直接查预约表?因为有些场地需要管理员预先放开某些时段(比如工作日晚上才开放灯光球场),单独一张场次表能承载"管理员预先排期"这个动作。

通知公告表(notice):id、title、content、create_time。用于管理员发布场馆维护、节假日闭馆等信息。

3.2 时间字段怎么存:一个容易被问倒的细节

这个细节我单独拎出来讲,因为十个人做预约系统,有八个人会在时间处理上出问题。

预约表里有一个"预约日期"和"开始时间/结束时间"。常见的错误做法是用一个字符串datetime存成"2025-06-20 18:00:00",然后前端传什么、后端存什么,之间一点类型转换逻辑都不做,结果就是:用户选了跨天的晚间时段(22:00到次日01:00),后端按字符串排序把日期搞乱了;或者查询当天预约时因时区问题前后偏差8小时。

正确的做法是分两层区分语义:

  • reserve_date只存日期,用DATE类型,表示"预约发生在哪一天"。
  • start_time和end_time存时间,用TIME类型,表示当天从几点到几点。
  • 后端统一用LocalDate、LocalTime、LocalDateTime,出参一律格式化ISO标准字符串(如2025-06-20T10:00:00),不让数据库时区参与业务计算。

前端Vue3里的Element Plus时间选择器可以直接绑定HH:mm格式的字符串,日期选择器绑定YYYY-MM-DD,交互层不用做太多额外的转换。这个设计,答辩的时候会被面试官高看一眼——因为这证明你真的想过"预约日期"和"时间点"是两种粒度不同的数据。

4. 预约冲突检测:核心算法就一行,但并发问题能让系统报废

4.1 冲突判断的数学逻辑

预约冲突的本质是判断两个时间段是否重叠。一个预约[newStart, newEnd]和已存在的有效预约[existStart, existEnd]冲突,等价于:

newStart < existEnd && newEnd > existStart

举个例子:已有预约是10:00到12:00,如果新预约从09:00到11:00,那么newStart(09:00) < existEnd(12:00)成立,newEnd(11:00) > existStart(10:00)成立,判断为冲突——两个时段确实在10:00-11:00重叠了。如果新预约是13:00到14:00,第一条件13:00 < 12:00不成立,不冲突。这个判断逻辑,可以翻译成MyBatis-Plus的条件查询:

// 伪代码逻辑:查同一场地、同一日期、状态为有效预约,且时间段重叠 List<Reservation> conflictList = reservationMapper.selectList( new LambdaQueryWrapper<Reservation>() .eq(Reservation::getVenueId, venueId) .eq(Reservation::getReserveDate, date) .in(Reservation::getStatus, 0, 1) // 只查待确认和已确认 .apply("start_time < {0} AND end_time > {1}", endTime, startTime) ); if (CollUtil.isNotEmpty(conflictList)) { throw new BizException("该时段已被预约,请选择其他时间"); }

apply里的参数直接绑定到SQL,不是字符串拼接,MyBatis-Plus会自动做预编译,这点别偷懒写成"start_time < '" + endTime + "'"——SQL注入风险在预约系统里照样存在。

4.2 并发超卖:单机版系统的隐形杀手

上面的查询单次执行没问题,但两个用户同时提交同一个场地同一时段时,就可能出现"两人都查询无冲突,同时写入成功"。原因很简单:查询和插入不是原子操作。

解决这个问题的标准做法有两个层级:

第一层:数据库唯一约束或者悲观锁。

可以在reservation表上,为(venue_id, reserve_date, start_time, status)做一个联合唯一索引——但如果一个场地允许多个流水号(比如三个羽毛球场地可以同时有人预约),这个索引就不适用。更通用的是在提交预约时锁住该场地当天的记录:

// 利用数据库的悲观锁,锁住场地当日记录 VenueDayLock lock = venueDayLockMapper.selectByIdForUpdate(venueId, date);

这个select ... for update会锁住行,另一个并发预约只能等着,等锁释放后重新执行冲突校验。

第二层:把冲突校验和新增预约放进同一个事务方法里。

这才是最关键的一步。很多新手把"校验"和"新增"分成两个Service方法调用,结果加了锁也白搭——因为锁早就在校验完释放了。正确写法是在同一事务内先锁、再查、再插入:

@Transactional(rollbackFor = Exception.class) public void createReservation(ReservationCreateDTO dto) { // 1. 悲观锁(或者使用selectForUpdate) // 2. 冲突校验(上一小节的逻辑) // 3. 插入预约记录 }

关于"锁+校验+插入"必须在一个事务层级的说法,我做项目时真见过有人把校验写在Controller,插入写在Service,结果并发量大一点,预约单直接超卖。串联动作归属同一事务方法是这个系统最重要的一个工程约束。

4.3 状态流转不容易:取消、完成、爽约的边界处理

预约状态流转设计得好不好,直接决定管理员的日常体验。我按实际业务经验列出流转规则:

  • 用户提交预约 → 状态0(待确认)或者直接1(已确认,无需审核的场地)。
  • 已确认后,在开始时间前用户可取消 → 状态2。
  • 开始时间到了用户未到场又未取消 → 超时记为爽约,状态5。
  • 开场后用户可以核销使用(或者管理员扫码验证) → 状态3(使用中),结束时间后自动变4(已完成)。
  • 已取消/爽约的记录不参与冲突检测。

这里有个隐蔽问题:取消后的时段需要重新开放给其他用户。所以取消操作也要做一次"释放"动作——要么物理删除预约记录,要么把状态置为2,同时冲突检测时只查状态0/1。推荐用状态2保留记录,方便审计,查询时过滤掉即可。

5. 后端关键实现:SpringBoot的配置与通用能力,别在基础处翻车

5.1 统一响应体与异常处理

预约系统的接口数量保守估计在30个以上。如果每个接口返回格式不一样,前端封装Axios会非常痛苦。我会在项目里定义统一返回结构:

@Data public class R<T> { private Integer code; // 200成功,4xx业务失败,5xx系统异常 private String msg; private T data; public static <T> R<T> ok(T data) { ... } public static <T> R<T> fail(String msg) { ... } }

配合全局异常处理器,把参数校验异常、业务冲突异常、系统异常分别映射到统一响应,前端只需要在Axios响应拦截器里判断code即可。这样接口层代码量大幅度减少,联调时也更顺畅。

5.2 时间传输:序列化格式统一是关键

SpringBoot默认使用Jackson序列化LocalDateTime时输出的是数组格式(比如[2025, 6, 20, 10, 0]),前端非常难处理。需要在配置文件里指定格式:

spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8

同时对于LocalDate和LocalTime分别设置格式。前端Vue3侧使用Axios发送请求时,body统一用JSON字符串传递"2025-06-20T10:00:00",后端DTO里用@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")接收。这个配合如果不提前统一,联调阶段百分之百会出现"前端传了日期,后端解析失败"的灵异现象。

5.3 JWT登录与拦截器:小白最容易漏掉的白名单配置

登录方案建议用JWT:用户登录成功后签发一个token,前端存到localStorage,Axios请求头带上Authorization: Bearer <token>。后端加一个拦截器验证token,并把用户信息解析出来放入ThreadLocal,方便Service层拿当前用户ID。

但这里有一个特别容易踩的坑:白名单配置不当。比如你拦截了所有请求,但登录接口、注册接口、场地列表查询(未登录也要能看吧)、静态资源路径没有放行,结果前端一打开页面全报401。正确做法是把不需要登录的接口路径纳入排除列表:

registry.addInterceptor(jwtInterceptor) .addPathPatterns("/**") .excludePathPatterns( "/api/auth/login", "/api/auth/register", "/api/venue/list", "/error", "/doc.html" // 如果集成了knife4j );

另一个坑是token过期时间。JWT的过期时间建议设置成2小时,前端拦截到401时需要跳转登录页并清理本地缓存。这块每次做联调都能碰到,属于必踩的项目。

5.4 文件上传(选做):场地图片的管理可以做简单方案

场地列表没图面感很差,但做一套完整的OSS又太重。实用做法是:后端提供上传接口,把图片存到服务器本地磁盘,访问时通过Nginx映射为静态资源URL。

@PostMapping("/api/file/upload") public R<String> upload(@RequestParam("file") MultipartFile file) { String filename = UUID.randomUUID() + "." + StringUtils.getFilenameExtension(file.getOriginalFilename()); file.transferTo(new File(uploadDir + filename)); return R.ok("/uploads/" + filename); }

Nginx里配置location /uploads/ { alias /data/uploads/; },前端直接引用这个URL就行。这个方案虽然不高级,但胜在简单,完全够个人项目用。

6. Vue3前端落地:预约页面做得顺不顺,决定归属感强不强

6.1 组合式API组织代码的思路

Vue3相比Vue2最核心的变化就是组合式API(Composition API)。做预约系统这种以表单和状态为主的业务,用<script setup>语法写起来一目了然。我把前端按"页面组件化 + 组合式函数"组织:

  • 页面级组件:场地列表页、场地详情页、预约表单页、我的预约页、后台管理页。
  • 组合式函数(useReservation.ts):把预约相关的状态、校验、提交逻辑抽出来,多个页面复用,避免在组件里堆大招。
// useReservation.ts 的核心示意 export function useReservation() { const date = ref(new Date()) const startTime = ref('18:00') const endTime = ref('19:00') const submitting = ref(false) const submitReservation = async () => { submitting.value = true try { await api.createReservation({ venueId, reserveDate: formatDate(date.value), startTime: startTime.value, endTime: endTime.value }) ElMessage.success('预约成功') } catch (e) { ElMessage.error(e.response?.data?.msg || '预约失败') } finally { submitting.value = false } } return { date, startTime, endTime, submitting, submitReservation } }

这里体现出组合式API对代码组织的好处:同一业务逻辑内聚在一起,而不是像选项式API那样把data、methods、computed分散在三个选项块里,维护时来回跳跃。面试时如果有人问Vue3和Vue2的区别,拿这个例子讲比背概念强一百倍。

6.2 预约页面核心交互:日期选择器 + 场地卡片 + 时间冲突提示

预约页面的交互是前端的灵魂。常见布局:左侧根据日期展示场地列表,右侧是某个场地的具体时段格子。时段格子可以根据场地开放时间自动生成,比如开放时间是08:00-22:00,按每30分钟或60分钟一个格子生成,已被预约的格子置灰。

Element Plus里的el-calendar和el-time-picker可以组合出这个效果:用户先选日期,页面根据日期加载该场地当日所有schedule记录,再渲染成时间轴。已被预约的格子禁用,用户只能点选空闲格子提交预约。

这里有个交互细节值得留意:选好日期后切换场地时,已选的时间要清空或重新校验完整性。我见过很多系统在场地切换后仍保留之前选的时间,用户不细看一提交,后端提示"该时段已被预约",体验很糟糕。处理方式是监听场地变化时重置时间选择。

6.3 路由、Pinia存储与Axios封装

后台管理页面做成独立Layout,使用Vue Router的嵌套路由,通过路由元信息requiresAuth控制是否需登录,再配合全局前置守卫:

router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.meta.requiresAuth && !token) { next('/login') } else { next() } })

用户信息存储在Pinia里,登录成功后调用store.setUser()保存,避免每个页面都去解析token。Axios实例需要做两层拦截:请求拦截器统一添加Authorization头;响应拦截器根据code统一提示错误、401跳转登录。

7. 前后端联调与部署的实测教训

7.1 跨域是个老问题,但解法必须写成配套

本地开发时,前端跑Vite的5173端口,后端跑8080端口,天然跨域。解决方式有两种:后端CorsFilter全局允许,或者前端Vite的server.proxy配置代理。我推荐后者:

// vite.config.js export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })

这样前端请求/api/...都走Vite代理转发到后端,浏览器无感知,又能避免把后端的CORS配置暴露到生产环境。部署到Nginx时,再配置一层反向代理规则,把/api/转发到后端端口。注意生产环境下不要再靠后端CORS兜底,统一由反向代理完成,减少配置面和安全隐患。

7.2 打包部署一次成功的标准流程

这块我给一个跑通的完整操作序列:

  1. 前端执行npm run build,产物在dist/目录。
  2. 后端执行mvn clean package -DskipTests,生成可执行jar包。
  3. 创建部署目录,比如/data/venue-front和/data/venue-back。
  4. 前端dist内容拷贝到venue-front;jar包拷贝到venue-back。
  5. 后端启动命令:
nohup java -jar venue-system.jar --server.port=8080 > app.log 2>&1 &
  1. Nginx配置前端静态资源与API代理反向:
server { listen 80; server_name venue.example.com; root /data/venue-front; index index.html; location / { 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 ... /index.html尤其重要,否则前端路由使用History模式时,刷新页面会404。

7.3 上线后我学到的三件事

第一件:服务器时间一定要设置成Asia/Shanghai。我遇到过服务器时区是UTC,后端取当前日期做LocalDate.now()时比国内慢8小时,导致预约记录"穿越"到前一天,排查了很久才发现是时区问题。

第二件:数据库备份策略提前定。预约数据虽然不大,但都是真实使用记录,丢了没法找回。建议加一个每天凌晨的mysqldump定时任务,哪怕只是导出到服务器本地。

第三件:不要过度设计。有同学总想加消息通知、微信小程序端、在线支付,最后一个人做不完,主体业务反而没做好。先把预约主体跑得又稳又流畅,再考虑扩展,这才是务实路径。


我个人的习惯是,每做完一个系统都会把过程中踩过的坑整理成一篇文档,这个项目里最值得往后借鉴的,就是"并发下的预约冲突控制"和"前后端时间格式统一"这两件事——代码层面看似不起眼,却是决定系统能不能上线跑的关键。如果你也在做类似的预约类系统,建议先把这两块测试用例写全,再谈功能堆叠。祝开发顺利。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询