diff --git a/addons/shopro/controller/Ccbpayment.php b/addons/shopro/controller/Ccbpayment.php index 256ff12..3516043 100644 --- a/addons/shopro/controller/Ccbpayment.php +++ b/addons/shopro/controller/Ccbpayment.php @@ -73,9 +73,10 @@ class Ccbpayment extends Common $this->error('订单ID不能为空'); } - // 2. 查询订单 + // 2. 查询订单(包含ccb_pay_flow_id字段,用于判断是否已推送) $order = OrderModel::where('id', $orderId) ->where('user_id', $this->auth->id) + ->field('id, order_sn, status, pay_fee, ccb_pay_flow_id') ->find(); if (!$order) { @@ -87,34 +88,43 @@ class Ccbpayment extends Common $this->error('订单已支付或已关闭'); } - // 4. ✅ 生成支付流水号(统一标识,用于订单推送和支付串生成) - // 格式: PAY + 年月日时分秒(14位) + 随机数(6位) = 23位 - $payFlowId = 'PAY' . date('YmdHis') . mt_rand(100000, 999999); - Log::info('[建行支付] 生成支付流水号 pay_flow_id:' . $payFlowId . ' order_id:' . $orderId); + // 4. ✅ 判断是否已推送过订单(根据ccb_pay_flow_id判断) + $payFlowId = $order['ccb_pay_flow_id']; + $needPushOrder = empty($payFlowId); - // 5. ✅ 先推送订单到建行生活(步骤2:调用A3341TP01订单推送接口) - // ⚠️ 重要:必须先推送订单,收银台才能校验订单信息 - // 根据《5.6.2 业务流程说明》步骤2:由服务方调用订单推送接口向建行生活推送订单信息 - try { - $pushResult = $this->orderService->pushOrder($orderId, $payFlowId); + if ($needPushOrder) { + // 4.1 生成新的支付流水号(统一标识,用于订单推送和支付串生成) + // 格式: PAY + 年月日时分秒(14位) + 随机数(6位) = 23位 + $payFlowId = 'PAY' . date('YmdHis') . mt_rand(100000, 999999); + Log::info('[建行支付] 首次支付,生成支付流水号 pay_flow_id:' . $payFlowId . ' order_id:' . $orderId); - if (!$pushResult['status']) { - // ⚠️ 推送失败必须阻塞支付流程!收银台会找不到订单 - Log::error('[建行支付] 订单推送失败(阻塞支付) order_id:' . $orderId . ' error:' . $pushResult['message']); - $this->error('订单推送失败,无法生成支付串: ' . $pushResult['message']); + // 4.2 推送订单到建行生活(步骤2:调用A3341TP01订单推送接口) + // ⚠️ 重要:必须先推送订单,收银台才能校验订单信息 + // 根据《5.6.2 业务流程说明》步骤2:由服务方调用订单推送接口向建行生活推送订单信息 + try { + $pushResult = $this->orderService->pushOrder($orderId, $payFlowId); + + if (!$pushResult['status']) { + // ⚠️ 推送失败必须阻塞支付流程!收银台会找不到订单 + Log::error('[建行支付] 订单推送失败(阻塞支付) order_id:' . $orderId . ' error:' . $pushResult['message']); + $this->error('订单推送失败,无法生成支付串: ' . $pushResult['message']); + } + + Log::info('[建行支付] 订单推送成功 order_id:' . $orderId); + + } catch (Exception $e) { + // ⚠️ 推送异常必须阻塞支付流程 + Log::error('[建行支付] 订单推送异常(阻塞支付) order_id:' . $orderId . ' error:' . $e->getMessage()); + $this->error('订单推送异常,无法生成支付串: ' . $e->getMessage()); } - - Log::info('[建行支付] 订单推送成功 order_id:' . $orderId); - - } catch (Exception $e) { - // ⚠️ 推送异常必须阻塞支付流程 - Log::error('[建行支付] 订单推送异常(阻塞支付) order_id:' . $orderId . ' error:' . $e->getMessage()); - $this->error('订单推送异常,无法生成支付串: ' . $e->getMessage()); + } else { + // 4.3 已推送过,复用现有支付流水号 + Log::info('[建行支付] 订单已推送过,复用支付流水号 pay_flow_id:' . $payFlowId . ' order_id:' . $orderId); } - // 6. 生成支付串(步骤3:调用收银台) + // 5. 生成支付串(步骤3:调用收银台) // ⚠️ 注意: generatePaymentString()内部已经完成了以下操作: - // - 更新订单的ccb_pay_flow_id字段 + // - 更新订单的ccb_pay_flow_id字段(幂等操作,重复更新不影响) // - 记录支付日志到ccb_payment_log表 // 控制器不应该重复操作,否则会导致数据重复写入! $result = $this->paymentService->generatePaymentString($orderId, $payFlowId); @@ -123,7 +133,7 @@ class Ccbpayment extends Common $this->error('支付串生成失败: ' . $result['message']); } - // 7. 返回支付串给前端调用收银台 + // 6. 返回支付串给前端调用收银台 $this->success('支付串生成成功', $result['data']); } catch (Exception $e) { @@ -137,22 +147,21 @@ class Ccbpayment extends Common * 查询订单支付状态 (前端轮询用) * * ⚠️ 重要说明: - * 本接口只查询订单状态,不执行任何业务逻辑! - * 订单状态由建行异步通知(notify)接口更新,前端只负责轮询查询。 + * 1. 本接口首先查询本地订单状态(已支付直接返回) + * 2. 若本地未支付,则调用建行API查询实际支付状态(补偿机制) + * 3. 若建行返回已支付,更新本地订单并同步到建行外联系统 + * 4. 使用事务+行锁保证幂等性,避免与notify()冲突 * - * 修改原因: - * 原callback()方法存在严重安全漏洞: - * 1. 前端可伪造支付成功请求 - * 2. 与notify()形成双通道,存在竞态条件 - * 3. 违反建行标准支付流程 - * - * 正确流程: - * 前端调起支付 → 建行处理 → 建行异步通知notify() → 前端轮询本接口查询状态 + * 流程: + * 前端调起支付 → 建行处理 → 建行异步通知notify() (主流程) + * → 前端轮询本接口 (补偿机制) * * @return void */ public function queryPaymentStatus() { + $orderId = null; + try { // 1. 获取订单ID $orderId = $this->request->get('order_id', 0); @@ -161,28 +170,192 @@ class Ccbpayment extends Common $this->error('订单ID不能为空'); } - // 2. 查询订单(只查询,不更新!) + // 2. 查询本地订单状态(不加锁,快速返回) $order = OrderModel::where('id', $orderId) ->where('user_id', $this->auth->id) - ->field('id, order_sn, status, paid_time, ccb_pay_flow_id') + ->field('id, order_sn, status, paid_time, ccb_pay_flow_id, pay_fee') ->find(); if (!$order) { $this->error('订单不存在'); } - // 3. 返回订单状态(只读操作,绝不修改数据!) - $this->success('查询成功', [ - 'order_id' => $order->id, - 'order_sn' => $order->order_sn, - 'status' => $order->status, - 'is_paid' => in_array($order->status, ['paid', 'completed', 'success']), - 'paid_time' => $order->paid_time, - 'pay_flow_id' => $order->ccb_pay_flow_id, - ]); + // 3. 如果订单已支付,直接返回(幂等性:避免重复查询建行API) + if (in_array($order->status, ['paid', 'completed', 'success'])) { + Log::info('[建行支付查询] 订单已支付,直接返回 order_id:' . $orderId . ' order_sn:' . $order->order_sn); + + $this->success('查询成功', [ + 'order_id' => $order->id, + 'order_sn' => $order->order_sn, + 'status' => $order->status, + 'is_paid' => true, + 'paid_time' => $order->paid_time, + 'pay_flow_id' => $order->ccb_pay_flow_id, + 'query_source' => 'local' // 标记数据来源 + ]); + } + + // 4. 订单未支付,调用建行API查询实际支付状态(补偿机制) + if (empty($order->ccb_pay_flow_id)) { + // 没有支付流水号,说明用户还未调起支付,直接返回未支付 + Log::info('[建行支付查询] 订单无支付流水号,未发起支付 order_id:' . $orderId); + + $this->success('查询成功', [ + 'order_id' => $order->id, + 'order_sn' => $order->order_sn, + 'status' => $order->status, + 'is_paid' => false, + 'paid_time' => null, + 'pay_flow_id' => null, + 'query_source' => 'local' + ]); + } + + Log::info('[建行支付查询] 调用建行API查询支付状态 order_id:' . $orderId . ' order_sn:' . $order->order_sn . ' pay_flow_id:' . $order->ccb_pay_flow_id); + + // 5. 调用建行订单查询接口 + // 参数说明:orderSn=支付流水号, startTime=7天前, endTime=当前, page=1, txType=0(支付交易), txnStatus=00(成功) + $queryResult = $this->orderService->queryOrder( + $order->ccb_pay_flow_id, // 使用支付流水号查询 + date('YmdHis', strtotime('-7 days')), // 开始时间 + date('YmdHis'), // 结束时间 + 1, // 页码 + '0', // 交易类型:0-支付交易 + '00' // 交易状态:00-成功 + ); + + // 6. 处理建行查询结果 + if (!$queryResult['status']) { + // 建行API调用失败,返回本地状态 + Log::warning('[建行支付查询] 建行API调用失败,返回本地状态 order_id:' . $orderId . ' error:' . $queryResult['message']); + + $this->success('查询成功', [ + 'order_id' => $order->id, + 'order_sn' => $order->order_sn, + 'status' => $order->status, + 'is_paid' => false, + 'paid_time' => null, + 'pay_flow_id' => $order->ccb_pay_flow_id, + 'query_source' => 'local', + 'ccb_error' => $queryResult['message'] + ]); + } + + // 7. 解析建行返回数据 + $ccbData = $queryResult['data']['CLD_BODY'] ?? []; + $txnList = $ccbData['TXN_LST'] ?? []; + + // 8. 检查是否有支付成功记录 + $isPaidInCcb = false; + $ccbPayTime = null; + $ccbTransId = null; + + if (!empty($txnList)) { + foreach ($txnList as $txn) { + // TXN_STATUS=00 表示交易成功 + if (isset($txn['TXN_STATUS']) && $txn['TXN_STATUS'] == '00') { + $isPaidInCcb = true; + $ccbPayTime = $txn['TXN_TIME'] ?? null; // 交易时间 YYYYMMDDHHmmss + $ccbTransId = $txn['TXN_SEQ'] ?? null; // 交易流水号 + break; + } + } + } + + // 9. 如果建行返回未支付,直接返回 + if (!$isPaidInCcb) { + Log::info('[建行支付查询] 建行返回未支付 order_id:' . $orderId . ' order_sn:' . $order->order_sn); + + $this->success('查询成功', [ + 'order_id' => $order->id, + 'order_sn' => $order->order_sn, + 'status' => $order->status, + 'is_paid' => false, + 'paid_time' => null, + 'pay_flow_id' => $order->ccb_pay_flow_id, + 'query_source' => 'ccb' + ]); + } + + // 10. ✅ 建行返回已支付,启动事务更新本地订单 + Log::info('[建行支付查询] 建行返回已支付,开始更新本地订单 order_id:' . $orderId . ' order_sn:' . $order->order_sn); + + Db::startTrans(); + try { + // 10.1 使用行锁重新查询订单状态(避免并发冲突) + $lockedOrder = OrderModel::where('id', $orderId) + ->where('user_id', $this->auth->id) + ->lock(true) // 悲观锁:FOR UPDATE + ->find(); + + if (!$lockedOrder) { + throw new Exception('订单不存在或无权限'); + } + + // 10.2 再次检查订单状态(可能已被notify()更新) + if (in_array($lockedOrder->status, ['paid', 'completed', 'success'])) { + Db::commit(); + Log::info('[建行支付查询] 订单已被其他流程更新为已支付,跳过更新 order_id:' . $orderId); + + $this->success('查询成功', [ + 'order_id' => $lockedOrder->id, + 'order_sn' => $lockedOrder->order_sn, + 'status' => $lockedOrder->status, + 'is_paid' => true, + 'paid_time' => $lockedOrder->paid_time, + 'pay_flow_id' => $lockedOrder->ccb_pay_flow_id, + 'query_source' => 'local_concurrent' + ]); + } + + // 10.3 更新订单状态为已支付 + $paidTime = time() * 1000; // Shopro使用毫秒时间戳 + + OrderModel::where('id', $orderId)->update([ + 'status' => 'paid', + 'pay_type' => 'offline', // 建行支付归类为线下银行支付 + 'paid_time' => $paidTime, + 'transaction_id' => $ccbTransId ?: $order->ccb_pay_flow_id, // 建行交易流水号 + 'updatetime' => time() + ]); + + Log::info('[建行支付查询] 本地订单状态更新成功 order_id:' . $orderId . ' status:paid'); + + // 10.4 提交事务 + Db::commit(); + + // 11. ✅ 更新订单状态到建行外联系统(异步,失败不影响本地) + try { + $updateResult = $this->orderService->updateOrderStatus($orderId, '1'); // 1-支付成功 + + if ($updateResult['status']) { + Log::info('[建行支付查询] 订单状态同步到建行成功 order_id:' . $orderId); + } else { + Log::warning('[建行支付查询] 订单状态同步到建行失败(本地已更新) order_id:' . $orderId . ' error:' . $updateResult['message']); + } + } catch (Exception $e) { + Log::error('[建行支付查询] 订单状态同步到建行异常(本地已更新) order_id:' . $orderId . ' error:' . $e->getMessage()); + } + + // 12. 返回成功结果 + $this->success('查询成功', [ + 'order_id' => $order->id, + 'order_sn' => $order->order_sn, + 'status' => 'paid', + 'is_paid' => true, + 'paid_time' => $paidTime, + 'pay_flow_id' => $order->ccb_pay_flow_id, + 'query_source' => 'ccb_updated' // 标记:从建行查询并更新 + ]); + + } catch (Exception $e) { + // 回滚事务 + Db::rollback(); + throw $e; + } } catch (Exception $e) { - Log::error('[建行支付] 查询订单状态失败 order_id:' . ($orderId ?? 0) . ' error:' . $e->getMessage()); + Log::error('[建行支付查询] 查询订单状态失败 order_id:' . ($orderId ?? 0) . ' error:' . $e->getMessage()); $this->error('查询失败: ' . $e->getMessage()); } @@ -330,149 +503,4 @@ class Ccbpayment extends Common 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/CcbOrderService.php b/addons/shopro/library/ccblife/CcbOrderService.php index 391fc5d..58dcde4 100644 --- a/addons/shopro/library/ccblife/CcbOrderService.php +++ b/addons/shopro/library/ccblife/CcbOrderService.php @@ -98,8 +98,8 @@ class CcbOrderService $costTime = round((microtime(true) - $startTime) * 1000, 2); $this->recordSyncLog($orderId, 'A3341TP01', $txSeq, $response, 'response', true, $costTime); - // 更新订单同步状态 - $this->updateOrderSyncStatus($orderId, 1); + // 更新订单同步状态为成功(清空错误信息) + $this->updateOrderSyncStatus($orderId, 1, ''); return [ 'status' => true, @@ -112,8 +112,8 @@ class CcbOrderService $costTime = round((microtime(true) - $startTime) * 1000, 2); $this->recordSyncLog($orderId, 'A3341TP01', '', null, 'error', false, $costTime, $e->getMessage()); - // 更新同步状态为失败 - $this->updateOrderSyncStatus($orderId, 2); + // 更新同步状态为失败,并保存错误信息 + $this->updateOrderSyncStatus($orderId, 2, $e->getMessage()); Log::error('建行订单推送失败: ' . $e->getMessage()); return [ @@ -569,14 +569,26 @@ class CcbOrderService * * @param int $orderId 订单ID * @param int $status 同步状态:0-未同步 1-已同步 2-同步失败 + * @param string $errorMsg 错误信息(失败时填写,成功时传空字符串清空) */ - private function updateOrderSyncStatus($orderId, $status) + private function updateOrderSyncStatus($orderId, $status, $errorMsg = '') { - Db::name('shopro_order')->where('id', $orderId)->update([ + $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); } /**