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(); try { if ($splitOrder && !empty($splitOrder['confirm_hf_seq_id'])) { return $this->confirmRefund($ext, $splitOrder, $refundLog, $refundMoney, $remark); } return $this->scanpayRefund($ext, $refundLog, $refundMoney, $remark); } catch (\Throwable $e) { // 核查是否此前已在汇付成功退款(网关状态幂等自动对齐,防多退、防报错卡死) if ($this->isAlreadyRefunded($ext, $refundMoney)) { \think\Log::info('[shopro_huifu] 汇付检测到该笔退款此前已成功执行,执行本地状态自动对齐: order_id=' . $order['id']); $refundLog->payment_json = json_encode([ 'idempotent' => true, 'remark' => '汇付核验已退款成功,自动对齐本地状态', 'raw_error' => $e->getMessage(), ], JSON_UNESCAPED_UNICODE); $refundLog->save(); return true; } if (strpos($e->getMessage(), '23000003') !== false || strpos($e->getMessage(), '申请退款金额大于可退款余额') !== false) { throw new \RuntimeException('汇付商户账户可用余额不足,请在汇付平台充值或留存可用资金后再退款'); } throw $e; } } public function isAlreadyRefunded($ext, $refundMoney) { $dates = array_filter(array_unique([ date('Ymd'), !empty($ext['req_date']) ? $ext['req_date'] : null, ])); foreach ($dates as $date) { if ($this->checkAcctLogForRefund($date, $refundMoney)) { return true; } } return false; } protected function checkAcctLogForRefund($acctDate, $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($acctDate); $request->setExtendInfo([ 'page_size' => '50', '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] 幂等核查汇付退款流水异常(' . $acctDate . '): ' . $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'] ?? '汇付确认退款失败'); } }