如何在 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 的回调仍需由你自己的服务器接收。
备注:内容仅供参考。
