重复提交是开发中经常遇到的问题。用户可能因为页面响应慢连续点击提交按钮,或者网络延迟时反复重试。这些情况会导致数据重复、业务混乱,比如生成重复订单、多次扣款等问题。
防止重复提交需要前后端配合。前端主要提升用户体验,后端才是真正的保障。
前端方法能防止用户误操作,但不能完全依赖,因为可以通过工具绕过前端验证。
function submitForm() {
    const btn = document.getElementById('submitBtn');
    
    // 如果按钮已禁用,直接返回
    if (btn.disabled) return;
    
    // 禁用按钮并改变文字
    btn.disabled = true;
    btn.textContent = '提交中...';
    
    // 发送请求
    fetch('/api/submit', {
        method: 'POST',
        body: JSON.stringify(formData)
    })
    .then(response => response.json())
    .then(data => {
        // 处理响应
    })
    .catch(error => {
        console.error('Error:', error);
    })
    .finally(() => {
        // 无论成功失败,都重新启用按钮
        btn.disabled = false;
        btn.textContent = '提交';
    });
}function debounce(func, wait) {
    let timeout;
    return function() {
        const context = this;
        const args = arguments;
        
        clearTimeout(timeout);
        timeout = setTimeout(() => {
            func.apply(context, args);
        }, wait);
    };
}
// 使用示例
const submitForm = debounce(function() {
    // 实际的提交逻辑
}, 1000); // 1秒内只能点击一次class RequestManager {
    constructor() {
        this.pendingRequests = new Map();
    }
    
    generateKey(config) {
        return `${config.method}-${config.url}-${JSON.stringify(config.data)}`;
    }
    
    addRequest(config) {
        const key = this.generateKey(config);
        if (this.pendingRequests.has(key)) {
            return false;
        }
        this.pendingRequests.set(key, true);
        return true;
    }
    
    removeRequest(config) {
        const key = this.generateKey(config);
        this.pendingRequests.delete(key);
    }
}
// 在axios拦截器中使用
const requestManager = new RequestManager();
axios.interceptors.request.use(config => {
    if (!requestManager.addRequest(config)) {
        return Promise.reject(new Error('请求已处理中'));
    }
    return config;
});
axios.interceptors.response.use(response => {
    requestManager.removeRequest(response.config);
    return response;
}, error => {
    if (error.config) {
        requestManager.removeRequest(error.config);
    }
    return Promise.reject(error);
});前端方法的优点是提升用户体验,缺点是可以被绕过。因此后端验证是必须的。
工作流程:
用户访问页面时,后端生成唯一Token
Token随页面返回给前端
提交表单时携带Token
后端验证Token有效性
验证成功后立即删除Token
Java实现示例:
@Component
public class TokenService {
    
    // 生成Token
    public String createToken(HttpServletRequest request) {
        String token = UUID.randomUUID().toString();
        // 存储到Session中
        request.getSession().setAttribute("FORM_TOKEN", token);
        return token;
    }
    
    // 验证Token
    public boolean verifyToken(HttpServletRequest request) {
        String clientToken = request.getParameter("token");
        if (clientToken == null) {
            return false;
        }
        
        HttpSession session = request.getSession();
        String serverToken = (String) session.getAttribute("FORM_TOKEN");
        
        if (serverToken == null || !serverToken.equals(clientToken)) {
            return false;
        }
        
        // 验证成功后立即删除
        session.removeAttribute("FORM_TOKEN");
        return true;
    }
}前端表单:
<form action="/submit" method="post">
    <input type="hidden" name="token" value="${token}">
    <!-- 其他表单字段 -->
    <button type="submit">提交</button>
</form>定义注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PreventDuplicate {
    int expire() default 5; // 锁定时间,默认5秒
    String key() default ""; // 自定义锁key
}实现切面:
@Aspect
@Component
public class DuplicateSubmitAspect {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    @Around("@annotation(preventDuplicate)")
    public Object checkDuplicate(ProceedingJoinPoint joinPoint, 
                               PreventDuplicate preventDuplicate) throws Throwable {
        
        HttpServletRequest request = getRequest();
        String lockKey = buildLockKey(request, preventDuplicate);
        int expireTime = preventDuplicate.expire();
        
        // 尝试加锁
        Boolean success = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, "1", Duration.ofSeconds(expireTime));
            
