ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

乒乓球预约管理系统源码解析:状态机与事务并发如何跑通预约闭环

乒乓球预约管理系统源码解析:状态机与事务并发如何跑通预约闭环 简介乒乓球预约管理系统是一套基于JSP的毕业设计项目源码适用于学校体育场馆、俱乐部等场景的场地预约管理面向Java Web学习者、课程设计学生及需要快速搭建预约类平台的开发者覆盖用户注册登录、场地查询、在线预约、取消修改、管理员后台及预约数据统计等完整功能。资源包为RAR压缩包大小36.95MB共810个文件主要包含Java源代码、JSP页面、Vue前端组件、CSS/JS脚本、SQL数据库脚本及演示视频等其中数据库表设计覆盖用户、场地和预约关系代码、配置与说明文档齐全目录结构清晰便于查阅和二次开发。目前已有137人浏览学习可直接作为毕业设计参考或项目实战练手。包内附带的安装运行脚本和演示录屏能帮助快速启动系统SQL初始化脚本便于建表通过阅读源码可深入理解JSP语法、MVC分层、数据库表设计及前后端交互逻辑同时掌握预约业务中并发冲突和展示优化的常见处理思路对完成毕设答辩和提升Web开发能力具有实际价值。1. 乒乓球预约管理系统一个能跑通预约闭环的源码项目手里这份乒乓球预约管理系统压缩包光看名字就值得拆开研究它同时给了源码和演示视频这意味着你不需要靠想象去猜系统长什么样打开视频就能看到预约流程怎么走。这类项目在课程设计、毕业设计里非常常见核心价值不只是能下单而是把场地、时段、订单、支付状态、核销动作串成一条完整链路。适合两类人一是急着交作业、想找一份能讲清楚逻辑的预约管理系统源码的学生二是想在小球馆里落地一套轻量预约工具、却不想从零开始写业务状态的开发者。我下面按自己落地这类系统的习惯从业务规则、数据库、接口、前端演示、部署排错一路讲到定时清理技巧照着做就能跑起来。2. 业务规则先行预约状态机、时段模型与场景选型很多人拿到源码第一件事就是建库、导入、跑前端跑通之后却讲不清系统为什么这么设计。我的习惯正好相反先理清楚业务规则再打开代码。乒乓球预约管理系统的灵魂不是页面样式而是状态机。状态机错了后面所有接口、表格、演示视频都会跟着错。2.1 用固定时段槽解决一小时怎么拆的模型难题乒乓球馆约球有个特点场地按小时或半小时计费一天从早上9点开到晚上10点中间全是可预约的连续时间段。如果只在订单表里存 start_time 和 end_time查询哪些时段空闲时就不得不拿订单表和场地表做时间区间比较SQL 又绕又容易漏边界两个订单时间重叠的判断写起来非常容易翻车。常见做法是提前把一天切成固定时段槽。比如早上 09:00-09:30、09:30-10:00、10:00-10:30……一直切到 21:30-22:00每天每个场地生成一批时段记录。每个时段槽带独立状态0 空闲、1 已锁定、2 已占用。用户选中的是一个个具体时段槽系统锁定的也是具体时段槽查询哪些能约就变成了一条简单语句SELECT id, start_time, end_time FROM t_court_session WHERE court_id 1 AND session_date 2025-06-20 AND status 0 ORDER BY start_time;参数说明court_id锁定具体场地session_date锁定日期status 0过滤掉已被锁定的槽。这个模型最大的好处是查询和更新都不需要做时间段相交判断数据库唯一索引也能直接派上用场。时段粒度怎么定半小时一档比较通用客单价高、翻台率高的球馆可以切到 15 分钟但如果只是做课设演示建议用一小时因为粒度越小并发抢约时的冲突概率越高演示时越容易暴露并发控制的短板。顺带说一句教室预约管理系统、会议室预约系统也是同一套模型把1号乒乓球台换成301教室把时段槽换成第几节课核心逻辑完全不用动。2.2 订单状态机待支付、已支付、已取消、已过期的流转边界预约系统的订单不能只有已预约一个状态否则后台没法区分哪些是有效占用、哪些是垃圾数据。我一般会设计五个状态并在代码里统一定义常量状态值状态名进入方式后续流转0待支付用户提交订单后支付成功转 1超时转 4取消转 31已支付支付回调成功后到店核销转 2可取消则转 32已核销前台管理员扫码/点核销终态3已取消用户主动取消或管理员取消终态4已过期超过支付时限且未支付终态这里有一个关键取舍是先锁时段再等支付还是先支付再锁时段前者体验好用户下单时看到时段就被占住了不会因为慢一步被别人抢走代价是恶意用户下单不支付时段被白白占用。后者不会占着茅坑不拉屎但用户支付成功才发现时段没了体验极差。我建议采用折中方案下单即锁时段同时给订单设置 15 分钟支付超时超时由定时任务自动把时段释放。这个策略既保体验又给了脏数据兜底出口后面第 6 章会专门讲释放脚本怎么写。状态机的另一个边界是已支付订单能不能取消。如果不允许取消用户体验差如果无条件允许取消球馆的排期会很被动。常见做法是允许开场前 30 分钟以外取消开场前 30 分钟内锁定不可退。这个判断放在业务层做不放 SQL 里因为还涉及时间计算、管理员权限判断。2.3 用户流程与管理员流程两条主线如何影响表结构画完状态机下一步是拉两条业务主线。用户主线注册登录 → 选场地 → 选日期 → 选时段 → 提交订单 → 模拟支付 → 查看订单凭证 → 到店核销。管理员主线维护场地信息 → 查看今日预约 → 核销订单 → 手动取消异常订单 → 看场地使用率。这两条主线直接决定数据库里必须有哪些表。用户主线需要用户表、场地表、时段表、订单表管理员主线额外要求操作日志表因为一旦出现这个订单被谁取消的纠纷没有日志就是黑匣子谁都说不清。日志表不用复杂记录操作人、操作类型、订单号、操作时间、备注五个字段就够。很多课设源码不包含日志表我建议自己补上答辩时这是一个很好的加分点。3. 数据模型与后端接口把时段抢约做成数据库事务业务规则定完代码就有了解释的依据。常见的源码是 PHP MySQL 组合也可能是 Java Spring Boot但核心表结构和接口思路大同小异。我以 PHP 原生写法为例把最关键的数据表和三段核心逻辑讲透。3.1 三张核心表场地表、时段表、订单表场地表最简单维护场地名称和状态CREATE TABLE t_court ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL COMMENT 场地名称如1号台, location VARCHAR(100) DEFAULT COMMENT 位置描述, status TINYINT NOT NULL DEFAULT 1 COMMENT 1可用 0停用, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT乒乓球场地表;字段说明status控制场地是否对外开放停用的场地在用户端直接不展示。created_at是审计字段所有表都应该有排查问题时能看出数据是什么时候产生的。时段表是预约系统的核心表CREATE TABLE t_court_session ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, court_id INT UNSIGNED NOT NULL COMMENT 场地ID, session_date DATE NOT NULL COMMENT 日期, start_time TIME NOT NULL COMMENT 开始时间, end_time TIME NOT NULL COMMENT 结束时间, status TINYINT NOT NULL DEFAULT 0 COMMENT 0空闲 1已锁定 2已占用, lock_order_id INT UNSIGNED DEFAULT NULL COMMENT 锁定订单ID, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_court_date_time (court_id, session_date, start_time), KEY idx_date_status (session_date, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT场地时段表;这里两个索引很关键。uk_court_date_time是唯一键保证同一个场地同一天同一时刻只能有一条时段记录从物理层面杜绝重复生成时段。idx_date_status是查询索引管理后台看某天哪些场地可约时走这个索引数据量大时不会全表扫描。lock_order_id字段是谁锁定了这个时段的凭证取消订单或定时过期释放时靠它精准释放。订单表记录用户、时段、金额和状态CREATE TABLE t_order ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL COMMENT 业务订单号, user_id INT UNSIGNED NOT NULL COMMENT 用户ID, court_id INT UNSIGNED NOT NULL COMMENT 场地ID, session_date DATE NOT NULL COMMENT 预约日期, start_time TIME NOT NULL COMMENT 开始时间, total_price DECIMAL(10,2) NOT NULL COMMENT 订单金额, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已核销 3已取消 4已过期, pay_time DATETIME DEFAULT NULL COMMENT 支付时间, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_order_no (order_no), KEY idx_user_status (user_id, status), KEY idx_created_at (created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT预约订单表;order_no建议用date(YmdHis) . mt_rand(1000, 9999)或 UUID 生成对外展示、取消、核销都走业务订单号不要把自增 ID 暴露给用户否则别人遍历 ID 就能看到你的全部订单量。3.2 抢约时段的原子更新条件 UPDATE 事务不用先查再插超卖是预约系统最容易翻车的地方。两个用户同时抢最后一个空闲时段如果代码写成先 SELECT 查 status0再 INSERT 订单两个请求都会读到空闲然后都下单成功。这个问题的根源是判断和更新分成了两步中间存在时间窗口。正确的做法是把判断时段是否空闲和把时段改为已锁定合并成一条 UPDATE?php require config.php; $courtId (int)$_POST[court_id]; $sessionDate $_POST[session_date]; $sessionIds array_map(intval, (array)$_POST[session_ids]); // 用户勾选的时段ID列表 $pdo-beginTransaction(); try { // 动态生成 IN 占位符 $placeholders implode(,, array_fill(0, count($sessionIds), ?)); $sql UPDATE t_court_session SET status 1, lock_order_id ? WHERE court_id ? AND session_date ? AND id IN ({$placeholders}) AND status 0; $params array_merge([$orderId, $courtId, $sessionDate], $sessionIds); $stmt $pdo-prepare($sql); $stmt-execute($params); if ($stmt-rowCount() ! count($sessionIds)) { throw new RuntimeException(部分时段已被抢约请刷新后重试); } // 插入订单记录order_no 在调用本接口前生成 $insert $pdo-prepare( INSERT INTO t_order (order_no, user_id, court_id, session_date, start_time, total_price, status) VALUES (?, ?, ?, ?, ?, ?, 0) ); $insert-execute([$orderNo, $userId, $courtId, $sessionDate, $startTime, $totalPrice]); $pdo-commit(); echo json_encode([code 0, msg 预约成功, order_no $orderNo]); } catch (Throwable $e) { $pdo-rollBack(); echo json_encode([code 1, msg $e-getMessage()]); }逻辑说明UPDATE ... WHERE status 0是原子操作InnoDB 会对命中的行加锁第二个请求执行同样语句时会阻塞或影响行数为 0。rowCount()是最终防线如果更新的行数不等于用户勾选的时段数说明其中有时段被别人抢先锁走整个事务回滚一个时段都不会锁定。参数说明$sessionIds是用户勾选的时段 ID可以一个订单预约多个时段但必须是同一场地同一天。lock_order_id先填入订单号再插入订单这里假设订单号已经在调用本接口前生成好或者先插入订单拿到自增 ID 再更新时段两种写法都可以但必须保证时段锁定和订单创建在同一个事务里。有人会问为什么不用SELECT ... FOR UPDATE条件 UPDATE 其实是更简洁的方案它把查询判断和状态更新合并成一条语句不需要显式处理行锁的释放。唯一要注意的是最终代码里所有值都必须走 PDO 预处理绑定千万不要用字符串拼接 SQL否则预约接口被注入一次整个库的账号数据就全没了。3.3 取消订单与释放时段状态机在代码里的最小实现取消订单要同时做两件事把订单状态改成已取消把时段状态改回空闲。两个动作必须放在同一个事务里否则会出现订单取消了但时段还被占着或时段释放了但订单还是已支付的半截状态。?php require config.php; $orderNo $_POST[order_no]; $userId (int)$_POST[user_id]; $pdo-beginTransaction(); try { // 关键WHERE 条件带上 status 1只允许取消已支付订单 $stmt $pdo-prepare( UPDATE t_order SET status 3 WHERE order_no ? AND user_id ? AND status 1 ); $stmt-execute([$orderNo, $userId]); if ($stmt-rowCount() 0) { // 拿到该订单对应的场地时段记录逐个释放 $pdo-prepare( UPDATE t_court_session SET status 0, lock_order_id NULL WHERE lock_order_id ? )-execute([$orderId]); } $pdo-commit(); echo json_encode([code 0, msg 取消成功]); } catch (Throwable $e) { $pdo-rollBack(); echo json_encode([code 1, msg 取消失败请稍后重试]); }这里最重要的细节是WHERE ... status 1这个条件。它保证了幂等性用户重复点击两次取消按钮第二次执行时订单状态已经是 3rowCount()返回 0不会再去执行释放时段语句。没有这个条件同样的取消请求发两次第一次正常第二次可能把其他订单锁定的时段错误释放这种 bug 很难排查前端看到报错后台数据也乱了。3.4 接口一览预约管理系统的核心路由清单把常用源码里的接口路径列一下整体结构大概是这样的接口路径方法作用核心参数/api/courts.phpGET场地列表status/api/sessions.phpGET某天可约时段court_id, session_date/api/order/create.phpPOST创建订单并锁时段court_id, session_date, session_ids/api/order/pay.phpPOST模拟支付order_no/api/order/cancel.phpPOST取消订单order_no, user_id/api/order/list.phpGET我的订单列表user_id, status/api/admin/checkin.phpPOST核销入场order_no接口统一返回 JSON 格式数据结构固定为code、msg、data三个字段。code 0表示成功非 0 表示失败。前端拿到响应后只看 code 做分支展示层只消费 data 里的业务数据。这个小规范成本极低但能让后续接 Vue、微信小程序端时少改一堆代码。4. 前端页面与演示视频选场、支付、核销的完整演示链路后端逻辑再完整演示视频里如果只录了一个下单成功的页面验收的人还是会觉得系统不真实。前端要做的不是花哨的页面而是把后端状态机完整呈现出来空闲的能点被占的变灰支付后状态变核销后流程关。4.1 时段选择器的状态渲染让已约时段变灰时段选择是预约系统的核心交互。用户选了场地和日期后前端请求后端接口拿到时段列表根据每个时段的 status 字段渲染不同样式fetch(/api/sessions.php?court_id1session_date2025-06-20) .then(res res.json()) .then(data { const container document.getElementById(sessionList); container.innerHTML ; data.data.forEach(slot { const btn document.createElement(button); btn.className session-btn; btn.textContent slot.start_time - slot.end_time; btn.dataset.sessionId slot.id; if (slot.status ! 0) { // 已被锁定或占用按钮置灰不可点 btn.classList.add(disabled); btn.disabled true; } else { // 空闲时段点击后记录选中状态 btn.addEventListener(click, function() { const allBtns container.querySelectorAll(.session-btn); allBtns.forEach(b b.classList.remove(active)); this.classList.add(active); selectedSessionId this.dataset.sessionId; document.getElementById(selectedSessionInfo).textContent this.textContent; }); } container.appendChild(btn); }); });逻辑说明前端通过slot.status判断是否可约不可约的按钮加 disabled 属性和灰色样式。selectedSessionId是全局变量提交订单时把它带到后端。这里要强调一句前端禁用只是用户体验层面的优化真正的防超卖判断还是在后端的条件 UPDATE。前端代码可以伪造请求绕过按钮直接调接口所以后端必须再校验一次。参数说明status字段复用后端定义0 空闲、1 已锁定、2 已占用。一个用户同时勾选多个时段时前端可以改成复选逻辑点击时把 sessionId 存进数组但提交前要做一次时间连续性检查比如用户勾了 09:00-09:30 和 10:00-10:30中间空了一小时要提示确认而不是直接提交。4.2 模拟支付与核销流程演示视频要覆盖的三条故事线演示视频是标题里的一个关键卖点。我见过太多人录演示视频只点几下鼠标就说系统完成结果评审问支付成功之后状态怎么变的别人约了之后你怎么看不到当场答不上来。建议视频按三条线来录第一条是正常预约线注册或登录 → 选场地 → 选日期 → 点可选时段 → 提交订单 → 模拟支付 → 订单状态变成已支付 → 管理员在后台核销 → 状态变成已核销。这条线证明了主流程通畅。第二条是冲突线开两个浏览器窗口A 窗口把某时段约走并支付B 窗口刷新后看到该时段变灰或者 B 强行提交后端返回已被抢占的错误提示。这条线证明了并发控制有效而不是只做了一个静态页面。第三条是异常处理线提交订单但不支付等订单状态在后台变成已过期或者调用取消接口把订单取消再回来看时段已经恢复为空闲。这条线证明了状态机真的在跑。录制工具没必要折腾Windows 直接用 Xbox Game Bar 或 OBS分辨率调成 1920x1080浏览器缩放比例调到 100%否则录出来的页面文字会虚。视频时长控制在 5 到 8 分钟核心是状态变化的过程不是反复点按钮。4.3 管理后台看板用数据证明系统跑通了演示视频里最好有一段管理后台的展示不一定非要复杂的图表三块数据就够今日预约数、场地使用率、订单状态分布。这几个数字能直接说明系统不是写死的页面而是有真实数据在流动。-- 今日预约数今天所有已支付和已核销的订单 SELECT COUNT(*) AS today_orders FROM t_order WHERE status IN (1, 2) AND session_date CURDATE(); -- 场地使用率已锁定和已占用时段数 / 当天总时段数 SELECT SUM(CASE WHEN status IN (1, 2) THEN 1 ELSE 0 END) / COUNT(*) * 100 AS usage_rate FROM t_court_session WHERE session_date CURDATE(); -- 订单状态分布 SELECT status, COUNT(*) AS cnt FROM t_order GROUP BY status;这三条 SQL 是管理后台最常见的查询。usage_rate算出来之后可以在页面用简单的进度条展示不用引入图表库。管理后台的实现不需要复杂前端框架服务端渲染一个表格再加几条 CSS 就够。要记住预约管理系统这类项目业务逻辑完整度比页面华丽程度重要得多。5. 部署与常见问题避坑从本地环境到手把手排错源码拿到手最怕的不是代码看不懂而是本地跑不起来。环境问题千奇百怪但九成以上集中在数据库连接、时区、字符集、端口占用这几类。下面把最常见的部署路径和坑一次说清。5.1 两种常见部署路径宝塔面板和 Docker Compose如果你在 Windows 上本地跑常见做法是装一个 PHPStudy 或 XAMPP把源码放到htdocs目录导入 SQL 文件改一下config.php里的数据库连接信息就能打开。如果源码本身是原生 PHP 写的没有 Composer 依赖这套路径最省事。如果要在 Linux 服务器上跑我一般有两个选择。一是宝塔面板安装 Nginx MySQL 5.7 或 8.0 PHP 7.4创建站点把源码传到站点目录导入数据库然后改config.php。源码建站这个说法其实核心动作就是这三步放代码、导数据、改配置。二是 Docker Compose适合想把环境固化下来的情况services: db: image: mysql:8.0 container_name: pingpong_db environment: MYSQL_ROOT_PASSWORD: root123456 MYSQL_DATABASE: pingpong_booking ports: - 3306:3306 volumes: - db_data:/var/lib/mysql command: --default-authentication-pluginmysql_native_password web: image: php:7.4-apache container_name: pingpong_web ports: - 8080:80 volumes: - ./project:/var/www/html - ./php.ini:/usr/local/etc/php/conf.d/custom.ini depends_on: - db volumes: db_data:启动命令就两条docker-compose up -d docker-compose ps参数说明MYSQL_DATABASE是容器启动时自动创建的数据库名MYSQL_ROOT_PASSWORD是 root 密码实际部署时务必改掉弱密码。web容器把当前目录下的project文件夹挂载到 Apache 的网页根目录改代码不需要重新构建镜像。php.ini挂载是为了设置时区和上传限制下面讲的时区坑就靠它解决。宿主机 3306 端口如果被本地 MySQL 占用把映射改成33060:3306代码里的数据库端口也要同步改。5.2 避坑时区错乱导致场次日期偏移现象页面显示的时段和数据库存的时间差了 8 小时预约今天的场地后台列表里却显示成昨天或明天的记录。原因PHP 默认date.timezone没设置MySQL 连接时区是 UTC两个时区叠加在一起日期就被推偏了。这个问题非常隐蔽前后端联调时偶尔出现过一会儿又正常特别像玄学。解决在php.ini里设置date.timezone Asia/ShanghaiPDO 的 DSN 里加上charsetutf8mb4MySQL 建库时指定字符集连接初始化时执行SET time_zone 08:00。这三个地方统一后时间问题基本绝迹。5.3 避坑并发抢约超卖现象两个用户同时提交预约同一个时段的两个订单都显示成功后台一看场地冲突。原因实现代码用的是先 SELECT 判断 status 0再 UPDATE 设置 status 1两个请求同时通过 SELECT 判断都认为自己抢到了。这是典型的竞态条件和小米秒杀、演唱会抢票遇到的问题一样只是预约系统的并发量小平时测不出来。解决把判断和更新合并成一条条件 UPDATE也就是第 3.2 节里的写法。UPDATE 语句中带上status 0条件InnoDB 会锁住命中的行后到的请求要么阻塞等待要么影响行数为 0。再不行就在订单表加一个唯一索引字段组合是(court_id, session_date, start_time)数据库层面做最后兜底。5.4 避坑演示视频中的脏数据影响验收效果现象录演示视频前反复测试下单、取消、支付订单列表里堆了十几条待支付已取消的记录。录出来的视频给老师看第一印象就是系统里数据好乱是不是有 bug。原因没有准备干净的演示数据也没有在录制前重置数据库。数据一乱状态机的展示效果就废了。解决写一个reset_demo.php脚本放在 admin 目录一键执行三条语句清空订单表、把时段表所有status改回 0、把lock_order_id置空。录制视频前先跑一遍脚本再手动造几条有代表性的数据一条已支付未核销、一条已取消、一条已过期刚好覆盖状态机的关键分支。这样视频里既能展示完整流程又不会显得脏乱。5.5 避坑中文乱码与 MySQL 8 认证报错现象页面框架显示正常插入的中文变成问号或者老版本 PHP 连 MySQL 8 时报The server requested authentication method unknown to the client。原因第一个是字符集不统一数据库、表、字段、连接串四处至少有一处不是 utf8mb4第二个是 MySQL 8 默认的caching_sha2_password认证插件和旧版 PHP 的 MySQL 驱动不兼容。解决建库语句显式写DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ciPDO DSN 加charsetutf8mb4如果还是报认证错在 MySQL 里执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码; FLUSH PRIVILEGES;这段 SQL 的意图是让 MySQL 8 用户改用老的密码认证方式兼容旧客户端。如果是新项目更推荐直接升级 PHP 到 8.0 以上一劳永逸。乱码问题的排查顺序是先看库字符集再看表字符集最后看连接串。5.6 避坑用户重复点击取消导致幂等失败现象用户点击取消按钮后没反应又点了一下第二次报错后台查数据发现订单状态已经是已取消但某个时段的lock_order_id被错改成了别的订单。原因取消接口没有检查订单当前状态直接执行UPDATE t_order SET status 3然后把所有lock_order_id ?的时段都释放了。第一次取消成功第二次重复执行时又把已被其他订单锁定的时段释放掉了。这是最危险的一类 bug数据错乱且难以追溯。解决取消接口的 UPDATE 必须带AND status 1只允许已支付订单被取消释放时段时先用lock_order_id确认是自己订单锁定的时段再执行释放。加上这两个条件后重复点击第二次时rowCount()为 0代码直接返回订单状态已变更请勿重复操作。前端再配合按钮防抖双保险。6. 实战技巧让过期未支付订单自动释放的定时任务状态机里最容易被人忽略的是待支付转已过期这一步。很多源码只在支付接口里判断超时但用户如果压根不支付不触发任何接口过期订单就会永远占着时段。解决办法是做一个独立的清理脚本交给系统的定时任务去跑。脚本放在项目根目录例如expire_orders.php核心逻辑是查出所有超过 N 分钟未支付的订单逐个把订单状态改为已过期同时释放对应的时段。N 分钟在配置项里定义一般是 15 分钟?php require config.php; define(ORDER_PAY_TIMEOUT, 15); // 分钟 $expireTime date(Y-m-d H:i:s, time() - ORDER_PAY_TIMEOUT * 60); // 先查出需要过期的订单ID加 LIMIT 防止一次处理太多 $stmt $pdo-prepare( SELECT id FROM t_order WHERE status 0 AND created_at ? LIMIT 100 ); $stmt-execute([$expireTime]); $orderIds $stmt-fetchAll(PDO::FETCH_COLUMN); if (empty($orderIds)) { exit(no expired orders); } $pdo-beginTransaction(); try { foreach ($orderIds as $id) { // 订单状态 0 改 4条件里保留 status 0 保证重复执行不误伤 $pdo-prepare( UPDATE t_order SET status 4 WHERE id ? AND status 0 )-execute([$id]); // 释放该订单锁定的所有时段 $pdo-prepare( UPDATE t_court_session SET status 0, lock_order_id NULL WHERE lock_order_id ? )-execute([$id]); } $pdo-commit(); echo expired . count($orderIds) . orders; } catch (Throwable $e) { $pdo-rollBack(); error_log($e-getMessage()); echo error: . $e-getMessage(); }逻辑说明先查订单再处理而不是直接在一条 SQL 里跨表更新原因是释放时段前必须确认订单确实是超时未支付的状态防止管理员还在处理中的订单被误伤。status 0条件在更新时再校验一次保证重复运行脚本时已经被处理过的订单不会第二次进入释放逻辑。参数说明LIMIT 100是性能保护如果积累了大量过期订单一次只处理 100 条下一轮定时任务继续处理避免长时间占用数据库连接。ORDER_PAY_TIMEOUT放在脚本顶部改成 30 分钟或 60 分钟都行球馆可以根据自己的客流特征调整。Linux 服务器上把它加进 crontab每 5 分钟跑一次*/5 * * * * /usr/bin/php /www/wwwroot/pingpong/expire_orders.php /www/wwwroot/pingpong/expire.log 21宝塔面板的操作更简单后台计划任务里新增一个 Shell 脚本任务周期选每 5 分钟命令填上面这段日志文件会自动生成。跑完之后验证方法也很直接手动把一条刚创建的订单created_at改成 20 分钟前执行一次脚本看订单状态是否变为已过期再看原时段是否恢复为status 0。我当年第一次做这套逻辑时只把过期逻辑写在页面加载里结果管理员不打开页面订单就永远不过期被场地空置率数据打脸后才老老实实改成 crontab。这个坑踩过一次就记住了现在接手任何预约类项目第一件事就是确认清理任务挂在系统定时器上而不是某个页面里。希望这个思路帮你在自己的项目里少走弯路。本文还有配套的精品资源点击获取
返回列表