config = $config; $this->v3Config = $v3Config; $this->payClient = $payClient; $this->isV3PAy = $this->v3Config->isV3PAy; $this->debug = !!DefaultConfig::value('logger'); } /** * @return Payment */ public static function instance(): static { return app()->make(static::class); } /** * @return Application * @throws InvalidArgumentException * @author 等风来 * @email 136327134@qq.com * @date 2022/10/11 */ public function application(): Application { $request = request(); $config = $this->config->all(); switch ($accessEnd = $this->getAuthAccessEnd($request)) { case self::APP: /** @var OpenAppConfig $make */ $make = app()->make(OpenAppConfig::class); $config['app_id'] = $make->appId; $config['notify_url'] = trim($make->getConfig(DefaultConfig::COMMENT_URL)) . DefaultConfig::value('app.notifyUrl'); break; case self::PC: /** @var OpenWebConfig $make */ $make = app()->make(OpenWebConfig::class); $config['app_id'] = $make->appId; break; case self::MINI: /** @var MiniProgramConfig $make */ $make = app()->make(MiniProgramConfig::class); $config['app_id'] = $make->appId; $config['notify_url'] = trim($make->getConfig(DefaultConfig::COMMENT_URL)) . DefaultConfig::value('mini.notifyUrl'); break; } if (!isset($this->application[$accessEnd])) { $this->application[$accessEnd] = new Application($config); $this->setHttpClient($this->application[$accessEnd], self::BASE_PAY_URL); $this->setRequest($this->application[$accessEnd]); } return $this->application[$accessEnd]; } /** * 发起订单支付接口入口 * @return OrderClient * @throws InvalidArgumentException * @throws InvalidConfigException * @author 等风来 * @email 136327134@qq.com * @date 2023/10/10 */ public function order() { $api = $this->application()->getClient(); $config = $this->application()->getConfig(); return new OrderClient($api, $config); } /** * 付款码支付 * @param string $authCode * @param string $outTradeNo * @param string $totalFee * @param string $attach * @param string $body * @param string $detail * @return array * @throws InvalidArgumentException * @throws InvalidConfigException * @throws TransportExceptionInterface */ public static function microPay(string $authCode, string $outTradeNo, string $totalFee, string $attach, string $body, string $detail = '') { $totalFee = bcmul($totalFee, 100, 0); $response = self::instance()->order()->pay([ 'auth_code' => $authCode, 'out_trade_no' => $outTradeNo, 'total_fee' => (int)$totalFee, 'attach' => $attach, 'body' => $body, 'detail' => $detail ]); $response = $response->toArray(); self::logger('付款码支付', compact('authCode', 'outTradeNo', 'totalFee', 'attach', 'body', 'detail'), $response); //下单成功 if ($response['return_code'] === 'SUCCESS') { //扫码付款直接支付成功 if ($response['result_code'] === 'SUCCESS' && $response['trade_type'] === 'MICROPAY') { return [ 'paid' => 1, 'message' => '支付成功', 'payInfo' => $response, ]; } else { return [ 'paid' => 0, 'message' => $response['err_code_des'], 'payInfo' => $response ]; } } else { throw new PayException($response['return_msg']); } } /** * 撤销订单 * @param string $outTradeNo * @return bool * @throws InvalidArgumentException * @throws InvalidConfigException * @throws TransportExceptionInterface */ public static function reverseOrder(string $outTradeNo): bool { $response = self::instance()->order()->reverseByOutTradeNumber($outTradeNo); $response = $response->toArray(); self::logger('撤销订单', compact('outTradeNo'), $response); if ($response['return_code'] === 'SUCCESS') { return true; } else { throw new PayException($response['return_msg']); } } /** * 查询订单支付状态 * @param string $outTradeNo * @return array * @throws InvalidArgumentException * @throws InvalidConfigException * @throws TransportExceptionInterface */ public static function queryOrder(string $outTradeNo) { $response = self::instance()->order()->queryByOutTradeNumber($outTradeNo); $response = $response->toArray(); self::logger('查询订单支付状态', compact('outTradeNo'), $response); if ($response['return_code'] === 'SUCCESS') { if ($response['result_code'] === 'SUCCESS') { return [ 'paid' => 1, 'out_trade_no' => $outTradeNo, 'payInfo' => $response ]; } else { return [ 'paid' => 0, 'out_trade_no' => $outTradeNo, 'payInfo' => $response ]; } } else { throw new PayException($response['return_msg']); } } /** * 企业付款到零钱 * @param string $openid openid * @param string $orderId 订单号 * @param string $amount 金额 * @param string $desc 说明 * @param string $type 类型 * @return bool * @throws InvalidArgumentException * @throws TransportExceptionInterface * @throws \Psr\SimpleCache\InvalidArgumentException|InvalidConfigException */ public static function merchantPay(string $channel_type, int $uid, string $orderId, string $amount, string $desc, string $user_name = '', array $transferDetailList = []) { /** @var WechatUserServices $wechatServices */ $wechatServices = app()->make(WechatUserServices::class); $typeMap = [ 'routine' => Payment::MINI, 'wechat' => Payment::WEB, 'app' => Payment::APP ]; $channelType = $channel_type; $openid = ''; $type = null; if (isset($typeMap[$channelType])) { $openid = $wechatServices->getWechatOpenid($uid, $channelType); $type = $typeMap[$channelType]; } if (!$openid) { throw new PayException('该用户暂不支持企业付款到零钱,请手动转账'); } $application = self::instance()->setAccessEnd($type)->application(); $config = $application->getConfig(); if (empty($config['certificate'])) { throw new PayException('企业付款到零钱需要支付cert证书,检测到您没有上传!'); } if (empty($config['private_key'])) { throw new PayException('企业付款到零钱需要支付key证书,检测到您没有上传!'); } if (self::instance()->isV3PAy) { if (sys_config('v3_pay_public_key') != '') { $transfer_scene_id = sys_config('pay_weixin_scene_id'); if (!$transfer_scene_id) { throw new PayException('请配置微信v3新支付场景ID!'); } $user_name = $user_name ?: app()->make(UserServices::class)->value(['uid' => $uid], 'nickname'); //v3新支付 $res = self::instance()->payClient->setType($type)->transferBills( outBatchNo: $orderId, amount: $amount, openid: $openid, userName: $user_name, remark: $desc, perception: $desc, transferDetailList: $transferDetailList, transfer_scene_id: $transfer_scene_id ); self::logger('商家转账到零钱', compact('orderId', 'amount', 'openid', 'desc', 'user_name', 'transferDetailList', 'transfer_scene_id'), $res); } else { //v3支付使用发起商家转账API $res = self::instance()->payClient->setType($type)->batches( outBatchNo: $orderId, amount: $amount, batchName: $desc, remark: $desc, transferDetailList: [ [ 'out_detail_no' => $orderId, 'transfer_amount' => $amount, 'transfer_remark' => $desc, 'openid' => $openid ] ] ); } return $res; } else { $merchantPayData = [ 'partner_trade_no' => $orderId, //随机字符串作为订单号,跟红包和支付一个概念。 'openid' => $openid, //收款人的openid 'check_name' => 'NO_CHECK', //文档中有三种校验实名的方法 NO_CHECK OPTION_CHECK FORCE_CHECK 'amount' => (int)bcmul($amount, '100', 0), //单位为分 'desc' => $desc, 'spbill_create_ip' => request()->ip(), //发起交易的IP地址 ]; $result = self::instance()->order()->toBalance($merchantPayData); $result = $result->toArray(); self::logger('企业付款到零钱', compact('merchantPayData'), $result); if ($result['return_code'] == 'SUCCESS' && $result['result_code'] != 'FAIL') { return true; } else { throw new PayException(($result['return_msg'] ?? '支付失败') . ':' . ($result['err_code_des'] ?? '发起企业支付到零钱失败')); } } } /** * 生成支付订单对象 * @param $openid * @param $out_trade_no * @param $total_fee * @param $attach * @param $body * @param string $detail * @param string $trade_type * @param array $options * @return \EasyWeChat\Kernel\HttpClient\Response * @throws TransportExceptionInterface */ public static function paymentOrder($openid, $out_trade_no, $total_fee, $attach, $body, $detail = '', $trade_type = 'JSAPI', array $options = []) { $total_fee = bcmul($total_fee, 100, 0); $order = array_merge(compact('out_trade_no', 'total_fee', 'attach', 'body', 'detail', 'trade_type'), $options); if (!is_null($openid)) $order['openid'] = $openid; if ($order['detail'] == '') unset($order['detail']); $result = self::instance()->order()->unify($order); $result = $result->toArray(); self::logger('生成支付订单对象', compact('order'), $result); if ($result['return_code'] == 'SUCCESS' && $result['result_code'] == 'SUCCESS') { return $result; } else { if ($result['return_code'] == 'FAIL') { throw new PayException('微信支付错误返回:' . $result['return_msg']); } else if (isset($result['err_code'])) { throw new PayException('微信支付错误返回:' . $result['err_code_des']); } else { throw new PayException('没有获取微信支付的预支付ID,请重新发起支付!'); } } } /** * 生成支付订单对象(小程序商户号支付时) * @param $openid * @param $out_trade_no * @param $total_fee * @param $attach * @param $body * @param string $detail * @param string $trade_type * @param array $options * @return \EasyWeChat\Kernel\HttpClient\Response|ResponseInterface * @throws InvalidArgumentException * @throws InvalidConfigException * @throws TransportExceptionInterface */ public static function paymentMiniOrder($openid, $out_trade_no, $total_fee, $attach, $body, $detail = '', $trade_type = 'JSAPI', array $options = []) { $total_fee = bcmul($total_fee, 100, 0); $order = array_merge(compact('out_trade_no', 'total_fee', 'attach', 'body', 'detail', 'trade_type'), $options); if (!is_null($openid)) $order['openid'] = $openid; if ($order['detail'] == '') unset($order['detail']); $order['spbill_create_ip'] = request()->ip(); $result = self::instance()->order()->createorder($order); $result = $result->toArray(); self::logger('生成支付订单对象', compact('order'), $result); if ($result['errcode'] == '0') { return $result; } else { throw new PayException('微信支付错误返回:' . $result['errmsg']); } } /** * 获得jsSdk支付参数 * @param $openid * @param $out_trade_no * @param $total_fee * @param $attach * @param $body * @param string $detail * @param string $trade_type * @param array $options * @return array * @throws InvalidArgumentException * @throws InvalidConfigException|TransportExceptionInterface */ public static function jsPay($openid, $out_trade_no, $total_fee, $attach, $body, $detail = '', $trade_type = 'JSAPI', $options = []) { $pay = self::instance(); $paymentPrepare = self::paymentOrder($openid, $out_trade_no, $total_fee, $attach, $body, $detail, $trade_type, $options); return $pay->application() ->getUtils() ->buildSdkConfig($paymentPrepare['prepay_id'], $pay->application()->getConfig()->get('app_id'), 'MD5'); } /** * 获得jsSdk支付参数(小程序商户号支付时) * @param $openid * @param $out_trade_no * @param $total_fee * @param $attach * @param $body * @param string $detail * @param string $trade_type * @param array $options * @return array * @throws TransportExceptionInterface */ public static function miniPay($openid, $out_trade_no, $total_fee, $attach, $body, $detail = '', $trade_type = 'JSAPI', $options = []) { $paymentPrepare = self::paymentMiniOrder($openid, $out_trade_no, $total_fee, $attach, $body, $detail, $trade_type, $options); $paymentPrepare['payment_params']['timestamp'] = $paymentPrepare['payment_params']['timeStamp']; return $paymentPrepare['payment_params'] ?? []; } /** * 获得APP付参数 * @param $openid * @param $out_trade_no * @param $total_fee * @param $attach * @param $body * @param string $detail * @param string $trade_type * @param array $options * @return array|string * @throws InvalidArgumentException * @throws InvalidConfigException * @throws TransportExceptionInterface * @throws \Psr\SimpleCache\InvalidArgumentException */ public static function appPay($openid, $out_trade_no, $total_fee, $attach, $body, $detail = '', $trade_type = 'APP', $options = []) { if (self::instance()->isV3PAy) { return self::instance()->payClient->appPay($out_trade_no, $total_fee, $body, $attach); } else { $paymentPrepare = self::paymentOrder($openid, $out_trade_no, $total_fee, $attach, $body, $detail, $trade_type, $options); return self::instance()->application()->getUtils()->buildAppConfig($paymentPrepare['prepay_id'], self::instance()->application()->getConfig()->get('app_id')); } } /** * v3 jspay 支付 * @return PayClient * @author 等风来 * @email 136327134@qq.com * @date 2023/10/10 */ public function payClient() { return $this->payClient; } /** * 获得native支付参数 * @param $openid * @param $out_trade_no * @param $total_fee * @param $attach * @param $body * @param string $detail * @param string $trade_type * @param array $options * @return array * @throws TransportExceptionInterface * @throws \Psr\SimpleCache\InvalidArgumentException */ public static function nativePay($openid, $out_trade_no, $total_fee, $attach, $body, $detail = '', $trade_type = 'NATIVE', $options = []) { $instance = self::instance(); if ($instance->isV3PAy) { $data = $instance->payClient->nativePay($out_trade_no, $total_fee, $body, $attach); $res['code_url'] = $data['code_url']; $res['invalid'] = time() + 60; $res['logo'] = []; return $res; } $data = $instance->setAccessEnd(self::WEB)->paymentOrder($openid, $out_trade_no, $total_fee, $attach, $body, $detail, $trade_type, $options); if ($data) { $res['code_url'] = $data['code_url']; $res['invalid'] = time() + 60; $res['logo'] = []; } else $res = []; return $res; } /** * 使用商户订单号退款 * @param $orderNo * @param $refundNo * @param $totalFee * @param null $refundFee * @param null $opUserId * @param string $refundReason * @param string $type * @param string $refundAccount * @return \EasyWeChat\Kernel\HttpClient\Response * @throws TransportExceptionInterface */ public function refund($orderNo, $refundNo, $totalFee, $refundFee = null, $opUserId = null, string $refundReason = '', string $type = 'out_trade_no', string $refundAccount = 'REFUND_SOURCE_UNSETTLED_FUNDS') { $totalFee = floatval($totalFee); $refundFee = floatval($refundFee); if ($type == 'out_trade_no') { $result = $this->order()->byOutTradeNumber($orderNo, $refundNo, $totalFee, $refundFee, [ 'refund_account' => $refundAccount, 'notify_url' => $this->config->refundUrl, 'refund_desc' => $refundReason ]); } else { $result = $this->order()->byTransactionId($orderNo, $refundNo, $totalFee, $refundFee, [ 'refund_account' => $refundAccount, 'notify_url' => $this->config->refundUrl, 'refund_desc' => $refundReason ]); } self::logger('使用商户订单号退款', compact('orderNo', 'refundNo', 'totalFee', 'refundFee', 'opUserId', 'refundReason', 'type', 'refundAccount'), $result); return $result; } /** * 小程序商户退款 * @param $orderNo //微信支付单号 * @param $refundNo //微信退款单号 * @param $totalFee * @param null $refundFee * @param array $opt * @return \EasyWeChat\Kernel\HttpClient\Response|ResponseInterface * @throws InvalidArgumentException * @throws InvalidConfigException * @throws TransportExceptionInterface */ public function miniRefund($orderNo, $refundNo, $totalFee, $refundFee = null, array $opt = []) { $totalFee = floatval($totalFee); $refundFee = floatval($refundFee); $order = [ 'openid' => $opt['open_id'], 'trade_no' => $opt['routine_order_id'], 'transaction_id' => $orderNo, 'refund_no' => $refundNo, 'total_amount' => $totalFee, 'refund_amount' => $refundFee, ]; $result = $this->order()->refundorder($order); self::logger('使用商户订单号退款', compact('orderNo', 'refundNo', 'totalFee', 'refundFee', 'opt'), $result); return $result; } /** * 退款 * @param $orderNo * @param array $opt * @return bool * @throws \Psr\SimpleCache\InvalidArgumentException|TransportExceptionInterface */ public function payOrderRefund($orderNo, array $opt) { if (isset($opt['pay_routine_open']) && $opt['pay_routine_open']) { return $this->payMiniOrderRefund($orderNo, $opt); } if (!isset($opt['pay_price'])) { throw new PayException('缺少pay_price'); } $certPath = $this->config->certPath; if (!$certPath) { throw new PayException('请上传支付证书cert'); } $keyPath = $this->config->keyPath; if (!$keyPath) { throw new PayException('请上传支付证书key'); } if (!is_file($certPath)) { throw new PayException('支付证书cert不存在'); } if (!is_file($keyPath)) { throw new PayException('支付证书key不存在'); } if ($this->isV3PAy) { return $this->payClient->refund($orderNo, $opt); } $totalFee = floatval(bcmul($opt['pay_price'], 100, 0)); $refundFee = isset($opt['refund_price']) ? floatval(bcmul($opt['refund_price'], 100, 0)) : null; $refundReason = $opt['desc'] ?? ''; $refundNo = $opt['refund_id'] ?? $orderNo; $opUserId = $opt['op_user_id'] ?? null; $type = $opt['type'] ?? 'out_trade_no'; /*仅针对老资金流商户使用 REFUND_SOURCE_UNSETTLED_FUNDS---未结算资金退款(默认使用未结算资金退款) REFUND_SOURCE_RECHARGE_FUNDS---可用余额退款*/ $refundAccount = $opt['refund_account'] ?? 'REFUND_SOURCE_UNSETTLED_FUNDS'; try { $res = $this->refund($orderNo, $refundNo, $totalFee, $refundFee, $opUserId, $refundReason, $type, $refundAccount); $res = $res->toArray(); if ($res['return_code'] == 'FAIL') { throw new PayException('退款失败:' . $res['return_msg']); } if (isset($res['err_code'])) { throw new PayException('退款失败:' . $res['err_code_des']); } } catch (\Exception $e) { self::error($e); throw new PayException($e->getMessage()); } return true; } /** * 小程序商户退款 * @param $orderNo * @param array $opt * @return bool * @throws TransportExceptionInterface */ public function payMiniOrderRefund($orderNo, array $opt) { if (!isset($opt['pay_price'])) { throw new PayException('缺少pay_price'); } if (!isset($opt['routine_order_id'])) { throw new PayException('缺少订单单号'); } $totalFee = floatval(bcmul($opt['pay_price'], 100, 0)); $refundFee = isset($opt['refund_price']) ? floatval(bcmul($opt['refund_price'], 100, 0)) : null; $refundNo = $opt['refund_no']; try { $result = $this->miniRefund($orderNo, $refundNo, $totalFee, $refundFee, $opt); $result = $result->toArray(); if ($result['errcode'] == '0') { return true; } else { throw new PayException('退款失败:' . $result['errmsg']); } } catch (\Exception $e) { self::error($e); throw new PayException($e->getMessage()); } } /** * 商家转账通知 * @return Response * User: liusl * DateTime: 2025/2/14 下午2:53 */ public function handleMchNotify() { $response = $this->payClient->handleNotify(function ($notify, $success) { self::logger('商家转账成功回调接口', [], $notify); if (isset($notify['transfer_bill_no']) && $success) { $res = Event::until('pay.mchNotify', [$notify]); if ($res) { return $res; } else { return false; } } }); return response($response->getContent()); } /** * 微信支付成功回调接口 * @return Response * @throws InvalidArgumentException * @throws RuntimeException * @throws ReflectionException * @throws Throwable */ public function handleNotify() { if ($this->isV3PAy) { $response = $this->payClient->handleNotify(function ($notify, $success) { self::logger('微信支付成功回调接口', [], $notify); if (isset($notify['out_trade_no']) && $success) { $res = Event::until('pay.notify', [$notify]); if ($res) { return $res; } else { return false; } } }); return response($response->getContent()); } else { $message = $this->application()->getServer()->getRequestMessage(); $message = $message->toArray(); if ($this->checkSignV2($message)) { self::logger('微信支付成功回调接口', [], $message); Event::until('pay.notify', [$message]); return response(XML::build(['return_code' => 'success', 'return_message' => 'OK']), 200, [], 'xml'); } else { return response(XML::build(['return_code' => 'fail', 'return_message' => 'FAIL']), 500, [], 'xml'); } } } /** * 验签名 * @param $message * @return bool */ public function checkSignV2($message) { $sign = $message['sign']; unset($message['sign']); if ($this->generate_sign($message, $this->config->key) !== $sign) { return false; } return true; } /** * @param array $attributes * @param $key * @param $encryptMethod * @return string */ function generate_sign(array $attributes, $key, $encryptMethod = 'md5') { ksort($attributes); $attributes['key'] = $key; return strtoupper(call_user_func_array($encryptMethod, [urldecode(http_build_query($attributes))])); } /** * 退款结果通知 * @return Response * @throws InvalidArgumentException * @throws ReflectionException * @throws RuntimeException * @throws Throwable */ public function handleRefundedNotify() { $response = $this->application()->getServer()->handleRefunded(function ($message, \Closure $next) { self::logger('退款结果通知', [], compact('message')); Event::until('pay.refunded.notify', [$message]); return $next($message); }); return response($response->serve()); } /** * 是否时微信付款二维码值 * @param string $authCode * @return bool */ public static function isWechatAuthCode(string $authCode) { return preg_match('/^[0-9]{18}$/', $authCode) && in_array(substr($authCode, 0, 2), ['10', '11', '12', '13', '14', '15']); } }