
简介这是一份基于Spring Boot与Vue技术栈开发的会议室预约系统完整项目源码面向计算机相关专业课程设计、毕业设计及需要快速搭建企业级预约管理场景的开发者。项目包含前后端分离实现后端以Java类、XML配置、SQL脚本和YML配置文件为主涵盖用户信息管理、会议室信息管理、预约信息管理及账户权限等核心模块前端由JS、CSS、HTML和JSP页面组成同时附带ECharts可视化图表模块便于统计会议室使用数据。资源共1049个文件除主要源码与页面资源外还包括GIF演示动画、PNG/JPG图标素材及字体文件压缩包整体大小仅17.73MB结构清晰、开箱即用。目前已有407人学习使用适合希望直接运行学习、二次扩展或借鉴前后端交互设计的开发者。压缩包内同时提供数据库脚本与核心业务逻辑类能帮助使用者快速理解预约冲突检测、权限控制等关键实现思路。1. 会议室预约系统是什么Spring Boot Vue 这套组合能解决什么公司里订会议室靠Excel、靠群里吼时间一撞就扯皮。会议室预约系统要解决的就是这个把“谁在哪段时间占用了哪个会议室”变成一个可查询、可约束的在线状态。Spring Boot负责提供预约接口和冲突校验Vue负责把日历、表单、列表这些交互摆到用户面前。这套前后端分离结构是Java方向最常见的毕业设计组合也适合后端新手完整走一遍“表设计→接口→联调→部署”的闭环。如果你手里拿到的是带.zip结尾的源码包先别急着在IDE里打开。这类包通常包含前端目录、后端目录和SQL脚本第一步是把环境对齐后端是Maven工程前端是Vue工程数据库要手动导入。后面几章按后端、前端、部署这条链路拆开讲包括参数怎么设、坑在哪儿照着做就能跑起来。2. 后端落地Spring Boot 里把预约核心逻辑做对后端部分最值得花时间的是两件事一件是预约不冲突另一件是接口能安全地用。会议室预约和购物车不一样它有时间边界和状态流转表结构和校验逻辑一旦设计错后面的接口全要返工。所以这一章从数据模型说起再到接口实现和配置。2.1 数据模型设计三张表和字段取舍我一般把表拆成三张用户表、会议室表、预约记录表。用户表存账号和密码BCrypt加密会议室表存名称、位置、容纳人数、设备预约记录表存谁在什么时间用了哪个房间以及当前状态。预约记录表是核心字段至少要有id、user_id、room_id、start_time、end_time、status、remark和create_time。status用数字表示1待确认、2已确认、3已取消学生项目里两个状态也够用。字段类型上时间段我建议用datetime不要拆成“日期开始时间结束时间”三个字段。拆开在一些简单查询里看起来直观但做跨天查询和月度视图时反而要拼字符串SQL写起来别扭。datetime可以直接用比较运算符和BETWEEN月份视图查询也简洁。下面是常见建表SQL注意预约表里我把(room_id, start_time, end_time)做成普通索引后面2.2会解释为什么不用它做唯一约束。CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, real_name VARCHAR(50), role TINYINT DEFAULT 1 COMMENT 1-普通用户 2-管理员, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE meeting_room ( id BIGINT PRIMARY KEY AUTO_INCREMENT, room_name VARCHAR(100) NOT NULL, location VARCHAR(200), capacity INT, equipment VARCHAR(500) COMMENT 投影、白板逗号分隔, status TINYINT DEFAULT 1 COMMENT 1-可用 0-停用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE reservation ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, room_id BIGINT NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, status TINYINT DEFAULT 1 COMMENT 1-待确认 2-已确认 3-已取消, remark VARCHAR(200), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_room_time (room_id, start_time, end_time), CONSTRAINT fk_res_user FOREIGN KEY (user_id) REFERENCES sys_user(id), CONSTRAINT fk_res_room FOREIGN KEY (room_id) REFERENCES meeting_room(id) );这段SQL里的两个设计细节值得展开。第一equipment用字符串存设备列表列表页展示没问题但后续要做“按设备筛选会议室”的查询就要考虑拆关联表或JSON类型。中小型场景用字符串够用别急着上关联表维护成本会高一截。第二role用TINYINT而不是字符串是为了避免角色名散落在代码里前端根据role决定显示“新增会议室”还是“只看预约记录”后端在拦截器校验接口权限时判断数字也比判断字符串可靠。业务表里有软删除需求的话再考虑加deleted字段这个系统可以不加。2.2 预约冲突校验重叠判断和并发双保险预约接口的核心是“这段空闲时间还空不空”。时间段重叠的判断是一个经典条件新预约的start_time小于已有预约的end_time同时新预约的end_time大于已有预约的start_time。用一句话记就是“新的开始早于旧的结束新的结束晚于旧的开始”两个条件同时成立就说明时间交叉了。-- 在新增预约前查同一会议室是否存在重叠记录 -- end_time newStart 表示旧预约还没结束 -- start_time newEnd 表示旧预约已经开始 -- 同时过滤掉已取消(status3)的记录 SELECT COUNT(*) FROM reservation WHERE room_id #{roomId} AND status ! 3 AND start_time #{endTime} AND end_time #{startTime}这个条件覆盖三种情况新预约完全在旧预约中间、旧预约完全在新预约中间、两者首尾部分交叉。唯一不重叠的情况只有“旧的结束新的开始”或“旧的开始新的结束”上面两个不等号刚好排除这两种。边界值我一般按“半开区间”处理结束时间等于开始时间的预约视为前后衔接不冲突数据库里存的就是时间段逻辑统一即可。对应到Service我会把参数校验、冲突检查、插入放在同一个事务里保证中间任何一步抛异常都不会留下半截数据。代码大致如下Transactional(rollbackFor Exception.class) public Reservation createReservation(ReservationDTO dto) { // 1. 参数校验开始时间必须小于结束时间不能预约过去 if (dto.getStartTime().isAfter(dto.getEndTime())) { throw new BusinessException(开始时间不能晚于结束时间); } if (dto.getStartTime().isBefore(LocalDateTime.now())) { throw new BusinessException(不能预约过去的时间); } // 2. 冲突检查查到 count 0 直接拒绝 int conflict reservationMapper.checkConflict( dto.getRoomId(), dto.getStartTime(), dto.getEndTime()); if (conflict 0) { throw new BusinessException(该时间段已被预约); } // 3. 落库默认待确认状态 Reservation reservation new Reservation(); BeanUtils.copyProperties(dto, reservation); reservation.setStatus(1); reservationMapper.insert(reservation); return reservation; }这里有几个参数需要说明。dto.getStartTime()用LocalDateTime接收只要前端传的字符串格式和后端JsonFormat对齐就不会解析失败具体格式问题在4.5单独讲。BeanUtils.copyProperties要求字段名一致DTO里不要包含id、createTime这类数据库生成字段避免从前端把不该有的值拷进去。Transactional指定rollbackForException.class是因为Spring默认只回滚RuntimeException如果BusinessException继承的是Exception不加这句话事务不会回滚这条血泪经验很容易被忽视。上面这个写法在单线程下没问题并发时两个请求会同时通过查重最终库里出现重叠记录。这个坑的表现在4.2专门展开这里先记住查询条件正确只是第一层保护并发场景要靠数据库锁或唯一约束兜底。2.3 springboot配置CORS、JSON序列化和拦截器前后端分开跑时跨域是第一个要解决的问题。后端跑8080前端Vite跑5173浏览器会拦截跨域请求。我的习惯是实现WebMvcConfigurer统一配置比在每个Controller上贴CrossOrigin干净也方便后续接权限拦截器时调整顺序Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }注意allowedOriginPatterns和allowedOrigins的区别allowCredentials(true)时allowedOrigins不能写“*”要么写具体域名要么用allowedOriginPatterns配模式。很多人在这里报错Cannot allow credentials for wildcard origin就是这两行搭配错了。maxAge设3600是为了让浏览器缓存预检结果减少OPTIONS请求次数但改了跨域配置后记得清浏览器缓存再试否则改了半天还是旧结果。JWT鉴权我用HandlerInterceptor做登录校验放行登录接口和预检请求。拦截器里先从Authorization头取出token解析失败就返回401成功就把用户id塞进request attributeController里用RequestAttribute(userId)取当前用户。拦截器注册时注意excludePathPatterns里要包含/login和静态资源前端预检的OPTIONS请求也要放行否则CORS配置还没执行就被拦截器挡了。application.yml里还有三个容易出问题的配置项。第一是日期格式前端传“2025-06-01 09:00:00”这种字符串后端要用JsonFormat或全局配置统一格式不然LocalDateTime解析直接报错。第二是MyBatis-Plus的分页插件要用PaginationInnerInterceptor注册否则分页查询会把全部数据查出来。第三是服务器端口IDE里调试时可以在启动配置里直接改端口不用改yml但要注意yml和启动配置哪个生效。另外pom.xml里Spring Boot版本如果选的是3.x要求JDK 17起本机是JDK 8时编译直接失败先把版本对好再排查代码。3. 前端落地Vue 3 从路由到预约表单的完整链路后端接口就绪后前端要解决的是让用户顺手完成“登录→看列表→选时间→预约→看自己的记录”这条链路。Vue 3 Vite Element Plus是目前最常用的组合组件现成重点是路由设计和请求封装。拿到前端源码包先别急着改代码第一步永远是npm install把依赖装上node_modules没就绪后面所有报错都是噪音。装依赖时注意Node版本Vite 5要求Node 18版本太低启动就报错。3.1 Vue 项目结构和路由设计登录守卫与页面跳转项目结构保持官方脚手架默认的src布局api目录单独放接口调用router目录放路由表views按页面放。路由分为公开路由和需要登录的路由两类用vue-router的beforeEach守卫做登录拦截// router/index.js import { createRouter, createWebHistory } from vue-router const routes [ { path: /login, component: () import(/views/Login.vue) }, { path: /, component: () import(/layout/index.vue), redirect: /rooms, children: [ { path: rooms, component: () import(/views/RoomList.vue) }, { path: reserve, component: () import(/views/ReserveForm.vue) }, { path: my, component: () import(/views/MyReservations.vue) } ] } ] const router createRouter({ history: createWebHistory(), routes }) // 导航守卫没有 token 就踢回登录页 router.beforeEach((to) { const token localStorage.getItem(token) if (to.path ! /login !token) { return /login } return true })routes里的component用箭头函数懒加载好处是首页只加载登录页代码进系统后再按路由加载对应页面打包文件会按路由自动拆成chunk首次打开更快。createWebHistory是history模式地址栏干净但部署时要处理后端404不想处理就直接换createWebHashHistory地址栏多个#部署省心。会议室预约系统的菜单只有两三层用静态路由加条件渲染就够没必要为了“动态路由”硬上addRoute路由重复添加的坑反而更多。会议室列表页用el-card展示房间信息卡片底部放“预约”和“详情”按钮操作区用Vue插槽扩展比写死按钮灵活。管理员角色在卡片上多一个“编辑”按钮根据用户role在v-if里判断就行。这里别把权限逻辑揉进路由表页面级的按钮显隐用角色判断最简单。3.2 预约表单的时间选择dayjs处理日期范围预约表单里最容易被忽视的是时间控件。用原生Date做比较时区、格式、夏令时这些历史包袱很容易砸手。常见做法是装dayjs体积小接口和moment几乎一样import dayjs from dayjs // 预约表单提交前把时间统一成 YYYY-MM-DD HH:mm:ss 字符串 function formatTime(date) { return dayjs(date).format(YYYY-MM-DD HH:mm:ss) } // 校验开始时间必须早于结束时间开始时间不能是过去 function validateTime(start, end) { if (!start || !end) return 请选择完整的预约时间段 if (dayjs(start).isAfter(dayjs(end))) return 开始时间不能晚于结束时间 if (dayjs(start).isBefore(dayjs())) return 不能预约过去的时间 return }Element Plus的el-date-picker配typedatetimerange可以直接选中开始和结束两个值配合上面这段校验表单提交链路就完整了。提交时把dayjs格式化后的字符串传给后端后端LocalDateTime接收两边格式约定一致就不会出4.5里那种解析错误。dayjs的isBefore(dayjs())判断过去时间时注意dayjs()取的是当前时刻用户选了今天上午但当前已是下午也会被拦这个行为符合“不能预约过去的时间”的业务约定。时间段粒度会影响冲突判断的复杂度。按小时预约start_time和end_time都是整点后端判断最简单支持半小时冲突判断本身没变界面要体现半小时格。见过有人把粒度做到15分钟界面好看但真没多少会议室这么用冲突率高用户反复调整反而体验差。半天和全天这种粒度不适合做成表单更适合在会议室卡片上直接放“上午/下午”按钮后端把时间范围计算好存进去别让用户自己填时间。3.3 axios 封装和路由参数预约流程的标准姿势每个页面直接调axios会很难维护我会在src/api/request.js里封装一个实例统一处理baseURL、超时时间、token注入和错误提示// src/api/request.js import axios from axios import { ElMessage } from element-plus const service axios.create({ baseURL: /api, // 开发环境用 vite proxy 转发部署后由后端收口 timeout: 10000 }) // 请求拦截器带上 token service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) // 响应拦截器401 跳登录业务错误弹提示 service.interceptors.response.use( response response.data, error { if (error.response?.status 401) { localStorage.removeItem(token) window.location.href /login } else { ElMessage.error(error.response?.data?.message || 请求失败) } return Promise.reject(error) } )baseURL写“/api”而不是写死后端地址是为了开发环境下用Vite的proxy把/api转发到127.0.0.1:8080部署后由Spring Boot或Nginx收口。这样代码里不出现具体IP换环境只改配置。这个细节很多人没注意前端写死127.0.0.1:8080打包发给别人就不能用了。响应拦截器里response response.data这一步是把axios包装的response对象拆掉后面业务代码只需要拿data如果后端返回的本身就是包装类的JSON这里要按结构取别把包装对象丢掉。从会议室列表跳预约页参数传递选query最简单this.$router.push({ path: /reserve, query: { roomId: room.id } })页面用route.query.roomId取。用pinia或sessionStorage传参数刷新页面就丢还得在onMounted里补“没参数就跳回列表”的判断。路由参数传对象则要做序列化处理起来麻烦。我的原则是能放query的就不放store能让页面自己取数据的就不靠别的页面传。预约记录列表里默认只展开第一条、其余折叠显示详情用el-collapse就能实现这个交互在“我的预约”页很常见。4. 避坑清单会议室预约系统最容易踩的5个坑这一章是血泪经验。下面这些坑在调试这类项目时反复出现每条按现象、原因、解决的顺序写遇到同款问题直接对应着改。4.1 时区问题预约时间莫名偏移8小时现象前端页面显示预约时间9:00数据库里存的是1:00或者反过来用户和管理员看到的对不上。原因前端new Date()按浏览器本地时区生成时间如果传给后端的是带时区的ISO字符串后端LocalDateTime解析时把UTC时间当北京时间存了。另一种情况是后端服务器时区不是Asia/ShanghaiJDBC连接串也没加serverTimezone参数驱动按默认时区转换了一次。解决前后端约定只传“YYYY-MM-DD HH:mm:ss”这种无时区的字符串前端用dayjs固定格式后端用JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8)接收。数据库连接串加serverTimezoneAsia/ShanghaiMySQL的time_zone设为8:00。三处对齐单时区场景里时区问题不会再出现。排查这类问题时先在数据库里select now()看MySQL自身时间对不对再排除是不是查询工具显示层的时区转换很多“偏移8小时”其实是Navicat按本机时区转换显示的结果数据本身是对的。4.2 并发抢约两个请求都通过了冲突检查现象两个用户同时提交同一会议室同一时间段两个请求都过了checkConflict最终库里出现两条重叠记录。原因问题出在“先查后插”不是原子操作。两个请求并发执行时请求A查完没有重叠请求B查完也没有重叠然后A插入、B插入中间没有互斥。synchronized加在service方法上没意义它只锁当前JVM实例内的方法多实例部署时锁不住。解决数据库层兜底。常见做法是给会议室行加锁在同一个事务里先执行SELECT id FROM meeting_room WHERE id? FOR UPDATE锁住对应行再查重、插入。锁的粒度是会议室不是整张表其他会议室预约不受影响。如果预约流程还要支持“按时间段模糊找任意空闲会议室”这类高级玩法再考虑Redis分布式锁会议室预约这个场景没必要。还有一种思路是把时间段转成固定格式字符串列比如把2025-06-01 09:00-10:00存入一个time_slot列并加唯一索引由数据库挡住重复插入做法简单但灵活性差不适合需要精准冲突判断的系统。4.3 打包后刷新404Vue history模式部署翻车现象本地npm run dev一切正常npm run build后把dist目录放进Spring Boot的static目录启动后访问首页正常在预约页按F5刷新报404。部署到Nginx也一样。原因history模式下访问/rooms时会向服务器发真实请求服务器找不到这个路径就返回404。开发环境有Vite的dev server兜底部署后没有。解决最省事是前端路由改hash模式。坚持history模式的话Spring Boot要加转发规则把非API和非静态资源的路径转发到index.htmlNginx里常见做法是try_files $uri $uri/ /index.html。我的习惯是前端打包放进Spring Boot就选hash避免服务器端多一道配置。改hash模式只把createWebHistory调用换成createWebHashHistoryroutes不用动重启前端就能生效。4.4 CORS配置看着对但就是报跨域现象前后端都配了跨域浏览器控制台还是报Access-Control-Allow-Origin缺失。后端明明写了允许全部来源前端还是被拦。原因最常见三种。一是前端配置了Vite proxy但axios的baseURL还写后端地址请求没走代理是真实跨域二是Spring Boot的JWT拦截器先于CORS配置返回了响应预检请求直接401跨域头根本没机会加三是上线接Nginx后Nginx层没加跨域头。解决开发环境统一走/api前缀加Vite proxy代理前端代码里不出现后端IP。后端CORS配置写在WebMvcConfigurer里拦截器放行OPTIONS预检请求。上线用Nginx的话在location块加add_header Access-Control-Allow-Origin *; 并处理OPTIONS。排查时用浏览器F12看请求头请求发到了哪个地址、响应头里有没有跨域字段、哪个网络层把请求拦了三步就能定位比盲改要快。Vite proxy配置在vite.config.js的server.proxy里target写后端地址changeOrigin设为true改完要重启dev server才生效。4.5 日期时间字符串的隐式转换坑现象前端传startTime为“2025-06-01 09:30”这种字符串后端接口用LocalDateTime接收报400日志有JSON parse error。原因前端传的字符串格式和后端期望不一致。JsonFormat默认按ISO标准解析带空格的格式解析不了。更隐蔽的是前端用toLocaleString()生成“2025/6/1 09:30:00”这种带斜杠的后端也解析不了。解决前端统一用dayjs的“YYYY-MM-DD HH:mm:ss”格式生成字符串后端全局配置Jackson的日期格式在application.yml里设置spring.jackson.date-format和time-zone。校验逻辑里再补一道“结束时间必须晚于开始时间”防止用户手输一个框架解析不了的格式直接被拦截报错不友好。这个坑和4.1是同一个根源前后端没有事先约定时间格式各按各的来。在接口文档里把时间格式写死比在代码里到处适配要省心得多。5. 验证与进阶预约核心链路跑通之后还能做什么项目能编译能启动只是开始预约核心链路要真验证过。我一般会按“登录→查空房间→提交预约→查我的预约→取消预约”走一遍每步看一眼数据库状态。用curl在服务器上验证最快# 1. 登录拿 token TOKEN$(curl -s -X POST http://localhost:8080/api/auth/login \ -H Content-Type: application/json \ -d {username:admin,password:123456} | jq -r .data.token) # 2. 提交预约房间1明天10点到11点 curl -X POST http://localhost:8080/api/reservation \ -H Authorization: Bearer $TOKEN \ -H Content-Type: application/json \ -d {roomId:1,startTime:2025-06-10 10:00:00,endTime:2025-06-10 11:00:00}登录接口返回的token需要jq这个命令行工具提取没装就手动从响应里拷贝。命令行验证的重点是“同一时间段提交第二次必须返回冲突”第一次提交成功后立刻再执行一次相同的curl如果返回“该时间段已被预约”说明查重逻辑生效。两个并发请求同时提交的情况则要看4.2说的数据库锁是否落到位。验证通过后再开前端页面如果还有问题问题就锁定在前端展示或传参而不是后端接口。跑通之后想进阶三个方向按难度递增会议室信息Excel导入省去管理端逐一录入预约成功发邮件通知参会人把“预约-通知”闭环补完整前端加WebSocket或轮询别人预约成功后页面日历自动刷新。这三个方向分别练到导入导出、JavaMail、WebSocket写在简历上比“实现了CRUD”有说服力。会议室预约系统的核心从来不是页面多漂亮而是状态的并发和一致性。后端冲突校验做了数据库层兜底前端时间格式统一部署模式选对这套系统就能按上线标准去要求。我的习惯是收尾前把4.1到4.5的检查清单过一遍省得部署时才发觉时区没对齐、路由模式没选对这类低级问题。希望帮到你。本文还有配套的精品资源点击获取