ShortPro-域名管理与PWA分流方案-v1.0_2.md
旧文件/ShortPro-域名管理与PWA分流方案-v1.0_2.md · 27.0 KB · 2026-09-22 15:07:00
原始文件 下载

ShortPro 域名管理与 PWA 分流方案

版本 v1.0
日期 2026-05-14
状态 设计草案,待评审
适用范围 ShortPro 印尼市场 Web/PWA 端


0. 文档目的

为 ShortPro 印尼业务设计一套抗封锁、可持续运营的域名与 PWA 分发架构。在 Kominfo 周期性封域名的现实下,实现:


1. 设计原则

  1. 三层域名彻底解耦:发现 / 安装 / 数据各自独立,任一层失效不传染至其他层
  2. 用户确定性分桶:hash(device_id) % N 把用户均匀分布到多个安装域名,单域被封只影响一个桶(N 分之一用户)
  3. 基础设施多样化:注册商、DNS、IP 段、TLS 证书、CDN、Server 指纹全部分散,避免被批量识别封禁
  4. 时间换迁移:Service Worker 缓存让被封域名上的 PWA 继续可用数周到数月,用这段时间通过召回机制平滑迁移用户
  5. 离线优先:app shell 必须 precache,PWA 启动不依赖任何网络请求
  6. 召回机制 Day 1 建好:不能等域名被封了才想怎么联系用户

2. 整体架构

┌─────────────────────────────────────────────────────────────────┐
│  推广渠道:WhatsApp / TikTok / KOL / TG Channel / 短链          │
└────────────────────────────┬────────────────────────────────────┘
                             ▼
┌─────────────────────────────────────────────────────────────────┐
│  第一层:分发域名池(Discovery)                                  │
│  spro.id / sp1.app / getshort.app / ...                         │
│  作用:动态告诉用户去哪个安装域名                                 │
│  特性:静态 JS / JSON 配置,几乎无业务逻辑,易替换                 │
└────────────────────────────┬────────────────────────────────────┘
                             ▼
                  hash(device_id) % N
                             ▼
┌─────────────────────────────────────────────────────────────────┐
│  第二层:安装域名池(Install)                                    │
│  dramaku-app.com / nontonpro.app / kisahdrama.net / ...         │
│  作用:PWA 真正的安装来源,manifest scope 绑定                    │
│  特性:用户加桌面后图标绑死此域名                                 │
└────────────────────────────┬────────────────────────────────────┘
                             ▼
                Service Worker 接管所有请求
                             ▼
┌─────────────────────────────────────────────────────────────────┐
│  第三层:API / 资源域名池                                        │
│  api-1.xxx / api-2.xxx(接口)                                   │
│  cdn-1.xxx / cdn-2.xxx(视频/图片)                              │
│  作用:App 内所有数据请求的目的地                                 │
│  特性:与安装域名解耦,优先级轮询 + 故障自动 fallback             │
└─────────────────────────────────────────────────────────────────┘

3. 域名架构详述

3.1 第一层:分发域名(Discovery Pool)

维度 配置
数量 活跃 3-5 个,冷备 5-10 个
作用 告诉用户"现在去哪个域名装 App"
内容 几乎静态的 H5 + 一段 JS 跳转逻辑
推广渠道 WhatsApp 邀请链接、TikTok bio、KOL 物料、印尼论坛
域名风格 短、好口播,如 spro.id / sp1.app / getshort.app
部署 Cloudflare Pages / Vercel + 自有域名 + CDN

备份发现路径(防分发域名也被封):

这些托管平台属于 Google / Microsoft / Cloudflare 基建,印尼几乎不会单独封禁。

3.2 第二层:安装域名(Install Pool)

维度 配置
数量 活跃 5-10 个,冷备 5-10 个
作用 PWA 真正的安装来源,manifest.json 的 origin
命名 无规律、混合 TLD,避免被批量识别
注册商 至少分散到 5 家(GoDaddy / Namecheap / Porkbun / Cloudflare Registrar / 印尼本地)
DNS 独立 NS,不全押 Cloudflare
IP 不同 IP 段,跨 ASN
TLS 不要全用同一 CA;Let's Encrypt + ZeroSSL + Cloudflare 混搭

命名示例(故意不规律):

dramaku-app.com
nontonpro.app
kisahdrama.net
layardrama.com
ceritamu.app
serial-malam.id
filmwiks.co
tontonan.app

关键约束:

3.3 第三层:API / 资源域名(API Pool)

