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