feat: draft review flow — autogen→draft, auto-approve 07:00 MSK, /api/drafts routes
This commit is contained in:
+101
-76
@@ -1,101 +1,126 @@
|
||||
/**
|
||||
* drafts.js — API для черновиков постов.
|
||||
*
|
||||
* POST /api/channels/:channelId/drafts/generate?count=3 — batch генерация
|
||||
* GET /api/drafts — все черновики юзера
|
||||
* GET /api/drafts/:channelId/channel — черновики канала
|
||||
* PATCH /api/drafts/:id — редактировать текст
|
||||
* POST /api/drafts/:id/approve — одобрить → scheduled_post
|
||||
* POST /api/drafts/:id/reject — отклонить
|
||||
* DELETE /api/drafts/:id — удалить
|
||||
*/
|
||||
// Роуты для работы с черновиками (draft review flow)
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { query } = require('../config/db');
|
||||
const channelsSvc = require('../services/channels');
|
||||
const draftSvc = require('../services/draftService');
|
||||
|
||||
function uid(req) { return req.headers['x-user-id'] ? parseInt(req.headers['x-user-id']) : null; }
|
||||
|
||||
// POST /api/channels/:channelId/drafts/generate
|
||||
router.post('/channels/:channelId/drafts/generate', async (req, res) => {
|
||||
const userId = uid(req);
|
||||
if (!userId) return res.status(401).json({ error: 'x-user-id required' });
|
||||
|
||||
const count = Math.min(parseInt(req.query.count || req.body.count || 3), 10);
|
||||
const withImage = req.body.withImage !== false;
|
||||
const { query } = require('../config/db');
|
||||
const { scheduleForArticle } = require('../services/articleAutoPublish');
|
||||
const { generateCover, COVER_STYLES } = require('../services/covers');
|
||||
|
||||
// GET /api/drafts — список черновиков
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const channel = await channelsSvc.getFullChannel(req.params.channelId);
|
||||
if (!channel) return res.status(404).json({ error: 'Channel not found' });
|
||||
|
||||
// Запускаем асинхронно — отвечаем сразу
|
||||
res.json({ ok: true, message: `Генерирую ${count} черновиков...`, count });
|
||||
|
||||
draftSvc.generateBatch(channel, { count, userId, withImage })
|
||||
.then(r => console.log(`[drafts] batch done ch=${channel.id}: ${r.generated} ok, ${r.errors.length} err`))
|
||||
.catch(e => console.error(`[drafts] batch error: ${e.message}`));
|
||||
const { rows } = await query(
|
||||
`SELECT id, slug, title, excerpt, cover_url, category, tags, reading_time, created_at
|
||||
FROM articles WHERE status='draft'
|
||||
ORDER BY created_at DESC`
|
||||
);
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
if (!res.headersSent) res.status(500).json({ error: err.message });
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/drafts — все черновики текущего пользователя
|
||||
router.get('/drafts', async (req, res) => {
|
||||
const userId = uid(req);
|
||||
if (!userId) return res.status(401).json({ error: 'x-user-id required' });
|
||||
const { status = 'pending', limit = 30, offset = 0 } = req.query;
|
||||
try {
|
||||
const result = await draftSvc.listDrafts({ userId, status, limit: parseInt(limit), offset: parseInt(offset) });
|
||||
res.json(result);
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
// GET /api/drafts/cover-styles — доступные стили обложки
|
||||
router.get('/cover-styles', (req, res) => {
|
||||
res.json(COVER_STYLES.map(s => ({ id: s.name, name: s.name })));
|
||||
});
|
||||
|
||||
// GET /api/drafts/:channelId/channel — черновики конкретного канала
|
||||
router.get('/drafts/:channelId/channel', async (req, res) => {
|
||||
const userId = uid(req);
|
||||
if (!userId) return res.status(401).json({ error: 'x-user-id required' });
|
||||
const { status = 'pending', limit = 30, offset = 0 } = req.query;
|
||||
// PATCH /api/drafts/:id — редактировать черновик (title, content, excerpt)
|
||||
router.patch('/:id', async (req, res) => {
|
||||
try {
|
||||
const result = await draftSvc.listDrafts({ channelId: req.params.channelId, status, limit: parseInt(limit), offset: parseInt(offset) });
|
||||
res.json(result);
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
const id = parseInt(req.params.id);
|
||||
const { title, excerpt, content } = req.body;
|
||||
const fields = [], vals = [];
|
||||
if (title !== undefined) { fields.push(`title=$${vals.push(title)}`); }
|
||||
if (excerpt !== undefined) { fields.push(`excerpt=$${vals.push(excerpt)}`); }
|
||||
if (content !== undefined) { fields.push(`content=$${vals.push(content)}`); }
|
||||
if (!fields.length) return res.status(400).json({ error: 'Nothing to update' });
|
||||
vals.push(id);
|
||||
const { rows } = await query(
|
||||
`UPDATE articles SET ${fields.join(',')} WHERE id=$${vals.length} AND status='draft' RETURNING id, title, slug`,
|
||||
vals
|
||||
);
|
||||
if (!rows.length) return res.status(404).json({ error: 'Draft not found' });
|
||||
res.json({ ok: true, article: rows[0] });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// PATCH /api/drafts/:id — редактировать
|
||||
router.patch('/drafts/:id', async (req, res) => {
|
||||
// PATCH /api/drafts/:id/approve — одобрить черновик вручную
|
||||
router.patch('/:id/approve', async (req, res) => {
|
||||
try {
|
||||
await draftSvc.updateDraft(req.params.id, req.body);
|
||||
res.json({ ok: true });
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
const id = parseInt(req.params.id);
|
||||
const { rows } = await query(
|
||||
`UPDATE articles SET status='published', published_at=NOW()
|
||||
WHERE id=$1 AND status='draft'
|
||||
RETURNING id, title, slug`,
|
||||
[id]
|
||||
);
|
||||
if (!rows.length) return res.status(404).json({ error: 'Draft not found' });
|
||||
|
||||
const scheduled = await scheduleForArticle(id);
|
||||
const slot = scheduled[0]?.scheduled_at;
|
||||
console.log(`[DraftApprove] manual approve article=${id} "${rows[0].title.slice(0,50)}"`);
|
||||
res.json({ ok: true, article: rows[0], scheduled_at: slot });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/drafts/:id/approve
|
||||
router.post('/drafts/:id/approve', async (req, res) => {
|
||||
const userId = uid(req);
|
||||
// POST /api/drafts/:id/regenerate-cover — перегенерировать обложку
|
||||
router.post('/:id/regenerate-cover', async (req, res) => {
|
||||
try {
|
||||
const result = await draftSvc.approveDraft(req.params.id, {
|
||||
scheduledAt: req.body.scheduled_at,
|
||||
userId,
|
||||
const id = parseInt(req.params.id);
|
||||
const { style } = req.body; // необязательно
|
||||
|
||||
// Берём статью
|
||||
const { rows: arts } = await query(
|
||||
`SELECT id, title, tags FROM articles WHERE id=$1 AND status='draft'`, [id]
|
||||
);
|
||||
if (!arts.length) return res.status(404).json({ error: 'Draft not found' });
|
||||
|
||||
// Получаем системный канал
|
||||
const { rows: chans } = await query(
|
||||
`SELECT id FROM channels WHERE is_system=true AND is_active=true LIMIT 1`
|
||||
);
|
||||
const channelId = chans[0]?.id || null;
|
||||
|
||||
// Если передан style — временно форсируем через channel_style
|
||||
if (style && channelId) {
|
||||
await query(
|
||||
`UPDATE channel_style SET image_style=$1 WHERE channel_id=$2`,
|
||||
[style, channelId]
|
||||
);
|
||||
}
|
||||
|
||||
const coverUrl = await generateCover({
|
||||
articleId: id,
|
||||
title: arts[0].title,
|
||||
tags: arts[0].tags || [],
|
||||
channelId,
|
||||
});
|
||||
res.json({ ok: true, ...result });
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
|
||||
if (coverUrl) {
|
||||
await query(`UPDATE articles SET cover_url=$1 WHERE id=$2`, [coverUrl, id]);
|
||||
}
|
||||
|
||||
console.log(`[DraftRegenCover] article=${id} cover=${coverUrl?.split('/').pop()}`);
|
||||
res.json({ ok: true, cover_url: coverUrl });
|
||||
} catch (err) {
|
||||
console.error('[DraftRegenCover] error:', err.message);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/drafts/:id/reject
|
||||
router.post('/drafts/:id/reject', async (req, res) => {
|
||||
// POST /api/drafts/approve-all — авто-одобрение всех (ручной вызов)
|
||||
router.post('/approve-all', async (req, res) => {
|
||||
try {
|
||||
await draftSvc.rejectDraft(req.params.id);
|
||||
const { runDraftAutoApprove } = require('../../draftAutoApprove');
|
||||
await runDraftAutoApprove();
|
||||
res.json({ ok: true });
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
});
|
||||
|
||||
// DELETE /api/drafts/:id
|
||||
router.delete('/drafts/:id', async (req, res) => {
|
||||
try {
|
||||
await query('DELETE FROM post_drafts WHERE id=$1', [req.params.id]);
|
||||
res.json({ ok: true });
|
||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
Reference in New Issue
Block a user