Массовая отправка документов на подписание: реализация с трекингом и напоминаниями
При слиянии двух компаний нужно разослать 5000 трудовых договоров за 48 часов. Ручная отправка потребует 10 сотрудников на неделю — с опечатками, потерянными письмами и нервотрёпкой. Мы автоматизируем этот процесс: загрузка CSV, генерация персонализированных PDF, отправка через очередь задач, отслеживание статусов и автоматические напоминания. Ниже — архитектура, которая выдержит нагрузку 10K+ документов без падения.
Какие проблемы решаем?
Генерация тысяч документов без подвисания сервера. Синхронная генерация убьёт сервер — используем очередь задач на BullMQ. Каждый документ генерируется отдельным воркером, параллельность до 10. Даже при пике в 5000 документов система остаётся отзывчивой.
Rate limiting email-провайдеров. Resend даёт 100 писем в секунду, Postmark — столько же. Для batch в 10K писем мы внедряем throttling в очереди: отправка идёт со скоростью 100–200 писем в минуту, чтобы не попасть в блокировку. Таймауты и повторные попытки — обязательны.
Отсутствие подписания у части получателей. Автоматические напоминания через 2 и 4 дня. Настраиваемое количество (до 3). Дашборд показывает, кто не открыл письмо, и позволяет отправить напоминание вручную.
Как мы это делаем?
Типовой стек: PostgreSQL + Redis + BullMQ + Node.js (NestJS) + Puppeteer для PDF. Воркеры развёрнуты в Docker на отдельном инстансе. Код хранится в GitLab CI, деплой в AWS ECS.
Кейс: для маркетплейса внедрили массовую отправку актов выполненных работ. Ежемесячно 15 000 актов. Ранее бухгалтерия тратила 3 дня на рассылку через Outlook. После внедрения — 30 минут. Ошибки сократились с 5% до 0.2%.
Процесс работы
- Аналитика. Собираем требования: объём, шаблоны документов, интеграции (CRM, бухгалтерия).
- Проектирование. Схема БД, очередь задач, API, страница подписания.
- Реализация. Разработка модулей: загрузка CSV, генерация PDF, отправка, дашборд.
- Тестирование. Нагрузочное тестирование с 10K документов, сценарии ошибок.
- Деплой. На сервер клиента или облако. Настройка CI/CD, мониторинг (Sentinel, Grafana).
Почему очередь задач лучше синхронной генерации?
Синхронная генерация 5000 PDF вызовет тайм-аут сервера. Очередь BullMQ с воркерами (параллельность 10) обрабатывает документы постепенно, не блокируя запросы. Redis хранит прогресс. Если воркер упал — задача автоматически перезапускается. Это даёт надёжность и масштабируемость.
Как работает очередь в коде?
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' });
const pdfBytes = await documentGenerator.generate(
request.template, request.templateData
);
const document = await documentStorage.store(pdfBytes, {
batchId: request.batchId,
requestId: request.id,
});
const signingUrl = `${process.env.APP_URL}/sign/${request.signingToken}`;
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(),
});
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,
});
Ограничения и надежность
Email-провайдеры блокируют при превышении лимитов. Мы настраиваем очередь с паузой между отправками. Используем выделенные серверы отправки (SMTP-ретрансляторы) и отслеживаем статусы писем (bounce, spam). Гарантируем доставку 99%.
Автоматические напоминания
Если получатель не подписал, автоматика отправляет напоминания на 2-й и 4-й день. На 7-й день — отметка "просрочено" и уведомление менеджеру. В дашборде можно отправить повторное письмо с новой ссылкой. Настраиваемый лимит напоминаний — от 0 до 5.
async function sendSigningReminders() {
const pending = await db.signingRequests.findAll({
status: 'sent',
reminderCount: { lt: 3 },
sentAt: { lt: subDays(new Date(), 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(),
});
}
}
}
Сравнение ручной и автоматической рассылки
| Параметр | Ручная (10 сотрудников) | Автоматическая (наше решение) |
|---|---|---|
| Время на 5000 документов | 40 человеко-часов | 2 часа машинного времени |
| Ошибки (опечатки, потеря) | 5–8% | <0.5% |
| Стоимость за одну рассылку | значительные затраты | экономия |
| Трекинг статусов | Excel | Дашборд в реальном времени |
Также добавляем сравнение по скорости: автоматическая рассылка в 20 раз быстрее ручной (40 часов против 2).
Модель данных
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',
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 };
}
Страница подписания по токену
Получатель переходит по ссылке 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 });
});
Дашборд мониторинга batch
Прогресс-бар: отправлено/подписано/не открыто/просрочено. Таблица с фильтрами по статусу. Экспорт в CSV. Кнопка «Отправить напоминание» для выбранных получателей.
Ориентировочные сроки
| Этап | Сроки |
|---|---|
| Загрузка CSV, валидация, очередь + генерация PDF + отправка | 7–10 дней |
| Страница подписания по токену, напоминания, дашборд | 5–7 дней |
| Интеграция с CRM / импорт существующих шаблонов | 3–5 дней дополнительно |
Что входит в работу?
Детальный список
- Исходный код на GitHub/GitLab (закрытый репозиторий)
- Инструкция по развёртыванию (Docker + docker-compose)
- Документация по API (OpenAPI/Swagger)
- Миграции БД
- Нагрузочное тестирование (отчёт)
- Гарантия 1 месяц на баги
Свяжитесь с нами для оценки вашего проекта. Получите консультацию по автоматизации подписания документов.







