PAR — PT Adapter Registry
PT 站点适配配置的版本化注册中心,负责配置的存储、校验、分发和生命周期管理
1. 系统定位与设计目标
PAR(PT Adapter Registry)是一个面向 PT 自动化工具生态的站点适配配置注册中心,以"静态分发 + 受控写入"为核心模型,提供配置的版本化管理、自动化校验、可靠分发和质量保障。
系统主要配套给 PT 自动化管理工具(如 mediabot),作为 PT 站点目录、HTML 解析规则、接口数据处理策略等配置的共享和管理中心,让多个使用 PT 自动化工具的系统可以方便地获取和更新这些配置。
1.1 设计目标(按优先级排列)
- 分发可靠性第一 — 工具拿不到配置就等于瘫痪,可用性比功能丰富更重要
- 多工具兼容 — 配置格式与具体工具解耦,mediabot、自定义脚本等都能消费
- 接入成本趋近于零 — 客户端不需要注册、不需要登录,开箱即用
- 写入质量可控 — 发布有门槛,防止劣质配置污染生态
- 运维成本可控 — 社区项目,资源有限,架构必须简单
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 | 审核工作台 + 用户管理 + 站点管理 + 回滚 |
| 客户端 SDK | Manifest 同步 + 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
3.1 工作方式
- 配置发布流程:通过管理 API 提交 -> 校验通过 -> 写入数据库元信息 + 生成静态 JSON 文件到本地磁盘
- 读取流程:客户端请求
GET /v1/configs/{siteId},由 Spring Boot Controller 处理,支持 ETag 条件请求,响应携带Cache-Control头CDN 为 Phase 2,Phase 1 仅 API 同域 - 管理 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/sites | API 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}/validate | API Key + HMAC | 预校验(不真正发布) | P1 |
POST | /v1/configs/{siteId}/rollback | API Key + HMAC | 回滚到指定版本(trusted+) | P1 |
POST | /v1/account/register | 公开 | 注册(email + password) | P1 |
POST | /v1/account/login | 公开 | 登录(email + password),返回 apiKey | P1 |
POST | /v1/account/forgot-password | 公开 | 忘记密码(发送邮件验证码) | P1 |
POST | /v1/account/reset-password | 公开 | 重置密码(code + newPassword) | P1 |
GET | /v1/account/me | API Key | 查询自身账户信息 | P1 |
POST | /v1/account/reset-key | API Key + HMAC | 重置 API Key | P1 |
POST | /v1/account/refresh-key | API Key + HMAC | 刷新 API Key | P1 |
POST | /v1/account/webhooks | API Key + HMAC | 注册 Webhook 回调 | P2 |
GET | /v1/reviews/pending | API Key | 待审核列表(reviewer+) | P1 |
POST | /v1/reviews/{siteId}/config/v{ver}/approve | API Key + HMAC | 批准配置变更 | P1 |
POST | /v1/reviews/{siteId}/config/v{ver}/reject | API Key + HMAC | 驳回配置变更 | P1 |
POST | /v1/reviews/{siteId}/meta/v{ver}/approve | API Key + HMAC | 批准站点信息变更 | P1 |
POST | /v1/reviews/{siteId}/meta/v{ver}/reject | API 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(同步入口)
{
"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 条件请求)
{
"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 启用
预校验接口
供 mediabot 在发布前调用,不真正创建版本,只返回校验结果。
{
"valid": false,
"errors": [
{ "path": "pages.search.fields.title.selector",
"message": "无效的 CSS 选择器语法" }
]
}
注册
邮箱注册,密码使用 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" }
登录
邮箱 + 密码登录,返回 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 } }
忘记密码
发送邮件验证码到注册邮箱,验证码 15 分钟内有效。
// 请求 { "email": "user@qq.com" } // 响应 { "sent": true, "message": "验证码已发送到邮箱,15分钟内有效" }
重置密码
使用邮件验证码重置密码,重置成功后当前 API Key 失效,需重新登录。
// 请求 { "email": "user@qq.com", "code": "123456", "newPassword": "newPassword456" } // 响应 { "success": true, "message": "密码已重置,请重新登录" }
刷新 API Key
主动刷新 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 (可选继承)"
6.2 表结构
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 — 站点元信息
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 — 站点配置版本(核心表)
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 — 站点元信息变更历史
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 — 匿名统计数据
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 — 邮件验证码
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_version | P2 |
template_versions | 引擎模板版本快照 | 不可变,存储 config_path 和 checksum | P2 |
config_reviews | 配置审核记录 | 与 site_configs 1:1,含审核意见 | P1 |
trust_level_changes | 信任等级变更审计 | 记录 account_id、old_level、new_level、reason、operator | P1 |
rollback_history | 回滚操作记录 | from_version、to_version、operator_id | P1 |
api_access_log | API 访问日志 | 高频写入,建议定期归档 | P1 |
anonymous_stats | 匿名统计数据 | anonymous_id、request_count、ip_address、linked_account_id | P1 |
verification_codes | 邮件验证码 | email、code、purpose、expires_at、used_at | P1 |
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
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[进入审核队列]
Phase 1 vs Phase 2
Phase 1 仅执行第 1 步(JSON Schema 校验)和第 2 步(选择器语法检查)。第 3 步冒烟测试为 P2,需要自动化请求站点页面验证选择器有效性。
8.2 三级信任体系
| 等级 | 获取方式 | 发布权限 | 审核权限 | 其他 |
|---|---|---|---|---|
| member | 手动注册(邮箱 + 密码) | 发布(需审核) | 无 | 审核超时 72h 自动通知通知为 P2 |
| trusted | 账户维度:连续 5 次发布无驳回 + 至少维护 2 个站点 | 免审核发布 | 审核他人配置 | 可紧急回滚 |
| admin | 项目维护者指定 | 全部 | 全部 | 管理信任等级 + 账户管理 |
8.3 统一审核模型
站点元信息变更和配置变更共享同一个审核队列。站点元信息也版本化,采用"提交 -> 审核 -> 发布"流程。
站点信息变更的权限矩阵
| 操作 | member | trusted | admin |
|---|---|---|---|
| 新建站点 | 提交 -> 审核 | 提交 -> 审核 | 直接生效 |
| 修改 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
关键原则
合并发生在发布时,不在运行时。客户端获取到的永远是完整的、可直接使用的配置,不需要了解模板系统的存在。
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 验证逻辑
- 时间戳校验:与服务器时间差 <= 60 秒
- 签名匹配:使用同一 apiKey 重新计算签名,与请求头对比
- 防重放:缓存最近 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: 显示发布结果
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 收到后:
- 创建账户,计算该邮箱的 SHA-256 哈希
- 如果
anonymous_stats表中存在匹配的anonymous_id,将linked_account_id指向新账户 - 更新
accounts.linked_anonymous_id字段 - 后续该用户的匿名统计数据即与注册账户关联
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" }
Manifest 中增加 status 和 deprecation 信息,客户端据此引导用户迁移。
15. 通知、限流与监控
15.1 通知机制 P2
双通道通知:Webhook(可选,给实时系统用)+ 邮件(兜底,审核结果通知提交者)。
| 事件 | 触发时机 | 通知对象 |
|---|---|---|
config.pending_review | member 提交配置 | 审核员 |
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 等外部监控即可满足需求。