286 lines
15 KiB
Markdown
286 lines
15 KiB
Markdown
---
|
||
name: Lucky Reward 双弹窗分析
|
||
overview: 被邀请人 Support Invite 应从 HomeEvent 迁到 lucky_reward_invite_bind 成功时 WS 即时推送(只弹一次);HomeEvent 仅保留邀请人 Claim Now(pending_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::notifyClientPOP;HomeEvent 移除被邀请人逻辑
|
||
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 抽 resolveHomePopups,HomeEvent 只 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 Now,B 只有 Support Invite(见 [`LuckyRewardInviteBindIntegrationTest`](slot_console/tests/Integration/LuckyRewardInviteBindIntegrationTest.php))。
|
||
|
||
**同一 uid 双角色时会出现**,典型场景:
|
||
|
||
1. **互邀 / 双链**:A 邀 B,B 也邀 A(同一 `cycle_id`,均新注册有效助力)
|
||
- A:`helper(invitee=A)` → Support Invite;`pending_spin_count>0` → Claim Now
|
||
- B:同理
|
||
2. **先被邀、后又成功邀别人**:用户 C 邀了 A(A 为被邀请人);之后 A 又邀了 D(A 为邀请人且未 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 Now;Support 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 链路,再谈弹窗互斥。
|
||
|