
在線預(yù)約系統(tǒng)是現(xiàn)代服務(wù)型網(wǎng)站常見的功能模塊,它允許訪客自助選擇時(shí)間、填寫需求并提交申請(qǐng),從而替代傳統(tǒng)電話或人工登記流程。對(duì)于許多中小型項(xiàng)目而言,從頭開發(fā)一套完整的預(yù)約系統(tǒng)成本較高,而直接復(fù)用并改造一段穩(wěn)定的PHP核心代碼,則是高效且經(jīng)濟(jì)的方案。本文提供一套基礎(chǔ)的預(yù)約處理邏輯,涵蓋數(shù)據(jù)庫(kù)設(shè)計(jì)、表單提交、時(shí)段校驗(yàn)、沖突檢測(cè)及結(jié)果返回等關(guān)鍵環(huán)節(jié),并預(yù)留了充分的擴(kuò)展接口,方便您根據(jù)實(shí)際業(yè)務(wù)場(chǎng)景進(jìn)行調(diào)整。
本段代碼面向單資源、多時(shí)段的預(yù)約模型,例如:咨詢時(shí)段預(yù)約、工位使用申請(qǐng)、設(shè)備借用登記、課程試聽報(bào)名等。其核心能力包括:
展示當(dāng)前可預(yù)約的日期與時(shí)間片;
限制每個(gè)時(shí)段的最大預(yù)約人數(shù);
防止同一用戶重復(fù)提交(基于Session或IP簡(jiǎn)單限流);
將預(yù)約記錄寫入數(shù)據(jù)庫(kù),并返回成功或失敗的狀態(tài)信息;
基礎(chǔ)的時(shí)間沖突判斷(例如:已滿時(shí)段不再接受新單)。
若您的業(yè)務(wù)涉及多資源并行(如多個(gè)房間、多位服務(wù)人員)、周期性規(guī)則(如每周一閉館)、或復(fù)雜的價(jià)格計(jì)算,本文末尾會(huì)給出改造方向,但主體代碼依然可作為底層邏輯的起點(diǎn)。
在部署代碼前,請(qǐng)確保服務(wù)器滿足以下最低要求:
PHP版本 ≥ 7.4(推薦8.0及以上,以支持更嚴(yán)格的類型聲明);
MySQL 5.7 或 MariaDB 10.3 以上,并開啟PDO擴(kuò)展;
Web服務(wù)器(Apache/Nginx)正確配置URL重寫規(guī)則(非必須,但建議開啟);
網(wǎng)站已建立全局?jǐn)?shù)據(jù)庫(kù)連接對(duì)象($db),且字符集統(tǒng)一為utf8mb4。
您需要預(yù)先創(chuàng)建一張預(yù)約記錄表,推薦結(jié)構(gòu)如下(可根據(jù)實(shí)際字段增減):
sql
復(fù)制
下載
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?'時(shí)段編號(hào):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唯一索引防止同一手機(jī)號(hào)在同一天同一時(shí)段重復(fù)預(yù)約,您也可以改為user_email或去掉該約束,改為業(yè)務(wù)層檢測(cè)。
同時(shí),建議維護(hù)一個(gè)時(shí)段配置表(或直接寫在PHP常量中),便于前端下拉渲染:
sql
復(fù)制
下載
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;
初始化幾條示例數(shù)據(jù):1-上午(09:00-12:00,容量5人),2-下午(14:00-17:00,容量5人),3-晚間(19:00-21:00,容量3人)。
以下為預(yù)約提交處理的入口文件?booking_submit.php,采用面向過程風(fēng)格但清晰分層,方便封裝為類。
php
復(fù)制
下載
<?php//?開啟會(huì)話用于簡(jiǎn)單防刷if?(session_status()?===?PHP_SESSION_NONE)?{
????session_start();}//?引入數(shù)據(jù)庫(kù)連接(請(qǐng)根據(jù)實(shí)際路徑調(diào)整)require_once?__DIR__?.?'/db_connect.php';//?設(shè)置響應(yīng)格式為JSON(適用于前后端分離或Ajax提交)header('Content-Type:?application/json;?charset=utf-8');//?僅接受POST請(qǐng)求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'])?:?'';//?基礎(chǔ)驗(yàn)證:必填項(xiàng)$errors?=?[];if?(mb_strlen($name)?<?2?||?mb_strlen($name)?>?30)?{
????$errors[]?=?'姓名請(qǐng)?zhí)顚?~30個(gè)字符';}if?(!preg_match('/^1[3-9]\d{9}$/',?$phone))?{??//?簡(jiǎn)單手機(jī)號(hào)格式(可替換為更寬松規(guī)則)
????$errors[]?=?'手機(jī)號(hào)格式不正確';}if?($email?&&?!filter_var($email,?FILTER_VALIDATE_EMAIL))?{
????$errors[]?=?'郵箱格式無效';}if?(!preg_match('/^\d{4}-\d{2}-\d{2}$/',?$date))?{
????$errors[]?=?'日期格式錯(cuò)誤,請(qǐng)使用YYYY-MM-DD';}if?($slotId?<?1)?{
????$errors[]?=?'請(qǐng)選擇有效的時(shí)段';}//?額外日期校驗(yàn):不能早于今天(允許當(dāng)天,可根據(jù)業(yè)務(wù)修改)$today?=?date('Y-m-d');if?($date?<?$today)?{
????$errors[]?=?'預(yù)約日期不能早于今天';}//?可選:限制最大提前天數(shù)(例如30天)$maxDate?=?date('Y-m-d',?strtotime('+30?days'));if?($date?>?$maxDate)?{
????$errors[]?=?'僅支持未來30天內(nèi)的預(yù)約';}if?(!empty($errors))?{
????echo?json_encode(['code'?=>?422,?'msg'?=>?'參數(shù)驗(yàn)證失敗',?'detail'?=>?$errors]);
????exit;}//?---?2.?防刷與重復(fù)提交檢查(輕量級(jí))?---$sessionId?=?session_id();$clientIp?=?$_SERVER['REMOTE_ADDR']????'0.0.0.0';//?同一會(huì)話或IP?10秒內(nèi)只能提交一次(避免暴力點(diǎn)擊)$lockKey?=?'booking_lock_'?.?md5($sessionId?.?$clientIp);if?(isset($_SESSION[$lockKey])?&&?(time()?-?$_SESSION[$lockKey]?<?10))?{
????echo?json_encode(['code'?=>?429,?'msg'?=>?'提交過于頻繁,請(qǐng)稍后再試']);
????exit;}$_SESSION[$lockKey]?=?time();//?---?3.?事務(wù)內(nèi)執(zhí)行核心業(yè)務(wù)?---try?{
????$db->beginTransaction();
????//?3.1?查詢時(shí)段配置,獲取容量和有效性
????$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('所選時(shí)段當(dāng)前不可用');
????}
????$maxCap?=?(int)$slot['max_capacity'];
????//?3.2?統(tǒng)計(jì)該日期+時(shí)段已有的有效預(yù)約數(shù)
????$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('該時(shí)段預(yù)約已滿,請(qǐng)選擇其他時(shí)間');
????}
????//?3.3?可選:同一手機(jī)號(hào)當(dāng)天同時(shí)段是否已存在(與唯一索引互補(bǔ),返回友好提示)
????$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('您已在該時(shí)段預(yù)約,請(qǐng)勿重復(fù)提交');
????}
????//?3.4?插入預(yù)約記錄
????$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('數(shù)據(jù)寫入失敗,請(qǐng)重試');
????}
????//?提交事務(wù)
????$db->commit();
????//?返回成功信息(包含預(yù)約ID,便于后續(xù)查詢)
????$newId?=?$db->lastInsertId();
????echo?json_encode([
????????'code'?=>?200,
????????'msg'?=>?'預(yù)約成功!',
????????'data'?=>?['appointment_id'?=>?$newId,?'date'?=>?$date,?'slot'?=>?$slotId]
????]);}?catch?(Exception?$e)?{
????$db->rollBack();
????//?記錄錯(cuò)誤日志(建議寫入文件,不暴露給前端)
????error_log('[Booking?Error]?'?.?$e->getMessage()?.?'?|?IP:?'?.?$clientIp);
????echo?json_encode(['code'?=>?500,?'msg'?=>?$e->getMessage()]);
????exit;}
在實(shí)際項(xiàng)目中,您需要配套一個(gè)前端預(yù)約表單頁(yè)面,通過Ajax將數(shù)據(jù)提交至上述接口。最簡(jiǎn)單的HTML+JS片段如下(省略樣式):
html
復(fù)制
下載
運(yùn)行
<form?id="bookingForm">
??<input?type="text"?name="user_name"?placeholder="您的姓名"?required>
??<input?type="tel"?name="user_phone"?placeholder="手機(jī)號(hào)"?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">提交預(yù)約</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?+?'?預(yù)約編號(hào):'?+?result.data.appointment_id);
??}?else?{
????alert('錯(cuò)誤:'?+?(result.detail???result.detail.join('\n')?:?result.msg));
??}
});</script>
同時(shí),您可能還需要提供可預(yù)約時(shí)段查詢接口(例如:根據(jù)日期返回剩余名額),避免用戶反復(fù)提交后才發(fā)現(xiàn)已滿。該接口只需復(fù)用上述統(tǒng)計(jì)邏輯,以GET方式返回JSON,此處不再贅述。
多資源并行預(yù)約
在預(yù)約表中增加?resource_id?字段,并在統(tǒng)計(jì)容量時(shí)增加?AND resource_id = ??條件。前端需先選擇資源,再選時(shí)段。
周期性規(guī)則(如每周二、四開放)
可在時(shí)段配置表中增加?weekday_mask(如二進(jìn)制位表示周幾),在提交前校驗(yàn)當(dāng)前日期是否滿足規(guī)則。
支付或押金關(guān)聯(lián)
在插入記錄后,可調(diào)用第三方支付接口生成預(yù)支付訂單,并將訂單號(hào)存入?payment_txn?字段,狀態(tài)改為“待支付”,支付成功后再變更為“有效”。
管理后臺(tái)審核
增加?status?枚舉值(待審核、已確認(rèn)、已取消),并在提交后發(fā)送郵件/短信通知管理員。此段代碼目前僅支持“有效/取消”兩種狀態(tài),可按需擴(kuò)充。
郵件或短信提醒
在事務(wù)提交成功后,異步調(diào)用消息隊(duì)列或直接發(fā)送(注意性能),提醒用戶預(yù)約詳情。
時(shí)區(qū)與節(jié)假日處理
若面向多時(shí)區(qū)用戶,統(tǒng)一存儲(chǔ)UTC時(shí)間,并在展示時(shí)轉(zhuǎn)換。節(jié)假日可維護(hù)一張閉館日期表,在驗(yàn)證時(shí)排除。
防止并發(fā)超賣
雖然使用了事務(wù)和唯一索引,但在極高并發(fā)下(如秒殺類預(yù)約),建議增加?SELECT ... FOR UPDATE?行鎖,或使用Redis原子遞減庫(kù)存。本代碼適用于中小流量(每分鐘幾十次提交)。
日志與監(jiān)控
在?catch?塊中,除了?error_log,可接入更完善的日志庫(kù),記錄請(qǐng)求參數(shù)和堆棧,便于排查。
SQL注入防護(hù):本代碼已使用PDO預(yù)處理,但仍需確保所有動(dòng)態(tài)字段均通過參數(shù)綁定,尤其注意?order by?或?表名?等無法綁定的位置。
XSS過濾:輸出到前端的用戶數(shù)據(jù)(如姓名、備注)需進(jìn)行?htmlspecialchars?轉(zhuǎn)義,本接口返回JSON,前端渲染時(shí)需自行防范。
CSRF防護(hù):建議在表單中植入一次性令牌(Token),并在服務(wù)端校驗(yàn)。
敏感數(shù)據(jù)脫敏:日志中不應(yīng)記錄完整手機(jī)號(hào)或郵箱,可部分掩碼。
限制請(qǐng)求頻率:除Session鎖外,建議在Nginx或防火墻層設(shè)置IP限流(例如每分鐘10次)。
HTTPS強(qiáng)制:全程使用加密傳輸,避免中間人截獲預(yù)約信息。
將代碼放置于網(wǎng)站目錄,確保?db_connect.php?正確配置數(shù)據(jù)庫(kù)賬號(hào)密碼。
使用測(cè)試工具(如Postman)模擬POST請(qǐng)求,驗(yàn)證正常提交、重復(fù)提交、滿額、日期越界等場(chǎng)景。
觀察數(shù)據(jù)庫(kù)記錄,檢查字符集是否亂碼,時(shí)區(qū)是否一致。
開啟PHP錯(cuò)誤日志,但關(guān)閉?display_errors,避免暴露路徑信息。
若使用緩存或CDN,注意動(dòng)態(tài)接口不應(yīng)被緩存(添加?Cache-Control: no-cache?響應(yīng)頭)。
建議將本代碼納入版本管理,并在每次修改時(shí)記錄變更日志。例如,當(dāng)您調(diào)整時(shí)段容量或新增字段時(shí),同步更新數(shù)據(jù)表結(jié)構(gòu)腳本。對(duì)于預(yù)約狀態(tài)的更新(如管理員取消),可另寫?booking_update.php,僅允許授權(quán)身份訪問。
上述代碼提供了一套可運(yùn)行、可修改的預(yù)約功能雛形,覆蓋了從數(shù)據(jù)驗(yàn)證到事務(wù)寫入的完整鏈路。您完全可以根據(jù)自己的業(yè)務(wù)規(guī)則,替換驗(yàn)證正則、調(diào)整容量邏輯、增加額外字段,或?qū)⑵渲貥?gòu)為面向?qū)ο蟮念惤Y(jié)構(gòu)。關(guān)鍵在于理解每一段代碼的職責(zé),并依照實(shí)際需求進(jìn)行裁剪。切記在修改后,對(duì)邊界條件進(jìn)行充分測(cè)試,確保線上運(yùn)行的穩(wěn)定性。希望這段代碼能成為您構(gòu)建在線預(yù)約系統(tǒng)的堅(jiān)實(shí)基礎(chǔ),讓您將更多精力投入到個(gè)性化體驗(yàn)與運(yùn)營(yíng)優(yōu)化之中。