config = HuifuConfig::getInstance(); $this->client = new HuifuClient(); } public function refund($order, $refundLog, $refundMoney, $remark = '') { $ext = HuifuPayService::getPayExt($order['id']); if (!$ext) { throw new \RuntimeException('未找到汇付支付记录'); } $splitOrder = SplitOrder::where('order_id', $order['id'])->where('channel', 'huifu')->where('status', 2)->find(); if ($splitOrder && !empty($splitOrder['confirm_hf_seq_id'])) { return $this->confirmRefund($ext, $splitOrder, $refundLog, $refundMoney, $remark); } try { return $this->scanpayRefund($ext, $refundLog, $refundMoney, $remark); } catch (\Throwable $e) { // 如果汇付报错 23000003(申请退款金额大于可退款余额),核查是否此前已成功退款(幂等对齐防多退) if (strpos($e->getMessage(), '23000003') !== false || strpos($e->getMessage(), '申请退款金额大于可退款余额') !== false) { if ($this->isAlreadyRefunded($ext, $refundMoney)) { \think\Log::info('[shopro_huifu] 汇付检测到该笔退款此前已成功执行,执行本地状态幂等对齐: order_id=' . $order['id']); $refundLog->payment_json = json_encode(['idempotent' => true, 'remark' => '汇付核验已退款成功,自动对齐本地状态']); $refundLog->save(); return true; } throw new \RuntimeException('汇付商户账户可用余额不足,请在汇付平台充值或留存可用资金后再退款'); } throw $e; } } public function isAlreadyRefunded($ext, $refundMoney) { try { SdkLoader::requireRequest('V2TradeAcctpaymentAcctlogQueryRequest.php'); $request = new \BsPaySdk\request\V2TradeAcctpaymentAcctlogQueryRequest(); $request->setReqSeqId(date('YmdHis') . mt_rand(1000, 9999)); $request->setHuifuId($this->config->get('huifu_id')); $request->setAcctDate($ext['req_date'] ?? date('Ymd')); $request->setExtendInfo([ 'page_size' => '20', 'page_num' => '1', 'acct_type' => '01', ]); $result = $this->client->postRequest($request); $data = $this->client->parseResponse($result); if (empty($data['acct_log_list'])) { return false; } $list = is_string($data['acct_log_list']) ? json_decode($data['acct_log_list'], true) : $data['acct_log_list']; if (!is_array($list)) { return false; } $targetAmt = number_format((float)$refundMoney, 2, '.', ''); foreach ($list as $log) { // 2006 为支付退款 if (($log['acct_trans_type_code'] ?? '') === '2006' && ($log['trans_amt'] ?? '') === $targetAmt) { return true; } } } catch (\Throwable $ex) { \think\Log::error('[shopro_huifu] 幂等核查汇付退款流水异常: ' . $ex->getMessage()); } return false; } protected function scanpayRefund($ext, $refundLog, $refundMoney, $remark) { SdkLoader::requireRequest('v4/payment/TradePaymentScanpayRefundRequest.php'); $request = new \BsPaySdk\request\v4\payment\TradePaymentScanpayRefundRequest(); $request->setReqDate(date('Ymd')); $request->setReqSeqId($refundLog['refund_sn']); $request->setHuifuId($this->config->get('huifu_id')); $request->setOrdAmt(number_format((float) $refundMoney, 2, '.', '')); $request->setOrgReqDate($ext['req_date']); $request->setOrgReqSeqId($ext['req_seq_id']); if ($this->config->get('refund_notify_url')) { $request->setNotifyUrl($this->config->getNotifyUrl('refund')); } if ($remark) { $request->setRemark($remark); } $result = $this->client->payment()->refund($request); $data = $this->client->parseResponse($result); if (($data['trans_stat'] ?? '') === 'S' || ($data['resp_code'] ?? '') === '00000000') { return true; } throw new \RuntimeException($data['resp_desc'] ?? '汇付退款失败'); } protected function confirmRefund($ext, $splitOrder, $refundLog, $refundMoney, $remark) { SdkLoader::requireRequest('V2TradePaymentDelaytransConfirmrefundRequest.php'); $request = new \BsPaySdk\request\V2TradePaymentDelaytransConfirmrefundRequest(); $request->setReqDate(date('Ymd')); $request->setReqSeqId($refundLog['refund_sn']); $request->setHuifuId($this->config->get('huifu_id')); $request->setExtendInfo([ 'org_req_date' => $ext['req_date'], 'org_req_seq_id' => $ext['req_seq_id'], 'confirm_hf_seq_id' => $splitOrder['confirm_hf_seq_id'], 'ord_amt' => number_format((float) $refundMoney, 2, '.', ''), 'remark' => $remark, ]); $result = $this->client->postRequest($request); $data = $this->client->parseResponse($result); if (($data['trans_stat'] ?? '') === 'S' || ($data['resp_code'] ?? '') === '00000000') { return true; } throw new \RuntimeException($data['resp_desc'] ?? '汇付确认退款失败'); } }