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. 查询订单 $order = OrderModel::where('id', $orderId) ->where('user_id', $this->auth->id) ->find(); if (!$order) { $this->error('订单不存在'); } // 3. 检查订单状态 if ($order['status'] != 'unpaid') { $this->error('订单已支付或已关闭'); } // 4. 生成支付串 // ⚠️ 注意: generatePaymentString()内部已经完成了以下操作: // - 更新订单的ccb_pay_flow_id字段 // - 记录支付日志到ccb_payment_log表 // 控制器不应该重复操作,否则会导致数据重复写入! $result = $this->paymentService->generatePaymentString($orderId); if (!$result['status']) { $this->error('支付串生成失败: ' . $result['message']); } // 5. 直接返回支付串(不再重复保存数据库!) $this->success('支付串生成成功', $result['data']); } catch (Exception $e) { Log::error('[建行支付] 生成支付串失败 order_id:' . ($orderId ?? 0) . ' error:' . $e->getMessage()); $this->error('生成支付串失败: ' . $e->getMessage()); } } /** * 查询订单支付状态 (前端轮询用) * * ⚠️ 重要说明: * 本接口只查询订单状态,不执行任何业务逻辑! * 订单状态由建行异步通知(notify)接口更新,前端只负责轮询查询。 * * 修改原因: * 原callback()方法存在严重安全漏洞: * 1. 前端可伪造支付成功请求 * 2. 与notify()形成双通道,存在竞态条件 * 3. 违反建行标准支付流程 * * 正确流程: * 前端调起支付 → 建行处理 → 建行异步通知notify() → 前端轮询本接口查询状态 * * @return void */ public function queryPaymentStatus() { 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') ->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, ]); } catch (Exception $e) { Log::error('[建行支付] 查询订单状态失败 order_id:' . ($orderId ?? 0) . ' error:' . $e->getMessage()); $this->error('查询失败: ' . $e->getMessage()); } } /** * ⚠️ 已废弃: 支付回调 (前端调用) * * @deprecated 2025-01-20 该方法存在严重安全漏洞,已被queryPaymentStatus()替代 * @see queryPaymentStatus() */ public function callback() { // 向后兼容:直接调用查询接口 Log::warning('[建行支付] callback()已废弃,请前端改用queryPaymentStatus()接口'); // 将POST的order_id转为GET参数 $_GET['order_id'] = $this->request->post('order_id', 0); return $this->queryPaymentStatus(); } /** * 建行支付通知 (建行服务器回调) * * 说明: * 建行支付成功后,会向notify_url发送支付通知 * 这是服务器到服务器的回调,需要验签 * * ⚠️ 重要:此接口为建行服务器异步回调,必须返回纯文本 'SUCCESS' 或 'FAIL' * * ✅ 正确流程: * 1. 验证签名 * 2. 更新订单状态(由handleNotify()完成) * 3. 推送订单到建行外联系统(本方法完成) * 4. 返回SUCCESS给建行 * * @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'); } // 6. 调用支付服务处理通知(返回订单ID) $result = $this->paymentService->handleNotify($params); // 7. 处理成功后推送订单到建行外联系统 if ($result['status'] === 'success' && !empty($result['order_id'])) { // ⚠️ 只有新支付才推送,已支付的订单跳过推送 if ($result['already_paid'] === false) { try { $this->pushOrderToCcb($result['order_id']); Log::info('[建行通知] 订单推送成功 order_id:' . $result['order_id']); } catch (Exception $e) { // ⚠️ 推送失败不影响支付成功,记录日志后续补推 Log::error('[建行通知] 订单推送失败 order_id:' . $result['order_id'] . ' error:' . $e->getMessage()); } } else { Log::info('[建行通知] 订单已支付且已推送,跳过推送 order_id:' . $result['order_id']); } } // 8. 返回处理结果 // ⚠️ 重要: 必须使用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'); } } /** * 推送订单到建行外联系统 * * ⚠️ 重要: 只在notify()支付成功后调用! * ✅ 幂等性: 支持重复调用,已推送的订单会跳过 * * @param int $orderId 订单ID * @return void * @throws Exception 推送失败时抛出异常 */ private function pushOrderToCcb($orderId) { try { // ✅ 重新查询订单,确保状态已更新 $order = OrderModel::find($orderId); if (!$order) { throw new Exception('订单不存在'); } // ✅ 验证订单状态 if ($order->status !== 'paid') { throw new Exception('订单状态不正确(status=' . $order->status . '),无法推送'); } // ✅ 幂等性检查: 如果已推送成功,跳过 if ($order->ccb_sync_status == 1) { Log::info('[建行推送] 订单已推送,跳过 order_id:' . $orderId); return; } // 获取订单商品列表 $orderItems = Db::name('shopro_order_item') ->where('order_id', $order->id) ->field('goods_id, goods_sku_text, goods_title, goods_price, goods_num, discount_fee') ->select(); $goodsList = []; foreach ($orderItems as $item) { $goodsList[] = [ 'goods_id' => $item['goods_id'], 'goods_name' => $item['goods_title'], 'goods_sku' => $item['goods_sku_text'], 'goods_price' => $item['goods_price'], 'goods_num' => $item['goods_num'], 'discount_amount' => $item['discount_fee'] ?? 0 ]; } // 获取用户的建行用户ID $user = Db::name('user')->where('id', $order->user_id)->field('ccb_user_id')->find(); // 构造订单数据 (使用Shopro实际字段名) $orderData = [ 'id' => $order->id, 'order_sn' => $order->order_sn, 'ccb_user_id' => $user['ccb_user_id'] ?? '', 'total_amount' => $order->total_amount, // 订单总金额 'pay_amount' => $order->total_fee, // 实际支付金额 'discount_amount' => $order->discount_fee, // 优惠金额 'status' => $order->status, // Shopro使用status枚举 'refund_status' => $order->aftersale_status ?? 0, // 售后状态 'create_time' => $order->createtime, // Shopro使用秒级时间戳 'paid_time' => $order->paid_time, // 支付时间 'ccb_pay_flow_id' => $order->ccb_pay_flow_id, 'goods_list' => $goodsList, ]; // 推送到建行 $result = $this->orderService->pushOrder($order->id); if (!$result['status']) { // ✅ 推送失败: 更新状态为失败,记录错误原因 OrderModel::where('id', $orderId)->update([ 'ccb_sync_status' => 2, // 2=失败 'ccb_sync_error' => $result['message'] ?? '未知错误', 'ccb_sync_time' => time(), ]); throw new Exception($result['message'] ?? '推送失败'); } // ✅ 推送成功: 更新同步状态 OrderModel::where('id', $orderId)->update([ 'ccb_sync_status' => 1, // 1=成功 'ccb_sync_time' => time(), 'ccb_sync_error' => '', // 清空错误信息 ]); Log::info('[建行推送] 订单推送成功 order_id:' . $order->id . ' order_sn:' . $order->order_sn); } catch (Exception $e) { // ✅ 记录失败原因,供后续补推 OrderModel::where('id', $orderId)->update([ 'ccb_sync_status' => 2, // 失败状态 'ccb_sync_error' => $e->getMessage(), ]); throw $e; // 向上抛出异常 } } /** * 保存支付日志 * * @param object $order 订单对象 * @param string $paymentString 支付串 * @param string $payFlowId 支付流水号 * @return void */ private function savePaymentLog($order, $paymentString, $payFlowId) { Db::name('ccb_payment_log')->insert([ 'order_id' => $order->id, 'order_sn' => $order->order_sn, 'pay_flow_id' => $payFlowId, 'payment_string' => $paymentString, 'user_id' => $order->user_id, 'ccb_user_id' => $order->ccb_user_id, 'amount' => $order->pay_amount, 'status' => 0, // 待支付 'create_time' => time(), ]); } /** * 更新支付日志 * * @param string $payFlowId 支付流水号 * @param array $data 更新数据 * @return void */ private function updatePaymentLog($payFlowId, $data) { Db::name('ccb_payment_log') ->where('pay_flow_id', $payFlowId) ->update($data); } }