diff --git a/domain_resource/Index.ets b/domain_resource/Index.ets index d37f7d2..441c5e4 100644 --- a/domain_resource/Index.ets +++ b/domain_resource/Index.ets @@ -1,3 +1,19 @@ -// domain_resource HAR —— 领域服务层(导出入口 SSOT) -// 各子模块实现完成后在此统一 export。占位常量确保 HAR 可编译。 -export const domain_resource_MODULE_VERSION: string = '1.0.0'; +// domain_resource HAR —— 领域服务层(导出入口 SSOT,框架 §8) + +// 配置 +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, + VersionFields, MatchKeys, VersionDecision } from './src/main/ets/config/RemoteConfig'; + +// 版本决策(纯函数) +export { VersionResolver } from './src/main/ets/version/VersionResolver'; + +// 资源 +export { VersionXml } from './src/main/ets/resource/VersionXml'; +export { ResourceManager, ResourcePaths } from './src/main/ets/resource/ResourceManager'; +export { AppDataInjector, AppDataValues } from './src/main/ets/resource/AppDataInjector'; + +// 启动编排 +export { StartupOrchestrator, StartupStage, StartupResult, StageCallback } + from './src/main/ets/startup/StartupOrchestrator'; diff --git a/domain_resource/src/main/ets/config/AppConfig.ets b/domain_resource/src/main/ets/config/AppConfig.ets new file mode 100644 index 0000000..5e59f37 --- /dev/null +++ b/domain_resource/src/main/ets/config/AppConfig.ets @@ -0,0 +1,64 @@ +/** + * 本地配置(契约 §4.2,框架 §8.2)。 + * + * Android 用"目录名编码",本平台改为随包内置 rawfile/app_config.json(KV), + * 只要提供同名配置值即可,H5 完全无感知。本仓库实测值见 rawfile/app_config.json。 + */ + +/** 本地配置(全键,§4.2)。 */ +export interface AppConfig { + /** 代理商 ID(分层匹配 key)。 */ + agent: string; + /** 渠道 ID(分层匹配 key)。 */ + channel: string; + /** 资源解压父目录名。 */ + gamedir: string; + /** 资源解压后的游戏目录名(大厅入口所在,通常 gamehall)。 */ + gamestart: string; + /** App 版本号(与远程 app_version 比较)。 */ + appversion: string; + /** 市场 ID(分层匹配 key;也经桥 getmarketname 暴露 H5)。 */ + market: string; + /** 游戏 ID(空时回退用 version.xml game id)。 */ + gameid: string; + /** 大厅 H5 远程地址;空 → 本地 file:// 加载。 */ + weburl: string; + /** 远程配置地址('-'→'/' 后拼 .txt)。 */ + gameconfig: string; + /** 业务自定义值(经桥 getOther/getothername 暴露 H5)。 */ + other: string; + /** 推广/邀请码(写入 app_data.js 的 app_invitationcode)。 */ + tuiguang: string; +} + +/** 解析中间体(字段全可选,用于从 JSON 归一化)。 */ +export interface AppConfigRaw { + agent?: string; + channel?: string; + gamedir?: string; + gamestart?: string; + appversion?: string; + market?: string; + gameid?: string; + weburl?: string; + gameconfig?: string; + other?: string; + tuiguang?: string; +} + +/** 把可选解析体归一化为完整 AppConfig(缺失键 → 空串)。 */ +export function normalizeConfig(raw: AppConfigRaw): AppConfig { + return { + agent: raw.agent ?? '', + channel: raw.channel ?? '', + gamedir: raw.gamedir ?? '', + gamestart: raw.gamestart ?? '', + appversion: raw.appversion ?? '', + market: raw.market ?? '', + gameid: raw.gameid ?? '', + weburl: raw.weburl ?? '', + gameconfig: raw.gameconfig ?? '', + other: raw.other ?? '', + tuiguang: raw.tuiguang ?? '', + }; +} diff --git a/domain_resource/src/main/ets/config/ConfigManager.ets b/domain_resource/src/main/ets/config/ConfigManager.ets new file mode 100644 index 0000000..31bce68 --- /dev/null +++ b/domain_resource/src/main/ets/config/ConfigManager.ets @@ -0,0 +1,133 @@ +/** + * 配置管理(契约 §4,框架 §8.2)。负责: + * - 加载本地配置(rawfile/app_config.json)。 + * - 拼远程配置 URL(gameconfig '-'→'/' + .txt),GET 禁缓存 + ?a=时间戳。 + * - 校验(长度<30 视为错误文案 → 走 showmessage 阻断)、解析、二级 url 二次请求。 + * - 经 VersionResolver 产出版本决策。 + */ +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 } from './RemoteConfig'; +import { VersionResolver } from '../version/VersionResolver'; + +/** 本地配置 rawfile 文件名。 */ +export const LOCAL_CONFIG_RAWFILE: string = 'app_config.json'; +/** 远程配置错误文案阈值(<30 视为非 JSON 错误提示)。 */ +const MIN_REMOTE_LEN: number = 30; + +export class ConfigManager { + private static readonly log: Logger = Logger.tag('ConfigManager'); + private readonly context: common.Context; + private readonly local: AppConfig; + + private constructor(context: common.Context, local: AppConfig) { + this.context = context; + this.local = local; + } + + /** 从 rawfile 加载本地配置。 */ + static load(context: common.Context): ConfigManager { + const bytes: Uint8Array = context.resourceManager.getRawFileContentSync(LOCAL_CONFIG_RAWFILE); + const text: string = util.TextDecoder.create('utf-8').decodeToString(bytes); + const raw: AppConfigRaw = JSON.parse(text) as AppConfigRaw; + const local: AppConfig = normalizeConfig(raw); + ConfigManager.log.i(`local config loaded: agent=${local.agent} gamedir=${local.gamedir} weburl=${local.weburl}`); + return new ConfigManager(context, local); + } + + getLocal(): AppConfig { + return this.local; + } + + /** 分层匹配 key(§4.4)。 */ + matchKeys(): MatchKeys { + return { + agentid: this.local.agent, + channelid: this.local.channel, + marketid: this.local.market, + gameid: this.local.gameid, + }; + } + + /** 远程配置主 URL(不含 ?a= 时间戳)。 */ + remoteUrl(): string { + return `http://${this.local.gameconfig.replace(/-/g, '/')}.txt`; + } + + /** + * 拉取并解析远程配置。返回 RemoteConfig。 + * 约定:长度<30 → 返回 { showmessage: 错误文案 }(统一走 showmessage 阻断)。 + * 网络/解析失败抛出,由 StartupOrchestrator 降级到本地缓存。 + */ + async fetchRemote(): Promise { + const url: string = `${this.remoteUrl()}?a=${Date.now()}`; + const res = await HttpClient.getString(url); + if (res.code < 200 || res.code >= 300) { + throw new Error(`remote config http ${res.code}`); + } + const body: string = res.body.trim(); + if (body.length < MIN_REMOTE_LEN) { + 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; + } + + /** 拉取远程配置并产出版本决策(一步到位)。 */ + async resolve(): 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 && 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 && subGame.agentlist !== undefined) { + game.agentlist = subGame.agentlist; + ConfigManager.log.i('secondary game config merged'); + } + } + } + + 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; + } + } +} diff --git a/domain_resource/src/main/ets/config/RemoteConfig.ets b/domain_resource/src/main/ets/config/RemoteConfig.ets new file mode 100644 index 0000000..d9af627 --- /dev/null +++ b/domain_resource/src/main/ets/config/RemoteConfig.ets @@ -0,0 +1,82 @@ +/** + * 远程配置 DTO(契约 §4.3 2.0 格式:agentlist / gamelist 两棵树并存)。 + * + * 每层节点可携带可覆盖字段(§4.4):app_version/app_download/app_size/ + * game_version/game_download/game_size/url/showmessage。越深层越后赋值,覆盖前层。 + * 字段名与契约真实字段(非旧示意文档)严格一致。 + */ + +/** 每层可覆盖的版本字段(§4.4)。空串视为"未设置",不覆盖前层。 */ +export interface VersionFields { + app_version?: string; + app_download?: string; + app_size?: string; + game_version?: string; + game_download?: string; + game_size?: string; + /** 指向二级配置地址,非空则二次请求(§4.3)。 */ + url?: string; + showmessage?: string; +} + +/** 市场层节点(marketid 命中;其 gamelist 为该市场下的游戏覆盖)。 */ +export interface MarketNode extends VersionFields { + marketid?: string; + gamelist?: GameOverrideNode[]; +} + +/** 市场下的游戏覆盖节点(gameid 命中,设 app/game 升级)。 */ +export interface GameOverrideNode extends VersionFields { + gameid?: string; +} + +/** 渠道层节点。 */ +export interface ChannelNode extends VersionFields { + channelid?: string; + marketlist?: MarketNode[]; +} + +/** 代理层节点。 */ +export interface AgentNode extends VersionFields { + agentid?: string; + agentname?: string; + channellist?: ChannelNode[]; +} + +/** 游戏树顶层节点(gamelist[]:gameid 命中;url 非空则二次请求;内含 agentlist)。 */ +export interface GameTreeNode extends VersionFields { + gameid?: string; + agentlist?: AgentNode[]; +} + +/** 远程配置根(2.0 格式)。 */ +export interface RemoteConfig extends VersionFields { + /** 顶层公告:非空 → 阻断弹窗(§3.2)。 */ + showmessage?: string; + /** 代理配置树。 */ + agentlist?: AgentNode[]; + /** 游戏配置树(2.0 才有;缺失即 1.0 老格式)。 */ + gamelist?: GameTreeNode[]; +} + +/** 分层匹配 key(来自本地配置 §4.2)。 */ +export interface MatchKeys { + agentid: string; + channelid: string; + marketid: string; + gameid: string; +} + +/** 最终版本决策结果。 */ +export interface VersionDecision { + /** 顶层公告(非空则阻断)。 */ + showmessage: string; + /** App 升级(取代理树/游戏树中 version 更高者)。 */ + appVersion: number; + appDownload: string; + appSize: string; + /** 资源(游戏)升级(取更高者)。 */ + gameVersion: number; + gameDownload: string; + gameSize: string; +} diff --git a/domain_resource/src/main/ets/resource/AppDataInjector.ets b/domain_resource/src/main/ets/resource/AppDataInjector.ets new file mode 100644 index 0000000..282c5dc --- /dev/null +++ b/domain_resource/src/main/ets/resource/AppDataInjector.ets @@ -0,0 +1,69 @@ +/** + * app_data.js 注入(契约 §6,框架 §8.4)。 + * + * 🔴 时序铁律:必须在大厅 Web **加载页面之前**写入 <解压根>/gamehall/app_data.js + * (H5 首页用