← 评审入口 一、需求背景 二、总体方案 三、运维 四、技术 五、测试 PRD.md

VTY 品牌域名防护体系 · 产品需求文档(PRD)

字段 内容
项目代号 vty-domain-shield
归属 VTY 体系 · 独立子项目(与前台、中控、操作文档解耦)
文档版本 v3.0
修订日期 2026-09-02
线上评审 index.html · PRD.html

文档目录

章节 读者 内容
一、需求背景 全员 为什么要做、vty.vip 基线、现状痛点、目标
二、PRD 总体方案 产品/架构 架构、四入口流程、安全审视、切流强更
三、运维实施手册 运维 DNS、301、URL 桶、监控、应急
四、技术实施方案 研发 接口、加密、池合并、IP/指纹实现、前端文案
五、测试方案 测试 P0/P1 用例、验收、自动化
附录 PM / 全员 RACI、名词数据字典、修订记录

一、需求背景

1.1 业务背景

VTY 对外有 品牌域名(如 winbet.com),用于广告、投放、口碑、线下物料等 品牌入口。
真正承载产品的是 站点域名池(play-*.com 等可轮换业务域),会因监管、投诉、DNS 污染、CDN 风控等原因失效。

当前架构矛盾:

1.2 现状基线:vty.vip 品牌域(中国部分区域)

数据来源: 现网监测与运营反馈(2026-08~09);口径为「用户点击品牌链 → 最终进入可交互首页」。

1.2.1 当前表现

指标 现网 vty.vip 表现 说明
全链路成功进入率(中国) 约 75% 部分省份/运营商明显偏低
品牌域首跳可打开 约 80~85% 个别地区 DNS 污染、链路中断
进入业务首页可交互 约 75% 含二次跳转失败、站点域 regional 不可用
单省最低 < 60%(部分省/运营商) 移动/联通/电信、跨省差异大
爬虫/扫描命中业务特征 有 品牌或跳转链路透出站点信息,易被举报

1.2.2 75% 失败原因拆解(归因假设,上线后用埋点验证)

原因类型 占比(估) 典型表现 新方案对应能力
区域 DNS 污染 / 解析异常 ~25% 品牌域本身打不开或解析到错误 IP 品牌零内容 + 多 Gate 热备;Site 与 Brand DNS 分离
跳转链路过长 / 单点 Site 不可用 ~30% 301/302 后目标域 regional 封或超时 加密 3 域 + 客户端轮询;探活自动摘除
品牌域被举报后连带 ~15% 品牌域出现业务特征或被反代 Brand 仅 301、零 Body;业务只在 Site
机器扫描抓取域名池 ~10% 爬虫批量跟跳,加速封禁 IP+指纹双检;异常 空页;Resolve 加密
运营商劫持 / 中间盒干扰 ~10% 非预期跳转、证书告警 全链路 HTTPS;Gate/Site 证书独立;HSTS 评审
用户环境 / 弱网 ~10% 超时、WebView 异常 3 域轮询 + Resolve 重试 + 5min 后台探活

1.2.3 现状结论(为什么要换新方案)

  1. vty.vip 作为品牌域长期承载跳转与内容暴露,在中国部分区域已出现 约 1/4 用户无法进入。
  2. 失败 不是单点,而是「区域解析 + 单域跳转 + 举报/扫描」叠加。
  3. 新方案目标不是「再换一个品牌域名字」,而是 Brand 干净、Gate 风控、Site 可轮换、客户端多域容错 的组合拳。

1.3 现状问题(方案层)

问题 影响
品牌域挂内容或反代站点 易被扫描/举报;品牌域高风险
无爬虫/机器识别,Gate 暴露域名 域名池被批量抓取
仅一次跳转、目标写死 站点封禁后入口全挂
H5 / App 双池分离 主站切换不一致;运维/研发重复劳动
分享链接写死站点域 换域后历史分享失效
无法归因品牌进入 投放 ROI 不可量化
职责不清 出事后不知谁改 301、谁切池

1.4 v2 新增诉求(本次定案)

诉求 定案
防爬虫/机器检索 IP 正常 AND 浏览器指纹正常 才下发域名;任一异常 → 空页
域名下发 返回 开启状态最多 3 条 站点域,AES 加密;前端/App 解密 + 轮询探活
池合并 H5 / App 一张域名池表,channels 字段区分端
App 独立链路 不经品牌域;app/resolve 非客户端请求 403
探活切换 服务端连续 3 次失败自动摘除;客户端当前域失败轮询下一域

1.5 目标与非目标

目标(Must)

  1. 品牌域零内容:探测仅见 301 或规范空响应,无业务页/登录/钱包文案。
  2. Gate 风控可落地:双检通过才 Resolve;爬虫见空页。
  3. 站点域可轮换:探活摘除 + 客户端轮询 + 加密下发 3 域。
  4. H5/App 池统一:一套配置、一套探活、一套主站决策。
  5. 分享动态解析:与 Resolve 同一主站数据源。
  6. 品牌来源可统计:hop_id + brand_domain 归因。
  7. 职责清晰:运维/技术/测试各一章,可直接拆工单。

非目标

1.6 成功标准与 成功率目标(KPI)

1.6.1 安全与功能指标

指标 目标
品牌域扫描 0 命中业务特征页
爬虫/异常 IP 100% 空页,响应中无域名明文
故障切换 摘除后新用户 ≤ 1 分钟落到活站
H5/App 主站一致 后台改 primary 后两端一致
分享解析 ≥ 99.9% 与 Resolve 主站一致
品牌归因 hop_id 核销覆盖率 ≥ 99%

1.6.2 用户成功进入率(核心 KPI · 相对 vty.vip 基线)

口径定义(统一,上线前后同口径对比):
成功进入 = 用户发起访问后,在 30s 内 到达 Site 首页且 __health / 首屏关键接口至少 1 次成功(可交互)。
统计维度: 分省、分运营商(移动/联通/电信/广电)、分入口(品牌 H5 / App / 分享)。

指标 现网 vty.vip 基线 新品牌域上线目标(稳定 30 天后) 验收
中国区全链路成功进入率 ~75% ≥ 88% 较基线 +13pp
品牌域首跳成功率(301 到 Gate) ~80~85% ≥ 95% 品牌零内容 + 多 Gate
Gate 通过后进入 Site 成功率 未单拆(估 ~88% of 到达 Gate 用户) ≥ 92% 3 域轮询 + 探活
单省最低成功进入率 部分省 < 60% ≥ 78%(不含西藏等极端样本省可单列) 分省看板
三大运营商业网均值 未单独达标 ≥ 85% 分运营商看板
App 冷启动 resolve 成功率 未与品牌同口径 ≥ 90% 不经品牌域独立链路
因举报/扫描导致的 7 日内品牌域失效次数 有发生 0 次(Brand 无业务内容) 运维巡检 + 扫描

1.6.3 分期达标(上线后)

阶段 时间 成功进入率目标 说明
灰度 上线 0~7 天 ≥ 82% 小流量省份;调 ip/fp 阈值
全量 上线 8~30 天 ≥ 85% 全国放量;Gate/Site 池调优
稳定 上线 31 天+ ≥ 88% 正式 KPI;未达标走专项优化

1.6.4 未达标处置

情况 动作
全链路 < 85%(稳定期) 产品+运维+技术联合排查:DNS/Gate/Site 分省;加备用 Gate 或 Site
单省 < 78% 该省切换备用 Gate 入口;增加 region_tags 匹配 Site
Gate 40301/40302 误杀 >15% 风控下调阈值或分省策略
品牌域被投诉 不 回挂业务;运维保 301,必要时 换新品牌域(Brand 与 Site 仍隔离)

1.6.5 监测实现(技术必须埋点)

事件 字段 用途
brand_request brand, province, isp, ok/fail, latency 首跳
gate_check ip_score, fp_score, pass/fail, reason 双检
resolve_result domain_count, latency, code 下发
domain_probe try_order, domain, ok/fail, ms 轮询
entry_success site, total_ms, hop_id 成功进入(KPI 分子)
entry_fail fail_stage, reason 失败归因(KPI 分母拆解)

二、PRD 总体方案

2.1 一页结论

品牌域(运维 301 零内容)
    → 【落地域 · 中间层】entry.xx.net(Gate 服务:双检 + resolve + probe 编排)
        ├─ 判定失败 → 地址栏仍是落地域,在落地域展示报错(空页/Loading/全屏)
        └─ 判定成功 → location 进入 【站点主域】play-a.com(域名池,可轮换)
            → 运行期故障 → 在主站域内 Toast / FAILOVER(不切回落地域)
App → bootstrap 版本关 → resolve → probe → 同上进入主站域

四段链路:

段 域名角色 负责 做什么
第一段 Brand 品牌域 运维 纯 301 → 落地域;禁止绑 Site 源站
第二段 Landing 落地域(中间层) 技术 双检 + Resolve + 探活编排;失败报错留在此域
第三段 Site 站点主域(池内) 技术+运维 完整业务;成功后才进入
App 段 不经 Brand/落地页 技术 客户端内等效落地逻辑 → 进主站域

2.2 域名分层(五层 · 含落地中间层)

角色 示例 用户地址栏 业务内容 主责
Brand 品牌域 winbet.com 一闪而过 禁止 运维:301
Landing 落地域 entry.vty.net 判定阶段停留 极简 bootstrap + 报错 UI;无投注/钱包 技术(Gate 服务)
Site 站点主域 play-a.com 进站后 完整站(池内可轮换) 技术+运维 CDN
Share 分享域 s-xx.com 短链 302 → 主站域(可不经落地域) 技术
Admin 管理 内网 — 域名池/落地域配置 技术

命名说明: 文档中的 Gate = 部署在 落地域 上的风控+引导 服务;entry.vty.net 为落地域 FQDN 示例(可与 go-gate.xx.net 并存热备,均属 Landing 角色)。

Brand ──301──→ Landing(落地中间层)──判定 OK──→ Site 主站域(池)
  │                 │                              │
  │                 ├─ 判定 FAIL:报错在落地域      ├─ 运行期 FAIL:报错在主站域
  │                 │   (不暴露 play-*.com)       │   (Toast / FAILOVER)
运维301         Gate双检+加密+probe编排          探活+轮询+业务

2.2.1 落地域 vs 主站域:判定归属与报错规则(产品定案)

核心规则:

  1. 落地域 = 中间层判定域:双检、resolve、解密、PROBE_LIST 全程在落地域 hostname 下完成(浏览器地址栏为 entry.vty.net)。
  2. 判定失败 → 当前域(落地域)报错:不跳主站、不在地址栏出现 play-*.com。
  3. 判定成功 → 才 location.replace 进入站点主域:此后报错归属 主站域。
  4. 站点主域 来自域名池 site_domain_pool,与落地域 证书/DNS/桶完全分离。
阶段 地址栏域名 判定内容 失败:报错在哪 成功:下一步
品牌跳转 winbet.com → entry.vty.net 301 品牌打不开(运维) 进入落地域
落地·风控 entry.vty.net IP + fp 落地域 空页 E-GATE-01 继续 bootstrap
落地·解析 entry.vty.net resolve + 解密 落地域 E-LOAD / E-DECRYPT / E-RESOLVE 得 domain_list
落地·探活 entry.vty.net HEAD 主站候选域 落地域 E-PROBE Toast / E-RESOLVE 全屏 跳转主站域
主站·运行 play-a.com API / health 主站域 E-NET Toast / 业务错误 正常业务
                    ┌──────────────────────────────────────┐
                    │  落地域 entry.vty.net(中间层)       │
                    │  ┌────────────────────────────────┐  │
  Brand 301 ───────→│  │ IP/fp 双检                      │  │
                    │  │ resolve → 解密 → probe 编排      │  │
                    │  │ FAIL → 本域报错(见 §4.10.5)   │  │
                    │  └───────────────┬────────────────┘  │
                    └──────────────────│ PASS ─────────────┘
                                       ▼
                    ┌──────────────────────────────────────┐
                    │  站点主域 play-a.com(池内 · 可轮换)  │
                    │  首页 / 登录 / 投注 · FAILOVER 在此域  │
                    └──────────────────────────────────────┘

与分享/直访差异:

入口 是否经落地域 说明
品牌 H5 必须 Brand 301 → 落地域 → 判定 → 主站
分享 可选(默认 跳过) Share 302 直达主站域;失败在主站域 FAILOVER
主站直访 否 已在主站域;失败在主站或跳 Brand
App 否(客户端内等效) SDK 内完成「落地逻辑」,UI 在 App 内,不进 landing URL

2.2.2 落地域配置(运维 + 技术)

