PAYATHON 2026

PHP 更新数据库失败:response_code 为 1 仍未更新

支付老李

结论

支付成功后,数据库偶尔没有更新,不能直接认定是 response_code 导致的。由于目前没有实际的 PHP 和 SQL 代码,暂时无法定位到具体错误。按照“约 10 次出现 1 次”的频率来看,更可能的原因包括数据库执行失败、并发覆盖、连接异常、更新条件未匹配或回调重复,而不是单纯的数值比较问题。

除了判断 response_code == 1,还需要:

  • 按支付网关文档解析并校验响应;
  • 确认数据库语句是否执行成功;
  • 记录 SQL 异常和受影响行数;
  • 使用支付平台的唯一交易号保证幂等;
  • 通过异步通知或主动查询补偿失败的更新。

先确认 response_code 的实际值

不同支付网关对 response_code 的定义不同。只有对应网关明确规定 1 代表批准,才能用它判断支付成功。

网关返回的数据通常是字符串。下面两个值的类型并不相同:

$responseCode = '1';

var_dump($responseCode == 1);  // true
var_dump($responseCode === 1); // false

如果代码使用严格比较:

if ($responseCode === 1) {
    // 不会执行
}

应先按照接口格式规范化数据:

$responseCode = trim((string) ($response['response_code'] ?? ''));

if ($responseCode === '1') {
    // 支付获得批准
}

不要用宽松比较来掩盖异常数据。应记录原始响应,确认字段是否存在空格、换行、缺失或解析错位:

