# App Entry State Machine 本文定义用户从打开 App 到进入主界面的完整状态机。它只描述 App 入口流程、接口调用顺序、跳转条件和异常恢复边界;登录注册细节见 [Login And Registration Technical Design](./登录注册技术设计.md),邀请码细节见 [Registration Invite Code Architecture](./注册邀请码架构.md),房间重连细节见 [Room Entry Reconnect Development Requirements](./进房与重连开发需求.md)。 ## Goals - 客户端启动时用确定的状态机决定去登录页、资料补全页、主界面或维护/升级页。 - 未完成资料的用户不能进入房间、IM、RTC、钱包和付费流程。 - access token 过期时优先 refresh,refresh 失败才回登录页。 - 主界面首屏只调用轻量一级接口:房间、消息、我的。 - 腾讯云 IM 登录必须在 `profile_completed=true` 后发生,避免未完成资料用户进入实时系统。 - 房间恢复必须独立于房间列表,不能靠列表猜测用户是否还在某个房间。 ## Current Interface Inventory | Capability | Endpoint | Status | Rule | | --- | --- | --- | --- | | 三方登录即注册 | `POST /api/v1/auth/third-party/login` | DONE | 返回 `is_new_user/profile_completed/onboarding_status` | | refresh token | `POST /api/v1/auth/token/refresh` | DONE | 成功后轮换 refresh token | | 资料补全 | `POST /api/v1/users/me/onboarding/complete` | DONE | 成功后返回新的 access token,不轮换 refresh token;可选提交 `invite_code` | | 注册国家 | `GET /api/v1/countries` | DONE | 登录前可用 | | 我的页首屏 | `GET /api/v1/users/me/overview` | DONE | 主界面轻聚合 | | 房间列表 | `GET /api/v1/rooms?tab=hot/new` | DONE | 需要 completed profile | | 消息 tab | `GET /api/v1/messages/tabs` | DONE | 需要 completed profile | | 腾讯 IM UserSig | `GET /api/v1/im/usersig` | DONE | 需要 completed profile | | App 启动配置 | `GET /api/v1/app/bootstrap` | DONE | 版本、维护、feature flags、配置版本 | | 当前房间恢复 | `GET /api/v1/rooms/current` | DONE | 判断是否有可恢复房间 | | 推送 token 绑定 | `POST /api/v1/devices/push-token` / `DELETE /api/v1/devices/push-token` | DONE | 离线消息触达 | ## Top Level State Machine ```mermaid stateDiagram-v2 [*] --> Launch Launch --> Bootstrap Bootstrap --> ForceUpgrade Bootstrap --> Maintenance Bootstrap --> TokenCheck TokenCheck --> LoginRequired: no local token TokenCheck --> AuthenticatedProbe: access token exists AuthenticatedProbe --> MainReady: overview ok and profile completed AuthenticatedProbe --> Refreshing: access token expired or 401 AuthenticatedProbe --> ProfileRequired: PROFILE_REQUIRED or token says profile incomplete Refreshing --> AuthenticatedProbe: refresh ok Refreshing --> LoginRequired: refresh failed LoginRequired --> LoginOrRegister LoginOrRegister --> ProfileRequired: login ok but profile incomplete LoginOrRegister --> MainReady: login ok and profile completed ProfileRequired --> CompleteOnboarding CompleteOnboarding --> MainReady: success with replacement access token MainReady --> MainTabs MainTabs --> IMLogin MainTabs --> RoomRestoreProbe ``` ## Launch Sequence 启动顺序: 1. App 启动,生成或读取 stable `device_id`。 2. 调 `GET /api/v1/app/bootstrap`,以 bootstrap 返回的版本、维护、feature flags 和配置版本作为入口决策事实。 3. 如果 bootstrap 返回强更,进入强更页。 4. 如果 bootstrap 返回维护中,进入维护页。 5. 检查本地是否有 access token。 6. 没有 token,进入登录页。 7. 有 token,调用 `GET /api/v1/users/me/overview` 做认证探测和首屏数据读取。 不要只靠本地缓存判断是否进入主界面。用户可能被禁用、token 可能过期、资料完成状态可能已经变化。 ## Bootstrap Contract 当前接口: ```http GET /api/v1/app/bootstrap ``` 登录前也可调用。请求必须携带 App 解析头: | Header | Meaning | | --- | --- | | `X-App-Code` / `X-HY-App-Code` | 已知内部 `app_code` | | `X-App-Package` / `X-Package-Name` | 包名或 Bundle ID | | `X-App-Platform` / `X-Platform` | `android` or `ios` | | `X-App-Version` | 客户端版本 | | `X-Build-Number` | 构建号 | 响应字段: ```json { "app_code": "lalu", "server_time_ms": 1777996800000, "force_upgrade": false, "maintenance": false, "minimum_version": "1.0.0", "latest_version": "1.2.0", "feature_flags": { "wallet_recharge": true, "wallet_withdraw": true, "diamond_exchange": true, "message_tab": true, "host_center": true, "room_create": true }, "bottom_tabs": [ {"key": "rooms", "title": "房间", "enabled": true}, {"key": "messages", "title": "消息", "enabled": true}, {"key": "me", "title": "我的", "enabled": true} ], "config_versions": { "countries": "v1", "banners": "v1", "h5_links": "v1", "resources": "v1", "gifts": "v1" } } ``` bootstrap 只返回配置摘要和版本号,不返回完整礼物、背包资源或 banner 列表。客户端发现版本变化后再拉对应配置接口。 ## Authentication Probe 有 access token 时,首选调用: ```http GET /api/v1/users/me/overview ``` 结果处理: | Result | Client Action | | --- | --- | | `200 OK` | 使用 overview 进入主界面 | | `401 AUTH_REQUIRED` / token expired | 调 refresh | | `403 PROFILE_REQUIRED` | 进入资料补全页 | | `403 USER_DISABLED/BANNED` | 进入账号异常页 | | `5xx/502` | 展示重试页,不清 token | `overview` 失败时不要直接清空登录态。只有 refresh 明确失败、session revoked 或用户主动 logout 才清本地 token。 ## Refresh State ```http POST /api/v1/auth/token/refresh ``` 请求必须带: - `refresh_token` - `device_id` 处理规则: | Result | Client Action | | --- | --- | | refresh ok | 替换 access token 和 refresh token,重试 `overview` | | `SESSION_EXPIRED` | 清 token,进登录页 | | `SESSION_REVOKED` | 清 token,进登录页 | | `AUTH_FAILED` | 清 token,进登录页 | | network/5xx | 不清 token,展示重试 | refresh 成功会轮换 refresh token。客户端必须原子保存新 access token 和新 refresh token,避免只保存一半导致下次启动失效。 ## Login State 三方登录: ```http POST /api/v1/auth/third-party/login ``` 状态跳转: | Response | Client Action | | --- | --- | | `is_new_user=true` | 进入资料补全页 | | `profile_completed=false` | 进入资料补全页 | | `profile_completed=true` | 进入主界面 | 服务端不会返回 `NOT_REGISTERED`。三方身份不存在时,该接口原子创建用户、三方绑定、默认短号和 session。 ## Profile Required State 资料补全页需要先拉: ```http GET /api/v1/countries ``` 提交: ```http POST /api/v1/users/me/onboarding/complete ``` 请求体包含 `username/avatar/gender/country`,可以选填 `invite_code`。如果用户填写邀请码但服务端无法解析到有效邀请人,返回 `INVALID_INVITE_CODE`,客户端停留在资料补全页让用户修改或清空邀请码。 成功后: - 替换响应里的新 access token。 - 保留原 refresh token;该接口不轮换 refresh token。 - 重新调用 `GET /api/v1/users/me/overview`。 - overview 成功后进入主界面。 未完成资料的 access token 会在 gateway profile gate 被拒绝。客户端不能绕过资料补全去请求房间、IM、RTC、钱包和付费接口。 ## Main Tabs Entry 进入主界面后,底部三个一级页使用这些接口: | Tab | Primary Endpoint | Notes | | --- | --- | --- | | 房间 | `GET /api/v1/rooms?tab=hot&limit=20` | 默认 hot;new 用同一接口 | | 消息 | `GET /api/v1/messages/tabs` | 用户私聊列表走腾讯云 IM SDK | | 我的 | `GET /api/v1/users/me/overview` | 轻聚合,不查流水、成员、工资 | 主界面首屏可以并行: ```mermaid sequenceDiagram participant C as Client participant G as gateway-service participant TIM as Tencent IM par room tab C->>G: GET /api/v1/rooms?tab=hot and message tab C->>G: GET /api/v1/messages/tabs and me tab C->>G: GET /api/v1/users/me/overview and IM C->>G: GET /api/v1/im/usersig C->>TIM: SDK login end ``` 如果当前默认落在“房间” tab,可以优先请求房间列表,消息和我的延后或并行低优先级请求。 ## IM Login State 腾讯云 IM 登录必须发生在 `profile_completed=true` 后。 ```http GET /api/v1/im/usersig ``` 规则: - UserSig 只用于腾讯云 IM SDK 登录。 - 用户私聊会话列表由 SDK 拉取。 - 系统/活动消息由 `/api/v1/messages/tabs` 和 `/api/v1/messages` 拉取。 - UserSig 过期时重新调用该接口,不使用 refresh token。 未完成资料用户请求 `/api/v1/im/usersig` 必须返回 `PROFILE_REQUIRED`。 ## Room Restore State 当前接口: ```http GET /api/v1/rooms/current ``` 用途:App 从后台回前台、进程重启、网络恢复时,判断是否有需要恢复的房间。不能通过房间列表猜测当前房间,因为列表是发现页,不表达当前用户 presence。 响应字段: ```json { "has_current_room": true, "room_id": "lalu_room_01", "room_version": 12, "role": "audience", "mic_session_id": "", "publish_state": "", "need_join_im_group": true, "need_rtc_token": false, "server_time_ms": 1777996800000 } ``` 恢复规则: | State | Client Action | | --- | --- | | `has_current_room=false` | 不展示恢复入口 | | `audience` | 重新登录 IM 并 join group;不申请 RTC token | | `on mic pending/publishing` | 调 `/api/v1/rtc/token`,恢复 RTC;随后按麦位状态确认发流 | | room closed / kicked / banned | 清本地房间态,展示原因或静默回房间列表 | `rooms/current` 由 room-service 的用户 presence 读模型和 Room Cell 快照二次校验共同决定。该接口只做恢复探测,不写命令日志、不刷新心跳、不隐式 JoinRoom。 ## Push Token State 当前接口: ```http POST /api/v1/devices/push-token DELETE /api/v1/devices/push-token ``` 绑定时机: - 资料完成后。 - 用户允许系统通知后。 - token 变化、App 重装、语言/时区变化时重新上报。 该能力服务系统/活动消息离线触达,不影响 App 能否进入主界面。 ## Error Handling Rules | Error | Meaning | Client Action | | --- | --- | --- | | `AUTH_REQUIRED` | access token 缺失或无效 | 有 refresh 则 refresh;否则登录 | | `SESSION_EXPIRED` | refresh session 过期 | 清 token,登录 | | `SESSION_REVOKED` | refresh session 已吊销 | 清 token,登录 | | `PROFILE_REQUIRED` | 已登录但资料未完成 | 资料补全 | | `INVALID_INVITE_CODE` | 注册页邀请码无效 | 停留资料补全页,提示修改或清空邀请码 | | `USER_DISABLED` | 用户被禁用 | 账号异常页 | | `USER_BANNED` | 用户被封禁 | 账号异常页 | | `UPSTREAM_ERROR` | 上游不可用 | 重试,不清 token | | network timeout | 网络异常 | 重试,不清 token | 不要因为普通 5xx、网络失败或 bootstrap 失败清空登录态。 ## Client Local State 客户端本地至少保存: - `access_token` - `refresh_token` - `session_id` - `user_id` - `app_code` - `profile_completed` - `onboarding_status` - `device_id` - `last_room_id`,只做恢复提示,不能作为 presence 事实 - config version cache 服务端事实优先级高于本地缓存。只要服务端返回 profile required、disabled、banned、session revoked,客户端必须更新本地状态。 ## Implementation Status 1. 服务端已固定 App 入口状态机文档,客户端入口必须按本文状态转移。 2. gateway 已实现 `GET /api/v1/app/bootstrap`,返回版本、维护、feature flags、bottom tabs 和配置版本。 3. gateway 已实现 `GET /api/v1/rooms/current`,只用 token user_id 做当前用户房间恢复探测,不修改房间状态。 4. user-service 已实现 push token 绑定/解绑 RPC,gateway 已暴露 `POST/DELETE /api/v1/devices/push-token`。 5. App 端下一步按本文状态机接入:bootstrap -> token probe -> refresh/login/onboarding -> main tabs -> IM login -> room restore。 6. 端到端 smoke 必须覆盖:新用户注册、老用户启动、access token 过期 refresh、profile required、IM login、房间恢复。 ## Verification Matrix | Scenario | Expected | | --- | --- | | 首次安装无 token | bootstrap 后进入登录页 | | 新用户三方登录 | 创建用户,进入资料补全页 | | 新用户资料补全填写有效邀请码 | 注册完成,邀请关系绑定到邀请码 owner | | 新用户资料补全填写无效邀请码 | 返回 `INVALID_INVITE_CODE`,仍在资料补全页 | | 资料补全成功 | 替换 access token,进入主界面 | | access token 过期 | refresh 成功后重试 overview | | refresh token 过期 | 清 token,进入登录页 | | 未完成资料请求房间列表 | 返回 `PROFILE_REQUIRED` | | 未完成资料请求 IM UserSig | 返回 `PROFILE_REQUIRED` | | 主界面打开房间 tab | 拉 `GET /api/v1/rooms?tab=hot` | | 主界面打开消息 tab | 拉 `GET /api/v1/messages/tabs`,用户私聊走 IM SDK | | 主界面打开我的 tab | 拉 `GET /api/v1/users/me/overview` | | App 回前台且有房间 presence | `rooms/current` 返回恢复信息 | | 普通 5xx 或网络失败 | 展示重试,不清本地 token | ## Critical Rules - 资料未完成不能进入实时系统、房间、钱包和付费流程。 - `request_id` 只做链路追踪,不做幂等键。 - 登录和 refresh 响应里的 token 必须原子保存。 - CompleteOnboarding 只替换 access token,不轮换 refresh token。 - IM 登录在主资料完成之后,用户私聊状态以腾讯云 IM SDK 为准。 - 房间恢复用 `rooms/current`,不要用房间列表代替。 - bootstrap 只返回配置摘要和版本,不返回大列表。