TP5 實現(xiàn)支付寶APP/PC端統(tǒng)一下單支付(詳細(xì)步驟)

1、前期準(zhǔn)備工作

1.1、申請支付寶支付

我們需要注意配置的以下幾點:

  • 需要配置 接口加簽方式支付寶開放平臺開發(fā)助手生成的公鑰需要配置,私鑰文件保存在文件中,
  • 需要配置 IP白名單配置的IP才可以調(diào)用該應(yīng)用的接口功能
  • 支付寶APP支付需要 上線 才能支付,應(yīng)用公私鑰自己生成,支付寶公鑰匙支付寶給的。

1.2、安裝依賴

composer 命令安裝: composer require yansongda/pay

  • 我這里安裝的是 yansongda/pay 擴(kuò)展包,包括詳細(xì)的相關(guān)操作和使用方法。
  • 該依賴包支持以下 :
    支付寶支付(電腦支付、手機(jī)網(wǎng)站支付、APP 支付、刷卡支付、掃碼支付、賬戶轉(zhuǎn)賬、小程序支付)
    微信支付(公眾號支付、小程序支付、H5 支付、刷卡支付、掃碼支付、APP 支付、企業(yè)付款、普通紅包、分裂紅包)

2、代碼實現(xiàn)

2.1、配置文件 config.php

//支付寶支付設(shè)置
'alipay' => [
    'app_id' => '2021001232323232',
    'notify_url' => '線上的支付寶異步跳轉(zhuǎn)地址',
    'ali_public_key' => '應(yīng)用公鑰',
    'private_key' => '生成的應(yīng)用秘鑰',
    // 使用公鑰證書模式,請配置下面兩個參數(shù),同時修改ali_public_key為以.crt結(jié)尾的支付寶公鑰證書路徑,如(./cert/alipayCertPublicKey_RSA2.crt)
    // 'app_cert_public_key' => './cert/appCertPublicKey.crt', //應(yīng)用公鑰證書路徑
    // 'alipay_root_cert' => './cert/alipayRootCert.crt', //支付寶根證書路徑
    'log' => [ // optional
        'file' => './logs/alipay.log',
        'level' => 'info', // 建議生產(chǎn)環(huán)境等級調(diào)整為 info,開發(fā)環(huán)境為 debug
//            'type' => 'daily', // optional, 可選 daily.
        'max_file' => 30, // optional, 當(dāng) type 為 daily 時有效,默認(rèn) 30 天
    ],
    'sign_type' => "RSA2",
    'http' => [ // optional
        'timeout' => 5.0,
        'connect_timeout' => 5.0,
        // 更多配置項請參考 [Guzzle](https://guzzle-cn.readthedocs.io/zh_CN/latest/request-options.html)
    ],
//        'mode' => 'dev', // optional,設(shè)置此參數(shù),將進(jìn)入沙箱模式
],

2.2、業(yè)務(wù)層代碼

2.2.1、根據(jù)業(yè)務(wù)創(chuàng)建訂單
/**
 * @ApiTitle    (用戶開通VIP創(chuàng)建不同的訂單)
 * @ApiMethod   (POST)
 */
