# 靓号管理服务端技术文档

## 当前结论

靓号管理不能只做成后台页面配置。当前仓库里 `user-service` 已经是 `user_id`、`display_user_id` 和临时靓号租约的 owner，`server/admin` 只能做后台 HTTP 入口、权限、审计和管理适配，App 入口继续由 `gateway-service` 暴露。

本需求新增的是“靓号池申请 + 后台发放靓号 + 资料返回靓号字段”，要在现有 `display_user_id` 体系上扩展，不重新引入一套用户 ID。

## 已有事实

- `users.user_id` 是内部不可变用户主键。
- `users.current_display_user_id` 是当前对外展示短号。
- `users.current_display_user_id_kind` 当前支持 `default` / `pretty`。
- `users.current_display_user_id_expires_at_ms` 为 `0` 或空时表示当前展示号不自动过期。
- `user_display_user_ids` 已保存默认短号和靓号的实时归属。
- `pretty_display_user_id_leases` 已保存靓号覆盖关系。
- 当前默认短号和旧自助靓号校验是 5 到 11 位、不能 0 开头的纯数字。
- 本需求的靓号池批量生成默认仍可使用数字规则；后台发放靓号不能复用纯数字限制，需要支持阿拉伯语、英文和数字组合。
- 当前 App 端已有 `/api/v1/users/me/display-id/pretty/apply`，但参数是 `pretty_display_user_id + lease_duration_ms + payment_receipt_id`，不是靓号池申请要的 `pretty_id`。

## 目标

- 后台在“用户管理”下新增二级菜单“靓号管理”。
- 后台可以配置靓号池的等级轨道、等级区间、生成规则和生成数量。
- 后台批量生成靓号时，不能和当前库里的 `users.user_id`、默认短号、当前短号、历史有效短号、已有靓号池号码冲突。
- 每个靓号池号码都有一个稳定 `pretty_id`，App 靓号池申请时只传 `pretty_id`。
- 靓号池申请默认长期持有；用户已有靓号时，新靓号直接覆盖旧靓号，旧靓号释放。
- 后台发放靓号可以直接输入靓号内容，可以填写有效期；不填有效期时默认长期持有。
- 后台发放靓号内容不限定纯数字，需要支持阿拉伯语、英文和数字组合。
- App 用户申请靓号池号码时，服务端根据靓号池配置校验用户是否在对应等级区间内，在区间内才允许占用。
- 用户拥有靓号后，个人信息、个人资料卡、房间个人资料卡、房间详情等资料返回都带当前靓号字段。

## 非目标

- 不把靓号作为内部主键使用，所有业务关系仍然使用 `user_id`。
- 不让 `room-service` 保存靓号状态。房间只保存房间状态和用户 `user_id`，展示资料由 gateway 汇总用户资料。
- 不让 `server/admin` 直接 import app 后端 `services/*` 或 `pkg/*`。
- 首版不做等级下降后的自动回收。用户申请时校验等级区间；后续如果需要降级回收，再新增独立回收规则和定时任务。
- 后台发放不需要等级校验，管理员只受后台权限和审计约束。
- 首版不对外推送 IM。靓号申请成功后，客户端以接口返回和下次资料刷新为准。

## 服务边界

| 模块 | 责任 |
| --- | --- |
| `user-service` | 靓号池、靓号项、后台发放、替换释放事务、短号唯一性、当前展示号覆盖、资料字段 |
| `activity-service` | 提供用户等级展示投影，至少能按 `user_id` 返回 `wealth/game/charm` 当前等级 |
| `gateway-service` | App HTTP 入口，按 token 用户申请靓号，返回统一 envelope |
| `server/admin` | 后台 HTTP API、菜单、权限、审计，调用 `hyapp.local/api` protobuf client |
| `hyapp-admin-platform` | 用户管理下新增“靓号管理”页面，消费后台 API |

## 等级区间规则

当前等级体系有三条轨道：

