SpringBoot+Vue3 企业会议室预约管理系统设计:2张表、时间冲突检测、5天×30分钟预约网格——从"口头占座"到"一键预约"

🌐 文档地址http://ruoyioffice.com | 📦 源码1https://gitcode.com/zhouzhongyan/ruoyi-office-vben.git |📦 源码2https://gitcode.com/zhouzhongyan/ruoyi-office.git |📦 源码3https://github.com/yuqing2026/ruoyi-office.git | 💬 微信:17156169080(备注「RuoYi Office」)

会议室是企业日常协作中使用频率最高的共享资源——却也是管理最混乱的资源。"口头预约被遗忘、到了才发现被占、谁定的不知道、投影仪坏了没人修"几乎是每家公司都经历过的场景。RuoYi Office 用 2 张表、3 模式时间冲突检测、needApproval 流程变量 + 5天×30分钟可视化预约网格,构建了从会议室建档→预约→审批→使用的全流程管理闭环。

image.png

▲ 会议室预约管理功能架构全景:2 张表承载会议室信息与预定申请,时间冲突检测防止双重预约,needApproval 流程变量按会议室独立控制审批,5天×30分钟预约网格实现可视化排期

引言:会议室预约到底难在哪?

“不就是记一下谁定了哪个会议室吗?”——大多数开发者的第一反应。但真正做过才会发现,会议室预约管理的复杂度远超想象:

时间冲突是核心痛点:周一早会 9:00-10:00 的大会议室被两个部门同时预约,到了门口才发现"撞车",双方都拿着"预约记录"互不相让。系统必须在预约提交时就精确检测时间段是否重叠——而时间重叠有三种模式(左交叉、右交叉、包含),任何一种漏掉都会导致冲突。

会议室类型多,权限不同:董事会议室只允许高管预约,普通会议室对全员开放,培训教室只对 HR 和培训部门开放。不同会议室的可预定范围和审批流程完全不同。传统做法是"全靠行政口头通知谁能定哪个室",换一个行政就乱了。

审批需求因室而异:小会议室随便定,大会议室需要部门审批,贵宾接待室必须行政审批。每个会议室是否需要审批应该独立可配,而不是"一刀切"。如果所有预约都要审批,8 人小会议室定个半小时也要等审批,效率大打折扣。

排期可视化缺失:想看一间会议室这周哪些时间段有空,传统做法是翻表格逐条核对。一个直观的"5 天×时间段"预约网格能让空闲时段一目了然——这是用户体验上的质变。

会议信息不完整:谁是主持人、哪些人参会、会议主题是什么、需不需要提醒——这些信息散落在微信群或邮件中,会议结束后无据可查。如果有人问"上周三下午的产品评审会决定了什么",没有任何系统记录。

痛点场景 传统方式 后果
预约时间冲突 纸质登记/口头沟通 到了才发现被占,浪费所有与会者时间
权限无管控 所有人都能定所有室 实习生占了董事会议室,高管无处开会
审批一刀切 所有预约统一走审批 小会议室预约也要等半天审批,效率低下
排期不可视 翻 Excel 逐条核对 选一个空闲时段要花 10 分钟
会议信息散落 微信群/邮件通知 参会人忘了、会议室位置搞错、会后无记录
设备信息缺失 到了才知道没投影仪 临时借设备耽误开会

本文以 RuoYi Office 的会议室预约管理模块为例,完整拆解其业务建模、数据结构、冲突检测算法、预约网格实现、前后端交互方案。


一、业务设计:从一次会议室预约说起

1.1 业务全景

一次完整的会议室预约涉及三个角色、四个阶段:

阶段 角色 操作 系统行为
建档 行政管理员 录入会议室(名称/位置/类型/容量/设备/管理人) 创建会议室记录,设置可预定范围和审批策略
预约 普通员工 选择会议室、填写会议信息 → 提交 创建预定申请,冲突检测,发起BPM流程
审批 审批人 审核预约合理性 审批通过/拒绝,根据 needApproval 变量决定是否跳过
使用 主持人/参会人 按时使用会议室 使用状态流转:待使用→使用中→已完成

1.2 needApproval:每个会议室独立控制审批

image.png

这是本方案最核心的业务抽象——会议室本身决定了预约是否需要审批

  • 不需审批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 列表展示所有会议室信息。
image.png

▲ 会议室信息管理:列表展示会议室照片、名称、位置、类型、座位数、设备配置(多选标签)、管理人信息、可用状态。支持新增、编辑、删除、查看预约排期

列表设计要点

  • 照片缩略图CellImage 组件展示会议室照片,支持点击放大预览
  • 设备多选标签equipment 字段通过字典 OA_MEETING_ROOM_EQUIPMENT 渲染为多个彩色标签(投影仪/白板/视频会议设备/音响等)
  • 预约排期入口:每行提供"查看排期"按钮,点击弹出 BookingScheduleModal 查看该会议室的 5 天预约情况
  • 可用状态三色标签:正常(绿色)/ 维修(橙色)/ 不可用(灰色)
  • 审批策略标识needApproval 字段在列表中展示为"是/否"标签,一目了然
  • 表单配置项:编辑弹窗中可设置 allowBooking(是否允许预定)、needApproval(是否需审批)、bookingScope(可预定范围)、bookingMembers(指定成员)

3.2 预定申请列表

申请列表展示当前用户创建的所有预定申请单,支持新建、查看详情、删除操作。

image.png

▲ 预定申请列表:支持按单据编号、流程状态、会议室名称、会议主题、使用状态等多维度搜索。列表默认只显示当前登录用户的申请单

