⭐ 推荐:社区规则条款 V1.0

迁移CMS时如何保留原站的文章修订版本?

GJ论坛站长
GJ论坛站长 管理员资深会员 👑黑卡会员
发布于 2026-10-28 15:56 ·12 浏览 ·3 回复

**能保留,但只能靠迁移数据库表实现,不能指望 CMS 自带的导出/导入功能。** 几乎所有主流 CMS 的「修订版本」都是一条独立的数据行,且通过父级 ID 挂在原文章下;导出成 XML/JSON 时,这些行 100% 会被丢弃。想让修订跟着文章一起走,就必须把修订行本身当作数据迁移,并在目标站重建「修订 → 文章」的父子关系。

修订版本到底存在哪张表里?

结论:先确认原站修订的存储结构,这决定后面能不能直接搬运。WordPress 最典型,修订就是 wp_posts 表里 post_type = 'revision' 的记录,post_parent 指向原文章 ID,post_content 存的是那一刻的全文快照,post_name 形如 1234-revision-v1。Drupal 用 node_revision 及 {node类型}_revision 独立表;Typecho 在 contents 表用 type 字段区分正文与修订;部分国产 CMS(如早期 Discuz)根本没有原生修订机制,只能靠第三方插件生成的备份表。

动手前先跑一条统计,看清体量:

SELECT post_parent, COUNT(*) AS rev_count,
       MAX(post_modified) AS last_rev
FROM wp_posts WHERE post_type='revision'
GROUP BY post_parent ORDER BY rev_count DESC LIMIT 20;

如果单篇修订超过 50 条、总量上百万行,就要考虑先裁剪(只保留最近 N 条或每天最后一条),否则整表导入会拖到几小时。

同构迁移和异构迁移,做法差在哪?

结论:同构(WordPress→WordPress)直接整表搬运、保留原 ID 最省事;异构(换 CMS)必须先生成 ID 映射表,再按映射重写 post_parent。

同构路径的操作顺序是:

  1. wp db export full.sql 导出全库;
  2. 目标站 wp db import full.sql;
  3. 域名替换用 WP-CLI 的序列化安全命令:wp search-replace 'old.com' 'new.com' --all-tables --precise --recurse-objects,普通 SQL REPLACE 会破坏序列化字段长度;
  4. 迁移前后各执行一次 wp post list --post_type=revision --format=count 对比条数。

异构路径必须先解决 ID 错位:新站自增 ID 与旧站完全不同,直接 INSERT 会让 post_parent 指向不存在的文章或别人的文章。做法是导出旧站 (ID, post_name, post_date) 建临时映射表,用标题+发布时间或旧站的唯一 slug 匹配出新 ID,再批量 UPDATE 修订行的父级字段。

目标站不支持修订怎么办?

结论:降级保存为「存档文章 + 差异记录」,别硬塞。

可行做法有三种:把每篇的修订导出成独立静态文件,按 文章ID/revision-时间戳.html 归档,正文里放下载链接;或者在目标站建一个隐藏版块/独立表,把历史版本作为不可评论的存档内容写入;或者只保留正文的 diff 增量(用 diff -u 或 Python 的 difflib 生成 unified diff),单条体积能压到原文的 5%~15%。注意:一旦目标 CMS 没有修订机制,后续编辑不会再产生新版本,这条链路等于终止,迁移前要和团队确认清楚。

迁移过程中有哪些坑必须避开?

结论:导入期间一定要关闭自动保存和修订清理,否则会出现「导入 20 万条、落地 19 万条」的情况。

具体清单:在 wp-config.php 里设 define('WP_POST_REVISIONS', 100);(默认值 true 表示不限条数,设成数字才是上限,设 false 直接关闭),并设 define('AUTOSAVE_INTERVAL', 300); 拉长自动保存间隔;导入前 define('DISABLE_WP_CRON', true); 并停掉定时任务,避免优化插件在后台跑清理;同时确认目标库 max_allowed_packet 不低于 64M,否则大文章快照会截断。修订行的 post_date 在 WordPress 里通常是 0000-00-00 00:00:00,真正有意义的是 post_modified,做时间排序时别用错字段。最后抽样 10 篇文章,用 wp post get <revision_id> --field=post_content 与原站逐字对比,确认换行和短代码没有被转义破坏。

迁移完成后的验证怎么做

结论:三个数字对齐才算迁移成功——文章数、修订总数、每篇文章的最新修订时间。

把这三项做成对照表,逐项比对源站与目标站;再挑 5 篇改动最频繁的文章,人工对比首个版本与最新版本的差异。GJ 站长论坛的问答区里这类迁移翻车案例不少,共同点都是只核对了文章数、没核对修订数。总耗时参考:1 万篇文章、平均 8 条修订的规模,同构迁移约 20 分钟,异构迁移含 ID 映射约 1.5~2 小时。

一句话收束:修订版本不是文章的附属属性,而是一张独立的数据表,迁移方案要按「数据表迁移」来设计,先盘点结构、再决定同构直搬还是异构映射、最后用三项指标验收,缺一步都会丢历史。

本文转载自 GJ站长论坛,原文地址:https://www.gj0.com/thread-2669.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 3

liulian
liulian 初级会员萌新站长 1楼 2026-10-28 16:02

补充几个实操里最容易翻车的点——尤其是“同构直接整表搬运”这条路,光搬 wp_posts 是不够的。

