SpringBoot+Vue3 企业会议室预约管理系统设计:2张表、时间冲突检测、5天×30分钟预约网格——从“口头占座“到“一键预约“
度拆解企业会议室预约管理的完整设计方案。2张表(会议室信息+预定申请单)支撑从会议室建档到预约、审批、使用的全流程,3模式时间冲突检测杜绝"到了才发现被占",needApproval流程变量实现按会议室独立控制审批,5天×30分钟可视化预约网格让空闲时段一目了然,bookingScope可预定范围控制实现会议室级别的访问权限管理。
SpringBoot+Vue3 企业会议室预约管理系统设计:2张表、时间冲突检测、5天×30分钟预约网格——从"口头占座"到"一键预约"
🌐 文档地址:http://ruoyioffice.com | 📦 源码1:https://gitcode.com/zhouzhongyan/ruoyi-office-vben.git |📦 源码2:https://gitcode.com/zhouzhongyan/ruoyi-office.git |📦 源码3:https://github.com/yuqing2026/ruoyi-office.git | 💬 微信:17156169080(备注「RuoYi Office」)
会议室是企业日常协作中使用频率最高的共享资源——却也是管理最混乱的资源。"口头预约被遗忘、到了才发现被占、谁定的不知道、投影仪坏了没人修"几乎是每家公司都经历过的场景。RuoYi Office 用 2 张表、3 模式时间冲突检测、needApproval 流程变量 + 5天×30分钟可视化预约网格,构建了从会议室建档→预约→审批→使用的全流程管理闭环。

▲ 会议室预约管理功能架构全景:2 张表承载会议室信息与预定申请,时间冲突检测防止双重预约,needApproval 流程变量按会议室独立控制审批,5天×30分钟预约网格实现可视化排期
引言:会议室预约到底难在哪?
“不就是记一下谁定了哪个会议室吗?”——大多数开发者的第一反应。但真正做过才会发现,会议室预约管理的复杂度远超想象:
时间冲突是核心痛点:周一早会 9:00-10:00 的大会议室被两个部门同时预约,到了门口才发现"撞车",双方都拿着"预约记录"互不相让。系统必须在预约提交时就精确检测时间段是否重叠——而时间重叠有三种模式(左交叉、右交叉、包含),任何一种漏掉都会导致冲突。
会议室类型多,权限不同:董事会议室只允许高管预约,普通会议室对全员开放,培训教室只对 HR 和培训部门开放。不同会议室的可预定范围和审批流程完全不同。传统做法是"全靠行政口头通知谁能定哪个室",换一个行政就乱了。
审批需求因室而异:小会议室随便定,大会议室需要部门审批,贵宾接待室必须行政审批。每个会议室是否需要审批应该独立可配,而不是"一刀切"。如果所有预约都要审批,8 人小会议室定个半小时也要等审批,效率大打折扣。
排期可视化缺失:想看一间会议室这周哪些时间段有空,传统做法是翻表格逐条核对。一个直观的"5 天×时间段"预约网格能让空闲时段一目了然——这是用户体验上的质变。
会议信息不完整:谁是主持人、哪些人参会、会议主题是什么、需不需要提醒——这些信息散落在微信群或邮件中,会议结束后无据可查。如果有人问"上周三下午的产品评审会决定了什么",没有任何系统记录。
| 痛点场景 | 传统方式 | 后果 |
|---|---|---|
| 预约时间冲突 | 纸质登记/口头沟通 | 到了才发现被占,浪费所有与会者时间 |
| 权限无管控 | 所有人都能定所有室 | 实习生占了董事会议室,高管无处开会 |
| 审批一刀切 | 所有预约统一走审批 | 小会议室预约也要等半天审批,效率低下 |
| 排期不可视 | 翻 Excel 逐条核对 | 选一个空闲时段要花 10 分钟 |
| 会议信息散落 | 微信群/邮件通知 | 参会人忘了、会议室位置搞错、会后无记录 |
| 设备信息缺失 | 到了才知道没投影仪 | 临时借设备耽误开会 |
本文以 RuoYi Office 的会议室预约管理模块为例,完整拆解其业务建模、数据结构、冲突检测算法、预约网格实现、前后端交互方案。
一、业务设计:从一次会议室预约说起
1.1 业务全景
一次完整的会议室预约涉及三个角色、四个阶段:
| 阶段 | 角色 | 操作 | 系统行为 |
|---|---|---|---|
| 建档 | 行政管理员 | 录入会议室(名称/位置/类型/容量/设备/管理人) | 创建会议室记录,设置可预定范围和审批策略 |
| 预约 | 普通员工 | 选择会议室、填写会议信息 → 提交 | 创建预定申请,冲突检测,发起BPM流程 |
| 审批 | 审批人 | 审核预约合理性 | 审批通过/拒绝,根据 needApproval 变量决定是否跳过 |
| 使用 | 主持人/参会人 | 按时使用会议室 | 使用状态流转:待使用→使用中→已完成 |
1.2 needApproval:每个会议室独立控制审批

