
本文深入剖析 ajax 调用 php 后端时出现“一次点击、两次插入”的典型问题,揭示隐藏在数据库连接函数中的隐式执行陷阱,并提供涵盖前端防重、后端幂等、数据库约束三层防护的专业级解决方案。
本文深入剖析 ajax 调用 php 后端时出现“一次点击、两次插入”的典型问题,揭示隐藏在数据库连接函数中的隐式执行陷阱,并提供涵盖前端防重、后端幂等、数据库约束三层防护的专业级解决方案。
在您提供的代码中,看似规范的 Ajax 提交流程(点击按钮 → 触发 opnActPag.addAct() → POST 至 save.php)却持续导致数据库重复插入,而返回 ID 却仅有一个——这强烈暗示问题并非来自客户端重复请求,而是服务端逻辑中存在未察觉的隐式执行分支。正如您最终定位到的关键线索:poke_db() 函数中这一行代码是罪魁祸首:
if( !$conn -> query($sql) ){ // ⚠️ 危险!此处已执行了一次 SQL!
die('[ERR] ' . $conn -> error);
}
该判断语句在检查 SQL 执行结果前,已主动调用 $conn->query($sql) 并完成了一次真实插入。随后在下方逻辑中($result = mysqli_query($conn, $sql);),同一 SQL 再次被执行——形成稳定复现的“双写”。这是典型的副作用误用(side-effect misuse):将“执行”与“判断执行结果”错误地耦合在同一表达式中。
✅ 正确修复:分离执行与校验逻辑
修改 poke_db() 函数,确保 SQL 仅执行一次,且执行逻辑集中可控:
function poke_db($sql, $multi = 0, $array = 0) {
global $db_name, $srv, $usr, $pas;
$conn = mysqli_init();
$conn->options(MYSQLI_OPT_CONNECT_TIMEOUT, 5);
$conn->real_connect($srv, $usr, $pas, $db_name);
if ($conn->connect_error) {
die('[ERR] Connection: ' . $conn->connect_error);
}
// ✅ 关键修正:只在此处执行一次 SQL,不再提前 query()
if ($multi == 1) {
$result = mysqli_multi_query($conn, $sql);
} else {
$result = mysqli_query($conn, $sql); // ← 唯一执行点
$id = $result ? mysqli_insert_id($conn) : null;
}
if (!$result) {
die('[ERR] Query failed: ' . $conn->error . ' | SQL: ' . htmlspecialchars($sql));
}
if ($array == 1) {
$out_array = [];
if (mysqli_num_rows($result) > 0) {
while ($row = mysqli_fetch_assoc($result)) {
$out_array[] = $row;
}
}
return $out_array;
} else {
return $id ?? 0;
}
}
? 验证建议:在 save.php 中添加日志,确认 poke_db() 调用前后是否仅有一条 INSERT 日志:
error_log("[SAVE.PHP] Insert attempt for user {$_POST['usr']}, id {$_POST['id']} — " . date('Y-m-d H:i:s'));
?️ 防御升级:构建三层幂等保障体系
仅修复执行逻辑仍不够稳健。真实生产环境需叠加以下防护,杜绝任何漏网之鱼:
1. 前端层:禁用按钮 + 防抖 + Token 校验
opnActPag['addAct'] = function () {
const btn = document.getElementById('act_subm_txt');
btn.disabled = true; // 立即禁用,防止连点
btn.textContent = '提交中...';
// 添加一次性 CSRF Token(服务端生成并注入到页面)
const token = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '';
$.ajax({
type: 'POST',
url: 'save.php',
data: {
act_txt: actTxtArea.value,
id: actHidVal,
usr: actHidUsr,
_token: token // 传入 Token
},
success: function(data) {
console.log('Success:', data);
},
error: function() {
alert('提交失败,请重试');
},
complete: function() {
btn.disabled = false;
btn.textContent = '✓';
}
});
};
2. 后端层:强制幂等 + 事务原子性
save.php 应使用预处理语句(杜绝 SQL 注入)、显式事务及唯一约束校验:
<?php session_start();
// ✅ 验证 CSRF Token
if (!hash_equals($_SESSION['csrf_token'] ?? '', $_POST['_token'] ?? '')) {
http_response_code(403);
exit('Invalid token');
}
if (isset($_POST['act_txt']) && !empty(trim($_POST['act_txt']))) {
$act_txt = trim($_POST['act_txt']);
$usr = filter_var($_POST['usr'], FILTER_SANITIZE_STRING);
$id = (int)$_POST['id'];
// ✅ 使用 PDO 预处理 + 事务(更安全可靠)
try {
$pdo = new PDO("mysql:host=localhost;dbname=dbname", "root", "pass");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->beginTransaction();
// ✅ 先查后插(或直接 ON DUPLICATE KEY UPDATE)
$stmt = $pdo->prepare(
"INSERT INTO actions (user_name, info, level_3_id, created_at)
VALUES (?, ?, ?, NOW())
ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id)"
);
$stmt->execute([$usr, preg_replace('/[\r\n]+/', ' ', htmlentities($act_txt, ENT_QUOTES)), $id]);
$insertId = $pdo->lastInsertId();
$pdo->commit();
echo json_encode(['success' => true, 'id' => $insertId]);
} catch (PDOException $e) {
$pdo->rollback();
error_log("DB Error: " . $e->getMessage());
echo json_encode(['success' => false, 'error' => 'Insert failed']);
}
}
?>
3. 数据库层:强制唯一索引(终极兜底)
在 actions 表上添加复合唯一索引,确保 (user_name, info, level_3_id) 组合不可重复:
ALTER TABLE actions ADD CONSTRAINT uk_user_info_level UNIQUE (user_name, info, level_3_id);
当重复插入发生时,数据库将直接抛出 1062 Duplicate entry 错误,由后端捕获并优雅降级(如返回“数据已存在”),而非静默写入脏数据。
⚠️ 重要注意事项
- 永远不要拼接 SQL 字符串:原代码中 '.$_POST['usr'].' 极易引发 SQL 注入,必须使用预处理语句。
- 避免 htmlentities() 在入库前双重编码:应仅在输出 HTML 时编码,入库应存原始内容(或统一 UTF-8 存储)。
- mysqli_insert_id() 依赖于上一次成功 INSERT:若前序查询非 INSERT(如 SELECT),该函数可能返回 0 或错误值,务必结合 $result 判断。
- PHP 8.1+ 更严格的行为:如您所遇,PHP 8.1 对错误抑制和执行时序更敏感,旧有“侥幸通过”的逻辑会迅速暴露,升级后务必全面审查副作用代码。
通过本次修复,您不仅解决了眼前问题,更建立了一套可复用于订单、用户注册、日志记录等所有关键写操作的工业级幂等框架——这才是专业后端开发的核心能力。











