Skip to content

5.17 防重复提交 ​

概述

防重复提交是保障数据一致性的重要措施。通过前端防抖和后端唯一性校验双重保护,防止用户重复点击导致的数据重复提交。

前端防抖 ​

按钮 loading ​

ElementPlus 的按钮组件支持 loading 属性,请求期间禁用按钮:

vue
<template>
  <el-button
    type="primary"
    :loading="submitting"
    @click="handleSubmit"
  >
    提交
  </el-button>
</template>

<script setup>
const submitting = ref(false);

async function handleSubmit() {
  submitting.value = true;
  try {
    await exampleAdd(formData.value);
    ElMessage.success('添加成功');
  } finally {
    submitting.value = false;
  }
}
</script>

useLockFn 钩子 ​

前端提供 useLockFn 钩子函数,实现请求级别的防重复提交:

typescript
import { useLockFn } from '@/utils/lock';

// 包装提交函数,防止重复点击
const handleSubmit = useLockFn(async () => {
  await exampleAdd(formData.value);
  ElMessage.success('添加成功');
});

useLockFn 原理:

typescript
function useLockFn(fn: Function) {
  let isLocked = false;
  return async (...args: any[]) => {
    if (isLocked) return;
    isLocked = true;
    try {
      return await fn(...args);
    } finally {
      isLocked = false;
    }
  };
}

后端唯一性校验 ​

Logic 层唯一性配置 ​

通过 uniqueFields 属性声明唯一性校验字段:

php
class ExampleLogic extends BaseLogic
{
    /**
     * 写法一:单字段全局唯一
     *
     * @var array
     */
    protected array $uniqueFields = ['name'];

    /**
     * 写法二:混合配置(全局唯一 + 分组唯一)
     *
     * @var array
     */
    protected array $uniqueFields = [
        'name',
        ['code', 'pid'],
    ];
}

BaseLogic 在 add/update 时自动校验,重复则抛出异常:

php
// BaseLogic::checkUnique()
protected function checkUnique(array $data, ?int $excludeId = null): void
{
    foreach ($this->uniqueFields as $rule) {
        if (is_string($rule)) {
            $field = $rule;
            $groupField = null;
        } else {
            $field = $rule[0];
            $groupField = $rule[1] ?? null;
        }

        if (!isset($data[$field]) || $data[$field] === '') {
            continue;
        }

        $query = $this->getModel()->where($field, $data[$field]);

        if ($groupField && isset($data[$groupField])) {
            $query = $query->where($groupField, $data[$groupField]);
        }

        if ($excludeId !== null) {
            $query = $query->where('id', '<>', $excludeId);
        }

        if ($query->find()) {
            $label = $this->getFieldLabel($field);
            throw new \Exception($label . '已存在');
        }
    }
}

数据库唯一索引 ​

软删除场景下,唯一索引需要特殊处理:

sql
-- MySQL:唯一索引包含 is_delete 字段
ALTER TABLE think_example ADD UNIQUE INDEX uk_name (name, is_delete);

最佳实践 ​

text
1. 前端防抖:按钮 loading 状态控制,防止用户重复点击
2. 后端校验:Logic uniqueFields + 数据库唯一索引,双重保障
3. 幂等设计:相同请求多次执行结果一致
4. 错误提示:重复提交时返回明确的错误提示("xxx已存在")

小蚂蚁云团队 · 提供技术支持