63 lines
4.8 KiB
TypeScript
63 lines
4.8 KiB
TypeScript
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" }; }
|