public function createOrder()
{
    //根據(jù)傳入內(nèi)容或者查庫獲取相關(guān)的數(shù)據(jù)
    //$amount 需要內(nèi)部計算,不能傳入
    //$userId 從Token中獲取用戶ID
    //$orderNumber 需要生成隨機(jī)不重復(fù)的字符串,用于商戶內(nèi)部訂單號

    //存在相同類型未失效的訂單時候則不創(chuàng)建訂單,直接返回訂單號

    //生成資金流水記錄
    Db::startTrans();
    try {
        // 具體業(yè)務(wù)數(shù)據(jù)表插入以及操作

        //將數(shù)據(jù)插入資金表
        (new UserAccountModel)->insert([
            'from_id' => $userId, //支付方ID(系統(tǒng)默認(rèn)為1)
            'to_id' => 1, //收款方ID(系統(tǒng)默認(rèn)為1)
            'type' => 1, //資金類型:1=VIP開通/升級,2=推薦提成,3=退款
            'money' => $amount, //資金金額
            'desc' => $subject, //相關(guān)描述
            'pay_status' => 0, //支付狀態(tài):0=未到賬,1=已到賬
            'order_number' => $orderNumber, //訂單流水號
            'create_time' => date('Y-m-d H:i:s', time()), //創(chuàng)建時間
        ]);
        Db::commit();

    } catch (\Exception $e) {
        Db::rollback();
        $this->error($e->getMessage());
    }

    $this->success('創(chuàng)建訂單成功!', ['order_number' => $orderNumber,]);
}
  • $amount 表示支付金額,需要根據(jù)具體業(yè)務(wù)計算,不能取 input 傳入的值。
  • pay_status 用于判斷訂單是否支付完成,這一步主要通過訂單查詢/異步調(diào)用時候成功的情況下才會變?yōu)?
  • 另外訂單表中需要有一個 is_deal字段,用于判斷 是否處理(0=否,1=是)支付成功的操作 ,因為 異步調(diào)用 有時候會出現(xiàn)問題,我們也需要在 調(diào)用支付寶查詢訂單 的時候通過這個字段去判斷是否處理從而去 更新訂單表。
2.2.2、用戶統(tǒng)一的支付訂單
 /**
     * @ApiTitle    (用戶支付訂單)
     * @ApiMethod   (POST)
     */
    public function payAmount()
    {
        //支付類型 10-APP微信支付 11-微信小程序支付 12-H5調(diào)起微信支付 13-生成微信支付二維碼 20-APP支付寶支付 21-PC支付寶支付 22-H5支付寶支付
        $payType = intval(input('pay_type')) ?? 0;
        $orderNumber = input('order_number') ?? 0; //內(nèi)部訂單號
        if (!$orderNumber) $this->error('訂單號不得為空!');

        //相關(guān)的業(yè)務(wù)類型判斷
        // ....
        // 計算相關(guān)的資金價格 $amount

        //操作備注
        $payTypeText = '';
        $accountType = 0;
        if ($payType == 10 || $payType == 11 || $payType == 12 || $payType == 13) {
            $payTypeText = '微信支付';
            $accountType = 1;
        }
        if ($payType == 20 || $payType == 21) {
            $payTypeText = '支付寶';
            $accountType = 2;
        }
        if ($vipType == 1) $subject = $payTypeText . '方式開通' . $newVip['title'] . ',充值金額:' . $amount . '元';
        if ($vipType == 2) $subject = $payTypeText . '方式續(xù)費' . $newVip['title'] . ',充值金額:' . $amount . '元';
        if ($vipType == 3) $subject = $payTypeText . '方式升級' . $newVip['title'] . ',充值金額:' . $amount . '元';

        //更新資金表
        (new UserAccountModel)->where('order_number', $orderNumber)->update([
            'desc' => $subject, //相關(guān)描述
            'pay_type' => $accountType, //支付方式:1=微信支付,2=支付寶,3=銀行卡,11=其他
        ]);

    if ($accountType == 1) { //10-APP微信支付 11-微信小程序支付 12-H5調(diào)起微信支付 13-生成微信支付二維碼
        $result = (new WeChatService())->unify($payType, $subject, $orderNumber, $amount);
        if(!$result) $this->error('調(diào)起微信支付失??!');
        $result['order_number'] = $orderNumber;
        $result['amount'] = $amount;
        $this->success('調(diào)起微信支付成功', $result); //支付成功

    } elseif ($accountType == 2) {
        //20-APP支付寶支付 21-PC支付寶支付 22-H5支付寶支付
        //訂單內(nèi)容
        $order = [
            'out_trade_no' => $orderNumber,
            'total_amount' => $amount,
            'subject' => $subject,
        ];
               $ailpayConfig = config('alipay');
        if ($payType == 20) $alipay = Pay::alipay($ailpayConfig)->app($order); //app支付
        if ($payType == 21) {
            //PC支付
            $ailpayConfig['return_url'] = '支付成功界面url';
            $alipay = Pay::alipay($ailpayConfig)->web($order);
        }
        if ($payType == 22) {
            //手機(jī)網(wǎng)站支付
            $ailpayConfig['return_url'] = '支付成功界面url?order_num=' . $orderNumber . '&money=' . $amount;
            $alipay = Pay::alipay($ailpayConfig)->wap($order);
        }

        $res = $alipay->getContent();
        $this->success('調(diào)起支付寶支付成功', $res);
    }
}

