15 KiB
name, overview, todos, isProject
| name | overview | todos | isProject | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Lucky Reward 双弹窗分析 | 被邀请人 Support Invite 应从 HomeEvent 迁到 lucky_reward_invite_bind 成功时 WS 即时推送(只弹一次);HomeEvent 仅保留邀请人 Claim Now(pending_spin_count,可重复补弹)。这样同一 uid 在 HomeEvent 不再同时命中两种 Lucky Rewards 弹窗。 |
|
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 Now,B 只有 Support Invite(见 LuckyRewardInviteBindIntegrationTest)。
同一 uid 双角色时会出现,典型场景:
- 互邀 / 双链:A 邀 B,B 也邀 A(同一
cycle_id,均新注册有效助力)- A:
helper(invitee=A)→ Support Invite;pending_spin_count>0→ Claim Now - B:同理
- A:
- 先被邀、后又成功邀别人:用户 C 邀了 A(A 为被邀请人);之后 A 又邀了 D(A 为邀请人且未 claim)→ 回大厅两种都满足
- 邀请人未 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_bind → status=success 后 |
WsService::notifyClientPOP(inviteeUid, [...]) |
一次(bind 幂等保证不重复推) |
| Claim Now | HomeEvent flush | 合并进 popList |
未 claim 前每次回大厅 |
代码改动要点
1. EventBus::luckyRewardInviteBindEvent
handleInviteBind 返回 success 后新增(建议封装为 LuckyRewardInviteLogic::pushInviteeSupportInvitePopup()):
ensureInviteeAutoOpenBox($inviteeUid, $source)— 从 HomeEvent 挪到这里(弹窗需要my_amount)buildSupportInvitePopup($inviteeUid, $source)- 非 false 则
WsService::notifyClientPOP($inviteeUid, [$popup])
already_processed / 各类 skipped_* 不推送。
2. HomeEvent::appendLuckyRewardHomePopups
删除:
ensureInviteeAutoOpenBoxbuildSupportInvitePopup及 merge
保留:
buildInviterClaimNowPopup→ Claim Now
方法可重命名为 appendInviterClaimNowPopup 或保持原名但注释只服务邀请人。
3. 测试
LuckyRewardInviteBindIntegrationTest:testEnsureInviteeAutoOpenBoxAndSupportInvitePopup改为断言 bind 成功路径(mock/spyWsService或抽pushInviteeSupportInvitePopup返回值)- 新增:bind 幂等第二次不推 WS
- HomeEvent 单测:被邀请人 uid flush 不应再含
lucky_reward_support_invite
4. 文档
05_slot_console_邀请助力与弹窗方案.md§5 表格拆分触发点- YApi #109 WS popup 示例补 Support Invite 即时推送说明
边界说明
| 点 | 处理 |
|---|---|
| MQ 消费略晚于注册完成 | 产品认定被邀请人在线;仍走 WS 即时推。若 hub 未连上,不做 HomeEvent 补弹(与「只弹一次、无离线」一致) |
| 游戏内禁弹 | bind 发生在注册链路,通常不在 enter_game;若需硬约束可后续加 hub 侧 scene 判断 |
| openBox 失败 | 记录 error,不推 Support Invite(无 my_amount) |
| 双角色用户 | HomeEvent 仅可能出 Claim Now;Support Invite 仅在作为被邀请人 bind 当时推一次 |
与「没有 helper」排查的关系
Support Invite 迁出 不解决 helper 未落库;须先保证 handleInviteBind → success 写 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 网关硬过滤:
LuckyRewardInviteCallbackGatewayService仅share_origin=wheel才投递 console_bus;user_agent→reason=skipped_not_wheel_origin,console 根本收不到消息。 - console 侧也会再拦:
resolveInviteBindSkipResultshare_origin !== wheel→skipped_not_wheel。 - 正确链接:须来自
GET /api/lucky-reward/invite-link或Gift/createShareUrl from=wheel(绑定当前lucky_reward_cycle的share_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=0→skipped_old_user。 - IMEI 路径:仅
ImeiRegisterService::run新建账号时notifyAgentInviteBind;已存在 IMEI 走 login,不发 invite_bind。 - 手动绑码
UserController固定is_new_register=0,且返回体未带share_origin(MQ 里为空)→ 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_uid 在 ref_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 字段 |
建议排查步骤(按顺序执行)
- 复现一次邀请注册,记录:邀请短码、
share_url.origin、share_url.biz_id、当前lucky_reward_cycle.id。 - 查 agent 日志
inviteBindEvent+luckyRewardCallback:- 无
luckyRewardCallback→ 看isNewInviteRelation或 MQ 未进 agent。 reason=skipped_not_wheel_origin→ 短码/来源不对。reason=callback_disabled→ center 开关。
- 无
- 查 console 日志
EventBus::luckyRewardInviteBindEvent的status:- 无日志 → console_bus 未消费或 agent 未投递。
skipped_expired_cycle/skipped_old_user/activity_closed/skipped_not_wheel→ 对号入座。
- 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 链路,再谈弹窗互斥。