Files
cursor/plans/lucky_reward_双弹窗分析_58c583c3.plan.md
ray zhou 2dd9f17da9 ok
2026-06-29 14:51:55 +08:00

15 KiB
Raw Blame History

name, overview, todos, isProject
name overview todos isProject
Lucky Reward 双弹窗分析 被邀请人 Support Invite 应从 HomeEvent 迁到 lucky_reward_invite_bind 成功时 WS 即时推送只弹一次HomeEvent 仅保留邀请人 Claim Nowpending_spin_count可重复补弹。这样同一 uid 在 HomeEvent 不再同时命中两种 Lucky Rewards 弹窗。
id content status
diagnose-no-helper 按日志/SQL 排查 invite_bind 全链路,定位 helper 未落库的具体 status/reason pending
id content status
move-support-invite-to-bind invite_bind success 后ensureInviteeAutoOpenBox + buildSupportInvitePopup + WsService::notifyClientPOPHomeEvent 移除被邀请人逻辑 pending
id content status
update-tests-docs-yapi 更新集成测、05 方案文档、YApi pending
id content status
confirm-product-rule 确认产品规则:双角色用户允许两种弹窗,还是同一 flush 只保留一种(及优先级) cancelled
id content status
implement-mutex-if-needed 若需互斥:在 LuckyRewardInviteLogic 抽 resolveHomePopupsHomeEvent 只 merge 一种;补互邀集成测 cancelled
id content status
support-invite-once (可选)若 Support Invite 应只弹一次:增加 consume/已读状态,与互斥分开评估 cancelled
false

Lucky Rewards 同一人两种弹窗 — 根因与可选改法

现象对应代码

HomeEvent::appendLuckyRewardHomePopups同一个 $uid 顺序做两件事,互不排斥

protected function appendLuckyRewardHomePopups(array &$popList, int $uid, UserInfoEntity $userInfoEntity): void
{
    // ...
    $inviteLogic->ensureInviteeAutoOpenBox($uid, $source);
    $supportInvitePop = $inviteLogic->buildSupportInvitePopup($uid, $source);
    if ($supportInvitePop !== false) {
        $luckyRewardPopups[] = $supportInvitePop;
    }

    $inviterClaimNowPop = $inviteLogic->buildInviterClaimNowPopup($uid, $source);
    if ($inviterClaimNowPop !== null) {
        $luckyRewardPopups[] = $inviterClaimNowPop->toHomePopListItem();
    }
    $popList = array_merge($luckyRewardPopups, $popList);
}

两种弹窗判定维度不同:

弹窗 type 判定方法 核心条件(同一 uid
Support Invite lucky_reward_support_invite buildSupportInvitePopup lucky_reward_helper 存在 invitee_uid = uid已开箱
Claim Now lucky_reward_invite_success buildInviterClaimNowPopup pending_spin_count > 0 且存在 未领的 GRANT_TYPE_INVITER grant

需求文档 05_slot_console_邀请助力与弹窗方案.md 表格里写的是「邀请人 / 被邀请人」两种角色,但代码没有写「同一 uid 只能占一种角色」。

flowchart TD
    homeFlush["HomeEvent flush uid=X"]
    openBox["ensureInviteeAutoOpenBox(X)"]
    checkInvitee["buildSupportInvitePopup(X)\nhelper.invitee_uid=X ?"]
    checkInviter["buildInviterClaimNowPopup(X)\npending_spin_count>0 ?"]
    popSupport["lucky_reward_support_invite"]
    popClaim["lucky_reward_invite_success"]
    merge["array_merge 两种都进 popList"]

    homeFlush --> openBox --> checkInvitee
    checkInvitee -->|是| popSupport
    checkInvitee -->|否| checkInviter
    popSupport --> checkInviter
    checkInviter -->|是| popClaim
    popClaim --> merge
    checkInviter -->|否| merge

为什么「一个人」会同时有两种?

正常单链邀请A 邀 B不会出现A 只有 Claim NowB 只有 Support InviteLuckyRewardInviteBindIntegrationTest)。

同一 uid 双角色时会出现,典型场景:

  1. 互邀 / 双链A 邀 BB 也邀 A同一 cycle_id,均新注册有效助力)
    • Ahelper(invitee=A) → Support Invitepending_spin_count>0 → Claim Now
    • B同理
  2. 先被邀、后又成功邀别人:用户 C 邀了 AA 为被邀请人);之后 A 又邀了 DA 为邀请人且未 claim→ 回大厅两种都满足
  3. 邀请人未 claim 的重复提醒 + 被邀请身份长期有效Claim Now 在 pending_spin_count>0 期间每次回大厅都弹Support Invite 在 helper 存在且已开箱后每次回大厅都弹(无 consume/已读标记)→ 双角色用户会持续收到两个 pop