| track | 含义 |
| --- | --- |
| `wealth` | 财富等级 |
| `game` | 游戏等级 |
| `charm` | 魅力等级 |

靓号池配置必须保存 `level_track`，不能在服务端猜默认轨道。后台页面可以默认选中 `wealth`，但请求和数据库必须明确保存轨道。

靓号池申请时：

1. 根据 `pretty_id` 锁定靓号项和所属靓号池。
2. 读取用户当前等级，使用池子的 `level_track`。
3. 判断 `min_level <= current_level <= max_level`。
4. 通过后进入靓号占用事务。

## 数据库设计

新增表归属 `hyapp_user`，因为靓号和短号唯一性属于 `user-service`。

### 现有短号表扩展

后台发放的靓号不一定是数字，现有短号相关字段需要从数字短号能力扩展成“展示号字符串”能力：

```sql
ALTER TABLE users
  MODIFY COLUMN current_display_user_id VARCHAR(64) NOT NULL,
  MODIFY COLUMN default_display_user_id VARCHAR(64) NOT NULL;

ALTER TABLE user_display_user_ids
  MODIFY COLUMN display_user_id VARCHAR(64) NOT NULL,
  MODIFY COLUMN live_display_user_id VARCHAR(64)
    GENERATED ALWAYS AS (
      CASE
        WHEN status IN ('active', 'held', 'reserved', 'blocked') THEN display_user_id
        ELSE NULL
      END
    ) STORED;

ALTER TABLE pretty_display_user_id_leases
  MODIFY COLUMN display_user_id VARCHAR(64) NOT NULL,
  MODIFY COLUMN active_display_user_id VARCHAR(64)
    GENERATED ALWAYS AS (
      CASE WHEN status = 'active' THEN display_user_id ELSE NULL END
    ) STORED,
  ADD COLUMN released_at_ms BIGINT NOT NULL DEFAULT 0 AFTER expires_at_ms,
  ADD COLUMN release_reason VARCHAR(64) NOT NULL DEFAULT '' AFTER released_at_ms;

ALTER TABLE display_user_id_change_logs
  MODIFY COLUMN old_display_user_id VARCHAR(64) NOT NULL,
  MODIFY COLUMN new_display_user_id VARCHAR(64) NOT NULL;
```

规则：

- 默认短号仍然按现有数字规则生成。
- 靓号池批量生成默认仍然按数字规则生成。
- 后台发放靓号允许 1 到 64 个字符。
- 后台发放靓号允许 Unicode 字母、Unicode 数字和组合标记，用于支持阿拉伯语、英文和数字。
- 后台发放靓号不允许空白字符、控制字符、emoji 和标点符号。
- 唯一性仍按 `app_code + display_user_id` 判断；如果产品要求大小写敏感，相关唯一列需要改成 `utf8mb4_bin` 或在写入前做明确大小写归一。

### `pretty_display_id_pools`

保存后台配置的等级区间和默认生成规则。

```sql
CREATE TABLE pretty_display_id_pools (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
  pool_id VARCHAR(64) NOT NULL PRIMARY KEY,
  name VARCHAR(128) NOT NULL,
  level_track VARCHAR(32) NOT NULL,
  min_level INT NOT NULL,
  max_level INT NOT NULL,
  rule_type VARCHAR(32) NOT NULL,
  rule_config_json JSON NULL,
  status VARCHAR(32) NOT NULL,
  sort_order INT NOT NULL DEFAULT 0,
  created_by_admin_id BIGINT NOT NULL DEFAULT 0,
  updated_by_admin_id BIGINT NOT NULL DEFAULT 0,
  created_at_ms BIGINT NOT NULL,
  updated_at_ms BIGINT NOT NULL,
  UNIQUE KEY uk_pretty_pool_range_rule (app_code, level_track, min_level, max_level, name),
  KEY idx_pretty_pool_status (app_code, status, sort_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='靓号池配置表';
```

字段说明：

