# Telegram 付费群门禁前端对接文档

本文档基于 `feature/tgbot` 对应的“增加 chat gate”改动（当前 `test` 分支提交 `483351f8`）整理，面向 App/H5 前端。

本次新增 Telegram 付费群入口：符合资格的用户可在前端按需获取个人邀请链接，跳转 Telegram 提交入群申请；后端机器人校验 Splay 与 Telegram 账号绑定关系后自动审批。

## 1. 前端改动总览

| 接口 | 用途 | 调用时机 |
| --- | --- | --- |
| `POST /v1/user/telegram/group/access/status/get` | 查询入口、申请和入群状态，不创建邀请链接 | 进入相关页面、App 回到前台、等待审批时 |
| `POST /v1/user/telegram/group/join-link/create` | 创建或复用当前用户的邀请链接 | 用户主动点击“加入 Telegram 群”时 |

前端只需要接入以上两个接口。机器人审批、账号绑定、邀请链接失效和未授权成员移除均由后端处理。

## 2. 通用约定

- 两个接口均为登录态 `POST` 请求，请继续携带现有的登录凭证、签名头和设备头。
- 请求体均为空对象 `{}`。
- JSON 字段使用 `snake_case`。
- `status` 按当前网关配置返回枚举字符串。
- `expires_at` 是 Unix 秒。它是 Protobuf `int64`，JSON 中通常为字符串；前端应兼容 `string | number`。
- 响应启用了零值输出，无链接时仍可能返回 `"link": ""`、`"expires_at": "0"`。
- 资格不足、功能关闭、等待审批和已经入群都不是接口异常，均通过 HTTP 200 响应中的 `status` 表达。

邀请链接与用户一一对应且有效期较短，不要写入埋点、日志或分享渠道。前端不需要、也不应持有 Telegram Bot Token 和目标群 `chat_id`。

## 3. 用户资格

入口资格由后端判断，前端不要根据订单列表自行计算。当前条件为同时满足：

1. Splay 用户状态正常；
2. 至少存在一笔金额大于 0、支付已完成的外部支付订单。

当前计入的支付渠道包括 Apple、Google、Stripe、In The Bag、Moneta Pay、EZ、HaiPay、FuturePay、Jaya、UnisPay 和 Bank。业务规则以后端返回状态为准，前端无需固化这份渠道列表。

## 4. 状态枚举与 UI 行为

| `status` | 数字值 | `show_entry` | `can_request_link` | 含义 | 前端建议 |
| --- | ---: | --- | --- | --- | --- |
| `TELEGRAM_GROUP_ACCESS_STATUS_UNKNOWN` | 0 | `false` | `false` | 未知状态，正常不应出现 | 隐藏入口并记录异常 |
| `TELEGRAM_GROUP_ACCESS_STATUS_DISABLED` | 1 | `false` | `false` | 功能未启用或配置不完整 | 隐藏入口 |
| `TELEGRAM_GROUP_ACCESS_STATUS_NOT_ELIGIBLE` | 2 | `false` | `false` | 当前用户不符合入群资格 | 隐藏入口 |
| `TELEGRAM_GROUP_ACCESS_STATUS_AVAILABLE` | 3 | `true` | `true` | 可以申请邀请链接 | 展示可点击的入群入口 |
| `TELEGRAM_GROUP_ACCESS_STATUS_LINK_READY` | 4 | `true` | `true` | 已有未过期链接可复用 | 展示可点击的入群入口；点击后仍调用创建接口取链接 |
| `TELEGRAM_GROUP_ACCESS_STATUS_PENDING` | 5 | `true` | `false` | 已在 Telegram 提交入群申请，等待机器人完成审批 | 展示“处理中”，禁用重复申请 |
| `TELEGRAM_GROUP_ACCESS_STATUS_JOINED` | 6 | `false` | `false` | 已经在目标群内 | 按返回字段隐藏入口 |

