--- 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`。