        if (!success) {
            throw new RuntimeException("请勿重复提交");
        }
        
        try {
            return joinPoint.proceed(); // 执行原方法
        } finally {
            // 根据业务需求决定是否立即删除锁
            // redisTemplate.delete(lockKey);
        }
    }
    
    private String buildLockKey(HttpServletRequest request, 
                              PreventDuplicate preventDuplicate) {
        String userId = getUserId(request); // 获取用户ID
        String uri = request.getRequestURI();
        String params = request.getQueryString() != null ? 
                       request.getQueryString() : "";
        
        return "submit:lock:" + userId + ":" + uri + ":" + 
               DigestUtils.md5DigestAsHex(params.getBytes());
    }
    
    private HttpServletRequest getRequest() {
        RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes sra = (ServletRequestAttributes) attributes;
        return sra.getRequest();
    }
}使用方式:
@PostMapping("/order/create")
@PreventDuplicate(expire = 10)
public Result createOrder(@RequestBody OrderDTO order) {
    // 业务逻辑
    return Result.success("订单创建成功");
}例如订单表:
CREATE TABLE orders (
    id BIGINT PRIMARY KEY,
    order_no VARCHAR(64) UNIQUE, -- 订单号唯一约束
    user_id BIGINT,
    amount DECIMAL(10,2),
    create_time DATETIME
);在业务代码中处理:
@Service
public class OrderService {
    
    public Result createOrder(OrderDTO order) {
        try {
            // 尝试插入订单
            orderMapper.insert(order);
            return Result.success("创建成功");
        } catch (DuplicateKeyException e) {
            // 捕获唯一约束异常
            log.warn("重复订单: {}", order.getOrderNo());
            return Result.error("订单已存在");
        }
    }
}| 方案 | 适用场景 | 优点 | 缺点 | 
|---|---|---|---|
| 按钮禁用 | 所有前端表单 | 用户体验好 | 可被绕过 | 
| Token机制 | 表单提交 | 安全可靠 | 分布式环境需要共享Session | 
| AOP+Redis | 分布式系统 | 无侵入,灵活 | 依赖Redis | 
| 数据库约束 | 有唯一性要求 | 绝对可靠 | 只能防止最终重复 | 
前后端结合使用:前端防止误操作,后端保障数据安全
合理设置超时时间:一般5-10秒足够,避免影响正常操作
友好提示用户:不要直接报错,提示"操作进行中"或"请勿重复提交"
记录重复提交:监控重复提交情况,帮助优化系统
考虑幂等性:重要业务要实现幂等接口
防止重复提交是系统稳定性的基础保障。选择方案时要根据实际业务需求,有时候需要多种方案组合使用,才能达到最好的效果。
最重要的是,不要完全依赖前端的防护,后端必须要有相应的验证机制。这样才能确保系统的数据安全和业务稳定。
本文内容仅供个人学习/研究/参考使用,不构成任何决策建议或专业指导。分享/转载时请标明原文来源,同时请勿将内容用于商业售卖、虚假宣传等非学习用途哦~感谢您的理解与支持!
Math 对象用于执行数学任务。并不像 Date 和 String 那样是对象的类,日期对象定义:JS DATE使用UTC(国际协调时间)1970,1,1,0,0,0,0所经过的毫秒数。在JS中日期也是它的内置对象,所以我们要对日期进行获取和操作,必须实例化对象。
 
 js中Date对象常用方法,一、Date的构造函数,二、返回日期对应的毫秒数,三,获取当前时间对应的毫秒数,四、常见的Date方法...
js中使用new Date()来获取当前设备的时间,修改当前设备的时间; 0时区时间比当前时间慢的分钟数 除以60就是当前时区 负数表示比0时区时间快(东时区);创建指定日期的时间
今天遇到一个this.$refs[formName].validate((valid) =>{} 无效的问题,当验证通过的时候点确定按钮没有报错,也没有任何反应。
大家平时在开发的时候有没被new Date()折磨过?就是它的诸多怪异的设定让你每每用的时候,都可能不小心踩坑。造成程序意外出错,却一下子找不到问题出处,那叫一个烦透了
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!