fengketrade/doc/建行支付架构修复报告.md
2025-10-20 17:10:16 +08:00

26 KiB

建行支付架构修复报告

项目: Shopro商城建行支付集成 修复时间: 2025-01-20 文档版本: v2.0 (根据官方流程图修正) 修复类型: 🔴 严重安全漏洞 + 架构偏离 + 订单同步时机错误 建行接口版本: v2.20 (2025-07-25) 参考文档: 建行生活服务方接入流程图


📋 问题概述

通过对比建行官方支付流程图和当前实现,发现严重的架构偏离和安全漏洞:

核心问题

  1. 前端callback机制违反建行标准流程 🔴 致命
  2. 前端可伪造支付成功请求 🔴 安全漏洞
  3. 双通道更新订单状态导致竞态条件 🔴 致命
  4. 订单推送时机错误 🔴 致命 - 应该在创建订单时推送,而不是支付成功后
  5. 订单更新时机错误 🔴 致命 - 支付成功后应调用更新接口,而不是推送接口
  6. 缺少轮询查询机制 🟡 严重

🔴 建行标准支付流程 vs 错误实现

建行标准流程(官方文档 - 根据流程图修正)

sequenceDiagram
    participant H5 as 前端H5页面
    participant Backend as 服务方后台
    participant CCBApp as 建行APP
    participant CCBBackend as 建行后端
    participant Merchant as 商户管理(外联)

    Note over H5,Backend: 步骤1: 请求下单
    H5->>Backend: POST /createPayment(生成支付串)
    Backend->>Backend: 保存订单(未支付状态)

    Note over Backend,CCBBackend: 步骤2: 推送订单 ✅ 推送未支付订单!
    Backend->>Merchant: 调用订单推送接口(A3341TP01)
    Merchant-->>Backend: 返回推送结果
    Backend-->>H5: {payment_string}

    Note over H5,CCBApp: 步骤3: 调起建行收银台
    H5->>CCBApp: JSBridge.ccbpay(支付串)
    activate CCBBackend
    CCBApp->>CCBBackend: 校验登录
    CCBApp->>CCBApp: 调用支付组件
    H5->>CCBApp: 确认支付、输入密码
    CCBApp->>CCBBackend: 发送支付请求
    Note right of CCBBackend: 用户在建行APP中完成支付

    Note over CCBBackend,Backend: 步骤10-12: 建行异步通知
    CCBBackend->>Merchant: 返回支付成功通知
    Merchant->>Backend: 推送服务器通知(notify)
    Backend->>Backend: 验证SIGN签名
    Backend->>Backend: 原子更新本地订单状态为paid
    deactivate CCBBackend

    Note over Backend,Merchant: 步骤13: 更新订单状态 ✅ 更新为已支付!
    Backend->>Merchant: 调用订单更新接口(A3341TP02)
    Merchant-->>Backend: 返回更新结果
    Backend-->>Merchant: 返回SUCCESS

    Note over H5,Backend: 步骤15-16: 前端轮询查询状态 (未收到通知时)
    loop 每2秒轮询(最多60秒)
        H5->>Backend: GET /queryPaymentStatus
        Backend-->>H5: {status: 'paid'或'unpaid'}
        alt status == 'paid'
            H5->>H5: 跳转到支付成功页
        end
    end

关键流程说明:

  1. 步骤2: 生成支付串后立即推送未支付订单到建行外联系统(A3341TP01)
  2. 步骤13: 收到支付成功通知后更新订单状态为已支付(A3341TP02)
  3. 步骤15: 前端轮询查询订单状态(用于未收到通知的降级方案)

修复前的错误实现

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):

// ❌ 错误: 前端主动调用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. 用户在浏览器控制台执行:
ccbApi.paymentCallback({
    order_id: 12345,
    trans_id: 'fake_trans_id',
    pay_time: Date.now()
})
  1. 后端callback()接口收到请求,没有验证签名,直接更新订单为已支付!

  2. 攻击者不花一分钱就能白嫖商品 🔥

