---
title: "Telegram 内置广告投放分析"
description: "如何使用唯一的 Telegram 链接衡量广告投放效果：频道邀请链接、机器人和 Mini App 的 start 与 startapp 参数，以及归因实现示例。"
outline: [2,3]
---
# Telegram 内置分析
Telegram 提供了借助唯一链接分析广告投放效果的工具。您可以为每个广告投放创建单独的链接。频道和群组的加入统计可直接在 Telegram 中查看，而机器人和 Mini App 则需要在项目端额外配置链接参数的保存和数据收集。

> [!WARNING] 注意
> 一个广告投放 = 一个唯一链接。如果在多个广告中使用同一个链接，流量来源会混在一起，事后无法再区分。

这种分析方式独立于 AdsGram 的广告活动统计和转化追踪。

## 第 1 部分：面向广告主一方的广告活动经理

### 1. 应该使用哪种链接？
要比较不同广告投放的效果，请为每个投放使用唯一的链接。

例如，如果您在三个 Telegram 频道中推广项目，请创建三个单独的链接。这样就能确定哪些投放带来了更多用户。

链接类型取决于广告的跳转目标：

| 广告跳转目标 | 链接格式 | 在哪里追踪结果 |
|---|---|---|
| Telegram 频道或群组 | 单独的邀请链接 | Telegram：频道或群组设置 → `Invite links` |
| 机器人 | `t.me/YourBot?start=ad_sep_anna` | 您的机器人（如果它保存该参数） |
| Mini App | `t.me/YourBot?startapp=ad_sep_anna` | 您的应用服务器（如果它保存该参数） |

将 `YourBot` 替换为您的机器人用户名，将 `ad_sep_anna` 替换为该广告投放的唯一参数。

### 2. 频道和群组：邀请链接
为每个广告投放创建单独的邀请链接。

1. 打开频道或群组，点击其名称。
2. 进入编辑或管理设置。在桌面版中，可以通过 ⋮ → `Manage channel` / `Manage group` 进入。
3. 打开 `Invite links`，选择创建新链接的选项。
4. 按投放为链接命名，例如 `ad_sep_anna`。该名称仅对管理员可见，打开链接的用户看不到。
5. 如有需要，可设置有效期、用户数量上限，或要求管理员审批加入申请。
6. 每个链接只在一个广告投放中使用。

**在哪里查看统计：** Telegram 的 `Invite links` 页面会显示通过每个链接加入的用户数量。打开某个链接即可查看用户列表。

`t.me/yourchannel` 这样的普通公开链接无法按广告投放区分加入来源。公开频道同样可以创建邀请链接，因此请在广告中使用单独的邀请链接。

### 3. 机器人：带 `start` 参数的链接
使用以下链接格式：

```
https://t.me/<bot>?start=<parameter>
```

例如：

```
https://t.me/YourBot?start=ad_sep_anna
```

当用户打开链接并点击 `Start` 时，机器人会收到带参数的命令：

```
/start ad_sep_anna
```

Telegram 不会自动统计这些机器人启动的广告归因。机器人必须在首次创建用户记录时保存该参数。

如果您的指标是新用户获取，请只统计新用户。现有用户打开广告链接并不会因此成为新用户。

### 4. Mini App：带 `startapp` 参数的链接
对于机器人的主 Mini App，请使用：

```
https://t.me/<bot>?startapp=<parameter>
```

对于通过 `@BotFather` 中的 `/newapp` 创建的 Mini App，可以使用：

```
https://t.me/<bot>/<app>?startapp=<parameter>
```

例如：

```
https://t.me/YourBot?startapp=ad_sep_anna
```

如有需要，可以添加显示模式参数：
- `&mode=compact` — 紧凑模式。
- `&mode=fullscreen` — 全屏模式。

Mini App 会在 `start_param` 字段中收到该参数。您的应用服务器必须在用户首次启动应用时保存它。

如果用户不是通过广告链接打开 Mini App（例如通过菜单按钮或机器人资料页），则可能没有广告参数。除非有其他可靠的归因来源，否则应将此类启动视为自然流量。

### 5. 参数规则
- 最大长度：
    - `start` 参数 — **64 个字符**
    - `startapp` 参数 — **512 个字符**
- 允许的字符：
    - `A-Z`、`a-z`、`0-9`、`_`、`-`
    - 不要使用空格、西里尔字母或句点。为避免兼容性问题，请只使用允许的字符集。
