146 lines
6.9 KiB
Markdown
146 lines
6.9 KiB
Markdown
---
|
||
name: Support Invite 离线补推
|
||
overview: 用户提出的「离线写 Redis + WS 连接后补推」方案可行;项目已有 `user_websocket_connect` 事件与在线 Redis Set,可在 `deliverInviteeSupportInvitePopup` 与连接回调中实现,无需回退 HomeEvent。
|
||
todos:
|
||
- id: pending-service
|
||
content: 新增 LuckyRewardSupportInvitePendingService:Redis key/TTL、enqueue、flush、可选 sent 标记
|
||
status: completed
|
||
- id: deliver-branch
|
||
content: deliverInviteeSupportInvitePopup:在线即时推,离线 enqueue
|
||
status: completed
|
||
- id: ws-connect-flush
|
||
content: WebsocketConnectEventService::run 连接后 flush pending(sAdd 之后)
|
||
status: completed
|
||
- id: tests
|
||
content: 集成/单测:离线 enqueue + connect flush;在线直推不写 Redis
|
||
status: completed
|
||
isProject: false
|
||
---
|
||
|
||
# Support Invite 离线 Redis 补推方案
|
||
|
||
## 结论:方案可行
|
||
|
||
你的思路与现网架构**匹配**,比把 Support Invite 塞回 HomeEvent 更贴切(仍只弹一次,且针对「注册时 WS 尚未连上」的竞态)。
|
||
|
||
现网已有能力:
|
||
|
||
| 能力 | 位置 |
|
||
| --- | --- |
|
||
| WS 连接 MQ | `slot_game` gateway → `console_bus` `TYPE_USER_WEBSOCKET_CONNECT` |
|
||
| 连接处理入口 | [`EventBus::websocketConnectEvent`](slot_console/app/command/EventBus.php) → [`WebsocketConnectEventService::run`](slot_console/app/service/event/WebsocketConnectEventService.php) |
|
||
| 在线判定 Redis Set | `RedisKeyManagerService::getUserOnlineKey()`,connect 时 `sAdd`、disconnect 时 `sRem` |
|
||
| 即时弹窗 | [`WsService::notifyClientPOP`](slot_console/app/service/WsService.php) → hub MQ(**离线会丢**) |
|
||
|
||
[`DelayTipPopService`](slot_console/app/service/DelayTipPopService.php) 是「延时 N 秒再推 tipPop」的 ZSet,**不适合**本场景;本需求是「等 WS 连上再推」,应走 **connect 时 flush**,不是 cron 扫延时队列。
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant Bind as invite_bind_success
|
||
participant Logic as deliverInviteeSupportInvitePopup
|
||
participant Redis as Redis_pending
|
||
participant Hub as WsService
|
||
participant WS as websocketConnectEvent
|
||
|
||
Bind->>Logic: openBox + buildPopup
|
||
alt user_online
|
||
Logic->>Hub: notifyClientPOP
|
||
else user_offline
|
||
Logic->>Redis: SET payload EX TTL
|
||
end
|
||
WS->>Redis: GET pending
|
||
alt has_pending
|
||
WS->>Hub: notifyClientPOP
|
||
WS->>Redis: DEL
|
||
end
|
||
```
|
||
|
||
---
|
||
|
||
## 推荐实现(在 Redis 方案上略作增强)
|
||
|
||
### 1. 新增 `LuckyRewardPendingPopupService`(或 Logic 内 private + 小 Service)
|
||
|
||
职责单一:**被邀请人 Support Invite 的 pending 投递**。
|
||
|
||
- **Redis Key**:`lucky_reward:support_invite_popup:{uid}:{cycle_id}`(幂等,同一轮次只存一条)
|
||
- **Value**:`json_encode` 后的 popList 单项(与现 `buildSupportInvitePopup` 结构一致)
|
||
- **TTL**:建议 **30~60 分钟**(覆盖注册加载 + WS 握手;过短会丢弹,过长无必要)
|
||
- **写入**:`SET key value EX ttl NX`(NX 防止 bind 重试覆盖/重复)
|
||
|
||
### 2. 改 [`deliverInviteeSupportInvitePopup`](slot_console/app/api/logic/LuckyRewardInviteLogic.php)
|
||
|
||
顺序不变:**先** `ensureInviteeAutoOpenBox`,**再** `buildSupportInvitePopup`;推送逻辑改为:
|
||
|
||
```php
|
||
if ($this->isUserOnline($inviteeUid)) {
|
||
WsService::notifyClientPOP($inviteeUid, [$supportInvitePopup]);
|
||
return;
|
||
}
|
||
$this->pendingPopupService->enqueueSupportInvite($inviteeUid, $cycleId, $supportInvitePopup);
|
||
```
|
||
|
||
**在线判定**:`Redis::sIsMember(RedisKeyManagerService::getUserOnlineKey(), $inviteeUid)`(与现网 disconnect 维护的 Set 一致)。
|
||
|
||
> 注意:存在 **竞态**(bind 比 `user_websocket_connect` 早几十~几百 ms)。可选增强(推荐):**在线则即时推 + 仍 SET NX pending**;connect flush 时 **GETDEL**,若已推过则 payload 相同、客户端需幂等忽略重复 type。更简单做法:**仅离线写 Redis**;若偶发丢弹可接受则不做双写。
|
||
|
||
### 3. WS 连接时 flush
|
||
|
||
在 [`WebsocketConnectEventService::run($uid)`](slot_console/app/service/event/WebsocketConnectEventService.php) 末尾(或 `EventBus::websocketConnectEvent` 调 Logic 一行):
|
||
|
||
1. 查当前 active `cycle_id`(与 `buildSupportInvitePopup` 同源)
|
||
2. `GETDEL lucky_reward:support_invite_popup:{uid}:{cycle_id}`
|
||
3. 有值 → `json_decode` → `WsService::notifyClientPOP($uid, [$popup])`
|
||
|
||
**只弹一次**:发送成功后 key 已 DEL;重连不会重复(除非 bind 再次 success 且 NX 允许新 key——同一 cycle 不会)。
|
||
|
||
### 4. 可选:DB 兜底(比纯 Redis 更稳)
|
||
|
||
若担心 TTL 内仍未连 WS:connect 时除读 Redis 外,可 **fallback**:
|
||
|
||
- `helper` 存在 + `box_opened_at > 0` + Redis 无 key + 从未发送标记
|
||
→ 再 `buildSupportInvitePopup` 推一次
|
||
|
||
发送标记可用 Redis `lucky_reward:support_invite_sent:{uid}:{cycle_id}` SET NX(永久或 7d TTL)。**与「只弹一次」产品一致**,且 bind 时 openBox 已落库,**不依赖 Redis 里缓存 payload**。
|
||
|
||
建议:**Redis 存 payload 为主,connect 时 GETDEL;TTL 过期则 fallback 从 DB 重建一次**(实现成本低、容错更好)。
|
||
|
||
---
|
||
|
||
## 不建议的做法
|
||
|
||
- **不要**再挂 HomeEvent(与「只弹一次、注册时触发」冲突,且会每次回大厅重复弹)
|
||
- **不要**复用 `user_pending_popup` 表(项目已废弃,见 deploy 文档)
|
||
- **不要**只用 `DelayTipPopService` 固定延时 N 秒(N 难估,且 WS 未连上仍会丢)
|
||
|
||
---
|
||
|
||
## 改动文件(实施范围)
|
||
|
||
| 文件 | 改动 |
|
||
| --- | --- |
|
||
| 新建 `app/service/luckyReward/LuckyRewardSupportInvitePendingService.php` | enqueue / flush / isUserOnline 封装 |
|
||
| [`LuckyRewardInviteLogic.php`](slot_console/app/api/logic/LuckyRewardInviteLogic.php) | `deliverInviteeSupportInvitePopup` 分支在线/离线 |
|
||
| [`WebsocketConnectEventService.php`](slot_console/app/service/event/WebsocketConnectEventService.php) | connect 时 flush pending |
|
||
| [`RedisKeyManagerService.php`](slot_console/app/service/RedisKeyManagerService.php) | 新增 key 常量 + 中文注释 |
|
||
| 单测/集成测 | mock Redis 或集成测:离线 enqueue → connect flush |
|
||
|
||
**不在本次范围**(除非你要求):YApi/05 文档、邀请人 Claim Now、helper 未落库排查。
|
||
|
||
---
|
||
|
||
## 风险与对策
|
||
|
||
| 风险 | 对策 |
|
||
| --- | --- |
|
||
| bind 早于 connect,误判离线 | 可接受则只写 Redis;要严格则 connect 必 flush + DB fallback |
|
||
| hub 已连但 console 在线 Set 未更新 | connect 事件顺序:flush 放在 `websocketConnectEvent` **sAdd 之后** |
|
||
| 重复弹 | key 含 cycle_id + GETDEL + 客户端按 type 去重 |
|
||
| TTL 过期未连 WS | fallback 从 DB rebuild(helper + opened box) |
|
||
|
||
---
|
||
|
||
## 结论
|
||
|
||
**可行,且是正确补洞方向**;实现上建议:**离线 Redis pending + WS connect flush**,并加 **DB fallback / sent 标记** 防止 TTL 丢弹。在线判定复用现有 `USER_ONLINE_SET`,接入点放在已存在的 `WebsocketConnectEventService`。
|