| 字段 | 说明 |
| --- | --- |
| `pool_id` | 靓号池 ID |
| `level_track` | `wealth` / `game` / `charm` |
| `min_level` | 可申请最小等级，包含 |
| `max_level` | 可申请最大等级，包含 |
| `rule_type` | 生成规则，例如 `aabbcc` |
| `rule_config_json` | 长度、号段、排除数字等扩展配置 |
| `status` | `active` / `disabled` |

约束：

- `min_level` 不能大于 `max_level`。
- `level_track` 必须是已有等级轨道。
- `active` 池才允许 App 用户申请。

### `pretty_display_ids`

保存每一个可申请靓号。每个靓号都有自己的 `pretty_id`。

```sql
CREATE TABLE pretty_display_ids (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
  pretty_id VARCHAR(64) NOT NULL PRIMARY KEY,
  pool_id VARCHAR(64) NOT NULL DEFAULT '',
  source VARCHAR(32) NOT NULL DEFAULT 'pool',
  display_user_id VARCHAR(64) NOT NULL,
  status VARCHAR(32) NOT NULL,
  assigned_user_id BIGINT NOT NULL DEFAULT 0,
  assigned_lease_id VARCHAR(64) NOT NULL DEFAULT '',
  assigned_at_ms BIGINT NOT NULL DEFAULT 0,
  released_at_ms BIGINT NOT NULL DEFAULT 0,
  release_reason VARCHAR(64) NOT NULL DEFAULT '',
  generated_batch_id VARCHAR(64) NOT NULL DEFAULT '',
  created_by_admin_id BIGINT NOT NULL DEFAULT 0,
  created_at_ms BIGINT NOT NULL,
  updated_at_ms BIGINT NOT NULL,
  live_display_user_id VARCHAR(64)
    GENERATED ALWAYS AS (
      CASE WHEN status IN ('available', 'assigned', 'disabled', 'reserved') THEN display_user_id ELSE NULL END
    ) STORED,
  UNIQUE KEY uk_pretty_display_ids_live (app_code, live_display_user_id),
  KEY idx_pretty_display_ids_pool_status (app_code, pool_id, status),
  KEY idx_pretty_display_ids_assigned_user (app_code, assigned_user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='靓号池号码表';
```

状态说明：

| status | 说明 |
| --- | --- |
| `available` | 可申请 |
| `assigned` | 已被用户占用 |
| `disabled` | 后台下架，不允许申请 |
| `reserved` | 系统预留，不允许普通用户申请 |
| `released` | 曾经发放或占用，已释放，不参与可申请列表 |
| `deleted` | 软删除，只保留审计 |

`source` 说明：

| source | 说明 |
| --- | --- |
| `pool` | 靓号池生成，释放后可以回到 `available` |
| `admin_grant` | 后台直接发放，释放后默认进入 `released` |

### `pretty_display_id_generation_batches`

保存批量生成记录，方便后台展示和排查。

```sql
CREATE TABLE pretty_display_id_generation_batches (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
  batch_id VARCHAR(64) NOT NULL PRIMARY KEY,
  pool_id VARCHAR(64) NOT NULL,
  rule_type VARCHAR(32) NOT NULL,
  rule_config_json JSON NULL,
  requested_count INT NOT NULL,
  generated_count INT NOT NULL,
  skipped_conflict_count INT NOT NULL,
  status VARCHAR(32) NOT NULL,
  operator_admin_id BIGINT NOT NULL,
  request_id VARCHAR(64) NOT NULL DEFAULT '',
  created_at_ms BIGINT NOT NULL,
  updated_at_ms BIGINT NOT NULL,
  KEY idx_pretty_batch_pool (app_code, pool_id, created_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='靓号批量生成记录表';
```

## 靓号池生成规则

靓号池批量生成支持数字规则、日期规则、寓意号和自定义规则。数字规则允许前导 0，号码以字符串保存。

