Chapter 03 · Database

数据库设计

MySQL 8.x(InnoDB · utf8mb4 · 时区 UTC)作为主数据存储,Redis 承担验证码、限流、节点实时状态与会话。schema 自蓝本 §2 全量移植并做 MySQL 类型适配,另为弹性拓扑(04 章)扩展三张表。

§1移植约定(Postgres 蓝本 → MySQL 8)

蓝本用法MySQL 8 适配说明
jsonbJSON审计 meta、节点标签等
tags[](数组)JSON(数组)查询用 JSON_CONTAINS,量大再拆关联表
timestamptzDATETIME(6),应用层一律 UTC连接串强制 time_zone='+00:00'
枚举字符串ENUM(...)状态机字段固定且少,ENUM 自带约束
主键BIGINT UNSIGNED AUTO_INCREMENT 内部主键 + 对外 uuid CHAR(36) 业务标识避免顺序 ID 外泄可枚举;UUID 不做主键防 InnoDB 页分裂

§2核心表(DDL 摘要)

-- 账户与设备
CREATE TABLE users (
  id          BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  uuid        CHAR(36) NOT NULL UNIQUE,            -- 对外标识
  email       VARCHAR(255) NOT NULL UNIQUE,
  pw_hash     VARCHAR(255) NOT NULL,               -- argon2id
  dp_uuid     CHAR(36) NOT NULL,                   -- 数据面凭证 UUID(可轮换,节点侧只见它)
  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;

-- 套餐与订阅(数字口径 = design/CLAUDE.md §7)
CREATE TABLE plans (
  id            BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  code          ENUM('free','pro','team') NOT NULL UNIQUE,
  max_devices   INT NOT NULL,                      -- free 1 / pro 5 / team 10
  daily_minutes INT NULL,                          -- free 10;NULL = 不限
  ad_gate       BOOLEAN NOT NULL DEFAULT FALSE     -- free 每日看广告解锁
) 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,       -- 注册自动 7 天试用 source='trial'
  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;

-- 激活码(App 内无支付,资金流外部化)
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,          -- SHA-256;明文只出现一次
  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;

-- 用量(最小数据原则:仅字节/分钟,无目的地)
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,                 -- 免费版当日激励视频解锁时刻
  PRIMARY KEY (user_id, date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 审计(兑换/封禁/节点操作必记)
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;

§3节点与弹性拓扑表(支撑 04 章)

-- 节点:蓝本 nodes 表 + 拓扑/生命周期扩展字段
CREATE TABLE nodes (
  id          BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  uuid        CHAR(36) NOT NULL UNIQUE,
  region      VARCHAR(8) NOT NULL,                 -- HK / JP / SG / US…(UI 显示 2 字母码块)
  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,         -- 消耗品池 / 精品池
  endpoint    VARCHAR(255) NOT NULL,               -- ip:port(REALITY)
  hy2_port    INT NULL,                            -- Hysteria2 端口跳跃区间起点
  reality_pbk VARCHAR(64) NOT NULL,
  reality_sni VARCHAR(128) NOT NULL,               -- 伪装目标 SNI
  provider_id BIGINT UNSIGNED NOT NULL,
  tags        JSON NULL,
  status      ENUM('provisioning','probing','up','draining','down','destroyed')
              NOT NULL DEFAULT 'provisioning',     -- 生命周期状态机(04 章 §3)
  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;

-- VPS 厂商池(多厂商、账号隔离,见 04 章 §5)
CREATE TABLE providers (
  id          BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  name        VARCHAR(64) NOT NULL,
  api_kind    VARCHAR(32) NOT NULL,                -- 开通自动化所用 API 适配器
  regions     JSON NOT NULL,
  pool        ENUM('consumable','premium') NOT NULL,
  enabled     BOOLEAN NOT NULL DEFAULT TRUE,
  note        VARCHAR(255) NULL                    -- 凭证不入库,存独立 secrets 管理
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 节点事件:状态迁移与判封依据(探针明细在 Redis,结论落库)
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,                             -- 探针命中率、判封依据等
  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;

-- 目录版本:nodes 任何变更 bump,客户端 if_version 灰度
CREATE TABLE directory_version (
  id      TINYINT PRIMARY KEY DEFAULT 1,
  version BIGINT UNSIGNED NOT NULL,
  CHECK (id = 1)
) ENGINE=InnoDB;
节点表里没有的东西(有意为之)节点服务器上不落任何用户身份;本表也不存节点登录凭证(SSH 密钥、厂商 API key 走独立 secrets 管理,与运营身份隔离)。

§4Redis 键设计

键模式类型 / TTL用途
auth:code:{email}STRING · 10min邮箱验证码(同邮箱 1/min 限发)
rl:{scope}:{key}ZSET · 滑动窗口接口限流(IP / 用户 / 邮箱多维度)
jwt:refresh:{jti}STRING · 30drefresh token 白名单(登出/封禁即删)
node:load:{node_uuid}HASH · 90sagent 每 30s 上报:在线数/带宽/CPU——目录排序与客户端"信号条"数据源
probe:{node_uuid}:{vantage}STRING · 30min境内外探针最近结果(判封原始数据)
dir:versionSTRING目录版本缓存(DB 为权威)
redeem:fail:{user_id}STRING · 1h兑换失败计数(5 次锁 1 小时)

Redis 全部可重建(无权威数据),故不参与备份;重启后由 agent 心跳与 DB 回填。

§5备份与恢复

策略目标
全量每日 xtrabackup(或托管 DB 自动快照)RPO ≤ 15min
增量binlog 持续归档(15min 滚动上传)
加密与异地备份用 age 公钥加密后传对象存储(与生产、与运营身份均隔离的账号);保留 30 天单点被端不丢数据
恢复演练每月在隔离环境做一次全量 + binlog 回放演练,记录耗时RTO ≤ 4h(控制面整体重建含 DB,见 05 章)
备份内容敏感性用户库(邮箱)是全系统最敏感资产:备份加密私钥离线保存、不进任何在线主机;对象存储账号用独立身份注册,与 VPS / 域名 / 收款不产生关联(身份隔离红线,见 06 章)。