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;
}
即使使用了事务,也不能把“网关扣款”和“本地数据库提交”当成同一个原子事务。网关批准付款后,本地数据库仍有可能暂时不可用,因此还需要补偿机制。
不要依赖用户返回成功页面
用户可能在付款后关闭页面、断网,浏览器也可能无法完成跳转。更稳妥的流程是:
- 发起支付前,创建状态为
pending的本地订单。 - 将本地订单号传给支付网关。
- 收到响应后,验证交易号、金额、币种和订单号。
- 通过服务端异步通知更新订单。
- 定时查询长期处于
pending状态的订单,并与网关状态对账。 - 所有更新都以网关交易号作为幂等依据。
如果网关支持签名或消息认证,必须先验证签名。仅凭客户端提交的 response_code=1 更新数据库,会留下伪造付款状态的安全漏洞。
排查时应记录的内容
每次支付至少需要记录:
- 本地订单号;
- 网关交易号;
- 原始
response_code、类型和规范化后的值; - 金额和币种;
- SQL 是否执行成功;
- 受影响行数;
- 数据库异常代码;
- 请求来源,例如同步返回、异步通知或后台对账;
- 处理时间和幂等结果。
排查的第一步不该是猜测 response_code 是否偶尔不等于 1,而是补全可追踪日志和数据库错误处理。拿到失败请求的原始响应、目标记录、SQL 异常和受影响行数后,才能判断问题出在支付响应解析、用户定位、数据库写入,还是后续的并发覆盖。
备注:内容仅供参考。