这是整套架构最关键的一层——它决定了"装在桌面上的 App 在安装域名被封后能不能继续用"。

子池 用途 数量
api-*.xxx 业务接口(登录、视频元数据、coin 交易等) 活跃 3-5 个
cdn-*.xxx 视频流、图片、字幕 活跃 3-5 个
static-*.xxx JS/CSS 静态包(可选) 活跃 2-3 个

与安装域名彻底解耦的设计要点:

  1. PWA 代码里 不写死任何 API URL,基础 URL 从 IndexedDB 读
  2. 首次启动:从一个 stable config 端点拉最新 API 池清单(Cloudflare Workers + KV,基本不会被封)
  3. SW 内置 fallback 列表,主接口失败自动切到下一个
  4. 接口域和视频域分开:接口被封不影响视频播放,反之亦然
  5. TLS 指纹 / Server header 错开:别让所有 API 域共用同一证书 fingerprint,防 ISP 按指纹批量识别

4. 用户分桶策略

4.1 分桶算法

bucket = hash(device_id) mod N

其中:
  device_id = 首次访问时生成的 UUID,存 localStorage + IndexedDB(双备份)
  N         = 当前活跃安装域名数
  hash      = FNV-1a 32-bit(轻量、分布均匀,不需要加密强度)

为什么用确定性 hash 而不是随机分配:

4.2 分桶持久化

服务端必须存一份映射表,即使 device_id 丢失,登录后仍能恢复:

CREATE TABLE user_install_binding (
  user_id          BIGINT      PRIMARY KEY,
  device_id        VARCHAR(64),
  install_origin   VARCHAR(128) NOT NULL,
  bucket           SMALLINT     NOT NULL,
  assigned_at      TIMESTAMP    DEFAULT NOW(),
  status           VARCHAR(16)  DEFAULT 'active',  -- active/deprecated/migrating
  migrated_to      VARCHAR(128) DEFAULT NULL,
  INDEX idx_origin (install_origin),
  INDEX idx_bucket (bucket)
);

4.3 不同获客渠道的入口策略

渠道 入口 分桶逻辑
WhatsApp 邀请 分发域名 动态分桶
TikTok / IG 广告 分发域名 动态分桶
KOL 专属链接 KOL 专属安装域名 不分桶,强绑定该域名(便于追踪 + 风险隔离)
自然搜索 主分发域名 动态分桶
老用户拉新 分发域名 + referral code 动态分桶 + 关联推荐人

5. PWA 安装流程

5.1 时序

用户                分发域名              Dispatch API         安装域名            浏览器
 │                    │                      │                    │                  │
 │─ 点 WA 链接 ──────▶│                      │                    │                  │
 │                    │─ 读/建 device_id     │                    │                  │
 │                    │─ POST /dispatch ────▶│                    │                  │
 │                    │                      │─ hash + 查表       │                  │
 │                    │◀── { install_origin, │                    │                  │
 │                    │      bucket }        │                    │                  │
 │                    │─ 302 跳转 ─────────────────────────────▶│                  │
 │                    │                      │                    │─ 加载 PWA        │
 │                    │                      │                    │─ 注册 SW         │
 │                    │                      │                    │─ Precache shell  │
 │                    │                      │                    │─ 提示加桌面 ────▶│
 │◀────────────────────────────────────── 用户点"加到主屏幕"────────────────────────│
 │                    │                      │                    │─ 写映射表        │

5.2 Dispatch 接口契约

Endpoint POST https://spro.id/dispatch

Request

{
  "device_id": "8f3a-e7b2-...",
  "channel": "whatsapp",
  "referrer": "user_12345",
  "ua": "Mozilla/5.0 ... Chrome/...",
  "lang": "id-ID"
}

Response

{
  "install_origin": "https://dramaku-app.com",
  "bucket": 3,
  "expires_at": 1715731200,
  "fallback_origins": [
    "https://nontonpro.app",
    "https://kisahdrama.net"
  ]
}

fallback_origins 写进客户端 localStorage,万一首选安装域名访问失败,前端可以自己切到 fallback。


6. Service Worker 设计(核心)

6.1 缓存策略总览

资源类型 策略 缓存名 失效
App shell(HTML/CSS/JS/字体) Cache-first shell-v{version} 仅版本升级
导航请求(navigate) Cache-first(回退到 shell) shell-v{version} 同上
API 请求 Network with pool fallback 不缓存(或短 TTL) 实时
图片/视频缩略图 Cache-first + LRU media-v1 LRU 50MB 上限
视频流 不走 SW(直接走 CDN) - -