PS:PC支付和手機(jī)H5支付的時候需要填寫 支付成功界面的url

  • 其中 $res 是返回給前端調(diào)起支付寶的參數(shù),如下:
app_id=2021123131332323232&format=JSON&charset=utf-8&sign_type=RSA2&version=1.0&notify_url=http%3A%2F%2Ftsdfst.asdasd.com%2Fapi%2FPayment%2FalipayNotify&timestamp=2020-07-09+16%3A20%3A50&biz_content=%7B%22out_trade_no%22%3A%22A1593a1312390%22%2C%22total_amount%22%3A%220.01%22%2C%22subject%22%3A%22%5Cu652f%5Cu4ed8%5Cu5b9d%5Cu65b9%5Cu5f0f%5Cu7eed%5Cu8d39%5Cu94c2%5Cu91d1VIP%5Cuff0c%5Cu5145%5Cu503c%5Cu91d1%5Cu989d%5Cuff1a0.01%5Cu5143%22%2C%22product_code%22%3A%22QUICK_MSECURITY_PAY%22%7D&method=alipay.trade.app.pay&sign=bvIwgGf%2FByYOjhNX%2B%2B0JmlPBwOwK%2BguZekrB1JZ6PJ61srGauandLwnDlj01u%2FyFo%2Fn5PNHyao%2FdDOCQCE5UxObqe03gw5PYv3oFFy42NEzTqD8J6cX91IMfSnxptQmN746lSqSmETyEHOR7LUNP%2BSajq58oOlF5Awke5XagBb5aW55R%2Ft5KwAOUiv%2FUCk6C2cEPUS2%2FfJAf8RdjkkYCKoaDCCcFwRoPhlW2YjuUu6Isdasdasdasta8WNliJA0j6HE6iB7%2BVLs0iskjiBc0hAP6i06i3H5DNz7%2FY8cvasdasdasdasdmyhTgcHA%3D%3D
2.2.3、查詢訂單
/**
 * @ApiTitle    (獲取訂單支付狀態(tài))
 * @ApiMethod   (GET)
 */
public function getPayStatus()
{
    $orderNumber = input('order_number'); //內(nèi)部訂單流水號
    $userAccount = (new UserAccountModel)->where('order_number', $orderNumber)->find();

    if (!$userAccount) $this->error('不存在該訂單!');

    //未處理:0=否,1=是(用于處理業(yè)務(wù)邏輯)
    if ($userAccount->is_deal == 0) {
        //微信異步調(diào)用異常情況下:
        //支付成功根據(jù)支付方式:1=微信支付,2=支付寶,3=銀行卡,11=其他
        if ($userAccount->pay_type == 1) {
            //微信支付查看訂單
            $app = (微信服務(wù)類)->connect(10);
            $res = $app->order->queryByOutTradeNumber($orderNumber);
            if ($res['return_code'] === 'SUCCESS') { // return_code 表示通信狀態(tài),不代表支付狀態(tài)
                if ($res['result_code'] === 'SUCCESS') { //以下字段在return_code為SUCCESS的時候有返回
                    if ($res['trade_state'] === 'SUCCESS') { //支付成功
                        $tradeNo = $res['transaction_id']; //微信支付訂單號
                        $totalFee = $res['total_fee']; //充值總金額
                        $timeEnd = $res['time_end']; //支付完成時間

                        //如果金額不匹配直接退出
                        if (($userAccount->money) != $totalFee / 100) goto S;
                        //支付成功
                        $this->paySuccess($orderNumber, $tradeNo, $timeEnd);
                    } else {
                        goto S;
                    }
                }
            }

        } elseif ($userAccount->pay_type == 2) {
            //支付寶查看訂單
            $res = Pay::alipay(config('alipay'))->find($orderNumber);
            $state = $res->trade_status; //訂單狀態(tài)
            $outTradeNo = $res->out_trade_no; //自定義訂單號
            $tradeNo = $res->trade_no; //支付寶訂單號
            $totalAmount = $res->total_amount; //充值總金額
            $appId = $res->app_id; //收款方的APPID
            $payTime = $res->gmt_payment; //交易付款時間

            if (!in_array($state, ['TRADE_SUCCESS', 'TRADE_FINISHED'])) goto S;
            if (!$userAccount) goto S;
            if ($userAccount['money'] != $totalAmount) goto S;
            if ($appId != config('alipay.app_id')) goto S;

            //支付成功
            $this->paySuccess($outTradeNo, $tradeNo, $payTime);
        }
    }

    S:
    //需要再查一次訂單狀態(tài)
    $userAccount = (用戶資金表)->where('order_number', $orderNumber)->find();

    //返回數(shù)據(jù)前端
    $data = [
        'order_number' => $orderNumber,
        'vip_title' => $vip['title'],
        'pay_status' => $userAccount['pay_status'], //支付狀態(tài):0=待支付,1=支付成功
        'pay_type' => $userAccount['pay_type'], //支付方式:1=微信支付,2=支付寶,3=銀行卡,11=其他
    ];

    $this->success('獲取訂單信息成功!', $data);
}
  • 查詢訂單時候用到 is_deal 用于判斷異步接口是否調(diào)用處理,沒有則調(diào)用一次
