first commit

This commit is contained in:
2025-11-28 10:08:12 +08:00
commit 09a14bf0a3
1088 changed files with 145132 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 消失的彩虹海
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+39
View File
@@ -0,0 +1,39 @@
# QQPay SDK for PHP
QQ钱包支付第三方 PHP SDK,基于官方最新版本。
### 功能特点
- 根据QQ钱包支付最新API开发,相比官方SDK,功能更完善,代码更简洁
- 支持Composer安装,无需加载多余组件,可应用于任何平台或框架
- 符合`PSR`标准,你可以各种方便的与你的框架集成
- 基本完善的PHPDoc,可以随心所欲添加本项目中没有的API接口
### 环境要求
`PHP` >= 7.1
### 使用方法
1. Composer 安装。
```bash
composer require cccyun/qqpay-sdk
```
2. 创建配置文件 [`config.php`](./examples/config.php),填写QQ钱包支付商户信息。
3. 引入配置文件,构造请求参数,调用PaymentService中的方法发起请求,参考 [`examples/qrpay.php`](./examples/qrpay.php)。
4. 更多实例,请移步 [`examples`](examples/) 目录。
5. 类功能说明
| 类名 | 说明 |
| --------------- | ------------------------------------ |
| PaymentService | 基础支付服务类,所有支付功能都用这个 |
| TransferService | QQ钱包企业付款功能 |
6. 要对接的API在以上实现类中没有,可根据QQ钱包官方的文档,使用BaseService类中的execute方法直接调用接口。
+27
View File
@@ -0,0 +1,27 @@
{
"name": "cccyun/qqpay-sdk",
"description": "QQ钱包支付第三方 PHP SDK,基于官方最新版本。",
"type": "library",
"keywords": [
"qqpay",
"qpay",
"QQ支付"
],
"license": "MIT",
"minimum-stability": "dev",
"prefer-stable": true,
"require": {
"php": ">=7.1"
},
"authors": [
{
"name": "caihong",
"email": "admin@cccyun.cn"
}
],
"autoload": {
"psr-4": {
"QQPay\\": "src/"
}
}
}
+238
View File
@@ -0,0 +1,238 @@
<?php
namespace QQPay;
class BaseService
{
//商户号
protected $mchId;
//商户API密钥
protected $apiKey;
//应用APPID(可空)
protected $appId;
//应用APPKEY(可空)
protected $appKey;
//商户证书路径
protected $sslCertPath;
//商户证书私钥路径
protected $sslKeyPath;
//操作员ID
protected $opUserId;
//操作员密码
protected $opUserPwd;
//公共请求参数
protected $publicParams = [];
/**
* @param $config 微信支付配置信息
*/
public function __construct($config)
{
if (empty($config['mchid'])) {
throw new \InvalidArgumentException("商户号不能为空");
}
if (empty($config['apikey'])) {
throw new \InvalidArgumentException("商户API密钥不能为空");
}
$this->mchId = $config['mchid'];
$this->apiKey = $config['apikey'];
if (isset($config['appid'])) {
$this->appId = $config['appid'];
}
if (isset($config['appkey'])) {
$this->appKey = $config['appkey'];
}
$this->sslCertPath = $config['sslcert_path'];
$this->sslKeyPath = $config['sslkey_path'];
if (isset($config['op_userid'])) {
$this->opUserId = $config['op_userid'];
}
if (isset($config['op_userpwd'])) {
$this->opUserPwd = $config['op_userpwd'];
}
}
/**
* 请求接口并解析返回数据
* @param $url url
* @param $params 请求参数
* @param $cert 是否需要证书
* @return mixed
*/
public function execute($url, $params, $cert = false)
{
$params = array_merge($this->publicParams, $params);
$params['sign'] = $this->makeSign($params);
$xml = $this->array2Xml($params);
$response = $this->curl($url, $xml, $cert);
$result = $this->xml2array($response);
if (isset($result['return_code']) && $result['return_code'] == 'SUCCESS') {
if (isset($result['result_code']) && $result['result_code'] == 'SUCCESS') {
if (isset($result['sign']) && !$this->checkSign($result)) {
throw new \Exception('返回数据验签失败');
}
return $result;
}
}
throw new QQPayException($result);
}
/**
* 下载账单接口
* @param $url url
* @param $params 请求参数
* @param $cert 是否需要证书
* @return mixed
*/
public function download($url, $params)
{
$params = array_merge($this->publicParams, $params);
$params['sign'] = $this->makeSign($params);
$xml = $this->array2Xml($params);
$response = $this->curl($url, $xml);
return $response;
}
/**
* 验签
* @param $data
* @return bool
*/
protected function checkSign($data)
{
if (!isset($data['sign'])) return false;
$sign = $this->makeSign($data);
return $sign === $data['sign'];
}
/**
* 生成签名
* @param $data
* @return string
*/
protected function makeSign($data)
{
ksort($data);
$signStr = '';
foreach ($data as $k => $v) {
if($k != 'sign' && !is_array($v) && !$this->isEmpty($v)){
$signStr .= $k . '=' . $v . '&';
}
}
$signStr = trim($signStr, '&') . '&key=' . $this->apiKey;
$sign = md5($signStr);
return strtoupper($sign);
}
/**
* 校验某字符串或可被转换为字符串的数据,是否为 NULL 或均为空白字符.
*
* @param string|null $value
*
* @return bool
*/
protected function isEmpty($value)
{
return $value === null || $value === '';
}
/**
* 产生随机字符串,不长于32位
* @param int $length
* @return 产生的随机字符串
*/
protected function getNonceStr($length = 32)
{
$chars = "abcdefghijklmnopqrstuvwxyz0123456789";
$str = "";
for ($i = 0; $i < $length; $i++) {
$str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
}
return $str;
}
/**
* 转为XML数据
* @param array $data 源数据
* @return string
*/
protected function array2Xml($data)
{
if (!is_array($data)) {
return false;
}
$xml = '<xml>';
foreach ($data as $key => $val) {
$xml .= (is_numeric($val) ? "<{$key}>{$val}</{$key}>" : "<{$key}><![CDATA[{$val}]]></{$key}>");
}
return $xml . '</xml>';
}
/**
* 解析XML数据
* @param string $xml 源数据
* @return mixed
*/
protected function xml2array($xml)
{
if (!$xml) {
return false;
}
LIBXML_VERSION < 20900 && libxml_disable_entity_loader(true);
return json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA), JSON_UNESCAPED_UNICODE), true);
}
/**
* 以post方式提交xml到对应的接口url
* @param string $url url
* @param string $xml 需要post的xml数据
* @param bool $useCert 是否需要证书
* @param int $second url执行超时时间
* @return string
*/
protected function curl($url, $xml, $useCert = false, $second = 10)
{
$ch = curl_init();
$curlVersion = curl_version();
$ua = "QQPaySDK/1.0 (" . PHP_OS . ") PHP/" . PHP_VERSION . " CURL/" . $curlVersion['version'] . " ". $this->mchId;
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);
curl_setopt($ch, CURLOPT_USERAGENT, $ua);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
if ($useCert) {
if (!file_exists($this->sslCertPath) || !file_exists($this->sslKeyPath)) {
throw new \Exception('商户证书文件不存在');
}
//使用证书:cert 与 key 分别属于两个.pem文件
curl_setopt($ch, CURLOPT_SSLCERTTYPE, 'PEM');
curl_setopt($ch, CURLOPT_SSLCERT, $this->sslCertPath);
curl_setopt($ch, CURLOPT_SSLKEYTYPE, 'PEM');
curl_setopt($ch, CURLOPT_SSLKEY, $this->sslKeyPath);
}
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
$data = curl_exec($ch);
if (curl_errno($ch) > 0) {
$errmsg = curl_error($ch);
curl_close($ch);
throw new \Exception($errmsg, 0);
}
curl_close($ch);
return $data;
}
}
+317
View File
@@ -0,0 +1,317 @@
<?php
namespace QQPay;
/**
* QQ钱包支付服务类
* @see https://mp.qpay.tenpay.cn/buss/wiki/38/1188
*/
class PaymentService extends BaseService
{
public function __construct($config)
{
parent::__construct($config);
$this->publicParams = [
'mch_id' => $this->mchId,
'nonce_str' => $this->getNonceStr(),
];
}
/**
* 统一下单
* @param $params 下单参数
* @return mixed
*/
public function unifiedOrder($params)
{
$url = 'https://qpay.qq.com/cgi-bin/pay/qpay_unified_order.cgi';
if (empty($params['out_trade_no'])) {
throw new \InvalidArgumentException('缺少统一支付接口必填参数out_trade_no');
}
if (empty($params['body'])) {
throw new \InvalidArgumentException('缺少统一支付接口必填参数body');
}
if (empty($params['total_fee'])) {
throw new \InvalidArgumentException('缺少统一支付接口必填参数total_fee');
}
if (empty($params['trade_type'])) {
throw new \InvalidArgumentException('缺少统一支付接口必填参数trade_type');
}
return $this->execute($url, $params);
}
/**
* NATIVE支付
* @param $params 下单参数
* @return mixed {"code_url":"二维码链接","prepay_id":"预支付会话标识"}
*/
public function nativePay($params)
{
$params['trade_type'] = 'NATIVE';
return $this->unifiedOrder($params);
}
/**
* JSAPI支付
* @param $params 下单参数
* @return mixed {"tokenId":"预支付会话标识","appInfo":"标记业务及渠道"}
*/
public function jsapiPay($params)
{
$params['trade_type'] = 'JSAPI';
$result = $this->unifiedOrder($params);
return ['tokenId' => $result['prepay_id'], 'appInfo' => 'appid#' . $this->appId . '|bargainor_id#' . $this->mchId . '|channel#wallet'];
}
/**
* APP支付
* @param $params 下单参数
* @return mixed APP支付json数据
*/
public function appPay($params)
{
$params['trade_type'] = 'APP';
$result = $this->unifiedOrder($params);
return $this->getAppParameters($result['prepay_id']);
}
/**
* 获取APP支付的参数
* @param $prepay_id 预支付交易会话标识
* @return array
*/
private function getAppParameters($prepay_id)
{
$params = [
'appId' => $this->appId,
'nonce' => $this->getNonceStr(),
'tokenId' => $prepay_id,
'pubAcc' => '',
'bargainorId' => $this->mchId,
];
$params['sig'] = $this->makeAppSign($params);
$params['sigType'] = 'HMAC-SHA1';
$params['timeStamp'] = time();
return $params;
}
/**
* 生成APP支付签名
* @param $data
* @return string
*/
private function makeAppSign()
{
ksort($data);
$signStr = '';
foreach ($data as $k => $v) {
$signStr .= $k . '=' . $v . '&';
}
$signStr = trim($signStr, '&');
$sign = base64_encode(hash_hmac("sha1", $signStr, $this->appKey.'&', true));
return $sign;
}
/**
* 付款码支付
* @param $params 下单参数
* @return mixed {"trade_state":"SUCCESS","total_fee":888,"cash_fee":888,"transaction_id":"QQ钱包订单号","out_trade_no":"商户订单号","time_end":"支付完成时间","trade_state_desc":"交易状态描述","openid":"用户标识"}
*/
public function microPay($params)
{
$url = 'https://qpay.qq.com/cgi-bin/pay/qpay_micro_pay.cgi';
if (empty($params['out_trade_no'])) {
throw new \InvalidArgumentException('缺少付款码支付接口必填参数out_trade_no');
}
if (empty($params['body'])) {
throw new \InvalidArgumentException('缺少付款码支付接口必填参数body');
}
if (empty($params['total_fee'])) {
throw new \InvalidArgumentException('缺少付款码支付接口必填参数total_fee');
}
if (empty($params['auth_code'])) {
throw new \InvalidArgumentException('缺少付款码支付接口必填参数auth_code');
}
return $this->execute($url, $params);
}
/**
* 撤销订单
* @param $out_trade_no 商户订单号
* @return mixed
*/
public function reverse($out_trade_no)
{
$url = 'https://api.qpay.qq.com/cgi-bin/pay/qpay_reverse.cgi';
$params = [
'out_trade_no' => $out_trade_no,
'op_user_id' => $this->opUserId,
'op_user_passwd' => md5($this->opUserPwd)
];
return $this->execute($url, $params, true);
}
/**
* 查询订单,QQ钱包订单号、商户订单号至少填一个
* @param $transaction_id QQ钱包订单号
* @param $out_trade_no 商户订单号
* @return mixed
*/
public function orderQuery($transaction_id = null, $out_trade_no = null)
{
$url = 'https://qpay.qq.com/cgi-bin/pay/qpay_order_query.cgi';
$params = [];
if ($transaction_id) {
$params['transaction_id'] = $transaction_id;
} elseif ($out_trade_no) {
$params['out_trade_no'] = $out_trade_no;
}
return $this->execute($url, $params);
}
/**
* 判断订单是否已完成
* @param $transaction_id QQ钱包订单号
* @return bool
*/
public function orderQueryResult($transaction_id)
{
try {
$data = $this->orderQuery($transaction_id);
return $data['trade_state'] == 'SUCCESS' || $data['trade_state'] == 'REFUND';
} catch (\Exception $e) {
return false;
}
}
/**
* 关闭订单
* @param $out_trade_no 商户订单号
* @return mixed
*/
public function closeOrder($out_trade_no)
{
$url = 'https://qpay.qq.com/cgi-bin/pay/qpay_close_order.cgi';
$params = [
'out_trade_no' => $out_trade_no
];
return $this->execute($url, $params);
}
/**
* 申请退款
* @param $params
* @return mixed
*/
public function refund($params)
{
$url = 'https://api.qpay.qq.com/cgi-bin/pay/qpay_refund.cgi';
if (empty($params['transaction_id']) && empty($params['out_trade_no'])) {
throw new \InvalidArgumentException('out_trade_no、transaction_id至少填一个');
}
if (empty($params['out_refund_no'])) {
throw new \InvalidArgumentException('out_refund_no参数不能为空');
}
if (empty($params['refund_fee'])) {
throw new \InvalidArgumentException('refund_fee参数不能为空');
}
$params += [
'op_user_id' => $this->opUserId,
'op_user_passwd' => md5($this->opUserPwd)
];
return $this->execute($url, $params, true);
}
/**
* 查询退款
* @param $params
* @return mixed
*/
public function refundQuery($params)
{
$url = 'https://qpay.qq.com/cgi-bin/pay/qpay_refund_query.cgi';
if (empty($params['transaction_id']) && empty($params['out_trade_no']) && empty($params['out_refund_no']) && empty($params['refund_id'])) {
throw new \InvalidArgumentException('退款查询接口中,out_refund_no、out_trade_no、transaction_id、refund_id四个参数必填一个');
}
return $this->execute($url, $params);
}
/**
* 下载对账单
* @param $params
* @return mixed
*/
public function downloadBill($params)
{
$url = 'https://qpay.qq.com/cgi-bin/sp_download/qpay_mch_statement_down.cgi';
if (empty($params['bill_date'])) {
throw new \InvalidArgumentException('bill_date参数不能为空');
}
if (empty($params['bill_type'])) {
throw new \InvalidArgumentException('bill_type参数不能为空');
}
return $this->download($url, $params);
}
/**
* 下载资金账单
* @param $params
* @return mixed
*/
public function downloadFundFlow($params)
{
$url = 'https://qpay.qq.com/cgi-bin/sp_download/qpay_mch_acc_roll.cgi';
if (empty($params['bill_date'])) {
throw new \InvalidArgumentException('bill_date参数不能为空');
}
if (empty($params['acc_type'])) {
throw new \InvalidArgumentException('acc_type参数不能为空');
}
return $this->download($url, $params);
}
/**
* 支付结果通知
* @return bool|mixed
*/
public function notify()
{
$xml = file_get_contents("php://input");
if (empty($xml)) {
throw new \Exception('NO_DATA');
}
$result = $this->xml2array($xml);
if (!$result) {
throw new \Exception('XML_ERROR');
}
if (!$this->checkSign($result)) {
throw new \Exception('签名校验失败');
}
if (!isset($result['transaction_id'])) {
throw new \Exception('缺少订单号参数');
}
if (!$this->orderQueryResult($result['transaction_id'])) {
throw new \Exception('订单未完成');
}
return $result;
}
/**
* 回复通知
* @param $isSuccess 是否成功
* @param $msg 失败原因
*/
public function replyNotify($isSuccess = true, $msg = '')
{
$data = [];
if ($isSuccess) {
$data['return_code'] = 'SUCCESS';
} else {
$data['return_code'] = 'FAIL';
$data['return_msg'] = $msg;
}
$xml = $this->array2Xml($data);
echo $xml;
}
}
+39
View File
@@ -0,0 +1,39 @@
<?php
namespace QQPay;
/**
* QQ钱包支付响应内容异常
*/
class QQPayException extends \Exception
{
private $res = [];
private $errCode;
/**
* @param array $res
*/
public function __construct($res)
{
$this->res = $res;
if (isset($res['err_code'])) {
$this->errCode = $res['err_code'];
$message = '['.$res['err_code'].']'.$res['err_code_des'];
} elseif (isset($res['return_code'])) {
$message = '['.$res['return_code'].']'.$res['return_msg'];
} else {
$message = '返回数据解析失败';
}
parent::__construct($message);
}
public function getResponse()
{
return $this->res;
}
public function getErrCode()
{
return $this->errCode;
}
}
@@ -0,0 +1,68 @@
<?php
namespace QQPay;
/**
* QQ钱包转账服务类
* @see https://mp.qpay.tenpay.cn/buss/wiki/206/1214
*/
class TransferService extends BaseService
{
public function __construct($config)
{
parent::__construct($config);
$this->publicParams = [
'mch_id' => $this->mchId,
'nonce_str' => $this->getNonceStr(),
];
}
/**
* 企业付款到余额
* @param $out_trade_no 商户订单号
* @param $uin 收款QQ号码
* @param $name 用户姓名(填写后校验)
* @param $amount 金额
* @param $memo 备注
* @return mixed {"out_trade_no":"商户订单号","transaction_id":"QQ钱包订单号"}
*/
public function transfer($out_trade_no, $uin, $name, $amount, $memo)
{
$url = 'https://api.qpay.qq.com/cgi-bin/epay/qpay_epay_b2c.cgi';
$params = [
'input_charset' => 'UTF-8',
'out_trade_no' => $out_trade_no,
'uin' => $uin,
'fee_type' => 'CNY',
'total_fee' => $amount,
'memo' => $memo,
'check_real_name' => '0'
];
if (!empty($name)) {
$params['check_name'] = 'FORCE_CHECK';
$params['re_user_name'] = $name;
}
$params += [
'op_user_id' => $this->opUserId,
'op_user_passwd' => md5($this->opUserPwd),
'spbill_create_ip' => $_SERVER['SERVER_ADDR']
];
return $this->execute($url, $params, true);
}
/**
* 查询企业付款
* @param $out_trade_no 商户订单号
* @return mixed {"out_trade_no":"商户订单号","detail_id":"微信付款单号","status":"转账状态","reason":"失败原因","openid":"用户openid","transfer_name":"用户姓名","payment_amount":"付款金额","transfer_time":"转账时间","payment_time":"付款成功时间","desc":"付款备注"}
*/
public function transferQuery($out_trade_no)
{
$url = 'https://qpay.qq.com/cgi-bin/pay/qpay_epay_query.cgi';
$params = [
'out_trade_no' => $out_trade_no
];
return $this->execute($url, $params);
}
}