ThinkPHP框架开发的CRMEB小程序商城v4.0集成支付宝支付

avatar
开发工程师 @西安众邦网络科技有限公司

前言

大家都知道支付宝支付和微信支付宝都只能局限在自己的平台,微信内支付宝支付是根本就不能使用,即使是公众号支付也需要跳转到外部浏览器才可以唤起支付宝支付,并且QQ浏览器唤起支付宝支付还是问题很多,所以一般在微信生态内的应用一般都不考虑接入支付宝,但仍然有不少用户有这方面的需求,今天就给大家做个详细接入流程!

开通支付宝支付

  1. 开通过程省略,可查看帮助文档:help.crmeb.net/crmeb_zsff/…, 这是crmeb知识付费系统的支付宝支付,不过开通的流程都是一样的。

2. 下载支付宝支付SDK

3. 创建aliapay支付类

创建路径:crmeb/services/AlipayService.php, 把下载好的文件解压在vendor目录中目录结构为: /vendor/alipay/

4. AlipayService

采用单例设计模式,支付宝支付的SDK旧版本不能用composer加载这就很不优雅了,不能在类初始化的时候加载,而且第一次载入时非常慢。

<?php
/**
 * @author: liaofei<136327134@qq.com>
 * @day: 2020/8/19
 */

namespace crmeb\services;

use think\exception\ValidateException;
use think\facade\Route as Url;
use think\facade\Log;

/**
 * Class AlipayService
 * @package crmeb\services
 */
class AlipayService
{
    /**
     * @var static
     */
    protected static $instance;

    /**
     * @var string
     */
    protected $alipayAppId;

    /**
     * @var string
     */
    protected $alipayPublicKey;

    /**
     * 支付宝
     * @var string
     */
    protected $alipayPrivateKey;

    /**
     * 同步回调地址
     * @var string
     */
    protected $returnUrl;

    /**
     * 异步回调地址
     * @var string
     */
    protected $notifyUrl;

    /**
     * 请求网关
     * @var string
     */
    protected $gatewayUrl = 'https://openapi.alipay.com/gateway.do';

    /**
     * 是否开启日志
     * @var bool
     */
    protected $isLog = false;

    /**
     * AlipayService constructor.
     */
    protected function __construct()
    {
        $this->initialize();
        $this->options();
    }

    /**
     * @param $name
     * @param $arguments
     */
    public function __call($name, $arguments)
    {
        if (strstr($name, 'set') !== false) {
            $name = ucwords(substr($name, 3, -1));
            if (in_array($name, ['returnUrl', 'isLog', 'notifyUrl', 'alipayPrivateKey', 'alipayAppId', 'alipayPublicKey'])) {
                $this->{$name} = $arguments[0];
            }
        } else {
            throw new ValidateException('访问方法不存在');
        }
    }

    /**
     * 初始化加载阿里云pay
     */
    protected function initialize()
    {
        $dir = app()->getRuntimePath() . 'alipay';
        if (!file_exists($dir)) {
            mkdir($dir, 0775, true);
        }
        define('AOP_SDK_WORK_DIR', $dir);
        include app()->getRootPath() . DS . 'vendor' . DS . 'alipay' . DS . 'AopSdk.php';
    }

    /**
     * 获取参数配置
     */
    protected function options()
    {
        $this->alipayAppId = sys_config('alipay_app_id');
        $this->alipayPublicKey = sys_config('alipay_public_key');
        $this->alipayPrivateKey = sys_config('alipay_private_key');
        $this->returnUrl = Url::buildUrl('/api/alipay/synchro')->domain(true)->build();
        $this->notifyUrl = Url::buildUrl('/api/alipay/notify')->domain(true)->build();
    }

    /**
     * @return static
     */
    public static function instance()
    {
        if (is_null(self::$instance)) {
            self::$instance = new static();
        }
        return self::$instance;
    }

    /**
     * 支付宝支付同步回调
     */
    public static function aliPayReturn()
    {
        
    }

    /**
     * 支付支付同步回调
     */
    public static function handleNotify()
    {

    }

    /**
     * 下单支付手机网站支付版本
     * @param string $outTradeNo 下单号
     * @param string $totalAmount 订单金额 单位元
     * @param string $subject 订单标题
     * @param string $passbackParams 订单备注 会原样返回通常用于回调监听函数
     * @param string $productCode 销售产品码,商家和支付宝签约的产品码
     * @param bool $isView 是否直接输出
     * @return $response 支付宝返回的信息
     */
    public function aliPayWap(string $outTradeNo, string $totalAmount, string $subject, string $passbackParams, string $productCode = 'QUICK_MSECURITY_PAY', bool $isView = true)
    {

    }

