// Авто-публикация статей в каналы. // // Логика: // 1. При сохранении статьи со status='published' — engine вызывает scheduleForArticle(articleId) // 2. Находим все системные каналы с auto_publish_enabled=true где (categories пустой ИЛИ категория статьи там есть) // 3. Для каждого канала ищем ближайший подходящий момент: // - если delay_min > 0 → now + delay_min // - иначе — ближайший publish_slot канала в будущем // - если у канала нет слотов и delay=0 — публикуем сразу (scheduled_at = NOW) // 4. Дедуп: один article × один channel = одна запись в scheduled_posts (skip если уже есть pending/sent) // 5. Создаём scheduled_posts с pending status — runner отработает по cron'у const { query } = require('../config/db'); /** * Подобрать ближайший момент публикации для канала. * @returns Date */ async function pickScheduleTime(channel) { const now = new Date(); if (channel.auto_publish_delay_min > 0) { return new Date(now.getTime() + channel.auto_publish_delay_min * 60_000); } // Ищем publish_slots const { rows: slots } = await query( `SELECT slot_hour, slot_minute FROM publish_slots WHERE channel_id=$1 AND enabled=true ORDER BY slot_hour, slot_minute`, [channel.id] ); if (slots.length === 0) { return now; // публикуем сразу } // Получаем уже занятые слоты (pending) const { rows: taken } = await query( `SELECT scheduled_at FROM scheduled_posts WHERE channel_id=$1 AND status='pending'`, [channel.id] ); const takenTimes = new Set(taken.map(r => new Date(r.scheduled_at).getTime())); // Ищем ближайший незанятый слот на ближайшие 7 дней for (let dayOffset = 0; dayOffset <= 7; dayOffset++) { const base = new Date(now); base.setDate(base.getDate() + dayOffset); for (const s of slots) { const t = new Date(base); t.setHours(s.slot_hour, s.slot_minute, 0, 0); if (t > now && !takenTimes.has(t.getTime())) { return t; } } } // Fallback — завтра первый слот const tomorrow = new Date(now); tomorrow.setDate(tomorrow.getDate() + 1); tomorrow.setHours(slots[0].slot_hour, slots[0].slot_minute, 0, 0); return tomorrow; } /** * Поставить статью на авто-публикацию во все подходящие каналы. * Идемпотентно: дубли в pending/sent не создаются. * @returns массив созданных scheduled_posts */ async function scheduleForArticle(articleId) { const { rows: arts } = await query( `SELECT id, slug, title, category, status FROM articles WHERE id=$1`, [articleId] ); if (!arts.length || arts[0].status !== 'published') return []; const article = arts[0]; const { rows: channels } = await query( `SELECT * FROM channels WHERE is_system=true AND is_active=true AND auto_publish_enabled=true AND (cardinality(auto_publish_categories) = 0 OR $1 = ANY(auto_publish_categories))`, [article.category] ); const created = []; for (const ch of channels) { // Дедуп const { rows: existing } = await query( `SELECT id FROM scheduled_posts WHERE channel_id=$1 AND article_id=$2 AND status IN ('pending','sent') LIMIT 1`, [ch.id, article.id] ); if (existing.length) continue; const scheduledAt = await pickScheduleTime(ch); const { rows: inserted } = await query( `INSERT INTO scheduled_posts (channel_id, article_id, scheduled_at, status) VALUES ($1,$2,$3,'pending') RETURNING *`, [ch.id, article.id, scheduledAt] ); created.push(inserted[0]); console.log(`[auto-publish] article=${article.id} → channel=${ch.id} at ${scheduledAt.toISOString()}`); } return created; } module.exports = { scheduleForArticle, pickScheduleTime };