388 lines
14 KiB
Markdown
388 lines
14 KiB
Markdown
# App Entry State Machine
|
||
|
||
本文定义用户从打开 App 到进入主界面的完整状态机。它只描述 App 入口流程、接口调用顺序、跳转条件和异常恢复边界;登录注册细节见 [Login And Registration Technical Design](./auth-login-register-technical-design.md),邀请码细节见 [Registration Invite Code Architecture](./registration-invite-code-architecture.md),房间重连细节见 [Room Entry Reconnect Development Requirements](./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 只返回配置摘要和版本,不返回大列表。
|