diff --git a/domain_resource/Index.ets b/domain_resource/Index.ets index bbfad02..fad6ae6 100644 --- a/domain_resource/Index.ets +++ b/domain_resource/Index.ets @@ -3,7 +3,7 @@ // 配置 export { AppConfig, AppConfigRaw, normalizeConfig } from './src/main/ets/config/AppConfig'; export { ConfigManager, LOCAL_CONFIG_RAWFILE } from './src/main/ets/config/ConfigManager'; -export { RemoteConfig, AgentNode, ChannelNode, MarketNode, GameOverrideNode, GameTreeNode, +export { RemoteConfig, AgentNode, GameNode, ChannelNode, MarketNode, Scalar, asStr, VersionFields, MatchKeys, VersionDecision } from './src/main/ets/config/RemoteConfig'; // 版本决策(纯函数) diff --git a/domain_resource/src/main/ets/config/ConfigManager.ets b/domain_resource/src/main/ets/config/ConfigManager.ets index 9d770e8..9fdf626 100644 --- a/domain_resource/src/main/ets/config/ConfigManager.ets +++ b/domain_resource/src/main/ets/config/ConfigManager.ets @@ -1,16 +1,19 @@ /** - * 配置管理(契约 §4,框架 §8.2)。负责: + * 配置管理(对齐真实线上配置 + Android chuliversion_1)。负责: * - 加载本地配置(rawfile/app_config.json)。 * - 拼远程配置 URL(gameconfig '-'→'/' + .txt),GET 禁缓存 + ?a=时间戳。 - * - 校验(长度<30 视为错误文案 → 走 showmessage 阻断)、解析、二级 url 二次请求。 - * - 经 VersionResolver 产出版本决策。 + * - 校验(长度<30 视为错误文案 → 走 showmessage 阻断)、解析为 RemoteConfig。 + * - 经 VersionResolver 产出版本决策(gameid 为空时由调用方传入 version.xml 回退值)。 + * + * 注:真实 `.txt` 配置(顶层仅 agentlist、字段 game_zip、数字 marketid)无二级 url 二次请求, + * 故移除旧的 fetchSecondary 编排。 */ import { common } from '@kit.AbilityKit'; import { util } from '@kit.ArkTS'; import { HttpClient } from 'platform'; import { Logger } from 'common'; import { AppConfig, AppConfigRaw, normalizeConfig } from './AppConfig'; -import { AgentNode, GameTreeNode, MatchKeys, RemoteConfig, VersionDecision, VersionFields } from './RemoteConfig'; +import { MatchKeys, RemoteConfig, VersionDecision } from './RemoteConfig'; import { VersionResolver } from '../version/VersionResolver'; /** 本地配置 rawfile 文件名。 */ @@ -42,13 +45,17 @@ export class ConfigManager { return this.local; } - /** 分层匹配 key(§4.4)。 */ - matchKeys(): MatchKeys { + /** + * 分层匹配 key(§4.4)。gameid 为空(大厅)时回退到 version.xml 的 game id + * (fallbackGameId,复刻 Android versd.getGameid())。 + */ + matchKeys(fallbackGameId: string = ''): MatchKeys { + const gameid: string = this.local.gameid !== '' ? this.local.gameid : fallbackGameId; return { agentid: this.local.agent, channelid: this.local.channel, marketid: this.local.market, - gameid: this.local.gameid, + gameid, }; } @@ -73,93 +80,17 @@ export class ConfigManager { ConfigManager.log.w(`remote config too short (${body.length}) -> treat as block message`); return { showmessage: body }; } - const remote: RemoteConfig = JSON.parse(body) as RemoteConfig; - await this.fetchSecondary(remote); - return remote; + return JSON.parse(body) as RemoteConfig; } - /** 拉取远程配置并产出版本决策(一步到位)。 */ - async resolve(): Promise { + /** 纯函数:用已拉取的远程配置 + gameid 回退值产出版本决策。 */ + resolveWith(remote: RemoteConfig, fallbackGameId: string): VersionDecision { + return VersionResolver.resolve(remote, this.matchKeys(fallbackGameId)); + } + + /** 便捷:拉取 + 解析一步到位(gameid 回退值由调用方提供)。 */ + async resolve(fallbackGameId: string = ''): Promise { const remote: RemoteConfig = await this.fetchRemote(); - return VersionResolver.resolve(remote, this.matchKeys()); - } - - /** - * 二级 url 二次请求(§4.3):匹配到的 agent/game 节点若带非空 url, - * 拉取其专属二级配置并用同 id 节点的子树覆盖内联节点。 - * ⚠ 合并语义按契约推断,需在 T-M2-12 用真实服务端响应校验;失败静默回退内联配置。 - */ - private async fetchSecondary(remote: RemoteConfig): Promise { - const k: MatchKeys = this.matchKeys(); - - const agent: AgentNode | undefined = - (remote.agentlist ?? []).find((a: AgentNode) => (a.agentid ?? '') === k.agentid); - if (agent !== undefined && agent.url !== undefined && agent.url !== '') { - const sub: RemoteConfig | undefined = await this.tryFetchSub(agent.url); - const subAgent: AgentNode | undefined = - sub === undefined ? undefined : (sub.agentlist ?? []).find((a: AgentNode) => (a.agentid ?? '') === k.agentid); - if (subAgent !== undefined) { - // 既并入子树,也并入二级节点本层的版本字段(此前只换 channellist 会丢节点自身的版本/下载地址) - ConfigManager.copyVersionFields(agent, subAgent); - if (subAgent.channellist !== undefined) { - agent.channellist = subAgent.channellist; - } - ConfigManager.log.i('secondary agent config merged'); - } - } - - const game: GameTreeNode | undefined = - (remote.gamelist ?? []).find((g: GameTreeNode) => (g.gameid ?? '') === k.gameid); - if (game !== undefined && game.url !== undefined && game.url !== '') { - const sub: RemoteConfig | undefined = await this.tryFetchSub(game.url); - const subGame: GameTreeNode | undefined = - sub === undefined ? undefined : (sub.gamelist ?? []).find((g: GameTreeNode) => (g.gameid ?? '') === k.gameid); - if (subGame !== undefined) { - ConfigManager.copyVersionFields(game, subGame); - if (subGame.agentlist !== undefined) { - game.agentlist = subGame.agentlist; - } - ConfigManager.log.i('secondary game config merged'); - } - } - } - - /** 用 src 的非空版本字段覆盖 dst(二级配置节点本层的版本/下载地址不丢失)。 */ - private static copyVersionFields(dst: VersionFields, src: VersionFields): void { - if (src.app_version !== undefined && src.app_version !== '') { - dst.app_version = src.app_version; - } - if (src.app_download !== undefined && src.app_download !== '') { - dst.app_download = src.app_download; - } - if (src.app_size !== undefined && src.app_size !== '') { - dst.app_size = src.app_size; - } - if (src.game_version !== undefined && src.game_version !== '') { - dst.game_version = src.game_version; - } - if (src.game_download !== undefined && src.game_download !== '') { - dst.game_download = src.game_download; - } - if (src.game_size !== undefined && src.game_size !== '') { - dst.game_size = src.game_size; - } - if (src.showmessage !== undefined && src.showmessage !== '') { - dst.showmessage = src.showmessage; - } - } - - private async tryFetchSub(url: string): Promise { - try { - const res = await HttpClient.getString(url); - if (res.code < 200 || res.code >= 300) { - return undefined; - } - return JSON.parse(res.body.trim()) as RemoteConfig; - } catch (e) { - const err = e as Error; - ConfigManager.log.w(`secondary config fetch failed ${url}: ${err.message}`); - return undefined; - } + return this.resolveWith(remote, fallbackGameId); } } diff --git a/domain_resource/src/main/ets/config/RemoteConfig.ets b/domain_resource/src/main/ets/config/RemoteConfig.ets index d9af627..5d6d78c 100644 --- a/domain_resource/src/main/ets/config/RemoteConfig.ets +++ b/domain_resource/src/main/ets/config/RemoteConfig.ets @@ -1,65 +1,72 @@ /** - * 远程配置 DTO(契约 §4.3 2.0 格式:agentlist / gamelist 两棵树并存)。 + * 远程配置 DTO —— **严格对齐真实线上 `.txt` 配置**(实测 http://tsgames.daoqi88.cn/config/update_jsonv2.txt) + * 与原 Android `Bean(Txt)` + `chuliversion_1` 解析路径,而非旧契约示意文档。 * - * 每层节点可携带可覆盖字段(§4.4):app_version/app_download/app_size/ - * game_version/game_download/game_size/url/showmessage。越深层越后赋值,覆盖前层。 - * 字段名与契约真实字段(非旧示意文档)严格一致。 + * 真实层级(game 在 channel/market 之上): + * agentlist[] → gamelist[] → channellist[] → marketlist[] + * + * 真实字段要点(与旧实现的差异,均为"破坏 H5 适配"的根因): + * - 资源下载字段名是 **game_zip**(非 game_download)。 + * - marketid / app_version / game_version 在 JSON 里是**数字**,匹配/比较需做 string↔number 归一。 + * - app 升级字段 app_download/app_version 落在 market 层;资源字段 game_zip/game_version + * 可出现在 game / channel / market 各层,越深越后、覆盖前层("0"/空视为未设置不覆盖)。 */ -/** 每层可覆盖的版本字段(§4.4)。空串视为"未设置",不覆盖前层。 */ +/** JSON 里既可能是字符串也可能是数字的标量(id、版本号等)。 */ +export type Scalar = string | number; + +/** 把 Scalar 归一为字符串(undefined/null → '')。 */ +export function asStr(v: Scalar | undefined): string { + if (v === undefined) { + return ''; + } + return typeof v === 'number' ? `${v}` : v; +} + +/** 每层可携带的资源/App 升级字段。空串或 "0"(version)视为未设置。 */ export interface VersionFields { - app_version?: string; + app_version?: Scalar; app_download?: string; app_size?: string; - game_version?: string; - game_download?: string; + game_version?: Scalar; + /** 资源 zip 下载地址(真实字段名 game_zip)。 */ + game_zip?: string; game_size?: string; - /** 指向二级配置地址,非空则二次请求(§4.3)。 */ - url?: string; showmessage?: string; } -/** 市场层节点(marketid 命中;其 gamelist 为该市场下的游戏覆盖)。 */ +/** 市场层(marketid 数字命中;携带 app 升级 + 可选资源覆盖)。 */ export interface MarketNode extends VersionFields { - marketid?: string; - gamelist?: GameOverrideNode[]; + marketid?: Scalar; } -/** 市场下的游戏覆盖节点(gameid 命中,设 app/game 升级)。 */ -export interface GameOverrideNode extends VersionFields { - gameid?: string; -} - -/** 渠道层节点。 */ +/** 渠道层(channelid 命中;可覆盖资源字段;含 marketlist)。 */ export interface ChannelNode extends VersionFields { channelid?: string; marketlist?: MarketNode[]; } -/** 代理层节点。 */ -export interface AgentNode extends VersionFields { - agentid?: string; - agentname?: string; +/** 游戏层(gameid 命中;携带大厅资源 game_zip/game_version;含 channellist)。 */ +export interface GameNode extends VersionFields { + gameid?: string; + game_hall_dir?: string; channellist?: ChannelNode[]; } -/** 游戏树顶层节点(gamelist[]:gameid 命中;url 非空则二次请求;内含 agentlist)。 */ -export interface GameTreeNode extends VersionFields { - gameid?: string; - agentlist?: AgentNode[]; +/** 代理层(agentid 命中;含 gamelist)。 */ +export interface AgentNode extends VersionFields { + agentid?: string; + agentname?: string; + gamelist?: GameNode[]; } -/** 远程配置根(2.0 格式)。 */ -export interface RemoteConfig extends VersionFields { - /** 顶层公告:非空 → 阻断弹窗(§3.2)。 */ +/** 远程配置根(真实格式仅顶层 agentlist;顶层 showmessage 非空则阻断)。 */ +export interface RemoteConfig { showmessage?: string; - /** 代理配置树。 */ agentlist?: AgentNode[]; - /** 游戏配置树(2.0 才有;缺失即 1.0 老格式)。 */ - gamelist?: GameTreeNode[]; } -/** 分层匹配 key(来自本地配置 §4.2)。 */ +/** 分层匹配 key(来自本地配置 + version.xml 回退的 gameid)。 */ export interface MatchKeys { agentid: string; channelid: string; @@ -69,13 +76,13 @@ export interface MatchKeys { /** 最终版本决策结果。 */ export interface VersionDecision { - /** 顶层公告(非空则阻断)。 */ + /** 顶层/命中层公告(非空则阻断)。 */ showmessage: string; - /** App 升级(取代理树/游戏树中 version 更高者)。 */ + /** App(apk)升级版本与下载地址(HarmonyOS 不安装 apk,仅作记录/比较用)。 */ appVersion: number; appDownload: string; appSize: string; - /** 资源(游戏)升级(取更高者)。 */ + /** 资源(大厅/子游戏 zip)升级版本与下载地址(来源 game_zip)。 */ gameVersion: number; gameDownload: string; gameSize: string; diff --git a/domain_resource/src/main/ets/resource/ResourceManager.ets b/domain_resource/src/main/ets/resource/ResourceManager.ets index 9b2b3e2..7dc76f5 100644 --- a/domain_resource/src/main/ets/resource/ResourceManager.ets +++ b/domain_resource/src/main/ets/resource/ResourceManager.ets @@ -84,7 +84,7 @@ export class ResourceManager { return VersionXml.parse(FileSystem.readText(p)); } } - return { version: 0, gameName: '' }; + return { version: 0, gameName: '', gameId: '' }; } /** 本地资源版本(version.xml 的 version;缺失返回 0)。 */ @@ -97,6 +97,11 @@ export class ResourceManager { return this.readVersionXml().gameName; } + /** 本地 version.xml 的 game id(大厅 app_config.gameid 为空时的回退匹配 key;缺失返回 '')。 */ + localGameId(): string { + return this.readVersionXml().gameId; + } + /** 持久化路径到 KvStore(供容器/重启读取)。 */ async persistPaths(): Promise { this.kv.putString(KEY_URLPATH, this.cachedPaths.urlpath); diff --git a/domain_resource/src/main/ets/resource/VersionXml.ets b/domain_resource/src/main/ets/resource/VersionXml.ets index f4d2b30..5d9c916 100644 --- a/domain_resource/src/main/ets/resource/VersionXml.ets +++ b/domain_resource/src/main/ets/resource/VersionXml.ets @@ -15,6 +15,8 @@ export interface VersionXmlInfo { version: number; /** 子 游戏名(缺失 → '')。 */ gameName: string; + /** 子 游戏 ID(大厅 app_config.gameid 为空时的回退匹配 key)。 */ + gameId: string; } export class VersionXml { @@ -22,7 +24,7 @@ export class VersionXml { /** 解析 version + gameName。属性回调紧随其开始标签,故用 currentTag 把 name 归属到 game 标签。 */ static parse(xmlText: string): VersionXmlInfo { - const info: VersionXmlInfo = { version: 0, gameName: '' }; + const info: VersionXmlInfo = { version: 0, gameName: '', gameId: '' }; if (xmlText.length === 0) { return info; } @@ -43,6 +45,9 @@ export class VersionXml { } else if (name === 'name' && currentTag === 'game') { // 根 无 name 属性;只有子 触发,正是所需游戏名 info.gameName = val; + } else if (name === 'id' && currentTag === 'game') { + // 子 的 id:大厅 gameid 回退匹配 key(根 无 id 属性) + info.gameId = val; } return true; }, diff --git a/domain_resource/src/main/ets/startup/StartupOrchestrator.ets b/domain_resource/src/main/ets/startup/StartupOrchestrator.ets index 472d6ef..ede5402 100644 --- a/domain_resource/src/main/ets/startup/StartupOrchestrator.ets +++ b/domain_resource/src/main/ets/startup/StartupOrchestrator.ets @@ -12,7 +12,7 @@ import { KvStore } from 'platform'; import { Logger } from 'common'; import { ConfigManager } from '../config/ConfigManager'; import { AppConfig } from '../config/AppConfig'; -import { VersionDecision } from '../config/RemoteConfig'; +import { RemoteConfig, VersionDecision } from '../config/RemoteConfig'; import { ResourceManager } from '../resource/ResourceManager'; import { AppDataInjector, AppDataValues } from '../resource/AppDataInjector'; @@ -93,24 +93,25 @@ export class StartupOrchestrator { return { enter: true, entryUrl, message: '' }; } - // T-M5-07 并行化:远程配置(网络 IO,1-5s)与内置资源准备(本地拷贝解压,0.2-0.8s) - // 无数据依赖——prepareBuiltin 只依赖本地 config,不依赖远程 decision。两者同时进行, - // 总耗时由 max(远程, 内置) 决定而非求和。 + // T-M5-07 并行化:远程配置**拉取**(网络 IO,1-5s)与内置资源准备(本地拷贝解压,0.2-0.8s) + // 并行。注意:版本**解析**需要 version.xml 的 game id 作回退匹配 key(大厅 app_config.gameid + // 为空,复刻 Android versd.getGameid()),故解析必须在内置资源就绪后做——只拉取可并行, + // 解析为纯函数置于汇合点,仍由 max(远程, 内置) 决定耗时。 this.stage(StartupStage.FETCH_REMOTE_CONFIG, 20); const tRemote: number = Date.now(); - const remotePromise: Promise = - (async (): Promise => { + const remotePromise: Promise = + (async (): Promise => { try { - const d: VersionDecision = await config.resolve(); + const r: RemoteConfig = await config.fetchRemote(); this.mark('fetch_remote_config', tRemote); - return d; + return r; } catch (e) { StartupOrchestrator.log.w(`remote config failed, fallback to local: ${(e as Error).message}`); return undefined; } })(); - // 内置资源准备(与上面远程请求并行推进) + // 内置资源准备(与上面远程请求并行推进)——为 version.xml(版本 + game id 种子)落盘 this.stage(StartupStage.PREPARE_RESOURCE, 35); const tBuiltin: number = Date.now(); if (!resource.isPrepared()) { @@ -118,8 +119,11 @@ export class StartupOrchestrator { } this.mark('prepare_builtin', tBuiltin); - // 汇合:取远程版本决策 - const decision: VersionDecision | undefined = await remotePromise; + // 汇合:取远程原始配置,用 version.xml 的 game id 作 gameid 回退后解析版本决策 + const remote: RemoteConfig | undefined = await remotePromise; + const fallbackGameId: string = resource.localGameId(); + const decision: VersionDecision | undefined = + remote === undefined ? undefined : config.resolveWith(remote, fallbackGameId); this.stage(StartupStage.RESOLVE_VERSION, 55); if (decision !== undefined && decision.showmessage !== '') { StartupOrchestrator.log.w(`blocked by showmessage: ${decision.showmessage}`); diff --git a/domain_resource/src/main/ets/version/VersionResolver.ets b/domain_resource/src/main/ets/version/VersionResolver.ets index 48dcd61..e56e139 100644 --- a/domain_resource/src/main/ets/version/VersionResolver.ets +++ b/domain_resource/src/main/ets/version/VersionResolver.ets @@ -1,151 +1,121 @@ /** - * 分层版本决策(契约 §4.4,框架 §8.2)。**纯函数,无 IO,便于单测**。 + * 分层版本决策 —— **复刻 Android `chuliversion_1` 真实逻辑**(纯函数,无 IO,便于单测)。 * - * 输入已拉取/合并好的远程配置(含两棵树)+ 本地匹配 key,输出最终 App/资源版本决策。 - * 规则: - * - 代理树:agentid → channelid → marketid → (market.gamelist) gameid,**越深越后、覆盖前层**。 - * - 游戏树:gameid → agentid → channelid → marketid,同样后层覆盖。 - * - 最终:app 升级与 game 升级各自取"两棵树中 version 更高者"的下载地址。 - * - 二级 `url` 二次请求(§4.3)属 IO,由 ConfigManager 编排后把合并结果喂给本函数。 + * 遍历真实层级 agentlist → gamelist → channellist → marketlist,逐层下钻匹配 + * agentid / gameid / channelid / marketid(marketid 数字与本地字符串归一比较)。 + * + * 累积规则(与 Android 一致): + * - 资源升级 game_zip/game_version:在 game、channel、market 三层累积,越深越后、覆盖前层; + * "0" 或空的 game_version 不覆盖(视为未设置)。 + * - App 升级 app_download/app_version:取 market 层值。 + * - showmessage:任一命中层非空即生效(阻断)。 + * - 最终资源版本与本地 version.xml 比较由调用方决定是否下载 game_zip。 */ -import { AgentNode, ChannelNode, GameOverrideNode, GameTreeNode, MarketNode, - MatchKeys, RemoteConfig, VersionDecision, VersionFields } from '../config/RemoteConfig'; +import { AgentNode, ChannelNode, GameNode, MarketNode, + MatchKeys, RemoteConfig, Scalar, VersionDecision, VersionFields, asStr } from '../config/RemoteConfig'; -/** 用 next 的非空字段覆盖 cur(空串/undefined 不覆盖)。 */ -function ov(cur: string | undefined, next: string | undefined): string | undefined { - return (next !== undefined && next !== '') ? next : cur; -} - -/** 把 n 的非空可覆盖字段并入 acc(深层覆盖浅层)。 */ -function merge(acc: VersionFields, n: VersionFields): void { - acc.app_version = ov(acc.app_version, n.app_version); - acc.app_download = ov(acc.app_download, n.app_download); - acc.app_size = ov(acc.app_size, n.app_size); - acc.game_version = ov(acc.game_version, n.game_version); - acc.game_download = ov(acc.game_download, n.game_download); - acc.game_size = ov(acc.game_size, n.game_size); - acc.showmessage = ov(acc.showmessage, n.showmessage); -} - -/** 版本号转整数(空/非数字 → 0)。 */ -function parseVer(s: string | undefined): number { - if (s === undefined || s === '') { +/** 版本号转整数(空/"0"/非数字 → 0)。 */ +function parseVer(v: Scalar | undefined): number { + const s: string = asStr(v); + if (s === '') { return 0; } const n: number = parseInt(s, 10); return isNaN(n) ? 0 : n; } -/** 取首个非空串。 */ -function firstNonEmpty(a: string | undefined, b: string | undefined, c: string | undefined): string { - if (a !== undefined && a !== '') { - return a; +/** game_version 是否为"有效非零"(用于决定是否覆盖累积值)。 */ +function hasVer(v: Scalar | undefined): boolean { + return parseVer(v) > 0; +} + +/** 累积器:复刻 chuliversion_1 的 newgame_zip / newgame_version + app 字段 + showmessage。 */ +class Acc { + gameZip: string = ''; + gameVersion: number = 0; + gameSize: string = ''; + appDownload: string = ''; + appVersion: number = 0; + appSize: string = ''; + showmessage: string = ''; + + /** 资源字段覆盖(game/channel/market 各层调用):非空 zip 覆盖;非零 version 覆盖。 */ + mergeGame(n: VersionFields): void { + const zip: string = n.game_zip ?? ''; + if (zip !== '') { + this.gameZip = zip; + } + if (hasVer(n.game_version)) { + this.gameVersion = parseVer(n.game_version); + } + const size: string = n.game_size ?? ''; + if (size !== '') { + this.gameSize = size; + } } - if (b !== undefined && b !== '') { - return b; + + /** App 升级字段(market 层):下载地址与版本均非空才采纳。 */ + mergeApp(n: VersionFields): void { + const dl: string = n.app_download ?? ''; + const ver: string = asStr(n.app_version); + if (dl !== '' && ver !== '') { + this.appDownload = dl; + this.appVersion = parseVer(n.app_version); + this.appSize = n.app_size ?? ''; + } + } + + /** 命中层公告非空即记录(最后非空者生效)。 */ + mergeMsg(n: VersionFields): void { + const msg: string = n.showmessage ?? ''; + if (msg !== '') { + this.showmessage = msg; + } } - return (c !== undefined && c !== '') ? c : ''; } export class VersionResolver { - /** 代理树:agentid → channelid → marketid → gameid,逐层并入。 */ - static resolveAgentTree(agents: AgentNode[], k: MatchKeys): VersionFields { - const acc: VersionFields = {}; - const agent: AgentNode | undefined = agents.find((a: AgentNode) => (a.agentid ?? '') === k.agentid); - if (agent === undefined) { - return acc; - } - merge(acc, agent); - const channel: ChannelNode | undefined = - (agent.channellist ?? []).find((c: ChannelNode) => (c.channelid ?? '') === k.channelid); - if (channel === undefined) { - return acc; - } - merge(acc, channel); - const market: MarketNode | undefined = - (channel.marketlist ?? []).find((m: MarketNode) => (m.marketid ?? '') === k.marketid); - if (market === undefined) { - return acc; - } - merge(acc, market); - // gameid 为空表示"不参与 game 维度匹配"(大厅);非空才在 market.gamelist 里命中 - if (k.gameid !== '') { - const game: GameOverrideNode | undefined = - (market.gamelist ?? []).find((g: GameOverrideNode) => (g.gameid ?? '') === k.gameid); - if (game !== undefined) { - merge(acc, game); - } - } - return acc; - } - - /** 游戏树:gameid → agentid → channelid → marketid,逐层并入。gameid 为空则不参与游戏树。 */ - static resolveGameTree(games: GameTreeNode[], k: MatchKeys): VersionFields { - const acc: VersionFields = {}; - if (k.gameid === '') { - return acc; - } - const game: GameTreeNode | undefined = games.find((g: GameTreeNode) => (g.gameid ?? '') === k.gameid); - if (game === undefined) { - return acc; - } - merge(acc, game); - const agent: AgentNode | undefined = - (game.agentlist ?? []).find((a: AgentNode) => (a.agentid ?? '') === k.agentid); - if (agent === undefined) { - return acc; - } - merge(acc, agent); - const channel: ChannelNode | undefined = - (agent.channellist ?? []).find((c: ChannelNode) => (c.channelid ?? '') === k.channelid); - if (channel === undefined) { - return acc; - } - merge(acc, channel); - const market: MarketNode | undefined = - (channel.marketlist ?? []).find((m: MarketNode) => (m.marketid ?? '') === k.marketid); - if (market !== undefined) { - merge(acc, market); - } - return acc; - } - - /** 最终决策:两棵树各取更高 version 的下载地址。 */ + /** 真实层级遍历:agent → game → channel → market。 */ static resolve(remote: RemoteConfig, keys: MatchKeys): VersionDecision { - const a: VersionFields = VersionResolver.resolveAgentTree(remote.agentlist ?? [], keys); - const g: VersionFields = VersionResolver.resolveGameTree(remote.gamelist ?? [], keys); - - const aGameV: number = parseVer(a.game_version); - const gGameV: number = parseVer(g.game_version); - // 取更高 version 者;平手时取"有下载地址"的一棵(避免选中只有版本号无下载地址的层) - let gameWin: VersionFields; - if (gGameV > aGameV) { - gameWin = g; - } else if (aGameV > gGameV) { - gameWin = a; - } else { - gameWin = (a.game_download !== undefined && a.game_download !== '') ? a : g; + const acc: Acc = new Acc(); + const rootMsg: string = remote.showmessage ?? ''; + if (rootMsg !== '') { + acc.showmessage = rootMsg; } - const aAppV: number = parseVer(a.app_version); - const gAppV: number = parseVer(g.app_version); - let appWin: VersionFields; - if (gAppV > aAppV) { - appWin = g; - } else if (aAppV > gAppV) { - appWin = a; - } else { - appWin = (a.app_download !== undefined && a.app_download !== '') ? a : g; + const agent: AgentNode | undefined = + (remote.agentlist ?? []).find((a: AgentNode) => (a.agentid ?? '') === keys.agentid); + if (agent !== undefined) { + acc.mergeMsg(agent); + const game: GameNode | undefined = + (agent.gamelist ?? []).find((g: GameNode) => (g.gameid ?? '') === keys.gameid); + if (game !== undefined) { + acc.mergeMsg(game); + acc.mergeGame(game); + const channel: ChannelNode | undefined = + (game.channellist ?? []).find((c: ChannelNode) => (c.channelid ?? '') === keys.channelid); + if (channel !== undefined) { + acc.mergeMsg(channel); + acc.mergeGame(channel); + const market: MarketNode | undefined = + (channel.marketlist ?? []).find((m: MarketNode) => asStr(m.marketid) === keys.marketid); + if (market !== undefined) { + acc.mergeMsg(market); + acc.mergeGame(market); + acc.mergeApp(market); + } + } + } } return { - showmessage: firstNonEmpty(a.showmessage, g.showmessage, remote.showmessage), - appVersion: Math.max(aAppV, gAppV), - appDownload: appWin.app_download ?? '', - appSize: appWin.app_size ?? '', - gameVersion: Math.max(aGameV, gGameV), - gameDownload: gameWin.game_download ?? '', - gameSize: gameWin.game_size ?? '', + showmessage: acc.showmessage, + appVersion: acc.appVersion, + appDownload: acc.appDownload, + appSize: acc.appSize, + gameVersion: acc.gameVersion, + gameDownload: acc.gameZip, + gameSize: acc.gameSize, }; } } diff --git a/entry/src/main/resources/rawfile/app_config.json b/entry/src/main/resources/rawfile/app_config.json index 217708b..1e8139b 100644 --- a/entry/src/main/resources/rawfile/app_config.json +++ b/entry/src/main/resources/rawfile/app_config.json @@ -7,7 +7,7 @@ "market": "3", "gameid": "", "weburl": "", - "gameconfig": "tsgames.daoqi88.cn-config_test-update_jsonv2_test", + "gameconfig": "tsgames.daoqi88.cn-config-update_jsonv2", "other": "", "tuiguang": "" } diff --git a/entry/src/main/resources/rawfile/gamehall_builtin.zip b/entry/src/main/resources/rawfile/gamehall_builtin.zip index d71bce1..dc5958a 100644 Binary files a/entry/src/main/resources/rawfile/gamehall_builtin.zip and b/entry/src/main/resources/rawfile/gamehall_builtin.zip differ diff --git a/entry/src/test/VersionResolver.test.ets b/entry/src/test/VersionResolver.test.ets index 05a5de6..7ba7be9 100644 --- a/entry/src/test/VersionResolver.test.ets +++ b/entry/src/test/VersionResolver.test.ets @@ -2,11 +2,13 @@ import { describe, it, expect } from '@ohos/hypium'; import { VersionResolver, RemoteConfig, MatchKeys, VersionDecision } from 'domain_resource'; /** - * 分层版本决策单测(T-M2-06,契约 §4.4)。纯函数,构造典型多层样例验证后层覆盖、 - * 两棵树取更高 version、showmessage 阻断。 + * 分层版本决策单测(对齐真实线上 `.txt` 配置 + Android chuliversion_1)。 + * 真实层级:agentlist → gamelist → channellist → marketlist;资源字段 game_zip; + * marketid/版本为数字;大厅 gameid 由 version.xml 回退提供。 */ export default function versionResolverTest() { - const keys: MatchKeys = { agentid: 'A1', channelid: 'C1', marketid: 'M1', gameid: 'G1' }; + // 大厅典型 key:gameid 来自 version.xml 回退(G1) + const keys: MatchKeys = { agentid: 'A1', channelid: 'C1', marketid: '3', gameid: 'G1' }; describe('VersionResolver', () => { it('empty_config_zero_decision', 0, () => { @@ -17,74 +19,83 @@ export default function versionResolverTest() { expect(d.gameDownload).assertEqual(''); }); - it('agent_tree_deeper_overrides', 0, () => { - // 代理树:agent 设 game_version=10,market 覆盖为 20(深层覆盖浅层) - const remote: RemoteConfig = { - agentlist: [{ - agentid: 'A1', game_version: '10', game_download: 'a_url', game_size: '100', - channellist: [{ - channelid: 'C1', - marketlist: [{ marketid: 'M1', game_version: '20', game_download: 'm_url' }], - }], - }], - }; - const d: VersionDecision = VersionResolver.resolve(remote, keys); - expect(d.gameVersion).assertEqual(20); - expect(d.gameDownload).assertEqual('m_url'); - }); - - it('market_game_level_overrides', 0, () => { - // market.gamelist 中 gameid 命中,覆盖到最深 + it('game_level_resource_picked', 0, () => { + // 资源 game_zip/game_version 落在 game 层,channel/market 不覆盖 → 取 game 层 const remote: RemoteConfig = { agentlist: [{ agentid: 'A1', - channellist: [{ - channelid: 'C1', - marketlist: [{ - marketid: 'M1', game_version: '20', - gamelist: [{ gameid: 'G1', game_version: '30', game_download: 'g_url' }], - }], + gamelist: [{ + gameid: 'G1', game_version: 261, game_zip: 'hall.zip', game_size: '4.6M', + channellist: [{ channelid: 'C1', game_version: 0, + marketlist: [{ marketid: 3, app_version: 49, app_download: 'apk' }] }], }], }], }; const d: VersionDecision = VersionResolver.resolve(remote, keys); - expect(d.gameVersion).assertEqual(30); - expect(d.gameDownload).assertEqual('g_url'); + expect(d.gameVersion).assertEqual(261); + expect(d.gameDownload).assertEqual('hall.zip'); + expect(d.appVersion).assertEqual(49); + expect(d.appDownload).assertEqual('apk'); }); - it('two_trees_take_higher_version', 0, () => { - // 代理树 game_version=20,游戏树 game_version=25 → 取 25 及其下载地址 + it('deeper_layer_overrides_resource', 0, () => { + // game 层 game_zip=hall.zip,market 层覆盖为 m.zip / 更高版本 const remote: RemoteConfig = { - agentlist: [{ agentid: 'A1', game_version: '20', game_download: 'agent_url' }], - gamelist: [{ - gameid: 'G1', - agentlist: [{ agentid: 'A1', game_version: '25', game_download: 'game_url' }], + agentlist: [{ + agentid: 'A1', + gamelist: [{ + gameid: 'G1', game_version: 100, game_zip: 'hall.zip', + channellist: [{ channelid: 'C1', + marketlist: [{ marketid: 3, game_version: 120, game_zip: 'm.zip' }] }], + }], }], }; const d: VersionDecision = VersionResolver.resolve(remote, keys); - expect(d.gameVersion).assertEqual(25); - expect(d.gameDownload).assertEqual('game_url'); + expect(d.gameVersion).assertEqual(120); + expect(d.gameDownload).assertEqual('m.zip'); }); - it('app_and_game_independent_winners', 0, () => { - // app 升级取代理树(app_version=49),game 升级取游戏树(game_version=30) + it('numeric_marketid_matches_string_key', 0, () => { + // JSON marketid 为数字 3,本地 key 为字符串 '3',应归一命中 const remote: RemoteConfig = { - agentlist: [{ agentid: 'A1', app_version: '49', app_download: 'apk_url', game_version: '10' }], - gamelist: [{ - gameid: 'G1', - agentlist: [{ agentid: 'A1', app_version: '40', game_version: '30', game_download: 'g_url' }], - }], + agentlist: [{ agentid: 'A1', + gamelist: [{ gameid: 'G1', game_version: 50, game_zip: 'z', + channellist: [{ channelid: 'C1', + marketlist: [{ marketid: 3, app_version: 49, app_download: 'apk' }] }] }] }], }; const d: VersionDecision = VersionResolver.resolve(remote, keys); expect(d.appVersion).assertEqual(49); - expect(d.appDownload).assertEqual('apk_url'); - expect(d.gameVersion).assertEqual(30); - expect(d.gameDownload).assertEqual('g_url'); + expect(d.appDownload).assertEqual('apk'); }); - it('no_match_returns_empty', 0, () => { + it('zero_game_version_does_not_override', 0, () => { + // channel 层 game_version=0 不应把 game 层 261 覆盖掉 const remote: RemoteConfig = { - agentlist: [{ agentid: 'OTHER', game_version: '99', game_download: 'x' }], + agentlist: [{ agentid: 'A1', + gamelist: [{ gameid: 'G1', game_version: 261, game_zip: 'hall.zip', + channellist: [{ channelid: 'C1', game_version: 0, game_zip: '', + marketlist: [{ marketid: 3, game_version: 0 }] }] }] }], + }; + const d: VersionDecision = VersionResolver.resolve(remote, keys); + expect(d.gameVersion).assertEqual(261); + expect(d.gameDownload).assertEqual('hall.zip'); + }); + + it('no_agent_match_returns_empty', 0, () => { + const remote: RemoteConfig = { + agentlist: [{ agentid: 'OTHER', + gamelist: [{ gameid: 'G1', game_version: 99, game_zip: 'x' }] }], + }; + const d: VersionDecision = VersionResolver.resolve(remote, keys); + expect(d.gameVersion).assertEqual(0); + expect(d.gameDownload).assertEqual(''); + }); + + it('no_game_match_returns_empty', 0, () => { + // gameid 回退值不匹配任何 gamelist 节点 + const remote: RemoteConfig = { + agentlist: [{ agentid: 'A1', + gamelist: [{ gameid: 'OTHER', game_version: 99, game_zip: 'x' }] }], }; const d: VersionDecision = VersionResolver.resolve(remote, keys); expect(d.gameVersion).assertEqual(0); @@ -95,49 +106,42 @@ export default function versionResolverTest() { const top: RemoteConfig = { showmessage: '停服维护公告' }; expect(VersionResolver.resolve(top, keys).showmessage).assertEqual('停服维护公告'); const layer: RemoteConfig = { - agentlist: [{ agentid: 'A1', showmessage: '代理层公告' }], + agentlist: [{ agentid: 'A1', showmessage: '代理层公告', + gamelist: [{ gameid: 'G1', showmessage: '游戏层公告' }] }], }; - expect(VersionResolver.resolve(layer, keys).showmessage).assertEqual('代理层公告'); + // 最后非空命中层(游戏层)生效 + expect(VersionResolver.resolve(layer, keys).showmessage).assertEqual('游戏层公告'); }); - it('empty_string_does_not_override', 0, () => { - // 深层空串不应覆盖浅层非空值 + it('realworld_lobby_sample', 0, () => { + // 复刻线上 tsgames.daoqi88.cn 大厅命中:game 261 + market3 app49(market 无 game_zip) + const liveKeys: MatchKeys = { + agentid: 'veRa0qrBf0df2K1G4de2tgfmVxB2jxpv', + channelid: 'FtJf073aa0d6rI1xD8J1Y42fINTm0ziK', + marketid: '3', + gameid: 'G2hw0ubng0zcoI0r4mx3H2yr4GejidwO', + }; const remote: RemoteConfig = { agentlist: [{ - agentid: 'A1', game_download: 'keep', game_version: '10', - channellist: [{ channelid: 'C1', game_download: '' }], + agentid: 'veRa0qrBf0df2K1G4de2tgfmVxB2jxpv', + gamelist: [{ + gameid: 'G2hw0ubng0zcoI0r4mx3H2yr4GejidwO', + game_version: 261, game_zip: 'http://tsgames.daoqi88.cn/zip2/gamehall.zip', game_size: '4.61M', + channellist: [{ + channelid: 'FtJf073aa0d6rI1xD8J1Y42fINTm0ziK', game_version: 0, + marketlist: [ + { marketid: 1, app_version: 1, app_download: '' }, + { marketid: 2, app_version: 43, app_download: 'itms://x' }, + { marketid: 3, app_version: 49, app_download: 'http://daoqi.daoqi88.cn/apk/gamehall.apk' }, + ], + }], + }], }], }; - const d: VersionDecision = VersionResolver.resolve(remote, keys); - expect(d.gameDownload).assertEqual('keep'); - }); - - it('empty_gameid_skips_game_tree', 0, () => { - // 大厅 gameid='' 不应误命中 gamelist 中 gameid 为空的节点 - const lobbyKeys: MatchKeys = { agentid: 'A1', channelid: 'C1', marketid: 'M1', gameid: '' }; - const remote: RemoteConfig = { - gamelist: [{ gameid: '', game_version: '99', game_download: 'should_not_match', - agentlist: [{ agentid: 'A1', game_version: '99', game_download: 'should_not_match' }] }], - agentlist: [{ agentid: 'A1', game_version: '10', game_download: 'agent_only', - channellist: [{ channelid: 'C1', - marketlist: [{ marketid: 'M1', - gamelist: [{ gameid: '', game_version: '88', game_download: 'should_not_match' }] }] }] }], - }; - const d: VersionDecision = VersionResolver.resolve(remote, lobbyKeys); - expect(d.gameVersion).assertEqual(10); - expect(d.gameDownload).assertEqual('agent_only'); - }); - - it('tie_prefers_nonempty_download', 0, () => { - // 两树 game_version 相等(20),代理树只有版本号无下载地址 → 应取游戏树的下载地址 - const remote: RemoteConfig = { - agentlist: [{ agentid: 'A1', game_version: '20' }], - gamelist: [{ gameid: 'G1', - agentlist: [{ agentid: 'A1', game_version: '20', game_download: 'game_url' }] }], - }; - const d: VersionDecision = VersionResolver.resolve(remote, keys); - expect(d.gameVersion).assertEqual(20); - expect(d.gameDownload).assertEqual('game_url'); + const d: VersionDecision = VersionResolver.resolve(remote, liveKeys); + expect(d.gameVersion).assertEqual(261); + expect(d.gameDownload).assertEqual('http://tsgames.daoqi88.cn/zip2/gamehall.zip'); + expect(d.appVersion).assertEqual(49); }); }); }