ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

景区旅游小程序源码拆包:PHP后端与小程序前端完整业务闭环

景区旅游小程序源码拆包:PHP后端与小程序前端完整业务闭环

简介:这份PHP经典源码面向旅游行业开发者与PHP学习者,提供景区旅游小程序V3.4.5完整实现,可用于搭建在线预订、景点导航与信息查询平台,适合中小型旅游业务二次开发或教学实战。压缩包共1952个文件,约27.88MB,以1186个php文件为核心业务逻辑,辅以phpt测试、png与svg图形资源、json与xml配置、html与js前端页面、wxss与wxml小程序视图,以及yml、md、license等工程化文件,目录结构完整,便于按模块检索。已有668人学习下载,说明其在PHP小程序开发领域具备一定参考价值。源码覆盖数据库设计、RESTful API、微信登录支付、模板引擎、安全防护、支付集成、错误日志与性能优化等关键环节,读者可借此理解前后端交互流程、学习框架与库的运用,并掌握部署运维与用户体验设计思路,是PHP旅游数字化项目的实战案例。

1. 景区旅游小程序源码拆包:一份 PHP 后端 + 小程序前端的完整业务闭环

手里拿到一个景区旅游小程序 V3.4.5.rar,第一反应不是急着解压看目录,而是先判断它到底属于哪一类东西。市面上叫“景区旅游小程序”的源码大致分三种:纯前端模板、前后端半成品、以及带完整订单和票务逻辑的业务系统。这份属于第三种,PHP 做后端接口,小程序端负责展示和下单,核心链路覆盖景区列表、门票详情、在线预订、订单核销、用户中心这几块。如果你手头正好有景区、乐园、展馆这类需要线上售票和入园核销的场景,或者你是个 PHP 开发者想找一个带真实业务流的练手项目,这份源码的参考价值比那些只有 UI 的模板高出一截。它不依赖任何小众框架,部署门槛低,但业务表结构和状态机设计得比较完整,值得逐层拆开看。

2. 环境搭建与目录结构:从解压到跑通第一个接口

2.1 运行环境的最低要求和常见组合

这份源码的后端是标准 PHP 技术栈,没有用 Laravel、ThinkPHP 这类重型框架,而是原生 PHP 加轻量级路由,数据库走 MySQL。我一般会先确认几个硬性条件:PHP 版本建议 7.2 到 7.4 之间,8.0 以上部分函数签名有变动,容易在订单回调那块翻车;MySQL 用 5.7 或 8.0 都行,但 8.0 要注意sql_mode里的ONLY_FULL_GROUP_BY可能让某些统计查询报错;Web 服务器用 Nginx 或 Apache 都可以,关键是伪静态规则要配对,否则接口路径会 404。

小程序端需要微信开发者工具打开,appid在project.config.json里替换成你自己的测试号。后端接口域名要在小程序后台配置合法域名,本地调试时勾选“不校验合法域名”就能先跑通。

提示:如果你打算直接部署到线上,PHP 版本和 MySQL 版本尽量和源码说明保持一致,版本差异带来的问题往往比代码本身的 bug 更难排查。

2.2 目录结构拆解与关键文件定位

解压后典型的目录长这样:

scenic-spot/ ├── api/ # 后端接口入口 │ ├── config/ │ │ └── database.php # 数据库连接配置 │ ├── controller/ │ │ ├── Scenic.php # 景区相关接口 │ │ ├── Order.php # 订单相关接口 │ │ └── User.php # 用户相关接口 │ ├── model/ │ │ └── BaseModel.php # 基础模型,封装了常用查询 │ └── index.php # 统一入口文件 ├── miniprogram/ # 小程序前端 │ ├── pages/ │ │ ├── index/ # 首页 │ │ ├── detail/ # 景区详情 │ │ ├── order/ # 下单页 │ │ └── user/ # 个人中心 │ └── utils/ │ └── request.js # 网络请求封装 └── install/ └── scenic.sql # 数据库初始化脚本

api/index.php是后端唯一入口,所有请求都从这里分发。config/database.php里改数据库账号密码。install/scenic.sql导入后会自动建表并插入几条测试景区数据。小程序端的utils/request.js里有一个baseUrl变量,改成你后端部署的域名或本地 IP 就行。

2.3 数据库导入与接口连通性验证

先把数据库建好,字符集用utf8mb4,然后导入 SQL 文件:

# 创建数据库,字符集必须用 utf8mb4,否则景区名称里的生僻字会乱码 mysql -u root -p -e "CREATE DATABASE scenic_tour DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" # 导入表结构和初始数据 mysql -u root -p scenic_tour < install/scenic.sql

导入完成后,改api/config/database.php:

// 数据库连接配置,host 一般是 localhost,如果你用 Docker 就改成容器名 return [ 'host' => '127.0.0.1', 'port' => 3306, 'dbname' => 'scenic_tour', 'username' => 'root', 'password' => 'your_password', // 改成你自己的密码 'charset' => 'utf8mb4', ];

