import cloud from "@lafjs/cloud"; import * as ConfigStore from "@/activityConfig/store"; import { randomBytes } from "crypto"; const users = (): any => (cloud.mongo.db as any).collection("users"); const terminal = (run: any): boolean => !!run && ["completed", "failed", "expired"].includes(run.status); const stateOf = (user: any): any => typeof user.cloudRiseState === "string" ? JSON.parse(user.cloudRiseState || "{}") : user.cloudRiseState || {}; /** Read only: a legacy active challenge retains its existing deadline through the rollout. */ export function periodForUser(user: any): any { if (!user) return null; if (user.cloudRisePeriod) return user.cloudRisePeriod; const run = stateOf(user).run; if (!run || terminal(run)) return null; return { periodId: run.periodId, configVersion: "legacy", startsAt: run.startedAt, endsAt: run.expiresAt, durationHours: run.durationHours || 24, unlockLevel: 1, pools: run.pools || [10000, 15000, 20000] }; } export function openPeriodForUser(user: any, now: number): any { const period = periodForUser(user); if (!period || !(period.startsAt <= now && now < period.endsAt)) return null; const run = stateOf(user).run; return run?.periodId === period.periodId && terminal(run) ? null : period; } /** Fractional hours are a server-side test-only setting; client flags cannot enable them. */ export function validDurationHours(hours: any): boolean { return Number.isFinite(hours) && hours > 0 && (Number.isSafeInteger(hours) || process.env.PAYMENT_APP_ENV === "test") && Number.isSafeInteger(hours * 3600000) && hours * 3600000 >= 1; } /** A published version is now a template; old global signup end times do not close personal periods. */ export async function templateAt(now: number): Promise { const row = await ConfigStore.latestEnabled("cloudRise", now); if (!row) return null; const config = row.config, durationHours = config?.durationHours ?? 24; const pools = config?.pools ?? [10000, 15000, 20000]; if (!config || !Number.isSafeInteger(config.unlockLevel) || config.unlockLevel < 1 || !validDurationHours(durationHours) || !Number.isSafeInteger(now + durationHours * 3600000) || !Array.isArray(pools) || pools.length !== 3 || !pools.every((p: any) => Number.isSafeInteger(p) && p > 0)) { throw new Error("百人赛活动模板无效"); } return { configVersion: row.configVersion, durationHours, unlockLevel: config.unlockLevel, pools: [...pools] }; } export const COOLDOWN_MS = 12 * 3600000; /** Expiry uses the original deadline, even when the server notices it hours later. */ export function lastEndedAt(user: any, now: number): number | null { const period = periodForUser(user), run = stateOf(user).run; const deadline = Number(period?.endsAt ?? run?.expiresAt); if (terminal(run) && (!period || run.periodId === period.periodId)) { if (run.status !== "expired") { const ended = Number(run.stages?.[run.stages.length - 1]?.endedAt); if (Number.isFinite(ended) && ended > 0) return Number.isFinite(deadline) ? Math.min(ended, deadline) : ended; } return Number.isFinite(deadline) && deadline > 0 ? deadline : null; } return Number.isFinite(deadline) && deadline > 0 && deadline <= now ? deadline : null; } /** Read only: HomeScene receives the facts needed to decide whether to request opening. */ export async function openingRuleForUser(user: any, now: number): Promise { const template = await templateAt(now); return { activityId: "cloudRise", enabled: !!template, unlockLevel: template?.unlockLevel ?? 0, cooldownMs: COOLDOWN_MS, lastEndedAt: lastEndedAt(user, now), hasOpenPeriod: !!openPeriodForUser(user, now) }; } /** Called only by an explicit HomeScene request; login, progress reports and queries never create periods. */ export async function openForUser(onlyId: number, now = Date.now()): Promise { for (let retry = 0; retry < 5; retry++) { const user = await users().findOne({ onlyId }); if (!user) throw new Error("玩家不存在"); const existing = openPeriodForUser(user, now); if (existing) return existing; // Idempotent retries, including two devices entering HomeScene together. const template = await templateAt(now); if (!template) return null; // A valid enabled template is needed to construct the period. const period = { ...template, periodId: "cloudRise:" + onlyId + ":" + randomBytes(12).toString("hex"), startsAt: now, endsAt: now + template.durationHours * 3600000 }; const result = await users().updateOne({ onlyId, cloudRisePeriod: user.cloudRisePeriod === undefined ? { $exists: false } : user.cloudRisePeriod, cloudRiseState: user.cloudRiseState === undefined ? { $exists: false } : user.cloudRiseState, }, { $set: { cloudRisePeriod: period } }); if (result.modifiedCount === 1) return period; } throw new Error("百人赛活动状态正在更新,请重试"); }