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

286 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
name: Lucky Reward 双弹窗分析
overview: 被邀请人 Support Invite 应从 HomeEvent 迁到 lucky_reward_invite_bind 成功时 WS 即时推送只弹一次HomeEvent 仅保留邀请人 Claim Nowpending_spin_count可重复补弹。这样同一 uid 在 HomeEvent 不再同时命中两种 Lucky Rewards 弹窗。
todos:
- id: diagnose-no-helper
content: 按日志/SQL 排查 invite_bind 全链路,定位 helper 未落库的具体 status/reason
status: pending
- id: move-support-invite-to-bind
content: invite_bind success 后ensureInviteeAutoOpenBox + buildSupportInvitePopup + WsService::notifyClientPOPHomeEvent 移除被邀请人逻辑
status: pending
- id: update-tests-docs-yapi
content: 更新集成测、05 方案文档、YApi #109 WS 弹窗示例Claim Now 仍走 HomeEvent
status: pending
- id: confirm-product-rule
content: 确认产品规则:双角色用户允许两种弹窗,还是同一 flush 只保留一种(及优先级)
status: cancelled
- id: implement-mutex-if-needed
content: 若需互斥:在 LuckyRewardInviteLogic 抽 resolveHomePopupsHomeEvent 只 merge 一种;补互邀集成测
status: cancelled
- id: support-invite-once
content: (可选)若 Support Invite 应只弹一次:增加 consume/已读状态,与互斥分开评估
status: cancelled
isProject: false
---
# Lucky Rewards 同一人两种弹窗 — 根因与可选改法
## 现象对应代码
[`HomeEvent::appendLuckyRewardHomePopups`](slot_console/app/command/event/HomeEvent.php) 对**同一个** `$uid` 顺序做两件事,**互不排斥**
```437:455:slot_console/app/command/event/HomeEvent.php
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`](slot_console/app/api/logic/LuckyRewardInviteLogic.php) | `lucky_reward_helper` 存在 **invitee_uid = uid** 且 **已开箱** |
| Claim Now | `lucky_reward_invite_success` | [`buildInviterClaimNowPopup`](slot_console/app/api/logic/LuckyRewardInviteLogic.php) | `pending_spin_count > 0` 且存在 **未领的 GRANT_TYPE_INVITER grant** |
需求文档 [`05_slot_console_邀请助力与弹窗方案.md`](docs/requirements/lucky_rewards/05_slot_console_邀请助力与弹窗方案.md) 表格里写的是「邀请人 / 被邀请人」两种**角色**,但代码**没有**写「同一 uid 只能占一种角色」。
```mermaid
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 Invite见 [`LuckyRewardInviteBindIntegrationTest`](slot_console/tests/Integration/LuckyRewardInviteBindIntegrationTest.php))。
**同一 uid 双角色时会出现**,典型场景:
1. **互邀 / 双链**A 邀 BB 也邀 A同一 `cycle_id`,均新注册有效助力)
- A`helper(invitee=A)` → Support Invite`pending_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`](slot_console/app/api/logic/LuckyRewardInviteLogic.php) 只给**邀请人**写 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`](slot_console/app/command/event/HomeEvent.php),导致:
- 被邀请人**每次回大厅重复弹** Support Invite无 consume
- 双角色用户同一 flush 可能同时出现两种 type
## 目标架构
```mermaid
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`](slot_console/app/command/EventBus.php)
`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`](slot_console/app/command/event/HomeEvent.php)
**删除**
- `ensureInviteeAutoOpenBox`
- `buildSupportInvitePopup` 及 merge
**保留**
- `buildInviterClaimNowPopup` → Claim Now
方法可重命名为 `appendInviterClaimNowPopup` 或保持原名但注释只服务邀请人。
### 3. 测试
- [`LuckyRewardInviteBindIntegrationTest`](slot_console/tests/Integration/LuckyRewardInviteBindIntegrationTest.php)`testEnsureInviteeAutoOpenBoxAndSupportInvitePopup` 改为断言 bind 成功路径mock/spy `WsService` 或抽 `pushInviteeSupportInvitePopup` 返回值)
- 新增bind 幂等第二次不推 WS
- HomeEvent 单测:被邀请人 uid flush **不应**再含 `lucky_reward_support_invite`
### 4. 文档
- [`05_slot_console_邀请助力与弹窗方案.md`](docs/requirements/lucky_rewards/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 NowSupport Invite 仅在作为被邀请人 bind 当时推一次 |
## 与「没有 helper」排查的关系
Support Invite 迁出 **不解决** helper 未落库;须先保证 `handleInviteBind` → `success` 写 helper再在 success 分支推 WS。
---
# 「没有助力」DB 无 lucky_reward_helper— 排查结论
> 你已确认:**不是弹窗问题**,而是 `lucky_reward_helper` 没有新记录 / `valid_invite_count` 不涨。
## 有效助力必须满足的链路
```mermaid
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`](slot_agent/app/service/LuckyRewardInviteCallbackGatewayService.php) 仅 `share_origin=wheel` 才投递 console_bus`user_agent` → `reason=skipped_not_wheel_origin`**console 根本收不到消息**。
- **console 侧也会再拦**[`resolveInviteBindSkipResult`](slot_console/app/api/logic/LuckyRewardInviteLogic.php) `share_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`](slot_console/app/entity/luckyReward/InviteBindResultEntity.php)**不写 helper**(见集成测 `testHandleInviteBindSkipsWhenShareCycleMismatchActiveCycle`)。
- **处理**:邀请人重新拉 `invite-link`,用**本轮**短码测。
### 3. 被邀请人不是「本次新注册」
- console 要求 `is_new_register=1`;老 IMEI 登录 / 手动绑码 → `is_new_register=0` → `skipped_old_user`。
- IMEI 路径:仅 [`ImeiRegisterService::run`](slot_user/app/service/register/ImeiRegisterService.php) **新建账号**时 `notifyAgentInviteBind`;已存在 IMEI 走 login**不发 invite_bind**。
- 手动绑码 [`UserController`](slot_user/app/innerapi/controller/UserController.php) 固定 `is_new_register=0`,且返回体**未带** `share_origin`MQ 里为空)→ agent 网关 wheel 过滤失败。
### 4. 活动未开启或无 active cycle
- 种子数据默认 [`lucky_reward_config.status=2`(关闭)](backend/slot_admin/db/lucky_reward.sql)。
- console `resolveInviteBindProcessContext` 活动关闭 → `activity_closed`。
- 弹窗/开宝箱也依赖开启配置,与 helper 同源。
### 5. agent 仅「首次 ref_invite_relation」才回调 console
[`slot_agent EventBus::inviteBindEvent`](slot_agent/app/command/EventBus.php)
```php
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` 字段 |
## 建议排查步骤(按顺序执行)
1. **复现一次邀请注册**,记录:邀请短码、`share_url.origin`、`share_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::luckyRewardInviteBindEvent` 的 `status`
- 无日志 → 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 链路,再谈弹窗互斥。