风险等级: 🔴 致命 - 可直接导致商户资金损失


漏洞2: callback()与notify()双通道竞态条件

问题代码 (addons/shopro/controller/Ccbpayment.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):

/**
 * 查询订单支付状态 (前端轮询用)
 *
 * ⚠️ 重要: 本接口只查询订单状态,不执行任何业务逻辑!
 * 订单状态由建行异步通知(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):

/**
 * 建行支付通知 (建行服务器回调)
 *
 * ✅ 正确流程:
 * 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: createPayment()推送未支付订单

修复后 (addons/shopro/controller/Ccbpayment.php:101-118):

// 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):

// 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):

/**
 * 处理异步通知
 *
 * ⚠️ 注意:这是唯一可信的支付确认来源!
 * 返回订单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标志防止重复推送
  • 幂等性保护(已支付的订单直接返回成功)

修复6: pushOrderToCcb()增加幂等性检查

⚠️ 已废弃: 根据建行流程图,pushOrderToCcb()方法已废弃。

正确流程:

  1. createPayment() → 调用orderService->pushOrder() → 推送未支付订单(A3341TP01)
  2. notify() → 调用orderService->updateOrderStatus() → 更新为已支付(A3341TP02)

⚠️ 已废弃: 根据建行流程图,订单推送和更新的幂等性由CcbOrderService内部保证。


修复6: 前端改为轮询查询

修复后 (frontend/sheep/platform/pay.js:325-386):

// 建行生活支付
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可能同时执行 🔴 只有notify会更新订单
幂等性 无幂等保护 支持重复调用,已推送/已更新的跳过
符合建行规范 完全符合流程图
订单状态一致性 可能重复更新或状态异常 🔴 原子更新,状态一致
建行订单同步 不同步或错误时机同步 按流程图正确同步

验证清单

修复完成后,请逐项验证:

1. 后端验证

# 验证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()已不能触发支付成功

# 尝试伪造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()是否幂等

# 模拟建行重复发送通知
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. 日志验证

# 查看创建支付串日志
tail -f runtime/log/$(date +%Y%m)/*.log | grep '建行支付'

# ✅ 正常流程应该看到:
# [建行支付] 订单推送成功 order_id:123  ← 步骤2: 推送未支付订单

# 查看notify日志
tail -f runtime/log/$(date +%Y%m)/*.log | grep '建行通知'

# ✅ 正常流程应该看到:
# [建行通知] 收到异步通知: ORDERID=...
# [建行通知] 解析参数: {...}
# [建行通知] 订单状态更新成功 order_id:123  ← 步骤13: 更新为已支付
# [建行通知] 处理完成,返回: SUCCESS

# 查看幂等性日志(重复通知时)
# [建行通知] 订单已支付,跳过处理 order_id:123
# [建行通知] 订单已支付且已更新,跳过更新 order_id:123

📝 数据库字段说明

确保订单表包含以下字段:

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. 备份现有代码

# 备份控制器
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. 部署后端代码

# 上传修复后的文件
# - addons/shopro/controller/Ccbpayment.php
# - addons/shopro/library/ccblife/CcbPaymentService.php

# 清除缓存
php think clear

3. 部署前端代码

cd frontend

# 上传修复后的文件
# - sheep/platform/pay.js
# - sheep/platform/provider/ccblife/api.js

# 重新打包发布

4. 数据库迁移(如果字段缺失)

-- 检查字段是否存在
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. 监控上线

# 实时监控notify日志
tail -f runtime/log/$(date +%Y%m)/*.log | grep '建行'

# 监控查询接口调用
tail -f runtime/log/$(date +%Y%m)/*.log | grep 'queryPaymentStatus'

⚠️ 回滚方案

如遇紧急问题,可立即回滚:

# 回滚后端
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 初始版本,完成严重安全漏洞和架构偏离修复
v2.0 2025-01-20 重大修正: 根据建行流程图修正订单同步时机 - 推送未支付订单在createPayment,更新已支付订单在notify

修复完成,已做好生产环境部署准备!