新闻
NEWS
网站想做在线预约功能?这段PHP代码可以直接拿去改
  • 来源: 网站建设:www.wsjz.net
  • 时间:2026-06-18 10:45
  • 阅读:6

在线预约系统是现代服务型网站常见的功能模块,它允许访客自助选择时间、填写需求并提交申请,从而替代传统电话或人工登记流程。对于许多中小型项目而言,从头开发一套完整的预约系统成本较高,而直接复用并改造一段稳定的PHP核心代码,则是高效且经济的方案。本文提供一套基础的预约处理逻辑,涵盖数据库设计、表单提交、时段校验、冲突检测及结果返回等关键环节,并预留了充分的扩展接口,方便您根据实际业务场景进行调整。

一、功能定位与适用场景

本段代码面向单资源、多时段的预约模型,例如:咨询时段预约、工位使用申请、设备借用登记、课程试听报名等。其核心能力包括:

  • 展示当前可预约的日期与时间片;

  • 限制每个时段的最大预约人数;

  • 防止同一用户重复提交(基于Session或IP简单限流);

  • 将预约记录写入数据库,并返回成功或失败的状态信息;

  • 基础的时间冲突判断(例如:已满时段不再接受新单)。

若您的业务涉及多资源并行(如多个房间、多位服务人员)、周期性规则(如每周一闭馆)、或复杂的价格计算,本文末尾会给出改造方向,但主体代码依然可作为底层逻辑的起点。

二、运行环境与准备工作

在部署代码前,请确保服务器满足以下最低要求:

  • PHP版本 ≥ 7.4(推荐8.0及以上,以支持更严格的类型声明);

  • MySQL 5.7 或 MariaDB 10.3 以上,并开启PDO扩展;

  • Web服务器(Apache/Nginx)正确配置URL重写规则(非必须,但建议开启);

  • 网站已建立全局数据库连接对象($db),且字符集统一为utf8mb4。

您需要预先创建一张预约记录表,推荐结构如下(可根据实际字段增减):

sql

复制

下载

CREATE TABLE `appointments` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_name` varchar(50) NOT NULL,
  `user_phone` varchar(20) NOT NULL,
  `user_email` varchar(100) DEFAULT NULL,
  `appoint_date` date NOT NULL,
  `appoint_time_slot` tinyint(2) NOT NULL COMMENT '时段编号:1上午 2下午 3晚间等',
  `remark` text DEFAULT NULL,
  `status` tinyint(1) DEFAULT 1 COMMENT '1有效 0取消',
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `ip_address` varchar(45) DEFAULT NULL,
  `session_id` varchar(64) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `unique_booking` (`appoint_date`,`appoint_time_slot`,`user_phone`),
  KEY `idx_date_slot` (`appoint_date`,`appoint_time_slot`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

其中unique_booking唯一索引防止同一手机号在同一天同一时段重复预约,您也可以改为user_email或去掉该约束,改为业务层检测。

同时,建议维护一个时段配置表(或直接写在PHP常量中),便于前端下拉渲染:

sql

复制

下载

CREATE TABLE `time_slots` (
  `slot_id` tinyint(2) NOT NULL,
  `slot_name` varchar(20) NOT NULL,
  `start_time` time NOT NULL,
  `end_time` time NOT NULL,
  `max_capacity` smallint(4) DEFAULT 5,
  `is_active` tinyint(1) DEFAULT 1,
  PRIMARY KEY (`slot_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

初始化几条示例数据:1-上午(09:00-12:00,容量5人),2-下午(14:00-17:00,容量5人),3-晚间(19:00-21:00,容量3人)。

三、核心PHP代码结构(可直接复制修改)

以下为预约提交处理的入口文件 booking_submit.php,采用面向过程风格但清晰分层,方便封装为类。

php

复制

下载

