721 lines
23 KiB
Markdown
721 lines
23 KiB
Markdown
# User ID Technical Design
|
||
|
||
本文档定义用户长 ID、默认短号、临时靓号的职责边界、数据库模型、跨服务通信规则和修改流程。核心原则:系统身份只认长 `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: 100001
|
||
current_display_user_id: 888888
|
||
display_user_id_kind: pretty
|
||
display_user_id_expires_at_ms: 1777000000000
|
||
```
|
||
|
||
`user_id` 是身份主键。`default_display_user_id` 是用户本身的号。临时靓号有效期内会覆盖当前展示号;过期后必须恢复到 `default_display_user_id`。
|
||
|
||
## Design Principles
|
||
|
||
1. 跨服务通信一律传 `user_id`。
|
||
2. 数据库主关联一律存 `user_id`。
|
||
3. outbox、审计、账单、房间状态、IM session、活动记录一律存 `user_id`。
|
||
4. `display_user_id` 只允许出现在展示、搜索、分享、登录名、客服查询和运营配置入口。
|
||
5. 任何通过 `display_user_id` 发起的请求,都必须先在 `user-service` 解析成 `user_id`,后续链路只传 `user_id`。
|
||
6. `display_user_id` 可修改,所以不能作为幂等键、外键、账务主体或事件主体。
|
||
7. 临时靓号不能修改 `user_id`,也不能删除或转移用户的默认短号。
|
||
8. 当前展示号的真实来源必须可解释:默认短号来自永久归属,临时靓号来自有效租约。
|
||
9. `ResolveDisplayUserID`、密码登录和用户资料查询都必须先处理过期靓号,再返回当前有效短号。
|
||
10. 靓号过期恢复必须事务化,不能只靠缓存 TTL。
|
||
|
||
## Ownership
|
||
|
||
`user-service` 是 `user_id` 和 `display_user_id` 的唯一 owner。
|
||
|
||
| Capability | Owner | Rule |
|
||
| --- | --- | --- |
|
||
| 生成 `user_id` | `user-service` | 注册或三方首次登录时生成 |
|
||
| 分配默认短号 | `user-service` | 新用户创建时分配 `default_display_user_id` |
|
||
| 修改默认短号 | `user-service` | 事务内检查唯一性和冷却期 |
|
||
| 申请临时靓号 | `user-service` | 创建靓号租约并覆盖当前展示号 |
|
||
| 过期临时靓号 | `user-service` | 恢复当前展示号为默认短号 |
|
||
| 解析 `display_user_id` | `user-service` | 只返回当前有效归属 |
|
||
| 房间状态 | `room-service` | 只保存 `user_id` |
|
||
| 钱包账务 | `wallet-service` | 只保存 `user_id` |
|
||
| IM 连接 | `im-service` | 只保存 `user_id` |
|
||
| 活动结算 | `activity-service` | 只保存 `user_id` |
|
||
|
||
其他服务不能自己缓存长期 `display_user_id -> user_id` 绑定作为事实来源。短期缓存必须有 TTL,并允许 user-service 失效或回源。
|
||
|
||
## Database Model
|
||
|
||
### `users`
|
||
|
||
用户主表保存不可变主键、默认短号和当前展示号快照。
|
||
|
||
```sql
|
||
CREATE TABLE users (
|
||
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,
|
||
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 (default_display_user_id),
|
||
UNIQUE KEY uk_users_current_display_user_id (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 (
|
||
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 (live_display_user_id),
|
||
UNIQUE KEY uk_user_display_user_ids_active_user (active_user_id),
|
||
KEY idx_user_display_user_ids_user_id (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 补偿。
|
||
|
||
## ID Generation
|
||
|
||
### `user_id`
|
||
|
||
`user_id` 推荐使用 int64 长 ID。
|
||
|
||
要求:
|
||
|
||
- 全局唯一。
|
||
- 不可变。
|
||
- 可排序优先,但不强制。
|
||
- 不依赖数据库自增暴露业务规模。
|
||
|
||
可选实现:
|
||
|
||
```text
|
||
Snowflake-like ID
|
||
Sonyflake-like ID
|
||
database segment allocator
|
||
```
|
||
|
||
当前仓库已有 `pkg/idgen` 生成字符串 ID,但用户主键更适合 int64。实现前需要确认是否新增 int64 ID generator。
|
||
|
||
### `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` 作为身份字段。
|
||
|
||
如果房间广播需要展示短号,由客户端或 im-service 通过用户资料缓存补全;Room Cell 不承担资料拼装。
|
||
|
||
### im-service
|
||
|
||
IM session 只保存:
|
||
|
||
```text
|
||
user_id
|
||
device_id
|
||
conn_id
|
||
node_id
|
||
```
|
||
|
||
单聊消息、系统通知、ACK、订阅校验都使用 `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 candidate
|
||
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-service 或缓存系统刷新。
|
||
|
||
## 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` | 修改短号冷却期未结束 |
|
||
| `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`。
|