* +---------------------------------------------------------------------- */ 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; } }