2.2.4、支付寶異步操作
/**
 * @ApiTitle    (支付寶異步接口)
 * @ApiMethod   (POST)
 * @ApiRoute    (/api/Payment/alipayNotify)
 * @ApiInternal
 */
public function alipayNotify()
{
    $alipay = Pay::alipay(config('alipay'));

    $data = $alipay->verify(); // 是的,驗簽就這么簡單!

    $state = $data->trade_status; //訂單狀態(tài)
    $outTradeNo = $data->out_trade_no; //自定義訂單號
    $tradeNo = $data->trade_no; //支付寶訂單號
    $totalAmount = $data->total_amount; //充值總金額
    $appId = $data->app_id; //收款方的APPID
    $payTime = $data->gmt_payment; //交易付款時間
    //獲取對應(yīng)訂單的資金流水信息
    $res = (new UserAccountModel)->where('order_number', $outTradeNo)->find();

    // 請自行對 trade_status 進(jìn)行判斷及其它邏輯進(jìn)行判斷,在支付寶的業(yè)務(wù)通知中,只有交易通知狀態(tài)為 TRADE_SUCCESS 或 TRADE_FINISHED 時,支付寶才會認(rèn)定為買家付款成功。
    // 1、商戶需要驗證該通知數(shù)據(jù)中的out_trade_no是否為商戶系統(tǒng)中創(chuàng)建的訂單號;
    // 2、判斷total_amount是否確實為該訂單的實際金額(即商戶訂單創(chuàng)建時的金額);
    // 3、校驗通知中的seller_id(或者seller_email) 是否為out_trade_no這筆單據(jù)的對應(yīng)的操作方(有的時候,一個商戶可能有多個seller_id/seller_email);
    // 4、驗證app_id是否為該商戶本身。
    // 5、其它業(yè)務(wù)邏輯情況。
    if (!in_array($state, ['TRADE_SUCCESS', 'TRADE_FINISHED'])) return $alipay->success()->send();
    if (!$res) return $alipay->success()->send();
    if ($res['money'] != $totalAmount) return $alipay->success()->send();
    if ($appId != config('alipay.app_id')) return $alipay->success()->send();

    //支付成功
    $this->paySuccess($outTradeNo, $tradeNo, $payTime);

    Log::debug('Alipay notify', $data->all());

    return $alipay->success()->send();// laravel 框架中請直接 `return $alipay->success()`
}
  • 其中 $this->paySuccess($outTradeNo, $tradeNo, $payTime);用于 支付成功調(diào)用的接口
  • 該接口主要根據(jù)(訂單號查詢到的)訂單業(yè)務(wù)類型 去調(diào)不同的方法,同時需要加入 并發(fā)鎖
