PAYATHON 2026

PHP Ecommerce API 如何处理支付流程?

支付小周

结论

这类支付无法由 Ecommerce API 通过一次请求完成。API 负责创建支付、保存支付状态、验证网关通知并更新订单;用户则通过浏览器或 App WebView 跳转到 CCAvenue、PayU 等支付页面,在银行页面完成身份验证。

完整流程可以分为以下几个阶段:

  1. 客户端通过 Ecommerce API 创建订单。
  2. API 创建支付记录,生成支付网关要求的签名或加密参数。
  3. 客户端跳转到支付网关,或向网关提交表单。
  4. 用户在支付网关和银行页面完成验证。
  5. 支付网关将用户重定向到商城的返回地址。
  6. 支付网关发送服务端通知,或者商城主动查询支付结果。
  7. API 验证结果,并以幂等方式更新支付和订单状态。

浏览器重定向只能用来恢复页面流程,不能单独作为支付成功的依据。

推荐的数据模型

订单和支付应当分别保存,因为同一个订单可能有多次支付尝试。

orders
- id
- order_no
- user_id
- amount
- currency
- status
- created_at

payments
- id
- payment_no
- order_id
- gateway
- gateway_transaction_id
- amount
- currency
- status
- request_payload
- response_payload
- created_at
- updated_at

订单状态可以设计为:

pending_payment
paid
processing
completed
cancelled
refunded

支付状态可以设计为:

created
pending
authorized
paid
failed
cancelled
refunded
unknown

不要把支付状态和订单状态简单合并。一次支付失败后,用户可能换一种支付方式再次尝试,而订单本身仍然有效。

完整支付流程

1. 创建订单

客户端先调用订单接口:

POST /api/orders
Content-Type: application/json
Authorization: Bearer <token>

{
  "items": [
    {
      "product_id": 101,
      "quantity": 2
    }
  ],
  "currency": "INR"
}

服务端必须根据数据库中的商品价格重新计算金额,不能信任客户端提交的单价或总金额。

返回示例:

{
  "order_id": 12345,
  "order_no": "ORD-20260913-0001",
  "amount": "1499.00",
  "currency": "INR",
  "status": "pending_payment"
}

金额最好使用最小货币单位的整数保存。例如,用 149900 表示 1499.00 INR,以免出现浮点数精度问题。

2. 创建支付会话

客户端选择支付渠道后,调用支付初始化接口:

POST /api/orders/12345/payments
Content-Type: application/json
Authorization: Bearer <token>

{
  "gateway": "ccavenue"
}

服务端需要完成以下操作:

  • 确认订单属于当前用户。
  • 确认订单仍然可以支付。
  • 再次核对金额、币种和库存。
  • 创建唯一的 payment_no。
  • 创建支付记录,并将状态保存为 created。
  • 生成网关要求的参数、签名或密文。
  • 返回支付跳转信息。

有些网关接受普通跳转 URL,有些则要求浏览器通过 POST 表单提交,因此 API 应支持这两种返回形式。

{
  "payment_id": 9876,
  "payment_no": "PAY-20260913-0001",
  "method": "POST",
  "gateway_url": "https://gateway.example/payment",
  "fields": {
    "merchant_id": "merchant-id",
    "order_id": "PAY-20260913-0001",
    "enc_request": "<encrypted-value>",
    "access_code": "<access-code>"
  }
}

这些字段仅用于说明返回结构。实际字段名、加密算法和网关地址必须以当前接入网关的官方文档为准。

3. 浏览器提交到支付网关

Web 前端收到响应后,可以动态创建表单:

function redirectToGateway(payment) {
    const form = document.createElement('form');

    form.method = payment.method;
    form.action = payment.gateway_url;

    for (const [name, value] of Object.entries(payment.fields)) {
        const input = document.createElement('input');
        input.type = 'hidden';
        input.name = name;
        input.value = value;
        form.appendChild(input);
    }

    document.body.appendChild(form);
    form.submit();
}

银行卡密码、OTP、3-D Secure 等身份验证信息必须直接输入银行或支付网关页面,不能经过商城 API。

移动 App 可以使用系统浏览器、支付网关 SDK,或者符合网关要求的 WebView。支付完成后,再通过 Universal Link、App Link 或自定义 URL Scheme 返回 App。

4. 处理浏览器返回

支付完成或取消后,网关通常会将用户重定向到预先配置的地址,例如:

https://shop.example.com/payment/return

这个页面主要用于恢复用户体验,例如显示”正在确认支付结果”,而不是直接宣布支付成功。

前端可以通过 payment_no 查询服务端状态:

GET /api/payments/PAY-20260913-0001
Authorization: Bearer <token>
{
  "payment_no": "PAY-20260913-0001",
  "status": "pending"
}

如果服务端还没有收到可信的支付结果,页面可以在短时间内轮询,但要设置合理的间隔和超时时间,不能无限高频请求。

服务端通知和支付验证

如果支付网关提供 webhook、server callback 或 transaction status API,应优先通过这些机制确认支付结果。

