3ae96ef229
覆盖前端五端/后端/MySQL/弹性节点拓扑/Web安全/安全总纲,遵循 design/ 设计系统视觉呈现。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
240 lines
13 KiB
HTML
240 lines
13 KiB
HTML
<!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 10;NULL = 不限</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:port(REALITY)</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>
|