插件升级时 `dbDelta` 给我开了个"字符集玩笑":latin1 表里硬塞 utf8mb4 数据,中文全变"�"的复盘

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

上周给老插件做版本迁移,本地测试一切正常,推到 staging 环境后用户反馈所有中文昵称变成了"�"。追查半天发现是 dbDelta 的"静默妥协"在搞鬼——它不会主动修改已有表的字符集,哪怕你 CREATE TABLE 语句里写得明明白白。

先贴我当时的"自信代码":

global $wpdb;
$charset_collate = $wpdb->get_charset_collate();

$sql = "CREATE TABLE {$wpdb->prefix}my_plugin_logs (
    id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
    user_name varchar(100) NOT NULL,
    action text NOT NULL,
    created_at datetime DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (id)
) $charset_collate;";

require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
dbDelta( $sql );

问题出在哪?这插件 v1.0 是 2019 年写的,那时候 WordPress 默认还是 utf8(三字节),表就这么建下来了。现在 v2.0 我要支持 emoji 评论,$wpdb->get_charset_collate() 返回的是 utf8mb4_unicode_520_ci,但 dbDelta 比对时看到表已存在,直接跳过,根本不会去 ALTER 字符集。

更坑的是,dbDelta 对字段长度的处理也很微妙。我把 varchar(100) 扩到 varchar(255),它能识别;但字符集这种"表级属性",它完全视而不见。结果就是新数据按 utf8mb4 编码往里写,老表按 utf8 解析,四字节字符直接截断成 �。

我现在养成的习惯:升级逻辑里拆成"结构变更"和"字符集迁移"两条线。结构交给 dbDelta,字符集必须显式检测、显式 ALTER:

function my_plugin_ensure_charset() {
    global $wpdb;
    $table = $wpdb->prefix . 'my_plugin_logs';
    
    $current = $wpdb->get_var( "SELECT CCSA.character_set_name 
        FROM information_schema.COLUMNS CCSA 
        WHERE CCSA.table_schema = DATABASE() 
        AND CCSA.table_name = '{$table}' 
        LIMIT 1" );
    
    if ( $current !== 'utf8mb4' ) {
        $wpdb->query( "ALTER TABLE {$table} CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci" );
    }
}

还有个隐藏雷:卸载时要不要清表?我现在的做法是——卸载钩子只删"可重建数据",表本身保留但打标记。真遇到用户误触"删除"又后悔的,至少表结构还在,重新激活时走升级流程能自动续上。之前试过卸载时 DROP TABLE IF EXISTS,结果用户重装后历史数据全丢,论坛里骂了我两页。

你们怎么处理"升级时字符集漂移"这类问题的?有没有更优雅的检测方式,不用去查 information_schema

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