列表设计要点

  • 单据编号链接:点击 billCode 列自动跳转到详情页,编号格式为 OA{typeCode}-{YYYYMMDD}{5位流水}
  • 双状态展示:同时展示「流程状态」(审批中/已通过/已拒绝)和「使用状态」(待使用/使用中/已完成/已取消),通过 CellDict 渲染为不同颜色标签
  • useStatus 行内编辑:使用状态列支持行内下拉编辑——但只有审批通过(processStatus = 2)且 useStatus != 3(未取消)时才可编辑
  • 删除约束:删除前校验流程状态,审批中的单据不可删除;取消的流程可以删除
  • 参会人展示attendeeNames 字段以逗号分隔的姓名列表展示

3.3 预定申请详情

image.png

详情页集 基本信息表单、会议室选择、时间选择、参会人管理、附件 于一体。

核心字段

字段 组件 说明
会议室 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分钟粒度的可视化预约网格
image.png

▲ 预约网格:横轴为 5 个工作日,纵轴为每 30 分钟一格的时间段(如 08:00-08:30、08:30-09:00),已占用时段标记为彩色块,空闲时段为白色,一目了然

网格设计要点

  • 数据来源:调用 getMeetingRoomBookingSchedule 接口,传入会议室ID和日期范围(默认当前周的 5 个工作日),返回该范围内所有预定记录
  • 占用判定processStatus = 2(已通过)且 useStatus != 3(未取消)的预约标记为"已占用"
  • 时间粒度:以 30 分钟为最小单位,将每个预约的 meetingStartTimemeetingEndTime 映射到对应的网格单元格。例如 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 流程编排

image.png

会议室预定使用 Flowable 引擎编排,流程定义 Key 为 oa_meeting_room_booking。流程设计的核心特色是根据会议室配置动态决定是否需要人工审批

  1. 员工选择会议室、填写会议信息(主题/时间/主持人/参会人) → 提交流程
  2. 系统校验时间合法性 + 冲突检测 → 通过后保存申请单
  3. 系统读取会议室 needApproval 配置,注入流程变量
  4. needApproval = false → 流程自动通过,预约即时生效,使用状态设为「待使用」
  5. needApproval = true → 进入人工审批节点(部门负责人/行政审批)
  6. 审批通过,使用状态变为「待使用」;审批拒绝,预约不生效
  7. 会议到期后,使用状态可通过行内编辑更新为「已完成」

流程变量的妙用:提交时读取会议室的 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(包含):    |==========新==========| → 新预约完全包含已有预约

冲突检测范围:只检测 useStatus0(待使用)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;
}

为什么用内存过滤而非 SQLbookingMembers 是逗号分隔的字符串,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);
}

排期查询同样使用了三种时间重叠模式——确保跨天的会议也能被正确包含在查询结果中。前端拿到数据后,将每条预约按 meetingStartTimemeetingEndTime 映射到 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 OfficeneedApproval 作为会议室属性 → 提交时动态注入流程变量 → BPM 条件网关分支 → 一套流程适配所有会议室

管理员只需在会议室编辑页面勾选/取消"需要审批",即可立即生效,无需任何流程配置变更。

6.2 5天×30分钟预约网格:可视化排期

用表格列表查空闲时段需要逐条核对,效率极低。预约网格将一周的预约情况压缩到一张可视化网格中:

传统列表查询 预约网格
逐条翻阅预约记录 一眼看到整周的占用/空闲分布
需要人工计算时间段是否重叠 颜色编码直观区分已占用/空闲
无法快速定位空闲时段 白色区域即空闲,可点击直接预约

网格的时间粒度设为 30 分钟——既满足了企业会议的最小预约单位需求,又避免了粒度过细导致的信息过载。

6.3 可预定范围控制:会议室级别的访问权限

通过 bookingScope + bookingMembers 的组合,实现了灵活的会议室级别权限控制:

  • 公共会议室bookingScope=0,全员可见可定
  • 部门会议室bookingScope=1bookingMembers 填入部门成员ID,只有本部门可见
  • VIP 接待室bookingScope=1bookingMembers 填入高管用户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 设计要点

  • 冗余存储:申请单冗余了 roomNameroomLocationroomType 等会议室信息。即使会议室名称后来修改了,历史预约记录中"当时预约的是哪个会议室"仍然准确
  • 参会人双字段attendees 存用户ID用于系统关联(如发送提醒),attendeeNames 存姓名用于列表展示,避免展示时反查用户表
  • 时间精度meetingStartTimemeetingEndTime 使用 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 → 会议室管理

推荐体验流程

  1. 建会议室:进入「会议室信息管理」,新增几间会议室(如大会议室/需审批、小会议室/无需审批),设置不同的可预定范围
  2. 配置策略:给大会议室勾选"需要审批",小会议室取消勾选;给 VIP 接待室设置 bookingScope=1,指定可预定成员
  3. 查看排期:点击某间会议室的"查看排期"按钮,打开 5天×30分钟的预约网格,确认当前时段空闲
  4. 发起预约:进入「预定申请管理」,点击「新建」,在弹窗中选择一间可预定的会议室,填写会议主题、时间、主持人、参会人
  5. 测试冲突:用同一间会议室再提交一张预约,时间段与步骤 4 重叠,观察系统报错"会议室预定时间冲突"
  6. 体验审批分支:分别为需要审批和不需审批的会议室提交预约,观察流程差异
  7. 使用状态管理:审批通过后,在列表中行内编辑使用状态,观察状态流转
  8. 取消预约:撤回流程,观察使用状态自动变为"已取消"

源码仓库

平台 地址
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 支持一下!


Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