这是本方案最核心的业务抽象——会议室本身决定了预约是否需要审批:
- 不需审批(
needApproval = false):提交后流程自动通过,预约即时生效。适用于普通小会议室,员工可以即定即用。 - 需要审批(
needApproval = true):提交后走 BPM 审批流程,审批人确认后预约才生效。适用于大会议室、贵宾接待室等重要资源。
提交预约时,系统读取会议室的 needApproval 配置,将其作为流程变量传入 BPM 引擎。Flowable 流程中通过条件表达式 ${needApproval == true} 判断是否需要人工审批节点,实现了一套流程定义覆盖所有会议室的灵活设计。
这种"配置驱动审批"的模式极大简化了运维——行政在管理页面勾选"需要审批"就能立即生效,无需IT人员修改流程定义。
1.3 bookingScope:可预定范围控制
会议室通过 bookingScope + bookingMembers 两个字段实现访问控制:
| bookingScope | 含义 | bookingMembers | 效果 |
|---|---|---|---|
0 |
全部人员可预定 | 不需要 | 所有员工都能预约此会议室 |
1 |
指定成员可预定 | 逗号分隔的用户ID | 只有列表中的员工能看到并预约此会议室 |
查询"可预定会议室"时,系统先过滤 availableStatus=0(正常)且 allowBooking=true(允许预定)的会议室,再根据 bookingScope 和当前用户ID进行权限过滤——不在可预定范围内的会议室,用户连看都看不到。
1.4 使用状态的生命周期
每张预定申请单有独立的使用状态 useStatus:
| 状态码 | 状态名 | 触发条件 | 说明 |
|---|---|---|---|
0 |
待使用 | 提交预约(审批通过后生效) | 会议尚未开始 |
1 |
使用中 | 到达会议开始时间 | 会议正在进行 |
2 |
已完成 | 到达会议结束时间 | 会议正常结束 |
3 |
已取消 | 用户取消或流程撤回 | 预约已取消,释放时间段 |
使用状态支持在列表中行内编辑——但只有审批通过(processStatus = 2)后才允许修改,避免审批中的预约被提前操作。
二、系统设计:两个子模块的协作
2.1 模块组成
RuoYi Office 的会议室管理位于 OA 办公管理 → 会议室管理 目录下,由两个紧密协作的子模块组成:
| 子模块 | 菜单名称 | 功能定位 | 面向角色 |
|---|---|---|---|
| 会议室信息 | 会议室信息管理 | 会议室主数据管理:名称、位置、类型、容量、设备、管理人、可预定范围 | 行政管理员 |
| 预定申请 | 预定申请管理 | 员工发起预约、走BPM审批、使用状态管理 | 全体员工 |
2.2 核心设计决策
| 决策点 | 方案 | 理由 |
|---|---|---|
| 数据模型 | 会议室+预定申请两张表 | 会议室管"有哪些室",申请单管"谁定了哪个时段" |
| 冲突检测范围 | 待使用 + 使用中 | 已完成和已取消不占用时间段 |
| 冲突检测时机 | 提交时校验 | 保存不校验(允许草稿),提交审批时严格检查 |
| 会议室选择范围 | 可预定且正常状态 | MeetingRoomSelectModal 强制 availableStatus=0 + allowBooking=true |
| 冗余存储 | 申请单冗余会议室名称/位置/类型 | 列表展示不需 JOIN,历史数据不受修改影响 |
| 审批策略 | needApproval 流程变量 | 每个会议室独立配置,BPM 流程条件分支 |
| 权限控制 | bookingScope + bookingMembers | 会议室级别的可预定范围控制 |
| 排期可视化 | 5天×30分钟预约网格 | BookingScheduleModal 直观展示占用/空闲时段 |
| 我的单据 | 后端强制注入 creator | 数据安全,员工只能看到自己的预约 |
三、PC 端功能实现
3.1 会议室信息管理
会议室信息页面是预约管理的"数据基石",采用 VxeGrid 列表展示所有会议室信息。
▲ 会议室信息管理:列表展示会议室照片、名称、位置、类型、座位数、设备配置(多选标签)、管理人信息、可用状态。支持新增、编辑、删除、查看预约排期
列表设计要点:
- 照片缩略图:
CellImage组件展示会议室照片,支持点击放大预览 - 设备多选标签:
equipment字段通过字典OA_MEETING_ROOM_EQUIPMENT渲染为多个彩色标签(投影仪/白板/视频会议设备/音响等) - 预约排期入口:每行提供"查看排期"按钮,点击弹出
BookingScheduleModal查看该会议室的 5 天预约情况 - 可用状态三色标签:正常(绿色)/ 维修(橙色)/ 不可用(灰色)
- 审批策略标识:
needApproval字段在列表中展示为"是/否"标签,一目了然 - 表单配置项:编辑弹窗中可设置
allowBooking(是否允许预定)、needApproval(是否需审批)、bookingScope(可预定范围)、bookingMembers(指定成员)
3.2 预定申请列表
申请列表展示当前用户创建的所有预定申请单,支持新建、查看详情、删除操作。

