简介:这份PHP票务管理系统源码面向有一定PHP基础的开发者与二次开发人员,用于快速搭建活动、赛事、演出等场景的在线售票平台,解决购票、选座、订单与后台管理等核心业务需求。压缩包共约2000个文件,整体25.59MB,以989个php业务代码为主,辅以415个html页面、173个js脚本、60个css样式及大量png、jpg、svg等图片素材,另含config配置、functions函数库、sql数据库脚本与字体、图标等资源,结构完整便于直接部署研究。系统采用MVC模式,涵盖用户注册登录、购票支付、订单追踪、座位图展示、管理员后台与报表统计等模块,并涉及SQL注入防护、缓存优化与响应式适配等实践要点。目前已有400人学习下载,适合借此理解票务系统架构、数据库设计与支付集成思路,并在此基础上按需定制开发。
1. 从一张演出票说起:这套 PHP 票务系统到底能扛什么事
上个月帮朋友的小剧场做线上售票,开场前两小时还有观众在门口问“还能买吗”,后台却因为并发锁票把同一排座位卖重了两次。那次之后我翻了不少票务源码,最后盯上一套 PHP 写的票务管理系统——它不是什么大厂中台,但胜在结构清楚、依赖少、能直接跑起来改。这套系统覆盖了演出/赛事/展览这类场景的核心链路:场次管理、座位分区、在线选座、下单锁座、支付回调、电子票核销,后台还带订单查询和退票处理。适合谁?一是想接私活做小型票务站点的独立开发者,二是拿它当 PHP 实战项目练手的学生,三是需要给自家场馆搭一套内部售票工具的小团队。它不追求高并发秒杀,但把“一张票从发布到核销”的完整状态流转讲明白了,这点比很多只堆 CRUD 的源码强。
2. 技术栈与目录结构:先看清骨架再动手
2.1 选型逻辑:为什么是 PHP + MySQL 这套组合
这套票务系统没有上微服务,也没引 Redis 集群,核心就是 PHP 原生 + MySQL + 少量前端模板。常见做法是后端用 ThinkPHP 或 Laravel 这类框架,但这套源码走的是轻量路线,入口文件直接路由分发,数据库用 PDO 预处理。这么选的原因很实际:小型票务场景的并发量通常在几百到几千 QPS 以内,MySQL 的行锁配合事务足够应付锁座;PHP 部署成本低,虚拟主机都能跑,场馆自己的运维不用懂容器编排。前端部分用的是原生 HTML + jQuery 提交选座请求,没有上 Vue/React,改起来门槛低,会点 JS 就能调样式。
提示:如果你的场景要扛演唱会级别的瞬时抢票,这套架构需要额外加缓存层和消息队列,后面改造章节会讲怎么加。
2.2 目录结构逐层拆解
拿到源码先别急着改代码,把目录跑一遍心里就有数了。典型结构如下:
ticket-system/ ├── index.php # 入口文件,路由分发 ├── config/ │ └── database.php # 数据库连接配置 ├── app/ │ ├── controller/ # 控制器:场次、订单、座位、用户 │ ├── model/ # 数据模型:封装 PDO 操作 │ └── service/ # 业务逻辑:锁座、下单、核销 ├── public/ │ ├── css/ # 样式 │ ├── js/ # 选座交互脚本 │ └── uploads/ # 演出海报等上传文件 ├── view/ │ ├── admin/ # 后台模板 │ └── front/ # 前台选座、订单页 ├── sql/ │ └── ticket.sql # 建表语句 + 初始数据 └── README.mdapp/controller里每个文件对应一个业务入口,比如ShowController.php管场次、OrderController.php管订单。app/service是重点,锁座和下单的并发控制都写在这里,不是散落在控制器里。sql/ticket.sql一定要先导入,里面除了建表还有几条测试场次数据,方便你跑通流程。
2.3 数据库表设计:座位和订单怎么关联
票务系统的核心表就几张,理解它们的关系比读代码更重要。常见设计是:
| 表名 | 作用 | 关键字段 |
|---|---|---|
show | 演出/场次 | id, title, show_time, venue |
seat_area | 座位分区 | id, show_id, area_name, price |
seat | 具体座位 | id, area_id, row_no, col_no, status |
order | 订单主表 | id, order_no, user_id, total, status |
order_seat | 订单座位关联 | order_id, seat_id, price |
ticket | 电子票 | id, order_id, qr_code, checked |
seat.status是关键字段,通常用 0 表示可售、1 表示锁定、2 表示已售。锁座就是把这个字段从 0 改成 1,支付成功再改成 2。order_seat做中间表是因为一个订单可能买多个座位,拆开方便退票时按座位粒度处理。
3. 核心链路拆解:锁座、下单、支付回调怎么写
3.1 选座锁座:用事务和行锁防超卖
锁座是整个系统最容易翻车的地方。两个人同时点同一个座位,如果先查再改,中间没有锁,就会卖重。这套源码在SeatService.php里的做法是:
// app/service/SeatService.php public function lockSeats($showId, $seatIds, $userId) { $pdo = $this->db->getConnection(); $pdo->beginTransaction(); // 开启事务 try { // 用 FOR UPDATE 锁住这些座位行,防止并发修改 $placeholders = implode(',', array_fill(0, count($seatIds), '?')); $sql = "SELECT id, status FROM seat WHERE id IN ($placeholders) AND show_id = ? FOR UPDATE"; $stmt = $pdo->prepare($sql); $stmt->execute(array_merge($seatIds, [$showId])); $seats = $stmt->fetchAll(PDO::FETCH_ASSOC); // 逐个检查状态,只要有一个不是可售就回滚 foreach ($seats as $seat) { if ($seat['status'] != 0) { $pdo->rollBack(); return ['code' => 1, 'msg' => '座位已被锁定']; } } // 全部可售,批量更新为锁定状态 $update = "UPDATE seat SET status = 1, lock_time = NOW(), lock_user = ? WHERE id IN ($placeholders)"; $stmt = $pdo->prepare($update); $stmt->execute(array_merge([$userId], $seatIds)); $pdo->commit(); return ['code' => 0, 'msg' => '锁定成功']; } catch (Exception $e) { $pdo->rollBack(); return ['code' => 2, 'msg' => '系统异常']; } }逻辑说明:FOR UPDATE是 MySQL 的排他锁,在事务里锁住查询到的行,其他事务再查同一批座位就得排队。参数$seatIds是前端传来的座位 ID 数组,$showId限定场次避免跨场次误锁。lock_time和lock_user是给后续超时释放用的——用户锁了座不付款,得有机制把座位放回去。
注意:
FOR UPDATE必须在事务里才生效,而且要求seat表的存储引擎是 InnoDB,MyISAM 不支持行锁,这点导入 SQL 时要确认。
3.2 下单与超时释放:别让座位被永久占住
锁座之后要生成订单,同时记录锁定时间。常见做法是订单创建时状态为“待支付”,并给一个 15 分钟的有效期。释放逻辑有两种:一种是定时脚本扫lock_time超过 15 分钟的座位批量释放,另一种是在用户下次选座时惰性检查。源码里用的是定时脚本,放在cron/release_seat.php:
// cron/release_seat.php 建议每 1 分钟跑一次 $expireTime = date('Y-m-d H:i:s', time() - 900); // 15 分钟前 $sql = "UPDATE seat SET status = 0, lock_time = NULL, lock_user = NULL WHERE status = 1 AND lock_time < ?"; $stmt = $pdo->prepare($sql); $stmt->execute([$expireTime]); echo "释放座位数:" . $stmt->rowCount();参数900就是 15 分钟,改成 600 就是 10 分钟,按你业务容忍度调。这个脚本用 crontab 挂上,* * * * * php /path/cron/release_seat.php。如果不挂,座位锁了不付款就永远占着,用户会投诉。
3.3 支付回调:验签和幂等一个都不能少
支付回调是另一个血泪坑。第三方支付会重复通知,如果不做幂等,同一笔订单可能被处理两次。源码在PayController.php里的处理:
// app/controller/PayController.php public function notify() { $data = $_POST; // 1. 验签,防止伪造回调 if (!$this->verifySign($data)) { exit('sign error'); } // 2. 幂等检查:订单已处理就直接返回成功 $order = $this->orderModel->getByOrderNo($data['out_trade_no']); if ($order['status'] == 'paid') { exit('success'); } // 3. 更新订单状态 + 座位改为已售 $this->db->beginTransaction(); try { $this->orderModel->updateStatus($order['id'], 'paid'); $this->seatModel->markSold($order['id']); $this->ticketModel->generate($order['id']); // 生成电子票 $this->db->commit(); } catch (Exception $e) { $this->db->rollBack(); exit('fail'); } exit('success'); }verifySign按你接的支付渠道实现,通常是 MD5 或 RSA。幂等那步最关键——先查订单状态,已支付就直接返回success,别重复生成票。generate里会往ticket表插一条带二维码内容的记录,核销时扫码查这张表。
4. 跑起来:环境准备到选座下单全流程
4.1 环境要求和导入步骤
这套系统对环境要求不高,PHP 7.2 以上、MySQL 5.7 以上就能跑。步骤:
- 把源码放到 Web 根目录,比如
/var/www/ticket。 - 建库:
CREATE DATABASE ticket_db DEFAULT CHARSET utf8mb4; - 导入
sql/ticket.sql:mysql -u root -p ticket_db < sql/ticket.sql - 改
config/database.php里的主机、库名、用户名、密码。 - 配 Nginx 或 Apache 指向
index.php,开伪静态。 - 浏览器访问,默认后台路径一般是
/index.php?m=admin。
提示:如果页面空白,先看 PHP 错误日志,常见是 PDO 扩展没开或
uploads目录没写权限。
4.2 一次完整购票流程走查
跑通的标准是能走完这条链路:前台选场次 → 选座 → 提交订单 → 模拟支付 → 后台看到已售 → 核销。选座页的 JS 会发一个 POST 到OrderController::create,带上show_id和seat_ids。下单成功后跳支付页,测试环境可以用源码里带的模拟支付入口,直接改订单状态。核销在后台“验票”菜单,输入电子票号或扫码,系统查ticket表把checked置 1。
4.3 后台管理能改什么
后台不只是看订单。场次管理可以新增演出、设置开售时间;座位分区能画座位图、改价格;订单管理支持按手机号、订单号搜索,也能手动退票——退票会把seat.status改回 0,同时order状态置为 refunded。用户管理里能看注册用户和他们的购票记录。这些功能对应view/admin下的模板,改字段直接改模板和对应控制器方法就行。
5. 避坑与排查:那些让我加班到凌晨的问题
5.1 座位卖重了
现象:同一座位出现两个已支付订单。原因:锁座没用事务或没用FOR UPDATE,两个请求同时读到 status=0。解决:确认SeatService::lockSeats在事务内且用了FOR UPDATE,并检查 MySQL 引擎是 InnoDB。
5.2 支付回调重复生成票
现象:一笔订单生成了多张电子票。原因:回调没做幂等,支付方重复通知。解决:在notify开头查订单状态,已 paid 直接返回 success,不重复执行后续逻辑。
5.3 锁定的座位不释放
现象:用户下单未付款,座位一直显示锁定。原因:没配释放脚本或 crontab 没生效。解决:手动跑一次php cron/release_seat.php看输出,再检查 crontab 时间表达式。
5.4 中文乱码
现象:演出标题、座位分区名显示问号。原因:数据库或连接字符集不是 utf8mb4。解决:建库时指定DEFAULT CHARSET utf8mb4,PDO 连接串加charset=utf8mb4。
5.5 上传海报失败
现象:后台上传演出海报提示失败。原因:public/uploads目录权限不足或 PHPupload_max_filesize太小。解决:chmod -R 755 public/uploads,并在 php.ini 调大上传限制。
6. 二次开发:从能跑到好用要改哪几刀
这套源码跑通之后,真正花时间的是按自己业务改。第一刀我一般砍在座位图上——源码自带的座位图是简单表格,实际场馆座位有弧形、有包厢,得把view/front/seat.php里的渲染逻辑换成 Canvas 或 SVG,座位坐标存到seat表的x_pos、y_pos字段。第二刀是加缓存,选座页每次刷新都查库,场次多了会慢,常见做法是用 Redis 缓存场次和座位状态,锁座时先操作 Redis 再异步落库,但这会引入一致性问题,小场馆不建议上。第三刀是支付渠道,源码里是模拟支付,接真实渠道要改PayController的verifySign和回调地址,注意回调必须公网可达。
改造时验证方法很直接:改完锁座逻辑,用ab或wrk压一下选座接口,看有没有超卖;改完支付,用支付方提供的沙箱环境跑几遍重复通知。我自己的习惯是每次动并发相关的代码,都强制走一遍“两人同时抢同一座位”的手动测试,别嫌麻烦。从那以后我每次改锁座逻辑,都先开两个浏览器窗口同时点同一个座位,确认只有一个成功才敢上线。希望帮到你。
本文还有配套的精品资源,点击获取