勤勤学长 发表于 2020-1-30 19:13

php开发第四方支付系统过程记录(2):完成了QQ支付及完善了整个支付流程,代码gon...

本帖最后由 勤勤学长 于 2020-1-31 15:17 编辑

前文地址:
php开发第四方支付系统过程记录(1):起因及为什么参数会有sign签名

有前辈提到sign和key的理解不到位.。虽然我认为1提到的方法是也是有效的,但是经受不住考验。
我昨天查了一下关于对称加密和非对称加密的资料。
非对称加密对我这种小白来说还是太过复杂,就想着如何用小白的方式解决算法泄露的问题。

1文提到的key实际还是等同于密码,假设支付系统用户获知他pid和key之后,他可以通过不断提交错误的pid和key值,会一直进行查库操作,那直接抛弃sign,从key入手。

那么,如何在客户端接收的时候,查库之前判断key是否是正确格式的,是否和用户ID匹配。正确且匹配之后再查库呢?

要求:生成key的时候,就要有规律且是对称加密,且生成的key被修改就无法解密。
我建立了数据库
pid=用户ID
key=密钥
money=账户余额

为了保证可以重置key,那么,key是固定值+随机值组成。
我这里用的是 pid=1001&rand=20200130180433 进行加密,rand后边是当前时间,生成/重置key的时间。
用参数的格式是为了方便服务端解密之后转成数组,
解密key失败拦截;
解密key里边的pid和提交支付订单的pid不一致就拦截。

本次开发使用的是thinkphp5.0框架
先把加解密方式写出来

//加密方式
    public function encode($data = '')
    {
      return openssl_encrypt($data, 'DES-ECB', '318692996', 0);
    }

    //解密方式
    public function decode($data = '')
    {
      return openssl_decrypt($data, 'DES-ECB', '318692996', 0);
    }


生成key

$key = $this->encode('pid=1001&rand='.date("YmdHis"));

商户信息到此就完成。
新增订单数据表
CREATE TABLE `xz_order`(
`out_trade_no` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '商户订单号',
`trade_no` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '支付宝交易号',
`money` decimal(7, 2) NULL DEFAULT NULL COMMENT '订单金额',
`total_amount` decimal(7, 2) NULL DEFAULT NULL COMMENT '订单金额',
`trade_status` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '交易状态',
`gmt_create` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '交易创建时间',
`gmt_payment` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '交易付款时间      ',
`paytype` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '支付方式',
`subject` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '订单描述',
`pid` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户id',
`notify_url` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '异步通知',
`return_url` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '同步通知'
) ENGINE = MyISAM CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
为了方便后边使用post,写个curl函数
public function http_post($url,$post_data=null)
{
    $con=curl_init((string)$url);
    curl_setopt($con,CURLOPT_HEADER,False);
    curl_setopt($con,CURLOPT_SSL_VERIFYPEER,False);
    curl_setopt($con,CURLOPT_RETURNTRANSFER,true);
    if ($post_data!=null) {
      curl_setopt($con, CURLOPT_POSTFIELDS, $post_data);
    }
    curl_setopt($con,CURLOPT_TIMEOUT,2);
    $result = curl_exec($con);
    return $result;
}
演示提交订单信息
public function test()
{
   
    $url = 'http://pay.11ak.cn/index/index/submit';
    $out_trade_no =date("YmdHis").mt_rand(1000,9999);

    $key =urlencode( 'Cm50Zej3dtS5FHchEDhdyqxE4pp1qw0ck+hYQuc0HRg=');

    $str = 'pid=1001&key='.$key.'&out_trade_no='.$out_trade_no."¬ify_url=http://pay.11ak.cn/index/index/testpost&return_url=http://pay.11ak.cn/index/index/testget&subject=订单标题&money=0.01&type=qqpay";

    echo $this->http_post($url, $str);
}

