React+Node.js支付成功后,如何恢复购票数据并存入Booking表?
解决方案推荐
针对你遇到的支付跳转后丢失观众数据的问题,这里提供几个比临时表更优的实现方案:
方案1:用Redis存储临时订单数据
利用Redis的自动过期特性替代定时任务,既高效又无需手动维护清理逻辑。
实现逻辑:
- 用户提交购票表单后,生成唯一
orderNumber,将event_id、spectators等数据存入Redis,并设置10分钟过期时间(与支付超时窗口一致)。 - 调用第三方支付API,携带
orderNumber和跳转地址。 - 支付完成跳转后,通过
orderNumber从Redis取出临时数据,写入正式Booking表,随后删除Redis中的临时记录。
代码示例(Node.js/Express):
const Redis = require('ioredis'); const redis = new Redis(); // 提交购票接口 app.post('/api/booking', async (req, res) => { const { event_id, spectators } = req.body; const orderNumber = `ORDER_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`; const totalPrice = spectators.reduce((sum, item) => sum + item.price, 0); // 存入Redis,10分钟后自动过期 await redis.setex( `temp_booking:${orderNumber}`, 600, // 10分钟 = 600秒 JSON.stringify({ event_id, spectators }) ); // 调用第三方支付API const paymentResp = await fetch(`api-e-paiment/?price=${totalPrice}&orderNumber=${orderNumber}&usernameApi=xxx&passwordApi=xxx&returnUrl=website/checkout`); const { errCode, formUrl } = await paymentResp.json(); if (errCode === 0) { return res.json({ formUrl }); } // 支付接口调用失败,清理临时数据 await redis.del(`temp_booking:${orderNumber}`); res.status(400).json({ error: '支付请求发起失败' }); }); // 支付结果查询接口 app.get('/api/checkout', async (req, res) => { const { orderNumber } = req.query; // 查询第三方支付状态 const statusResp = await fetch(`api-e-paiment/${orderNumber}`); const paymentStatus = await statusResp.json(); if (!paymentStatus.success) { // 支付失败,清理临时数据 await redis.del(`temp_booking:${orderNumber}`); return res.redirect('/website/payment-fail'); } // 从Redis取出临时数据 const tempData = await redis.get(`temp_booking:${orderNumber}`); if (!tempData) { return res.status(400).json({ error: '订单已过期或不存在' }); } const { event_id, spectators } = JSON.parse(tempData); // 写入正式Booking表 await Booking.bulkCreate( spectators.map(item => ({ event_id, ticket_id: item.ticket_id, price: item.price, first_name: item.first_name, order_number: orderNumber })) ); // 清理Redis临时数据 await redis.del(`temp_booking:${orderNumber}`); res.redirect('/website/payment-success'); });
优缺点:
- ✅ 无需手动编写定时清理任务,Redis自动处理过期数据
- ✅ 读写性能远优于数据库,不占用主库资源
- ❌ 需要额外部署维护Redis服务
方案2:预创建订单到正式Booking表(增加状态字段)
直接在现有Booking表中扩展状态字段,先写入待支付的临时订单,支付成功后更新状态,过期订单通过定时任务清理。
实现逻辑:
- 在
Booking表中添加status(可选值:pending/paid/expired)和expire_at(过期时间)字段。 - 用户提交表单时,开启事务将
spectators数据批量插入Booking表,状态设为pending,过期时间设为当前时间+10分钟。 - 调用第三方支付API,支付完成后将订单状态更新为
paid;支付失败或过期则标记为expired。 - 用定时任务定期清理
pending且已过期的订单。
代码示例:
// 提交购票接口 app.post('/api/booking', async (req, res) => { const { event_id, spectators } = req.body; const orderNumber = `ORDER_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`; const expireAt = new Date(Date.now() + 600000); // 10分钟后 const transaction = await sequelize.transaction(); try { // 批量插入待支付订单 await Booking.bulkCreate( spectators.map(item => ({ event_id, ticket_id: item.ticket_id, price: item.price, first_name: item.first_name, order_number: orderNumber, status: 'pending', expire_at: expireAt })), { transaction } ); // 调用第三方支付API const totalPrice = spectators.reduce((sum, item) => sum + item.price, 0); const paymentResp = await fetch(`api-e-paiment/?price=${totalPrice}&orderNumber=${orderNumber}&usernameApi=xxx&passwordApi=xxx&returnUrl=website/checkout`); const { errCode, formUrl } = await paymentResp.json(); if (errCode === 0) { await transaction.commit(); return res.json({ formUrl }); } // 支付接口调用失败,回滚事务 await transaction.rollback(); res.status(400).json({ error: '支付请求发起失败' }); } catch (err) { await transaction.rollback(); res.status(500).json({ error: '服务器内部错误' }); } }); // 支付结果查询接口 app.get('/api/checkout', async (req, res) => { const { orderNumber } = req.query; const statusResp = await fetch(`api-e-paiment/${orderNumber}`); const paymentStatus = await statusResp.json(); if (paymentStatus.success) { // 更新订单为已支付 await Booking.update( { status: 'paid' }, { where: { order_number: orderNumber } } ); return res.redirect('/website/payment-success'); } // 支付失败,标记为过期 await Booking.update( { status: 'expired' }, { where: { order_number: orderNumber } } ); res.redirect('/website/payment-fail'); }); // 定时清理过期的待支付订单(每10分钟执行一次) setInterval(async () => { await Booking.destroy({ where: { status: 'pending', expire_at: { [Op.lt]: new Date() } } }); }, 600000);
优缺点:
- ✅ 无需额外依赖服务,与现有业务表逻辑统一
- ✅ 事务保证数据一致性
- ❌ 需要维护定时清理任务,主库会存在临时数据(可通过索引优化查询)
方案3:加密数据附加到跳转URL(仅适合小数据量场景)
将event_id和spectators加密后作为参数附加到returnUrl,支付完成后解密获取数据。注意:URL长度有限制,观众数据过多时不适用。
核心逻辑:
- 对观众数据进行对称加密,编码后拼接到
returnUrl中。 - 支付完成跳转后,从URL参数中取出加密数据并解密,写入
Booking表。
代码示例(简化版):
const crypto = require('crypto'); const secretKey = 'your-encryption-secret'; // 加密函数 function encrypt(data) { const cipher = crypto.createCipher('aes192', secretKey); let encrypted = cipher.update(JSON.stringify(data), 'utf8', 'base64'); encrypted += cipher.final('base64'); return encrypted; } // 解密函数 function decrypt(encryptedStr) { const decipher = crypto.createDecipher('aes192', secretKey); let decrypted = decipher.update(encryptedStr, 'base64', 'utf8'); decrypted += decipher.final('utf8'); return JSON.parse(decrypted); } // 提交购票接口 app.post('/api/booking', async (req, res) => { const { event_id, spectators } = req.body; const orderNumber = `ORDER_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`; // 加密数据 const encryptedData = encrypt({ event_id, spectators }); const returnUrl = `website/checkout?orderNumber=${orderNumber}&data=${encodeURIComponent(encryptedData)}`; // 调用第三方支付API... }); // 支付结果查询接口 app.get('/api/checkout', async (req, res) => { const { orderNumber, data } = req.query; const { event_id, spectators } = decrypt(data); // 查询支付状态并写入Booking表... });
优缺点:
- ✅ 无需额外存储服务
- ❌ URL长度限制,不适合多观众场景
- ❌ 需维护加密密钥,存在数据泄露风险(需HTTPS配合)
方案选择建议
- 如果团队已部署Redis,优先选方案1,性能和维护成本最优;
- 若不想额外维护Redis,选方案2,逻辑更贴合现有业务;
- 仅当观众数据极少时,才考虑方案3。
内容的提问来源于stack exchange,提问作者Js lover
相关产品推荐
相关产品推荐

