如何在 aiogram 3 的机器人中开通 Telegram Stars 支付功能:附代码的分步指南

在 aiogram 3 机器人中通过 Telegram 星币进行支付:账单、pre_checkout_query、支付成功、退款、支付链接及星币提现。附带说明的完整可运行代码。

阅读 4 分钟

Telegram Stars 是 Telegram 的内置虚拟货币。用户可直接在应用内购买星星,随后只需轻点两下即可在机器人中使用,无需输入银行卡信息,也无需跳转至第三方网站。 对于机器人所有者而言,这是最简便的收款方式:无需支付服务商、合同或支付令牌——一切均通过常规的 Bot API 完成。

根据 Telegram 的规定,机器人中的数字商品和服务(如访问权限、订阅、文件、游戏内道具)必须使用 Stars 进行销售。 以下是一个基于 aiogram 3 的可运行机器人,它能够开具账单、接收付款、交付商品并支持退款。

如何使用Stars进行支付

  1. 机器人向用户发送以XTR为货币单位的账单(invoice)——这是Bot API中对Stars的标识。
  2. 用户点击“支付”,Telegram会向机器人发送pre_checkout_query——这是扣款前的最后一次请求。机器人必须在10秒内对此作出响应:确认或拒绝。
  3. 确认后,Telegram 会扣除星币并发送包含 successful_payment 的消息。只有此时,付款才被视为完成——商品也将在此时交付。

所需条件

  • 通过 @BotFather 创建的机器人及其令牌。无需在 BotFather 中单独为星币启用支付功能。
  • Python 3.10 或更高版本。
  • aiogram 3 — 通过一条命令即可安装:
pip install -U aiogram

请勿将机器人令牌写入代码中——应通过环境变量或.env文件传递,该文件不会被提交到代码库。在下面的示例中,机器人会读取 BOT_TOKEN 和 ADMIN_ID——即您在 Telegram 上的 ID,该 ID 被授权接收退款。

机器人完整代码

将代码保存到 bot.py 文件中:

import asyncio
import logging
import os

from aiogram import Bot, Dispatcher, F, Router
from aiogram.exceptions import TelegramBadRequest
from aiogram.filters import Command, CommandObject, CommandStart
from aiogram.types import LabeledPrice, Message, PreCheckoutQuery

ADMIN_ID = int(os.environ["ADMIN_ID"])  # ваш Telegram ID — только ему доступен /refund

# Товар: название, описание, цена в звёздах (целое число) и метка заказа
PRODUCT_TITLE = "Доступ к курсу"
PRODUCT_DESCRIPTION = "Полный доступ ко всем урокам курса навсегда"
PRODUCT_PAYLOAD = "course_full"
PRICE_STARS = 50

router = Router()


@router.message(CommandStart())
async def start(message: Message) -> None:
    await message.answer(f"Привет! Курс стоит {PRICE_STARS} ⭐. Купить: /buy")


@router.message(Command("buy"))
async def buy(message: Message) -> None:
    await message.answer_invoice(
        title=PRODUCT_TITLE,
        description=PRODUCT_DESCRIPTION,
        payload=PRODUCT_PAYLOAD,  # вернётся в pre_checkout_query и successful_payment
        currency="XTR",           # XTR — Telegram Stars, provider_token не нужен
        prices=[LabeledPrice(label=PRODUCT_TITLE, amount=PRICE_STARS)],
    )


@router.pre_checkout_query()
async def pre_checkout(query: PreCheckoutQuery) -> None:
    # Последняя проверка перед списанием: товар существует и в наличии.
    # Ответить нужно за 10 секунд, иначе Telegram отменит оплату.
    if query.invoice_payload != PRODUCT_PAYLOAD:
        await query.answer(ok=False, error_message="Товар не найден. Создайте счёт заново: /buy")
        return
    await query.answer(ok=True)


@router.message(F.successful_payment)
async def paid(message: Message) -> None:
    payment = message.successful_payment
    if payment.invoice_payload != PRODUCT_PAYLOAD or payment.total_amount != PRICE_STARS:
        logging.warning("Неожиданный платёж: %s", payment.invoice_payload)
        return
    # charge_id сохраните в базе вместе с заказом — без него не сделать возврат
    logging.info("Оплата %s ⭐ от %s, charge_id=%s",
                 payment.total_amount, message.from_user.id, payment.telegram_payment_charge_id)
    # Здесь выдайте товар: откройте доступ, отправьте файл, начислите баланс
    await message.answer("Спасибо за покупку! Доступ к курсу открыт ✅")


@router.message(Command("refund"), F.from_user.id == ADMIN_ID)
async def refund(message: Message, command: CommandObject, bot: Bot) -> None:
    # /refund <user_id> <charge_id> — вернуть звёзды покупателю
    args = (command.args or "").split()
    if len(args) != 2 or not args[0].isdigit():
        await message.answer("Формат: /refund <user_id> <charge_id>")
        return
    try:
        await bot.refund_star_payment(user_id=int(args[0]), telegram_payment_charge_id=args[1])
    except TelegramBadRequest as e:
        await message.answer(f"Возврат не прошёл: {e.message}")
        return
    await message.answer("Звёзды возвращены покупателю")


