2025-10-20 17:10:16 +08:00

316 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace addons\shopro\controller;
use addons\shopro\controller\Common;
use addons\shopro\library\ccblife\CcbPaymentService;
use addons\shopro\library\ccblife\CcbOrderService;
use app\admin\model\shopro\order\Order as OrderModel;
use think\Db;
use think\Exception;
use think\Log;
/**
* 建行支付控制器
*
* 功能:
* - 生成支付串
* - 处理支付回调
* - 验证支付结果
*
* @author Billy
* @date 2025-01-16
*/
class Ccbpayment extends Common
{
/**
* 不需要登录的方法 (支付回调不需要登录)
* @var array
*/
protected $noNeedLogin = ['notify'];
/**
* 不需要权限的方法
* @var array
*/
protected $noNeedRight = ['*'];
/**
* 支付服务
* @var CcbPaymentService
*/
private $paymentService;
/**
* 订单服务
* @var CcbOrderService
*/
private $orderService;
/**
* 初始化
*/
public function _initialize()
{
parent::_initialize();
$this->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. ✅ 推送订单到建行(步骤2:调用订单推送接口将已提交订单)
// ⚠️ 注意:此时推送的是未支付状态的订单
try {
$pushResult = $this->orderService->pushOrder($orderId);
if ($pushResult['status']) {
Log::info('[建行支付] 订单推送成功 order_id:' . $orderId);
} else {
// ⚠️ 推送失败不阻塞支付流程,只记录日志
Log::warning('[建行支付] 订单推送失败(不阻塞支付) order_id:' . $orderId . ' error:' . $pushResult['message']);
}
} catch (Exception $e) {
// ⚠️ 推送异常不阻塞支付流程
Log::error('[建行支付] 订单推送异常(不阻塞支付) order_id:' . $orderId . ' error:' . $e->getMessage());
}
// 6. 返回支付串
$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());
}
}
/**
* 建行支付通知 (建行服务器回调)
*
* 说明:
* 建行支付成功后,会向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. ✅ 处理成功后更新订单状态到建行(步骤13:调用订单更新接口更新订单状态)
if ($result['status'] === 'success' && !empty($result['order_id'])) {
// ⚠️ 只有新支付才更新,已支付的订单跳过更新
if ($result['already_paid'] === false) {
try {
// 调用订单更新接口,将订单状态从未支付更新为已支付
$updateResult = $this->orderService->updateOrderStatus($result['order_id']);
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']);
}
}
// 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');
}
}
/**
* ⚠️ 已废弃: 推送订单到建行外联系统
*
* @deprecated 2025-01-20 根据建行流程图,订单推送应在createPayment()时完成,此方法已废弃
* @see createPayment() 步骤5: 推送未支付订单
* @see notify() 步骤7: 更新订单为已支付
*/
private function pushOrderToCcb($orderId)
{
Log::warning('[建行支付] pushOrderToCcb()已废弃,请使用orderService->pushOrder()或updateOrderStatus()');
}
/**
* 保存支付日志
*
* @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);
}
}