| rule_type | 说明 |
| --- | --- |
| `aa`、`aaa`、`aaaa`、`aaaaa`、`aaaaaa`、`aaaaaaa` | 2A 到 7A 连号 |
| `aabb` | 两组对子 |
| `aabbcc` | 三组重复数字 |
| `aabbccdd` | 四组对子 |
| `abab`、`ababab` | 交替号 |
| `abba`、`abcba`、`abccba` | 回文号 |
| `aaab`、`aaaab`、`aaaaab` | 拖一号 |
| `aaabbb`、`aaaabbbb` | 双组号 |
| `abc`、`abcd`、`abcde`、`abcdef` | 顺子号 |
| `cba`、`dcba`、`edcba`、`fedcba` | 倒顺号 |
| `abcabc` | 顺子重复号 |
| `date_yyyymmdd`、`date_yymmdd` | 日期号 |
| `love` | 520、521、1314 等寓意号 |
| `custom_values` | 使用 `rule_config_json.values` 固定候选 |
| `custom_pattern` | 使用 `rule_config_json.pattern` 自定义模式 |

`rule_config_json` 可选字段：

| 字段 | 说明 |
| --- | --- |
| `digits` | 限制可用数字，例如 `["6","8","9"]` |
| `exclude_digits` | 排除数字，例如 `["4"]` |
| `prefix`、`suffix` | 给生成候选加前缀或后缀 |
| `values` | `custom_values` 的候选数组 |
| `pattern` | `custom_pattern` 的模式，例如 `AAAABBBB` |
| `year_from`、`year_to` | 日期号年份范围 |
| `start_date`、`end_date` | 日期号精确范围，格式 `yyyy-mm-dd` |

后台直接发放靓号不受本节数字生成规则限制，按“现有短号表扩展”里的后台发放字符规则校验。

## 批量生成事务

批量生成时由 `user-service` 完成冲突检查和入库。

冲突范围：

- `CAST(users.user_id AS CHAR)`。
- `users.default_display_user_id`。
- `users.current_display_user_id`。
- `user_display_user_ids` 中 `status IN ('active','held','reserved','blocked')` 的号码。
- `pretty_display_ids` 中 `status IN ('available','assigned','disabled','reserved')` 的号码。

流程：

1. 后台提交 `pool_id + rule_type + rule_config + count`。
2. `user-service` 生成候选号码。
3. 每个候选号码先做数字短号格式校验。
4. 批量查询冲突号码，过滤已冲突项。
5. 插入 `pretty_display_ids`，唯一键兜底处理并发冲突。
6. 写 `pretty_display_id_generation_batches`。
7. 返回本次生成数量和跳过冲突数量。

为了避免生成器死循环，服务端需要设置最大尝试次数，例如 `count * 20`，仍不足时返回实际生成数量和 `skipped_conflict_count`。

## 通用替换释放事务

靓号池申请和后台发放都复用同一个“替换当前靓号”事务。

如果用户没有 active pretty 展示号：

1. 默认短号从 `active` 改为 `held`。
2. 新靓号写入 `pretty_display_user_id_leases`。
3. 新靓号写入 `user_display_user_ids`，`display_user_id_kind='pretty'`。
4. 更新 `users.current_display_user_id`、`current_display_user_id_kind`、`current_display_user_id_expires_at_ms`。
5. 写 `display_user_id_change_logs`。

如果用户已有 active pretty 展示号：

1. 锁定当前用户行和旧 active lease。
2. 旧 `pretty_display_user_id_leases.status` 改为 `cancelled`，`release_reason='replaced_by_new_pretty'`。
3. 旧 `user_display_user_ids.status` 改为 `released`，记录 `released_at_ms`。
4. 如果旧靓号来自靓号池，旧 `pretty_display_ids.status` 改回 `available`，清空 `assigned_user_id`、`assigned_lease_id`、`assigned_at_ms`。
5. 如果旧靓号来自后台发放，旧 `pretty_display_ids.status` 改为 `released`，保留历史。
6. 写旧靓号释放日志。
7. 再按新靓号写入当前 active 展示号。

