PAYATHON 2026

如何在 nodejs 中接收 custom payment provider 支付确认

支付阿杰

结论

server_url 必须指向由你控制、且支付服务商能从公网访问的 HTTPS 接口,例如:

https://example.com/webhooks/liqpay

不能设置为:

https://api.telegram.org/bot<TOKEN>/some_method

Telegram Bot API 只接受已经定义的方法,不会把支付服务商发来的任意 POST 请求转发给你的 Node.js 程序。

正确的处理流程是:支付服务商向你的 Node.js webhook 发起请求。服务器验证支付结果并更新订单后,再调用 Telegram Bot API 通知用户。

Node.js 接收支付通知

以下示例使用 Express。LiqPay 回调通常通过 application/x-www-form-urlencoded 发送 data 和 signature,所以需要启用相应的请求解析器。

npm install express
const express = require('express');
const crypto = require('crypto');

const app = express();

app.use(express.urlencoded({ extended: false }));
app.use(express.json());

const privateKey = process.env.LIQPAY_PRIVATE_KEY;

function createLiqPaySignature(data) {
    return crypto
        .createHash('sha1')
        .update(privateKey + data + privateKey)
        .digest('base64');
}

app.post('/webhooks/liqpay', async (req, res) => {
    const { data, signature } = req.body;

    if (!data || !signature) {
        return res.status(400).send('Missing callback parameters');
    }

    const expectedSignature = createLiqPaySignature(data);

    const actualBuffer = Buffer.from(signature);
    const expectedBuffer = Buffer.from(expectedSignature);

    const signatureIsValid =
        actualBuffer.length === expectedBuffer.length &&
        crypto.timingSafeEqual(actualBuffer, expectedBuffer);

    if (!signatureIsValid) {
        return res.status(401).send('Invalid signature');
    }

    let payment;

    try {
        payment = JSON.parse(
            Buffer.from(data, 'base64').toString('utf8')
        );
    } catch (error) {
        return res.status(400).send('Invalid payment data');
    }

    console.log('Payment callback:', payment);

    /*
     * 字段名称和状态值需要以当前支付服务商文档为准。
     * LiqPay 常见字段包括:
     * payment.order_id
     * payment.status
     * payment.amount
     * payment.currency
     */

    if (payment.status === 'success' || payment.status === 'sandbox') {
        // 1. 根据 order_id 查询订单
        // 2. 核对金额和币种
        // 3. 以幂等方式将订单标记为已支付
        // 4. 通过 Telegram Bot API 通知用户
        console.log(`Order ${payment.order_id} has been paid`);
    }

    // 验证并接收完成后尽快返回成功响应
    return res.status(200).send('OK');
});

app.listen(3000, () => {
    console.log('Webhook server is listening on port 3000');
});

生产环境中的回调地址应设置为:

const request = {
    action: 'invoice_bot',
    version: '3',
    email: 'alexXX@gmail.com',
    amount: this.price,
    currency: 'UAH',
    order_id: this.orderId,
    phone: '3806750000',
    server_url: 'https://example.com/webhooks/liqpay'
};

示例中的字段到底是 order_id 还是 orderId,要以你使用的 LiqPay API 版本和 SDK 文档为准,不能只参考应用内部的字段命名。

支付成功后通知 Telegram 用户

创建订单时,需要保存 order_id 和 chat_id 的对应关系。收到回调并完成验证后,通过 order_id 找到对应用户,再调用 Telegram 的 sendMessage:

async function notifyTelegram(chatId, orderId) {
    const botToken = process.env.TELEGRAM_BOT_TOKEN;

    const response = await fetch(
        `https://api.telegram.org/bot${botToken}/sendMessage`,
        {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({
                chat_id: chatId,
                text: `订单 ${orderId} 支付成功`
            })
        }
    );

    if (!response.ok) {
        throw new Error(`Telegram API returned ${response.status}`);
    }
}

回调中的业务逻辑可以这样写:

if (payment.status === 'success' || payment.status === 'sandbox') {
    const order = await findOrderById(payment.order_id);

    if (!order) {
        return res.status(404).send('Order not found');
    }

    if (
        Number(payment.amount) !== Number(order.amount) ||
        payment.currency !== order.currency
    ) {
        return res.status(400).send('Payment details do not match');
    }

    const changed = await markOrderAsPaidIfPending(order.id);

    if (changed) {
        await notifyTelegram(order.chatId, order.id);
    }
}

findOrderById() 和 markOrderAsPaidIfPending() 需要根据实际使用的数据库来实现。

本地开发时如何测试

支付服务商无法直接访问:

http://localhost:3000/webhooks/liqpay

本地测试时,需要通过反向代理或隧道服务,把本机端口临时映射到一个公网 HTTPS 地址,再将该地址配置到 server_url。正式环境应使用自己的域名、有效的 TLS 证书和稳定的服务器。

注意事项

  • 必须验证 signature,不能只根据 status 判断支付是否成功。
  • 还要核对 order_id、金额、币种和商户信息。
  • 支付回调可能重复发送,因此订单更新必须具备幂等性,防止重复发货或通知。
  • 浏览器跳转页不能代替支付结果通知。最终状态应以后端 webhook 或主动查询结果为准。
  • webhook 应尽快返回 2xx。耗时操作最好交给任务队列处理。
  • 不要把 Telegram Bot Token 或 LiqPay private key 写入源码和日志,应通过环境变量保存。
  • 沙箱环境可能返回 sandbox 状态。生产环境是否也应将其视为成功,需要根据运行环境明确区分。
  • 如果使用普通的 function (json) {},其中的 this 通常不会指向当前类实例。原代码可以改为箭头函数:
liqpay.api('request', request, (json) => {
    this.sendPaymentlinkToUser(chatId, json.href);
});

如果使用 Telegram 原生 Payments API,支付成功后还可能向机器人 webhook 发送包含 successful_payment 的 Telegram update。直接接入自定义支付服务商时,服务商发往 server_url 的回调仍需由你自己的服务器接收。

备注:内容仅供参考。