页面加载中
页面加载中
防抖、loading状态、订单ID幂等性、乐观更新
前端防护
▾代码块TypeScript自动换行123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899class OrderSubmissionGuard { constructor() { this.pendingOrders = new Set(); this.recentOrders = new Map(); // clientOrderId -> timestamp this.deduplicateWindow = 5000; // 5秒去重窗口 } // 生成唯一订单ID generateClientOrderId() { const timestamp = Date.now(); const random = Math.random().toString(36).substring(2, 15); return \\${timestamp}-\${random}\; } // 检查是否可以提交 canSubmit(orderData) { const signature = this.getOrderSignature(orderData); // 检查是否有相同订单正在提交 if (this.pendingOrders.has(signature)) { return { allowed: false, reason: '订单正在提交中' }; } // 检查最近是否提交过相同订单 const lastSubmitTime = this.recentOrders.get(signature); if (lastSubmitTime && Date.now() - lastSubmitTime < this.deduplicateWindow) { return { allowed: false, reason: '请勿重复提交相同订单' }; } return { allowed: true }; } // 订单签名(用于去重) getOrderSignature(orderData) { const { symbol, side, type, price, quantity } = orderData; return \\${symbol}|\${side}|\${type}|\${price}|\${quantity}\; } // 标记订单开始提交 markSubmitting(orderData) { const signature = this.getOrderSignature(orderData); this.pendingOrders.add(signature); return signature; } // 标记订单提交完成 markCompleted(signature) { this.pendingOrders.delete(signature); this.recentOrders.set(signature, Date.now()); // 清理过期记录 this.cleanupRecentOrders(); } cleanupRecentOrders() { const now = Date.now(); for (const [sig, timestamp] of this.recentOrders.entries()) { if (now - timestamp > this.deduplicateWindow) { this.recentOrders.delete(sig); } } } // 提交订单(带防护) async submitOrder(orderData) { // 1. 检查是否可以提交 const check = this.canSubmit(orderData); if (!check.allowed) { throw new Error(check.reason); } // 2. 生成客户端订单ID const clientOrderId = this.generateClientOrderId(); const fullOrderData = { ...orderData, clientOrderId }; // 3. 标记为提交中 const signature = this.markSubmitting(orderData); try { // 4. 发送请求 const response = await fetch('/api/orders', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Request-ID': clientOrderId // 用于链路追踪 }, body: JSON.stringify(fullOrderData) }); if (!response.ok) { const error = await response.json(); throw new Error(error.message); } const result = await response.json(); return result; } finally { // 5. 无论成功失败都标记为完成 this.markCompleted(signature); } } }
后端幂等性保障
▾代码块TypeScript自动换行123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778// 服务端实现 class OrderService { constructor() { this.redis = new Redis(); this.orderIdTTL = 60 * 60; // 1小时 } async createOrder(userId, orderData) { const { clientOrderId } = orderData; // 1. 检查clientOrderId是否已处理过 const cacheKey = \order:client:\${userId}:\${clientOrderId}\; const cachedResult = await this.redis.get(cacheKey); if (cachedResult) { console.log('幂等性拦截:返回缓存结果'); return JSON.parse(cachedResult); } // 2. 使用分布式锁防止并发 const lockKey = \lock:order:\${userId}:\${clientOrderId}\; const lock = await this.redis.set(lockKey, '1', 'EX', 10, 'NX'); if (!lock) { throw new Error('订单正在处理中,请稍后'); } try { // 3. 创建订单 const order = await this.db.transaction(async (trx) => { // 检查余额 const balance = await trx('balances') .where({ userId, asset: orderData.asset }) .forUpdate() .first(); if (balance.available < orderData.requiredAmount) { throw new Error('余额不足'); } // 冻结资产 await trx('balances') .where({ userId, asset: orderData.asset }) .decrement('available', orderData.requiredAmount) .increment('frozen', orderData.requiredAmount); // 插入订单 const [orderId] = await trx('orders').insert({ userId, clientOrderId, symbol: orderData.symbol, side: orderData.side, type: orderData.type, price: orderData.price, quantity: orderData.quantity, status: 'pending', createdAt: new Date() }); return await trx('orders').where({ id: orderId }).first(); }); // 4. 缓存结果 await this.redis.setex( cacheKey, this.orderIdTTL, JSON.stringify(order) ); // 5. 发送到撮合引擎 await this.matchingEngine.submitOrder(order); return order; } finally { // 6. 释放锁 await this.redis.del(lockKey); } } }
UI层面的优化
▾代码块React TSX自动换行12345678910111213141516171819202122232425262728293031323334353637383940414243// 按钮防抖 + 禁用 function SubmitButton({ onSubmit, isSubmitting, disabled }) { const [localLoading, setLocalLoading] = useState(false); const lastClickTime = useRef(0); const handleClick = async () => { const now = Date.now(); // 500ms内的点击忽略 if (now - lastClickTime.current < 500) { console.log('忽略重复点击'); return; } lastClickTime.current = now; setLocalLoading(true); try { await onSubmit(); } catch (error) { console.error(error); } finally { setLocalLoading(false); } }; const loading = isSubmitting || localLoading; return ( <button onClick={handleClick} disabled={disabled || loading} className="submit-button" > {loading ? ( <> <Spinner /> <span>提交中...</span> </> ) : ( '提交订单' )} </button> ); }
最佳实践总结