官方SDK安装(PHP版)

# API V2 V3 对比
# https://pay.weixin.qq.com/doc/global/v3/zh/4012354999
composer require wechatpay/wechatpay

支付和回调

class WechatPayV3
{
    // 支付
    public static function pay($body, $sn, $money, $openid, $notify_url)
    {
        $mchid     = '你的商户号';
        $appId     = '你的小程序appid';
        $apiV3Key  = '你的APIv3密钥';
        $serial_no = '你的商户证书序列号';
        $merchant_category_code = '你的商户行业编码';

        $mchid     = '964383722';
        $appid     = 'wx6278f6d625cb66be';
        $merchant_category_code = '7339';

        /////////////////////////////
        // 微信商户平台
        /////////////////////////////
        // 证书和密钥相关设置教程 https://pay.weixin.qq.com/doc/global/v3/zh/4012354144

        /////////////////////////////
        // APIv3 密钥
        /////////////////////////////
        // https://pay.weixin.qq.com/index.php/core/v/cert/api_cert
        // 该密钥用于加密APIv3的“下载平台证书”和“支付回调通知”中的消息
        $apiV3Key     = 'dxtdj8x4mu2hhp2s6izms6x956d2iu24';

        /////////////////////////////
        // 商户证书序列号
        /////////////////////////////
        // API certificate (CA issued), 点击 View 后可以查看 Serial Number
        // https://pay.weixin.qq.com/index.php/core/v/cert/api_cert
        // 也可以根据商户证书公钥生成
        // openssl x509 -in /www/wwwroot/wechat.app.com/apiclient_cert.pem -noout -serial
        $serial_no    = '7F0234A979F1CA9AC88219D5025FC6F7F26240DA';

        /////////////////////////////
        // API证书
        /////////////////////////////
        // https://pay.weixin.qq.com/index.php/core/v/cert/api_cert
        // API证书用于识别和定义您的身份;部分安全级别较高的API会要求使用证书来识别您的身份,以避免身份被盗用造成的损失。 帮助链接 https://kf.qq.com/product/wechatpaymentmerchant.html#hid=2774
        // 商户证书公钥(apiclient_cert.pem)
        $apiclient_cert = ROOT_PATH .'/apiclient_cert.pem';
        // 商户证书私钥(apiclient_key.pem)
        $apiclient_key = ROOT_PATH .'/apiclient_key.pem';

        /////////////////////////////
        // 微信支付平台证书
        /////////////////////////////
        // 根据“商户证书私钥”和“APIv3 密钥”生成时候会显示,记录下来
        // cd /www/wwwroot/wechat.app.com
        // composer exec CertificateDownloader.php -- -k ${apiV3key} -m ${mchId} -f ${mchPrivateKeyFilePath} -s ${mchSerialNo} -o ${outputFilePath}
        // composer exec CertificateDownloader.php -- -k dxtdj8x4mu2hhp2s6izms6x956d2iu24 -m 964383722 -f /www/wwwroot/wechat.app.com/apiclient_key.pem -s 7F0234A979F1CA9AC88219D5025FC6F7F26240DA -o ./
        $wechatpay_cert_path = ROOT_PATH .'/wechatpay_28D0D5D220A7D5401CA681B0DD5C494277788652.pem';

        /////////////////////////////
        // 平台证书序列号
        /////////////////////////////
        // 根据上步骤的“微信支付平台证书”生成
        // 或者使用程序生成
        $wechatpay_cert_path_content = file_get_contents($wechatpay_cert_path);
        $cert = openssl_x509_parse($wechatpay_cert_path_content);
        $platform_serial_no = strtoupper(dechex($cert['serialNumber']));
        $platform_serial_no = "28D0D5D220A7D5401CA681B0DD5C494277788652";

        // 创建支付对象
        $apiclient_key_content   = file_get_contents($apiclient_key);

        // 创建支付对象
        $instance = Builder::factory([
            'mchid'      => $mchid,
            'serial'     => $serial_no,
            'privateKey' => $apiclient_key_content,
            'certs'      => [ $platform_serial_no => file_get_contents($wechatpay_cert_path) ],
            'base_uri' => 'https://apihk.mch.weixin.qq.com',
        ]);

        // 下单接口参数
        // 直连模式
        // https://pay.weixin.qq.com/doc/global/v3/zh/4013014150
        // 国内支付
        // $result = $instance->v3->pay->transactions->jsapi->post([])
        $result = $instance->v3->global->transactions->jsapi->post([
            'json' => [
                'mchid'        => $mchid,
                'appid'        => $appid,
                'description'  => $body,
                'out_trade_no' => $sn,
                'notify_url'   => $notify_url,
                'trade_type'   => 'JSAPI',
                'merchant_category_code' => $merchant_category_code,
                'amount'       => [
                    'total'    => $money * 100, // 单位:分
                    'currency' => 'CNY', // CNY
                ],
                'payer' => [
                    'openid' => $openid,
                ],
            ],
        ]);

        $resp = $result->getBody()->getContents();
        /**
        // 成功示例
        {"prepay_id": "wx201411101639507cbf6ffd8b0779950874"}
        // 错误示例
        {
            "code": "INVALID_REQUEST",
            "message": "Parameter format verification error",
            "detail": {
                "field": "#/properties/payer",
                "value": "1346177081915535577",
                "issue": "与ALLOF schema不符",
                "location": "body"
            }
        }
         */
        $resp = json_decode($resp, true);
        // 小程序端需要的参数
        $timeStamp = (string)time();
        $nonceStr  = bin2hex(random_bytes(16));
        $package   = "prepay_id=" . $resp['prepay_id'];
        // 生成签名
        $message = $appid . "\n" . $timeStamp . "\n" . $nonceStr . "\n" . $package . "\n";
        $sign = base64_encode(
            openssl_sign($message, $signature, $apiclient_key_content, 'sha256WithRSAEncryption') ? $signature : ''
        );
        return ([
            'timeStamp' => $timeStamp,
            'nonceStr'  => $nonceStr,
            'package'   => $package,
            'signType'  => 'RSA',
            'paySign'   => $sign,
        ]);
    }

