发卡源码如何接入支付接口:配置流程与常见问题

最新新闻5小时前发布 jdkjadmin
0 00
发卡源码如何接入支付接口:配置流程与常见问题

发卡源码如何接入支付接口:配置流程与常见问题

发卡网站通常销售兑换码、软件授权、课程卡密等数字商品。接入支付接口后,买家可以在线付款,系统再按订单状态交付商品。接入工作不只是把“支付按钮”连到一个网址:还要正确创建订单、将买家带到支付渠道、验证支付结果,并保证重复通知、网络超时等情况下不会重复发货或错误发货。

不同发卡源码、支付机构和通道的接口规范可能完全不同。下文用通用流程说明架构,并以 PHP 展示若干可运行的基础代码。示例中的字段名和签名方式是教学约定,不代表任何特定支付平台的接口规范。正式接入时,必须以所选平台的商户文档为准。

一、适用场景与接入边界

适用场景

  • 自建发卡网站,需要让买家通过银行卡、钱包或其他渠道付款。
  • 更换支付服务商,或新增一个支付通道,同时希望保留现有商品、订单和库存逻辑。
  • 已有支付接口,但订单状态、异步通知或自动发货不够可靠,需要梳理支付闭环。

如果使用的是托管式商城,平台可能已经提供支付能力;自行接入前应确认是否允许更改支付流程,以及相关服务协议和当地法规是否允许销售对应商品。不要把支付接口当作绕过平台风控、交易限制或身份审核的工具。数字商品的可售范围、退款规则、用户信息处理及税务要求,也应按经营所在地和所使用平台的规则核实。

明确源码与支付机构的职责

  • 发卡源码负责:商品和库存管理、订单创建、订单状态、支付结果核验、发货、退款记录和后台审计。
  • 支付机构负责:受理支付请求、向用户展示或处理付款流程,并按其协议返回支付结果。
  • 商户负责:保护密钥、核对交易金额和商户订单号、处理重复通知与异常订单,并遵守平台规则。

浏览器跳转到“支付成功”页面不是支付凭证。用户可以关闭页面,也可能直接访问成功页;因此不能仅凭 URL 参数、前端回调或页面提示发货。实际发货必须依赖经过验证的服务端通知,必要时再向支付平台查询订单。

二、先确认接口文档和配置项

开始改代码之前,先从支付平台的商户后台和正式文档确认以下信息:

  • 商户号、应用 ID、密钥或证书,以及测试环境与生产环境的区别。
  • 创建订单接口、请求方法、字段类型、金额单位和签名算法。
  • 同步跳转地址与异步通知地址的用途、可访问性要求和响应格式。
  • 订单查询、退款、关闭订单接口,以及各接口的限流和有效期要求。
  • 支付状态定义、通知重试策略、验签原文规则、证书轮换方式。
  • 允许销售的商品类型、结算条件、退款规则和账户审核要求。

这些信息不能从通用教程中推定。比如金额可能要求以元表示,也可能要求以最小货币单位表示;签名可能使用 HMAC、RSA、证书或平台自定义算法;通知可能是 JSON、表单或原始文本。字段名、字符集、金额精度和签名范围均以对应文档为准,不能直接照搬其他支付平台的示例。

配置文件中通常需要支付方式标识、接口地址、商户号、密钥、回调地址和环境开关。密钥应放在服务器环境变量或受访问控制的配置文件中,不要提交到 Git 仓库、前端代码、公开日志或工单截图。测试密钥和生产密钥也应分开管理。

三、推荐的支付闭环

  1. 服务端读取商品信息,重新计算价格和库存,不接受客户端传来的商品价格作为最终金额。
  2. 在数据库中创建本地订单,初始状态为待支付,并生成唯一商户订单号。
  3. 服务端调用支付平台的下单接口,使用本地订单号作为商户订单号或可追踪的关联字段。
  4. 向买家返回平台要求的跳转链接、二维码内容或其他支付凭据。
  5. 买家付款后,支付平台向服务端异步通知支付结果;服务端验签并校验订单、金额和状态。
  6. 在数据库事务中将订单标记为已支付并执行一次性发货。重复通知应得到平台认可的成功响应,但不能再次发货。
  7. 若通知未到、状态不明或发生超时,通过平台订单查询接口核实,不能根据浏览器页面自行推断结果。

