364 lines
17 KiB
Markdown
364 lines
17 KiB
Markdown
# artalk-cf —— 跑在 Cloudflare 上的 Artalk 评论后端 + RSS 订阅机器人
|
||||
|
|
|
|||
|
|
自研的 **Artalk API v2 兼容服务端**,以及合并进来的 **RSS 订阅机器人(rss-robot)**,
|
|||
|
|
一起跑在 **Cloudflare Workers + D1 + KV** 上,零服务器、零运维、免费额度内。
|
|||
|
|
|
|||
|
|
> **状态:已上线** → `https://api.200181.xyz`
|
|||
|
|
> D1 `artalk-cf`(`78140776-2bfd-439a-8b05-c3eebe5d5b47`)| KV `RSS_KV`(`d7f86a0fb43f4450a10b786fc2635128`)
|
|||
|
|
> 管理员账号 `admin`(后台登录用邮箱+密码,见「部署」)
|
|||
|
|
>
|
|||
|
|
> **两个后台都可用**:`/admin/` 自研管理面板 | `/sidebar/` 官方 Artalk 后台
|
|||
|
|
> **评论前端一行不用改**:继续用官方 Artalk 客户端(博客里打包的 2.8.7),换后端只改 `hugo.toml` 一行 `server`。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 目录
|
|||
|
|
|
|||
|
|
1. [这个项目是什么](#这个项目是什么)
|
|||
|
|
2. [技术选型](#技术选型)
|
|||
|
|
3. [模块分布](#模块分布)
|
|||
|
|
4. [数据模型](#数据模型)
|
|||
|
|
5. [部署](#部署)
|
|||
|
|
6. [接到博客上](#接到博客上)
|
|||
|
|
7. [评论数据迁移](#评论数据迁移)
|
|||
|
|
8. [性能与免费额度](#性能与免费额度)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 这个项目是什么
|
|||
|
|
|
|||
|
|
两件事,各对应一个原本独立的仓库,现在合并进同一个 Worker:
|
|||
|
|
|
|||
|
|
| 子项目 | 来源 | 职责 | 路径 |
|
|||
|
|
|---|---|---|---|
|
|||
|
|
| **artalk-cf** | 自研 | Artalk API v2 兼容评论后端(44 路径 / 54 操作已实现 46 个) | `/api/v2/*` |
|
|||
|
|
| **rss-robot** | 迁移自 EdgeOne 的 `api/*.js` | RSS 订阅抓取 + 友链/友圈 + 公众号内容 | `/api/*`(不含 `/api/v2`) |
|
|||
|
|
|
|||
|
|
两者共存于一个 Worker、一个 D1、一个 KV,互不冲突:
|
|||
|
|
Artalk 客户端只打 `/api/v2/*`,RSS 模块只打 `/api/*`(非 v2),路径天然隔离。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 技术选型
|
|||
|
|
|
|||
|
|
### 整体判断:为什么是 Cloudflare Workers + D1,而不是自建服务器
|
|||
|
|
|
|||
|
|
| 考量 | 结论 |
|
|||
|
|
|---|---|
|
|||
|
|
| 成本 | 免费版够用(Workers 10 万请求/天、D1 读 500 万行/天、存储 5GB),博客评论量级远用不满 |
|
|||
|
|
| 运维 | 无服务器、无 SSH、无进程守护;`wrangler deploy` 一条命令发布 |
|
|||
|
|
| 大陆可用性 | 见下方「已知取舍」——这是唯一的短板 |
|
|||
|
|
|
|||
|
|
### 逐项选型与理由
|
|||
|
|
|
|||
|
|
**1. 运行环境:Cloudflare Workers(免费版)**
|
|||
|
|
|
|||
|
|
- 全球边缘函数,`fetch` 入口 + `scheduled` 定时入口
|
|||
|
|
- **致命限制**:**没有 TCP socket** → 无法直连 SMTP 发邮件、无法跑原生 bcrypt,这两点直接决定了后续选型(见下)
|
|||
|
|
- 单请求 CPU 预算约 10ms(免费版),驱动了「读写分流 + 覆盖索引 + 计数缓存」一系列优化
|
|||
|
|
|
|||
|
|
**2. 数据库:D1(Cloudflare 的 SQLite 边缘库)**
|
|||
|
|
|
|||
|
|
- 兼容 SQLite 语法,`wrangler d1 execute` 直接跑 SQL,批量导入零成本
|
|||
|
|
- 读写分离:**D1 Sessions API + 只读副本**(`first-primary` 写 / `first-unconstrained` 读)
|
|||
|
|
- 免费额度:读 500 万行/天、写 10 万行/天。**这是本项目最需要盯的资源**(2026-10-03 曾被打爆过一次,见性能章节的「打爆复盘」)
|
|||
|
|
|
|||
|
|
**3. KV(键值存储):RSS 订阅与验证码通行证**
|
|||
|
|
|
|||
|
|
- RSS 的 `feeds_config` / 抓取游标 / 失败退避、人机验证的通行证、管理员通知节流,全放 KV
|
|||
|
|
- 理由:这些是「低频读、高频写、无关系」的数据,放 D1 会白白烧行数额度
|
|||
|
|
|
|||
|
|
**4. 语言:TypeScript + Wrangler**
|
|||
|
|
|
|||
|
|
- 与 Workers 官方工具链天然契合;`tsc --noEmit` 做类型检查
|
|||
|
|
- 不引入任何运行时依赖(`package.json` 里只有 `@cloudflare/workers-types`、`typescript`、`wrangler` 三个 devDependencies)
|
|||
|
|
- 需要原生缺失的东西就自己写:`MD5`(Gravatar hash)、`Markdown 子集解析`、`PBKDF2`(用 WebCrypto)
|
|||
|
|
|
|||
|
|
**5. 鉴权:无状态 HMAC-SHA256 token + PBKDF2 密码**
|
|||
|
|
|
|||
|
|
- token 用 `TOKEN_SECRET` 做 HMAC 签名、7 天过期,无状态不落库
|
|||
|
|
- 密码 PBKDF2-SHA256 12 万轮(WebCrypto 实现);兼容读取 `(md5)` 和明文(仅迁移期)
|
|||
|
|
- **不用 bcrypt** 是因为 Workers 没有 Node 的 native 依赖、也不值得为一个算法引进包
|
|||
|
|
|
|||
|
|
**6. 邮件:Resend 的 HTTP API(而非 SMTP)**
|
|||
|
|
|
|||
|
|
- Workers 无 TCP,SMTP 连不上;Resend 免费 3000 封/月
|
|||
|
|
- 全程 `ctx.waitUntil()` 异步 + try/catch,发信失败绝不影响评论落库
|
|||
|
|
|
|||
|
|
**7. 人机验证:自研「一键验证」(PoW + 行为信号),而非 Turnstile/reCAPTCHA**
|
|||
|
|
|
|||
|
|
- 第三方脚本在大陆加载不稳定,失败会卡死评论提交;还会把访客指纹送给第三方
|
|||
|
|
- 自研版:浏览器静默算 20~80ms 的 PoW(sha256 前缀 4 个 0),命中可疑信号才让访客「点一下」,再可疑才回退图形验证码
|
|||
|
|
- 挑战无状态(HMAC 签名不写库),通行证放 KV 不占 D1 额度
|
|||
|
|
|
|||
|
|
**8. 前端:零重写,直接复用官方产物**
|
|||
|
|
|
|||
|
|
- 客户端 + 官方后台(`/sidebar/`)是 Artalk v2.8.7 的预构建产物,内置在 `public/` 里
|
|||
|
|
- 自研管理面板 `/admin/` 用**纯原生 JS + 零依赖**(补齐官方后台缺的「待审视图」「关键词搜索」等)
|
|||
|
|
|
|||
|
|
### 已知取舍(有意为之)
|
|||
|
|
|
|||
|
|
| 项 | 取舍 | 影响 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| 大陆 → CF 边缘 ~170ms 延迟 | CF 免费版在大陆无节点,流量被送到 LAX | 加 `<link rel="preconnect">` 省掉 0.5s TLS 握手,立竿见影 |
|
|||
|
|
| 页面标题自动抓取 | 不做(跨境请求收益低) | 标题改为发评论时从 `page_title` 落库 |
|
|||
|
|
| 邮箱验证码登录 / 社交登录 | 不做 | 管理员走密码登录,不受影响 |
|
|||
|
|
| `url_resolver` | 必须保持 false | 开启会重写 page_key,现有 `/20210911.html` 全失配 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 模块分布
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
artalk-cf/
|
|||
|
|
├── wrangler.toml ★ Worker 配置:D1/KV/静态资源/域名/cron/变量
|
|||
|
|
├── schema.sql ★ 建表(10 张表 + 覆盖索引)
|
|||
|
|
├── 部署清单.md ★ 照着敲的部署步骤(含域名那一坑)
|
|||
|
|
├── .dev.vars.example 本地开发机密模板(复制成 .dev.vars)
|
|||
|
|
│
|
|||
|
|
├── src/ ===== 服务端(TypeScript)=====
|
|||
|
|
│ ├── index.ts 入口:路由注册 + fetch + scheduled 定时任务
|
|||
|
|
│ ├── router.ts 迷你路由器(路径参数匹配,挂 / 与 /api/v2)
|
|||
|
|
│ ├── rss-api.ts RSS 模块入口(路由表 + cron 轮转抓取)
|
|||
|
|
│ ├── types.ts Env / 实体 / 响应类型定义
|
|||
|
|
│ ├── template.generated.ts 设置页 YAML 模板(由官方 conf 生成)
|
|||
|
|
│ │
|
|||
|
|
│ ├── lib/ ===== 通用库(无 I/O 依赖的纯逻辑)=====
|
|||
|
|
│ │ ├── db.ts D1 查询封装 + 限流 + 验证码状态
|
|||
|
|
│ │ ├── cook.ts 实体 → API 输出(对应官方 dao/cook.go)
|
|||
|
|
│ │ ├── session.ts token 签发/校验 + 密码 + 管理员判定
|
|||
|
|
│ │ ├── md5.ts MD5(Gravatar hash,Workers 没有)
|
|||
|
|
│ │ ├── md.ts Markdown 子集 → HTML(先转义再解析,防 XSS)
|
|||
|
|
│ │ ├── util.ts 时间 / CORS / 响应 / 校验工具
|
|||
|
|
│ │ ├── errors.ts ★ 统一错误出口(技术错误翻译成人话 + 机器码)
|
|||
|
|
│ │ ├── human.ts 人机验证:PoW 挑战 + KV 通行证
|
|||
|
|
│ │ ├── mail.ts 邮件发送(Resend HTTP API)
|
|||
|
|
│ │ ├── admin-notify.ts 管理员提醒邮件(带节流)
|
|||
|
|
│ │ └── rss/ ===== RSS 订阅子系统 =====
|
|||
|
|
│ │ ├── cron.ts 每小时轮转抓取(66 源 ÷ 22 = 3 小时一轮)
|
|||
|
|
│ │ ├── fetch.ts 抓取(超时/重试/代理回退/熔断)
|
|||
|
|
│ │ ├── discover.ts feed 地址自动发现
|
|||
|
|
│ │ ├── parse.ts RSS/Atom 解析
|
|||
|
|
│ │ ├── greetings.ts 问候语/每日一图
|
|||
|
|
│ │ ├── notify.ts 更新推送(飞书 webhook 等)
|
|||
|
|
│ │ ├── proxy-health.ts 国内代理(SCF)体检
|
|||
|
|
│ │ └── util.ts RSS 工具
|
|||
|
|
│ │
|
|||
|
|
│ └── routes/ ===== 路由处理器(按业务域分组)=====
|
|||
|
|
│ ├── public.ts conf / 验证码 / 投票 / pv / setup / healthz
|
|||
|
|
│ ├── comments.ts 评论 CRUD + 列表与树(含人机验证门)
|
|||
|
|
│ ├── user.ts 用户 / 登录 / 合并 / SSO
|
|||
|
|
│ ├── admin.ts 设置 / 站点 / 页面 / 用户 / 消息 / 统计 / 导入导出 / 上传
|
|||
|
|
│ ├── human.ts 人机验证端点(challenge / verify / status)
|
|||
|
|
│ └── rss/ ===== RSS 路由 =====
|
|||
|
|
│ ├── feeds.ts 订阅源管理
|
|||
|
|
│ ├── links.ts 友链/友圈
|
|||
|
|
│ ├── results.ts 抓取结果 / 文章
|
|||
|
|
│ ├── media.ts 随机图 / 评论机器人
|
|||
|
|
│ ├── greeting.ts 问候语
|
|||
|
|
│ ├── misc.ts 鉴权 / 健康检查
|
|||
|
|
│ ├── tools.ts favicon / 代理 / cron / 部署状态 / 邮件
|
|||
|
|
│ └── wechat.ts 微信公众号内容
|
|||
|
|
│
|
|||
|
|
├── public/ 静态资源(优先级高于 Worker,改这里不用动 Worker 代码)
|
|||
|
|
│ ├── index.html / 入口页(健康状态 + 两个后台入口)
|
|||
|
|
│ ├── admin/ /admin/ 自研管理面板(原生 JS,6 个视图)
|
|||
|
|
│ │ ├── admin.css 设计令牌 + 全部组件样式
|
|||
|
|
│ │ ├── admin.js 仪表盘/评论/页面/用户/设置/状态
|
|||
|
|
│ │ └── fonts/ Press Start 2P + JetBrains Mono
|
|||
|
|
│ ├── sidebar/ /sidebar/ 官方 v2.8.7 管理后台(Vue 预构建)
|
|||
|
|
│ ├── dist/ 官方 v2.8.7 客户端与插件
|
|||
|
|
│ └── feed.html /feed/ RSS 订阅源管理页
|
|||
|
|
│
|
|||
|
|
└── tools/ 工具脚本(开发/部署/自检)
|
|||
|
|
├── deploy.sh 一键部署
|
|||
|
|
├── import_artrans.py .artrans → D1 SQL(批量迁移)
|
|||
|
|
├── import_delta.py 增量导入
|
|||
|
|
├── gen_template.py 从官方 conf YAML 生成设置模板常量
|
|||
|
|
├── selftest.ts 纯逻辑自检(MD5/时间/Markdown/密码/token/YAML)
|
|||
|
|
├── smoke.sh 冒烟测试
|
|||
|
|
├── preview-mail.ts 邮件样张预览
|
|||
|
|
├── mailtest.ts 发信测试
|
|||
|
|
└── check-*.mjs / probe-*.mjs / shot-*.mjs / measure-*.mjs
|
|||
|
|
无头浏览器(CDP)测量/回归脚本,调试评论前端用
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 数据流一览
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
浏览器(博客评论区 / 后台 iframe)
|
|||
|
|
│ /api/v2/*(评论)/ /api/*(RSS)
|
|||
|
|
▼
|
|||
|
|
Worker fetch(src/index.ts)
|
|||
|
|
├─ /api/* 且非 /api/v2/* → rss-api.ts(RSS 子系统)
|
|||
|
|
└─ 其余 → router.ts 分发到 routes/*
|
|||
|
|
│
|
|||
|
|
├─ 读请求 → D1 只读副本(first-unconstrained)
|
|||
|
|
├─ 写请求 → D1 主库(first-primary)
|
|||
|
|
└─ 通行证 → KV(人机验证 / RSS 游标 / 通知节流)
|
|||
|
|
│
|
|||
|
|
scheduled(cron)
|
|||
|
|
├─ 0 * * * * → RSS 轮转抓取(每小时 22 源)
|
|||
|
|
└─ 17 3 * * * → 评论 GC(限流/验证码清理 + healthz 缓存刷新 + 代理体检)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 数据模型
|
|||
|
|
|
|||
|
|
**10 张表**(`schema.sql`),时间统一存 unix 毫秒,输出时按 `Asia/Shanghai` 格式化:
|
|||
|
|
|
|||
|
|
| 表 | 用途 | 关键字段 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| `sites` | 站点 | name(唯一)、urls |
|
|||
|
|
| `pages` | 页面 | key(=page_key 如 `/20210911.html`)、site_name、pv、vote |
|
|||
|
|
| `users` | 用户 | name+email(唯一)、password(pbkdf2/md5/明文兼容)、徽章、头衔 |
|
|||
|
|
| `comments` | 评论 | rid(父节点)、root_id(顶层祖先)、is_pending/pinned/verified、软删除 deleted_at |
|
|||
|
|
| `votes` | 投票 | target_id + user_id + type 唯一 |
|
|||
|
|
| `notifies` | 通知 | user_id、comment_id、is_read/is_emailed |
|
|||
|
|
| `settings` | 键值配置 | key(主键)、value(frontend_conf / admin_users 等) |
|
|||
|
|
| `rate_limits` | 限流 | bucket(如 `comment:1.2.3.4`)、count、expires_at |
|
|||
|
|
| `captcha_passes` | 验证码通过状态 | ip(主键)、expires_at |
|
|||
|
|
| `captcha_challenges` | 验证码题目 | answer_hash(只存哈希) |
|
|||
|
|
|
|||
|
|
**覆盖索引**(性能关键,见下):
|
|||
|
|
|
|||
|
|
- `idx_comments_page_list` — 列表 `ORDER BY is_pinned DESC, created_at DESC` 专用
|
|||
|
|
- `idx_comments_children` — 子评论 `root_id IN (...)` 专用
|
|||
|
|
- `idx_comments_site_list` — 站点级列表
|
|||
|
|
|
|||
|
|
`root_id` 语义与官方一致:顶层为 0,子孙为该顶层评论的 id。列表默认 `nested` 模式——取一页顶层,再捞回全部子孙交给前端组树。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 部署
|
|||
|
|
|
|||
|
|
**完整步骤看 [`部署清单.md`](部署清单.md)**(照着敲约 10 分钟)。快速版:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
cd blog-admin && npm install
|
|||
|
|
npx wrangler login
|
|||
|
|
npx wrangler d1 create artalk-cf # 把返回的 database_id 填进 wrangler.toml
|
|||
|
|
npx wrangler secret put TOKEN_SECRET
|
|||
|
|
npx wrangler secret put ADMIN_PASSWORD
|
|||
|
|
npx wrangler d1 execute artalk-cf --remote --file=./schema.sql
|
|||
|
|
npx wrangler deploy
|
|||
|
|
curl -X POST https://<worker>/api/v2/setup
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### ★ 域名这一步有个坑,动手前先看
|
|||
|
|
|
|||
|
|
**Cloudflare Worker 的自定义域名要求「域名的 NS 托管在 Cloudflare」。**
|
|||
|
|
而 `usj.cc` 的 NS 在 DNSPod(还挂着又拍云 + 腾讯云 EdgeOne 调度)——**千万不要为了评论 API 把整站 NS 搬去 Cloudflare**,会弄坏现有国内双 CDN 链路。
|
|||
|
|
|
|||
|
|
本项目用的是 **`api.200181.xyz`**(NS 本来就在 Cloudflare):
|
|||
|
|
|
|||
|
|
```toml
|
|||
|
|
[[routes]]
|
|||
|
|
pattern = "api.200181.xyz"
|
|||
|
|
custom_domain = true
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`*.workers.dev` 地址在大陆基本不通,只能自测。四种域名方案取舍见 `部署清单.md` 第 9 步。
|
|||
|
|
|
|||
|
|
### 机密清单(不写进仓库)
|
|||
|
|
|
|||
|
|
| Secret / 变量 | 用途 |
|
|||
|
|
|---|---|
|
|||
|
|
| `TOKEN_SECRET` | 登录 token 签名 |
|
|||
|
|
| `ADMIN_PASSWORD` | 管理员密码 |
|
|||
|
|
| `RESEND_API_KEY` | 邮件(可选) |
|
|||
|
|
| `MAIL_FROM` / `MAIL_ADMIN` | 邮件发件人 / 收件人(可选) |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 接到博客上
|
|||
|
|
|
|||
|
|
`E:\GitHub\blog\hugo.toml`:
|
|||
|
|
|
|||
|
|
```toml
|
|||
|
|
[params.artalk]
|
|||
|
|
server = "https://api.200181.xyz" # ← 只改这一行
|
|||
|
|
site = "优世界" # 必须与 SITE_DEFAULT 一致
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
客户端调用 `${server}/api/v2/...`,`pageKey` 用 `.RelPermalink`(`/20210911.html`),与 D1 的 `page_key` 完全对应,**不需要改任何模板或前端代码**。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 评论数据迁移
|
|||
|
|
|
|||
|
|
### 方式一:批量 SQL(推荐)
|
|||
|
|
|
|||
|
|
Worker 逐条 INSERT 会产生几千次 D1 往返,免费版单请求 CPU 只有 10ms 容易超时,所以走 `wrangler d1 execute --file`:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
python tools/import_artrans.py # 读 .artrans,只导「优世界」,生成 SQL
|
|||
|
|
bash tools/import-out/run.sh --remote
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
脚本做了:按 site_name 过滤、按「昵称+邮箱」重建用户、保留原始 id 与 rid 关系、rid 回填与 root_id 计算分三趟做(与导入顺序无关)、最后自检打印各表条数。
|
|||
|
|
|
|||
|
|
### 方式二:后台界面导入
|
|||
|
|
|
|||
|
|
登录 `/sidebar/` → 迁移页 → 上传 `.artrans`(小数据量可用)。
|
|||
|
|
|
|||
|
|
### ⚠️ 别踩的坑
|
|||
|
|
|
|||
|
|
- `url_resolver` 保持 false,否则 page_key 被重写,评论区变空
|
|||
|
|
- 导出里 `created_at` 为 Go 零值(`0001-01-01`)的 6 条会被兜底成当前时间
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 性能与免费额度
|
|||
|
|
|
|||
|
|
### 免费额度够不够(3440 条评论 / 138 篇文章)
|
|||
|
|
|
|||
|
|
| 资源 | 免费额度 | 你的用量 | 结论 |
|
|||
|
|
|---|---|---|---|
|
|||
|
|
| Workers | 10 万请求/天 | 评论量级一天最多几千 | ✅ |
|
|||
|
|
| D1 存储 | 5 GB | 评论约几 MB | ✅ |
|
|||
|
|
| D1 读 | 500 万行/天 | 一次列表查询几十行 | ✅ |
|
|||
|
|
| Workers CPU | 10 ms/请求 | 列表查询含 markdown 渲染,需观察 | ⚠️ |
|
|||
|
|
|
|||
|
|
### 分层耗时(中国大陆网络实测)
|
|||
|
|
|
|||
|
|
| 项 | 耗时 |
|
|||
|
|
|---|---|
|
|||
|
|
| 冷连接 TLS 握手 | ~0.51s |
|
|||
|
|
| 连接复用后端到端 | ~190ms |
|
|||
|
|
| 1 次 D1 查询 | 8~20ms |
|
|||
|
|
|
|||
|
|
**真正的瓶颈是「大陆 → CF 边缘」这段(~170ms),不是数据库。** 改善:博客里加 `<link rel="preconnect" href="https://api.200181.xyz">`,省掉 0.5s TLS 握手,成本最低。
|
|||
|
|
|
|||
|
|
### 打爆复盘(2026-10-03 D1 额度用尽)
|
|||
|
|
|
|||
|
|
D1 免费版读额度(500 万行/天,UTC 00:00 = 北京 08:00 重置)被一次打爆,实测 525 万行。定位与修复:
|
|||
|
|
|
|||
|
|
1. **取证**:CF GraphQL `d1QueriesAdaptiveGroups`(维度名是 `query`)按 `sum_rowsRead` 排序,锁定 `COUNT(*)` + `LEFT JOIN users` 是无索引全表扫
|
|||
|
|
2. **修复**:
|
|||
|
|
- 三个覆盖索引(`idx_comments_page_list` / `_children` / `_site_list`)
|
|||
|
|
- 计数 KV 缓存(只在增删/审核时失效)
|
|||
|
|
- 列表/计数不再无谓 JOIN users
|
|||
|
|
- 两个 COUNT 合并成一条 `COUNT(*) + SUM(CASE ...)`
|
|||
|
|
3. **教训**:改动前**先本地预览 + 冒烟测试**,别拿线上打爆的额度再跑测试
|
|||
|
|
|
|||
|
|
### 自查方法
|
|||
|
|
|
|||
|
|
打开 <https://api.200181.xyz/api/v2/healthz> 看三个字段:
|
|||
|
|
|
|||
|
|
- `colo` —— 请求落在哪个 CF 机房(决定 RTT)
|
|||
|
|
- `region` / `primary` —— 查询由主库还是副本处理
|
|||
|
|
- `mail` —— 邮件是否已配置(`"on"`)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 安全
|
|||
|
|
|
|||
|
|
- 登录 token:HMAC-SHA256 签名,无状态、7 天过期;改密码写 `token_valid_from` 让旧 token 立即失效
|
|||
|
|
- 密码:PBKDF2-SHA256 12 万轮;兼容 `(md5)` / 明文(仅迁移)
|
|||
|
|
- 后台接口全部走 `requireAdmin`,未登录 403
|
|||
|
|
- 评论软删除(`deleted_at`),误删可从 D1 恢复
|
|||
|
|
- CORS 白名单 + 预检处理;写接口走 D1 计数限流
|
|||
|
|
- 验证码答案只存哈希;人机验证挑战 HMAC 签名无状态
|
|||
|
|
- 顶层 fetch 兜底:任何漏网异常也带 CORS + 人话错误,不给读者甩「Failed to fetch」
|