Реализация массовой отправки документов на подписание на сайте
Массовая отправка документов на подписание — это сценарий, когда одна компания отправляет сотни или тысячи персонализированных документов получателям: трудовые договоры при массовом найме, акты выполненных работ фрилансерам, согласия на обработку данных пользователям.
Архитектура системы
Массовая рассылка не выполняется синхронно — это фоновая задача с очередью и прогресс-трекингом:
Пользователь загружает 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 дней.







