This commit is contained in:
ray zhou
2026-06-29 14:51:55 +08:00
parent 225fb2bd28
commit 2dd9f17da9
319 changed files with 29461 additions and 9412 deletions

View File

@@ -0,0 +1,145 @@
---
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`