2025-10-20 16:36:06 +08:00

398 lines
14 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 = ['callback', '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. 直接返回支付串(不再重复保存数据库!)
$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);
}
}