Files
cursor/plans/support_invite_离线补推_847de6ef.plan.md
ray zhou 2dd9f17da9 ok
2026-06-29 14:51:55 +08:00

146 lines
6.9 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: Support Invite 离线补推
overview: 用户提出的「离线写 Redis + WS 连接后补推」方案可行;项目已有 `user_websocket_connect` 事件与在线 Redis Set可在 `deliverInviteeSupportInvitePopup` 与连接回调中实现,无需回退 HomeEvent。
todos:
- id: pending-service
content: 新增 LuckyRewardSupportInvitePendingServiceRedis key/TTL、enqueue、flush、可选 sent 标记
status: completed
- id: deliver-branch
content: deliverInviteeSupportInvitePopup在线即时推离线 enqueue
status: completed
- id: ws-connect-flush
content: WebsocketConnectEventService::run 连接后 flush pendingsAdd 之后)
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**:建议 **3060 分钟**(覆盖注册加载 + 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 内仍未连 WSconnect 时除读 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 时 GETDELTTL 过期则 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 rebuildhelper + opened box |
---
## 结论
**可行,且是正确补洞方向**;实现上建议:**离线 Redis pending + WS connect flush**,并加 **DB fallback / sent 标记** 防止 TTL 丢弹。在线判定复用现有 `USER_ONLINE_SET`,接入点放在已存在的 `WebsocketConnectEventService`