forked from admin/zeropost-engine
116f15bf21
- БД: таблица series (slug, title, intro, icon, color, article_ids JSONB, is_featured, sort_order) - routes/series.js: CRUD серий, GET /:slug возвращает серию вместе со статьями (через JOIN по array_position для сохранения порядка) - Индекс idx_series_slug
194 lines
8.2 KiB
JavaScript
194 lines
8.2 KiB
JavaScript
const { Pool } = require('pg');
|
|
const config = require('../config');
|
|
|
|
const pool = new Pool(config.db);
|
|
|
|
pool.on('error', (err) => {
|
|
console.error('[DB] Unexpected error on idle client', err);
|
|
});
|
|
|
|
const query = (text, params) => pool.query(text, params);
|
|
|
|
const migrate = async () => {
|
|
// users — расширили под план/api_key/баланс
|
|
await query(`
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id SERIAL PRIMARY KEY,
|
|
email VARCHAR(255) UNIQUE NOT NULL,
|
|
password TEXT NOT NULL,
|
|
name VARCHAR(255),
|
|
plan VARCHAR(20) DEFAULT 'free',
|
|
api_key VARCHAR(64) UNIQUE,
|
|
tokens_used BIGINT DEFAULT 0,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
`);
|
|
|
|
// channels — базовые настройки канала
|
|
await query(`
|
|
CREATE TABLE IF NOT EXISTS channels (
|
|
id SERIAL PRIMARY KEY,
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
name VARCHAR(255) NOT NULL,
|
|
tg_channel_id VARCHAR(255),
|
|
tg_username VARCHAR(255),
|
|
bot_token TEXT,
|
|
niche TEXT, -- узкая тематика
|
|
audience TEXT, -- описание ЦА
|
|
goal VARCHAR(50) DEFAULT 'educational', -- educational/news/entertainment/expert/sales
|
|
language VARCHAR(10) DEFAULT 'ru',
|
|
region VARCHAR(50) DEFAULT 'ru', -- ru/cis/west
|
|
is_active BOOLEAN DEFAULT true,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
`);
|
|
|
|
// channel_style — стилевые настройки (1:1 с channels)
|
|
await query(`
|
|
CREATE TABLE IF NOT EXISTS channel_style (
|
|
channel_id INTEGER PRIMARY KEY REFERENCES channels(id) ON DELETE CASCADE,
|
|
tone VARCHAR(50) DEFAULT 'friendly', -- friendly/serious/ironic/provocative/academic/custom
|
|
tone_custom TEXT, -- если tone='custom'
|
|
formality VARCHAR(20) DEFAULT 'informal', -- formal(вы)/informal(ты)
|
|
humor VARCHAR(20) DEFAULT 'moderate', -- none/dry/moderate/playful
|
|
post_length VARCHAR(20) DEFAULT 'medium', -- short(<300)/medium(300-800)/long(800-2000)
|
|
structure VARCHAR(50) DEFAULT 'mixed', -- plain/lists/headers/mixed
|
|
emoji_level VARCHAR(20) DEFAULT 'moderate', -- none/moderate/active
|
|
hashtags_mode VARCHAR(20) DEFAULT 'end', -- none/end/inline
|
|
cta_mode VARCHAR(20) DEFAULT 'sometimes', -- always/sometimes/never
|
|
example_posts JSONB DEFAULT '[]'::jsonb, -- массив строк-эталонов (few-shot)
|
|
banned_words JSONB DEFAULT '[]'::jsonb, -- стоп-слова
|
|
banned_topics JSONB DEFAULT '[]'::jsonb, -- запрещённые темы
|
|
expertise JSONB DEFAULT '[]'::jsonb, -- темы где автор силён
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
`);
|
|
|
|
// channel_schedule — расписание и рубрики
|
|
await query(`
|
|
CREATE TABLE IF NOT EXISTS channel_schedule (
|
|
channel_id INTEGER PRIMARY KEY REFERENCES channels(id) ON DELETE CASCADE,
|
|
posts_per_day INTEGER DEFAULT 1,
|
|
time_slots JSONB DEFAULT '[]'::jsonb, -- ["09:00","18:00"]
|
|
timezone VARCHAR(50) DEFAULT 'Europe/Moscow',
|
|
rubrics JSONB DEFAULT '[]'::jsonb, -- [{name,description,days:[1,3,5]}]
|
|
sources JSONB DEFAULT '[]'::jsonb, -- [{type:'rss',url:'...'},{type:'tg',channel:'@...'}]
|
|
auto_publish BOOLEAN DEFAULT false, -- авто-постинг без подтверждения
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
`);
|
|
|
|
// generation_jobs — задачи генерации
|
|
await query(`
|
|
CREATE TABLE IF NOT EXISTS generation_jobs (
|
|
id SERIAL PRIMARY KEY,
|
|
user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
|
channel_id INTEGER REFERENCES channels(id) ON DELETE SET NULL,
|
|
type VARCHAR(50) NOT NULL, -- post/article/topics/rewrite
|
|
topic TEXT,
|
|
rubric VARCHAR(255),
|
|
prompt_debug TEXT, -- финальный промпт для отладки
|
|
result TEXT,
|
|
tokens_in INTEGER,
|
|
tokens_out INTEGER,
|
|
cost_cents INTEGER DEFAULT 0,
|
|
status VARCHAR(20) DEFAULT 'pending',
|
|
error TEXT,
|
|
metadata JSONB DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
`);
|
|
|
|
// posts — готовые посты (черновики и опубликованные)
|
|
await query(`
|
|
CREATE TABLE IF NOT EXISTS posts (
|
|
id SERIAL PRIMARY KEY,
|
|
channel_id INTEGER REFERENCES channels(id) ON DELETE CASCADE,
|
|
job_id INTEGER REFERENCES generation_jobs(id) ON DELETE SET NULL,
|
|
content TEXT NOT NULL,
|
|
image_url TEXT,
|
|
status VARCHAR(20) DEFAULT 'draft', -- draft/scheduled/published/failed
|
|
scheduled_at TIMESTAMPTZ,
|
|
published_at TIMESTAMPTZ,
|
|
tg_message_id BIGINT,
|
|
metadata JSONB DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
`);
|
|
|
|
// articles — статьи для публичного блога zeropost.ru
|
|
await query(`
|
|
CREATE TABLE IF NOT EXISTS articles (
|
|
id SERIAL PRIMARY KEY,
|
|
slug VARCHAR(255) UNIQUE NOT NULL,
|
|
title VARCHAR(500) NOT NULL,
|
|
excerpt TEXT,
|
|
content TEXT NOT NULL,
|
|
cover_url TEXT,
|
|
tags JSONB DEFAULT '[]'::jsonb,
|
|
author VARCHAR(100) DEFAULT 'ZeroPost AI',
|
|
reading_time INTEGER,
|
|
status VARCHAR(20) DEFAULT 'published',
|
|
views INTEGER DEFAULT 0,
|
|
seo_title VARCHAR(500),
|
|
seo_descr TEXT,
|
|
job_id INTEGER REFERENCES generation_jobs(id) ON DELETE SET NULL,
|
|
published_at TIMESTAMPTZ DEFAULT NOW(),
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
`);
|
|
|
|
// editor_notes — короткие заметки от редактора-человека
|
|
await query(`
|
|
CREATE TABLE IF NOT EXISTS editor_notes (
|
|
id SERIAL PRIMARY KEY,
|
|
title VARCHAR(255),
|
|
content TEXT NOT NULL,
|
|
author VARCHAR(100) DEFAULT 'Редактор',
|
|
tags JSONB DEFAULT '[]'::jsonb,
|
|
is_pinned BOOLEAN DEFAULT false,
|
|
is_published BOOLEAN DEFAULT true,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
`);
|
|
|
|
// series — тематические серии статей
|
|
await query(`
|
|
CREATE TABLE IF NOT EXISTS series (
|
|
id SERIAL PRIMARY KEY,
|
|
slug VARCHAR(120) UNIQUE NOT NULL,
|
|
title VARCHAR(255) NOT NULL,
|
|
intro TEXT,
|
|
icon VARCHAR(40),
|
|
color VARCHAR(20) DEFAULT 'emerald',
|
|
article_ids JSONB DEFAULT '[]'::jsonb,
|
|
is_featured BOOLEAN DEFAULT false,
|
|
sort_order INTEGER DEFAULT 0,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
`);
|
|
|
|
// индексы
|
|
await query(`
|
|
CREATE INDEX IF NOT EXISTS idx_channels_user ON channels(user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_posts_channel ON posts(channel_id);
|
|
CREATE INDEX IF NOT EXISTS idx_posts_scheduled ON posts(scheduled_at) WHERE status='scheduled';
|
|
CREATE INDEX IF NOT EXISTS idx_jobs_status ON generation_jobs(status);
|
|
CREATE INDEX IF NOT EXISTS idx_jobs_user ON generation_jobs(user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_articles_slug ON articles(slug);
|
|
CREATE INDEX IF NOT EXISTS idx_articles_status_pub ON articles(status, published_at DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_notes_pub ON editor_notes(is_published, created_at DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_series_slug ON series(slug);
|
|
`);
|
|
|
|
console.log('[DB] Migrations applied');
|
|
};
|
|
|
|
module.exports = { query, migrate };
|