☰
会议室预订预约小程序前后台源码:防超订与自动释放实战
2026/10/6 3:48:15 网站建设 项目流程

简介:这是一套面向写字楼、高校及创业园区的会议室在线预订系统源码,采用小程序前端与原生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:00,B 约 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 status=1", (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(host='127.0.0.1', port=6379, decode_responses=True) def acquire_room_lock(room_id, date_str, timeout=5): key = f"lock:room:{room_id}:{date_str}" token = uuid.uuid4().hex # SET NX EX 原子操作,抢到返回 True ok = r.set(key, token, nx=True, ex=timeout) return token if ok else None def release_room_lock(room_id, date_str, token): key = f"lock: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)

nx=True, ex=timeout是原子操作,不会出现“先查再设”的竞态。释放锁用 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 表示部门专属,小程序端查询时带上当前用户的部门 ID,WHERE 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 的会议室,我会在后台标红,提醒行政去问使用部门是不是会议室配置不合理。另一个技巧是给“高频预约人”做白名单,比如行政自己经常要临时约会议室,可以给他们开“免审批+可插队”权限,但插队要记录日志,避免滥用。最后说个我自己的习惯:每次上线新版本前,用两个账号同时点同一个时段的“提交”,看是不是只有一个成功,这个手动并发测试比写自动化测试脚本还快,三十秒就能验完防超订有没有失效。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询