这个规则意味着：用户申请或被后台发放新靓号时，不返回“已有靓号”冲突，而是直接覆盖旧靓号。

## 靓号池申请事务

App 靓号池申请只接受 `pretty_id`，不接受客户端自报号码、等级、用户 ID 或租期。

事务流程：

1. 从 access token 读取当前 `user_id`。
2. `SELECT ... FOR UPDATE` 锁定 `pretty_display_ids`。
3. 校验靓号项为 `available` 且 `source='pool'`。
4. 锁定所属 `pretty_display_id_pools`，校验为 `active`。
5. 调用 `activity-service` 读取当前用户在 `level_track` 下的等级。
6. 校验用户等级在 `[min_level, max_level]`。
7. 进入通用替换释放事务。
8. 写新 `pretty_display_user_id_leases`，`source='level_pool'`，`expires_at_ms=0`。
9. 更新新 `pretty_display_ids.status='assigned'`，写入 `assigned_user_id` 和 `assigned_lease_id`。
10. 返回当前身份投影。

靓号池申请默认长期持有：

- `pretty_display_user_id_leases.expires_at_ms = 0`。
- `users.current_display_user_id_expires_at_ms = 0`。
- 不由定时任务自动过期。

## 后台发放事务

后台发放不依赖靓号池，也不校验用户等级。

输入：

- `target_user_id`：目标 App 用户。
- `display_user_id`：管理员输入的靓号内容。
- `duration_ms`：可选；大于 0 表示限时靓号，不传或传 0 表示长期。
- `reason`：发放原因，写入审计和 change log。

事务流程：

1. 校验管理员有 `pretty-id:grant` 权限。
2. 校验 `display_user_id` 符合后台发放字符规则。
3. 检查 `display_user_id` 不与当前有效 `users.user_id` 字符串、默认短号、当前短号、历史有效短号、已有靓号冲突。
4. 锁定目标用户行。
5. 进入通用替换释放事务。
6. 生成新的 `pretty_id` 和 `lease_id`。
7. 写 `pretty_display_ids`，`source='admin_grant'`，`status='assigned'`。
8. 写 `pretty_display_user_id_leases`，`source='admin_grant'`。
9. 如果 `duration_ms > 0`，`expires_at_ms = now_ms + duration_ms`；否则 `expires_at_ms = 0`。
10. 更新 `users.current_display_user_id_expires_at_ms` 为同一个过期时间。
11. 返回当前身份投影。

限时后台靓号：

- `expires_at_ms > 0` 时继续复用现有靓号过期恢复逻辑。
- 到期后恢复默认短号，不回到靓号池。
- 如果到期前又发放或申请了新靓号，旧限时靓号按通用替换释放事务取消。

## Proto 变更

在 `api/proto/user/v1/user.proto` 中扩展或新增 identity 管理 RPC。

新增 App 申请 RPC：

```proto
message ApplyPrettyDisplayIDFromPoolRequest {
  RequestMeta meta = 1;
  int64 user_id = 2;
  string pretty_id = 3;
}

message ApplyPrettyDisplayIDFromPoolResponse {
  UserIdentity identity = 1;
  string pretty_id = 2;
  string lease_id = 3;
}
```

新增后台管理 RPC：

