You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React+Node.js支付成功后,如何恢复购票数据并存入Booking表?

解决方案推荐

针对你遇到的支付跳转后丢失观众数据的问题,这里提供几个比临时表更优的实现方案:

方案1:用Redis存储临时订单数据

利用Redis的自动过期特性替代定时任务,既高效又无需手动维护清理逻辑。

实现逻辑:

  1. 用户提交购票表单后,生成唯一orderNumber,将event_id、spectators等数据存入Redis,并设置10分钟过期时间(与支付超时窗口一致)。
  2. 调用第三方支付API,携带orderNumber和跳转地址。
  3. 支付完成跳转后,通过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表中扩展状态字段,先写入待支付的临时订单,支付成功后更新状态,过期订单通过定时任务清理。

实现逻辑:

  1. 在Booking表中添加status(可选值:pending/paid/expired)和expire_at(过期时间)字段。
  2. 用户提交表单时,开启事务将spectators数据批量插入Booking表,状态设为pending,过期时间设为当前时间+10分钟。
  3. 调用第三方支付API,支付完成后将订单状态更新为paid;支付失败或过期则标记为expired。
  4. 用定时任务定期清理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长度有限制,观众数据过多时不适用。

核心逻辑:

  1. 对观众数据进行对称加密,编码后拼接到returnUrl中。
  2. 支付完成跳转后,从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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.25 18:36:25