316 lines
11 KiB
PHP
Raw Normal View History

2025-10-17 16:32:16 +08:00
<?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
*/
2025-10-20 17:10:16 +08:00
protected $noNeedLogin = ['notify'];
2025-10-17 16:32:16 +08:00
/**
* 不需要权限的方法
* @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. 生成支付串
2025-10-20 15:51:06 +08:00
// ⚠️ 注意: generatePaymentString()内部已经完成了以下操作:
// - 更新订单的ccb_pay_flow_id字段
// - 记录支付日志到ccb_payment_log表
// 控制器不应该重复操作,否则会导致数据重复写入!
2025-10-17 17:18:15 +08:00
$result = $this->paymentService->generatePaymentString($orderId);
2025-10-17 16:32:16 +08:00
2025-10-17 17:18:15 +08:00
if (!$result['status']) {
$this->error('支付串生成失败: ' . $result['message']);
2025-10-17 16:32:16 +08:00
}
2025-10-20 17:10:16 +08:00
// 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. 返回支付串
2025-10-20 15:51:06 +08:00
$this->success('支付串生成成功', $result['data']);
2025-10-17 16:32:16 +08:00
} catch (Exception $e) {
Log::error('[建行支付] 生成支付串失败 order_id:' . ($orderId ?? 0) . ' error:' . $e->getMessage());
$this->error('生成支付串失败: ' . $e->getMessage());
}
}
/**
2025-10-20 16:36:06 +08:00
* 查询订单支付状态 (前端轮询用)
2025-10-17 16:32:16 +08:00
*
2025-10-20 16:36:06 +08:00
* ⚠️ 重要说明:
* 本接口只查询订单状态,不执行任何业务逻辑!
* 订单状态由建行异步通知(notify)接口更新,前端只负责轮询查询。
*
* 修改原因:
* 原callback()方法存在严重安全漏洞:
* 1. 前端可伪造支付成功请求
* 2. 与notify()形成双通道,存在竞态条件
* 3. 违反建行标准支付流程
*
* 正确流程:
* 前端调起支付 建行处理 建行异步通知notify() 前端轮询本接口查询状态
2025-10-17 16:32:16 +08:00
*
* @return void
*/
2025-10-20 16:36:06 +08:00
public function queryPaymentStatus()
2025-10-17 16:32:16 +08:00
{
try {
2025-10-20 16:36:06 +08:00
// 1. 获取订单ID
$orderId = $this->request->get('order_id', 0);
2025-10-17 16:32:16 +08:00
if (empty($orderId)) {
$this->error('订单ID不能为空');
}
2025-10-20 16:36:06 +08:00
// 2. 查询订单(只查询,不更新!)
$order = OrderModel::where('id', $orderId)
->where('user_id', $this->auth->id)
->field('id, order_sn, status, paid_time, ccb_pay_flow_id')
->find();
2025-10-17 16:32:16 +08:00
if (!$order) {
$this->error('订单不存在');
}
2025-10-20 16:36:06 +08:00
// 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,
]);
2025-10-17 16:32:16 +08:00
2025-10-20 16:36:06 +08:00
} catch (Exception $e) {
Log::error('[建行支付] 查询订单状态失败 order_id:' . ($orderId ?? 0) . ' error:' . $e->getMessage());
2025-10-17 16:32:16 +08:00
2025-10-20 16:36:06 +08:00
$this->error('查询失败: ' . $e->getMessage());
}
}
2025-10-17 16:32:16 +08:00
/**
* 建行支付通知 (建行服务器回调)
*
* 说明:
* 建行支付成功后,会向notify_url发送支付通知
* 这是服务器到服务器的回调,需要验签
*
2025-10-18 01:16:25 +08:00
* ⚠️ 重要:此接口为建行服务器异步回调,必须返回纯文本 'SUCCESS' 'FAIL'
*
2025-10-20 16:36:06 +08:00
* 正确流程:
* 1. 验证签名
* 2. 更新订单状态(由handleNotify()完成)
* 3. 推送订单到建行外联系统(本方法完成)
* 4. 返回SUCCESS给建行
*
2025-10-17 16:32:16 +08:00
* @return void
*/
public function notify()
{
try {
2025-10-18 01:16:25 +08:00
// 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);
}
2025-10-17 16:32:16 +08:00
2025-10-18 01:16:25 +08:00
// 4. 记录参数
Log::info('[建行通知] 解析参数: ' . json_encode($params, JSON_UNESCAPED_UNICODE));
// 5. 验证必需参数
if (empty($params['ORDERID'])) {
Log::error('[建行通知] 缺少ORDERID参数');
2025-10-20 16:36:06 +08:00
exit('FAIL');
2025-10-18 01:16:25 +08:00
}
2025-10-20 16:36:06 +08:00
// 6. 调用支付服务处理通知(返回订单ID)
2025-10-18 01:16:25 +08:00
$result = $this->paymentService->handleNotify($params);
2025-10-20 17:10:16 +08:00
// 7. ✅ 处理成功后更新订单状态到建行(步骤13:调用订单更新接口更新订单状态)
2025-10-20 16:36:06 +08:00
if ($result['status'] === 'success' && !empty($result['order_id'])) {
2025-10-20 17:10:16 +08:00
// ⚠️ 只有新支付才更新,已支付的订单跳过更新
2025-10-20 16:36:06 +08:00
if ($result['already_paid'] === false) {
try {
2025-10-20 17:10:16 +08:00
// 调用订单更新接口,将订单状态从未支付更新为已支付
$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']);
}
2025-10-20 16:36:06 +08:00
} catch (Exception $e) {
2025-10-20 17:10:16 +08:00
// ⚠️ 更新异常不影响支付成功,记录日志后续补推
Log::error('[建行通知] 订单状态更新异常(本地已支付) order_id:' . $result['order_id'] . ' error:' . $e->getMessage());
2025-10-20 16:36:06 +08:00
}
} else {
2025-10-20 17:10:16 +08:00
Log::info('[建行通知] 订单已支付且已更新,跳过更新 order_id:' . $result['order_id']);
2025-10-20 16:36:06 +08:00
}
}
// 8. 返回处理结果
2025-10-20 15:29:15 +08:00
// ⚠️ 重要: 必须使用exit直接退出,防止ThinkPHP框架追加额外内容
// 建行要求返回纯文本 'SUCCESS' 或 'FAIL',任何额外字符都会导致建行认为通知失败
2025-10-20 16:36:06 +08:00
$response = ($result['status'] === 'success') ? 'SUCCESS' : 'FAIL';
Log::info('[建行通知] 处理完成,返回: ' . $response);
2025-10-18 01:16:25 +08:00
2025-10-20 15:29:15 +08:00
// 直接退出,确保只输出SUCCESS/FAIL
2025-10-20 16:36:06 +08:00
exit($response);
2025-10-17 16:32:16 +08:00
} catch (Exception $e) {
Log::error('[建行通知] 处理失败 error:' . $e->getMessage());
2025-10-20 15:29:15 +08:00
// 异常情况也要直接退出
exit('FAIL');
2025-10-17 16:32:16 +08:00
}
}
/**
2025-10-20 17:10:16 +08:00
* ⚠️ 已废弃: 推送订单到建行外联系统
2025-10-20 16:36:06 +08:00
*
2025-10-20 17:10:16 +08:00
* @deprecated 2025-01-20 根据建行流程图,订单推送应在createPayment()时完成,此方法已废弃
* @see createPayment() 步骤5: 推送未支付订单
* @see notify() 步骤7: 更新订单为已支付
2025-10-17 16:32:16 +08:00
*/
2025-10-20 16:36:06 +08:00
private function pushOrderToCcb($orderId)
2025-10-17 16:32:16 +08:00
{
2025-10-20 17:10:16 +08:00
Log::warning('[建行支付] pushOrderToCcb()已废弃,请使用orderService->pushOrder()或updateOrderStatus()');
2025-10-17 16:32:16 +08:00
}
/**
* 保存支付日志
*
* @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);
}
}