PAYATHON 2026

开发App时,支付宝支付通知应对接哪个接口?

支付小周

明确结论

应接入支付宝 App 支付接口 alipay.trade.app.pay,并配置用于接收支付结果异步通知的 notify_url。

支付成功通知不会直接发给 App 客户端。支付宝服务器会通过 HTTP POST 请求将结果发送到业务服务器。业务服务器完成验签并保存支付记录后,再通过接口查询、WebSocket 或消息推送等方式将结果告知 App。

典型流程如下:

App 发起支付
    ↓
业务服务器调用 alipay.trade.app.pay 生成支付参数
    ↓
App 调起支付宝客户端完成支付
    ↓
支付宝服务器 POST 支付结果到 notify_url
    ↓
业务服务器验签、核对订单并更新支付状态
    ↓
App 查询订单状态或接收业务服务器推送

为什么不能只处理 App 的支付回调

支付宝 SDK 会将客户端支付结果返回给 App,但这个结果只能用于界面展示,不能作为最终入账依据,原因包括:

  • App 可能被关闭、断网或切到后台,因而收不到结果。
  • 客户端返回的数据可能被篡改。
  • 支付成功后,客户端回调可能因网络问题丢失。
  • 用户完成支付,不代表业务订单一定处理成功。

订单是否支付成功,应以服务端收到的支付宝异步通知为准。如果通知暂时没有到达,可以调用 alipay.trade.query 主动查询订单状态。

接入步骤

1. 服务端创建 App 支付订单

服务端调用 alipay.trade.app.pay,填写商户订单号、金额和商品标题等信息,同时设置可通过公网访问的 notify_url。

下面的示例使用支付宝 Java SDK。具体依赖版本和客户端初始化方式,应以当前接入的支付宝开放平台 SDK 文档为准:

AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest();

AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
model.setOutTradeNo("ORDER_202609090001");
model.setTotalAmount("99.00");
model.setSubject("会员服务");
model.setProductCode("QUICK_MSECURITY_PAY");

request.setBizModel(model);
request.setNotifyUrl("https://api.example.com/payment/alipay/notify");

AlipayTradeAppPayResponse response = alipayClient.sdkExecute(request);

if (response.isSuccess()) {
    // 将 response.getBody() 返回给 App,由 App 调起支付宝 SDK
    String orderString = response.getBody();
}

notify_url 必须指向服务端地址,例如:

https://api.example.com/payment/alipay/notify

不能使用 App Scheme、Universal Link、localhost,或者只能在局域网内访问的地址。

2. App 调起支付宝支付

服务端返回 orderString 后,App 使用支付宝客户端 SDK 发起支付。客户端回调可以用来提示“支付处理中”,但不能仅凭这个回调将订单标记为已支付。

伪代码如下:

orderString = requestOrderFromBusinessServer()
result = alipaySdk.pay(orderString)

showPaymentProcessing()
queryOrderStatusFromBusinessServer()

App 应向自己的业务服务器查询订单状态,不应直接信任支付宝客户端的返回值。

3. 服务端接收异步通知

支付宝会以表单参数的形式向 notify_url 发起 POST 请求。服务端收到通知后,应先验签,再核对订单信息。

Spring Boot 示例:

import com.alipay.api.internal.util.AlipaySignature;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import jakarta.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

@RestController
public class AlipayNotifyController {

    private static final String ALIPAY_PUBLIC_KEY = "支付宝公钥";
    private static final String CHARSET = "UTF-8";
    private static final String SIGN_TYPE = "RSA2";