    // 余额查询
    public function balance()
    {
        try {
            $instance = WxappV3::getPayInstance();
            // 商家充值退款余额查询
            // https://pay.weixin.qq.com/doc/global/v3/zh/4013068956
            $response = $instance->chain('v3/global/refund/recharge-balance')->get(['debug' => false]);
            // $response => object(GuzzleHttp/Psr7/Response)
            // {"currency":"HKD","remaining_amount":0}
            $resp = $response->getBody()->getContents();
            $jsonArr = json_decode($resp, true);
            // 检查json_decode是否成功
            if (json_last_error() === JSON_ERROR_NONE && is_array($jsonArr)) {
                $remaining_amount = $jsonArr['remaining_amount'] ?? 0;
                $remaining_amount = sprintf("%.2f", $remaining_amount);
                $currency = $jsonArr['currency'] ?? '';
                $balance = $remaining_amount . $currency;
                return json([
                    'code' => 0,
                    'msg' => 'success',
                    'time' => time(),
                    'data' => $jsonArr,
                ]);
                //$this->success('success', $jsonArr);
            }
            $this->error(json_last_error());
        } catch (Exception $e) {
            //throw new Exception('Balance Exception: ' . $e->getMessage());
            $msg = $e->getMessage();
            preg_match('/(\{.*\})/', $msg, $matches);
            if (isset($matches[1])) {
                $jsonArr = json_decode($matches[1], true);
                // 检查json_decode是否成功
                if (json_last_error() === JSON_ERROR_NONE && is_array($jsonArr)) {
                    $msg = isset($jsonArr['message']) && $jsonArr['message'] ? $jsonArr['message'] : $msg;
                }
            }
            $this->error($msg);
        }
    }


