fullstack · backend-heavy · коммерческий проект

Агрегатор строительных тендеров

Внутренняя система для строительной компании: собирает тендеры из разных источников, помогает отсеивать нерелевантные заявки и запускать рассылку информации субподрядчикам.

Next.jsReactTypeScriptNode.jsNestJSPostgreSQLPrismaDockerREST APICron / Jobs
скрин проекта
Слайд 1
01 / контекст

Бизнесу нужно было не больше данных, а меньше ручного отбора

как появилась задача

Менеджеры тратили время не на оценку тендеров, а на поиск, перенос данных, первичный отсев и рассылку подрядчикам. Готовые решения не подходили: процесс был завязан на регионы, типы работ, подрядчиков, статусы и внутренние правила компании.

анализ текущего процесса

Проблема

Ручная проверка площадок, перенос заявок, оценка релевантности и отправка информации подрядчикам.

Риск

Можно было пропустить подходящий тендер, потратить время на мусорную заявку или забыть отправить данные нужному подрядчику.

Цель

Собрать заявки в одном месте, ускорить первичный отбор и сделать работу с подрядчиками управляемой.

02 / моя роль

Я отвечал за систему от backend-логики до пользовательских сценариев

формат работы

Переводил рабочий процесс в backend-логику, API и интерфейс

Разбирал, как менеджеры обрабатывают тендеры, выделял повторяемые сценарии и превращал их в сущности, статусы, API, фильтры и UI.

backend · frontend · api · data · product flows

Backend

  • модель тендера и жизненный цикл
  • сервисы импорта
  • REST API
  • фильтрация, скоринг, retry

Frontend

  • таблицы и фильтры
  • состояния загрузки и ошибок
  • пагинация и поиск
  • сценарии менеджеров

Data

  • Prisma schema
  • связи сущностей
  • индексы для поиска
  • дедупликация по source + externalId

Product flows

  • рассылка подрядчикам
  • роли пользователей
  • изменение статусов
  • повторный импорт
03 / задачи

Бизнес-проблему я разложил на конкретные технические задачи

Импорт тендеров из разных источников

Бизнес: Менеджеры вручную проверяли площадки и переносили подходящие заявки в рабочий процесс.

Техника: Источники нужно было привести к единой модели и не завязать систему на формат одной площадки.

Что сделал: Реализовал adapter-based импорт: каждый источник сам получает и нормализует данные, а сервис импорта работает только с общей моделью.

Скоринг и фильтрация заявок

Бизнес: Большая часть заявок была нерелевантной, но полностью ручная проверка занимала много времени.

Техника: Нужны были фильтры по региону, типу работ, бюджету, срокам, источнику и полноте данных.

Что сделал: Скоринг не скрывает заявки полностью, а поднимает более релевантные выше и объясняет, почему заявка получила такой балл.

Ошибки и повторный запуск

Бизнес: Если один источник недоступен, работа менеджеров не должна останавливаться.

Техника: Нужно было хранить статусы, ошибки импорта и давать возможность безопасного retry.

Что сделал: Каждый запуск импорта имеет lifecycle: pending → processing → completed / failed. Ошибка сохраняется отдельно.

Роли и доступы

Бизнес: Разные сотрудники должны были видеть и менять разные части системы.

Техника: Критичные действия нельзя было защищать только скрытием кнопок на frontend.

Что сделал: Проверки ролей вынесены в backend guards: импорт, retry, статусы, пользователи и рассылки ограничены по ролям.

04 / архитектура

Архитектура простая, но разделённая по ответственности

Архитектура системы

Backend application

NestJS-сервисы для тендеров, фильтров, статусов, пользователей и рассылок.

Import layer

Адаптеры источников, нормализация данных, дедупликация и сохранение результата.

PostgreSQL

Тендеры, источники, роли, import jobs, подрядчики и история действий.

Jobs / retry

Фоновые запуски импорта, статусы обработки и повторные попытки после ошибок.

06 / решения

Ключевые решения: как бизнес-задачи превратились в код

Проблема

У площадок разные форматы данных. Если писать импорт напрямую в бизнес-логику, система быстро станет неподдерживаемой.

Решение

Я разделил источники через адаптеры: каждый адаптер отвечает только за получение и преобразование внешних данных в единую модель тендера.

Компромисс

На первом этапе не стал делать универсальный конструктор парсеров. Явный адаптер под каждый источник был быстрее и надёжнее.

фрагмент реализации
export interface TenderSourceAdapter {
  readonly source: TenderSource;

  fetch(params: ImportParams): Promise<RawTender[]>;
  normalize(raw: RawTender): NormalizedTender;
}

@Injectable()
export class TenderImportService {
  constructor(private readonly tenders: TenderRepository) {}

  async importFrom(adapter: TenderSourceAdapter, params: ImportParams) {
    const rawTenders = await adapter.fetch(params);

    return Promise.all(
      rawTenders.map((raw) => {
        const tender = adapter.normalize(raw);

        return this.tenders.upsertByExternalId({
          externalId: tender.externalId,
          source: adapter.source,
          data: tender,
        });
      }),
    );
  }
}
07 / результат

Результаты внедрения

6+

источников подключено

1 500+

тендеров в день

~70%

меньше ручного отбора

10+ пользователей получили доступ к единому интерфейсу

Настроены роли и разграничение доступа на backend-уровне

Импорт запускается без ручного переноса данных

Добавлены повторные запуски при ошибках обработки

Тендеры проходят фильтрацию, скоринг и дедупликацию

Сформирована база для автоматизации рассылок подрядчикам

Система перевела ручную работу с тендерами в управляемый процесс

От сбора данных до фильтрации, retry и рассылок — основной цикл был автоматизирован внутри одного fullstack-приложения.

к другим проектам