- 不要在参数中传递用户的个人数据。
- 推荐的命名规则。使用统一的命名格式，便于在报告中识别各个投放：`ad_<month>_<source>_<creative>`

  示例：
    - `ad_sep_tgads_video1`
    - `ad_sep_blogger_anna`

### 6. 广告链接不是推荐链接
**推荐链接**让用户可以邀请他人并获得奖励。

**广告链接**用于衡量来自某个广告投放的流量。它本身不应触发推荐奖励。

使用前缀区分这两类链接：
- `ref_…` — 用户推荐链接。
- `ad_…` — 广告投放链接。

为每个广告活动单独创建一个推荐链接只是临时方案。这可能导致为广告流量发放推荐奖励，并使统计数据混在一起。

### 7. 如何统计点击
Telegram 不提供 `t.me` 链接的点击统计。

如果需要统计点击，请先让用户经过您自己的重定向，再跳转到 Telegram。

示例：

```
https://yourdomain.com/go/ad_sep_anna → https://t.me/YourBot?start=ad_sep_anna
```

您的服务器记录点击，并将用户重定向到目标链接。这需要您自行实现，并且不能替代对启动、新用户或频道加入的统计。

### 8. 维护结果表格
创建一个表格，每个唯一链接占一行。

推荐的列：

| 字段 | 记录内容 |
|---|---|
| 链接参数 | 例如 `ad_sep_anna` |
| 投放 | 频道、帖子、博主或广告名称 |
| 日期 | 投放周期 |
| 花费 | 广告费用 |
| 新用户 | 归因于该链接的新用户数量 |
| 单用户成本 | 花费 ÷ 新用户数量 |

### 9. 上线前检查清单
发布广告前，请确认：
- 每个投放都有自己的链接；链接没有在多个投放中重复使用。
- 参数未超过最大长度，且只包含允许的字符。
- 已用第二个账号测试链接：加入被统计，或参数已被机器人或 Mini App 保存。
- 链接、投放名称和花费已记录到表格中。

## 第 2 部分：面向开发者的实现
以下示例使用 Python、SQL，机器人部分使用 `aiogram 3`。同样的原则也可以用其他技术栈实现。

这些代码只是实现基础，而不是可直接使用的完整集成。数据库访问函数和部分集成细节需要根据您的应用进行调整。

### 10. 需要存储哪些数据
归因需要三类实体：
1. 广告链接和广告活动的目录。
2. 用户的获客来源，在创建用户记录时保存。
3. 广告链接访问历史，包括现有用户的访问。

SQL 结构示例：

```sql
-- 每个广告链接一条记录。
-- 例如，参数 "ad_sep_anna" 对应代码 "sep_anna"。
CREATE TABLE campaigns (
    code       text PRIMARY KEY,
    name       text NOT NULL,
    created_at timestamptz NOT NULL DEFAULT now()
);

-- 用户的获客来源在创建时设置。
-- NULL 表示自然流量用户。
ALTER TABLE users
ADD COLUMN campaign text REFERENCES campaigns (code);

-- 所有打开过广告链接的用户，
-- 包括新用户和现有用户。
CREATE TABLE campaign_visits (
    code     text REFERENCES campaigns (code),
    user_id  bigint,
    first_at timestamptz NOT NULL DEFAULT now(),
    PRIMARY KEY (code, user_id)
);
```

`code` 字段应包含简短的广告活动代码，例如 `sep_anna`。`name` 字段用于描述投放，例如 `Anna 的频道，九月帖子`。

`campaign_visits` 表用于将访问记录与用户的原始获客来源分开统计。不应在用户每次打开其他链接时覆盖其原始来源。

### 11. 解析参数：机器人和 Mini App 共用一个解析器
对广告参数和推荐参数使用同一个解析器。

```python
import re

AD = re.compile(r"ad_([A-Za-z0-9_-]{1,61})")
REF = re.compile(r"ref_([A-Za-z0-9]{4,32})")


def parse_start(param: str | None) -> tuple[str, str] | None:
    """返回 ("ad", code)、("ref", code)，其他情况返回 None。"""
    if not param:
        return None

    if m := AD.fullmatch(param):
        return "ad", m[1]

    if m := REF.fullmatch(param):
        return "ref", m[1]

    return None
```

解析后，检查得到的代码是否存在于 `campaigns` 表中。
- 不要将未知代码归因到现有的广告活动。应将此类访问视为自然流量或未归因流量。
- 不要保存未经校验的原始参数值。
- 对机器人和 Mini App 收到的参数使用相同的解析规则。

