MetaNode社区
找工作面试题库领SepoliaETH

© 2025 MetaNode社区. All rights reserved.

Powered by MetaNode

VIP

尊享永久会员

解锁所有面试题解,一次性买断

当前等级普通用户
限时优惠
¥129¥399

/永久

✓解锁全部企业高频面试题及高质量题解
✓参与模拟面试,获取百套模拟面试视频
✓加入永久会员交流群,专属答疑

点击按钮联系客服获取兑换码

扫码添加老师微信

获取兑换码 · 干货不错过

微信二维码
Logo

关注我们

B站抖音小红书
一笔交易怎么确定需要多少 Gas?如何防止用户重复提交订单?ethers 模拟调用和正常调用有什么区别?
返回题库

如何防止用户重复提交订单?

中等
00

核心考察点

防抖、loading状态、订单ID幂等性、乐观更新

完整解决方案

前端防护

TypeScript
1class OrderSubmissionGuard { 2 constructor() { 3 this.pendingOrders = new Set(); 4 this.recentOrders = new Map(); // clientOrderId -> timestamp 5 this.deduplicateWindow = 5000; // 5秒去重窗口 6 } 7 8 // 生成唯一订单ID 9 generateClientOrderId() { 10 const timestamp = Date.now(); 11 const random = Math.random().toString(36).substring(2, 15); 12 return \\${timestamp}-\${random}\; 13 } 14 15 // 检查是否可以提交 16 canSubmit(orderData) { 17 const signature = this.getOrderSignature(orderData); 18 // 检查是否有相同订单正在提交 19 if (this.pendingOrders.has(signature)) { 20 return { allowed: false, reason: '订单正在提交中' }; 21 } 22 // 检查最近是否提交过相同订单 23 const lastSubmitTime = this.recentOrders.get(signature); 24 if (lastSubmitTime && Date.now() - lastSubmitTime < this.deduplicateWindow) { 25 return { allowed: false, reason: '请勿重复提交相同订单' }; 26 } 27 return { allowed: true }; 28 } 29 30 // 订单签名(用于去重) 31 getOrderSignature(orderData) { 32 const { symbol, side, type, price, quantity } = orderData; 33 return \\${symbol}|\${side}|\${type}|\${price}|\${quantity}\; 34 } 35 36 // 标记订单开始提交 37 markSubmitting(orderData) { 38 const signature = this.getOrderSignature(orderData); 39 this.pendingOrders.add(signature); 40 return signature; 41 } 42 43 // 标记订单提交完成 44 markCompleted(signature) { 45 this.pendingOrders.delete(signature); 46 this.recentOrders.set(signature, Date.now()); 47 // 清理过期记录 48 this.cleanupRecentOrders(); 49 } 50 51 cleanupRecentOrders() { 52 const now = Date.now(); 53 for (const [sig, timestamp] of this.recentOrders.entries()) { 54 if (now - timestamp > this.deduplicateWindow) { 55 this.recentOrders.delete(sig); 56 } 57 } 58 } 59 60 // 提交订单(带防护) 61 async submitOrder(orderData) { 62 // 1. 检查是否可以提交 63 const check = this.canSubmit(orderData); 64 if (!check.allowed) { 65 throw new Error(check.reason); 66 } 67 68 // 2. 生成客户端订单ID 69 const clientOrderId = this.generateClientOrderId(); 70 const fullOrderData = { ...orderData, clientOrderId }; 71 72 // 3. 标记为提交中 73 const signature = this.markSubmitting(orderData); 74 75 try { 76 // 4. 发送请求 77 const response = await fetch('/api/orders', { 78 method: 'POST', 79 headers: { 80 'Content-Type': 'application/json', 81 'X-Request-ID': clientOrderId // 用于链路追踪 82 }, 83 body: JSON.stringify(fullOrderData) 84 }); 85 86 if (!response.ok) { 87 const error = await response.json(); 88 throw new Error(error.message); 89 } 90 91 const result = await response.json(); 92 return result; 93 94 } finally { 95 // 5. 无论成功失败都标记为完成 96 this.markCompleted(signature); 97 } 98 } 99}

后端幂等性保障

TypeScript
1// 服务端实现 2class OrderService { 3 constructor() { 4 this.redis = new Redis(); 5 this.orderIdTTL = 60 * 60; // 1小时 6 } 7 8 async createOrder(userId, orderData) { 9 const { clientOrderId } = orderData; 10 11 // 1. 检查clientOrderId是否已处理过 12 const cacheKey = \order:client:\${userId}:\${clientOrderId}\; 13 const cachedResult = await this.redis.get(cacheKey); 14 if (cachedResult) { 15 console.log('幂等性拦截:返回缓存结果'); 16 return JSON.parse(cachedResult); 17 } 18 19 // 2. 使用分布式锁防止并发 20 const lockKey = \lock:order:\${userId}:\${clientOrderId}\; 21 const lock = await this.redis.set(lockKey, '1', 'EX', 10, 'NX'); 22 if (!lock) { 23 throw new Error('订单正在处理中,请稍后'); 24 } 25 26 try { 27 // 3. 创建订单 28 const order = await this.db.transaction(async (trx) => { 29 // 检查余额 30 const balance = await trx('balances') 31 .where({ userId, asset: orderData.asset }) 32 .forUpdate() 33 .first(); 34 35 if (balance.available < orderData.requiredAmount) { 36 throw new Error('余额不足'); 37 } 38 39 // 冻结资产 40 await trx('balances') 41 .where({ userId, asset: orderData.asset }) 42 .decrement('available', orderData.requiredAmount) 43 .increment('frozen', orderData.requiredAmount); 44 45 // 插入订单 46 const [orderId] = await trx('orders').insert({ 47 userId, 48 clientOrderId, 49 symbol: orderData.symbol, 50 side: orderData.side, 51 type: orderData.type, 52 price: orderData.price, 53 quantity: orderData.quantity, 54 status: 'pending', 55 createdAt: new Date() 56 }); 57 58 return await trx('orders').where({ id: orderId }).first(); 59 }); 60 61 // 4. 缓存结果 62 await this.redis.setex( 63 cacheKey, 64 this.orderIdTTL, 65 JSON.stringify(order) 66 ); 67 68 // 5. 发送到撮合引擎 69 await this.matchingEngine.submitOrder(order); 70 71 return order; 72 73 } finally { 74 // 6. 释放锁 75 await this.redis.del(lockKey); 76 } 77 } 78}

UI层面的优化

React TSX
1// 按钮防抖 + 禁用 2function SubmitButton({ onSubmit, isSubmitting, disabled }) { 3 const [localLoading, setLocalLoading] = useState(false); 4 const lastClickTime = useRef(0); 5 6 const handleClick = async () => { 7 const now = Date.now(); 8 // 500ms内的点击忽略 9 if (now - lastClickTime.current < 500) { 10 console.log('忽略重复点击'); 11 return; 12 } 13 lastClickTime.current = now; 14 setLocalLoading(true); 15 16 try { 17 await onSubmit(); 18 } catch (error) { 19 console.error(error); 20 } finally { 21 setLocalLoading(false); 22 } 23 }; 24 25 const loading = isSubmitting || localLoading; 26 27 return ( 28 <button 29 onClick={handleClick} 30 disabled={disabled || loading} 31 className="submit-button" 32 > 33 {loading ? ( 34 <> 35 <Spinner /> 36 <span>提交中...</span> 37 </> 38 ) : ( 39 '提交订单' 40 )} 41 </button> 42 ); 43}

最佳实践总结

  • 前端:防抖 + loading状态
  • 前端:订单签名去重
  • 客户端生成唯一订单ID
  • 后端:clientOrderId幂等性
  • 后端:分布式锁防并发
  • 后端:结果缓存
  • UI:禁用按钮 + 加载动画
  • 网络层:请求超时和重试机制