• PHP —— CI 框架实现微信小程序支付


    PHP —— CI 框架实现微信小程序支付

    《工欲善其事,必先利其器》

    大家好,之前学习了 原生 PHP 和 框架的知识,也着手用 TP5 实现了几个简单的接口,那么今天我们就开始学习复杂一点的东西,我们开始用 CI 框架实现一个微信小程序的支付功能。

    在这之前,我希望你能先看一下微信开发文档的内容。因为实现的过程中会涉及到很多加密和转码的功能,这些都是微信官方文档的标准要求,所以你必须先了解,每一个参数,需要什么类型的值,看下去,才不会容易迷惑。 —— 点击跳转

    一、前端小程序代码

    先从前端入手,一般前端要做的不多,只需要调用一个统一下单接口和一个小程序的 API,就可以接收支付成功或失败的消息了:

    app.post('apiorder/buynow', {
    	goods_id: this.data.goods.goods_id, // 商品ID
    	goods_num: this.data.goods.goods_num, // 商品数量
    	address_id: this.data.address.address_id, // 地址ID
    	sku: this.data.skuId // skuID
    }, res => {
    	if (res.code == 1) {
    		// 拉起微信支付
    		wx.requestPayment({
    			timeStamp: res.msg.timeStamp, // 时间戳
    			nonceStr: res.msg.nonceStr, // 随机字符串
    			package: 'prepay_id=' + res.msg.prepay_id, // 统一下单接口返回的 prepay_id 参数值,提交格式如:prepay_id=***
    			signType: 'MD5', // 签名算法,应与后台下单时的值一致
    			paySign: res.msg.paySign, // 统一下单接口返回的支付加密串
    			success: res => {
    				wx.showToast({
    					title: '付款成功'
    				})
    			},
    			fail: err => {
    				wx.showToast({
    					icon: 'none',
    					title: '未付款'
    				})
    			},
    			complete: () => {
    				wx.redirectTo({
    					url: '/pages/myOrder/index'
    				});
    			}
    		})
    	}
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    二、实现 Apiorder控制器的 buynow 方法

    这个控制器需要实现4个小功能点:

    1. 获取商品信息以及获取地址信息;
    2. 生成以及入库订单、订单商品、订单收货地址信息;
    3. 开启数据库手动事务;
    4. 执行统一下单。

    我先把主要的代码放出来,让我们依次实现。至于那些加密解密和转码的函数,我会放在最后。这样方便你们提取,看着也舒服点:

    
    public function buynow() {
    	// 如果有数据提交
    	if ($this->post()) {
    		$post=$this->post(); // 数据提取
    		// 判断收货地址
    		if (is_null($post['address_id'])) {
            	$this->show(array('code'=>0,'msg'=>'收货地址不能为空'));
            }
            $post['user_id']=$this->user_id; // 用户ID
    		$goods=$this->GetGoods($post['goods_id'],$post['sku']); // 查询商品信息
    		$address=$this->db->where('address_id',$post['address_id'])->get('address')->row_array(); // 查询地址信息
    		// 开启数据库手动事务
    		$this->db->trans_begin();
    
    		$order['order_no']=$this->orderNo(); // 生成订单号,下面有
    		$order['pay_price']=$post['goods_num']*$goods['goods_price'];
    		$order['user_id']=$this->user_id;
    		$order['create_time']=time();
    		$this->db->insert('order',$order); // 入库订单信息
    		$order_id=$this->db->insert_id(); // 获取订单ID
    
    		$address['order_id']=$order_id;
    		unset($address['default']);
    		unset($address['address_id']);
    		$this->db->insert('order_address',$address); // 入库订单收货地址信息,包含订单ID
    
    		$goods['order_id']=$order_id;
    		$goods['goods_num']=$post['goods_num'];
    		$this->db->insert('order_goods',$goods); // 入库订单商品信息,包含订单ID
    
    		// 执行统一下单,传入订单号,openid 以及 商品价格,下面有
    		$res=$this->unifiedorder($order['order_no'],$this->GetUser()['openid'],$order['pay_price']);
    
    		// 判断事务执行状态
    		if ($this->db->trans_status() === FALSE) {
    			$this->db->trans_rollback(); // 失败则回滚,撤销之前的入库信息
    			$this->show(array('code'=>0,'msg'=>'提交失败'));
    		} else {
    			$this->db->trans_commit(); // 成功则继续执行,返回前端数据
    			$this->show(array('code'=>1,'msg'=>$res));
    		}
    	} else {
    		// 如果没有数据提交,就展示商品信息
    		$post['user_id']=$this->user_id;
    		$post['goods_id']=$this->url->segment(4); // 获取url上面的第四个参数
    		$post['goods_num']=$this->uri->segment(5); // 获取url上面的第五个参数
    		$post['sku']=$this->uri->segment(6); // 获取url上面的第六个参数
    		// 查询默认地址
    		$map['user_id']=$this->user_id;
    		$map['default']=10; // 是否默认地址
    		$address=$this->db->where($map)->get('address')->row_array();
    		// 查询商品信息
    		$goods=$this->GetGoods($post['goods_id'],$post['sku']); // 查询 skuID 种类对应的 商品ID 的商品信息
    		$goods['goods_num']=$post['goods_num']; // 数量重置为用户选择的商品数量
    	 	$this->show(array('address'=>$address,'goods'=>$goods)); // 返回前端数据
    	}
    }
    ?>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59

    上面就是关于前三点小功能的实现,现在我们来实现统一下单:

    
    
    // 生成订单号
    protected function orderNo(){
    	return date('Ymd') . substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
    }
    
    /**
     * 统一下单API
     * @param $order_no
     * @param $openid
     * @param $total_fee
     * @return array
     * @throws BaseException
     */
    public function unifiedorder($order_no, $openid, $total_fee){
    	// 当前时间
    	$time = time();
    	// 生成随机字符串,对应返回给前端
    	$nonceStr = md5($time . $openid);
    	// API参数
    	$params = array(
    		'appid' => $this->configs['AppID'], // 你的 appid
    		'attach' => 'test', // 请求的备注
    		'body' => $order_no, // 请求的数据
    		'mch_id' => $this->configs['mchid'], // 你的商户号ID
    		'nonce_str' => $nonceStr, // 随机字符串
    		'notify_url' =>'https://'.$_SERVER['HTTP_HOST'].'/Apiorder/notify',  // 异步通知回调地址
    		'openid' => $openid, // 用户的openid
    		'out_trade_no' => $order_no, // 外部商户订单号
    		'spbill_create_ip' => $_SERVER["REMOTE_ADDR"], // 机器的IP地址
    		'total_fee' => $total_fee * 100, // 价格:单位分
    		'trade_type' => 'JSAPI', // 小程序固定以JSAPI调用
    	);
    	// 生成签名
    	$params['sign'] = $this->makeSign($params); // 生成统一下单临时签名
    	// 请求API
    	$url = 'https://api.mch.weixin.qq.com/pay/unifiedorder';
    	$result = $this->posts($url, $this->toXml($params)); // 发送 post 请求,需要以xml格式发送
    
    	$prepay = $this->fromXml($result); // 数据提取,把xml转回 PHP 数组
    
    	// 请求失败
    	if ($prepay['return_code'] === 'FAIL') {
    		$this->show(array('msg' => $prepay['return_msg'], 'code' => -10));
    	}
    	if ($prepay['result_code'] === 'FAIL') {
    		$this->show(array('msg' => $prepay['err_code_des'], 'code' => -10));
    	}
    	
    	// 生成正式支付加密串供前端使用,传入上面的随机字符串和统一下单的订单ID和当前时间,总之,我们必须要保证,不管怎么加密,类型怎么转
    	// 前端发送的参数,必须跟我们后端的参数,还有统一订单的参数,是一套的。
    	$paySign = $this->makePaySign($params['nonce_str'], $prepay['prepay_id'], $time);
    	// 返回对应前端调用支付API的请求数据
    	return array(
    		'prepay_id' => $prepay['prepay_id'],
    		'nonceStr' => $nonceStr,
    		'timeStamp' => (string)$time,
    		'paySign' => $paySign
    	);
    }
    
    /**
     * 支付成功异步通知
     * @param \app\task\model\Order $OrderModel
     * @throws BaseException
     * @throws \Exception
     * @throws \think\exception\DbException
     */
    public function notify(){
    	if (!$xml = file_get_contents('php://input')) {
    		$this->returnCode(false, 'Not found DATA');
    	}
    	// 将服务器返回的XML数据转化为数组
    	$data = $this->fromXml($xml);
    
    	// 保存微信服务器返回的签名sign
    	$dataSign = $data['sign'];
    	// sign不参与签名算法
    	unset($data['sign']);
    	// 生成签名
    	$sign = $this->makeSign($data);
    	// 判断签名是否正确  判断支付状态
    	if (($sign === $dataSign)
    		&& ($data['return_code'] == 'SUCCESS')
    		&& ($data['result_code'] == 'SUCCESS')) {
    		// 订单支付成功业务处理
               $Where['order_no']=$data['out_trade_no'];
               $update['order_status']=20;
               $update['pay_time']=time();
               $update['transaction_id']=$data['transaction_id'];
               
               $this->db->where($Where)->update('order', $update);
    		// 返回状态
    		$this->returnCode(true, 'OK');
    	}
    	// 返回状态
    	$this->returnCode(false, '签名失败');
    }
    
    ?>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101

    三、实现退款

    用户可以下单支付,那我们肯定也是可以进行退款申请的嘛对不?让我们继续实现一下:

    
    
    /**
     * 申请退款API
     * @param $transaction_id 微信交易流水号
     * @param  double $total_fee 订单总金额
     * @param  double $refund_fee 退款金额
     * @return bool
     * @throws BaseException
     */
    public function refund($transaction_id, $total_fee, $refund_fee){
    	// 当前时间
    	$time = time();
    	// 生成随机字符串
    	$nonceStr = md5($time . $transaction_id . $total_fee . $refund_fee);
    	// API参数
    	$params = array(
    		'appid' => $this->appId,
    		'mch_id' => $this->configs['mchid'],
    		'nonce_str' => $nonceStr,
    		'transaction_id' => $transaction_id,
    		'out_refund_no' => $time,
    		'total_fee' => $total_fee * 100,
    		'refund_fee' => $refund_fee * 100,
    	);
    	// 生成签名
    	$params['sign'] = $this->makeSign($params);
    	// 请求API
    	$url = 'https://api.mch.weixin.qq.com/secapi/pay/refund';
    	$result = $this->post($url, $this->toXml($params), true, $this->getCertPem());
    	$prepay = $this->fromXml($result);
    	// 请求失败
    	if ($prepay['return_code'] === 'FAIL') {
    		$this->show(array('msg' => 'return_msg: ' . $prepay['return_msg']));
    	}
    	if ($prepay['result_code'] === 'FAIL') {
    		$this->show(array('msg' => 'err_code_des: ' . $prepay['err_code_des']));
    	}
    	return true;
    }
    
    /**
     * 获取cert证书文件
     * @return array
     * @throws BaseException
     */
    private function getCertPem(){
    	if (empty($this->configs['cert_pem']) || empty($this->configs['key_pem'])) {
    		$this->show(array('msg' => '请先到后台小程序设置填写微信支付证书文件'));
    	}
    	// cert目录
    	$filePath = __DIR__ . '/cert/' . $this->configs['wxapp_id'] . '/';
    	!is_dir($filePath) && mkdir($filePath, 0755, true);
    	$certPem = $filePath . 'cert.pem';
    	!file_exists($certPem) && file_put_contents($certPem, $this->configs['cert_pem']);
    	$keyPem = $filePath . 'key.pem';
    	!file_exists($keyPem) && file_put_contents($keyPem, $this->configs['key_pem']);
    	return compact('certPem', 'keyPem');
    }
    
    ?>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

    四、工具函数

    很多工具函数,我一般都是会放在最后面,方便提取:

    
    
    // 查询商品数据
    public function GetGoods($goods_id,$sku){
       // 根据 goods_id 查询商品数据
       $goods=$this->db->select('goods_id,goods_name')->where('goods_id',$goods_id)->get('goods')->row_array();
       // 根据 goods_id 查询商品图片集合
       $img=$this->GetGoodsImage($goods_id);
       if (!empty($img)){
           // 如果没有图片再根据 image_id 去查询
    	   $goods['image']=$this->GetImg($img[0]['image_id']);
       }else{
           // 否则置空
    	   $goods['image']='';
       }
       // 查询商品价格信息
       $goods['goods_price']=$this->getSkuPrice($sku);
       return $goods;
    }
    
    // 查询 file 图图片
    public function GetImg($id){
    	// 根据 image_id 去查询图片
    	$list=$this->db->where('id',$id)->get('file')->row_array();
    	if ($list){
    		// 拼接域名
    		return 'https://'.$_SERVER['HTTP_HOST'].'/'.$list['url'];
    	}
    }
    
    // 查询 商品图片
    public function GetGoodsImage($goods_id){
    	// 根据 goods_id 查询图片集合
    	$where['goods_id']=$goods_id;
    	$list=$this->db->where($where)->get('goods_image')->result_array();
    	return $list;
    }
    
    // 查询 商品价格
    public function getSkuPrice($sku){
    	// 根据 sku_id 查询商品价格信息
    	$where['id']=$sku;
    	$list=$this->db->where($where)->get('goods_sku')->row_array();
    	return $list['price'];	
    }
    
    /**
     * 生成统一下单临时签名
     * @param $values
     * @return string 本函数不覆盖sign成员变量,如要设置签名需要调用SetSign方法赋值
     */
    private function makeSign($values)
    {
    	//签名步骤一:按字典序排序参数
    	ksort($values);
    	$string = $this->toUrlParams($values);
    	//签名步骤二:在string后加入KEY
    	$string = $string . '&key=' . $this->configs['apikey'];
    	//签名步骤三:MD5加密
    	$string = md5($string);
    	//签名步骤四:所有字符转为大写
    	$result = strtoupper($string);
    	return $result;
    }
    
    /**
     * 生成正式支付加密串 paySign
     * @param $nonceStr
     * @param $prepay_id
     * @param $timeStamp
     * @return string
     */
    private function makePaySign($nonceStr, $prepay_id, $timeStamp){
    	$data = array(
    		'appId' =>$this->configs['AppID'],
    		'nonceStr' => $nonceStr,
    		'package' => 'prepay_id=' . $prepay_id,
    		'signType' => 'MD5',
    		'timeStamp' => $timeStamp,
    	);
    	// 签名步骤一:按字典序排序参数
    	ksort($data);
    	$string = $this->toUrlParams($data);
    	// 签名步骤二:在string后加入KEY
    	$string = $string . '&key=' . $this->configs['apikey'];
    	// 签名步骤三:MD5加密
    	$string = md5($string);
    	// 签名步骤四:所有字符转为大写
    	$result = strtoupper($string);
    	return $result;
    }
    
    /**
     * 格式化参数格式化成url参数
     * @param $values
     * @return string
     */
    private function toUrlParams($values){
    	$buff = '';
    	foreach ($values as $k => $v) {
    		if ($k != 'sign' && $v != '' && !is_array($v)) {
    			$buff .= $k . '=' . $v . '&';
    		}
    	}
    	return trim($buff, '&');
    }
    
    /**
     * 输出xml字符
     * @param $values
     * @return bool|string
     */
    private function toXml($values){
    	if (!is_array($values)
    		|| count($values) <= 0
    	) {
    		return false;
    	}
    
    	$xml = "";
    	foreach ($values as $key => $val) {
    		if (is_numeric($val)) {
    			$xml .= "<" . $key . ">" . $val . " . $key . ">";
    		} else {
    			$xml .= "<" . $key . "> . $val . "]]> . $key . ">";
    		}
    	}
    	$xml .= "";
    	return $xml;
    }
    
    /**
     * 将xml转为array
     * @param $xml
     * @return mixed
     */
    private function fromXml($xml){
    	// 禁止引用外部xml实体
    	libxml_disable_entity_loader(true);
    	return json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);
    }
    
    /**
     * 返回状态给微信服务器
     * @param boolean $return_code
     * @param string $msg
     */
    private function returnCode($return_code = true, $msg = null){
    	// 返回状态
    	$return = array(
    		'return_code' => $return_code ? 'SUCCESS' : 'FAIL',
    		'return_msg' => $msg ?: 'OK',
    	);
    	die($this->toXml($return));
    }
    
    /**
     * 模拟POST请求
     * @param $url
     * @param array $data
     * @param bool $useCert
     * @param array $sslCert
     * @return mixed
     */
    public function posts($url, $data =array(), $useCert = false, $sslCert =array()){
    	$header = array('Content-type: application/json;');
    	$curl = curl_init();
    	curl_setopt($curl, CURLOPT_URL, $url);
    	curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
    	curl_setopt($curl, CURLOPT_HEADER, false);
    	curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
    	curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
    	curl_setopt($curl, CURLOPT_POST, TRUE);
    	curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
    	if ($useCert == true) {
    		// 设置证书:cert 与 key 分别属于两个.pem文件
    		curl_setopt($curl, CURLOPT_SSLCERTTYPE, 'PEM');
    		curl_setopt($curl, CURLOPT_SSLCERT, $sslCert['certPem']);
    		curl_setopt($curl, CURLOPT_SSLKEYTYPE, 'PEM');
    		curl_setopt($curl, CURLOPT_SSLKEY, $sslCert['keyPem']);
    	}
    	$result = curl_exec($curl);
    	curl_close($curl);
    	return $result;
    }
    
    ?>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187

    整理不易,可能不同框架会有不同的用法,但是整理的思路是一样的,希望能够帮助到你。
    感谢你的阅读。

  • 相关阅读:
    Java抽象类和普通类区别、 数组跟List的区别
    Liunx-01Liunx初相识
    论文浅尝 | 基于预训练语言模型的简单问题知识图谱问答
    Vue刷新页面VueX中数据清空了,怎么重新获取?
    ⑦、企业快速开发平台Spring Cloud之HTML 图像
    平行进口美规,加版奔驰S500 S580更换主机,汉化导航,语音交互等功能
    Android主流插件化
    面向对象(一)
    命名路由、组件中name的作用
    【Linux】用户权限——命令大全
  • 原文地址:https://blog.csdn.net/LizequaNNN/article/details/126121281