旅游小程序源碼拆包:PHP后端與小程序前端完整業(yè)務(wù)閉環(huán))
簡(jiǎn)介這份PHP經(jīng)典源碼面向旅游行業(yè)開(kāi)發(fā)者與PHP學(xué)習(xí)者提供景區(qū)旅游小程序V3.4.5完整實(shí)現(xiàn)可用于搭建在線預(yù)訂、景點(diǎn)導(dǎo)航與信息查詢平臺(tái)適合中小型旅游業(yè)務(wù)二次開(kāi)發(fā)或教學(xué)實(shí)戰(zhàn)。壓縮包共1952個(gè)文件約27.88MB以1186個(gè)php文件為核心業(yè)務(wù)邏輯輔以phpt測(cè)試、png與svg圖形資源、json與xml配置、html與js前端頁(yè)面、wxss與wxml小程序視圖以及yml、md、license等工程化文件目錄結(jié)構(gòu)完整便于按模塊檢索。已有668人學(xué)習(xí)下載說(shuō)明其在PHP小程序開(kāi)發(fā)領(lǐng)域具備一定參考價(jià)值。源碼覆蓋數(shù)據(jù)庫(kù)設(shè)計(jì)、RESTful API、微信登錄支付、模板引擎、安全防護(hù)、支付集成、錯(cuò)誤日志與性能優(yōu)化等關(guān)鍵環(huán)節(jié)讀者可借此理解前后端交互流程、學(xué)習(xí)框架與庫(kù)的運(yùn)用并掌握部署運(yùn)維與用戶體驗(yàn)設(shè)計(jì)思路是PHP旅游數(shù)字化項(xiàng)目的實(shí)戰(zhàn)案例。1. 景區(qū)旅游小程序源碼拆包一份 PHP 后端 小程序前端的完整業(yè)務(wù)閉環(huán)手里拿到一個(gè)景區(qū)旅游小程序 V3.4.5.rar第一反應(yīng)不是急著解壓看目錄而是先判斷它到底屬于哪一類(lèi)東西。市面上叫“景區(qū)旅游小程序”的源碼大致分三種純前端模板、前后端半成品、以及帶完整訂單和票務(wù)邏輯的業(yè)務(wù)系統(tǒng)。這份屬于第三種PHP 做后端接口小程序端負(fù)責(zé)展示和下單核心鏈路覆蓋景區(qū)列表、門(mén)票詳情、在線預(yù)訂、訂單核銷(xiāo)、用戶中心這幾塊。如果你手頭正好有景區(qū)、樂(lè)園、展館這類(lèi)需要線上售票和入園核銷(xiāo)的場(chǎng)景或者你是個(gè) PHP 開(kāi)發(fā)者想找一個(gè)帶真實(shí)業(yè)務(wù)流的練手項(xiàng)目這份源碼的參考價(jià)值比那些只有 UI 的模板高出一截。它不依賴任何小眾框架部署門(mén)檻低但業(yè)務(wù)表結(jié)構(gòu)和狀態(tài)機(jī)設(shè)計(jì)得比較完整值得逐層拆開(kāi)看。2. 環(huán)境搭建與目錄結(jié)構(gòu)從解壓到跑通第一個(gè)接口2.1 運(yùn)行環(huán)境的最低要求和常見(jiàn)組合這份源碼的后端是標(biāo)準(zhǔn) PHP 技術(shù)棧沒(méi)有用 Laravel、ThinkPHP 這類(lèi)重型框架而是原生 PHP 加輕量級(jí)路由數(shù)據(jù)庫(kù)走 MySQL。我一般會(huì)先確認(rèn)幾個(gè)硬性條件PHP 版本建議 7.2 到 7.4 之間8.0 以上部分函數(shù)簽名有變動(dòng)容易在訂單回調(diào)那塊翻車(chē)MySQL 用 5.7 或 8.0 都行但 8.0 要注意sql_mode里的ONLY_FULL_GROUP_BY可能讓某些統(tǒng)計(jì)查詢報(bào)錯(cuò)Web 服務(wù)器用 Nginx 或 Apache 都可以關(guān)鍵是偽靜態(tài)規(guī)則要配對(duì)否則接口路徑會(huì) 404。小程序端需要微信開(kāi)發(fā)者工具打開(kāi)appid在project.config.json里替換成你自己的測(cè)試號(hào)。后端接口域名要在小程序后臺(tái)配置合法域名本地調(diào)試時(shí)勾選“不校驗(yàn)合法域名”就能先跑通。提示如果你打算直接部署到線上PHP 版本和 MySQL 版本盡量和源碼說(shuō)明保持一致版本差異帶來(lái)的問(wèn)題往往比代碼本身的 bug 更難排查。2.2 目錄結(jié)構(gòu)拆解與關(guān)鍵文件定位解壓后典型的目錄長(zhǎng)這樣scenic-spot/ ├── api/ # 后端接口入口 │ ├── config/ │ │ └── database.php # 數(shù)據(jù)庫(kù)連接配置 │ ├── controller/ │ │ ├── Scenic.php # 景區(qū)相關(guān)接口 │ │ ├── Order.php # 訂單相關(guān)接口 │ │ └── User.php # 用戶相關(guān)接口 │ ├── model/ │ │ └── BaseModel.php # 基礎(chǔ)模型封裝了常用查詢 │ └── index.php # 統(tǒng)一入口文件 ├── miniprogram/ # 小程序前端 │ ├── pages/ │ │ ├── index/ # 首頁(yè) │ │ ├── detail/ # 景區(qū)詳情 │ │ ├── order/ # 下單頁(yè) │ │ └── user/ # 個(gè)人中心 │ └── utils/ │ └── request.js # 網(wǎng)絡(luò)請(qǐng)求封裝 └── install/ └── scenic.sql # 數(shù)據(jù)庫(kù)初始化腳本api/index.php是后端唯一入口所有請(qǐng)求都從這里分發(fā)。config/database.php里改數(shù)據(jù)庫(kù)賬號(hào)密碼。install/scenic.sql導(dǎo)入后會(huì)自動(dòng)建表并插入幾條測(cè)試景區(qū)數(shù)據(jù)。小程序端的utils/request.js里有一個(gè)baseUrl變量改成你后端部署的域名或本地 IP 就行。2.3 數(shù)據(jù)庫(kù)導(dǎo)入與接口連通性驗(yàn)證先把數(shù)據(jù)庫(kù)建好字符集用utf8mb4然后導(dǎo)入 SQL 文件# 創(chuàng)建數(shù)據(jù)庫(kù)字符集必須用 utf8mb4否則景區(qū)名稱(chēng)里的生僻字會(huì)亂碼 mysql -u root -p -e CREATE DATABASE scenic_tour DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; # 導(dǎo)入表結(jié)構(gòu)和初始數(shù)據(jù) mysql -u root -p scenic_tour install/scenic.sql導(dǎo)入完成后改api/config/database.php// 數(shù)據(jù)庫(kù)連接配置host 一般是 localhost如果你用 Docker 就改成容器名 return [ host 127.0.0.1, port 3306, dbname scenic_tour, username root, password your_password, // 改成你自己的密碼 charset utf8mb4, ];然后驗(yàn)證接口是否通。用瀏覽器或 curl 訪問(wèn)http://你的域名/api/index.php?cScenicalist正常應(yīng)該返回 JSON 格式的景區(qū)列表。如果返回空白或 500先看 PHP 錯(cuò)誤日志常見(jiàn)原因是database.php里密碼沒(méi)改、或者 PHP 缺少pdo_mysql擴(kuò)展。# 檢查 PHP 是否裝了 pdo_mysql 擴(kuò)展 php -m | grep pdo_mysql # 如果沒(méi)有輸出安裝一下以 Ubuntu 為例 sudo apt-get install php7.4-mysql接口通了之后打開(kāi)微信開(kāi)發(fā)者工具導(dǎo)入miniprogram目錄改request.js里的baseUrl首頁(yè)應(yīng)該能加載出景區(qū)列表。到這一步整個(gè)前后端鏈路就算跑通了。3. 核心業(yè)務(wù)邏輯拆解訂單狀態(tài)機(jī)與票務(wù)核銷(xiāo)怎么實(shí)現(xiàn)的3.1 訂單表結(jié)構(gòu)與狀態(tài)流轉(zhuǎn)設(shè)計(jì)這份源碼的訂單模塊是整份代碼里最值得看的部分。scenic_order表的核心字段包括order_id自增主鍵、order_no對(duì)外訂單號(hào)、user_id、scenic_id、ticket_type票種、quantity、total_amount、status訂單狀態(tài)、verify_code核銷(xiāo)碼、create_time、pay_time、verify_time。狀態(tài)流轉(zhuǎn)是這樣的0待支付 →1已支付待核銷(xiāo) →2已核銷(xiāo) →3已取消 →4已退款。這個(gè)狀態(tài)機(jī)不復(fù)雜但有幾個(gè)細(xì)節(jié)容易忽略支付回調(diào)里要把status從0改成1并寫(xiě)入pay_time核銷(xiāo)時(shí)要把status從1改成2并寫(xiě)入verify_time取消訂單只能從0或1狀態(tài)操作已核銷(xiāo)的訂單不允許取消。// api/controller/Order.php 中支付回調(diào)的核心邏輯 public function payNotify() { $orderNo $_POST[out_trade_no]; // 商戶訂單號(hào) $order $this-model-getOrderByNo($orderNo); // 只有待支付狀態(tài)才處理防止重復(fù)回調(diào) if ($order[status] ! 0) { return json_encode([code SUCCESS, msg 已處理]); } // 更新訂單狀態(tài)為已支付 $this-model-updateOrder($orderNo, [ status 1, pay_time date(Y-m-d H:i:s), ]); // 生成核銷(xiāo)碼一般是訂單號(hào)后 8 位加隨機(jī)數(shù) $verifyCode substr($orderNo, -8) . mt_rand(1000, 9999); $this-model-updateOrder($orderNo, [verify_code $verifyCode]); return json_encode([code SUCCESS, msg OK]); }這段代碼里有兩個(gè)關(guān)鍵點(diǎn)一是冪等判斷防止微信重復(fù)回調(diào)導(dǎo)致訂單狀態(tài)被多次修改二是核銷(xiāo)碼的生成時(shí)機(jī)放在支付成功之后而不是下單時(shí)避免未支付訂單占用核銷(xiāo)碼資源。3.2 核銷(xiāo)碼生成與驗(yàn)證的完整鏈路核銷(xiāo)碼是景區(qū)票務(wù)系統(tǒng)里最容易出問(wèn)題的環(huán)節(jié)。這份源碼的核銷(xiāo)流程分兩步用戶在小程序里展示核銷(xiāo)碼工作人員用管理端掃碼或輸入核銷(xiāo)碼完成驗(yàn)證。核銷(xiāo)碼的驗(yàn)證邏輯在api/controller/Order.php的verify方法里// 核銷(xiāo)接口管理端調(diào)用 public function verify() { $code $_POST[verify_code]; $order $this-model-getOrderByVerifyCode($code); if (!$order) { return json_encode([code 404, msg 核銷(xiāo)碼不存在]); } // 只有已支付待核銷(xiāo)狀態(tài)才能核銷(xiāo) if ($order[status] ! 1) { return json_encode([code 400, msg 訂單狀態(tài)不允許核銷(xiāo)]); } // 檢查是否在有效期內(nèi)一般票有使用日期限制 if (strtotime($order[use_date]) strtotime(date(Y-m-d))) { return json_encode([code 400, msg 票已過(guò)期]); } $this-model-updateOrder($order[order_no], [ status 2, verify_time date(Y-m-d H:i:s), ]); return json_encode([code 0, msg 核銷(xiāo)成功]); }這里有個(gè)血淚經(jīng)驗(yàn)核銷(xiāo)接口一定要加并發(fā)控制。如果兩個(gè)工作人員同時(shí)掃同一個(gè)碼不加鎖的話兩邊都會(huì)讀到status1然后都執(zhí)行更新雖然最終狀態(tài)是對(duì)的但核銷(xiāo)記錄會(huì)重復(fù)。常見(jiàn)做法是在updateOrder的 SQL 里加AND status 1條件用數(shù)據(jù)庫(kù)的行鎖來(lái)保證原子性。// 帶狀態(tài)條件的更新利用 MySQL 行鎖保證原子性 public function updateOrderWithStatus($orderNo, $data, $expectedStatus) { $sql UPDATE scenic_order SET status ?, verify_time ? WHERE order_no ? AND status ?; $stmt $this-pdo-prepare($sql); $stmt-execute([$data[status], $data[verify_time], $orderNo, $expectedStatus]); return $stmt-rowCount(); // 返回受影響行數(shù)0 表示狀態(tài)已被其他請(qǐng)求修改 }調(diào)用時(shí)判斷rowCount()如果返回 0 就說(shuō)明訂單狀態(tài)已經(jīng)被改過(guò)了直接返回“請(qǐng)勿重復(fù)核銷(xiāo)”。3.3 小程序端請(qǐng)求封裝與登錄態(tài)維護(hù)小程序端的utils/request.js封裝了統(tǒng)一的請(qǐng)求方法核心是帶上token并處理登錄過(guò)期。源碼里的做法是用戶首次進(jìn)入時(shí)調(diào)用wx.login拿到code傳給后端換openid和token之后所有請(qǐng)求都在 header 里帶token。// utils/request.js 核心邏輯 const baseUrl https://your-domain.com/api/index.php; function request(url, method GET, data {}) { const token wx.getStorageSync(token); return new Promise((resolve, reject) { wx.request({ url: baseUrl url, method: method, data: data, header: { content-type: application/json, Authorization: token ? Bearer token : }, success: (res) { // 401 表示登錄過(guò)期清除本地 token 并重新登錄 if (res.data.code 401) { wx.removeStorageSync(token); // 重新走登錄流程 login().then(() { // 登錄成功后重試當(dāng)前請(qǐng)求 request(url, method, data).then(resolve).catch(reject); }); return; } resolve(res.data); }, fail: reject }); }); }這個(gè)封裝里有一個(gè)容易踩的坑重試邏輯沒(méi)有限制次數(shù)如果登錄接口本身也返回 401就會(huì)無(wú)限遞歸。穩(wěn)妥的做法是加一個(gè)retryCount參數(shù)超過(guò)一次就直接跳轉(zhuǎn)到登錄頁(yè)讓用戶手動(dòng)操作。4. 避坑與常見(jiàn)問(wèn)題排查部署和調(diào)試中容易翻車(chē)的五個(gè)點(diǎn)4.1 接口返回 404 或空白頁(yè)現(xiàn)象訪問(wèn)api/index.php?cScenicalist返回 404 或者一片空白PHP 錯(cuò)誤日志里也沒(méi)有明顯報(bào)錯(cuò)。原因Nginx 的try_files配置不對(duì)或者index.php里的路由分發(fā)邏輯沒(méi)有正確處理c和a參數(shù)。有些環(huán)境還會(huì)因?yàn)镻ATH_INFO沒(méi)開(kāi)導(dǎo)致路由解析失敗。解決先確認(rèn) Nginx 配置里有try_files $uri $uri/ /index.php?$query_string;然后檢查index.php里是否用了$_GET[c]和$_GET[a]來(lái)分發(fā)。如果用的是PATH_INFO模式需要在 Nginx 里加fastcgi_split_path_info相關(guān)配置。最直接的辦法是打開(kāi) PHP 的display_errors看具體報(bào)錯(cuò)。4.2 數(shù)據(jù)庫(kù)連接失敗但賬號(hào)密碼確認(rèn)無(wú)誤現(xiàn)象database.php里賬號(hào)密碼都填對(duì)了但接口就是報(bào)“數(shù)據(jù)庫(kù)連接失敗”。原因常見(jiàn)的有三種——MySQL 沒(méi)啟動(dòng)、pdo_mysql擴(kuò)展沒(méi)裝、或者h(yuǎn)ost用了localhost但 MySQL 只監(jiān)聽(tīng)127.0.0.1的 TCP 連接。解決先用mysql -u root -p在命令行確認(rèn)能連上然后php -m | grep pdo_mysql確認(rèn)擴(kuò)展存在。如果都正常把host從localhost改成127.0.0.1試試因?yàn)閘ocalhost會(huì)走 Unix socket而127.0.0.1走 TCP兩者在部分環(huán)境下行為不一致。4.3 小程序端請(qǐng)求被攔截或跨域報(bào)錯(cuò)現(xiàn)象開(kāi)發(fā)者工具里請(qǐng)求接口報(bào)“不在以下 request 合法域名列表中”或者真機(jī)上請(qǐng)求直接失敗。原因微信小程序?qū)φ?qǐng)求域名有白名單限制本地調(diào)試時(shí)需要在開(kāi)發(fā)者工具里勾選“不校驗(yàn)合法域名”真機(jī)調(diào)試則必須在微信后臺(tái)配置合法域名。解決開(kāi)發(fā)階段在開(kāi)發(fā)者工具“詳情 → 本地設(shè)置”里勾選“不校驗(yàn)合法域名、web-view業(yè)務(wù)域名、TLS 版本以及 HTTPS 證書(shū)”。上線前把后端域名加到微信后臺(tái)的request合法域名里注意必須是 HTTPS 且不能帶端口號(hào)。4.4 訂單支付回調(diào)不觸發(fā)或重復(fù)觸發(fā)現(xiàn)象用戶支付成功后訂單狀態(tài)還是“待支付”或者一筆訂單被多次更新?tīng)顟B(tài)。原因支付回調(diào)地址配置錯(cuò)誤或者回調(diào)處理邏輯沒(méi)有做冪等判斷。解決先檢查支付配置里的notify_url是否指向了正確的接口地址并且這個(gè)地址必須外網(wǎng)可訪問(wèn)。然后在回調(diào)邏輯里加狀態(tài)判斷只有status0的訂單才處理處理前先查一次訂單狀態(tài)處理時(shí)用UPDATE ... WHERE status 0的方式保證原子性。4.5 核銷(xiāo)碼在管理端掃不出來(lái)現(xiàn)象用戶展示的核銷(xiāo)碼管理端掃碼后提示“核銷(xiāo)碼不存在”或“訂單狀態(tài)不允許核銷(xiāo)”。原因核銷(xiāo)碼生成時(shí)用了訂單號(hào)后 8 位加隨機(jī)數(shù)但訂單號(hào)本身可能包含字母掃碼槍識(shí)別時(shí)大小寫(xiě)敏感導(dǎo)致匹配失敗。另一個(gè)原因是訂單狀態(tài)不是1比如用戶還沒(méi)支付或者已經(jīng)核銷(xiāo)過(guò)了。解決核銷(xiāo)碼統(tǒng)一用純數(shù)字生成避免字母大小寫(xiě)問(wèn)題。管理端掃碼后先調(diào)接口查訂單狀態(tài)根據(jù)返回的具體錯(cuò)誤碼給工作人員提示而不是籠統(tǒng)地報(bào)“核銷(xiāo)失敗”。5. 二次開(kāi)發(fā)與接口擴(kuò)展從跑通到改造成自己的業(yè)務(wù)5.1 新增票種和價(jià)格策略的改法源碼里票種是寫(xiě)死在scenic_ticket表里的字段有ticket_id、scenic_id、ticket_name、price、stock、use_date。如果你想加“平日票”和“周末票”兩種價(jià)格最直接的做法是在scenic_ticket表里加一個(gè)ticket_type字段然后在Order.php的下單邏輯里根據(jù)當(dāng)前日期判斷用哪個(gè)價(jià)格。// 根據(jù)日期動(dòng)態(tài)計(jì)算票價(jià) public function getTicketPrice($ticketId, $useDate) { $ticket $this-model-getTicketById($ticketId); $dayOfWeek date(N, strtotime($useDate)); // 1-5 工作日6-7 周末 if ($dayOfWeek 6 $ticket[weekend_price] 0) { return $ticket[weekend_price]; } return $ticket[price]; }改完之后記得在小程序端的下單頁(yè)把價(jià)格展示同步改掉否則用戶看到的價(jià)格和實(shí)際支付金額不一致投訴就來(lái)了。5.2 訂單導(dǎo)出與對(duì)賬接口的補(bǔ)充源碼本身沒(méi)有對(duì)賬功能但實(shí)際運(yùn)營(yíng)中景區(qū)財(cái)務(wù)每天都要核對(duì)訂單。我一般會(huì)加一個(gè)export接口按日期范圍導(dǎo)出訂單 CSV。核心是用fputcsv逐行寫(xiě)入注意中文要用GBK編碼否則 Excel 打開(kāi)會(huì)亂碼。// 訂單導(dǎo)出接口按日期范圍導(dǎo)出 CSV public function export() { $startDate $_GET[start_date]; $endDate $_GET[end_date]; $orders $this-model-getOrdersByDateRange($startDate, $endDate); header(Content-Type: text/csv; charsetGBK); header(Content-Disposition: attachment; filenameorders_ . $startDate . _ . $endDate . .csv); $output fopen(php://output, w); // 轉(zhuǎn)成 GBK 編碼否則 Excel 打開(kāi)中文亂碼 fputcsv($output, array_map(function($v) { return mb_convert_encoding($v, GBK, UTF-8); }, [訂單號(hào), 景區(qū)名稱(chēng), 票種, 數(shù)量, 金額, 狀態(tài), 下單時(shí)間])); foreach ($orders as $order) { fputcsv($output, array_map(function($v) { return mb_convert_encoding($v, GBK, UTF-8); }, [ $order[order_no], $order[scenic_name], $order[ticket_name], $order[quantity], $order[total_amount], $order[status_text], $order[create_time], ])); } fclose($output); exit; }這個(gè)接口加完之后財(cái)務(wù)那邊每天早上的對(duì)賬時(shí)間從半小時(shí)縮到了兩分鐘。從那以后我每次接類(lèi)似項(xiàng)目都會(huì)在第一天就把導(dǎo)出接口預(yù)留好不管當(dāng)時(shí)用不用得上。5.3 接口安全加固的幾個(gè)實(shí)用手段源碼默認(rèn)沒(méi)有做接口簽名所有請(qǐng)求只要知道 URL 就能調(diào)。上線前至少要做三件事一是給所有寫(xiě)操作加token校驗(yàn)二是對(duì)order_no這類(lèi)敏感參數(shù)做權(quán)限判斷只能查自己的訂單三是給接口加頻率限制。// 簡(jiǎn)單的頻率限制基于 Redis 或文件緩存 public function checkRateLimit($userId, $action, $limit 10, $window 60) { $key rate_limit:{$action}:{$userId}; $count $this-cache-incr($key); if ($count 1) { $this-cache-expire($key, $window); } if ($count $limit) { return false; // 超過(guò)限制 } return true; }調(diào)用時(shí)在Order.php的create方法開(kāi)頭加一句if (!$this-checkRateLimit($userId, create_order, 5, 60)) { return json_encode([code 429, msg 操作太頻繁]); }就能防止惡意刷單。這個(gè)習(xí)慣我每次做交易類(lèi)項(xiàng)目都會(huì)帶上后面省了很多事。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取