简介:本资源为基于SpringBoot与Vue技术栈实现的会议室预约系统完整项目源码,面向计算机相关专业学生、课程设计开发者及需要企业级前后端分离项目练手的初中级开发者,可用于毕业设计、课程作业或技术栈实战学习。压缩包共1049个文件,约17.73MB,涵盖Java后端源码、JSP页面、HTML与CSS样式、JavaScript脚本、XML配置、SQL建表脚本及大量图片、字体、图标等静态资源,前后端结构完整,便于直接运行与二次开发。项目围绕用户信息、会议室信息、预约信息等核心模块展开,包含控制器、服务层与实体类等典型分层代码,适合研究预约流程、权限管理与数据交互的实现思路。目前已有407人学习下载,可作为课程设计参考模板,帮助读者快速理解SpringBoot整合Vue的工程组织方式,掌握从数据库设计到接口联调的完整开发链路。
1. 从一份会议室预约系统源码说起:前后端分离到底解决了谁的痛
公司行政群里最常出现的两句话,一句是「三楼小会议室谁又占了」,另一句是「这个时间段到底有没有空」。我最早接触会议室预约这个需求,就是被行政同事拉去救火的——他们当时用共享表格登记,冲突、覆盖、找不到记录是家常便饭。后来我拿到一份基于 springboot+vue 的会议室预约系统实现,才算把这件事从「人肉协调」变成「系统约束」。这套组合的核心价值很直接:SpringBoot 负责把预约规则、时间冲突校验、权限这些后端逻辑收拢成接口,Vue 负责把日历、时间段、审批状态渲染成一个人愿意点的界面。它适合两类人:一类是正在做课程设计或毕设、需要一套能跑通的前后端分离项目练手;另一类是小团队里想自建内部工具、又不想上重型 OA 的开发者。这篇文章不讲空泛概念,我会顺着这份源码的典型结构,把环境怎么搭、接口怎么设计、时间冲突怎么防、部署怎么落地,一层层拆开讲清楚,让你拿到手能复现,遇到坑知道往哪看。
2. 环境搭建与项目骨架:把 SpringBoot 和 Vue 两条腿都立起来
2.1 后端为什么选 SpringBoot,而不是 Servlet 或纯 SSM
会议室预约系统的后端本质是一组 REST 接口加数据库操作,业务不复杂,但对开发速度和可维护性要求高。SpringBoot 的最大优势是自动配置和内嵌容器,你不用再手写一堆 web.xml,也不用单独装 Tomcat。常见做法是用 Spring Initializr 生成骨架,勾选 Spring Web、MyBatis(或 Spring Data JPA)、MySQL Driver、Lombok 就够了。这里有个选型细节:如果你的预约逻辑里涉及审批流,比如「预约超过两小时需要主管审批」,可以考虑引入 Flowable,但多数会议室场景用一张状态字段表就能覆盖,不必上工作流引擎,否则复杂度会反噬。
依赖版本上,我一般会避开最新的大版本。SpringBoot 3.x 要求 JDK 17,而很多学校机房或老服务器还停在 JDK 8,这时候选 2.7.x 更稳。下面是一个典型的pom.xml关键依赖片段:
<!-- pom.xml 关键依赖,JDK8 环境建议 SpringBoot 2.7.x --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> </parent> <dependencies> <!-- Web 接口能力 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- MyBatis 持久层,配合分页插件使用 --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <!-- Lombok 减少 getter/setter 样板代码 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>这段配置里,spring-boot-starter-parent统一管理版本,避免你手动对版本号对到崩溃;mybatis-spring-boot-starter让 Mapper 接口能被自动扫描;MySQL 驱动在 8.x 之后 groupId 变成了com.mysql,如果你照抄老教程写mysql-connector-java会拉不到包,这是新手最容易翻车的地方之一。
2.2 Vue 前端环境:node 版本和依赖安装的坑
前端这边,Vue 2 和 Vue 3 的脚手架命令不一样,拿到源码第一件事是看package.json里的vue版本。Vue 2 项目用vue-cli,Vue 3 项目用vite或@vue/cli5.x。我见过太多人 node 版本装到 18 以上,去跑一个 Vue 2 的老项目,结果node-sass编译直接报错。稳妥做法是用 nvm 切到 node 16 再装依赖。
# 查看当前 node 版本,Vue2 老项目建议 14/16 node -v # 安装依赖,国内网络建议先配镜像 npm config set registry https://registry.npmmirror.com npm install # 启动开发服务器 npm run serve # vue-cli 项目 npm run dev # vite 项目npm install之后如果出现Module not found或Cannot find module 'vue',八成是依赖没装全或者 node_modules 损坏,删掉node_modules和package-lock.json重装通常能解决。启动成功后默认跑在 8080 或 5173,后端 SpringBoot 默认 8080,两个端口会撞,记得在vue.config.js或vite.config.js里改前端端口,或者改后端server.port。
2.3 前后端联调:跨域和代理配置
前后端分离项目第一次联调,浏览器控制台大概率会甩你一个 CORS 跨域错误。这不是代码写错了,是浏览器同源策略在拦。两种解法:后端加全局跨域配置,或者前端配代理。开发阶段我更推荐前端代理,因为不用改后端代码,部署时也不留隐患。
// vue.config.js 开发代理配置 module.exports = { devServer: { port: 8081, // 前端端口,避开后端 8080 proxy: { '/api': { target: 'http://localhost:8080', // 后端地址 changeOrigin: true, // 允许跨域 pathRewrite: { '^/api': '' } // 去掉 /api 前缀再转发 } } } }changeOrigin: true的作用是把请求头里的 host 改成目标地址,绕过部分服务端的 host 校验;pathRewrite是因为后端接口路径通常不带/api,前端统一加前缀方便区分静态资源和接口。配好之后前端请求写/api/meeting/list,实际打到后端就是/meeting/list。这个代理只在开发环境生效,生产环境要靠 Nginx 转发,后面部署章节会讲。
3. 数据库设计与预约冲突校验:系统能不能用,全看这一层
3.1 会议室预约的表结构怎么定
会议室预约系统的数据模型不复杂,但字段设计直接决定后面查询好不好写。核心三张表:会议室表、预约记录表、用户表。会议室表存名称、容纳人数、位置、设备(投影、白板);预约记录表存会议室 id、预约人、开始时间、结束时间、状态;用户表存账号、角色。这里的关键是时间字段用datetime而不是字符串,否则后面做区间查询会非常难受。
-- 会议室表 CREATE TABLE meeting_room ( id BIGINT PRIMARY KEY AUTO_INCREMENT, room_name VARCHAR(64) NOT NULL COMMENT '会议室名称', capacity INT DEFAULT 0 COMMENT '容纳人数', location VARCHAR(128) COMMENT '位置', status TINYINT DEFAULT 1 COMMENT '1可用 0停用' ); -- 预约记录表 CREATE TABLE reservation ( id BIGINT PRIMARY KEY AUTO_INCREMENT, room_id BIGINT NOT NULL, user_id BIGINT NOT NULL, start_time DATETIME NOT NULL COMMENT '开始时间', end_time DATETIME NOT NULL COMMENT '结束时间', status TINYINT DEFAULT 0 COMMENT '0待审批 1已通过 2已拒绝 3已取消', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_room_time (room_id, start_time, end_time) );idx_room_time这个联合索引是必须的,因为冲突校验的查询条件永远是「某个会议室 + 某段时间」,没有索引的话数据量一上来查询就会变慢。status字段用数字而不是字符串,是为了查询和判断方便,但要在代码里用枚举或常量类维护,别到处写魔法数字。
3.2 时间冲突校验的 SQL 逻辑
预约系统最核心的一条规则:同一会议室,时间段不能重叠。判断两个时间段是否重叠的标准逻辑是「新预约开始时间 < 已有预约结束时间 且 新预约结束时间 > 已有预约开始时间」。这个条件写进 SQL 就是:
-- 查询是否存在冲突的预约,返回数量大于 0 即冲突 SELECT COUNT(*) FROM reservation WHERE room_id = #{roomId} AND status IN (0, 1) -- 待审批和已通过的都算占用 AND start_time < #{endTime} -- 已有开始 < 新的结束 AND end_time > #{startTime} -- 已有结束 > 新的开始注意status只算待审批和已通过,已拒绝和已取消的不占用时间段。这里有个容易忽略的点:边界时间。如果已有预约是 9:00-10:00,新预约是 10:00-11:00,按上面的条件start_time < 10:00不成立(10:00 不小于 10:00),所以不算冲突,这是符合预期的——前一场结束时间等于后一场开始时间,可以无缝衔接。如果你希望留出 10 分钟间隔,就要在业务层把新预约的开始时间往前推 10 分钟再校验。
3.3 并发场景下的重复预约怎么防
上面的 SQL 在单用户操作时没问题,但两个人同时提交同一时间段,可能都查到「无冲突」然后双双插入成功。这是典型的并发问题,血泪经验是:光靠查询判断挡不住。解决办法有两种,一种是在数据库层加唯一约束,但时间段重叠不是简单的唯一键能表达的;另一种是用悲观锁或分布式锁。小系统里最实用的做法是在 Service 层对「会议室 id」加锁:
// 以会议室 id 为粒度加锁,避免同一会议室并发插入 public synchronized Result reserve(Long roomId, ReservationDTO dto) { // 先查冲突 int conflict = reservationMapper.countConflict(roomId, dto.getStartTime(), dto.getEndTime()); if (conflict > 0) { return Result.fail("该时间段已被预约"); } // 无冲突再插入 reservationMapper.insert(dto.toEntity()); return Result.success(); }synchronized在单机部署时够用,但如果后端多实例部署,它就不管用了,需要换成 Redis 分布式锁。判断标准很简单:你的系统只跑一个 jar 包,就用synchronized;要上多副本,就老老实实上 Redis 锁。别为了「看起来高级」在小项目里硬上分布式锁,维护成本不划算。
4. 前端交互与接口对接:日历、时间段和状态流转怎么落地
4.1 用 Vue 渲染一个可点击的预约日历
会议室预约的界面核心是一个按天或按周展示的时间轴,用户点某个时间段就能发起预约。Vue 里可以用现成的日历组件,也可以自己用表格渲染。自己写的可控性更强,逻辑也简单:拿到当天所有预约记录,按小时切分格子,有预约的格子标灰禁用。
// 生成当天 8:00-20:00 的时间段,标记已被占用的格子 computed: { timeSlots() { const slots = []; for (let h = 8; h < 20; h++) { const start = `${String(h).padStart(2, '0')}:00`; const end = `${String(h + 1).padStart(2, '0')}:00`; // 判断该小时是否与已有预约重叠 const occupied = this.reservations.some(r => r.startTime < `${this.date} ${end}` && r.endTime > `${this.date} ${start}` ); slots.push({ start, end, occupied }); } return slots; } }这段计算属性依赖reservations和date,只要这两个数据变化,时间段占用状态会自动重算,不用手动刷新。padStart(2, '0')是为了让 8 点显示成08:00,和后端时间格式对齐。实际项目里时间段粒度可能是半小时,把循环步长改一下即可。
4.2 预约接口的参数设计和返回结构
前后端对接最容易扯皮的地方是参数格式。我的习惯是统一用 JSON,时间统一用yyyy-MM-dd HH:mm:ss字符串,后端用@JsonFormat注解转成 Date。接口返回统一包一层code、msg、data,前端只判断code是否为 200。
// 预约请求参数 public class ReservationDTO { private Long roomId; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private Date startTime; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private Date endTime; private String purpose; // 会议主题 }timezone = "GMT+8"必须写,否则服务器时区不是东八区时,时间会莫名其妙差 8 小时,这种问题排查起来很折磨人。前端传参时也要保证格式一致,用 dayjs 或 moment 格式化后再发请求。
4.3 预约状态流转和前端按钮控制
预约记录有状态:待审批、已通过、已拒绝、已取消。不同状态能做的操作不同,前端要根据状态决定按钮显示。比如待审批的记录,普通用户可以取消,管理员可以通过或拒绝;已通过的记录,用户只能取消不能改时间。这个逻辑建议在后端也校验一遍,前端隐藏按钮只是体验优化,不能当权限控制。
// 根据状态计算可执行操作 methods: { canCancel(row) { return row.status === 0 || row.status === 1; // 待审批或已通过可取消 }, canApprove(row) { return this.isAdmin && row.status === 0; // 管理员才能审批待审批记录 } }把权限判断写成方法而不是散在模板里,后面加角色或改规则时只改一处。isAdmin从登录后存的状态里取,别在前端硬编码。
5. 避坑与排查:这套系统上线前必须过的几道坎
5.1 时间格式前后端不一致导致查询为空
现象:前端选了 9 点到 10 点,提交后后端查冲突查不到,但数据库里明明有记录。原因通常是前端传的是时间戳或yyyy-MM-ddTHH:mm格式,后端按yyyy-MM-dd HH:mm:ss解析失败或解析成别的值。解决:在 Controller 入口打印一次收到的参数,确认格式;统一用@JsonFormat并在前端用 dayjs 格式化,两边对齐。
5.2 分页插件没配导致查询返回全部数据
现象:会议室列表接口返回了几千条,前端卡死。原因是用 MyBatis 分页插件时忘了加配置类,PageHelper.startPage没生效。解决:确认引入了pagehelper-spring-boot-starter,并且查询语句紧跟在startPage之后,中间不能插入其他查询。
5.3 打包后前端 404 或接口 404
现象:本地跑得好好的,npm run build后丢到服务器,页面白屏或接口报 404。原因通常是前端路由用了 history 模式但 Nginx 没配try_files,或者接口代理没配。解决:Nginx 里加try_files $uri $uri/ /index.html;,接口用location /api/转发到后端端口。
5.4 数据库时区问题导致时间差 8 小时
现象:存进去是 9 点,查出来是 1 点或 17 点。原因是 JDBC 连接串没指定时区。解决:连接串加serverTimezone=Asia/Shanghai,同时确认 MySQL 服务器时区。
5.5 并发预约导致重复插入
现象:两个人同时预约同一时间段,都成功了。原因就是前面说的查询和插入之间的竞态。解决:Service 层加锁,或数据库层用SELECT ... FOR UPDATE锁住相关行再判断。
6. 部署与进阶:从本地跑通到真正能给别人用
本地跑通只是第一步,要让行政同事真的用起来,还得部署。后端打成 jar 包,前端 build 出静态文件,用 Nginx 做统一入口。下面是一个能直接抄的 Nginx 配置:
server { listen 80; server_name your-domain.com; # 前端静态资源 location / { root /var/www/meeting-ui; try_files $uri $uri/ /index.html; # history 模式必须 } # 后端接口转发 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那行是 history 路由的后悔药,不加的话刷新页面就 404。proxy_pass末尾的斜杠要注意,带斜杠会把/api/替换掉,不带则保留,配错会导致接口路径多一层或少一层。
后端启动用nohup java -jar meeting.jar --spring.profiles.active=prod &,生产环境的数据库密码、端口这些放到application-prod.yml里,别写死在代码里。如果服务器内存小,加-Xms256m -Xmx512m限制堆内存。
验证部署是否成功,我一般按这个顺序查:先curl http://127.0.0.1:8080/meeting/list看后端通不通,再浏览器打开首页看静态资源加载,最后点一次预约走完整流程。哪一步断了就查对应日志,后端看nohup.out,前端看浏览器 Network 面板。
这套系统真正值得做的点,不在于技术多新,而在于它把「时间冲突」这个规则用代码固化下来,省掉了大量沟通成本。我做完第一版后最大的教训是:别急着写界面,先把冲突校验的 SQL 和并发场景想清楚,否则后面改起来牵一发动全身。希望帮到你。
本文还有配套的精品资源,点击获取