576 lines
24 KiB
JavaScript
576 lines
24 KiB
JavaScript
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||
import { useAppScope } from "@/app/app-scope/AppScopeProvider.jsx";
|
||
import { getVipConfig, getVipProgram, updateVipConfig, updateVipProgram } from "@/features/vip-config/api";
|
||
import { p1BenefitCatalog, vipProgramDefaults } from "@/features/vip-config/constants.js";
|
||
import { normalizeVipBenefitMetadata } from "@/features/vip-config/metadata.js";
|
||
import { useVipConfigAbilities } from "@/features/vip-config/permissions.js";
|
||
import { vipConfigFormSchema, vipProgramFormSchema } from "@/features/vip-config/schema.js";
|
||
import { listResourceGroups, listResources } from "@/features/resources/api";
|
||
import { parseForm } from "@/shared/forms/validation";
|
||
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
||
|
||
const dayMillis = 24 * 60 * 60 * 1000;
|
||
|
||
export function useVipConfigPage() {
|
||
const abilities = useVipConfigAbilities();
|
||
const { appCode } = useAppScope();
|
||
const { showToast } = useToast();
|
||
const initialProgram = defaultProgram(appCode);
|
||
const [program, setProgram] = useState(initialProgram);
|
||
const [config, setConfig] = useState({ levels: defaultLevels(initialProgram) });
|
||
const [programForm, setProgramForm] = useState(programToForm(initialProgram));
|
||
const [form, setForm] = useState({
|
||
levels: defaultLevels(initialProgram).map((level) => levelToForm(level, initialProgram)),
|
||
});
|
||
const [resourceGroups, setResourceGroups] = useState([]);
|
||
const [resources, setResources] = useState([]);
|
||
const [resourcesLoading, setResourcesLoading] = useState(false);
|
||
const [editorMode, setEditorMode] = useState("");
|
||
const [error, setError] = useState(null);
|
||
const [loading, setLoading] = useState(false);
|
||
const [saving, setSaving] = useState(false);
|
||
const reloadSequence = useRef(0);
|
||
const resourceLoadSequence = useRef(0);
|
||
const resourcesApp = useRef("");
|
||
|
||
const activeCount = useMemo(
|
||
() => (config.levels || []).filter((level) => level.status === "active").length,
|
||
[config.levels],
|
||
);
|
||
|
||
const reload = useCallback(async () => {
|
||
const sequence = ++reloadSequence.current;
|
||
setLoading(true);
|
||
setError(null);
|
||
try {
|
||
const [remoteProgram, remoteConfig, groups] = await Promise.all([
|
||
getVipProgram(),
|
||
getVipConfig(),
|
||
listResourceGroups({ page: 1, page_size: 100, status: "active" }),
|
||
]);
|
||
if (sequence !== reloadSequence.current) {
|
||
return;
|
||
}
|
||
const nextProgram = normalizeProgram(remoteProgram, appCode);
|
||
const levels = completeVipLevels(remoteConfig.levels || [], nextProgram);
|
||
setProgram(nextProgram);
|
||
setProgramForm(programToForm(nextProgram));
|
||
setConfig({ ...remoteConfig, levels });
|
||
setForm({ levels: levels.map((level) => levelToForm(level, nextProgram)) });
|
||
setResourceGroups(groups.items || []);
|
||
} catch (err) {
|
||
if (sequence !== reloadSequence.current) {
|
||
return;
|
||
}
|
||
setError(err);
|
||
showToast(err.message || "加载 VIP 配置失败", "error");
|
||
} finally {
|
||
if (sequence === reloadSequence.current) {
|
||
setLoading(false);
|
||
}
|
||
}
|
||
}, [appCode, showToast]);
|
||
|
||
const loadResources = useCallback(async () => {
|
||
if (resourcesApp.current === appCode) {
|
||
return;
|
||
}
|
||
const sequence = ++resourceLoadSequence.current;
|
||
setResourcesLoading(true);
|
||
try {
|
||
const resourceItems = await listAllVipResources();
|
||
if (sequence !== resourceLoadSequence.current) {
|
||
return;
|
||
}
|
||
setResources(resourceItems.filter((resource) => resource.resourceType !== "vip_trial_card"));
|
||
resourcesApp.current = appCode;
|
||
} catch (err) {
|
||
if (sequence === resourceLoadSequence.current) {
|
||
// 资源目录只影响装扮权益绑定;读取失败不关闭等级编辑,运营可退出后重进并重试。
|
||
setResources([]);
|
||
showToast(err.message || "加载权益资源失败", "error");
|
||
}
|
||
} finally {
|
||
if (sequence === resourceLoadSequence.current) {
|
||
setResourcesLoading(false);
|
||
}
|
||
}
|
||
}, [appCode, showToast]);
|
||
|
||
useEffect(() => {
|
||
// X-App-Code 是隔离边界;切换 App 时必须丢弃上一 App 的编辑态并重新读取完整配置,避免跨 App 误保存。
|
||
const fallback = defaultProgram(appCode);
|
||
setProgram(fallback);
|
||
setProgramForm(programToForm(fallback));
|
||
setConfig({ levels: defaultLevels(fallback) });
|
||
setForm({ levels: defaultLevels(fallback).map((level) => levelToForm(level, fallback)) });
|
||
setResources([]);
|
||
setResourcesLoading(false);
|
||
resourcesApp.current = "";
|
||
resourceLoadSequence.current += 1;
|
||
setEditorMode("");
|
||
void reload();
|
||
}, [appCode, reload]);
|
||
|
||
const openProgramEditor = () => {
|
||
setProgramForm(programToForm(program));
|
||
setEditorMode("program");
|
||
};
|
||
|
||
const openLevelsEditor = () => {
|
||
setForm({
|
||
levels: completeVipLevels(config.levels || [], program).map((level) => levelToForm(level, program)),
|
||
});
|
||
setEditorMode("levels");
|
||
// 总览不依赖几百页资源目录;仅进入权益编辑时异步加载,避免阻塞 VIP 表格首屏。
|
||
void loadResources();
|
||
};
|
||
|
||
const closeEditor = () => {
|
||
if (saving) {
|
||
return;
|
||
}
|
||
setProgramForm(programToForm(program));
|
||
setForm({
|
||
levels: completeVipLevels(config.levels || [], program).map((level) => levelToForm(level, program)),
|
||
});
|
||
setEditorMode("");
|
||
};
|
||
|
||
const updateProgramField = (patch) => {
|
||
setProgramForm((current) => {
|
||
const next = { ...current, ...patch };
|
||
if (patch.programType && patch.programType !== current.programType) {
|
||
// 体系决定产品已确认的等级数量;不允许切换体系后遗留另一体系的 9/10 级配置。
|
||
next.levelCount = String(vipProgramDefaults[patch.programType].levelCount);
|
||
}
|
||
return next;
|
||
});
|
||
};
|
||
|
||
const updateLevel = (levelNumber, patch) => {
|
||
setForm((current) => ({
|
||
...current,
|
||
levels: (current.levels || []).map((item) => (item.level === levelNumber ? { ...item, ...patch } : item)),
|
||
}));
|
||
};
|
||
|
||
const updateBenefit = (levelNumber, benefitCode, patch) => {
|
||
setForm((current) => ({
|
||
...current,
|
||
levels: (current.levels || []).map((level) =>
|
||
level.level !== levelNumber
|
||
? level
|
||
: {
|
||
...level,
|
||
benefits: (level.benefits || []).map((benefit) =>
|
||
benefit.benefitCode === benefitCode
|
||
? {
|
||
...benefit,
|
||
...patch,
|
||
// 目录占位项只有在运营实际修改后才转成显式等级配置,避免打开并保存就补齐整张表。
|
||
catalogPlaceholder: false,
|
||
}
|
||
: benefit,
|
||
),
|
||
},
|
||
),
|
||
}));
|
||
};
|
||
|
||
const updateAllBenefits = (levelNumber, status) => {
|
||
setForm((current) => ({
|
||
...current,
|
||
levels: (current.levels || []).map((level) =>
|
||
level.level !== levelNumber
|
||
? level
|
||
: {
|
||
...level,
|
||
// “全选”只改变当前等级已有权益行,不按目录补齐缺项,保持 target_only 的最终快照语义。
|
||
benefits: (level.benefits || []).map((benefit) => ({
|
||
...benefit,
|
||
status,
|
||
// 全选开启属于明确配置;取消全选时仍不把从未配置过的目录占位项写入数据库。
|
||
catalogPlaceholder: status === "active" ? false : benefit.catalogPlaceholder,
|
||
})),
|
||
},
|
||
),
|
||
}));
|
||
};
|
||
|
||
const submitProgram = async (event) => {
|
||
event.preventDefault();
|
||
if (!abilities.canUpdate) {
|
||
return;
|
||
}
|
||
let payload;
|
||
try {
|
||
payload = programPayloadFromForm(programForm);
|
||
} catch (err) {
|
||
showToast(err.message || "VIP 体系参数不正确", "error");
|
||
return;
|
||
}
|
||
setSaving(true);
|
||
try {
|
||
const saved = normalizeProgram(await updateVipProgram(payload), appCode);
|
||
setProgram(saved);
|
||
setProgramForm(programToForm(saved));
|
||
setEditorMode("");
|
||
showToast("VIP 体系已保存", "success");
|
||
await reload();
|
||
} catch (err) {
|
||
showToast(err.message || "保存 VIP 体系失败", "error");
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
};
|
||
|
||
const submitLevels = async (event) => {
|
||
event.preventDefault();
|
||
if (!abilities.canUpdate) {
|
||
return;
|
||
}
|
||
let payload;
|
||
try {
|
||
payload = vipConfigPayloadFromForm(form, program);
|
||
} catch (err) {
|
||
showToast(err.message || "VIP 等级参数不正确", "error");
|
||
return;
|
||
}
|
||
setSaving(true);
|
||
try {
|
||
const saved = await updateVipConfig(payload);
|
||
const levels = completeVipLevels(saved.levels || [], program);
|
||
setConfig({ ...saved, levels });
|
||
setForm({ levels: levels.map((level) => levelToForm(level, program)) });
|
||
setEditorMode("");
|
||
showToast("VIP 等级与权益已保存", "success");
|
||
} catch (err) {
|
||
showToast(err.message || "保存 VIP 等级失败", "error");
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
};
|
||
|
||
return {
|
||
abilities,
|
||
activeCount,
|
||
appCode,
|
||
closeEditor,
|
||
config,
|
||
editorMode,
|
||
error,
|
||
form,
|
||
loading,
|
||
openLevelsEditor,
|
||
openProgramEditor,
|
||
program,
|
||
programForm,
|
||
reload,
|
||
resourceGroups,
|
||
resources,
|
||
resourcesLoading,
|
||
saving,
|
||
submitLevels,
|
||
submitProgram,
|
||
updateAllBenefits,
|
||
updateBenefit,
|
||
updateLevel,
|
||
updateProgramField,
|
||
};
|
||
}
|
||
|
||
async function listAllVipResources() {
|
||
const items = [];
|
||
const pageSize = 100;
|
||
for (let page = 1; page <= 200; page += 1) {
|
||
const result = await listResources({ page, page_size: pageSize, status: "active" });
|
||
items.push(...(result.items || []));
|
||
if (items.length >= Number(result.total || 0) || (result.items || []).length < pageSize) {
|
||
return items;
|
||
}
|
||
}
|
||
throw new Error("权益资源数量超过后台可加载上限");
|
||
}
|
||
|
||
function defaultProgram(appCode) {
|
||
const programType = String(appCode || "").toLowerCase() === "fami" ? "tiered_privilege_v1" : "legacy_timed";
|
||
const defaults = vipProgramDefaults[programType];
|
||
return {
|
||
appCode: String(appCode || "").toLowerCase(),
|
||
programType,
|
||
levelCount: defaults.levelCount,
|
||
status: "active",
|
||
configVersion: 0,
|
||
sameLevelExpiryPolicy: defaults.sameLevelExpiryPolicy,
|
||
upgradeExpiryPolicy: defaults.upgradeExpiryPolicy,
|
||
downgradePurchasePolicy: defaults.downgradePurchasePolicy,
|
||
benefitInheritancePolicy: defaults.benefitInheritancePolicy,
|
||
grantMode: defaults.grantMode,
|
||
trialCardEnabled: defaults.trialCardEnabled,
|
||
};
|
||
}
|
||
|
||
function normalizeProgram(program, appCode) {
|
||
const fallback = defaultProgram(appCode);
|
||
const programType = program?.programType === "tiered_privilege_v1" ? "tiered_privilege_v1" : "legacy_timed";
|
||
const defaults = vipProgramDefaults[programType];
|
||
const levelCount = Number(program?.levelCount || defaults.levelCount);
|
||
return {
|
||
...fallback,
|
||
...program,
|
||
appCode: program?.appCode || fallback.appCode,
|
||
programType,
|
||
levelCount: Number.isInteger(levelCount) && levelCount > 0 ? levelCount : defaults.levelCount,
|
||
status: program?.status === "disabled" ? "disabled" : "active",
|
||
sameLevelExpiryPolicy: program?.sameLevelExpiryPolicy || defaults.sameLevelExpiryPolicy,
|
||
upgradeExpiryPolicy: program?.upgradeExpiryPolicy || defaults.upgradeExpiryPolicy,
|
||
downgradePurchasePolicy: program?.downgradePurchasePolicy || defaults.downgradePurchasePolicy,
|
||
benefitInheritancePolicy: program?.benefitInheritancePolicy || defaults.benefitInheritancePolicy,
|
||
grantMode: program?.grantMode || defaults.grantMode,
|
||
trialCardEnabled:
|
||
typeof program?.trialCardEnabled === "boolean" ? program.trialCardEnabled : defaults.trialCardEnabled,
|
||
};
|
||
}
|
||
|
||
function programToForm(program) {
|
||
return {
|
||
enabled: program.status !== "disabled",
|
||
levelCount: String(program.levelCount || vipProgramDefaults[program.programType].levelCount),
|
||
programType: program.programType,
|
||
};
|
||
}
|
||
|
||
function programPayloadFromForm(form) {
|
||
const parsed = parseForm(vipProgramFormSchema, form);
|
||
const defaults = vipProgramDefaults[parsed.programType];
|
||
const levelCount = Number(parsed.levelCount || 0);
|
||
if (!Number.isInteger(levelCount) || levelCount <= 0 || levelCount > 20) {
|
||
throw new Error("VIP 等级数量必须为 1 至 20");
|
||
}
|
||
return {
|
||
programType: parsed.programType,
|
||
levelCount,
|
||
sameLevelExpiryPolicy: defaults.sameLevelExpiryPolicy,
|
||
upgradeExpiryPolicy: defaults.upgradeExpiryPolicy,
|
||
downgradePurchasePolicy: defaults.downgradePurchasePolicy,
|
||
benefitInheritancePolicy: defaults.benefitInheritancePolicy,
|
||
grantMode: defaults.grantMode,
|
||
trialCardEnabled: defaults.trialCardEnabled,
|
||
status: parsed.enabled ? "active" : "disabled",
|
||
};
|
||
}
|
||
|
||
function defaultLevels(program) {
|
||
return Array.from({ length: program.levelCount }, (_, index) => defaultLevel(index + 1));
|
||
}
|
||
|
||
function defaultLevel(level) {
|
||
return {
|
||
level,
|
||
name: `VIP${level}`,
|
||
status: "disabled",
|
||
priceCoin: 0,
|
||
durationMs: 30 * dayMillis,
|
||
rewardResourceGroupId: 0,
|
||
requiredRechargeCoinAmount: 0,
|
||
rechargeGateRequired: false,
|
||
sortOrder: level * 10,
|
||
// target_only 下新等级从空权益快照开始;后台不能依据历史 unlockLevel 猜测并开启权益。
|
||
benefits: [],
|
||
};
|
||
}
|
||
|
||
export function completeVipLevels(levels, program) {
|
||
const byLevel = new Map((levels || []).map((level) => [Number(level.level), level]));
|
||
return defaultLevels(program).map((fallback) => {
|
||
const existing = byLevel.get(fallback.level);
|
||
if (!existing) {
|
||
return fallback;
|
||
}
|
||
const merged = { ...fallback, ...existing };
|
||
merged.benefits =
|
||
program.programType === "tiered_privilege_v1"
|
||
? explicitP1Benefits(existing.benefits || [])
|
||
: existing.benefits || [];
|
||
return merged;
|
||
});
|
||
}
|
||
|
||
function explicitP1Benefits(benefits) {
|
||
// 每级权益是最终快照:只保留该等级真实返回的行,缺项既不按 unlockLevel 补齐,也不从体系目录继承。
|
||
return (benefits || []).map((benefit) =>
|
||
benefit.benefitCode === "daily_coin_rebate" ? { ...benefit, trialEnabled: false } : benefit,
|
||
);
|
||
}
|
||
|
||
export function levelToForm(level, program) {
|
||
return {
|
||
level: Number(level.level || 0),
|
||
name: level.name || `VIP${level.level}`,
|
||
status: level.status || "disabled",
|
||
priceCoin: level.priceCoin ? String(level.priceCoin) : "",
|
||
durationDays: level.durationMs ? String(Math.max(1, Math.round(Number(level.durationMs) / dayMillis))) : "30",
|
||
rewardResourceGroupId: level.rewardResourceGroupId ? String(level.rewardResourceGroupId) : "",
|
||
sortOrder: String(level.sortOrder || Number(level.level || 0) * 10),
|
||
benefits: benefitFormsForLevel(level, program),
|
||
};
|
||
}
|
||
|
||
function benefitFormsForLevel(level, program) {
|
||
const explicitBenefits = level.benefits || [];
|
||
if (program?.programType !== "tiered_privilege_v1") {
|
||
return explicitBenefits.map((benefit, index) => benefitToForm(benefit, level.level, index, false));
|
||
}
|
||
const catalog = completeP1BenefitCatalog(program?.benefits || []);
|
||
const explicitByCode = new Map(explicitBenefits.map((benefit) => [benefit.benefitCode, benefit]));
|
||
const catalogCodes = new Set(catalog.map((benefit) => benefit.benefitCode));
|
||
const rows = catalog.map((template, index) => {
|
||
const explicit = explicitByCode.get(template.benefitCode);
|
||
return benefitToForm(explicit || template, level.level, index, !explicit);
|
||
});
|
||
// 服务端可能先上线新权益而当前前端目录尚未更新;显式行必须保留,不能因目录版本差异在保存时被删除。
|
||
explicitBenefits.forEach((benefit, index) => {
|
||
if (!catalogCodes.has(benefit.benefitCode)) {
|
||
rows.push(benefitToForm(benefit, level.level, catalog.length + index, false));
|
||
}
|
||
});
|
||
return rows;
|
||
}
|
||
|
||
function completeP1BenefitCatalog(remoteCatalog) {
|
||
const remoteByCode = new Map(remoteCatalog.map((benefit) => [benefit.benefitCode, benefit]));
|
||
const builtInCodes = new Set(p1BenefitCatalog.map((benefit) => benefit.benefitCode));
|
||
const catalog = p1BenefitCatalog.map((template) => ({
|
||
...template,
|
||
// 服务端最高等级行含最新名称、类型与执行域;内置目录只负责保证被删除的已知权益仍可重新配置。
|
||
...(remoteByCode.get(template.benefitCode) || {}),
|
||
}));
|
||
remoteCatalog.forEach((benefit) => {
|
||
if (!builtInCodes.has(benefit.benefitCode)) {
|
||
catalog.push(benefit);
|
||
}
|
||
});
|
||
return catalog;
|
||
}
|
||
|
||
function benefitToForm(benefit, level, index, catalogPlaceholder) {
|
||
const benefitCode = benefit.benefitCode || "";
|
||
return {
|
||
benefitCode,
|
||
name: benefit.name || benefitCode,
|
||
benefitType: benefit.benefitType === "decoration" ? "decoration" : "function",
|
||
unlockLevel: Number(benefit.unlockLevel || level || 1),
|
||
status: catalogPlaceholder ? "disabled" : benefit.status === "active" ? "active" : "disabled",
|
||
trialEnabled: benefitCode === "daily_coin_rebate" ? false : Boolean(benefit.trialEnabled),
|
||
resourceId: catalogPlaceholder ? "" : benefit.resourceId ? String(benefit.resourceId) : "",
|
||
resourceType: catalogPlaceholder ? "" : benefit.resourceType || "",
|
||
executionScope: ["wallet", "room", "user", "notice"].includes(benefit.executionScope)
|
||
? benefit.executionScope
|
||
: "wallet",
|
||
autoEquip: Boolean(benefit.autoEquip),
|
||
sortOrder: String(benefit.sortOrder || (index + 1) * 10),
|
||
// 高等级目录只提供字段模板;等级未配置的参数必须从空对象开始,不能复制高等级返现金额、文案或资源。
|
||
metadataJson: catalogPlaceholder ? "{}" : benefit.metadataJson || "{}",
|
||
catalogPlaceholder,
|
||
};
|
||
}
|
||
|
||
export function vipConfigPayloadFromForm(form, program) {
|
||
const parsed = parseForm(vipConfigFormSchema, form);
|
||
if (parsed.levels.length !== program.levelCount) {
|
||
throw new Error(`当前体系必须完整配置 VIP1 至 VIP${program.levelCount}`);
|
||
}
|
||
const levels = parsed.levels.map((level) => levelPayload(level, program));
|
||
return { levels };
|
||
}
|
||
|
||
function levelPayload(level, program) {
|
||
const priceCoin = Number(level.priceCoin || 0);
|
||
const durationDays = Number(level.durationDays || 0);
|
||
const rewardResourceGroupId = Number(level.rewardResourceGroupId || 0);
|
||
const sortOrder = Number(level.sortOrder || level.level * 10);
|
||
const status = level.status === "active" ? "active" : "disabled";
|
||
if (!Number.isInteger(priceCoin) || priceCoin < 0) {
|
||
throw new Error(`VIP${level.level} 购买金币数不正确`);
|
||
}
|
||
if (!Number.isInteger(durationDays) || durationDays <= 0) {
|
||
throw new Error(`VIP${level.level} 有效天数必须大于 0`);
|
||
}
|
||
if (!Number.isInteger(sortOrder) || sortOrder < 0) {
|
||
throw new Error(`VIP${level.level} 排序不能小于 0`);
|
||
}
|
||
if (status === "active" && priceCoin <= 0) {
|
||
throw new Error(`VIP${level.level} 启用时购买金币数必须大于 0`);
|
||
}
|
||
if (!Number.isInteger(rewardResourceGroupId) || rewardResourceGroupId < 0) {
|
||
throw new Error(`VIP${level.level} 奖励资源组不正确`);
|
||
}
|
||
// Fami 的等级礼包是可选表现资源;经典 Lalu VIP 仍沿用启用等级必须配置礼包的既有约束。
|
||
if (status === "active" && program.programType !== "tiered_privilege_v1" && rewardResourceGroupId <= 0) {
|
||
throw new Error(`VIP${level.level} 启用时必须选择奖励资源组`);
|
||
}
|
||
const benefits = (level.benefits || [])
|
||
// 未触碰的目录占位项只服务复选 UI;显式禁用行和运营修改后的占位项仍需提交,保持最终快照可审计。
|
||
.filter((benefit) => !benefit.catalogPlaceholder || benefit.status === "active")
|
||
.map((benefit) => benefitPayload(benefit, level.level));
|
||
if (program.programType === "tiered_privilege_v1") {
|
||
const benefitCodes = new Set(benefits.map((benefit) => benefit.benefitCode));
|
||
if (benefitCodes.size !== benefits.length) {
|
||
throw new Error(`VIP${level.level} 权益编码不能重复`);
|
||
}
|
||
}
|
||
return {
|
||
level: Number(level.level),
|
||
name: String(level.name || "").trim(),
|
||
status,
|
||
priceCoin,
|
||
durationMs: durationDays * dayMillis,
|
||
rewardResourceGroupId,
|
||
requiredRechargeCoinAmount: 0,
|
||
sortOrder,
|
||
benefits,
|
||
};
|
||
}
|
||
|
||
function benefitPayload(benefit, level) {
|
||
const rawMetadataJson = String(benefit.metadataJson || "{}").trim() || "{}";
|
||
const sortOrder = Number(benefit.sortOrder || 0);
|
||
const resourceId = Number(benefit.resourceId || 0);
|
||
if (!Number.isInteger(sortOrder) || sortOrder < 0) {
|
||
throw new Error(`VIP${level} 的 ${benefit.name || benefit.benefitCode} 排序不能小于 0`);
|
||
}
|
||
try {
|
||
const parsedConfig = JSON.parse(rawMetadataJson);
|
||
if (!parsedConfig || Array.isArray(parsedConfig) || typeof parsedConfig !== "object") {
|
||
throw new Error("config must be an object");
|
||
}
|
||
} catch {
|
||
throw new Error(`VIP${level} 的 ${benefit.name || benefit.benefitCode} 元数据必须是 JSON 对象`);
|
||
}
|
||
if (!Number.isInteger(resourceId) || resourceId < 0) {
|
||
throw new Error(`VIP${level} 的 ${benefit.name || benefit.benefitCode} 资源不正确`);
|
||
}
|
||
const status = benefit.status === "active" ? "active" : "disabled";
|
||
const metadataJson = normalizeVipBenefitMetadata({
|
||
benefitCode: benefit.benefitCode,
|
||
label: `VIP${level} 的 ${benefit.name || benefit.benefitCode}`,
|
||
metadataJson: rawMetadataJson,
|
||
status,
|
||
});
|
||
return {
|
||
benefitCode: String(benefit.benefitCode || "").trim(),
|
||
name: String(benefit.name || "").trim(),
|
||
benefitType: benefit.benefitType === "decoration" ? "decoration" : "function",
|
||
unlockLevel: Number(benefit.unlockLevel || level),
|
||
status,
|
||
trialEnabled: benefit.benefitCode === "daily_coin_rebate" ? false : Boolean(benefit.trialEnabled),
|
||
resourceId,
|
||
resourceType: resourceId > 0 ? String(benefit.resourceType || "").trim() : "",
|
||
executionScope: benefit.executionScope,
|
||
autoEquip: Boolean(benefit.autoEquip),
|
||
sortOrder,
|
||
metadataJson,
|
||
};
|
||
}
|