PHP电影选座系统源码解析:环境搭建、锁座机制与部署迁移指南
简介这是一套基于PHP实现的电影购票选座系统源码面向计算机专业学生、PHP初学者及需要完成课程设计或毕业设计的人群涵盖用户购票选座、后台管理与影院端运营三类页面可用于影院管理类Web项目的学习与二次开发。压缩包共约2000个文件体积47.58MB其中JavaScript文件913个、CSS样式332个、HTML页面306个另有PHP业务文件40个、LESS与SCSS样式约88个以及PNG、JPG等界面素材与SQL脚本、Markdown说明文档前端依赖与静态资源比重较高便于直接查阅样式与交互实现。目前已有505人学习下载。读者可据此梳理三端分离的目录结构理解选座、下单、后台管理等模块的请求流程与数据交互方式并参考Bootstrap、AdminLTE等界面框架的引用与布局写法结合数据库脚本可较快搭建本地调试环境。整体更适合具备PHP与前端基础的开发者作为实战参考素材与依赖较为齐备能减少自行搜集静态资源的成本。1. 从 PHP 源码包到可运行选座系统的第一个 20 分钟下载到的基于 PHP 电影购票选座系统不是那种几十个文件的教学 demo。解压之后是 2341 个文件光 PHP 类文件就有 75 个再加上 913 个 JavaScript、332 个 CSS、304 张 PNG第一次看到的人很容易把它误判成资源站打包模板。实际上这是一套三入口共存的完整业务系统用户端从 localhost/index.php 进入管理员从 localhost/admin/index.php 管理排片和订单影院侧通过 localhost/theatre/index 做现场取票和场次确认。它没有 Laravel 或 ThinkPHP 这种框架层包装路由、会话、权限、数据库连接全部原生 PHP对想研究“网页点击到数据库写入”全链路的人来说拆解价值比脚手架工程更高。如果你手头只有源码包而不知道从哪下手本文会按环境搭建、选座链路、后台权限、部署迁移、一致性校验五条线把每一处关键位置标出来。2. 源码目录解剖与 PHP 环境配置的工程化选择2.1 文件分布决定了它不是静态模板站先把顶层目录打开。项目里 JavaScript 文件数量远超 PHP这个比例很容易让人以为重点在前端展示。但真正决定业务能力的是 75 个 PHP 类文件这类 php源码 的目录风格通常是 controllers、models、views 并列不依赖 Composer 自动加载而是通过 require_once 按页面手动引入。我拿到源码包的第一件事不是复制到 Web 目录而是用命令统计真实代码入口find . -name *.php -type f | wc -l find ./controllers -name *.php -type f | sort grep -rn mysql_connect\|new PDO --include*.php . | head -20第一行确认 PHP 文件总数和是否存在二级目录被忽略第二行看控制器文件的划分粒度如果 controllers 下按电影、场次、订单、用户拆成多个类说明没有走单文件 if-else 的老路。第三行最关键能直接判断项目用的是 mysql_* 函数还是 PDO。mysql_* 是 PHP 7.0 废除的函数碰到 mysqli 或 PDO 则版本兼容性更好。这个检查结果直接影响本地 PHP 版本选择。提示如果 grep 结果里全是 mysql_connect()就用 PHP 5.6 或 7.0若是 new PDO用 7.4 没问题。PHP 8.0 以上对短标签和某些字符串函数不友好不建议作为这套源码的运行版本。2.2 phpstudy 与 XAMPP 哪一种更适合本机复现源码包没有 composer.json 也没有 vendor 目录显然是为共享主机或集成环境准备的。Windows 用户用 phpstudy 的好处是能一键切换 PHP 7.4 / 5.6MySQL 和 Nginx 都能独立启停Mac / Linux 下用 XAMPP 或直接装 php-fpm 更顺手。两种环境的核心差异集中在下面这张表对比项phpstudyXAMPPPHP 版本切换面板内下拉切换需改启动脚本或调整 PATH数据库工具phpMyAdminphpMyAdmin / Adminer默认 Web 服务器Apache Nginx 可选Apache适合场景频繁测试多套 php源码一次配置长期使用参数解释phpstudy 的虚拟主机配置在 vhosts.conf 中对应 Nginx 则是 vhosts 目录。项目带伪静态规则时Apache 用 .htaccess 自动生效Nginx 需要手动加 try_files。如果路径解析后始终访问到默认页面先看 host 文件里是否写了带域名的映射再看站点配置的 root 是否指到源码物理路径。很多时候访问 localhost 能打开首页、子页面 404是因为站点根目录多套了一层解压出来的文件夹导致相对路径和 URL 路由对不上。2.3 数据库导入与 config 里的隐藏参数源码包的数据库脚本一般在 sql/ 或 database/ 目录下也可能保存在 install 页面里。导入表结构前先建一个独立库不建议直接覆盖已有业务库。以选座核心表为例必须有场次、座位、订单三张表外加 user 表。一张典型座位表长这样CREATE TABLE IF NOT EXISTS seat_map ( id INT UNSIGNED AUTO_INCREMENT, theatre_id INT NOT NULL, hall_id INT NOT NULL, row_no CHAR(2) NOT NULL COMMENT 排号A/B/C, col_no SMALLINT NOT NULL, seat_type TINYINT DEFAULT 0 COMMENT 0普通 1情侣 2VIP, status TINYINT NOT NULL DEFAULT 0 COMMENT 0空闲 1锁定 2售出, PRIMARY KEY (id), KEY idx_hall (theatre_id, hall_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;再配一张订单表CREATE TABLE IF NOT EXISTS seat_order ( id INT UNSIGNED AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, schedule_id INT NOT NULL, seat_map_id INT NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已取消, create_time INT NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_schedule_seat (schedule_id, seat_map_id), KEY idx_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;seat_map 存场地永久座位seat_order 存选座和购买关系唯一键 uk_schedule_seat 保证同一个场次下同一个座位不能被两笔订单同时记录。很多源码字段名会不同比如改成 seat_row / seat_col但唯一约束不能少。如果项目里没有这个唯一键二开时务必补上。导入完数据后改配置文件常见配置项在根目录 config.php 或 bootstrap.php 里形如define(DB_HOST, 127.0.0.1); define(DB_NAME, cinema_db); define(DB_USER, root); define(DB_PASS, 123456); define(DB_PREFIX, t_);这里的 DB_PREFIX 是表前缀项目中所有 SQL 查询都会拼上这个前缀。改完配置不能只刷新浏览器要确认 MySQL 用户对 cinema_db 有 SELECT、INSERT、UPDATE、DELETE 权限。localhost 与 127.0.0.1 在 MySQL 授权里是两个不同 host如果连接失败看报错是 Access denied 还是 Unknown database。Access denied 通常是命令行能登录、PHP-FPM 进程用的 socket 端口不一致导致的。3. 电影选座核心链路请求路由、锁座与订单落库3.1 index.php 入口怎么分发用户入口 index.php 不直接显示页面而是根据请求参数加载控制器。原生 PHP 的路由写法通常是$module isset($_GET[m]) ? $_GET[m] : film; $action isset($_GET[a]) ? $_GET[a] : list; $controllerFile ./app/controllers/ . ucfirst($module) . Controller.php; if (!file_exists($controllerFile)) { http_response_code(404); exit(controller not found); } require_once $controllerFile; $controllerName ucfirst($module) . Controller; $controller new $controllerName(); $controller-$action();m 参数决定控制器a 参数决定方法比如 index.php?mscheduleaselect 会加载 schedule 控制器里的 select() 方法。新手喜欢把业务逻辑直接写在这个入口文件里代码一多就难以维护。源码里的 controller 类通常还会调用 model把 SQL 封装在模型层。如果要改动购票流程只动控制器不够必须把下单操作下沉到 OrderModel否则排片、锁座、支付回调三处逻辑重复。这个路由示例没有做白名单$module 来自用户输入就直接拼文件名攻击者可以用路径穿越构造异常请求实际项目里应该在 include 前加一层 preg_match(/^[a-z]$/, $module) 校验。3.2 选座的锁座策略比付款更重要在线选座最直观的问题是同一座位被两个人同时抢到。前端把座位渲染成网格时用户点击座位后一般先调锁座接口锁座成功再弹出订单确认框。这里不建议先 create 订单再改座位状态因为订单表只记录业务数据不负责并发控制。正确顺序是先把座位状态改成锁定只有修改行数为 1 时才允许插入订单记录$sql UPDATE seat_map SET status 1, lock_time :now WHERE id :seatId AND status 0; $stmt $pdo-prepare($sql); $stmt-execute([:now time(), :seatId $seatId]); if ($stmt-rowCount() ! 1) { throw new RuntimeException(座位已不可选); } $pdo-beginTransaction(); try { $stmt $pdo-prepare(INSERT INTO seat_order (order_no, schedule_id, seat_map_id, status, create_time) VALUES (?, ?, ?, 0, ?)); $stmt-execute([$orderNo, $scheduleId, $seatId, time()]); $pdo-commit(); } catch (Throwable $e) { $pdo-rollBack(); $pdo-prepare(UPDATE seat_map SET status 0 WHERE id ?) -execute([$seatId]); throw $e; }逻辑说明UPDATE 的 where 条件里带 status 0相当于数据库层面的乐观锁。同一时间两个请求都读到 status 0InnoDB 会锁行第二个 UPDATE 影响行数为 0直接抛异常避免程序锁造成的死锁。beginTransaction 放在 UPDATE 之后时第一条 UPDATE 实际上已经隐式提交严格做法是把 UPDATE 也放进事务内。常见源码把 UPDATE 放在事务外再用 rowCount 判断低并发下没明显问题但在票务这种高冲突场景会产生“座位状态已改订单未生成”的中间状态。lock_time 字段记录锁座时间需要配一个每分钟执行一次的释放脚本UPDATE seat_map SET status 0 WHERE status 1 AND lock_time UNIX_TIMESTAMP() - 900;900 表示 15 分钟内未完成支付就释放座位。有些源码把释放逻辑放在用户再次访问选座页时触发那样只会释放当前场次处理不了用户中途放弃的情况。3.3 选座接口参数与前端状态同步选座页面通过 AJAX 拉取当前场次座位数据接口返回的每个座位至少要包含 seat_map_id、row_no、col_no、status 四个字段。status 的语义必须与后端完全统一0 空闲、1 锁定、2 售出。前端根据数字渲染颜色不能把 0 以外的值都当作不可选否则会遇到座位已解锁但页面仍然灰色的问题。前端点击座位的完整交互拆分如下步骤请求参数返回处理1POST /ajax/lock_seat.phpschedule_id, seat_map_idcode:0 则选择成功2POST /ajax/order_create.phpschedule_id, seat_ids[], user_id返回 order_no 进入支付3POST /pay/notify.phporder_no, pay_status回调更新订单状态第一步和第二步可以合并成一个接口但合并后锁座粒度变大不利于多场次联选。源码里为了省事会把多个座位拼成数组一次性提交后端需要循环执行上面的 UPDATE只要一个座位更新失败就回滚所有已改状态。建议把 seat_ids 数组长度限制在 8 个以内并在接口层做数量校验避免选座请求变成批量占座工具。JSON 返回结构建议统一为{ code: 0, msg: 锁座成功, data: { order_no: 2025060712345678, expire_seconds: 900 } }data.expire_seconds 是锁座倒计时前端拿到后启动定时器刷新剩余支付时间。这个字段直接决定体验设置太短用户还没输完验证码座位就释放了太长又让热门场次座位被无效占用。一般配置 900 秒大型活动可以调成 300 秒配合每分钟的锁释放脚本。4. 后台管理边界admin 与 theatre 的权限和数据流4.1 两套后台入口的资源隔离admin 和 theatre 是两套视觉、功能都不同的后台。admin 管理全局数据电影、影院、场次、订单、用户theatre 管理单个影院的执行操作今日排片、取票、退票和对账。很多二开者给源码加功能时习惯直接把功能挂到 admin 菜单下忽略了两边操作人员差异。从入口看localhost/admin/index.php 与 localhost/theatre/index 对应不同控制器目录但都读取同一张 user 表通过 user_type 字段区分身份。管理员登录后写入 SESSION[role]admin影院员工写入 theatre。如果只用 SESSION[uid] 判断是否登录会出现 admin 登录后直接访问 theatre 页面也能进入的安全漏洞。正确写法是session_start(); if (empty($_SESSION[uid]) || $_SESSION[role] ! admin) { header(Location: login.php); exit; }这段代码放在 admin 页面的公共头部文件里。theatre 端入口公共文件把 role 判断改成 theatre。注意有些源码把公共头部写在 admin/common.php却被 theatre 页面 include导致影院员工被要求必须是 admin这种耦合在拆权限时必须一起修复。4.2 排片管理和座位状态重置管理员新增排片时需要选择的字段包括影片、影厅、开场时间、结束时间、票价。票价可能有普通价、会员价和不同座位类型加价。排片表的设计直接影响后面订单金额计算字段说明schedule_id场次 ID订单和座位都挂在这个 ID 下film_id影片 IDhall_id影厅 ID决定座位图show_time开场时间用 int 存时间戳price基础票价浮点值seat_extra_priceJSON 字段如 {1:10,2:20}seat_extra_price 存座位类型额外加价例如 VIP 加 10 元。排片时校验 show_time 不能小于当前时间同时同一影厅两个场次之间要留出散场间隔。没有间隔的话上一场还没散场下一场观众已经进厅现场一定会冲突。座位状态重置不是每天清空整张表而是按场次维度处理。每场结束后把 seat_map 的 status 改回 0 并清 lock_time。因为同一时间可能有多个场次在进行不能全表更新正确做法是关联当前场次的订单UPDATE seat_map sm LEFT JOIN seat_order so ON sm.id so.seat_map_id AND so.schedule_id ? SET sm.status 0, sm.lock_time 0 WHERE sm.theatre_id ? AND sm.hall_id ? AND (so.status 2 OR so.id IS NULL);so.status 2 代表订单已取消座位可以释放so.id IS NULL 代表没有被当前场次订单占用的座位。已付款订单对应的座位不能释放否则会造成现场无座可坐。这个动作可以由计划任务定时触发也可以由管理员点击“结束场次”按钮触发后者要加操作确认避免误释放正在放映中的场次。4.3 订单状态机与退款联动订单状态应该是有限状态机待支付、已支付、已取消、已退款、已使用。每次状态变更都要同时影响订单表和座位表。后台退款如果只改订单状态而不释放座位用户再看到座位仍然显示已售体验很差。退款操作的正确写法public function refund(int $orderId): void { $db-beginTransaction(); try { $db-update(seat_order, [status 3], [id $orderId]); $db-exec(UPDATE seat_map sm INNER JOIN seat_order so ON sm.id so.seat_map_id SET sm.status 0 WHERE so.id ?, [$orderId]); $db-commit(); } catch (Throwable $e) { $db-rollBack(); } }这里一个事务修改两张表第二次 UPDATE 失败时 rollBack 会让订单仍保持已支付状态。数据一致性不能靠定时任务退款必须用事务。有些源码会在退款时把 seat_map 的 status 改成 2 而不是 0这是为了防止库存串号但退款后的座位应该立即可售否则后续收入会受损。5. 迁移到 Nginx PHP-FPM 后的伪静态与路径修复5.1 从 Apache 到 Nginx 的第一处差异源码自带 .htaccessApache 下自动启用Nginx 不读这个文件。选座系统链接如果形如 index.php?mfilmadetailid3Nginx 不需要额外重写如果源码做过 SEO 伪静态URL 形如 film-detail-3.html就必须手工转换规则。转换后的 Nginx 配置通常是location / { try_files $uri $uri/ /index.php?$query_string; } location ~ [^/]\.php(/|$) { fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; }location / 里的 try_files 把不存在的路径交给 index.php 处理同时保留原始 query_string。第二段 location 负责 PHP 解析fastcgi_pass 必须指向 php-fpm 实际监听的端口或 socket。最常见的坑是 fastcgi_param SCRIPT_FILENAME 写错导致所有 PHP 页面 404 或 500。如果只配了 location / 而没有 PHP 解析段访问 index.php 会返回空白页。Apache 和 Nginx 的参数差异可以对照下面这张表配置项ApacheNginx入口重写.htaccess 内 RewriteRuletry_files / location rewritePHP 执行mod_php 自动处理fastcgi_pass 指定 php-fpm静态资源DocumentRoot 默认root 指令错误显示直接输出到浏览器error_log 写文件5.2 静态资源 404 的原因与修复选座页面由大量 bootstrap.css、AdminLTE.css 和 JS 文件组成迁移后经常出现样式全部丢失。打开浏览器 Network 面板会发现 .css 和 .js 返回 404。常见原因不是文件缺失而是 Nginx 的 root 路径和源码内部 basePath 不一致。假设源码解压在 /srv/www/cinemaNginx 配置 root 指向这里server { listen 80; server_name cinema.local; root /srv/www/cinema; index index.php index.html; }页面里引用 /assets/css/bootstrap.min.css浏览器请求的是 /assets/css/bootstrap.min.cssNginx 会查找 /srv/www/cinema/assets/css/...。如果实际目录在 /srv/www/cinema/public/assets/...root 需要指向 public或者模板里给资源路径加 /public 前缀。另一个排查方法是直接看日志tail -f /var/log/nginx/error.log如果日志显示 open() /srv/www/cinema/assets/bootstrap.css failed说明 root 和请求 URI 拼接出的路径不存在调整 root 或加 alias 映射即可。还有一类 404 是伪静态 rewrite 把静态文件也转发到 index.php 了需要在 location 里加一句location ~* \.(css|js|png|jpg)$ { expires 7d; }优先处理静态资源。5.3 PHP 错误在 Nginx 下不显示的处理Apache 下 PHP 报错直接输出到浏览器Nginx PHP-FPM 下默认写到 php-fpm 日志页面只显示空白或 500。调试时我一般同时开两个日志sudo tail -f /var/log/php-fpm/www-error.log sudo tail -n 100 /var/log/nginx/error.log然后在 PHP 端临时打开显示错误ini_set(display_errors, 1); error_reporting(E_ALL);display_errors 只适合开发环境线上环境保持关闭log_errors 打开。很多二开者改完代码发现页面空白直接在文件里搜 die() 或 var_dump()这些调试输出在迁移后可能因为输出缓冲看不到反而把时间浪费在无关代码上。6. 用 SQL 和 PHP 脚本对账选座在线状态6.1 三分钟跑完的异常对账选座系统上线后最常见的问题是订单状态和座位状态悄悄不同步。比如支付回调失败导致订单已支付但座位还是锁定或者退款脚本被中断导致座位已释放但订单还显示待支付。与其靠用户投诉不如把下面的对账 SQL 放进定时任务。SELECT so.id AS order_id, so.status AS order_status, sm.status AS seat_status, sm.hall_id FROM seat_order so JOIN seat_map sm ON sm.id so.seat_map_id WHERE (so.status 1 AND sm.status 2) OR (so.status 0 AND sm.status NOT IN (0, 1));这段 SQL 查出两类异常已支付订单对应座位不是售出状态待支付订单对应座位不是空闲或锁定状态。第一类异常会导致用户付了钱但没有座位第二类异常会导致用户在未付款情况下锁座。配合 PHP 脚本可以做自动修复?php // consistency_check.php require config.php; $pdo new PDO(DB_DSN, DB_USER, DB_PASS); $sql SELECT so.id, so.order_no, sm.status FROM seat_order so JOIN seat_map sm ON sm.id so.seat_map_id WHERE so.status 1 AND sm.status 2; foreach ($pdo-query($sql, PDO::FETCH_ASSOC) as $row) { $fix $pdo-prepare( UPDATE seat_map SET status 2 WHERE id ( SELECT seat_map_id FROM seat_order WHERE id ? ) ); $fix-execute([$row[id]]); error_log(fixed order {$row[order_no]} seat status to sold); }逻辑说明脚本里以订单状态为准修正座位状态必要时再补充通知支付中心的逻辑。参数说明把这段脚本挂到 crontab 每五分钟执行一次能发现大多数由异常中断导致的选座状态漂移表名和状态值要根据实际源码调整例如 status 含义不同时需修改常量。最后再配合后台的场次结束触发器整套系统在无人工介入的情况下也能保持前台可见的座位图与实际订单一致。本文还有配套的精品资源点击获取