mirror of
https://gitee.com/liuxioabin/fengketrade.git
synced 2026-04-17 21:03:17 +08:00
398 lines
14 KiB
PHP
398 lines
14 KiB
PHP
<?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);
|
||
}
|
||
}
|