155 lines
9.2 KiB
TypeScript
155 lines
9.2 KiB
TypeScript
import Utils from '../Script/module/Pay/Utils';
|
|
import WeeklyCache from '../ranking_v2_entry/WeeklyCache';
|
|
import { RankingSnapshot, RankingReward, RankingUser } from './RankingData';
|
|
|
|
function rewards(items: any[]): RankingReward[] {
|
|
if (!Array.isArray(items)) throw new Error('奖励配置异常');
|
|
return items.map(item => {
|
|
if (!item || typeof item.type !== 'string' || !Number.isSafeInteger(item.count) || item.count <= 0) throw new Error('奖励配置异常');
|
|
return { id: item.type, name: item.type === 'coin' ? '金币' : item.type, amount: item.count };
|
|
});
|
|
}
|
|
export function weeklySnapshot(data: any, uid: string, startedAt: number): RankingSnapshot {
|
|
if (!data || !['open', 'closed'].includes(data.status) || !Number.isFinite(data.serverTime)
|
|
|| !Number.isFinite(data.cacheTtlSeconds) || data.cacheTtlSeconds < 0
|
|
|| !data.currentWeek || !Number.isFinite(data.currentWeek.endAt)
|
|
|| typeof data.canClaim !== 'boolean') throw new Error('周竞赛返回数据异常');
|
|
const board = data.board;
|
|
if (board && (!Array.isArray(board.rank) || !Array.isArray(board.reward))) throw new Error('周竞赛榜单异常');
|
|
if (data.canClaim && (!board || !data.pendingReward || !/^[a-f0-9]{64}$/.test(data.pendingReward.claimId))) throw new Error('领取凭证异常');
|
|
const users: RankingUser[] = board ? board.rank.map(row => ({
|
|
id: String(row.uid), name: row.username || '匿名玩家', avatar: String(row.useravatar || ''),
|
|
rank: row.rank, score: row.weeklyWins, trophy: 0
|
|
})) : [];
|
|
const me = cc.fx.GameConfig.GM_INFO;
|
|
return {
|
|
users, self: users.find(user => user.id === uid) || {
|
|
id: uid, name: me.username || '玩家', avatar: String(me.useravatar || ''),
|
|
rank: board ? board.selfRank : null, score: board ? board.weeklyWins : 0, trophy: 0
|
|
}, mock: false,
|
|
weekly: {
|
|
seasonId: board ? board.weekId : data.currentWeek.weekId, groupId: '',
|
|
serverTime: data.serverTime + Math.max(0, Date.now() - startedAt),
|
|
endsAt: data.currentWeek.endAt, phase: data.canClaim ? 'settled' : 'active',
|
|
prizes: board ? board.reward.map(item => ({ rank: item.rank, items: rewards(item.rewards) })) : [],
|
|
claimStatus: data.canClaim ? 'claimable' : 'unavailable',
|
|
claimId: data.canClaim ? data.pendingReward.claimId : '',
|
|
closed: data.status === 'closed' && !board,
|
|
historical: !!board && board.weekId !== data.currentWeek.weekId,
|
|
rules: '按服务器返回的本组排名展示。周一09:00结算;未领取的历史奖励保留一份,后续结算不会叠加。'
|
|
}
|
|
};
|
|
}
|
|
|
|
/** One client per account/environment, shared across panel reopenings. */
|
|
export default class WeeklyCompetition {
|
|
private static clients: { [scope: string]: WeeklyCompetition } = {};
|
|
static current(): WeeklyCompetition {
|
|
const me = cc.fx.GameConfig.GM_INFO;
|
|
if (!me.uid || !me.token) throw new Error('请先登录');
|
|
const scope = WeeklyCache.scope(Utils.serverUrl(), me.uid);
|
|
return this.clients[scope] || (this.clients[scope] = new WeeklyCompetition(String(me.uid), scope));
|
|
}
|
|
private inflight: Promise<RankingSnapshot> = null;
|
|
private claiming: Promise<RankingReward[]> = null;
|
|
private generation = 0;
|
|
private ledger: any;
|
|
afterClaim: RankingSnapshot;
|
|
private constructor(private uid: string, private scope: string) {
|
|
// Corrupt reward journals must not silently become an empty ledger.
|
|
const raw = cc.sys.localStorage.getItem(this.journalKey());
|
|
this.ledger = raw ? JSON.parse(raw) : { pending: '', grants: {} };
|
|
if (!this.ledger.grants) throw new Error('奖励记录异常,请联系客服');
|
|
}
|
|
private journalKey() { return 'weeklyCompetition:rewards:v1:' + this.scope; }
|
|
private persist() { cc.sys.localStorage.setItem(this.journalKey(), JSON.stringify(this.ledger)); }
|
|
private active() {
|
|
const me = cc.fx.GameConfig.GM_INFO;
|
|
if (String(me.uid) !== this.uid || WeeklyCache.scope(Utils.serverUrl(), me.uid) !== this.scope) throw new Error('账号或环境已切换,请重新打开排行榜');
|
|
}
|
|
private async post(path: string, body: any = {}): Promise<any> {
|
|
this.active();
|
|
const result: any = await new Promise(resolve => Utils.POSTJSON(path, { ...body, uid: this.uid }, resolve));
|
|
this.active();
|
|
if (!result || result.code !== 1) {
|
|
const messages = {
|
|
UNAUTHORIZED: '登录已失效,请重新登录', CLAIM_NOT_FOUND: '奖励记录不存在,请刷新榜单后重试',
|
|
CONFIG_INVALID: '奖励配置暂不可用,请联系管理员', REWARD_VERSION_NOT_FOUND: '历史奖励配置缺失,请联系管理员',
|
|
INVALID_INPUT: '领取凭证无效,请刷新榜单', RETRYABLE: '服务暂不可用,请稍后重试'
|
|
};
|
|
const error: any = new Error(messages[result && result.errorCode] || '网络请求未确认,请重试');
|
|
error.code = result && result.errorCode; throw error;
|
|
}
|
|
return result.data;
|
|
}
|
|
load(force = false): Promise<RankingSnapshot> {
|
|
this.active();
|
|
if (force) WeeklyCache.invalidate(this.scope);
|
|
if (this.claiming) return this.claiming.then(() => this.afterClaim);
|
|
if (this.ledger.pending) return this.claim('', this.ledger.pending).then(() => this.afterClaim);
|
|
const cache = WeeklyCache.read(this.scope);
|
|
if (cache) return Promise.resolve(weeklySnapshot(cache.data, this.uid, cache.startedAt));
|
|
if (this.inflight) return this.inflight;
|
|
const generation = this.generation, revision = WeeklyCache.revision(this.scope), startedAt = Date.now();
|
|
const request = this.post('getWeeklyCompetitionRank').then(data => {
|
|
if (generation !== this.generation) {
|
|
return this.claiming ? this.claiming.then(() => this.afterClaim) : Promise.reject(new Error('榜单状态已变化,请重试'));
|
|
}
|
|
const snapshot = weeklySnapshot(data, this.uid, startedAt);
|
|
WeeklyCache.write(this.scope, data, startedAt, revision);
|
|
return snapshot;
|
|
});
|
|
this.inflight = request;
|
|
request.then(() => { if (this.inflight === request) this.inflight = null; },
|
|
() => { if (this.inflight === request) this.inflight = null; });
|
|
return request;
|
|
}
|
|
claim(_seasonId: string, claimId: string): Promise<RankingReward[]> {
|
|
this.active();
|
|
if (this.claiming) return this.claiming;
|
|
const id = this.ledger.pending || claimId;
|
|
if (!/^[a-f0-9]{64}$/.test(id)) return Promise.reject(new Error('领取凭证无效'));
|
|
++this.generation;
|
|
WeeklyCache.invalidate(this.scope);
|
|
this.ledger.pending = id; this.persist();
|
|
const work = this.deliver(id);
|
|
this.claiming = work;
|
|
work.then(() => { this.claiming = null; }, () => { this.claiming = null; });
|
|
return work;
|
|
}
|
|
private async deliver(id: string): Promise<RankingReward[]> {
|
|
const startedAt = Date.now(), revision = WeeklyCache.revision(this.scope);
|
|
let data;
|
|
try { data = await this.post('claimWeeklyCompetitionReward', { claimId: id }); }
|
|
catch (error) {
|
|
if (error.code === 'CLAIM_NOT_FOUND') { this.ledger.pending = ''; this.persist(); }
|
|
throw error;
|
|
}
|
|
const snapshot = weeklySnapshot(data, this.uid, startedAt), receipt = data.receipt;
|
|
if (!receipt || receipt.claimId !== id) throw new Error('奖励回执不匹配');
|
|
const items = rewards(receipt.rewards);
|
|
// Same supported reward types as GoldMiner. Never silently discard unknown items.
|
|
if (!items.length || items.some(item => item.id !== 'coin')) throw new Error('奖励类型暂不支持,请更新游戏或联系客服');
|
|
await Utils.withCoinSave(async () => {
|
|
this.active();
|
|
const record = this.ledger.grants[id];
|
|
if (record && record.phase === 'saved') return;
|
|
if (record && record.phase === 'saving') throw new Error('金币保存结果待核对,请联系客服,勿重复领取');
|
|
const amount = items.reduce((sum, item) => sum + item.amount, 0);
|
|
const balance = Math.floor(cc.fx.GameConfig.GM_INFO.coin) + amount;
|
|
if (!Number.isSafeInteger(balance) || balance < amount) throw new Error('金币余额异常');
|
|
// Durable journal before changing the wallet; ambiguous uploads never add twice.
|
|
this.ledger.grants[id] = { phase: 'saving', receipt, balance }; this.persist();
|
|
cc.fx.GameConfig.GM_INFO.coin = balance;
|
|
cc.fx.StorageMessage.setStorage('coin', { coin: balance, timestamp: Date.now() });
|
|
const saved = await this.post('userCoin', { action: 'save', coinAmount: balance });
|
|
if (!saved || saved.coinAmount !== balance) throw new Error('金币保存结果待核对,请联系客服');
|
|
this.ledger.grants[id].phase = 'saved'; this.persist();
|
|
});
|
|
this.ledger.pending = ''; this.persist();
|
|
this.afterClaim = weeklySnapshot(data, this.uid, startedAt);
|
|
WeeklyCache.write(this.scope, data, startedAt, revision);
|
|
return items;
|
|
}
|
|
}
|