6.2 完整 SW 代码模板

// sw.js
const SHELL_VERSION = 'shell-v20260514-01';
const MEDIA_CACHE = 'media-v1';
const SHELL_ASSETS = [
  '/',
  '/index.html',
  '/offline.html',
  '/assets/app.js',
  '/assets/app.css',
  '/assets/fonts/inter.woff2',
  '/assets/icons/icon-192.png',
  '/assets/icons/icon-512.png',
];

// API 池配置(初始,后续从 config 端点动态更新)
const API_POOL_DEFAULT = [
  'https://api-1.shortpro-api.com',
  'https://api-2.shortpro-api.com',
  'https://api-3.shortpro-api.com',
];
const CONFIG_ENDPOINT = 'https://config.shortpro.workers.dev/v1/config';

// ============ 安装阶段:precache app shell ============
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(SHELL_VERSION)
      .then(cache => cache.addAll(SHELL_ASSETS))
      .then(() => self.skipWaiting())
  );
});

// ============ 激活阶段:清理旧版本 ============
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then(keys =>
      Promise.all(
        keys
          .filter(k => k.startsWith('shell-') && k !== SHELL_VERSION)
          .map(k => caches.delete(k))
      )
    ).then(() => self.clients.claim())
  );
});

// ============ 请求拦截 ============
self.addEventListener('fetch', (event) => {
  const req = event.request;
  const url = new URL(req.url);

  // 1. 导航请求 → cache-first,网络失败也无所谓
  if (req.mode === 'navigate') {
    event.respondWith(handleNavigation(req));
    return;
  }

  // 2. API 请求 → 池化路由 + fallback
  if (url.pathname.startsWith('/api/')) {
    event.respondWith(routeToApiPool(req));
    return;
  }

  // 3. 媒体资源 → cache-first
  if (req.destination === 'image') {
    event.respondWith(cacheFirstMedia(req));
    return;
  }

  // 4. 其他静态资源 → cache-first
  event.respondWith(
    caches.match(req).then(r => r || fetch(req))
  );
});

// 导航处理:始终返回 cached shell
async function handleNavigation(req) {
  const cache = await caches.open(SHELL_VERSION);
  const cached = await cache.match('/index.html');
  if (cached) return cached;

  try {
    return await fetch(req);
  } catch {
    return cache.match('/offline.html');
  }
}

// API 池路由:从 IndexedDB 读最新池子,挨个试
async function routeToApiPool(req) {
  const pool = await getApiPool();
  const path = new URL(req.url).pathname + new URL(req.url).search;

  for (const base of pool) {
    try {
      const target = base + path;
      const response = await fetch(target, {
        method: req.method,
        headers: req.headers,
        body: req.method !== 'GET' ? await req.clone().blob() : undefined,
        credentials: 'include',
      });

      if (response.ok || response.status < 500) {
        return response;
      }
    } catch (err) {
      console.warn(`[API Pool] ${base} failed:`, err.message);
      reportApiFailure(base, err.message); // 异步上报,不 await
    }
  }

  // 所有 API 都挂了
  return new Response(
    JSON.stringify({ error: 'all_api_unreachable' }),
    { status: 503, headers: { 'Content-Type': 'application/json' } }
  );
}

// 从 IndexedDB 读 API 池(每次启动刷新一次)
async function getApiPool() {
  const db = await openDB();
  const stored = await db.get('config', 'api_pool');
  return stored?.value || API_POOL_DEFAULT;
}

// 后台静默刷新 config(在 activate 后调用,或定时调用)
async function refreshConfig() {
  try {
    const resp = await fetch(CONFIG_ENDPOINT, { cache: 'no-store' });
    const config = await resp.json();
    const db = await openDB();
    await db.put('config', { key: 'api_pool', value: config.api_pool });

    // 同时检查当前安装域名是否被标记为废弃
    if (config.deprecated_origins?.includes(self.location.origin)) {
      notifyMigration(config.new_install_url);
    }
  } catch (err) {
    console.warn('[Config] refresh failed:', err);
  }
}

// 通知前端弹出迁移引导横幅
function notifyMigration(newUrl) {
  self.clients.matchAll().then(clients => {
    clients.forEach(client => {
      client.postMessage({
        type: 'MIGRATION_REQUIRED',
        newInstallUrl: newUrl,
      });
    });
  });
}