然后验证接口是否通。用浏览器或 curl 访问http://你的域名/api/index.php?c=Scenic&a=list,正常应该返回 JSON 格式的景区列表。如果返回空白或 500,先看 PHP 错误日志,常见原因是database.php里密码没改、或者 PHP 缺少pdo_mysql扩展。

# 检查 PHP 是否装了 pdo_mysql 扩展 php -m | grep pdo_mysql # 如果没有输出,安装一下(以 Ubuntu 为例) sudo apt-get install php7.4-mysql

接口通了之后,打开微信开发者工具,导入miniprogram目录,改request.js里的baseUrl,首页应该能加载出景区列表。到这一步,整个前后端链路就算跑通了。

3. 核心业务逻辑拆解:订单状态机与票务核销怎么实现的

3.1 订单表结构与状态流转设计

这份源码的订单模块是整份代码里最值得看的部分。scenic_order表的核心字段包括:order_id(自增主键)、order_no(对外订单号)、user_id、scenic_id、ticket_type(票种)、quantity、total_amount、status(订单状态)、verify_code(核销码)、create_time、pay_time、verify_time。

状态流转是这样的:0待支付 →1已支付待核销 →2已核销 →3已取消 →4已退款。这个状态机不复杂,但有几个细节容易忽略:支付回调里要把status从0改成1并写入pay_time,核销时要把status从1改成2并写入verify_time,取消订单只能从0或1状态操作,已核销的订单不允许取消。

// api/controller/Order.php 中支付回调的核心逻辑 public function payNotify() { $orderNo = $_POST['out_trade_no']; // 商户订单号 $order = $this->model->getOrderByNo($orderNo); // 只有待支付状态才处理,防止重复回调 if ($order['status'] != 0) { return json_encode(['code' => 'SUCCESS', 'msg' => '已处理']); } // 更新订单状态为已支付 $this->model->updateOrder($orderNo, [ 'status' => 1, 'pay_time' => date('Y-m-d H:i:s'), ]); // 生成核销码,一般是订单号后 8 位加随机数 $verifyCode = substr($orderNo, -8) . mt_rand(1000, 9999); $this->model->updateOrder($orderNo, ['verify_code' => $verifyCode]); return json_encode(['code' => 'SUCCESS', 'msg' => 'OK']); }

这段代码里有两个关键点:一是幂等判断,防止微信重复回调导致订单状态被多次修改;二是核销码的生成时机,放在支付成功之后而不是下单时,避免未支付订单占用核销码资源。

3.2 核销码生成与验证的完整链路

核销码是景区票务系统里最容易出问题的环节。这份源码的核销流程分两步:用户在小程序里展示核销码,工作人员用管理端扫码或输入核销码完成验证。

核销码的验证逻辑在api/controller/Order.php的verify方法里:

// 核销接口,管理端调用 public function verify() { $code = $_POST['verify_code']; $order = $this->model->getOrderByVerifyCode($code); if (!$order) { return json_encode(['code' => 404, 'msg' => '核销码不存在']); } // 只有已支付待核销状态才能核销 if ($order['status'] != 1) { return json_encode(['code' => 400, 'msg' => '订单状态不允许核销']); } // 检查是否在有效期内,一般票有使用日期限制 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' => '核销成功']); }

这里有个血泪经验:核销接口一定要加并发控制。如果两个工作人员同时扫同一个码,不加锁的话两边都会读到status=1,然后都执行更新,虽然最终状态是对的,但核销记录会重复。常见做法是在updateOrder的 SQL 里加AND status = 1条件,用数据库的行锁来保证原子性。

// 带状态条件的更新,利用 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(); // 返回受影响行数,0 表示状态已被其他请求修改 }

调用时判断rowCount(),如果返回 0 就说明订单状态已经被改过了,直接返回“请勿重复核销”。

3.3 小程序端请求封装与登录态维护

