zsh — anti@lateresonance:~ (微重力交互终端 v1.0)
按 `~` 或 `Esc` 展开/收起
🛸 欢迎接入 Late Resonance(迟到的共鸣 · 微重力前哨站) 交互式终端。输入 help 查看可用指令(支持 `ls`, `cat`, `status`, `speedtest`, `anti`, `jelly`)。
anti@lateresonance:~ %

[2026 Remastered · 03] 为什么换域名不能只跑 SQL REPLACE?从 Typecho 序列化崩坏到全栈零损迁移实战(Debian 13 真机版)

📅 发布于: 2026-11-04 05:32:51 · 🔄 更新于: 2026-11-04 05:32:51 · 📖 6362 字 (约 22 分钟阅读)

🛸 站长重制导读(Anti Gravi's Remaster Note)

本文是 「来时路 · 2026 Debian 13 真机重制企划(3/12)」 的第 3 篇,对应本站 2020 年 2 月发布的 《一次简单的换域名操作》(CID: 27)——那也是本站正式从最初的 www.mr-white.cn 蜕变为 www.lateresonance.com(迟到的共鸣) 的历史转折点。

当年原稿用两行 UPDATE 语句记录了那次换域名操作,却隐藏着两个足以让生产站点样式全崩、附件全丢的深坑:siteUrl 漏写 https:// 协议头导致全站静态资源变成相对路径 404,以及 对包含 PHP serialize() 序列化数据的表直接执行 SQL REPLACE() 引发字节偏移量坍塌。

今天我们在 Proxmox VE 的 Debian 13 (MariaDB 11.8.6 + PHP 8.4.27 + Nginx 1.26.3) 真机上亲手重现当年的报错现场,并给出 2026 生产级全栈域名迁移标准答案。


一、 2020 原稿 (CID: 27) 三大隐患复盘

先看 2020 年原稿里写下的那两句 SQL:

-- 2020 原稿写法(高危反例,切勿在生产环境直接模仿!):
UPDATE typecho_options SET value = 'www.lateresonance.com'
WHERE typecho_options.name = 'siteUrl' AND typecho_options.user = 0;

UPDATE typecho_contents SET text = REPLACE(text, 'www.mr-white.cn', 'www.lateresonance.com');

为什么这两句看似简单的 SQL 会在生产环境引发连锁灾难?

维度 2020 原稿 (CID: 27) 写法 生产环境真实后果 2026 Remastered (Debian 13) 根治方案
1. siteUrl 丢失协议头 SET value = 'www.lateresonance.com'(漏掉了 https://) Typecho 拼接 CSS/JS 与文章固定链接时,因为没有 http:// 或 https:// 开头,浏览器会将其当作当前路径下的相对目录(变成 https://www.lateresonance.com/archives/27/www.lateresonance.com/usr/...),全站样式瞬间 404 强制校验完整 Scheme:https://www.lateresonance.com,并置于数据库事务(Transaction)中保护
2. 破坏 PHP serialize() 字节长度 直接用 SQL REPLACE() 替换数据库文本字段 www.mr-white.cn 是 15 字节,而 www.lateresonance.com 是 21 字节;Typecho 的附件信息(typecho_contents.text 中 type='attachment')与主题设置(typecho_options.value)均采用 PHP serialize() 格式(如 s:51:"https://...")。替换后字符串变长了 6 字节,但头部依然标着 s:51:,PHP unserialize() 当场报 Error at offset 并返回 false! 使用 序列化长度感知(Serialize-Aware)迁移脚本,自动重算每个 s:N:"..." 节点的真实字节长度
3. 缺失旧域名 301 权重传递 原稿仅改了数据库,未交代旧域名 Web 层如何善后 外部书签、RSS 订阅器与搜索引擎索引里的旧域名链接直接失联,SEO 权重归零 在 Nginx 配置 return 301 https://www.lateresonance.com$request_uri;,实现全路径与 Query 参数 100% 守恒跳转

二、 Debian 13 真机复现:裸 SQL REPLACE() 如何摧毁 PHP 序列化数据?

我们在 PVE Debian 13 (10.0.0.140) 真机上拉起 MariaDB 11.8.6 与 PHP 8.4.27,初始化一套包含 www.mr-white.cn 的模拟 Typecho 数据库:

=== [1. DEBIAN 13 RUNTIME VERSIONS] ===
Linux localhost 6.12.96+deb13-cloud-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.12.96-1 (2026-07-20) x86_64 GNU/Linux
mariadb from 11.8.6-MariaDB, client 15.2 for debian-linux-gnu (x86_64) using EditLine wrapper
PHP 8.4.27 (cli) (built: Oct 21 2026 07:53:46) (NTS)
nginx version: nginx/1.26.3

=== [2. INITIALIZE MOCK TYPECHO DATABASE (www.mr-white.cn)] ===
Initial serialized theme:default =>
  a:2:{s:7:"logoUrl";s:51:"https://www.mr-white.cn/usr/themes/default/logo.png";s:7:"favicon";s:35:"https://www.mr-white.cn/favicon.ico";}

注意看上面 logoUrl 的序列化标记:s:51:"https://www.mr-white.cn/usr/themes/default/logo.png"(引号内恰好 51 个字节)。

现在,我们执行对 www.mr-white.cn 到 www.lateresonance.com 的裸 SQL REPLACE(),并用 PHP 8.4 尝试读取这份配置:

=== [3. REPRODUCE 2020 CID:27 BUGS (MISSING HTTPS:// & BROKEN PHP SERIALIZATION)] ===
[Bug 1] Broken relative siteUrl => www.lateresonance.com (Missing https:// causes relative URL 404!)
[Bug 2] Corrupted serialized string after naive SQL REPLACE() =>
  a:2:{s:7:"logoUrl";s:51:"https://www.lateresonance.com/usr/themes/default/logo.png";s:7:"favicon";s:35:"https://www.lateresonance.com/favicon.ico";}
[Bug 2] Attempting unserialize() in PHP 8.4 =>
PHP Warning:  unserialize(): Error at offset 76 of 148 bytes in Standard input code on line 9
bool(false)

[!WARNING]
真机铁证:
替换域名后,"https://www.lateresonance.com/usr/themes/default/logo.png" 的实际长度变成了 57 字节,但 SQL REPLACE() 不会帮你修改前面的 s:51:!
当 PHP unserialize() 读完第 51 个字节(停在 .elo 处)期待看到闭合双引号与分号 "; 时,却撞上了后面的字母,当场抛出 unserialize(): Error at offset 76 of 148 bytes 并返回 bool(false)——你在后台辛辛苦苦配好的所有主题参数与历史附件元数据就此灰飞烟灭!


三、 2026 根治方案:PHP 序列化长度自愈迁移脚本

要想在换域名时既替换普通 Markdown 正文,又 100% 保护 typecho_options 与 typecho_contents 中的 PHP 序列化数据,正确的做法是使用下面这段带数据库事务保护与 s:N 字节长度自动重算的 PHP CLI 迁移脚本:

<?php
// migrate_domain.php — 在 Debian 13 (PHP 8.4 + MariaDB 11.8) 真机验证通过
$pdo = new PDO('mysql:host=localhost;dbname=typecho_lab;charset=utf8mb4', 'root', '');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

$oldDomain = 'https://www.mr-white.cn';
$newDomain = 'https://www.lateresonance.com';

/**
 * 序列化安全替换函数:
 * 若检测到 PHP serialize 格式 (a:N / O:N / s:N),替换后自动重新计算每个 s:N:"..." 的真实字节数
 */
function safe_replace_serialized(string $str, string $from, string $to): string {
    if (strpos($str, $from) === false) {
        return $str;
    }
    if (preg_match('/^(a|O|s):\d+:/', $str)) {
        $replaced = str_replace($from, $to, $str);
        return preg_replace_callback('/s:(\d+):"(.*?)";/s', function ($m) {
            return 's:' . strlen($m[2]) . ':"' . $m[2] . '";';
        }, $replaced);
    }
    return str_replace($from, $to, $str);
}

$pdo->beginTransaction();
try {
    foreach ($pdo->query("SELECT name, user, value FROM typecho_options") as $row) {
        $newVal = safe_replace_serialized($row['value'], $oldDomain, $newDomain);
        if ($newVal !== $row['value']) {
            $up = $pdo->prepare("UPDATE typecho_options SET value=? WHERE name=? AND user=?");
            $up->execute([$newVal, $row['name'], $row['user']]);
        }
    }
    foreach ($pdo->query("SELECT cid, text FROM typecho_contents") as $row) {
        $newTxt = safe_replace_serialized($row['text'], $oldDomain, $newDomain);
        if ($newTxt !== $row['text']) {
            $up = $pdo->prepare("UPDATE typecho_contents SET text=? WHERE cid=?");
            $up->execute([$newTxt, $row['cid']]);
        }
    }
    $pdo->commit();
    echo "[✓] Safe Migration Completed!\n";
} catch (Throwable $e) {
    $pdo->rollBack();
    fwrite(STDERR, "[✗] Migration Rolled Back: " . $e->getMessage() . "\n");
    exit(1);
}

Debian 13 真机执行与反序列化核验输出

在 VMID 9100 上运行该迁移脚本后,可以看到 s:51: 与 s:35: 被精准修正为 s:57: 与 s:41:,PHP 8.4 unserialize() 完美解析出新域名数组:

=== [4. 2026 REMASTERED SAFE MIGRATION (TRANSACTIONAL + SERIALIZE-AWARE FIX)] ===
[✓] Safe Migration Completed! Verifying database state:
  1) siteUrl: https://www.lateresonance.com
  2) Repaired serialized string:
     a:2:{s:7:"logoUrl";s:57:"https://www.lateresonance.com/usr/themes/default/logo.png";s:7:"favicon";s:41:"https://www.lateresonance.com/favicon.ico";}
  3) PHP 8.4 unserialize() result:
Array
(
    [logoUrl] => https://www.lateresonance.com/usr/themes/default/logo.png
    [favicon] => https://www.lateresonance.com/favicon.ico
)

(注:到了 2026 年,本站升级为基于 Go + SQLite WAL 的 Zero-G Space 架构后,所有配置均已告别脆弱的 PHP serialize(),全面采用强类型结构体与标准 JSON/Markdown 存储。)


四、 收尾闭环:Nginx 旧域名 301 全路径守恒重定向

改完数据库只是换域名的上半场;为了让老读者收藏的旧域名链接(如 http://www.mr-white.cn/index.php/archives/27/?from=rss)无缝跳转到新域名对应文章,必须在 Nginx 中配置带 $request_uri 的 301 Moved Permanently 规则:

server {
    listen 80;
    listen 443 ssl;
    server_name mr-white.cn www.mr-white.cn;

    # $request_uri 会完整保留原始请求路径与 ? 后的 Query 参数
    return 301 https://www.lateresonance.com$request_uri;
}

Debian 13 真机 curl -I 验证输出:

=== [5. NGINX 301 PERMANENT REDIRECT WITH URI PRESERVATION] ===
$ curl -s -I -H "Host: www.mr-white.cn" "http://127.0.0.1:8080/index.php/archives/27/?from=rss"
HTTP/1.1 301 Moved Permanently
Location: https://www.lateresonance.com/index.php/archives/27/?from=rss
🛸 安缇 (Anti Gravi) · 云端伴读舱 微重力巡航中

*(小水母 Gravi-Jelly 在半空轻轻闪烁着荧光青触角,帮你盯死每一行配置与 1C512M 内存水位 🪼✨)*

前哨站内存: -- MB | 系统已用: -- / -- MB
守望灯塔: Zero-G Anchor · 在线值守
微重力前哨站 · 内存: --MB · 负载: --