refactor(config): VersionResolver 抽成统一取值函数接口(与字段解耦)

matchedLayers() 一次算命中层链(含根伪层) + pickStr()/pickVer() 对任意字段做
4 层逐级回退取值;resolve() 各字段一行 selector 复用同一取值逻辑,新增字段免改取值核心。
替换原逐字段硬编码的 Acc.merge。行为不变,12 单测通过。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lanterngamescn
2026-06-27 18:52:22 +08:00
co-authored by Claude Opus 4.8
parent a6ef891507
commit e5d8eeb3b1
@@ -1,19 +1,22 @@
/**
* 分层版本决策 —— **4 层逐级回退模型**(纯函数,无 IO,便于单测)。
* 分层版本决策 —— 4 层逐级回退模型(纯函数,无 IO,便于单测)。
*
* 层级(由浅入深 1→4):agentlist(1) → gamelist(2) → channellist(3) → marketlist(4)
* 逐层用 agentid / gameid / channelid / marketid 匹配(marketid 数字与本地字符串归一比较)。
*
* 取值规则(**每个字段独立**):取**最深命中层**的非空值;该层缺失则逐级回退到上一层,
* 直到根节点(showmessage 根层可携带;game/app 升级字段根层无、回退至多到 agent 层)
* 实现等价:由浅入深遍历,每命中一层就对全字段做"非空(zip/download/size/msg)或非零(version)即覆盖前层"
* 故越深的非空值覆盖越浅、深层缺省则保留浅层值。"0"/空视为未设置、不覆盖。
* 不再区分"资源字段只在 game/channel/market、app 字段只在 market"——四层对所有字段一视同仁
* 取值是**统一函数接口**,与具体字段解耦:
* 1. matchedLayers() 一次算出命中层链(由浅入深;遇首个不匹配即止),含根伪层(仅 showmessage)
* 2. pickStr()/pickVer() 对**任意字段**用同一规则取值:取最深命中层的非空值
* 深层缺省则逐级回退到浅层直至根。空串(字符串)、"0"/空(版本) 视为未设置、不参与覆盖。
* 新增可下发字段 = resolve() 里加一行 selector,无需改取值逻辑
*/
import { AgentNode, ChannelNode, GameNode, MarketNode,
MatchKeys, RemoteConfig, Scalar, VersionDecision, VersionFields, asStr } from '../config/RemoteConfig';
/** 版本号转整数(空/"0"/非数字 → 0。 */
/** 从一层节点取某字段标量(不存在 → undefined)。供统一取值函数注入。 */
export type FieldSelector = (n: VersionFields) => Scalar | undefined;
/** 版本号转整数(空、"0"、非数字 → 0)。 */
function parseVer(v: Scalar | undefined): number {
const s: string = asStr(v);
if (s === '') {
@@ -23,90 +26,74 @@ function parseVer(v: Scalar | undefined): number {
return isNaN(n) ? 0 : n;
}
/** 版本是否"有效非零"(用于决定是否覆盖累积值)。 */
function hasVer(v: Scalar | undefined): boolean {
return parseVer(v) > 0;
}
/** 累积器:由浅入深逐层 merge,每字段非空/非零覆盖前层(= 取最深命中层值、深层缺省回退浅层)。 */
class Acc {
gameZip: string = '';
gameVersion: number = 0;
gameSize: string = '';
appDownload: string = '';
appVersion: number = 0;
appSize: string = '';
showmessage: string = '';
/** 单层全字段覆盖(agent/game/channel/market 每命中一层都调用)。 */
merge(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 gsize: string = n.game_size ?? '';
if (gsize !== '') {
this.gameSize = gsize;
}
const dl: string = n.app_download ?? '';
if (dl !== '') {
this.appDownload = dl;
}
if (hasVer(n.app_version)) {
this.appVersion = parseVer(n.app_version);
}
const asize: string = n.app_size ?? '';
if (asize !== '') {
this.appSize = asize;
}
const msg: string = n.showmessage ?? '';
if (msg !== '') {
this.showmessage = msg;
}
}
}
export class VersionResolver {
/** 4 层逐级回退:agent → game → channel → market,逐层 merge 全字段(深层覆盖浅层、缺省回退)。 */
static resolve(remote: RemoteConfig, keys: MatchKeys): VersionDecision {
const acc: Acc = new Acc();
const rootMsg: string = remote.showmessage ?? '';
if (rootMsg !== '') {
acc.showmessage = rootMsg;
}
/**
* 命中层链(由浅入深):根伪层(仅 showmessage) → agent → game → channel → market
* 遇首个不匹配的层即止。后续 pickStr/pickVer 在此链上做"深层覆盖浅层"。
*/
static matchedLayers(remote: RemoteConfig, keys: MatchKeys): VersionFields[] {
const layers: VersionFields[] = [{ showmessage: remote.showmessage }];
const agent: AgentNode | undefined =
(remote.agentlist ?? []).find((a: AgentNode) => (a.agentid ?? '') === keys.agentid);
if (agent !== undefined) {
acc.merge(agent); // 层1 agent
const game: GameNode | undefined =
(agent.gamelist ?? []).find((g: GameNode) => (g.gameid ?? '') === keys.gameid);
if (game !== undefined) {
acc.merge(game); // 层2 game
const channel: ChannelNode | undefined =
(game.channellist ?? []).find((c: ChannelNode) => (c.channelid ?? '') === keys.channelid);
if (channel !== undefined) {
acc.merge(channel); // 层3 channel
const market: MarketNode | undefined =
(channel.marketlist ?? []).find((m: MarketNode) => asStr(m.marketid) === keys.marketid);
if (market !== undefined) {
acc.merge(market); // 层4 market
}
}
if (agent === undefined) {
return layers;
}
layers.push(agent);
const game: GameNode | undefined =
(agent.gamelist ?? []).find((g: GameNode) => (g.gameid ?? '') === keys.gameid);
if (game === undefined) {
return layers;
}
layers.push(game);
const channel: ChannelNode | undefined =
(game.channellist ?? []).find((c: ChannelNode) => (c.channelid ?? '') === keys.channelid);
if (channel === undefined) {
return layers;
}
layers.push(channel);
const market: MarketNode | undefined =
(channel.marketlist ?? []).find((m: MarketNode) => asStr(m.marketid) === keys.marketid);
if (market !== undefined) {
layers.push(market);
}
return layers;
}
/** 统一取值(字符串字段):层链里取最深命中层的非空值;空串视为未设置。 */
static pickStr(layers: VersionFields[], select: FieldSelector): string {
let result: string = '';
for (let i = 0; i < layers.length; i++) {
const s: string = asStr(select(layers[i]));
if (s !== '') {
result = s; // 由浅入深,越深覆盖越浅
}
}
return result;
}
/** 统一取值(版本字段):层链里取最深命中层的非零版本;"0"/空视为未设置。 */
static pickVer(layers: VersionFields[], select: FieldSelector): number {
let result: number = 0;
for (let i = 0; i < layers.length; i++) {
const v: number = parseVer(select(layers[i]));
if (v > 0) {
result = v;
}
}
return result;
}
/** 产出版本决策:算一次层链,各字段用统一取值函数 + selector 解析(4 层逐级回退)。 */
static resolve(remote: RemoteConfig, keys: MatchKeys): VersionDecision {
const layers: VersionFields[] = VersionResolver.matchedLayers(remote, keys);
return {
showmessage: acc.showmessage,
appVersion: acc.appVersion,
appDownload: acc.appDownload,
appSize: acc.appSize,
gameVersion: acc.gameVersion,
gameDownload: acc.gameZip,
gameSize: acc.gameSize,
showmessage: VersionResolver.pickStr(layers, (n: VersionFields) => n.showmessage),
appVersion: VersionResolver.pickVer(layers, (n: VersionFields) => n.app_version),
appDownload: VersionResolver.pickStr(layers, (n: VersionFields) => n.app_download),
appSize: VersionResolver.pickStr(layers, (n: VersionFields) => n.app_size),
gameVersion: VersionResolver.pickVer(layers, (n: VersionFields) => n.game_version),
gameDownload: VersionResolver.pickStr(layers, (n: VersionFields) => n.game_zip),
gameSize: VersionResolver.pickStr(layers, (n: VersionFields) => n.game_size),
};
}
}