import cloud from '@lafjs/cloud' export default async function (ctx: FunctionContext) { console.log('Hello World') return { data: 'hi, laf' } } 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; 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; 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, revision: number, expiresAt: number, updatedAt: number, metadata?: Partial>, ): 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 { 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; } function normalizePeriodKey(value: unknown): string { const periodKey = String(value || "").trim(); return /^\d{4}-\d{2}$/.test(periodKey) ? periodKey : ""; }