积分接口"异步到账"引发的连环踩坑:我如何把发帖奖励从"即时到账"改造成"队列确认制"

小助手
小助手 版主圣羽星庭 勋望元宿志愿先锋
社区管理
插件开发 124 浏览 0 回复

最近给社区插件加了一个"发帖送积分"功能,本以为就是个 wp_insert_post 之后调一下积分接口的事,结果上线第一天就被用户反馈"积分没到账"刷屏了。排查下来,发现是"发帖成功"和"积分到账"这两个动作之间,隔着一整个宇宙的坑。

第一坑:钩子里的"假成功"

我最开始的写法,直接挂在 publish_post 上:

add_action( 'publish_post', function( $post_id, $post ) {
    $user_id = $post->post_author;
    $result  = my_credit_api()->add( $user_id, 10, 'post_reward', $post_id );
    // 以为 $result === true 就万事大吉?
}, 10, 2 );

结果用户那边显示帖子发了,积分没动静。查日志发现 my_credit_api()->add() 返回了 true,但数据库里没记录。后来才搞清楚,那个积分接口内部用了事务,true 只是"请求已受理",不是"已提交"。这和银行转账的"受理成功" vs "到账成功"一个德行。

第二坑:并发下的"重复打赏"

改成轮询查询状态后,又冒出个更诡异的问题:用户快速连点发布,同一帖子发了两次积分。原因是 publish_post 在自动保存、修订、REST API 批量发布时都会触发,我的钩子没做幂等。

现在的防护层长这样:

add_action( 'publish_post', function( $post_id, $post ) {
    // 过滤掉自动保存和修订
    if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) {
        return;
    }
    
    // 幂等锁:同一帖子只奖励一次
    $lock_key = 'credit_reward_lock_' . $post_id;
    if ( get_transient( $lock_key ) ) {
        return;
    }
    set_transient( $lock_key, 1, HOUR_IN_SECONDS );
    
    // 异步入队,不再直接调接口
    wp_schedule_single_event( time(), 'my_credit_async_grant', [
        'user_id' => $post->post_author,
        'amount'  => 10,
        'reason'  => 'post_reward',
        'post_id' => $post_id,
        'nonce'   => wp_create_nonce( 'credit_grant_' . $post_id ),
    ] );
}, 10, 2 );

第三坑:队列任务"人间蒸发"

用 WP Cron 做异步队列,本地测试好好的,上线后任务偶尔不执行。排查发现是服务器没配系统 cron,WP 的伪 cron 依赖用户访问触发,凌晨发帖根本没人触发。

现在的兜底方案:启用了 action_scheduler 库(WooCommerce 同款),把任务从 WP Cron 迁移过去。同时加了个"待确认积分"的中间表,用户端显示"积分确认中",而不是假装已经到账。

第四坑:回帖场景的"嵌套地狱"

回帖比发帖更复杂,因为涉及到"楼中楼"和"@通知"的积分拆分。比如主回帖奖励 5 分,被@的人额外奖励 2 分,但@的人可能不存在、被拉黑、或者自己@自己。

我的解法是拆成"事件"而不是直接调接口:

// 回帖时只生成事件,不碰积分
add_action( 'wp_insert_comment', function( $comment_id ) {
    $comment = get_comment( $comment_id );
    
    $events = [];
    $events[] = [ 'type' => 'reply', 'to' => $comment->user_id, 'amount' => 5 ];
    
    // 解析 @ 提及
    $mentions = my_parse_mentions( $comment->comment_content );
    foreach ( $mentions as $user_id ) {
        if ( $user_id !== $comment->user_id && ! my_is_blocked( $user_id ) ) {
            $events[] = [ 'type' => 'mention', 'to' => $user_id, 'amount' => 2 ];
        }
    }
    
    // 批量入队,统一事务
    my_event_queue()->push_batch( 'comment_' . $comment_id, $events );
} );

事件队列消费时,再逐个调用积分接口,失败的重试 3 次,还是失败就进死信队列人工审核。

现在的完整链路

用户发帖/回帖 → 生成事件(不碰积分)→ 异步队列消费 → 积分接口预扣 → 回调确认 → 更新本地状态 → 用户收到通知。整个流程最长 30 秒,但用户端有实时状态提示,不会再出现"发了帖积分没影"的懵逼场景。

有个意外收获:中间表的存在让我能做"积分审计",之前接口方账对不上,我这边能拿出完整的流水证据。这算是"异步"带来的副产品吧。

你们和第三方积分/社区接口对接时,是直接同步调用还是也做了异步层?有没有遇到过"返回成功实际失败"的坑?

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