TP5 實現支付寶APP支付(詳細步驟) 1、前期準備工作 2、代碼實現

1、前期準備工作

1.1、申請支付寶支付

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

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

1.2、安裝依賴

composer 命令安裝: composer require yansongda/pay

  • 我這裏安裝的是 yansongda/pay 擴展包,包括詳細的相關操作和使用方法。
  • 該依賴包支持以下 :
    支付寶支付(電腦支付、手機網站支付、APP 支付、刷卡支付、掃碼支付、賬戶轉賬、小程序支付)
    微信支付(公衆號支付、小程序支付、H5 支付、刷卡支付、掃碼支付、APP 支付、企業付款、普通紅包、分裂紅包)

2、代碼實現

2.1、配置文件 config.php

//支付寶支付設置
'alipay' => [
    'app_id' => '2021001232323232',
    'notify_url' => '線上的支付寶異步跳轉地址',
    'ali_public_key' => '應用公鑰',
    'private_key' => '生成的應用祕鑰',
    // 使用公鑰證書模式,請配置下面兩個參數,同時修改ali_public_key爲以.crt結尾的支付寶公鑰證書路徑,如(./cert/alipayCertPublicKey_RSA2.crt)
    // 'app_cert_public_key' => './cert/appCertPublicKey.crt', //應用公鑰證書路徑
    // 'alipay_root_cert' => './cert/alipayRootCert.crt', //支付寶根證書路徑
    'log' => [ // optional
        'file' => './logs/alipay.log',
        'level' => 'info', // 建議生產環境等級調整爲 info,開發環境爲 debug
//            'type' => 'daily', // optional, 可選 daily.
        'max_file' => 30, // optional, 當 type 爲 daily 時有效,默認 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,設置此參數,將進入沙箱模式
],

2.2、業務層代碼

2.2.1、根據業務創建訂單
/**
 * @ApiTitle    (用戶開通VIP創建不同的訂單)
 * @ApiMethod   (POST)
 */
public function createOrder()
{
    //根據傳入內容或者查庫獲取相關的數據
    //$amount 需要內部計算,不能傳入
    //$userId 從Token中獲取用戶ID
    //$orderNumber 需要生成隨機不重複的字符串,用於商戶內部訂單號

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

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

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

    $this->success('創建訂單成功!', ['order_number' => $orderNumber,]);
}
  • $amount 表示支付金額,需要根據具體業務計算,不能取 input 傳入的值。
  • pay_status 用於判斷訂單是否支付完成,這一步主要通過訂單查詢/異步調用時候成功的情況下才會變爲1
  • 另外訂單表中需要有一個 is_deal字段,用於判斷 是否處理(0=否,1=是)支付成功的操作 ,因爲 異步調用 有時候會出現問題,我們也需要在 調用支付寶查詢訂單 的時候通過這個字段去判斷是否處理從而去 更新訂單表
2.2.2、用戶統一的支付訂單
 /**
     * @ApiTitle    (用戶支付訂單)
     * @ApiMethod   (POST)
     */
    public function payAmount()
    {
        $payType = intval(input('pay_type')) ?? 0; //支付類型 10-APP微信支付 20-APP支付寶
        $orderNumber = input('order_number') ?? 0; //內部訂單號
        if (!$orderNumber) $this->error('訂單號不得爲空!');

        //相關的業務類型判斷
        // ....
        // 計算相關的資金價格 $amount

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

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

        if ($payType == 10) { //APP微信支付
            $result = (new WeChatService())->unify(10, 'APP', $subject, $orderNumber, $amount);

            $this->success('調起微信支付成功', $result); //支付成功

        } elseif ($payType == 20) { //支付寶支付
            //訂單內容
            $order = [
                'out_trade_no' => $orderNumber,
                'total_amount' => $amount,
                'subject' => $subject,
            ];
            $alipay = Pay::alipay(config('alipay'))->app($order); //app支付
            $res = $alipay->getContent();
            $this->success('調起支付寶支付成功', $res);
        }
    }
  • 其中 $res 是返回給前端調起支付寶的參數,如下:
app_id=202100116232323232&format=JSON&charset=utf-8&sign_type=RSA2&version=1.0&notify_url=http%3A%2F%2Ftest.sulinks.com%2Fapi%2FPayment%2FalipayNotify&timestamp=2020-07-09+16%3A20%3A50&biz_content=%7B%22out_trade_no%22%3A%22A15938403231190%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%2FfJAf8RdjkkYCKoaDCCcFwRoPhlW2YjuUu6IeMtdWtTGYTifoIUpf8WNliJA0j6HE6iB7%2BVLs0iskjiBc0hAP6i06i3H5DNz7%2FY8cvBxln573DXKdnLABzGu4TFb3UQSDl0JTXfmyhTgcHA%3D%3D
2.2.3、查詢訂單
/**
 * @ApiTitle    (獲取訂單支付狀態)
 * @ApiMethod   (GET)
 */
public function getPayStatus()
{
    $orderNumber = input('order_number'); //內部訂單流水號
    $userAccount = (new UserAccountModel)->where('order_number', $orderNumber)->find();

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

    //未處理:0=否,1=是(用於處理業務邏輯)
    if ($userAccount->is_deal == 0) {
        //微信異步調用異常情況下:
        //支付成功根據支付方式:1=微信支付,2=支付寶,3=銀行卡,11=其他
        if ($userAccount->pay_type == 1) {
            //微信支付查看訂單
            $app = (new WeChatService)->connect(10);
            $res = $app->order->queryByOutTradeNumber($orderNumber);
            if ($res['return_code'] === 'SUCCESS') { // return_code 表示通信狀態,不代表支付狀態
                if ($res['result_code'] === 'SUCCESS') { //以下字段在return_code爲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);
                }
            }

        } elseif ($userAccount->pay_type == 2) {
            //支付寶查看訂單
            $res = Pay::alipay(config('alipay'))->find($orderNumber);
            $state = $res->trade_status; //訂單狀態
            $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:
    //需要再查一次訂單狀態
    $userAccount = (new UserAccountModel)->where('order_number', $orderNumber)->find();

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

    $this->success('獲取訂單信息成功!', $data);
}
  • 查詢訂單時候用到 is_deal 用於判斷異步接口是否調用處理,沒有則調用一次
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; //訂單狀態
    $outTradeNo = $data->out_trade_no; //自定義訂單號
    $tradeNo = $data->trade_no; //支付寶訂單號
    $totalAmount = $data->total_amount; //充值總金額
    $appId = $data->app_id; //收款方的APPID
    $payTime = $data->gmt_payment; //交易付款時間
    //獲取對應訂單的資金流水信息
    $res = (new UserAccountModel)->where('order_number', $outTradeNo)->find();

    // 請自行對 trade_status 進行判斷及其它邏輯進行判斷,在支付寶的業務通知中,只有交易通知狀態爲 TRADE_SUCCESS 或 TRADE_FINISHED 時,支付寶纔會認定爲買家付款成功。
    // 1、商戶需要驗證該通知數據中的out_trade_no是否爲商戶系統中創建的訂單號;
    // 2、判斷total_amount是否確實爲該訂單的實際金額(即商戶訂單創建時的金額);
    // 3、校驗通知中的seller_id(或者seller_email) 是否爲out_trade_no這筆單據的對應的操作方(有的時候,一個商戶可能有多個seller_id/seller_email);
    // 4、驗證app_id是否爲該商戶本身。
    // 5、其它業務邏輯情況。
    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);用於 支付成功調用的接口
  • 該接口主要根據(訂單號查詢到的)訂單業務類型 去調不同的方法,同時需要加入 併發鎖
2.2.5、支付成功的方法
/**
 * @ApiTitle    (支付成功的操作,需要鎖)
 * @ApiInternal
 * @param string $outTradeNo 商戶內部訂單號
 * @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 = (new UserAccountModel)->where('order_number', $outTradeNo)->find();
    if (!$res) return false;
    Db::startTrans();
    try {

        //更新資金錶狀態
        (new UserAccountModel)->where('order_number', $outTradeNo)->update([
            'trade_no' => $tradeNo, //微信訂單號
            'pay_time' => $payTime, //支付時間
            'pay_status' => 1, //支付狀態: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('訂單狀態處理異常');
        }

        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 服務類
<?php

namespace app\common\service;

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

class RedisService extends Api
{
    //Redis併發鎖
    const SU_REDIS_LOCK = 'redis::lock::'; //Redis併發鎖(後面跟對應業務的鎖名)

    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,
            ];
            //沒有配置密碼時,不傳入密碼項參數
            if (config('redis_password')) $config['password'] = config('redis_password');

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

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

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

        return $flag;
    }

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

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

}

大功告成,其中調試也會遇到問題,我們可以在日誌中進行查看,日誌在配置中可以進行修改。
如果有什麼問題可以留言,歡迎互相交流共進步。
另外還有詳細的 微信APP支付過程

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章