ShortPro 域名管理与 PWA 分流方案
版本 v1.0
日期 2026-05-14
状态 设计草案,待评审
适用范围 ShortPro 印尼市场 Web/PWA 端
0. 文档目的
为 ShortPro 印尼业务设计一套抗封锁、可持续运营的域名与 PWA 分发架构。在 Kominfo 周期性封域名的现实下,实现:
- 已安装用户:任意单一域名被封不丢失访问能力
- 新增用户:任意时刻都有可用的安装入口
- 运营团队:对域名健康度有完整可观测性,域名封禁从"事故"变成"KPI 可控的运营动作"
1. 设计原则
- 三层域名彻底解耦:发现 / 安装 / 数据各自独立,任一层失效不传染至其他层
- 用户确定性分桶:
hash(device_id) % N把用户均匀分布到多个安装域名,单域被封只影响一个桶(N 分之一用户) - 基础设施多样化:注册商、DNS、IP 段、TLS 证书、CDN、Server 指纹全部分散,避免被批量识别封禁
- 时间换迁移:Service Worker 缓存让被封域名上的 PWA 继续可用数周到数月,用这段时间通过召回机制平滑迁移用户
- 离线优先:app shell 必须 precache,PWA 启动不依赖任何网络请求
- 召回机制 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 |
备份发现路径(防分发域名也被封):
- Telegram Channel bio(
@shortpro_id)永远写最新分发域名 - GitHub Gist 公开 JSON(
gist.github.com/shortpro/domains.json) - Pastebin / Hastebin 备份
- bit.ly / lnk.bio 短链指向当前活跃分发域名
这些托管平台属于 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
关键约束:
- 每个域名跑的是同一份代码,但 manifest 的
start_url和scope各自指向自己 - PWA 一旦"加到主屏幕",桌面图标和此 origin 绑死(浏览器硬规则,无法绕开)
- 这意味着:用户从
dramaku-app.com装的 PWA,桌面图标永远打开dramaku-app.com
3.3 第三层:API / 资源域名(API Pool)
这是整套架构最关键的一层——它决定了"装在桌面上的 App 在安装域名被封后能不能继续用"。
| 子池 | 用途 | 数量 |
|---|---|---|
api-*.xxx |
业务接口(登录、视频元数据、coin 交易等) | 活跃 3-5 个 |
cdn-*.xxx |
视频流、图片、字幕 | 活跃 3-5 个 |
static-*.xxx |
JS/CSS 静态包(可选) | 活跃 2-3 个 |
与安装域名彻底解耦的设计要点:
- PWA 代码里 不写死任何 API URL,基础 URL 从 IndexedDB 读
- 首次启动:从一个 stable config 端点拉最新 API 池清单(Cloudflare Workers + KV,基本不会被封)
- SW 内置 fallback 列表,主接口失败自动切到下一个
- 接口域和视频域分开:接口被封不影响视频播放,反之亦然
- 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 而不是随机分配:
- 出事能精确定位影响范围:桶 3 用户集体反馈 → 知道是哪个域名挂了
- 灰度发布可以按桶推
- 域名被封时,只迁移那个桶的用户,其他人完全无感
- 同一设备多次访问,落到同一个桶(避免反复重装)
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 域名被封
- SW 内置优先级列表,主接口失败自动切下一个
- 启动时拉一次最新 API 清单(从 Cloudflare Workers KV)
- 所有失败异步上报到自有埋点(同样走 API 池,确保自身能上报)
- 失败率超过阈值(比如 5 分钟内 30% 失败)自动从池中移除
7.3 分发域名被封
影响范围最小,只影响新增:
- 监控告警 → 运营切换推广物料里的链接
- WhatsApp 群发新链接给现有用户(虽然他们已装,但可能要分享给朋友)
- Telegram Channel、KOL bio、短链指向同步更新
- 备份发现路径(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 推送细节
- 走 Google 基建,印尼不会封
- 注册 PWA 时主动请求通知权限(印尼用户接受度较高)
- 推送 payload 带 deeplink 到最新分发域名
- 即使原安装域名彻底死,推送通知点击仍可跳转到新域名
8.3 WhatsApp 召回
- 注册流程强制收集 WhatsApp 号(印尼场景非常自然,不会被认为是侵犯隐私)
- 通过 WhatsApp Business API(Twilio / 360dialog / Wati)群发
- 模板示例:
Halo {nama}! 🎬 ShortPro pindah ke alamat baru. Klik untuk lanjut nonton drama favoritmu: 👉 {new_url} - 控制频率,避免被 WA 风控
8.4 离线 fallback 页
每个安装域名的 SW 必须 precache 一个 offline.html,内容包括:
- 当前入口暂不可用的说明
- 备用入口的动态加载(从 Cloudflare Workers JSON)
- WhatsApp 客服联系方式
- Telegram Channel 链接
<!-- 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 采购策略
- 注册商分散到 5+ 家:GoDaddy / Namecheap / Porkbun / Cloudflare Registrar / 印尼本地(Niagahoster / IDwebhost)
- TLD 多样化:.com / .app / .id / .co / .net / .io,别全押一种
- 批量错峰:每批不超过 5 个,间隔 2-4 周,避免同一注册时间被批量识别
- WHOIS:开 privacy,但准备好真实联系信息(出问题能申诉)
- 付费 1 年:别一次买 5 年,被封了就是沉没成本
9.3 CDN / 基础设施分散
| 域名类型 | 推荐 CDN |
|---|---|
| 分发域名 | Cloudflare Pages / Vercel(便宜、静态) |
| 安装域名 | Cloudflare + Bunny.net + Fastly(混搭) |
| API 域名 | 自建 ALB + Cloudflare 中转 |
| 视频 CDN | Bunny Stream + 火山 / 阿里(东南亚节点) |
9.4 轮换节奏
- 每周轮换 1 个:让"老"域名进入冷备休眠 14 天,降低批量老化风险
- 冷备域名定期热身:每月 1 次小流量探活,确保需要时立刻能用
- dead 域名处置:确认彻底封死后,3 个月不续费(避免成本黑洞)
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 监控
- 每天 1 次扫描 https://trustpositif.komdigi.go.id 数据库
- 命中告警 + 自动从 dispatch 池移除
- 内部 wiki 维护"被封域名时间线",积累经验
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 维度统计:
- DAU / 启动次数(突然为 0 → 被封)
- API 失败率(突然飙升 → 域名劫持)
- SW 缓存命中率(异常低 → 缓存策略问题)
11. 实施计划(6 周)
Week 1 — 域名与基础设施
- [ ] 采购首批 20 个域名(分布在 5+ 注册商)
- [ ] DNS 配置,IP/CDN 分散部署
- [ ] Cloudflare Workers config 端点搭建
- [ ] Service Worker 缓存策略原型代码
Week 2 — 三层架构基础
- [ ] 分发域名静态页 + dispatch 接口
- [ ] 安装域名模板部署(一份代码 N 份部署)
- [ ] API 网关 + 多域名路由
- [ ] 数据库表结构(user_install_binding 等)
Week 3 — 分桶与 SW
- [ ] 分桶算法 + 服务端持久化
- [ ] Service Worker 完整实现(precache / 路由 / fallback)
- [ ] API 池动态配置流程
- [ ] 健康检查接口
Week 4 — 召回机制
- [ ] FCM 集成 + 推送模板
- [ ] WhatsApp Business API 接入
- [ ] 离线 fallback 页 + 迁移引导横幅
- [ ] 邮件召回模板
Week 5 — 监控与运营工具
- [ ] 多 ISP 印尼侧探测节点
- [ ] Kominfo TrustPositif 监控脚本
- [ ] 客户端埋点 + dashboard
- [ ] 运营 SOP 文档(域名被封 → 切换 → 召回的标准动作)
Week 6 — 灰度与压测
- [ ] 内测 100 用户,验证三层切换流程
- [ ] 模拟封禁演练(主动下线 1 个安装域名,观察故障切换)
- [ ] 大盘灰度 10% → 50% → 100%
- [ ] 复盘 + 调优
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. 风险与边界
这套方案能做到的:
- 单一域名被封,大多数已安装用户继续可用数周到数月
- 新增用户始终有可用入口
- 域名被封从事故变成可控的运营动作
这套方案做不到的:
- 永久豁免封锁:被封域名上的 PWA 终有一天会失活,只是时间问题
- 100% 召回:再好的机制也会有 10-30% 的长尾用户流失
- 防住国家级深度封锁:如果印尼上 GFW 级别的全栈封锁,任何方案都难
结论:这是一套"延迟战术 + 平滑迁移"的组合拳,目标不是不被封,而是让封禁不影响业务连续性。
附录 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