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

63 lines
4.8 KiB
TypeScript
Raw Permalink 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.

import * as ConfigStore from "@/activityConfig/store";
import { calendar, periodDates, key, reject, isTestPeriod, testPeriodsEnabled, validatePeriodId, validateConfig } from "@/goldMiner/config";
// One CAS-protected schedule makes overlap checks atomic, including concurrent admin requests.
const scheduleId = key("goldMinerTestSchedule");
const scheduleQuery = { _id: scheduleId, activityId: "goldMiner", recordType: "testSchedule" };
export async function testPeriods() {
if (!testPeriodsEnabled()) return [];
return (await ConfigStore.records().findOne(scheduleQuery))?.periods || [];
}
export async function resolvePeriodDates(id: string): Promise<any> {
validatePeriodId(id);
if (!isTestPeriod(id)) return periodDates(id);
const period = (await testPeriods()).find((p: any) => p.periodId === id);
if (!period) reject("INVALID_PERIOD", "临时期不存在");
return { periodId: period.periodId, startsAt: period.startsAt, endsAt: period.endsAt,
nextPeriodId: period.nextPeriodId, nextStartsAt: period.nextStartsAt };
}
/** Immutable test fixtures; past starts are allowed for immediate testing, expired new periods are not. */
export async function publishTestPeriod(input: any) {
if (!testPeriodsEnabled()) reject("TEST_PERIOD_DISABLED", "需设置 PAYMENT_APP_ENV=test 和 GOLD_MINER_TEST_PERIODS_ENABLED=true");
const enabled = input.enabled === undefined ? true : input.enabled;
if (typeof enabled !== "boolean") reject("INVALID_INPUT", "enabled 必须为布尔值");
validatePeriodId(input.periodId);
if (!isTestPeriod(input.periodId)) reject("INVALID_PERIOD", "publish_test_period 的 periodId 必须使用 goldMiner:test: 前缀");
if (!Number.isSafeInteger(input.startsAt) || input.startsAt <= 0 || !Number.isFinite(new Date(input.startsAt).getTime())) reject("INVALID_PERIOD", "startsAt 必须为有效的正整数 Unix 毫秒时间戳,不能使用日期字符串");
if (!Number.isSafeInteger(input.endsAt) || input.endsAt <= 0 || !Number.isFinite(new Date(input.endsAt).getTime())) reject("INVALID_PERIOD", "endsAt 必须为有效的正整数 Unix 毫秒时间戳,不能使用日期字符串");
if (input.endsAt <= input.startsAt) reject("INVALID_PERIOD", "endsAt 必须严格晚于 startsAt");
const nextPeriodId = input.nextPeriodId || calendar(input.endsAt - 1).nextPeriodId;
const next = await resolvePeriodDates(nextPeriodId);
if (next.startsAt < input.endsAt || nextPeriodId === input.periodId) reject("INVALID_PERIOD", "顺延目标期必须在本期结束之后开始");
if (!input.config || typeof input.config !== "object" || Array.isArray(input.config)) reject("CONFIG_UNAVAILABLE", "config 必须为 JSON 对象,请把活动参数放在请求的 config 字段内");
const configSnapshot = validateConfig({ ...input.config, effectiveFromPeriodId: input.periodId, publishedAt: Date.now() });
const period = { periodId: input.periodId, startsAt: input.startsAt, endsAt: input.endsAt,
nextPeriodId, nextStartsAt: next.startsAt, configVersion: configSnapshot.configVersion,
configSnapshot, configHash: key(configSnapshot), enabled };
const records = ConfigStore.records();
try { await records.updateOne(scheduleQuery, { $setOnInsert: { ...scheduleQuery, revision: 0, periods: [] } }, { upsert: true }); }
catch (error) { if (!await records.findOne(scheduleQuery)) throw error; }
for (let attempt = 0; attempt < 12; attempt++) {
const schedule = await records.findOne(scheduleQuery);
const existing = schedule.periods.find((p: any) => p.periodId === period.periodId);
if (existing) {
const retrySnapshot = { ...configSnapshot, publishedAt: existing.configSnapshot.publishedAt };
if (JSON.stringify({ ...existing, enabled: ConfigStore.isEnabled(existing) }) !== JSON.stringify({ ...period, configSnapshot: retrySnapshot, configHash: key(retrySnapshot) }))
reject("CONFIG_UNAVAILABLE", "临时期已发布,不可修改;请使用新的期标识");
return existing;
}
if (period.endsAt <= Date.now()) reject("INVALID_PERIOD", "不能新增已经结束的临时期");
const overlap = schedule.periods.find((p: any) => p.startsAt < period.endsAt && p.endsAt > period.startsAt);
if (overlap) reject("CONFIG_UNAVAILABLE", `临时期时间不能重叠,已存在冲突期 ${overlap.periodId}(startsAt=${overlap.startsAt}, endsAt=${overlap.endsAt})`);
if (schedule.periods.length >= 100) reject("CONFIG_UNAVAILABLE", "测试排期已达100期,请在新的独立测试数据库中继续测试");
const result = await records.updateOne({ ...scheduleQuery, revision: schedule.revision },
{ $set: { periods: [...schedule.periods, period], updatedAt: Date.now() }, $inc: { revision: 1 } });
if (result.modifiedCount === 1) return period;
}
reject("RETRYABLE");
}
export default async function () { return { code: 0, msg: "internal module" }; }