Files
cursor/plans/is_new_user recommend-game-09c08dbd.plan.md
ray zhou 2dd9f17da9 ok
2026-06-29 14:51:55 +08:00

280 lines
12 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.

<!-- 09c08dbd-5e66-4d08-b29e-090a18ddb10d -->
---
todos:
- id: "user-is-new-user"
content: "slot-userAbstractRegisterService 增加 isNewUser 标志Mobile/Imei/Name RegisterService 赋值UserController::register 返回 is_new_user"
status: pending
- id: "console-ggame-query"
content: "slot-consoleGGame 新增按 game_code 查可展示单条游戏方法"
status: pending
- id: "console-recommend-logic"
content: "slot-consoleOnboardingRecommendLogiccenter 取池 → 随机 → g_game 组装 → 降级)"
status: pending
- id: "console-recommend-api"
content: "slot-consoleOnboardingController + GET /api/onboarding/recommend-game"
status: pending
- id: "update-docs"
content: "更新 register_popup_game_entry 文档:前端 is_new_user 条件请求 recommend-game 链路"
status: pending
- id: "lobby-auth-types"
content: "lobbyAuthResponse 增加 is_new_userauth.service 注册后按标志决定是否拉 recommend-game"
status: pending
- id: "lobby-onboarding-api"
content: "lobby新增 api/console/onboarding.api.ts 调用 GET /slot-console/api/onboarding/recommend-game"
status: pending
- id: "lobby-onboarding-popup"
content: "lobby新用户注册成功后展示 OnboardingGamePopup倒计时 + LET'S PLAY + 关闭)"
status: pending
isProject: false
---
# is_new_user + console recommend-game 方案
## 方案评价
该方案**推荐采用**,理由:
- 符合 SSOT [`register_popup_game_entry.md`](docs/requirements/register_popup_game_entry.md) 服务边界user 管身份console 管展示聚合
- 前端仍直连 `slot-user` 注册,无需改注册入口
- `MobileRegisterService` / `ImeiRegisterService` 已有「账号存在则走登录」逻辑,正好需要 `is_new_user` 区分
- 老用户误走 register 接口时不会触发 onboarding 弹窗
- 注册主链路不受 center/console 降级影响(推荐游戏失败只影响弹窗)
```mermaid
sequenceDiagram
participant FE as lobby_PWA
participant User as slot_user
participant Console as slot_console
participant Center as slot_center
participant GGame as g_game
FE->>User: POST register
alt 新用户创建
User-->>FE: token + uid + is_new_user=true
FE->>Console: GET recommend-game (Bearer token)
Console->>Center: innerapi onboarding/channel-game
Center-->>Console: game_codes
Console->>GGame: 按 game_code 查可展示游戏
Console-->>FE: onboarding payload
else 老用户登录回退
User-->>FE: token + uid + is_new_user=false
Note over FE: 不请求 recommend-game
end
```
---
## Part 1slot-user 增加 `is_new_user`
**涉及仓库**[`/Users/ray/Documents/project/www/slot/slot_user`](/Users/ray/Documents/project/www/slot/slot_user)
### 现状
[`MobileRegisterService.php`](/Users/ray/Documents/project/www/slot/slot_user/app/service/register/MobileRegisterService.php) 与 [`ImeiRegisterService.php`](/Users/ray/Documents/project/www/slot/slot_user/app/service/register/ImeiRegisterService.php) 在 `check()` 返回 false 时会走 `MobileLoginService` / `ImeiLoginService`,但 Controller 无法区分「新建」与「登录回退」。
[`NameRegisterService.php`](/Users/ray/Documents/project/www/slot/slot_user/app/service/register/NameRegisterService.php) 账号已存在直接返回 null报错成功则一定是新用户。
### 实现方式(最小改动)
在 [`AbstractRegisterService.php`](/Users/ray/Documents/project/www/slot/slot_user/app/service/register/AbstractRegisterService.php) 增加:
```php
/** 本次 register 请求是否真正创建了新用户false 表示走了登录回退) */
public bool $isNewUser = false;
```
各 RegisterService 在对应分支赋值:
| 类型 | 新用户创建 | 登录回退 |
|------|-----------|---------|
| mobile | `isNewUser = true` | `isNewUser = false` |
| imei | `isNewUser = true` | `isNewUser = false` |
| name | `isNewUser = true` | N/A失败返回 null |
在 [`UserController::register()`](`/Users/ray/Documents/project/www/slot/slot_user/app/api/controller/UserController.php) 响应中追加:
```php
$data['is_new_user'] = $registerService->isNewUser;
```
**login 接口不改**(老用户走 login 本就不弹窗)。
### 注册响应示例
```json
{
"token": "...",
"uid": 1387459,
"expires_in": 1234567890,
"is_new_user": true
}
```
---
## Part 2slot-console 实现 recommend-game BFF03 子需求)
**涉及仓库**[`/Users/ray/Documents/project/www/ray/slot-console`](/Users/ray/Documents/project/www/ray/slot-console)
### 路由
沿用 Webman 自动路由(参照 [`RewardGrantController`](slot-console/app/api/controller/RewardGrantController.php)
```text
GET /api/onboarding/recommend-game
```
需登录JWT`$request->userEntity->uid` / `$request->userEntity->source` 取用户上下文。
### 核心 Logic新建
`app/api/logic/OnboardingRecommendLogic.php` 编排步骤(对齐需求 §5.1
1.`uid``source`source 为空按 `DEFAULT` 交给 center 回退)
2.`CenterClient``onboardingChannelGame($source)`SDK 已有:[`CenterService.php`](slot_sdk/src/service/center/CenterService.php)
3. `enabled=false``game_codes` 为空 → 返回降级 payload
4.`game_codes` **简单随机**选一个;查 [`GGame`](slot-console/app/model/common/GGame.php) 可展示游戏
5. 不可用则剔除后继续随机;全部不可用 → `enabled=false`
6. 组装返回(字段见需求 §6.1
### GGame 扩展
在 [`GGame.php`](slot-console/app/model/common/GGame.php) 新增按 `game_code` 查单条可展示游戏方法(复用 `applyLobbyListableJoinsAndWhere`,返回 `id/name/icon_url/provider_code/game_code/category`)。
可参照 slot-admin [`OnboardingChannelGameLogic::resolveGamesByGameCodes`](slot-admin/app/game/logic/OnboardingChannelGameLogic.php) 的字段映射,但用户侧只返回需求文档定义的 `game` 结构。
### GatewayService可选薄封装
`app/service/center/CenterGatewayService.php`:封装 `CenterClient` 初始化host 来自 `ShareConfigService::get('centerApiHost')`header `server-name`Logic 不直接拼 HTTP。
### Controller新建
`app/api/controller/OnboardingController.php`
- `recommendGame(Request $request)` → 调 Logic → `success($payload)`
- center 调用失败 / 异常:记录日志,返回 `enabled=false`**不抛错阻断**
### 展示字段来源
| 字段 | 来源 |
|------|------|
| `uid` / `player_number` | JWT uid |
| `player_number_display` | `number_format($uid)` |
| `platform_name` | `ShareConfigService::sourceInfo($source)` 或 center 渠道配置(首版可用 `config`/常量,与运营名对齐) |
| `trial_bonus_amount` | 活动配置常量 `20000`$20 × 1000对齐 [`03_wallet_fund_flow.md`](docs/requirements/trial_withdrawal/03_wallet_fund_flow.md) |
| `countdown_seconds` | 有游戏时 `3`,无游戏时 `0` |
| `game` | GGame 查询结果 |
---
## Part 3lobby 前端接入(游戏大厅 PWA
**涉及仓库**[`/Users/ray/Documents/project/www/ray/lobby`](/Users/ray/Documents/project/www/ray/lobby)
这就是你的前端应用:**Vue 3 + Vite + Vant**,网关前缀通过 `VITE_APP_API_BASE_URL` 代理到各 slot 服务。
### 现有注册链路(已确认)
| 项 | 位置 |
| --- | --- |
| 注册 API | [`lobby/src/api/user/auth.api.ts`](lobby/src/api/user/auth.api.ts) → `POST /slot-user/api/user/register` |
| 注册编排 | [`lobby/src/services/user/auth.service.ts`](lobby/src/services/user/auth.service.ts) → `setAuth(result)` 存 token |
| 类型定义 | [`lobby/src/types/user/auth.d.ts`](lobby/src/types/user/auth.d.ts)(目前只有 token 字段,**无** `is_new_user` |
| Console API 范例 | [`lobby/src/api/console/console.ts`](lobby/src/api/console/console.ts) → `/slot-console/api/...` |
| 注册入口(需统一改) | [`telregisterbox.vue`](lobby/src/pages/sign-in/components/telregisterbox.vue)、[`emailregisterbox.vue`](lobby/src/pages/sign-in/components/emailregisterbox.vue)、[`login/index.vue`](lobby/src/pages/login/index.vue)、[`auth/telegram-callback/index.vue`](lobby/src/pages/auth/telegram-callback/index.vue)、[`register/index.vue`](lobby/src/pages/register/index.vue) |
典型流程(以手机注册为例):
```text
userAuthService.register(...) → setAuth → initializeUserSession() → router.push('/')
```
弹窗现有模式可参考 WS `Notify.popup`[`notifyPopupPayload.ts`](lobby/src/utils/notifyPopupPayload.ts) + `DailySignInBox.vue`),但 onboarding **不走 WS**,注册成功后 **HTTP 主动拉取**
### lobby 改动点
**1. 类型与 API**
- `AuthResponse` 增加 `is_new_user?: boolean``uid?: number`
- 新建 `lobby/src/types/console/onboarding.d.ts`(对齐需求 §6.1 payload
- 新建 `lobby/src/api/console/onboarding.api.ts`
```ts
GET /slot-console/api/onboarding/recommend-game
```
**2. 注册编排(集中在一处,避免各页面重复)**
扩展 [`auth.service.ts`](lobby/src/services/user/auth.service.ts)
```text
register(data):
1. result = registerAPI(data)
2. setAuth(result)
3. if result.is_new_user === true:
onboarding = await fetchRecommendGame() // 已 setAuthrequest 拦截器会自动带 Authorization
return { ...result, onboarding }
4. return result
```
recommend-game 失败时 **不阻断注册**`onboarding.enabled = false`,正常进首页。
**3. 弹窗 UI**
新建组件如 `lobby/src/components/OnboardingGamePopup.vue`(或 `pages/home/components/`
- `enabled=true` 时展示:玩家编号、`trial_bonus_amount`、游戏封面、`countdown_seconds` 倒计时
- LET'S PLAY → 用 `game.id` 走现有进游戏链路([`game.api.ts`](lobby/src/api/game/game.api.ts) / 首页 launch 逻辑)
- 关闭 → 仅关弹窗,不调后端
**4. 触发时机**
在注册成功且 `is_new_user` 的分支里,**跳转首页前或首页 onMounted** 展示弹窗(推荐:注册页拿到 onboarding 后 `router.push('/')` + 通过 pinia/commonStore 或 route state 传给首页展示,避免路由切换丢状态)。
各注册入口最终都应走 `userAuthService.register`,无需每个页面单独写 recommend 逻辑。
### 前端调用链路lobby
```text
1. lobby → POST /slot-user/api/user/register
2. is_new_user === true
lobby → GET /slot-console/api/onboarding/recommend-game
enabled=true → OnboardingGamePopup
3. is_new_user === false → 正常登录,无弹窗
4. initializeUserSession() → 进大厅首页 /
```
---
## Part 4文档更新
更新 [`03_console_recommend_game_bff.md`](docs/requirements/register_popup_game_entry/03_console_recommend_game_bff.md) 与父文档 §3.1:明确前端为 **lobby PWA**,注册后条件请求 recommend-game。
**注意**:后端不记录弹窗状态;刷新后是否再弹由 lobby 本地逻辑决定(需求 §8.3)。
---
## Part 5不在本次范围
- user 不调 center / console innerapi按你确认的方案
- 不新增 `user_onboarding_popup`
- 不实现 `mark_shown` / `mark_entered`
- login 接口不加 `is_new_user`(非必须)
---
## 验收要点
**后端**
1. 新手机号/新 IMEI 注册 → `is_new_user=true`;同账号再次 register → `is_new_user=false`
2. `is_new_user=true` 时 recommend-game 返回渠道游戏池随机一款(或 DEFAULT 回退)
3. center 无配置 / 调用失败 → `enabled=false`,注册仍成功
4. `game_codes` 含下架游戏 → 跳过,从剩余随机
**lobby 前端**
5. 老用户走 register 登录回退(`is_new_user=false`)→ 不请求 recommend-game无弹窗
6. 新用户注册成功 → 自动请求 recommend-game`enabled=true` 时展示 onboarding 弹窗
7. 点击 LET'S PLAY → 进入推荐游戏;点击关闭 → 留在大厅,不影响 Trial Balance
8. 各注册入口(手机/邮箱/Telegram 等)行为一致(均经 `userAuthService.register`