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 { 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" }; }