启动链路真实化:版本解析层对齐线上配置,复刻 Android chuliversion_1(H5 零改动)
对照原 Android 工程 + 线上实测配置,修正 4 处会破坏 H5 适配的根因: 1. 配置树层级改为真实的 agentlist→gamelist→channellist→marketlist (原抄了废弃 Bean1 的 agent→channel→market→game)。 2. 资源下载字段名 game_download → 真实的 game_zip(原永远取空,资源升级永不触发)。 3. marketid/版本为数字,匹配/比较做 string↔number 归一(原 === "3" 永不命中 market 层)。 4. 大厅 app_config.gameid 为空时回退 version.xml 的 <game id>(复刻 versd.getGameid()), 否则大厅永远定位不到自己的 game_zip。 实现: - RemoteConfig/VersionResolver 按真实格式重写,VersionResolver 复刻 chuliversion_1 的 game/channel/market 三层累积 + market 层 app 升级,"0"/空不覆盖。 - ConfigManager 拆分 fetchRemote(IO) / resolveWith(纯函数 + gameid 回退),移除失效的二级 url 编排。 - VersionXml 补解析 <game id>;ResourceManager 增 localGameId()。 - StartupOrchestrator 调整时序:内置资源(version.xml 种子)就绪→取 gameid→解析远程,拉取仍并行。 - 内置 gamehall_builtin.zip 改为真实版本种子(game id=G2hw... version=42),离线首启阻断、联网下载真实大厅。 - app_config.json gameconfig 切生产端点(去 _test),不再使用测试配置。 - VersionResolver 单测重写为真实格式 + 线上大厅样例。 注:远程配置格式属原生内部消费(H5 不可见),按铁律以线上服务器为准对齐。devecocli build 通过。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
4b671c7437
commit
799e78d8af
@@ -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';
|
||||
|
||||
// 版本决策(纯函数)
|
||||
|
||||
@@ -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<VersionDecision> {
|
||||
/** 纯函数:用已拉取的远程配置 + gameid 回退值产出版本决策。 */
|
||||
resolveWith(remote: RemoteConfig, fallbackGameId: string): VersionDecision {
|
||||
return VersionResolver.resolve(remote, this.matchKeys(fallbackGameId));
|
||||
}
|
||||
|
||||
/** 便捷:拉取 + 解析一步到位(gameid 回退值由调用方提供)。 */
|
||||
async resolve(fallbackGameId: string = ''): Promise<VersionDecision> {
|
||||
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<void> {
|
||||
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<RemoteConfig | undefined> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<void> {
|
||||
this.kv.putString(KEY_URLPATH, this.cachedPaths.urlpath);
|
||||
|
||||
@@ -15,6 +15,8 @@ export interface VersionXmlInfo {
|
||||
version: number;
|
||||
/** 子 <game name="..."> 游戏名(缺失 → '')。 */
|
||||
gameName: string;
|
||||
/** 子 <game id="..."> 游戏 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') {
|
||||
// 根 <game> 无 name 属性;只有子 <game id name/> 触发,正是所需游戏名
|
||||
info.gameName = val;
|
||||
} else if (name === 'id' && currentTag === 'game') {
|
||||
// 子 <game id="..."> 的 id:大厅 gameid 回退匹配 key(根 <game> 无 id 属性)
|
||||
info.gameId = val;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
@@ -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<VersionDecision | undefined> =
|
||||
(async (): Promise<VersionDecision | undefined> => {
|
||||
const remotePromise: Promise<RemoteConfig | undefined> =
|
||||
(async (): Promise<RemoteConfig | undefined> => {
|
||||
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}`);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user