fallback 链头嵌入根(RemoteConfig 顶层):所有 7 字段真正统一,删字段级特殊兜底
【动机】 上一版"统一接口"虽然合并了 resolve / resolveAudio,但 resolve 内部仍有不对称: - showmessage / audioDomain / audioBucket: pickString(chain) ?? config.xxx - appVersion / appDownload / gameVersion / gameZip: 只 pickInt/String(chain), 顶层声明这 4 个字段也读不到 【方案】 - RemoteConfig 加 4 个 String? 字段(appVersion/appDownload/gameVersion/gameZip) 配齐与节点 struct 同款 7 字段集合 - extension RemoteConfig: RemoteConfigNode(自然 conform) - buildChain 首行 var chain = [config]:根始终作为最浅一层 - resolve() 去掉 ?? config.xxx 三处特殊兜底,全 7 字段一律 pickString/Int(chain) 【效果】 - 节点链 1..5 长(含根);所有字段一视同仁,pickString 倒序找即天然包含顶层 - 任何字段都可以在顶层 / agent / game / channel / market 任一层声明,最深层赢 - 算法对称、代码无字段级特殊分支 【硬约束(Plan ADR-009 同步)】 新增字段:加进协议 + 5 个 struct(含 RemoteConfig)+ ResolvedVersion + resolve() 内一行 pickString/Int(chain, \.xxx);不允许 ?? config.xxx 类回退。 BuildProject 通过。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
485f0c6679
commit
692c849e0d
@@ -10,19 +10,25 @@ import Foundation
|
||||
|
||||
// MARK: - Codable 模型(单链 4 层嵌套:agent → game → channel → market)
|
||||
|
||||
/// 远端配置顶层。所有字段(appVersion / appDownload / gameVersion / gameZip /
|
||||
/// showmessage / audioDomain / audioBucket)都可在顶层声明 ——作为节点链最浅一层
|
||||
/// 参与 4 层 fallback 兜底,与 Agent/Game/Channel/Market 同款语义。
|
||||
nonisolated public struct RemoteConfig: Codable, Sendable {
|
||||
public let appVersion: String?
|
||||
public let appDownload: String?
|
||||
public let gameVersion: String?
|
||||
public let gameZip: String?
|
||||
public let showmessage: String?
|
||||
/// 七牛 CDN 域名(**不带 http:// 前缀**),录音上传后拼公开访问 URL 用。
|
||||
/// 顶层值作为整链最浅层 fallback;与版本字段同款 4 层 fallback:
|
||||
/// agent → game → channel → market 任一层均可声明,最深层赢;都不写则用顶层。
|
||||
/// 启动期由 VersionResolver.resolveAudio(...) 解析,缺失抛 BootError.audioConfigMissing。
|
||||
public let audioDomain: String?
|
||||
/// 七牛 bucket 名(putPolicy.scope)。语义同 audioDomain,同款 4 层 fallback。
|
||||
public let audioBucket: String?
|
||||
public let agentlist: [Agent]?
|
||||
|
||||
public init(from decoder: any Decoder) throws {
|
||||
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||
appVersion = try c.decodeFlexibleStringIfPresent(forKey: .appVersion)
|
||||
appDownload = try c.decodeFlexibleStringIfPresent(forKey: .appDownload)
|
||||
gameVersion = try c.decodeFlexibleStringIfPresent(forKey: .gameVersion)
|
||||
gameZip = try c.decodeFlexibleStringIfPresent(forKey: .gameZip)
|
||||
showmessage = try c.decodeFlexibleStringIfPresent(forKey: .showmessage)
|
||||
audioDomain = try c.decodeFlexibleStringIfPresent(forKey: .audioDomain)
|
||||
audioBucket = try c.decodeFlexibleStringIfPresent(forKey: .audioBucket)
|
||||
@@ -31,8 +37,10 @@ nonisolated public struct RemoteConfig: Codable, Sendable {
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
// 驼峰 case 名(不带 rawValue),依赖 JSONDecoder.convertFromSnakeCase 自动把
|
||||
// JSON 的 audio_domain / audio_bucket 归一化到 audioDomain / audioBucket
|
||||
case showmessage, agentlist, audioDomain, audioBucket
|
||||
// JSON 的 app_version / audio_domain 等归一化为 appVersion / audioDomain
|
||||
case showmessage, agentlist
|
||||
case appVersion, appDownload, gameVersion, gameZip
|
||||
case audioDomain, audioBucket
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,17 +2,20 @@
|
||||
// VersionResolver.swift
|
||||
// ylgamehall
|
||||
//
|
||||
// 把远端 RemoteConfig 化简为 5 个目标字段(appVersion/appDownload/gameVersion/
|
||||
// gameZip/showmessage)的纯函数实现。
|
||||
// 把远端 RemoteConfig 化简为 7 个目标字段(appVersion / appDownload /
|
||||
// gameVersion / gameZip / showmessage / audioDomain / audioBucket)的纯函数实现。
|
||||
//
|
||||
// 契约结构(参 daoqi `NewRootVC.m:689-810` 主路径):
|
||||
// agentlist[agentid] → gamelist[gameid] → channellist[channelid] → marketlist[marketid]
|
||||
// 契约结构(参 daoqi `NewRootVC.m:689-810` 主路径,扩展为 5 层 fallback):
|
||||
// root(RemoteConfig 顶层)
|
||||
// → agentlist[agentid] → gamelist[gameid]
|
||||
// → channellist[channelid] → marketlist[marketid]
|
||||
//
|
||||
// 查找语义:从最深层(market)向上回退取第一个非空值,等价于
|
||||
// msext 那段从根向下"非空即覆盖"的 if 链,但写法更直接、可复用。
|
||||
// 查找语义:从最深层(market)向上回退取第一个非空值,"根"(顶层)作为最浅一层
|
||||
// 自动包含在链中 —— 等价于 msext 那段从根向下"非空即覆盖"的 if 链,但写法更直接、
|
||||
// 可复用,且天然支持顶层声明字段(任何字段都能放顶层兜底)。
|
||||
//
|
||||
// 统一接口:所有 5 个字段都用同一套 `pickString` / `pickInt` 倒序查找,
|
||||
// 不再有"双子树合并 / chulishengji"等历史包袱的多处实现。
|
||||
// 统一接口:所有 7 个字段都用同一套 `pickString` / `pickInt` 倒序查找,
|
||||
// 无任何字段级特殊兜底逻辑(如 `?? config.xxx`),所有字段平等。
|
||||
//
|
||||
|
||||
import Foundation
|
||||
@@ -55,6 +58,9 @@ extension Agent: RemoteConfigNode {}
|
||||
extension Game: RemoteConfigNode {}
|
||||
extension Channel: RemoteConfigNode {}
|
||||
extension Market: RemoteConfigNode {}
|
||||
// RemoteConfig 顶层也作为节点参与 fallback 链,处于最浅一层("根")。
|
||||
// 这样所有字段的 fallback 完全统一:节点链倒序找第一个非空 → 链上即包含根。
|
||||
extension RemoteConfig: RemoteConfigNode {}
|
||||
|
||||
// MARK: - VersionResolver
|
||||
|
||||
@@ -81,28 +87,30 @@ public enum VersionResolver {
|
||||
marketId: marketId
|
||||
)
|
||||
|
||||
// 所有 7 字段统一走"chain 倒序找第一个非空",无任何字段级特殊兜底。
|
||||
// chain 头部就是 RemoteConfig 顶层("根"),最深层赢、根作为最浅层兜底。
|
||||
return ResolvedVersion(
|
||||
appVersion: pickInt(chain, \.appVersion),
|
||||
appVersion: pickInt(chain, \.appVersion),
|
||||
appDownload: pickString(chain, \.appDownload),
|
||||
gameVersion: pickInt(chain, \.gameVersion),
|
||||
gameZip: pickString(chain, \.gameZip),
|
||||
// showmessage / audioDomain / audioBucket:链上任一层都不写 → 回退顶层 config
|
||||
showmessage: pickString(chain, \.showmessage) ?? config.showmessage,
|
||||
audioDomain: pickString(chain, \.audioDomain) ?? config.audioDomain,
|
||||
audioBucket: pickString(chain, \.audioBucket) ?? config.audioBucket
|
||||
gameVersion: pickInt(chain, \.gameVersion),
|
||||
gameZip: pickString(chain, \.gameZip),
|
||||
showmessage: pickString(chain, \.showmessage),
|
||||
audioDomain: pickString(chain, \.audioDomain),
|
||||
audioBucket: pickString(chain, \.audioBucket)
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - 节点链匹配
|
||||
|
||||
/// 沿 agent → game → channel → market 顺序逐层匹配,遇到不匹配的层立刻截断。
|
||||
/// 沿 root → agent → game → channel → market 顺序逐层匹配,遇到不匹配的层立刻截断。
|
||||
/// 链头始终是 RemoteConfig 顶层("根"),作为最浅一层兜底。
|
||||
///
|
||||
/// 返回数组从根到叶有序,长度 0..4:
|
||||
/// - `[]` :agent 未匹配
|
||||
/// - `[agent]` :agent 命中、game 未匹配
|
||||
/// - `[agent, game]` :再深一层未命中
|
||||
/// 返回数组从根到叶有序,长度 1..5:
|
||||
/// - `[root]` :agent 未匹配(链最短为根)
|
||||
/// - `[root, agent]` :agent 命中、game 未匹配
|
||||
/// - `[root, agent, game]` :再深一层未命中
|
||||
/// - ...
|
||||
/// - `[agent, game, channel, market]`:全链命中
|
||||
/// - `[root, agent, game, channel, market]`:全链命中
|
||||
private static func buildChain(
|
||||
config: RemoteConfig,
|
||||
agentId: String,
|
||||
@@ -110,7 +118,7 @@ public enum VersionResolver {
|
||||
channelId: String,
|
||||
marketId: String
|
||||
) -> [RemoteConfigNode] {
|
||||
var chain: [RemoteConfigNode] = []
|
||||
var chain: [RemoteConfigNode] = [config]
|
||||
|
||||
guard let agent = config.agentlist?.first(where: { $0.agentid == agentId })
|
||||
else { return chain }
|
||||
|
||||
Reference in New Issue
Block a user