积分商城插件对接社区发帖接口时,我踩了三个"文档没写但代码里有"的暗坑
最近在做一个积分兑换虚拟道具的插件,核心逻辑是用户发帖/回帖赚积分,积分再换发帖特权(比如彩色标题、置顶时长)。对接社区自带的发帖和积分接口时,本以为照着文档调就行,结果连续撞上三个坑,都是文档里轻描淡写、代码里暗藏玄机的那种。
坑一:`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 版本里看到的行为不一致,想确认下是不是我漏了什么版本差异。