### 12. 机器人：处理 `/start`
用户启动机器人时，Telegram 会在 `/start` 命令的参数中传递该参数。

使用 `aiogram 3` 的示例：

```python
@router.message(CommandStart())
async def start(message: Message, command: CommandObject):
    source = parse_start(command.args)
    # "/start ad_sep_anna" -> ("ad", "sep_anna")

    async with db.transaction():
        await create_user_if_new(message.from_user, source)

        if source and source[0] == "ad":
            await record_visit(source[1], message.from_user.id)
```

`create_user_if_new` 函数只应在用户尚不存在时创建用户记录。

为防止重复记录，请使用原子操作，例如：

```sql
-- create_user_if_new：原子操作，机器人和 Mini App 同时请求时也安全
INSERT INTO users (id, campaign, referrer_id)
VALUES ($1, $2, $3)
ON CONFLICT (id) DO NOTHING
RETURNING id; -- 返回一行 = 新用户；未返回 = 现有用户，保持不变
```

如果查询返回了一行，说明用户已创建。如果没有返回任何行，说明用户已存在，其原始获客来源必须保持不变。

记录广告链接访问可以使用：

```sql
-- record_visit
INSERT INTO campaign_visits (code, user_id)
VALUES ($1, $2)
ON CONFLICT DO NOTHING;
```

如果现有用户打开了另一个链接并收到 `/start ad_…`，请记录这次访问，但**不要覆盖**其 `campaign` 字段。

对于推荐归因，只为新用户设置 `referrer_id`。不允许用户推荐自己。

### 13. Mini App：处理首次启动
Mini App 可以在 Telegram Web App 启动数据中收到 `start_param` 参数。

在前端，将 Telegram 签名的数据发送到您的服务器：

```javascript
// 前端：每个请求都携带 Telegram 签名的初始化数据
fetch("/api/me", { headers: { Authorization: `tma ${Telegram.WebApp.initData}` } })
```

在服务器端，使用用户 ID 或 `start_param` 之前，必须先校验 `initData` 的真实性及其时效。

Python 校验函数示例：

```python
import hashlib, hmac, json, time
from urllib.parse import parse_qsl


def check_init_data(
    raw: str,
    bot_token: str,
    max_age: int = 86400
) -> dict:
    fields = dict(
        parse_qsl(
            raw,
            keep_blank_values=True,
            strict_parsing=True
        )
    )

    received = fields.pop("hash", "")
    # 除 hash 外的所有字段（包括 "signature"），按顺序排序，每行一个 key=value
    data = "\n".join(
        f"{k}={v}" for k, v in sorted(fields.items())
    )

    secret = hmac.new(
        b"WebAppData",
        bot_token.encode(),
        hashlib.sha256
    ).digest()

    expected = hmac.new(
        secret,
        data.encode(),
        hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(expected, received):
        raise PermissionError("bad signature")

    if time.time() - int(fields["auth_date"]) > max_age:
        raise PermissionError("expired")

    return {
        "user": json.loads(fields["user"]),
        "start_param": fields.get("start_param")
    }
```

校验成功后，按照与机器人相同的流程处理（`create_user_if_new` 和 `record_visit`）：
1. 从已校验的数据中获取 `user` 和 `start_param`。
2. 使用 `parse_start` 解析参数。
3. 仅在用户尚不存在时创建用户。
4. 如果参数对应已知的广告活动，记录广告链接访问。

**安全提示：** 只使用经过校验的 `initData`。不要将 `initDataUnsafe` 或 URL 参数作为用户身份或归因的来源，因为它们可以被伪造。

`start_param` 参数会在通过相应链接启动应用时传递。之后从菜单启动时可能不再包含该参数。因此，请在创建用户时设置原始获客来源，而不是在每次启动应用时设置。

### 14. 一个用户，两个入口
机器人和 Mini App 可以共用一个 `users` 表，用户通过 Telegram user ID 识别。

例如，用户可能先启动机器人，再打开 Mini App，或者反过来。

归因规则：
- 第一个创建用户记录的处理程序设置原始获客来源。
- 第二个处理程序不得覆盖该来源。
- 之后的访问可以单独记录在 `campaign_visits` 中。

使用原子插入，例如 `INSERT … ON CONFLICT DO NOTHING`。这样可以避免机器人和 Mini App 的请求几乎同时到达时出错。