error_log(json_encode([
    'response_code' => $response['response_code'] ?? null,
    'response_type' => gettype($response['response_code'] ?? null),
    'transaction_id' => $response['transaction_id'] ?? null,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));

日志中不要保存完整卡号、CVV、密码或其他敏感支付信息。

检查数据库更新是否真的成功

不少代码调用 execute() 后,没有检查返回值、异常和受影响行数。可以让 PDO 直接抛出异常:

<?php

$pdo = new PDO($dsn, $username, $password, [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES => false,
]);

$responseCode = trim((string) ($response['response_code'] ?? ''));

if ($responseCode !== '1') {
    throw new RuntimeException('Payment was not approved');
}

$userId = filter_input(INPUT_POST, 'user_id', FILTER_VALIDATE_INT);

if (!$userId) {
    throw new InvalidArgumentException('Invalid user ID');
}

$stmt = $pdo->prepare(
    'UPDATE users
     SET payment_status = :status,
         updated_at = CURRENT_TIMESTAMP
     WHERE id = :user_id'
);

$stmt->execute([
    ':status' => 'paid',
    ':user_id' => $userId,
]);

if ($stmt->rowCount() === 0) {
    // 可能是用户不存在,也可能是数据库判断新旧值没有变化。
    error_log("No user row changed for user_id={$userId}");
}

需要注意的是,rowCount() === 0 不一定说明 SQL 执行失败。如果写入值与原值完全相同,某些数据库或驱动也会返回零。要区分“记录不存在”和“值没有变化”,可以再查询一次目标记录。

常见的间歇性原因

更新条件偶尔不匹配

例如,user_id 来自表单、Session 或回调参数,而部分请求可能没有这个值:

$userId = $_SESSION['user_id'] ?? null;

支付流程可能会跳转到其他域名。Session Cookie 也可能因为 SameSite、域名、HTTPS 配置或会话过期而丢失。支付完成后的数据库更新不应只依赖浏览器 Session。更合适的做法是在发起支付前创建本地订单,并把订单号和网关交易关联起来。

SQL 错误被忽略

字段长度不足、唯一键冲突、数据类型错误、连接中断、锁等待超时或死锁,都可能让更新失败。如果 PDO 没有开启异常模式,这些错误很容易被漏掉。

try {
    $stmt->execute($params);
} catch (PDOException $e) {
    error_log(json_encode([
        'message' => $e->getMessage(),
        'sql_state' => $e->errorInfo[0] ?? null,
        'driver_code' => $e->errorInfo[1] ?? null,
        'transaction_id' => $transactionId,
    ], JSON_UNESCAPED_UNICODE));

    throw $e;
}

生产日志中应避免记录完整的 SQL 参数,因为参数里可能包含个人信息或支付数据。

并发请求覆盖状态

用户返回页面、支付回调和后台任务可能同时更新同一条记录。后到的请求有可能把 paid 覆盖为旧状态。

可以限制状态只能向前转换:

UPDATE orders
SET payment_status = 'paid',
    gateway_transaction_id = :transaction_id,
    paid_at = CURRENT_TIMESTAMP
WHERE id = :order_id
  AND payment_status IN ('pending', 'processing');

还需要检查其他代码路径是否也在更新同一个字段。

交易重复通知

支付平台可能会重试通知,用户也可能刷新成功页面,所以数据库处理必须具备幂等性。常见做法是为网关交易号建立唯一约束:

CREATE UNIQUE INDEX ux_payments_gateway_transaction
ON payments (gateway_transaction_id);

重复通知到达时,应将其识别为已经处理,不能再次增加余额、重复发货或重复记录付款。

更可靠的处理方式

如果付款记录和用户状态更新属于同一个本地业务操作,应将它们放进数据库事务:

<?php

$responseCode = trim((string) ($response['response_code'] ?? ''));
$transactionId = trim((string) ($response['transaction_id'] ?? ''));

if ($responseCode !== '1' || $transactionId === '') {
    throw new RuntimeException('Invalid or unapproved payment response');
}

try {
    $pdo->beginTransaction();

    $payment = $pdo->prepare(
        'INSERT INTO payments
            (order_id, gateway_transaction_id, status, created_at)
         VALUES
            (:order_id, :transaction_id, :status, CURRENT_TIMESTAMP)'
    );

    $payment->execute([
        ':order_id' => $orderId,
        ':transaction_id' => $transactionId,
        ':status' => 'approved',
    ]);

    $order = $pdo->prepare(
        'UPDATE orders
         SET payment_status = :status,
             paid_at = CURRENT_TIMESTAMP
         WHERE id = :order_id
           AND payment_status <> :status'
    );

    $order->execute([
        ':status' => 'paid',
        ':order_id' => $orderId,
    ]);

    $pdo->commit();
} catch (Throwable $e) {
    if ($pdo->inTransaction()) {
        $pdo->rollBack();
    }

    error_log(json_encode([
        'order_id' => $orderId,
        'transaction_id' => $transactionId,
        'error' => $e->getMessage(),
    ], JSON_UNESCAPED_UNICODE));

    throw $e;
}

即使使用了事务,也不能把“网关扣款”和“本地数据库提交”当成同一个原子事务。网关批准付款后,本地数据库仍有可能暂时不可用,因此还需要补偿机制。

不要依赖用户返回成功页面

用户可能在付款后关闭页面、断网,浏览器也可能无法完成跳转。更稳妥的流程是:

  1. 发起支付前,创建状态为 pending 的本地订单。
  2. 将本地订单号传给支付网关。
  3. 收到响应后,验证交易号、金额、币种和订单号。
  4. 通过服务端异步通知更新订单。
  5. 定时查询长期处于 pending 状态的订单,并与网关状态对账。
  6. 所有更新都以网关交易号作为幂等依据。

如果网关支持签名或消息认证,必须先验证签名。仅凭客户端提交的 response_code=1 更新数据库,会留下伪造付款状态的安全漏洞。

排查时应记录的内容

每次支付至少需要记录:

  • 本地订单号;
  • 网关交易号;
  • 原始 response_code、类型和规范化后的值;
  • 金额和币种;
  • SQL 是否执行成功;
  • 受影响行数;
  • 数据库异常代码;
  • 请求来源,例如同步返回、异步通知或后台对账;
  • 处理时间和幂等结果。

排查的第一步不该是猜测 response_code 是否偶尔不等于 1,而是补全可追踪日志和数据库错误处理。拿到失败请求的原始响应、目标记录、SQL 异常和受影响行数后,才能判断问题出在支付响应解析、用户定位、数据库写入,还是后续的并发覆盖。

备注:内容仅供参考。