Files
charge-pile-serve/app/controller/WechatReimburse.php
MeSHard 94f7e83679 init
2025-11-10 16:12:07 +08:00

615 lines
26 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\controller;
use think\facade\Db;
use think\facade\Log;
class WechatReimburse
{
//微信支付
const KEY_LENGTH_BYTE = 32;
const AUTH_TAG_LENGTH_BYTE = 16;
//用户余额退款
public function Refund($transaction_id, $openid, $refundAmount, $OriginalOrderAmount)
{ //商户订单号,微信生成的退款订单号 二选一即可
$file = file_get_contents(__DIR__ . config('wx.apiclientKey'));
$mch_private_key = openssl_get_privatekey($file);
$time = time();
$out_refund_no = $this->generate_tuikuan(6);
$refundData = [
'transaction_id' => $transaction_id,
'out_refund_no' => $out_refund_no,
'reason' => '余额退款',
'notify_url' => 'https://' . $_SERVER['HTTP_HOST'] . '/refund_notify',
'funds_account' => 'AVAILABLE',
'amount' => [
'refund' => floor($refundAmount * 100), //退款标价金额,单位为分,可以做部分退款
'total' => $OriginalOrderAmount * 100, //订单总金额,单位为分
'currency' => 'CNY'
]
];
// if(!$transaction_id){ //商户订单号,微信生成的退款订单号 二选一即可
// if(!$out_trade_no){
// return ['code'=>0,'msg'=>'退款订单号不能为空'];
// }else{
// $refundData['out_trade_no']=$out_trade_no;
// }
// }else{
// $refundData['transaction_id']=$transaction_id;
// }
$url = 'https://api.mch.weixin.qq.com/v3/refund/domestic/refunds';
$url_parts = parse_url($url); //拆解为:[scheme=>https,host=>api.mch.weixin.qq.com,path=>/v3/pay/transactions/native]
$mchid = config('wx.merchantId');//商户ID
$xlid = config('wx.merchantSerialNumber');//证书序列号
$refundData = json_encode($refundData);
$nonce = date('YmdHis', time()) . rand(1000, 9999);
$canonical_url = ($url_parts['path'] . (!empty($url_parts['query']) ? "?${url_parts['query']}" : ""));
// $key = $this->getSign($refundData,$urlarr['path'],$nonce,$time);
$message = 'POST' . "\n" .
$canonical_url . "\n" .
$time . "\n" .
$nonce . "\n" .
$refundData . "\n";
openssl_sign($message, $signature, $mch_private_key, "sha256WithRSAEncryption");
$sign = base64_encode($signature);
$schema = 'WECHATPAY2-SHA256-RSA2048';
$token = sprintf('mchid="%s",serial_no="%s",nonce_str="%s",timestamp="%d",signature="%s"', $mchid, $xlid, $nonce, $time, $sign);
$header = "Authorization: " . $schema . " " . $token;
// $header = array(
// 'Accept: application/json',
// 'Content-Type: application/json',
// 'User-Agent:*/*',
// 'Authorization: WECHATPAY2-SHA256-RSA2048 '.$token
// );
$res = $this->http_post($url, $header, $refundData);
Db::table('charge_logo')->save(['name' => '余额提现', 'mark' => $res]);
$res_array = json_decode($res, true);
// dd($res_array);
if (isset($res_array['status']) && ($res_array['status'] == 'PROCESSING' || $res_array['status'] == 'SUCCESS')) {
$kk['openid'] = $openid;
$create_time = $res_array['create_time'];
$create_time = str_replace('T', ' ', $create_time);
$create_time = substr($create_time, 0, -6);
$kk = [
'openid' => $openid,
'create_time' => $create_time,
'out_trade_no' => $res_array['out_trade_no'],
'out_refund_no' => $res_array['out_refund_no'],
'refund_id' => $res_array['refund_id'],
'status' => $res_array['status'],
'transaction_id' => $res_array['transaction_id'],
'user_received_account' => $res_array['user_received_account'],
'refund_total' => $res_array['amount']['refund'],
];
$table = 'zxc_refund';
$table_recharge = 'zxc_recharge';
$table_user = 'zxc_user';
Db::transaction(function () use ($table_recharge, $res_array, $openid, $table_user, $kk, $table) {
Db::table($table)->save($kk);
Db::table($table_user)->where('openid', $openid)
->update([
'account' => Db::raw('account-' . ($res_array['amount']['refund'] / 100)),
]);
Db::table($table_recharge)->where('out_trade_no', $res_array['out_trade_no'])->update(['total_used' => Db::raw('total_used+' . ($res_array['amount']['refund']))]);
});
return ['code' => 200, 'msg' => '退款受理成功'];
} else {
return ['code' => 1, 'msg' => $res_array['message']];
}
}
//退款回调地址
public function refund_notify()
{
try {
//code...
$header = $this->getHeaders(); //读取http头信息 见下文
$body = file_get_contents('php://input'); //读取微信传过来的信息是一个json字符串
if (empty($header) || empty($body)) {
throw new \Exception('通知参数为空', 2001);
}
$timestamp = $header['WECHATPAY-TIMESTAMP'];
$nonce = $header['WECHATPAY-NONCE'];
$signature = $header['WECHATPAY-SIGNATURE'];
$serialNo = $header['WECHATPAY-SERIAL'];
if (empty($timestamp) || empty($nonce) || empty($signature) || empty($serialNo)) {
throw new \Exception('通知头参数为空', 2002);
}
$cert = $this->getzhengshuDb(1);
if ($cert != $serialNo) {
throw new \Exception('验签失败', 2005);
}
$message = "$timestamp\n$nonce\n$body\n";
//校验签名
if (!$this->verify($message, $signature, __DIR__ . config('wx.pingtai_public_key_path'))) {
throw new \Exception('验签失败', 2005);
}
$decodeBody = json_decode($body, true);
if (empty($decodeBody) || !isset($decodeBody['resource'])) {
throw new \Exception('通知参数内容为空', 2003);
}
$decodeBodyResource = $decodeBody['resource'];
$decodeData_res = $this->decryptToString($decodeBodyResource['associated_data'], $decodeBodyResource['nonce'], $decodeBodyResource['ciphertext'], ''); //解密resource
$decodeData = json_decode($decodeData_res, true);
Db::table('charge_logo')->save(['name' => '余额提现回调', 'mark' => $decodeData]);
Log::error('余额提现: ' . $decodeData);
//返回结果格式
//array (
// 'mchid' => 'xxx',
// 'appid' => 'xxxxxxx',
// 'out_trade_no' => '1217752501201407033233368026',
// 'transaction_id' => '4200001336202201037507057791',
// 'trade_type' => 'NATIVE',
// 'trade_state' => 'SUCCESS',
// 'trade_state_desc' => '支付成功',
// 'bank_type' => 'OTHERS',
// 'attach' => '',
// 'success_time' => '2022-01-03T19:43:05+08:00',
// 'payer' =>
// array (
// 'openid' => 'ovs326bgwfA4o8jlFQXMEma2JZek',
// ),
// 'amount' =>
// array (
// 'total' => 1,
// 'payer_total' => 1,
// 'currency' => 'CNY',
// 'payer_currency' => 'CNY',
// ),
// )
//执行自己的代码start
$out_refund_no = $decodeData['out_refund_no'];
$openid = Db::table('zxc_refund')->where('out_refund_no', $out_refund_no)->value('openid');
$table = 'zxc_refund';
$table_user = 'zxc_user';
$kk['status'] = $decodeData['refund_status'];
if ($decodeData['refund_status'] == 'SUCCESS') {
$success_time = $decodeData['success_time'];
$success_time = str_replace('T', ' ', $success_time);
$success_time = substr($success_time, 0, -6);
$kk['success_time'] = $success_time;
Db::table($table_user)->where('openid', $openid)->update(['FrozenAccount' => Db::raw('FrozenAccount-' . ($decodeData['amount']['refund'] / 100))]);
}
Db::table($table)->where('out_refund_no', $out_refund_no)->save($kk);
\app\model\User::addMoneyLog($openid, $decodeData['amount']['refund'], 3, '用户提现');
// $order_info = Db::table($table)->save($data);
// Db::table($table_user)->where('openid',$openid)->update(['account' => Db::raw('account+'.($total/100))]);
//执行自己的代码end
$arr = array("code" => "SUCCESS", "message" => "");
echo json_encode($arr);
} catch (\Exception $e) {
Log::error($e->getMessage());
$arr = array("code" => "ERROR", "message" => $e->getMessage());
echo json_encode($arr);
}
// $notifiedData = file_get_contents('php://input');
// $data = json_decode($notifiedData, true);
// $nonceStr = $data['resource']['nonce'];
// $associatedData = $data['resource']['associated_data'];
// $ciphertext = $data['resource']['ciphertext'];
// $ciphertext = base64_decode($ciphertext);
// //php>7.1,为了使用这个扩展你必须将extension=php_sodium.dll添加到php.ini
// if (function_exists('\sodium_crypto_aead_aes256gcm_is_available') && \sodium_crypto_aead_aes256gcm_is_available()) {
// //$APIv3_KEY就是在商户平台后端设置是APIv3秘钥
// $orderData = \sodium_crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $this->config['apiv3_private_key']);
// $orderData = json_decode($orderData, true);
// if ($orderData['refund_status']=='SUCCESS'){
// $transaction_id=$orderData['transaction_id']; //退款单号
//
// /*业务处理*/
// return json(['code'=>'SUCCESS','message'=>'成功']);
// }
// }
}
// 预充电余额退款
public function Refund2($charge_id, $openid, $refundAmount, $OriginalOrderAmount, $out_trade_no)
{ //商户订单号,微信生成的退款订单号 二选一即可
$file = file_get_contents(__DIR__ . config('wx.apiclientKey'));
$mch_private_key = openssl_get_privatekey($file);
$time = time();
$out_refund_no = $this->generate_tuikuan(6);
$refundData = [
'out_trade_no' => $out_trade_no,
'out_refund_no' => $out_refund_no,
'reason' => '预充电余额退款',
'notify_url' => 'https://' . $_SERVER['HTTP_HOST'] . '/refund_notify2',
'funds_account' => 'AVAILABLE',
'amount' => [
'refund' => floor($refundAmount * 100), //退款标价金额,单位为分,可以做部分退款
'total' => $OriginalOrderAmount * 100, //订单总金额,单位为分
'currency' => 'CNY'
]
];
$url = 'https://api.mch.weixin.qq.com/v3/refund/domestic/refunds';
$url_parts = parse_url($url); //拆解为:[scheme=>https,host=>api.mch.weixin.qq.com,path=>/v3/pay/transactions/native]
$mchid = config('wx.merchantId');//商户ID
$xlid = config('wx.merchantSerialNumber');//证书序列号
$refundData = json_encode($refundData);
$nonce = date('YmdHis', time()) . rand(1000, 9999);
$canonical_url = ($url_parts['path'] . (!empty($url_parts['query']) ? "?${url_parts['query']}" : ""));
// $key = $this->getSign($refundData,$urlarr['path'],$nonce,$time);
$message = 'POST' . "\n" .
$canonical_url . "\n" .
$time . "\n" .
$nonce . "\n" .
$refundData . "\n";
openssl_sign($message, $signature, $mch_private_key, "sha256WithRSAEncryption");
$sign = base64_encode($signature);
$schema = 'WECHATPAY2-SHA256-RSA2048';
$token = sprintf('mchid="%s",serial_no="%s",nonce_str="%s",timestamp="%d",signature="%s"', $mchid, $xlid, $nonce, $time, $sign);
$header = "Authorization: " . $schema . " " . $token;
$res = $this->http_post($url, $header, $refundData);
$res_array = json_decode($res, true);
if (isset($res_array['status']) && ($res_array['status'] == 'PROCESSING' || $res_array['status'] == 'SUCCESS')) {
$info = Db::table('zxc_charge_order')
->where('order_id', $charge_id)->where('is_wind', 0)->find();
if ($info) {
Db::table('zxc_charge_order')
->where('order_id', $charge_id)
->update([
'directly_refund_no' => $out_refund_no,
'directly_refund_status' => 1,
'directly_refund_amount' => $res_array['amount']['refund'],
'directly_refund_time' => time(),
'is_wind' => 1
]);
$create_time = $res_array['create_time'];
$create_time = str_replace('T', ' ', $create_time);
$create_time = substr($create_time, 0, -6);
Db::table('zxc_refund')
->save([
'openid' => $openid,
'create_time' => $create_time,
'out_trade_no' => $res_array['out_trade_no'],
'out_refund_no' => $res_array['out_refund_no'],
'refund_id' => $res_array['refund_id'],
'status' => $res_array['status'],
'transaction_id' => $res_array['transaction_id'],
'user_received_account' => $res_array['user_received_account'],
'refund_total' => $res_array['amount']['refund'],
]);
\app\model\User::addMoneyLog($openid, $res_array['amount']['refund']/100, 3, '用户使用即充即退-退款');
}
return ['code' => 200, 'msg' => '退款受理成功'];
} else {
return ['code' => 1, 'msg' => $res_array['message']];
}
}
//退款回调地址
public function refund_notify2()
{
try {
//code...
$header = $this->getHeaders(); //读取http头信息 见下文
$body = file_get_contents('php://input'); //读取微信传过来的信息是一个json字符串
if (empty($header) || empty($body)) {
throw new \Exception('通知参数为空', 2001);
}
$timestamp = $header['WECHATPAY-TIMESTAMP'];
$nonce = $header['WECHATPAY-NONCE'];
$signature = $header['WECHATPAY-SIGNATURE'];
$serialNo = $header['WECHATPAY-SERIAL'];
if (empty($timestamp) || empty($nonce) || empty($signature) || empty($serialNo)) {
throw new \Exception('通知头参数为空', 2002);
}
$cert = $this->getzhengshuDb(1);
if ($cert != $serialNo) {
throw new \Exception('验签失败', 2005);
}
$message = "$timestamp\n$nonce\n$body\n";
//校验签名
if (!$this->verify($message, $signature, __DIR__ . config('wx.pingtai_public_key_path'))) { //$this->pingtai_public_key_path是获取平台证书序列号$this->getzhengshuDb()时保存下来的平台公钥文件
throw new \Exception('验签失败', 2005);
}
$decodeBody = json_decode($body, true);
if (empty($decodeBody) || !isset($decodeBody['resource'])) {
throw new \Exception('通知参数内容为空', 2003);
}
$decodeBodyResource = $decodeBody['resource'];
$decodeData_res = $this->decryptToString($decodeBodyResource['associated_data'], $decodeBodyResource['nonce'], $decodeBodyResource['ciphertext'], ''); //解密resource
$decodeData = json_decode($decodeData_res, true);
Log::error('用户使用即充即退退款: ' . $decodeData_res);
//返回结果格式
//array (
// 'mchid' => 'xxx',
// 'appid' => 'xxxxxxx',
// 'out_trade_no' => '1217752501201407033233368026',
// 'transaction_id' => '4200001336202201037507057791',
// 'trade_type' => 'NATIVE',
// 'trade_state' => 'SUCCESS',
// 'trade_state_desc' => '支付成功',
// 'bank_type' => 'OTHERS',
// 'attach' => '',
// 'success_time' => '2022-01-03T19:43:05+08:00',
// 'payer' =>
// array (
// 'openid' => 'ovs326bgwfA4o8jlFQXMEma2JZek',
// ),
// 'amount' =>
// array (
// 'total' => 1,
// 'payer_total' => 1,
// 'currency' => 'CNY',
// 'payer_currency' => 'CNY',
// ),
// )
//执行自己的代码start
$out_refund_no = $decodeData['out_refund_no'];
$arr = array("code" => "SUCCESS", "message" => "");
echo json_encode($arr);
} catch (\Exception $e) {
Log::error($e->getMessage());
$arr = array("code" => "ERROR", "message" => $e->getMessage());
echo json_encode($arr);
}
}
private function verify($message, $signature, $merchantPublicKey)
{
if (!in_array('sha256WithRSAEncryption', \openssl_get_md_methods(true))) {
throw new \RuntimeException("当前PHP环境不支持SHA256withRSA");
}
$signature = base64_decode($signature);
$a = openssl_verify($message, $signature, $this->getWxPublicKey($merchantPublicKey), 'sha256WithRSAEncryption');
return $a;
}
public function getNonceStr()
{
$strs = "QWERTYUIOPASDFGHJKLZXCVBNM1234567890";
$name = substr(str_shuffle($strs), mt_rand(0, strlen($strs) - 11), 32);
return $name;
}
private function getzhengshuDb($getNew = 0)
{
if ($getNew !== 1) {
dump(file_get_contents(__DIR__ . config('wx.pingtai_public_key_path')));
}
$url = "https://api.mch.weixin.qq.com/v3/certificates";
$timestamp = time(); //时间戳
$nonce = $this->nonce_str(); //获取一个随机数
$body = "";
$mch_private_key = $this->getPrivateKey(); //读取商户api证书私钥
$merchant_id = config('wx.merchantId'); //服务商商户号
$serial_no = config('wx.merchantSerialNumber'); //在API安全中获取
$sign = $this->sign($url, 'GET', $timestamp, $nonce, $body, $mch_private_key, $merchant_id, $serial_no); //签名
$header = [
'Authorization:WECHATPAY2-SHA256-RSA2048 ' . $sign,
'Accept:application/json',
'User-Agent:' . $merchant_id
];
$result = $this->curl($url, '', $header, 'GET');
$result = json_decode($result, true);
$serial_no = $result['data'][0]['serial_no'];
file_put_contents(__DIR__ . '/../../Secret/tuikuan/serial_no.txt', $serial_no);
$encrypt_certificate = $result['data'][0]['encrypt_certificate'];
$sign_key = config('wx.apiV3key'); //在API安全中设置
$result = $this->decryptToString($encrypt_certificate['associated_data'], $encrypt_certificate['nonce'], $encrypt_certificate['ciphertext'], $sign_key); //解密
file_put_contents(__DIR__ . config('wx.pingtai_public_key_path'), $result);
return $serial_no;
}
private function getHeaders()
{
$header = array();
foreach ($_SERVER as $key => $value) {
if ('HTTP_' == substr($key, 0, 5)) {
$header[str_replace('_', '-', substr($key, 5))] = $value;
}
if (isset($_SERVER['PHP_AUTH_DIGEST'])) {
$header['AUTHORIZATION'] = $_SERVER['PHP_AUTH_DIGEST'];
} elseif (isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) {
$header['AUTHORIZATION'] = base64_encode($_SERVER['PHP_AUTH_USER'] . ':' . $_SERVER['PHP_AUTH_PW']);
}
if (isset($_SERVER['CONTENT_LENGTH'])) {
$header['CONTENT-LENGTH'] = $_SERVER['CONTENT_LENGTH'];
}
if (isset($_SERVER['CONTENT_TYPE'])) {
$header['CONTENT-TYPE'] = $_SERVER['CONTENT_TYPE'];
}
}
return $header;
}
private function decryptToString($associatedData, $nonceStr, $ciphertext, $aesKey = '')
{
if (empty($aesKey)) {
$aesKey = config('wx.apiV3key'); //微信商户平台 api安全中设置获取
}
$ciphertext = \base64_decode($ciphertext);
if (strlen($ciphertext) <= self::AUTH_TAG_LENGTH_BYTE) {
return false;
}
// ext-sodium (default installed on >= PHP 7.2)
if (
function_exists('\sodium_crypto_aead_aes256gcm_is_available') && \sodium_crypto_aead_aes256gcm_is_available()
) {
return \sodium_crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $aesKey);
}
// ext-libsodium (need install libsodium-php 1.x via pecl)
if (
function_exists('\Sodium\crypto_aead_aes256gcm_is_available') && \Sodium\crypto_aead_aes256gcm_is_available()
) {
return \Sodium\crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $aesKey);
}
// openssl (PHP >= 7.1 support AEAD)
if (PHP_VERSION_ID >= 70100 && in_array('aes-256-gcm', \openssl_get_cipher_methods())) {
$ctext = substr($ciphertext, 0, -self::AUTH_TAG_LENGTH_BYTE);
$authTag = substr($ciphertext, -self::AUTH_TAG_LENGTH_BYTE);
return \openssl_decrypt(
$ctext,
'aes-256-gcm',
$aesKey,
\OPENSSL_RAW_DATA,
$nonceStr,
$authTag,
$associatedData
);
}
throw new \RuntimeException('AEAD_AES_256_GCM需要PHP 7.1以上或者安装libsodium-php');
}
private function http_post($url, $header, $data)
{
$headers[] = "Accept:application/json";
$headers[] = "Content-Type:application/json";
$headers[] = "User-Agent:application/json";
$headers[] = $header;
$curl = curl_init(); // 启动一个CURL会话
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_HEADER, 0);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_POST, 1);
curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); // 跳过证书检查
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false); // 从证书中检查SSL加密算法是否存在
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
$tmpInfo = curl_exec($curl);
//关闭URL请求
curl_close($curl);
return $tmpInfo;
}
//生成随机字符串
public function nonce_str($length = 32)
{
$chars = "abcdefghijklmnopqrstuvwxyz0123456789";
$str = "";
for ($i = 0; $i < $length; $i++) {
$str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
}
return $str;
}
//读取商户api证书私钥
public function getPrivateKey()
{
return openssl_get_privatekey(file_get_contents(__DIR__ . config('wx.apiclientKey'))); //微信商户平台中下载下来,保存到服务器直接读取
}
//签名
public function sign($url, $http_method, $timestamp, $nonce, $body, $mch_private_key, $merchant_id, $serial_no)
{
$url_parts = parse_url($url);
$canonical_url = ($url_parts['path'] . (!empty($url_parts['query']) ? "?${url_parts['query']}" : ""));
$message =
$http_method . "\n" .
$canonical_url . "\n" .
$timestamp . "\n" .
$nonce . "\n" .
$body . "\n";
openssl_sign($message, $raw_sign, $mch_private_key, 'sha256WithRSAEncryption');
$sign = base64_encode($raw_sign);
$schema = 'WECHATPAY2-SHA256-RSA2048';
$token = sprintf(
'mchid="%s",nonce_str="%s",signature="%s",timestamp="%d",serial_no="%s"',
$merchant_id,
$nonce,
$sign,
$timestamp,
$serial_no
);
return $token;
}
//curl提交
public function curl($url, $data = [], $header, $method = 'POST')
{
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
curl_setopt($curl, CURLOPT_HEADER, false);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
if ($method == "POST") {
curl_setopt($curl, CURLOPT_POST, TRUE);
curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
}
$result = curl_exec($curl);
curl_close($curl);
return $result;
}
protected function generate_tuikuan($length)
{
$chars = '0123456789';
$time = time();
$password = 'DZZS' . $time . 'TK';
for ($i = 0; $i < $length; $i++) {
$password .= $chars[mt_rand(0, strlen($chars) - 1)];
}
return $password;
}
protected function getWxPublicKey($key)
{
$public_content = file_get_contents($key);
$a = openssl_get_publickey($public_content);
return $a;
}
}