# 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_ms) 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`。