ensurePlayerRow 会在 invite_bind 时为邀请人创建 player 行(不必先开箱),因此「邀请人侧 Claim Now」与「被邀请人侧 Support Invite」在数据上可以并存。

这不是 invite_bind 写错

persistInviteHelperInTransaction 只给邀请人写 grant + pending_spin_count;被邀请人不再写 invitee grant测试也断言 invite_invitee: grant 为 null。双弹窗来自 HomeEvent 合并逻辑,不是一次回调写了两种状态。

若产品期望「同一时刻只弹一种」

需产品定优先级,再在 HomeEvent 加互斥 已按产品意见调整方案:被邀请人弹窗不再走 HomeEvent,见下文 §「被邀请人弹窗迁出 HomeEvent」HomeEvent 只留邀请人 Claim Now从架构上消除「同一人两种 Lucky Rewards 弹窗」。


被邀请人 Support Invite 迁出 HomeEvent产品定案

背景

  • 被邀请人IMEI 进游戏即注册,invite_bind 发生时用户在线Support Invite 只弹一次,不存在「离线补弹」。
  • 邀请人:可能正在玩游戏/离线Claim Now 须 pending_spin_count + 每次回大厅重复弹,继续走 HomeEvent。

当前实现把两者都塞进 appendLuckyRewardHomePopups,导致:

  • 被邀请人每次回大厅重复弹 Support Invite无 consume
  • 双角色用户同一 flush 可能同时出现两种 type

目标架构

sequenceDiagram
    participant Agent as agent_invite_bind
    participant ConsoleBus as console_bus
    participant Logic as LuckyRewardInviteLogic
    participant WS as WsService_notifyClientPOP
    participant Home as HomeEvent

    Agent->>ConsoleBus: lucky_reward_invite_bind
    ConsoleBus->>Logic: handleInviteBind success
    Logic->>Logic: ensureInviteeAutoOpenBox(invitee)
    Logic->>Logic: buildSupportInvitePopup(invitee)
    Logic->>WS: 即时推送 invitee 仅一次

    Note over Home: 邀请人路径不变
    Home->>Logic: buildInviterClaimNowPopup(inviter)
    Home->>WS: home flush popList
弹窗 触发点 下发方式 重复策略
Support Invite lucky_reward_invite_bindstatus=success WsService::notifyClientPOP(inviteeUid, [...]) 一次bind 幂等保证不重复推)
Claim Now HomeEvent flush 合并进 popList 未 claim 前每次回大厅

代码改动要点

1. EventBus::luckyRewardInviteBindEvent

handleInviteBind 返回 success 后新增(建议封装为 LuckyRewardInviteLogic::pushInviteeSupportInvitePopup()

  1. ensureInviteeAutoOpenBox($inviteeUid, $source) — 从 HomeEvent 挪到这里(弹窗需要 my_amount
  2. buildSupportInvitePopup($inviteeUid, $source)
  3. 非 false 则 WsService::notifyClientPOP($inviteeUid, [$popup])

already_processed / 各类 skipped_* 不推送

2. HomeEvent::appendLuckyRewardHomePopups

删除

  • ensureInviteeAutoOpenBox
  • buildSupportInvitePopup 及 merge

保留

  • buildInviterClaimNowPopup → Claim Now

方法可重命名为 appendInviterClaimNowPopup 或保持原名但注释只服务邀请人。

3. 测试

  • LuckyRewardInviteBindIntegrationTesttestEnsureInviteeAutoOpenBoxAndSupportInvitePopup 改为断言 bind 成功路径mock/spy WsService 或抽 pushInviteeSupportInvitePopup 返回值)
  • 新增bind 幂等第二次不推 WS
  • HomeEvent 单测:被邀请人 uid flush 不应再含 lucky_reward_support_invite

4. 文档

边界说明

处理
MQ 消费略晚于注册完成 产品认定被邀请人在线;仍走 WS 即时推。若 hub 未连上,不做 HomeEvent 补弹(与「只弹一次、无离线」一致)
游戏内禁弹 bind 发生在注册链路,通常不在 enter_game;若需硬约束可后续加 hub 侧 scene 判断
openBox 失败 记录 error不推 Support Invite无 my_amount
双角色用户 HomeEvent 仅可能出 Claim NowSupport Invite 仅在作为被邀请人 bind 当时推一次

与「没有 helper」排查的关系

Support Invite 迁出 不解决 helper 未落库;须先保证 handleInviteBindsuccess 写 helper再在 success 分支推 WS。


「没有助力」DB 无 lucky_reward_helper— 排查结论

你已确认:不是弹窗问题,而是 lucky_reward_helper 没有新记录 / valid_invite_count 不涨。

有效助力必须满足的链路

sequenceDiagram
    participant User as slot_user注册
    participant AgentMQ as agent_bus
    participant Agent as slot_agent_EventBus
    participant Gateway as LuckyRewardInviteCallbackGateway
    participant ConsoleMQ as console_bus
    participant Console as slot_console_EventBus
    participant Logic as handleInviteBind

    User->>AgentMQ: TYPE_INVITE_BIND\nshare_origin=wheel\nis_new_register=1
    Agent->>Agent: ref_invite_relation 新建?
    Agent->>Gateway: 仅 isNewInviteRelation=true 时调用
    Gateway->>Gateway: lucky_reward_callback_enabled\nshare_origin=wheel
    Gateway->>ConsoleMQ: TYPE_LUCKY_REWARD_INVITE_BIND
    Console->>Logic: handleInviteBind
    Logic->>Logic: 无 skip 则写 helper + inviter grant

任意一环失败 → DB 无 helper。

按概率排序的常见原因

1. 用了 Agent 中心短码,不是转盘 wheel 短码(最常见误用)

  • agent 网关硬过滤LuckyRewardInviteCallbackGatewayServiceshare_origin=wheel 才投递 console_bususer_agentreason=skipped_not_wheel_originconsole 根本收不到消息
  • console 侧也会再拦resolveInviteBindSkipResult share_origin !== wheelskipped_not_wheel
  • 正确链接:须来自 GET /api/lucky-reward/invite-linkGift/createShareUrl from=wheel(绑定当前 lucky_reward_cycleshare_url.origin=wheel)。

2. wheel 短码已过期(「以前有,现在没有」高概率)

  • 轮次轮转后,旧短码 share_url.biz_id 指向已结束 cycle,与当前 active cycle 不一致 → console 返回 STATUS_SKIPPED_EXPIRED_CYCLE不写 helper(见集成测 testHandleInviteBindSkipsWhenShareCycleMismatchActiveCycle)。
  • 处理:邀请人重新拉 invite-link,用本轮短码测。

3. 被邀请人不是「本次新注册」

  • console 要求 is_new_register=1;老 IMEI 登录 / 手动绑码 → is_new_register=0skipped_old_user
  • IMEI 路径:仅 ImeiRegisterService::run 新建账号notifyAgentInviteBind;已存在 IMEI 走 login不发 invite_bind
  • 手动绑码 UserController 固定 is_new_register=0,且返回体未带 share_originMQ 里为空)→ agent 网关 wheel 过滤失败。

