Files
pangolin/doc/03-database.html
T
wangjia 3ae96ef229 docs: 新增整体架构设计文档(doc/,HTML 七章)
覆盖前端五端/后端/MySQL/弹性节点拓扑/Web安全/安全总纲,遵循 design/ 设计系统视觉呈现。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 23:57:49 +08:00

240 lines
13 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>03 数据库 · 穿山甲架构设计</title>
<link rel="stylesheet" href="assets/style.css">
</head>
<body>
<header class="site-header"><div class="inner">
<a class="brand" href="index.html"><img src="assets/logo-mark.svg" alt="Pangolin"><span><span class="zh">穿山甲 · 架构设计</span><br><span class="en">Pangolin Architecture</span></span></a>
<nav class="site-nav">
<a href="index.html">总览</a>
<a href="01-frontend.html">01 前端</a>
<a href="02-backend.html">02 后端</a>
<a href="03-database.html" class="active">03 数据库</a>
<a href="04-infra.html">04 网络拓扑</a>
<a href="05-web-security.html">05 Web 安全</a>
<a href="06-security.html">06 安全总纲</a>
</nav>
<button class="theme-btn" id="themeBtn" title="切换深浅主题"><i data-lucide="moon"></i></button>
</div></header>
<main class="doc">
<div class="doc-hero">
<div class="overline">Chapter 03 · Database</div>
<h1>数据库设计</h1>
<p class="lede"><strong>MySQL 8.x</strong>InnoDB · utf8mb4 · 时区 UTC)作为主数据存储,Redis 承担验证码、限流、节点实时状态与会话。schema 自蓝本 §2 全量移植并做 MySQL 类型适配,另为弹性拓扑(04 章)扩展三张表。</p>
</div>
<section id="conventions">
<h2><span class="num">§1</span>移植约定(Postgres 蓝本 → MySQL 8</h2>
<div class="tbl-wrap"><table>
<thead><tr><th>蓝本用法</th><th>MySQL 8 适配</th><th>说明</th></tr></thead>
<tbody>
<tr><td><code>jsonb</code></td><td><code>JSON</code></td><td>审计 meta、节点标签等</td></tr>
<tr><td><code>tags[]</code>(数组)</td><td><code>JSON</code>(数组)</td><td>查询用 <code>JSON_CONTAINS</code>,量大再拆关联表</td></tr>
<tr><td><code>timestamptz</code></td><td><code>DATETIME(6)</code>,应用层一律 UTC</td><td>连接串强制 <code>time_zone='+00:00'</code></td></tr>
<tr><td>枚举字符串</td><td><code>ENUM(...)</code></td><td>状态机字段固定且少,ENUM 自带约束</td></tr>
<tr><td>主键</td><td><code>BIGINT UNSIGNED AUTO_INCREMENT</code> 内部主键 + 对外 <code>uuid CHAR(36)</code> 业务标识</td><td>避免顺序 ID 外泄可枚举;UUID 不做主键防 InnoDB 页分裂</td></tr>
</tbody>
</table></div>
</section>
<section id="schema">
<h2><span class="num">§2</span>核心表(DDL 摘要)</h2>
<pre><code><span class="c">-- 账户与设备</span>
CREATE TABLE users (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
uuid CHAR(36) NOT NULL UNIQUE, <span class="c">-- 对外标识</span>
email VARCHAR(255) NOT NULL UNIQUE,
pw_hash VARCHAR(255) NOT NULL, <span class="c">-- argon2id</span>
dp_uuid CHAR(36) NOT NULL, <span class="c">-- 数据面凭证 UUID(可轮换,节点侧只见它)</span>
status ENUM('active','banned') NOT NULL DEFAULT 'active',
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE devices (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
uuid CHAR(36) NOT NULL UNIQUE,
user_id BIGINT UNSIGNED NOT NULL,
name VARCHAR(64) NOT NULL,
platform ENUM('ios','android','windows','macos') NOT NULL,
last_seen DATETIME(6) NULL,
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
FOREIGN KEY (user_id) REFERENCES users(id),
INDEX idx_user (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
<span class="c">-- 套餐与订阅(数字口径 = design/CLAUDE.md §7</span>
CREATE TABLE plans (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
code ENUM('free','pro','team') NOT NULL UNIQUE,
max_devices INT NOT NULL, <span class="c">-- free 1 / pro 5 / team 10</span>
daily_minutes INT NULL, <span class="c">-- free 10NULL = 不限</span>
ad_gate BOOLEAN NOT NULL DEFAULT FALSE <span class="c">-- free 每日看广告解锁</span>
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE subscriptions (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL,
plan_id BIGINT UNSIGNED NOT NULL,
expires_at DATETIME(6) NOT NULL,
source ENUM('trial','code') NOT NULL, <span class="c">-- 注册自动 7 天试用 source='trial'</span>
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (plan_id) REFERENCES plans(id),
INDEX idx_user_exp (user_id, expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
<span class="c">-- 激活码(App 内无支付,资金流外部化)</span>
CREATE TABLE code_batches (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
channel ENUM('store','tg','line','manual') NOT NULL,
created_by VARCHAR(64) NOT NULL,
note VARCHAR(255) NULL,
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE codes (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
code_hash CHAR(64) NOT NULL UNIQUE, <span class="c">-- SHA-256;明文只出现一次</span>
plan_id BIGINT UNSIGNED NOT NULL,
duration_days INT NOT NULL,
batch_id BIGINT UNSIGNED NOT NULL,
status ENUM('unused','redeemed','void') NOT NULL DEFAULT 'unused',
redeemed_by BIGINT UNSIGNED NULL,
redeemed_at DATETIME(6) NULL,
FOREIGN KEY (plan_id) REFERENCES plans(id),
FOREIGN KEY (batch_id) REFERENCES code_batches(id),
INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
<span class="c">-- 用量(最小数据原则:仅字节/分钟,无目的地)</span>
CREATE TABLE usage_daily (
user_id BIGINT UNSIGNED NOT NULL,
date DATE NOT NULL,
bytes_up BIGINT UNSIGNED NOT NULL DEFAULT 0,
bytes_down BIGINT UNSIGNED NOT NULL DEFAULT 0,
minutes_used INT NOT NULL DEFAULT 0,
ad_unlocked_at DATETIME(6) NULL, <span class="c">-- 免费版当日激励视频解锁时刻</span>
PRIMARY KEY (user_id, date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
<span class="c">-- 审计(兑换/封禁/节点操作必记)</span>
CREATE TABLE audit_log (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
actor VARCHAR(64) NOT NULL,
action VARCHAR(64) NOT NULL,
target VARCHAR(128) NOT NULL,
meta JSON NULL,
at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
INDEX idx_at (at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;</code></pre>
</section>
<section id="topo-schema">
<h2><span class="num">§3</span>节点与弹性拓扑表(支撑 04 章)</h2>
<pre><code><span class="c">-- 节点:蓝本 nodes 表 + 拓扑/生命周期扩展字段</span>
CREATE TABLE nodes (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
uuid CHAR(36) NOT NULL UNIQUE,
region VARCHAR(8) NOT NULL, <span class="c">-- HK / JP / SG / US…(UI 显示 2 字母码块)</span>
name_zh VARCHAR(64) NOT NULL,
name_en VARCHAR(64) NOT NULL,
role ENUM('entry','relay','exit') NOT NULL DEFAULT 'entry',
tier ENUM('free','pro') NOT NULL, <span class="c">-- 消耗品池 / 精品池</span>
endpoint VARCHAR(255) NOT NULL, <span class="c">-- ip:portREALITY</span>
hy2_port INT NULL, <span class="c">-- Hysteria2 端口跳跃区间起点</span>
reality_pbk VARCHAR(64) NOT NULL,
reality_sni VARCHAR(128) NOT NULL, <span class="c">-- 伪装目标 SNI</span>
provider_id BIGINT UNSIGNED NOT NULL,
tags JSON NULL,
status ENUM('provisioning','probing','up','draining','down','destroyed')
NOT NULL DEFAULT 'provisioning', <span class="c">-- 生命周期状态机(04 章 §3</span>
weight INT NOT NULL DEFAULT 100,
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
FOREIGN KEY (provider_id) REFERENCES providers(id),
INDEX idx_status_tier (status, tier)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
<span class="c">-- VPS 厂商池(多厂商、账号隔离,见 04 章 §5)</span>
CREATE TABLE providers (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(64) NOT NULL,
api_kind VARCHAR(32) NOT NULL, <span class="c">-- 开通自动化所用 API 适配器</span>
regions JSON NOT NULL,
pool ENUM('consumable','premium') NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
note VARCHAR(255) NULL <span class="c">-- 凭证不入库,存独立 secrets 管理</span>
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
<span class="c">-- 节点事件:状态迁移与判封依据(探针明细在 Redis,结论落库)</span>
CREATE TABLE node_events (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
node_id BIGINT UNSIGNED NOT NULL,
event ENUM('provisioned','probe_pass','probe_fail','marked_up','draining',
'blocked_suspect','blocked_confirmed','replaced','destroyed') NOT NULL,
detail JSON NULL, <span class="c">-- 探针命中率、判封依据等</span>
at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
FOREIGN KEY (node_id) REFERENCES nodes(id),
INDEX idx_node_at (node_id, at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
<span class="c">-- 目录版本:nodes 任何变更 bump,客户端 if_version 灰度</span>
CREATE TABLE directory_version (
id TINYINT PRIMARY KEY DEFAULT 1,
version BIGINT UNSIGNED NOT NULL,
CHECK (id = 1)
) ENGINE=InnoDB;</code></pre>
<div class="callout"><b>节点表里没有的东西(有意为之)</b>节点服务器上不落任何用户身份;本表也不存节点登录凭证(SSH 密钥、厂商 API key 走独立 secrets 管理,与运营身份隔离)。</div>
</section>
<section id="redis">
<h2><span class="num">§4</span>Redis 键设计</h2>
<div class="tbl-wrap"><table>
<thead><tr><th>键模式</th><th>类型 / TTL</th><th>用途</th></tr></thead>
<tbody>
<tr><td><code>auth:code:{email}</code></td><td>STRING · 10min</td><td>邮箱验证码(同邮箱 1/min 限发)</td></tr>
<tr><td><code>rl:{scope}:{key}</code></td><td>ZSET · 滑动窗口</td><td>接口限流(IP / 用户 / 邮箱多维度)</td></tr>
<tr><td><code>jwt:refresh:{jti}</code></td><td>STRING · 30d</td><td>refresh token 白名单(登出/封禁即删)</td></tr>
<tr><td><code>node:load:{node_uuid}</code></td><td>HASH · 90s</td><td>agent 每 30s 上报:在线数/带宽/CPU——目录排序与客户端"信号条"数据源</td></tr>
<tr><td><code>probe:{node_uuid}:{vantage}</code></td><td>STRING · 30min</td><td>境内外探针最近结果(判封原始数据)</td></tr>
<tr><td><code>dir:version</code></td><td>STRING</td><td>目录版本缓存(DB 为权威)</td></tr>
<tr><td><code>redeem:fail:{user_id}</code></td><td>STRING · 1h</td><td>兑换失败计数(5 次锁 1 小时)</td></tr>
</tbody>
</table></div>
<p class="muted">Redis 全部可重建(无权威数据),故不参与备份;重启后由 agent 心跳与 DB 回填。</p>
</section>
<section id="backup">
<h2><span class="num">§5</span>备份与恢复</h2>
<div class="tbl-wrap"><table>
<thead><tr><th></th><th>策略</th><th>目标</th></tr></thead>
<tbody>
<tr><td>全量</td><td>每日 <code>xtrabackup</code>(或托管 DB 自动快照)</td><td rowspan="2">RPO ≤ 15min</td></tr>
<tr><td>增量</td><td>binlog 持续归档(15min 滚动上传)</td></tr>
<tr><td>加密与异地</td><td>备份用 <code>age</code> 公钥加密后传对象存储(与生产、与运营身份均隔离的账号);保留 30 天</td><td>单点被端不丢数据</td></tr>
<tr><td>恢复演练</td><td>每月在隔离环境做一次全量 + binlog 回放演练,记录耗时</td><td>RTO ≤ 4h(控制面整体重建含 DB,见 05 章)</td></tr>
</tbody>
</table></div>
<div class="callout warn"><b>备份内容敏感性</b>用户库(邮箱)是全系统最敏感资产:备份加密私钥离线保存、不进任何在线主机;对象存储账号用独立身份注册,与 VPS / 域名 / 收款不产生关联(身份隔离红线,见 06 章)。</div>
</section>
</main>
<nav class="pager">
<a href="02-backend.html"><span>上一章</span><b>← 02 后端设计</b></a>
<a class="next" href="04-infra.html"><span>下一章</span><b>04 机器与网络拓扑 →</b></a>
</nav>
<footer class="colophon">穿山甲 · Pangolin — 内部架构设计文档 · 遵循 design/ 设计系统 · 2026-06</footer>
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
<script src="assets/doc.js"></script>
</body>
</html>