440 lines
14 KiB
TypeScript
440 lines
14 KiB
TypeScript
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;
|
||
}
|