    // 回调
    public function notify(){
        $apiV3Key     = 'dxtdj8x4mu2hhp2s6izms6x956d2iu24';
        // 1. 获取微信POST的原始数据
        $body = file_get_contents('php://input');
        $data = json_decode($body, true);

        file_put_contents(ROOT_PATH . 'runtime/wx_notify.log', date('[Y-m-d H:i:s]')."\n".print_r($data, true)."\n", FILE_APPEND);

        Log::record([
            'type' => 'notify',
            'date' => date('Y-m-d H:i:s'),
            'data' => $data,
        ], 'info');

        // 交易回调数据解密样本
        $data2 = [
            'id' => 'a9598c5b-c828-5a51-b86a-c5680ab413c2',
            'create_time' => '2025-06-27T18:01:58+08:00',
            'resource_type' => 'encrypt-resource',
            'event_type' => 'TRANSACTION.SUCCESS',
            'summary' => '支付成功',
            'resource' => [
                'original_type' => 'transaction',
                'algorithm' => 'AEAD_AES_256_GCM',
                'ciphertext' => 'PLpQ63fioczBwi6z6tMcqmpEr/rNH9krzoWGdiDM84Y1uQNtKjZlLjhxLxLlTNYQouVvYZN/OSXcTU59ZL6qiu0sPqWEkoYdUnEAMnDN0DddwAwNppV3J24IGMB7Kuy958pPa1/u3QUAoSkesrxfHrOowncqUyPiSQgRx89u4PfJAhcZ/z/RR7hH/rMi73goad6HMZhZhPGluM3Aas3MwWZCvxo+h0tzKb/5spKnGLLXdp/xoJKvsZv6P3rbWiwIkExF0b8tRN7F/1MLaH0peOk8nJkMUIPE7X92m03m8SNXVCiXrsRxV8IOEg9sw7H5V30Us7akPjI3D4C6LWDttTk9I6tV0Jt4aZBEP6zZgbvHnpO2L/oa1cE/6WPkppb3RmuMSCVbrmVYVw2I01htG7modpurI94yQIl3OnK/Mnd7WVb9YnepczaWs4Gw6EXDmi/6L0/IA0/f6W1QLlpDtF0fg8C1yCIUowWwMZdc71lVRN2DaELuzqOcw+bzXwt/rO5tky3W48zVxA6Wbgx2JOhRRvg2vKUjR+IilNhB8jo0muzBKlKIGM+bX+7D+GZPotL7Y9XTcMkp26rhZv+R/y4eXd7dXSBrcIKB0XkfrQqio4sXMQ==',
                'associated_data' => 'transaction',
                'nonce' => 'ezbtGNqQCZXC',
            ]
        ];

        if (!isset($data['resource'])) {
            $this->wx_pay_fail('No resource found');
        }

        // 2. 解密resource字段
        $associated_data = $data['resource']['associated_data'] ?? '';
        $nonce = $data['resource']['nonce'];
        $ciphertext = $data['resource']['ciphertext'];

        // 解密前,先 base64 解码
        $ciphertext_dec = base64_decode($ciphertext);
        // 最后 16 字节是 tag,前面是密文
        $ciphertext_real = substr($ciphertext_dec, 0, -16);
        $authTag = substr($ciphertext_dec, -16);

        /*
        {
            "id": "4200002695202506278115459465",
            "appid": "wx6278f6d625cb66be",
            "mchid": "964383722",
            "out_trade_no": "202506271801470087",
            "payer": {
                "openid": "o_-6x7c_efnEnnJQAqWIHOTAj1e0"
            },
            "amount": {
                "total": 10,
                "currency": "CNY",
                "payer_total": 10,
                "payer_currency": "CNY",
                "exchange_rate": {
                    "type": "SETTLEMENT_RATE",
                    "rate": 91827010
                }
            },
            "trade_type": "JSAPI",
            "trade_state": "SUCCESS",
            "trade_state_desc": "支付成功",
            "bank_type": "OTHERS",
            "success_time": "2025-06-27T18:01:58+08:00"
        }
        * */

        $plaintext = openssl_decrypt(
            $ciphertext_real,         // 密文
            'aes-256-gcm',            // 算法
            $apiV3Key,                // 密钥
            OPENSSL_RAW_DATA,         // 选项
            $nonce,                   // 随机串
            $authTag,                 // tag
            $associated_data          // 附加数据
        );
        // 明文一般是 JSON,需要再 json_decode
        $info_data = json_decode($plaintext, true);

        // 用APIv3密钥解密
        // $info_body = AesGcm::decrypt($ciphertext, $apiV3Key, $associated_data, $nonce);
        // $info_data = json_decode($info_body, true);

        // 3. 检查订单支付状态
        if ($info_data['trade_state'] == 'SUCCESS') {
            file_put_contents(ROOT_PATH . 'runtime/wx_notify.log', date('[Y-m-d H:i:s]')." trade_state==SUCCESS\n", FILE_APPEND);
            // $info_data['out_trade_no'] 商户订单号
            // $info_data['id'] 微信支付单号
            // $info_data['amount']['total'] 实付金额,单位分
            $order =$this->model->where('order_no',$info_data['out_trade_no'])->find();
            if (!$order || $order->state == '2') {
                $this->wx_pay_success(); // wx
            }
            if ($order['delivery_method'] == 2) {
                // 生成唯一的6位随机数字
                do {
                    $pickUpCode = mt_rand(100000, 999999);  // 生成6位随机数
                    $exists = $this->model->where('pick_up_code', $pickUpCode)->count();  // 检查数据库中是否已存在该 pick_up_code
                } while ($exists > 0);  // 如果该 pick_up_code 已经存在,重新生成

                $order->pick_up_code = $pickUpCode;
            }
            Db::startTrans();
            try {
                $order->pay_time = time();
                $order->state = '2';
                $order->paid = '2';
                $order->trade_no = $info_data['id'];
                $order->save();
                Db::commit();
            } catch (\think\Exception $e) {
                Db::rollback();
                $this->wx_pay_fail(); // wx
            }
            // 4. 返回微信指定成功JSON
            $this->wx_pay_success(); // wx
            exit;
        } else {
            $this->wx_pay_fail(); // wx
        }
    }

