本文目录导读:

在PHP项目中实现资讯阅读时长统计,核心思路是客户端行为追踪 + 服务端日志记录。
因为PHP本身是无状态的(每次请求结束后变量销毁),所以无法像长连接一样实时计算用户读了多久,通常采用事件上报的机制来实现。
下面是行业主流且技术成熟的三种实现方案:
基于“可见性”的定时上报(推荐,最准确)
用户在浏览器中,页面会通过JS定时(如每5-10秒)向服务端发送心跳请求,服务端根据累积心跳计算时长。
实现步骤:
-
前端(JavaScript):
- 使用
Page Visibility API监听页面切换(用户切到其他标签页或最小化窗口时暂停计时)。 - 使用
setInterval每t秒向服务端上报一次当前状态(阅读中/暂停)。 - 在页面关闭或切换时,发送最后一次上报请求(可用
navigator.sendBeacon确保数据送达)。
// 伪代码 let timer = null; let articleId = <?php echo $articleId; ?>; function startTracking() { timer = setInterval(() => { navigator.sendBeacon('/api/log_read_time.php', JSON.stringify({ article_id: articleId, event: 'heartbeat', // 心跳 duration: 10 // 心跳间隔10秒 })); }, 10000); // 每10秒上报一次 } function stopTracking() { clearInterval(timer); // 最后上报一次,确保不丢数据 navigator.sendBeacon('/api/log_read_time.php', JSON.stringify({ article_id: articleId, event: 'leave', duration: 0 })); } // 页面可见性变化时暂停/恢复 document.addEventListener('visibilitychange', () => { if (document.hidden) { stopTracking(); } else { startTracking(); } }); // 页面加载开始计时 startTracking(); // 页面关闭前上报 window.addEventListener('beforeunload', stopTracking); - 使用
-
后端(PHP):
- 接收心跳请求,将用户ID、文章ID、阅读时长(心跳间隔)写入数据库。
- 注意:这里需要判断用户是否“真正在阅读”,而不是挂机,可以结合用户鼠标/键盘事件(前端只在有交互时发送心跳)来过滤。
// api/log_read_time.php <?php session_start(); if (!isset($_SESSION['user_id'])) { exit; } $input = json_decode(file_get_contents('php://input'), true); $articleId = intval($input['article_id']); $event = $input['event']; $duration = intval($input['duration']); // 前端发来的本次阅读时长(秒) // 连接数据库 $pdo = new PDO('mysql:host=localhost;dbname=test', 'root', ''); // 1. 写入阅读日志表(记录每次心跳) // 2. 或者直接累加到文章阅读总时长表 $stmt = $pdo->prepare("INSERT INTO reading_log (user_id, article_id, duration, created_at) VALUES (?, ?, ?, NOW())"); $stmt->execute([$_SESSION['user_id'], $articleId, $duration]); echo 'ok';
优点:数据颗粒度细,准确性高。 缺点:对服务器有一定请求压力(但10秒一次通常可以接受)。
前后端时间戳差值计算(简单粗暴)
前端记录进入页面的时间,离开时上报给后端,但这种方法非常容易被用户挂机刷时长,所以通常需要配合其他信号使用。
步骤:
- 文章页面加载时,前端获取服务器时间(或生成一个唯一标识)。
- 页面关闭时,前端提交“开始时间戳”和“结束时间戳”给后端。
- 后端计算差值。
// 页面加载时生成一个token,记录开始时间 $entryToken = md5(uniqid(rand(), true)); $_SESSION['read_start'][$entryToken] = time(); // 页面关闭时,用户请求 /api/end_read.php?token=xxx $token = $_GET['token']; $startTime = $_SESSION['read_start'][$token]; $duration = time() - $startTime; // 阅读时长秒数 unset($_SESSION['read_start'][$token]); // 清理 // 存入数据库
缺点:如果用户打开页面后不关(甚至挂一天),时长会被严重夸大。不推荐单独使用,可配合方案一使用。
后端日志 + 离线/异步分析(轻量级,适合高并发)
如果不要求实时显示阅读时长(比如只记录用户读了多久用于后台分析),可以只在文章页面埋一个“曝光”日志。
- 结构:
log表中每行记录一条行为。 - 思路:记录用户进入文章页面的时间点,然后在Hive/Spark/ES中做离线聚合。
user_id,article_id,event(enter/scroll/leave),timestamp
- PHP后端只负责写日志,不做计算,后续通过大数据平台或SQL分析。
SELECT user_id, article_id, SUM(timestamp_diff) as total_read_time FROM reading_log GROUP BY user_id, article_id;
优点:对PHP服务器压力最小,吞吐量高。 缺点:无法实时展示给用户看“您已阅读XX分钟”。
数据表设计建议
无论用哪种方案,数据库表结构可以这样设计:
CREATE TABLE user_reading_records (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id INT UNSIGNED NOT NULL COMMENT '用户ID',
article_id INT UNSIGNED NOT NULL COMMENT '资讯ID',
read_duration INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '阅读时长(秒)',
session_id VARCHAR(64) NOT NULL COMMENT '一次阅读会话标识',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录时间',
INDEX idx_user_article (user_id, article_id),
INDEX idx_article (article_id),
INDEX idx_session (session_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
说明:session_id 用于标识用户“从打开到关闭”的一次完整阅读,可以根据这个字段做去重。
总结建议
| 需求场景 | 推荐方案 |
|---|---|
| 需要准确统计(如作为付费或积分依据) | 方案一(可见性+定时心跳) |
| 简单的排行榜/展示,允许少量误差 | 方案三(异步日志) |
| 给用户展示“您已阅读X分钟” | 方案一(实时性) |
| 纯后端分析,不要求实时 | 方案三(性能好) |
避坑指南:
- 防刷:服务端记录心跳时,判断相邻两次心跳时间戳的间隔是否合理(如不超过30秒)。
- 断点续读:如果用户关闭浏览器,之前的心跳已经记录,不需要额外处理。
- 匿名用户:如果站点不强制登录,可以用
cookie + session_id来标记匿名用户。