生成订单
public function submit()
{
    $data = input('post.');
   
    if (empty($data['pid']) || empty($data['key'])|| empty($data['type']) || empty($data['out_trade_no']) || empty($data['notify_url']) || empty($data['return_url']) || empty($data['subject']) || empty($data['money'])) {
      return json(['code' => 101, 'msg' => '参数不完整']);
    }


    if(strlen($data['out_trade_no']) < 16 ||strlen($data['out_trade_no']) >64){
      return json(['code' => 102, 'msg' => 'out_trade_no长度不能少于16或大于64位']);
    }

    if ($data['type'] != 'alipay' && $data['type'] != 'qqpay' && $data['type'] != 'wxpay') {
      return json(['code' => 106, 'msg' => '支付方式不存在']);
    }
   
    $key = $this->decode($data['key']);

    if (!$key) {
      //解密失败
      return json(['code' => 108, 'msg' => 'key格式不正确']);
    }else{
      parse_str($key, $key_data );//把url转为数组格式
      if ($key_data['pid'] != $data['pid']) {
            return json(['code' => 109, 'msg' => 'pid与key不匹配']);
      }
    }

    $userRes = db('user')->where(['pid' => $data['pid'], 'key' => $data['key']])->find();
    if (!$userRes) {
      return json(['code' => 103, 'msg' => 'pid或key错误']);
    }

    $orderRes = db('order')->where('out_trade_no',$data['out_trade_no'])->find();
    if ($orderRes) {
      return json(['code' => 104, 'msg' => 'out_trade_no已存在']);
    }


    $sql = array();
    $sql['out_trade_no'] = $data['out_trade_no'];//本地单号
    $sql['money'] = $data['money'];//订单金额
    $sql['subject'] = $data['subject'];//订单标题
    $sql['pid'] = $data['pid'];//用户ID
    $sql['paytype'] = $data['type'];//付款方式
    $sql['gmt_create'] =date('Y-m-d H:i:s',time());//时间;
    $sql['notify_url'] = $data['notify_url'];
    $sql['return_url'] = $data['return_url'];

    $addRes = db('order')->insert($sql);
    if ($addRes) {
      $url = 'http://pay.11ak.cn/index/index/pay?order='.urlencode($this->encode(http_build_query($data)));
      return "<script language='javascript' type='text/javascript'> window.location.href='".$url."';</script>";

      //header('location:'.$url);
    }else{
      return json(['code' => 105, 'msg' => '订单数据添加失败']);
    }
}
订单校验且生成成功,生成带参数的url,跳转到订单支付页面。

public function pay()
{
    $str = input('get.order');
    if (empty($str)) {
      return $this->err('order为空,请返回来源地重新发起请求!');
    }
    $str = $this->decode($str);
    parse_str($str, $data);   //第一个参数为字符串,第二个参数为结果

    //halt($data);
    if (empty($data['pid']) || empty($data['key']) || empty($data['type'])|| empty($data['out_trade_no']) || empty($data['notify_url']) || empty($data['return_url']) || empty($data['subject']) || empty($data['money'])) {
      return $this->err('参数不完整,请返回来源地重新发起请求!');
    }

    $orderRes = db('order')->where('out_trade_no',$data['out_trade_no'])->find();
    if ($orderRes) {
      if ($orderRes['trade_status'] == 1) {
            //订单已完成支付
            $post_data = array();
            $post_data['trade_no'] = $orderRes['trade_no'];//交易订单号
            $post_data['out_trade_no'] = $orderRes['trade_no'];//本地订单号
            $post_data['total_amount'] = $orderRes['out_trade_no'];//实际付款金额
            $post_data['gmt_payment'] = $orderRes['gmt_payment'];//付款时间
            $post_data['trade_status'] = 'TRADE_SUCCESS';//交易状态
            $return_url =urldecode($orderRes['return_url'].'?'.http_build_query($post_data));
            return "<script language='javascript' type='text/javascript'> window.location.href='".$return_url."';</script>";

      }
    }else{
      return $this->err('订单不存在!');
    }

    if ($data['type']=='alipay') {

    }elseif ($data['type']=='qqpay') {
      $site['title'] = 'QQ钱包';
      $site['type'] = '手机QQ';

//下面是QQ钱包的支付代码
      $params = array();
      $params["out_trade_no"] = $data['out_trade_no'];//商户订单号
      $params["mch_id"] = "318692996";//我的id
      $params["body"] = $data['subject'];//商品描述
      $params["nonce_str"] = \qqpay\Util::createNoncestr();//随机字符串
      $params["fee_type"] = "CNY";//货币类型定义
      $params["notify_url"] = "http://pay.11ak.cn/index/index/notify_url";//异步通知地址
      $params["spbill_create_ip"] = $_SERVER["REMOTE_ADDR"];//访问者IP
      $params["total_fee"] = $data['money']*100;//订单金额,单位是分,所以要×100
      $params["trade_type"] = "NATIVE";//Native 场景类型



      //api调用
      $qpayApi = new \qqpay\Api('https://qpay.qq.com/cgi-bin/pay/qpay_unified_order.cgi', null, 10);
      $ret = $qpayApi->reqQpay($params);

      $data3 = \qqpay\Util::xmlToArray($ret);

      $cards = array_merge($data3, $orderRes);


      //手机QQ直接支付
      if (strstr($_SERVER['HTTP_USER_AGENT'],'QQ/')) {
            return "<script language='javascript' type='text/javascript'> window.location.href='".$cards['code_url']."';</script>";
      }


      $this->assign('data',$cards);
      $this->assign('site',$site);

      return $this->fetch();

    }elseif ($data['type']=='wxpay') {

    }else{
      return $this->err('支付方式'.$data['type'].'不存在');
    }


}

