研究背景
我们在 个人资料
→ 总结
→ 统计信息
中可以看到我们的阅读时间。
那 Discourse 是如何进行用户阅读时间的统计的?
研究方法
- 控制台看网络请求。
- 阅读 Discourse 源码。
查看网络请求
阅读帖子过程中浏览器会向 /topics/timings
路由发送请求,且无返回。
阅读 Discourse 源码
前端代码
在 185 行中我们发现向后端 /topics/timings
路由发送请求的代码。
await ajax("/topics/timings", {
data,
type: "POST",
headers: {
"X-SILENCE-LOGGER": "true",
"Discourse-Background": "true",
},
});
通过本文件的其他代码发现有许多控制逻辑:
后端请求
- 跟踪阅读时间: 用户在帖子上的停留时间通过
tick()
方法周期性地更新。这个方法计算自上次"tick"以来经过的时间,并将这个时间累加到对应帖子的阅读时间上。 - 记录滚动事件: 当用户滚动页面时,
scrolled()
方法会被触发,更新_lastScrolled
时间戳,这样可以防止有些人挂机过度计算阅读时间。 - 数据合并和发送:
flush()
方法负责将当前会话的阅读时间聚合并准备发送给服务器。consolidateTimings()
方法用于合并当前话题的阅读时间数据。sendNextConsolidatedTiming()
方法通过 AJAX 请求将累积的阅读时间数据发送到服务器。
计时规则
- 暂停计时: 如果用户一段时间内没有滚动页面(由
PAUSE_UNLESS_SCROLLED
定义,为3分钟),tick()
方法将停止累加阅读时间,直到下一次用户滚动。 - 最大跟踪时间: 每个帖子的跟踪时间有上限(由
MAX_TRACKING_TIME
定义,为6分钟)。所以用户每次(重新进入帖子会重新计算)在这个帖子停留时间过长,超过这个时间的部分不会被计入总阅读时间。 - 匿名用户限制: 系统通过
ANON_MAX_TOPIC_IDS
限制了匿名用户可以跟踪的不同话题ID数量(最多5个)。 - 请求失败处理: 如果数据发送失败(基于
ALLOWED_AJAX_FAILURES
定义的允许的失败状态),会根据AJAX_FAILURE_DELAYS
数组定义的延迟策略重新尝试发送。
后端代码
在 946 行中我们发现前端向后端发送的请求通过 PostTiming 类进行处理
def timings
allowed_params = topic_params
topic_id = allowed_params[:topic_id].to_i
topic_time = allowed_params[:topic_time].to_i
timings = allowed_params[:timings].to_h || {}
# ensure we capture current user for the block
user = current_user
hijack do
PostTiming.process_timings(
user,
topic_id,
topic_time,
timings.map { |post_number, t| [post_number.to_i, t.to_i] },
mobile: view_context.mobile_view?,
)
render body: nil
end
end
在这个文件中,我们可以看到如何将阅读时间存储到数据库中的
pretend_read
方法
此方法用于“假装”用户已经阅读了特定帖子。通过执行一条 SQL 插入语句来实现,这条语句只在目标帖子尚未被记录为由特定用户读取时才插入新的 post_timings
记录。
比如当用户直接回复一个他们实际上没有阅读的帖子时,需要将此帖子标记为已读。
record_new_timing
此方法用于记录新的阅读时间。通过执行一条 SQL 插入语句来实现,如果新记录与现有记录冲突(基于 topic_id
、post_number
、user_id
的唯一性约束),则什么都不做。插入成功的话还会更新相关帖子的 reads
计数器和用户的 posts_read_count
(除是私人消息以外)。
record_timing
此方法用于更新已存在的 post_timings
记录,增加 msecs
值。如果指定的记录不存在(即没有为特定用户、话题和帖子编号找到匹配的记录),则调用 record_new_timing
来创建新记录。
destroy_last_for
和 destroy_for
此方法用于删除用户对特定话题或多个话题的阅读时间记录。在删除记录后还会更新相关的 TopicUser
和 UserStat
记录,以保持数据的一致性。
process_timings
此方法用于处理一批阅读时间数据。它首先更新用户的总阅读时间,然后处理每个帖子的阅读时间。这里会限制每个帖子的最大阅读时间(见以下代码),并尝试更新现有的 post_timings
记录。如果记录不存在,则创建新的记录。此方法还用于处理帖子阅读通知的发送和用户最后阅读位置的更新。
max_time_per_post = ((Time.now - current_user.created_at) * 1000.0)
max_time_per_post = MAX_READ_TIME_PER_BATCH if max_time_per_post > MAX_READ_TIME_PER_BATCH
结论
每次循环请求后端 /topics/timings
只能填 60000 以下。