    /**
     * 统一收单交易退款接口
     * @param string $outTradeNo 下单订单号
     * @param string $tradeNo 支付宝订单号
     * @param string $refundAmount 退款金额
     * @param string $refundReason 退款说明
     * @param string $passbackParams 备注
     * @return $response 支付宝返回的信息
     */
    public function aliPayRefund(string $outTradeNo, string $tradeNo, string $refundAmount, string $refundReason, string $passbackParams)
    {

    }
    
    /**
     * 设置业务参数
     * @param array $biz_content
     * @return string
     */
    protected function setBizContent(array $bizContent = [])
    {
        if (isset($bizContent['passback_params'])) $bizContent['passback_params'] = urlencode($bizContent['passback_params']);
        if (isset($bizContent['trade_no']) && empty($bizContent['trade_no'])) unset($bizContent['trade_no']);
        $bizContent = json_encode($bizContent);
        //打印业务参数
        $this->isLog && $this->writeLog($bizContent);
        return $bizContent;
    }

    /**
     * 写入日志
     * @param $content string | array | object
     * @return Log
     */
    protected function writeLog($content)
    {
        if (is_array($content)) $content = 'response: ' . var_export($content, true);
        if (is_object($content)) $content = 'response: ' . var_export($content, true);
        return Log::write(date('Y-m-d H:i:s', time()) . '   ' . $content);
    }


}

首先要把从数据库中获取到的参数放入支付配置里

创建aopclientRequestExecute()方法把 options()方法获取到的参数赋值给支付宝此方法会输出或者直接返回HTML的文本,前后端分离可直接用返回提交数据.

	/**
     * 初始化参数
     * @param $request
     * @param bool $isView
     * @return mixed|\SimpleXMLElement|string|\提交表单HTML文本
     * @throws \Exception
     */
    protected function aopclientRequestExecute(\AlipayTradeWapPayRequest $request, bool $isView = false)
    {
        $aop = new \AopClient();
        //网管地址
        $aop->gatewayUrl = $this->gatewayUrl;
        //appid
        $aop->appId = $this->alipayAppId;
        //私钥
        $aop->rsaPrivateKey = $this->alipayPrivateKey;
        //公钥
        $aop->alipayrsaPublicKey = $this->alipayPublicKey;
        //版本
        $aop->apiVersion = "1.0";
        //编码格式
        $aop->postCharset = 'UTF-8';
        //内容格式
        $aop->format = 'JSON';
        //加密方式
        $aop->signType = 'RSA2';
        // 开启页面信息输出
        $aop->debugInfo = false;
        if ($isView) {
            $result = $aop->pageExecute($request, "post");
            echo $result;
        } else {
            $result = $aop->Execute($request);
        }
        //打开后,将报文写入log文件
        $this->isLog && $this->writeLog($result);
        return $result;
    }

创建订单

上面已经创建好aliPayWap()方法,接下来我们来完成它,aliPayWap()方法的逻辑也非常的简单,只需要传入订单号,支付金额,订单标题,订单备注.订单备注一般会原样返回的,这样可以利用订单备注来让异步回调执行对应的方法 例如有用户下单支付和用户充值回调回调方法为一个,为一个回调方法处理的逻辑就比较混乱。

	/**
     * 下单支付手机网站支付版本
     * @param string $outTradeNo 下单号
     * @param string $totalAmount 订单金额 单位元
     * @param string $subject 订单标题
     * @param string $passbackParams 订单备注 会原样返回通常用于回调监听函数
     * @param string $productCode 销售产品码,商家和支付宝签约的产品码
     * @param bool $isView 是否直接输出
     * @return $response 支付宝返回的信息
     */
    public function aliPayWap(string $outTradeNo, string $totalAmount, string $subject, string $passbackParams, string $productCode = 'QUICK_MSECURITY_PAY', bool $isView = true)
    {
        $request = new \AlipayTradeWapPayRequest();
		//设置异步回调地址
        $request->setNotifyUrl($this->notifyUrl);
		//设置同步回调地址
        $request->setReturnUrl($this->returnUrl);
		//用内置方法格式化参数
        $content = $this->setBizContent([
            'out_trade_no' => $outTradeNo,
            'total_amount' => $totalAmount,
            'subject' => $subject,
            'passback_params' => $passbackParams,
            'product_code' => $productCode,
        ]);
		//设置下单参数
        $request->setBizContent($content);
		//执行请求进行下单,返回对应的支付参数
        return $this->aopclientRequestExecute($request, $isView);
    }