前端页面,监控订单是否完成
<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <meta http-equiv="Content-Language" content="zh-cn">
    <meta name="renderer" content="webkit">
    <title>安全支付 - 勤勤学长支付站点</title>
    <link href="/static/assets/css/mqq_pay.css?v=1" rel="stylesheet" media="screen">
    <linkrel="stylesheet">
</head>
<body>
<div class="body">
    <h1 class="mod-title">
      <span class="ico-wechat"></span><span class="text">{$site.type}支付</span>
    </h1>
    <div class="container">
      <div class="row">
            <div class="col-md-8 col-sm-6 col-md-offset-2 col-sm-offset-3">
                <div class="mod-ct">
                  <div class="order">
                  </div>
                  <div class="amount">{$data.money}元</div>
                  <div class="qr-image" id="qrcode" title="{$data.code_url}">
                  </div>

                  <div class="detail" id="orderDetail">
                        <div class="detail-ct" style="display: none;">
                            <p>购买物品:{$data.subject}</p>
                            <p>商户订单号:{$data.out_trade_no}</p>
                            <p>创建时间:{$data.gmt_create}</p>
                        </div>
                        <a href="javascript:void(0)" class="arrow"><i class="ico-arrow"></i></a>
                  </div>
                  <div class="tip"><!--
                        <span class="dec dec-left"></span>
                        <span class="dec dec-right"></span>-->
                        <div class="ico-scan"></div>
                        <div class="tip-text">
                            <p>请使用{$site.title}扫一扫</p>
                            <p>扫描二维码完成支付</p>
                        </div>
                  </div>
                  <div class="tip-text">
                  </div>
                </div>

            </div>
      </div>
    </div>
    <div class="foot">
      <div class="inner">
            <p><atarget="_blank">南宁市勤哥网络科技工作室</a> </p>
      </div>
    </div>
</div>
<script src="/static/assets/js/qrcode.min.js"></script>
<script src="/static/assets/js/qcloud_util.js"></script>
<script src="/static/assets/js/layer.js"></script>
<script>
    var isSafari = navigator.userAgent.indexOf("Safari") > -1;
    var code_url = '{$data.code_url}';
    var t;
    var qrcode = new QRCode("qrcode", {
      text: code_url,
      width: 230,
      height: 230,
      colorDark: "#000000",
      colorLight: "#ffffff",
      correctLevel: QRCode.CorrectLevel.H
    });

    // 订单详情
    $('#orderDetail .arrow').click(function (event) {
      if ($('#orderDetail').hasClass('detail-open')) {
            $('#orderDetail .detail-ct').slideUp(500, function () {
                $('#orderDetail').removeClass('detail-open');
            });
      } else {
            $('#orderDetail .detail-ct').slideDown(500, function () {
                $('#orderDetail').addClass('detail-open');
            });
      }
    });
    // 检查是否支付完成
    function loadmsg() {
      $.ajax({
            type: "GET",
            dataType: "json",
            url: "{:url('getshop')}",
            timeout: 10000, //ajax请求超时时间10s
            data: {order: "{php}echo $_GET['order'];{/php}"}, //post数据
            success: function (data, textStatus) {
                //从服务器得到数据,显示数据并继续查询

                if (data.code == 1010) {
                  clearTimeout(t);
                  if (confirm("您已支付完成,需要跳转到用户中心吗?")) {
                        //layer.msg('支付成功,正在跳转中...', {icon: 1, shade: 0.01, time: 15000});
                        window.location.href = data.return_url;

                  } else {
                        // 用户取消
                  }

                } else if (data.code == 1014) {
                  t = setTimeout("loadmsg()", 4000);
                } else {
                  layer.alert(data.msg, {icon: 0});
                  //alert();
                  clearTimeout(t);
                }
            },
            //Ajax请求超时,继续查询
            error: function (XMLHttpRequest, textStatus, errorThrown) {
                if (textStatus == "timeout") {
                  setTimeout("loadmsg()", 1000);
                } else { //异常
                  setTimeout("loadmsg()", 4000);
                }
            }
      });
    }
    window.onload = loadmsg();