```proto
message AdminGrantPrettyDisplayIDRequest {
  RequestMeta meta = 1;
  int64 target_user_id = 2;
  string display_user_id = 3;
  int64 duration_ms = 4;
  string reason = 5;
  int64 operator_admin_id = 6;
}

message AdminGrantPrettyDisplayIDResponse {
  UserIdentity identity = 1;
  string pretty_id = 2;
  string lease_id = 3;
}

service UserPrettyDisplayIDAdminService {
  rpc ListPrettyDisplayIDPools(ListPrettyDisplayIDPoolsRequest) returns (ListPrettyDisplayIDPoolsResponse);
  rpc CreatePrettyDisplayIDPool(CreatePrettyDisplayIDPoolRequest) returns (PrettyDisplayIDPoolResponse);
  rpc UpdatePrettyDisplayIDPool(UpdatePrettyDisplayIDPoolRequest) returns (PrettyDisplayIDPoolResponse);
  rpc GeneratePrettyDisplayIDs(GeneratePrettyDisplayIDsRequest) returns (GeneratePrettyDisplayIDsResponse);
  rpc ListPrettyDisplayIDs(ListPrettyDisplayIDsRequest) returns (ListPrettyDisplayIDsResponse);
  rpc SetPrettyDisplayIDStatus(SetPrettyDisplayIDStatusRequest) returns (PrettyDisplayIDResponse);
  rpc AdminGrantPrettyDisplayID(AdminGrantPrettyDisplayIDRequest) returns (AdminGrantPrettyDisplayIDResponse);
}
```

资料投影建议增加字段：

```proto
message User {
  // existing fields...
  string pretty_id = 24;
  string pretty_display_user_id = 25;
}

message UserIdentity {
  // existing fields...
  string pretty_id = 8;
  string pretty_display_user_id = 9;
}
```

字段规则：

| 字段 | 说明 |
| --- | --- |
| `display_user_id` | 当前对外展示号；有靓号时等于靓号 |
| `display_user_id_kind` | `default` / `pretty` |
| `display_user_id_expires_at_ms` | 长期持有为 `0` |
| `pretty_id` | 当前占用的靓号池记录 ID，没有则为空 |
| `pretty_display_user_id` | 当前占用的靓号号码，没有则为空 |

## 后台 HTTP 接口

后台接口归属 `server/admin/internal/modules/prettyid`，路径放在用户管理域。

### 靓号池列表

| 项 | 内容 |
| --- | --- |
| 地址 | `GET /api/v1/admin/users/pretty-id-pools` |
| 参数 | `status`、`level_track`、`page`、`page_size` |
| 返回值 | `items[]`、`total`，每项包含 `pool_id`、`name`、`level_track`、`min_level`、`max_level`、`rule_type`、`status` |
| 相关 IM | 无 |

### 创建靓号池

| 项 | 内容 |
| --- | --- |
| 地址 | `POST /api/v1/admin/users/pretty-id-pools` |
| 参数 | `name`、`level_track`、`min_level`、`max_level`、`rule_type`、`rule_config_json`、`status` |
| 返回值 | 靓号池详情 |
| 相关 IM | 无 |

### 更新靓号池

| 项 | 内容 |
| --- | --- |
| 地址 | `PUT /api/v1/admin/users/pretty-id-pools/{pool_id}` |
| 参数 | `name`、`level_track`、`min_level`、`max_level`、`rule_type`、`rule_config_json`、`status` |
| 返回值 | 靓号池详情 |
| 相关 IM | 无 |

### 批量生成靓号

| 项 | 内容 |
| --- | --- |
| 地址 | `POST /api/v1/admin/users/pretty-id-pools/{pool_id}/generate` |
| 参数 | `rule_type`、`rule_config_json`、`count` |
| 返回值 | `batch_id`、`requested_count`、`generated_count`、`skipped_conflict_count` |
| 相关 IM | 无 |

### 靓号列表

| 项 | 内容 |
| --- | --- |
| 地址 | `GET /api/v1/admin/users/pretty-ids` |
| 参数 | `pool_id`、`source`、`status`、`keyword`、`assigned_user_id`、`page`、`page_size` |
| 返回值 | `items[]`、`total`，每项包含 `pretty_id`、`source`、`display_user_id`、`pool`、`status`、`assigned_user_id`、`assigned_lease_id` |
| 相关 IM | 无 |

### 后台发放靓号

