You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
485 lines
15 KiB
485 lines
15 KiB
<?php
|
|
/**
|
|
* +----------------------------------------------------------------------
|
|
* | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
|
|
* +----------------------------------------------------------------------
|
|
* | Copyright (c) 2016~2023 https://www.crmeb.com All rights reserved.
|
|
* +----------------------------------------------------------------------
|
|
* | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
|
|
* +----------------------------------------------------------------------
|
|
* | Author: CRMEB Team <admin@crmeb.com>
|
|
* +----------------------------------------------------------------------
|
|
*/
|
|
|
|
namespace crmeb\services\easywechat\v3pay;
|
|
|
|
|
|
use crmeb\exceptions\PayException;
|
|
use crmeb\services\wechat\Payment;
|
|
use EasyWeChat\Payment\Order;
|
|
|
|
/**
|
|
* v3支付
|
|
* Class PayClient
|
|
* @package crmeb\services\easywechat\v3pay
|
|
*/
|
|
class PayClient extends BaseClient
|
|
{
|
|
//app支付
|
|
const API_APP_APY_URL = 'v3/pay/transactions/app';
|
|
//app支付-服务商模式
|
|
const API_APP_APY_PARTNER_URL = 'v3/pay/partner/transactions/app';
|
|
//Native下单API
|
|
const API_NATIVE_URL = 'v3/pay/transactions/native';
|
|
//Native下单API-服务商模式
|
|
const API_NATIVE_PARTNER_URL = 'v3/pay/partner/transactions/native';
|
|
//h5支付接口
|
|
const API_H5_URL = 'v3/pay/transactions/h5';
|
|
//h5支付接口-服务商模式
|
|
const API_H5_PARTNER_URL = 'v3/pay/partner/transactions/h5';
|
|
//jsapi支付接口
|
|
const API_JSAPI_URL = 'v3/pay/transactions/jsapi';
|
|
//jsapi支付接口-服务商模式
|
|
const API_JSAPI_PARTNER_URL = 'v3/pay/partner/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}';
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
protected $type = Order::JSAPI;
|
|
|
|
/**
|
|
* @param string $type
|
|
* @return $this
|
|
* @author 等风来
|
|
* @email 136327134@qq.com
|
|
* @date 2023/2/10
|
|
*/
|
|
public function setType(string $type)
|
|
{
|
|
$this->type = $type;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* 公众号jsapi支付下单
|
|
* @param string $outTradeNo
|
|
* @param string $total
|
|
* @param string $description
|
|
* @param string $attach
|
|
* @return mixed
|
|
*/
|
|
public function jsapiPay(string $openid, string $outTradeNo, string $total, string $description, string $attach)
|
|
{
|
|
$appId = $this->app['config']['wechat']['appid'];
|
|
$res = $this->pay('jsapi', $appId, $outTradeNo, $total, $description, $attach, ['openid' => $openid]);
|
|
return $this->configForJSSDKPayment($appId, $res['prepay_id']);
|
|
}
|
|
|
|
/**
|
|
* 小程序支付
|
|
* @param string $outTradeNo
|
|
* @param string $total
|
|
* @param string $description
|
|
* @param string $attach
|
|
* @return array|false|string
|
|
*/
|
|
public function miniprogPay(string $openid, string $outTradeNo, string $total, string $description, string $attach)
|
|
{
|
|
$appId = $this->app['config']['miniprog']['appid'];
|
|
$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
|
|
*/
|
|
public function appPay(string $outTradeNo, string $total, string $description, string $attach)
|
|
{
|
|
$res = $this->pay('app', $this->app['config']['app']['appid'], $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
|
|
*/
|
|
public function nativePay(string $outTradeNo, string $total, string $description, string $attach)
|
|
{
|
|
return $this->pay('native', $this->app['config']['web']['appid'], $outTradeNo, $total, $description, $attach);
|
|
}
|
|
|
|
/**
|
|
* h5支付下单
|
|
* @param string $outTradeNo
|
|
* @param string $total
|
|
* @param string $description
|
|
* @param string $attach
|
|
* @return mixed
|
|
*/
|
|
public function h5Pay(string $outTradeNo, string $total, string $description, string $attach)
|
|
{
|
|
return $this->pay('h5', $this->app['config']['wechat']['appid'], $outTradeNo, $total, $description, $attach);
|
|
}
|
|
|
|
/**
|
|
* 下单
|
|
* @param string $type
|
|
* @param string $appid
|
|
* @param string $outTradeNo
|
|
* @param string $total
|
|
* @param string $description
|
|
* @param string $attach
|
|
* @param array $payer
|
|
* @return mixed
|
|
*/
|
|
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->app['config']['v3_payment']['mchid'],
|
|
'out_trade_no' => $outTradeNo,
|
|
'attach' => $attach,
|
|
'description' => $description,
|
|
'notify_url' => $this->app['config']['v3_payment']['notify_url'],
|
|
'amount' => [
|
|
'total' => $totalFee,
|
|
'currency' => 'CNY'
|
|
],
|
|
];
|
|
|
|
if ($payer) {
|
|
$data['payer'] = $payer;
|
|
}
|
|
|
|
//服务商支付模式
|
|
if ($this->app['config']['v3_payment']['mer_type']) {
|
|
|
|
$mchid = $data['mchid'];
|
|
$appid = $data['appid'];
|
|
unset($data['mchid'], $data['appid'], $data['payer']);
|
|
$data['sp_appid'] = $appid;
|
|
$data['sp_mchid'] = $mchid;
|
|
$data['sub_mchid'] = $this->app['config']['v3_payment']['sub_mch_id'];
|
|
if (!empty($payer['openid'])) {
|
|
$data['payer']['sp_openid'] = $payer['openid'];
|
|
}
|
|
|
|
$url = '';
|
|
switch ($type) {
|
|
case 'h5':
|
|
$url = self::API_H5_PARTNER_URL;
|
|
$data['scene_info'] = [
|
|
'payer_client_ip' => request()->ip(),
|
|
'h5_info' => [
|
|
'type' => 'Wap'
|
|
]
|
|
];
|
|
break;
|
|
case 'native':
|
|
$url = self::API_NATIVE_PARTNER_URL;
|
|
break;
|
|
case 'app':
|
|
$url = self::API_APP_APY_PARTNER_URL;
|
|
break;
|
|
case 'jsapi':
|
|
$url = self::API_JSAPI_PARTNER_URL;
|
|
break;
|
|
}
|
|
|
|
} else {
|
|
$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;
|
|
}
|
|
|
|
/**
|
|
* 发起商家转账API
|
|
* @param string $outBatchNo
|
|
* @param string $amount
|
|
* @param string $batchName
|
|
* @param string $remark
|
|
* @param array $transferDetailList
|
|
* @return mixed
|
|
*/
|
|
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 === Order::JSAPI) {
|
|
$appid = $this->app['config']['wechat']['appid'];
|
|
} else if ($this->type === 'mini') {
|
|
$appid = $this->app['config']['miniprog']['appid'];
|
|
} else if ($this->type === Order::APP) {
|
|
$appid = $this->app['config']['app']['appid'];
|
|
}
|
|
|
|
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
|
|
*/
|
|
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;
|
|
}
|
|
|
|
//服务商支付退款
|
|
$merType = $this->app['config']['v3_payment']['mer_type'];
|
|
if ($merType) {
|
|
$data['sub_mchid'] = $this->app['config']['v3_payment']['sub_mch_id'];
|
|
}
|
|
|
|
$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
|
|
*/
|
|
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->app['config']['app']['appid'],
|
|
'partnerid' => $this->app['config']['v3_payment']['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();
|
|
$success = $request->post('event_type') === 'TRANSACTION.SUCCESS';
|
|
$data = $this->decrypt($request->post('resource', []));
|
|
|
|
$handleResult = call_user_func_array($callback, [json_decode($data), $success]);
|
|
if (is_bool($handleResult) && $handleResult) {
|
|
$response = [
|
|
'code' => 'SUCCESS',
|
|
'message' => 'OK',
|
|
];
|
|
} else {
|
|
$response = [
|
|
'code' => 'FAIL',
|
|
'message' => $handleResult,
|
|
];
|
|
}
|
|
|
|
return response($response, 200, [], 'json');
|
|
}
|
|
}
|
|
|