最近后台好多人在问:有没有适合练手、又能写进简历里的 Spring Boot 项目?问的人多了,我干脆把之前做的会议室管理系统翻出来重新整理了一遍。先说结论:这个项目作为 Spring Boot 入门到进阶的练手项目非常合适,代码量不大,但该有的东西都有了,做完你基本能摸透一个企业级 Web 系统从零到上线的大部分环节。
一个会议室管理系统表面看只是管理"会议室预订"这点事,实际上它是一个典型的"业务管理系统"样板间。用户管理、权限控制、数据关联、唯一性校验、时间冲突检测、状态流转、操作日志,这些在企业系统里反复出现的高频需求,它全占了。如果你用 Spring Boot 写一个 hello world 觉得不过瘾,直接拿这个项目动刀就对了。
这篇我会按一套完整项目的梳理方式来讲,不光是罗列功能点,还会拆解设计决策和踩坑记录——为什么选这个方案、数据库为什么要这么建、并发场景下预订冲突怎么处理。项目源码在文末有获取方式,正文部分先把关键逻辑捋清楚。
1. 会议室系统的通用痛点:为什么简单业务也要认真设计
别觉得会议室管理是个小系统就轻视它。随便搜一下相关项目,你会发现 90% 的版本都只做了"增删改查"级别的功能,但真正能在公司内部落地使用、不会被人骂的系统,至少要解决三个核心痛点。
首先是预订冲突。这个好理解:多人同时订同一间会议室,系统必须保证同一时段不会有两个会议。这是整个系统最核心的业务规则,也是技术含量最高的点——怎么在并发请求下保证数据不冲突,后面我会展开讲。
其次是审批流程。现实场景中,会议室预订通常需要主管或行政确认,尤其是涉及外部访客的会议。一个合格的系统必须支持"预订—审批—反馈"的闭环,而不是订了就算完。
第三个痛点是资源展示的实时性。用户打开系统时,看到哪间空闲哪间占用、每个时段的状态是什么。如果数据延迟或不准确,这个系统基本就是废的。会议室管理系统的核心不是管理,而是让"找会议室"这个动作在两分钟内完成。
理解这三个痛点后再回头看需求,你就能明白设计数据表、写业务接口时该往哪个方向使劲。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型与项目结构:照着这个搭架子不会走弯路
先说技术栈,这一套组合是当前 Spring Boot 项目中最常见的搭配,求职市场上认可度也高:
| 技术组件 | 选型 | 理由 |
|---|---|---|
| 核心框架 | Spring Boot 2.7.x | 稳定、资料多、生态兼容好,初学者不容易被版本坑到 |
| 持久层 | MyBatis-Plus | 单表 CRUD 零 SQL,复杂查询自己写 XML 也方便 |
| 权限控制 | Sa-Token | 比 Spring Security 上手难度低一个量级,API 设计直观,适合中小型系统 |
| 数据库 | MySQL 8.x | 主流业务系统标配,支持事务和行级锁 |
| 前端 | Vue 2 + Element UI | 后台管理系统最常见组合,组件现成,能快速搭出能看的界面 |
| 构建工具 | Maven | 项目标配,不用多解释 |
关于版本,这里多提醒一句。如果在网上搜 Spring Boot 教程,你可能会看到 3.x 甚至更高版本的内容。如果是为了快速跑通项目练手,建议先选 2.7.x。原因很现实:目前大量生产项目、网上的教程文章、开源组件都停留在 2.x 时代,3.x 的 Jakarta EE 迁移(javax 包改成 jakarta)和 Spring Security 6 的配置方式变化,会让新手在找资料时被各种版本差异狂虐。等你把 2.x 玩明白了,再迁移到 3.x 会轻松得多。这个项目的源码是基于 Spring Boot 2.7 搭建的,JDK 建议用 1.8 或 11,搭配起来最省心。
项目结构上,我采用经典的分层架构。这里放一个简化版的包结构示意:
code复制com.example.meeting
├── controller # 接口层:接收请求、参数校验、返回结果
├── service # 业务层:处理业务规则、事务控制
│ └── impl # 业务实现
├── mapper # 数据访问层:MyBatis-Plus 的 Mapper 接口
├── entity # 实体类:对应数据库表结构
├── dto # 数据传输对象:接收前端入参、返回前端出参
├── vo # 视图对象:组装前端展示数据
├── config # 配置类:跨域、拦截器、异常处理等
├── common # 通用类:统一返回结果、常量、状态枚举
└── exception # 异常定义与全局异常处理器
这套分层的核心思想是单向依赖。Controller 只调 Service,Service 只调 Mapper,谁也不越级。初学者最常犯的错误是在 Controller 里直接写业务逻辑甚至直接操作数据库,这样短平快,但后期加一个审批流程,代码就会乱成一锅粥。分层前期多写几行代码,后期能省你大量时间。
3. 数据库设计:会议室系统最关键的五张表
数据库设计是这个项目的地基。会议室系统数据量不大,但表与表之间的关联关系很典型,理解透了,以后做订单系统、库存系统都会受益。
核心表我设计了五张:
3.1 用户表(sys_user)
字段包括用户 ID、用户名、密码(BCrypt 加密存储)、姓名、部门、手机号、邮箱、角色 ID、状态。会议室系统一般不做用户自助注册,管理员统一创建账号即可,省去一堆注册和邮件验证的流程。
3.2 会议室表(meeting_room)
房间 ID、房间名称、容纳人数、所在楼层、设备配置(投影仪、视频会议、白板等,用逗号分隔存储或单独的关联表)、是否启用。这里有一个 status 字段,区分该房间是可预订还是维护中,避免有人订了一间正在装修的会议室。
3.3 预订记录表(meeting_booking)
这是整个系统的核心。字段包括:
room_id:哪间会议室booker_id:谁预订的title:会议主题start_time/end_time:会议起止时间participant_count:参会人数(用于校验是否超出房间容量)status:预订状态,0 待审批、1 已通过、2 已拒绝、3 已取消、4 已结束remark:备注说明create_time/update_time:创建与更新时间
特别注意,start_time 和 end_time 一定要用 datetime 类型,不要拆成年月日和时分秒两个字段,不然时间比较和校验会非常痛苦。
3.4 审批记录表(meeting_approval)
审批人、预订记录 ID、审批意见、审批结果、审批时间。设计成独立表而不是直接在预订表上改字段,是因为一次预订可能涉及多级审批(比如先主管后行政),留一张流水表既能追溯,也能随时扩展新流程。
3.5 操作日志表(sys_log)
记录谁在什么时间做了什么操作。这个在企业系统里几乎必备,出问题时靠它追溯。
这里额外说一个设计要点。预订记录表要加 booker_id 和 department_id 两个维度,因为后续可能统计"哪个部门会议室使用率最高""哪些会议室长期空置",没有这两个字段,报表功能做起来非常费劲。
建表语句我摘取预订记录表的关键部分:
sql复制CREATE TABLE `meeting_booking` (
`id` bigint NOT NULL AUTO_INCREMENT,
`room_id` bigint NOT NULL COMMENT '会议室ID',
`booker_id` bigint NOT NULL COMMENT '预订人ID',
`title` varchar(100) DEFAULT NULL COMMENT '会议主题',
`start_time` datetime NOT NULL COMMENT '开始时间',
`end_time` datetime NOT NULL COMMENT '结束时间',
`participant_count` int DEFAULT 0 COMMENT '参会人数',
`status` tinyint NOT NULL DEFAULT 0 COMMENT '状态:0待审批 1已通过 2已拒绝 3已取消 4已结束',
`remark` varchar(500) DEFAULT NULL,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_room_time` (`room_id`, `start_time`, `end_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会议室预订表';
idx_room_time 这个联合索引不是随便加的。后面做时间冲突检测时要按 room_id + 时间范围查询,没有这个索引,数据量稍大就会出现慢查询。
4. 预订冲突检测:这个系统的技术核心
会议室系统最大的技术难点不在 CRUD,而在并发场景下的预订冲突检测。两个用户同时提交同一时间段的同一会议室,系统只能允许一个成功,另一个必须收到"该时段已被预订"的提示。
实现这个功能,我用了"数据库层条件插入 + 唯一约束兜底"的组合方案。
4.1 基于 SQL 的时间冲突判定
两条预订是否冲突,本质是判断两段时间区间是否有交集。假设新预订的时间段是 [new_start, new_end],已存在的预订时间段是 [exist_start, exist_end],两者冲突的条件是:
code复制new_start < exist_end AND new_end > exist_start
这个条件覆盖了所有重叠情况,包括部分重叠、完全包含、完全相等。在 Mapper XML 里写自定义 SQL:
xml复制<select id="selectConflictCount" resultType="java.lang.Integer">
SELECT COUNT(*)
FROM meeting_booking
WHERE room_id = #{roomId}
AND status IN (0, 1)
AND start_time < #{endTime}
AND end_time > #{startTime}
</select>
status IN (0, 1) 这一段特别重要,待审批和已通过的预订都要算作占用,否则会出现"先钻审批空子"的问题——A 预订了并被批准,C 在审批期间也能预订同一时段,审批通过后才发现撞车。
4.2 并发场景必须加锁
但是光有 SQL 还不够。如果 A 和 B 同时提交请求,两边同时执行 selectConflictCount,同时发现没有冲突,然后同时插入数据,就双成功双写入了。这是典型的"先查后插"竞态条件。
解决办法有两种:
方案一:悲观锁(事务内 FOR UPDATE)
查询时加上 FOR UPDATE 对会议室记录或相关行加锁,事务结束才释放,后续请求必须等待。实现简单,但并发量高时会有锁等待。
方案二:唯一索引兜底(推荐)
在预订表上创建一个唯一索引,让数据库从物理层面阻止冲突数据:
sql复制ALTER TABLE meeting_booking
ADD UNIQUE KEY `uk_room_time` (`room_id`, `start_time`, `end_time`);
只要同一房间、同一开始时间、同一结束时间的预订重复,数据库就会报 Duplicate entry 异常。此时在 Service 层捕获 DuplicateKeyException,转换成友好的提示信息返回前端即可。
实际项目中我两个方案都用,SQL 条件判断负责正常流程的友好提示,唯一索引负责极端并发下的最后兜底。这种架构下,不管多少并发请求同时进来,永远只有一条能成功插入。
4.3 时间段校验
除了并发冲突,前端传过来乱时间也要防——比如结束时间早于开始时间、预订时长超过 8 小时、开始时间已经是过去时间。这类校验放在 Service 层统一处理,不要依赖前端校验,前端只是用户体验,后端校验才是安全底线。
java复制if (booking.getEndTime().isBefore(booking.getStartTime())) {
throw new ServiceException("结束时间必须晚于开始时间");
}
if (booking.getStartTime().isBefore(LocalDateTime.now())) {
throw new ServiceException("不能预订过去的时间");
}
到这里,会议室管理系统的核心业务规则已经闭环了。接下来解决系统使用权限和会议流程的问题。
5. 权限模型与审批流程:从能用变成好用
会议室系统如果人人用完都能订,然后又没人管,过不了多久就乱套了。权限和审批流程是把系统从"能用"推向"好用"的关键。
5.1 基于 RBAC 的权限控制
系统采用经典的 RBAC 权限模型:用户 → 角色 → 菜单/权限。普通用户能查看会议室列表、提交预订、取消自己的预订;管理员能审批、管理会议室信息、查看全量预订记录、查看统计数据。这个模型是几乎所有企业系统的标配,找工作时面试官问权限设计,你把这个说清楚就能拿不少分。
技术实现上,我用 Sa-Token 的注解控制接口权限:
java复制@SaCheckRole("admin")
@PostMapping("/room")
public Result addRoom(@RequestBody MeetingRoom room) {
meetingRoomService.save(room);
return Result.ok();
}
配合登录拦截器,每个请求都会先校验 token 是否有效,再校验角色是否匹配。
5.2 审批流程的状态机设计
审批功能最怕的是状态混乱。一个预订记录可能经历:待审批 → 已通过、待审批 → 已拒绝、已通过 → 已取消、已通过 → 已结束。如果代码里到处都能改状态,改着改着就出现脏数据。
所以我用状态枚举严格限制状态流转路径。枚举定义:
java复制public enum BookingStatus {
PENDING(0, "待审批"),
APPROVED(1, "已通过"),
REJECTED(2, "已拒绝"),
CANCELED(3, "已取消"),
FINISHED(4, "已结束");
public final int code;
public final String desc;
BookingStatus(int code, String desc) {
this.code = code;
this.desc = desc;
}
public static BookingStatus of(int code) {
return Arrays.stream(values())
.filter(s -> s.code == code)
.findFirst()
.orElseThrow(() -> new ServiceException("未知预订状态"));
}
}
审批方法里只允许将 PENDING 改为 APPROVED 或 REJECTED,如果传入的当前状态不是 PENDING,直接拒绝操作。取消操作只允许将 APPROVED 改为 CANCELED。代码里这种"只允许特定路径转换"的写法,能挡掉大量逻辑 bug。
5.3 会议结束与统计
预订状态什么时候变成"已结束"?真实场景有两种思路:一是管理员手动点结束;二是用定时任务扫描,凡是 end_time < now 且状态为 APPROVED 的预订自动置为 FINISHED。这个项目里我选择了定时任务自动处理,用 Spring Boot 自带的 @Scheduled 注解即可,不需要引入额外组件。
java复制@Scheduled(cron = "0 */5 * * * ?")
public void autoFinishMeeting() {
LambdaQueryWrapper<MeetingBooking> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(MeetingBooking::getStatus, BookingStatus.APPROVED.code);
wrapper.lt(MeetingBooking::getEndTime, LocalDateTime.now());
List<MeetingBooking> list = bookingMapper.selectList(wrapper);
for (MeetingBooking booking : list) {
booking.setStatus(BookingStatus.FINISHED.code);
bookingMapper.updateById(booking);
}
}
这个定时任务每五分钟跑一次,把已经开完的会议置为结束状态。有了这个基础,管理员统计"过去一周各会议室使用率"时数据就是准的。
6. 开发过程中最容易翻车的三个细节
这个项目整体难度中等,但我在写的时候有三次踩坑记忆很深,写出来给大家排雷。
6.1 MyBatis-Plus 的自动填充失效
设计表时 create_time 和 update_time 我用了数据库的 DEFAULT CURRENT_TIMESTAMP。但用 MyBatis-Plus 插入数据时,如果没有在实体类上配置自动填充,这两个字段的值是 null,插入后数据库也没自动填充——因为 MyBatis-Plus 的 insert 语句会显式包含所有字段,null 也会写入,覆盖了数据库默认值。
解决办法是在实体类上配自动填充:
java复制@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
然后再写一个 MetaObjectHandler 实现类,在插入和更新时自动填充当前时间。这是 MyBatis-Plus 的高频考点,几乎每个项目都会遇到。
6.2 时间比较的"天坑"
前端传过来的时间格式五花八门,有传字符串的,有传时间戳的,有传 Date 的。如果后端用 String 接收再手动解析,格式稍微不对就报错。我建议 DTO 里直接用 LocalDateTime 接收,Spring 会自动完成参数转换,前提是前端传的格式是 "yyyy-MM-dd HH:mm:ss",这个是 JSON 序列化组件自带的默认格式。
如果前端传来的是时间戳,需要在配置文件里加依赖并设置格式:
yaml复制spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
不加 time-zone 的后果是,数据库存了上午 10 点,查询出来变成下午 2 点,时区偏移整整 8 小时。我在这个坑上差点把整个系统的预订时间全部搞乱。
6.3 逻辑删除与唯一索引的冲突
这套系统里我用了 MyBatis-Plus 的逻辑删除——删除预订记录时不是物理删除,而是给 deleted 字段置 1。逻辑删除的好处是数据可追溯,但它和唯一索引一起使用时有个隐蔽问题:如果 meeting_booking 表加了 (room_id, start_time, end_time) 的唯一索引,逻辑删除后再次预订同一时间段,插入时会因为和已删除的记录(deleted=0 的那个旧记录)冲突而报错。
处理方式有两种,一是在唯一索引中带上 deleted 字段,二是在查询冲突时强制过滤 deleted = 0。走了这个坑之后,处理这类"逻辑删除 + 唯一约束"组合都形成了条件反射。
7. 前端核心功能怎么配合后端
如果这个项目你想做成前后端分离,最核心的三个页面是表格、表单、日历视图。会议室查询、预订和审批主要依赖后端的接口,这里挑两个关键交互来说。
7.1 时间选择器的一体化设计
预订表单里,开始时间和结束时间建议用日期时间范围选择器一体选择,不要分成两个独立的时间输入框。这样不仅能避免用户选错先后顺序,后端校验也能少处理一个异常分支。Element UI 的 el-date-picker 的 type="datetimerange" 组件可以一步到位。
7.2 日历视图的展示适配
日历视图直观展示某个会议室的占用情况,对会议室管理体验提升巨大。后端提供一个按日期返回全天预订列表的接口,前端通过日历插件渲染。我用的 FullCalendar,接口返回的数据结构直接按它的格式组装,不需要做额外转换。
日历视图有个值得注意的地方:接口返回的时间是 UTC 还是本地时间。如果前后端时区没有对齐,日历上显示的会议时间会整体偏移。后端统一返回 yyyy-MM-dd HH:mm:ss 字符串,前端不处理,直接用字符串展示,能直接避开时区问题。
8. 项目扩展方向:做完了还能往哪里加东西
如果你已经把这个系统的核心功能做完并能运行,还想让它更有亮点、简历上更好看,我给你指三个值得投入的方向。
8.1 使用率统计与可视化报表
会议室管理系统的进阶价值在于"数据"。统计各会议室的周使用率、月度使用高峰时段、部门预订排行等,用 ECharts 画几张图表放首页。这功能技术难度不高,但拿出去演示很加分,面试时也能展示你对业务数据的思考。
8.2 对接企业微信/钉钉消息通知
预订成功、审批驳回时自动推送消息到预订人的企业微信或钉钉。这一步主要练的是调用外部 API 的流程和容错处理,项目里可以集成一个简单的消息推送模块。
8.3 无纸化会议物资管理
把会议室相关的硬件设备(投影仪、设备转接头、白板笔)也纳入预订管理,预订会议室的同时选择需要的物资,管理后台维护库存。这个功能可以把项目从"会议室管理"升级成"会议资源管理",业务广度和深度都出来了。
关于源码获取:项目源码我已经打包整理好了,包含完整的后端代码、前端页面和数据库初始化脚本(SQL 文件),导入数据库后直接启动即可看到登录页面。源码文件较大,放在网盘了,需要的朋友直接找我拿就行。相关技术问题也欢迎交流,尤其是 MyBatis-Plus 自动填充、Sa-Token 权限配置、时间冲突检测这几个模块,遇到问题多调试几次,吃透比什么都强。
