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