Розробка аукціону в реальному часі: сервер, клієнт, антиснайпінг

Втрачені ставки та спірні результати — знайома біль для власників аукціонів. Ми розробляємо аукціони в реальному часі, які виключають такі помилки завдяки синхронізації через WebSocket та захисту від антиснайпінгу. Наша команда реалізує проєкт під ключ — від серверної логіки до клієнтської частини, забезпечуючи стабільну роботу та подальшу підтримку.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Послуги, які ми пропонуємо
Показано 1 з 1Усі 2062 послуг
Розробка аукціону в реальному часі: сервер, клієнт, антиснайпінг
Середній
~5 днів
Часті запитання

На минулому тижні до нас прийшов клієнт: його аукціон «зависав», коли два користувачі одночасно ставили на один лот — одна ставка зникала, інша дублювалася. Втрати становили до $5000 за лот. Така ситуація знайома багатьом власникам аукціонів. Ми розробляємо аукціонні системи, які виключають подібні помилки «з коробки». За 7 років ми реалізували понад 30 проєктів для e-commerce та торгових майданчиків.

Проблеми, які вирішуємо

Race condition при паралельних ставках — найпоширеніша біда. Два запити обробляються одночасно, кожен бачить поточну ціну $100, обидва записують $110 — перемагає перший, другий має бути відхилений. Без блокування клієнт отримує хибне підтвердження, а аукціон — спірну ситуацію. Ми усуваємо це через песимістичне блокування Redis, виключаючи подвійні записи.

Хибне визначення переможця — проблема через несинхронізовані таймери. Сервер показує 5 секунд до кінця, але через затримку мережі клієнт вважає, що залишилося 10. Ставка йде вчасно, але сервер вже закрив лот. Втрати для організатора — тисячі доларів. Наш підхід — синхронізація часу через WebSocket із серверним таймером.

Антиснайпінг — коли учасник чекає останньої секунди і «краде» лот. Без механізму продовження аукціон втрачає чесність. Ми додаємо автоматичне продовження на 2 хвилини після кожної ставки у фінальній фазі.

Як ми це робимо: стек та ключові рішення

Обираємо Node.js (NestJS для строгої архітектури) та PostgreSQL — перевірена зв'язка для транзакційних навантажень. Песимістичне блокування через Redis SETNX — щоб жоден запит не проскочив без блокування. WebSocket (Socket.IO) — для миттєвих update всіх клієнтів; fallback на long-polling, якщо мережа блокує WebSocket. Фронтенд — React 18 з Next.js (SSR для SEO) та TypeScript, компоненти на Tailwind.

Порівняємо два підходи до блокування:

Метод Продуктивність Надійність Складність
Оптимістичне (retry) Висока, але повторні запити Низька при високій конкуренції Низька
Песимістичне (Redis lock) Середня, блокування на 5 с Висока, виключає race condition Середня

Ми використовуємо песимістичне блокування як більш надійне для аукціонів з високою конкуренцією.

Як запобігти конфлікту ставок?

Песимістичне блокування через Redis — найнадійніше. Алгоритм:

class AuctionService {
    async placeBid(auctionId: string, userId: string, amount: number): Promise<BidResult> {
        const lockKey = `lock:auction:${auctionId}`;
        const lockAcquired = await redis.set(lockKey, userId, 'NX', 'PX', 5000);
        if (!lockAcquired) {
            throw new Error('Auction is processing another bid, try again');
        }
        try {
            const auction = await auctionRepo.findById(auctionId);
            if (auction.status !== 'active') throw new BidError('Auction is not active');
            if (new Date() > auction.endsAt) throw new BidError('Auction has ended');
            if (amount <= auction.currentPrice) {
                throw new BidError(`Bid must be higher than ${auction.currentPrice}`);
            }
            if (amount < auction.currentPrice + auction.minIncrement) {
                throw new BidError(`Minimum increment is ${auction.minIncrement}`);
            }
            if (auction.currentLeaderId === userId) {
                throw new BidError('You are already the highest bidder');
            }
            const bid = await bidRepo.create({ auctionId, userId, amount });
            await auctionRepo.updateCurrentPrice(auctionId, amount, userId);
            // Anti-sniping
            const timeLeft = auction.endsAt.getTime() - Date.now();
            if (timeLeft < 2 * 60 * 1000) {
                const newEndTime = new Date(Date.now() + 2 * 60 * 1000);
                await auctionRepo.extendTime(auctionId, newEndTime);
            }
            await this.broadcastBid(auctionId, bid, auction);
            return { success: true, bid };
        } finally {
            await redis.del(lockKey);
        }
    }

