积分商城插件对接社区发帖/回帖接口:我如何用 `comment_post` 和 `wp_insert_post` 双钩子实现"行为-积分-等级"实时闭环,以及踩过的三个异步陷阱

插件开发 27 浏览 0 回复 返回上级

最近在给一个客户做积分商城插件,核心需求是:用户在论坛发帖、回帖、被点赞都要实时换算成积分,积分累计到一定阈值自动升级,升级后解锁新的发帖权限和商城折扣。听起来简单,实际对接社区模块的接口时,踩了三个大坑,记录一下。

一、为什么不用 `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 做异步结算,避免审核通过时瞬时高峰拖慢评论发布速度。有做过类似方案的欢迎交流。

评论0
回复 · 0
还没有回复
微信客服 微信客服