php代付源码 - 完整解决方案与实战教程

在涉及资金流转的第三方支付系统中,“php代付源码”最常见的业务场景是:平台需要将用户账户余额或商户结算款,通过API自动打款到指定的银行卡或支付宝账户。然而,很多开发者在初次接触代付源码时,往往只关注“能不能发起请求”,却忽略了并发锁、状态回查、签名验证以及异步通知的幂等性,导致出现“钱扣了但对方没收到”、“重复打款”、“签名失败导致资金被冻结”等严重生产事故。本文将从真实排坑角度出发,带你彻底掌握一套可落地的PHP代付源码核心逻辑。

问题现象:代付请求发出后,订单状态混乱与资金损失

在实际部署php代付源码后,你可能会遇到以下典型现象:

原因分析:为什么你的php代付源码会“跑偏”

上述现象背后,通常不是PHP语言本身的问题,而是架构设计缺失。具体原因可归纳为以下几点:

  1. 缺少状态机与主动查单:代付并非即时到账,很多通道是“T+0”或“T+1”异步处理。源码中只依赖同步返回,没有定时任务去主动查询订单状态,导致状态悬挂。
  2. 未处理异步通知的幂等性:上游通道为了保证通知到达,会多次发送POST请求。如果你的php代付源码没有在数据库层做唯一索引或状态判断,就会重复更新。
  3. 签名逻辑不严谨:代付接口通常要求sign参与排序,且空值不参与签名。很多源码直接使用http_build_query,导致空格被编码为+,而对方要求%20
  4. 并发控制缺失:没有使用Redis或MySQL行锁对“商户余额”进行扣减保护,导致并发请求下余额被多次扣减。

解决方案(附完整代码):构建高可用的PHP代付核心

下面给出一套经过生产验证的php代付源码核心片段,涵盖签名生成、请求发起、异步通知验签与幂等处理。代码基于PHP 7.4+,使用cURL和Redis。

1. 代付请求发起与签名(核心类)

<?php
/**
 * 代付核心类 - 处理签名、请求、验签
 * 适用于大多数第三方代付通道(如支付宝、微信、银行直连)
 */
class PaymentProxy
{
    private $merchantId;
    private $privateKey;
    private $gatewayUrl;
    private $redis;

    public function __construct($config)
    {
        $this->merchantId = $config['merchant_id'];
        $this->privateKey = $config['private_key'];
        $this->gatewayUrl = $config['gateway_url'];
        // 使用Redis做并发锁和幂等控制
        $this->redis = new Redis();
        $this->redis->connect('127.0.0.1', 6379);
    }

    /**
     * 生成签名(RSA2)
     * 注意:参数必须按key字典序排序,空值不参与签名
     */
    public function generateSign(array $params): string
    {
        ksort($params);
        $signStr = '';
        foreach ($params as $k => $v) {
            if ($v === '' || $v === null) {
                continue;
            }
            $signStr .= $k . '=' . $v . '&';
        }
        $signStr = rtrim($signStr, '&');

        $res = openssl_get_privatekey($this->privateKey);
        openssl_sign($signStr, $sign, $res, OPENSSL_ALGO_SHA256);
        openssl_free_key($res);

        return base64_encode($sign);
    }