本地订单状态建议至少区分“待支付”“已支付”“已发货”“已关闭”“退款中”“已退款”等状态。具体状态机应符合业务需求。尤其要区分“支付成功”和“商品已交付”:二者可能因库存不足、发货失败或人工审核而暂时不一致。

四、PHP 下单示例:金额由服务端计算

以下示例展示如何用 PDO 创建本地订单。它假设商品表有 id、price_minor 和 stock 字段,订单表有 order_no、amount_minor、status 和 created_at 字段。price_minor 表示以最小货币单位存储的整数,例如某些货币可用分;实际货币精度必须按经营币种和支付平台要求确定。不要把“分”作为所有货币通用的精度假设。

示例只创建本地待支付订单,不包含平台特定的下单请求。数据库连接变量 $pdo 应由应用配置初始化,并启用 PDO 异常模式。实际项目还应加入登录态、CSRF 防护、频率限制和业务级库存策略。

<?php
declare(strict_types=1);

/** 生成不可预测的商户订单号;数据库仍需对 order_no 建唯一索引。 */
function newOrderNo(): string
{
    return 'FD' . gmdate('YmdHis') . bin2hex(random_bytes(8));
}

function createLocalOrder(PDO $pdo, int $productId, int $quantity): array
{
    if ($productId <= 0 || $quantity < 1 || $quantity > 10) {
        throw new InvalidArgumentException('商品或购买数量不合法');
    }

    $pdo->beginTransaction();

    try {
        // 锁定商品行,避免并发请求读取到相同库存后超卖。
        $stmt = $pdo->prepare(
            'SELECT id, price_minor, stock FROM products WHERE id = ? FOR UPDATE'
        );
        $stmt->execute([$productId]);
        $product = $stmt->fetch(PDO::FETCH_ASSOC);

        if (!$product) {
            throw new RuntimeException('商品不存在');
        }

        $price = filter_var($product['price_minor'], FILTER_VALIDATE_INT);
        $stock = filter_var($product['stock'], FILTER_VALIDATE_INT);

        if ($price === false || $price < 0 || $stock === false) {
            throw new RuntimeException('商品价格或库存数据异常');
        }
        if ($stock < $quantity) {
            throw new RuntimeException('库存不足');
        }

        // 防止整数乘法溢出;真实项目还应设置符合业务的订单金额上限。
        if ($price !== 0 && $quantity > intdiv(PHP_INT_MAX, $price)) {
            throw new RuntimeException('订单金额超出系统范围');
        }

        $amount = $price * $quantity;
        $orderNo = newOrderNo();

        $insert = $pdo->prepare(
            'INSERT INTO orders
             (order_no, product_id, quantity, amount_minor, status, created_at)
             VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)'
        );
        $insert->execute([$orderNo, $productId, $quantity, $amount, 'pending']);

        $pdo->commit();

        return [
            'order_no' => $orderNo,
            'amount_minor' => $amount,
            'status' => 'pending',
        ];
    } catch (Throwable $e) {
        if ($pdo->inTransaction()) {
            $pdo->rollBack();
        }
        throw $e;
    }
}
?>

示例中的购买数量上限 10 只是业务校验示例,不是 PHP 或支付平台的通用限制,应按商品规则调整。FOR UPDATE 是否有效以及具体写法取决于数据库引擎;应使用支持事务和行锁的数据库配置,并为订单号创建唯一索引。若系统采用“支付后扣库存”或预占库存,应设计对应的超时释放和并发规则,不能只在页面上显示库存。

随后服务端使用本地订单数据组装平台请求,不能从浏览器提交的金额直接构造支付金额。调用平台时还需设置合理的连接与读取超时、校验 HTTPS 证书、处理非 2xx 响应和平台业务错误,并保存可安全记录的请求标识。不要把密钥或完整敏感响应写进普通日志。

五、异步通知:验签、核对、幂等

