config = include $configFile; } else { throw new \Exception('建行生活配置文件不存在'); } // 处理BASE64格式的密钥,添加PEM包装 $this->config = $this->processPemKeys($this->config); $this->orderService = new CcbOrderService(); } /** * 处理PEM格式密钥 * 如果密钥是BASE64格式(不含-----BEGIN-----),则添加PEM包装 * * @param array $config 配置数组 * @return array */ private function processPemKeys($config) { // 处理私钥 if (!empty($config['private_key']) && strpos($config['private_key'], '-----BEGIN') === false) { $config['private_key'] = "-----BEGIN PRIVATE KEY-----\n" . chunk_split($config['private_key'], 64, "\n") . "-----END PRIVATE KEY-----"; } // 处理公钥 if (!empty($config['public_key']) && strpos($config['public_key'], '-----BEGIN') === false) { $config['public_key'] = "-----BEGIN PUBLIC KEY-----\n" . chunk_split($config['public_key'], 64, "\n") . "-----END PUBLIC KEY-----"; } // 兼容merchant_public_key字段 if (empty($config['merchant_public_key'])) { $config['merchant_public_key'] = $config['public_key']; } // 处理平台公钥 if (!empty($config['platform_public_key'])) { // 如果有配置平台公钥且是BASE64格式,添加PEM包装 if (strpos($config['platform_public_key'], '-----BEGIN') === false) { $config['platform_public_key'] = "-----BEGIN PUBLIC KEY-----\n" . chunk_split($config['platform_public_key'], 64, "\n") . "-----END PUBLIC KEY-----"; } } else { // 如果没有配置平台公钥,使用商户公钥作为默认值 $config['platform_public_key'] = $config['public_key']; } return $config; } /** * 生成建行支付串 * 用于前端JSBridge调用建行收银台 * * ⚠️ 注意:必须包含所有必需参数,签名前按ASCII排序 * * @param int $orderId Shopro订单ID * @return array ['status' => bool, 'message' => string, 'data' => array] */ public function generatePaymentString($orderId) { 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('用户未绑定建行账号'); } // 生成支付流水号(使用订单号作为唯一标识) $payFlowId = 'PAY' . date('YmdHis') . mt_rand(100000, 999999); // 构建完整的支付参数(34个参数) $paymentParams = [ 'MERCHANTID' => $this->config['merchant_id'], // 商户代码 'POSID' => $this->config['pos_id'], // 柜台代码 'BRANCHID' => $this->config['branch_id'], // 分行代码 'ORDERID' => $payFlowId, // 支付流水号(必须唯一!) 'PAYMENT' => number_format($order['pay_fee'], 2, '.', ''), // 支付金额(Shopro使用pay_fee) 'CURCODE' => '01', // 币种(01=人民币) 'TXCODE' => '520100', // 交易码(520100=即时支付) 'REMARK1' => '', // 备注1 'REMARK2' => $this->config['service_id'], // 备注2(服务方编号) 'TYPE' => '1', // 支付类型(1=个人) 'GATEWAY' => '0', // 网关标志 'CLIENTIP' => $this->getClientIp(), // 客户端IP 'REGINFO' => '', // 注册信息 'PROINFO' => $this->buildProductInfo($order), // 商品信息 'REFERER' => '', // 来源页面 'THIRDAPPINFO' => 'comccbpay1234567890cloudmerchant', // 第三方应用信息(固定值) 'USER_ORDERID' => $order['order_sn'], // 商户订单号 'TIMEOUT' => date('YmdHis', strtotime('+30 minutes')) // 超时时间 ]; // 按ASCII排序 ksort($paymentParams); // 生成签名字符串 $signString = http_build_query($paymentParams); // ⚠️ 建行支付串签名规则(v2.2版本): // 1. PLATFORMPUB字段仅参与MD5计算,不作为HTTP参数传递 // 2. 签名 = MD5(参数字符串 + &PLATFORMPUB= + 服务方公钥内容) // 3. 生成32位大写MD5字符串(对照MD5Util.java第30行) $platformPubKey = $this->config['public_key']; // 服务方公钥 $mac = strtoupper(md5($signString . '&PLATFORMPUB=' . $platformPubKey)); // 使用RSA加密商户公钥后30位(用于ENCPUB字段) $encryption = new CcbEncryption($this->config); $encpub = $encryption->encryptMerchantPublicKeyLast30(); // 组装最终支付串 $finalPaymentString = $signString . '&MAC=' . $mac . '&PLATFORMID=' . $this->config['service_id'] . '&ENCPUB=' . urlencode($encpub); // 保存支付流水号到订单 Order::where('id', $orderId)->update([ 'ccb_pay_flow_id' => $payFlowId, 'updatetime' => time() ]); // 构建完整的支付URL $paymentUrl = $this->config['cashier_url'] . '?' . $finalPaymentString; // 记录支付请求 $this->recordPaymentRequest($orderId, [ 'payment_string' => $finalPaymentString, 'params' => $paymentParams, 'mac' => $mac, 'pay_flow_id' => $payFlowId ]); return [ 'status' => true, 'message' => '支付串生成成功', 'data' => [ 'payment_string' => $finalPaymentString, 'mac' => $mac, 'payment_url' => $paymentUrl, 'order_sn' => $order['order_sn'], 'pay_flow_id' => $payFlowId, 'amount' => number_format($order['pay_fee'], 2, '.', '') ] ]; } catch (\Exception $e) { 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'; } } /** * 构建商品信息字符串 * * @param object $order 订单对象 * @return string */ private function buildProductInfo($order) { // 获取订单商品 $orderItems = Db::name('shopro_order_item') ->where('order_id', $order['id']) ->limit(3) // 最多取3个商品 ->column('goods_title'); if (empty($orderItems)) { return '商城订单'; } // 拼接商品名称 return implode(',', $orderItems); } /** * 处理支付回调 * 建行支付完成后的同步回调 * * @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); // 同步订单到建行 $this->orderService->pushOrder($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 ]; } } /** * 处理异步通知 * 建行支付异步通知处理 * * @param array $params 通知参数 * @return string 'success' 或 'fail' */ 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') { return 'success'; } // 更新订单状态 $this->updateOrderPaymentStatus($order, $params); // 同步到建行 $this->orderService->pushOrder($order['id']); return 'success'; } catch (\Exception $e) { Log::error('建行支付异步通知处理失败: ' . $e->getMessage()); return 'fail'; } } /** * 验证支付结果 * 主动查询订单支付状态 * * @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; } } /** * 构建支付URL * * @param array $params 支付参数 * @param string $mac 签名 * @return string */ private function buildPaymentUrl($params, $mac) { // 添加必要参数 $params['MAC'] = $mac; $params['REMARK2'] = $this->config['service_id']; // 服务方编号 // 生成查询字符串 $queryString = http_build_query($params); // 返回完整URL(实际使用时通过JSBridge调用,不直接访问) return $this->config['cashier_url'] . '?' . $queryString; } /** * 更新订单支付状态 * * @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; } } /** * 记录支付请求 * * @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(); // 记录到建行支付日志表 Db::name('ccb_payment_log')->insert([ 'order_id' => $orderId, 'order_sn' => $order['order_sn'], 'pay_flow_id' => $paymentData['pay_flow_id'] ?? '', // ✅ 使用真实的支付流水号 '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, // 待支付 'create_time' => time() ]); } /** * 记录支付日志 * * @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 ]; } } }