Реализация массовой отправки документов на подписание на сайте

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

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

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация массовой отправки документов на подписание на сайте
Средняя
~3-5 рабочих дней
Часто задаваемые вопросы

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

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Реализация массовой отправки документов на подписание на сайте

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

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

Массовая рассылка не выполняется синхронно — это фоновая задача с очередью и прогресс-трекингом:

Пользователь загружает CSV/Excel со списком получателей
  ↓
Валидация данных (email, ФИО, обязательные поля шаблона)
  ↓
Создание batch записи в БД
  ↓
Job queue: генерация персональных документов
  ├── Подстановка данных в шаблон
  ├── Генерация PDF
  ├── Создание уникальной ссылки на подписание
  └── Отправка email/SMS
  ↓
Мониторинг: кто открыл, кто подписал, кто проигнорировал

Модель данных

CREATE TABLE signing_batches (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name            VARCHAR(500),
  template_id     UUID REFERENCES document_templates(id),
  initiated_by    UUID REFERENCES users(id),
  total_count     INT NOT NULL,
  sent_count      INT DEFAULT 0,
  signed_count    INT DEFAULT 0,
  failed_count    INT DEFAULT 0,
  status          VARCHAR(50) DEFAULT 'pending',
  -- pending → processing → completed / partially_failed
  deadline_at     TIMESTAMPTZ,
  created_at      TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE signing_requests (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  batch_id        UUID REFERENCES signing_batches(id),
  recipient_email VARCHAR(500) NOT NULL,
  recipient_name  VARCHAR(500),
  recipient_phone VARCHAR(50),
  template_data   JSONB NOT NULL,     -- Данные для подстановки в шаблон
  document_id     UUID REFERENCES documents(id),
  signing_token   UUID UNIQUE DEFAULT gen_random_uuid(), -- Уникальный токен для ссылки
  status          VARCHAR(50) DEFAULT 'pending',
  -- pending, generating, sent, opened, signed, declined, expired
  sent_at         TIMESTAMPTZ,
  opened_at       TIMESTAMPTZ,
  signed_at       TIMESTAMPTZ,
  reminder_count  INT DEFAULT 0,
  expires_at      TIMESTAMPTZ,
  error_message   TEXT
);

Загрузка и валидация CSV

// Парсинг и валидация файла получателей
async function processBatchUpload(file: Express.Multer.File, templateId: string) {
  const records = await parseCSV(file.buffer, { headers: true });
  const template = await db.documentTemplates.findByPk(templateId);
  const requiredFields = extractTemplateVariables(template.content);

  const errors: ValidationError[] = [];
  const validRows: RecipientRow[] = [];

  records.forEach((row, index) => {
    const rowErrors = [];

    if (!row.email || !isValidEmail(row.email)) {
      rowErrors.push(`Строка ${index + 2}: некорректный email`);
    }

    for (const field of requiredFields) {
      if (!row[field]) {
        rowErrors.push(`Строка ${index + 2}: отсутствует поле "${field}"`);
      }
    }

    if (rowErrors.length > 0) {
      errors.push(...rowErrors);
    } else {
      validRows.push(row);
    }
  });

  return { valid: validRows, errors, totalRows: records.length };
}

Обработка очереди

// BullMQ worker: обрабатывает задачи из очереди
const signingWorker = new Worker('document-signing', async (job) => {
  const { requestId } = job.data;
  const request = await db.signingRequests.findByPk(requestId);

  try {
    await db.signingRequests.update(requestId, { status: 'generating' });

    // 1. Генерируем персональный документ
    const pdfBytes = await documentGenerator.generate(
      request.template,
      request.templateData
    );

    // 2. Сохраняем документ
    const document = await documentStorage.store(pdfBytes, {
      batchId: request.batchId,
      requestId: request.id,
    });

    // 3. Создаём ссылку на подписание
    const signingUrl = `${process.env.APP_URL}/sign/${request.signingToken}`;

    // 4. Отправляем уведомление
    await emailService.send({
      to: request.recipientEmail,
      subject: 'Документ ожидает вашей подписи',
      template: 'signing-invitation',
      data: {
        recipientName: request.recipientName,
        documentName: request.template.name,
        signingUrl,
        expiresAt: request.expiresAt,
      },
    });

    await db.signingRequests.update(requestId, {
      status: 'sent',
      documentId: document.id,
      sentAt: new Date(),
    });

    // Обновляем счётчик batch
    await db.signingBatches.increment(request.batchId, 'sent_count');
  } catch (error) {
    await db.signingRequests.update(requestId, {
      status: 'failed',
      errorMessage: error.message,
    });
    await db.signingBatches.increment(request.batchId, 'failed_count');
  }
}, {
  concurrency: 10, // Параллельная обработка
  connection: redisConnection,
});

Страница подписания (без авторизации)

Получатель переходит по ссылке https://app.example.com/sign/{token} — авторизация не нужна, доступ только по токену:

app.get('/sign/:token', async (req, res) => {
  const request = await db.signingRequests.findOne({
    signingToken: req.params.token,
    status: { not: ['expired', 'signed', 'declined'] },
  });

  if (!request) return res.redirect('/sign/invalid');
  if (request.expiresAt < new Date()) {
    await db.signingRequests.update(request.id, { status: 'expired' });
    return res.redirect('/sign/expired');
  }

  // Фиксируем открытие
  if (!request.openedAt) {
    await db.signingRequests.update(request.id, { openedAt: new Date() });
  }

  res.render('signing-page', { request, document: request.document });
});

Напоминания

// Cron: ежедневная отправка напоминаний
async function sendSigningReminders() {
  const pending = await db.signingRequests.findAll({
    status: 'sent',
    reminderCount: { lt: 3 },
    sentAt: { lt: subDays(new Date(), 2) }, // Напоминание через 2 дня
    expiresAt: { gt: new Date() },
  });

  for (const request of pending) {
    const lastReminderAt = request.lastReminderAt || request.sentAt;
    const daysSinceLastReminder = differenceInDays(new Date(), lastReminderAt);

    if (daysSinceLastReminder >= 2) {
      await emailService.sendReminder(request);
      await db.signingRequests.update(request.id, {
        reminderCount: request.reminderCount + 1,
        lastReminderAt: new Date(),
      });
    }
  }
}

Дашборд мониторинга batch

Прогресс bar: отправлено/подписано/не открыто/просрочено. Таблица с фильтрами по статусу. Экспорт в CSV для кофигурации получателей и статусов. Кнопка «Отправить напоминание» для всех незакрытых.

Ограничения скорости рассылки

Email-провайдеры имеют лимиты. Для batch из 10K документов рассылка идёт со скоростью 100–200 писем в минуту через очередь с throttling. При использовании Resend — rate limit 100 req/s, Postmark — 100 req/s.

Сроки

Загрузка CSV, валидация, создание batch и queue-based генерация PDF + отправка — 7–10 дней. Страница подписания по токену, напоминания, мониторинг dashboard — 5–7 дней.