From 3b8735a09d5d729c1504b65d9ce507ad1b1e2c23 Mon Sep 17 00:00:00 2001 From: Billy <641833868@qq.com> Date: Wed, 22 Oct 2025 20:32:53 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9B=9E=E8=B0=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- addons/shopro/controller/Ccbpayment.php | 231 ++++++++++++++++-- .../shopro/library/ccblife/CcbHttpClient.php | 148 +++++++---- .../library/ccblife/CcbOrderService.php | 172 +++++++------ addons/shopro/library/ccblife/CcbRSA.php | 74 ++++++ 4 files changed, 480 insertions(+), 145 deletions(-) diff --git a/addons/shopro/controller/Ccbpayment.php b/addons/shopro/controller/Ccbpayment.php index 26149bd..256ff12 100644 --- a/addons/shopro/controller/Ccbpayment.php +++ b/addons/shopro/controller/Ccbpayment.php @@ -27,7 +27,7 @@ class Ccbpayment extends Common * 不需要登录的方法 (支付回调不需要登录) * @var array */ - protected $noNeedLogin = ['notify']; + protected $noNeedLogin = ['notify', 'refundNotify']; /** * 不需要权限的方法 @@ -206,12 +206,23 @@ class Ccbpayment extends Common * * @return void */ + /** + * 建行生活支付通知接口 + * + * 📋 接口说明(文档7.1): + * - 建行生活主动推送支付结果 + * - 不会附带服务方编号 + * - 通过 REMARK2 字段识别服务方 + * - SIGN 字段使用商户私钥签名,需用建行公钥验签 + * + * @return void + */ public function notify() { try { // 1. 获取原始请求数据 $rawData = file_get_contents('php://input'); - Log::info('[建行通知] 收到异步通知: ' . $rawData); + Log::info('[建行支付通知] 收到异步通知: ' . $rawData); // 2. 解析POST参数 $params = $this->request->post(); @@ -222,51 +233,243 @@ class Ccbpayment extends Common } // 4. 记录参数 - Log::info('[建行通知] 解析参数: ' . json_encode($params, JSON_UNESCAPED_UNICODE)); + Log::info('[建行支付通知] 解析参数: ' . json_encode($params, JSON_UNESCAPED_UNICODE)); // 5. 验证必需参数 if (empty($params['ORDERID'])) { - Log::error('[建行通知] 缺少ORDERID参数'); + Log::error('[建行支付通知] 缺少ORDERID参数'); exit('FAIL'); } - // 6. 调用支付服务处理通知(返回订单ID) + if (empty($params['SIGN'])) { + Log::error('[建行支付通知] 缺少SIGN签名'); + exit('FAIL'); + } + + if (empty($params['SUCCESS'])) { + Log::error('[建行支付通知] 缺少SUCCESS字段'); + exit('FAIL'); + } + + // 6. ✅ 验证签名(使用建行公钥验签) + try { + $signature = $params['SIGN']; + $verifyParams = $params; // 复制参数用于验签 + + // 加载建行公钥配置 + $configFile = __DIR__ . '/../config/ccblife.php'; + if (!file_exists($configFile)) { + throw new \Exception('建行生活配置文件不存在'); + } + $config = include $configFile; + + // 验签 + $verifyResult = \addons\shopro\library\ccblife\CcbRSA::verifyNotify( + $verifyParams, + $signature, + $config['ccb_public_key'] // 建行公钥 + ); + + if (!$verifyResult) { + Log::error('[建行支付通知] 签名验证失败 ORDERID:' . $params['ORDERID']); + exit('FAIL'); + } + + Log::info('[建行支付通知] 签名验证成功 ORDERID:' . $params['ORDERID']); + + } catch (\Exception $e) { + Log::error('[建行支付通知] 签名验证异常: ' . $e->getMessage()); + exit('FAIL'); + } + + // 7. 检查支付状态 + if ($params['SUCCESS'] !== 'Y') { + Log::warning('[建行支付通知] 支付未成功 ORDERID:' . $params['ORDERID'] . ' SUCCESS:' . $params['SUCCESS']); + exit('SUCCESS'); // ⚠️ 仍然返回SUCCESS,表示通知已接收 + } + + // 8. 调用支付服务处理通知(返回订单ID) $result = $this->paymentService->handleNotify($params); - // 7. ✅ 处理成功后更新订单状态到建行(步骤13:调用订单更新接口更新订单状态) + // 9. ✅ 处理成功后更新订单状态到建行(步骤13:调用订单更新接口更新订单状态) if ($result['status'] === 'success' && !empty($result['order_id'])) { // ⚠️ 只有新支付才更新,已支付的订单跳过更新 if ($result['already_paid'] === false) { try { // 调用订单更新接口,将订单状态从未支付更新为已支付 - $updateResult = $this->orderService->updateOrderStatus($result['order_id']); + $updateResult = $this->orderService->updateOrderStatus($result['order_id'], '1'); // 1-支付成功 if ($updateResult['status']) { - Log::info('[建行通知] 订单状态更新成功 order_id:' . $result['order_id']); + Log::info('[建行支付通知] 订单状态更新成功 order_id:' . $result['order_id']); } else { // ⚠️ 更新失败不影响本地支付状态,记录日志后续补推 - Log::warning('[建行通知] 订单状态更新失败(本地已支付) order_id:' . $result['order_id'] . ' error:' . $updateResult['message']); + Log::warning('[建行支付通知] 订单状态更新失败(本地已支付) order_id:' . $result['order_id'] . ' error:' . $updateResult['message']); } } catch (Exception $e) { // ⚠️ 更新异常不影响支付成功,记录日志后续补推 - Log::error('[建行通知] 订单状态更新异常(本地已支付) order_id:' . $result['order_id'] . ' error:' . $e->getMessage()); + Log::error('[建行支付通知] 订单状态更新异常(本地已支付) order_id:' . $result['order_id'] . ' error:' . $e->getMessage()); } } else { - Log::info('[建行通知] 订单已支付且已更新,跳过更新 order_id:' . $result['order_id']); + Log::info('[建行支付通知] 订单已支付且已更新,跳过更新 order_id:' . $result['order_id']); } } - // 8. 返回处理结果 + // 10. 返回处理结果 // ⚠️ 重要: 必须使用exit直接退出,防止ThinkPHP框架追加额外内容 // 建行要求返回纯文本 'SUCCESS' 或 'FAIL',任何额外字符都会导致建行认为通知失败 $response = ($result['status'] === 'success') ? 'SUCCESS' : 'FAIL'; - Log::info('[建行通知] 处理完成,返回: ' . $response); + Log::info('[建行支付通知] 处理完成,返回: ' . $response); // 直接退出,确保只输出SUCCESS/FAIL exit($response); } catch (Exception $e) { - Log::error('[建行通知] 处理失败 error:' . $e->getMessage()); + Log::error('[建行支付通知] 处理失败 error:' . $e->getMessage()); + + // 异常情况也要直接退出 + exit('FAIL'); + } + } + + /** + * 建行生活退款操作通知接口 + * + * 📋 接口说明(文档7.2): + * - 建行生活主动推送退款操作消息 + * - 仅推送少量信息(商户号、订单号、退款时间) + * - ⚠️ 不能用于退款结果判断,需通过 A3341TP03 查询接口获取详细信息 + * - SIGN 字段使用商户私钥签名,需用建行公钥验签 + * + * @return void + */ + public function refundNotify() + { + try { + // 1. 获取原始请求数据 + $rawData = file_get_contents('php://input'); + Log::info('[建行退款通知] 收到异步通知: ' . $rawData); + + // 2. 解析POST参数 + $params = $this->request->post(); + + // 3. 如果POST为空,尝试解析原始数据 + if (empty($params) && $rawData) { + parse_str($rawData, $params); + } + + // 4. 记录参数 + Log::info('[建行退款通知] 解析参数: ' . json_encode($params, JSON_UNESCAPED_UNICODE)); + + // 5. 验证必需参数 + if (empty($params['ORDERID'])) { + Log::error('[建行退款通知] 缺少ORDERID参数'); + exit('FAIL'); + } + + if (empty($params['SIGN'])) { + Log::error('[建行退款通知] 缺少SIGN签名'); + exit('FAIL'); + } + + if (empty($params['REFUND_DTM'])) { + Log::error('[建行退款通知] 缺少REFUND_DTM退款时间'); + exit('FAIL'); + } + + // 6. ✅ 验证签名(使用建行公钥验签) + try { + $signature = $params['SIGN']; + $verifyParams = $params; + + // 加载建行公钥配置 + $configFile = __DIR__ . '/../config/ccblife.php'; + if (!file_exists($configFile)) { + throw new \Exception('建行生活配置文件不存在'); + } + $config = include $configFile; + + // 验签 + $verifyResult = \addons\shopro\library\ccblife\CcbRSA::verifyNotify( + $verifyParams, + $signature, + $config['ccb_public_key'] // 建行公钥 + ); + + if (!$verifyResult) { + Log::error('[建行退款通知] 签名验证失败 ORDERID:' . $params['ORDERID']); + exit('FAIL'); + } + + Log::info('[建行退款通知] 签名验证成功 ORDERID:' . $params['ORDERID']); + + } catch (\Exception $e) { + Log::error('[建行退款通知] 签名验证异常: ' . $e->getMessage()); + exit('FAIL'); + } + + // 7. ⚠️ 重要提示:退款通知仅包含少量信息,不能用于退款结果判断 + // 需要通过 A3341TP03 订单查询接口获取详细的退款信息 + Log::info('[建行退款通知] 退款操作通知 ORDERID:' . $params['ORDERID'] . ' REFUND_DTM:' . $params['REFUND_DTM']); + + // 8. 调用订单查询接口获取详细退款信息 + try { + $orderSn = $params['ORDERID']; + $refundTime = $params['REFUND_DTM']; + + // 计算查询时间范围(退款时间前后1天) + $refundTimestamp = strtotime($refundTime); + $startTime = date('YmdHis', $refundTimestamp - 86400); // 前1天 + $endTime = date('YmdHis', $refundTimestamp + 86400); // 后1天 + + // 查询退款交易详情 + $queryResult = $this->orderService->queryOrder( + $orderSn, + $startTime, + $endTime, + 1, + '1', // 交易类型:1-退款交易 + '00' // 交易状态:00-成功 + ); + + if ($queryResult['status']) { + Log::info('[建行退款通知] 查询退款详情成功: ' . json_encode($queryResult['data'], JSON_UNESCAPED_UNICODE)); + + // 9. 根据查询结果处理本地订单退款状态 + // 查找本地订单 + $order = Db::name('shopro_order') + ->where('pay_flow_id', $orderSn) + ->find(); + + if ($order) { + // 更新订单退款状态到建行 + $updateResult = $this->orderService->updateOrderStatus($order['id'], null, '2'); // 2-已退款 + + if ($updateResult['status']) { + Log::info('[建行退款通知] 订单退款状态更新成功 order_id:' . $order['id']); + } else { + Log::warning('[建行退款通知] 订单退款状态更新失败 order_id:' . $order['id'] . ' error:' . $updateResult['message']); + } + } else { + Log::warning('[建行退款通知] 未找到本地订单 pay_flow_id:' . $orderSn); + } + } else { + Log::error('[建行退款通知] 查询退款详情失败: ' . $queryResult['message']); + } + + } catch (\Exception $e) { + Log::error('[建行退款通知] 查询退款详情异常: ' . $e->getMessage()); + // ⚠️ 查询失败不影响通知接收,仍然返回SUCCESS + } + + // 10. 返回处理结果 + // ⚠️ 重要: 必须使用exit直接退出,防止ThinkPHP框架追加额外内容 + Log::info('[建行退款通知] 处理完成,返回: SUCCESS'); + + // 直接退出,确保只输出SUCCESS + exit('SUCCESS'); + + } catch (Exception $e) { + Log::error('[建行退款通知] 处理失败 error:' . $e->getMessage()); // 异常情况也要直接退出 exit('FAIL'); diff --git a/addons/shopro/library/ccblife/CcbHttpClient.php b/addons/shopro/library/ccblife/CcbHttpClient.php index 412f660..4295d8e 100644 --- a/addons/shopro/library/ccblife/CcbHttpClient.php +++ b/addons/shopro/library/ccblife/CcbHttpClient.php @@ -321,85 +321,147 @@ class CcbHttpClient /** * 订单状态更新(A3341TP02) * - * @param string $userId 用户ID - * @param string $orderId 订单ID - * @param string $orderStatus 订单状态 - * @param string $refundStatus 退款状态 + * @param string $orderId 订单编号(用户订单号,对应收银台USER_ORDERID字段) + * @param string $informId 通知类型(0-支付状态修改 1-退款状态修改) + * @param string $payFlowId 支付流水号(对应收银台ORDERID字段) + * @param string $payMrchId 支付商户号 + * @param array $additionalParams 额外参数(PAY_STATUS、REFUND_STATUS、PAY_AMT、DISCOUNT_AMT、CUS_ORDER_URL等) * @return array 响应数据 * @throws \Exception */ - public function updateOrderStatus($userId, $orderId, $orderStatus, $refundStatus = '0') + public function updateOrderStatus($orderId, $informId, $payFlowId, $payMrchId, $additionalParams = []) { + // 验证通知类型 + if (!in_array($informId, ['0', '1'])) { + throw new \Exception('通知类型INFORM_ID必须为0(支付状态修改)或1(退款状态修改)'); + } + + // 验证互斥规则 + if ($informId == '0') { + // 支付状态修改时,PAY_STATUS必填,REFUND_STATUS为空 + if (empty($additionalParams['PAY_STATUS'])) { + throw new \Exception('支付状态修改时PAY_STATUS不能为空'); + } + $additionalParams['REFUND_STATUS'] = null; + } elseif ($informId == '1') { + // 退款状态修改时,REFUND_STATUS必填,PAY_STATUS为空 + if (empty($additionalParams['REFUND_STATUS'])) { + throw new \Exception('退款状态修改时REFUND_STATUS不能为空'); + } + $additionalParams['PAY_STATUS'] = null; + } + + // 构建请求体(必填字段) $body = [ - 'USER_ID' => $userId, 'ORDER_ID' => $orderId, - 'ORDER_STATUS' => $orderStatus, - 'REFUND_STATUS' => $refundStatus + 'INFORM_ID' => $informId, + 'PAY_FLOW_ID' => $payFlowId, + 'PAY_MRCH_ID' => $payMrchId ]; + // 合并额外参数 + $body = array_merge($body, $additionalParams); + + // 移除空值字段 + $body = array_filter($body, function($value) { + return $value !== null && $value !== ''; + }); + return $this->request($this->config['tx_codes']['order_update'], $body); } /** * 订单查询(A3341TP03) * - * @param string $onlnPyTxnOrdrId 支付订单ID - * @param string $txnStatus 交易状态 + * @param string $onlnPyTxnOrdrId 订单编号(调用收银台时支付流水号,对应字段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-不确定) + * @param array $additionalParams 额外参数(PLAT_MCT_ID、CUSTOMERID、BRANCHID、SCN_IDR等) * @return array 响应数据 * @throws \Exception */ - public function queryOrder($onlnPyTxnOrdrId, $txnStatus = '00') + public function queryOrder($onlnPyTxnOrdrId, $startTime = null, $endTime = null, $page = 1, $txType = '0', $txnStatus = '00', $additionalParams = []) { + // 默认查询最近7天 + if (empty($startTime)) { + $startTime = date('YmdHis', strtotime('-7 days')); + } + if (empty($endTime)) { + $endTime = date('YmdHis'); + } + + // 构建请求体(必填字段) $body = [ + 'TX_TYPE' => $txType, + 'TXN_PRD_TPCD' => '99', // 99-自定义时间段查询(文档要求) + 'STDT_TM' => $startTime, + 'EDDT_TM' => $endTime, 'ONLN_PY_TXN_ORDR_ID' => $onlnPyTxnOrdrId, - 'PAGE' => '1', - 'TXN_PRD_TPCD' => '06', 'TXN_STATUS' => $txnStatus, - 'TX_TYPE' => '0' + 'PAGE' => (string)$page ]; + // 合并额外参数(商户信息等) + $body = array_merge($body, $additionalParams); + + // 移除空值字段 + $body = array_filter($body, function($value) { + return $value !== null && $value !== ''; + }); + return $this->request($this->config['tx_codes']['order_query'], $body); } /** * 退款接口(A3341TP04) * - * @param string $orderId 订单ID - * @param string $refundAmount 退款金额 - * @param string $refundReason 退款原因 + * @param string $orderId 订单号(调用收银台时支付流水号,对应字段ORDERID) + * @param float|string $refundAmount 退款金额(单位:元) + * @param string|int $payTime 支付时间(时间戳或日期时间字符串) + * @param string|null $refundCode 退款流水号(可选,建议填写用于查询退款结果) + * @param array $additionalParams 额外参数(PLAT_MCT_ID、CUSTOMERID、BRANCHID等) * @return array 响应数据 * @throws \Exception */ - public function refund($orderId, $refundAmount, $refundReason = '') + public function refund($orderId, $refundAmount, $payTime, $refundCode = null, $additionalParams = []) { + // 计算时间范围(支付时间前后4小时) + $payTimestamp = is_numeric($payTime) ? $payTime : strtotime($payTime); + if (!$payTimestamp) { + throw new \Exception('支付时间格式错误,请传入时间戳或有效的日期时间字符串'); + } + + $stat_tm = date('YmdHis', $payTimestamp - 4*3600); // 支付时间往前4小时 + $edit_tm = date('YmdHis', min($payTimestamp + 4*3600, time())); // 支付时间往后4小时,但不超过当前时间 + + // 生成退款流水号(如果未提供) + if (empty($refundCode)) { + $refundCode = 'RF' . date('YmdHis') . str_pad(mt_rand(0, 9999), 4, '0', STR_PAD_LEFT); + } + + // 格式化退款金额(保留2位小数) + $refundAmount = number_format((float)$refundAmount, 2, '.', ''); + + // 构建请求体(必填字段) $body = [ - 'ORDER_ID' => $orderId, - 'REFUND_AMOUNT' => $refundAmount, - 'REFUND_REASON' => $refundReason, - 'REFUND_TIME' => date('YmdHis') + 'ORDER' => $orderId, // 注意:字段名是 ORDER,不是 ORDER_ID + 'MONEY' => $refundAmount, // 注意:字段名是 MONEY,不是 REFUND_AMOUNT + 'STDT_TM' => $stat_tm, + 'EDDT_TM' => $edit_tm, + 'REFUND_CODE' => $refundCode ]; + // 合并额外参数(商户信息等) + $body = array_merge($body, $additionalParams); + + // 移除空值字段 + $body = array_filter($body, function($value) { + return $value !== null && $value !== ''; + }); + return $this->request($this->config['tx_codes']['order_refund'], $body); } - - /** - * 测试连接 - * 使用查询接口测试连接是否正常 - * - * @return bool 是否连接成功 - */ - public function testConnection() - { - try { - // 使用一个不存在的订单号进行查询测试 - $this->queryOrder('TEST' . time()); - return true; - } catch (\Exception $e) { - // 如果是业务错误(订单不存在),说明连接正常 - if (strpos($e->getMessage(), '业务处理失败') !== false) { - return true; - } - return false; - } - } } diff --git a/addons/shopro/library/ccblife/CcbOrderService.php b/addons/shopro/library/ccblife/CcbOrderService.php index 2f6e0fc..391fc5d 100644 --- a/addons/shopro/library/ccblife/CcbOrderService.php +++ b/addons/shopro/library/ccblife/CcbOrderService.php @@ -128,8 +128,8 @@ class CcbOrderService * 更新订单状态到建行生活 * * @param int $orderId 订单ID - * @param string $status 订单状态 - * @param string $refundStatus 退款状态 + * @param string|null $status 支付状态(0-待支付 1-支付成功 2-已过期 3-支付失败 4-取消) + * @param string|null $refundStatus 退款状态(0-无退款 1-退款申请 2-已退款 3-部分退款) * @return array */ public function updateOrderStatus($orderId, $status = null, $refundStatus = null) @@ -138,7 +138,7 @@ class CcbOrderService $txSeq = CcbMD5::generateTransactionSeq(); try { - // 获取订单信息 + // 获取订单信息(包含支付流水号) $order = Db::name('shopro_order') ->alias('o') ->join('user u', 'o.user_id = u.id', 'LEFT') @@ -150,31 +150,60 @@ class CcbOrderService throw new \Exception('订单不存在'); } - // 获取建行用户ID - $ccbUserId = $order['ccb_user_id']; - if (!$ccbUserId) { - throw new \Exception('用户未绑定建行生活账号'); + // 获取支付流水号 + $payFlowId = $order['pay_flow_id'] ?? null; + if (empty($payFlowId)) { + throw new \Exception('订单缺少支付流水号,无法更新状态到建行'); + } + + // 获取支付商户号 + $payMrchId = $this->config['merchant_id'] ?? null; + if (empty($payMrchId)) { + throw new \Exception('配置中缺少支付商户号(merchant_id)'); } // 映射订单状态 - $orderStatus = $status ?: $this->mapOrderStatus($order['status']); - $refundStatus = $refundStatus ?: $this->mapRefundStatus($order['refund_status'] ?? 0); + $payStatus = $status ?: $this->mapOrderStatus($order['status']); + $mappedRefundStatus = $refundStatus ?: $this->mapRefundStatus($order['refund_status'] ?? 0); + + // 确定通知类型(0-支付状态修改 1-退款状态修改) + $informId = !empty($refundStatus) && $refundStatus != '0' ? '1' : '0'; + + // 构建额外参数 + $additionalParams = []; + if ($informId == '0') { + // 支付状态修改 + $additionalParams['PAY_STATUS'] = $payStatus; + $additionalParams['PAY_AMT'] = number_format($order['pay_fee'] ?? 0, 2, '.', ''); + } else { + // 退款状态修改 + $additionalParams['REFUND_STATUS'] = $mappedRefundStatus; + $additionalParams['TOTAL_REFUND_AMT'] = number_format($order['refund_fee'] ?? 0, 2, '.', ''); + } + + // 添加其他可选参数 + $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 = [ - 'ccb_user_id' => $ccbUserId, - 'order_sn' => $order['order_sn'], - 'order_status' => $orderStatus, - 'refund_status' => $refundStatus + '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更新状态 + // 调用建行API更新状态(使用新接口) $response = $this->httpClient->updateOrderStatus( - $ccbUserId, - $order['order_sn'], - $orderStatus, - $refundStatus + $order['order_sn'], // 订单编号 + $informId, // 通知类型 + $payFlowId, // 支付流水号 + $payMrchId, // 支付商户号 + $additionalParams // 额外参数 ); // 记录响应 @@ -203,14 +232,26 @@ class CcbOrderService /** * 查询建行订单信息 * - * @param string $orderSn 订单号 + * @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) + public function queryOrder($orderSn, $startTime = null, $endTime = null, $page = 1, $txType = '0', $txnStatus = '00') { try { - // 调用建行API查询订单 - $response = $this->httpClient->queryOrder($orderSn); + // 调用建行API查询订单(使用新接口) + $response = $this->httpClient->queryOrder( + $orderSn, + $startTime, + $endTime, + $page, + $txType, + $txnStatus + ); return [ 'status' => true, @@ -232,14 +273,14 @@ class CcbOrderService * 处理订单退款 * * @param int $orderId 订单ID - * @param float $refundAmount 退款金额 - * @param string $refundReason 退款原因 + * @param float $refundAmount 退款金额(单位:元) + * @param string|null $refundCode 退款流水号(可选,建议填写用于查询退款结果) * @return array */ - public function refundOrder($orderId, $refundAmount, $refundReason = '') + public function refundOrder($orderId, $refundAmount, $refundCode = null) { try { - // 获取订单信息 + // 获取订单信息(需要支付流水号和支付时间) $order = Db::name('shopro_order')->where('id', $orderId)->find(); if (!$order) { throw new \Exception('订单不存在'); @@ -250,11 +291,24 @@ class CcbOrderService throw new \Exception('退款金额不能超过订单总额'); } - // 调用建行API发起退款 + // 获取支付流水号(必须) + $payFlowId = $order['pay_flow_id'] ?? null; + if (empty($payFlowId)) { + throw new \Exception('订单缺少支付流水号,无法执行退款'); + } + + // 获取支付时间(用于计算查询时间范围) + $payTime = $order['pay_time'] ?? $order['createtime']; + if (empty($payTime)) { + throw new \Exception('订单缺少支付时间,无法执行退款'); + } + + // 调用建行API发起退款(使用新接口) $response = $this->httpClient->refund( - $order['order_sn'], - number_format($refundAmount, 2, '.', ''), - $refundReason + $payFlowId, // 支付流水号(对应收银台ORDERID) + $refundAmount, // 退款金额 + $payTime, // 支付时间(用于计算查询时间范围) + $refundCode // 退款流水号(可选) ); // 更新订单退款状态 @@ -559,62 +613,4 @@ class CcbOrderService if ($refundStatus == 2) return '2'; // 已退款 return '0'; } - - /** - * 批量同步订单 - * 用于初始化或定时同步 - * - * @param array $conditions 查询条件 - * @param int $limit 批量数量 - * @return array - */ - public function batchSync($conditions = [], $limit = 100) - { - $successCount = 0; - $failCount = 0; - $errors = []; - - try { - // 构建查询 - $query = Db::name('shopro_order'); - - if (!empty($conditions)) { - $query->where($conditions); - } - - // 查询需要同步的订单 - $orders = $query->where('status', '<>', 'cancelled') - ->where('ccb_sync_status', 'in', [0, 2]) // 未同步或同步失败的 - ->limit($limit) - ->select(); - - foreach ($orders as $order) { - $result = $this->pushOrder($order['id']); - if ($result['status']) { - $successCount++; - } else { - $failCount++; - $errors[] = "订单{$order['order_sn']}: {$result['message']}"; - } - } - - return [ - 'status' => true, - 'message' => "批量同步完成", - 'data' => [ - 'total' => count($orders), - 'success' => $successCount, - 'fail' => $failCount, - 'errors' => $errors - ] - ]; - - } catch (\Exception $e) { - return [ - 'status' => false, - 'message' => '批量同步失败: ' . $e->getMessage(), - 'data' => null - ]; - } - } } diff --git a/addons/shopro/library/ccblife/CcbRSA.php b/addons/shopro/library/ccblife/CcbRSA.php index a3ee74b..a6c24a4 100644 --- a/addons/shopro/library/ccblife/CcbRSA.php +++ b/addons/shopro/library/ccblife/CcbRSA.php @@ -239,4 +239,78 @@ class CcbRSA 'private_key' => $privateKey ]; } + + /** + * RSA公钥验签(用于建行回调通知) + * + * 建行回调通知中的 SIGN 字段是使用商户私钥签名的, + * 服务方需要使用建行公钥进行验签 + * + * @param string $data 待验签的原始数据 + * @param string $signature 签名字符串(十六进制) + * @param string $publicKey 建行公钥(BASE64编码) + * @return bool 验签是否成功 + * @throws \Exception + */ + public static function verify($data, $signature, $publicKey) + { + // 格式化公钥 + $publicKey = self::formatPublicKey($publicKey); + + // 加载公钥资源 + $pubKey = openssl_pkey_get_public($publicKey); + if (!$pubKey) { + throw new \Exception('公钥格式错误: ' . openssl_error_string()); + } + + // 将十六进制签名转换为二进制 + $signatureBinary = hex2bin($signature); + if ($signatureBinary === false) { + throw new \Exception('签名格式错误:无法从十六进制转换'); + } + + // 使用公钥验签(SHA256算法) + $result = openssl_verify($data, $signatureBinary, $pubKey, OPENSSL_ALGO_SHA256); + + openssl_free_key($pubKey); + + if ($result === 1) { + return true; // 验签成功 + } elseif ($result === 0) { + return false; // 验签失败 + } else { + throw new \Exception('验签过程出错: ' . openssl_error_string()); + } + } + + /** + * 建行通知验签(针对回调通知) + * + * 用于验证建行支付通知和退款通知的签名 + * + * @param array $params 通知参数(不包含SIGN字段) + * @param string $signature SIGN字段的值 + * @param string $ccbPublicKey 建行公钥 + * @return bool 验签是否成功 + * @throws \Exception + */ + public static function verifyNotify($params, $signature, $ccbPublicKey) + { + // 移除 SIGN 字段(如果存在) + unset($params['SIGN']); + + // 按照建行规范拼接验签字符串 + // 格式:将参数按字典序排列后拼接(key=value&key=value) + ksort($params); + $signStr = ''; + foreach ($params as $key => $value) { + if ($value !== '' && $value !== null) { + $signStr .= $key . '=' . $value . '&'; + } + } + $signStr = rtrim($signStr, '&'); + + // 调用验签方法 + return self::verify($signStr, $signature, $ccbPublicKey); + } } \ No newline at end of file