    @PostMapping(
        value = "/payment/alipay/notify",
        produces = "text/plain;charset=UTF-8"
    )
    public String receiveNotify(HttpServletRequest request) {
        try {
            Map<String, String> params = new HashMap<>();

            request.getParameterMap().forEach((name, values) -> {
                if (values != null && values.length > 0) {
                    params.put(name, String.join(",", values));
                }
            });

            boolean verified = AlipaySignature.rsaCheckV1(
                params,
                ALIPAY_PUBLIC_KEY,
                CHARSET,
                SIGN_TYPE
            );

            if (!verified) {
                return "failure";
            }

            String appId = params.get("app_id");
            String sellerId = params.get("seller_id");
            String outTradeNo = params.get("out_trade_no");
            String tradeNo = params.get("trade_no");
            String tradeStatus = params.get("trade_status");
            String totalAmount = params.get("total_amount");

            // 还需要从数据库读取本地订单并核对:
            // 1. app_id 是否属于当前应用
            // 2. seller_id 是否属于当前收款账号
            // 3. out_trade_no 是否存在
            // 4. total_amount 是否与订单金额一致
            // 5. 当前订单是否已经处理过

            if ("TRADE_SUCCESS".equals(tradeStatus)
                    || "TRADE_FINISHED".equals(tradeStatus)) {

                // 在数据库事务中执行幂等更新:
                // updateOrderPaid(outTradeNo, tradeNo, totalAmount);

                return "success";
            }

            // 对已正确接收且无需进一步处理的合法通知,
            // 通常也应按接入规范返回 success。
            return "success";

        } catch (Exception e) {
            // 发生异常时不要确认成功,以便支付宝按机制重新通知。
            return "failure";
        }
    }
}

如果使用支付宝公钥证书模式,验签初始化和调用方法可能与普通公钥模式不同。此时应使用对应的证书模式 SDK 配置,不能直接套用示例中的普通支付宝公钥配置。

4. 将支付状态同步给 App

支付宝异步通知到达业务服务器后,可以通过以下方式让 App 获取结果:

  • App 定时调用订单查询接口。
  • App 返回支付页面时立即查询一次,随后进行短时间轮询。
  • 业务服务器通过 WebSocket 通知在线 App。
  • 使用 APNs、厂商推送等方式发送支付成功消息。

实际项目中,“支付完成后立即查询 + 短时间轮询”通常更稳妥。即使推送丢失,App 仍可从业务服务器获取最终状态。

业务查询接口可以设计为:

GET /api/orders/ORDER_202609090001/payment-status
Authorization: Bearer <token>

返回示例:

{
  "orderNo": "ORDER_202609090001",
  "paymentStatus": "PAID",
  "channel": "ALIPAY",
  "tradeNo": "支付宝交易号"
}

异步通知必须检查的内容

验签成功后,不能直接发货或开通服务,还要核对以下内容:

  • app_id 是否等于当前支付宝应用的 App ID。
  • seller_id 是否为预期的收款方。
  • out_trade_no 是否对应本地真实订单。
  • total_amount 是否与本地订单金额一致。
  • trade_status 是否为业务认可的成功状态。
  • 订单是否已经完成过支付处理。

金额应使用 BigDecimal 比较,不要使用 double:

BigDecimal notifiedAmount =
    new BigDecimal(params.get("total_amount"));

BigDecimal orderAmount =
    order.getPayAmount();

if (notifiedAmount.compareTo(orderAmount) != 0) {
    return "failure";
}

注意事项

异步通知必须幂等

支付宝可能重复发送同一笔交易的通知。每次通知都要正常验签,但订单状态更新、发货、积分发放和会员开通等业务操作只能执行一次。

可以为 out_trade_no 或支付宝交易号 trade_no 建立唯一约束,并在数据库事务中完成状态更新。

正确返回响应内容

通知处理成功后,应按照支付宝接入规范返回以下纯文本:

success

不要返回 JSON、HTML 或其他提示文字。如果没有返回支付宝能够识别的成功响应,支付宝可能再次发送通知。

不要通过金额或订单号判断通知真实性

攻击者可以自行构造 POST 请求。服务端必须先使用支付宝公钥验证 sign,不能只检查订单号、交易状态或金额。

保存原始通知和处理记录

建议记录以下信息:

  • out_trade_no
  • trade_no
  • trade_status
  • 通知时间
  • 验签结果
  • 业务处理结果

日志中不要输出私钥、访问令牌或完整的敏感信息。

增加主动查询作为补偿

异步通知可能延迟,也可能因服务器暂时不可用而处理失败。可以调用 alipay.trade.query,通过 out_trade_no 或 trade_no 主动查询交易状态,并配合定时对账任务修复异常订单。

这个需求对应的不是一个“直接通知 App 的支付宝接口”。正确做法是接入 alipay.trade.app.pay,由业务服务器通过 notify_url 接收支付结果的异步通知,再将可靠的订单状态同步给 App。

备注:内容仅供参考。