317 lines
11 KiB
Markdown
317 lines
11 KiB
Markdown
# My Page Overview API
|
||
|
||
本文定义 App 底部 `我的` 页首屏聚合接口。该接口只返回首屏渲染需要的轻量摘要:用户资料、我的邀请码、核心资产余额、身份开关、消息红点和入口显隐。流水、成员列表、背包完整列表、工资明细、邀请列表和团队统计必须放到二级页接口。
|
||
|
||
## Goals
|
||
|
||
- `我的` 页首屏一次请求拿到稳定摘要,减少 App 冷启动后的接口数量。
|
||
- 聚合已有轻量能力:`/users/me`、`/wallet/me/balances`、`/users/me/host-identity`、`/messages/tabs`。
|
||
- 避免实时 `COUNT(*)`、扫流水、扫成员、扫背包、现算工资。
|
||
- 后端控制入口显隐,App 不硬编码复杂身份组合。
|
||
- 所有数据按 `app_code` 和当前 JWT `user_id` 隔离。
|
||
|
||
## Endpoint
|
||
|
||
```http
|
||
GET /api/v1/users/me/overview
|
||
Authorization: Bearer <access_token>
|
||
```
|
||
|
||
响应仍使用 gateway `/api/v1` envelope:
|
||
|
||
```json
|
||
{
|
||
"code": "OK",
|
||
"message": "ok",
|
||
"request_id": "req_01",
|
||
"data": {
|
||
"profile": {},
|
||
"wallet": {},
|
||
"invite": {},
|
||
"roles": {},
|
||
"badges": {},
|
||
"entries": [],
|
||
"server_time_ms": 1777996800000
|
||
}
|
||
}
|
||
```
|
||
|
||
## Response Shape
|
||
|
||
```json
|
||
{
|
||
"profile": {
|
||
"user_id": "10001",
|
||
"display_user_id": "163000",
|
||
"default_display_user_id": "163000",
|
||
"display_user_id_kind": "default",
|
||
"display_user_id_expires_at_ms": 0,
|
||
"username": "Alice",
|
||
"avatar": "https://cdn.example.com/avatar.png",
|
||
"gender": "female",
|
||
"birth": "2000-01-01",
|
||
"country": "US",
|
||
"country_id": 1,
|
||
"country_name": "United States",
|
||
"country_display_name": "United States",
|
||
"region_id": 10,
|
||
"region_code": "north_america",
|
||
"region_name": "North America",
|
||
"language": "en",
|
||
"profile_completed": true,
|
||
"onboarding_status": "completed"
|
||
},
|
||
"wallet": {
|
||
"balances": [
|
||
{
|
||
"asset_type": "COIN",
|
||
"available_amount": 12000,
|
||
"frozen_amount": 0,
|
||
"version": 8
|
||
},
|
||
{
|
||
"asset_type": "DIAMOND",
|
||
"available_amount": 300,
|
||
"frozen_amount": 0,
|
||
"version": 3
|
||
},
|
||
{
|
||
"asset_type": "USD_BALANCE",
|
||
"available_amount": 2500,
|
||
"frozen_amount": 500,
|
||
"version": 2
|
||
}
|
||
],
|
||
"recharge_enabled": true,
|
||
"diamond_exchange_enabled": true,
|
||
"withdraw_enabled": true
|
||
},
|
||
"invite": {
|
||
"my_invite_code": "A1B2C3",
|
||
"invite_enabled": true,
|
||
"invite_count": 12,
|
||
"valid_invite_count": 3,
|
||
"valid_invite_threshold_coin": 80000
|
||
},
|
||
"roles": {
|
||
"is_host": true,
|
||
"is_agent": false,
|
||
"is_bd": false,
|
||
"is_bd_leader": false,
|
||
"is_coin_seller": false
|
||
},
|
||
"badges": {
|
||
"unread_system_messages": 2,
|
||
"unread_activity_messages": 1,
|
||
"pending_role_invitations": 0
|
||
},
|
||
"entries": [
|
||
{
|
||
"key": "wallet",
|
||
"title": "Wallet",
|
||
"visible": true,
|
||
"badge_count": 0
|
||
},
|
||
{
|
||
"key": "host_center",
|
||
"title": "Host Center",
|
||
"visible": true,
|
||
"badge_count": 0
|
||
},
|
||
{
|
||
"key": "coin_seller_center",
|
||
"title": "Coin Seller",
|
||
"visible": false,
|
||
"badge_count": 0
|
||
},
|
||
{
|
||
"key": "backpack",
|
||
"title": "Backpack",
|
||
"visible": true,
|
||
"badge_count": 0
|
||
},
|
||
{
|
||
"key": "settings",
|
||
"title": "Settings",
|
||
"visible": true,
|
||
"badge_count": 0
|
||
}
|
||
],
|
||
"server_time_ms": 1777996800000
|
||
}
|
||
```
|
||
|
||
## Field Sources
|
||
|
||
| Field | Source | Query Pattern |
|
||
| --- | --- | --- |
|
||
| `profile` | `user-service.GetUser` | single row by `(app_code, user_id)` |
|
||
| `wallet.balances` | `wallet-service.GetBalances` | point read for `COIN`、`DIAMOND`、`USD_BALANCE` |
|
||
| `invite.my_invite_code` | `user-service` invite code module | point read active primary code by `(app_code, user_id)` |
|
||
| `invite.invite_count` | invite read model | users that successfully filled this invite code; without read model return `0` or omit |
|
||
| `invite.valid_invite_count` | invite read model | invited users whose cumulative eligible recharge reaches the configured threshold |
|
||
| `invite.valid_invite_threshold_coin` | invite validity policy | current configured threshold, default `80000` `COIN` |
|
||
| `roles` | `user-service` host identity module | point read role facts or future role summary read model |
|
||
| `badges.unread_*` | message inbox owner | counter/read model, not message list scan |
|
||
| `entries` | gateway composition rules | computed from `roles` and feature flags |
|
||
| `server_time_ms` | gateway | current server clock |
|
||
|
||
当前实现已有 `/users/me`、`/wallet/me/balances`、`/users/me/host-identity`、`/messages/tabs`。`/users/me/overview` 可以先在 `gateway-service` 聚合这些能力;后续如果角色判断的三次 not-found RPC 变慢,再在 `user-service` 增加 `GetUserRoleSummary` 内部 RPC。
|
||
|
||
## Asset Rules
|
||
|
||
首屏只返回三类资产:
|
||
|
||
| Asset | Show Rule |
|
||
| --- | --- |
|
||
| `COIN` | 所有用户展示 |
|
||
| `DIAMOND` | 所有用户展示,但文案不能暗示可直接消费 |
|
||
| `USD_BALANCE` | 有余额、冻结金额、host/agent/bd/coin_seller 身份时展示 |
|
||
|
||
不要返回 `GIFT_POINT`。积分是后台工资/活动计算输入,不是普通用户钱包可直接操作资产。
|
||
|
||
## Entry Rules
|
||
|
||
入口显隐由后端返回,App 只负责渲染。
|
||
|
||
| Entry Key | Visible Rule |
|
||
| --- | --- |
|
||
| `wallet` | always true |
|
||
| `host_center` | `roles.is_host == true` |
|
||
| `apply_host` | `roles.is_host == false` |
|
||
| `agency_center` | `roles.is_agent == true` |
|
||
| `bd_center` | `roles.is_bd == true` |
|
||
| `bd_leader_center` | `roles.is_bd_leader == true` |
|
||
| `coin_seller_center` | `roles.is_coin_seller == true` |
|
||
| `backpack` | always true |
|
||
| `settings` | always true |
|
||
| `support` | always true |
|
||
|
||
同一个用户可能同时是 BD 和 host。不要用互斥枚举表达角色,必须使用 bool flags。
|
||
|
||
## Expensive Fields To Exclude
|
||
|
||
不要在 `overview` 中实时查询这些字段:
|
||
|
||
| Field | Reason | Replacement |
|
||
| --- | --- | --- |
|
||
| 邀请用户列表 | 需要分页,可能扫 `user_invite_relations` | 二级页 `GET /api/v1/invites/users` |
|
||
| 实时邀请人数 `COUNT(*)` | 高活跃邀请人会让首页变慢 | read model counter 或返回 `0` |
|
||
| 邀请奖励流水 | 属于钱包/活动明细 | 二级页分页 |
|
||
|
||
这些字段不要放进首屏 overview:
|
||
|
||
| Field | Reason | Replacement |
|
||
| --- | --- | --- |
|
||
| `period_mic_minutes` | 需要统计聚合,可能扫 stats | Host Center 二级页 |
|
||
| `period_gift_points` | 需要统计聚合,可能跨日汇总 | Host Center 二级页 |
|
||
| `estimated_earning_usd_minor` | 不能首屏现算工资 | 后台工资 read model 或收益页 |
|
||
| `agency_member_count` | 容易 `COUNT(*)` members | Agency Center 二级页或 summary table |
|
||
| `bd_host_count` | 团队规模统计昂贵 | BD Center 二级页或 summary table |
|
||
| `pending_withdrawals` | 扫提现/流水表 | wallet badge counter |
|
||
| `recharge_count` | 扫充值记录 | wallet 二级页 |
|
||
| `resource_count` | 扫背包权益表 | backpack 二级页 |
|
||
| `latest_transactions` | 扫交易流水 | wallet 二级页 |
|
||
| `inbox_items` | 扫消息列表 | messages 二级页 |
|
||
|
||
首屏接口禁止为了红点实时扫大表。需要红点就维护 counter/read model;没有 counter 时先不展示该红点。
|
||
|
||
## Performance Budget
|
||
|
||
目标:
|
||
|
||
- P95 backend latency: `< 120ms` in normal region deployment.
|
||
- Gateway fanout RPC count: max 4.
|
||
- No list scan.
|
||
- No transaction history query.
|
||
- No member `COUNT(*)`.
|
||
- No resource entitlement list.
|
||
- No salary calculation.
|
||
|
||
推荐并行 fanout:
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant C as Client
|
||
participant G as gateway-service
|
||
participant U as user-service
|
||
participant W as wallet-service
|
||
participant M as message inbox owner
|
||
|
||
C->>G: GET /api/v1/users/me/overview
|
||
par profile and roles
|
||
G->>U: GetUser(user_id)
|
||
G->>U: GetUserRoleSummary(user_id)
|
||
and wallet
|
||
G->>W: GetBalances(COIN, DIAMOND, USD_BALANCE)
|
||
and badges
|
||
G->>M: ListMessageTabs(user_id)
|
||
end
|
||
G->>G: compose entries and feature flags
|
||
G-->>C: overview
|
||
```
|
||
|
||
MVP 可以复用已有 `GetHostProfile`、`GetBDProfile`、`GetCoinSellerProfile` 聚合角色;优化版应增加单个 `GetUserRoleSummary`,减少 not-found RPC 和错误映射成本。
|
||
|
||
## Degraded Responses
|
||
|
||
首屏接口可以部分降级,但必须清楚标记。
|
||
|
||
| Upstream Failure | Response Rule |
|
||
| --- | --- |
|
||
| `user-service.GetUser` fail | 整体失败,资料是首屏主数据 |
|
||
| `wallet-service.GetBalances` fail | 返回空 balances,并加 `wallet.unavailable=true` |
|
||
| role summary fail | 返回 all false,并加 `roles.unavailable=true`,不要误展示特权入口 |
|
||
| message tabs fail | unread 置 0,并加 `badges.unavailable=true` |
|
||
|
||
如果某个上游超时,gateway 不应无限等待。overview 是首屏接口,应该有比普通操作接口更短的聚合超时预算。
|
||
|
||
## HTTP Handler Shape
|
||
|
||
新增路由:
|
||
|
||
```go
|
||
mux.Handle(apiV1Prefix+"/users/me/overview", h.profileAPIHandler(jwtVerifier, h.getMyOverview))
|
||
```
|
||
|
||
handler 只做协议聚合:
|
||
|
||
1. 从 JWT context 取 `user_id` 和 `app_code`。
|
||
2. 并行请求 profile、balances、roles、message tabs。
|
||
3. 把资产补齐为固定三类,缺失资产返回 0。
|
||
4. 根据 roles 和 feature flags 生成 entries。
|
||
5. 返回 envelope data。
|
||
|
||
不要在 gateway 写 SQL,也不要在 gateway 做工资、成员、资源统计。
|
||
|
||
## Verification Matrix
|
||
|
||
| Scenario | Expected |
|
||
| --- | --- |
|
||
| 普通用户打开我的页 | 返回 profile、COIN/DIAMOND/USD_BALANCE、普通入口 |
|
||
| host 打开我的页 | `roles.is_host=true`,展示 `host_center` |
|
||
| BD Leader 打开我的页 | `is_bd=true` 且 `is_bd_leader=true`,展示 leader 入口 |
|
||
| 币商打开我的页 | `is_coin_seller=true`,展示 coin seller 入口 |
|
||
| 钱包没有账户行 | 三类资产都返回 0 投影 |
|
||
| 消息服务失败 | 我的页仍可展示,消息红点降级为 0 |
|
||
| 用户资料服务失败 | overview 整体失败 |
|
||
| 大量成员/流水存在 | overview 延迟不受影响,因为不查成员和流水 |
|
||
|
||
## Implementation Order
|
||
|
||
1. 新增本文档,确认字段边界和慢查询排除规则。
|
||
2. 在 `gateway-service` 增加 `GET /api/v1/users/me/overview` handler。
|
||
3. 复用现有 `GetUser`、`GetBalances`、`GetHostProfile/GetBDProfile/GetCoinSellerProfile`、`ListMessageTabs`。
|
||
4. 增加 handler 单元测试,覆盖普通用户、host、BD Leader、币商、钱包缺失、消息降级。
|
||
5. 如果角色 RPC 成本偏高,再在 `user-service` 增加 `GetUserRoleSummary`。
|
||
6. 如果红点需要更多类型,先补 counter/read model,再把 badge 加进 overview。
|
||
|
||
## Critical Rules
|
||
|
||
- `我的` 页 overview 是轻聚合接口,不是用户全量详情接口。
|
||
- 首屏不做 `COUNT(*)`、不扫流水、不扫成员、不查完整背包、不现算工资。
|
||
- 钱包余额以 `wallet-service` 为准。
|
||
- 用户资料以 `user-service` 为准。
|
||
- 角色入口以服务端返回的 `roles` 和 `entries` 为准。
|
||
- 消息红点以 message inbox counter/read model 为准,用户私聊未读仍以腾讯云 IM SDK 为准。
|