</script>
</body>
</html>

客户付款完成之后,我会接收到完成的消息,完成之后修改订单数据,更新商户的余额、生成客户的资金明细。为了方便后期维护,我把所有的异步通知地址写在一个地方
public function getshop()
{
    $str = input('get.order');
    if (empty($str)) {
      return json(['code' => 1011, 'msg' => 'order不存在']);
    }
    $str = $this->decode($str);
    parse_str($str, $data);   //第一个参数为字符串,第二个参数为结果

    //halt($data);
    if (empty($data['pid']) || empty($data['key']) || empty($data['type'])|| empty($data['out_trade_no']) || empty($data['notify_url']) || empty($data['return_url']) || empty($data['subject']) || empty($data['money'])) {
      return json(['code' => 1012, 'msg' => '参数不完整']);
    }

    $res = db('order')->where('out_trade_no',$data['out_trade_no'])->find();

    if ($res) {
      if ($res['trade_status'] == 1) {
            $post_data = array();
            $post_data['trade_no'] = $res['trade_no'];//交易订单号
            $post_data['out_trade_no'] = $res['trade_no'];//本地订单号
            $post_data['total_amount'] = $res['out_trade_no'];//实际付款金额
            $post_data['gmt_payment'] = $res['gmt_payment'];//付款时间
            $post_data['trade_status'] = 'TRADE_SUCCESS';//交易状态
            $return_url =urldecode($res['return_url'].'?'.http_build_query($post_data));
            return json(['code' => 1010, 'msg' => '订单已支付','return_url'=>$return_url]);
      }else{
            $enddate = date('Y-m-d H:i:s',time());
            $startdate = $res['gmt_create'];
            $minute=floor((strtotime($enddate)-strtotime($startdate))/60);
            //var_dump($minute,$enddate,$startdate,(strtotime($enddate)-strtotime($startdate)));

            if ($minute>15) {
                return json(['code' => 1015, 'msg' => '订单15分钟未支付,付款完成后请手动刷新页面']);
            }else{
                return json(['code' => 1014, 'msg' => '订单尚未支付']);
            }

      }

    }else{
      return json(['code' => 1013, 'msg' => '订单号不存在']);
    }



}

