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

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

1.1、申請(qǐng)支付寶支付

我們需要注意配置的以下幾點(diǎn):

  • 需要配置 接口加簽方式支付寶開(kāi)放平臺(tái)開(kāi)發(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)賬、小程序支付)
    微信支付(公眾號(hào)支付、小程序支付、H5 支付、刷卡支付、掃碼支付、APP 支付、企業(yè)付款、普通紅包、分裂紅包)

2、代碼實(shí)現(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ū)模式,請(qǐng)配置下面兩個(gè)參數(shù),同時(shí)修改ali_public_key為以.crt結(jié)尾的支付寶公鑰證書(shū)路徑,如(./cert/alipayCertPublicKey_RSA2.crt)
    // 'app_cert_public_key' => './cert/appCertPublicKey.crt', //應(yīng)用公鑰證書(shū)路徑
    // 'alipay_root_cert' => './cert/alipayRootCert.crt', //支付寶根證書(shū)路徑
    'log' => [ // optional
        'file' => './logs/alipay.log',
        'level' => 'info', // 建議生產(chǎn)環(huán)境等級(jí)調(diào)整為 info,開(kāi)發(fā)環(huán)境為 debug
//            'type' => 'daily', // optional, 可選 daily.
        'max_file' => 30, // optional, 當(dāng) type 為 daily 時(shí)有效,默認(rèn) 30 天
    ],
    'sign_type' => "RSA2",
    'http' => [ // optional
        'timeout' => 5.0,
        'connect_timeout' => 5.0,
        // 更多配置項(xiàng)請(qǐng)參考 [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    (用戶開(kāi)通VIP創(chuàng)建不同的訂單)
 * @ApiMethod   (POST)
 */
public function createOrder()
{
    //根據(jù)傳入內(nèi)容或者查庫(kù)獲取相關(guān)的數(shù)據(jù)
    //$amount 需要內(nèi)部計(jì)算,不能傳入
    //$userId 從Token中獲取用戶ID
    //$orderNumber 需要生成隨機(jī)不重復(fù)的字符串,用于商戶內(nèi)部訂單號(hào)

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

    //生成資金流水記錄
    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開(kāi)通/升級(jí),2=推薦提成,3=退款
            'money' => $amount, //資金金額
            'desc' => $subject, //相關(guān)描述
            'pay_status' => 0, //支付狀態(tài):0=未到賬,1=已到賬
            'order_number' => $orderNumber, //訂單流水號(hào)
            'create_time' => date('Y-m-d H:i:s', time()), //創(chuàng)建時(shí)間
        ]);
        Db::commit();

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

    $this->success('創(chuàng)建訂單成功!', ['order_number' => $orderNumber,]);
}
  • $amount 表示支付金額,需要根據(jù)具體業(yè)務(wù)計(jì)算,不能取 input 傳入的值。
  • pay_status 用于判斷訂單是否支付完成,這一步主要通過(guò)訂單查詢/異步調(diào)用時(shí)候成功的情況下才會(huì)變?yōu)?
  • 另外訂單表中需要有一個(gè) is_deal字段,用于判斷 是否處理(0=否,1=是)支付成功的操作 ,因?yàn)?異步調(diào)用 有時(shí)候會(huì)出現(xiàn)問(wèn)題,我們也需要在 調(diào)用支付寶查詢訂單 的時(shí)候通過(guò)這個(gè)字段去判斷是否處理從而去 更新訂單表
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)部訂單號(hào)
        if (!$orderNumber) $this->error('訂單號(hào)不得為空!');

        //相關(guān)的業(yè)務(wù)類型判斷
        // ....
        // 計(jì)算相關(guān)的資金價(jià)格 $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 . '方式開(kāi)通' . $newVip['title'] . ',充值金額:' . $amount . '元';
        if ($vipType == 2) $subject = $payTypeText . '方式續(xù)費(fèi)' . $newVip['title'] . ',充值金額:' . $amount . '元';
        if ($vipType == 3) $subject = $payTypeText . '方式升級(jí)' . $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支付的時(shí)候需要填寫(xiě) 支付成功界面的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)部訂單流水號(hào)
    $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的時(shí)候有返回
                    if ($res['trade_state'] === 'SUCCESS') { //支付成功
                        $tradeNo = $res['transaction_id']; //微信支付訂單號(hào)
                        $totalFee = $res['total_fee']; //充值總金額
                        $timeEnd = $res['time_end']; //支付完成時(shí)間

                        //如果金額不匹配直接退出
                        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; //自定義訂單號(hào)
            $tradeNo = $res->trade_no; //支付寶訂單號(hào)
            $totalAmount = $res->total_amount; //充值總金額
            $appId = $res->app_id; //收款方的APPID
            $payTime = $res->gmt_payment; //交易付款時(shí)間

            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();

    //返回?cái)?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);
}
  • 查詢訂單時(shí)候用到 is_deal 用于判斷異步接口是否調(diào)用處理,沒(méi)有則調(diào)用一次
