签入版本
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
# 5.17 防重复提交
|
||||
|
||||
:::tip 概述
|
||||
防重复提交是保障数据一致性的重要措施。通过前端防抖和后端唯一性校验双重保护,防止用户重复点击导致的数据重复提交。
|
||||
:::
|
||||
|
||||
## 前端防抖
|
||||
|
||||
### 按钮 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已存在")
|
||||
```
|
||||
Reference in New Issue
Block a user