网站接入支付宝后付款回调失败并显示未付款是什么原因?
结论
支付宝已经扣款,但网站仍显示“未付款”,一般不是付款失败,而是网站没有正确接收、验证或处理支付宝发送的异步通知。
常见原因有:
- 支付宝服务器无法访问
notify_url; - 网站把浏览器同步跳转当成了付款结果;
- 程序读取回调参数的方式不对;
- RSA2 签名验证失败;
- 程序没有正确处理
trade_status; - 商户订单号、金额、
app_id或收款方信息不一致; - 回调接口没有返回支付宝要求的成功响应;
- 数据库更新失败、事务回滚,或重复通知处理有误;
- 前端查到了缓存数据或错误的订单记录。
排查时要结合支付宝的通知记录、网站回调日志和订单更新逻辑,不能只看付款后的跳转页面。
同步跳转不等于支付回调
支付完成后,通常会发生两件事:
- 浏览器跳转回网站,例如访问
return_url; - 支付宝服务器向网站的
notify_url发送异步通知。
用户可能关闭浏览器,网络也可能中断,跳转还可能被浏览器拦截。因此,网站不能把同步跳转作为最终付款依据,而应以验证通过的异步通知为准,然后查询自身的订单状态并展示结果。
还有一种情况是,用户已经回到网站,但异步通知尚未处理完成。此时页面短暂显示“未付款”属于正常的时序差异。前端可以轮询订单查询接口,但不能根据前端参数直接把订单改为已付款。
检查 notify_url 是否可以公网访问
支付宝服务器必须能够访问回调地址。以下问题都会导致通知失败:
- 地址使用了
localhost、127.0.0.1或局域网 IP; - 域名解析错误;
- HTTPS 证书无效或证书链不完整;
- 防火墙、WAF、CDN 或反向代理拦截了请求;
- 回调接口要求登录,或者依赖 Cookie、验证码、CSRF Token;
- 接口返回
301或302跳转; - 请求超时,或者返回
404、500; - 路由只允许
GET,而实际通知使用POST; - 沙箱环境和正式环境的应用、网关或密钥混在一起使用。
回调接口应该使用稳定的公网地址,并允许支付宝直接发送请求。例如:
https://example.com/payment/alipay/notify
排查时可以记录请求时间、来源、HTTP 状态码和必要的业务字段,但不要将私钥、完整签名或用户敏感信息写入公开日志。
正确读取回调参数
支付宝通知通常以表单形式提交。如果程序只解析 JSON,请求参数可能一直为空。
以 Express 为例,需要启用表单解析:
import express from "express";
const app = express();
app.use(express.urlencoded({ extended: false }));
app.post("/payment/alipay/notify", async (req, res) => {
const params = req.body;
console.log({
out_trade_no: params.out_trade_no,
trade_no: params.trade_no,
trade_status: params.trade_status
});
res.type("text/plain").send("success");
});
不要先把参数转换成 JSON,再使用转换后的内容验签。签名验证应使用支付宝实际发送、并由框架正常解析出来的通知参数。
必须验证签名和业务字段
收到通知并不代表它可信。程序至少要完成这些检查:
- 使用支付宝公钥验证签名;
- 确认
app_id属于当前应用; - 确认
out_trade_no能匹配本地订单; - 确认通知金额与本地订单的应付金额相同;
- 按当前产品协议核对
seller_id等收款方标识; - 确认订单当前状态允许改为已支付;
- 只处理符合业务要求的
trade_status。
常见的支付成功状态包括:
TRADE_SUCCESS
TRADE_FINISHED
究竟要处理哪些状态,应以接入产品的协议和业务规则为准。不能看到任意 trade_status 就更新订单。
比较金额时要避免浮点数误差,建议统一转换成最小货币单位:
function yuanToFen(value) {
if (!/^\d+(\.\d{1,2})?$/.test(String(value))) {
throw new Error("Invalid amount");
}
const [yuan, decimal = ""] = String(value).split(".");
return Number(yuan) * 100 + Number(decimal.padEnd(2, "0"));
}
if (yuanToFen(notify.total_amount) !== order.amountFen) {
throw new Error("Payment amount mismatch");
}
验签应优先使用与当前接入方案匹配的支付宝官方 SDK,不要自行调整字符编码、参数排序或签名算法。
还要分清支付宝公钥和应用公钥。验签通常配置支付宝公钥,商户发起请求时则使用应用私钥签名。
回调处理示例
下面是一份简化的处理结构。verifyAlipayNotify 代表官方 SDK 提供的通知验签能力,实际类名和调用方式取决于使用的语言及 SDK 版本。
app.post("/payment/alipay/notify", async (req, res) => {
const notify = req.body;
try {
const signatureValid = verifyAlipayNotify(notify);
if (!signatureValid) {
return res.status(400).type("text/plain").send("failure");
}
if (notify.app_id !== config.alipayAppId) {
return res.status(400).type("text/plain").send("failure");
}
if (
notify.trade_status !== "TRADE_SUCCESS" &&
notify.trade_status !== "TRADE_FINISHED"
) {
return res.type("text/plain").send("success");
}
await database.transaction(async (tx) => {
const order = await tx.orders.findForUpdate(notify.out_trade_no);
if (!order) {
throw new Error("Order not found");
}
if (yuanToFen(notify.total_amount) !== order.amountFen) {
throw new Error("Payment amount mismatch");
}
if (order.status === "PAID") {
return;
}
await tx.orders.markAsPaid({
orderNo: order.orderNo,
alipayTradeNo: notify.trade_no,
paidAt: notify.gmt_payment
});
await tx.paymentNotifications.save({
notifyId: notify.notify_id,
orderNo: order.orderNo,
tradeNo: notify.trade_no
});
});
return res.type("text/plain").send("success");
} catch (error) {
console.error("Alipay notify processing failed", {
message: error.message,
out_trade_no: notify.out_trade_no,
trade_no: notify.trade_no
});
return res.status(500).type("text/plain").send("failure");
}
});
示例中的数据库接口需要替换成项目实际使用的 ORM 或数据库操作。
成功处理后应返回正确响应
回调处理成功后,需要按照当前支付宝接口规范返回指定内容。常见的响应是纯文本:
success
不要返回 HTML 页面、JSON 对象或带有额外空格的调试信息,例如:
{"status":"success"}
如果支付宝没有识别出成功响应,通常会继续重试通知。因此,回调处理必须具备幂等性。同一笔支付通知即使到达多次,也只能更新一次订单,不能重复发货、充值或增加余额。
可以为 trade_no、notify_id 或业务订单号建立唯一约束,具体方案要结合实际支付场景设计。
数据库更新失败也会显示未付款
即使验签通过,以下问题仍可能让订单状态保持不变:
- 查询订单时使用了错误的
out_trade_no; - 数据库事务最终回滚;
- 字段长度不足,导致保存
trade_no失败; - 订单状态条件过严,更新语句影响的行数为零;
- 回调写入主库,但页面读取了延迟较高的从库;
- 页面或接口缓存了旧状态;
- 回调进程异常退出,却提前返回了
success; - 订单已经更新,但后续业务异常导致整个事务回滚。
日志最好分别记录这些节点:
收到通知
验签通过
订单匹配成功
金额校验通过
订单更新成功
返回 success
这样更容易确认处理停在了哪一步。
推荐排查顺序
- 到支付宝商户平台查看这笔订单的异步通知发送记录和响应结果。
- 检查服务器访问日志,确认回调请求是否到达。
- 确认接口可以解析表单参数,而不是只接收 JSON。
- 核对应用环境、
app_id、支付宝公钥、签名算法和字符集。 - 检查
trade_status、out_trade_no、total_amount和收款方信息。 - 查看数据库事务结果和更新语句影响的行数。
- 确认处理成功后返回的是规范要求的纯文本响应。
- 检查前端是否通过后端订单查询接口获取最新状态,并排除缓存和主从延迟。
不要为了让页面显示“已付款”而跳过验签,也不要直接相信浏览器传回的金额和订单状态,否则会留下伪造支付结果、篡改金额和重复入账等严重风险。
备注:内容仅供参考。
