Как реализовать логику обработки POST-запроса покупки товара так, чтобы при нехватке хотя бы одного товара вся покупка отменялась?

В серверном файле Node.js в обработчике POST-запроса есть следующий фрагмент кода: <br/> <pre><code class="javascript">const count = {};
  • 10 июня 2026 г. 5:00
Ответы на вопрос 2
Коротко — нужно либо сделать все изменения в транзакции, либо выполнять атомарные условные обновления (и при неудаче откатывать уже сделанные изменения). Важно сначала сгруппировать позиции запроса по id (чтобы учесть несколько одинаковых позиций), затем проверять/декрементировать запасы атомарно. Ниже — несколько вариантов с примерами кода.

1) Рекомендация (если у вас БД поддерживает транзакции): выполнить все проверки и уменьшения в одной транзакции. Пример для MongoDB + Mongoose:

```javascript
// Express handler
app.post('/buy', async (req, res) => {
  const items = req.body.items; // [{ productId, qty }, ...]
  // сгруппировать по productId
  const counts = {};
  for (const it of items) counts[it.productId] = (counts[it.productId] || 0) + it.qty;

  const session = await mongoose.startSession();
  try {
    await session.withTransaction(async () => {
      // для каждой позиции попытаться снизить stock на qty,
      // условие в фильтре гарантирует, что не уйдем в минус
      for (const [productId, qty] of Object.entries(counts)) {
        const updated = await Product.findOneAndUpdate(
          { _id: productId, stock: { $gte: qty } },
          { $inc: { stock: -qty } },
          { session, new: true }
        );
        if (!updated) {
          // если хоть один товар недоступен — выбросим ошибку, транзакция отменится
          throw new Error(`Insufficient stock for product ${productId}`);
        }
      }

      // здесь можно создать запись заказа и т.п.
    });

    res.status(200).json({ ok: true });
  } catch (err) {
    await session.abortTransaction(); // на всякий случай
    res.status(409).json({ ok: false, message: err.message });
  } finally {
    session.endSession();
  }
});
```

Примечания:
- MongoDB транзакции работают только на реплика сете; если у вас standalone — транзакций нет.
- findOneAndUpdate с фильтром stock: {$gte: qty} делает проверку и декремент атомарно (в пределах транзакции — дополнительно защищает от конкурирующих транзакций).

2) Если транзакций нет (или хотите избежать), можно делать атомарные условные обновления по одному и при неудаче откатывать предыдущие изменения (ручной «rollback»). Этот подход сложнее и менее устойчив к сбоям, но иногда приемлем:

```javascript
app.post('/buy', async (req, res) => {
  const counts = /* сгруппировать как выше */;
  const updatedIds = [];

  try {
    for (const [productId, qty] of Object.entries(counts)) {
      // попробуем уменьшить атомарно; если фильтр не прошел — null
      const updated = await Product.findOneAndUpdate(
        { _id: productId, stock: { $gte: qty } },
        { $inc: { stock: -qty } },
        { new: true }
      );
      if (!updated) {
        // откатываем все, что уже уменьшили
        for (const u of updatedIds) {
          await Product.updateOne({ _id: u.id }, { $inc: { stock: u.qty } });
        }
        return res.status(409).json({ ok: false, message: 'Insufficient stock' });
      }
      updatedIds.push({ id: productId, qty });
    }

    // создать заказ
    res.json({ ok: true });
  } catch (err) {
    // попытаться откатить при ошибке
    for (const u of updatedIds) {
      try { await Product.updateOne({ _id: u.id }, { $inc: { stock: u.qty } }); } catch (e) {}
    }
    res.status(500).json({ ok: false });
  }
});
```

3) SQL (Postgres/MySQL): использовать транзакцию + SELECT ... FOR UPDATE или UPDATE с проверкой в транзакции:

Пример псевдокода с mysql2/await:

```javascript
const conn = await pool.getConnection();
try {
  await conn.beginTransaction();

  // получаем и блокируем строки
  for (const [id, qty] of Object.entries(counts)) {
    const [rows] = await conn.query('SELECT stock FROM products WHERE id = ? FOR UPDATE', [id]);
    if (!rows.length || rows[0].stock < qty) {
      await conn.rollback();
      return res.status(409).json({ ok: false, message: 'Insufficient stock' });
    }
  }

  // если всё ок, делаем update
  for (const [id, qty] of Object.entries(counts)) {
    await conn.query('UPDATE products SET stock = stock - ? WHERE id = ?', [qty, id]);
  }

  // создать запись заказа
  await conn.commit();
  res.json({ ok: true });
} catch (err) {
  await conn.rollback();
  res.status(500).json({ ok: false });
} finally {
  conn.release();
}
```

Рекомендации и замечания
- Всегда группируйте одинаковые productId (иначе можно перепродать при суммировании).
- Предпочтительны транзакции БД — они проще и надежнее для консистентности при параллельных запросах.
- Возвращайте 409 Conflict или 400 с понятным сообщением при нехватке товара.
- Логируйте и тестируйте конкурентные сценарии (несколько покупок одного товара одновременно).
- Если используете MongoDB и транзакции — ваша БД должна быть в replica set.

Если покажете, какую именно БД и ORM/драйвер используете (MongoDB/Mongoose, Postgres/knex, MySQL/mysql2 и т.д.), дам точный пример, подгоненный под ваш стек.
тут не условие сломано — forEach не ждёт async-колбэки. Нужен mysql2/promise.
Похожие вопросы