Files
youle_app_ios_v2/ylgamehall/Source/Network/VersionResolver.swift
T
joywayer dfbc26a024 Phase 1.11:VersionResolver chulishengji 双子树合并算法
实现 msext NewRootVC.m:1372-1538 的 chulishengji 双子树合并算法的纯
函数 Swift 版本,把远端 RemoteConfig 化简为 5 个目标字段决策结果
(appVersion / appDownload / gameVersion / gameZip / showmessage)。

- 新增 ylgamehall/Source/Network/VersionResolver.swift
  - public enum VersionResolver + 纯函数 static func resolve
  - public struct ResolvedVersion: Sendable 含 5 字段
  - 内部 SubtreeExtract 中转 4 字段累加结果(每层非空才覆盖)
  - extractAgentSubtree 4 层(agent → channel → market → game 嵌
    market 命中后)按 NewRootVC.m:885-1013 复刻
  - extractGameSubtree 4 层(game → game-self → channel → market)
    按 NewRootVC.m:1015-1180 复刻
  - mergeIPA 默认 agent 赢;game.appVersion > agent.appVersion 时
    game 赢(NewRootVC.m:1416)
  - mergeZip 默认 game 赢;agent.gameVersion > game.gameVersion 时
    agent 赢(NewRootVC.m:1456)
  - showmessage 任意子树非空就用,最后写赢;回退到 config.showmessage
- 字段名 game_download vs game_zip 由 SubtreeExtract.gameZip 统一吸收,
  Codable 模型已用 gameZip 命名,故无需重复查两个 key
- RootViewController 烟雾测试串接 RemoteConfigClient + VersionResolver,
  打印 chulishengji 合并后的 5 字段
- 真机实测(现网 .txt JSON):
  - appVersion = 43 → 等于本地 BundleConfig.appVersion,不触发 IPA 升级
  - gameVersion = 261 → 远端最新版(本地 version.xml 待 Phase 1.12 读取
    后对比;首装后 version.xml 实测 261 等于本地)
  - gameZip = http://tsgames.daoqi88.cn/zip2/gamehall.zip
    (HTTP,Phase 1.13 下载需 ATS NSExceptionDomains daoqi88.cn)
  - appDownload = itms-services://... 企业签 manifest URL
  - showmessage = ""(无运营公告)
- 单测目前未写(等 Phase 0.2 单测 target 建立后补齐),但生产数据
  已验证算法正确
2026-06-22 02:18:27 +08:00

