paymentService = new CcbPaymentService(); $this->orderService = new CcbOrderService(); } /** * 生成支付串 * * @return void */ public function createPayment() { try { // 1. 获取订单ID $orderId = $this->request->post('order_id', 0); if (empty($orderId)) { $this->error('订单ID不能为空'); } // 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) { $this->error('订单不存在'); } // 3. 检查订单状态 if ($order['status'] != 'unpaid') { $this->error('订单已支付或已关闭'); } // 4. ✅ 判断是否已推送过订单(根据ccb_pay_flow_id判断) $payFlowId = $order['ccb_pay_flow_id']; $needPushOrder = empty($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); // 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()); } } else { // 4.3 已推送过,复用现有支付流水号 Log::info('[建行支付] 订单已推送过,复用支付流水号 pay_flow_id:' . $payFlowId . ' order_id:' . $orderId); } // 5. 生成支付串(步骤3:调用收银台) // ⚠️ 注意: generatePaymentString()内部已经完成了以下操作: // - 更新订单的ccb_pay_flow_id字段(幂等操作,重复更新不影响) // - 记录支付日志到ccb_payment_log表 // 控制器不应该重复操作,否则会导致数据重复写入! $result = $this->paymentService->generatePaymentString($orderId, $payFlowId); if (!$result['status']) { $this->error('支付串生成失败: ' . $result['message']); } // 6. 返回支付串给前端调用收银台 $this->success('支付串生成成功', $result['data']); } catch (Exception $e) { Log::error('[建行支付] 生成支付串失败 order_id:' . ($orderId ?? 0) . ' error:' . $e->getMessage()); $this->error('生成支付串失败: ' . $e->getMessage()); } } /** * 查询订单支付状态 (前端轮询用) * * ⚠️ 重要说明: * 1. 本接口首先查询本地订单状态(已支付直接返回) * 2. 若本地未支付,则调用建行API查询实际支付状态(补偿机制) * 3. 若建行返回已支付,更新本地订单并同步到建行外联系统 * 4. 使用事务+行锁保证幂等性,避免与notify()冲突 * * 流程: * 前端调起支付 → 建行处理 → 建行异步通知notify() (主流程) * → 前端轮询本接口 (补偿机制) * * @return void */ public function queryPaymentStatus() { $orderId = null; try { // 1. 获取订单ID $orderId = $this->request->get('order_id', 0); if (empty($orderId)) { $this->error('订单ID不能为空'); } // 2. 查询本地订单状态(不加锁,快速返回) $order = OrderModel::where('id', $orderId) ->where('user_id', $this->auth->id) ->field('id, order_sn, status, paid_time, ccb_pay_flow_id, pay_fee') ->find(); if (!$order) { $this->error('订单不存在'); } // 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()); $this->error('查询失败: ' . $e->getMessage()); } } /** * 建行支付通知 (建行服务器回调) * * 说明: * 建行支付成功后,会向notify_url发送支付通知 * 这是服务器到服务器的回调,需要验签 * * ⚠️ 重要:此接口为建行服务器异步回调,必须返回纯文本 'SUCCESS' 或 'FAIL' * * ✅ 正确流程: * 1. 验证签名 * 2. 更新订单状态(由handleNotify()完成) * 3. 推送订单到建行外联系统(本方法完成) * 4. 返回SUCCESS给建行 * * @return void */ /** * 建行生活支付通知接口 * * 📋 接口说明(文档7.1): * - 建行生活主动推送支付结果 * - 不会附带服务方编号 * - 通过 REMARK2 字段识别服务方 * - SIGN 字段使用商户私钥签名,需用建行公钥验签 * * @return void */ public function notify() { 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['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); // 9. ✅ 处理成功后更新订单状态到建行(步骤13:调用订单更新接口更新订单状态) if ($result['status'] === 'success' && !empty($result['order_id'])) { // ⚠️ 只有新支付才更新,已支付的订单跳过更新 if ($result['already_paid'] === false) { try { // 调用订单更新接口,将订单状态从未支付更新为已支付 $updateResult = $this->orderService->updateOrderStatus($result['order_id'], '1'); // 1-支付成功 if ($updateResult['status']) { Log::info('[建行支付通知] 订单状态更新成功 order_id:' . $result['order_id']); } else { // ⚠️ 更新失败不影响本地支付状态,记录日志后续补推 Log::warning('[建行支付通知] 订单状态更新失败(本地已支付) order_id:' . $result['order_id'] . ' error:' . $updateResult['message']); } } catch (Exception $e) { // ⚠️ 更新异常不影响支付成功,记录日志后续补推 Log::error('[建行支付通知] 订单状态更新异常(本地已支付) order_id:' . $result['order_id'] . ' error:' . $e->getMessage()); } } else { Log::info('[建行支付通知] 订单已支付且已更新,跳过更新 order_id:' . $result['order_id']); } } // 10. 返回处理结果 // ⚠️ 重要: 必须使用exit直接退出,防止ThinkPHP框架追加额外内容 // 建行要求返回纯文本 'SUCCESS' 或 'FAIL',任何额外字符都会导致建行认为通知失败 $response = ($result['status'] === 'success') ? 'SUCCESS' : 'FAIL'; Log::info('[建行支付通知] 处理完成,返回: ' . $response); // 直接退出,确保只输出SUCCESS/FAIL exit($response); } catch (Exception $e) { Log::error('[建行支付通知] 处理失败 error:' . $e->getMessage()); // 异常情况也要直接退出 exit('FAIL'); } } }