hyapp-server/docs/user-id-technical-design.md
2026-05-02 13:02:38 +08:00

777 lines
27 KiB
Markdown
Raw 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.

# User ID Technical Design
本文档定义数值型用户主键、默认短号、临时靓号的职责边界、数据库模型、跨服务通信规则和修改流程。核心原则:`user_id` 是唯一用户主键,后端和客户端响应都使用它;`display_user_id` 只做展示、搜索、登录名和运营入口。
## Terminology
| Field | Meaning | Mutable | Unique | Used By Internal Services |
| --- | --- | --- | --- | --- |
| `user_id` | 数值型用户 ID服务间主关联和客户端响应统一使用 | No | Yes | Yes |
| `default_display_user_id` | 用户本身的默认短号 | Yes by explicit change | Yes while owned | No |
| `current_display_user_id` | 当前对外展示、搜索、登录的短号 | Yes | Yes while effective | No |
| `pretty_display_user_id` | 用户申请的临时靓号 | Yes by lease | Yes while leased | No |
| `display_user_id` | API 中返回的当前展示号,等同 `current_display_user_id` | Yes | Yes while effective | No |
| `display_user_id_kind` | 当前展示号来源,`default``pretty` | Yes | No | No |
| `display_user_id_expires_at_ms` | 当前靓号过期时间;默认短号为空 | Yes | No | No |
示例:
```text
user_id: 918274650129384448
default_display_user_id: 163000
current_display_user_id: 888888
display_user_id_kind: pretty
display_user_id_expires_at_ms: 1777000000000
```
`user_id` 是内部关系主键,也可以作为接口响应中的用户 ID。`default_display_user_id` 是用户本身的号。临时靓号有效期内会覆盖当前展示号;过期后必须恢复到 `default_display_user_id`
所有展示号唯一性都限定在 `app_code` 内。不同 App 可以使用同一个 `display_user_id`,但同一个 App 内只能有一个当前有效归属。
## Design Principles
1. HTTP、客户端 JSON 和 JWT claims 中的 `user_id` 一律返回数值型用户 ID 的字符串形式。
2. 跨服务通信一律传数值型 `user_id`
3. 数据库主关联一律存数值型 `user_id`,并用 `app_code` 做租户作用域。
4. outbox、审计、账单、房间状态、IM session、活动记录一律存数值型 `user_id`
5. `display_user_id` 只允许出现在展示、搜索、分享、登录名、客服查询和运营配置入口。
6. 任何通过 `display_user_id` 发起的请求,都必须先在 `user-service` 解析成内部 `user_id`,后续链路只传内部 `user_id`
7. `display_user_id` 可修改,所以不能作为幂等键、外键、账务主体或事件主体。
8. 临时靓号不能修改 `user_id`,也不能删除或转移用户的默认短号。
9. 当前展示号的真实来源必须可解释:默认短号来自永久归属,临时靓号来自有效租约。
10. `ResolveDisplayUserID`、密码登录和用户资料查询都必须先处理过期靓号,再返回当前有效短号。
11. 靓号过期恢复必须事务化,不能只靠缓存 TTL。
## Ownership
`user-service``user_id``display_user_id` 的唯一 owner。
| Capability | Owner | Rule |
| --- | --- | --- |
| 生成 `user_id` | `user-service` | 注册或三方首次登录时生成 |
| 分配默认短号 | `user-service` | 新用户创建时按 App 序列分配,首号 `163000` |
| 修改默认短号 | `user-service` | 事务内检查唯一性和冷却期 |
| 申请临时靓号 | `user-service` | 创建靓号租约并覆盖当前展示号 |
| 过期临时靓号 | `user-service` | 恢复当前展示号为默认短号 |
| 解析 `display_user_id` | `user-service` | 只返回当前有效归属 |
| 房间状态 | `room-service` | 只保存 `user_id` |
| 钱包账务 | `wallet-service` | 只保存 `user_id` |
| IM 连接 | 腾讯云 IM | identifier 使用内部长 `user_id` 的十进制字符串 |
| 活动结算 | `activity-service` | 只保存 `user_id` |
其他服务不能自己缓存长期 `display_user_id -> user_id` 绑定作为事实来源。短期缓存必须有 TTL并允许 user-service 失效或回源。
## Role Identity Facts
用户 ID 和短号不表达业务身份。主播、Agency、BD、BD Leader、coin_seller 都是挂在同一个 `user_id` 下的业务事实,不应该把身份塞进 `display_user_id` 或 JWT 文案里。
| Identity | Table | Can Coexist | Account Boundary |
| --- | --- | --- | --- |
| `host` | `host_profiles` | 可和 BD、BD Leader、coin_seller 并存 | 主播收益余额在 `wallet-service` |
| `agency owner` | `agencies` + owner membership | 可和 BD、BD Leader、coin_seller 并存 | Agency 工资结算后入钱包 |
| `bd` | `bd_profiles(role='bd')` | 可和 host、Agency、coin_seller 并存 | BD 工资结算后入钱包 |
| `bd_leader` | `bd_profiles(role='bd_leader')` | 可和 host、Agency、coin_seller 并存 | BD Leader 工资结算后入钱包 |
| `coin_seller` | `coin_seller_profiles` | 可和 Agency、BD、BD Leader 并存 | 币商专用资产 `COIN_SELLER_COIN` |
`coin_seller` 是币商身份,拥有独立于玩家普通 `COIN` 的币商金币账户。币商给玩家转金币时,内部链路只能使用数值型 `user_id`gateway 校验当前用户是 active `coin_seller`wallet-service 扣减该用户 `COIN_SELLER_COIN` 并给目标用户增加普通 `COIN``display_user_id` 只可用于后台或客户端搜索目标用户,解析后必须转换成内部 `user_id` 再进入账务。
## Database Model
### `users`
用户主表保存不可变主键、默认短号、当前展示号快照,以及注册资料和来源快照。
```sql
CREATE TABLE users (
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
user_id BIGINT NOT NULL PRIMARY KEY,
default_display_user_id VARCHAR(32) NOT NULL,
current_display_user_id VARCHAR(32) NOT NULL,
current_display_user_id_kind VARCHAR(32) NOT NULL,
current_display_user_id_expires_at_ms BIGINT NULL,
username VARCHAR(64) NULL,
gender VARCHAR(32) NULL,
country VARCHAR(64) NULL,
invite_code VARCHAR(64) NULL,
register_ip VARCHAR(64) NULL,
register_user_agent VARCHAR(512) NULL,
country_by_ip VARCHAR(64) NULL,
register_device_id VARCHAR(128) NULL,
register_device VARCHAR(128) NULL,
os_version VARCHAR(64) NULL,
avatar VARCHAR(512) NULL,
birth_date DATE NULL,
app_version VARCHAR(64) NULL,
build_number VARCHAR(64) NULL,
source VARCHAR(64) NULL,
install_channel VARCHAR(64) NULL,
campaign VARCHAR(128) NULL,
platform VARCHAR(16) NULL,
language VARCHAR(32) NULL,
timezone VARCHAR(64) NULL,
status VARCHAR(32) NOT NULL,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
UNIQUE KEY uk_users_default_display_user_id (app_code, default_display_user_id),
UNIQUE KEY uk_users_current_display_user_id (app_code, current_display_user_id)
);
```
`current_display_user_id` 是读优化快照,不是变更历史来源。它必须和 `user_display_user_ids` 的当前有效记录保持一致。
快照规则:
| Case | `current_display_user_id` | `current_display_user_id_kind` | `current_display_user_id_expires_at_ms` |
| --- | --- | --- | --- |
| 没有靓号 | `default_display_user_id` | `default` | `NULL` |
| 靓号有效 | 靓号 | `pretty` | 靓号过期时间 |
| 靓号过期后 | `default_display_user_id` | `default` | `NULL` |
### `user_display_user_ids`
短号归属历史表。默认短号和临时靓号都进入这张表。
```sql
CREATE TABLE user_display_user_ids (
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
id BIGINT NOT NULL PRIMARY KEY,
display_user_id VARCHAR(32) NOT NULL,
user_id BIGINT NOT NULL,
display_user_id_kind VARCHAR(32) NOT NULL,
status VARCHAR(32) NOT NULL,
assigned_at_ms BIGINT NOT NULL,
activated_at_ms BIGINT NULL,
expires_at_ms BIGINT NULL,
released_at_ms BIGINT NULL,
release_reason VARCHAR(64) NULL,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
live_display_user_id VARCHAR(32)
GENERATED ALWAYS AS (
CASE
WHEN status IN ('active', 'held', 'reserved', 'blocked') THEN display_user_id
ELSE NULL
END
) STORED,
active_user_id BIGINT
GENERATED ALWAYS AS (
CASE WHEN status = 'active' THEN user_id ELSE NULL END
) STORED,
UNIQUE KEY uk_user_display_user_ids_live (app_code, live_display_user_id),
UNIQUE KEY uk_user_display_user_ids_active_user (app_code, active_user_id),
KEY idx_user_display_user_ids_user_id (app_code, user_id)
);
```
`status` 建议:
```text
active
held
expired
released
reserved
blocked
```
状态含义:
| Status | Meaning |
| --- | --- |
| `active` | 当前可被普通业务解析的展示号 |
| `held` | 默认短号仍归用户所有,但被临时靓号覆盖,普通业务不解析 |
| `expired` | 临时靓号到期 |
| `released` | 曾经归属用户,已经释放 |
| `reserved` | 系统预留,不允许普通用户申请 |
| `blocked` | 运营阻断,不允许分配 |
生成列用于模拟部分唯一索引:同一个短号不能同时处于 `active``held``reserved``blocked`;同一个用户只能有一个 `active` 当前展示号。历史 `expired``released` 记录可以保留多条。
### `pretty_display_user_id_leases`
靓号租约表保存临时覆盖关系。它是“过期恢复默认短号”的事实来源。
```sql
CREATE TABLE pretty_display_user_id_leases (
lease_id VARCHAR(64) NOT NULL PRIMARY KEY,
display_user_id VARCHAR(32) NOT NULL,
user_id BIGINT NOT NULL,
status VARCHAR(32) NOT NULL,
starts_at_ms BIGINT NOT NULL,
expires_at_ms BIGINT NOT NULL,
payment_receipt_id VARCHAR(64) NULL,
source VARCHAR(32) NOT NULL,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
active_display_user_id VARCHAR(32)
GENERATED ALWAYS AS (
CASE WHEN status = 'active' THEN display_user_id ELSE NULL END
) STORED,
active_user_id BIGINT
GENERATED ALWAYS AS (
CASE WHEN status = 'active' THEN user_id ELSE NULL END
) STORED,
UNIQUE KEY uk_pretty_display_user_id_active (active_display_user_id),
UNIQUE KEY uk_pretty_display_user_id_active_user (active_user_id),
KEY idx_pretty_display_user_id_leases_user (user_id, status),
KEY idx_pretty_display_user_id_leases_expire (status, expires_at_ms)
);
```
`status` 建议:
```text
active
expired
cancelled
refunded
```
首版规则:
- 一个用户同一时间只能有一个 active 靓号租约。
- 一个靓号同一时间只能租给一个用户。
- 申请新靓号不会改变 `default_display_user_id`
- 靓号有效期内,默认短号处于 `held`,不能被其他用户申请。
- 靓号过期后,必须把 `users.current_display_user_id` 恢复为 `users.default_display_user_id`
### `display_user_id_change_logs`
短号修改审计表。
```sql
CREATE TABLE display_user_id_change_logs (
id BIGINT NOT NULL PRIMARY KEY,
user_id BIGINT NOT NULL,
old_display_user_id VARCHAR(32) NOT NULL,
new_display_user_id VARCHAR(32) NOT NULL,
old_display_user_id_kind VARCHAR(32) NOT NULL,
new_display_user_id_kind VARCHAR(32) NOT NULL,
change_type VARCHAR(32) NOT NULL,
lease_id VARCHAR(64) NULL,
reason VARCHAR(64) NOT NULL,
operator_user_id BIGINT NULL,
request_id VARCHAR(64) NOT NULL,
created_at_ms BIGINT NOT NULL,
KEY idx_display_user_id_change_logs_user_id (user_id),
KEY idx_display_user_id_change_logs_request_id (request_id)
);
```
修改短号必须写审计。审计失败不能静默忽略;如果审计和主变更不能同事务完成,需要进入 user-service outbox 补偿。
### `user_country_change_logs`
国家修改日志只服务资料审计和冷却期,不参与 `display_user_id` 解析。
```sql
CREATE TABLE user_country_change_logs (
id BIGINT NOT NULL PRIMARY KEY,
user_id BIGINT NOT NULL,
old_country VARCHAR(64) NULL,
new_country VARCHAR(64) NOT NULL,
request_id VARCHAR(96) NOT NULL,
changed_at_ms BIGINT NOT NULL,
KEY idx_user_country_change_logs_user_changed (user_id, changed_at_ms),
KEY idx_user_country_change_logs_request_id (request_id)
);
```
同一用户修改国家按滚动 30 天冷却。冷却期检查必须在锁定 `users` 行后的事务内读取最新日志,不能只依赖 gateway 侧判断。
## ID Generation
### `user_id`
`user_id` 使用 int64在服务间 RPC、数据库关系、客户端响应和内部审计中统一使用。人工输入、搜索、分享和登录名使用 `display_user_id`
要求:
- `user_id` 全局唯一。
- 不可变。
- 客户端不能自报或修改 `user_id`
- `user_id` 不依赖数据库自增暴露业务规模。
可选实现:
```text
Snowflake-like ID
Sonyflake-like ID
database segment allocator
```
当前实现由 user-service 创建 `user_id`
### `display_user_id`
`display_user_id` 推荐用字符串保存,即使首版只允许数字。
原因:
- 后续可能支持靓号、带前缀号、运营号段。
- 字符串可以保留前导零。
- 可以统一处理大小写和格式归一化。
首版格式可以定为:
```text
^[1-9][0-9]{5,10}$
```
格式规则必须由 user-service 校验。客户端校验只做体验,不作为安全边界。
## Pretty Display User ID Model
临时靓号是短号的一种,但它不是永久改号。它只是在有效期内覆盖当前展示号。
有效展示号计算规则:
```text
if active_pretty_lease exists and now < expires_at_ms:
display_user_id = pretty_display_user_id
display_user_id_kind = pretty
display_user_id_expires_at_ms = expires_at_ms
else:
display_user_id = default_display_user_id
display_user_id_kind = default
display_user_id_expires_at_ms = null
```
首版不做多个靓号叠加。用户已有 active 靓号时,再申请其他靓号返回 `DISPLAY_USER_ID_PRETTY_ACTIVE`。如果后续要支持续期,只允许对同一个靓号延长 `expires_at_ms`,不能生成第二条 active 租约。
默认短号和临时靓号的关系:
| Scenario | Behavior |
| --- | --- |
| 新用户注册 | 分配默认短号,当前展示号等于默认短号 |
| 申请靓号成功 | 当前展示号改为靓号,默认短号变为 `held` |
| 靓号有效期内搜索 | 只能通过靓号解析到用户 |
| 靓号有效期内密码登录 | 只能通过当前靓号登录 |
| 靓号过期 | 当前展示号恢复默认短号 |
| 靓号过期后搜索 | 靓号不再解析,默认短号重新解析 |
默认短号在 `held` 期间仍属于原用户,不能被其他用户申请、租赁或永久占用。
## Cross-Service Contract
内部 proto 字段统一使用:
```protobuf
int64 user_id = 1;
```
禁止在内部主链路使用:
```protobuf
string display_user_id = 1;
```
允许 user-service 暴露解析接口:
```protobuf
service UserIdentityService {
rpc ResolveDisplayUserID(ResolveDisplayUserIDRequest) returns (ResolveDisplayUserIDResponse);
rpc GetUserIdentity(GetUserIdentityRequest) returns (GetUserIdentityResponse);
rpc ApplyPrettyDisplayUserID(ApplyPrettyDisplayUserIDRequest) returns (ApplyPrettyDisplayUserIDResponse);
}
message ResolveDisplayUserIDRequest {
string display_user_id = 1;
}
message ResolveDisplayUserIDResponse {
int64 user_id = 1;
string display_user_id = 2;
string status = 3;
string default_display_user_id = 4;
string display_user_id_kind = 5;
int64 display_user_id_expires_at_ms = 6;
}
message GetUserIdentityRequest {
int64 user_id = 1;
}
message GetUserIdentityResponse {
int64 user_id = 1;
string display_user_id = 2;
string status = 3;
string default_display_user_id = 4;
string display_user_id_kind = 5;
int64 display_user_id_expires_at_ms = 6;
}
message ApplyPrettyDisplayUserIDRequest {
int64 user_id = 1;
string pretty_display_user_id = 2;
int64 lease_duration_ms = 3;
string payment_receipt_id = 4;
}
message ApplyPrettyDisplayUserIDResponse {
int64 user_id = 1;
string display_user_id = 2;
string default_display_user_id = 3;
string display_user_id_kind = 4;
int64 display_user_id_expires_at_ms = 5;
string lease_id = 6;
}
```
`ResolveDisplayUserID` 只能返回当前有效归属。默认短号被靓号覆盖时处于 `held`,普通解析接口不能返回它。历史归属查询必须使用运营或审计接口,不能给普通业务链路使用。
## Service Usage Rules
### gateway-service
gateway 可以接收客户端传入的 `display_user_id`,但只能用于调用 `user-service.ResolveDisplayUserID`
解析后:
```text
display_user_id -> user_id -> downstream RPC
```
gateway 不缓存永久映射。
密码登录入口也按当前展示号解析。用户有 active 靓号时,默认短号暂时不能登录;靓号过期后,默认短号恢复登录能力。
### room-service
Room Cell 中只保存:
```text
OwnerID int64
HostID int64
MicSeat.UserID int64
OnlineUsers map[int64]
AdminUsers map[int64]bool
BanUsers map[int64]bool
MuteUsers map[int64]bool
```
房间状态、snapshot、command log、outbox 事件都不能保存 `display_user_id` 作为身份字段。
如果房间广播需要展示短号,由客户端或网关资料接口通过 user-service 补全Room Cell 不承担资料拼装。
### Tencent Cloud IM
腾讯云 IM identifier 只使用不可变长 `user_id` 的十进制字符串:
```text
user_id
```
单聊消息、系统通知、群消息和后续回调校验都使用 `user_id`
客户端通过短号发起私聊时,必须先解析成 `user_id`
### wallet-service
账务主体只允许是:
```text
user_id
```
账单、ledger、幂等记录、余额账户都不能用 `display_user_id`
### activity-service
活动积分、奖励、风控、排行榜内部主体只用 `user_id`
活动榜单展示短号时,展示层再补资料。活动结算不能依赖短号。
## User Creation Flow
新用户创建时必须同时分配 `user_id``default_display_user_id`。创建完成后,`current_display_user_id` 等于默认短号。
```mermaid
sequenceDiagram
participant U as user-service
participant DB as MySQL
U->>U: generate user_id
U->>U: allocate default_display_user_id from 163000 sequence
U->>DB: begin transaction
U->>DB: insert users(default=current)
U->>DB: insert user_display_user_ids(default,active)
U->>DB: commit
```
如果默认短号唯一冲突,重新生成候选短号后重试。重试次数必须有限,超过后返回 `DISPLAY_USER_ID_ALLOCATE_FAILED`
## Change Default Display User ID Flow
修改默认短号必须事务化。首版建议禁止在 active 靓号期间修改默认短号,避免“过期恢复哪个号”的用户理解成本;如果业务必须支持,需要在变更日志中明确记录新的恢复目标。
```mermaid
sequenceDiagram
participant C as Client
participant G as gateway-service
participant U as user-service
participant DB as MySQL
C->>G: ChangeDisplayUserID
G->>U: ChangeDisplayUserID(user_id,new_default_display_user_id)
U->>U: validate format and cooldown
U->>DB: begin transaction
U->>DB: lock user row
U->>DB: reject if active pretty lease exists
U->>DB: lock current default display_user_id
U->>DB: check new display_user_id uniqueness
U->>DB: mark old default display_user_id released
U->>DB: insert new default display_user_id active
U->>DB: update users.default_display_user_id
U->>DB: update users.current_display_user_id
U->>DB: insert display_user_id_change_logs
U->>DB: commit
```
修改成功后user-service 可以发布用户资料变更事件,供客户端资料缓存或服务端资料缓存刷新。腾讯云 IM 的 identifier 不随短号变化。
## Apply Pretty Display User ID Flow
申请临时靓号必须先确认靓号可用、租期合法、支付结果有效。金币扣费或订单支付归属账务服务user-service 只消费可信的 `payment_receipt_id` 或运营授权结果。
```mermaid
sequenceDiagram
participant C as Client
participant G as gateway-service
participant U as user-service
participant DB as MySQL
C->>G: ApplyPrettyDisplayUserID
G->>U: ApplyPrettyDisplayUserID(user_id,pretty_display_user_id,duration,receipt)
U->>U: validate format, duration, receipt
U->>DB: begin transaction
U->>DB: lock user row
U->>DB: reject if user already has active pretty lease
U->>DB: check pretty display_user_id availability
U->>DB: mark default display_user_id held
U->>DB: insert pretty_display_user_id_leases(active)
U->>DB: insert user_display_user_ids(pretty,active,expires_at)
U->>DB: update users.current_display_user_id=pretty
U->>DB: insert display_user_id_change_logs(change_type=pretty_apply)
U->>DB: commit
```
申请成功后:
- `ResolveDisplayUserID(pretty)` 返回该用户。
- `ResolveDisplayUserID(default)` 不返回该用户。
- `GetUserIdentity(user_id)` 返回 `display_user_id_kind=pretty``display_user_id_expires_at_ms`
- 密码登录使用当前靓号,默认短号暂时不能作为登录名。
## Expire Pretty Display User ID Flow
靓号过期有两条触发路径:
| Path | Rule |
| --- | --- |
| 定时任务 | 扫描 `pretty_display_user_id_leases(status=active, expires_at_ms<=now)` |
| 懒过期 | `ResolveDisplayUserID``GetUserIdentity`、密码登录发现快照过期时触发 |
过期恢复必须走同一段事务逻辑:
```mermaid
sequenceDiagram
participant U as user-service
participant DB as MySQL
U->>DB: begin transaction
U->>DB: lock user row
U->>DB: lock active pretty lease
U->>DB: mark pretty lease expired
U->>DB: mark pretty display_user_id expired
U->>DB: mark default display_user_id active
U->>DB: update users.current_display_user_id=default
U->>DB: clear users.current_display_user_id_expires_at_ms
U->>DB: insert display_user_id_change_logs(change_type=pretty_expire)
U->>DB: commit
```
过期成功后:
- 旧靓号不能继续登录、搜索或分享解析。
- 默认短号恢复为当前展示号。
- user-service 必须失效 `user_id -> display_user_id` 和相关 `display_user_id -> user_id` 缓存。
- 如果过期事务失败,不能只删缓存;必须重试事务或告警。
## Display User ID Reuse
默认建议:短号释放或靓号过期后进入冷却期,不立即复用。
状态流转:
```text
default: active -> held -> active
default change: active -> released -> reserved -> active
pretty lease: active -> expired -> reserved -> active
```
首版可以先不复用已释放默认短号;靓号是否可再次出租由运营库存规则决定,但必须有冷却期和历史归属审计。
如果后续要复用:
- 需要冷却期。
- 需要运营规则。
- 需要历史归属审计。
- 普通业务查询只能返回当前有效归属。
## API And Proto Additions
建议在:
```text
api/proto/user/v1/user.proto
```
增加:
```protobuf
message UserIdentity {
int64 user_id = 1;
string display_user_id = 2;
string status = 3;
string default_display_user_id = 4;
string display_user_id_kind = 5;
int64 display_user_id_expires_at_ms = 6;
}
message GetUserIdentityRequest {
int64 user_id = 1;
}
message GetUserIdentityResponse {
UserIdentity identity = 1;
}
message ResolveDisplayUserIDRequest {
string display_user_id = 1;
}
message ResolveDisplayUserIDResponse {
UserIdentity identity = 1;
}
message ChangeDisplayUserIDRequest {
RequestMeta meta = 1;
int64 user_id = 2;
string new_display_user_id = 3;
}
message ChangeDisplayUserIDResponse {
UserIdentity identity = 1;
}
message ApplyPrettyDisplayUserIDRequest {
RequestMeta meta = 1;
int64 user_id = 2;
string pretty_display_user_id = 3;
int64 lease_duration_ms = 4;
string payment_receipt_id = 5;
}
message ApplyPrettyDisplayUserIDResponse {
UserIdentity identity = 1;
string lease_id = 2;
}
message ExpirePrettyDisplayUserIDRequest {
RequestMeta meta = 1;
int64 user_id = 2;
string lease_id = 3;
}
message ExpirePrettyDisplayUserIDResponse {
UserIdentity identity = 1;
}
```
HTTP 外部接口由 gateway 暴露:
```text
GET /api/v1/users/by-display-user-id/{display_user_id}
GET /api/v1/users/{user_id}/identity
POST /api/v1/users/me/display-id/change
POST /api/v1/users/me/display-id/pretty/apply
```
具体路径可以后续和 OpenAPI 一起定,但内部 proto 必须先保证主链路只用 `user_id`
## Error Reasons
user-service 需要增加这些 reason
| Reason | gRPC Code | Meaning |
| --- | --- | --- |
| `DISPLAY_USER_ID_INVALID` | `InvalidArgument` | 短号格式非法 |
| `DISPLAY_USER_ID_EXISTS` | `AlreadyExists` | 短号已被当前有效、持有、预留或阻断 |
| `DISPLAY_USER_ID_COOLDOWN` | `FailedPrecondition` | 修改短号冷却期未结束 |
| `COUNTRY_CHANGE_COOLDOWN` | `FailedPrecondition` | 修改国家冷却期未结束 |
| `DISPLAY_USER_ID_NOT_FOUND` | `NotFound` | 当前有效短号不存在 |
| `DISPLAY_USER_ID_ALLOCATE_FAILED` | `Internal` | 默认短号分配失败 |
| `DISPLAY_USER_ID_PRETTY_NOT_AVAILABLE` | `FailedPrecondition` | 靓号不可申请、已被租用或被预留 |
| `DISPLAY_USER_ID_PRETTY_ACTIVE` | `FailedPrecondition` | 用户已有 active 靓号租约 |
| `DISPLAY_USER_ID_LEASE_EXPIRED` | `FailedPrecondition` | 靓号租约已经过期 |
| `DISPLAY_USER_ID_PAYMENT_REQUIRED` | `FailedPrecondition` | 申请靓号缺少有效支付凭证 |
gateway 映射到 HTTP envelope 时沿用相同 `code`
## Caching Rules
允许缓存:
```text
user_id -> display_user_id
display_user_id -> user_id
```
限制:
- `display_user_id -> user_id` 必须短 TTL。
- 靓号缓存 TTL 不能超过 `display_user_id_expires_at_ms - now`
- 申请靓号后必须失效默认短号和新靓号相关缓存。
- 靓号过期后必须失效旧靓号、默认短号和用户身份缓存。
- 修改默认短号后必须主动失效相关缓存。
- 账务、房间、活动不能依赖缓存中的短号做身份判断。
- 缓存 miss 必须回源 user-service。
- 缓存命中也必须尊重 `expires_at_ms`,不能返回已过期靓号。
## Migration Impact
后续修改现有 proto 时:
- 所有跨服务用户字段保持 `int64 user_id`
- 如果要额外返回短号,只能追加 `string display_user_id`,不能替换 `user_id`
- 如果返回当前展示号,必须同时考虑追加 `display_user_id_kind``display_user_id_expires_at_ms`,避免客户端把临时靓号误认为永久短号。
- 历史 outbox 事件不补写短号。
- 客户端显示短号时通过用户资料接口或缓存补全。
## Test Matrix
必须覆盖:
- 新用户创建时生成唯一 `user_id` 和默认 `display_user_id`
- 默认短号冲突时可重试。
- 通过当前有效 `display_user_id` 解析到正确 `user_id`
- 不存在的短号返回 `DISPLAY_USER_ID_NOT_FOUND`
- 修改短号成功后旧短号不再解析到用户。
- 修改短号遇到已占用短号返回 `DISPLAY_USER_ID_EXISTS`
- 冷却期内修改返回 `DISPLAY_USER_ID_COOLDOWN`
- 申请靓号成功后,当前展示号变为靓号,默认短号进入 `held`
- 靓号有效期内,默认短号不能被普通解析接口解析。
- 靓号有效期内,密码登录使用当前靓号。
- 用户已有 active 靓号时再次申请其他靓号返回 `DISPLAY_USER_ID_PRETTY_ACTIVE`
- 靓号过期后,当前展示号恢复默认短号。
- 靓号过期后,旧靓号不再解析到用户。
- 懒过期和定时过期并发执行时,只能有一个事务成功。
- 房间、钱包、IM、活动 proto 和存储仍只使用 `user_id` 作为主体。
## Final Rule
`user_id` 是系统身份,`default_display_user_id` 是用户本身的门牌号,临时靓号只是限时覆盖当前门牌号。
服务之间传 `user_id`。客户端展示、搜索、分享可以用 `display_user_id`