积分商城插件对接社区发帖接口时,我踩了三个"文档没写但代码里有"的暗坑

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

最近在做一个积分兑换虚拟道具的插件,核心逻辑是用户发帖/回帖赚积分,积分再换发帖特权(比如彩色标题、置顶时长)。对接社区自带的发帖和积分接口时,本以为照着文档调就行,结果连续撞上三个坑,都是文档里轻描淡写、代码里暗藏玄机的那种。

坑一:`wp_insert_post` 的 `post_author` 在积分回调里被静默覆盖

我的插件在 `save_post` 钩子里给用户加积分,同时用 `wp_insert_post` 自动创建一条"积分变动记录"到自定义 post type。诡异的是,记录里的 author 总是变成当前操作者,而不是我传进去的 `post_author` 参数。

排查发现,后台快速编辑或批量操作时,`save_post` 是在一个已经设置了全局 `$current_user` 的上下文里跑的。而我的积分记录创建用了 `wp_insert_post`,它内部会走 `wp_check_post_lock`,然后……就没有然后了,author 被当前会话用户覆盖。

解决不是传 `post_author`,而是先 `wp_set_current_user($target_user_id)`,创建完再 `wp_set_current_user($original_user_id)` 恢复。或者更干净:直接用 `$wpdb->insert` 绕开整套 post 权限机制,反正积分记录不需要 revision、不需要 thumbnail、不需要所有那套 overhead。

// 错误示范:你以为 author 是你传的,其实是当前登录用户
wp_insert_post([
    'post_type'   => 'credit_log',
    'post_author' => $target_user_id,  // 被忽略
    'post_title'  => "积分+{$points}",
]);

// 我的最终方案:直接写表,顺便把元信息塞 JSON 避免 N+1 查询
$wpdb->insert($wpdb->prefix . 'credit_logs', [
    'user_id'     => $target_user_id,
    'action'      => 'post_reward',
    'points'      => $points,
    'object_id'   => $post_id,
    'context'     => wp_json_encode(['ip' => $ip, 'ua' => substr($ua, 0, 200)]),
    'created_at'  => current_time('mysql'),
]);

坑二:回帖积分去重,不能信 `comment_post` 钩子的触发次数

社区模块的回帖接口我接的是 `comment_post` 和 `wp_set_comment_status`。测试时发现,同一条回帖用户被加了两次积分。日志显示 `comment_post` 确实只触发了一次,但我的积分逻辑在 `transition_comment_status` 里也有一份"兜底"——因为有些反垃圾插件会先把评论设成 `hold` 再自动 `approve`。

结果两个钩子都跑了,而且时间差在 50ms 以内,数据库行锁没拦住(MyISAM 表,别问为什么)。

最后我的方案是:积分操作前插一条 `unique_key` 到 Redis(或 transients 当降级),key 格式是 `credit_lock:{$user_id}:{$action}:{$object_id}:{$date_ymd}`。不是用数据库唯一索引,因为同一个用户一天内可以多次回帖拿积分,只是同一篇回帖不能重复。

function maybe_grant_comment_credit($comment_id, $comment_approved) {
    $comment = get_comment($comment_id);
    $lock_key = sprintf(
        'credit_lock:%d:comment:%d:%s',
        $comment->user_id,
        $comment_id,  // 用 comment_id 而不是 post_id,避免"同一帖子多次回帖只算一次"的误杀
        wp_date('Y-m-d')
    );
    
    // 5 秒过期,足够挡住并发双钩
    if (!set_transient($lock_key, 1, 5)) {
        return;  // 已存在,说明另一个钩子正在处理或已处理
    }
    
    // 真正发积分...
}

这里有个细节:`set_transient` 在对象缓存启用时其实是原子操作(Redis SETNX),但用数据库 fallback 时不是。生产环境建议上 Redis,或者至少把 transients 表改成 InnoDB 并加唯一索引。

坑三:积分接口的"异步通知"把发帖回调拖超时了

插件有个功能是"发帖时自动扣除积分购买置顶",我在 `publish_post` 里同步调了积分扣除接口,然后调社区置顶接口。结果用户发帖后白屏 30 秒,Nginx 504。

问题出在积分扣除接口里有个"异步通知"——给用户的积分变动发邮件、推 WebSocket。这本来是积分服务内部的事,但它用了 `wp_remote_post` 调自己的另一个端点,而那个端点又触发了 `shutdown` 钩子里的 `fastcgi_finish_request`……嵌套死锁。

我的解法不是改积分服务(第三方模块,动不了),而是在发帖回调里把"扣积分+置顶"拆成两步:第一步只扣积分并写一条待执行任务到自定义表,第二步用 WP Cron(实际生产用真正的队列,比如 RabbitMQ 或 AWS SQS)异步执行置顶。

// publish_post 钩子只做最轻的事
add_action('publish_post', function($post_id) {
    $needed = get_option('sticky_credit_cost', 100);
    $user_id = get_post_field('post_author', $post_id);
    
    // 同步扣积分,但明确关闭所有 hook 通知
    $result = apply_filters('credit_deduct_silent', null, $user_id, $needed, [
        'post_id' => $post_id,
        'skip_notify' => true,  // 这个参数是我给积分接口提 PR 加的
    ]);
    
    if (is_wp_error($result)) {
        // 积分不够,把帖子打回草稿,给用户发站内信(也是异步)
        wp_update_post(['ID' => $post_id, 'post_status' => 'draft']);
        return;
    }
    
    // 写入异步队列,置顶交给后台进程
    wp_schedule_single_event(time(), 'my_sticky_async_hook', [
        'post_id' => $post_id,
        'priority' => 5,
    ]);
});

三个坑总结下来,对接社区模块的核心经验是:别假设接口是"纯"的。发帖接口里可能有 SEO 插件在写元数据,积分接口里可能有营销系统在发通知,回帖接口里可能有反垃圾在改状态。你的插件只是无数钩子中的一个,要么把自己包得足够紧(原子锁、幂等 key),要么把自己拆得足够松(异步队列、状态机)。

有人做过类似对接吗?特别是 `comment_post` 和 `wp_insert_comment` 的触发顺序,我在不同 WP 版本里看到的行为不一致,想确认下是不是我漏了什么版本差异。

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