--- name: InviteBind 规范与弹窗 overview: 重构 `LuckyRewardInviteLogic::handleInviteBind` 以符合 php-clean-code 分层写法,并在有效助力成功时可靠入队邀请人「Claim Now」弹窗(在线 WS 即时推送 + 离线/游戏中 HomeEvent 补弹),对齐 PRD 4.2 与子需求 F/D 文档。 todos: - id: invite-bind-context-dto content: 新增 InviteBindProcessContextDTO;fetchUserProfile 改为返回 LuckyRewardUserProfileEntity status: completed - id: refactor-handle-invite-bind content: 拆分 handleInviteBind 为编排 + private 步骤(校验/上下文/事务/弹窗/日志) status: completed - id: inviter-popup-prd-42 content: notifyInviterInviteSuccess:成功必入队,补全 PRD 4.2 payload,去掉 spin_reward>0 早退 status: completed - id: tests-docs-verify content: 增强 InviteBind 集成测 payload 断言;更新 05 文档;跑 verify + phpunit status: completed isProject: false --- # LuckyRewardInviteLogic handleInviteBind 重构与邀请人弹窗 ## 现状与问题 [`handleInviteBind`](slot_console/app/api/logic/LuckyRewardInviteLogic.php) 约 160 行,单方法内堆叠:参数校验、活动/轮次解析、幂等判断、事务写 helper/grant/player、弹窗入队、日志。 **编码规范偏差(对照 `php-clean-code`):** | 问题 | 现状 | |------|------| | 方法过长 | `handleInviteBind` 远超 50 行建议 | | 编排不清晰 | 校验 / 发奖 / 弹窗混在一个方法 | | 返回值不规范 | `fetchUserProfile()` 返回 `array{nickname,avatar}`,同模块已有 [`LuckyRewardUserProfileEntity`](slot_console/app/entity/luckyReward/LuckyRewardUserProfileEntity.php)([`LuckyRewardLogic`](slot_console/app/api/logic/LuckyRewardLogic.php) 已使用) | | 弹窗条件偏窄 | [`enqueueInviterSuccessPopup`](slot_console/app/api/logic/LuckyRewardInviteLogic.php) 在 `inviterSpinReward <= 0` 时直接 return,有效助力成功但配置为 0 时不入队 | **弹窗能力已存在但未在编排层显式表达:** - 成功路径已调用 `enqueueInviterSuccessPopup` → [`PendingPopupService::enqueue`](slot_console/app/service/PendingPopupService.php) → 写 `user_pending_popup` + [`WsService::notifyClientPOP`](slot_console/app/service/WsService.php)(在线即时推送) - 离线/游戏中:`HomeEvent::appendLuckyRewardHomePopups` 从待弹队列补弹([`05 邀请方案 §5`](docs/requirements/lucky_rewards/05_slot_console_邀请助力与弹窗方案.md)、[`07 待弹窗中心`](docs/requirements/lucky_rewards/07_用户定向弹窗通知机制方案.md)) - 集成测已断言 pending 记录存在([`LuckyRewardInviteBindIntegrationTest`](slot_console/tests/Integration/LuckyRewardInviteBindIntegrationTest.php)),但未覆盖「成功必入队 + payload 结构」 PRD [4.2 邀请成功](docs/requirements/转盘活动-功能需求文档.md):被邀请人注册成功后,**邀请人**收到 LUCKY YOU / 1 Free Spin / Claim Now;仅回大厅弹、游戏内禁弹(由客户端忽略 WS + HomeEvent flush 保证)。 ```mermaid sequenceDiagram participant Agent as slot_agent_MQ participant Logic as handleInviteBind participant DB as s_common participant Popup as PendingPopupService participant WS as WsService participant Home as HomeEvent Agent->>Logic: invite_bind callback Logic->>DB: helper + grant + player Logic->>Popup: enqueue inviter popup Popup->>DB: user_pending_popup Popup->>WS: notifyClientPOP Note over WS: 在线大厅即时弹 Home->>Popup: fetchPending on home Popup->>WS: 补弹离线/回大厅 ``` --- ## 目标结构 ### `handleInviteBind` 编排(public,读流程) ```php public function handleInviteBind(InviteBindCallbackDTO $callbackDto): InviteBindResultEntity { $earlyResult = $this->resolveInviteBindSkipResult($callbackDto); if ($earlyResult !== null) { return $earlyResult; } $processContext = $this->resolveInviteBindProcessContext($callbackDto); if ($processContext instanceof InviteBindResultEntity) { return $processContext; // activity closed 等 } if ($this->isInviteHelperAlreadyProcessed($processContext->cycleId, $callbackDto->inviteeUid)) { return $this->buildInviteBindResult(STATUS_ALREADY_PROCESSED, ...); } $this->persistInviteHelperInTransaction($callbackDto, $processContext); $this->notifyInviterInviteSuccess($callbackDto, $processContext); $this->logInviteBindSuccess($callbackDto, $processContext); return $this->buildInviteBindResult(STATUS_SUCCESS, ...); } ``` ### 新增编排上下文 DTO [`InviteBindProcessContextDTO`](slot_console/app/dto/luckyReward/InviteBindProcessContextDTO.php)(readonly),承载本用例多步共享字段,避免 private 方法重复挂标量: - `resolvedSource`, `cycleId`, `inviterSpinReward`, `inviteeSpinReward` - `inviterBizId`, `inviteeBizId`, `popupBizId` ### 拆分 private 方法(均有中文 PHPDoc) | 方法 | 职责 | |------|------| | `resolveInviteBindSkipResult` | 参数非法 / 自邀 / 老用户 / 非 wheel 来源 → early return Entity | | `resolveInviteBindProcessContext` | 解析活动配置与活跃轮次;失败返回 `InviteBindResultEntity` | | `isInviteHelperAlreadyProcessed` | 封装 `LuckyRewardHelperModel::findByCycleAndInvitee` | | `persistInviteHelperInTransaction` | 事务:写 helper、发 inviter/invitee grant、更新 player | | `createInviteHelperRecord` | 写 helper 行 | | `grantInviterSpinForHelper` | inviter grant + player spin/valid_invite_count | | `grantInviteeSpinForHelper` | invitee grant + player spin | | `notifyInviterInviteSuccess` | **PRD 4.2**:有效助力成功后入队 + WS(委托 `PendingPopupService`) | | `logInviteBindSuccess` | 成功业务日志 | ### 弹窗行为调整(PRD 4.2) - **成功即入队**:`notifyInviterInviteSuccess` 在 `STATUS_SUCCESS` 路径**始终**调用(去掉 `inviterSpinReward <= 0` 早退);`payload.spin_reward` 仍传配置值(可为 0) - **payload 对齐前端**(在现有字段基础上补全,不破坏已对接字段): - 保留:`invitee_uid`, `invitee_nickname`, `invitee_avatar`, `spin_reward`, `cycle_id` - 新增(PRD 文案,便于客户端少写死):`title`=`LUCKY YOU!`, `content`=`You've received 1 Free Spin`, `button_text`=`Claim Now`, `priority` - **在线推送**:继续复用 `PendingPopupService::enqueue`(内部已 WS);Logic 不重复调 `WsService` - **离线/游戏中**:依赖已有 `user_pending_popup` + `HomeEvent::appendLuckyRewardHomePopups`,无需 Logic 判断在线 ### 用户资料返回 Entity - `fetchUserProfile(int $uid): LuckyRewardUserProfileEntity` 替换 array 返回 - `enqueueInviterSuccessPopup` / `buildSupportInvitePopup` 同步改用 Entity 字段(后者可顺带改,范围小) --- ## 测试 | 用例 | 文件 | |------|------| | 现有 early-return 单测 | [`LuckyRewardInviteLogicUnitTest`](slot_console/tests/Unit/LuckyRewardInviteLogicUnitTest.php) 保持 | | 集成:成功写 helper + pending popup | 已有;**增强**断言 `popup_type`、`payload` 含 `title/content/spin_reward` | | 集成:第二次回调幂等不入队新 popup | 已有 `already_processed`;确认 popup 仍仅 1 条 | 运行:`RUN_DB_TESTS=1` 跑 `LuckyRewardInviteBindIntegrationTest` + 现有 Unit 测。 --- ## 文档 - 更新 [`docs/requirements/lucky_rewards/05_slot_console_邀请助力与弹窗方案.md`](docs/requirements/lucky_rewards/05_slot_console_邀请助力与弹窗方案.md) §5 payload 示例(若新增 title/content 字段) - 无需改 plan 文件 --- ## 不在本次范围 - `buildSupportInvitePopup` 改返回 Entity(可选后续;本次仅改其用到的 `fetchUserProfile`) - 服务端精确判断「是否在大厅/是否在线」(07 文档一期不做,依赖 WS + home flush) - slot_hub / 客户端弹窗 UI 实现 --- ## 自检 - `verify-slot-backend.sh` + docker `php -l` - 对照 Logic §3/§8:方法长度、中文 PHPDoc、无 Logic 返回 array、编排 DTO 不双份拷贝