自己写个 WordPress 插件,让发文自动推送百度收录

搜了一圈「WordPress 自动推送百度」,能用的插件要么几年没更新,要么设置页只有孤零零一个输入框。我想要的不止这些:编辑文章时决定这篇推不推、列表里一眼看到哪篇推成功、有个地方随时换 token。

那就自己写。代码放进 wp-content/mu-plugins/ —— 这个目录下的文件自动加载、后台停不掉,适合放这种基础设施级别的功能。

整件事拆开就是四块,各自挂在独立的钩子上:

自己写的百度推送插件由四块拼成:发布时推送、编辑页开关、列表状态列、后台设置页
自己写的百度推送插件由四块拼成:发布时推送、编辑页开关、列表状态列、后台设置页

写完之后功能全通。我在后台发了一篇测试文,点发布,日志里出现了推送成功的记录,列表页状态列也亮了。收工。

设置页保存了,但读出来还是旧值

问题出在设置页。我改了个 token,点保存,页面刷新回来 —— 显示的居然还是旧值。

第一反应是保存失败了。去数据库翻 wp_options,zz_baidu_push 这条记录明明写着新 token:

mysql> select option_value from wp_options where option_name='zz_baidu_push';
{"token":"新token","site":"www.example.com","enabled":"1"}

写进去了,但读出来是旧的。这就有意思了 —— 写成功、读失败,说明问题在读取的那一层。

回头看我读配置的函数:

function zz_baidu_cfg( $key ) {
    static $cache = null;
    if ( null === $cache ) {
        $cache = get_option( ZZ_BAIDU_OPT, array() );
    }
    // ... 从 $cache 里取值
}

static $cache。

我加它,是想少查几次数据库。当时觉得挺合理 —— 一次请求里配置又不会变。

但一次请求里,配置是会变的。

用户点「保存更改」,WordPress 走 options.php → update_option() 把新值写进数据库 → 然后 wp_redirect() 跳回设置页。正常情况下这是两次请求,第二次请求里 static 重新初始化,读到新值。

可我调试时是在同一个 PHP 进程里连写带读的 —— static $cache 早在第一次读的时候就锁死了旧值,后面 update_option 写得再对,我读到的都是那份缓存。

更麻烦的是:如果那次 302 跳转因为任何原因失败(比如前面已经有输出、headers 发不出去),用户就会留在同一次请求里看页面 —— 看到的就是旧 token,以为没保存成功。

static 变量在同一次请求内锁死旧配置的问题链:写成功、读旧值,以及去掉缓存后的正确行为
static 变量在同一次请求内锁死旧配置的问题链:写成功、读旧值,以及去掉缓存后的正确行为

修法:把这个缓存删掉

function zz_baidu_cfg( $key ) {
    $o = get_option( ZZ_BAIDU_OPT, array() );
    if ( ! is_array( $o ) ) {
        $o = array();
    }
    // ... 直接从 $o 取值
}

删掉 static 就够了。

有人担心性能 —— 每次调用都查一次数据库?其实 get_option() 本身就走 WordPress 的对象缓存,同一次请求里重复调用读的是内存。而且 update_option() 会自动失效这层缓存,它永远不会给你脏数据。

我这个手写的 static 缓存,恰好把这层保护绕过去了。

顺手说个别的坑,跟缓存不是一回事,但同在配置读取里:配置来源最好做成两层回落 —— 数据库 option 优先,文件里的 define() 常量兜底。这样已部署的站点在用户第一次打开设置页之前功能不会中断,填过之后就以 option 为准。

还有两个坑

第一个:不同钩子传的参数类型不一样。

我原本只挂了 publish_post,后来发现从草稿点「发布」根本不触发它 —— 走的是 draft_to_publish。补上之后,日志里蹦出警告:

PHP Warning: Object of class WP_Post could not be converted to int

因为 publish_post 传的是文章 ID(整数),而 draft_to_publish 传的是 WP_Post 对象。同一个回调接两种参数,得在开头做个规范化:

if ( $post_id instanceof WP_Post ) {
    $post_id = (int) $post_id->ID;
}
WordPress 发布一篇文章时两个钩子的触发顺序与参数类型差异
WordPress 发布一篇文章时两个钩子的触发顺序与参数类型差异

第二个:我的「跳过」提示,把真实的成功结果盖掉了。

发布文章时 WordPress 会连着触发两个钩子(draft_to_publish 然后 publish_post)。第一个真发了请求、写了一条「推送成功」;第二个撞上我的 6 小时去重逻辑、写了一条「已推送过」—— 把它盖了。用户看到的提醒成了"好像没推",实际推成功了。

修法:没真正发出请求的分支,不许写通知。一条都不许。

写完怎么验

功能类代码最怕"看着能跑"。我的做法是写个一次性探针脚本,塞进服务器跑,测完删掉:

define('WP_ADMIN', true);
require ABSPATH . 'wp-load.php';
require ABSPATH . 'wp-admin/includes/admin.php';   // 后台函数在这里
wp_set_current_user( $admin_id );
do_action('admin_init');                     // 触发设置字段注册
do_action('add_meta_boxes', 'post', $post);  // 触发 meta box 注册

关键是要主动触发钩子。我一开始只调 do_meta_boxes(),结果显示「meta box 缺失」——其实是我没触发 add_meta_boxes,那些框压根没注册。差点以为自己写错了。

最后跑了 15 项检查全绿。最有价值的一条是「保存后 zz_baidu_cfg() 能读到新 token 吗」—— 就是它把那个 static 缓存揪出来的。

顺便说,这插件里还有几处我至今没深究的地方,比如后台那些 do_action 的触发时机在不同版本里偶尔有差异。我目前的办法就是多测,用真实的发布动作走一遍,而不是只信代码逻辑。

如果你也在折腾百度推送,先看看我配了半年其实一直失败的那篇,讲的是 site 参数不能带协议这个更致命的坑。

发表评论