防重复提交详解:从前端Vue到后端Java的全面解决方案

06-01 1434阅读

防重复提交详解:从前端Vue到后端Java的全面解决方案

一、重复提交问题概述

在Web应用开发中,表单重复提交是一个常见问题,可能导致:

  • 数据库中出现重复记录
  • 重复执行业务逻辑(如多次扣款)
  • 系统资源浪费
  • 用户体验下降

    本文将从前端Vue和后端Java两个层面,详细介绍防止重复提交的多种解决方案。

    二、前端防重复提交(Vue.js)

    1. 禁用提交按钮方案

    最基本的防重复提交方法是在表单提交后禁用提交按钮,直到请求完成。

    案例实现:

      

    方案一:禁用提交按钮

    用户名
    邮箱
    提交中... 提交
    {{ message }}
    import { ref, reactive } from 'vue'; const formData = reactive({ username: '', email: '' }); const isSubmitting = ref(false); const message = ref(''); const success = ref(false); async function submitForm() { // 如果已经在提交中,直接返回 if (isSubmitting.value) { return; } try { // 设置提交状态为true isSubmitting.value = true; message.value = ''; // 模拟API请求 await new Promise(resolve => setTimeout(resolve, 2000)); // 请求成功 success.value = true; message.value = '表单提交成功!'; // 重置表单 formData.username = ''; formData.email = ''; } catch (error) { // 请求失败 success.value = false; message.value = '提交失败:' + (error.message || '未知错误'); } finally { // 无论成功失败,都将提交状态设为false isSubmitting.value = false; } }

    优点:

    • 实现简单,适用于大多数场景
    • 用户体验良好,提供明确的视觉反馈

      缺点:

      • 如果用户刷新页面,状态会丢失
      • 不能防止用户通过其他方式(如API工具)重复提交

        2. 提交状态与加载指示器方案

        增强用户体验,添加加载指示器,让用户知道请求正在处理中。

        案例实现:

          

        方案二:提交状态与加载指示器

        标题
        内容
        处理中... 发布文章
        {{ submitStatus.message }}

        正在提交您的文章,请稍候...

        import { ref, reactive } from 'vue'; const formData = reactive({ title: '', content: '' }); const isSubmitting = ref(false); const submitStatus = reactive({ show: false, success: false, message: '' }); async function submitForm() { if (isSubmitting.value) { return; } try { isSubmitting.value = true; submitStatus.show = false; // 模拟API请求 await new Promise(resolve => setTimeout(resolve, 3000)); // 请求成功 submitStatus.success = true; submitStatus.message = '文章发布成功!'; submitStatus.show = true; // 重置表单 formData.title = ''; formData.content = ''; } catch (error) { // 请求失败 submitStatus.success = false; submitStatus.message = '发布失败:' + (error.message || '服务器错误'); submitStatus.show = true; } finally { isSubmitting.value = false; } }

        优点:

        • 提供更丰富的视觉反馈
        • 防止用户在请求处理过程中进行其他操作

          缺点:

          • 仍然不能防止用户刷新页面后重新提交
          • 不能防止恶意用户通过其他方式重复提交

            3. 表单令牌方案

            使用唯一令牌标识每个表单实例,确保同一表单只能提交一次。

            案例实现:

              

            方案三:表单令牌

            姓名
            电话
            地址
            提交中... 提交订单
            {{ resultMessage }}

            检测到此表单已提交过,请勿重复提交!

            重置表单
            import { ref, reactive, onMounted } from 'vue'; const formData = reactive({ name: '', phone: '', address: '' }); const isSubmitting = ref(false); const resultMessage = ref(''); const isSuccess = ref(false); const isTokenUsed = ref(false); const formToken = ref(''); // 生成唯一令牌 function generateToken() { return Date.now().toString(36) + Math.random().toString(36).substring(2); } // 检查令牌是否已使用 function checkTokenUsed(token) { const usedTokens = JSON.parse(localStorage.getItem('usedFormTokens') || '[]'); return usedTokens.includes(token); } // 标记令牌为已使用 function markTokenAsUsed(token) { const usedTokens = JSON.parse(localStorage.getItem('usedFormTokens') || '[]'); usedTokens.push(token); localStorage.setItem('usedFormTokens', JSON.stringify(usedTokens)); } // 重置表单和令牌 function resetForm() { formData.name = ''; formData.phone = ''; formData.address = ''; formToken.value = generateToken(); isTokenUsed.value = false; resultMessage.value = ''; } async function submitForm() { // 检查令牌是否已使用 if (checkTokenUsed(formToken.value)) { isTokenUsed.value = true; return; } if (isSubmitting.value) { return; } try { isSubmitting.value = true; resultMessage.value = ''; // 模拟API请求 await new Promise(resolve => setTimeout(resolve, 2000)); // 标记令牌为已使用 markTokenAsUsed(formToken.value); // 请求成功 isSuccess.value = true; resultMessage.value = '订单提交成功!'; } catch (error) { // 请求失败 isSuccess.value = false; resultMessage.value = '提交失败:' + (error.message || '服务器错误'); } finally { isSubmitting.value = false; } } onMounted(() => { // 组件挂载时生成令牌 formToken.value = generateToken(); });

            优点:

            防重复提交详解:从前端Vue到后端Java的全面解决方案
            (图片来源网络,侵删)
            • 可以防止同一表单多次提交
            • 即使用户刷新页面,也能检测到表单已提交

              缺点:

              • 本地存储的令牌可能被清除
              • 需要后端配合验证令牌

                4. 防抖与节流方案

                使用防抖(debounce)或节流(throttle)技术防止用户快速多次点击提交按钮。

                防重复提交详解:从前端Vue到后端Java的全面解决方案
                (图片来源网络,侵删)

                案例实现:

                  

                方案四:防抖与节流

                搜索关键词
                普通提交
                点击次数: {{ normalClickCount }}
                防抖提交
                实际提交次数: {{ debounceSubmitCount }}
                节流提交
                实际提交次数: {{ throttleSubmitCount }}
                重置计数

                日志:

                {{ log }}
                import { ref, onUnmounted } from 'vue'; const searchTerm = ref(''); const normalClickCount = ref(0); const debounceSubmitCount = ref(0); const throttleSubmitCount = ref(0); const logs = ref([]); // 添加日志 function addLog(message) { const now = new Date(); const timeStr = `${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}.${now.getMilliseconds()}`; logs.value.unshift(`[${timeStr}] ${message}`); } // 普通提交 function normalSubmit() { normalClickCount.value++; addLog(`普通提交被触发,搜索词: ${searchTerm.value}`); } // 防抖函数 function debounce(func, delay) { let timer = null; return function(...args) { if (timer) clearTimeout(timer); timer = setTimeout(() => { func.apply(this, args); }, delay); }; } // 节流函数 function throttle(func, limit) { let inThrottle = false; return function(...args) { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => { inThrottle = false; }, limit); } }; } // 防抖提交处理函数 function handleDebouncedSubmit() { debounceSubmitCount.value++; addLog(`防抖提交被触发,搜索词: ${searchTerm.value}`); } // 节流提交处理函数 function handleThrottledSubmit() { throttleSubmitCount.value++; addLog(`节流提交被触发,搜索词: ${searchTerm.value}`); } // 创建防抖和节流版本的提交函数 const debouncedSubmit = debounce(handleDebouncedSubmit, 1000); // 1秒防抖 const throttledSubmit = throttle(handleThrottledSubmit, 2000); // 2秒节流 // 重置计数 function resetCounts() { normalClickCount.value = 0; debounceSubmitCount.value = 0; throttleSubmitCount.value = 0; logs.value = []; addLog('计数已重置'); } // 组件卸载时清除定时器 onUnmounted(() => { // 这里应该清除定时器,但由于我们的防抖和节流函数是闭包形式, // 实际项目中应该使用更完善的实现方式,确保定时器被正确清除 });

                优点:

                防重复提交详解:从前端Vue到后端Java的全面解决方案
                (图片来源网络,侵删)
                • 有效防止用户快速多次点击
                • 减轻服务器负担
                • 适用于搜索、自动保存等场景

                  缺点:

                  • 不适用于所有场景,如支付等需要精确控制的操作
                  • 需要合理设置延迟时间

                    三、后端防重复提交(Java)

                    1. 表单令牌验证方案

                    后端验证前端提交的表单令牌,确保同一令牌只能使用一次。

                    案例实现:

                    // Controller层
                    @RestController
                    @RequestMapping("/api")
                    public class FormController {
                        
                        private final FormTokenService tokenService;
                        private final FormService formService;
                        
                        public FormController(FormTokenService tokenService, FormService formService) {
                            this.tokenService = tokenService;
                            this.formService = formService;
                        }
                        
                        @PostMapping("/submit")
                        public ResponseEntity submitForm(@RequestBody FormRequest request,
                                                           @RequestHeader("X-Form-Token") String token) {
                            
                            // 验证令牌是否有效
                            if (!tokenService.isValidToken(token)) {
                                return ResponseEntity
                                    .status(HttpStatus.BAD_REQUEST)
                                    .body(new ApiResponse(false, "无效的表单令牌"));
                            }
                            
                            // 验证令牌是否已使用
                            if (tokenService.isTokenUsed(token)) {
                                return ResponseEntity
                                    .status(HttpStatus.TOO_MANY_REQUESTS)
                                    .body(new ApiResponse(false, "表单已提交,请勿重复提交"));
                            }
                            
                            try {
                                // 标记令牌为已使用(在处理业务逻辑前)
                                tokenService.markTokenAsUsed(token);
                                
                                // 处理表单提交
                                String formId = formService.processForm(request);
                                
                                return ResponseEntity.ok(new ApiResponse(true, "表单提交成功", formId));
                                
                            } catch (Exception e) {
                                // 发生异常时,可以选择是否将令牌标记为未使用
                                // tokenService.invalidateToken(token);
                                
                                return ResponseEntity
                                    .status(HttpStatus.INTERNAL_SERVER_ERROR)
                                    .body(new ApiResponse(false, "表单提交失败: " + e.getMessage()));
                            }
                        }
                    }
                    // 令牌服务接口
                    public interface FormTokenService {
                        boolean isValidToken(String token);
                        boolean isTokenUsed(String token);
                        void markTokenAsUsed(String token);
                        void invalidateToken(String token);
                    }
                    // 令牌服务实现(使用内存缓存)
                    @Service
                    public class FormTokenServiceImpl implements FormTokenService {
                        
                        // 使用Caffeine缓存库
                        private final Cache usedTokens;
                        
                        public FormTokenServiceImpl() {
                            // 创建缓存,24小时后过期
                            this.usedTokens = Caffeine.newBuilder()
                                .expireAfterWrite(24, TimeUnit.HOURS)
                                .maximumSize(10_000)
                                .build();
                        }
                        
                        @Override
                        public boolean isValidToken(String token) {
                            // 简单验证:非空且长度合适
                            return token != null && token.length() >= 8;
                        }
                        
                        @Override
                        public boolean isTokenUsed(String token) {
                            return usedTokens.getIfPresent(token) != null;
                        }
                        
                        @Override
                        public void markTokenAsUsed(String token) {
                            usedTokens.put(token, Boolean.TRUE);
                        }
                        
                        @Override
                        public void invalidateToken(String token) {
                            usedTokens.invalidate(token);
                        }
                    }
                    // 请求和响应类
                    public class FormRequest {
                        private String name;
                        private String email;
                        private String content;
                        
                        // getters and setters
                    }
                    public class ApiResponse {
                        private boolean success;
                        private String message;
                        private Object data;
                        
                        public ApiResponse(boolean success, String message) {
                            this.success = success;
                            this.message = message;
                        }
                        
                        public ApiResponse(boolean success, String message, Object data) {
                            this.success = success;
                            this.message = message;
                            this.data = data;
                        }
                        
                        // getters
                    }
                    

                    优点:

                    • 可靠地防止重复提交
                    • 可以设置令牌过期时间
                    • 适用于各种表单提交场景

                      缺点:

                      • 需要前后端配合
                      • 缓存管理可能增加系统复杂性

                        2. 数据库唯一约束方案

                        利用数据库唯一约束防止重复数据插入。

                        案例实现:

                        // 实体类
                        @Entity
                        @Table(name = "orders", 
                               uniqueConstraints = @UniqueConstraint(columnNames = {"order_number"}))
                        public class Order {
                            
                            @Id
                            @GeneratedValue(strategy = GenerationType.IDENTITY)
                            private Long id;
                            
                            @Column(name = "order_number", unique = true, nullable = false)
                            private String orderNumber;
                            
                            @Column(name = "customer_name")
                            private String customerName;
                            
                            @Column(name = "amount")
                            private BigDecimal amount;
                            
                            @Column(name = "created_at")
                            private LocalDateTime createdAt;
                            
                            // getters and setters
                        }
                        // 仓库接口
                        @Repository
                        public interface OrderRepository extends JpaRepository {
                            boolean existsByOrderNumber(String orderNumber);
                        }
                        // 服务实现
                        @Service
                        public class OrderServiceImpl implements OrderService {
                            
                            private final OrderRepository orderRepository;
                            
                            public OrderServiceImpl(OrderRepository orderRepository) {
                                this.orderRepository = orderRepository;
                            }
                            
                            @Override
                            @Transactional
                            public String createOrder(OrderRequest request) {
                                // 生成订单号
                                String orderNumber = generateOrderNumber();
                                
                                // 检查订单号是否已存在
                                if (orderRepository.existsByOrderNumber(orderNumber)) {
                                    throw new DuplicateOrderException("订单号已存在");
                                }
                                
                                // 创建订单
                                Order order = new Order();
                                order.setOrderNumber(orderNumber);
                                order.setCustomerName(request.getCustomerName());
                                order.setAmount(request.getAmount());
                                order.setCreatedAt(LocalDateTime.now());
                                
                                try {
                                    orderRepository.save(order);
                                    return orderNumber;
                                } catch (DataIntegrityViolationException e) {
                                    // 捕获唯一约束违反异常
                                    throw new DuplicateOrderException("创建订单失败,可能是重复提交", e);
                                }
                            }
                            
                            private String generateOrderNumber() {
                                // 生成唯一订单号的逻辑
                                return "ORD" + System.currentTimeMillis() + 
                                       String.format("%04d", new Random().nextInt(10000));
                            }
                        }
                        // 控制器
                        @RestController
                        @RequestMapping("/api/orders")
                        public class OrderController {
                            
                            private final OrderService orderService;
                            
                            public OrderController(OrderService orderService) {
                                this.orderService = orderService;
                            }
                            
                            @PostMapping
                            public ResponseEntity createOrder(@RequestBody OrderRequest request) {
                                try {
                                    String orderNumber = orderService.createOrder(request);
                                    return ResponseEntity.ok(new ApiResponse(true, "订单创建成功", orderNumber));
                                } catch (DuplicateOrderException e) {
                                    return ResponseEntity
                                        .status(HttpStatus.CONFLICT)
                                        .body(new ApiResponse(false, e.getMessage()));
                                } catch (Exception e) {
                                    return ResponseEntity
                                        .status(HttpStatus.INTERNAL_SERVER_ERROR)
                                        .body(new ApiResponse(false, "创建订单失败: " + e.getMessage()));
                                }
                            }
                        }
                        // 异常类
                        public class DuplicateOrderException extends RuntimeException {
                            public DuplicateOrderException(String message) {
                                super(message);
                            }
                            
                            public DuplicateOrderException(String message, Throwable cause) {
                                super(message, cause);
                            }
                        }
                        

                        优点:

                        • 在数据库层面保证数据唯一性
                        • 即使应用服务器出现问题,也能保证数据一致性
                        • 适用于关键业务数据

                          缺点:

                          • 只能防止数据重复,不能防止业务逻辑重复执行
                          • 可能导致用户体验不佳(如果没有适当的错误处理)

                            3. 事务隔离与锁机制方案

                            使用数据库事务隔离级别和锁机制防止并发提交。

                            案例实现:

                            // 服务实现
                            @Service
                            public class PaymentServiceImpl implements PaymentService {
                                
                                private final PaymentRepository paymentRepository;
                                private final AccountRepository accountRepository;
                                
                                public PaymentServiceImpl(PaymentRepository paymentRepository, 
                                                         AccountRepository accountRepository) {
                                    this.paymentRepository = paymentRepository;
                                    this.accountRepository = accountRepository;
                                }
                                
                                @Override
                                @Transactional(isolation = Isolation.SERIALIZABLE)
                                public String processPayment(PaymentRequest request) {
                                    // 检查是否存在相同的支付请求
                                    if (paymentRepository.existsByTransactionId(request.getTransactionId())) {
                                        throw new DuplicatePaymentException("该交易已处理,请勿重复支付");
                                    }
                                    
                                    // 获取账户(使用悲观锁)
                                    Account account = accountRepository.findByIdWithLock(request.getAccountId())
                                        .orElseThrow(() -> new AccountNotFoundException("账户不存在"));
                                    
                                    // 检查余额
                                    if (account.getBalance().compareTo(request.getAmount())  
                            

                            优点:

                            • 可以有效防止并发情况下的重复提交
                            • 保证数据一致性
                            • 适用于金融交易等高敏感度场景

                              缺点:

                              • 高隔离级别可能影响系统性能
                              • 锁机制可能导致死锁
                              • 实现复杂度较高

                                4. 分布式锁方案

                                在分布式系统中使用分布式锁防止重复提交。

                                案例实现(使用Redis实现分布式锁):

                                // 分布式锁服务接口
                                public interface DistributedLockService {
                                    boolean tryLock(String lockKey, long waitTime, long leaseTime, TimeUnit unit);
                                    void unlock(String lockKey);
                                    boolean isLocked(String lockKey);
                                }
                                // Redis实现的分布式锁服务
                                @Service
                                public class RedisDistributedLockService implements DistributedLockService {
                                    
                                    private final RedissonClient redissonClient;
                                    
                                    public RedisDistributedLockService(RedissonClient redissonClient) {
                                        this.redissonClient = redissonClient;
                                    }
                                    
                                    @Override
                                    public boolean tryLock(String lockKey, long waitTime, long leaseTime, TimeUnit unit) {
                                        RLock lock = redissonClient.getLock(lockKey);
                                        try {
                                            return lock.tryLock(waitTime, leaseTime, unit);
                                        } catch (InterruptedException e) {
                                            Thread.currentThread().interrupt();
                                            return false;
                                        }
                                    }
                                    
                                    @Override
                                    public void unlock(String lockKey) {
                                        RLock lock = redissonClient.getLock(lockKey);
                                        if (lock.isHeldByCurrentThread()) {
                                            lock.unlock();
                                        }
                                    }
                                    
                                    @Override
                                    public boolean isLocked(String lockKey) {
                                        RLock lock = redissonClient.getLock(lockKey);
                                        return lock.isLocked();
                                    }
                                }
                                // 使用分布式锁的服务实现
                                @Service
                                public class RegistrationServiceImpl implements RegistrationService {
                                    
                                    private final DistributedLockService lockService;
                                    private final UserRepository userRepository;
                                    
                                    public RegistrationServiceImpl(DistributedLockService lockService,
                                                                  UserRepository userRepository) {
                                        this.lockService = lockService;
                                        this.userRepository = userRepository;
                                    }
                                    
                                    @Override
                                    public String registerUser(UserRegistrationRequest request) {
                                        // 创建锁键(基于用户名或邮箱)
                                        String lockKey = "user_registration:" + request.getEmail();
                                        
                                        boolean locked = false;
                                        try {
                                            // 尝试获取锁,等待5秒,锁定30秒
                                            locked = lockService.tryLock(lockKey, 5, 30, TimeUnit.SECONDS);
                                            
                                            if (!locked) {
                                                throw new ConcurrentOperationException("操作正在处理中,请稍后再试");
                                            }
                                            
                                            // 检查用户是否已存在
                                            if (userRepository.existsByEmail(request.getEmail())) {
                                                throw new DuplicateUserException("该邮箱已注册");
                                            }
                                            
                                            // 创建用户
                                            User user = new User();
                                            user.setUsername(request.getUsername());
                                            user.setEmail(request.getEmail());
                                            user.setPassword(encryptPassword(request.getPassword()));
                                            user.setCreatedAt(LocalDateTime.now());
                                            
                                            userRepository.save(user);
                                            
                                            return user.getId().toString();
                                            
                                        } finally {
                                            // 释放锁
                                            if (locked) {
                                                lockService.unlock(lockKey);
                                            }
                                        }
                                    }
                                    
                                    private String encryptPassword(String password) {
                                        // 密码加密逻辑
                                        return BCrypt.hashpw(password, BCrypt.gensalt());
                                    }
                                }
                                // 控制器
                                @RestController
                                @RequestMapping("/api/users")
                                public class UserController {
                                    
                                    private final RegistrationService registrationService;
                                    
                                    public UserController(RegistrationService registrationService) {
                                        this.registrationService = registrationService;
                                    }
                                    
                                    @PostMapping("/register")
                                    public ResponseEntity registerUser(@RequestBody UserRegistrationRequest request) {
                                        try {
                                            String userId = registrationService.registerUser(request);
                                            return ResponseEntity.ok(new ApiResponse(true, "用户注册成功", userId));
                                        } catch (DuplicateUserException e) {
                                            return ResponseEntity
                                                .status(HttpStatus.CONFLICT)
                                                .body(new ApiResponse(false, e.getMessage()));
                                        } catch (ConcurrentOperationException e) {
                                            return ResponseEntity
                                                .status(HttpStatus.TOO_MANY_REQUESTS)
                                                .body(new ApiResponse(false, e.getMessage()));
                                        } catch (Exception e) {
                                            return ResponseEntity
                                                .status(HttpStatus.INTERNAL_SERVER_ERROR)
                                                .body(new ApiResponse(false, "注册失败: " + e.getMessage()));
                                        }
                                    }
                                }
                                

                                优点:

                                • 适用于分布式系统环境
                                • 可以跨服务器防止重复提交
                                • 灵活的锁定策略

                                  缺点:

                                  • 依赖外部系统(如Redis)
                                  • 实现复杂度高
                                  • 需要处理锁超时和失效情况

                                    四、前后端结合的完整解决方案

                                    完整案例:订单提交系统

                                    下面是一个结合前端Vue和后端Java的完整订单提交系统,综合运用多种防重复提交技术。

                                    前端实现(Vue.js):

                                      

                                    订单提交系统

                                    客户信息

                                    客户姓名
                                    联系电话

                                    订单信息

                                    产品选择 请选择产品 产品A - ¥100 产品B - ¥200 产品C - ¥300
                                    数量
                                    收货地址

                                    订单摘要

                                    产品价格: ¥{{ productPrice }}
                                    数量: {{ orderData.quantity || 0 }}
                                    总计: ¥{{ totalPrice }}
                                    订单提交中... 订单已提交 提交订单

                                    {{ resultMessage }}

                                    订单号: {{ orderNumber }}

                                    确认提交订单

                                    您确定要提交此订单吗?提交后将无法修改。

                                    取消 确认提交
                                    import { ref, reactive, computed, onMounted } from 'vue'; // 订单数据 const orderData = reactive({ customerName: '', phone: '', productId: '', quantity: 1, address: '' }); // 状态变量 const isSubmitting = ref(false); const isOrderSubmitted = ref(false); const resultMessage = ref(''); const isSuccess = ref(false); const orderNumber = ref(''); const orderToken = ref(''); const showConfirmDialog = ref(false); // 计算属性 const productPrice = computed(() => { switch (orderData.productId) { case '1': return 100; case '2': return 200; case '3': return 300; default: return 0; } }); const totalPrice = computed(() => { return productPrice.value * (orderData.quantity || 0); }); // 生成唯一令牌 function generateToken() { return Date.now().toString(36) + Math.random().toString(36).substring(2); } // 防抖函数 function debounce(func, delay) { let timer = null; return function(...args) { if (timer) clearTimeout(timer); timer = setTimeout(() => { func.apply(this, args); }, delay); }; } // 提交订单(显示确认对话框) function submitOrder() { // 如果已提交或正在提交,直接返回 if (isSubmitting.value || isOrderSubmitted.value) { return; } // 显示确认对话框 showConfirmDialog.value = true; } // 确认提交(实际提交逻辑) const confirmSubmit = debounce(async function() { showConfirmDialog.value = false; if (isSubmitting.value || isOrderSubmitted.value) { return; } try { isSubmitting.value = true; resultMessage.value = ''; // 准备提交数据 const payload = { ...orderData, totalPrice: totalPrice.value, _token: orderToken.value }; // 发送到后端 const response = await fetch('/api/orders', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Order-Token': orderToken.value }, body: JSON.stringify(payload) }); const data = await response.json(); if (!response.ok) { throw new Error(data.message || '订单提交失败'); } // 提交成功 isSuccess.value = true; resultMessage.value = '订单提交成功!'; orderNumber.value = data.data; // 订单号 isOrderSubmitted.value = true; // 生成新令牌(以防用户想再次提交) orderToken.value = generateToken(); } catch (error) { // 提交失败 isSuccess.value = false; resultMessage.value = error.message; } finally { isSubmitting.value = false; } }, 300); onMounted(() => { // 组件挂载时生成令牌 orderToken.value = generateToken(); });

                                    后端实现(Java Spring Boot):

                                    // 订单实体
                                    @Entity
                                    @Table(name = "orders", 
                                           uniqueConstraints = @UniqueConstraint(columnNames = {"order_number"}))
                                    public class Order {
                                        
                                        @Id
                                        @GeneratedValue(strategy = GenerationType.IDENTITY)
                                        private Long id;
                                        
                                        @Column(name = "order_number", unique = true, nullable = false)
                                        private String orderNumber;
                                        
                                        @Column(name = "customer_name")
                                        private String customerName;
                                        
                                        @Column(name = "phone")
                                        private String phone;
                                        
                                        @Column(name = "product_id")
                                        private Long productId;
                                        
                                        @Column(name = "quantity")
                                        private Integer quantity;
                                        
                                        @Column(name = "address")
                                        private String address;
                                        
                                        @Column(name = "total_price")
                                        private BigDecimal totalPrice;
                                        
                                        @Column(name = "status")
                                        private String status;
                                        
                                        @Column(name = "created_at")
                                        private LocalDateTime createdAt;
                                        
                                        // getters and setters
                                    }
                                    // 订单服务接口
                                    public interface OrderService {
                                        String createOrder(OrderRequest request);
                                    }
                                    // 订单服务实现
                                    @Service
                                    @Transactional
                                    public class OrderServiceImpl implements OrderService {
                                        
                                        private final OrderRepository orderRepository;
                                        private final OrderTokenService tokenService;
                                        
                                        public OrderServiceImpl(OrderRepository orderRepository, 
                                                               OrderTokenService tokenService) {
                                            this.orderRepository = orderRepository;
                                            this.tokenService = tokenService;
                                        }
                                        
                                        @Override
                                        @Transactional(isolation = Isolation.SERIALIZABLE)
                                        public String createOrder(OrderRequest request) {
                                            // 验证令牌
                                            String token = request.getToken();
                                            if (tokenService.isTokenUsed(token)) {
                                                throw new DuplicateOrderException("订单已提交,请勿重复提交");
                                            }
                                            
                                            try {
                                                // 标记令牌为已使用
                                                tokenService.markTokenAsUsed(token);
                                                
                                                // 生成订单号
                                                String orderNumber = generateOrderNumber();
                                                
                                                // 创建订单
                                                Order order = new Order();
                                                order.setOrderNumber(orderNumber);
                                                order.setCustomerName(request.getCustomerName());
                                                order.setPhone(request.getPhone());
                                                order.setProductId(request.getProductId());
                                                order.setQuantity(request.getQuantity());
                                                order.setAddress(request.getAddress());
                                                order.setTotalPrice(request.getTotalPrice());
                                                order.setStatus("PENDING");
                                                order.setCreatedAt(LocalDateTime.now());
                                                
                                                orderRepository.save(order);
                                                
                                                // 异步处理订单(示例)
                                                processOrderAsync(order);
                                                
                                                return orderNumber;
                                                
                                            } catch (DataIntegrityViolationException e) {
                                                // 捕获数据库唯一约束异常
                                                throw new DuplicateOrderException("订单创建失败,可能是重复提交", e);
                                            }
                                        }
                                        
                                        private String generateOrderNumber() {
                                            return "ORD" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) + 
                                                   String.format("%04d", new Random().nextInt(10000));
                                        }
                                        
                                        @Async
                                        public void processOrderAsync(Order order) {
                                            // 异步处理订单的逻辑
                                            try {
                                                // 模拟处理时间
                                                Thread.sleep(5000);
                                                
                                                // 更新订单状态
                                                order.setStatus("PROCESSED");
                                                orderRepository.save(order);
                                                
                                            } catch (Exception e) {
                                                // 处理异常
                                                order.setStatus("ERROR");
                                                orderRepository.save(order);
                                            }
                                        }
                                    }
                                    // 令牌服务实现
                                    @Service
                                    public class OrderTokenServiceImpl implements OrderTokenService {
                                        
                                        private final RedisTemplate redisTemplate;
                                        
                                        public OrderTokenServiceImpl(RedisTemplate redisTemplate) {
                                            this.redisTemplate = redisTemplate;
                                        }
                                        
                                        @Override
                                        public boolean isTokenUsed(String token) {
                                            Boolean used = redisTemplate.opsForValue().get("order_token:" + token);
                                            return used != null && used;
                                        }
                                        
                                        @Override
                                        public void markTokenAsUsed(String token) {
                                            redisTemplate.opsForValue().set("order_token:" + token, true, 24, TimeUnit.HOURS);
                                        }
                                        
                                        @Override
                                        public void invalidateToken(String token) {
                                            redisTemplate.delete("order_token:" + token);
                                        }
                                    }
                                    // 控制器
                                    @RestController
                                    @RequestMapping("/api/orders")
                                    public class OrderController {
                                        
                                        private final OrderService orderService;
                                        private static final Logger logger = LoggerFactory.getLogger(OrderController.class);
                                        
                                        public OrderController(OrderService orderService) {
                                            this.orderService = orderService;
                                        }
                                        
                                        @PostMapping
                                        public ResponseEntity createOrder(@RequestBody OrderRequest request,
                                                                            @RequestHeader("X-Order-Token") String token) {
                                            // 设置令牌(以防请求体中没有)
                                            request.setToken(token);
                                            
                                            try {
                                                // 记录请求日志
                                                logger.info("Received order request with token: {}", token);
                                                
                                                // 创建订单
                                                String orderNumber = orderService.createOrder(request);
                                                
                                                // 记录成功日志
                                                logger.info("Order created successfully: {}", orderNumber);
                                                
                                                return ResponseEntity.ok(new ApiResponse(true, "订单提交成功", orderNumber));
                                                
                                            } catch (DuplicateOrderException e) {
                                                // 记录重复提交日志
                                                logger.warn("Duplicate order submission: {}", e.getMessage());
                                                
                                                return ResponseEntity
                                                    .status(HttpStatus.CONFLICT)
                                                    .body(new ApiResponse(false, e.getMessage()));
                                                    
                                            } catch (Exception e) {
                                                // 记录错误日志
                                                logger.error("Error creating order", e);
                                                
                                                return ResponseEntity
                                                    .status(HttpStatus.INTERNAL_SERVER_ERROR)
                                                    .body(new ApiResponse(false, "订单提交失败: " + e.getMessage()));
                                            }
                                        }
                                    }
                                    

                                    五、最佳实践与总结

                                    最佳实践

                                    1. 多层防护:

                                    2. 前端:禁用按钮 + 视觉反馈 + 表单令牌

                                    3. 后端:令牌验证 + 数据库约束 + 事务隔离

                                    4. 分布式系统:分布式锁 + 幂等性设计

                                    5. 前端防护:

                                    6. 禁用提交按钮,防止用户多次点击

                                    7. 提供明确的加载状态反馈

                                    8. 使用防抖/节流限制快速点击

                                    9. 添加确认对话框增加用户确认步骤

                                    10. 生成并使用表单令牌

                                    11. 后端防护:

                                    12. 验证前端提交的令牌

                                    13. 使用数据库唯一约束

                                    14. 选择合适的事务隔离级别

                                    15. 实现幂等性API设计

                                    16. 使用分布式锁(在分布式系统中)

                                    17. 记录详细日志,便于问题排查

                                    18. 异常处理:

                                    19. 前端友好展示错误信息

                                    20. 后端返回明确的错误状态码和信息

                                    21. 区分不同类型的错误(如重复提交、服务器错误等)

                                    22. 性能考虑:

                                    23. 避免过度使用高隔离级别事务

                                    24. 合理设置锁超时时间

                                    25. 使用异步处理长时间运行的任务

                                    总结

                                    防止表单重复提交是Web应用开发中的重要环节,需要前后端协同配合。本文详细介绍了多种防重复提交的解决方案:

                                    1. 前端Vue.js解决方案:

                                    2. 禁用提交按钮

                                    3. 提交状态与加载指示器

                                    4. 表单令牌

                                    5. 防抖与节流

                                    6. 后端Java解决方案:

                                    7. 表单令牌验证

                                    8. 数据库唯一约束

                                    9. 事务隔离与锁机制

                                    10. 分布式锁

                                    11. 综合解决方案:

                                    12. 结合前后端多种技术

                                    13. 多层次防护机制

                                    14. 完善的异常处理

                                    15. 良好的用户体验

                                    通过合理选择和组合这些技术,可以有效防止表单重复提交问题,保证系统数据一致性和用户体验。在实际应用中,应根据业务场景和系统架构选择最适合的解决方案。

免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们。

相关阅读

目录[+]

取消
微信二维码
微信二维码
支付宝二维码