常见错误是直接修改字段值或用触发器导致数据覆盖和事务冲突;正确做法是在beforeWrite事件中用指数衰减公式动态更新hot_score,并同步清理缓存。

ThinkPHP 模型中实现关键词热度衰减的常见错误
直接在 save() 前手动改字段值,或用 setAttr() 覆盖原始值——这会导致旧数据被覆盖、无法回溯、且不支持批量更新。更糟的是,有人把衰减逻辑写进数据库触发器,结果和 ThinkPHP 的事务、缓存机制冲突,查不到最新热度。
用模型事件 + 时间戳字段做衰减计算
核心是利用 beforeWrite 事件,在写入前动态修正热度值。前提是模型必须有 updated_time(或类似时间字段)和 hot_score 字段。
- 衰减公式建议用指数衰减:
new_score = old_score * pow(0.99, days_passed),比线性衰减更符合真实传播规律 - 务必检查
old_score是否为 null 或 0,避免 NaN 或负数 - 不要在
afterRead里算——那是读时计算,不持久化,下次查还是旧值 - 示例代码片段(放在模型类中):
protected static function init() { self::event('beforeWrite', function ($model) { if ($model->isUpdate() && $model->hot_score > 0) { $days = (time() - strtotime($model->updated_time)) / 86400; $model->hot_score = max(0.1, $model->hot_score * pow(0.99, $days)); } }); }
批量更新时热度衰减失效怎么办
Db::name('keywords')->where(...)->update([...]) 这种写法完全绕过模型事件,beforeWrite 根本不触发。
- 必须用模型方式批量操作:
KeywordModel::where(...)->save(['title' => 'xxx']),才能走事件流 - 如果真要原生 Db 批量更新,得自己算衰减值,用子查询或先查后更:例如
Db::execute("UPDATE keywords SET hot_score = GREATEST(0.1, hot_score * POW(0.99, (UNIX_TIMESTAMP() - UNIX_TIMESTAMP(updated_time)) / 86400)) WHERE ...") - 注意 MySQL 的
POW在低版本可能返回 null,建议加IFNULL包裹
缓存与热度衰减的冲突点
缓存没失效,用户看到的永远是旧热度值,哪怕数据库已更新。
立即学习“PHP免费学习笔记(深入)”;
- 在
beforeWrite里顺手删对应缓存:Cache::delete('keyword_hot_' . $model->id) - 别依赖「缓存自动过期」——衰减是连续过程,5 分钟过期根本不够细粒度
- 如果用了 Redis 的有序集合(zset)存热度,记得同步更新 score:
$redis->zAdd('hot_keywords', $new_score, $id)
衰减不是越快越好,0.99 是日衰减率,换成 0.9 就变成小时级崩盘;另外,所有时间计算必须统一用 UTC 时间戳,否则跨时区部署时衰减会错乱。



















