# 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已存在") ```