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

本文详细讲解如何使用CI框架实现微信小程序支付功能,包括前端调用统一下单接口、后端处理支付逻辑、数据库事务操作及退款功能。涉及统一下单API、支付成功异步通知和退款API的实现,以及相关加密解密工具函数。
摘要由CSDN通过智能技术生成

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'
				});
			}
		})
	}
})

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

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

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

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

<?php
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)); // 返回前端数据
	}
}
?>

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

<?php

// 生成订单号
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, '签名失败');
}

?>

三、实现退款

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

<?php

/**
 * 申请退款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');
}

?>

四、工具函数

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

<?php

// 查询商品数据
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 = "<xml>";
	foreach ($values as $key => $val) {
		if (is_numeric($val)) {
			$xml .= "<" . $key . ">" . $val . "</" . $key . ">";
		} else {
			$xml .= "<" . $key . "><![CDATA[" . $val . "]]></" . $key . ">";
		}
	}
	$xml .= "</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;
}

?>

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值