▲ 预定申请列表:支持按单据编号、流程状态、会议室名称、会议主题、使用状态等多维度搜索。列表默认只显示当前登录用户的申请单
列表设计要点:
- 单据编号链接:点击
billCode列自动跳转到详情页,编号格式为OA{typeCode}-{YYYYMMDD}{5位流水} - 双状态展示:同时展示「流程状态」(审批中/已通过/已拒绝)和「使用状态」(待使用/使用中/已完成/已取消),通过
CellDict渲染为不同颜色标签 - useStatus 行内编辑:使用状态列支持行内下拉编辑——但只有审批通过(
processStatus = 2)且useStatus != 3(未取消)时才可编辑 - 删除约束:删除前校验流程状态,审批中的单据不可删除;取消的流程可以删除
- 参会人展示:
attendeeNames字段以逗号分隔的姓名列表展示
3.3 预定申请详情

详情页集 基本信息表单、会议室选择、时间选择、参会人管理、附件 于一体。
核心字段:
| 字段 | 组件 | 说明 |
|---|---|---|
| 会议室 | HelpInput + MeetingRoomSelectModal | 点击弹出选择弹窗,选中后自动回填名称/位置/类型 |
| 会议主题 | Input | 必填,描述会议内容 |
| 会议开始时间 | DatePicker(showTime, 30min step) | 30 分钟为最小粒度 |
| 会议结束时间 | DatePicker(showTime, 30min step) | 30 分钟为最小粒度 |
| 主持人 | UserSelectInput | 选择主持人,自动填充 moderatorName |
| 参会人员 | UserSelectInput(多选) | 逗号分隔存储,自动填充 attendeeNames |
| 提醒方式 | Select | 字典控制(如:会前15分钟/30分钟/1小时) |
| 会议说明 | TextArea | 选填,描述会议背景和议程 |
| 附件 | Upload | 支持上传会议材料 |
| 备注 | TextArea | 选填 |
MeetingRoomSelectModal(会议室选择弹窗):
弹窗内部是一个独立的分页表格,只展示可预定且状态正常的会议室,并根据当前用户的权限过滤。选中一间会议室后双击即可确认选择,信息自动回填到表单:
query: async ({ page }, formValues) => {
const queryParams = {
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
};
return await getBookableMeetingRoomPage(queryParams);
},
弹窗中还提供"查看排期"按钮,点击可弹出 BookingScheduleModal 查看该会议室的预约情况,帮助用户在选择会议室时就能判断目标时段是否空闲。
3.4 预约网格 BookingScheduleModal
这是会议室管理中最具特色的前端组件——5天×30分钟粒度的可视化预约网格。
▲ 预约网格:横轴为 5 个工作日,纵轴为每 30 分钟一格的时间段(如 08:00-08:30、08:30-09:00),已占用时段标记为彩色块,空闲时段为白色,一目了然
网格设计要点:
- 数据来源:调用
getMeetingRoomBookingSchedule接口,传入会议室ID和日期范围(默认当前周的 5 个工作日),返回该范围内所有预定记录 - 占用判定:
processStatus = 2(已通过)且useStatus != 3(未取消)的预约标记为"已占用" - 时间粒度:以 30 分钟为最小单位,将每个预约的
meetingStartTime到meetingEndTime映射到对应的网格单元格。例如 9:00-10:30 的会议会占据 3 个格子(9:00、9:30、10:00) - 当日审批列表:网格下方展示当天已审批通过的预约列表,包含会议主题、时间、主持人等信息,行政人员无需切换页面就能掌握当日会议全貌
- 颜色编码:已占用(蓝色标记会议主题)/ 空闲(白色)/ 当前时段(浅黄色高亮)
- 切换日期范围:支持前后翻页切换查看不同周的排期,方便提前规划会议
网格数据映射逻辑:
前端拿到预约数据后,将连续时间段拆分为 30 分钟的离散格子。关键在于处理跨格子的预约——一个 9:00-11:00 的会议需要映射到 [9:00, 9:30, 10:00, 10:30] 四个格子,每个格子都显示相同的会议主题和预约人信息。这种映射方式让用户对时间占用一目了然。
四、流程设计:BPM 审批与 needApproval 分支
4.1 流程编排

