fix: 每日首次登录时战令、月卡超出限制后当日不再弹出

This commit is contained in:
guanchao
2026-11-04 13:49:34 +08:00
parent b6aaf71ac3
commit f12e167be2
3 changed files with 34 additions and 21 deletions
+8 -5
View File
@@ -58,9 +58,6 @@ export default class HomePopupPolicy {
if (this.record.day !== day) {
this.record.day = day; this.record.shown = {}; this.record.blocked = {};
}
// Older clients persisted purchase-cap skips for the whole day. Caps are now per batch.
delete this.record.blocked.passCheckIntro;
delete this.record.blocked.monthlyCard;
this.forced = level >= threshold && !this.record.tutorial.done;
this.save(this.record);
}
@@ -85,7 +82,7 @@ export default class HomePopupPolicy {
request(key: string, type: PopupFrequency = HOME_POPUP_RULES[key]?.type): boolean {
this.refreshDay();
if (type !== 1 && type !== 2) return false;
if (type === 1 ? this.record.shown[key] : this.session.shown[key]) {
if (type === 1 ? (this.record.shown[key] || this.record.blocked[key]) : this.session.shown[key]) {
this.clearPending(key); return false;
}
this.session.pending[key] = true; this.savePending(); return true;
@@ -105,7 +102,13 @@ export default class HomePopupPolicy {
if (!this.eligible(key, type)) return false;
this.attempts.add(key);
const limit = HOME_POPUP_RULES[key]?.purchaseLimit;
if (limit !== undefined && this.otherPopupCount >= limit) return false;
if (limit !== undefined && this.otherPopupCount >= limit) {
// Suppress this purchase for today without recording an impression.
this.record.blocked[key] = true;
this.save(this.record);
this.clearPending(key);
return false;
}
return true;
}
shown(key: string): void {
+1 -1
View File
@@ -53,7 +53,7 @@
| 3 | 允许 | 跳过 |
| ≥4 | 跳过 | 跳过 |
超限只跳过本批,不写当天禁止展示记录;旧版持久化的两个购买弹窗 blocked 标记在创建规则时清理,已成功展示的当天记录保留。已入队的类型 1/2 候选在实际显示前保留待展示状态;加载失败或离开首页不消耗次数,下次返回首页或登录时可重试,同一次首页停留不循环重试。新批次计数从零开始,手动入口不受额度限制。
购买弹窗因限额跳过时,清除其待展示请求,并将该购买意图记为当天禁止自动展示(`blocked`)。当天返回首页、重新登录或再次触发请求均不补弹;北京时间次日零点恢复资格。跳过不计实际展示或曝光。未超限但加载失败或展示前离开首页的候选仍保留待展示状态,下次返回首页或登录可重试。新批次计数从零开始,手动入口不受额度限制。
战令结算准备失败释放当前队列,但不把业务结算标记为已完成。小鱼干普通弹窗排队期间转为到期结算时,释放普通意图,本批已关闭收集则等待下次返回首页;手动打开仍可直接处理结算。月卡奖励发放与通知分离,已发放但没展示的通知持久化为 pending,并登记为类型 1 补弹请求,下次返回首页或登录时展示不重复发奖。
+25 -15
View File
@@ -52,8 +52,8 @@ for (const count of [0, 1, 2, 3, 4, 5]) test(`purchase limits use ${count} other
if (count < 3) p.shown('monthlyCard');
assert.equal(p.otherPopupCount, count);
assert.equal(p.count, count + Number(count < 4) + Number(count < 3));
assert.equal(policy(p.record).eligible('passCheckIntro'), count >= 4);
assert.equal(policy(p.record).eligible('monthlyCard'), count >= 3);
assert.equal(policy(p.record).eligible('passCheckIntro'), false);
assert.equal(policy(p.record).eligible('monthlyCard'), false);
assert.equal(policy(p.record, false).eligible('passCheckIntro'), false);
assert.equal(policy(p.record, false).eligible('monthlyCard'), false);
assert.equal(policy(p.record, true, day + 86400000).eligible('monthlyCard'), true);
@@ -76,17 +76,12 @@ for (const count of [0, 2, 3, 4]) test(`settlements do not consume purchase allo
assert.equal(p.begin('monthlyCard'), count < 3);
});
test('legacy daily cap blocks are cleared but successful daily displays are preserved', () => {
const raw = policy().record;
raw.blocked = { passCheckIntro: true, monthlyCard: true };
raw.shown.passCheckIntro = true;
const p = policy(raw);
assert.equal(p.eligible('passCheckIntro'), false);
assert.equal(p.eligible('monthlyCard'), true);
assert.deepEqual(p.record.blocked, {});
p.begin('monthlyCard'); // Loading fails; next login can retry.
assert.equal(policy(p.record).begin('monthlyCard'), true);
assert.equal(policy(p.record).otherPopupCount, 0);
test('a purchase that fails to load below its cap can still retry on return home', () => {
const h = harness(), q = h.create();
for (const key of ['monthlyCard', 'passCheckIntro']) q.enqueue(key, 0, done => done());
h.ready(q); q.update(); q.update(); q.onDestroy();
const next = h.create(false);
for (const key of ['monthlyCard', 'passCheckIntro']) assert.equal(next.canCollect(key), true);
});
test('Beijing day changes at 16:00 UTC and accounts do not share display records', () => {
@@ -331,8 +326,23 @@ for (const count of [0, 1, 2, 3, 4]) test(`purchase checks run last after ${coun
for (let i = 0; i < 10; i++) q.update();
assert.deepEqual(order, others.concat(count < 4 ? ['passCheckIntro'] : [], count < 3 ? ['monthlyCard'] : []));
assert.equal(q.policy.otherPopupCount, count);
assert.equal(q.policy.record.blocked.monthlyCard, undefined);
assert.equal(q.policy.record.blocked.passCheckIntro, undefined);
for (const [key, limit] of [['monthlyCard', 3], ['passCheckIntro', 4]]) {
assert.equal(!!q.policy.record.blocked[key], count >= limit);
assert.equal(q.wasShownToday(key), count < limit, 'cap skips are not impressions');
assert.equal(q.policy.hasPending(key), false);
}
q.onDestroy();
for (const login of [false, true]) {
const next = h.create(login);
for (const key of ['monthlyCard', 'passCheckIntro']) {
assert.equal(next.requestPresentation(key), false);
assert.equal(next.canCollect(key), false);
}
next.onDestroy();
}
h.setNow(Date.UTC(2026, 9, 21, 16)); // Midnight in Beijing.
const tomorrow = h.create();
for (const key of ['monthlyCard', 'passCheckIntro']) assert.equal(tomorrow.canCollect(key), true);
});
test('a visible previous-season offer does not suppress the current-season purchase in the same batch', () => {