第二轮调研 daoqi/NewRootVC.m 全文核对后,把对原项目远程配置流程的精确 理解固化进 ADR-008 + Design §6.3.2,并补齐 RemoteConfigClient 缺失的 3 个契约(cache-busting query、短文本响应识别、字段类型混乱兼容)。 核心修正:线上热路径是 chulishengji 双子树合并算法(NewRootVC.m:1372-1538), 不是 onnet 简单 4 层覆盖。viewWillAppear 与 gonet 都会强制把当前 agent 的 gamelist 提到 self.gamelist,只要服务器 JSON 含 agent.gamelist(线上 100% 都有), self.gamelist 就非 nil,走 else 分支 → chulishengji。本次烟雾测试实测: agent[0].gamelist = 7 个,完美印证 chulishengji 必要性。 - RemoteConfigClient 新增 cache-busting query:?vXXXXXXXXYYYYYYYY(两段 8 位 hex 连写,无 =,照搬 msext NewRootVC.m:1226 契约),每次重试都重新生成 - 新增 RemoteConfigOutcome enum:.parsed(RemoteConfig) 与 .shortText(String) 两种成功状态。短文本响应(trim 后 utf16.count ≤ 100)是服务端运营杀手锏 #1, msext 当 alert 文本弹窗 + 停止重试(NewRootVC.m:1239-1243) - UTF-8 解码 + trim 跟 msext NewRootVC.m:1237-1238 严格对齐 - 所有 String? 字段改用 decodeFlexibleStringIfPresent 扩展(兼容 number-as-string): agentid / channelid / marketid / gameid / showmessage / appVersion / appDownload / gameVersion / gameZip 全部 9 字段。msext 用 NSNumber.intValue/description 静默 吞下 number,新外壳 Codable 严格类型必须显式处理 - 远端 URL 从 http 改为 https(后台已切,无需 ATS 例外) - Plan ADR-008 修订为含 9 个子节(A-I)的精确决策记录,含 chulishengji 双子树算法、agent 子树结构、game 子树结构、字段级合并优先级、决策顺序、 与 msext 差异表 - Design §6.3.2 VersionResolver 重写:从"简单 4 层 reduce"改为"chulishengji 双子树合并 + 字段级合并",附 13 项单测覆盖矩阵 - RootViewController 烟雾测试切换为 outcome enum switch case,覆盖 .parsed 与 .shortText 两条路径 - 实测烟雾:HTTPS 拉到 30 KB JSON 解析 .parsed 成功(耗时 2.7s 含 TLS 握手), agent[0].gamelist 含 7 个 game 节点,证明 chulishengji 路径是热路径
121 lines
4.8 KiB
Swift
121 lines
4.8 KiB
Swift
//
|
||
// RootViewController.swift
|
||
// ylgamehall
|
||
//
|
||
// M0 阶段临时根控制器,后续将被 Coordinator + LobbyViewController 替换。
|
||
//
|
||
|
||
import UIKit
|
||
|
||
final class RootViewController: UIViewController {
|
||
|
||
override func viewDidLoad() {
|
||
super.viewDidLoad()
|
||
view.backgroundColor = .black
|
||
|
||
// Phase 1.2 烟雾测试:打印 ChannelConfig.plist 的 11 项渠道注入值
|
||
let cfg = BundleConfig.shared
|
||
print("""
|
||
[BundleConfig] 渠道注入读取结果:
|
||
qiniuDomain = \(cfg.qiniuDomain)
|
||
gameId = \(cfg.gameId)
|
||
channel = \(cfg.channel)
|
||
gameDir = \(cfg.gameDir)
|
||
gameStart = \(cfg.gameStart)
|
||
gameConfig = \(cfg.gameConfig)
|
||
market = \(cfg.market)
|
||
agent = \(cfg.agent)
|
||
appVersion = \(cfg.appVersion)
|
||
other = "\(cfg.other)"
|
||
appleConfig = "\(cfg.appleConfig)"
|
||
""")
|
||
|
||
// Phase 1.3 烟雾测试:打印 SandboxPaths 计算路径
|
||
print("""
|
||
[SandboxPaths] 路径计算结果:
|
||
caches = \(SandboxPaths.caches.path)
|
||
documents = \(SandboxPaths.documents.path)
|
||
bundle = \(SandboxPaths.bundle.path)
|
||
lobbyRoot = \(SandboxPaths.lobbyRoot.path)
|
||
lobbyIndex = \(SandboxPaths.lobbyIndex.path)
|
||
lobbyVersionXML = \(SandboxPaths.lobbyVersionXML.path)
|
||
""")
|
||
|
||
// Phase 1.5 烟雾测试:触发 ResourceUnzipper,确认 lobby index.html 存在
|
||
Task {
|
||
let start = Date()
|
||
do {
|
||
try await ResourceUnzipper.shared.ensureReady()
|
||
let elapsed = Date().timeIntervalSince(start)
|
||
let exists = FileManager.default.fileExists(atPath: SandboxPaths.lobbyIndex.path)
|
||
print("""
|
||
[ResourceUnzipper] ensureReady 完成(耗时 \(String(format: "%.3f", elapsed))s)
|
||
lobbyIndex 存在: \(exists)
|
||
""")
|
||
} catch {
|
||
print("[ResourceUnzipper] ERROR: \(error)")
|
||
}
|
||
}
|
||
|
||
// Phase 1.8 烟雾测试:确认 WebViewJavascriptBridge.js 已入 Bundle 且可读
|
||
// 注:synchronized group 会把 ylgamehall/Resources/JS/ 子目录扁平化为 bundle 根,
|
||
// 所以无需 subdirectory 参数。
|
||
if let url = Bundle.main.url(forResource: "WebViewJavascriptBridge",
|
||
withExtension: "js"),
|
||
let content = try? String(contentsOf: url, encoding: .utf8) {
|
||
print("""
|
||
[WVJB.js] 已就绪
|
||
path = \(url.path)
|
||
bytes = \(content.utf8.count)
|
||
head = \(content.prefix(60).replacingOccurrences(of: "\n", with: " ⏎ "))…
|
||
""")
|
||
} else {
|
||
print("[WVJB.js] ERROR: 找不到 Bundle 内 JS/WebViewJavascriptBridge.js")
|
||
}
|
||
|
||
// Phase 1.10 烟雾测试:拉远端渠道配置 .txt(含 cache-busting + 短文本响应识别)
|
||
Task {
|
||
let start = Date()
|
||
do {
|
||
let outcome = try await RemoteConfigClient.shared.fetch()
|
||
let elapsed = Date().timeIntervalSince(start)
|
||
switch outcome {
|
||
case .parsed(let cfg):
|
||
print("""
|
||
[RemoteConfigClient] 拉取成功 .parsed(耗时 \(String(format: "%.3f", elapsed))s)
|
||
showmessage = \(cfg.showmessage ?? "<nil>")
|
||
agentlist count = \(cfg.agentlist?.count ?? 0)
|
||
""")
|
||
if let first = cfg.agentlist?.first {
|
||
print("""
|
||
agent[0].agentid = \(first.agentid ?? "<nil>")
|
||
agent[0].appVersion = \(first.appVersion ?? "<nil>")
|
||
agent[0].gameVersion = \(first.gameVersion ?? "<nil>")
|
||
agent[0].channellist = \(first.channellist?.count ?? 0) 个
|
||
agent[0].gamelist = \(first.gamelist?.count ?? 0) 个
|
||
""")
|
||
}
|
||
case .shortText(let msg):
|
||
print("""
|
||
[RemoteConfigClient] 拉到短文本响应(耗时 \(String(format: "%.3f", elapsed))s)
|
||
运营杀手锏:应当弹窗显示并停止重试
|
||
内容 = "\(msg)"
|
||
""")
|
||
}
|
||
} catch {
|
||
print("[RemoteConfigClient] ERROR: \(error)")
|
||
}
|
||
}
|
||
}
|
||
|
||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||
.landscape
|
||
}
|
||
|
||
override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
|
||
.landscapeRight
|
||
}
|
||
|
||
override var prefersStatusBarHidden: Bool { false }
|
||
}
|