前端应优先使用后端返回的 `show_entry` 和 `can_request_link` 控制 UI，不要重复推导：

```ts
type TelegramGroupAccess = {
  status: string;
  show_entry: boolean;
  can_request_link: boolean;
};

function telegramEntryState(data: TelegramGroupAccess) {
  return {
    visible: data.show_entry,
    disabled: !data.can_request_link,
    pending:
      data.status === "TELEGRAM_GROUP_ACCESS_STATUS_PENDING",
  };
}
```

`PENDING` 可能持续时间很短，机器人审批较快时，前端可能直接从 `LINK_READY` 看到 `JOINED`，不要依赖一定观察到中间状态。

## 5. 查询 Telegram 群入口状态

```http
POST /v1/user/telegram/group/access/status/get
```

请求体：

```json
{}
```

可申请时的响应示例：

```json
{
  "status": "TELEGRAM_GROUP_ACCESS_STATUS_AVAILABLE",
  "show_entry": true,
  "can_request_link": true
}
```

等待审批时的响应示例：

```json
{
  "status": "TELEGRAM_GROUP_ACCESS_STATUS_PENDING",
  "show_entry": true,
  "can_request_link": false
}
```

注意：本接口只查询状态，不创建邀请链接。即使返回 `LINK_READY`，响应中也不包含链接；用户点击入口后仍需调用下一节的创建接口。

建议在以下时机重新查询：

- 进入包含 Telegram 群入口的页面；
- 从 Telegram 返回 App/H5、页面重新获得焦点时；
- 创建链接并跳转 Telegram 后，短时间等待审批时；
- 用户手动点击“刷新状态”时。

如果需要自动刷新，建议前台每 3 秒查询一次，最多持续 30 秒；页面退到后台后停止轮询，回到前台立即查询一次。

## 6. 创建或复用邀请链接

```http
POST /v1/user/telegram/group/join-link/create
```

只在用户主动点击入口且 `can_request_link = true` 时调用。请求期间应禁用按钮，避免重复点击。

请求体：

```json
{}
```

成功取得链接：

```json
{
  "status": "TELEGRAM_GROUP_ACCESS_STATUS_LINK_READY",
  "link": "https://t.me/+exampleInviteCode",
  "expires_at": "1786781400"
}
```

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `status` | enum string | 创建后的最新业务状态，必须重新判断，不能假定一定是 `LINK_READY` |
| `link` | string | 当前用户的 Telegram 邀请链接；仅 `LINK_READY` 时使用 |
| `expires_at` | `string \| number` | 链接过期时间，Unix 秒；仅 `LINK_READY` 时有效 |

处理逻辑：

1. 判断响应 `status`；
2. 仅当状态为 `LINK_READY` 且 `link` 非空时，使用系统浏览器或 Telegram Deep Link 能力打开 `link`；
3. 用户需要在 Telegram 页面点击“申请加入”；打开链接本身不代表已经入群；
4. 返回 App/H5 后重新查询入口状态；
5. 如果链接已过期，再次调用本接口获取新链接，不要继续使用前端缓存。

示例：

```ts
const LINK_READY = "TELEGRAM_GROUP_ACCESS_STATUS_LINK_READY";

async function joinTelegramGroup() {
  const result = await api.post("/v1/user/telegram/group/join-link/create", {});

  if (result.status === LINK_READY && result.link) {
    openExternalUrl(result.link);
    return;
  }

  // 资格、审批或成员状态可能已发生变化，按最新状态刷新页面。
  applyTelegramGroupStatus(result.status);
}
```

本接口具备复用语义：已有未过期邀请链接时返回原链接；并发创建时后端只保留一个有效链接。但前端仍应在请求期间禁用按钮，避免无意义的重复请求。

## 7. 创建接口的非链接响应

用户点击按钮到请求到达后端之间，状态可能已经变化。因此，HTTP 200 不代表一定能获得链接：