2.2.4、支付寶異步操作
/**
 * @ApiTitle    (支付寶異步接口)
 * @ApiMethod   (POST)
 * @ApiRoute    (/api/Payment/alipayNotify)
 * @ApiInternal
 */
public function alipayNotify()
{
    $alipay = Pay::alipay(config('alipay'));

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

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

    // 請(qǐng)自行對(duì) trade_status 進(jìn)行判斷及其它邏輯進(jìn)行判斷,在支付寶的業(yè)務(wù)通知中,只有交易通知狀態(tài)為 TRADE_SUCCESS 或 TRADE_FINISHED 時(shí),支付寶才會(huì)認(rèn)定為買家付款成功。
    // 1、商戶需要驗(yàn)證該通知數(shù)據(jù)中的out_trade_no是否為商戶系統(tǒng)中創(chuàng)建的訂單號(hào);
    // 2、判斷total_amount是否確實(shí)為該訂單的實(shí)際金額(即商戶訂單創(chuàng)建時(shí)的金額);
    // 3、校驗(yàn)通知中的seller_id(或者seller_email) 是否為out_trade_no這筆單據(jù)的對(duì)應(yīng)的操作方(有的時(shí)候,一個(gè)商戶可能有多個(gè)seller_id/seller_email);
    // 4、驗(yàn)證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 框架中請(qǐng)直接 `return $alipay->success()`
}
  • 其中 $this->paySuccess($outTradeNo, $tradeNo, $payTime);用于 支付成功調(diào)用的接口
  • 該接口主要根據(jù)(訂單號(hào)查詢到的)訂單業(yè)務(wù)類型 去調(diào)不同的方法,同時(shí)需要加入 并發(fā)鎖
2.2.5、支付成功的方法
/**
 * @ApiTitle    (支付成功的操作,需要鎖)
 * @ApiInternal
 * @param string $outTradeNo 商戶內(nèi)部訂單號(hào)
 * @param string $tradeNo 微信/支付寶訂單號(hào)
 * @param string $payTime 支付時(shí)間
 * @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, //微信訂單號(hào)
            'pay_time' => $payTime, //支付時(shí)間
            'pay_status' => 1, //支付狀態(tài):0=未到賬,1=已到賬
        ]);

        //查看訂單類型  1=VIP開(kāi)通/升級(jí),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);是充值會(huì)員成功的方法
2.2.6、Redis鎖方法
  • composer安裝 predis,命令行:composer require predis/predis,點(diǎn)擊查看鏈接。
  • 新建一個(gè) 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ā)鎖(后面跟對(duì)應(yīng)業(yè)務(wù)的鎖名)

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

    /**
     * 單例模式獲取redis連接實(shí)例
     * @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,
            ];
            //沒(méi)有配置密碼時(shí),不傳入密碼項(xiàng)參數(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 過(guò)期時(shí)間
     * @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); //只有第一個(gè)操作的返回是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)試也會(huì)遇到問(wèn)題,我們可以在日志中進(jìn)行查看,日志在配置中可以進(jìn)行修改。
如果有什么問(wèn)題可以留言,歡迎互相交流共進(jìn)步。
另外還有詳細(xì)的 TP5 實(shí)現(xiàn)APP/二維碼/小程序/H5等微信支付(詳細(xì)步驟)

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

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