会议室预订预约小程序前后台源码:防超订与自动释放实战
简介这是一套面向写字楼、高校及创业园区的会议室在线预订系统源码采用小程序前端与原生PHP后台组合适合需要快速搭建预约平台的开发者或企业二次开发。前端基于小程序实现会议室查询、时段选择与预订操作后台负责资源管理、订单处理与数据维护并支持二维码现场核销参会人员扫码即可完成验证入场减少人工核对环节。压缩包共1185个文件约1021KB以js、ts、wxss、json、wxml、wxs等小程序页面与逻辑文件为主配合少量png图片资源目录结构完整便于按模块阅读与改造。目前已有4147人学习下载说明该方案在预约类项目中具有一定参考价值。读者可从中获取前后台完整代码、预约与核销流程实现思路、数据管理逻辑以及可复用的页面组件适合作为课程设计、企业办公工具或预约类小程序开发的实践参考。1. 会议室预订预约小程序从“抢会议室”到“扫码即用”的落地拆解周五下午两点行政在群里吼了一句“三楼小会议室谁又占了”紧接着三个部门同时甩出截图说“我明明预约了”。这种场景几乎每家公司每周都在上演。会议室预订预约小程序要解决的就是这件事把线下抢钥匙、群里喊话、Excel 排期的混乱收敛成一套“看空闲、点预约、到点扫码开门、超时自动释放”的闭环。它适合两类人一类是公司内部想自建工具的行政或 IT另一类是接私活做企业办公套件的前后端开发者。标题里的“前后台源码”意味着这套东西不是纯前端 Demo而是包含用户端小程序、管理后台、服务端接口和数据库的完整工程。我做过三版类似的系统第一版踩了并发超订的坑第二版被“预约了不来”拖垮了会议室周转率第三版才把签到、释放、权限、审批串成一条线。下面按“先想清楚数据模型再跑通最小预约链路最后处理并发和释放”的顺序讲能直接照着搭。2. 会议室预订预约小程序的数据模型与前后台职责划分2.1 先定四张核心表别急着写页面很多人一上来就画日历 UI结果写到一半发现“跨天预约”“周期性会议”“设备绑定”全塞不进现有字段。我一般先把数据模型定死再动接口和页面。会议室预订预约小程序的核心表就四张会议室表、预约记录表、用户表、审批/签到记录表。会议室表存容量、位置、设备投影/视频会议、开放时间段、是否需要审批预约记录表存会议室 ID、预约人、开始结束时间、状态待审批/已通过/已签到/已取消/已释放、实际签到时间用户表存 openid、姓名、部门、角色普通/管理员签到记录表存预约 ID、签到方式、签到时间、签到位置。状态机是这套系统的灵魂状态流转错了后面释放和统计全乱。-- 会议室表把“能不能约”和“约了怎么用”分开存 CREATE TABLE meeting_room ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL COMMENT 会议室名称如“三楼小会议室”, location VARCHAR(128) NOT NULL COMMENT 楼层具体位置, capacity INT NOT NULL DEFAULT 0 COMMENT 容纳人数, devices VARCHAR(255) DEFAULT COMMENT 投影,视频会议,白板, open_start TIME NOT NULL DEFAULT 08:00:00 COMMENT 每日开放开始, open_end TIME NOT NULL DEFAULT 20:00:00 COMMENT 每日开放结束, need_approval TINYINT NOT NULL DEFAULT 0 COMMENT 0免审批 1需审批, status TINYINT NOT NULL DEFAULT 1 COMMENT 1启用 0停用 ); -- 预约记录表状态机字段是核心别用布尔值糊弄 CREATE TABLE room_booking ( id BIGINT PRIMARY KEY AUTO_INCREMENT, room_id BIGINT NOT NULL, user_id BIGINT NOT NULL, subject VARCHAR(128) NOT NULL COMMENT 会议主题, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待审批 1已通过 2已签到 3已取消 4已释放 5已拒绝, checkin_time DATETIME DEFAULT NULL COMMENT 实际签到时间, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_room_time (room_id, start_time, end_time), KEY idx_user (user_id), KEY idx_status_start (status, start_time) );上面uk_room_time这个唯一索引是防超订的第一道闸但它只能挡住“完全相同的开始和结束时间”挡不住“9:00-10:00”和“9:30-10:30”这种交叉重叠。真正的重叠校验必须放在服务层用 SQL 范围查询做后面第 4 章会展开。status用数字枚举而不是布尔是因为预约生命周期有六种状态用is_booked这种字段迟早要重构。open_start/open_end放在会议室表而不是写死在代码里是因为不同楼层会议室开放时间经常不一样行政改配置比改代码快。2.2 前后台职责怎么切小程序管“约”后台管“配”前后台源码最容易犯的错是职责重叠小程序里也能改会议室信息后台里也能替人预约最后权限和日志一团糟。我的切法是——小程序端只做四件事查空闲、提交预约、签到、取消自己的预约管理后台做四件事会议室增删改、审批预约、查看统计、强制释放。服务端接口按角色鉴权普通用户 token 调不了管理接口。这样切的好处是小程序端逻辑轻审核发布快后台功能重但只给行政几个人用迭代慢一点没关系。接口层面小程序端走/api/wx/前缀后台走/api/admin/前缀网关层按前缀做权限拦截比在每个接口里写 if-else 判断角色干净得多。2.3 最小可跑通的预约链路先把“免审批会议室”的链路跑通再叠加审批流。链路是小程序拉会议室列表 → 选日期拉某会议室当天已占用时间段 → 用户选空闲段提交 → 服务端做重叠校验 → 写入预约记录 → 返回成功。这条链路里唯一有技术含量的是“拉已占用时间段”和“提交时重叠校验”必须用同一套时间判断逻辑否则会出现“列表显示空闲但提交失败”的玄学问题。我一般把时间重叠判断抽成一个工具函数列表查询和提交校验都调它保证口径一致。// 时间重叠判断列表和提交共用避免口径不一致 // 判断两个时间段是否重叠新段开始 旧段结束 且 新段结束 旧段开始 function isOverlap(newStart, newEnd, oldStart, oldEnd) { return new Date(newStart) new Date(oldEnd) new Date(newEnd) new Date(oldStart); } // 查询某会议室某天已占用时间段只查有效状态 async function getBusySlots(roomId, day) { const dayStart ${day} 00:00:00; const dayEnd ${day} 23:59:59; // 状态 0待审批 1已通过 2已签到 都算占用3取消 4释放 5拒绝 不算 const rows await db.query( SELECT start_time, end_time FROM room_booking WHERE room_id ? AND status IN (0,1,2) AND start_time ? AND end_time ?, [roomId, dayEnd, dayStart] ); return rows; }isOverlap里两个条件必须同时成立才算重叠少一个就会把“刚好首尾相接”的时段误判为冲突。比如 A 约 9:00-10:00B 约 10:00-11:00这是合法的newStart oldEnd在 10:00 10:00 时为 false正确放行。getBusySlots里status IN (0,1,2)是关键待审批的时段也要占住否则两个人同时提交同一时段一个审批通过另一个才发现冲突就晚了。查询条件用start_time dayEnd AND end_time dayStart而不是BETWEEN是为了把跨天预约比如 23:00-次日 01:00也能正确捞出来。3. 会议室预订预约小程序的服务端接口与并发防超订3.1 提交预约接口的完整实现提交预约是整个系统最容易翻车的地方。我见过最典型的翻车是两个请求几乎同时到达都查了“该时段空闲”都通过了校验然后都写入成功会议室被约了两次。解决这个问题有三层防线我一般三层都上。第一层是数据库唯一索引兜底第二层是事务内加行锁或间隙锁第三层是应用层用 Redis 分布式锁把同一会议室的提交串行化。小公司并发不高的话前两层足够如果你们公司有“周一早上九点抢会议室”的盛况第三层也加上。# 提交预约事务 行锁防并发超订 import pymysql from dbutils.pooled_db import PooledDB def create_booking(room_id, user_id, subject, start_time, end_time): conn pool.connection() try: conn.begin() with conn.cursor() as cur: # 1. 锁定该会议室当天所有有效预约防止其他事务插入重叠段 cur.execute( SELECT id FROM room_booking WHERE room_id %s AND status IN (0,1,2) AND start_time %s AND end_time %s FOR UPDATE, (room_id, end_time, start_time) ) if cur.fetchone(): conn.rollback() return {code: 409, msg: 该时段已被预约} # 2. 校验会议室开放时间 cur.execute( SELECT open_start, open_end, need_approval FROM meeting_room WHERE id%s AND status1, (room_id,) ) room cur.fetchone() if not room: conn.rollback() return {code: 404, msg: 会议室不存在或已停用} # 开放时间校验逻辑略按 start_time/end_time 的时分比较 # 3. 写入预约需审批的进待审批否则直接通过 status 0 if room[need_approval] else 1 cur.execute( INSERT INTO room_booking (room_id, user_id, subject, start_time, end_time, status) VALUES (%s,%s,%s,%s,%s,%s), (room_id, user_id, subject, start_time, end_time, status) ) booking_id cur.lastrowid conn.commit() return {code: 0, data: {booking_id: booking_id, status: status}} except Exception as e: conn.rollback() raise e finally: conn.close()FOR UPDATE这行是防超订的核心它把该会议室当天所有有效预约行锁住其他事务想插重叠段必须等锁释放等到了再查就发现已经有记录了。注意FOR UPDATE必须放在事务里才生效conn.begin()不能省。status 0 if room[need_approval] else 1这行决定了预约是直接生效还是走审批免审批会议室直接置 1用户体验是“点完就约上了”。如果你们用 MySQL 且隔离级别是 RR默认FOR UPDATE在room_id有索引的情况下锁的是行不会锁全表但如果room_id没索引会退化成锁全表并发直接崩所以room_id索引必须有。3.2 用 Redis 锁把同一会议室的提交串行化数据库行锁能防住大部分并发但在“秒杀式抢会议室”场景下大量请求同时打到数据库锁等待会拖慢响应。我一般会在应用层再加一道 Redis 锁key 用lock:room:{room_id}:{date}过期时间设 5 秒抢不到锁的请求直接返回“手速慢了请重试”。这样数据库压力小很多用户体验也更干脆。import redis, uuid, time r redis.Redis(host127.0.0.1, port6379, decode_responsesTrue) def acquire_room_lock(room_id, date_str, timeout5): key flock:room:{room_id}:{date_str} token uuid.uuid4().hex # SET NX EX 原子操作抢到返回 True ok r.set(key, token, nxTrue, extimeout) return token if ok else None def release_room_lock(room_id, date_str, token): key flock:room:{room_id}:{date_str} # Lua 脚本保证“判断 token 再删”的原子性防止误删别人的锁 lua if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end r.eval(lua, 1, key, token)nxTrue, extimeout是原子操作不会出现“先查再设”的竞态。释放锁用 Lua 脚本判断 token是因为如果 A 的锁过期了、B 抢到了锁A 执行完直接del会把 B 的锁删掉导致 C 又能抢进来超订就回来了。这个坑我踩过血泪经验是凡是分布式锁释放必须校验持有者。锁的粒度按“会议室日期”而不是“会议室”是因为同一天不同时段的预约其实不冲突粒度太粗会误伤并发。3.3 审批流与状态流转的接口设计需要审批的会议室预约提交后状态是 0行政在后台点“通过”变 1点“拒绝”变 5。这里有个细节审批通过时也要重新做一次重叠校验因为从提交到审批可能过了几小时期间可能有人约了同一时段且已通过。我一般把审批接口写成“先校验再改状态”校验逻辑复用提交时的isOverlap。状态流转只允许单向0→1、0→5、1→2、1→3、2→4不允许从 5 回到 1也不允许从 4 回到 2。用状态机约束住后面统计“会议室利用率”时数据才干净。接口返回里带上当前状态和可执行操作列表前端按操作列表渲染按钮比前端写死“待审批显示通过/拒绝”更不容易出错。4. 会议室预订预约小程序的签到、释放与避坑排查4.1 签到与超时自动释放怎么做“约了不来”是会议室周转率的最大杀手。我的做法是预约开始后 15 分钟内必须签到签到方式有两种——扫会议室二维码或者连上会议室所在区域的 Wi-Fi 后点签到Wi-Fi 方案需要额外开发小团队用二维码就够了。签到接口把状态从 1 改成 2并记录checkin_time。释放用一个定时任务每 5 分钟扫一次状态为 1、开始时间已过 15 分钟、checkin_time为空的记录状态改成 4已释放并给预约人发一条订阅消息“您的预约因超时未签到已释放”。释放后该时段重新变为可约别人就能捡漏。-- 超时释放每5分钟执行一次注意只释放“已通过但未签到”的 UPDATE room_booking SET status 4 WHERE status 1 AND checkin_time IS NULL AND start_time DATE_SUB(NOW(), INTERVAL 15 MINUTE) AND end_time NOW();end_time NOW()这个条件不能少否则会把“已经开完但忘了签到”的历史预约也释放掉虽然状态改了不影响历史但会触发一堆无意义的释放通知。释放后要不要通知预约人看公司文化我一般通知因为确实有人是“临时不开会但忘了取消”通知一下让他知道系统在管下次就会主动取消。定时任务用UPDATE批量改比逐条查出来再改效率高但要注意单次更新量如果公司有几千条积压分批更新别一次锁太多行。4.2 避坑排查五条我踩过的真实记录现象列表显示空闲提交却提示“已被预约”。原因列表查询和提交校验用了两套时间判断逻辑列表用了BETWEEN提交用了isOverlap对“首尾相接”的判断不一致。解决抽成同一个函数列表和提交都调它改一处两处都生效。现象两个人同时提交都成功了会议室被约两次。原因只做了应用层查询校验没加数据库行锁两个事务同时查到“空闲”然后都插入。解决提交接口加FOR UPDATE并给room_id建索引避免锁全表。现象预约开始后 15 分钟没签到但状态没释放。原因定时任务里start_time DATE_SUB(NOW(), INTERVAL 15 MINUTE)写成了start_time 方向反了。解决定时任务上线前用测试数据跑一遍确认释放的是“已过 15 分钟”而不是“还没到 15 分钟”的。现象行政在后台改了会议室开放时间小程序端还是旧时间。原因会议室列表接口加了 5 分钟缓存改配置后缓存没失效。解决后台改会议室信息时主动删缓存或者把缓存时间降到 1 分钟行政改完等一分钟也能接受。现象用户取消预约后该时段还是显示被占用。原因取消接口把状态改成了 3但列表查询的status IN (0,1,2)没包含 3 是对的问题出在取消接口没提交事务状态没落库。解决所有写操作统一用事务包裹取消接口也要commit。4.3 权限与数据隔离的注意点小程序端拿到的会议室列表不应该包含“停用”的会议室也不应该包含其他部门专属的会议室如果你们有部门专属会议室的话。我一般给会议室表加一个dept_id字段0 表示公共非 0 表示部门专属小程序端查询时带上当前用户的部门 IDWHERE dept_id 0 OR dept_id ?。后台管理员不受此限制。另外用户只能取消自己的预约不能取消别人的这个在接口层用user_id校验别指望前端不显示按钮就安全了。统计接口只给管理员普通用户看不到“谁约了哪个会议室”的全量数据避免隐私问题。5. 会议室预订预约小程序的进阶技巧用“释放率”反推会议室该不该加系统跑起来之后最有价值的不是“预约成功数”而是“释放率”和“高峰时段冲突数”。释放率 已释放数 / 已通过数如果某个会议室释放率长期高于 30%说明要么这个会议室太大、大家约了不用要么签到提醒不够狠。高峰时段冲突数 同一时段被拒绝或提交失败的次数如果某个时段冲突数很高说明会议室不够用该考虑加会议室或者把大会议室拆成两个小的。我一般每周导一次这两组数据用下面这个 SQL 看趋势。-- 按会议室统计近30天释放率和冲突数 SELECT r.name, COUNT(CASE WHEN b.status 4 THEN 1 END) AS released_cnt, COUNT(CASE WHEN b.status IN (1,2) THEN 1 END) AS approved_cnt, ROUND(COUNT(CASE WHEN b.status 4 THEN 1 END) / NULLIF(COUNT(CASE WHEN b.status IN (1,2,4) THEN 1 END), 0), 2) AS release_rate FROM meeting_room r LEFT JOIN room_booking b ON b.room_id r.id AND b.create_time DATE_SUB(NOW(), INTERVAL 30 DAY) GROUP BY r.id ORDER BY release_rate DESC;NULLIF是防止除零某个会议室 30 天没人约时approved_cnt为 0不加NULLIF会报错。release_rate高于 0.3 的会议室我会在后台标红提醒行政去问使用部门是不是会议室配置不合理。另一个技巧是给“高频预约人”做白名单比如行政自己经常要临时约会议室可以给他们开“免审批可插队”权限但插队要记录日志避免滥用。最后说个我自己的习惯每次上线新版本前用两个账号同时点同一个时段的“提交”看是不是只有一个成功这个手动并发测试比写自动化测试脚本还快三十秒就能验完防超订有没有失效。希望帮到你。本文还有配套的精品资源点击获取