docs: 全量梳理文档结构;补 Gitea 迁移指南 / 架构精简候选;架构总览补证书管家子系统

用户四问:「把项目文件全部理一遍 还有readme文件」「gitea 后面可能还要迁移 帮我写一个迁移文档」
「证书管家做完了吗」「感觉现在架构还是复杂了」

一、文档梳理:根目录 15 个 md → 2 个

根目录只留 README.md(入口)+ 架构总览.md(现状唯一事实源),其余全部归位:

  docs/
  ├── README.md              ← 新增:文档地图(入口 / 当前有效 / 历史归档 三层)
  ├── 证书管家.md             ← 原「函数版证书管家-方案.md」改名
  │                              (它早已是「现状+沿革」文档,标题名不副实 —— 签发早已不在 CF Worker)
  ├── Gitea迁移指南.md        ← 新增
  ├── 架构精简候选.md         ← 新增
  ├── CNB构建落地方案.md / GITEA_SECRETS.md
  └── archive/{平台与选型,功能与修复,主题与内容}/   ← 32 份

- 32 个归档文档统一加 `> 📦 本文档已归档` banner,并指向架构总览
  —— 这个仓库历史文档里全是「权威口径」,混看很容易拿废弃结论当现状
- gitea-backup/ → deploy/gitea/(原名「backup」不准,它是部署包;与其它 deploy 单元并列)
- 新增 md 链接校验(python 脚本,中文路径用 sed 不安全)→ 首轮 18 处断链
  (多一层目录要让 banner 里的相对路径补一个 ../)→ 修正后 0 断链

二、新增 docs/Gitea迁移指南.md(★ 有死线:境外 VPS 11 月到期)

★ 核心价值是「指出迁移面已大幅缩小」:仓库原有的 deploy/gitea/迁移前检查清单.md
(2026-10-04)是为「Gitea + act_runner + 又拍云同步 *整套* 搬迁」写的,**那个前提已经不存在**——
构建归 CNB,runner / upyun-sync / 中转机 / COS 都不需要了。迁移实际只剩「搬数据卷 + 改地址」。

★★ 全文最重要:本仓有 9 处写死了旧地址,按易漏程度排序(1-3 本机,4-5 线上)
  1 本机 .git/config 的 gitea remote
  2 scripts/setup-cnb-remotes.sh 的 GITEA_URL 默认值
  3 deploy/editor-api/bootstrap.sh 的 GITEA_URL 默认值
  4 ★ /srv/editor-api/.env 的 GITEA_URL      ← 漏了会「持续报错但只警告不阻断」,没人发现
  5 ★ /srv/blog 的 gitea remote               ← 同上
  6 docs/GITEA_SECRETS.md
  7-9 架构总览.md / README.md / editor-api/README.md
(不用改:docker-compose.editor.yml、server.mjs、pushall 别名 —— 只引用 remote 名字)

另含:建议新实例改用域名而非 IP(以后搬机器只改 DNS;⚠️ Gitea 28 起只读 ROOT_URL 不读
[server] DOMAIN);rsync 而非 dump(属主必须 uid/gid 1000);切换顺序「先建新的→验证→再拆旧的」;
验证清单强调 `git push gitea --dry-run`;第九节给出「干脆不留这个辅仓」的选项与判断依据。

顺带修掉两个硬伤:
- deploy/gitea/docker-compose.yml 的镜像 tag `1.28.0-rootless` 根本不存在
  (Gitea 28 起去掉 1. 前缀)→ 改 28.0.0-rootless(pin 死,不用 latest)
- deploy/gitea/.gitignore 漏了 backups/(跑一次备份就会把含 secrets 的 dump 写进历史)

三、证书管家核实(结论:已上线运行,2 个遗留)

线上实测:容器 Up (healthy);/preflight 7/7 全过;3 组域名;下次自动续期 04:10。

遗留 ① writeapi.usj.cc 证书没纳管(真问题):nginx 配置在 /www/conf.d/writeapi.usj.cc.conf,
不在 /www/sites/ 管理树里 → 1Panel 的 ssl/upload+sslID 物化碰不到它。
实测仍是 RSA / CN=usj.cc / 到期 2026-12-07,本项目续期不会更新 → 12 月会断。
遗留 ② dnsapi.usj.cc 没上 HTTPS。
(澄清假问题:200181.xyz 公网看到的 LE 证书是 CF 自家边缘证书,与本项目无关)

四、新增 docs/架构精简候选.md(回应「架构还是复杂了」)

结论:复杂度不在件数,在「跨 6 个环境,其中 4 个要自己维护」。国内机 9 个容器里属于本项目的
只有 3 个,能动的只有 2 个。5 个候选 + 建议执行顺序:
  1 停 certimate 容器(工作流已全停用)——先 stop 观察,别急着删
  2 cn-dns-helper 大概率已是遗留(「Worker 侧签发」时代产物;现在签发在国内机且自带 dnsprovider,
    /preflight 显示 CF 凭据可读;Worker 侧 dnsremoted.ts 已无任何引用)
  3 Gitea:迁 or 不留
  4 两套写作前端收敛(本轮不动,先看使用频率)
  5 writeapi.usj.cc 证书纳管(12 月死线)
并明确列出 6 项「必要复杂度,不建议动」。

五、架构总览补齐证书管家子系统(★ 之前完全缺席)

一个完整子系统在「事实源文档」里一个字都没有 —— 这本身就是文档债。补:
- §0 一句话(四→五个子系统)、§1.1 子系统表、§1.2 地址地图两行
- **新增 §5.7 证书管家**:职责划分 / 为什么非要这么分(Worker 免费版 CPU 10ms)/
  三条不能破的红线(Worker 不得签发 · 证书路由只认 Bearer 会话 · 角色必须正着枚举放行)/
  纳管域名表 / 与 certimate 的关系 / 已知遗留
- .cnb.yml 行数 284 → 337(数字漂了);头部更新时间 → 2026-10-06;附录表改指 docs/
- §6 待办:#13(证书遗留)、#14(复杂度盘点)新增;#7 改写为「Gitea 11 月到期,★ 有死线」

六、记忆

.workbuddy/memory/MEMORY.md 新增「文档结构」「Gitea 迁移死线」两节;证书管家节补实测与遗留。
This commit is contained in:
zqlit committed 2026-10-06 22:27:36 +08:00
1 parent 15a8b502db
commit 1568b150b3
48 files changed
+782 -100

No files matched your search

