diff --git a/src/services/autogen.js b/src/services/autogen.js index 35e9d73..6631544 100644 --- a/src/services/autogen.js +++ b/src/services/autogen.js @@ -70,24 +70,33 @@ async function getNextTopic(category) { return { id: rows[0].id, topic: rows[0].topic, tags: rows[0].tags || [], keywords: rows[0].keywords || [] }; } - // 2. DB-банк тем — неиспользованные + // 2. DB-банк тем — атомарно захватываем следующую свободную тему. + // FOR UPDATE SKIP LOCKED + немедленный UPDATE is_used=true устраняет race condition: + // параллельные генерации не могут выбрать одну и ту же тему. const { rows: dbTopics } = await query(` - SELECT bt.id, bt.topic FROM blog_topics bt - WHERE bt.category = $1 - AND bt.is_used = false - AND NOT EXISTS ( - SELECT 1 FROM articles a - WHERE a.source_topic = bt.topic AND a.category = $1 - ) - ORDER BY bt.priority DESC, bt.created_at ASC - LIMIT 1 + UPDATE blog_topics + SET is_used=true, used_at=NOW() + WHERE id = ( + SELECT bt.id FROM blog_topics bt + WHERE bt.category = $1 + AND bt.is_used = false + AND NOT EXISTS ( + SELECT 1 FROM articles a + WHERE a.source_topic = bt.topic AND a.category = $1 + ) + ORDER BY bt.priority DESC, bt.created_at ASC + LIMIT 1 + FOR UPDATE SKIP LOCKED + ) + RETURNING id, topic `, [category]); if (dbTopics.length) { return { id: null, topic: dbTopics[0].topic, tags: [], keywords: [], blog_topic_id: dbTopics[0].id }; } - // 3. Fallback: хардкод если DB пустой + // 3. Fallback: хардкод если DB-банк пустой или все темы использованы. + // Проверяем использованные темы (всё время) чтобы не повторяться. const bank = TOPIC_BANK[category] || TOPIC_BANK['ai-tools']; const { rows: usedTopics } = await query( `SELECT source_topic FROM articles WHERE category=$1 AND source_topic IS NOT NULL`, @@ -95,8 +104,11 @@ async function getNextTopic(category) { ); const usedSet = new Set(usedTopics.map(r => r.source_topic?.toLowerCase().trim()).filter(Boolean)); const unused = bank.filter(t => !usedSet.has(t.toLowerCase().trim())); + // Если все темы уже использованы — берём рандомную (лучше повтор чем пустой контент) const pool = unused.length > 0 ? unused : bank; - const topic = pool[Math.floor(Math.random() * pool.length)]; + // Перемешиваем и берём первую (вместо случайного — детерминированно для одного запуска) + const shuffled = [...pool].sort(() => Math.random() - 0.5); + const topic = shuffled[0]; return { id: null, topic, tags: [], keywords: [] }; }