PAYATHON 2026

Telegram Bot 无法接收 preCheckoutQuery update

支付阿杰

结论

pre_checkout_query 被发送到了当前登记的 webhook:

https://telegram.botframework.com/api/telegram/...

它并没有发送到你的 UpdateController。Telegram 对每个 bot token 只保留一个 webhook,无法同时把普通消息交给 Bot Framework,再把支付更新发往另一个地址。

要接收 pre_checkout_query,需要将 Telegram 的 webhook 设为你自己的公网 HTTPS 地址,并在收到更新后及时调用 answerPreCheckoutQuery。如果继续使用 Bot Framework 的 Telegram Channel,能否处理支付更新取决于连接器是否支持并转发 pre_checkout_query。结合 HTTP 400 和控制器始终收不到请求这两个现象,这类更新很可能在 Bot Framework 的入口处就被拒绝了。

为什么普通消息正常,支付更新却失败

当前的数据流是:

Telegram
    → telegram.botframework.com
    → Bot Framework
    → 你的 Bot Framework endpoint

getWebhookInfo 返回的 URL 表明,Telegram 并未直接调用你的控制器:

{
  "ok": true,
  "result": {
    "url": "https://telegram.botframework.com/api/telegram/XXX/XXX",
    "pending_update_count": 0,
    "max_connections": 40
  }
}

普通消息能够正常收发,只能说明 Bot Framework Telegram Channel 可以处理这些消息,不能说明它支持 Telegram Payments 的所有 update 类型。

pending_update_count 偶尔变为 1,通常说明有一个更新尚未成功投递。Bot Framework 返回 HTTP 400 后,Telegram 会暂时保留该更新并重试。这和 TLS 1.2 没有关系,因为请求已经到达 Bot Framework,BadRequest 也是由它返回的。

正确的处理方式

1. 将 webhook 改为自己的地址

你的地址需要满足以下条件:

  • 可通过公网访问;
  • 使用有效的 HTTPS 证书;
  • 路由与 ASP.NET Controller 完全匹配;
  • 能接收 Telegram Bot API 的原始 Update JSON;
  • 成功处理后返回 HTTP 200。

例如:

https://example.com/api/update

可以使用 Telegram.Bot 设置 webhook。不同版本的 Telegram.Bot 在方法名和参数形式上可能略有区别,核心写法如下:

await botClient.SetWebhookAsync(
    url: "https://example.com/api/update",
    allowedUpdates: new[]
    {
        UpdateType.Message,
        UpdateType.PreCheckoutQuery
    });

也可以直接调用 Bot API:

curl -X POST "https://api.telegram.org/bot<BOT_TOKEN>/setWebhook" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/api/update",
    "allowed_updates": ["message", "pre_checkout_query"]
  }'

设置后再次检查:

curl "https://api.telegram.org/bot<BOT_TOKEN>/getWebhookInfo"

返回结果中的 result.url 必须是你自己的地址,不能再是:

https://telegram.botframework.com/...

改用自己的 webhook 后,Bot Framework Telegram Channel 将不再直接收到这个 bot 的 Telegram updates。

2. 在控制器中处理 PreCheckoutQuery

下面的代码展示了基本处理逻辑。具体名称可能因 Telegram.Bot 版本而异,但处理步骤相同:

using Microsoft.AspNetCore.Mvc;
using Telegram.Bot;
using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums;

[ApiController]
[Route("api/update")]
public class UpdateController : ControllerBase
{
    private readonly ITelegramBotClient _botClient;

    public UpdateController(ITelegramBotClient botClient)
    {
        _botClient = botClient;
    }