    // 退款
    // https://pay.weixin.qq.com/doc/global/v3/zh/4012354571
    public static function refund($number, $refundNumber, $refundFee, $refundDesc)
    {
        $mchid     = '964383722';
        $appid     = 'wx6278f6d625cb66be';
        try {
            $instance = self::getPayInstance();
            $result = $instance->v3->global->refunds->post([
                'json' => [
                    'mchid'  => $mchid,
                    'appid'  => $appid,
                    'out_trade_no' => $number,
                    'out_refund_no' => $refundNumber,
                    'reason'        => $refundDesc,
                    'notify_url'    => request()->domain().'/api/refund_notify', // 退款结果回调
                    'amount' => [
                        'refund'   => intval($refundFee * 100),   // 退款金额,分
                        'total'    => intval($refundFee * 100),   // 订单总金额,分
                        'currency' => 'CNY'  // 货币代码,人民币: CNY,港币: HKD,美元: USD 等
                    ]
                ]
            ]);

            $resp = $result->getBody()->getContents();

            Log::record([
                'type' => 'refund_response',
                'date' => date('Y-m-d H:i:s'),
                'body' => $resp,
            ], 'info');

            /**
            // 正常示例
            {
                "id": "50200603742025070135848778847",
                "out_refund_no": "202507011525530888",
                "create_time": "2025-07-01T15:24:44+08:00",
                "amount": {
                    "refund": 10,
                    "currency": "CNY",
                    "payer_refund": 10,
                    "payer_currency": "CNY",
                    "exchange_rate": {
                        "type": "SETTLEMENT_RATE",
                        "rate": 91827010
                    },
                    "settlement_refund": 10,
                    "settlement_currency": "HKD"
                }
            }
            // 异常示例
            {
                "code": "INVALID_REQUEST",
                "message": "Parameter format verification error",
                "detail": {
                    "field": "#/properties/payer",
                    "value": "1346177081915535577",
                    "issue": "与ALLOF schema不符",
                    "location": "body"
                }
            }

             */
            $resp = json_decode($resp, true);
            return $resp;
        } catch (GuzzleException $e) {
            throw new Exception('Refund GuzzleException: ' . $e->getMessage());
        }
    }

    public function wx_pay_success($msg = 'OK'){
        file_put_contents(ROOT_PATH . 'runtime/wx_notify.log', date('[Y-m-d H:i:s]')." ".$msg."\n", FILE_APPEND);
        // 验签通过,处理订单逻辑
        $response = [
            'code' => 'SUCCESS',
            'message' => $msg
        ];
        http_response_code(200);
        echo json_encode($response);
        exit;
    }

