hyapp-server/docs/App入口状态机.md

388 lines
14 KiB
Markdown
Raw Permalink 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.

# 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 过期时优先 refreshrefresh 失败才回登录页。
- 主界面首屏只调用轻量一级接口:房间、消息、我的。
- 腾讯云 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` | 默认 hotnew 用同一接口 |
| 消息 | `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 绑定/解绑 RPCgateway 已暴露 `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 只返回配置摘要和版本,不返回大列表。