积分接口与发帖回帖的"时空错位":我是如何用"事件溯源"思路重构社区联动逻辑的
之前做社区插件的时候,积分系统和发帖回帖模块各自跑得很顺,一联调就炸。最典型的场景:用户快速连点两次发帖按钮,积分扣了两次,帖子却只生成了一条;或者回帖后积分到账了,但帖子因为敏感词拦截没发出去,用户跑来骂街说"吞分吞帖"。
根本问题是状态不同步。发帖、回帖、积分变动这三个操作散落在不同的钩子里,没有统一的"事务边界"。我最初的写法大概长这样,估计不少人踩过同款坑:
// 典型的"散弹枪"式联动 —— 别学
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.php、PostEvent.php、CreditSubscriber.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_post 和 wp_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"的写法,还是已经上了类似的事件驱动?如果有更好的幂等方案或者队列选型,欢迎砸过来。

