PHP开发CMS:on_desk字段更新异常及beforeunload替代方案咨询
问题与解决方案优化
需求背景
我正在用PHP开发一款CMS,需要实现:
- 打开文章编辑页面时,将数据库中对应文章的
on_desk字段设为1; - 退出该页面时,将
on_desk字段设为0;
以此避免同一文章被多个用户同时在编辑页面打开。
当前使用基于beforeunload的方案,但存在稳定性问题——比如用户取消退出对话框时,状态重置逻辑依然触发,导致功能失效。现寻求不依赖beforeunload的替代方案。
当前方案代码(供参考)
Javascript|jQuery
function clearDesk(){ $.post('clearOnDesk.php',{ "id":parseInt($('body').attr('data-pageId')), }, function(){ $('body').attr('data-desk',1) }); } function setDesk(){ $.post('setOnDesk.php',{ "id":parseInt($('body').attr('data-pageId')), }, function(){ $('body').attr('data-desk',2) }); } setDesk(); const beforLoadFunc = function beforLoadFunc () { clearDesk(); if(parseInt($('body').attr('data-desk')) == 1){ setTimeout(() => { window.addEventListener('mousemove', (e) => { setDesk(); }); }, 1000); } } window.addEventListener('beforeunload', function onBeforeUnload(e) { setTimeout(beforLoadFunc, 500); const dialogText = 'are you sure?'; e.returnValue = dialogText; return dialogText; });
clearOnDesk.php
if($_POST) { $id = (int)$this->post('id'); if($id && $id != '') { $w['id'] = $id; } else die("die!"); $d['on_desk'] = 1; $this->db->update('post',$d,$w,["i","i"]); // 实际需求是将on_desk设为0,此处代码注释与逻辑不符 }
setOnDesk.php
if($_POST) { $id = (int)$this->post('id'); if($id && $id != '') { $w['id'] = $id; } else die("die!"); $d['on_desk'] = 2; $this->db->update('post',$d,$w,["i","i"]); // 实际需求是将on_desk设为1,此处代码注释与逻辑不符 }
当前方案的核心问题
beforeunload不可靠:用户取消退出时,事件已触发,异步延迟执行的clearDesk()依然会错误修改on_desk状态;- 异步请求易丢失:页面卸载时浏览器可能中断未完成的AJAX请求,导致状态无法正确重置;
- 冗余依赖事件:通过
mousemove重置状态的逻辑既冗余又容易引发重复请求。
替代方案1:心跳检测+页面可见性API
这个方案通过定时心跳维持编辑状态,后端自动清理超时的无效状态,完全不依赖beforeunload。
步骤1:数据库修改
给post表新增last_active字段(datetime类型),用于记录最后一次心跳的时间。
步骤2:前端实现
const pageId = parseInt($('body').data('pageId')); let heartbeatTimer = null; // 初始化编辑状态 function initEditingStatus() { $.post('setEditingStatus.php', { id: pageId }, () => { startHeartbeat(); }); } // 发送心跳,更新最后活跃时间 function sendHeartbeat() { $.post('heartbeat.php', { id: pageId }); } // 启动心跳(每10秒一次) function startHeartbeat() { sendHeartbeat(); heartbeatTimer = setInterval(sendHeartbeat, 10000); } // 停止心跳 function stopHeartbeat() { if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; } } // 监听页面可见性变化:切标签/最小化时停止心跳,恢复可见时重新初始化 document.addEventListener('visibilitychange', () => { if (document.hidden) { stopHeartbeat(); } else { initEditingStatus(); } }); // 页面加载时初始化 $(document).ready(() => { initEditingStatus(); });
步骤3:后端接口实现
setEditingStatus.php(初始化编辑状态)
if ($_POST) { $id = (int)$this->post('id'); if (!$id) die('无效ID'); $where = ['id' => $id]; $data = [ 'on_desk' => 1, 'last_active' => date('Y-m-d H:i:s') ]; $this->db->update('post', $data, $where, ["i", "s"]); }
heartbeat.php(心跳更新)
if ($_POST) { $id = (int)$this->post('id'); if (!$id) die('无效ID'); $where = ['id' => $id]; $data = ['last_active' => date('Y-m-d H:i:s')]; $this->db->update('post', $data, $where, ["i", "s"]); }
步骤4:后端定时清理任务
用Cron或服务器定时任务,每隔30秒清理超时的编辑状态(比如超过20秒无心跳则视为已退出):
checkEditingStatus.php
// 连接数据库(替换为你的数据库连接逻辑) $db = $this->db; // 计算超时时间:当前时间减20秒 $expireTime = date('Y-m-d H:i:s', time() - 20); // 更新超时的on_desk状态 $sql = "UPDATE post SET on_desk = 0 WHERE on_desk = 1 AND last_active < ?"; $stmt = $db->prepare($sql); $stmt->bind_param('s', $expireTime); $stmt->execute(); $stmt->close();
Cron配置示例
# 每30秒执行一次清理脚本 */30 * * * * php /path/to/your/cms/checkEditingStatus.php
步骤5:编辑页前置检查
用户打开编辑页时,先查询数据库:
- 如果
on_desk=1且last_active在20秒内,提示用户“该文章正在被编辑”; - 否则正常进入编辑页,执行初始化逻辑。
替代方案2:WebSocket实时连接
如果你的服务器支持WebSocket(比如用Swoole、Workerman扩展),可以实现更实时的状态同步:
- 页面加载时,建立WebSocket连接,发送文章ID给后端;
- 后端收到连接请求后,设置对应文章的
on_desk=1; - 当WebSocket连接断开(页面关闭、网络中断、标签页切换),后端立即将
on_desk设为0; - 打开编辑页时,直接查询
on_desk状态,实时判断是否被占用。
这个方案无需定时任务,状态更新即时,但需要额外的WebSocket服务支持。
方案对比
| 方案 | 实现难度 | 实时性 | 可靠性 | 服务器要求 |
|---|---|---|---|---|
| 心跳检测+可见性API | 低 | 一般(延迟10-30秒) | 高 | 普通PHP环境+定时任务 |
| WebSocket连接 | 中 | 即时 | 高 | 支持WebSocket的服务器 |
内容的提问来源于stack exchange,提问作者ramin
相关产品推荐
相关产品推荐

