# 5.17 防重复提交
:::tip 概述
防重复提交是保障数据一致性的重要措施。通过前端防抖和后端唯一性校验双重保护,防止用户重复点击导致的数据重复提交。
:::
## 前端防抖
### 按钮 loading
ElementPlus 的按钮组件支持 `loading` 属性,请求期间禁用按钮:
```vue
提交
```
### 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已存在")
```