必须全链路统一为utf8mb4:mysql服务端(my.cnf配置并重启)、laravel连接层(config/database.php中charset、collation及pdo初始化命令)、表结构与字段级(alter table convert或modify),缺一不可,否则仍会乱码或报错1366。

要让Laravel项目正确显示中文、emoji和生僻字,必须从MySQL服务端、连接层、表结构到字段级全部统一为utf8mb4,漏掉任意一环都会出现???乱码或SQLSTATE[HY000]: General error: 1366报错。
确认MySQL服务端已启用utf8mb4
这一步不完成,后面所有配置都白搭。Laravel连得再对,MySQL底层不认utf8mb4照样丢数据。
打开my.cnf(Linux)或my.ini(Windows),在[mysqld]段落里添加两行:
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
重启MySQL服务后,进命令行执行:SHOW VARIABLES LIKE 'character_set_server';,确认返回值是utf8mb4。如果还是utf8,说明配置没生效或没重启。
修改Laravel连接层字符集
只改.env里的DB_CHARSET无效——Laravel官方代码根本没读这个变量,纯属误导。
打开config/database.php,在mysql配置块里,把charset和collation显式设为utf8mb4:
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
最关键的是加这一行options配置:
'options' => [
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci"
],
没有这行,Laravel 8+在某些PDO版本下仍会走默认utf8连接,emoji存进去直接变???。
重建已有表和字段的字符集
已有表不会自动继承新连接配置,必须手动升级。只改表默认字符集不够,旧字段仍是utf8。
方法一:单表快速转换(适合小表)
在phpMyAdmin或HeidiSQL中执行:ALTER TABLE users CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
方法二:逐字段指定(精准控制,推荐)
对关键字段如title、content执行:ALTER TABLE posts MODIFY title VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
注意:MODIFY会锁表,大表慎用。
方法三:迁移文件中补救(适用于后续维护)
新建迁移:php artisan make:migration update_users_table_charset
在up()里写:DB::statement("ALTER TABLE users CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
新建迁移时强制使用utf8mb4
Laravel 9.x之前Schema::create()默认建utf8表,哪怕你改了配置也不生效——这是长期被忽略的隐性行为。
第一步:在AppServiceProvider::boot()中加全局设置
Schema::defaultStringLength(191); // 避免utf8mb4索引超限
第二步:迁移文件里显式声明字段字符集
$table->string('title')->charset('utf8mb4')->collation('utf8mb4_unicode_ci');
第三步:建表后追加强制转换(双重保险)
DB::statement("ALTER TABLE posts CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
别用DB::connection()->getPdo()->exec("SET NAMES utf8mb4")——它只影响当前连接,对migration命令无效。
验证最终效果
执行php artisan tinker,输入:
DB::select("SELECT @@character_set_client, @@collation_connection, @@character_set_database");
三个值必须全是utf8mb4开头。再插入一条含emoji的数据:DB::insert("INSERT INTO posts (title) VALUES (?)", ["Hello ? 世界"]),查出来还是原样才算成功。











