ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

大神接单订单防重复下单方案分享

大神接单订单防重复下单方案分享

大神接单订单防重复下单方案分享

在电竞大神接单小程序场景里,用户连续点击提交、网络抖动、网关重试、支付回调重复推送,都可能触发重复下单,产生多笔同需求订单,进而引发重复扣费、多派单、佣金结算错乱、售后纠纷等问题。很多项目只依靠前端按钮置灰做拦截,很容易被绕过。本文分享一套前端拦截+Redis令牌校验+数据库兜底的多层防重复下单方案,保障下单接口幂等。

后端基于SpringBoot + MyBatis-Plus开发,Redis存储幂等令牌,使用Lua脚本实现原子校验与删除,MySQL订单表增加唯一索引兜底。下单链路分为预生成令牌、提交校验、创建订单三层校验,层层拦截重复请求。

整体方案分为四层防护:

  1. 前端防抖:提交后按钮置灰,短时间禁止再次点击,作为第一层软拦截;
  2. 幂等Token校验:进入下单确认页时获取唯一令牌,提交下单携带令牌,Redis原子校验消费;
  3. 业务维度锁:同一用户、同一大神、同一服务在短时间内加分布式锁,防止并发重复提交;
  4. 数据库唯一索引兜底:订单请求号设置唯一约束,拦截穿透上层校验的重复写入。

业务流程:用户打开下单确认页 → 请求接口生成幂等Token存入Redis → 前端提交下单,携带Token和业务参数 → 后端执行Lua脚本校验Token,校验通过直接删除Token → 校验成功后执行业务创建订单;校验失败直接返回重复提交提示。

下面是订单实体和幂等工具核心代码示例:

@Data @TableName("t_player_order") public class PlayerOrder { private Long id; // 业务请求幂等号 private String requestNo; private String orderNo; private Long userId; private Long playerId; private String gameCode; private Long serviceId; private BigDecimal orderAmount; private Integer orderStatus; private LocalDateTime createTime; } @Component public class IdempotentTokenService { @Autowired private StringRedisTemplate redisTemplate; private static final String TOKEN_PREFIX = "idempotent:order:token:"; private static final long EXPIRE_SECONDS = 300; /** * 生成幂等Token */ public String generateToken() { String token = UUID.randomUUID().toString().replace("-", ""); redisTemplate.opsForValue().set(TOKEN_PREFIX + token, "1", EXPIRE_SECONDS, TimeUnit.SECONDS); return token; } /** * Lua脚本原子校验并删除token */ public boolean checkToken(String token) { String key = TOKEN_PREFIX + token; String script = "if redis.call('GET',KEYS[1]) == ARGV[1] then return redis.call('DEL',KEYS[1]) else return 0 end"; DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class); Long result = redisTemplate.execute(redisScript, Collections.singletonList(key), "1"); return result != null && result > 0; } }

下单接口层先调用checkToken校验令牌,校验不通过直接返回“请勿重复提交订单”。令牌校验通过之后,再尝试获取业务分布式锁,锁key使用lock:order:user:userId:playerId:serviceId,锁定时长控制在几秒,用来挡住同一用户对同一个大神服务的并发请求。

@Service public class OrderCreateService { @Autowired private IdempotentTokenService idempotentTokenService; @Autowired private StringRedisTemplate redisTemplate; public void createOrder(OrderCreateDTO dto) { boolean pass = idempotentTokenService.checkToken(dto.getToken()); if (!pass) { throw new RuntimeException("请勿重复提交订单"); } String lockKey = "lock:order:" + dto.getUserId() + ":" + dto.getPlayerId() + ":" + dto.getServiceId(); Boolean lock = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 5, TimeUnit.SECONDS); if (Boolean.FALSE.equals(lock)) { throw new RuntimeException("订单提交中,请勿频繁操作"); } try { // 组装订单、入库逻辑 } finally { redisTemplate.delete(lockKey); } } }

数据库兜底机制是最后一道防线。订单表的request_no字段建立唯一索引,就算Redis异常、令牌校验失效,重复插入相同requestNo时,数据库抛出唯一键冲突异常,代码捕获异常后查询已有订单返回,不会生成多条重复订单。requestNo可以由前端携带或者后端随Token一起生成,一单对应唯一请求号。

支付回调场景同样需要幂等处理。支付渠道会存在多次回调通知,不能重复更新订单状态、重复发放权益。以业务订单号作为幂等key,回调进来先查询订单状态,如果已经是已支付,直接返回成功,不再执行业务逻辑。

落地时需要注意几个细节:Token设置合理过期时间,下单超时未提交自动失效,用户重新进入页面会获取新Token;分布式锁不能设置过长时间,防止业务异常锁死;Redis做好降级预案,如果Redis服务不可用,依赖数据库唯一索引兜底。

风控层面增加监控,短时间内多次触发重复提交拦截的用户,后台进行预警,识别脚本刷单、恶意请求行为。所有重复拦截记录写入日志,方便问题排查,区分是用户操作问题还是接口重试导致。

这套方案的核心思路是前端减负、Redis原子校验防并发、数据库兜底防数据错乱。三层防护互相配合,既满足正常用户操作体验,又能挡住网络重试、恶意刷请求带来的重复下单问题。后续还可以扩展全局幂等注解,统一接入所有写类型接口。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进