异步通知是支付完成后由平台服务器请求商户服务器的接口。通知地址应通过 HTTPS 对外可访问,不能要求用户登录,也不应依赖浏览器 Cookie。服务器收到通知后,至少应核验:

  • 签名确实按平台规定通过验证,且使用正确环境对应的密钥或证书。
  • 商户号、应用、币种、商户订单号和金额与本地订单一致。
  • 通知表示的是平台认可的支付成功状态,而不是“处理中”或“已受理”。
  • 订单尚未处理,或重复通知属于已成功处理的同一笔交易。

下面的函数只演示一种常见的 HMAC-SHA256 签名形式:对原始请求体计算 HMAC,再与十六进制签名比较。这不是通用支付验签协议。平台可能要求对规范化字段串签名、使用 RSA、公钥证书或另外处理时间戳、随机数及编码。只有平台文档明确规定与示例一致时,才可以使用这段函数。

<?php
declare(strict_types=1);

/**
 * 仅适用于“原始请求体 + HMAC-SHA256 + 十六进制签名”的约定。
 * $secret 必须来自服务端安全配置,不能由请求参数提供。
 */
function verifyExampleHmac(string $rawBody, string $receivedHex, string $secret): bool
{
    if ($rawBody === '' || $receivedHex === '' || $secret === '') {
        return false;
    }

    $expectedHex = hash_hmac('sha256', $rawBody, $secret);
    return hash_equals($expectedHex, strtolower($receivedHex));
}

以下是通知处理逻辑的结构示例。假设平台文档规定请求体是 JSON,并且签名放在 X-Pay-Signature 请求头中;这两个约定仍然只是示例。真正的生产端点必须替换为平台规定的解析方式和验签代码,并对数据库表名、金额精度和通知响应格式进行适配。

<?php
declare(strict_types=1);

$rawBody = file_get_contents('php://input') ?: '';
$signature = $_SERVER['HTTP_X_PAY_SIGNATURE'] ?? '';
$secret = getenv('PAYMENT_WEBHOOK_SECRET') ?: '';

if (!verifyExampleHmac($rawBody, $signature, $secret)) {
    http_response_code(400);
    exit('invalid signature');
}

$event = json_decode($rawBody, true);
if (!is_array($event)) {
    http_response_code(400);
    exit('invalid body');
}

// 以下字段是教学示例字段,须映射到实际平台通知字段。
$orderNo = $event['merchant_order_no'] ?? null;
$tradeId = $event['platform_trade_id'] ?? null;
$amount = $event['amount_minor'] ?? null;
$status = $event['trade_status'] ?? null;

if (!is_string($orderNo) || !is_string($tradeId)
    || !is_int($amount) || $status !== 'PAID') {
    http_response_code(400);
    exit('invalid fields');
}

try {
    $pdo->beginTransaction();

    $stmt = $pdo->prepare(
        'SELECT order_no, amount_minor, status
         FROM orders WHERE order_no = ? FOR UPDATE'
    );
    $stmt->execute([$orderNo]);
    $order = $stmt->fetch(PDO::FETCH_ASSOC);

    if (!$order) {
        throw new RuntimeException('unknown order');
    }
    if ((int)$order['amount_minor'] !== $amount) {
        throw new RuntimeException('amount mismatch');
    }

    if ($order['status'] === 'pending') {
        $update = $pdo->prepare(
            'UPDATE orders
             SET status = ?, platform_trade_id = ?
             WHERE order_no = ? AND status = ?'
        );
        $update->execute(['paid', $tradeId, $orderNo, 'pending']);

        // 可在同一事务中写入“待发货”任务,交由可靠任务队列执行。
        // 具体发货逻辑须确保每个订单只交付一次。
    } elseif ($order['status'] !== 'paid' && $order['status'] !== 'delivered') {
        throw new RuntimeException('unexpected order state');
    }

    $pdo->commit();

    // 正式接口应返回支付平台文档要求的成功响应。
    http_response_code(200);
    exit('ok');
} catch (Throwable $e) {
    if ($pdo->inTransaction()) {
        $pdo->rollBack();
    }

    // 生产环境记录订单号和内部错误编号,不要把密钥、完整签名等写入日志。
    http_response_code(500);
    exit('retry');
}

