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

在Promise.all中批量创建Invoice时如何实现编号递增?

解决批量创建Invoice时编号重复的问题

问题核心在于Promise.all的并发特性:所有创建Invoice的请求同时发起,会同时读取数据库中同一个"最后发票编号",后续的number++只是内存中的操作,无法同步到并发的其他请求,最终导致所有发票使用相同编号。

以下是几种可行的解决方法:

1. 依赖数据库自增机制(最推荐)

直接利用数据库的自增特性生成唯一编号,避免手动维护编号的并发问题。如果需要带格式的编号(如INV-2024-0001),可以通过模型钩子结合自增ID生成:

示例代码:

// Invoice 模型定义
const Invoice = sequelize.define('Invoice', {
  id: {
    type: DataTypes.INTEGER,
    autoIncrement: true,
    primaryKey: true
  },
  invoiceNumber: {
    type: DataTypes.STRING,
    allowNull: false,
    unique: true
  },
  // 其他字段...
}, {
  hooks: {
    afterCreate: async (invoice) => {
      // 生成带格式的编号,基于自增ID
      const year = new Date().getFullYear();
      const formattedNumber = `INV-${year}-${String(invoice.id).padStart(4, '0')}`;
      await invoice.update({ invoiceNumber: formattedNumber });
    }
  }
});

这种方式完全由数据库保证编号的唯一性和递增性,无需手动处理并发。

2. 串行创建而非并发(适合小批量场景)

放弃Promise.all的并发执行,改用串行循环逐个创建Invoice,确保每次创建前都能获取到最新的编号:

示例代码:

async function batchCreateInvoices(invoiceDataList) {
  const transaction = await sequelize.transaction();
  try {
    for (const data of invoiceDataList) {
      // 获取当前最新发票编号
      const lastInvoice = await Invoice.findOne({
        order: [['invoiceNumber', 'DESC']],
        transaction
      });
      let nextNumber = 1;
      if (lastInvoice) {
        // 解析编号中的数字部分并加1(假设编号格式为 INV-2024-0001)
        const numPart = parseInt(lastInvoice.invoiceNumber.split('-')[2], 10);
        nextNumber = numPart + 1;
      }
      const formattedNumber = `INV-${new Date().getFullYear()}-${String(nextNumber).padStart(4, '0')}`;
      // 创建发票
      await Invoice.create({
        ...data,
        invoiceNumber: formattedNumber
      }, { transaction });
      // 同步更新Packages、发送邮件
      await updatePackage(data.packageId, { status: 'invoiced' }, transaction);
      await sendInvoiceEmail(formattedNumber, data.email);
    }
    await transaction.commit();
  } catch (err) {
    await transaction.rollback();
    throw err;
  }
}

这种方式虽然牺牲了一点并发性能,但彻底避免了编号重复问题,适合批量不大的场景。

3. 事务内加锁保证原子性

如果必须保留并发创建(或批量较大),可以在获取最新编号时加数据库锁,确保同一时间只有一个请求能读取并更新编号:

示例代码:

async function createInvoiceWithLock(data, transaction) {
  // 加锁获取最新发票,阻止其他事务读取
  const lastInvoice = await Invoice.findOne({
    order: [['invoiceNumber', 'DESC']],
    lock: true, // 行锁,不同数据库语法可能略有差异
    transaction
  });
  // 生成下一个编号逻辑同前
  let nextNumber = 1;
  if (lastInvoice) {
    const numPart = parseInt(lastInvoice.invoiceNumber.split('-')[2], 10);
    nextNumber = numPart + 1;
  }
  const formattedNumber = `INV-${new Date().getFullYear()}-${String(nextNumber).padStart(4, '0')}`;
  return await Invoice.create({
    ...data,
    invoiceNumber: formattedNumber
  }, { transaction });
}

// 批量创建时仍用Promise.all,但每个创建操作都在事务内加锁
async function batchCreateInvoices(invoiceDataList) {
  const transaction = await sequelize.transaction();
  try {
    const promises = invoiceDataList.map(data => 
      createInvoiceWithLock(data, transaction).then(async (invoice) => {
        await updatePackage(data.packageId, { status: 'invoiced' }, transaction);
        await sendInvoiceEmail(invoice.invoiceNumber, data.email);
      })
    );
    await Promise.all(promises);
    await transaction.commit();
  } catch (err) {
    await transaction.rollback();
    throw err;
  }
}

注意:数据库锁可能会影响性能,需根据业务场景评估。


内容的提问来源于stack exchange,提问作者NaguiHW

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 07:27:03