    async broadcastBid(auctionId: string, bid: Bid, auction: Auction) {
        io.to(`auction:${auctionId}`).emit('bid:new', {
            bidId: bid.id,
            amount: bid.amount,
            bidderId: bid.userId,
            bidderName: anonymizeBidder(bid.userId),
            timestamp: bid.createdAt,
            totalBids: auction.bidCount + 1,
            newEndTime: auction.endsAt
        });
    }
}

Чому антиснайпінг критичний для аукціону?

Без нього будь-який учасник може дочекатися останніх мілісекунд і виграти без конкуренції. Наш таймер автоматично продовжується на 2 хвилини, даючи шанс відповісти. Сервер транслює оновлений час усім клієнтам через WebSocket.

class AuctionTimer {
  async startTimer(auctionId: string, endTime: Date): Promise<void> {
    const msUntilEnd = endTime.getTime() - Date.now();
    setTimeout(async () => {
      await this.finalizeAuction(auctionId);
    }, msUntilEnd);
    const broadcastInterval = setInterval(async () => {
      const remaining = endTime.getTime() - Date.now();
      if (remaining <= 0) {
        clearInterval(broadcastInterval);
        return;
      }
      if (remaining <= 60000) {
        io.to(`auction:${auctionId}`).emit('timer:tick', { remaining: Math.ceil(remaining / 1000) });
      }
    }, 1000);
  }

  async finalizeAuction(auctionId: string): Promise<void> {
    const auction = await auctionRepo.findById(auctionId);
    if (auction.status !== 'active') return;
    await auctionRepo.finalize(auctionId);
    io.to(`auction:${auctionId}`).emit('auction:ended', {
      winnerId: auction.currentLeaderId,
      winnerName: await getUserName(auction.currentLeaderId),
      finalPrice: auction.currentPrice
    });
    await this.notifyParticipants(auction);
  }
}

Клієнтський компонент на React

Усі дані надходять по WebSocket — компонент реагує миттєво. Жодних зайвих re-render, лише точкові оновлення.

function AuctionRoom({ auctionId }) {
  const [auction, setAuction] = useState<AuctionState>();
  const [bids, setBids] = useState<Bid[]>([]);
  const [timeLeft, setTimeLeft] = useState<number>(0);
  const socket = useSocket();

  useEffect(() => {
    if (!socket) return;

    socket.emit('auction:join', { auctionId });
    socket.on('auction:state', (state) => setAuction(state));
    socket.on('bid:new', (bid) => {
      setBids(prev => [bid, ...prev].slice(0, 50));
      setAuction(prev => prev ? { ...prev, currentPrice: bid.amount } : prev);
    });
    socket.on('timer:tick', ({ remaining }) => setTimeLeft(remaining));
    socket.on('auction:ended', ({ winnerId, finalPrice }) => {
      setAuction(prev => prev ? { ...prev, status: 'ended' } : prev);
      if (winnerId === currentUserId) {
        showCongratulations(finalPrice);
      }
    });

    return () => socket.emit('auction:leave', { auctionId });
  }, [socket, auctionId]);

  const placeBid = async (amount: number) => {
    try {
      await fetch(`/api/auctions/${auctionId}/bids`, {
        method: 'POST',
        body: JSON.stringify({ amount })
      });
    } catch (e) {
      showError(e.message);
    }
  };

  return (
    <div className="auction-room">
      <CurrentPrice price={auction?.currentPrice} />
      <AuctionTimer seconds={timeLeft} critical={timeLeft < 30} />
      <BidForm
        minBid={(auction?.currentPrice ?? 0) + (auction?.minIncrement ?? 100)}
        onBid={placeBid}
        disabled={auction?.status !== 'active'}
      />
      <BidHistory bids={bids} currentUserId={currentUserId} />
    </div>
  );
}

