Phase 1.10:RemoteConfigClient 完整版(HTTPS + cache-busting + 短文本 + FlexibleString)
第二轮调研 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 路径是热路径
This commit is contained in:
@@ -1083,7 +1083,9 @@ public struct Game: Codable, Sendable { ... }
|
||||
public struct Market: Codable, Sendable { ... }
|
||||
```
|
||||
|
||||
#### 6.3.2 VersionResolver(纯函数)
|
||||
#### 6.3.2 VersionResolver(chulishengji 双子树合并,纯函数)
|
||||
|
||||
**核心修正(ADR-008 二次精确化)**:线上热路径是 chulishengji 双子树合并,**不是**简单的"顶 → agent → channel → market"线性覆盖。
|
||||
|
||||
```swift
|
||||
// Source/Network/VersionResolver.swift
|
||||
@@ -1094,10 +1096,16 @@ public enum VersionResolver {
|
||||
public let appDownload: String?
|
||||
public let gameVersion: Int // 0 表示远端未指定
|
||||
public let gameZip: String?
|
||||
public let showmessage: String? // 非空时阻塞,运营杀手锏
|
||||
}
|
||||
|
||||
/// 4 级覆盖:顶层 → agent → channel → market,后者覆盖前者。
|
||||
/// agent 子树和 game 子树各自合并,game 子树最终优先(msext 'chulishengji' 中的语义)。
|
||||
/// chulishengji 双子树合并(NewRootVC.m:1372-1538):
|
||||
/// 1. 从 agent 子树提取(agent → channel → market → game,game 嵌在 market 命中后才遍历)
|
||||
/// 2. 从 game 子树提取(game → game-self → channel → market)
|
||||
/// 3. 字段级合并:
|
||||
/// - IPA(appVersion / appDownload)默认 agent 赢;game 版本号更大时 game 赢(line 1416)
|
||||
/// - zip(gameVersion / gameZip)默认 game 赢;agent 版本号更大时 agent 赢(line 1456)
|
||||
/// 4. showmessage 两条子树多层多次覆盖,最后写赢
|
||||
public static func resolve(
|
||||
config: RemoteConfig,
|
||||
agentId: String,
|
||||
@@ -1105,13 +1113,71 @@ public enum VersionResolver {
|
||||
marketId: String,
|
||||
gameId: String
|
||||
) -> Resolved {
|
||||
// pure function: 不依赖任何 actor / global
|
||||
// 实现:reduce over [top, agent, channel, market]
|
||||
...
|
||||
// 1. 找到当前 agent / channel / market / game 节点
|
||||
guard let agent = config.agentlist?.first(where: { $0.agentid == agentId }),
|
||||
let game = agent.gamelist?.first(where: { $0.gameid == gameId }) else {
|
||||
// 找不到匹配(极端情况):返回 0 / nil,让 WebContainer 走"无升级"路径
|
||||
return Resolved(appVersion: 0, appDownload: nil,
|
||||
gameVersion: 0, gameZip: nil,
|
||||
showmessage: config.showmessage)
|
||||
}
|
||||
|
||||
let agentExtract = extractAgentSubtree(
|
||||
agent: agent, channelId: channelId, marketId: marketId, gameId: gameId)
|
||||
let gameExtract = extractGameSubtree(
|
||||
game: game, channelId: channelId, marketId: marketId)
|
||||
|
||||
// IPA 合并(NewRootVC.m:1408-1447)
|
||||
let (appVer, appDL) = mergeIPA(agent: agentExtract, game: gameExtract)
|
||||
// zip 合并(NewRootVC.m:1450-1492)
|
||||
let (gameVer, gameZip) = mergeZip(agent: agentExtract, game: gameExtract)
|
||||
// showmessage:任意子树非空就用(最后写赢)
|
||||
let msg = gameExtract.showmessage ?? agentExtract.showmessage ?? config.showmessage
|
||||
|
||||
return Resolved(
|
||||
appVersion: appVer,
|
||||
appDownload: appDL,
|
||||
gameVersion: gameVer,
|
||||
gameZip: gameZip,
|
||||
showmessage: msg
|
||||
)
|
||||
}
|
||||
|
||||
/// agent 子树 4 层提取(getagentversion,NewRootVC.m:885-1013)
|
||||
/// agent → channel → market → game(game 嵌在 market 命中后才遍历)
|
||||
/// 注意:agent 子树用 game_download 字段名,game 子树用 game_zip——两个都要查
|
||||
private static func extractAgentSubtree(...) -> SubtreeResult { ... }
|
||||
|
||||
/// game 子树 4 层提取(getgameversion,NewRootVC.m:1015-1180)
|
||||
/// game → game-self(gamedata 自身再嵌 channellist)→ channel → market
|
||||
private static func extractGameSubtree(...) -> SubtreeResult { ... }
|
||||
|
||||
/// IPA 合并:默认 agent 赢;game.appVersion > agent.appVersion 时 game 赢
|
||||
private static func mergeIPA(...) -> (Int, String?) { ... }
|
||||
|
||||
/// zip 合并:默认 game 赢;agent.gameVersion > game.gameVersion 时 agent 赢
|
||||
private static func mergeZip(...) -> (Int, String?) { ... }
|
||||
}
|
||||
```
|
||||
|
||||
**关键测试用例**(单测必须覆盖):
|
||||
|
||||
| 用例 | 验证 |
|
||||
|------|------|
|
||||
| 顶层 / agent / channel / market / game 都有 app_version | agent 子树最深层赢;与 game 子树合并时按版本号大者 |
|
||||
| 仅 agent 子树有,game 子树无 app_version | 取 agent |
|
||||
| 仅 game 子树有,agent 子树无 app_version | 取 game |
|
||||
| game.appVersion = 10 vs agent.appVersion = 5 | game 赢 |
|
||||
| game.appVersion = 5 vs agent.appVersion = 10 | agent 赢 |
|
||||
| game.gameVersion = 10 vs agent.gameVersion = 5 | game 赢(默认) |
|
||||
| game.gameVersion = 5 vs agent.gameVersion = 10 | agent 赢 |
|
||||
| showmessage 仅顶层有 | 取顶层 |
|
||||
| showmessage agent 层有 + 顶层有 | 取 agent(深层覆盖浅层) |
|
||||
| showmessage market 层有 + 全部层都有 | 取 market(最深) |
|
||||
| agentid 找不到匹配 | 返回 default(0/nil),不抛 |
|
||||
| gameid 找不到匹配 | 返回 default,不抛 |
|
||||
| 字段名 game_download vs game_zip 混用 | 两个 key 都查,择一非空 |
|
||||
|
||||
#### 6.3.3 LocalVersionReader
|
||||
|
||||
```swift
|
||||
|
||||
Reference in New Issue
Block a user