使用uniapp开发字节跳动小程序的微信支付和支付宝支付(后端PHP,tp5)

1,931 阅读6分钟

使用uniapp开发字节跳动小程序的微信支付和支付宝支付(后端PHP,tp5)
准备工作
微信支付配置
支付宝支付配置
字节跳动配置
1,微信支付配置:
开通微信支付的h5支付,关联APPID,保存key,商户ID(mch_id ),配置回调域名,记得把字节跳动的回调域名也写进h5支付的域名配置当中:snssdk.com

2,支付宝支付配置:
开通支付宝的app支付,在网页&移动应用中创建一个应用接入app支付,并且签约并且保证生效。我是通过支付宝证书进行支付的,所以支付宝这边需要自己去配置证书并且下载下来,后面要用到。

3,字节跳动配置
开通字节跳动的支付,获得字节跳动的商户号,appid,支付secret

业务代码:
1,后端
2,前端

1,后端
上传参数,通过支付宝和微信配置订单信息传给前端uniapp,,微信和支付宝的回调正确的话,自己配置正确,按自己的情况添加订单到数据库就可以了,这里就不写了。aliUrlZhengshu方法是支付宝支付,unifiedOrder方法是微信支付。
抖音的配置我写在了config.php里面

 'douyin'=>[
        'appId'=>'*****',
        'appSecret'=>'*****',
        'url'=>'*****',//抖音授权链接
        'payAppId' =>'*****',
        'tt_pay_app_secret'=>'*****',//支付secret
        'merchant_id'=>'*****',//支付secret
    ],

