积分商城插件对接社区发帖/回帖接口:我如何用 `comment_post` 和 `wp_insert_post` 双钩子实现"行为-积分-等级"实时闭环,以及踩过的三个异步陷阱
最近在给一个客户做积分商城插件,核心需求是:用户在论坛发帖、回帖、被点赞都要实时换算成积分,积分累计到一定阈值自动升级,升级后解锁新的发帖权限和商城折扣。听起来简单,实际对接社区模块的接口时,踩了三个大坑,记录一下。
一、为什么不用 `save_post` 而用 `wp_insert_post`
一开始我图省事挂了 `save_post`,结果发现每次自动保存草稿、修订版本都会触发积分增加。用户发了篇长文,自动保存了 7 次,积分直接加了 7 倍。后来换到 `wp_insert_post` 的 `publish` 状态判断:
add_action( 'wp_insert_post', function( $post_id, $post, $update ) {
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
if ( wp_is_post_revision( $post_id ) ) {
return;
}
if ( $post->post_status !== 'publish' || $post->post_type !== 'topic' ) {
return;
}
// 区分新建 vs 更新:$update 为 false 才是首次发布
if ( $update ) {
return;
}
$author_id = $post->post_author;
$points = apply_filters( 'myplugin_topic_points', 10, $post );
// 调用积分核心服务
MyPlugin_Points::credit( $author_id, $points, 'publish_topic', $post_id );
}, 10, 3 );
关键点:`$update` 参数很多人误用,以为 `true` 是"有更新",其实 `false` 才代表"新建"。这个参数名确实反直觉。
二、`comment_post` 的时序地狱:评论ID还没写入元数据表
回帖积分更麻烦。`comment_post` 触发时,评论刚写入 `wp_comments`,但自定义字段(比如标记是否为"精华回复")可能还没通过 `add_comment_meta` 落地。我最初在回调里直接查 `get_comment_meta`,返回全是空。
解决方案:把"精华判定"逻辑延后到 `wp_set_comment_status` 或者更晚的 `comment_post` 末尾,用 `wp_schedule_single_event` 做 5 秒延迟处理。但延迟任务在并发高时会堆积,最后改成在 `comment_post` 里只记原始行为,另挂 `added_comment_meta` 做二次修正:
add_action( 'comment_post', function( $comment_id, $comment_approved, $commentdata ) {
if ( $comment_approved !== 1 ) {
// 待审核评论先记流水,不结算
MyPlugin_Points::hold( $comment_id, 'pending_reply' );
return;
}
MyPlugin_Points::credit(
$commentdata['user_id'],
apply_filters( 'myplugin_reply_points', 2, $commentdata ),
'publish_reply',
$comment_id
);
}, 10, 3 );
// 精华回复追加积分
add_action( 'added_comment_meta', function( $mid, $object_id, $meta_key, $_meta_value ) {
if ( $meta_key !== 'is_elite_reply' || ! $_meta_value ) {
return;
}
$comment = get_comment( $object_id );
if ( ! $comment || $comment->comment_approved !== '1' ) {
return;
}
// 避免重复奖励:查流水是否存在 elite_bonus 记录
if ( MyPlugin_Points::has_log( $object_id, 'elite_bonus' ) ) {
return;
}
MyPlugin_Points::credit(
$comment->user_id,
apply_filters( 'myplugin_elite_bonus', 15, $comment ),
'elite_bonus',
$object_id
);
}, 10, 4 );
三、积分变动触发的等级升级,不能反过来改发帖权限的"死循环"
最隐蔽的 bug:用户积分达到 1000,等级升到 Lv.3,Lv.3 解锁了"发帖免审核"权限。我在 `myplugin_points_credited` 自定义钩子里调了 `update_user_meta( $user_id, 'can_publish_direct', true )`,结果这个 `update_user_meta` 触发了 `updated_user_meta`,另一个模块监听这个钩子做了日志记录,日志记录又调了积分接口做"系统奖励"……
死循环直到内存溢出。修复是在积分核心层加"事务深度"计数:
class MyPlugin_Points {
private static $nesting_level = 0;
private static $max_nesting = 3;
public static function credit( $user_id, $points, $type, $object_id = 0 ) {
if ( self::$nesting_level >= self::$max_nesting ) {
error_log( "Points nesting limit reached: {$type}" );
return false;
}
self::$nesting_level++;
// ... 实际积分计算、写库 ...
do_action( 'myplugin_points_credited', $user_id, $points, $type, $object_id );
self::$nesting_level--;
return true;
}
}
超过 3 层嵌套直接截断,同时打日志方便排查哪个环节在乱联动。
四、接口化之后的外部调用
积分模块拆成独立类后,社区之外的其他插件(比如签到、投票)也要用。我暴露了两个安全入口:REST 端点给前端 AJAX,内部函数给同进程调用。REST 端点必须校验 `current_user_can` + `nonce`,但内部函数不做重复校验——信任调用方已经验过。这个边界画清楚,避免权限检查到处重复或者到处漏掉。
目前跑了一周,日均 2000+ 发帖回帖,积分流水 8000+ 条,没再出现重复计分或者死循环。下一步想把 `hold` 的待审核积分改成用 Action Scheduler 做异步结算,避免审核通过时瞬时高峰拖慢评论发布速度。有做过类似方案的欢迎交流。