代码使用事务和行锁降低并发重复处理风险,但并不自动解决全部幂等问题。生产数据库应为平台交易号、商户订单号等关键字段设置合适的唯一约束;如平台为每次通知提供事件 ID,也可以将事件 ID 写入通知记录表并建立唯一索引。发货操作如果调用外部服务,不能假定数据库事务能回滚那个外部调用。更稳妥的做法是在事务中创建唯一的发货任务,再由后台任务执行,并以订单号作为幂等键。

返回什么内容才能让平台停止重试,要以平台文档为准。有的平台要求特定纯文本,有的平台要求特定 JSON。验签失败或关键字段不匹配时,不应返回“成功”并静默丢弃;但也要避免对每次重复通知都永久报错。对于状态不明的情况,记录请求标识并查询平台订单详情。

六、同步跳转和异步通知不要混用

同步跳转是买家付款后浏览器返回商户网站的流程,适合展示“正在确认”“支付结果待核实”等页面。它可能没有发生,也可能被用户重复打开。页面可以读取本地订单状态并展示结果,但不能直接把订单改为已支付。

异步通知由平台服务器调用,适合驱动订单状态变更。即使通知已经验签通过,也要校验通知对应的订单金额和商户信息。两条路径可能先后到达,也可能只到达一条,因此订单处理应允许安全重入,并由数据库状态和唯一约束保证只发货一次。

七、配置和上线步骤

  1. 阅读文档并确认资质:核对商品是否允许销售、账户是否完成审核、测试与生产环境分别使用什么凭据。
  2. 加入配置而非硬编码:为每个环境配置接口地址、商户标识、密钥或证书路径、通知地址和币种。配置项名称和必填项以平台文档为准。
  3. 实现服务端订单:服务器自行读商品价格、校验用户权限和库存、创建唯一订单号,采用明确的订单状态。
  4. 接入下单请求:严格按平台格式构造请求、签名并处理响应。只有获得有效的支付凭据后,才将其返回给浏览器。
  5. 实现通知端点:确保公网 DNS、HTTPS、反向代理和防火墙允许平台访问;按文档解析原始请求并验签。
  6. 联调边界场景:测试成功、失败、取消、超时、重复通知、金额不匹配、未知订单和平台重试。
  7. 小范围上线:启用生产配置后先做小额真实测试,核对平台账单、本地订单、退款记录和交付结果。
  8. 建立对账机制:定期比对支付平台交易记录与本地订单,处理长时间待支付、已付款未发货和退款状态不一致的记录。

测试环境与生产环境的域名、密钥、商户号可能不同。上线时不要只切换一个“测试模式”开关就认为配置完成;应逐项确认请求地址、证书、公钥、回调 URL 和后台账户对应关系。若支付平台要求固定来源 IP、域名备案或回调白名单,应按其文档配置,不能假定所有平台都有相同要求。

八、常见问题与排查方法

1. 下单接口返回签名错误

检查签名字段排序、空值是否参与签名、字符编码、换行、URL 编码、金额格式、密钥环境和请求体是否与签名时完全一致。不同平台对字段排序和签名原文的规定可能不同,不能靠尝试更换算法解决。对照平台提供的官方测试样例逐字验证,并避免日志泄露密钥。

2. 付款成功,但本地订单仍是待支付

先检查通知 URL 是否能从公网访问,再查看 Web 服务器、反向代理和应用日志中的请求状态码。还要确认 HTTPS 证书有效、路由没有拦截平台请求、通知验签使用了正确环境的凭据,以及程序返回了平台要求的响应。不能以浏览器返回成功作为修复方法;应通过平台订单查询核实交易状态后再补偿更新。

3. 回调一直重试

平台通常会在未收到认可响应时重试,但具体次数、间隔和判定规则各不相同。检查响应体、HTTP 状态码、超时、程序异常和验签结果。重复通知属于正常情况,服务端应做到重复处理安全,并记录事件是否已处理。不要用“收到过一次就无条件返回成功”的方式掩盖首次处理失败。

