From 8de5d880915bdefe7d2901f70b3106882359d6dc Mon Sep 17 00:00:00 2001 From: Billy <641833868@qq.com> Date: Tue, 21 Oct 2025 11:11:59 +0800 Subject: [PATCH] =?UTF-8?q?=E6=97=A5=E5=BF=97=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- addons/shopro/config/ccblife.php | 2 +- .../shopro/library/ccblife/CcbHttpClient.php | 42 +++++++ .../library/ccblife/CcbOrderService.php | 107 +++++++++++++++--- 3 files changed, 134 insertions(+), 17 deletions(-) diff --git a/addons/shopro/config/ccblife.php b/addons/shopro/config/ccblife.php index 8d98b0b..82d9e03 100644 --- a/addons/shopro/config/ccblife.php +++ b/addons/shopro/config/ccblife.php @@ -108,7 +108,7 @@ return [ // 商户信息 'merchant' => [ - 'name' => Env::get('ccb.merchant_name', '商户名称'), + 'name' => Env::get('ccb.merchant_name', '丰科贸易(荷西嘉园店)'), 'logo_url' => Env::get('ccb.merchant_logo', ''), 'order_detail_url' => Env::get('app_url', 'http://fengketrade.test') . '/pages/order/detail?id=', ], diff --git a/addons/shopro/library/ccblife/CcbHttpClient.php b/addons/shopro/library/ccblife/CcbHttpClient.php index 0d46db5..a7fe9a2 100644 --- a/addons/shopro/library/ccblife/CcbHttpClient.php +++ b/addons/shopro/library/ccblife/CcbHttpClient.php @@ -2,6 +2,8 @@ namespace addons\shopro\library\ccblife; +use think\Log; + /** * 建行生活HTTP客户端 * 处理与建行API的通信,包括加密、签名、发送请求和解密响应 @@ -48,6 +50,10 @@ class CcbHttpClient // 构建请求报文 $message = $this->buildMessage($txCode, $body, $txSeq); + // 📝 记录原始请求报文(加密前) + Log::info('建行生活API原始请求报文 [txCode=' . $txCode . '] [txSeq=' . $txSeq . ']'); + Log::info('原始报文内容: ' . $message); + // 加密报文 $encryptedMessage = CcbRSA::encryptForCcb($message, $this->config['public_key']); @@ -99,6 +105,9 @@ class CcbHttpClient */ private function sendHttpRequest($txCode, $cnt, $mac) { + // 记录请求开始时间 + $startTime = microtime(true); + // 构建完整的API URL(基础URL + ?txcode=交易代码) $apiUrl = $this->config['api_base_url'] . '?txcode=' . $txCode; @@ -108,6 +117,11 @@ class CcbHttpClient 'mac' => $mac ]; + // 📝 记录请求参数(加密后的完整内容) + Log::info('建行生活API加密请求参数 [txCode=' . $txCode . '] [url=' . $apiUrl . '] [timeout=' . self::DEFAULT_TIMEOUT . 's]'); + Log::info('加密参数 cnt: ' . $cnt); + Log::info('加密参数 mac: ' . $mac); + // 初始化CURL $ch = curl_init(); @@ -130,17 +144,32 @@ class CcbHttpClient $response = curl_exec($ch); $error = curl_error($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $curlInfo = curl_getinfo($ch); curl_close($ch); + // 计算请求耗时 + $costTime = round((microtime(true) - $startTime) * 1000, 2); // 毫秒 + // 检查错误 if ($error) { + // 📝 记录错误日志 + Log::error('建行生活API请求失败 [txCode=' . $txCode . '] [url=' . $apiUrl . '] [error=' . $error . '] [cost_time=' . $costTime . 'ms] [total_time=' . ($curlInfo['total_time'] ?? 0) . 's] [connect_time=' . ($curlInfo['connect_time'] ?? 0) . 's]'); throw new \Exception('HTTP请求失败: ' . $error); } if ($httpCode !== 200) { + // 📝 记录HTTP状态码异常日志 + $responsePreview = mb_substr($response, 0, 300); + Log::error('建行生活API响应状态码异常 [txCode=' . $txCode . '] [url=' . $apiUrl . '] [http_code=' . $httpCode . '] [cost_time=' . $costTime . 'ms] [response=' . $responsePreview . ']'); throw new \Exception('HTTP状态码异常: ' . $httpCode . ', 响应内容: ' . $response); } + // 📝 记录成功响应日志 + $totalTime = round(($curlInfo['total_time'] ?? 0) * 1000, 2); + $connectTime = round(($curlInfo['connect_time'] ?? 0) * 1000, 2); + $responseLength = mb_strlen($response); + Log::info('建行生活API请求成功 [txCode=' . $txCode . '] [url=' . $apiUrl . '] [http_code=' . $httpCode . '] [response_length=' . $responseLength . '] [cost_time=' . $costTime . 'ms] [total_time=' . $totalTime . 'ms] [connect_time=' . $connectTime . 'ms]'); + return $response; } @@ -153,6 +182,9 @@ class CcbHttpClient */ private function handleResponse($response) { + // 📝 记录原始响应内容 + Log::info('建行生活API原始响应内容: ' . $response); + // 解析JSON响应 $responseData = json_decode($response, true); if (json_last_error() !== JSON_ERROR_NONE) { @@ -164,15 +196,25 @@ class CcbHttpClient throw new \Exception('响应格式错误,缺少cnt或mac字段'); } + // 📝 记录加密响应参数 + Log::info('加密响应参数 cnt: ' . $responseData['cnt']); + Log::info('加密响应参数 mac: ' . $responseData['mac']); + // 解密响应内容 $decryptedContent = CcbRSA::decryptFromCcb($responseData['cnt'], $this->config['private_key']); + // 📝 记录解密后的响应内容 + Log::info('解密后响应内容: ' . $decryptedContent); + // 验证签名 $isValid = CcbMD5::verifyApiSignature($decryptedContent, $responseData['mac'], $this->config['private_key']); if (!$isValid) { + Log::error('响应签名验证失败 [expected_mac=' . $responseData['mac'] . ']'); throw new \Exception('响应签名验证失败'); } + Log::info('响应签名验证成功'); + // 解析解密后的内容 $decryptedData = json_decode($decryptedContent, true); if (json_last_error() !== JSON_ERROR_NONE) { diff --git a/addons/shopro/library/ccblife/CcbOrderService.php b/addons/shopro/library/ccblife/CcbOrderService.php index 8352489..86fe33b 100644 --- a/addons/shopro/library/ccblife/CcbOrderService.php +++ b/addons/shopro/library/ccblife/CcbOrderService.php @@ -272,7 +272,7 @@ class CcbOrderService /** * 构建符合建行 A3341TP01 接口规范的订单数据 * - * 📋 建行生活订单推送接口规范说明: + * 📋 建行生活订单推送接口规范说明(v1.1.6): * * 必填字段(11个): * - USER_ID: 客户编号(建行用户ID) @@ -280,27 +280,30 @@ class CcbOrderService * - ORDER_DT: 订单日期(yyyyMMddHHmmss格式) * - TOTAL_AMT: 订单原金额 * - ORDER_STATUS: 订单状态 + * - REFUND_STATUS: 退款状态 * - MCT_NM: 商户名称 * - CUS_ORDER_URL: 订单详情链接 - * - PAY_FLOW_ID: 支付流水号(从 shopro_pay.pay_sn 获取) - * - PRPFTL_MRCH_ID: 门店商户号(使用 merchant_id) - * - PAY_MRCH_ID: 支付商户号(使用 merchant_id) + * - PAY_FLOW_ID: 支付流水号 + * - PAY_MRCH_ID: 支付商户号 * - SKU_LIST: 商品信息JSON字符串 * - * 非必填但推荐字段: - * - PAY_AMT: 订单实际支付金额 - * - DISCOUNT_AMT: 第三方平台优惠金额 + * 重要可选字段(建议必填): + * - PAY_AMT: 订单实际支付金额(文档要求:如为空必须在状态变更时推送) + * - DISCOUNT_AMT: 第三方平台优惠金额(文档要求:如为空必须在状态变更时推送) * - DISCOUNT_AMT_DESC: 第三方平台优惠说明 - * - REFUND_STATUS: 退款状态 - * - PAY_MODE: 支付方式 + * - INV_DT: 订单过期日期 + * - GOODS_NM: 商品名称 + * - PREFTL_MRCH_ID: 门店商户号 + * - PLAT_MCT_ID: 服务商门店编号 * - PLAT_ORDER_TYPE: 服务方订单类型 - * - COUPON_AMT: 优惠券金额 + * - 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 订单商品列表 @@ -309,10 +312,21 @@ class CcbOrderService */ private function buildOrderData($order, $orderItems, $ccbUserId) { + // ⚠️ 验证必填字段:PAY_FLOW_ID(支付流水号) + // 这个字段在 createPayment 时设置,推送订单前必须存在 + if (empty($order['ccb_pay_flow_id'])) { + throw new \Exception('订单支付流水号(ccb_pay_flow_id)不存在,请先调用createPayment生成支付串'); + } + // 构建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, '.', ''); + // 处理订单时间(Shopro的createtime是毫秒时间戳,需要除以1000) $createTimeValue = $order['createtime'] ?? null; if (empty($createTimeValue) || !is_numeric($createTimeValue)) { @@ -320,8 +334,42 @@ class CcbOrderService } $orderDt = date('YmdHis', intval($createTimeValue / 1000)); + // 处理订单过期时间 + $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接口规范的订单数据 - return [ + $orderData = [ // ========== 必填字段 ========== 'USER_ID' => $ccbUserId, // 客户编号 'ORDER_ID' => $order['order_sn'], // 订单号 @@ -330,13 +378,40 @@ class CcbOrderService 'ORDER_STATUS' => $this->mapOrderStatus($order['status']), // 订单状态 'REFUND_STATUS' => $this->mapRefundStatus($order['refund_status'] ?? 0), // 退款状态 'MCT_NM' => $this->config['merchant']['name'] ?? '商户名称', // 商户名称 - 'CUS_ORDER_URL' => $this->config['merchant']['order_detail_url'] . $order['id'], // 订单详情链接 - 'PAY_FLOW_ID' => $order['ccb_pay_flow_id'], // 支付流水号(必填!) - 'PAY_MRCH_ID' => $this->config['merchant_id'], // 支付商户号(必填!) + 'PAY_FLOW_ID' => $order['ccb_pay_flow_id'], // 支付流水号(必填!已在上方验证) + 'PAY_MRCH_ID' => $this->config['merchant_id'], // 支付商户号(必填!) 'SKU_LIST' => $skuList, // 商品信息JSON字符串(必填!) - // ========== 非必填字段 ========== - 'PLAT_ORDER_TYPE' => "T0000", // 服务方订单类型 + + // ========== 重要可选字段(强烈建议填写) ========== + '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; } /**