一、修订行的附属数据必须一起走。 修订本身也有 postmeta(自定义字段的历史值存在修订行下),另外 wp_term_relationships.object_id、wp_postmeta 里的 _thumbnail_id 全都是写死的文章 ID。异构迁移时如果只重写了修订行的 post_parent,而分类、标签、特色图片没跟着做映射,结果就是文章搬过去了、归类全空。建议映射表统一用一张 id_map(old_id, new_id) 临时表,最后所有引用文章 ID 的字段一次性 JOIN UPDATE 完,比一条条改省事也安全。

二、异构匹配别只靠标题。 同标题重发、草稿标题重复太常见了,实测优先级应该是:旧站 slug(post_name)唯一匹配 → post_title + post_date 组合匹配 → 剩下的打日志人工核。匹配失败率超过 5% 就别硬灌了,先回头查源数据。

三、目标站不支持修订的降级方案,我更推荐“存档表 + 只读页”:建一张 revision_archive(article_id, rev_time, content, diff_html),前台只读页展示、不进搜索索引、不进评论流,比塞隐藏版块干净得多,后期想清理也只是一张表。

四、迁移完必做两步校验:先用 wp post list --post_type=revision --format=count 比对新旧条数;再随机抽 20 篇,比对“最新修订内容”与“当前正文”是否一致——很多站当前正文其实就是最新修订的复制,这里对不上说明写入顺序错了(先灌正文后灌修订会导致时间戳倒挂)。

最后提一句:WP 站点如果修订条数爆炸,导入前 define('WP_POST_REVISIONS', 10) 配合 wp post delete $(wp post list --post_type=revision --format=ids) 先裁一遍,百万行能砍掉一大半导入时间。Drupal 那边注意 node_revision 和 node_field_revision 是两张表,只搬前者会出现正文历史在、字段历史不在的半吊子状态。

juming
juming 作者初级会员 #3748 2楼 2026-10-28 16:21
liulian:补充几个实操里最容易翻车的点——尤其是“同构直接整表搬运”这条路,光搬 `wp_posts` 是不够的。 **一、修订行的附属数据必须一起走。** 修订本身也…

id_map 的思路完全对,但有两处得补一刀。

一是 define('WP_POST_REVISIONS', 10) 这行最容易误用:它只在走 wp_insert_post() 的写入路径里生效,wp db import 或者裸 INSERT 是绕过这层的,常量设了也删不掉一行。所以裁剪必须在导出前在源站执行,顺序反了就是白等一轮导入。真正想控量,可以先 wp post delete 裁完再 wp db export。

二是「引用文章 ID 的字段」其实散在三个不同的列名上,写 JOIN UPDATE 时别只盯着 post_parent:wp_postmeta 里是 post_id,wp_term_relationships 和 wp_commentmeta 里是 object_id,wp_posts 里才是 post_parent。_thumbnail_id 的 meta_value 是纯数字,走映射很安全;但 _wp_attachment_metadata 和 Gutenberg 块注释里的引用嵌的是序列化结构,必须 --precise 处理,裸 REPLACE 会把长度字段写坏。

校验建议在条数之外再加一层内容指纹,新旧站跑同一条语句比对,比抽 20 篇更靠谱:

SELECT post_parent, COUNT(*) AS c,
       MD5(GROUP_CONCAT(MD5(post_content) ORDER BY post_date)) AS fp
FROM wp_posts WHERE post_type='revision' GROUP BY post_parent;

另外轮询时间戳倒挂那点,根因往往是 INSERT 没显式带上 post_date / post_modified,让列默认值兜了底,导数据时把这两个字段写死就没这问题。

chinaz
chinaz 初级会员萌新站长 #3751 3楼 2026-10-28 16:30
juming:id_map 的思路完全对,但有两处得补一刀。 一是 `define('WP_POST_REVISIONS', 10)` 这行最容易误用:它只在走 `wp_i…

两条都站得住,尤其是第一条——常量管不住裸 INSERT 这一点,我见过两次有人设了 WP_POST_REVISIONS 就直接导库,白等几个小时才发现一行没少。

指纹校验那条我再补一个隐藏问题:GROUP_CONCAT 默认 group_concat_max_len 只有 1024 字节,一篇有几十个修订、每个快照几 KB,聚合串早就被静默截断了,新旧站算出来的 fp 反而“看起来完全一致”,比不校验更危险。跑之前先 SET SESSION group_concat_max_len = 100000000;(同会话内有效,写成迁移脚本时别丢这句),或者换成 BIT_XOR(CRC32(MD5(post_content))) 这类定长聚合,天然不受长度限制。

时间戳再往下钻一层:只写 post_date / post_modified 不够,post_date_gmt 和 post_modified_gmt 必须一起写死。这俩字段一旦让默认值兜底,前台按 GMT 排序、RSS 输出、date_query 全错,而且不会报任何错,通常要等用户反馈“文章时间差八小时”才被发现。

裁剪那步也顺带提一句:wp post delete 删修订行不会连带清 wp_postmeta 里挂在这些修订 ID 下的记录,裁完补一条 DELETE FROM wp_postmeta WHERE post_id NOT IN (SELECT ID FROM wp_posts),不然瘦身效果要打对折。

另外一个常见问题:导完后跑一遍 wp post list --post_type=revision,确认 post_status 都是 inherit,有些导出工具会把它写成 publish,结果修订全跑到前台列表里去了。