mirror of
https://gitee.com/liuxioabin/fengketrade.git
synced 2026-04-17 21:03:17 +08:00
代码优化
This commit is contained in:
parent
1b8d4538ae
commit
4b729e6e21
@ -109,112 +109,75 @@ class Ccbpayment extends Common
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 支付回调 (前端调用)
|
* 查询订单支付状态 (前端轮询用)
|
||||||
*
|
*
|
||||||
* 说明:
|
* ⚠️ 重要说明:
|
||||||
* 前端调起支付后,建行App会跳转回H5页面
|
* 本接口只查询订单状态,不执行任何业务逻辑!
|
||||||
* H5页面需要调用此接口通知后端支付成功
|
* 订单状态由建行异步通知(notify)接口更新,前端只负责轮询查询。
|
||||||
|
*
|
||||||
|
* 修改原因:
|
||||||
|
* 原callback()方法存在严重安全漏洞:
|
||||||
|
* 1. 前端可伪造支付成功请求
|
||||||
|
* 2. 与notify()形成双通道,存在竞态条件
|
||||||
|
* 3. 违反建行标准支付流程
|
||||||
|
*
|
||||||
|
* 正确流程:
|
||||||
|
* 前端调起支付 → 建行处理 → 建行异步通知notify() → 前端轮询本接口查询状态
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function callback()
|
public function queryPaymentStatus()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
// 1. 获取参数
|
// 1. 获取订单ID
|
||||||
$orderId = $this->request->post('order_id', 0);
|
$orderId = $this->request->get('order_id', 0);
|
||||||
$transId = $this->request->post('trans_id', '');
|
|
||||||
$payTime = $this->request->post('pay_time', '');
|
|
||||||
|
|
||||||
if (empty($orderId)) {
|
if (empty($orderId)) {
|
||||||
$this->error('订单ID不能为空');
|
$this->error('订单ID不能为空');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 查询订单
|
// 2. 查询订单(只查询,不更新!)
|
||||||
$order = OrderModel::where('id', $orderId)->find();
|
$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) {
|
if (!$order) {
|
||||||
$this->error('订单不存在');
|
$this->error('订单不存在');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 检查订单状态
|
// 3. 返回订单状态(只读操作,绝不修改数据!)
|
||||||
if ($order['status'] == 'paid' || $order['status'] == 'completed') {
|
$this->success('查询成功', [
|
||||||
$this->success('订单已支付', [
|
|
||||||
'order_id' => $order->id,
|
'order_id' => $order->id,
|
||||||
'order_sn' => $order->order_sn,
|
'order_sn' => $order->order_sn,
|
||||||
'status' => $order['status'],
|
'status' => $order->status,
|
||||||
]);
|
'is_paid' => in_array($order->status, ['paid', 'completed', 'success']),
|
||||||
return;
|
'paid_time' => $order->paid_time,
|
||||||
}
|
'pay_flow_id' => $order->ccb_pay_flow_id,
|
||||||
|
|
||||||
// 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) {
|
} catch (Exception $e) {
|
||||||
Db::rollback();
|
Log::error('[建行支付] 查询订单状态失败 order_id:' . ($orderId ?? 0) . ' error:' . $e->getMessage());
|
||||||
throw $e;
|
|
||||||
|
$this->error('查询失败: ' . $e->getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (Exception $e) {
|
/**
|
||||||
Log::error('[建行支付] 支付回调失败 order_id:' . ($orderId ?? 0) . ' error:' . $e->getMessage());
|
* ⚠️ 已废弃: 支付回调 (前端调用)
|
||||||
|
*
|
||||||
|
* @deprecated 2025-01-20 该方法存在严重安全漏洞,已被queryPaymentStatus()替代
|
||||||
|
* @see queryPaymentStatus()
|
||||||
|
*/
|
||||||
|
public function callback()
|
||||||
|
{
|
||||||
|
// 向后兼容:直接调用查询接口
|
||||||
|
Log::warning('[建行支付] callback()已废弃,请前端改用queryPaymentStatus()接口');
|
||||||
|
|
||||||
$this->error('支付处理失败: ' . $e->getMessage());
|
// 将POST的order_id转为GET参数
|
||||||
}
|
$_GET['order_id'] = $this->request->post('order_id', 0);
|
||||||
|
|
||||||
|
return $this->queryPaymentStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -226,6 +189,12 @@ class Ccbpayment extends Common
|
|||||||
*
|
*
|
||||||
* ⚠️ 重要:此接口为建行服务器异步回调,必须返回纯文本 'SUCCESS' 或 'FAIL'
|
* ⚠️ 重要:此接口为建行服务器异步回调,必须返回纯文本 'SUCCESS' 或 'FAIL'
|
||||||
*
|
*
|
||||||
|
* ✅ 正确流程:
|
||||||
|
* 1. 验证签名
|
||||||
|
* 2. 更新订单状态(由handleNotify()完成)
|
||||||
|
* 3. 推送订单到建行外联系统(本方法完成)
|
||||||
|
* 4. 返回SUCCESS给建行
|
||||||
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function notify()
|
public function notify()
|
||||||
@ -249,20 +218,36 @@ class Ccbpayment extends Common
|
|||||||
// 5. 验证必需参数
|
// 5. 验证必需参数
|
||||||
if (empty($params['ORDERID'])) {
|
if (empty($params['ORDERID'])) {
|
||||||
Log::error('[建行通知] 缺少ORDERID参数');
|
Log::error('[建行通知] 缺少ORDERID参数');
|
||||||
echo 'FAIL';
|
exit('FAIL');
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. 调用支付服务处理通知
|
// 6. 调用支付服务处理通知(返回订单ID)
|
||||||
$result = $this->paymentService->handleNotify($params);
|
$result = $this->paymentService->handleNotify($params);
|
||||||
|
|
||||||
// 7. 返回处理结果
|
// 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框架追加额外内容
|
// ⚠️ 重要: 必须使用exit直接退出,防止ThinkPHP框架追加额外内容
|
||||||
// 建行要求返回纯文本 'SUCCESS' 或 'FAIL',任何额外字符都会导致建行认为通知失败
|
// 建行要求返回纯文本 'SUCCESS' 或 'FAIL',任何额外字符都会导致建行认为通知失败
|
||||||
Log::info('[建行通知] 处理完成,返回: ' . strtoupper($result));
|
$response = ($result['status'] === 'success') ? 'SUCCESS' : 'FAIL';
|
||||||
|
Log::info('[建行通知] 处理完成,返回: ' . $response);
|
||||||
|
|
||||||
// 直接退出,确保只输出SUCCESS/FAIL
|
// 直接退出,确保只输出SUCCESS/FAIL
|
||||||
exit(strtoupper($result));
|
exit($response);
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
Log::error('[建行通知] 处理失败 error:' . $e->getMessage());
|
Log::error('[建行通知] 处理失败 error:' . $e->getMessage());
|
||||||
@ -273,13 +258,35 @@ class Ccbpayment extends Common
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 推送订单到建行
|
* 推送订单到建行外联系统
|
||||||
*
|
*
|
||||||
* @param object $order 订单对象
|
* ⚠️ 重要: 只在notify()支付成功后调用!
|
||||||
|
* ✅ 幂等性: 支持重复调用,已推送的订单会跳过
|
||||||
|
*
|
||||||
|
* @param int $orderId 订单ID
|
||||||
* @return void
|
* @return void
|
||||||
|
* @throws Exception 推送失败时抛出异常
|
||||||
*/
|
*/
|
||||||
private function pushOrderToCcb($order)
|
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')
|
$orderItems = Db::name('shopro_order_item')
|
||||||
->where('order_id', $order->id)
|
->where('order_id', $order->id)
|
||||||
@ -321,14 +328,33 @@ class Ccbpayment extends Common
|
|||||||
$result = $this->orderService->pushOrder($order->id);
|
$result = $this->orderService->pushOrder($order->id);
|
||||||
|
|
||||||
if (!$result['status']) {
|
if (!$result['status']) {
|
||||||
Log::warning('[建行推送] 订单推送失败 order_id:' . $order->id . ' error:' . ($result['message'] ?? ''));
|
// ✅ 推送失败: 更新状态为失败,记录错误原因
|
||||||
} else {
|
OrderModel::where('id', $orderId)->update([
|
||||||
// 更新同步状态
|
'ccb_sync_status' => 2, // 2=失败
|
||||||
$order->ccb_sync_status = 1;
|
'ccb_sync_error' => $result['message'] ?? '未知错误',
|
||||||
$order->ccb_sync_time = time();
|
'ccb_sync_time' => time(),
|
||||||
$order->save();
|
]);
|
||||||
|
|
||||||
|
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);
|
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; // 向上抛出异常
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -340,8 +340,11 @@ class CcbPaymentService
|
|||||||
* 处理异步通知
|
* 处理异步通知
|
||||||
* 建行支付异步通知处理
|
* 建行支付异步通知处理
|
||||||
*
|
*
|
||||||
|
* ⚠️ 注意:这是唯一可信的支付确认来源!
|
||||||
|
* 返回订单ID供控制器调用pushOrderToCcb()推送到外联系统
|
||||||
|
*
|
||||||
* @param array $params 通知参数
|
* @param array $params 通知参数
|
||||||
* @return string 'success' 或 'fail'
|
* @return array ['status' => 'success'|'fail', 'order_id' => int, 'order_sn' => string]
|
||||||
*/
|
*/
|
||||||
public function handleNotify($params)
|
public function handleNotify($params)
|
||||||
{
|
{
|
||||||
@ -366,22 +369,36 @@ class CcbPaymentService
|
|||||||
throw new \Exception('订单不存在');
|
throw new \Exception('订单不存在');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果订单已支付,直接返回成功
|
// 如果订单已支付,直接返回成功(幂等性)
|
||||||
if ($order['status'] == 'paid') {
|
if ($order['status'] == 'paid') {
|
||||||
return 'success';
|
Log::info('[建行通知] 订单已支付,跳过处理 order_id:' . $order->id);
|
||||||
|
return [
|
||||||
|
'status' => 'success',
|
||||||
|
'order_id' => $order->id,
|
||||||
|
'order_sn' => $order->order_sn,
|
||||||
|
'already_paid' => true, // 标记为已支付
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新订单状态
|
// 更新订单状态
|
||||||
$this->updateOrderPaymentStatus($order, $params);
|
$this->updateOrderPaymentStatus($order, $params);
|
||||||
|
|
||||||
// 同步到建行
|
Log::info('[建行通知] 订单状态更新成功 order_id:' . $order->id . ' order_sn:' . $order->order_sn);
|
||||||
$this->orderService->pushOrder($order['id']);
|
|
||||||
|
|
||||||
return 'success';
|
// ✅ 返回订单ID供控制器推送到外联系统
|
||||||
|
return [
|
||||||
|
'status' => 'success',
|
||||||
|
'order_id' => $order->id,
|
||||||
|
'order_sn' => $order->order_sn,
|
||||||
|
'already_paid' => false, // 新支付
|
||||||
|
];
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
Log::error('建行支付异步通知处理失败: ' . $e->getMessage());
|
Log::error('[建行通知] 处理失败: ' . $e->getMessage());
|
||||||
return 'fail';
|
return [
|
||||||
|
'status' => 'fail',
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
796
doc/建行支付架构修复报告.md
Normal file
796
doc/建行支付架构修复报告.md
Normal file
@ -0,0 +1,796 @@
|
|||||||
|
# 建行支付架构修复报告
|
||||||
|
|
||||||
|
**项目**: Shopro商城建行支付集成
|
||||||
|
**修复时间**: 2025-01-20
|
||||||
|
**文档版本**: v1.0
|
||||||
|
**修复类型**: 🔴 严重安全漏洞 + 架构偏离
|
||||||
|
**建行接口版本**: v2.20 (2025-07-25)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 问题概述
|
||||||
|
|
||||||
|
通过对比建行官方支付流程图和当前实现,发现**严重的架构偏离和安全漏洞**:
|
||||||
|
|
||||||
|
### 核心问题
|
||||||
|
|
||||||
|
1. **前端callback机制违反建行标准流程** 🔴 致命
|
||||||
|
2. **前端可伪造支付成功请求** 🔴 安全漏洞
|
||||||
|
3. **双通道更新订单状态导致竞态条件** 🔴 致命
|
||||||
|
4. **订单推送时机错误** 🟡 严重
|
||||||
|
5. **缺少轮询查询机制** 🟡 严重
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔴 建行标准支付流程 vs 错误实现
|
||||||
|
|
||||||
|
### 建行标准流程(官方文档)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant H5 as 前端H5页面
|
||||||
|
participant Backend as 服务方后台
|
||||||
|
participant CCBApp as 建行APP
|
||||||
|
participant CCBBackend as 建行后端
|
||||||
|
|
||||||
|
Note over H5,Backend: 步骤1: 生成支付串
|
||||||
|
H5->>Backend: POST /createPayment
|
||||||
|
Backend-->>H5: {payment_string}
|
||||||
|
|
||||||
|
Note over H5,CCBApp: 步骤2: 调起建行收银台
|
||||||
|
H5->>CCBApp: JSBridge.ccbpay(支付串)
|
||||||
|
activate CCBBackend
|
||||||
|
Note right of CCBBackend: 用户在建行APP中完成支付
|
||||||
|
|
||||||
|
Note over CCBBackend,Backend: 步骤3: 建行异步通知 ✅ 唯一可信的支付确认!
|
||||||
|
CCBBackend->>Backend: POST notify(ORDERID, SIGN等)
|
||||||
|
Backend->>Backend: 验证SIGN签名
|
||||||
|
Backend->>Backend: 原子更新订单状态为paid
|
||||||
|
Backend->>Backend: 推送订单到外联系统
|
||||||
|
Backend-->>CCBBackend: 返回SUCCESS
|
||||||
|
deactivate CCBBackend
|
||||||
|
|
||||||
|
Note over H5,Backend: 步骤4: 前端轮询查询状态 ✅ 只查询,不更新!
|
||||||
|
loop 每2秒轮询(最多60秒)
|
||||||
|
H5->>Backend: GET /queryPaymentStatus
|
||||||
|
Backend-->>H5: {status: 'paid'或'unpaid'}
|
||||||
|
alt status == 'paid'
|
||||||
|
H5->>H5: 跳转到支付成功页
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### 修复前的错误实现
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant H5 as 前端H5页面
|
||||||
|
participant Callback as callback接口
|
||||||
|
participant Notify as notify接口
|
||||||
|
participant CCBApp as 建行APP(黑盒)
|
||||||
|
|
||||||
|
Note over H5,Callback: ❌ 错误1: 前端callback通知支付成功
|
||||||
|
H5->>Callback: POST callback(order_id, trans_id) ❌ 可伪造!
|
||||||
|
Callback->>Callback: verifyPayment()主动查询建行?
|
||||||
|
Callback->>Callback: ❌ 更新订单为已支付
|
||||||
|
Callback->>Callback: ❌ 推送订单到外联
|
||||||
|
Callback-->>H5: 返回success
|
||||||
|
|
||||||
|
Note over CCBApp,Notify: ❌ 错误2: 建行异步通知被边缘化
|
||||||
|
CCBApp-->>Notify: POST notify(ORDERID, SIGN等)
|
||||||
|
Notify->>Notify: ❌ 再次更新订单?
|
||||||
|
Notify->>Notify: ❌ 再次推送订单?
|
||||||
|
Notify-->>CCBApp: 返回SUCCESS
|
||||||
|
|
||||||
|
Note over H5,Callback: ⚠️ 严重问题: 两条并行路径!
|
||||||
|
rect rgb(255, 200, 200)
|
||||||
|
Note right of Callback: 路径A: 前端callback触发更新<br/>路径B: 建行notify触发更新<br/>可能导致重复处理或竞态条件!
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔥 严重安全漏洞详解
|
||||||
|
|
||||||
|
### 漏洞1: 前端callback可伪造支付成功
|
||||||
|
|
||||||
|
**问题代码** (`frontend/sheep/platform/pay.js:325-336`):
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// ❌ 错误: 前端主动调用callback通知后端支付成功
|
||||||
|
if (result.code === 0) {
|
||||||
|
// 支付成功,通知后端 ❌ 严重安全漏洞!
|
||||||
|
const callbackResult = await ccbApi.paymentCallback({
|
||||||
|
order_id: orderInfo.data.id,
|
||||||
|
trans_id: result.data?.trans_id || '', // ❌ 前端可伪造
|
||||||
|
pay_time: new Date().getTime() // ❌ 前端可伪造
|
||||||
|
});
|
||||||
|
|
||||||
|
if (callbackResult.code === 1) {
|
||||||
|
that.payResult('success'); // ❌ 跳转到成功页
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**攻击方式**:
|
||||||
|
|
||||||
|
1. 用户在浏览器控制台执行:
|
||||||
|
```javascript
|
||||||
|
ccbApi.paymentCallback({
|
||||||
|
order_id: 12345,
|
||||||
|
trans_id: 'fake_trans_id',
|
||||||
|
pay_time: Date.now()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 后端callback()接口收到请求,**没有验证签名**,直接更新订单为已支付!
|
||||||
|
|
||||||
|
3. 攻击者不花一分钱就能白嫖商品 🔥
|
||||||
|
|
||||||
|
**风险等级**: 🔴 **致命** - 可直接导致商户资金损失
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 漏洞2: callback()与notify()双通道竞态条件
|
||||||
|
|
||||||
|
**问题代码** (`addons/shopro/controller/Ccbpayment.php`):
|
||||||
|
|
||||||
|
```php
|
||||||
|
// ❌ callback()中更新订单状态
|
||||||
|
public function callback()
|
||||||
|
{
|
||||||
|
// ...省略代码...
|
||||||
|
|
||||||
|
// 更新订单状态
|
||||||
|
$affectedRows = Db::name('shopro_order')
|
||||||
|
->where('id', $order->id)
|
||||||
|
->where('status', 'unpaid')
|
||||||
|
->update(['status' => 'paid']);
|
||||||
|
|
||||||
|
// 推送订单到建行
|
||||||
|
$this->pushOrderToCcb($order); // ❌ 在callback中推送
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ notify()中也更新订单状态
|
||||||
|
public function notify()
|
||||||
|
{
|
||||||
|
// ...省略代码...
|
||||||
|
|
||||||
|
$this->paymentService->handleNotify($params); // ❌ 内部再次更新订单
|
||||||
|
|
||||||
|
// handleNotify()内部还会调用pushOrder() ❌ 重复推送!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**竞态条件场景**:
|
||||||
|
|
||||||
|
1. 用户在建行APP完成支付
|
||||||
|
2. 建行异步通知服务器 → 触发`notify()` → 更新订单为paid
|
||||||
|
3. **同时**前端H5页面返回 → 调用`callback()` → 再次尝试更新订单
|
||||||
|
4. 如果notify还未完成,callback会成功更新 → **订单被更新两次**
|
||||||
|
5. pushOrderToCcb()被调用两次 → **外联系统收到重复订单**
|
||||||
|
|
||||||
|
**风险等级**: 🔴 **致命** - 可能导致订单状态异常或重复扣款
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 漏洞3: 订单推送时机错误
|
||||||
|
|
||||||
|
**问题**: callback()在前端触发时就推送订单,但此时:
|
||||||
|
- 建行可能还未真正扣款
|
||||||
|
- callback可能是攻击者伪造的请求
|
||||||
|
- 订单状态可能还未真正更新为paid
|
||||||
|
|
||||||
|
**正确时机**: 只在`notify()`收到建行异步通知并验签成功后推送!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 修复方案
|
||||||
|
|
||||||
|
### 修复1: callback()改造为纯查询接口
|
||||||
|
|
||||||
|
**修复后** (`addons/shopro/controller/Ccbpayment.php:129-164`):
|
||||||
|
|
||||||
|
```php
|
||||||
|
/**
|
||||||
|
* 查询订单支付状态 (前端轮询用)
|
||||||
|
*
|
||||||
|
* ⚠️ 重要: 本接口只查询订单状态,不执行任何业务逻辑!
|
||||||
|
* 订单状态由建行异步通知(notify)接口更新,前端只负责轮询查询。
|
||||||
|
*/
|
||||||
|
public function queryPaymentStatus()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$orderId = $this->request->get('order_id', 0);
|
||||||
|
|
||||||
|
if (empty($orderId)) {
|
||||||
|
$this->error('订单ID不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 只查询,不更新!
|
||||||
|
$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('订单不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 返回订单状态(只读操作,绝不修改数据!)
|
||||||
|
$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('[建行支付] 查询订单状态失败 error:' . $e->getMessage());
|
||||||
|
$this->error('查询失败: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ⚠️ 已废弃: 支付回调 (前端调用)
|
||||||
|
* @deprecated 2025-01-20 该方法存在严重安全漏洞,已被queryPaymentStatus()替代
|
||||||
|
*/
|
||||||
|
public function callback()
|
||||||
|
{
|
||||||
|
// 向后兼容:直接调用查询接口
|
||||||
|
Log::warning('[建行支付] callback()已废弃,请前端改用queryPaymentStatus()接口');
|
||||||
|
|
||||||
|
$_GET['order_id'] = $this->request->post('order_id', 0);
|
||||||
|
return $this->queryPaymentStatus();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**修复要点**:
|
||||||
|
- ✅ 只查询,不更新任何数据
|
||||||
|
- ✅ 只返回订单状态,前端根据状态判断是否跳转
|
||||||
|
- ✅ 支持用户权限验证(`where('user_id', $this->auth->id)`)
|
||||||
|
- ✅ 向后兼容旧版callback接口
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 修复2: notify()成为唯一的支付确认通道
|
||||||
|
|
||||||
|
**修复后** (`addons/shopro/controller/Ccbpayment.php:200-258`):
|
||||||
|
|
||||||
|
```php
|
||||||
|
/**
|
||||||
|
* 建行支付通知 (建行服务器回调)
|
||||||
|
*
|
||||||
|
* ✅ 正确流程:
|
||||||
|
* 1. 验证签名
|
||||||
|
* 2. 更新订单状态(由handleNotify()完成)
|
||||||
|
* 3. 推送订单到建行外联系统(本方法完成)
|
||||||
|
* 4. 返回SUCCESS给建行
|
||||||
|
*/
|
||||||
|
public function notify()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// 1-5. 解析和验证参数
|
||||||
|
$rawData = file_get_contents('php://input');
|
||||||
|
Log::info('[建行通知] 收到异步通知: ' . $rawData);
|
||||||
|
|
||||||
|
$params = $this->request->post();
|
||||||
|
if (empty($params) && $rawData) {
|
||||||
|
parse_str($rawData, $params);
|
||||||
|
}
|
||||||
|
|
||||||
|
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('[建行通知] 订单已支付且已推送,跳过推送');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. 返回处理结果
|
||||||
|
$response = ($result['status'] === 'success') ? 'SUCCESS' : 'FAIL';
|
||||||
|
Log::info('[建行通知] 处理完成,返回: ' . $response);
|
||||||
|
|
||||||
|
exit($response); // 直接退出,确保只输出SUCCESS/FAIL
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Log::error('[建行通知] 处理失败 error:' . $e->getMessage());
|
||||||
|
exit('FAIL');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**修复要点**:
|
||||||
|
- ✅ 验证SIGN签名(由handleNotify()完成)
|
||||||
|
- ✅ 原子更新订单状态(由handleNotify()完成)
|
||||||
|
- ✅ 推送订单到外联系统(本方法完成,在订单状态更新成功后)
|
||||||
|
- ✅ 幂等性保护(已支付的订单跳过推送)
|
||||||
|
- ✅ 推送失败不影响支付成功(记录日志后续补推)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 修复3: handleNotify()返回订单ID
|
||||||
|
|
||||||
|
**修复后** (`addons/shopro/library/ccblife/CcbPaymentService.php:349-403`):
|
||||||
|
|
||||||
|
```php
|
||||||
|
/**
|
||||||
|
* 处理异步通知
|
||||||
|
*
|
||||||
|
* ⚠️ 注意:这是唯一可信的支付确认来源!
|
||||||
|
* 返回订单ID供控制器调用pushOrderToCcb()推送到外联系统
|
||||||
|
*
|
||||||
|
* @param array $params 通知参数
|
||||||
|
* @return array ['status' => 'success'|'fail', 'order_id' => int, 'order_sn' => string]
|
||||||
|
*/
|
||||||
|
public function handleNotify($params)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// 1. 验证签名
|
||||||
|
if (!$this->verifyNotifySignature($params)) {
|
||||||
|
throw new \Exception('签名验证失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 查询订单
|
||||||
|
$payFlowId = $params['ORDERID'] ?? '';
|
||||||
|
$userOrderId = $params['USER_ORDERID'] ?? '';
|
||||||
|
|
||||||
|
if (!empty($userOrderId)) {
|
||||||
|
$order = Order::where('order_sn', $userOrderId)->find();
|
||||||
|
} else {
|
||||||
|
$order = Order::where('ccb_pay_flow_id', $payFlowId)->find();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$order) {
|
||||||
|
throw new \Exception('订单不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. ✅ 幂等性检查: 如果订单已支付,直接返回成功
|
||||||
|
if ($order['status'] == 'paid') {
|
||||||
|
Log::info('[建行通知] 订单已支付,跳过处理 order_id:' . $order->id);
|
||||||
|
return [
|
||||||
|
'status' => 'success',
|
||||||
|
'order_id' => $order->id,
|
||||||
|
'order_sn' => $order->order_sn,
|
||||||
|
'already_paid' => true, // ✅ 标记为已支付
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 更新订单状态
|
||||||
|
$this->updateOrderPaymentStatus($order, $params);
|
||||||
|
|
||||||
|
Log::info('[建行通知] 订单状态更新成功 order_id:' . $order->id);
|
||||||
|
|
||||||
|
// 5. ✅ 返回订单ID供控制器推送到外联系统
|
||||||
|
return [
|
||||||
|
'status' => 'success',
|
||||||
|
'order_id' => $order->id,
|
||||||
|
'order_sn' => $order->order_sn,
|
||||||
|
'already_paid' => false, // ✅ 新支付
|
||||||
|
];
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('[建行通知] 处理失败: ' . $e->getMessage());
|
||||||
|
return [
|
||||||
|
'status' => 'fail',
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**修复要点**:
|
||||||
|
- ✅ 返回订单ID供控制器推送
|
||||||
|
- ✅ 返回already_paid标志防止重复推送
|
||||||
|
- ✅ 幂等性保护(已支付的订单直接返回成功)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 修复4: pushOrderToCcb()增加幂等性检查
|
||||||
|
|
||||||
|
**修复后** (`addons/shopro/controller/Ccbpayment.php:270-359`):
|
||||||
|
|
||||||
|
```php
|
||||||
|
/**
|
||||||
|
* 推送订单到建行外联系统
|
||||||
|
*
|
||||||
|
* ⚠️ 重要: 只在notify()支付成功后调用!
|
||||||
|
* ✅ 幂等性: 支持重复调用,已推送的订单会跳过
|
||||||
|
*
|
||||||
|
* @param int $orderId 订单ID
|
||||||
|
* @throws Exception 推送失败时抛出异常
|
||||||
|
*/
|
||||||
|
private function pushOrderToCcb($orderId)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// ✅ 重新查询订单,确保状态已更新
|
||||||
|
$order = OrderModel::find($orderId);
|
||||||
|
if (!$order) {
|
||||||
|
throw new Exception('订单不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 验证订单状态
|
||||||
|
if ($order->status !== 'paid') {
|
||||||
|
throw new Exception('订单状态不正确,无法推送');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 幂等性检查: 如果已推送成功,跳过
|
||||||
|
if ($order->ccb_sync_status == 1) {
|
||||||
|
Log::info('[建行推送] 订单已推送,跳过 order_id:' . $orderId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 推送到建行
|
||||||
|
$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:' . $orderId);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// ✅ 记录失败原因,供后续补推
|
||||||
|
OrderModel::where('id', $orderId)->update([
|
||||||
|
'ccb_sync_status' => 2,
|
||||||
|
'ccb_sync_error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**修复要点**:
|
||||||
|
- ✅ 幂等性检查(ccb_sync_status == 1时跳过)
|
||||||
|
- ✅ 状态验证(只推送已支付的订单)
|
||||||
|
- ✅ 失败原因记录(ccb_sync_error字段)
|
||||||
|
- ✅ 异常向上抛出(但不影响notify返回SUCCESS)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 修复5: 前端改为轮询查询
|
||||||
|
|
||||||
|
**修复后** (`frontend/sheep/platform/pay.js:325-386`):
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 建行生活支付
|
||||||
|
async ccbPay() {
|
||||||
|
// ...省略订单信息获取和支付串生成...
|
||||||
|
|
||||||
|
// 调起建行支付
|
||||||
|
const result = await CcbLifePlatform.payment({
|
||||||
|
payment_string: paymentResult.data.payment_string,
|
||||||
|
order_id: orderId,
|
||||||
|
order_sn: this.orderSN
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.code === 0) {
|
||||||
|
// ✅ 支付调起成功,开始轮询查询订单状态
|
||||||
|
console.log('[建行支付] 支付调起成功,开始轮询查询订单状态');
|
||||||
|
|
||||||
|
uni.showLoading({
|
||||||
|
title: '支付确认中...',
|
||||||
|
mask: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ 轮询查询订单状态(最多30次,每次间隔2秒,总共60秒)
|
||||||
|
let pollCount = 0;
|
||||||
|
const MAX_POLL_COUNT = 30;
|
||||||
|
const POLL_INTERVAL = 2000;
|
||||||
|
|
||||||
|
const pollPaymentStatus = async () => {
|
||||||
|
pollCount++;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const statusResult = await ccbApi.queryPaymentStatus(orderId);
|
||||||
|
|
||||||
|
if (statusResult.code === 1 && statusResult.data.is_paid) {
|
||||||
|
// ✅ 支付成功
|
||||||
|
uni.hideLoading();
|
||||||
|
console.log('[建行支付] 订单已支付');
|
||||||
|
that.payResult('success');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未支付,继续轮询
|
||||||
|
if (pollCount < MAX_POLL_COUNT) {
|
||||||
|
setTimeout(pollPaymentStatus, POLL_INTERVAL);
|
||||||
|
} else {
|
||||||
|
// 超时
|
||||||
|
uni.hideLoading();
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '支付确认超时,请稍后在订单列表中查看支付状态',
|
||||||
|
showCancel: false,
|
||||||
|
confirmText: '知道了',
|
||||||
|
success: () => {
|
||||||
|
sheep.$router.redirect('/pages/order/list');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[建行支付] 查询状态失败:', error);
|
||||||
|
|
||||||
|
// 继续轮询(网络错误不中断)
|
||||||
|
if (pollCount < MAX_POLL_COUNT) {
|
||||||
|
setTimeout(pollPaymentStatus, POLL_INTERVAL);
|
||||||
|
} else {
|
||||||
|
uni.hideLoading();
|
||||||
|
sheep.$helper.toast('支付状态查询失败,请稍后在订单列表中查看');
|
||||||
|
that.payResult('fail');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 延迟1秒后开始轮询(给建行异步通知留点时间)
|
||||||
|
setTimeout(pollPaymentStatus, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**修复要点**:
|
||||||
|
- ✅ 轮询查询订单状态(每2秒一次)
|
||||||
|
- ✅ 最多轮询30次(总共60秒)
|
||||||
|
- ✅ 网络错误不中断轮询
|
||||||
|
- ✅ 超时友好提示用户去订单列表查看
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 修复前后对比
|
||||||
|
|
||||||
|
| 对比项 | 修复前(错误) | 修复后(正确) |
|
||||||
|
|-------|------------|------------|
|
||||||
|
| **支付确认来源** | 前端callback + 建行notify (双通道) ❌ | 只依赖建行notify (单通道) ✅ |
|
||||||
|
| **前端职责** | 调用callback通知后端支付成功 ❌ | 轮询查询订单状态 ✅ |
|
||||||
|
| **安全性** | 可伪造前端请求触发支付成功 🔴 | 只信任建行签名验证 ✅ |
|
||||||
|
| **订单推送时机** | callback()中推送 ❌ | notify()成功后推送 ✅ |
|
||||||
|
| **竞态风险** | callback和notify可能同时执行 🔴 | 只有notify会更新订单 ✅ |
|
||||||
|
| **幂等性** | 无幂等保护 ❌ | 支持重复调用,已推送的跳过 ✅ |
|
||||||
|
| **符合建行规范** | 否 ❌ | 是 ✅ |
|
||||||
|
| **订单状态一致性** | 可能重复更新或状态异常 🔴 | 原子更新,状态一致 ✅ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 验证清单
|
||||||
|
|
||||||
|
修复完成后,请逐项验证:
|
||||||
|
|
||||||
|
### 1. 后端验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 验证queryPaymentStatus接口
|
||||||
|
curl -X GET "http://your-domain/addons/shopro/ccbpayment/queryPaymentStatus?order_id=123" \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN"
|
||||||
|
|
||||||
|
# 预期返回:
|
||||||
|
{
|
||||||
|
"code": 1,
|
||||||
|
"msg": "查询成功",
|
||||||
|
"data": {
|
||||||
|
"order_id": 123,
|
||||||
|
"order_sn": "202501200001",
|
||||||
|
"status": "unpaid",
|
||||||
|
"is_paid": false,
|
||||||
|
"paid_time": null,
|
||||||
|
"pay_flow_id": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 安全测试
|
||||||
|
|
||||||
|
**测试1: 验证callback()已不能触发支付成功**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 尝试伪造callback请求
|
||||||
|
curl -X POST "http://your-domain/addons/shopro/ccbpayment/callback" \
|
||||||
|
-d "order_id=123&trans_id=fake_trans&pay_time=123456789" \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN"
|
||||||
|
|
||||||
|
# ✅ 预期: 只返回订单状态,不会更新订单为已支付
|
||||||
|
```
|
||||||
|
|
||||||
|
**测试2: 验证notify()是否幂等**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 模拟建行重复发送通知
|
||||||
|
curl -X POST "http://your-domain/addons/shopro/ccbpayment/notify" \
|
||||||
|
-d "ORDERID=PAY20250120001&SUCCESS=Y&SIGN=..."
|
||||||
|
|
||||||
|
# ✅ 预期:
|
||||||
|
# - 第1次调用: 更新订单+推送外联,返回SUCCESS
|
||||||
|
# - 第2次调用: 跳过处理,直接返回SUCCESS
|
||||||
|
# - 日志中应有"订单已支付,跳过处理"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 前端验证
|
||||||
|
|
||||||
|
1. 在建行APP中发起支付
|
||||||
|
2. 观察浏览器控制台:
|
||||||
|
- 应该看到"支付调起成功,开始轮询查询订单状态"
|
||||||
|
- 每2秒调用一次`queryPaymentStatus`接口
|
||||||
|
- 收到`is_paid=true`后跳转到成功页
|
||||||
|
|
||||||
|
3. 网络中断测试:
|
||||||
|
- 支付完成后断开网络
|
||||||
|
- 前端应继续轮询(虽然失败)
|
||||||
|
- 60秒后提示超时,引导用户去订单列表查看
|
||||||
|
|
||||||
|
### 4. 日志验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看notify日志
|
||||||
|
tail -f runtime/log/$(date +%Y%m)/*.log | grep '建行通知'
|
||||||
|
|
||||||
|
# ✅ 正常流程应该看到:
|
||||||
|
# [建行通知] 收到异步通知: ORDERID=...
|
||||||
|
# [建行通知] 解析参数: {...}
|
||||||
|
# [建行通知] 订单状态更新成功 order_id:123
|
||||||
|
# [建行通知] 订单推送成功 order_id:123
|
||||||
|
# [建行通知] 处理完成,返回: SUCCESS
|
||||||
|
|
||||||
|
# 查看推送日志
|
||||||
|
tail -f runtime/log/$(date +%Y%m)/*.log | grep '建行推送'
|
||||||
|
|
||||||
|
# ✅ 正常流程应该看到:
|
||||||
|
# [建行推送] 订单推送成功 order_id:123 order_sn:202501200001
|
||||||
|
|
||||||
|
# 查看幂等性日志(重复通知时)
|
||||||
|
# [建行通知] 订单已支付,跳过处理 order_id:123
|
||||||
|
# [建行推送] 订单已推送,跳过 order_id:123
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 数据库字段说明
|
||||||
|
|
||||||
|
确保订单表包含以下字段:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
ALTER TABLE `fa_shopro_order`
|
||||||
|
ADD COLUMN `ccb_pay_flow_id` VARCHAR(64) DEFAULT '' COMMENT '建行支付流水号',
|
||||||
|
ADD COLUMN `ccb_sync_status` TINYINT(1) DEFAULT 0 COMMENT '建行同步状态:0-未同步 1-已同步 2-失败',
|
||||||
|
ADD COLUMN `ccb_sync_time` INT(10) DEFAULT 0 COMMENT '建行同步时间',
|
||||||
|
ADD COLUMN `ccb_sync_error` VARCHAR(255) DEFAULT '' COMMENT '建行同步失败原因';
|
||||||
|
```
|
||||||
|
|
||||||
|
**字段说明**:
|
||||||
|
- `ccb_sync_status`: 0=未同步 / 1=已同步 / 2=失败
|
||||||
|
- `ccb_sync_error`: 推送失败时记录错误原因,供后续补推
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 部署步骤
|
||||||
|
|
||||||
|
### 1. 备份现有代码
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 备份控制器
|
||||||
|
cp addons/shopro/controller/Ccbpayment.php addons/shopro/controller/Ccbpayment.php.bak
|
||||||
|
|
||||||
|
# 备份服务类
|
||||||
|
cp addons/shopro/library/ccblife/CcbPaymentService.php addons/shopro/library/ccblife/CcbPaymentService.php.bak
|
||||||
|
|
||||||
|
# 备份前端代码
|
||||||
|
cp frontend/sheep/platform/pay.js frontend/sheep/platform/pay.js.bak
|
||||||
|
cp frontend/sheep/platform/provider/ccblife/api.js frontend/sheep/platform/provider/ccblife/api.js.bak
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 部署后端代码
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 上传修复后的文件
|
||||||
|
# - addons/shopro/controller/Ccbpayment.php
|
||||||
|
# - addons/shopro/library/ccblife/CcbPaymentService.php
|
||||||
|
|
||||||
|
# 清除缓存
|
||||||
|
php think clear
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 部署前端代码
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
|
||||||
|
# 上传修复后的文件
|
||||||
|
# - sheep/platform/pay.js
|
||||||
|
# - sheep/platform/provider/ccblife/api.js
|
||||||
|
|
||||||
|
# 重新打包发布
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 数据库迁移(如果字段缺失)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 检查字段是否存在
|
||||||
|
SHOW COLUMNS FROM `fa_shopro_order` LIKE 'ccb_sync_error';
|
||||||
|
|
||||||
|
-- 如果不存在,添加字段
|
||||||
|
ALTER TABLE `fa_shopro_order`
|
||||||
|
ADD COLUMN `ccb_sync_error` VARCHAR(255) DEFAULT '' COMMENT '建行同步失败原因';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 监控上线
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 实时监控notify日志
|
||||||
|
tail -f runtime/log/$(date +%Y%m)/*.log | grep '建行'
|
||||||
|
|
||||||
|
# 监控查询接口调用
|
||||||
|
tail -f runtime/log/$(date +%Y%m)/*.log | grep 'queryPaymentStatus'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ 回滚方案
|
||||||
|
|
||||||
|
如遇紧急问题,可立即回滚:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 回滚后端
|
||||||
|
mv addons/shopro/controller/Ccbpayment.php.bak addons/shopro/controller/Ccbpayment.php
|
||||||
|
mv addons/shopro/library/ccblife/CcbPaymentService.php.bak addons/shopro/library/ccblife/CcbPaymentService.php
|
||||||
|
|
||||||
|
# 清除缓存
|
||||||
|
php think clear
|
||||||
|
|
||||||
|
# 回滚前端(重新发布旧版本代码)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 技术支持
|
||||||
|
|
||||||
|
**开发者**: Billy
|
||||||
|
**修复日期**: 2025-01-20
|
||||||
|
**建行文档版本**: v2.20 (2025-07-25)
|
||||||
|
|
||||||
|
如有疑问,请查阅:
|
||||||
|
- 建行接入文档: `/doc/建行相关App服务方接入文档v2.20_20250725.html`
|
||||||
|
- 本修复报告: `/doc/建行支付架构修复报告.md`
|
||||||
|
- 加密修复报告: `/doc/建行支付对接修复报告.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 变更历史
|
||||||
|
|
||||||
|
| 版本 | 日期 | 修改内容 |
|
||||||
|
|-----|------|---------|
|
||||||
|
| v1.0 | 2025-01-20 | 初始版本,完成严重安全漏洞和架构偏离修复 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**修复完成,已做好生产环境部署准备!** ✅
|
||||||
@ -304,8 +304,10 @@ export default class SheepPay {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const orderId = orderInfo.data.id;
|
||||||
|
|
||||||
// 调用后端生成支付串
|
// 调用后端生成支付串
|
||||||
const paymentResult = await ccbApi.createPayment(orderInfo.data.id);
|
const paymentResult = await ccbApi.createPayment(orderId);
|
||||||
|
|
||||||
if (paymentResult.code !== 1) {
|
if (paymentResult.code !== 1) {
|
||||||
sheep.$helper.toast(paymentResult.msg || '生成支付串失败');
|
sheep.$helper.toast(paymentResult.msg || '生成支付串失败');
|
||||||
@ -316,24 +318,73 @@ export default class SheepPay {
|
|||||||
try {
|
try {
|
||||||
const result = await CcbLifePlatform.payment({
|
const result = await CcbLifePlatform.payment({
|
||||||
payment_string: paymentResult.data.payment_string,
|
payment_string: paymentResult.data.payment_string,
|
||||||
order_id: orderInfo.data.id,
|
order_id: orderId,
|
||||||
order_sn: this.orderSN
|
order_sn: this.orderSN
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.code === 0) {
|
if (result.code === 0) {
|
||||||
// 支付成功,通知后端
|
// ✅ 支付调起成功,开始轮询查询订单状态
|
||||||
const callbackResult = await ccbApi.paymentCallback({
|
console.log('[建行支付] 支付调起成功,开始轮询查询订单状态');
|
||||||
order_id: orderInfo.data.id,
|
|
||||||
trans_id: result.data?.trans_id || '',
|
// 显示加载提示
|
||||||
pay_time: new Date().getTime()
|
uni.showLoading({
|
||||||
|
title: '支付确认中...',
|
||||||
|
mask: true
|
||||||
});
|
});
|
||||||
|
|
||||||
if (callbackResult.code === 1) {
|
// 轮询查询订单状态(最多30次,每次间隔2秒,总共60秒)
|
||||||
|
let pollCount = 0;
|
||||||
|
const MAX_POLL_COUNT = 30;
|
||||||
|
const POLL_INTERVAL = 2000; // 2秒
|
||||||
|
|
||||||
|
const pollPaymentStatus = async () => {
|
||||||
|
pollCount++;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const statusResult = await ccbApi.queryPaymentStatus(orderId);
|
||||||
|
|
||||||
|
if (statusResult.code === 1 && statusResult.data.is_paid) {
|
||||||
|
// ✅ 支付成功
|
||||||
|
uni.hideLoading();
|
||||||
|
console.log('[建行支付] 订单已支付 order_id:' + orderId);
|
||||||
that.payResult('success');
|
that.payResult('success');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未支付,继续轮询
|
||||||
|
if (pollCount < MAX_POLL_COUNT) {
|
||||||
|
setTimeout(pollPaymentStatus, POLL_INTERVAL);
|
||||||
} else {
|
} else {
|
||||||
sheep.$helper.toast('支付确认失败,请联系客服');
|
// 超时
|
||||||
|
uni.hideLoading();
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '支付确认超时,请稍后在订单列表中查看支付状态',
|
||||||
|
showCancel: false,
|
||||||
|
confirmText: '知道了',
|
||||||
|
success: () => {
|
||||||
|
// 跳转到订单列表
|
||||||
|
sheep.$router.redirect('/pages/order/list');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[建行支付] 查询状态失败:', error);
|
||||||
|
|
||||||
|
// 继续轮询(网络错误不中断)
|
||||||
|
if (pollCount < MAX_POLL_COUNT) {
|
||||||
|
setTimeout(pollPaymentStatus, POLL_INTERVAL);
|
||||||
|
} else {
|
||||||
|
uni.hideLoading();
|
||||||
|
sheep.$helper.toast('支付状态查询失败,请稍后在订单列表中查看');
|
||||||
that.payResult('fail');
|
that.payResult('fail');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 延迟1秒后开始轮询(给建行异步通知留点时间)
|
||||||
|
setTimeout(pollPaymentStatus, 1000);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// 支付失败或取消
|
// 支付失败或取消
|
||||||
if (result.msg && result.msg.includes('取消')) {
|
if (result.msg && result.msg.includes('取消')) {
|
||||||
|
|||||||
@ -59,10 +59,33 @@ const ccbApi = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 支付回调
|
* 查询订单支付状态 (轮询用)
|
||||||
* 支付完成后通知后端
|
*
|
||||||
|
* ⚠️ 重要: 本接口只查询状态,不更新订单!
|
||||||
|
* 订单状态由建行异步通知(notify)接口更新。
|
||||||
|
*
|
||||||
|
* @deprecated paymentCallback已废弃,改用本方法
|
||||||
|
*/
|
||||||
|
queryPaymentStatus: (orderId) => {
|
||||||
|
return request({
|
||||||
|
url: '/ccbpayment/queryPaymentStatus',
|
||||||
|
method: 'GET',
|
||||||
|
params: {
|
||||||
|
order_id: orderId
|
||||||
|
},
|
||||||
|
custom: {
|
||||||
|
showLoading: false,
|
||||||
|
auth: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated 2025-01-20 该方法存在安全漏洞,已废弃
|
||||||
|
* @see queryPaymentStatus
|
||||||
*/
|
*/
|
||||||
paymentCallback: (data) => {
|
paymentCallback: (data) => {
|
||||||
|
// 向后兼容:调用查询接口
|
||||||
return request({
|
return request({
|
||||||
url: '/ccbpayment/callback',
|
url: '/ccbpayment/callback',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user