## 背景与目标
- 目标:构建“热门文章”排行,既能反映近期热度,又能避免历史长尾压制,保证指标可复现与参数可验证。
- 范围:前端埋点(PV/停留时长)、后端统计(UV/权重)、排名计算(时间衰减)、校验流程(回放与对比)。
## 指标定义与采集
- PV(Page View):页面浏览次数,前端每次进入页面计一次。
- UV(Unique Visitor):去重用户数,可通过匿名 `visitorId`(如 `localStorage` + 指纹或登录用户 ID)统计。
- 停留时长(Dwell Time):单次会话在文章页面的停留秒数,采用 `visibilitychange`/`pagehide` 与心跳采样结合。
- 事件埋点:统一事件结构 `HotEvent`,前端以 TypeScript 发送到后端;后端进行聚合。
示例事件结构(前端):
type HotEventType = "pv" | "dwell";
interface HotEventBase {
articleId: string; // 文章唯一标识(可用 slug 或数据库 ID)
visitorId: string; // 匿名或登录用户标识,用于 UV 去重
ts: number; // 事件时间戳(毫秒)
}
interface PvEvent extends HotEventBase { type: "pv" }
interface DwellEvent extends HotEventBase { type: "dwell"; seconds: number }
type HotEvent = PvEvent | DwellEvent;
前端埋点要点:
- `visitorId`:优先使用登录用户 ID;未登录使用稳定指纹+随机种子,持久化于 `localStorage`。
- 停留时长:进入页面开始计时,`visibilitychange` 失焦暂停;离开或 `pagehide` 时上报累计秒数。
- 采样与丢弃:对极端时长(如 > 1 小时)做截断,避免异常影响。
## 排名算法与时间衰减
采用指数衰减模型,保证新近交互获得更高权重,同时长期稳定不被瞬时峰值淹没。
核心公式:
score = Σ_i [ w_pv * PV_i + w_uv * UV_i + w_dt * DT_i ] * decay(t_i)
decay(t) = exp( - ln(2) * Δt / half_life )
- `PV_i`:第 i 个时间片内的 PV 增量(或单事件贡献)
- `UV_i`:第 i 个时间片内的 UV 增量(去重)
- `DT_i`:第 i 个时间片内的停留秒数总和(或均值/中位数)
- `w_pv, w_uv, w_dt`:权重,默认建议 `w_pv=1.0, w_uv=2.0, w_dt=0.02`(每 50 秒约等价 1 PV)
- `half_life`:半衰期,建议默认 `24h`;范围可在 `6h~72h` 基于业务调参
- `Δt`:当前计算时刻与事件发生时刻的时间差(小时)
参数验证说明:
- 指数衰减采用标准公式 `exp(-λt)`,令 `λ=ln(2)/half_life` 保证在半衰期时权重减半;该公式为成熟的时间加权评分常用模型。
- 权重建议通过回放数据网格搜索确定,使不同指标贡献在典型流量下具有可比性;文末给出校验方法。
## 后端聚合与评分实现(TypeScript)
示例实现(Node.js + TypeScript,内存版),用于说明算法与参数,可迁移到数据库聚合:
type TimestampMs = number;
interface WeightedParams {
wPv: number;
wUv: number;
wDt: number;
halfLifeHours: number;
}
interface AggregatedSlice {
ts: TimestampMs; // 时间片起点(可按 5min/15min/1h)
pv: number;
uv: number;
dtSeconds: number;
}
function decayWeight(nowMs: number, eventMs: number, halfLifeHours: number): number {
const deltaHours = (nowMs - eventMs) / 3_600_000;
const lambda = Math.log(2) / halfLifeHours;
return Math.exp(-lambda * deltaHours);
}
export function computeHotScore(slices: AggregatedSlice[], nowMs: number, p: WeightedParams): number {
let score = 0;
for (const s of slices) {
const base = p.wPv * s.pv + p.wUv * s.uv + p.wDt * s.dtSeconds;
score += base * decayWeight(nowMs, s.ts, p.halfLifeHours);
}
return score;
}
// 示例参数(可通过配置或 A/B 实验调优)
export const DefaultParams: WeightedParams = {
wPv: 1.0,
wUv: 2.0,
wDt: 0.02, // 50 秒 ≈ 1 PV
halfLifeHours: 24,
};
实现要点:
- 聚合粒度:优先固定窗口(如 15 分钟),便于离线与实时一致;事件时间戳 `ts` 取窗口起点。
- UV 去重:窗口内按 `visitorId` 去重,跨窗口不去重以保留时间衰减效果。
- 防作弊:对同一 `visitorId` 的高频 PV 限流;极端停留时长截断。
## 校验与可复现
- 回放测试:从日志或事件存储中回放 7 天数据,以不同 `halfLifeHours` 与权重网格计算排名,观察稳定性与对热点的响应速度。
- 指标对齐:选择一组基准文章,验证 PV/UV/DT 单独变化对总分的单调性与比例关系是否符合预期。
- 离线对比:与过去一周的人工精选或业务指标(如转化)对比,确认排名的业务合理性。
- 稳定性测试:对突发峰值(如活动发布)进行压力回放,观察 24~48 小时内的回落曲线与尾部稳定性。
## 前端埋点参考实现(浏览器)
function ensureVisitorId(): string {
const key = "visitor_id";
let id = localStorage.getItem(key);
if (!id) {
id = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
localStorage.setItem(key, id);
}
return id;
}
function sendEvent(ev: HotEvent) {
navigator.sendBeacon("/api/hot/event", JSON.stringify(ev));
}
export function trackArticle(articleId: string) {
const visitorId = ensureVisitorId();
const start = Date.now();
sendEvent({ type: "pv", articleId, visitorId, ts: start });
let active = true;
function onVisible() { active = !document.hidden }
document.addEventListener("visibilitychange", onVisible);
function flush() {
const now = Date.now();
const seconds = Math.min(Math.floor((now - start) / 1000), 3600); // 截断 1h
sendEvent({ type: "dwell", articleId, visitorId, ts: now, seconds });
document.removeEventListener("visibilitychange", onVisible);
}
window.addEventListener("pagehide", flush, { once: true });
}
## 部署与参数调优建议
- 半衰期:从 `24h` 起步;内容更新频繁时可降至 `12h`,长尾内容可升至 `48~72h`。
- 权重:将 `w_uv` 设为 `w_pv` 的 2~3 倍以抑制刷量;`w_dt` 通过历史数据回归确定,使 30~60 秒对排名有温和影响。
- 计算频率:每 5~15 分钟增量计算即可满足实时性与成本平衡。
## 注意事项(专业与真实)
- 指数衰减与半衰期参数为业界常用做法;公式与实现可通过回放数据验证,确保结果可复现。
- 所有示例代码为 TypeScript,可在 Node.js 18+ 或浏览器环境编译执行;`decayWeight` 与评分函数经单元测试可直接校验(建议添加 3~5 个用例,覆盖 0h、halfLife、2*halfLife)。
- 隐私与合规:确保 `visitorId` 不包含个人敏感信息;遵守地区隐私政策与告知用户采集目的。
## 总结
本文给出了 TypeScript 端到端的热门文章实现方案:定义可验证指标、采用指数时间衰减的稳定排名模型、提供前后端示例与校验方法,并附带参数调优建议,确保在生产环境中专业、真实、可复现。

发表评论 取消回复