From f12e167be2e64595eafc427e4bf8898ae17d5f44 Mon Sep 17 00:00:00 2001 From: guanchao <18612113337@163.com> Date: Wed, 4 Nov 2026 13:49:34 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=AF=8F=E6=97=A5=E9=A6=96=E6=AC=A1?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E6=97=B6=E6=88=98=E4=BB=A4=E3=80=81=E6=9C=88?= =?UTF-8?q?=E5=8D=A1=E8=B6=85=E5=87=BA=E9=99=90=E5=88=B6=E5=90=8E=E5=BD=93?= =?UTF-8?q?=E6=97=A5=E4=B8=8D=E5=86=8D=E5=BC=B9=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/home_popup_queue/HomePopupPolicy.ts | 13 ++++--- docs/home-popup-queue.md | 2 +- tools/test-home-popup-policy.cjs | 40 ++++++++++++++-------- 3 files changed, 34 insertions(+), 21 deletions(-) diff --git a/assets/home_popup_queue/HomePopupPolicy.ts b/assets/home_popup_queue/HomePopupPolicy.ts index 9347a1e5..a558ec57 100644 --- a/assets/home_popup_queue/HomePopupPolicy.ts +++ b/assets/home_popup_queue/HomePopupPolicy.ts @@ -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 { diff --git a/docs/home-popup-queue.md b/docs/home-popup-queue.md index 977d0db4..4fad1667 100644 --- a/docs/home-popup-queue.md +++ b/docs/home-popup-queue.md @@ -53,7 +53,7 @@ | 3 | 允许 | 跳过 | | ≥4 | 跳过 | 跳过 | -超限只跳过本批,不写当天禁止展示记录;旧版持久化的两个购买弹窗 blocked 标记在创建规则时清理,已成功展示的当天记录保留。已入队的类型 1/2 候选在实际显示前保留待展示状态;加载失败或离开首页不消耗次数,下次返回首页或登录时可重试,同一次首页停留不循环重试。新批次计数从零开始,手动入口不受额度限制。 +购买弹窗因限额跳过时,清除其待展示请求,并将该购买意图记为当天禁止自动展示(`blocked`)。当天返回首页、重新登录或再次触发请求均不补弹;北京时间次日零点恢复资格。跳过不计实际展示或曝光。未超限但加载失败或展示前离开首页的候选仍保留待展示状态,下次返回首页或登录可重试。新批次计数从零开始,手动入口不受额度限制。 战令结算准备失败释放当前队列,但不把业务结算标记为已完成。小鱼干普通弹窗排队期间转为到期结算时,释放普通意图,本批已关闭收集则等待下次返回首页;手动打开仍可直接处理结算。月卡奖励发放与通知分离,已发放但没展示的通知持久化为 pending,并登记为类型 1 补弹请求,下次返回首页或登录时展示不重复发奖。 diff --git a/tools/test-home-popup-policy.cjs b/tools/test-home-popup-policy.cjs index 0448a1cd..e9f965bb 100644 --- a/tools/test-home-popup-policy.cjs +++ b/tools/test-home-popup-policy.cjs @@ -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', () => {