React游戏购卡包功能:Express请求3-4次后停止响应
问题:React游戏购买卡包后Express服务器多次操作无响应
项目背景
- 开发React游戏,玩家使用
essences货币购买三种卡包:mini(500)、normal(1500)、mega(3000) - 每个卡包按钮绑定
BuyPack函数,传入对应价格参数
前端BuyPack函数代码
const BuyPack = (arg) => { let nb = (document.getElementById('essences').innerText) // that obviously sucks lol let nbInt = parseInt(nb) // converts innerText to int const newSolde = (nbInt - arg) // The calculated amount after the purchase // Update visually the amount of essences localStorage.setItem('essences', newSolde) document.getElementById('essences').innerHTML = newSolde; // Let's set the new amount let datas = {id: localStorage.getItem('id'), typeOfPack: arg}; // id = the user id / arg = 500, 1500 or 3000, it's basically the pack price. console.log('datas', datas) // Output : datas {id: '196', typeOfPack: 500} Axios.post('http://localhost:3001/buyPack', {data: datas}) // The axios call that doesn't work as expected }
后端Express接口代码
app.post('/buyPack', function (req, res) { const userId = (req.body.data.id) const typeOfPack = (req.body.data.typeOfPack) console.log(typeOfPack) // I make a first request to get the "pre-buy" amount of money query("select essences FROM users WHERE id = ?", [userId], (err, rows) => { const actualEssences = (rows[0].essences); // The amount we just got // Second query where we update and decrease the amount of money for the user query("update users set essences = " + (actualEssences - typeOfPack) + " where id = " + userId) }) })
问题现象
- 执行3-4次购买操作后,Express服务器停止响应
- 客户端视觉上货币数值减少正常,点击日志能打印,但后端无后续请求日志输出
问题分析与修复方案
1. 后端未处理请求响应与数据库错误
原后端接口没有向客户端返回任何响应,也未处理数据库查询的异常,导致客户端请求长时间挂起,服务器连接池被耗尽,最终无法接收新请求。
修改后端代码:
app.post('/buyPack', function (req, res) { const userId = req.body.data.id; const typeOfPack = req.body.data.typeOfPack; console.log(typeOfPack); query("select essences FROM users WHERE id = ?", [userId], (err, rows) => { // 处理查询错误 if (err) { console.error('查询用户余额失败:', err); return res.status(500).json({success: false, message: '查询失败'}); } // 处理用户不存在的情况 if (!rows || rows.length === 0) { return res.status(404).json({success: false, message: '用户不存在'}); } const actualEssences = rows[0].essences; // 使用参数化查询避免SQL注入,同时处理更新错误 query("update users set essences = ? where id = ?", [actualEssences - typeOfPack, userId], (updateErr) => { if (updateErr) { console.error('更新用户余额失败:', updateErr); return res.status(500).json({success: false, message: '购买失败'}); } // 返回成功响应给客户端 res.status(200).json({success: true, newEssences: actualEssences - typeOfPack}); }); }); });
2. 前端先本地计算余额存在数据不一致风险
原前端先通过DOM获取余额并计算,再请求后端,容易出现本地数据与后端数据不一致的情况,且DOM解析可能出错。应改为依赖后端返回的正确余额更新本地状态。
修改前端代码:
const BuyPack = async (arg) => { const userId = localStorage.getItem('id'); if (!userId) { console.error('用户未登录'); return; } try { const response = await Axios.post('http://localhost:3001/buyPack', {data: {id: userId, typeOfPack: arg}}); if (response.data.success) { const newSolde = response.data.newEssences; localStorage.setItem('essences', newSolde); document.getElementById('essences').innerHTML = newSolde; } else { // 购买失败时恢复本地显示的余额 const currentEssences = localStorage.getItem('essences'); document.getElementById('essences').innerHTML = currentEssences; console.error('购买失败:', response.data.message); } } catch (err) { console.error('请求失败:', err); // 请求异常时恢复本地余额 const currentEssences = localStorage.getItem('essences'); document.getElementById('essences').innerHTML = currentEssences; } }
3. 解决竞态条件与SQL注入风险
原后端先查询再更新的逻辑存在竞态条件(多次快速请求时,查询的余额可能已经过期),且字符串拼接SQL语句存在注入风险。改为原子更新语句,确保操作的安全性与一致性:
app.post('/buyPack', function (req, res) { const userId = req.body.data.id; const typeOfPack = req.body.data.typeOfPack; console.log(typeOfPack); // 原子更新:同时检查余额是否足够,避免竞态条件 query("UPDATE users SET essences = essences - ? WHERE id = ? AND essences >= ?", [typeOfPack, userId, typeOfPack], (err, result) => { if (err) { console.error('更新失败:', err); return res.status(500).json({success: false, message: '购买失败'}); } // 没有更新行说明余额不足或用户不存在 if (result.affectedRows === 0) { return res.status(400).json({success: false, message: '余额不足或用户不存在'}); } // 返回更新后的余额 query("SELECT essences FROM users WHERE id = ?", [userId], (selectErr, rows) => { if (selectErr) { console.error('查询新余额失败:', selectErr); return res.status(200).json({success: true}); } res.status(200).json({success: true, newEssences: rows[0].essences}); }); }); });
内容的提问来源于stack exchange,提问作者skeenotihk
相关产品推荐
相关产品推荐

