架构修复

This commit is contained in:
Billy 2025-10-20 17:10:16 +08:00
parent 4b729e6e21
commit 639732e6d1
2 changed files with 166 additions and 219 deletions

View File

@ -27,7 +27,7 @@ class Ccbpayment extends Common
* 不需要登录的方法 (支付回调不需要登录) * 不需要登录的方法 (支付回调不需要登录)
* @var array * @var array
*/ */
protected $noNeedLogin = ['callback', 'notify']; protected $noNeedLogin = ['notify'];
/** /**
* 不需要权限的方法 * 不需要权限的方法
@ -98,7 +98,23 @@ class Ccbpayment extends Common
$this->error('支付串生成失败: ' . $result['message']); $this->error('支付串生成失败: ' . $result['message']);
} }
// 5. 直接返回支付串(不再重复保存数据库!) // 5. ✅ 推送订单到建行(步骤2:调用订单推送接口将已提交订单)
// ⚠️ 注意:此时推送的是未支付状态的订单
try {
$pushResult = $this->orderService->pushOrder($orderId);
if ($pushResult['status']) {
Log::info('[建行支付] 订单推送成功 order_id:' . $orderId);
} else {
// ⚠️ 推送失败不阻塞支付流程,只记录日志
Log::warning('[建行支付] 订单推送失败(不阻塞支付) order_id:' . $orderId . ' error:' . $pushResult['message']);
}
} catch (Exception $e) {
// ⚠️ 推送异常不阻塞支付流程
Log::error('[建行支付] 订单推送异常(不阻塞支付) order_id:' . $orderId . ' error:' . $e->getMessage());
}
// 6. 返回支付串
$this->success('支付串生成成功', $result['data']); $this->success('支付串生成成功', $result['data']);
} catch (Exception $e) { } catch (Exception $e) {
@ -163,22 +179,6 @@ class Ccbpayment extends Common
} }
} }
/**
* ⚠️ 已废弃: 支付回调 (前端调用)
*
* @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();
}
/** /**
* 建行支付通知 (建行服务器回调) * 建行支付通知 (建行服务器回调)
@ -224,19 +224,26 @@ class Ccbpayment extends Common
// 6. 调用支付服务处理通知(返回订单ID) // 6. 调用支付服务处理通知(返回订单ID)
$result = $this->paymentService->handleNotify($params); $result = $this->paymentService->handleNotify($params);
// 7. 处理成功后推送订单到建行外联系统 // 7. ✅ 处理成功后更新订单状态到建行(步骤13:调用订单更新接口更新订单状态)
if ($result['status'] === 'success' && !empty($result['order_id'])) { if ($result['status'] === 'success' && !empty($result['order_id'])) {
// ⚠️ 只有新支付才推送,已支付的订单跳过推送 // ⚠️ 只有新支付才更新,已支付的订单跳过更新
if ($result['already_paid'] === false) { if ($result['already_paid'] === false) {
try { try {
$this->pushOrderToCcb($result['order_id']); // 调用订单更新接口,将订单状态从未支付更新为已支付
Log::info('[建行通知] 订单推送成功 order_id:' . $result['order_id']); $updateResult = $this->orderService->updateOrderStatus($result['order_id']);
if ($updateResult['status']) {
Log::info('[建行通知] 订单状态更新成功 order_id:' . $result['order_id']);
} else {
// ⚠️ 更新失败不影响本地支付状态,记录日志后续补推
Log::warning('[建行通知] 订单状态更新失败(本地已支付) order_id:' . $result['order_id'] . ' error:' . $updateResult['message']);
}
} catch (Exception $e) { } catch (Exception $e) {
// ⚠️ 推送失败不影响支付成功,记录日志后续补推 // ⚠️ 更新异常不影响支付成功,记录日志后续补推
Log::error('[建行通知] 订单推送失败 order_id:' . $result['order_id'] . ' error:' . $e->getMessage()); Log::error('[建行通知] 订单状态更新异常(本地已支付) order_id:' . $result['order_id'] . ' error:' . $e->getMessage());
} }
} else { } else {
Log::info('[建行通知] 订单已支付且已推送,跳过推送 order_id:' . $result['order_id']); Log::info('[建行通知] 订单已支付且已更新,跳过更新 order_id:' . $result['order_id']);
} }
} }
@ -258,104 +265,15 @@ class Ccbpayment extends Common
} }
/** /**
* 推送订单到建行外联系统 * ⚠️ 已废弃: 推送订单到建行外联系统
* *
* ⚠️ 重要: 只在notify()支付成功后调用! * @deprecated 2025-01-20 根据建行流程图,订单推送应在createPayment()时完成,此方法已废弃
* 幂等性: 支持重复调用,已推送的订单会跳过 * @see createPayment() 步骤5: 推送未支付订单
* * @see notify() 步骤7: 更新订单为已支付
* @param int $orderId 订单ID
* @return void
* @throws Exception 推送失败时抛出异常
*/ */
private function pushOrderToCcb($orderId) private function pushOrderToCcb($orderId)
{ {
try { Log::warning('[建行支付] pushOrderToCcb()已废弃,请使用orderService->pushOrder()或updateOrderStatus()');
// ✅ 重新查询订单,确保状态已更新
$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; // 向上抛出异常
}
} }
/** /**

View File

@ -2,9 +2,10 @@
**项目**: Shopro商城建行支付集成 **项目**: Shopro商城建行支付集成
**修复时间**: 2025-01-20 **修复时间**: 2025-01-20
**文档版本**: v1.0 **文档版本**: v2.0 (根据官方流程图修正)
**修复类型**: 🔴 严重安全漏洞 + 架构偏离 **修复类型**: 🔴 严重安全漏洞 + 架构偏离 + 订单同步时机错误
**建行接口版本**: v2.20 (2025-07-25) **建行接口版本**: v2.20 (2025-07-25)
**参考文档**: 建行生活服务方接入流程图
--- ---
@ -17,14 +18,15 @@
1. **前端callback机制违反建行标准流程** 🔴 致命 1. **前端callback机制违反建行标准流程** 🔴 致命
2. **前端可伪造支付成功请求** 🔴 安全漏洞 2. **前端可伪造支付成功请求** 🔴 安全漏洞
3. **双通道更新订单状态导致竞态条件** 🔴 致命 3. **双通道更新订单状态导致竞态条件** 🔴 致命
4. **订单推送时机错误** 🟡 严重 4. **订单推送时机错误** 🔴 致命 - 应该在创建订单时推送,而不是支付成功后
5. **缺少轮询查询机制** 🟡 严重 5. **订单更新时机错误** 🔴 致命 - 支付成功后应调用更新接口,而不是推送接口
6. **缺少轮询查询机制** 🟡 严重
--- ---
## 🔴 建行标准支付流程 vs 错误实现 ## 🔴 建行标准支付流程 vs 错误实现
### 建行标准流程(官方文档) ### 建行标准流程(官方文档 - 根据流程图修正)
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
@ -32,25 +34,39 @@ sequenceDiagram
participant Backend as 服务方后台 participant Backend as 服务方后台
participant CCBApp as 建行APP participant CCBApp as 建行APP
participant CCBBackend as 建行后端 participant CCBBackend as 建行后端
participant Merchant as 商户管理(外联)
Note over H5,Backend: 步骤1: 生成支付串 Note over H5,Backend: 步骤1: 请求下单
H5->>Backend: POST /createPayment H5->>Backend: POST /createPayment(生成支付串)
Backend->>Backend: 保存订单(未支付状态)
Note over Backend,CCBBackend: 步骤2: 推送订单 ✅ 推送未支付订单!
Backend->>Merchant: 调用订单推送接口(A3341TP01)
Merchant-->>Backend: 返回推送结果
Backend-->>H5: {payment_string} Backend-->>H5: {payment_string}
Note over H5,CCBApp: 步骤2: 调起建行收银台 Note over H5,CCBApp: 步骤3: 调起建行收银台
H5->>CCBApp: JSBridge.ccbpay(支付串) H5->>CCBApp: JSBridge.ccbpay(支付串)
activate CCBBackend activate CCBBackend
CCBApp->>CCBBackend: 校验登录
CCBApp->>CCBApp: 调用支付组件
H5->>CCBApp: 确认支付、输入密码
CCBApp->>CCBBackend: 发送支付请求
Note right of CCBBackend: 用户在建行APP中完成支付 Note right of CCBBackend: 用户在建行APP中完成支付
Note over CCBBackend,Backend: 步骤3: 建行异步通知 ✅ 唯一可信的支付确认! Note over CCBBackend,Backend: 步骤10-12: 建行异步通知
CCBBackend->>Backend: POST notify(ORDERID, SIGN等) CCBBackend->>Merchant: 返回支付成功通知
Merchant->>Backend: 推送服务器通知(notify)
Backend->>Backend: 验证SIGN签名 Backend->>Backend: 验证SIGN签名
Backend->>Backend: 原子更新订单状态为paid Backend->>Backend: 原子更新本地订单状态为paid
Backend->>Backend: 推送订单到外联系统
Backend-->>CCBBackend: 返回SUCCESS
deactivate CCBBackend deactivate CCBBackend
Note over H5,Backend: 步骤4: 前端轮询查询状态 ✅ 只查询,不更新! Note over Backend,Merchant: 步骤13: 更新订单状态 ✅ 更新为已支付!
Backend->>Merchant: 调用订单更新接口(A3341TP02)
Merchant-->>Backend: 返回更新结果
Backend-->>Merchant: 返回SUCCESS
Note over H5,Backend: 步骤15-16: 前端轮询查询状态 (未收到通知时)
loop 每2秒轮询(最多60秒) loop 每2秒轮询(最多60秒)
H5->>Backend: GET /queryPaymentStatus H5->>Backend: GET /queryPaymentStatus
Backend-->>H5: {status: 'paid'或'unpaid'} Backend-->>H5: {status: 'paid'或'unpaid'}
@ -60,6 +76,11 @@ sequenceDiagram
end end
``` ```
**关键流程说明**:
1. **步骤2**: 生成支付串后**立即推送未支付订单**到建行外联系统(A3341TP01)
2. **步骤13**: 收到支付成功通知后**更新订单状态为已支付**(A3341TP02)
3. **步骤15**: 前端轮询查询订单状态(用于未收到通知的降级方案)
### 修复前的错误实现 ### 修复前的错误实现
```mermaid ```mermaid
@ -327,7 +348,76 @@ public function notify()
--- ---
### 修复3: handleNotify()返回订单ID ### 修复3: createPayment()推送未支付订单
**修复后** (`addons/shopro/controller/Ccbpayment.php:101-118`):
```php
// 5. ✅ 推送订单到建行(步骤2:调用订单推送接口将已提交订单)
// ⚠️ 注意:此时推送的是未支付状态的订单
try {
$pushResult = $this->orderService->pushOrder($orderId);
if ($pushResult['status']) {
Log::info('[建行支付] 订单推送成功 order_id:' . $orderId);
} else {
// ⚠️ 推送失败不阻塞支付流程,只记录日志
Log::warning('[建行支付] 订单推送失败(不阻塞支付) order_id:' . $orderId . ' error:' . $pushResult['message']);
}
} catch (Exception $e) {
// ⚠️ 推送异常不阻塞支付流程
Log::error('[建行支付] 订单推送异常(不阻塞支付) order_id:' . $orderId . ' error:' . $e->getMessage());
}
// 6. 返回支付串
$this->success('支付串生成成功', $result['data']);
```
**修复要点**:
- ✅ 生成支付串后立即调用`pushOrder()`推送未支付订单(A3341TP01)
- ✅ 推送失败不阻塞支付流程,用户仍可继续支付
- ✅ 记录推送结果日志,失败的可后续补推
---
### 修复4: notify()更新订单状态为已支付
**修复后** (`addons/shopro/controller/Ccbpayment.php:227-248`):
```php
// 7. ✅ 处理成功后更新订单状态到建行(步骤13:调用订单更新接口更新订单状态)
if ($result['status'] === 'success' && !empty($result['order_id'])) {
// ⚠️ 只有新支付才更新,已支付的订单跳过更新
if ($result['already_paid'] === false) {
try {
// 调用订单更新接口,将订单状态从未支付更新为已支付
$updateResult = $this->orderService->updateOrderStatus($result['order_id']);
if ($updateResult['status']) {
Log::info('[建行通知] 订单状态更新成功 order_id:' . $result['order_id']);
} else {
// ⚠️ 更新失败不影响本地支付状态,记录日志后续补推
Log::warning('[建行通知] 订单状态更新失败(本地已支付) order_id:' . $result['order_id'] . ' error:' . $updateResult['message']);
}
} catch (Exception $e) {
// ⚠️ 更新异常不影响支付成功,记录日志后续补推
Log::error('[建行通知] 订单状态更新异常(本地已支付) order_id:' . $result['order_id'] . ' error:' . $e->getMessage());
}
} else {
Log::info('[建行通知] 订单已支付且已更新,跳过更新 order_id:' . $result['order_id']);
}
}
```
**修复要点**:
- ✅ 收到支付成功通知后调用`updateOrderStatus()`(A3341TP02)
- ✅ 将订单状态从"未支付"更新为"已支付"
- ✅ 更新失败不影响本地支付状态(本地订单已标记为paid)
- ✅ 幂等性保护(已支付的订单跳过更新)
---
### 修复5: handleNotify()返回订单ID
**修复后** (`addons/shopro/library/ccblife/CcbPaymentService.php:349-403`): **修复后** (`addons/shopro/library/ccblife/CcbPaymentService.php:349-403`):
@ -404,84 +494,21 @@ public function handleNotify($params)
--- ---
### 修复4: pushOrderToCcb()增加幂等性检查 ### ~~修复6: pushOrderToCcb()增加幂等性检查~~
**修复后** (`addons/shopro/controller/Ccbpayment.php:270-359`): **⚠️ 已废弃**: 根据建行流程图,`pushOrderToCcb()`方法已废弃。
```php **正确流程**:
/** 1. `createPayment()` → 调用`orderService->pushOrder()` → 推送未支付订单(A3341TP01)
* 推送订单到建行外联系统 2. `notify()` → 调用`orderService->updateOrderStatus()` → 更新为已支付(A3341TP02)
*
* ⚠️ 重要: 只在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: 前端改为轮询查询 **⚠️ 已废弃**: 根据建行流程图,订单推送和更新的幂等性由`CcbOrderService`内部保证。
---
### 修复6: 前端改为轮询查询
**修复后** (`frontend/sheep/platform/pay.js:325-386`): **修复后** (`frontend/sheep/platform/pay.js:325-386`):
@ -576,11 +603,13 @@ async ccbPay() {
| **支付确认来源** | 前端callback + 建行notify (双通道) ❌ | 只依赖建行notify (单通道) ✅ | | **支付确认来源** | 前端callback + 建行notify (双通道) ❌ | 只依赖建行notify (单通道) ✅ |
| **前端职责** | 调用callback通知后端支付成功 ❌ | 轮询查询订单状态 ✅ | | **前端职责** | 调用callback通知后端支付成功 ❌ | 轮询查询订单状态 ✅ |
| **安全性** | 可伪造前端请求触发支付成功 🔴 | 只信任建行签名验证 ✅ | | **安全性** | 可伪造前端请求触发支付成功 🔴 | 只信任建行签名验证 ✅ |
| **订单推送时机** | callback()中推送 ❌ | notify()成功后推送 ✅ | | **订单推送时机** | 支付成功后推送 ❌ | **创建订单时推送未支付状态** ✅ |
| **订单更新时机** | 未更新到建行 ❌ | **支付成功后更新为已支付** ✅ |
| **竞态风险** | callback和notify可能同时执行 🔴 | 只有notify会更新订单 ✅ | | **竞态风险** | callback和notify可能同时执行 🔴 | 只有notify会更新订单 ✅ |
| **幂等性** | 无幂等保护 ❌ | 支持重复调用,已推送的跳过 ✅ | | **幂等性** | 无幂等保护 ❌ | 支持重复调用,已推送/已更新的跳过 ✅ |
| **符合建行规范** | 否 ❌ | ✅ | | **符合建行规范** | 否 ❌ | **完全符合流程图** ✅ |
| **订单状态一致性** | 可能重复更新或状态异常 🔴 | 原子更新,状态一致 ✅ | | **订单状态一致性** | 可能重复更新或状态异常 🔴 | 原子更新,状态一致 ✅ |
| **建行订单同步** | 不同步或错误时机同步 ❌ | **按流程图正确同步** ✅ |
--- ---
@ -652,25 +681,24 @@ curl -X POST "http://your-domain/addons/shopro/ccbpayment/notify" \
### 4. 日志验证 ### 4. 日志验证
```bash ```bash
# 查看创建支付串日志
tail -f runtime/log/$(date +%Y%m)/*.log | grep '建行支付'
# ✅ 正常流程应该看到:
# [建行支付] 订单推送成功 order_id:123 ← 步骤2: 推送未支付订单
# 查看notify日志 # 查看notify日志
tail -f runtime/log/$(date +%Y%m)/*.log | grep '建行通知' tail -f runtime/log/$(date +%Y%m)/*.log | grep '建行通知'
# ✅ 正常流程应该看到: # ✅ 正常流程应该看到:
# [建行通知] 收到异步通知: ORDERID=... # [建行通知] 收到异步通知: ORDERID=...
# [建行通知] 解析参数: {...} # [建行通知] 解析参数: {...}
# [建行通知] 订单状态更新成功 order_id:123 # [建行通知] 订单状态更新成功 order_id:123 ← 步骤13: 更新为已支付
# [建行通知] 订单推送成功 order_id:123
# [建行通知] 处理完成,返回: SUCCESS # [建行通知] 处理完成,返回: SUCCESS
# 查看推送日志
tail -f runtime/log/$(date +%Y%m)/*.log | grep '建行推送'
# ✅ 正常流程应该看到:
# [建行推送] 订单推送成功 order_id:123 order_sn:202501200001
# 查看幂等性日志(重复通知时) # 查看幂等性日志(重复通知时)
# [建行通知] 订单已支付,跳过处理 order_id:123 # [建行通知] 订单已支付,跳过处理 order_id:123
# [建行推送] 订单已推送,跳过 order_id:123 # [建行通知] 订单已支付且已更新,跳过更新 order_id:123
``` ```
--- ---
@ -790,6 +818,7 @@ php think clear
| 版本 | 日期 | 修改内容 | | 版本 | 日期 | 修改内容 |
|-----|------|---------| |-----|------|---------|
| v1.0 | 2025-01-20 | 初始版本,完成严重安全漏洞和架构偏离修复 | | v1.0 | 2025-01-20 | 初始版本,完成严重安全漏洞和架构偏离修复 |
| v2.0 | 2025-01-20 | **重大修正**: 根据建行流程图修正订单同步时机 - 推送未支付订单在createPayment,更新已支付订单在notify |
--- ---