# 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 只返回配置摘要和版本，不返回大列表。