2.2.5、支付成功的方法
/**
 * @ApiTitle    (支付成功的操作,需要鎖)
 * @ApiInternal
 * @param string $outTradeNo 商戶內(nèi)部訂單號
 * @param string $tradeNo 微信/支付寶訂單號
 * @param string $payTime 支付時間
 * @return bool|string
 * @throws \think\db\exception\DataNotFoundException
 * @throws \think\db\exception\ModelNotFoundException
 * @throws \think\exception\DbException
 */
private function paySuccess($outTradeNo, $tradeNo, $payTime)
{
    //加鎖失??!
    if (!RedisService::lock('paySuccess_' . $outTradeNo)) return false;

    //查詢訂單
    $res = (用戶資金表)->where('order_number', $outTradeNo)->find();
    if (!$res) return false;
    Db::startTrans();
    try {

        //更新資金表狀態(tài)
        (用戶資金表)->where('order_number', $outTradeNo)->update([
            'trade_no' => $tradeNo, //微信訂單號
            'pay_time' => $payTime, //支付時間
            'pay_status' => 1, //支付狀態(tài):0=未到賬,1=已到賬
        ]);

        //查看訂單類型  1=VIP開通/升級,2=推薦提成,3=退款
        if ($res->type == 1 && $res->is_deal == 0) {
            $res = $this->vipSuccess($res['from_id'], $outTradeNo);
            if (!$res) throw new Exception('訂單狀態(tài)處理異常');
        }

        Db::commit();
    } catch (\Exception $e) {
        Db::rollback();
        //記錄資金日志
        Log::warning($e->error());
        return false;
    }
    return true;
}
  • $this->vipSuccess($res['from_id'], $outTradeNo);是充值會員成功的方法
2.2.6、Redis鎖方法
  • composer安裝 predis,命令行:composer require predis/predis點擊查看鏈接。
  • 新建一個 RedisServer.php 服務(wù)類
<?php

namespace app\common\service;

use app\common\controller\Api;
use Predis\Client;

class RedisService extends Api
{
    //Redis并發(fā)鎖
    const SU_REDIS_LOCK = 'redis::lock::'; //Redis并發(fā)鎖(后面跟對應(yīng)業(yè)務(wù)的鎖名)

    private static $prefix = '';
    private static $client;

    /**
     * 單例模式獲取redis連接實例
     * @return Client
     */
    public static function connect()
    {
        if (!self::$client) {
            self::$prefix = config('redis_prefix');
            $config = [
                'scheme' => 'tcp',
                'host' => config('redis_host'),
                'port' => config('redis_port'),
                'timeout' => 60,
                'read_write_timeout ' => 60,
            ];
            //沒有配置密碼時,不傳入密碼項參數(shù)
            if (config('redis_password')) $config['password'] = config('redis_password');

            self::$client = new Client($config, ['prefix' => self::$prefix]);
        }

        return self::$client;
    }
    /**
     * 添加自定義并發(fā)鎖
     * 原理是redis的單線程操作
     * @param string $lockName 鎖名
     * @param int $expireTTL 過期時間
     * @return bool 是否由當(dāng)前調(diào)用加鎖成功
     */
    public static function lock(string $lockName, int $expireTTL = 10)
    {
        $redis = self::connect();
        $countKey = self::SU_REDIS_LOCK . $lockName;
        $flag = false; //默認(rèn)是加鎖失敗

        $redisIncr = $redis->incr($countKey); //只有第一個操作的返回是1
        if ($redisIncr === 1) {
            $redis->expire($countKey, $expireTTL);
            $flag = true; //只有第一次操作的才算加鎖成功
        }

        return $flag;
    }

    /**
     * 解除自定義并發(fā)鎖
     * @param string $lockName 鎖名
     * @return bool 是否成功
     */
    public static function unlock(string $lockName)
    {
        $countKey = self::SU_REDIS_LOCK . $lockName;

        return (bool)self::connect()->del([$countKey]);
    }

}

大功告成,其中調(diào)試也會遇到問題,我們可以在日志中進(jìn)行查看,日志在配置中可以進(jìn)行修改。
如果有什么問題可以留言,歡迎互相交流共進(jìn)步。
另外還有詳細(xì)的 TP5 實現(xiàn)APP/二維碼/小程序/H5等微信支付(詳細(xì)步驟)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容