积分接口与发帖回帖的"时空错位":我是如何用"事件溯源"思路重构社区联动逻辑的

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

之前做社区插件的时候,积分系统和发帖回帖模块各自跑得很顺,一联调就炸。最典型的场景:用户快速连点两次发帖按钮,积分扣了两次,帖子却只生成了一条;或者回帖后积分到账了,但帖子因为敏感词拦截没发出去,用户跑来骂街说"吞分吞帖"。

根本问题是状态不同步。发帖、回帖、积分变动这三个操作散落在不同的钩子里,没有统一的"事务边界"。我最初的写法大概长这样,估计不少人踩过同款坑:

// 典型的"散弹枪"式联动 —— 别学
add_action('wp_insert_post', function($post_id, $post, $update) {
    if (!$update && $post->post_type === 'community_topic') {
        // 这里发积分
        $credits = new Credits_API();
        $credits->add(get_current_user_id(), 10, '发帖奖励');
        
        // 然后做用户等级校验
        do_something_with_level();
        
        // 再写一条动态到首页流
        add_to_activity_stream($post_id);
    }
}, 10, 3);

这代码看着没问题,但每个动作都是"即时生效"的硬编码,没有回滚能力,也扛不住并发。我后来换了个思路:把发帖/回帖本身当作"领域事件",积分、等级、动态流全部变成事件的订阅者,中间加一层轻量的事件总线。

核心结构就三个文件,EventBus.phpPostEvent.phpCreditSubscriber.php。事件总线用 WordPress 的 do_action 做底层,但包了一层"执行上下文",记录事件ID、触发时间、幂等键:

// EventBus.php 核心片段
public function dispatch(DomainEvent $event) {
    $context = [
        'event_id'    => wp_generate_uuid4(),
        'idempotency' => $event->getIdempotencyKey(), // 防重放关键
        'occurred_at' => microtime(true),
        'payload'     => $event->toArray()
    ];
    
    // 先写事件日志表,再广播
    $this->log->append($context);
    
    do_action("community/event/{$event->getName()}", $context);
}

幂等键的生成策略我试了几版,最终定的是 user_id + action_type + date_ymd + content_hash前8位。比如用户ID 42 今天发了一篇标题hash为 a3f7e2d1 的帖子,幂等键就是 42:post:20260910:a3f7e2d1。连点十次,事件日志表里只落一条,积分接口消费时自然去重。

积分订阅者的写法也变样了,不再是"收到钩子立刻改余额",而是先预扣、后确认、失败则回滚

// CreditSubscriber.php
public function onPostCreated($context) {
    $key = $context['idempotency'];
    
    // 第一步:查重
    if ($this->ledger->exists($key)) {
        return; // 已处理,静默跳过
    }
    
    // 第二步:预扣(写入pending状态流水)
    $hold_id = $this->ledger->hold([
        'user_id'   => $context['payload']['author_id'],
        'amount'    => 10,
        'hold_key'  => $key,
        'expires'   => time() + 300 // 5分钟确认窗口
    ]);
    
    // 第三步:异步确认——这里挂到 shutdown 或 cron
    wp_schedule_single_event(time(), 'community/credit/confirm', [
        'hold_id' => $hold_id,
        'post_id' => $context['payload']['post_id']
    ]);
}

确认任务跑的时候,会去反查帖子真实状态。帖子确实发布了,hold 转正式入账;帖子被拦截或删除了,hold 自动释放。这个"反查"动作解耦了积分系统和发帖流程,两边可以独立迭代。

有个细节差点坑死我:wp_schedule_single_event 在并发高的时候会丢事件,因为 WordPress 的 cron 不是真队列。我后来换成了自定义表 + 守护进程消费,但保留 schedule 作为降级兜底。生产环境里,事件日志表成了最好的审计线索,用户投诉"我发帖了没加分"的时候,一查 event_id 链路全清楚。

回帖的联动更复杂一点,因为涉及嵌套层级@通知。我的做法是把 comment_postwp_insert_comment 都映射到统一的 ReplyEvent,但携带不同的 trigger_source。积分规则可以按来源差异化配置,比如"直接回帖+2分,被@回复+5分,楼主标记为最佳答案+20分",全部走订阅者各自判断,不硬编码在回帖逻辑里。

最后贴一张我现在的钩子挂载图,比原来那团 spaghetti 清晰多了:

wp_insert_post ──┬──> EventBus::dispatch(PostEvent)
                 │        ├──> CreditSubscriber(积分)
                 │        ├──> LevelSubscriber(等级)
                 │        ├──> ActivitySubscriber(动态流)
                 │        └──> NotificationSubscriber(@通知)
                 │
wp_insert_comment ──┬──> EventBus::dispatch(ReplyEvent)
                    │        └──> ...同上,不同权重配置

这套东西跑了大半年,最直观的收益是加新联动逻辑不用碰旧代码。上周产品说要加"连续发帖勋章",我新写了一个 StreakSubscriber,注册到总线上,二十分钟搞定,发帖核心文件一行没改。

你们社区模块的联动是怎么做的?还在用"钩子里面直接调API"的写法,还是已经上了类似的事件驱动?如果有更好的幂等方案或者队列选型,欢迎砸过来。

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