简介:这是一套面向写字楼、高校及创业园区的会议室在线预订系统源码,采用小程序前端与原生PHP后台相结合的方式,解决会议室资源查询、时段预约与现场核销的管理需求,适合具备一定小程序与PHP基础的开发者二次开发或课程设计参考。压缩包为rar格式,共1185个文件,约1021KB,以js、ts、wxss、wxml、json等小程序页面与逻辑文件为主,配合wxs脚本、png图片及若干配置与说明文件,前后台模块划分清晰,便于按功能检索。目前已有4147人学习下载。资源完整覆盖预约下单、会议室资源管理、预订记录维护与二维码现场核销等核心流程,后台基于原生PHP实现,便于理解业务逻辑与数据处理方式,读者可据此快速搭建可运行的预约系统,并在此基础上扩展定时预约、多人共享及系统集成等场景。
1. 会议室预订预约小程序:从「抢会议室」到「扫码即用」的完整落地路径
行政群里每天上午九点准时上演的「抢会议室大战」,本质上是信息不对称——谁先到谁占,谁嗓门大谁赢。会议室预订预约小程序要解决的就是把「占坑」变成「预约」,把「口头确认」变成「系统留痕」。这套前后台源码方案适合两类人:一是公司内部IT或行政,想用最低成本把会议室管理跑起来;二是接私活的开发者,客户要一个能扫码看空闲、能审批、能统计使用率的轻量系统。核心链路不复杂:用户端选时间段提交预约,后台审核并释放冲突时段,管理员看板统计利用率。真正花时间的不是写页面,而是时间段冲突检测、审批流状态机和并发下的数据一致性。下面按「能跑起来 → 能管起来 → 能扛住用」的顺序拆。
2. 前后台源码怎么拆:技术选型与最小可运行架构
2.1 为什么选「小程序 + 轻后端」而不是纯前端方案
会议室预订预约小程序的前台通常跑在微信小程序里,后台管理用Web端。纯前端方案(比如用云开发数据库直接读写)在演示阶段很爽,但一旦涉及审批流、冲突检测、操作日志,就会变成玄学——你没法在客户端可靠地做事务。常见做法是:小程序端只负责展示和提交,所有写操作走服务端API,服务端做冲突校验和状态流转。
技术栈上,我一般会选:
| 层 | 选型 | 理由 |
|---|---|---|
| 小程序端 | 原生小程序或uni-app | 原生性能好,uni-app多端省事 |
| 后端 | Node.js (Express/Koa) 或 Python (FastAPI) | 轻量、上手快、生态够用 |
| 数据库 | MySQL 或 PostgreSQL | 时间段查询和唯一约束成熟 |
| 后台管理 | Vue3 + Element Plus | 表格、表单、审批流组件现成 |
| 部署 | 单台云服务器 + Nginx | 会议室场景并发不高,够用 |
这套组合的边界很清楚:日预约量在几百到几千条、同时在线几十人,完全扛得住。再往上走才需要考虑Redis缓存时段、消息队列解耦通知。
2.2 数据库表设计:三张核心表撑起整个预约逻辑
前后台源码能不能改得动,第一眼看表结构。会议室预订预约小程序的核心表就三张:会议室表、预约记录表、用户/审批表。下面给出最小可用DDL,字段注释写清楚用途。
-- 会议室表:存物理空间信息 CREATE TABLE meeting_room ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL COMMENT '会议室名称,如“3楼-黄河厅”', location VARCHAR(128) COMMENT '位置描述', capacity INT DEFAULT 0 COMMENT '容纳人数', status TINYINT DEFAULT 1 COMMENT '1可用 0停用', open_time TIME DEFAULT '08:00:00' COMMENT '每日开放起始', close_time TIME DEFAULT '20:00:00' COMMENT '每日开放结束' ); -- 预约记录表:核心表,冲突检测靠它 CREATE TABLE booking ( id INT PRIMARY KEY AUTO_INCREMENT, room_id INT NOT NULL, user_id INT NOT NULL, book_date DATE NOT NULL COMMENT '预约日期', start_time DATETIME NOT NULL COMMENT '开始时间(含日期)', end_time DATETIME NOT NULL COMMENT '结束时间', title VARCHAR(128) COMMENT '会议主题', status TINYINT DEFAULT 0 COMMENT '0待审批 1已通过 2已拒绝 3已取消', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_room_date (room_id, book_date), INDEX idx_status (status) ); -- 审批日志表:留痕,方便追责和排查 CREATE TABLE booking_log ( id INT PRIMARY KEY AUTO_INCREMENT, booking_id INT NOT NULL, operator_id INT NOT NULL, action VARCHAR(32) COMMENT 'approve/reject/cancel', remark VARCHAR(255), created_at DATETIME DEFAULT CURRENT_TIMESTAMP );逻辑说明:booking表用room_id + book_date建联合索引,是因为冲突检测的查询条件永远是「某会议室某天」。start_time和end_time存完整DATETIME而不是分开存日期和时间,是为了避免跨天预约时拼接出错。参数上,status用TINYINT而不是枚举字符串,查询快且省空间。
注意:不要用「时间段字符串」存预约,比如存"09:00-10:00"。一旦要查「10:00有没有空」,字符串比较会翻车。
2.3 最小可运行后端:三个接口跑通预约闭环
前后台源码里后端最核心的就三个接口:查空闲时段、提交预约、审批。下面用FastAPI写最小实现,重点看冲突检测那段。
from fastapi import FastAPI, HTTPException from pydantic import BaseModel from datetime import datetime, date import pymysql app = FastAPI() class BookingReq(BaseModel): room_id: int user_id: int start_time: datetime end_time: datetime title: str @app.get("/rooms/{room_id}/slots") def get_slots(room_id: int, day: date): """查某会议室某天已被占用的时段""" conn = pymysql.connect(host='localhost', user='root', password='', db='meeting') cur = conn.cursor() cur.execute(""" SELECT start_time, end_time FROM booking WHERE room_id=%s AND book_date=%s AND status IN (0,1) """, (room_id, day)) rows = cur.fetchall() conn.close() return {"busy": [{"start": str(r[0]), "end": str(r[1])} for r in rows]} @app.post("/booking") def create_booking(req: BookingReq): if req.start_time >= req.end_time: raise HTTPException(400, "结束时间必须晚于开始时间") conn = pymysql.connect(host='localhost', user='root', password='', db='meeting') cur = conn.cursor() # 冲突检测:同会议室、同天、状态有效、时间段有重叠 cur.execute(""" SELECT COUNT(*) FROM booking WHERE room_id=%s AND book_date=%s AND status IN (0,1) AND start_time < %s AND end_time > %s """, (req.room_id, req.start_time.date(), req.end_time, req.start_time)) if cur.fetchone()[0] > 0: conn.close() raise HTTPException(409, "该时段已被预约") cur.execute(""" INSERT INTO booking (room_id, user_id, book_date, start_time, end_time, title, status) VALUES (%s, %s, %s, %s, %s, %s, 0) """, (req.room_id, req.user_id, req.start_time.date(), req.start_time, req.end_time, req.title)) conn.commit() conn.close() return {"msg": "提交成功,等待审批"}逻辑说明:冲突检测的SQL条件是start_time < 新end AND end_time > 新start,这是判断两个时间段是否重叠的标准写法。参数上,status IN (0,1)表示待审批和已通过的都算占用,已拒绝和已取消的不算。这里没有加锁,高并发下两个请求可能同时通过检测然后都插入——后面避坑章节会讲怎么补。
2.4 后台管理端:审批列表和状态流转
后台管理用Vue3写,核心是一个审批列表页加一个状态流转按钮。前端不直接改数据库,调后端接口。审批接口逻辑:
// 审批操作:通过或拒绝 async function approve(bookingId, action, remark) { // action: 'approve' | 'reject' const res = await axios.post('/api/booking/audit', { booking_id: bookingId, action: action, remark: remark }) if (res.data.code === 0) { // 刷新列表 await loadBookingList() } }后端对应接口要做三件事:校验当前状态是否允许审批(只有status=0能审批)、更新booking状态、写booking_log。参数上,remark在拒绝时必填,通过时可选。状态机很简单:0→1(通过)、0→2(拒绝)、1→3(取消)。不允许从2直接到1,避免审批翻烧饼。
3. 时间段冲突检测与并发控制:别让两个人订到同一间
3.1 冲突检测的三种写法与性能差异
会议室预订预约小程序最容易出bug的地方就是冲突检测。常见三种写法:
第一种,应用层查出来再比较。先把当天该会议室所有预约查出来,在代码里循环判断有没有重叠。优点是逻辑直观,缺点是数据量大时慢,而且并发下不可靠。
第二种,数据库层用SQL条件判断。就是上面写的start_time < end AND end_time > start。优点是快,走索引,缺点是并发下仍有竞态。
第三种,数据库唯一约束或排他锁。用SELECT ... FOR UPDATE锁住相关行,或者用数据库的排他约束。优点是可靠,缺点是实现复杂,且需要事务支持。
我一般用第二种做常规检测,第三种做兜底。具体做法:在booking表上加一个slot_key字段,存room_id + date + start_time的哈希,建唯一索引。插入时如果冲突会直接报唯一键错误,捕获后返回「已被预约」。这样即使两个请求同时通过SQL检测,也只有一个能插入成功。
ALTER TABLE booking ADD COLUMN slot_key VARCHAR(64) UNIQUE; -- 插入时计算 slot_key = md5(concat(room_id, book_date, start_time))注意:slot_key方案只能防「开始时间完全相同」的冲突,如果一个是09:00-10:00、另一个是09:30-10:30,slot_key不同但实际重叠。所以slot_key是兜底,不能替代SQL重叠检测。
3.2 用事务和行锁把并发问题摁住
更稳妥的做法是在事务里用FOR UPDATE。修改create_booking:
@app.post("/booking") def create_booking(req: BookingReq): conn = pymysql.connect(host='localhost', user='root', password='', db='meeting', autocommit=False) try: cur = conn.cursor() # 锁住该会议室当天的预约记录 cur.execute(""" SELECT id FROM booking WHERE room_id=%s AND book_date=%s AND status IN (0,1) FOR UPDATE """, (req.room_id, req.start_time.date())) # 再查重叠 cur.execute(""" SELECT COUNT(*) FROM booking WHERE room_id=%s AND book_date=%s AND status IN (0,1) AND start_time < %s AND end_time > %s """, (req.room_id, req.start_time.date(), req.end_time, req.start_time)) if cur.fetchone()[0] > 0: conn.rollback() raise HTTPException(409, "该时段已被预约") cur.execute("""INSERT INTO booking ...""") conn.commit() except Exception as e: conn.rollback() raise e finally: conn.close()逻辑说明:FOR UPDATE会锁住查询到的行,其他事务在提交前无法修改这些行。参数上,autocommit=False必须设,否则每条SQL自动提交,事务失效。这个方案在MySQL InnoDB下有效,MyISAM不支持行锁。
3.3 跨天预约和时区问题的处理
会议室预订预约小程序如果只服务一个时区,问题不大。但如果有跨天预约(比如晚上10点到第二天凌晨1点),book_date字段就会尴尬——按开始时间算还是结束时间算?我的做法是:book_date始终按开始时间所在日期存,冲突检测时把查询范围放宽到book_date IN (当天, 当天+1),避免跨天预约漏检。
时区上,所有时间统一存UTC或统一存本地时间,不要混。小程序端传时间时带时区偏移,后端统一转换。常见翻车场景:服务器时区是UTC,数据库存的是本地时间,前端显示又按手机时区转,最后预约时间对不上。
4. 审批流与通知机制:让预约状态「活」起来
4.1 审批状态机的设计与边界
会议室预订预约小程序的审批流不能太复杂,否则行政嫌烦。我一般设计成两级:普通用户预约需要审批,管理员预约直接通过。状态机如下:
| 当前状态 | 允许操作 | 目标状态 | 操作人 |
|---|---|---|---|
| 0待审批 | 通过 | 1已通过 | 管理员 |
| 0待审批 | 拒绝 | 2已拒绝 | 管理员 |
| 0待审批 | 取消 | 3已取消 | 预约人 |
| 1已通过 | 取消 | 3已取消 | 预约人或管理员 |
| 2已拒绝 | 无 | - | - |
| 3已取消 | 无 | - | - |
边界在于:已拒绝的不能重新审批,已取消的不能恢复。如果用户想改时间,只能取消后重新预约。这样逻辑简单,日志清晰。
4.2 小程序端订阅消息与后台通知
审批结果要通知到人。小程序端用订阅消息,后台管理端用站内信或邮件。订阅消息的关键是模板ID和跳转页面配置。
// 小程序端:审批通过后发送订阅消息 wx.requestSubscribeMessage({ tmplIds: ['你的模板ID'], success(res) { if (res['你的模板ID'] === 'accept') { // 用户同意订阅,后端可发消息 } } })后端发消息时调微信接口,参数包括touser(用户openid)、template_id、page(跳转页面)、data(模板内容)。注意:订阅消息是一次性的,用户同意一次只能发一条。所以要在用户提交预约时就让其订阅,审批完成时发。
注意:不要频繁弹订阅授权,用户会烦。我一般只在提交预约那一步弹一次,审批结果用站内信兜底。
4.3 预约提醒与超时释放
会议室预订预约小程序还有一个实用功能:会议开始前15分钟提醒,开始后10分钟未签到自动释放。提醒用定时任务扫booking表,找start_time在15分钟后的记录,发通知。超时释放则是找start_time已过10分钟且status=1的记录,改成3已取消,释放时段。
# 定时任务:每分钟跑一次 def auto_release(): conn = pymysql.connect(...) cur = conn.cursor() cur.execute(""" UPDATE booking SET status=3 WHERE status=1 AND start_time < NOW() - INTERVAL 10 MINUTE AND id NOT IN (SELECT booking_id FROM checkin) """) conn.commit()参数上,INTERVAL 10 MINUTE可以根据公司习惯调,有的公司是5分钟,有的是15分钟。签到表checkin记录用户扫码签到,没有签到记录的才释放。
5. 避坑与排查:会议室预订系统上线后最容易翻车的5个点
5.1 现象:两个人同时提交,都显示预约成功
原因:冲突检测在应用层做,两个请求几乎同时查数据库,都查到没有冲突,然后都插入。解决:用事务加FOR UPDATE行锁,或者加唯一约束兜底。上面3.2节给了完整代码。血泪经验是:演示时用一个人操作永远发现不了,上线第一天就翻车。
5.2 现象:审批通过后,用户端还是显示「待审批」
原因:小程序端缓存了列表数据,审批后没有刷新。或者后端更新了booking状态但没更新缓存。解决:审批接口返回后,前端强制重新拉取列表;如果用了Redis缓存,审批时删掉对应缓存key。我一般会在审批成功后让前端调一次loadBookingList(),不依赖本地状态。
5.3 现象:跨天预约(22:00-次日01:00)查不到冲突
原因:book_date只存了开始日期,冲突检测时只查了当天,没有查次日。解决:冲突检测的日期条件改成book_date IN (开始日期, 开始日期+1天),或者干脆用start_time和end_time的DATETIME直接比较,不依赖book_date。后者更稳,但索引要调整。
5.4 现象:会议室开放时间外也能预约
原因:后端没有校验open_time和close_time。解决:在create_booking里加校验,req.start_time.time()必须大于等于room.open_time,req.end_time.time()必须小于等于room.close_time。参数从meeting_room表查。注意跨天预约时这个校验要特殊处理,比如允许结束时间在次日close_time之前。
5.5 现象:取消预约后时段没释放,别人还是订不了
原因:冲突检测的SQL里status IN (0,1),取消后status=3,按理说应该释放。但如果代码里写的是status != 2,就会把已取消的也算进去。解决:统一用status IN (0,1)表示有效占用,已拒绝(2)和已取消(3)都不算。排查时直接查SELECT * FROM booking WHERE room_id=? AND book_date=?看status值。
6. 从能用 to 好用:三个提升预约体验的进阶技巧
6.1 用「时间段模板」减少用户选择成本
用户预约时最烦的是选时间。我一般会在后台配置几个常用时段模板,比如「09:00-10:00」「10:00-11:00」「14:00-15:00」,小程序端直接展示成按钮,点一下选中,不用手动拖时间选择器。实现上,加一张slot_template表,后台可增删改,小程序端拉取后渲染。
CREATE TABLE slot_template ( id INT PRIMARY KEY AUTO_INCREMENT, label VARCHAR(32) COMMENT '如“上午第一场”', start_time TIME NOT NULL, end_time TIME NOT NULL, sort_order INT DEFAULT 0 );这个改动很小,但用户满意度提升明显。尤其是行政帮别人代订时,点按钮比选时间快得多。
6.2 用「使用率统计」帮行政做决策
后台管理端加一个统计页,按会议室、按周、按月统计预约次数和实际使用时长。SQL用GROUP BY就能出:
SELECT room_id, COUNT(*) AS book_count, SUM(TIMESTAMPDIFF(MINUTE, start_time, end_time)) AS total_minutes FROM booking WHERE status=1 AND book_date BETWEEN ? AND ? GROUP BY room_id;参数上,status=1只统计已通过的,已取消的不算。这个数据能帮行政判断哪些会议室该扩、哪些该合并。我做过的一个项目里,统计发现最大的会议室使用率只有30%,后来改成了两个小会议室,利用率直接上去了。
6.3 扫码签到与自动释放的联动
每个会议室门口贴一张二维码,用户到场后扫码签到。签到接口写checkin表,同时把booking状态标记为「已签到」。定时任务扫未签到的已通过预约,超时自动释放。这样能解决「订了不来」的老大难问题。
@app.post("/checkin") def checkin(booking_id: int, user_id: int): conn = pymysql.connect(...) cur = conn.cursor() # 校验预约存在且属于该用户 cur.execute("SELECT id FROM booking WHERE id=%s AND user_id=%s AND status=1", (booking_id, user_id)) if not cur.fetchone(): raise HTTPException(403, "无权签到或预约无效") cur.execute("INSERT INTO checkin (booking_id, user_id, checkin_time) VALUES (%s, %s, NOW())", (booking_id, user_id)) conn.commit()参数上,签到时间由服务端生成,不信任客户端传的时间。签到成功后可以给用户返回一个「签到成功」的提示,同时更新预约状态为「进行中」。
这套前后台源码方案我从头搭过三次,每次都会在冲突检测和审批流上花最多时间。最深的教训是:不要相信「演示没问题」,一定要用并发工具压一下预约接口,哪怕只是用两个浏览器同时提交。另外,数据库表结构一旦上线就别轻易改,尤其是booking表,改字段意味着历史数据要迁移。我现在的习惯是,设计阶段就把status的所有可能值列全,宁可多留一个「已归档」状态,也别上线后加字段。希望帮到你。
本文还有配套的精品资源,点击获取