330 lines
12 KiB
PHP
330 lines
12 KiB
PHP
<?php
|
||
|
||
namespace app\common\server;
|
||
|
||
use app\MyController;
|
||
use app\common\helper\WxPayHelper;
|
||
|
||
|
||
class WechatRefund extends MyController
|
||
{
|
||
|
||
/**
|
||
* 订单退款
|
||
* @param $params 下单参数
|
||
*/
|
||
public function OrderRefund($info)
|
||
{
|
||
// 提取订单号中的商户前缀和小程序前缀
|
||
$prefixInfo = null;
|
||
$merchant_prefix = null;
|
||
$miniprogram_prefix = null;
|
||
|
||
if (!empty($info['send_num']) &&( strpos($info['send_num'], 'MH_') === 0|| strpos($info['send_num'], 'FH_') === 0)) {
|
||
$prefixInfo = WxPayHelper::extractOrderPrefix($info['send_num']);
|
||
|
||
// 新格式返回为数组,包含商户前缀和可能的小程序前缀
|
||
if (is_array($prefixInfo)) {
|
||
$merchant_prefix = $prefixInfo['merchant_prefix'] ?? null;
|
||
$miniprogram_prefix = $prefixInfo['miniprogram_prefix'] ?? null;
|
||
} else {
|
||
// 兼容旧格式,直接作为商户前缀
|
||
$merchant_prefix = $prefixInfo;
|
||
}
|
||
}
|
||
|
||
// 优先根据小程序前缀获取配置
|
||
$wxpayConfig = null;
|
||
if (!empty($miniprogram_prefix)) {
|
||
// 通过小程序前缀获取小程序配置
|
||
$miniprogramConfig = \app\common\helper\MiniprogramHelper::getMiniprogramConfigByOrderPrefix($miniprogram_prefix);
|
||
|
||
if (!empty($miniprogramConfig)) {
|
||
// 使用小程序关联的商户配置
|
||
if (!empty($merchant_prefix)) {
|
||
$wxpayConfig = WxPayHelper::getFixedWxPayConfig($merchant_prefix);
|
||
// 确保使用小程序的appid
|
||
$wxpayConfig['appid'] = $miniprogramConfig['appid'];
|
||
} else {
|
||
// 没有商户前缀但有小程序配置,使用小程序默认关联的商户
|
||
$wxpayConfig = WxPayHelper::getWxPayConfig();
|
||
$wxpayConfig['appid'] = $miniprogramConfig['appid'];
|
||
}
|
||
}
|
||
}
|
||
|
||
// 如果没有通过小程序前缀获取到配置,则回退到商户前缀
|
||
if (empty($wxpayConfig) && !empty($merchant_prefix)) {
|
||
$wxpayConfig = WxPayHelper::getFixedWxPayConfig($merchant_prefix);
|
||
}
|
||
|
||
// 如果前两种方式都没有获取到配置,则使用默认配置
|
||
if (empty($wxpayConfig)) {
|
||
$wxpayConfig = WxPayHelper::getWxPayConfig();
|
||
}
|
||
|
||
$appid = $wxpayConfig['appid'];
|
||
$merchant = $wxpayConfig['merchant']['mch_id'];
|
||
|
||
$params['appid'] = $appid;
|
||
$params['mch_id'] = $merchant;
|
||
$params['nonce_str'] = $this->genRandomString();
|
||
$params['out_trade_no'] = $info['send_num'];
|
||
$params['out_refund_no'] = $this->genRandomString(64);
|
||
$params['total_fee'] = round($info['freight'] * 100, 2);
|
||
$params['refund_fee'] = round($info['freight'] * 100, 2);
|
||
$params['sign'] = $this->MakeSign($params, $wxpayConfig['merchant']['keys']);
|
||
$xml = $this->data_to_xml($params);
|
||
$url = 'https://api.mch.weixin.qq.com/secapi/pay/refund';
|
||
$tui_info = $this->postXmlCurl($xml, $url, $wxpayConfig['merchant']);
|
||
$result = $this->xml_to_data($tui_info);
|
||
if ($result['return_code'] === 'SUCCESS' && $result['result_code'] === 'SUCCESS') {
|
||
return ['status' => 1, 'msg' => $result];
|
||
} else {
|
||
return ['status' => 0, 'msg' => $result['err_code_des']];
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* 生成签名
|
||
* @param array $params 签名参数
|
||
* @param string $secretKey 可选,指定密钥,不指定则通过WxPayHelper获取
|
||
* @return string 签名
|
||
*/
|
||
public function MakeSign($params, $secretKey = null)
|
||
{
|
||
// 如果未提供密钥,则使用WxPayHelper获取随机商户配置
|
||
if (empty($secretKey)) {
|
||
$wxpayConfig = WxPayHelper::getWxPayConfig();
|
||
$secretKey = $wxpayConfig['merchant']['keys'];
|
||
}
|
||
|
||
//签名步骤一:按字典序排序数组参数
|
||
ksort($params);
|
||
$string = $this->ToUrlParams($params);
|
||
//签名步骤二:在string后加入KEY
|
||
$string = $string . "&key=" . $secretKey;
|
||
//签名步骤三:MD5加密
|
||
$string = md5($string);
|
||
//签名步骤四:所有字符转为大写
|
||
$result = strtoupper($string);
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* 将参数拼接为url: key=value&key=value
|
||
* @param $params
|
||
* @return string
|
||
*/
|
||
public function ToUrlParams($params)
|
||
{
|
||
$string = '';
|
||
if (!empty($params)) {
|
||
$array = array();
|
||
foreach ($params as $key => $value) {
|
||
$array[] = $key . '=' . $value;
|
||
}
|
||
$string = implode("&", $array);
|
||
}
|
||
return $string;
|
||
}
|
||
|
||
/**
|
||
* 输出xml字符
|
||
* @param $params 参数名称
|
||
* return string 返回组装的xml
|
||
**/
|
||
public function data_to_xml($params)
|
||
{
|
||
if (!is_array($params) || 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;
|
||
}
|
||
|
||
/**
|
||
* 将xml转为array
|
||
* @param string $xml
|
||
* return array
|
||
*/
|
||
public function xml_to_data($xml)
|
||
{
|
||
if (!$xml) {
|
||
return false;
|
||
}
|
||
//将XML转为array
|
||
//禁止引用外部xml实体
|
||
libxml_disable_entity_loader(true);
|
||
$data = json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);
|
||
return $data;
|
||
}
|
||
|
||
/**
|
||
* 产生一个指定长度的随机字符串,并返回给用户
|
||
* @param type $len 产生字符串的长度
|
||
* @return string 随机字符串
|
||
*/
|
||
private function genRandomString($len = 32)
|
||
{
|
||
$chars = array(
|
||
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k",
|
||
"l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v",
|
||
"w", "x", "y", "z", "A", "B", "C", "D", "E", "F", "G",
|
||
"H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R",
|
||
"S", "T", "U", "V", "W", "X", "Y", "Z", "0", "1", "2",
|
||
"3", "4", "5", "6", "7", "8", "9"
|
||
);
|
||
$charsLen = count($chars) - 1;
|
||
// 将数组打乱
|
||
shuffle($chars);
|
||
$output = "";
|
||
for ($i = 0; $i < $len; $i++) {
|
||
$output .= $chars[mt_rand(0, $charsLen)];
|
||
}
|
||
return $output;
|
||
}
|
||
|
||
/**
|
||
* 以post方式提交xml到对应的接口url
|
||
*
|
||
* @param string $xml 需要post的xml数据
|
||
* @param string $url url
|
||
* @param array|null $merchant 商户配置,如果不传则获取随机商户
|
||
* @param int $second url执行超时时间,默认30s
|
||
* @throws WxPayException
|
||
*/
|
||
private function postXmlCurl($xml, $url, $merchant = null, $second = 30)
|
||
{
|
||
$path = app()->getRootPath();
|
||
|
||
// 如果未指定商户配置,则使用随机商户配置
|
||
if (empty($merchant)) {
|
||
$wxpayConfig = WxPayHelper::getWxPayConfig();
|
||
$merchant = $wxpayConfig['merchant'];
|
||
}
|
||
|
||
// 优先使用商户配置中的证书路径,如果未设置则使用按商户ID查找
|
||
$ssl_cert = isset($merchant['ssl_cert']) && !empty($merchant['ssl_cert'])
|
||
? $merchant['ssl_cert']
|
||
: '';
|
||
|
||
$ssl_key = isset($merchant['ssl_key']) && !empty($merchant['ssl_key'])
|
||
? $merchant['ssl_key']
|
||
: '';
|
||
|
||
// 如果没有指定证书路径,尝试按商户ID查找
|
||
if (empty($ssl_cert) || empty($ssl_key)) {
|
||
$mch_id = isset($merchant['mch_id']) ? $merchant['mch_id'] : '';
|
||
|
||
if (!empty($mch_id)) {
|
||
// 按商户ID构建证书路径
|
||
$merchant_cert_dir = $path . 'app/common/ssl/' . $mch_id . '/';
|
||
|
||
// 检查商户证书目录是否存在
|
||
if (is_dir($merchant_cert_dir)) {
|
||
// 如果ssl_cert未指定且商户目录下存在证书文件,则使用该路径
|
||
if (empty($ssl_cert) && file_exists($merchant_cert_dir . 'apiclient_cert.pem')) {
|
||
$ssl_cert = $merchant_cert_dir . 'apiclient_cert.pem';
|
||
}
|
||
|
||
// 如果ssl_key未指定且商户目录下存在密钥文件,则使用该路径
|
||
if (empty($ssl_key) && file_exists($merchant_cert_dir . 'apiclient_key.pem')) {
|
||
$ssl_key = $merchant_cert_dir . 'apiclient_key.pem';
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 如果商户目录下没有找到证书,则使用默认路径
|
||
if (empty($ssl_cert)) {
|
||
$ssl_cert = $path . 'app/common/ssl/apiclient_cert.pem';
|
||
}
|
||
|
||
if (empty($ssl_key)) {
|
||
$ssl_key = $path . 'app/common/ssl/apiclient_key.pem';
|
||
}
|
||
|
||
$ch = curl_init();
|
||
//设置超时
|
||
curl_setopt($ch, CURLOPT_TIMEOUT, $second);
|
||
curl_setopt($ch, CURLOPT_URL, $url);
|
||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
|
||
curl_setopt($ch, CURLOPT_SSLCERTTYPE, 'pem');
|
||
curl_setopt($ch, CURLOPT_SSLCERT, $ssl_cert);
|
||
curl_setopt($ch, CURLOPT_SSLCERTTYPE, 'pem');
|
||
curl_setopt($ch, CURLOPT_SSLKEY, $ssl_key);
|
||
//设置header
|
||
curl_setopt($ch, CURLOPT_HEADER, FALSE);
|
||
//要求结果为字符串且输出到屏幕上
|
||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
|
||
//post提交方式
|
||
curl_setopt($ch, CURLOPT_POST, TRUE);
|
||
curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
|
||
//运行curl
|
||
$data = curl_exec($ch);
|
||
//返回结果
|
||
if ($data) {
|
||
curl_close($ch);
|
||
return $data;
|
||
} else {
|
||
$error = curl_errno($ch);
|
||
curl_close($ch);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 错误代码
|
||
* @param $code 服务器输出的错误代码
|
||
* return string
|
||
*/
|
||
public function error_code($code)
|
||
{
|
||
$errList = array(
|
||
'NOAUTH' => '商户未开通此接口权限',
|
||
'NOTENOUGH' => '用户帐号余额不足',
|
||
'ORDERNOTEXIST' => '订单号不存在',
|
||
'ORDERPAID' => '商户订单已支付,无需重复操作',
|
||
'ORDERCLOSED' => '当前订单已关闭,无法支付',
|
||
'SYSTEMERROR' => '系统错误!系统超时',
|
||
'APPID_NOT_EXIST' => '参数中缺少APPID',
|
||
'MCHID_NOT_EXIST' => '参数中缺少MCHID',
|
||
'APPID_MCHID_NOT_MATCH' => 'appid和mch_id不匹配',
|
||
'LACK_PARAMS' => '缺少必要的请求参数',
|
||
'OUT_TRADE_NO_USED' => '同一笔交易不能多次提交',
|
||
'SIGNERROR' => '参数签名结果不正确',
|
||
'XML_FORMAT_ERROR' => 'XML格式错误',
|
||
'REQUIRE_POST_METHOD' => '未使用post传递参数 ',
|
||
'POST_DATA_EMPTY' => 'post数据不能为空',
|
||
'NOT_UTF8' => '未使用指定编码格式',
|
||
);
|
||
if (array_key_exists($code, $errList)) {
|
||
return $errList[$code];
|
||
}
|
||
}
|
||
|
||
function get_client_ip()
|
||
{
|
||
if (isset($_SERVER['REMOTE_ADDR'])) {
|
||
$cip = $_SERVER['REMOTE_ADDR'];
|
||
} elseif (getenv("REMOTE_ADDR")) {
|
||
$cip = getenv("REMOTE_ADDR");
|
||
} elseif (getenv("HTTP_CLIENT_IP")) {
|
||
$cip = getenv("HTTP_CLIENT_IP");
|
||
} else {
|
||
$cip = "127.0.0.1";
|
||
}
|
||
return $cip;
|
||
}
|
||
|
||
} |