System Design Document

PAR — PT Adapter Registry

PT 站点适配配置的版本化注册中心,负责配置的存储、校验、分发和生命周期管理

版本: 2.0 日期: 2026-06-29 状态: 最终设计定稿

1. 系统定位与设计目标

PAR(PT Adapter Registry)是一个面向 PT 自动化工具生态的站点适配配置注册中心,以"静态分发 + 受控写入"为核心模型,提供配置的版本化管理、自动化校验、可靠分发和质量保障。

系统主要配套给 PT 自动化管理工具(如 mediabot),作为 PT 站点目录、HTML 解析规则、接口数据处理策略等配置的共享和管理中心,让多个使用 PT 自动化工具的系统可以方便地获取和更新这些配置。

1.1 设计目标(按优先级排列)

  1. 分发可靠性第一 — 工具拿不到配置就等于瘫痪,可用性比功能丰富更重要
  2. 多工具兼容 — 配置格式与具体工具解耦,mediabot、自定义脚本等都能消费
  3. 接入成本趋近于零 — 客户端不需要注册、不需要登录,开箱即用
  4. 写入质量可控 — 发布有门槛,防止劣质配置污染生态
  5. 运维成本可控 — 社区项目,资源有限,架构必须简单

1.2 核心设计原则

配置即包(Config as Package)

借鉴 npm/Docker Hub 的模型:每个站点有一个"包",包有多个版本,每个版本是一个不可变的配置快照。不存在"覆盖"的概念,只有"发布新版本"。客户端始终通过 siteId + version 精确获取。latest 标签指向当前推荐版本,可回退。

读开放、写受控

读取配置无需鉴权(和病毒库一样),降低客户端接入成本。写入(发布配置)需要认证 + 权限,保证质量。类比:任何人可以 npm install,但发布包需要登录。

校验前置

配置在入库前必须通过 Schema 校验,并支持自动化冒烟测试(用配置去实际请求站点页面,验证选择器是否有效)。冒烟测试为 Phase 2

1.3 技术选型

类别技术
后端Java 17 + Spring Boot 3.x + MyBatis-Plus
数据库MySQL 8.0
认证bcrypt 密码存储 + HMAC-SHA256 请求签名
构建Maven 多模块
部署Docker(纯 Java 镜像,JDK 17 JRE Alpine)
文档SpringDoc OpenAPI(自动生成)P2

2. 分阶段实施计划

阅读指引

本章定义了 Phase 1(最小可用版本)和 Phase 2(质量体系增强)的功能边界。后续各章节中,Phase 1 内容正常展示,Phase 2 内容以 P2 标签标记并以半透明样式呈现,方便读者快速区分。

2.1 Phase 1 — 最小可用版本 P1

核心目标:让 mediabot 能够通过 PAR 完成配置的发布、审核和同步。

模块范围
配置 Schema固定 v2 Schema,覆盖 search/detail/upload 页面
API 核心CRUD + ETag 条件请求 + Manifest
认证手动注册(邮箱 + 密码)+ 登录获取 API Key + HMAC 请求签名
匿名统计可选 X-Anonymous-Id 头,注册后自动关联匿名数据
静态分发Spring Boot 内嵌 Tomcat 直接服务静态文件(CDN 可后加)
校验JSON Schema 校验 + 选择器语法检查
审核基础审核流程(配置 + 站点信息)
PAR Admin审核工作台 + 用户管理 + 站点管理 + 回滚
客户端 SDKManifest 同步 + ETag + 本地缓存 + checksum 校验
部署纯 Java Docker 镜像(JDK 17 JRE Alpine + Spring Boot Fat JAR)

2.2 Phase 2 — 质量体系增强 P2

核心目标:提升配置质量保障和分发可靠性。

模块范围
引擎模板模板管理 + 差异提交 + 发布时合并
冒烟测试自动请求站点页面验证选择器(校验流水线第 3 步)
配置签名JWS 签名 + 客户端公钥验证
CDN 分发引入 Nginx 前置代理 + CDN 回源
通知Webhook + 邮件通知
站点生命周期弃用归档、迁移引导
OpenAPI 规范自动生成 API 文档 + SDK

3. 整体架构

PAR 采用静态优先(Static-First)架构。PT 站点适配配置天然适合静态分发:体积极小(单个配置 2-8 KB,全量不到 1 MB),读写比极高,更新频率低。

