Files
cursor/plans/invitebind_规范与弹窗_23c1cd10.plan.md
ray zhou 2dd9f17da9 ok
2026-06-29 14:51:55 +08:00

161 lines
8.0 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: InviteBind 规范与弹窗
overview: 重构 `LuckyRewardInviteLogic::handleInviteBind` 以符合 php-clean-code 分层写法并在有效助力成功时可靠入队邀请人「Claim Now」弹窗在线 WS 即时推送 + 离线/游戏中 HomeEvent 补弹),对齐 PRD 4.2 与子需求 F/D 文档。
todos:
- id: invite-bind-context-dto
content: 新增 InviteBindProcessContextDTOfetchUserProfile 改为返回 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`(内部已 WSLogic 不重复调 `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 不双份拷贝