public function _initialize()
    {
        $this->appid = '*****';//微信的appid
        $this->mch_id = '*****';//微信的商户id
        $this->key = '*****';//key
        $this->notify_url = '*****';//自己配置的微信支付回调地址
    }
   public function payReady(){
        $config = config('douyin');
        $openid = $this->request->post('openid');
        $truePrice = $this->request->post('price');
        $video_id = $this->request->post('video_id');
        $user_id = $this->request->post('user_id');
        $subject = $this->request->post('subject');
        $body = $this->request->post('body');
        $truePrice = (int)$truePrice;
        $tt_result = $this->ttOrder($user_id,$video_id,$truePrice,$subject,$body,$openid);
        $out_order_no = $tt_result['out_order_no'];//自定义的订单号

        $arr = [
            'merchant_id' =>$config['merchant_id'],//字节跳动商户号 前提条件->字节跳动->4 完成填写后
            'app_id' => $config['payAppId'],//字节跳动APPID  前提条件->字节跳动->4 完成填写后
            'sign_type' => 'MD5',//定死的别动!!!
            'timestamp' => strval($tt_result['time']),//需要为字符串类型的时间戳
            'version' => '2.0',//定死的别动!!!
            'trade_type' => 'H5',//定死的别动!!!
            'product_code' => 'pay',//定死的别动!!!
            'payment_type' => 'direct',//定死的别动!!!
            'out_order_no' => strval($out_order_no),//自定义的订单号
            'uid' => $openid,// 用户的openid 登录后可以获取到
            'total_amount' => $tt_result['fee'],//金额 这里单位:分
            'currency' => 'CNY',//定死的别动!
            'trade_no' => $tt_result['out_order_no'],//刚刚获取的字节跳动订单 忘了往上找找
            'subject' => $tt_result['subject'],//之前定好的标题
            'body' => $tt_result['body'],//之前定好的内容
            'trade_time' => strval($tt_result['time']),//一定要和 上面的 timestamp 字段相同
            'valid_time' => '3000',//测试留的时间长
            'notify_url' => 'https://tp-pay.snssdk.com/paycallback',//定死的别动!!!
        ];

            $aliurl = $this->aliUrlZhengshu($tt_result);//获取 alipay_url
            $arr['alipay_url'] = $aliurl;
            $wx_res = $this->unifiedOrder($tt_result['out_order_no'],$body,$subject);
            if(!empty($wx_res)){
                $pay_url  = $wx_res;
            }
            $arr['wx_type'] = 'MWEB';
            $arr['wx_url'] = $pay_url;


        $stringToBeSigned = $this->getSignContent($arr);//这里待签名处理.方法下面

        $sign = md5($stringToBeSigned . $config['tt_pay_app_secret']);//这生成签名咯, 不要乱, 签名好多的

        //这两个字段的写入原因: 在待签名字符串 getSignContent 方法中不能有 sign和risk_info 所以在生成签名($sign)之后写入到里面
        $arr['sign'] = $sign;
        $arr['risk_info'] = json_encode(['ip' => request()->ip()]);
        //这两个字段的写入原因: 在待签名字符串  getSignContent 方法中不能有 sign和risk_info 所以在生成签名($sign)之后写入到里面
        $res = htmlspecialchars_decode(json_encode($arr));//这里html的编译解析, 防止html编译

        //            $this->success('返回orderinfo', $res);
        $this->success('返回orderinfo', ['list' => $res]);
    }
     /**
     * 获取抖音订单号
     */
    public function ttOrder($user_id,$video_id,$order_price,$subject,$body,$openid){
        $config = config('douyin');
        $time = time();
//        if(!$user_id){
//            $this->error('请先授权登录');
//        }
        $price = $order_price*100;
        //$price = 1;
        //------------------自定义订单信息--------------------------
        $data = [
            'out_order_no' =>  $time, //随便搞个订单号
            'openid' => $openid,       //抖音用户openid
            'fee' => $price,           //金额 单位:分!分!分!
            'cid' => 1,
            'time' => $time,
            'body' => $body,           //支付的内容(支付宝)
            'subject' => $subject,        //支付的标题(支付宝)
            //body 和 subject 刚开始先用数字(中文会有其他问题)
        ];
        //------------------自定义订单信息--------------------------

        //TP5框架 fastadmin
        //------------------组合请求sign信息--------------------------
        //↓↓↓获取用户真实IP
        $risk_info = request()->ip();
        //↓↓↓头条支付分配给业务方的ID(不是头条小程序的appid)
        $payload['app_id'] = $config['payAppId'];
        //↓↓↓头条支付分配给业务方的支付秘钥
        $app_secret = $config['tt_pay_app_secret'];
        //↓↓↓请求使用的编码格式
        $payload['charset'] = "utf-8";
        //↓↓↓接口名称
        $payload['method'] = "tp.trade.create";
        //↓↓↓发送请求的时间
        $payload['timestamp'] = $time;
        // 请求参数的集合   json
        $biz_content = [
            //商户订单号
            "out_order_no" => $data['out_order_no'],
            //唯一标识用户open_id
            "uid" => $data['openid'],
            //金额,分为单位,应传整型
            "total_amount" => $data['fee'],
            //商户订单名称
            "subject" => $data['subject'],
            //商户订单详情
            "body" => $data['body'],
            //头条支付分配给业务方的商户号
            "merchant_id" => $config['merchant_id'],
            //货币种类
            "currency" => "CNY",
            //下单时间戳
            "trade_time" => $time,
            //订单有效时间(此处测试 时间留的长)     单位:秒
            "valid_time" => "3000",
            //服务器异步通知地址尽量https 没试过http
            "notify_url" => 'https://admin.shitutu.com/public/api/Dou_alipay/notify',
            //用户的真实ip 一定要json序列化
            "risk_info" => json_encode(['ip' => $risk_info]),
        ];
        $payload['biz_content'] = json_encode($biz_content);
        //字节跳动采用的是MD5加密
        $payload['sign_type'] = "MD5";
        $payload['format'] = "json";
        $payload['version'] = "1.0";
        //这里写了一个签名的方法, 千万别乱, 此处签名用来请求的, 与其他签名没有任何关联;(共3个签名)
        $stringToBeSigned = $this->getSignContent($payload, $payload['charset']);
        $payload["sign"] = md5($stringToBeSigned . $app_secret);

        $url = "https://tp-pay.snssdk.com/gateway"; // 请求地址正式环境
        // $url = "$url?app_id=$payload[app_id]&secret=$appSecret&code=$code&anonymous_code=$anonymous_code";
        $result = $this->file_get_contents_post($url, $payload);
        /*返回示例:
        {"response":{"code":"10000","msg":"Success","trade_no":"SP2020081509594510822988870791"},"sign":"E7RRJSJCVAhA4DMyMPr/Q1IOc1RKpyQNikl9l8b3ObW6dAYzep7rK6wY5YVjSubhmINsI0iWb/cu+YCqp1D+amifkXh4nX2JG3D0xgi2eWJrTv3Ou27zuEPbEb5y10SBG1f4QCYoa7r2upmOL5xbjY6kG5iDPjiS4JIthsojR5Q="}*/
        $result = json_decode($result, true);
        //------------------组合请求sign信息--------------------------
        $res['out_order_no'] = $result['response']['trade_no'];
        $res['fee'] = $price;
        $res['cid'] = 1;
        $res['time'] = $time;
        $res['body'] = $body;
        $res['subject'] = $subject;
        $order_res = $this->order_create($user_id,$video_id,$order_price,$res['out_order_no']);//这里是我自己的创建订单到本地数据库,这个方法就不展示了。
        if ($result['response']['code'] != 10000) {
            $this->error('错误', $res['response']['code']);
        } elseif ($res) {
            return $res;
        }
    }
     /**
     * 签名处理
     * @param $params
     * @param $charset
     * @return string
     */
    public function getSignContent($params, $charset = 'utf-8')
    {
        ksort($params);
        $stringToBeSigned = "";
        $i = 0;
        foreach ($params as $k => $v) {
            if (false === $this->checkEmpty($v) && "@" != substr($v, 0, 1)) {
                // 转换成目标字符集
                $v = $this->characet($v, $charset);
                if ($i == 0) {
                    $stringToBeSigned .= "$k" . "=" . "$v";
                } else {
                    $stringToBeSigned .= "&" . "$k" . "=" . "$v";
                }
                $i++;
            }
        }
        unset ($k, $v);
        return $stringToBeSigned;
    }
    public function file_get_contents_post($url, $post) {

        $options = array(

            'http' => array(

                'method' => 'POST',

                // 'content' => 'name=caiknife&email=caiknife@gmail.com',
                'header' => "Content-type: application/x-www-form-urlencoded ",
                'content' => http_build_query($post),

            ),

        );

        $result = file_get_contents($url, false, stream_context_create($options));

        return $result;

    }
	
	/**
     * @param $data
     * @return mixed
     * 阿里url证书
     */
    public function aliUrlZhengshu($data)
    {
        //需要在AopCertClient.php文件中加入
        //namespace app\api\controller;
        //use think\Exception;
        //引入文件 用来实例化
        $config = config('douyin');
        //require_once EXTEND_PATH . '/alipay/AopSdk.php';
        require_once VENDOR_PATH . 'aop/AopCertClient.php';
       // require_once EXTEND_PATH  . 'aop/AopCertClient.php';
        $c = new AopCertClient;

        $appCertPath = VENDOR_PATH  . 'aop/crt/appCertPublicKey_2021002114608375.crt';//应用证书路径(要确保证书文件可读)
        $alipayCertPath = VENDOR_PATH  . 'aop/crt/alipayCertPublicKey_RSA2.crt';//支付宝公钥证书路径(要确保证书文件可读)
        $rootCertPath = VENDOR_PATH  . 'aop/crt/alipayRootCert.crt';//支付宝根证书路径(要确保证书文件可读)
        $c->gatewayUrl = "https://openapi.alipay.com/gateway.do";
        $c->appId = self::APPID;
        $c->rsaPrivateKey = self::RSA_PRIVATE_KEY;
        $c->format = "json";
        $c->charset = "UTF-8";
        $c->signType = "RSA2";
        //调用getPublicKey从支付宝公钥证书中提取公钥
        $c->alipayrsaPublicKey = $c->getPublicKey($alipayCertPath);
        //是否校验自动下载的支付宝公钥证书,如果开启校验要保证支付宝根证书在有效期内
        $c->isCheckAlipayPublicCert = true;
        //调用getCertSN获取证书序列号
        $c->appCertSN = $c->getCertSN($appCertPath);
        //调用getRootCertSN获取支付宝根证书序列号
        $c->alipayRootCertSN = $c->getRootCertSN($rootCertPath);
        //实例化具体API对应的request类,类名称和接口名称对应,当前调用接口名称:alipay.open.public.template.message.industry.modify


        //文件中加入 namespace app\api\controller; 即可
        require_once VENDOR_PATH . 'aop/request/AlipayTradeAppPayRequest.php';
        $request = new AlipayTradeAppPayRequest();


        //此次只是参数展示,未进行字符串转义,实际情况下请转义
        $request->setBizContent($this->getcontent($data));
        $response = $c->sdkExecute($request);
        return $response;
    }
 /**
     * @param $data
     * @return false|string
     * 业务数据
     */
    public function getcontent($data)
    {
        $biz_content = array(
            'out_trade_no' => $data['out_order_no'],    //之前咱们自定义的订单号 out_trade_no
            'product_code' => 'QUICK_MSECURITY_PAY',    //定死了 别动
            'total_amount' => $data['fee'] / 100,       //单位换算
            'subject' => $data['subject'],              //之前定好的 标题
            'method' => 'alipay.trade.app.pay',         //定死了 别动
            'notify_url' => '*****',//回调接口需要配置到支付宝
            'body' => $data['body'],                    //之前定好的 内容
            'timeout_express' => '1m',                  //支付超时时间 文档去支付宝搜索咯 1m-15d
        );
        return json_encode($biz_content);
    }
    /**
     * 下单方法
     * param   $params 下单参数
     */
    public function unifiedOrder($order_no,$body,$subject){
        $config = config('douyin');
        $params['body'] = $subject; //商品描述
        $params['out_trade_no'] = $order_no; //订单号
        $params['total_fee'] = 1; //金额是以分为单位,除测试外,需乘以100
        $params['trade_type'] = 'MWEB';    //交易类型,h5支付,默认如此
        $params['scene_info'] = $body;   //场景信息,h5固定
        $params['spbill_create_ip'] = $this->getIp();   //终端IP
        $params['appid'] = $this->appid;
        $params['mch_id'] = $this->mch_id;
        $params['nonce_str'] = $this->genRandomString();    //随机字符串
        $params['notify_url'] = $this->notify_url;  //通知地址
        //获取签名数据
        $params['sign'] = $this->MakeSign( $params );   //签名
        $xml = $this->data_to_xml($params);

        $uri = 'https://api.mch.weixin.qq.com/pay/unifiedorder';    //请求地址
        $response = $this->postXmlCurl($uri,$xml);   //自定义封装的xml请求格式,文章最下面为参考postxml
        if( !$response ){
            return false;
        }
        $result = $this->xml_to_data( $response );
        if( !empty($result['result_code']) && !empty($result['err_code']) ){
            $result['err_msg'] = $this->error_code( $result['err_code'] );
        }

        if($result['result_code'] == 'SUCCESS' && $result['return_msg'] == 'OK'){
            //发起微信支付url
            $pay_url = $result['mweb_url'];//.'&redirect_url='.urlencode($this->notify_url)
        }else{
            $pay_url = '';
        }
        return $pay_url;


        //return $result;
    }
     /**
     * 生成签名
     *  @return 签名
     */
    public function MakeSign( $params ){
        //签名步骤一:按字典序排序数组参数
        ksort($params);
        $string = $this->ToUrlParams($params);
        //签名步骤二:在string后加入KEY
        $string = $string . "&key=".$this->key;
        //签名步骤三:MD5加密
        $string = md5($string);
        //签名步骤四:所有字符转为大写
        $result = strtoupper($string);
        return $result;
    }
    /**
     * 输出xml字符
     * param   $params     参数名称
     * return   string      返回组装的xml
     **/
    public function data_to_xml( $params ){
        if(!is_array($params)|| count($params) <= 0)
        {
            return false;
        }
        $xml = "<xml>";
        foreach ($params as $key=>$val)
        {
            if (is_numeric($val)){
                $xml.="<".$key.">".$val."</".$key.">";
            }else{
                $xml.="<".$key."><![CDATA[".$val."]]></".$key.">";
            }
        }
        $xml.="</xml>";
        return $xml;
    }
     /**
     * @param $url
     * @param $xml
     * @param int $second
     * @return bool|string
     * xml请求
     */
    public function postXmlCurl($url,$xml,$second = 30){
        $ch = curl_init();
        //设置超时
        curl_setopt($ch, CURLOPT_TIMEOUT, $second);
        curl_setopt($ch,CURLOPT_URL, $url);
        curl_setopt($ch,CURLOPT_SSL_VERIFYPEER,FALSE);
        curl_setopt($ch,CURLOPT_SSL_VERIFYHOST,FALSE);
        //设置 header
        curl_setopt($ch, CURLOPT_HEADER, FALSE);
        //要求结果为字符串且输出到屏幕上
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
        //post 提交方式
        curl_setopt($ch, CURLOPT_POST, TRUE);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
        //运行 curl
        $data = curl_exec($ch);
        //返回结果
        if($data){
            curl_close($ch);
            return $data;
        }else{
            $error = curl_errno($ch);
            curl_close($ch);
            echo "curl 出错,错误码:$error"."<br>";
        }
    }
    /**
     * 将xml转为array
     * param string $xml
     * return array
     */
    public function xml_to_data($xml){
        if(!$xml){
            return false;
        }
        //将XML转为array
        //禁止引用外部xml实体
        libxml_disable_entity_loader(true);
        $data = json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);
        return $data;
    }
    /**
     * 校验$value是否非空
     * @param $value
     * @return  boolean;
     *  if not set ,return true;
     *  if is null , return true;
     **/
    public function checkEmpty($value)
    {
        if (!isset($value))
            return true;
        if ($value === null)
            return true;
        if (trim($value) === "")
            return true;
        return false;
    }

    /**
     * 转换字符集编码
     * @param $data
     * @param $targetCharset
     * @return string
     */
    public function characet($data, $targetCharset)
    {
        if (!empty($data)) {
            $fileType = "UTF-8";
            if (strcasecmp($fileType, $targetCharset) != 0) {
                $data = mb_convert_encoding($data, $targetCharset, $fileType);
            }
        }
        return $data;
    }