    public function wx_pay_fail($msg = 'FAIL'){
        file_put_contents(ROOT_PATH . 'runtime/wx_notify.log', date('[Y-m-d H:i:s]')." ".$msg."\n", FILE_APPEND);
        // 支付失败,记录日志或其他处理
        $response = [
            'code' => 'SYSTEM_ERROR',
            'message' => $msg
        ];
        http_response_code(400);
        echo json_encode($response);
        exit;
    }
}
# tail -f /www/wwwroot/wechat.app.com/runtime/log/202506/28.log -n500 -f
# 支付回调
[2025-06-28 16:01:34]
Array
(
    [id] => 62abc233-59a9-5e43-92f9-45137a182f77
    [create_time] => 2025-06-28T16:01:33+08:00
    [resource_type] => encrypt-resource
    [event_type] => TRANSACTION.SUCCESS
    [summary] => 支付成功
    [resource] => Array
        (
            [original_type] => transaction
            [algorithm] => AEAD_AES_256_GCM
            [ciphertext] => /gtBbEH2H/p5mv2+URCJZUCCPW3MY+ZvXLXNlHp3ljg04sLrP73ZOcyUZkNBevEmYfE3/23gadwIq6Q6V5072M4UpkZUUg9/ELk8TEeRnFv+xHoQPpwfcqRibVJNRgwiglZuG1kFE9nbMMBRFg01sRmEWyUVJAyCnvNKUPd/5D8vVKzRjGJ1OsVugNYB0TXmdKFufzQkFcFxHNnntqnYCh2NttuQcod6IeA4UVTbBKneQCuGz+2CiTaDDvWCM7eX+vGPWsXEPElNllypuEdb3HlRRg/H3l8XwqeGyejPgFG5NmTIyYS/DV3RaL/8GCVoaw+V9k+HsC4Up8huUKGlI3biS3r3HsyzjF59d9hX4eFD0M/emppc/MNKLUk/GgXyOp+sOcc5Q14r34KtIoZFsNXMnBTo8z9FfZ17VdHwiE3o0gBIcL6itEzwyOMnYw7NAv5MckbMof1EynkZIZgRtpOmKpIYOYNFET38bfPPr25GOoFiXjJPLLDz+QtchtR57Fl8VD1iL6QnrBvGV+IKYT8KLAUHYUX1OHXpE4nH93ejBWO4uwiSHHhyEDxk8t7sBu39OTNUc0XCRx5C4G/NrOfhRoXmJz2xLvhiBRqjjFCNRvw=
            [associated_data] => transaction
            [nonce] => 5T4dZ9fO9sdx
        )

)
# 退款回调
[2025-07-01 16:07:58]
Array
(
    [id] => 7dc5c5dd-3d4f-5f79-ad56-ce96ff9c5fca
    [create_time] => 2025-07-01T16:07:51+08:00
    [resource_type] => encrypt-resource
    [event_type] => REFUND.SUCCESS
    [summary] => 退款成功
    [resource] => Array
        (
            [original_type] => refund
            [algorithm] => AEAD_AES_256_GCM
            [ciphertext] => FO8/PKYW2UO2KnB9r064E8wKUW2KeauFmK+17r7nj85aTlVaJsERTALo0/9dnWZ1s1LQUpkttJVdAx3QCTPLSVe6L885Wx2CeEpGvHCnnAj+p5YsE1ihfe9KxATawv5GP7Gf5knRfNC8gPx+JUINWx3eFuLiafdYWYW1ppiwxFOc0M6kasTFeN5mBk5NFmG+PyCZreO9CUMMBtpTJop/dZzyBtEodeYUJMJvNBYXZ1KWHr67/5s0azx98K0MXdOklBgUjndM7X0kKeNT+GzUK7iVq8P+QoL78clASW1GnkUn0HgGzX5S/ohceARFqgDY4F5i7GjD/EB8T3WfpP3FKxmZ/FYhVoai9WOGav2Z/IX9sSNIL3PrA/WivLogs7T9iLl/UTEhW0a2vDhZ/Ff8uLMa072t/nie/O0rYoioCk9tF82uwC5Awj8JECh8255h4hdC/WRZXycWidFsit56a8eTzSnym3s8jXFCwzInsbzpw/NFETpcYxD5b3stIPASx2Jo+ibI4rTKXzK/fAnDjvAK6pQdk4m1gSQiRuJhmg+VUhCC+DvpQXVgTLXYGpuiNgBds1XCoa1vB+L6ShPOVS5LG1UVX2Q2eNDJVjWyYsxrJ7yIwQcwuEmz8OJju9ALcz8lqDQNviRAl/0EQ6qEISM3q2Wgz/5jd2Eh7TKtnrCEJ4e2QiQH9Vw=
            [associated_data] => refund
            [nonce] => ToK9fpFXUxNR
        )

)

文档资料

  • composer require wechatpay/wechatpay https://github.com/wechatpay-apiv3/wechatpay-php

  • 商户号管理 https://pay.weixin.qq.com/index.php/xphp/v/coverseas_mch_pin/sec_center_page#/

  • 小程序管理 https://mp.weixin.qq.com/wxamp/devprofile/get_profile?token=266415899&lang=zh_CN

  • 商户 API 证书 && 商户 API 私钥 && 微信支付平台证书 概念理解 https://github.com/wechatpay-apiv3/wechatpay-php?tab=readme-ov-file#%E6%A6%82%E5%BF%B5

  • JSAPI支付下单 https://pay.weixin.qq.com/doc/global/v3/zh/4013014150

  • 微信支付API列表 https://pay.weixin.qq.com/doc/global/v3/zh/4012354163

  • uni.requestPayment https://uniapp.dcloud.net.cn/api/plugins/payment.html#requestpayment

  • 国内版支付文档(跟境外版本不一样) https://pay.weixin.qq.com/doc/v3/merchant/4012076511 https://pay.weixin.qq.com/doc/v3/merchant/4012791897

  • 如何获取商户API证书 https://pay.weixin.qq.com/doc/v3/merchant/4013053053

  • 通过HBuilderX运行uniapp到微信者开发工具 https://cloud.tencent.com/developer/article/2344571