# Multi App Tenant Architecture

本文定义 `hyapp-server` 同时服务多个 App 时的租户边界。当前后端不再默认只服务单个 App；`popoo`、`lalu`、`siya` 这类产品都必须先解析成内部 `app_code`，再进入用户、房间、钱包、活动和 host 业务链路。

## Core Rule

`app_code` 是后端内部租户键。客户端包名、Bundle ID、渠道名和展示 App 名都不能直接作为业务表隔离键。

首个内置映射：

| package_name | platform | app_code | status |
| --- | --- | --- | --- |
| `com.org.laluparty` | 任意或空 | `lalu` | `active` |
| `com.app.huwaa` | 任意或空 | `huwaa` | `active` |
| `com.chat.fami` | 任意或空 | `fami` | `active` |

新增 App 时，只新增 `apps` 注册表记录和对应 App 的基础配置/种子数据，不新建一套服务，也不复制业务代码。

## App Registry

`apps` 表由 `user-service` 持有，放在 `hyapp_user` 库：

```sql
apps(
  app_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
  app_code VARCHAR(32) NOT NULL,
  app_name VARCHAR(128) NOT NULL,
  package_name VARCHAR(191) NOT NULL,
  platform VARCHAR(32) NOT NULL DEFAULT '',
  status VARCHAR(32) NOT NULL,
  created_at_ms BIGINT NOT NULL,
  updated_at_ms BIGINT NOT NULL,
  UNIQUE KEY uk_apps_code (app_code),
  UNIQUE KEY uk_apps_package_platform (package_name, platform)
);
```

`gateway-service` 可以通过包名和平台解析 `app_code`，也可以校验显式传入的 `app_code`。解析入口是 `user-service.AppRegistryService.ResolveApp`。

## Request Flow

```mermaid
sequenceDiagram
  participant C as Client
  participant G as gateway-service
  participant U as user-service
  participant R as room-service / wallet-service / activity-service

  C->>G: HTTP request + package/platform/app_code header
  G->>U: ResolveApp(package_name, platform, app_code)
  U-->>G: active app_code
  G->>G: write request context and JWT claims
  G->>R: gRPC RequestMeta.app_code
  R->>R: all reads and writes are scoped by app_code
```

Header 约定：

| Header | Meaning |
| --- | --- |
| `X-App-Code` / `X-HY-App-Code` / `App-Code` | 已知内部 `app_code` |
| `X-App-Package` / `X-Package-Name` / `X-App-Bundle-ID` / `X-Bundle-ID` | 客户端包名或 Bundle ID |
| `X-App-Platform` / `X-Platform` | `android` 或 `ios` |

已登录请求以 JWT 里的 `app_code` 为准。未登录登录/注册请求允许通过包名解析；解析成功后，`user-service` 签发的 token 必须带同一个 `app_code`。

## App Scoped IDs

对客户端暴露的用户长 ID 和房间长 ID 都带 App 前缀，格式为：

```text
<app_code>_<uuid>
```

例如 `com.org.laluparty` 解析到 `lalu` 后，新用户的 `user_id` 是数值型主键，新房间的 `room_id` 仍按 App 生成 `lalu_<uuid>`。服务间主关联使用 `users.user_id`，JWT 只携带数值型 `user_id`，gateway 直接把该 ID 透传给后端 RPC。

用户短号 `display_user_id` 按 App 独立发号，首个号段从 `163000` 开始。创建房间时 `room_short_id` 等于创建者当前短号，用于客户端搜索、分享和展示。

## Data Boundary

以下业务库表都必须有 `app_code`，并且查询、唯一键和幂等键都必须带 `app_code`：

| Service | Database | Scope |
| --- | --- | --- |
| `user-service` | `hyapp_user` | `users`、登录身份、session、国家、区域、短号、host/Agency/BD 事实 |
| `room-service` | `hyapp_room` | 房间元数据、房间列表、快照、命令日志、room outbox |
| `wallet-service` | `hyapp_wallet` | 账户、交易、分录、wallet outbox、礼物价格 |
| `activity-service` | `hyapp_activity` | 活动状态、事件消费幂等、activity outbox |

规则：

- 跨 App 可以存在相同 `display_user_id`、相同三方 `provider_subject`、相同钱包 `command_id`；对外长 ID 自带 `app_code` 前缀，仍然要求落库唯一键带 `app_code`。
- 业务查询不能只用 `user_id`、`room_id` 或 `command_id`；必须同时带 `app_code`。
- outbox envelope 必须带 `app_code`，消费者按事件所属 App 落库和幂等。
- 默认 `lalu` 只用于本地开发和历史测试兜底；真实客户端请求必须能解析出明确 App。

## Admin Boundary

后台管理后端使用独立 `hyapp_admin` 库，不把后台审计表写进 App 业务库。后台操作某个 App 的用户、区域、host、钱包或房间时，HTTP 请求必须显式携带或由后台上下文选择 `app_code`，再调用对应业务能力。

`hyapp_admin.admin_operation_logs` 可以在 `detail` 中记录 `app_code`、目标对象和变更摘要；不要为了审计把 admin 表复制到各 App 业务库。

## Adding A New App

以新增 `popoo` 为例：

1. 在 `apps` 表插入 `app_code=popoo`、包名、平台和状态。
2. 为 `popoo` 初始化国家、区域、礼物价格、登录 provider/Firebase 配置。
3. 客户端请求携带包名和平台，gateway 解析到 `popoo`。
4. 注册、房间、钱包、活动、host 数据自动按 `app_code=popoo` 写入现有表。

新增 App 不需要拆新微服务；只有当业务规则、部署生命周期或数据合规要求必须强隔离时，才考虑单独部署一套服务集群。
