import cloud from "@lafjs/cloud"; import Utils from "@/Utils"; import { openPeriodForUser, openingRuleForUser } from "@/cloudRise/periods"; const LOOKAHEAD_MS = 7 * 24 * 60 * 60 * 1000; const REFRESH_MS = 30 * 1000; type ActivityInfo = { activityId: string; periodId: string; configVersion: string; startsAt: number; endsAt: number; unlockLevel: number; phase: "active" | "upcoming"; purchaseEnabled?: boolean; }; /** Only an explicit HomeScene request creates a period; listing is read only. */ async function cloudRiseActivities(now: number, _windowEndsAt: number, user: any) { const period = openPeriodForUser(user, now); const activities: ActivityInfo[] = period ? [{ activityId: "cloudRise", periodId: period.periodId, configVersion: period.configVersion, startsAt: period.startsAt, endsAt: period.endsAt, unlockLevel: period.unlockLevel, phase: "active" }] : []; return { activities, openingRules: user ? [await openingRuleForUser(user, now)] : [] }; } // 百人赛单独上线:黄金矿工暂不注册,也不导入其尚未部署的模块。 const providers = [cloudRiseActivities]; export async function activityList(now = Date.now(), user: any = null) { const windowEndsAt = now + LOOKAHEAD_MS; const results = await Promise.all(providers.map(provider => provider(now, windowEndsAt, user))); const candidates = results.flatMap(result => result.activities); const openingRules = results.flatMap(result => result.openingRules); // Each activity exposes one period: current first, otherwise the earliest upcoming period. // Overlapping current periods keep the existing latest-start precedence. candidates.sort((a, b) => a.activityId < b.activityId ? -1 : a.activityId > b.activityId ? 1 : Number(b.startsAt <= now) - Number(a.startsAt <= now) || (a.startsAt <= now ? b.startsAt - a.startsAt : a.startsAt - b.startsAt) || (a.periodId < b.periodId ? -1 : a.periodId > b.periodId ? 1 : 0)); const selected = new Set(); const activities = candidates.filter(activity => { if (selected.has(activity.activityId)) return false; selected.add(activity.activityId); return true; }); let refreshAt = now + REFRESH_MS; // A currently hidden test period may take precedence when it starts; refresh at that boundary too. for (const activity of candidates) { for (const boundary of [activity.startsAt, activity.endsAt]) { if (boundary > now) refreshAt = Math.min(refreshAt, boundary); } } return { schemaVersion: 1, serverNow: now, windowEndsAt, refreshAt, activities, openingRules }; } /** Authenticated users receive their personal CloudRise window; anonymous callers see public activities only. */ export default async function (ctx: FunctionContext) { try { const body = ctx.body || {}, supplied = body.uid !== undefined; let user: any = null; if (supplied) { const uid = typeof body.uid === "number" ? body.uid : typeof body.uid === "string" && /^[1-9]\d*$/.test(body.uid) ? Number(body.uid) : NaN; if (!Number.isSafeInteger(uid) || uid < 1) return { code: 0, msg: "玩家 uid 无效", data: null }; user = await (cloud.mongo.db as any).collection("users").findOne({ onlyId: uid }); if (!user?.token || !Utils.checkToken(body.token, user.token)) return { code: 0, msg: "token校验失败", data: null }; } return { code: 1, msg: "ok", data: await activityList(Date.now(), user) }; } catch (error) { console.error("activityConfig/list query failed", error); // A failed query must not look like a successful empty list and erase a client's cached entries. return { code: 0, msg: "活动信息暂时无法获取,请稍后重试", data: null }; } }