处理通知时,至少要检查:

  • 签名、MAC 或加密响应是否合法。
  • 商户号是否属于当前系统。
  • 商户支付单号是否存在。
  • 通知中的金额和币种是否与本地记录一致。
  • 网关交易号是否有效,以及是否已经绑定到其他支付。
  • 支付状态是否来自可信字段。
  • 必要时,是否已通过网关查询接口进行二次确认。

下面是一个简化的 PHP 处理结构:

<?php

final class PaymentWebhookController
{
    public function handle(
        PaymentGateway $gateway,
        PaymentRepository $payments,
        OrderRepository $orders,
        array $payload
    ): void {
        $notification = $gateway->parseAndVerifyNotification($payload);

        if (!$notification->isValid()) {
            http_response_code(400);
            echo 'invalid notification';
            return;
        }

        $payment = $payments->findByPaymentNo(
            $notification->getPaymentNo()
        );

        if ($payment === null) {
            http_response_code(404);
            echo 'payment not found';
            return;
        }

        if (
            $payment->getAmountMinor() !== $notification->getAmountMinor()
            || $payment->getCurrency() !== $notification->getCurrency()
        ) {
            http_response_code(400);
            echo 'amount or currency mismatch';
            return;
        }

        $payments->transaction(function () use (
            $payments,
            $orders,
            $payment,
            $notification
        ): void {
            $lockedPayment = $payments->lockById($payment->getId());

            // 重复通知直接返回成功,避免重复发货或重复记账。
            if ($lockedPayment->isPaid()) {
                return;
            }

            if ($notification->isSuccessful()) {
                $payments->markAsPaid(
                    $lockedPayment->getId(),
                    $notification->getGatewayTransactionId(),
                    $notification->getRawPayload()
                );

                $orders->markAsPaid($lockedPayment->getOrderId());
                return;
            }

            $payments->markAsFailed(
                $lockedPayment->getId(),
                $notification->getRawPayload()
            );
        });

        http_response_code(200);
        echo 'OK';
    }
}

parseAndVerifyNotification() 必须按照相应网关的规定完成验签或解密,不能只读取类似 status=success 的字段。

使用网关适配器隔离差异

CCAvenue、PayU 等网关使用的字段、签名方式和通知格式各不相同,可以先定义一个统一接口:

<?php

interface PaymentGateway
{
    public function createPayment(PaymentRequest $request): RedirectData;

    public function parseAndVerifyNotification(array $payload): PaymentNotification;

    public function queryPayment(string $gatewayTransactionId): PaymentResult;

    public function refund(RefundRequest $request): RefundResult;
}

再为不同网关分别实现:

final class CcavenueGateway implements PaymentGateway
{
    // CCAvenue 的参数生成、加密、通知解密和交易查询
}

final class PayuGateway implements PaymentGateway
{
    // PayU 的签名、通知验证和交易查询
}

业务层只依赖 PaymentGateway 接口,不直接处理各家网关的具体字段。以后增加新的支付方式时,订单的核心逻辑也不需要随之修改。

如果没有收到回调怎么办

用户可能在银行页面关闭浏览器,网络也可能在支付成功后中断,所以”没有跳转回来”不等于支付失败。

可以通过以下机制进行补偿:

  • 支付页面定期查询 Ecommerce API。
  • 当支付状态不确定时,由 API 调用网关的交易查询接口。
  • 使用定时任务扫描长时间处于 pending 或 unknown 状态的支付。
  • 根据网关返回的查询结果修正本地状态。
  • 每天通过网关结算文件或对账接口核对交易。

状态尚未明确时,应继续保留为 pending 或 unknown,不要立即允许再次扣款。如果允许用户重新支付,需要创建一条新的支付记录,并处理原支付稍后成功的情况。

安全和一致性注意事项

  • 商户密钥、加密密钥和 API Secret 只能保存在服务端。
  • 所有请求都应使用 HTTPS。
  • return_url 和 callback_url 应使用固定的白名单地址。
  • 不要记录银行卡号、CVV、OTP、密码等敏感信息。
  • 保存网关原始响应前,应先对数据进行脱敏。
  • 每次支付都要使用唯一且不可预测的支付单号。
  • webhook 必须允许重复调用,并保证处理过程幂等。
  • 支付状态和订单状态应在同一个数据库事务中更新。
  • 发货、积分、优惠券核销等操作应由”支付成功事件”触发,并分别进行幂等控制。
  • 已经处于 paid 状态的支付不能改回 failed。
  • 用户访问成功页面时,不能直接修改订单状态。
  • 超时和失败页面可以允许用户安全重试,但每次尝试都要创建独立的支付记录。
  • 上线前,应在支付网关提供的测试环境中验证成功、失败、取消、超时、重复通知和金额不一致等场景。

API 不需要代替用户完成银行身份验证。它负责创建一笔可追踪的支付,引导客户端进入支付网关,并在用户离开商城期间维护订单状态。收到网关提供的可信结果后,API 再以安全、幂等的方式完成订单。

备注:内容仅供参考。