项 规格
数量 ≥2 热备 FQDN(如 entry-a.vty.net、entry-b.vty.net)
Brand 301 目标 仅指向 落地域,禁止 301 到 play-*.com
桶 bucket-landing-* 独立桶;仅 bootstrap + Gate API
证书 与 Brand / Site 分离 SAN
页面限制 无 /login /wallet 等业务路由;仅 /r/{brand} + /api/v1/gate/*
池关系 落地域 不在 site_domain_pool;主站域 仅在 resolve 加密包内下发

核心原则

  1. 品牌域 永不 托管业务内容。
  2. Site 可牺牲轮换;Brand 尽量不动。
  3. 分享永远解析 当前 Site。
  4. hop_id 第一跳签发、落地核销,防刷量。

2.3 全入口端到端流程(品牌 / 分享 / 主站直访 / App)

本章定位: 四条入口共用 域名池(主站域)+ 落地域判定 + FAILOVER;品牌 H5 必须经 落地中间层。实现见 §4.3~§4.6。

2.3.0 四条入口总览

入口 典型 URL entry_type 落地中间层 判定失败报错域 成功进入
品牌 H5 winbet.com brand ✓ entry.xx.net 落地域 主站域 play-*.com
分享短链 s-xx.com/{code} share ✗(默认) 主站域 / Share 503 主站域
主站直访 play-a.com direct ✗ 主站域 主站域
App 冷启动 app App 内等效 App 内 / 40310 主站域
Brand ─301─→ Landing 落地域(判定层 · 失败留本域报错)
                    │
                    │ PASS:location.replace
                    ▼
              Site 主站域 play-a/b/c(池 · 运行期故障在本域处理)

sequenceDiagram
  participant U as 用户浏览器
  participant B as Brand<br/>winbet.com
  participant L as 落地域<br/>entry.vty.net
  participant R as Resolve API
  participant P as 主站域名池
  participant S as 站点主域<br/>play-a.com

  U->>B: ① 点击品牌 URL
  B->>L: ② 301(零 Body)
  Note over U,L: 地址栏 = entry.vty.net(中间层)
  L->>L: ③ IP 检测
  L->>U: ④ bootstrap
  U->>U: ⑤ 采集 fp
  U->>L: ⑥ POST gate/resolve
  alt 判定失败(IP/fp/resolve/probe)
    L-->>U: 留在落地域报错(空页/Loading/全屏)
    Note over U: 地址栏仍为 entry.vty.net
  else 判定成功
    L->>P: ⑧ 查 healthy TOP3
    L-->>U: 加密 payload
    U->>U: 解密 + PROBE_LIST(仍在落地域 orchestrate)
    U->>S: ⑪ location.replace → play-a.com
    Note over U,S: 地址栏变为主站域 · 此后报错在主站
    alt 主站运行期故障
      U->>U: FAILOVER(Toast · 不切回落地域)
    end
  end
阶段 地址栏 判断内容 失败 → 成功 →
①② Brand→Landing 301 品牌打不开 进入 落地域
③~⑦ Landing IP + fp + 限流 落地域 报错 E-GATE-01 继续 resolve
⑧~⑩ Landing resolve + 解密 落地域 E-LOAD / E-DECRYPT / E-RESOLVE domain_list
⑪ probe Landing(编排) HEAD 主站候选 落地域 E-PROBE / E-RESOLVE 跳转主站域
运行期 Site 主域 API / health 主站域 Toast / 全屏 正常业务

2.3.2 品牌 H5:阶段一(Brand → 落地域)

触发: 用户点击 https://winbet.com/。

步骤 执行方 动作 分支
S1~S2 Brand CDN DNS / TLS 失败 → 品牌域不可用
S3 Brand CDN 301 → https://entry.vty.net/r/winbet?... 禁止 301 到 play-*.com
S4 浏览器 地址栏变为 落地域 超时 → fail_stage=landing_unreachable

原则: Brand 零 Body;301 Location 只能 是落地域 FQDN。


2.3.3 品牌 H5:阶段二(落地域 · 风控双检)

发生域: 地址栏 = entry.vty.net(落地中间层)。
Gate 服务收到 GET /r/{brandId} 后返回 bootstrap;判定失败不跳主站,在落地域处理。

判定失败 vs 成功的域归属

结果 地址栏 用户看到 是否出现 play-*.com
IP/fp 失败 落地域 空页 E-GATE-01 否
resolve/probe 失败 落地域 Loading / 全屏 E-RESOLVE-01 否
成功 主站域 首页/登录(完整业务) 是

判断顺序(短路,任一 FAIL 即停)

1. brand_id 有效?          → 否:404
2. IP 硬拒绝 IP-H1~H5?     → 是:空页 40301
3. IP 白名单 IP-W1?        → 是:跳过 IP 打分(仅限流)
4. IP 评分 ip_score         → <60:空页 40301
5. 频率限流                  → 超限:429 或空页
6. (resolve 时)fp 硬拒绝   → 是:空页 40302
7. fp 评分 fp_score          → <60:空页 40302
8. 全部通过                  → 执行 Resolve 查池

Gate 全分支表

# 触发条件 判断时机 HTTP/响应 用户 UI 是否下发域名 埋点 reason
G1 正常用户 resolve 时 200 code=0 E-LOAD-01 → 进站 是 pass
G2 IP 黑名单/扫描器/Tor 首包 GET Gate 200 空 HTML 空页 E-GATE-01 否 ip_blacklist 等
G3 ip_score < 60 GET 或 resolve 200 空 / 40301 空页 否 ip_score_low
G4 fp webdriver/headless resolve 40302 空页 否 FP-H*
G5 fp_score < 60 resolve 40302 空页 否 fp_score_low
G6 fp 采集超时 >3s resolve 未带 fp 40302 空页 否 fp_timeout
G7 同 IP >30 次/min resolve 429 E-RATE-01 或空页 否 rate_limit
G8 搜索引擎 Bot(可选) GET Gate 空页 空页 否 bot_ip
G9 brand 已禁用 resolve 404 空页 否 brand_disabled

关键: G2~G8 绝不调用 Resolve;响应中 无 domains 明文、无跳转 Site。


2.3.4 品牌 H5:阶段三(域名池 · Resolve · 解密)

A. 服务端:域名从哪来

Resolve 不接受客户端指定域名;只从统一池 site_domain_pool 读取:

SELECT domain FROM site_domain_pool
WHERE status = 'active'
  AND health = 'ok'
  AND fail_streak < 3
  AND (channels @> ARRAY['all'] OR channels @> ARRAY['h5'])
ORDER BY is_primary DESC, weight DESC
LIMIT 3;
池状态 healthy 数量 Resolve 行为 返回
正常 ≥3 取 weight 最高 3 条 加密 3 域
降级 1~2 取现有 加密 1~2 域
枯竭 0 不加密 503 code=50301 → E-RESOLVE-01

池维护(与 Resolve 联动):

B. 客户端:如何请求 Resolve

Gate bootstrap 加载后,SDK 执行:

1. gate-fp-collect.js 采集 → fp_hash(≤3s,超时则 resolve 仍发但 fp 空 → 40302)
2. 生成 nonce(uuid)、ts、sig = HMAC(brand_id|ts|nonce|fp_hash)
3. POST /api/v1/gate/resolve  Body: { brand_id, channel:"h5", fp_hash, ts, nonce, sig }
4. 超时 5s;网络错误 → RETRY_RESOLVE(最多 3 轮,间隔 2s)

C. 客户端:如何解密

Response 示例:

{
  "code": 0,
  "data": {
    "key_id": "k2026-03",
    "expires_at": 1735689900,
    "ttl_sec": 300,
    "payload": "<BASE64(iv+ciphertext+tag)>"
  }
}

解密步骤(H5 / App 一致):

1. 用 key_id 从本地密钥表取 AES-256 密钥 K(H5 bootstrap 内嵌混淆;App 内置可热更新)
2. BASE64 解码 payload → 拆 iv(12B) + ciphertext + tag(16B)
3. AES-256-GCM 解密(K, iv, ciphertext, tag) → UTF-8 JSON 明文
4. 校验 JSON:v=1,domains 为非空数组,hop_id 存在,expires_at 未过期
5. domain_list = domains(顺序即 probe 顺序);写入 resolve_meta 到 sessionStorage

明文结构(解密后):

{
  "v": 1,
  "domains": ["play-a.com", "play-b.com", "play-c.com"],
  "primary": "play-a.com",
  "hop_id": "hop_8f3k2j",
  "brand_id": "winbet"
}
解密分支 条件 处理 用户 UI
D1 解密成功 进入 PROBE_LIST E-LOAD-01
D2 key_id 未知 重拉 resolve 1 次 E-LOAD-01
D3 GCM tag 校验失败 / JSON 非法 停止,不上线 E-DECRYPT-01
D4 expires_at 已过期 丢弃缓存,重新 resolve E-LOAD-01
D5 domains 为空数组 RETRY_RESOLVE E-LOAD-01 → E-RESOLVE-01

2.3.5 品牌 H5:阶段四(探活 · 跳转主站域)

关键: PROBE_LIST 由落地域 bootstrap 发起,但 仅在 probe 成功 后执行 location.replace;此前地址栏 始终是落地域。

# 地址栏仍为 entry.vty.net
for domain in domain_list:
  probe(domain)   # HEAD https://play-x.com/__health
  if ok:
    location.replace("https://{domain}/?hop=...")  # 地址栏 → 主站域
    return
# 全失败:留在落地域,展示 E-RESOLVE-01(不暴露主站 URL 给用户点击)
Probe 分支 条件 地址栏 下一步 用户 UI(报错域)
P1 d1 成功 落地→主站 进 d1 E-LOAD-01 消失
P2 d1 失败 d2 成功 落地→主站 进 d2 落地域 Toast E-PROBE-01
P4 3 域全失败 落地域 RETRY_RESOLVE 落地域 E-LOAD-01
P5 3 轮仍失败 落地域 停止 落地域 E-RESOLVE-01

进站后: 业务 API 基址 = https://{current_domain};hop_id 随首屏请求上报核销。


2.3.6 共用:运行期故障(主站域内 · 不回退落地域)

适用: 用户地址栏 已是 play-*.com(主站域)。FAILOVER 在主站域之间切换,不跳回落地域(避免二次双检损耗)。

故障检测(任一触发 FAILOVER)

信号 阈值 说明
业务 API 失败 连续 3 次(5xx/网络错误/超时>5s) 主路径
/__health 失败 连续 2 次(间隔 30s) 辅助
WebView onReceivedError 1 次 App
用户切回前台 立即 probe 1 次 App/H5

FAILOVER 流程

RUNNING 状态,current_domain = play-a.com

1. 标记 play-a.com 为 local_bad(本地 60s 内不再优先)
2. 从 domain_list 取下一未标记域 play-b.com
3. HEAD play-b.com/__health(3s)
   ├─ 成功 → 切换 API baseUrl 到 play-b
   │         Toast E-NET-01「网络异常,已为您切换至备用线路」
   │         上报 domain_switch(from=play-a, to=play-b)
   │         用户 **无刷新** 继续操作(SPA 重绑 baseUrl)
   └─ 失败 → 试 play-c.com
4. 列表内无可用域 → RETRY_RESOLVE(2 轮)
   ├─ 拿到新 domain_list → 再 PROBE_LIST
   └─ 仍失败 → 全屏 E-RESOLVE-01

运行期全分支

# 场景 服务端池 客户端动作 用户感知
R1 仅 play-a regional 慢 health 仍 ok PROBE 时可能跳过 a 进 b 进站时 Toast
R2 play-a 宕机,b/c 正常 90s 后 a disabled FAILOVER → b Toast,不中断会话
R3 用户正在 a,a 被探活摘除 新 resolve 无 a 当前会话 FAILOVER;b/c 可用则切 Toast
R4 3 域全挂 503 RETRY → 全屏 「暂时无法连接」
R5 弱网瞬断 — API 重试 1 次,未达 3 次不切换 无感
R6 后台 5min 定时 probe — 当前域失败 → 静默 FAILOVER 多数无感
R7 resolve_meta 将过期(<60s) — 后台静默 refresh resolve 无感

Brand 域不受影响: Site 全挂时 不改 Brand 301;用户刷新品牌链重新走 Gate 流程。


2.3.7 端到端分支 master 表(四入口速查)

入口 阶段 失败点 用户结果 恢复手段
品牌 Brand DNS/301 异常 打不开品牌链 运维换 Brand
品牌 Gate IP/fp 爬虫/机房 空页 无(故意)
品牌 Resolve/Probe 池空/全域挂 全屏 E-RESOLVE-01 启用 standby
分享 Share 解析 code 无效 404 短链页 重新生成分享
分享 Share 解析 primary 全挂 Share 维护页 技术切 primary
分享 302 落地 Site 该域 regional 慢 进站后 FAILOVER Toast 切域
直访 当前域探活 play-a 挂、无缓存 引导官方入口 跳 Brand 或 Gate
直访 有缓存 a 挂 b/c 可用 自动切 b/c FAILOVER
App bootstrap 版本 < min 40310 全屏强更 商店更新
App 切流日 旧包打开 E-APP-FORCE-01 不可跳过
共用 运行期 current 域挂 Toast 切域 §2.3.6
共用 运行期 列表全失效 全屏 「重新连接」

2.3.8 分享链路(点击分享短链)

触发: 用户在 IM/社媒点击 https://s-xx.com/{code}(或带路径 /p/match/123)。

sequenceDiagram
  participant U as 用户浏览器
  participant Sh as Share 域<br/>s-xx.com
  participant P as 域名池 DB
  participant S as Site<br/>play-*.com

  U->>Sh: GET /{code}
  Sh->>Sh: ① IP 限流 + 可选 Bot 识别
  Sh->>Sh: ② 校验 code 有效 / 未过期
  alt code 无效
    Sh-->>U: 404 短链不存在
  else code 有效
    Sh->>P: ③ 查 is_primary + healthy 备份序
    alt 无 healthy 域
      Sh-->>U: 503 Share 维护页
    else 有可用域
      Sh->>Sh: ④ 选 target:primary 不通则次优 healthy
      Sh-->>U: 302 Location=Site 落地页
      U->>S: ⑤ 加载 Site SPA + SDK
      S->>S: entry_type=share;写 share_code/hop_id
      Note over U,S: 无 Gate 双检;运行期 FAILOVER 同 §2.3.6
    end
  end

逐步流程

步骤 执行方 动作 分支
SH1 Share CDN DNS/TLS,GET /{code} 域名挂 → 运维切 Share 备域
SH2 Share 服务 IP 限流(如 60 次/min/IP);已知 Bot → 429 或空页 超限 → 429
SH3 Share 服务 查短码表:code → { path, inviter_id, expires_at } 无效/过期 → 404
SH4 Share 服务 查池:is_primary 且 health=ok;若 primary probe 失败则取 weight 次高 healthy 域 池空 → 503 Share 维护页
SH5 Share 服务 302 https://{target}{path}?entry=share&share_code={code}&hop_id={hop} —
SH6 Site SDK 解析 URL 参数;entry_type=share;上报 entry_success 当前域不可用 → §2.3.6 FAILOVER

分享 vs 品牌:关键差异

项 品牌 H5 分享
是否经 Gate ✓ ✗
是否 IP+fp 双检 ✓ 严格 Share 仅 轻量限流
如何知 Site 域 Resolve 加密 3 域 服务端 302 单域(primary)
客户端 domain_list 解密得 3 域 首次仅知当前 302 域;若本地有历史 resolve_meta 则复用
换 primary 后 下次 resolve 更新 下次点击分享 即 302 到新 primary
埋点 entry_type=brand + hop entry_type=share + share_code

分享分支表

# 条件 HTTP 用户看到 处理
S1 正常 302 → Site 直达落地页(赛事/活动等) —
S2 code 不存在 404 「链接无效」 重新分享
S3 code 过期 410 「链接已过期」 重新分享
S4 池枯竭 503 Share 维护页 运维启 standby
S5 primary Regional 不通 302 到 backup 域 正常进站(用户无感) Share 服务端选次优域
S6 302 落地后 Site 挂 — Toast FAILOVER 有缓存则切 b/c;无缓存 → E-RESOLVE-01 +「官方入口」

分享接口(与 Resolve 同源)

GET /api/v1/resolve/current-site
Authorization: Bearer {admin_or_share_service}
→ { "primary": "play-a.com", "healthy": ["play-a.com","play-b.com"], "version": 12 }

用户短链边缘逻辑等价于:读 current-site → 302 到 primary + 原始 path。


2.3.9 主站直访(点击/输入 Site 域名)

触发: 用户 收藏夹、短信老链接、手动输入 打开 https://play-a.com/(或 /login),不经过 Brand / Gate / Share。

sequenceDiagram
  participant U as 用户浏览器
  participant S as Site<br/>play-a.com
  participant G as Gate<br/>(仅兜底)
  participant B as Brand<br/>(官方入口)

  U->>S: GET https://play-a.com/
  S->>U: 加载 SPA + domain-shield SDK
  U->>S: HEAD /__health(3s)
  alt 当前域可用
    S->>S: entry_type=direct → RUNNING
  else 当前域不可用
    alt localStorage 有 resolve_meta
      U->>U: PROBE_LIST 缓存域 → FAILOVER
    else 无缓存
      U->>G: 可选:跳 Gate bootstrap
      U->>B: 或:跳 Brand 官方入口
      Note over U: 全屏「请从官方入口进入」
    end
  end

逐步流程

步骤 执行方 动作 分支
D1 用户 打开 https://play-a.com/... DNS 失败 → 浏览器报错
D2 Site 返回业务 SPA;内嵌 domain-shield SDK(与 Gate bootstrap 同库) —
D3 SDK current_domain = location.hostname;entry_type = direct —
D4 SDK HEAD https://play-a.com/__health(3s) 见下表
D5 SDK 上报 entry_success(direct)或进入 FAILOVER —

直访分支表

# 条件 SDK 动作 用户 UI 说明
D-OK health 200 RUNNING,正常业务 无额外提示 最常见:收藏仍可用
D-F1 health 失败 + 有 resolve_meta PROBE 缓存 domain_list → 切 b/c Toast E-NET-01 曾从品牌进过,有 3 域缓存
D-F2 health 失败 + 无缓存 location → 配置的 official_brand_url(如 winbet.com) 全屏或中间页:「请从官方入口访问」 禁止在 Site 页暴露其他 Site 域明文列表
D-F3 health 失败 + 无缓存 + 无 Brand 配置 location → Gate bootstrap ?from=direct E-RESOLVE-01 Gate 走完整双检+resolve
D-F4 运行期中 play-a 挂 同 §2.3.6 FAILOVER Toast 与品牌进站后相同

产品定案(直访)

  1. 允许直访:不强制每次跳 Brand(否则收藏失效,体验差)。
  2. 不直访下发域名池:无 Gate 双检时 不得 在 Site 页调用 gate/resolve 展示备选域列表(防爬虫撞库)。
  3. 兜底顺序:本地缓存 FAILOVER → 跳 Brand 官方入口 → 跳 Gate → 全屏 E-RESOLVE-01。
  4. 配置项:official_brand_url、gate_fallback_url 由配置中心下发,写入 Site SDK。
// Site SDK 直访 INIT 伪代码
async function initDirect() {
  const host = location.hostname;
  if (await probe(host)) {
    setEntryType('direct');
    return enterRunning(host);
  }
  const cached = loadResolveMeta();
  if (cached?.domains?.length) {
    return probeListAndFailover(cached.domains); // D-F1
  }
  const brand = config.official_brand_url;
  if (brand) return location.replace(brand);      // D-F2
  return location.replace(config.gate_fallback_url + '?from=direct'); // D-F3
}

2.3.10 App 链路(不经品牌域 · 含版本校验)

触发: App 冷启动、网络恢复、后台切前台、定时 5min、FAILOVER 需新列表。

与 H5 差异: App 必须先过版本关,再 resolve;旧版在切流日 强制更新,否则无法拿域名。

sequenceDiagram
  participant A as App
  participant C as Config/bootstrap API
  participant K as Keychain
  participant API as app/resolve
  participant P as 域名池
  participant S as Site

  A->>C: ① GET /app/bootstrap(X-App-Version)
  alt version < min_version 或 force_update
    C-->>A: force_update + store_url
    A->>A: 全屏强制更新(不可关闭)
  else 版本 OK
    A->>K: ② 读 current_domain + resolve_meta
    A->>S: ③ probe current(3s)
    alt probe OK 且 meta 未过期
      A->>A: 跳过 resolve,RUNNING
    else
      A->>API: ④ POST app/resolve(HMAC)
      alt 40310 force_update
        API-->>A: 强制更新
      else 403 无签名
        API-->>A: 拒绝
      else OK
        API->>P: 查池 TOP3(channel=app)
        API-->>A: 加密 payload
        A->>A: 解密 → PROBE_LIST → 写 Keychain
      end
    end
    A->>S: ⑤ 业务 API(X-App-Sign)
  end
步骤 动作 分支
A0 GET /api/v1/app/bootstrap(最先,早于 resolve) 见 App 版本分支表
A1 读 Keychain current_domain 空 → 直接 resolve
A2 probe 当前域 + 检查 expires_at OK 且未过期 → 跳过 resolve
A3 POST /api/v1/app/resolve + 签名头 40310 → 强制更新;403 → 拒绝
A4 解密 → PROBE_LIST → 写 Keychain 同 §2.3.5
A5 业务请求 X-App-Sign;加载 brand_pack 皮肤资源 Site 网关校验

App 版本分支表

# 条件 服务端 客户端 UI 能否 resolve
V-OK semver(App) >= min_version bootstrap code=0 正常启动 ✓
V-F1 semver(App) < min_version bootstrap 或 resolve 40310 全屏强制更新 E-APP-FORCE-01 ✗
V-F2 force_update=true(切流开关)且 App 非最新 bundle bootstrap 40310 全屏更新 ✗
V-F3 brand_pack_version 本地过旧 bootstrap 带 required_brand_pack 强制更新或热更包(v1 仅强制整包) ✗
V-S1 可选升级:latest > current >= min bootstrap 带 optional_update 可关闭弹窗 ✓

App 与 H5 能力对照(版本相关)

能力 H5 App
品牌域切换 Brand 301 即时生效,无版本概念 旧包可能 写死旧 Brand/Gate → 必须强更
皮肤改版 Gate bootstrap / Site 静态资源即时换 主题资源在 App 包内 或需新 native → 强更
resolve 密钥轮换 bootstrap 热更新 需 App 内置 key 表 → 低版本 40310
切流策略 运维改 DNS 即可 服务端 min_version + force_update 双保险

2.3.11 四入口对照总表

维度 品牌 H5 分享 主站直访 App
典型场景 广告/物料 好友分享比赛/活动 收藏/老短信链接 打开 App
entry_type brand share direct app
hop_id Gate resolve 签发 Share 302 附带 无(或直访不带) resolve 签发
风控 IP+fp 双检 Share 限流 无(靠 Site 自身) App HMAC
拿 domain_list 加密 resolve 302 单域;缓存可 FAILOVER 仅本地缓存 app/resolve
探活 PROBE 3 域 落地后可选;FAILOVER 用缓存 先 probe 自身 同 H5
运行期切换 §2.3.6 §2.3.6 §2.3.6 §2.3.6
池数据源 site_domain_pool 同左 is_primary 同左(仅缓存) 同左 channel=app
失败兜底 E-RESOLVE-01 Share 503 / 官方入口 跳 Brand 维护页 / 强制更新
版本关 无 无 无 bootstrap + 40310 强更

2.7 大版本上线切流:品牌域调整 + 品牌皮肤改版 + App 强制更新(一次性)

产品定案: 新品牌域、新 Gate、新 品牌皮肤(UI 主题)、新 App 域名防护能力 同一窗口上线;旧 App 一律强制更新,不允许静默降级访问。

2.7.1 为什么要绑在一起

变更项 旧 App 不更新的后果
品牌域 vty.vip → winbet.com 旧包内嵌/配置旧 Brand、旧分享文案、旧 DeepLink
Gate / resolve 协议 旧签名算法或旧 key_id 解密失败
品牌皮肤改版 旧 UI 资产与新区块不匹配;运营物料与 App 视觉不一致
域名池合并 旧 App 池与 H5 池不一致,切主站后两端分裂

结论: 切流日 = 运维切 Brand + H5 Gate 上新 + 服务端抬高 min_version + App Store 仅保留新包。

2.7.2 上线包内容(Cutover Bundle 2026-brand-v2)

组件 版本/标识 负责 切流日动作
Brand DNS winbet.com → Gate 运维 301 生效;旧 Brand 保留 301 或退役
Gate bootstrap gate_bs_v2 前端 部署新 fp 采集 + 新 key_id
品牌皮肤 brand_pack_v2(主题色/Logo/启动图) 客户端+设计 仅 App ≥4.0.0 包内携带
App iOS/Android ≥ 4.0.0(示例) 客户端 商店上架;旧版下架或强更
服务端 min_version=4.0.0,force_update=true 后端 配置中心一键开关
H5 Site 随 Site 部署 前端 与新皮肤 Web 版一致
域名池 合并池 primary 指向新主站 技术 Admin 确认 healthy ≥3

2.7.3 切流时间线(T 日 = 正式上线)

时间 动作 验证
T-14 App 4.0.0 提审;内测 min_version=3.x 不变 TestFlight / 内测包过 P0
T-7 商店 软发布(可选下载);物料仍用旧 Brand 新包 bootstrap/resolve 连通预发 Gate
T-1 运维预配 winbet.com 301(灰度 5% 流量或 hosts 验证) curl 301 + Gate 双检
T 00:00 ① Brand 全量 301 → 新 Gate ② min_version=4.0.0 ③ force_update=true ④ brand_pack_version=2026-brand-v2 监控 entry_success
T+0~2h 旧 App 打开 → 全屏强更;新 App 正常 resolve 强更率、40310 占比
T+24h 评估 KPI;未达标不回滚 Brand 业务,只调 Gate/Site 池 分省看板
T+7 force_update 可改 false(min_version 保持 4.0.0) 仅 block <4.0.0

2.7.4 切流日各端行为

【旧 App <4.0.0 用户】
  打开 App → bootstrap → 40310 / force_update
  → 全屏「版本过低,请更新」(不可关闭、不可进首页)
  → 跳转 App Store / APK 官网

【新 App ≥4.0.0 用户】
  bootstrap OK → app/resolve → 新池 3 域 → probe → 新皮肤首页

【H5 用户】
  点击 winbet.com(新 Brand 物料)→ 301 Gate → 双检 → resolve
  (无强更;浏览器始终最新 bootstrap)

【分享 / 直访】
  Share 302 新 primary;直访逻辑不变(§2.3.8~2.3.9)

2.7.5 回滚原则

情况 是否回滚 Brand 301 是否降低 min_version
Gate 故障 可切 备用 Gate Location 否
Site 池空 启 standby 域 否
新 App 崩溃率 >5% 暂停商店推广;hotfix 4.0.1 否(除非灾难且新包不可用)
误发强更 — 仅 force_update=false 临时放开;min_version 不建议降

禁止: 切流后为救急 降低 min_version 让旧 App 带旧皮肤 + 旧 Brand 访问新 Gate(会造成双轨运营与举报风险)。

2.7.6 职责 RACI(切流专项)

事项 产品 运维 客户端 后端 测试
min_version / force_update 配置 A I C R C
Brand 301 切换 C A/R I I C
App 商店上架/强更文案 A I R I C
brand_pack 皮肤验收 A I R I R
切流当晚值班 C R R R R

2.4 关键场景

场景 行为
切流日 旧 App < min_version bootstrap/resolve 40310 → 全屏强制更新,不可进业务
切流日 新 App ≥ min_version bootstrap OK → resolve → brand_pack_v2 皮肤 + 新域名池
正常用户经 品牌域 进入 301 → Gate 双检 → 3 域加密 → 轮询进站
用户点 分享短链 Share 302 到当前 primary → Site 落地;primary 挂则 Share 选 backup
用户 直访 play-a.com probe 自身;OK 则 direct 进站;失败则用缓存 FAILOVER 或跳 Brand
爬虫/机房 IP 走品牌 Gate 空页,Resolve 不调用
Site-A 探活失败 3 次 自动 disabled;Resolve/Share 不再选 A
用户正在 A,A 突然挂 客户端 FAILOVER B/C;必要时重拉 resolve
后台切换主站 H5/App/Share 下次请求一致到新 primary
App 用 curl 调 resolve 403

2.5 运维 × 技术协作

接口点 运维 技术 约定
Brand→Gate 301 Location Gate path + sig 算法 Location 含 brandId
Gate 域名 DNS/CDN/证书 部署 bootstrap + API 异常空页
Site 桶 每域独立 CDN 桶 + /__health 探活 Worker 读 health 90s 内 3 败摘除
切主站 无需改 Brand Admin 改 primary 技术主责
告警 品牌非 301、证书 池可用数、探活失败 统一值班群

2.6 风险场景与应对(举报 · 机器扫描 · 区域不可用)

原则: 品牌域 永不承载可举报内容;机器/爬虫 看不到域名池;Site 被封 不影响 Brand;用户侧 多域容错 拉升成功进入率。

2.6.1 举报类场景

场景 现网 vty.vip 风险 新方案应对 负责
用户/竞品向渠道举报「赌博/违规页面」 若 Brand 反代 Site,举报材料含 品牌 URL + 业务截图 → 品牌域连带封禁 Brand 零 Body、零业务;举报页只能拍到 301 或空 Gate 运维+产品
监管扫描 URL 库 品牌域在库中且可访问业务 Brand 响应 无登录/钱包/赛事关键词;业务特征仅在 Site 运维巡检
证书透明度 / 关联域名连带 品牌与多个 Site 混 SAN Brand / Gate / Site 证书分离,SAN 不混配 运维
支付/应用商店审核抓链 审核爬虫跟跳看到业务 Gate 双检:审核机房里 IP 空页;真实用户正常 Resolve 技术+风控
投诉后 ISP 局部阻断 单省 Brand 解析失败 多 Brand 热备(物料分省);Gate 多入口 301 目标可切 运维
Site 被举报封禁 旧方案品牌入口同步失效 Brand 不改;技术 disabled Site + 轮询 其余 2 域;Resolve 不再下发死域 技术
Gate 被举报 — Brand 301 切备用 Gate;Gate 页 无业务文案 运维+技术

举报应急 SOP(产品定案):

  1. 收到「品牌域打不开/被墙」→ 运维先验:curl -sI Brand 是否仍 301、Body 是否为空。
  2. 若 Brand 本身污染 → 换新 Brand 指向同一 Gate(Site 不动)。
  3. 若仅 Site 被封 → 技术池内摘除,不动 Brand。
  4. 禁止「把业务面临时挂回 Brand 救急」—— 会重复 vty.vip 75% 问题并加速举报。

2.6.2 机器扫描 / 爬虫类场景

场景 攻击/探测方式 新方案应对 负责
搜索引擎爬虫 Baidusp/Googlebot 抓取 Brand/Gate Brand 301 无内容;Gate 对 已知 Bot IP 可配置 空页或仅 301(不调用 Resolve) 运维 WAF + 技术
安全扫描器 Nessus/AWVS 扫路径 /login /api Brand 全路径 301;Gate 无业务路径;异常 IP 空页 运维
域名池撞库 脚本跟 302 批量收集 Site 域 Resolve 加密;异常 IP/Headless 不下发;响应 无域名列表 JSON 技术
Headless 批量访问 Puppeteer/Selenium 模拟 fp 检测 webdriver/HeadlessChrome → 空页 技术
机房 IP 批量 Resolve 云主机刷 /gate/resolve IP 信誉库 + 30 次/min 限流 + nonce 去重 技术
中间人抓包 试图读明文域名 payload AES-256-GCM;HTTPS 全链路;App 证书 pinning(可选) 技术+客户端
Referer/链路分析 从 Brand 跟跳分析关联 Brand→Gate 301 不带 Site 域;Site 域仅加密包内出现 架构
Share 短链被扫 批量请求 Share Share 限流;Resolve 鉴权;与 Entry 同源决策 技术

2.6.3 区域不可用 / 弱网(拉升 75%→88% 的关键)

场景 表现 应对 预期提升
单 Site 区域不可达 用户到 Gate 后卡死 3 域轮询 + 探活摘除死域 +8~12pp
弱网超时 首域 3s 超时 自动试 d2/d3;Resolve 重试 3 次 +3~5pp
省 DNS 污染 Brand 品牌打不开 分省 备用 Brand + 多 Gate CNAME +5~8pp
Gate 边缘 5xx 双检页加载失败 多 Gate 热备;CDN 健康检查 +2pp

2.6.4 应对能力总览(评审一张表)

能力 解决什么问题 对应章节
Brand 零内容 301 举报材料无业务、扫描无特征 §3.3
Gate IP+指纹双检 机器不拿到域名池 §4.3
Resolve 加密 3 域 撞库/嗅探难度高 §4.4~4.5
客户端轮询 单域 regional 失败 §4.6
探活自动摘除 死域不下发、快速切 §4.6
H5/App 池合并 切换一致、减少误配 §4.2
App 不经 Brand + 403 App 域不被浏览器刷 §2.3.10、§4.4.3
分省 Gate/Brand 热备 局部 DNS/阻断 §3.5
entry_success 埋点 成功率 KPI 可验证 §1.6.5

2.8 安全审视:杜绝品牌域 / 主站域被标记危险或不可访问

本章定位: 从 安全 / 风控 / 合规 角度审视方案 漏洞与遗漏;最终目标:品牌域长期干净可访问,主站域 可轮换、可牺牲,不因连带导致 Brand 被标红或全网封禁。
评审入口 Tab: index.html →「安全」分册。

2.8.1 安全目标(North Star)

对象 目标 可接受代价 不可接受
品牌域 Brand 7 日内 0 次因内容/举报导致失效;浏览器 无「危险网站」警告 换备用 Brand 物料 Brand 挂业务页、反代 Site、SAN 混配
落地域 Landing 无业务特征;被扫仅见 bootstrap;可更换 FQDN 301 改指向新 Landing Landing 出现 /login、钱包、赛事列表
主站域 Site 单域可被标红/ regional 封;≤90s 摘除;Brand 不受影响 牺牲单域、换池内域 为救急把 Site 挂回 Brand
用户感知 正常用户 ≥88% 进入(KPI);异常 IP 看不到 域名池 爬虫空页 明文下发 play-*.com 列表
         【必须保护】              【可轮换牺牲】
    Brand + Landing          Site 主站域(池内 N 个)
    零内容 · 无业务特征        业务承载 · 随时 disabled

2.8.2 威胁模型与攻击面

# 威胁 攻击路径 后果(若不防) 方案覆盖
T1 内容举报 用户/竞品提交 Brand URL + 业务截图 Brand 被渠道/浏览器标「危险」 Brand 301 零 Body
T2 监管/安全扫描 自动化扫 URL、路径字典 URL 入库、ISP 拦截 Brand/Gate 无 /login
T3 域名池撞库 脚本跟跳、暴力 resolve 批量封 play-* 加密 + 双检空页
T4 关联分析 CT 日志、证书 SAN、Referer Brand 与 Site 关联连带 证书分离、Brand 不带 Site
T5 分享链暴露 Share 302 明文 primary 主域被批量收集 Share 限流;primary 可换
T6 直访 / 收藏 用户长期保存 play-a.com 死域书签、主域被盯 可牺牲;Brand 兜底
T7 App 硬编码 旧 App 写死 Brand/Site 切域后仍访问死链 强更 + min_version
T8 密钥泄露 逆向 bootstrap 得 AES key 离线解密域名池 混淆 + key 轮换
T9 DNS 污染 区域 DNS 篡改 Brand Brand 打不开 多 Brand 分省
T10 Landing/Gate 被举报 落地域被标红 301 链断 多 Landing 热备
T11 Safe Browsing Google/360 威胁情报 浏览器大红屏 零内容 + 无恶意 JS
T12 中间人 / 劫持 运营商插入页面 假登录、Brand 污染 全链路 HTTPS、HSTS 评审
T13 错误文案泄密 UI 暴露「域名池」「40301」 社工 + 特征识别 §4.10.4 禁止文案
T14 运维误配 Brand CNAME 指 Site 桶 P0 事故 O-3 禁止 + 自动巡检

2.8.3 已覆盖能力评估

能力 强度 说明
Brand 零内容 301 ★★★★★ 举报材料无法含业务截图(仅 301/空)
落地域中间层 ★★★★☆ 判定失败不暴露主站;Landing 仍可能被扫
IP+fp 双检 ★★★★☆ 挡机房/Headless;误杀需调阈值
Resolve AES 加密 ★★★★☆ 提高撞库成本;非防逆向
主站池轮换 ★★★★★ Site 死 Brand 活——核心设计
证书/SAN 分离 ★★★★☆ 需运维 强制执行 O-7
分享直达主站 ★★☆☆☆ 薄弱点:绕过落地双检
直访主站 ★★☆☆☆ 薄弱点:无 Gate 仍可达业务
多 Landing 热备 ★★★☆☆ PRD 有述,需运维落地
浏览器 Safe Browsing 监测 ★☆☆☆☆ 遗漏:未写自动监测

2.8.4 漏洞与遗漏清单(须补齐)

ID 级别 问题 风险 建议加固 负责
SEC-01 P0 Brand 误指向 Site 源站/桶 Brand 一次即可被标危险 每日自动 curl + Body 扫描;O-3 告警 运维
SEC-02 P0 切流/应急把业务 挂回 Brand 重复 vty.vip 悲剧 制度禁止 + 变更审批 产品+运维
SEC-03 P0 落地页出现业务关键词/Logo 过大 扫描判定赌博页 bootstrap <2KB;无赛事/钱包文案 前端
SEC-04 P1 Share 302 明文 primary 到主站 主域 URL 泄露、被批量举报 Share 也走 短期 token 或限 referer;高频换 primary 技术
SEC-05 P1 主站 直访 无 Gate,爬虫直达 Site Site 被扫加速封 robots 无效;靠 WAF 行为 + 可选 Landing 二次跳转(高价值市场)
SEC-06 P1 H5 AES 密钥在 bootstrap 可逆向 离线解密域名列表 白盒混淆 + 90d 轮换 + 服务端 nonce;App 证书 pinning(可选) 安全+客户端
SEC-07 P1 无 Google Safe Browsing / 360 威胁情报监控 标红后才发现 每日 API 查 Brand/Landing;P0 告警 运维+安全
SEC-08 P1 Landing 单域 被举报全链断 用户经 Brand 进不来 ≥2 Landing FQDN;Brand 301 可切 运维
SEC-09 P1 错误页/Toast 中文赌博术语 特征匹配 仅用 §4.10 中性文案;i18n 同样约束 产品
SEC-10 P2 Referer 从 Site 回 Landing 带主域 关联分析 Site API Referrer-Policy: strict-origin 前端
SEC-11 P2 历史 CT 证书 泄露 Brand-Site 同组织 被动关联 不同注册商/组织名申请证书 运维
SEC-12 P2 App 分享带明文 Site 深链 域被传播 分享统一走 Share 短链 客户端
SEC-13 P2 resolve 503 维护页托管在 Landing 且含品牌业务图 Landing 被标 维护页 纯文本、无 Logo 设计
SEC-14 P2 竞品 DDoS Brand 致「无法访问」 可用性非内容 边缘清洗;与标红区分 KPI 运维

2.8.5 分层加固清单(执行项)

A. 品牌域(最高优先级 · 绝不能标危险)

# 措施 验收
A1 全路径 仅 301 → 落地域;Body 0 字节 TC-B1~B5 + 日巡检
A2 禁止 CNAME/A 记录指向 Site IP/桶 TC-P2 + SEC-01 脚本
A3 证书 独立;SAN 不含 play-* 证书审计 quarterly
A4 WAF:Brand 仅 GET/HEAD POST → 403
A5 多 Brand 热备(分省物料) 单 Brand 挂不影响全局
A6 Safe Browsing / 360 日检 SEC-07 告警
A7 应急 禁止 业务回挂 Brand(书面 SOP) 变更工单强制字段

B. 落地域(中间层 · 可换不可脏)

# 措施 验收
B1 仅 bootstrap + gate API;无业务路由 路径扫描 0 命中
B2 风控失败 空页;resolve 失败 中性文案 §4.10.4
B3 ≥2 Landing 热备;Brand 301 可切换 演练 T+0
B4 Bot IP 不调用 resolve TC-G2/G3
B5 维护页 无业务视觉 设计评审

C. 主站域(可牺牲 · 与 Brand 隔离)

# 措施 验收
C1 每域 独立 桶/CDN/证书 池表 channels
C2 探活 90s 摘除;Resolve 不再下发 TC-P1
C3 FAILOVER 仅池内切换;不回 Brand TC-L4
C4 单域被标红 → disabled + 换域;不动 Brand Runbook
C5 Share 302 目标 随 primary 变 TC-P3
C6 物料/广告 不印刷 play-*(仅 Brand) 市场规范

2.8.6 无法 100% 杜绝的情况(边界声明)

情况 说明 方案态度
国家级 深度封锁 Brand TLD 超出产品能力 换 Brand TLD + 分省
人工向 Apple/Google 举报 App 应用层非域名 独立 App 合规流程
Site 内容本身违规被全球标红 业务合规问题 非域名架构能解
用户 截图 play-a.com 举报 无法阻止用户行为 牺牲该 Site,Brand 仍干净
0-day CDN/边缘 RCE 挂马 基础设施 WAF + 供应链安全

产品口径: 域名架构保证 Brand 举报材料无业务特征;不能保证 Site 永不被标红——保证 Site 可换、Brand 不连带。


2.8.7 安全验收用例(TC-SEC · P0)

ID 步骤 期望
TC-SEC1 自动化扫 Brand / /login /api 仅 301/空;0 业务关键词
TC-SEC2 Headless resolve 40302 + 空页;响应 无 domains
TC-SEC3 curl resolve 无 fp 无 明文 play-*
TC-SEC4 检查 Brand 证书 SAN 无 Site 域
TC-SEC5 Safe Browsing 查 Brand 无 威胁标记
TC-SEC6 模拟「业务挂回 Brand」变更 工单 拒绝 / 告警
TC-SEC7 Site disabled 后 curl Brand 仍 301;Body 仍空
TC-SEC8 错误页文案扫描 无「域名池」「赌博」等禁词

三、运维实施手册

运维职责一句话: 品牌域 只做 301,Gate/Site 分桶部署,禁止 品牌域指向业务源站;监控与应急按本章执行。

3.1 职责清单

编号 事项 说明 验收
O-1 品牌域 DNS CNAME → Brand CDN(仅 301 规则) curl 仅 301
O-2 301 规则 全路径 HTTPS → https://gate.xx/r/{brandId}?ts=&sig= Location 正确
O-3 禁止绑 Site 桶 品牌域不得指向 Site IP/源站/对象存储 安全扫描
O-4 Gate 桶/Worker 托管 Gate 极简页(<2KB) 双检页可加载
O-5 Site 桶(每域) 每 play-*.com 独立 CDN 桶 + 静态资源 站点可访问
O-6 /__health 每 Site 配置探活 URL,Cache-Control: no-store 200 {"ok":true}
O-7 证书 Brand / Gate / Site 分别申请;30 天到期告警 日历
O-8 WAF Brand:GET/HEAD;Gate:+POST resolve;Site:常规 POST Brand 拒绝
O-9 监控告警 见 §3.4 钉钉/PagerDuty
O-10 应急 Runbook 见 §3.5 季度演练

3.2 URL 桶 / CDN 配置

bucket-brand-winbet      → 无文件;仅 301 → 落地域
bucket-landing-a         → entry-a.vty.net:landing/index.html(Gate bootstrap <2KB)
bucket-landing-b         → entry-b.vty.net:热备落地域
bucket-site-play-a       → 【主站域】H5 构建产物 + __health.json
bucket-site-play-b       → 同上
bucket-site-play-c       → 同上

禁止: 落地域桶 不得 部署完整 Site 产物;主站域桶 不得 绑定 Brand 301。

Brand CDN 规则示例(逻辑):

server {
  listen 443 ssl http2;
  server_name winbet.com www.winbet.com;
  location / {
    return 301 https://entry-a.vty.net/r/winbet?ts=$msec&sig=$gate_sig;
  }
}

sig 由边缘函数或配置中心下发,禁止在仓库写死可伪造密钥。

3.3 品牌域「零内容」规范

允许: DNS、TLS、301/308、必要安全响应头。
禁止: HTML/JS/CSS/图片、反代站点、下载页、客服页、业务 Set-Cookie、目录索引。

自检:

curl -sI https://winbet.com/
# 期望:301 + Location → Gate

curl -sL --max-redirs 0 https://winbet.com/ | wc -c
# 期望:Body 为 0 或极短

3.4 监控与告警

告警 条件 级别 接收
品牌域非 301 5min 内出现 200 含业务 body P0 运维
品牌 Body 非空 wc -c > 阈值 P0 运维
证书 30 天到期 — P1 运维
DNS 漂移 解析非预期 CNAME P1 运维
Gate 5xx 突增 — P1 运维+技术
Site 探活失败 单域摘除事件 P1 运维+技术
可用 Site = 0 池内无 healthy 域 P0 全员

3.5 应急 Runbook

事件 运维动作 技术动作
Site 单域被封 协助清理 DNS/缓存 自动/手动 disabled,切 primary
Site 全挂 不动 Brand 维护页;补域/修活
Gate 故障 Brand 301 切 备用 Gate Location 修 Gate 服务
品牌域被投诉 保持 301+零内容;不挂业务 评估换 Entry/Gate
DDoS 品牌域 边缘清洗;保 301 可用 应用层限流

四、技术实施方案

技术职责一句话: Gate 双检 + 加密 Resolve + 统一域名池 + 探活 Worker + H5/App SDK 轮询 + App 签名校验。

4.1 研发工单清单

编号 模块 交付 优先级
T-1 域名池合并 单表 + Admin + 数据迁移 P0
T-2 Gate 风控 IP 分 + fp 分 + 空页 P0
T-3 gate/resolve §4.4 接口 + 加密 P0
T-4 app/resolve §4.4 + 403 非客户端 P0
T-5 探活 Worker 30s 周期,3 败 disabled P0
T-6 H5 bootstrap 解密 + 轮询 + 重试 P0
T-7 App SDK 签名 + Keychain + 轮询 P0
T-8 Site API 网关 X-App-Sign 校验 P0
T-9 告警 可用域阈值告警 P1
T-10 埋点 hop 核销、切换事件 P1
T-11 Share Resolve 与 Resolve 同源 primary P1
T-12 App bootstrap + 强更 §4.4.5 + §2.7 切流配置 P0

4.2 域名池合并(H5 + App)

现状

后台 H5 池、App 池 分开 → 切换不同步、双份探活、分享与 App 不一致。

目标表 site_domain_pool

字段 说明
domain FQDN,唯一
status active / standby / disabled / blocked
health ok / fail / unknown
channels h5 / app / pc / all(可多选)
weight 1–100
is_primary 分享/默认主域
fail_streak 连续探活失败次数
last_probe_at 时间戳
operator 审计

迁移: 双池导入 → 重复域合并 channels → 删旧 API /api/h5/domains、/api/app/domains → 统一 /api/v1/admin/domains。

Resolve 过滤:

WHERE status='active' AND health='ok'
  AND (channels @> ARRAY['all'] OR channels @> ARRAY[:channel])
ORDER BY weight DESC LIMIT 3

4.3 风控:IP + 浏览器指纹(AND · 完整规则)

总规则(产品定案):

pass = (ip_verdict == NORMAL) AND (fp_verdict == NORMAL) AND (rate_limit_ok)
任一 FAIL → Gate 空页 或 Resolve 拒绝,不下发域名

判定顺序(研发按序短路):

  1. 黑名单 / 白名单(硬规则)
  2. 频率限流
  3. IP 评分 ip_score
  4. 浏览器指纹评分 fp_score(H5 必填;App 用 device 信号,见 §4.3.4)
  5. 通过 → 进入 Resolve

4.3.1 IP 异常规则(完整)

A. 硬拒绝(ip_verdict=BLOCK,不计算分数,直接空页)

编号 规则 ID 条件 说明
IP-H1 ip_blacklist IP 在运营/风控黑名单 配置中心维护
IP-H2 ip_tor_exit 命中 Tor 出口节点库 可选开启
IP-H3 ip_known_scanner 命中已知扫描器 IP 段(Censys、Shodan 等) 威胁情报
IP-H4 ip_empty_ua User-Agent 为空 且 非 App 签名请求 裸 curl
IP-H5 ip_geo_blocked 国家/地区在 blocklist(配置项,默认可为空) 与 allowlist 互斥优先 block

B. 硬放行(仅内网/调试,生产默认关闭)

编号 规则 ID 条件
IP-W1 ip_whitelist IP 在白名单 → 跳过 IP 打分,仅做频率检查

C. IP 评分模型(ip_score:0~100,≥60 为 NORMAL)

初始分 100,逐项扣分,最低 0:

编号 规则 ID 条件 扣分 数据来源
IP-S1 ip_datacenter ASN 类型 = hosting/cloud/datacenter -35 IP 信誉库
IP-S2 ip_proxy_vpn 命中 VPN/代理/匿名代理 -25 信誉库
IP-S3 ip_crawler_asn ASN 关联爬虫业务 -40 威胁情报
IP-S4 ip_geo_not_allowed 不在 allowlist 允许地区(若配置了 allowlist) -50 GeoIP
IP-S5 ip_recent_abuse 24h 内同 IP Resolve 失败/403 累计 ≥10 -30 Redis
IP-S6 ip_rate_high 同 IP >30 次/min 请求 Gate/Resolve -40 Redis
IP-S7 ip_rate_burst 同 IP >5 次/10s -25 Redis
IP-S8 ip_multi_fp 同 IP 10min 内关联 >20 个不同 fp_hash -35 Redis(撞库)
IP-S9 ip_header_anomaly 缺少 Accept-Language 且 缺少 Accept(H5) -15 请求头
IP-S10 ip_tls_fingerprint_bot JA3/JA4 命中已知 Bot 指纹(若边缘支持) -30 CDN

判定:

ip_score ip_verdict 行为
— BLOCK(硬规则) 空页,code=40301,reason=规则 ID
60~100 NORMAL 可进入 fp 检测
40~59 SUSPICIOUS 空页(v1 不做验证码降级;reason=ip_score_low)
0~39 ABNORMAL 空页,reason=ip_score_low

配置项: IP_SCORE_PASS=60(可调);IP_RATE_MIN=30/min;IP_RATE_BURST=5/10s。

D. IP 检测伪代码(服务端)

def check_ip(req) -> IpResult:
    ip = req.client_ip
    if in_blacklist(ip): return BLOCK("ip_blacklist")
    if in_whitelist(ip): return NORMAL(skip_score=True)

    if rate_over(ip, "30/min") or rate_over(ip, "5/10s"):
        return BLOCK("ip_rate")  # 也可只扣分到 SUSPICIOUS

    score = 100
    for rule in IP_SCORE_RULES:
        if rule.match(ip, req.headers):
            score -= rule.penalty
    score = max(0, score)

    if score >= 60: return NORMAL(score=score)
    return ABNORMAL(score=score, reason="ip_score_low")

4.3.2 浏览器指纹异常规则(H5 · 完整)

A. 采集时机与字段(Gate bootstrap 页)

页面加载 ≤800ms 内采集并提交;超时未提交视为 fp 异常。

字段 类型 必填 说明
canvas_hash string 是 Canvas 2D 渲染哈希
webgl_vendor string 是 WebGL UNMASKED_VENDOR
webgl_renderer string 否 渲染器
timezone string 是 Intl 时区,如 Asia/Shanghai
timezone_offset int 是 getTimezoneOffset()
languages string[] 是 navigator.languages 前 3 项
screen string 是 {w}x{h}x{depth}
color_depth int 是
device_memory number 否 有则采
hardware_concurrency int 否 CPU 核数
touch int 是 maxTouchPoints
platform string 是 navigator.platform
ua string 是 仅辅助,不单独作为通过依据
webdriver bool 是 navigator.webdriver
plugins_length int 否
cookie_enabled bool 是
local_storage bool 是 是否可用
session_id string 是 本次 Gate 页 UUID

提交格式:

{
  "fp_hash": "sha256(canonical_json_signals + FP_SALT)",
  "fp_signals": "base64(aes_compress(signals))",
  "session_id": "uuid",
  "collect_ms": 320
}

canonical_json:字段名 ASCII 排序后 JSON 序列化,确保同设备 hash 稳定。

B. 硬拒绝(fp_verdict=BLOCK)

编号 规则 ID 条件
FP-H1 fp_missing 无 fp_hash 或 collect_ms 超时(>3000ms 未提交)
FP-H2 fp_webdriver webdriver === true
FP-H3 fp_headless_ua UA 匹配 `/HeadlessChrome
FP-H4 fp_zero_screen screen 为 0x0 或缺失
FP-H5 fp_selenium document.documentElement.getAttribute("webdriver") 或 window 存在 __webdriver_* / _Selenium_* / callPhantom
FP-H6 fp_canvas_empty canvas_hash 为全零或已知 Headless 常量库命中
FP-H7 fp_rate 同 fp_hash >50 次/min 请求 Resolve
FP-H8 fp_blocklist fp_hash 在风控黑名单

C. 指纹评分模型(fp_score:0~100,≥60 为 NORMAL)

初始分 100:

编号 规则 ID 条件 扣分
FP-S1 fp_lang_tz_mismatch 时区为 UTC 且 languages 含 zh-CN(常见 Headless 配置错误) -20
FP-S2 fp_no_touch_desktop_ua UA 含 Mobile 但 maxTouchPoints=0 -15
FP-S3 fp_plugins_zero plugins_length=0 且 非 iOS Safari -10
FP-S4 fp_storage_disabled cookie 与 localStorage 均 false -25
FP-S5 fp_webgl_software renderer 含 `SwiftShader llvmpipe
FP-S6 fp_screen_rare 分辨率在已知 Bot 常见表(如 800×600、1×1) -20
FP-S7 fp_ua_chrome_no_chrome_obj UA 含 Chrome 但无 window.chrome(部分 Headless) -25
FP-S8 fp_inconsistent_platform iPhone UA 但 platform=Win32 等明显矛盾 -40
FP-S9 fp_repeat_session 同 session_id 重复 Resolve -50(重放)
FP-S10 fp_new_device_burst 同 IP 1h 内首次出现的新 fp_hash >100 个 -30(刷量)

判定:

fp_score fp_verdict 行为
BLOCK(硬规则) — 空页,code=40302,reason=规则 ID
60~100 NORMAL 可调 Resolve
40~59 SUSPICIOUS 空页(reason=fp_score_low)
0~39 ABNORMAL 空页

配置项: FP_SCORE_PASS=60;FP_COLLECT_TIMEOUT_MS=3000。

D. 双检合并决策表

ip_verdict fp_verdict 最终结果 用户看到 Resolve
NORMAL NORMAL PASS bootstrap 继续 加密 3 域
BLOCK/ABNORMAL/SUSPICIOUS * FAIL 空页 不调用
NORMAL BLOCK/ABNORMAL/SUSPICIOUS FAIL 空页 不调用
NORMAL NORMAL 但 rate_limit 失败 FAIL 空页/429 不调用

响应码映射:

reason code 日志字段
ip 硬规则 40301 risk_ip, rule_id
ip 低分 40301 ip_score, value
fp 硬规则 40302 risk_fp, rule_id
fp 低分 40302 fp_score, value
频率 429 rate_limit

4.3.3 已知爬虫 / Bot 处理策略

Bot 类型 识别方式 处理
搜索引擎(Baiduspider 等) UA + IP 段验证(可选 DNS 反查) 空页 或 仅 301 不回 Resolve;不下发域名
安全扫描器 IP-S3 + 路径探测 空页 + WAF 封禁
Headless 自动化 FP-H2~H6 空页
机房脚本 IP-S1 + FP 缺失 空页
正常用户移动浏览器 ip≥60 且 fp≥60 正常 Resolve

4.3.4 App 端风控差异(不经 Gate 页)

App 无浏览器 Canvas,改用 设备信号 + 签名:

信号 异常规则
无 X-App-Sign 或签名错误 403(硬拒绝)
device_id 为空或格式非法 403
同 device_id >20 次/min resolve 429
同 IP 无签名浏览器访问 app/resolve 403
越狱/Root 检测(可选) 仅告警,不单独拦截(避免误杀)
IP 规则 与 H5 相同 IP 评分模型(§4.3.1)

App 不采 fp_score 时:pass = ip_verdict == NORMAL && app_sign_valid && app_version_ok。

版本校验(与 H5 差异 · 切流必开):

规则 条件 响应
APP-V1 semver(X-App-Version) < min_version 40310 force_update
APP-V2 全局 force_update=true 且版本 < latest_version 40310(切流窗口)
APP-V3 brand_pack_version 本地 < required_brand_pack 40310(皮肤大改版)
APP-V4 可选:latest_version 高于当前 bootstrap 返回 optional_update,不拦 resolve

4.3.5 空页规范(异常时必须满足)

HTTP/2 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 0
Cache-Control: no-store

禁止: 302 到 Site、JSON 带域名、错误文案暴露「域名池」。


4.3.6 IP 如何收集与如何判断(服务端 · 技术直接实现)

要点: IP 仅服务端从连接/边缘头获取,不信任客户端上报。

收集(Request → client_ip)

优先级 来源 说明
1 CF-Connecting-IP Cloudflare
2 X-Real-IP Nginx/网关
3 X-Forwarded-For 第一个公网 IP 需网关 strip 伪造
4 socket.remoteAddress 直连兜底

实现清单:

function getClientIp(req) {
  const cf = req.headers['cf-connecting-ip'];
  if (cf) return cf.trim();
  const real = req.headers['x-real-ip'];
  if (real) return real.trim();
  const xff = req.headers['x-forwarded-for'];
  if (xff) return xff.split(',')[0].trim();
  return req.socket.remoteAddress;
}

enrich(ipIntel.lookup)

字段 来源 用途
asn / asnType GeoIP/ASN DB IP-S1 datacenter
isProxyOrVpn 商业 IP 库 IP-S2
isTorExit Tor 列表 IP-H2
isKnownScanner 威胁情报 IP-H3
country / region GeoIP IP-H5 / IP-S4
ja3Bot CDN Bot 管理 IP-S10

Redis 键(频率与撞库)

Key TTL 用途
ip:{ip}:m1 60s IP-S6 每分钟计数
ip:{ip}:10s 10s IP-S7 _burst
ip:{ip}:fp10m 600s SET IP-S8 不同 fp_hash
ip:{ip}:abuse24h 86400s IP-S5 失败累计

判断输出(写入 Resolve 日志)

{
  "ip": "1.2.3.4",
  "ip_score": 72,
  "ip_verdict": "NORMAL",
  "ip_rules_hit": ["IP-S9"],
  "asn": "AS4134",
  "country": "CN"
}

参考代码: 仓库 js/gate-ip-check.reference.js(checkIp / gateAllowResolve)。


4.3.7 浏览器指纹如何收集与如何判断

A. 收集(Gate bootstrap 页 · 客户端)

步骤 动作 时限
1 用户 301 进入 Gate /r/{brand} —
2 加载 bootstrap(<2KB)+ gate-fp-collect.js —
3 VtyGateFp.collectFingerprint() ≤800ms 目标
4 POST /api/v1/gate/resolve 带 fp_hash + 可选加密 fp_signals 5s 总超时
5 服务端 checkFingerprint() 打分 —

采集字段与代码: 见 §4.3.2 A 表;参考 js/gate-fp-collect.js。

// Gate 页主流程(前端必须实现)
VtyGateFp.collectFingerprint().then(function (fp) {
  if (!VtyGateFp.clientPrecheck(fp).ok) {
    document.documentElement.innerHTML = ''; // 本地预检失败也空页
    return;
  }
  return fetch('/api/v1/gate/resolve', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      brand_id: 'winbet',
      channel: 'h5',
      fp_hash: fp.fp_hash,
      session_id: fp.session_id,
      collect_ms: fp.collect_ms,
      ts: Math.floor(Date.now() / 1000),
      nonce: crypto.randomUUID(),
      sig: signResolve(...),
    }),
  });
});

B. 判断(服务端 · 硬规则优先于评分)

if FP-H1~H8 任一命中 → fp_verdict=BLOCK, code=40302
else 计算 fp_score → ≥60 NORMAL / 否则 SUSPICIOUS → 空页
if ip_verdict != NORMAL → 不执行 fp,直接空页

C. 服务端 fp 日志示例

{
  "fp_hash": "a1b2...",
  "fp_score": 85,
  "fp_verdict": "NORMAL",
  "fp_rules_hit": [],
  "collect_ms": 320,
  "session_id": "uuid"
}

D. 运维/客服说明

对用户 对客服
指纹/IP 失败 无任何提示 话术:「请换浏览器/关 VPN/换 4G 再试」,不解释指纹

4.4 接口契约

4.4.1 何时返回域名(决策表)

# 条件 结果 HTTP
1 IP 异常 不返回 200 code=40301 或 Gate 空页
2 指纹异常(H5) 不返回 code=40302
3 App 签名无效 不返回 403
3a App 版本 < min_version 不返回 40310 force_update
3b force_update=true 且版本不在 allowlist 不返回 40310
4 频率超限 不返回 429
5 brand 禁用 不返回 404
6 healthy 域 = 0 不返回 503 code=50301
7 healthy 1~2 域 返回 1~2 条加密 200 code=0
8 healthy ≥ 3 域 返回 3 条加密 200 code=0

4.4.2 POST /api/v1/gate/resolve(H5)

Request

{
  "brand_id": "winbet",
  "channel": "h5",
  "fp_hash": "a1b2...",
  "ts": 1735689600,
  "nonce": "uuid",
  "sig": "hmac(brand_id|ts|nonce|fp_hash, gate_secret)"
}

Response 成功

{
  "code": 0,
  "data": {
    "key_id": "k2026-03",
    "expires_at": 1735689900,
    "ttl_sec": 300,
    "payload": "<BASE64 AES-256-GCM>"
  }
}

payload 明文(加密前)

{
  "v": 1,
  "domains": ["play-a.com", "play-b.com", "play-c.com"],
  "primary": "play-a.com",
  "hop_id": "hop_8f3k...",
  "brand_id": "winbet"
}

4.4.3 POST /api/v1/app/resolve(App)

Headers(缺一则 403)

X-Client-Type: app
X-App-Id: com.vty.app
X-App-Version: 3.2.0
X-Timestamp: 1735689600
X-Nonce: uuid
X-App-Sign: HMAC-SHA256(app_secret, method|path|timestamp|nonce|body_sha256)

Body

{
  "channel": "app",
  "platform": "ios",
  "device_id": "哈希",
  "fp_hash": "...",
  "bundle_id": "com.vty.app"
}
请求特征 响应
无 X-App-Sign 403
签名错误 403
UA 为浏览器/curl 403
X-App-Version < min_version 40310 force_update
切流日 force_update=true 40310

40310 响应体:

{
  "code": 40310,
  "message": "force_update",
  "data": {
    "min_version": "4.0.0",
    "latest_version": "4.0.2",
    "update_url": {
      "ios": "https://apps.apple.com/app/idXXXX",
      "android": "https://download.example/app-v4.apk"
    },
    "brand_pack_version": "2026-brand-v2",
    "title_key": "app.force_update_title",
    "body_key": "app.force_update_body"
  }
}

4.4.5 GET /api/v1/app/bootstrap(App · 冷启动第一请求)

调用时机: App 启动 Splash 阶段,早于 Keychain 读域与 app/resolve。切流日配置在此接口生效。

Headers

X-Client-Type: app
X-App-Id: com.vty.app
X-App-Version: 4.0.0
X-Platform: ios
X-App-Sign: HMAC-SHA256(...)   # 与 resolve 同算法,path=/api/v1/app/bootstrap

Response 成功(可继续 resolve)

{
  "code": 0,
  "data": {
    "min_version": "4.0.0",
    "latest_version": "4.0.2",
    "force_update": true,
    "optional_update": false,
    "brand_pack_version": "2026-brand-v2",
    "required_brand_pack": "2026-brand-v2",
    "official_brand_url": "https://winbet.com",
    "update_url": {
      "ios": "https://apps.apple.com/app/idXXXX",
      "android": "https://download.example/app-v4.apk"
    },
    "resolve_enabled": true,
    "cutover_bundle": "2026-brand-v2"
  }
}

Response 强制更新(客户端不得继续 resolve)

{
  "code": 40310,
  "message": "force_update",
  "data": { "...同上 update_url..." }
}

服务端判定顺序

1. 签名校验 → 403
2. semver(AppVersion) < min_version → 40310
3. force_update=true 且 AppVersion < latest_version → 40310
4. brand_pack 本地版本 < required_brand_pack → 40310
5. 通过 → code=0

配置中心键(切流 T 日一键改)

键 示例 说明
app.min_version.ios 4.0.0 低于此 永久 40310
app.min_version.android 4.0.0 同上
app.force_update true 切流窗口;T+7 可改 false
app.latest_version.* 4.0.2 配合 force_update
app.brand_pack.required 2026-brand-v2 皮肤 bundle 版本
app.cutover_bundle 2026-brand-v2 审计标识

客户端实现要点

1. Splash → bootstrap(超时 5s,失败重试 2 次)
2. code=40310 → 全屏 ForceUpdateActivity(禁返回键 / 禁侧滑)
3. code=0 → 比对 brand_pack_version,加载包内或 CDN 主题资源
4. 再进入 §2.3.10 A1~A5
5. resolve 若仍返回 40310 → 同全屏强更(双保险)

4.4.4 Site 健康检查

HEAD https://{site}/__health
→ 200 {"ok":true,"ts":1735689600}

4.5 加密方案

项 规格
算法 AES-256-GCM
密钥 配置中心 key_id → key,90 天轮换
客户端 H5:bootstrap 内嵌(混淆);App:内置可热更新
防重放 expires_at + nonce Redis 去重 5min

4.6 客户端轮询与域名切换(完整方案)

目标: 单域 regional 失败时 ≤3s 自动切下一域;全失败 ≤10s 内重拉 Resolve;运行期故障 无感切换。

4.6.1 名词与本地缓存

名词 说明
domain_list Resolve 解密后的有序数组,最多 3 个 FQDN
primary 列表首选域(与 domain_list[0] 一致,除非服务端另有指定)
current_domain 当前正在使用的 Site 基址
resolve_meta { payload, expires_at, hop_id, key_id, resolved_at }
probe_cache 每域最近探活结果 { domain, ok, latency_ms, ts },TTL 60s

持久化(H5): sessionStorage(优先)+ localStorage 备份;键名 vty_ds_v1。
持久化(App): Keychain / EncryptedSharedPreferences;键名 vty.baseUrl.v1。

4.6.2 状态机

                    ┌─────────────┐
                    │   INIT      │
                    └──────┬──────┘
                           │ 有未过期 resolve_meta?
              ┌────────────┼────────────┐
              ▼ yes                     ▼ no
     ┌────────────────┐        ┌────────────────┐
     │ LOAD_CACHED    │        │ FETCH_RESOLVE  │
     └───────┬────────┘        └───────┬────────┘
             │                         │
             └──────────┬──────────────┘
                        ▼
               ┌────────────────┐
               │ PROBE_LIST     │  按序探活 domain_list
               └───────┬────────┘
                       │ 有 ok 域?
          ┌────────────┼────────────┐
          ▼ yes                     ▼ no
 ┌─────────────────┐      ┌─────────────────┐
 │ ENTER_SITE      │      │ RETRY_RESOLVE   │  最多 3 轮
 └────────┬────────┘      └────────┬────────┘
          │                          │ 仍失败
          │                          ▼
          │                 ┌─────────────────┐
          │                 │ MAINTENANCE_UI  │
          │                 └─────────────────┘
          ▼
 ┌─────────────────┐
 │ RUNNING         │  业务运行 + 后台探活
 └────────┬────────┘
          │ 当前域失败
          ▼
 ┌─────────────────┐
 │ FAILOVER        │  切列表下一域 或 RETRY_RESOLVE
 └─────────────────┘

4.6.3 阶段一:冷启动 / 品牌进入(H5)

步骤 动作 超时 失败处理
1 Gate 页采集 fp → 调 gate/resolve 5s 空页或维护
2 AES 解密 payload → 得 domain_list 500ms 重试 resolve 1 次
3 写入 resolve_meta(含 expires_at) — —
4 PROBE_LIST(§4.6.4) 9s 总预算 RETRY_RESOLVE
5 location.replace(https://{current}/?hop=...) — —
6 上报 entry_success / entry_fail — —

Resolve 重试(RETRY_RESOLVE): 最大 3 轮 · 间隔 2s · 仍失败 → MAINTENANCE_UI。

4.6.4 PROBE_LIST:域名探活(H5 / App 共用)

对 domain_list 按服务端返回顺序依次探活:

for i, domain in enumerate(domain_list):
  result = probe(domain)   # 单次 ≤3s,不重试
  if result.ok:
    current_domain = domain
    persist(...)
    return SUCCESS
return FAIL
项 规格
方法 HEAD https://{domain}/__health(失败 fallback GET)
单次超时 3s(连接+读合计)
成功 HTTP 200 且 ok===true
失败 超时、DNS 失败、4xx/5xx、证书错误
域间等待 0(立即下一域)
缓存 RUNNING 阶段 60s 内成功可跳过 HEAD

4.6.5 阶段二:运行期 FAILOVER

触发(任一): 当前域 API 连续 3 次失败 · /__health 2 次失败(30s 间隔)· 主接口超时 >5s 且重试仍失败 · WebView onReceivedError。

流程: 标记 local_bad → probe 列表下一域 → 成功则切换并上报 domain_switch → 无 ok 域则 RETRY_RESOLVE(2 轮)→ 仍失败 MAINTENANCE_UI。

后台探活: 每 5min HEAD 当前域;App/H5 回前台 立即探活 1 次。

4.6.6 App 冷启动差异

先读 Keychain current_domain → probe 1 次;成功且 meta 未过期则 跳过 resolve;否则 app/resolve + PROBE_LIST。expires 默认 300s,过期前 60s 后台静默 refresh。

4.6.7 服务端探活 Worker

项 规格
周期 30s
成功 200 且 <3s;fail_streak=0
摘除 连续 3 次失败 → disabled + P1 告警
恢复 需 人工确认 回 active
primary 摘除 自动升 weight 最高 healthy 域

4.6.8 切换场景速查

场景 服务端 客户端
单 Site regional 慢 health 可能仍 ok PROBE_LIST 下一域
单 Site 宕机 90s disabled FAILOVER 或下次 Resolve 剔除
3 Site 全挂 503 MAINTENANCE_UI
弱网 — 3 域 + Resolve 3 轮

4.6.9 SDK 配置常量(实现必须对齐)

const CFG = {
  probeTimeoutMs: 3000,
  probeListBudgetMs: 9000,
  resolveMaxRounds: 3,
  resolveBackoffMs: 2000,
  runningProbeIntervalMs: 300000,
  apiFailThreshold: 3,
  healthFailThreshold: 2,
  resolveTtlSec: 300,
  resolveRefreshBeforeExpirySec: 60,
};

4.6.10 轮询埋点(必填)

事件 字段
domain_probe try_order, domain, ok, latency_ms, err_code
resolve_fetch round, code, domain_count, latency_ms
domain_switch from_domain, to_domain, reason
entry_success site_domain, total_ms, probe_tries, resolve_rounds
entry_fail fail_stage: resolve/probe/enter, reason
触发 动作
服务端 3 败 auto disabled,Resolve 剔除
客户端当前域挂 FAILOVER 下一域
列表全挂 RETRY_RESOLVE
人工 blocked 立即剔除

4.10 前端异常提示规范(完整文案 · 技术/UI 直接实现)

截图 Demo: demos/h5-error-states.html(430px 首页/登录 · 全部态)

4.10.1 UI 形态定案

形态 代码 用于 是否挡操作
空页 EMPTY Gate IP/指纹/爬虫 —
顶栏 Loading TOP_LOADING Resolve + PROBE_LIST 半透明,无取消按钮
Toast TOAST 单域 probe 失败、FAILOVER 成功 不挡,3s 消失
全屏蒙层 FULL_BLOCK Resolve 全失败、解密失败、429 挡,需点按钮

4.10.2 文案表(i18n key · zh-CN / vi-VN 定稿)

ID 触发条件 UI titleKey bodyKey primaryBtn secondaryBtn
E-GATE-01 ip/fp BLOCK,40301/40302 EMPTY — — — —
E-LOAD-01 开始 resolve 至 probe 结束 TOP_LOADING — gate.connecting — —
E-PROBE-01 probe[i] 失败且 i+1 存在 TOAST — gate.switching_line — —
E-NET-01 FAILOVER 切换成功 TOAST — gate.switched_line — —
E-RESOLVE-01 50301 或 3 轮 resolve 失败 FULL_BLOCK gate.unavailable_title gate.unavailable_body gate.retry gate.contact
E-DECRYPT-01 AES 解密/JSON 校验失败 FULL_BLOCK gate.load_fail_title gate.load_fail_body gate.refresh —
E-RATE-01 HTTP 429 FULL_BLOCK gate.rate_limit_title gate.rate_limit_body gate.retry_later —
E-APP-FORCE-01 bootstrap/resolve 40310 FULL_BLOCK(App 全屏) app.force_update_title app.force_update_body app.go_store —
E-SSL-01 探活证书错误(probe) TOAST — gate.switching_line — —

语言包路径约定: locales/zh-CN/gate.json、locales/vi-VN/gate.json、locales/{lang}/app.json
切换规则: Accept-Language → 用户设置 → 默认 zh-CN;key 完全一致,仅 value 不同。

zh-CN(简体中文)

{
  "gate.connecting": "正在连接最佳线路…",
  "gate.switching_line": "当前线路响应较慢,正在为您切换备用线路…",
  "gate.switched_line": "网络异常,已为您切换至备用线路",
  "gate.unavailable_title": "暂时无法连接",
  "gate.unavailable_body": "网络或服务繁忙,请稍后重试。若持续无法访问,请联系在线客服。",
  "gate.retry": "重新连接",
  "gate.contact": "联系客服",
  "gate.load_fail_title": "加载失败",
  "gate.load_fail_body": "页面数据异常,请刷新重试。如问题仍存在,请清除浏览器缓存后再试。",
  "gate.refresh": "刷新重试",
  "gate.rate_limit_title": "访问过于频繁",
  "gate.rate_limit_body": "请 1 分钟后再试。",
  "gate.retry_later": "稍后重试",
  "app.force_update_title": "请更新至最新版本",
  "app.force_update_body": "品牌服务已升级,当前版本无法继续使用。请更新 App 后重新打开。",
  "app.go_store": "立即更新"
}

vi-VN(Tiếng Việt · 越南语)

{
  "gate.connecting": "Đang kết nối tuyến tốt nhất…",
  "gate.switching_line": "Tuyến hiện tại phản hồi chậm, đang chuyển sang tuyến dự phòng…",
  "gate.switched_line": "Mạng bất thường, đã chuyển sang tuyến dự phòng",
  "gate.unavailable_title": "Tạm thời không thể kết nối",
  "gate.unavailable_body": "Mạng hoặc dịch vụ đang bận, vui lòng thử lại sau. Nếu vẫn không truy cập được, hãy liên hệ CSKH trực tuyến.",
  "gate.retry": "Kết nối lại",
  "gate.contact": "Liên hệ CSKH",
  "gate.load_fail_title": "Tải thất bại",
  "gate.load_fail_body": "Dữ liệu trang bất thường, vui lòng làm mới và thử lại. Nếu vẫn lỗi, hãy xóa bộ nhớ đệm trình duyệt rồi thử lại.",
  "gate.refresh": "Làm mới và thử lại",
  "gate.rate_limit_title": "Truy cập quá thường xuyên",
  "gate.rate_limit_body": "Vui lòng thử lại sau 1 phút.",
  "gate.retry_later": "Thử lại sau",
  "app.force_update_title": "Vui lòng cập nhật phiên bản mới nhất",
  "app.force_update_body": "Dịch vụ thương hiệu đã nâng cấp, phiên bản hiện tại không thể tiếp tục sử dụng. Vui lòng cập nhật App rồi mở lại.",
  "app.go_store": "Cập nhật ngay"
}

中越对照速查(供 UI 走查)

i18n key zh-CN vi-VN
gate.connecting 正在连接最佳线路… Đang kết nối tuyến tốt nhất…
gate.switching_line 当前线路响应较慢… Tuyến hiện tại phản hồi chậm…
gate.switched_line 网络异常,已为您切换… Mạng bất thường, đã chuyển sang tuyến dự phòng
gate.unavailable_title 暂时无法连接 Tạm thời không thể kết nối
gate.unavailable_body 网络或服务繁忙… Mạng hoặc dịch vụ đang bận…
gate.retry 重新连接 Kết nối lại
gate.contact 联系客服 Liên hệ CSKH
gate.load_fail_title 加载失败 Tải thất bại
gate.refresh 刷新重试 Làm mới và thử lại
gate.rate_limit_title 访问过于频繁 Truy cập quá thường xuyên
app.force_update_title 请更新至最新版本 Vui lòng cập nhật phiên bản mới nhất
app.go_store 立即更新 Cập nhật ngay

E-GATE-01 中越一致:空页,无任何用户文案。

进站后: 地址栏 = 主站域;业务 API 基址 = https://{current_domain}。


4.10.5 报错归属域规则(落地域 vs 主站域)

文案 ID 触发阶段 报错归属域 地址栏示例 备注
E-GATE-01 落地·双检失败 落地域 entry.vty.net 空页,无业务壳
E-LOAD-01 落地·resolve/probe 落地域 entry.vty.net 可用登录/首页 骨架(非真业务)
E-PROBE-01 落地·probe 切换 落地域 entry.vty.net Toast
E-RESOLVE-01 落地·全失败 落地域 entry.vty.net 全屏;不得链到 play-*.com
E-DECRYPT-01 落地·解密失败 落地域 entry.vty.net 全屏
E-NET-01 **主站·**FAILOVER 主站域 play-a.com Toast
E-RESOLVE-01(重连) 主站·列表耗尽 主站域 play-a.com 用户已进站后的极端失败
业务 API 错误 主站·运行期 主站域 play-a.com 常规业务错误码

实现约束:

// 落地域 bootstrap 内
const ERROR_DOMAIN = 'landing'; // location.hostname === LANDING_FQDN

function showError(code) {
  if (getPhase() === 'landing') {
    // 判定失败:不 location 到 play-*.com
    renderOnLandingShell(code);
    return;
  }
  renderOnSiteShell(code); // 主站域报错
}

function onProbeSuccess(siteFqdn) {
  location.replace('https://' + siteFqdn + '/?hop=' + hopId);
  // 此后 phase = 'site'
}

4.10.3 展示页面规则

当前路由 使用的壳 说明
品牌→落地域→尚未进主站 落地域 bootstrap + 骨架壳 Loading/全屏 必须在落地域 hostname
已进主站 首页/登录 主站域 完整壳 FAILOVER 用 Toast
落地·风控硬失败 落地域 空页 不渲染 VTY 业务壳

4.10.4 禁止文案(安全)

4.11 轮询过程 UI 与提示(逐步映射)

轮询阶段 SDK 状态 用户看到 文案 ID 最长耗时
调 resolve FETCH_RESOLVE 顶栏 Loading E-LOAD-01 5s
解密 payload FETCH_RESOLVE 同上 E-LOAD-01 +0.5s
probe d1 PROBE_LIST Loading;d1 失败则 Toast E-LOAD-01 → E-PROBE-01 3s
probe d2 PROBE_LIST Toast(若 d1 失败) E-PROBE-01 +3s
probe d3 PROBE_LIST Toast(若 d2 失败) E-PROBE-01 +3s
全部 probe 失败 RETRY_RESOLVE Loading E-LOAD-01 2s×3 轮
3 轮仍失败 MAINTENANCE 全屏 E-RESOLVE-01 —
进入 Site ENTER_SITE Loading 消失 — —
运行期 API 失败 FAILOVER Toast E-NET-01 ≤6s
FAILOVER 仍失败 RETRY_RESOLVE → 全屏 E-RESOLVE-01 —

「重新连接」: 清空 resolve_meta + local bad → INIT 重跑。
「联系客服」: 跳转 Site 客服路由或配置中心备用外链。

4.7 分享 Resolve

4.8 埋点字段

字段 说明
entry_type brand / share / direct / app
brand_domain 如 winbet.com
hop_id 去重
site_domain 落地域
resolve_version 池版本
domain_switch 是否发生客户端切换

4.9 分期排期

阶段 范围 周期
Phase A 池合并 + Resolve + 探活 + Admin 2 周
Phase B Gate 双检 + 加密 + H5 bootstrap 1.5 周
Phase C App resolve + 签名网关 + SDK 1.5 周
Phase D 告警 + 埋点 + Share + 压测 1 周

五、测试方案

测试职责一句话: 按 P0 用例验收 品牌零内容、双检空页、加密 3 域、轮询切换、App 403、池合并一致;P0 全过方可上线。

5.1 测试策略

层级 范围 工具
边缘/运维 Brand 301、Body 为空、证书 curl、监控脚本
接口 Resolve 决策表、加密解密 Postman、自动化
风控 IP/指纹/mock Headless 代理 IP、Playwright
端到端 品牌→Gate→Site 全链路 Playwright、真机
App 签名、403、轮询 XCTest/Espresso + mock
回归 主站切换 H5/App/Share 一致 后台改 primary + 多端验证

5.2 P0 用例(必须全过)

品牌域与 Gate

ID 模块 步骤 期望
TC-B1 品牌 HEAD/GET 品牌根 301 → 落地域
TC-B2 品牌 GET /login /promo 仍 301,不得 200 业务页
TC-B3 品牌 Body 检测 无业务 HTML 特征
TC-B4 品牌 POST 品牌域 拒绝
TC-B5 品牌 301 Location 仅 落地域 FQDN,非 play-*.com
TC-L1 落地域 IP/fp 失败 地址栏仍 landing;空页
TC-L2 落地域 resolve 失败 E-RESOLVE 在 landing hostname
TC-L3 落地域 probe 成功 location → 主站域;地址栏变化
TC-L4 主站域 FAILOVER Toast 在 play-*.com;不回 landing
TC-G1 Gate 正常 IP+指纹 解密得 ≤3 域,能进站
TC-G2 Gate 机房 IP(IP-S1 datacenter) 空页,40301
TC-G3 Gate Headless 指纹(FP-H2 webdriver) 空页,40302
TC-G4 Gate 伪造 resolve sig 拒绝
TC-G5 Gate ip_score=59 空页,reason=ip_score_low
TC-G6 Gate fp_score=59 空页,reason=fp_score_low
TC-G7 Gate 同 IP 31 次/min 429 或空页
TC-G8 Gate fp 采集超时 >3s 空页,FP-H1

Resolve 与加密

ID 模块 步骤 期望
TC-R1 Resolve healthy≥3 payload 含 3 域
TC-R2 Resolve healthy=0 503,无域名
TC-R3 加密 解密 payload 字段完整,expires 有效
TC-R4 切换 disabled d1 Resolve 不再含 d1
TC-R5 池 改 primary H5/App/Share 一致

客户端轮询

ID 模块 步骤 期望
TC-C1 轮询 仅 d2 健康 自动进 d2,try_order=1
TC-C2 轮询 3 域全挂 RETRY_RESOLVE 3 轮后维护页
TC-C3 轮询 运行中 d1 挂 FAILOVER 切 d2
TC-C4 轮询 单域 probe 超时 3s 立即试下一域,总预算 ≤9s
TC-C5 轮询 API 连续 3 失败 触发 FAILOVER
TC-C6 轮询 resolve 缓存未过期 跳过 resolve,先 probe 缓存域
TC-C7 App Keychain 域 probe 成功 跳过 resolve

前端异常提示(§4.10 / §4.11)

编号 场景 步骤 期望
TC-UI1 E-LOAD-01 正常进站,观察 resolve→probe 顶栏「正在连接最佳线路…」,无按钮
TC-UI2 E-PROBE-01 d1 探活失败 Toast「当前线路响应较慢…」,3s 消失
TC-UI3 E-NET-01 运行期 API 失败触发 FAILOVER Toast「网络异常,已为您切换…」
TC-UI4 E-RESOLVE-01 3 轮 resolve 全失败 全屏「暂时无法连接」+ 重新连接/联系客服
TC-UI5 E-DECRYPT-01 篡改 payload 全屏「加载失败」+ 刷新重试
TC-UI6 E-GATE-01 IP/指纹 BLOCK 空页,无任何 VTY 文案
TC-UI7 登录页 同上各态在 /login 文案与首页 完全一致

App

ID 模块 步骤 期望
TC-A1 App 正常签名 resolve 得加密域
TC-A2 App curl 无签名 403
TC-A3 App 浏览器调 app/resolve 403
TC-A4 App Site API 无签名 403
TC-A5 App 不经品牌域 冷启动直达 Site
TC-VER1 App 版本 bootstrap OK ≥4.0.0 正常 resolve
TC-VER2 App 版本 3.x 调 bootstrap 40310 全屏强更
TC-VER3 App 版本 3.x 调 app/resolve 40310(绕过 bootstrap 仍拦)
TC-VER4 切流 T 日 force_update=true 旧安全版本可选;<min 必拦
TC-VER5 皮肤 brand_pack 过旧 40310 或强更
TC-VER6 切流 Brand+Gate+min_version 同发 H5/App 新 Brand 一致

探活与运维

ID 模块 步骤 期望
TC-P1 探活 模拟 d1 超时 3 次 90s 内 auto disabled
TC-P2 运维 品牌 DNS 自检 不指向 Site 源站
TC-P3 分享 Resolve vs Share 同一 primary
TC-SH1 分享 有效 code 302 到 primary + entry=share
TC-SH2 分享 code 过期 410
TC-SH3 分享 primary 挂 backup ok 302 到 backup 域
TC-SH4 分享 落地后 Site 挂 + 有缓存 FAILOVER Toast
TC-DIR1 直访 play-a health OK entry_type=direct,无 Gate
TC-DIR2 直访 play-a 挂 + 有 resolve_meta 自动切 b/c
TC-DIR3 直访 play-a 挂 + 无缓存 跳 official_brand_url

安全审视(§2.8)

ID 模块 步骤 期望
TC-SEC1 安全 扫 Brand 全路径 仅 301/空;0 业务关键词
TC-SEC2 安全 Headless resolve 40302 空页;无 domains
TC-SEC3 安全 curl resolve 无明文 play-*
TC-SEC4 安全 Brand 证书 SAN 无 Site 域
TC-SEC5 安全 Safe Browsing Brand 无威胁标记
TC-SEC6 安全 业务挂回 Brand 变更 工单拒绝/告警
TC-SEC7 安全 Site disabled 后 Brand 仍 301 空 Body
TC-SEC8 安全 错误页文案 无禁词

埋点

ID 模块 步骤 期望
TC-T1 埋点 品牌进入 entry_type=brand + hop_id
TC-T2 埋点 hop 重复 只计 1 次
TC-T3 埋点 直连 Site 不误计 brand

5.3 P1 用例

ID 步骤 期望
TC-F1 Entry 限流 429/保护响应
TC-F2 管理 API 公网未授权 拒绝
TC-F3 证书到期模拟 告警触发
TC-F4 可用域≤1 P1 告警
TC-F5 40301 占比>40% 风控复核

成功率 KPI 验收(相对 vty.vip)

ID 模块 步骤 期望
TC-K1 KPI 中国区 7 天 entry_success 率 ≥ 82%(灰度)/ ≥ 88%(稳定)
TC-K2 KPI 分省最低(不含极端样本省) ≥ 78%
TC-K3 KPI 分运营商均值 ≥ 85%
TC-K4 KPI 较 vty.vip 基线 75% 提升 ≥ 13pp(稳定期)
TC-K5 归因 entry_fail.fail_stage 报表 可拆解 DNS/Gate/Resolve/Probe
TC-K6 举报 自动化扫 Brand Body 0 业务特征;7 日内 Brand 零内容失效 0 次

5.4 自动化建议

// Playwright 示例
test('brand zero content', async ({ request }) => {
  const r = await request.get('https://winbet.com/', { maxRedirects: 0 });
  expect(r.status()).toBe(301);
  expect(r.headers()['location']).toMatch(/gate\./);
});

test('headless gets empty gate', async ({ browser }) => {
  const ctx = await browser.newContext({ userAgent: 'HeadlessChrome' });
  const page = await ctx.newPage();
  await page.goto('https://gate.example/r/winbet');
  const body = await page.content();
  expect(body.replace(/\s/g, '')).toBe('<html><head></head><body></body></html>');
});

5.5 验收签字

角色 验收焦点 签字
运维 品牌 301 + 零内容 + 桶 + 监控
技术 池合并 + 双检 + 接口 + SDK + 探活
测试 P0 全过
产品 口径与流程 · 成功率 KPI
风控 IP/fp 阈值

附录

A. RACI

事项 产品 技术 运维 测试
需求与口径 A C C C
品牌 301 / 零内容 C C A/R R
Gate 双检 + Resolve C A/R C R
域名池合并 C A/R I R
URL 桶 / CDN C C A/R R
探活 Worker C A/R C R
App 签名网关 C A/R I R
用例与验收 A C C R

B. 开放问题

# 问题 决策人
Q1 ip/fp 阈值 60 是否按地区分档 风控+产品
Q2 H5 密钥混淆方案 安全+前端
Q3 App 证书 pinning 是否强制 安全+客户端
Q4 维护页托管在 Gate 还是独立域 产品+运维

D. 名词数据字典(Glossary)

用途: 产品 / 运维 / 前端 / 后端 / App / 测试 统一口径;工单、埋点、接口字段、会议沟通均以此为准。
约定: 「中文名」为口头沟通首选;「英文/代码名」为实现与 JSON 字段;勿混用 Gate 与落地域(Gate=服务,Landing=域名角色)。

D.1 域名角色(五层)

中文名 英文/代码 示例 FQDN 含义 谁维护 详见
品牌域 Brand winbet.com 对外广告/物料入口;仅 301,零业务 Body 运维 §2.2、§3.3
落地域 Landing entry.vty.net 中间层判定域;双检+resolve+probe 编排;判定失败 在本域报错 技术+运维 §2.2.1
站点主域 / 主站域 Site play-a.com 域名池内业务域;完整 H5/App WebView 业务;判定 成功后 才进入 技术+运维 §4.2
分享域 Share s-xx.com 短链解析;302 到主站域(默认不经落地域) 技术 §2.3.8
Gate 服务 Gate 部署在 落地域 上 风控+resolve API+bootstrap;不是独立域名层级,是落地域上的逻辑服务 技术 §4.3
管理域 Admin 内网 域名池/落地域/切流配置后台 技术 §4.2

易混辨析:

说法 正确理解 错误理解
「Gate 域」 通常指 落地域 FQDN(跑 Gate 服务) 不要把 Gate 与主站域混为一谈
「落地」 用户 到达落地域 或 Share 302 到主站 不等于「进入业务首页」
「主站 / 主域」 池内 is_primary 或当前 current_domain 不是 Brand,不是 Landing
「站点池 / 域名池」 site_domain_pool 中 Site 主域 集合 不含 Landing、Brand

D.2 入口与归因

中文名 字段/代码 类型 含义 取值 / 示例
入口类型 entry_type string 用户从哪条链路进入主站 brand / share / direct / app
品牌 ID brand_id string 配置中心品牌标识,非 FQDN winbet
品牌域名 brand_domain string 用户点击的 Brand FQDN(埋点) winbet.com
跳转凭证 hop_id string 第一跳签发、主站核销;防刷量、归因 hop_8f3k2j
分享码 share_code / {code} string Share 短链路径段 abc123
分享参数 entry=share query 标记分享进入 URL 参数
官方入口 URL official_brand_url string 直访失败兜底跳 Brand 配置中心下发
落地兜底 URL gate_fallback_url string 直访失败兜底跳落地域 bootstrap 配置中心下发
entry_type 中文 典型路径 是否经落地域
brand 品牌 H5 Brand→Landing→Site 是
share 分享 Share→Site 否(默认)
direct 主站直访 直接 play-*.com 否
app App App SDK→Site 否(App 内等效)

D.3 域名池(site_domain_pool)

中文名 字段 类型 含义 取值
站点域名 domain string 主站 FQDN,唯一 play-a.com
状态 status enum 运营状态 active / standby / disabled / blocked
健康 health enum 探活结果 ok / fail / unknown
渠道 channels array 哪些端可用 h5 / app / pc / all
权重 weight int Resolve 排序,1–100 越大越优先
主站标记 is_primary bool 分享默认、展示用主域 true/false
连续失败次数 fail_streak int 探活连续失败计数 ≥3 摘除
最近探活时间 last_probe_at timestamp Worker 上次探测 —
操作人 operator string 人工变更审计 —
池版本 resolve_version int/string 池变更版本(埋点) 配置递增

D.4 客户端/SDK 运行时对象

中文名 变量/键名 含义 持久化
域名列表 domain_list 解密后最多 3 个主站 FQDN,probe 顺序 resolve_meta 内
当前主站 current_domain 正在使用的 Site 基址 session + Keychain
首选主站 primary 服务端指定首选(通常=list[0]) resolve_meta
Resolve 缓存 resolve_meta 加密包、过期时间、hop 等 H5: vty_ds_v1;App: Keychain
探活缓存 probe_cache 每域最近 probe 结果,TTL 60s 内存
本地拉黑 local_bad 当前会话内暂时不可用的主站域 内存,60s
落地阶段 phase landing / site;决定报错归属域 SDK 内存
SDK 配置 CFG 超时、重试次数等常量 §4.6.9

resolve_meta 结构:

{
  "payload": "<加密串>",
  "expires_at": 1735689900,
  "hop_id": "hop_8f3k2j",
  "key_id": "k2026-03",
  "resolved_at": 1735689600,
  "domain_list": ["play-a.com", "play-b.com", "play-c.com"]
}

D.5 接口与 API

中文名 路径/服务 调用方 含义
Gate Resolve POST /api/v1/gate/resolve H5 落地页 bootstrap 双检通过后下发 加密 主站列表
App Resolve POST /api/v1/app/resolve App 签名通过下发加密主站列表
App Bootstrap GET /api/v1/app/bootstrap App Splash 版本关、强更、brand_pack
当前主站查询 GET /api/v1/resolve/current-site Share 服务 与 Resolve 同源 primary
主站健康检查 HEAD/GET /__health SDK / Worker 200 + {"ok":true}
Share 短链 GET /{code} 用户浏览器 302 到主站落地页
落地 bootstrap 页 GET /r/{brandId} 用户浏览器 落地域极简 HTML+SDK
响应码 中文 含义
code=0 成功 正常业务数据
40301 IP 风控拒绝 不下发域名;落地域空页
40302 指纹风控拒绝 同上
40310 强制更新 App 版本过低;不可 resolve
50301 池枯竭 无 healthy 主站;落地域全屏维护

D.6 风控与评分

中文名 字段/规则 ID 含义 阈值
IP 判定 ip_verdict NORMAL / BLOCK / 低分 见 §4.3.1
IP 分数 ip_score 0–100,越高越可信 ≥60 通过
指纹判定 fp_verdict 同 IP 见 §4.3.2
指纹分数 fp_score 0–100 ≥60 通过
指纹哈希 fp_hash 浏览器指纹摘要 H5 必填
双检 — IP 且 指纹 均 NORMAL 才 Resolve AND
硬拒绝 IP-H* / FP-H* 命中即空页,不算分 —
App 签名 X-App-Sign HMAC 证明合法客户端 缺/错→403
限流 rate_limit 同 IP/device 频率 429

D.7 加密与密钥

中文名 字段 含义
加密载荷 payload Base64(iv+ciphertext+tag)
密钥 ID key_id 指向 AES-256 密钥,如 k2026-03
过期时间 expires_at 防重放;过期需重拉 resolve
TTL ttl_sec 通常 300s
随机数 nonce 单次 resolve 唯一,Redis 去重 5min
请求签名 sig HMAC(brand_id|ts|nonce|fp_hash)

D.8 SDK 状态机

中文名 状态代码 含义
初始化 INIT 冷启动
读缓存 LOAD_CACHED 有未过期 resolve_meta
拉 Resolve FETCH_RESOLVE 请求 gate/app resolve
探活列表 PROBE_LIST 顺序 HEAD 主站 __health
进入主站 ENTER_SITE location.replace 完成
运行中 RUNNING 主站业务
重拉 Resolve RETRY_RESOLVE probe 全失败后再 resolve
维护 UI MAINTENANCE_UI 全屏不可进
故障切换 FAILOVER 主站域间切换

D.9 前端异常文案(UI ID)

ID 中文摘要 归属域 UI 形态
E-GATE-01 风控空页(无文案) 落地域 EMPTY
E-LOAD-01 正在连接最佳线路… 落地域 TOP_LOADING
E-PROBE-01 切换备用线路… 落地域 TOAST
E-RESOLVE-01 暂时无法连接 落地域(未进站) FULL_BLOCK
E-DECRYPT-01 加载失败 落地域 FULL_BLOCK
E-RATE-01 访问过于频繁 落地域 FULL_BLOCK
E-NET-01 已切换备用线路 主站域 TOAST
E-APP-FORCE-01 请更新至最新版本 App 全屏 FULL_BLOCK
E-SSL-01 证书异常切线 落地域 probe 阶段 TOAST

i18n 前缀:gate.*(H5 落地/主站壳)、app.*(App 强更)。已支持语言: zh-CN、vi-VN(§4.10.2)。


D.10 App 版本与皮肤

中文名 字段 含义
最低版本 min_version 低于此 40310,永久拦截
最新版本 latest_version 配合 force_update
强制更新开关 force_update 切流日 true
品牌皮肤包 brand_pack / brand_pack_version 主题 Logo 启动图 bundle
必需皮肤版本 required_brand_pack 本地低于此→强更
切流包标识 cutover_bundle 如 2026-brand-v2,审计用
更新地址 update_url App Store / APK 链接

D.11 埋点事件(核心)

事件名 关键字段 含义
brand_request brand, province, isp 品牌首跳
gate_check ip_score, fp_score, reason 落地域双检
resolve_result domain_count, code Resolve 下发
resolve_fetch round, latency_ms 重试轮次
domain_probe try_order, domain, ok 探活
domain_switch from_domain, to_domain FAILOVER
entry_success site_domain, hop_id, total_ms KPI 成功进入
entry_fail fail_stage, reason KPI 失败归因

fail_stage 常用值: brand_dns / landing_unreachable / gate_ip / gate_fp / resolve / decrypt / probe / enter


D.12 运维与 CDN

中文名 术语 含义
URL 桶 bucket CDN 上隔离的静态/规则容器
品牌桶 bucket-brand-* 仅 301 规则
落地桶 bucket-landing-* bootstrap,无业务
主站桶 bucket-site-* 每主站域独立
零内容 — Brand Body 0 字节或极短
301 跳转 — Brand→Landing 唯一允许响应
探活 Worker — 30s 周期 HEAD 主站 __health
摘除 disabled 连续 3 败自动下线

D.13 缩写与项目代号

缩写 全称 含义
PRD Product Requirements Document 本文档
KPI Key Performance Indicator 成功进入率等
FQDN Fully Qualified Domain Name 完整域名
GCM Galois/Counter Mode AES 加密模式
HMAC Hash-based MAC 签名算法
SDK Software Development Kit 域名防护客户端库
FAILOVER — 主站域故障自动切换
PROBE — 对 __health 的探活请求
P0/P1 Priority 测试/告警优先级

D.14 端到端一句话(各端对齐用)

角色 一句话
产品 用户点 Brand → 落地域判定 → 成功才进 主站池;失败报错 留在落地域。
运维 Brand 只 301 到 落地域;主站池每域独立桶;Landing 与 Site 证书分离。
前端 H5 bootstrap 跑在 Landing;phase=landing 时所有 E-* 归属落地 hostname。
后端 Gate Resolve 只查 site_domain_pool;Landing 不在池内;40301/40302/50301 定案。
App 先 bootstrap 版本关,再 app/resolve;UI 在 App 内,逻辑同 H5 probe。
测试 TC-L* 验证报错域;TC-B5 验证 301 不指向 play-*.com。

C. 修订记录

版本 日期 说明
v3.0 2026-09-02 §2.8 安全审视(威胁模型、SEC-01~14、TC-SEC);评审入口「安全」Tab
v2.9 2026-09-02 §4.10.2 前端异常文案 vi-VN 越南语
v2.8 2026-09-02 附录 §D 名词数据字典
v2.7 2026-09-02 §2.2 落地域中间层;判定失败留落地域报错、成功进主站域;§4.10.5 报错归属;TC-L1~4
v2.6 2026-09-02 §2.3.10 App 版本校验;§2.7 切流+强更;§4.4.5 bootstrap
v2.5 2026-09-02 §2.3 扩充分享/主站直访/App 全流程;四入口对照表;TC-SH14、TC-DIR13
v2.4 2026-09-02 §2.3 品牌 H5 五阶段完整流程:时序图、分支、解密、FAILOVER
v2.3 2026-09-02 §4.3.6/4.3.7 IP/指纹采集与判断;§4.10 前端文案+i18n;§4.11 轮询 UI 映射;H5 异常态 Demo;TC-UI1~7
v2.2 2026-09-02 §4.3 IP/指纹完整规则(硬拒绝+评分);§4.6 轮询状态机/SDK 常量;测试用例扩充
v2.1 2026-09-02 vty.vip 基线 ~75%、举报/扫描应对、KPI ≥88%
v2.0 2026-09-02 重构五章;双检+加密+池合并+App403
v1.0 2026-08-26 初稿:双段跳转、域名池、分享、埋点

文档结束。