fix: prevent duplicate lucky gift tier ids

This commit is contained in:
zhx 2026-06-12 23:22:45 +08:00
parent 3983e355fb
commit 282c9f1734
4 changed files with 71 additions and 3 deletions

View File

@ -130,12 +130,16 @@ export function addTierToStage(stages, stageKey) {
if (stage.stage !== stageKey) {
return stage;
}
const index = stage.tiers.length + 1;
const nextMultiplier = nextSuggestedTierMultiplier(stage.tiers);
return {
...stage,
tiers: [
...stage.tiers,
tier(`${stage.stage}_${index}x`, "", "", { broadcastLevel: "none", rewardSource: "base_rtp" }),
tier(tierIDFromMultiplier(stage.stage, nextMultiplier), nextMultiplier, "", {
broadcastLevel: "none",
highWaterOnly: nextMultiplier >= 5,
rewardSource: "base_rtp",
}),
],
};
});
@ -231,6 +235,28 @@ function defaultStages() {
return stageOptions.map(([stage]) => ({ stage, tiers: defaultStageTiers[stage].map((item) => ({ ...item })) }));
}
function nextSuggestedTierMultiplier(tiers) {
const usedMultipliers = (tiers || [])
.map((item) => decimal(item.multiplier))
.filter((value) => Number.isFinite(value) && value > 0);
if (usedMultipliers.length === 0) {
return 1;
}
const maxMultiplier = Math.max(...usedMultipliers);
if (maxMultiplier >= 5) {
return maxMultiplier * 2;
}
if (maxMultiplier >= 2) {
return 5;
}
return maxMultiplier + 1;
}
function tierIDFromMultiplier(stage, multiplier) {
const multiplierText = String(multiplier).replace(".", "_");
return `${stage}_${multiplierText}x`;
}
function normalizeStages(stages) {
// server 返回缺阶段时仍用默认三阶段补齐表单,保证运营看到的是完整发布形态;
// 保存时 schema 会再次校验概率合计和 0x 档,不让半成品配置进入发布请求。

View File

@ -56,7 +56,13 @@ describe("lucky gift config payload", () => {
const advanced = nextStages.find((stage) => stage.stage === "advanced");
expect(advanced.tiers).toHaveLength(5);
expect(advanced.tiers[4]).toMatchObject({ broadcastLevel: "none", rewardSource: "base_rtp" });
expect(advanced.tiers[4]).toMatchObject({
broadcastLevel: "none",
highWaterOnly: true,
multiplier: 10,
rewardSource: "base_rtp",
tierId: "advanced_10x",
});
});
test("corrects a stage probability to requested RTP without zeroing enabled tiers", () => {

View File

@ -18,4 +18,28 @@ describe("lucky gift config schema", () => {
expect(result.error.issues.map((issue) => issue.message)).toContain("启用奖档概率合计必须等于 100%");
}
});
test("rejects duplicated tier id inside the same stage before submit", () => {
const form = emptyLuckyGiftForm();
const advanced = form.stages.find((stage: { stage: string }) => stage.stage === "advanced");
if (!advanced) {
throw new Error("advanced stage missing");
}
advanced.tiers = [
...advanced.tiers,
{
...advanced.tiers[advanced.tiers.length - 1],
multiplier: 10,
probabilityPercent: 0,
tierId: "advanced_5x",
},
];
const result = luckyGiftConfigFormSchema.safeParse(form);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues.map((issue) => issue.message)).toContain("同一阶段内奖档 ID 不能重复");
}
});
});

View File

@ -93,6 +93,18 @@ export const luckyGiftConfigFormSchema = z
path: ["stages", stageIndex, "tiers"],
});
}
const tierIDSet = new Set<string>();
stage.tiers.forEach((tier, tierIndex) => {
const tierID = String(tier.tierId || "").trim();
if (tierIDSet.has(tierID)) {
context.addIssue({
code: "custom",
message: "同一阶段内奖档 ID 不能重复",
path: ["stages", stageIndex, "tiers", tierIndex, "tierId"],
});
}
tierIDSet.add(tierID);
});
const expectedRTPPPM = stage.tiers.reduce((sum, tier) => {
if (!tier.enabled || tier.rewardSource !== "base_rtp") {
return sum;