<?php// 开启会话用于简单防刷if (session_status() === PHP_SESSION_NONE) {
    session_start();}// 引入数据库连接(请根据实际路径调整)require_once __DIR__ . '/db_connect.php';// 设置响应格式为JSON(适用于前后端分离或Ajax提交)header('Content-Type: application/json; charset=utf-8');// 仅接受POST请求if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    echo json_encode(['code' => 405, 'msg' => '仅支持POST提交']);
    exit;}// --- 1. 获取并过滤输入 ---$name = trim($_POST['user_name'] ?? '');$phone = trim($_POST['user_phone'] ?? '');$email = isset($_POST['user_email']) ? trim($_POST['user_email']) : null;$date = $_POST['appoint_date'] ?? '';$slotId = (int)($_POST['slot_id'] ?? 0);$remark = isset($_POST['remark']) ? trim($_POST['remark']) : '';// 基础验证:必填项$errors = [];if (mb_strlen($name) < 2 || mb_strlen($name) > 30) {
    $errors[] = '姓名请填写2~30个字符';}if (!preg_match('/^1[3-9]\d{9}$/', $phone)) {  // 简单手机号格式(可替换为更宽松规则)
    $errors[] = '手机号格式不正确';}if ($email && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
    $errors[] = '邮箱格式无效';}if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
    $errors[] = '日期格式错误,请使用YYYY-MM-DD';}if ($slotId < 1) {
    $errors[] = '请选择有效的时段';}// 额外日期校验:不能早于今天(允许当天,可根据业务修改)$today = date('Y-m-d');if ($date < $today) {
    $errors[] = '预约日期不能早于今天';}// 可选:限制最大提前天数(例如30天)$maxDate = date('Y-m-d', strtotime('+30 days'));if ($date > $maxDate) {
    $errors[] = '仅支持未来30天内的预约';}if (!empty($errors)) {
    echo json_encode(['code' => 422, 'msg' => '参数验证失败', 'detail' => $errors]);
    exit;}// --- 2. 防刷与重复提交检查(轻量级) ---$sessionId = session_id();$clientIp = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';// 同一会话或IP 10秒内只能提交一次(避免暴力点击)$lockKey = 'booking_lock_' . md5($sessionId . $clientIp);if (isset($_SESSION[$lockKey]) && (time() - $_SESSION[$lockKey] < 10)) {
    echo json_encode(['code' => 429, 'msg' => '提交过于频繁,请稍后再试']);
    exit;}$_SESSION[$lockKey] = time();// --- 3. 事务内执行核心业务 ---try {
    $db->beginTransaction();

    // 3.1 查询时段配置,获取容量和有效性
    $slotStmt = $db->prepare("SELECT slot_id, max_capacity, is_active FROM time_slots WHERE slot_id = ?");
    $slotStmt->execute([$slotId]);
    $slot = $slotStmt->fetch(PDO::FETCH_ASSOC);
    if (!$slot || $slot['is_active'] != 1) {
        throw new Exception('所选时段当前不可用');
    }
    $maxCap = (int)$slot['max_capacity'];

    // 3.2 统计该日期+时段已有的有效预约数
    $countStmt = $db->prepare("SELECT COUNT(*) AS booked FROM appointments 
                               WHERE appoint_date = ? AND appoint_time_slot = ? AND status = 1");
    $countStmt->execute([$date, $slotId]);
    $row = $countStmt->fetch(PDO::FETCH_ASSOC);
    $bookedCount = (int)$row['booked'];

    if ($bookedCount >= $maxCap) {
        throw new Exception('该时段预约已满,请选择其他时间');
    }

    // 3.3 可选:同一手机号当天同时段是否已存在(与唯一索引互补,返回友好提示)
    $dupStmt = $db->prepare("SELECT id FROM appointments WHERE appoint_date = ? AND appoint_time_slot = ? AND user_phone = ? AND status = 1");
    $dupStmt->execute([$date, $slotId, $phone]);
    if ($dupStmt->fetch()) {
        throw new Exception('您已在该时段预约,请勿重复提交');
    }

    // 3.4 插入预约记录
    $insertSql = "INSERT INTO appointments 
                  (user_name, user_phone, user_email, appoint_date, appoint_time_slot, remark, ip_address, session_id) 
                  VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
    $insertStmt = $db->prepare($insertSql);
    $result = $insertStmt->execute([
        $name,
        $phone,
        $email,
        $date,
        $slotId,
        $remark,
        $clientIp,
        $sessionId
    ]);

    if (!$result || $insertStmt->rowCount() < 1) {
        throw new Exception('数据写入失败,请重试');
    }

    // 提交事务
    $db->commit();

    // 返回成功信息(包含预约ID,便于后续查询)
    $newId = $db->lastInsertId();
    echo json_encode([
        'code' => 200,
        'msg' => '预约成功!',
        'data' => ['appointment_id' => $newId, 'date' => $date, 'slot' => $slotId]
    ]);} catch (Exception $e) {
    $db->rollBack();
    // 记录错误日志(建议写入文件,不暴露给前端)
    error_log('[Booking Error] ' . $e->getMessage() . ' | IP: ' . $clientIp);
    echo json_encode(['code' => 500, 'msg' => $e->getMessage()]);
    exit;}

四、前端调用示例与数据获取

在实际项目中,您需要配套一个前端预约表单页面,通过Ajax将数据提交至上述接口。最简单的HTML+JS片段如下(省略样式):

html

复制

下载

运行