4. 活动未开启或无 active cycle

  • 种子数据默认 lucky_reward_config.status=2(关闭)
  • console resolveInviteBindProcessContext 活动关闭 → activity_closed
  • 弹窗/开宝箱也依赖开启配置,与 helper 同源。

5. agent 仅「首次 ref_invite_relation」才回调 console

slot_agent EventBus::inviteBindEvent

if ($isNewInviteRelation) {
    LuckyRewardInviteCallbackGatewayService::...->notifyFromInviteBindEntity($entity);
}

若该 invitee_uidref_invite_relation 已有行(历史任何来源绑过),不会再投递 lucky_reward_invite_bind,即使本次 MQ 仍处理 agent 奖励。

6. MQ / 开关 / 进程

检查项 位置
lucky_reward_callback_enabled=1 center → agent
php webman event:bus 消费 agent_bus slot_agent
php webman event:bus 消费 console_bus slot_console
agent 日志 luckyRewardCallback dispatched / reason
console 日志 luckyRewardInviteBindEvent status 字段

建议排查步骤(按顺序执行)

  1. 复现一次邀请注册,记录:邀请短码、share_url.originshare_url.biz_id、当前 lucky_reward_cycle.id
  2. 查 agent 日志 inviteBindEvent + luckyRewardCallback
    • luckyRewardCallback → 看 isNewInviteRelation 或 MQ 未进 agent。
    • reason=skipped_not_wheel_origin → 短码/来源不对。
    • reason=callback_disabled → center 开关。
  3. 查 console 日志 EventBus::luckyRewardInviteBindEventstatus
    • 无日志 → console_bus 未消费或 agent 未投递。
    • skipped_expired_cycle / skipped_old_user / activity_closed / skipped_not_wheel → 对号入座。
  4. SQL 验证(替换 uid/cycle
    • SELECT * FROM lucky_reward_helper WHERE invitee_uid=? ORDER BY id DESC LIMIT 5;
    • SELECT * FROM ref_invite_relation WHERE invitee_uid=?;
    • SELECT id,status,cycle_no,end_at FROM lucky_reward_cycle WHERE source='default' ORDER BY id DESC LIMIT 3;
    • SELECT short_code,origin,biz_type,biz_id FROM share_url WHERE short_code='?';

若日志 status=success 仍无 helper

才考虑事务异常/库连接错误(少见);查 console LuckyReward inviteBind failed error 日志。

与弹窗问题的关系

helper 未落库时:valid_invite_count 不涨、Record/helpers 为空、Claim Now / Support Invite 都不会出现(弹窗是 helper/grant 的下游展示)。应先修 invite_bind 链路,再谈弹窗互斥。