美文网首页PHP开发程序员PHP经验分享
TP5 实现支付宝APP支付(详细步骤)

TP5 实现支付宝APP支付(详细步骤)

作者: 华仔233 | 来源:发表于2020-07-09 16:39 被阅读0次

    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支付过程

    相关文章

      网友评论

        本文标题:TP5 实现支付宝APP支付(详细步骤)

        本文链接:https://www.haomeiwen.com/subject/quoaqktx.html