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:
joywayer
2026-06-22 02:14:57 +08:00
parent 5f032342e7
commit 1bc287bafc
4 changed files with 526 additions and 36 deletions
+72 -6
View File
@@ -1083,7 +1083,9 @@ public struct Game: Codable, Sendable { ... }
public struct Market: Codable, Sendable { ... }
```
#### 6.3.2 VersionResolver(纯函数)
#### 6.3.2 VersionResolverchulishengji 双子树合并,纯函数)
**核心修正(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 gamegame market
/// 2. game game game-self channel market
/// 3.
/// - IPAappVersion / appDownload agent game game line 1416
/// - zipgameVersion / 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 getagentversionNewRootVC.m:885-1013
/// agent channel market gamegame market
/// agent game_download game game_zip
private static func extractAgentSubtree(...) -> SubtreeResult { ... }
/// game 4 getgameversionNewRootVC.m:1015-1180
/// game game-selfgamedata 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 找不到匹配 | 返回 default0/nil),不抛 |
| gameid 找不到匹配 | 返回 default,不抛 |
| 字段名 game_download vs game_zip 混用 | 两个 key 都查,择一非空 |
#### 6.3.3 LocalVersionReader
```swift