@@ -0,0 +1,179 @@
> 📦 **本文档已归档**(2026-10-06)。它记录的是**当时的评估与方案**,其中的结论可能已被后续决策推翻。
> **请勿据此判断当前架构** —— 现行架构唯一事实源是 [`架构总览.md`](../../../架构总览.md)。
# 在线编辑文章 · 集成到 api.200181.xyz/admin
> 日期:2026-10-04(定稿)
> 结论:**已实现并本地全链路验证通过。**
> 形态:前端长在现有 `api.200181.xyz/admin` 面板里(新增「文章编辑」tab),
> 后端是一个**零 npm 依赖的轻量 Docker 容器**(`editor-api/`),替代臃肿的 `write-server`。
---
## 0. 一句话
把 `write-server/` 里**只有「文章在线编辑」这一件事**剥出来,做成 200 行级的轻量后端
容器;`/admin` 面板加一个 tab,Worker 只做「登录鉴权 + 注入令牌转发」。
---
## 1. 最终架构
```
浏览器
│ (只用后台已有的管理员登录态,不需要任何新凭据)
▼
Cloudflare Worker api.200181.xyz blog-admin/src/routes/editor.ts
/api/v2/editor/* 只做两件事:
│ ① isAdminRequest() 鉴权
│ X-Editor-Token 只存在这里 ② 注入 X-Editor-Token 反代
▼
nginx post.usj.cc /editor-api/ ──► 127.0.0.1:8017
editor-api 容器(零 npm 依赖)
读写 /srv/blog/content/posts/**,
图片落文章同级目录,
发布 = git add/commit/pull --rebase/push
```
三层各自的职责被切得很干净:
| 层 | 文件 | 职责 | 故意不做的 |
|---|---|---|---|
| 前端 | `blog-admin/public/admin/admin.js`(新增 ~450 行) | 列表 / 编辑 / 图片粘贴 / 保存 / 发布 | 不碰令牌、不直连后端 |
| Worker | `blog-admin/src/routes/editor.ts`(新增,150 行) | 鉴权 + 反代,10 条路由 | 不碰仓库 |
| 后端 | `editor-api/`(新增目录) | 读写 Markdown + git | 不做账号、评论、AI、部署编排 |
---
## 2. 顺带发现的安全问题(write-server 生产环境)
评估过程中实测发现 **`write-server` 线上完全没有鉴权**(只读核实,未做任何写操作):
| 现象 | 证据 |
|---|---|
| 后端公网裸奔 | `23.254.236.47:8016` 直接返回 200 |
| 文章全量泄露 | `https://post.usj.cc/api/posts` 无凭据返回全部 133 篇 |
| `X-Auth-User` 无签名 | 只读明文用户名,可任意伪造 |
| 写操作全裸 | PUT / DELETE / upload / deploy / ai 全部无鉴权、无 middleware |
| nginx 未加 auth_basic | — |
**这正是这次重写的动机**:不是把 write-server 搬个家,而是换成一个「默认安全」的小东西——
令牌只在 Worker 里、端口只绑回环、没配令牌直接拒绝启动。
---
## 3. 改动清单
**新增**
- `editor-api/server.mjs` + `editor-api/src/{frontmatter,posts,git}.mjs` —— 零依赖后端
- `editor-api/test/{frontmatter-roundtrip,save-roundtrip,api-e2e}.mjs` —— 三个测试
- `editor-api/{Dockerfile,README.md}`、`docker-compose.editor.yml`
- `blog-admin/src/routes/editor.ts` —— Worker 反代
- `blog-admin/.dev.vars.example` 的编辑器两项
**修改**
- `blog-admin/src/index.ts` —— 注册 10 条 `/editor/*` 路由
- `blog-admin/src/types.ts` —— Env 加 `EDITOR_API_BASE` / `EDITOR_TOKEN`
- `blog-admin/wrangler.toml` —— `[vars]` 加 `EDITOR_API_BASE`
- `blog-admin/public/admin/admin.js` —— 新增「文章编辑」tab(+约 450 行)
- `blog-admin/public/admin/admin.css` —— 编辑器样式(+约 80 行)
- `.gitignore` —— 加 `.editor-tmp/` / `.editor-trash/`
---
## 4. 测试结论(全部通过)
| 测试 | 结果 | 说明 |
|---|---|---|
| `frontmatter-roundtrip.mjs` | **130/130** | split/join 逐字节还原 + parse/stringify 深相等 |
| `save-roundtrip.mjs` | **130/130** | 每篇「读出来原样存回去」sha256 不变,测完全部还原 |
| `api-e2e.mjs` | **44/44** | 鉴权/读取/回存/只改正文/改字段/新建删除/上传/git/撞名/无空行 |
| Worker 反代链路(`.editor-tmp/verify-relay.mjs`) | **26/26** | 真实走 wrangler dev,含鉴权 403 / 404 / 409 |
| 后台 UI(无头 Chrome 真点) | **全通过** | 登录→列表→打开→改→保存→发布弹层,4 张截图 |
**过程中抓到并修掉的真 bug:**
1. **front matter 后的空行**:语料里 111 篇有空行、19 篇没有;`getPost` 把前导空行剥掉后
信息丢了,`savePost` 又无条件补一个 → 那 19 篇一保存就被平白多插一个空行。
修法:`readIndex` 记 `bodyLead`,回写时按原文件风格还原。
2. **slug 撞名静默改错文件**(详见第 6 节):定位键从 slug 换成目录名。
3. CRLF/LF、块标量 chomping(`|-` / `|` / `|+`)、`getPost` 漏 `filePath`/`eol`、
重复 slug 返回 500 —— 都是早期测试抓到的。
---
## 5. 上线步骤
### ① 服务器侧(23.254.236.47)
```bash
# 1. 博客仓库 checkout 到 /srv/blog(main 分支,git 工作区)
# (原 write-server 用的那份仓库可以直接沿用,确认 remote 是 CNB + GitHub 两段)
# 2. 放 compose 与令牌
cd /srv/blog
mkdir -p /srv/editor-trash
cat > .env <<'EOF'
EDITOR_TOKEN=<openssl rand -hex 32 生成的值>
EOF
docker compose -f docker-compose.editor.yml up -d --build
curl -s http://127.0.0.1:8017/health # 应返回 {"ok":true,...}
```
### ② nginx(post.usj.cc 的 server 块里加一段)
```nginx
# —— 文章编辑后端:只给 Cloudflare Worker 反代用 ——
# 令牌本身就是鉴权(X-Editor-Token),所以这里不再叠 auth_basic。
location /editor-api/ {
proxy_pass http://127.0.0.1:8017/; # 末尾的 / 会剥掉 /editor-api 前缀
proxy_http_version 1.1;
proxy_set_header Host $host;
client_max_body_size 25m; # 图片直传
proxy_read_timeout 120s; # git push 可能慢
}
```
> 可选加固:`location` 里再叠 `allow <Cloudflare IP 段>; deny all;`,
> 让这条路径只有 CF 能碰到。令牌泄露才是真风险,这层属于纵深防御。
### ③ Cloudflare 侧
```bash
cd blog-admin
npx wrangler secret put EDITOR_TOKEN # 粘贴与服务器 .env 相同的值
npx wrangler deploy
```
`EDITOR_API_BASE` 已写在 `wrangler.toml` 的 `[vars]`(`https://post.usj.cc/editor-api`)。
### ④ 验证
打开 `https://api.200181.xyz/admin` → 「内容管理 / 文章编辑」→ 随便开一篇 → 改一个字 →
保存 → 「发布 / 同步」里确认文件出现在待发布列表。
### ⑤ 下线 write-server(**确认新编辑器好用之后再做**)
1. nginx 里摘掉 write-server 的 `location /`(先只留 `location /editor-api/`)
2. `docker stop write-server && docker update --restart=no write-server`
3. 关掉 `8016` 的公网映射(compose 里删 ports 或改绑 127.0.0.1)
4. 观察几天没问题,再删镜像与数据卷(**删前先备份**)
---
## 6. 遗留问题:slug 撞名(需要你决定)
仓库里有 **5 组**文章共用同一个 slug,Hugo 的 permalink 是 `/:slug`,所以每组里
**有一篇在线上是被另一篇覆盖掉的(打不开)**:
| slug | 两篇 |
|---|---|
| `20210901` | Twitter主题加入加载耗时… / 无悔 |
| `20211122` | 情侣恋爱倒计时小工具… / 这组照片的主题,咱就叫它光吧 |
| `20211128` | 大学生体测… / 可惜不能一直做小孩子… |
| `20211223` | 更换掉jsdelivr… / 放假之前最后一次的照片合集… |
| `20240602` | parsec远程软件报6023错误 / idea关闭ai自动补全 |
编辑器已经把这件事**标出来了**(列表里黄色「URL 冲突」标签;拿撞名 slug 去查会返回 409
并列出候选篇目,不会猜)。但**改哪一篇的 slug、还是让后写的那篇换个 slug**,需要你定。
> 顺带:改 slug = 改网址,旧链接会 404。如果在意 SEO,得配套做重定向。
@@ -0,0 +1,284 @@
> 📦 **本文档已归档**(2026-10-06)。它记录的是**当时的评估与方案**,其中的结论可能已被后续决策推翻。
> **请勿据此判断当前架构** —— 现行架构唯一事实源是 [`架构总览.md`](../../../架构总览.md)。
# 评论加载优化方案(Cloudflare 免费版)
> 起因:读者侧「评论加载太慢」。
> 约束:**Cloudflare 免费版**,无中国节点;全程不引回自维护机器(除非你主动选第三档)。
> 状态:**第一档 + 2-A 已实施**(本地,待预览确认);2-B 待做,2-C 已由实测数据关闭。
---
## 0. 先厘清「慢」到底慢在哪一层
这一点决定每一项优化的真实价值,否则容易做一堆「看起来很快」的改动却没有体感。
| 层 | 链路 | 现状 | 谁能修 |
|---|---|---|---|
| **L1 跨境** | 读者浏览器 → CF 边缘机房 | CF 免费版无中国节点,每次请求跨境 ≈ 250–400ms | 只能靠**少发请求**(前端),或第三档反代 |
| **L2 回源** | CF 边缘机房 → D1 主库 | D1 主库在 WNAM(美国西),代码注释:「每次读都要跨太平洋」 | **边缘缓存**(省掉这一跳) |
| **L3 结构** | CF 在中国大陆无节点 | 结构性,无解 | 只有第三档(境内反代) |
**关键结论:边缘缓存(第二档)治的是 L2 和额度,治不了 L1。** 读者感受到的「慢」主要在 L1,所以**降请求数比加缓存更直接**。
---
## 1. 当前一次评论首屏的请求链(来自代码,非推测)
Artalk 客户端 `libs/Artalk.js` 的初始化是**串行**的——已从压缩源码确认:
```js
// conf 请求(await)→ mounted → 之后才发评论列表
...getApi().conf.conf().catch(...)
... e.trigger("mounted"), e.conf.remoteConfModifier || e.fetch({offset:0})
```
即:**`conf` 不回来,评论列表请求根本不会发出**。所以首屏是严格串行的两跳:
```
浏览器 ──① GET /api/v2/conf ──────► CF 边缘 ──► D1 ≈ 1×L1 + 1×L2
──② GET /api/v2/comments ─► CF 边缘 ──► D1 ≈ 1×L1 + 1×L2(含多条查询)
──③ POST /api/v2/pages/pv ─► CF 边缘 ──► D1 (异步,不阻塞渲染)
```
页面加载时还会打 `/captcha/status` 或 `/human/*`(仅在需要时)。
**L1 一次 ≈ 250–400ms(国内实测 CF 0.75s vs EdgeOne 0.25s)**,所以 ①→② 两跳 ≈ 0.5–0.8s 纯网络。把 ① 干掉,首屏网络时间**直接减半**。
> 已有的优化不动:`window.friendLinks/friendFeeds` 是 Hugo **构建期内联**的(零运行时请求);Artalk 的 JS/CSS 走又拍云本地资源;`conf` 已有 localStorage 缓存(**但只在 PJAX 第二次以上生效,首屏吃不到**);`/api/favicon` 已有 KV 缓存 + `max-age=86400`;计数查询已有 KV 短缓存(10 分钟 + 版本号失效)。
---
## 2. ✅ 第一档(已实施):修 `preconnect`
**问题**:`themes/Ying/layouts/partials/head.html` 里给 API 域名预热了连接,但**没带 `crossorigin`**。
Artalk 发的是 CORS 跨域请求。浏览器把「带凭据的连接」和「匿名连接」分池缓存:不带 `crossorigin` 预热出来的是普通连接,**CORS 请求无法复用它**——等于白预热,还是要重付一次 DNS + TCP + TLS(约 2–3 个 RTT)。
**改法**:`preconnect ... crossorigin` + `dns-prefetch` 兜底(老浏览器不支持 preconnect);host 取 `artalk.server` 与 `rssapi.base` 去重后渲染,以后两处配置分家也不会漏。
**验证**:本地 `hugo` 构建产物确认(1270 个页面全部生效,两个 host 正确去重):
```html
<link rel="preconnect" href="https://cravatar.cn">
<link rel="preconnect" href="https://api.200181.xyz" crossorigin>
<link rel="dns-prefetch" href="https://api.200181.xyz">
```
**估算收益**:首屏第一跳省下 ~2–3 个 RTT 的握手(≈200–400ms),且**后续 ②③ 请求复用同一条连接**,各自只花 1 个 RTT。
---
## 3. 📋 第二档评估
### 2-A. ✅ 首屏内联 `conf`(已实施)
> ⚠️ **先记一次认知纠正:本节原评估的方向是错的。**
>
> 原方案写的是「前端首屏直接 `useBackendConf: false` 用内联配置起步,跳过 ①」。
> 读 `libs/Artalk.js` 压缩源码后确认**做不到**:
>
> ```js
> const { data: i } = await e.getApi().conf.conf() // ← 无条件发请求
> if (e.conf.useBackendConf) { ...merge frontend_conf... } // ← 只管要不要「用后端覆盖前端」
> ```
>
> `useBackendConf` 决定的是**要不要采用后端的配置**,与**发不发这次请求**无关。
> 所以内联配置在 Artalk 内部绕不过请求 ① —— **必须拦在 fetch 层**。
> (PJAX 缓存那条路径也没省掉请求,它省的是「重新渲染」,不是「重新请求」。)
**做法**(三段,缺一不可):
1. **构建期抓快照** —— `.cnb.yml` 的「Hugo 构建」stage 里 `curl` 一次 `/api/v2/conf`,
落到 `data/artalk_conf.json`(Hugo 读成 `site.Data.artalk_conf`)。
拉取失败就删文件降级(失败发生在 `if` 条件里,不触发 `set -e`,**绝不让它拖垮构建**)。
该文件已 gitignore:每次构建现拉,避免陈旧配置被 git 固化。
2. **模板内联** —— `partials/artalk.html` 输出 `window.__artalkConfSnapshot = {…}`,
放在 `window.artalkConfig` 之前。`</` 会被替换成 `<\/` 防 `</script>` 逃逸。
3. **fetch 拦截** —— `modules/artalk.js` 的包装层里命中 `/api/v2/conf` 时,
直接 `new Response(JSON.stringify(快照))` 返回,**完全不发网络**。
(Artalk 的通用请求函数 `w()` 只检查 `r.ok` 就把 Response 原样返回,所以合成响应可无缝接管。)
**为什么可以安全复用「一份固定响应」**:服务端 `getConf` 是
`{...DEFAULT_FRONTEND_CONF, ...settings.frontend_conf, imgUpload: imgUpload || admin}` ——
**除 `imgUpload` 外全部来自 settings 表,与访客无关**。于是:
- **匿名访客** → conf 人人相同 → 用快照 ✅
- **已登录用户** → 管理员会拿到 `imgUpload: true`(个性化)→ **放行真实请求**
判断方式:`w()` 在无 token 时会把 `Authorization` 头删掉,所以「该头存在」即「有凭据」;
判不准时保守放行(宁可多一跳,也不错配)。
**实测验证**(Chrome CDP 抓真实页面网络):
| | `/api/v2/conf` 请求数 |
|---|---|
| 改动前(对照:CDP 注入脚本吞掉快照变量的赋值) | **2 次** |
| **改动后** | **0 次** ✅ |
页面探针同时确认 Artalk 完全正常:`artalkInited: true`、`editorPresent: true`、
`listPresent: true`、`errText: ""`;且 conf 内容确实来自快照
(`sendBtn:"评论一下"` / `nestMax:2` / `locale:"zh-CN"` / `emoticons` 全对);
conf 之后的评论列表请求照常发出 —— **串行链没断**。
**成本**:conf 响应 771B,内联进 123 个带评论区的页面(占页面体积 **1.6%**),全站内容一致。
| | 结果 |
|---|---|
| 收益 | 首屏少一整跳跨境(≈250–400ms);**实际比预期多省一倍**,见下方「顺带发现」 |
| 风险 | 低。构建期失败自动降级;带凭据请求自动放行 |
| 代价 | 后台改评论配置后要等下次构建才生效(已有每日 9:00 定时构建兜底) |
**怎么回退**:删掉 `data/artalk_conf.json` 再构建即可 —— 模板不输出快照,前端自动走原生远程请求,行为等同今天(无需改代码)。
### 2-B. 给读接口加边缘缓存(Cache API)
**注意**:Worker 的响应默认**不进** Cloudflare 缓存。光加 `Cache-Control` 头是**没有效果的**,必须显式用 `caches.default.put()/match()`。(这也是为什么这项不能"顺手加个头"就完事。)
必须做对的四件事:
1. **只缓存匿名公开视图**。`listComments` 的结果依赖是否管理员(`is_pending` 过滤、`includeMarked`)、`name`/`email`、`scope=user`、`type=mine|pending|mentions`、`view_only_admin`——这些**一律不缓存**,只缓存 `scope=page` 的默认视图。`/conf` 同理(含 `imgUpload || admin`)。
2. **缓存 key 要带 origin**。`corsHeaders()` 已经把 `Access-Control-Allow-Origin` 回显成请求方的 Origin(并设了 `Vary: Origin`),缓存 key 里必须把 origin 编进去,否则 A 域名的响应会被回给 B 域名。
3. **失效策略复用现有机制**。项目里已有 `cml:ver:<site>:<page>` 这个 KV 版本号,评论新增/删除/审核会 `bumpCountVersion()`。**直接把它编进缓存 key** → 有评论变化时 key 自然变化,旧条目靠 TTL 自然过期,不用做 purge。
4. **TTL 取短**:30–60s。评论不是毫秒级强一致场景。
| | 评估 |
|---|---|
| 收益 | **D1 额度**:命中时省掉「计数 + 根列表 + 子评论 + page + site」多条查询(单页几十~几百 rows_read)。这个是真金白银——代码注释里记着当年额度被打爆过 |
| 收益 | **延迟**:命中时省掉 L2 那一跳(≈100–200ms)。但**L1 那一跳仍然要付**,所以体感提升有限 |
| 风险 | 低–中。要点全在「不可缓存哪些请求」上,漏一个就是串号/看到别人的数据 |
| 代价 | 评论提交后,**同一机房的其他读者最多看到 TTL 秒的旧列表**(可接受) |
### 2-C. Smart Placement(`[placement] mode = "smart"`)
让 Worker 跑在**离 D1 更近**的机房,代价是可能离**读者**更远。
**要不要开不用猜**:你的 `/api/v2/healthz` 已经专门返回这两个字段了(当初就是为这个判断留的):
```
colo —— Worker 实际执行机房
region —— D1 实际服务区域
```
- **同区域** → 开了没收益,还可能变慢,**别开**
- **不同区域** → 有意义,可以开
**实测结果(2026-10-04,用户提供)**:
```json
{"region":"WNAM","colo":"LAX"}
```
两者同为美西 → **结论:不开**。Worker 已经在 D1 同区域执行,Smart Placement 带不来收益,
还可能把请求推到离读者更远的机房。**本项关闭,不用再做。**
### 2-D. 「加 HTTP 缓存头」——不是独立项
它只是 2-B 的实现细节(`caches.default.put()` 要求响应显式可缓存)。**单独加头没有任何效果**,不要当成一项来做。
### 顺带发现(本轮实测捡到的既有问题,与 2-A 独立)
**① `initArtalk()` 被调用两次 → conf / comments / pv 全部发两遍。**
根因是既有代码的重复触发:
```
partials/artalk.html 内联脚本 → initArtalk 尚未定义 → 注册 DOMContentLoaded 回调(兜底)
assets/js/main.js:70 → 也在 DOMContentLoaded 里调 window.initArtalk()
↓
两个监听都会触发 → initArtalk 跑 2 次
```
CDP 实测(改动前):`GET /api/v2/conf` ×2、`GET /api/v2/comments` ×2、`POST /api/v2/pages/pv` ×2。
2-A 把 conf 那两次消掉了,但 **comments 仍多付一次跨境,`pv` 仍是双倍计数**
(**浏览量统计偏高 100%**,这是数据准确性问题,不只是性能)。
修法很轻:在 `initArtalk` 入口加幂等守卫(同一 `pageKey` 已初始化过就 `return`),
PJAX 切页时 `pageKey` 变化自然放行。**但改动落在 PJAX 路径上,建议单独一轮做 + 单独验证。**
**② `head.html:59` 有一个第三方统计脚本。**
```html
<script async src="https://019e3dec-3312-7873-862a-3f56ac99ea83.spst2.com/ustat.js"></script>
```
它不在主题自身模块里,是页面**唯一的外部脚本**,会把访问数据上报给第三方。
**确认是不是你自己加的**;若不是,建议连同这一行一起删掉。
**③ ✅ 已修:非评论类错误层会让整个评论区「假故障」一次(顿一下 + 弹「已恢复」toast)。**
**现象**:进文章页时评论区整体置灰、插「评论服务暂时不可用」横幅,约 1 秒后自己恢复并弹
`评论服务已恢复,可以继续评论了 ✓` —— 读者看到的就是「顿一下 + 一条莫名其妙的提示」。
**根因**(CDP 时间线实测,非推测):
```
表情包 /emotion/OwO.json 加载失败(本地预览跨域;线上则可能是 CDN 抖动)
→ Artalk 在「编辑器插件面板」内渲染 .atk-error-layer,文本:
Artalk Error / [表情] 加载失败: TypeError: Failed to fetch
→ rewrite() 只用 layer.querySelector('.error-message') 取文本 —— 而插件面板的错误层
没有这个 class,取到空串
→ 空串不匹配任何特征词 → 兜底 code = 'internal'
→ if (code !== 'network') setSvcDown(code) ← 只有 network 被豁免,internal 不豁免
→ 整个评论区置灰 + 插横幅(此时评论数据其实还没回来,也没失败)
→ 约 1s 后 Artalk 收掉错误层 / 评论数据正常返回 → setSvcUp() → 弹 toast
```
CDP 抓到的判定证据:错误卡片上的 `data-raw` = `"|internal"`(竖线左边就是空串 `raw`),
而错误层的真实 DOM 位置是 `atk-editor-plug-emoticons < atk-plug-panel-wrap < atk-main-editor`。
**修复**(`themes/Ying/assets/js/modules/artalk.js`,三处):
1. `rewrite()` 开头直接跳过 `.atk-plug-panel-wrap / .atk-editor-plug-emoticons` 内的错误层 ——
既不改写文案,也不参与故障判定(这类层是资源加载失败,不是评论服务故障)。
2. 故障判定加证据要求:`if (code !== 'network' && (raw || (fresh && fresh.code)))` ——
**读不到可归因证据时不下结论**,宁可只渲染卡片也不误置灰。
3. `setSvcUp()` 加最短门槛:禁用态不足 3 秒不弹 toast(真实故障不会两三秒就恢复)。
**验证**:CDP 复跑,`svc-down` / 横幅 / toast 三项全部 0 次,评论数据仍 200;
另做三组回归:真实故障层仍置灰 ✅、无文本层不置灰 ✅、插件面板层完全放行 ✅。
> ⚠️ **线上本来不出现这个现象**:线上页面在 `usj.cc`、表情包也在 `usj.cc`,同源不触发 CORS。
> 它只在本地预览(跨域)必现,线上要等 `OwO.json` 真出问题(CDN 抖动 / 404)才会踩到 ——
> 所以这算是一次「本地预览帮忙提前暴露了线上潜在缺陷」。
### 不建议做的
- **改 Artalk 客户端把 ① ② 并行**:要 fork 官方库,收益和 2-A 重叠,维护成本高。
- **给 `/pages/pv` 加速**:它已经是异步的,不阻塞渲染。
---
## 4. 建议的执行顺序
| 顺序 | 项目 | 收益 | 风险 | 状态 |
|---|---|---|---|---|
| ✅ 1 | 第一档 preconnect | 首屏 −200~400ms | 极低 | **已完成** |
| ✅ 2 | 2-A 首屏内联 conf | conf 请求 **2 次 → 0 次** | 低 | **已完成** |
| ✅ 3 | 2-C Smart Placement | — | 零 | **实测同区域 → 关闭** |
| 4 | 修「`initArtalk` 跑两次」 | 再省 1 跳,且**修正 PV 双倍计数** | 中(涉及 PJAX) | **建议做,单独一轮** |
| 5 | 2-B 读接口边缘缓存 | 保 D1 额度;命中 −100~200ms | 中 | 推荐 |
做完 2 + 4 + 5,首屏从「2 跳跨境」变成「1 跳 + 命中即回」,D1 读量大幅下降,PV 计数恢复正确。
**预期天花板**:即使全部做完,L1(跨境那一跳)依然存在——免费版 CF 在国内就是没有节点。
---
## 5. 第三档(真正的解法,需要你权衡)
`usj.cc` 的 NS 在 DNSPod,可以对 `api.usj.cc` 做**分线路解析**:境内 → 境内机器反代 → CF;境外 → 直连 CF。这才治本(L1 也消失)。
两条硬约束:
1. **`api.200181.xyz` 本身做不了**——200181.xyz 的 NS 在 Cloudflare,免费版不支持按国家分线路解析。所以必须换成 `api.usj.cc` 这类「NS 在 DNSPod」的域名。
2. **代价是把一台自维护机器重新拉回关键路径**——与 2026-10-04 刚完成的「0 台自维护机器」方向相反。
**没有白吃的午餐**:这一档能根治,但要把刚拆掉的东西装回去。
---
## 6. 不变量(别被顺手改坏)
- `SERVER_API_VERSION` 必须与博客里打包的 Artalk 客户端版本一致(当前 2.8.7),否则前端弹版本警告。
- `listComments` 里 `countFromSql` 的「不用 JOIN users 就不 JOIN」是额度优化,别回退。
- 任何新增缓存都必须先回答:**这个响应会不会因为「谁在问」而不同?** 会 → 不能缓存,或必须把身份编进 key。
@@ -0,0 +1,124 @@
> 📦 **本文档已归档**(2026-10-06)。它记录的是**当时的评估与方案**,其中的结论可能已被后续决策推翻。
> **请勿据此判断当前架构** —— 现行架构唯一事实源是 [`架构总览.md`](../../../架构总览.md)。
# 评论区孤儿评论修复方案(2026-10-06)
> ## ✅ 状态:**已执行完成**(2026-10-06 12:2x)
>
> - 21 条 UPDATE 全部成功,**816 条评论已迁回正确页面**
> - 22 个目标 key 条数精确到位;残留老 key 已清空
> - 端到端验证通过:`/20230424.html`(苏州一日游)已能显示评论
> - 备份:`.editor-tmp/BACKUP_816条_20261006.txt`
> - 未迁移的 13 个 key / 377 条保持原位未动
## 一句话结论
**你的评论一条都没丢。** 线上 D1 里有 **3423 条评论**,
其中 **1193 条**因为老站 URL「日期错了一天」变成了孤儿(挂在 404 页面上),
**816 条**可以精确找回,**377 条**因现站已无对应文章而保留原样。
---
## 一、线上库真实基线
```
comments 表:3423 条(存活 3380 条)
id 范围:10393 ~ 15674
94 个 page_key:
· /comment.html 留言板 1001 条
· 65 个「老站日期型」key(/20230423.html) 1975 条
· 28 个「新站时间戳型」key(/20260708110221.html) 404 条
```
**你举例的 `/20230424.html`(苏州一日游)有 40 条评论 —— 找到了,挂在 `/20230423.html` 上。**
---
## 二、问题成因
老站迁移时,**部分文章的 URL 日期错位了一天**:
| | 错误 URL(有评论) | 正确 URL(无评论) |
|---|---|---|
| 苏州一日游 | `/20230423.html`(40 条,title 显示 "404 Page not found") | `/20230424.html`(页面正常,0 条) |
| 新的开始 | `/20230527.html`(76 条,title=404) | `/20230528.html`(页面正常,0 条) |
- 评论**绑在**老 URL 上
- 页面**只认**新 URL(老 URL 返回 302 → /404.html)
- 所以评论「有数据但显示不出来」
---
## 三、修复方案(待批准执行)
**23 个孤儿 key / 816 条** → 改挂到现站正确 slug。
`UPDATE comments SET page_key='/新key' WHERE page_key='/老key'`(**按 page_key 定位,不用 id**)
| 孤儿 key | 条数 | → 目标 | 文章标题 |
|---|---|---|---|
| `/20230527.html` | 76 | `/20230528.html` | 新的开始 |
| `/20220810.html` | 58 | `/20220811.html` | typecho实现QQ头像用户评论加密 |
| `/20220906.html` | 57 | `/20220907.html` | 主力机由荣耀20切换到iPhone13 |
| `/20221102.html` | 52 | `/20221103.html` | 毕业篇:最后几个月的大学生活 |
| `/20221203.html` | 50 | `/20221204.html` | 2022·襄阳第一场雪 |
| `/20220227.html` | 48 | `/20220228.html` | 2022,除夕过后的那些事 |
| `/20211121.html` | 46 | `/20211122.html` | 这组照片的主题,咱就叫它光吧 |
| `/20220430.html` | 40 | `/20220501.html` | iphone快捷指令发布动态说说(合并) |
| **`/20230423.html`** | **40** | **`/20230424.html`** | **苏州一日游 ★** |
| `/20220503.html` | 34 | `/20220501.html` | iphone快捷指令发布动态说说(合并) |
| `/20211222.html` | 33 | `/20211223.html` | 放假之前最后一次的照片合集 |
| `/20230803.html` | 32 | `/20230804.html` | 家乡随拍 |
| `/20230125.html` | 31 | `/20230126.html` | 感谢哥哥给的网站 |
| `/20220619.html` | 30 | `/20220620.html` | 我跳绳的那些日子 |
| `/20221016.html` | 28 | `/20221017.html` | 毕业纪念篇:图书馆 |
| `/20220104.html` | 26 | `/20220102.html` | 祝大家元旦快乐 |
| `/20211113.html` | 24 | `/20211114.html` | 双十一已经变味了 |
| `/20230409.html` | 22 | `/20230410.html` | 四月随笔 |
| `/20211210.html` | 21 | `/20211211.html` | 盘点注册的域名 |
| `/20211007.html` | 19 | `/20211008.html` | 记录人生第一次洗牙 |
| `/20210911.html` | 19 | `/20210912.html` | 别让抖音支配了你的大学生活 |
| `/20221213.html` | 16 | `/20221214.html` | Apple Watch Series7 体验 |
| `/20230716.html` | 14 | `/20230717.html` | 襄阳唐城 |
✅ **已核实:18 个目标页当前评论数全部为 0** —— 不会覆盖、不会冲突。
---
## 四、不迁移的(377 条,保留原样)
现站已无对应文章,评论**保留在原位不删**(万一以后想恢复旧文章,评论还在):
| 孤儿 key | 条数 | 内容主题 |
|---|---|---|
| `/20220115.html` | 54 | 开发 app / uniapp |
| `/20220410.html` | 51 | iPhone 更新 |
| `/20211117.html` | 47 | Twitter 主题界面 |
| `/20211111.html` | 43 | 网站头像插件 |
| `/20211203.html` | 32 | 人像摄影 |
| `/20220328.html` | 32 | 青年大学习 |
| `/20211124.html` | 30 | RSS 阅读器(蚁阅) |
| `/20260724101237.html` | 24 | 流量卡/广电(现站 slug 冲突) |
| `/20211024.html` | 20 | 医疗/手术 |
| `/20211017.html` | 17 | 弟弟听力/同济医院 |
| `/20211015.html` | 14 | 恋爱话题 |
| `/20220607.html` | 11 | 腾讯游戏/王者 |
| `/202610011226.html` | 2 | Gitea 测试文 |
---
## 五、执行方式
```bash
# 1. 备份(先把这 816 条 dump 出来)
# 2. 执行 21 条 UPDATE(按 page_key 定位)
# 3. 核对:目标 slug 评论数 = 预期值;总量仍为 3423
```
SQL 在 `.editor-tmp/FINAL_SQL.json`。
---
## 六、顺带修掉的两个坑
1. `scripts/refresh_cdn.js` —— 多吉云 `rtype=path` 目录刷新**必须带尾斜杠**(已加兜底 + 日志)
2. `blog-admin/src/routes/rss/tools.ts` —— 加 `s-maxage` / `CDN-Cache-Control`,让 CF 边缘真正缓存
@@ -0,0 +1,335 @@
> 📦 **本文档已归档**(2026-10-06)。它记录的是**当时的评估与方案**,其中的结论可能已被后续决策推翻。
> **请勿据此判断当前架构** —— 现行架构唯一事实源是 [`架构总览.md`](../../../架构总览.md)。
# 四问诊断报告(2026-10-06)
一次性回答四个问题:CDN 刷新、KV 配额、D1 评论对账、certimate 迁移。
---
## 一、多吉云刷新 & 已删文章还能访问
### 1.1 先更正我自己的话
我一开始说「多吉云那条已经是全量(`--all`)」——**这句话是错的**,实测证据如下:
```
cnb-secrets.yml:33 CDN_URL_LIST: "https://usj.cc"
```
`scripts/refresh_cdn.js` 只做一件确定性的事:把 `CDN_URL_LIST` 按逗号/换行拆开,
逐条调 `/cdn/refresh/add.json`(`rtype: "path"`)。**当前配置里这个变量只有首页一个 URL**。
结论:**多吉云确实不是全量刷新,只刷新了首页 `https://usj.cc`。**
### 1.2 那为什么删掉的文章还能访问?
不是多吉云的问题。实测这条 URL:
```
$ curl -sI https://usj.cc/202610021614.html
HTTP/1.1 200 OK
Server: marco/3.2 ← 又拍云特征头
X-Cache-Lookup: Hit From Upstream Cluster ← 命中缓存
Last-Modified: Sun, 04 Oct 2026 14:12:26 GMT ← 旧副本时间
Cache-Control: max-age=691200 ← 8 天 TTL
Age: 131684 ← 已缓存约 1.5 天
```
判别实验(关键):
| URL | 状态 | 说明 |
|---|---|---|
| `/202610021614.html`(已删除) | **200** | 缓存里还有旧副本 |
| `/zzz-not-exist-111.html`(从未存在) | **302 → /404.html** | 源站没这个文件 |
两者行为不同 ⇒ 说明**源站确实已经没有这个文件了**(本地 `content/` 和 `public/` 均已确认无此页),
现在返回 200 完全是**又拍云上的 8 天旧副本**。
### 1.3 根因:两条 CDN 线路都刷不到「已删除页」
`scripts/purge_list.js` 的逻辑是:遍历 **构建产物 `public/`**,把里面所有 `.html` 列出来提交刷新。
```js
// 全量时 walkHtml('public') —— 遍历构建产物
// 已删除的页面自然不在里面 → 永远不会被提交刷新
```
也就是说:**刷新清单只会包含「现在存在的页面」**。一个页面被删掉后,
它就从清单里消失了,于是 CDN 上的旧副本只能等 TTL 自然过期。
- 又拍云:HTML `max-age=691200` = **8 天**
- 多吉云:只刷首页,问题更明显
### 1.4 处置建议
| 方案 | 说明 | 成本 |
|---|---|---|
| **A. 什么都不做** | 等 8 天自然过期 | 0 |
| **B. 手动刷新一次**(推荐) | 把已知的已删 URL 提一次刷新,立刻生效 | 一次性 |
| C. 流水线加「删除清单」 | 构建时对比上一次产物,把消失的 `.html` 追加进 purge 清单 | 改脚本 |
对已删文章这种低频事件,**B 足够**。需要的话我可以立刻把已知的那几个已删 URL 提交到又拍云+多吉云。
---
## 二、KV 配额告警:需要调整吗?
### 2.1 告警内容
> 已使用 Workers KV 免费套餐每日限制的 **50%**,超限将返回 429;重置时间 2026-10-06 00:00 UTC。
> 免费额度:读 100,000/天、写 1,000/天、删 1,000/天、list 1,000/天。
### 2.2 谁在吃配额?(实测 4 类来源)
| 来源 | 代码位置 | 频率 | 影响 |
|---|---|---|---|
| **评论数缓存** | `comments.ts:180/206` | 每次评论列表请求,miss 时写(TTL 600s) | 读 + 写 |
| **评论数版本号** | `comments.ts:53/61` | 评论增删时写 | 写 |
| **friend-link favicon** | `routes/rss/tools.ts:86` | **每次访问友链/友圈页,每个友链一次** | **读(大户)** |
| 人机验证 / 通知节流 | `human.ts` / `admin-notify.ts` | 低 | 少量 |
**读数最大的就是 favicon 接口。** 友链共 54 条(可见 42 条),其中 15 条头像走 `/api/favicon`:
```js
// circles.html / linkify.js
img.src = RSS_API_BASE + '/api/favicon?url=' + encodeURIComponent(target);
```
每刷一次友圈/友链页 → 触发 N 次 `/api/favicon` → 每次至少 1 次 KV `get`。
favicon 本体有 24h 浏览器缓存,但**首访、清缓存、换设备都会重新打**。
### 2.3 建议:**暂时不用调整,但建议做一个小优化**
- **读 50% 是「接近」不是「超了」**,且每天 UTC 0 点清零。按当前站点的访问量,正常不会打穿。
- 即使打穿,后果是 `/api/favicon` 走兜底字母图、评论数退化为实时查库——**不是灾难性故障**。
- 免费版写限额 1,000/天,比读更容易被评论数缓存写穿。真正的风险点是**评论突然暴涨**或**有人刷评论**。
**可做的小优化(成本低、收益直接):**
1. favicon 接口响应已经带了 `Cache-Control: max-age=86400`,可以加一层 **Cloudflare Cache Rule**,
让 `/api/favicon*` 在边缘缓存 24h,这样 KV 读基本归零。
2. 若担心写限额,把 `cml:ver:` 的写入合并/延迟(比如 5 分钟内同页重复 bump 只写一次)。
**要不要升级到付费($5/月)?** 目前**不需要**。等到真收到「已超限」而不是「接近」的邮件再说。
---
## 三、D1 评论对账:老文章评论为什么没了
### 3.1 先说结论(这个和你想的不一样)
原库 **3979 条**评论 / 线上 **3423 条**。乍看差 556 条,但拆开看,**「缺失」的绝大部分根本不是评论**:
| 项 | 数量 |
|---|---|
| 线上缺失合计 | **711 条 / 63 页** |
| 其中 **点赞型记录**(内容含 `[LIKE]`) | **494 条(69%)** |
| → **真实评论型缺失** | **217 条** |
**关键发现:线上 `[LIKE]` 记录为 0 条。**
```
$ SELECT SUM(CASE WHEN content LIKE '%[LIKE]%' THEN 1 ELSE 0 END) FROM comments;
→ 0
```
说明当年数据导入时,**已经主动过滤掉了所有点赞型记录**——这是正确的设计(点赞不该占用评论表)。
所以拿原库和线上直接比条数,本身就会假性多出 494 条。
### 3.2 缺失的 217 条真实评论,按站点拆
| 站 | 缺失 | 其中点赞 | 真实 |
|---|---|---|---|
| 伍比贰 | 521 | 431 | **90** |
| 优世界 | 101 | 0 | **101** |
| 演示站 | 48 | 41 | 7 |
| 往后 | 41 | 22 | 19 |
**注意:「伍比贰」「演示站」「往后」是另外的站点,不属于 usj.cc。** 它们的数据本来就不该进本站。
### 3.3 「优世界」那 101 条对得上现站吗?
逐页核对,结果是 **一页都对不上**:
| page_key | 条数 | 现站情况 |
|---|---|---|
| `/yl` | 48 | 页面不存在(是 2022 年的旧友链页) |
| `/20241018-some-useful-win-tools` | 12 | 文章还在,但 slug 已改为 `20241019`,且**是草稿**(未发布) |
| `/27`、`/13`、`/m/17` 等 | 41 | 不存在(旧站的分页/短链) |
| `/20240921-clock-in` | 5 | 文章已删除 |
**根因不是「key 对不上」,而是这些文章本身已经不存在了。**
评论是挂在「已删除文章」上的,key 再怎么规范化也接不回来。
### 3.4 站上现存页面 vs 线上 —— 有没有真的错位?
**没有。** 用现站 284 个 slug 逐一匹配线上缺失的 63 个 key:
```
能对上现存页面: 1 页 / 4 条 (只有 /about.html)
对不上 : 62 页 / 707 条
```
**唯一一个疑似错位:`/about.html`(4 条)**——但深挖后发现**它也不需要修**:
```
id=14229 /about.html [伍比贰] 2026-02-07 👍 已点赞 Cool [LIKE]
id=14268 /about.html [伍比贰] 2026-02-12 👍 已点赞 Interesting [LIKE]
id=14269 /about.html [伍比贰] 2026-02-12 👍 已点赞 很棒的文章! [LIKE]
id=14276 /about.html [伍比贰] 2026-02-13 👍 已点赞 写得很好 [LIKE]
```
这 4 条全是**点赞型记录**,而且属于**「伍比贰」站**,本就不该进本站。
→ **结论:线上 D1 没有任何错位,完全不需要修复。**
### 3.5 线上「多出」的 7 页是什么?
| page_key | 条数 |
|---|---|
| `/20260630162329.html` | 25 |
| `/20260724101237.html` | 24 |
| `/20211001.html` | 14 |
| `/20261005113439.html` | 13 |
| `/20260629223659.html` | 10 |
| `/202610011226.html` | 2 |
| `/20261005213010.html` | 2 |
这些是**导出 db 之后新增的评论**(2026-06 之后),属于正常增长,不用动。
### 3.6 修复建议
**结论:线上 D1 不需要做任何修复。**
你的直觉「很多评论没跟页面 key 对应上、老文章没评论了」——
真实情况是:**那些评论对应的文章,本身已经不在了**(已删除 / 仍是草稿 / 属于别的站),
key 再怎么规范化也接不回来。线上数据是**干净的**。
**不建议做的**:
- ❌ 批量把「伍比贰/演示站/往后」的评论导进来 —— 那是别的站的数据(含大量点赞记录)
- ❌ 把 `/yl`、`/27` 等改成别的 key —— 没有正确的目标,改了反而污染
- ❌ 恢复 494 条点赞记录 —— 线上设计本就不存点赞(`[LIKE]` 记录当前为 0)
**如果你更看重「把老评论展示出来」**,唯一的正路是**重建这些页面**
(把已删文章从 git 历史恢复,或用 slug 重定向)。这个要单独评估,工作量比改 key 大得多。
**但请注意**:即使恢复了页面,能救回的也只是极小一部分——
「优世界」站真实缺失的 101 条里,48 条属于 `/yl`(2022 年旧友链页),
41 条属于 `/27` `/13` `/m/17` 等旧站分页短链,只有 12 条是正经文章评论。
### 3.7 那「评论少了」的体感从哪来?
很可能来自这两点(都正常):
1. **点赞不再算评论**:原库 494 条点赞在导入时被过滤,前端看到的评论数自然变少。
2. **老文章页面已删**:文章没了,评论区自然也没了。
如果确实想提升「评论数」,更实际的做法是**在新文章上做引导**,而不是抢救旧数据。
---
## 四、certimate 能否迁到 Cloudflare
### 4.1 你现在的架构
```
certimate(跑在某台服务器上)
├─ 申请:litessl CA,*.usj.cc + usj.cc,DNS-01,tencentcloud-dns
├─ 部署:dogecloud-cdn(多吉云 CDN)
└─ 部署:1panel website(那台服务器上的站点)
定时:10 12 * * *(每天 UTC 12:10)
失败邮件:177018615@qq.com
```
当前线上证书实测:
```
issuer = LiteSSL RSA CA 2025(TrustAsia)
subject = CN=usj.cc
SAN = usj.cc, *.usj.cc
有效期 = 2026-09-08 → 2026-12-07
```
### 4.2 certimate 支持 Cloudflare 吗?
**支持,而且支持两种角色:**
| 角色 | 支持 | 说明 |
|---|---|---|
| **DNS provider(申请用)** | ✅ | 填 Cloudflare API Token 即可,用于 DNS-01 验证 |
| **Deploy provider(部署用)** | ✅ | 可将证书部署到 Cloudflare |
(certimate 官方:60+ DNS 托管商、120+ 部署目标,Cloudflare 在列。)
### 4.3 你的真实目标:「把服务器干掉」
需要先厘清一件事:**usj.cc 现在到底谁在提供 HTTPS?**
从实测响应头看:
```
Server: marco/3.2 ← 又拍云
Via: T.206.M, V.403-zj-fud-202, ... ← 又拍云多级节点
```
**对外服务的是又拍云 CDN**,不是那台服务器。那台服务器承担的是:
- 跑 certimate(证书申请+分发)
- 1Panel 上的 write-server / editor-api 等自建服务
所以「干掉服务器」要分清两件事:
**① 只干掉「证书维护」这件事** → **完全可以,用 Cloudflare 托管 DNS + certimate 的 CF provider。**
路径:
1. 把 `usj.cc` 的 **DNS 托管迁到 Cloudflare**(当前在腾讯云 DNSPod)
2. certimate 里把「DNS 提供商」从 `tencentcloud-dns` 换成 `cloudflare`(填 CF API Token)
3. 证书申请照旧(litessl 或换 Let's Encrypt),DNS-01 走 CF API
4. 部署目标改为「Cloudflare」(如果把站点也迁到 CF)或保留又拍云/多吉云
**② 连「站点托管」也迁到 Cloudflare** → **可行但有取舍:**
| 优点 | 缺点 |
|---|---|
| CF 免费版自带 Universal SSL(免申请免续期) | 国内访问速度**不如又拍云/多吉云**(CF 免费版节点在境外) |
| 不用再管 90 天续期 | 免费版 Universal SSL 只覆盖 `example.com` + `*.example.com` |
| 有 CF Origin CA(15 年有效期)给回源用 | 想用 CF 自签源站证书需另配 |
### 4.4 ⚠️ 关键提醒:国内访问速度
你的读者主要在国内。**又拍云/多吉云是国内 CDN,Cloudflare 免费版是境外节点**——
如果为了省一台服务器而把整个站点切到 CF,**国内访问大概率变慢**(首包延迟从几十 ms 变成几百 ms)。
**所以我建议:**
- ✅ **可以做的**:把 `usj.cc` 的 DNS 托管迁到 Cloudflare(CF DNS 免费、管理方便、API 完善),
certimate 用它做 DNS-01,证书照旧签发后部署到**又拍云 + 多吉云**。这样证书维护不再依赖服务器上的 1Panel 部署环节。
- ⚠️ **建议保留又拍云/多吉云做 CDN**,不要为了「干掉服务器」把站点也搬到 CF。
- ❌ 如果那台服务器还跑着 write-server / editor-api,**那就不能关**——关之前先确认没有别的服务在跑。
### 4.5 你现在到底能不能关服务器?
需要先回答这个问题:**那台服务器上除了 certimate,还跑着什么?**
- 如果只有 certimate → 迁完就能关 ✅
- 如果还有 write-server / editor-api(按记忆应该有)→ **关不掉** ⚠️
建议你在关之前先列一下那台机器上的服务清单。需要的话我可以帮你连上去盘一遍。
---
## 附:本次用到的排查命令
```bash
# CDN 缓存判别
curl -sI https://usj.cc/202610021614.html | grep -iE "server|age|last-modified|x-cache"
# D1 查询(curl 走 IPv4,Node fetch 会 ENETUNREACH)
curl -s -X POST \
"https://api.cloudflare.com/client/v4/accounts/$ACC/d1/database/$DB/query" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"sql":"SELECT page_key, COUNT(*) FROM comments GROUP BY page_key"}'
# 线上是否有点赞记录
# → SELECT SUM(CASE WHEN content LIKE '%[LIKE]%' THEN 1 ELSE 0 END) FROM comments; → 0
```