4. 提示金额不一致

检查商品价格是否在服务端重新读取、平台要求的金额单位、币种精度、优惠和运费计算,以及订单记录是否与通知字段采用同一单位。应以整数最小货币单位或经过规范化的十进制定点方式保存金额;不要用二进制浮点数直接进行货币相等判断。平台通知中的金额必须与本地应付金额精确匹配,不能因为只有一分钱差异就忽略校验。

5. 重复发货或库存变成负数

检查通知处理是否存在“先查询、后更新”的竞态、订单号是否唯一、发货是否有幂等约束,以及多进程下是否使用事务或原子更新。仅在 PHP 代码中写 if ($status !== 'paid') 不足以保证并发安全。可以通过条件更新、行锁、唯一事件记录和可重试的任务队列共同防护。

6. 本地测试可用,线上回调失败

检查公网解析、TLS 证书链、服务器时间、WAF、防火墙、代理转发的请求体和客户端 IP 限制。若签名含时间戳,服务器时间偏差可能导致校验失败;允许偏差范围由平台规范决定,不应随意放宽。也要确认线上运行的 PHP 扩展、配置缓存和密钥没有与本地环境混淆。

7. 支付链接被篡改或订单被伪造

支付跳转参数不应成为商品定价的权威来源。即使前端提交了订单号,服务器也必须确认订单属于当前用户、仍处于允许支付的状态,并从数据库读取金额。回调中也必须校验商户订单号、金额、商户身份和支付状态。不要把密钥放进 HTML、JavaScript 或移动端客户端。

九、关键安全边界

  • 密钥只在服务端使用:限制文件和环境变量的访问权限,定期轮换,并避免记录到日志、报错页或版本库。
  • 验证请求来源不能替代验签:IP 白名单可作为额外防护,但 IP 范围可能变化;即使来源 IP 符合预期,仍需按协议验签。
  • 不要信任用户可控字段:商品价格、订单状态、支付成功标记和回调地址都不能由浏览器决定。
  • 限制回调端点的数据处理:设置合理的请求体大小、超时和输入校验。除非文档要求,不要让服务端根据请求参数任意访问外部 URL,以免引入 SSRF 风险。
  • 日志只保留必要信息:可记录订单号、事件 ID、处理结果和内部错误编号;按法规和平台要求处理个人信息、支付标识及其他敏感数据。
  • 退款与支付分开处理:“退款请求已提交”不等于“退款成功”。应依照退款接口及通知更新退款状态,并保留可核对的退款流水。

支付平台的速率限制、金额上下限、订单有效期、通知重试次数、签名算法和字段长度都属于平台或接口版本的具体约束。除非文档明确说明,不应在代码中自行假设一个通用数值。项目自身可以设置更严格的商品数量、金额和请求频率限制,但要明确这是本地业务规则,而不是平台参数限制。

十、上线前检查清单

  • 生产商户信息、接口地址、密钥或证书均来自正式配置,测试凭据未混入生产环境。
  • 服务端计算商品价格和应付金额,金额表示方式与平台文档一致。
  • 商户订单号唯一,订单状态转换有明确约束,数据库事务和索引已验证。
  • 异步通知按正式文档验签,并校验商户、订单、金额、币种和支付状态。
  • 通知重试不会重复发货,发货失败有可恢复的任务或人工处理流程。
  • 同步跳转不直接修改支付状态,未收到通知时有订单查询和对账流程。
  • 退款、关闭订单和超时订单有各自的状态处理逻辑。
  • 错误日志不包含密钥等敏感信息,异常订单能通过订单号追踪。
  • 已测试重复通知、并发请求、网络超时、错误金额、未知订单和平台查询失败。

接入支付接口的核心不是让请求“返回成功”,而是让每笔交易都能从本地订单追踪到平台交易,再安全地完成一次交付或退款。先遵循支付平台的正式规范,再把金额校验、状态机、幂等处理和对账机制落到服务端,通常比单纯增加更多支付方式更能减少线上故障。

© 版权声明

相关文章