2025-10-20 15:51:06 +08:00

372 lines
12 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());
}
}
/**
* 支付回调 (前端调用)
*
* 说明:
* 前端调起支付后,建行App会跳转回H5页面
* H5页面需要调用此接口通知后端支付成功
*
* @return void
*/
public function callback()
{
try {
// 1. 获取参数
$orderId = $this->request->post('order_id', 0);
$transId = $this->request->post('trans_id', '');
$payTime = $this->request->post('pay_time', '');
if (empty($orderId)) {
$this->error('订单ID不能为空');
}
// 2. 查询订单
$order = OrderModel::where('id', $orderId)->find();
if (!$order) {
$this->error('订单不存在');
}
// 3. 检查订单状态
if ($order['status'] == 'paid' || $order['status'] == 'completed') {
$this->success('订单已支付', [
'order_id' => $order->id,
'order_sn' => $order->order_sn,
'status' => $order['status'],
]);
return;
}
// 4. 验证支付结果 (调用建行查询接口)
$verifyResult = $this->paymentService->verifyPayment($order->order_sn);
if (!$verifyResult) {
$this->error('支付验证失败,请稍后再试');
}
// 5. 更新订单状态(防止重复支付)
Db::startTrans();
try {
// ⚠️ 使用原子性更新,防止并发重复支付
$affectedRows = Db::name('shopro_order')
->where('id', $order->id)
->where('status', 'unpaid') // 只更新未支付的订单
->update([
'status' => 'paid',
'paid_time' => time() * 1000, // Shopro使用毫秒时间戳
'updatetime' => time()
]);
if ($affectedRows === 0) {
// 订单状态不正确或已支付,回滚事务
Db::rollback();
// 检查订单当前状态
$order->refresh();
if ($order->status === 'paid') {
// 订单已支付,直接返回成功
$this->success('订单已支付', [
'order_id' => $order->id,
'order_sn' => $order->order_sn,
'status' => 'paid',
]);
return;
} else {
throw new Exception('订单状态异常,无法更新为已支付');
}
}
// 6. 推送订单状态到建行
$this->pushOrderToCcb($order);
// 7. 更新支付日志
$this->updatePaymentLog($order->ccb_pay_flow_id, [
'status' => 1,
'pay_time' => time(),
'trans_id' => $transId,
]);
Db::commit();
Log::info('[建行支付] 支付成功 order_id:' . $order->id . ' order_sn:' . $order->order_sn . ' trans_id:' . $transId);
$this->success('支付成功', [
'order_id' => $order->id,
'order_sn' => $order->order_sn,
'status' => 'paid',
]);
} catch (Exception $e) {
Db::rollback();
throw $e;
}
} catch (Exception $e) {
Log::error('[建行支付] 支付回调失败 order_id:' . ($orderId ?? 0) . ' error:' . $e->getMessage());
$this->error('支付处理失败: ' . $e->getMessage());
}
}
/**
* 建行支付通知 (建行服务器回调)
*
* 说明:
* 建行支付成功后,会向notify_url发送支付通知
* 这是服务器到服务器的回调,需要验签
*
* ⚠️ 重要:此接口为建行服务器异步回调,必须返回纯文本 'SUCCESS' 或 'FAIL'
*
* @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参数');
echo 'FAIL';
return;
}
// 6. 调用支付服务处理通知
$result = $this->paymentService->handleNotify($params);
// 7. 返回处理结果
// ⚠️ 重要: 必须使用exit直接退出,防止ThinkPHP框架追加额外内容
// 建行要求返回纯文本 'SUCCESS' 或 'FAIL',任何额外字符都会导致建行认为通知失败
Log::info('[建行通知] 处理完成,返回: ' . strtoupper($result));
// 直接退出,确保只输出SUCCESS/FAIL
exit(strtoupper($result));
} catch (Exception $e) {
Log::error('[建行通知] 处理失败 error:' . $e->getMessage());
// 异常情况也要直接退出
exit('FAIL');
}
}
/**
* 推送订单到建行
*
* @param object $order 订单对象
* @return void
*/
private function pushOrderToCcb($order)
{
// 获取订单商品列表
$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']) {
Log::warning('[建行推送] 订单推送失败 order_id:' . $order->id . ' error:' . ($result['message'] ?? ''));
} else {
// 更新同步状态
$order->ccb_sync_status = 1;
$order->ccb_sync_time = time();
$order->save();
Log::info('[建行推送] 订单推送成功 order_id:' . $order->id . ' order_sn:' . $order->order_sn);
}
}
/**
* 保存支付日志
*
* @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);
}
}