From 19d590a60b99f55a51a45433e77d50c257a58a85 Mon Sep 17 00:00:00 2001 From: Billy <641833868@qq.com> Date: Tue, 28 Oct 2025 14:51:33 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AE=A2=E5=8D=95=E7=8A=B6=E6=80=81=E5=8F=98?= =?UTF-8?q?=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- addons/shopro/controller/order/Order.php | 8 + .../library/ccblife/CcbOrderService.php | 1251 ++++++++--------- 2 files changed, 614 insertions(+), 645 deletions(-) diff --git a/addons/shopro/controller/order/Order.php b/addons/shopro/controller/order/Order.php index 1f278e0..102ee35 100755 --- a/addons/shopro/controller/order/Order.php +++ b/addons/shopro/controller/order/Order.php @@ -2,6 +2,7 @@ namespace addons\shopro\controller\order; +use addons\shopro\library\ccblife\CcbOrderService; use think\Db; use addons\shopro\exception\ShoproException; use addons\shopro\controller\Common; @@ -182,6 +183,9 @@ class Order extends Common // 订单未支付,处理 item 状态 $order = $order->setOrderItemStatusByOrder($order); // 这里订单转 数组了 + $orderService = new CcbOrderService(); + $orderService->updateOrderStatus($id, 4);//取消订单 + $this->success('取消成功', $order); } @@ -206,6 +210,10 @@ class Order extends Common $order = OrderModel::with(['items', 'invoice'])->find($id); $order = $order->setOrderItemStatusByOrder($order); // 这里订单转 数组了 + + $orderService = new CcbOrderService(); + $orderService->updateOrderStatus($id, 0,1);//申请退款 + $this->success('申请成功', $order); } diff --git a/addons/shopro/library/ccblife/CcbOrderService.php b/addons/shopro/library/ccblife/CcbOrderService.php index 09803ad..4570464 100644 --- a/addons/shopro/library/ccblife/CcbOrderService.php +++ b/addons/shopro/library/ccblife/CcbOrderService.php @@ -1,645 +1,606 @@ -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 = null, $refundStatus = null) - { - $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('订单不存在'); - } - - // 获取支付流水号 - // ✅ 修复:订单表字段名是 ccb_pay_flow_id,不是 pay_flow_id - $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)'); - } - - // 映射订单状态 - $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 = [ - '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, null, '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' => $this->mapOrderStatus($order['status']), // 订单状态 - 'REFUND_STATUS' => $this->mapRefundStatus($order['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); - } - - /** - * 映射订单状态 - * - * @param string $status Shopro订单状态 - * @return string 建行订单状态 - */ - private function mapOrderStatus($status) - { - $statusMap = [ - 'unpaid' => '0', // 待支付 - 'paid' => '1', // 已支付 - 'shipped' => '2', // 已发货 - 'received' => '3', // 已收货 - 'completed' => '4', // 已完成 - 'cancelled' => '5', // 已取消 - 'refunded' => '6' // 已退款 - ]; - - return $statusMap[$status] ?? '0'; - } - - /** - * 映射退款状态 - * - * @param int $refundStatus 退款状态 - * @return string - */ - private function mapRefundStatus($refundStatus) - { - if ($refundStatus == 0) return '0'; // 无退款 - if ($refundStatus == 1) return '1'; // 退款中 - if ($refundStatus == 2) return '2'; // 已退款 - return '0'; - } -} +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, null, '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); + } +}