// 媒体缓存(LRU 简化版,实际可用 workbox)
async function cacheFirstMedia(req) {
  const cache = await caches.open(MEDIA_CACHE);
  const cached = await cache.match(req);
  if (cached) return cached;

  const response = await fetch(req);
  if (response.ok) cache.put(req, response.clone());
  return response;
}

// 启动时刷新 config
self.addEventListener('activate', (event) => {
  event.waitUntil(refreshConfig());
});

// 接收前端消息(例如手动触发 config 刷新)
self.addEventListener('message', (event) => {
  if (event.data?.type === 'REFRESH_CONFIG') {
    refreshConfig();
  }
});

6.3 前端迁移引导(配合 SW 的 MIGRATION_REQUIRED 消息)

// app.js 启动逻辑
navigator.serviceWorker.addEventListener('message', (event) => {
  if (event.data?.type === 'MIGRATION_REQUIRED') {
    showMigrationBanner(event.data.newInstallUrl);
  }
});

function showMigrationBanner(newUrl) {
  const banner = document.createElement('div');
  banner.className = 'migration-banner';
  banner.innerHTML = `
    <div class="banner-content">
      <span>🚀 Aplikasi sudah upgrade! Klik untuk pindah ke versi terbaru</span>
      <button onclick="migrateTo('${newUrl}')">Pindah</button>
      <button onclick="this.parentElement.parentElement.remove()">Nanti</button>
    </div>
  `;
  document.body.appendChild(banner);
}

function migrateTo(newUrl) {
  // 携带 device_id 和当前 user token,新域名落地页可以无感登录
  const deviceId = localStorage.getItem('device_id');
  const migrationToken = generateMigrationToken(); // 一次性 token
  window.location.href = `${newUrl}/migrate?d=${deviceId}&t=${migrationToken}`;
}

7. 故障切换机制

7.1 安装域名被封

已安装用户 → 几乎无感: - SW 拦截导航 → cache hit → shell 加载 → API 调用走 API 池 → 正常使用 - 这就是三层架构的核心收益

新增用户: - 服务端把此域名 status 改为 cooling - Dispatch 算法把这个域名从分桶池移除,N 减 1 - 原来分到此桶的新用户重新打散到其他桶 - 此域名进入观察期(7-14 天后若仍不可达,转为 dead)

7.2 API 域名被封

7.3 分发域名被封

影响范围最小,只影响新增:

  1. 监控告警 → 运营切换推广物料里的链接
  2. WhatsApp 群发新链接给现有用户(虽然他们已装,但可能要分享给朋友)
  3. Telegram Channel、KOL bio、短链指向同步更新
  4. 备份发现路径(Gist / Pastebin)同步更新

8. 用户召回机制(必须 Day 1 建好)

域名被封是必然事件,差别只是何时。用户召回能力直接决定业务能不能扛过封禁周期。

8.1 召回渠道优先级

优先级 渠道 覆盖率 实时性 印尼适用度
P0 FCM 推送 60-70% 秒级 ⭐⭐⭐⭐⭐
P1 WhatsApp 广播 80%+(注册时收集) 分钟级 ⭐⭐⭐⭐⭐
P2 邮件 50% 分钟级 ⭐⭐⭐
P3 应用内迁移横幅 100%(仍能打开的) 实时 ⭐⭐⭐⭐⭐
P4 Telegram Channel 置顶 长尾 静态 ⭐⭐⭐
P5 TikTok / IG bio 更新 长尾 静态 ⭐⭐⭐⭐

8.2 FCM 推送细节

8.3 WhatsApp 召回

8.4 离线 fallback 页

每个安装域名的 SW 必须 precache 一个 offline.html,内容包括:

<!-- offline.html 关键片段 -->
<script>
  fetch('https://config.shortpro.workers.dev/v1/fallback')
    .then(r => r.json())
    .then(data => {
      document.getElementById('new-url').href = data.discovery_url;
      document.getElementById('new-url').textContent = data.discovery_url;
    })
    .catch(() => {
      document.getElementById('new-url').href = 'https://t.me/shortpro_id';
    });
</script>

9. 域名采购与运营节奏

9.1 采购规模

类型 持有量 活跃量 冷备量
分发域名 8-10 3-5 5
安装域名 20-25 5-10 10-15
API 域名 10-15 3-5 7-10
合计 40+ 15-20 25+

9.2 采购策略

9.3 CDN / 基础设施分散