flowchart TB
    subgraph Clients["客户端工具"]
        A[mediabot]
        B[自定义脚本]
        C[其他 PT 工具]
    end

    subgraph CDN_Layer["CDN 缓存(Phase 2)"]
        CDN[CDN 节点]
    end

    subgraph Server["API 服务器(Spring Boot 内嵌 Tomcat)"]
        API["API 服务
动态请求处理"] STATIC["静态配置文件分发
(内嵌 Tomcat 直接服务)"] DB[(MySQL 数据库)] STORE[本地静态存储 JSON] end subgraph Admin["管理面板"] PANEL["PAR Admin
(Spring Boot 托管 SPA)"] end A -->|读取配置| API B -->|读取配置| API C -->|读取配置| API A -->|写入配置| API API --> DB API -->|发布时写入| STORE PANEL -->|审核/管理| API CDN -.->|回源| API
图 1:PAR 系统整体架构(纯 Java 部署,CDN 为 Phase 2)

3.1 工作方式

  1. 配置发布流程:通过管理 API 提交 -> 校验通过 -> 写入数据库元信息 + 生成静态 JSON 文件到本地磁盘
  2. 读取流程:客户端请求 GET /v1/configs/{siteId},由 Spring Boot Controller 处理,支持 ETag 条件请求,响应携带 Cache-Control 头CDN 为 Phase 2,Phase 1 仅 API 同域
  3. 管理 API:只处理写入、审核、账户等需要鉴权的操作,读压力极小

设计优势

Phase 1 采用纯 Java 部署(Spring Boot 内嵌 Tomcat),动态 API 和静态文件均由同一 Java 进程处理,架构极简。Spring Boot 的 ResourceHandler 可高效服务静态文件,配合 ETag 和 Cache-Control 响应头实现客户端缓存。Phase 2 引入 CDN 后,CDN 回源到 Spring Boot,CDN 不可用时仍可直接访问 API 源。CDN 和多源回退为 Phase 2

3.2 与 mediabot 的职责划分

能力mediabot(客户端工具)PAR(注册中心)
站点录入入口提供录入表单接收注册 + 唯一性校验
适配器编辑内置可视化编辑器-
配置发布调用 PAR API 提交校验 + 存储 + 版本化
配置预览实时预览解析结果-
配置分发拉取并本地缓存Manifest + ETag + 静态文件
配置审核-审核工作台(PAR Admin)
用户/信任管理PAR 账户注册/登录入口,本地多账户切换账户认证 + 信任等级 + HMAC 签名验证
回滚操作-版本历史 + 一键回滚

4. 配置 Schema 设计

4.1 设计原则

  • 按页面功能组织(和用户心智对齐)
  • 声明式为主(尽量不依赖代码逻辑)
  • 可扩展但不松散(核心字段必填,扩展字段可选)

4.2 完整 Schema 结构

// ═══════ 站点元信息 ═══════
{
  "$schema":      "https://par.dev/schema/v2",
  "schemaVersion": 2,
  "siteId":        "ptfans",              // 唯一标识
  "siteName":      "PTFans",
  "siteType":      "nexusphp",            // 站点引擎类型,决定解析框架
  "domains":       ["ptfans.cc"],
  "homeUrl":       "https://ptfans.cc",
  "tags":          ["影视", "综合"],
  "description":   "综合类PT站点,以影视资源为主",

  // ═══════ 认证配置 ═══════
  "auth": {
    "type":        "cookie",             // cookie | basic | api_key | oauth
    "loginUrl":    "/takelogin.php",
    "loginMethod": "POST",
    "loginFields": {
      "username": { "selector": "#username", "type": "text" },
      "password": { "selector": "#password", "type": "password" },
      "captcha":  { "selector": "#imagehash", "type": "image", "optional": true }
    },
    "cookie": {
      "requiredKeys":  ["nexusphp_*"],
      "sessionKey":    "nexusphp_*",
      "estimatedExpiry": "30d"
    },
    "signIn": {
      "url":     "/attendance.php",
      "method":  "GET",
      "successIndicator": "class:success_msg"
    }
  },

  // ═══════ 页面解析规则(开放式 map)═══════
  "pages": {
    "search": {
      "url":            "/torrents.php?inclbookmarked=0&incldead=0",
      "method":         "GET",
      "listContainer": "table.torrents tbody tr",
      "skipRows":       0,
      "fields": {
        "title":     { "selector": "td.name a", "type": "text" },
        "size":      { "selector": "td.size", "type": "text", "transform": "fileSize" },
        "seeders":  { "selector": "td.seeders", "type": "text", "transform": "int" },
        "leechers": { "selector": "td.leechers", "type": "text", "transform": "int" },
        "detailUrl": { "selector": "td.name a", "type": "attribute",
                       "attribute": "href", "transform": "absoluteUrl" },
        "freeFlag":  { "selector": "td.pro_free, td.pro_2xup",
                       "type": "attribute", "attribute": "class",
                       "transform": "freeFlag", "optional": true }
      },
      "pagination": {
        "containerSelector": "div.page_nav",
        "nextSelector":     "a.next",
        "pageParam":        "page",
        "maxPages":         50
      },
      "errorDetection": {
        "captcha":       { "selector": "#captchaimg", "present": true },
        "rateLimited":    { "selector": ".error",
                           "textContains": ["频率", "frequent"] },
        "loginRequired":  { "selector": "#loginform", "present": true }
      },
      "smokeTest": {
        "url":           "/torrents.php",
        "expectMinRows": 1,
        "requiredFields": ["title", "size", "seeders"]
      }
    },
    "detail": {
      "urlPattern": "/details.php?id={torrentId}",
      "method":     "GET",
      "fields": { /* ... */ }
    },
    "upload": {
      "url":     "/upload.php",
      "method":  "POST",
      "enctype": "multipart/form-data",
      "fields": { /* ... */ }
    },
    "attendance": { // 扩展页面
      "url":    "/attendance.php",
      "method": "GET",
      "fields": { /* ... */ }
    }
  },

  // ═══════ 内置转换器 ═══════
  "transforms": {
    "fileSize":    { "type": "regex", "pattern": "^(\\d+\\.?\\d*)\\s*(GB|MB|KB|TB|B)$" },
    "int":         { "type": "builtIn", "name": "parseInt" },
    "dateTime":    { "type": "builtIn", "name": "parseDate" },
    "absoluteUrl": { "type": "builtIn", "name": "resolveUrl", "base": "https://ptfans.cc" },
    "freeFlag":    { "type": "mapping", "values": {
                       "pro_free": "free", "pro_2xup": "2x_upload",
                       "pro_2xfree": "2x_free", "pro_50pctdown": "50%_down" }},
    "imdbId":      { "type": "regex", "pattern": "(tt\\d+)" },
    "doubanId":    { "type": "regex", "pattern": "(\\d+)" }
  },

  // ═══════ API 配置(站点如果有 REST API)═══════
  "api": {
    "baseUrl":  "https://ptfans.cc/api/v1",
    "auth":      { "type": "cookie" },
    "endpoints": {
      "search": { "path": "/torrents", "method": "GET", /* ... */ }
    }
  },

  // ═══════ 站点能力声明 ═══════
  "capabilities": {
    "search": true,
    "upload": true,
    "rss":          { "supported": true,  "url": "/rss.php" },
    "imdbSearch":   { "supported": true },
    "doubanSearch": { "supported": false },
    "anonymousSearch": false,
    "captcha": { "login": false, "search": false, "upload": false },
    "downloadRequiresCookie": true
  },

  // ═══════ 速率限制(指导客户端行为)═══════
  "rateLimits": {
    "search":   { "maxRequests": 30,  "perSeconds": 60 },
    "download": { "maxRequests": 10, "perSeconds": 60 },
    "upload":   { "maxRequests": 5,   "perSeconds": 3600 },
    "global":   { "maxRequests": 60,  "perSeconds": 60 }
  },

  // ═══════ 站点特殊行为 ═══════
  "behaviors": {
    "encoding": "UTF-8",
    "timezone": "Asia/Shanghai",
    "downloadReferer": true,
    "specialRules": [
      { "name": "转免时间", "selector": "td.pro_free .expires",
        "type": "relativeTime" }
    ]
  }
}

4.3 关键设计要点

siteType 与引擎模板的关系 模板系统为 Phase 2

siteType 是分类标记(nexusphp、unit3d、gazelle 等),决定 mediabot 用哪套解析框架。每种引擎有一个默认模板,覆盖该引擎的通用选择器和转换器。合并发生在发布时,不在运行时。客户端获取到的永远是完整的、可直接使用的配置,不需要了解模板系统的存在。P2

pages 的开放性

search、detail、upload 是约定优先级最高的 key,所有客户端都应支持。attendance、userProfile 等扩展 key 是可选的,客户端按需实现。遇到未知的 key,客户端应安全跳过,不阻塞核心功能。

errorDetection 的价值

让客户端能自动识别并处理验证码、限速、登录失效等异常,而不是静默返回空结果。这直接决定了工具在生产环境中的可靠性。

transforms 的混合模型

类型用途示例
regex正则匹配提取fileSize、imdbId
builtIn内置解析函数parseInt、parseDate、resolveUrl
mapping枚举值映射freeFlag 的多种 CSS class 到统一状态

5. API 设计

5.1 路由总览

方法路径鉴权说明阶段
GET/v1/manifest公开全局清单(version + checksum)P1
GET/v1/sites公开站点目录P1
GET/v1/sites/{siteId}公开单个站点元信息P1
POST/v1/sitesAPI Key + HMAC注册新站点P1
PATCH/v1/sites/{siteId}API Key + HMAC修改站点元信息(需审核)P1
GET/v1/configs/{siteId}公开下载最新配置(支持 ETag)P1
GET/v1/configs/{siteId}/versions公开版本历史P1
GET/v1/configs/batch?sites=a,b,c公开批量获取配置P1
POST/v1/configs/{siteId}API Key + HMAC发布新版本配置P1
POST/v1/configs/{siteId}/validateAPI Key + HMAC预校验(不真正发布)P1
POST/v1/configs/{siteId}/rollbackAPI Key + HMAC回滚到指定版本(trusted+)P1
POST/v1/account/register公开注册(email + password)P1
POST/v1/account/login公开登录(email + password),返回 apiKeyP1
POST/v1/account/forgot-password公开忘记密码(发送邮件验证码)P1
POST/v1/account/reset-password公开重置密码(code + newPassword)P1
GET/v1/account/meAPI Key查询自身账户信息P1
POST/v1/account/reset-keyAPI Key + HMAC重置 API KeyP1
POST/v1/account/refresh-keyAPI Key + HMAC刷新 API KeyP1
POST/v1/account/webhooksAPI Key + HMAC注册 Webhook 回调P2
GET/v1/reviews/pendingAPI Key待审核列表(reviewer+)P1
POST/v1/reviews/{siteId}/config/v{ver}/approveAPI Key + HMAC批准配置变更P1
POST/v1/reviews/{siteId}/config/v{ver}/rejectAPI Key + HMAC驳回配置变更P1
POST/v1/reviews/{siteId}/meta/v{ver}/approveAPI Key + HMAC批准站点信息变更P1
POST/v1/reviews/{siteId}/meta/v{ver}/rejectAPI Key + HMAC驳回站点信息变更P1
POST/v1/admin/trust/{accountId}Admin + HMAC调整信任等级(作用于账户)P1
GET/v1/templates/*公开引擎模板相关接口P2
GET/v1/schemas公开Schema 版本文档P1
GET/v1/health公开健康检查P1
GET/v1/openapi.json公开OpenAPI 规范文档P2

5.2 核心接口详情

静态文件处理方式

Phase 1:Spring Boot 统一处理

配置 JSON 文件由 Java 应用在发布时写入本地磁盘。GET /v1/configs/{siteId} 由 Spring Boot Controller 处理,读取本地 JSON 文件并返回。响应头携带 ETag(基于文件内容的 MD5)和 Cache-Control: public, max-age=3600,客户端使用 If-None-Match 实现条件请求,无更新时返回 304 Not Modified。Phase 2 将引入 Nginx 前置代理 + CDN 回源

Manifest(同步入口)

GET /v1/manifest 公开 P1
{
  "schemaVersion": 2,
  "generatedAt":  "2026-06-28T00:00:00Z",
  "totalSites":   42,
  "sites": {
    "ptfans": {
      "version":   3,
      "channel":   "stable",
      "status":    "active",
      "checksum":  "sha256:abc123...",
      "size":      4200,
      "updatedAt": "2026-06-28T18:00:00Z"
    }
  }
}

客户端同步流程:请求 Manifest -> 与本地对比 version + checksum -> 对有更新的站点请求配置 -> 校验 checksum -> 更新本地文件。

配置下载(ETag 条件请求)

GET /v1/configs/{siteId}?version=latest&channel=stable 公开 P1
{
  "siteId":      "ptfans",
  "version":     4,
  "channel":     "stable",
  "publishedAt": "2026-06-28T20:00:00Z",
  "publisher":   "user@example.com",
  "changelog":   "修复搜索页标题选择器",
  "checksum":    "sha256:...",
  "signature":   "eyJhbGci...",    // Phase 2 启用 JWS 签名
  "config":     { /* 完整配置 JSON */ }
}

