PHP开发Telegram Bot:点击callback query后2分钟发消息并执行数据库操作
PHP实现Telegram Bot回调查询触发2分钟延迟任务方案
核心避坑
- 绝对不要在接收到Callback Query的请求流程中直接写
sleep(120)再执行后续逻辑:Telegram对Bot回调请求的超时阈值仅30秒左右,超时会重复推送回调事件,不仅会导致用户端按钮一直处于加载转圈状态,还会重复触发延迟任务,出现重复发消息、重复执行数据库操作的问题。 - 所有计时逻辑必须放在服务端,不要依赖前端/客户端计时。
- 不要在任务存储中直接写入待执行的原生SQL语句,必须做操作映射白名单,避免SQL注入风险。
生产级稳定实现(MySQL延迟表+Crontab定时消费)
这个方案不需要安装额外的重型服务,常规PHP虚拟主机/云服务器都能部署,稳定性极高,运维成本极低。
第一步:即时响应回调,写入延迟任务
用户点击按钮触发回调查询后,第一时间完成两个操作,不要做任何耗时等待:
- 立刻调用Telegram Bot的
answerCallbackQuery接口返回成功响应,关闭按钮加载状态,结束当前HTTP请求,避免Telegram超时重试。 - 校验回调合法性、用户权限后,将待执行的任务信息写入提前建好的延迟任务表,任务计划执行时间设为当前时间戳+120秒。
延迟任务表参考建表语句:
CREATE TABLE `tg_delay_tasks` ( `id` int UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, `chat_id` bigint NOT NULL COMMENT '目标用户Telegram ID', `msg_content` text NOT NULL COMMENT '待发送的指定消息内容', `db_action` varchar(100) NOT NULL COMMENT '待执行数据库操作的白名单标识', `action_params` json NOT NULL COMMENT '数据库操作所需参数', `execute_at` int UNSIGNED NOT NULL COMMENT '计划执行的Unix时间戳', `status` tinyint NOT NULL DEFAULT '0' COMMENT '任务状态:0待执行 1已执行 2执行失败', `error_msg` varchar(255) NOT NULL DEFAULT '' COMMENT '执行失败错误信息', `created_at` int UNSIGNED NOT NULL COMMENT '任务创建时间戳' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
小提示:写入任务前可先查询当前用户是否存在同类型的待执行任务,避免用户短时间重复点击按钮生成重复任务。
第二步:编写定时消费脚本
新建独立的PHP消费脚本(例如命名为delayed_task_worker.php),核心逻辑是扫描到期任务,依次执行发消息、数据库操作,标记任务状态,核心代码参考:
<?php // 引入项目公共配置、数据库连接、Telegram Bot实例初始化文件 require __DIR__ . '/bot_bootstrap.php'; $currentTimestamp = time(); // 查询到期的待执行任务,加写锁避免多进程重复消费 $stmt = $db->prepare("SELECT * FROM tg_delay_tasks WHERE status = 0 AND execute_at <= ? LIMIT 100 FOR UPDATE"); $stmt->execute([$currentTimestamp]); $tasks = $stmt->fetchAll(PDO::FETCH_ASSOC); foreach ($tasks as $task) { try { // 1. 向目标用户发送指定消息 $bot->sendMessage([ 'chat_id' => $task['chat_id'], 'text' => $task['msg_content'] ]); // 2. 执行对应数据库操作,通过白名单映射匹配逻辑,禁止直接执行存储的SQL $actionParams = json_decode($task['action_params'], true); match($task['db_action']) { 'grant_user_vip' => $db->prepare("UPDATE users SET vip_expire = vip_expire + 86400 * 30 WHERE chat_id = ?")->execute([$task['chat_id']]), 'clear_temp_data' => $db->prepare("DELETE FROM user_temp_operation WHERE chat_id = ?")->execute([$task['chat_id']]), // 按照自身业务需求新增其他操作映射即可 default => throw new Exception("未注册的操作类型:{$task['db_action']}") }; // 标记任务执行成功 $db->prepare("UPDATE tg_delay_tasks SET status = 1 WHERE id = ?")->execute([$task['id']]); } catch (Exception $e) { // 标记任务执行失败,记录错误日志 $db->prepare("UPDATE tg_delay_tasks SET status = 2, error_msg = ? WHERE id = ?")->execute([$e->getMessage(), $task['id']]); error_log("Telegram延迟任务[{$task['id']}]执行失败:" . $e->getMessage()); } }
第三步:配置系统定时任务
用Linux系统自带的Crontab定时执行消费脚本,设置每分钟执行1次即可,时间误差不会超过60秒,完全满足2分钟延迟的需求:
* * * * * /usr/bin/php /你的项目实际存放路径/delayed_task_worker.php > /dev/null 2>&1
如果需要更高的时间精度,可以把Crontab执行频率改成每20秒1次,或者用Redis Zset结构替代MySQL表做延迟队列,消费逻辑完全一致,性能更好,适合任务量超过日均1万条的场景。
不推荐的实现方式
fastcgi_finish_request()+sleep:仅支持FPM运行模式,sleep等待的2分钟会持续占用一个FPM工作进程,任务量稍大就会占满进程池导致整个服务502,生产环境禁止使用。- 基于Swoole/ReactPHP写常驻消费进程:如果没有完善的进程守护、异常重启机制,进程意外退出后所有待执行任务都会卡住,运维成本远高于Crontab方案,没有相关运维经验不建议使用。
内容的提问来源于stack exchange,提问作者Seyed reza Asadi
相关产品推荐
相关产品推荐

