积分流转"断链"现场:发帖回帖的 hook 触发顺序让我丢了 30% 的积分记录
最近给社区做积分系统对接,踩了个特别隐蔽的坑——发帖和回帖的积分增减看起来都走通了,但用户实际到账率只有七成左右。排查一圈发现是 hook 执行顺序和异步任务撞车,积分记录表出现了大量 "幽灵事务"。
场景还原
社区用的是自定义 post type 做帖子,评论走标准 wp_comments。积分规则简单:发帖 +10,回帖 +5,被回复 +2。我分别在 wp_insert_post 和 wp_insert_comment 挂了积分逻辑,大致这样:
add_action( 'wp_insert_post', function( $post_id, $post, $update ) {
if ( $update || $post->post_type !== 'forum_topic' ) return;
// 给用户加 10 积分
$user_id = $post->post_author;
add_user_points( $user_id, 10, 'publish_topic', $post_id );
}, 10, 3 );
回帖类似,挂在 comment_post 上。本地测试完美通过,上线后却频繁收到用户投诉 "发了帖积分没涨"。
断链根因:hook 的"伪同步"陷阱
问题出在两个层面。第一是 wp_insert_post 在文章保存过程中可能被多次触发——自动草稿、修订版本、最终发布,每次都会进这个钩子。我加了 $update 判断过滤了修订,但没拦住某些 SEO 插件在 save_post 里二次调用 wp_update_post,导致同一篇帖子触发多次积分发放。
更坑的是第二个:评论场景里,用户快速连续回帖时,comment_post 和后续的 wp_set_comment_status(如果开了审核)会形成竞态。我在 comment_post 里直接写积分,但另一个 hook 在改评论状态,两个请求同时打过来,MyISAM 引擎下的积分表锁表,后到的请求直接丢了。
我的修补方案:事务化 + 队列化
第一步,把即时积分改成"预扣+确认"模式。hook 里只写一条 pending 状态的流水,不直接改用户总积分:
add_action( 'wp_insert_post', function( $post_id, $post, $update ) {
if ( $update || $post->post_status !== 'publish' ) return;
Points_Log::create_pending([
'user_id' => $post->post_author,
'points' => 10,
'event' => 'publish_topic',
'ref_id' => $post_id,
'ref_type' => 'post',
'nonce' => wp_hash( $post_id . $post->post_author . $post->post_date_gmt )
]);
}, 10, 3 );
注意这里用 post_status === 'publish' 替代 !$update,彻底避开草稿/修订/二次更新的干扰。nonce 字段用来幂等去重,同一篇帖子不管触发几次 hook,数据库唯一索引会拦住重复流水。
第二步,用 WP Cron 做异步确认。每分钟扫一次 pending 超过 30 秒的流水,校验关联内容确实存在且状态正常,再真正把积分加到用户表:
function points_commit_pending() {
global $wpdb;
$pendings = $wpdb->get_results( "
SELECT * FROM {$wpdb->prefix}points_log
WHERE status = 'pending'
AND created_at < DATE_SUB(NOW(), INTERVAL 30 SECOND)
LIMIT 100
" );
foreach ( $pendings as $row ) {
// 校验原文还在
if ( $row->ref_type === 'post' && ! get_post( $row->ref_id ) ) {
$wpdb->update( /* 标记为 invalid */ );
continue;
}
// 幂等校验
if ( Points_Log::is_committed( $row->id ) ) continue;
// 真正到账
$wpdb->query( 'START TRANSACTION' );
User_Points::increment( $row->user_id, $row->points );
Points_Log::commit( $row->id );
$wpdb->query( 'COMMIT' );
}
}
add_action( 'points_cron_commit', 'points_commit_pending' );
这里特意用了显式事务,因为用户总积分和流水状态必须原子更新。之前直接 hook 里写两条 SQL,中间崩掉就会出现"流水有了积分没到"或者反过来。
接口层的兜底设计
前端个人中心要展示实时积分,如果走异步确认,用户刚发帖会看到积分没变化,体验很差。我的折中办法是接口读"预估值":用户表总积分 + 该用户 pending 流水总和,但标记一个 syncing: true 状态提示"积分结算中"。
// REST 接口片段
$confirmed = get_user_meta( $user_id, 'points_total', true );
$pending = Points_Log::sum_pending( $user_id );
return [
'points' => (int) $confirmed + (int) $pending,
'confirmed' => (int) $confirmed,
'syncing' => $pending > 0,
'next_sync' => wp_next_scheduled( 'points_cron_commit' )
];
还没解决的边角
评论被删除或帖子被移入回收站时,积分要不要回滚?目前我的策略是:pending 状态的直接作废,已确认的走负向流水,但不在用户感知层扣成负数(最低到 0)。这个规则产品和运营吵了两轮才定下来,代码里用 points_rollback_policy 过滤器扔出去,方便不同社区按需覆盖。
另外 wp_insert_comment 和 comment_post 的区别也坑了我一下——前者在评论数据入库前触发,拿不到 comment_ID;后者在之后,但某些反垃圾插件会在中间拦截。最终我选了 comment_post 并且加了个 comment_approved !== 'spam' 的前置判断。
有同样在做社区积分对接的老哥吗?你们是怎么处理 "发帖后秒删" 这种薅积分场景的?我现在的 30 秒延迟确认能拦住大部分,但总感觉还有更优雅的解法。

