Administrator
发布于 2026-10-29 / 0 阅读
0
0

一次真实的博客迁移:从阿里云到腾讯云,顺手把图片搬进了 COS

这篇文章记录了我把「渣渣辉的小站」从阿里云迁到腾讯云的全过程:Halo 2.21 重新部署、域名卡在备案、小程序接口从 1.x 迁到 2.x、141 张封面图搬到对象存储、插件配置、定时备份。

全程踩了不少坑,代码和命令都留在下面,希望能让后来的人少走两步弯路。


一、起因:为什么要搬

原来的架构很简单,也很"经典":阿里云一台服务器跑 Halo,图片单独扔在七牛云,绑了个自定义域名做图床,小程序直接调 Halo 的接口。

问题出在到期续费和备案这两件事上。旧服务器不打算续了,新服务器在腾讯云(轻量应用服务器,北京区),于是整站迁移被提上日程。

我最初以为这是"复制粘贴 + 改个 IP"的活,实际做下来发现,真正耗时间的从来不是搬家本身,而是搬家时暴露出来的所有历史欠账。

最终的目标架构是这样:

层次

迁移前

迁移后

服务器

阿里云 ECS

腾讯云轻量应用服务器(北京)

博客程序

旧版 Halo

Halo 2.21(1Panel + Docker 部署)

数据库

旧库

MySQL 容器,独立数据卷

图片存储

七牛云 + 自定义域名

腾讯云 COS

小程序接口

Halo 1.x API

Halo 2.x 公开内容 API

备份

手动

每日 03:30 自动备份到 COS


二、第一步:把 Halo 2.21 跑起来

新服务器上用 1Panel 的应用商店装 Halo,本质上是起两个容器:一个 halo,一个 mysql,数据目录挂载到宿主机。

这一步没什么技术含量,但有两个习惯我建议你养成:

1. 数据库单独起容器,不要用 Halo 内置的 H2。 H2 文件库在容器里一旦挂载出问题,恢复起来非常痛苦;MySQL 至少可以随时 mysqldump。

2. 数据目录一定要挂到宿主机。 容器的生命周期是不可靠的,/opt/1panel/apps/halo/halo/data 这种宿主机路径才是你真正的家底。

装完之后,把旧站导出的备份(Markdown + 附件 + 数据库)导入,站点就"活"过来了。


三、第二步:域名卡在备案,只能先用 IP 顶着

这是整个迁移里唯一一个纯技术解决不了的问题。

域名解析到新服务器后,访问会 302 跳到一个"网站未备案"的拦截页;HTTPS 更干脆,TLS 握手阶段直接 ERR_CONNECTION_RESET。

原因不复杂:境内服务器 + 未备案域名 = 80/443 被合规拦截。这不是配置问题,也不是证书问题,等备案下来之前,任何折腾都是白费。

我的临时方案:

  • 网站端:直接走 http://<服务器IP>,能用,但不体面。

  • 小程序端:app.js 里的 baseUrl 从域名改成 IP,并在开发者工具里勾选「不校验合法域名、TLS 版本以及 HTTPS 证书」。

这里有个坑值得记一笔: 小程序真机预览和开发者工具的行为并不完全一致。工具里勾了"不校验合法域名"能跑,真机上依然会被拦。所以备案期间只适合自己调试,不适合给别人体验。

另外一个容易忽略的点:网站的 ICP 备案和小程序的备案是两件事。 网站备案下来之后,小程序还需要在小程序后台单独提交一次备案,别以为一次搞定就万事大吉。


四、第三步:小程序白屏 —— 1.x 到 2.x 的接口断层

网站能访问了,小程序却调不出数据。这是迁移里最有价值的一段经验。

Halo 从 1.x 升到 2.x,不只是界面换了,公开 API 整个重写了。旧代码里的接口路径和字段结构,在 2.x 上全部失效。

4.1 接口路径变了

用途

1.x

2.x

文章列表

/api/content/posts

/apis/api.content.halo.run/v1alpha1/posts

文章详情

/api/content/posts/{id}

/apis/api.content.halo.run/v1alpha1/posts/{name}

2.x 的路径是 Kubernetes 风格的资源路径,又长又丑,但好处是语义清晰:api.content.halo.run 是 API 组,v1alpha1 是版本,posts 是资源。

还有一个细节:详情接口的参数从 id 变成了 name,也就是列表项里的 metadata.name(等同于文章的 slug)。拿 1.x 的 id 去请求 2.x 的详情,只会得到 404。

分页参数倒是友好,page 从 0 开始,size 是每页条数:

4.2 返回结构也变了

1.x 的列表项里套着一个 post 对象,字段都在里面;2.x 把这个嵌套层去掉了,metadata / spec / status / content / stats / categories 全部平铺在每一项的顶层。

两个必须知道的点:

  • 列表接口返回的 content.content 是 null,正文只在详情接口里给。所以别指望列表页能拿到正文做摘要,摘要要用 status.excerpt,或者自己从详情里截。

  • 分类和标签是对象数组,名字在 spec.displayName 里,不是直接一个字符串。

