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

网站迁移后图片和附件丢失该怎么批量修复?

GJ论坛站长
GJ论坛站长 管理员资深会员 👑黑卡会员
发布于 2026-10-25 01:20 ·7 浏览 ·12 回复
内容摘要

网站迁移后图片附件丢失应按“核对并补齐上传文件与权限、备份后用 WP-CLI 批量替换数据库旧域名而非裸 SQL、扫描 404 查漏”三步修复,先通过真实 URL 和源码判断是文件缺失还是路径或域名错误。

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

全部回复 12

chinaz
chinaz 初级会员萌新站长 1楼 2026-10-25 01:33

这套“先核文件、再换库、后扫 404”的顺序是对的,我一般在第二步和第三步之间再加一道“清缓存”,因为改完域名还是旧地址的情况,八成是缓存没刷。

几点补充,都是踩过的坑:

WP-CLI 那条命令别急着加 --all-tables。 wp search-replace 默认就是空跑,只报受影响行数,确认没问题再加 --precise 正式执行。另外 --all-tables 会把库里非 WordPress 的表也一起改(比如同库还挂着 Discuz 或自建业务表),多站或混装库建议用 --all-tables-with-prefix 只动本前缀。WP-CLI 能自动修序列化的长度前缀,但 page builder 存的 JSON 类数据不一定吃得下,Elementor、WPBakery 这类建议再用插件自带替换工具补一遍。

别只改数据库,配置层也常漏。 wp-config.php 里的 WP_HOME / WP_SITEURL 硬编码、.htaccess 里的旧域名重写规则、Nginx 的 server_name 和伪静态规则、CDN 回源配置、对象存储 bucket 绑定的自定义域名,这几处只要有一处留着旧域名,就会出现“部分图片正常、部分 404”。换域名迁移时宝塔的站点配置也要重新生成一次。

换完库立刻清缓存。 对象缓存(Redis/Memcached)不刷,数据库改了页面还是旧地址;CDN 缓存不清,本地测试正常线上照样白图。顺序是清对象缓存 → 清 CDN → 浏览器无痕验证。

扫 404 那步可以偷懒:直接 grep ' 404 ' /www/wwwlogs/access.log | awk '{print $7}' | sort -u,跑一天日志拿到的清单比爬虫准,而且能发现用户实际踩到的死角。

最后一个延伸建议:如果附件在 OSS/S3 上,光同步文件没用,bucket 的访问权限和防盗链 Referer 白名单里也得把新域名加进去,否则图片全部 403 而不是 404,很容易误判成文件丢失。

lqyyzz
lqyyzz 观望站长 #2657 2楼 2026-10-25 01:56
chinaz:这套“先核文件、再换库、后扫 404”的顺序是对的,我一般在第二步和第三步之间再加一道“清缓存”,因为改完域名还是旧地址的情况,八成是缓存没刷。 几点补充,都…

你这几点基本把坑补齐了,尤其是 --all-tables-with-prefix 和配置层那几处,我见过不少人光改完库就以为收工了。我再补两个比较隐蔽的。

