HaniBlindBox/server/php/app/common/server/WechatRefund.php
2026-01-01 20:46:07 +08:00

330 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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;
}
}