### 15. 频道：按邀请链接统计加入
如果您需要自己统计加入情况，而不只依赖 Telegram 界面中的统计，请为每个广告活动创建单独的邀请链接，并与广告活动代码一起保存。

Python 创建链接示例：

```python
# 为每个广告活动创建链接，并与代码一起保存
link = await bot.create_chat_invite_link(
    CHANNEL_ID,
    name="ad_sep_anna"
)  # 名称：1–32 个字符
save_channel_link("ad_sep_anna", link.invite_link)
```

本示例中邀请链接名称限制为 1–32 个字符。

要接收加入事件，机器人必须是频道管理员，并接收相应的 `chat_member` 更新。如果加入申请需要审批，还必须接收 `chat_join_request` 更新。

polling 配置示例：

```python
# 加入事件以 chat_member 更新的形式到达：机器人必须是管理员并请求这些更新
await dp.start_polling(
    bot,
    allowed_updates=[
        "message",
        "chat_member",
        "chat_join_request"
    ]
)
```

加入处理示例：

```python
@router.chat_member()
async def member(update: ChatMemberUpdated):
    joined = (
        update.old_chat_member.status in ("left", "kicked")
        and update.new_chat_member.status == "member"
    )

    if joined and update.invite_link:
        save_channel_join(
            update.invite_link.name,
            update.new_chat_member.user.id
        )
```

如果链接需要管理员审批，请处理 `chat_join_request`。邀请链接可在 `update.invite_link` 中获取；审核申请后，可以调用 `approve_chat_join_request`。

您也可以记录用户退出频道的情况，以了解哪些用户留了下来。

Telegram 已经向管理员显示每个邀请链接的加入人数。如果您希望将频道加入数据与机器人用户信息或其他分析数据结合起来，自建数据库会很有用。

### 16. 可选：通过重定向统计点击
要在用户进入 Telegram 之前统计点击，请创建您自己的重定向端点。

Mini App 的 Python 示例：

```python
@app.get("/go/{code}")
async def go(code: str, request: Request):
    agent = request.headers.get("user-agent", "")

    if code_exists(code) and "TelegramBot" not in agent:  # 忽略 Telegram 用于生成链接预览的爬虫
        record_click(code)

    return RedirectResponse(
        f"https://t.me/{BOT}?startapp=ad_{code}",
        status_code=302
    )
```

该示例会校验代码，并排除 User-Agent 中包含 `TelegramBot` 的请求，以免将 Telegram 链接预览请求计为普通用户点击。

请根据您的基础设施调整实现，并使用真实链接进行测试。重定向必须将用户引导到带有相应参数的正确机器人或 Mini App 链接。

### 17. 报告示例
以下查询按广告活动统计广告链接的独立访问者、新用户和已激活用户数量。

```sql
SELECT
    c.code,
    c.name,
    (
        SELECT count(*)
        FROM campaign_visits v
        WHERE v.code = c.code
    ) AS visitors,
    (
        SELECT count(*)
        FROM users u
        WHERE u.campaign = c.code
    ) AS new_users,
    (
        SELECT count(*)
        FROM users u
        WHERE u.campaign = c.code
          AND u.first_action_at IS NOT NULL
    ) AS active
FROM campaigns c
ORDER BY new_users DESC;
-- first_action_at：用户首次完成广告活动付费的目标操作（任务、购买）的时间
```

在本示例中，`first_action_at` 是用户首次完成广告主付费的目标操作（例如完成任务或进行购买）的时间。

`visitors` 指标表示在 `campaign_visits` 中记录了对应广告活动访问的用户。除非通过重定向单独记录点击，否则它不等于链接的点击次数。

### 18. 常见问题和限制
- **只有用户点击 `Start` 后，机器人才会收到参数。** 并不是每个点击链接的人都会启动机器人，因此启动次数可能少于点击次数。
- **重新启动不代表新用户。** 如果用户删除聊天后再次启动机器人，参数可能会再次传递，但该用户并不是新用户。
- **不要覆盖原始获客来源。** 使用带时间戳的单独访问表来分析后续访问。
- **区分新账号和现有账号。** 如果目标是获客，重复访问不应增加新用户数量。
- **排除机器人和自我推荐。** 检查 `is_bot`，不允许用户推荐自己。
- **遵守参数限制。** `start` 参数的总长度不得超过 64 个字符。使用 `sep_anna` 这样的简短代码，为 `ad_` 或 `ref_` 前缀留出空间。
- **将推荐归因和广告归因分开。** 使用不同的前缀和处理规则，有助于避免错误的奖励发放和报告。