HTTP 响应头携带 ETag、Cache-Control: public, max-age=3600。客户端使用 If-None-Match 实现条件请求,无更新时返回 304 Not Modified。signature 字段 Phase 2 启用

预校验接口

POST /v1/configs/{siteId}/validate API Key P1

供 mediabot 在发布前调用,不真正创建版本,只返回校验结果。

{
  "valid": false,
  "errors": [
    { "path": "pages.search.fields.title.selector",
      "message": "无效的 CSS 选择器语法" }
  ]
}

注册

POST /v1/account/register 公开 P1

邮箱注册,密码使用 bcrypt 存储。可选携带 anonymousId,注册后自动关联匿名统计数据。

// 请求
{
  "email": "user@qq.com",
  "password": "yourPassword123",
  "anonymousId": "sha256_hash_optional"   // 可选
}

// 响应
{
  "accountId":  "acct_a1b2c3d4",
  "email":      "user@qq.com",
  "trustLevel": "member",
  "createdAt":  "2026-06-29T10:00:00Z"
}

登录

POST /v1/account/login 公开 P1

邮箱 + 密码登录,返回 API Key。客户端本地加密存储,后续写入请求携带 API Key 和 HMAC 签名。

// 请求
{
  "email": "user@qq.com",
  "password": "yourPassword123"
}

