--- todos: - id: "user-is-new-user" content: "slot-user:AbstractRegisterService 增加 isNewUser 标志,Mobile/Imei/Name RegisterService 赋值,UserController::register 返回 is_new_user" status: pending - id: "console-ggame-query" content: "slot-console:GGame 新增按 game_code 查可展示单条游戏方法" status: pending - id: "console-recommend-logic" content: "slot-console:OnboardingRecommendLogic(center 取池 → 随机 → g_game 组装 → 降级)" status: pending - id: "console-recommend-api" content: "slot-console:OnboardingController + 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: "lobby:AuthResponse 增加 is_new_user;auth.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 1:slot-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 2:slot-console 实现 recommend-game BFF(03 子需求) **涉及仓库**:[`/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 3:lobby 前端接入(游戏大厅 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() // 已 setAuth,request 拦截器会自动带 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`)