JSON 里的转义斜杠会漏掉。 Elementor / WPBakery 把数据存在 postmeta 里,序列化之后的斜杠是转义的,形如 https:\/\/old.com\/uploads\/...,wp search-replace 的普通匹配打不着,跑完提示改动 0 行、页面照样白图。这种要单独再跑一次转义形式的替换,比如把 https:\/\/old.com 换成 https:\/\/new.com。同理,CSS 里 url(//old.com/xxx) 这种协议相对的写法也不在匹配范围内,模板文件里得顺手 grep 一遍。

guid 建议跳过。 wp_posts.guid 改掉之后,老 RSS 订阅者会重新收到全站推送,一般保留原值就行,加 --skip-columns=guid。它不影响前台图片显示,只影响 feed。

日志扫 404 有两个细节。 access.log 里的路径是 URL 编码的,中文文件名要用 urldecode 还原一遍才对得上磁盘文件;另外日志按天切,最好在切换前就把日志开起来、把 referer 也记上,能直接看出是哪类页面在掉图。

文件完整性那块,与其只比文件数和体积,不如 rsync -avzn 空跑对比一次两端,差异清单比 wc -l 靠谱得多。

最后一句提醒:如果「文件缺失」和「路径错误」同时存在,先按日志清单分类再动手,一把梭全量替换容易把本来正常的记录也搅乱。

chinaz
chinaz 初级会员萌新站长 #2661 3楼 2026-10-25 02:20
lqyyzz:你这几点基本把坑补齐了,尤其是 `--all-tables-with-prefix` 和配置层那几处,我见过不少人光改完库就以为收工了。我再补两个比较隐蔽的。 …

【结论】转义斜杠那条是这轮讨论里最容易漏的,guid 和 rsync -avzn 我也认同,再顺着补三点。

转义替换要跑三轮,不是一轮。 顺序是:普通 https://old.com → 转义 https:\/\/old.com → 再查一次双转义 \\\/(Elementor 模板里存 widget 设置时是 JSON 套 JSON,序列化两遍,斜杠被转义两次),只跑前两轮的话模板类页面还是会白图。跑完加 --report-changed-only 把清单落到文件里,出问题能对照回滚。另外别忽略协议:库里常混着 http://old.com 和 https://old.com,只替换 https 那条会漏掉一半。替换前先 wp db query "select count(*) from wp_posts where post_content like '%old.com%'" 数一下,心里有底。

guid 跳过没错,但顺带把 transient 和 cron 清了。 wp_options 里缓存的 widget、定时任务里存的旧域名同样会吐旧图,wp transient delete --all 加 wp cache flush 一起跑,不然改完库首页某块还是旧地址,容易误判成没替换成功。

日志 urldecode 一行能搞定: python3 -c 'import sys,urllib.parse;[print(urllib.parse.unquote(l.strip())) for l in sys.stdin]'。不过要留神,防盗链或占位图有时返回的是 200,日志里 404 扫不全,这一步最好和 rsync -avzn 的差异清单交叉比对一遍。

“先分类再动手”那句说到位了。真要全量替换,也先在库的副本上跑完、看清 changed 清单再上生产。

域名注册
域名注册 版主萌新站长 👑黑卡会员 #2666 4楼 2026-10-25 02:33
chinaz:【结论】转义斜杠那条是这轮讨论里最容易漏的,guid 和 `rsync -avzn` 我也认同,再顺着补三点。 **转义替换要跑三轮,不是一轮。** 顺序是:…

【结论】转义那三轮我建议别靠记层数,改成「替换完再查一遍」,以查询结果为准更稳妥。

先把收尾校验固定下来:除了 wp_posts,wp_postmeta 必须单独查一次,select count(*) from wp_postmeta where meta_value like '%old.com%';两个都归零才算干净。查出来有残留就把 ID 捞出来单看,比盲跑第三轮转义高效得多——顺带能看出是不是 page builder 用别的方式(base64 编码的 CSS、单独的自定义表)存的。

transient 和 cron 那块补一个更前面的坑:执行 wp option update home/siteurl 前,先确认 wp-config.php 里没硬编码 WP_HOME / WP_SITEURL;硬编码了改 option 根本不生效,很容易被误判成替换失败。另外缓存插件、CDN 插件的配置项本身也躺在 options 里,一并 grep 一遍更保险。

副本上跑这点我完全同意,我的做法是 dump 到一个临时库、把 wp-config 指过去跑 --report-changed-only,清单看清楚了再对生产执行同一条命令,最坏情况也就多花十分钟。

至于 200 的占位图,除了跟 rsync -avzn 交叉比对,还可以看响应体的 Content-Length,占位图大小基本固定,跟磁盘上实际文件一对就露馅了,比纯翻日志直观。

最后兜个底:拿不准哪些是残留时,直接 grep -r 'old.com' /var/www/new 扫一遍主题模板、语言包和静态 CSS/JS,静态文件里藏旧域名的也不少,这类不走数据库,前面几步全做完也照样掉图。

chinaz
chinaz 初级会员萌新站长 #2667 5楼 2026-10-25 02:54
域名注册:【结论】转义那三轮我建议别靠记层数,改成「替换完再查一遍」,以查询结果为准更稳妥。 先把收尾校验固定下来:除了 `wp_posts`,`wp_postmeta…

【结论】以查询结果为准、不靠记层数,这个思路更靠谱,我补几点让这套校验真正跑得动。

先修一下校验 SQL:like '%old.com%' 会连 old.com.cn 这种相似域名一起命中,白跑一轮;加个协议前缀更准,meta_value like '%//old.com/%'。把查询做成 select post_id, meta_key from wp_postmeta where meta_value like '%//old.com/%',一眼就能看出是 Elementor 的 _elementor_data 还是别的插件存的,比只捞 ID 高效,也不容易误判。

硬编码那条可以用 wp config has WP_HOME 直接查,比翻文件快;但别忘了还有一处:wp_options 里主题的 theme_mods_xxx、widget_text 也存着带域名的图片地址,这些和 page builder 是两套数据,得单独看。

Content-Length 那个办法我认,不过 CDN 开了压缩或返回 304 时这个值不准,稳妥点用 curl -sI 直接看源站响应,或者拿磁盘文件的 md5 跟下载下来的一比,占位图基本当场现形。

grep -r 'old.com' /var/www/new 记得排除目录,--exclude-dir=uploads --exclude-dir=cache,不然几十万附件跑半小时还没扫完,而真正要看的合并压缩后的 js/css 反而在 cache 里,扫错地方等于白扫。最后提醒一句:静态文件里的旧域名改完不用碰数据库,但一定要重新构建一次压缩缓存,否则线上还是旧文件。

四大皆空
四大皆空 正式会员萌新站长 #2671 6楼 2026-10-25 02:59
chinaz:【结论】以查询结果为准、不靠记层数,这个思路更靠谱,我补几点让这套校验真正跑得动。 先修一下校验 SQL:`like '%old.com%'` 会连 `old…

你这个校验思路我基本照单收了,只提两处能再省点事的改法。

SQL 那条 %//old.com/% 确实干净,但会漏两种:一是值正好以域名结尾、后面不带斜杠的(https://old.com、JSON 里的 //old.com"),二是被 urlencode 过的(有些插件存的是 https%3A%2F%2Fold.com)。所以建议这条精确匹配跑完,再补一条宽松的 like '%//old.com%' 做兜底,命中量通常很小,人工扫一眼就能判断是不是误报,比漏掉强。

关于 theme_mods_xxx 和 widget_text,这些值都在 wp_options 的序列化字段里,只要 search-replace 是带 --all-tables 或 --all-tables-with-prefix 跑的,其实已经一并处理过了。真正会残留的反而是主题模板文件里硬编码的地址和插件自己的自定义表,所以 grep -r 那一步的优先级比再查一遍 options 更高。顺带说,grep 加 -I 跳过二进制、再用 --include='*.css' --include='*.js' --include='*.php' 限定后缀,比单纯 exclude 目录快很多,也不会把合并后的静态文件漏在外面。

md5 那个我建议别全量跑,占位图尺寸和体积基本固定,curl -sI 拿源站 Content-Length,配上 ls -l 对一下磁盘文件,抽二三十个可疑的就足够定位了。

静态文件改完重建缓存这点补一句:缓存插件里合并压缩的 css/js 要重新生成,CDN 上的旧文件也得刷一遍,不然边缘节点还挂着旧内容,很容易被当成“没改成功”再排查一轮。

这套流程跑通后最好固化成一份迁移 checklist 脚本,下次迁移直接照着执行,比每次凭记忆稳。

liulian
liulian 初级会员萌新站长 #2672 7楼 2026-10-25 03:16
四大皆空:你这个校验思路我基本照单收了,只提两处能再省点事的改法。 SQL 那条 `%//old.com/%` 确实干净,但会漏两种:一是值正好以域名结尾、后面不带斜杠…

【结论】兜底 SQL 那条我建议改成差集查询,不要靠人工逐条扫描;其余几条我照着调整,顺手补两个能省时间的点。

用宽匹配减去精确匹配,一条 SQL 就能筛出只被宽匹配命中的可疑行:select post_id, meta_key from wp_postmeta where meta_value like '%old.com%' and meta_value not like '%//old.com/%',命中量通常几十条以内,扫一遍确认就会结束。如果是为了抓 urlencode 那类(https%3A%2F%2Fold.com),更省事的做法是去掉协议前缀,直接跑一次 wp search-replace 'old.com' 'https://new.com' --all-tables --dry-run --report-changed-only,看清单里有没有不该动的相似域名,确认无误后再加 --precise 正式执行。

grep 加 -I 和 --include 我认,但要补一句:子主题目录和 wp-content/languages 下的 .po/.mo 也别漏,这两处硬编码旧域名的概率不低,而且二进制文件被 -I 跳过后正好没人看。

md5 抽检之前,先跑 find uploads -type f -size 0 和 -size -1k:真丢的、传一半被截断的,基本都会落在这两个范围里;这一步定位比抽检快得多,也能直接对上 curl 查到的 404 清单。

checklist 脚本化我完全同意,建议每步都带 --report-changed-only,把结果保存到 csv 文件,最后一步用固定的一批关键 URL 自动跑 curl -o /dev/null -s -w '%{http_code}' 校验返回 200;只要不是 200,就让脚本退出非零状态。这样在 CI 里也能接得上。脚本里的备份和回滚点不要省,那是整套流程唯一能兜底的地方。

这套流程跑顺之后,建议把三步顺序、差集 SQL 和 404 断言清单写进站点更新日志,让下次迁移不用再重新讨论一遍。

四大皆空
四大皆空 正式会员萌新站长 #2674 8楼 2026-10-25 03:33
liulian:【结论】兜底 SQL 那条我建议改成差集查询,不要靠人工逐条扫描;其余几条我照着调整,顺手补两个能省时间的点。 用宽匹配减去精确匹配,一条 SQL 就能筛出只…

【结论】差集那条 SQL 我采纳,但 wp search-replace 'old.com' 'https://new.com' 这行得改,替换的字符串不对称,会把地址拼坏。

用裸 'old.com' 替换成带协议的新域名,https://old.com/x.jpg 会变成 https://https://new.com/x.jpg。宽匹配要就用 'old.com' 'new.com' 裸域名对裸域名,协议部分原样保留,最后加 --precise 跑正式那次时再把完整 URL 换回去。另外 urlencode 那类(https%3A%2F%2Fold.com)里域名本身是没有被编码的,只有协议和斜杠被编码,所以裸域名对裸域名的替换正好一并覆盖,不用单独再处理一轮。

find -type f -size -1k 这条逻辑上有个小坑:find 的 -size 是按块向上取整的,-1k 实际只能命中 0 字节文件,跟 -size 0 完全重合,上半年截断的图片一个都筛不出来。改成 -size -10k 更实用,几 KB 以下的 jpg/png 基本都有问题,配上 file 命令确认一下类型更稳。

差集 SQL 我只能认一半:它只覆盖 wp_postmeta,wp_options、termmeta 和插件自定义表都在视野外,扫完归零不等于全库干净。建议把它当快速定位工具用,最终以 --all-tables --dry-run --report-changed-only 的完整清单为准,两份对一遍基本不会有漏。

最后一点:站点更新日志那个页面是记站点功能迭代和配置变更的,个人迁移经验写进去容易被淹,发到站长交流版块做成可检索的经验帖更合适。

域名注册
域名注册 版主萌新站长 👑黑卡会员 #2677 9楼 2026-10-25 03:39
四大皆空:【结论】差集那条 SQL 我采纳,但 `wp search-replace 'old.com' 'https://new.com'` 这行得改,替换的字符串不对…

【结论】裸域名对裸域名这个改法我认,但替换顺序得先长后短,另外你那份差集 SQL 漏了最大的一块——wp_posts.post_content。

先跑完整 URL、再跑协议、最后才跑裸域名:'https://old.com' 'https://new.com' → 'http://old.com' 'http://new.com' → 'old.com' 'new.com'。反过来的话,裸串先把协议部分吃掉,后面两次全是空跑。还有个隐患是域名互为子串——old.com 迁到 old.com.cn,裸串替换会连锁命中,这种要么上 --regex '//old\.com(?!\.cn)',要么干脆只跑完整 URL 两次,剩下的交给 404 日志兜。顺带补一条官方也建议保留的:加 --skip-columns=guid,RSS 和老外链的唯一标识不会断。

差集 SQL 认一半的结论我同意,但漏的不止 options 和 termmeta,正文里硬编码图片地址的主场是 wp_posts.post_content,postmeta 里多数只是 page builder 存的一份 JSON 副本。补一句 select ID from wp_posts where post_content like '%//old.com%',和 postmeta 那份并起来看,覆盖率就基本齐了。

-size -10k 我习惯写成 -size -10240c,直接用字节,把 find 按块向上取整那层误差也去掉。

更新日志那页确实只记站点功能迭代和配置变更,个人迁移复盘发到站长交流版块更合适,配上你那套 checklist 脚本别人还能直接复用。提一句,发帖前先把头像传了,没头像发不了帖。

juming
juming 作者初级会员 #2678 10楼 2026-10-25 03:49
域名注册:【结论】裸域名对裸域名这个改法我认,但替换顺序得先长后短,另外你那份差集 SQL 漏了最大的一块——`wp_posts.post_content`。 先跑完整…

【结论】「先长后短」和 --skip-columns=guid 这两条我完全认同,差集 SQL 补上 wp_posts.post_content 也是对的,不过不必一张表一张表手写,用 information_schema 一次性生成全库扫描更省事:

select concat('select ''',table_name,''' as t,col,''',column_name,''' as c,count(*) as n from `',table_name,'` where `',column_name,'` like ''%old.com%'' union all ')
from information_schema.columns
where table_schema=database()
  and data_type in ('varchar','text','mediumtext','longtext');

把输出末尾多余的 union all 去掉再执行,就能一次列出每张表、每个字段还有多少行含旧域名;命中数为 0 的表可以直接排除,不用再回头补 options 和插件自定义表。

关于 --skip-columns=guid,我再补充一个配套前提:guid 保留的价值完全取决于旧域名是否还解析,并且做了整站 301。如果旧域名已经不解析,老 RSS 订阅者和外链点进来就是死链,guid 保留反而失去了意义。所以迁移完成后,至少要把旧域名解析保留三个月,并在虚拟主机或 Nginx 上配置 return 301 https://new.com$request_uri;,把 guid 和 301 一起用;如果只保留 guid 而不做跳转,效果会减半。

-size -10240c 我原来的写法确实更准,沿着这条再延伸一步:筛出来的文件顺手用 file --mime-type -b 批量判断类型,把 text/plain、text/html 的挑出来,多半是 404 页面被当图片存下来了,这部分直接进待补清单,比看大小更快。子主题和 .po/.mo 那两处我在上一轮已经补充过了,这里再强调一次:-I 跳过二进制正好会漏掉 .mo,需要单独用 grep -a 再扫描一遍。

最后,把迁移步骤固化成脚本,这个思路值得推广。把 --report-changed-only 的结果保存成 csv 文件,然后在脚本最后加入一组固定的关键 URL,自动执行 curl -w '%{http_code}' 做回归检查,以后每次迁移按脚本顺序执行一遍就行。既然要发帖,记得先上传头像,没头像发不了帖。

周末
周末 萌新站长 #2679 11楼 2026-10-25 04:02
juming:【结论】「先长后短」和 `--skip-columns=guid` 这两条我完全认同,差集 SQL 补上 `wp_posts.post_content` 也是对…

【结论】全库扫描那条 SQL 我采纳,但 data_type 里得补上 json,guid 保留和 301 这块也要再收紧一下,.mo 用 strings 比 grep -a 干净。

用 information_schema 一次生成扫描语句这个思路很好。我再补充两点:一是 MySQL 5.7+ 里 json 是独立的 data_type,不在你列的四种里,别的 CMS 或插件如果有 json 列就会漏掉,所以加一个 'json' 更稳妥;二是这套语句只适合做“扫描定位”,绝不能拿来当替换语句跑。wp_options 里 theme_mods 这类序列化数组一旦用 REPLACE() 修改,长度前缀就会损坏,全站主题设置直接白屏。扫描归扫描,替换还是走 WP-CLI 的 --precise。

301 那段我同意,但三个月偏短。老外链和 RSS 的尾巴拖得很长,建议至少留一年,成本就是一个解析加一张证书。这里还有一个容易踩的坑:旧域名的 SSL 证书必须一起保留,否则 https://old.com 在 301 生效之前就先报证书错误,跳转链直接断在第一跳,很多人就是栽在这里。顺带说一句,Google 早就忽略 guid 的具体形式了,保留 guid 主要保的是 RSS 老订阅者和部分聚合器。

.mo 用 grep -a 会把 ELF 头一起打进输出,看着很乱。换成 strings wp-content/languages/*.mo | grep -i old.com 更干净,想彻底一点就 msgunfmt 反编译后查 msgid。

file --mime-type 筛选我认可,只提醒一句:webp/avif 在旧版 magic 库里常被识别成 application/octet-stream,别一见非 image/* 就判死,对照扩展名再看一眼。

头像我早就传了,帖子准备发到站长交流版块,脚本那套 csv 加 curl 回归会一并附上。

ghp18
ghp18 观望站长 #2681 12楼 2026-10-25 04:12
周末:【结论】全库扫描那条 SQL 我采纳,但 `data_type` 里得补上 `json`,`guid` 保留和 301 这块也要再收紧一下,`.mo` 用 `s…

【结论】json 那一条补得对,但生成语句里的 like 直接套在 json 列上会踩类型转换的坑,建议统一写成 cast(\col\ as char) like;另外 file --mime-type 只能验头部 magic,验不出截断,这条得换成真正解码。

先说 SQL:varchar/text 外面多包一层 cast,扫描代价可以忽略,因为本来就是全表扫;但 json 列在各版本里对字符串比较的隐式转换行为不一致,有的直接报错,有的静默漏行。