| 项 | 内容 |
| --- | --- |
| 地址 | `POST /api/v1/admin/users/pretty-ids/grant` |
| 参数 | `target_user_id`、`display_user_id`、`duration_ms`、`reason`；`duration_ms` 不传或为 `0` 表示长期 |
| 返回值 | `user_id`、`display_user_id`、`display_user_id_kind`、`display_user_id_expires_at_ms`、`pretty_id`、`pretty_display_user_id`、`lease_id` |
| 相关 IM | 无 |

### 靓号状态修改

| 项 | 内容 |
| --- | --- |
| 地址 | `POST /api/v1/admin/users/pretty-ids/{pretty_id}/status` |
| 参数 | `status`，只允许未占用的靓号在 `available`、`disabled`、`reserved` 之间切换 |
| 返回值 | 靓号详情 |
| 相关 IM | 无 |

## App HTTP 接口

### 可申请靓号列表

| 项 | 内容 |
| --- | --- |
| 地址 | `GET /api/v1/users/pretty-ids/available` |
| 参数 | `page`、`page_size` |
| 返回值 | `items[]`、`total`，每项包含 `pretty_id`、`display_user_id`、`level_track`、`min_level`、`max_level` |
| 相关 IM | 无 |

返回只包含当前用户等级满足条件的 `available` 靓号。

### 靓号池申请

| 项 | 内容 |
| --- | --- |
| 地址 | `POST /api/v1/users/me/display-id/pretty/apply` |
| 参数 | `pretty_id` |
| 返回值 | `user_id`、`display_user_id`、`display_user_id_kind`、`display_user_id_expires_at_ms`、`pretty_id`、`pretty_display_user_id`、`lease_id` |
| 相关 IM | 无 |

说明：

- 保留当前地址，避免前端路由重复；请求体改为优先读取 `pretty_id`。
- 靓号池申请默认长期持有，`display_user_id_expires_at_ms=0`。
- 用户已有靓号时，新靓号直接覆盖旧靓号，旧靓号释放。
- 如果要兼容旧付费参数，服务端可以临时保留 `pretty_display_user_id + lease_duration_ms + payment_receipt_id` 分支，但新 App 只使用 `pretty_id`。

## 资料返回改造

所有资料返回统一使用同一套字段。

| 场景 | 当前链路 | 改造点 |
| --- | --- | --- |
| 个人信息 | `GET /api/v1/users/me` -> `user-service.GetUser` | `User` proto 增加 `pretty_id`、`pretty_display_user_id` |
| 个人资料卡 | 用户资料批量接口或资料详情 | 复用 `User` 投影字段 |
| 房间个人资料卡 | `GET /api/v1/users/room-display-profiles:batch` | `roomDisplayProfileData` 增加靓号字段 |
| 房间详情 | `room detail` 中的 `profiles[]` | 来自同一批 `roomDisplayProfileData` |
| 登录 / 刷新 token | `AuthToken` 已有 display 字段 | 如客户端需要启动后立即知道靓号，补 `pretty_id`、`pretty_display_user_id` |

响应规则：

- 用户没有靓号：`display_user_id` 返回默认短号，`display_user_id_kind='default'`，`pretty_id=''`，`pretty_display_user_id=''`。
- 用户有靓号：`display_user_id` 返回靓号，`display_user_id_kind='pretty'`，`pretty_id` 返回池记录 ID，`pretty_display_user_id` 返回靓号号码。
- 长期持有靓号：`display_user_id_expires_at_ms=0`。
- 后台限时发放靓号：`display_user_id_expires_at_ms` 返回实际过期时间。

## 后台菜单和权限

新增权限：

| 权限码 | kind | 说明 |
| --- | --- | --- |
| `pretty-id:view` | `menu` | 查看靓号管理 |
| `pretty-id:update` | `button` | 新增、编辑、上下架靓号池和靓号 |
| `pretty-id:generate` | `button` | 批量生成靓号 |
| `pretty-id:grant` | `button` | 后台直接发放靓号 |

