QQ小程序支付 QQ钱包支付 微信支付
日期: 2020-12-14 分类: 跨站数据 473次阅读
前言
由于公司业务需要,最近这段时间对接了QQ小程序支付【包括QQ钱包支付 和 QQ小程序内微信支付】,由于网络上相关的资料很少,遂留此文,以备后用。【顺便吐槽一下,官方文档不可全信】
由于业务关系,此处将 QQ钱包支付 和 QQ小程序内微信支付 两种支付放在一起,通过条件选择相应支付方式。如你的业务不需要同时接入两种支付方式,可自由拆分
准备工作
语言:PHP
- QQ钱包支付
官方流程图:QQ钱包支付
必要步骤:先开通QQ钱包的商户号,然后和QQ小程序进行绑定。
后端流程:
- 接收前端参数,如用户ID,商品ID…
- 根据自有规则生成订单
- 组装参数,调用QQ 统一下单接口,生成预付单
- 将QQ后台返回的参数 pre_payid 返回给前端
- 前端支付成功,QQ后台会异步回调我们的回调接口(调用QQ统一下单接口时传入)
- 回调接口里根据接收参数处理订单逻辑,如标记订单已完成支付、通知商品购买成功、消费记录…
- 微信支付
官方流程图:微信支付
必要步骤:在QQ小程序开发者管理端绑定一个微信支付商户号【这意味着你首先要开通微信支付】
后端流程:
- 后端流程基本和QQ钱宝支付差不多,不同的是QQ后台不对预付单请求做处理,只是充当一个代理转发的角色-------将请求转发至微信后台【使用微信H5统一下单方式】,在微信后台生成预付单信息
- 微信H5支付有两个版本,代号 V2 和 V3,区别和文档地址。此文使用的是 V2版本,签名采用的加密方式为 MD5
PHP服务端代码
支付类规范接口
<?php
namespace xxxx;
interface PayInterface
{
/**
* @desc 支付 Interface
*/
/**
* @desc 用户下单
* @return mixed
*/
public function createOrder();
/**
* @desc 支付回调
* @return mixed
*/
public function payNotify();
/**
* @desc 支付订单查询
* @return mixed
*/
public function queryPayOrder();
/**
* @desc 申请退款
* @return mixed
*/
public function payRefund();
}
QQ小程序支付类
<?php
namespace xxxx;
use xxxx;
class QqPay implements PayInterface
{
/************************************** QQ参数 【QQ支付相关】 ***************************************/
//QQ小程序 appID
private $appId = '';
//QQ小程序 secret
private $appSecret = '';
//QQ小程序 商户号
private $mchId = '';
//支付秘钥---需在QQ商户后台设置
private $key = '';
//获取access token url
private $getTokenUrl = 'https://api.q.qq.com/api/getToken';
//QQ钱包支付回调地址---填写处理QQ支付完成逻辑地址(需要公网能访问,不能带参数)
private $notifyUrl = '';
//接口API URL base(QQ支付接口基地址)
private $apiUrlPrefix = 'https://qpay.qq.com';
//下单地址URL (QQ钱包支付--生成预付单地址)
private $unifiedOrderUrl = "/cgi-bin/pay/qpay_unified_order.cgi";
//查询订单URL(QQ钱包支付)
private $orderQueryUrl = "/cgi-bin/pay/qpay_order_query.cgi";
//关闭订单URL (QQ钱包支付)
private $closeOrderUrl = "/cgi-bin/pay/qpay_close_order.cgi";
/************************************** 微信参数 【用于QQ小程序调用微信支付】 ***************************************/
//wx key
private $wxKey = '';
//微信公众号appId
private $wxAppId = '';
//微信商户号
private $wxMchId = '';
//QQ 小程序平台 V2 版 支付回调代理地址
private $notifyUrlQqAgent = 'https://api.q.qq.com/wxpay/notify';
//wap_url WAP网站URL地址 【微信H5下单必填参数,详见:https://pay.weixin.qq.com/wiki/doc/api/H5.php?chapter=9_20&index=1】
private $wapUrl = '';
//wap_name WAP 网站名 【微信H5下单必填参数】
private $wapName = '';
//QQ小程序内微信支付回调url---填写处理微信支付完成逻辑地址(需要公网能访问,不能带参数)【微信H5支付成功通知地址,微信回调的实际是QQ后台地址,然后QQ后台转发到此地址】
private $wxNotifyUrl = '';
//H5下单代理url base
private $qqAgentUrl = 'https://api.q.qq.com/wxpay/unifiedorder';
/**
* @desc 用户下单 创建预支付订单
* @return array|mixed|string[]
* @author BaTianHu
*/
public function createOrder()
{
//校验用户登录态--如无特殊原因,此步骤强烈建议在逻辑层之前进行(如:鉴权层),如未登陆,或者权限不够,直接返回
//接收参数,如用户ID,商品ID...
//查询相应信息,并验证信息的有效性,如商品信息,如无效,直接返回
//todo 如有需要,可将订单逻辑拆分出去,会显得数据层级更合理,更易维护
try {
//生成订单号
$orderNo = Pay::createOrderNo();
//应付价格 单位 /分
$totalFee = $price * 100;
//赋值
$pay = new Pay();
$pay->order_no = $orderNo;
......
$pay->created_at = time();
//保存订单
if ($pay->save(false)) {
//预下单
$retInfo = $this->unifiedOrder($orderNo, $totalFee, $isWx);
//判断预字符订单是否生成成功
if ($retInfo['return_code'] === 'SUCCESS' && $retInfo['result_code'] === 'SUCCESS') {
//QQ钱包支付需返回参数
$data['prepay_id'] = $retInfo['prepay_id'];
if ($isWx) {
//微信支付需返回参数-----用于前端跳转微信支付
$data['mweb_url'] = $retInfo['mweb_url'];
//用于前端查询订单状态
$data['order_no'] = $orderNo;
}
return ['errcode' => 'ok', 'errmsg' => '创建订单成功', 'data' => $data];
}
//todo 如果需要,这里可以存一个日志,保存失败原因
return ['errcode' => 'fail', 'errmsg' => '预下单失败'];
} else {
return ['errcode' => 'fail', 'errmsg' => '创建订单失败'];
}
} catch (\Exception $exception) {
//todo 记录错误信息
return ['errcode' => 'fail', 'errmsg' => '创建订单失败'];
}
}
/**
* @desc 支付回调【QQ钱包支付】
* @return array|false|mixed
* @throws Exception
* @author BaTianHu
*/
public function payNotify()
{
$notifyDataXml = file_get_contents("php://input");
$data = OtherCommon::xml_to_data($notifyDataXml);
$sign = $data['sign'];
unset($data['sign']);
if ($sign <> $this->sign($data)) {
// 验签失败
return $this->setRetInfo('签名失败');
}
// 如果订单已支付,进行业务处理并返回核销信息
if(isset($data['trade_state']) && $data['trade_state'] == 'SUCCESS') {
//处理订单支付逻辑---此处调用处理订单逻辑方法(根据各自业务场景或有不同)
$notifyDealInfo = Pay::payNotifyPro($data['out_trade_no'], $data['total_fee'], $data['transaction_id']);
if ($notifyDealInfo === true) {
//逻辑处理成功
return $this->setRetInfo();
}
//逻辑处理失败
return $this->setRetInfo('逻辑处理失败');
}
//参数格式错误
return $this->setRetInfo('参数格式校验错误');
}
/**
* @desc QQ预下单 【QQ钱包支付 or QQ内微信支付】
* @param string $orderNo
* @param int $totalFee
* @param bool $isWx
* @return array
* @throws LocalRedisException
* @author BaTianHu
*/
private function unifiedOrder(string $orderNo, int $totalFee, $isWx = false) :array
{
$params['nonce_str'] = 'hdakhgdjsa'; //随机数---生成一个随机数
$params['body'] = '测试---不可描述'; //商品描述
$params['out_trade_no'] = $orderNo; //订单号--商户平台订单号
$params['total_fee'] = $totalFee; //总金额 单位 /分
$params['spbill_create_ip'] = OtherCommon::getUserIp(); //终端IP---用户端实际IP
if ($isWx) {
//QQ内微信支付
$params['appid'] = $this->wxAppId;
$params['mch_id'] = $this->wxMchId;
$params['sign_type'] = 'MD5'; //签名方式
$params['notify_url'] = $this->notifyUrlQqAgent;
$params['trade_type'] = 'MWEB'; //交易类型
$params['scene_info'] = '{"h5_info": {"type":"Wap","wap_url": '.$this->wapUrl.',"wap_name": "'.$this->wapName.'"}}';
//签名
$params['sign'] = $this->sign($params, $this->wxKey);
//获取代理支付地址
$url = $this->getQqAgentUrl($this->qqAgentUrl);
} else {
//QQ钱包支付
$params['appid'] = $this->appId;
$params['mch_id'] = $this->mchId;
$params['fee_type'] = 'CNY'; //货币类型 人民币
$params['notify_url'] = OtherCommon::getBaseUrl($this->notifyUrl, 'https://'); //回调地址
$params['trade_type'] = 'MINIAPP'; //支付场景
//签名
$params['sign'] = $this->sign($params);
//支付地址
$url = $this->getQqUrl($this->unifiedOrderUrl);
}
//数组转xml
$xml = OtherCommon::data_to_xml($params);
//请求QQ预下单接口
$response = OtherCommon::postXmlCurl($xml, $url);
//返回数组结果
return OtherCommon::xml_to_data($response);
}
/**
* @desc 支付订单查询
* @return mixed
*/
public function queryPayOrder()
{
// TODO: Implement queryPayOrder() method.
}
/**
* @desc 申请退款
* @return mixed
*/
public function payRefund()
{
// TODO: Implement payRefund() method.
}
/**
* @desc 生成|校验 签名
* @param array $signParam 参与签名的参数
* @param string $key 默认为QQ支付 key
* @return string
* @author BaTianHu
*/
private function sign(array $signParam, string $key = '')
{
$sign = '';
if (empty($signParam)) {
return $sign;
}
//按字母升序排序
ksort($signParam);
$parts = array();
foreach ($signParam as $k => $v) {
$parts[] = $k . '=' . $v;
}
$sign = implode('&', $parts);
if (empty($key)) {
$key = $this->key;
}
$sign = $sign . "&key=".$key;
return strtoupper(md5($sign));
}
/**
* @desc 组装请求目的地址
* @param $relativeUrl
* @return string
* @author BaTianHu
*/
private function getQqUrl($relativeUrl)
{
return $this->apiUrlPrefix.$relativeUrl;
}
/**
* @desc 设置返回信息
* @param string $errMsg 错误信息
* @return array|false|mixed
* @author BaTianHu
*/
private function setRetInfo(string $errMsg = '')
{
if (empty($errMsg)) {
//处理成功
$data['return_code'] = 'SUCCESS';
} else {
//处理失败
$data['return_code'] = 'FAIL';
//失败原因
$data['return_msg'] = $errMsg;
}
return OtherCommon::data_to_xml($data);
}
/**
* @desc 组装支付代理地址【QQ小程序平台-代理微信H5支付】
* @param $baseAgentUrl
* @return string
* @throws LocalRedisException
* @author BaTianHu
*/
private function getQqAgentUrl($baseAgentUrl)
{
//获取access token
$accessToken = $this->getAccessToken();
//组装真实回调url
$UrlEncodedNotifyUrl = urlencode(OtherCommon::getBaseUrl($this->wxNotifyUrl, 'https://'));
//组装返回QQ代理微信支付URL
return $baseAgentUrl.'?appid='.$this->appId.'&access_token='.$accessToken.'&&real_notify_url='.$UrlEncodedNotifyUrl;
}
/**
* @desc 获取AccessToken
* @return bool|mixed|string
* @throws LocalRedisException
* @author BaTianHu
*/
private function getAccessToken()
{
//先尝试redis获取AccessToken
$accessToken = $redis->get(ConstKeyHelpers::QQ_ACCESS_TOKEN_PRO);
if (empty($accessToken)) {
$param = [
'grant_type' => 'client_credential',
'appid' => $this->appId,
'secret' => $this->appSecret,
];
//请求QQ接口,获取AccessToken
$retInfo = OtherCommon::curlGet($this->getTokenUrl, $param);
$retInfo = json_decode($retInfo, true);
if ($retInfo['errcode'] === 0) {
$accessToken = $retInfo['access_token'];
$expressTime = $retInfo['expires_in'] - 100;
//将AccessToken存入redis
$redis->set(ConstKeyHelpers::QQ_ACCESS_TOKEN_PRO, $accessToken, $expressTime);
}
}
return $accessToken;
}
}
公共方法文件
<?php
namespace xxx;
use xxxxx;
class OtherCommon
{
/**
* @desc 组装绝对地址 || 获取网站基地址
* @param string $relativeUrl
* @param string $protocolType 示例:https://
* @return string
* @author BaTianHu
*/
public static function getBaseUrl($relativeUrl = '', string $protocolType = '') :string
{
if (empty($protocolType)) {
$protocolType = self::getHttpType();
}
return $protocolType.self::getHostDomain().$relativeUrl;
}
/**
* @desc 获取当前网址协议(HTTP/HTTPS)
* @return string
* @author BaTianHu
*/
public static function getHttpType() :string
{
return ((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') || (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https')) ? 'https://' : 'http://';
}
//获取host信息
public static function getHostDomain()
{
return $_SERVER['HTTP_HOST'] ?? '';
}
/**
* @desc 获取用户的 IP 地址
* @return mixed|string
* @author BaTianHu
*/
public static function getUserIp()
{
return $_SERVER['REMOTE_ADDR'] ?? '';
}
/**
* @param $stringOne
* @param $stringTwo
* @return float|int
* 返回两个字符串的相似度
* vine 2019年4月22日10:10:57
*/
public static function diffWords($stringOne, $stringTwo)
{
$sameNumber = similar_text($stringOne, $stringTwo);
return $sameNumber * 2 / (strlen($stringOne) + strlen($stringTwo));
}
/**
* @desc curl get
* @param string $url
* @param array $arr
* @return bool|string
* @author BaTianHu
*/
public static function curlGet(string $url, array $arr = [])
{
//组装get参数
if (!empty($arr)) {
$tempArr = [];
foreach ($arr as $k => $v) {
$tempArr[] = $k.'='.$v;
}
$str = implode('&',$tempArr);
$url = $url.'?'.$str;
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$output = curl_exec($ch);
curl_close($ch);
return $output;
}
/**
* @desc post curl
* @param string $url
* @param array $data
* @return bool|string
* @author BaTianHu
*/
public static function postJsonCurl(string $url, array $data)
{
$data_string = json_encode($data);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_POSTFIELDS,$data_string);
curl_setopt($ch, CURLOPT_RETURNTRANSFER,true);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Content-Type: application/json',
'Accept: application/json',
'User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; SV1)',
'Content-Length: ' . strlen($data_string))
);
//忽略ssl检查
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$data = curl_exec($ch);
if (curl_errno($ch)) {
$data = curl_error($ch);
}
curl_close($ch);
return $data;
}
/**
* @desc 以post方式提交xml到对应的接口url
*
* @param string $xml 需要post的xml数据
* @param string $url url
* @param bool $useCert 是否需要证书,默认不需要
* @param int $second url执行超时时间,默认30s
* @return bool|string
* @author BaTianHu
*/
public static function postXmlCurl(string $xml, string $url, $useCert = false, $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,2);
//设置header
curl_setopt($ch, CURLOPT_HEADER, FALSE);
//要求结果为字符串且输出到屏幕上
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
if($useCert == true){
//设置证书
//使用证书:cert 与 key 分别属于两个.pem文件
curl_setopt($ch,CURLOPT_SSLCERTTYPE,'PEM');
//curl_setopt($ch,CURLOPT_SSLCERT, WxPayConfig::SSLCERT_PATH);
curl_setopt($ch,CURLOPT_SSLKEYTYPE,'PEM');
//curl_setopt($ch,CURLOPT_SSLKEY, WxPayConfig::SSLKEY_PATH);
}
//post提交方式
curl_setopt($ch, CURLOPT_POST, TRUE);
curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
//运行curl
$data = curl_exec($ch);
curl_close($ch);
return $data;
}
/**
* @desc 数组 转 XML
* @param array $params 参数名称
* @return false|string
* @author BaTianHu
*/
public static function data_to_xml(array $params)
{
if (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;
}
/**
* @desc 将xml转为array
* @param string $xml
* @return false|mixed
* @author BaTianHu
*/
public static function xml_to_data(string $xml = '')
{
if (empty($xml)) {
return [];
}
//将XML转为array 禁止引用外部xml实体
libxml_disable_entity_loader(true);
return json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);
}
}
值得注意的事
I. QQ钱包支付通知文档中,trade_state 参数写的是 首字母大写 Success,但其实是全大写 SUCCESS
II. QQ商户后台无法登陆,下载安装安全控件后依然无法登陆
若安装安全控件后不生效,页面一直提示未安装的情况,可安装如下浏览器操作。(适用于Windows及Mac OS等系统)
1、Chrome浏览器
若浏览器已升级到Chrome 76.0.3809.87(简称Chrome 76)及以上版本,请查看下述指引:
(1)在浏览器的地址栏输入 chrome://flags/#enable-nacl
(2)找到Native Client插件,将Native Client的状态改为Enabled
(3)重启浏览器,再重新登录QQ钱包商户平台,尝试安装安全控件
2、Internet Explorer 11浏览器
请打开如下链接:https://support.microsoft.com/zh-cn/help/17621/internet-explorer-downloads安装 Internet Explorer 11浏览器后,再尝试安装安全控件。
除特别声明,本站所有文章均为原创,如需转载请以超级链接形式注明出处:SmartCat's Blog
上一篇: C++P1803 凌乱的yyy / 线段覆盖贪心(DP)
下一篇: 连接查询
精华推荐