改造方式很朴素:写一层归一化函数,把 2.x 的结构转换成小程序页面认识的老字段,这样 wxml 一行都不用改。

小提示:2.x 的 publishTime 是带时区的 ISO 字符串,直接 slice(0, 10) 取到的是 UTC 日期。对博客来说差这几小时通常无所谓,但如果你的文章经常在深夜发布,最好显式做一次时区转换。


五、第四步:图片还躺在七牛云,封面全挂了

数据通了之后,最刺眼的问题来了:所有封面图都是裂的。

图片当年上传在七牛云,绑的是旧站时期的一个自定义域名。这个域名现在指向的是早已下线的阿里云旧站,解析过去全站 403 —— 也就是说,链接还在,图没了。

5.1 为什么不能"换个域名就行"

很多人第一反应是:把图片域名换一个新的不就好了?

不行。因为你换的只是域名,图片本体还在七牛的存储空间里。老域名一停,等于图的"门牌号"作废了,而新门牌号必须指向同一批文件。

所以正确的顺序是:先把文件搬走,再改链接。

七牛当时还有一个限制:自定义域名必须备案,而临时测试域名只有 30 天有效期。这等于给了个倒计时——要么在 30 天内把图搬完,要么等域名失效后彻底没辙。

5.2 迁移的实操步骤

  1. 在腾讯云 COS 建桶,选北京区(和服务器同地域,走内网免流量费),权限设为「公有读私有写」。博客图片是给所有人看的,私有读会让小程序和网页都加载不出来。

  1. 从七牛导出:用控制台的批量操作把原空间的文件移动到一个归档空间,再从归档空间批量下载。

  1. 上传到 COS:直接 PUT,保留原始文件名(这点很重要,见下一步)。

  1. 改数据库里的链接。

第 4 步是重点。全站所有图片引用都存在数据库里,分散在文章正文、封面字段、设置项等地方。我是直接在库里做替换的:

执行前务必先备份:

这次一共处理了 230 行记录,成功替换 229 行。剩下那 1 行不是替换失败,而是早期就存了一个截断的坏链——这种历史遗留问题,只能等哪天肉眼看到图裂了再手工修。

另外还有一个隐蔽的坑:图片文件名里如果带括号,用正则批量提取链接时很容易被截断,导致 URL 变成半截。这次 146 处引用里,最后只有 141 张真正搬到位,剩 5 处全是这种带括号的截断链接。批量处理文件名时,用解析器不要用正则。

5.3 顺手把"以后上传"也解决掉

图片搬完了,但如果后台发文时上传还是往本地磁盘塞,那过不了多久又得搬一次。

Halo 2.x 用插件解决这个问题:plugin-s3。装它的过程,比想象中曲折。

第一个坑:版本兼容。

插件版本

要求 Halo 版本

1.17.0

≥ 2.24

1.14.0

≥ 2.22

1.13.2

≥ 2.21

我的站点是 2.21.0,只能用 1.13.2。新版插件在低版本 Halo 上装不上,报的错还不明显,很容易以为是插件坏了。装插件前先看版本要求,这是硬门槛。

第二个坑:上传 403。

插件装好、密钥填好,测试上传却报 403 Forbidden,错误信息指向 HeadObject。

排查后发现是两个原因叠加:

  1. 子账号权限不全。 COS 的权限分两层:桶级(列出桶、访问桶元信息)和对象级(读写具体文件)。只给对象级权限,插件在初始化时去查桶信息就会 403。策略里两级都要写:

