MatchMaster/server/laf-cloud/functions/jungleConfig.ts
WIN-GKKD951VVMJ\Administrator 6cf2e385e6 更新UI小鱼干活动
2026-08-28 19:19:27 +08:00

838 lines
25 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[];
}
export type JungleActivityType = "personal" | "calendar";
export interface JungleCalendarConfig {
enabled: boolean;
dayOfMonth: number;
startHour: number;
startMinute: number;
timezoneOffsetMinutes: number;
durationMs: number;
testUserIds: readonly string[];
}
export interface JungleCalendarPeriod {
key: string;
startsAt: number;
expiresAt: number;
}
/** 0=付费档未解锁,1=已解锁未领取,2=已领取。 */
export type JungleTierState = 0 | 1 | 2;
/** 仅服务端存储,paidOrderNos 不返回客户端。 */
export interface JungleStoredState {
activityId: string;
configVersion: number;
activityType: JungleActivityType;
periodKey: string;
startsAt: number;
firstTriggered: boolean;
lastCalendarPeriodKey: string;
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 type JungleRewardTotals = Record<JungleRewardType, number>;
export interface JungleExpiredSettlementResult {
ok: boolean;
claimedTierIds: number[];
rewards: JungleRewardTotals;
state?: JungleStoredState;
message?: string;
}
export interface JungleActivityResolution {
available: boolean;
changed: boolean;
state?: JungleStoredState;
reason: string;
}
export interface JungleActivityResolutionOptions {
allowCalendarStart?: boolean;
calendarConfig?: JungleCalendarConfig;
calendarUserId?: unknown;
}
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;
/** 全服周期正式配置:每月 28 日 00:00(北京时间)开始。 */
export const JUNGLE_TREASURE_CALENDAR_CONFIG: JungleCalendarConfig = {
enabled: true,
dayOfMonth: 28,
startHour: 0,
startMinute: 0,
timezoneOffsetMinutes: 8 * 60,
durationMs: JUNGLE_TREASURE_DURATION_MS,
testUserIds: [],
};
/** 后端礼包配置入口。修改数值时只调整此函数中的档位数据。 */
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: 5000 },
{ 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,
{
activityType: "personal",
periodKey: "",
startsAt: now,
firstTriggered: true,
lastCalendarPeriodKey: "",
},
);
}
/** 只处理当前 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,
{
activityType: value.activityType === "calendar" ? "calendar" : "personal",
periodKey: normalizePeriodKey(value.periodKey),
startsAt: toPositiveTimestamp(value.startsAt)
|| Math.max(1, expiresAt - JUNGLE_TREASURE_DURATION_MS),
// 任何已有 Jungle 状态都视为首次独立活动已经触发。
firstTriggered: true,
lastCalendarPeriodKey: normalizePeriodKey(value.lastCalendarPeriodKey),
},
);
}
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
&& (value.activityType === "personal" || value.activityType === "calendar")
&& typeof value.periodKey === "string"
&& toPositiveTimestamp(value.startsAt) > 0
&& value.firstTriggered === true
&& typeof value.lastCalendarPeriodKey === "string"
&& 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;
}
/** 返回当前正在进行的月历窗口;窗口外返回 null。 */
export function getCurrentJungleCalendarPeriod(
now = Date.now(),
config: JungleCalendarConfig = JUNGLE_TREASURE_CALENDAR_CONFIG,
): JungleCalendarPeriod | null {
if (!isValidJungleCalendarConfig(config) || !Number.isFinite(now) || now <= 0) {
return null;
}
const offsetMs = config.timezoneOffsetMinutes * 60 * 1000;
const localNow = new Date(now + offsetMs);
let year = localNow.getUTCFullYear();
let month = localNow.getUTCMonth();
let startsAt = Date.UTC(
year,
month,
config.dayOfMonth,
config.startHour,
config.startMinute,
) - offsetMs;
if (now < startsAt) {
month--;
if (month < 0) {
month = 11;
year--;
}
startsAt = Date.UTC(
year,
month,
config.dayOfMonth,
config.startHour,
config.startMinute,
) - offsetMs;
}
const expiresAt = startsAt + config.durationMs;
if (now < startsAt || now >= expiresAt) {
return null;
}
return {
key: `${year}-${String(month + 1).padStart(2, "0")}`,
startsAt,
expiresAt,
};
}
export function isValidJungleCalendarConfig(
config: JungleCalendarConfig = JUNGLE_TREASURE_CALENDAR_CONFIG,
): boolean {
return Boolean(
config
&& typeof config.enabled === "boolean"
&& Number.isInteger(config.dayOfMonth)
&& config.dayOfMonth >= 1
&& config.dayOfMonth <= 28
&& Number.isInteger(config.startHour)
&& config.startHour >= 0
&& config.startHour <= 23
&& Number.isInteger(config.startMinute)
&& config.startMinute >= 0
&& config.startMinute <= 59
&& Number.isInteger(config.timezoneOffsetMinutes)
&& Math.abs(config.timezoneOffsetMinutes) <= 14 * 60
&& Number.isFinite(config.durationMs)
&& config.durationMs > 0
&& Array.isArray(config.testUserIds)
&& config.testUserIds.every((uid) => typeof uid === "string" && Boolean(uid.trim()))
);
}
/** 全服开关关闭时,仅后端代码白名单中的用户可开启月历活动。 */
export function isJungleCalendarEnabledForUser(
userId: unknown,
config: JungleCalendarConfig = JUNGLE_TREASURE_CALENDAR_CONFIG,
): boolean {
if (!isValidJungleCalendarConfig(config)) {
return false;
}
if (config.enabled) {
return true;
}
const normalizedUserId = String(userId || "").trim();
return Boolean(
normalizedUserId
&& config.testUserIds.some((uid) => uid.trim() === normalizedUserId),
);
}
/** C 方案:只统计第一处未解锁付费档之前尚未领取的档位。 */
export function hasBlockingJungleClaims(raw: unknown, now = Date.now()): boolean {
const state = normalizeStoredJungleState(raw, now);
for (const tierState of state.tierStates) {
if (tierState === 0) {
break;
}
if (tierState === 1) {
return true;
}
}
return false;
}
/** 到期结算只领取第一处未解锁付费档之前的 state=1 档位。 */
export function settleExpiredJungleTiers(
raw: unknown,
now = Date.now(),
): JungleExpiredSettlementResult {
const state = normalizeStoredJungleState(raw, now);
const rewards = createEmptyRewardTotals();
if (now < state.expiresAt) {
return { ok: false, claimedTierIds: [], rewards, message: "Jungle 活动尚未结束" };
}
const tierStates = [...state.tierStates];
const claimedTierIds: number[] = [];
for (let index = 0; index < tierStates.length; index++) {
const tierState = tierStates[index];
if (tierState === 0) {
break;
}
if (tierState !== 1) {
continue;
}
tierStates[index] = 2;
claimedTierIds.push(index + 1);
for (const reward of JUNGLE_TREASURE_CONFIG.tiers[index].rewards) {
rewards[reward.type] += reward.count;
}
}
if (claimedTierIds.length === 0) {
return { ok: false, claimedTierIds, rewards, message: "当前没有可结算的 Jungle 奖励" };
}
return {
ok: true,
claimedTierIds,
rewards,
state: createStoredState(
tierStates,
state.paidOrderNos,
state.revision + 1,
state.expiresAt,
now,
getStoredStateMetadata(state),
),
};
}
/**
* read 调用时解析玩家应看到的活动。无历史状态优先创建首次独立活动;
* 已有状态结束后,只有月历窗口内且边界时未被旧活动阻挡才创建统一周期。
*/
export function resolveJungleActivity(
raw: unknown,
now = Date.now(),
options: JungleActivityResolutionOptions = {},
): JungleActivityResolution {
const config = options.calendarConfig || JUNGLE_TREASURE_CALENDAR_CONFIG;
const allowCalendarStart = options.allowCalendarStart !== false;
if (!hasStoredJungleValue(raw)) {
return {
available: true,
changed: true,
state: createDefaultJungleState(now),
reason: "personal_started",
};
}
let state = normalizeStoredJungleState(raw, now);
let changed = !isCurrentJungleState(raw) || typeof raw !== "string";
if (now < state.expiresAt) {
return { available: true, changed, state, reason: "active" };
}
const period = getCurrentJungleCalendarPeriod(now, config);
if (hasBlockingJungleClaims(state, now)) {
if (period && state.lastCalendarPeriodKey !== period.key) {
state = markCalendarPeriodHandled(state, period.key, now);
changed = true;
}
return { available: true, changed, state, reason: "claim_only" };
}
if (!isValidJungleCalendarConfig(config)) {
return { available: false, changed, state, reason: "invalid_calendar_config" };
}
if (!period) {
return { available: false, changed, state, reason: "outside_calendar_window" };
}
if (state.lastCalendarPeriodKey === period.key) {
return { available: false, changed, state, reason: "calendar_period_handled" };
}
if (state.expiresAt > period.startsAt) {
state = markCalendarPeriodHandled(state, period.key, now);
return { available: false, changed: true, state, reason: "blocked_at_calendar_start" };
}
if (!isJungleCalendarEnabledForUser(options.calendarUserId, config)) {
state = markCalendarPeriodHandled(state, period.key, now);
return { available: false, changed: true, state, reason: "calendar_disabled" };
}
if (!allowCalendarStart) {
return { available: false, changed, state, reason: "calendar_start_not_allowed" };
}
return {
available: true,
changed: true,
state: createCalendarJungleState(period, state, now),
reason: "calendar_started",
};
}
/** Jungle 下单只校验订单创建瞬间是否仍处于活动倒计时内。 */
export function isJungleActivityPurchasable(raw: unknown, now = Date.now()): boolean {
const value = parseUnknownValue(raw);
if (!value || Array.isArray(value) || !Array.isArray(value.tierStates)) {
return false;
}
const state = normalizeStoredJungleState(value, now);
return now >= state.startsAt && now < state.expiresAt;
}
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,
getStoredStateMetadata(state),
)
: state,
};
}
/** 活动中保持原单档规则;活动过期后禁止越过第一处未解锁付费档。 */
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: "该档位已经领取" };
}
if (now >= stateExpiresAt) {
const firstLockedIndex = state.tierStates.indexOf(0);
if (firstLockedIndex >= 0 && tierId - 1 > firstLockedIndex) {
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,
getStoredStateMetadata(state),
),
};
}
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,
metadata?: Partial<Pick<
JungleStoredState,
"activityType" | "periodKey" | "startsAt" | "firstTriggered" | "lastCalendarPeriodKey"
>>,
): JungleStoredState {
return {
activityId: JUNGLE_TREASURE_ACTIVITY_ID,
configVersion: JUNGLE_TREASURE_CONFIG_VERSION,
activityType: metadata?.activityType === "calendar" ? "calendar" : "personal",
periodKey: normalizePeriodKey(metadata?.periodKey),
startsAt: toPositiveTimestamp(metadata?.startsAt)
|| Math.max(1, expiresAt - JUNGLE_TREASURE_DURATION_MS),
firstTriggered: metadata?.firstTriggered !== false,
lastCalendarPeriodKey: normalizePeriodKey(metadata?.lastCalendarPeriodKey),
tierStates,
paidOrderNos: { ...paidOrderNos },
revision,
expiresAt,
updatedAt,
};
}
function createEmptyRewardTotals(): JungleRewardTotals {
return {
coin: 0,
freeze: 0,
hammer: 0,
magic_wand: 0,
infinite_health: 0,
};
}
function createCalendarJungleState(
period: JungleCalendarPeriod,
previousState: JungleStoredState,
now: number,
): JungleStoredState {
return createStoredState(
JUNGLE_TREASURE_CONFIG.tiers.map((tier) => tier.isPaid ? 0 : 1),
{},
previousState.revision + 1,
period.expiresAt,
now,
{
activityType: "calendar",
periodKey: period.key,
startsAt: period.startsAt,
firstTriggered: true,
lastCalendarPeriodKey: period.key,
},
);
}
function markCalendarPeriodHandled(
state: JungleStoredState,
periodKey: string,
now: number,
): JungleStoredState {
return createStoredState(
[...state.tierStates],
state.paidOrderNos,
state.revision + 1,
state.expiresAt,
now,
{
...getStoredStateMetadata(state),
lastCalendarPeriodKey: periodKey,
},
);
}
function getStoredStateMetadata(state: JungleStoredState) {
return {
activityType: state.activityType,
periodKey: state.periodKey,
startsAt: state.startsAt,
firstTriggered: state.firstTriggered,
lastCalendarPeriodKey: state.lastCalendarPeriodKey,
};
}
function hasStoredJungleValue(raw: unknown): boolean {
return raw !== undefined && raw !== null && !(typeof raw === "string" && !raw.trim());
}
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;
}
function normalizePeriodKey(value: unknown): string {
const periodKey = String(value || "").trim();
return /^\d{4}-\d{2}$/.test(periodKey) ? periodKey : "";
}