订单退款

aliPayRefund()方法负责退款处理,需要参数下单订单号,支付宝订单号,退款金额,退款说明,备注.支付宝订单号需要在异步支付回调中或者同步回调中获取更新在数据库中,方便退款处理。

/**
     * 统一收单交易退款接口
     * @param string $outTradeNo 下单订单号
     * @param string $tradeNo 支付宝订单号
     * @param string $refundAmount 退款金额
     * @param string $refundReason 退款说明
     * @param string $passbackParams 备注
     * @return $response 支付宝返回的信息
     */
    public function aliPayRefund(string $outTradeNo, string $tradeNo, string $refundAmount, string $refundReason, string $passbackParams)
    {
        $request = new \AlipayTradeRefundRequest();
        $content = $this->setBizContent([
            'out_trade_no' => $outTradeNo,
            'trade_no' => $tradeNo,
            'refund_amount' => $refundAmount,
            'passback_params' => $passbackParams,
            'refund_reason' => $refundReason,
            'product_code' => $passbackParams,
        ]);
        $request->setBizContent($content);
        return $this->aopclientRequestExecute($request);
    }

回调验签

主要给异步和同步回调验证签名和数据处理方便调用

	/**
     * 验签方法
     * @param array $post 验签支付宝返回的信息,使用支付宝公钥。
     * @return boolean
     */
    protected function aliPaycheck(array $post)
    {
        $aop = new \AopClient();
        $aop->alipayrsaPublicKey = $this->alipayPublicKey;
        return $aop->rsaCheckV1($post, $this->alipayPrivateKey, 'RSA2');
    }

异步回调

创建异步回调路由,修改文件:app/api/route/v1.php,在顶部中增加以下代码,别忘了创建对应的AlipayController文件

Route::any('alipay/notify', 'v1.alipay.AlipayController/notify');//支付宝支付回调

首先需要处理支付包异步返回来的数据,新增aliPayNotify()方法来解析处理异步回调返回的参数。

	/**
     * 支付宝异步回调
     * @param callable $notifyFn 闭包函数 参数1,回调返回的参数,回调结果
     * @return bool
     */
    protected function aliPayNotify(callable $notifyFn)
    {
        $post = app()->request->post();
        $result = $this->aliPaycheck($post);
        if ($result) {
            //商户订单号
            $post['out_trade_no'] = isset($post['out_trade_no']) ? $post['out_trade_no'] : '';
            //支付宝交易号
            $post['trade_no'] = isset($post['trade_no']) ? $post['trade_no'] : '';
            //交易状态
            $post['trade_status'] = isset($post['trade_status']) ? $post['trade_status'] : '';
            //备注
            $post['attach'] = isset($post['passback_params']) ? urldecode($post['passback_params']) : '';
            //异步回调成功执行
            try {
                if (is_callable($notifyFn)) $notifyFn((object)$post, $result);
            } catch (\Exception $e) {
                $this->isLog && $this->writeLog('支付宝支付成功,订单号为:' . $post['out_trade_no'] . '.回调报错:' . $e->getMessage());
            }
            echo 'success';
        } else {
            echo 'fail';
        }
        $this->isLog && $this->writeLog($result);
        return true;

    }

执行instance()方法实例本类调用aliPayNotify()方法

	/**
     * 支付宝支付同步回调
     */
    public static function aliPayReturn()
    {
        self::instance()->aliPayNotify(function ($data, $result) {
			//$data为支付宝回调返回通过解析后重组的数据
			//$result为验签结果
			//这里要写异步回调的逻辑
			//可根据$data->attach来判断需要执行的逻辑
			//可以调用TP6中的事件来执行对应的业务逻辑
			//event($data->attach,$data)
        });
    }

同步回调

同步回调使用在支付成功后跳转回去的页面一遍验证下返回的数据是否正常,记录下支付宝订单号,或者其他逻辑

	/**
     * 支付支付同步回调
     */
    public static function handleNotify()
    {
        //获取返回参数
        $get = app()->request->get();
        //验签成功与否
        $result = self::instance()->aliPaycheck($get);
        //记录日志
        self::instance()->isLog && self::instance()->writeLog(compact('result', 'get'));
        return compact('result', 'get');
    }

其他的业务逻辑可根据自身需求编写,以上就是支付宝支付的下单,退款,异步同步回调 记得进入后台增加对应的支付宝配置.