You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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,此处代码注释与逻辑不符
}

当前方案的核心问题

  1. beforeunload不可靠:用户取消退出时,事件已触发,异步延迟执行的clearDesk()依然会错误修改on_desk状态;
  2. 异步请求易丢失:页面卸载时浏览器可能中断未完成的AJAX请求,导致状态无法正确重置;
  3. 冗余依赖事件:通过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扩展),可以实现更实时的状态同步:

  1. 页面加载时,建立WebSocket连接,发送文章ID给后端;
  2. 后端收到连接请求后,设置对应文章的on_desk=1;
  3. 当WebSocket连接断开(页面关闭、网络中断、标签页切换),后端立即将on_desk设为0;
  4. 打开编辑页时,直接查询on_desk状态,实时判断是否被占用。

这个方案无需定时任务,状态更新即时,但需要额外的WebSocket服务支持。


方案对比

方案实现难度实时性可靠性服务器要求
心跳检测+可见性API低一般(延迟10-30秒)高普通PHP环境+定时任务
WebSocket连接中即时高支持WebSocket的服务器

内容的提问来源于stack exchange,提问作者ramin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 05:16:24