最近在梳理旅游行业的小程序项目时,正好把一个全套开源的门票预订系统完整跑了一遍。这套系统用的是 FastAdmin + ThinkPHP 后端,前端是 Uniapp,也就是说前后端从后台管理到用户端小程序全都有源码。如果你正打算做景区门票、乐园票务、或者本地生活类的预约核销业务,这套源码可以算是一个很完整的起步模板。
先说结论:这套系统不是那种只能看不能跑的玩具项目,而是把真实业务里会用到的商品管理、下单支付、订单核销、后台统计这些环节都串起来了。我实际部署之后,感觉最有价值的点反而不是代码本身,而是它把“门票类业务”和“普通电商业务”的差异点都做了出来,比如日期场次、限购规则、核销二维码、退款逻辑这些。本文就按我的实际部署和二次开发过程,把项目的架构、关键实现、部署步骤和踩坑经验完整写一遍。
1. 项目整体设计与技术选型思路
1.1 为什么是 FastAdmin 和 ThinkPHP 的组合
这套系统后端选择了 FastAdmin 作为后台开发框架,底层则是 ThinkPHP 5.x。FastAdmin 在 PHP 圈子里的口碑一直很稳定,它本质上是一套基于 ThinkPHP 的后台管理脚手架,内置了权限管理(RBAC)、通用 CRUD 生成器、插件机制、后台模板等一整套东西。我第一次用 FastAdmin 的时候最直观的感受是:它把“后台管理系统”里那些重复劳动全部压缩了,尤其是菜单管理和权限分配,几乎是图形化操作,不用手写代码。
ThinkPHP 作为底层框架,在国内中小型项目里的占有率一直很高。它的 ORM、验证器、中间件这些设计比较贴近国内开发者的习惯,文档和社区都很成熟。对于门票预订这种业务来说,核心压力不在高并发,而在业务状态机的准确性上。ThinkPHP 的事务机制和模型关联在订单、退款、核销这些场景下用起来很顺手,这套系统选择它是有道理的。
如果你之前做的是纯接口开发,习惯了 Laravel 的 MVC,那 ThinkPHP 上手几乎没成本。FastAdmin 在安装后会生成一个 admin 后台模块,同时保留了一个 api 模块给前端接口用。也就是说,后端天然被分成了“管理端”和“客户端”两条线,前端 Uniapp 对接的是 api 模块,管理员登录用的是 admin 模块。这种拆分在实际开发中非常关键,避免了两套逻辑互相污染。
1.2 Uniapp 的跨端优势与取舍
前端的 Uniapp 选择则更明确:一套代码同时输出微信小程序、支付宝小程序、H5、App。旅游门票的核心流量入口几乎都在微信生态,用户搜小程序、公众号里打开 H5、或者扫码购票,这几种场景是刚需。如果每端单独开发一套,维护成本直接翻倍。Uniapp 基于 Vue 语法,对做过后端的开发者来说也比较友好,模板、脚本、样式三段式结构很清晰。
实际跑下来,这套源码在微信小程序端的表现还不错,页面跳转、登录授权、支付调用这些基础能力都封装好了。Uniapp 最大的好处是条件编译,比如“H5 端需要区分公众号环境”、“小程序端需要调 wx.login”,一套代码里用条件注释就能拆分,不用单独维护分支。
当然,也不是没有取舍。Uniapp 在复杂自定义组件和原生性能要求高的场景下会有些限制,但门票预订这种表单为主、列表为辅的业务形态,完全在它的舒适区内。我的经验是:只要不是重度视频编辑、AR 互动这类场景,Uniapp 基本能覆盖 90% 的业务需求。
1.3 整个系统的模块划分
从功能模块来看,这套系统大致可以拆成这么几个部分:
- 后台管理端:管理员登录、景区管理、门票商品管理、订单管理、核销管理、用户管理、退款管理、系统配置。
- API 接口端:用户注册登录、首页数据、景区列表、门票详情、生成订单、支付下单、订单列表、订单详情、核销验证。
- 用户前端(Uniapp):首页、分类、景点列表、门票详情、下单页面、支付流程、个人中心、订单中心、优惠券。
这套源码有一点做得不错:后台管理的业务闭环是完整的。从上线一个新景区、添加一个门票商品、设置售卖日期和限购数量,到用户下单支付,再到现场核销员扫码通过,整个过程全部能走通。这意味着你不用再去网上到处找插件拼装,拿下来整理一下就能用。
2. 门票预订核心业务逻辑与数据库设计
2.1 门票商品和普通电商商品的差别
一开始我打开数据库表的时候,第一反应是这表和普通商城系统的商品表差别很大。门票商品的核心不是 SKU 颜色尺码,而是“日期 + 场次 + 票型”。举个例子:一个景区,可能平时门票 80 元,节假日 100 元,学生票半价,老人票免票,而且每天可售数量是有限制的。这种数据模型如果做成普通的商品规格,维护成本会很高。
这套系统里的门票商品表基本包含了这样几个关键字段:
- 景区ID和分类,决定商品归属和列表展示位置
- 票型名称和描述,比如成人票、儿童票、亲子票
- 原价和售价,这里比较推荐统一用“分”为单位,避免浮点误差
- 有效期类型:有的票是固定日期使用,有的是有效期内任意一天可用
- 限购规则:每个用户每天限购几张,每次下单限购几张
- 库存模式:有的是总库存固定,有的是按天库存
这些字段里,最容易忽略的是“按天库存”。很多景区不是总量控制,而是每天限流,比如每天最多进 5000 人。如果只做一个总库存字段,卖完第一天就全线下架了,这明显不对。这套系统把“日期库存表”独立出来,每天生成一条库存记录,下单时校验对应日期剩余数量,是合理的方案。
2.2 订单状态机设计与支付流程
订单状态的流转是这类系统的命脉。正常情况下的状态流大致是:待支付 -> 已支付 -> 待使用 -> 已核销,分支状态还有已取消、已退款、已过期。如果状态设置不合理,后面排查问题会非常痛苦。
我在实际翻代码的时候,发现系统对订单操作的前置校验写得很详细。比如取消订单,先判断状态是不是待支付;申请退款,先判断状态是不是已支付,并且核销次数必须是 0;核销的时候,先判断当前时间在不在有效期内。这些校验看起来“多此一举”,但真实场景里用户的操作路径千奇百怪,如果不在服务端做严格校验,很容易出现超卖、重复核销、退款后仍能用票等问题。
支付环节我单独提一下:这套系统在服务端生成支付参数,然后由 Uniapp 端拉起微信支付或支付宝支付,支付完成后微信服务器会回调后端通知接口,后端在回调里更新订单状态。这里有一个极其重要的细节:支付回调必须做幂等处理。因为网络原因,微信服务器可能多次发送同一个支付结果通知,如果你每次都去改订单状态和加余额,就会出大问题。标准做法是用“外部订单号”查一次订单状态,只有待支付状态才执行更新,否则直接返回成功。
2.3 库存、核销、退款三个容易翻车的地方
库存这块,最容易出事的是“扣减库存”和“生成订单”不是原子的。如果先扣库存再生成订单,用户支付失败后库存没了;如果先生成订单再扣库存,高并发下个订单会超卖。正确做法是用数据库事务,把这两步包在一起执行,同时对库存表加行级锁。
核销环节,我特别喜欢这套系统里二维码核销的实现。用户订单里有一个对应的核销码,后台核销员登录后,可以扫描用户的二维码,系统自动校验订单是否有效、是否过期、是否已被核销。校验通过后标记为已核销,同时记录核销时间和核销员信息,方便事后审计。对于景区入园这种线下场景,这个流程已经够用了。
退款这里也有讲究。门票类业务的退款通常比较复杂,因为可能有部分门票已经核销、剩余未核销的情况。我建议在二次开发时把退款的校验逻辑写得更细一点:只允许全额退款,还是允许部分退款;退款是原路退回还是余额退回;退款是否需要人工审核。这套系统的基础版本走的是“申请退款 -> 后台审核 -> 确认退款”,更符合国内景区实际的管理习惯。
3. 本地搭建与源码部署实操记录
3.1 后端环境准备与伪静态配置
我最开始是在宝塔面板环境下部署的,整体过程比较顺。PHP 版本建议用 7.4 或 8.0,MySQL 用 5.7/8.0,Nginx 和 Apache 都可以。部署时需要注意运行目录要指向 public,伪静态规则也要正确配置,否则访问除了首页以外全是 404。
如果你也是用宝塔,操作步骤大概是:
- 新建站点,把源码根目录放进去。
- 站点运行目录选择
/public,取消勾选“防跨站攻击”。 - 伪静态选择 thinkphp 规则。
- 配置伪静态后重启一下 Nginx,再访问站点域名,正常情况下会进入安装引导页。
- 填写数据库信息,安装完成后删除或改名 install 目录。
这个安装环节里有一个很容易忽略的点:FastAdmin 安装完成后,默认后台地址是/admin123之类的随机目录。很多人装完找不到后台,其实是安装时设置了安全入口,这在生产环境中是保护措施,本地调试时可以修改后台配置文件里的入口地址,改成简单的/admin方便开发。
3.2 FastAdmin 后台的初始化操作
后台进入之后,第一件事不是急着配商品,而是先做基础设置。站点名称、logo、备案号、微信支付参数、小程序 appid 和 secret,都是在这里填。腾讯地图或高德地图的 key 也要提前准备好,景区地图功能会用到。
接下来是管理员和权限。FastAdmin 内置的权限管理比你自己写要省心太多,直接在后台“权限管理”里给不同管理员分配菜单权限就行。你可以创建“运营”角色,只给商品和订单权限;创建“核销员”角色,只给核销权限,避免误操作。这种“角色-菜单-管理员”三级结构在上线后非常实用。
然后就是录入景区和门票商品数据了。这里我想特别强调:门票商品的排序和上下架设置会直接影响前端展示,建议提前把景区图片整理好,因为前端首页很大的篇幅是图片轮播和列表缩略图。没有好图,页面整体档次会掉一大截。
3.3 Uniapp 前端运行与打包配置
Uniapp 端的代码拿到之后,直接用 HBuilderX 打开即可运行到微信开发者工具。当然,这里需要先在manifest.json里配置你自己的微信小程序 appid。之后在“运行 -> 运行到小程序模拟器 -> 微信开发者工具”选择对应项目,就能看到页面了。
登录逻辑这一块,小程序端默认用的是uni.login获取 code,然后传给后端换取 openid 和 token。开发时本地接口建议开启“不校验合法域名”,否则请求会被拦截。上线前记得在微信公众平台配置 request 合法域名,必须是 HTTPS 的域名,还得把 API 的域名加进去。
如果你还要做 H5 版本,打包时选择“发行 -> 网站-H5手机版”,运行环境需要是浏览器。H5 端的登录方式一般会用公众号网页授权,这个和纯小程序的登录逻辑会有一点差异,源码里已经有对应处理,自己接的时候注意别用错接口。
3.4 接口联调:从登录到下单全链路验证
代码跑起来之后,我建议按下面的顺序把全链路测一遍,任何一个环节断了都能马上定位问题:
- 用户登录,确认能拿到 token,并能通过 token 获取用户信息。
- 首页接口,确认景区数据和轮播图能正常返回。
- 门票详情页,点击某个门票商品,确认详情和库存信息正确。
- 下单接口,选择日期和门票数量,先生成一个待支付订单,确认库存被扣减。
- 拉起支付,这一步本地测试比较麻烦,可以用微信支付的沙箱环境,如果不行可以先让后台手动把订单状态改成已支付,验证后续流程。
- 核销闭环,用另一个账号登录核销后台,扫描订单二维码,确认能正常核销,二次核销应该被拦截。
这套流程走完,系统基本就达到可上线状态了。我实际操作时,最容易出问题的地方往往是支付回调地址写错,或者本地调试时微信回调无法到达内网。这里有个小技巧:内网调试可以用花生壳类的内网穿透工具,把回调地址临时映射到外网域名,等联调完再换正式地址。
4. 常见问题与踩坑排查实录
4.1 后台管理相关的坑
FastAdmin 后台跑起来之后,有两个问题非常常见。
第一个是“后台可以登录,但显示无权限”。这个大多数是权限缓存问题,去“权限管理 -> 菜单规则”里点一下“生成菜单”和“刷新缓存”,问题基本就解决了。如果还不行,检查当前账号的角色是否绑定了对应菜单权限。
第二个是“上传图片失败”。这个需要检查二点:一是上传目录有没有写权限,设置 uploads 权限为 755;二是 PHP 的 upload_max_filesize 和 post_max_size 是不是够大,景区图片经常几 MB 的,默认 2M 肯定不够,建议调到 20M 以上。
还有一个小坑是“后台页面样式错乱”,大概率是 CDN 资源加载失败。FastAdmin 有些版本会从网络加载静态资源,内网环境就会卡住,改一下后台配置里的 CDN 地址,或者把静态资源本地化就行。
4.2 接口与支付相关的坑
接口这一层,遇到最多的是跨域问题。如果你前端用的是 H5,而 API 部署在另一个域名,浏览器会拦截请求。解决方法是后端对 API 路由统一加跨域头,或者前端的代理转发到同源域名。生产环境我更推荐用 Nginx 同一域名下做反向代理,把/api路径转发到后端服务,省心很多,也避免暴露真实接口地址。
支付环节的坑主要集中在回调上。如果你发现用户已经支付了,但订单状态一直没变,先去看支付回调日志,确认回调有没有成功到达后端。如果到达了,再检查验签逻辑。微信支付 v3 的验签方式和之前 v2 完全不一样,很多老项目升级到 v3 时验签代码没同步更新,导致回调全部失败。实在排查不出来,可以临时把验签代码改成先打印原始报文,等能正确解析后再开启验签,但上线前一定要改回来。
另外一个容易被忽略的问题:支付金额单位。微信支付金额单位是“分”,如果你数据库存的是“元”,下单时不转换,支付金额会被放大 100 倍,用户会吓跑的。这套系统的数据库字段基本都是按“元”存储的,但在接口对接时使用了格式化处理,二次开发时要格外留意。
4.3 Uniapp 端的坑
Uniapp 开发遇到的第一个坑是预览时图片不显示。这个很多时候不是代码问题,而是开发者工具的“本地资源”权限设置。排查思路:先看图片请求有没有报错,如果是 403,八成是防盗链配置问题。如果后台图片能正常打开,前端加载不出来,检查一下接口返回的图片地址是不是完整的 URL,如果是相对路径,Uniapp 里需要做拼接转换。
第二个常见坑是登录状态失效问题。用户打开小程序时,Uniapp 会先从本地拿 token,然后请求用户信息。如果 token 过期,后端返回 401,前端需要自动跳转登录。但这里有个陷阱:小程序里很多页面是 tabBar 页面,不能通过uni.navigateTo跳转,需要用uni.switchTab或直接使用uni.reLaunch清空页面栈。我在调试时经常遇到“跳转登录页失败”的问题,大多是这个原因。
第三个坑是安卓机上的样式兼容。Uniapp 直接支持 Vue 语法,但小程序的 CSS 支持范围有限,比如部分选择器在安卓低版本上有兼容问题。实际开发时尽量少用复杂选择器和固定定位,多用 flex 布局,减少样式差异带来的 UI 错乱。
5. 二次开发方向与扩展建议
5.1 营销裂变方向:拼团与分销
如果你想把这套系统真正跑出量,第一优先级是加营销玩法。门票类商品的决策成本低,非常适合拼团。一个简单的做法是加一个拼团表,里面存成团人数、开团时间、到期时间,用户支付后生成拼团记录,邀请好友参团,人数够了就成团,否则自动退款。这类逻辑在 ThinkPHP 里实现不难,关键是把订单表、拼团表、退款逻辑的关系理清楚。
还有一个效果非常好的功能是“分销裂变”。用户购买后可以生成专属分销海报,朋友通过海报购买,原用户可以获得佣金。这个功能在旅游行业几乎是标配了,很多景区的门票都是靠本地导游和自媒体带流量。分销系统实现时需要注意佣金结算时机,建议在订单核销后再结算佣金,避免退款和佣金冲突。
5.2 多商户与平台化方向
单个景区使用这套系统完全没问题,但如果要做区域平台,聚合多个景区的门票,那就需要增加“商户”概念了。比较简单的做法是给景区表加一个“商户ID”字段,每个管理员只管理自己商户的景区和订单。这听起来改动不大,但一旦涉及资金分账,复杂度会上升不少。平台收一笔订单的钱,需要把其中的一部分结算给景区,这就要引入独立的结算记录表和提现流程。
做平台化时还有一个重点:统一核销。游客在平台上买了一个景区 A 和一个景区 B 的门票,可能收到两个二维码,体验不好。更好的做法是做一个“一码多票”的方案,每个游客只有一个主码,核销时自动关联该游客当天所有可核销的门票,这样后台核销员的体验也会提升很多。
5.3 数据驱动方向:报表与用户画像
这套系统目前的统计功能还比较基础,基本只覆盖订单量、销售额、退款额。如果想用它指导运营,我建议单独开发几个报表模块:
- 景区维度分日销售报表,看哪个景区销量有异常波动,方便及时调整推广策略。
- 票型维度转化率分析,对比不同票型的访问量、下单量、支付量,找出性价比最高的流量入口。
- 用户复购分析,识别高价值用户,后续做会员定向优惠。
实现方式也不复杂,订单表有景区ID、票型ID、用户ID、支付时间这些字段,按日期分组聚合查询即可。数据量大了以后再用 Redis 做缓存,前期直接查 MySQL 足够。
我个人在实际操作中的一个体会是,全开源项目的价值不在代码本身,而在于你拿到它之后的“二次思考能力”。这套旅行吧门票系统,从技术选型到业务建模已经给了你一个高完成度的底座。你要做的是把它理解透,再往里面加入你自己对业务的理解,比如更适合本地景区的核销流程、更高效的供应链管理、更丝滑的用户体验。源码可以让你少走很多弯路,但真正的竞争力永远来自你对业务场景的深入思考。