config = include $configFile; } else { throw new \Exception('建行生活配置文件不存在'); } // ✅ 修复: 删除processPemKeys()调用 // 密钥格式化统一由CcbRSA类处理,避免重复格式化导致OpenSSL ASN1解析错误 // CcbRSA::formatPublicKey/formatPrivateKey 会在加密/解密时自动处理密钥格式 $this->orderService = new CcbOrderService(); } /** * 生成建行支付串 * 用于前端JSBridge调用建行收银台 * * ⚠️ 注意:必须包含所有必需参数,签名前按ASCII排序 * * @param int $orderId Shopro订单ID * @param string $payFlowId 支付流水号(由控制器统一生成) * @return array ['status' => bool, 'message' => string, 'data' => array] */ public function generatePaymentString($orderId, $payFlowId) { // ⚠️ 开启事务保护,确保数据一致性 Db::startTrans(); try { // 获取订单信息 $order = Order::find($orderId); if (!$order) { throw new \Exception('订单不存在'); } // 检查订单状态 if ($order['status'] != 'unpaid') { throw new \Exception('订单状态不正确'); } // 获取用户建行生活ID(用于订单推送) $user = Db::name('user')->where('id', $order['user_id']) ->field('ccb_user_id') ->find(); if (empty($user['ccb_user_id'])) { throw new \Exception('用户未绑定建行生活账号'); } // ✅ 使用控制器传入的统一支付流水号(确保与订单推送使用同一流水号) if (empty($payFlowId)) { throw new \Exception('支付流水号不能为空'); } // ⚠️ 关键:建行要求参数按照文档表格定义的顺序拼接,不是ASCII排序! // 根据建行文档4.1和4.2,必须严格按照参数表顺序构建签名字符串 // 1. 定义参与MAC签名的参数数组(按文档表格顺序) $macParams = []; // 1.1 商户信息(必填,二选一:建行商户号组合 或 外部平台商户号) $usePlatMctId = !empty($this->config['plat_mct_id']); if ($usePlatMctId) { // 使用外部平台商户号 $macParams['PLATMCTID'] = $this->config['plat_mct_id']; } else { // 使用建行商户号组合 $macParams['MERCHANTID'] = $this->config['merchant_id']; $macParams['POSID'] = $this->config['pos_id']; $macParams['BRANCHID'] = $this->config['branch_id']; } // 1.2 订单信息(必填) $macParams['ORDERID'] = $payFlowId; // 支付流水号 $macParams['USER_ORDERID'] = $order['order_sn']; // 用户订单号 $macParams['PAYMENT'] = number_format($order['pay_fee'], 2, '.', ''); // 支付金额 $macParams['CURCODE'] = '01'; // 币种(01=人民币) $macParams['TXCODE'] = '520100'; // 交易码 $macParams['REMARK1'] = ''; // 备注1(空字符串也要传) $macParams['REMARK2'] = $this->config['service_id']; // 备注2(服务方编号) $macParams['TYPE'] = '1'; // 接口类型(1=防钓鱼) $macParams['GATEWAY'] = '0'; // 网关类型 $macParams['CLIENTIP'] = ''; // 客户端IP(建行生活环境送空) $macParams['REGINFO'] = ''; // 客户注册信息(空字符串) $macParams['PROINFO'] = $this->buildProductInfo($order); // 商品信息(escape编码) $macParams['REFERER'] = ''; // 商户URL(空字符串) $macParams['THIRDAPPINFO'] = 'comccbpay1234567890cloudmerchant'; // 客户端标识(固定值) // 1.3 可选参数(按文档表格顺序,有值才参与MAC) // ⚠️ 注意:根据文档4.2,橙色字段有值时才参与MAC,空值不参与 // 分期期数(在THIRDAPPINFO之后) if (!empty($this->config['install_num'])) { $macParams['INSTALLNUM'] = $this->config['install_num']; } // 超时时间 if (!empty($this->config['timeout'])) { $macParams['TIMEOUT'] = $this->config['timeout']; } else { // 默认30分钟超时 $macParams['TIMEOUT'] = date('YmdHis', strtotime('+30 minutes')); } // 中国建设银行App环境参数 if (!empty($this->config['user_id'])) { $macParams['USERID'] = $this->config['user_id']; } if (!empty($this->config['token'])) { $macParams['TOKEN'] = $this->config['token']; } if (!empty($this->config['pay_success_url'])) { $macParams['PAYSUCCESSURL'] = urlencode($this->config['pay_success_url']); } // 支付位图和账户位图 if (!empty($this->config['pay_bitmap'])) { $macParams['PAYBITMAP'] = $this->config['pay_bitmap']; } if (!empty($this->config['account_bitmap'])) { $macParams['ACCOUNTBITMAP'] = $this->config['account_bitmap']; } // 积分相关 if (!empty($this->config['point_avy_id'])) { $macParams['POINTAVYID'] = $this->config['point_avy_id']; } if (!empty($this->config['fixed_point_val'])) { $macParams['FIXEDPOINTVAL'] = $this->config['fixed_point_val']; } if (!empty($this->config['min_point_limit'])) { $macParams['MINPOINTLIMIT'] = $this->config['min_point_limit']; } // 有价券相关 if (!empty($this->config['coupon_avy_id'])) { $macParams['COUPONAVYID'] = $this->config['coupon_avy_id']; } if (!empty($this->config['only_credit_pay_flag'])) { $macParams['ONLY_CREDIT_PAY_FLAG'] = $this->config['only_credit_pay_flag']; } // 数字人民币参数 if (!empty($this->config['dcep_mct_type'])) { $macParams['DCEP_MCT_TYPE'] = $this->config['dcep_mct_type']; if ($this->config['dcep_mct_type'] == '2') { // 非融合商户需要填写数币商户号 if (!empty($this->config['dcep_merchant_id'])) { $macParams['DCEP_MERCHANTID'] = $this->config['dcep_merchant_id']; } if (!empty($this->config['dcep_pos_id'])) { $macParams['DCEP_POSID'] = $this->config['dcep_pos_id']; } if (!empty($this->config['dcep_branch_id'])) { $macParams['DCEP_BRANCHID'] = $this->config['dcep_branch_id']; } } if (!empty($this->config['dcep_dep_acc_no'])) { $macParams['DCEPDEPACCNO'] = $this->config['dcep_dep_acc_no']; } } // 二级商户参数 if (!empty($this->config['sub_mct_id'])) { $macParams['SUB_MCT_ID'] = $this->config['sub_mct_id']; } if (!empty($this->config['sub_mct_name'])) { $macParams['SUB_MCT_NAME'] = $this->config['sub_mct_name']; } if (!empty($this->config['sub_mct_mcc'])) { $macParams['SUB_MCT_MCC'] = $this->config['sub_mct_mcc']; } // 扩展域 if (!empty($this->config['extend_params'])) { $macParams['EXTENDPARAMS'] = urlencode($this->config['extend_params']); } // 特殊字段(中石化专用) if (!empty($this->config['identity_code'])) { $macParams['IDENTITYCODE'] = $this->config['identity_code']; } if (!empty($this->config['notify_url'])) { $macParams['NOTIFY_URL'] = urlencode($this->config['notify_url']); } // 2. 构建签名字符串(按照定义顺序拼接,不排序!) $signString = http_build_query($macParams); // 3. 添加PLATFORMPUB参与MD5签名(但不作为HTTP参数传递) $platformPubKey = $this->config['public_key']; // 服务方公钥 $macSignString = $signString . '&PLATFORMPUB=' . $platformPubKey; // 4. 生成MAC签名(32位小写MD5) $mac = strtolower(md5($macSignString)); // 5. 构建不参与MAC的参数 $nonMacParams = []; // 微信支付19位终端号(不参与MAC校验) if (!empty($this->config['pos_id_19'])) { $nonMacParams['POSID19'] = $this->config['pos_id_19']; } // 场景编号(埋点使用,不参与MAC校验) if (!empty($this->config['scn_id'])) { $nonMacParams['SCNID'] = $this->config['scn_id']; } if (!empty($this->config['scn_pltfrm_id'])) { $nonMacParams['SCN_PLTFRM_ID'] = $this->config['scn_pltfrm_id']; } // 6. 生成ENCPUB(商户公钥密文,不参与MAC校验) $encpub = $this->encryptPublicKeyLast30(); // 7. 组装最终支付串 // 格式:参与MAC的参数 + 不参与MAC的参数 + MAC + PLATFORMID + ENCPUB $finalPaymentString = $signString; // 添加不参与MAC的参数 if (!empty($nonMacParams)) { $finalPaymentString .= '&' . http_build_query($nonMacParams); } // 添加MAC、PLATFORMID、ENCPUB $finalPaymentString .= '&MAC=' . $mac . '&PLATFORMID=' . $this->config['service_id'] . '&ENCPUB=' . urlencode($encpub); // 保存支付流水号到订单 Order::where('id', $orderId)->update([ 'ccb_pay_flow_id' => $payFlowId, 'updatetime' => time() ]); // 记录支付请求 $this->recordPaymentRequest($orderId, [ 'payment_string' => $finalPaymentString, 'params' => $paymentParams, 'mac' => $mac, 'pay_flow_id' => $payFlowId ]); // ✅ 提交事务 Db::commit(); return [ 'status' => true, 'message' => '支付串生成成功', 'data' => [ 'payment_string' => $finalPaymentString, 'mac' => $mac, 'order_sn' => $order['order_sn'], 'pay_flow_id' => $payFlowId, 'amount' => number_format($order['pay_fee'], 2, '.', ''), 'order_id' => $orderId // 补充order_id字段 ] ]; } catch (\Exception $e) { // ❌ 回滚事务 Db::rollback(); Log::error('建行支付串生成失败: ' . $e->getMessage()); return [ 'status' => false, 'message' => $e->getMessage(), 'data' => null ]; } } /** * 获取客户端IP * * @return string */ private function getClientIp() { try { $ip = request()->ip(); return $ip ?: '127.0.0.1'; } catch (\Exception $e) { // CLI模式或其他异常情况,使用默认值 return '127.0.0.1'; } } /** * 实现JavaScript的escape()编码 * 用于REGINFO和PROINFO字段的中文编码 * * 根据建行文档4.3: * "使用js的escape()方法对REGINFO(客户注册信息)和PROINFO(商品信息)进行编码" * * @param string $str 要编码的字符串 * @return string 编码后的字符串 */ private function jsEscape($str) { if (empty($str)) { return ''; } $result = ''; $length = mb_strlen($str, 'UTF-8'); for ($i = 0; $i < $length; $i++) { $char = mb_substr($str, $i, 1, 'UTF-8'); // ASCII字符(数字、字母、部分符号)不编码 if (preg_match('/^[A-Za-z0-9@*_+\-.\\/]$/', $char)) { $result .= $char; } else { // 非ASCII字符转为 %uXXXX 格式(如:小 -> %u5C0F) $unicode = mb_ord($char, 'UTF-8'); $result .= '%u' . strtoupper(str_pad(dechex($unicode), 4, '0', STR_PAD_LEFT)); } } return $result; } /** * 构建商品信息字符串 * 根据建行文档4.3:商品信息中文需使用escape()编码 * * @param object $order 订单对象 * @return string escape编码后的商品信息 */ private function buildProductInfo($order) { // 获取订单商品 $orderItems = Db::name('shopro_order_item') ->where('order_id', $order['id']) ->limit(3) // 最多取3个商品 ->column('goods_title'); if (empty($orderItems)) { $productInfo = '商城订单'; } else { // 拼接商品名称,建议不超过50字符(编码前) $productInfo = mb_substr(implode(',', $orderItems), 0, 50, 'UTF-8'); } // ✅ 使用JavaScript的escape()编码中文 return $this->jsEscape($productInfo); } /** * 加密商户公钥后30位(用于支付串的ENCPUB字段) * * 根据建行文档v2.2规范: * "使用服务方公钥对商户公钥后30位进行RSA加密并base64后的密文" * * @return string BASE64编码的加密密文 * @throws \Exception */ private function encryptPublicKeyLast30() { $publicKey = $this->config['public_key'] ?? ''; if (empty($publicKey)) { throw new \Exception('服务方公钥未配置'); } // 去除 PEM 格式的头尾和空白字符,获取纯 BASE64 内容 $publicKeyContent = str_replace([ '-----BEGIN PUBLIC KEY-----', '-----END PUBLIC KEY-----', '-----BEGIN RSA PUBLIC KEY-----', '-----END RSA PUBLIC KEY-----', "\r", "\n", " ", "\t" ], '', $publicKey); // 取后30位 $last30Chars = substr($publicKeyContent, -30); if (strlen($last30Chars) < 30) { throw new \Exception('商户公钥长度不足30位'); } // ✅ 使用 CcbRSA 类进行加密(统一密钥格式化逻辑) return CcbRSA::encrypt($last30Chars, $publicKey); } /** * 处理支付回调 * 建行支付完成后的同步回调 * * @param array $params URL参数 * @return array */ public function handleCallback($params) { try { // 解密ccbParamSJ参数(使用服务方私钥) if (isset($params['ccbParamSJ'])) { $decryptedParams = CcbUrlDecrypt::decrypt($params['ccbParamSJ'], $this->config['private_key']); if ($decryptedParams) { $params = array_merge($params, $decryptedParams); } } // 获取关键参数 $payFlowId = $params['ORDERID'] ?? ''; // 支付流水号 $userOrderId = $params['USER_ORDERID'] ?? ''; // 商户订单号 $posId = $params['POSID'] ?? ''; $success = $params['SUCCESS'] ?? 'N'; // 验证参数 if (empty($payFlowId)) { throw new \Exception('支付流水号不能为空'); } // 验证POS号 if ($posId != $this->config['pos_id']) { throw new \Exception('POS号验证失败'); } // ⚠️ 重要:ORDERID是支付流水号,不是订单号! // 优先使用USER_ORDERID查询,如果没有则用ccb_pay_flow_id查询 if (!empty($userOrderId)) { $order = Order::where('order_sn', $userOrderId)->find(); } else { $order = Order::where('ccb_pay_flow_id', $payFlowId)->find(); } if (!$order) { throw new \Exception('订单不存在'); } // 处理支付结果 if ($success == 'Y') { // 支付成功,更新订单状态 $this->updateOrderPaymentStatus($order, $params); // ✅ 更新订单状态到建行(订单已在createPayment时推送,这里只需更新状态) $this->orderService->updateOrderStatus($order['id']); return [ 'status' => true, 'message' => '支付成功', 'data' => [ 'order_id' => $order['id'], 'order_sn' => $order['order_sn'], // ✅ 返回真正的订单号 'pay_flow_id' => $payFlowId, // 支付流水号 'amount' => $params['PAYMENT'] ?? '' ] ]; } else { // 支付失败 return [ 'status' => false, 'message' => '支付失败', 'data' => [ 'order_id' => $order['id'], 'order_sn' => $order['order_sn'], // ✅ 返回真正的订单号 'pay_flow_id' => $payFlowId, // 支付流水号 'error_code' => $params['ERRCODE'] ?? '', 'error_msg' => $params['ERRMSG'] ?? '' ] ]; } } catch (\Exception $e) { Log::error('建行支付回调处理失败: ' . $e->getMessage()); return [ 'status' => false, 'message' => $e->getMessage(), 'data' => null ]; } } /** * 处理异步通知 * 建行支付异步通知处理 * * ⚠️ 注意:这是唯一可信的支付确认来源! * 返回订单ID供控制器调用pushOrderToCcb()推送到外联系统 * * @param array $params 通知参数 * @return array ['status' => 'success'|'fail', 'order_id' => int, 'order_sn' => string] */ public function handleNotify($params) { try { // 验证签名 if (!$this->verifyNotifySignature($params)) { throw new \Exception('签名验证失败'); } // ⚠️ 重要:ORDERID是支付流水号,不是订单号! // 优先使用USER_ORDERID查询,如果没有则用ccb_pay_flow_id查询 $payFlowId = $params['ORDERID'] ?? ''; // 支付流水号 $userOrderId = $params['USER_ORDERID'] ?? ''; // 商户订单号 if (!empty($userOrderId)) { $order = Order::where('order_sn', $userOrderId)->find(); } else { $order = Order::where('ccb_pay_flow_id', $payFlowId)->find(); } if (!$order) { throw new \Exception('订单不存在'); } // 如果订单已支付,直接返回成功(幂等性) if ($order['status'] == 'paid') { Log::info('[建行通知] 订单已支付,跳过处理 order_id:' . $order->id); return [ 'status' => 'success', 'order_id' => $order->id, 'order_sn' => $order->order_sn, 'already_paid' => true, // 标记为已支付 ]; } // 更新订单状态 $this->updateOrderPaymentStatus($order, $params); Log::info('[建行通知] 订单状态更新成功 order_id:' . $order->id . ' order_sn:' . $order->order_sn); // ✅ 返回订单ID供控制器推送到外联系统 return [ 'status' => 'success', 'order_id' => $order->id, 'order_sn' => $order->order_sn, 'already_paid' => false, // 新支付 ]; } catch (\Exception $e) { Log::error('[建行通知] 处理失败: ' . $e->getMessage()); return [ 'status' => 'fail', 'message' => $e->getMessage(), ]; } } /** * 验证支付结果 * 主动查询订单支付状态 * * @param string $orderSn 订单号 * @return bool */ public function verifyPayment($orderSn) { try { // 查询建行订单状态 $result = $this->orderService->queryOrder($orderSn); if ($result['status']) { $data = $result['data']['CLD_BODY'] ?? []; $txnStatus = $data['TXN_STATUS'] ?? ''; // 00=交易成功 return $txnStatus == '00'; } return false; } catch (\Exception $e) { Log::error('建行支付验证失败: ' . $e->getMessage()); return false; } } /** * 更新订单支付状态 * * @param object $order 订单对象 * @param array $params 支付参数 */ private function updateOrderPaymentStatus($order, $params) { // ⚠️ 重要字段说明: // 1. paid_time: Shopro使用毫秒时间戳(time() * 1000) // 2. pay_type: 建行支付暂用'offline'(建行线下银行支付),后续可扩展枚举 // 3. transaction_id: 存储建行支付流水号(ORDERID) Order::where('id', $order['id'])->update([ 'status' => 'paid', 'pay_type' => 'offline', // 建行支付归类为线下银行支付 'paid_time' => time() * 1000, // 毫秒时间戳 'transaction_id' => $params['ORDERID'] ?? '', // 建行支付流水号 'updatetime' => time() ]); // 记录支付日志 $this->recordPaymentLog($order['id'], 'payment_success', $params); } /** * 验证异步通知签名 * * ⚠️ 建行异步通知签名规则: * 1. SIGN字段为256字符十六进制字符串(2048位RSA签名) * 2. NT_TYPE=YS时,使用"建行生活分配的服务商支付验签公钥" * 3. 签名算法: RSA-SHA256或SHA1(需建行技术支持确认) * * 📌 配置说明: * - 如果配置了ccb_payment_verify_public_key: 使用RSA验签 * - 如果未配置: 降级为POSID验证(临时方案) * * @param array $params 通知参数 * @return bool */ private function verifyNotifySignature($params) { try { // 1. 提取SIGN字段 $sign = $params['SIGN'] ?? ''; if (empty($sign)) { Log::error('[建行验签] SIGN字段为空'); return false; } // 验证SIGN长度(256个十六进制字符 = 2048位RSA签名) if (strlen($sign) !== 256) { Log::error('[建行验签] SIGN长度错误: ' . strlen($sign) . ', 应为256'); return false; } // 2. 检查是否配置了建行支付验签公钥 $ccbVerifyPublicKey = $this->config['ccb_payment_verify_public_key'] ?? ''; if (empty($ccbVerifyPublicKey)) { // 降级方案: 未配置验签公钥时,使用POSID验证 Log::warning('[建行验签] 未配置ccb_payment_verify_public_key,使用降级验证方案'); // 验证POSID是否匹配 if (($params['POSID'] ?? '') !== $this->config['pos_id']) { Log::error('[建行验签] POSID不匹配,预期: ' . $this->config['pos_id'] . ', 实际: ' . ($params['POSID'] ?? '')); return false; } // 验证订单号是否存在 $orderSn = $params['USER_ORDERID'] ?? $params['ORDERID'] ?? ''; if (empty($orderSn)) { Log::error('[建行验签] 订单号为空'); return false; } Log::warning('[建行验签] 降级验证通过,建议联系建行技术支持获取验签公钥'); return true; } // 3. 移除SIGN字段,构建签名原串 $verifyParams = $params; unset($verifyParams['SIGN']); // 4. 按key排序 ksort($verifyParams); // 5. 构建签名原串(非空参数) $signStr = ''; foreach ($verifyParams as $key => $value) { // 跳过空值参数 if ($value !== '' && $value !== null) { $signStr .= $key . '=' . $value . '&'; } } $signStr = rtrim($signStr, '&'); Log::info('[建行验签] 签名原串: ' . $signStr); Log::info('[建行验签] 收到SIGN前32位: ' . substr($sign, 0, 32) . '...'); // 6. 将十六进制SIGN转为二进制 $signBinary = hex2bin($sign); if ($signBinary === false) { Log::error('[建行验签] SIGN十六进制转换失败'); return false; } // 7. 加载建行支付验签公钥 $pubKey = openssl_pkey_get_public($ccbVerifyPublicKey); if (!$pubKey) { Log::error('[建行验签] 验签公钥加载失败: ' . openssl_error_string()); return false; } // 8. 先尝试SHA256验签 $verifyResult = openssl_verify($signStr, $signBinary, $pubKey, OPENSSL_ALGO_SHA256); if ($verifyResult !== 1) { // SHA256失败,尝试SHA1 Log::info('[建行验签] SHA256验签失败,尝试SHA1'); $verifyResult = openssl_verify($signStr, $signBinary, $pubKey, OPENSSL_ALGO_SHA1); } // PHP 8+ 资源自动释放 if (PHP_VERSION_ID < 80000) { openssl_free_key($pubKey); } if ($verifyResult === 1) { Log::info('[建行验签] RSA验签成功'); return true; } elseif ($verifyResult === 0) { Log::error('[建行验签] RSA验签失败,签名不匹配'); return false; } else { Log::error('[建行验签] RSA验签过程发生错误: ' . openssl_error_string()); return false; } } catch (\Exception $e) { Log::error('[建行验签] 验签异常: ' . $e->getMessage()); return false; } } /** * 记录支付请求 * * ⚠️ 幂等性说明: * 1. 当用户多次点击支付按钮时,会复用同一个pay_flow_id * 2. 本方法会先检查是否已存在记录,如果存在则更新,否则插入 * 3. 这样可以避免唯一键冲突,并记录最新的支付串生成时间 * * @param int $orderId 订单ID * @param array $paymentData 支付数据 */ private function recordPaymentRequest($orderId, $paymentData) { // 获取订单信息 $order = Order::find($orderId); $user = Db::name('user')->where('id', $order['user_id'])->field('ccb_user_id')->find(); $payFlowId = $paymentData['pay_flow_id'] ?? ''; // 检查是否已存在记录(根据pay_flow_id唯一键) $existingLog = Db::name('ccb_payment_log') ->where('pay_flow_id', $payFlowId) ->find(); $logData = [ 'order_id' => $orderId, 'order_sn' => $order['order_sn'], 'payment_string' => $paymentData['payment_string'] ?? '', 'user_id' => $order['user_id'], 'ccb_user_id' => $user['ccb_user_id'] ?? '', 'amount' => $order['pay_fee'], // 使用Shopro的pay_fee字段 'status' => 0, // 待支付 ]; if ($existingLog) { // 已存在记录,更新支付串(保留原create_time,重置status为待支付) // 注意:用户重复点击支付时,会生成新的支付串,需要更新 Db::name('ccb_payment_log') ->where('pay_flow_id', $payFlowId) ->update([ 'payment_string' => $logData['payment_string'], 'status' => 0, // 重置为待支付(因为是新的支付串) 'amount' => $logData['amount'], // 更新金额(订单金额可能变化) ]); Log::info('[建行支付] 更新支付日志 pay_flow_id:' . $payFlowId . ' order_id:' . $orderId); } else { // 不存在记录,插入新记录 $logData['pay_flow_id'] = $payFlowId; $logData['create_time'] = time(); Db::name('ccb_payment_log')->insert($logData); Log::info('[建行支付] 插入支付日志 pay_flow_id:' . $payFlowId . ' order_id:' . $orderId); } } /** * 记录支付日志 * * @param int $orderId 订单ID * @param string $type 日志类型 * @param array $data 数据 */ private function recordPaymentLog($orderId, $type, $data) { // 更新建行支付日志 if ($type == 'payment_success') { Db::name('ccb_payment_log') ->where('order_id', $orderId) ->update([ 'status' => 1, // 支付成功 'pay_time' => time(), 'trans_id' => $data['ORDERID'] ?? '', 'callback_data' => json_encode($data, JSON_UNESCAPED_UNICODE) ]); } } /** * 生成退款申请 * * @param int $orderId 订单ID * @param float $refundAmount 退款金额 * @param string $refundReason 退款原因 * @return array */ public function refund($orderId, $refundAmount, $refundReason = '') { try { // 调用订单服务处理退款 $result = $this->orderService->refundOrder($orderId, $refundAmount, $refundReason); if ($result['status']) { // 记录退款日志 $this->recordPaymentLog($orderId, 'refund_request', [ 'amount' => $refundAmount, 'reason' => $refundReason ]); } return $result; } catch (\Exception $e) { Log::error('建行退款申请失败: ' . $e->getMessage()); return [ 'status' => false, 'message' => $e->getMessage(), 'data' => null ]; } } }