docs: 新增整体架构设计文档(doc/,HTML 七章)

覆盖前端五端/后端/MySQL/弹性节点拓扑/Web安全/安全总纲,遵循 design/ 设计系统视觉呈现。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-11 23:57:49 +08:00
parent 4bff8593e7
commit 3ae96ef229
10 changed files with 1688 additions and 0 deletions
+152
View File
@@ -0,0 +1,152 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>01 前端架构 · 穿山甲架构设计</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" class="active">01 前端</a>
<a href="02-backend.html">02 后端</a>
<a href="03-database.html">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 01 · Frontend</div>
<h1>前端架构</h1>
<p class="lede">原则:性能、稳定性、用户体验优先;不同平台允许不同技术栈,但视觉与交互必须 100% 还原 <code>design/</code> 设计系统——以 <code>ui_kits/</code> React 原型为像素验收标准,以 <code>colors_and_type.css</code> 为令牌唯一真相源。</p>
</div>
<section id="stack">
<h2><span class="num">§1</span>分端技术栈选型</h2>
<div class="tbl-wrap"><table>
<thead><tr><th></th><th>技术栈</th><th>选型理由(性能 / 稳定 / UX</th><th>像素基准</th></tr></thead>
<tbody>
<tr>
<td><strong>移动 App</strong><br><span class="muted">iOS / Android</span></td>
<td><strong>Flutter</strong> + sing-box <code>libbox</code>gomobile 桥接)<br>iOS NetworkExtension / Android VpnService</td>
<td>需要系统级隧道能力,Web 栈不可行;Flutter 自绘渲染保证两端像素一致,<code>design/flutter/</code> 起步包(theme + widgets + main)拿来即跑。</td>
<td><code>ui_kits/mobile/</code></td>
</tr>
<tr>
<td><strong>iPad 客户端</strong></td>
<td><strong>Flutter</strong>——与移动 App 同一工程,宽度 ≥900 自适应切换为侧栏分栏布局</td>
<td>不是第五套代码:复用移动端全部原子组件与内核层,仅布局开关;横屏 1180×820 基准,左侧栏导航(触控尺寸,行高 ≥48px),连接页双栏(左大连接键 / 右信息列),节点双列网格。</td>
<td><code>ui_kits/tablet/</code></td>
</tr>
<tr>
<td><strong>桌面客户端</strong><br><span class="muted">Windows / macOS</span></td>
<td><strong>Flutter desktop</strong> + sing-box 子进程(TUN 模式)</td>
<td>与移动端共享 ~90% UI 代码(连接键、节点列表、统计、账户全复用),920×600 固定窗 + 侧栏布局照搬原型;避免 Electron 的内存与启动开销。</td>
<td><code>ui_kits/desktop/</code></td>
</tr>
<tr>
<td><strong>官网</strong></td>
<td><strong>Astro</strong>SSG)→ Cloudflare Pages</td>
<td>纯静态 = 极致加载性能 + 整站可秒级复制到任意备用域名(灾备关键,见 05 章);Astro 直接吃 JSX<code>ui_kits/website</code> 组件近乎逐字迁移。</td>
<td><code>ui_kits/website/</code></td>
</tr>
<tr>
<td><strong>Web 用户中心</strong></td>
<td><strong>Next.js</strong>(React,静态导出 + 客户端取数)</td>
<td><code>ui_kits/usercenter</code> 本身是 React——组件直接复用是保真上限;静态导出后同样可多镜像部署,动态数据全走 API。</td>
<td><code>ui_kits/usercenter/</code></td>
</tr>
</tbody>
</table></div>
<div class="callout"><b>为什么不强行统一一个栈</b>
客户端离不开系统级隧道与开机自启,必须是原生壳;Web 端的两个原型本身就是 React,重写成 Flutter Web 反而损失保真与首屏性能。五个产品面、四套实现(iPad 归入移动端工程的自适应断点),通过<strong>同一套设计令牌</strong>CSS 变量 ↔ <code>pangolin_theme.dart</code> 一一对应)保证视觉同源——栈不同,像素相同。</div>
</section>
<section id="client-arch">
<h2><span class="num">§2</span>客户端分层架构(移动 + 桌面共享)</h2>
<div class="diagram">
<div class="stack">
<div class="layer accent"><span class="t">UI 层(共享 ~90%</span><span class="d">flutter/widgets/:连接键三态 · 智能选择推荐卡 · 免费额度卡 · 节点列表 · 统计 · 账户;明暗双主题 + 中英单显;宽度 ≥900 自适应切侧栏分栏布局(iPad / 桌面同构)</span></div>
<div class="layer"><span class="t">状态层</span><span class="d">Riverpod:连接状态机(off / connecting / on)· 会话 · 节点目录(带 version)· 用量额度</span></div>
<div class="layer"><span class="t">服务层</span><span class="d">API client(域名池 + 重试退避 + 响应签名校验)· 节点目录缓存 · 订阅凭证存储(Keychain / Keystore</span></div>
<div class="layer"><span class="t">内核层</span><span class="d">sing-box libbox(移动:gomobile AAR/XCFramework;桌面:子进程 + TUN)· URLTest 智能选线 · Kill-switch</span></div>
<div class="layer"><span class="t">平台壳</span><span class="d">iOS NetworkExtension · Android VpnService(前台服务)· macOS/Win TUN 设备 + 开机自启</span></div>
</div>
<div class="cap">UI 与内核之间仅通过状态层通信;内核崩溃不拖垮 UI,UI 重启不掉隧道</div>
</div>
<ul>
<li><strong>连接状态机</strong>是核心:UI 三态(off / connecting / on)严格对应内核事件,禁止 UI 侧"乐观显示已连接"。</li>
<li><strong>智能选择</strong>(默认选中):sing-box URLTest 组在本地探测延迟自动选优,节点列表置顶推荐卡与原型一致。</li>
<li><strong>免费版额度</strong>:本地倒计时仅作展示,权威额度以 API 为准(见 02 章 <code>/v1/ads/unlock</code> 与 connect 校验)。</li>
<li><strong>默认安全</strong>Kill-switch 默认开启;DNS 走 DoH 防泄露;阻断 WebRTC 直连泄露(桌面端注入策略)。</li>
</ul>
</section>
<section id="fidelity">
<h2><span class="num">§3</span>100% 还原策略</h2>
<h3>3.1 令牌同源</h3>
<div class="tbl-wrap"><table>
<thead><tr><th></th><th>令牌载体</th><th>同步方式</th></tr></thead>
<tbody>
<tr><td>官网 / 用户中心</td><td><code>colors_and_type.css</code> 直接链入</td><td>原样引用,不复制不改写</td></tr>
<tr><td>移动 / 桌面</td><td><code>flutter/pangolin_theme.dart</code></td><td>与 CSS 一一对应的 Dart 镜像;改令牌必须两处同改(design/CLAUDE.md §6</td></tr>
</tbody>
</table></div>
<h3>3.2 像素基准与组件对照</h3>
<p>每个界面动手前先打开对应 React 原型比对;以下关键组件逐一对照验收:</p>
<ul>
<li><strong>核心连接键</strong>:off 暖灰底 + 虚线轨道环 / connecting 旋转弧 / on 绿底满环 + 圆内计时 + 光晕(过渡只用 <code>box-shadow</code>/<code>background-color</code>,禁 <code>transition: all</code></li>
<li><strong>智能选择推荐卡</strong>:常驻 accent-subtle 底 + clay 渐变 zap 图标 + 「推荐」胶囊,默认选中</li>
<li><strong>免费额度卡</strong>:剩余分钟 + 进度条(≤3 分钟变 warning 色)+「看广告开始使用」→ 解锁后变绿</li>
<li><strong>国家码块</strong>2 字母,无 emoji 国旗)、<strong>状态胶囊</strong>(色点 + 文字,无 emoji)、Lucide 细线图标</li>
<li><strong>Tab 滑动切换</strong>:>60px 且横向位移明显大于纵向,200ms 方向感知滑入;子页不响应</li>
<li><strong>iPad 分栏布局</strong>(对照 <code>ui_kits/tablet/</code>):左侧栏导航行高 ≥48px 触控尺寸;连接页双栏(左大连接键 / 右额度卡→当前节点→实时速率);节点页置顶智能推荐卡 + 双列网格(同桌面)</li>
</ul>
<h3>3.3 提交前验收清单(每个界面,引用 design/CLAUDE.md §9</h3>
<ul>
<li>颜色全部来自语义 token,无硬编码十六进制</li>
<li>明 / 暗两主题、中 / 英两语言(单显不并排)共四态验证</li>
<li>文案无铁律 13 红线词;套餐数字与 design/CLAUDE.md §7 一致</li>
<li>App / 官网内无任何支付表单,购买只引导到外部渠道</li>
</ul>
</section>
<section id="resilience">
<h2><span class="num">§4</span>客户端断网弹性</h2>
<p>设计目标:<strong>API 全灭、域名全被污染时,客户端仍能连上节点</strong>。详见 06 章断网应对矩阵,此处为客户端侧实现。</p>
<div class="tbl-wrap"><table>
<thead><tr><th>机制</th><th>实现</th><th>覆盖的故障</th></tr></thead>
<tbody>
<tr><td><strong>节点目录缓存</strong></td><td>每次成功拉取 <code>/v1/nodes</code> 后落盘(带 version 与时间戳);启动时先用缓存渲染并尝试连接,后台再刷新</td><td>API 暂时不可达</td></tr>
<tr><td><strong>API 端点池</strong></td><td>客户端内置:主域名 + 备用域名 N 个 + IP 直连兜底;按序故障转移,成功的端点置顶记忆</td><td>单个域名被墙 / 被污染</td></tr>
<tr><td><strong>DoH 解析</strong></td><td>域名解析优先走 DoH(多个提供方),绕开本地污染</td><td>DNS 污染</td></tr>
<tr><td><strong>签名端点更新</strong></td><td>定期从多镜像静态文件(Cloudflare Pages / GitHub 等)拉取 Ed25519 签名的端点列表,验签后合并进端点池</td><td>内置端点全部失效</td></tr>
<tr><td><strong>紧急逃生配置</strong></td><td>安装包内置 1–2 个应急节点参数(低速、仅够拉新目录),所有在线途径失效时启用</td><td>极端封锁</td></tr>
<tr><td><strong>应急公告</strong></td><td>客户端内公告位从签名静态 JSON 读取(多镜像),可引导用户更新或切换渠道</td><td>需要人工广播时</td></tr>
</tbody>
</table></div>
<div class="callout warn"><b>注意</b>所有兜底参数(IP、应急节点、签名公钥)属于敏感资产:随版本轮换、按渠道分包(不同分发渠道内置不同 IP 子集),泄露一个渠道不烧全部。</div>
</section>
</main>
<nav class="pager">
<a href="index.html"><span>上一章</span><b>← 总览</b></a>
<a class="next" href="02-backend.html"><span>下一章</span><b>02 后端设计 →</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>
+174
View File
@@ -0,0 +1,174 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>02 后端设计 · 穿山甲架构设计</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" class="active">02 后端</a>
<a href="03-database.html">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 02 · Backend</div>
<h1>后端设计</h1>
<p class="lede">Go 模块化单体,满足前端全部功能即可、不做过度设计。API 契约完整继承 <code>design/server/ARCHITECTURE.md</code> §3 蓝本,唯一语义修订:数据面从 WireGuard 换为 sing-boxREALITY / Hysteria2),<code>connect</code> 改为下发用户凭证 + 节点连接参数。</p>
</div>
<section id="modules">
<h2><span class="num">§1</span>服务形态与模块划分</h2>
<p>单体起步、按模块分包(实现顺序即蓝本 §7):</p>
<div class="diagram">
<div class="flow">
<div class="fbox accent"><b>HTTP API</b><span>chi · /v1</span></div>
<div class="farr"></div>
<div class="fbox"><b>auth</b><span>验证码/注册/JWT</span></div>
<div class="fbox"><b>codes</b><span>激活码/兑换/webhook</span></div>
<div class="fbox"><b>devices</b><span>设备/订阅校验</span></div>
<div class="fbox"><b>nodes</b><span>目录/connect/调度</span></div>
<div class="fbox"><b>usage</b><span>用量/额度</span></div>
<div class="fbox"><b>admin</b><span>管理端(独立监听)</span></div>
</div>
<div class="flow">
<div class="fbox ghost"><b>gRPC servermTLS</b><span>节点 agent:注册 / 心跳 / 凭证下发与回收 / 用量上报</span></div>
<div class="fbox ghost"><b>scheduler</b><span>探针汇聚 · 判封 · 节点生命周期驱动(见 04 章)</span></div>
<div class="farr"></div>
<div class="fbox"><b>MySQL</b><span>主数据</span></div>
<div class="fbox"><b>Redis</b><span>验证码/限流/实时负载</span></div>
</div>
<div class="cap">一个二进制三个监听:公网 API(CDN 后)· agent gRPCmTLS 双向认证)· 管理端(内网/白名单)</div>
</div>
<ul>
<li>每模块完成定义(蓝本约定):单测 + OpenAPI 同步 + 错误文案双语(<code>{code, message_zh, message_en}</code>+ 符合脱敏口径。</li>
<li>管理端绝不与公网 API 共用监听端口;部署时仅绑内网地址,前置白名单 + 2FA(见 06 章)。</li>
</ul>
</section>
<section id="api">
<h2><span class="num">§2</span>API 契约 v1(继承蓝本 §3,正式版用 OpenAPI 定义)</h2>
<pre><code><span class="c"># 认证(无需 JWT</span>
POST /v1/auth/code {email} <span class="c"># 发 6 位验证码(Redis 10min,限 1/min</span>
POST /v1/auth/register {email, code, password} <span class="c"># 建号 + 7 天试用 + JWT</span>
POST /v1/auth/login {email, password} <span class="c"># JWTaccess 15min + refresh 30d</span>
POST /v1/auth/refresh {refresh_token}
<span class="c"># 账户</span>
GET /v1/me <span class="c"># 账户 + 订阅 + 用量摘要</span>
GET /v1/me/devices
DELETE /v1/me/devices/:id <span class="c"># 移除设备(同步回收节点侧凭证)</span>
<span class="c"># 商业闭环(App 内无支付,只有兑换)</span>
POST /v1/redeem {code} <span class="c"># 兑换激活码(幂等 + 审计)</span>
POST /v1/ads/unlock {device_id, ad_token} <span class="c"># 免费版激励视频解锁当日时长(验 SDK 回执)</span>
GET /v1/plans <span class="c"># 套餐目录(数字与 design/CLAUDE.md §7 一致)</span>
<span class="c"># 节点(数据面入口)</span>
GET /v1/nodes ?if_version=N <span class="c"># 节点目录(按套餐过滤,304 支持)</span>
POST /v1/nodes/:id/connect {device_id} <span class="c"># 下发连接凭证(见 §3,语义已适配 sing-box)</span>
POST /v1/nodes/:id/disconnect {device_id}
GET /v1/usage ?days=7 <span class="c"># 用量曲线(统计页)</span>
GET /v1/notices <span class="c"># 公告(亦发布为多镜像签名静态 JSON)</span></code></pre>
<ul>
<li>除 auth 外全部接口要求 JWT;限流用 Redis 滑动窗口(按 IP + 用户双维度)。</li>
<li>错误体统一 <code>{code, message_zh, message_en}</code>;文案遵守铁律 13 脱敏口径。</li>
<li>节点目录响应带 <code>version</code>;客户端 <code>if_version</code> 命中返回 304——这是节点秒级灰度的基础(见 04 章)。</li>
</ul>
</section>
<section id="connect">
<h2><span class="num">§3</span>connect 语义适配(相对蓝本 v0.1 的唯一修订)</h2>
<div class="callout"><b>修订理由</b>蓝本 v0.1 的数据面是 WireGuardconnect = 下发 WG peer)。WG 协议特征明显,在目标网络环境会被快速识别阻断;plan/ phase-0~3 已选定 REALITY 主线 + Hysteria2 备线。API 形状不变,仅返回体语义调整。</div>
<h3>3.1 连接流程</h3>
<div class="diagram">
<div class="flow">
<div class="fbox accent"><b>客户端</b><span>POST /nodes/:id/connect</span></div>
<div class="farr"></div>
<div class="fbox"><b>API 校验</b><span>订阅有效 · 设备数 ≤ 上限 · 免费版校验 ad_unlocked_at + 剩余分钟</span></div>
<div class="farr"></div>
<div class="fbox"><b>gRPC → agent</b><span>确保该用户 UUID 已在节点 sing-box 用户表</span></div>
<div class="farr"></div>
<div class="fbox ok"><b>返回连接参数</b><span>server, port, uuid, flow, reality pbk/sid/sni · hy2 备用</span></div>
</div>
<div class="cap">免费版凭证带 TTL = 当日剩余分钟,到时 agent 自动从用户表移除;付费版 TTL 24h 在线自动续期</div>
</div>
<h3>3.2 凭证模型</h3>
<ul>
<li><strong>每用户一个数据面 UUID</strong>(与账号 ID 解耦,可轮换):节点侧只见 UUID,零账号信息——单节点被抄不泄露用户库。</li>
<li>凭证轮换:用户改密 / 管理端封禁 / 定期轮换时生成新 UUID,gRPC 广播到所有在册节点替换;旧 UUID 给 5 分钟宽限期平滑重连。</li>
<li>订阅过期、设备被移除、套餐降级 → scheduler 驱动 agent 即时回收对应 UUID。</li>
<li>Hysteria2 凭证同源派生(同一 UUID 作 auth password),客户端按网络状况自动选协议。</li>
</ul>
</section>
<section id="flows">
<h2><span class="num">§4</span>关键业务流程</h2>
<h3>4.1 注册与试用(对齐客户端 UI:邮箱 → 验证码 → 设密码)</h3>
<ul>
<li><code>POST /auth/code</code>:风控(IP + 邮箱限频、一次性邮箱域黑名单)→ 发码(Redis 10min)。</li>
<li><code>POST /auth/register</code>:验码 → 建号 → <strong>自动写入 7 天 PRO 试用</strong><code>subscriptions(plan=pro, expires_at=now()+7d, source='trial')</code>,一邮箱仅一次)→ 返回 JWT。</li>
<li>试用到期回落 free:仅 tier=free 节点、每日 10 分钟、每日首连前需激励视频解锁。</li>
</ul>
<h3>4.2 激活码生命周期</h3>
<div class="diagram">
<div class="flow">
<div class="fbox"><b>发卡店售出</b><span>webhookHMAC 签名)</span></div>
<div class="fbox"><b>人工渠道</b><span>TG/LINE/邮箱 · 管理端批量生成 batch</span></div>
<div class="farr"></div>
<div class="fbox"><b>codes 入库</b><span>status=unused · 库存 hash</span></div>
<div class="farr"></div>
<div class="fbox accent"><b>POST /redeem</b><span>事务:redeemed + 订阅顺延(叠加非覆盖)</span></div>
<div class="farr"></div>
<div class="fbox"><b>audit_log</b><span>必记</span></div>
</div>
</div>
<ul>
<li>码格式:Crockford Base32,16 位含校验位;明文只出现一次(生成响应 / 发卡店),库内存 hash。</li>
<li>兑换幂等:同一用户重复提交同一码返回首次结果;风控:单用户失败 5 次/小时锁 1 小时。</li>
</ul>
<h3>4.3 免费版广告解锁</h3>
<ul>
<li>客户端播完激励视频 → <code>POST /v1/ads/unlock</code> 携带广告 SDK 回执 → 服务端向 AdMob/Unity 校验回执真伪 → 记 <code>usage_daily.ad_unlocked_at</code></li>
<li><code>connect</code> 对 free 用户强制校验当日 <code>ad_unlocked_at</code> 与剩余分钟;分钟数由 agent 上报的会话时长累计,服务端为权威。</li>
</ul>
</section>
<section id="hardening">
<h2><span class="num">§5</span>后端安全要点(详见 06 章总纲)</h2>
<ul>
<li>密码 argon2id;JWT RS256,签名密钥定期轮换;全站 TLS 1.3。</li>
<li>验证码 / 兑换 / 登录接口限流 + 防一次性邮箱 + 失败锁定;所有敏感操作进 <code>audit_log</code></li>
<li><strong>无日志口径</strong>(写进隐私政策并据实执行):不记录目的地址 / DNS 查询 / 流量内容;仅 <code>usage_daily</code> 字节数与分钟数。</li>
<li>agent gRPC 双向 mTLS:节点证书由内部 CA 签发、与节点 ID 绑定,节点被回收即吊销。</li>
<li>CIlint + 单测 + OpenAPI 校验 + 镜像构建;OpenAPI 即 API 文档与客户端 SDK 生成源。</li>
</ul>
</section>
</main>
<nav class="pager">
<a href="01-frontend.html"><span>上一章</span><b>← 01 前端架构</b></a>
<a class="next" href="03-database.html"><span>下一章</span><b>03 数据库 →</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>
+239
View File
@@ -0,0 +1,239 @@
<!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>
+187
View File
@@ -0,0 +1,187 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>04 机器与网络拓扑 · 穿山甲架构设计</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">03 数据库</a>
<a href="04-infra.html" class="active">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 04 · Infrastructure & Topology</div>
<h1>机器与网络拓扑</h1>
<p class="lede">目标:<strong>弹性可变的网络拓扑</strong>——所有节点可一键更换或自动更换,IP 可随时轮换;任何单点被识别、被阻断都不影响整体服务。设计核心是「节点是牲口不是宠物」:节点无状态、可丢弃、分钟级重建。</p>
</div>
<section id="layers">
<h2><span class="num">§1</span>三层弹性拓扑</h2>
<div class="diagram">
<div class="flow">
<div class="fbox accent"><b>用户</b><span>REALITY / Hysteria2</span></div>
<div class="farr"></div>
<div class="fbox"><b>入口层 Entry</b><span>消耗品 IP 池 · 多厂商 · 高频轮换</span></div>
<div class="farr"></div>
<div class="fbox ghost"><b>中转层 Relay(可选)</b><span>内部隧道转发</span></div>
<div class="farr"></div>
<div class="fbox ok"><b>落地层 Exit</b><span>精品 IP · 稳定出口 · 用户不可见</span></div>
</div>
<div class="flow">
<div class="fnote">免费用户:直连消耗品池入口(单跳直出)。付费用户:入口 → 落地两跳为主,中转层在入口与落地间链路质量差时按需启用。</div>
</div>
<div class="cap">被识别阻断的永远是入口 IP(暴露面);落地 IP 只对内部隧道可见,几乎不会被烧</div>
</div>
<div class="tbl-wrap"><table>
<thead><tr><th>角色</th><th>暴露面</th><th>IP 等级</th><th>更换频率</th><th>跑什么</th></tr></thead>
<tbody>
<tr><td><strong>entry 入口</strong></td><td>直面用户与审查(唯一公网暴露)</td><td>消耗品池:冷门小厂、便宜、可隐私购买</td><td>被封即换;常态每 24 周主动轮换</td><td>sing-boxREALITY + Hy2 端口跳跃)+ agent</td></tr>
<tr><td><strong>relay 中转</strong></td><td>仅入口可见</td><td>低价大带宽</td><td>低频</td><td>内部隧道转发 + agent</td></tr>
<tr><td><strong>exit 落地</strong></td><td>仅中转/入口可见</td><td>精品池:稳定厂商、原生 IP、解锁好</td><td>极低频</td><td>出口 + agent</td></tr>
<tr><td><strong>probe 探针</strong></td><td></td><td>境内多 ISP 拨测点 + 境外对照点</td><td></td><td>探测脚本,结果回报控制面</td></tr>
</tbody>
</table></div>
<ul>
<li><strong>分级对应套餐</strong>(plan/ 既定策略):免费用户消耗免费池入口(tier=free),被封损失最小;付费用户走 tier=pro 入口 + 落地分层,体验稳定。</li>
<li>起步规模可以「入口=落地」单跳跑通,拓扑字段(role)从第一天就建模,后续平滑演进为分层,客户端无感。</li>
</ul>
</section>
<section id="cattle">
<h2><span class="num">§2</span>节点无状态化(可丢弃的前提)</h2>
<ul>
<li>节点上只有:sing-box + agent 二进制 + 由控制面下发的运行时配置(用户 UUID 表、伪装 SNI、密钥)。<strong>零用户库、零日志、零持久数据。</strong></li>
<li>全部由 <code>cloud-init</code> 一段脚本拉起:装 agent → agent 持引导 token 向控制面 mTLS 注册 → 控制面下发完整配置。重建一台 = 开机 + 等注册,约 3–5 分钟。</li>
<li>节点间不互信:中转/落地链路用独立隧道密钥,按节点对发放,回收节点即吊销。</li>
</ul>
</section>
<section id="lifecycle">
<h2><span class="num">§3</span>节点生命周期状态机</h2>
<div class="diagram">
<div class="flow">
<div class="fbox"><b>provisioning</b><span>厂商 API 开机中</span></div>
<div class="farr"></div>
<div class="fbox warn"><b>probing</b><span>探活:境内外可达性验证</span></div>
<div class="farr"></div>
<div class="fbox ok"><b>up</b><span>在目录 · 接受连接</span></div>
<div class="farr"></div>
<div class="fbox warn"><b>draining</b><span>不接新连接 · 排空存量</span></div>
<div class="farr"></div>
<div class="fbox bad"><b>down</b><span>已出目录</span></div>
<div class="farr"></div>
<div class="fbox ghost"><b>destroyed</b><span>销毁 · IP 释放</span></div>
</div>
<div class="flow"><div class="fnote">up → down(跳过 draining):判封确认时立即下架。probing 失败 → 直接 destroyed(坏 IP 不入池)。每次迁移写 node_events 并 bump 目录 version。</div></div>
</div>
<ul>
<li><strong>目录灰度</strong><code>nodes</code> 任何变更 bump 全局 version;客户端带 <code>if_version</code> 轮询(在线连接中收到目录推送可平滑迁移);客户端缓存最后一份可用目录兜底。</li>
<li><strong>draining 排空</strong>:默认 30 分钟或存量连接归零,先到为准;免费节点可直接硬切(用户重连成本低)。</li>
</ul>
</section>
<section id="replace">
<h2><span class="num">§4</span>一键更换与自动更换</h2>
<h3>4.1 一键更换(管理端按钮 / CLI 一条命令)</h3>
<div class="diagram">
<div class="flow">
<div class="fbox accent"><b>replace(node)</b><span>管理端触发</span></div>
<div class="farr"></div>
<div class="fbox"><b>开新机</b><span>厂商 API(同区域 · 厂商池内可换家)</span></div>
<div class="farr"></div>
<div class="fbox"><b>cloud-init</b><span>装 agent · 自注册</span></div>
<div class="farr"></div>
<div class="fbox warn"><b>probing</b><span>境内外探活通过</span></div>
<div class="farr"></div>
<div class="fbox ok"><b>新机入池</b><span>version bump</span></div>
<div class="farr"></div>
<div class="fbox"><b>旧机 draining</b><span>→ destroy · IP 释放</span></div>
</div>
<div class="cap">先建后拆(make-before-break):任何更换动作期间容量不下降</div>
</div>
<ul>
<li><strong>换 IP 不换机</strong>(更轻量):厂商支持弹性 IP 时直接 API 换绑 → 探活 → 更新 endpoint → version bump,全程分钟级。</li>
<li><strong>全量轮换</strong>:同一命令对整个池滚动执行(并发度 1–2),用于例行轮换或大面积事件后重建。</li>
<li>所有动作幂等、写 <code>audit_log</code><code>node_events</code>;基础设施定义用 Terraform + 厂商 API 适配层(providers 表登记,凭证独立 secrets 管理)。</li>
</ul>
<h3>4.2 自动更换:判封 → 下架 → 补新</h3>
<h4>判封信号(三路互证,避免误判)</h4>
<div class="tbl-wrap"><table>
<thead><tr><th>信号源</th><th>采集</th><th>说明</th></tr></thead>
<tbody>
<tr><td><strong>境内探针</strong></td><td>电信 / 联通 / 移动 ≥3 个拨测点,每 5 分钟:TCP 连通 + TLS 握手 + 真实协议握手</td><td>核心信号;探针点用境内云函数/拨测服务,与运营身份隔离</td></tr>
<tr><td><strong>境外对照</strong></td><td>境外探针同款探测</td><td>区分「被墙」与「机器挂了」:境内挂 + 境外通 = 疑似被封;两边都挂 = 故障</td></tr>
<tr><td><strong>流量侧</strong></td><td>agent 上报在线数/带宽,环比骤降(如 15 分钟内 −80%)</td><td>辅助信号,提早怀疑</td></tr>
</tbody>
</table></div>
<h4>判定与处置规则(scheduler 驱动,阈值可配)</h4>
<ul>
<li><strong>疑似</strong>:境内 ≥2/3 拨测点连续 2 个周期失败,且境外正常 → <code>blocked_suspect</code>,节点降权(weight→10),加密探测频率。</li>
<li><strong>确认</strong>:连续 6 个周期(30 分钟)维持上述状态 → <code>blocked_confirmed</code> → 立即 <code>down</code> 出目录 + 自动触发 §4.1 补新流程。</li>
<li><strong>故障</strong>(境内外同挂):走告警人工介入,不消耗 IP 配额重建。</li>
<li><strong>容量保护</strong>:自动补新失败(厂商 API 错误、新机探活不过)连续 3 次 → 升级为告警;池内 up 节点低于水位线(如 tier 内 <70% 无条件告警</li>
<li><strong>熔断</strong>:1 小时内同池自动更换 >N 台(疑似大面积事件)→ 暂停自动重建、保留现场、人工决策(可能需要换厂商/换区域/换伪装 SNI 而非头铁补机)。</li>
</ul>
<div class="callout warn"><b>主动降低被封概率(比换得快更重要)</b>
<ul>
<li>REALITY 伪装 SNI 定期评估更换(目标站不可被墙、延迟特征合理);密钥/short_id 随节点轮换。</li>
<li>Hysteria2 端口跳跃(区间随机),抗 UDP QoS 与端口特征。</li>
<li>新 IP 上线前先「养」:探活通过后小流量灰度 24h 再放满 weight,避免新 IP 行为突变被标记。</li>
<li>免费池与付费池物理隔离(不同厂商账号),免费池被批量烧不殃及付费池。</li>
</ul></div>
</section>
<section id="scheduling">
<h2><span class="num">§5</span>调度、容量与厂商池</h2>
<h3>5.1 节点目录排序(服务端)与智能选择(客户端)</h3>
<ul>
<li>服务端目录按 <code>score = f(实时负载, 探针延迟, weight, tier)</code> 排序下发,给客户端一个好的默认顺序与「信号条」数据。</li>
<li>客户端「智能选择」(UI 默认选中):sing-box URLTest 在候选前 N 个节点本地实测自动择优——最终路径决策在端上,服务端只缩小候选集。</li>
</ul>
<h3>5.2 厂商池策略</h3>
<div class="tbl-wrap"><table>
<thead><tr><th></th><th>厂商画像</th><th>要求</th></tr></thead>
<tbody>
<tr><td><strong>消耗品池</strong>entry/free</td><td>冷门小厂 ≥3 家,按量/小时计费,IP 便宜量大</td><td>有 API 可自动开退机;支持加密货币付款、隐私注册;避开被重点封锁的大厂 IP 段</td></tr>
<tr><td><strong>精品池</strong>exit/pro entry</td><td>稳定厂商 1–2 家,原生 IP、带宽质量好</td><td>同样隐私注册;区域:HK / JP / SG / US 起步</td></tr>
</tbody>
</table></div>
<ul>
<li>每家厂商独立账号、独立邮箱、加密货币付款——厂商之间、厂商与域名/收款之间不产生身份关联(红线,见 06 章)。</li>
<li>控制面(API + MySQL + Redis)部署在与节点完全无关的稳定云上,不放任何代理流量;节点全灭不影响控制面,反之亦然。</li>
</ul>
<h3>5.3 监控告警</h3>
<ul>
<li>节点心跳缺失(>90s)、池水位、自动更换熔断、探针自身失联、兑换失败率、注册转化漏斗。</li>
<li>告警出口:TG bot(运营专用匿名号)。所有告警附 runbook 链接(05 章)。</li>
</ul>
</section>
</main>
<nav class="pager">
<a href="03-database.html"><span>上一章</span><b>← 03 数据库</b></a>
<a class="next" href="05-web-security.html"><span>下一章</span><b>05 Web 安全与灾备 →</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>
+160
View File
@@ -0,0 +1,160 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>05 Web 安全与灾备 · 穿山甲架构设计</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">03 数据库</a>
<a href="04-infra.html">04 网络拓扑</a>
<a href="05-web-security.html" class="active">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 05 · Web Security & DR</div>
<h1>Web 安全与灾备</h1>
<p class="lede">Web 面资产:官网(获客入口)、用户中心、API、公告/端点分发。威胁两类:常规 Web 攻击,以及本项目特有的<strong>域名被墙、源站被定位、服务节点被封</strong>。原则:一切可被秒级重建,一切入口皆有备份。</p>
</div>
<section id="assets">
<h2><span class="num">§1</span>资产与暴露面</h2>
<div class="tbl-wrap"><table>
<thead><tr><th>资产</th><th>形态</th><th>暴露面</th><th>被打击后果</th></tr></thead>
<tbody>
<tr><td>官网</td><td>Astro 纯静态</td><td>主域名 + 镜像域名</td><td>获客入口断(存量用户无感)</td></tr>
<tr><td>用户中心</td><td>Next.js 静态导出</td><td>同上</td><td>网页端兑换/订阅导入断(客户端内同功能仍可用)</td></tr>
<tr><td>API</td><td>Go 服务(CDN 后)</td><td>API 域名池</td><td>登录/兑换/目录刷新断(客户端缓存兜底)</td></tr>
<tr><td>公告与端点分发</td><td>签名静态 JSON</td><td>多镜像(CF Pages / GitHub / 对象存储)</td><td>失去应急广播能力——所以镜像最多、优先级最高</td></tr>
</tbody>
</table></div>
</section>
<section id="hardening">
<h2><span class="num">§2</span>常规 Web 安全基线</h2>
<ul>
<li><strong>CDN 前置(Cloudflare</strong>WAF + bot 管理 + 速率限制全开;源站只允许 CDN 回源网段 + 回源鉴权 header,直接命中源站 IP 一律拒绝。</li>
<li><strong>源站 IP 隐藏</strong>:源站从未以裸 IP 提供过服务(历史 DNS 记录是最常见泄露途径);出站请求(webhook 回调、广告回执校验)走独立出口,不暴露源站地址。</li>
<li><strong>TLS 1.3 全站</strong>、HSTS;静态站点 CSP 严格白名单;用户中心开启 SRI。</li>
<li><strong>账户安全</strong>:登录限流 + 失败锁定;用户中心支持 TOTP 2FA(设计稿既有功能);会话仅 HttpOnly + Secure cookie 或 header token。</li>
<li><strong>注入面</strong>:API 全参数化查询(03 章 schema 无拼接场景);上传/富文本一概没有,攻击面天然小。</li>
<li><strong>依赖与供应链</strong>:CI 里跑依赖审计;前端构建锁定版本;不引入第三方统计脚本(也符合隐私承诺)。</li>
</ul>
</section>
<section id="domains">
<h2><span class="num">§3</span>域名体系与解析兜底</h2>
<div class="tbl-wrap"><table>
<thead><tr><th>域名组</th><th>用途</th><th>策略</th></tr></thead>
<tbody>
<tr><td>主域名</td><td>官网 + 用户中心</td><td>品牌入口;被墙概率最高,做好牺牲准备</td></tr>
<tr><td>API 域名池(N≥3</td><td>客户端 API</td><td>与主域不同注册商、不同 WHOIS(隐私保护)、互不关联;客户端内置全部</td></tr>
<tr><td>订阅/分发域名</td><td>端点更新、公告 JSON</td><td>独立于以上两组——「取订阅」行为最容易被关联封锁,单独隔离</td></tr>
<tr><td>冷备域名池</td><td>未启用</td><td>常备 ≥5 个已注册未使用的域名,启用即配 CDN,随签名端点更新推给客户端</td></tr>
</tbody>
</table></div>
<ul>
<li>全部域名:隐私注册、加密货币付款、注册商分散;域名之间不共享 NS 特征化配置。</li>
<li>客户端解析优先 DoH(多提供方轮询),其次系统 DNS,最后 IP 直连兜底(01 章端点池)。</li>
<li><strong>被墙检测</strong>:域名级探针与节点探针同体系(04 章)——境内拨测 DNS 污染 + TCP 阻断,境外对照。确认被墙 → 从端点池降级 → 启用冷备域名 → 通过签名端点更新下发。</li>
</ul>
</section>
<section id="backup">
<h2><span class="num">§4</span>备份与可重建性(一切皆可秒建)</h2>
<div class="tbl-wrap"><table>
<thead><tr><th>资产</th><th>备份形态</th><th>重建路径</th><th>RTO</th></tr></thead>
<tbody>
<tr><td>官网 / 用户中心</td><td>git 仓库即备份(纯静态)</td><td>CI 一键部署到任意新域名/新平台(CF Pages、对象存储+CDN</td><td>< 30min</td></tr>
<tr><td>API 服务</td><td>容器镜像 + IaCTerraform/Ansible 全量定义)</td><td>新机拉镜像 + 配置注入,接回 DB</td><td>< 2h</td></tr>
<tr><td>MySQL</td><td>每日全量 + binlog 增量,age 加密异地(03 章 §5)</td><td>新实例还原 + 回放</td><td>≤ 4hRPO ≤ 15min</td></tr>
<tr><td>节点</td><td>无需备份(无状态,04 章)</td><td>cloud-init 自动重建</td><td>35min/台</td></tr>
<tr><td>密钥类</td><td>JWT 私钥 / 内部 CA / 签名私钥 / 备份解密钥:离线密管,两地保存</td><td></td><td></td></tr>
</tbody>
</table></div>
<div class="callout"><b>演练制度</b>每月一次「假装全没了」演练:从干净环境凭 git + IaC + 备份把官网、API、DB 全链路拉起来并跑通注册→兑换→连接,记录实际 RTO。没演练过的备份等于没有备份。</div>
</section>
<section id="runbook">
<h2><span class="num">§5</span>被封 / 被打击应对 Runbook</h2>
<div class="tbl-wrap"><table>
<thead><tr><th>场景</th><th>检测</th><th>应对动作</th><th>目标恢复</th></tr></thead>
<tbody>
<tr>
<td><strong>API 域名被墙</strong></td>
<td>域名探针:境内 DNS 污染/TCP 阻断,境外正常</td>
<td>① 客户端自动故障转移到池内下一域名(无人工动作)② 启用一个冷备域名补池 ③ 签名端点更新下发新池</td>
<td>用户无感;池恢复 < 1d</td>
</tr>
<tr>
<td><strong>主域名(官网)被墙</strong></td>
<td>同上</td>
<td>① 镜像域名顶上(官网常备 ≥2 个活跃镜像)② TG 频道 + 客户端公告位广播新地址 ③ 评估是否换主品牌域名</td>
<td>获客入口 < 1d</td>
</tr>
<tr>
<td><strong>入口节点大面积被封</strong></td>
<td>04 章判封体系 + 熔断触发</td>
<td>① 自动:存量好节点承载 + 客户端目录刷新 ② 人工:判断封锁模式(IP 段?协议特征?SNI?)③ 对症换厂商/换区域/换伪装参数后批量重建</td>
<td>核心连接 < 4h</td>
</tr>
<tr>
<td><strong>源站 IP 暴露 / 被攻击</strong></td>
<td>源站直连流量告警、异常负载</td>
<td>① 立即换源站 IP(IaC 重建)② 复查泄露途径(DNS 历史、出站请求、证书透明度日志)③ 轮换回源鉴权</td>
<td>< 2h</td>
</tr>
<tr>
<td><strong>CDN 账号风险</strong>(封号/要求实名)</td>
<td>账号通知 / 服务异常</td>
<td>① 备好第二 CDN 账号与配置导出,DNS 切换 ② 极端情况:API 走多域名直连源站集群(牺牲部分防护换可用性)</td>
<td>< 1d</td>
</tr>
<tr>
<td><strong>发卡店/收款通道失效</strong></td>
<td>webhook 静默 / 渠道通知</td>
<td>① 切换备用发卡店(激活码体系与渠道解耦,codes.channel 隔离)② 客户端「兑换/购买」页指向新渠道(远程配置,无需发版)</td>
<td>< 1d</td>
</tr>
<tr>
<td><strong>DB 损坏 / 控制面全灭</strong></td>
<td>监控全红</td>
<td>走 §4 重建路径;期间客户端靠缓存目录 + 已下发凭证维持已有用户连接(数据面独立存活)</td>
<td>≤ 4h,存量连接不断</td>
</tr>
</tbody>
</table></div>
<h3>应急广播体系(所有场景的公共依赖)</h3>
<ul>
<li><strong>TG 频道</strong>:运营匿名号维护,安装包/官网/客户端「联系我们」均长期展示频道地址。</li>
<li><strong>客户端公告位</strong>:读多镜像签名 JSON(Ed25519 验签防伪造),可推送新域名、新下载地址、事件说明。</li>
<li><strong>下载分发冗余</strong>:安装包同时发布于官网、GitHub Releases、网盘镜像、TG 频道——官网被墙不影响获取更新。</li>
</ul>
</section>
</main>
<nav class="pager">
<a href="04-infra.html"><span>上一章</span><b>← 04 机器与网络拓扑</b></a>
<a class="next" href="06-security.html"><span>下一章</span><b>06 安全总纲 →</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>
+187
View File
@@ -0,0 +1,187 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>06 安全总纲 · 穿山甲架构设计</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">03 数据库</a>
<a href="04-infra.html">04 网络拓扑</a>
<a href="05-web-security.html">05 Web 安全</a>
<a href="06-security.html" class="active">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 06 · Security</div>
<h1>安全总纲</h1>
<p class="lede">安全是本项目的第一优先级,且分三个互不混淆的层面:<strong>运营者安全</strong>(身份隔离)、<strong>系统安全</strong>(攻击面与数据保护)、<strong>可用性安全</strong>(被封/断网后的存活)。任何功能与安全冲突时,安全赢。</p>
</div>
<section id="threat-model">
<h2><span class="num">§1</span>威胁模型总表</h2>
<div class="tbl-wrap"><table>
<thead><tr><th>资产</th><th>威胁</th><th>对策</th><th>详见</th></tr></thead>
<tbody>
<tr><td><strong>运营主体身份</strong></td><td>服务器/域名/收款被关联追踪</td><td>全链路境外身份、加密货币付款、WHOIS 隐私、专用管理环境;各资产间零身份交叉</td><td>本章 §2</td></tr>
<tr><td><strong>用户库</strong>(邮箱)</td><td>控制面被入侵、备份泄露</td><td>管理端内网+2FA、最小攻击面、备份 age 加密 + 私钥离线、audit_log</td><td>02/03/05 章</td></tr>
<tr><td><strong>用户行为隐私</strong></td><td>连接行为被记录/关联</td><td>无日志口径(仅字节/分钟)、节点零用户库(只见 dp_uuid)、DoH、Kill-switch</td><td>本章 §4</td></tr>
<tr><td><strong>数据面节点</strong></td><td>IP 被识别封锁、机器被抄</td><td>REALITY 无指纹 + Hy2 端口跳跃、三层拓扑、判封自动换、节点无状态零数据</td><td>04 章</td></tr>
<tr><td><strong>Web 入口</strong></td><td>域名被墙、源站被定位、DDoS</td><td>CDN 前置、域名池 + 冷备、源站隐藏、全资产可秒建</td><td>05 章</td></tr>
<tr><td><strong>资金流</strong></td><td>收款账户冻结、被关联</td><td>App 内零支付;USDT(TRC20) + 发卡店,通道可热切换,绝不绑实名账户</td><td>02 章 §4</td></tr>
<tr><td><strong>客户端</strong></td><td>安装包被篡改、端点被伪造</td><td>安装包签名 + 多渠道校验值;端点/公告更新 Ed25519 验签;敏感参数按渠道分包</td><td>01 章 §4</td></tr>
</tbody>
</table></div>
</section>
<section id="redlines">
<h2><span class="num">§2</span>身份隔离红线(继承 plan/security,任何阶段不得触碰)</h2>
<div class="callout danger"><b>红线清单</b>
<ul>
<li>不得用国内真实身份注册任何环节(服务器、域名、CDN、对象存储、收款、应用商店)。</li>
<li>管理后台不得暴露公网(仅内网/白名单 + 强口令 + 2FA)。</li>
<li>节点不得保留连接日志。</li>
<li>收款不得绑定个人实名账户。</li>
<li>服务器、域名、支付、邮箱、手机号之间不得产生可被关联的身份线索;管理操作只在专用环境进行。</li>
</ul></div>
<p class="muted">架构上的落实:providers 凭证、域名注册商、CDN 账号、备份存储账号全部独立身份并在文档中登记隔离关系;CI/CD 与管理操作走专用通道。</p>
</section>
<section id="system">
<h2><span class="num">§3</span>系统安全基线(汇总各章)</h2>
<div class="grid2">
<div class="card">
<h4 style="margin-top:0">控制面</h4>
<ul style="margin-bottom:0">
<li>管理端独立监听:内网 + IP 白名单 + 2FA</li>
<li>SSH 仅密钥、禁 root 口令、最小开放端口</li>
<li>密码 argon2idJWT RS256 密钥轮换;TLS 1.3</li>
<li>验证码/兑换/登录全限流 + 失败锁定 + 防一次性邮箱</li>
<li>敏感操作全量 <code>audit_log</code></li>
</ul>
</div>
<div class="card">
<h4 style="margin-top:0">数据面</h4>
<ul style="margin-bottom:0">
<li>节点零用户库:只持有 dp_uuid 表,被抄一台不泄露任何账号</li>
<li>agent ↔ 控制面双向 mTLS,证书绑节点、回收即吊销</li>
<li>节点间隧道密钥按节点对发放</li>
<li>节点无状态、无日志、无持久数据(04 章 §2)</li>
</ul>
</div>
<div class="card">
<h4 style="margin-top:0">密码学口径</h4>
<ul style="margin-bottom:0">
<li>口令哈希 argon2id;激活码库内只存 SHA-256</li>
<li>端点/公告分发 Ed25519 签名,公钥随安装包内置</li>
<li>备份 age 公钥加密,私钥离线两地保存</li>
<li>内部 CA 签发 agent 证书,根私钥离线</li>
</ul>
</div>
<div class="card">
<h4 style="margin-top:0">客户端</h4>
<ul style="margin-bottom:0">
<li>凭证存 Keychain / Keystore,不落明文</li>
<li>Kill-switch 默认开;DoH 防 DNS 泄露;阻 WebRTC 泄露</li>
<li>安装包多渠道发布 + 校验值公示</li>
<li>敏感内置参数按分发渠道分包轮换</li>
</ul>
</div>
</div>
</section>
<section id="privacy">
<h2><span class="num">§4</span>无日志口径(写进隐私政策并据实执行)</h2>
<div class="tbl-wrap"><table>
<thead><tr><th>数据</th><th>是否保留</th><th>说明</th></tr></thead>
<tbody>
<tr><td>浏览内容 / 目的地址 / DNS 查询</td><td><span class="pill bad"><span class="dot"></span>不记录</span></td><td>节点与控制面均无此数据,技术上不可补记</td></tr>
<tr><td>连接日志(谁何时连了哪个节点)</td><td><span class="pill bad"><span class="dot"></span>不记录</span></td><td>节点 no-log;控制面仅 connect 校验通过与否的限流计数(短 TTL)</td></tr>
<tr><td>用量字节数 / 分钟数(按日聚合)</td><td><span class="pill warn"><span class="dot"></span>保留</span></td><td>计费与免费额度所需的最小元数据(usage_daily</td></tr>
<tr><td>账号(邮箱)、设备名、最后活跃</td><td><span class="pill warn"><span class="dot"></span>保留</span></td><td>登录与设备数限制所需;不收集真实身份</td></tr>
</tbody>
</table></div>
<p class="muted">对外文案遵守设计铁律 13 的脱敏口径:「我们不记录你的浏览数据 · 端到端加密」。</p>
</section>
<section id="offline-matrix">
<h2><span class="num">§5</span>断网应对策略矩阵(端 × 故障)</h2>
<p>「断网」指各类不可达事件。目标排序:① 已连接用户不掉线 → ② 能重连 → ③ 能获取新入口 → ④ 能联系到我们。</p>
<div class="tbl-wrap"><table>
<thead><tr><th>故障 \ 端</th><th>移动 / 桌面客户端</th><th>Web 用户中心</th><th>官网</th></tr></thead>
<tbody>
<tr>
<td><strong>API 不可达</strong>(单域名)</td>
<td>端点池自动故障转移(域名池→IP 直连→DoH 重解析),用户无感</td>
<td>同源 API 池故障转移;只读功能照常</td>
<td>纯静态,不依赖 API</td>
</tr>
<tr>
<td><strong>API 全灭</strong>(控制面宕机)</td>
<td>缓存目录 + 已下发凭证继续连接(数据面独立);登录/兑换暂停,连接不受影响</td>
<td>暂不可用,公告位提示</td>
<td>照常;展示状态公告</td>
</tr>
<tr>
<td><strong>节点大面积被封</strong></td>
<td>URLTest 自动切存量好节点;目录刷新拿补充节点;极端时启用内置应急节点</td>
<td>订阅导入页提示刷新订阅</td>
<td>不受影响</td>
</tr>
<tr>
<td><strong>域名被墙 / DNS 污染</strong></td>
<td>DoH 绕污染 → 失败走 IP 直连 → 签名端点更新拿新域名</td>
<td>切镜像域名(公告/TG 广播)</td>
<td>切镜像域名(≥2 活跃镜像常备)</td>
</tr>
<tr>
<td><strong>用户本地断网</strong>(非封锁)</td>
<td>连接键回 off 态 + 明确文案;Kill-switch 防泄露;网络恢复自动重连</td>
<td>浏览器原生提示</td>
<td></td>
</tr>
<tr>
<td><strong>一切在线途径失效</strong></td>
<td>内置应急节点拉新目录;客户端内「联系我们」展示 TG/LINE/邮箱(离线可见)</td>
<td colspan="2">TG 频道为最终广播渠道;安装包在 GitHub/网盘多镜像可获取</td>
</tr>
</tbody>
</table></div>
<div class="callout ok"><b>设计自检</b>上表每个格子都不依赖「正在故障的那个组件」自身恢复——客户端兜底链(缓存 → 端点池 → DoH → 签名更新 → 应急节点 → TG)层层递降,每层的信任锚(内置公钥、应急参数)都在安装包内离线可用。</div>
</section>
<section id="review">
<h2><span class="num">§6</span>安全例行制度</h2>
<ul>
<li><strong>每月</strong>:灾备演练(05 章 §4);备份恢复验证;依赖漏洞审计。</li>
<li><strong>每季</strong>:JWT 密钥与回源鉴权轮换;伪装 SNI 评估;红线清单自查(对照 §2 逐条核)。</li>
<li><strong>每次发版</strong>:设计验收清单(铁律 13 条)+ 脱敏文案扫描(CI 关键词检查 UI 文案资源)。</li>
<li><strong>事件后</strong>node_events / audit_log 复盘,更新判封阈值与 runbook。</li>
</ul>
</section>
</main>
<nav class="pager">
<a href="05-web-security.html"><span>上一章</span><b>← 05 Web 安全与灾备</b></a>
<a class="next" href="index.html"><span>返回</span><b>总览 →</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>
+17
View File
@@ -0,0 +1,17 @@
lucide.createIcons();
(function () {
var btn = document.getElementById('themeBtn');
if (localStorage.getItem('pg-doc-theme') === 'dark') {
document.documentElement.setAttribute('data-theme', 'dark');
}
btn.addEventListener('click', function () {
var root = document.documentElement;
if (root.getAttribute('data-theme') === 'dark') {
root.removeAttribute('data-theme');
localStorage.setItem('pg-doc-theme', 'light');
} else {
root.setAttribute('data-theme', 'dark');
localStorage.setItem('pg-doc-theme', 'dark');
}
});
})();
+11
View File
@@ -0,0 +1,11 @@
<svg width="48" height="48" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="穿山甲 mark">
<g transform="translate(96,0) scale(-1,1)">
<path d="M16 59 Q17 51 26 50 Q34 31 45 30 Q55 30 62 37 Q69 41 75 45 Q85 49 89 44 Q92 50 84 52 Q75 53 67 53 Q66 62 60 62 L56 62 Q54 55 49 55 Q47 62 37 62 L33 62 Q31 56 26 56 Q20 59 16 59 Z" fill="#B96A3D"></path>
<g stroke="#FAF3ED" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" fill="none" opacity="0.85">
<path d="M26 50 Q34 43 42 50"></path>
<path d="M36 48 Q44 41 52 48"></path>
<path d="M46 48 Q54 41 62 49"></path>
</g>
<circle cx="23" cy="55" r="2.2" fill="#FAF3ED"></circle>
</g>
</svg>

After

Width:  |  Height:  |  Size: 730 B

+408
View File
@@ -0,0 +1,408 @@
/* =============================================================
穿山甲 · Pangolin — 架构设计文档样式
令牌部分逐字拷贝自 design/colors_and_type.css(唯一真相源);
改令牌请先改源文件再同步此处。文档版式附在令牌之后。
============================================================= */
@import url('https://fonts.googleapis.com/css2?family=Sora:wght@500;600;700&family=Manrope:wght@400;500;600;700&family=Noto+Sans+SC:wght@400;500;700&family=JetBrains+Mono:wght@400;500&display=swap');
:root {
/* Clay / Copper — the pangolin-armor primary (warm earth) */
--clay-50: #FAF3ED;
--clay-100: #F2E2D4;
--clay-200: #E6C7AC;
--clay-300: #D9A982;
--clay-400: #CC8B5C;
--clay-500: #B96A3D; /* ← brand primary */
--clay-600: #9E5630;
--clay-700: #7E4426;
--clay-800: #5E331D;
--clay-900: #3D2213;
/* Sand / Taupe — warm neutral ramp */
--sand-50: #FAF8F4;
--sand-100: #F2EEE7;
--sand-200: #E6DFD3;
--sand-300: #D2C8B8;
--sand-400: #B0A491;
--sand-500: #8C8270;
--sand-600: #6B6253;
--sand-700: #4E4940;
--sand-800: #2E2A24;
--sand-900: #1F1C18;
--sand-950: #14110E;
/* Semantic hues (earth-tuned) */
--green-400: #7FB07A;
--green-500: #5B8C5A;
--green-600: #467046;
--amber-400: #E2B05A;
--amber-500: #D69A3C;
--amber-600: #B47E29;
--red-400: #D4715A;
--red-500: #C0533B;
--red-600: #9E4230;
/* Semantic tokens — light */
--bg: var(--sand-50);
--bg-subtle: var(--sand-100);
--surface: #FFFFFF;
--surface-2: var(--sand-50);
--overlay: rgba(31, 28, 24, 0.45);
--fg1: var(--sand-900);
--fg2: var(--sand-600);
--fg3: var(--sand-500);
--fg-on-accent: #FFFFFF;
--accent: var(--clay-500);
--accent-hover: var(--clay-600);
--accent-press: var(--clay-700);
--accent-subtle: var(--clay-50);
--accent-border: var(--clay-200);
--border: var(--sand-200);
--border-strong: var(--sand-300);
--ring: rgba(185, 106, 61, 0.35);
--success: var(--green-500);
--success-subtle: #E9F0E6;
--warning: var(--amber-500);
--warning-subtle: #F8EED6;
--danger: var(--red-500);
--danger-subtle: #F6E1DA;
/* Typography */
--font-display: 'Sora', 'Noto Sans SC', system-ui, sans-serif;
--font-sans: 'Manrope', 'Noto Sans SC', system-ui, sans-serif;
--font-cjk: 'Noto Sans SC', 'Manrope', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', ui-monospace, 'SFMono-Regular', monospace;
--text-display-xl: 3rem;
--text-display: 2.25rem;
--text-h1: 1.875rem;
--text-h2: 1.5rem;
--text-h3: 1.25rem;
--text-body-lg: 1.125rem;
--text-body: 1rem;
--text-sm: 0.875rem;
--text-caption: 0.75rem;
--leading-tight: 1.15;
--leading-snug: 1.3;
--leading-normal:1.5;
--leading-relaxed:1.65;
--tracking-tight: -0.02em;
--tracking-snug: -0.01em;
--tracking-wide: 0.04em;
--tracking-caps: 0.08em;
/* Spacing (4px base) */
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-5: 1.25rem;
--space-6: 1.5rem;
--space-8: 2rem;
--space-10: 2.5rem;
--space-12: 3rem;
--space-16: 4rem;
/* Radii */
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 14px;
--radius-xl: 20px;
--radius-2xl: 28px;
--radius-full: 999px;
/* Shadows (warm-tinted, soft) */
--shadow-sm: 0 1px 2px rgba(45, 30, 20, 0.06);
--shadow-md: 0 4px 14px rgba(45, 30, 20, 0.08);
--shadow-lg: 0 12px 32px rgba(45, 30, 20, 0.12);
--shadow-xl: 0 24px 60px rgba(45, 30, 20, 0.16);
--shadow-focus: 0 0 0 4px var(--ring);
/* Motion */
--ease-out: cubic-bezier(0.22, 1, 0.36, 1);
--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
--dur-fast: 140ms;
--dur-base: 220ms;
--dur-slow: 360ms;
}
[data-theme="dark"] {
--bg: var(--sand-950);
--bg-subtle: var(--sand-900);
--surface: #221E19;
--surface-2: #2A251F;
--overlay: rgba(0, 0, 0, 0.6);
--fg1: #F4EFE8;
--fg2: #B6AC9C;
--fg3: #897F6F;
--fg-on-accent: #1F1C18;
--accent: var(--clay-400);
--accent-hover: var(--clay-300);
--accent-press: var(--clay-500);
--accent-subtle: rgba(204, 139, 92, 0.14);
--accent-border: rgba(204, 139, 92, 0.30);
--border: rgba(242, 238, 231, 0.10);
--border-strong: rgba(242, 238, 231, 0.18);
--ring: rgba(204, 139, 92, 0.45);
--success: var(--green-400);
--success-subtle: rgba(127, 176, 122, 0.16);
--warning: var(--amber-400);
--warning-subtle: rgba(226, 176, 90, 0.16);
--danger: var(--red-400);
--danger-subtle: rgba(212, 113, 90, 0.16);
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
--shadow-md: 0 4px 14px rgba(0, 0, 0, 0.45);
--shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.5);
--shadow-xl: 0 24px 60px rgba(0, 0, 0, 0.55);
}
/* =============================================================
文档版式(doc-only
============================================================= */
* { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body {
background: var(--bg);
color: var(--fg1);
font-family: var(--font-sans);
font-size: var(--text-body);
line-height: var(--leading-normal);
transition: background-color var(--dur-base) var(--ease-in-out), color var(--dur-base) var(--ease-in-out);
}
a { color: var(--accent); text-decoration: none; transition: color var(--dur-fast) var(--ease-in-out); }
a:hover { color: var(--accent-hover); }
strong { font-weight: 600; }
/* ---- 顶栏 ---- */
.site-header {
position: sticky; top: 0; z-index: 50;
backdrop-filter: blur(12px);
background: color-mix(in srgb, var(--bg) 82%, transparent);
border-bottom: 1px solid var(--border);
}
.site-header .inner {
max-width: 1120px; margin: 0 auto;
display: flex; align-items: center; gap: var(--space-4);
padding: var(--space-3) var(--space-6);
}
.brand { display: flex; align-items: center; gap: var(--space-2); color: var(--fg1); }
.brand img { width: 26px; height: 26px; display: block; }
.brand .zh { font-family: var(--font-display); font-weight: 600; font-size: var(--text-body); letter-spacing: var(--tracking-snug); }
.brand .en { font-size: 0.625rem; font-weight: 600; letter-spacing: var(--tracking-caps); color: var(--fg3); text-transform: uppercase; }
.site-nav { margin-left: auto; display: flex; gap: 2px; flex-wrap: wrap; }
.site-nav a {
padding: 5px 11px; border-radius: var(--radius-full);
font-size: var(--text-sm); color: var(--fg2); white-space: nowrap;
transition: background-color var(--dur-fast) var(--ease-in-out), color var(--dur-fast) var(--ease-in-out);
}
.site-nav a:hover { background: var(--accent-subtle); color: var(--accent); }
.site-nav a.active { background: var(--accent-subtle); color: var(--accent); font-weight: 600; }
.theme-btn {
margin-left: var(--space-2);
width: 32px; height: 32px; border-radius: var(--radius-full);
border: 1px solid var(--border); background: var(--surface); color: var(--fg2);
cursor: pointer; display: grid; place-items: center;
transition: border-color var(--dur-fast) var(--ease-in-out);
}
.theme-btn:hover { border-color: var(--accent-border); color: var(--accent); }
.theme-btn svg { width: 15px; height: 15px; }
/* ---- 正文容器 ---- */
.doc { max-width: 1120px; margin: 0 auto; padding: var(--space-12) var(--space-6) var(--space-16); }
.doc-hero { max-width: 760px; margin-bottom: var(--space-10); }
.overline {
font-size: var(--text-caption); font-weight: 600;
letter-spacing: var(--tracking-caps); text-transform: uppercase;
color: var(--accent); margin-bottom: var(--space-3);
}
h1 {
font-family: var(--font-display); font-size: var(--text-display);
font-weight: 700; line-height: var(--leading-tight); letter-spacing: var(--tracking-tight);
margin-bottom: var(--space-4);
}
.doc-hero .lede { color: var(--fg2); font-size: var(--text-body-lg); line-height: var(--leading-relaxed); }
.doc-meta { display: flex; gap: var(--space-4); margin-top: var(--space-5); font-size: var(--text-caption); color: var(--fg3); font-family: var(--font-mono); }
section { margin-top: var(--space-12); }
h2 {
font-family: var(--font-display); font-size: var(--text-h2); font-weight: 600;
letter-spacing: var(--tracking-snug); line-height: var(--leading-snug);
margin-bottom: var(--space-5);
padding-top: var(--space-2);
}
h2 .num { color: var(--accent); font-family: var(--font-mono); font-size: var(--text-body); font-weight: 500; margin-right: var(--space-2); }
h3 { font-size: var(--text-h3); font-weight: 600; margin: var(--space-8) 0 var(--space-3); }
h4 { font-size: var(--text-body); font-weight: 600; margin: var(--space-6) 0 var(--space-2); }
p { margin-bottom: var(--space-3); color: var(--fg1); max-width: 76ch; }
p.muted, .muted { color: var(--fg2); }
ul, ol { margin: 0 0 var(--space-4) 1.25em; max-width: 76ch; }
li { margin-bottom: var(--space-1); }
li::marker { color: var(--accent); }
/* ---- 卡片与栅格 ---- */
.card {
background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius-xl); padding: var(--space-6);
box-shadow: var(--shadow-sm);
}
.grid2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--space-4); }
.grid3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-4); }
@media (max-width: 860px) { .grid2, .grid3 { grid-template-columns: 1fr; } }
/* 章节导航卡(index 用) */
.chapter-card {
display: block; background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius-xl); padding: var(--space-6);
box-shadow: var(--shadow-sm); color: var(--fg1);
transition: box-shadow var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out), border-color var(--dur-base) var(--ease-out);
}
.chapter-card:hover { box-shadow: var(--shadow-md); transform: translateY(-2px); border-color: var(--accent-border); color: var(--fg1); }
.chapter-card .ic {
width: 38px; height: 38px; border-radius: var(--radius-md);
background: var(--accent-subtle); color: var(--accent);
display: grid; place-items: center; margin-bottom: var(--space-3);
}
.chapter-card .ic svg { width: 19px; height: 19px; }
.chapter-card .no { font-family: var(--font-mono); font-size: var(--text-caption); color: var(--fg3); }
.chapter-card b { display: block; font-family: var(--font-display); font-size: var(--text-body-lg); font-weight: 600; margin: 2px 0 var(--space-2); }
.chapter-card p { font-size: var(--text-sm); color: var(--fg2); margin: 0; }
/* ---- 表格 ---- */
.tbl-wrap { overflow-x: auto; border: 1px solid var(--border); border-radius: var(--radius-lg); background: var(--surface); box-shadow: var(--shadow-sm); margin-bottom: var(--space-4); }
table { width: 100%; border-collapse: collapse; font-size: var(--text-sm); }
th {
text-align: left; background: var(--bg-subtle); color: var(--fg2);
font-size: var(--text-caption); font-weight: 600; letter-spacing: var(--tracking-wide);
padding: 10px 14px; border-bottom: 1px solid var(--border); white-space: nowrap;
}
td { padding: 11px 14px; border-bottom: 1px solid var(--border); vertical-align: top; }
tr:last-child td { border-bottom: none; }
td code, li code, p code {
font-family: var(--font-mono); font-size: 0.85em;
background: var(--bg-subtle); border: 1px solid var(--border);
padding: 1px 6px; border-radius: var(--radius-sm); white-space: nowrap;
}
/* ---- 代码块 ---- */
pre {
background: var(--sand-900); color: #F4EFE8;
padding: var(--space-5); border-radius: var(--radius-lg);
overflow-x: auto; font-size: var(--text-sm); line-height: 1.65;
margin-bottom: var(--space-4); box-shadow: var(--shadow-sm);
}
pre code { font-family: var(--font-mono); background: none; border: none; padding: 0; }
pre .c { color: #8C8270; } /* comment */
pre .k { color: #CC8B5C; } /* keyword (clay-400) */
pre .s { color: #7FB07A; } /* string (green-400) */
[data-theme="dark"] pre { background: var(--surface-2); border: 1px solid var(--border); }
/* ---- 状态胶囊(色点 + 文字,铁律 7) ---- */
.pill {
display: inline-flex; align-items: center; gap: 6px;
padding: 2px 10px; border-radius: var(--radius-full);
font-size: var(--text-caption); font-weight: 600; white-space: nowrap;
}
.pill .dot { width: 7px; height: 7px; border-radius: 50%; flex: none; }
.pill.ok { background: var(--success-subtle); color: var(--success); }
.pill.ok .dot { background: var(--success); }
.pill.warn { background: var(--warning-subtle); color: var(--warning); }
.pill.warn .dot { background: var(--warning); }
.pill.bad { background: var(--danger-subtle); color: var(--danger); }
.pill.bad .dot { background: var(--danger); }
.pill.brand { background: var(--accent-subtle); color: var(--accent); }
.pill.brand .dot { background: var(--accent); }
.pill.dim { background: var(--bg-subtle); color: var(--fg2); }
.pill.dim .dot { background: var(--fg3); }
/* ---- 提示块 ---- */
.callout {
border-left: 3px solid var(--accent); background: var(--accent-subtle);
border-radius: var(--radius-md); padding: var(--space-4) var(--space-5);
margin: var(--space-4) 0; font-size: var(--text-sm); max-width: 86ch;
}
.callout b:first-child { display: block; margin-bottom: 4px; }
.callout.warn { border-left-color: var(--warning); background: var(--warning-subtle); }
.callout.danger { border-left-color: var(--danger); background: var(--danger-subtle); }
.callout.ok { border-left-color: var(--success); background: var(--success-subtle); }
.callout p { margin-bottom: var(--space-2); }
.callout p:last-child, .callout ul:last-child { margin-bottom: 0; }
/* ---- 流程图(CSS 盒 + 箭头) ---- */
.diagram {
background: var(--bg-subtle); border: 1px solid var(--border);
border-radius: var(--radius-xl); padding: var(--space-6);
overflow-x: auto; margin-bottom: var(--space-4);
}
.diagram .cap {
font-size: var(--text-caption); color: var(--fg3); text-align: center;
margin-top: var(--space-4); font-family: var(--font-mono);
}
.flow { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.flow + .flow { margin-top: var(--space-4); }
.fbox {
background: var(--surface); border: 1.5px solid var(--border-strong);
border-radius: var(--radius-lg); padding: var(--space-3) var(--space-4);
min-width: 110px; text-align: center; flex: none;
box-shadow: var(--shadow-sm);
}
.fbox b { display: block; font-size: var(--text-sm); font-weight: 600; }
.fbox span { display: block; font-size: var(--text-caption); color: var(--fg3); margin-top: 2px; }
.fbox.accent { border-color: var(--accent-border); background: var(--accent-subtle); }
.fbox.accent b { color: var(--accent); }
.fbox.ok { border-color: var(--success); background: var(--success-subtle); }
.fbox.ok b { color: var(--success); }
.fbox.warn { border-color: var(--warning); background: var(--warning-subtle); }
.fbox.warn b { color: var(--warning); }
.fbox.bad { border-color: var(--danger); background: var(--danger-subtle); }
.fbox.bad b { color: var(--danger); }
.fbox.ghost { border-style: dashed; background: transparent; }
.farr { color: var(--fg3); font-family: var(--font-mono); font-size: var(--text-sm); flex: none; padding: 0 2px; }
.fnote { font-size: var(--text-caption); color: var(--fg3); flex-basis: 100%; }
/* 分层堆叠图 */
.stack { display: flex; flex-direction: column; gap: var(--space-2); }
.stack .layer {
background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius-lg); padding: var(--space-3) var(--space-5);
display: flex; align-items: baseline; gap: var(--space-4); flex-wrap: wrap;
}
.stack .layer .t { font-weight: 600; font-size: var(--text-sm); min-width: 11em; }
.stack .layer .d { font-size: var(--text-caption); color: var(--fg2); }
.stack .layer.accent { background: var(--accent-subtle); border-color: var(--accent-border); }
.stack .layer.accent .t { color: var(--accent); }
/* ---- 翻页 ---- */
.pager {
max-width: 1120px; margin: 0 auto; padding: 0 var(--space-6) var(--space-16);
display: flex; justify-content: space-between; gap: var(--space-4);
}
.pager a {
flex: 1; max-width: 380px; display: block;
background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius-lg); padding: var(--space-4) var(--space-5);
color: var(--fg1); box-shadow: var(--shadow-sm);
transition: border-color var(--dur-fast) var(--ease-in-out), box-shadow var(--dur-base) var(--ease-out);
}
.pager a:hover { border-color: var(--accent-border); box-shadow: var(--shadow-md); }
.pager a span { display: block; font-size: var(--text-caption); color: var(--fg3); margin-bottom: 2px; }
.pager a b { font-size: var(--text-sm); font-weight: 600; }
.pager a.next { text-align: right; margin-left: auto; }
footer.colophon {
text-align: center; padding: var(--space-8) var(--space-6) var(--space-12);
font-size: var(--text-caption); color: var(--fg3); border-top: 1px solid var(--border);
}
+153
View File
@@ -0,0 +1,153 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>总览 · 穿山甲架构设计</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" class="active">总览</a>
<a href="01-frontend.html">01 前端</a>
<a href="02-backend.html">02 后端</a>
<a href="03-database.html">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">Architecture Design · v1.0</div>
<h1>穿山甲 整体架构设计</h1>
<p class="lede">极简、轻量、亲和的跨平台网络加速应用。本设计覆盖前端五端(移动 / iPad / 桌面 / 官网 / 用户中心)、Go 控制面、MySQL 数据层、弹性节点拓扑、Web 安全与灾备、以及贯穿全局的安全与断网应对策略。UI 实现以 <code>design/ui_kits/</code> React 原型为像素基准,100% 还原既有设计系统。</p>
<div class="doc-meta"><span>2026-06-11</span><span>依据 design/ + plan/ + server/ARCHITECTURE.md v0.1</span></div>
</div>
<section id="decisions">
<h2><span class="num">§1</span>关键架构决策</h2>
<div class="tbl-wrap"><table>
<thead><tr><th>领域</th><th>决策</th><th>理由 / 相对蓝本的变化</th></tr></thead>
<tbody>
<tr><td><strong>数据面协议</strong></td><td>sing-boxVLESS + Vision + <strong>REALITY</strong> 主线,<strong>Hysteria2</strong> 备线(端口跳跃)</td><td>替换蓝本 v0.1 的 WireGuard——WG 协议特征明显,在目标网络环境必被识别阻断;plan/ phase-0~3 已实测选定 REALITY。API 契约形状保留,仅 <code>connect</code> 语义适配(见 02 章)。</td></tr>
<tr><td><strong>数据库</strong></td><td><strong>MySQL 8.x</strong>InnoDB / utf8mb4+ Redis</td><td>按本次要求替换蓝本的 Postgres;schema 全量移植并做类型适配(见 03 章)。</td></tr>
<tr><td><strong>移动 / iPad / 桌面客户端</strong></td><td><strong>Flutter</strong>(共享 ~90% UI 代码)+ sing-box libbox 内核;iPad = 移动端工程宽度 ≥900 自适应切侧栏分栏(像素基准 <code>ui_kits/tablet/</code></td><td><code>design/flutter/</code> 起步包即为此准备;原生 VPN 壳(NetworkExtension / VpnService / TUN)性能与稳定性最优。</td></tr>
<tr><td><strong>官网</strong></td><td><strong>Astro</strong> SSG → Cloudflare Pages 多镜像</td><td>纯静态、秒级整站复制到新域名,抗封锁;<code>ui_kits/website</code> JSX 近乎逐字迁移。</td></tr>
<tr><td><strong>Web 用户中心</strong></td><td><strong>Next.js</strong>React</td><td><code>ui_kits/usercenter</code> 本身是 React——组件直接复用,还原保真度最高。</td></tr>
<tr><td><strong>节点拓扑</strong></td><td>三层弹性拓扑(入口 / 中转 / 落地)+ 节点生命周期状态机 + 一键/自动换节点换 IP</td><td>补全 plan/ 中"IP 轮换自动化"的具体设计:判封探针、多厂商 VPS 池、目录版本灰度(见 04 章)。</td></tr>
</tbody>
</table></div>
</section>
<section id="topology">
<h2><span class="num">§2</span>系统全景</h2>
<div class="diagram">
<div class="flow">
<div class="fbox accent"><b>移动 App</b><span>Flutter · iOS/Android/iPad</span></div>
<div class="fbox accent"><b>桌面客户端</b><span>Flutter · Win/macOS</span></div>
<div class="fbox accent"><b>用户中心</b><span>Next.js</span></div>
<div class="fbox accent"><b>官网</b><span>Astro · 静态</span></div>
</div>
<div class="flow">
<div class="farr">│ HTTPS(域名池 + CDN 前置 + IP 直连兜底)</div>
</div>
<div class="flow">
<div class="fbox"><b>CDN / 域名池</b><span>Cloudflare · 源站隐藏</span></div>
<div class="farr"></div>
<div class="fbox accent"><b>API 控制面</b><span>Go 单体 · 模块分包</span></div>
<div class="farr"></div>
<div class="fbox"><b>MySQL 8</b><span>主数据 · 加密异地备份</span></div>
<div class="fbox"><b>Redis</b><span>验证码/限流/节点状态</span></div>
</div>
<div class="flow">
<div class="fbox ghost"><b>发卡店 Webhook</b><span>激活码入库(签名)</span></div>
<div class="farr">→ 控制面 ←</div>
<div class="fbox ghost"><b>管理端</b><span>内网 · 白名单 + 2FA</span></div>
<div class="fbox ghost"><b>探针系统</b><span>多 ISP 拨测 · 判封</span></div>
</div>
<div class="flow">
<div class="farr">│ mTLS gRPC(节点注册 / 心跳 / 凭证下发 / 回收)</div>
</div>
<div class="flow">
<div class="fbox"><b>入口节点池</b><span>消耗品 IP · 可秒换</span></div>
<div class="farr"></div>
<div class="fbox"><b>中转层(可选)</b><span>隧道转发</span></div>
<div class="farr"></div>
<div class="fbox"><b>落地节点池</b><span>精品 IP · 稳定出口</span></div>
<div class="fnote">用户流量(REALITY / Hysteria2)只走数据面,绝不经过 API;节点零用户库。</div>
</div>
<div class="cap">控制面 / 数据面彻底分离 · 节点目录带版本灰度 · 任一节点被封不影响全局</div>
</div>
<p class="muted">四条铁律(继承蓝本 §0):① 控制面/数据面分离;② App 内无支付,资金流全部外部化为激活码;③ 节点可秒级灰度,客户端缓存最后一份可用目录兜底;④ 最小数据原则——不记录浏览内容与目的地,仅保留计费所需的字节数/分钟数。</p>
</section>
<section id="chapters">
<h2><span class="num">§3</span>章节导航</h2>
<div class="grid3">
<a class="chapter-card" href="01-frontend.html">
<div class="ic"><i data-lucide="smartphone"></i></div>
<span class="no">01</span><b>前端架构</b>
<p>五端技术栈选型、100% 还原策略(令牌 / 像素基准 / 验收清单)、客户端断网弹性。</p>
</a>
<a class="chapter-card" href="02-backend.html">
<div class="ic"><i data-lucide="server"></i></div>
<span class="no">02</span><b>后端设计</b>
<p>Go 模块化单体、API 契约 v1(遵循设计蓝本)、激活码 / 试用 / 广告解锁关键流程。</p>
</a>
<a class="chapter-card" href="03-database.html">
<div class="ic"><i data-lucide="database"></i></div>
<span class="no">03</span><b>数据库</b>
<p>MySQL 8 全量 schema(蓝本移植 + 拓扑扩展)、Redis 键设计、备份与恢复策略。</p>
</a>
<a class="chapter-card" href="04-infra.html">
<div class="ic"><i data-lucide="network"></i></div>
<span class="no">04</span><b>机器与网络拓扑</b>
<p>三层弹性拓扑、节点生命周期状态机、一键/自动换节点换 IP、判封探针与调度。</p>
</a>
<a class="chapter-card" href="05-web-security.html">
<div class="ic"><i data-lucide="shield"></i></div>
<span class="no">05</span><b>Web 安全与灾备</b>
<p>CDN 前置与源站隐藏、域名池、备份体系、被封应对 Runbook、应急广播。</p>
</a>
<a class="chapter-card" href="06-security.html">
<div class="ic"><i data-lucide="lock"></i></div>
<span class="no">06</span><b>安全总纲</b>
<p>威胁模型总表、身份隔离红线、无日志口径、密码学口径、断网应对策略矩阵。</p>
</a>
</div>
</section>
<section id="sources">
<h2><span class="num">§4</span>设计依据与文档关系</h2>
<div class="tbl-wrap"><table>
<thead><tr><th>来源</th><th>角色</th><th>本设计如何使用</th></tr></thead>
<tbody>
<tr><td><code>design/CLAUDE.md</code> + <code>colors_and_type.css</code></td><td>设计铁律与令牌真相源</td><td>13 条铁律作为前端验收清单;套餐口径(§7)为所有界面/接口数字的单一来源。</td></tr>
<tr><td><code>design/ui_kits/*</code></td><td>像素验收标准</td><td>各端实现逐屏对照 React 原型(含新增 <code>tablet/</code> iPad kit),差异即缺陷。</td></tr>
<tr><td><code>design/server/ARCHITECTURE.md</code>v0.1</td><td>后端蓝本</td><td>API 契约、数据模型、激活码流程全部继承;数据面协议与 DB 选型按本设计修订。</td></tr>
<tr><td><code>plan/phase-0~3</code> + <code>plan/security-运营安全.md</code></td><td>实施路线与威胁模型</td><td>协议选型(REALITY/Hy2)、IP 分级(消耗品/精品池)、身份隔离红线全部纳入。</td></tr>
</tbody>
</table></div>
<div class="callout"><b>套餐口径(引用 design/CLAUDE.md §7,改数字先改那里)</b>
注册享 7 天免费试用(不限时长与节点)→ 之后免费版 1 个基础节点 + 每日 10 分钟 + 每日使用前看激励视频解锁;PRO ¥25/月(年付 ¥20/月),80+ 线路,5 台设备;团队版 ¥99/月 10 席位。收款走外部渠道(发卡店 / USDT TRC20 / Telegram / LINE / 邮箱),App 与官网内无支付表单。</div>
</section>
</main>
<nav class="pager">
<a class="next" href="01-frontend.html"><span>下一章</span><b>01 前端架构 →</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>