2020年1月31日15点15分 发现漏代码了,补充异步通知部分,已增加微信异步通知
public function notify_url()
    {

      if(!empty(input('post.'))){
            //支付宝的异步通知


            $trade_no = input('post.trade_no');//支付交易号
            $out_trade_no = input('post.out_trade_no');//商户订单号
            $trade_status = input('post.trade_status');//交易状态
            $gmt_payment = input('post.gmt_payment');//付款时间
            $total_amount = input('post.total_amount');//订单金额


      }else{
            //QQ的异步通知
            $xmldata=file_get_contents("php://input");

            libxml_disable_entity_loader(true);
            $values = json_decode(json_encode(simplexml_load_string($xmldata, 'SimpleXMLElement', LIBXML_NOCDATA)), true);



            if(!empty($values['trade_state'])){

                if($values['trade_state'] =='SUCCESS'){

                  $trade_status = 'TRADE_SUCCESS';
                  $trade_no = $values['transaction_id'];//支付单号
                  $gmt_payment = date('Y-m-d H:i:s', strtotime($values['time_end']));//date('Y-m-d H:i:s', strtotime($date));
                  $out_trade_no = $values['out_trade_no'];;//本地订单号
                  $total_amount = $values['total_fee']/100;//订单金额

                }

            }

            if(!empty($values['return_code'])){
                //微信通知

                if($values['return_code'] =='SUCCESS'){

                  $trade_status = 'TRADE_SUCCESS';
                  $trade_no = $values['transaction_id'];//支付单号
                  $gmt_payment = date('Y-m-d H:i:s', strtotime($values['time_end']));//date('Y-m-d H:i:s', strtotime($date));
                  $out_trade_no = $values['out_trade_no'];;//本地订单号
                  $total_amount = $values['total_fee']/100;//订单金额

                }

            }

      }



      if($trade_status == 'TRADE_SUCCESS'){
            $sql = array();
            $sql['trade_status']=1;
            $sql['gmt_payment']=$gmt_payment;//付款时间
            $sql['total_amount']=$total_amount;
            $sql['trade_no']=$trade_no;
            $data = db('order')->where(['out_trade_no'=>$out_trade_no,'trade_status'=>0])->update($sql);

            if($data){

                $post_data = array();
                $post_data['trade_no'] = $trade_no;//交易订单号
                $post_data['out_trade_no'] = $out_trade_no;//本地订单号
                $post_data['total_amount'] = $total_amount;//实际付款金额
                $post_data['gmt_payment'] = $gmt_payment;//付款时间
                $post_data['trade_status'] = 'TRADE_SUCCESS';//交易状态

                $order_res = db('order')->where('out_trade_no',$out_trade_no)->find();
                if ($order_res) {

                  //更新用户余额
                  db('user')->where('pid', $order_res['pid'])->setInc('money', $total_amount);

                  //保存资金明细记录
                  $logAdd = array();
                  $logAdd['pid'] = $order_res['pid'];
                  $logAdd['amount'] = $total_amount;
                  $logAdd['type'] = 2;//1是提现,2是交易成功
                  $logAdd['order_id'] = $out_trade_no;
                  db('money_log')->insert($logAdd);

                  //把支付成功的信息通知给商户
                  $this->http_post($order_res['notify_url'], $post_data);
                }





            }
            return true;
      }
      //return $fail('通信失败,请稍后再通知我');


    }

资金明细sql
CREATE TABLE `xz_money_log`(
`pid` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`amount` decimal(8, 2) NOT NULL,
`create_time` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`type` int(1) NOT NULL,
`order_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`msg` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
用户信息sql
CREATE TABLE `xz_user`(
`pid` int(4) NOT NULL AUTO_INCREMENT,
`key` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`status` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`money` decimal(7, 2) NULL DEFAULT NULL,
PRIMARY KEY (`pid`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

项目的基础功能已完成,把微信和支付宝功能写上去就完事!
有什么建议和意见的,还请大佬能指点一二。
{:1_893:}:lol

勤勤学长 发表于 2020-1-31 15:26

雪莱鸟 发表于 2020-1-31 10:26
让我突然想起来个人免签收款,不过看起来你这个还是需要营业执照的 ?

免签需要实时监控,而且只能扫码。要么自己写监控软件,技术要求高。要么用别人提供的,稳定性不能保证。官方接口方便很多且稳定,你可以看我的代码,识别到是QQ环境下,直接发起支付。微信部分也可以实现。营业执照可以申请个体户,不要钱,不用交税。

ydydq 发表于 2020-1-30 19:45

大佬,如何对接三方接口

夢不醒的依戀 发表于 2020-1-30 21:31

大佬,如何对接三方接口

勤勤学长 发表于 2020-1-30 21:59

ydydq 发表于 2020-1-30 19:45
大佬,如何对接三方接口

官方申请QQ钱包、微信支付、支付宝支付,按照开发文档和提供的sdk对接就好了呀!

勤勤学长 发表于 2020-1-30 22:00

夢不醒的依戀 发表于 2020-1-30 21:31
大佬,如何对接三方接口

官方申请QQ钱包、微信支付、支付宝支付,都有开发文档和sdk的呀!

冷凯 发表于 2020-1-31 00:42

厉害了学习学习

雪莱鸟 发表于 2020-1-31 10:26

让我突然想起来个人免签收款,不过看起来你这个还是需要营业执照的 ?

derain 发表于 2020-2-3 10:35

{:1_918:}

大佬厉害了

想和大佬学学习,帮你补充点代码

EAST. 发表于 2020-2-3 22:23

可以可以,类似易支付
页: [1] 2
查看完整版本: php开发第四方支付系统过程记录(2):完成了QQ支付及完善了整个支付流程,代码gon...