小程序端的utils/request.js封装了统一的请求方法,核心是带上token并处理登录过期。源码里的做法是:用户首次进入时调用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(() => { // 登录成功后重试当前请求 request(url, method, data).then(resolve).catch(reject); }); return; } resolve(res.data); }, fail: reject }); }); }

这个封装里有一个容易踩的坑:重试逻辑没有限制次数,如果登录接口本身也返回 401,就会无限递归。稳妥的做法是加一个retryCount参数,超过一次就直接跳转到登录页让用户手动操作。

4. 避坑与常见问题排查:部署和调试中容易翻车的五个点

4.1 接口返回 404 或空白页

现象:访问api/index.php?c=Scenic&a=list返回 404 或者一片空白,PHP 错误日志里也没有明显报错。

原因:Nginx 的try_files配置不对,或者index.php里的路由分发逻辑没有正确处理c和a参数。有些环境还会因为PATH_INFO没开导致路由解析失败。

解决:先确认 Nginx 配置里有try_files $uri $uri/ /index.php?$query_string;,然后检查index.php里是否用了$_GET['c']和$_GET['a']来分发。如果用的是PATH_INFO模式,需要在 Nginx 里加fastcgi_split_path_info相关配置。最直接的办法是打开 PHP 的display_errors看具体报错。

4.2 数据库连接失败但账号密码确认无误

现象:database.php里账号密码都填对了,但接口就是报“数据库连接失败”。

原因:常见的有三种——MySQL 没启动、pdo_mysql扩展没装、或者host用了localhost但 MySQL 只监听127.0.0.1的 TCP 连接。

解决:先用mysql -u root -p在命令行确认能连上,然后php -m | grep pdo_mysql确认扩展存在。如果都正常,把host从localhost改成127.0.0.1试试,因为localhost会走 Unix socket,而127.0.0.1走 TCP,两者在部分环境下行为不一致。

4.3 小程序端请求被拦截或跨域报错

现象:开发者工具里请求接口报“不在以下 request 合法域名列表中”,或者真机上请求直接失败。

原因:微信小程序对请求域名有白名单限制,本地调试时需要在开发者工具里勾选“不校验合法域名”,真机调试则必须在微信后台配置合法域名。

解决:开发阶段在开发者工具“详情 → 本地设置”里勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”。上线前把后端域名加到微信后台的request合法域名里,注意必须是 HTTPS 且不能带端口号。

4.4 订单支付回调不触发或重复触发

现象:用户支付成功后订单状态还是“待支付”,或者一笔订单被多次更新状态。

原因:支付回调地址配置错误,或者回调处理逻辑没有做幂等判断。

解决:先检查支付配置里的notify_url是否指向了正确的接口地址,并且这个地址必须外网可访问。然后在回调逻辑里加状态判断,只有status=0的订单才处理,处理前先查一次订单状态,处理时用UPDATE ... WHERE status = 0的方式保证原子性。

4.5 核销码在管理端扫不出来

现象:用户展示的核销码,管理端扫码后提示“核销码不存在”或“订单状态不允许核销”。

原因:核销码生成时用了订单号后 8 位加随机数,但订单号本身可能包含字母,扫码枪识别时大小写敏感导致匹配失败。另一个原因是订单状态不是1,比如用户还没支付或者已经核销过了。

解决:核销码统一用纯数字生成,避免字母大小写问题。管理端扫码后先调接口查订单状态,根据返回的具体错误码给工作人员提示,而不是笼统地报“核销失败”。

5. 二次开发与接口扩展:从跑通到改造成自己的业务

5.1 新增票种和价格策略的改法

源码里票种是写死在scenic_ticket表里的,字段有ticket_id、scenic_id、ticket_name、price、stock、use_date。如果你想加“平日票”和“周末票”两种价格,最直接的做法是在scenic_ticket表里加一个ticket_type字段,然后在Order.php的下单逻辑里根据当前日期判断用哪个价格。

// 根据日期动态计算票价 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']; }

改完之后记得在小程序端的下单页把价格展示同步改掉,否则用户看到的价格和实际支付金额不一致,投诉就来了。

5.2 订单导出与对账接口的补充

源码本身没有对账功能,但实际运营中景区财务每天都要核对订单。我一般会加一个export接口,按日期范围导出订单 CSV。核心是用fputcsv逐行写入,注意中文要用GBK编码,否则 Excel 打开会乱码。

// 订单导出接口,按日期范围导出 CSV public function export() { $startDate = $_GET['start_date']; $endDate = $_GET['end_date']; $orders = $this->model->getOrdersByDateRange($startDate, $endDate); header('Content-Type: text/csv; charset=GBK'); header('Content-Disposition: attachment; filename="orders_' . $startDate . '_' . $endDate . '.csv"'); $output = fopen('php://output', 'w'); // 转成 GBK 编码,否则 Excel 打开中文乱码 fputcsv($output, array_map(function($v) { return mb_convert_encoding($v, 'GBK', 'UTF-8'); }, ['订单号', '景区名称', '票种', '数量', '金额', '状态', '下单时间'])); 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; }

这个接口加完之后,财务那边每天早上的对账时间从半小时缩到了两分钟。从那以后我每次接类似项目,都会在第一天就把导出接口预留好,不管当时用不用得上。

5.3 接口安全加固的几个实用手段

源码默认没有做接口签名,所有请求只要知道 URL 就能调。上线前至少要做三件事:一是给所有写操作加token校验,二是对order_no这类敏感参数做权限判断(只能查自己的订单),三是给接口加频率限制。

// 简单的频率限制,基于 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; }

调用时在Order.php的create方法开头加一句if (!$this->checkRateLimit($userId, 'create_order', 5, 60)) { return json_encode(['code' => 429, 'msg' => '操作太频繁']); },就能防止恶意刷单。这个习惯我每次做交易类项目都会带上,后面省了很多事。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进