域名类型 推荐 CDN
分发域名 Cloudflare Pages / Vercel(便宜、静态)
安装域名 Cloudflare + Bunny.net + Fastly(混搭)
API 域名 自建 ALB + Cloudflare 中转
视频 CDN Bunny Stream + 火山 / 阿里(东南亚节点)

9.4 轮换节奏


10. 监控与健康检查

10.1 多 ISP 印尼侧探测

部署探测节点(可用低成本 VPS 或者付费 uptime 服务 + 印尼出口):

ISP 占比 监测方式
Telkomsel ~40% 印尼云主机 + Telkomsel SIM 卡
Indosat Ooredoo ~25% 同上,Indosat SIM
XL Axiata ~15% XL SIM
Smartfren ~10% Smartfren SIM
其他 ~10% 通用印尼云主机

每 5 分钟从各 ISP 出口 DNS + HTTPS 探测全部活跃域名,失败 3 次连续触发告警。

10.2 Kominfo TrustPositif 监控

10.3 客户端埋点

每次启动上报(发送到 API 池):

{
  "event": "app_launch",
  "device_id": "...",
  "install_origin": "https://dramaku-app.com",
  "bucket": 3,
  "api_origin_in_use": "https://api-2.xxx",
  "sw_version": "v20260514-01",
  "shell_cache_hit": true,
  "online": true,
  "ts": 1715731200
}

后台 dashboard 按 install_origin 维度统计:


11. 实施计划(6 周)

Week 1 — 域名与基础设施

Week 2 — 三层架构基础

Week 3 — 分桶与 SW

Week 4 — 召回机制

Week 5 — 监控与运营工具

Week 6 — 灰度与压测


12. 关键技术坑与对策

# 对策
1 manifest.json 必须每个安装域名独立 host,跨 origin 不算同一个 PWA 部署脚本自动生成各域名版 manifest
2 Service Worker scope 只能管自己的源 接受此限制,通过分桶 + 召回机制弥补
3 登录态跨域无法共享 device_id + 一次性 migration token + WhatsApp OTP 兜底
4 部分 Android 浏览器(三星浏览器、UC)PWA 支持差 引导用 Chrome,首屏检测浏览器并提示
5 ISP 按 TLS 指纹批量封 各安装域名 TLS 证书不要共用同一 CA / fingerprint
6 DNS over HTTPS 在印尼部分被干扰 SW 不依赖 DOH,正常 DNS + cache 兜底
7 FCM 在国行 ROM(印尼小米/Vivo 部分机型)不稳定 WhatsApp 召回作为主路径
8 WhatsApp Business 模板审核严格 提前备 5+ 个不同话术模板
9 域名突然被注册商收回(GoDaddy 偶发) 注册商分散,域名分批,WHOIS 真实联系方式备用

13. 成功指标(KPI)

维度 指标 目标
稳定性 单域名被封后 7 天内 DAU 流失率 < 10%
召回效率 域名失效后 72h 内重新激活率 > 70%
系统健康 SW 缓存命中率 > 95%
系统健康 API 池 fallback 触发率 < 1%
运营效率 域名被封到完成切换平均耗时 < 30 分钟
成本 单用户月均域名/CDN 成本 < $0.05

14. 风险与边界

这套方案能做到的:

这套方案做不到的:

结论:这是一套"延迟战术 + 平滑迁移"的组合拳,目标不是不被封,而是让封禁不影响业务连续性


附录 A:Config 端点 schema

GET https://config.shortpro.workers.dev/v1/config

{
  "version": "20260514-01",
  "api_pool": [
    "https://api-1.xxx",
    "https://api-2.xxx",
    "https://api-3.xxx"
  ],
  "cdn_pool": [
    "https://cdn-1.xxx",
    "https://cdn-2.xxx"
  ],
  "deprecated_origins": [
    "https://oldsite.com"
  ],
  "new_install_url": "https://spro.id",
  "fallback_discovery": [
    "https://t.me/shortpro_id",
    "https://gist.github.com/shortpro/domains"
  ],
  "min_sw_version": "v20260501-01",
  "ttl": 3600
}

附录 B:常用命令参考

# 多 ISP 探测脚本(从印尼云主机跑)
for domain in $(cat domains.txt); do
  for ns in 8.8.8.8 1.1.1.1 202.134.0.155; do  # 含 Telkomsel DNS
    dig +short @$ns $domain
  done
done

# Kominfo TrustPositif 检查
curl -s "https://trustpositif.komdigi.go.id/api/check?url=$DOMAIN"

# Cloudflare Workers config 更新
wrangler kv:key put --binding=CONFIG "v1" --path=./config.json