MatchMaster/server/laf-cloud/functions/jungleConfig.ts
2026-08-07 10:59:22 +08:00

440 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<string, string>;
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<string, string>,
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<string, string> {
const result: Record<string, string> = {};
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;
}