// // VersionResolver.swift // ylgamehall // // 把远端 RemoteConfig 化简为 5 个目标字段(appVersion/appDownload/gameVersion/ // gameZip/showmessage)的纯函数实现。 // // 契约结构(参 daoqi `NewRootVC.m:689-810` 主路径): // agentlist[agentid] → gamelist[gameid] → channellist[channelid] → marketlist[marketid] // // 查找语义:从最深层(market)向上回退取第一个非空值,等价于 // msext 那段从根向下"非空即覆盖"的 if 链,但写法更直接、可复用。 // // 统一接口:所有 5 个字段都用同一套 `pickString` / `pickInt` 倒序查找, // 不再有"双子树合并 / chulishengji"等历史包袱的多处实现。 // import Foundation // MARK: - 决策输出 /// 4 层 fallback 解析后的 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: - 节点协议(4 个层级共用同一查找接口) /// `Agent` / `Game` / `Channel` / `Market` 都暴露相同字段集合, /// 由 VersionResolver 用同一套倒序 fallback 算法统一处理。 private protocol RemoteConfigNode { var appVersion: String? { get } var appDownload: String? { get } var gameVersion: String? { get } var gameZip: String? { get } var showmessage: String? { get } } extension Agent: RemoteConfigNode {} extension Game: RemoteConfigNode {} extension Channel: RemoteConfigNode {} extension Market: RemoteConfigNode {} // MARK: - VersionResolver public enum VersionResolver { /// 4 层 fallback 解析。 /// /// - Parameters: /// - config: 远端 RemoteConfig /// - agentId/channelId/marketId/gameId: 当前渠道注入值(BundleConfig) /// - Returns: 5 字段决策结果。任一字段缺失返回 0 / nil。 public static func resolve( config: RemoteConfig, agentId: String, channelId: String, marketId: String, gameId: String ) -> ResolvedVersion { let chain = buildChain( config: config, agentId: agentId, gameId: gameId, channelId: channelId, marketId: marketId ) return ResolvedVersion( appVersion: pickInt(chain, \.appVersion), appDownload: pickString(chain, \.appDownload), gameVersion: pickInt(chain, \.gameVersion), gameZip: pickString(chain, \.gameZip), // showmessage:链上任一层都不写 → 回退顶层 config.showmessage showmessage: pickString(chain, \.showmessage) ?? config.showmessage ) } // MARK: - 节点链匹配 /// 沿 agent → game → channel → market 顺序逐层匹配,遇到不匹配的层立刻截断。 /// /// 返回数组从根到叶有序,长度 0..4: /// - `[]` :agent 未匹配 /// - `[agent]` :agent 命中、game 未匹配 /// - `[agent, game]` :再深一层未命中 /// - ... /// - `[agent, game, channel, market]`:全链命中 private static func buildChain( config: RemoteConfig, agentId: String, gameId: String, channelId: String, marketId: String ) -> [RemoteConfigNode] { var chain: [RemoteConfigNode] = [] guard let agent = config.agentlist?.first(where: { $0.agentid == agentId }) else { return chain } chain.append(agent) guard let game = agent.gamelist?.first(where: { $0.gameid == gameId }) else { return chain } chain.append(game) guard let channel = game.channellist?.first(where: { $0.channelid == channelId }) else { return chain } chain.append(channel) guard let market = channel.marketlist?.first(where: { $0.marketid == marketId }) else { return chain } chain.append(market) return chain } // MARK: - 字段倒序 fallback(最深层赢) /// 从最深层(market)向根(agent)回退,取第一个非空字符串。 private static func pickString( _ chain: [RemoteConfigNode], _ keyPath: KeyPath ) -> String? { for node in chain.reversed() { if let s = node[keyPath: keyPath], !s.isEmpty { return s } } return nil } /// 从最深层向根回退,取第一个能解析为非零整数的字段值。 /// 字段在 JSON 里可能是 string / int / double 之一(详见 `decodeFlexibleStringIfPresent`), /// 解析失败 / 为 0 视同未提供继续向上找。 private static func pickInt( _ chain: [RemoteConfigNode], _ keyPath: KeyPath ) -> Int { for node in chain.reversed() { if let s = node[keyPath: keyPath], let v = Int(s), v != 0 { return v } } return 0 } }