new files
This commit is contained in:
281
pro_v3.5.1/crmeb/services/wechat/v3pay/BaseClient.php
Normal file
281
pro_v3.5.1/crmeb/services/wechat/v3pay/BaseClient.php
Normal file
@@ -0,0 +1,281 @@
|
||||
<?php
|
||||
/**
|
||||
* +----------------------------------------------------------------------
|
||||
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
|
||||
* +----------------------------------------------------------------------
|
||||
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
|
||||
* +----------------------------------------------------------------------
|
||||
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
|
||||
* +----------------------------------------------------------------------
|
||||
* | Author: CRMEB Team <admin@crmeb.com>
|
||||
* +----------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
namespace crmeb\services\wechat\v3pay;
|
||||
|
||||
|
||||
use crmeb\exceptions\PayException;
|
||||
use crmeb\services\wechat\config\V3PaymentConfig;
|
||||
use crmeb\services\wechat\WechatException;
|
||||
use think\exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class BaseClient
|
||||
* @author 等风来
|
||||
* @email 136327134@qq.com
|
||||
* @date 2022/9/30
|
||||
* @package crmeb\services\wechat\v3pay
|
||||
*/
|
||||
class BaseClient
|
||||
{
|
||||
|
||||
use Certficates;
|
||||
|
||||
const BASE_URL = 'https://api.mch.weixin.qq.com/';
|
||||
|
||||
const AUTH_TAG_LENGTH_BYTE = 16;
|
||||
|
||||
/**
|
||||
* BaseClient constructor.
|
||||
* @param V3PaymentConfig $config
|
||||
*/
|
||||
public function __construct(protected V3PaymentConfig $config)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* request.
|
||||
*
|
||||
* @param string $endpoint
|
||||
* @param string $method
|
||||
* @param array $options
|
||||
* @param bool $serial
|
||||
* @return mixed
|
||||
* @throws \Psr\SimpleCache\InvalidArgumentException
|
||||
*/
|
||||
public function request(string $endpoint, string $method = 'POST', array $options = [], bool $serial = true)
|
||||
{
|
||||
$body = $options['body'] ?? '';
|
||||
|
||||
if (isset($options['json'])) {
|
||||
$body = json_encode($options['json']);
|
||||
$options['body'] = $body;
|
||||
unset($options['json']);
|
||||
}
|
||||
|
||||
$headers = [
|
||||
'Content-Type' => 'application/json',
|
||||
'User-Agent' => 'curl',
|
||||
'Accept' => 'application/json',
|
||||
'Authorization' => $this->getAuthorization($endpoint, $method, $body),
|
||||
];
|
||||
|
||||
$options['headers'] = array_merge($headers, ($options['headers'] ?? []));
|
||||
|
||||
if ($serial) {
|
||||
if ($this->config->publicKey != '') {
|
||||
$options['headers']['Wechatpay-Serial'] = $this->config->publicKey;
|
||||
} else {
|
||||
$options['headers']['Wechatpay-Serial'] = $this->getCertficatescAttr('serial_no');
|
||||
}
|
||||
}
|
||||
return $this->_doRequestCurl($method, self::BASE_URL . $endpoint, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $method
|
||||
* @param string $location
|
||||
* @param array $options
|
||||
* @return mixed
|
||||
*/
|
||||
private function _doRequestCurl(string $method, string $location, array $options = [])
|
||||
{
|
||||
$curl = curl_init();
|
||||
// POST数据设置
|
||||
if (strtolower($method) === 'post') {
|
||||
curl_setopt($curl, CURLOPT_POST, true);
|
||||
curl_setopt($curl, CURLOPT_POSTFIELDS, $options['data'] ?? $options['body'] ?? '');
|
||||
}
|
||||
// CURL头信息设置
|
||||
if (!empty($options['headers'])) {
|
||||
$headers = [];
|
||||
foreach ($options['headers'] as $k => $v) {
|
||||
$headers[] = "$k: $v";
|
||||
}
|
||||
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
|
||||
}
|
||||
curl_setopt($curl, CURLOPT_URL, $location);
|
||||
curl_setopt($curl, CURLOPT_HEADER, true);
|
||||
curl_setopt($curl, CURLOPT_TIMEOUT, 60);
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
|
||||
$content = curl_exec($curl);
|
||||
$headerSize = curl_getinfo($curl, CURLINFO_HEADER_SIZE);
|
||||
curl_close($curl);
|
||||
|
||||
return json_decode(substr($content, $headerSize), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* To id card, mobile phone number and other fields sensitive information encryption.
|
||||
*
|
||||
* @param string $string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function encryptSensitiveInformation(string $string)
|
||||
{
|
||||
$certificates = $this->config->certPath;
|
||||
if (null === $certificates) {
|
||||
throw new WechatException('config certificate connot be empty.');
|
||||
}
|
||||
$encrypted = '';
|
||||
if (openssl_public_encrypt($string, $encrypted, $certificates, OPENSSL_PKCS1_OAEP_PADDING)) {
|
||||
//base64编码
|
||||
$sign = base64_encode($encrypted);
|
||||
} else {
|
||||
throw new WechatException('Encryption of sensitive information failed');
|
||||
}
|
||||
return $sign;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @param string $method
|
||||
* @param string $body
|
||||
* @return string
|
||||
*/
|
||||
protected function getAuthorization(string $url, string $method, string $body)
|
||||
{
|
||||
$nonceStr = uniqid();
|
||||
$timestamp = time();
|
||||
$message = $method . "\n" .
|
||||
'/' . $url . "\n" .
|
||||
$timestamp . "\n" .
|
||||
$nonceStr . "\n" .
|
||||
$body . "\n";
|
||||
openssl_sign($message, $raw_sign, $this->getPrivateKey(), 'sha256WithRSAEncryption');
|
||||
$sign = base64_encode($raw_sign);
|
||||
$schema = 'WECHATPAY2-SHA256-RSA2048 ';
|
||||
$token = sprintf('mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"',
|
||||
$this->config->mchId, $nonceStr, $timestamp, $this->config->serialNo, $sign);
|
||||
return $schema . $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商户私钥
|
||||
* @return bool|resource
|
||||
*/
|
||||
protected function getPrivateKey()
|
||||
{
|
||||
$key_path = $this->config->keyPath;
|
||||
if (!file_exists($key_path)) {
|
||||
throw new \InvalidArgumentException(
|
||||
"SSL certificate not found: {$key_path}"
|
||||
);
|
||||
}
|
||||
return openssl_pkey_get_private(file_get_contents($key_path));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商户公钥
|
||||
* @return bool|resource
|
||||
*/
|
||||
protected function getPublicKey()
|
||||
{
|
||||
if ($this->config->publicKey != '') {
|
||||
$key_path = $this->config->publicPem;
|
||||
} else {
|
||||
$key_path = $this->config->certPath;
|
||||
}
|
||||
if (!file_exists($key_path)) {
|
||||
throw new \InvalidArgumentException(
|
||||
"SSL certificate not found: {$key_path}"
|
||||
);
|
||||
}
|
||||
return openssl_pkey_get_public(file_get_contents($key_path));
|
||||
}
|
||||
|
||||
/**
|
||||
* 替换url
|
||||
* @param string $url
|
||||
* @param $search
|
||||
* @param $replace
|
||||
* @return array|string|string[]
|
||||
*/
|
||||
public function getApiUrl(string $url, $search, $replace)
|
||||
{
|
||||
$newSearch = [];
|
||||
foreach ($search as $key) {
|
||||
$newSearch[] = '{' . $key . '}';
|
||||
}
|
||||
return str_replace($newSearch, $replace, $url);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $padding
|
||||
*/
|
||||
private static function paddingModeLimitedCheck(int $padding): void
|
||||
{
|
||||
if (!($padding === OPENSSL_PKCS1_OAEP_PADDING || $padding === OPENSSL_PKCS1_PADDING)) {
|
||||
throw new PayException(sprintf("Doesn't supported padding mode(%d), here only support OPENSSL_PKCS1_OAEP_PADDING or OPENSSL_PKCS1_PADDING.", $padding));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密数据
|
||||
* @param string $plaintext
|
||||
* @param int $padding
|
||||
* @return string
|
||||
*/
|
||||
public function encryptor(string $plaintext, int $padding = OPENSSL_PKCS1_OAEP_PADDING)
|
||||
{
|
||||
self::paddingModeLimitedCheck($padding);
|
||||
|
||||
if (!openssl_public_encrypt($plaintext, $encrypted, $this->getPublicKey(), $padding)) {
|
||||
throw new PayException('Encrypting the input $plaintext failed, please checking your $publicKey whether or nor correct.');
|
||||
}
|
||||
|
||||
return base64_encode($encrypted);
|
||||
}
|
||||
|
||||
/**
|
||||
* decrypt ciphertext.
|
||||
*
|
||||
* @param array $encryptCertificate
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function decrypt(array $encryptCertificate)
|
||||
{
|
||||
$ciphertext = base64_decode($encryptCertificate['ciphertext'], true);
|
||||
$associatedData = $encryptCertificate['associated_data'];
|
||||
$nonceStr = $encryptCertificate['nonce'];
|
||||
$aesKey = $this->config->key;
|
||||
|
||||
try {
|
||||
// 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);
|
||||
}
|
||||
} catch (\Exception $exception) {
|
||||
throw new InvalidArgumentException($exception->getMessage(), $exception->getCode());
|
||||
} catch (\SodiumException $exception) {
|
||||
throw new InvalidArgumentException($exception->getMessage(), $exception->getCode());
|
||||
}
|
||||
throw new InvalidArgumentException('AEAD_AES_256_GCM 需要 PHP 7.1 以上或者安装 libsodium-php');
|
||||
}
|
||||
}
|
||||
|
||||
71
pro_v3.5.1/crmeb/services/wechat/v3pay/Certficates.php
Normal file
71
pro_v3.5.1/crmeb/services/wechat/v3pay/Certficates.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
/**
|
||||
* +----------------------------------------------------------------------
|
||||
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
|
||||
* +----------------------------------------------------------------------
|
||||
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
|
||||
* +----------------------------------------------------------------------
|
||||
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
|
||||
* +----------------------------------------------------------------------
|
||||
* | Author: CRMEB Team <admin@crmeb.com>
|
||||
* +----------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
namespace crmeb\services\wechat\v3pay;
|
||||
|
||||
|
||||
use crmeb\exceptions\PayException;
|
||||
use crmeb\services\wechat\config\V3PaymentConfig;
|
||||
use Psr\SimpleCache\InvalidArgumentException;
|
||||
use think\facade\Cache;
|
||||
|
||||
/**
|
||||
* Class Certficates
|
||||
* @package crmeb\services\easywechat\v3pay
|
||||
* @property V3PaymentConfig $config
|
||||
*/
|
||||
trait Certficates
|
||||
{
|
||||
|
||||
/**
|
||||
* @param string|null $key
|
||||
* @return array|mixed|null
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function getCertficatescAttr(string $key = null)
|
||||
{
|
||||
$driver = Cache::store('file');
|
||||
$cacheKey = '_wx_v3' . $this->config->serialNo;
|
||||
if ($driver->has($cacheKey)) {
|
||||
$res = $driver->get($cacheKey);
|
||||
if ($key && $res) {
|
||||
return $res[$key] ?? null;
|
||||
} else {
|
||||
return $res;
|
||||
}
|
||||
}
|
||||
$certficates = $this->getCertficates();
|
||||
$driver->set($cacheKey, $certficates, 3600 * 24 * 30);
|
||||
if ($key && $certficates) {
|
||||
return $certficates[$key] ?? null;
|
||||
}
|
||||
return $certficates;
|
||||
}
|
||||
|
||||
/**
|
||||
* get certficates.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getCertficates()
|
||||
{
|
||||
$response = $this->request('v3/certificates', 'GET', [], false);
|
||||
if (isset($response['code'])) {
|
||||
throw new PayException($response['message']);
|
||||
}
|
||||
$certificates = $response['data'][0];
|
||||
$certificates['certificates'] = $this->decrypt($certificates['encrypt_certificate']);
|
||||
unset($certificates['encrypt_certificate']);
|
||||
return $certificates;
|
||||
}
|
||||
}
|
||||
526
pro_v3.5.1/crmeb/services/wechat/v3pay/PayClient.php
Normal file
526
pro_v3.5.1/crmeb/services/wechat/v3pay/PayClient.php
Normal file
@@ -0,0 +1,526 @@
|
||||
<?php
|
||||
/**
|
||||
* +----------------------------------------------------------------------
|
||||
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
|
||||
* +----------------------------------------------------------------------
|
||||
* | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
|
||||
* +----------------------------------------------------------------------
|
||||
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
|
||||
* +----------------------------------------------------------------------
|
||||
* | Author: CRMEB Team <admin@crmeb.com>
|
||||
* +----------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
namespace crmeb\services\wechat\v3pay;
|
||||
|
||||
|
||||
use crmeb\exceptions\PayException;
|
||||
use crmeb\services\wechat\config\V3PaymentConfig;
|
||||
use crmeb\services\wechat\Payment;
|
||||
use Psr\SimpleCache\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* v3支付
|
||||
* Class PayClient
|
||||
* @package crmeb\services\easywechat\v3pay
|
||||
*/
|
||||
class PayClient extends BaseClient
|
||||
{
|
||||
//app支付
|
||||
const API_APP_APY_URL = 'v3/pay/transactions/app';
|
||||
//二维码支付截图
|
||||
const API_NATIVE_URL = 'v3/pay/transactions/native';
|
||||
//h5支付接口
|
||||
const API_H5_URL = 'v3/pay/transactions/h5';
|
||||
//jsapi支付接口
|
||||
const API_JSAPI_URL = 'v3/pay/transactions/jsapi';
|
||||
//发起商家转账API
|
||||
const API_BATCHES_URL = 'v3/transfer/batches';
|
||||
//退款
|
||||
const API_REFUND_URL = 'v3/refund/domestic/refunds';
|
||||
//退款查询接口
|
||||
const API_REFUND_QUERY_URL = 'v3/refund/domestic/refunds/{out_refund_no}';
|
||||
//发起转账
|
||||
const API_TRANSFER_BILLS_URL = 'v3/fund-app/mch-transfer/transfer-bills';
|
||||
//撤销转账
|
||||
const API_TRANSFER_BILLS_CANCEL_URL = '/v3/fund-app/mch-transfer/transfer-bills/out-bill-no/{out_bill_no}/cancel';
|
||||
//查询转账
|
||||
const API_TRANSFER_QUERY_URL = 'v3/fund-app/mch-transfer/transfer-bills/out-bill-no/{out_bill_no}';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected string $type = Payment::WEB;
|
||||
protected string $notify_url = '';
|
||||
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @return $this
|
||||
* @author 等风来
|
||||
* @email 136327134@qq.com
|
||||
* @date 2022/11/18
|
||||
*/
|
||||
public function setType(string $type)
|
||||
{
|
||||
$this->type = $type;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 公众号jsapi支付下单
|
||||
* @param string $openid
|
||||
* @param string $outTradeNo
|
||||
* @param string $total
|
||||
* @param string $description
|
||||
* @param string $attach
|
||||
* @return mixed
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function jsapiPay(string $openid, string $outTradeNo, string $total, string $description, string $attach)
|
||||
{
|
||||
$appId = $this->config->wechatAppid;
|
||||
$res = $this->pay('jsapi', $appId, $outTradeNo, $total, $description, $attach, ['openid' => $openid]);
|
||||
return $this->configForJSSDKPayment($appId, $res['prepay_id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 小程序支付
|
||||
* @param string $openid
|
||||
* @param string $outTradeNo
|
||||
* @param string $total
|
||||
* @param string $description
|
||||
* @param string $attach
|
||||
* @return array|false|string
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function miniprogPay(string $openid, string $outTradeNo, string $total, string $description, string $attach)
|
||||
{
|
||||
$appId = $this->config->miniprogAppid;
|
||||
$res = $this->pay('jsapi', $appId, $outTradeNo, $total, $description, $attach, ['openid' => $openid]);
|
||||
return $this->configForJSSDKPayment($appId, $res['prepay_id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* APP支付下单
|
||||
* @param string $outTradeNo
|
||||
* @param string $total
|
||||
* @param string $description
|
||||
* @param string $attach
|
||||
* @return mixed
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function appPay(string $outTradeNo, string $total, string $description, string $attach)
|
||||
{
|
||||
$res = $this->pay('app', $this->config->appAppid, $outTradeNo, $total, $description, $attach);
|
||||
return $this->configForAppPayment($res['prepay_id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* native支付下单
|
||||
* @param string $outTradeNo
|
||||
* @param string $total
|
||||
* @param string $description
|
||||
* @param string $attach
|
||||
* @return mixed
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function nativePay(string $outTradeNo, string $total, string $description, string $attach)
|
||||
{
|
||||
return $this->pay('native', $this->config->wechatAppid, $outTradeNo, $total, $description, $attach);
|
||||
}
|
||||
|
||||
/**
|
||||
* h5支付下单
|
||||
* @param string $outTradeNo
|
||||
* @param string $total
|
||||
* @param string $description
|
||||
* @param string $attach
|
||||
* @return mixed
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function h5Pay(string $outTradeNo, string $total, string $description, string $attach)
|
||||
{
|
||||
$res = $this->pay('h5', $this->config->wechatAppid, $outTradeNo, $total, $description, $attach);
|
||||
return ['mweb_url' => $res['h5_url']];
|
||||
}
|
||||
|
||||
/**
|
||||
* 下单
|
||||
* @param string $type
|
||||
* @param string $appid
|
||||
* @param string $outTradeNo
|
||||
* @param string $total
|
||||
* @param string $description
|
||||
* @param string $attach
|
||||
* @param array $payer
|
||||
* @return mixed
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function pay(string $type, string $appid, string $outTradeNo, string $total, string $description, string $attach, array $payer = [])
|
||||
{
|
||||
$totalFee = (int)bcmul($total, '100');
|
||||
|
||||
$data = [
|
||||
'appid' => $appid,
|
||||
'mchid' => $this->config->mchId,
|
||||
'out_trade_no' => $outTradeNo,
|
||||
'attach' => $attach,
|
||||
'description' => $description,
|
||||
'notify_url' => $this->config->notifyUrl,
|
||||
'amount' => [
|
||||
'total' => $totalFee,
|
||||
'currency' => 'CNY'
|
||||
],
|
||||
];
|
||||
|
||||
if ($payer) {
|
||||
$data['payer'] = $payer;
|
||||
}
|
||||
|
||||
$url = '';
|
||||
switch ($type) {
|
||||
case 'h5':
|
||||
$url = self::API_H5_URL;
|
||||
$data['scene_info'] = [
|
||||
'payer_client_ip' => request()->ip(),
|
||||
'h5_info' => [
|
||||
'type' => 'Wap'
|
||||
]
|
||||
];
|
||||
break;
|
||||
case 'native':
|
||||
$url = self::API_NATIVE_URL;
|
||||
break;
|
||||
case 'app':
|
||||
$url = self::API_APP_APY_URL;
|
||||
break;
|
||||
case 'jsapi':
|
||||
$url = self::API_JSAPI_URL;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!$url) {
|
||||
throw new PayException('缺少请求地址');
|
||||
}
|
||||
|
||||
$res = $this->request($url, 'POST', ['json' => $data]);
|
||||
|
||||
if (!$res) {
|
||||
throw new PayException('微信支付:下单失败');
|
||||
}
|
||||
if (isset($res['code']) && isset($res['message'])) {
|
||||
throw new PayException($res['message']);
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起转账新接口(2025年1月15日升级)
|
||||
* @param string $outBatchNo
|
||||
* @param string $amount
|
||||
* @param string $userName
|
||||
* @param string $remark
|
||||
* @param array $transferDetailList
|
||||
* @return void
|
||||
* User: liusl
|
||||
* DateTime: 2025/2/14 上午11:20
|
||||
*/
|
||||
/**
|
||||
* @param string $outBatchNo
|
||||
* @param string $amount
|
||||
* @param $openid
|
||||
* @param string $userName
|
||||
* @param string $remark
|
||||
* @param string $perception
|
||||
* @param array $transferDetailList
|
||||
* @return mixed
|
||||
* @throws InvalidArgumentException
|
||||
* User: liusl
|
||||
* DateTime: 2025/2/14 下午2:14
|
||||
*/
|
||||
public function transferBills(string $outBatchNo, string $amount, $openid, string $userName, string $remark, string $perception, array $transferDetailList, string $transfer_scene_id = '1000')
|
||||
{
|
||||
$data = [
|
||||
'transfer_amount' => (int)bcmul($amount, 100, 0),
|
||||
'out_bill_no' => $outBatchNo,
|
||||
'transfer_scene_id' => $transfer_scene_id,
|
||||
'openid' => $openid,
|
||||
'transfer_remark' => $remark,
|
||||
'notify_url' => sys_config('site_url') . '/api/pay/mchNotify/'.$this->type,
|
||||
'user_recv_perception' => '现金奖励',
|
||||
'transfer_scene_report_infos' => $transferDetailList,
|
||||
];
|
||||
|
||||
if ($amount >= 2000) {
|
||||
if (empty($userName)) {
|
||||
throw new PayException('明细金额大于等于2000时,收款人姓名必须填写');
|
||||
}
|
||||
$data['user_name'] = $this->encryptor($userName);
|
||||
}
|
||||
|
||||
$appidMap = [
|
||||
Payment::WEB => $this->config->wechatAppid,
|
||||
Payment::MINI => $this->config->miniprogAppid,
|
||||
Payment::APP => $this->config->appAppid,
|
||||
];
|
||||
|
||||
if (!isset($appidMap[$this->type])) {
|
||||
throw new PayException('暂时只支持微信用户、小程序用户、APP微信登录用户提现');
|
||||
}
|
||||
|
||||
$data['appid'] = $appidMap[$this->type];
|
||||
$res = $this->request(self::API_TRANSFER_BILLS_URL, 'POST', ['json' => $data]);
|
||||
if (!$res || isset($res['code'], $res['message'])) {
|
||||
throw new PayException($res['message'] ?? '微信支付:发起商家转账失败');
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起商家转账API
|
||||
* @param string $outBatchNo
|
||||
* @param string $amount
|
||||
* @param string $batchName
|
||||
* @param string $remark
|
||||
* @param array $transferDetailList
|
||||
* @return mixed
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function batches(string $outBatchNo, string $amount, string $batchName, string $remark, array $transferDetailList)
|
||||
{
|
||||
$totalFee = '0';
|
||||
$amount = bcadd($amount, '0', 2);
|
||||
foreach ($transferDetailList as &$item) {
|
||||
if ($item['transfer_amount'] >= 2000 && !empty($item['user_name'])) {
|
||||
throw new PayException('明细金额大于等于2000时,收款人姓名必须填写');
|
||||
}
|
||||
$totalFee = bcadd($totalFee, $item['transfer_amount'], 2);
|
||||
$item['transfer_amount'] = (int)bcmul($item['transfer_amount'], 100, 0);
|
||||
if (isset($item['user_name'])) {
|
||||
$item['user_name'] = $this->encryptor($item['user_name']);
|
||||
}
|
||||
}
|
||||
|
||||
if ($totalFee !== $amount) {
|
||||
throw new PayException('转账明细金额总和和转账总金额不一致');
|
||||
}
|
||||
|
||||
$amount = (int)bcmul($amount, 100, 0);
|
||||
|
||||
$appid = null;
|
||||
if ($this->type === Payment::WEB) {
|
||||
$appid = $this->config->wechatAppid;
|
||||
} else if ($this->type === Payment::MINI) {
|
||||
$appid = $this->config->miniprogAppid;
|
||||
} else if ($this->type === Payment::APP) {
|
||||
$appid = $this->config->appAppid;
|
||||
}
|
||||
|
||||
if (!$appid) {
|
||||
throw new PayException('暂时只支持微信用户、小程序用户、APP微信登录用户提现');
|
||||
}
|
||||
|
||||
$data = [
|
||||
'appid' => $appid,
|
||||
'out_batch_no' => $outBatchNo,
|
||||
'batch_name' => $batchName,
|
||||
'batch_remark' => $remark,
|
||||
'total_amount' => $amount,
|
||||
'total_num' => count($transferDetailList),
|
||||
'transfer_detail_list' => $transferDetailList
|
||||
];
|
||||
|
||||
$res = $this->request(self::API_BATCHES_URL, 'POST', ['json' => $data]);
|
||||
|
||||
if (!$res) {
|
||||
throw new PayException('微信支付:发起商家转账失败');
|
||||
}
|
||||
|
||||
if (isset($res['code']) && isset($res['message'])) {
|
||||
throw new PayException($res['message']);
|
||||
}
|
||||
|
||||
return $res;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 退款
|
||||
* @param string $outTradeNo
|
||||
* @param array $options
|
||||
* @return mixed
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function refund(string $outTradeNo, array $options = [])
|
||||
{
|
||||
if (!isset($options['pay_price'])) {
|
||||
throw new PayException(400730);
|
||||
}
|
||||
$totalFee = floatval(bcmul($options['pay_price'], 100, 0));
|
||||
$refundFee = isset($options['refund_price']) ? floatval(bcmul($options['refund_price'], 100, 0)) : null;
|
||||
$refundReason = $options['desc'] ?? '';
|
||||
$refundNo = $options['refund_id'] ?? $outTradeNo;
|
||||
/*仅针对老资金流商户使用
|
||||
REFUND_SOURCE_UNSETTLED_FUNDS---未结算资金退款(默认使用未结算资金退款)
|
||||
REFUND_SOURCE_RECHARGE_FUNDS---可用余额退款
|
||||
*/
|
||||
$refundAccount = $opt['refund_account'] ?? 'AVAILABLE';
|
||||
|
||||
$data = [
|
||||
'transaction_id' => $outTradeNo,
|
||||
'out_refund_no' => $refundNo,
|
||||
'amount' => [
|
||||
'refund' => (int)$refundFee,
|
||||
'currency' => 'CNY',
|
||||
'total' => (int)$totalFee
|
||||
],
|
||||
'funds_account' => $refundAccount
|
||||
];
|
||||
|
||||
if ($refundReason) {
|
||||
$data['reason'] = $refundReason;
|
||||
}
|
||||
|
||||
$res = $this->request(self::API_REFUND_URL, 'POST', ['json' => $data]);
|
||||
|
||||
if (!$res) {
|
||||
throw new PayException('微信支付:发起退款失败');
|
||||
}
|
||||
|
||||
if (isset($res['code']) && isset($res['message'])) {
|
||||
throw new PayException($res['message']);
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询退款
|
||||
* @param string $outRefundNo
|
||||
* @return mixed
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function queryRefund(string $outRefundNo)
|
||||
{
|
||||
$res = $this->request($this->getApiUrl(self::API_REFUND_QUERY_URL, ['out_refund_no'], [$outRefundNo]), 'GET');
|
||||
|
||||
if (!$res) {
|
||||
throw new PayException(500000);
|
||||
}
|
||||
|
||||
if (isset($res['code']) && isset($res['message'])) {
|
||||
throw new PayException($res['message']);
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* jsapi支付
|
||||
* @param string $appid
|
||||
* @param string $prepayId
|
||||
* @param bool $json
|
||||
* @return array|false|string
|
||||
*/
|
||||
public function configForPayment(string $appid, string $prepayId, bool $json = true)
|
||||
{
|
||||
$params = [
|
||||
'appId' => $appid,
|
||||
'timeStamp' => strval(time()),
|
||||
'nonceStr' => uniqid(),
|
||||
'package' => "prepay_id=$prepayId",
|
||||
'signType' => 'RSA',
|
||||
];
|
||||
$message = $params['appId'] . "\n" .
|
||||
$params['timeStamp'] . "\n" .
|
||||
$params['nonceStr'] . "\n" .
|
||||
$params['package'] . "\n";
|
||||
openssl_sign($message, $raw_sign, $this->getPrivateKey(), 'sha256WithRSAEncryption');
|
||||
$sign = base64_encode($raw_sign);
|
||||
|
||||
$params['paySign'] = $sign;
|
||||
|
||||
return $json ? json_encode($params) : $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate app payment parameters.
|
||||
* @param string $prepayId
|
||||
* @return array
|
||||
*/
|
||||
public function configForAppPayment(string $prepayId): array
|
||||
{
|
||||
$params = [
|
||||
'appid' => $this->config->wechatAppid,
|
||||
'partnerid' => $this->config->mchId,
|
||||
'prepayid' => $prepayId,
|
||||
'noncestr' => uniqid(),
|
||||
'timestamp' => time(),
|
||||
'package' => 'Sign=WXPay',
|
||||
];
|
||||
$message = $params['appid'] . "\n" .
|
||||
$params['timestamp'] . "\n" .
|
||||
$params['noncestr'] . "\n" .
|
||||
$params['prepayid'] . "\n";
|
||||
openssl_sign($message, $raw_sign, $this->getPrivateKey(), 'sha256WithRSAEncryption');
|
||||
$sign = base64_encode($raw_sign);
|
||||
|
||||
$params['sign'] = $sign;
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* 小程序支付
|
||||
* @param string $appid
|
||||
* @param string $prepayId
|
||||
* @return array|false|string
|
||||
*/
|
||||
public function configForJSSDKPayment(string $appid, string $prepayId)
|
||||
{
|
||||
$config = $this->configForPayment($appid, $prepayId, false);
|
||||
|
||||
$config['timestamp'] = $config['timeStamp'];
|
||||
unset($config['timeStamp']);
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $callback
|
||||
* @return \think\Response
|
||||
*/
|
||||
public function handleNotify($callback)
|
||||
{
|
||||
$request = request();
|
||||
$event_type = $request->post('event_type');
|
||||
|
||||
// 判断成功事件
|
||||
$successEvents = ['TRANSACTION.SUCCESS', 'MCHTRANSFER.BILL.FINISHED'];
|
||||
$success = in_array($event_type, $successEvents);
|
||||
|
||||
$data = $this->decrypt($request->post('resource', []));
|
||||
$handleResult = call_user_func_array($callback, [json_decode($data, true), $success]);
|
||||
|
||||
$response = [
|
||||
'code' => $handleResult === true ? 'SUCCESS' : 'FAIL',
|
||||
'message' => $handleResult === true ? 'OK' : $handleResult,
|
||||
];
|
||||
|
||||
return response($response, 200, [], 'json');
|
||||
}
|
||||
|
||||
public function queryTransferBills(string $outBillNo)
|
||||
{
|
||||
$res = $this->request($this->getApiUrl(self::API_TRANSFER_QUERY_URL, ['out_bill_no'], [$outBillNo]), 'GET');
|
||||
|
||||
if (!$res) {
|
||||
throw new PayException(500000);
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user