    [HttpPost]
    public async Task<IActionResult> Post(
        [FromBody] Update update,
        CancellationToken cancellationToken)
    {
        if (update.Type == UpdateType.PreCheckoutQuery &&
            update.PreCheckoutQuery is not null)
        {
            var query = update.PreCheckoutQuery;

            bool paymentCanProceed = ValidateOrder(
                query.InvoicePayload,
                query.Currency,
                query.TotalAmount);

            if (paymentCanProceed)
            {
                await _botClient.AnswerPreCheckoutQueryAsync(
                    preCheckoutQueryId: query.Id,
                    cancellationToken: cancellationToken);
            }
            else
            {
                await _botClient.AnswerPreCheckoutQueryAsync(
                    preCheckoutQueryId: query.Id,
                    errorMessage: "订单信息已失效,请重新发起支付。",
                    cancellationToken: cancellationToken);
            }
        }

        return Ok();
    }

    private static bool ValidateOrder(
        string invoicePayload,
        string currency,
        int totalAmount)
    {
        // 根据 invoicePayload 查找服务端订单,
        // 并校验币种、金额和订单状态。
        return true;
    }
}

有些 Telegram.Bot 版本使用不带 Async 后缀的方法,例如:

await _botClient.AnswerPreCheckoutQuery(
    preCheckoutQueryId: query.Id,
    cancellationToken: cancellationToken);

请以项目实际安装版本的 API 为准,不要混用不同版本的示例。

必须及时确认预结账请求

收到 pre_checkout_query 后,bot 必须尽快调用:

answerPreCheckoutQuery

Telegram 对确认时间有严格限制,通常要求在 10 秒内完成。因此,不要在确认前执行耗时任务,例如:

  • 长时间数据库事务;
  • 调用响应很慢的第三方服务;
  • 生成文件;
  • 等待人工确认。

订单校验应尽量简短。接受支付时,不要提供 error_message;拒绝支付时,则应提供用户能够理解的错误说明。

还要处理支付成功消息

answerPreCheckoutQuery 仅表示允许 Telegram 继续支付流程,并不代表款项已经支付成功。最终结果应从消息中的 successful_payment 获取:

if (update.Message?.SuccessfulPayment is { } payment)
{
    string invoicePayload = payment.InvoicePayload;
    string currency = payment.Currency;
    int totalAmount = payment.TotalAmount;
    string telegramPaymentChargeId = payment.TelegramPaymentChargeId;
    string providerPaymentChargeId = payment.ProviderPaymentChargeId;

    // 使用 invoicePayload 查找订单,并以幂等方式标记为已支付。
}

业务逻辑应以 successful_payment 作为最终支付依据,同时保证重复 update 不会导致重复发货或重复记账。

如果必须保留 Bot Framework

如果现有系统仍需依赖 Bot Framework,可以让自建 webhook 成为 Telegram 的唯一入口:

Telegram
    → 自建 webhook
        ├─ 处理 pre_checkout_query 和 successful_payment
        └─ 将受支持的普通消息交给现有业务层

不要尝试多次调用 setWebhook 来配置两个地址,因为后一次设置会覆盖前一次设置。

你也可以确认当前使用的 Bot Framework Telegram Channel 是否明确支持 Telegram Payments。如果连接器不支持 pre_checkout_query,修改 ASP.NET Controller、TLS 设置或 Telegram.Bot 的反序列化配置都无法解决问题,因为更新根本没有到达你的应用。

排查要点

按以下顺序检查:

  1. getWebhookInfo.result.url 是否为自己的 webhook。
  2. allowed_updates 是否包含 pre_checkout_query,也可以不限制 update 类型。
  3. ASP.NET 路由是否与 webhook URL 一致。
  4. Controller 参数中的 Update 是否来自 Telegram.Bot.Types。
  5. webhook 是否向 Telegram 返回 HTTP 200。
  6. 是否及时调用了 answerPreCheckoutQuery。
  7. getWebhookInfo 中是否出现 last_error_message、last_error_date,或者 pending_update_count 是否持续增长。
  8. 是否仍有 Bot Framework 或部署脚本在重新设置 webhook。

问题的核心不是 ServicePointManager.SecurityProtocol,而是 webhook 当前属于谁。只要 getWebhookInfo 仍指向 telegram.botframework.com,Telegram 就不会直接调用你的 UpdateController。

备注:内容仅供参考。