export type JungleRewardType = "coin" | "freeze" | "hammer" | "magic_wand" | "infinite_health"; export interface JungleRewardConfig { type: JungleRewardType; count: number; itemId?: number; } export interface JungleTierConfig { id: number; price: number; isPaid: boolean; productId: string; priceTemporary?: boolean; rewards: JungleRewardConfig[]; } export interface JungleTreasureConfig { activityId: string; version: number; totalTiers: number; tiers: JungleTierConfig[]; } /** 0=付费档未解锁,1=已解锁未领取,2=已领取。 */ export type JungleTierState = 0 | 1 | 2; /** 仅服务端存储,paidOrderNos 不返回客户端。 */ export interface JungleStoredState { activityId: string; configVersion: number; tierStates: JungleTierState[]; paidOrderNos: Record; revision: number; expiresAt: number; updatedAt: number; } export interface JungleUnlockResult { ok: boolean; changed: boolean; tierId?: number; state?: JungleStoredState; message?: string; } export interface JungleClaimResult { ok: boolean; tierId?: number; paidOrderNo?: string; state?: JungleStoredState; message?: string; } export const JUNGLE_TREASURE_ACTIVITY_ID = "jungle_treasure_v1"; export const JUNGLE_TREASURE_CONFIG_VERSION = 2; export const JUNGLE_TREASURE_TOTAL_TIERS = 51; export const JUNGLE_TREASURE_DURATION_MS = 7 * 24 * 60 * 60 * 1000; /** 后端礼包配置入口。修改数值时只调整此函数中的档位数据。 */ export function createJungleTreasureConfig(): JungleTreasureConfig { const tiers: JungleTierConfig[] = [ createTier(1, 0, [{ type: "coin", count: 100 }]), createTier(2, 0, [{ type: "hammer", count: 1 }]), createTier(3, 0, [ { type: "coin", count: 200 }, { type: "freeze", count: 1 }, { type: "magic_wand", count: 1 }, ]), createTier(4, 3, [{ type: "coin", count: 700 }], "jungle_treasure_1"), createTier(5, 0, [{ type: "hammer", count: 1 }]), createTier(6, 0, [{ type: "infinite_health", count: 900 }]), createTier(7, 0, [ { type: "coin", count: 200 }, { type: "magic_wand", count: 1 }, ]), createTier(8, 0, [{ type: "coin", count: 100 }]), createTier(9, 0, [{ type: "freeze", count: 1 }]), createTier(10, 6, [ { type: "coin", count: 1500 }, { type: "freeze", count: 1 }, { type: "magic_wand", count: 1 }, ], "jungle_treasure_2"), createTier(11, 0, [{ type: "infinite_health", count: 900 }]), createTier(12, 0, [{ type: "hammer", count: 1 }]), createTier(13, 0, [{ type: "coin", count: 500 }]), createTier(14, 0, [{ type: "hammer", count: 1 }]), createTier(15, 0, [{ type: "magic_wand", count: 1 }]), createTier(16, 9, [ { type: "coin", count: 2000 }, { type: "freeze", count: 1 }, { type: "magic_wand", count: 1 }, ], "jungle_treasure_3"), createTier(17, 0, [{ type: "infinite_health", count: 900 }]), createTier(18, 0, [ { type: "hammer", count: 2 }, { type: "freeze", count: 2 }, { type: "magic_wand", count: 2 }, ]), createTier(19, 0, [{ type: "coin", count: 200 }]), createTier(20, 0, [{ type: "freeze", count: 1 }]), createTier(21, 0, [{ type: "magic_wand", count: 1 }]), createTier(22, 15, [ { type: "coin", count: 3000 }, { type: "freeze", count: 3 }, { type: "magic_wand", count: 3 }, ], "jungle_treasure_4"), createTier(23, 0, [ { type: "hammer", count: 2 }, { type: "freeze", count: 1 }, ]), createTier(24, 0, [ { type: "infinite_health", count: 1800 }, { type: "freeze", count: 2 }, ]), createTier(25, 0, [{ type: "magic_wand", count: 2 }]), createTier(26, 0, [{ type: "hammer", count: 1 }]), createTier(27, 0, [{ type: "magic_wand", count: 1 }]), createTier(28, 24, [ { type: "coin", count: 3500 }, { type: "hammer", count: 2 }, ], "jungle_treasure_5"), createTier(29, 0, [ { type: "hammer", count: 2 }, { type: "freeze", count: 1 }, { type: "magic_wand", count: 1 }, ]), createTier(30, 0, [ { type: "freeze", count: 4 }, { type: "magic_wand", count: 4 }, ]), createTier(31, 0, [ { type: "infinite_health", count: 1800 }, { type: "freeze", count: 2 }, ]), createTier(32, 0, [{ type: "magic_wand", count: 2 }]), createTier(33, 0, [ { type: "freeze", count: 1 }, { type: "magic_wand", count: 1 }, ]), createTier(34, 42, [ { type: "coin", count: 10000 }, { type: "hammer", count: 3 }, { type: "freeze", count: 5 }, ], "jungle_treasure_6"), createTier(35, 0, [ { type: "coin", count: 3000 }, { type: "freeze", count: 2 }, { type: "magic_wand", count: 2 }, ]), createTier(36, 0, [ { type: "infinite_health", count: 3600 }, { type: "hammer", count: 2 }, ]), createTier(37, 0, [ { type: "coin", count: 2000 }, { type: "magic_wand", count: 5 }, ]), createTier(38, 0, [{ type: "freeze", count: 3 }]), createTier(39, 0, [{ type: "magic_wand", count: 3 }]), createTier(40, 68, [ { type: "coin", count: 21000 }, { type: "hammer", count: 10 }, ], "jungle_treasure_7"), createTier(41, 0, [ { type: "infinite_health", count: 7200 }, { type: "freeze", count: 15 }, ]), createTier(42, 0, [{ type: "magic_wand", count: 15 }]), createTier(43, 0, [{ type: "coin", count: 2500 }]), createTier(44, 0, [{ type: "freeze", count: 5 }]), createTier(45, 0, [{ type: "magic_wand", count: 5 }]), createTier(46, 98, [ { type: "coin", count: 35000 }, { type: "freeze", count: 15 }, { type: "magic_wand", count: 15 }, ], "jungle_treasure_8"), createTier(47, 0, [ { type: "infinite_health", count: 14400 }, { type: "hammer", count: 10 }, ]), createTier(48, 0, [{ type: "hammer", count: 5 }]), createTier(49, 0, [ { type: "freeze", count: 10 }, { type: "magic_wand", count: 5 }, ]), createTier(50, 0, [{ type: "coin", count: 2500 }]), createTier(51, 0, [ { type: "freeze", count: 5 }, { type: "magic_wand", count: 10 }, ]), ]; validateTierOrder(tiers); return { activityId: JUNGLE_TREASURE_ACTIVITY_ID, version: JUNGLE_TREASURE_CONFIG_VERSION, totalTiers: JUNGLE_TREASURE_TOTAL_TIERS, tiers, }; } export const JUNGLE_TREASURE_CONFIG = createJungleTreasureConfig(); export function createDefaultJungleState(now = Date.now()): JungleStoredState { return createStoredState( JUNGLE_TREASURE_CONFIG.tiers.map((tier) => tier.isPaid ? 0 : 1), {}, 0, now + JUNGLE_TREASURE_DURATION_MS, now, ); } /** 只处理当前 JSON 字符串状态;无状态时创建默认 7 天数据。 */ export function normalizeStoredJungleState(raw: unknown, now = Date.now()): JungleStoredState { const value = parseUnknownValue(raw); const defaultState = createDefaultJungleState(now); if (!value || Array.isArray(value) || !Array.isArray(value.tierStates)) { return defaultState; } const expiresAt = toPositiveTimestamp(value.expiresAt); if (!expiresAt) { return defaultState; } const tierStates = [...defaultState.tierStates]; for (let index = 0; index < JUNGLE_TREASURE_TOTAL_TIERS; index++) { const configuredTier = JUNGLE_TREASURE_CONFIG.tiers[index]; const state = toTierState(value.tierStates[index]); if (state !== null) { tierStates[index] = configuredTier.isPaid ? state : (state === 2 ? 2 : 1); } else { tierStates[index] = defaultState.tierStates[index]; } } return createStoredState( tierStates, normalizePaidOrderNos(value), toNonNegativeInteger(value.revision), expiresAt, toNonNegativeInteger(value.updatedAt) || now, ); } export function isCurrentJungleState(raw: unknown): boolean { const value = parseUnknownValue(raw); return Boolean( value && !Array.isArray(value) && value.activityId === JUNGLE_TREASURE_ACTIVITY_ID && value.configVersion === JUNGLE_TREASURE_CONFIG_VERSION && Array.isArray(value.tierStates) && value.tierStates.length === JUNGLE_TREASURE_TOTAL_TIERS && value.tierStates.every((item: unknown) => toTierState(item) !== null) && toPositiveTimestamp(value.expiresAt) > 0 ); } /** 用户表中按 JSON 字符串保存,避免 Laf 数据面板展开 51 项状态数组。 */ export function serializeJungleState(state: JungleStoredState): string { return JSON.stringify(state); } export function getJunglePaidTierByProductId(productId: unknown): JungleTierConfig | null { const normalizedProductId = String(productId || "").trim(); if (!normalizedProductId) { return null; } return JUNGLE_TREASURE_CONFIG.tiers.find( (tier) => tier.isPaid && tier.productId === normalizedProductId, ) || null; } export function unlockJungleTierByProductId( raw: unknown, productId: unknown, outTradeNo = "", now = Date.now(), ): JungleUnlockResult { const tier = getJunglePaidTierByProductId(productId); if (!tier) { return { ok: false, changed: false, message: "未找到对应的 Jungle 付费档位" }; } // Laf 可能暂时使用共享函数的旧类型缓存,这里按实际返回结构读取新字段。 const state: any = normalizeStoredJungleState(raw, now); const stateExpiresAt = Number(state["expiresAt"]); const tierStates = [...state.tierStates]; const paidOrderNos = { ...state.paidOrderNos }; let changed = false; if (tierStates[tier.id - 1] === 0) { tierStates[tier.id - 1] = 1; changed = true; } if (outTradeNo && !paidOrderNos[String(tier.id)]) { paidOrderNos[String(tier.id)] = outTradeNo; changed = true; } return { ok: true, changed, tierId: tier.id, state: changed ? createStoredState(tierStates, paidOrderNos, state.revision + 1, stateExpiresAt, now) : state, }; } /** 按最终需求,不检查前置档位;只检查目标档位自身是否为 1。 */ export function claimJungleTier(raw: unknown, rawTierId: unknown, now = Date.now()): JungleClaimResult { const tierId = Number(rawTierId); if (!Number.isInteger(tierId) || tierId < 1 || tierId > JUNGLE_TREASURE_TOTAL_TIERS) { return { ok: false, message: "礼包档位编号不合法" }; } const state: any = normalizeStoredJungleState(raw, now); const stateExpiresAt = Number(state["expiresAt"]); const currentState = state.tierStates[tierId - 1]; if (currentState === 0) { return { ok: false, tierId, message: "该付费档位尚未解锁" }; } if (currentState === 2) { return { ok: false, tierId, message: "该档位已经领取" }; } const tierStates = [...state.tierStates]; tierStates[tierId - 1] = 2; return { ok: true, tierId, paidOrderNo: state.paidOrderNos[String(tierId)] || "", state: createStoredState(tierStates, state.paidOrderNos, state.revision + 1, stateExpiresAt, now), }; } function createTier( id: number, price: number, rewards: JungleRewardConfig[], productId = "", priceTemporary = false, ): JungleTierConfig { const isPaid = price > 0; if (isPaid && !productId) { throw new Error(`[JungleTreasureConfig] 第 ${id} 档为付费档,必须配置 productId`); } if (!rewards.length || rewards.length > 5) { throw new Error(`[JungleTreasureConfig] 第 ${id} 档奖励数量必须为 1 至 5`); } return { id, price, isPaid, productId: isPaid ? productId : "", ...(priceTemporary ? { priceTemporary: true } : {}), rewards, }; } function validateTierOrder(tiers: JungleTierConfig[]): void { if (tiers.length !== JUNGLE_TREASURE_TOTAL_TIERS) { throw new Error(`[JungleTreasureConfig] 应配置 ${JUNGLE_TREASURE_TOTAL_TIERS} 档,实际为 ${tiers.length} 档`); } tiers.forEach((tier, index) => { if (tier.id !== index + 1) { throw new Error(`[JungleTreasureConfig] 第 ${index + 1} 个配置的 id 必须为 ${index + 1}`); } }); } function createStoredState( tierStates: JungleTierState[], paidOrderNos: Record, revision: number, expiresAt: number, updatedAt: number, ): JungleStoredState { return { activityId: JUNGLE_TREASURE_ACTIVITY_ID, configVersion: JUNGLE_TREASURE_CONFIG_VERSION, tierStates, paidOrderNos: { ...paidOrderNos }, revision, expiresAt, updatedAt, }; } function normalizePaidOrderNos(value: any): Record { const result: Record = {}; if (!value || Array.isArray(value) || !value.paidOrderNos || typeof value.paidOrderNos !== "object") { return result; } for (const tier of JUNGLE_TREASURE_CONFIG.tiers) { if (!tier.isPaid) { continue; } const orderNo = String(value.paidOrderNos[String(tier.id)] || "").trim(); if (orderNo) { result[String(tier.id)] = orderNo; } } return result; } function parseUnknownValue(raw: unknown): any { if (Array.isArray(raw) || (raw && typeof raw === "object")) { return raw; } if (typeof raw !== "string" || !raw.trim()) { return null; } try { return JSON.parse(raw); } catch { return null; } } function toTierState(value: unknown): JungleTierState | null { const state = Number(value); return state === 0 || state === 1 || state === 2 ? state : null; } function toNonNegativeInteger(value: unknown): number { const numberValue = Number(value); return Number.isInteger(numberValue) && numberValue >= 0 ? numberValue : 0; } function toPositiveTimestamp(value: unknown): number { const numberValue = Number(value); return Number.isFinite(numberValue) && numberValue > 0 ? Math.floor(numberValue) : 0; }