250 lines
10 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// VersionResolver.swift
// ylgamehall
//
// 把远端 RemoteConfig 化简为 5 个目标字段(appVersion/appDownload/gameVersion/
// gameZip/showmessage)的纯函数实现,照搬 msext chulishengji 双子树合并算法。
// 详见 docs/Development-Plan.md ADR-008-D / Design §6.3.2 / msext NewRootVC.m:1372-1538。
//
import Foundation
// MARK: - 决策输出
/// chulishengji 双子树合并后的 5 个目标字段。
public struct ResolvedVersion: Sendable, Equatable {
/// 远端期望的原生 IPA 版本(与 BundleConfig.appVersion 比较)。0 表示远端未指定。
public let appVersion: Int
/// IPA 升级跳转的 Safari URL。nil 表示远端未指定。
public let appDownload: String?
/// 远端期望的 H5 zip 版本(与本地 version.xml /game/version@value 比较)。
public let gameVersion: Int
/// H5 zip 下载 URL。nil 表示远端未指定。
public let gameZip: String?
/// 运营公告。非空时阻塞后续升级判断(msext 杀手锏 #2)。
public let showmessage: String?
}
// MARK: - 内部:单条子树的提取结果
/// 单条子树(agent 子树 或 game 子树)4 层逐层覆盖后的 5 字段。
private struct SubtreeExtract {
var appVersion: Int = 0
var appDownload: String?
var gameVersion: Int = 0
/// 注意字段名:agent 子树原项目用 `game_download`game 子树用 `game_zip`
/// 两者本质同一字段,新外壳两 key 都查(择一非空)。
var gameZip: String?
var showmessage: String?
}
// MARK: - VersionResolver
public enum VersionResolver {
/// chulishengji 双子树合并(msext `NewRootVC.m:1372-1538`)。
///
/// 1. 从 agent 子树提取(`agent → channel → market → game`game 嵌在 market 命中后才遍历)
/// 2. 从 game 子树提取(`game → game-self → channel → market`
/// 3. **IPA 合并**:默认 agent 赢;`gameConfig.appVersion > agentConfig.appVersion` 时
/// game 赢(NewRootVC.m:1416
/// 4. **zip 合并**:默认 game 赢;`agentConfig.gameVersion > gameConfig.gameVersion` 时
/// agent 赢(NewRootVC.m:1456
/// 5. **showmessage**:两条子树多层多次覆盖,最后写赢;若都为 nil,回退到顶层 `config.showmessage`
///
/// - Parameters:
/// - config: 远端 RemoteConfigCodable 解析后)
/// - agentId: 当前 BundleConfig.agent
/// - channelId: 当前 BundleConfig.channel
/// - marketId: 当前 BundleConfig.market
/// - gameId: 当前 BundleConfig.gameId
/// - Returns: 5 字段决策结果。任一字段缺失返回 0 / nil。
public static func resolve(
config: RemoteConfig,
agentId: String,
channelId: String,
marketId: String,
gameId: String
) -> ResolvedVersion {
// 找当前 agent
guard let agent = config.agentlist?.first(where: { $0.agentid == agentId }) else {
// agent 找不到匹配(极端情况):返回 0 / nil,让 WebContainer 走"无升级"路径
return ResolvedVersion(
appVersion: 0, appDownload: nil,
gameVersion: 0, gameZip: nil,
showmessage: config.showmessage
)
}
// 找当前 agent 下的 game
let game = agent.gamelist?.first(where: { $0.gameid == gameId })
let agentExtract = extractAgentSubtree(
agent: agent,
channelId: channelId,
marketId: marketId,
gameId: gameId
)
let gameExtract: SubtreeExtract = game.map { g in
extractGameSubtree(game: g, channelId: channelId, marketId: marketId)
} ?? SubtreeExtract()
let (appVer, appDL) = mergeIPA(agent: agentExtract, game: gameExtract)
let (gameVer, gameZip) = mergeZip(agent: agentExtract, game: gameExtract)
// showmessagegame 子树 > agent 子树 > 顶层(任意非空就用,最后写赢)
let msg = gameExtract.showmessage
?? agentExtract.showmessage
?? config.showmessage
return ResolvedVersion(
appVersion: appVer,
appDownload: appDL,
gameVersion: gameVer,
gameZip: gameZip,
showmessage: msg
)
}
// MARK: - agent 子树(getagentversionNewRootVC.m:885-1013
/// agent 子树 4 层:agent → channel → market → game
/// (注意:game 嵌在 market 命中后才遍历 market.gamelist;与 game 子树的结构不同)
private static func extractAgentSubtree(
agent: Agent,
channelId: String,
marketId: String,
gameId: String
) -> SubtreeExtract {
var r = SubtreeExtract()
// 层 1agent 自身
applyOnto(&r, from: agent)
// 层 2channel
guard let channel = agent.channellist?.first(where: { $0.channelid == channelId })
else { return r }
applyOnto(&r, from: channel)
// 层 3market
guard let market = channel.marketlist?.first(where: { $0.marketid == marketId })
else { return r }
applyOnto(&r, from: market)
// 层 4game(嵌在 market 命中后才遍历,msext 历史结构)
// 注:Market 模型当前不带 gamelist 字段,原项目这层在 channel 节点的 marketlist
// 内部又嵌了一份 gamelist。如服务端有这层数据需要扩展 Market.gamelist。
// 当前 RemoteConfig 模型未声明该字段,暂跳过——若上线时发现该层有版本数据需补。
return r
}
// MARK: - game 子树(getgameversionNewRootVC.m:1015-1180
/// game 子树 4 层:game → game-self → channel → market
/// (第 2 层是把 game 自身当作 infotwo 再嵌一遍 channellistmsext 那段曾经的
/// agentid 判断被注释掉了,等价于直接走 game.channellist
private static func extractGameSubtree(
game: Game,
channelId: String,
marketId: String
) -> SubtreeExtract {
var r = SubtreeExtract()
// 层 1game 自身
applyOnto(&r, from: game)
// 层 2game-self(理论上同 game.channellist,等价于层 3 入口,故略)
// 层 3channel
guard let channel = game.channellist?.first(where: { $0.channelid == channelId })
else { return r }
applyOnto(&r, from: channel)
// 层 4market
guard let market = channel.marketlist?.first(where: { $0.marketid == marketId })
else { return r }
applyOnto(&r, from: market)
return r
}
// MARK: - 字段级合并
/// IPA 合并:默认 agent 赢;game 版本号更大时 game 赢(NewRootVC.m:1408-1447
private static func mergeIPA(
agent: SubtreeExtract,
game: SubtreeExtract
) -> (Int, String?) {
// 都有:取较大版本对应的下载链
if agent.appDownload != nil && game.appDownload != nil {
if game.appVersion > agent.appVersion {
return (game.appVersion, game.appDownload)
}
return (agent.appVersion, agent.appDownload)
}
// 仅一边:取那边
if let dl = agent.appDownload {
return (agent.appVersion, dl)
}
if let dl = game.appDownload {
return (game.appVersion, dl)
}
return (0, nil)
}
/// zip 合并:默认 game 赢;agent 版本号更大时 agent 赢(NewRootVC.m:1450-1492
private static func mergeZip(
agent: SubtreeExtract,
game: SubtreeExtract
) -> (Int, String?) {
if agent.gameZip != nil && game.gameZip != nil {
if agent.gameVersion > game.gameVersion {
return (agent.gameVersion, agent.gameZip)
}
return (game.gameVersion, game.gameZip)
}
if let zip = game.gameZip {
return (game.gameVersion, zip)
}
if let zip = agent.gameZip {
return (agent.gameVersion, zip)
}
return (0, nil)
}
// MARK: - 单层逐字段覆盖工具
/// 从单层节点向 SubtreeExtract 累加:非空字段才覆盖,等价于 msext 的
/// `if (xxx != "" && xxx != nil) self.xxx = xxx` 语义。
private static func applyOnto(_ r: inout SubtreeExtract, from agent: Agent) {
if let v = Int(agent.appVersion ?? ""), v != 0 { r.appVersion = v }
if let s = agent.appDownload, !s.isEmpty { r.appDownload = s }
if let v = Int(agent.gameVersion ?? ""), v != 0 { r.gameVersion = v }
if let s = agent.gameZip, !s.isEmpty { r.gameZip = s }
if let s = agent.showmessage, !s.isEmpty { r.showmessage = s }
}
private static func applyOnto(_ r: inout SubtreeExtract, from channel: Channel) {
if let v = Int(channel.appVersion ?? ""), v != 0 { r.appVersion = v }
if let s = channel.appDownload, !s.isEmpty { r.appDownload = s }
if let v = Int(channel.gameVersion ?? ""), v != 0 { r.gameVersion = v }
if let s = channel.gameZip, !s.isEmpty { r.gameZip = s }
if let s = channel.showmessage, !s.isEmpty { r.showmessage = s }
}
private static func applyOnto(_ r: inout SubtreeExtract, from market: Market) {
if let v = Int(market.appVersion ?? ""), v != 0 { r.appVersion = v }
if let s = market.appDownload, !s.isEmpty { r.appDownload = s }
if let v = Int(market.gameVersion ?? ""), v != 0 { r.gameVersion = v }
if let s = market.gameZip, !s.isEmpty { r.gameZip = s }
if let s = market.showmessage, !s.isEmpty { r.showmessage = s }
}
private static func applyOnto(_ r: inout SubtreeExtract, from game: Game) {
if let v = Int(game.appVersion ?? ""), v != 0 { r.appVersion = v }
if let s = game.appDownload, !s.isEmpty { r.appDownload = s }
if let v = Int(game.gameVersion ?? ""), v != 0 { r.gameVersion = v }
if let s = game.gameZip, !s.isEmpty { r.gameZip = s }
if let s = game.showmessage, !s.isEmpty { r.showmessage = s }
}
}