```json
{
  "status": "TELEGRAM_GROUP_ACCESS_STATUS_PENDING",
  "link": "",
  "expires_at": "0"
}
```

| 返回状态 | 前端处理 |
| --- | --- |
| `DISABLED` / `NOT_ELIGIBLE` / `JOINED` | 关闭加载态，按状态响应中的最新语义隐藏入口 |
| `PENDING` | 展示“入群申请处理中”，开始有限轮询或提供刷新按钮 |
| `LINK_READY` | 打开非空 `link` |
| `AVAILABLE` / `UNKNOWN` | 不打开空链接，重新查询状态并提供重试 |

## 8. 完整交互流程

```text
页面加载
  → 查询 access/status/get
  → show_entry = false：隐藏入口
  → show_entry = true：展示入口
      → can_request_link = false：展示“处理中”，禁止点击
      → can_request_link = true：允许点击

用户点击入口
  → 调用 join-link/create
  → LINK_READY + link：跳转 Telegram
  → 其他状态：不跳转，按最新状态刷新 UI

Telegram 内
  → 用户点击“申请加入”
  → 机器人校验资格及账号绑定
  → 校验通过：自动批准并私聊成功消息
  → 校验失败：拒绝申请并私聊失败原因

用户回到前端
  → 再次查询 access/status/get
  → PENDING：短时间轮询
  → JOINED：隐藏入口
  → LINK_READY / AVAILABLE：仍可重新申请
```

## 9. Telegram 账号绑定限制

系统维护 Splay 账号与 Telegram 账号的一对一绑定：

- 一个 Splay 账号不能绑定多个 Telegram 账号；
- 一个 Telegram 账号不能绑定多个 Splay 账号。

绑定冲突、链接过期或资格失效发生在 Telegram 申请阶段，前端接口不会返回对应的专用错误码。机器人会在 Telegram 中拒绝申请并私聊具体原因。前端可统一提示用户：

> 如果入群申请被拒绝，请查看 Telegram 中机器人发送的消息，并确认当前 Telegram 账号是否已经绑定其他 Splay 账号。

## 10. 接口错误处理

统一错误响应示例：

```json
{
  "code": 11003,
  "message": "服务器内部错误"
}
```

| HTTP 状态 | `code` | 场景 | 前端处理建议 |
| --- | ---: | --- | --- |
| `401` | `11004` `CODE_UNAUTHORIZED` | 登录失效 | 走现有重新登录流程 |
| `500` | `11009` `CODE_DATABASE` | 查询或保存状态失败 | Toast“加载失败，请稍后重试”，保留重试入口 |
| `503` | `11003` `CODE_INTERNAL_SERVER` | Telegram 服务、机器人客户端或邀请链接创建暂不可用 | Toast“Telegram 服务暂不可用，请稍后重试” |

前端应以 `code` 为准做分支，`message` 只用于兜底展示。通用签名错误、限流和网络异常继续使用现有全局处理逻辑。

## 11. 联调验收清单

- 功能关闭时返回 `DISABLED`，入口不展示。
- 无符合条件订单的用户返回 `NOT_ELIGIBLE`，入口不展示。
- 符合资格且未入群的用户返回 `AVAILABLE`，入口可点击。
- 点击后返回 `LINK_READY`、非空 `https://t.me/+...` 链接和有效 `expires_at`。
- 有效期内重复点击返回可复用链接，不产生多个前端入口状态。
- 链接过期后可以重新获取新链接。
- 在 Telegram 点击申请后能够被机器人自动批准。
- 回到前端重新查询最终返回 `JOINED`，入口隐藏。
- 用户主动退群后重新查询返回 `AVAILABLE`，可再次获取链接。
- 使用已绑定其他 Splay 账号的 Telegram 账号申请时，Telegram 收到拒绝和说明消息。
- App/H5 使用系统能力打开 Telegram 链接，不在内部 WebView 中拦截 `t.me` 跳转。