<form id="bookingForm">
  <input type="text" name="user_name" placeholder="您的姓名" required>
  <input type="tel" name="user_phone" placeholder="手机号" required>
  <input type="email" name="user_email" placeholder="邮箱(选填)">
  <input type="date" name="appoint_date" min="<?= date('Y-m-d') ?>" max="<?= date('Y-m-d', strtotime('+30 days')) ?>">
  <select name="slot_id">
    <option value="1">上午 09:00-12:00</option>
    <option value="2">下午 14:00-17:00</option>
    <option value="3">晚间 19:00-21:00</option>
  </select>
  <textarea name="remark" placeholder="备注需求"></textarea>
  <button type="submit">提交预约</button></form><script>document.getElementById('bookingForm').addEventListener('submit', async function(e) {
  e.preventDefault();
  const formData = new FormData(this);
  const resp = await fetch('/booking_submit.php', { method: 'POST', body: formData });
  const result = await resp.json();
  if (result.code === 200) {
    alert(result.msg + ' 预约编号:' + result.data.appointment_id);
  } else {
    alert('错误:' + (result.detail ? result.detail.join('\n') : result.msg));
  }
});</script>

同时,您可能还需要提供可预约时段查询接口(例如:根据日期返回剩余名额),避免用户反复提交后才发现已满。该接口只需复用上述统计逻辑,以GET方式返回JSON,此处不再赘述。

五、改造与扩展方向(针对复杂业务)

  1. 多资源并行预约
    在预约表中增加 resource_id 字段,并在统计容量时增加 AND resource_id = ? 条件。前端需先选择资源,再选时段。

  2. 周期性规则(如每周二、四开放)
    可在时段配置表中增加 weekday_mask(如二进制位表示周几),在提交前校验当前日期是否满足规则。

  3. 支付或押金关联
    在插入记录后,可调用第三方支付接口生成预支付订单,并将订单号存入 payment_txn 字段,状态改为“待支付”,支付成功后再变更为“有效”。

  4. 管理后台审核
    增加 status 枚举值(待审核、已确认、已取消),并在提交后发送邮件/短信通知管理员。此段代码目前仅支持“有效/取消”两种状态,可按需扩充。

  5. 邮件或短信提醒
    在事务提交成功后,异步调用消息队列或直接发送(注意性能),提醒用户预约详情。

  6. 时区与节假日处理
    若面向多时区用户,统一存储UTC时间,并在展示时转换。节假日可维护一张闭馆日期表,在验证时排除。

  7. 防止并发超卖
    虽然使用了事务和唯一索引,但在极高并发下(如秒杀类预约),建议增加 SELECT ... FOR UPDATE 行锁,或使用Redis原子递减库存。本代码适用于中小流量(每分钟几十次提交)。

  8. 日志与监控
    在 catch 块中,除了 error_log,可接入更完善的日志库,记录请求参数和堆栈,便于排查。

六、安全加固建议(必须执行)

  • SQL注入防护:本代码已使用PDO预处理,但仍需确保所有动态字段均通过参数绑定,尤其注意 order by 或 表名 等无法绑定的位置。

  • XSS过滤:输出到前端的用户数据(如姓名、备注)需进行 htmlspecialchars 转义,本接口返回JSON,前端渲染时需自行防范。

  • CSRF防护:建议在表单中植入一次性令牌(Token),并在服务端校验。

  • 敏感数据脱敏:日志中不应记录完整手机号或邮箱,可部分掩码。

  • 限制请求频率:除Session锁外,建议在Nginx或防火墙层设置IP限流(例如每分钟10次)。

  • HTTPS强制:全程使用加密传输,避免中间人截获预约信息。

七、部署与测试要点

  1. 将代码放置于网站目录,确保 db_connect.php 正确配置数据库账号密码。

  2. 使用测试工具(如Postman)模拟POST请求,验证正常提交、重复提交、满额、日期越界等场景。

  3. 观察数据库记录,检查字符集是否乱码,时区是否一致。

  4. 开启PHP错误日志,但关闭 display_errors,避免暴露路径信息。

  5. 若使用缓存或CDN,注意动态接口不应被缓存(添加 Cache-Control: no-cache 响应头)。

八、维护与迭代记录

建议将本代码纳入版本管理,并在每次修改时记录变更日志。例如,当您调整时段容量或新增字段时,同步更新数据表结构脚本。对于预约状态的更新(如管理员取消),可另写 booking_update.php,仅允许授权身份访问。

结语

上述代码提供了一套可运行、可修改的预约功能雏形,覆盖了从数据验证到事务写入的完整链路。您完全可以根据自己的业务规则,替换验证正则、调整容量逻辑、增加额外字段,或将其重构为面向对象的类结构。关键在于理解每一段代码的职责,并依照实际需求进行裁剪。切记在修改后,对边界条件进行充分测试,确保线上运行的稳定性。希望这段代码能成为您构建在线预约系统的坚实基础,让您将更多精力投入到个性化体验与运营优化之中。

分享 SHARE
在线咨询
联系电话

13463989299