// // VersionResolver.swift // ylgamehall // // 把远端 RemoteConfig 化简为 7 个目标字段(appVersion / appDownload / // gameVersion / gameZip / showmessage / audioDomain / audioBucket)的纯函数实现。 // // 契约结构(参 daoqi `NewRootVC.m:689-810` 主路径,扩展为 5 层 fallback): // root(RemoteConfig 顶层) // → agentlist[agentid] → gamelist[gameid] // → channellist[channelid] → marketlist[marketid] // // 查找语义:从最深层(market)向上回退取第一个非空值,"根"(顶层)作为最浅一层 // 自动包含在链中 —— 等价于 msext 那段从根向下"非空即覆盖"的 if 链,但写法更直接、 // 可复用,且天然支持顶层声明字段(任何字段都能放顶层兜底)。 // // 统一接口:所有 7 个字段都用同一套 `pickString` / `pickInt` 倒序查找, // 无任何字段级特殊兜底逻辑(如 `?? config.xxx`),所有字段平等。 // import Foundation // MARK: - 决策输出 /// 4 层 fallback 解析后的所有目标字段(所有字段统一走同一套 chain + pickString/Int)。 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? /// 七牛 CDN 域名(不带 http:// 前缀),录音上传后拼公开访问 URL 用。nil 表示远端未指定。 public let audioDomain: String? /// 七牛 bucket 名(putPolicy.scope)。nil 表示远端未指定。 public let audioBucket: 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 } var audioDomain: String? { get } var audioBucket: String? { get } } extension Agent: RemoteConfigNode {} extension Game: RemoteConfigNode {} extension Channel: RemoteConfigNode {} extension Market: RemoteConfigNode {} // RemoteConfig 顶层也作为节点参与 fallback 链,处于最浅一层("根")。 // 这样所有字段的 fallback 完全统一:节点链倒序找第一个非空 → 链上即包含根。 extension RemoteConfig: 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 ) // 所有 7 字段统一走"chain 倒序找第一个非空",无任何字段级特殊兜底。 // chain 头部就是 RemoteConfig 顶层("根"),最深层赢、根作为最浅层兜底。 return ResolvedVersion( appVersion: pickInt(chain, \.appVersion), appDownload: pickString(chain, \.appDownload), gameVersion: pickInt(chain, \.gameVersion), gameZip: pickString(chain, \.gameZip), showmessage: pickString(chain, \.showmessage), audioDomain: pickString(chain, \.audioDomain), audioBucket: pickString(chain, \.audioBucket) ) } // MARK: - 节点链匹配 /// 沿 root → agent → game → channel → market 顺序逐层匹配,遇到不匹配的层立刻截断。 /// 链头始终是 RemoteConfig 顶层("根"),作为最浅一层兜底。 /// /// 返回数组从根到叶有序,长度 1..5: /// - `[root]` :agent 未匹配(链最短为根) /// - `[root, agent]` :agent 命中、game 未匹配 /// - `[root, agent, game]` :再深一层未命中 /// - ... /// - `[root, agent, game, channel, market]`:全链命中 private static func buildChain( config: RemoteConfig, agentId: String, gameId: String, channelId: String, marketId: String ) -> [RemoteConfigNode] { var chain: [RemoteConfigNode] = [config] 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 } }