Процес роботи

  1. Аналітика — дивимося на бізнес-логіку, визначаємо типи аукціонів (англійський, голландський, закритий), навантаження, інтеграції з платіжними системами.
  2. Проектування — малюємо архітектуру: схему БД, протокол WebSocket, стратегію блокувань, anti-sniping, таймери.
  3. Реалізація — пишемо сервер і клієнт паралельно. На кожен спринт — демо.
  4. Тестування — unit-тести, інтеграційні тести, навантажувальне тестування (до 2000 RPS). Перевіряємо race condition і таймінги.
  5. Деплой — налаштовуємо CI/CD, контейнеризацію, моніторинг (Grafana, Prometheus). Передаємо документацію та доступи.

Що входить в роботу

  • Повна кодова база сервера (Node.js + TypeScript) та клієнта (React/Next.js).
  • Документація API (Swagger) та архітектурна схема.
  • Навантажувальне тестування зі звітом.
  • Інструкція з розгортання та налаштування моніторингу.
  • Один місяць підтримки після запуску (виправлення багів, консультації).

Строки орієнтовно

Комплектація Строк
Базовий аукціон: ставки, таймер, історія 2–3 тижні
+ Антиснайпінг, сповіщення, email-розсилки +1 тиждень
+ Мультилот, інтеграція з платіжними системами, особистий кабінет 4–6 тижнів

Точні строки оцінюємо після аналізу вашого ТЗ. Зв'яжіться з нами — надішлемо оцінку за 1 день.

Типові помилки при розробці аукціонів

Помилка Наслідок Наше рішення
Відсутність блокування ставок Race condition, подвійні ставки Песимістичне блокування Redis
Реалізація таймера лише на клієнті Розсинхронізація часу Серверний таймер з broadcast
Немає anti-sniping Лоти йдуть за мінімальною ціною Автоматичне продовження на 2 хв
Ігнорування затримок мережі Запізнілі ставки WebSocket з підтвердженням

«Після впровадження системи кількість спірних ставок знизилася до нуля» — клієнт з e-commerce.

Приклад конфлікту ставок та його вирішення

Одного разу на аукціоні два учасники одночасно подали ставки по $1000. Без блокування обидва запити пройшли, і система зафіксувала дві ставки. Після впровадження нашого блокування другий запит отримує помилку і клієнт повторює ставку з більшою сумою. У результаті — чесна перемога першого.

Уникнути цих граблів допомагає наш досвід — 7 років у розробці real-time систем, понад 30 завершених проєктів для e-commerce та аукціонів. Гарантуємо стабільність та масштабованість.

Якщо ви хочете таку саму систему, замовте консультацію — обговоримо деталі та приступимо. Отримайте консультацію — розкажіть про ваш аукціон, і ми запропонуємо оптимальне рішення.

Наші компетенції:

Останні роботи

  • Розробка сайту компанії B2B ADVANCE
    Розробка сайту компанії B2B ADVANCE
    1542
  • Розробка веб-додатків для компанії FEEDME
    Розробка веб-додатків для компанії FEEDME
    1394
  • Розробка веб-сайту для компанії БЕЛФІНГРУП
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    1081
  • Розробка інтернет магазину для компанії FURNORO
    Розробка інтернет магазину для компанії FURNORO
    1369
  • Розробка веб-додатків для компанії Enviok
    Розробка веб-додатків для компанії Enviok
    1142
  • Розробка веб-сайту для компанії ФІКСПЕР
    Розробка веб-сайту для компанії ФІКСПЕР
    1060