@router.message(Command("paysupport"))
async def pay_support(message: Message) -> None:
    await message.answer("Вопросы по оплате: напишите @your_support — ответим в течение дня.")


async def main() -> None:
    logging.basicConfig(level=logging.INFO)
    bot = Bot(token=os.environ["BOT_TOKEN"])
    dp = Dispatcher()
    dp.include_router(router)
    await dp.start_polling(bot)


if __name__ == "__main__":
    asyncio.run(main())

在 Linux 或 macOS 上运行:

export BOT_TOKEN="токен_от_BotFather"
export ADMIN_ID="ваш_telegram_id"
python bot.py

向机器人发送 /buy — 系统将发送一张附有“支付 50 ⭐”按钮的账单。

代码解析

账单:answer_invoice

  • currency="XTR" — 使用星币支付。对于星币,不传递 provider_token 参数。
  • prices — 商品列表。对于 Stars,该列表中必须恰好包含一项商品,且 amount 必须是整数个星(不包含分或美分)。
  • payload — 您的内部订单标识符。 用户无法看到该标记,Telegram会在支付完成后返回该标记——您可通过它了解具体支付了什么。在本商店中,此处填写的是数据库中的订单号。

pre_checkout_query

此处用于验证商品是否仍可销售:是否已售罄、价格是否发生变化、订单是否已被提前支付。如果出现任何异常,请返回 ok=False 并附上清晰的错误信息,Telegram 会将其显示给用户。

successful_payment

主要处理流程:星币已扣除,需要发货。必须:

  • 核对 payload 和金额 是否与数据库中的订单一致;
  • 保存 telegram_payment_charge_id — 这是支付编号,用于退款和处理纠纷;
  • 仅发放一次商品 — 在数据库中将订单标记为已支付。

退还星星

refund_star_payment 方法会将星星全额退还给买家。在示例中,/refund 命令仅对管理员可用——过滤条件为 F.from_user.id == ADMIN_ID。请勿允许用户自行办理退款:否则可能会出现先收到商品又立即退款的情况。

退款后,请务必收回已发放的商品:在数据库中关闭访问权限或取消购买。

使用支付链接代替消息

如果需要将账单发布在频道、按钮或迷你应用(Web App)中,请创建支付链接:

link = await bot.create_invoice_link(
    title="Доступ к курсу",
    description="Полный доступ ко всем урокам курса",
    payload="course_full",
    currency="XTR",
    prices=[LabeledPrice(label="Доступ к курсу", amount=50)],
)

通过此链接,支付流程同样如下:机器人会收到 pre_checkout_query 和 successful_payment 消息。

/paysupport 命令

Telegram 要求负责收款的机器人必须提供清晰的支付支持说明——为此需要使用 /paysupport 命令。请通过 @BotFather → Edit Bot → Edit Commands 将其与 /buy 命令一起添加到菜单中。

如何提取赚取的星星

星星会累积在机器人的余额中。您可以在 Telegram 中的机器人个人资料(星星余额部分)查看并提取它们——提取通过 TON 中的 Fragment 进行。 星星并非立即可提现,而需经过一段锁定期;具体时限和最低提现金额请查看Telegram应用内——相关条款会不定期调整。

常见错误

  • 机器人已在 pre_checkout_query 阶段交付商品。 支付尚未完成——请将交付操作移至 successful_payment 阶段。
  • pre_checkout_query 处理程序无响应或运行时间超过 10 秒。 这将导致用户端出现错误并取消支付。请勿在此处执行耗时过长的请求。
  • prices 中包含多个商品项。 XTR 仅允许一个商品项。
  • 缺少 charge_id 字段。 缺少该字段将无法办理退款。
  • 代码中包含令牌。 请将其保存在 .env 文件中,切勿发布到代码仓库。

其他支付方式——通过 CryptoBot 接收加密货币,或 xRocket——相关操作已在单独的指南中详细说明,所有支付方式的概述请参阅文章《如何在 Telegram 机器人中接收付款》。

常见问题

使用Stars支付是否需要provider_token?

不需要。对于XTR货币,无需支付服务商,因此不传递provider_token参数。

如何在不花钱的情况下测试Stars支付?

购买少量星星,将价格设为 1 ⭐,完成支付后使用 /refund 命令申请退款——星星将返还至您的余额中。

能否用星星支付实物商品?

星星专用于数字商品和服务。 若需销售实物商品,请使用支付服务商或其他支付方式。

如何从机器人中提取星星?

通过机器人个人资料中的星星余额进行操作——在锁定期结束后,将通过 TON 中的 Fragment 进行提现。 具体条款请参阅 Telegram。

为什么点击“支付”后交易立即被取消?

机器人未在 10 秒内响应 pre_checkout_query,或返回了 ok=False。请检查处理程序是否已连接且运行迅速。

相关现成解决方案