Files
youle_app_ios_v2/ylgamehall/Source/Network/VersionResolver.swift
T
joywayerandClaude Opus 4.7 8125dd4d11 1.11 修订:VersionResolver 改单链 4 层 fallback,删除双子树合并算法(契约影响)
【契约影响】
- RemoteConfig 模型:Agent 节点移除 channellist 字段(线上 JSON 实际只挂 gamelist;此前误读 chulishengji 引入的 channellist 是空 schema)
- VersionResolver 解析算法:从"双子树 + 字段级合并"改为"agentlist → gamelist → channellist → marketlist 单链 4 层 fallback",行为按 daoqi NewRootVC.m:689-810 主路径
- 对外 API ResolvedVersion / resolve(config:agentId:channelId:marketId:gameId:) 签名零变化,WebContainerViewController / SubGameViewController 调用点未动

【设计要点】
- 4 节点类型 conform 同一 private protocol RemoteConfigNode
- 5 字段共用 pickString / pickInt 两个倒序 fallback 工具,不允许为某字段单独写 if 链
- buildChain 集中处理"任一层 id 不匹配立刻截断"语义

【文档同步】
- Design §6.3.2 整段重写
- Plan ADR-008 顶部追加"第三轮修订(2026-06-27)"小节 + 第二轮误读复盘
- Plan ADR-008-D 替换为单链 4 层算法说明 + 保留修订史
- Plan §1.11 / ADR-008-I / 最后更新日期 / Verification-Checklist L73 同步

参考契约章节:docs/H5-Native-Implementation-Design.md §6.3.2、docs/Development-Plan.md ADR-008-D
BuildProject 通过。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-27 20:04:19 +08:00

152 lines
5.3 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)的纯函数实现。
//
// 契约结构(参 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<RemoteConfigNode, String?>
) -> 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<RemoteConfigNode, String?>
) -> Int {
for node in chain.reversed() {
if let s = node[keyPath: keyPath], let v = Int(s), v != 0 { return v }
}
return 0
}
}