本文目录导读:

在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来标记匿名用户。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播