2,前端,uniapp
我是通过字节跳动的收银台吊起的支付的,吊起收银台后可以选择支付宝或者微信支付,然后跳转到微信或者支付宝,所以上面要把微信和支付宝的支付url都传给了前端。

gopay(){
					wxpay({
						'openid':'****',
						'price':'****',
						'video_id':'****',
						'user_id':'****',
						'subject':'****',
						'body':this.detailList.title,
					}).then(e => {
						let order = JSON.parse(e.data.data.list);
						let order_no = order.trade_no
						if(e.data.code == 1){
						        uni.requestPayment({
									service: 1, // 不拉起字节跳动小程序收银台
									 _debug: 1,
									payChannel: {
						            default_pay_channel: 'alipay' // wx || alipay
									},
									orderInfo: order, // 订单信息
									getOrderStatus(res) {
						            let { out_order_no } = res;
									    return new Promise(function (resolve, reject) {
						                })
									},
									success: (res) => {
										console.log(res)
										if(res.code == 0){
											uni.showToast({
											    title: '支付成功',
											    duration: 2000
											});
										}
										if(res.code == 1){
											uni.showToast({
											    title: '支付超时',
											    duration: 2000
											});
										}
										if(res.code == 2){
											uni.showToast({
											    title: '支付失败',
											    duration: 2000
											});
										}
										if(res.code == 2){
											uni.showToast({
											    title: '支付失败',
											    duration: 2000
											});
										}
										if(res.code == 4){
											orderQuery({
												'order_no':order_no,
											}).then(e => {
												if(e.data.code == 1){
													uni.showToast({
													    title: '支付成功',
													    duration: 2000
													});
												}else{
													uni.showToast({
													    title: '支付失败',
													    duration: 2000
													});
													
												}
												
											});
										}
										if(res.code == 9){
											uni.showToast({
											    title: '订单状态未知',
											    duration: 2000
											});
										}
									},
									fail: (res) => {
						            console.log("失败1");
						            console.log(res);  // 错误代码:CD0015 CD0025
						        },
								complete: (res) => {
								    console.log("结束")
								}
								})
								// _this.loadModal = false;
							}
					 })
				},

这里有一个问题我不知道是不是字节官方的原因,我通过收银台支付,使用支付宝支付的时候,支付成功,返回小程序是有字节的支付回调页面并且提示成功的。但是微信支付没有显示这个支付回调的页面提示,并且code一直是4,所以我在code等于4的时候去后端验证了一下支付状态,手动给用户提示了支付状态,如果有朋友知道原因可以麻烦告知我一下,有问题也欢迎留言。