新增菜单：

| 字段 | 值 |
| --- | --- |
| parent | `app-users` |
| title | `靓号管理` |
| code | `app-user-pretty-ids` |
| path | `/app/users/pretty-ids` |
| icon | `badge` |
| permission_code | `pretty-id:view` |
| sort | `65` |

需要同步：

- `server/admin/migrations/044_pretty_id_management.sql`。
- `server/admin/internal/repository/seed.go` 默认权限和菜单。
- `platform-admin` 默认绑定全部新增权限。
- 只读角色只绑定 `pretty-id:view`。

## 代码落点

| 位置 | 改动 |
| --- | --- |
| `api/proto/user/v1/user.proto` | 增加靓号池 RPC、后台发放 RPC、资料字段 |
| `services/user-service/internal/domain/user` | 增加 pool、pretty item、申请命令、后台发放命令和非数字靓号校验 |
| `services/user-service/internal/storage/mysql/identity` | 增加靓号池、生成、申请、后台发放、替换释放事务 |
| `services/user-service/deploy/mysql/initdb/001_user_service.sql` | 增加新表并扩展短号字段长度 |
| `services/user-service/migrations` | 增加线上迁移 |
| `services/gateway-service/internal/transport/http/userapi` | 改造靓号池申请接口、可申请列表、资料字段 |
| `services/gateway-service/internal/transport/http/roomapi` | 房间资料结构补靓号字段 |
| `server/admin/internal/integration/userclient` | 增加后台靓号管理 client |
| `server/admin/internal/modules/prettyid` | 新增后台模块 |
| `server/admin/internal/router/router.go` | 注册后台模块 |
| `server/admin/migrations` | 菜单和权限迁移 |

## 错误码建议

| code | 场景 |
| --- | --- |
| `PRETTY_ID_NOT_FOUND` | `pretty_id` 不存在 |
| `PRETTY_ID_NOT_AVAILABLE` | 靓号不可申请或已被占用 |
| `PRETTY_ID_LEVEL_NOT_MATCH` | 用户等级不在池区间内 |
| `PRETTY_DISPLAY_ID_INVALID` | 后台发放的靓号内容不符合字符规则 |
| `DISPLAY_USER_ID_CONFLICT` | 生成或申请时发现短号冲突 |

## 验证项

必须验证：

- `make proto` 成功，生成 `.pb.go` 和 `_grpc.pb.go`。
- `go test ./...` 在 `hyapp-server` 根目录通过。
- `go test ./...` 在 `hyapp-server/server/admin` 通过。
- 批量生成 1000 个 `aabbcc` 靓号时，不插入与 `users.user_id`、默认短号、当前短号、已有靓号冲突的号码。
- 用户等级低于池区间时，申请返回 `PRETTY_ID_LEVEL_NOT_MATCH`。
- 用户等级在池区间内时，申请成功并更新：
  - `pretty_display_ids.status='assigned'`
  - `pretty_display_user_id_leases.source='pool'`
  - `user_display_user_ids.display_user_id_kind='pretty'`
  - `users.current_display_user_id_kind='pretty'`
- 用户已有靓号时，再申请另一个靓号会覆盖旧靓号，旧靓号释放；旧池靓号回到 `available`，旧后台发放靓号进入 `released`。
- 后台发放 `VIP2026`、`ملك123` 这类英文数字或阿拉伯语数字组合可以成功。
- 后台发放不传 `duration_ms` 时，返回 `display_user_id_expires_at_ms=0`。
- 后台发放传 `duration_ms` 时，返回实际 `display_user_id_expires_at_ms`，到期后恢复默认短号。
- `GET /api/v1/users/me` 返回靓号字段。
- `GET /api/v1/users/room-display-profiles:batch` 返回靓号字段。
- 房间详情 `profiles[]` 返回靓号字段。
- 后台 `/api/v1/navigation/menus` 能看到“用户管理 > 靓号管理”。