// 响应
{
  "accountId":  "acct_a1b2c3d4",
  "apiKey":     "mbt_k7x9m2p4q8...",
  "trustLevel": "member",
  "permissions": {
    "canPublish": true, "autoApprove": false,
    "canReview":  false, "canRollback": false
  }
}

忘记密码

POST /v1/account/forgot-password 公开 P1

发送邮件验证码到注册邮箱,验证码 15 分钟内有效。

// 请求
{
  "email": "user@qq.com"
}

// 响应
{
  "sent": true,
  "message": "验证码已发送到邮箱,15分钟内有效"
}

重置密码

POST /v1/account/reset-password 公开 P1

使用邮件验证码重置密码,重置成功后当前 API Key 失效,需重新登录。

// 请求
{
  "email": "user@qq.com",
  "code": "123456",
  "newPassword": "newPassword456"
}

// 响应
{
  "success": true,
  "message": "密码已重置,请重新登录"
}

刷新 API Key

POST /v1/account/refresh-key API Key + HMAC P1

主动刷新 API Key,旧 key 5 分钟宽限期后失效。需要 HMAC 签名验证。

// 请求(空体,通过 Authorization 识别)
{ }

// 响应
{
  "apiKey": "mbt_newkey_xyz...",
  "refreshedAt": "2026-06-29T10:00:00Z"
}

HMAC 请求签名说明

写入 API 必须携带 HMAC 签名

所有写入操作(POST / PATCH / DELETE)除 Authorization: Bearer {apiKey} 外,还必须携带以下请求头:

  • X-Timestamp:Unix 时间戳(秒),与服务器时间差必须 <= 60 秒
  • X-Signature:HMAC-SHA256 签名

签名内容:HTTP方法 + "\n" + 请求路径 + "\n" + 时间戳 + "\n" + 请求体SHA-256(无请求体时为空字符串)

签名计算:HMAC-SHA256(apiKey, 签名内容)

PAR 验证:时间戳窗口 + 签名匹配 + 防重放(缓存最近 60 秒内的签名,重复拒绝)。读取 API(公开接口)不需要签名。

6. 数据库设计

核心原则

配置体不存数据库 — 数据库只存元信息和校验数据,完整 JSON 配置存在静态存储中,数据库里只保存文件路径引用。版本不可变 — 所有配置表只 INSERT 不 UPDATE。软删除 — 用 deleted_at 标记。

6.1 ER 关系

