feat: topic bank + channel limit + onboarding

Topic bank (P6):
- DB: channel_topics(channel_id, topic, is_used)
- services/topicBank.js: nextTopic, refillManual, addManual, listTopics, checkAndRefill
  Авто-пополнение когда <5 тем, пачками по 10 через Claude Haiku
- routes/generate.js: GET/POST /topics-bank/:channelId, /refill, /add, DELETE /item/:id

Channel limit (P7):
- routes/channels.js: POST / → проверяет billing.getBalance().channelsMax перед созданием
  HTTP 402 + CHANNEL_LIMIT_REACHED если лимит исчерпан
- channels/new/page.js: при 402 → ошибка + redirect на /plans через 2 сек

ENGINE_URL fix: 3040 → 3030 (lib/engine.js)
This commit is contained in:
Ник (Claude)
2026-06-11 23:04:45 +03:00
parent 10c138aa33
commit bbae6c8832
3 changed files with 230 additions and 0 deletions
+17
View File
@@ -250,6 +250,23 @@ router.post('/', async (req, res) => {
const userId = getUserId(req);
if (!userId) return res.status(401).json({ error: 'x-user-id required' });
try {
// Проверяем лимит каналов по тарифу
const billing = require('../services/billing');
const bal = await billing.getBalance(userId);
if (bal.channelsMax !== -1) {
const { rows: [{ cnt }] } = await require('../config/db').query(
'SELECT count(*)::int as cnt FROM channels WHERE user_id=$1 AND is_active=true', [userId]
);
if (cnt >= bal.channelsMax) {
return res.status(402).json({
error: `Лимит каналов по тарифу ${bal.planName}: максимум ${bal.channelsMax}. Перейдите на следующий тариф.`,
code: 'CHANNEL_LIMIT_REACHED',
current: cnt,
max: bal.channelsMax,
plan: bal.plan,
});
}
}
const channel = await channelsSvc.createChannel(userId, req.body);
res.json(channel);
} catch (err) {
+46
View File
@@ -168,3 +168,49 @@ router.post('/from-url', async (req, res) => {
});
module.exports = router;
// ── Topic Bank API ──────────────────────────────────────────
const topicBank = require('../services/topicBank');
// GET /api/generate/topics-bank/:channelId — список тем из банка
router.get('/topics-bank/:channelId', async (req, res) => {
try {
const topics = await topicBank.listTopics(req.params.channelId, {
limit: 30,
includeUsed: req.query.includeUsed === 'true',
});
const { rows: [{ cnt }] } = await require('../config/db').query(
'SELECT count(*)::int as cnt FROM channel_topics WHERE channel_id=$1 AND is_used=false',
[req.params.channelId]
);
res.json({ topics, total_unused: cnt });
} catch (err) { res.status(500).json({ error: err.message }); }
});
// POST /api/generate/topics-bank/:channelId/refill — пополнить банк вручную
router.post('/topics-bank/:channelId/refill', async (req, res) => {
try {
const saved = await topicBank.refillManual(req.params.channelId);
res.json({ ok: true, added: saved.length, topics: saved });
} catch (err) { res.status(500).json({ error: err.message }); }
});
// POST /api/generate/topics-bank/:channelId/add — добавить темы вручную
router.post('/topics-bank/:channelId/add', async (req, res) => {
try {
const { topics = [] } = req.body;
const saved = await topicBank.addManual(req.params.channelId, topics);
res.json({ ok: true, added: saved.length });
} catch (err) { res.status(500).json({ error: err.message }); }
});
// DELETE /api/generate/topics-bank/:id — удалить тему
router.delete('/topics-bank/item/:id', async (req, res) => {
try {
await require('../config/db').query('DELETE FROM channel_topics WHERE id=$1', [req.params.id]);
res.json({ ok: true });
} catch (err) { res.status(500).json({ error: err.message }); }
});
module.exports = router;