Ajax 表单提交导致数据库重复插入的根因分析与终极解决方案

轻墨大大_1475

轻墨大大_1475

2026-04-16

351人浏览

原创

Ajax 表单提交导致数据库重复插入的根因分析与终极解决方案

本文深入剖析 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'));

?️ 防御升级:构建三层幂等保障体系

仅修复执行逻辑仍不够稳健。真实生产环境需叠加以下防护,杜绝任何漏网之鱼:

jQuery金融借款单提交表单
jQuery金融借款单提交表单

jQuery金融借款单提交表单,包含出借金额和日期的选择,且自动计算利息,非常的简单方便。

下载

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 对错误抑制和执行时序更敏感,旧有“侥幸通过”的逻辑会迅速暴露,升级后务必全面审查副作用代码。

通过本次修复,您不仅解决了眼前问题,更建立了一套可复用于订单、用户注册、日志记录等所有关键写操作的工业级幂等框架——这才是专业后端开发的核心能力。

相关专题

更多
ajax教程
ajax教程

php中文网为大家带来ajax教程合集,Ajax是一种用于创建快速动态网页的技术。通过在后台与服务器进行少量数据交换,Ajax可以使网页实现异步更新。这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。php中文网还为大家带来ajax的相关下载资源、相关课程以及相关文章等内容,供大家免费下载使用。

2023.06.14

3080

4

ajax中文乱码解决方法
ajax中文乱码解决方法

ajax中文乱码解决方法有设置请求头部的字符编码、在服务器端设置响应头部的字符编码和使用encodeURIComponent对中文进行编码。本专题为大家提供ajax中文乱码相关的文章、下载、课程内容,供大家免费下载体验。

2023.08.31

360

5

ajax传递中文乱码怎么办
ajax传递中文乱码怎么办

ajax传递中文乱码的解决办法:1、设置统一的编码方式;2、服务器端编码;3、客户端解码;4、设置HTTP响应头;5、使用JSON格式。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

2023.11.15

2893

5

ajax网站有哪些
ajax网站有哪些

使用ajax的网站有谷歌、维基百科、脸书、纽约时报、亚马逊、stackoverflow、twitter、hacker news、shopify和basecamp等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

2024.09.24

1161

6

ThinkPHP表单验证与数据安全处理实战
ThinkPHP表单验证与数据安全处理实战

本专题聚焦 ThinkPHP 在表单处理中的验证与安全机制,系统讲解验证器使用、自定义规则、场景验证以及错误提示处理。内容涵盖 XSS 防护、SQL 注入防御、数据过滤与输入校验等关键安全措施。通过实际案例,帮助开发者构建安全可靠的数据处理流程。

2026.03.30

261

20

Gradle插件开发入门教程
Gradle插件开发入门教程

本专题介绍Gradle插件开发的基本流程,涵盖插件类编写、自定义任务注册、扩展参数设计和插件应用方法,帮助你理解插件如何组织构建逻辑,减少多个项目之间的重复配置。

2026.10.22

80

11

Gradle Wrapper配置教程
Gradle Wrapper配置教程

本专题介绍Gradle Wrapper的基本作用,涵盖文件组成、生成方法、版本配置和任务调用,帮助你理解项目如何使用指定的Gradle版本,减少不同开发环境带来的构建差异。

2026.10.22

40

10

Golangci-lint检查器使用教程
Golangci-lint检查器使用教程

本专题整理Golangci-lint常用Linters的用途与配置方法,覆盖错误处理、无效代码、安全、复杂度、命名和依赖检查,帮助项目合理选规则。

2026.10.22

40

15

Gradle怎么构建Java项目
Gradle怎么构建Java项目

本专题介绍Gradle Java项目构建方法,涵盖Java插件配置、依赖声明、源码编译、测试执行和打包检查,帮助你理解各阶段的任务关系,逐步完成项目构建。

2026.10.22

20

10

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
jQuery官方API文档
jQuery官方API文档

共0课时 | 0人学习

Symfony教程(入门篇+基础篇)
Symfony教程(入门篇+基础篇)

共18课时 | 1.8万人学习