505 lines
21 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-22 20:32:53 +08:00
protected $noNeedLogin = ['notify', 'refundNotify'];
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不能为空');
}
2025-10-22 21:06:35 +08:00
// 2. 查询订单包含ccb_pay_flow_id字段用于判断是否已推送
2025-10-17 16:32:16 +08:00
$order = OrderModel::where('id', $orderId)
->where('user_id', $this->auth->id)
2025-10-22 21:06:35 +08:00
->field('id, order_sn, status, pay_fee, ccb_pay_flow_id')
2025-10-17 16:32:16 +08:00
->find();
if (!$order) {
$this->error('订单不存在');
}
// 3. 检查订单状态
if ($order['status'] != 'unpaid') {
$this->error('订单已支付或已关闭');
}
2025-10-22 21:06:35 +08:00
// 4. ✅ 判断是否已推送过订单根据ccb_pay_flow_id判断
$payFlowId = $order['ccb_pay_flow_id'];
$needPushOrder = empty($payFlowId);
if ($needPushOrder) {
// 4.1 生成新的支付流水号(统一标识,用于订单推送和支付串生成)
// 格式: PAY + 年月日时分秒(14位) + 随机数(6位) = 23位
$payFlowId = 'PAY' . date('YmdHis') . mt_rand(100000, 999999);
Log::info('[建行支付] 首次支付,生成支付流水号 pay_flow_id:' . $payFlowId . ' order_id:' . $orderId);
// 4.2 推送订单到建行生活步骤2调用A3341TP01订单推送接口
// ⚠️ 重要:必须先推送订单,收银台才能校验订单信息
// 根据《5.6.2 业务流程说明》步骤2由服务方调用订单推送接口向建行生活推送订单信息
try {
$pushResult = $this->orderService->pushOrder($orderId, $payFlowId);
if (!$pushResult['status']) {
// ⚠️ 推送失败必须阻塞支付流程!收银台会找不到订单
Log::error('[建行支付] 订单推送失败(阻塞支付) order_id:' . $orderId . ' error:' . $pushResult['message']);
$this->error('订单推送失败,无法生成支付串: ' . $pushResult['message']);
}
2025-10-21 14:33:20 +08:00
2025-10-22 21:06:35 +08:00
Log::info('[建行支付] 订单推送成功 order_id:' . $orderId);
2025-10-21 14:33:20 +08:00
2025-10-22 21:06:35 +08:00
} catch (Exception $e) {
// ⚠️ 推送异常必须阻塞支付流程
Log::error('[建行支付] 订单推送异常(阻塞支付) order_id:' . $orderId . ' error:' . $e->getMessage());
$this->error('订单推送异常,无法生成支付串: ' . $e->getMessage());
2025-10-21 14:33:20 +08:00
}
2025-10-22 21:06:35 +08:00
} else {
// 4.3 已推送过,复用现有支付流水号
Log::info('[建行支付] 订单已推送过,复用支付流水号 pay_flow_id:' . $payFlowId . ' order_id:' . $orderId);
2025-10-21 14:33:20 +08:00
}
2025-10-22 21:06:35 +08:00
// 5. 生成支付串步骤3调用收银台
2025-10-21 14:33:20 +08:00
$result = $this->paymentService->generatePaymentString($orderId, $payFlowId);
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-22 21:06:35 +08:00
// 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
* ⚠️ 重要说明:
2025-10-22 21:06:35 +08:00
* 1. 本接口首先查询本地订单状态(已支付直接返回)
* 2. 若本地未支付则调用建行API查询实际支付状态补偿机制
* 3. 若建行返回已支付,更新本地订单并同步到建行外联系统
* 4. 使用事务+行锁保证幂等性避免与notify()冲突
2025-10-20 16:36:06 +08:00
*
2025-10-22 21:06:35 +08:00
* 流程:
* 前端调起支付 建行处理 建行异步通知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
{
2025-10-22 21:06:35 +08:00
$orderId = null;
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-22 21:06:35 +08:00
// 2. 查询本地订单状态(不加锁,快速返回)
2025-10-20 16:36:06 +08:00
$order = OrderModel::where('id', $orderId)
->where('user_id', $this->auth->id)
2025-10-22 21:06:35 +08:00
->field('id, order_sn, status, paid_time, ccb_pay_flow_id, pay_fee')
2025-10-20 16:36:06 +08:00
->find();
2025-10-17 16:32:16 +08:00
if (!$order) {
$this->error('订单不存在');
}
2025-10-22 21:06:35 +08:00
// 3. 如果订单已支付直接返回幂等性避免重复查询建行API
if (in_array($order->status, ['paid', 'completed', 'success'])) {
Log::info('[建行支付查询] 订单已支付,直接返回 order_id:' . $orderId . ' order_sn:' . $order->order_sn);
$this->success('查询成功', [
'order_id' => $order->id,
'order_sn' => $order->order_sn,
'status' => $order->status,
'is_paid' => true,
'paid_time' => $order->paid_time,
'pay_flow_id' => $order->ccb_pay_flow_id,
'query_source' => 'local' // 标记数据来源
]);
}
// 4. 订单未支付调用建行API查询实际支付状态补偿机制
if (empty($order->ccb_pay_flow_id)) {
// 没有支付流水号,说明用户还未调起支付,直接返回未支付
Log::info('[建行支付查询] 订单无支付流水号,未发起支付 order_id:' . $orderId);
$this->success('查询成功', [
'order_id' => $order->id,
'order_sn' => $order->order_sn,
'status' => $order->status,
'is_paid' => false,
'paid_time' => null,
'pay_flow_id' => null,
'query_source' => 'local'
]);
}
Log::info('[建行支付查询] 调用建行API查询支付状态 order_id:' . $orderId . ' order_sn:' . $order->order_sn . ' pay_flow_id:' . $order->ccb_pay_flow_id);
// 5. 调用建行订单查询接口
// 参数说明orderSn=支付流水号, startTime=7天前, endTime=当前, page=1, txType=0(支付交易), txnStatus=00(成功)
$queryResult = $this->orderService->queryOrder(
$order->ccb_pay_flow_id, // 使用支付流水号查询
date('YmdHis', strtotime('-7 days')), // 开始时间
date('YmdHis'), // 结束时间
1, // 页码
'0', // 交易类型0-支付交易
'00' // 交易状态00-成功
);
// 6. 处理建行查询结果
if (!$queryResult['status']) {
// 建行API调用失败返回本地状态
Log::warning('[建行支付查询] 建行API调用失败返回本地状态 order_id:' . $orderId . ' error:' . $queryResult['message']);
$this->success('查询成功', [
'order_id' => $order->id,
'order_sn' => $order->order_sn,
'status' => $order->status,
'is_paid' => false,
'paid_time' => null,
'pay_flow_id' => $order->ccb_pay_flow_id,
'query_source' => 'local',
'ccb_error' => $queryResult['message']
]);
}
// 7. 解析建行返回数据
$ccbData = $queryResult['data']['CLD_BODY'] ?? [];
2025-10-27 20:28:01 +08:00
// ✅ 修复:建行实际返回字段名是 LIST不是 TXN_LST
$txnList = $ccbData['LIST'] ?? [];
2025-10-22 21:06:35 +08:00
// 8. 检查是否有支付成功记录
$isPaidInCcb = false;
$ccbPayTime = null;
$ccbTransId = null;
if (!empty($txnList)) {
foreach ($txnList as $txn) {
// TXN_STATUS=00 表示交易成功
if (isset($txn['TXN_STATUS']) && $txn['TXN_STATUS'] == '00') {
$isPaidInCcb = true;
2025-10-27 20:28:01 +08:00
// ✅ 修复:建行实际返回的交易时间字段是 CLRG_STM_DT_TM清算时间
$ccbPayTime = $txn['CLRG_STM_DT_TM'] ?? null; // 交易时间 YYYYMMDDHHmmss
// ✅ 修复:建行实际返回的交易流水号是 EBNK_VCHR_NO电子银行凭证号
// 备选字段ATM_TXN_SNATM交易流水号
$ccbTransId = $txn['EBNK_VCHR_NO'] ?? $txn['ATM_TXN_SN'] ?? null;
2025-10-22 21:06:35 +08:00
break;
}
}
}
// 9. 如果建行返回未支付,直接返回
if (!$isPaidInCcb) {
Log::info('[建行支付查询] 建行返回未支付 order_id:' . $orderId . ' order_sn:' . $order->order_sn);
$this->success('查询成功', [
'order_id' => $order->id,
'order_sn' => $order->order_sn,
'status' => $order->status,
'is_paid' => false,
'paid_time' => null,
'pay_flow_id' => $order->ccb_pay_flow_id,
'query_source' => 'ccb'
]);
}
// 10. ✅ 建行返回已支付,启动事务更新本地订单
Log::info('[建行支付查询] 建行返回已支付,开始更新本地订单 order_id:' . $orderId . ' order_sn:' . $order->order_sn);
Db::startTrans();
try {
// 10.1 使用行锁重新查询订单状态(避免并发冲突)
$lockedOrder = OrderModel::where('id', $orderId)
->where('user_id', $this->auth->id)
->lock(true) // 悲观锁FOR UPDATE
->find();
if (!$lockedOrder) {
throw new Exception('订单不存在或无权限');
}
// 10.2 再次检查订单状态可能已被notify()更新)
if (in_array($lockedOrder->status, ['paid', 'completed', 'success'])) {
Db::commit();
Log::info('[建行支付查询] 订单已被其他流程更新为已支付,跳过更新 order_id:' . $orderId);
$this->success('查询成功', [
'order_id' => $lockedOrder->id,
'order_sn' => $lockedOrder->order_sn,
'status' => $lockedOrder->status,
'is_paid' => true,
'paid_time' => $lockedOrder->paid_time,
'pay_flow_id' => $lockedOrder->ccb_pay_flow_id,
'query_source' => 'local_concurrent'
]);
}
// 10.3 更新订单状态为已支付
$paidTime = time() * 1000; // Shopro使用毫秒时间戳
OrderModel::where('id', $orderId)->update([
'status' => 'paid',
'paid_time' => $paidTime,
'updatetime' => time()
]);
Log::info('[建行支付查询] 本地订单状态更新成功 order_id:' . $orderId . ' status:paid');
// 10.4 提交事务
Db::commit();
// 11. ✅ 更新订单状态到建行外联系统(异步,失败不影响本地)
try {
$updateResult = $this->orderService->updateOrderStatus($orderId, '1'); // 1-支付成功
if ($updateResult['status']) {
Log::info('[建行支付查询] 订单状态同步到建行成功 order_id:' . $orderId);
} else {
Log::warning('[建行支付查询] 订单状态同步到建行失败(本地已更新) order_id:' . $orderId . ' error:' . $updateResult['message']);
}
} catch (Exception $e) {
Log::error('[建行支付查询] 订单状态同步到建行异常(本地已更新) order_id:' . $orderId . ' error:' . $e->getMessage());
}
// 12. 返回成功结果
$this->success('查询成功', [
'order_id' => $order->id,
'order_sn' => $order->order_sn,
'status' => 'paid',
'is_paid' => true,
'paid_time' => $paidTime,
'pay_flow_id' => $order->ccb_pay_flow_id,
'query_source' => 'ccb_updated' // 标记:从建行查询并更新
]);
} catch (Exception $e) {
// 回滚事务
Db::rollback();
throw $e;
}
2025-10-17 16:32:16 +08:00
2025-10-20 16:36:06 +08:00
} catch (Exception $e) {
2025-10-22 21:06:35 +08:00
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
*/
2025-10-22 20:32:53 +08:00
/**
* 建行生活支付通知接口
*
* 📋 接口说明文档7.1
* - 建行生活主动推送支付结果
* - 不会附带服务方编号
* - 通过 REMARK2 字段识别服务方
* - SIGN 字段使用商户私钥签名,需用建行公钥验签
*
* @return void
*/
2025-10-17 16:32:16 +08:00
public function notify()
{
try {
2025-10-18 01:16:25 +08:00
// 1. 获取原始请求数据
$rawData = file_get_contents('php://input');
2025-10-22 20:32:53 +08:00
Log::info('[建行支付通知] 收到异步通知: ' . $rawData);
2025-10-18 01:16:25 +08:00
// 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. 记录参数
2025-10-22 20:32:53 +08:00
Log::info('[建行支付通知] 解析参数: ' . json_encode($params, JSON_UNESCAPED_UNICODE));
2025-10-18 01:16:25 +08:00
// 5. 验证必需参数
if (empty($params['ORDERID'])) {
2025-10-22 20:32:53 +08:00
Log::error('[建行支付通知] 缺少ORDERID参数');
exit('FAIL');
}
if (empty($params['SIGN'])) {
Log::error('[建行支付通知] 缺少SIGN签名');
exit('FAIL');
}
if (empty($params['SUCCESS'])) {
Log::error('[建行支付通知] 缺少SUCCESS字段');
2025-10-20 16:36:06 +08:00
exit('FAIL');
2025-10-18 01:16:25 +08:00
}
2025-10-22 20:32:53 +08:00
// 6. ✅ 验证签名(使用建行公钥验签)
try {
$signature = $params['SIGN'];
$verifyParams = $params; // 复制参数用于验签
// 加载建行公钥配置
$configFile = __DIR__ . '/../config/ccblife.php';
if (!file_exists($configFile)) {
throw new \Exception('建行生活配置文件不存在');
}
$config = include $configFile;
// 验签
$verifyResult = \addons\shopro\library\ccblife\CcbRSA::verifyNotify(
$verifyParams,
$signature,
$config['ccb_public_key'] // 建行公钥
);
if (!$verifyResult) {
Log::error('[建行支付通知] 签名验证失败 ORDERID:' . $params['ORDERID']);
exit('FAIL');
}
Log::info('[建行支付通知] 签名验证成功 ORDERID:' . $params['ORDERID']);
} catch (\Exception $e) {
Log::error('[建行支付通知] 签名验证异常: ' . $e->getMessage());
exit('FAIL');
}
// 7. 检查支付状态
if ($params['SUCCESS'] !== 'Y') {
Log::warning('[建行支付通知] 支付未成功 ORDERID:' . $params['ORDERID'] . ' SUCCESS:' . $params['SUCCESS']);
exit('SUCCESS'); // ⚠️ 仍然返回SUCCESS表示通知已接收
}
// 8. 调用支付服务处理通知(返回订单ID)
2025-10-18 01:16:25 +08:00
$result = $this->paymentService->handleNotify($params);
2025-10-22 20:32:53 +08:00
// 9. ✅ 处理成功后更新订单状态到建行(步骤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
// 调用订单更新接口,将订单状态从未支付更新为已支付
2025-10-22 20:32:53 +08:00
$updateResult = $this->orderService->updateOrderStatus($result['order_id'], '1'); // 1-支付成功
2025-10-20 17:10:16 +08:00
if ($updateResult['status']) {
2025-10-22 20:32:53 +08:00
Log::info('[建行支付通知] 订单状态更新成功 order_id:' . $result['order_id']);
2025-10-20 17:10:16 +08:00
} else {
// ⚠️ 更新失败不影响本地支付状态,记录日志后续补推
2025-10-22 20:32:53 +08:00
Log::warning('[建行支付通知] 订单状态更新失败(本地已支付) order_id:' . $result['order_id'] . ' error:' . $updateResult['message']);
2025-10-20 17:10:16 +08:00
}
2025-10-20 16:36:06 +08:00
} catch (Exception $e) {
2025-10-20 17:10:16 +08:00
// ⚠️ 更新异常不影响支付成功,记录日志后续补推
2025-10-22 20:32:53 +08:00
Log::error('[建行支付通知] 订单状态更新异常(本地已支付) order_id:' . $result['order_id'] . ' error:' . $e->getMessage());
2025-10-20 16:36:06 +08:00
}
} else {
2025-10-22 20:32:53 +08:00
Log::info('[建行支付通知] 订单已支付且已更新,跳过更新 order_id:' . $result['order_id']);
2025-10-20 16:36:06 +08:00
}
}
2025-10-22 20:32:53 +08:00
// 10. 返回处理结果
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';
2025-10-22 20:32:53 +08:00
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) {
2025-10-22 20:32:53 +08:00
Log::error('[建行支付通知] 处理失败 error:' . $e->getMessage());
// 异常情况也要直接退出
exit('FAIL');
}
}
2025-10-17 16:32:16 +08:00
}