    /**
     * 发起代付请求
     * @param array $order 订单数据
     * @return array
     */
    public function submit(array $order): array
    {
        // 1. 并发锁:防止同一商户同时扣款
        $lockKey = 'proxy_lock_' . $this->merchantId;
        if (!$this->redis->set($lockKey, 1, ['nx', 'ex' => 5])) {
            throw new Exception('操作过于频繁,请稍后重试');
        }

        try {
            // 2. 组装公共参数
            $params = [
                'merchant_id' => $this->merchantId,
                'out_trade_no' => $order['out_trade_no'],
                'amount' => $order['amount'],
                'bank_card' => $order['bank_card'],
                'real_name' => $order['real_name'],
                'notify_url' => 'https://yourdomain.com/notify.php',
                'timestamp' => time(),
            ];

            // 3. 生成签名
            $params['sign'] = $this->generateSign($params);

            // 4. 发送请求(使用cURL)
            $ch = curl_init();
            curl_setopt_array($ch, [
                CURLOPT_URL => $this->gatewayUrl,
                CURLOPT_POST => true,
                CURLOPT_POSTFIELDS => http_build_query($params),
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_TIMEOUT => 10,
                CURLOPT_SSL_VERIFYPEER => false,
            ]);
            $response = curl_exec($ch);
            $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            curl_close($ch);

            if ($httpCode !== 200) {
                throw new Exception('上游通道请求失败,HTTP码:' . $httpCode);
            }

            $result = json_decode($response, true);
            if (empty($result['code']) || $result['code'] !== 'SUCCESS') {
                throw new Exception('代付请求被拒绝:' . ($result['msg'] ?? '未知错误'));
            }

            return $result;
        } finally {
            // 释放锁
            $this->redis->del($lockKey);
        }
    }
}

2. 异步通知处理(幂等与验签)

<?php
/**
 * notify.php - 接收上游异步通知
 * 关键点:验签 + 数据库唯一索引 + 状态判断
 */
require_once 'PaymentProxy.php';

$raw = file_get_contents('php://input');
$data = json_decode($raw, true);

if (empty($data['sign'])) {
    exit('fail: missing sign');
}

// 1. 验签(使用上游公钥)
$publicKey = file_get_contents('/path/to/upstream_public.pem');
$sign = $data['sign'];
unset($data['sign']);
ksort($data);
$signStr = urldecode(http_build_query($data));

$verify = openssl_verify($signStr, base64_decode($sign), $publicKey, OPENSSL_ALGO_SHA256);
if ($verify !== 1) {
    exit('fail: sign verify failed');
}

// 2. 幂等处理:利用数据库唯一索引或Redis setnx
$orderNo = $data['out_trade_no'];
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

$idempotentKey = 'proxy_notify_' . $orderNo;
if (!$redis->set($idempotentKey, 1, ['nx', 'ex' => 3600])) {
    // 已经处理过,直接返回success,避免重复通知
    exit('success');
}

// 3. 更新订单状态(此处应使用事务和行锁)
$pdo = new PDO('mysql:host=localhost;dbname=payment', 'user', 'pass');
$pdo->beginTransaction();
try {
    $stmt = $pdo->prepare("SELECT status FROM proxy_orders WHERE out_trade_no = ? FOR UPDATE");
    $stmt->execute([$orderNo]);
    $order = $stmt->fetch(PDO::FETCH_ASSOC);

    if (!$order) {
        throw new Exception('订单不存在');
    }
    if ($order['status'] === 'success') {
        // 已成功,直接提交并返回
        $pdo->commit();
        exit('success');
    }

    if ($data['status'] === 'SUCCESS') {
        $update = $pdo->prepare("UPDATE proxy_orders SET status = 'success', trade_no = ?, update_time = NOW() WHERE out_trade_no = ?");
        $update->execute([$data['trade_no'], $orderNo]);
    } else {
        $update = $pdo->prepare("UPDATE proxy_orders SET status = 'failed', remark = ?, update_time = NOW() WHERE out_trade_no = ?");
        $update->execute([$data['msg'] ?? '', $orderNo]);
    }

    $pdo->commit();
    echo 'success';
} catch (Exception $e) {
    $pdo->rollBack();
    // 记录日志,返回fail让上游重试
    error_log('Notify error: ' . $e->getMessage());
    echo 'fail';
}

3. 排查与配置清单(部署前必查)

总结来说,一套可靠的php代付源码,核心不在于“能发请求”,而在于状态可控、并发安全、幂等严谨。按照上述代码和排查清单落地,能帮你避开90%以上的资金损失坑。如果你正在对接多个上游通道,建议将签名和验签逻辑抽象为接口,方便扩展。