Aiogram 3.3.0 调用 send_invoice() 无响应
结论
send_invoice() 并非没有执行,而是在调用 Telegram Bot API 时抛出了异常。当前代码至少有以下几个问题:
provider_token='TEST_TOKEN'是占位字符串,必须换成 BotFather 为支付提供商生成的真实 token。suggested_tip_amounts=[100*100, 300*100]中的30000超过了max_tip_amount=10000,Telegram 会拒绝这张账单。- 发送账单时使用的
payload是test-invoice-payload,预结账处理器却只接受test_payload。即使账单成功发出,付款也会在预结账阶段被拒绝。 bot=Bot把类对象设成了默认值,不是规范的依赖注入声明。这里应写成bot: Bot。- 金额采用货币的最小单位。对于 USD,
299 * 100表示$299.00,而不是$2.99。 - 当前的异常记录方式没有输出 traceback,很容易让人误以为程序完全没有报错。
正确的 Bot 注入方式
Aiogram 3 会通过 Dispatcher 注入当前的 Bot 实例,处理器应这样声明:
async def pay(message: Message, bot: Bot):
...
不要写成:
async def pay(message: Message, bot=Bot):
...
后一种写法把 Bot 类本身设成了默认值。虽然 Dispatcher 通常会根据参数名注入 bot,但这种声明的含义仍然不正确,也会给脱离 Dispatcher 的调用或测试带来难以判断的问题。
如果处理器只需要回复当前聊天,也可以直接调用 message.answer_invoice(),避免处理 Bot 注入。
修正后的示例
import logging
from aiogram import Bot, F, Router
from aiogram.filters import Command
from aiogram.types import (
LabeledPrice,
Message,
PreCheckoutQuery,
)
router = Router()
logger = logging.getLogger(__name__)
INVOICE_PAYLOAD = "test-invoice-payload"
@router.message(Command("payment"))
async def pay(message: Message, bot: Bot):
await message.answer("Your order payment")
try:
await bot.send_invoice(
chat_id=message.chat.id,
title="Subscription",
description="One month subscription",
payload=INVOICE_PAYLOAD,
provider_token="REAL_PROVIDER_TOKEN_FROM_BOTFATHER",
currency="USD",
prices=[
LabeledPrice(
label="Sub month",
amount=299, # $2.99
)
],
max_tip_amount=1000, # $10.00
suggested_tip_amounts=[100, 300], # $1.00、$3.00
start_parameter="testpaymentbot",
need_email=True,
need_phone_number=True,
need_name=True,
need_shipping_address=False,
is_flexible=False,
protect_content=True,
)
except Exception:
logger.exception("Failed to send invoice")
await message.answer(
"创建付款账单失败,请查看服务端日志中的 Telegram API 错误。"
)
@router.pre_checkout_query()
async def process_pre_checkout_query(
pre_checkout_query: PreCheckoutQuery,
bot: Bot,
):
if pre_checkout_query.invoice_payload != INVOICE_PAYLOAD:
await bot.answer_pre_checkout_query(
pre_checkout_query.id,
ok=False,
error_message="Invalid invoice payload",
)
return
await bot.answer_pre_checkout_query(
pre_checkout_query.id,
ok=True,
)
@router.message(F.successful_payment)
async def successful_payment(message: Message):
payment = message.successful_payment
await message.answer(
f"Thank you for your payment: "
f"{payment.total_amount / 100:.2f} "
f"{payment.currency}"
)
也可以直接通过 Message 发送账单:
@router.message(Command("payment"))
async def pay(message: Message):
await message.answer_invoice(
title="Subscription",
description="One month subscription",
payload=INVOICE_PAYLOAD,
provider_token="REAL_PROVIDER_TOKEN_FROM_BOTFATHER",
currency="USD",
prices=[
LabeledPrice(label="Sub month", amount=299)
],
)
provider_token 应该填什么
provider_token 不是自行定义的字符串,也不是支付平台的普通 API key。需要先通过 BotFather 为当前 Telegram bot 连接支付提供商,再使用由 BotFather 生成的 token。
通常需要完成以下配置:
- 在 BotFather 中选择目标 bot。
- 进入 Payments 配置。
- 选择 Telegram 支持的支付提供商。
- 完成测试环境或正式环境的配置。
- 将生成的 provider token 保存到环境变量。
- 由程序读取环境变量,并传给
provider_token。
例如:
import os
provider_token = os.environ["TELEGRAM_PROVIDER_TOKEN"]
不要把正式 token 直接写进源码或提交到 Git 仓库。测试 token 也必须是 BotFather 实际生成的测试凭据,不能直接填写字面量 TEST_TOKEN。
小费参数为什么会导致发送失败
Telegram 对小费参数有以下约束:
suggested_tip_amounts必须按升序排列;- 每个建议金额都不能超过
max_tip_amount; - 所有金额同样使用货币的最小单位。
原代码是:
max_tip_amount=100 * 100
suggested_tip_amounts=[100 * 100, 300 * 100]
计算结果为:
max_tip_amount = 10000
suggested_tip_amounts = [10000, 30000]
第二个建议金额 30000 超过了最大值 10000,因此 Telegram 会拒绝整个 sendInvoice 请求。
排查时可以先删掉所有小费参数:
await bot.send_invoice(
...
prices=[LabeledPrice(label="Sub month", amount=299)],
)
确认基础账单能够正常发送后,再加入符合约束的小费配置。
金额不应无条件再乘以 100
LabeledPrice.amount 接收的是货币最小单位,不是带小数点的金额。
对于 USD:
amount=299
表示 $2.99。
amount=299 * 100
表示 $299.00。
只有当业务变量保存的是美元金额时,才需要将它转换为美分。例如:
from decimal import Decimal
price_usd = Decimal("2.99")
amount = int(price_usd * 100)
不同货币的小数位数不一定与 USD 相同。生产环境应按照 Telegram 支持的货币配置及对应指数进行换算,不能对所有货币一律乘以 100。
payload 必须前后一致
发送账单时使用的是:
payload="test-invoice-payload"
预结账处理器检查的却是:
if pre_checkout_query.invoice_payload != "test_payload":
两个字符串并不相等。用户点击付款后,预结账请求一定会进入拒绝分支。
最好把它定义成一个常量:
INVOICE_PAYLOAD = "test-invoice-payload"
发送账单和检查预结账请求时都使用该常量。实际项目中的 payload 通常还应包含可验证的订单标识,但不能直接信任客户端返回的数据。服务端需要查询订单,并核对用户、金额、币种和订单状态。
如何看到实际异常
先启用基础日志:
import asyncio
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
)
然后在处理器中使用:
try:
await bot.send_invoice(...)
except Exception:
logger.exception("Failed to send invoice")
logger.exception() 会自动记录 traceback。相比之下,下面的写法通常只会输出异常文本,缺少具体的调用位置:
logger.critical(f"Can't start bot! Error: {_ex}")
如果需要在 except 中记录异常,可以写成:
except Exception:
logger.exception("Bot polling failed")
还可以为 Aiogram 注册错误处理器:
from aiogram.types import ErrorEvent
@router.error()
async def error_handler(event: ErrorEvent):
logger.exception(
"Unhandled update exception",
exc_info=event.exception,
)
不要只在 start_polling() 外层捕获异常。支付请求发生在 update handler 内部,具体异常应通过处理器日志或 Aiogram 的错误处理器记录。
关于“订阅支付”
普通的 send_invoice() 发送的是付款账单,并不等于自动续费订阅。能否实现周期扣款,取决于使用的 Telegram 支付类型、支付提供商的能力以及当前 Bot API 的支持范围。
如果目标是每月自动续费,需要先确认所选支付方案是否支持 recurring payment,并按要求保存和使用相应的付款凭据。仅仅把标题写成 Subscription 或设置 start_parameter,不会让普通账单自动按月扣款。
建议先跑通最小的一次性付款流程:
- 使用 BotFather 生成的有效
provider_token。 - 删除小费和其他非必要参数。
- 使用正确的最小货币单位。
- 确保
payload完全一致。 - 在规定时间内响应
pre_checkout_query。 - 收到
successful_payment后,再为用户开通对应权益。
基础流程验证成功后,再根据实际支付渠道处理续费、到期和订单幂等。
备注:内容仅供参考。
