server/laf-cloud/functions/activityConfig/list.ts
2026-09-24 17:59:52 +08:00

76 lines
3.6 KiB
TypeScript

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<string>();
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 };
}
}