config = include $configFile; } else { throw new \Exception('建行生活配置文件不存在'); } // ✅ 修复: 删除processPemKeys()调用 // 密钥格式化统一由CcbRSA类处理,避免重复格式化导致OpenSSL ASN1解析错误 // CcbRSA::formatPublicKey/formatPrivateKey 会在加密/解密时自动处理密钥格式 $this->httpClient = new CcbHttpClient($this->config); } /** * 推送订单到建行生活平台 * 当用户下单后调用此方法同步订单信息 * * @param int $orderId Shopro订单ID * @param string $payFlowId 支付流水号(由控制器统一生成) * @return array ['status' => bool, 'message' => string, 'data' => array] * @throws \Exception */ public function pushOrder($orderId, $payFlowId) { $startTime = microtime(true); try { // ✅ 验证支付流水号 if (empty($payFlowId)) { throw new \Exception('支付流水号不能为空'); } // 获取订单信息 $order = Db::name('shopro_order') ->alias('o') ->join('user u', 'o.user_id = u.id', 'LEFT') ->where('o.id', $orderId) ->field('o.*, u.ccb_user_id') ->find(); if (!$order) { throw new \Exception('订单不存在'); } // 获取建行用户ID $ccbUserId = $order['ccb_user_id']; if (!$ccbUserId) { throw new \Exception('用户未绑定建行生活账号'); } // 获取订单商品列表 $orderItems = Db::name('shopro_order_item') ->where('order_id', $orderId) ->select(); // 构建订单数据(符合A3341TP01接口规范) // ✅ 传入统一的支付流水号 $orderData = $this->buildOrderData($order, $orderItems, $ccbUserId, $payFlowId); // 记录请求数据(同步日志) $txSeq = CcbMD5::generateTransactionSeq(); $this->recordSyncLog($orderId, 'A3341TP01', $txSeq, $orderData, 'request'); // 调用建行API推送订单 $response = $this->httpClient->pushOrder($orderData); // 记录响应数据和耗时 $costTime = round((microtime(true) - $startTime) * 1000, 2); $this->recordSyncLog($orderId, 'A3341TP01', $txSeq, $response, 'response', true, $costTime); // 更新订单同步状态为成功(清空错误信息) $this->updateOrderSyncStatus($orderId, 1, ''); return [ 'status' => true, 'message' => '订单推送成功', 'data' => $response ]; } catch (\Exception $e) { // 记录错误 $costTime = round((microtime(true) - $startTime) * 1000, 2); $this->recordSyncLog($orderId, 'A3341TP01', '', null, 'error', false, $costTime, $e->getMessage()); // 更新同步状态为失败,并保存错误信息 $this->updateOrderSyncStatus($orderId, 2, $e->getMessage()); Log::error('建行订单推送失败: ' . $e->getMessage()); return [ 'status' => false, 'message' => $e->getMessage(), 'data' => null ]; } } /** * 更新订单状态到建行生活 * * @param int $orderId 订单ID * @param string|null $status 支付状态(0-待支付 1-支付成功 2-已过期 3-支付失败 4-取消) * @param string|null $refundStatus 退款状态(0-无退款 1-退款申请 2-已退款 3-部分退款) * @return array */ public function updateOrderStatus($orderId, $status = 0, $refundStatus = 0) { $startTime = microtime(true); $txSeq = CcbMD5::generateTransactionSeq(); try { // 获取订单信息(包含支付流水号) $order = Db::name('shopro_order') ->alias('o') ->join('user u', 'o.user_id = u.id', 'LEFT') ->where('o.id', $orderId) ->field('o.*, u.ccb_user_id') ->find(); if (!$order) { throw new \Exception('订单不存在'); } // 获取支付流水号 $payFlowId = $order['ccb_pay_flow_id'] ?? null; if (empty($payFlowId)) { throw new \Exception('订单缺少支付流水号,无法更新状态到建行'); } // 获取支付商户号 $payMrchId = $this->config['merchant_id'] ?? null; if (empty($payMrchId)) { throw new \Exception('配置中缺少支付商户号(merchant_id)'); } // 映射订单状态 // 确定通知类型(0-支付状态修改 1-退款状态修改) $informId = !empty($refundStatus) && $refundStatus != '0' ? '1' : '0'; // 构建额外参数 $additionalParams = []; if ($informId == '0') { // 支付状态修改 $additionalParams['PAY_STATUS'] = $status; $additionalParams['PAY_AMT'] = number_format($order['pay_fee'] ?? 0, 2, '.', ''); } else { // 退款状态修改 $additionalParams['REFUND_STATUS'] = $refundStatus; } // 添加其他可选参数 $additionalParams['DISCOUNT_AMT'] = number_format($order['total_discount_fee'] ?? 0, 2, '.', ''); if (!empty($order['goods_name'])) { $additionalParams['GOODS_NM'] = mb_substr($order['goods_name'], 0, 200); } // 记录请求 $requestData = [ 'order_id' => $order['order_sn'], 'inform_id' => $informId, 'pay_flow_id' => $payFlowId, 'pay_mrch_id' => $payMrchId, 'additional_params' => $additionalParams ]; $this->recordSyncLog($orderId, 'A3341TP02', $txSeq, $requestData, 'request'); // 调用建行API更新状态(使用新接口) $response = $this->httpClient->updateOrderStatus( $order['order_sn'], // 订单编号 $informId, // 通知类型 $payFlowId, // 支付流水号 $payMrchId, // 支付商户号 $additionalParams // 额外参数 ); // 记录响应 $costTime = round((microtime(true) - $startTime) * 1000, 2); $this->recordSyncLog($orderId, 'A3341TP02', $txSeq, $response, 'response', true, $costTime); return [ 'status' => true, 'message' => '订单状态更新成功', 'data' => $response ]; } catch (\Exception $e) { $costTime = round((microtime(true) - $startTime) * 1000, 2); $this->recordSyncLog($orderId, 'A3341TP02', $txSeq, null, 'error', false, $costTime, $e->getMessage()); Log::error('建行订单状态更新失败: ' . $e->getMessage()); return [ 'status' => false, 'message' => $e->getMessage(), 'data' => null ]; } } /** * 查询建行订单信息 * * @param string $orderSn 订单号(支付流水号,对应收银台ORDERID字段) * @param string|null $startTime 开始时间(格式yyyyMMddHHmmss,默认7天前) * @param string|null $endTime 结束时间(格式yyyyMMddHHmmss,默认当前时间) * @param int $page 页码(默认1) * @param string $txType 交易类型(0-支付交易 1-退款交易 a-查询可退款订单) * @param string $txnStatus 交易状态(00-成功 01-失败 02-不确定) * @return array */ public function queryOrder($orderSn, $startTime = null, $endTime = null, $page = 1, $txType = '0', $txnStatus = '00') { try { $additionalParams = [ 'CUSTOMERID' => $this->config['merchant_id'], // ✅ 改为 CUSTOMERID 'BRANCHID' => $this->config['branch_id'] // ✅ 保持 BRANCHID ]; // 调用建行API查询订单(使用新接口) $response = $this->httpClient->queryOrder( $orderSn, $startTime, $endTime, $page, $txType, $txnStatus, $additionalParams // 传递商户信息 ); return [ 'status' => true, 'message' => '订单查询成功', 'data' => $response ]; } catch (\Exception $e) { Log::error('建行订单查询失败: ' . $e->getMessage()); return [ 'status' => false, 'message' => $e->getMessage(), 'data' => null ]; } } /** * 处理订单退款 * * @param int $orderId 订单ID * @param float $refundAmount 退款金额(单位:元) * @param string|null $refundCode 退款流水号(可选,建议填写用于查询退款结果) * @return array */ public function refundOrder($orderId, $refundAmount, $refundCode = null) { try { // 获取订单信息(需要支付流水号和支付时间) $order = Db::name('shopro_order')->where('id', $orderId)->find(); if (!$order) { throw new \Exception('订单不存在'); } // 验证退款金额 if ($refundAmount > $order['order_amount']) { throw new \Exception('退款金额不能超过订单总额'); } // 获取支付流水号(必须) // ✅ 修复:订单表字段名是 ccb_pay_flow_id,不是 pay_flow_id $payFlowId = $order['ccb_pay_flow_id'] ?? null; if (empty($payFlowId)) { throw new \Exception('订单缺少支付流水号,无法执行退款'); } // 获取支付时间(用于计算查询时间范围) $payTime = $order['pay_time'] ?? $order['createtime']; if (empty($payTime)) { throw new \Exception('订单缺少支付时间,无法执行退款'); } // ✅ 修复:添加商户信息参数(与查询接口保持一致) // 根据建行文档 A3341TP04 退款接口规范: // - CUSTOMERID: 建行商户编号(必填) // - BRANCHID: 商户一级分行号(必填,与 CUSTOMERID 配合使用) $additionalParams = [ 'CUSTOMERID' => $this->config['merchant_id'], 'BRANCHID' => $this->config['branch_id'] ]; // 调用建行API发起退款(使用新接口) $response = $this->httpClient->refund( $payFlowId, // 支付流水号(对应收银台ORDERID) $refundAmount, // 退款金额 $payTime, // 支付时间(用于计算查询时间范围) $refundCode, // 退款流水号(可选) $additionalParams // ✅ 添加商户信息 ); // 更新订单退款状态 $this->updateOrderStatus($orderId, 0, '2'); return [ 'status' => true, 'message' => '退款申请成功', 'data' => $response ]; } catch (\Exception $e) { Log::error('建行订单退款失败: ' . $e->getMessage()); return [ 'status' => false, 'message' => $e->getMessage(), 'data' => null ]; } } /** * 构建符合建行 A3341TP01 接口规范的订单数据 * * 📋 建行生活订单推送接口规范说明(v1.1.6): * * 必填字段(11个): * - USER_ID: 客户编号(建行用户ID) * - ORDER_ID: 订单号 * - ORDER_DT: 订单日期(yyyyMMddHHmmss格式) * - TOTAL_AMT: 订单原金额 * - ORDER_STATUS: 订单状态 * - REFUND_STATUS: 退款状态 * - MCT_NM: 商户名称 * - CUS_ORDER_URL: 订单详情链接 * - PAY_FLOW_ID: 支付流水号 * - PAY_MRCH_ID: 支付商户号 * - SKU_LIST: 商品信息JSON字符串 * * 重要可选字段(建议必填): * - PAY_AMT: 订单实际支付金额(文档要求:如为空必须在状态变更时推送) * - DISCOUNT_AMT: 第三方平台优惠金额(文档要求:如为空必须在状态变更时推送) * - DISCOUNT_AMT_DESC: 第三方平台优惠说明 * - INV_DT: 订单过期日期 * - GOODS_NM: 商品名称 * - PREFTL_MRCH_ID: 门店商户号 * - PLAT_MCT_ID: 服务商门店编号 * - PLAT_ORDER_TYPE: 服务方订单类型 * - PLATFORM: 下单场景 * * ⚠️ 注意:Shopro字段映射 * - pay_fee → PAY_AMT(实际支付金额) * - order_amount → TOTAL_AMT(订单总金额) * - total_discount_fee → DISCOUNT_AMT(优惠总金额) * - createtime → ORDER_DT(毫秒时间戳需除以1000) * - expiry_time → INV_DT(过期时间) * * @param array $order 订单数组 * @param array $orderItems 订单商品列表 * @param string $ccbUserId 建行用户ID * @return array */ private function buildOrderData($order, $orderItems, $ccbUserId, $payFlowId) { // ✅ 使用控制器传入的统一支付流水号(确保与支付串生成使用同一流水号) if (empty($payFlowId)) { throw new \Exception('支付流水号不能为空'); } // 构建SKU商品列表(JSON字符串格式) $skuList = $this->buildSkuList($orderItems); // 计算各项金额(保留2位小数) $totalAmount = number_format($order['order_amount'] ?? 0, 2, '.', ''); $payAmount = number_format($order['pay_fee'] ?? $order['order_amount'] ?? 0, 2, '.', ''); $discountAmount = number_format($order['total_discount_fee'] ?? 0, 2, '.', ''); $totalRefundAmount = number_format($order['refund_fee'] ?? 0, 2, '.', ''); // 处理订单时间(自动判断秒级或毫秒级时间戳) $createTimeValue = $order['createtime'] ?? null; if (empty($createTimeValue) || !is_numeric($createTimeValue)) { $createTimeValue = time(); // 使用当前秒级时间戳 } // 判断时间戳类型:大于9999999999说明是毫秒级(13位数),否则是秒级(10位数) if ($createTimeValue > 9999999999) { // 毫秒级时间戳,除以1000转为秒 $timestamp = intval($createTimeValue / 1000); } else { // 秒级时间戳,直接使用 $timestamp = intval($createTimeValue); } $orderDt = date('YmdHis', $timestamp); // 处理订单过期时间 $invDt = ''; if (!empty($order['expiry_time'])) { // Shopro 的 expiry_time 可能是时间戳或日期字符串 if (is_numeric($order['expiry_time'])) { // 如果是毫秒时间戳,需要除以1000 $timestamp = intval($order['expiry_time']); if ($timestamp > 9999999999) { $timestamp = intval($timestamp / 1000); } $invDt = date('YmdHis', $timestamp); } else { $invDt = date('YmdHis', strtotime($order['expiry_time'])); } } // 获取商品名称(取第一个商品) $goodsName = ''; if (!empty($orderItems)) { $goodsName = $orderItems[0]['goods_title'] ?? ''; // 如果有多个商品,可以拼接 if (count($orderItems) > 1) { $goodsName .= ' 等' . count($orderItems) . '件商品'; } } // 构建优惠说明(如果有优惠金额) $discountAmtDesc = ''; if ($discountAmount > 0) { // 格式:名称=金额|@|名称=金额 // 这里简化处理,实际应该根据具体优惠券信息构建 $discountAmtDesc = '平台优惠=' . $discountAmount; } // 构建符合A3341TP01接口规范的订单数据 $orderData = [ // ========== 必填字段 ========== 'USER_ID' => $ccbUserId, // 客户编号 'ORDER_ID' => $order['order_sn'], // 订单号 'ORDER_DT' => $orderDt, // 订单日期(yyyyMMddHHmmss) 'TOTAL_AMT' => $totalAmount, // 订单原金额 'ORDER_STATUS' => 0, // 订单状态 'REFUND_STATUS' => 0, // 退款状态 'MCT_NM' => $this->config['merchant']['name'] ?? '商户名称', // 商户名称 'PAY_FLOW_ID' => $payFlowId, // ✅ 支付流水号(使用控制器传入的统一流水号) 'PAY_MRCH_ID' => $this->config['merchant_id'], // 支付商户号(必填!) 'SKU_LIST' => $skuList, // 商品信息JSON字符串(必填!) // ========== 重要可选字段(强烈建议填写) ========== 'PAY_AMT' => $payAmount, // 订单实际支付金额 'DISCOUNT_AMT' => $discountAmount, // 第三方平台优惠金额 'PLAT_ORDER_TYPE' => 'T0000', // 服务方订单类型(T0000-普通类型) 'PLATFORM' => '99', // 下单场景(99-建行生活APP) ]; // ========== 条件可选字段(有值才添加) ========== // 优惠说明 if (!empty($discountAmtDesc)) { $orderData['DISCOUNT_AMT_DESC'] = $discountAmtDesc; } // 订单过期时间 if (!empty($invDt)) { $orderData['INV_DT'] = $invDt; } // 商品名称 if (!empty($goodsName)) { $orderData['GOODS_NM'] = mb_substr($goodsName, 0, 200); // 限制长度200字符 } // 累计退款金额(如果有退款) if ($totalRefundAmount > 0) { $orderData['TOTAL_REFUND_AMT'] = $totalRefundAmount; } return $orderData; } /** * 构建符合建行规范的SKU商品列表(JSON字符串格式) * * 📋 建行 SKU_LIST 字段规范: * * 必填字段(4个): * - SKU_NAME: 商品名称(必填) * - SKU_REF_PRICE: 商品参考价(必填,支持小数最多2位) * - SKU_NUM: 商品数量(必填,支持小数最多1位) * - SKU_SELL_PRICE: 商品售价(必填,支持小数最多2位) * * ⚠️ 注意:Shopro字段映射 * - goods_title → SKU_NAME(商品名称) * - goods_original_price → SKU_REF_PRICE(商品原价作为参考价) * - goods_num → SKU_NUM(购买数量) * - goods_price → SKU_SELL_PRICE(商品实际售价) * * @param array $items 订单商品项数组 * @return string JSON字符串格式的SKU列表 */ private function buildSkuList($items) { $skuList = []; foreach ($items as $item) { $skuList[] = [ 'SKU_NAME' => $item['goods_title'], // 商品名称(必填) 'SKU_REF_PRICE' => number_format($item['goods_original_price'] ?? $item['goods_price'], 2, '.', ''), // 商品参考价(必填) 'SKU_NUM' => $item['goods_num'], // 商品数量(必填) 'SKU_SELL_PRICE' => number_format($item['goods_price'], 2, '.', ''), // 商品售价(必填) ]; } // 返回JSON字符串(不转义Unicode,保持中文可读) return json_encode($skuList, JSON_UNESCAPED_UNICODE); } /** * 记录同步日志 * * @param int $orderId 订单ID * @param string $txCode 交易代码 * @param string $txSeq 交易流水号 * @param mixed $data 数据 * @param string $type 类型:request/response/error * @param bool $success 是否成功 * @param float $costTime 耗时(毫秒) * @param string $errorMsg 错误信息 */ private function recordSyncLog($orderId, $txCode, $txSeq, $data, $type = 'request', $success = true, $costTime = 0, $errorMsg = '') { try { // 获取订单号 $orderSn = Db::name('shopro_order')->where('id', $orderId)->value('order_sn'); $logData = [ 'order_id' => $orderId, 'order_sn' => $orderSn ?: '', 'tx_code' => $txCode, 'tx_seq' => $txSeq, 'sync_status' => $success ? 1 : 0, 'sync_time' => time(), 'cost_time' => intval($costTime), 'retry_times' => 0 ]; if ($type == 'request') { $logData['request_data'] = is_array($data) ? json_encode($data, JSON_UNESCAPED_UNICODE) : $data; } elseif ($type == 'response') { $logData['response_data'] = is_array($data) ? json_encode($data, JSON_UNESCAPED_UNICODE) : $data; } elseif ($type == 'error') { $logData['error_msg'] = $errorMsg; } Db::name('ccb_sync_log')->insert($logData); } catch (\Exception $e) { Log::error('记录同步日志失败: ' . $e->getMessage()); } } /** * 更新订单同步状态 * * @param int $orderId 订单ID * @param int $status 同步状态:0-未同步 1-已同步 2-同步失败 * @param string $errorMsg 错误信息(失败时填写,成功时传空字符串清空) */ private function updateOrderSyncStatus($orderId, $status, $errorMsg = '') { $updateData = [ 'ccb_sync_status' => $status, 'ccb_sync_time' => time(), 'updatetime' => time() ]; // 根据状态处理错误信息 if ($status == 1) { // 同步成功,清空错误信息 $updateData['ccb_sync_error'] = ''; } elseif ($status == 2 && !empty($errorMsg)) { // 同步失败,保存错误信息(限制长度255字符) $updateData['ccb_sync_error'] = mb_substr($errorMsg, 0, 255, 'UTF-8'); } Db::name('shopro_order')->where('id', $orderId)->update($updateData); } }