erDiagram
    accounts ||--o{ site_configs : "publisher_id"
    accounts ||--o{ site_meta_versions : "submitted_by"
    accounts ||--o{ config_reviews : "reviewer_id"
    accounts ||--o{ trust_level_changes : "account_id"
    accounts ||--o{ anonymous_stats : "linked_anonymous_id"
    sites ||--o{ site_configs : "site_id (1:N)"
    sites ||--o{ site_meta_versions : "site_id (1:N)"
    site_configs ||--o| config_reviews : "config_id (1:1)"
    engine_templates ||--o{ template_versions : "template_id"
    engine_templates ||--o{ site_configs : "template_id (可选继承)"
          
图 2:数据库 ER 关系图(v2.0 简化版)

6.2 表结构

accounts — 邮箱账户(简化)

accounts
CREATE TABLE accounts (
    id                BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    account_id        VARCHAR(32)  NOT NULL        -- 对外标识 acct_xxxx
    email             VARCHAR(255) NOT NULL
    password_hash     VARCHAR(255) NOT NULL        -- bcrypt 哈希
    api_key_hash      VARCHAR(255) NOT NULL        -- API Key 的 SHA-256
    api_key_prefix    VARCHAR(12)  NOT NULL        -- 日志脱敏 mbt_abc1
    trust_level       ENUM('member','trusted','admin') NOT NULL DEFAULT 'member',
    permissions       JSON         NOT NULL
    linked_anonymous_id VARCHAR(64) NULL           -- 关联的匿名 ID(SHA-256)
    created_at        DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
    updated_at        DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
    last_login_at     DATETIME(3)  NULL,
    deleted_at        DATETIME(3)  NULL,
    PRIMARY KEY (id),
    UNIQUE KEY uk_account_id (account_id),
    UNIQUE KEY uk_email (email),
    UNIQUE KEY uk_api_key_hash (api_key_hash),
    KEY idx_linked_anon (linked_anonymous_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

sites — 站点元信息

sites
CREATE TABLE sites (
    id              BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    site_id         VARCHAR(64)  NOT NULL
    site_name       VARCHAR(128) NOT NULL
    site_type       VARCHAR(32)  NOT NULL
    home_url        VARCHAR(512) NULL
    description     TEXT         NULL
    tags            JSON         NULL
    domains         JSON         NOT NULL
    logo_url        VARCHAR(512) NULL
    maintainers     JSON         NULL
    metadata        JSON         NULL
    current_version INT UNSIGNED NOT NULL DEFAULT 1
    review_status   ENUM('active','pending_review','rejected') NOT NULL DEFAULT 'active'
    created_at      DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
    updated_at      DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
    deleted_at      DATETIME(3)  NULL,
    PRIMARY KEY (id),
    UNIQUE KEY uk_site_id (site_id),
    KEY idx_site_type (site_type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

site_configs — 站点配置版本(核心表)

site_configs
CREATE TABLE site_configs (
    id              BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    site_id         VARCHAR(64)  NOT NULL,
    version         INT UNSIGNED NOT NULL        -- 单调递增
    channel         ENUM('stable','beta') NOT NULL DEFAULT 'stable',
    status          ENUM('published','pending_review','rejected') NOT NULL,
    config_path     VARCHAR(512) NOT NULL     -- 静态存储中的文件路径
    config_size     INT UNSIGNED NOT NULL
    checksum        CHAR(64)     NOT NULL     -- SHA-256
    publisher_id    BIGINT UNSIGNED NOT NULL
    template_id     VARCHAR(64)  NULL       -- 继承的引擎模板(Phase 2 启用)
    extends_version INT UNSIGNED NULL
    changelog       TEXT         NULL
    schema_valid    TINYINT(1)   NOT NULL DEFAULT 0
    selector_valid  TINYINT(1)   NOT NULL DEFAULT 0
    smoke_tested    TINYINT(1)   NOT NULL DEFAULT 0   -- Phase 2 启用
    smoke_test_pass TINYINT(1)   NULL                  -- Phase 2 启用
    smoke_test_url  VARCHAR(512) NULL              -- Phase 2 启用
    smoke_test_at   DATETIME(3)  NULL               -- Phase 2 启用
    is_latest       TINYINT(1)   NOT NULL DEFAULT 0  -- 同 channel 下唯一
    published_at    DATETIME(3)  NULL
    created_at      DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
    PRIMARY KEY (id),
    UNIQUE KEY uk_site_version (site_id, version),
    UNIQUE KEY uk_site_channel_latest (site_id, channel, is_latest),
    KEY idx_status (status),
    KEY idx_publisher (publisher_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

site_meta_versions — 站点元信息变更历史

site_meta_versions
CREATE TABLE site_meta_versions (
    id              BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    site_id         VARCHAR(64)  NOT NULL,
    version         INT UNSIGNED NOT NULL,
    site_name       VARCHAR(128) NOT NULL,
    site_type       VARCHAR(32)  NOT NULL,
    domains         JSON         NOT NULL,
    tags            JSON         NULL,
    description     TEXT         NULL,
    submitted_by    BIGINT UNSIGNED NOT NULL
    changelog       TEXT         NULL
    status          ENUM('published','pending_review','rejected') NOT NULL,
    created_at      DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
    reviewed_at     DATETIME(3)  NULL,
    reviewed_by     BIGINT UNSIGNED NULL,
    review_comment  TEXT         NULL,
    PRIMARY KEY (id),
    UNIQUE KEY uk_site_version (site_id, version),
    KEY idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

anonymous_stats — 匿名统计数据

anonymous_stats
CREATE TABLE anonymous_stats (
    id              BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    anonymous_id    VARCHAR(64)  NOT NULL        -- SHA-256 哈希
    first_seen      DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
    last_seen       DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
    request_count   INT UNSIGNED NOT NULL DEFAULT 0,
    ip_address      VARCHAR(64)  NULL
    user_agent      VARCHAR(512) NULL
    linked_account_id BIGINT UNSIGNED NULL      -- 注册后关联的账户
    created_at      DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
    updated_at      DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
    PRIMARY KEY (id),
    UNIQUE KEY uk_anonymous_id (anonymous_id),
    KEY idx_linked_account (linked_account_id),
    KEY idx_last_seen (last_seen)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

verification_codes — 邮件验证码

verification_codes
CREATE TABLE verification_codes (
    id              BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    email           VARCHAR(255) NOT NULL
    code            VARCHAR(16)  NOT NULL
    purpose         ENUM('reset_password') NOT NULL DEFAULT 'reset_password',
    expires_at      DATETIME(3)  NOT NULL
    used_at         DATETIME(3)  NULL
    created_at      DATETIME(3)  NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
    PRIMARY KEY (id),
    KEY idx_email (email),
    KEY idx_expires (expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

其他表

表名用途关键说明阶段
engine_templates引擎模板定义template_id、template_name、current_versionP2
template_versions引擎模板版本快照不可变,存储 config_path 和 checksumP2
config_reviews配置审核记录与 site_configs 1:1,含审核意见P1
trust_level_changes信任等级变更审计记录 account_id、old_level、new_level、reason、operatorP1
rollback_history回滚操作记录from_version、to_version、operator_idP1
api_access_logAPI 访问日志高频写入,建议定期归档P1
anonymous_stats匿名统计数据anonymous_id、request_count、ip_address、linked_account_idP1
verification_codes邮件验证码email、code、purpose、expires_at、used_atP1

6.3 核心查询示例

获取站点最新配置

SELECT sc.*
FROM site_configs sc
WHERE sc.site_id = 'ptfans'
  AND sc.channel = 'stable'
  AND sc.is_latest = 1
  AND sc.status = 'published'
LIMIT 1;

回滚操作(事务中执行)

START TRANSACTION;
-- 取消当前版本的 is_latest 标记
UPDATE site_configs SET is_latest = 0
WHERE site_id = 'ptfans' AND channel = 'stable' AND is_latest = 1;
-- 设置目标版本为 is_latest
UPDATE site_configs SET is_latest = 1
WHERE site_id = 'ptfans' AND channel = 'stable' AND version = 2 AND status = 'published';
-- 记录回滚历史
INSERT INTO rollback_history (site_id, from_version, to_version, operator_id, reason)
VALUES ('ptfans', 4, 2, 123, '搜索页选择器在新版本失效');
COMMIT;

查询匿名统计与注册关联

SELECT a.email, a.trust_level, s.request_count, s.first_seen, s.last_seen
FROM anonymous_stats s
LEFT JOIN accounts a ON s.linked_account_id = a.id
WHERE s.anonymous_id = 'sha256_abc123...';

关联匿名数据到注册账户

UPDATE anonymous_stats
SET linked_account_id = 123
WHERE anonymous_id = 'sha256_abc123...'
  AND linked_account_id IS NULL;

UPDATE accounts
SET linked_anonymous_id = 'sha256_abc123...'
WHERE id = 123;

查询信任等级变更历史

SELECT a.account_id, tlc.old_level, tlc.new_level, tlc.reason, tlc.created_at
FROM trust_level_changes tlc
JOIN accounts a ON tlc.account_id = a.id
WHERE a.account_id = 'acct_a1b2c3d4'
ORDER BY tlc.created_at DESC;

7. 分发可靠性设计

7.1 分层分发模型

flowchart TB
    CLIENT[客户端工具]
    LOCAL[本地文件缓存
~/.par/cache/] API_SERVER["优先源:API 服务器(Phase 1)"] CDN_SERVER["回退源:CDN / 静态存储(Phase 2)"] FALLBACK[兜底:使用本地缓存] CLIENT -->|缓存命中| LOCAL CLIENT -->|缓存未命中| API_SERVER API_SERVER -->|超时/不可用| CDN_SERVER CDN_SERVER -->|仍然不可用| FALLBACK
图 3:分层分发模型(CDN 和多源回退为 Phase 2)

7.2 Phase 1 分发方案

Phase 1 采用 Spring Boot 内嵌 Tomcat 直接服务静态文件。配置 JSON 文件由 Java 应用在发布时写入本地磁盘,客户端通过 GET /v1/configs/{siteId} 由 Spring Boot Controller 读取并返回。API 同域静态文件服务,配合 ETag(基于文件内容的 MD5)和 Cache-Control 响应头实现高效同步。CDN 层和三级回退中的多源回滚将在 Phase 2 引入。

Phase 2 扩展

Phase 2 将在 Spring Boot 前面增加 Nginx 反向代理,Nginx 负责静态文件缓存和 CDN 回源,实现 CDN -> Nginx -> Spring Boot -> 本地缓存的三级回退。Nginx 不可用时仍可直连 Spring Boot 源。

7.3 客户端本地存储结构

~/.par/
├── cache/
│   ├── manifest.json           # 全局清单
│   └── configs/
│       ├── ptfans/
│       │   ├── v1.json         # 历史版本保留
│       │   ├── v2.json
│       │   ├── v3.json         # 当前最新
│       │   └── current.json → v3.json  # 软链接
│       └── ...
├── registry-pubkey.pem         # Registry 公钥(Phase 2 启用签名验证)
└── client-state.json           # 客户端状态

回滚只需切换 current.json 的软链接目标,客户端代码零改动。

7.4 更新策略

  • 默认每 6 小时检查一次 manifest
  • 客户端启动时检查一次
  • 遇到解析失败时,主动触发该站点的配置更新
  • 配置签名(JWS)在 Phase 2 引入,客户端用内嵌公钥验证真实性P2

8. 质量与信任体系

8.1 校验流水线

flowchart TD
    SUBMIT[提交配置] --> SCHEMA[1. JSON Schema 校验]
    SCHEMA -->|通过| SELECTOR[2. 选择器语法检查]
    SCHEMA -->|失败| ERR1[返回错误]
    SELECTOR -->|通过| SMOKE["3. 冒烟测试 - 可选(Phase 2)"]
    SELECTOR -->|失败| ERR2[返回错误]
    SMOKE -->|通过/跳过| ROUTE[4. 信任路由]
    ROUTE -->|trusted+ : 直接发布| PUBLISH[发布为 stable]
    ROUTE -->|member : 需审核| REVIEW[进入审核队列]
          
图 4:配置校验流水线

Phase 1 vs Phase 2

Phase 1 仅执行第 1 步(JSON Schema 校验)和第 2 步(选择器语法检查)。第 3 步冒烟测试为 P2,需要自动化请求站点页面验证选择器有效性。

8.2 三级信任体系

等级获取方式发布权限审核权限其他
member 手动注册(邮箱 + 密码) 发布(需审核) 无 审核超时 72h 自动通知通知为 P2
trusted 账户维度:连续 5 次发布无驳回 + 至少维护 2 个站点 免审核发布 审核他人配置 可紧急回滚
admin 项目维护者指定 全部 全部 管理信任等级 + 账户管理

8.3 统一审核模型

站点元信息变更和配置变更共享同一个审核队列。站点元信息也版本化,采用"提交 -> 审核 -> 发布"流程。

站点信息变更的权限矩阵

操作membertrustedadmin
新建站点提交 -> 审核提交 -> 审核直接生效
修改 tags/description提交 -> 审核直接生效直接生效
修改 domains提交 -> 审核提交 -> 审核直接生效
修改 siteType不允许提交 -> 审核直接生效
弃用站点不允许不允许直接生效弃用归档为 P2

9. 引擎模板系统 P2

对于同一引擎类型的站点(如 NexusPHP),80% 的配置是相同的。模板系统减少配置编写工作量。整个引擎模板系统属于 Phase 2 功能范围。

9.1 模板继承模型

flowchart LR
    TEMPLATE["引擎模板
nexusphp-default-v2.json
(服务端维护)
默认选择器 + 转换器 + capabilities"] SITE["站点配置
ptfans-config.json
(提交者编写)
覆盖差异部分"] RESULT["最终存储的完整配置
ptfans-v3.json
(不可变快照)"] TEMPLATE -->|"发布时自动合并"| RESULT SITE -->|"overrides"| RESULT
图 5:模板继承模型

关键原则

合并发生在发布时,不在运行时。客户端获取到的永远是完整的、可直接使用的配置,不需要了解模板系统的存在。

9.2 差异提交

发布配置时支持只写差异部分:

POST /v1/configs/ptfans
{
  "extends": "nexusphp@v2",       // 继承哪个模板
  "overrides": {                  // 只写差异部分
    "siteId": "ptfans",
    "domains": ["ptfans.cc"],
    "pages": {
      "search": {
        "fields": {
          "title": { "selector": "td.name a font", "type": "text" }
        }
      }
    }
  },
  "changelog": "覆盖默认标题选择器"
}

10. 前端设计

10.1 mediabot 侧(主要前端)

mediabot 是面向用户的完整前端,承担以下职责:

站点录入表单

mediabot 适配器管理页面提供 [新增站点] 按钮,填写 siteId(自动校验可用性)、siteName、siteType(下拉选择)、domains、tags。支持 [仅本地创建] 和 [提交到 PAR] 两种模式。

适配器编辑器

mediabot 内置的可视化编辑器,支持实时预览解析结果。编辑完成后通过 PAR API 提交发布。

PAR 账户注册与登录

mediabot 设置页提供 PAR 账户管理入口。用户首次点击"发布到 PAR"时,mediabot 引导用户注册(邮箱 + 密码)或登录,获取 API Key 后本地加密存储。后续写入操作自动携带 API Key 和 HMAC 请求签名。

多账户管理 P1

mediabot 支持在同一设备上管理多个 PAR 账户。用户在 mediabot 本地添加多个账户,每个账户独立存储 API Key。切换账户时 mediabot 自动使用对应账户的 API Key 和 HMAC 签名。PAR 端不感知多账户关系,每个账户独立认证、独立计算信任等级。

10.2 PAR Admin(管理面板)

纯内部管理工具,只有 admin 和 trusted 用户使用。API Key 直接登录,不需要自建登录系统。

页面路径功能优先级
仪表盘/ 站点总数、配置总数、待审核数、近期活动时间线 P1
审核工作台/reviews 待审核队列(配置变更 + 站点变更混合)、配置 diff 对比、批准/驳回 P1
用户管理/admin/users 用户列表、信任等级调整、封禁/解封、账户操作记录 P1
站点管理/admin/sites 站点列表、版本历史、回滚操作 P1
模板管理/admin/templates 引擎模板列表、版本管理、编辑模板 P2
全局设置/admin/settings Schema 版本管理、冒烟测试开关、公钥轮换 P2

技术选型

  • React + Vite(或其他 SPA 框架)
  • Monaco Diff Editor(审核时的配置对比)
  • 部署方式:Spring Boot 直接托管 SPA 静态资源,/admin/* 路径由 Spring Boot 的 ResourceHandler 处理,无需 Nginx

11. 认证、HMAC 签名与安全设计

11.1 认证模型:手动注册 + 登录

PAR 采用简单的账户模型,取消了两层模型(Account + Identity)和自动关联机制:

  • 读取完全公开:任何人无需注册即可下载配置,降低客户端接入成本。
  • 写入需要注册:使用邮箱 + 密码手动注册,密码使用 bcrypt 存储。
  • 登录获取 API Key:登录成功后返回 API Key,后续写入请求用 API Key 进行 HMAC 请求签名认证。
  • 找回密码:通过邮件验证码重置密码。

11.2 HMAC 请求签名

所有写入 API 除 Authorization: Bearer {apiKey} 外,还必须携带 HMAC 请求签名,防止 API Key 泄露后被滥用。

签名机制

  • 请求头:
    • X-Timestamp:Unix 时间戳(秒)
    • X-Signature:HMAC-SHA256 签名值(Base64 编码)
  • 签名内容:
    HTTP方法 + "\n" + 请求路径 + "\n" + 时间戳 + "\n" + 请求体SHA-256

    无请求体时,最后一项为空字符串。

  • 签名计算:HMAC-SHA256(apiKey, 签名内容)

PAR 验证逻辑

  1. 时间戳校验:与服务器时间差 <= 60 秒
  2. 签名匹配:使用同一 apiKey 重新计算签名,与请求头对比
  3. 防重放:缓存最近 60 秒内的签名,重复签名直接拒绝

为什么读取 API 不需要签名

读取 API 是公开资源,不需要 API Key,自然也不需要 HMAC 签名。这保证了客户端(包括未注册用户)可以零成本获取配置。HMAC 签名仅保护写入操作,即使 API Key 在传输中被截获,攻击者也无法在 60 秒窗口外重放请求。

11.3 注册与登录流程

sequenceDiagram
    participant U as mediabot 用户
    participant M as mediabot
    participant P as PAR

    U->>M: 点击"发布到 PAR"
    M->>M: 提示注册或登录
    alt 注册
        U->>M: 输入邮箱 + 密码
        M->>P: POST /v1/account/register
{email, password, anonymousId?} Note right of P: bcrypt 存储密码
生成 API Key P-->>M: 返回 accountId M-->>U: 注册成功,请登录 end U->>M: 输入邮箱 + 密码 M->>P: POST /v1/account/login
{email, password} Note right of P: 验证 bcrypt 密码
返回 API Key P-->>M: 返回 apiKey + trustLevel M->>M: 本地加密存储 apiKey U->>M: 提交配置发布 M->>M: 计算 HMAC 签名
X-Timestamp + X-Signature M->>P: POST /v1/configs/ptfans
Authorization: Bearer {apiKey}
X-Timestamp: 1234567890
X-Signature: hmac_sha256(...) Note right of P: 验证时间戳
验证 HMAC 签名
防重放检查 P-->>M: 返回校验结果 + 版本号 M-->>U: 显示发布结果
图 6:mediabot -> PAR 注册登录与 HMAC 签名流程

11.4 API Key 安全实现

  • 生成:mbt_ 前缀 + 32 字节 secrets.token_urlsafe 随机值
  • 存储:数据库只存 SHA-256 哈希,明文只在登录响应中返回
  • 验证:客户端发送 Bearer token + HMAC 签名 -> 服务端计算哈希查库 -> 校验签名 -> 校验账户权限
  • 日志脱敏:只记录前 8 字符前缀(mbt_k7x9...)
  • 重置/刷新:支持主动刷新,旧 key 5 分钟宽限期后失效

11.5 密码安全

  • 存储:bcrypt(cost factor 12+),不存明文密码
  • 重置:邮件验证码 15 分钟有效期,使用后立即失效
  • 限制:连续 5 次登录失败锁定账户 15 分钟

11.6 配置签名 P2

每个配置版本由 registry 私钥签名(JWS 格式),客户端用内嵌公钥验证。确保即使通过非安全渠道获取配置,也能确认来源可信。checksum(SHA-256)保证完整性,signature 保证真实性,两者缺一不可。Phase 1 暂不启用配置签名。

12. 匿名统计与注册关联

12.1 设计目标

PAR 需要了解配置的活跃使用情况(哪些站点配置下载最多、有多少活跃用户),但不想强制用户注册即可读取配置。匿名统计机制在保护隐私的前提下提供基础使用数据:

  • 完全可选:客户端可选择不发送匿名 ID,不影响任何功能
  • 隐私保护:匿名 ID 是邮箱的 SHA-256 哈希,不可逆推原始邮箱
  • 注册后自动关联:同一邮箱的哈希值匹配,匿名数据与注册账户自动关联
  • 用户可控:用户可在 mediabot 设置中关闭匿名统计

12.2 匿名 ID 生成

mediabot 计算用户邮箱的 SHA-256 哈希作为 anonymous_id:

anonymous_id = SHA-256(email.trim().toLowerCase())

读取请求可选携带 X-Anonymous-Id: {anonymous_id} 请求头。PAR 用此 ID 统计活跃用户数、配置下载热度等。

为什么不直接用 deviceId

deviceId 会暴露设备指纹,且重装后变化。邮箱哈希虽然也不完美,但用户更换邮箱的概率低于重装工具,且哈希不可逆推,隐私风险可控。用户关闭统计后,mediabot 不再发送该头。

12.3 注册时自动关联

用户注册时,mediabot 可选择将当前 anonymous_id 一并提交:

POST /v1/account/register
{
  "email": "user@qq.com",
  "password": "yourPassword123",
  "anonymousId": "sha256_of_user@qq.com"   // 可选
}

PAR 收到后:

  1. 创建账户,计算该邮箱的 SHA-256 哈希
  2. 如果 anonymous_stats 表中存在匹配的 anonymous_id,将 linked_account_id 指向新账户
  3. 更新 accounts.linked_anonymous_id 字段
  4. 后续该用户的匿名统计数据即与注册账户关联

12.4 数据统计用途

指标说明
活跃用户数按 anonymous_id 去重统计最近 30 天有读取请求的用户数
配置下载热度各站点配置的下载次数排名
注册转化率匿名用户中后续注册并关联的比例
客户端分布按 User-Agent 分析使用的工具类型和版本

12.5 隐私与关闭机制

  • mediabot 默认开启匿名统计,但用户可在设置中关闭
  • 关闭后,mediabot 不再发送 X-Anonymous-Id 头
  • PAR 不存储任何可逆推个人身份的信息(只存 SHA-256 哈希)
  • 匿名统计数据保留 90 天,过期自动清理
  • 用户注销账户时,可选择删除关联的匿名统计数据

与 GDPR 的兼容性

匿名 ID 是单向哈希,技术上无法还原为邮箱。如果用户关闭统计,PAR 不记录任何该用户的数据。注销时删除关联数据即可满足"被遗忘权"要求。

13. 站点元信息版本化

mediabot 侧提供站点信息编辑入口,提交到 PAR 后进入审核流程。站点元信息和配置版本共享统一的"提交 -> 审核 -> 发布"模型。

13.1 版本历史可追溯

GET /v1/sites/ptfans/meta/versions

→ {
  "versions": [
    { "version": 3, "status": "published",
      "domains": ["ptfans.to", "ptfans.org"],
      "changelog": "站点域名迁移到 .to",
      "submittedBy": "user@qq.com" },
    { "version": 2, "status": "published",
      "domains": ["ptfans.cc"],
      "changelog": "更新描述信息" }
  ]
}

13.2 审核工作台统一视图

PAR Admin 审核工作台混合展示配置变更和站点信息变更,支持按类型筛选:

/reviews(审核工作台)
├── 待审核队列(混合显示)
│   ├── [config]  ptfans v4 — 修复搜索选择器
│   ├── [site]    ptfans 域名变更为 ptfans.to
│   ├── [config]  hdfans v2 — 新增签到页解析
│   └── [site]    hdfans 新增备用域名
├── 筛选标签:全部 / 配置变更 / 站点变更
└── 操作:批准 / 驳回(驳回必填原因)

14. Schema 演进与站点生命周期

14.1 Schema 演进策略

Schema 版本号规则:主版本.次版本(如 2.0 -> 2.1 -> 3.0)

类型范围客户端处理
次版本更新(向后兼容)新增可选字段、新增 transform 类型、新增 page key忽略未知字段
主版本更新(不兼容)删除/重命名核心字段、改变字段语义需要迁移工具适配

每个配置快照记录 $schema 字段,Manifest 中声明 minClientSchemaVersion,客户端发现不兼容时提示升级。通过 GET /v1/schemas 接口提供所有 Schema 版本的 JSON Schema 文档。OpenAPI 规范自动生成 API 文档为 Phase 2

14.2 站点弃用与归档 P2

PT 站点会关闭、合并、更换域名,PAR 提供站点生命周期管理:

POST /v1/sites/{siteId}/deprecate
Authorization: Bearer {apiKey}
{
  "reason": "站点已关闭",
  "redirectSiteId": "newsite",
  "sunsetAt": "2026-08-01"
}
active
→
deprecated
→
inactive
→
archived

Manifest 中增加 status 和 deprecation 信息,客户端据此引导用户迁移。

15. 通知、限流与监控

15.1 通知机制 P2

双通道通知:Webhook(可选,给实时系统用)+ 邮件(兜底,审核结果通知提交者)。

事件触发时机通知对象
config.pending_reviewmember 提交配置审核员
config.approved审核通过提交者
config.rejected审核驳回提交者
config.published新版本发布订阅 Webhook 的系统
config.rolled_back执行回滚订阅 Webhook 的系统
site.deprecated站点标记为弃用订阅 Webhook 的系统

15.2 API 限流(写入侧)

读取不限流(公开资源),写入 API 按账户限流:

接口限流规则
POST /v1/configs/*每账户每分钟 10 次,每小时 30 次
POST /v1/sites每账户每小时 5 次
POST /v1/account/register每 IP 每小时 5 次
POST /v1/account/login每 IP 每分钟 10 次,连续失败 5 次锁定 15 分钟
POST /v1/account/forgot-password每 IP 每小时 3 次
POST /v1/account/reset-key每 API Key + HMAC 每天 3 次
其他管理 API每 API Key 每分钟 30 次

响应头携带 X-RateLimit-Limit、X-RateLimit-Remaining、X-RateLimit-Reset,超限返回 429 Too Many Requests。

15.3 监控与告警

指标告警阈值含义
配置下载成功率< 99.5%Spring Boot 服务或存储故障
API 响应延迟 P99> 2s数据库或服务端问题
待审核队列积压> 24h审核员不活跃
冒烟测试失败率> 10%(单站点)站点可能改版P2

通过 GET /v1/health 接口暴露健康状态,结合 UptimeRobot 等外部监控即可满足需求。