Files
zeropost-engine/src/services/articleAutoPublish.js
T

109 lines
4.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Авто-публикация статей в каналы.
//
// Логика:
// 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 };