注意第二个 resource 结尾是 / 而不是 /* —— 这就是桶本身的权限。

  1. 访问方式选错了。 插件里有个 enablePathStyleAccess 开关,默认走 Path Style(域名/桶名/文件)。腾讯云 COS 只认 Virtual Hosted Style(桶名.域名/文件)。这个开关必须关掉,否则签名永远对不上。

第三个坑:插件启用了,但不起作用。

后台显示插件已启用,上传依然落在本地。原因是没设默认附件策略。插件只是提供了"可以用 COS"的能力,但博客的默认存储还是本地。

Halo 2.x 的默认附件策略存在一个系统级 ConfigMap 里,后台界面上不一定能直接改。我是直接改数据库,然后重启容器生效的:

改完之后传了一张测试图,公网访问 200,才算真正跑通。最后把测试附件删干净,媒体库回到 139 个文件的干净状态。


六、第五步:备份是最后一根安全绳

前面所有操作里,我最怕的不是失败,是不可逆的失败。所以备份是和迁移同步做的,不是等"有空再说"。

方案很简单:每天凌晨 3:30,用 cron 跑一个脚本,把数据库和 Halo 数据目录打包,传到 COS 的不同前缀下。

脚本里做三件事:

  1. mysqldump 导出数据库,gzip 压缩

  1. tar 打包 Halo 的 data 目录(文章、附件、配置)

  1. 用 coscli 上传到 backup/db/ 和 backup/halo2/

有两个细节值得说:

  • 备份用的密钥,权限只要"能写进 backup 目录"。 绝对不要图省事用主账号密钥,更不能给 cos:* 全桶权限。备份脚本是长期自动运行的,它是整个系统里最容易被忽略的提权入口。

  • 备份要验证,不是跑完就算成功。 我第一次跑完看着日志全绿,还是去 COS 控制台把文件下载下来解压了一遍,确认能还原才算数。从来没验证过的备份,等于没有备份。

同时做了一次密钥收尾:

  1. 创建子账号,只给 COS 的最小权限

  1. 插件、备份脚本全部换成子账号密钥

  1. 确认一切正常后,把主账号的两把密钥禁用

顺序很重要——先建立替代方案,再收回旧权限。反过来做,中间那段时间你就是拿不到钥匙但站在门外的状态。


七、第六步:小程序界面重做

功能通了之后,界面还是原样:白底卡片、纯文字、连 tabBar 都是光秃秃的。

老实说功能能用的时候,视觉是最容易被推迟的部分。但小程序这个场景不太一样——用户第一眼看到的就是界面,丑就是丑,没有第二次机会。

这次重做的几个点:

  • 配色统一到一套变量。之前颜色是散落在各个 wxss 里的硬编码,改一处要翻五个文件。现在全部抽到 app.wxss 的 CSS 变量里。

  • tabBar 补上图标。没有图标的 tabBar 看起来像个半成品。

  • 首页改成杂志式布局:第一篇用大图精选卡,其余用"左文右图"紧凑卡片。这样既保证首屏有视觉冲击,长列表又不会因为全是巨图而滚动疲劳。

  • 详情页做沉浸式:封面大图打底、标题压图上、正文白卡圆角叠压,顶部加阅读进度条。

这里遇到一个小程序特有的问题,值得单独说:rich-text 组件内部的元素,不受 wxss 控制。

也就是说,你写 .content p { line-height: 1.8 },对 rich-text 渲染出来的 <p> 完全无效。它只认内联样式。

解决办法是在把 HTML 交给 rich-text 之前,先遍历给每个标签注入样式:

用占位符先"隔离"代码块,是为了避免给 <pre> 里的 <code> 又加一层行内代码的背景色和 padding,那样渲染出来会非常难看。


八、现在的状态

  • 网站和小程序都正常访问,58 篇文章、141 张图片全部迁到新环境

  • 图片上传走 COS,后台发文直接存对象存储

  • 每天 03:30 自动备份,已验证可还原

  • 主账号密钥已禁用,日常运行全部使用最小权限子账号

  • 小程序界面完成改版

还没做完的只有一件事:备案。

这是唯一一个技术手段解决不了的问题。备案下来之后,还需要:

  1. 把 app.js 的 baseUrl 从 IP 换回域名

  1. 取消开发者工具的"不校验合法域名"选项

  1. 在小程序后台单独提交一次小程序备案

  1. 把 COS 域名加入小程序的 downloadFile 合法域名列表

SSL 证书 2027 年 1 月到期,记得提前开自动续期——这种一年才提醒一次的事情,靠记性是最不可靠的。


九、复盘:五条经验

1. 迁移的难点从来不在"搬",而在"搬的时候暴露出来的旧债"。 真正花时间的不是部署 Halo,是七牛的老域名、截断的图片链接、1.x 时代的接口调用。平时能跑的代码,换个环境就全是坑。

2. 版本兼容是插件生态的第一道坎。 Halo 插件有明确的版本要求,"最新版"往往是你装不上的那个。先查兼容矩阵,再动手。

3. 云服务的权限是分层的。 桶级和对象级、账号级和资源级,少给一层就是 403。报错信息通常不会告诉你少了哪一层,只能一层层试。

4. 先建立替代,再收回旧权限。 密钥轮换、域名切换、服务迁移,都是这个顺序。反过来做,中间会出现一段"旧的不能用、新的还没好"的空窗期,而这段时间往往正好出问题。

5. 备份要验证,还要用最小权限的密钥。 没验证过的备份只是心理安慰,权限过大的备份脚本是长期存在的安全隐患。


迁移这件事有个特点:过程很琐碎,回头看又很清晰。

真正让我印象深刻的不是某个技术点,而是"所有以为没问题的地方都会出问题"这件事本身。旧域名、旧接口、旧链接、旧权限——它们在原地待了两年没出过事,一旦换了环境,全部同时爆发。

所以如果让我给一句建议:迁移之前,先把"哪些东西是隐式依赖"列一遍。 图片域名、接口路径、定时任务、硬编码的 IP、没写进文档的手工配置——这些才是真正决定迁移要花多久的东西。


本文记录于 2026 年 10 月,Halo 2.21.0 / 腾讯云轻量应用服务器 / COS 北京区。


评论