会议室预定使用 Flowable 引擎编排,流程定义 Key 为 oa_meeting_room_booking。流程设计的核心特色是根据会议室配置动态决定是否需要人工审批:
- 员工选择会议室、填写会议信息(主题/时间/主持人/参会人) → 提交流程
- 系统校验时间合法性 + 冲突检测 → 通过后保存申请单
- 系统读取会议室
needApproval配置,注入流程变量 - needApproval = false → 流程自动通过,预约即时生效,使用状态设为「待使用」
- needApproval = true → 进入人工审批节点(部门负责人/行政审批)
- 审批通过,使用状态变为「待使用」;审批拒绝,预约不生效
- 会议到期后,使用状态可通过行内编辑更新为「已完成」
流程变量的妙用:提交时读取会议室的 needApproval 属性并注入 BPM 流程变量。Flowable 通过条件网关 ${needApproval == true} 判断是否走审批分支,实现了一套流程定义适配所有会议室的灵活方案——新增会议室时只需在管理页面勾选"需要审批"即可,无需修改流程定义。
4.2 FlowBillService 回调驱动状态变化
预定申请服务实现了 FlowBillService 接口,BPM 引擎在流程状态变化时自动回调:
@Override
public void updateProcessStatus(String businessKey, Integer status) {
Long id = Long.parseLong(businessKey);
MeetingRoomBookingDO updateObj = new MeetingRoomBookingDO()
.setId(id)
.setProcessStatus(status);
// 流程取消时同步取消使用状态
if (status == BpmTaskStatusEnum.CANCEL.getStatus()) {
updateObj.setUseStatus(3); // 已取消
}
meetingRoomBookingMapper.updateById(updateObj);
}
关键设计:
- 流程取消时,
useStatus自动设为3(已取消),释放该时间段,其他人可以重新预约 - 审批拒绝时,只更新
processStatus,预约单在业务层面"从未生效" - 整个状态流转由 BPM 框架通过
processDefinitionKey匹配FlowBillService实现类,零硬编码
五、后端核心实现
5.1 两层数据模型
会议室管理使用 2 张表实现完整的业务数据存储:
oa_meeting_room(会议室信息)
└── 1:N ──▶ oa_meeting_room_booking(预定申请单)
通过 room_id 关联
一间会议室可以有多条预定记录,但每条预定只关联一间会议室。与用印管理类似,数据模型保持简洁,不需要明细行表——因为每次预约只针对一间会议室,不存在"一次预约多间室"的场景(如果确实需要多间,分别提交即可)。
5.2 单据编号生成
每张预定申请单有唯一的单据编号,格式为 OA{typeCode}-{YYYYMMDD}{5位流水}。编号在首次保存或提交时自动生成:
if (StringUtils.isBlank(saveReqVO.getBillCode())) {
saveReqVO.setBillCode(BillCodeUtils.generateBillCode(
SystemEnum.OA, OaBillTypeEnum.OA_MEETING_ROOM_BOOKING));
}
流水号基于 Redis 自增,key 为 bill_code:OA:{typeCode}:{yyyyMMdd},过期时间 2 天,保证分布式环境下编号唯一且递增。
5.3 提交流程:冲突检测 + needApproval 变量注入
提交预定申请是整个业务中逻辑最密集的环节——需要生成编号、校验时间合法性、检测时间冲突、读取审批策略、发起 BPM 流程:
public Long submitMeetingRoomBooking(MeetingRoomBookingSaveReqVO saveReqVO) {
// 1. 生成单据编号
if (StringUtils.isBlank(saveReqVO.getBillCode())) {
saveReqVO.setBillCode(BillCodeUtils.generateBillCode(
SystemEnum.OA, OaBillTypeEnum.OA_MEETING_ROOM_BOOKING));
}
// 2. 校验会议时间合法性(开始<结束)
validateMeetingTime(saveReqVO);
// 3. 时间冲突检测(核心!)
validateTimeConflict(saveReqVO);
// 4. 保存预定申请(状态设为审批中)
MeetingRoomBookingDO meetingRoomBooking = BeanUtils.toBean(
saveReqVO, MeetingRoomBookingDO.class)
.setProcessStatus(BpmTaskStatusEnum.RUNNING.getStatus())
.setUseStatus(0);
meetingRoomBookingMapper.insertOrUpdate(meetingRoomBooking);
// 5. 读取会议室 needApproval 配置
Boolean needApproval = false;
if (saveReqVO.getRoomId() != null) {
var meetingRoom = meetingRoomService.getMeetingRoom(saveReqVO.getRoomId());
if (meetingRoom != null) {
needApproval = meetingRoom.getNeedApproval() != null
? meetingRoom.getNeedApproval() : false;
}
}
// 6. 构建流程变量并提交 BPM
Map<String, Object> processInstanceVariables =
BpmProcessVariableUtils.buildBillVariables(saveReqVO);
processInstanceVariables.put(PV_MEETING_ROOM_NEED_APPROVAL, needApproval);
String processInstanceId = processInstanceApi.submitProcessInstance(...)
.getCheckedData();
// 7. 回写流程实例ID,保存附件
// ...
return meetingRoomBooking.getId();
}
关键:
needApproval不是写死在流程定义里的,而是运行时从会议室配置中动态读取。新增会议室或修改审批策略时无需调整流程。
5.4 时间冲突检测:3 模式时间重叠比对
validateTimeConflict 是会议室预约中最核心的校验逻辑——判断同一间会议室在申请的时间段内是否已有未取消的预约:
private void validateTimeConflict(MeetingRoomBookingSaveReqVO saveReqVO) {
List<MeetingRoomBookingDO> existingBookings = meetingRoomBookingMapper.selectList(
new LambdaQueryWrapperX<MeetingRoomBookingDO>()
.eq(MeetingRoomBookingDO::getRoomId, saveReqVO.getRoomId())
.in(MeetingRoomBookingDO::getUseStatus, Arrays.asList(0, 1))
.and(wrapper -> wrapper
// 模式1(左交叉):已有开始 ≤ 新开始 < 已有结束
.or(w -> w.le(MeetingRoomBookingDO::getMeetingStartTime,
saveReqVO.getMeetingStartTime())
.gt(MeetingRoomBookingDO::getMeetingEndTime,
saveReqVO.getMeetingStartTime()))
// 模式2(右交叉):已有开始 < 新结束 ≤ 已有结束
.or(w -> w.lt(MeetingRoomBookingDO::getMeetingStartTime,
saveReqVO.getMeetingEndTime())
.ge(MeetingRoomBookingDO::getMeetingEndTime,
saveReqVO.getMeetingEndTime()))
// 模式3(包含):新开始 ≤ 已有开始 且 新结束 ≥ 已有结束
.or(w -> w.ge(MeetingRoomBookingDO::getMeetingStartTime,
saveReqVO.getMeetingStartTime())
.le(MeetingRoomBookingDO::getMeetingEndTime,
saveReqVO.getMeetingEndTime()))
)
);
// 编辑时排除自身
if (saveReqVO.getId() != null) {
existingBookings = existingBookings.stream()
.filter(booking -> !booking.getId().equals(saveReqVO.getId()))
.toList();
}
if (!existingBookings.isEmpty()) {
throw exception(MEETING_ROOM_BOOKING_TIME_CONFLICT);
}
}
三种时间重叠模式图解:
已有预约: |=====已有预约=====|
新预约区间:
模式1(左交叉): |====新====| → 新开始时间落在已有区间内
模式2(右交叉): |====新====| → 新结束时间落在已有区间内
模式3(包含): |==========新==========| → 新预约完全包含已有预约
冲突检测范围:只检测 useStatus 为 0(待使用) 和 1(使用中) 的预约——已完成和已取消的预约不再占用时间段。编辑自己的预约时通过 .filter(booking -> !booking.getId().equals(saveReqVO.getId())) 排除自身。
5.5 可预定会议室查询:权限过滤
查询可预定会议室时,除了基础过滤(正常状态+允许预定),还需要根据 bookingScope 和当前用户进行权限过滤:
public PageResult<MeetingRoomDO> getBookableMeetingRoomPage(
MeetingRoomPageReqVO pageReqVO, Long currentUserId) {
pageReqVO.setAvailableStatus(0);
pageReqVO.setAllowBooking(true);
PageResult<MeetingRoomDO> pageResult = meetingRoomMapper.selectPage(pageReqVO);
if (currentUserId != null) {
List<MeetingRoomDO> filteredList = pageResult.getList().stream()
.filter(room -> {
// 全部人员可预定
if (room.getBookingScope() == null
|| room.getBookingScope() == 0) return true;
// 指定成员可预定
if (room.getBookingScope() == 1) {
if (StrUtil.isBlank(room.getBookingMembers())) return false;
String[] memberIds = room.getBookingMembers().split(",");
for (String memberId : memberIds) {
if (String.valueOf(currentUserId)
.equals(memberId.trim())) return true;
}
return false;
}
return true;
}).collect(Collectors.toList());
return new PageResult<>(filteredList, (long) filteredList.size());
}
return pageResult;
}
为什么用内存过滤而非 SQL:bookingMembers 是逗号分隔的字符串,SQL LIKE '%1%' 无法精确匹配用户ID(ID=1 会误匹配 ID=11、ID=21 等)。FIND_IN_SET 函数虽然能精确匹配,但不是所有数据库都支持且无法利用索引。在企业级场景下,会议室数量通常在几十间量级,内存过滤的性能开销可以忽略不计。
如果未来会议室数量增长到数百间,可以考虑引入
oa_meeting_room_member关联表替代逗号分隔字符串——但在当前规模下,简单方案就是最好的方案。
5.6 预约排期查询:5 天网格数据
getMeetingRoomBookingSchedule 为前端的预约网格组件提供数据——查询指定会议室在日期范围内的所有预约记录:
public MeetingRoomBookingScheduleRespVO getMeetingRoomBookingSchedule(
MeetingRoomBookingScheduleReqVO reqVO) {
LocalDateTime startDateTime = reqVO.getStartDate().atStartOfDay();
LocalDateTime endDateTime = reqVO.getEndDate().atTime(23, 59, 59);
// 查询该会议室在时间范围内的所有预约
List<MeetingRoomBookingDO> bookings = meetingRoomBookingMapper.selectList(
new LambdaQueryWrapperX<MeetingRoomBookingDO>()
.eq(MeetingRoomBookingDO::getRoomId, reqVO.getRoomId())
.and(wrapper -> wrapper
.or(w -> w.ge(MeetingRoomBookingDO::getMeetingStartTime, startDateTime)
.le(MeetingRoomBookingDO::getMeetingStartTime, endDateTime))
.or(w -> w.ge(MeetingRoomBookingDO::getMeetingEndTime, startDateTime)
.le(MeetingRoomBookingDO::getMeetingEndTime, endDateTime))
.or(w -> w.le(MeetingRoomBookingDO::getMeetingStartTime, startDateTime)
.ge(MeetingRoomBookingDO::getMeetingEndTime, endDateTime))
).orderByAsc(MeetingRoomBookingDO::getMeetingStartTime)
);
// 同时查询当天已审批通过的预约列表
// 返回包含预约列表和当日审批列表的响应对象
return new MeetingRoomBookingScheduleRespVO(bookings, todayApproved);
}
排期查询同样使用了三种时间重叠模式——确保跨天的会议也能被正确包含在查询结果中。前端拿到数据后,将每条预约按 meetingStartTime 和 meetingEndTime 映射到 30 分钟粒度的网格单元格中。
为什么排期查询也要三种重叠模式:假设查询周一到周五的排期,一个从周日 16:00 到周一 10:00 的会议,只用"开始时间在范围内"是查不到的——它的开始时间在周日。三种模式确保了无论会议是完全在范围内、还是跨越范围边界,都能被正确查出。
5.7 删除时清理流程
删除预定申请时,需要同时清理 BPM 流程实例:
@Override
public void deleteMeetingRoomBooking(Long id) {
MeetingRoomBookingDO booking = meetingRoomBookingMapper.selectById(id);
if (booking == null) {
throw exception(MEETING_ROOM_BOOKING_NOT_EXISTS);
}
// 清理 BPM 流程实例(尽力清理,不阻塞删除)
if (StringUtils.isNotBlank(booking.getProcessInstanceId())) {
try {
processInstanceApi.deleteProcessInstance(
Long.valueOf(booking.getCreator()),
booking.getProcessInstanceId());
} catch (Exception e) {
log.warn("[deleteMeetingRoomBooking] 清理流程失败: {}",
e.getMessage());
}
}
meetingRoomBookingMapper.deleteById(id);
}
流程清理使用 try-catch 包裹——即使 Flowable 引擎出错也不影响业务数据删除,这是一种"尽力清理"的防御性设计。
六、RuoYi Office 的创新设计
6.1 needApproval 流程变量:会议室级别的审批控制
很多会议室预约系统要么"全部需要审批",要么"全部不审批"。RuoYi Office 将审批策略下放到每个会议室独立配置:
- 传统方案:在流程定义中硬编码审批/不审批 → 修改策略需要改流程 → 不同会议室无法差异化
- RuoYi Office:
needApproval作为会议室属性 → 提交时动态注入流程变量 → BPM 条件网关分支 → 一套流程适配所有会议室
管理员只需在会议室编辑页面勾选/取消"需要审批",即可立即生效,无需任何流程配置变更。
6.2 5天×30分钟预约网格:可视化排期
用表格列表查空闲时段需要逐条核对,效率极低。预约网格将一周的预约情况压缩到一张可视化网格中:
| 传统列表查询 | 预约网格 |
|---|---|
| 逐条翻阅预约记录 | 一眼看到整周的占用/空闲分布 |
| 需要人工计算时间段是否重叠 | 颜色编码直观区分已占用/空闲 |
| 无法快速定位空闲时段 | 白色区域即空闲,可点击直接预约 |
网格的时间粒度设为 30 分钟——既满足了企业会议的最小预约单位需求,又避免了粒度过细导致的信息过载。
6.3 可预定范围控制:会议室级别的访问权限
通过 bookingScope + bookingMembers 的组合,实现了灵活的会议室级别权限控制:
- 公共会议室:
bookingScope=0,全员可见可定 - 部门会议室:
bookingScope=1,bookingMembers填入部门成员ID,只有本部门可见 - VIP 接待室:
bookingScope=1,bookingMembers填入高管用户ID,只有指定人员可见
不在可预定范围内的用户,在会议室选择弹窗中根本看不到该会议室——这比"看得到但不能定"的体验更好,避免了用户的困惑和挫败感。而且这种控制是在后端 Service 层完成的,即使绕过前端直接调 API 也无法预约没有权限的会议室。
| 权限控制方式 | 传统做法 | RuoYi Office |
|---|---|---|
| 可见性 | 所有会议室都能看到 | 无权限的会议室直接不显示 |
| 预约限制 | 前端提示"无权限" | 从列表源头过滤,用户无感知 |
| 维护方式 | 改代码/改配置文件 | 管理页面直接配置成员列表 |
6.4 冲突检测:3 模式时间重叠防双重预约
时间冲突检测覆盖了三种重叠场景(左交叉/右交叉/包含),杜绝了任何形式的时间段重叠。检测范围限定为 useStatus 为待使用和使用中的预约——已完成和已取消的预约自动释放时间段,不影响后续预约。
6.5 useStatus 行内编辑:审批后快速管理
使用状态支持在列表中直接通过下拉框编辑——但有严格的前置条件:
- 审批通过(
processStatus = 2)后才可编辑 - 已取消(
useStatus = 3)的预约不可编辑 - 审批中或审批拒绝的预约,使用状态列为只读
这种条件性行内编辑既保证了操作便捷性,又防止了不合规的状态修改。前端通过 VxeTable 的 editRender 配合 beforeEditMethod 实现条件判断——不满足条件时点击单元格不会进入编辑态。
6.6 当日已审批列表:排期 API 的附加价值
排期查询 API 不只返回时间段内的预约记录,还额外返回当天已审批通过的预约列表。前端在网格下方展示这个列表,行政人员无需切换页面就能看到今天的完整会议安排——这个小细节极大提升了日常使用效率。
七、数据结构
7.1 表结构:oa_meeting_room(会议室信息)
| 字段 | 类型 | 说明 |
|---|---|---|
id |
bigint | 主键 |
room_name |
varchar(200) | 会议室名称 |
room_location |
varchar(500) | 会议室位置(如:A栋3楼301) |
room_type |
int | 会议室类型(字典 OA_MEETING_ROOM_TYPE) |
manager_id |
bigint | 管理人ID |
manager_name |
varchar(100) | 管理人姓名 |
manager_phone |
varchar(20) | 管理人电话 |
available_status |
int | 可用状态(0正常 / 1维修 / 2不可用) |
pic_url |
varchar(500) | 会议室照片 |
seat_count |
int | 座位数/容量 |
equipment |
varchar(500) | 设备配置(多选字典 OA_MEETING_ROOM_EQUIPMENT) |
attachment_url |
varchar(500) | 附件 |
allow_booking |
tinyint(1) | 是否允许预定 |
need_approval |
tinyint(1) | 是否需要审批 |
booking_scope |
int | 可预定范围(0全部 / 1指定成员) |
booking_members |
text | 可预定成员(逗号分隔用户ID) |
sort |
int | 排序 |
remark |
varchar(500) | 备注 |
7.2 表结构:oa_meeting_room_booking(预定申请单)
| 字段 | 类型 | 说明 |
|---|---|---|
id |
bigint | 主键 |
bill_code |
varchar(50) | 单据编号(OA 开头) |
process_instance_id |
varchar(64) | Flowable 流程实例 ID |
process_status |
int | 流程状态(BpmTaskStatusEnum) |
room_id |
bigint | 关联会议室ID |
room_name |
varchar(200) | 会议室名称(冗余) |
room_location |
varchar(500) | 会议室位置(冗余) |
room_type |
int | 会议室类型(冗余) |
meeting_title |
varchar(200) | 会议主题 |
meeting_start_time |
datetime | 会议开始时间 |
meeting_end_time |
datetime | 会议结束时间 |
moderator_id |
bigint | 主持人ID |
moderator_name |
varchar(100) | 主持人姓名 |
meeting_remark |
text | 会议说明 |
reminder_type |
int | 提醒方式 |
attendees |
text | 参会人员ID(逗号分隔) |
attendee_names |
text | 参会人员姓名(逗号分隔) |
attachment_urls |
text | 附件 |
use_status |
int | 使用状态(0待使用/1使用中/2已完成/3已取消) |
creator_name |
varchar(100) | 申请人姓名 |
company_id / company_name |
bigint / varchar | 所属公司 |
dept_id / dept_name |
bigint / varchar | 所属部门 |
remark |
varchar(500) | 备注 |
7.3 设计要点
- 冗余存储:申请单冗余了
roomName、roomLocation、roomType等会议室信息。即使会议室名称后来修改了,历史预约记录中"当时预约的是哪个会议室"仍然准确 - 参会人双字段:
attendees存用户ID用于系统关联(如发送提醒),attendeeNames存姓名用于列表展示,避免展示时反查用户表 - 时间精度:
meetingStartTime和meetingEndTime使用 datetime 类型,配合前端 30 分钟步长限制,保证时间粒度统一 - 双状态字段:
processStatus跟踪审批流转,useStatus跟踪使用状态,两者独立但有联动(流程取消时使用状态同步取消) - bookingMembers 文本存储:使用逗号分隔的字符串存储可预定成员ID,简单直接。虽不支持 SQL 精确查询,但配合内存过滤在企业级规模下完全够用
八、技术亮点总结
| 设计要点 | 实现方式 | 价值 |
|---|---|---|
| 2 表简洁模型 | 会议室+预定申请,一次预约对应一间室 | 数据模型清晰,无需明细行 |
| needApproval 流程变量 | 运行时从会议室配置读取,注入 BPM | 每间会议室独立控制审批策略 |
| 3 模式时间冲突检测 | 左交叉/右交叉/包含三种重叠模式全覆盖 | 杜绝双重预约 |
| bookingScope 权限控制 | 全部/指定成员两种范围 + 内存过滤 | 会议室级别的访问控制 |
| 5天×30分钟预约网格 | BookingScheduleModal 可视化组件 | 一眼看清整周的空闲/占用分布 |
| useStatus 行内编辑 | 审批通过后才可编辑,条件性开放 | 操作便捷又不失合规 |
| 当日审批列表附加 | 排期 API 额外返回当天已通过预约 | 日常管理效率提升 |
| MeetingRoomSelectModal | 独立弹窗+权限过滤+排期预览 | 选室→查排期→确认一站式完成 |
| FlowBillService 标准化 | 统一接口接收 BPM 回调 | 新增单据只需实现接口 |
| 冗余保障追溯 | 申请单冗余会议室名称/位置/类型 | 历史数据不受修改影响 |
| 流程取消联动 | updateProcessStatus 同步 useStatus=3 | 释放时间段,数据一致 |
| 我的单据过滤 | 后端强制注入 creator | 数据安全,用户只看自己的 |
九、快速体验
在线演示:http://ruoyioffice.com/web/(账号 admin / admin123)
操作路径:OA → 会议室管理
推荐体验流程:
- 建会议室:进入「会议室信息管理」,新增几间会议室(如大会议室/需审批、小会议室/无需审批),设置不同的可预定范围
- 配置策略:给大会议室勾选"需要审批",小会议室取消勾选;给 VIP 接待室设置
bookingScope=1,指定可预定成员 - 查看排期:点击某间会议室的"查看排期"按钮,打开 5天×30分钟的预约网格,确认当前时段空闲
- 发起预约:进入「预定申请管理」,点击「新建」,在弹窗中选择一间可预定的会议室,填写会议主题、时间、主持人、参会人
- 测试冲突:用同一间会议室再提交一张预约,时间段与步骤 4 重叠,观察系统报错"会议室预定时间冲突"
- 体验审批分支:分别为需要审批和不需审批的会议室提交预约,观察流程差异
- 使用状态管理:审批通过后,在列表中行内编辑使用状态,观察状态流转
- 取消预约:撤回流程,观察使用状态自动变为"已取消"
源码仓库:
| 平台 | 地址 |
|---|---|
| GitCode(后端) | https://gitcode.com/zhouzhongyan/ruoyi-office.git |
| GitCode(前端) | https://gitcode.com/zhouzhongyan/ruoyi-office-vben.git |
| GitHub(后端) | https://github.com/yuqing2026/ruoyi-office.git |
结语
会议室预约看起来是 OA 系统中"最日常"的功能,但它承载的是企业最高频的共享资源调度需求。2 张表的简洁模型让数据职责清晰,3 模式时间冲突检测杜绝了双重预约,needApproval 流程变量让每间会议室都有自己的审批策略,5天×30分钟预约网格让排期可视化不再是奢望。
这套设计模式不仅适用于会议室预约,还可以推广到其他"共享资源+时间段调度"类场景(如公车预约、培训教室预约、工位预定等)。核心思想是:用时间段重叠检测保护共享资源,用流程变量实现按资源差异化审批,用可视化网格降低用户的决策成本。
如果你正在设计类似的资源预约模块,或者对时间冲突检测算法和流程变量驱动审批感兴趣,欢迎参考源码实现。
你们公司的会议室预约用的什么系统?有没有遇到过"到了才发现会议室被占"的尴尬? 欢迎在评论区分享你的经历和方案。
📚 想要体验 RuoYi Office 的强大功能?
🌐 在线演示:http://ruoyioffice.com/web/(账号 admin / admin123)
📦 GitCode 开源:https://gitcode.com/zhouzhongyan/ruoyi-office.git
💬 技术咨询:添加微信 17156169080,备注「RuoYi Office」
⭐ 如果觉得不错,请给个 Star 支持一下!
更多推荐
所有评论(0)